mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-01 19:25:10 +02:00
fix: pair-agent tunnel drops after 15s (v0.15.15.1) (#868)
* fix: remove stray `domains` reference crashing connect command The connect command's status fetch had an undefined `domains` variable in the JSON body, causing "Connect failed: domains is not defined" and preventing headed mode from initializing properly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: pair-agent server dies 15s after CLI exits The server monitors BROWSE_PARENT_PID and self-terminates when the parent exits. For pair-agent, the connect subprocess is the parent, so the server dies 15s after connect finishes. Disable parent-PID monitoring for pair-agent sessions so the server outlives the CLI. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v0.15.15.1) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: newtab blocked by tab ownership check for scoped tokens The tab ownership check ran before the newtab handler, checking the active tab (owned by root) against the scoped token. Since the scoped token doesn't own the root tab, newtab returned 403. Skip the ownership check for newtab since it creates a new tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: regression tests for pair-agent tunnel fixes Three source-level tests covering the bugs fixed on this branch: - connect status fetch has no undefined variable references (domains) - pair-agent disables parent PID monitoring (BROWSE_PARENT_PID=0) - newtab excluded from tab ownership check for scoped tokens Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
+9
-4
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user