Skip to content

fix(node-host): clarify unusable node exec cwd failures#97105

Merged
steipete merged 5 commits into
openclaw:mainfrom
clawSean:fix/node-exec-missing-workdir-diagnostics
Jul 9, 2026
Merged

fix(node-host): clarify unusable node exec cwd failures#97105
steipete merged 5 commits into
openclaw:mainfrom
clawSean:fix/node-exec-missing-workdir-diagnostics

Conversation

@clawSean

Copy link
Copy Markdown
Contributor

PR draft — node exec: report an unusable working directory instead of blaming the shell

What Problem This Solves

When exec host=node runs a command and the forwarded working directory does not
exist on the node host, the node host surfaces a misleading error:

spawn /bin/sh ENOENT

/bin/sh is present — the real cause is the missing cwd. Node/libuv performs the
pre-exec chdir(cwd) and, when it fails, reports the failure against the command
(argv[0]) rather than the directory. This exact misattribution drove issue #85202
and 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 main
by #58977 — the gateway no longer forwards its own process.cwd() to remote node
exec. The residual gap is the non-approval node-exec path (security=full /
ask=off, the headless-node default): an explicit forwarded workdir that is
absent on the node still reaches spawn() unvalidated and yields the misleading
spawn <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 cwd is never dropped, rewritten,
or replaced with the node default — only the error text changes, and only when the
spawn failed because the cwd is not a usable directory.

There are two distinct spawn failure modes for a bad cwd, and Node surfaces them
differently:

  • Missing directory → libuv fails the pre-exec chdir and emits an asynchronous
    error event carrying spawn <argv0> ENOENT. The run already failed closed
    (success=false); only the message was misleading. The fix rewrites that message.
  • cwd exists but is a filespawn() throws ENOTDIR synchronously rather
    than emitting error. Because the node host dispatches handleInvoke with void
    (src/node-host/runner.ts), that throw escaped as an unhandled rejection, which the
    global handler (src/index.ts) converts into process.exit(1) — crashing the node
    host instead of failing closed and never sending a result. The fix wraps spawn() so
    this 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:

  • The clarification lives in the node host (runCommand), where the cwd is actually
    interpreted. The gateway cannot stat a remote node's filesystem, so this must be
    node-side.
  • Zero hot-path cost: the statSync runs only on a spawn failure (the async
    error handler or the synchronous spawn() catch), after the command has already
    failed, honoring the "no freshness polling on hot paths" rule.
  • The predicate leaves genuine missing-executable errors untouched: if the cwd
    exists and is a directory, an ENOENT is about argv[0], and the original
    message is preserved.

User Impact

Operators running exec host=node against nodes whose filesystem layout differs
from the gateway (e.g. Docker gateway + systemd Linux nodes, cross-platform) now get
an actionable error:

node exec working directory does not exist on the node host: /gateway/only/path (os reported: spawn /bin/sh ENOENT)

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 of
crashing the node-host process on an unhandled ENOTDIR. No behavior, config, or
compatibility change; the command still fails closed and the cwd is never dropped.

Evidence

  • Unit coverage in src/node-host/invoke.test.ts for clarifyNodeExecCwdSpawnError:
    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.
  • Behavioral coverage for runCommand (real spawn): a missing cwd (async ENOENT) and
    a cwd that is a file (synchronous ENOTDIR throw) both return success=false with the
    clarified message instead of rejecting the promise / crashing the node host.
  • Reproduced the libuv behavior locally on Node 22.22.2: a missing cwd emits an async
    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 the error event.
  • 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/echo allow-always coverage test, which fails identically on clean main
    in this sandbox).
  • oxfmt --check + oxlint on the touched files → clean.
  • tsgo:core (prod, includes src/node-host/invoke.ts) and tsgo:core:test
    (includes src/node-host/invoke.test.ts) → both clean.

Follow-up (sibling surface): the macOS app exec host (runViaMacAppExecHost) spawns on
a 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).

@clawSean

Copy link
Copy Markdown
Contributor Author

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:

  • missing explicit cwd: fail closed with an actionable cwd error instead of misleading spawn /bin/sh ENOENT
  • cwd points at a file: catch synchronous ENOTDIR and return a failed invoke result instead of letting the node host crash on an unhandled rejection

Local validation:

  • direct tsx behavior probe for missing cwd, cwd-is-file, and genuine missing executable preservation ✅
  • touched-file oxfmt --check
  • touched-file oxlint
  • git diff --check upstream/main..HEAD

