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.
Revoke a grant in Dispatch
Section titled “Revoke a grant in Dispatch”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.
$ 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.)
Revoke a grant from your shell
Section titled “Revoke a grant from your shell”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.
$ agent-secrets grant listGRANT_ID SECRETS GRANTED APPROVER DECIDED_BY SESSION OPERATOR EXPIRESf26b6b78-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:07Zcc63d479-ddae-4052-b93b-b4b5df37adf0 DEMO_READ_TOKEN automatic - - session/example-host-devbox:1989344:35362384 ada@example.com session-endEvery row is a grant from the broker AGENT_SECRETS_URL names (AWS Secrets Manager).$ agent-secrets grant revoke f26b6b78-2819-4b1d-abbd-d6990ca1a776revoked f26b6b78-2819-4b1d-abbd-d6990ca1a776 at the broker AGENT_SECRETS_URL namesThe 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.
Revoke a grant from its session
Section titled “Revoke a grant from its session”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.
$ agent-secrets revoke b51e58bc-3b7b-4144-a2ed-5821f232d612revoked at the broker AGENT_SECRETS_URL names$ agent-secrets selfenrollment_id: 2b8627f4-9a2a-4c12-85bf-910144bc0b5ckind: sessionoperator: ada@example.comlease_expires_at: 2026-10-03T03:36:40Zsource: the broker AGENT_SECRETS_URL names (AWS Secrets Manager)End every session’s access to a secret
Section titled “End every session’s access to a secret”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.
Cancel a pending request
Section titled “Cancel a pending request”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):
$ agent-secrets cancel 715ea84a-6f5c-4230-83c9-1cf258acd88bcancelled at the broker AGENT_SECRETS_URL names$ agent-secrets status 715ea84a-6f5c-4230-83c9-1cf258acd88bstate: cancelledsource: the broker AGENT_SECRETS_URL names (AWS Secrets Manager)decided_by: session:2b8627f4-9a2a-4c12-85bf-910144bc0b5cEnd a session
Section titled “End a session”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 sessionslists the live ones. A box’s launcher revokes it withagent-secrets unenroll --helper --enrollment <id>. - Its lease lapses. A session that stops renewing (a pod that is gone, a box whose
agent-secrets renewstopped, or a machine that went away) is refusedENROLLMENT_NOT_LIVEon its next renewal from the moment its lease lapses,BROKER_LEASE_SECONDSafter its last renewal. Other session calls are refusedPROOF_INVALID. The broker’s sweep ends it on its next run, withinBROKER_SWEEP_SECONDS, and logsbroker 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 refusedENROLLMENT_NOT_LIVE, the broker logsbroker: machine session renewal refused: its machine login has ended, and it lapses withinBROKER_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).
End a machine’s login
Section titled “End a machine’s login”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:
-
Remove the key. Delete
~/.config/agent-secrets/identity-key.pemon the machine. -
Restart the helper, with
systemctl --user restart agent-secrets-helper.serviceoragent-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 whyinstallrefuses one without that flag. -
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.
$ agent-secrets machine listCREDENTIAL_ID HOST APPROVED_BY ISSUED EXPIRES STATEd4187389-953c-4156-90c1-1dd937e9bf0b example-host-devbox ada@example.com 2026-10-10T14:56:01Z 2026-10-17T14:56:01Z oka2df6d16-adc1-434e-b479-496defb3a5c8 example-host-laptop ada@example.com 2026-10-10T14:55:53Z 2026-10-17T14:55:53Z okEvery row is a login to the broker AGENT_SECRETS_URL names.$ agent-secrets machine revoke 8f4c8ae4-b73e-4a60-a591-725667480f1aagent-secrets machine revoke: no such machine login (NOT_FOUND); asked the broker AGENT_SECRETS_URL names$ agent-secrets machine revoke a2df6d16-adc1-434e-b479-496defb3a5c8revoked a2df6d16-adc1-434e-b479-496defb3a5c8 at the broker AGENT_SECRETS_URL namesThe 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:
$ 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:
$ agent-secrets machine listagent-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:
$ agent-secrets machine login-statusexpiredagent-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 loginThe 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.
End your sign-in on a machine
Section titled “End your sign-in on a machine”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.
End a person’s sign-in to the broker
Section titled “End a person’s sign-in to the broker”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.