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>
This commit is contained in:
Garry Tan
2026-08-21 15:15:13 -07:00
committed by Garry Tan
co-authored by Claude Fable 5
parent 6fe3e67736
commit d86edf1f8f
7 changed files with 85 additions and 5 deletions
+2 -2
View File
@@ -26,9 +26,9 @@ Re-pair is now the real tightening lever. Re-pair an agent with its **same `--cl
### Itemized changes
#### Fixed
- A reducing re-pair (`/pair` with fewer scopes, tighter domains, a lower rate, or a stricter tab policy) revokes the client's live session and releases its tabs before minting the new key; the response carries `superseded`. Non-reducing re-pairs keep the session and only drop stale pending setup keys, so a broaden or refresh never strands a working agent. (`browse/src/server.ts`, `browse/src/token-registry.ts`)
- A reducing re-pair (`/pair` with fewer scopes, tighter domains, a lower rate, or a stricter tab policy) revokes the client's live session and releases its tabs before minting the new key; the response carries `superseded`. Non-reducing re-pairs keep the session and only drop stale pending setup keys, so a broaden or refresh never strands a working agent. The requested grant is validated before any revoke, so a re-pair with a bad scope or rate is rejected without knocking the live session offline. (`browse/src/server.ts`, `browse/src/token-registry.ts`)
- A narrowing re-pair issued before the agent connects invalidates the earlier, broader setup key, so it can no longer be exchanged. (`browse/src/token-registry.ts`)
- Revoking an agent releases the tab ownership it held: `DELETE /token` runs the release unconditionally (ownership outlives the token) and reports `tabs_released`, and an own-only client re-paired under the same name can no longer read those tabs. (`browse/src/browser-manager.ts`, `browse/src/server.ts`)
- Revoking an agent releases the tab ownership it held: `DELETE /token` runs the release unconditionally (ownership outlives the token) and reports `tabs_released`, and an own-only client re-paired under the same name can no longer read those tabs. A re-pair with no live session likewise frees any tabs orphaned by an expired incarnation, so a fresh session can't inherit them. (`browse/src/browser-manager.ts`, `browse/src/server.ts`)
- `root` is rejected as a `clientId` at every token writer, so a scoped token can never carry the sentinel that bypasses scope, domain, rate, and tab checks; `/pair` and `/token` return a named 400 and the CLI rejects `--client root` before it reaches the daemon. A persisted `root` entry is skipped when the registry is restored. (`browse/src/token-registry.ts`, `browse/src/cli.ts`)
#### For contributors