Skip to content

Commit 97e1437

Browse files
committed
fix: clarify exec node routing guidance
1 parent 466e174 commit 97e1437

18 files changed

Lines changed: 315 additions & 75 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ Docs: https://docs.openclaw.ai
5151

5252
- Security: preserve restrictive plugin-only tool allowlists, require owner access for `/allowlist add` and `/allowlist remove`, fail closed when `before_tool_call` hooks crash, block browser SSRF redirect bypasses earlier, and keep non-interactive auth-choice inference scoped to bundled and already-trusted plugins. (#58476, #59836, #59822, #58771, #59120) Thanks @eleqtrizit and @pgondhi987.
5353
- Auto-reply: unify reply lifecycle ownership across preflight compaction, session rotation, CLI-backed runs, and gateway restart handling so `/stop` and same-session overlap checks target the right active turn and restart-interrupted turns return the restart notice instead of being silently dropped. (#61267) Thanks @dutifulbob.
54+
- Exec/remote skills: stop advertising `exec host=node` when the current exec policy cannot route to a node, and clarify blocked exec-host override errors with both the requested host and allowed config path.
5455
- Agents/Claude CLI/security: clear inherited Claude Code config-root and plugin-root env overrides like `CLAUDE_CONFIG_DIR` and `CLAUDE_CODE_PLUGIN_*`, so OpenClaw-launched Claude CLI runs cannot be silently pointed at an alternate Claude config/plugin tree with different hooks, plugins, or auth context. Thanks @vincentkoc.
5556
- Agents/Claude CLI/security: clear inherited Claude Code provider-routing and managed-auth env overrides, and mark OpenClaw-launched Claude CLI runs as host-managed, so Claude CLI backdoor sessions cannot be silently redirected to proxy, Bedrock, Vertex, Foundry, or parent-managed token contexts. Thanks @vincentkoc.
5657
- Agents/Claude CLI/security: force host-managed Claude CLI backdoor runs to `--setting-sources user`, even under custom backend arg overrides, so repo-local `.claude` project/local settings, hooks, and plugin discovery do not silently execute inside non-interactive OpenClaw sessions. Thanks @vincentkoc.

src/agents/agent-command.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ import { updateSessionStoreAfterAgentRun } from "./command/session-store.js";
6565
import { resolveSession } from "./command/session.js";
6666
import type { AgentCommandIngressOpts, AgentCommandOpts } from "./command/types.js";
6767
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js";
68+
import { canExecRequestNode } from "./exec-defaults.js";
6869
import { AGENT_LANE_SUBAGENT } from "./lanes.js";
6970
import { LiveSessionModelSwitchError } from "./live-model-switch.js";
7071
import { loadModelCatalog } from "./model-catalog.js";
@@ -508,7 +509,16 @@ async function agentCommandInternal(
508509
const skillsSnapshot = needsSkillsSnapshot
509510
? buildWorkspaceSkillSnapshot(workspaceDir, {
510511
config: cfg,
511-
eligibility: { remote: getRemoteSkillEligibility() },
512+
eligibility: {
513+
remote: getRemoteSkillEligibility({
514+
advertiseExecNode: canExecRequestNode({
515+
cfg,
516+
sessionEntry,
517+
sessionKey,
518+
agentId: sessionAgentId,
519+
}),
520+
}),
521+
},
512522
snapshotVersion: skillsSnapshotVersion,
513523
skillFilter,
514524
agentId: sessionAgentId,

src/agents/bash-tools.exec-runtime.test.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,9 @@ describe("resolveExecTarget", () => {
126126
elevatedRequested: false,
127127
sandboxAvailable: true,
128128
}),
129-
).toThrow("exec host not allowed");
129+
).toThrow(
130+
"exec host not allowed (requested gateway; configured host is auto; set tools.exec.host=gateway or auto to allow this override).",
131+
);
130132
});
131133

132134
it("allows per-call host=sandbox override when configured host is auto", () => {
@@ -153,7 +155,9 @@ describe("resolveExecTarget", () => {
153155
elevatedRequested: false,
154156
sandboxAvailable: false,
155157
}),
156-
).toThrow("exec host not allowed");
158+
).toThrow(
159+
"exec host not allowed (requested gateway; configured host is node; set tools.exec.host=gateway or auto to allow this override).",
160+
);
157161
});
158162

