mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(codex): expose selectable node exec (#104198)
* fix(codex): expose selectable node exec * chore: defer Codex node release note
This commit is contained in:
committed by
GitHub
parent
96b1be10f5
commit
82abf7244d
@@ -26,6 +26,12 @@ dynamic tools routed through the app-server `item/tool/call` bridge. An
|
||||
active OpenClaw sandbox or restricted tool policy disables native code mode
|
||||
entirely unless you opt into the experimental sandbox exec-server path.
|
||||
|
||||
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
|
||||
(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.
|
||||
|
||||
This Codex-native feature is separate from
|
||||
[OpenClaw code mode](/reference/code-mode), an opt-in QuickJS-WASI runtime
|
||||
for generic OpenClaw runs with a different `exec` input shape. For the
|
||||
|
||||
@@ -855,6 +855,128 @@ describe("Codex app-server dynamic tool build", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("exposes selectable node shell tools beside native shell for auto host runs", async () => {
|
||||
const execTool = {
|
||||
...createRuntimeDynamicTool("exec"),
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
command: { type: "string" },
|
||||
host: { type: "string" },
|
||||
security: { type: "string" },
|
||||
ask: { type: "string" },
|
||||
node: { type: "string" },
|
||||
},
|
||||
required: ["command"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
};
|
||||
vi.mocked(execTool.execute).mockResolvedValueOnce({
|
||||
content: [{ type: "text", text: "arm64" }],
|
||||
details: { status: "completed" },
|
||||
});
|
||||
const processTool = createRuntimeDynamicTool("process");
|
||||
setOpenClawCodingToolsFactoryForTests(() => [
|
||||
execTool,
|
||||
processTool,
|
||||
createRuntimeDynamicTool("message"),
|
||||
]);
|
||||
const sessionFile = path.join(tempDir, "auto-node-session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
|
||||
const tools = await buildDynamicToolsForTest(params, workspaceDir, {
|
||||
nativeToolSurfaceEnabled: true,
|
||||
});
|
||||
|
||||
expect(tools.map((tool) => tool.name)).toEqual(["message", "node_exec", "node_process"]);
|
||||
const nodeExec = tools.find((tool) => tool.name === "node_exec");
|
||||
expect(nodeExec?.description).toContain("Select the node by name or id");
|
||||
expect(nodeExec?.parameters).toEqual({
|
||||
type: "object",
|
||||
properties: {
|
||||
command: { type: "string" },
|
||||
node: { type: "string" },
|
||||
},
|
||||
required: ["command"],
|
||||
additionalProperties: false,
|
||||
});
|
||||
await nodeExec?.execute(
|
||||
"call-auto-node",
|
||||
{
|
||||
command: "/usr/bin/uname -m",
|
||||
node: "mac-mini",
|
||||
host: "gateway",
|
||||
security: "full",
|
||||
ask: "off",
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
expect(execTool.execute).toHaveBeenCalledWith(
|
||||
"call-auto-node",
|
||||
{
|
||||
command: "/usr/bin/uname -m",
|
||||
node: "mac-mini",
|
||||
host: "node",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
|
||||
vi.mocked(execTool.execute).mockResolvedValueOnce({
|
||||
content: [{ type: "text", text: "arm64" }],
|
||||
details: { status: "completed" },
|
||||
});
|
||||
const boundAutoParams = createParams(
|
||||
path.join(tempDir, "bound-auto-node-session.jsonl"),
|
||||
workspaceDir,
|
||||
);
|
||||
boundAutoParams.disableTools = false;
|
||||
boundAutoParams.runtimePlan = createCodexRuntimePlanFixture();
|
||||
boundAutoParams.config = {
|
||||
tools: { exec: { host: "auto", node: "bound-mac-mini" } },
|
||||
} as never;
|
||||
const boundAutoTools = await buildDynamicToolsForTest(boundAutoParams, workspaceDir, {
|
||||
nativeToolSurfaceEnabled: true,
|
||||
});
|
||||
const boundNodeExec = boundAutoTools.find((tool) => tool.name === "node_exec");
|
||||
expect(boundNodeExec?.parameters).toEqual({
|
||||
type: "object",
|
||||
properties: { command: { type: "string" } },
|
||||
required: ["command"],
|
||||
additionalProperties: false,
|
||||
});
|
||||
await boundNodeExec?.execute(
|
||||
"call-bound-auto-node",
|
||||
{ command: "/usr/bin/uname -m", node: "other-node" },
|
||||
undefined,
|
||||
);
|
||||
expect(execTool.execute).toHaveBeenLastCalledWith(
|
||||
"call-bound-auto-node",
|
||||
{
|
||||
command: "/usr/bin/uname -m",
|
||||
node: "bound-mac-mini",
|
||||
host: "node",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
|
||||
const gatewayParams = createParams(
|
||||
path.join(tempDir, "gateway-node-session.jsonl"),
|
||||
workspaceDir,
|
||||
);
|
||||
gatewayParams.disableTools = false;
|
||||
gatewayParams.runtimePlan = createCodexRuntimePlanFixture();
|
||||
gatewayParams.execOverrides = { host: "gateway" };
|
||||
const gatewayTools = await buildDynamicToolsForTest(gatewayParams, workspaceDir, {
|
||||
nativeToolSurfaceEnabled: true,
|
||||
});
|
||||
expect(gatewayTools.map((tool) => tool.name)).toEqual(["message"]);
|
||||
});
|
||||
|
||||
it("exposes Docker sandbox shell tools when native Code Mode cannot honor sandbox paths", async () => {
|
||||
setOpenClawCodingToolsFactoryForTests(() => [
|
||||
createRuntimeDynamicTool("exec"),
|
||||
@@ -882,7 +1004,7 @@ describe("Codex app-server dynamic tool build", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("does not expose sandbox shell tools when sandbox routing is disabled", async () => {
|
||||
it("exposes node shell but not sandbox shell tools when sandbox routing is disabled", async () => {
|
||||
setOpenClawCodingToolsFactoryForTests(() => [
|
||||
createRuntimeDynamicTool("exec"),
|
||||
createRuntimeDynamicTool("process"),
|
||||
@@ -898,7 +1020,11 @@ describe("Codex app-server dynamic tool build", () => {
|
||||
sandbox: { enabled: false, backendId: "ssh" } as never,
|
||||
});
|
||||
|
||||
expect(disabledSandboxTools.map((tool) => tool.name)).toEqual(["message"]);
|
||||
expect(disabledSandboxTools.map((tool) => tool.name)).toEqual([
|
||||
"message",
|
||||
"node_exec",
|
||||
"node_process",
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not expose sandbox_exec without a matching process follow-up tool", async () => {
|
||||
|
||||
@@ -59,7 +59,7 @@ const CODEX_NATIVE_SANDBOX_TOOL_REQUIREMENTS = [
|
||||
const CODEX_MEMORY_FLUSH_DYNAMIC_TOOL_ALLOW = new Set(["read", "write"]);
|
||||
const CODEX_NODE_EXEC_DYNAMIC_TOOL_NAME = "node_exec";
|
||||
const CODEX_NODE_PROCESS_DYNAMIC_TOOL_NAME = "node_process";
|
||||
const CODEX_NODE_EXEC_HIDDEN_PARAMETER_NAMES = new Set(["host", "security", "ask", "node"]);
|
||||
const CODEX_NODE_EXEC_POLICY_PARAMETER_NAMES = new Set(["host", "security", "ask"]);
|
||||
|
||||
/** Runtime inputs needed to derive the exact Codex dynamic tool surface for a turn. */
|
||||
export type DynamicToolBuildParams = {
|
||||
@@ -775,7 +775,10 @@ function addNodeShellDynamicToolsIfNeeded(
|
||||
if (isCodexMemoryFlushRun(input.params)) {
|
||||
return filteredTools;
|
||||
}
|
||||
if (nodePolicy.effectiveExecHost !== "node") {
|
||||
const nodeExecIsDefault = nodePolicy.effectiveExecHost === "node";
|
||||
const nodeExecAvailableFromAuto =
|
||||
nodePolicy.requestedExecHost === "auto" && nodePolicy.effectiveExecHost === "gateway";
|
||||
if (!nodeExecIsDefault && !nodeExecAvailableFromAuto) {
|
||||
return filteredTools;
|
||||
}
|
||||
const execTool = allTools.find((tool) => normalizeCodexDynamicToolName(tool.name) === "exec");
|
||||
@@ -812,16 +815,20 @@ function createNodeExecDynamicTool(
|
||||
execTool: OpenClawDynamicTool,
|
||||
configuredNode: string | undefined,
|
||||
): OpenClawDynamicTool {
|
||||
const pinnedNode = configuredNode?.trim();
|
||||
return {
|
||||
...execTool,
|
||||
name: CODEX_NODE_EXEC_DYNAMIC_TOOL_NAME,
|
||||
description:
|
||||
"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 node_process for follow-up on backgrounded node_exec sessions. Use Codex's native shell for local app-server work.",
|
||||
parameters: hideNodeExecDynamicToolParameters(execTool.parameters),
|
||||
description: 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 node_process for follow-up on backgrounded node_exec sessions. 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 node_process for follow-up on backgrounded node_exec sessions. Use Codex's native shell for local app-server work.",
|
||||
parameters: hideNodeExecDynamicToolParameters(execTool.parameters, {
|
||||
hideNode: Boolean(pinnedNode),
|
||||
}),
|
||||
execute: async (toolCallId, args, signal, onUpdate) => {
|
||||
const result = await execTool.execute(
|
||||
toolCallId,
|
||||
pinNodeExecDynamicToolArgs(args, configuredNode),
|
||||
pinNodeExecDynamicToolArgs(args, pinnedNode),
|
||||
signal,
|
||||
onUpdate,
|
||||
);
|
||||
@@ -847,7 +854,7 @@ function createNodeProcessDynamicTool(processTool: OpenClawDynamicTool): OpenCla
|
||||
...processTool,
|
||||
name: CODEX_NODE_PROCESS_DYNAMIC_TOOL_NAME,
|
||||
description:
|
||||
"Manage node_exec sessions that were started on the OpenClaw configured remote node for this session: list, poll, log, write, send-keys, submit, paste, kill, clear, or remove. Use only for node_exec follow-up; use Codex's native shell session handling for local app-server work.",
|
||||
"Manage node_exec sessions that were started on OpenClaw remote nodes: list, poll, log, write, send-keys, submit, paste, kill, clear, or remove. Use only for node_exec follow-up; use Codex's native shell session handling for local app-server work.",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -856,8 +863,8 @@ function pinNodeExecDynamicToolArgs(args: unknown, configuredNode: string | unde
|
||||
args && typeof args === "object" && !Array.isArray(args)
|
||||
? (args as Record<string, unknown>)
|
||||
: {};
|
||||
const { host: _host, security: _security, ask: _ask, node: _node, ...rest } = source;
|
||||
const node = configuredNode?.trim();
|
||||
const { host: _host, security: _security, ask: _ask, node: requestedNode, ...rest } = source;
|
||||
const node = configuredNode ?? (typeof requestedNode === "string" ? requestedNode.trim() : "");
|
||||
return {
|
||||
...rest,
|
||||
host: "node",
|
||||
@@ -865,7 +872,10 @@ function pinNodeExecDynamicToolArgs(args: unknown, configuredNode: string | unde
|
||||
};
|
||||
}
|
||||
|
||||
function hideNodeExecDynamicToolParameters(parameters: OpenClawDynamicTool["parameters"]) {
|
||||
function hideNodeExecDynamicToolParameters(
|
||||
parameters: OpenClawDynamicTool["parameters"],
|
||||
options: { hideNode: boolean },
|
||||
) {
|
||||
if (!parameters || typeof parameters !== "object" || Array.isArray(parameters)) {
|
||||
return parameters;
|
||||
}
|
||||
@@ -876,7 +886,9 @@ function hideNodeExecDynamicToolParameters(parameters: OpenClawDynamicTool["para
|
||||
}
|
||||
const nextProperties = Object.fromEntries(
|
||||
Object.entries(rawProperties).filter(
|
||||
([name]) => !CODEX_NODE_EXEC_HIDDEN_PARAMETER_NAMES.has(normalizeCodexDynamicToolName(name)),
|
||||
([name]) =>
|
||||
!CODEX_NODE_EXEC_POLICY_PARAMETER_NAMES.has(normalizeCodexDynamicToolName(name)) &&
|
||||
!(options.hideNode && normalizeCodexDynamicToolName(name) === "node"),
|
||||
),
|
||||
);
|
||||
const rawRequired = schema.required;
|
||||
@@ -884,7 +896,8 @@ function hideNodeExecDynamicToolParameters(parameters: OpenClawDynamicTool["para
|
||||
? rawRequired.filter(
|
||||
(name) =>
|
||||
typeof name !== "string" ||
|
||||
!CODEX_NODE_EXEC_HIDDEN_PARAMETER_NAMES.has(normalizeCodexDynamicToolName(name)),
|
||||
(!CODEX_NODE_EXEC_POLICY_PARAMETER_NAMES.has(normalizeCodexDynamicToolName(name)) &&
|
||||
!(options.hideNode && normalizeCodexDynamicToolName(name) === "node")),
|
||||
)
|
||||
: rawRequired;
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user