Skip to content

fix(terminal): guard the tmux socket derivation and stop NODE_ENV reaching a drive - #363

Merged
anconina merged 2 commits into
comisai:mainfrom
roeikosh:fix/terminal-tmux-socket-derivation-guard
Jul 28, 2026
Merged

fix(terminal): guard the tmux socket derivation and stop NODE_ENV reaching a drive#363
anconina merged 2 commits into
comisai:mainfrom
roeikosh:fix/terminal-tmux-socket-derivation-guard

Conversation

@roeikosh

Copy link
Copy Markdown
Contributor

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.mdx and terminal-tmux-backend.linux.test.ts.

1. The socket-derivation guard the comment promises did not exist.
setup-terminal-tools.ts says the daemon's derivation "MUST match the worker's
own derivation… only a test can catch that mismatch." There was no such test —
the only two tests injecting tmuxSocketForSession supply fakes. The two sides
meet only through COMIS_TERMINAL_DATA_DIR. Both halves are now pinned: the
worker side asserts durableDir() equals terminalWorkerDir(dataDir) for the
same 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-session on a live
socket commands the existing server rather than starting one.

3. NODE_ENV was named in a test title but never scrubbed. It does not
belong in INTERPRETER_CONTROL_BLOCKLIST — that set mirrors the MCP client's
list and means "startup code-injection vector", which NODE_ENV is 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 through
bwrap --unsetenv (the backend-independent half — the same gap that let
NODE_OPTIONS through on a real VPS).

Related Issue

Fixes #

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • Refactor

Checklist

  • Targeted checks for the changed area pass
  • Full repository validation passes (pnpm validate) — see Additional Notes
  • New or changed behavior has a test that demonstrated the RED state before the production change
  • Documentation updated (if applicable)
  • No secrets or credentials committed
  • Security implications considered
  • The change is focused on one concern and links the related issue, or states the allowed N/A
    reason

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

  • Contribution path: workload (durable-drive recovery correctness) plus a
    defence-in-depth env-scrub change; no public attack path
  • Evidence level: deterministic test for all three fixes; mutation testing
    for the derivation guard; live-tmux run on a real server for the positive
    control
  • Tested profile: Linux (Ubuntu 6.8), Node 22, real tmux 3.x on a local
    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.ts all green
  • Residual risk: the derivation guard pins daemon and worker to the same
    helper 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_ENV strip is asserted at scrubChildEnv and
    JAIL_UNSET_ENV_VARS, not observed inside a live jailed pane. And stripping
    NODE_ENV is a behavior change for any drive that today relies on inheriting
    the 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), so
pnpm validate cannot go green here and the per-package coverage floors never
evaluate. Per CLAUDE.md this change class (sandboxing + daemon behavior) wants
pnpm validate:full on Linux before merge.

…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>
@roeikosh
roeikosh requested a review from anconina as a code owner July 27, 2026 14:22
@roeikosh roeikosh changed the title fix(terminal): test guard the tmux socket derivation and stop NODE_ENV reaching a drive test(terminal): guard the tmux socket derivation and stop NODE_ENV reaching a drive Jul 27, 2026
@roeikosh roeikosh changed the title test(terminal): guard the tmux socket derivation and stop NODE_ENV reaching a drive fix(terminal): guard the tmux socket derivation and stop NODE_ENV reaching a drive Jul 27, 2026
@anconina
anconina enabled auto-merge (squash) July 28, 2026 07:15
@anconina
anconina merged commit af35aa9 into comisai:main Jul 28, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants