@openclaw/codex 2026.8.1-beta.2 → 2026.8.1-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.js +147 -0
- package/dist/app-server-policy-bg5QPSVP.js +25 -0
- package/dist/{attempt-client-cleanup-DMLXUfa8.js → attempt-client-cleanup-URjUlCHg.js} +1 -1
- package/dist/{binding-connection-I5Bo-AsX.js → binding-connection-DAvZwp96.js} +15 -3
- package/dist/{bounded-turn-qgZmzy_4.js → bounded-turn-4BjrZEwW.js} +22 -16
- package/dist/{capabilities-CybGI1oz.js → capabilities-CH3K57v4.js} +1 -1
- package/dist/cli-metadata.js +1 -1
- package/dist/{command-formatters-B1AEz8ZA.js → command-formatters-D0nZ3qtd.js} +2 -2
- package/dist/{command-handlers-Db8pu_ds.js → command-handlers-DC86lgT4.js} +39 -22
- package/dist/command-rpc-xUIIonsO.js +90 -0
- package/dist/{compact-LspQaGfl.js → compact-COqgBRny.js} +35 -29
- package/dist/{session-binding-DPxIvMgE.js → config-CpNeo5AE.js} +28 -888
- package/dist/context-compaction-activity-BOeJf7yo.js +58 -0
- package/dist/doctor-contract-api.js +15 -14
- package/dist/{dynamic-tools-BVdyWkUD.js → dynamic-tools-fJZhl4CE.js} +1284 -651
- package/dist/{effective-mcp-catalog-DIIhAtOZ.js → effective-mcp-catalog-4YKn51aG.js} +2 -2
- package/dist/elicitation-input-DMkeki8d.js +71 -0
- package/dist/{event-projector-usage-e5huIQWu.js → event-projector-usage-CLwNEJqW.js} +16 -16
- package/dist/harness-DoKQcF8n.js +258 -0
- package/dist/harness.js +2 -226
- package/dist/index.js +171 -48
- package/dist/{isolated-completion-C58NbL40.js → isolated-completion-DPMtlH9r.js} +3 -3
- package/dist/managed-binary-By4FZC6i.js +212 -0
- package/dist/media-understanding-provider.js +2 -2
- package/dist/model-catalog-CmGoqVKB.js +48 -0
- package/dist/{models-BFlpn1Ex.js → models-CZw1wClT.js} +2 -2
- package/dist/{native-subagent-monitor-DyTGmTUx.js → native-subagent-monitor-GGq3RnOo.js} +185 -71
- package/dist/node-exec-server.runtime-Bsu1LdFZ.js +192 -0
- package/dist/{plugin-app-cache-key-C3H2CVp6.js → plugin-app-cache-key-DMNQ8Zi8.js} +15 -3
- package/dist/plugin-approval-roundtrip-C75Yiiwc.js +95 -0
- package/dist/{plugin-marketplace-discovery-CzrGJdCR.js → plugin-marketplace-discovery-CSWDkMrP.js} +40 -26
- package/dist/{plugin-metadata-cache-CNJPj4_b.js → plugin-metadata-cache-BXH5NF-4.js} +8 -5
- package/dist/{protocol-validators-B4F7L8Rf.js → protocol-validators-VAVyEnDp.js} +55 -2
- package/dist/{rate-limits-DZXmOL8-.js → rate-limits-BI1dMHDG.js} +1 -1
- package/dist/{request-BRl5_UfX.js → request-DSK0W6YI.js} +2 -2
- package/dist/{run-attempt-P8BdxC1I.js → run-attempt-BV3HzuCf.js} +1098 -666
- package/dist/{runtime-artifact-BVSPdd6g.js → runtime-artifact-BvyT1uyd.js} +4 -7
- package/dist/{sandbox-guard-Cmwc9eIk.js → sandbox-guard-C1qEeZ5C.js} +25 -19
- package/dist/session-binding-BTrLf9Qm.js +874 -0
- package/dist/{session-catalog-xwNi_SDM.js → session-catalog-ezP_UcRM.js} +1541 -1296
- package/dist/session-catalog-home-id-BNwAJ9D7.js +18 -0
- package/dist/{session-cli-7hgXZDV7.js → session-cli-B1hKvfkI.js} +20 -7
- package/dist/{session-retirement-DoMMJFKm.js → session-retirement-CE4iMiLh.js} +3 -3
- package/dist/{session-upstream-marker-CrIFX06J.js → session-upstream-marker-D_2npHlA.js} +1 -1
- package/dist/{settled-turn-finalizer-BRWBO4MO.js → settled-turn-finalizer-Dx1onIqg.js} +23 -19
- package/dist/{shared-client-kIBbGjk4.js → shared-client-Dw-rzii0.js} +755 -273
- package/dist/{side-question-9HAL26l9.js → side-question-DbXkJZzg.js} +52 -30
- package/dist/{thread-lifecycle-Bu1BTDXr.js → thread-lifecycle-DKdrnTMX.js} +310 -92
- package/dist/{thread-ownership-6kOQHlyC.js → thread-ownership-i2NBwDUL.js} +2 -2
- package/dist/{thread-resume-CPrJNQVM.js → thread-resume-CdJk2Ehf.js} +3 -3
- package/dist/{transcript-mirror-B9gGgnJS.js → transcript-mirror-BU0_LKAL.js} +46 -28
- package/dist/{transport-stdio-DIImwDVS.js → transport-stdio-DhoGy33Z.js} +2 -2
- package/dist/{upstream-prompt-provenance-ClRBije0.js → upstream-prompt-provenance-CFmsKwr9.js} +12 -14
- package/dist/{upstream-session-fork-CE-_LdnP.js → upstream-session-fork-BDVJCLAD.js} +12 -6
- package/dist/{usage-CHlV0e-o.js → usage-9dKJCgcy.js} +3 -3
- package/dist/{web-search-provider.runtime-DRw2Vl42.js → web-search-provider.runtime-DCdx4DGu.js} +3 -3
- package/openclaw.plugin.json +31 -1
- package/package.json +6 -5
- package/dist/app-server-policy-zcBDc1JU.js +0 -49
|
@@ -1,45 +1,48 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { E as itemStatus, F as readItem, b as auditNativeToolUnfinishedStatus, h as isCodexNotificationForTurn, v as auditNativeToolName, y as auditNativeToolTerminalStatus } from "./attempt-client-cleanup-
|
|
4
|
-
import { l as readCodexTurn } from "./protocol-validators-
|
|
5
|
-
import {
|
|
1
|
+
import { h as readCodexPluginConfig, p as isCodexRemoteExecPlacementSandbox } from "./config-CpNeo5AE.js";
|
|
2
|
+
import { V as createCodexElicitationResponse, ct as CODEX_OPENCLAW_DIRECT_DYNAMIC_TOOL_NAMESPACE, l as releaseLeasedSharedCodexAppServerClient, ut as isJsonObject } from "./shared-client-Dw-rzii0.js";
|
|
3
|
+
import { E as itemStatus, F as readItem, b as auditNativeToolUnfinishedStatus, h as isCodexNotificationForTurn, v as auditNativeToolName, y as auditNativeToolTerminalStatus } from "./attempt-client-cleanup-URjUlCHg.js";
|
|
4
|
+
import { l as readCodexTurn } from "./protocol-validators-VAVyEnDp.js";
|
|
5
|
+
import { A as isForcedPrivateQaCodexRuntime, M as isSystemAgentOnlyCodexDynamicToolAllowlist, N as normalizeCodexDynamicToolName, O as filterCodexDynamicTools, f as resolveCodexWebSearchPlan, k as filterCodexDynamicToolsForDisabledNativeSurface } from "./thread-lifecycle-DKdrnTMX.js";
|
|
6
6
|
import { r as sanitizeInlineImageDataUrl, t as invalidInlineImageText } from "./image-payload-sanitizer-CnfoUPCE.js";
|
|
7
|
-
import { r as formatCodexDisplayText } from "./command-formatters-
|
|
8
|
-
import { a as resolveCodexNodeExecToolOverrides, i as resolveCodexNativeExecutionPolicy } from "./sandbox-guard-
|
|
7
|
+
import { r as formatCodexDisplayText } from "./command-formatters-D0nZ3qtd.js";
|
|
8
|
+
import { a as resolveCodexNodeExecToolOverrides, i as resolveCodexNativeExecutionPolicy } from "./sandbox-guard-C1qEeZ5C.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-C75Yiiwc.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
14
|
import { addTimerTimeoutGraceMs, asDateTimestampMs, finiteSecondsToTimerSafeMilliseconds, parseStrictNonNegativeInteger } from "openclaw/plugin-sdk/number-runtime";
|
|
13
|
-
import { HEARTBEAT_RESPONSE_TOOL_NAME, buildAgentHookContextChannelFields, buildEmbeddedAttemptToolRunContext, consumeAdjustedParamsForToolCall, consumePreExecutionBlockedToolCall, createAgentToolResultMiddlewareRunner, createCodexAppServerToolResultExtensionRunner, embeddedAgentLog, extractMessagingToolSend, extractMessagingToolSendResult, extractToolResultMediaArtifact, filterProviderNormalizableTools, filterToolResultMediaUrls, finalizeToolTerminalPresentation, formatApprovalDisplayPath, formatToolExecutionErrorMessage, getBeforeToolCallFailureDisposition, getChannelAgentToolMeta, getPluginToolMeta, hasNativeHookRelayInvocation, invokeNativeHookRelay, isDeliveredMessageToolOnlySourceReplyResult, isDeliveredMessagingToolResult, isHostScopedAgentToolActive, isMessagingTool, isMessagingToolSendAction, isReplaySafeToolCall, isSubagentSessionKey, isToolResultError, isToolWrappedWithBeforeToolCallHook, normalizeAgentRuntimeTools, normalizeHeartbeatToolResponse, projectRuntimeToolInputSchema, resolveAttemptSpawnWorkspaceDir, resolveModelAuthMode, resolveNativeHookRelayDeferredToolApproval, resolveToolExecutionErrorKind, resolveToolResultFailureKind, runAgentHarnessAfterToolCallHook, sanitizeToolResult, setBeforeToolCallDiagnosticsEnabled, supportsModelTools, wrapToolWithBeforeToolCallHook } from "openclaw/plugin-sdk/agent-harness-runtime";
|
|
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 { killProcessTree } from "openclaw/plugin-sdk/process-runtime";
|
|
19
|
+
import { formatErrorMessage as formatErrorMessage$1, redactSensitiveText } from "openclaw/plugin-sdk/security-runtime";
|
|
16
20
|
import { SsrFBlockedError, isBlockedHostnameOrIp } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
17
21
|
import { spawn } from "node:child_process";
|
|
18
|
-
import { coerceErrorMessage
|
|
22
|
+
import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
19
23
|
import { DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS, estimateToolResultTextChars, resolveLiveToolResultMaxChars, sliceToolResultTextToBudget, sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
20
24
|
import { once } from "node:events";
|
|
21
25
|
import { isIP } from "node:net";
|
|
22
26
|
import { WebSocketServer } from "ws";
|
|
23
27
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
24
28
|
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
|
29
|
+
import { redactToolPayloadText } from "openclaw/plugin-sdk/logging-core";
|
|
30
|
+
import { isPathStrictlyInside, root } from "openclaw/plugin-sdk/file-access-runtime";
|
|
25
31
|
import { runWithCronCreatorAuthorityCapabilityResolver } from "openclaw/plugin-sdk/codex-mcp-projection";
|
|
26
32
|
import { emitTrustedDiagnosticEvent, hasPendingInternalDiagnosticEvent } from "openclaw/plugin-sdk/diagnostic-runtime";
|
|
27
|
-
import { isToolAllowed, sanitizeEnvVars } from "openclaw/plugin-sdk/sandbox";
|
|
33
|
+
import { buildRemoteCommand, isToolAllowed, sanitizeEnvVars } from "openclaw/plugin-sdk/sandbox";
|
|
28
34
|
import { saveMediaBuffer } from "openclaw/plugin-sdk/media-store";
|
|
29
35
|
import { registerRetainedNativeHookRelayForBundledRuntime } from "openclaw/plugin-sdk/native-hook-relay-runtime";
|
|
30
|
-
import { root } from "openclaw/plugin-sdk/file-access-runtime";
|
|
31
36
|
import { getMediaDir } from "openclaw/plugin-sdk/media-runtime";
|
|
32
37
|
import { normalizeOpenAIToolSchemas } from "openclaw/plugin-sdk/provider-tools";
|
|
33
38
|
//#region extensions/codex/src/app-server/dynamic-tool-response-state.ts
|
|
34
39
|
/** Retains the host-owned app preview without adding it to Codex's response payload. */
|
|
35
40
|
function withDynamicToolTranscriptDetails(response, details) {
|
|
36
|
-
if (
|
|
37
|
-
const mcpAppPreview = details.mcpAppPreview;
|
|
38
|
-
if (!mcpAppPreview || typeof mcpAppPreview !== "object" || Array.isArray(mcpAppPreview)) return response;
|
|
41
|
+
if (details === void 0) return response;
|
|
39
42
|
Object.defineProperty(response, "transcriptDetails", {
|
|
40
43
|
configurable: true,
|
|
41
44
|
enumerable: false,
|
|
42
|
-
value:
|
|
45
|
+
value: details
|
|
43
46
|
});
|
|
44
47
|
return response;
|
|
45
48
|
}
|
|
@@ -228,12 +231,14 @@ async function handleDynamicToolCallWithTimeout(params) {
|
|
|
228
231
|
const conservativeRaceResponses = /* @__PURE__ */ new WeakSet();
|
|
229
232
|
const finalizeTerminal = (response) => {
|
|
230
233
|
const executionSnapshot = params.toolBridge.consumeToolExecutionSnapshot?.(params.call.callId);
|
|
234
|
+
const ownerKey = params.toolBridge.sideEffectOwnerKeyForTool?.(params.call.tool);
|
|
231
235
|
const observedExecutionStarted = executionSnapshot?.executionStarted ?? (conservativeRaceResponses.has(response) ? void 0 : response.executionStarted);
|
|
232
236
|
const terminalResolution = params.observeToolTerminal?.({
|
|
233
237
|
toolCallId: params.call.callId,
|
|
234
238
|
toolName: params.call.tool,
|
|
235
239
|
arguments: response.executedArguments ?? executionSnapshot?.executedArguments ?? params.call.arguments,
|
|
236
240
|
...params.toolMeta ? { meta: params.toolMeta } : {},
|
|
241
|
+
...ownerKey ? { ownerMutation: { ownerKey } } : {},
|
|
237
242
|
...observedExecutionStarted !== void 0 ? { executionStarted: observedExecutionStarted } : {},
|
|
238
243
|
outcome: response.success ? "success" : "failure",
|
|
239
244
|
...!response.success ? { failure: { error: readDynamicToolResponseText(response) } } : {}
|
|
@@ -358,13 +363,15 @@ function toCodexDynamicToolProtocolResponse(response) {
|
|
|
358
363
|
}
|
|
359
364
|
/** Adds async-started progress details when a tool result continues out of band. */
|
|
360
365
|
function toCodexDynamicToolProgressResponse(response, protocolResponse) {
|
|
361
|
-
const transcriptDetails = response.transcriptDetails;
|
|
362
|
-
|
|
366
|
+
const transcriptDetails = isJsonObject(response.transcriptDetails) ? response.transcriptDetails : void 0;
|
|
367
|
+
const mcpAppPreview = isJsonObject(transcriptDetails?.mcpAppPreview) ? transcriptDetails.mcpAppPreview : void 0;
|
|
368
|
+
const progressDetails = mcpAppPreview ? { mcpAppPreview } : void 0;
|
|
369
|
+
if (response.asyncStarted !== true && progressDetails === void 0) return protocolResponse;
|
|
363
370
|
return {
|
|
364
371
|
...protocolResponse,
|
|
365
|
-
...
|
|
372
|
+
...progressDetails ? { details: progressDetails } : {},
|
|
366
373
|
...response.asyncStarted === true ? { details: {
|
|
367
|
-
...
|
|
374
|
+
...progressDetails,
|
|
368
375
|
async: true,
|
|
369
376
|
status: "started"
|
|
370
377
|
} } : {}
|
|
@@ -440,7 +447,7 @@ function readConfiguredDynamicToolTimeoutMs(toolName, config) {
|
|
|
440
447
|
if (!imageModel || typeof imageModel !== "object") return CODEX_DYNAMIC_IMAGE_GENERATION_TOOL_TIMEOUT_MS;
|
|
441
448
|
return readPositiveFiniteTimeoutMs(imageModel.timeoutMs) ?? CODEX_DYNAMIC_IMAGE_GENERATION_TOOL_TIMEOUT_MS;
|
|
442
449
|
}
|
|
443
|
-
if (toolName === "
|
|
450
|
+
if (toolName === "view_image") {
|
|
444
451
|
const candidates = (config?.tools?.media?.models ?? []).filter((entry) => !entry.capabilities || entry.capabilities.includes("image"));
|
|
445
452
|
const capabilityTimeoutMs = readTimeoutSecondsAsMs(config?.tools?.media?.image?.timeoutSeconds);
|
|
446
453
|
return Math.max(capabilityTimeoutMs ?? CODEX_DYNAMIC_IMAGE_TOOL_TIMEOUT_MS, ...candidates.map((entry) => readTimeoutSecondsAsMs(entry.timeoutSeconds) ?? capabilityTimeoutMs ?? CODEX_DYNAMIC_IMAGE_TOOL_TIMEOUT_MS));
|
|
@@ -641,6 +648,7 @@ function createCodexNativeHookRelay(params) {
|
|
|
641
648
|
authorizeRetentionAfterSuccessfulYield: () => {
|
|
642
649
|
successfulYieldRetentionAuthorized = true;
|
|
643
650
|
},
|
|
651
|
+
hasClaimedDirectChild: () => directChildClaims.size > 0,
|
|
644
652
|
rejectPendingDirectChild: (threadIdInput, reason) => {
|
|
645
653
|
const threadId = threadIdInput.trim();
|
|
646
654
|
const pending = threadId ? pendingDirectChildAdmissions.get(threadId) : void 0;
|
|
@@ -716,15 +724,14 @@ function buildCodexNativeHookRelayConfig(params) {
|
|
|
716
724
|
const codexEvent = CODEX_HOOK_EVENT_BY_NATIVE_EVENT[event];
|
|
717
725
|
const selected = selectedEvents.has(event);
|
|
718
726
|
const shouldRelay = params.relay.shouldRelayEvent(event);
|
|
719
|
-
|
|
720
|
-
if (!selected || !shouldRelay && !selectedNoopPreToolUse) {
|
|
727
|
+
if (!selected || !shouldRelay) {
|
|
721
728
|
if (selected || params.clearOmittedEvents) config[`hooks.${codexEvent}`] = [];
|
|
722
729
|
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
730
|
continue;
|
|
724
731
|
}
|
|
725
732
|
const timeout = normalizeHookTimeoutSec(params.hookTimeoutSec);
|
|
726
733
|
const command = params.relay.commandForEvent(event, { timeoutMs: resolveCodexNativeHookRelayCommandTimeoutMs(timeout) });
|
|
727
|
-
const matcher =
|
|
734
|
+
const matcher = buildCodexNativeToolMatcher(params.relay.toolMatcherForEvent(event));
|
|
728
735
|
config[`hooks.${codexEvent}`] = [{
|
|
729
736
|
...matcher ? { matcher } : {},
|
|
730
737
|
hooks: [{
|
|
@@ -1198,7 +1205,7 @@ async function prepareCodexRemoteWorkspaceMessageMedia(params) {
|
|
|
1198
1205
|
let attachmentEntries = 0;
|
|
1199
1206
|
const mapMediaPath = (value) => {
|
|
1200
1207
|
if (typeof value !== "string") return value;
|
|
1201
|
-
if (
|
|
1208
|
+
if (path.isAbsolute(value) && isPathStrictlyInside(gatewayMediaRoot, value)) {
|
|
1202
1209
|
attachmentEntries += 1;
|
|
1203
1210
|
gatewayManagedPaths.add(value);
|
|
1204
1211
|
return value;
|
|
@@ -1281,11 +1288,6 @@ async function prepareCodexRemoteWorkspaceMessageMedia(params) {
|
|
|
1281
1288
|
}
|
|
1282
1289
|
return mapMessageMediaValues(mappedArgs, (value) => stagedPaths.get(value) ?? value);
|
|
1283
1290
|
}
|
|
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
1291
|
async function assertGatewayManagedMediaPath(value, mediaRoot) {
|
|
1290
1292
|
const opened = await (await root(mediaRoot, { symlinks: "reject" })).open(path.relative(mediaRoot, value), { symlinks: "reject" });
|
|
1291
1293
|
try {
|
|
@@ -1322,57 +1324,80 @@ const dynamicToolBuildState = {};
|
|
|
1322
1324
|
//#endregion
|
|
1323
1325
|
//#region extensions/codex/src/app-server/shell-dynamic-tools.ts
|
|
1324
1326
|
const CODEX_NODE_EXEC_DYNAMIC_TOOL_NAME = "node_exec";
|
|
1325
|
-
const
|
|
1326
|
-
const
|
|
1327
|
+
const CODEX_GATEWAY_EXEC_DYNAMIC_TOOL_NAME = "gateway_exec";
|
|
1328
|
+
const CODEX_GATEWAY_PROCESS_DYNAMIC_TOOL_NAME = "gateway_process";
|
|
1329
|
+
const CODEX_EXEC_POLICY_PARAMETER_NAMES = /* @__PURE__ */ new Set([
|
|
1327
1330
|
"host",
|
|
1328
1331
|
"security",
|
|
1329
1332
|
"ask"
|
|
1330
1333
|
]);
|
|
1334
|
+
const CODEX_NODE_EXEC_PARAMETER_NAMES = /* @__PURE__ */ new Set([
|
|
1335
|
+
"command",
|
|
1336
|
+
"workdir",
|
|
1337
|
+
"env",
|
|
1338
|
+
"timeoutSeconds",
|
|
1339
|
+
"node"
|
|
1340
|
+
]);
|
|
1341
|
+
const PROCESS_FOLLOWUP_TEXT = "Use process (list/poll/log/write/send-keys/submit/paste/kill/clear/remove) for follow-up.";
|
|
1331
1342
|
/** Returns true when plugin config explicitly removes any named dynamic tool. */
|
|
1332
1343
|
function isCodexDynamicToolExcluded(config, names) {
|
|
1333
1344
|
const normalizedNames = new Set(names.map((name) => normalizeCodexDynamicToolName(name)));
|
|
1334
1345
|
return (config.codexDynamicToolsExclude ?? []).some((name) => normalizedNames.has(normalizeCodexDynamicToolName(name)));
|
|
1335
1346
|
}
|
|
1336
|
-
function
|
|
1337
|
-
const pinnedNode =
|
|
1347
|
+
function createExecAliasDynamicTool(execTool, params) {
|
|
1348
|
+
const pinnedNode = params.host === "node" ? params.node?.trim() : void 0;
|
|
1349
|
+
const nodeAlias = params.host === "node";
|
|
1350
|
+
const gatewayProcessAliasAvailable = params.host === "gateway" && params.processAliasAvailable;
|
|
1351
|
+
const name = nodeAlias ? CODEX_NODE_EXEC_DYNAMIC_TOOL_NAME : CODEX_GATEWAY_EXEC_DYNAMIC_TOOL_NAME;
|
|
1352
|
+
const description = nodeAlias ? pinnedNode ? "Run a shell command to completion on the OpenClaw configured remote node for this session. This tool always uses OpenClaw host=node internally and follows the existing node exec approval and allowlist policy. Remote-node background follow-up is unavailable. Use Codex's native shell for local app-server work." : "Run a shell command to completion on an OpenClaw remote node. Select the node by name or id when multiple nodes are available. This tool always uses OpenClaw host=node internally and follows the existing node exec approval and allowlist policy. Remote-node background follow-up is unavailable. Use Codex's native shell for local app-server work." : "Run a shell command through OpenClaw on the Gateway host for OpenClaw-managed Gateway environment access, including Secret Store agent-readable environment values and protected egress sentinels. Native Codex shell remains preferred for ordinary local work. This tool always uses OpenClaw host=gateway internally and follows Gateway exec approval and allowlist policy.";
|
|
1353
|
+
const followupText = nodeAlias ? "Remote-node background follow-up is unavailable. Wait for the command to complete." : gatewayProcessAliasAvailable ? "Use gateway_process (list/poll/log/write/send-keys/submit/paste/kill/clear/remove) for follow-up." : "Background session follow-up is unavailable because gateway_process is not exposed. Rerun without background=true and set yieldMs high enough to wait for completion.";
|
|
1338
1354
|
return {
|
|
1339
1355
|
...execTool,
|
|
1340
|
-
name
|
|
1341
|
-
description
|
|
1342
|
-
parameters:
|
|
1356
|
+
name,
|
|
1357
|
+
description,
|
|
1358
|
+
parameters: hideExecDynamicToolParameters(execTool.parameters, !nodeAlias || Boolean(pinnedNode), nodeAlias),
|
|
1343
1359
|
execute: async (toolCallId, args, signal, onUpdate) => {
|
|
1344
|
-
const result = await execTool.execute(toolCallId,
|
|
1360
|
+
const result = await execTool.execute(toolCallId, pinExecDynamicToolArgs(args, params.host, pinnedNode), signal, onUpdate);
|
|
1345
1361
|
return {
|
|
1346
1362
|
...result,
|
|
1347
|
-
content: result.content.map((item) => item.type === "text" ? Object.assign({}, item, { text: item.text.replace(
|
|
1363
|
+
content: result.content.map((item) => item.type === "text" ? Object.assign({}, item, { text: item.text.replace(PROCESS_FOLLOWUP_TEXT, followupText) }) : item)
|
|
1348
1364
|
};
|
|
1349
1365
|
}
|
|
1350
1366
|
};
|
|
1351
1367
|
}
|
|
1352
|
-
function
|
|
1368
|
+
function createGatewayProcessAliasDynamicTool(processTool) {
|
|
1353
1369
|
return {
|
|
1354
1370
|
...processTool,
|
|
1355
|
-
name:
|
|
1356
|
-
description: "Manage
|
|
1371
|
+
name: CODEX_GATEWAY_PROCESS_DYNAMIC_TOOL_NAME,
|
|
1372
|
+
description: "Manage background shell sessions in the existing per-session OpenClaw process scope: list, poll, log, write, send-keys, submit, paste, kill, clear, or remove. Use for gateway_exec follow-up; use native Codex shell session handling for ordinary local work."
|
|
1357
1373
|
};
|
|
1358
1374
|
}
|
|
1359
|
-
function
|
|
1360
|
-
const { host: _host, security: _security, ask: _ask, node: requestedNode, ...rest } =
|
|
1375
|
+
function pinExecDynamicToolArgs(args, host, configuredNode) {
|
|
1376
|
+
const { host: _host, security: _security, ask: _ask, node: requestedNode, ...rest } = normalizeExecDynamicToolArgs(args);
|
|
1377
|
+
if (host === "gateway") return {
|
|
1378
|
+
...rest,
|
|
1379
|
+
host
|
|
1380
|
+
};
|
|
1381
|
+
const nodeArgs = Object.fromEntries(Object.entries(rest).filter(([name]) => CODEX_NODE_EXEC_PARAMETER_NAMES.has(name)));
|
|
1361
1382
|
const node = configuredNode ?? (typeof requestedNode === "string" ? requestedNode.trim() : "");
|
|
1362
1383
|
return {
|
|
1363
|
-
...
|
|
1364
|
-
host
|
|
1384
|
+
...nodeArgs,
|
|
1385
|
+
host,
|
|
1365
1386
|
...node ? { node } : {}
|
|
1366
1387
|
};
|
|
1367
1388
|
}
|
|
1368
|
-
function
|
|
1389
|
+
function normalizeExecDynamicToolArgs(args) {
|
|
1390
|
+
return args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
1391
|
+
}
|
|
1392
|
+
function hideExecDynamicToolParameters(parameters, hideNode, nodeOnly) {
|
|
1369
1393
|
if (!parameters || typeof parameters !== "object" || Array.isArray(parameters)) return parameters;
|
|
1370
1394
|
const schema = parameters;
|
|
1371
1395
|
const rawProperties = schema.properties;
|
|
1372
1396
|
if (!rawProperties || typeof rawProperties !== "object" || Array.isArray(rawProperties)) return parameters;
|
|
1373
|
-
const
|
|
1397
|
+
const includeParameter = (name) => nodeOnly ? CODEX_NODE_EXEC_PARAMETER_NAMES.has(name) && !(hideNode && name === "node") : !CODEX_EXEC_POLICY_PARAMETER_NAMES.has(normalizeCodexDynamicToolName(name)) && !(hideNode && normalizeCodexDynamicToolName(name) === "node");
|
|
1398
|
+
const nextProperties = Object.fromEntries(Object.entries(rawProperties).filter(([name]) => includeParameter(name)));
|
|
1374
1399
|
const rawRequired = schema.required;
|
|
1375
|
-
const nextRequired = Array.isArray(rawRequired) ? rawRequired.filter((name) => typeof name !== "string" ||
|
|
1400
|
+
const nextRequired = Array.isArray(rawRequired) ? rawRequired.filter((name) => typeof name !== "string" || includeParameter(name)) : rawRequired;
|
|
1376
1401
|
return {
|
|
1377
1402
|
...schema,
|
|
1378
1403
|
properties: nextProperties,
|
|
@@ -1382,13 +1407,12 @@ function hideNodeExecDynamicToolParameters(parameters, options) {
|
|
|
1382
1407
|
//#endregion
|
|
1383
1408
|
//#region extensions/codex/src/app-server/vision-tools.ts
|
|
1384
1409
|
/**
|
|
1385
|
-
*
|
|
1386
|
-
*
|
|
1410
|
+
* Codex's enabled native surface includes its stable view_image loader. Keep
|
|
1411
|
+
* OpenClaw's view_image tool only when that surface or model vision is unavailable.
|
|
1387
1412
|
*/
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
return tools.filter((tool) => tool.name !== "image");
|
|
1413
|
+
function filterCodexVisionTools(tools, params) {
|
|
1414
|
+
if (!params.modelHasVision || !params.nativeImageInspectionEnabled) return tools;
|
|
1415
|
+
return tools.filter((tool) => tool.name !== "view_image");
|
|
1392
1416
|
}
|
|
1393
1417
|
//#endregion
|
|
1394
1418
|
//#region extensions/codex/src/app-server/dynamic-tool-build.ts
|
|
@@ -1406,6 +1430,18 @@ const CODEX_NATIVE_SANDBOX_TOOL_REQUIREMENTS = [
|
|
|
1406
1430
|
"apply_patch"
|
|
1407
1431
|
];
|
|
1408
1432
|
const CODEX_MEMORY_FLUSH_DYNAMIC_TOOL_ALLOW = /* @__PURE__ */ new Set(["read", "write"]);
|
|
1433
|
+
/** Keeps paired-device filesystem and process ownership on its native exec-server. */
|
|
1434
|
+
function resolveCodexNodePlacementToolConstructionPlan(sandbox, nativeToolSurfaceEnabled) {
|
|
1435
|
+
if (!isCodexRemoteExecPlacementSandbox(sandbox) || sandbox?.backendId !== "node" || !("placementNodeId" in sandbox) || typeof sandbox.placementNodeId !== "string" || !sandbox.placementNodeId) return;
|
|
1436
|
+
if (!nativeToolSurfaceEnabled) throw new Error("Codex paired-device remote execution requires its native exec-server tool surface; adjust the session tool policy and start a fresh attempt.");
|
|
1437
|
+
return {
|
|
1438
|
+
includeBaseCodingTools: false,
|
|
1439
|
+
includeShellTools: false,
|
|
1440
|
+
includeChannelTools: true,
|
|
1441
|
+
includeOpenClawTools: true,
|
|
1442
|
+
includePluginTools: true
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1409
1445
|
function preserveRingZeroSystemAgentTool(allTools, filteredTools) {
|
|
1410
1446
|
const openclaw = allTools.find((tool) => tool.name === "openclaw" && tool.catalogMode === "direct-only");
|
|
1411
1447
|
if (!openclaw) return filteredTools;
|
|
@@ -1497,101 +1533,123 @@ async function buildDynamicTools(input) {
|
|
|
1497
1533
|
const injectedOpenClawCodingToolsFactory = dynamicToolBuildState.openClawCodingToolsFactory;
|
|
1498
1534
|
let agentHarnessModule;
|
|
1499
1535
|
const loadAgentHarnessModule = async () => agentHarnessModule ??= await import("openclaw/plugin-sdk/agent-harness");
|
|
1500
|
-
const createOpenClawCodingTools = injectedOpenClawCodingToolsFactory ?? (await loadAgentHarnessModule()).createOpenClawCodingTools;
|
|
1501
1536
|
toolBuildStages.mark("load-agent-harness-tools");
|
|
1502
1537
|
const sessionKeys = resolveOpenClawCodingToolsSessionKeys(params, input.sandboxSessionKey);
|
|
1503
1538
|
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
|
-
}),
|
|
1539
|
+
const webSearchPlan = resolveCodexWebSearchPlan({
|
|
1547
1540
|
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
|
-
|
|
1541
|
+
disableTools: params.disableTools,
|
|
1542
|
+
nativeToolSurfaceEnabled: input.nativeToolSurfaceEnabled,
|
|
1543
|
+
nativeProviderWebSearchSupport: input.nativeProviderWebSearchSupport
|
|
1544
|
+
});
|
|
1545
|
+
const webFetchHostnameAllowlistRef = {};
|
|
1546
|
+
const buildOpenClawCodingTools = () => {
|
|
1547
|
+
const toolConstructionPlan = resolveCodexNodePlacementToolConstructionPlan(input.sandbox, input.nativeToolSurfaceEnabled);
|
|
1548
|
+
const options = {
|
|
1549
|
+
agentId: input.sessionAgentId,
|
|
1550
|
+
...buildEmbeddedAttemptToolRunContext(params),
|
|
1551
|
+
exec: {
|
|
1552
|
+
...params.execOverrides,
|
|
1553
|
+
...resolveCodexNodeExecToolOverrides(nativeExecutionPolicy),
|
|
1554
|
+
config: params.config,
|
|
1555
|
+
elevated: params.bashElevated
|
|
1556
|
+
},
|
|
1557
|
+
sessionPermissionPolicy: params.permissionMode && params.sessionRoot ? {
|
|
1558
|
+
mode: params.permissionMode,
|
|
1559
|
+
root: params.sessionRoot
|
|
1560
|
+
} : void 0,
|
|
1561
|
+
sandbox: input.sandbox,
|
|
1562
|
+
...toolConstructionPlan ? { toolConstructionPlan } : {},
|
|
1563
|
+
messageProvider: resolveCodexMessageToolProvider(params),
|
|
1564
|
+
toolPolicyMessageProvider: params.messageProvider ?? params.messageChannel,
|
|
1565
|
+
clientCaps: params.clientCaps,
|
|
1566
|
+
chatType: params.chatType,
|
|
1567
|
+
agentAccountId: params.agentAccountId,
|
|
1568
|
+
messageTo: params.messageTo,
|
|
1569
|
+
messageThreadId: params.messageThreadId,
|
|
1570
|
+
nativeChannelId: params.chatId,
|
|
1571
|
+
messageActionTurnCapability: params.messageActionTurnCapability,
|
|
1572
|
+
groupId: params.groupId,
|
|
1573
|
+
groupChannel: params.groupChannel,
|
|
1574
|
+
groupSpace: params.groupSpace,
|
|
1575
|
+
spawnedBy: params.spawnedBy,
|
|
1576
|
+
senderId: params.senderId,
|
|
1577
|
+
senderName: params.senderName,
|
|
1578
|
+
senderUsername: params.senderUsername,
|
|
1579
|
+
senderE164: params.senderE164,
|
|
1580
|
+
senderIsOwner: params.senderIsOwner,
|
|
1581
|
+
inputProvenance: params.inputProvenance,
|
|
1582
|
+
trustedInternalHandoff: params.trustedInternalHandoff,
|
|
1583
|
+
scheduledToolPolicy: params.scheduledToolPolicy,
|
|
1584
|
+
allowGatewaySubagentBinding: params.allowGatewaySubagentBinding || isForcedPrivateQaCodexRuntime(),
|
|
1585
|
+
...sessionKeys,
|
|
1586
|
+
sessionId: params.sessionId,
|
|
1587
|
+
runId: params.runId,
|
|
1588
|
+
approvalReviewerDeviceId: params.approvalReviewerDeviceId,
|
|
1589
|
+
agentDir,
|
|
1590
|
+
preparedModelRuntime: params.preparedModelRuntime,
|
|
1591
|
+
cwd: input.effectiveCwd ?? input.effectiveWorkspace,
|
|
1592
|
+
workspaceDir: input.effectiveWorkspace,
|
|
1593
|
+
spawnWorkspaceDir: input.effectiveCwd && input.effectiveCwd !== input.effectiveWorkspace ? input.resolvedWorkspace : resolveAttemptSpawnWorkspaceDir({
|
|
1594
|
+
sandbox: input.sandbox,
|
|
1595
|
+
resolvedWorkspace: input.resolvedWorkspace
|
|
1596
|
+
}),
|
|
1597
|
+
config: params.config,
|
|
1598
|
+
authProfileStore: params.toolAuthProfileStore ?? params.authProfileStore,
|
|
1599
|
+
abortSignal: input.runAbortController.signal,
|
|
1600
|
+
emitBeforeToolCallDiagnostics: false,
|
|
1601
|
+
modelProvider: params.model.provider,
|
|
1602
|
+
modelId: params.modelId,
|
|
1603
|
+
modelCompat: params.model.compat && typeof params.model.compat === "object" ? params.model.compat : void 0,
|
|
1604
|
+
modelApi: params.model.api,
|
|
1605
|
+
modelContextWindowTokens: params.model.contextWindow,
|
|
1606
|
+
delegationCapability: params.delegationCapability,
|
|
1607
|
+
modelAuthMode: resolveModelAuthMode(params.model.provider, params.config, params.toolAuthProfileStore ?? params.authProfileStore, { workspaceDir: input.effectiveWorkspace }),
|
|
1608
|
+
suppressManagedWebSearch: false,
|
|
1609
|
+
webFetchHostnameAllowlistRef,
|
|
1610
|
+
currentChannelId: params.currentChannelId,
|
|
1611
|
+
currentMessagingTarget: params.currentMessagingTarget,
|
|
1612
|
+
hookChannelId: resolveCodexAppServerHookChannelId(params, input.sandboxSessionKey),
|
|
1613
|
+
currentThreadTs: params.currentThreadTs,
|
|
1614
|
+
currentMessageId: params.currentMessageId,
|
|
1615
|
+
replyToMode: params.replyToMode,
|
|
1616
|
+
hasRepliedRef: params.hasRepliedRef,
|
|
1617
|
+
modelHasVision,
|
|
1618
|
+
computerContextEpoch: input.computerContextEpoch,
|
|
1619
|
+
requireExplicitMessageTarget: params.requireExplicitMessageTarget ?? isSubagentSessionKey(params.sessionKey),
|
|
1620
|
+
sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
|
|
1621
|
+
taskSuggestionDeliveryMode: params.taskSuggestionDeliveryMode,
|
|
1622
|
+
disableMessageTool: input.ignoreDisableMessageTool ? false : params.disableMessageTool,
|
|
1623
|
+
forceMessageTool: shouldForceMessageTool(messagePolicyParams),
|
|
1624
|
+
enableHeartbeatTool: params.trigger === "heartbeat" || input.forceHeartbeatTool === true,
|
|
1625
|
+
forceHeartbeatTool: params.trigger === "heartbeat" || input.forceHeartbeatTool === true,
|
|
1626
|
+
onYield: (message, acknowledgment) => {
|
|
1627
|
+
input.onYieldDetected(acknowledgment);
|
|
1628
|
+
input.onCodexAppServerEvent?.({
|
|
1629
|
+
stream: "codex_app_server.tool",
|
|
1630
|
+
data: {
|
|
1631
|
+
name: "sessions_yield",
|
|
1632
|
+
message
|
|
1633
|
+
}
|
|
1634
|
+
});
|
|
1635
|
+
},
|
|
1636
|
+
claimYieldCompletion: input.claimYieldCompletion,
|
|
1637
|
+
recordToolPrepStage: (name) => {
|
|
1638
|
+
toolBuildStages.mark(name);
|
|
1639
|
+
},
|
|
1640
|
+
onToolOutcome: params.onToolOutcome,
|
|
1641
|
+
isTurnTainted: params.isTurnTainted,
|
|
1642
|
+
allocateToolOutcomeOrdinal: params.allocateToolOutcomeOrdinal,
|
|
1643
|
+
cronCreatorToolAllowlistRef: input.cronCreatorToolAllowlistRef,
|
|
1644
|
+
cronCreatorToolAllowlistCaptureRef: input.cronCreatorToolAllowlistCaptureRef,
|
|
1645
|
+
cronCreatorAuthorityUnavailableReason: input.cronCreatorAuthorityUnavailableReason
|
|
1646
|
+
};
|
|
1647
|
+
const bindingOptions = { cwd: input.effectiveCwd ?? input.effectiveWorkspace };
|
|
1648
|
+
if (injectedOpenClawCodingToolsFactory) return params.hostCapabilities.bindToolSurface(injectedOpenClawCodingToolsFactory(options), bindingOptions);
|
|
1649
|
+
const createToolSurface = params.hostCapabilities.createToolSurface;
|
|
1650
|
+
if (!createToolSurface) throw new Error("Codex tool construction requires a current host capability");
|
|
1651
|
+
return createToolSurface(options, bindingOptions);
|
|
1652
|
+
};
|
|
1595
1653
|
const allTools = input.resolveCronCreatorToolAuthority ? runWithCronCreatorAuthorityCapabilityResolver({
|
|
1596
1654
|
capability: params.cronCreatorAuthorityCapability,
|
|
1597
1655
|
runId: params.runId,
|
|
@@ -1602,20 +1660,14 @@ async function buildDynamicTools(input) {
|
|
|
1602
1660
|
const preNormalizationDiagnostics = [];
|
|
1603
1661
|
const readableAllToolProjection = filterProviderNormalizableTools(allTools);
|
|
1604
1662
|
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
1663
|
const readableAllTools = [...readableAllToolProjection.tools];
|
|
1612
1664
|
const normallyProfiledTools = input.nativeToolSurfaceEnabled === false ? filterCodexDynamicToolsForDisabledNativeSurface(readableAllTools, input.pluginConfig, { preserveShell: shouldKeepOpenClawShellDynamicTools(input, nativeExecutionPolicy) }) : filterCodexDynamicTools(readableAllTools, input.pluginConfig);
|
|
1613
1665
|
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);
|
|
1666
|
+
const codexFilteredTools = addNodeShellDynamicToolsIfNeeded(addGatewayShellDynamicToolsIfAvailable(addSandboxShellDynamicToolsIfAvailable(isCodexMemoryFlushRun(params) ? filterCodexMemoryFlushDynamicTools(readableAllTools) : profileFilteredTools, readableAllTools, input), readableAllTools, input, nativeExecutionPolicy), readableAllTools, input, nativeExecutionPolicy);
|
|
1615
1667
|
toolBuildStages.mark("codex-filtering");
|
|
1616
|
-
const visionFilteredTools =
|
|
1668
|
+
const visionFilteredTools = filterCodexVisionTools(codexFilteredTools, {
|
|
1617
1669
|
modelHasVision,
|
|
1618
|
-
|
|
1670
|
+
nativeImageInspectionEnabled: input.nativeToolSurfaceEnabled === true
|
|
1619
1671
|
});
|
|
1620
1672
|
toolBuildStages.mark("vision-filtering");
|
|
1621
1673
|
const webSearchPresent = visionFilteredTools.some((tool) => tool.name === "web_search");
|
|
@@ -1662,7 +1714,9 @@ async function buildDynamicTools(input) {
|
|
|
1662
1714
|
onPreNormalizationSchemaDiagnostics: (diagnostics) => preNormalizationDiagnostics.push(...diagnostics)
|
|
1663
1715
|
});
|
|
1664
1716
|
toolBuildStages.mark("runtime-normalization");
|
|
1665
|
-
|
|
1717
|
+
const webSearchAllowed = normalizedTools.some((tool) => tool.name === "web_search");
|
|
1718
|
+
webFetchHostnameAllowlistRef.value = webSearchAllowed ? webSearchPlan.webFetchHostnameAllowlist : void 0;
|
|
1719
|
+
input.onWebSearchPolicyResolved?.(webSearchAllowed);
|
|
1666
1720
|
const exposedTools = webSearchPlan.suppressManagedWebSearch ? normalizedTools.filter((tool) => tool.name !== "web_search") : normalizedTools;
|
|
1667
1721
|
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
1722
|
runId: params.runId,
|
|
@@ -1695,6 +1749,23 @@ async function buildDynamicTools(input) {
|
|
|
1695
1749
|
}
|
|
1696
1750
|
return exposedTools;
|
|
1697
1751
|
}
|
|
1752
|
+
/** Keeps the OpenClaw Gateway execution path available beside Codex native shell. */
|
|
1753
|
+
function addGatewayShellDynamicToolsIfAvailable(filteredTools, allTools, input, executionPolicy) {
|
|
1754
|
+
if (isCodexMemoryFlushRun(input.params) || input.nativeToolSurfaceEnabled !== true || input.sandbox?.enabled === true || !executionPolicy.nativeToolSurfaceAllowed || executionPolicy.effectiveExecHost !== "gateway") return filteredTools;
|
|
1755
|
+
const execTool = allTools.find((tool) => normalizeCodexDynamicToolName(tool.name) === "exec");
|
|
1756
|
+
const processTool = allTools.find((tool) => normalizeCodexDynamicToolName(tool.name) === "process");
|
|
1757
|
+
const existingNames = new Set(filteredTools.map((tool) => normalizeCodexDynamicToolName(tool.name)));
|
|
1758
|
+
const execExcluded = isCodexDynamicToolExcluded(input.pluginConfig, ["exec", CODEX_GATEWAY_EXEC_DYNAMIC_TOOL_NAME]);
|
|
1759
|
+
if (!execTool || execExcluded || existingNames.has("gateway_exec")) return filteredTools;
|
|
1760
|
+
const processExcluded = isCodexDynamicToolExcluded(input.pluginConfig, ["process", CODEX_GATEWAY_PROCESS_DYNAMIC_TOOL_NAME]);
|
|
1761
|
+
const processAliasAvailable = Boolean(processTool && !processExcluded && !existingNames.has("gateway_process"));
|
|
1762
|
+
const toolsToAppend = [createExecAliasDynamicTool(execTool, {
|
|
1763
|
+
host: "gateway",
|
|
1764
|
+
processAliasAvailable
|
|
1765
|
+
})];
|
|
1766
|
+
if (processAliasAvailable && processTool) toolsToAppend.push(createGatewayProcessAliasDynamicTool(processTool));
|
|
1767
|
+
return [...filteredTools, ...toolsToAppend];
|
|
1768
|
+
}
|
|
1698
1769
|
/** Preserves delivery-critical tools when a narrow allowlist would otherwise hide them. */
|
|
1699
1770
|
function includeForcedCodexDynamicToolAllow(toolsAllow, params) {
|
|
1700
1771
|
if (toolsAllow === void 0 || hasWildcardCodexToolsAllow(toolsAllow)) return toolsAllow;
|
|
@@ -1710,6 +1781,11 @@ function shouldEnableCodexAppServerNativeToolSurface(params, sandbox, options =
|
|
|
1710
1781
|
if (params.pluginHarnessToolPolicyRestricted === true) return false;
|
|
1711
1782
|
if (isCodexMemoryFlushRun(params)) return false;
|
|
1712
1783
|
if (params.disableTools) return false;
|
|
1784
|
+
if (isCodexNativeExecutionBlockedByNodeExecHost(params, {
|
|
1785
|
+
agentId: options.agentId,
|
|
1786
|
+
runtimeSessionKey: options.runtimeSessionKey,
|
|
1787
|
+
sandbox
|
|
1788
|
+
})) return false;
|
|
1713
1789
|
const toolsAllow = includeForcedCodexDynamicToolAllow(params.toolsAllow, params);
|
|
1714
1790
|
if (toolsAllow === void 0) return canCodexAppServerNativeToolSurfaceHonorSandbox(sandbox, options);
|
|
1715
1791
|
return hasWildcardCodexToolsAllow(toolsAllow) && canCodexAppServerNativeToolSurfaceHonorSandbox(sandbox, options);
|
|
@@ -1731,7 +1807,7 @@ function resolveCodexRuntimePolicySessionKey(params, runtimeSessionKey) {
|
|
|
1731
1807
|
}
|
|
1732
1808
|
function canCodexAppServerNativeToolSurfaceHonorSandbox(sandbox, options = {}) {
|
|
1733
1809
|
if (!sandbox?.enabled) return true;
|
|
1734
|
-
if (options.sandboxExecServerEnabled === true && sandbox.backend && canSandboxToolPolicyExposeCodexNativeToolSurface(sandbox)) return true;
|
|
1810
|
+
if (options.sandboxExecServerEnabled === true && (sandbox.backend || isCodexRemoteExecPlacementSandbox(sandbox)) && canSandboxToolPolicyExposeCodexNativeToolSurface(sandbox)) return true;
|
|
1735
1811
|
return false;
|
|
1736
1812
|
}
|
|
1737
1813
|
function canSandboxToolPolicyExposeCodexNativeToolSurface(sandbox) {
|
|
@@ -1807,7 +1883,7 @@ function addSandboxShellDynamicToolsIfAvailable(filteredTools, allTools, input)
|
|
|
1807
1883
|
const sandboxProcessTool = {
|
|
1808
1884
|
...processTool,
|
|
1809
1885
|
name: "sandbox_process",
|
|
1810
|
-
description: "Manage
|
|
1886
|
+
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
1887
|
};
|
|
1812
1888
|
return [
|
|
1813
1889
|
...filteredTools,
|
|
@@ -1839,12 +1915,12 @@ function addNodeShellDynamicToolsIfNeeded(filteredTools, allTools, input, nodePo
|
|
|
1839
1915
|
const nodeExecAvailableFromAuto = nodePolicy.requestedExecHost === "auto" && nodePolicy.effectiveExecHost === "gateway";
|
|
1840
1916
|
if (!nodeExecIsDefault && !nodeExecAvailableFromAuto) return filteredTools;
|
|
1841
1917
|
const execTool = allTools.find((tool) => normalizeCodexDynamicToolName(tool.name) === "exec");
|
|
1842
|
-
|
|
1843
|
-
if (!
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
return
|
|
1918
|
+
if (!execTool) return filteredTools;
|
|
1919
|
+
if (!isCodexDynamicToolExcluded(input.pluginConfig, ["exec", "node_exec"]) && !filteredTools.some((tool) => normalizeCodexDynamicToolName(tool.name) === "node_exec")) return [...filteredTools, createExecAliasDynamicTool(execTool, {
|
|
1920
|
+
host: "node",
|
|
1921
|
+
node: nodePolicy.node
|
|
1922
|
+
})];
|
|
1923
|
+
return filteredTools;
|
|
1848
1924
|
}
|
|
1849
1925
|
function shouldKeepOpenClawShellDynamicTools(input, nodePolicy) {
|
|
1850
1926
|
return !isCodexMemoryFlushRun(input.params) && input.nativeToolSurfaceEnabled === false && input.sandbox?.enabled !== true && nodePolicy.effectiveExecHost !== "node";
|
|
@@ -1868,7 +1944,7 @@ function filterCodexDynamicToolsForAllowlist(tools, toolsAllow) {
|
|
|
1868
1944
|
const allowSet = new Set(toolsAllow.map((name) => normalizeCodexDynamicToolName(name)).filter(Boolean));
|
|
1869
1945
|
return tools.filter((tool) => {
|
|
1870
1946
|
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 === "
|
|
1947
|
+
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
1948
|
});
|
|
1873
1949
|
}
|
|
1874
1950
|
/** Detects the wildcard allowlist marker after Codex tool-name normalization. */
|
|
@@ -1880,9 +1956,317 @@ function shouldForceMessageTool(params) {
|
|
|
1880
1956
|
return params.disableMessageTool !== true && params.sourceReplyDeliveryMode === "message_tool_only";
|
|
1881
1957
|
}
|
|
1882
1958
|
//#endregion
|
|
1959
|
+
//#region extensions/codex/src/app-server/sandbox-exec-server-node-relay.ts
|
|
1960
|
+
const CODEX_NODE_EXEC_SERVER_MAX_MESSAGE_BYTES$1 = 64 * 1024 * 1024;
|
|
1961
|
+
const CODEX_NODE_EXEC_SERVER_MAX_FAILURE_DETAIL_CHARS = 240;
|
|
1962
|
+
const CODEX_NODE_HTTP_CREDENTIAL_BODY_MAX_BYTES = 1024 * 1024;
|
|
1963
|
+
const CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_FIELDS = 256;
|
|
1964
|
+
const CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_DEPTH = 8;
|
|
1965
|
+
const CODEX_NODE_HTTP_CREDENTIAL_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
1966
|
+
"authorization",
|
|
1967
|
+
"proxy-authorization",
|
|
1968
|
+
"cookie",
|
|
1969
|
+
"set-cookie",
|
|
1970
|
+
"x-api-key",
|
|
1971
|
+
"api-key",
|
|
1972
|
+
"apikey",
|
|
1973
|
+
"x-auth-token",
|
|
1974
|
+
"auth-token",
|
|
1975
|
+
"x-access-token",
|
|
1976
|
+
"access-token",
|
|
1977
|
+
"x-secret-key",
|
|
1978
|
+
"secret-key",
|
|
1979
|
+
"x-goog-api-key",
|
|
1980
|
+
"x-vault-token",
|
|
1981
|
+
"x-api-token"
|
|
1982
|
+
]);
|
|
1983
|
+
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;
|
|
1984
|
+
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;
|
|
1985
|
+
const nodeExecServerTextDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
1986
|
+
/** Produces the bounded, redacted terminal failure shared by pending and claimed node leases. */
|
|
1987
|
+
function createCodexNodeExecServerDisconnectError(reason, cause) {
|
|
1988
|
+
const detail = cause === void 0 ? "" : `: ${truncateUtf16Safe(redactSensitiveText(formatErrorMessage$1(cause), { mode: "tools" }), CODEX_NODE_EXEC_SERVER_MAX_FAILURE_DETAIL_CHARS)}`;
|
|
1989
|
+
return /* @__PURE__ */ new Error(`Codex paired execution device disconnected; start a fresh attempt. (${reason}${detail})`);
|
|
1990
|
+
}
|
|
1991
|
+
/** Relays one authorized, single-use Codex exec-server channel without interpreting its protocol. */
|
|
1992
|
+
async function startCodexNodeExecServerRelay(params) {
|
|
1993
|
+
const { channel } = params.lease;
|
|
1994
|
+
const { socket } = params;
|
|
1995
|
+
let closed = false;
|
|
1996
|
+
const { promise: finished, resolve: finish } = createDeferred();
|
|
1997
|
+
let unsubscribe = () => {};
|
|
1998
|
+
const closeBoth = (code = 1001, reason = "execution channel closed") => {
|
|
1999
|
+
if (closed) return;
|
|
2000
|
+
closed = true;
|
|
2001
|
+
unsubscribe();
|
|
2002
|
+
params.lease.closeRelay = void 0;
|
|
2003
|
+
params.lease.onChannelClosed = void 0;
|
|
2004
|
+
if (!params.lease.closed) {
|
|
2005
|
+
params.lease.closed = true;
|
|
2006
|
+
channel.close();
|
|
2007
|
+
}
|
|
2008
|
+
if (socket.readyState === socket.OPEN || socket.readyState === socket.CONNECTING) socket.close(code, reason);
|
|
2009
|
+
finish();
|
|
2010
|
+
};
|
|
2011
|
+
params.lease.closeRelay = closeBoth;
|
|
2012
|
+
const failUnexpectedly = (code, reason, cause) => {
|
|
2013
|
+
if (!closed && !params.lease.closed) params.lease.onDisconnected?.(createCodexNodeExecServerDisconnectError(reason, cause));
|
|
2014
|
+
closeBoth(code, reason);
|
|
2015
|
+
};
|
|
2016
|
+
params.lease.onChannelClosed = ({ failed, error }) => failUnexpectedly(failed ? 1011 : 1001, failed ? "execution device failed" : "execution device disconnected", error);
|
|
2017
|
+
socket.once("close", () => failUnexpectedly(1001, "execution socket closed"));
|
|
2018
|
+
socket.once("error", () => failUnexpectedly(1011, "execution socket failed"));
|
|
2019
|
+
let toNode = Promise.resolve();
|
|
2020
|
+
socket.on("message", (data) => {
|
|
2021
|
+
if (closed) return;
|
|
2022
|
+
socket.pause();
|
|
2023
|
+
toNode = toNode.then(async () => {
|
|
2024
|
+
const frame = normalizeCodexExecServerFrame(data);
|
|
2025
|
+
const request = validateCodexExecServerMessage(frame);
|
|
2026
|
+
const rejection = rejectCredentialedCodexNodeHttpRequest(request);
|
|
2027
|
+
if (rejection) await sendCodexExecServerFrame(socket, rejection);
|
|
2028
|
+
else await channel.send(sanitizeCodexExecServerRequest(frame, request));
|
|
2029
|
+
if (!closed) socket.resume();
|
|
2030
|
+
}).catch((error) => {
|
|
2031
|
+
failUnexpectedly(error instanceof RangeError ? 1009 : 1007, "invalid execution message");
|
|
2032
|
+
});
|
|
2033
|
+
});
|
|
2034
|
+
unsubscribe = channel.onMessage(async (message) => {
|
|
2035
|
+
if (closed) return;
|
|
2036
|
+
try {
|
|
2037
|
+
const frame = normalizeCodexExecServerFrame(message);
|
|
2038
|
+
validateCodexExecServerMessage(frame);
|
|
2039
|
+
await sendCodexExecServerFrame(socket, frame);
|
|
2040
|
+
} catch (error) {
|
|
2041
|
+
failUnexpectedly(error instanceof RangeError ? 1009 : 1007, "invalid device message");
|
|
2042
|
+
}
|
|
2043
|
+
});
|
|
2044
|
+
await finished;
|
|
2045
|
+
}
|
|
2046
|
+
function sendCodexExecServerFrame(socket, frame) {
|
|
2047
|
+
return new Promise((resolve, reject) => {
|
|
2048
|
+
socket.send(frame, { binary: false }, (error) => {
|
|
2049
|
+
if (error) {
|
|
2050
|
+
reject(error);
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
2053
|
+
resolve();
|
|
2054
|
+
});
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
2057
|
+
function normalizeCodexExecServerFrame(data) {
|
|
2058
|
+
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);
|
|
2059
|
+
if (frame.length > CODEX_NODE_EXEC_SERVER_MAX_MESSAGE_BYTES$1) throw new RangeError("Codex exec-server message exceeds its 64 MiB limit.");
|
|
2060
|
+
if (frame.includes(10) || frame.includes(13)) throw new Error("Codex exec-server messages must occupy exactly one stdio line.");
|
|
2061
|
+
return frame;
|
|
2062
|
+
}
|
|
2063
|
+
function validateCodexExecServerMessage(frame) {
|
|
2064
|
+
const parsed = JSON.parse(nodeExecServerTextDecoder.decode(frame));
|
|
2065
|
+
if (!isRecord(parsed)) throw new Error("Codex exec-server message must be a JSON object.");
|
|
2066
|
+
return parsed;
|
|
2067
|
+
}
|
|
2068
|
+
function rejectCredentialedCodexNodeHttpRequest(request) {
|
|
2069
|
+
if (request.method !== "http/request") return;
|
|
2070
|
+
if (!isRecord(request.params)) throw new Error("Codex http/request params must be an object.");
|
|
2071
|
+
const headers = request.params.headers ?? [];
|
|
2072
|
+
if (!Array.isArray(headers)) throw new Error("Codex http/request headers must be an array.");
|
|
2073
|
+
if (headers.length > CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_FIELDS) return createCredentialedCodexNodeHttpRejection(request);
|
|
2074
|
+
let credentialBearing = false;
|
|
2075
|
+
let contentType;
|
|
2076
|
+
for (const header of headers) {
|
|
2077
|
+
if (!isRecord(header) || typeof header.name !== "string" || typeof header.value !== "string") throw new Error("Codex http/request headers must contain string names and values.");
|
|
2078
|
+
const name = header.name.trim().toLowerCase();
|
|
2079
|
+
if (CODEX_NODE_HTTP_CREDENTIAL_HEADER_NAMES.has(name) || CODEX_NODE_HTTP_CREDENTIAL_HEADER_NAME_PATTERN.test(name) || hasSensitiveCodexNodeText(header.value)) credentialBearing = true;
|
|
2080
|
+
if (name === "content-type") {
|
|
2081
|
+
const declared = header.value.split(";", 1)[0]?.trim().toLowerCase();
|
|
2082
|
+
credentialBearing ||= Boolean(contentType && contentType !== declared);
|
|
2083
|
+
contentType = declared;
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
if (!credentialBearing && typeof request.params.url === "string") credentialBearing = hasCredentialedCodexNodeHttpUrl(request.params.url);
|
|
2087
|
+
if (!credentialBearing && request.params.bodyBase64 != null) credentialBearing = hasCredentialedCodexNodeHttpBody(request.params.bodyBase64, contentType);
|
|
2088
|
+
return credentialBearing ? createCredentialedCodexNodeHttpRejection(request) : void 0;
|
|
2089
|
+
}
|
|
2090
|
+
function hasCredentialedCodexNodeHttpUrl(value) {
|
|
2091
|
+
let url;
|
|
2092
|
+
try {
|
|
2093
|
+
url = new URL(value);
|
|
2094
|
+
} catch {
|
|
2095
|
+
throw new Error("Codex http/request URL must be valid.");
|
|
2096
|
+
}
|
|
2097
|
+
if (url.username || url.password || hasSensitiveCodexNodeText(value)) return true;
|
|
2098
|
+
let fields = 0;
|
|
2099
|
+
const hasCredentialedParameter = (name, parameterValue) => ++fields > CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_FIELDS || isCodexNodeCredentialField(name) || hasSensitiveCodexNodeText(parameterValue);
|
|
2100
|
+
for (const parameters of [url.searchParams, new URLSearchParams(url.hash.slice(1))]) for (const [name, parameterValue] of parameters) if (hasCredentialedParameter(name, parameterValue)) return true;
|
|
2101
|
+
for (const initial of [
|
|
2102
|
+
url.pathname,
|
|
2103
|
+
url.hash.slice(1),
|
|
2104
|
+
...url.searchParams.values()
|
|
2105
|
+
]) {
|
|
2106
|
+
let component = initial;
|
|
2107
|
+
for (let depth = 0; depth <= CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_DEPTH; depth += 1) {
|
|
2108
|
+
for (const nestedQuery of component.split(/[?#]/u).slice(1)) for (const [name, parameterValue] of new URLSearchParams(nestedQuery)) if (hasCredentialedParameter(name, parameterValue)) return true;
|
|
2109
|
+
for (const segment of component.split("/")) for (const parameter of segment.split(";").slice(1)) {
|
|
2110
|
+
const separator = parameter.indexOf("=");
|
|
2111
|
+
if (hasCredentialedParameter(separator < 0 ? parameter : parameter.slice(0, separator), separator < 0 ? "" : parameter.slice(separator + 1))) return true;
|
|
2112
|
+
}
|
|
2113
|
+
if (!/%[\da-f]{2}/iu.test(component)) break;
|
|
2114
|
+
if (depth === CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_DEPTH) return true;
|
|
2115
|
+
try {
|
|
2116
|
+
component = decodeURIComponent(component);
|
|
2117
|
+
} catch {
|
|
2118
|
+
return true;
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
return false;
|
|
2123
|
+
}
|
|
2124
|
+
function hasCredentialedCodexNodeHttpBody(value, contentType) {
|
|
2125
|
+
if (typeof value !== "string") throw new Error("Codex http/request bodyBase64 must be a string.");
|
|
2126
|
+
const declaredText = contentType === "text/plain";
|
|
2127
|
+
if (!declaredText && value.length > Math.ceil(CODEX_NODE_HTTP_CREDENTIAL_BODY_MAX_BYTES / 3) * 4) return true;
|
|
2128
|
+
let body;
|
|
2129
|
+
try {
|
|
2130
|
+
const decoded = Buffer.from(value, "base64");
|
|
2131
|
+
if (!declaredText && decoded.length > CODEX_NODE_HTTP_CREDENTIAL_BODY_MAX_BYTES || decoded.toString("base64") !== value) return true;
|
|
2132
|
+
body = nodeExecServerTextDecoder.decode(decoded);
|
|
2133
|
+
} catch {
|
|
2134
|
+
return true;
|
|
2135
|
+
}
|
|
2136
|
+
if (!body) return false;
|
|
2137
|
+
if (hasSensitiveCodexNodeText(body)) return true;
|
|
2138
|
+
const declaredJson = contentType === "application/json" || contentType?.endsWith("+json") === true;
|
|
2139
|
+
const declaredForm = contentType === "application/x-www-form-urlencoded";
|
|
2140
|
+
if (contentType && !declaredJson && !declaredForm && !declaredText) return true;
|
|
2141
|
+
const trimmed = body.trimStart();
|
|
2142
|
+
if (trimmed.startsWith("<")) return true;
|
|
2143
|
+
if (declaredJson || trimmed.startsWith("{") || trimmed.startsWith("[")) try {
|
|
2144
|
+
if (hasCredentialedCodexNodeRawJsonStrings(body)) return true;
|
|
2145
|
+
return hasCredentialedCodexNodeJsonFields(JSON.parse(body));
|
|
2146
|
+
} catch {
|
|
2147
|
+
if (!declaredText) return true;
|
|
2148
|
+
}
|
|
2149
|
+
if (!declaredForm && !body.includes("=")) return !declaredText;
|
|
2150
|
+
let fields = 0;
|
|
2151
|
+
for (const [name, parameterValue] of new URLSearchParams(body)) if (++fields > CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_FIELDS || isCodexNodeCredentialField(name) || hasSensitiveCodexNodeText(parameterValue)) return true;
|
|
2152
|
+
return false;
|
|
2153
|
+
}
|
|
2154
|
+
function hasCredentialedCodexNodeRawJsonStrings(body) {
|
|
2155
|
+
const tokens = body.matchAll(/("(?:\\.|[^"\\])*")(\s*:)?/gu);
|
|
2156
|
+
let fields = 0;
|
|
2157
|
+
for (const match of tokens) {
|
|
2158
|
+
const value = JSON.parse(match[1]);
|
|
2159
|
+
if (++fields > CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_FIELDS || typeof value !== "string" || hasSensitiveCodexNodeText(value) || match[2] !== void 0 && isCodexNodeCredentialField(value)) return true;
|
|
2160
|
+
}
|
|
2161
|
+
return false;
|
|
2162
|
+
}
|
|
2163
|
+
function hasCredentialedCodexNodeJsonFields(value) {
|
|
2164
|
+
const pending = [{
|
|
2165
|
+
value,
|
|
2166
|
+
depth: 0
|
|
2167
|
+
}];
|
|
2168
|
+
let fields = 0;
|
|
2169
|
+
while (pending.length > 0) {
|
|
2170
|
+
const current = pending.pop();
|
|
2171
|
+
if (typeof current?.value === "string" && hasSensitiveCodexNodeText(current.value)) return true;
|
|
2172
|
+
if (!current || !Array.isArray(current.value) && !isRecord(current.value)) continue;
|
|
2173
|
+
if (current.depth >= CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_DEPTH) return true;
|
|
2174
|
+
const entries = Array.isArray(current.value) ? current.value.map((entry) => [void 0, entry]) : Object.entries(current.value);
|
|
2175
|
+
for (const [name, nested] of entries) {
|
|
2176
|
+
if (++fields > CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_FIELDS || typeof name === "string" && isCodexNodeCredentialField(name)) return true;
|
|
2177
|
+
pending.push({
|
|
2178
|
+
value: nested,
|
|
2179
|
+
depth: current.depth + 1
|
|
2180
|
+
});
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
return false;
|
|
2184
|
+
}
|
|
2185
|
+
function hasSensitiveCodexNodeText(value) {
|
|
2186
|
+
let decoded = value;
|
|
2187
|
+
for (let depth = 0; depth <= CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_DEPTH; depth += 1) {
|
|
2188
|
+
if (redactToolPayloadText(decoded) !== decoded) return true;
|
|
2189
|
+
if (!/%[\da-f]{2}/iu.test(decoded)) return false;
|
|
2190
|
+
try {
|
|
2191
|
+
decoded = decodeURIComponent(decoded);
|
|
2192
|
+
} catch {
|
|
2193
|
+
return true;
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
return true;
|
|
2197
|
+
}
|
|
2198
|
+
function isCodexNodeCredentialField(value) {
|
|
2199
|
+
let decoded = value;
|
|
2200
|
+
for (let depth = 0; depth < CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_DEPTH; depth += 1) {
|
|
2201
|
+
let next;
|
|
2202
|
+
try {
|
|
2203
|
+
next = decodeURIComponent(decoded);
|
|
2204
|
+
} catch {
|
|
2205
|
+
return true;
|
|
2206
|
+
}
|
|
2207
|
+
if (next === decoded) break;
|
|
2208
|
+
decoded = next;
|
|
2209
|
+
}
|
|
2210
|
+
const normalized = decoded.replace(/[\p{C}\p{Z}\u115F\u1160\u3164\uFFA0+]/gu, "").replace(/([a-z\d])([A-Z])/gu, "$1_$2").replaceAll("-", "_").toLowerCase();
|
|
2211
|
+
return normalized.length > CODEX_NODE_HTTP_CREDENTIAL_SCAN_MAX_FIELDS || normalized.split(/[.[\]]+/u).some((component) => CODEX_NODE_HTTP_CREDENTIAL_FIELD_NAME_PATTERN.test(component));
|
|
2212
|
+
}
|
|
2213
|
+
function createCredentialedCodexNodeHttpRejection(request) {
|
|
2214
|
+
if (typeof request.id !== "string" && typeof request.id !== "number") throw new Error("Codex http/request must have a JSON-RPC request id.");
|
|
2215
|
+
return Buffer.from(JSON.stringify({
|
|
2216
|
+
jsonrpc: "2.0",
|
|
2217
|
+
id: request.id,
|
|
2218
|
+
error: {
|
|
2219
|
+
code: -32602,
|
|
2220
|
+
message: "Authenticated remote HTTP is unavailable on paired devices; run on Gateway or use an intentionally credential-free endpoint."
|
|
2221
|
+
}
|
|
2222
|
+
}));
|
|
2223
|
+
}
|
|
2224
|
+
function sanitizeCodexExecServerRequest(frame, request) {
|
|
2225
|
+
if (request.method !== "process/start") return frame;
|
|
2226
|
+
if (!isRecord(request.params)) throw new Error("Codex process/start params must be an object.");
|
|
2227
|
+
sanitizeCodexExecServerEnvironment(request.params, "env");
|
|
2228
|
+
if (request.params.envPolicy !== void 0) {
|
|
2229
|
+
if (!isRecord(request.params.envPolicy)) throw new Error("Codex process/start envPolicy must be an object.");
|
|
2230
|
+
sanitizeCodexExecServerEnvironment(request.params.envPolicy, "set");
|
|
2231
|
+
}
|
|
2232
|
+
return normalizeCodexExecServerFrame(Buffer.from(JSON.stringify(request)));
|
|
2233
|
+
}
|
|
2234
|
+
function sanitizeCodexExecServerEnvironment(record, key) {
|
|
2235
|
+
const environment = record[key];
|
|
2236
|
+
if (environment === void 0) return;
|
|
2237
|
+
if (!isRecord(environment)) throw new Error(`Codex process/start ${key} must be an object.`);
|
|
2238
|
+
const values = {};
|
|
2239
|
+
for (const [name, value] of Object.entries(environment)) {
|
|
2240
|
+
if (typeof value !== "string") throw new Error(`Codex process/start ${key} values must be strings.`);
|
|
2241
|
+
try {
|
|
2242
|
+
const url = new URL(value);
|
|
2243
|
+
if (url.username || url.password) continue;
|
|
2244
|
+
} catch {}
|
|
2245
|
+
values[name] = value;
|
|
2246
|
+
}
|
|
2247
|
+
record[key] = sanitizeEnvVars(values).allowed;
|
|
2248
|
+
}
|
|
2249
|
+
//#endregion
|
|
1883
2250
|
//#region extensions/codex/src/app-server/sandbox-exec-server-registry.ts
|
|
1884
2251
|
const sandboxExecServerRegistry = {
|
|
1885
2252
|
servers: /* @__PURE__ */ new Map(),
|
|
2253
|
+
async close(server) {
|
|
2254
|
+
if (server.closed) return;
|
|
2255
|
+
server.closed = true;
|
|
2256
|
+
if ("node" in server) {
|
|
2257
|
+
for (const lease of server.node.leases.values()) if (!lease.closed) {
|
|
2258
|
+
lease.closed = true;
|
|
2259
|
+
lease.channel.close();
|
|
2260
|
+
}
|
|
2261
|
+
server.node.leases.clear();
|
|
2262
|
+
}
|
|
2263
|
+
for (const client of server.server.clients) client.close(1001, "shutdown");
|
|
2264
|
+
await new Promise((resolve) => {
|
|
2265
|
+
server.server.close(() => resolve());
|
|
2266
|
+
});
|
|
2267
|
+
const failures = (await Promise.allSettled([...server.cleanupTasks, ...[...server.children].map(async (child) => await child.terminate())])).flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
2268
|
+
if (failures.length > 0) throw new AggregateError(failures, "Codex sandbox exec-server child cleanup failed");
|
|
2269
|
+
},
|
|
1886
2270
|
async closeAll() {
|
|
1887
2271
|
const servers = await Promise.allSettled(this.servers.values());
|
|
1888
2272
|
this.servers.clear();
|
|
@@ -1890,12 +2274,7 @@ const sandboxExecServerRegistry = {
|
|
|
1890
2274
|
if (entry.status !== "fulfilled") return;
|
|
1891
2275
|
const server = entry.value;
|
|
1892
2276
|
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
|
-
});
|
|
2277
|
+
await this.close(server);
|
|
1899
2278
|
}));
|
|
1900
2279
|
}
|
|
1901
2280
|
};
|
|
@@ -1912,9 +2291,8 @@ var JsonRpcProtocolError = class extends Error {
|
|
|
1912
2291
|
this.code = code;
|
|
1913
2292
|
}
|
|
1914
2293
|
};
|
|
1915
|
-
/** Parses
|
|
1916
|
-
function parseRequest(
|
|
1917
|
-
const text = (Array.isArray(data) ? Buffer.concat(data) : Buffer.isBuffer(data) ? data : Buffer.from(data)).toString("utf8");
|
|
2294
|
+
/** Parses a normalized JSON message into a JSON-RPC request object. */
|
|
2295
|
+
function parseRequest(text) {
|
|
1918
2296
|
return requireObject(JSON.parse(text), "JSON-RPC request");
|
|
1919
2297
|
}
|
|
1920
2298
|
/** Validates that a JSON value is a non-array object. */
|
|
@@ -1954,24 +2332,24 @@ function readHttpHeaders(value) {
|
|
|
1954
2332
|
};
|
|
1955
2333
|
});
|
|
1956
2334
|
}
|
|
1957
|
-
/** Sends a JSON-RPC success response
|
|
1958
|
-
function sendResult(
|
|
1959
|
-
|
|
2335
|
+
/** Sends a JSON-RPC success response through the connection message sink. */
|
|
2336
|
+
function sendResult(send, id, result) {
|
|
2337
|
+
send({
|
|
1960
2338
|
jsonrpc: "2.0",
|
|
1961
2339
|
id,
|
|
1962
|
-
result
|
|
1963
|
-
})
|
|
2340
|
+
result
|
|
2341
|
+
});
|
|
1964
2342
|
}
|
|
1965
|
-
/** Sends a JSON-RPC error response
|
|
1966
|
-
function sendError(
|
|
1967
|
-
|
|
2343
|
+
/** Sends a JSON-RPC error response through the connection message sink. */
|
|
2344
|
+
function sendError(send, id, code, message) {
|
|
2345
|
+
send({
|
|
1968
2346
|
jsonrpc: "2.0",
|
|
1969
2347
|
id: id ?? null,
|
|
1970
2348
|
error: {
|
|
1971
2349
|
code,
|
|
1972
2350
|
message
|
|
1973
2351
|
}
|
|
1974
|
-
})
|
|
2352
|
+
});
|
|
1975
2353
|
}
|
|
1976
2354
|
//#endregion
|
|
1977
2355
|
//#region extensions/codex/src/app-server/sandbox-exec-server/path-uri.ts
|
|
@@ -2216,20 +2594,6 @@ function joinSandboxChildPath(parent, child) {
|
|
|
2216
2594
|
return parent.endsWith("/") ? `${parent}${child}` : `${parent}/${child}`;
|
|
2217
2595
|
}
|
|
2218
2596
|
//#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
2597
|
//#region extensions/codex/src/app-server/sandbox-exec-server/filesystem.ts
|
|
2234
2598
|
/**
|
|
2235
2599
|
* Implements filesystem JSON-RPC handlers for the Codex sandbox exec-server
|
|
@@ -2252,7 +2616,7 @@ async function openFile(execServer, handles, params) {
|
|
|
2252
2616
|
path: filePath,
|
|
2253
2617
|
access: "read"
|
|
2254
2618
|
}]);
|
|
2255
|
-
const fsBridge =
|
|
2619
|
+
const fsBridge = execServer.fsBridge;
|
|
2256
2620
|
const handle = {
|
|
2257
2621
|
abortController: new AbortController(),
|
|
2258
2622
|
closeRequested: false,
|
|
@@ -2339,7 +2703,7 @@ async function readFile(execServer, params) {
|
|
|
2339
2703
|
path: filePath,
|
|
2340
2704
|
access: "read"
|
|
2341
2705
|
}]);
|
|
2342
|
-
const fsBridge =
|
|
2706
|
+
const fsBridge = execServer.fsBridge;
|
|
2343
2707
|
const stat = await fsBridge.stat({ filePath });
|
|
2344
2708
|
if (!stat) throw new JsonRpcProtocolError(JSON_RPC_NOT_FOUND, "file not found");
|
|
2345
2709
|
assertSandboxFileReadWithinLimit(stat);
|
|
@@ -2356,7 +2720,7 @@ async function writeFile(execServer, params) {
|
|
|
2356
2720
|
path: filePath,
|
|
2357
2721
|
access: "write"
|
|
2358
2722
|
}]);
|
|
2359
|
-
const fsBridge =
|
|
2723
|
+
const fsBridge = execServer.fsBridge;
|
|
2360
2724
|
if ((await fsBridge.stat({ filePath: posix.dirname(filePath) }))?.type !== "directory") throw new JsonRpcProtocolError(JSON_RPC_NOT_FOUND, "parent directory not found");
|
|
2361
2725
|
await fsBridge.writeFile({
|
|
2362
2726
|
filePath,
|
|
@@ -2372,7 +2736,7 @@ async function createDirectory(execServer, params) {
|
|
|
2372
2736
|
path: filePath,
|
|
2373
2737
|
access: "write"
|
|
2374
2738
|
}]);
|
|
2375
|
-
const fsBridge =
|
|
2739
|
+
const fsBridge = execServer.fsBridge;
|
|
2376
2740
|
if (record.recursive === false) {
|
|
2377
2741
|
const parentPath = posix.dirname(filePath);
|
|
2378
2742
|
if ((await fsBridge.stat({ filePath: parentPath }))?.type !== "directory") throw new JsonRpcProtocolError(JSON_RPC_NOT_FOUND, "parent directory not found");
|
|
@@ -2387,7 +2751,7 @@ async function getMetadata(execServer, params) {
|
|
|
2387
2751
|
path: filePath,
|
|
2388
2752
|
access: "read"
|
|
2389
2753
|
}]);
|
|
2390
|
-
const stat = await
|
|
2754
|
+
const stat = await execServer.fsBridge.stat({ filePath });
|
|
2391
2755
|
if (!stat) throw new JsonRpcProtocolError(JSON_RPC_NOT_FOUND, "file not found");
|
|
2392
2756
|
return metadataResponse(stat);
|
|
2393
2757
|
}
|
|
@@ -2401,11 +2765,9 @@ async function listDirectoryEntries(execServer, filePath, fsSandboxPolicy) {
|
|
|
2401
2765
|
path: filePath,
|
|
2402
2766
|
access: "read"
|
|
2403
2767
|
}]);
|
|
2404
|
-
const
|
|
2405
|
-
const backend = requireBackend(execServer);
|
|
2406
|
-
const resolved = fsBridge.resolvePath({ filePath });
|
|
2768
|
+
const resolved = execServer.fsBridge.resolvePath({ filePath });
|
|
2407
2769
|
if (!resolved) throw new Error(`Cannot resolve sandbox path: ${filePath}`);
|
|
2408
|
-
const result = await backend.runShellCommand({
|
|
2770
|
+
const result = await execServer.backend.runShellCommand({
|
|
2409
2771
|
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
2772
|
args: [resolved.containerPath],
|
|
2411
2773
|
allowFailure: true
|
|
@@ -2433,7 +2795,7 @@ async function removePath(execServer, params) {
|
|
|
2433
2795
|
access: "write"
|
|
2434
2796
|
}]);
|
|
2435
2797
|
if (record.recursive !== false) assertNoReadOnlyDescendant(fsSandboxPolicy, filePath, "remove");
|
|
2436
|
-
await
|
|
2798
|
+
await execServer.fsBridge.remove({
|
|
2437
2799
|
filePath,
|
|
2438
2800
|
recursive: record.recursive !== false,
|
|
2439
2801
|
force: record.force !== false
|
|
@@ -2460,8 +2822,7 @@ async function copyPath(execServer, params) {
|
|
|
2460
2822
|
});
|
|
2461
2823
|
}
|
|
2462
2824
|
async function copySandboxPath(execServer, params) {
|
|
2463
|
-
const fsBridge = execServer.
|
|
2464
|
-
if (!fsBridge) throw new Error("Sandbox filesystem bridge is unavailable.");
|
|
2825
|
+
const fsBridge = execServer.fsBridge;
|
|
2465
2826
|
assertResolvedFsSandboxAccess(params.fsSandboxPolicy, [{
|
|
2466
2827
|
path: params.sourcePath,
|
|
2467
2828
|
access: "read"
|
|
@@ -2519,6 +2880,126 @@ function metadataResponse(stat) {
|
|
|
2519
2880
|
};
|
|
2520
2881
|
}
|
|
2521
2882
|
//#endregion
|
|
2883
|
+
//#region extensions/codex/src/app-server/sandbox-exec-server/sandbox-child.ts
|
|
2884
|
+
/** Owns one sandbox subprocess tree through close, reaping, and backend finalization. */
|
|
2885
|
+
const SANDBOX_CHILD_TERM_GRACE_MS = 1e3;
|
|
2886
|
+
const SANDBOX_CHILD_REAP_TIMEOUT_MS = 4500;
|
|
2887
|
+
const SANDBOX_EXEC_MARKER = "CODEX_SANDBOX_EXEC_ID";
|
|
2888
|
+
async function spawnSandboxChild(params) {
|
|
2889
|
+
const [command, ...args] = params.argv;
|
|
2890
|
+
const finalize = async (status, exitCode) => await params.finalizeExec?.({
|
|
2891
|
+
status,
|
|
2892
|
+
exitCode,
|
|
2893
|
+
timedOut: false,
|
|
2894
|
+
token: params.finalizeToken
|
|
2895
|
+
});
|
|
2896
|
+
if (!command) {
|
|
2897
|
+
await finalize("failed", null).catch(params.onFinalizeError);
|
|
2898
|
+
throw new Error("OpenClaw sandbox exec spec did not provide a command.");
|
|
2899
|
+
}
|
|
2900
|
+
let child;
|
|
2901
|
+
try {
|
|
2902
|
+
child = spawn(command, args, {
|
|
2903
|
+
detached: process.platform !== "win32",
|
|
2904
|
+
env: params.env,
|
|
2905
|
+
stdio: [
|
|
2906
|
+
"pipe",
|
|
2907
|
+
"pipe",
|
|
2908
|
+
"pipe"
|
|
2909
|
+
]
|
|
2910
|
+
});
|
|
2911
|
+
} catch (error) {
|
|
2912
|
+
await finalize("failed", null).catch(params.onFinalizeError);
|
|
2913
|
+
throw error;
|
|
2914
|
+
}
|
|
2915
|
+
let outcome;
|
|
2916
|
+
const closed = new Promise((resolve) => {
|
|
2917
|
+
child.once("close", (code, signal) => resolve(outcome = {
|
|
2918
|
+
exitCode: code ?? 1,
|
|
2919
|
+
signal
|
|
2920
|
+
}));
|
|
2921
|
+
});
|
|
2922
|
+
let finalizePromise;
|
|
2923
|
+
let terminationCleanup;
|
|
2924
|
+
let terminationError;
|
|
2925
|
+
const settled = closed.then(async (result) => {
|
|
2926
|
+
await terminationCleanup;
|
|
2927
|
+
child.stdin.destroy();
|
|
2928
|
+
await (finalizePromise ??= finalize(params.finalizeStatus(result), result.exitCode));
|
|
2929
|
+
return result;
|
|
2930
|
+
});
|
|
2931
|
+
settled.catch(params.onFinalizeError);
|
|
2932
|
+
let terminationPromise;
|
|
2933
|
+
const owner = {
|
|
2934
|
+
process: child,
|
|
2935
|
+
settled,
|
|
2936
|
+
terminate: () => terminationPromise ??= (async () => {
|
|
2937
|
+
child.stdin.destroy();
|
|
2938
|
+
terminationCleanup = params.terminateRemote?.().catch((error) => {
|
|
2939
|
+
terminationError = error instanceof Error ? error : new Error(String(error));
|
|
2940
|
+
});
|
|
2941
|
+
await terminationCleanup;
|
|
2942
|
+
if (!outcome) {
|
|
2943
|
+
if (child.pid) killProcessTree(child.pid, {
|
|
2944
|
+
detached: process.platform !== "win32",
|
|
2945
|
+
graceMs: SANDBOX_CHILD_TERM_GRACE_MS
|
|
2946
|
+
});
|
|
2947
|
+
else child.kill("SIGTERM");
|
|
2948
|
+
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.`);
|
|
2949
|
+
}
|
|
2950
|
+
const result = await settled;
|
|
2951
|
+
if (terminationError) throw terminationError;
|
|
2952
|
+
return result;
|
|
2953
|
+
})()
|
|
2954
|
+
};
|
|
2955
|
+
params.owners.add(owner);
|
|
2956
|
+
settled.then(() => params.owners.delete(owner), () => params.owners.delete(owner));
|
|
2957
|
+
return owner;
|
|
2958
|
+
}
|
|
2959
|
+
function prepareSandboxChildExec(backend, env) {
|
|
2960
|
+
const marker = randomUUID();
|
|
2961
|
+
return {
|
|
2962
|
+
env: {
|
|
2963
|
+
...env,
|
|
2964
|
+
[SANDBOX_EXEC_MARKER]: marker
|
|
2965
|
+
},
|
|
2966
|
+
terminate: async () => {
|
|
2967
|
+
const result = await backend.runShellCommand({
|
|
2968
|
+
script: SANDBOX_REMOTE_TERMINATE_SCRIPT,
|
|
2969
|
+
args: [`${SANDBOX_EXEC_MARKER}=${marker}`],
|
|
2970
|
+
allowFailure: true,
|
|
2971
|
+
signal: AbortSignal.timeout(SANDBOX_CHILD_REAP_TIMEOUT_MS)
|
|
2972
|
+
});
|
|
2973
|
+
if (result.code !== 0) {
|
|
2974
|
+
const detail = result.stderr.toString("utf8").trim() || result.stdout.toString("utf8").trim();
|
|
2975
|
+
throw new Error(detail || `Sandbox process tree cleanup failed with code ${result.code}; tear down the sandbox environment and inspect surviving processes before retrying.`);
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
};
|
|
2979
|
+
}
|
|
2980
|
+
const SANDBOX_REMOTE_TERMINATE_SCRIPT = String.raw`
|
|
2981
|
+
find_owned_pids() {
|
|
2982
|
+
for env_file in /proc/[0-9]*/environ; do
|
|
2983
|
+
if [ -r "$env_file" ] && tr '\0' '\n' < "$env_file" 2>/dev/null | grep -Fqx "$1"; then
|
|
2984
|
+
basename "$(dirname "$env_file")"
|
|
2985
|
+
fi
|
|
2986
|
+
done
|
|
2987
|
+
}
|
|
2988
|
+
owned="$(find_owned_pids "$1")"
|
|
2989
|
+
[ -z "$owned" ] || kill -TERM $owned 2>/dev/null || true
|
|
2990
|
+
sleep 1
|
|
2991
|
+
owned="$(find_owned_pids "$1")"
|
|
2992
|
+
[ -z "$owned" ] || kill -KILL $owned 2>/dev/null || true
|
|
2993
|
+
sleep 1
|
|
2994
|
+
owned="$(find_owned_pids "$1")"
|
|
2995
|
+
[ -z "$owned" ] || { echo "Sandbox process IDs survived SIGKILL: $owned" >&2; exit 1; }
|
|
2996
|
+
`.trim();
|
|
2997
|
+
function delay(ms) {
|
|
2998
|
+
return new Promise((resolve) => {
|
|
2999
|
+
setTimeout(resolve, ms).unref?.();
|
|
3000
|
+
});
|
|
3001
|
+
}
|
|
3002
|
+
//#endregion
|
|
2522
3003
|
//#region extensions/codex/src/app-server/sandbox-exec-server/http.ts
|
|
2523
3004
|
/**
|
|
2524
3005
|
* Implements sandboxed HTTP requests for Codex native tools by routing network
|
|
@@ -2527,7 +3008,7 @@ function metadataResponse(stat) {
|
|
|
2527
3008
|
/** Maximum JSON-line size accepted from the streaming HTTP helper process. */
|
|
2528
3009
|
const SANDBOX_HTTP_STREAM_LINE_MAX_CHARS = 256 * 1024;
|
|
2529
3010
|
/** Handles one sandbox HTTP JSON-RPC request, optionally streaming response body deltas. */
|
|
2530
|
-
async function httpRequest(execServer,
|
|
3011
|
+
async function httpRequest(execServer, notifications, params) {
|
|
2531
3012
|
const record = requireObject(params, "http/request params");
|
|
2532
3013
|
const requestId = requireString(record.requestId, "requestId");
|
|
2533
3014
|
const url = requireString(record.url, "url");
|
|
@@ -2540,7 +3021,7 @@ async function httpRequest(execServer, socket, params) {
|
|
|
2540
3021
|
timeoutMs: typeof record.timeoutMs === "number" && record.timeoutMs > 0 ? Math.floor(record.timeoutMs) : void 0,
|
|
2541
3022
|
streamResponse: record.streamResponse === true
|
|
2542
3023
|
};
|
|
2543
|
-
if (request.streamResponse) return await runStreamingSandboxHttpRequest(execServer,
|
|
3024
|
+
if (request.streamResponse) return await runStreamingSandboxHttpRequest(execServer, notifications, requestId, request);
|
|
2544
3025
|
return await runSandboxHttpRequest(execServer, {
|
|
2545
3026
|
...request,
|
|
2546
3027
|
streamResponse: false
|
|
@@ -2557,7 +3038,7 @@ function assertSandboxHttpRequestTargetAllowed(url) {
|
|
|
2557
3038
|
if (isBlockedHostnameOrIp(parsed.hostname)) throw new SsrFBlockedError(`Blocked hostname or private/internal IP in sandbox http/request: ${parsed.hostname}`);
|
|
2558
3039
|
}
|
|
2559
3040
|
async function runSandboxHttpRequest(execServer, params) {
|
|
2560
|
-
const result = await
|
|
3041
|
+
const result = await execServer.backend.runShellCommand({
|
|
2561
3042
|
script: SANDBOX_HTTP_REQUEST_SCRIPT,
|
|
2562
3043
|
stdin: JSON.stringify(params),
|
|
2563
3044
|
allowFailure: true
|
|
@@ -2574,44 +3055,40 @@ async function runSandboxHttpRequest(execServer, params) {
|
|
|
2574
3055
|
bodyBase64: typeof parsed.bodyBase64 === "string" ? parsed.bodyBase64 : ""
|
|
2575
3056
|
};
|
|
2576
3057
|
}
|
|
2577
|
-
async function runStreamingSandboxHttpRequest(execServer,
|
|
2578
|
-
const backend =
|
|
3058
|
+
async function runStreamingSandboxHttpRequest(execServer, notifications, requestId, params) {
|
|
3059
|
+
const backend = execServer.backend;
|
|
3060
|
+
const remoteExec = prepareSandboxChildExec(backend, {});
|
|
2579
3061
|
const execSpec = await backend.buildExecSpec({
|
|
2580
3062
|
command: SANDBOX_HTTP_REQUEST_SCRIPT,
|
|
2581
3063
|
workdir: execServer.sandbox.containerWorkdir,
|
|
2582
|
-
env:
|
|
3064
|
+
env: remoteExec.env,
|
|
2583
3065
|
usePty: false
|
|
2584
3066
|
});
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
3067
|
+
const lifecycle = { failed: false };
|
|
3068
|
+
const owner = await spawnSandboxChild({
|
|
3069
|
+
argv: execSpec.argv,
|
|
3070
|
+
env: execSpec.env,
|
|
3071
|
+
finalizeExec: backend.finalizeExec,
|
|
3072
|
+
finalizeToken: execSpec.finalizeToken,
|
|
3073
|
+
finalizeStatus: (outcome) => lifecycle.failed || outcome.exitCode !== 0 ? "failed" : "completed",
|
|
3074
|
+
onFinalizeError: (error) => {
|
|
3075
|
+
embeddedAgentLog.warn("codex sandbox http/request finalize failed", { error });
|
|
3076
|
+
},
|
|
3077
|
+
owners: execServer.children,
|
|
3078
|
+
terminateRemote: remoteExec.terminate
|
|
3079
|
+
});
|
|
3080
|
+
const child = owner.process;
|
|
3081
|
+
const abortOnSessionClose = () => {
|
|
3082
|
+
lifecycle.failed = true;
|
|
3083
|
+
owner.terminate().catch((error) => {
|
|
3084
|
+
embeddedAgentLog.warn("codex sandbox http/request cleanup failed", { error });
|
|
2596
3085
|
});
|
|
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);
|
|
3086
|
+
};
|
|
3087
|
+
notifications.signal.addEventListener("abort", abortOnSessionClose, { once: true });
|
|
2612
3088
|
child.once("close", () => {
|
|
2613
|
-
|
|
3089
|
+
notifications.signal.removeEventListener("abort", abortOnSessionClose);
|
|
2614
3090
|
});
|
|
3091
|
+
if (notifications.signal.aborted) abortOnSessionClose();
|
|
2615
3092
|
child.stdin.on("error", (error) => {
|
|
2616
3093
|
if (error.code === "EPIPE" || error.code === "ERR_STREAM_DESTROYED") return;
|
|
2617
3094
|
embeddedAgentLog.warn("codex sandbox http/request stdin write failed", { error });
|
|
@@ -2619,10 +3096,10 @@ async function runStreamingSandboxHttpRequest(execServer, socket, requestId, par
|
|
|
2619
3096
|
child.stdin.end(JSON.stringify(params));
|
|
2620
3097
|
return await readStreamingSandboxHttpResponse({
|
|
2621
3098
|
child,
|
|
2622
|
-
|
|
2623
|
-
|
|
3099
|
+
lifecycle,
|
|
3100
|
+
owner,
|
|
2624
3101
|
requestId,
|
|
2625
|
-
|
|
3102
|
+
notifications
|
|
2626
3103
|
});
|
|
2627
3104
|
}
|
|
2628
3105
|
function readStreamingSandboxHttpResponse(params) {
|
|
@@ -2633,22 +3110,15 @@ function readStreamingSandboxHttpResponse(params) {
|
|
|
2633
3110
|
let lastBodySeq = 0;
|
|
2634
3111
|
let stdoutBuffer = "";
|
|
2635
3112
|
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) => {
|
|
3113
|
+
const fail = (message, _exitCode) => {
|
|
2645
3114
|
if (failed) return;
|
|
2646
3115
|
failed = true;
|
|
2647
|
-
|
|
2648
|
-
|
|
3116
|
+
params.lifecycle.failed = true;
|
|
3117
|
+
params.owner.terminate().catch((error) => {
|
|
3118
|
+
embeddedAgentLog.warn("codex sandbox http/request cleanup failed", { error });
|
|
2649
3119
|
});
|
|
2650
3120
|
if (headerResolved) {
|
|
2651
|
-
|
|
3121
|
+
if (params.notifications.isOpen()) params.notifications.send("http/request/bodyDelta", {
|
|
2652
3122
|
requestId: params.requestId,
|
|
2653
3123
|
seq: lastBodySeq + 1,
|
|
2654
3124
|
deltaBase64: "",
|
|
@@ -2679,7 +3149,7 @@ function readStreamingSandboxHttpResponse(params) {
|
|
|
2679
3149
|
} else if (type === "bodyDelta") {
|
|
2680
3150
|
const seq = requireNumber(message.seq, "http body sequence");
|
|
2681
3151
|
lastBodySeq = Math.max(lastBodySeq, seq);
|
|
2682
|
-
|
|
3152
|
+
if (params.notifications.isOpen()) params.notifications.send("http/request/bodyDelta", {
|
|
2683
3153
|
requestId: params.requestId,
|
|
2684
3154
|
seq,
|
|
2685
3155
|
deltaBase64: typeof message.deltaBase64 === "string" ? message.deltaBase64 : "",
|
|
@@ -2692,10 +3162,7 @@ function readStreamingSandboxHttpResponse(params) {
|
|
|
2692
3162
|
}
|
|
2693
3163
|
newline = stdoutBuffer.indexOf("\n");
|
|
2694
3164
|
}
|
|
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
|
-
}
|
|
3165
|
+
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
3166
|
});
|
|
2700
3167
|
params.child.stderr.setEncoding("utf8");
|
|
2701
3168
|
params.child.stderr.on("data", (chunk) => {
|
|
@@ -2703,6 +3170,7 @@ function readStreamingSandboxHttpResponse(params) {
|
|
|
2703
3170
|
});
|
|
2704
3171
|
params.child.once("error", (error) => {
|
|
2705
3172
|
childFailure ??= error.message;
|
|
3173
|
+
params.lifecycle.failed = true;
|
|
2706
3174
|
});
|
|
2707
3175
|
params.child.once("close", (code) => {
|
|
2708
3176
|
const exitCode = code ?? 1;
|
|
@@ -2712,10 +3180,10 @@ function readStreamingSandboxHttpResponse(params) {
|
|
|
2712
3180
|
return;
|
|
2713
3181
|
}
|
|
2714
3182
|
if (exitCode === 0) {
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
3183
|
+
if (!headerResolved) {
|
|
3184
|
+
params.lifecycle.failed = true;
|
|
3185
|
+
reject(/* @__PURE__ */ new Error("sandbox http/request exited before returning headers"));
|
|
3186
|
+
}
|
|
2719
3187
|
return;
|
|
2720
3188
|
}
|
|
2721
3189
|
fail(stderr.trim() || `sandbox http/request failed with code ${exitCode}`, exitCode);
|
|
@@ -2921,20 +3389,6 @@ if __name__ == "__main__":
|
|
|
2921
3389
|
PY
|
|
2922
3390
|
python3 "$tmp"
|
|
2923
3391
|
`.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
3392
|
//#endregion
|
|
2939
3393
|
//#region extensions/codex/src/app-server/sandbox-exec-server/processes.ts
|
|
2940
3394
|
/**
|
|
@@ -2945,7 +3399,7 @@ const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
|
2945
3399
|
const RETAINED_PROCESS_OUTPUT_BYTES = 1024 * 1024;
|
|
2946
3400
|
const CLOSED_PROCESS_EVICTION_MS = 6e4;
|
|
2947
3401
|
/** Starts a sandbox-backed process and registers it in the connection-local process table. */
|
|
2948
|
-
async function startProcess(execServer, processes,
|
|
3402
|
+
async function startProcess(execServer, processes, notify, params) {
|
|
2949
3403
|
const record = requireObject(params, "process/start params");
|
|
2950
3404
|
const processId = requireString(record.processId, "processId");
|
|
2951
3405
|
if (processes.has(processId)) throw new Error(`process already exists: ${processId}`);
|
|
@@ -2964,17 +3418,10 @@ async function startProcess(execServer, processes, socket, params) {
|
|
|
2964
3418
|
failure: null,
|
|
2965
3419
|
tty: record.tty === true,
|
|
2966
3420
|
pipeStdin: record.pipeStdin === true,
|
|
2967
|
-
|
|
3421
|
+
terminationRequested: false,
|
|
2968
3422
|
child: null,
|
|
2969
|
-
finalized: false,
|
|
2970
3423
|
waiters: [],
|
|
2971
|
-
emitNotification:
|
|
2972
|
-
if (socket.readyState === 1) socket.send(JSON.stringify({
|
|
2973
|
-
jsonrpc: "2.0",
|
|
2974
|
-
method,
|
|
2975
|
-
params: notificationParams
|
|
2976
|
-
}));
|
|
2977
|
-
},
|
|
3424
|
+
emitNotification: notify,
|
|
2978
3425
|
evictProcess: () => {
|
|
2979
3426
|
if (managed.evictionTimer) return;
|
|
2980
3427
|
managed.evictionTimer = setTimeout(() => {
|
|
@@ -2984,12 +3431,14 @@ async function startProcess(execServer, processes, socket, params) {
|
|
|
2984
3431
|
}
|
|
2985
3432
|
};
|
|
2986
3433
|
processes.set(processId, managed);
|
|
3434
|
+
const startPromise = runProcess(execServer, managed, {
|
|
3435
|
+
argv,
|
|
3436
|
+
cwd,
|
|
3437
|
+
env
|
|
3438
|
+
});
|
|
3439
|
+
managed.startPromise = startPromise;
|
|
2987
3440
|
try {
|
|
2988
|
-
await
|
|
2989
|
-
argv,
|
|
2990
|
-
cwd,
|
|
2991
|
-
env
|
|
2992
|
-
});
|
|
3441
|
+
await startPromise;
|
|
2993
3442
|
} catch (error) {
|
|
2994
3443
|
processes.delete(processId);
|
|
2995
3444
|
managed.failure = coerceErrorMessage(error);
|
|
@@ -2998,47 +3447,49 @@ async function startProcess(execServer, processes, socket, params) {
|
|
|
2998
3447
|
managed.closed = true;
|
|
2999
3448
|
notifyProcessWaiters(managed);
|
|
3000
3449
|
throw error;
|
|
3450
|
+
} finally {
|
|
3451
|
+
if (managed.startPromise === startPromise) managed.startPromise = void 0;
|
|
3001
3452
|
}
|
|
3002
3453
|
return { processId };
|
|
3003
3454
|
}
|
|
3004
3455
|
async function runProcess(execServer, managed, params) {
|
|
3005
|
-
const backend = execServer.
|
|
3006
|
-
if (!backend) throw new Error("OpenClaw sandbox backend is unavailable.");
|
|
3456
|
+
const backend = execServer.backend;
|
|
3007
3457
|
throwIfProcessStartCancelled(managed);
|
|
3458
|
+
const remoteExec = prepareSandboxChildExec(backend, params.env);
|
|
3008
3459
|
const execSpec = await backend.buildExecSpec({
|
|
3009
|
-
command:
|
|
3460
|
+
command: buildRemoteCommand(params.argv),
|
|
3010
3461
|
workdir: params.cwd,
|
|
3011
|
-
env:
|
|
3462
|
+
env: remoteExec.env,
|
|
3012
3463
|
usePty: false
|
|
3013
3464
|
});
|
|
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
|
-
]
|
|
3465
|
+
if (managed.terminationRequested) {
|
|
3466
|
+
await backend.finalizeExec?.({
|
|
3467
|
+
status: "failed",
|
|
3468
|
+
exitCode: null,
|
|
3469
|
+
timedOut: false,
|
|
3470
|
+
token: execSpec.finalizeToken
|
|
3028
3471
|
});
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3472
|
+
throw new Error("process start cancelled");
|
|
3473
|
+
}
|
|
3474
|
+
const owner = await spawnSandboxChild({
|
|
3475
|
+
argv: execSpec.argv,
|
|
3476
|
+
env: execSpec.env,
|
|
3477
|
+
finalizeExec: backend.finalizeExec,
|
|
3478
|
+
finalizeToken: execSpec.finalizeToken,
|
|
3479
|
+
finalizeStatus: () => managed.failure ? "failed" : "completed",
|
|
3480
|
+
onFinalizeError: (error) => {
|
|
3481
|
+
const message = coerceErrorMessage(error);
|
|
3482
|
+
managed.failure ??= message;
|
|
3483
|
+
embeddedAgentLog.warn("codex sandbox exec-server finalize failed", {
|
|
3033
3484
|
processId: managed.processId,
|
|
3034
|
-
error:
|
|
3485
|
+
error: message
|
|
3035
3486
|
});
|
|
3036
|
-
}
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3487
|
+
},
|
|
3488
|
+
owners: execServer.children,
|
|
3489
|
+
terminateRemote: remoteExec.terminate
|
|
3490
|
+
});
|
|
3491
|
+
managed.child = owner;
|
|
3492
|
+
const child = owner.process;
|
|
3042
3493
|
child.stdout.on("data", (chunk) => appendProcessChunk(managed, managed.tty ? "pty" : "stdout", chunk));
|
|
3043
3494
|
child.stderr.on("data", (chunk) => appendProcessChunk(managed, "stderr", chunk));
|
|
3044
3495
|
child.once("error", (error) => {
|
|
@@ -3046,13 +3497,12 @@ async function runProcess(execServer, managed, params) {
|
|
|
3046
3497
|
notifyProcessWaiters(managed);
|
|
3047
3498
|
});
|
|
3048
3499
|
child.once("close", (code) => {
|
|
3049
|
-
managed.abortController.signal.removeEventListener("abort", abortListener);
|
|
3050
3500
|
emitProcessClosed(managed, code ?? 1);
|
|
3051
3501
|
});
|
|
3052
3502
|
if (!managed.tty && !managed.pipeStdin) child.stdin.end();
|
|
3053
3503
|
}
|
|
3054
3504
|
function throwIfProcessStartCancelled(managed) {
|
|
3055
|
-
if (managed.
|
|
3505
|
+
if (managed.terminationRequested) throw new Error("process start cancelled");
|
|
3056
3506
|
}
|
|
3057
3507
|
function appendProcessChunk(managed, stream, data) {
|
|
3058
3508
|
if (data.length === 0) return;
|
|
@@ -3098,28 +3548,9 @@ function emitProcessClosed(managed, exitCode) {
|
|
|
3098
3548
|
seq: closeSeq
|
|
3099
3549
|
});
|
|
3100
3550
|
}
|
|
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
3551
|
managed.evictProcess();
|
|
3110
3552
|
notifyProcessWaiters(managed);
|
|
3111
3553
|
}
|
|
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
3554
|
function limitProcessChunks(chunks, maxBytes) {
|
|
3124
3555
|
if (!maxBytes) return chunks;
|
|
3125
3556
|
const retained = [];
|
|
@@ -3158,19 +3589,20 @@ function writeProcess(processes, params) {
|
|
|
3158
3589
|
const managed = processes.get(processId);
|
|
3159
3590
|
if (!managed) return { status: "unknownProcess" };
|
|
3160
3591
|
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);
|
|
3592
|
+
if (!managed.tty && !managed.pipeStdin || managed.closed || !managed.child?.process.stdin.writable) return { status: "stdinClosed" };
|
|
3593
|
+
managed.child.process.stdin.write(chunk);
|
|
3163
3594
|
return { status: "accepted" };
|
|
3164
3595
|
}
|
|
3165
3596
|
/** Requests process termination and reports whether it was running at call time. */
|
|
3166
|
-
function terminateProcess(processes, params) {
|
|
3597
|
+
async function terminateProcess(processes, params) {
|
|
3167
3598
|
const processId = requireString(requireObject(params, "process/terminate params").processId, "processId");
|
|
3168
3599
|
const managed = processes.get(processId);
|
|
3169
3600
|
if (!managed) return { running: false };
|
|
3170
3601
|
const running = !managed.exited;
|
|
3171
|
-
managed.
|
|
3172
|
-
managed.
|
|
3173
|
-
if (
|
|
3602
|
+
managed.terminationRequested = true;
|
|
3603
|
+
await managed.startPromise?.catch(() => void 0);
|
|
3604
|
+
if (managed.child) await managed.child.terminate();
|
|
3605
|
+
else if (running && !managed.closed) emitProcessClosed(managed, null);
|
|
3174
3606
|
return { running };
|
|
3175
3607
|
}
|
|
3176
3608
|
function waitForProcessUpdate(managed, waitMs) {
|
|
@@ -3192,12 +3624,6 @@ function notifyProcessWaiters(managed) {
|
|
|
3192
3624
|
function hasChunksAtOrAfter(managed, afterSeq) {
|
|
3193
3625
|
return managed.chunks.some((chunk) => chunk.seq > afterSeq);
|
|
3194
3626
|
}
|
|
3195
|
-
function shellCommandFromArgv(argv) {
|
|
3196
|
-
return argv.map(shellEscape).join(" ");
|
|
3197
|
-
}
|
|
3198
|
-
function shellEscape(value) {
|
|
3199
|
-
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
3200
|
-
}
|
|
3201
3627
|
function requireProcess(processes, processId) {
|
|
3202
3628
|
const managed = processes.get(processId);
|
|
3203
3629
|
if (!managed) throw new Error(`unknown process: ${processId}`);
|
|
@@ -3241,47 +3667,148 @@ function readStringList(value) {
|
|
|
3241
3667
|
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
3242
3668
|
}
|
|
3243
3669
|
//#endregion
|
|
3670
|
+
//#region extensions/codex/src/app-server/sandbox-exec-server/session.ts
|
|
3671
|
+
/** Owns the JSON-RPC protocol and resources of one sandbox execution connection. */
|
|
3672
|
+
/** Connection-local execution state; closing it never enables session resumption. */
|
|
3673
|
+
var CodexSandboxExecSession = class {
|
|
3674
|
+
constructor(execServer, transport) {
|
|
3675
|
+
this.execServer = execServer;
|
|
3676
|
+
this.transport = transport;
|
|
3677
|
+
this.processes = /* @__PURE__ */ new Map();
|
|
3678
|
+
this.fileReads = /* @__PURE__ */ new Map();
|
|
3679
|
+
this.closeController = new AbortController();
|
|
3680
|
+
this.notifications = {
|
|
3681
|
+
isOpen: transport.isOpen,
|
|
3682
|
+
signal: this.closeController.signal,
|
|
3683
|
+
send: (method, params) => {
|
|
3684
|
+
if (transport.isOpen()) transport.send({
|
|
3685
|
+
jsonrpc: "2.0",
|
|
3686
|
+
method,
|
|
3687
|
+
params
|
|
3688
|
+
});
|
|
3689
|
+
}
|
|
3690
|
+
};
|
|
3691
|
+
}
|
|
3692
|
+
async handleRequest(request) {
|
|
3693
|
+
const method = request.method;
|
|
3694
|
+
if (!method) {
|
|
3695
|
+
sendError(this.transport.send, request.id, -32600, "Invalid Request");
|
|
3696
|
+
return;
|
|
3697
|
+
}
|
|
3698
|
+
if (request.id === void 0) {
|
|
3699
|
+
if (method !== "initialized") sendError(this.transport.send, -1, -32600, `Unexpected notification: ${method}`);
|
|
3700
|
+
return;
|
|
3701
|
+
}
|
|
3702
|
+
try {
|
|
3703
|
+
const result = await this.dispatchRequest(method, request.params);
|
|
3704
|
+
sendResult(this.transport.send, request.id, result);
|
|
3705
|
+
} catch (error) {
|
|
3706
|
+
sendError(this.transport.send, request.id, error instanceof JsonRpcProtocolError ? error.code : -32603, error instanceof Error ? error.message : String(error));
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
close() {
|
|
3710
|
+
if (!this.cleanup) {
|
|
3711
|
+
this.closeController.abort();
|
|
3712
|
+
closeAllFileReads(this.fileReads);
|
|
3713
|
+
this.cleanup = Promise.all([...this.processes.keys()].map(async (processId) => terminateProcess(this.processes, { processId }))).then(() => void 0);
|
|
3714
|
+
}
|
|
3715
|
+
return this.cleanup;
|
|
3716
|
+
}
|
|
3717
|
+
async dispatchRequest(method, params) {
|
|
3718
|
+
switch (method) {
|
|
3719
|
+
case "initialize": return { sessionId: randomUUID() };
|
|
3720
|
+
case "environment/info": return {
|
|
3721
|
+
shell: {
|
|
3722
|
+
name: "sh",
|
|
3723
|
+
path: "/bin/sh"
|
|
3724
|
+
},
|
|
3725
|
+
cwd: pathToFileURL(this.execServer.sandbox.containerWorkdir, { windows: false }).href,
|
|
3726
|
+
capabilities: { networkProxyLaunch: false }
|
|
3727
|
+
};
|
|
3728
|
+
case "environment/status": return { status: "ready" };
|
|
3729
|
+
case "process/start": return startProcess(this.execServer, this.processes, this.notifications.send, params);
|
|
3730
|
+
case "process/read": return await readProcess(this.processes, params);
|
|
3731
|
+
case "process/write": return writeProcess(this.processes, params);
|
|
3732
|
+
case "process/terminate": return await terminateProcess(this.processes, params);
|
|
3733
|
+
case "fs/open": return await openFile(this.execServer, this.fileReads, params);
|
|
3734
|
+
case "fs/readBlock": return readFileBlock(this.fileReads, params);
|
|
3735
|
+
case "fs/close": return closeFile(this.fileReads, params);
|
|
3736
|
+
case "fs/readFile": return await readFile(this.execServer, params);
|
|
3737
|
+
case "fs/writeFile":
|
|
3738
|
+
await writeFile(this.execServer, params);
|
|
3739
|
+
return {};
|
|
3740
|
+
case "fs/createDirectory":
|
|
3741
|
+
await createDirectory(this.execServer, params);
|
|
3742
|
+
return {};
|
|
3743
|
+
case "fs/getMetadata": return await getMetadata(this.execServer, params);
|
|
3744
|
+
case "fs/readDirectory": return await readDirectory(this.execServer, params);
|
|
3745
|
+
case "fs/remove":
|
|
3746
|
+
await removePath(this.execServer, params);
|
|
3747
|
+
return {};
|
|
3748
|
+
case "fs/copy":
|
|
3749
|
+
await copyPath(this.execServer, params);
|
|
3750
|
+
return {};
|
|
3751
|
+
case "http/request": return await httpRequest(this.execServer, this.notifications, params);
|
|
3752
|
+
default: throw new JsonRpcProtocolError(JSON_RPC_METHOD_NOT_FOUND, `Unsupported OpenClaw sandbox exec-server method: ${method}`);
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3755
|
+
};
|
|
3756
|
+
//#endregion
|
|
3244
3757
|
//#region extensions/codex/src/app-server/sandbox-exec-server.ts
|
|
3245
3758
|
/**
|
|
3246
3759
|
* Hosts the local OpenClaw sandbox exec-server that Codex app-server native
|
|
3247
3760
|
* execution can register as an external environment.
|
|
3248
3761
|
*/
|
|
3249
3762
|
const CODEX_SANDBOX_EXEC_SERVER_MAX_INBOUND_MESSAGE_BYTES = 100 * 1024 * 1024;
|
|
3763
|
+
const CODEX_NODE_EXEC_SERVER_MAX_MESSAGE_BYTES = 64 * 1024 * 1024;
|
|
3764
|
+
const codexNodeExecServerLeases = /* @__PURE__ */ new WeakMap();
|
|
3250
3765
|
/** Starts or reuses a sandbox exec-server and registers it with Codex app-server. */
|
|
3251
3766
|
async function ensureCodexSandboxExecServerEnvironment(params) {
|
|
3252
|
-
if (!params.sandbox?.enabled
|
|
3767
|
+
if (!params.sandbox?.enabled) return;
|
|
3768
|
+
const placementNodeId = readCodexPlacementNodeId(params.sandbox);
|
|
3769
|
+
if (!params.sandbox.backend && !placementNodeId) return;
|
|
3770
|
+
if (placementNodeId && !params.runtime) throw new Error("Codex paired-device execution requires its active plugin runtime.");
|
|
3253
3771
|
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(
|
|
3772
|
+
const { server: execServer, nodeLease } = await acquireOpenClawExecServer({
|
|
3773
|
+
sandbox: params.sandbox,
|
|
3774
|
+
runtime: params.runtime,
|
|
3775
|
+
signal: params.signal,
|
|
3776
|
+
onExecutionDisconnect: params.onExecutionDisconnect
|
|
3777
|
+
});
|
|
3778
|
+
const environmentId = nodeLease ? `openclaw-node-${nodeLease.id}` : execServer.environmentId;
|
|
3255
3779
|
try {
|
|
3780
|
+
const execServerUrl = nodeLease ? `${execServer.url}?lease=${nodeLease.id}` : execServer.url;
|
|
3256
3781
|
await params.client.request("environment/add", {
|
|
3257
|
-
environmentId
|
|
3258
|
-
execServerUrl
|
|
3782
|
+
environmentId,
|
|
3783
|
+
execServerUrl
|
|
3259
3784
|
}, {
|
|
3260
3785
|
timeoutMs: params.timeoutMs,
|
|
3261
3786
|
signal: params.signal
|
|
3262
3787
|
});
|
|
3263
3788
|
} catch (error) {
|
|
3789
|
+
if (nodeLease && "node" in execServer) closeCodexNodeExecServerLease(execServer, nodeLease);
|
|
3264
3790
|
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
3791
|
throw error;
|
|
3270
3792
|
}
|
|
3271
|
-
|
|
3272
|
-
environmentId
|
|
3793
|
+
const environment = {
|
|
3794
|
+
environmentId,
|
|
3273
3795
|
cwd: params.sandbox.containerWorkdir
|
|
3274
3796
|
};
|
|
3797
|
+
if (nodeLease) codexNodeExecServerLeases.set(environment, nodeLease);
|
|
3798
|
+
return environment;
|
|
3275
3799
|
}
|
|
3276
3800
|
/** Releases the sandbox exec-server lease associated with a sandbox runtime. */
|
|
3277
|
-
async function releaseCodexSandboxExecServerEnvironment(sandbox) {
|
|
3801
|
+
async function releaseCodexSandboxExecServerEnvironment(sandbox, environment) {
|
|
3278
3802
|
if (!sandbox?.enabled) return;
|
|
3279
3803
|
const server = await sandboxExecServerRegistry.servers.get(sandbox.runtimeId)?.catch(() => void 0);
|
|
3280
|
-
if (server)
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3804
|
+
if (server) {
|
|
3805
|
+
const nodeLease = environment && codexNodeExecServerLeases.get(environment);
|
|
3806
|
+
if (nodeLease && "node" in server) {
|
|
3807
|
+
codexNodeExecServerLeases.delete(environment);
|
|
3808
|
+
closeCodexNodeExecServerLease(server, nodeLease);
|
|
3809
|
+
}
|
|
3810
|
+
await releaseOpenClawExecServer(server);
|
|
3811
|
+
}
|
|
3285
3812
|
}
|
|
3286
3813
|
function canExposeLocalExecServerToAppServer(startOptions) {
|
|
3287
3814
|
if (!startOptions || startOptions.transport !== "websocket") return true;
|
|
@@ -3295,14 +3822,60 @@ function canExposeLocalExecServerToAppServer(startOptions) {
|
|
|
3295
3822
|
return false;
|
|
3296
3823
|
}
|
|
3297
3824
|
}
|
|
3298
|
-
async function acquireOpenClawExecServer(
|
|
3825
|
+
async function acquireOpenClawExecServer(params) {
|
|
3826
|
+
const { sandbox, runtime, signal, onExecutionDisconnect } = params;
|
|
3299
3827
|
const key = sandbox.runtimeId;
|
|
3300
3828
|
while (true) {
|
|
3301
3829
|
const promise = sandboxExecServerRegistry.servers.get(key) ?? startAndRememberOpenClawExecServer(sandbox);
|
|
3302
3830
|
const server = await promise;
|
|
3303
3831
|
if (!server.closed && sandboxExecServerRegistry.servers.get(key) === promise) {
|
|
3304
3832
|
server.refCount += 1;
|
|
3305
|
-
return server;
|
|
3833
|
+
if (!("node" in server)) return { server };
|
|
3834
|
+
if (!runtime || !signal) {
|
|
3835
|
+
await releaseOpenClawExecServer(server);
|
|
3836
|
+
throw new Error("Codex paired-device execution requires an active runtime and attempt.");
|
|
3837
|
+
}
|
|
3838
|
+
try {
|
|
3839
|
+
const placementIdentity = readCodexPlacementWorkspaceIdentity(sandbox);
|
|
3840
|
+
const channel = await runtime.nodes.openDuplex({
|
|
3841
|
+
nodeId: server.node.id,
|
|
3842
|
+
command: "codex.exec-server.stdio.v1",
|
|
3843
|
+
params: {
|
|
3844
|
+
cwd: sandbox.containerWorkdir,
|
|
3845
|
+
...placementIdentity
|
|
3846
|
+
},
|
|
3847
|
+
sessionKey: sandbox.sessionKey,
|
|
3848
|
+
timeoutMs: 0,
|
|
3849
|
+
maxMessageBytes: CODEX_NODE_EXEC_SERVER_MAX_MESSAGE_BYTES,
|
|
3850
|
+
maxOutstandingDeliveryBytes: 69206016,
|
|
3851
|
+
signal
|
|
3852
|
+
});
|
|
3853
|
+
if (signal.aborted || server.closed || sandboxExecServerRegistry.servers.get(key) !== promise) {
|
|
3854
|
+
channel.close();
|
|
3855
|
+
throw new Error("Codex paired-device execution retired before its channel was ready.");
|
|
3856
|
+
}
|
|
3857
|
+
const nodeLease = {
|
|
3858
|
+
id: randomUUID(),
|
|
3859
|
+
channel,
|
|
3860
|
+
claimed: false,
|
|
3861
|
+
closed: false,
|
|
3862
|
+
onDisconnected: onExecutionDisconnect
|
|
3863
|
+
};
|
|
3864
|
+
server.node.leases.set(nodeLease.id, nodeLease);
|
|
3865
|
+
channel.closed.then(() => handleClosedCodexNodeExecServerLease(server, nodeLease, { failed: false }), (error) => handleClosedCodexNodeExecServerLease(server, nodeLease, {
|
|
3866
|
+
failed: true,
|
|
3867
|
+
error
|
|
3868
|
+
})).catch((error) => {
|
|
3869
|
+
embeddedAgentLog.warn("codex paired-device exec-server lease cleanup failed", { error: error instanceof Error ? error.message : String(error) });
|
|
3870
|
+
});
|
|
3871
|
+
return {
|
|
3872
|
+
server,
|
|
3873
|
+
nodeLease
|
|
3874
|
+
};
|
|
3875
|
+
} catch (error) {
|
|
3876
|
+
await releaseOpenClawExecServer(server);
|
|
3877
|
+
throw error;
|
|
3878
|
+
}
|
|
3306
3879
|
}
|
|
3307
3880
|
}
|
|
3308
3881
|
}
|
|
@@ -3316,24 +3889,54 @@ function startAndRememberOpenClawExecServer(sandbox) {
|
|
|
3316
3889
|
return created;
|
|
3317
3890
|
}
|
|
3318
3891
|
async function startOpenClawExecServer(sandbox) {
|
|
3892
|
+
const backend = sandbox.backend;
|
|
3893
|
+
const fsBridge = sandbox.fsBridge;
|
|
3894
|
+
const placementNodeId = readCodexPlacementNodeId(sandbox);
|
|
3895
|
+
let connection;
|
|
3896
|
+
if (placementNodeId) connection = {
|
|
3897
|
+
kind: "node",
|
|
3898
|
+
id: placementNodeId
|
|
3899
|
+
};
|
|
3900
|
+
else {
|
|
3901
|
+
if (!backend) throw new Error("OpenClaw sandbox backend is unavailable.");
|
|
3902
|
+
if (!fsBridge) throw new Error("Sandbox filesystem bridge is unavailable.");
|
|
3903
|
+
connection = {
|
|
3904
|
+
kind: "sandbox",
|
|
3905
|
+
backend,
|
|
3906
|
+
fsBridge
|
|
3907
|
+
};
|
|
3908
|
+
}
|
|
3319
3909
|
const server = new WebSocketServer({
|
|
3320
3910
|
host: "127.0.0.1",
|
|
3321
3911
|
port: 0,
|
|
3322
|
-
maxPayload: CODEX_SANDBOX_EXEC_SERVER_MAX_INBOUND_MESSAGE_BYTES
|
|
3912
|
+
maxPayload: connection.kind === "node" ? CODEX_NODE_EXEC_SERVER_MAX_MESSAGE_BYTES : CODEX_SANDBOX_EXEC_SERVER_MAX_INBOUND_MESSAGE_BYTES
|
|
3323
3913
|
});
|
|
3324
3914
|
await once(server, "listening");
|
|
3325
3915
|
const address = server.address();
|
|
3326
3916
|
if (!address || typeof address === "string") throw new Error("OpenClaw Codex exec-server did not bind to a TCP port.");
|
|
3327
3917
|
const environmentId = buildEnvironmentId(sandbox);
|
|
3328
3918
|
const authPath = `/openclaw-${randomUUID()}`;
|
|
3329
|
-
const
|
|
3919
|
+
const common = {
|
|
3330
3920
|
authPath,
|
|
3331
3921
|
closed: false,
|
|
3332
3922
|
environmentId,
|
|
3333
3923
|
refCount: 0,
|
|
3334
3924
|
url: `ws://127.0.0.1:${address.port}${authPath}`,
|
|
3335
3925
|
sandbox,
|
|
3336
|
-
server
|
|
3926
|
+
server,
|
|
3927
|
+
children: /* @__PURE__ */ new Set(),
|
|
3928
|
+
cleanupTasks: /* @__PURE__ */ new Set()
|
|
3929
|
+
};
|
|
3930
|
+
const execServer = connection.kind === "node" ? {
|
|
3931
|
+
...common,
|
|
3932
|
+
node: {
|
|
3933
|
+
id: connection.id,
|
|
3934
|
+
leases: /* @__PURE__ */ new Map()
|
|
3935
|
+
}
|
|
3936
|
+
} : {
|
|
3937
|
+
...common,
|
|
3938
|
+
backend: connection.backend,
|
|
3939
|
+
fsBridge: connection.fsBridge
|
|
3337
3940
|
};
|
|
3338
3941
|
server.on("connection", (socket, request) => {
|
|
3339
3942
|
socket.on("error", handleExecServerSocketError);
|
|
@@ -3341,6 +3944,10 @@ async function startOpenClawExecServer(sandbox) {
|
|
|
3341
3944
|
socket.close(1008, "unauthorized");
|
|
3342
3945
|
return;
|
|
3343
3946
|
}
|
|
3947
|
+
if ("node" in execServer) {
|
|
3948
|
+
handleNodeConnection(execServer, socket, request);
|
|
3949
|
+
return;
|
|
3950
|
+
}
|
|
3344
3951
|
handleConnection(execServer, socket);
|
|
3345
3952
|
});
|
|
3346
3953
|
embeddedAgentLog.info("codex sandbox exec-server started", {
|
|
@@ -3357,15 +3964,7 @@ async function releaseOpenClawExecServer(execServer) {
|
|
|
3357
3964
|
const current = await sandboxExecServerRegistry.servers.get(execServer.sandbox.runtimeId)?.catch(() => void 0);
|
|
3358
3965
|
if (execServer.refCount > 0 || execServer.closed) return;
|
|
3359
3966
|
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
|
-
});
|
|
3967
|
+
await sandboxExecServerRegistry.close(execServer);
|
|
3369
3968
|
}
|
|
3370
3969
|
function buildEnvironmentId(sandbox) {
|
|
3371
3970
|
return `openclaw-sandbox-${createHash("sha256").update(sandbox.runtimeId).digest("hex").slice(0, 16)}`;
|
|
@@ -3373,80 +3972,82 @@ function buildEnvironmentId(sandbox) {
|
|
|
3373
3972
|
function isAuthorizedExecServerRequest(execServer, request) {
|
|
3374
3973
|
return new URL(request.url ?? "", "ws://127.0.0.1").pathname === execServer.authPath;
|
|
3375
3974
|
}
|
|
3975
|
+
function readCodexPlacementNodeId(sandbox) {
|
|
3976
|
+
if (!("placementExecutionMode" in sandbox) || sandbox.placementExecutionMode !== "remote-exec" || !("placementNodeId" in sandbox) || typeof sandbox.placementNodeId !== "string" || !sandbox.placementNodeId) return;
|
|
3977
|
+
return sandbox.placementNodeId;
|
|
3978
|
+
}
|
|
3979
|
+
function readCodexPlacementWorkspaceIdentity(sandbox) {
|
|
3980
|
+
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 paired-device execution requires its exact placement workspace identity.");
|
|
3981
|
+
return {
|
|
3982
|
+
environmentId: sandbox.placementEnvironmentId,
|
|
3983
|
+
sessionId: sandbox.placementSessionId,
|
|
3984
|
+
ownerEpoch: sandbox.placementOwnerEpoch,
|
|
3985
|
+
sessionKey: sandbox.sessionKey
|
|
3986
|
+
};
|
|
3987
|
+
}
|
|
3988
|
+
function handleNodeConnection(execServer, socket, request) {
|
|
3989
|
+
const leaseId = new URL(request.url ?? "", "ws://127.0.0.1").searchParams.get("lease");
|
|
3990
|
+
const lease = leaseId ? execServer.node.leases.get(leaseId) : void 0;
|
|
3991
|
+
if (!lease || lease.claimed || lease.closed) {
|
|
3992
|
+
socket.close(1008, "execution channel unavailable");
|
|
3993
|
+
return;
|
|
3994
|
+
}
|
|
3995
|
+
lease.claimed = true;
|
|
3996
|
+
const cleanup = startCodexNodeExecServerRelay({
|
|
3997
|
+
lease,
|
|
3998
|
+
socket
|
|
3999
|
+
});
|
|
4000
|
+
execServer.cleanupTasks.add(cleanup);
|
|
4001
|
+
cleanup.then(() => execServer.cleanupTasks.delete(cleanup), (error) => {
|
|
4002
|
+
execServer.cleanupTasks.delete(cleanup);
|
|
4003
|
+
embeddedAgentLog.warn("codex paired-device exec-server relay failed", { error: error instanceof Error ? error.message : String(error) });
|
|
4004
|
+
});
|
|
4005
|
+
}
|
|
4006
|
+
function closeCodexNodeExecServerLease(execServer, lease) {
|
|
4007
|
+
execServer.node.leases.delete(lease.id);
|
|
4008
|
+
if (!lease.closed) {
|
|
4009
|
+
lease.closed = true;
|
|
4010
|
+
lease.closeRelay?.();
|
|
4011
|
+
lease.channel.close();
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
4014
|
+
function handleClosedCodexNodeExecServerLease(execServer, lease, result) {
|
|
4015
|
+
if (lease.closed) return;
|
|
4016
|
+
if (lease.onChannelClosed) {
|
|
4017
|
+
lease.onChannelClosed(result);
|
|
4018
|
+
return;
|
|
4019
|
+
}
|
|
4020
|
+
try {
|
|
4021
|
+
lease.onDisconnected?.(createCodexNodeExecServerDisconnectError(result.failed ? "execution device failed" : "execution device disconnected", result.error));
|
|
4022
|
+
} finally {
|
|
4023
|
+
closeCodexNodeExecServerLease(execServer, lease);
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
3376
4026
|
function handleConnection(execServer, socket) {
|
|
3377
|
-
const
|
|
3378
|
-
|
|
4027
|
+
const session = new CodexSandboxExecSession(execServer, {
|
|
4028
|
+
isOpen: () => socket.readyState === socket.OPEN,
|
|
4029
|
+
send: (message) => socket.send(JSON.stringify(message))
|
|
4030
|
+
});
|
|
3379
4031
|
socket.on("message", (data) => {
|
|
3380
|
-
handleMessage(
|
|
4032
|
+
handleMessage(session, data).catch((error) => {
|
|
3381
4033
|
embeddedAgentLog.warn("codex sandbox exec-server message failed", { error });
|
|
3382
4034
|
});
|
|
3383
4035
|
});
|
|
3384
4036
|
socket.on("close", () => {
|
|
3385
|
-
|
|
3386
|
-
|
|
4037
|
+
const cleanup = session.close();
|
|
4038
|
+
execServer.cleanupTasks.add(cleanup);
|
|
4039
|
+
cleanup.then(() => execServer.cleanupTasks.delete(cleanup), (error) => {
|
|
4040
|
+
execServer.cleanupTasks.delete(cleanup);
|
|
4041
|
+
embeddedAgentLog.warn("codex sandbox exec-server socket cleanup failed", { error: error instanceof Error ? error.message : String(error) });
|
|
4042
|
+
});
|
|
3387
4043
|
});
|
|
3388
4044
|
}
|
|
3389
4045
|
function handleExecServerSocketError(error) {
|
|
3390
4046
|
embeddedAgentLog.debug("codex sandbox exec-server websocket failed", { error });
|
|
3391
4047
|
}
|
|
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
|
-
}
|
|
4048
|
+
async function handleMessage(session, data) {
|
|
4049
|
+
const buffer = Array.isArray(data) ? Buffer.concat(data) : Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
4050
|
+
await session.handleRequest(parseRequest(buffer.toString("utf8")));
|
|
3450
4051
|
}
|
|
3451
4052
|
//#endregion
|
|
3452
4053
|
//#region extensions/codex/src/app-server/provider-capabilities.ts
|
|
@@ -3495,87 +4096,6 @@ async function resolveCodexProviderWebSearchSupport(params) {
|
|
|
3495
4096
|
}
|
|
3496
4097
|
}
|
|
3497
4098
|
//#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
4099
|
//#region extensions/codex/src/app-server/approval-bridge.ts
|
|
3580
4100
|
/**
|
|
3581
4101
|
* Bridges Codex app-server approval requests into OpenClaw policy hooks and
|
|
@@ -3600,19 +4120,55 @@ async function handleCodexAppServerApprovalRequest(params) {
|
|
|
3600
4120
|
requestParams,
|
|
3601
4121
|
paramsForRun: params.paramsForRun
|
|
3602
4122
|
});
|
|
3603
|
-
|
|
4123
|
+
if (params.signal?.aborted) {
|
|
4124
|
+
recordNativeToolFailureDisposition(params, context, "cancelled");
|
|
4125
|
+
return buildApprovalResponse(params.method, context.requestParams, "cancelled");
|
|
4126
|
+
}
|
|
4127
|
+
let revalidateMutableFileApproval;
|
|
4128
|
+
let mutableFileApprovalRequiresOneShot = false;
|
|
4129
|
+
const resolvePolicyApproval = async (outcome, message = approvalResolutionMessage(outcome), approvalId) => {
|
|
4130
|
+
let resolvedOutcome = outcome;
|
|
4131
|
+
let resolvedMessage = message;
|
|
4132
|
+
if (outcome !== "denied" && revalidateMutableFileApproval) {
|
|
4133
|
+
const binding = await revalidateMutableFileApproval();
|
|
4134
|
+
if (!binding.ok) {
|
|
4135
|
+
resolvedOutcome = "denied";
|
|
4136
|
+
resolvedMessage = binding.message;
|
|
4137
|
+
}
|
|
4138
|
+
}
|
|
4139
|
+
if (resolvedOutcome === "approved-session" && mutableFileApprovalRequiresOneShot) {
|
|
4140
|
+
resolvedOutcome = "approved-once";
|
|
4141
|
+
resolvedMessage = "Codex app-server approval granted for this byte-bound command only.";
|
|
4142
|
+
}
|
|
3604
4143
|
emitApprovalEvent(params.paramsForRun, {
|
|
3605
4144
|
phase: "resolved",
|
|
3606
4145
|
kind: context.kind,
|
|
3607
|
-
status:
|
|
4146
|
+
status: resolvedOutcome === "denied" ? "denied" : "approved",
|
|
3608
4147
|
title: context.title,
|
|
4148
|
+
...approvalId ? {
|
|
4149
|
+
approvalId,
|
|
4150
|
+
approvalSlug: approvalId
|
|
4151
|
+
} : {},
|
|
3609
4152
|
...context.eventDetails,
|
|
3610
|
-
...approvalEventScope(params.method,
|
|
3611
|
-
message
|
|
4153
|
+
...approvalEventScope(params.method, resolvedOutcome),
|
|
4154
|
+
message: resolvedMessage
|
|
3612
4155
|
});
|
|
3613
|
-
return buildApprovalResponse(params.method, context.requestParams,
|
|
4156
|
+
return buildApprovalResponse(params.method, context.requestParams, resolvedOutcome);
|
|
3614
4157
|
};
|
|
3615
4158
|
try {
|
|
4159
|
+
if (params.method === "item/commandExecution/requestApproval") {
|
|
4160
|
+
const command = readPolicyCommand(requestParams);
|
|
4161
|
+
const cwd = readStringField(requestParams, "cwd") ?? params.paramsForRun.workspaceDir;
|
|
4162
|
+
const prepareMutableFileApproval = params.paramsForRun.hostCapabilities.prepareMutableFileApproval;
|
|
4163
|
+
if (!prepareMutableFileApproval) return await resolvePolicyApproval("denied", "SYSTEM_RUN_DENIED: mutable file approval binding is unavailable");
|
|
4164
|
+
const prepared = await prepareMutableFileApproval({
|
|
4165
|
+
command: command ?? "",
|
|
4166
|
+
cwd
|
|
4167
|
+
});
|
|
4168
|
+
if (!prepared.ok) return await resolvePolicyApproval("denied", prepared.message);
|
|
4169
|
+
mutableFileApprovalRequiresOneShot = prepared.requiresOneShot;
|
|
4170
|
+
revalidateMutableFileApproval = prepared.revalidate;
|
|
4171
|
+
}
|
|
3616
4172
|
const policyOutcome = await runOpenClawToolPolicyForApprovalRequest({
|
|
3617
4173
|
method: params.method,
|
|
3618
4174
|
requestParams,
|
|
@@ -3624,19 +4180,22 @@ async function handleCodexAppServerApprovalRequest(params) {
|
|
|
3624
4180
|
});
|
|
3625
4181
|
if (policyOutcome?.outcome === "denied") {
|
|
3626
4182
|
recordNativeToolFailureDisposition(params, context, policyOutcome.failureDisposition);
|
|
3627
|
-
return resolvePolicyApproval("denied", policyOutcome.reason);
|
|
4183
|
+
return await resolvePolicyApproval("denied", policyOutcome.reason);
|
|
3628
4184
|
}
|
|
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.");
|
|
4185
|
+
if (policyOutcome?.outcome === "approved-once" || policyOutcome?.outcome === "approved-session") return await resolvePolicyApproval(policyOutcome.outcome);
|
|
4186
|
+
if (CONCRETE_TOOL_AUTO_APPROVAL_METHODS.has(params.method) && params.autoApprove === true) return await resolvePolicyApproval("approved-session", "Codex app-server approval auto-approved by runtime policy.");
|
|
3633
4187
|
const requestResult = await requestPluginApproval({
|
|
3634
4188
|
hostCapabilities: params.paramsForRun.hostCapabilities,
|
|
3635
4189
|
title: context.title,
|
|
3636
4190
|
description: context.description,
|
|
3637
4191
|
severity: context.severity,
|
|
3638
4192
|
toolName: context.toolName,
|
|
3639
|
-
toolCallId: context.approvalId
|
|
4193
|
+
toolCallId: context.approvalId,
|
|
4194
|
+
allowedDecisions: nativeApprovalAllowedDecisions({
|
|
4195
|
+
method: params.method,
|
|
4196
|
+
requestParams,
|
|
4197
|
+
requiresOneShot: mutableFileApprovalRequiresOneShot
|
|
4198
|
+
})
|
|
3640
4199
|
});
|
|
3641
4200
|
const approvalId = requestResult?.id;
|
|
3642
4201
|
if (!approvalId) {
|
|
@@ -3662,16 +4221,17 @@ async function handleCodexAppServerApprovalRequest(params) {
|
|
|
3662
4221
|
...context.eventDetails,
|
|
3663
4222
|
message: "Codex app-server approval requested."
|
|
3664
4223
|
});
|
|
3665
|
-
const
|
|
3666
|
-
const decision = requestUnavailable ? null : await waitForPluginApprovalDecision({
|
|
4224
|
+
const approvalResult = approvalRequestExplicitlyUnavailable(requestResult) ? void 0 : await waitForPluginApprovalDecision({
|
|
3667
4225
|
approvalId,
|
|
3668
4226
|
signal: params.signal,
|
|
3669
4227
|
hostCapabilities: params.paramsForRun.hostCapabilities
|
|
3670
4228
|
});
|
|
3671
|
-
const
|
|
3672
|
-
const outcome = params.signal?.aborted ? "cancelled" : mapExecDecisionToOutcome(decision);
|
|
3673
|
-
if (
|
|
3674
|
-
else if (outcome === "
|
|
4229
|
+
const approvalTimedOut = !params.signal?.aborted && approvalResult?.terminalReason === "timeout";
|
|
4230
|
+
const outcome = params.signal?.aborted ? "cancelled" : mapExecDecisionToOutcome(approvalResult?.decision);
|
|
4231
|
+
if (approvalTimedOut) recordNativeToolFailureDisposition(params, context, "timed_out", context.approvalKind);
|
|
4232
|
+
else if (outcome === "cancelled") recordNativeToolFailureDisposition(params, context, params.signal?.aborted ? resolveCodexToolAbortTerminalReason(params.signal) : "cancelled");
|
|
4233
|
+
else if (outcome === "unavailable") recordNativeToolFailureDisposition(params, context, "failed");
|
|
4234
|
+
if (outcome === "approved-once" || outcome === "approved-session") return await resolvePolicyApproval(outcome, approvalResolutionMessage(outcome), approvalId);
|
|
3675
4235
|
emitApprovalEvent(params.paramsForRun, {
|
|
3676
4236
|
phase: "resolved",
|
|
3677
4237
|
kind: context.kind,
|
|
@@ -3681,7 +4241,7 @@ async function handleCodexAppServerApprovalRequest(params) {
|
|
|
3681
4241
|
approvalSlug: approvalId,
|
|
3682
4242
|
...context.eventDetails,
|
|
3683
4243
|
...approvalEventScope(params.method, outcome),
|
|
3684
|
-
message: approvalResolutionMessage(outcome)
|
|
4244
|
+
message: approvalTimedOut ? codexApprovalTimeoutText(context.approvalKind) : approvalResolutionMessage(outcome)
|
|
3685
4245
|
});
|
|
3686
4246
|
return buildApprovalResponse(params.method, context.requestParams, outcome);
|
|
3687
4247
|
} catch (error) {
|
|
@@ -3699,10 +4259,11 @@ async function handleCodexAppServerApprovalRequest(params) {
|
|
|
3699
4259
|
return buildApprovalResponse(params.method, context.requestParams, cancelled ? "cancelled" : "denied");
|
|
3700
4260
|
}
|
|
3701
4261
|
}
|
|
3702
|
-
function recordNativeToolFailureDisposition(params, context, disposition) {
|
|
4262
|
+
function recordNativeToolFailureDisposition(params, context, disposition, approvalKind) {
|
|
3703
4263
|
if (!context.itemId || !disposition) return;
|
|
3704
4264
|
try {
|
|
3705
|
-
|
|
4265
|
+
const resolvedDisposition = params.signal?.aborted ? resolveCodexToolAbortTerminalReason(params.signal) : disposition;
|
|
4266
|
+
params.onNativeToolFailureDisposition?.(context.itemId, resolvedDisposition, ...resolvedDisposition === "timed_out" && approvalKind ? [approvalKind] : []);
|
|
3706
4267
|
} catch {}
|
|
3707
4268
|
}
|
|
3708
4269
|
/** Converts an OpenClaw approval outcome into the app-server method response. */
|
|
@@ -3719,7 +4280,10 @@ function buildApprovalResponse(method, requestParams, outcome) {
|
|
|
3719
4280
|
scope: "turn"
|
|
3720
4281
|
};
|
|
3721
4282
|
}
|
|
3722
|
-
return
|
|
4283
|
+
return {
|
|
4284
|
+
decision: "decline",
|
|
4285
|
+
reason: "OpenClaw codex app-server bridge does not grant native approvals yet."
|
|
4286
|
+
};
|
|
3723
4287
|
}
|
|
3724
4288
|
function matchesCurrentTurn(requestParams, threadId, turnId) {
|
|
3725
4289
|
if (!requestParams) return false;
|
|
@@ -3735,18 +4299,16 @@ function buildApprovalContext(params) {
|
|
|
3735
4299
|
const reasonPreview = sanitizeApprovalPreview(readStringPreview(params.requestParams, "reason"), 180);
|
|
3736
4300
|
const command = commandPreview.text;
|
|
3737
4301
|
const reason = reasonPreview.text;
|
|
3738
|
-
const
|
|
4302
|
+
const approvalKind = params.method.includes("commandExecution") ? "command" : params.method.includes("fileChange") ? "file-change" : params.method.includes("permissions") ? "permissions" : "other";
|
|
4303
|
+
const kind = approvalKind === "command" ? "exec" : approvalKind === "other" ? "unknown" : "plugin";
|
|
3739
4304
|
const permissionLines = params.method === "item/permissions/requestApproval" ? describeRequestedPermissions(params.requestParams) : [];
|
|
3740
4305
|
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
4306
|
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}`);
|
|
3742
4307
|
return {
|
|
4308
|
+
approvalKind,
|
|
3743
4309
|
kind,
|
|
3744
4310
|
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"),
|
|
4311
|
+
description: permissionLines.length > 0 ? joinDescriptionLinesWithinLimit(permissionLines, PERMISSION_DESCRIPTION_MAX_LENGTH) : [subject, ...commandDetailLines].join("\n"),
|
|
3750
4312
|
severity: kind === "exec" ? "warning" : "info",
|
|
3751
4313
|
toolName: kind === "exec" ? "codex_command_approval" : params.method === "item/permissions/requestApproval" ? "codex_permission_approval" : "codex_file_approval",
|
|
3752
4314
|
itemId,
|
|
@@ -3960,14 +4522,27 @@ function stableJsonText(value) {
|
|
|
3960
4522
|
}
|
|
3961
4523
|
}
|
|
3962
4524
|
function commandApprovalDecision(requestParams, outcome) {
|
|
3963
|
-
if (outcome === "cancelled") return
|
|
3964
|
-
if (outcome === "denied" || outcome === "unavailable") return
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
4525
|
+
if (outcome === "cancelled") return "cancel";
|
|
4526
|
+
if (outcome === "denied" || outcome === "unavailable") return "decline";
|
|
4527
|
+
const capabilities = commandApprovalCapabilities(requestParams);
|
|
4528
|
+
if (outcome === "approved-session" && capabilities.sessionDecision !== void 0) return capabilities.sessionDecision;
|
|
4529
|
+
return capabilities.once ? "accept" : "decline";
|
|
4530
|
+
}
|
|
4531
|
+
function nativeApprovalAllowedDecisions(params) {
|
|
4532
|
+
if (params.method === "item/fileChange/requestApproval") return [
|
|
4533
|
+
"allow-once",
|
|
4534
|
+
"allow-always",
|
|
4535
|
+
"deny"
|
|
4536
|
+
];
|
|
4537
|
+
if (params.method !== "item/commandExecution/requestApproval") return;
|
|
4538
|
+
const available = params.requestParams?.availableDecisions;
|
|
4539
|
+
if (!Array.isArray(available)) return;
|
|
4540
|
+
const capabilities = commandApprovalCapabilities(params.requestParams);
|
|
4541
|
+
const decisions = [];
|
|
4542
|
+
if (capabilities.once) decisions.push("allow-once");
|
|
4543
|
+
if (!params.requiresOneShot && capabilities.sessionDecision !== void 0) decisions.push("allow-always");
|
|
4544
|
+
decisions.push("deny");
|
|
4545
|
+
return decisions;
|
|
3971
4546
|
}
|
|
3972
4547
|
function fileChangeApprovalDecision(outcome) {
|
|
3973
4548
|
if (outcome === "cancelled") return "cancel";
|
|
@@ -3981,12 +4556,6 @@ function requestedPermissions(requestParams) {
|
|
|
3981
4556
|
if (isJsonObject(permissions.fileSystem)) granted.fileSystem = permissions.fileSystem;
|
|
3982
4557
|
return granted;
|
|
3983
4558
|
}
|
|
3984
|
-
function unsupportedApprovalResponse() {
|
|
3985
|
-
return {
|
|
3986
|
-
decision: "decline",
|
|
3987
|
-
reason: "OpenClaw codex app-server bridge does not grant native approvals yet."
|
|
3988
|
-
};
|
|
3989
|
-
}
|
|
3990
4559
|
function describeRequestedPermissions(requestParams) {
|
|
3991
4560
|
return describePermissionProfile(requestedPermissions(requestParams), "Permissions");
|
|
3992
4561
|
}
|
|
@@ -4154,43 +4723,38 @@ function isPrivateNetworkHostPattern(value) {
|
|
|
4154
4723
|
if (wildcardStripped.startsWith("10.") || wildcardStripped.startsWith("127.") || wildcardStripped.startsWith("192.168.") || wildcardStripped.startsWith("169.254.")) return true;
|
|
4155
4724
|
return /^172\.(1[6-9]|2\d|3[0-1])\./.test(wildcardStripped);
|
|
4156
4725
|
}
|
|
4157
|
-
function
|
|
4726
|
+
function commandApprovalCapabilities(requestParams) {
|
|
4158
4727
|
const available = requestParams?.availableDecisions;
|
|
4159
|
-
if (!Array.isArray(available)) return
|
|
4160
|
-
|
|
4728
|
+
if (!Array.isArray(available)) return {
|
|
4729
|
+
once: true,
|
|
4730
|
+
sessionDecision: "acceptForSession"
|
|
4731
|
+
};
|
|
4732
|
+
return {
|
|
4733
|
+
once: available.includes("accept"),
|
|
4734
|
+
...available.includes("acceptForSession") ? { sessionDecision: "acceptForSession" } : { sessionDecision: findAvailableCommandAmendmentDecision(requestParams) }
|
|
4735
|
+
};
|
|
4161
4736
|
}
|
|
4162
4737
|
function findAvailableCommandAmendmentDecision(requestParams) {
|
|
4163
4738
|
const available = requestParams?.availableDecisions;
|
|
4164
4739
|
if (!Array.isArray(available)) return;
|
|
4165
4740
|
return available.find((entry) => isJsonObject(entry) && (isJsonObject(entry.acceptWithExecpolicyAmendment) || isJsonObject(entry.applyNetworkPolicyAmendment)));
|
|
4166
4741
|
}
|
|
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
4742
|
function approvalResolutionMessage(outcome) {
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4743
|
+
return {
|
|
4744
|
+
"approved-session": "Codex app-server approval granted for the session.",
|
|
4745
|
+
"approved-once": "Codex app-server approval granted for this turn.",
|
|
4746
|
+
cancelled: "Codex app-server approval cancelled.",
|
|
4747
|
+
unavailable: "Codex app-server approval unavailable.",
|
|
4748
|
+
denied: "Codex app-server approval denied."
|
|
4749
|
+
}[outcome];
|
|
4181
4750
|
}
|
|
4182
4751
|
function approvalEventScope(method, outcome) {
|
|
4183
4752
|
return method === "item/permissions/requestApproval" ? { scope: outcome === "approved-session" ? "session" : "turn" } : {};
|
|
4184
4753
|
}
|
|
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
4754
|
function emitApprovalEvent(params, data) {
|
|
4191
4755
|
params.onAgentEvent?.({
|
|
4192
4756
|
stream: "approval",
|
|
4193
|
-
data
|
|
4757
|
+
data: { ...data }
|
|
4194
4758
|
});
|
|
4195
4759
|
}
|
|
4196
4760
|
function readDisplayCommandPreview(record) {
|
|
@@ -4389,11 +4953,11 @@ const MAX_DISPLAY_VALUE_ARRAY_ITEMS = 8;
|
|
|
4389
4953
|
const MAX_DISPLAY_VALUE_OBJECT_KEYS = 8;
|
|
4390
4954
|
const MAX_DISPLAY_VALUE_DEPTH = 3;
|
|
4391
4955
|
const DISPLAY_TEXT_SCAN_MAX_LENGTH = 4096;
|
|
4392
|
-
async function
|
|
4956
|
+
async function routeCodexAppServerElicitationRequest(params) {
|
|
4393
4957
|
const requestParams = isJsonObject(params.requestParams) ? params.requestParams : void 0;
|
|
4394
|
-
if (!requestParams || readNonBlankStringField(requestParams, "threadId") !== params.threadId) return;
|
|
4958
|
+
if (!requestParams || readNonBlankStringField(requestParams, "threadId") !== params.threadId) return { kind: "not-mine" };
|
|
4395
4959
|
const requestTurnId = requestParams.turnId;
|
|
4396
|
-
if (requestTurnId !== null && requestTurnId !== void 0 && requestTurnId !== params.turnId) return;
|
|
4960
|
+
if (requestTurnId !== null && requestTurnId !== void 0 && requestTurnId !== params.turnId) return { kind: "not-mine" };
|
|
4397
4961
|
const pluginResolution = resolvePluginElicitation({
|
|
4398
4962
|
requestParams,
|
|
4399
4963
|
pluginAppPolicyContext: params.pluginAppPolicyContext
|
|
@@ -4401,32 +4965,38 @@ async function handleCodexAppServerElicitationRequest(params) {
|
|
|
4401
4965
|
if (pluginResolution.kind !== "not_plugin") {
|
|
4402
4966
|
if (params.paramsForRun.trigger === "cron" && params.paramsForRun.scheduledRuntimeAuthority) {
|
|
4403
4967
|
logPluginElicitationDecline("scheduled_authority_non_interactive", requestParams);
|
|
4404
|
-
return
|
|
4968
|
+
return handled(createCodexElicitationResponse("decline"));
|
|
4405
4969
|
}
|
|
4406
4970
|
if (pluginResolution.kind === "decline") {
|
|
4407
4971
|
logPluginElicitationDecline(pluginResolution.reason, requestParams);
|
|
4408
|
-
return
|
|
4972
|
+
return handled(createCodexElicitationResponse("decline"));
|
|
4409
4973
|
}
|
|
4410
4974
|
if (requestTurnId !== params.turnId) {
|
|
4411
4975
|
logPluginElicitationDecline("missing_active_turn", requestParams);
|
|
4412
|
-
return
|
|
4976
|
+
return handled(createCodexElicitationResponse("decline"));
|
|
4413
4977
|
}
|
|
4414
|
-
return await buildPluginPolicyElicitationResponse({
|
|
4978
|
+
return handled(await buildPluginPolicyElicitationResponse({
|
|
4415
4979
|
entry: pluginResolution.entry,
|
|
4416
4980
|
requestParams,
|
|
4417
4981
|
paramsForRun: params.paramsForRun,
|
|
4418
4982
|
signal: params.signal
|
|
4419
|
-
});
|
|
4983
|
+
}));
|
|
4420
4984
|
}
|
|
4421
4985
|
const approvalPrompt = readComputerUseApprovalElicitation(requestParams, params.computerUseMcpServerName) ?? readBridgeableApprovalElicitation(requestParams);
|
|
4422
|
-
if (!approvalPrompt) return;
|
|
4423
|
-
return buildElicitationResponse(approvalPrompt, await requestPluginApprovalOutcome({
|
|
4986
|
+
if (!approvalPrompt) return (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 ? handled(createCodexElicitationResponse("decline")) : { kind: "not-mine" };
|
|
4987
|
+
return handled(buildElicitationResponse(approvalPrompt, await requestPluginApprovalOutcome({
|
|
4424
4988
|
paramsForRun: params.paramsForRun,
|
|
4425
4989
|
title: approvalPrompt.title,
|
|
4426
4990
|
description: approvalPrompt.description,
|
|
4427
4991
|
allowedDecisions: approvalPrompt.allowedDecisions,
|
|
4428
4992
|
signal: params.signal
|
|
4429
|
-
}));
|
|
4993
|
+
})));
|
|
4994
|
+
}
|
|
4995
|
+
function handled(response) {
|
|
4996
|
+
return {
|
|
4997
|
+
kind: "handled",
|
|
4998
|
+
response
|
|
4999
|
+
};
|
|
4430
5000
|
}
|
|
4431
5001
|
function resolvePluginElicitation(params) {
|
|
4432
5002
|
const requestParams = params.requestParams;
|
|
@@ -4527,26 +5097,27 @@ async function buildPluginPolicyElicitationResponse(params) {
|
|
|
4527
5097
|
const mode = resolvePluginDestructiveApprovalMode(params.entry);
|
|
4528
5098
|
if (mode === "deny") {
|
|
4529
5099
|
logPluginElicitationDecline("destructive_actions_disabled", params.requestParams);
|
|
4530
|
-
return
|
|
5100
|
+
return createCodexElicitationResponse("decline");
|
|
4531
5101
|
}
|
|
4532
5102
|
const approvalPrompt = readPluginApprovalElicitation(params.entry, params.requestParams);
|
|
4533
5103
|
if (!approvalPrompt) {
|
|
4534
5104
|
logPluginElicitationDecline("unsupported_schema", params.requestParams);
|
|
4535
|
-
return
|
|
5105
|
+
return createCodexElicitationResponse("decline");
|
|
4536
5106
|
}
|
|
4537
5107
|
const response = buildElicitationResponse(approvalPrompt, "approved-once");
|
|
4538
|
-
if (
|
|
5108
|
+
if (response.action === "accept") {
|
|
4539
5109
|
if (mode === "allow") return response;
|
|
4540
|
-
|
|
5110
|
+
const outcome = await requestPluginApprovalOutcome({
|
|
4541
5111
|
paramsForRun: params.paramsForRun,
|
|
4542
5112
|
title: approvalPrompt.title,
|
|
4543
5113
|
description: approvalPrompt.description,
|
|
4544
5114
|
allowedDecisions: allowedPluginPolicyApprovalDecisions(mode, approvalPrompt),
|
|
4545
5115
|
signal: params.signal
|
|
4546
|
-
})
|
|
5116
|
+
});
|
|
5117
|
+
return buildElicitationResponse(approvalPrompt, mode === "ask" && outcome === "approved-session" ? "approved-once" : outcome);
|
|
4547
5118
|
}
|
|
4548
5119
|
logPluginElicitationDecline("unmappable_schema", params.requestParams);
|
|
4549
|
-
return
|
|
5120
|
+
return createCodexElicitationResponse("decline");
|
|
4550
5121
|
}
|
|
4551
5122
|
function resolvePluginDestructiveApprovalMode(entry) {
|
|
4552
5123
|
return entry.destructiveApprovalMode ?? (entry.allowDestructiveActions ? "allow" : "deny");
|
|
@@ -4556,9 +5127,6 @@ function allowedPluginPolicyApprovalDecisions(mode, approvalPrompt) {
|
|
|
4556
5127
|
if (mode !== "ask") return allowedDecisions;
|
|
4557
5128
|
return allowedDecisions.filter((decision) => decision !== "allow-always");
|
|
4558
5129
|
}
|
|
4559
|
-
function oneShotPluginPolicyApprovalOutcome(mode, outcome) {
|
|
4560
|
-
return mode === "ask" && outcome === "approved-session" ? "approved-once" : outcome;
|
|
4561
|
-
}
|
|
4562
5130
|
function readPluginApprovalElicitation(entry, requestParams) {
|
|
4563
5131
|
if (readNonBlankStringField(requestParams, "mode") !== "form" || !isJsonObject(requestParams.requestedSchema)) return;
|
|
4564
5132
|
const requestedSchema = requestParams.requestedSchema;
|
|
@@ -4602,13 +5170,6 @@ function canMapPersistentApproval(requestedSchema, meta) {
|
|
|
4602
5170
|
}) && chooseAlwaysPersistOptionValue(readEnumOptions(schema)) !== void 0;
|
|
4603
5171
|
});
|
|
4604
5172
|
}
|
|
4605
|
-
function declineElicitationResponse() {
|
|
4606
|
-
return {
|
|
4607
|
-
action: "decline",
|
|
4608
|
-
content: null,
|
|
4609
|
-
_meta: null
|
|
4610
|
-
};
|
|
4611
|
-
}
|
|
4612
5173
|
function logPluginElicitationDecline(reason, requestParams) {
|
|
4613
5174
|
embeddedAgentLog.debug("codex plugin elicitation declined", {
|
|
4614
5175
|
reason,
|
|
@@ -4751,27 +5312,23 @@ async function requestPluginApprovalOutcome(params) {
|
|
|
4751
5312
|
});
|
|
4752
5313
|
const approvalId = requestResult?.id;
|
|
4753
5314
|
if (!approvalId) return "unavailable";
|
|
4754
|
-
|
|
5315
|
+
const approvalResult = approvalRequestExplicitlyUnavailable(requestResult) ? void 0 : await waitForPluginApprovalDecision({
|
|
4755
5316
|
hostCapabilities: params.paramsForRun.hostCapabilities,
|
|
4756
5317
|
approvalId,
|
|
4757
5318
|
signal: params.signal
|
|
4758
|
-
})
|
|
5319
|
+
});
|
|
5320
|
+
if (params.signal?.aborted) return "cancelled";
|
|
5321
|
+
if (approvalResult?.terminalReason === "timeout") return "timed-out";
|
|
5322
|
+
return mapExecDecisionToOutcome(approvalResult?.decision);
|
|
4759
5323
|
} catch {
|
|
4760
5324
|
return params.signal?.aborted ? "cancelled" : "denied";
|
|
4761
5325
|
}
|
|
4762
5326
|
}
|
|
4763
5327
|
function buildElicitationResponse(approvalPrompt, outcome) {
|
|
4764
5328
|
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
|
-
};
|
|
5329
|
+
if (outcome === "cancelled") return createCodexElicitationResponse("cancel");
|
|
5330
|
+
if (outcome === "timed-out") return createCodexElicitationResponse("decline", null, { message: codexApprovalTimeoutText("other") });
|
|
5331
|
+
if (outcome === "denied" || outcome === "unavailable") return createCodexElicitationResponse("decline");
|
|
4775
5332
|
const content = buildAcceptedContent(approvalPrompt, outcome);
|
|
4776
5333
|
if (!content && !hasNoSchemaProperties(requestedSchema)) {
|
|
4777
5334
|
embeddedAgentLog.warn("codex MCP approval elicitation approved without a mappable response", {
|
|
@@ -4779,13 +5336,9 @@ function buildElicitationResponse(approvalPrompt, outcome) {
|
|
|
4779
5336
|
fields: Object.keys(requestedSchema.properties ?? {}),
|
|
4780
5337
|
outcome
|
|
4781
5338
|
});
|
|
4782
|
-
return
|
|
5339
|
+
return createCodexElicitationResponse("decline");
|
|
4783
5340
|
}
|
|
4784
|
-
return
|
|
4785
|
-
action: "accept",
|
|
4786
|
-
content: content ?? null,
|
|
4787
|
-
_meta: buildAcceptedMeta(meta, outcome, approvalPrompt.persistHintsMode ?? "legacy")
|
|
4788
|
-
};
|
|
5341
|
+
return createCodexElicitationResponse("accept", content ?? null, buildAcceptedMeta(meta, outcome, approvalPrompt.persistHintsMode ?? "legacy"));
|
|
4789
5342
|
}
|
|
4790
5343
|
function buildAcceptedContent(approvalPrompt, outcome) {
|
|
4791
5344
|
const { requestedSchema, meta } = approvalPrompt;
|
|
@@ -4920,6 +5473,36 @@ function readFirstString$1(record, keys) {
|
|
|
4920
5473
|
* Bridges OpenClaw runtime tools into Codex app-server dynamic tool specs and
|
|
4921
5474
|
* tool-call responses.
|
|
4922
5475
|
*/
|
|
5476
|
+
const INTERNAL_TOOL_EXECUTION_VALIDATION = Symbol.for("openclaw.internalToolExecutionValidation");
|
|
5477
|
+
const MAX_CODEX_DYNAMIC_TOOL_VALIDATION_ERRORS = 4;
|
|
5478
|
+
const MAX_CODEX_DYNAMIC_TOOL_VALIDATION_ERROR_CHARS = 160;
|
|
5479
|
+
const CODEX_DYNAMIC_TOOL_VALIDATION_TRUNCATED_SUFFIX = " [detail truncated]";
|
|
5480
|
+
function shouldValidateCodexDynamicToolInput(tool) {
|
|
5481
|
+
return getPluginToolMeta(tool)?.mcp?.operation !== "tool";
|
|
5482
|
+
}
|
|
5483
|
+
function assertCodexDynamicToolInputMatchesSchema(params) {
|
|
5484
|
+
const validation = validateJsonSchemaValue({
|
|
5485
|
+
schema: params.schema,
|
|
5486
|
+
cacheKey: `codex-dynamic-tool-input:${params.toolName}:${JSON.stringify(params.schema)}`,
|
|
5487
|
+
value: params.value
|
|
5488
|
+
});
|
|
5489
|
+
if (validation.ok) return;
|
|
5490
|
+
const visibleErrors = validation.errors.slice(0, MAX_CODEX_DYNAMIC_TOOL_VALIDATION_ERRORS);
|
|
5491
|
+
const details = visibleErrors.map((error) => {
|
|
5492
|
+
if (error.text.length <= MAX_CODEX_DYNAMIC_TOOL_VALIDATION_ERROR_CHARS) return error.text;
|
|
5493
|
+
return `${error.text.slice(0, MAX_CODEX_DYNAMIC_TOOL_VALIDATION_ERROR_CHARS - 19)}${CODEX_DYNAMIC_TOOL_VALIDATION_TRUNCATED_SUFFIX}`;
|
|
5494
|
+
}).join("; ");
|
|
5495
|
+
const omitted = validation.errors.length - visibleErrors.length;
|
|
5496
|
+
const omittedSuffix = omitted > 0 ? `; ${omitted} more violation(s) omitted` : "";
|
|
5497
|
+
throw new Error(`Invalid arguments for tool "${params.toolName}": ${details}${omittedSuffix}.`);
|
|
5498
|
+
}
|
|
5499
|
+
function createCodexDynamicToolValidationControl(params) {
|
|
5500
|
+
return {
|
|
5501
|
+
[INTERNAL_TOOL_EXECUTION_VALIDATION]: true,
|
|
5502
|
+
toolCallId: params.toolCallId,
|
|
5503
|
+
validate: params.validate
|
|
5504
|
+
};
|
|
5505
|
+
}
|
|
4923
5506
|
function applyCurrentMessageProvider(toolName, args, currentProvider) {
|
|
4924
5507
|
const hasProvider = typeof args.provider === "string" && args.provider.trim().length > 0 ? true : typeof args.channel === "string" && args.channel.trim().length > 0;
|
|
4925
5508
|
const provider = currentProvider?.trim();
|
|
@@ -5050,6 +5633,8 @@ function normalizeAcceptedSessionSpawn(result) {
|
|
|
5050
5633
|
}
|
|
5051
5634
|
/** Namespace attached to OpenClaw-owned dynamic tools exposed to Codex. */
|
|
5052
5635
|
const CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE = "openclaw";
|
|
5636
|
+
const CODEX_DYNAMIC_TOOL_NAME_MAX_CHARS = 128;
|
|
5637
|
+
const CODEX_DYNAMIC_TOOL_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/u;
|
|
5053
5638
|
const ALWAYS_DIRECT_DYNAMIC_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
5054
5639
|
"agents_list",
|
|
5055
5640
|
"sessions_spawn",
|
|
@@ -5100,7 +5685,11 @@ function createCodexDynamicToolBridge(params) {
|
|
|
5100
5685
|
const toolMap = new Map(availableTools.map((entry) => [entry.name, entry]));
|
|
5101
5686
|
const registeredToolNames = new Set(registeredSpecTools.map((entry) => entry.name));
|
|
5102
5687
|
const quarantinedTools = dedupeQuarantinedDynamicTools([...availableProjection.quarantinedTools, ...registeredProjection.quarantinedTools]);
|
|
5103
|
-
warnQuarantinedDynamicTools(
|
|
5688
|
+
warnQuarantinedDynamicTools({
|
|
5689
|
+
tools: quarantinedTools,
|
|
5690
|
+
availableToolCount: availableTools.length,
|
|
5691
|
+
registeredToolCount: registeredSpecTools.length
|
|
5692
|
+
});
|
|
5104
5693
|
emitQuarantinedDynamicToolDiagnostics(quarantinedTools, params.hookContext);
|
|
5105
5694
|
const telemetry = {
|
|
5106
5695
|
didSendViaMessagingTool: false,
|
|
@@ -5140,6 +5729,10 @@ function createCodexDynamicToolBridge(params) {
|
|
|
5140
5729
|
directToolNames
|
|
5141
5730
|
}),
|
|
5142
5731
|
resultContentSourceForTool: (toolName) => toolMap.get(toolName)?.tool.resultContentSource,
|
|
5732
|
+
sideEffectOwnerKeyForTool: (toolName) => {
|
|
5733
|
+
const tool = toolMap.get(toolName)?.tool;
|
|
5734
|
+
return tool ? getPluginToolSideEffectOwnerKey(tool) : void 0;
|
|
5735
|
+
},
|
|
5143
5736
|
telemetry,
|
|
5144
5737
|
setRemoteWorkspaceFileReader: (reader) => {
|
|
5145
5738
|
readRemoteWorkspaceFile = reader;
|
|
@@ -5171,7 +5764,8 @@ function createCodexDynamicToolBridge(params) {
|
|
|
5171
5764
|
});
|
|
5172
5765
|
}
|
|
5173
5766
|
const { tool, name: toolName } = toolEntry;
|
|
5174
|
-
const
|
|
5767
|
+
const rawArguments = call.arguments;
|
|
5768
|
+
const args = asNonArrayRecord(rawArguments);
|
|
5175
5769
|
const startedAt = Date.now();
|
|
5176
5770
|
const signal = composeAbortSignals(params.signal, options?.signal);
|
|
5177
5771
|
let didStartExecution = false;
|
|
@@ -5194,7 +5788,8 @@ function createCodexDynamicToolBridge(params) {
|
|
|
5194
5788
|
};
|
|
5195
5789
|
};
|
|
5196
5790
|
try {
|
|
5197
|
-
const
|
|
5791
|
+
const prepare = tool.prepareArguments;
|
|
5792
|
+
const toolArgs = prepare ? Reflect.apply(prepare, tool, [rawArguments]) : args;
|
|
5198
5793
|
const preparedArgs = toolName === "message" && isRecord(toolArgs) ? await prepareCodexRemoteWorkspaceMessageMedia({
|
|
5199
5794
|
args: toolArgs,
|
|
5200
5795
|
localWorkspaceRoot: params.hookContext?.workspaceDir,
|
|
@@ -5214,7 +5809,20 @@ function createCodexDynamicToolBridge(params) {
|
|
|
5214
5809
|
hasRepliedRef: params.hookContext?.hasRepliedRef ? { value: params.hookContext.hasRepliedRef.value } : void 0
|
|
5215
5810
|
};
|
|
5216
5811
|
didDispatchExecution = true;
|
|
5217
|
-
const
|
|
5812
|
+
const executionArgs = [
|
|
5813
|
+
call.callId,
|
|
5814
|
+
preparedArgs,
|
|
5815
|
+
signal
|
|
5816
|
+
];
|
|
5817
|
+
if (shouldValidateCodexDynamicToolInput(tool)) executionArgs.push(createCodexDynamicToolValidationControl({
|
|
5818
|
+
toolCallId: call.callId,
|
|
5819
|
+
validate: (value) => assertCodexDynamicToolInputMatchesSchema({
|
|
5820
|
+
toolName,
|
|
5821
|
+
schema: toolEntry.inputSchema,
|
|
5822
|
+
value
|
|
5823
|
+
})
|
|
5824
|
+
}));
|
|
5825
|
+
const rawResult = await Reflect.apply(tool.execute, tool, executionArgs);
|
|
5218
5826
|
captureExecutionBoundary();
|
|
5219
5827
|
const telemetryRawResult = sanitizeToolResult(rawResult);
|
|
5220
5828
|
const rawIsError = isToolResultError(rawResult);
|
|
@@ -5282,7 +5890,7 @@ function createCodexDynamicToolBridge(params) {
|
|
|
5282
5890
|
contentItems,
|
|
5283
5891
|
success: !resultIsError
|
|
5284
5892
|
}, terminalType);
|
|
5285
|
-
withDynamicToolTranscriptDetails(response, result
|
|
5893
|
+
withDynamicToolTranscriptDetails(response, asOptionalRecord(sanitizeToolResult(result))?.details);
|
|
5286
5894
|
withDiagnosticFailureDisposition(response, resultFailureKind);
|
|
5287
5895
|
const blocksSourceReplyTermination = hasExplicitNonSourceMessageRoute(executedArgs, params.hookContext, confirmedMessagingTarget);
|
|
5288
5896
|
const deliveredSourceReply = isDeliveredMessageToolOnlySourceReplyResult({
|
|
@@ -5533,6 +6141,13 @@ function projectCodexDynamicTools(tools) {
|
|
|
5533
6141
|
});
|
|
5534
6142
|
continue;
|
|
5535
6143
|
}
|
|
6144
|
+
if (!isRecord(projection.schema)) {
|
|
6145
|
+
quarantinedTools.push({
|
|
6146
|
+
tool: descriptor.name,
|
|
6147
|
+
violations: [`${descriptor.name}.inputSchema must be a JSON object schema`]
|
|
6148
|
+
});
|
|
6149
|
+
continue;
|
|
6150
|
+
}
|
|
5536
6151
|
projectedTools.push({
|
|
5537
6152
|
tool,
|
|
5538
6153
|
name: descriptor.name,
|
|
@@ -5557,6 +6172,20 @@ function readCodexDynamicToolDescriptor(tool, toolIndex) {
|
|
|
5557
6172
|
violations: [`${fallbackName}.name must be a non-empty string`]
|
|
5558
6173
|
}
|
|
5559
6174
|
};
|
|
6175
|
+
const trimmedName = rawName.trim();
|
|
6176
|
+
let nameViolation;
|
|
6177
|
+
if (!trimmedName) nameViolation = `${rawName}.name must not be empty`;
|
|
6178
|
+
else if (trimmedName !== rawName) nameViolation = `${rawName}.name must not have leading or trailing whitespace`;
|
|
6179
|
+
else if (!CODEX_DYNAMIC_TOOL_NAME_PATTERN.test(rawName)) nameViolation = `${rawName}.name must match ^[a-zA-Z0-9_-]+$`;
|
|
6180
|
+
else if (rawName.length > CODEX_DYNAMIC_TOOL_NAME_MAX_CHARS) nameViolation = `${rawName}.name must be at most ${CODEX_DYNAMIC_TOOL_NAME_MAX_CHARS} characters`;
|
|
6181
|
+
else if (rawName === "mcp" || rawName.startsWith("mcp__")) nameViolation = `${rawName}.name is reserved by Codex app-server`;
|
|
6182
|
+
if (nameViolation) return {
|
|
6183
|
+
ok: false,
|
|
6184
|
+
diagnostic: {
|
|
6185
|
+
tool: rawName,
|
|
6186
|
+
violations: [nameViolation]
|
|
6187
|
+
}
|
|
6188
|
+
};
|
|
5560
6189
|
name = rawName;
|
|
5561
6190
|
} catch {
|
|
5562
6191
|
return {
|
|
@@ -5598,14 +6227,18 @@ function readCodexDynamicToolDescriptor(tool, toolIndex) {
|
|
|
5598
6227
|
parameters
|
|
5599
6228
|
};
|
|
5600
6229
|
}
|
|
5601
|
-
function warnQuarantinedDynamicTools(
|
|
5602
|
-
if (tools.length === 0) return;
|
|
6230
|
+
function warnQuarantinedDynamicTools(params) {
|
|
6231
|
+
if (params.tools.length === 0) return;
|
|
5603
6232
|
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
|
-
|
|
6233
|
+
for (const tool of params.tools) unique.set(tool.tool, tool.violations);
|
|
6234
|
+
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`, {
|
|
6235
|
+
tools: [...unique.entries()].map(([tool, violations]) => ({
|
|
6236
|
+
tool,
|
|
6237
|
+
violations
|
|
6238
|
+
})),
|
|
6239
|
+
availableToolCount: params.availableToolCount,
|
|
6240
|
+
registeredToolCount: params.registeredToolCount
|
|
6241
|
+
});
|
|
5609
6242
|
}
|
|
5610
6243
|
function emitQuarantinedDynamicToolDiagnostics(tools, ctx) {
|
|
5611
6244
|
for (const tool of tools) emitTrustedDiagnosticEvent({
|
|
@@ -5893,4 +6526,4 @@ function isCronAddAction(args) {
|
|
|
5893
6526
|
return typeof action === "string" && action.trim().toLowerCase() === "add";
|
|
5894
6527
|
}
|
|
5895
6528
|
//#endregion
|
|
5896
|
-
export {
|
|
6529
|
+
export { buildCodexNativeHookRelayConfig as A, isMatchingDynamicToolTerminalDiagnostic as B, shouldRequireCodexSandboxExecServerEnvironment as C, CodexNativeToolLifecycleProjector as D, readBoundedCodexRemoteWorkspaceFile as E, resolveCodexNativeHookRelayTtlMs as F, toCodexDynamicToolProgressResponse as G, resolveTerminalDynamicToolBatchAction as H, scheduleCodexNativeHookRelayUnregister as I, toCodexDynamicToolProtocolResponse as K, handleDynamicToolCallWithTimeout as L, createCodexNativeHookRelay as M, emitCodexNativePreToolUseFailureDiagnostic as N, CODEX_NATIVE_HOOK_RELAY_EVENTS as O, resolveCodexNativeHookRelayEvents as P, hasPendingDynamicToolTerminalDiagnostic as R, shouldEnableCodexAppServerNativeToolSurface as S, filterCodexVisionTools as T, shouldBlockTerminalReleaseForNonTerminalDynamicToolResult as U, resolveDynamicToolCallTimeoutMs as V, shouldReleaseTurnAfterTerminalDynamicTool as W, 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, buildCodexNativeHookRelayDisabledConfig as j, CODEX_NATIVE_HOOK_RELAY_TTL_GRACE_MS as k, resolveCodexProviderWebSearchSupportForClient as l, disableCodexPluginThreadConfig as m, projectCodexExecutableDynamicTools as n, emitDynamicToolTerminalDiagnostic as o, createCodexDynamicToolBuildStageTracker as p, resolveCodexToolAbortTerminalReason as q, routeCodexAppServerElicitationRequest as r, handleCodexAppServerApprovalRequest as s, createCodexDynamicToolBridge as t, ensureCodexSandboxExecServerEnvironment as u, resolveCodexExternalSandboxPolicyForOpenClawSandbox as v, shouldWarnCodexDynamicToolBuildStageSummary as w, resolveCodexSandboxEnvironmentSelection as x, resolveCodexMessageToolProvider as y, isDynamicToolTerminalDiagnosticEvent as z };
|