@openclaw/codex 2026.8.1-beta.2 → 2026.9.1-beta.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 (60) hide show
  1. package/dist/api.js +147 -0
  2. package/dist/{attempt-client-cleanup-DMLXUfa8.js → attempt-client-cleanup-bfsmcQ6m.js} +21 -2
  3. package/dist/{binding-connection-I5Bo-AsX.js → binding-connection-BMvPX73o.js} +15 -3
  4. package/dist/{bounded-turn-qgZmzy_4.js → bounded-turn-B7iBUcuP.js} +22 -16
  5. package/dist/{capabilities-CybGI1oz.js → capabilities-BRMh0aIq.js} +1 -1
  6. package/dist/cli-metadata.js +1 -1
  7. package/dist/{command-formatters-B1AEz8ZA.js → command-formatters-DEsIufkt.js} +2 -2
  8. package/dist/{command-handlers-Db8pu_ds.js → command-handlers-D_-1_qRp.js} +65 -37
  9. package/dist/command-rpc-CCeCkf_v.js +90 -0
  10. package/dist/{compact-LspQaGfl.js → compact-BglNFZrQ.js} +35 -29
  11. package/dist/{session-binding-DPxIvMgE.js → config-giW8X0QA.js} +108 -914
  12. package/dist/context-compaction-activity-BOeJf7yo.js +58 -0
  13. package/dist/doctor-contract-api.js +23 -21
  14. package/dist/{dynamic-tools-BVdyWkUD.js → dynamic-tools-Dnlfa--w.js} +1345 -1424
  15. package/dist/{effective-mcp-catalog-DIIhAtOZ.js → effective-mcp-catalog-CTeGrnfO.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-Bk1I90Bg.js} +18 -18
  19. package/dist/harness-Csz0F-DK.js +258 -0
  20. package/dist/harness.js +2 -226
  21. package/dist/index.js +262 -181
  22. package/dist/{isolated-completion-C58NbL40.js → isolated-completion-Cv6FqygM.js} +4 -4
  23. package/dist/managed-binary-DFYK4DWq.js +248 -0
  24. package/dist/media-understanding-provider.js +2 -2
  25. package/dist/model-catalog-i9rNBthX.js +48 -0
  26. package/dist/{models-BFlpn1Ex.js → models-TS3HBU-q.js} +4 -3
  27. package/dist/{native-subagent-monitor-DyTGmTUx.js → native-subagent-monitor-4Mfq2HDs.js} +354 -83
  28. package/dist/node-exec-server.runtime-BpWqJO1V.js +192 -0
  29. package/dist/{plugin-app-cache-key-C3H2CVp6.js → plugin-app-cache-key-C1CAdfcP.js} +19 -4
  30. package/dist/plugin-approval-roundtrip-bUm777tR.js +95 -0
  31. package/dist/{plugin-marketplace-discovery-CzrGJdCR.js → plugin-marketplace-discovery-B9DvaAG-.js} +59 -54
  32. package/dist/{plugin-metadata-cache-CNJPj4_b.js → plugin-metadata-cache-CTCT3w5L.js} +164 -134
  33. package/dist/{protocol-validators-B4F7L8Rf.js → protocol-validators-VeJxxJ0U.js} +80 -5
  34. package/dist/{rate-limits-DZXmOL8-.js → rate-limits-C5_D8Gmb.js} +1 -1
  35. package/dist/{request-BRl5_UfX.js → request-DXXGHko0.js} +2 -2
  36. package/dist/{run-attempt-P8BdxC1I.js → run-attempt-DYc0QCBF.js} +1276 -765
  37. package/dist/{runtime-artifact-BVSPdd6g.js → runtime-artifact-BvyT1uyd.js} +4 -7
  38. package/dist/{sandbox-guard-Cmwc9eIk.js → sandbox-guard-kuWTn9C5.js} +25 -19
  39. package/dist/session-binding-Bcynfd-_.js +874 -0
  40. package/dist/{session-catalog-xwNi_SDM.js → session-catalog-DaLKVtpH.js} +1534 -1288
  41. package/dist/session-catalog-home-id-BNwAJ9D7.js +18 -0
  42. package/dist/{session-cli-7hgXZDV7.js → session-cli-DMc4oS2w.js} +20 -7
  43. package/dist/session-permission-policy-BrNH1bjy.js +160 -0
  44. package/dist/{session-retirement-DoMMJFKm.js → session-retirement-CcNC3YSY.js} +3 -3
  45. package/dist/{session-upstream-marker-CrIFX06J.js → session-upstream-marker-BIkusb95.js} +1 -1
  46. package/dist/{settled-turn-finalizer-BRWBO4MO.js → settled-turn-finalizer-Dc3ApjU4.js} +24 -20
  47. package/dist/{shared-client-kIBbGjk4.js → shared-client-DKz23hHY.js} +1632 -372
  48. package/dist/{side-question-9HAL26l9.js → side-question-DjA_jsNe.js} +105 -54
  49. package/dist/{thread-lifecycle-Bu1BTDXr.js → thread-lifecycle-L7oEkTlq.js} +1147 -114
  50. package/dist/{thread-ownership-6kOQHlyC.js → thread-ownership-DZeuKH1j.js} +2 -2
  51. package/dist/{thread-resume-CPrJNQVM.js → thread-resume-BE9qNEW0.js} +3 -3
  52. package/dist/{transcript-mirror-B9gGgnJS.js → transcript-mirror-DsCMWBca.js} +137 -38
  53. package/dist/{transport-stdio-DIImwDVS.js → transport-stdio-DhoGy33Z.js} +2 -2
  54. package/dist/{upstream-prompt-provenance-ClRBije0.js → upstream-prompt-provenance-CFmsKwr9.js} +12 -14
  55. package/dist/{upstream-session-fork-CE-_LdnP.js → upstream-session-fork-DlaHxSLC.js} +12 -6
  56. package/dist/{usage-CHlV0e-o.js → usage-ROIaW3wo.js} +3 -3
  57. package/dist/{web-search-provider.runtime-DRw2Vl42.js → web-search-provider.runtime-ClaNPV7N.js} +3 -3
  58. package/openclaw.plugin.json +49 -2
  59. package/package.json +9 -8
  60. package/dist/app-server-policy-zcBDc1JU.js +0 -49
@@ -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 { h as readCodexPluginConfig, p as isCodexRemoteExecPlacementSandbox } from "./config-giW8X0QA.js";
2
+ import { K as createCodexElicitationResponse, gt as CODEX_OPENCLAW_DIRECT_DYNAMIC_TOOL_NAMESPACE, p as releaseLeasedSharedCodexAppServerClient, vt as isJsonObject } from "./shared-client-DKz23hHY.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-bfsmcQ6m.js";
4
+ import { l as readCodexTurn } from "./protocol-validators-VeJxxJ0U.js";
5
+ import { $ as isForcedPrivateQaCodexRuntime, D as withDynamicToolExecutionState, E as createFailedDynamicToolResponse, F as resolveCodexWebSearchPlan, O as withDynamicToolTranscriptDetails, Q as filterCodexDynamicToolsForDisabledNativeSurface, T as resolveCodexToolAbortTerminalReason, Z as filterCodexDynamicTools, d as emitCodexNativePreToolUseFailureDiagnostic, nt as normalizeCodexDynamicToolName, tt as isSystemAgentOnlyCodexDynamicToolAllowlist } from "./thread-lifecycle-L7oEkTlq.js";
6
6
  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";
7
+ import { r as formatCodexDisplayText } from "./command-formatters-DEsIufkt.js";
8
+ import { a as resolveCodexNodeExecToolOverrides, i as resolveCodexNativeExecutionPolicy } from "./sandbox-guard-kuWTn9C5.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-bUm777tR.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";
22
+ import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
19
23
  import { DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS, estimateToolResultTextChars, resolveLiveToolResultMaxChars, sliceToolResultTextToBudget, sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
20
24
  import { once } from "node:events";
21
25
  import { isIP } from "node:net";
22
26
  import { WebSocketServer } from "ws";
23
27
  import { fileURLToPath, pathToFileURL } from "node:url";
24
28
  import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
29
+ import { redactToolPayloadText } from "openclaw/plugin-sdk/logging-core";
30
+ import { isPathStrictlyInside, root } from "openclaw/plugin-sdk/file-access-runtime";
25
31
  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";
32
+ import { emitTrustedDiagnosticEvent } from "openclaw/plugin-sdk/diagnostic-runtime";
33
+ import { buildRemoteCommand, isToolAllowed, sanitizeEnvVars } from "openclaw/plugin-sdk/sandbox";
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,57 +545,81 @@ 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([
548
+ const CODEX_GATEWAY_EXEC_DYNAMIC_TOOL_NAME = "gateway_exec";
549
+ const CODEX_GATEWAY_PROCESS_DYNAMIC_TOOL_NAME = "gateway_process";
550
+ const CODEX_EXEC_POLICY_PARAMETER_NAMES = /* @__PURE__ */ new Set([
1327
551
  "host",
1328
552
  "security",
1329
553
  "ask"
1330
554
  ]);
555
+ const CODEX_NODE_EXEC_PARAMETER_NAMES = /* @__PURE__ */ new Set([
556
+ "command",
557
+ "workdir",
558
+ "env",
559
+ "timeoutSeconds",
560
+ "node"
561
+ ]);
562
+ const PROCESS_FOLLOWUP_TEXT = "Use process (list/poll/log/write/send-keys/submit/paste/kill/clear/remove) for follow-up.";
1331
563
  /** Returns true when plugin config explicitly removes any named dynamic tool. */
1332
564
  function isCodexDynamicToolExcluded(config, names) {
1333
565
  const normalizedNames = new Set(names.map((name) => normalizeCodexDynamicToolName(name)));
1334
566
  return (config.codexDynamicToolsExclude ?? []).some((name) => normalizedNames.has(normalizeCodexDynamicToolName(name)));
1335
567
  }
1336
- function createNodeExecDynamicTool(execTool, configuredNode) {
1337
- const pinnedNode = configuredNode?.trim();
568
+ function createExecAliasDynamicTool(execTool, params) {
569
+ const pinnedNode = params.host === "node" ? params.node?.trim() : void 0;
570
+ const nodeAlias = params.host === "node";
571
+ const gatewayProcessAliasAvailable = params.host === "gateway" && params.processAliasAvailable;
572
+ const name = nodeAlias ? CODEX_NODE_EXEC_DYNAMIC_TOOL_NAME : CODEX_GATEWAY_EXEC_DYNAMIC_TOOL_NAME;
573
+ const description = nodeAlias ? 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." : "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.";
574
+ const followupText = nodeAlias ? "Remote-node background follow-up is unavailable. Wait for the command to complete." : gatewayProcessAliasAvailable ? "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.";
1338
575
  return {
1339
576
  ...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) }),
577
+ name,
578
+ description,
579
+ parameters: hideExecDynamicToolParameters(execTool.parameters, !nodeAlias || Boolean(pinnedNode), nodeAlias),
1343
580
  execute: async (toolCallId, args, signal, onUpdate) => {
1344
- const result = await execTool.execute(toolCallId, pinNodeExecDynamicToolArgs(args, pinnedNode), signal, onUpdate);
581
+ const result = await execTool.execute(toolCallId, pinExecDynamicToolArgs(args, params, pinnedNode), signal, onUpdate);
1345
582
  return {
1346
583
  ...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)
584
+ content: result.content.map((item) => item.type === "text" ? Object.assign({}, item, { text: item.text.replace(PROCESS_FOLLOWUP_TEXT, followupText) }) : item)
1348
585
  };
1349
586
  }
1350
587
  };
1351
588
  }