159163
it("allows explicit auto request when configured host is auto", () => {
@@ -180,7 +184,9 @@ describe("resolveExecTarget", () => {
180184
elevatedRequested: false,
181185
sandboxAvailable: true,
182186
}),
183-
).toThrow("exec host not allowed");
187+
).toThrow(
188+
"exec host not allowed (requested auto; configured host is gateway; set tools.exec.host=auto to allow this override).",
189+
);
184190
});
185191

186192
it("allows exact node matches", () => {

src/agents/bash-tools.exec-runtime.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,9 +259,17 @@ export function resolveExecTarget(params: {
259259
sandboxAvailable: params.sandboxAvailable,
260260
})
261261
) {
262+
const allowedConfig = Array.from(
263+
new Set(
264+
requestedTarget === "gateway" && !params.sandboxAvailable
265+
? ["gateway", "auto"]
266+
: [renderExecTargetLabel(requestedTarget), "auto"],
267+
),
268+
).join(" or ");
262269
throw new Error(
263270
`exec host not allowed (requested ${renderExecTargetLabel(requestedTarget)}; ` +
264-
`configure tools.exec.host=${renderExecTargetLabel(requestedTarget)} to allow).`,
271+
`configured host is ${renderExecTargetLabel(configuredTarget)}; ` +
272+
`set tools.exec.host=${allowedConfig} to allow this override).`,
265273
);
266274
}
267275
const selectedTarget = requestedTarget ?? configuredTarget;

src/agents/exec-defaults.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { describe, expect, it } from "vitest";
2+
import type { SessionEntry } from "../config/sessions.js";
3+
import { resolveExecDefaults } from "./exec-defaults.js";
4+
5+
describe("resolveExecDefaults", () => {
6+
it("does not advertise node routing when exec host is pinned to gateway", () => {
7+
expect(
8+
resolveExecDefaults({
9+
cfg: {
10+
tools: {
11+
exec: {
12+
host: "gateway",
13+
},
14+
},
15+
},
16+
sandboxAvailable: false,
17+
}).canRequestNode,
18+
).toBe(false);
19+
});
20+
21+
it("keeps node routing available when exec host is auto", () => {
22+
expect(
23+
resolveExecDefaults({
24+
cfg: {
25+
tools: {
26+
exec: {
27+
host: "auto",
28+
},
29+
},
30+
},
31+
sandboxAvailable: true,
32+
}),
33+
).toMatchObject({
34+
host: "auto",
35+
effectiveHost: "sandbox",
36+
canRequestNode: true,
37+
});
38+
});
39+
40+
it("honors session-level exec host overrides", () => {
41+
const sessionEntry = {
42+
execHost: "node",
43+
} as SessionEntry;
44+
expect(
45+
resolveExecDefaults({
46+
cfg: {
47+
tools: {
48+
exec: {
49+
host: "gateway",
50+
},
51+
},
52+
},
53+
sessionEntry,
54+
sandboxAvailable: false,
55+
}).canRequestNode,
56+
).toBe(true);
57+
});
58+
});

src/agents/exec-defaults.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import type { OpenClawConfig } from "../config/config.js";
2+
import type { SessionEntry } from "../config/sessions.js";
3+
import type { ExecAsk, ExecHost, ExecSecurity, ExecTarget } from "../infra/exec-approvals.js";
4+
import { resolveAgentConfig, resolveSessionAgentId } from "./agent-scope.js";
5+
import { isRequestedExecTargetAllowed, resolveExecTarget } from "./bash-tools.exec-runtime.js";
6+
import { resolveSandboxRuntimeStatus } from "./sandbox/runtime-status.js";
7+
8+
type ResolvedExecConfig = {
9+
host?: ExecTarget;
10+
security?: ExecSecurity;
11+
ask?: ExecAsk;
12+
node?: string;
13+
};
14+
15+
function resolveExecConfigState(params: {
16+
cfg?: OpenClawConfig;
17+
sessionEntry?: SessionEntry;
18+
agentId?: string;
19+
sessionKey?: string;
20+
}): {
21+
cfg: OpenClawConfig;
22+
host: ExecTarget;
23+
agentExec?: ResolvedExecConfig;
24+
globalExec?: ResolvedExecConfig;
25+
} {
26+
const cfg = params.cfg ?? {};
27+
const resolvedAgentId =
28+
params.agentId ??
29+
resolveSessionAgentId({
30+
sessionKey: params.sessionKey,
31+
config: cfg,
32+
});
33+
const globalExec = cfg.tools?.exec;
34+
const agentExec = resolvedAgentId
35+
? resolveAgentConfig(cfg, resolvedAgentId)?.tools?.exec
36+
: undefined;
37+
const host =
38+
(params.sessionEntry?.execHost as ExecTarget | undefined) ??
39+
(agentExec?.host as ExecTarget | undefined) ??
40+
(globalExec?.host as ExecTarget | undefined) ??
41+
"auto";
42+
return {
43+
cfg,
44+
host,
45+
agentExec,
46+
globalExec,
47+
};
48+
}
49+
50+
export function canExecRequestNode(params: {
51+
cfg?: OpenClawConfig;
52+
sessionEntry?: SessionEntry;
53+
agentId?: string;
54+
sessionKey?: string;
55+
}): boolean {
56+
const { host } = resolveExecConfigState(params);
57+
return isRequestedExecTargetAllowed({
58+
configuredTarget: host,
59+
requestedTarget: "node",
60+
});
61+
}
62+
63+
export function resolveExecDefaults(params: {
64+
cfg?: OpenClawConfig;
65+
sessionEntry?: SessionEntry;
66+
agentId?: string;
67+
sessionKey?: string;
68+
sandboxAvailable?: boolean;
69+
}): {
70+
host: ExecTarget;
71+
effectiveHost: ExecHost;
72+
security: ExecSecurity;
73+
ask: ExecAsk;
74+
node?: string;
75+
canRequestNode: boolean;
76+
} {
77+
const { cfg, host, agentExec, globalExec } = resolveExecConfigState(params);
78+
const sandboxAvailable =
79+
params.sandboxAvailable ??
80+
(params.sessionKey
81+
? resolveSandboxRuntimeStatus({
82+
cfg,
83+
sessionKey: params.sessionKey,
84+
}).sandboxed
85+
: false);
86+
const resolved = resolveExecTarget({
87+
configuredTarget: host,
88+
elevatedRequested: false,
89+
sandboxAvailable,
90+
});
91+
return {
92+
host,
93+
effectiveHost: resolved.effectiveHost,
94+
security:
95+
(params.sessionEntry?.execSecurity as ExecSecurity | undefined) ??
96+
agentExec?.security ??
97+
globalExec?.security ??
98+
"deny",
99+
ask:
100+
(params.sessionEntry?.execAsk as ExecAsk | undefined) ??
101+
agentExec?.ask ??
102+
globalExec?.ask ??
103+
"on-miss",
104+
node: params.sessionEntry?.execNode ?? agentExec?.node ?? globalExec?.node,
105+
canRequestNode: isRequestedExecTargetAllowed({
106+
configuredTarget: host,
107+
requestedTarget: "node",
108+
}),
109+
};
110+
}

src/agents/sandbox/context.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import { DEFAULT_BROWSER_EVALUATE_ENABLED } from "../../plugin-sdk/browser-profiles.js";
1010
import { defaultRuntime } from "../../runtime.js";
1111
import { resolveUserPath } from "../../utils.js";
12+
import { canExecRequestNode } from "../exec-defaults.js";
1213
import { syncSkillsToWorkspace } from "../skills.js";
1314
import { DEFAULT_AGENT_WORKSPACE_DIR } from "../workspace.js";
1415
import { requireSandboxBackendFactory } from "./backend.js";
@@ -58,7 +59,15 @@ async function ensureSandboxWorkspaceLayout(params: {
5859
targetWorkspaceDir: sandboxWorkspaceDir,
5960
config: params.config,
6061
agentId: params.agentId,
61-
eligibility: { remote: getRemoteSkillEligibility() },
62+
eligibility: {
63+
remote: getRemoteSkillEligibility({
64+
advertiseExecNode: canExecRequestNode({
65+
cfg: params.config,
66+
sessionKey: rawSessionKey,
67+
agentId: params.agentId,
68+
}),
69+
}),
70+
},
6271
});
6372
} catch (error) {
6473
const message = error instanceof Error ? error.message : JSON.stringify(error);

src/auto-reply/reply/commands-system-prompt.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ vi.mock("../../agents/skills/refresh.js", () => ({
2525
}));
2626

2727
vi.mock("../../agents/agent-scope.js", () => ({
28+
resolveAgentConfig: vi.fn(() => undefined),
2829
resolveSessionAgentIds: vi.fn(() => ({ sessionAgentId: "main" })),
2930
}));
3031

src/auto-reply/reply/commands-system-prompt.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { AgentTool } from "@mariozechner/pi-agent-core";
22
import { resolveSessionAgentIds } from "../../agents/agent-scope.js";
33
import { resolveBootstrapContextForRun } from "../../agents/bootstrap-files.js";
4+
import { canExecRequestNode } from "../../agents/exec-defaults.js";
45
import { resolveDefaultModelForAgent } from "../../agents/model-selection.js";
56
import type { EmbeddedContextFile } from "../../agents/pi-embedded-helpers.js";
67
import { createOpenClawCodingTools } from "../../agents/pi-tools.js";
@@ -38,23 +39,32 @@ export async function resolveCommandsSystemPromptBundle(
3839
sessionKey: params.sessionKey,
3940
sessionId: params.sessionEntry?.sessionId,
4041
});
42+
const sandboxRuntime = resolveSandboxRuntimeStatus({
43+
cfg: params.cfg,
44+
sessionKey: params.ctx.SessionKey ?? params.sessionKey,
45+
});
4146
const skillsSnapshot = (() => {
4247
try {
4348
return buildWorkspaceSkillSnapshot(workspaceDir, {
4449
config: params.cfg,
4550
agentId: sessionAgentId,
46-
eligibility: { remote: getRemoteSkillEligibility() },
51+
eligibility: {
52+
remote: getRemoteSkillEligibility({
53+
advertiseExecNode: canExecRequestNode({
54+
cfg: params.cfg,
55+
sessionEntry: params.sessionEntry,
56+
sessionKey: params.sessionKey,
57+
agentId: sessionAgentId,
58+
}),
59+
}),
60+
},
4761
snapshotVersion: getSkillsSnapshotVersion(workspaceDir),
4862
});
4963
} catch {
5064
return { prompt: "", skills: [], resolvedSkills: [] };
5165
}
5266
})();
5367
const skillsPrompt = skillsSnapshot.prompt ?? "";
54-
const sandboxRuntime = resolveSandboxRuntimeStatus({
55-
cfg: params.cfg,
56-
sessionKey: params.ctx.SessionKey ?? params.sessionKey,
57-
});
5868
const tools = (() => {
5969
try {
6070
return createOpenClawCodingTools({

0 commit comments

Comments
 (0)