fix(node-host): clarify unusable node exec cwd failures#97105
Conversation
|
Replacement for closed #85543 after re-checking the close rationale and related work. This intentionally does not restore the rejected cwd-dropping/shell-fallback behavior. #58977 already fixed the omitted-workdir half by not forwarding gateway default cwd to remote node exec. This PR only handles the residual explicit bad-cwd cases on the node host:
Local validation:
The full @clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
Codex review: needs maintainer review before merge. Reviewed July 9, 2026, 6:22 AM ET / 10:22 UTC. Summary PR surface: Source +51, Tests +92. Total +143 across 2 files. Reproducibility: yes. for source-level reproduction. Current main forwards explicit node cwd into Review metrics: 1 noteworthy metric.
Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land the node-host diagnostic and fail-closed fix with its focused tests after normal exact-head CI and maintainer review; track macOS companion parity separately if maintainers want that sibling surface covered. Do we have a high-confidence way to reproduce the issue? Yes for source-level reproduction. Current main forwards explicit node cwd into Is this the best way to solve the issue? Yes. The node host is the right layer because it interprets the remote cwd, and this patch preserves fail-closed behavior instead of dropping cwd or adding shell fallback behavior. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 2f66c3c0c845. Label changesLabel justifications:
Evidence reviewedPR surface: Source +51, Tests +92. Total +143 across 2 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
Review history (4 earlier review cycles)
|
ab9a19a to
0245de5
Compare
|
Rebased current head The prior CI failures (
This PR's only touched files ( @clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
# Conflicts: # CHANGELOG.md
|
Merged via squash.
|
PR draft — node exec: report an unusable working directory instead of blaming the shell
What Problem This Solves
When
exec host=noderuns a command and the forwarded working directory does notexist on the node host, the node host surfaces a misleading error:
/bin/shis present — the real cause is the missingcwd. Node/libuv performs thepre-exec
chdir(cwd)and, when it fails, reports the failure against the command(
argv[0]) rather than the directory. This exact misattribution drove issue #85202and three revisions of the closed PR #85543, all of which chased a non-existent
"missing shell" problem (shell fallback) that could never fix a missing-directory
error.
The omitted-cwd case (the headline #85202 reproduction) is already fixed on
mainby #58977 — the gateway no longer forwards its own
process.cwd()to remote nodeexec. The residual gap is the non-approval node-exec path (
security=full/ask=off, the headless-node default): an explicit forwarded workdir that isabsent on the node still reaches
spawn()unvalidated and yields the misleadingspawn <shell> ENOENT. The approval path already fails closed with a clear message(
approval requires an existing canonical cwd); the non-approval path does not.Why This Change Was Made
This is a diagnostic-only change: the supplied
cwdis never dropped, rewritten,or replaced with the node default — only the error text changes, and only when the
spawn failed because the
cwdis not a usable directory.There are two distinct spawn failure modes for a bad
cwd, and Node surfaces themdifferently:
chdirand emits an asynchronouserrorevent carryingspawn <argv0> ENOENT. The run already failed closed(
success=false); only the message was misleading. The fix rewrites that message.cwdexists but is a file →spawn()throwsENOTDIRsynchronously ratherthan emitting
error. Because the node host dispatcheshandleInvokewithvoid(
src/node-host/runner.ts), that throw escaped as an unhandled rejection, which theglobal handler (
src/index.ts) converts intoprocess.exit(1)— crashing the nodehost instead of failing closed and never sending a result. The fix wraps
spawn()sothis case is routed through the same fail-closed result and clarified message.
Deliberately not done: dropping a missing/explicit cwd and falling back to the
node's default directory. That was the rejected shape of the prior PR #85543 and
would silently run a command in the wrong place — a fail-closed regression.
Design notes:
runCommand), where the cwd is actuallyinterpreted. The gateway cannot stat a remote node's filesystem, so this must be
node-side.
statSyncruns only on a spawn failure (the asyncerrorhandler or the synchronousspawn()catch), after the command has alreadyfailed, honoring the "no freshness polling on hot paths" rule.
cwdexists and is a directory, an
ENOENTis aboutargv[0], and the originalmessage is preserved.
User Impact
Operators running
exec host=nodeagainst nodes whose filesystem layout differsfrom the gateway (e.g. Docker gateway + systemd Linux nodes, cross-platform) now get
an actionable error:
instead of a message that points at the shell. And when the forwarded path resolves to
a file on the node, the run now fails closed with
node exec working directory is not a directory on the node host: …instead ofcrashing the node-host process on an unhandled
ENOTDIR. No behavior, config, orcompatibility change; the command still fails closed and the
cwdis never dropped.Evidence
src/node-host/invoke.test.tsforclarifyNodeExecCwdSpawnError:missing cwd → clarified; cwd-is-a-file → "is not a directory"; existing cwd (genuine
missing executable) → message preserved; no cwd → preserved; unrelated error code →
preserved.
runCommand(real spawn): a missing cwd (asyncENOENT) anda cwd that is a file (synchronous
ENOTDIRthrow) both returnsuccess=falsewith theclarified message instead of rejecting the promise / crashing the node host.
error(code=ENOENT message="spawn /bin/sh ENOENT"— the command, not the dir),while a cwd that is a file makes
spawn()throw synchronously(
code=ENOTDIR message="spawn ENOTDIR" syscall="spawn"), bypassing theerrorevent.PNPM_CONFIG_MODULES_DIR=… node scripts/run-vitest.mjs src/node-host/invoke.test.ts→ 11 passed; 1 pre-existing, environment-specific failure unrelated to this change
(the
/bin/echoallow-always coverage test, which fails identically on cleanmainin this sandbox).
oxfmt --check+oxlinton the touched files → clean.tsgo:core(prod, includessrc/node-host/invoke.ts) andtsgo:core:test(includes
src/node-host/invoke.test.ts) → both clean.Follow-up (sibling surface): the macOS app exec host (
runViaMacAppExecHost) spawns ona different machine and would need the same clarification in the companion app to cover
that transport; out of scope here and flagged for a separate change.
Fixes the diagnostic half of #85202 (the omitted-cwd half is already fixed by #58977).