1352
- function createNodeProcessDynamicTool(processTool) {
589
+ function createGatewayProcessAliasDynamicTool(processTool) {
1353
590
  return {
1354
591
  ...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."
592
+ name: CODEX_GATEWAY_PROCESS_DYNAMIC_TOOL_NAME,
593
+ 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."
1357
594
  };
1358
595
  }
1359
- function pinNodeExecDynamicToolArgs(args, configuredNode) {
1360
- const { host: _host, security: _security, ask: _ask, node: requestedNode, ...rest } = args && typeof args === "object" && !Array.isArray(args) ? args : {};
596
+ function pinExecDynamicToolArgs(args, params, configuredNode) {
597
+ const { host: _host, security: _security, ask: _ask, node: requestedNode, ...rest } = normalizeExecDynamicToolArgs(args);
598
+ if (params.host === "gateway") return {
599
+ ...rest,
600
+ host: params.host,
601
+ ...params.ask ? { ask: params.ask } : {}
602
+ };
603
+ const nodeArgs = Object.fromEntries(Object.entries(rest).filter(([name]) => CODEX_NODE_EXEC_PARAMETER_NAMES.has(name)));
1361
604
  const node = configuredNode ?? (typeof requestedNode === "string" ? requestedNode.trim() : "");
1362
605
  return {
1363
- ...rest,
1364
- host: "node",
606
+ ...nodeArgs,
607
+ host: params.host,
1365
608
  ...node ? { node } : {}
1366
609
  };
1367
610
  }
1368
- function hideNodeExecDynamicToolParameters(parameters, options) {
611
+ function normalizeExecDynamicToolArgs(args) {
612
+ return args && typeof args === "object" && !Array.isArray(args) ? args : {};
613
+ }
614
+ function hideExecDynamicToolParameters(parameters, hideNode, nodeOnly) {
1369
615
  if (!parameters || typeof parameters !== "object" || Array.isArray(parameters)) return parameters;
1370
616
  const schema = parameters;
1371
617
  const rawProperties = schema.properties;
1372
618
  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")));
619
+ const includeParameter = (name) => nodeOnly ? CODEX_NODE_EXEC_PARAMETER_NAMES.has(name) && !(hideNode && name === "node") : !CODEX_EXEC_POLICY_PARAMETER_NAMES.has(normalizeCodexDynamicToolName(name)) && !(hideNode && normalizeCodexDynamicToolName(name) === "node");
620
+ const nextProperties = Object.fromEntries(Object.entries(rawProperties).filter(([name]) => includeParameter(name)));
1374
621
  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;
622
+ const nextRequired = Array.isArray(rawRequired) ? rawRequired.filter((name) => typeof name !== "string" || includeParameter(name)) : rawRequired;
1376
623
  return {
1377
624
  ...schema,
1378
625
  properties: nextProperties,
@@ -1382,13 +629,12 @@ function hideNodeExecDynamicToolParameters(parameters, options) {
1382
629
  //#endregion
1383
630
  //#region extensions/codex/src/app-server/vision-tools.ts
1384
631
  /**
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.
632
+ * Codex's enabled native surface includes its stable view_image loader. Keep
633
+ * OpenClaw's view_image tool only when that surface or model vision is unavailable.
1387
634
  */
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");
635
+ function filterCodexVisionTools(tools, params) {
636
+ if (!params.modelHasVision || !params.nativeImageInspectionEnabled) return tools;
637
+ return tools.filter((tool) => tool.name !== "view_image");
1392
638
  }
1393
639
  //#endregion
1394
640
  //#region extensions/codex/src/app-server/dynamic-tool-build.ts
@@ -1406,6 +652,18 @@ const CODEX_NATIVE_SANDBOX_TOOL_REQUIREMENTS = [
1406
652
  "apply_patch"
1407
653
  ];
1408
654
  const CODEX_MEMORY_FLUSH_DYNAMIC_TOOL_ALLOW = /* @__PURE__ */ new Set(["read", "write"]);
655
+ /** Keeps node filesystem and process ownership on its native exec-server. */
656
+ function resolveCodexNodePlacementToolConstructionPlan(sandbox, nativeToolSurfaceEnabled) {
657
+ if (!isCodexRemoteExecPlacementSandbox(sandbox) || sandbox?.backendId !== "node" || !("placementNodeId" in sandbox) || typeof sandbox.placementNodeId !== "string" || !sandbox.placementNodeId) return;
658
+ 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.");
659
+ return {
660
+ includeBaseCodingTools: false,
661
+ includeShellTools: false,
662
+ includeChannelTools: true,
663
+ includeOpenClawTools: true,
664
+ includePluginTools: true
665
+ };
666
+ }
1409
667
  function preserveRingZeroSystemAgentTool(allTools, filteredTools) {
1410
668
  const openclaw = allTools.find((tool) => tool.name === "openclaw" && tool.catalogMode === "direct-only");
1411
669
  if (!openclaw) return filteredTools;
@@ -1497,101 +755,125 @@ async function buildDynamicTools(input) {
1497
755
  const injectedOpenClawCodingToolsFactory = dynamicToolBuildState.openClawCodingToolsFactory;
1498
756
  let agentHarnessModule;
1499
757
  const loadAgentHarnessModule = async () => agentHarnessModule ??= await import("openclaw/plugin-sdk/agent-harness");
1500
- const createOpenClawCodingTools = injectedOpenClawCodingToolsFactory ?? (await loadAgentHarnessModule()).createOpenClawCodingTools;
1501
758
  toolBuildStages.mark("load-agent-harness-tools");
1502
759
  const sessionKeys = resolveOpenClawCodingToolsSessionKeys(params, input.sandboxSessionKey);
1503
760
  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
- }),
761
+ const webSearchPlan = resolveCodexWebSearchPlan({
1547
762
  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 });
763
+ disableTools: params.disableTools,
764
+ nativeToolSurfaceEnabled: input.nativeToolSurfaceEnabled,
765
+ nativeProviderWebSearchSupport: input.nativeProviderWebSearchSupport
766
+ });
767
+ const webFetchHostnameAllowlistRef = {};
768
+ const buildOpenClawCodingTools = () => {
769
+ const toolConstructionPlan = resolveCodexNodePlacementToolConstructionPlan(input.sandbox, input.nativeToolSurfaceEnabled);
770
+ const options = {
771
+ agentId: input.sessionAgentId,
772
+ ...buildEmbeddedAttemptToolRunContext(params),
773
+ exec: {
774
+ ...params.execOverrides,
775
+ ...input.sessionPermissionPolicy ? { mode: input.sessionPermissionPolicy.execMode } : {},
776
+ ...resolveCodexNodeExecToolOverrides(nativeExecutionPolicy),
777
+ config: params.config,
778
+ elevated: params.bashElevated
779
+ },
780
+ sessionPermissionPolicy: input.sessionPermissionPolicy ? {
781
+ mode: input.sessionPermissionPolicy.mode,
782
+ root: input.sessionPermissionPolicy.root
783
+ } : void 0,
784
+ sandbox: input.sandbox,
785
+ ...toolConstructionPlan ? { toolConstructionPlan } : {},
786
+ messageProvider: resolveCodexMessageToolProvider(params),
787
+ toolPolicyMessageProvider: params.messageProvider ?? params.messageChannel,
788
+ clientCaps: params.clientCaps,
789
+ chatType: params.chatType,
790
+ agentAccountId: params.agentAccountId,
791
+ messageTo: params.messageTo,
792
+ messageThreadId: params.messageThreadId,
793
+ nativeChannelId: params.chatId,
794
+ messageActionTurnCapability: params.messageActionTurnCapability,
795
+ groupId: params.groupId,
796
+ groupChannel: params.groupChannel,
797
+ groupSpace: params.groupSpace,
798
+ spawnedBy: params.spawnedBy,
799
+ senderId: params.senderId,
800
+ senderName: params.senderName,
801
+ senderUsername: params.senderUsername,
802
+ senderE164: params.senderE164,
803
+ senderIsOwner: params.senderIsOwner,
804
+ inputProvenance: params.inputProvenance,
805
+ trustedInternalHandoff: params.trustedInternalHandoff,
806
+ scheduledToolPolicy: params.scheduledToolPolicy,
807
+ allowGatewaySubagentBinding: params.allowGatewaySubagentBinding || isForcedPrivateQaCodexRuntime(),
808
+ ...sessionKeys,
809
+ sessionId: params.sessionId,
810
+ runId: params.runId,
811
+ approvalReviewerDeviceId: params.approvalReviewerDeviceId,
812
+ agentDir,
813
+ preparedModelRuntime: params.preparedModelRuntime,
814
+ cwd: input.effectiveCwd ?? input.effectiveWorkspace,
815
+ workspaceDir: input.effectiveWorkspace,
816
+ spawnWorkspaceDir: input.effectiveCwd && input.effectiveCwd !== input.effectiveWorkspace ? input.resolvedWorkspace : resolveAttemptSpawnWorkspaceDir({
817
+ sandbox: input.sandbox,
818
+ resolvedWorkspace: input.resolvedWorkspace
819
+ }),
820
+ config: params.config,
821
+ githubPublicationAvailable: params.githubPublicationAvailable,
822
+ authProfileStore: params.toolAuthProfileStore ?? params.authProfileStore,
823
+ abortSignal: input.runAbortController.signal,
824
+ emitBeforeToolCallDiagnostics: false,
825
+ modelProvider: params.model.provider,
826
+ modelId: params.modelId,
827
+ modelCompat: params.model.compat && typeof params.model.compat === "object" ? params.model.compat : void 0,
828
+ modelApi: params.model.api,
829
+ modelContextWindowTokens: params.model.contextWindow,
830
+ delegationCapability: params.delegationCapability,
831
+ modelAuthMode: resolveModelAuthMode(params.model.provider, params.config, params.toolAuthProfileStore ?? params.authProfileStore, { workspaceDir: input.effectiveWorkspace }),
832
+ suppressManagedWebSearch: false,
833
+ webFetchHostnameAllowlistRef,
834
+ currentChannelId: params.currentChannelId,
835
+ currentMessagingTarget: params.currentMessagingTarget,
836
+ hookChannelId: resolveCodexAppServerHookChannelId(params, input.sandboxSessionKey),
837
+ currentThreadTs: params.currentThreadTs,
838
+ currentMessageId: params.currentMessageId,
839
+ replyToMode: params.replyToMode,
840
+ hasRepliedRef: params.hasRepliedRef,
841
+ modelHasVision,
842
+ computerContextEpoch: input.computerContextEpoch,
843
+ requireExplicitMessageTarget: params.requireExplicitMessageTarget ?? isSubagentSessionKey(params.sessionKey),
844
+ sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
845
+ taskSuggestionDeliveryMode: params.taskSuggestionDeliveryMode,
846
+ disableMessageTool: input.ignoreDisableMessageTool ? false : params.disableMessageTool,
847
+ forceMessageTool: shouldForceMessageTool(messagePolicyParams),
848
+ enableHeartbeatTool: params.trigger === "heartbeat" || input.forceHeartbeatTool === true,
849
+ forceHeartbeatTool: params.trigger === "heartbeat" || input.forceHeartbeatTool === true,
850
+ onYield: (message, acknowledgment) => {
851
+ input.onYieldDetected(acknowledgment);
852
+ input.onCodexAppServerEvent?.({
853
+ stream: "codex_app_server.tool",
854
+ data: {
855
+ name: "sessions_yield",
856
+ message
857
+ }
858
+ });
859
+ },
860
+ claimYieldCompletion: input.claimYieldCompletion,
861
+ recordToolPrepStage: (name) => {
862
+ toolBuildStages.mark(name);
863
+ },
864
+ onToolOutcome: params.onToolOutcome,
865
+ isTurnTainted: params.isTurnTainted,
866
+ allocateToolOutcomeOrdinal: params.allocateToolOutcomeOrdinal,
867
+ cronCreatorToolAllowlistRef: input.cronCreatorToolAllowlistRef,
868
+ cronCreatorToolAllowlistCaptureRef: input.cronCreatorToolAllowlistCaptureRef,
869
+ cronCreatorAuthorityUnavailableReason: input.cronCreatorAuthorityUnavailableReason
870
+ };
871
+ const bindingOptions = { cwd: input.effectiveCwd ?? input.effectiveWorkspace };
872
+ if (injectedOpenClawCodingToolsFactory) return params.hostCapabilities.bindToolSurface(injectedOpenClawCodingToolsFactory(options), bindingOptions);
873
+ const createToolSurface = params.hostCapabilities.createToolSurface;
874
+ if (!createToolSurface) throw new Error("Codex tool construction requires a current host capability");
875
+ return createToolSurface(options, bindingOptions);
876
+ };
1595
877
  const allTools = input.resolveCronCreatorToolAuthority ? runWithCronCreatorAuthorityCapabilityResolver({
1596
878
  capability: params.cronCreatorAuthorityCapability,
1597
879
  runId: params.runId,
@@ -1602,20 +884,14 @@ async function buildDynamicTools(input) {
1602
884
  const preNormalizationDiagnostics = [];
1603
885
  const readableAllToolProjection = filterProviderNormalizableTools(allTools);
1604
886
  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
887
  const readableAllTools = [...readableAllToolProjection.tools];
1612
888
  const normallyProfiledTools = input.nativeToolSurfaceEnabled === false ? filterCodexDynamicToolsForDisabledNativeSurface(readableAllTools, input.pluginConfig, { preserveShell: shouldKeepOpenClawShellDynamicTools(input, nativeExecutionPolicy) }) : filterCodexDynamicTools(readableAllTools, input.pluginConfig);
1613
889
  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);
890
+ const codexFilteredTools = addNodeShellDynamicToolsIfNeeded(addGatewayShellDynamicToolsIfAvailable(addSandboxShellDynamicToolsIfAvailable(isCodexMemoryFlushRun(params) ? filterCodexMemoryFlushDynamicTools(readableAllTools) : profileFilteredTools, readableAllTools, input), readableAllTools, input, nativeExecutionPolicy), readableAllTools, input, nativeExecutionPolicy);
1615
891
  toolBuildStages.mark("codex-filtering");
1616
- const visionFilteredTools = filterToolsForVisionInputs(codexFilteredTools, {
892
+ const visionFilteredTools = filterCodexVisionTools(codexFilteredTools, {
1617
893
  modelHasVision,
1618
- hasInboundImages: (params.images?.length ?? 0) > 0
894
+ nativeImageInspectionEnabled: input.nativeToolSurfaceEnabled === true
1619
895
  });
1620
896
  toolBuildStages.mark("vision-filtering");
1621
897
  const webSearchPresent = visionFilteredTools.some((tool) => tool.name === "web_search");
@@ -1662,7 +938,9 @@ async function buildDynamicTools(input) {
1662
938
  onPreNormalizationSchemaDiagnostics: (diagnostics) => preNormalizationDiagnostics.push(...diagnostics)
1663
939
  });
1664
940
  toolBuildStages.mark("runtime-normalization");
1665
- input.onWebSearchPolicyResolved?.(normalizedTools.some((tool) => tool.name === "web_search"));
941
+ const webSearchAllowed = normalizedTools.some((tool) => tool.name === "web_search");
942
+ webFetchHostnameAllowlistRef.value = webSearchAllowed ? webSearchPlan.webFetchHostnameAllowlist : void 0;
943
+ input.onWebSearchPolicyResolved?.(webSearchAllowed);
1666
944
  const exposedTools = webSearchPlan.suppressManagedWebSearch ? normalizedTools.filter((tool) => tool.name !== "web_search") : normalizedTools;
1667
945
  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
946
  runId: params.runId,
@@ -1695,6 +973,24 @@ async function buildDynamicTools(input) {
1695
973
  }
1696
974
  return exposedTools;
1697
975
  }
976
+ /** Keeps the OpenClaw Gateway execution path available beside Codex native shell. */
977
+ function addGatewayShellDynamicToolsIfAvailable(filteredTools, allTools, input, executionPolicy) {
978
+ if (isCodexMemoryFlushRun(input.params) || input.nativeToolSurfaceEnabled !== true || input.sandbox?.enabled === true || !executionPolicy.nativeToolSurfaceAllowed || executionPolicy.effectiveExecHost !== "gateway") return filteredTools;
979
+ const execTool = allTools.find((tool) => normalizeCodexDynamicToolName(tool.name) === "exec");
980
+ const processTool = allTools.find((tool) => normalizeCodexDynamicToolName(tool.name) === "process");
981
+ const existingNames = new Set(filteredTools.map((tool) => normalizeCodexDynamicToolName(tool.name)));
982
+ const execExcluded = isCodexDynamicToolExcluded(input.pluginConfig, ["exec", CODEX_GATEWAY_EXEC_DYNAMIC_TOOL_NAME]);
983
+ if (!execTool || execExcluded || existingNames.has("gateway_exec")) return filteredTools;
984
+ const processExcluded = isCodexDynamicToolExcluded(input.pluginConfig, ["process", CODEX_GATEWAY_PROCESS_DYNAMIC_TOOL_NAME]);
985
+ const processAliasAvailable = Boolean(processTool && !processExcluded && !existingNames.has("gateway_process"));
986
+ const toolsToAppend = [createExecAliasDynamicTool(execTool, {
987
+ host: "gateway",
988
+ processAliasAvailable,
989
+ ...input.sessionPermissionPolicy?.mode === "guarded" ? { ask: "always" } : {}
990
+ })];
991
+ if (processAliasAvailable && processTool) toolsToAppend.push(createGatewayProcessAliasDynamicTool(processTool));
992
+ return [...filteredTools, ...toolsToAppend];
993
+ }
1698
994
  /** Preserves delivery-critical tools when a narrow allowlist would otherwise hide them. */
1699
995
  function includeForcedCodexDynamicToolAllow(toolsAllow, params) {
1700
996
  if (toolsAllow === void 0 || hasWildcardCodexToolsAllow(toolsAllow)) return toolsAllow;
@@ -1710,6 +1006,11 @@ function shouldEnableCodexAppServerNativeToolSurface(params, sandbox, options =
1710
1006
  if (params.pluginHarnessToolPolicyRestricted === true) return false;
1711
1007
  if (isCodexMemoryFlushRun(params)) return false;
1712
1008
  if (params.disableTools) return false;
1009
+ if (isCodexNativeExecutionBlockedByNodeExecHost(params, {
1010
+ agentId: options.agentId,
1011
+ runtimeSessionKey: options.runtimeSessionKey,
1012
+ sandbox
1013
+ })) return false;
1713
1014
  const toolsAllow = includeForcedCodexDynamicToolAllow(params.toolsAllow, params);
1714
1015
  if (toolsAllow === void 0) return canCodexAppServerNativeToolSurfaceHonorSandbox(sandbox, options);
1715
1016
  return hasWildcardCodexToolsAllow(toolsAllow) && canCodexAppServerNativeToolSurfaceHonorSandbox(sandbox, options);
@@ -1731,7 +1032,7 @@ function resolveCodexRuntimePolicySessionKey(params, runtimeSessionKey) {
1731
1032
  }
1732
1033
  function canCodexAppServerNativeToolSurfaceHonorSandbox(sandbox, options = {}) {
1733
1034
  if (!sandbox?.enabled) return true;
1734
- if (options.sandboxExecServerEnabled === true && sandbox.backend && canSandboxToolPolicyExposeCodexNativeToolSurface(sandbox)) return true;
1035
+ if (options.sandboxExecServerEnabled === true && (sandbox.backend || isCodexRemoteExecPlacementSandbox(sandbox)) && canSandboxToolPolicyExposeCodexNativeToolSurface(sandbox)) return true;
1735
1036
  return false;
1736
1037
  }
1737
1038
  function canSandboxToolPolicyExposeCodexNativeToolSurface(sandbox) {
@@ -1807,7 +1108,7 @@ function addSandboxShellDynamicToolsIfAvailable(filteredTools, allTools, input)
1807
1108
  const sandboxProcessTool = {
1808
1109
  ...processTool,
1809
1110
  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."
1111
+ 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
1112
  };
1812
1113
  return [
1813
1114
  ...filteredTools,
@@ -1839,12 +1140,12 @@ function addNodeShellDynamicToolsIfNeeded(filteredTools, allTools, input, nodePo
1839
1140
  const nodeExecAvailableFromAuto = nodePolicy.requestedExecHost === "auto" && nodePolicy.effectiveExecHost === "gateway";
1840
1141
  if (!nodeExecIsDefault && !nodeExecAvailableFromAuto) return filteredTools;
1841
1142
  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;
1143
+ if (!execTool) return filteredTools;
1144
+ if (!isCodexDynamicToolExcluded(input.pluginConfig, ["exec", "node_exec"]) && !filteredTools.some((tool) => normalizeCodexDynamicToolName(tool.name) === "node_exec")) return [...filteredTools, createExecAliasDynamicTool(execTool, {
1145
+ host: "node",
1146
+ node: nodePolicy.node
1147
+ })];
1148
+ return filteredTools;
1848
1149
  }
1849
1150
  function shouldKeepOpenClawShellDynamicTools(input, nodePolicy) {
1850
1151
  return !isCodexMemoryFlushRun(input.params) && input.nativeToolSurfaceEnabled === false && input.sandbox?.enabled !== true && nodePolicy.effectiveExecHost !== "node";
@@ -1868,7 +1169,7 @@ function filterCodexDynamicToolsForAllowlist(tools, toolsAllow) {
1868
1169
  const allowSet = new Set(toolsAllow.map((name) => normalizeCodexDynamicToolName(name)).filter(Boolean));
1869
1170
  return tools.filter((tool) => {
1870
1171
  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"));
1172
+ 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
1173
  });
1873
1174
  }
1874
1175
  /** Detects the wildcard allowlist marker after Codex tool-name normalization. */
@@ -1880,9 +1181,317 @@ function shouldForceMessageTool(params) {
1880
1181
  return params.disableMessageTool !== true && params.sourceReplyDeliveryMode === "message_tool_only";
1881
1182
  }
1882
1183
  //#endregion
1184
+ //#region extensions/codex/src/app-server/sandbox-exec-server-node-relay.ts
1185
+ const CODEX_NODE_EXEC_SERVER_MAX_MESSAGE_BYTES$1 = 64 * 1024 * 1024;
1186
+ const CODEX_NODE_EXEC_SERVER_MAX_FAILURE_DETAIL_CHARS = 240;
1187
+ const CODEX_NODE_HTTP_CREDENTIAL_BODY_MAX_BYTES = 1024 * 1024;
1188
+ const CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_FIELDS = 256;
1189
+ const CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_DEPTH = 8;
1190
+ const CODEX_NODE_HTTP_CREDENTIAL_HEADER_NAMES = /* @__PURE__ */ new Set([
1191
+ "authorization",
1192
+ "proxy-authorization",
1193
+ "cookie",
1194
+ "set-cookie",
1195
+ "x-api-key",
1196
+ "api-key",
1197
+ "apikey",
1198
+ "x-auth-token",
1199
+ "auth-token",
1200
+ "x-access-token",
1201
+ "access-token",
1202
+ "x-secret-key",
1203
+ "secret-key",
1204
+ "x-goog-api-key",
1205
+ "x-vault-token",
1206
+ "x-api-token"
1207
+ ]);
1208
+ 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;
1209
+ 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;
1210
+ const nodeExecServerTextDecoder = new TextDecoder("utf-8", { fatal: true });
1211
+ /** Produces the bounded, redacted terminal failure shared by pending and claimed node leases. */
1212
+ function createCodexNodeExecServerDisconnectError(reason, cause) {
1213
+ const detail = cause === void 0 ? "" : `: ${truncateUtf16Safe(redactSensitiveText(formatErrorMessage$1(cause), { mode: "tools" }), CODEX_NODE_EXEC_SERVER_MAX_FAILURE_DETAIL_CHARS)}`;
1214
+ return /* @__PURE__ */ new Error(`Codex execution node disconnected; start a fresh attempt. (${reason}${detail})`);
1215
+ }
1216
+ /** Relays one authorized, single-use Codex exec-server channel without interpreting its protocol. */
1217
+ async function startCodexNodeExecServerRelay(params) {
1218
+ const { channel } = params.lease;
1219
+ const { socket } = params;
1220
+ let closed = false;
1221
+ const { promise: finished, resolve: finish } = createDeferred();
1222
+ let unsubscribe = () => {};
1223
+ const closeBoth = (code = 1001, reason = "execution channel closed") => {
1224
+ if (closed) return;
1225
+ closed = true;
1226
+ unsubscribe();
1227
+ params.lease.closeRelay = void 0;
1228
+ params.lease.onChannelClosed = void 0;
1229
+ if (!params.lease.closed) {
1230
+ params.lease.closed = true;
1231
+ channel.close();
1232
+ }
1233
+ if (socket.readyState === socket.OPEN || socket.readyState === socket.CONNECTING) socket.close(code, reason);
1234
+ finish();
1235
+ };
1236
+ params.lease.closeRelay = closeBoth;
1237
+ const failUnexpectedly = (code, reason, cause) => {
1238
+ if (!closed && !params.lease.closed) params.lease.onDisconnected?.(createCodexNodeExecServerDisconnectError(reason, cause));
1239
+ closeBoth(code, reason);
1240
+ };
1241
+ params.lease.onChannelClosed = ({ failed, error }) => failUnexpectedly(failed ? 1011 : 1001, failed ? "execution node failed" : "execution node disconnected", error);
1242
+ socket.once("close", () => failUnexpectedly(1001, "execution socket closed"));
1243
+ socket.once("error", () => failUnexpectedly(1011, "execution socket failed"));
1244
+ let toNode = Promise.resolve();
1245
+ socket.on("message", (data) => {
1246
+ if (closed) return;
1247
+ socket.pause();
1248
+ toNode = toNode.then(async () => {
1249
+ const frame = normalizeCodexExecServerFrame(data);
1250
+ const request = validateCodexExecServerMessage(frame);
1251
+ const rejection = rejectCredentialedCodexNodeHttpRequest(request);
1252
+ if (rejection) await sendCodexExecServerFrame(socket, rejection);
1253
+ else await channel.send(sanitizeCodexExecServerRequest(frame, request));
1254
+ if (!closed) socket.resume();
1255
+ }).catch((error) => {
1256
+ failUnexpectedly(error instanceof RangeError ? 1009 : 1007, "invalid execution message");
1257
+ });
1258
+ });
1259
+ unsubscribe = channel.onMessage(async (message) => {
1260
+ if (closed) return;
1261
+ try {
1262
+ const frame = normalizeCodexExecServerFrame(message);
1263
+ validateCodexExecServerMessage(frame);
1264
+ await sendCodexExecServerFrame(socket, frame);
1265
+ } catch (error) {
1266
+ failUnexpectedly(error instanceof RangeError ? 1009 : 1007, "invalid device message");
1267
+ }
1268
+ });
1269
+ await finished;
1270
+ }
1271
+ function sendCodexExecServerFrame(socket, frame) {
1272
+ return new Promise((resolve, reject) => {
1273
+ socket.send(frame, { binary: false }, (error) => {
1274
+ if (error) {
1275
+ reject(error);
1276
+ return;
1277
+ }
1278
+ resolve();
1279
+ });
1280
+ });
1281
+ }
1282
+ function normalizeCodexExecServerFrame(data) {
1283
+ 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);
1284
+ if (frame.length > CODEX_NODE_EXEC_SERVER_MAX_MESSAGE_BYTES$1) throw new RangeError("Codex exec-server message exceeds its 64 MiB limit.");
1285
+ if (frame.includes(10) || frame.includes(13)) throw new Error("Codex exec-server messages must occupy exactly one stdio line.");
1286
+ return frame;
1287
+ }
1288
+ function validateCodexExecServerMessage(frame) {
1289
+ const parsed = JSON.parse(nodeExecServerTextDecoder.decode(frame));
1290
+ if (!isRecord(parsed)) throw new Error("Codex exec-server message must be a JSON object.");
1291
+ return parsed;
1292
+ }
1293
+ function rejectCredentialedCodexNodeHttpRequest(request) {
1294
+ if (request.method !== "http/request") return;
1295
+ if (!isRecord(request.params)) throw new Error("Codex http/request params must be an object.");
1296
+ const headers = request.params.headers ?? [];
1297
+ if (!Array.isArray(headers)) throw new Error("Codex http/request headers must be an array.");
1298
+ if (headers.length > CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_FIELDS) return createCredentialedCodexNodeHttpRejection(request);
1299
+ let credentialBearing = false;
1300
+ let contentType;
1301
+ for (const header of headers) {
1302
+ if (!isRecord(header) || typeof header.name !== "string" || typeof header.value !== "string") throw new Error("Codex http/request headers must contain string names and values.");
1303
+ const name = header.name.trim().toLowerCase();
1304
+ if (CODEX_NODE_HTTP_CREDENTIAL_HEADER_NAMES.has(name) || CODEX_NODE_HTTP_CREDENTIAL_HEADER_NAME_PATTERN.test(name) || hasSensitiveCodexNodeText(header.value)) credentialBearing = true;
1305
+ if (name === "content-type") {
1306
+ const declared = header.value.split(";", 1)[0]?.trim().toLowerCase();
1307
+ credentialBearing ||= Boolean(contentType && contentType !== declared);
1308
+ contentType = declared;
1309
+ }
1310
+ }
1311
+ if (!credentialBearing && typeof request.params.url === "string") credentialBearing = hasCredentialedCodexNodeHttpUrl(request.params.url);
1312
+ if (!credentialBearing && request.params.bodyBase64 != null) credentialBearing = hasCredentialedCodexNodeHttpBody(request.params.bodyBase64, contentType);
1313
+ return credentialBearing ? createCredentialedCodexNodeHttpRejection(request) : void 0;
1314
+ }
1315
+ function hasCredentialedCodexNodeHttpUrl(value) {
1316
+ let url;
1317
+ try {
1318
+ url = new URL(value);
1319
+ } catch {
1320
+ throw new Error("Codex http/request URL must be valid.");
1321
+ }
1322
+ if (url.username || url.password || hasSensitiveCodexNodeText(value)) return true;
1323
+ let fields = 0;
1324
+ const hasCredentialedParameter = (name, parameterValue) => ++fields > CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_FIELDS || isCodexNodeCredentialField(name) || hasSensitiveCodexNodeText(parameterValue);
1325
+ for (const parameters of [url.searchParams, new URLSearchParams(url.hash.slice(1))]) for (const [name, parameterValue] of parameters) if (hasCredentialedParameter(name, parameterValue)) return true;
1326
+ for (const initial of [
1327
+ url.pathname,
1328
+ url.hash.slice(1),
1329
+ ...url.searchParams.values()
1330
+ ]) {
1331
+ let component = initial;
1332
+ for (let depth = 0; depth <= CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_DEPTH; depth += 1) {
1333
+ for (const nestedQuery of component.split(/[?#]/u).slice(1)) for (const [name, parameterValue] of new URLSearchParams(nestedQuery)) if (hasCredentialedParameter(name, parameterValue)) return true;
1334
+ for (const segment of component.split("/")) for (const parameter of segment.split(";").slice(1)) {
1335
+ const separator = parameter.indexOf("=");
1336
+ if (hasCredentialedParameter(separator < 0 ? parameter : parameter.slice(0, separator), separator < 0 ? "" : parameter.slice(separator + 1))) return true;
1337
+ }
1338
+ if (!/%[\da-f]{2}/iu.test(component)) break;
1339
+ if (depth === CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_DEPTH) return true;
1340
+ try {
1341
+ component = decodeURIComponent(component);
1342
+ } catch {
1343
+ return true;
1344
+ }
1345
+ }
1346
+ }
1347
+ return false;
1348
+ }
1349
+ function hasCredentialedCodexNodeHttpBody(value, contentType) {
1350
+ if (typeof value !== "string") throw new Error("Codex http/request bodyBase64 must be a string.");
1351
+ const declaredText = contentType === "text/plain";
1352
+ if (!declaredText && value.length > Math.ceil(CODEX_NODE_HTTP_CREDENTIAL_BODY_MAX_BYTES / 3) * 4) return true;
1353
+ let body;
1354
+ try {
1355
+ const decoded = Buffer.from(value, "base64");
1356
+ if (!declaredText && decoded.length > CODEX_NODE_HTTP_CREDENTIAL_BODY_MAX_BYTES || decoded.toString("base64") !== value) return true;
1357
+ body = nodeExecServerTextDecoder.decode(decoded);
1358
+ } catch {
1359
+ return true;
1360
+ }
1361
+ if (!body) return false;
1362
+ if (hasSensitiveCodexNodeText(body)) return true;
1363
+ const declaredJson = contentType === "application/json" || contentType?.endsWith("+json") === true;
1364
+ const declaredForm = contentType === "application/x-www-form-urlencoded";
1365
+ if (contentType && !declaredJson && !declaredForm && !declaredText) return true;
1366
+ const trimmed = body.trimStart();
1367
+ if (trimmed.startsWith("<")) return true;
1368
+ if (declaredJson || trimmed.startsWith("{") || trimmed.startsWith("[")) try {
1369
+ if (hasCredentialedCodexNodeRawJsonStrings(body)) return true;
1370
+ return hasCredentialedCodexNodeJsonFields(JSON.parse(body));
1371
+ } catch {
1372
+ if (!declaredText) return true;
1373
+ }
1374
+ if (!declaredForm && !body.includes("=")) return !declaredText;
1375
+ let fields = 0;
1376
+ for (const [name, parameterValue] of new URLSearchParams(body)) if (++fields > CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_FIELDS || isCodexNodeCredentialField(name) || hasSensitiveCodexNodeText(parameterValue)) return true;
1377
+ return false;
1378
+ }
1379
+ function hasCredentialedCodexNodeRawJsonStrings(body) {
1380
+ const tokens = body.matchAll(/("(?:\\.|[^"\\])*")(\s*:)?/gu);
1381
+ let fields = 0;
1382
+ for (const match of tokens) {
1383
+ const value = JSON.parse(match[1]);
1384
+ if (++fields > CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_FIELDS || typeof value !== "string" || hasSensitiveCodexNodeText(value) || match[2] !== void 0 && isCodexNodeCredentialField(value)) return true;
1385
+ }
1386
+ return false;
1387
+ }
1388
+ function hasCredentialedCodexNodeJsonFields(value) {
1389
+ const pending = [{
1390
+ value,
1391
+ depth: 0
1392
+ }];
1393
+ let fields = 0;
1394
+ while (pending.length > 0) {
1395
+ const current = pending.pop();
1396
+ if (typeof current?.value === "string" && hasSensitiveCodexNodeText(current.value)) return true;
1397
+ if (!current || !Array.isArray(current.value) && !isRecord(current.value)) continue;
1398
+ if (current.depth >= CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_DEPTH) return true;
1399
+ const entries = Array.isArray(current.value) ? current.value.map((entry) => [void 0, entry]) : Object.entries(current.value);
1400
+ for (const [name, nested] of entries) {
1401
+ if (++fields > CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_FIELDS || typeof name === "string" && isCodexNodeCredentialField(name)) return true;
1402
+ pending.push({
1403
+ value: nested,
1404
+ depth: current.depth + 1
1405
+ });
1406
+ }
1407
+ }
1408
+ return false;
1409
+ }
1410
+ function hasSensitiveCodexNodeText(value) {
1411
+ let decoded = value;
1412
+ for (let depth = 0; depth <= CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_DEPTH; depth += 1) {
1413
+ if (redactToolPayloadText(decoded) !== decoded) return true;
1414
+ if (!/%[\da-f]{2}/iu.test(decoded)) return false;
1415
+ try {
1416
+ decoded = decodeURIComponent(decoded);
1417
+ } catch {
1418
+ return true;
1419
+ }
1420
+ }
1421
+ return true;
1422
+ }
1423
+ function isCodexNodeCredentialField(value) {
1424
+ let decoded = value;
1425
+ for (let depth = 0; depth < CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_DEPTH; depth += 1) {
1426
+ let next;
1427
+ try {
1428
+ next = decodeURIComponent(decoded);
1429
+ } catch {
1430
+ return true;
1431
+ }
1432
+ if (next === decoded) break;
1433
+ decoded = next;
1434
+ }
1435
+ const normalized = decoded.replace(/[\p{C}\p{Z}\u115F\u1160\u3164\uFFA0+]/gu, "").replace(/([a-z\d])([A-Z])/gu, "$1_$2").replaceAll("-", "_").toLowerCase();
1436
+ return normalized.length > CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_FIELDS || normalized.split(/[.[\]]+/u).some((component) => CODEX_NODE_HTTP_CREDENTIAL_FIELD_NAME_PATTERN.test(component));
1437
+ }
1438
+ function createCredentialedCodexNodeHttpRejection(request) {
1439
+ if (typeof request.id !== "string" && typeof request.id !== "number") throw new Error("Codex http/request must have a JSON-RPC request id.");
1440
+ return Buffer.from(JSON.stringify({
1441
+ jsonrpc: "2.0",
1442
+ id: request.id,
1443
+ error: {
1444
+ code: -32602,
1445
+ message: "Authenticated remote HTTP is unavailable on execution nodes; run on Gateway or use an intentionally credential-free endpoint."
1446
+ }
1447
+ }));
1448
+ }
1449
+ function sanitizeCodexExecServerRequest(frame, request) {
1450
+ if (request.method !== "process/start") return frame;
1451
+ if (!isRecord(request.params)) throw new Error("Codex process/start params must be an object.");
1452
+ sanitizeCodexExecServerEnvironment(request.params, "env");
1453
+ if (request.params.envPolicy !== void 0) {
1454
+ if (!isRecord(request.params.envPolicy)) throw new Error("Codex process/start envPolicy must be an object.");
1455
+ sanitizeCodexExecServerEnvironment(request.params.envPolicy, "set");
1456
+ }
1457
+ return normalizeCodexExecServerFrame(Buffer.from(JSON.stringify(request)));
1458
+ }
1459
+ function sanitizeCodexExecServerEnvironment(record, key) {
1460
+ const environment = record[key];
1461
+ if (environment === void 0) return;
1462
+ if (!isRecord(environment)) throw new Error(`Codex process/start ${key} must be an object.`);
1463
+ const values = {};
1464
+ for (const [name, value] of Object.entries(environment)) {
1465
+ if (typeof value !== "string") throw new Error(`Codex process/start ${key} values must be strings.`);
1466
+ try {
1467
+ const url = new URL(value);
1468
+ if (url.username || url.password) continue;
1469
+ } catch {}
1470
+ values[name] = value;
1471
+ }
1472
+ record[key] = sanitizeEnvVars(values).allowed;
1473
+ }
1474
+ //#endregion
1883
1475
  //#region extensions/codex/src/app-server/sandbox-exec-server-registry.ts
1884
1476
  const sandboxExecServerRegistry = {
1885
1477
  servers: /* @__PURE__ */ new Map(),
1478
+ async close(server) {
1479
+ if (server.closed) return;
1480
+ server.closed = true;
1481
+ if ("node" in server) {
1482
+ for (const lease of server.node.leases.values()) if (!lease.closed) {
1483
+ lease.closed = true;
1484
+ lease.channel.close();
1485
+ }
1486
+ server.node.leases.clear();
1487
+ }
1488
+ for (const client of server.server.clients) client.close(1001, "shutdown");
1489
+ await new Promise((resolve) => {
1490
+ server.server.close(() => resolve());
1491
+ });
1492
+ const failures = (await Promise.allSettled([...server.cleanupTasks, ...[...server.children].map(async (child) => await child.terminate())])).flatMap((result) => result.status === "rejected" ? [result.reason] : []);
1493
+ if (failures.length > 0) throw new AggregateError(failures, "Codex sandbox exec-server child cleanup failed");
1494
+ },
1886
1495
  async closeAll() {
1887
1496
  const servers = await Promise.allSettled(this.servers.values());
1888
1497
  this.servers.clear();
@@ -1890,12 +1499,7 @@ const sandboxExecServerRegistry = {
1890
1499
  if (entry.status !== "fulfilled") return;
1891
1500
  const server = entry.value;
1892
1501
  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
- });
1502
+ await this.close(server);
1899
1503
  }));
1900
1504
  }
1901
1505
  };
@@ -1912,9 +1516,8 @@ var JsonRpcProtocolError = class extends Error {
1912
1516
  this.code = code;
1913
1517
  }
1914
1518
  };
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");
1519
+ /** Parses a normalized JSON message into a JSON-RPC request object. */
1520
+ function parseRequest(text) {
1918
1521
  return requireObject(JSON.parse(text), "JSON-RPC request");
1919
1522
  }
1920
1523
  /** Validates that a JSON value is a non-array object. */
@@ -1954,24 +1557,24 @@ function readHttpHeaders(value) {
1954
1557
  };
1955
1558
  });
1956
1559
  }
1957
- /** Sends a JSON-RPC success response over the WebSocket. */
1958
- function sendResult(socket, id, result) {
1959
- socket.send(JSON.stringify({
1560
+ /** Sends a JSON-RPC success response through the connection message sink. */
1561
+ function sendResult(send, id, result) {
1562
+ send({
1960
1563
  jsonrpc: "2.0",
1961
1564
  id,
1962
- result: result === void 0 ? {} : result
1963
- }));
1565
+ result
1566
+ });
1964
1567
  }
1965
- /** Sends a JSON-RPC error response over the WebSocket. */
1966
- function sendError(socket, id, code, message) {
1967
- socket.send(JSON.stringify({
1568
+ /** Sends a JSON-RPC error response through the connection message sink. */
1569
+ function sendError(send, id, code, message) {
1570
+ send({
1968
1571
  jsonrpc: "2.0",
1969
1572
  id: id ?? null,
1970
1573
  error: {
1971
1574
  code,
1972
1575
  message
1973
1576
  }
1974
- }));
1577
+ });
1975
1578
  }
1976
1579
  //#endregion
1977
1580
  //#region extensions/codex/src/app-server/sandbox-exec-server/path-uri.ts
@@ -2216,20 +1819,6 @@ function joinSandboxChildPath(parent, child) {
2216
1819
  return parent.endsWith("/") ? `${parent}${child}` : `${parent}/${child}`;
2217
1820
  }
2218
1821
  //#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
1822
  //#region extensions/codex/src/app-server/sandbox-exec-server/filesystem.ts
2234
1823
  /**
2235
1824
  * Implements filesystem JSON-RPC handlers for the Codex sandbox exec-server
@@ -2252,7 +1841,7 @@ async function openFile(execServer, handles, params) {
2252
1841
  path: filePath,
2253
1842
  access: "read"
2254
1843
  }]);
2255
- const fsBridge = requireFsBridge(execServer);
1844
+ const fsBridge = execServer.fsBridge;
2256
1845
  const handle = {
2257
1846
  abortController: new AbortController(),
2258
1847
  closeRequested: false,
@@ -2339,7 +1928,7 @@ async function readFile(execServer, params) {
2339
1928
  path: filePath,
2340
1929
  access: "read"
2341
1930
  }]);
2342
- const fsBridge = requireFsBridge(execServer);
1931
+ const fsBridge = execServer.fsBridge;
2343
1932
  const stat = await fsBridge.stat({ filePath });
2344
1933
  if (!stat) throw new JsonRpcProtocolError(JSON_RPC_NOT_FOUND, "file not found");
2345
1934
  assertSandboxFileReadWithinLimit(stat);
@@ -2356,7 +1945,7 @@ async function writeFile(execServer, params) {
2356
1945
  path: filePath,
2357
1946
  access: "write"
2358
1947
  }]);
2359
- const fsBridge = requireFsBridge(execServer);
1948
+ const fsBridge = execServer.fsBridge;
2360
1949
  if ((await fsBridge.stat({ filePath: posix.dirname(filePath) }))?.type !== "directory") throw new JsonRpcProtocolError(JSON_RPC_NOT_FOUND, "parent directory not found");
2361
1950
  await fsBridge.writeFile({
2362
1951
  filePath,
@@ -2372,7 +1961,7 @@ async function createDirectory(execServer, params) {
2372
1961
  path: filePath,
2373
1962
  access: "write"
2374
1963
  }]);
2375
- const fsBridge = requireFsBridge(execServer);
1964
+ const fsBridge = execServer.fsBridge;
2376
1965
  if (record.recursive === false) {
2377
1966
  const parentPath = posix.dirname(filePath);
2378
1967
  if ((await fsBridge.stat({ filePath: parentPath }))?.type !== "directory") throw new JsonRpcProtocolError(JSON_RPC_NOT_FOUND, "parent directory not found");
@@ -2387,7 +1976,7 @@ async function getMetadata(execServer, params) {
2387
1976
  path: filePath,
2388
1977
  access: "read"
2389
1978
  }]);
2390
- const stat = await requireFsBridge(execServer).stat({ filePath });
1979
+ const stat = await execServer.fsBridge.stat({ filePath });
2391
1980
  if (!stat) throw new JsonRpcProtocolError(JSON_RPC_NOT_FOUND, "file not found");
2392
1981
  return metadataResponse(stat);
2393
1982
  }
@@ -2401,11 +1990,9 @@ async function listDirectoryEntries(execServer, filePath, fsSandboxPolicy) {
2401
1990
  path: filePath,
2402
1991
  access: "read"
2403
1992
  }]);
2404
- const fsBridge = requireFsBridge(execServer);
2405
- const backend = requireBackend(execServer);
2406
- const resolved = fsBridge.resolvePath({ filePath });
1993
+ const resolved = execServer.fsBridge.resolvePath({ filePath });
2407
1994
  if (!resolved) throw new Error(`Cannot resolve sandbox path: ${filePath}`);
2408
- const result = await backend.runShellCommand({
1995
+ const result = await execServer.backend.runShellCommand({
2409
1996
  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
1997
  args: [resolved.containerPath],
2411
1998
  allowFailure: true
@@ -2433,7 +2020,7 @@ async function removePath(execServer, params) {
2433
2020
  access: "write"
2434
2021
  }]);
2435
2022
  if (record.recursive !== false) assertNoReadOnlyDescendant(fsSandboxPolicy, filePath, "remove");
2436
- await requireFsBridge(execServer).remove({
2023
+ await execServer.fsBridge.remove({
2437
2024
  filePath,
2438
2025
  recursive: record.recursive !== false,
2439
2026
  force: record.force !== false
@@ -2460,8 +2047,7 @@ async function copyPath(execServer, params) {
2460
2047
  });
2461
2048
  }
2462
2049
  async function copySandboxPath(execServer, params) {
2463
- const fsBridge = execServer.sandbox.fsBridge;
2464
- if (!fsBridge) throw new Error("Sandbox filesystem bridge is unavailable.");
2050
+ const fsBridge = execServer.fsBridge;
2465
2051
  assertResolvedFsSandboxAccess(params.fsSandboxPolicy, [{
2466
2052
  path: params.sourcePath,
2467
2053
  access: "read"
@@ -2519,6 +2105,126 @@ function metadataResponse(stat) {
2519
2105
  };
2520
2106
  }
2521
2107
  //#endregion
2108
+ //#region extensions/codex/src/app-server/sandbox-exec-server/sandbox-child.ts
2109
+ /** Owns one sandbox subprocess tree through close, reaping, and backend finalization. */
2110
+ const SANDBOX_CHILD_TERM_GRACE_MS = 1e3;
2111
+ const SANDBOX_CHILD_REAP_TIMEOUT_MS = 4500;
2112
+ const SANDBOX_EXEC_MARKER = "CODEX_SANDBOX_EXEC_ID";
2113
+ async function spawnSandboxChild(params) {
2114
+ const [command, ...args] = params.argv;
2115
+ const finalize = async (status, exitCode) => await params.finalizeExec?.({
2116
+ status,
2117
+ exitCode,
2118
+ timedOut: false,
2119
+ token: params.finalizeToken
2120
+ });
2121
+ if (!command) {
2122
+ await finalize("failed", null).catch(params.onFinalizeError);
2123
+ throw new Error("OpenClaw sandbox exec spec did not provide a command.");
2124
+ }
2125
+ let child;
2126
+ try {
2127
+ child = spawn(command, args, {
2128
+ detached: process.platform !== "win32",
2129
+ env: params.env,
2130
+ stdio: [
2131
+ "pipe",
2132
+ "pipe",
2133
+ "pipe"
2134
+ ]
2135
+ });
2136
+ } catch (error) {
2137
+ await finalize("failed", null).catch(params.onFinalizeError);
2138
+ throw error;
2139
+ }
2140
+ let outcome;
2141
+ const closed = new Promise((resolve) => {
2142
+ child.once("close", (code, signal) => resolve(outcome = {
2143
+ exitCode: code ?? 1,
2144
+ signal
2145
+ }));
2146
+ });
2147
+ let finalizePromise;
2148
+ let terminationCleanup;
2149
+ let terminationError;
2150
+ const settled = closed.then(async (result) => {
2151
+ await terminationCleanup;
2152
+ child.stdin.destroy();
2153
+ await (finalizePromise ??= finalize(params.finalizeStatus(result), result.exitCode));
2154
+ return result;
2155
+ });
2156
+ settled.catch(params.onFinalizeError);
2157
+ let terminationPromise;
2158
+ const owner = {
2159
+ process: child,
2160
+ settled,
2161
+ terminate: () => terminationPromise ??= (async () => {
2162
+ child.stdin.destroy();
2163
+ terminationCleanup = params.terminateRemote?.().catch((error) => {
2164
+ terminationError = error instanceof Error ? error : new Error(String(error));
2165
+ });
2166
+ await terminationCleanup;
2167
+ if (!outcome) {
2168
+ if (child.pid) killProcessTree(child.pid, {
2169
+ detached: process.platform !== "win32",
2170
+ graceMs: SANDBOX_CHILD_TERM_GRACE_MS
2171
+ });
2172
+ else child.kill("SIGTERM");
2173
+ 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.`);
2174
+ }
2175
+ const result = await settled;
2176
+ if (terminationError) throw terminationError;
2177
+ return result;
2178
+ })()
2179
+ };
2180
+ params.owners.add(owner);
2181
+ settled.then(() => params.owners.delete(owner), () => params.owners.delete(owner));
2182
+ return owner;
2183
+ }
2184
+ function prepareSandboxChildExec(backend, env) {
2185
+ const marker = randomUUID();
2186
+ return {
2187
+ env: {
2188
+ ...env,
2189
+ [SANDBOX_EXEC_MARKER]: marker
2190
+ },
2191
+ terminate: async () => {
2192
+ const result = await backend.runShellCommand({
2193
+ script: SANDBOX_REMOTE_TERMINATE_SCRIPT,
2194
+ args: [`${SANDBOX_EXEC_MARKER}=${marker}`],
2195
+ allowFailure: true,
2196
+ signal: AbortSignal.timeout(SANDBOX_CHILD_REAP_TIMEOUT_MS)
2197
+ });
2198
+ if (result.code !== 0) {
2199
+ const detail = result.stderr.toString("utf8").trim() || result.stdout.toString("utf8").trim();
2200
+ throw new Error(detail || `Sandbox process tree cleanup failed with code ${result.code}; tear down the sandbox environment and inspect surviving processes before retrying.`);
2201
+ }
2202
+ }
2203
+ };
2204
+ }
2205
+ const SANDBOX_REMOTE_TERMINATE_SCRIPT = String.raw`
2206
+ find_owned_pids() {
2207
+ for env_file in /proc/[0-9]*/environ; do
2208
+ if [ -r "$env_file" ] && tr '\0' '\n' < "$env_file" 2>/dev/null | grep -Fqx "$1"; then
2209
+ basename "$(dirname "$env_file")"
2210
+ fi
2211
+ done
2212
+ }
2213
+ owned="$(find_owned_pids "$1")"
2214
+ [ -z "$owned" ] || kill -TERM $owned 2>/dev/null || true
2215
+ sleep 1
2216
+ owned="$(find_owned_pids "$1")"
2217
+ [ -z "$owned" ] || kill -KILL $owned 2>/dev/null || true
2218
+ sleep 1
2219
+ owned="$(find_owned_pids "$1")"
2220
+ [ -z "$owned" ] || { echo "Sandbox process IDs survived SIGKILL: $owned" >&2; exit 1; }
2221
+ `.trim();
2222
+ function delay(ms) {
2223
+ return new Promise((resolve) => {
2224
+ setTimeout(resolve, ms).unref?.();
2225
+ });
2226
+ }
2227
+ //#endregion
2522
2228
  //#region extensions/codex/src/app-server/sandbox-exec-server/http.ts
2523
2229
  /**
2524
2230
  * Implements sandboxed HTTP requests for Codex native tools by routing network
@@ -2527,10 +2233,12 @@ function metadataResponse(stat) {
2527
2233
  /** Maximum JSON-line size accepted from the streaming HTTP helper process. */
2528
2234
  const SANDBOX_HTTP_STREAM_LINE_MAX_CHARS = 256 * 1024;
2529
2235
  /** Handles one sandbox HTTP JSON-RPC request, optionally streaming response body deltas. */
2530
- async function httpRequest(execServer, socket, params) {
2236
+ async function httpRequest(execServer, notifications, params) {
2531
2237
  const record = requireObject(params, "http/request params");
2532
2238
  const requestId = requireString(record.requestId, "requestId");
2533
2239
  const url = requireString(record.url, "url");
2240
+ const redirectPolicy = record.redirectPolicy ?? "follow";
2241
+ if (redirectPolicy !== "follow" && redirectPolicy !== "stop") throw new Error("http/request redirectPolicy must be follow or stop");
2534
2242
  assertSandboxHttpRequestTargetAllowed(url);
2535
2243
  const request = {
2536
2244
  method: requireString(record.method, "method"),
@@ -2538,9 +2246,10 @@ async function httpRequest(execServer, socket, params) {
2538
2246
  headers: readHttpHeaders(record.headers),
2539
2247
  bodyBase64: typeof record.bodyBase64 === "string" ? record.bodyBase64 : void 0,
2540
2248
  timeoutMs: typeof record.timeoutMs === "number" && record.timeoutMs > 0 ? Math.floor(record.timeoutMs) : void 0,
2249
+ redirectPolicy,
2541
2250
  streamResponse: record.streamResponse === true
2542
2251
  };
2543
- if (request.streamResponse) return await runStreamingSandboxHttpRequest(execServer, socket, requestId, request);
2252
+ if (request.streamResponse) return await runStreamingSandboxHttpRequest(execServer, notifications, requestId, request);
2544
2253
  return await runSandboxHttpRequest(execServer, {
2545
2254
  ...request,
2546
2255
  streamResponse: false
@@ -2557,7 +2266,7 @@ function assertSandboxHttpRequestTargetAllowed(url) {
2557
2266
  if (isBlockedHostnameOrIp(parsed.hostname)) throw new SsrFBlockedError(`Blocked hostname or private/internal IP in sandbox http/request: ${parsed.hostname}`);
2558
2267
  }
2559
2268
  async function runSandboxHttpRequest(execServer, params) {
2560
- const result = await requireBackend(execServer).runShellCommand({
2269
+ const result = await execServer.backend.runShellCommand({
2561
2270
  script: SANDBOX_HTTP_REQUEST_SCRIPT,
2562
2271
  stdin: JSON.stringify(params),
2563
2272
  allowFailure: true
@@ -2574,44 +2283,40 @@ async function runSandboxHttpRequest(execServer, params) {
2574
2283
  bodyBase64: typeof parsed.bodyBase64 === "string" ? parsed.bodyBase64 : ""
2575
2284
  };
2576
2285
  }
2577
- async function runStreamingSandboxHttpRequest(execServer, socket, requestId, params) {
2578
- const backend = requireBackend(execServer);
2286
+ async function runStreamingSandboxHttpRequest(execServer, notifications, requestId, params) {
2287
+ const backend = execServer.backend;
2288
+ const remoteExec = prepareSandboxChildExec(backend, {});
2579
2289
  const execSpec = await backend.buildExecSpec({
2580
2290
  command: SANDBOX_HTTP_REQUEST_SCRIPT,
2581
2291
  workdir: execServer.sandbox.containerWorkdir,
2582
- env: {},
2292
+ env: remoteExec.env,
2583
2293
  usePty: false
2584
2294
  });
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
- ]
2295
+ const lifecycle = { failed: false };
2296
+ const owner = await spawnSandboxChild({
2297
+ argv: execSpec.argv,
2298
+ env: execSpec.env,
2299
+ finalizeExec: backend.finalizeExec,
2300
+ finalizeToken: execSpec.finalizeToken,
2301
+ finalizeStatus: (outcome) => lifecycle.failed || outcome.exitCode !== 0 ? "failed" : "completed",
2302
+ onFinalizeError: (error) => {
2303
+ embeddedAgentLog.warn("codex sandbox http/request finalize failed", { error });
2304
+ },
2305
+ owners: execServer.children,
2306
+ terminateRemote: remoteExec.terminate
2307
+ });
2308
+ const child = owner.process;
2309
+ const abortOnSessionClose = () => {
2310
+ lifecycle.failed = true;
2311
+ owner.terminate().catch((error) => {
2312
+ embeddedAgentLog.warn("codex sandbox http/request cleanup failed", { error });
2596
2313
  });
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);
2314
+ };
2315
+ notifications.signal.addEventListener("abort", abortOnSessionClose, { once: true });
2612
2316
  child.once("close", () => {
2613
- socket.off("close", abortOnSocketClose);
2317
+ notifications.signal.removeEventListener("abort", abortOnSessionClose);
2614
2318
  });
2319
+ if (notifications.signal.aborted) abortOnSessionClose();
2615
2320
  child.stdin.on("error", (error) => {
2616
2321
  if (error.code === "EPIPE" || error.code === "ERR_STREAM_DESTROYED") return;
2617
2322
  embeddedAgentLog.warn("codex sandbox http/request stdin write failed", { error });
@@ -2619,10 +2324,10 @@ async function runStreamingSandboxHttpRequest(execServer, socket, requestId, par
2619
2324
  child.stdin.end(JSON.stringify(params));
2620
2325
  return await readStreamingSandboxHttpResponse({
2621
2326
  child,
2622
- execSpec,
2623
- finalizeExec: backend.finalizeExec,
2327
+ lifecycle,
2328
+ owner,
2624
2329
  requestId,
2625
- socket
2330
+ notifications
2626
2331
  });
2627
2332
  }
2628
2333
  function readStreamingSandboxHttpResponse(params) {
@@ -2633,22 +2338,15 @@ function readStreamingSandboxHttpResponse(params) {
2633
2338
  let lastBodySeq = 0;
2634
2339
  let stdoutBuffer = "";
2635
2340
  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) => {
2341
+ const fail = (message, _exitCode) => {
2645
2342
  if (failed) return;
2646
2343
  failed = true;
2647
- finalize("failed", exitCode).catch((error) => {
2648
- embeddedAgentLog.warn("codex sandbox http/request finalize failed", { error });
2344
+ params.lifecycle.failed = true;
2345
+ params.owner.terminate().catch((error) => {
2346
+ embeddedAgentLog.warn("codex sandbox http/request cleanup failed", { error });
2649
2347
  });
2650
2348
  if (headerResolved) {
2651
- sendHttpBodyDelta(params.socket, {
2349
+ if (params.notifications.isOpen()) params.notifications.send("http/request/bodyDelta", {
2652
2350
  requestId: params.requestId,
2653
2351
  seq: lastBodySeq + 1,
2654
2352
  deltaBase64: "",
@@ -2679,7 +2377,7 @@ function readStreamingSandboxHttpResponse(params) {
2679
2377
  } else if (type === "bodyDelta") {
2680
2378
  const seq = requireNumber(message.seq, "http body sequence");
2681
2379
  lastBodySeq = Math.max(lastBodySeq, seq);
2682
- sendHttpBodyDelta(params.socket, {
2380
+ if (params.notifications.isOpen()) params.notifications.send("http/request/bodyDelta", {
2683
2381
  requestId: params.requestId,
2684
2382
  seq,
2685
2383
  deltaBase64: typeof message.deltaBase64 === "string" ? message.deltaBase64 : "",
@@ -2692,10 +2390,7 @@ function readStreamingSandboxHttpResponse(params) {
2692
2390
  }
2693
2391
  newline = stdoutBuffer.indexOf("\n");
2694
2392
  }
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
- }
2393
+ 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
2394
  });
2700
2395
  params.child.stderr.setEncoding("utf8");
2701
2396
  params.child.stderr.on("data", (chunk) => {
@@ -2703,6 +2398,7 @@ function readStreamingSandboxHttpResponse(params) {
2703
2398
  });
2704
2399
  params.child.once("error", (error) => {
2705
2400
  childFailure ??= error.message;
2401
+ params.lifecycle.failed = true;
2706
2402
  });
2707
2403
  params.child.once("close", (code) => {
2708
2404
  const exitCode = code ?? 1;
@@ -2712,10 +2408,10 @@ function readStreamingSandboxHttpResponse(params) {
2712
2408
  return;
2713
2409
  }
2714
2410
  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"));
2411
+ if (!headerResolved) {
2412
+ params.lifecycle.failed = true;
2413
+ reject(/* @__PURE__ */ new Error("sandbox http/request exited before returning headers"));
2414
+ }
2719
2415
  return;
2720
2416
  }
2721
2417
  fail(stderr.trim() || `sandbox http/request failed with code ${exitCode}`, exitCode);
@@ -2847,9 +2543,40 @@ def assert_url_allowed(url):
2847
2543
  PINNED_ADDRESSES[hostname] = sorted(addresses)
2848
2544
 
2849
2545
  class GuardedRedirectHandler(urllib.request.HTTPRedirectHandler):
2546
+ def __init__(self, redirect_policy):
2547
+ self.redirect_policy = redirect_policy
2548
+
2850
2549
  def redirect_request(self, req, fp, code, msg, headers, newurl):
2550
+ if self.redirect_policy == "stop":
2551
+ return None
2851
2552
  assert_url_allowed(newurl)
2852
- return super().redirect_request(req, fp, code, msg, headers, newurl)
2553
+ method = req.get_method()
2554
+ drop_body = code == 303 or (code in (301, 302) and method == "POST")
2555
+ next_headers = dict(req.headers)
2556
+ if drop_body:
2557
+ if method != "HEAD":
2558
+ method = "GET"
2559
+ for name in ("content-type", "content-length", "content-encoding", "transfer-encoding"):
2560
+ next_headers.pop(name.capitalize(), None)
2561
+ redirected = urllib.request.Request(
2562
+ newurl,
2563
+ data=None if drop_body else req.data,
2564
+ headers=next_headers,
2565
+ method=method,
2566
+ origin_req_host=req.origin_req_host,
2567
+ unverifiable=True,
2568
+ )
2569
+ previous = urllib.parse.urlsplit(req.full_url)
2570
+ target = urllib.parse.urlsplit(newurl)
2571
+ previous_port = previous.port or (443 if previous.scheme == "https" else 80)
2572
+ target_port = target.port or (443 if target.scheme == "https" else 80)
2573
+ if (previous.scheme, previous.hostname, previous_port) != (
2574
+ target.scheme, target.hostname, target_port
2575
+ ):
2576
+ # Match Codex's route-aware client: cross-origin hops never inherit secrets.
2577
+ for name in ("authorization", "cookie", "proxy-authorization", "www-authenticate", "cookie2"):
2578
+ redirected.remove_header(name.capitalize())
2579
+ return redirected
2853
2580
 
2854
2581
  def pinned_getaddrinfo(original_getaddrinfo):
2855
2582
  def getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
@@ -2905,7 +2632,8 @@ def main():
2905
2632
  timeout = None
2906
2633
  if isinstance(timeout_ms, (int, float)) and timeout_ms > 0:
2907
2634
  timeout = timeout_ms / 1000
2908
- opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), GuardedRedirectHandler)
2635
+ redirect_handler = GuardedRedirectHandler(input_data.get("redirectPolicy", "follow"))
2636
+ opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), redirect_handler)
2909
2637
  original_getaddrinfo = socket.getaddrinfo
2910
2638
  socket.getaddrinfo = pinned_getaddrinfo(original_getaddrinfo)
2911
2639
  try:
@@ -2921,20 +2649,6 @@ if __name__ == "__main__":
2921
2649
  PY
2922
2650
  python3 "$tmp"
2923
2651
  `.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
2652
  //#endregion
2939
2653
  //#region extensions/codex/src/app-server/sandbox-exec-server/processes.ts
2940
2654
  /**
@@ -2945,13 +2659,14 @@ const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
2945
2659
  const RETAINED_PROCESS_OUTPUT_BYTES = 1024 * 1024;
2946
2660
  const CLOSED_PROCESS_EVICTION_MS = 6e4;
2947
2661
  /** Starts a sandbox-backed process and registers it in the connection-local process table. */
2948
- async function startProcess(execServer, processes, socket, params) {
2662
+ async function startProcess(execServer, processes, notify, params) {
2949
2663
  const record = requireObject(params, "process/start params");
2950
2664
  const processId = requireString(record.processId, "processId");
2951
2665
  if (processes.has(processId)) throw new Error(`process already exists: ${processId}`);
2952
2666
  const argv = requireStringArray(record.argv, "argv");
2953
2667
  const cwd = resolveExecServerPath(requireString(record.cwd, "cwd"), "process cwd");
2954
2668
  rejectUnsupportedArg0(record.arg0);
2669
+ assertSupportedProcessSandbox(execServer, record);
2955
2670
  const env = readProcessEnv(record);
2956
2671
  const managed = {
2957
2672
  processId,
@@ -2964,17 +2679,10 @@ async function startProcess(execServer, processes, socket, params) {
2964
2679
  failure: null,
2965
2680
  tty: record.tty === true,
2966
2681
  pipeStdin: record.pipeStdin === true,
2967
- abortController: new AbortController(),
2682
+ terminationRequested: false,
2968
2683
  child: null,
2969
- finalized: false,
2970
2684
  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
- },
2685
+ emitNotification: notify,
2978
2686
  evictProcess: () => {
2979
2687
  if (managed.evictionTimer) return;
2980
2688
  managed.evictionTimer = setTimeout(() => {
@@ -2984,12 +2692,14 @@ async function startProcess(execServer, processes, socket, params) {
2984
2692
  }
2985
2693
  };
2986
2694
  processes.set(processId, managed);
2695
+ const startPromise = runProcess(execServer, managed, {
2696
+ argv,
2697
+ cwd,
2698
+ env
2699
+ });
2700
+ managed.startPromise = startPromise;
2987
2701
  try {
2988
- await runProcess(execServer, managed, {
2989
- argv,
2990
- cwd,
2991
- env
2992
- });
2702
+ await startPromise;
2993
2703
  } catch (error) {
2994
2704
  processes.delete(processId);
2995
2705
  managed.failure = coerceErrorMessage(error);
@@ -2998,47 +2708,60 @@ async function startProcess(execServer, processes, socket, params) {
2998
2708
  managed.closed = true;
2999
2709
  notifyProcessWaiters(managed);
3000
2710
  throw error;
2711
+ } finally {
2712
+ if (managed.startPromise === startPromise) managed.startPromise = void 0;
3001
2713
  }
3002
- return { processId };
2714
+ return {
2715
+ processId,
2716
+ sandboxType: "none"
2717
+ };
2718
+ }
2719
+ function assertSupportedProcessSandbox(execServer, record) {
2720
+ if (record.networkProxy !== void 0 && record.networkProxy !== null) throw new Error("Codex sandbox exec-server network proxy launch is not supported.");
2721
+ 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.");
2722
+ if (resolveFsSandboxPolicy(execServer, record)?.unrestricted === false) throw new Error("Codex process filesystem sandbox restrictions cannot be enforced by the backend.");
2723
+ if (record.sandbox === void 0 || record.sandbox === null) return;
2724
+ if (requireObject(requireObject(record.sandbox, "process sandbox context").permissions, "process sandbox permissions").network !== "restricted") return;
2725
+ if (!execServer.networkIsolated) throw new Error("Codex network restrictions cannot be enforced by the sandbox backend.");
3003
2726
  }
3004
2727
  async function runProcess(execServer, managed, params) {
3005
- const backend = execServer.sandbox.backend;
3006
- if (!backend) throw new Error("OpenClaw sandbox backend is unavailable.");
2728
+ const backend = execServer.backend;
3007
2729
  throwIfProcessStartCancelled(managed);
2730
+ const remoteExec = prepareSandboxChildExec(backend, params.env);
3008
2731
  const execSpec = await backend.buildExecSpec({
3009
- command: shellCommandFromArgv(params.argv),
2732
+ command: buildRemoteCommand(params.argv),
3010
2733
  workdir: params.cwd,
3011
- env: params.env,
2734
+ env: remoteExec.env,
3012
2735
  usePty: false
3013
2736
  });
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
- ]
2737
+ if (managed.terminationRequested) {
2738
+ await backend.finalizeExec?.({
2739
+ status: "failed",
2740
+ exitCode: null,
2741
+ timedOut: false,
2742
+ token: execSpec.finalizeToken
3028
2743
  });
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", {
2744
+ throw new Error("process start cancelled");
2745
+ }
2746
+ const owner = await spawnSandboxChild({
2747
+ argv: execSpec.argv,
2748
+ env: execSpec.env,
2749
+ finalizeExec: backend.finalizeExec,
2750
+ finalizeToken: execSpec.finalizeToken,
2751
+ finalizeStatus: () => managed.failure ? "failed" : "completed",
2752
+ onFinalizeError: (error) => {
2753
+ const message = coerceErrorMessage(error);
2754
+ managed.failure ??= message;
2755
+ embeddedAgentLog.warn("codex sandbox exec-server finalize failed", {
3033
2756
  processId: managed.processId,
3034
- error: coerceErrorMessage(finalizeError)
2757
+ error: message
3035
2758
  });
3036
- });
3037
- throw error;
3038
- }
3039
- managed.child = child;
3040
- const abortListener = () => child.kill("SIGTERM");
3041
- managed.abortController.signal.addEventListener("abort", abortListener, { once: true });
2759
+ },
2760
+ owners: execServer.children,
2761
+ terminateRemote: remoteExec.terminate
2762
+ });
2763
+ managed.child = owner;
2764
+ const child = owner.process;
3042
2765
  child.stdout.on("data", (chunk) => appendProcessChunk(managed, managed.tty ? "pty" : "stdout", chunk));
3043
2766
  child.stderr.on("data", (chunk) => appendProcessChunk(managed, "stderr", chunk));
3044
2767
  child.once("error", (error) => {
@@ -3046,13 +2769,12 @@ async function runProcess(execServer, managed, params) {
3046
2769
  notifyProcessWaiters(managed);
3047
2770
  });
3048
2771
  child.once("close", (code) => {
3049
- managed.abortController.signal.removeEventListener("abort", abortListener);
3050
2772
  emitProcessClosed(managed, code ?? 1);
3051
2773
  });
3052
2774
  if (!managed.tty && !managed.pipeStdin) child.stdin.end();
3053
2775
  }
3054
2776
  function throwIfProcessStartCancelled(managed) {
3055
- if (managed.abortController.signal.aborted) throw new Error("process start cancelled");
2777
+ if (managed.terminationRequested) throw new Error("process start cancelled");
3056
2778
  }
3057
2779
  function appendProcessChunk(managed, stream, data) {
3058
2780
  if (data.length === 0) return;
@@ -3098,28 +2820,9 @@ function emitProcessClosed(managed, exitCode) {
3098
2820
  seq: closeSeq
3099
2821
  });
3100
2822
  }
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
2823
  managed.evictProcess();
3110
2824
  notifyProcessWaiters(managed);
3111
2825
  }
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
2826
  function limitProcessChunks(chunks, maxBytes) {
3124
2827
  if (!maxBytes) return chunks;
3125
2828
  const retained = [];
@@ -3158,19 +2861,20 @@ function writeProcess(processes, params) {
3158
2861
  const managed = processes.get(processId);
3159
2862
  if (!managed) return { status: "unknownProcess" };
3160
2863
  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);
2864
+ if (!managed.tty && !managed.pipeStdin || managed.closed || !managed.child?.process.stdin.writable) return { status: "stdinClosed" };
2865
+ managed.child.process.stdin.write(chunk);
3163
2866
  return { status: "accepted" };
3164
2867
  }
3165
2868
  /** Requests process termination and reports whether it was running at call time. */
3166
- function terminateProcess(processes, params) {
2869
+ async function terminateProcess(processes, params) {
3167
2870
  const processId = requireString(requireObject(params, "process/terminate params").processId, "processId");
3168
2871
  const managed = processes.get(processId);
3169
2872
  if (!managed) return { running: false };
3170
2873
  const running = !managed.exited;
3171
- managed.abortController.abort();
3172
- managed.child?.kill("SIGTERM");
3173
- if (running && !managed.child) emitProcessClosed(managed, null);
2874
+ managed.terminationRequested = true;
2875
+ await managed.startPromise?.catch(() => void 0);
2876
+ if (managed.child) await managed.child.terminate();
2877
+ else if (running && !managed.closed) emitProcessClosed(managed, null);
3174
2878
  return { running };
3175
2879
  }
3176
2880
  function waitForProcessUpdate(managed, waitMs) {
@@ -3192,12 +2896,6 @@ function notifyProcessWaiters(managed) {
3192
2896
  function hasChunksAtOrAfter(managed, afterSeq) {
3193
2897
  return managed.chunks.some((chunk) => chunk.seq > afterSeq);
3194
2898
  }
3195
- function shellCommandFromArgv(argv) {
3196
- return argv.map(shellEscape).join(" ");
3197
- }
3198
- function shellEscape(value) {
3199
- return `'${value.replaceAll("'", `'"'"'`)}'`;
3200
- }
3201
2899
  function requireProcess(processes, processId) {
3202
2900
  const managed = processes.get(processId);
3203
2901
  if (!managed) throw new Error(`unknown process: ${processId}`);
@@ -3241,47 +2939,148 @@ function readStringList(value) {
3241
2939
  return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
3242
2940
  }
3243
2941
  //#endregion
2942
+ //#region extensions/codex/src/app-server/sandbox-exec-server/session.ts
2943
+ /** Owns the JSON-RPC protocol and resources of one sandbox execution connection. */
2944
+ /** Connection-local execution state; closing it never enables session resumption. */
2945
+ var CodexSandboxExecSession = class {
2946
+ constructor(execServer, transport) {
2947
+ this.execServer = execServer;
2948
+ this.transport = transport;
2949
+ this.processes = /* @__PURE__ */ new Map();
2950
+ this.fileReads = /* @__PURE__ */ new Map();
2951
+ this.closeController = new AbortController();
2952
+ this.notifications = {
2953
+ isOpen: transport.isOpen,
2954
+ signal: this.closeController.signal,
2955
+ send: (method, params) => {
2956
+ if (transport.isOpen()) transport.send({
2957
+ jsonrpc: "2.0",
2958
+ method,
2959
+ params
2960
+ });
2961
+ }
2962
+ };
2963
+ }
2964
+ async handleRequest(request) {
2965
+ const method = request.method;
2966
+ if (!method) {
2967
+ sendError(this.transport.send, request.id, -32600, "Invalid Request");
2968
+ return;
2969
+ }
2970
+ if (request.id === void 0) {
2971
+ if (method !== "initialized") sendError(this.transport.send, -1, -32600, `Unexpected notification: ${method}`);
2972
+ return;
2973
+ }
2974
+ try {
2975
+ const result = await this.dispatchRequest(method, request.params);
2976
+ sendResult(this.transport.send, request.id, result);
2977
+ } catch (error) {
2978
+ sendError(this.transport.send, request.id, error instanceof JsonRpcProtocolError ? error.code : -32603, error instanceof Error ? error.message : String(error));
2979
+ }
2980
+ }
2981
+ close() {
2982
+ if (!this.cleanup) {
2983
+ this.closeController.abort();
2984
+ closeAllFileReads(this.fileReads);
2985
+ this.cleanup = Promise.all([...this.processes.keys()].map(async (processId) => terminateProcess(this.processes, { processId }))).then(() => void 0);
2986
+ }
2987
+ return this.cleanup;
2988
+ }
2989
+ async dispatchRequest(method, params) {
2990
+ switch (method) {
2991
+ case "initialize": return { sessionId: randomUUID() };
2992
+ case "environment/info": return {
2993
+ shell: {
2994
+ name: "sh",
2995
+ path: "/bin/sh"
2996
+ },
2997
+ cwd: pathToFileURL(this.execServer.sandbox.containerWorkdir, { windows: false }).href,
2998
+ capabilities: { networkProxyLaunch: false }
2999
+ };
3000
+ case "environment/status": return { status: "ready" };
3001
+ case "process/start": return startProcess(this.execServer, this.processes, this.notifications.send, params);
3002
+ case "process/read": return await readProcess(this.processes, params);
3003
+ case "process/write": return writeProcess(this.processes, params);
3004
+ case "process/terminate": return await terminateProcess(this.processes, params);
3005
+ case "fs/open": return await openFile(this.execServer, this.fileReads, params);
3006
+ case "fs/readBlock": return readFileBlock(this.fileReads, params);
3007
+ case "fs/close": return closeFile(this.fileReads, params);
3008
+ case "fs/readFile": return await readFile(this.execServer, params);
3009
+ case "fs/writeFile":
3010
+ await writeFile(this.execServer, params);
3011
+ return {};
3012
+ case "fs/createDirectory":
3013
+ await createDirectory(this.execServer, params);
3014
+ return {};
3015
+ case "fs/getMetadata": return await getMetadata(this.execServer, params);
3016
+ case "fs/readDirectory": return await readDirectory(this.execServer, params);
3017
+ case "fs/remove":
3018
+ await removePath(this.execServer, params);
3019
+ return {};
3020
+ case "fs/copy":
3021
+ await copyPath(this.execServer, params);
3022
+ return {};
3023
+ case "http/request": return await httpRequest(this.execServer, this.notifications, params);
3024
+ default: throw new JsonRpcProtocolError(JSON_RPC_METHOD_NOT_FOUND, `Unsupported OpenClaw sandbox exec-server method: ${method}`);
3025
+ }
3026
+ }
3027
+ };
3028
+ //#endregion
3244
3029
  //#region extensions/codex/src/app-server/sandbox-exec-server.ts
3245
3030
  /**
3246
3031
  * Hosts the local OpenClaw sandbox exec-server that Codex app-server native
3247
3032
  * execution can register as an external environment.
3248
3033
  */
3249
3034
  const CODEX_SANDBOX_EXEC_SERVER_MAX_INBOUND_MESSAGE_BYTES = 100 * 1024 * 1024;
3035
+ const CODEX_NODE_EXEC_SERVER_MAX_MESSAGE_BYTES = 64 * 1024 * 1024;
3036
+ const codexNodeExecServerLeases = /* @__PURE__ */ new WeakMap();
3250
3037
  /** Starts or reuses a sandbox exec-server and registers it with Codex app-server. */
3251
3038
  async function ensureCodexSandboxExecServerEnvironment(params) {
3252
- if (!params.sandbox?.enabled || !params.sandbox.backend) return;
3039
+ if (!params.sandbox?.enabled) return;
3040
+ const placementNodeId = readCodexPlacementNodeId(params.sandbox);
3041
+ if (!params.sandbox.backend && !placementNodeId) return;
3042
+ if (placementNodeId && !params.runtime) throw new Error("Codex node execution requires its active plugin runtime.");
3253
3043
  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);
3044
+ const { server: execServer, nodeLease } = await acquireOpenClawExecServer({
3045
+ sandbox: params.sandbox,
3046
+ runtime: params.runtime,
3047
+ signal: params.signal,
3048
+ onExecutionDisconnect: params.onExecutionDisconnect
3049
+ });
3050
+ const environmentId = nodeLease ? `openclaw-node-${nodeLease.id}` : execServer.environmentId;
3255
3051
  try {
3052
+ const execServerUrl = nodeLease ? `${execServer.url}?lease=${nodeLease.id}` : execServer.url;
3256
3053
  await params.client.request("environment/add", {
3257
- environmentId: execServer.environmentId,
3258
- execServerUrl: execServer.url
3054
+ environmentId,
3055
+ execServerUrl
3259
3056
  }, {
3260
3057
  timeoutMs: params.timeoutMs,
3261
3058
  signal: params.signal
3262
3059
  });
3263
3060
  } catch (error) {
3061
+ if (nodeLease && "node" in execServer) closeCodexNodeExecServerLease(execServer, nodeLease);
3264
3062
  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
3063
  throw error;
3270
3064
  }
3271
- return {
3272
- environmentId: execServer.environmentId,
3065
+ const environment = {
3066
+ environmentId,
3273
3067
  cwd: params.sandbox.containerWorkdir
3274
3068
  };
3069
+ if (nodeLease) codexNodeExecServerLeases.set(environment, nodeLease);
3070
+ return environment;
3275
3071
  }
3276
3072
  /** Releases the sandbox exec-server lease associated with a sandbox runtime. */
3277
- async function releaseCodexSandboxExecServerEnvironment(sandbox) {
3073
+ async function releaseCodexSandboxExecServerEnvironment(sandbox, environment) {
3278
3074
  if (!sandbox?.enabled) return;
3279
3075
  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"));
3076
+ if (server) {
3077
+ const nodeLease = environment && codexNodeExecServerLeases.get(environment);
3078
+ if (nodeLease && "node" in server) {
3079
+ codexNodeExecServerLeases.delete(environment);
3080
+ closeCodexNodeExecServerLease(server, nodeLease);
3081
+ }
3082
+ await releaseOpenClawExecServer(server);
3083
+ }
3285
3084
  }
3286
3085
  function canExposeLocalExecServerToAppServer(startOptions) {
3287
3086
  if (!startOptions || startOptions.transport !== "websocket") return true;
@@ -3295,14 +3094,60 @@ function canExposeLocalExecServerToAppServer(startOptions) {
3295
3094
  return false;
3296
3095
  }
3297
3096
  }
3298
- async function acquireOpenClawExecServer(sandbox) {
3097
+ async function acquireOpenClawExecServer(params) {
3098
+ const { sandbox, runtime, signal, onExecutionDisconnect } = params;
3299
3099
  const key = sandbox.runtimeId;
3300
3100
  while (true) {
3301
3101
  const promise = sandboxExecServerRegistry.servers.get(key) ?? startAndRememberOpenClawExecServer(sandbox);
3302
3102
  const server = await promise;
3303
3103
  if (!server.closed && sandboxExecServerRegistry.servers.get(key) === promise) {
3304
3104
  server.refCount += 1;
3305
- return server;
3105
+ if (!("node" in server)) return { server };
3106
+ if (!runtime || !signal) {
3107
+ await releaseOpenClawExecServer(server);
3108
+ throw new Error("Codex node execution requires an active runtime and attempt.");
3109
+ }
3110
+ try {
3111
+ const placementIdentity = readCodexPlacementWorkspaceIdentity(sandbox);
3112
+ const channel = await runtime.nodes.openDuplex({
3113
+ nodeId: server.node.id,
3114
+ command: "codex.exec-server.stdio.v1",
3115
+ params: {
3116
+ cwd: sandbox.containerWorkdir,
3117
+ ...placementIdentity
3118
+ },
3119
+ sessionKey: sandbox.sessionKey,
3120
+ timeoutMs: 0,
3121
+ maxMessageBytes: CODEX_NODE_EXEC_SERVER_MAX_MESSAGE_BYTES,
3122
+ maxOutstandingDeliveryBytes: 69206016,
3123
+ signal
3124
+ });
3125
+ if (signal.aborted || server.closed || sandboxExecServerRegistry.servers.get(key) !== promise) {
3126
+ channel.close();
3127
+ throw new Error("Codex node execution retired before its channel was ready.");
3128
+ }
3129
+ const nodeLease = {
3130
+ id: randomUUID(),
3131
+ channel,
3132
+ claimed: false,
3133
+ closed: false,
3134
+ onDisconnected: onExecutionDisconnect
3135
+ };
3136
+ server.node.leases.set(nodeLease.id, nodeLease);
3137
+ channel.closed.then(() => handleClosedCodexNodeExecServerLease(server, nodeLease, { failed: false }), (error) => handleClosedCodexNodeExecServerLease(server, nodeLease, {
3138
+ failed: true,
3139
+ error
3140
+ })).catch((error) => {
3141
+ embeddedAgentLog.warn("codex paired-device exec-server lease cleanup failed", { error: error instanceof Error ? error.message : String(error) });
3142
+ });
3143
+ return {
3144
+ server,
3145
+ nodeLease
3146
+ };
3147
+ } catch (error) {
3148
+ await releaseOpenClawExecServer(server);
3149
+ throw error;
3150
+ }
3306
3151
  }
3307
3152
  }
3308
3153
  }
@@ -3316,24 +3161,55 @@ function startAndRememberOpenClawExecServer(sandbox) {
3316
3161
  return created;
3317
3162
  }
3318
3163
  async function startOpenClawExecServer(sandbox) {
3164
+ const backend = sandbox.backend;
3165
+ const fsBridge = sandbox.fsBridge;
3166
+ const placementNodeId = readCodexPlacementNodeId(sandbox);
3167
+ let connection;
3168
+ if (placementNodeId) connection = {
3169
+ kind: "node",
3170
+ id: placementNodeId
3171
+ };
3172
+ else {
3173
+ if (!backend) throw new Error("OpenClaw sandbox backend is unavailable.");
3174
+ if (!fsBridge) throw new Error("Sandbox filesystem bridge is unavailable.");
3175
+ connection = {
3176
+ kind: "sandbox",
3177
+ backend,
3178
+ fsBridge
3179
+ };
3180
+ }
3319
3181
  const server = new WebSocketServer({
3320
3182
  host: "127.0.0.1",
3321
3183
  port: 0,
3322
- maxPayload: CODEX_SANDBOX_EXEC_SERVER_MAX_INBOUND_MESSAGE_BYTES
3184
+ maxPayload: connection.kind === "node" ? CODEX_NODE_EXEC_SERVER_MAX_MESSAGE_BYTES : CODEX_SANDBOX_EXEC_SERVER_MAX_INBOUND_MESSAGE_BYTES
3323
3185
  });
3324
3186
  await once(server, "listening");
3325
3187
  const address = server.address();
3326
3188
  if (!address || typeof address === "string") throw new Error("OpenClaw Codex exec-server did not bind to a TCP port.");
3327
3189
  const environmentId = buildEnvironmentId(sandbox);
3328
3190
  const authPath = `/openclaw-${randomUUID()}`;
3329
- const execServer = {
3191
+ const common = {
3330
3192
  authPath,
3331
3193
  closed: false,
3332
3194
  environmentId,
3333
3195
  refCount: 0,
3334
3196
  url: `ws://127.0.0.1:${address.port}${authPath}`,
3335
3197
  sandbox,
3336
- server
3198
+ server,
3199
+ children: /* @__PURE__ */ new Set(),
3200
+ cleanupTasks: /* @__PURE__ */ new Set()
3201
+ };
3202
+ const execServer = connection.kind === "node" ? {
3203
+ ...common,
3204
+ node: {
3205
+ id: connection.id,
3206
+ leases: /* @__PURE__ */ new Map()
3207
+ }
3208
+ } : {
3209
+ ...common,
3210
+ backend: connection.backend,
3211
+ fsBridge: connection.fsBridge,
3212
+ networkIsolated: (connection.backend.id === "docker" || connection.backend.id === "podman") && sandbox.docker.network.trim().toLowerCase() === "none"
3337
3213
  };
3338
3214
  server.on("connection", (socket, request) => {
3339
3215
  socket.on("error", handleExecServerSocketError);
@@ -3341,6 +3217,10 @@ async function startOpenClawExecServer(sandbox) {
3341
3217
  socket.close(1008, "unauthorized");
3342
3218
  return;
3343
3219
  }
3220
+ if ("node" in execServer) {
3221
+ handleNodeConnection(execServer, socket, request);
3222
+ return;
3223
+ }
3344
3224
  handleConnection(execServer, socket);
3345
3225
  });
3346
3226
  embeddedAgentLog.info("codex sandbox exec-server started", {
@@ -3357,15 +3237,7 @@ async function releaseOpenClawExecServer(execServer) {
3357
3237
  const current = await sandboxExecServerRegistry.servers.get(execServer.sandbox.runtimeId)?.catch(() => void 0);
3358
3238
  if (execServer.refCount > 0 || execServer.closed) return;
3359
3239
  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
- });
3240
+ await sandboxExecServerRegistry.close(execServer);
3369
3241
  }
3370
3242
  function buildEnvironmentId(sandbox) {
3371
3243
  return `openclaw-sandbox-${createHash("sha256").update(sandbox.runtimeId).digest("hex").slice(0, 16)}`;
@@ -3373,80 +3245,82 @@ function buildEnvironmentId(sandbox) {
3373
3245
  function isAuthorizedExecServerRequest(execServer, request) {
3374
3246
  return new URL(request.url ?? "", "ws://127.0.0.1").pathname === execServer.authPath;
3375
3247
  }
3248
+ function readCodexPlacementNodeId(sandbox) {
3249
+ if (!("placementExecutionMode" in sandbox) || sandbox.placementExecutionMode !== "remote-exec" || !("placementNodeId" in sandbox) || typeof sandbox.placementNodeId !== "string" || !sandbox.placementNodeId) return;
3250
+ return sandbox.placementNodeId;
3251
+ }
3252
+ function readCodexPlacementWorkspaceIdentity(sandbox) {
3253
+ 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.");
3254
+ return {
3255
+ environmentId: sandbox.placementEnvironmentId,
3256
+ sessionId: sandbox.placementSessionId,
3257
+ ownerEpoch: sandbox.placementOwnerEpoch,
3258
+ sessionKey: sandbox.sessionKey
3259
+ };
3260
+ }
3261
+ function handleNodeConnection(execServer, socket, request) {
3262
+ const leaseId = new URL(request.url ?? "", "ws://127.0.0.1").searchParams.get("lease");
3263
+ const lease = leaseId ? execServer.node.leases.get(leaseId) : void 0;
3264
+ if (!lease || lease.claimed || lease.closed) {
3265
+ socket.close(1008, "execution channel unavailable");
3266
+ return;
3267
+ }
3268
+ lease.claimed = true;
3269
+ const cleanup = startCodexNodeExecServerRelay({
3270
+ lease,
3271
+ socket
3272
+ });
3273
+ execServer.cleanupTasks.add(cleanup);
3274
+ cleanup.then(() => execServer.cleanupTasks.delete(cleanup), (error) => {
3275
+ execServer.cleanupTasks.delete(cleanup);
3276
+ embeddedAgentLog.warn("codex paired-device exec-server relay failed", { error: error instanceof Error ? error.message : String(error) });
3277
+ });
3278
+ }
3279
+ function closeCodexNodeExecServerLease(execServer, lease) {
3280
+ execServer.node.leases.delete(lease.id);
3281
+ if (!lease.closed) {
3282
+ lease.closed = true;
3283
+ lease.closeRelay?.();
3284
+ lease.channel.close();
3285
+ }
3286
+ }
3287
+ function handleClosedCodexNodeExecServerLease(execServer, lease, result) {
3288
+ if (lease.closed) return;
3289
+ if (lease.onChannelClosed) {
3290
+ lease.onChannelClosed(result);
3291
+ return;
3292
+ }
3293
+ try {
3294
+ lease.onDisconnected?.(createCodexNodeExecServerDisconnectError(result.failed ? "execution node failed" : "execution node disconnected", result.error));
3295
+ } finally {
3296
+ closeCodexNodeExecServerLease(execServer, lease);
3297
+ }
3298
+ }
3376
3299
  function handleConnection(execServer, socket) {
3377
- const processes = /* @__PURE__ */ new Map();
3378
- const fileReads = /* @__PURE__ */ new Map();
3300
+ const session = new CodexSandboxExecSession(execServer, {
3301
+ isOpen: () => socket.readyState === socket.OPEN,
3302
+ send: (message) => socket.send(JSON.stringify(message))
3303
+ });
3379
3304
  socket.on("message", (data) => {
3380
- handleMessage(execServer, processes, fileReads, socket, data).catch((error) => {
3305
+ handleMessage(session, data).catch((error) => {
3381
3306
  embeddedAgentLog.warn("codex sandbox exec-server message failed", { error });
3382
3307
  });
3383
3308
  });
3384
3309
  socket.on("close", () => {
3385
- closeAllFileReads(fileReads);
3386
- for (const process of processes.values()) process.abortController.abort();
3310
+ const cleanup = session.close();
3311
+ execServer.cleanupTasks.add(cleanup);
3312
+ cleanup.then(() => execServer.cleanupTasks.delete(cleanup), (error) => {
3313
+ execServer.cleanupTasks.delete(cleanup);
3314
+ embeddedAgentLog.warn("codex sandbox exec-server socket cleanup failed", { error: error instanceof Error ? error.message : String(error) });
3315
+ });
3387
3316
  });
3388
3317
  }
3389
3318
  function handleExecServerSocketError(error) {
3390
3319
  embeddedAgentLog.debug("codex sandbox exec-server websocket failed", { error });
3391
3320
  }
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
- }
3321
+ async function handleMessage(session, data) {
3322
+ const buffer = Array.isArray(data) ? Buffer.concat(data) : Buffer.isBuffer(data) ? data : Buffer.from(data);
3323
+ await session.handleRequest(parseRequest(buffer.toString("utf8")));
3450
3324
  }
3451
3325
  //#endregion
3452
3326
  //#region extensions/codex/src/app-server/provider-capabilities.ts
@@ -3495,87 +3369,6 @@ async function resolveCodexProviderWebSearchSupport(params) {
3495
3369
  }
3496
3370
  }
3497
3371
  //#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
3372
  //#region extensions/codex/src/app-server/approval-bridge.ts
3580
3373
  /**
3581
3374
  * Bridges Codex app-server approval requests into OpenClaw policy hooks and
@@ -3600,19 +3393,55 @@ async function handleCodexAppServerApprovalRequest(params) {
3600
3393
  requestParams,
3601
3394
  paramsForRun: params.paramsForRun
3602
3395
  });
3603
- const resolvePolicyApproval = (outcome, message = approvalResolutionMessage(outcome)) => {
3396
+ if (params.signal?.aborted) {
3397
+ recordNativeToolFailureDisposition(params, context, "cancelled");
3398
+ return buildApprovalResponse(params.method, context.requestParams, "cancelled");
3399
+ }
3400
+ let revalidateMutableFileApproval;
3401
+ let mutableFileApprovalRequiresOneShot = false;
3402
+ const resolvePolicyApproval = async (outcome, message = approvalResolutionMessage(outcome), approvalId) => {
3403
+ let resolvedOutcome = outcome;
3404
+ let resolvedMessage = message;
3405
+ if (outcome !== "denied" && revalidateMutableFileApproval) {
3406
+ const binding = await revalidateMutableFileApproval();
3407
+ if (!binding.ok) {
3408
+ resolvedOutcome = "denied";
3409
+ resolvedMessage = binding.message;
3410
+ }
3411
+ }
3412
+ if (resolvedOutcome === "approved-session" && mutableFileApprovalRequiresOneShot) {
3413
+ resolvedOutcome = "approved-once";
3414
+ resolvedMessage = "Codex app-server approval granted for this byte-bound command only.";
3415
+ }
3604
3416
  emitApprovalEvent(params.paramsForRun, {
3605
3417
  phase: "resolved",
3606
3418
  kind: context.kind,
3607
- status: outcome === "denied" ? "denied" : "approved",
3419
+ status: resolvedOutcome === "denied" ? "denied" : "approved",
3608
3420
  title: context.title,
3421
+ ...approvalId ? {
3422
+ approvalId,
3423
+ approvalSlug: approvalId
3424
+ } : {},
3609
3425
  ...context.eventDetails,
3610
- ...approvalEventScope(params.method, outcome),
3611
- message
3426
+ ...approvalEventScope(params.method, resolvedOutcome),
3427
+ message: resolvedMessage
3612
3428
  });
3613
- return buildApprovalResponse(params.method, context.requestParams, outcome);
3429
+ return buildApprovalResponse(params.method, context.requestParams, resolvedOutcome);
3614
3430
  };
3615
3431
  try {
3432
+ if (params.method === "item/commandExecution/requestApproval" && !readNetworkApprovalContext(requestParams)) {
3433
+ const command = readPolicyCommand(requestParams);
3434
+ const cwd = readStringField(requestParams, "cwd") ?? params.paramsForRun.workspaceDir;
3435
+ const prepareMutableFileApproval = params.paramsForRun.hostCapabilities.prepareMutableFileApproval;
3436
+ if (!prepareMutableFileApproval) return await resolvePolicyApproval("denied", "SYSTEM_RUN_DENIED: mutable file approval binding is unavailable");
3437
+ const prepared = await prepareMutableFileApproval({
3438
+ command: command ?? "",
3439
+ cwd
3440
+ });
3441
+ if (!prepared.ok) return await resolvePolicyApproval("denied", prepared.message);
3442
+ mutableFileApprovalRequiresOneShot = prepared.requiresOneShot;
3443
+ revalidateMutableFileApproval = prepared.revalidate;
3444
+ }
3616
3445
  const policyOutcome = await runOpenClawToolPolicyForApprovalRequest({
3617
3446
  method: params.method,
3618
3447
  requestParams,
@@ -3624,19 +3453,22 @@ async function handleCodexAppServerApprovalRequest(params) {
3624
3453
  });
3625
3454
  if (policyOutcome?.outcome === "denied") {
3626
3455
  recordNativeToolFailureDisposition(params, context, policyOutcome.failureDisposition);
3627
- return resolvePolicyApproval("denied", policyOutcome.reason);
3456
+ return await resolvePolicyApproval("denied", policyOutcome.reason);
3628
3457
  }
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.");
3458
+ if (policyOutcome?.outcome === "approved-once" || policyOutcome?.outcome === "approved-session") return await resolvePolicyApproval(policyOutcome.outcome);
3459
+ 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
3460
  const requestResult = await requestPluginApproval({
3634
3461
  hostCapabilities: params.paramsForRun.hostCapabilities,
3635
3462
  title: context.title,
3636
3463
  description: context.description,
3637
3464
  severity: context.severity,
3638
3465
  toolName: context.toolName,
3639
- toolCallId: context.approvalId
3466
+ toolCallId: context.approvalId,
3467
+ allowedDecisions: nativeApprovalAllowedDecisions({
3468
+ method: params.method,
3469
+ requestParams,
3470
+ requiresOneShot: mutableFileApprovalRequiresOneShot
3471
+ })
3640
3472
  });
3641
3473
  const approvalId = requestResult?.id;
3642
3474
  if (!approvalId) {
@@ -3662,16 +3494,17 @@ async function handleCodexAppServerApprovalRequest(params) {
3662
3494
  ...context.eventDetails,
3663
3495
  message: "Codex app-server approval requested."
3664
3496
  });
3665
- const requestUnavailable = approvalRequestExplicitlyUnavailable(requestResult);
3666
- const decision = requestUnavailable ? null : await waitForPluginApprovalDecision({
3497
+ const approvalResult = approvalRequestExplicitlyUnavailable(requestResult) ? void 0 : await waitForPluginApprovalDecision({
3667
3498
  approvalId,
3668
3499
  signal: params.signal,
3669
3500
  hostCapabilities: params.paramsForRun.hostCapabilities
3670
3501
  });
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");
3502
+ const approvalTimedOut = !params.signal?.aborted && approvalResult?.terminalReason === "timeout";
3503
+ const outcome = params.signal?.aborted ? "cancelled" : mapExecDecisionToOutcome(approvalResult?.decision);
3504
+ if (approvalTimedOut) recordNativeToolFailureDisposition(params, context, "timed_out", context.approvalKind);
3505
+ else if (outcome === "cancelled") recordNativeToolFailureDisposition(params, context, params.signal?.aborted ? resolveCodexToolAbortTerminalReason(params.signal) : "cancelled");
3506
+ else if (outcome === "unavailable") recordNativeToolFailureDisposition(params, context, "failed");
3507
+ if (outcome === "approved-once" || outcome === "approved-session") return await resolvePolicyApproval(outcome, approvalResolutionMessage(outcome), approvalId);
3675
3508
  emitApprovalEvent(params.paramsForRun, {
3676
3509
  phase: "resolved",
3677
3510
  kind: context.kind,
@@ -3681,7 +3514,7 @@ async function handleCodexAppServerApprovalRequest(params) {
3681
3514
  approvalSlug: approvalId,
3682
3515
  ...context.eventDetails,
3683
3516
  ...approvalEventScope(params.method, outcome),
3684
- message: approvalResolutionMessage(outcome)
3517
+ message: approvalTimedOut ? codexApprovalTimeoutText(context.approvalKind) : approvalResolutionMessage(outcome)
3685
3518
  });
3686
3519
  return buildApprovalResponse(params.method, context.requestParams, outcome);
3687
3520
  } catch (error) {
@@ -3699,10 +3532,11 @@ async function handleCodexAppServerApprovalRequest(params) {
3699
3532
  return buildApprovalResponse(params.method, context.requestParams, cancelled ? "cancelled" : "denied");
3700
3533
  }
3701
3534
  }
3702
- function recordNativeToolFailureDisposition(params, context, disposition) {
3535
+ function recordNativeToolFailureDisposition(params, context, disposition, approvalKind) {
3703
3536
  if (!context.itemId || !disposition) return;
3704
3537
  try {
3705
- params.onNativeToolFailureDisposition?.(context.itemId, params.signal?.aborted ? resolveCodexToolAbortTerminalReason(params.signal) : disposition);
3538
+ const resolvedDisposition = params.signal?.aborted ? resolveCodexToolAbortTerminalReason(params.signal) : disposition;
3539
+ params.onNativeToolFailureDisposition?.(context.itemId, resolvedDisposition, ...resolvedDisposition === "timed_out" && approvalKind ? [approvalKind] : []);
3706
3540
  } catch {}
3707
3541
  }
3708
3542
  /** Converts an OpenClaw approval outcome into the app-server method response. */
@@ -3719,7 +3553,10 @@ function buildApprovalResponse(method, requestParams, outcome) {
3719
3553
  scope: "turn"
3720
3554
  };
3721
3555
  }
3722
- return unsupportedApprovalResponse();
3556
+ return {
3557
+ decision: "decline",
3558
+ reason: "OpenClaw codex app-server bridge does not grant native approvals yet."
3559
+ };
3723
3560
  }
3724
3561
  function matchesCurrentTurn(requestParams, threadId, turnId) {
3725
3562
  if (!requestParams) return false;
@@ -3735,20 +3572,19 @@ function buildApprovalContext(params) {
3735
3572
  const reasonPreview = sanitizeApprovalPreview(readStringPreview(params.requestParams, "reason"), 180);
3736
3573
  const command = commandPreview.text;
3737
3574
  const reason = reasonPreview.text;
3738
- const kind = approvalKindForMethod(params.method);
3575
+ const networkApproval = params.method === "item/commandExecution/requestApproval" ? readNetworkApprovalContext(params.requestParams) : void 0;
3576
+ const approvalKind = params.method.includes("commandExecution") ? "command" : params.method.includes("fileChange") ? "file-change" : params.method.includes("permissions") ? "permissions" : "other";
3577
+ const kind = approvalKind === "command" ? "exec" : approvalKind === "other" ? "unknown" : "plugin";
3739
3578
  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}`);
3579
+ 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";
3580
+ 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
3581
  return {
3582
+ approvalKind,
3743
3583
  kind,
3744
3584
  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"),
3585
+ description: permissionLines.length > 0 ? joinDescriptionLinesWithinLimit(permissionLines, PERMISSION_DESCRIPTION_MAX_LENGTH) : [subject, ...commandDetailLines].join("\n"),
3750
3586
  severity: kind === "exec" ? "warning" : "info",
3751
- toolName: kind === "exec" ? "codex_command_approval" : params.method === "item/permissions/requestApproval" ? "codex_permission_approval" : "codex_file_approval",
3587
+ toolName: networkApproval ? "codex_network_approval" : kind === "exec" ? "codex_command_approval" : params.method === "item/permissions/requestApproval" ? "codex_permission_approval" : "codex_file_approval",
3752
3588
  itemId,
3753
3589
  approvalId,
3754
3590
  requestParams: params.requestParams,
@@ -3920,6 +3756,10 @@ function sanitizeRelayDecisionReason(value) {
3920
3756
  }
3921
3757
  function buildOpenClawToolPolicyRequest(method, requestParams) {
3922
3758
  if (method === "item/commandExecution/requestApproval") {
3759
+ if (readNetworkApprovalContext(requestParams)) return {
3760
+ toolName: "codex_network_approval",
3761
+ params: { approval: requestParams ?? {} }
3762
+ };
3923
3763
  const command = readPolicyCommand(requestParams);
3924
3764
  return {
3925
3765
  toolName: "exec",
@@ -3960,14 +3800,27 @@ function stableJsonText(value) {
3960
3800
  }
3961
3801
  }
3962
3802
  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");
3803
+ if (outcome === "cancelled") return "cancel";
3804
+ if (outcome === "denied" || outcome === "unavailable") return "decline";
3805
+ const capabilities = commandApprovalCapabilities(requestParams);
3806
+ if (outcome === "approved-session" && capabilities.sessionDecision !== void 0) return capabilities.sessionDecision;
3807
+ return capabilities.once ? "accept" : "decline";
3808
+ }
3809
+ function nativeApprovalAllowedDecisions(params) {
3810
+ if (params.method === "item/fileChange/requestApproval") return [
3811
+ "allow-once",
3812
+ "allow-always",
3813
+ "deny"
3814
+ ];
3815
+ if (params.method !== "item/commandExecution/requestApproval") return;
3816
+ const available = params.requestParams?.availableDecisions;
3817
+ if (!Array.isArray(available)) return;
3818
+ const capabilities = commandApprovalCapabilities(params.requestParams);
3819
+ const decisions = [];
3820
+ if (capabilities.once) decisions.push("allow-once");
3821
+ if (!params.requiresOneShot && capabilities.sessionDecision !== void 0) decisions.push("allow-always");
3822
+ decisions.push("deny");
3823
+ return decisions;
3971
3824
  }
3972
3825
  function fileChangeApprovalDecision(outcome) {
3973
3826
  if (outcome === "cancelled") return "cancel";
@@ -3981,12 +3834,6 @@ function requestedPermissions(requestParams) {
3981
3834
  if (isJsonObject(permissions.fileSystem)) granted.fileSystem = permissions.fileSystem;
3982
3835
  return granted;
3983
3836
  }
3984
- function unsupportedApprovalResponse() {
3985
- return {
3986
- decision: "decline",
3987
- reason: "OpenClaw codex app-server bridge does not grant native approvals yet."
3988
- };
3989
- }
3990
3837
  function describeRequestedPermissions(requestParams) {
3991
3838
  return describePermissionProfile(requestedPermissions(requestParams), "Permissions");
3992
3839
  }
@@ -4154,43 +4001,38 @@ function isPrivateNetworkHostPattern(value) {
4154
4001
  if (wildcardStripped.startsWith("10.") || wildcardStripped.startsWith("127.") || wildcardStripped.startsWith("192.168.") || wildcardStripped.startsWith("169.254.")) return true;
4155
4002
  return /^172\.(1[6-9]|2\d|3[0-1])\./.test(wildcardStripped);
4156
4003
  }
4157
- function hasAvailableDecision(requestParams, decision) {
4004
+ function commandApprovalCapabilities(requestParams) {
4158
4005
  const available = requestParams?.availableDecisions;
4159
- if (!Array.isArray(available)) return true;
4160
- return available.includes(decision);
4006
+ if (!Array.isArray(available)) return {
4007
+ once: true,
4008
+ sessionDecision: "acceptForSession"
4009
+ };
4010
+ return {
4011
+ once: available.includes("accept"),
4012
+ ...available.includes("acceptForSession") ? { sessionDecision: "acceptForSession" } : { sessionDecision: findAvailableCommandAmendmentDecision(requestParams) }
4013
+ };
4161
4014
  }
4162
4015
  function findAvailableCommandAmendmentDecision(requestParams) {
4163
4016
  const available = requestParams?.availableDecisions;
4164
4017
  if (!Array.isArray(available)) return;
4165
4018
  return available.find((entry) => isJsonObject(entry) && (isJsonObject(entry.acceptWithExecpolicyAmendment) || isJsonObject(entry.applyNetworkPolicyAmendment)));
4166
4019
  }
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
4020
  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.";
4021
+ return {
4022
+ "approved-session": "Codex app-server approval granted for the session.",
4023
+ "approved-once": "Codex app-server approval granted for this turn.",
4024
+ cancelled: "Codex app-server approval cancelled.",
4025
+ unavailable: "Codex app-server approval unavailable.",
4026
+ denied: "Codex app-server approval denied."
4027
+ }[outcome];
4181
4028
  }
4182
4029
  function approvalEventScope(method, outcome) {
4183
4030
  return method === "item/permissions/requestApproval" ? { scope: outcome === "approved-session" ? "session" : "turn" } : {};
4184
4031
  }
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
4032
  function emitApprovalEvent(params, data) {
4191
4033
  params.onAgentEvent?.({
4192
4034
  stream: "approval",
4193
- data
4035
+ data: { ...data }
4194
4036
  });
4195
4037
  }
4196
4038
  function readDisplayCommandPreview(record) {
@@ -4205,6 +4047,15 @@ function readPolicyCommand(record) {
4205
4047
  const actionCommands = readCommandActions(record);
4206
4048
  if (actionCommands.length > 0) return actionCommands.join(" && ");
4207
4049
  }
4050
+ function readNetworkApprovalContext(record) {
4051
+ const context = isJsonObject(record?.networkApprovalContext) ? record.networkApprovalContext : void 0;
4052
+ const host = readStringField(context, "host");
4053
+ const protocol = readStringField(context, "protocol");
4054
+ return host && protocol ? {
4055
+ host,
4056
+ protocol
4057
+ } : void 0;
4058
+ }
4208
4059
  function readCommandActions(record) {
4209
4060
  const actions = record?.commandActions;
4210
4061
  if (!Array.isArray(actions)) return [];
@@ -4389,11 +4240,12 @@ const MAX_DISPLAY_VALUE_ARRAY_ITEMS = 8;
4389
4240
  const MAX_DISPLAY_VALUE_OBJECT_KEYS = 8;
4390
4241
  const MAX_DISPLAY_VALUE_DEPTH = 3;
4391
4242
  const DISPLAY_TEXT_SCAN_MAX_LENGTH = 4096;
4392
- async function handleCodexAppServerElicitationRequest(params) {
4243
+ async function routeCodexAppServerElicitationRequest(params) {
4393
4244
  const requestParams = isJsonObject(params.requestParams) ? params.requestParams : void 0;
4394
- if (!requestParams || readNonBlankStringField(requestParams, "threadId") !== params.threadId) return;
4245
+ if (!requestParams || readNonBlankStringField(requestParams, "threadId") !== params.threadId) return { kind: "not-mine" };
4395
4246
  const requestTurnId = requestParams.turnId;
4396
- if (requestTurnId !== null && requestTurnId !== void 0 && requestTurnId !== params.turnId) return;
4247
+ if (requestTurnId !== null && requestTurnId !== void 0 && requestTurnId !== params.turnId) return { kind: "not-mine" };
4248
+ 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
4249
  const pluginResolution = resolvePluginElicitation({
4398
4250
  requestParams,
4399
4251
  pluginAppPolicyContext: params.pluginAppPolicyContext
@@ -4401,32 +4253,38 @@ async function handleCodexAppServerElicitationRequest(params) {
4401
4253
  if (pluginResolution.kind !== "not_plugin") {
4402
4254
  if (params.paramsForRun.trigger === "cron" && params.paramsForRun.scheduledRuntimeAuthority) {
4403
4255
  logPluginElicitationDecline("scheduled_authority_non_interactive", requestParams);
4404
- return declineElicitationResponse();
4256
+ return handled(createCodexElicitationResponse("decline"));
4405
4257
  }
4406
4258
  if (pluginResolution.kind === "decline") {
4407
4259
  logPluginElicitationDecline(pluginResolution.reason, requestParams);
4408
- return declineElicitationResponse();
4260
+ return handled(createCodexElicitationResponse("decline"));
4409
4261
  }
4410
4262
  if (requestTurnId !== params.turnId) {
4411
4263
  logPluginElicitationDecline("missing_active_turn", requestParams);
4412
- return declineElicitationResponse();
4264
+ return handled(createCodexElicitationResponse("decline"));
4413
4265
  }
4414
- return await buildPluginPolicyElicitationResponse({
4266
+ return handled(await buildPluginPolicyElicitationResponse({
4415
4267
  entry: pluginResolution.entry,
4416
4268
  requestParams,
4417
4269
  paramsForRun: params.paramsForRun,
4418
4270
  signal: params.signal
4419
- });
4271
+ }));
4420
4272
  }
4421
4273
  const approvalPrompt = readComputerUseApprovalElicitation(requestParams, params.computerUseMcpServerName) ?? readBridgeableApprovalElicitation(requestParams);
4422
- if (!approvalPrompt) return;
4423
- return buildElicitationResponse(approvalPrompt, await requestPluginApprovalOutcome({
4274
+ if (!approvalPrompt) return handled(createCodexElicitationResponse("decline"));
4275
+ return handled(buildElicitationResponse(approvalPrompt, await requestPluginApprovalOutcome({
4424
4276
  paramsForRun: params.paramsForRun,
4425
4277
  title: approvalPrompt.title,
4426
4278
  description: approvalPrompt.description,
4427
4279
  allowedDecisions: approvalPrompt.allowedDecisions,
4428
4280
  signal: params.signal
4429
- }));
4281
+ })));
4282
+ }
4283
+ function handled(response) {
4284
+ return {
4285
+ kind: "handled",
4286
+ response
4287
+ };
4430
4288
  }
4431
4289
  function resolvePluginElicitation(params) {
4432
4290
  const requestParams = params.requestParams;
@@ -4527,26 +4385,27 @@ async function buildPluginPolicyElicitationResponse(params) {
4527
4385
  const mode = resolvePluginDestructiveApprovalMode(params.entry);
4528
4386
  if (mode === "deny") {
4529
4387
  logPluginElicitationDecline("destructive_actions_disabled", params.requestParams);
4530
- return declineElicitationResponse();
4388
+ return createCodexElicitationResponse("decline");
4531
4389
  }
4532
4390
  const approvalPrompt = readPluginApprovalElicitation(params.entry, params.requestParams);
4533
4391
  if (!approvalPrompt) {
4534
4392
  logPluginElicitationDecline("unsupported_schema", params.requestParams);
4535
- return declineElicitationResponse();
4393
+ return createCodexElicitationResponse("decline");
4536
4394
  }
4537
4395
  const response = buildElicitationResponse(approvalPrompt, "approved-once");
4538
- if (isJsonObject(response) && response.action === "accept") {
4396
+ if (response.action === "accept") {
4539
4397
  if (mode === "allow") return response;
4540
- return buildElicitationResponse(approvalPrompt, oneShotPluginPolicyApprovalOutcome(mode, await requestPluginApprovalOutcome({
4398
+ const outcome = await requestPluginApprovalOutcome({
4541
4399
  paramsForRun: params.paramsForRun,
4542
4400
  title: approvalPrompt.title,
4543
4401
  description: approvalPrompt.description,
4544
4402
  allowedDecisions: allowedPluginPolicyApprovalDecisions(mode, approvalPrompt),
4545
4403
  signal: params.signal
4546
- })));
4404
+ });
4405
+ return buildElicitationResponse(approvalPrompt, mode === "ask" && outcome === "approved-session" ? "approved-once" : outcome);
4547
4406
  }
4548
4407
  logPluginElicitationDecline("unmappable_schema", params.requestParams);
4549
- return declineElicitationResponse();
4408
+ return createCodexElicitationResponse("decline");
4550
4409
  }
4551
4410
  function resolvePluginDestructiveApprovalMode(entry) {
4552
4411
  return entry.destructiveApprovalMode ?? (entry.allowDestructiveActions ? "allow" : "deny");
@@ -4556,9 +4415,6 @@ function allowedPluginPolicyApprovalDecisions(mode, approvalPrompt) {
4556
4415
  if (mode !== "ask") return allowedDecisions;
4557
4416
  return allowedDecisions.filter((decision) => decision !== "allow-always");
4558
4417
  }
4559
- function oneShotPluginPolicyApprovalOutcome(mode, outcome) {
4560
- return mode === "ask" && outcome === "approved-session" ? "approved-once" : outcome;
4561
- }
4562
4418
  function readPluginApprovalElicitation(entry, requestParams) {
4563
4419
  if (readNonBlankStringField(requestParams, "mode") !== "form" || !isJsonObject(requestParams.requestedSchema)) return;
4564
4420
  const requestedSchema = requestParams.requestedSchema;
@@ -4602,13 +4458,6 @@ function canMapPersistentApproval(requestedSchema, meta) {
4602
4458
  }) && chooseAlwaysPersistOptionValue(readEnumOptions(schema)) !== void 0;
4603
4459
  });
4604
4460
  }
4605
- function declineElicitationResponse() {
4606
- return {
4607
- action: "decline",
4608
- content: null,
4609
- _meta: null
4610
- };
4611
- }
4612
4461
  function logPluginElicitationDecline(reason, requestParams) {
4613
4462
  embeddedAgentLog.debug("codex plugin elicitation declined", {
4614
4463
  reason,
@@ -4751,27 +4600,23 @@ async function requestPluginApprovalOutcome(params) {
4751
4600
  });
4752
4601
  const approvalId = requestResult?.id;
4753
4602
  if (!approvalId) return "unavailable";
4754
- return mapExecDecisionToOutcome(approvalRequestExplicitlyUnavailable(requestResult) ? null : await waitForPluginApprovalDecision({
4603
+ const approvalResult = approvalRequestExplicitlyUnavailable(requestResult) ? void 0 : await waitForPluginApprovalDecision({
4755
4604
  hostCapabilities: params.paramsForRun.hostCapabilities,
4756
4605
  approvalId,
4757
4606
  signal: params.signal
4758
- }));
4607
+ });
4608
+ if (params.signal?.aborted) return "cancelled";
4609
+ if (approvalResult?.terminalReason === "timeout") return "timed-out";
4610
+ return mapExecDecisionToOutcome(approvalResult?.decision);
4759
4611
  } catch {
4760
4612
  return params.signal?.aborted ? "cancelled" : "denied";
4761
4613
  }
4762
4614
  }
4763
4615
  function buildElicitationResponse(approvalPrompt, outcome) {
4764
4616
  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
- };
4617
+ if (outcome === "cancelled") return createCodexElicitationResponse("cancel");
4618
+ if (outcome === "timed-out") return createCodexElicitationResponse("decline", null, { message: codexApprovalTimeoutText("other") });
4619
+ if (outcome === "denied" || outcome === "unavailable") return createCodexElicitationResponse("decline");
4775
4620
  const content = buildAcceptedContent(approvalPrompt, outcome);
4776
4621
  if (!content && !hasNoSchemaProperties(requestedSchema)) {
4777
4622
  embeddedAgentLog.warn("codex MCP approval elicitation approved without a mappable response", {
@@ -4779,13 +4624,9 @@ function buildElicitationResponse(approvalPrompt, outcome) {
4779
4624
  fields: Object.keys(requestedSchema.properties ?? {}),
4780
4625
  outcome
4781
4626
  });
4782
- return declineElicitationResponse();
4627
+ return createCodexElicitationResponse("decline");
4783
4628
  }
4784
- return {
4785
- action: "accept",
4786
- content: content ?? null,
4787
- _meta: buildAcceptedMeta(meta, outcome, approvalPrompt.persistHintsMode ?? "legacy")
4788
- };
4629
+ return createCodexElicitationResponse("accept", content ?? null, buildAcceptedMeta(meta, outcome, approvalPrompt.persistHintsMode ?? "legacy"));
4789
4630
  }
4790
4631
  function buildAcceptedContent(approvalPrompt, outcome) {
4791
4632
  const { requestedSchema, meta } = approvalPrompt;
@@ -4920,6 +4761,36 @@ function readFirstString$1(record, keys) {
4920
4761
  * Bridges OpenClaw runtime tools into Codex app-server dynamic tool specs and
4921
4762
  * tool-call responses.
4922
4763
  */
4764
+ const INTERNAL_TOOL_EXECUTION_VALIDATION = Symbol.for("openclaw.internalToolExecutionValidation");
4765
+ const MAX_CODEX_DYNAMIC_TOOL_VALIDATION_ERRORS = 4;
4766
+ const MAX_CODEX_DYNAMIC_TOOL_VALIDATION_ERROR_CHARS = 160;
4767
+ const CODEX_DYNAMIC_TOOL_VALIDATION_TRUNCATED_SUFFIX = " [detail truncated]";
4768
+ function shouldValidateCodexDynamicToolInput(tool) {
4769
+ return getPluginToolMeta(tool)?.mcp?.operation !== "tool";
4770
+ }
4771
+ function assertCodexDynamicToolInputMatchesSchema(params) {
4772
+ const validation = validateJsonSchemaValue({
4773
+ schema: params.schema,
4774
+ cacheKey: `codex-dynamic-tool-input:${params.toolName}:${JSON.stringify(params.schema)}`,
4775
+ value: params.value
4776
+ });
4777
+ if (validation.ok) return;
4778
+ const visibleErrors = validation.errors.slice(0, MAX_CODEX_DYNAMIC_TOOL_VALIDATION_ERRORS);
4779
+ const details = visibleErrors.map((error) => {
4780
+ if (error.text.length <= MAX_CODEX_DYNAMIC_TOOL_VALIDATION_ERROR_CHARS) return error.text;
4781
+ return `${error.text.slice(0, MAX_CODEX_DYNAMIC_TOOL_VALIDATION_ERROR_CHARS - 19)}${CODEX_DYNAMIC_TOOL_VALIDATION_TRUNCATED_SUFFIX}`;
4782
+ }).join("; ");
4783
+ const omitted = validation.errors.length - visibleErrors.length;
4784
+ const omittedSuffix = omitted > 0 ? `; ${omitted} more violation(s) omitted` : "";
4785
+ throw new Error(`Invalid arguments for tool "${params.toolName}": ${details}${omittedSuffix}.`);
4786
+ }
4787
+ function createCodexDynamicToolValidationControl(params) {
4788
+ return {
4789
+ [INTERNAL_TOOL_EXECUTION_VALIDATION]: true,
4790
+ toolCallId: params.toolCallId,
4791
+ validate: params.validate
4792
+ };
4793
+ }
4923
4794
  function applyCurrentMessageProvider(toolName, args, currentProvider) {
4924
4795
  const hasProvider = typeof args.provider === "string" && args.provider.trim().length > 0 ? true : typeof args.channel === "string" && args.channel.trim().length > 0;
4925
4796
  const provider = currentProvider?.trim();
@@ -5050,6 +4921,8 @@ function normalizeAcceptedSessionSpawn(result) {
5050
4921
  }
5051
4922
  /** Namespace attached to OpenClaw-owned dynamic tools exposed to Codex. */
5052
4923
  const CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE = "openclaw";
4924
+ const CODEX_DYNAMIC_TOOL_NAME_MAX_CHARS = 128;
4925
+ const CODEX_DYNAMIC_TOOL_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/u;
5053
4926
  const ALWAYS_DIRECT_DYNAMIC_TOOL_NAMES = /* @__PURE__ */ new Set([
5054
4927
  "agents_list",
5055
4928
  "sessions_spawn",
@@ -5100,7 +4973,11 @@ function createCodexDynamicToolBridge(params) {
5100
4973
  const toolMap = new Map(availableTools.map((entry) => [entry.name, entry]));
5101
4974
  const registeredToolNames = new Set(registeredSpecTools.map((entry) => entry.name));
5102
4975
  const quarantinedTools = dedupeQuarantinedDynamicTools([...availableProjection.quarantinedTools, ...registeredProjection.quarantinedTools]);
5103
- warnQuarantinedDynamicTools(quarantinedTools);
4976
+ warnQuarantinedDynamicTools({
4977
+ tools: quarantinedTools,
4978
+ availableToolCount: availableTools.length,
4979
+ registeredToolCount: registeredSpecTools.length
4980
+ });
5104
4981
  emitQuarantinedDynamicToolDiagnostics(quarantinedTools, params.hookContext);
5105
4982
  const telemetry = {
5106
4983
  didSendViaMessagingTool: false,
@@ -5140,6 +5017,10 @@ function createCodexDynamicToolBridge(params) {
5140
5017
  directToolNames
5141
5018
  }),
5142
5019
  resultContentSourceForTool: (toolName) => toolMap.get(toolName)?.tool.resultContentSource,
5020
+ sideEffectOwnerKeyForTool: (toolName) => {
5021
+ const tool = toolMap.get(toolName)?.tool;
5022
+ return tool ? getPluginToolSideEffectOwnerKey(tool) : void 0;
5023
+ },
5143
5024
  telemetry,
5144
5025
  setRemoteWorkspaceFileReader: (reader) => {
5145
5026
  readRemoteWorkspaceFile = reader;
@@ -5171,7 +5052,8 @@ function createCodexDynamicToolBridge(params) {
5171
5052
  });
5172
5053
  }
5173
5054
  const { tool, name: toolName } = toolEntry;
5174
- const args = asNonArrayRecord(call.arguments);
5055
+ const rawArguments = call.arguments;
5056
+ const args = asNonArrayRecord(rawArguments);
5175
5057
  const startedAt = Date.now();
5176
5058
  const signal = composeAbortSignals(params.signal, options?.signal);
5177
5059
  let didStartExecution = false;
@@ -5194,7 +5076,8 @@ function createCodexDynamicToolBridge(params) {
5194
5076
  };
5195
5077
  };
5196
5078
  try {
5197
- const toolArgs = tool.prepareArguments ? tool.prepareArguments(args) : args;
5079
+ const prepare = tool.prepareArguments;
5080
+ const toolArgs = prepare ? Reflect.apply(prepare, tool, [rawArguments]) : args;
5198
5081
  const preparedArgs = toolName === "message" && isRecord(toolArgs) ? await prepareCodexRemoteWorkspaceMessageMedia({
5199
5082
  args: toolArgs,
5200
5083
  localWorkspaceRoot: params.hookContext?.workspaceDir,
@@ -5214,7 +5097,20 @@ function createCodexDynamicToolBridge(params) {
5214
5097
  hasRepliedRef: params.hookContext?.hasRepliedRef ? { value: params.hookContext.hasRepliedRef.value } : void 0
5215
5098
  };
5216
5099
  didDispatchExecution = true;
5217
- const rawResult = await tool.execute(call.callId, preparedArgs, signal);
5100
+ const executionArgs = [
5101
+ call.callId,
5102
+ preparedArgs,
5103
+ signal
5104
+ ];
5105
+ if (shouldValidateCodexDynamicToolInput(tool)) executionArgs.push(createCodexDynamicToolValidationControl({
5106
+ toolCallId: call.callId,
5107
+ validate: (value) => assertCodexDynamicToolInputMatchesSchema({
5108
+ toolName,
5109
+ schema: toolEntry.inputSchema,
5110
+ value
5111
+ })
5112
+ }));
5113
+ const rawResult = await Reflect.apply(tool.execute, tool, executionArgs);
5218
5114
  captureExecutionBoundary();
5219
5115
  const telemetryRawResult = sanitizeToolResult(rawResult);
5220
5116
  const rawIsError = isToolResultError(rawResult);
@@ -5282,7 +5178,7 @@ function createCodexDynamicToolBridge(params) {
5282
5178
  contentItems,
5283
5179
  success: !resultIsError
5284
5180
  }, terminalType);
5285
- withDynamicToolTranscriptDetails(response, result.details);
5181
+ withDynamicToolTranscriptDetails(response, asOptionalRecord(sanitizeToolResult(result))?.details);
5286
5182
  withDiagnosticFailureDisposition(response, resultFailureKind);
5287
5183
  const blocksSourceReplyTermination = hasExplicitNonSourceMessageRoute(executedArgs, params.hookContext, confirmedMessagingTarget);
5288
5184
  const deliveredSourceReply = isDeliveredMessageToolOnlySourceReplyResult({
@@ -5533,6 +5429,13 @@ function projectCodexDynamicTools(tools) {
5533
5429
  });
5534
5430
  continue;
5535
5431
  }
5432
+ if (!isRecord(projection.schema)) {
5433
+ quarantinedTools.push({
5434
+ tool: descriptor.name,
5435
+ violations: [`${descriptor.name}.inputSchema must be a JSON object schema`]
5436
+ });
5437
+ continue;
5438
+ }
5536
5439
  projectedTools.push({
5537
5440
  tool,
5538
5441
  name: descriptor.name,
@@ -5557,6 +5460,20 @@ function readCodexDynamicToolDescriptor(tool, toolIndex) {
5557
5460
  violations: [`${fallbackName}.name must be a non-empty string`]
5558
5461
  }
5559
5462
  };
5463
+ const trimmedName = rawName.trim();
5464
+ let nameViolation;
5465
+ if (!trimmedName) nameViolation = `${rawName}.name must not be empty`;
5466
+ else if (trimmedName !== rawName) nameViolation = `${rawName}.name must not have leading or trailing whitespace`;
5467
+ else if (!CODEX_DYNAMIC_TOOL_NAME_PATTERN.test(rawName)) nameViolation = `${rawName}.name must match ^[a-zA-Z0-9_-]+$`;
5468
+ else if (rawName.length > CODEX_DYNAMIC_TOOL_NAME_MAX_CHARS) nameViolation = `${rawName}.name must be at most ${CODEX_DYNAMIC_TOOL_NAME_MAX_CHARS} characters`;
5469
+ else if (rawName === "mcp" || rawName.startsWith("mcp__")) nameViolation = `${rawName}.name is reserved by Codex app-server`;
5470
+ if (nameViolation) return {
5471
+ ok: false,
5472
+ diagnostic: {
5473
+ tool: rawName,
5474
+ violations: [nameViolation]
5475
+ }
5476
+ };
5560
5477
  name = rawName;
5561
5478
  } catch {
5562
5479
  return {
@@ -5598,14 +5515,18 @@ function readCodexDynamicToolDescriptor(tool, toolIndex) {
5598
5515
  parameters
5599
5516
  };
5600
5517
  }
5601
- function warnQuarantinedDynamicTools(tools) {
5602
- if (tools.length === 0) return;
5518
+ function warnQuarantinedDynamicTools(params) {
5519
+ if (params.tools.length === 0) return;
5603
5520
  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
- })) });
5521
+ for (const tool of params.tools) unique.set(tool.tool, tool.violations);
5522
+ 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`, {
5523
+ tools: [...unique.entries()].map(([tool, violations]) => ({
5524
+ tool,
5525
+ violations
5526
+ })),
5527
+ availableToolCount: params.availableToolCount,
5528
+ registeredToolCount: params.registeredToolCount
5529
+ });
5609
5530
  }
5610
5531
  function emitQuarantinedDynamicToolDiagnostics(tools, ctx) {
5611
5532
  for (const tool of tools) emitTrustedDiagnosticEvent({
@@ -5893,4 +5814,4 @@ function isCronAddAction(args) {
5893
5814
  return typeof action === "string" && action.trim().toLowerCase() === "add";
5894
5815
  }
5895
5816
  //#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 };
5817
+ 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 };