Skip to content

Commit 4e97217

Browse files
committed
fix(codex): route node exec through openclaw tools
1 parent cc91ff0 commit 4e97217

3 files changed

Lines changed: 148 additions & 13 deletions

File tree

CHANGELOG.md

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

3838
### Fixes
3939

40+
- Codex app-server: disable native Code Mode when the effective exec host is `node` and keep OpenClaw `exec`/`process` available, so `/exec host=node` routes shell commands through the selected node instead of the gateway. Fixes #85012. (#85090) Thanks @sahilsatralkar.
4041
- Gateway: defer provider auth-state prewarm until after startup readiness so early gateway tool/session requests are not blocked by provider auth discovery. (#85272) Thanks @dutifulbob.
4142
- Agents/Codex: show the first plan update as a transient chat status notice without counting it as final assistant content.
4243
- Gateway/LaunchAgent: wait for launchd reload bootout to finish and fall back to kickstart when bootstrap races, so reload handoff does not leave the service deregistered. Fixes #84630. (#84641) Thanks @NianJiuZst.

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

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -895,6 +895,44 @@ describe("runCodexAppServerAttempt", () => {
895895
]);
896896
});
897897

898+
it("keeps OpenClaw shell tools for node-targeted Codex app-server runs", async () => {
899+
testing.setOpenClawCodingToolsFactoryForTests(() => [
900+
createRuntimeDynamicTool("exec"),
901+
createRuntimeDynamicTool("process"),
902+
createRuntimeDynamicTool("message"),
903+
]);
904+
const sessionFile = path.join(tempDir, "session.jsonl");
905+
const workspaceDir = path.join(tempDir, "workspace");
906+
const params = createParams(sessionFile, workspaceDir);
907+
params.disableTools = false;
908+
params.runtimePlan = createCodexRuntimePlanFixture();
909+
params.execOverrides = {
910+
host: "node",
911+
node: "mac-mini",
912+
security: "full",
913+
ask: "off",
914+
};
915+
const sandboxSessionKey = params.sessionKey;
916+
if (!sandboxSessionKey) {
917+
throw new Error("createParams must provide a sessionKey for Codex dynamic tool tests.");
918+
}
919+
920+
const tools = await testing.buildDynamicTools({
921+
params,
922+
resolvedWorkspace: workspaceDir,
923+
effectiveWorkspace: workspaceDir,
924+
sandboxSessionKey,
925+
sandbox: { enabled: false, backendId: "docker" } as never,
926+
nativeToolSurfaceEnabled: false,
927+
runAbortController: new AbortController(),
928+
sessionAgentId: "main",
929+
pluginConfig: {},
930+
onYieldDetected: () => undefined,
931+
});
932+
933+
expect(tools.map((tool) => tool.name)).toEqual(["message", "exec", "process"]);
934+
});
935+
898936
it("exposes Docker sandbox shell tools when native Code Mode cannot honor sandbox paths", async () => {
899937
testing.setOpenClawCodingToolsFactoryForTests(() => [
900938
createRuntimeDynamicTool("exec"),
@@ -1449,9 +1487,12 @@ describe("runCodexAppServerAttempt", () => {
14491487
details: { status: "running" },
14501488
});
14511489
const processTool = createRuntimeDynamicTool("process");
1490+
const workspaceDir = path.join(tempDir, "workspace");
14521491
const tools = testing.addSandboxShellDynamicToolsIfAvailable([], [execTool, processTool], {
1492+
params: createParams(path.join(tempDir, "session.jsonl"), workspaceDir),
14531493
sandbox: { enabled: true, backendId: "ssh" },
14541494
nativeToolSurfaceEnabled: false,
1495+
sessionAgentId: "main",
14551496
pluginConfig: {},
14561497
} as never);
14571498

@@ -1853,6 +1894,43 @@ describe("runCodexAppServerAttempt", () => {
18531894
expect(testing.shouldEnableCodexAppServerNativeToolSurface(params)).toBe(false);
18541895
});
18551896

1897+
it("disables Codex native tool surfaces when the effective exec target is node", () => {
1898+
const workspaceDir = path.join(tempDir, "workspace");
1899+
const sessionParams = createParams(path.join(tempDir, "session.jsonl"), workspaceDir);
1900+
sessionParams.disableTools = false;
1901+
sessionParams.execOverrides = {
1902+
host: "node",
1903+
node: "mac-mini",
1904+
security: "full",
1905+
ask: "off",
1906+
};
1907+
1908+
expect(testing.shouldEnableCodexAppServerNativeToolSurface(sessionParams)).toBe(false);
1909+
1910+
sessionParams.toolsAllow = ["*"];
1911+
expect(testing.shouldEnableCodexAppServerNativeToolSurface(sessionParams)).toBe(false);
1912+
1913+
const globalParams = createParams(path.join(tempDir, "global-session.jsonl"), workspaceDir);
1914+
globalParams.disableTools = false;
1915+
globalParams.config = { tools: { exec: { host: "node" } } } as never;
1916+
1917+
expect(testing.shouldEnableCodexAppServerNativeToolSurface(globalParams)).toBe(false);
1918+
1919+
const agentParams = createParams(path.join(tempDir, "agent-session.jsonl"), workspaceDir);
1920+
agentParams.disableTools = false;
1921+
agentParams.config = {
1922+
agents: {
1923+
list: [{ id: "main", tools: { exec: { host: "node" } } }],
1924+
},
1925+
} as never;
1926+
1927+
expect(
1928+
testing.shouldEnableCodexAppServerNativeToolSurface(agentParams, undefined, {
1929+
agentId: "main",
1930+
}),
1931+
).toBe(false);
1932+
});
1933+
18561934
it("disables Codex native tool surfaces whenever an OpenClaw sandbox is active", () => {
18571935
const workspaceDir = path.join(tempDir, "workspace");
18581936
const params = createParams(path.join(tempDir, "session.jsonl"), workspaceDir);

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

Lines changed: 69 additions & 13 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,
@@ -956,6 +960,7 @@ export async function runCodexAppServerAttempt(
956960
});
957961
const sandboxExecServerEnabled = isCodexSandboxExecServerEnabled(pluginConfig);
958962
const nativeToolSurfaceEnabled = shouldEnableCodexAppServerNativeToolSurface(params, sandbox, {
963+
agentId: sessionAgentId,
959964
sandboxExecServerEnabled,
960965
});
961966
for (const diagnostic of bundleMcpThreadConfig.diagnostics) {
@@ -3666,10 +3671,14 @@ async function buildDynamicTools(input: DynamicToolBuildParams) {
36663671
input.runAbortController.abort("sessions_yield");
36673672
},
36683673
});
3669-
const codexFilteredTools = addSandboxShellDynamicToolsIfAvailable(
3670-
isCodexMemoryFlushRun(params)
3671-
? filterCodexMemoryFlushDynamicTools(allTools)
3672-
: filterCodexDynamicTools(allTools, input.pluginConfig),
3674+
const codexFilteredTools = addNodeShellDynamicToolsIfNeeded(
3675+
addSandboxShellDynamicToolsIfAvailable(
3676+
isCodexMemoryFlushRun(params)
3677+
? filterCodexMemoryFlushDynamicTools(allTools)
3678+
: filterCodexDynamicTools(allTools, input.pluginConfig),
3679+
allTools,
3680+
input,
3681+
),
36733682
allTools,
36743683
input,
36753684
);
@@ -3713,11 +3722,14 @@ function includeForcedMessageToolAllow(
37133722
function shouldEnableCodexAppServerNativeToolSurface(
37143723
params: EmbeddedRunAttemptParams,
37153724
sandbox?: OpenClawSandboxContext,
3716-
options: { sandboxExecServerEnabled?: boolean } = {},
3725+
options: { agentId?: string; sandboxExecServerEnabled?: boolean } = {},
37173726
): boolean {
37183727
if (isCodexMemoryFlushRun(params)) {
37193728
return false;
37203729
}
3730+
if (isEffectiveExecHostNode(params, options.agentId)) {
3731+
return false;
3732+
}
37213733
const toolsAllow = includeForcedMessageToolAllow(params.toolsAllow, params);
37223734
if (toolsAllow === undefined) {
37233735
return canCodexAppServerNativeToolSurfaceHonorSandbox(sandbox, options);
@@ -3731,6 +3743,14 @@ function shouldEnableCodexAppServerNativeToolSurface(
37313743
);
37323744
}
37333745

3746+
function isEffectiveExecHostNode(params: EmbeddedRunAttemptParams, agentId?: string): boolean {
3747+
const agentExec =
3748+
params.config && agentId ? resolveAgentConfig(params.config, agentId)?.tools?.exec : undefined;
3749+
return (
3750+
(params.execOverrides?.host ?? agentExec?.host ?? params.config?.tools?.exec?.host) === "node"
3751+
);
3752+
}
3753+
37343754
function canCodexAppServerNativeToolSurfaceHonorSandbox(
37353755
sandbox: OpenClawSandboxContext | undefined,
37363756
options: { sandboxExecServerEnabled?: boolean } = {},
@@ -3882,22 +3902,58 @@ function shouldExposeSandboxExecDynamicTool(input: DynamicToolBuildParams): bool
38823902
if (isCodexMemoryFlushRun(input.params)) {
38833903
return false;
38843904
}
3905+
if (isEffectiveExecHostNode(input.params, input.sessionAgentId)) {
3906+
return false;
3907+
}
38853908
const backendId = input.sandbox?.enabled ? input.sandbox.backendId.trim().toLowerCase() : "";
38863909
return Boolean(backendId && input.nativeToolSurfaceEnabled === false);
38873910
}
38883911

3889-
function isSandboxShellDynamicToolExcluded(config: CodexPluginConfig): boolean {
3912+
function isCodexDynamicToolExcluded(config: CodexPluginConfig, names: string[]): boolean {
3913+
const normalizedNames = new Set(names.map((name) => normalizeCodexDynamicToolName(name)));
38903914
return (config.codexDynamicToolsExclude ?? []).some((name) => {
38913915
const normalized = normalizeCodexDynamicToolName(name);
3892-
return (
3893-
normalized === "exec" ||
3894-
normalized === "sandbox_exec" ||
3895-
normalized === "process" ||
3896-
normalized === "sandbox_process"
3897-
);
3916+
return normalizedNames.has(normalized);
38983917
});
38993918
}
39003919

3920+
function isSandboxShellDynamicToolExcluded(config: CodexPluginConfig): boolean {
3921+
return isCodexDynamicToolExcluded(config, ["exec", "sandbox_exec", "process", "sandbox_process"]);
3922+
}
3923+
3924+
function addNodeShellDynamicToolsIfNeeded(
3925+
filteredTools: OpenClawDynamicTool[],
3926+
allTools: OpenClawDynamicTool[],
3927+
input: DynamicToolBuildParams,
3928+
): OpenClawDynamicTool[] {
3929+
if (
3930+
isCodexMemoryFlushRun(input.params) ||
3931+
!isEffectiveExecHostNode(input.params, input.sessionAgentId)
3932+
) {
3933+
return filteredTools;
3934+
}
3935+
let next = filteredTools;
3936+
for (const toolName of ["exec", "process"]) {
3937+
if (isCodexDynamicToolExcluded(input.pluginConfig, [toolName])) {
3938+
continue;
3939+
}
3940+
if (next.some((tool) => normalizeCodexDynamicToolName(tool.name) === toolName)) {
3941+
continue;
3942+
}
3943+
const tool = allTools.find(
3944+
(candidate) => normalizeCodexDynamicToolName(candidate.name) === toolName,
3945+
);
3946+
if (!tool) {
3947+
continue;
3948+
}
3949+
if (next === filteredTools) {
3950+
next = [...filteredTools];
3951+
}
3952+
next.push(tool);
3953+
}
3954+
return next;
3955+
}
3956+
39013957
function filterCodexDynamicToolsForAllowlist<T extends { name: string }>(
39023958
tools: T[],
39033959
toolsAllow?: string[],

0 commit comments

Comments
 (0)