The full invoke.test.ts file still has the unrelated /bin/echo allow-always sandbox failure here; that same failure reproduces on clean main, so it is not from this diff.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@clawsweeper

clawsweeper Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed July 9, 2026, 6:22 AM ET / 10:22 UTC.

Summary
The branch adds node-host cwd-specific spawn error clarification and catches synchronous cwd ENOTDIR throws, with focused runCommand tests.

PR surface: Source +51, Tests +92. Total +143 across 2 files.

Reproducibility: yes. for source-level reproduction. Current main forwards explicit node cwd into system.run and returns raw child-process spawn errors, and a Node 24 probe reproduced missing-cwd async ENOENT plus file-cwd sync ENOTDIR.

Review metrics: 1 noteworthy metric.

  • Release-owned files touched: 0 modified. The prior review blocker was a direct CHANGELOG.md hunk, and the current PR diff no longer changes release-owned changelog content.

Root-cause cluster
Relationship: fixed_by_candidate
Canonical: #85202
Summary: This PR is a candidate partial fix for the explicit bad-cwd diagnostic and fail-closed portion of the canonical Linux node exec ENOENT report.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🦞 diamond lobster
Patch quality: 🐚 platinum hermit
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • none.

Risk before merge

  • [P1] The reporter's exact remote cwd from the related issue is still not captured, so this PR should be treated as fixing the verified unusable-cwd diagnostic and fail-closed path rather than proving every possible /bin/sh ENOENT cause is gone.
  • [P1] The PR is mergeable but currently behind main, so landing should rely on refreshed exact-head CI or the merge queue rather than the older base SHA alone.

Maintainer options:

  1. Decide the mitigation before merge
    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.
  2. Pause or close
    Do not merge this PR until maintainers decide whether the risk is worth taking.

Next step before merge

  • No ClawSweeper repair lane is needed; the branch has no blocking findings and should proceed through maintainer review plus refreshed exact-head CI or merge queue handling.

Security
Cleared: Cleared: the diff changes node-host child-process error handling and tests without changing command selection, permissions, secrets, dependencies, workflows, or package resolution.

Review details

Best 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 system.run and returns raw child-process spawn errors, and a Node 24 probe reproduced missing-cwd async ENOENT plus file-cwd sync ENOTDIR.

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 changes

Label justifications:

  • P2: The PR fixes a bounded node-host exec diagnostic and fail-closed failure path with limited runtime blast radius.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): Sufficient: the PR body and author comments include after-fix terminal/runtime evidence for missing cwd, file-valued cwd, genuine missing executable preservation, and focused validation commands.
  • proof: sufficient: Contributor real behavior proof is sufficient. Sufficient: the PR body and author comments include after-fix terminal/runtime evidence for missing cwd, file-valued cwd, genuine missing executable preservation, and focused validation commands.
Evidence reviewed

PR surface:

Source +51, Tests +92. Total +143 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 58 7 +51
Tests 1 92 0 +92
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 150 7 +143

What I checked:

  • Repository policy read: Root AGENTS.md was read fully; its review-depth, fallback, runtime hot-path, and changelog ownership rules were relevant to this review. (AGENTS.md:26, 2f66c3c0c845)
  • Repository policy read remainder: The remainder of root AGENTS.md was read through EOF, including release/changelog and platform guidance. (AGENTS.md:261, 2f66c3c0c845)
  • Scoped policy and notes check: No scoped AGENTS.md or maintainer notes were found for src/node-host, so root policy is the applicable repository guidance for the touched files.
  • Current main behavior: Current main passes the supplied cwd directly into spawn(), so an explicit remote cwd is still interpreted by the node host. (src/node-host/invoke.ts:288, 2f66c3c0c845)
  • Current main raw error path: Current main finalizes child error events with err.message, which preserves the misleading spawn /bin/sh ENOENT text when Node reports a cwd chdir failure against argv[0]. (src/node-host/invoke.ts:384, 2f66c3c0c845)
  • PR implementation: At PR head, clarifyNodeExecCwdSpawnError only rewrites ENOENT/ENOTDIR cwd failures, preserves genuine executable errors, catches synchronous spawn throws, and routes async child errors through the same helper. (src/node-host/invoke.ts:273, 163b3377d61a)

