mirror of
https://github.com/garrytan/gstack.git
synced 2026-07-06 16:17:56 +02:00
fix(browse): identity-based terminal-agent kill replaces pkill regex
Commit 0 of the v1.44 long-lived-sidebar PR — foundation for the watchdog
and removes a latent cross-session footgun.
`pkill -f terminal-agent\.ts` (cli.ts spawn site + server.ts shutdown) matched
by argv regex and would kill ANY process whose argv contained the string —
sibling gstack sessions on the same host, an editor with the file open, a
second `$B connect` run. Identity-based PID kill via a new helper module
removes that whole class of bug.
* New `browse/src/terminal-agent-control.ts`: `readAgentRecord`,
`writeAgentRecord`, `clearAgentRecord`, `killAgentByRecord`. Validates
PID liveness via `isProcessAlive` before signaling (PID-reuse defense).
* `terminal-agent.ts` writes `<stateDir>/terminal-agent-pid` (JSON
`{pid, gen, startedAt}`) at boot; clears on SIGTERM/SIGINT.
* New per-boot `CURRENT_GEN` (16-byte random); `/internal/*` callers can
include `X-Browse-Gen` to defend against split-brain in the upcoming
watchdog. Absent header is accepted (backward compat); mismatch returns
409. New `checkInternalAuth` helper centralizes bearer + gen checks.
* New `/internal/healthz` route — agent liveness probe used by the
upcoming watchdog (returns pid/gen/sessions, no claude-binary lookup).
* `cli.ts` and `server.ts` both call `killAgentByRecord` instead of pkill.
* `ServerConfig.ownsTerminalAgent` JSDoc updated; the gated teardown now
runs 4 side effects (was 3) — adds the new agent-record unlink.
Test changes:
* New `browse/test/terminal-agent-pid-identity.test.ts` — static-grep
tripwire that fails CI if any source file re-introduces `pkill ...
terminal-agent` or `spawnSync('pkill', ...)`; round-trips
write/read/clear; verifies killAgentByRecord no-ops on dead PIDs.
* `browse/test/server-embedder-terminal-port.test.ts` rewritten to
intercept `process.kill` (not `child_process.spawnSync`); writes a
sentinel agent-record with a guaranteed-dead PID; asserts probe-only
(signal 0) calls, no termination signals; verifies all 3 discovery
files including the new terminal-agent-pid.
Closes TODOS.md P3 ("Identity-based terminal-agent kill").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+23
-13
@@ -43,6 +43,7 @@ import { inspectElement, modifyStyle, resetModifications, getModificationHistory
|
||||
// Bun.spawn used instead of child_process.spawn (compiled bun binaries
|
||||
// fail posix_spawn on all executables including /bin/bash)
|
||||
import { safeUnlink, safeUnlinkQuiet, safeKill } from './error-handling';
|
||||
import { readAgentRecord, killAgentByRecord, clearAgentRecord, agentRecordPath } from './terminal-agent-control';
|
||||
import { sanitizeBody, stripLoneSurrogateEscapes } from './sanitize';
|
||||
import { startSocksBridge, testUpstream, type BridgeHandle } from './socks-bridge';
|
||||
import { parseProxyConfig, toUpstreamConfig, ProxyConfigError } from './proxy-config';
|
||||
@@ -207,31 +208,34 @@ export interface ServerConfig {
|
||||
beforeRoute?: (req: Request, surface: Surface, auth: TokenInfo | null) => Promise<Response | null>;
|
||||
/**
|
||||
* Whether gstack owns the lifecycle of the terminal-agent process and its
|
||||
* discovery files (`<stateDir>/terminal-port`, `<stateDir>/terminal-internal-token`).
|
||||
* discovery files (`<stateDir>/terminal-port`, `<stateDir>/terminal-internal-token`,
|
||||
* `<stateDir>/terminal-agent-pid`).
|
||||
*
|
||||
* When true (default), shutdown() runs three side effects:
|
||||
* 1. `pkill -f terminal-agent\.ts` — regex-broad, matches ANY process whose
|
||||
* command line contains `terminal-agent.ts` on this host (including
|
||||
* sibling gstack sessions). Pre-existing CLI behavior, not introduced by
|
||||
* this flag. Identity-based PID kill is a separate followup (see TODOS).
|
||||
* When true (default), shutdown() runs four side effects:
|
||||
* 1. Identity-based kill via `killAgentByRecord(readAgentRecord(stateDir))`
|
||||
* (v1.44+). Only signals the PID recorded by THIS daemon's agent.
|
||||
* Replaced the historical `pkill -f terminal-agent\.ts` regex that
|
||||
* matched sibling gstack sessions on the same host — see
|
||||
* terminal-agent-control.ts for rationale.
|
||||
* 2. `safeUnlinkQuiet(<stateDir>/terminal-port)`
|
||||
* 3. `safeUnlinkQuiet(<stateDir>/terminal-internal-token)`
|
||||
* 4. `safeUnlinkQuiet(<stateDir>/terminal-agent-pid)` (the v1.44 record)
|
||||
*
|
||||
* This is correct for gstack's CLI path, which spawns `terminal-agent.ts` as
|
||||
* the producer of those files (see cli.ts:1037-1063).
|
||||
*
|
||||
* Embedders (gbrowser phoenix overlay, future hosts) that run their own PTY
|
||||
* server and write those files themselves should pass `false`. When `false`,
|
||||
* the embedder owns BOTH the agent process AND both discovery files —
|
||||
* terminal-agent.ts's own SIGTERM cleanup only removes `terminal-port`
|
||||
* (see terminal-agent.ts:558), so the internal-token file is the embedder's
|
||||
* full responsibility.
|
||||
* the embedder owns BOTH the agent process AND all three discovery files.
|
||||
* Note that terminal-agent.ts's own SIGTERM cleanup removes `terminal-port`
|
||||
* and `terminal-agent-pid` (the agent writes both at boot), so embedders
|
||||
* that pre-launch their own agent must ensure their cleanup matches.
|
||||
*
|
||||
* Polarity note: this differs from `xvfb?` and `proxyBridge?`, which gate by
|
||||
* the *presence* of a caller-owned handle (presence ⇒ don't close). This
|
||||
* field gates by an explicit boolean because there is no handle object —
|
||||
* the terminal-agent is started elsewhere (cli.ts), and shutdown's only
|
||||
* reference is the regex-based pkill + the file paths.
|
||||
* reference is the PID record + the file paths.
|
||||
*/
|
||||
ownsTerminalAgent?: boolean;
|
||||
}
|
||||
@@ -1319,14 +1323,20 @@ export function buildFetchHandler(cfg: ServerConfig): ServerHandle {
|
||||
|
||||
console.log('[browse] Shutting down...');
|
||||
if (ownsTerminalAgent) {
|
||||
// Identity-based kill (v1.44+). Replaces the v1.43- `pkill -f
|
||||
// terminal-agent\.ts` regex teardown which matched sibling gstack
|
||||
// sessions on the same host. Only the PID recorded in
|
||||
// `<stateDir>/terminal-agent-pid` by THIS daemon's agent is signaled.
|
||||
try {
|
||||
const { spawnSync } = require('child_process');
|
||||
spawnSync('pkill', ['-f', 'terminal-agent\\.ts'], { stdio: 'ignore', timeout: 3000 });
|
||||
const stateDir = path.dirname(config.stateFile);
|
||||
const record = readAgentRecord(stateDir);
|
||||
if (record) killAgentByRecord(record, 'SIGTERM');
|
||||
} catch (err: any) {
|
||||
console.warn('[browse] Failed to kill terminal-agent:', err.message);
|
||||
}
|
||||
safeUnlinkQuiet(path.join(path.dirname(config.stateFile), 'terminal-port'));
|
||||
safeUnlinkQuiet(path.join(path.dirname(config.stateFile), 'terminal-internal-token'));
|
||||
safeUnlinkQuiet(agentRecordPath(path.dirname(config.stateFile)));
|
||||
}
|
||||
try { detachSession(); } catch (err: any) {
|
||||
console.warn('[browse] Failed to detach CDP session:', err.message);
|
||||
|
||||
Reference in New Issue
Block a user