mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 02:45:38 -06:00
fix(codex): prevent node process control from targeting gateway sessions (#126253)
This commit is contained in:
committed by
GitHub
parent
30337c8962
commit
fef5fc55f4
@@ -57,18 +57,19 @@ existing per-session OpenClaw process scope for background follow-up. Prefer
|
||||
Codex native shell for ordinary local work.
|
||||
|
||||
With the default `tools.exec.host: "auto"` and no active OpenClaw sandbox,
|
||||
Codex also receives `node_exec` and `node_process` tools for commands on paired
|
||||
nodes. Native shell remains on the Codex app-server host and workspace
|
||||
Codex also receives `node_exec` for commands on paired nodes. Native shell
|
||||
remains on the Codex app-server host and workspace
|
||||
(Gateway-local for the default stdio deployment); `node_exec` selects a node by
|
||||
name or id and keeps OpenClaw's node approval policy in force. If a finite
|
||||
runtime allowlist disables native Code Mode and leaves the turn without an
|
||||
execution environment, OpenClaw keeps its policy-filtered `exec` and `process`
|
||||
tools available instead for direct, unsandboxed execution.
|
||||
name or id, keeps OpenClaw's node approval policy in force, and waits for the
|
||||
remote command to finish. Remote-node background follow-up is not available. If
|
||||
a finite runtime allowlist disables native Code Mode and leaves the turn without
|
||||
an execution environment, OpenClaw keeps its policy-filtered `exec` and
|
||||
`process` tools available instead for direct, unsandboxed execution.
|
||||
|
||||
When `tools.exec.host: "node"` or `/exec host=node` makes the node the session
|
||||
default, OpenClaw hides the Codex-native shell and exposes `node_exec` and
|
||||
`node_process` as the shell path. This keeps the configured execution host from
|
||||
silently falling back to the app-server or Gateway machine.
|
||||
default, OpenClaw hides the Codex-native shell and exposes `node_exec` as the
|
||||
shell path. This keeps the configured execution host from silently falling
|
||||
back to the app-server or Gateway machine.
|
||||
|
||||
`gateway_exec` is not exposed when an active OpenClaw sandbox, a node-default
|
||||
execution policy, memory-flush restrictions, tool allow/deny policy, or
|
||||
|
||||
@@ -1350,8 +1350,12 @@ describe("Codex app-server dynamic tool build", () => {
|
||||
security: { type: "string" },
|
||||
ask: { type: "string" },
|
||||
node: { type: "string" },
|
||||
background: { type: "boolean" },
|
||||
yieldMs: { type: "number" },
|
||||
pty: { type: "boolean" },
|
||||
elevated: { type: "boolean" },
|
||||
},
|
||||
required: ["command", "host", "node"],
|
||||
required: ["command", "host", "node", "background", "yieldMs", "pty", "elevated"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
};
|
||||
@@ -1364,12 +1368,7 @@ describe("Codex app-server dynamic tool build", () => {
|
||||
],
|
||||
details: { status: "running" },
|
||||
});
|
||||
const processTool = createRuntimeDynamicTool("process");
|
||||
setOpenClawCodingToolsFactoryForTests(() => [
|
||||
execTool,
|
||||
processTool,
|
||||
createRuntimeDynamicTool("message"),
|
||||
]);
|
||||
setOpenClawCodingToolsFactoryForTests(() => [execTool, createRuntimeDynamicTool("message")]);
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
@@ -1388,11 +1387,10 @@ describe("Codex app-server dynamic tool build", () => {
|
||||
});
|
||||
|
||||
expect(nativeToolSurfaceEnabled).toBe(false);
|
||||
expect(tools.map((tool) => tool.name)).toEqual(["message", "node_exec", "node_process"]);
|
||||
expect(tools.map((tool) => tool.name)).toEqual(["message", "node_exec"]);
|
||||
const nodeExec = tools.find((tool) => tool.name === "node_exec");
|
||||
const nodeProcess = tools.find((tool) => tool.name === "node_process");
|
||||
expect(nodeExec?.description).toContain("host=node internally");
|
||||
expect(nodeProcess?.description).toContain("background shell sessions");
|
||||
expect(nodeExec?.description).toContain("background follow-up is unavailable");
|
||||
expect(nodeExec?.parameters).toEqual({
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -1410,6 +1408,10 @@ describe("Codex app-server dynamic tool build", () => {
|
||||
node: "model-selected-node",
|
||||
security: "full",
|
||||
ask: "off",
|
||||
background: true,
|
||||
yieldMs: 10,
|
||||
pty: true,
|
||||
elevated: true,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
@@ -1426,7 +1428,7 @@ describe("Codex app-server dynamic tool build", () => {
|
||||
expect(result?.content).toEqual([
|
||||
{
|
||||
type: "text",
|
||||
text: "Command still running (session exec-1, pid 123). Use remote-node background-session control (list/poll/log/write/send-keys/submit/paste/kill/clear/remove) for follow-up when available.",
|
||||
text: "Command still running (session exec-1, pid 123). Remote-node background follow-up is unavailable. Wait for the command to complete.",
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -1456,14 +1458,10 @@ describe("Codex app-server dynamic tool build", () => {
|
||||
});
|
||||
|
||||
expect(runtimePolicyNativeToolSurfaceEnabled).toBe(false);
|
||||
expect(runtimePolicyTools.map((tool) => tool.name)).toEqual([
|
||||
"message",
|
||||
"node_exec",
|
||||
"node_process",
|
||||
]);
|
||||
expect(runtimePolicyTools.map((tool) => tool.name)).toEqual(["message", "node_exec"]);
|
||||
});
|
||||
|
||||
it("exposes selectable node shell tools beside native shell for auto host runs", async () => {
|
||||
it("does not expose Gateway process sessions as remote-node control in auto host runs", async () => {
|
||||
const execTool = {
|
||||
...createRuntimeDynamicTool("exec"),
|
||||
parameters: {
|
||||
@@ -1504,8 +1502,39 @@ describe("Codex app-server dynamic tool build", () => {
|
||||
"gateway_exec",
|
||||
"gateway_process",
|
||||
"node_exec",
|
||||
"node_process",
|
||||
]);
|
||||
const bridge = createCodexDynamicToolBridge({
|
||||
tools,
|
||||
signal: new AbortController().signal,
|
||||
loading: "direct",
|
||||
});
|
||||
const gatewayList = await bridge.handleToolCall({
|
||||
threadId: "auto-thread",
|
||||
turnId: "auto-turn",
|
||||
tool: "gateway_process",
|
||||
callId: "gateway-process-list",
|
||||
arguments: { action: "list" },
|
||||
});
|
||||
expect(gatewayList.success).toBe(true);
|
||||
expect(processTool.execute).toHaveBeenCalledWith(
|
||||
"gateway-process-list",
|
||||
{ action: "list" },
|
||||
expect.any(AbortSignal),
|
||||
undefined,
|
||||
);
|
||||
vi.mocked(processTool.execute).mockClear();
|
||||
const nodeList = await bridge.handleToolCall({
|
||||
threadId: "auto-thread",
|
||||
turnId: "auto-turn",
|
||||
tool: "node_process",
|
||||
callId: "node-process-list",
|
||||
arguments: { action: "list" },
|
||||
});
|
||||
expect(nodeList.success).toBe(false);
|
||||
expect(nodeList.contentItems).toEqual([
|
||||
{ type: "inputText", text: "Unknown OpenClaw tool: node_process" },
|
||||
]);
|
||||
expect(processTool.execute).not.toHaveBeenCalled();
|
||||
const nodeExec = tools.find((tool) => tool.name === "node_exec");
|
||||
expect(nodeExec?.description).toContain("Select the node by name or id");
|
||||
expect(nodeExec?.parameters).toEqual({
|
||||
@@ -1621,13 +1650,7 @@ describe("Codex app-server dynamic tool build", () => {
|
||||
});
|
||||
|
||||
expect(nativeToolSurfaceEnabled).toBe(false);
|
||||
expect(tools.map((tool) => tool.name)).toEqual([
|
||||
"exec",
|
||||
"process",
|
||||
"message",
|
||||
"node_exec",
|
||||
"node_process",
|
||||
]);
|
||||
expect(tools.map((tool) => tool.name)).toEqual(["exec", "process", "message", "node_exec"]);
|
||||
|
||||
const bridge = createCodexDynamicToolBridge({
|
||||
tools,
|
||||
@@ -1711,7 +1734,6 @@ describe("Codex app-server dynamic tool build", () => {
|
||||
"process",
|
||||
"message",
|
||||
"node_exec",
|
||||
"node_process",
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -46,9 +46,8 @@ import {
|
||||
CODEX_GATEWAY_EXEC_DYNAMIC_TOOL_NAME,
|
||||
CODEX_GATEWAY_PROCESS_DYNAMIC_TOOL_NAME,
|
||||
CODEX_NODE_EXEC_DYNAMIC_TOOL_NAME,
|
||||
CODEX_NODE_PROCESS_DYNAMIC_TOOL_NAME,
|
||||
createExecAliasDynamicTool,
|
||||
createProcessAliasDynamicTool,
|
||||
createGatewayProcessAliasDynamicTool,
|
||||
isCodexDynamicToolExcluded,
|
||||
} from "./shell-dynamic-tools.js";
|
||||
import { filterCodexVisionTools } from "./vision-tools.js";
|
||||
@@ -585,7 +584,7 @@ function addGatewayShellDynamicToolsIfAvailable(
|
||||
}),
|
||||
];
|
||||
if (processAliasAvailable && processTool) {
|
||||
toolsToAppend.push(createProcessAliasDynamicTool(processTool, "gateway"));
|
||||
toolsToAppend.push(createGatewayProcessAliasDynamicTool(processTool));
|
||||
}
|
||||
return [...filteredTools, ...toolsToAppend];
|
||||
}
|
||||
@@ -874,35 +873,21 @@ function addNodeShellDynamicToolsIfNeeded(
|
||||
return filteredTools;
|
||||
}
|
||||
const execTool = allTools.find((tool) => normalizeCodexDynamicToolName(tool.name) === "exec");
|
||||
const processTool = allTools.find(
|
||||
(tool) => normalizeCodexDynamicToolName(tool.name) === "process",
|
||||
);
|
||||
if (!execTool || !processTool) {
|
||||
if (!execTool) {
|
||||
return filteredTools;
|
||||
}
|
||||
const toolsToAppend: OpenClawDynamicTool[] = [];
|
||||
if (
|
||||
!isCodexDynamicToolExcluded(input.pluginConfig, ["exec", CODEX_NODE_EXEC_DYNAMIC_TOOL_NAME]) &&
|
||||
!filteredTools.some(
|
||||
(tool) => normalizeCodexDynamicToolName(tool.name) === CODEX_NODE_EXEC_DYNAMIC_TOOL_NAME,
|
||||
)
|
||||
) {
|
||||
toolsToAppend.push(
|
||||
return [
|
||||
...filteredTools,
|
||||
createExecAliasDynamicTool(execTool, { host: "node", node: nodePolicy.node }),
|
||||
);
|
||||
];
|
||||
}
|
||||
if (
|
||||
!isCodexDynamicToolExcluded(input.pluginConfig, [
|
||||
"process",
|
||||
CODEX_NODE_PROCESS_DYNAMIC_TOOL_NAME,
|
||||
]) &&
|
||||
!filteredTools.some(
|
||||
(tool) => normalizeCodexDynamicToolName(tool.name) === CODEX_NODE_PROCESS_DYNAMIC_TOOL_NAME,
|
||||
)
|
||||
) {
|
||||
toolsToAppend.push(createProcessAliasDynamicTool(processTool, "node"));
|
||||
}
|
||||
return toolsToAppend.length > 0 ? [...filteredTools, ...toolsToAppend] : filteredTools;
|
||||
return filteredTools;
|
||||
}
|
||||
function shouldKeepOpenClawShellDynamicTools(
|
||||
input: DynamicToolBuildParams,
|
||||
@@ -956,9 +941,7 @@ function filterCodexDynamicToolsForAllowlist<T extends { name: string }>(
|
||||
(normalized === CODEX_GATEWAY_EXEC_DYNAMIC_TOOL_NAME && allowSet.has("exec")) ||
|
||||
(normalized === CODEX_GATEWAY_PROCESS_DYNAMIC_TOOL_NAME &&
|
||||
(allowSet.has("exec") || allowSet.has("process"))) ||
|
||||
(normalized === CODEX_NODE_EXEC_DYNAMIC_TOOL_NAME && allowSet.has("exec")) ||
|
||||
(normalized === CODEX_NODE_PROCESS_DYNAMIC_TOOL_NAME &&
|
||||
(allowSet.has("exec") || allowSet.has("process")))
|
||||
(normalized === CODEX_NODE_EXEC_DYNAMIC_TOOL_NAME && allowSet.has("exec"))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,10 +9,16 @@ type ExecAliasParams =
|
||||
| { host: "node"; node?: string };
|
||||
|
||||
export const CODEX_NODE_EXEC_DYNAMIC_TOOL_NAME = "node_exec";
|
||||
export const CODEX_NODE_PROCESS_DYNAMIC_TOOL_NAME = "node_process";
|
||||
export const CODEX_GATEWAY_EXEC_DYNAMIC_TOOL_NAME = "gateway_exec";
|
||||
export const CODEX_GATEWAY_PROCESS_DYNAMIC_TOOL_NAME = "gateway_process";
|
||||
const CODEX_EXEC_POLICY_PARAMETER_NAMES = new Set(["host", "security", "ask"]);
|
||||
const CODEX_NODE_EXEC_PARAMETER_NAMES = new Set([
|
||||
"command",
|
||||
"workdir",
|
||||
"env",
|
||||
"timeoutSeconds",
|
||||
"node",
|
||||
]);
|
||||
const PROCESS_FOLLOWUP_TEXT =
|
||||
"Use process (list/poll/log/write/send-keys/submit/paste/kill/clear/remove) for follow-up.";
|
||||
|
||||
@@ -37,11 +43,11 @@ export function createExecAliasDynamicTool(
|
||||
const name = nodeAlias ? CODEX_NODE_EXEC_DYNAMIC_TOOL_NAME : CODEX_GATEWAY_EXEC_DYNAMIC_TOOL_NAME;
|
||||
const description = nodeAlias
|
||||
? pinnedNode
|
||||
? "Run a shell command on the OpenClaw configured remote node for this session. This tool always uses OpenClaw host=node internally and follows the existing node exec approval and allowlist policy. Use remote-node background-session control for follow-up when available. Use Codex's native shell for local app-server work."
|
||||
: "Run a shell command on an OpenClaw remote node. Select the node by name or id when multiple nodes are available. This tool always uses OpenClaw host=node internally and follows the existing node exec approval and allowlist policy. Use remote-node background-session control for follow-up when available. Use Codex's native shell for local app-server work."
|
||||
? "Run a shell command to completion on the OpenClaw configured remote node for this session. This tool always uses OpenClaw host=node internally and follows the existing node exec approval and allowlist policy. Remote-node background follow-up is unavailable. Use Codex's native shell for local app-server work."
|
||||
: "Run a shell command to completion on an OpenClaw remote node. Select the node by name or id when multiple nodes are available. This tool always uses OpenClaw host=node internally and follows the existing node exec approval and allowlist policy. Remote-node background follow-up is unavailable. Use Codex's native shell for local app-server work."
|
||||
: "Run a shell command through OpenClaw on the Gateway host for OpenClaw-managed Gateway environment access, including Secret Store agent-readable environment values and protected egress sentinels. Native Codex shell remains preferred for ordinary local work. This tool always uses OpenClaw host=gateway internally and follows Gateway exec approval and allowlist policy.";
|
||||
const followupText = nodeAlias
|
||||
? "Use remote-node background-session control (list/poll/log/write/send-keys/submit/paste/kill/clear/remove) for follow-up when available."
|
||||
? "Remote-node background follow-up is unavailable. Wait for the command to complete."
|
||||
: gatewayProcessAliasAvailable
|
||||
? "Use gateway_process (list/poll/log/write/send-keys/submit/paste/kill/clear/remove) for follow-up."
|
||||
: "Background session follow-up is unavailable because gateway_process is not exposed. Rerun without background=true and set yieldMs high enough to wait for completion.";
|
||||
@@ -52,6 +58,7 @@ export function createExecAliasDynamicTool(
|
||||
parameters: hideExecDynamicToolParameters(
|
||||
execTool.parameters,
|
||||
!nodeAlias || Boolean(pinnedNode),
|
||||
nodeAlias,
|
||||
),
|
||||
execute: async (toolCallId, args, signal, onUpdate) => {
|
||||
const result = await execTool.execute(
|
||||
@@ -74,18 +81,15 @@ export function createExecAliasDynamicTool(
|
||||
};
|
||||
}
|
||||
|
||||
export function createProcessAliasDynamicTool(
|
||||
export function createGatewayProcessAliasDynamicTool(
|
||||
processTool: OpenClawDynamicTool,
|
||||
host: "gateway" | "node",
|
||||
): OpenClawDynamicTool {
|
||||
const nodeAlias = host === "node";
|
||||
const name = nodeAlias
|
||||
? CODEX_NODE_PROCESS_DYNAMIC_TOOL_NAME
|
||||
: CODEX_GATEWAY_PROCESS_DYNAMIC_TOOL_NAME;
|
||||
const description = nodeAlias
|
||||
? "Manage background shell sessions on OpenClaw remote nodes: list, poll, log, write, send-keys, submit, paste, kill, clear, or remove. Use only for remote-node follow-up; use Codex's native shell session handling for local app-server work."
|
||||
: "Manage background shell sessions in the existing per-session OpenClaw process scope: list, poll, log, write, send-keys, submit, paste, kill, clear, or remove. Use for gateway_exec follow-up; use native Codex shell session handling for ordinary local work.";
|
||||
return { ...processTool, name, description };
|
||||
return {
|
||||
...processTool,
|
||||
name: CODEX_GATEWAY_PROCESS_DYNAMIC_TOOL_NAME,
|
||||
description:
|
||||
"Manage background shell sessions in the existing per-session OpenClaw process scope: list, poll, log, write, send-keys, submit, paste, kill, clear, or remove. Use for gateway_exec follow-up; use native Codex shell session handling for ordinary local work.",
|
||||
};
|
||||
}
|
||||
|
||||
function pinExecDynamicToolArgs(
|
||||
@@ -98,9 +102,12 @@ function pinExecDynamicToolArgs(
|
||||
if (host === "gateway") {
|
||||
return { ...rest, host };
|
||||
}
|
||||
const nodeArgs = Object.fromEntries(
|
||||
Object.entries(rest).filter(([name]) => CODEX_NODE_EXEC_PARAMETER_NAMES.has(name)),
|
||||
);
|
||||
const node = configuredNode ?? (typeof requestedNode === "string" ? requestedNode.trim() : "");
|
||||
return {
|
||||
...rest,
|
||||
...nodeArgs,
|
||||
host,
|
||||
...(node ? { node } : {}),
|
||||
};
|
||||
@@ -115,6 +122,7 @@ function normalizeExecDynamicToolArgs(args: unknown): Record<string, unknown> {
|
||||
function hideExecDynamicToolParameters(
|
||||
parameters: OpenClawDynamicTool["parameters"],
|
||||
hideNode: boolean,
|
||||
nodeOnly: boolean,
|
||||
) {
|
||||
if (!parameters || typeof parameters !== "object" || Array.isArray(parameters)) {
|
||||
return parameters;
|
||||
@@ -124,21 +132,17 @@ function hideExecDynamicToolParameters(
|
||||
if (!rawProperties || typeof rawProperties !== "object" || Array.isArray(rawProperties)) {
|
||||
return parameters;
|
||||
}
|
||||
const includeParameter = (name: string) =>
|
||||
nodeOnly
|
||||
? CODEX_NODE_EXEC_PARAMETER_NAMES.has(name) && !(hideNode && name === "node")
|
||||
: !CODEX_EXEC_POLICY_PARAMETER_NAMES.has(normalizeCodexDynamicToolName(name)) &&
|
||||
!(hideNode && normalizeCodexDynamicToolName(name) === "node");
|
||||
const nextProperties = Object.fromEntries(
|
||||
Object.entries(rawProperties).filter(
|
||||
([name]) =>
|
||||
!CODEX_EXEC_POLICY_PARAMETER_NAMES.has(normalizeCodexDynamicToolName(name)) &&
|
||||
!(hideNode && normalizeCodexDynamicToolName(name) === "node"),
|
||||
),
|
||||
Object.entries(rawProperties).filter(([name]) => includeParameter(name)),
|
||||
);
|
||||
const rawRequired = schema.required;
|
||||
const nextRequired = Array.isArray(rawRequired)
|
||||
? rawRequired.filter(
|
||||
(name) =>
|
||||
typeof name !== "string" ||
|
||||
(!CODEX_EXEC_POLICY_PARAMETER_NAMES.has(normalizeCodexDynamicToolName(name)) &&
|
||||
!(hideNode && normalizeCodexDynamicToolName(name) === "node")),
|
||||
)
|
||||
? rawRequired.filter((name) => typeof name !== "string" || includeParameter(name))
|
||||
: rawRequired;
|
||||
return {
|
||||
...schema,
|
||||
|
||||
@@ -469,7 +469,8 @@ export function createExecTool(
|
||||
approvalRunningNoticeMs,
|
||||
warnings,
|
||||
foregroundWarnings: foregroundFallbackWarning ? [foregroundFallbackWarning] : [],
|
||||
processContinuationAvailable: allowBackground,
|
||||
// Remote system.run has no process-session owner.
|
||||
processContinuationAvailable: false,
|
||||
notifySessionKey,
|
||||
notifyOnExit,
|
||||
trustedSafeBinDirs,
|
||||
|
||||
@@ -244,12 +244,17 @@ function expectPendingApprovalText(
|
||||
expect(pendingText).toContain(options.command);
|
||||
if (options.interactive) {
|
||||
expect(pendingText).toContain("Mode: foreground (interactive approvals available).");
|
||||
}
|
||||
if (options.interactive && options.host !== "node") {
|
||||
expect(pendingText).toContain(
|
||||
(options.allowedDecisions ?? "").includes("allow-always")
|
||||
? "Background mode requires pre-approved policy"
|
||||
: "Background mode requires an effective policy that allows pre-approval",
|
||||
);
|
||||
}
|
||||
if (options.host === "node") {
|
||||
expect(pendingText).not.toContain("Background mode");
|
||||
}
|
||||
return details;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user