mirror of
https://github.com/garrytan/gstack.git
synced 2026-08-08 15:28:36 +02:00
test(server): lock the shared-vs-own-only tab gate contract
The pre-fix tests at tab-isolation.test.ts:43,57 encoded the broken
behavior as the contract — they specifically asserted "scoped agent
cannot write to unowned tab," which was the exact failure mode that
broke browser-skills. They passed because they tested the wrong
invariant.
This commit replaces those tests with explicit shared-vs-own-only
coverage that documents what each policy actually means:
- Shared scoped agents (skill spawns, default scoped clients) can
read AND write any tab — unowned, their own, or another agent's.
The capability is gated by scope checks + rate limits, not by tab
ownership.
- Own-only scoped agents (pair-agent over tunnel) cannot read OR
write any tab they don't own. Pre-fix this case was conflated with
shared writes; now it's explicit.
9 unit assertions on checkTabAccess, up from 6. Each test names
the policy axis it's covering so a future refactor can't quietly
flip the contract.
Adds source-shape regression test 10a in server-auth.test.ts:
"tab gate predicate is own-only-scoped, not write-scoped." The
gate's `if (...)` line MUST contain `tabPolicy === 'own-only'` and
MUST NOT contain `WRITE_COMMANDS.has(command) ||`. If a future
refactor re-introduces the write-scoped gate, this fails immediately
in free-tier `bun test`.
Updates the marker for the existing newtab-excluded test to match
the new comment block ("Tab ownership check (own-only tokens /
pair-agent isolation)").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
17b556f309
commit
6022db2c9a
@@ -145,6 +145,30 @@ describe('Server auth security', () => {
|
||||
expect(handleBlock).toContain('Tab not owned by your agent');
|
||||
});
|
||||
|
||||
// Test 10a: tab gate is gated on own-only, not on isWrite
|
||||
// Regression test for v1.20.0.0 footgun fix. Pre-fix the gate fired for
|
||||
// any write command from any non-root token, which 403'd local skill
|
||||
// spawns trying to drive the user's natural (unowned) tabs. The bundled
|
||||
// hackernews-frontpage skill failed identically. The fix narrows the
|
||||
// gate to `tabPolicy === 'own-only'` so pair-agent tunnel tokens stay
|
||||
// strict while local shared-policy tokens (skill spawns) get unblocked.
|
||||
test('tab gate predicate is own-only-scoped, not write-scoped', () => {
|
||||
const handleBlock = sliceBetween(SERVER_SRC, "async function handleCommand", "Block mutation commands while watching");
|
||||
// The gate condition must include the own-only check.
|
||||
expect(handleBlock).toContain("tabPolicy === 'own-only'");
|
||||
// It must NOT depend on WRITE_COMMANDS in the gate predicate (only inside
|
||||
// the checkTabAccess call's isWrite arg, which is informational). The
|
||||
// surrounding `if (...) {` for the gate must use `tabPolicy === 'own-only'`
|
||||
// as the trigger, not `WRITE_COMMANDS.has(command) || ...`.
|
||||
const gateLine = handleBlock.split('\n').find(l =>
|
||||
l.includes("command !== 'newtab'") &&
|
||||
l.includes('tokenInfo') &&
|
||||
l.includes('tabPolicy')
|
||||
);
|
||||
expect(gateLine).toBeTruthy();
|
||||
expect(gateLine).not.toMatch(/WRITE_COMMANDS\.has\(command\)\s*\|\|/);
|
||||
});
|
||||
|
||||
// Test 10b: chain command pre-validates subcommand scopes
|
||||
test('chain handler checks scope for each subcommand before dispatch', () => {
|
||||
const metaSrc = fs.readFileSync(path.join(import.meta.dir, '../src/meta-commands.ts'), 'utf-8');
|
||||
@@ -317,7 +341,7 @@ describe('Server auth security', () => {
|
||||
// Regression: newtab returned 403 for scoped tokens because the tab ownership
|
||||
// check ran before the newtab handler, checking the active tab (owned by root).
|
||||
test('newtab is excluded from tab ownership check', () => {
|
||||
const ownershipBlock = sliceBetween(SERVER_SRC, 'Tab ownership check (for scoped tokens)', 'newtab with ownership for scoped tokens');
|
||||
const ownershipBlock = sliceBetween(SERVER_SRC, 'Tab ownership check (own-only tokens / pair-agent isolation)', 'newtab with ownership for scoped tokens');
|
||||
// The ownership check condition must exclude newtab
|
||||
expect(ownershipBlock).toContain("command !== 'newtab'");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user