@openclaw/codex 2026.8.1-beta.2 → 2026.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/api.js +146 -0
  2. package/dist/{attempt-client-cleanup-DMLXUfa8.js → attempt-client-cleanup-CVPvp9dL.js} +47 -5
  3. package/dist/{auth-binding-B4FfbcCg.js → auth-binding-Ds_QVm-X.js} +18 -3
  4. package/dist/{binding-connection-I5Bo-AsX.js → binding-connection-CCQFs8lf.js} +15 -3
  5. package/dist/{bounded-turn-qgZmzy_4.js → bounded-turn-rq5tiSXk.js} +27 -26
  6. package/dist/{capabilities-CybGI1oz.js → capabilities-CGm_94eL.js} +1 -1
  7. package/dist/cli-metadata.js +1 -1
  8. package/dist/{command-formatters-B1AEz8ZA.js → command-formatters-C54Yg3eA.js} +2 -2
  9. package/dist/{command-handlers-Db8pu_ds.js → command-handlers-BvOAHTrA.js} +111 -67
  10. package/dist/{compact-LspQaGfl.js → compact-Hd7SAc0N.js} +35 -30
  11. package/dist/{session-binding-DPxIvMgE.js → config-Cxw3Cl3k.js} +361 -1161
  12. package/dist/context-compaction-activity-BOeJf7yo.js +58 -0
  13. package/dist/doctor-contract-api.js +128 -26
  14. package/dist/{dynamic-tools-BVdyWkUD.js → dynamic-tools-BFoUcRdx.js} +1341 -1464
  15. package/dist/{effective-mcp-catalog-DIIhAtOZ.js → effective-mcp-catalog-BTJHnDSx.js} +2 -2
  16. package/dist/elicitation-input-DMkeki8d.js +71 -0
  17. package/dist/{event-projector-assistant-message-BwxV4d6N.js → event-projector-assistant-message-C5-Ds8mA.js} +18 -1
  18. package/dist/{event-projector-usage-e5huIQWu.js → event-projector-usage-D3PHFFXh.js} +18 -18
  19. package/dist/harness-DVfUQBlQ.js +248 -0
  20. package/dist/harness.js +2 -226
  21. package/dist/index.js +405 -299
  22. package/dist/{isolated-completion-C58NbL40.js → isolated-completion-Cbbx_5Ed.js} +4 -4
  23. package/dist/launch-args-sXOAg0SX.js +84 -0
  24. package/dist/managed-binary-ZkGodW19.js +248 -0
  25. package/dist/media-understanding-provider.js +2 -2
  26. package/dist/model-catalog-o6Cgjoss.js +101 -0
  27. package/dist/{models-BFlpn1Ex.js → models-DgFWSuxA.js} +25 -28
  28. package/dist/{native-subagent-monitor-DyTGmTUx.js → native-subagent-monitor-CwFyoylz.js} +385 -94
  29. package/dist/node-exec-server.runtime-DXrqpw1c.js +195 -0
  30. package/dist/plugin-approval-roundtrip-D80UZiJ_.js +95 -0
  31. package/dist/{plugin-marketplace-discovery-CzrGJdCR.js → plugin-marketplace-discovery-mdmScYOj.js} +60 -55
  32. package/dist/{plugin-metadata-cache-CNJPj4_b.js → plugin-metadata-cache-CulOABwu.js} +164 -134
  33. package/dist/{protocol-validators-B4F7L8Rf.js → protocol-validators-Bg5rpNxk.js} +203 -7
  34. package/dist/{rate-limits-DZXmOL8-.js → rate-limits-hFtzt2Up.js} +1 -1
  35. package/dist/{sandbox-guard-Cmwc9eIk.js → request-CCm4LaED.js} +207 -20
  36. package/dist/{run-attempt-P8BdxC1I.js → run-attempt-BwNQDH2u.js} +1506 -836
  37. package/dist/{runtime-artifact-BVSPdd6g.js → runtime-artifact-DeBvleJw.js} +4 -7
  38. package/dist/session-binding-q2WSTVmC.js +947 -0
  39. package/dist/{session-catalog-xwNi_SDM.js → session-catalog-C2Tae4rI.js} +1698 -1346
  40. package/dist/session-catalog-home-id-DAKUrm3C.js +112 -0
  41. package/dist/{session-cli-7hgXZDV7.js → session-cli-BuDjm5Ou.js} +20 -7
  42. package/dist/session-permission-policy-gLm40Vtq.js +160 -0
  43. package/dist/session-retirement-CdkI0bvb.js +90 -0
  44. package/dist/{session-upstream-marker-CrIFX06J.js → session-upstream-marker-Bcy33kam.js} +2 -3
  45. package/dist/{settled-turn-finalizer-BRWBO4MO.js → settled-turn-finalizer-B8JJcxCi.js} +27 -22
  46. package/dist/{shared-client-kIBbGjk4.js → shared-client-kNkljY-R.js} +2023 -607
  47. package/dist/{side-question-9HAL26l9.js → side-question-GwD73tX6.js} +109 -57
  48. package/dist/{thread-lifecycle-Bu1BTDXr.js → thread-lifecycle-Ss-uwnC4.js} +1208 -497
  49. package/dist/{thread-ownership-6kOQHlyC.js → thread-ownership-CeYBqg_4.js} +18 -8
  50. package/dist/{plugin-app-cache-key-C3H2CVp6.js → thread-resume-DF54hagX.js} +64 -6
  51. package/dist/{transcript-mirror-B9gGgnJS.js → transcript-mirror-CkEQIna-.js} +158 -41
  52. package/dist/transport-stdio-C7-s9TLG.js +708 -0
  53. package/dist/{upstream-prompt-provenance-ClRBije0.js → upstream-prompt-provenance-CFmsKwr9.js} +12 -14
  54. package/dist/{upstream-session-fork-CE-_LdnP.js → upstream-session-fork-BACGlOFa.js} +86 -88
  55. package/dist/{usage-CHlV0e-o.js → usage-C2l8Mils.js} +3 -3
  56. package/dist/{web-search-provider.runtime-DRw2Vl42.js → web-search-provider.runtime-B2XgRdFv.js} +3 -3
  57. package/openclaw.plugin.json +50 -2
  58. package/package.json +9 -8
  59. package/dist/app-server-policy-zcBDc1JU.js +0 -49
  60. package/dist/incognito-session-ET_ufDH-.js +0 -2
  61. package/dist/request-BRl5_UfX.js +0 -157
  62. package/dist/session-retirement-DoMMJFKm.js +0 -36
  63. package/dist/thread-resume-CPrJNQVM.js +0 -30
  64. package/dist/transport-stdio-DIImwDVS.js +0 -99
@@ -1,811 +1,39 @@
1
- import { C as isCodexRemoteExecPlacementSandbox, T as readCodexPluginConfig } from "./session-binding-DPxIvMgE.js";
2
- import { S as resolveCodexGatewayTimeoutWithGraceMs, ct as isJsonObject, l as releaseLeasedSharedCodexAppServerClient, ot as CODEX_OPENCLAW_DIRECT_DYNAMIC_TOOL_NAMESPACE } from "./shared-client-kIBbGjk4.js";
3
- import { E as itemStatus, F as readItem, b as auditNativeToolUnfinishedStatus, h as isCodexNotificationForTurn, v as auditNativeToolName, y as auditNativeToolTerminalStatus } from "./attempt-client-cleanup-DMLXUfa8.js";
4
- import { l as readCodexTurn } from "./protocol-validators-B4F7L8Rf.js";
5
- import { D as isSystemAgentOnlyCodexDynamicToolAllowlist, E as isForcedPrivateQaCodexRuntime, O as normalizeCodexDynamicToolName, T as filterCodexDynamicToolsForDisabledNativeSurface, f as resolveCodexWebSearchPlan, w as filterCodexDynamicTools } from "./thread-lifecycle-Bu1BTDXr.js";
1
+ import { C as isCodexRemoteExecPlacementSandbox, T as readCodexPluginConfig } from "./config-Cxw3Cl3k.js";
2
+ import { Bt as CODEX_OPENCLAW_DIRECT_DYNAMIC_TOOL_NAMESPACE, Ht as isJsonObject, dt as resolveCodexToolAbortTerminalReason, et as createCodexElicitationResponse, ft as createFailedDynamicToolResponse, h as releaseLeasedSharedCodexAppServerClient, mt as withDynamicToolTranscriptDetails, pt as withDynamicToolExecutionState } from "./shared-client-kNkljY-R.js";
3
+ import { d as readCodexTurn } from "./protocol-validators-Bg5rpNxk.js";
4
+ import { D as itemStatus, I as readItem, b as auditNativeToolTerminalStatus, g as isCodexNotificationForTurn, x as auditNativeToolUnfinishedStatus, y as auditNativeToolName } from "./attempt-client-cleanup-CVPvp9dL.js";
5
+ import { H as normalizeCodexDynamicToolName, L as filterCodexDynamicTools, R as filterCodexDynamicToolsForDisabledNativeSurface, V as isSystemAgentOnlyCodexDynamicToolAllowlist, d as emitCodexNativePreToolUseFailureDiagnostic, x as resolveCodexWebSearchPlan, z as isForcedPrivateQaCodexRuntime } from "./thread-lifecycle-Ss-uwnC4.js";
6
+ import { l as resolveCodexNativeExecutionPolicy, u as resolveCodexNodeExecToolOverrides } from "./request-CCm4LaED.js";
6
7
  import { r as sanitizeInlineImageDataUrl, t as invalidInlineImageText } from "./image-payload-sanitizer-CnfoUPCE.js";
7
- import { r as formatCodexDisplayText } from "./command-formatters-B1AEz8ZA.js";
8
- import { a as resolveCodexNodeExecToolOverrides, i as resolveCodexNativeExecutionPolicy } from "./sandbox-guard-Cmwc9eIk.js";
8
+ import { r as formatCodexDisplayText } from "./command-formatters-C54Yg3eA.js";
9
+ import { a as sanitizeCodexApprovalVisibleText, c as waitForPluginApprovalDecision, i as requestPluginApproval, n as codexApprovalTimeoutText, o as stripDanglingCodexApprovalTerminalSequence, r as mapExecDecisionToOutcome, s as truncateCodexApprovalDisplayText, t as approvalRequestExplicitlyUnavailable } from "./plugin-approval-roundtrip-D80UZiJ_.js";
9
10
  import { asNonArrayRecord, asOptionalRecord, isRecord, normalizeOptionalString, normalizeTrimmedStringList, readStringField } from "openclaw/plugin-sdk/string-coerce-runtime";
11
+ import { validateJsonSchemaValue } from "openclaw/plugin-sdk/json-schema-runtime";
10
12
  import path, { posix } from "node:path";
11
13
  import { createDeferred } from "openclaw/plugin-sdk/extension-shared";
12
- import { addTimerTimeoutGraceMs, asDateTimestampMs, finiteSecondsToTimerSafeMilliseconds, parseStrictNonNegativeInteger } from "openclaw/plugin-sdk/number-runtime";
13
- import { HEARTBEAT_RESPONSE_TOOL_NAME, buildAgentHookContextChannelFields, buildEmbeddedAttemptToolRunContext, consumeAdjustedParamsForToolCall, consumePreExecutionBlockedToolCall, createAgentToolResultMiddlewareRunner, createCodexAppServerToolResultExtensionRunner, embeddedAgentLog, extractMessagingToolSend, extractMessagingToolSendResult, extractToolResultMediaArtifact, filterProviderNormalizableTools, filterToolResultMediaUrls, finalizeToolTerminalPresentation, formatApprovalDisplayPath, formatToolExecutionErrorMessage, getBeforeToolCallFailureDisposition, getChannelAgentToolMeta, getPluginToolMeta, hasNativeHookRelayInvocation, invokeNativeHookRelay, isDeliveredMessageToolOnlySourceReplyResult, isDeliveredMessagingToolResult, isHostScopedAgentToolActive, isMessagingTool, isMessagingToolSendAction, isReplaySafeToolCall, isSubagentSessionKey, isToolResultError, isToolWrappedWithBeforeToolCallHook, normalizeAgentRuntimeTools, normalizeHeartbeatToolResponse, projectRuntimeToolInputSchema, resolveAttemptSpawnWorkspaceDir, resolveModelAuthMode, resolveNativeHookRelayDeferredToolApproval, resolveToolExecutionErrorKind, resolveToolResultFailureKind, runAgentHarnessAfterToolCallHook, sanitizeToolResult, setBeforeToolCallDiagnosticsEnabled, supportsModelTools, wrapToolWithBeforeToolCallHook } from "openclaw/plugin-sdk/agent-harness-runtime";
14
+ import { asDateTimestampMs } from "openclaw/plugin-sdk/number-runtime";
15
+ import { HEARTBEAT_RESPONSE_TOOL_NAME, buildAgentHookContextChannelFields, buildEmbeddedAttemptToolRunContext, consumeAdjustedParamsForToolCall, consumePreExecutionBlockedToolCall, createAgentToolResultMiddlewareRunner, createCodexAppServerToolResultExtensionRunner, embeddedAgentLog, extractMessagingToolSend, extractMessagingToolSendResult, extractToolResultMediaArtifact, filterProviderNormalizableTools, filterToolResultMediaUrls, finalizeToolTerminalPresentation, formatApprovalDisplayPath, formatToolExecutionErrorMessage, getBeforeToolCallFailureDisposition, getChannelAgentToolMeta, getPluginToolMeta, getPluginToolSideEffectOwnerKey, hasNativeHookRelayInvocation, invokeNativeHookRelay, isDeliveredMessageToolOnlySourceReplyResult, isDeliveredMessagingToolResult, isHostScopedAgentToolActive, isMessagingTool, isMessagingToolSendAction, isReplaySafeToolCall, isSubagentSessionKey, isToolResultError, isToolWrappedWithBeforeToolCallHook, normalizeAgentRuntimeTools, normalizeHeartbeatToolResponse, projectRuntimeToolInputSchema, resolveAttemptSpawnWorkspaceDir, resolveModelAuthMode, resolveNativeHookRelayDeferredToolApproval, resolveToolExecutionErrorKind, resolveToolResultFailureKind, runAgentHarnessAfterToolCallHook, sanitizeToolResult, setBeforeToolCallDiagnosticsEnabled, supportsModelTools, wrapToolWithBeforeToolCallHook } from "openclaw/plugin-sdk/agent-harness-runtime";
14
16
  import { createHash, randomUUID } from "node:crypto";
15
17
  import { resolveAgentDir as resolveAgentDir$1 } from "openclaw/plugin-sdk/agent-runtime";
18
+ import { formatErrorMessage as formatErrorMessage$1, redactSensitiveText } from "openclaw/plugin-sdk/security-runtime";
19
+ import { killProcessTree } from "openclaw/plugin-sdk/process-runtime";
16
20
  import { SsrFBlockedError, isBlockedHostnameOrIp } from "openclaw/plugin-sdk/ssrf-runtime";
17
21
  import { spawn } from "node:child_process";
18
- import { coerceErrorMessage, isApprovalNotFoundError, toErrorObject } from "openclaw/plugin-sdk/error-runtime";
19
- import { DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS, estimateToolResultTextChars, resolveLiveToolResultMaxChars, sliceToolResultTextToBudget, sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
20
22
  import { once } from "node:events";
23
+ import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
24
+ import { DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS, estimateToolResultTextChars, resolveLiveToolResultMaxChars, sliceToolResultTextToBudget, sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
25
+ import { emitTrustedDiagnosticEvent } from "openclaw/plugin-sdk/diagnostic-runtime";
21
26
  import { isIP } from "node:net";
22
27
  import { WebSocketServer } from "ws";
23
28
  import { fileURLToPath, pathToFileURL } from "node:url";
24
29
  import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
25
- import { runWithCronCreatorAuthorityCapabilityResolver } from "openclaw/plugin-sdk/codex-mcp-projection";
26
- import { emitTrustedDiagnosticEvent, hasPendingInternalDiagnosticEvent } from "openclaw/plugin-sdk/diagnostic-runtime";
27
- import { isToolAllowed, sanitizeEnvVars } from "openclaw/plugin-sdk/sandbox";
30
+ import { redactToolPayloadText } from "openclaw/plugin-sdk/logging-core";
31
+ import { isPathStrictlyInside, root } from "openclaw/plugin-sdk/file-access-runtime";
32
+ import { buildRemoteCommand, isToolAllowed, sanitizeEnvVars } from "openclaw/plugin-sdk/sandbox";
33
+ import { pinExecToolTarget, resolveCodexScheduledToolProjectionFactory, runWithCronCreatorAuthorityCapabilityResolver } from "openclaw/plugin-sdk/codex-mcp-projection";
28
34
  import { saveMediaBuffer } from "openclaw/plugin-sdk/media-store";
29
- import { registerRetainedNativeHookRelayForBundledRuntime } from "openclaw/plugin-sdk/native-hook-relay-runtime";
30
- import { root } from "openclaw/plugin-sdk/file-access-runtime";
31
35
  import { getMediaDir } from "openclaw/plugin-sdk/media-runtime";
32
36
  import { normalizeOpenAIToolSchemas } from "openclaw/plugin-sdk/provider-tools";
33
- //#region extensions/codex/src/app-server/dynamic-tool-response-state.ts
34
- /** Retains the host-owned app preview without adding it to Codex's response payload. */
35
- function withDynamicToolTranscriptDetails(response, details) {
36
- if (!details || typeof details !== "object" || Array.isArray(details)) return response;
37
- const mcpAppPreview = details.mcpAppPreview;
38
- if (!mcpAppPreview || typeof mcpAppPreview !== "object" || Array.isArray(mcpAppPreview)) return response;
39
- Object.defineProperty(response, "transcriptDetails", {
40
- configurable: true,
41
- enumerable: false,
42
- value: { mcpAppPreview }
43
- });
44
- return response;
45
- }
46
- function withDynamicToolTerminalResolution(response, terminalResolution) {
47
- if (terminalResolution) {
48
- Object.defineProperties(response, {
49
- terminalResolution: {
50
- configurable: true,
51
- enumerable: false,
52
- value: terminalResolution
53
- },
54
- executionStarted: {
55
- configurable: true,
56
- enumerable: false,
57
- value: terminalResolution.executionStarted
58
- },
59
- ...terminalResolution.executedArguments ? { executedArguments: {
60
- configurable: true,
61
- enumerable: false,
62
- value: terminalResolution.executedArguments
63
- } } : {}
64
- });
65
- withDynamicToolSideEffectEvidence(response, terminalResolution.sideEffectEvidence);
66
- }
67
- return response;
68
- }
69
- function withDynamicToolExecutionState(response, state) {
70
- Object.defineProperties(response, {
71
- executedArguments: {
72
- configurable: true,
73
- enumerable: false,
74
- value: state.executedArguments
75
- },
76
- executionStarted: {
77
- configurable: true,
78
- enumerable: false,
79
- value: state.executionStarted
80
- }
81
- });
82
- return withDynamicToolSideEffectEvidence(response, state.sideEffectEvidence === true);
83
- }
84
- function withDynamicToolSideEffectEvidence(response, sideEffectEvidence) {
85
- if (!sideEffectEvidence) {
86
- delete response.sideEffectEvidence;
87
- return response;
88
- }
89
- Object.defineProperty(response, "sideEffectEvidence", {
90
- configurable: true,
91
- enumerable: false,
92
- value: true
93
- });
94
- return response;
95
- }
96
- function createFailedDynamicToolResponse(message, options) {
97
- const response = {
98
- contentItems: [{
99
- type: "inputText",
100
- text: message
101
- }],
102
- success: false
103
- };
104
- Object.defineProperties(response, {
105
- diagnosticTerminalReason: {
106
- configurable: true,
107
- enumerable: false,
108
- value: options?.terminalReason ?? "failed"
109
- },
110
- diagnosticTerminalType: {
111
- configurable: true,
112
- enumerable: false,
113
- value: "error"
114
- }
115
- });
116
- if (options?.executionStarted !== void 0) Object.defineProperty(response, "executionStarted", {
117
- configurable: true,
118
- enumerable: false,
119
- value: options.executionStarted
120
- });
121
- if (options?.executedArguments !== void 0) Object.defineProperty(response, "executedArguments", {
122
- configurable: true,
123
- enumerable: false,
124
- value: options.executedArguments
125
- });
126
- return withDynamicToolSideEffectEvidence(response, options?.sideEffectEvidence === true);
127
- }
128
- //#endregion
129
- //#region extensions/codex/src/app-server/tool-abort-terminal-reason.ts
130
- /** Leaf helper shared by native and dynamic tool diagnostics. */
131
- const CODEX_TIMEOUT_ABORT_REASONS = /* @__PURE__ */ new Set([
132
- "codex_startup_timeout",
133
- "turn_completion_idle_timeout",
134
- "turn_progress_idle_timeout",
135
- "turn_terminal_idle_timeout"
136
- ]);
137
- /** Preserves timeout provenance when an enclosing run aborts an active tool. */
138
- function resolveCodexToolAbortTerminalReason(signal) {
139
- try {
140
- const reason = signal.reason;
141
- if (typeof reason === "string") {
142
- if (CODEX_TIMEOUT_ABORT_REASONS.has(reason)) return "timed_out";
143
- return reason === "client_closed" ? "failed" : "cancelled";
144
- }
145
- if (reason && typeof reason === "object") {
146
- const record = reason;
147
- if (record.name === "TimeoutError" || record.reason === "timeout") return "timed_out";
148
- }
149
- } catch {
150
- return "cancelled";
151
- }
152
- return "cancelled";
153
- }
154
- //#endregion
155
- //#region extensions/codex/src/app-server/dynamic-tool-execution.ts
156
- /**
157
- * Timeout, terminal-release, and diagnostic helpers for Codex dynamic tool
158
- * calls.
159
- */
160
- /** Default timeout for Codex dynamic tool calls. */
161
- const CODEX_DYNAMIC_TOOL_TIMEOUT_MS = 9e4;
162
- /** Hard cap for per-call Codex dynamic tool timeout overrides. */
163
- const CODEX_DYNAMIC_TOOL_MAX_TIMEOUT_MS = 6e5;
164
- const CODEX_DYNAMIC_TOOL_TIMEOUT_SECONDS_GRACE_MS = 3e4;
165
- const CODEX_DYNAMIC_IMAGE_GENERATION_TOOL_TIMEOUT_MS = 12e4;
166
- const CODEX_DYNAMIC_COMPUTER_GATEWAY_TIMEOUT_MS = 3e4;
167
- const CODEX_DYNAMIC_COMPUTER_COMPLETION_GRACE_MS = 3e4;
168
- /** Timeout for image-understanding style dynamic tool calls. */
169
- const CODEX_DYNAMIC_IMAGE_TOOL_TIMEOUT_MS = 6e4;
170
- /** Timeout for message-delivery dynamic tool calls. */
171
- const CODEX_DYNAMIC_MESSAGE_TOOL_TIMEOUT_MS = 6e5;
172
- /** Outer default for collector waits: full swarm budget plus completion grace. */
173
- const CODEX_DYNAMIC_AGENTS_WAIT_TOOL_TIMEOUT_MS = 63e4;
174
- const LOG_FIELD_MAX_LENGTH = 160;
175
- function normalizeLogField(value) {
176
- if (typeof value !== "string") return;
177
- const normalized = value.replaceAll(String.fromCharCode(27), " ").replaceAll("\r", " ").replaceAll("\n", " ").replaceAll(" ", " ").trim();
178
- if (!normalized) return;
179
- return normalized.length > LOG_FIELD_MAX_LENGTH ? `${truncateUtf16Safe(normalized, LOG_FIELD_MAX_LENGTH - 3)}...` : normalized;
180
- }
181
- function readNumericTimeoutMs(value) {
182
- if (typeof value === "number" && Number.isFinite(value)) return Math.max(0, Math.floor(value));
183
- if (typeof value === "string") {
184
- const parsed = parseStrictNonNegativeInteger(value);
185
- if (parsed !== void 0) return Math.max(0, Math.floor(parsed));
186
- }
187
- }
188
- function formatDynamicToolTimeoutDetails(params) {
189
- const tool = normalizeLogField(params.call.tool) ?? "unknown";
190
- const baseMeta = {
191
- tool: params.call.tool,
192
- toolCallId: params.call.callId,
193
- threadId: params.call.threadId,
194
- turnId: params.call.turnId,
195
- timeoutMs: params.timeoutMs,
196
- timeoutKind: "codex_dynamic_tool_rpc"
197
- };
198
- if (tool !== "process" || !isJsonObject(params.call.arguments)) return {
199
- responseMessage: `OpenClaw dynamic tool call timed out after ${params.timeoutMs}ms while running tool ${tool}.`,
200
- consoleMessage: `codex dynamic tool timeout: tool=${tool} toolTimeoutMs=${params.timeoutMs}; per-tool-call watchdog, not session idle`,
201
- meta: baseMeta
202
- };
203
- const action = normalizeLogField(params.call.arguments.action);
204
- const sessionId = normalizeLogField(params.call.arguments.sessionId);
205
- const requestedTimeoutMs = readNumericTimeoutMs(params.call.arguments.timeout);
206
- const actionPart = action ? ` action=${action}` : "";
207
- const sessionPart = sessionId ? ` sessionId=${sessionId}` : "";
208
- const requestedPart = requestedTimeoutMs === void 0 ? "" : ` requestedWaitMs=${requestedTimeoutMs}`;
209
- const retryHint = action === "poll" ? "; repeated lines usually mean process-poll retry churn, not model progress" : "";
210
- const responseTarget = action || sessionId ? ` while waiting for process${actionPart}${sessionPart}` : " while waiting for the process tool";
211
- return {
212
- responseMessage: `OpenClaw dynamic tool call timed out after ${params.timeoutMs}ms${responseTarget}. This is a tool RPC timeout, not a session idle timeout.`,
213
- consoleMessage: `codex process tool timeout:${actionPart}${sessionPart} toolTimeoutMs=${params.timeoutMs}${requestedPart}; per-tool-call watchdog, not session idle${retryHint}`,
214
- meta: {
215
- ...baseMeta,
216
- processAction: action,
217
- processSessionId: sessionId,
218
- processRequestedTimeoutMs: requestedTimeoutMs
219
- }
220
- };
221
- }
222
- /**
223
- * Runs a dynamic tool call with run-abort and per-call timeout handling,
224
- * returning a Codex protocol response instead of throwing.
225
- */
226
- async function handleDynamicToolCallWithTimeout(params) {
227
- let didNotifyAgentToolResult = false;
228
- const conservativeRaceResponses = /* @__PURE__ */ new WeakSet();
229
- const finalizeTerminal = (response) => {
230
- const executionSnapshot = params.toolBridge.consumeToolExecutionSnapshot?.(params.call.callId);
231
- const observedExecutionStarted = executionSnapshot?.executionStarted ?? (conservativeRaceResponses.has(response) ? void 0 : response.executionStarted);
232
- const terminalResolution = params.observeToolTerminal?.({
233
- toolCallId: params.call.callId,
234
- toolName: params.call.tool,
235
- arguments: response.executedArguments ?? executionSnapshot?.executedArguments ?? params.call.arguments,
236
- ...params.toolMeta ? { meta: params.toolMeta } : {},
237
- ...observedExecutionStarted !== void 0 ? { executionStarted: observedExecutionStarted } : {},
238
- outcome: response.success ? "success" : "failure",
239
- ...!response.success ? { failure: { error: readDynamicToolResponseText(response) } } : {}
240
- });
241
- return withDynamicToolTerminalResolution(response, terminalResolution);
242
- };
243
- const createFailedAfterPossibleDispatch = (message, terminalReason) => {
244
- const response = createFailedDynamicToolResponse(message, {
245
- executionStarted: true,
246
- sideEffectEvidence: true,
247
- terminalReason
248
- });
249
- conservativeRaceResponses.add(response);
250
- return response;
251
- };
252
- const notifyAgentToolResult = (event) => {
253
- if (didNotifyAgentToolResult) return;
254
- didNotifyAgentToolResult = true;
255
- try {
256
- params.onAgentToolResult?.(event);
257
- } catch (error) {
258
- embeddedAgentLog.warn(`onAgentToolResult handler failed: tool=${params.call.tool} error=${String(error)}`);
259
- }
260
- };
261
- const notifyFailedToolResult = (message, terminalReason = "failed") => {
262
- notifyAgentToolResult({
263
- toolName: params.call.tool,
264
- result: {
265
- content: [{
266
- type: "text",
267
- text: message
268
- }],
269
- details: {
270
- status: terminalReason,
271
- error: message
272
- }
273
- },
274
- isError: true
275
- });
276
- };
277
- if (params.signal.aborted) {
278
- const message = "OpenClaw dynamic tool call aborted before execution.";
279
- const terminalReason = resolveCodexToolAbortTerminalReason(params.signal);
280
- params.onFallbackSelected?.();
281
- notifyFailedToolResult(message, terminalReason);
282
- return finalizeTerminal(createFailedDynamicToolResponse(message, {
283
- executionStarted: false,
284
- terminalReason
285
- }));
286
- }
287
- const controller = new AbortController();
288
- let timeout;
289
- let timedOut = false;
290
- let resolveAbort;
291
- const abortFromRun = () => {
292
- const message = "OpenClaw dynamic tool call aborted.";
293
- const terminalReason = resolveCodexToolAbortTerminalReason(params.signal);
294
- params.onFallbackSelected?.();
295
- controller.abort(params.signal.reason ?? /* @__PURE__ */ new Error(message));
296
- notifyFailedToolResult(message, terminalReason);
297
- resolveAbort?.(createFailedAfterPossibleDispatch(message, terminalReason));
298
- };
299
- const abortPromise = new Promise((resolve) => {
300
- resolveAbort = resolve;
301
- });
302
- const timeoutPromise = new Promise((resolve) => {
303
- const timeoutMs = clampDynamicToolTimeoutMs(params.timeoutMs);
304
- timeout = setTimeout(() => {
305
- timedOut = true;
306
- const timeoutDetails = formatDynamicToolTimeoutDetails({
307
- call: params.call,
308
- timeoutMs
309
- });
310
- params.onFallbackSelected?.();
311
- controller.abort(new Error(timeoutDetails.responseMessage));
312
- params.onTimeout?.();
313
- embeddedAgentLog.warn("codex dynamic tool call timed out", {
314
- ...timeoutDetails.meta,
315
- consoleMessage: timeoutDetails.consoleMessage
316
- });
317
- notifyFailedToolResult(timeoutDetails.responseMessage, "timed_out");
318
- resolve(createFailedAfterPossibleDispatch(timeoutDetails.responseMessage, "timed_out"));
319
- }, timeoutMs);
320
- timeout.unref?.();
321
- });
322
- try {
323
- params.signal.addEventListener("abort", abortFromRun, { once: true });
324
- if (params.signal.aborted) abortFromRun();
325
- const response = await Promise.race([
326
- params.toolBridge.handleToolCall(params.call, {
327
- signal: controller.signal,
328
- onAgentToolResult: notifyAgentToolResult,
329
- toolCallOrdinal: params.toolCallOrdinal,
330
- retainExecutionSnapshot: true
331
- }),
332
- abortPromise,
333
- timeoutPromise
334
- ]);
335
- if (!response.success && !didNotifyAgentToolResult) notifyFailedToolResult(readDynamicToolResponseText(response), response.diagnosticTerminalReason ?? "failed");
336
- return finalizeTerminal(response);
337
- } catch (error) {
338
- const terminalReason = params.signal.aborted ? resolveCodexToolAbortTerminalReason(params.signal) : resolveToolExecutionErrorKind(error);
339
- const message = formatToolExecutionErrorMessage(error, "OpenClaw dynamic tool call failed.");
340
- notifyFailedToolResult(message, terminalReason);
341
- return finalizeTerminal(createFailedAfterPossibleDispatch(message, terminalReason));
342
- } finally {
343
- if (timeout) clearTimeout(timeout);
344
- params.signal.removeEventListener("abort", abortFromRun);
345
- resolveAbort = void 0;
346
- if (!timedOut && !controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("OpenClaw dynamic tool call finished."));
347
- }
348
- }
349
- function readDynamicToolResponseText(response) {
350
- return response.contentItems.flatMap((item) => item.type === "inputText" && typeof item.text === "string" ? [item.text] : []).join("\n").trim() || "OpenClaw dynamic tool call failed.";
351
- }
352
- /** Strips OpenClaw-only metadata before sending a dynamic tool response to Codex. */
353
- function toCodexDynamicToolProtocolResponse(response) {
354
- return {
355
- contentItems: response.contentItems,
356
- success: response.success
357
- };
358
- }
359
- /** Adds async-started progress details when a tool result continues out of band. */
360
- function toCodexDynamicToolProgressResponse(response, protocolResponse) {
361
- const transcriptDetails = response.transcriptDetails;
362
- if (response.asyncStarted !== true && transcriptDetails === void 0) return protocolResponse;
363
- return {
364
- ...protocolResponse,
365
- ...transcriptDetails ? { details: transcriptDetails } : {},
366
- ...response.asyncStarted === true ? { details: {
367
- ...transcriptDetails,
368
- async: true,
369
- status: "started"
370
- } } : {}
371
- };
372
- }
373
- /** Decides whether a terminal dynamic tool response can release the Codex turn. */
374
- function shouldReleaseTurnAfterTerminalDynamicTool(state) {
375
- return !state.completed && !state.aborted && state.responseSuccess && !state.currentTurnHadNonTerminalDynamicToolResult && state.activeAppServerTurnRequests === 0 && state.activeTurnItemIdsCount === 0 && state.pendingOpenClawDynamicToolCompletionIdsCount === 0;
376
- }
377
- /** Returns true when a non-async result should block terminal-release shortcuts. */
378
- function shouldBlockTerminalReleaseForNonTerminalDynamicToolResult(response) {
379
- return response.asyncStarted !== true;
380
- }
381
- /** Resolves whether terminal diagnostic state should release, wait, or stay idle. */
382
- function resolveTerminalDynamicToolBatchAction(state) {
383
- if (state.activeAppServerTurnRequests > 0 || state.activeTurnItemIdsCount > 0 || state.pendingOpenClawDynamicToolCompletionIdsCount > 0) return "wait";
384
- if (state.currentTurnHadNonTerminalDynamicToolResult) return "clear-nonterminal-batch";
385
- if (state.hasPendingTerminalDynamicToolRelease) return "release-pending-terminal";
386
- return "idle";
387
- }
388
- /** Returns true for diagnostic events that terminate a dynamic tool call. */
389
- function isDynamicToolTerminalDiagnosticEvent(event) {
390
- return event.type === "tool.execution.completed" || event.type === "tool.execution.error" || event.type === "tool.execution.blocked";
391
- }
392
- /** Matches terminal diagnostics to a specific dynamic tool call id/name. */
393
- function isMatchingDynamicToolTerminalDiagnostic(params) {
394
- if (params.event.toolCallId !== params.call.callId || params.event.toolName !== params.call.tool) return false;
395
- if (params.runId !== void 0) return params.event.runId === params.runId;
396
- if (params.sessionId !== void 0) return params.event.sessionId === params.sessionId;
397
- if (params.sessionKey !== void 0) return params.event.sessionKey === params.sessionKey;
398
- return params.event.runId === void 0 && params.event.sessionId === void 0 && params.event.sessionKey === void 0;
399
- }
400
- /** Checks pending diagnostics for a terminal event matching a tool call. */
401
- function hasPendingDynamicToolTerminalDiagnostic(params) {
402
- return hasPendingInternalDiagnosticEvent((event) => {
403
- if (!isDynamicToolTerminalDiagnosticEvent(event)) return false;
404
- return isMatchingDynamicToolTerminalDiagnostic({
405
- event,
406
- call: params.call,
407
- runId: params.runId,
408
- sessionId: params.sessionId,
409
- sessionKey: params.sessionKey
410
- });
411
- });
412
- }
413
- /** Resolves per-tool timeout, applying media/message defaults and hard caps. */
414
- function resolveDynamicToolCallTimeoutMs(params) {
415
- if (params.call.tool === "computer") return clampDynamicToolTimeoutMs(readComputerToolTimeoutMs(params.call.arguments));
416
- if (params.call.tool === "message") return CODEX_DYNAMIC_MESSAGE_TOOL_TIMEOUT_MS;
417
- if (params.call.tool === "agents_wait") {
418
- const requestedMs = readDynamicToolCallTimeoutMs(params.call.arguments) ?? readConfiguredDynamicToolTimeoutMs(params.call.tool, params.config) ?? CODEX_DYNAMIC_AGENTS_WAIT_TOOL_TIMEOUT_MS;
419
- return Math.max(1, Math.min(63e4, Math.floor(requestedMs)));
420
- }
421
- return clampDynamicToolTimeoutMs(readDynamicToolCallTimeoutMs(params.call.arguments) ?? readConfiguredDynamicToolTimeoutMs(params.call.tool, params.config) ?? CODEX_DYNAMIC_TOOL_TIMEOUT_MS);
422
- }
423
- function readComputerToolTimeoutMs(value) {
424
- const args = isJsonObject(value) ? value : void 0;
425
- const action = typeof args?.action === "string" ? args.action : void 0;
426
- const gatewayTimeoutMs = readPositiveFiniteTimeoutMs(args?.timeoutMs) ?? CODEX_DYNAMIC_COMPUTER_GATEWAY_TIMEOUT_MS;
427
- const gatewayCallCount = action === "screenshot" || action === "wait" ? 3 : 4;
428
- return (action === "wait" || action === "hold_key" ? Math.max(0, Number(args?.duration) || 0) * 1e3 : 0) + gatewayCallCount * gatewayTimeoutMs + CODEX_DYNAMIC_COMPUTER_COMPLETION_GRACE_MS;
429
- }
430
- function readDynamicToolCallTimeoutMs(value) {
431
- if (!isJsonObject(value)) return;
432
- const timeoutMs = readPositiveFiniteTimeoutMs(value.timeoutMs);
433
- if (timeoutMs !== void 0) return timeoutMs;
434
- const timeoutSecondsMs = readDynamicToolTimeoutSecondsAsMs(value.timeoutSeconds);
435
- return timeoutSecondsMs === void 0 ? void 0 : addTimerTimeoutGraceMs(timeoutSecondsMs, CODEX_DYNAMIC_TOOL_TIMEOUT_SECONDS_GRACE_MS);
436
- }
437
- function readConfiguredDynamicToolTimeoutMs(toolName, config) {
438
- if (toolName === "image_generate") {
439
- const imageModel = config?.agents?.defaults?.mediaModels?.image;
440
- if (!imageModel || typeof imageModel !== "object") return CODEX_DYNAMIC_IMAGE_GENERATION_TOOL_TIMEOUT_MS;
441
- return readPositiveFiniteTimeoutMs(imageModel.timeoutMs) ?? CODEX_DYNAMIC_IMAGE_GENERATION_TOOL_TIMEOUT_MS;
442
- }
443
- if (toolName === "image") {
444
- const candidates = (config?.tools?.media?.models ?? []).filter((entry) => !entry.capabilities || entry.capabilities.includes("image"));
445
- const capabilityTimeoutMs = readTimeoutSecondsAsMs(config?.tools?.media?.image?.timeoutSeconds);
446
- return Math.max(capabilityTimeoutMs ?? CODEX_DYNAMIC_IMAGE_TOOL_TIMEOUT_MS, ...candidates.map((entry) => readTimeoutSecondsAsMs(entry.timeoutSeconds) ?? capabilityTimeoutMs ?? CODEX_DYNAMIC_IMAGE_TOOL_TIMEOUT_MS));
447
- }
448
- if (toolName === "message") return CODEX_DYNAMIC_MESSAGE_TOOL_TIMEOUT_MS;
449
- }
450
- function readTimeoutSecondsAsMs(value) {
451
- const seconds = readPositiveFiniteTimeoutMs(value);
452
- return seconds === void 0 ? void 0 : seconds * 1e3;
453
- }
454
- function readDynamicToolTimeoutSecondsAsMs(value) {
455
- if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value <= 0) return;
456
- return value * 1e3;
457
- }
458
- function readPositiveFiniteTimeoutMs(value) {
459
- return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : void 0;
460
- }
461
- function clampDynamicToolTimeoutMs(timeoutMs) {
462
- return Math.max(1, Math.min(CODEX_DYNAMIC_TOOL_MAX_TIMEOUT_MS, Math.floor(timeoutMs)));
463
- }
464
- //#endregion
465
- //#region extensions/codex/src/app-server/native-hook-relay-state.ts
466
- const pending = /* @__PURE__ */ new Set();
467
- /** Owns delayed hook-relay cleanup across runtime scheduling and test teardown. */
468
- const nativeHookRelayUnregisterQueue = {
469
- add(entry) {
470
- pending.add(entry);
471
- },
472
- delete(entry) {
473
- return pending.delete(entry);
474
- },
475
- flush() {
476
- while (pending.size > 0) {
477
- const entry = pending.values().next().value;
478
- if (!entry) return;
479
- clearTimeout(entry.timeout);
480
- entry.unregister();
481
- }
482
- },
483
- clear() {
484
- for (const entry of pending) clearTimeout(entry.timeout);
485
- pending.clear();
486
- }
487
- };
488
- //#endregion
489
- //#region extensions/codex/src/app-server/native-hook-relay.ts
490
- /**
491
- * Bridges Codex native hook callbacks into OpenClaw's native hook relay so
492
- * app-server tool events can still run OpenClaw policy and diagnostics.
493
- */
494
- /** Codex hook events that can be registered through OpenClaw's native relay. */
495
- const CODEX_NATIVE_HOOK_RELAY_EVENTS = [
496
- "pre_tool_use",
497
- "post_tool_use",
498
- "permission_request",
499
- "before_agent_finalize"
500
- ];
501
- const CODEX_NATIVE_HOOK_RELAY_EVENTS_WITH_APP_SERVER_APPROVALS = CODEX_NATIVE_HOOK_RELAY_EVENTS.filter((event) => event !== "permission_request");
502
- const CODEX_NATIVE_HOOK_RELAY_MIN_TTL_MS = 30 * 6e4;
503
- /** Extra relay lifetime after the expected turn budget, preventing late hook drops. */
504
- const CODEX_NATIVE_HOOK_RELAY_TTL_GRACE_MS = 5 * 6e4;
505
- const CODEX_NATIVE_HOOK_RELAY_COMMAND_MIN_PARENT_MARGIN_MS = 250;
506
- const CODEX_NATIVE_HOOK_RELAY_COMMAND_MAX_PARENT_MARGIN_MS = 1e3;
507
- const CODEX_NATIVE_HOOK_RELAY_DEFAULT_TIMEOUT_SEC = 10;
508
- const CODEX_NATIVE_HOOK_RELAY_UNREGISTER_GRACE_MS = 1e4;
509
- const CODEX_NATIVE_HOOK_RELAY_UNREGISTER_EXTRA_GRACE_MS = 5e3;
510
- const MAX_PENDING_DIRECT_CHILD_ADMISSIONS = 32;
511
- const CODEX_HOOK_MATCHER_NAMES_BY_TOOL_ID = {
512
- exec: [
513
- "Bash",
514
- "exec",
515
- "exec_command"
516
- ],
517
- apply_patch: [
518
- "apply_patch",
519
- "Write",
520
- "Edit"
521
- ],
522
- spawn_agent: ["spawn_agent", "Agent"]
523
- };
524
- /** Defers relay unregister so late native hook subprocesses can still resolve. */
525
- function scheduleCodexNativeHookRelayUnregister(params) {
526
- let pending;
527
- const unregister = () => {
528
- if (!pending) return;
529
- const current = pending;
530
- pending = void 0;
531
- if (!nativeHookRelayUnregisterQueue.delete(current)) return;
532
- params.relay.unregister();
533
- };
534
- const timeout = setTimeout(unregister, resolveCodexNativeHookRelayUnregisterGraceMs(params.hookTimeoutSec));
535
- pending = {
536
- timeout,
537
- unregister
538
- };
539
- nativeHookRelayUnregisterQueue.add(pending);
540
- timeout.unref();
541
- }
542
- /** Computes the delayed unregister window from Codex's hook timeout. */
543
- function resolveCodexNativeHookRelayUnregisterGraceMs(hookTimeoutSec) {
544
- const hookTimeoutMs = finiteSecondsToTimerSafeMilliseconds(normalizeHookTimeoutSec(hookTimeoutSec)) ?? 0;
545
- return Math.max(CODEX_NATIVE_HOOK_RELAY_UNREGISTER_GRACE_MS, addTimerTimeoutGraceMs(hookTimeoutMs, CODEX_NATIVE_HOOK_RELAY_UNREGISTER_EXTRA_GRACE_MS) ?? 0);
546
- }
547
- /** Records a native pre-tool failure that Codex does not project as a tool item. */
548
- function emitCodexNativePreToolUseFailureDiagnostic(params) {
549
- emitTrustedDiagnosticEvent({
550
- type: "tool.execution.error",
551
- ...params.agentId ? { agentId: params.agentId } : {},
552
- sessionId: params.sessionId,
553
- ...params.sessionKey ? { sessionKey: params.sessionKey } : {},
554
- runId: params.runId,
555
- toolName: params.failure.toolName,
556
- toolCallId: params.failure.toolCallId,
557
- durationMs: params.failure.durationMs,
558
- errorCategory: "before_tool_call",
559
- terminalReason: params.terminalReason ?? (params.signal?.aborted ? resolveCodexToolAbortTerminalReason(params.signal) : params.failure.disposition),
560
- ...params.sourceTimestampMs !== void 0 ? { sourceTimestampMs: params.sourceTimestampMs } : {}
561
- });
562
- }
563
- /** Registers an OpenClaw native hook relay for a Codex app-server turn. */
564
- function createCodexNativeHookRelay(params) {
565
- if (params.options?.enabled === false) return;
566
- const directChildClaims = /* @__PURE__ */ new Map();
567
- const pendingDirectChildAdmissions = /* @__PURE__ */ new Map();
568
- let foregroundClosed = false;
569
- let successfulYieldRetentionAuthorized = false;
570
- const assertClaim = (threadId, claim) => () => directChildClaims.get(threadId) === claim;
571
- const rejectPendingAdmissions = (reason) => {
572
- for (const pending of pendingDirectChildAdmissions.values()) pending.reject(new Error(reason));
573
- pendingDirectChildAdmissions.clear();
574
- };
575
- const relay = registerRetainedNativeHookRelayForBundledRuntime({
576
- provider: "codex",
577
- relayId: buildCodexNativeHookRelayId({
578
- agentId: params.agentId,
579
- sessionId: params.sessionId,
580
- sessionKey: params.sessionKey
581
- }),
582
- ...params.generation ? { generation: params.generation } : {},
583
- ...params.generationMismatchGraceMs ? { generationMismatchGraceMs: params.generationMismatchGraceMs } : {},
584
- ...params.agentId ? { agentId: params.agentId } : {},
585
- sessionId: params.sessionId,
586
- ...params.sessionKey ? { sessionKey: params.sessionKey } : {},
587
- ...params.config ? { config: params.config } : {},
588
- runId: params.runId,
589
- ...params.channelId ? { channelId: params.channelId } : {},
590
- ...params.requester ? { requester: params.requester } : {},
591
- ...params.approvalContext ? { approvalContext: params.approvalContext } : {},
592
- allowedEvents: params.events,
593
- preToolUseLoopDetection: params.loopDetectionPreToolUseRelay,
594
- ttlMs: resolveCodexNativeHookRelayTtlMs({
595
- explicitTtlMs: params.options?.ttlMs,
596
- attemptTimeoutMs: params.attemptTimeoutMs,
597
- startupTimeoutMs: params.startupTimeoutMs,
598
- turnStartTimeoutMs: params.turnStartTimeoutMs
599
- }),
600
- signal: params.signal,
601
- runBeforeToolCall: params.hostCapabilities.runBeforeToolCall,
602
- assertActive: params.hostCapabilities.assertActive,
603
- retention: {
604
- readClaim: readCodexNativeChildThreadId,
605
- shouldRetainAfterForegroundClose: () => successfulYieldRetentionAuthorized && directChildClaims.size > 0,
606
- allowPreToolUse: (childThreadId) => directChildClaims.has(childThreadId),
607
- awaitForegroundAdmission: (childThreadId) => {
608
- if (foregroundClosed) return Promise.reject(/* @__PURE__ */ new Error("native hook relay foreground admission unavailable"));
609
- const existingClaim = directChildClaims.get(childThreadId);
610
- if (existingClaim) return Promise.resolve(assertClaim(childThreadId, existingClaim));
611
- const existingPending = pendingDirectChildAdmissions.get(childThreadId);
612
- if (existingPending) return existingPending.promise.then((claim) => assertClaim(childThreadId, claim));
613
- if (pendingDirectChildAdmissions.size >= MAX_PENDING_DIRECT_CHILD_ADMISSIONS) return Promise.reject(/* @__PURE__ */ new Error("native hook relay foreground admission capacity reached"));
614
- const { promise, resolve, reject } = createDeferred();
615
- pendingDirectChildAdmissions.set(childThreadId, {
616
- promise,
617
- resolve,
618
- reject
619
- });
620
- return promise.then((claim) => assertClaim(childThreadId, claim));
621
- },
622
- onDispose: () => {
623
- foregroundClosed = true;
624
- rejectPendingAdmissions("native hook relay registration closed");
625
- }
626
- },
627
- onPreToolUseFailure: params.onPreToolUseFailure,
628
- command: {
629
- nice: 10,
630
- timeoutMs: params.options?.gatewayTimeoutMs
631
- }
632
- });
633
- const unregister = () => {
634
- foregroundClosed = true;
635
- rejectPendingAdmissions("native hook relay foreground closed");
636
- relay.unregister();
637
- };
638
- return {
639
- ...relay,
640
- unregister,
641
- authorizeRetentionAfterSuccessfulYield: () => {
642
- successfulYieldRetentionAuthorized = true;
643
- },
644
- rejectPendingDirectChild: (threadIdInput, reason) => {
645
- const threadId = threadIdInput.trim();
646
- const pending = threadId ? pendingDirectChildAdmissions.get(threadId) : void 0;
647
- if (!pending) return;
648
- pendingDirectChildAdmissions.delete(threadId);
649
- pending.reject(new Error(reason));
650
- },
651
- claimDirectChild: (threadIdInput) => {
652
- const threadId = threadIdInput.trim();
653
- if (!threadId) return () => void 0;
654
- if (directChildClaims.get(threadId)) return () => void 0;
655
- const claim = Symbol(threadId);
656
- directChildClaims.set(threadId, claim);
657
- const pending = pendingDirectChildAdmissions.get(threadId);
658
- pendingDirectChildAdmissions.delete(threadId);
659
- pending?.resolve(claim);
660
- let released = false;
661
- return () => {
662
- if (released) return;
663
- released = true;
664
- if (directChildClaims.get(threadId) !== claim) return;
665
- directChildClaims.delete(threadId);
666
- if (foregroundClosed && directChildClaims.size === 0) relay.unregister();
667
- };
668
- }
669
- };
670
- }
671
- function readCodexNativeChildThreadId(rawPayload) {
672
- if (!isJsonObject(rawPayload) || typeof rawPayload.agent_id !== "string") return;
673
- return rawPayload.agent_id.trim() || void 0;
674
- }
675
- /** Selects the native hook events Codex should install for the current approval mode. */
676
- function resolveCodexNativeHookRelayEvents(params) {
677
- if (params.configuredEvents?.length) return params.configuredEvents;
678
- return params.appServer.approvalPolicy === "never" ? CODEX_NATIVE_HOOK_RELAY_EVENTS : CODEX_NATIVE_HOOK_RELAY_EVENTS_WITH_APP_SERVER_APPROVALS;
679
- }
680
- /** Derives the native hook relay TTL from the turn budget unless explicitly configured. */
681
- function resolveCodexNativeHookRelayTtlMs(params) {
682
- if (params.explicitTtlMs !== void 0) return params.explicitTtlMs;
683
- const relayBudgetMs = params.attemptTimeoutMs + params.startupTimeoutMs + params.turnStartTimeoutMs + CODEX_NATIVE_HOOK_RELAY_TTL_GRACE_MS;
684
- return Math.max(CODEX_NATIVE_HOOK_RELAY_MIN_TTL_MS, Math.floor(relayBudgetMs));
685
- }
686
- /** Builds a stable relay id scoped to the agent and session identity. */
687
- function buildCodexNativeHookRelayId(params) {
688
- const hash = createHash("sha256");
689
- hash.update("openclaw:codex:native-hook-relay:v1");
690
- hash.update("\0");
691
- hash.update(params.agentId?.trim() || "");
692
- hash.update("\0");
693
- hash.update(params.sessionKey?.trim() || params.sessionId);
694
- return `codex-${hash.digest("hex").slice(0, 40)}`;
695
- }
696
- const CODEX_HOOK_EVENT_BY_NATIVE_EVENT = {
697
- pre_tool_use: "PreToolUse",
698
- post_tool_use: "PostToolUse",
699
- permission_request: "PermissionRequest",
700
- before_agent_finalize: "Stop"
701
- };
702
- const CODEX_HOOK_KEY_LABEL_BY_NATIVE_EVENT = {
703
- pre_tool_use: "pre_tool_use",
704
- post_tool_use: "post_tool_use",
705
- permission_request: "permission_request",
706
- before_agent_finalize: "stop"
707
- };
708
- const CODEX_SESSION_FLAGS_HOOK_SOURCE_PATHS = ["/<session-flags>/config.toml", "<session-flags>/config.toml"];
709
- /** Builds the Codex config overlay that installs trusted command hooks for relay events. */
710
- function buildCodexNativeHookRelayConfig(params) {
711
- const events = params.events?.length ? params.events : CODEX_NATIVE_HOOK_RELAY_EVENTS;
712
- const selectedEvents = new Set(events);
713
- const config = { "features.hooks": true };
714
- const hookState = {};
715
- for (const event of CODEX_NATIVE_HOOK_RELAY_EVENTS) {
716
- const codexEvent = CODEX_HOOK_EVENT_BY_NATIVE_EVENT[event];
717
- const selected = selectedEvents.has(event);
718
- const shouldRelay = params.relay.shouldRelayEvent(event);
719
- const selectedNoopPreToolUse = selected && event === "pre_tool_use" && !shouldRelay && params.loopDetectionPreToolUseRelay;
720
- if (!selected || !shouldRelay && !selectedNoopPreToolUse) {
721
- if (selected || params.clearOmittedEvents) config[`hooks.${codexEvent}`] = [];
722
- if (params.clearOmittedEvents) for (const sourcePath of CODEX_SESSION_FLAGS_HOOK_SOURCE_PATHS) hookState[`${sourcePath}:${CODEX_HOOK_KEY_LABEL_BY_NATIVE_EVENT[event]}:0:0`] = { enabled: false };
723
- continue;
724
- }
725
- const timeout = normalizeHookTimeoutSec(params.hookTimeoutSec);
726
- const command = params.relay.commandForEvent(event, { timeoutMs: resolveCodexNativeHookRelayCommandTimeoutMs(timeout) });
727
- const matcher = selectedNoopPreToolUse ? void 0 : buildCodexNativeToolMatcher(params.relay.toolMatcherForEvent(event));
728
- config[`hooks.${codexEvent}`] = [{
729
- ...matcher ? { matcher } : {},
730
- hooks: [{
731
- type: "command",
732
- command,
733
- timeout,
734
- async: false,
735
- statusMessage: "OpenClaw native hook relay"
736
- }]
737
- }];
738
- const state = {
739
- enabled: true,
740
- trusted_hash: codexCommandHookTrustedHash({
741
- event,
742
- command,
743
- matcher,
744
- timeout,
745
- statusMessage: "OpenClaw native hook relay"
746
- })
747
- };
748
- for (const sourcePath of CODEX_SESSION_FLAGS_HOOK_SOURCE_PATHS) hookState[`${sourcePath}:${CODEX_HOOK_KEY_LABEL_BY_NATIVE_EVENT[event]}:0:0`] = state;
749
- }
750
- config["hooks.state"] = hookState;
751
- return config;
752
- }
753
- /** Builds a Codex config overlay that disables native hooks and clears hook arrays. */
754
- function buildCodexNativeHookRelayDisabledConfig() {
755
- return {
756
- "features.hooks": false,
757
- "hooks.PreToolUse": [],
758
- "hooks.PostToolUse": [],
759
- "hooks.PermissionRequest": [],
760
- "hooks.Stop": []
761
- };
762
- }
763
- function normalizeHookTimeoutSec(value) {
764
- return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.ceil(value) : CODEX_NATIVE_HOOK_RELAY_DEFAULT_TIMEOUT_SEC;
765
- }
766
- function resolveCodexNativeHookRelayCommandTimeoutMs(hookTimeoutSec) {
767
- const parentTimeoutMs = finiteSecondsToTimerSafeMilliseconds(normalizeHookTimeoutSec(hookTimeoutSec)) ?? 5e3;
768
- const parentMarginMs = Math.min(CODEX_NATIVE_HOOK_RELAY_COMMAND_MAX_PARENT_MARGIN_MS, Math.max(CODEX_NATIVE_HOOK_RELAY_COMMAND_MIN_PARENT_MARGIN_MS, Math.floor(parentTimeoutMs / 5)));
769
- return Math.max(1, parentTimeoutMs - parentMarginMs);
770
- }
771
- function buildCodexNativeToolMatcher(toolNames) {
772
- if (toolNames === void 0) return;
773
- if (toolNames.length === 0) throw new TypeError("Codex native hook matcher requires at least one tool name");
774
- const nativeNames = /* @__PURE__ */ new Set();
775
- let hasCustomToolName = false;
776
- for (const toolName of toolNames) {
777
- const canonicalToolName = toolName.trim();
778
- if (!canonicalToolName || canonicalToolName === "*") throw new TypeError("Codex native hook matcher requires canonical OpenClaw tool ids");
779
- const nativeAliases = CODEX_HOOK_MATCHER_NAMES_BY_TOOL_ID[canonicalToolName];
780
- if (!nativeAliases) hasCustomToolName = true;
781
- for (const nativeName of nativeAliases ?? [canonicalToolName]) nativeNames.add(nativeName);
782
- }
783
- const sortedNames = Array.from(nativeNames).toSorted();
784
- if (!hasCustomToolName && sortedNames.every((toolName) => /^[A-Za-z0-9_]+$/.test(toolName))) return sortedNames.join("|");
785
- return `(?i)^(?:${sortedNames.map((toolName) => toolName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})$`;
786
- }
787
- function codexCommandHookTrustedHash(params) {
788
- const identity = {
789
- event_name: CODEX_HOOK_KEY_LABEL_BY_NATIVE_EVENT[params.event],
790
- ...params.matcher ? { matcher: params.matcher } : {},
791
- hooks: [{
792
- async: false,
793
- command: params.command,
794
- statusMessage: params.statusMessage,
795
- timeout: params.timeout,
796
- type: "command"
797
- }]
798
- };
799
- return `sha256:${createHash("sha256").update(JSON.stringify(sortJsonValue(identity))).digest("hex")}`;
800
- }
801
- function sortJsonValue(value) {
802
- if (!value || typeof value !== "object") return value;
803
- if (Array.isArray(value)) return value.map(sortJsonValue);
804
- const sorted = {};
805
- for (const [key, entry] of Object.entries(value).toSorted(([left], [right]) => left.localeCompare(right))) sorted[key] = sortJsonValue(entry);
806
- return sorted;
807
- }
808
- //#endregion
809
37
  //#region extensions/codex/src/app-server/event-projector-native-tool-lifecycle.ts
810
38
  /** Projects metadata-only lifecycle diagnostics for native tool items. */
811
39
  var CodexNativeToolLifecycleProjector = class {
@@ -1198,7 +426,7 @@ async function prepareCodexRemoteWorkspaceMessageMedia(params) {
1198
426
  let attachmentEntries = 0;
1199
427
  const mapMediaPath = (value) => {
1200
428
  if (typeof value !== "string") return value;
1201
- if (isGatewayManagedMediaPath(value, gatewayMediaRoot)) {
429
+ if (path.isAbsolute(value) && isPathStrictlyInside(gatewayMediaRoot, value)) {
1202
430
  attachmentEntries += 1;
1203
431
  gatewayManagedPaths.add(value);
1204
432
  return value;
@@ -1281,11 +509,6 @@ async function prepareCodexRemoteWorkspaceMessageMedia(params) {
1281
509
  }
1282
510
  return mapMessageMediaValues(mappedArgs, (value) => stagedPaths.get(value) ?? value);
1283
511
  }
1284
- function isGatewayManagedMediaPath(value, mediaRoot) {
1285
- if (!path.isAbsolute(value)) return false;
1286
- const relativePath = path.relative(mediaRoot, value);
1287
- return Boolean(relativePath && relativePath !== ".." && !relativePath.startsWith(`..${path.sep}`) && !path.isAbsolute(relativePath));
1288
- }
1289
512
  async function assertGatewayManagedMediaPath(value, mediaRoot) {
1290
513
  const opened = await (await root(mediaRoot, { symlinks: "reject" })).open(path.relative(mediaRoot, value), { symlinks: "reject" });
1291
514
  try {
@@ -1322,73 +545,59 @@ const dynamicToolBuildState = {};
1322
545
  //#endregion
1323
546
  //#region extensions/codex/src/app-server/shell-dynamic-tools.ts
1324
547
  const CODEX_NODE_EXEC_DYNAMIC_TOOL_NAME = "node_exec";
1325
- const CODEX_NODE_PROCESS_DYNAMIC_TOOL_NAME = "node_process";
1326
- const CODEX_NODE_EXEC_POLICY_PARAMETER_NAMES = /* @__PURE__ */ new Set([
1327
- "host",
1328
- "security",
1329
- "ask"
1330
- ]);
548
+ const CODEX_GATEWAY_EXEC_DYNAMIC_TOOL_NAME = "gateway_exec";
549
+ const CODEX_GATEWAY_PROCESS_DYNAMIC_TOOL_NAME = "gateway_process";
550
+ const PROCESS_FOLLOWUP_TEXT = "Use process (list/poll/log/write/send-keys/submit/paste/kill/clear/remove) for follow-up.";
1331
551
  /** Returns true when plugin config explicitly removes any named dynamic tool. */
1332
552
  function isCodexDynamicToolExcluded(config, names) {
1333
553
  const normalizedNames = new Set(names.map((name) => normalizeCodexDynamicToolName(name)));
1334
554
  return (config.codexDynamicToolsExclude ?? []).some((name) => normalizedNames.has(normalizeCodexDynamicToolName(name)));
1335
555
  }
1336
- function createNodeExecDynamicTool(execTool, configuredNode) {
1337
- const pinnedNode = configuredNode?.trim();
1338
- return {
1339
- ...execTool,
1340
- name: CODEX_NODE_EXEC_DYNAMIC_TOOL_NAME,
1341
- 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.",
1342
- parameters: hideNodeExecDynamicToolParameters(execTool.parameters, { hideNode: Boolean(pinnedNode) }),
1343
- execute: async (toolCallId, args, signal, onUpdate) => {
1344
- const result = await execTool.execute(toolCallId, pinNodeExecDynamicToolArgs(args, pinnedNode), signal, onUpdate);
1345
- return {
1346
- ...result,
1347
- 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)
1348
- };
1349
- }
556
+ function createNodeExecAliasDynamicTool(execTool, node) {
557
+ const pinnedNode = node?.trim();
558
+ const pinnedTool = pinExecToolTarget(execTool, {
559
+ host: "node",
560
+ ...pinnedNode ? { node: pinnedNode } : {}
561
+ });
562
+ const execute = async (toolCallId, args, signal, onUpdate) => {
563
+ const result = await pinnedTool.execute(toolCallId, args, signal, onUpdate);
564
+ return {
565
+ ...result,
566
+ content: result.content.map((item) => item.type === "text" ? Object.assign({}, item, { text: item.text.replace(PROCESS_FOLLOWUP_TEXT, "Remote-node background follow-up is unavailable. Wait for the command to complete.") }) : item)
567
+ };
1350
568
  };
1351
- }
1352
- function createNodeProcessDynamicTool(processTool) {
1353
569
  return {
1354
- ...processTool,
1355
- name: CODEX_NODE_PROCESS_DYNAMIC_TOOL_NAME,
1356
- 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."
570
+ ...pinnedTool,
571
+ name: CODEX_NODE_EXEC_DYNAMIC_TOOL_NAME,
572
+ description: pinnedNode ? "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.",
573
+ execute
1357
574
  };
1358
575
  }
1359
- function pinNodeExecDynamicToolArgs(args, configuredNode) {
1360
- const { host: _host, security: _security, ask: _ask, node: requestedNode, ...rest } = args && typeof args === "object" && !Array.isArray(args) ? args : {};
1361
- const node = configuredNode ?? (typeof requestedNode === "string" ? requestedNode.trim() : "");
1362
- return {
1363
- ...rest,
1364
- host: "node",
1365
- ...node ? { node } : {}
1366
- };
576
+ function createGatewayExecProjection(createProjection, execTool, params) {
577
+ return createProjection(execTool, {
578
+ kind: "exec",
579
+ name: CODEX_GATEWAY_EXEC_DYNAMIC_TOOL_NAME,
580
+ description: "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.",
581
+ followupText: params.processAliasAvailable ? "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.",
582
+ ...params.ask ? { ask: params.ask } : {}
583
+ });
1367
584
  }
1368
- function hideNodeExecDynamicToolParameters(parameters, options) {
1369
- if (!parameters || typeof parameters !== "object" || Array.isArray(parameters)) return parameters;
1370
- const schema = parameters;
1371
- const rawProperties = schema.properties;
1372
- if (!rawProperties || typeof rawProperties !== "object" || Array.isArray(rawProperties)) return parameters;
1373
- const nextProperties = Object.fromEntries(Object.entries(rawProperties).filter(([name]) => !CODEX_NODE_EXEC_POLICY_PARAMETER_NAMES.has(normalizeCodexDynamicToolName(name)) && !(options.hideNode && normalizeCodexDynamicToolName(name) === "node")));
1374
- const rawRequired = schema.required;
1375
- 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;
1376
- return {
1377
- ...schema,
1378
- properties: nextProperties,
1379
- ...Array.isArray(rawRequired) ? { required: nextRequired } : {}
1380
- };
585
+ function createGatewayProcessProjection(createProjection, processTool) {
586
+ return createProjection(processTool, {
587
+ kind: "process",
588
+ name: CODEX_GATEWAY_PROCESS_DYNAMIC_TOOL_NAME,
589
+ 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."
590
+ });
1381
591
  }
1382
592
  //#endregion
1383
593
  //#region extensions/codex/src/app-server/vision-tools.ts
1384
594
  /**
1385
- * Filters Codex dynamic tools for turns that already contain image inputs so
1386
- * models with native vision do not get redundant image-inspection tools.
595
+ * Codex's enabled native surface includes its stable view_image loader. Keep
596
+ * OpenClaw's view_image tool only when that surface or model vision is unavailable.
1387
597
  */
1388
- /** Removes the image tool when the model can directly consume inbound images. */
1389
- function filterToolsForVisionInputs(tools, params) {
1390
- if (!params.modelHasVision || !params.hasInboundImages) return tools;
1391
- return tools.filter((tool) => tool.name !== "image");
598
+ function filterCodexVisionTools(tools, params) {
599
+ if (!params.modelHasVision || !params.nativeImageInspectionEnabled) return tools;
600
+ return tools.filter((tool) => tool.name !== "view_image");
1392
601
  }
1393
602
  //#endregion
1394
603
  //#region extensions/codex/src/app-server/dynamic-tool-build.ts
@@ -1406,6 +615,18 @@ const CODEX_NATIVE_SANDBOX_TOOL_REQUIREMENTS = [
1406
615
  "apply_patch"
1407
616
  ];
1408
617
  const CODEX_MEMORY_FLUSH_DYNAMIC_TOOL_ALLOW = /* @__PURE__ */ new Set(["read", "write"]);
618
+ /** Keeps node filesystem and process ownership on its native exec-server. */
619
+ function resolveCodexNodePlacementToolConstructionPlan(sandbox, nativeToolSurfaceEnabled) {
620
+ if (!isCodexRemoteExecPlacementSandbox(sandbox) || sandbox?.backendId !== "node" || !("placementNodeId" in sandbox) || typeof sandbox.placementNodeId !== "string" || !sandbox.placementNodeId) return;
621
+ if (!nativeToolSurfaceEnabled) throw new Error("Codex node execution requires its native exec-server tool surface; adjust the session tool policy and start a fresh attempt.");
622
+ return {
623
+ includeBaseCodingTools: false,
624
+ includeShellTools: false,
625
+ includeChannelTools: true,
626
+ includeOpenClawTools: true,
627
+ includePluginTools: true
628
+ };
629
+ }
1409
630
  function preserveRingZeroSystemAgentTool(allTools, filteredTools) {
1410
631
  const openclaw = allTools.find((tool) => tool.name === "openclaw" && tool.catalogMode === "direct-only");
1411
632
  if (!openclaw) return filteredTools;
@@ -1482,6 +703,10 @@ async function buildDynamicTools(input) {
1482
703
  ...params,
1483
704
  disableMessageTool: false
1484
705
  } : params;
706
+ const toolRunContext = buildEmbeddedAttemptToolRunContext({
707
+ ...params,
708
+ forceMessageTool: shouldForceMessageTool(messagePolicyParams)
709
+ });
1485
710
  if (params.disableTools) {
1486
711
  input.onWebSearchPolicyResolved?.(false);
1487
712
  return [];
@@ -1497,101 +722,126 @@ async function buildDynamicTools(input) {
1497
722
  const injectedOpenClawCodingToolsFactory = dynamicToolBuildState.openClawCodingToolsFactory;
1498
723
  let agentHarnessModule;
1499
724
  const loadAgentHarnessModule = async () => agentHarnessModule ??= await import("openclaw/plugin-sdk/agent-harness");
1500
- const createOpenClawCodingTools = injectedOpenClawCodingToolsFactory ?? (await loadAgentHarnessModule()).createOpenClawCodingTools;
1501
725
  toolBuildStages.mark("load-agent-harness-tools");
1502
726
  const sessionKeys = resolveOpenClawCodingToolsSessionKeys(params, input.sandboxSessionKey);
1503
727
  const nativeExecutionPolicy = resolveCodexNativeExecutionPolicyForDynamicTools(input);
1504
- const buildOpenClawCodingTools = () => params.hostCapabilities.bindToolSurface(createOpenClawCodingTools({
1505
- agentId: input.sessionAgentId,
1506
- ...buildEmbeddedAttemptToolRunContext(params),
1507
- exec: {
1508
- ...params.execOverrides,
1509
- ...resolveCodexNodeExecToolOverrides(nativeExecutionPolicy),
1510
- config: params.config,
1511
- elevated: params.bashElevated
1512
- },
1513
- sandbox: input.sandbox,
1514
- messageProvider: resolveCodexMessageToolProvider(params),
1515
- toolPolicyMessageProvider: params.messageProvider ?? params.messageChannel,
1516
- clientCaps: params.clientCaps,
1517
- chatType: params.chatType,
1518
- agentAccountId: params.agentAccountId,
1519
- messageTo: params.messageTo,
1520
- messageThreadId: params.messageThreadId,
1521
- nativeChannelId: params.chatId,
1522
- messageActionTurnCapability: params.messageActionTurnCapability,
1523
- groupId: params.groupId,
1524
- groupChannel: params.groupChannel,
1525
- groupSpace: params.groupSpace,
1526
- spawnedBy: params.spawnedBy,
1527
- senderId: params.senderId,
1528
- senderName: params.senderName,
1529
- senderUsername: params.senderUsername,
1530
- senderE164: params.senderE164,
1531
- senderIsOwner: params.senderIsOwner,
1532
- inputProvenance: params.inputProvenance,
1533
- trustedInternalHandoff: params.trustedInternalHandoff,
1534
- scheduledToolPolicy: params.scheduledToolPolicy,
1535
- allowGatewaySubagentBinding: params.allowGatewaySubagentBinding || isForcedPrivateQaCodexRuntime(),
1536
- ...sessionKeys,
1537
- sessionId: params.sessionId,
1538
- runId: params.runId,
1539
- approvalReviewerDeviceId: params.approvalReviewerDeviceId,
1540
- agentDir,
1541
- cwd: input.effectiveCwd ?? input.effectiveWorkspace,
1542
- workspaceDir: input.effectiveWorkspace,
1543
- spawnWorkspaceDir: input.effectiveCwd && input.effectiveCwd !== input.effectiveWorkspace ? input.resolvedWorkspace : resolveAttemptSpawnWorkspaceDir({
1544
- sandbox: input.sandbox,
1545
- resolvedWorkspace: input.resolvedWorkspace
1546
- }),
728
+ const webSearchPlan = resolveCodexWebSearchPlan({
1547
729
  config: params.config,
1548
- authProfileStore: params.toolAuthProfileStore ?? params.authProfileStore,
1549
- abortSignal: input.runAbortController.signal,
1550
- emitBeforeToolCallDiagnostics: false,
1551
- modelProvider: params.model.provider,
1552
- modelId: params.modelId,
1553
- modelCompat: params.model.compat && typeof params.model.compat === "object" ? params.model.compat : void 0,
1554
- modelApi: params.model.api,
1555
- modelContextWindowTokens: params.model.contextWindow,
1556
- delegationCapability: params.delegationCapability,
1557
- modelAuthMode: resolveModelAuthMode(params.model.provider, params.config, params.toolAuthProfileStore ?? params.authProfileStore, { workspaceDir: input.effectiveWorkspace }),
1558
- suppressManagedWebSearch: false,
1559
- currentChannelId: params.currentChannelId,
1560
- currentMessagingTarget: params.currentMessagingTarget,
1561
- hookChannelId: resolveCodexAppServerHookChannelId(params, input.sandboxSessionKey),
1562
- currentThreadTs: params.currentThreadTs,
1563
- currentMessageId: params.currentMessageId,
1564
- replyToMode: params.replyToMode,
1565
- hasRepliedRef: params.hasRepliedRef,
1566
- modelHasVision,
1567
- computerContextEpoch: input.computerContextEpoch,
1568
- requireExplicitMessageTarget: params.requireExplicitMessageTarget ?? isSubagentSessionKey(params.sessionKey),
1569
- sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
1570
- taskSuggestionDeliveryMode: params.taskSuggestionDeliveryMode,
1571
- disableMessageTool: input.ignoreDisableMessageTool ? false : params.disableMessageTool,
1572
- forceMessageTool: shouldForceMessageTool(messagePolicyParams),
1573
- enableHeartbeatTool: params.trigger === "heartbeat" || input.forceHeartbeatTool === true,
1574
- forceHeartbeatTool: params.trigger === "heartbeat" || input.forceHeartbeatTool === true,
1575
- onYield: (message) => {
1576
- input.onYieldDetected();
1577
- input.onCodexAppServerEvent?.({
1578
- stream: "codex_app_server.tool",
1579
- data: {
1580
- name: "sessions_yield",
1581
- message
1582
- }
1583
- });
1584
- },
1585
- recordToolPrepStage: (name) => {
1586
- toolBuildStages.mark(name);
1587
- },
1588
- onToolOutcome: params.onToolOutcome,
1589
- isTurnTainted: params.isTurnTainted,
1590
- allocateToolOutcomeOrdinal: params.allocateToolOutcomeOrdinal,
1591
- cronCreatorToolAllowlistRef: input.cronCreatorToolAllowlistRef,
1592
- cronCreatorToolAllowlistCaptureRef: input.cronCreatorToolAllowlistCaptureRef,
1593
- cronCreatorAuthorityUnavailableReason: input.cronCreatorAuthorityUnavailableReason
1594
- }), { cwd: input.effectiveCwd ?? input.effectiveWorkspace });
730
+ disableTools: params.disableTools,
731
+ nativeToolSurfaceEnabled: input.nativeToolSurfaceEnabled,
732
+ nativeProviderWebSearchSupport: input.nativeProviderWebSearchSupport
733
+ });
734
+ const webFetchHostnameAllowlistRef = {};
735
+ const buildOpenClawCodingTools = () => {
736
+ const toolConstructionPlan = resolveCodexNodePlacementToolConstructionPlan(input.sandbox, input.nativeToolSurfaceEnabled);
737
+ const options = {
738
+ agentId: input.sessionAgentId,
739
+ ...toolRunContext,
740
+ exec: {
741
+ ...params.execOverrides,
742
+ ...input.sessionPermissionPolicy ? { mode: input.sessionPermissionPolicy.execMode } : {},
743
+ ...resolveCodexNodeExecToolOverrides(nativeExecutionPolicy),
744
+ config: params.config,
745
+ elevated: params.bashElevated
746
+ },
747
+ sessionPermissionPolicy: input.sessionPermissionPolicy ? {
748
+ mode: input.sessionPermissionPolicy.mode,
749
+ root: input.sessionPermissionPolicy.root
750
+ } : void 0,
751
+ sandbox: input.sandbox,
752
+ ...toolConstructionPlan ? { toolConstructionPlan } : {},
753
+ messageProvider: resolveCodexMessageToolProvider(params),
754
+ toolPolicyMessageProvider: params.messageProvider ?? params.messageChannel,
755
+ clientCaps: params.clientCaps,
756
+ chatType: params.chatType,
757
+ agentAccountId: params.agentAccountId,
758
+ messageTo: params.messageTo,
759
+ messageThreadId: params.messageThreadId,
760
+ nativeChannelId: params.chatId,
761
+ messageActionTurnCapability: params.messageActionTurnCapability,
762
+ groupId: params.groupId,
763
+ groupChannel: params.groupChannel,
764
+ groupSpace: params.groupSpace,
765
+ spawnedBy: params.spawnedBy,
766
+ senderId: params.senderId,
767
+ senderName: params.senderName,
768
+ senderUsername: params.senderUsername,
769
+ senderE164: params.senderE164,
770
+ senderIsOwner: params.senderIsOwner,
771
+ inputProvenance: params.inputProvenance,
772
+ trustedInternalHandoff: params.trustedInternalHandoff,
773
+ scheduledToolPolicy: params.scheduledToolPolicy,
774
+ allowGatewaySubagentBinding: params.allowGatewaySubagentBinding || isForcedPrivateQaCodexRuntime(),
775
+ ...sessionKeys,
776
+ sessionId: params.sessionId,
777
+ runId: params.runId,
778
+ approvalReviewerDeviceId: params.approvalReviewerDeviceId,
779
+ agentDir,
780
+ preparedModelRuntime: params.preparedModelRuntime,
781
+ cwd: input.effectiveCwd ?? input.effectiveWorkspace,
782
+ workspaceDir: input.effectiveWorkspace,
783
+ spawnWorkspaceDir: input.effectiveCwd && input.effectiveCwd !== input.effectiveWorkspace ? input.resolvedWorkspace : resolveAttemptSpawnWorkspaceDir({
784
+ sandbox: input.sandbox,
785
+ resolvedWorkspace: input.resolvedWorkspace
786
+ }),
787
+ config: params.config,
788
+ githubPublicationAvailable: params.githubPublicationAvailable,
789
+ authProfileStore: params.toolAuthProfileStore ?? params.authProfileStore,
790
+ abortSignal: input.runAbortController.signal,
791
+ emitBeforeToolCallDiagnostics: false,
792
+ modelProvider: params.model.provider,
793
+ modelId: params.modelId,
794
+ modelCompat: params.model.compat && typeof params.model.compat === "object" ? params.model.compat : void 0,
795
+ modelApi: params.model.api,
796
+ modelContextWindowTokens: params.model.contextWindow,
797
+ delegationCapability: params.delegationCapability,
798
+ modelAuthMode: resolveModelAuthMode(params.model.provider, params.config, params.toolAuthProfileStore ?? params.authProfileStore, { workspaceDir: input.effectiveWorkspace }),
799
+ suppressManagedWebSearch: false,
800
+ webFetchHostnameAllowlistRef,
801
+ currentChannelId: params.currentChannelId,
802
+ currentMessagingTarget: params.currentMessagingTarget,
803
+ hookChannelId: resolveCodexAppServerHookChannelId(params, input.sandboxSessionKey),
804
+ currentThreadTs: params.currentThreadTs,
805
+ currentMessageId: params.currentMessageId,
806
+ replyToMode: params.replyToMode,
807
+ hasRepliedRef: params.hasRepliedRef,
808
+ modelHasVision,
809
+ computerContextEpoch: input.computerContextEpoch,
810
+ registerRunCleanup: input.registerRunCleanup,
811
+ requireExplicitMessageTarget: params.requireExplicitMessageTarget ?? isSubagentSessionKey(params.sessionKey),
812
+ sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
813
+ taskSuggestionDeliveryMode: params.taskSuggestionDeliveryMode,
814
+ disableMessageTool: input.ignoreDisableMessageTool ? false : params.disableMessageTool,
815
+ forceMessageTool: shouldForceMessageTool(messagePolicyParams),
816
+ enableHeartbeatTool: params.trigger === "heartbeat" || input.forceHeartbeatTool === true,
817
+ forceHeartbeatTool: params.trigger === "heartbeat" || input.forceHeartbeatTool === true,
818
+ onYield: (message, acknowledgment) => {
819
+ input.onYieldDetected(acknowledgment);
820
+ input.onCodexAppServerEvent?.({
821
+ stream: "codex_app_server.tool",
822
+ data: {
823
+ name: "sessions_yield",
824
+ message
825
+ }
826
+ });
827
+ },
828
+ claimYieldCompletion: input.claimYieldCompletion,
829
+ recordToolPrepStage: (name) => {
830
+ toolBuildStages.mark(name);
831
+ },
832
+ onToolOutcome: params.onToolOutcome,
833
+ isTurnTainted: params.isTurnTainted,
834
+ allocateToolOutcomeOrdinal: params.allocateToolOutcomeOrdinal,
835
+ cronCreatorToolAllowlistRef: input.cronCreatorToolAllowlistRef,
836
+ cronCreatorToolAllowlistCaptureRef: input.cronCreatorToolAllowlistCaptureRef,
837
+ cronCreatorAuthorityUnavailableReason: input.cronCreatorAuthorityUnavailableReason
838
+ };
839
+ const bindingOptions = { cwd: input.effectiveCwd ?? input.effectiveWorkspace };
840
+ if (injectedOpenClawCodingToolsFactory) return params.hostCapabilities.bindToolSurface(injectedOpenClawCodingToolsFactory(options), bindingOptions);
841
+ const createToolSurface = params.hostCapabilities.createToolSurface;
842
+ if (!createToolSurface) throw new Error("Codex tool construction requires a current host capability");
843
+ return createToolSurface(options, bindingOptions);
844
+ };
1595
845
  const allTools = input.resolveCronCreatorToolAuthority ? runWithCronCreatorAuthorityCapabilityResolver({
1596
846
  capability: params.cronCreatorAuthorityCapability,
1597
847
  runId: params.runId,
@@ -1602,20 +852,14 @@ async function buildDynamicTools(input) {
1602
852
  const preNormalizationDiagnostics = [];
1603
853
  const readableAllToolProjection = filterProviderNormalizableTools(allTools);
1604
854
  preNormalizationDiagnostics.push(...readableAllToolProjection.diagnostics);
1605
- const webSearchPlan = resolveCodexWebSearchPlan({
1606
- config: params.config,
1607
- disableTools: params.disableTools,
1608
- nativeToolSurfaceEnabled: input.nativeToolSurfaceEnabled,
1609
- nativeProviderWebSearchSupport: input.nativeProviderWebSearchSupport
1610
- });
1611
855
  const readableAllTools = [...readableAllToolProjection.tools];
1612
856
  const normallyProfiledTools = input.nativeToolSurfaceEnabled === false ? filterCodexDynamicToolsForDisabledNativeSurface(readableAllTools, input.pluginConfig, { preserveShell: shouldKeepOpenClawShellDynamicTools(input, nativeExecutionPolicy) }) : filterCodexDynamicTools(readableAllTools, input.pluginConfig);
1613
857
  const profileFilteredTools = (input.isHostScopedToolActive?.("openclaw") ?? isHostScopedAgentToolActive("openclaw")) && isSystemAgentOnlyCodexDynamicToolAllowlist(params.toolsAllow) ? preserveRingZeroSystemAgentTool(readableAllTools, normallyProfiledTools) : normallyProfiledTools;
1614
- const codexFilteredTools = addNodeShellDynamicToolsIfNeeded(addSandboxShellDynamicToolsIfAvailable(isCodexMemoryFlushRun(params) ? filterCodexMemoryFlushDynamicTools(readableAllTools) : profileFilteredTools, readableAllTools, input), readableAllTools, input, nativeExecutionPolicy);
858
+ const codexFilteredTools = addNodeShellDynamicToolsIfNeeded(addGatewayShellDynamicToolsIfAvailable(addSandboxShellDynamicToolsIfAvailable(isCodexMemoryFlushRun(params) ? filterCodexMemoryFlushDynamicTools(readableAllTools) : profileFilteredTools, readableAllTools, input), readableAllTools, input, nativeExecutionPolicy), readableAllTools, input, nativeExecutionPolicy);
1615
859
  toolBuildStages.mark("codex-filtering");
1616
- const visionFilteredTools = filterToolsForVisionInputs(codexFilteredTools, {
860
+ const visionFilteredTools = filterCodexVisionTools(codexFilteredTools, {
1617
861
  modelHasVision,
1618
- hasInboundImages: (params.images?.length ?? 0) > 0
862
+ nativeImageInspectionEnabled: input.nativeToolSurfaceEnabled === true
1619
863
  });
1620
864
  toolBuildStages.mark("vision-filtering");
1621
865
  const webSearchPresent = visionFilteredTools.some((tool) => tool.name === "web_search");
@@ -1646,7 +890,7 @@ async function buildDynamicTools(input) {
1646
890
  };
1647
891
  const transientWebSearchRestriction = !webSearchPolicy.allowed && webSearchPolicy.persistentAllowed || isCodexMemoryFlushRun(params);
1648
892
  input.onPersistentWebSearchPolicyResolved?.(webSearchPresent || persistentCodexWebSearchSurface && transientWebSearchRestriction && webSearchPolicy.persistentAllowed);
1649
- const filteredTools = filterCodexDynamicToolsForAllowlist(visionFilteredTools, includeForcedCodexDynamicToolAllow(params.toolsAllow, messagePolicyParams));
893
+ const filteredTools = filterCodexDynamicToolsForAllowlist(visionFilteredTools, toolRunContext.runtimeToolAllowlist);
1650
894
  toolBuildStages.mark("allowlist-filter");
1651
895
  const normalizedTools = normalizeAgentRuntimeTools({
1652
896
  runtimePlan: input.ignoreRuntimePlan ? void 0 : params.runtimePlan,
@@ -1662,7 +906,9 @@ async function buildDynamicTools(input) {
1662
906
  onPreNormalizationSchemaDiagnostics: (diagnostics) => preNormalizationDiagnostics.push(...diagnostics)
1663
907
  });
1664
908
  toolBuildStages.mark("runtime-normalization");
1665
- input.onWebSearchPolicyResolved?.(normalizedTools.some((tool) => tool.name === "web_search"));
909
+ const webSearchAllowed = normalizedTools.some((tool) => tool.name === "web_search");
910
+ webFetchHostnameAllowlistRef.value = webSearchAllowed ? webSearchPlan.webFetchHostnameAllowlist : void 0;
911
+ input.onWebSearchPolicyResolved?.(webSearchAllowed);
1666
912
  const exposedTools = webSearchPlan.suppressManagedWebSearch ? normalizedTools.filter((tool) => tool.name !== "web_search") : normalizedTools;
1667
913
  if (preNormalizationDiagnostics.length > 0) embeddedAgentLog.warn(`codex app-server quarantined ${preNormalizationDiagnostics.length} unsupported runtime tool schema${preNormalizationDiagnostics.length === 1 ? "" : "s"} before dynamic tool registration`, {
1668
914
  runId: params.runId,
@@ -1695,22 +941,36 @@ async function buildDynamicTools(input) {
1695
941
  }
1696
942
  return exposedTools;
1697
943
  }
1698
- /** Preserves delivery-critical tools when a narrow allowlist would otherwise hide them. */
1699
- function includeForcedCodexDynamicToolAllow(toolsAllow, params) {
1700
- if (toolsAllow === void 0 || hasWildcardCodexToolsAllow(toolsAllow)) return toolsAllow;
1701
- const forcedToolNames = shouldForceMessageTool(params) ? ["message"] : [];
1702
- if (forcedToolNames.length === 0) return toolsAllow;
1703
- if (toolsAllow.length === 0) return forcedToolNames;
1704
- const normalized = new Set(toolsAllow.map((name) => normalizeCodexDynamicToolName(name)));
1705
- const missingToolNames = forcedToolNames.filter((toolName) => !normalized.has(normalizeCodexDynamicToolName(toolName)));
1706
- return missingToolNames.length === 0 ? toolsAllow : [...toolsAllow, ...missingToolNames];
944
+ /** Keeps the OpenClaw Gateway execution path available beside Codex native shell. */
945
+ function addGatewayShellDynamicToolsIfAvailable(filteredTools, allTools, input, executionPolicy) {
946
+ if (isCodexMemoryFlushRun(input.params) || input.nativeToolSurfaceEnabled !== true || input.sandbox?.enabled === true || !executionPolicy.nativeToolSurfaceAllowed || executionPolicy.effectiveExecHost !== "gateway") return filteredTools;
947
+ const execTool = allTools.find((tool) => normalizeCodexDynamicToolName(tool.name) === "exec");
948
+ const processTool = allTools.find((tool) => normalizeCodexDynamicToolName(tool.name) === "process");
949
+ const existingNames = new Set(filteredTools.map((tool) => normalizeCodexDynamicToolName(tool.name)));
950
+ const execExcluded = isCodexDynamicToolExcluded(input.pluginConfig, ["exec", CODEX_GATEWAY_EXEC_DYNAMIC_TOOL_NAME]);
951
+ if (!execTool || execExcluded || existingNames.has("gateway_exec")) return filteredTools;
952
+ const processExcluded = isCodexDynamicToolExcluded(input.pluginConfig, ["process", CODEX_GATEWAY_PROCESS_DYNAMIC_TOOL_NAME]);
953
+ const processAliasAvailable = Boolean(processTool && !processExcluded && !existingNames.has("gateway_process"));
954
+ const createProjection = resolveCodexScheduledToolProjectionFactory(input.params.hostCapabilities);
955
+ if (!createProjection) return filteredTools;
956
+ const toolsToAppend = [createGatewayExecProjection(createProjection, execTool, {
957
+ processAliasAvailable,
958
+ ...input.sessionPermissionPolicy?.mode === "guarded" ? { ask: "always" } : {}
959
+ })];
960
+ if (processAliasAvailable && processTool) toolsToAppend.push(createGatewayProcessProjection(createProjection, processTool));
961
+ return [...filteredTools, ...toolsToAppend];
1707
962
  }
1708
963
  /** Decides whether Codex native code mode can own shell/file tools for this turn. */
1709
964
  function shouldEnableCodexAppServerNativeToolSurface(params, sandbox, options = {}) {
1710
965
  if (params.pluginHarnessToolPolicyRestricted === true) return false;
1711
966
  if (isCodexMemoryFlushRun(params)) return false;
1712
967
  if (params.disableTools) return false;
1713
- const toolsAllow = includeForcedCodexDynamicToolAllow(params.toolsAllow, params);
968
+ if (isCodexNativeExecutionBlockedByNodeExecHost(params, {
969
+ agentId: options.agentId,
970
+ runtimeSessionKey: options.runtimeSessionKey,
971
+ sandbox
972
+ })) return false;
973
+ const toolsAllow = params.toolsAllow;
1714
974
  if (toolsAllow === void 0) return canCodexAppServerNativeToolSurfaceHonorSandbox(sandbox, options);
1715
975
  return hasWildcardCodexToolsAllow(toolsAllow) && canCodexAppServerNativeToolSurfaceHonorSandbox(sandbox, options);
1716
976
  }
@@ -1731,7 +991,7 @@ function resolveCodexRuntimePolicySessionKey(params, runtimeSessionKey) {
1731
991
  }
1732
992
  function canCodexAppServerNativeToolSurfaceHonorSandbox(sandbox, options = {}) {
1733
993
  if (!sandbox?.enabled) return true;
1734
- if (options.sandboxExecServerEnabled === true && sandbox.backend && canSandboxToolPolicyExposeCodexNativeToolSurface(sandbox)) return true;
994
+ if (options.sandboxExecServerEnabled === true && (sandbox.backend || isCodexRemoteExecPlacementSandbox(sandbox)) && canSandboxToolPolicyExposeCodexNativeToolSurface(sandbox)) return true;
1735
995
  return false;
1736
996
  }
1737
997
  function canSandboxToolPolicyExposeCodexNativeToolSurface(sandbox) {
@@ -1807,7 +1067,7 @@ function addSandboxShellDynamicToolsIfAvailable(filteredTools, allTools, input)
1807
1067
  const sandboxProcessTool = {
1808
1068
  ...processTool,
1809
1069
  name: "sandbox_process",
1810
- description: "Manage sandbox_exec sessions that were started through OpenClaw's configured sandbox backend for this session: list, poll, log, write, send-keys, submit, paste, kill, clear, or remove. Use only for sandbox_exec follow-up; use Codex's native shell session handling only when no OpenClaw sandbox is active and native Code Mode is available."
1070
+ description: "Manage background shell sessions through OpenClaw's configured sandbox backend for this session: list, poll, log, write, send-keys, submit, paste, kill, clear, or remove. Use only for sandbox follow-up; use Codex's native shell session handling only when no OpenClaw sandbox is active and native Code Mode is available."
1811
1071
  };
1812
1072
  return [
1813
1073
  ...filteredTools,
@@ -1839,12 +1099,9 @@ function addNodeShellDynamicToolsIfNeeded(filteredTools, allTools, input, nodePo
1839
1099
  const nodeExecAvailableFromAuto = nodePolicy.requestedExecHost === "auto" && nodePolicy.effectiveExecHost === "gateway";
1840
1100
  if (!nodeExecIsDefault && !nodeExecAvailableFromAuto) return filteredTools;
1841
1101
  const execTool = allTools.find((tool) => normalizeCodexDynamicToolName(tool.name) === "exec");
1842
- const processTool = allTools.find((tool) => normalizeCodexDynamicToolName(tool.name) === "process");
1843
- if (!execTool || !processTool) return filteredTools;
1844
- const toolsToAppend = [];
1845
- if (!isCodexDynamicToolExcluded(input.pluginConfig, ["exec", "node_exec"]) && !filteredTools.some((tool) => normalizeCodexDynamicToolName(tool.name) === "node_exec")) toolsToAppend.push(createNodeExecDynamicTool(execTool, nodePolicy.node));
1846
- if (!isCodexDynamicToolExcluded(input.pluginConfig, ["process", "node_process"]) && !filteredTools.some((tool) => normalizeCodexDynamicToolName(tool.name) === "node_process")) toolsToAppend.push(createNodeProcessDynamicTool(processTool));
1847
- return toolsToAppend.length > 0 ? [...filteredTools, ...toolsToAppend] : filteredTools;
1102
+ if (!execTool) return filteredTools;
1103
+ if (!isCodexDynamicToolExcluded(input.pluginConfig, ["exec", "node_exec"]) && !filteredTools.some((tool) => normalizeCodexDynamicToolName(tool.name) === "node_exec")) return [...filteredTools, createNodeExecAliasDynamicTool(execTool, nodePolicy.node)];
1104
+ return filteredTools;
1848
1105
  }
1849
1106
  function shouldKeepOpenClawShellDynamicTools(input, nodePolicy) {
1850
1107
  return !isCodexMemoryFlushRun(input.params) && input.nativeToolSurfaceEnabled === false && input.sandbox?.enabled !== true && nodePolicy.effectiveExecHost !== "node";
@@ -1868,7 +1125,7 @@ function filterCodexDynamicToolsForAllowlist(tools, toolsAllow) {
1868
1125
  const allowSet = new Set(toolsAllow.map((name) => normalizeCodexDynamicToolName(name)).filter(Boolean));
1869
1126
  return tools.filter((tool) => {
1870
1127
  const normalized = normalizeCodexDynamicToolName(tool.name);
1871
- return allowSet.has(normalized) || normalized === "sandbox_exec" && allowSet.has("exec") || normalized === "sandbox_process" && (allowSet.has("exec") || allowSet.has("process")) || normalized === "node_exec" && allowSet.has("exec") || normalized === "node_process" && (allowSet.has("exec") || allowSet.has("process"));
1128
+ return allowSet.has(normalized) || normalized === "sandbox_exec" && allowSet.has("exec") || normalized === "sandbox_process" && (allowSet.has("exec") || allowSet.has("process")) || normalized === "gateway_exec" && allowSet.has("exec") || normalized === "gateway_process" && (allowSet.has("exec") || allowSet.has("process")) || normalized === "node_exec" && allowSet.has("exec");
1872
1129
  });
1873
1130
  }
1874
1131
  /** Detects the wildcard allowlist marker after Codex tool-name normalization. */
@@ -1880,9 +1137,317 @@ function shouldForceMessageTool(params) {
1880
1137
  return params.disableMessageTool !== true && params.sourceReplyDeliveryMode === "message_tool_only";
1881
1138
  }
1882
1139
  //#endregion
1140
+ //#region extensions/codex/src/app-server/sandbox-exec-server-node-relay.ts
1141
+ const CODEX_NODE_EXEC_SERVER_MAX_MESSAGE_BYTES$1 = 64 * 1024 * 1024;
1142
+ const CODEX_NODE_EXEC_SERVER_MAX_FAILURE_DETAIL_CHARS = 240;
1143
+ const CODEX_NODE_HTTP_CREDENTIAL_BODY_MAX_BYTES = 1024 * 1024;
1144
+ const CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_FIELDS = 256;
1145
+ const CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_DEPTH = 8;
1146
+ const CODEX_NODE_HTTP_CREDENTIAL_HEADER_NAMES = /* @__PURE__ */ new Set([
1147
+ "authorization",
1148
+ "proxy-authorization",
1149
+ "cookie",
1150
+ "set-cookie",
1151
+ "x-api-key",
1152
+ "api-key",
1153
+ "apikey",
1154
+ "x-auth-token",
1155
+ "auth-token",
1156
+ "x-access-token",
1157
+ "access-token",
1158
+ "x-secret-key",
1159
+ "secret-key",
1160
+ "x-goog-api-key",
1161
+ "x-vault-token",
1162
+ "x-api-token"
1163
+ ]);
1164
+ const CODEX_NODE_HTTP_CREDENTIAL_HEADER_NAME_PATTERN = /(?:^|[-_])(?:auth(?:orization|entication)?|token|secret|api[-_]?key|apikey|key|password|passwd|pwd|passphrase|passcode|credentials?|session|jwt|assertion|verifier|sig(?:nature)?|hmac|bearer|ticket|challenge|proof|dpop|otp|totp|pin|mfa)(?:[-_]|$)/iu;
1165
+ const CODEX_NODE_HTTP_CREDENTIAL_FIELD_NAME_PATTERN = /^(?:(?:[a-z\d]+_)*(?:token|secret|password|passwd|pwd|passphrase|passcode|credentials?|authorization|api_?key|private_key|secret_key|secret_access_key|jwt|assertion|verifier|signature|hmac|bearer|ticket|(?:oauth|consumer|auth|access)_key|otp|totp|pin)|(?:device|authorization|auth|verification|mfa)_code|session(?:_id)?|jsessionid|saml(?:_?response|_?assertion)?|auth|jwt|code|sig|signature|hmac|key|pass)$/u;
1166
+ const nodeExecServerTextDecoder = new TextDecoder("utf-8", { fatal: true });
1167
+ /** Produces the bounded, redacted terminal failure shared by pending and claimed node leases. */
1168
+ function createCodexNodeExecServerDisconnectError(reason, cause) {
1169
+ const detail = cause === void 0 ? "" : `: ${truncateUtf16Safe(redactSensitiveText(formatErrorMessage$1(cause), { mode: "tools" }), CODEX_NODE_EXEC_SERVER_MAX_FAILURE_DETAIL_CHARS)}`;
1170
+ return /* @__PURE__ */ new Error(`Codex execution node disconnected; start a fresh attempt. (${reason}${detail})`);
1171
+ }
1172
+ /** Relays one authorized, single-use Codex exec-server channel without interpreting its protocol. */
1173
+ async function startCodexNodeExecServerRelay(params) {
1174
+ const { channel } = params.lease;
1175
+ const { socket } = params;
1176
+ let closed = false;
1177
+ const { promise: finished, resolve: finish } = createDeferred();
1178
+ let unsubscribe = () => {};
1179
+ const closeBoth = (code = 1001, reason = "execution channel closed") => {
1180
+ if (closed) return;
1181
+ closed = true;
1182
+ unsubscribe();
1183
+ params.lease.closeRelay = void 0;
1184
+ params.lease.onChannelClosed = void 0;
1185
+ if (!params.lease.closed) {
1186
+ params.lease.closed = true;
1187
+ channel.close();
1188
+ }
1189
+ if (socket.readyState === socket.OPEN || socket.readyState === socket.CONNECTING) socket.close(code, reason);
1190
+ finish();
1191
+ };
1192
+ params.lease.closeRelay = closeBoth;
1193
+ const failUnexpectedly = (code, reason, cause) => {
1194
+ if (!closed && !params.lease.closed) params.lease.onDisconnected?.(createCodexNodeExecServerDisconnectError(reason, cause));
1195
+ closeBoth(code, reason);
1196
+ };
1197
+ params.lease.onChannelClosed = ({ failed, error }) => failUnexpectedly(failed ? 1011 : 1001, failed ? "execution node failed" : "execution node disconnected", error);
1198
+ socket.once("close", () => failUnexpectedly(1001, "execution socket closed"));
1199
+ socket.once("error", () => failUnexpectedly(1011, "execution socket failed"));
1200
+ let toNode = Promise.resolve();
1201
+ socket.on("message", (data) => {
1202
+ if (closed) return;
1203
+ socket.pause();
1204
+ toNode = toNode.then(async () => {
1205
+ const frame = normalizeCodexExecServerFrame(data);
1206
+ const request = validateCodexExecServerMessage(frame);
1207
+ const rejection = rejectCredentialedCodexNodeHttpRequest(request);
1208
+ if (rejection) await sendCodexExecServerFrame(socket, rejection);
1209
+ else await channel.send(sanitizeCodexExecServerRequest(frame, request));
1210
+ if (!closed) socket.resume();
1211
+ }).catch((error) => {
1212
+ failUnexpectedly(error instanceof RangeError ? 1009 : 1007, "invalid execution message");
1213
+ });
1214
+ });
1215
+ unsubscribe = channel.onMessage(async (message) => {
1216
+ if (closed) return;
1217
+ try {
1218
+ const frame = normalizeCodexExecServerFrame(message);
1219
+ validateCodexExecServerMessage(frame);
1220
+ await sendCodexExecServerFrame(socket, frame);
1221
+ } catch (error) {
1222
+ failUnexpectedly(error instanceof RangeError ? 1009 : 1007, "invalid device message");
1223
+ }
1224
+ });
1225
+ await finished;
1226
+ }
1227
+ function sendCodexExecServerFrame(socket, frame) {
1228
+ return new Promise((resolve, reject) => {
1229
+ socket.send(frame, { binary: false }, (error) => {
1230
+ if (error) {
1231
+ reject(error);
1232
+ return;
1233
+ }
1234
+ resolve();
1235
+ });
1236
+ });
1237
+ }
1238
+ function normalizeCodexExecServerFrame(data) {
1239
+ const frame = Array.isArray(data) ? Buffer.concat(data) : Buffer.isBuffer(data) ? data : data instanceof Uint8Array ? Buffer.from(data.buffer, data.byteOffset, data.byteLength) : Buffer.from(data);
1240
+ if (frame.length > CODEX_NODE_EXEC_SERVER_MAX_MESSAGE_BYTES$1) throw new RangeError("Codex exec-server message exceeds its 64 MiB limit.");
1241
+ if (frame.includes(10) || frame.includes(13)) throw new Error("Codex exec-server messages must occupy exactly one stdio line.");
1242
+ return frame;
1243
+ }
1244
+ function validateCodexExecServerMessage(frame) {
1245
+ const parsed = JSON.parse(nodeExecServerTextDecoder.decode(frame));
1246
+ if (!isRecord(parsed)) throw new Error("Codex exec-server message must be a JSON object.");
1247
+ return parsed;
1248
+ }
1249
+ function rejectCredentialedCodexNodeHttpRequest(request) {
1250
+ if (request.method !== "http/request") return;
1251
+ if (!isRecord(request.params)) throw new Error("Codex http/request params must be an object.");
1252
+ const headers = request.params.headers ?? [];
1253
+ if (!Array.isArray(headers)) throw new Error("Codex http/request headers must be an array.");
1254
+ if (headers.length > CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_FIELDS) return createCredentialedCodexNodeHttpRejection(request);
1255
+ let credentialBearing = false;
1256
+ let contentType;
1257
+ for (const header of headers) {
1258
+ if (!isRecord(header) || typeof header.name !== "string" || typeof header.value !== "string") throw new Error("Codex http/request headers must contain string names and values.");
1259
+ const name = header.name.trim().toLowerCase();
1260
+ if (CODEX_NODE_HTTP_CREDENTIAL_HEADER_NAMES.has(name) || CODEX_NODE_HTTP_CREDENTIAL_HEADER_NAME_PATTERN.test(name) || hasSensitiveCodexNodeText(header.value)) credentialBearing = true;
1261
+ if (name === "content-type") {
1262
+ const declared = header.value.split(";", 1)[0]?.trim().toLowerCase();
1263
+ credentialBearing ||= Boolean(contentType && contentType !== declared);
1264
+ contentType = declared;
1265
+ }
1266
+ }
1267
+ if (!credentialBearing && typeof request.params.url === "string") credentialBearing = hasCredentialedCodexNodeHttpUrl(request.params.url);
1268
+ if (!credentialBearing && request.params.bodyBase64 != null) credentialBearing = hasCredentialedCodexNodeHttpBody(request.params.bodyBase64, contentType);
1269
+ return credentialBearing ? createCredentialedCodexNodeHttpRejection(request) : void 0;
1270
+ }
1271
+ function hasCredentialedCodexNodeHttpUrl(value) {
1272
+ let url;
1273
+ try {
1274
+ url = new URL(value);
1275
+ } catch {
1276
+ throw new Error("Codex http/request URL must be valid.");
1277
+ }
1278
+ if (url.username || url.password || hasSensitiveCodexNodeText(value)) return true;
1279
+ let fields = 0;
1280
+ const hasCredentialedParameter = (name, parameterValue) => ++fields > CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_FIELDS || isCodexNodeCredentialField(name) || hasSensitiveCodexNodeText(parameterValue);
1281
+ for (const parameters of [url.searchParams, new URLSearchParams(url.hash.slice(1))]) for (const [name, parameterValue] of parameters) if (hasCredentialedParameter(name, parameterValue)) return true;
1282
+ for (const initial of [
1283
+ url.pathname,
1284
+ url.hash.slice(1),
1285
+ ...url.searchParams.values()
1286
+ ]) {
1287
+ let component = initial;
1288
+ for (let depth = 0; depth <= CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_DEPTH; depth += 1) {
1289
+ for (const nestedQuery of component.split(/[?#]/u).slice(1)) for (const [name, parameterValue] of new URLSearchParams(nestedQuery)) if (hasCredentialedParameter(name, parameterValue)) return true;
1290
+ for (const segment of component.split("/")) for (const parameter of segment.split(";").slice(1)) {
1291
+ const separator = parameter.indexOf("=");
1292
+ if (hasCredentialedParameter(separator < 0 ? parameter : parameter.slice(0, separator), separator < 0 ? "" : parameter.slice(separator + 1))) return true;
1293
+ }
1294
+ if (!/%[\da-f]{2}/iu.test(component)) break;
1295
+ if (depth === CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_DEPTH) return true;
1296
+ try {
1297
+ component = decodeURIComponent(component);
1298
+ } catch {
1299
+ return true;
1300
+ }
1301
+ }
1302
+ }
1303
+ return false;
1304
+ }
1305
+ function hasCredentialedCodexNodeHttpBody(value, contentType) {
1306
+ if (typeof value !== "string") throw new Error("Codex http/request bodyBase64 must be a string.");
1307
+ const declaredText = contentType === "text/plain";
1308
+ if (!declaredText && value.length > Math.ceil(CODEX_NODE_HTTP_CREDENTIAL_BODY_MAX_BYTES / 3) * 4) return true;
1309
+ let body;
1310
+ try {
1311
+ const decoded = Buffer.from(value, "base64");
1312
+ if (!declaredText && decoded.length > CODEX_NODE_HTTP_CREDENTIAL_BODY_MAX_BYTES || decoded.toString("base64") !== value) return true;
1313
+ body = nodeExecServerTextDecoder.decode(decoded);
1314
+ } catch {
1315
+ return true;
1316
+ }
1317
+ if (!body) return false;
1318
+ if (hasSensitiveCodexNodeText(body)) return true;
1319
+ const declaredJson = contentType === "application/json" || contentType?.endsWith("+json") === true;
1320
+ const declaredForm = contentType === "application/x-www-form-urlencoded";
1321
+ if (contentType && !declaredJson && !declaredForm && !declaredText) return true;
1322
+ const trimmed = body.trimStart();
1323
+ if (trimmed.startsWith("<")) return true;
1324
+ if (declaredJson || trimmed.startsWith("{") || trimmed.startsWith("[")) try {
1325
+ if (hasCredentialedCodexNodeRawJsonStrings(body)) return true;
1326
+ return hasCredentialedCodexNodeJsonFields(JSON.parse(body));
1327
+ } catch {
1328
+ if (!declaredText) return true;
1329
+ }
1330
+ if (!declaredForm && !body.includes("=")) return !declaredText;
1331
+ let fields = 0;
1332
+ for (const [name, parameterValue] of new URLSearchParams(body)) if (++fields > CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_FIELDS || isCodexNodeCredentialField(name) || hasSensitiveCodexNodeText(parameterValue)) return true;
1333
+ return false;
1334
+ }
1335
+ function hasCredentialedCodexNodeRawJsonStrings(body) {
1336
+ const tokens = body.matchAll(/("(?:\\.|[^"\\])*")(\s*:)?/gu);
1337
+ let fields = 0;
1338
+ for (const match of tokens) {
1339
+ const value = JSON.parse(match[1]);
1340
+ if (++fields > CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_FIELDS || typeof value !== "string" || hasSensitiveCodexNodeText(value) || match[2] !== void 0 && isCodexNodeCredentialField(value)) return true;
1341
+ }
1342
+ return false;
1343
+ }
1344
+ function hasCredentialedCodexNodeJsonFields(value) {
1345
+ const pending = [{
1346
+ value,
1347
+ depth: 0
1348
+ }];
1349
+ let fields = 0;
1350
+ while (pending.length > 0) {
1351
+ const current = pending.pop();
1352
+ if (typeof current?.value === "string" && hasSensitiveCodexNodeText(current.value)) return true;
1353
+ if (!current || !Array.isArray(current.value) && !isRecord(current.value)) continue;
1354
+ if (current.depth >= CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_DEPTH) return true;
1355
+ const entries = Array.isArray(current.value) ? current.value.map((entry) => [void 0, entry]) : Object.entries(current.value);
1356
+ for (const [name, nested] of entries) {
1357
+ if (++fields > CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_FIELDS || typeof name === "string" && isCodexNodeCredentialField(name)) return true;
1358
+ pending.push({
1359
+ value: nested,
1360
+ depth: current.depth + 1
1361
+ });
1362
+ }
1363
+ }
1364
+ return false;
1365
+ }
1366
+ function hasSensitiveCodexNodeText(value) {
1367
+ let decoded = value;
1368
+ for (let depth = 0; depth <= CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_DEPTH; depth += 1) {
1369
+ if (redactToolPayloadText(decoded) !== decoded) return true;
1370
+ if (!/%[\da-f]{2}/iu.test(decoded)) return false;
1371
+ try {
1372
+ decoded = decodeURIComponent(decoded);
1373
+ } catch {
1374
+ return true;
1375
+ }
1376
+ }
1377
+ return true;
1378
+ }
1379
+ function isCodexNodeCredentialField(value) {
1380
+ let decoded = value;
1381
+ for (let depth = 0; depth < CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_DEPTH; depth += 1) {
1382
+ let next;
1383
+ try {
1384
+ next = decodeURIComponent(decoded);
1385
+ } catch {
1386
+ return true;
1387
+ }
1388
+ if (next === decoded) break;
1389
+ decoded = next;
1390
+ }
1391
+ const normalized = decoded.replace(/[\p{C}\p{Z}\u115F\u1160\u3164\uFFA0+]/gu, "").replace(/([a-z\d])([A-Z])/gu, "$1_$2").replaceAll("-", "_").toLowerCase();
1392
+ return normalized.length > CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_FIELDS || normalized.split(/[.[\]]+/u).some((component) => CODEX_NODE_HTTP_CREDENTIAL_FIELD_NAME_PATTERN.test(component));
1393
+ }
1394
+ function createCredentialedCodexNodeHttpRejection(request) {
1395
+ if (typeof request.id !== "string" && typeof request.id !== "number") throw new Error("Codex http/request must have a JSON-RPC request id.");
1396
+ return Buffer.from(JSON.stringify({
1397
+ jsonrpc: "2.0",
1398
+ id: request.id,
1399
+ error: {
1400
+ code: -32602,
1401
+ message: "Authenticated remote HTTP is unavailable on execution nodes; run on Gateway or use an intentionally credential-free endpoint."
1402
+ }
1403
+ }));
1404
+ }
1405
+ function sanitizeCodexExecServerRequest(frame, request) {
1406
+ if (request.method !== "process/start") return frame;
1407
+ if (!isRecord(request.params)) throw new Error("Codex process/start params must be an object.");
1408
+ sanitizeCodexExecServerEnvironment(request.params, "env");
1409
+ if (request.params.envPolicy !== void 0) {
1410
+ if (!isRecord(request.params.envPolicy)) throw new Error("Codex process/start envPolicy must be an object.");
1411
+ sanitizeCodexExecServerEnvironment(request.params.envPolicy, "set");
1412
+ }
1413
+ return normalizeCodexExecServerFrame(Buffer.from(JSON.stringify(request)));
1414
+ }
1415
+ function sanitizeCodexExecServerEnvironment(record, key) {
1416
+ const environment = record[key];
1417
+ if (environment === void 0) return;
1418
+ if (!isRecord(environment)) throw new Error(`Codex process/start ${key} must be an object.`);
1419
+ const values = {};
1420
+ for (const [name, value] of Object.entries(environment)) {
1421
+ if (typeof value !== "string") throw new Error(`Codex process/start ${key} values must be strings.`);
1422
+ try {
1423
+ const url = new URL(value);
1424
+ if (url.username || url.password) continue;
1425
+ } catch {}
1426
+ values[name] = value;
1427
+ }
1428
+ record[key] = sanitizeEnvVars(values).allowed;
1429
+ }
1430
+ //#endregion
1883
1431
  //#region extensions/codex/src/app-server/sandbox-exec-server-registry.ts
1884
1432
  const sandboxExecServerRegistry = {
1885
1433
  servers: /* @__PURE__ */ new Map(),
1434
+ async close(server) {
1435
+ if (server.closed) return;
1436
+ server.closed = true;
1437
+ if ("node" in server) {
1438
+ for (const lease of server.node.leases.values()) if (!lease.closed) {
1439
+ lease.closed = true;
1440
+ lease.channel.close();
1441
+ }
1442
+ server.node.leases.clear();
1443
+ }
1444
+ for (const client of server.server.clients) client.close(1001, "shutdown");
1445
+ await new Promise((resolve) => {
1446
+ server.server.close(() => resolve());
1447
+ });
1448
+ const failures = (await Promise.allSettled([...server.cleanupTasks, ...[...server.children].map(async (child) => await child.terminate())])).flatMap((result) => result.status === "rejected" ? [result.reason] : []);
1449
+ if (failures.length > 0) throw new AggregateError(failures, "Codex sandbox exec-server child cleanup failed");
1450
+ },
1886
1451
  async closeAll() {
1887
1452
  const servers = await Promise.allSettled(this.servers.values());
1888
1453
  this.servers.clear();
@@ -1890,12 +1455,7 @@ const sandboxExecServerRegistry = {
1890
1455
  if (entry.status !== "fulfilled") return;
1891
1456
  const server = entry.value;
1892
1457
  server.refCount = 0;
1893
- if (server.closed) return;
1894
- server.closed = true;
1895
- for (const client of server.server.clients) client.close(1001, "shutdown");
1896
- await new Promise((resolve) => {
1897
- server.server.close(() => resolve());
1898
- });
1458
+ await this.close(server);
1899
1459
  }));
1900
1460
  }
1901
1461
  };
@@ -1912,9 +1472,8 @@ var JsonRpcProtocolError = class extends Error {
1912
1472
  this.code = code;
1913
1473
  }
1914
1474
  };
1915
- /** Parses raw WebSocket data into a JSON-RPC request object. */
1916
- function parseRequest(data) {
1917
- const text = (Array.isArray(data) ? Buffer.concat(data) : Buffer.isBuffer(data) ? data : Buffer.from(data)).toString("utf8");
1475
+ /** Parses a normalized JSON message into a JSON-RPC request object. */
1476
+ function parseRequest(text) {
1918
1477
  return requireObject(JSON.parse(text), "JSON-RPC request");
1919
1478
  }
1920
1479
  /** Validates that a JSON value is a non-array object. */
@@ -1954,24 +1513,24 @@ function readHttpHeaders(value) {
1954
1513
  };
1955
1514
  });
1956
1515
  }
1957
- /** Sends a JSON-RPC success response over the WebSocket. */
1958
- function sendResult(socket, id, result) {
1959
- socket.send(JSON.stringify({
1516
+ /** Sends a JSON-RPC success response through the connection message sink. */
1517
+ function sendResult(send, id, result) {
1518
+ send({
1960
1519
  jsonrpc: "2.0",
1961
1520
  id,
1962
- result: result === void 0 ? {} : result
1963
- }));
1521
+ result
1522
+ });
1964
1523
  }
1965
- /** Sends a JSON-RPC error response over the WebSocket. */
1966
- function sendError(socket, id, code, message) {
1967
- socket.send(JSON.stringify({
1524
+ /** Sends a JSON-RPC error response through the connection message sink. */
1525
+ function sendError(send, id, code, message) {
1526
+ send({
1968
1527
  jsonrpc: "2.0",
1969
1528
  id: id ?? null,
1970
1529
  error: {
1971
1530
  code,
1972
1531
  message
1973
1532
  }
1974
- }));
1533
+ });
1975
1534
  }
1976
1535
  //#endregion
1977
1536
  //#region extensions/codex/src/app-server/sandbox-exec-server/path-uri.ts
@@ -2216,20 +1775,6 @@ function joinSandboxChildPath(parent, child) {
2216
1775
  return parent.endsWith("/") ? `${parent}${child}` : `${parent}/${child}`;
2217
1776
  }
2218
1777
  //#endregion
2219
- //#region extensions/codex/src/app-server/sandbox-exec-server/runtime.ts
2220
- /** Returns the configured sandbox backend or fails the current JSON-RPC request. */
2221
- function requireBackend(execServer) {
2222
- const backend = execServer.sandbox.backend;
2223
- if (!backend) throw new Error("OpenClaw sandbox backend is unavailable.");
2224
- return backend;
2225
- }
2226
- /** Returns the configured filesystem bridge or fails the current JSON-RPC request. */
2227
- function requireFsBridge(execServer) {
2228
- const fsBridge = execServer.sandbox.fsBridge;
2229
- if (!fsBridge) throw new Error("Sandbox filesystem bridge is unavailable.");
2230
- return fsBridge;
2231
- }
2232
- //#endregion
2233
1778
  //#region extensions/codex/src/app-server/sandbox-exec-server/filesystem.ts
2234
1779
  /**
2235
1780
  * Implements filesystem JSON-RPC handlers for the Codex sandbox exec-server
@@ -2252,7 +1797,7 @@ async function openFile(execServer, handles, params) {
2252
1797
  path: filePath,
2253
1798
  access: "read"
2254
1799
  }]);
2255
- const fsBridge = requireFsBridge(execServer);
1800
+ const fsBridge = execServer.fsBridge;
2256
1801
  const handle = {
2257
1802
  abortController: new AbortController(),
2258
1803
  closeRequested: false,
@@ -2339,7 +1884,7 @@ async function readFile(execServer, params) {
2339
1884
  path: filePath,
2340
1885
  access: "read"
2341
1886
  }]);
2342
- const fsBridge = requireFsBridge(execServer);
1887
+ const fsBridge = execServer.fsBridge;
2343
1888
  const stat = await fsBridge.stat({ filePath });
2344
1889
  if (!stat) throw new JsonRpcProtocolError(JSON_RPC_NOT_FOUND, "file not found");
2345
1890
  assertSandboxFileReadWithinLimit(stat);
@@ -2356,7 +1901,7 @@ async function writeFile(execServer, params) {
2356
1901
  path: filePath,
2357
1902
  access: "write"
2358
1903
  }]);
2359
- const fsBridge = requireFsBridge(execServer);
1904
+ const fsBridge = execServer.fsBridge;
2360
1905
  if ((await fsBridge.stat({ filePath: posix.dirname(filePath) }))?.type !== "directory") throw new JsonRpcProtocolError(JSON_RPC_NOT_FOUND, "parent directory not found");
2361
1906
  await fsBridge.writeFile({
2362
1907
  filePath,
@@ -2372,7 +1917,7 @@ async function createDirectory(execServer, params) {
2372
1917
  path: filePath,
2373
1918
  access: "write"
2374
1919
  }]);
2375
- const fsBridge = requireFsBridge(execServer);
1920
+ const fsBridge = execServer.fsBridge;
2376
1921
  if (record.recursive === false) {
2377
1922
  const parentPath = posix.dirname(filePath);
2378
1923
  if ((await fsBridge.stat({ filePath: parentPath }))?.type !== "directory") throw new JsonRpcProtocolError(JSON_RPC_NOT_FOUND, "parent directory not found");
@@ -2387,7 +1932,7 @@ async function getMetadata(execServer, params) {
2387
1932
  path: filePath,
2388
1933
  access: "read"
2389
1934
  }]);
2390
- const stat = await requireFsBridge(execServer).stat({ filePath });
1935
+ const stat = await execServer.fsBridge.stat({ filePath });
2391
1936
  if (!stat) throw new JsonRpcProtocolError(JSON_RPC_NOT_FOUND, "file not found");
2392
1937
  return metadataResponse(stat);
2393
1938
  }
@@ -2401,11 +1946,9 @@ async function listDirectoryEntries(execServer, filePath, fsSandboxPolicy) {
2401
1946
  path: filePath,
2402
1947
  access: "read"
2403
1948
  }]);
2404
- const fsBridge = requireFsBridge(execServer);
2405
- const backend = requireBackend(execServer);
2406
- const resolved = fsBridge.resolvePath({ filePath });
1949
+ const resolved = execServer.fsBridge.resolvePath({ filePath });
2407
1950
  if (!resolved) throw new Error(`Cannot resolve sandbox path: ${filePath}`);
2408
- const result = await backend.runShellCommand({
1951
+ const result = await execServer.backend.runShellCommand({
2409
1952
  script: "find \"$1\" -mindepth 1 -maxdepth 1 -exec sh -c 'for path do name=${path##*/}; if [ -L \"$path\" ]; then kind=o; elif [ -d \"$path\" ]; then kind=d; elif [ -f \"$path\" ]; then kind=f; else kind=o; fi; printf \"%s\\t%s\\n\" \"$kind\" \"$name\"; done' sh {} +",
2410
1953
  args: [resolved.containerPath],
2411
1954
  allowFailure: true
@@ -2433,7 +1976,7 @@ async function removePath(execServer, params) {
2433
1976
  access: "write"
2434
1977
  }]);
2435
1978
  if (record.recursive !== false) assertNoReadOnlyDescendant(fsSandboxPolicy, filePath, "remove");
2436
- await requireFsBridge(execServer).remove({
1979
+ await execServer.fsBridge.remove({
2437
1980
  filePath,
2438
1981
  recursive: record.recursive !== false,
2439
1982
  force: record.force !== false
@@ -2460,8 +2003,7 @@ async function copyPath(execServer, params) {
2460
2003
  });
2461
2004
  }
2462
2005
  async function copySandboxPath(execServer, params) {
2463
- const fsBridge = execServer.sandbox.fsBridge;
2464
- if (!fsBridge) throw new Error("Sandbox filesystem bridge is unavailable.");
2006
+ const fsBridge = execServer.fsBridge;
2465
2007
  assertResolvedFsSandboxAccess(params.fsSandboxPolicy, [{
2466
2008
  path: params.sourcePath,
2467
2009
  access: "read"
@@ -2519,6 +2061,126 @@ function metadataResponse(stat) {
2519
2061
  };
2520
2062
  }
2521
2063
  //#endregion
2064
+ //#region extensions/codex/src/app-server/sandbox-exec-server/sandbox-child.ts
2065
+ /** Owns one sandbox subprocess tree through close, reaping, and backend finalization. */
2066
+ const SANDBOX_CHILD_TERM_GRACE_MS = 1e3;
2067
+ const SANDBOX_CHILD_REAP_TIMEOUT_MS = 4500;
2068
+ const SANDBOX_EXEC_MARKER = "CODEX_SANDBOX_EXEC_ID";
2069
+ async function spawnSandboxChild(params) {
2070
+ const [command, ...args] = params.argv;
2071
+ const finalize = async (status, exitCode) => await params.finalizeExec?.({
2072
+ status,
2073
+ exitCode,
2074
+ timedOut: false,
2075
+ token: params.finalizeToken
2076
+ });
2077
+ if (!command) {
2078
+ await finalize("failed", null).catch(params.onFinalizeError);
2079
+ throw new Error("OpenClaw sandbox exec spec did not provide a command.");
2080
+ }
2081
+ let child;
2082
+ try {
2083
+ child = spawn(command, args, {
2084
+ detached: process.platform !== "win32",
2085
+ env: params.env,
2086
+ stdio: [
2087
+ "pipe",
2088
+ "pipe",
2089
+ "pipe"
2090
+ ]
2091
+ });
2092
+ } catch (error) {
2093
+ await finalize("failed", null).catch(params.onFinalizeError);
2094
+ throw error;
2095
+ }
2096
+ let outcome;
2097
+ const closed = new Promise((resolve) => {
2098
+ child.once("close", (code, signal) => resolve(outcome = {
2099
+ exitCode: code ?? 1,
2100
+ signal
2101
+ }));
2102
+ });
2103
+ let finalizePromise;
2104
+ let terminationCleanup;
2105
+ let terminationError;
2106
+ const settled = closed.then(async (result) => {
2107
+ await terminationCleanup;
2108
+ child.stdin.destroy();
2109
+ await (finalizePromise ??= finalize(params.finalizeStatus(result), result.exitCode));
2110
+ return result;
2111
+ });
2112
+ settled.catch(params.onFinalizeError);
2113
+ let terminationPromise;
2114
+ const owner = {
2115
+ process: child,
2116
+ settled,
2117
+ terminate: () => terminationPromise ??= (async () => {
2118
+ child.stdin.destroy();
2119
+ terminationCleanup = params.terminateRemote?.().catch((error) => {
2120
+ terminationError = error instanceof Error ? error : new Error(String(error));
2121
+ });
2122
+ await terminationCleanup;
2123
+ if (!outcome) {
2124
+ if (child.pid) killProcessTree(child.pid, {
2125
+ detached: process.platform !== "win32",
2126
+ graceMs: SANDBOX_CHILD_TERM_GRACE_MS
2127
+ });
2128
+ else child.kill("SIGTERM");
2129
+ if (!await Promise.race([closed.then(() => true), delay(SANDBOX_CHILD_REAP_TIMEOUT_MS).then(() => false)])) throw new Error(`Sandbox child process tree ${child.pid ?? "unknown"} survived SIGKILL; tear down the sandbox environment and inspect the surviving process tree before retrying.`);
2130
+ }
2131
+ const result = await settled;
2132
+ if (terminationError) throw terminationError;
2133
+ return result;
2134
+ })()
2135
+ };
2136
+ params.owners.add(owner);
2137
+ settled.then(() => params.owners.delete(owner), () => params.owners.delete(owner));
2138
+ return owner;
2139
+ }
2140
+ function prepareSandboxChildExec(backend, env) {
2141
+ const marker = randomUUID();
2142
+ return {
2143
+ env: {
2144
+ ...env,
2145
+ [SANDBOX_EXEC_MARKER]: marker
2146
+ },
2147
+ terminate: async () => {
2148
+ const result = await backend.runShellCommand({
2149
+ script: SANDBOX_REMOTE_TERMINATE_SCRIPT,
2150
+ args: [`${SANDBOX_EXEC_MARKER}=${marker}`],
2151
+ allowFailure: true,
2152
+ signal: AbortSignal.timeout(SANDBOX_CHILD_REAP_TIMEOUT_MS)
2153
+ });
2154
+ if (result.code !== 0) {
2155
+ const detail = result.stderr.toString("utf8").trim() || result.stdout.toString("utf8").trim();
2156
+ throw new Error(detail || `Sandbox process tree cleanup failed with code ${result.code}; tear down the sandbox environment and inspect surviving processes before retrying.`);
2157
+ }
2158
+ }
2159
+ };
2160
+ }
2161
+ const SANDBOX_REMOTE_TERMINATE_SCRIPT = String.raw`
2162
+ find_owned_pids() {
2163
+ for env_file in /proc/[0-9]*/environ; do
2164
+ if [ -r "$env_file" ] && tr '\0' '\n' < "$env_file" 2>/dev/null | grep -Fqx "$1"; then
2165
+ basename "$(dirname "$env_file")"
2166
+ fi
2167
+ done
2168
+ }
2169
+ owned="$(find_owned_pids "$1")"
2170
+ [ -z "$owned" ] || kill -TERM $owned 2>/dev/null || true
2171
+ sleep 1
2172
+ owned="$(find_owned_pids "$1")"
2173
+ [ -z "$owned" ] || kill -KILL $owned 2>/dev/null || true
2174
+ sleep 1
2175
+ owned="$(find_owned_pids "$1")"
2176
+ [ -z "$owned" ] || { echo "Sandbox process IDs survived SIGKILL: $owned" >&2; exit 1; }
2177
+ `.trim();
2178
+ function delay(ms) {
2179
+ return new Promise((resolve) => {
2180
+ setTimeout(resolve, ms).unref?.();
2181
+ });
2182
+ }
2183
+ //#endregion
2522
2184
  //#region extensions/codex/src/app-server/sandbox-exec-server/http.ts
2523
2185
  /**
2524
2186
  * Implements sandboxed HTTP requests for Codex native tools by routing network
@@ -2527,10 +2189,12 @@ function metadataResponse(stat) {
2527
2189
  /** Maximum JSON-line size accepted from the streaming HTTP helper process. */
2528
2190
  const SANDBOX_HTTP_STREAM_LINE_MAX_CHARS = 256 * 1024;
2529
2191
  /** Handles one sandbox HTTP JSON-RPC request, optionally streaming response body deltas. */
2530
- async function httpRequest(execServer, socket, params) {
2192
+ async function httpRequest(execServer, notifications, params) {
2531
2193
  const record = requireObject(params, "http/request params");
2532
2194
  const requestId = requireString(record.requestId, "requestId");
2533
2195
  const url = requireString(record.url, "url");
2196
+ const redirectPolicy = record.redirectPolicy ?? "follow";
2197
+ if (redirectPolicy !== "follow" && redirectPolicy !== "stop") throw new Error("http/request redirectPolicy must be follow or stop");
2534
2198
  assertSandboxHttpRequestTargetAllowed(url);
2535
2199
  const request = {
2536
2200
  method: requireString(record.method, "method"),
@@ -2538,9 +2202,10 @@ async function httpRequest(execServer, socket, params) {
2538
2202
  headers: readHttpHeaders(record.headers),
2539
2203
  bodyBase64: typeof record.bodyBase64 === "string" ? record.bodyBase64 : void 0,
2540
2204
  timeoutMs: typeof record.timeoutMs === "number" && record.timeoutMs > 0 ? Math.floor(record.timeoutMs) : void 0,
2205
+ redirectPolicy,
2541
2206
  streamResponse: record.streamResponse === true
2542
2207
  };
2543
- if (request.streamResponse) return await runStreamingSandboxHttpRequest(execServer, socket, requestId, request);
2208
+ if (request.streamResponse) return await runStreamingSandboxHttpRequest(execServer, notifications, requestId, request);
2544
2209
  return await runSandboxHttpRequest(execServer, {
2545
2210
  ...request,
2546
2211
  streamResponse: false
@@ -2557,7 +2222,7 @@ function assertSandboxHttpRequestTargetAllowed(url) {
2557
2222
  if (isBlockedHostnameOrIp(parsed.hostname)) throw new SsrFBlockedError(`Blocked hostname or private/internal IP in sandbox http/request: ${parsed.hostname}`);
2558
2223
  }
2559
2224
  async function runSandboxHttpRequest(execServer, params) {
2560
- const result = await requireBackend(execServer).runShellCommand({
2225
+ const result = await execServer.backend.runShellCommand({
2561
2226
  script: SANDBOX_HTTP_REQUEST_SCRIPT,
2562
2227
  stdin: JSON.stringify(params),
2563
2228
  allowFailure: true
@@ -2574,44 +2239,40 @@ async function runSandboxHttpRequest(execServer, params) {
2574
2239
  bodyBase64: typeof parsed.bodyBase64 === "string" ? parsed.bodyBase64 : ""
2575
2240
  };
2576
2241
  }
2577
- async function runStreamingSandboxHttpRequest(execServer, socket, requestId, params) {
2578
- const backend = requireBackend(execServer);
2242
+ async function runStreamingSandboxHttpRequest(execServer, notifications, requestId, params) {
2243
+ const backend = execServer.backend;
2244
+ const remoteExec = prepareSandboxChildExec(backend, {});
2579
2245
  const execSpec = await backend.buildExecSpec({
2580
2246
  command: SANDBOX_HTTP_REQUEST_SCRIPT,
2581
2247
  workdir: execServer.sandbox.containerWorkdir,
2582
- env: {},
2248
+ env: remoteExec.env,
2583
2249
  usePty: false
2584
2250
  });
2585
- let child;
2586
- try {
2587
- const [command, ...args] = execSpec.argv;
2588
- if (!command) throw new Error("OpenClaw sandbox HTTP exec spec did not provide a command.");
2589
- child = spawn(command, args, {
2590
- env: execSpec.env,
2591
- stdio: [
2592
- "pipe",
2593
- "pipe",
2594
- "pipe"
2595
- ]
2251
+ const lifecycle = { failed: false };
2252
+ const owner = await spawnSandboxChild({
2253
+ argv: execSpec.argv,
2254
+ env: execSpec.env,
2255
+ finalizeExec: backend.finalizeExec,
2256
+ finalizeToken: execSpec.finalizeToken,
2257
+ finalizeStatus: (outcome) => lifecycle.failed || outcome.exitCode !== 0 ? "failed" : "completed",
2258
+ onFinalizeError: (error) => {
2259
+ embeddedAgentLog.warn("codex sandbox http/request finalize failed", { error });
2260
+ },
2261
+ owners: execServer.children,
2262
+ terminateRemote: remoteExec.terminate
2263
+ });
2264
+ const child = owner.process;
2265
+ const abortOnSessionClose = () => {
2266
+ lifecycle.failed = true;
2267
+ owner.terminate().catch((error) => {
2268
+ embeddedAgentLog.warn("codex sandbox http/request cleanup failed", { error });
2596
2269
  });
2597
- } catch (error) {
2598
- try {
2599
- await backend.finalizeExec?.({
2600
- status: "failed",
2601
- exitCode: null,
2602
- timedOut: false,
2603
- token: execSpec.finalizeToken
2604
- });
2605
- } catch (finalizeError) {
2606
- embeddedAgentLog.warn("codex sandbox http/request finalize after start failure failed", { error: finalizeError });
2607
- }
2608
- throw error;
2609
- }
2610
- const abortOnSocketClose = () => child.kill("SIGTERM");
2611
- socket.once("close", abortOnSocketClose);
2270
+ };
2271
+ notifications.signal.addEventListener("abort", abortOnSessionClose, { once: true });
2612
2272
  child.once("close", () => {
2613
- socket.off("close", abortOnSocketClose);
2273
+ notifications.signal.removeEventListener("abort", abortOnSessionClose);
2614
2274
  });
2275
+ if (notifications.signal.aborted) abortOnSessionClose();
2615
2276
  child.stdin.on("error", (error) => {
2616
2277
  if (error.code === "EPIPE" || error.code === "ERR_STREAM_DESTROYED") return;
2617
2278
  embeddedAgentLog.warn("codex sandbox http/request stdin write failed", { error });
@@ -2619,10 +2280,10 @@ async function runStreamingSandboxHttpRequest(execServer, socket, requestId, par
2619
2280
  child.stdin.end(JSON.stringify(params));
2620
2281
  return await readStreamingSandboxHttpResponse({
2621
2282
  child,
2622
- execSpec,
2623
- finalizeExec: backend.finalizeExec,
2283
+ lifecycle,
2284
+ owner,
2624
2285
  requestId,
2625
- socket
2286
+ notifications
2626
2287
  });
2627
2288
  }
2628
2289
  function readStreamingSandboxHttpResponse(params) {
@@ -2633,22 +2294,15 @@ function readStreamingSandboxHttpResponse(params) {
2633
2294
  let lastBodySeq = 0;
2634
2295
  let stdoutBuffer = "";
2635
2296
  let stderr = "";
2636
- const finalize = async (status, exitCode) => {
2637
- await params.finalizeExec?.({
2638
- status,
2639
- exitCode,
2640
- timedOut: false,
2641
- token: params.execSpec.finalizeToken
2642
- });
2643
- };
2644
- const fail = (message, exitCode) => {
2297
+ const fail = (message, _exitCode) => {
2645
2298
  if (failed) return;
2646
2299
  failed = true;
2647
- finalize("failed", exitCode).catch((error) => {
2648
- embeddedAgentLog.warn("codex sandbox http/request finalize failed", { error });
2300
+ params.lifecycle.failed = true;
2301
+ params.owner.terminate().catch((error) => {
2302
+ embeddedAgentLog.warn("codex sandbox http/request cleanup failed", { error });
2649
2303
  });
2650
2304
  if (headerResolved) {
2651
- sendHttpBodyDelta(params.socket, {
2305
+ if (params.notifications.isOpen()) params.notifications.send("http/request/bodyDelta", {
2652
2306
  requestId: params.requestId,
2653
2307
  seq: lastBodySeq + 1,
2654
2308
  deltaBase64: "",
@@ -2679,7 +2333,7 @@ function readStreamingSandboxHttpResponse(params) {
2679
2333
  } else if (type === "bodyDelta") {
2680
2334
  const seq = requireNumber(message.seq, "http body sequence");
2681
2335
  lastBodySeq = Math.max(lastBodySeq, seq);
2682
- sendHttpBodyDelta(params.socket, {
2336
+ if (params.notifications.isOpen()) params.notifications.send("http/request/bodyDelta", {
2683
2337
  requestId: params.requestId,
2684
2338
  seq,
2685
2339
  deltaBase64: typeof message.deltaBase64 === "string" ? message.deltaBase64 : "",
@@ -2692,10 +2346,7 @@ function readStreamingSandboxHttpResponse(params) {
2692
2346
  }
2693
2347
  newline = stdoutBuffer.indexOf("\n");
2694
2348
  }
2695
- if (stdoutBuffer.length > SANDBOX_HTTP_STREAM_LINE_MAX_CHARS) {
2696
- params.child.kill("SIGKILL");
2697
- fail(`sandbox http/request produced an unterminated stdout line longer than ${SANDBOX_HTTP_STREAM_LINE_MAX_CHARS} characters`, null);
2698
- }
2349
+ if (stdoutBuffer.length > SANDBOX_HTTP_STREAM_LINE_MAX_CHARS) fail(`sandbox http/request produced an unterminated stdout line longer than ${SANDBOX_HTTP_STREAM_LINE_MAX_CHARS} characters`, null);
2699
2350
  });
2700
2351
  params.child.stderr.setEncoding("utf8");
2701
2352
  params.child.stderr.on("data", (chunk) => {
@@ -2703,6 +2354,7 @@ function readStreamingSandboxHttpResponse(params) {
2703
2354
  });
2704
2355
  params.child.once("error", (error) => {
2705
2356
  childFailure ??= error.message;
2357
+ params.lifecycle.failed = true;
2706
2358
  });
2707
2359
  params.child.once("close", (code) => {
2708
2360
  const exitCode = code ?? 1;
@@ -2712,10 +2364,10 @@ function readStreamingSandboxHttpResponse(params) {
2712
2364
  return;
2713
2365
  }
2714
2366
  if (exitCode === 0) {
2715
- finalize("completed", exitCode).catch((error) => {
2716
- embeddedAgentLog.warn("codex sandbox http/request finalize failed", { error });
2717
- });
2718
- if (!headerResolved) reject(/* @__PURE__ */ new Error("sandbox http/request exited before returning headers"));
2367
+ if (!headerResolved) {
2368
+ params.lifecycle.failed = true;
2369
+ reject(/* @__PURE__ */ new Error("sandbox http/request exited before returning headers"));
2370
+ }
2719
2371
  return;
2720
2372
  }
2721
2373
  fail(stderr.trim() || `sandbox http/request failed with code ${exitCode}`, exitCode);
@@ -2847,9 +2499,40 @@ def assert_url_allowed(url):
2847
2499
  PINNED_ADDRESSES[hostname] = sorted(addresses)
2848
2500
 
2849
2501
  class GuardedRedirectHandler(urllib.request.HTTPRedirectHandler):
2502
+ def __init__(self, redirect_policy):
2503
+ self.redirect_policy = redirect_policy
2504
+
2850
2505
  def redirect_request(self, req, fp, code, msg, headers, newurl):
2506
+ if self.redirect_policy == "stop":
2507
+ return None
2851
2508
  assert_url_allowed(newurl)
2852
- return super().redirect_request(req, fp, code, msg, headers, newurl)
2509
+ method = req.get_method()
2510
+ drop_body = code == 303 or (code in (301, 302) and method == "POST")
2511
+ next_headers = dict(req.headers)
2512
+ if drop_body:
2513
+ if method != "HEAD":
2514
+ method = "GET"
2515
+ for name in ("content-type", "content-length", "content-encoding", "transfer-encoding"):
2516
+ next_headers.pop(name.capitalize(), None)
2517
+ redirected = urllib.request.Request(
2518
+ newurl,
2519
+ data=None if drop_body else req.data,
2520
+ headers=next_headers,
2521
+ method=method,
2522
+ origin_req_host=req.origin_req_host,
2523
+ unverifiable=True,
2524
+ )
2525
+ previous = urllib.parse.urlsplit(req.full_url)
2526
+ target = urllib.parse.urlsplit(newurl)
2527
+ previous_port = previous.port or (443 if previous.scheme == "https" else 80)
2528
+ target_port = target.port or (443 if target.scheme == "https" else 80)
2529
+ if (previous.scheme, previous.hostname, previous_port) != (
2530
+ target.scheme, target.hostname, target_port
2531
+ ):
2532
+ # Match Codex's route-aware client: cross-origin hops never inherit secrets.
2533
+ for name in ("authorization", "cookie", "proxy-authorization", "www-authenticate", "cookie2"):
2534
+ redirected.remove_header(name.capitalize())
2535
+ return redirected
2853
2536
 
2854
2537
  def pinned_getaddrinfo(original_getaddrinfo):
2855
2538
  def getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
@@ -2905,7 +2588,8 @@ def main():
2905
2588
  timeout = None
2906
2589
  if isinstance(timeout_ms, (int, float)) and timeout_ms > 0:
2907
2590
  timeout = timeout_ms / 1000
2908
- opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), GuardedRedirectHandler)
2591
+ redirect_handler = GuardedRedirectHandler(input_data.get("redirectPolicy", "follow"))
2592
+ opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), redirect_handler)
2909
2593
  original_getaddrinfo = socket.getaddrinfo
2910
2594
  socket.getaddrinfo = pinned_getaddrinfo(original_getaddrinfo)
2911
2595
  try:
@@ -2921,20 +2605,6 @@ if __name__ == "__main__":
2921
2605
  PY
2922
2606
  python3 "$tmp"
2923
2607
  `.trim();
2924
- function sendHttpBodyDelta(socket, params) {
2925
- if (socket.readyState !== 1) return;
2926
- socket.send(JSON.stringify({
2927
- jsonrpc: "2.0",
2928
- method: "http/request/bodyDelta",
2929
- params: {
2930
- requestId: params.requestId,
2931
- seq: params.seq,
2932
- deltaBase64: params.deltaBase64,
2933
- done: params.done,
2934
- error: params.error ?? null
2935
- }
2936
- }));
2937
- }
2938
2608
  //#endregion
2939
2609
  //#region extensions/codex/src/app-server/sandbox-exec-server/processes.ts
2940
2610
  /**
@@ -2945,13 +2615,14 @@ const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
2945
2615
  const RETAINED_PROCESS_OUTPUT_BYTES = 1024 * 1024;
2946
2616
  const CLOSED_PROCESS_EVICTION_MS = 6e4;
2947
2617
  /** Starts a sandbox-backed process and registers it in the connection-local process table. */
2948
- async function startProcess(execServer, processes, socket, params) {
2618
+ async function startProcess(execServer, processes, notify, params) {
2949
2619
  const record = requireObject(params, "process/start params");
2950
2620
  const processId = requireString(record.processId, "processId");
2951
2621
  if (processes.has(processId)) throw new Error(`process already exists: ${processId}`);
2952
2622
  const argv = requireStringArray(record.argv, "argv");
2953
2623
  const cwd = resolveExecServerPath(requireString(record.cwd, "cwd"), "process cwd");
2954
2624
  rejectUnsupportedArg0(record.arg0);
2625
+ assertSupportedProcessSandbox(execServer, record);
2955
2626
  const env = readProcessEnv(record);
2956
2627
  const managed = {
2957
2628
  processId,
@@ -2964,17 +2635,10 @@ async function startProcess(execServer, processes, socket, params) {
2964
2635
  failure: null,
2965
2636
  tty: record.tty === true,
2966
2637
  pipeStdin: record.pipeStdin === true,
2967
- abortController: new AbortController(),
2638
+ terminationRequested: false,
2968
2639
  child: null,
2969
- finalized: false,
2970
2640
  waiters: [],
2971
- emitNotification: (method, notificationParams) => {
2972
- if (socket.readyState === 1) socket.send(JSON.stringify({
2973
- jsonrpc: "2.0",
2974
- method,
2975
- params: notificationParams
2976
- }));
2977
- },
2641
+ emitNotification: notify,
2978
2642
  evictProcess: () => {
2979
2643
  if (managed.evictionTimer) return;
2980
2644
  managed.evictionTimer = setTimeout(() => {
@@ -2984,12 +2648,14 @@ async function startProcess(execServer, processes, socket, params) {
2984
2648
  }
2985
2649
  };
2986
2650
  processes.set(processId, managed);
2651
+ const startPromise = runProcess(execServer, managed, {
2652
+ argv,
2653
+ cwd,
2654
+ env
2655
+ });
2656
+ managed.startPromise = startPromise;
2987
2657
  try {
2988
- await runProcess(execServer, managed, {
2989
- argv,
2990
- cwd,
2991
- env
2992
- });
2658
+ await startPromise;
2993
2659
  } catch (error) {
2994
2660
  processes.delete(processId);
2995
2661
  managed.failure = coerceErrorMessage(error);
@@ -2998,47 +2664,60 @@ async function startProcess(execServer, processes, socket, params) {
2998
2664
  managed.closed = true;
2999
2665
  notifyProcessWaiters(managed);
3000
2666
  throw error;
2667
+ } finally {
2668
+ if (managed.startPromise === startPromise) managed.startPromise = void 0;
3001
2669
  }
3002
- return { processId };
2670
+ return {
2671
+ processId,
2672
+ sandboxType: "none"
2673
+ };
2674
+ }
2675
+ function assertSupportedProcessSandbox(execServer, record) {
2676
+ if (record.networkProxy !== void 0 && record.networkProxy !== null) throw new Error("Codex sandbox exec-server network proxy launch is not supported.");
2677
+ if (record.enforceManagedNetwork === true || record.managedNetwork !== void 0 && record.managedNetwork !== null) throw new Error("Codex managed network restrictions cannot be enforced by the sandbox backend.");
2678
+ if (resolveFsSandboxPolicy(execServer, record)?.unrestricted === false) throw new Error("Codex process filesystem sandbox restrictions cannot be enforced by the backend.");
2679
+ if (record.sandbox === void 0 || record.sandbox === null) return;
2680
+ if (requireObject(requireObject(record.sandbox, "process sandbox context").permissions, "process sandbox permissions").network !== "restricted") return;
2681
+ if (!execServer.networkIsolated) throw new Error("Codex network restrictions cannot be enforced by the sandbox backend.");
3003
2682
  }
3004
2683
  async function runProcess(execServer, managed, params) {
3005
- const backend = execServer.sandbox.backend;
3006
- if (!backend) throw new Error("OpenClaw sandbox backend is unavailable.");
2684
+ const backend = execServer.backend;
3007
2685
  throwIfProcessStartCancelled(managed);
2686
+ const remoteExec = prepareSandboxChildExec(backend, params.env);
3008
2687
  const execSpec = await backend.buildExecSpec({
3009
- command: shellCommandFromArgv(params.argv),
2688
+ command: buildRemoteCommand(params.argv),
3010
2689
  workdir: params.cwd,
3011
- env: params.env,
2690
+ env: remoteExec.env,
3012
2691
  usePty: false
3013
2692
  });
3014
- managed.finalizeToken = execSpec.finalizeToken;
3015
- managed.finalizeExec = backend.finalizeExec;
3016
- let child;
3017
- try {
3018
- if (managed.abortController.signal.aborted) throw new Error("process start cancelled");
3019
- const [command, ...args] = execSpec.argv;
3020
- if (!command) throw new Error("OpenClaw sandbox exec spec did not provide a command.");
3021
- child = spawn(command, args, {
3022
- env: execSpec.env,
3023
- stdio: [
3024
- "pipe",
3025
- "pipe",
3026
- "pipe"
3027
- ]
2693
+ if (managed.terminationRequested) {
2694
+ await backend.finalizeExec?.({
2695
+ status: "failed",
2696
+ exitCode: null,
2697
+ timedOut: false,
2698
+ token: execSpec.finalizeToken
3028
2699
  });
3029
- } catch (error) {
3030
- managed.failure = coerceErrorMessage(error);
3031
- await finalizeProcess(managed).catch((finalizeError) => {
3032
- embeddedAgentLog.warn("codex sandbox exec-server finalize after start failure failed", {
2700
+ throw new Error("process start cancelled");
2701
+ }
2702
+ const owner = await spawnSandboxChild({
2703
+ argv: execSpec.argv,
2704
+ env: execSpec.env,
2705
+ finalizeExec: backend.finalizeExec,
2706
+ finalizeToken: execSpec.finalizeToken,
2707
+ finalizeStatus: () => managed.failure ? "failed" : "completed",
2708
+ onFinalizeError: (error) => {
2709
+ const message = coerceErrorMessage(error);
2710
+ managed.failure ??= message;
2711
+ embeddedAgentLog.warn("codex sandbox exec-server finalize failed", {
3033
2712
  processId: managed.processId,
3034
- error: coerceErrorMessage(finalizeError)
2713
+ error: message
3035
2714
  });
3036
- });
3037
- throw error;
3038
- }
3039
- managed.child = child;
3040
- const abortListener = () => child.kill("SIGTERM");
3041
- managed.abortController.signal.addEventListener("abort", abortListener, { once: true });
2715
+ },
2716
+ owners: execServer.children,
2717
+ terminateRemote: remoteExec.terminate
2718
+ });
2719
+ managed.child = owner;
2720
+ const child = owner.process;
3042
2721
  child.stdout.on("data", (chunk) => appendProcessChunk(managed, managed.tty ? "pty" : "stdout", chunk));
3043
2722
  child.stderr.on("data", (chunk) => appendProcessChunk(managed, "stderr", chunk));
3044
2723
  child.once("error", (error) => {
@@ -3046,13 +2725,12 @@ async function runProcess(execServer, managed, params) {
3046
2725
  notifyProcessWaiters(managed);
3047
2726
  });
3048
2727
  child.once("close", (code) => {
3049
- managed.abortController.signal.removeEventListener("abort", abortListener);
3050
2728
  emitProcessClosed(managed, code ?? 1);
3051
2729
  });
3052
2730
  if (!managed.tty && !managed.pipeStdin) child.stdin.end();
3053
2731
  }
3054
2732
  function throwIfProcessStartCancelled(managed) {
3055
- if (managed.abortController.signal.aborted) throw new Error("process start cancelled");
2733
+ if (managed.terminationRequested) throw new Error("process start cancelled");
3056
2734
  }
3057
2735
  function appendProcessChunk(managed, stream, data) {
3058
2736
  if (data.length === 0) return;
@@ -3098,28 +2776,9 @@ function emitProcessClosed(managed, exitCode) {
3098
2776
  seq: closeSeq
3099
2777
  });
3100
2778
  }
3101
- finalizeProcess(managed).catch((error) => {
3102
- const message = coerceErrorMessage(error);
3103
- managed.failure ??= message;
3104
- embeddedAgentLog.warn("codex sandbox exec-server finalize failed", {
3105
- processId: managed.processId,
3106
- error: message
3107
- });
3108
- });
3109
2779
  managed.evictProcess();
3110
2780
  notifyProcessWaiters(managed);
3111
2781
  }
3112
- async function finalizeProcess(managed) {
3113
- if (managed.finalized) return;
3114
- managed.finalized = true;
3115
- managed.child?.stdin.destroy();
3116
- await managed.finalizeExec?.({
3117
- status: managed.failure ? "failed" : "completed",
3118
- exitCode: managed.exitCode,
3119
- timedOut: false,
3120
- token: managed.finalizeToken
3121
- });
3122
- }
3123
2782
  function limitProcessChunks(chunks, maxBytes) {
3124
2783
  if (!maxBytes) return chunks;
3125
2784
  const retained = [];
@@ -3158,19 +2817,20 @@ function writeProcess(processes, params) {
3158
2817
  const managed = processes.get(processId);
3159
2818
  if (!managed) return { status: "unknownProcess" };
3160
2819
  const chunk = Buffer.from(requireString(record.chunk, "chunk"), "base64");
3161
- if (!managed.tty && !managed.pipeStdin || managed.closed || !managed.child?.stdin.writable) return { status: "stdinClosed" };
3162
- managed.child.stdin.write(chunk);
2820
+ if (!managed.tty && !managed.pipeStdin || managed.closed || !managed.child?.process.stdin.writable) return { status: "stdinClosed" };
2821
+ managed.child.process.stdin.write(chunk);
3163
2822
  return { status: "accepted" };
3164
2823
  }
3165
2824
  /** Requests process termination and reports whether it was running at call time. */
3166
- function terminateProcess(processes, params) {
2825
+ async function terminateProcess(processes, params) {
3167
2826
  const processId = requireString(requireObject(params, "process/terminate params").processId, "processId");
3168
2827
  const managed = processes.get(processId);
3169
2828
  if (!managed) return { running: false };
3170
2829
  const running = !managed.exited;
3171
- managed.abortController.abort();
3172
- managed.child?.kill("SIGTERM");
3173
- if (running && !managed.child) emitProcessClosed(managed, null);
2830
+ managed.terminationRequested = true;
2831
+ await managed.startPromise?.catch(() => void 0);
2832
+ if (managed.child) await managed.child.terminate();
2833
+ else if (running && !managed.closed) emitProcessClosed(managed, null);
3174
2834
  return { running };
3175
2835
  }
3176
2836
  function waitForProcessUpdate(managed, waitMs) {
@@ -3192,12 +2852,6 @@ function notifyProcessWaiters(managed) {
3192
2852
  function hasChunksAtOrAfter(managed, afterSeq) {
3193
2853
  return managed.chunks.some((chunk) => chunk.seq > afterSeq);
3194
2854
  }
3195
- function shellCommandFromArgv(argv) {
3196
- return argv.map(shellEscape).join(" ");
3197
- }
3198
- function shellEscape(value) {
3199
- return `'${value.replaceAll("'", `'"'"'`)}'`;
3200
- }
3201
2855
  function requireProcess(processes, processId) {
3202
2856
  const managed = processes.get(processId);
3203
2857
  if (!managed) throw new Error(`unknown process: ${processId}`);
@@ -3241,47 +2895,148 @@ function readStringList(value) {
3241
2895
  return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
3242
2896
  }
3243
2897
  //#endregion
2898
+ //#region extensions/codex/src/app-server/sandbox-exec-server/session.ts
2899
+ /** Owns the JSON-RPC protocol and resources of one sandbox execution connection. */
2900
+ /** Connection-local execution state; closing it never enables session resumption. */
2901
+ var CodexSandboxExecSession = class {
2902
+ constructor(execServer, transport) {
2903
+ this.execServer = execServer;
2904
+ this.transport = transport;
2905
+ this.processes = /* @__PURE__ */ new Map();
2906
+ this.fileReads = /* @__PURE__ */ new Map();
2907
+ this.closeController = new AbortController();
2908
+ this.notifications = {
2909
+ isOpen: transport.isOpen,
2910
+ signal: this.closeController.signal,
2911
+ send: (method, params) => {
2912
+ if (transport.isOpen()) transport.send({
2913
+ jsonrpc: "2.0",
2914
+ method,
2915
+ params
2916
+ });
2917
+ }
2918
+ };
2919
+ }
2920
+ async handleRequest(request) {
2921
+ const method = request.method;
2922
+ if (!method) {
2923
+ sendError(this.transport.send, request.id, -32600, "Invalid Request");
2924
+ return;
2925
+ }
2926
+ if (request.id === void 0) {
2927
+ if (method !== "initialized") sendError(this.transport.send, -1, -32600, `Unexpected notification: ${method}`);
2928
+ return;
2929
+ }
2930
+ try {
2931
+ const result = await this.dispatchRequest(method, request.params);
2932
+ sendResult(this.transport.send, request.id, result);
2933
+ } catch (error) {
2934
+ sendError(this.transport.send, request.id, error instanceof JsonRpcProtocolError ? error.code : -32603, error instanceof Error ? error.message : String(error));
2935
+ }
2936
+ }
2937
+ close() {
2938
+ if (!this.cleanup) {
2939
+ this.closeController.abort();
2940
+ closeAllFileReads(this.fileReads);
2941
+ this.cleanup = Promise.all([...this.processes.keys()].map(async (processId) => terminateProcess(this.processes, { processId }))).then(() => void 0);
2942
+ }
2943
+ return this.cleanup;
2944
+ }
2945
+ async dispatchRequest(method, params) {
2946
+ switch (method) {
2947
+ case "initialize": return { sessionId: randomUUID() };
2948
+ case "environment/info": return {
2949
+ shell: {
2950
+ name: "sh",
2951
+ path: "/bin/sh"
2952
+ },
2953
+ cwd: pathToFileURL(this.execServer.sandbox.containerWorkdir, { windows: false }).href,
2954
+ capabilities: { networkProxyLaunch: false }
2955
+ };
2956
+ case "environment/status": return { status: "ready" };
2957
+ case "process/start": return startProcess(this.execServer, this.processes, this.notifications.send, params);
2958
+ case "process/read": return await readProcess(this.processes, params);
2959
+ case "process/write": return writeProcess(this.processes, params);
2960
+ case "process/terminate": return await terminateProcess(this.processes, params);
2961
+ case "fs/open": return await openFile(this.execServer, this.fileReads, params);
2962
+ case "fs/readBlock": return readFileBlock(this.fileReads, params);
2963
+ case "fs/close": return closeFile(this.fileReads, params);
2964
+ case "fs/readFile": return await readFile(this.execServer, params);
2965
+ case "fs/writeFile":
2966
+ await writeFile(this.execServer, params);
2967
+ return {};
2968
+ case "fs/createDirectory":
2969
+ await createDirectory(this.execServer, params);
2970
+ return {};
2971
+ case "fs/getMetadata": return await getMetadata(this.execServer, params);
2972
+ case "fs/readDirectory": return await readDirectory(this.execServer, params);
2973
+ case "fs/remove":
2974
+ await removePath(this.execServer, params);
2975
+ return {};
2976
+ case "fs/copy":
2977
+ await copyPath(this.execServer, params);
2978
+ return {};
2979
+ case "http/request": return await httpRequest(this.execServer, this.notifications, params);
2980
+ default: throw new JsonRpcProtocolError(JSON_RPC_METHOD_NOT_FOUND, `Unsupported OpenClaw sandbox exec-server method: ${method}`);
2981
+ }
2982
+ }
2983
+ };
2984
+ //#endregion
3244
2985
  //#region extensions/codex/src/app-server/sandbox-exec-server.ts
3245
2986
  /**
3246
2987
  * Hosts the local OpenClaw sandbox exec-server that Codex app-server native
3247
2988
  * execution can register as an external environment.
3248
2989
  */
3249
2990
  const CODEX_SANDBOX_EXEC_SERVER_MAX_INBOUND_MESSAGE_BYTES = 100 * 1024 * 1024;
2991
+ const CODEX_NODE_EXEC_SERVER_MAX_MESSAGE_BYTES = 64 * 1024 * 1024;
2992
+ const codexNodeExecServerLeases = /* @__PURE__ */ new WeakMap();
3250
2993
  /** Starts or reuses a sandbox exec-server and registers it with Codex app-server. */
3251
2994
  async function ensureCodexSandboxExecServerEnvironment(params) {
3252
- if (!params.sandbox?.enabled || !params.sandbox.backend) return;
2995
+ if (!params.sandbox?.enabled) return;
2996
+ const placementNodeId = readCodexPlacementNodeId(params.sandbox);
2997
+ if (!params.sandbox.backend && !placementNodeId) return;
2998
+ if (placementNodeId && !params.runtime) throw new Error("Codex node execution requires its active plugin runtime.");
3253
2999
  if (!canExposeLocalExecServerToAppServer(params.appServerStartOptions)) throw new Error("OpenClaw Codex exec-server uses a local loopback URL and cannot be registered with a remote Codex app-server.");
3254
- const execServer = await acquireOpenClawExecServer(params.sandbox);
3000
+ const { server: execServer, nodeLease } = await acquireOpenClawExecServer({
3001
+ sandbox: params.sandbox,
3002
+ runtime: params.runtime,
3003
+ signal: params.signal,
3004
+ onExecutionDisconnect: params.onExecutionDisconnect
3005
+ });
3006
+ const environmentId = nodeLease ? `openclaw-node-${nodeLease.id}` : execServer.environmentId;
3255
3007
  try {
3008
+ const execServerUrl = nodeLease ? `${execServer.url}?lease=${nodeLease.id}` : execServer.url;
3256
3009
  await params.client.request("environment/add", {
3257
- environmentId: execServer.environmentId,
3258
- execServerUrl: execServer.url
3010
+ environmentId,
3011
+ execServerUrl
3259
3012
  }, {
3260
3013
  timeoutMs: params.timeoutMs,
3261
3014
  signal: params.signal
3262
3015
  });
3263
3016
  } catch (error) {
3017
+ if (nodeLease && "node" in execServer) closeCodexNodeExecServerLease(execServer, nodeLease);
3264
3018
  await releaseOpenClawExecServer(execServer);
3265
- if (isEnvironmentAddUnsupported(error)) {
3266
- embeddedAgentLog.warn("codex app-server does not support remote environments yet", { environmentId: execServer.environmentId });
3267
- return;
3268
- }
3269
3019
  throw error;
3270
3020
  }
3271
- return {
3272
- environmentId: execServer.environmentId,
3021
+ const environment = {
3022
+ environmentId,
3273
3023
  cwd: params.sandbox.containerWorkdir
3274
3024
  };
3025
+ if (nodeLease) codexNodeExecServerLeases.set(environment, nodeLease);
3026
+ return environment;
3275
3027
  }
3276
3028
  /** Releases the sandbox exec-server lease associated with a sandbox runtime. */
3277
- async function releaseCodexSandboxExecServerEnvironment(sandbox) {
3029
+ async function releaseCodexSandboxExecServerEnvironment(sandbox, environment) {
3278
3030
  if (!sandbox?.enabled) return;
3279
3031
  const server = await sandboxExecServerRegistry.servers.get(sandbox.runtimeId)?.catch(() => void 0);
3280
- if (server) await releaseOpenClawExecServer(server);
3281
- }
3282
- function isEnvironmentAddUnsupported(error) {
3283
- if (!(error instanceof Error)) return false;
3284
- return error.message.includes("environment/add") && (error.message.includes("unknown variant") || error.message.includes("Method not found"));
3032
+ if (server) {
3033
+ const nodeLease = environment && codexNodeExecServerLeases.get(environment);
3034
+ if (nodeLease && "node" in server) {
3035
+ codexNodeExecServerLeases.delete(environment);
3036
+ closeCodexNodeExecServerLease(server, nodeLease);
3037
+ }
3038
+ await releaseOpenClawExecServer(server);
3039
+ }
3285
3040
  }
3286
3041
  function canExposeLocalExecServerToAppServer(startOptions) {
3287
3042
  if (!startOptions || startOptions.transport !== "websocket") return true;
@@ -3295,14 +3050,60 @@ function canExposeLocalExecServerToAppServer(startOptions) {
3295
3050
  return false;
3296
3051
  }
3297
3052
  }
3298
- async function acquireOpenClawExecServer(sandbox) {
3053
+ async function acquireOpenClawExecServer(params) {
3054
+ const { sandbox, runtime, signal, onExecutionDisconnect } = params;
3299
3055
  const key = sandbox.runtimeId;
3300
3056
  while (true) {
3301
3057
  const promise = sandboxExecServerRegistry.servers.get(key) ?? startAndRememberOpenClawExecServer(sandbox);
3302
3058
  const server = await promise;
3303
3059
  if (!server.closed && sandboxExecServerRegistry.servers.get(key) === promise) {
3304
3060
  server.refCount += 1;
3305
- return server;
3061
+ if (!("node" in server)) return { server };
3062
+ if (!runtime || !signal) {
3063
+ await releaseOpenClawExecServer(server);
3064
+ throw new Error("Codex node execution requires an active runtime and attempt.");
3065
+ }
3066
+ try {
3067
+ const placementIdentity = readCodexPlacementWorkspaceIdentity(sandbox);
3068
+ const channel = await runtime.nodes.openDuplex({
3069
+ nodeId: server.node.id,
3070
+ command: "codex.exec-server.stdio.v1",
3071
+ params: {
3072
+ cwd: sandbox.containerWorkdir,
3073
+ ...placementIdentity
3074
+ },
3075
+ sessionKey: sandbox.sessionKey,
3076
+ timeoutMs: 0,
3077
+ maxMessageBytes: CODEX_NODE_EXEC_SERVER_MAX_MESSAGE_BYTES,
3078
+ maxOutstandingDeliveryBytes: 69206016,
3079
+ signal
3080
+ });
3081
+ if (signal.aborted || server.closed || sandboxExecServerRegistry.servers.get(key) !== promise) {
3082
+ channel.close();
3083
+ throw new Error("Codex node execution retired before its channel was ready.");
3084
+ }
3085
+ const nodeLease = {
3086
+ id: randomUUID(),
3087
+ channel,
3088
+ claimed: false,
3089
+ closed: false,
3090
+ onDisconnected: onExecutionDisconnect
3091
+ };
3092
+ server.node.leases.set(nodeLease.id, nodeLease);
3093
+ channel.closed.then(() => handleClosedCodexNodeExecServerLease(server, nodeLease, { failed: false }), (error) => handleClosedCodexNodeExecServerLease(server, nodeLease, {
3094
+ failed: true,
3095
+ error
3096
+ })).catch((error) => {
3097
+ embeddedAgentLog.warn("codex paired-device exec-server lease cleanup failed", { error: error instanceof Error ? error.message : String(error) });
3098
+ });
3099
+ return {
3100
+ server,
3101
+ nodeLease
3102
+ };
3103
+ } catch (error) {
3104
+ await releaseOpenClawExecServer(server);
3105
+ throw error;
3106
+ }
3306
3107
  }
3307
3108
  }
3308
3109
  }
@@ -3316,24 +3117,55 @@ function startAndRememberOpenClawExecServer(sandbox) {
3316
3117
  return created;
3317
3118
  }
3318
3119
  async function startOpenClawExecServer(sandbox) {
3120
+ const backend = sandbox.backend;
3121
+ const fsBridge = sandbox.fsBridge;
3122
+ const placementNodeId = readCodexPlacementNodeId(sandbox);
3123
+ let connection;
3124
+ if (placementNodeId) connection = {
3125
+ kind: "node",
3126
+ id: placementNodeId
3127
+ };
3128
+ else {
3129
+ if (!backend) throw new Error("OpenClaw sandbox backend is unavailable.");
3130
+ if (!fsBridge) throw new Error("Sandbox filesystem bridge is unavailable.");
3131
+ connection = {
3132
+ kind: "sandbox",
3133
+ backend,
3134
+ fsBridge
3135
+ };
3136
+ }
3319
3137
  const server = new WebSocketServer({
3320
3138
  host: "127.0.0.1",
3321
3139
  port: 0,
3322
- maxPayload: CODEX_SANDBOX_EXEC_SERVER_MAX_INBOUND_MESSAGE_BYTES
3140
+ maxPayload: connection.kind === "node" ? CODEX_NODE_EXEC_SERVER_MAX_MESSAGE_BYTES : CODEX_SANDBOX_EXEC_SERVER_MAX_INBOUND_MESSAGE_BYTES
3323
3141
  });
3324
3142
  await once(server, "listening");
3325
3143
  const address = server.address();
3326
3144
  if (!address || typeof address === "string") throw new Error("OpenClaw Codex exec-server did not bind to a TCP port.");
3327
3145
  const environmentId = buildEnvironmentId(sandbox);
3328
3146
  const authPath = `/openclaw-${randomUUID()}`;
3329
- const execServer = {
3147
+ const common = {
3330
3148
  authPath,
3331
3149
  closed: false,
3332
3150
  environmentId,
3333
3151
  refCount: 0,
3334
3152
  url: `ws://127.0.0.1:${address.port}${authPath}`,
3335
3153
  sandbox,
3336
- server
3154
+ server,
3155
+ children: /* @__PURE__ */ new Set(),
3156
+ cleanupTasks: /* @__PURE__ */ new Set()
3157
+ };
3158
+ const execServer = connection.kind === "node" ? {
3159
+ ...common,
3160
+ node: {
3161
+ id: connection.id,
3162
+ leases: /* @__PURE__ */ new Map()
3163
+ }
3164
+ } : {
3165
+ ...common,
3166
+ backend: connection.backend,
3167
+ fsBridge: connection.fsBridge,
3168
+ networkIsolated: (connection.backend.id === "docker" || connection.backend.id === "podman") && sandbox.docker.network.trim().toLowerCase() === "none"
3337
3169
  };
3338
3170
  server.on("connection", (socket, request) => {
3339
3171
  socket.on("error", handleExecServerSocketError);
@@ -3341,6 +3173,10 @@ async function startOpenClawExecServer(sandbox) {
3341
3173
  socket.close(1008, "unauthorized");
3342
3174
  return;
3343
3175
  }
3176
+ if ("node" in execServer) {
3177
+ handleNodeConnection(execServer, socket, request);
3178
+ return;
3179
+ }
3344
3180
  handleConnection(execServer, socket);
3345
3181
  });
3346
3182
  embeddedAgentLog.info("codex sandbox exec-server started", {
@@ -3357,15 +3193,7 @@ async function releaseOpenClawExecServer(execServer) {
3357
3193
  const current = await sandboxExecServerRegistry.servers.get(execServer.sandbox.runtimeId)?.catch(() => void 0);
3358
3194
  if (execServer.refCount > 0 || execServer.closed) return;
3359
3195
  if (current === execServer) sandboxExecServerRegistry.servers.delete(execServer.sandbox.runtimeId);
3360
- await closeOpenClawExecServer(execServer);
3361
- }
3362
- async function closeOpenClawExecServer(execServer) {
3363
- if (execServer.closed) return;
3364
- execServer.closed = true;
3365
- for (const client of execServer.server.clients) client.close(1001, "shutdown");
3366
- await new Promise((resolve) => {
3367
- execServer.server.close(() => resolve());
3368
- });
3196
+ await sandboxExecServerRegistry.close(execServer);
3369
3197
  }
3370
3198
  function buildEnvironmentId(sandbox) {
3371
3199
  return `openclaw-sandbox-${createHash("sha256").update(sandbox.runtimeId).digest("hex").slice(0, 16)}`;
@@ -3373,80 +3201,82 @@ function buildEnvironmentId(sandbox) {
3373
3201
  function isAuthorizedExecServerRequest(execServer, request) {
3374
3202
  return new URL(request.url ?? "", "ws://127.0.0.1").pathname === execServer.authPath;
3375
3203
  }
3204
+ function readCodexPlacementNodeId(sandbox) {
3205
+ if (!("placementExecutionMode" in sandbox) || sandbox.placementExecutionMode !== "remote-exec" || !("placementNodeId" in sandbox) || typeof sandbox.placementNodeId !== "string" || !sandbox.placementNodeId) return;
3206
+ return sandbox.placementNodeId;
3207
+ }
3208
+ function readCodexPlacementWorkspaceIdentity(sandbox) {
3209
+ if (!("placementEnvironmentId" in sandbox) || typeof sandbox.placementEnvironmentId !== "string" || !sandbox.placementEnvironmentId || sandbox.placementEnvironmentId.trim() !== sandbox.placementEnvironmentId || !("placementSessionId" in sandbox) || typeof sandbox.placementSessionId !== "string" || !sandbox.placementSessionId || sandbox.placementSessionId.trim() !== sandbox.placementSessionId || !("placementOwnerEpoch" in sandbox) || typeof sandbox.placementOwnerEpoch !== "number" || !Number.isSafeInteger(sandbox.placementOwnerEpoch) || sandbox.placementOwnerEpoch < 1 || !sandbox.sessionKey || sandbox.sessionKey.trim() !== sandbox.sessionKey) throw new Error("Codex node execution requires its exact placement workspace identity.");
3210
+ return {
3211
+ environmentId: sandbox.placementEnvironmentId,
3212
+ sessionId: sandbox.placementSessionId,
3213
+ ownerEpoch: sandbox.placementOwnerEpoch,
3214
+ sessionKey: sandbox.sessionKey
3215
+ };
3216
+ }
3217
+ function handleNodeConnection(execServer, socket, request) {
3218
+ const leaseId = new URL(request.url ?? "", "ws://127.0.0.1").searchParams.get("lease");
3219
+ const lease = leaseId ? execServer.node.leases.get(leaseId) : void 0;
3220
+ if (!lease || lease.claimed || lease.closed) {
3221
+ socket.close(1008, "execution channel unavailable");
3222
+ return;
3223
+ }
3224
+ lease.claimed = true;
3225
+ const cleanup = startCodexNodeExecServerRelay({
3226
+ lease,
3227
+ socket
3228
+ });
3229
+ execServer.cleanupTasks.add(cleanup);
3230
+ cleanup.then(() => execServer.cleanupTasks.delete(cleanup), (error) => {
3231
+ execServer.cleanupTasks.delete(cleanup);
3232
+ embeddedAgentLog.warn("codex paired-device exec-server relay failed", { error: error instanceof Error ? error.message : String(error) });
3233
+ });
3234
+ }
3235
+ function closeCodexNodeExecServerLease(execServer, lease) {
3236
+ execServer.node.leases.delete(lease.id);
3237
+ if (!lease.closed) {
3238
+ lease.closed = true;
3239
+ lease.closeRelay?.();
3240
+ lease.channel.close();
3241
+ }
3242
+ }
3243
+ function handleClosedCodexNodeExecServerLease(execServer, lease, result) {
3244
+ if (lease.closed) return;
3245
+ if (lease.onChannelClosed) {
3246
+ lease.onChannelClosed(result);
3247
+ return;
3248
+ }
3249
+ try {
3250
+ lease.onDisconnected?.(createCodexNodeExecServerDisconnectError(result.failed ? "execution node failed" : "execution node disconnected", result.error));
3251
+ } finally {
3252
+ closeCodexNodeExecServerLease(execServer, lease);
3253
+ }
3254
+ }
3376
3255
  function handleConnection(execServer, socket) {
3377
- const processes = /* @__PURE__ */ new Map();
3378
- const fileReads = /* @__PURE__ */ new Map();
3256
+ const session = new CodexSandboxExecSession(execServer, {
3257
+ isOpen: () => socket.readyState === socket.OPEN,
3258
+ send: (message) => socket.send(JSON.stringify(message))
3259
+ });
3379
3260
  socket.on("message", (data) => {
3380
- handleMessage(execServer, processes, fileReads, socket, data).catch((error) => {
3261
+ handleMessage(session, data).catch((error) => {
3381
3262
  embeddedAgentLog.warn("codex sandbox exec-server message failed", { error });
3382
3263
  });
3383
3264
  });
3384
3265
  socket.on("close", () => {
3385
- closeAllFileReads(fileReads);
3386
- for (const process of processes.values()) process.abortController.abort();
3266
+ const cleanup = session.close();
3267
+ execServer.cleanupTasks.add(cleanup);
3268
+ cleanup.then(() => execServer.cleanupTasks.delete(cleanup), (error) => {
3269
+ execServer.cleanupTasks.delete(cleanup);
3270
+ embeddedAgentLog.warn("codex sandbox exec-server socket cleanup failed", { error: error instanceof Error ? error.message : String(error) });
3271
+ });
3387
3272
  });
3388
3273
  }
3389
3274
  function handleExecServerSocketError(error) {
3390
3275
  embeddedAgentLog.debug("codex sandbox exec-server websocket failed", { error });
3391
3276
  }
3392
- async function handleMessage(execServer, processes, fileReads, socket, data) {
3393
- const request = parseRequest(data);
3394
- if (!request.method) {
3395
- sendError(socket, request.id, -32600, "Invalid Request");
3396
- return;
3397
- }
3398
- const method = request.method;
3399
- if (request.id === void 0) {
3400
- if (method !== "initialized") sendError(socket, -1, -32600, `Unexpected notification: ${method}`);
3401
- return;
3402
- }
3403
- try {
3404
- const result = await dispatchRequest(execServer, processes, fileReads, socket, {
3405
- ...request,
3406
- method
3407
- });
3408
- sendResult(socket, request.id, result);
3409
- } catch (error) {
3410
- sendError(socket, request.id, error instanceof JsonRpcProtocolError ? error.code : -32603, error instanceof Error ? error.message : String(error));
3411
- }
3412
- }
3413
- async function dispatchRequest(execServer, processes, fileReads, socket, request) {
3414
- switch (request.method) {
3415
- case "initialize": return { sessionId: randomUUID() };
3416
- case "environment/info": return {
3417
- shell: {
3418
- name: "sh",
3419
- path: "/bin/sh"
3420
- },
3421
- cwd: pathToFileURL(execServer.sandbox.containerWorkdir, { windows: false }).href,
3422
- capabilities: { networkProxyLaunch: false }
3423
- };
3424
- case "environment/status": return { status: "ready" };
3425
- case "process/start": return startProcess(execServer, processes, socket, request.params);
3426
- case "process/read": return await readProcess(processes, request.params);
3427
- case "process/write": return writeProcess(processes, request.params);
3428
- case "process/terminate": return terminateProcess(processes, request.params);
3429
- case "fs/open": return await openFile(execServer, fileReads, request.params);
3430
- case "fs/readBlock": return readFileBlock(fileReads, request.params);
3431
- case "fs/close": return closeFile(fileReads, request.params);
3432
- case "fs/readFile": return await readFile(execServer, request.params);
3433
- case "fs/writeFile":
3434
- await writeFile(execServer, request.params);
3435
- return {};
3436
- case "fs/createDirectory":
3437
- await createDirectory(execServer, request.params);
3438
- return {};
3439
- case "fs/getMetadata": return await getMetadata(execServer, request.params);
3440
- case "fs/readDirectory": return await readDirectory(execServer, request.params);
3441
- case "fs/remove":
3442
- await removePath(execServer, request.params);
3443
- return {};
3444
- case "fs/copy":
3445
- await copyPath(execServer, request.params);
3446
- return {};
3447
- case "http/request": return await httpRequest(execServer, socket, request.params);
3448
- default: throw new JsonRpcProtocolError(JSON_RPC_METHOD_NOT_FOUND, `Unsupported OpenClaw sandbox exec-server method: ${request.method}`);
3449
- }
3277
+ async function handleMessage(session, data) {
3278
+ const buffer = Array.isArray(data) ? Buffer.concat(data) : Buffer.isBuffer(data) ? data : Buffer.from(data);
3279
+ await session.handleRequest(parseRequest(buffer.toString("utf8")));
3450
3280
  }
3451
3281
  //#endregion
3452
3282
  //#region extensions/codex/src/app-server/provider-capabilities.ts
@@ -3495,87 +3325,6 @@ async function resolveCodexProviderWebSearchSupport(params) {
3495
3325
  }
3496
3326
  }
3497
3327
  //#endregion
3498
- //#region extensions/codex/src/app-server/plugin-approval-roundtrip.ts
3499
- const DEFAULT_CODEX_APPROVAL_TIMEOUT_MS = 12e4;
3500
- const MAX_PLUGIN_APPROVAL_TITLE_LENGTH = 80;
3501
- const MAX_PLUGIN_APPROVAL_DESCRIPTION_LENGTH = 256;
3502
- const ANSI_OSC_SEQUENCE_RE = new RegExp(String.raw`(?:\u001b]|\u009d)[^\u001b\u009c\u0007]*(?:\u0007|\u001b\\|\u009c)`, "g");
3503
- const ANSI_CONTROL_SEQUENCE_RE = new RegExp(String.raw`(?:\u001b\[[0-?]*[ -/]*[@-~]|\u009b[0-?]*[ -/]*[@-~]|\u001b[@-Z\\-_])`, "g");
3504
- const CONTROL_CHARACTER_RE = new RegExp(String.raw`[\u0000-\u001f\u007f-\u009f]+`, "g");
3505
- const INVISIBLE_FORMATTING_CONTROL_RE = new RegExp(String.raw`[\u00ad\u034f\u061c\u200b-\u200f\u202a-\u202e\u2060-\u206f\ufeff\ufe00-\ufe0f\u{e0100}-\u{e01ef}]`, "gu");
3506
- const DANGLING_TERMINAL_SEQUENCE_SUFFIX_RE = new RegExp(String.raw`(?:\u001b\][^\u001b\u009c\u0007]*|\u009d[^\u001b\u009c\u0007]*|\u001b\[[0-?]*[ -/]*|\u009b[0-?]*[ -/]*|\u001b)$`);
3507
- /** Starts a two-phase plugin approval request through the OpenClaw gateway. */
3508
- async function requestPluginApproval(params) {
3509
- const timeoutMs = DEFAULT_CODEX_APPROVAL_TIMEOUT_MS;
3510
- return params.hostCapabilities.requestApproval({
3511
- title: truncateCodexApprovalDisplayText(params.title, MAX_PLUGIN_APPROVAL_TITLE_LENGTH),
3512
- description: truncateCodexApprovalDisplayText(params.description, MAX_PLUGIN_APPROVAL_DESCRIPTION_LENGTH),
3513
- severity: params.severity,
3514
- toolName: params.toolName,
3515
- toolCallId: params.toolCallId,
3516
- timeoutMs,
3517
- transportTimeoutMs: resolveCodexGatewayTimeoutWithGraceMs(timeoutMs),
3518
- ...params.allowedDecisions ? { allowedDecisions: params.allowedDecisions } : {}
3519
- });
3520
- }
3521
- /** Detects the gateway's explicit null-decision marker for unavailable approvals. */
3522
- function approvalRequestExplicitlyUnavailable(result) {
3523
- if (result === null || result === void 0 || typeof result !== "object") return false;
3524
- let descriptor;
3525
- try {
3526
- descriptor = Object.getOwnPropertyDescriptor(result, "decision");
3527
- } catch {
3528
- return false;
3529
- }
3530
- return descriptor !== void 0 && "value" in descriptor && descriptor.value === null;
3531
- }
3532
- /** Waits for the gateway's final approval decision, respecting turn aborts. */
3533
- async function waitForPluginApprovalDecision(params) {
3534
- const timeoutMs = DEFAULT_CODEX_APPROVAL_TIMEOUT_MS;
3535
- const waitPromise = params.hostCapabilities.waitForApproval({
3536
- approvalId: params.approvalId,
3537
- timeoutMs,
3538
- transportTimeoutMs: resolveCodexGatewayTimeoutWithGraceMs(timeoutMs),
3539
- signal: params.signal
3540
- }).catch((error) => {
3541
- if (isApprovalNotFoundError(error)) return null;
3542
- throw error;
3543
- });
3544
- const bindDecision = (result) => result;
3545
- if (!params.signal) return bindDecision(await waitPromise);
3546
- let onAbort;
3547
- const abortPromise = new Promise((_, reject) => {
3548
- if (params.signal.aborted) {
3549
- reject(toErrorObject(params.signal.reason, "Non-Error rejection"));
3550
- return;
3551
- }
3552
- onAbort = () => reject(toErrorObject(params.signal.reason, "Non-Error rejection"));
3553
- params.signal.addEventListener("abort", onAbort, { once: true });
3554
- });
3555
- try {
3556
- return bindDecision(await Promise.race([waitPromise, abortPromise]));
3557
- } finally {
3558
- if (onAbort) params.signal.removeEventListener("abort", onAbort);
3559
- }
3560
- }
3561
- /** Converts a gateway exec approval decision into the app-server approval outcome enum. */
3562
- function mapExecDecisionToOutcome(decision) {
3563
- if (decision === "allow-once") return "approved-once";
3564
- if (decision === "allow-always") return "approved-session";
3565
- if (decision === null || decision === void 0) return "unavailable";
3566
- return "denied";
3567
- }
3568
- function truncateCodexApprovalDisplayText(value, maxLength) {
3569
- return value.length <= maxLength ? value : `${truncateUtf16Safe(value, maxLength - 3)}...`;
3570
- }
3571
- function stripDanglingCodexApprovalTerminalSequence(value) {
3572
- return value.replace(DANGLING_TERMINAL_SEQUENCE_SUFFIX_RE, "");
3573
- }
3574
- function sanitizeCodexApprovalVisibleText(value, options = {}) {
3575
- const terminalSafe = value.replace(ANSI_OSC_SEQUENCE_RE, "").replace(ANSI_CONTROL_SEQUENCE_RE, "");
3576
- return (options.stripDanglingTerminalSequence ? stripDanglingCodexApprovalTerminalSequence(terminalSafe) : terminalSafe).replace(INVISIBLE_FORMATTING_CONTROL_RE, " ").replace(CONTROL_CHARACTER_RE, " ").replace(/\s+/g, " ").trim();
3577
- }
3578
- //#endregion
3579
3328
  //#region extensions/codex/src/app-server/approval-bridge.ts
3580
3329
  /**
3581
3330
  * Bridges Codex app-server approval requests into OpenClaw policy hooks and
@@ -3600,19 +3349,55 @@ async function handleCodexAppServerApprovalRequest(params) {
3600
3349
  requestParams,
3601
3350
  paramsForRun: params.paramsForRun
3602
3351
  });
3603
- const resolvePolicyApproval = (outcome, message = approvalResolutionMessage(outcome)) => {
3352
+ if (params.signal?.aborted) {
3353
+ recordNativeToolFailureDisposition(params, context, "cancelled");
3354
+ return buildApprovalResponse(params.method, context.requestParams, "cancelled");
3355
+ }
3356
+ let revalidateMutableFileApproval;
3357
+ let mutableFileApprovalRequiresOneShot = false;
3358
+ const resolvePolicyApproval = async (outcome, message = approvalResolutionMessage(outcome), approvalId) => {
3359
+ let resolvedOutcome = outcome;
3360
+ let resolvedMessage = message;
3361
+ if (outcome !== "denied" && revalidateMutableFileApproval) {
3362
+ const binding = await revalidateMutableFileApproval();
3363
+ if (!binding.ok) {
3364
+ resolvedOutcome = "denied";
3365
+ resolvedMessage = binding.message;
3366
+ }
3367
+ }
3368
+ if (resolvedOutcome === "approved-session" && mutableFileApprovalRequiresOneShot) {
3369
+ resolvedOutcome = "approved-once";
3370
+ resolvedMessage = "Codex app-server approval granted for this byte-bound command only.";
3371
+ }
3604
3372
  emitApprovalEvent(params.paramsForRun, {
3605
3373
  phase: "resolved",
3606
3374
  kind: context.kind,
3607
- status: outcome === "denied" ? "denied" : "approved",
3375
+ status: resolvedOutcome === "denied" ? "denied" : "approved",
3608
3376
  title: context.title,
3377
+ ...approvalId ? {
3378
+ approvalId,
3379
+ approvalSlug: approvalId
3380
+ } : {},
3609
3381
  ...context.eventDetails,
3610
- ...approvalEventScope(params.method, outcome),
3611
- message
3382
+ ...approvalEventScope(params.method, resolvedOutcome),
3383
+ message: resolvedMessage
3612
3384
  });
3613
- return buildApprovalResponse(params.method, context.requestParams, outcome);
3385
+ return buildApprovalResponse(params.method, context.requestParams, resolvedOutcome);
3614
3386
  };
3615
3387
  try {
3388
+ if (params.method === "item/commandExecution/requestApproval" && !readNetworkApprovalContext(requestParams)) {
3389
+ const command = readPolicyCommand(requestParams);
3390
+ const cwd = readStringField(requestParams, "cwd") ?? params.paramsForRun.workspaceDir;
3391
+ const prepareMutableFileApproval = params.paramsForRun.hostCapabilities.prepareMutableFileApproval;
3392
+ if (!prepareMutableFileApproval) return await resolvePolicyApproval("denied", "SYSTEM_RUN_DENIED: mutable file approval binding is unavailable");
3393
+ const prepared = await prepareMutableFileApproval({
3394
+ command: command ?? "",
3395
+ cwd
3396
+ });
3397
+ if (!prepared.ok) return await resolvePolicyApproval("denied", prepared.message);
3398
+ mutableFileApprovalRequiresOneShot = prepared.requiresOneShot;
3399
+ revalidateMutableFileApproval = prepared.revalidate;
3400
+ }
3616
3401
  const policyOutcome = await runOpenClawToolPolicyForApprovalRequest({
3617
3402
  method: params.method,
3618
3403
  requestParams,
@@ -3624,19 +3409,22 @@ async function handleCodexAppServerApprovalRequest(params) {
3624
3409
  });
3625
3410
  if (policyOutcome?.outcome === "denied") {
3626
3411
  recordNativeToolFailureDisposition(params, context, policyOutcome.failureDisposition);
3627
- return resolvePolicyApproval("denied", policyOutcome.reason);
3412
+ return await resolvePolicyApproval("denied", policyOutcome.reason);
3628
3413
  }
3629
- if (policyOutcome?.outcome === "approved-once" || policyOutcome?.outcome === "approved-session") return resolvePolicyApproval(policyOutcome.outcome);
3630
- const canAutoApproveConcreteToolCall = CONCRETE_TOOL_AUTO_APPROVAL_METHODS.has(params.method);
3631
- if (canAutoApproveConcreteToolCall && params.autoApproveOpenClawToolPolicy === true && policyOutcome?.outcome === "allowed") return resolvePolicyApproval("approved-once", "Codex app-server approval accepted by OpenClaw tool policy.");
3632
- if (canAutoApproveConcreteToolCall && params.autoApprove === true) return resolvePolicyApproval("approved-session", "Codex app-server approval auto-approved by runtime policy.");
3414
+ if (policyOutcome?.outcome === "approved-once" || policyOutcome?.outcome === "approved-session") return await resolvePolicyApproval(policyOutcome.outcome);
3415
+ if (CONCRETE_TOOL_AUTO_APPROVAL_METHODS.has(params.method) && !readNetworkApprovalContext(requestParams) && params.autoApprove === true) return await resolvePolicyApproval("approved-session", "Codex app-server approval auto-approved by runtime policy.");
3633
3416
  const requestResult = await requestPluginApproval({
3634
3417
  hostCapabilities: params.paramsForRun.hostCapabilities,
3635
3418
  title: context.title,
3636
3419
  description: context.description,
3637
3420
  severity: context.severity,
3638
3421
  toolName: context.toolName,
3639
- toolCallId: context.approvalId
3422
+ toolCallId: context.approvalId,
3423
+ allowedDecisions: nativeApprovalAllowedDecisions({
3424
+ method: params.method,
3425
+ requestParams,
3426
+ requiresOneShot: mutableFileApprovalRequiresOneShot
3427
+ })
3640
3428
  });
3641
3429
  const approvalId = requestResult?.id;
3642
3430
  if (!approvalId) {
@@ -3662,16 +3450,17 @@ async function handleCodexAppServerApprovalRequest(params) {
3662
3450
  ...context.eventDetails,
3663
3451
  message: "Codex app-server approval requested."
3664
3452
  });
3665
- const requestUnavailable = approvalRequestExplicitlyUnavailable(requestResult);
3666
- const decision = requestUnavailable ? null : await waitForPluginApprovalDecision({
3453
+ const approvalResult = approvalRequestExplicitlyUnavailable(requestResult) ? void 0 : await waitForPluginApprovalDecision({
3667
3454
  approvalId,
3668
3455
  signal: params.signal,
3669
3456
  hostCapabilities: params.paramsForRun.hostCapabilities
3670
3457
  });
3671
- const approvalExpired = !requestUnavailable && decision === null;
3672
- const outcome = params.signal?.aborted ? "cancelled" : mapExecDecisionToOutcome(decision);
3673
- if (outcome === "cancelled") recordNativeToolFailureDisposition(params, context, params.signal?.aborted ? resolveCodexToolAbortTerminalReason(params.signal) : "cancelled");
3674
- else if (outcome === "unavailable") recordNativeToolFailureDisposition(params, context, approvalExpired ? "timed_out" : "failed");
3458
+ const approvalTimedOut = !params.signal?.aborted && approvalResult?.terminalReason === "timeout";
3459
+ const outcome = params.signal?.aborted ? "cancelled" : mapExecDecisionToOutcome(approvalResult?.decision);
3460
+ if (approvalTimedOut) recordNativeToolFailureDisposition(params, context, "timed_out", context.approvalKind);
3461
+ else if (outcome === "cancelled") recordNativeToolFailureDisposition(params, context, params.signal?.aborted ? resolveCodexToolAbortTerminalReason(params.signal) : "cancelled");
3462
+ else if (outcome === "unavailable") recordNativeToolFailureDisposition(params, context, "failed");
3463
+ if (outcome === "approved-once" || outcome === "approved-session") return await resolvePolicyApproval(outcome, approvalResolutionMessage(outcome), approvalId);
3675
3464
  emitApprovalEvent(params.paramsForRun, {
3676
3465
  phase: "resolved",
3677
3466
  kind: context.kind,
@@ -3681,7 +3470,7 @@ async function handleCodexAppServerApprovalRequest(params) {
3681
3470
  approvalSlug: approvalId,
3682
3471
  ...context.eventDetails,
3683
3472
  ...approvalEventScope(params.method, outcome),
3684
- message: approvalResolutionMessage(outcome)
3473
+ message: approvalTimedOut ? codexApprovalTimeoutText(context.approvalKind) : approvalResolutionMessage(outcome)
3685
3474
  });
3686
3475
  return buildApprovalResponse(params.method, context.requestParams, outcome);
3687
3476
  } catch (error) {
@@ -3699,10 +3488,11 @@ async function handleCodexAppServerApprovalRequest(params) {
3699
3488
  return buildApprovalResponse(params.method, context.requestParams, cancelled ? "cancelled" : "denied");
3700
3489
  }
3701
3490
  }
3702
- function recordNativeToolFailureDisposition(params, context, disposition) {
3491
+ function recordNativeToolFailureDisposition(params, context, disposition, approvalKind) {
3703
3492
  if (!context.itemId || !disposition) return;
3704
3493
  try {
3705
- params.onNativeToolFailureDisposition?.(context.itemId, params.signal?.aborted ? resolveCodexToolAbortTerminalReason(params.signal) : disposition);
3494
+ const resolvedDisposition = params.signal?.aborted ? resolveCodexToolAbortTerminalReason(params.signal) : disposition;
3495
+ params.onNativeToolFailureDisposition?.(context.itemId, resolvedDisposition, ...resolvedDisposition === "timed_out" && approvalKind ? [approvalKind] : []);
3706
3496
  } catch {}
3707
3497
  }
3708
3498
  /** Converts an OpenClaw approval outcome into the app-server method response. */
@@ -3719,7 +3509,10 @@ function buildApprovalResponse(method, requestParams, outcome) {
3719
3509
  scope: "turn"
3720
3510
  };
3721
3511
  }
3722
- return unsupportedApprovalResponse();
3512
+ return {
3513
+ decision: "decline",
3514
+ reason: "OpenClaw codex app-server bridge does not grant native approvals yet."
3515
+ };
3723
3516
  }
3724
3517
  function matchesCurrentTurn(requestParams, threadId, turnId) {
3725
3518
  if (!requestParams) return false;
@@ -3735,20 +3528,19 @@ function buildApprovalContext(params) {
3735
3528
  const reasonPreview = sanitizeApprovalPreview(readStringPreview(params.requestParams, "reason"), 180);
3736
3529
  const command = commandPreview.text;
3737
3530
  const reason = reasonPreview.text;
3738
- const kind = approvalKindForMethod(params.method);
3531
+ const networkApproval = params.method === "item/commandExecution/requestApproval" ? readNetworkApprovalContext(params.requestParams) : void 0;
3532
+ const approvalKind = params.method.includes("commandExecution") ? "command" : params.method.includes("fileChange") ? "file-change" : params.method.includes("permissions") ? "permissions" : "other";
3533
+ const kind = approvalKind === "command" ? "exec" : approvalKind === "other" ? "unknown" : "plugin";
3739
3534
  const permissionLines = params.method === "item/permissions/requestApproval" ? describeRequestedPermissions(params.requestParams) : [];
3740
- const title = kind === "exec" ? "Codex app-server command approval" : params.method === "item/permissions/requestApproval" ? "Codex app-server permission approval" : kind === "plugin" ? "Codex app-server file approval" : "Codex app-server approval";
3741
- const subject = permissionLines[0] ?? (command ? `Command: ${formatApprovalPreviewSubject(command, commandPreview.omitted)}` : commandPreview.omitted ? `Command: ${APPROVAL_PREVIEW_OMITTED}` : reason ? `Reason: ${formatApprovalPreviewSubject(reason, reasonPreview.omitted)}` : reasonPreview.omitted ? `Reason: ${APPROVAL_PREVIEW_OMITTED}` : `Request method: ${params.method}`);
3535
+ const title = networkApproval ? "Codex app-server network approval" : kind === "exec" ? "Codex app-server command approval" : params.method === "item/permissions/requestApproval" ? "Codex app-server permission approval" : kind === "plugin" ? "Codex app-server file approval" : "Codex app-server approval";
3536
+ const subject = (networkApproval ? `Network: ${sanitizePermissionScalar(networkApproval.protocol)}://${sanitizePermissionHostValue(networkApproval.host)}` : void 0) ?? permissionLines[0] ?? (command ? `Command: ${formatApprovalPreviewSubject(command, commandPreview.omitted)}` : commandPreview.omitted ? `Command: ${APPROVAL_PREVIEW_OMITTED}` : reason ? `Reason: ${formatApprovalPreviewSubject(reason, reasonPreview.omitted)}` : reasonPreview.omitted ? `Reason: ${APPROVAL_PREVIEW_OMITTED}` : `Request method: ${params.method}`);
3742
3537
  return {
3538
+ approvalKind,
3743
3539
  kind,
3744
3540
  title,
3745
- description: permissionLines.length > 0 ? joinDescriptionLinesWithinLimit(permissionLines, PERMISSION_DESCRIPTION_MAX_LENGTH) : [
3746
- subject,
3747
- ...commandDetailLines,
3748
- params.paramsForRun.sessionKey && `Session: ${params.paramsForRun.sessionKey}`
3749
- ].filter(Boolean).join("\n"),
3541
+ description: permissionLines.length > 0 ? joinDescriptionLinesWithinLimit(permissionLines, PERMISSION_DESCRIPTION_MAX_LENGTH) : [subject, ...commandDetailLines].join("\n"),
3750
3542
  severity: kind === "exec" ? "warning" : "info",
3751
- toolName: kind === "exec" ? "codex_command_approval" : params.method === "item/permissions/requestApproval" ? "codex_permission_approval" : "codex_file_approval",
3543
+ toolName: networkApproval ? "codex_network_approval" : kind === "exec" ? "codex_command_approval" : params.method === "item/permissions/requestApproval" ? "codex_permission_approval" : "codex_file_approval",
3752
3544
  itemId,
3753
3545
  approvalId,
3754
3546
  requestParams: params.requestParams,
@@ -3920,6 +3712,10 @@ function sanitizeRelayDecisionReason(value) {
3920
3712
  }
3921
3713
  function buildOpenClawToolPolicyRequest(method, requestParams) {
3922
3714
  if (method === "item/commandExecution/requestApproval") {
3715
+ if (readNetworkApprovalContext(requestParams)) return {
3716
+ toolName: "codex_network_approval",
3717
+ params: { approval: requestParams ?? {} }
3718
+ };
3923
3719
  const command = readPolicyCommand(requestParams);
3924
3720
  return {
3925
3721
  toolName: "exec",
@@ -3960,14 +3756,27 @@ function stableJsonText(value) {
3960
3756
  }
3961
3757
  }
3962
3758
  function commandApprovalDecision(requestParams, outcome) {
3963
- if (outcome === "cancelled") return commandRejectionDecision(requestParams, "cancel");
3964
- if (outcome === "denied" || outcome === "unavailable") return commandRejectionDecision(requestParams, "decline");
3965
- if (outcome === "approved-session") {
3966
- if (hasAvailableDecision(requestParams, "acceptForSession")) return "acceptForSession";
3967
- const amendmentDecision = findAvailableCommandAmendmentDecision(requestParams);
3968
- if (amendmentDecision) return amendmentDecision;
3969
- }
3970
- return hasAvailableDecision(requestParams, "accept") ? "accept" : commandRejectionDecision(requestParams, "decline");
3759
+ if (outcome === "cancelled") return "cancel";
3760
+ if (outcome === "denied" || outcome === "unavailable") return "decline";
3761
+ const capabilities = commandApprovalCapabilities(requestParams);
3762
+ if (outcome === "approved-session" && capabilities.sessionDecision !== void 0) return capabilities.sessionDecision;
3763
+ return capabilities.once ? "accept" : "decline";
3764
+ }
3765
+ function nativeApprovalAllowedDecisions(params) {
3766
+ if (params.method === "item/fileChange/requestApproval") return [
3767
+ "allow-once",
3768
+ "allow-always",
3769
+ "deny"
3770
+ ];
3771
+ if (params.method !== "item/commandExecution/requestApproval") return;
3772
+ const available = params.requestParams?.availableDecisions;
3773
+ if (!Array.isArray(available)) return;
3774
+ const capabilities = commandApprovalCapabilities(params.requestParams);
3775
+ const decisions = [];
3776
+ if (capabilities.once) decisions.push("allow-once");
3777
+ if (!params.requiresOneShot && capabilities.sessionDecision !== void 0) decisions.push("allow-always");
3778
+ decisions.push("deny");
3779
+ return decisions;
3971
3780
  }
3972
3781
  function fileChangeApprovalDecision(outcome) {
3973
3782
  if (outcome === "cancelled") return "cancel";
@@ -3981,12 +3790,6 @@ function requestedPermissions(requestParams) {
3981
3790
  if (isJsonObject(permissions.fileSystem)) granted.fileSystem = permissions.fileSystem;
3982
3791
  return granted;
3983
3792
  }
3984
- function unsupportedApprovalResponse() {
3985
- return {
3986
- decision: "decline",
3987
- reason: "OpenClaw codex app-server bridge does not grant native approvals yet."
3988
- };
3989
- }
3990
3793
  function describeRequestedPermissions(requestParams) {
3991
3794
  return describePermissionProfile(requestedPermissions(requestParams), "Permissions");
3992
3795
  }
@@ -4154,43 +3957,38 @@ function isPrivateNetworkHostPattern(value) {
4154
3957
  if (wildcardStripped.startsWith("10.") || wildcardStripped.startsWith("127.") || wildcardStripped.startsWith("192.168.") || wildcardStripped.startsWith("169.254.")) return true;
4155
3958
  return /^172\.(1[6-9]|2\d|3[0-1])\./.test(wildcardStripped);
4156
3959
  }
4157
- function hasAvailableDecision(requestParams, decision) {
3960
+ function commandApprovalCapabilities(requestParams) {
4158
3961
  const available = requestParams?.availableDecisions;
4159
- if (!Array.isArray(available)) return true;
4160
- return available.includes(decision);
3962
+ if (!Array.isArray(available)) return {
3963
+ once: true,
3964
+ sessionDecision: "acceptForSession"
3965
+ };
3966
+ return {
3967
+ once: available.includes("accept"),
3968
+ ...available.includes("acceptForSession") ? { sessionDecision: "acceptForSession" } : { sessionDecision: findAvailableCommandAmendmentDecision(requestParams) }
3969
+ };
4161
3970
  }
4162
3971
  function findAvailableCommandAmendmentDecision(requestParams) {
4163
3972
  const available = requestParams?.availableDecisions;
4164
3973
  if (!Array.isArray(available)) return;
4165
3974
  return available.find((entry) => isJsonObject(entry) && (isJsonObject(entry.acceptWithExecpolicyAmendment) || isJsonObject(entry.applyNetworkPolicyAmendment)));
4166
3975
  }
4167
- function commandRejectionDecision(requestParams, preferred) {
4168
- const available = requestParams?.availableDecisions;
4169
- if (!Array.isArray(available)) return preferred;
4170
- if (available.includes(preferred)) return preferred;
4171
- const alternate = preferred === "decline" ? "cancel" : "decline";
4172
- if (available.includes(alternate)) return alternate;
4173
- return preferred;
4174
- }
4175
3976
  function approvalResolutionMessage(outcome) {
4176
- if (outcome === "approved-session") return "Codex app-server approval granted for the session.";
4177
- if (outcome === "approved-once") return "Codex app-server approval granted for this turn.";
4178
- if (outcome === "cancelled") return "Codex app-server approval cancelled.";
4179
- if (outcome === "unavailable") return "Codex app-server approval unavailable.";
4180
- return "Codex app-server approval denied.";
3977
+ return {
3978
+ "approved-session": "Codex app-server approval granted for the session.",
3979
+ "approved-once": "Codex app-server approval granted for this turn.",
3980
+ cancelled: "Codex app-server approval cancelled.",
3981
+ unavailable: "Codex app-server approval unavailable.",
3982
+ denied: "Codex app-server approval denied."
3983
+ }[outcome];
4181
3984
  }
4182
3985
  function approvalEventScope(method, outcome) {
4183
3986
  return method === "item/permissions/requestApproval" ? { scope: outcome === "approved-session" ? "session" : "turn" } : {};
4184
3987
  }
4185
- function approvalKindForMethod(method) {
4186
- if (method.includes("commandExecution") || method.includes("execCommand")) return "exec";
4187
- if (method.includes("fileChange") || method.includes("Patch") || method.includes("permissions")) return "plugin";
4188
- return "unknown";
4189
- }
4190
3988
  function emitApprovalEvent(params, data) {
4191
3989
  params.onAgentEvent?.({
4192
3990
  stream: "approval",
4193
- data
3991
+ data: { ...data }
4194
3992
  });
4195
3993
  }
4196
3994
  function readDisplayCommandPreview(record) {
@@ -4205,6 +4003,15 @@ function readPolicyCommand(record) {
4205
4003
  const actionCommands = readCommandActions(record);
4206
4004
  if (actionCommands.length > 0) return actionCommands.join(" && ");
4207
4005
  }
4006
+ function readNetworkApprovalContext(record) {
4007
+ const context = isJsonObject(record?.networkApprovalContext) ? record.networkApprovalContext : void 0;
4008
+ const host = readStringField(context, "host");
4009
+ const protocol = readStringField(context, "protocol");
4010
+ return host && protocol ? {
4011
+ host,
4012
+ protocol
4013
+ } : void 0;
4014
+ }
4208
4015
  function readCommandActions(record) {
4209
4016
  const actions = record?.commandActions;
4210
4017
  if (!Array.isArray(actions)) return [];
@@ -4389,11 +4196,12 @@ const MAX_DISPLAY_VALUE_ARRAY_ITEMS = 8;
4389
4196
  const MAX_DISPLAY_VALUE_OBJECT_KEYS = 8;
4390
4197
  const MAX_DISPLAY_VALUE_DEPTH = 3;
4391
4198
  const DISPLAY_TEXT_SCAN_MAX_LENGTH = 4096;
4392
- async function handleCodexAppServerElicitationRequest(params) {
4199
+ async function routeCodexAppServerElicitationRequest(params) {
4393
4200
  const requestParams = isJsonObject(params.requestParams) ? params.requestParams : void 0;
4394
- if (!requestParams || readNonBlankStringField(requestParams, "threadId") !== params.threadId) return;
4201
+ if (!requestParams || readNonBlankStringField(requestParams, "threadId") !== params.threadId) return { kind: "not-mine" };
4395
4202
  const requestTurnId = requestParams.turnId;
4396
- if (requestTurnId !== null && requestTurnId !== void 0 && requestTurnId !== params.turnId) return;
4203
+ if (requestTurnId !== null && requestTurnId !== void 0 && requestTurnId !== params.turnId) return { kind: "not-mine" };
4204
+ if (!((isJsonObject(requestParams["_meta"]) ? requestParams["_meta"] : void 0)?.[MCP_TOOL_APPROVAL_KIND_KEY] === MCP_TOOL_APPROVAL_KIND || params.computerUseMcpServerName !== void 0 && readNonBlankStringField(requestParams, "serverName") === params.computerUseMcpServerName)) return { kind: "not-mine" };
4397
4205
  const pluginResolution = resolvePluginElicitation({
4398
4206
  requestParams,
4399
4207
  pluginAppPolicyContext: params.pluginAppPolicyContext
@@ -4401,32 +4209,38 @@ async function handleCodexAppServerElicitationRequest(params) {
4401
4209
  if (pluginResolution.kind !== "not_plugin") {
4402
4210
  if (params.paramsForRun.trigger === "cron" && params.paramsForRun.scheduledRuntimeAuthority) {
4403
4211
  logPluginElicitationDecline("scheduled_authority_non_interactive", requestParams);
4404
- return declineElicitationResponse();
4212
+ return handled(createCodexElicitationResponse("decline"));
4405
4213
  }
4406
4214
  if (pluginResolution.kind === "decline") {
4407
4215
  logPluginElicitationDecline(pluginResolution.reason, requestParams);
4408
- return declineElicitationResponse();
4216
+ return handled(createCodexElicitationResponse("decline"));
4409
4217
  }
4410
4218
  if (requestTurnId !== params.turnId) {
4411
4219
  logPluginElicitationDecline("missing_active_turn", requestParams);
4412
- return declineElicitationResponse();
4220
+ return handled(createCodexElicitationResponse("decline"));
4413
4221
  }
4414
- return await buildPluginPolicyElicitationResponse({
4222
+ return handled(await buildPluginPolicyElicitationResponse({
4415
4223
  entry: pluginResolution.entry,
4416
4224
  requestParams,
4417
4225
  paramsForRun: params.paramsForRun,
4418
4226
  signal: params.signal
4419
- });
4227
+ }));
4420
4228
  }
4421
4229
  const approvalPrompt = readComputerUseApprovalElicitation(requestParams, params.computerUseMcpServerName) ?? readBridgeableApprovalElicitation(requestParams);
4422
- if (!approvalPrompt) return;
4423
- return buildElicitationResponse(approvalPrompt, await requestPluginApprovalOutcome({
4230
+ if (!approvalPrompt) return handled(createCodexElicitationResponse("decline"));
4231
+ return handled(buildElicitationResponse(approvalPrompt, await requestPluginApprovalOutcome({
4424
4232
  paramsForRun: params.paramsForRun,
4425
4233
  title: approvalPrompt.title,
4426
4234
  description: approvalPrompt.description,
4427
4235
  allowedDecisions: approvalPrompt.allowedDecisions,
4428
4236
  signal: params.signal
4429
- }));
4237
+ })));
4238
+ }
4239
+ function handled(response) {
4240
+ return {
4241
+ kind: "handled",
4242
+ response
4243
+ };
4430
4244
  }
4431
4245
  function resolvePluginElicitation(params) {
4432
4246
  const requestParams = params.requestParams;
@@ -4527,26 +4341,27 @@ async function buildPluginPolicyElicitationResponse(params) {
4527
4341
  const mode = resolvePluginDestructiveApprovalMode(params.entry);
4528
4342
  if (mode === "deny") {
4529
4343
  logPluginElicitationDecline("destructive_actions_disabled", params.requestParams);
4530
- return declineElicitationResponse();
4344
+ return createCodexElicitationResponse("decline");
4531
4345
  }
4532
4346
  const approvalPrompt = readPluginApprovalElicitation(params.entry, params.requestParams);
4533
4347
  if (!approvalPrompt) {
4534
4348
  logPluginElicitationDecline("unsupported_schema", params.requestParams);
4535
- return declineElicitationResponse();
4349
+ return createCodexElicitationResponse("decline");
4536
4350
  }
4537
4351
  const response = buildElicitationResponse(approvalPrompt, "approved-once");
4538
- if (isJsonObject(response) && response.action === "accept") {
4352
+ if (response.action === "accept") {
4539
4353
  if (mode === "allow") return response;
4540
- return buildElicitationResponse(approvalPrompt, oneShotPluginPolicyApprovalOutcome(mode, await requestPluginApprovalOutcome({
4354
+ const outcome = await requestPluginApprovalOutcome({
4541
4355
  paramsForRun: params.paramsForRun,
4542
4356
  title: approvalPrompt.title,
4543
4357
  description: approvalPrompt.description,
4544
4358
  allowedDecisions: allowedPluginPolicyApprovalDecisions(mode, approvalPrompt),
4545
4359
  signal: params.signal
4546
- })));
4360
+ });
4361
+ return buildElicitationResponse(approvalPrompt, mode === "ask" && outcome === "approved-session" ? "approved-once" : outcome);
4547
4362
  }
4548
4363
  logPluginElicitationDecline("unmappable_schema", params.requestParams);
4549
- return declineElicitationResponse();
4364
+ return createCodexElicitationResponse("decline");
4550
4365
  }
4551
4366
  function resolvePluginDestructiveApprovalMode(entry) {
4552
4367
  return entry.destructiveApprovalMode ?? (entry.allowDestructiveActions ? "allow" : "deny");
@@ -4556,9 +4371,6 @@ function allowedPluginPolicyApprovalDecisions(mode, approvalPrompt) {
4556
4371
  if (mode !== "ask") return allowedDecisions;
4557
4372
  return allowedDecisions.filter((decision) => decision !== "allow-always");
4558
4373
  }
4559
- function oneShotPluginPolicyApprovalOutcome(mode, outcome) {
4560
- return mode === "ask" && outcome === "approved-session" ? "approved-once" : outcome;
4561
- }
4562
4374
  function readPluginApprovalElicitation(entry, requestParams) {
4563
4375
  if (readNonBlankStringField(requestParams, "mode") !== "form" || !isJsonObject(requestParams.requestedSchema)) return;
4564
4376
  const requestedSchema = requestParams.requestedSchema;
@@ -4602,13 +4414,6 @@ function canMapPersistentApproval(requestedSchema, meta) {
4602
4414
  }) && chooseAlwaysPersistOptionValue(readEnumOptions(schema)) !== void 0;
4603
4415
  });
4604
4416
  }
4605
- function declineElicitationResponse() {
4606
- return {
4607
- action: "decline",
4608
- content: null,
4609
- _meta: null
4610
- };
4611
- }
4612
4417
  function logPluginElicitationDecline(reason, requestParams) {
4613
4418
  embeddedAgentLog.debug("codex plugin elicitation declined", {
4614
4419
  reason,
@@ -4751,27 +4556,23 @@ async function requestPluginApprovalOutcome(params) {
4751
4556
  });
4752
4557
  const approvalId = requestResult?.id;
4753
4558
  if (!approvalId) return "unavailable";
4754
- return mapExecDecisionToOutcome(approvalRequestExplicitlyUnavailable(requestResult) ? null : await waitForPluginApprovalDecision({
4559
+ const approvalResult = approvalRequestExplicitlyUnavailable(requestResult) ? void 0 : await waitForPluginApprovalDecision({
4755
4560
  hostCapabilities: params.paramsForRun.hostCapabilities,
4756
4561
  approvalId,
4757
4562
  signal: params.signal
4758
- }));
4563
+ });
4564
+ if (params.signal?.aborted) return "cancelled";
4565
+ if (approvalResult?.terminalReason === "timeout") return "timed-out";
4566
+ return mapExecDecisionToOutcome(approvalResult?.decision);
4759
4567
  } catch {
4760
4568
  return params.signal?.aborted ? "cancelled" : "denied";
4761
4569
  }
4762
4570
  }
4763
4571
  function buildElicitationResponse(approvalPrompt, outcome) {
4764
4572
  const { requestedSchema, meta } = approvalPrompt;
4765
- if (outcome === "cancelled") return {
4766
- action: "cancel",
4767
- content: null,
4768
- _meta: null
4769
- };
4770
- if (outcome === "denied" || outcome === "unavailable") return {
4771
- action: "decline",
4772
- content: null,
4773
- _meta: null
4774
- };
4573
+ if (outcome === "cancelled") return createCodexElicitationResponse("cancel");
4574
+ if (outcome === "timed-out") return createCodexElicitationResponse("decline", null, { message: codexApprovalTimeoutText("other") });
4575
+ if (outcome === "denied" || outcome === "unavailable") return createCodexElicitationResponse("decline");
4775
4576
  const content = buildAcceptedContent(approvalPrompt, outcome);
4776
4577
  if (!content && !hasNoSchemaProperties(requestedSchema)) {
4777
4578
  embeddedAgentLog.warn("codex MCP approval elicitation approved without a mappable response", {
@@ -4779,13 +4580,9 @@ function buildElicitationResponse(approvalPrompt, outcome) {
4779
4580
  fields: Object.keys(requestedSchema.properties ?? {}),
4780
4581
  outcome
4781
4582
  });
4782
- return declineElicitationResponse();
4583
+ return createCodexElicitationResponse("decline");
4783
4584
  }
4784
- return {
4785
- action: "accept",
4786
- content: content ?? null,
4787
- _meta: buildAcceptedMeta(meta, outcome, approvalPrompt.persistHintsMode ?? "legacy")
4788
- };
4585
+ return createCodexElicitationResponse("accept", content ?? null, buildAcceptedMeta(meta, outcome, approvalPrompt.persistHintsMode ?? "legacy"));
4789
4586
  }
4790
4587
  function buildAcceptedContent(approvalPrompt, outcome) {
4791
4588
  const { requestedSchema, meta } = approvalPrompt;
@@ -4920,6 +4717,36 @@ function readFirstString$1(record, keys) {
4920
4717
  * Bridges OpenClaw runtime tools into Codex app-server dynamic tool specs and
4921
4718
  * tool-call responses.
4922
4719
  */
4720
+ const INTERNAL_TOOL_EXECUTION_VALIDATION = Symbol.for("openclaw.internalToolExecutionValidation");
4721
+ const MAX_CODEX_DYNAMIC_TOOL_VALIDATION_ERRORS = 4;
4722
+ const MAX_CODEX_DYNAMIC_TOOL_VALIDATION_ERROR_CHARS = 160;
4723
+ const CODEX_DYNAMIC_TOOL_VALIDATION_TRUNCATED_SUFFIX = " [detail truncated]";
4724
+ function shouldValidateCodexDynamicToolInput(tool) {
4725
+ return getPluginToolMeta(tool)?.mcp?.operation !== "tool";
4726
+ }
4727
+ function assertCodexDynamicToolInputMatchesSchema(params) {
4728
+ const validation = validateJsonSchemaValue({
4729
+ schema: params.schema,
4730
+ cacheKey: `codex-dynamic-tool-input:${params.toolName}:${JSON.stringify(params.schema)}`,
4731
+ value: params.value
4732
+ });
4733
+ if (validation.ok) return;
4734
+ const visibleErrors = validation.errors.slice(0, MAX_CODEX_DYNAMIC_TOOL_VALIDATION_ERRORS);
4735
+ const details = visibleErrors.map((error) => {
4736
+ if (error.text.length <= MAX_CODEX_DYNAMIC_TOOL_VALIDATION_ERROR_CHARS) return error.text;
4737
+ return `${error.text.slice(0, MAX_CODEX_DYNAMIC_TOOL_VALIDATION_ERROR_CHARS - 19)}${CODEX_DYNAMIC_TOOL_VALIDATION_TRUNCATED_SUFFIX}`;
4738
+ }).join("; ");
4739
+ const omitted = validation.errors.length - visibleErrors.length;
4740
+ const omittedSuffix = omitted > 0 ? `; ${omitted} more violation(s) omitted` : "";
4741
+ throw new Error(`Invalid arguments for tool "${params.toolName}": ${details}${omittedSuffix}.`);
4742
+ }
4743
+ function createCodexDynamicToolValidationControl(params) {
4744
+ return {
4745
+ [INTERNAL_TOOL_EXECUTION_VALIDATION]: true,
4746
+ toolCallId: params.toolCallId,
4747
+ validate: params.validate
4748
+ };
4749
+ }
4923
4750
  function applyCurrentMessageProvider(toolName, args, currentProvider) {
4924
4751
  const hasProvider = typeof args.provider === "string" && args.provider.trim().length > 0 ? true : typeof args.channel === "string" && args.channel.trim().length > 0;
4925
4752
  const provider = currentProvider?.trim();
@@ -5050,6 +4877,8 @@ function normalizeAcceptedSessionSpawn(result) {
5050
4877
  }
5051
4878
  /** Namespace attached to OpenClaw-owned dynamic tools exposed to Codex. */
5052
4879
  const CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE = "openclaw";
4880
+ const CODEX_DYNAMIC_TOOL_NAME_MAX_CHARS = 128;
4881
+ const CODEX_DYNAMIC_TOOL_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/u;
5053
4882
  const ALWAYS_DIRECT_DYNAMIC_TOOL_NAMES = /* @__PURE__ */ new Set([
5054
4883
  "agents_list",
5055
4884
  "sessions_spawn",
@@ -5100,7 +4929,11 @@ function createCodexDynamicToolBridge(params) {
5100
4929
  const toolMap = new Map(availableTools.map((entry) => [entry.name, entry]));
5101
4930
  const registeredToolNames = new Set(registeredSpecTools.map((entry) => entry.name));
5102
4931
  const quarantinedTools = dedupeQuarantinedDynamicTools([...availableProjection.quarantinedTools, ...registeredProjection.quarantinedTools]);
5103
- warnQuarantinedDynamicTools(quarantinedTools);
4932
+ warnQuarantinedDynamicTools({
4933
+ tools: quarantinedTools,
4934
+ availableToolCount: availableTools.length,
4935
+ registeredToolCount: registeredSpecTools.length
4936
+ });
5104
4937
  emitQuarantinedDynamicToolDiagnostics(quarantinedTools, params.hookContext);
5105
4938
  const telemetry = {
5106
4939
  didSendViaMessagingTool: false,
@@ -5140,6 +4973,10 @@ function createCodexDynamicToolBridge(params) {
5140
4973
  directToolNames
5141
4974
  }),
5142
4975
  resultContentSourceForTool: (toolName) => toolMap.get(toolName)?.tool.resultContentSource,
4976
+ sideEffectOwnerKeyForTool: (toolName) => {
4977
+ const tool = toolMap.get(toolName)?.tool;
4978
+ return tool ? getPluginToolSideEffectOwnerKey(tool) : void 0;
4979
+ },
5143
4980
  telemetry,
5144
4981
  setRemoteWorkspaceFileReader: (reader) => {
5145
4982
  readRemoteWorkspaceFile = reader;
@@ -5171,7 +5008,8 @@ function createCodexDynamicToolBridge(params) {
5171
5008
  });
5172
5009
  }
5173
5010
  const { tool, name: toolName } = toolEntry;
5174
- const args = asNonArrayRecord(call.arguments);
5011
+ const rawArguments = call.arguments;
5012
+ const args = asNonArrayRecord(rawArguments);
5175
5013
  const startedAt = Date.now();
5176
5014
  const signal = composeAbortSignals(params.signal, options?.signal);
5177
5015
  let didStartExecution = false;
@@ -5194,7 +5032,8 @@ function createCodexDynamicToolBridge(params) {
5194
5032
  };
5195
5033
  };
5196
5034
  try {
5197
- const toolArgs = tool.prepareArguments ? tool.prepareArguments(args) : args;
5035
+ const prepare = tool.prepareArguments;
5036
+ const toolArgs = prepare ? Reflect.apply(prepare, tool, [rawArguments]) : args;
5198
5037
  const preparedArgs = toolName === "message" && isRecord(toolArgs) ? await prepareCodexRemoteWorkspaceMessageMedia({
5199
5038
  args: toolArgs,
5200
5039
  localWorkspaceRoot: params.hookContext?.workspaceDir,
@@ -5214,7 +5053,20 @@ function createCodexDynamicToolBridge(params) {
5214
5053
  hasRepliedRef: params.hookContext?.hasRepliedRef ? { value: params.hookContext.hasRepliedRef.value } : void 0
5215
5054
  };
5216
5055
  didDispatchExecution = true;
5217
- const rawResult = await tool.execute(call.callId, preparedArgs, signal);
5056
+ const executionArgs = [
5057
+ call.callId,
5058
+ preparedArgs,
5059
+ signal
5060
+ ];
5061
+ if (shouldValidateCodexDynamicToolInput(tool)) executionArgs.push(createCodexDynamicToolValidationControl({
5062
+ toolCallId: call.callId,
5063
+ validate: (value) => assertCodexDynamicToolInputMatchesSchema({
5064
+ toolName,
5065
+ schema: toolEntry.inputSchema,
5066
+ value
5067
+ })
5068
+ }));
5069
+ const rawResult = await Reflect.apply(tool.execute, tool, executionArgs);
5218
5070
  captureExecutionBoundary();
5219
5071
  const telemetryRawResult = sanitizeToolResult(rawResult);
5220
5072
  const rawIsError = isToolResultError(rawResult);
@@ -5282,7 +5134,7 @@ function createCodexDynamicToolBridge(params) {
5282
5134
  contentItems,
5283
5135
  success: !resultIsError
5284
5136
  }, terminalType);
5285
- withDynamicToolTranscriptDetails(response, result.details);
5137
+ withDynamicToolTranscriptDetails(response, asOptionalRecord(sanitizeToolResult(result))?.details);
5286
5138
  withDiagnosticFailureDisposition(response, resultFailureKind);
5287
5139
  const blocksSourceReplyTermination = hasExplicitNonSourceMessageRoute(executedArgs, params.hookContext, confirmedMessagingTarget);
5288
5140
  const deliveredSourceReply = isDeliveredMessageToolOnlySourceReplyResult({
@@ -5533,6 +5385,13 @@ function projectCodexDynamicTools(tools) {
5533
5385
  });
5534
5386
  continue;
5535
5387
  }
5388
+ if (!isRecord(projection.schema)) {
5389
+ quarantinedTools.push({
5390
+ tool: descriptor.name,
5391
+ violations: [`${descriptor.name}.inputSchema must be a JSON object schema`]
5392
+ });
5393
+ continue;
5394
+ }
5536
5395
  projectedTools.push({
5537
5396
  tool,
5538
5397
  name: descriptor.name,
@@ -5557,6 +5416,20 @@ function readCodexDynamicToolDescriptor(tool, toolIndex) {
5557
5416
  violations: [`${fallbackName}.name must be a non-empty string`]
5558
5417
  }
5559
5418
  };
5419
+ const trimmedName = rawName.trim();
5420
+ let nameViolation;
5421
+ if (!trimmedName) nameViolation = `${rawName}.name must not be empty`;
5422
+ else if (trimmedName !== rawName) nameViolation = `${rawName}.name must not have leading or trailing whitespace`;
5423
+ else if (!CODEX_DYNAMIC_TOOL_NAME_PATTERN.test(rawName)) nameViolation = `${rawName}.name must match ^[a-zA-Z0-9_-]+$`;
5424
+ else if (rawName.length > CODEX_DYNAMIC_TOOL_NAME_MAX_CHARS) nameViolation = `${rawName}.name must be at most ${CODEX_DYNAMIC_TOOL_NAME_MAX_CHARS} characters`;
5425
+ else if (rawName === "mcp" || rawName.startsWith("mcp__")) nameViolation = `${rawName}.name is reserved by Codex app-server`;
5426
+ if (nameViolation) return {
5427
+ ok: false,
5428
+ diagnostic: {
5429
+ tool: rawName,
5430
+ violations: [nameViolation]
5431
+ }
5432
+ };
5560
5433
  name = rawName;
5561
5434
  } catch {
5562
5435
  return {
@@ -5598,14 +5471,18 @@ function readCodexDynamicToolDescriptor(tool, toolIndex) {
5598
5471
  parameters
5599
5472
  };
5600
5473
  }
5601
- function warnQuarantinedDynamicTools(tools) {
5602
- if (tools.length === 0) return;
5474
+ function warnQuarantinedDynamicTools(params) {
5475
+ if (params.tools.length === 0) return;
5603
5476
  const unique = /* @__PURE__ */ new Map();
5604
- for (const tool of tools) unique.set(tool.tool, tool.violations);
5605
- embeddedAgentLog.warn(`codex app-server quarantined ${unique.size} dynamic ${unique.size === 1 ? "tool" : "tools"} with unsupported input schemas: ${[...unique.keys()].join(", ")}`, { tools: [...unique.entries()].map(([tool, violations]) => ({
5606
- tool,
5607
- violations
5608
- })) });
5477
+ for (const tool of params.tools) unique.set(tool.tool, tool.violations);
5478
+ embeddedAgentLog.warn(`codex app-server quarantined ${unique.size} unsupported dynamic tool ${unique.size === 1 ? "definition" : "definitions"}: ${[...unique.keys()].join(", ")}; retained ${params.availableToolCount} available and ${params.registeredToolCount} registered tools`, {
5479
+ tools: [...unique.entries()].map(([tool, violations]) => ({
5480
+ tool,
5481
+ violations
5482
+ })),
5483
+ availableToolCount: params.availableToolCount,
5484
+ registeredToolCount: params.registeredToolCount
5485
+ });
5609
5486
  }
5610
5487
  function emitQuarantinedDynamicToolDiagnostics(tools, ctx) {
5611
5488
  for (const tool of tools) emitTrustedDiagnosticEvent({
@@ -5893,4 +5770,4 @@ function isCronAddAction(args) {
5893
5770
  return typeof action === "string" && action.trim().toLowerCase() === "add";
5894
5771
  }
5895
5772
  //#endregion
5896
- export { buildCodexNativeHookRelayDisabledConfig as A, resolveDynamicToolCallTimeoutMs as B, shouldWarnCodexDynamicToolBuildStageSummary as C, CODEX_NATIVE_HOOK_RELAY_EVENTS as D, CodexNativeToolLifecycleProjector as E, scheduleCodexNativeHookRelayUnregister as F, toCodexDynamicToolProtocolResponse as G, shouldBlockTerminalReleaseForNonTerminalDynamicToolResult as H, handleDynamicToolCallWithTimeout as I, resolveCodexToolAbortTerminalReason as K, hasPendingDynamicToolTerminalDiagnostic as L, emitCodexNativePreToolUseFailureDiagnostic as M, resolveCodexNativeHookRelayEvents as N, CODEX_NATIVE_HOOK_RELAY_TTL_GRACE_MS as O, resolveCodexNativeHookRelayTtlMs as P, isDynamicToolTerminalDiagnosticEvent as R, shouldRequireCodexSandboxExecServerEnvironment as S, readBoundedCodexRemoteWorkspaceFile as T, shouldReleaseTurnAfterTerminalDynamicTool as U, resolveTerminalDynamicToolBatchAction as V, toCodexDynamicToolProgressResponse as W, resolveCodexAppServerHookChannelId as _, emitDynamicToolStartedDiagnostic as a, resolveCodexSandboxEnvironmentSelection as b, resolveCodexProviderWebSearchSupport as c, releaseCodexSandboxExecServerEnvironment as d, buildDynamicTools as f, resolveCodexAppServerExecutionCwd as g, formatCodexDynamicToolBuildStageSummary as h, emitDynamicToolErrorDiagnostic as i, createCodexNativeHookRelay as j, buildCodexNativeHookRelayConfig as k, resolveCodexProviderWebSearchSupportForClient as l, disableCodexPluginThreadConfig as m, projectCodexExecutableDynamicTools as n, emitDynamicToolTerminalDiagnostic as o, createCodexDynamicToolBuildStageTracker as p, handleCodexAppServerElicitationRequest as r, handleCodexAppServerApprovalRequest as s, createCodexDynamicToolBridge as t, ensureCodexSandboxExecServerEnvironment as u, resolveCodexExternalSandboxPolicyForOpenClawSandbox as v, filterToolsForVisionInputs as w, shouldEnableCodexAppServerNativeToolSurface as x, resolveCodexMessageToolProvider as y, isMatchingDynamicToolTerminalDiagnostic as z };
5773
+ export { shouldRequireCodexSandboxExecServerEnvironment as C, CodexNativeToolLifecycleProjector as D, readBoundedCodexRemoteWorkspaceFile as E, shouldEnableCodexAppServerNativeToolSurface as S, filterCodexVisionTools as T, resolveCodexAppServerHookChannelId as _, emitDynamicToolStartedDiagnostic as a, resolveCodexNodePlacementToolConstructionPlan as b, resolveCodexProviderWebSearchSupport as c, releaseCodexSandboxExecServerEnvironment as d, buildDynamicTools as f, resolveCodexAppServerExecutionCwd as g, formatCodexDynamicToolBuildStageSummary as h, emitDynamicToolErrorDiagnostic as i, resolveCodexProviderWebSearchSupportForClient as l, disableCodexPluginThreadConfig as m, projectCodexExecutableDynamicTools as n, emitDynamicToolTerminalDiagnostic as o, createCodexDynamicToolBuildStageTracker as p, routeCodexAppServerElicitationRequest as r, handleCodexAppServerApprovalRequest as s, createCodexDynamicToolBridge as t, ensureCodexSandboxExecServerEnvironment as u, resolveCodexExternalSandboxPolicyForOpenClawSandbox as v, shouldWarnCodexDynamicToolBuildStageSummary as w, resolveCodexSandboxEnvironmentSelection as x, resolveCodexMessageToolProvider as y };