Skip to content

Commit 21f7e1d

Browse files
fix(codex): disable native shell for node exec sessions
1 parent 016c34f commit 21f7e1d

2 files changed

Lines changed: 147 additions & 11 deletions

File tree

extensions/codex/src/app-server/run-attempt.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,44 @@ describe("runCodexAppServerAttempt", () => {
799799
expect(dockerTools.map((tool) => tool.name)).toEqual(["message"]);
800800
});
801801

802+
it("keeps OpenClaw shell tools for node-targeted Codex app-server runs", async () => {
803+
testing.setOpenClawCodingToolsFactoryForTests(() => [
804+
createRuntimeDynamicTool("exec"),
805+
createRuntimeDynamicTool("process"),
806+
createRuntimeDynamicTool("message"),
807+
]);
808+
const sessionFile = path.join(tempDir, "session.jsonl");
809+
const workspaceDir = path.join(tempDir, "workspace");
810+
const params = createParams(sessionFile, workspaceDir);
811+
params.disableTools = false;
812+
params.runtimePlan = createCodexRuntimePlanFixture();
813+
params.execOverrides = {
814+
host: "node",
815+
node: "mac-mini",
816+
security: "full",
817+
ask: "off",
818+
};
819+
const sandboxSessionKey = params.sessionKey;
820+
if (!sandboxSessionKey) {
821+
throw new Error("createParams must provide a sessionKey for Codex dynamic tool tests.");
822+
}
823+
824+
const tools = await testing.buildDynamicTools({
825+
params,
826+
resolvedWorkspace: workspaceDir,
827+
effectiveWorkspace: workspaceDir,
828+
sandboxSessionKey,
829+
sandbox: { enabled: false, backendId: "docker" } as never,
830+
nativeToolSurfaceEnabled: false,
831+
runAbortController: new AbortController(),
832+
sessionAgentId: "main",
833+
pluginConfig: {},
834+
onYieldDetected: () => undefined,
835+
});
836+
837+
expect(tools.map((tool) => tool.name)).toEqual(["message", "exec", "process"]);
838+
});
839+
802840
it("exposes Docker sandbox shell tools when native Code Mode cannot honor sandbox paths", async () => {
803841
testing.setOpenClawCodingToolsFactoryForTests(() => [
804842
createRuntimeDynamicTool("exec"),
@@ -944,8 +982,11 @@ describe("runCodexAppServerAttempt", () => {
944982
details: { status: "running" },
945983
});
946984
const processTool = createRuntimeDynamicTool("process");
985+
const workspaceDir = path.join(tempDir, "workspace");
947986
const tools = testing.addSandboxShellDynamicToolsIfAvailable([], [execTool, processTool], {
987+
params: createParams(path.join(tempDir, "session.jsonl"), workspaceDir),
948988
sandbox: { enabled: true, backendId: "ssh" },
989+
sessionAgentId: "main",
949990
pluginConfig: {},
950991
} as never);
951992

@@ -1347,6 +1388,44 @@ describe("runCodexAppServerAttempt", () => {
13471388
expect(testing.shouldEnableCodexAppServerNativeToolSurface(params)).toBe(false);
13481389
});
13491390

1391+
it("disables Codex native tool surface when session exec target is node", () => {
1392+
const workspaceDir = path.join(tempDir, "workspace");
1393+
const params = createParams(path.join(tempDir, "session.jsonl"), workspaceDir);
1394+
params.disableTools = false;
1395+
params.execOverrides = {
1396+
host: "node",
1397+
node: "mac-mini",
1398+
security: "full",
1399+
ask: "off",
1400+
};
1401+
1402+
expect(testing.shouldEnableCodexAppServerNativeToolSurface(params)).toBe(false);
1403+
1404+
params.toolsAllow = ["*"];
1405+
expect(testing.shouldEnableCodexAppServerNativeToolSurface(params)).toBe(false);
1406+
});
1407+
1408+
it("disables Codex native tool surface when configured exec target is node", () => {
1409+
const workspaceDir = path.join(tempDir, "workspace");
1410+
const globalParams = createParams(path.join(tempDir, "global-session.jsonl"), workspaceDir);
1411+
globalParams.disableTools = false;
1412+
globalParams.config = { tools: { exec: { host: "node" } } } as never;
1413+
1414+
expect(testing.shouldEnableCodexAppServerNativeToolSurface(globalParams)).toBe(false);
1415+
1416+
const agentParams = createParams(path.join(tempDir, "agent-session.jsonl"), workspaceDir);
1417+
agentParams.disableTools = false;
1418+
agentParams.config = {
1419+
agents: {
1420+
list: [{ id: "main", tools: { exec: { host: "node" } } }],
1421+
},
1422+
} as never;
1423+
1424+
expect(
1425+
testing.shouldEnableCodexAppServerNativeToolSurface(agentParams, undefined, "main"),
1426+
).toBe(false);
1427+
});
1428+
13501429
it("disables Codex native tool surfaces when Docker bind targets need container paths", () => {
13511430
const workspaceDir = path.join(tempDir, "workspace");
13521431
const params = createParams(path.join(tempDir, "session.jsonl"), workspaceDir);

extensions/codex/src/app-server/run-attempt.ts

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,11 @@ import {
4848
type NativeHookRelayEvent,
4949
type NativeHookRelayRegistrationHandle,
5050
} from "openclaw/plugin-sdk/agent-harness-runtime";
51-
import { markAuthProfileBlockedUntil, resolveAgentDir } from "openclaw/plugin-sdk/agent-runtime";
51+
import {
52+
markAuthProfileBlockedUntil,
53+
resolveAgentConfig,
54+
resolveAgentDir,
55+
} from "openclaw/plugin-sdk/agent-runtime";
5256
import {
5357
emitTrustedDiagnosticEvent,
5458
hasPendingInternalDiagnosticEvent,
@@ -921,7 +925,11 @@ export async function runCodexAppServerAttempt(
921925
disableTools: params.disableTools,
922926
toolsAllow: params.toolsAllow,
923927
});
924-
const nativeToolSurfaceEnabled = shouldEnableCodexAppServerNativeToolSurface(params, sandbox);
928+
const nativeToolSurfaceEnabled = shouldEnableCodexAppServerNativeToolSurface(
929+
params,
930+
sandbox,
931+
sessionAgentId,
932+
);
925933
for (const diagnostic of bundleMcpThreadConfig.diagnostics) {
926934
embeddedAgentLog.warn(`bundle-mcp: ${diagnostic.pluginId}: ${diagnostic.message}`);
927935
}
@@ -3394,8 +3402,12 @@ async function buildDynamicTools(input: DynamicToolBuildParams) {
33943402
input.runAbortController.abort("sessions_yield");
33953403
},
33963404
});
3397-
const codexFilteredTools = addSandboxShellDynamicToolsIfAvailable(
3398-
filterCodexDynamicTools(allTools, input.pluginConfig),
3405+
const codexFilteredTools = addNodeShellDynamicToolsIfNeeded(
3406+
addSandboxShellDynamicToolsIfAvailable(
3407+
filterCodexDynamicTools(allTools, input.pluginConfig),
3408+
allTools,
3409+
input,
3410+
),
33993411
allTools,
34003412
input,
34013413
);
@@ -3439,7 +3451,11 @@ function includeForcedMessageToolAllow(
34393451
function shouldEnableCodexAppServerNativeToolSurface(
34403452
params: EmbeddedRunAttemptParams,
34413453
sandbox?: OpenClawSandboxContext,
3454+
agentId?: string,
34423455
): boolean {
3456+
if (isEffectiveExecHostNode(params, agentId)) {
3457+
return false;
3458+
}
34433459
const toolsAllow = includeForcedMessageToolAllow(params.toolsAllow, params);
34443460
if (toolsAllow === undefined) {
34453461
return canCodexAppServerNativeToolSurfaceHonorSandbox(sandbox);
@@ -3453,6 +3469,14 @@ function shouldEnableCodexAppServerNativeToolSurface(
34533469
);
34543470
}
34553471

3472+
function isEffectiveExecHostNode(params: EmbeddedRunAttemptParams, agentId?: string): boolean {
3473+
const agentExec =
3474+
params.config && agentId ? resolveAgentConfig(params.config, agentId)?.tools?.exec : undefined;
3475+
return (
3476+
(params.execOverrides?.host ?? agentExec?.host ?? params.config?.tools?.exec?.host) === "node"
3477+
);
3478+
}
3479+
34563480
function canCodexAppServerNativeToolSurfaceHonorSandbox(
34573481
sandbox: OpenClawSandboxContext | undefined,
34583482
): boolean {
@@ -3526,22 +3550,55 @@ function addSandboxShellDynamicToolsIfAvailable(
35263550
}
35273551

35283552
function shouldExposeSandboxExecDynamicTool(input: DynamicToolBuildParams): boolean {
3553+
if (isEffectiveExecHostNode(input.params, input.sessionAgentId)) {
3554+
return false;
3555+
}
35293556
const backendId = input.sandbox?.enabled ? input.sandbox.backendId.trim().toLowerCase() : "";
35303557
return Boolean(backendId && (backendId !== "docker" || input.nativeToolSurfaceEnabled === false));
35313558
}
35323559

3533-
function isSandboxShellDynamicToolExcluded(config: CodexPluginConfig): boolean {
3560+
function isCodexDynamicToolExcluded(config: CodexPluginConfig, names: string[]): boolean {
3561+
const normalizedNames = new Set(names.map((name) => normalizeCodexDynamicToolName(name)));
35343562
return (config.codexDynamicToolsExclude ?? []).some((name) => {
35353563
const normalized = normalizeCodexDynamicToolName(name);
3536-
return (
3537-
normalized === "exec" ||
3538-
normalized === "sandbox_exec" ||
3539-
normalized === "process" ||
3540-
normalized === "sandbox_process"
3541-
);
3564+
return normalizedNames.has(normalized);
35423565
});
35433566
}
35443567

3568+
function isSandboxShellDynamicToolExcluded(config: CodexPluginConfig): boolean {
3569+
return isCodexDynamicToolExcluded(config, ["exec", "sandbox_exec", "process", "sandbox_process"]);
3570+
}
3571+
3572+
function addNodeShellDynamicToolsIfNeeded(
3573+
filteredTools: OpenClawDynamicTool[],
3574+
allTools: OpenClawDynamicTool[],
3575+
input: DynamicToolBuildParams,
3576+
): OpenClawDynamicTool[] {
3577+
if (!isEffectiveExecHostNode(input.params, input.sessionAgentId)) {
3578+
return filteredTools;
3579+
}
3580+
let next = filteredTools;
3581+
for (const toolName of ["exec", "process"]) {
3582+
if (isCodexDynamicToolExcluded(input.pluginConfig, [toolName])) {
3583+
continue;
3584+
}
3585+
if (next.some((tool) => normalizeCodexDynamicToolName(tool.name) === toolName)) {
3586+
continue;
3587+
}
3588+
const tool = allTools.find(
3589+
(candidate) => normalizeCodexDynamicToolName(candidate.name) === toolName,
3590+
);
3591+
if (!tool) {
3592+
continue;
3593+
}
3594+
if (next === filteredTools) {
3595+
next = [...filteredTools];
3596+
}
3597+
next.push(tool);
3598+
}
3599+
return next;
3600+
}
3601+
35453602
function filterCodexDynamicToolsForAllowlist<T extends { name: string }>(
35463603
tools: T[],
35473604
toolsAllow?: string[],

0 commit comments

Comments
 (0)