@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.
- package/dist/api.js +147 -0
- package/dist/{attempt-client-cleanup-DMLXUfa8.js → attempt-client-cleanup-bfsmcQ6m.js} +21 -2
- package/dist/{binding-connection-I5Bo-AsX.js → binding-connection-BMvPX73o.js} +15 -3
- package/dist/{bounded-turn-qgZmzy_4.js → bounded-turn-B7iBUcuP.js} +22 -16
- package/dist/{capabilities-CybGI1oz.js → capabilities-BRMh0aIq.js} +1 -1
- package/dist/cli-metadata.js +1 -1
- package/dist/{command-formatters-B1AEz8ZA.js → command-formatters-DEsIufkt.js} +2 -2
- package/dist/{command-handlers-Db8pu_ds.js → command-handlers-D_-1_qRp.js} +65 -37
- package/dist/command-rpc-CCeCkf_v.js +90 -0
- package/dist/{compact-LspQaGfl.js → compact-BglNFZrQ.js} +35 -29
- package/dist/{session-binding-DPxIvMgE.js → config-giW8X0QA.js} +108 -914
- package/dist/context-compaction-activity-BOeJf7yo.js +58 -0
- package/dist/doctor-contract-api.js +23 -21
- package/dist/{dynamic-tools-BVdyWkUD.js → dynamic-tools-Dnlfa--w.js} +1345 -1424
- package/dist/{effective-mcp-catalog-DIIhAtOZ.js → effective-mcp-catalog-CTeGrnfO.js} +2 -2
- package/dist/elicitation-input-DMkeki8d.js +71 -0
- package/dist/{event-projector-assistant-message-BwxV4d6N.js → event-projector-assistant-message-C5-Ds8mA.js} +18 -1
- package/dist/{event-projector-usage-e5huIQWu.js → event-projector-usage-Bk1I90Bg.js} +18 -18
- package/dist/harness-Csz0F-DK.js +258 -0
- package/dist/harness.js +2 -226
- package/dist/index.js +262 -181
- package/dist/{isolated-completion-C58NbL40.js → isolated-completion-Cv6FqygM.js} +4 -4
- package/dist/managed-binary-DFYK4DWq.js +248 -0
- package/dist/media-understanding-provider.js +2 -2
- package/dist/model-catalog-i9rNBthX.js +48 -0
- package/dist/{models-BFlpn1Ex.js → models-TS3HBU-q.js} +4 -3
- package/dist/{native-subagent-monitor-DyTGmTUx.js → native-subagent-monitor-4Mfq2HDs.js} +354 -83
- package/dist/node-exec-server.runtime-BpWqJO1V.js +192 -0
- package/dist/{plugin-app-cache-key-C3H2CVp6.js → plugin-app-cache-key-C1CAdfcP.js} +19 -4
- package/dist/plugin-approval-roundtrip-bUm777tR.js +95 -0
- package/dist/{plugin-marketplace-discovery-CzrGJdCR.js → plugin-marketplace-discovery-B9DvaAG-.js} +59 -54
- package/dist/{plugin-metadata-cache-CNJPj4_b.js → plugin-metadata-cache-CTCT3w5L.js} +164 -134
- package/dist/{protocol-validators-B4F7L8Rf.js → protocol-validators-VeJxxJ0U.js} +80 -5
- package/dist/{rate-limits-DZXmOL8-.js → rate-limits-C5_D8Gmb.js} +1 -1
- package/dist/{request-BRl5_UfX.js → request-DXXGHko0.js} +2 -2
- package/dist/{run-attempt-P8BdxC1I.js → run-attempt-DYc0QCBF.js} +1276 -765
- package/dist/{runtime-artifact-BVSPdd6g.js → runtime-artifact-BvyT1uyd.js} +4 -7
- package/dist/{sandbox-guard-Cmwc9eIk.js → sandbox-guard-kuWTn9C5.js} +25 -19
- package/dist/session-binding-Bcynfd-_.js +874 -0
- package/dist/{session-catalog-xwNi_SDM.js → session-catalog-DaLKVtpH.js} +1534 -1288
- package/dist/session-catalog-home-id-BNwAJ9D7.js +18 -0
- package/dist/{session-cli-7hgXZDV7.js → session-cli-DMc4oS2w.js} +20 -7
- package/dist/session-permission-policy-BrNH1bjy.js +160 -0
- package/dist/{session-retirement-DoMMJFKm.js → session-retirement-CcNC3YSY.js} +3 -3
- package/dist/{session-upstream-marker-CrIFX06J.js → session-upstream-marker-BIkusb95.js} +1 -1
- package/dist/{settled-turn-finalizer-BRWBO4MO.js → settled-turn-finalizer-Dc3ApjU4.js} +24 -20
- package/dist/{shared-client-kIBbGjk4.js → shared-client-DKz23hHY.js} +1632 -372
- package/dist/{side-question-9HAL26l9.js → side-question-DjA_jsNe.js} +105 -54
- package/dist/{thread-lifecycle-Bu1BTDXr.js → thread-lifecycle-L7oEkTlq.js} +1147 -114
- package/dist/{thread-ownership-6kOQHlyC.js → thread-ownership-DZeuKH1j.js} +2 -2
- package/dist/{thread-resume-CPrJNQVM.js → thread-resume-BE9qNEW0.js} +3 -3
- package/dist/{transcript-mirror-B9gGgnJS.js → transcript-mirror-DsCMWBca.js} +137 -38
- package/dist/{transport-stdio-DIImwDVS.js → transport-stdio-DhoGy33Z.js} +2 -2
- package/dist/{upstream-prompt-provenance-ClRBije0.js → upstream-prompt-provenance-CFmsKwr9.js} +12 -14
- package/dist/{upstream-session-fork-CE-_LdnP.js → upstream-session-fork-DlaHxSLC.js} +12 -6
- package/dist/{usage-CHlV0e-o.js → usage-ROIaW3wo.js} +3 -3
- package/dist/{web-search-provider.runtime-DRw2Vl42.js → web-search-provider.runtime-ClaNPV7N.js} +3 -3
- package/openclaw.plugin.json +49 -2
- package/package.json +9 -8
- package/dist/app-server-policy-zcBDc1JU.js +0 -49
|
@@ -1,811 +1,39 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { E as itemStatus, F as readItem, b as auditNativeToolUnfinishedStatus, h as isCodexNotificationForTurn, v as auditNativeToolName, y as auditNativeToolTerminalStatus } from "./attempt-client-cleanup-
|
|
4
|
-
import { l as readCodexTurn } from "./protocol-validators-
|
|
5
|
-
import { D as
|
|
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-
|
|
8
|
-
import { a as resolveCodexNodeExecToolOverrides, i as resolveCodexNativeExecutionPolicy } from "./sandbox-guard-
|
|
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 {
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
1326
|
-
const
|
|
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
|
|
1337
|
-
const pinnedNode =
|
|
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
|
|
1341
|
-
description
|
|
1342
|
-
parameters:
|
|
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,
|
|
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(
|
|
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
|
|
589
|
+
function createGatewayProcessAliasDynamicTool(processTool) {
|
|
1353
590
|
return {
|
|
1354
591
|
...processTool,
|
|
1355
|
-
name:
|
|
1356
|
-
description: "Manage
|
|
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
|
|
1360
|
-
const { host: _host, security: _security, ask: _ask, node: requestedNode, ...rest } =
|
|
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
|
-
...
|
|
1364
|
-
host:
|
|
606
|
+
...nodeArgs,
|
|
607
|
+
host: params.host,
|
|
1365
608
|
...node ? { node } : {}
|
|
1366
609
|
};
|
|
1367
610
|
}
|
|
1368
|
-
function
|
|
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
|
|
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" ||
|
|
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
|
-
*
|
|
1386
|
-
*
|
|
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
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
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
|
|
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
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
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 =
|
|
892
|
+
const visionFilteredTools = filterCodexVisionTools(codexFilteredTools, {
|
|
1617
893
|
modelHasVision,
|
|
1618
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1843
|
-
if (!
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
return
|
|
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 === "
|
|
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
|
-
|
|
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
|
|
1916
|
-
function parseRequest(
|
|
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
|
|
1958
|
-
function sendResult(
|
|
1959
|
-
|
|
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
|
|
1963
|
-
})
|
|
1565
|
+
result
|
|
1566
|
+
});
|
|
1964
1567
|
}
|
|
1965
|
-
/** Sends a JSON-RPC error response
|
|
1966
|
-
function sendError(
|
|
1967
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
2578
|
-
const backend =
|
|
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
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
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
|
-
}
|
|
2598
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2623
|
-
|
|
2327
|
+
lifecycle,
|
|
2328
|
+
owner,
|
|
2624
2329
|
requestId,
|
|
2625
|
-
|
|
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
|
|
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
|
-
|
|
2648
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
2682
|
+
terminationRequested: false,
|
|
2968
2683
|
child: null,
|
|
2969
|
-
finalized: false,
|
|
2970
2684
|
waiters: [],
|
|
2971
|
-
emitNotification:
|
|
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
|
|
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 {
|
|
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.
|
|
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:
|
|
2732
|
+
command: buildRemoteCommand(params.argv),
|
|
3010
2733
|
workdir: params.cwd,
|
|
3011
|
-
env:
|
|
2734
|
+
env: remoteExec.env,
|
|
3012
2735
|
usePty: false
|
|
3013
2736
|
});
|
|
3014
|
-
managed.
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
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
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
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:
|
|
2757
|
+
error: message
|
|
3035
2758
|
});
|
|
3036
|
-
}
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
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.
|
|
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.
|
|
3172
|
-
managed.
|
|
3173
|
-
if (
|
|
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
|
|
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(
|
|
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
|
|
3258
|
-
execServerUrl
|
|
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
|
-
|
|
3272
|
-
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)
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
3378
|
-
|
|
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(
|
|
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
|
-
|
|
3386
|
-
|
|
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(
|
|
3393
|
-
const
|
|
3394
|
-
|
|
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
|
-
|
|
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:
|
|
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,
|
|
3611
|
-
message
|
|
3426
|
+
...approvalEventScope(params.method, resolvedOutcome),
|
|
3427
|
+
message: resolvedMessage
|
|
3612
3428
|
});
|
|
3613
|
-
return buildApprovalResponse(params.method, context.requestParams,
|
|
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
|
-
|
|
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
|
|
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
|
|
3672
|
-
const outcome = params.signal?.aborted ? "cancelled" : mapExecDecisionToOutcome(decision);
|
|
3673
|
-
if (
|
|
3674
|
-
else if (outcome === "
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
3964
|
-
if (outcome === "denied" || outcome === "unavailable") return
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
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
|
|
4004
|
+
function commandApprovalCapabilities(requestParams) {
|
|
4158
4005
|
const available = requestParams?.availableDecisions;
|
|
4159
|
-
if (!Array.isArray(available)) return
|
|
4160
|
-
|
|
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
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
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
|
|
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
|
|
4256
|
+
return handled(createCodexElicitationResponse("decline"));
|
|
4405
4257
|
}
|
|
4406
4258
|
if (pluginResolution.kind === "decline") {
|
|
4407
4259
|
logPluginElicitationDecline(pluginResolution.reason, requestParams);
|
|
4408
|
-
return
|
|
4260
|
+
return handled(createCodexElicitationResponse("decline"));
|
|
4409
4261
|
}
|
|
4410
4262
|
if (requestTurnId !== params.turnId) {
|
|
4411
4263
|
logPluginElicitationDecline("missing_active_turn", requestParams);
|
|
4412
|
-
return
|
|
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
|
|
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
|
|
4393
|
+
return createCodexElicitationResponse("decline");
|
|
4536
4394
|
}
|
|
4537
4395
|
const response = buildElicitationResponse(approvalPrompt, "approved-once");
|
|
4538
|
-
if (
|
|
4396
|
+
if (response.action === "accept") {
|
|
4539
4397
|
if (mode === "allow") return response;
|
|
4540
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
4767
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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 ? "
|
|
5606
|
-
tool,
|
|
5607
|
-
|
|
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 {
|
|
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 };
|