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
+15
View File
@@ -155,6 +155,21 @@ describe('pair-agent scope-flag validation (pre-server)', () => {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
}, 30_000);
test('--client root → exit 1 reserved-name error, NO daemon spawned', async () => {
// `root` is the sentinel that bypasses every scope/domain/rate/tab check;
// the CLI must reject it pre-server (the daemon also 400s it as defense-in-depth).
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'browse-client-root-'));
const stateFile = path.join(tmpDir, 'browse.json');
try {
const result = await runCli(['pair-agent', '--client', 'root'], baseEnv(stateFile));
expect(result.code).toBe(1);
expect(result.stderr).toContain('reserved');
expect(fs.existsSync(stateFile)).toBe(false);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
}, 30_000);
});
describe('tunnel against no daemon (#2254 — never boot one)', () => {