Likely related people:

  • steipete: Recent current-main node exec history includes fix(nodes): restore Mac app allowlist execution, and the live PR commits show this person authored the latest runtime cleanup and changelog-removal commits on this branch. (role: recent area contributor and PR fixup author; confidence: high; commits: 8a7242461c37, cf1400beb752, e66cb8897179; files: src/agents/bash-tools.exec-host-node-phases.ts, src/node-host/invoke.ts, src/node-host/invoke.run-command.test.ts)
  • cxbAsDev: Authored the recently merged runCommand stream-error handling fix in the same node-host helper this PR modifies. (role: recent runCommand contributor; confidence: high; commits: ec8129ef1215; files: src/node-host/invoke.ts)
  • Starhappysh: Authored the merged repair that stopped forwarding the gateway default cwd for remote node exec while preserving explicit cwd forwarding for node-side interpretation. (role: introduced related omitted-cwd repair; confidence: high; commits: b6a01e5a3e57, fc76f667c23b; files: src/agents/bash-tools.exec.ts, src/agents/bash-tools.exec-host-node.ts, src/agents/bash-tools.exec-approval-request.ts)
  • renaudcerrato: Authored the merged local invalid-workdir preflight repair, which is the closest shipped sibling surface to this remote node-host cwd diagnostic path. (role: adjacent workdir contributor; confidence: medium; commits: 95b97e5b0b5e; files: src/agents/bash-tools.exec-workdir.ts, src/agents/bash-tools.exec.ts)
  • vincentkoc: The live PR is assigned to this person for review, and current-main history includes adjacent node-host invoke work by the same person. (role: current reviewer and adjacent contributor; confidence: medium; commits: 58de2b689fdf; files: src/node-host/invoke.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

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
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
Review history (4 earlier review cycles)
  • reviewed 2026-06-29T07:51:57.552Z sha 97f0ddd :: needs maintainer review before merge. :: none
  • reviewed 2026-07-08T09:11:51.262Z sha dce5d17 :: needs maintainer review before merge. :: none
  • reviewed 2026-07-09T09:46:02.586Z sha e66cb88 :: needs changes before merge. :: [P3] Remove the direct changelog edit
  • reviewed 2026-07-09T10:10:10.386Z sha 163b337 :: needs maintainer review before merge. :: none

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. P2 Normal backlog priority with limited blast radius. labels Jun 26, 2026
@vincentkoc vincentkoc self-assigned this Jun 28, 2026
@vincentkoc
vincentkoc requested a review from a team as a code owner June 28, 2026 22:46
@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation channel: discord Channel integration: discord channel: googlechat Channel integration: googlechat channel: line Channel integration: line channel: matrix Channel integration: matrix channel: mattermost Channel integration: mattermost channel: msteams Channel integration: msteams channel: nextcloud-talk Channel integration: nextcloud-talk channel: nostr Channel integration: nostr channel: signal Channel integration: signal channel: slack Channel integration: slack channel: telegram Channel integration: telegram channel: voice-call Channel integration: voice-call channel: whatsapp-web Channel integration: whatsapp-web channel: zalo Channel integration: zalo app: android App: android app: ios App: ios app: macos App: macos app: web-ui App: web-ui gateway Gateway runtime labels Jun 28, 2026
@vincentkoc
vincentkoc force-pushed the fix/node-exec-missing-workdir-diagnostics branch from ab9a19a to 0245de5 Compare June 28, 2026 22:48
@openclaw-barnacle openclaw-barnacle Bot removed docs Improvements or additions to documentation channel: discord Channel integration: discord channel: googlechat Channel integration: googlechat channel: line Channel integration: line channel: matrix Channel integration: matrix channel: mattermost Channel integration: mattermost channel: msteams Channel integration: msteams channel: nextcloud-talk Channel integration: nextcloud-talk channel: nostr Channel integration: nostr channel: signal Channel integration: signal channel: slack Channel integration: slack channel: telegram Channel integration: telegram labels Jun 28, 2026
@clawSean

Copy link
Copy Markdown
Contributor Author

Rebased current head 97f0ddd57a onto latest main (0ce10d7793) — 322 commits ahead.

The prior CI failures (check-lint, check-test-types, check-additional-extension-bundled, checks-node-compact-small-*) were all in files unrelated to this PR's changes (src/node-host/invoke.ts and its test):

  • check-lint: extensions/telegram/src/reasoning-lane-coordinator.ts no-unnecessary-boolean-literal-compare — pre-existing main issue, now fixed on main
  • check-test-types: type errors in src/agents/embedded-agent-* — pre-existing main issue, now fixed on main

This PR's only touched files (src/node-host/invoke.ts, src/node-host/invoke.test.ts) are unchanged. Rebase was conflict-free (1 commit onto 322 main commits).

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@steipete

steipete commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Merged via squash.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: S status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants