Skip to content

Revoke a session or a grant

A grant ends on its own when it expires. To end access sooner, revoke the grant, or end the session that holds it: ending a session revokes every grant it held and cancels every request it still had pending. Ending a grant stops the session reading its values again; a command already running with a value keeps it.

Revoking a grant in Dispatch ends it at once. A grant someone approved is asked for again from its approver at the session’s next request. When you revoke a grant your own session got without asking, you withhold its secrets from that session: the session’s other grants that got them without asking end with it, and its next request for them is sent to their owner for approval (to anyone signed in, for a shared secret) instead of being granted at once, so the session asks before it gets them again; your other sessions still get them at once. The withhold is the session’s, not the grant’s, so it holds even when the grant had already ended by the time you clicked Revoke (the session revoked it itself, or it expired, after the page loaded). Only the session’s own person withholds: a person who revokes a grant they approved on someone else’s session ends that grant alone. To end every session’s access to a secret, change its tags. To end every session a machine started, and stop it starting more, revoke its machine login.

Dispatch’s Settings page lists your Live grants: every live grant of a session you operate, whether it was granted automatically or approved by someone, and every grant whose approver is you, on anyone’s session — the ones you approved, and the ones the model judge approved in your stead. It lists to everyone, in the same way, every live grant the judge approved in anyone’s stead: a shared secret’s, whose approver is the sentinel anyone, so it names no person who could end it. It also lists, to everyone, every live grant that belongs to no person: one a pod or a VM got without asking, under a machine login that is a service’s rather than a person’s. Nobody operates such a session and nobody approved such a grant, so without this nobody would see what a pod had taken. Each row names its enrollment, its secrets, how it was Granted (Automatically, or Approved by whoever decided it: an email, or judge:<model id> where the model judge decided it in an approver’s stead), and when it was created and expires. Click Revoke to end one at once; the session’s next read of it is refused GRANT_NOT_LIVE. The broker allows it when you are the grant’s approver or its session’s operator, and it allows anyone signed in to end a grant whose approver is anyone. It refuses everyone else NOT_APPROVER — so a grant with no approver and no operator has no Revoke button at all. Its row reads Ends with its machine login instead: revoke the service’s machine login, which ends every session it started, rather than that one row.

Terminal window
$ curl -s -X POST -H "Authorization: Bearer $(cat "$AGENT_SECRETS_ACCOUNT_TOKEN_FILE")" -d '{"approver":"ada@example.com"}' "$AGENT_SECRETS_URL/v1/grants/<grant id>/revoke-by-approver"
{"state":"revoked"}

(That is the call Dispatch’s server makes as its own ServiceAccount, the caller the broker lists in BROKER_API_ACCOUNTS; on the local stack you can make it yourself with the token in $AGENT_SECRETS_ACCOUNT_TOKEN_FILE. The broker takes the revoking person the body names on its caller’s word, as it does for a machine login’s decision: a revoke only takes power away, so unlike approving a secret request it needs no sign-in to the broker.)

From your own shell on a machine that is logged in (not from inside an agent session), agent-secrets grant list lists the same grants as Live grants, and agent-secrets grant revoke ends one as Revoke does, withholding included. Both call the broker under the machine’s login: the helper signs each call with the machine’s credential, and refuses a process inside a registered session’s process tree (IN_SESSION), so an agent’s own commands cannot act as you. The check covers that process tree only. A process a session sends out of it, with ( cmd & ), setsid -f or a tmux server the session started, passes it, and any process running as your user can stop the helper anyway. Through these commands such a process can list and revoke your own machine logins, never a service’s machine login, and your grants, which include every grant whose approver is you, on any session, a service’s pod among them — those you approved, and those the model judge approved in your stead — and every grant the judge approved in anyone’s stead. agent-secrets enroll --helper is open to any process of your user, so such a process can also enroll a box and read your agent secrets.

