fix(terminal): guard the tmux socket derivation and stop NODE_ENV reaching a drive - #363
Merged
anconina merged 2 commits intoJul 28, 2026
Conversation
…ching a drive Three gaps the per-session-socket change left behind. The daemon stamps each durable session's socket with `tmuxSocketPathForSession(terminalWorkerDir(dataDir), id)` and the worker re-derives it from `COMIS_TERMINAL_DATA_DIR`; the wiring comment says the two MUST agree and that only a test can catch a mismatch — no such test existed. Both halves are now pinned: the worker side asserts `durableDir()` equals `terminalWorkerDir(dataDir)` for the same socket, and the daemon side asserts the wiring derives from that dir plus the session id (verified by mutation — passing `deps.dataDir` instead reddens it). A silent divergence would make recover-on-boot probe an empty socket: every durable drive fails to re-attach while the reaper kills nothing. The live per-session-socket test only checked that a fresh server sees its own spawn env, which is true of any fresh server — it would have stayed green if production regressed to one shared socket. It now exercises that regression as a positive control: a session created on a socket that already has a server reads that server's boot-time env, which is the staleness per-session sockets remove. `NODE_ENV` was named in a test title but was never scrubbed. It does not belong in INTERPRETER_CONTROL_BLOCKLIST — that list mirrors the MCP client's and means "startup code-injection vector" — so it is scrubbed as a separate daemon runtime-mode leak, on both env sources and via bwrap `--unsetenv`. Without it a driven `pnpm install` inherits the daemon's `production` and prunes devDependencies in the workspace the user is developing in. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
anconina
enabled auto-merge (squash)
July 28, 2026 07:15
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Follow-up to #358 (review of the merged branch). Three gaps the
per-session-socket change left behind. Stacked on #362 — both touch
terminal-driver.mdxandterminal-tmux-backend.linux.test.ts.1. The socket-derivation guard the comment promises did not exist.
setup-terminal-tools.tssays the daemon's derivation "MUST match the worker'sown derivation… only a test can catch that mismatch." There was no such test —
the only two tests injecting
tmuxSocketForSessionsupply fakes. The two sidesmeet only through
COMIS_TERMINAL_DATA_DIR. Both halves are now pinned: theworker side asserts
durableDir()equalsterminalWorkerDir(dataDir)for thesame socket; the daemon side asserts the wiring derives from that dir plus the
session id, checked on the spy's arguments rather than a re-implemented path
string (a mock that recomputes the path would pass regardless of what production
passed), plus a check that two sessions get two different sockets.
2. The live per-session-socket test could not fail. It asserted only that a
fresh server sees its own spawn env — true of any fresh server — and would have
stayed green if production regressed to one shared socket. It now exercises that
regression as a positive control: a session created on a socket that already has
a server reads that server's boot-time env, because
new-sessionon a livesocket commands the existing server rather than starting one.
3.
NODE_ENVwas named in a test title but never scrubbed. It does notbelong in
INTERPRETER_CONTROL_BLOCKLIST— that set mirrors the MCP client'slist and means "startup code-injection vector", which
NODE_ENVis not.Widening it would blur that meaning and break the mirror. It is scrubbed instead
via a separate
DAEMON_RUNTIME_MODE_BLOCKLIST, on both env sources and throughbwrap
--unsetenv(the backend-independent half — the same gap that letNODE_OPTIONSthrough on a real VPS).Related Issue
Fixes #
Type of Change
Checklist
pnpm validate) — see Additional NotesN/Areason
RED Test Proof
scrubChildEnv — the daemon's NODE_ENV must not reach the drive
▎ strips NODE_ENV on both sources while keeping the rest of the env rich
▎ AssertionError: NODE_ENV must be stripped from the inherited source:
▎ expected { NODE_ENV: 'production', …(2) } to not have property "NODE_ENV"
▎ at packages/skills/src/tools/builtin/terminal-driver/terminal-env-scrub.test.ts:185:78
scrubChildEnv — the daemon's NODE_ENV must not reach the drive
▎ is cleared on the JAILED backend too (bwrap --unsetenv takes the exact name)
▎ AssertionError: expected [ 'BASH_ENV', 'ENV', …(8) ] to include 'NODE_ENV'
▎ at packages/skills/src/tools/builtin/terminal-driver/terminal-env-scrub.test.ts:195:33
unsandboxed drive env
▎ strips NODE_OPTIONS (interpreter vector) AND NODE_ENV (daemon runtime mode)
▎ AssertionError: expected 'production' to be undefined
▎ at packages/skills/src/tools/builtin/terminal-driver/terminal-spawn-plan.test.ts:303:31
Socket-derivation guard (test for already-correct code, so it is green as
written). Verified by mutation instead: replacing
tmuxSocketPathForSession(terminalWorkerDir(deps.dataDir), sessionId) with
written). Verified by mutation instead: replacing
tmuxSocketPathForSession(terminalWorkerDir(deps.dataDir), sessionId) with
tmuxSocketPathForSession(deps.dataDir, sessionId) in setup-terminal-tools.ts
turns it RED, then the mutation was reverted:
wireTerminalTools — the per-session tmux socket the daemon stamps on each descriptor
▎ derives it as tmuxSocketPathForSession(terminalWorkerDir(dataDir), sessionId)
▎ AssertionError: expected "vi.fn()" to be called with arguments:
▎ [ '/tmp/comis-sock-derivation' ]
▎ Number of calls: 0
▎ at packages/daemon/src/wiring/setup-terminal-tools-mr.test.ts:136:34
Evidence and Residual Risk
defence-in-depth env-scrub change; no public attack path
for the derivation guard; live-tmux run on a real server for the positive
control
socket, unit tier outside bwrap; existing state.
docs:check,tsc -b,skills unit (920), mcp-client (280),
lint:security(0 errors),test/architecture/generic-runtime-boundary.test.tsall greenhelper and dir, not to a live re-attach — a real recover-on-boot across a
daemon restart is still only covered by the bwrap tier I could not run here.
The
NODE_ENVstrip is asserted atscrubChildEnvandJAIL_UNSET_ENV_VARS, not observed inside a live jailed pane. And strippingNODE_ENVis a behavior change for any drive that today relies on inheritingthe daemon's
production— intended, but worth a reviewer's eye.Screenshots
N/A
Additional Notes
The bwrap / live-PTY / jsdom test tier fails environmentally on my dev box
(
bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted), sopnpm validatecannot go green here and the per-package coverage floors neverevaluate. Per CLAUDE.md this change class (sandboxing + daemon behavior) wants
pnpm validate:fullon Linux before merge.