v1.68.3.0 fix(pairing): re-pair to narrow revokes the old grant on the spot (#2665)

* fix(pairing): reject reserved clientId 'root' at all token writers

'root' is the sentinel checkScope/checkDomain/checkRate and the server
command gate use for the omnipotent caller, so a scoped token carrying it
bypasses every enforcement path. Add ReservedClientIdError + a shared
assertValidClientId; createToken/createSetupKey throw, restoreRegistry
skips-and-logs (a corrupt state file must not brick boot). /pair and /token
surface it as a named 400, and the CLI fast-fails --client root.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(pairing): release tab ownership on revoke

tabOwnership cleared only on tab close, so after DELETE /token a same-name
re-pair inherited the revoked agent's authenticated tabs (own-only access
keys on owner === clientId). Add BrowserManager.releaseClientTabs and run it
unconditionally in DELETE /token (ownership outlives the token, so an
expired-token client can still own tabs); 404 only when both nothing was
revoked and nothing released. Response now carries tabs_released.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* v1.68.3.0 fix(pairing): re-pair to narrow revokes the old grant on the spot

POST /pair minted a new setup key but never touched the agent's live
session, so re-pairing --client X --restrict read while X was connected (or
whose 5-min key expired unexchanged) left the original full-access session,
eval included, alive up to 24h.

A reducing re-pair (fewer scopes, tighter domains, lower rate, stricter tab
policy) now revokes the live session and releases its tabs before minting
the new key (grantReducesAccess + revokeClientFully; superseded in the
response). Non-reducing re-pairs keep the session and only drop stale PENDING
setup keys, so a broaden/refresh never strands a working agent and a
narrowing re-pair issued before the agent connects can't leave the old broad
key exchangeable. Revoke happens before mint (revokeToken deletes all of a
client's tokens). CLI prints a version-skew-safe supersede notice and warns
when a re-pair-shaped call omits --client. Docs + CHANGELOG + VERSION.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(pairing): harden re-pair per adversarial review

Adversarial review of the diff found four issues, now fixed:
- Validate the requested grant BEFORE the supersede revoke: a reducing
  re-pair with a bad scope/rate no longer destroys the live session and
  then fails to mint a replacement (assertValidTokenOptions runs up front).
- A re-pair with no live session releases tabs orphaned by an expired
  incarnation, closing the tab-inheritance gap /pair had (DELETE /token
  already released unconditionally).
- Test the DELETE /token revoked=0/tabs>0 path and the /pair orphaned-tab
  release at the handler level (HTTP e2e can't, headless owns no tabs).
- Test the CLI --client root fast-fail; fix its null-guard (parseFlag
  returns null when --client is absent).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Garry Tan <garry@ycombinator.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-08-21 15:32:26 -07:00
committed by GitHub
co-authored by Claude Fable 5 Garry Tan
parent 51932eceef
commit 85fd9db554
15 changed files with 592 additions and 18 deletions
+10 -2
View File
@@ -1102,6 +1102,13 @@ With --restrict (`--restrict read`, `--restrict "read,write"`):
a trusted agent can be prompt-injected by pages it reads, and scope caps the
blast radius (eval works over the tunnel).
- `--restrict` never grants `control`; that scope stays behind --control.
- To tighten an agent that is ALREADY paired, re-pair it with the **same
`--client` name** and the narrower `--restrict`/`--domain`. A reducing re-pair
revokes the previous session immediately and releases its tabs — the agent
must reconnect with the new key, so the old wide access does not linger.
Re-pairing without `--client` mints a brand-new agent and leaves the old one
untouched. Broadening or refreshing keeps the working session (no outage).
- `root` is a reserved `--client` name (it would bypass all scope enforcement).
With --control (--admin is the legacy alias):
- Everything, plus browser-wide destructive ops (stop, restart, disconnect)
@@ -1112,8 +1119,9 @@ With --control (--admin is the legacy alias):
**"Tab not owned by your agent"** — The remote agent tried to interact with a tab
it didn't create. Tell it to run `newtab` first to get its own tab.
**"Domain not allowed"** — The token has domain restrictions. Re-pair with broader
domain access or no domain restrictions.
**"Domain not allowed"** — The token has domain restrictions. Re-pair with the
same `--client` name and broader (or no) `--domain`. A broadening re-pair keeps
the working session; a narrowing one revokes it immediately.
**"Rate limit exceeded"** — The agent is sending > 10 requests/second. It should
wait for the Retry-After header and slow down.
+10 -2
View File
@@ -287,6 +287,13 @@ With --restrict (`--restrict read`, `--restrict "read,write"`):
a trusted agent can be prompt-injected by pages it reads, and scope caps the
blast radius (eval works over the tunnel).
- `--restrict` never grants `control`; that scope stays behind --control.
- To tighten an agent that is ALREADY paired, re-pair it with the **same
`--client` name** and the narrower `--restrict`/`--domain`. A reducing re-pair
revokes the previous session immediately and releases its tabs — the agent
must reconnect with the new key, so the old wide access does not linger.
Re-pairing without `--client` mints a brand-new agent and leaves the old one
untouched. Broadening or refreshing keeps the working session (no outage).
- `root` is a reserved `--client` name (it would bypass all scope enforcement).
With --control (--admin is the legacy alias):
- Everything, plus browser-wide destructive ops (stop, restart, disconnect)
@@ -297,8 +304,9 @@ With --control (--admin is the legacy alias):
**"Tab not owned by your agent"** — The remote agent tried to interact with a tab
it didn't create. Tell it to run `newtab` first to get its own tab.
**"Domain not allowed"** — The token has domain restrictions. Re-pair with broader
domain access or no domain restrictions.
**"Domain not allowed"** — The token has domain restrictions. Re-pair with the
same `--client` name and broader (or no) `--domain`. A broadening re-pair keeps
the working session; a narrowing one revokes it immediately.
**"Rate limit exceeded"** — The agent is sending > 10 requests/second. It should
wait for the Retry-After header and slow down.