Terminal window
$ agent-secrets grant list
GRANT_ID SECRETS GRANTED APPROVER DECIDED_BY SESSION OPERATOR EXPIRES
f26b6b78-2819-4b1d-abbd-d6990ca1a776 DEMO_API_KEY approval ada@example.com ada@example.com session/example-host-laptop:1989345:35362384 ada@example.com 2026-10-10T15:56:07Z
cc63d479-ddae-4052-b93b-b4b5df37adf0 DEMO_READ_TOKEN automatic - - session/example-host-devbox:1989344:35362384 ada@example.com session-end
Every row is a grant from the broker AGENT_SECRETS_URL names (AWS Secrets Manager).
$ agent-secrets grant revoke f26b6b78-2819-4b1d-abbd-d6990ca1a776
revoked f26b6b78-2819-4b1d-abbd-d6990ca1a776 at the broker AGENT_SECRETS_URL names

The line after the table goes to standard error, naming the broker and kind of store every row is from, so the table stays one row per grant for a script.

GRANTED is automatic or approval. APPROVER is who may end the grant. DECIDED_BY is who decided the request it rests on: that same person for a person’s own approval, and judge:<model id> where the model judge approved it in that person’s stead. Both are - for an automatic grant, which nobody decided. EXPIRES is session-end for a grant that lasts until its session ends, and a time for one its request gave a --ttl. --json prints the broker’s answer, the same body the Live grants page reads, whose expires_at is null for a grant that lasts until its session ends.

A session can end any of its own grants, by the grant id agent-secrets self lists. A session ending its own grant withholds nothing: its next request is decided as before.

Terminal window
$ agent-secrets revoke b51e58bc-3b7b-4144-a2ed-5821f232d612
revoked at the broker AGENT_SECRETS_URL names
$ agent-secrets self
enrollment_id: 2b8627f4-9a2a-4c12-85bf-910144bc0b5c
kind: session
operator: ada@example.com
lease_expires_at: 2026-10-03T03:36:40Z
source: the broker AGENT_SECRETS_URL names (AWS Secrets Manager)

Retag the secret so its sessions no longer get it automatically: set its tier to human (agent-secrets secret retag NAME --tier human), so every request for it needs a person’s approval, or give it another owner, so the sessions that had it are no longer the owner’s own. Deleting the secret (agent-secrets secret delete NAME), or moving it out of BROKER_SECRETS_PREFIX, ends access altogether (manage a secret). Once the broker rereads the secret, at once after a write through agent-secrets secret and, after one made any other way, within about ten minutes on AWS Secrets Manager (on Google Secret Manager usually as soon, but Google bounds its listing’s lag only at a few hours, so ask for a reread after a gcloud write), it refuses each read of a grant given automatically under the old tags with GRANT_NOT_LIVE, and the session’s next request for the secret waits for a person’s approval. One reread ends it everywhere: the broker process that answers the call tells every other process serving the same database to reread the same name, so the retag is in force across the deployment within seconds, except on a process whose database connection has dropped, which catches up at its own next five-minute read of the namespace. The change applies to every session that got the secret automatically, where revoking a grant of your own session in Dispatch withholds it from that session alone.

A session that no longer needs what it asked for withdraws the request, which takes it off the approver’s list. It names the request by the request id agent-secrets printed when it asked (agent-secrets request prints it, and so does a command whose --wait ran out):

Terminal window
$ agent-secrets cancel 715ea84a-6f5c-4230-83c9-1cf258acd88b
cancelled at the broker AGENT_SECRETS_URL names
$ agent-secrets status 715ea84a-6f5c-4230-83c9-1cf258acd88b
state: cancelled
source: the broker AGENT_SECRETS_URL names (AWS Secrets Manager)
decided_by: session:2b8627f4-9a2a-4c12-85bf-910144bc0b5c

