mirror of
https://github.com/garrytan/gstack.git
synced 2026-07-09 17:38:40 +02:00
feat: default paired agents to full access, split SCOPE_CONTROL
The trust boundary for paired agents is the pairing ceremony itself, not the scope. An agent with write scope can already click anything and navigate anywhere. Gating js/cookies behind --admin was security theater. Changes: - Default pair scopes: read+write+admin+meta (was read+write) - New SCOPE_CONTROL for browser-wide destructive ops (stop, restart, disconnect, state, handoff, resume, connect) - --admin flag now grants control scope (backward compat) - New --restrict flag for limited access (e.g., --restrict read) - Updated hint text: "re-pair with --control" instead of "--admin" Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -113,15 +113,15 @@ describe('generateInstructionBlock', () => {
|
||||
expect(block).not.toContain('re-pair with --admin');
|
||||
});
|
||||
|
||||
it('shows re-pair hint when admin not included', () => {
|
||||
it('shows re-pair hint when control not included', () => {
|
||||
const block = generateInstructionBlock({
|
||||
setupKey: 'gsk_setup_nonadmin',
|
||||
setupKey: 'gsk_setup_nocontrol',
|
||||
serverUrl: 'https://test.ngrok.dev',
|
||||
scopes: ['read', 'write'],
|
||||
scopes: ['read', 'write', 'admin', 'meta'],
|
||||
expiresAt: '2026-04-06T00:00:00Z',
|
||||
});
|
||||
|
||||
expect(block).toContain('re-pair with --admin');
|
||||
expect(block).toContain('re-pair with --control');
|
||||
});
|
||||
|
||||
it('includes newtab as step 2 (agents must own their tab)', () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
validateToken, checkScope, checkDomain, checkRate,
|
||||
revokeToken, rotateRoot, listTokens, recordCommand,
|
||||
serializeRegistry, restoreRegistry, checkConnectRateLimit,
|
||||
SCOPE_READ, SCOPE_WRITE, SCOPE_ADMIN, SCOPE_META,
|
||||
SCOPE_READ, SCOPE_WRITE, SCOPE_ADMIN, SCOPE_CONTROL, SCOPE_META,
|
||||
} from '../src/token-registry';
|
||||
|
||||
describe('token-registry', () => {
|
||||
@@ -25,7 +25,7 @@ describe('token-registry', () => {
|
||||
const info = validateToken('root-token-for-tests');
|
||||
expect(info).not.toBeNull();
|
||||
expect(info!.clientId).toBe('root');
|
||||
expect(info!.scopes).toEqual(['read', 'write', 'admin', 'meta']);
|
||||
expect(info!.scopes).toEqual(['read', 'write', 'admin', 'meta', 'control']);
|
||||
expect(info!.rateLimit).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -324,7 +324,7 @@ describe('token-registry', () => {
|
||||
it('every command in commands.ts is covered by a scope', () => {
|
||||
// Import the command sets to verify coverage
|
||||
const allInScopes = new Set([
|
||||
...SCOPE_READ, ...SCOPE_WRITE, ...SCOPE_ADMIN, ...SCOPE_META,
|
||||
...SCOPE_READ, ...SCOPE_WRITE, ...SCOPE_ADMIN, ...SCOPE_CONTROL, ...SCOPE_META,
|
||||
]);
|
||||
// chain is a special case (checked via meta scope but dispatches subcommands)
|
||||
allInScopes.add('chain');
|
||||
@@ -339,8 +339,12 @@ describe('token-registry', () => {
|
||||
expect(SCOPE_ADMIN.has('cookies')).toBe(true);
|
||||
expect(SCOPE_ADMIN.has('storage')).toBe(true);
|
||||
expect(SCOPE_ADMIN.has('useragent')).toBe(true);
|
||||
expect(SCOPE_ADMIN.has('state')).toBe(true);
|
||||
expect(SCOPE_ADMIN.has('handoff')).toBe(true);
|
||||
// Browser-wide destructive commands moved to SCOPE_CONTROL
|
||||
expect(SCOPE_CONTROL.has('state')).toBe(true);
|
||||
expect(SCOPE_CONTROL.has('handoff')).toBe(true);
|
||||
expect(SCOPE_CONTROL.has('stop')).toBe(true);
|
||||
expect(SCOPE_CONTROL.has('restart')).toBe(true);
|
||||
expect(SCOPE_CONTROL.has('disconnect')).toBe(true);
|
||||
|
||||
// Verify safe read commands are NOT in admin
|
||||
expect(SCOPE_ADMIN.has('text')).toBe(false);
|
||||
|
||||
Reference in New Issue
Block a user