Skip to content

fix(platform): revoke the gateway key on every session teardown - #3194

Closed
Israeltheminer wants to merge 1 commit into
mainfrom
fix/revoke-gateway-keys-v2
Closed

fix(platform): revoke the gateway key on every session teardown#3194
Israeltheminer wants to merge 1 commit into
mainfrom
fix/revoke-gateway-keys-v2

Conversation

@Israeltheminer

Copy link
Copy Markdown
Collaborator

A sandbox session's LLM-gateway virtual key outlived the session. The gateway
has no native TTL on a virtual key, and mintVirtualKey gives every key a
reset_duration: '1M' budget window on the assumption that teardown deletes
it long before any reset matters. A key that survives teardown is therefore a
permanent bearer credential with a self-refilling monthly allowance against
the organization's own provider keys.

Rebase of #3148 onto current main, plus unit coverage the original lacked.

One seam, every teardown edge

revokeSessionGatewayKeys is the 0.5 twin of 0.4's
node_only/sandbox/session_teardown.ts plus the revoke half of
session_admin_actions.destroySandbox. All four edges route through it — TTL
expiry, admin destroy, phantom heal, and a deadline-failed agent run — so
they cannot drift apart.

The election is the token flip. UPDATE … WHERE revoked_at_ms IS NULL RETURNING llm_gateway_key_id hands each live key to exactly one caller, so a
watchdog sweeping the same session twice, or a destroy racing the expiry
sweep, revokes once. revokeVirtualKey also treats a 404 as success, so even
a genuine double DELETE cannot fail a teardown.

Exec scope matters. Without execId the whole session is reclaimed. With
one — a deadline-failed task-agent run — only that exec's minted key is
taken, so a sibling turn still running on the same pa-<agentId> session
keeps its credential.

The unit coverage is new

The original PR proved this only through integration-check.ts. That check
needs SANDBOX_LLM_GATEWAY_ADMIN_PASSWORD, and without it every revoke fails
authentication — so the check exercised its own fail-open lane and proved
nothing until that was fixed. Meanwhile a 487-line new module had no test in
CI at all.

gateway-keys.test.ts covers the glue, where the consequences are as sharp
as in the SQL:

Mutation Went red
let a gateway failure throw never throws when the gateway fails, keeps revoking the remaining keys after one fails
clear the session row even when exec-scoped leaves the session row alone for an exec-scoped teardown

The first is the one that matters most: an unreachable gateway wedging a
teardown is worse than a leaked key, so the failure is counted and logged —
at console.error, not warn, because the key stays spendable and only an
operator can delete it by hand.

The SQL election itself stays the harness's job; a mocked sql cannot prove
a race.

Gate

typecheck 0 errors, oxlint --type-aware clean, oxfmt --check clean,
sandbox + tasks suites 24 passed (5 files).

A sandbox session mints an LLM gateway virtual key and hands its value into
the container. revokeVirtualKey had one caller where 0.4 had four, so the
TTL expiry sweep, admin destroy, phantom heal and the task-agent lane all
tore sessions down and left the key live.

The mint body states the invariant twice: the gateway has no native TTL, and
reset_duration '1M' is safe BECAUSE teardown revokes the key long before any
reset matters. It did not. So each abandoned key is a permanent bearer
credential with a self-refilling monthly allowance against the org's own
provider keys, one per expired, deadline-failed or destroyed session.

All teardown edges now go through one seam, domains/sandbox/gateway-keys.ts.
The token flip IS the election: UPDATE ... WHERE revoked_at_ms IS NULL
RETURNING hands each live key to exactly one caller, so a watchdog sweeping
twice or a destroy racing the expiry revokes once. An exec-scoped teardown
narrows to that turn's key, so a sibling turn on the same standing session
keeps its own credential.

Best-effort per key, as 0.4 was: an unreachable gateway must never wedge a
teardown. But the failure logs at console.error naming the key id, because
that key stays spendable and only an operator can delete it by hand.
@Israeltheminer

Copy link
Copy Markdown
Collaborator Author

Superseded by the rebuilt branch — main restructured the harness runner into a Lane list while this sat open, and rebasing produced a 500-line conflict across that restructure. The replacement re-applies the same change with the new check registered as a lane.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant