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