agents: add --revoke-previous to triggers rotate-secret - #1922
Conversation
rotate-secret now revokes the old webhook secret immediately by default (server-side MARSOHS-1078). Add --allow-grace to opt into the bounded grace window for routine key-hygiene rotations, where in-flight deliveries signed with the old secret need to keep verifying briefly. Never use it after a secret is compromised. When --allow-grace is set the response carries previous_secret_expires_at, which is surfaced in both text and json output so operators know when the old secret dies. Co-authored-by: Cursor <cursoragent@cursor.com>
Integrate --allow-grace with the newer agents help/style layer: use agentsTriggersRotateSecretHelpMD, agentPrettyErrors(), and printWebhookSecretCard while preserving grace expiry output. Co-authored-by: Cursor <cursoragent@cursor.com>
|
LGTM |
Reverses the default this branch previously shipped. rotate-secret keeps its grace window: the provider side holds only one secret -- a GitHub webhook has a single Secret field -- so it keeps signing with the old value until a human pastes the new one in, and revoking on the call would turn every routine rotation into a delivery outage lasting as long as that handoff takes. --revoke-previous is the breach path instead. It retires the old secret at once and accepts that gap. Either way the command now states the outcome -- the expiry instant, or that the old secret is already dead -- because "rotated" alone leaves an operator unable to tell whether a leaked secret still works. Tracks the server-side contract in cthulhu (MARSOHS-1078), which is additive: ?revoke_previous=true, and previous_secret_revoked in the response. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Thanks @SSharma-10 — agreed on the vendor point, and it's now the only thing gating this PR. Heads up that the change has also changed shape since your review. After thinking through the availability side, flipping the default was the wrong call: the provider side holds only one secret, so it keeps signing with the old value until a human pastes the new one in. Immediate revocation by default would turn every routine rotation into a delivery outage lasting as long as that handoff takes. So the default stays as-is and breach response is opt-in instead. On the vendor patch: that means the godo change to land upstream is now
That keeps the vendor patch from ever reaching the base branch, which is what you were flagging. Happy to open the godo PR as soon as the contract is locked — just didn't want to land a signature upstream that the API PR might still move. |
Mirrors digitalocean/godo#1092, which takes an options pointer rather than a bare bool (consistent with the other List* calls in that file) and returns the expiry as *Timestamp. Vendor stays a patch only until that lands and we can bump the dependency. Co-authored-by: Cursor <cursoragent@cursor.com>
|
@SSharma-10 the godo side is now up: digitalocean/godo#1092, based on One thing worth a second look, because it changed from what you reviewed here. Upstream I made RotateSecret(ctx, triggerID string, opt *HostedAgentTriggerRotateSecretOptions)Two reasons. It matches how I've pushed 0714ec2 here so the vendored copy and the call site match #1092 byte for byte. Sequencing from here, as you called out: #1092 merges → bump the dependency here → drop the vendor patch. I'll do that bump as the last commit on this PR, so nothing merges to For completeness on the SDK question: pydo needs nothing. It's AutoRest-generated from digitalocean/openapi, and that spec has no hosted-agents surface at all yet — no |
|
Correction to my last comment: pydo did need a change — I was wrong. I'd only checked the generated seam (the public openapi spec still has no hosted-agents surface, so no Autorest regen is involved) and stopped there. I missed that the triggers surface in pydo is hand-authored in Fixed in digitalocean/pydo#714, based on Full set for this ticket, all additive and all defaulting to the existing grace behaviour:
|
The CLI treated a missing previous_secret_expires_at as proof the old secret was revoked. The API sets exactly one of the two fields, so the revoked flag can just be read; inferring it meant any other cause of an absent expiry would tell an operator a live secret was dead. When neither field arrives the CLI now says so rather than guessing. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Note for the latest review pass: this branch is at
The third branch is the point. The server guarantees exactly one field, so "neither" shouldn't happen — but if it does (older server, proxy dropping a field), the reading an operator would act on is the dangerous one, and guessing "revoked" on a live secret is the guess that gets someone hurt. The matching godo change is digitalocean/godo#1092; this PR needs the dependency bump once that merges. |
|
@SSharma-10 re-review request. Your earlier LGTM was on a revision whose default has since been reversed, so it's worth another pass rather than carrying that approval forward. What changed since: the flag is Blocked on godo#1092 merging first for the dependency bump. |
02273f9
into
digitalocean:feat/agents-subcommands
Summary
Replaces the
--allow-graceflag this branch previously carried. The server-side default is not changing after all, so the CLI shouldn't imply it does.rotate-secretkeeps its grace window by default: the provider side holds only one secret (a GitHub webhook has a singleSecretfield), so it keeps signing with the old value until a human pastes the new one in. Revoking on the call would turn every routine rotation into a delivery outage lasting as long as that handoff takes.--revoke-previousis the breach path instead — it retires the old secret immediately and accepts that gap.Either way the command now states the outcome: the expiry instant, or that the old secret is already dead. "Rotated" on its own leaves an operator unable to tell whether a leaked secret still works.
In
-o json, exactly one ofprevious_secret_expires_at/previous_secret_revokedis present, so scripts never infer the outcome from a missing field.Depends on
Server-side contract: cthulhu#172453 (MARSOHS-1078), which is additive —
?revoke_previous=trueplusprevious_secret_revokedin the response. The default response shape is unchanged.Testing
go build ./...,go vet, andgo test ./commands/... ./do/...pass. NewTestAgentTriggersRotateSecret_RevokePreviouscovers the breach path; the existing rotate tests now assert the expiry is surfaced on the default path.Made with Cursor