Skip to content

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

Merged
Israeltheminer merged 1 commit into
mainfrom
fix/revoke-gateway-keys-v3
Sep 4, 2026
Merged

fix(platform): revoke the gateway key on every session teardown#3213
Israeltheminer merged 1 commit into
mainfrom
fix/revoke-gateway-keys-v3

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.

Replaces #3194, rebuilt on current main.

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).

Rebuilt rather than rebased

Main restructured the harness runner while this sat open: the flat sequence of
await checkX(...) calls is now a Lane list with truncation reporting and a
shared-session liveness assertion after each lane. Rebasing produced a
500-line conflict across that restructure.

So this is the same change re-applied on top: the five source files as they
were, and checkSandboxGatewayKeyReclaim registered as a lane in the new
structure rather than appended to a list that no longer exists. A throw in it
now reports as a failed check naming the lane instead of truncating the run.

The watchdogs.ts docblock conflict was resolved by taking main's text — it
gained the FAIR-walk detail and the whole RECLAIM bullet — and re-applying
only my clause about the expiry pass revoking gateway keys.

Both mutations re-verified after the rebuild.

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.

## 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.

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.

## Failure posture

Best-effort per key, as in 0.4. An unreachable gateway must never wedge a
teardown, so the HTTP failure is caught — at `console.error` with the key
id, because that key stays spendable and only an operator can delete it.

## Tests

`gateway-keys.test.ts` covers the glue in CI; the SQL election stays the
harness's job, since a mocked `sql` cannot prove a race.

| 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` |

`checkSandboxGatewayKeyReclaim` drives every edge against a fake gateway
that records the DELETEs. It registers as a lane in the harness runner, so
a throw is reported as a failed check naming the lane rather than
truncating the run silently.
@Israeltheminer
Israeltheminer merged commit f0e92a7 into main Sep 4, 2026
48 checks passed
@Israeltheminer
Israeltheminer deleted the fix/revoke-gateway-keys-v3 branch September 4, 2026 10:01
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