mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-22 18:51:16 +00:00
fix(codex): restore shell for restricted turns (#92294)
* fix(codex): keep OpenClaw exec when native surface has no environment (#92238) * chore: keep release note in PR * fix(codex): keep shell tool type internal Co-authored-by: yetval <yetvald@gmail.com> --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -467,9 +467,9 @@ and `HOME` stays inherited so subprocesses can use normal user-home state.
|
||||
## Dynamic tools
|
||||
|
||||
Codex dynamic tools default to `searchable` loading, exposed under the
|
||||
`openclaw` namespace with `deferLoading: true`. OpenClaw does not expose
|
||||
dynamic tools that duplicate Codex-native workspace operations or Codex's own
|
||||
tool-search surface:
|
||||
`openclaw` namespace with `deferLoading: true`. OpenClaw normally does not
|
||||
expose dynamic tools that duplicate Codex-native workspace operations or
|
||||
Codex's own tool-search surface:
|
||||
|
||||
- `read`
|
||||
- `write`
|
||||
@@ -483,6 +483,11 @@ tool-search surface:
|
||||
- `tool_search`
|
||||
- `tool_search_code`
|
||||
|
||||
When a finite runtime allowlist disables native Code Mode, OpenClaw sends an
|
||||
empty execution-environment selection. In that direct, unsandboxed case,
|
||||
OpenClaw keeps its policy-filtered `exec` and `process` tools as the shell
|
||||
fallback. Runtime allowlists and `codexDynamicToolsExclude` still apply.
|
||||
|
||||
Most remaining OpenClaw integration tools, such as messaging, media, cron,
|
||||
browser, nodes, gateway, `heartbeat_respond`, and `web_search`, are available
|
||||
through Codex tool search under that namespace. This keeps the initial model
|
||||
|
||||
@@ -39,7 +39,10 @@ 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.
|
||||
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.
|
||||
|
||||
This Codex-native feature is separate from
|
||||
[OpenClaw code mode](/reference/code-mode), an opt-in QuickJS-WASI runtime
|
||||
@@ -623,14 +626,16 @@ normal user-home state.
|
||||
|
||||
### Dynamic tools and web search
|
||||
|
||||
Codex dynamic tools default to `searchable` loading. OpenClaw does not
|
||||
expose dynamic tools that duplicate Codex-native workspace operations:
|
||||
Codex dynamic tools default to `searchable` loading. OpenClaw normally does
|
||||
not expose dynamic tools that duplicate Codex-native workspace operations:
|
||||
`read`, `write`, `edit`, `apply_patch`, `exec`, `process`, `update_plan`,
|
||||
`tool_call`, `tool_describe`, `tool_search`, and `tool_search_code`. Most
|
||||
remaining OpenClaw integration tools, such as messaging, media, cron,
|
||||
browser, nodes, gateway, and `heartbeat_respond`, are available through
|
||||
Codex tool search under the `openclaw` namespace, keeping the initial model
|
||||
context smaller.
|
||||
context smaller. The restricted-turn shell fallback is the exception for
|
||||
`exec` and `process` when a finite allowlist disables native Code Mode;
|
||||
runtime allowlists and `codexDynamicToolsExclude` still apply.
|
||||
|
||||
Tools marked `catalogMode: "direct-only"`, including the OpenClaw `computer`
|
||||
tool, use the `openclaw_direct` namespace instead. Codex treats that namespace
|
||||
|
||||
@@ -981,6 +981,73 @@ describe("Codex app-server dynamic tool build", () => {
|
||||
nativeToolSurfaceEnabled: true,
|
||||
});
|
||||
expect(gatewayTools.map((tool) => tool.name)).toEqual(["message"]);
|
||||
|
||||
const allowlistedParams = {
|
||||
...gatewayParams,
|
||||
toolsAllow: ["message"],
|
||||
} as EmbeddedRunAttemptParams;
|
||||
const allowlistedTools = await buildDynamicToolsForTest(allowlistedParams, workspaceDir, {
|
||||
nativeToolSurfaceEnabled: true,
|
||||
});
|
||||
expect(allowlistedTools.map((tool) => tool.name)).toEqual(["message"]);
|
||||
});
|
||||
|
||||
it("restores the policy-filtered OpenClaw shell when a finite allowlist disables native Code Mode", async () => {
|
||||
const execTool = createRuntimeDynamicTool("exec");
|
||||
const processTool = createRuntimeDynamicTool("process");
|
||||
const messageTool = createRuntimeDynamicTool("message");
|
||||
setOpenClawCodingToolsFactoryForTests(() => [execTool, processTool, messageTool]);
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(path.join(tempDir, "restricted-session.jsonl"), workspaceDir);
|
||||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
params.toolsAllow = ["exec", "process", "message"];
|
||||
const nativeToolSurfaceEnabled = shouldEnableCodexAppServerNativeToolSurface(params);
|
||||
|
||||
const tools = await buildDynamicToolsForTest(params, workspaceDir, {
|
||||
nativeToolSurfaceEnabled,
|
||||
});
|
||||
|
||||
expect(nativeToolSurfaceEnabled).toBe(false);
|
||||
expect(tools.map((tool) => tool.name)).toEqual([
|
||||
"exec",
|
||||
"process",
|
||||
"message",
|
||||
"node_exec",
|
||||
"node_process",
|
||||
]);
|
||||
|
||||
const bridge = createCodexDynamicToolBridge({
|
||||
tools,
|
||||
signal: new AbortController().signal,
|
||||
loading: "direct",
|
||||
});
|
||||
await bridge.handleToolCall({
|
||||
threadId: "restricted-thread",
|
||||
turnId: "restricted-turn",
|
||||
tool: "exec",
|
||||
callId: "restricted-exec",
|
||||
arguments: { command: "echo restored" },
|
||||
});
|
||||
expect(execTool.execute).toHaveBeenCalledWith(
|
||||
"restricted-exec",
|
||||
{ command: "echo restored" },
|
||||
expect.any(AbortSignal),
|
||||
undefined,
|
||||
);
|
||||
|
||||
const excludedTools = await buildDynamicToolsForTest(params, workspaceDir, {
|
||||
nativeToolSurfaceEnabled,
|
||||
pluginConfig: { codexDynamicToolsExclude: ["exec", "process"] },
|
||||
});
|
||||
expect(excludedTools.map((tool) => tool.name)).toEqual(["message"]);
|
||||
|
||||
const messageOnlyTools = await buildDynamicToolsForTest(
|
||||
{ ...params, toolsAllow: ["message"] },
|
||||
workspaceDir,
|
||||
{ nativeToolSurfaceEnabled: false },
|
||||
);
|
||||
expect(messageOnlyTools.map((tool) => tool.name)).toEqual(["message"]);
|
||||
});
|
||||
|
||||
it("exposes Docker sandbox shell tools when native Code Mode cannot honor sandbox paths", async () => {
|
||||
@@ -1024,9 +1091,12 @@ describe("Codex app-server dynamic tool build", () => {
|
||||
|
||||
const disabledSandboxTools = await buildDynamicToolsForTest(params, workspaceDir, {
|
||||
sandbox: { enabled: false, backendId: "ssh" } as never,
|
||||
nativeToolSurfaceEnabled: false,
|
||||
});
|
||||
|
||||
expect(disabledSandboxTools.map((tool) => tool.name)).toEqual([
|
||||
"exec",
|
||||
"process",
|
||||
"message",
|
||||
"node_exec",
|
||||
"node_process",
|
||||
@@ -1046,6 +1116,7 @@ describe("Codex app-server dynamic tool build", () => {
|
||||
|
||||
const tools = await buildDynamicToolsForTest(params, workspaceDir, {
|
||||
sandbox: { enabled: true, backendId: "ssh" } as never,
|
||||
nativeToolSurfaceEnabled: false,
|
||||
});
|
||||
|
||||
expect(tools.map((tool) => tool.name)).toEqual(["message"]);
|
||||
@@ -1066,6 +1137,7 @@ describe("Codex app-server dynamic tool build", () => {
|
||||
for (const excludedToolName of ["sandbox_exec", "process"]) {
|
||||
const tools = await buildDynamicToolsForTest(params, workspaceDir, {
|
||||
sandbox: { enabled: true, backendId: "ssh" } as never,
|
||||
nativeToolSurfaceEnabled: false,
|
||||
pluginConfig: { codexDynamicToolsExclude: [excludedToolName] },
|
||||
});
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import { readCodexPluginConfig, type CodexPluginConfig } from "./config.js";
|
||||
import { dynamicToolBuildState } from "./dynamic-tool-build-state.js";
|
||||
import {
|
||||
filterCodexDynamicTools,
|
||||
filterCodexDynamicToolsWithOpenClawShell,
|
||||
isSystemAgentOnlyCodexDynamicToolAllowlist,
|
||||
isForcedPrivateQaCodexRuntime,
|
||||
normalizeCodexDynamicToolName,
|
||||
@@ -36,6 +37,13 @@ import {
|
||||
} from "./native-execution-policy.js";
|
||||
import type { CodexSandboxPolicy, CodexTurnEnvironmentParams } from "./protocol.js";
|
||||
import type { CodexSandboxExecEnvironment } from "./sandbox-exec-server.js";
|
||||
import {
|
||||
CODEX_NODE_EXEC_DYNAMIC_TOOL_NAME,
|
||||
CODEX_NODE_PROCESS_DYNAMIC_TOOL_NAME,
|
||||
createNodeExecDynamicTool,
|
||||
createNodeProcessDynamicTool,
|
||||
isCodexDynamicToolExcluded,
|
||||
} from "./shell-dynamic-tools.js";
|
||||
import { filterToolsForVisionInputs } from "./vision-tools.js";
|
||||
import { resolveCodexWebSearchPlan, type CodexNativeWebSearchSupport } from "./web-search.js";
|
||||
|
||||
@@ -60,9 +68,6 @@ const CODEX_NATIVE_SANDBOX_TOOL_REQUIREMENTS = [
|
||||
"apply_patch",
|
||||
] as const;
|
||||
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_POLICY_PARAMETER_NAMES = new Set(["host", "security", "ask"]);
|
||||
function preserveRingZeroSystemAgentTool<T extends { name: string; catalogMode?: string }>(
|
||||
allTools: T[],
|
||||
filteredTools: T[],
|
||||
@@ -338,7 +343,9 @@ export async function buildDynamicTools(input: DynamicToolBuildParams) {
|
||||
nativeProviderWebSearchSupport: input.nativeProviderWebSearchSupport,
|
||||
});
|
||||
const readableAllTools = [...readableAllToolProjection.tools];
|
||||
const normallyProfiledTools = filterCodexDynamicTools(readableAllTools, input.pluginConfig);
|
||||
const normallyProfiledTools = shouldKeepOpenClawShellDynamicTools(input, nativeExecutionPolicy)
|
||||
? filterCodexDynamicToolsWithOpenClawShell(readableAllTools, input.pluginConfig)
|
||||
: filterCodexDynamicTools(readableAllTools, input.pluginConfig);
|
||||
const hostSystemAgentActive =
|
||||
input.isHostScopedToolActive?.("openclaw") ?? isHostScopedAgentToolActive("openclaw");
|
||||
const profileFilteredTools =
|
||||
@@ -736,13 +743,6 @@ function shouldExposeSandboxExecDynamicTool(input: DynamicToolBuildParams): bool
|
||||
const backendId = input.sandbox?.enabled ? input.sandbox.backendId.trim().toLowerCase() : "";
|
||||
return Boolean(backendId && input.nativeToolSurfaceEnabled === false);
|
||||
}
|
||||
function isCodexDynamicToolExcluded(config: CodexPluginConfig, names: string[]): boolean {
|
||||
const normalizedNames = new Set(names.map((name) => normalizeCodexDynamicToolName(name)));
|
||||
return (config.codexDynamicToolsExclude ?? []).some((name) => {
|
||||
const normalized = normalizeCodexDynamicToolName(name);
|
||||
return normalizedNames.has(normalized);
|
||||
});
|
||||
}
|
||||
function isSandboxShellDynamicToolExcluded(config: CodexPluginConfig): boolean {
|
||||
return isCodexDynamicToolExcluded(config, ["exec", "sandbox_exec", "process", "sandbox_process"]);
|
||||
}
|
||||
@@ -790,97 +790,18 @@ function addNodeShellDynamicToolsIfNeeded(
|
||||
}
|
||||
return toolsToAppend.length > 0 ? [...filteredTools, ...toolsToAppend] : filteredTools;
|
||||
}
|
||||
function createNodeExecDynamicTool(
|
||||
execTool: OpenClawDynamicTool,
|
||||
configuredNode: string | undefined,
|
||||
): OpenClawDynamicTool {
|
||||
const pinnedNode = configuredNode?.trim();
|
||||
return {
|
||||
...execTool,
|
||||
name: CODEX_NODE_EXEC_DYNAMIC_TOOL_NAME,
|
||||
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, pinnedNode),
|
||||
signal,
|
||||
onUpdate,
|
||||
);
|
||||
return {
|
||||
...result,
|
||||
content: result.content.map((item) =>
|
||||
item.type === "text"
|
||||
? Object.assign({}, item, {
|
||||
text: item.text.replace(
|
||||
"Use process (list/poll/log/write/send-keys/submit/paste/kill/clear/remove) for follow-up.",
|
||||
"Use node_process (list/poll/log/write/send-keys/submit/paste/kill/clear/remove) for follow-up.",
|
||||
),
|
||||
})
|
||||
: item,
|
||||
),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
function createNodeProcessDynamicTool(processTool: OpenClawDynamicTool): OpenClawDynamicTool {
|
||||
return {
|
||||
...processTool,
|
||||
name: CODEX_NODE_PROCESS_DYNAMIC_TOOL_NAME,
|
||||
description:
|
||||
"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.",
|
||||
};
|
||||
}
|
||||
function pinNodeExecDynamicToolArgs(args: unknown, configuredNode: string | undefined): unknown {
|
||||
const source =
|
||||
args && typeof args === "object" && !Array.isArray(args)
|
||||
? (args as Record<string, unknown>)
|
||||
: {};
|
||||
const { host: _host, security: _security, ask: _ask, node: requestedNode, ...rest } = source;
|
||||
const node = configuredNode ?? (typeof requestedNode === "string" ? requestedNode.trim() : "");
|
||||
return {
|
||||
...rest,
|
||||
host: "node",
|
||||
...(node ? { node } : {}),
|
||||
};
|
||||
}
|
||||
function hideNodeExecDynamicToolParameters(
|
||||
parameters: OpenClawDynamicTool["parameters"],
|
||||
options: { hideNode: boolean },
|
||||
) {
|
||||
if (!parameters || typeof parameters !== "object" || Array.isArray(parameters)) {
|
||||
return parameters;
|
||||
}
|
||||
const schema = parameters as Record<string, unknown>;
|
||||
const rawProperties = schema.properties;
|
||||
if (!rawProperties || typeof rawProperties !== "object" || Array.isArray(rawProperties)) {
|
||||
return parameters;
|
||||
}
|
||||
const nextProperties = Object.fromEntries(
|
||||
Object.entries(rawProperties).filter(
|
||||
([name]) =>
|
||||
!CODEX_NODE_EXEC_POLICY_PARAMETER_NAMES.has(normalizeCodexDynamicToolName(name)) &&
|
||||
!(options.hideNode && normalizeCodexDynamicToolName(name) === "node"),
|
||||
),
|
||||
function shouldKeepOpenClawShellDynamicTools(
|
||||
input: DynamicToolBuildParams,
|
||||
nodePolicy: CodexNativeExecutionPolicy,
|
||||
): boolean {
|
||||
return (
|
||||
!isCodexMemoryFlushRun(input.params) &&
|
||||
// Disabled native Code Mode sends `environments: []`, so Codex cannot
|
||||
// advertise a shell. Preserve OpenClaw's policy-filtered direct shell.
|
||||
input.nativeToolSurfaceEnabled === false &&
|
||||
input.sandbox?.enabled !== true &&
|
||||
nodePolicy.effectiveExecHost !== "node"
|
||||
);
|
||||
const rawRequired = schema.required;
|
||||
const nextRequired = Array.isArray(rawRequired)
|
||||
? rawRequired.filter(
|
||||
(name) =>
|
||||
typeof name !== "string" ||
|
||||
(!CODEX_NODE_EXEC_POLICY_PARAMETER_NAMES.has(normalizeCodexDynamicToolName(name)) &&
|
||||
!(options.hideNode && normalizeCodexDynamicToolName(name) === "node")),
|
||||
)
|
||||
: rawRequired;
|
||||
return {
|
||||
...schema,
|
||||
properties: nextProperties,
|
||||
...(Array.isArray(rawRequired) ? { required: nextRequired } : {}),
|
||||
};
|
||||
}
|
||||
function resolveCodexNativeExecutionPolicyForDynamicTools(
|
||||
input: DynamicToolBuildParams,
|
||||
|
||||
@@ -21,6 +21,7 @@ const CODEX_APP_SERVER_OWNED_DYNAMIC_TOOL_EXCLUDES = [
|
||||
"tool_search",
|
||||
"tool_search_code",
|
||||
] as const;
|
||||
const CODEX_APP_SERVER_OWNED_SHELL_TOOL_EXCLUDES = new Set(["exec", "process"]);
|
||||
|
||||
const DYNAMIC_TOOL_NAME_ALIASES: Record<string, string> = {
|
||||
bash: "exec",
|
||||
@@ -113,10 +114,35 @@ export function filterCodexDynamicTools<T extends { name: string }>(
|
||||
tools: T[],
|
||||
config: Pick<CodexPluginConfig, "codexDynamicToolsExclude">,
|
||||
env: CodexDynamicToolProfileEnv = process.env,
|
||||
): T[] {
|
||||
return filterCodexDynamicToolsWithOptions(tools, config, env, {
|
||||
preserveOpenClawShell: false,
|
||||
});
|
||||
}
|
||||
|
||||
/** Keeps exec/process only when Codex cannot advertise an environment-backed native shell. */
|
||||
export function filterCodexDynamicToolsWithOpenClawShell<T extends { name: string }>(
|
||||
tools: T[],
|
||||
config: Pick<CodexPluginConfig, "codexDynamicToolsExclude">,
|
||||
env: CodexDynamicToolProfileEnv = process.env,
|
||||
): T[] {
|
||||
return filterCodexDynamicToolsWithOptions(tools, config, env, {
|
||||
preserveOpenClawShell: true,
|
||||
});
|
||||
}
|
||||
|
||||
function filterCodexDynamicToolsWithOptions<T extends { name: string }>(
|
||||
tools: T[],
|
||||
config: Pick<CodexPluginConfig, "codexDynamicToolsExclude">,
|
||||
env: CodexDynamicToolProfileEnv,
|
||||
options: { preserveOpenClawShell: boolean },
|
||||
): T[] {
|
||||
const excludes = new Set<string>();
|
||||
if (!isForcedPrivateQaCodexRuntime(env)) {
|
||||
for (const name of CODEX_APP_SERVER_OWNED_DYNAMIC_TOOL_EXCLUDES) {
|
||||
if (options.preserveOpenClawShell && CODEX_APP_SERVER_OWNED_SHELL_TOOL_EXCLUDES.has(name)) {
|
||||
continue;
|
||||
}
|
||||
excludes.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
119
extensions/codex/src/app-server/shell-dynamic-tools.ts
Normal file
119
extensions/codex/src/app-server/shell-dynamic-tools.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import type { CodexPluginConfig } from "./config.js";
|
||||
import { normalizeCodexDynamicToolName } from "./dynamic-tool-profile.js";
|
||||
|
||||
type OpenClawCodingToolsFactory =
|
||||
(typeof import("openclaw/plugin-sdk/agent-harness"))["createOpenClawCodingTools"];
|
||||
type OpenClawDynamicTool = ReturnType<OpenClawCodingToolsFactory>[number];
|
||||
|
||||
export const CODEX_NODE_EXEC_DYNAMIC_TOOL_NAME = "node_exec";
|
||||
export const CODEX_NODE_PROCESS_DYNAMIC_TOOL_NAME = "node_process";
|
||||
const CODEX_NODE_EXEC_POLICY_PARAMETER_NAMES = new Set(["host", "security", "ask"]);
|
||||
|
||||
/** Returns true when plugin config explicitly removes any named dynamic tool. */
|
||||
export function isCodexDynamicToolExcluded(
|
||||
config: Pick<CodexPluginConfig, "codexDynamicToolsExclude">,
|
||||
names: readonly string[],
|
||||
): boolean {
|
||||
const normalizedNames = new Set(names.map((name) => normalizeCodexDynamicToolName(name)));
|
||||
return (config.codexDynamicToolsExclude ?? []).some((name) =>
|
||||
normalizedNames.has(normalizeCodexDynamicToolName(name)),
|
||||
);
|
||||
}
|
||||
|
||||
export function createNodeExecDynamicTool(
|
||||
execTool: OpenClawDynamicTool,
|
||||
configuredNode: string | undefined,
|
||||
): OpenClawDynamicTool {
|
||||
const pinnedNode = configuredNode?.trim();
|
||||
return {
|
||||
...execTool,
|
||||
name: CODEX_NODE_EXEC_DYNAMIC_TOOL_NAME,
|
||||
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, pinnedNode),
|
||||
signal,
|
||||
onUpdate,
|
||||
);
|
||||
return {
|
||||
...result,
|
||||
content: result.content.map((item) =>
|
||||
item.type === "text"
|
||||
? Object.assign({}, item, {
|
||||
text: item.text.replace(
|
||||
"Use process (list/poll/log/write/send-keys/submit/paste/kill/clear/remove) for follow-up.",
|
||||
"Use node_process (list/poll/log/write/send-keys/submit/paste/kill/clear/remove) for follow-up.",
|
||||
),
|
||||
})
|
||||
: item,
|
||||
),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createNodeProcessDynamicTool(
|
||||
processTool: OpenClawDynamicTool,
|
||||
): OpenClawDynamicTool {
|
||||
return {
|
||||
...processTool,
|
||||
name: CODEX_NODE_PROCESS_DYNAMIC_TOOL_NAME,
|
||||
description:
|
||||
"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.",
|
||||
};
|
||||
}
|
||||
|
||||
function pinNodeExecDynamicToolArgs(args: unknown, configuredNode: string | undefined): unknown {
|
||||
const source =
|
||||
args && typeof args === "object" && !Array.isArray(args)
|
||||
? (args as Record<string, unknown>)
|
||||
: {};
|
||||
const { host: _host, security: _security, ask: _ask, node: requestedNode, ...rest } = source;
|
||||
const node = configuredNode ?? (typeof requestedNode === "string" ? requestedNode.trim() : "");
|
||||
return {
|
||||
...rest,
|
||||
host: "node",
|
||||
...(node ? { node } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function hideNodeExecDynamicToolParameters(
|
||||
parameters: OpenClawDynamicTool["parameters"],
|
||||
options: { hideNode: boolean },
|
||||
) {
|
||||
if (!parameters || typeof parameters !== "object" || Array.isArray(parameters)) {
|
||||
return parameters;
|
||||
}
|
||||
const schema = parameters as Record<string, unknown>;
|
||||
const rawProperties = schema.properties;
|
||||
if (!rawProperties || typeof rawProperties !== "object" || Array.isArray(rawProperties)) {
|
||||
return parameters;
|
||||
}
|
||||
const nextProperties = Object.fromEntries(
|
||||
Object.entries(rawProperties).filter(
|
||||
([name]) =>
|
||||
!CODEX_NODE_EXEC_POLICY_PARAMETER_NAMES.has(normalizeCodexDynamicToolName(name)) &&
|
||||
!(options.hideNode && normalizeCodexDynamicToolName(name) === "node"),
|
||||
),
|
||||
);
|
||||
const rawRequired = schema.required;
|
||||
const nextRequired = Array.isArray(rawRequired)
|
||||
? rawRequired.filter(
|
||||
(name) =>
|
||||
typeof name !== "string" ||
|
||||
(!CODEX_NODE_EXEC_POLICY_PARAMETER_NAMES.has(normalizeCodexDynamicToolName(name)) &&
|
||||
!(options.hideNode && normalizeCodexDynamicToolName(name) === "node")),
|
||||
)
|
||||
: rawRequired;
|
||||
return {
|
||||
...schema,
|
||||
properties: nextProperties,
|
||||
...(Array.isArray(rawRequired) ? { required: nextRequired } : {}),
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
// Codex plugin module implements side question behavior.
|
||||
import { randomUUID } from "node:crypto";
|
||||
import {
|
||||
buildAgentHookContextChannelFields,
|
||||
@@ -253,8 +252,7 @@ export async function runCodexAppServerSideQuestion(
|
||||
const appServer = connection.appServer;
|
||||
const cwd = binding.cwd || params.workspaceDir || process.cwd();
|
||||
const runId = params.opts?.runId ?? randomUUID();
|
||||
// A supervised side run inherits capability facts from the private binding.
|
||||
// Outer model metadata may describe another provider or disable tools entirely.
|
||||
// Side runs inherit private-binding capabilities, not outer model metadata.
|
||||
const effectiveParams: AgentHarnessSideQuestionParams = supervisionModelSelection
|
||||
? {
|
||||
...params,
|
||||
|
||||
Reference in New Issue
Block a user