Merge remote-tracking branch 'origin/main' into garrytan/browser-batch-multitab

This commit is contained in:
Garry Tan
2026-04-06 22:41:40 -07:00
5 changed files with 58 additions and 6 deletions
+6
View File
@@ -1,5 +1,11 @@
# Changelog
## [0.15.15.1] - 2026-04-06
### Fixed
- pair-agent tunnel drops after 15 seconds. The browse server was monitoring its parent process ID and self-terminating when the CLI exited. Now pair-agent sessions disable the parent watchdog so the server and tunnel stay alive.
- `$B connect` crashes with "domains is not defined". A stray variable reference in the headed-mode status check prevented GStack Browser from initializing properly.
## [0.15.15.0] - 2026-04-06
Community security wave: 8 PRs from 4 contributors, every fix credited as co-author.
+1 -1
View File
@@ -1 +1 @@
0.15.15.0
0.15.15.1
+9 -4
View File
@@ -839,6 +839,11 @@ Refs: After 'snapshot', use @e1, @e2... as selectors:
BROWSE_PORT: '34567',
BROWSE_SIDEBAR_CHAT: '1',
};
// If parent explicitly set BROWSE_PARENT_PID=0 (pair-agent disabling
// self-termination), pass it through so startServer doesn't override it.
if (process.env.BROWSE_PARENT_PID === '0') {
serverEnv.BROWSE_PARENT_PID = '0';
}
const newState = await startServer(serverEnv);
// Print connected status
@@ -848,9 +853,7 @@ Refs: After 'snapshot', use @e1, @e2... as selectors:
'Content-Type': 'application/json',
'Authorization': `Bearer ${newState.token}`,
},
body: JSON.stringify({
domains,
command: 'status', args: [] }),
body: JSON.stringify({ command: 'status', args: [] }),
signal: AbortSignal.timeout(5000),
});
const status = await resp.text();
@@ -977,7 +980,9 @@ Refs: After 'snapshot', use @e1, @e2... as selectors:
const connectProc = Bun.spawn([browseBin, 'connect'], {
cwd: process.cwd(),
stdio: ['ignore', 'inherit', 'inherit'],
env: process.env,
// Disable parent-PID monitoring: pair-agent needs the server to outlive
// the connect subprocess. Setting to 0 tells the server not to self-terminate.
env: { ...process.env, BROWSE_PARENT_PID: '0' },
});
await connectProc.exited;
// Re-read state after headed mode switch
+2 -1
View File
@@ -929,7 +929,8 @@ async function handleCommandInternal(
}
// ─── Tab ownership check (for scoped tokens) ──────────────
if (tokenInfo && tokenInfo.clientId !== 'root' && (WRITE_COMMANDS.has(command) || tokenInfo.tabPolicy === 'own-only')) {
// Skip for newtab — it creates a new tab, doesn't access an existing one.
if (command !== 'newtab' && tokenInfo && tokenInfo.clientId !== 'root' && (WRITE_COMMANDS.has(command) || tokenInfo.tabPolicy === 'own-only')) {
const targetTab = tabId ?? browserManager.getActiveTabId();
if (!browserManager.checkTabAccess(targetTab, tokenInfo.clientId, { isWrite: WRITE_COMMANDS.has(command), ownOnly: tokenInfo.tabPolicy === 'own-only' })) {
return {
+40
View File
@@ -277,4 +277,44 @@ describe('Server auth security', () => {
expect(batchBlock).toContain('tabId: cmd.tabId');
expect(batchBlock).toContain('handleCommandInternal');
});
// ─── Pair-agent regression tests ──────────────────────────
// Regression: connect command crashed with "domains is not defined" because
// a stray `domains,` variable was in the status fetch body (cli.ts:852).
test('connect command status fetch body has no undefined variable references', () => {
const connectBlock = sliceBetween(CLI_SRC, 'Launching headed Chromium', 'Sidebar agent started');
// The status fetch should use a clean JSON body
expect(connectBlock).toContain("command: 'status'");
// Must NOT contain a bare `domains` reference in the fetch body
// (it would be `domains,` on its own line, not part of a key like `domains:`)
const bodyMatch = connectBlock.match(/body:\s*JSON\.stringify\(\{([^}]+)\}\)/);
expect(bodyMatch).not.toBeNull();
if (bodyMatch) {
// The body should only contain command and args, no stray variables
expect(bodyMatch[1]).not.toMatch(/\bdomains\b/);
}
});
// Regression: pair-agent server died 15s after CLI exited because the server
// monitored the connect subprocess PID. pair-agent must set BROWSE_PARENT_PID=0
// to disable self-termination.
test('pair-agent disables parent PID monitoring via BROWSE_PARENT_PID=0', () => {
const pairBlock = sliceBetween(CLI_SRC, 'Ensure headed mode', 'handlePairAgent');
// The connect subprocess env must override BROWSE_PARENT_PID
expect(pairBlock).toContain("BROWSE_PARENT_PID");
expect(pairBlock).toContain("'0'");
// The connect command must propagate BROWSE_PARENT_PID=0 to serverEnv
const connectBlock = sliceBetween(CLI_SRC, 'Launching headed Chromium', 'Sidebar agent started');
expect(connectBlock).toContain("BROWSE_PARENT_PID");
expect(connectBlock).toContain("serverEnv.BROWSE_PARENT_PID");
});
// 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');
// The ownership check condition must exclude newtab
expect(ownershipBlock).toContain("command !== 'newtab'");
});
});