fix(exec): skip gateway cwd injection for remote node host

When exec runs with host=node and no explicit cwd is provided, the
gateway was injecting its own process.cwd() as the default working
directory. In cross-platform setups (e.g. Linux gateway + Windows node),
this gateway-local path does not exist on the node, causing
"SYSTEM_RUN_DENIED: approval requires an existing canonical cwd".

This change detects when no explicit workdir was provided (neither via
the tool call params.workdir nor via agent defaults.cwd) and passes
undefined instead of the gateway cwd. This lets the remote node use its
own default working directory.

Changes:
- bash-tools.exec.ts: Track whether workdir was explicitly provided;
  when host=node and no explicit workdir, pass undefined instead of
  gateway process.cwd()
- bash-tools.exec-host-node.ts: Accept workdir as string | undefined;
  only send cwd to system.run.prepare when defined
- bash-tools.exec-approval-request.ts: Accept workdir as
  string | undefined in HostExecApprovalParams

Fixes #58934

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jianxing zhang
2026-04-01 19:34:44 +08:00
committed by Peter Steinberger
parent 8aceaf5d0f
commit 3b3191ab3a
3 changed files with 20 additions and 9 deletions

View File

@@ -154,7 +154,7 @@ type HostExecApprovalParams = {
commandArgv?: string[];
systemRunPlan?: SystemRunApprovalPlan;
env?: Record<string, string>;
workdir: string;
workdir: string | undefined;
host: "gateway" | "node";
nodeId?: string;
security: ExecSecurity;
@@ -245,3 +245,4 @@ export async function registerExecApprovalRequestForHostOrThrow(
throw new Error(`Exec approval registration failed: ${String(err)}`, { cause: err });
}
}

View File

@@ -35,7 +35,7 @@ import { listNodes, resolveNodeIdFromList } from "./tools/nodes-utils.js";
export type ExecuteNodeHostCommandParams = {
command: string;
workdir: string;
workdir: string | undefined;
env: Record<string, string>;
requestedEnv?: Record<string, string>;
requestedNode?: string;
@@ -108,7 +108,7 @@ export async function executeNodeHostCommand(
params: {
command: argv,
rawCommand: params.command,
cwd: params.workdir,
...(params.workdir != null ? { cwd: params.workdir } : {}),
agentId: params.agentId,
sessionKey: params.sessionKey,
},
@@ -121,7 +121,7 @@ export async function executeNodeHostCommand(
}
const runArgv = prepared.plan.argv;
const runRawCommand = prepared.plan.commandText;
const runCwd = prepared.plan.cwd ?? params.workdir;
const runCwd = prepared.plan.cwd ?? params.workdir ?? undefined;
const runAgentId = prepared.plan.agentId ?? params.agentId;
const runSessionKey = prepared.plan.sessionKey ?? params.sessionKey;
@@ -453,3 +453,4 @@ export async function executeNodeHostCommand(
} satisfies ExecToolDetails,
};
}

View File

@@ -1343,8 +1343,9 @@ export function createExecTool(
].join("\n"),
);
}
const hasExplicitWorkdir = !!(params.workdir?.trim() || defaults?.cwd);
const rawWorkdir = params.workdir?.trim() || defaults?.cwd || process.cwd();
let workdir = rawWorkdir;
let workdir: string | undefined = rawWorkdir;
let containerWorkdir = sandbox?.containerWorkdir;
if (sandbox) {
const resolved = await resolveSandboxWorkdir({
@@ -1354,10 +1355,17 @@ export function createExecTool(
});
workdir = resolved.hostWorkdir;
containerWorkdir = resolved.containerWorkdir;
} else if (host !== "node") {
// Skip local workdir resolution for remote node execution: the remote node's
// filesystem is not visible to the gateway, so resolveWorkdir() would incorrectly
// fall back to the gateway's cwd. The node is responsible for validating its own cwd.
} else if (host === "node") {
// For remote node execution, only forward an explicit cwd provided by the
// user or agent config. When no explicit cwd was given, the gateway's own
// process.cwd() is meaningless on the remote node (especially cross-platform,
// e.g. Linux gateway + Windows node) and would cause
// "SYSTEM_RUN_DENIED: approval requires an existing canonical cwd".
// Passing undefined lets the node use its own default working directory.
if (!hasExplicitWorkdir) {
workdir = undefined;
}
} else {
workdir = resolveWorkdir(rawWorkdir, warnings);
}
rejectExecApprovalShellCommand(params.command);
@@ -1627,3 +1635,4 @@ export function createExecTool(
}
export const execTool = createExecTool();