Ending a session’s enrollment revokes all of its grants and cancels all of its pending requests in one step, and the approver’s Inbox drops them. A session ends in one of four ways:

  • Its launcher revokes it. A machine session ends when its process exits: the helper sees the registered process go and revokes its enrollment (it logs session retired … why="process exited"). agent-secrets-helper sessions lists the live ones. A box’s launcher revokes it with agent-secrets unenroll --helper --enrollment <id>.
  • Its lease lapses. A session that stops renewing (a pod that is gone, a box whose agent-secrets renew stopped, or a machine that went away) is refused ENROLLMENT_NOT_LIVE on its next renewal from the moment its lease lapses, BROKER_LEASE_SECONDS after its last renewal. Other session calls are refused PROOF_INVALID. The broker’s sweep ends it on its next run, within BROKER_SWEEP_SECONDS, and logs broker sweeper: ended an enrollment whose lease lapsed. A lease that ran out while no broker was serving (every broker process down, or the database out of reach) is not ended: the broker gives the session back that time once it serves again (when no broker is serving). The configuration reference gives both settings’ defaults.
  • Its machine login is revoked. Whoever may revoke the machine login that enrolled it — the person who approved a person’s machine login, or anyone signed in for a service’s — revokes that login, which ends every session it enrolled (end a machine’s login).
  • Its machine login ends, for a machine’s own session. A session a machine enrolled under the login its Google identity token traded for renews only while that login is live, so it also ends when that login passes its expiry — an hour from the token it was minted from, unless the machine trades again — or when the machine’s account is dropped from the broker’s BROKER_VM_SERVICES. Its next renewal is refused ENROLLMENT_NOT_LIVE, the broker logs broker: machine session renewal refused: its machine login has ended, and it lapses within BROKER_LEASE_SECONDS. A person’s sign-in sessions on that box are not ended this way; they end when the sign-in is revoked (end your sign-in on a machine).

A person’s machine login lapses BROKER_LAUNCHER_CREDENTIAL_SECONDS after its last refresh, and its helper refreshes it at least daily, but its sessions outlive it either way: a session renews its lease with its own key, and the broker reads no credential to renew a session a person’s machine login enrolled, so a box keeps working after the machine’s credential expires, and so does a machine session while the helper that renews it runs. (A machine’s own session, enrolled under the login its Google identity traded for, is the exception above: it renews only while that login is live.) A login can be ended, with every session it enrolled, at any time: for a machine that is lost, compromised or no longer used, or a Legion daemon whose pods must lose their secrets now. Dispatch’s machine-login page (/credentials/machine) lists Machine logins: every machine logged in as you, and every service’s login, such as the Legion daemon’s, shown as legion-daemon on <host>, each with its Credential id, who approved it (No one: and the ServiceAccount that logged in, for a daemon’s token login), when its login was issued and when it expires. The host is the name its machine reported, so two logins can share it; the credential id is the one every enrollment the login made names, and it tells them apart. A login that has expired stays listed, marked expired, sessions still running, while a session it enrolled still runs, so revoking every row a machine has ends every session it started, those of its earlier logins included. Click Revoke on its row and confirm. The broker then, at once:

  • refuses the login’s credential, so it enrolls no more sessions, and ends its refresh tokens, so it cannot be refreshed; and
  • ends every session it enrolled (machine sessions and boxes, or every worker pod of the Legion daemon’s login) as ending a session does: each one’s grants are revoked and its pending requests cancelled, and its next call is refused PROOF_INVALID.

Only the person who approved a person’s machine login may revoke it; the broker refuses anyone else NOT_APPROVER. Anyone signed in may revoke a service’s login. A revoked login stays revoked: the machine runs agent-secrets machine login again and its person approves the new code, while the Legion daemon logs in again with its own Kubernetes token, which no person approves, and enrolls the pods it launches from then on. A worker pod already running when its login was revoked stays without secrets until the daemon replaces it. To keep a daemon from logging in again, remove its account from the broker’s BROKER_SERVICE_LOGINS and redeploy the broker (Operate).

A machine that logs in with its own identity

Section titled “A machine that logs in with its own identity”

A box of a listed fleet needs no human to log in again: its helper trades the Google identity the machine already holds, and that trade mints the login. Its credential is keyed by the machine and the key the trade proved, and agent-secrets-helper keeps that key at ~/.config/agent-secrets/identity-key.pem so a restart moves the one credential the machine already holds rather than leaving a second behind. The two together mean a revoke alone does not cut such a box off: within the hour the helper trades again, the broker inserts a new live credential bound to that same key — a revoked row is no longer the one a trade of that subject moves — and anyone who copied that key file signs launcher proofs again under the new credential’s id, enrolling sessions that read what the fleet’s service may read.

To cut off a machine whose key may have been copied, in this order:

  1. Remove the key. Delete ~/.config/agent-secrets/identity-key.pem on the machine.

  2. Restart the helper, with systemctl --user restart agent-secrets-helper.service or agent-secrets-helper install --restart-with-sessions. It makes a new key, and its next trade holds a credential under a subject of its own, so the copied key is the old login’s and nothing else’s. A restart ends the grants of the sessions registered with it, which is why install refuses one without that flag.

  3. Revoke the old login on the machine-login page, by the credential id the old key held — the row whose Credential id is not the one the new trade issued. That ends it and every session it enrolled, as above.

In the other order the revoke is undone within the hour, because step 1 has not happened yet and the helper trades the key it still holds. None of it reaches the machine’s own Google identity token, which is what the trade proves in the first place: a box you believe is compromised comes out of its fleet (BROKER_VM_SERVICES, then redeploy the broker), after which its credential stops authenticating at its next call and no trade of it is answered (Operate).

From your own shell on a logged-in machine, agent-secrets machine list lists your own machines’ logins, row for row as the machine-login page lists them, and agent-secrets machine revoke ends one, under that machine’s login. A service’s login is not listed there, and machine revoke of one is refused NOT_FOUND as for an id it does not know. Revoking the machine’s own login (its id is the one machine revoke warns about) ends that machine’s access too: its next command is refused LAUNCHER_INVALID until it logs in again.

Terminal window
$ agent-secrets machine list
CREDENTIAL_ID HOST APPROVED_BY ISSUED EXPIRES STATE
d4187389-953c-4156-90c1-1dd937e9bf0b example-host-devbox ada@example.com 2026-10-10T14:56:01Z 2026-10-17T14:56:01Z ok
a2df6d16-adc1-434e-b479-496defb3a5c8 example-host-laptop ada@example.com 2026-10-10T14:55:53Z 2026-10-17T14:55:53Z ok
Every row is a login to the broker AGENT_SECRETS_URL names.
$ agent-secrets machine revoke 8f4c8ae4-b73e-4a60-a591-725667480f1a
agent-secrets machine revoke: no such machine login (NOT_FOUND); asked the broker AGENT_SECRETS_URL names
$ agent-secrets machine revoke a2df6d16-adc1-434e-b479-496defb3a5c8
revoked a2df6d16-adc1-434e-b479-496defb3a5c8 at the broker AGENT_SECRETS_URL names

The machine-login page lists the services’ logins, the Legion daemon’s, beside them, and revokes them. This one a person approved before services logged in with their own Kubernetes token; a daemon’s token login is listed the same way with approved_by empty and subject naming its ServiceAccount:

Terminal window
$ curl -s -H "Authorization: Bearer $(cat "$AGENT_SECRETS_ACCOUNT_TOKEN_FILE")" "$AGENT_SECRETS_URL/v1/launcher-credentials?approver=ada@example.com"
{"credentials":[{"credential_id":"d4187389-953c-4156-90c1-1dd937e9bf0b","host":"example-host-devbox","service":null,"subject":null,"approved_by":"ada@example.com","issued_at":"2026-10-10T14:56:01.121955Z","expires_at":"2026-10-17T14:56:01.122701Z","expired":false},{"credential_id":"8f4c8ae4-b73e-4a60-a591-725667480f1a","host":"example-host-cluster","service":"legion-daemon","subject":null,"approved_by":"bob@example.com","issued_at":"2026-10-10T14:55:59.906427Z","expires_at":"2026-10-17T14:55:59.9071Z","expired":false}]}
$ curl -s -X POST -H "Authorization: Bearer $(cat "$AGENT_SECRETS_ACCOUNT_TOKEN_FILE")" -d '{"approver":"ada@example.com"}' "$AGENT_SECRETS_URL/v1/launcher-credentials/8f4c8ae4-b73e-4a60-a591-725667480f1a/revoke-by-approver"
{"state":"revoked"}

(Those are the calls Dispatch’s server makes, as above.)

STATE is expired for a login past its expiry whose sessions still run. --json prints the broker’s answer, the machine-login page’s rows for your own machines. On the machine whose login was revoked, every machine and grant command is refused from then on:

Terminal window
$ agent-secrets machine list
agent-secrets machine list: the launcher credential is not valid (LAUNCHER_INVALID); this machine's login is expired or revoked (run: agent-secrets machine login)

Nothing tells the machine’s helper; it finds out the next time it calls the broker. A session’s renewal, within a third of BROKER_LEASE_SECONDS, is refused; revoking that session’s enrollment is refused too, and the helper drops the credential and logs at ERROR the broker refused the launcher credential (…); cleared: no session can enroll until a human approves a new machine login. A helper with no session running finds out at its next refresh, within a day, or when it restarts: the broker refuses the refresh, and the helper drops the login, deletes its machine file and logs at ERROR the broker refused the machine login's refresh: it was revoked, lapsed, or its refresh token was reused; cleared: no session can enroll until a human approves a new machine login. From then agent-secrets machine login-status exits 1 and prints the cause, here a refused session call:

Terminal window
$ agent-secrets machine login-status
expired
agent-secrets machine login-status: the broker refused the launcher credential (expired or revoked, or a proof it could not verify, such as clock skew or an AGENT_SECRETS_URL mismatch); run: agent-secrets machine login

The Legion daemon finds out when it next enrolls or ends a pod: the broker refuses its credential LAUNCHER_INVALID, and the daemon logs in again with its Kubernetes token, under a new credential, unless its account is no longer in the broker’s BROKER_SERVICE_LOGINS. The pods it had enrolled do not wait for that: their sessions ended with the revoke, so their agents’ agent-secrets calls are refused PROOF_INVALID from then on. The daemon does not enroll a running pod again: each stays without secrets until the daemon replaces it, and only the pods launched after its new login are enrolled.

Stopping the helper stops the machine from enrolling sessions until it starts again, but it ends none, and the helper resumes its login when it starts. A machine session lapses within BROKER_LEASE_SECONDS once the helper stops, since the helper is what renews it. A box renews itself (agent-secrets renew), so it keeps working after the helper stops and after the machine’s credential expires. Revoking the machine login ends both, and its row stays on the machine-login page while either runs, marked expired, sessions still running once the login has expired. Without Dispatch, end each box with agent-secrets unenroll --helper --enrollment <id>, run against a helper logged in as the box’s operator, or stop its agent-secrets renew, after which it lapses within BROKER_LEASE_SECONDS.

Signing in on a machine with agent-secrets login gives that machine a login of its own carrying you, so a session it enrols reads your secrets (Concepts). Dispatch’s Settings page lists those sign-ins beside your live grants, as Sign-ins: one row per machine you have signed in on and not revoked, naming the machine’s fleet account, the first characters of the box key’s thumbprint, which is what tells two boxes of one fleet apart, when you signed in there and when that box last used the sign-in. Click Revoke on a row and confirm.

Revoking ends three things at once: the sign-in, so the box key it was made with trades no token for you again; every access token it already minted, so the sessions running there lose Dispatch within seconds; and the machine login that sign-in held, so every session it enrolled ends as ending a session does. Dispatch serves a revoked access token for up to five seconds: it asks the broker whether a token is live and reuses that answer for five seconds (AffirmativeCacheTTL, packages/envoy/internal/brokertoken/brokertoken.go) rather than asking once per request. Your sign-ins on other machines, and the fleet’s own machine login, are untouched — the machine goes on reading what its service owns. Nobody sees or revokes anyone else’s sign-ins: Dispatch names the person from its own session, so another person’s id is an id the broker does not have.

What it ends is the box key, not your Google grant. agent-secrets login keeps Google’s refresh token on the machine beside that key, and anyone who can read that machine’s files can copy both and sign in again from somewhere else, as a sign-in of its own, which this revoke does not reach. So to cut off a box you believe is compromised, revoke the grant at Google — your Google Account’s Security → Your connections to third-party apps page, the entry for the sign-in client your deployment uses — which ends every copy of that refresh token at once. Revoking the sign-in here is the right tool for a box you have simply finished with.

Signing in again with the very key that was revoked is refused: the box makes a new key, which is a sign-in of its own. Run agent-secrets login on the machine to sign in again.

The sessions above are agents’. A person’s own sign-in to the broker’s approval page is separate: it lasts a day at most, is never extended, and ends when they sign out. Nobody ends someone else’s from a page; an operator ends every browser session one login holds by running the broker’s own subcommand in a broker pod, envoy-broker end-sessions <login> (Ending a person’s sessions). Revoking a grant or a machine login does not need a broker sign-in and never ends one.