@openclaw/codex 2026.7.2-beta.7 → 2026.8.1-beta.2
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/README.md +2 -0
- package/dist/{app-server-policy-BSaO2JIT.js → app-server-policy-zcBDc1JU.js} +1 -1
- package/dist/attempt-client-cleanup-DMLXUfa8.js +735 -0
- package/dist/{notification-correlation-CU6_AnOb.js → binding-connection-I5Bo-AsX.js} +3 -37
- package/dist/{bounded-turn-D2ZAgZVz.js → bounded-turn-qgZmzy_4.js} +50 -49
- package/dist/{capabilities-BFDTVYr4.js → capabilities-CybGI1oz.js} +8 -2
- package/dist/cli-metadata.js +1 -1
- package/dist/{command-formatters-C336eRVS.js → command-formatters-B1AEz8ZA.js} +18 -18
- package/dist/{command-handlers-DAHUSry9.js → command-handlers-Db8pu_ds.js} +484 -95
- package/dist/{compact-D_uvHurS.js → compact-LspQaGfl.js} +213 -199
- package/dist/{conversation-binding-data-BHRpYbX3.js → conversation-binding-data-CW2GuupV.js} +6 -10
- package/dist/doctor-contract-api.js +40 -43
- package/dist/{dynamic-tools-D2gYkSBO.js → dynamic-tools-BVdyWkUD.js} +2597 -1221
- package/dist/{effective-mcp-catalog-Bbd98Inw.js → effective-mcp-catalog-DIIhAtOZ.js} +20 -15
- package/dist/{event-projector-assistant-message-DTgnPPVF.js → event-projector-assistant-message-BwxV4d6N.js} +11 -23
- package/dist/{attempt-notifications-AP-a-Q5k.js → event-projector-usage-e5huIQWu.js} +113 -29
- package/dist/harness.js +80 -14
- package/dist/image-payload-sanitizer-CnfoUPCE.js +65 -0
- package/dist/index.js +591 -240
- package/dist/isolated-completion-C58NbL40.js +75 -0
- package/dist/local-runtime-attribution-B_2mnqhb.js +19 -0
- package/dist/media-understanding-provider.js +5 -5
- package/dist/{models-BBMKU4jA.js → models-BFlpn1Ex.js} +12 -17
- package/dist/native-subagent-monitor-DyTGmTUx.js +3111 -0
- package/dist/{plugin-app-cache-key-DztPafOA.js → plugin-app-cache-key-C3H2CVp6.js} +5 -5
- package/dist/plugin-marketplace-discovery-CzrGJdCR.js +385 -0
- package/dist/{computer-use-CYEc8U3w.js → plugin-metadata-cache-CNJPj4_b.js} +105 -5
- package/dist/protocol-validators-B4F7L8Rf.js +2346 -0
- package/dist/{rate-limits-CU-gVCCS.js → rate-limits-DZXmOL8-.js} +17 -35
- package/dist/{request-qKh331yu.js → request-BRl5_UfX.js} +3 -3
- package/dist/{run-attempt-CY9WlDLc.js → run-attempt-P8BdxC1I.js} +4837 -9047
- package/dist/{runtime-artifact-CmtG_t1L.js → runtime-artifact-BVSPdd6g.js} +8 -23
- package/dist/{sandbox-guard-NuhpXmPB.js → sandbox-guard-Cmwc9eIk.js} +6 -13
- package/dist/{session-binding-BXpo9ymA.js → session-binding-DPxIvMgE.js} +246 -265
- package/dist/{session-catalog-DyOgR4WH.js → session-catalog-xwNi_SDM.js} +159 -109
- package/dist/{session-cli-2PZaFLRh.js → session-cli-7hgXZDV7.js} +1 -1
- package/dist/session-retirement-DoMMJFKm.js +36 -0
- package/dist/{session-upstream-marker-B6U7tLw5.js → session-upstream-marker-CrIFX06J.js} +1 -1
- package/dist/{settled-turn-finalizer-CXY2Z9dT.js → settled-turn-finalizer-BRWBO4MO.js} +21 -13
- package/dist/{shared-client-DTbvH-XV.js → shared-client-kIBbGjk4.js} +587 -223
- package/dist/{side-question-_ulBk1AL.js → side-question-9HAL26l9.js} +101 -52
- package/dist/{thread-lifecycle-DF53ogiA.js → thread-lifecycle-Bu1BTDXr.js} +694 -753
- package/dist/thread-ownership-6kOQHlyC.js +92 -0
- package/dist/thread-resume-CPrJNQVM.js +30 -0
- package/dist/transcript-mirror-B9gGgnJS.js +570 -0
- package/dist/{transport-stdio-DXgGZ7Ib.js → transport-stdio-DIImwDVS.js} +11 -0
- package/dist/upstream-prompt-provenance-ClRBije0.js +41 -0
- package/dist/{upstream-session-fork-Dy4Xvuoj.js → upstream-session-fork-CE-_LdnP.js} +5 -5
- package/dist/{usage-CQ6GhD5-.js → usage-CHlV0e-o.js} +3 -3
- package/dist/{web-search-provider.runtime-CCFhg2z5.js → web-search-provider.runtime-DRw2Vl42.js} +8 -9
- package/openclaw.plugin.json +18 -3
- package/package.json +5 -5
- package/dist/conversation-control-xsIeXbo4.js +0 -287
- package/dist/event-projector-usage-Xp4AlR2k.js +0 -86
- package/dist/transcript-mirror-DzsdNCf5.js +0 -6924
|
@@ -1,136 +1,35 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
1
|
+
import { C as isCodexRemoteExecPlacementSandbox, T as readCodexPluginConfig } from "./session-binding-DPxIvMgE.js";
|
|
2
|
+
import { S as resolveCodexGatewayTimeoutWithGraceMs, ct as isJsonObject, l as releaseLeasedSharedCodexAppServerClient, ot as CODEX_OPENCLAW_DIRECT_DYNAMIC_TOOL_NAMESPACE } from "./shared-client-kIBbGjk4.js";
|
|
3
|
+
import { E as itemStatus, F as readItem, b as auditNativeToolUnfinishedStatus, h as isCodexNotificationForTurn, v as auditNativeToolName, y as auditNativeToolTerminalStatus } from "./attempt-client-cleanup-DMLXUfa8.js";
|
|
4
|
+
import { l as readCodexTurn } from "./protocol-validators-B4F7L8Rf.js";
|
|
5
|
+
import { D as isSystemAgentOnlyCodexDynamicToolAllowlist, E as isForcedPrivateQaCodexRuntime, O as normalizeCodexDynamicToolName, T as filterCodexDynamicToolsForDisabledNativeSurface, f as resolveCodexWebSearchPlan, w as filterCodexDynamicTools } from "./thread-lifecycle-Bu1BTDXr.js";
|
|
6
|
+
import { r as sanitizeInlineImageDataUrl, t as invalidInlineImageText } from "./image-payload-sanitizer-CnfoUPCE.js";
|
|
7
|
+
import { r as formatCodexDisplayText } from "./command-formatters-B1AEz8ZA.js";
|
|
8
|
+
import { a as resolveCodexNodeExecToolOverrides, i as resolveCodexNativeExecutionPolicy } from "./sandbox-guard-Cmwc9eIk.js";
|
|
9
|
+
import { asNonArrayRecord, asOptionalRecord, isRecord, normalizeOptionalString, normalizeTrimmedStringList, readStringField } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
10
|
+
import path, { posix } from "node:path";
|
|
11
|
+
import { createDeferred } from "openclaw/plugin-sdk/extension-shared";
|
|
9
12
|
import { addTimerTimeoutGraceMs, asDateTimestampMs, finiteSecondsToTimerSafeMilliseconds, parseStrictNonNegativeInteger } from "openclaw/plugin-sdk/number-runtime";
|
|
10
|
-
import { HEARTBEAT_RESPONSE_TOOL_NAME, buildAgentHookContextChannelFields, buildEmbeddedAttemptToolRunContext,
|
|
11
|
-
import { createHash } from "node:crypto";
|
|
13
|
+
import { HEARTBEAT_RESPONSE_TOOL_NAME, buildAgentHookContextChannelFields, buildEmbeddedAttemptToolRunContext, consumeAdjustedParamsForToolCall, consumePreExecutionBlockedToolCall, createAgentToolResultMiddlewareRunner, createCodexAppServerToolResultExtensionRunner, embeddedAgentLog, extractMessagingToolSend, extractMessagingToolSendResult, extractToolResultMediaArtifact, filterProviderNormalizableTools, filterToolResultMediaUrls, finalizeToolTerminalPresentation, formatApprovalDisplayPath, formatToolExecutionErrorMessage, getBeforeToolCallFailureDisposition, getChannelAgentToolMeta, getPluginToolMeta, hasNativeHookRelayInvocation, invokeNativeHookRelay, isDeliveredMessageToolOnlySourceReplyResult, isDeliveredMessagingToolResult, isHostScopedAgentToolActive, isMessagingTool, isMessagingToolSendAction, isReplaySafeToolCall, isSubagentSessionKey, isToolResultError, isToolWrappedWithBeforeToolCallHook, normalizeAgentRuntimeTools, normalizeHeartbeatToolResponse, projectRuntimeToolInputSchema, resolveAttemptSpawnWorkspaceDir, resolveModelAuthMode, resolveNativeHookRelayDeferredToolApproval, resolveToolExecutionErrorKind, resolveToolResultFailureKind, runAgentHarnessAfterToolCallHook, sanitizeToolResult, setBeforeToolCallDiagnosticsEnabled, supportsModelTools, wrapToolWithBeforeToolCallHook } from "openclaw/plugin-sdk/agent-harness-runtime";
|
|
14
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
12
15
|
import { resolveAgentDir as resolveAgentDir$1 } from "openclaw/plugin-sdk/agent-runtime";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
16
|
+
import { SsrFBlockedError, isBlockedHostnameOrIp } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
17
|
+
import { spawn } from "node:child_process";
|
|
18
|
+
import { coerceErrorMessage, isApprovalNotFoundError, toErrorObject } from "openclaw/plugin-sdk/error-runtime";
|
|
19
|
+
import { DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS, estimateToolResultTextChars, resolveLiveToolResultMaxChars, sliceToolResultTextToBudget, sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
20
|
+
import { once } from "node:events";
|
|
21
|
+
import { isIP } from "node:net";
|
|
22
|
+
import { WebSocketServer } from "ws";
|
|
23
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
15
24
|
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
|
25
|
+
import { runWithCronCreatorAuthorityCapabilityResolver } from "openclaw/plugin-sdk/codex-mcp-projection";
|
|
16
26
|
import { emitTrustedDiagnosticEvent, hasPendingInternalDiagnosticEvent } from "openclaw/plugin-sdk/diagnostic-runtime";
|
|
17
|
-
import { isToolAllowed } from "openclaw/plugin-sdk/sandbox";
|
|
27
|
+
import { isToolAllowed, sanitizeEnvVars } from "openclaw/plugin-sdk/sandbox";
|
|
18
28
|
import { saveMediaBuffer } from "openclaw/plugin-sdk/media-store";
|
|
29
|
+
import { registerRetainedNativeHookRelayForBundledRuntime } from "openclaw/plugin-sdk/native-hook-relay-runtime";
|
|
19
30
|
import { root } from "openclaw/plugin-sdk/file-access-runtime";
|
|
20
31
|
import { getMediaDir } from "openclaw/plugin-sdk/media-runtime";
|
|
21
|
-
import { isApprovalNotFoundError } from "openclaw/plugin-sdk/error-runtime";
|
|
22
32
|
import { normalizeOpenAIToolSchemas } from "openclaw/plugin-sdk/provider-tools";
|
|
23
|
-
//#region extensions/codex/src/app-server/event-projector-items.ts
|
|
24
|
-
function itemKind(item) {
|
|
25
|
-
switch (item.type) {
|
|
26
|
-
case "dynamicToolCall":
|
|
27
|
-
case "mcpToolCall": return "tool";
|
|
28
|
-
case "commandExecution": return "command";
|
|
29
|
-
case "fileChange": return "patch";
|
|
30
|
-
case "webSearch": return "search";
|
|
31
|
-
case "reasoning":
|
|
32
|
-
case "contextCompaction": return "analysis";
|
|
33
|
-
default: return;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
function itemTitle(item) {
|
|
37
|
-
switch (item.type) {
|
|
38
|
-
case "commandExecution": return "Command";
|
|
39
|
-
case "fileChange": return "File change";
|
|
40
|
-
case "mcpToolCall": return "MCP tool";
|
|
41
|
-
case "dynamicToolCall": return "Tool";
|
|
42
|
-
case "webSearch": return "Web search";
|
|
43
|
-
case "contextCompaction": return "Context compaction";
|
|
44
|
-
case "reasoning": return "Reasoning";
|
|
45
|
-
default: return item.type;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
function itemStatus(item) {
|
|
49
|
-
const status = readItemString(item, "status");
|
|
50
|
-
if (status === "failed" || status === "error") return "failed";
|
|
51
|
-
if (status === "declined") return "blocked";
|
|
52
|
-
if (status === "inProgress" || status === "in_progress" || status === "running") return "running";
|
|
53
|
-
return "completed";
|
|
54
|
-
}
|
|
55
|
-
function unknownItemStatus(item) {
|
|
56
|
-
const status = readItemString(item, "status");
|
|
57
|
-
switch (status) {
|
|
58
|
-
case void 0:
|
|
59
|
-
case "completed":
|
|
60
|
-
case "failed":
|
|
61
|
-
case "error":
|
|
62
|
-
case "declined":
|
|
63
|
-
case "inProgress":
|
|
64
|
-
case "in_progress":
|
|
65
|
-
case "running": return;
|
|
66
|
-
default: return status;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
function auditNativeToolTerminalStatus(item) {
|
|
70
|
-
if (item.type === "imageView" || item.type === "sleep") return "completed";
|
|
71
|
-
const status = readItemString(item, "status");
|
|
72
|
-
if (status === "completed") return "completed";
|
|
73
|
-
if (status === "failed" || status === "error") return "failed";
|
|
74
|
-
if (status === "declined") return "blocked";
|
|
75
|
-
return "unknown";
|
|
76
|
-
}
|
|
77
|
-
function auditNativeToolUnfinishedStatus(item) {
|
|
78
|
-
return item.type === "webSearch" || item.type === "imageGeneration" ? "unknown" : "failed";
|
|
79
|
-
}
|
|
80
|
-
function isNonSuccessItemStatus(status) {
|
|
81
|
-
return status === "failed" || status === "blocked";
|
|
82
|
-
}
|
|
83
|
-
function itemName(item) {
|
|
84
|
-
if (item.type === "dynamicToolCall" && typeof item.tool === "string") return item.tool;
|
|
85
|
-
if (item.type === "mcpToolCall" && typeof item.tool === "string") {
|
|
86
|
-
const server = typeof item.server === "string" ? item.server : void 0;
|
|
87
|
-
return server ? `${server}.${item.tool}` : item.tool;
|
|
88
|
-
}
|
|
89
|
-
if (item.type === "commandExecution") return "bash";
|
|
90
|
-
if (item.type === "fileChange") return "apply_patch";
|
|
91
|
-
if (item.type === "webSearch") return "web_search";
|
|
92
|
-
}
|
|
93
|
-
function auditNativeToolName(item) {
|
|
94
|
-
if (item.type === "dynamicToolCall") return;
|
|
95
|
-
const progressName = itemName(item);
|
|
96
|
-
if (progressName) return progressName;
|
|
97
|
-
if (item.type === "collabAgentToolCall") return typeof item.tool === "string" && item.tool.trim() ? `collab.${item.tool.trim()}` : "collab_agent";
|
|
98
|
-
if (item.type === "imageGeneration") return "image_generation";
|
|
99
|
-
if (item.type === "imageView") return "image_view";
|
|
100
|
-
if (item.type === "sleep") return "sleep";
|
|
101
|
-
}
|
|
102
|
-
function isSideEffectingNativeToolItem(item) {
|
|
103
|
-
return itemStatus(item) !== "blocked" && (isMutatingNativeToolItem(item) || item.type === "mcpToolCall");
|
|
104
|
-
}
|
|
105
|
-
function shouldSynthesizeToolProgressForItem(item) {
|
|
106
|
-
switch (item.type) {
|
|
107
|
-
case "commandExecution":
|
|
108
|
-
case "fileChange":
|
|
109
|
-
case "webSearch":
|
|
110
|
-
case "mcpToolCall": return true;
|
|
111
|
-
default: return false;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
function shouldRecordNativeToolTranscript(item) {
|
|
115
|
-
return shouldSynthesizeToolProgressForItem(item);
|
|
116
|
-
}
|
|
117
|
-
function isMutatingNativeToolItem(item) {
|
|
118
|
-
if (item.type === "commandExecution") return true;
|
|
119
|
-
return item.type === "fileChange" || item.type === "collabAgentToolCall" || item.type === "imageGeneration";
|
|
120
|
-
}
|
|
121
|
-
function shouldClearTerminalPresentationForNativeItem(item) {
|
|
122
|
-
switch (item.type) {
|
|
123
|
-
case "collabAgentToolCall":
|
|
124
|
-
case "commandExecution":
|
|
125
|
-
case "fileChange":
|
|
126
|
-
case "imageGeneration":
|
|
127
|
-
case "imageView":
|
|
128
|
-
case "mcpToolCall":
|
|
129
|
-
case "webSearch": return true;
|
|
130
|
-
default: return false;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
//#endregion
|
|
134
33
|
//#region extensions/codex/src/app-server/dynamic-tool-response-state.ts
|
|
135
34
|
/** Retains the host-owned app preview without adding it to Codex's response payload. */
|
|
136
35
|
function withDynamicToolTranscriptDetails(response, details) {
|
|
@@ -608,6 +507,7 @@ const CODEX_NATIVE_HOOK_RELAY_COMMAND_MAX_PARENT_MARGIN_MS = 1e3;
|
|
|
608
507
|
const CODEX_NATIVE_HOOK_RELAY_DEFAULT_TIMEOUT_SEC = 10;
|
|
609
508
|
const CODEX_NATIVE_HOOK_RELAY_UNREGISTER_GRACE_MS = 1e4;
|
|
610
509
|
const CODEX_NATIVE_HOOK_RELAY_UNREGISTER_EXTRA_GRACE_MS = 5e3;
|
|
510
|
+
const MAX_PENDING_DIRECT_CHILD_ADMISSIONS = 32;
|
|
611
511
|
const CODEX_HOOK_MATCHER_NAMES_BY_TOOL_ID = {
|
|
612
512
|
exec: [
|
|
613
513
|
"Bash",
|
|
@@ -663,7 +563,16 @@ function emitCodexNativePreToolUseFailureDiagnostic(params) {
|
|
|
663
563
|
/** Registers an OpenClaw native hook relay for a Codex app-server turn. */
|
|
664
564
|
function createCodexNativeHookRelay(params) {
|
|
665
565
|
if (params.options?.enabled === false) return;
|
|
666
|
-
|
|
566
|
+
const directChildClaims = /* @__PURE__ */ new Map();
|
|
567
|
+
const pendingDirectChildAdmissions = /* @__PURE__ */ new Map();
|
|
568
|
+
let foregroundClosed = false;
|
|
569
|
+
let successfulYieldRetentionAuthorized = false;
|
|
570
|
+
const assertClaim = (threadId, claim) => () => directChildClaims.get(threadId) === claim;
|
|
571
|
+
const rejectPendingAdmissions = (reason) => {
|
|
572
|
+
for (const pending of pendingDirectChildAdmissions.values()) pending.reject(new Error(reason));
|
|
573
|
+
pendingDirectChildAdmissions.clear();
|
|
574
|
+
};
|
|
575
|
+
const relay = registerRetainedNativeHookRelayForBundledRuntime({
|
|
667
576
|
provider: "codex",
|
|
668
577
|
relayId: buildCodexNativeHookRelayId({
|
|
669
578
|
agentId: params.agentId,
|
|
@@ -689,12 +598,79 @@ function createCodexNativeHookRelay(params) {
|
|
|
689
598
|
turnStartTimeoutMs: params.turnStartTimeoutMs
|
|
690
599
|
}),
|
|
691
600
|
signal: params.signal,
|
|
601
|
+
runBeforeToolCall: params.hostCapabilities.runBeforeToolCall,
|
|
602
|
+
assertActive: params.hostCapabilities.assertActive,
|
|
603
|
+
retention: {
|
|
604
|
+
readClaim: readCodexNativeChildThreadId,
|
|
605
|
+
shouldRetainAfterForegroundClose: () => successfulYieldRetentionAuthorized && directChildClaims.size > 0,
|
|
606
|
+
allowPreToolUse: (childThreadId) => directChildClaims.has(childThreadId),
|
|
607
|
+
awaitForegroundAdmission: (childThreadId) => {
|
|
608
|
+
if (foregroundClosed) return Promise.reject(/* @__PURE__ */ new Error("native hook relay foreground admission unavailable"));
|
|
609
|
+
const existingClaim = directChildClaims.get(childThreadId);
|
|
610
|
+
if (existingClaim) return Promise.resolve(assertClaim(childThreadId, existingClaim));
|
|
611
|
+
const existingPending = pendingDirectChildAdmissions.get(childThreadId);
|
|
612
|
+
if (existingPending) return existingPending.promise.then((claim) => assertClaim(childThreadId, claim));
|
|
613
|
+
if (pendingDirectChildAdmissions.size >= MAX_PENDING_DIRECT_CHILD_ADMISSIONS) return Promise.reject(/* @__PURE__ */ new Error("native hook relay foreground admission capacity reached"));
|
|
614
|
+
const { promise, resolve, reject } = createDeferred();
|
|
615
|
+
pendingDirectChildAdmissions.set(childThreadId, {
|
|
616
|
+
promise,
|
|
617
|
+
resolve,
|
|
618
|
+
reject
|
|
619
|
+
});
|
|
620
|
+
return promise.then((claim) => assertClaim(childThreadId, claim));
|
|
621
|
+
},
|
|
622
|
+
onDispose: () => {
|
|
623
|
+
foregroundClosed = true;
|
|
624
|
+
rejectPendingAdmissions("native hook relay registration closed");
|
|
625
|
+
}
|
|
626
|
+
},
|
|
692
627
|
onPreToolUseFailure: params.onPreToolUseFailure,
|
|
693
628
|
command: {
|
|
694
629
|
nice: 10,
|
|
695
630
|
timeoutMs: params.options?.gatewayTimeoutMs
|
|
696
631
|
}
|
|
697
632
|
});
|
|
633
|
+
const unregister = () => {
|
|
634
|
+
foregroundClosed = true;
|
|
635
|
+
rejectPendingAdmissions("native hook relay foreground closed");
|
|
636
|
+
relay.unregister();
|
|
637
|
+
};
|
|
638
|
+
return {
|
|
639
|
+
...relay,
|
|
640
|
+
unregister,
|
|
641
|
+
authorizeRetentionAfterSuccessfulYield: () => {
|
|
642
|
+
successfulYieldRetentionAuthorized = true;
|
|
643
|
+
},
|
|
644
|
+
rejectPendingDirectChild: (threadIdInput, reason) => {
|
|
645
|
+
const threadId = threadIdInput.trim();
|
|
646
|
+
const pending = threadId ? pendingDirectChildAdmissions.get(threadId) : void 0;
|
|
647
|
+
if (!pending) return;
|
|
648
|
+
pendingDirectChildAdmissions.delete(threadId);
|
|
649
|
+
pending.reject(new Error(reason));
|
|
650
|
+
},
|
|
651
|
+
claimDirectChild: (threadIdInput) => {
|
|
652
|
+
const threadId = threadIdInput.trim();
|
|
653
|
+
if (!threadId) return () => void 0;
|
|
654
|
+
if (directChildClaims.get(threadId)) return () => void 0;
|
|
655
|
+
const claim = Symbol(threadId);
|
|
656
|
+
directChildClaims.set(threadId, claim);
|
|
657
|
+
const pending = pendingDirectChildAdmissions.get(threadId);
|
|
658
|
+
pendingDirectChildAdmissions.delete(threadId);
|
|
659
|
+
pending?.resolve(claim);
|
|
660
|
+
let released = false;
|
|
661
|
+
return () => {
|
|
662
|
+
if (released) return;
|
|
663
|
+
released = true;
|
|
664
|
+
if (directChildClaims.get(threadId) !== claim) return;
|
|
665
|
+
directChildClaims.delete(threadId);
|
|
666
|
+
if (foregroundClosed && directChildClaims.size === 0) relay.unregister();
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
function readCodexNativeChildThreadId(rawPayload) {
|
|
672
|
+
if (!isJsonObject(rawPayload) || typeof rawPayload.agent_id !== "string") return;
|
|
673
|
+
return rawPayload.agent_id.trim() || void 0;
|
|
698
674
|
}
|
|
699
675
|
/** Selects the native hook events Codex should install for the current approval mode. */
|
|
700
676
|
function resolveCodexNativeHookRelayEvents(params) {
|
|
@@ -848,7 +824,7 @@ var CodexNativeToolLifecycleProjector = class {
|
|
|
848
824
|
}
|
|
849
825
|
handleNotification(notification) {
|
|
850
826
|
const params = isJsonObject(notification.params) ? notification.params : void 0;
|
|
851
|
-
if (!params ||
|
|
827
|
+
if (!params || !isCodexNotificationForTurn(params, this.threadId, this.turnId)) return;
|
|
852
828
|
if (notification.method === "turn/completed") {
|
|
853
829
|
const turn = readCodexTurn(params.turn);
|
|
854
830
|
if (!turn || turn.id !== this.turnId) return;
|
|
@@ -906,12 +882,12 @@ var CodexNativeToolLifecycleProjector = class {
|
|
|
906
882
|
this.preToolUseFailureByItem.set(failure.toolCallId, record);
|
|
907
883
|
}
|
|
908
884
|
recordRawWebSearchResult(item) {
|
|
909
|
-
if (
|
|
910
|
-
const toolCallId =
|
|
885
|
+
if (readStringField(item, "type") !== "web_search_call") return;
|
|
886
|
+
const toolCallId = readStringField(item, "id");
|
|
911
887
|
if (!toolCallId || this.completedItemIds.has(toolCallId)) return;
|
|
912
888
|
const toolName = "web_search";
|
|
913
889
|
this.recordStarted(toolCallId, toolName, "unknown");
|
|
914
|
-
const rawStatus =
|
|
890
|
+
const rawStatus = readStringField(item, "status");
|
|
915
891
|
if (rawStatus === "in_progress" || rawStatus === "running") return;
|
|
916
892
|
const status = rawStatus === "completed" ? "completed" : rawStatus === "cancelled" ? "cancelled" : rawStatus === "failed" || rawStatus === "error" || rawStatus === "incomplete" ? "failed" : "unknown";
|
|
917
893
|
this.recordTerminal(toolCallId, toolName, status, { sourceTimestampMs: this.webSearchCompletionByItem.get(toolCallId)?.sourceTimestampMs });
|
|
@@ -1344,37 +1320,6 @@ function mapMessageMediaValues(args, mapValue) {
|
|
|
1344
1320
|
/** Mutable dependency seam shared by dynamic-tool construction and its behavioral tests. */
|
|
1345
1321
|
const dynamicToolBuildState = {};
|
|
1346
1322
|
//#endregion
|
|
1347
|
-
//#region extensions/codex/src/app-server/message-tool-final-control.ts
|
|
1348
|
-
/**
|
|
1349
|
-
* `final` is a Codex-only control for message-tool-only source delivery. Keep
|
|
1350
|
-
* it on the projected Codex schema so other agent runtimes never receive an
|
|
1351
|
-
* API contract they do not implement.
|
|
1352
|
-
*/
|
|
1353
|
-
function addCodexMessageToolOnlyFinalControl(tools, sourceReplyDeliveryMode) {
|
|
1354
|
-
if (sourceReplyDeliveryMode !== "message_tool_only") return tools;
|
|
1355
|
-
for (const tool of tools) if (normalizeCodexDynamicToolName(tool.name) === "message") {
|
|
1356
|
-
const mutableTool = tool;
|
|
1357
|
-
mutableTool.parameters = addCodexMessageToolOnlyFinalParameter(mutableTool.parameters);
|
|
1358
|
-
}
|
|
1359
|
-
return tools;
|
|
1360
|
-
}
|
|
1361
|
-
function addCodexMessageToolOnlyFinalParameter(parameters) {
|
|
1362
|
-
if (!parameters || typeof parameters !== "object" || Array.isArray(parameters)) return parameters;
|
|
1363
|
-
const schema = parameters;
|
|
1364
|
-
const rawProperties = schema.properties;
|
|
1365
|
-
if (!rawProperties || typeof rawProperties !== "object" || Array.isArray(rawProperties)) return parameters;
|
|
1366
|
-
return {
|
|
1367
|
-
...schema,
|
|
1368
|
-
properties: {
|
|
1369
|
-
...rawProperties,
|
|
1370
|
-
final: {
|
|
1371
|
-
type: "boolean",
|
|
1372
|
-
description: "Set false for progress or true to complete the current source reply. If omitted, OpenClaw continues and resolves the latest omitted source reply when the turn ends."
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1375
|
-
};
|
|
1376
|
-
}
|
|
1377
|
-
//#endregion
|
|
1378
1323
|
//#region extensions/codex/src/app-server/shell-dynamic-tools.ts
|
|
1379
1324
|
const CODEX_NODE_EXEC_DYNAMIC_TOOL_NAME = "node_exec";
|
|
1380
1325
|
const CODEX_NODE_PROCESS_DYNAMIC_TOOL_NAME = "node_process";
|
|
@@ -1556,7 +1501,7 @@ async function buildDynamicTools(input) {
|
|
|
1556
1501
|
toolBuildStages.mark("load-agent-harness-tools");
|
|
1557
1502
|
const sessionKeys = resolveOpenClawCodingToolsSessionKeys(params, input.sandboxSessionKey);
|
|
1558
1503
|
const nativeExecutionPolicy = resolveCodexNativeExecutionPolicyForDynamicTools(input);
|
|
1559
|
-
const
|
|
1504
|
+
const buildOpenClawCodingTools = () => params.hostCapabilities.bindToolSurface(createOpenClawCodingTools({
|
|
1560
1505
|
agentId: input.sessionAgentId,
|
|
1561
1506
|
...buildEmbeddedAttemptToolRunContext(params),
|
|
1562
1507
|
exec: {
|
|
@@ -1622,6 +1567,7 @@ async function buildDynamicTools(input) {
|
|
|
1622
1567
|
computerContextEpoch: input.computerContextEpoch,
|
|
1623
1568
|
requireExplicitMessageTarget: params.requireExplicitMessageTarget ?? isSubagentSessionKey(params.sessionKey),
|
|
1624
1569
|
sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
|
|
1570
|
+
taskSuggestionDeliveryMode: params.taskSuggestionDeliveryMode,
|
|
1625
1571
|
disableMessageTool: input.ignoreDisableMessageTool ? false : params.disableMessageTool,
|
|
1626
1572
|
forceMessageTool: shouldForceMessageTool(messagePolicyParams),
|
|
1627
1573
|
enableHeartbeatTool: params.trigger === "heartbeat" || input.forceHeartbeatTool === true,
|
|
@@ -1640,11 +1586,21 @@ async function buildDynamicTools(input) {
|
|
|
1640
1586
|
toolBuildStages.mark(name);
|
|
1641
1587
|
},
|
|
1642
1588
|
onToolOutcome: params.onToolOutcome,
|
|
1643
|
-
|
|
1644
|
-
|
|
1589
|
+
isTurnTainted: params.isTurnTainted,
|
|
1590
|
+
allocateToolOutcomeOrdinal: params.allocateToolOutcomeOrdinal,
|
|
1591
|
+
cronCreatorToolAllowlistRef: input.cronCreatorToolAllowlistRef,
|
|
1592
|
+
cronCreatorToolAllowlistCaptureRef: input.cronCreatorToolAllowlistCaptureRef,
|
|
1593
|
+
cronCreatorAuthorityUnavailableReason: input.cronCreatorAuthorityUnavailableReason
|
|
1594
|
+
}), { cwd: input.effectiveCwd ?? input.effectiveWorkspace });
|
|
1595
|
+
const allTools = input.resolveCronCreatorToolAuthority ? runWithCronCreatorAuthorityCapabilityResolver({
|
|
1596
|
+
capability: params.cronCreatorAuthorityCapability,
|
|
1597
|
+
runId: params.runId,
|
|
1598
|
+
resolve: input.resolveCronCreatorToolAuthority,
|
|
1599
|
+
run: buildOpenClawCodingTools
|
|
1600
|
+
}) : buildOpenClawCodingTools();
|
|
1645
1601
|
toolBuildStages.mark("create-openclaw-coding-tools");
|
|
1646
1602
|
const preNormalizationDiagnostics = [];
|
|
1647
|
-
const readableAllToolProjection = filterProviderNormalizableTools(
|
|
1603
|
+
const readableAllToolProjection = filterProviderNormalizableTools(allTools);
|
|
1648
1604
|
preNormalizationDiagnostics.push(...readableAllToolProjection.diagnostics);
|
|
1649
1605
|
const webSearchPlan = resolveCodexWebSearchPlan({
|
|
1650
1606
|
config: params.config,
|
|
@@ -1653,7 +1609,7 @@ async function buildDynamicTools(input) {
|
|
|
1653
1609
|
nativeProviderWebSearchSupport: input.nativeProviderWebSearchSupport
|
|
1654
1610
|
});
|
|
1655
1611
|
const readableAllTools = [...readableAllToolProjection.tools];
|
|
1656
|
-
const normallyProfiledTools =
|
|
1612
|
+
const normallyProfiledTools = input.nativeToolSurfaceEnabled === false ? filterCodexDynamicToolsForDisabledNativeSurface(readableAllTools, input.pluginConfig, { preserveShell: shouldKeepOpenClawShellDynamicTools(input, nativeExecutionPolicy) }) : filterCodexDynamicTools(readableAllTools, input.pluginConfig);
|
|
1657
1613
|
const profileFilteredTools = (input.isHostScopedToolActive?.("openclaw") ?? isHostScopedAgentToolActive("openclaw")) && isSystemAgentOnlyCodexDynamicToolAllowlist(params.toolsAllow) ? preserveRingZeroSystemAgentTool(readableAllTools, normallyProfiledTools) : normallyProfiledTools;
|
|
1658
1614
|
const codexFilteredTools = addNodeShellDynamicToolsIfNeeded(addSandboxShellDynamicToolsIfAvailable(isCodexMemoryFlushRun(params) ? filterCodexMemoryFlushDynamicTools(readableAllTools) : profileFilteredTools, readableAllTools, input), readableAllTools, input, nativeExecutionPolicy);
|
|
1659
1615
|
toolBuildStages.mark("codex-filtering");
|
|
@@ -1702,6 +1658,7 @@ async function buildDynamicTools(input) {
|
|
|
1702
1658
|
modelId: params.modelId,
|
|
1703
1659
|
modelApi: params.model.api,
|
|
1704
1660
|
model: params.model,
|
|
1661
|
+
allowProviderRuntimePluginLoad: input.ignoreRuntimePlan ? false : void 0,
|
|
1705
1662
|
onPreNormalizationSchemaDiagnostics: (diagnostics) => preNormalizationDiagnostics.push(...diagnostics)
|
|
1706
1663
|
});
|
|
1707
1664
|
toolBuildStages.mark("runtime-normalization");
|
|
@@ -1750,6 +1707,7 @@ function includeForcedCodexDynamicToolAllow(toolsAllow, params) {
|
|
|
1750
1707
|
}
|
|
1751
1708
|
/** Decides whether Codex native code mode can own shell/file tools for this turn. */
|
|
1752
1709
|
function shouldEnableCodexAppServerNativeToolSurface(params, sandbox, options = {}) {
|
|
1710
|
+
if (params.pluginHarnessToolPolicyRestricted === true) return false;
|
|
1753
1711
|
if (isCodexMemoryFlushRun(params)) return false;
|
|
1754
1712
|
if (params.disableTools) return false;
|
|
1755
1713
|
const toolsAllow = includeForcedCodexDynamicToolAllow(params.toolsAllow, params);
|
|
@@ -1787,11 +1745,11 @@ function filterCodexMemoryFlushDynamicTools(tools) {
|
|
|
1787
1745
|
}
|
|
1788
1746
|
/** Requires a Codex sandbox environment only when native tools must run inside OpenClaw sandboxing. */
|
|
1789
1747
|
function shouldRequireCodexSandboxExecServerEnvironment(params) {
|
|
1790
|
-
return Boolean(params.sandbox?.enabled && params.nativeToolSurfaceEnabled && params.sandboxExecServerEnabled);
|
|
1748
|
+
return Boolean(isCodexRemoteExecPlacementSandbox(params.sandbox) || params.sandbox?.enabled && params.nativeToolSurfaceEnabled && params.sandboxExecServerEnabled);
|
|
1791
1749
|
}
|
|
1792
1750
|
/** Selects the sandbox exec-server environment passed through the Codex app-server protocol. */
|
|
1793
1751
|
function resolveCodexSandboxEnvironmentSelection(environment, nativeToolSurfaceEnabled) {
|
|
1794
|
-
return
|
|
1752
|
+
return nativeToolSurfaceEnabled ? environment ? [environment] : void 0 : [];
|
|
1795
1753
|
}
|
|
1796
1754
|
/** Chooses the cwd visible to Codex native execution after sandbox exec-server setup. */
|
|
1797
1755
|
function resolveCodexAppServerExecutionCwd(params) {
|
|
@@ -1808,8 +1766,12 @@ function resolveCodexExternalSandboxPolicyForOpenClawSandbox(sandbox) {
|
|
|
1808
1766
|
networkAccess: codexNetworkAccessForOpenClawSandbox(sandbox) ? "enabled" : "restricted"
|
|
1809
1767
|
};
|
|
1810
1768
|
}
|
|
1769
|
+
function usesDockerNetworkConfig(sandbox) {
|
|
1770
|
+
const backendId = sandbox?.backendId.trim().toLowerCase();
|
|
1771
|
+
return backendId === "docker" || backendId === "podman";
|
|
1772
|
+
}
|
|
1811
1773
|
function codexNetworkAccessForOpenClawSandbox(sandbox) {
|
|
1812
|
-
if (sandbox
|
|
1774
|
+
if (!usesDockerNetworkConfig(sandbox)) return true;
|
|
1813
1775
|
const network = sandbox?.docker?.network?.trim().toLowerCase();
|
|
1814
1776
|
return Boolean(network && network !== "none");
|
|
1815
1777
|
}
|
|
@@ -1918,834 +1880,1700 @@ function shouldForceMessageTool(params) {
|
|
|
1918
1880
|
return params.disableMessageTool !== true && params.sourceReplyDeliveryMode === "message_tool_only";
|
|
1919
1881
|
}
|
|
1920
1882
|
//#endregion
|
|
1921
|
-
//#region extensions/codex/src/app-server/
|
|
1922
|
-
const
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1883
|
+
//#region extensions/codex/src/app-server/sandbox-exec-server-registry.ts
|
|
1884
|
+
const sandboxExecServerRegistry = {
|
|
1885
|
+
servers: /* @__PURE__ */ new Map(),
|
|
1886
|
+
async closeAll() {
|
|
1887
|
+
const servers = await Promise.allSettled(this.servers.values());
|
|
1888
|
+
this.servers.clear();
|
|
1889
|
+
await Promise.all(servers.map(async (entry) => {
|
|
1890
|
+
if (entry.status !== "fulfilled") return;
|
|
1891
|
+
const server = entry.value;
|
|
1892
|
+
server.refCount = 0;
|
|
1893
|
+
if (server.closed) return;
|
|
1894
|
+
server.closed = true;
|
|
1895
|
+
for (const client of server.server.clients) client.close(1001, "shutdown");
|
|
1896
|
+
await new Promise((resolve) => {
|
|
1897
|
+
server.server.close(() => resolve());
|
|
1898
|
+
});
|
|
1899
|
+
}));
|
|
1929
1900
|
}
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1901
|
+
};
|
|
1902
|
+
//#endregion
|
|
1903
|
+
//#region extensions/codex/src/app-server/sandbox-exec-server/json-rpc.ts
|
|
1904
|
+
/** JSON-RPC error code used when a sandbox filesystem resource does not exist. */
|
|
1905
|
+
const JSON_RPC_NOT_FOUND = -32004;
|
|
1906
|
+
/** JSON-RPC error code used when a sandbox exec-server method is unsupported. */
|
|
1907
|
+
const JSON_RPC_METHOD_NOT_FOUND = -32601;
|
|
1908
|
+
/** Protocol-level error carrying the JSON-RPC error code to send to the client. */
|
|
1909
|
+
var JsonRpcProtocolError = class extends Error {
|
|
1910
|
+
constructor(code, message) {
|
|
1911
|
+
super(message);
|
|
1912
|
+
this.code = code;
|
|
1940
1913
|
}
|
|
1941
|
-
|
|
1942
|
-
|
|
1914
|
+
};
|
|
1915
|
+
/** Parses raw WebSocket data into a JSON-RPC request object. */
|
|
1916
|
+
function parseRequest(data) {
|
|
1917
|
+
const text = (Array.isArray(data) ? Buffer.concat(data) : Buffer.isBuffer(data) ? data : Buffer.from(data)).toString("utf8");
|
|
1918
|
+
return requireObject(JSON.parse(text), "JSON-RPC request");
|
|
1919
|
+
}
|
|
1920
|
+
/** Validates that a JSON value is a non-array object. */
|
|
1921
|
+
function requireObject(value, label) {
|
|
1922
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object.`);
|
|
1923
|
+
return value;
|
|
1924
|
+
}
|
|
1925
|
+
/** Validates a non-empty string JSON-RPC parameter. */
|
|
1926
|
+
function requireString(value, label) {
|
|
1927
|
+
if (typeof value !== "string" || !value) throw new Error(`${label} must be a non-empty string.`);
|
|
1928
|
+
return value;
|
|
1929
|
+
}
|
|
1930
|
+
/** Validates a base64 payload parameter as a string; decoding happens at call sites. */
|
|
1931
|
+
function requireBase64String(value, label) {
|
|
1932
|
+
if (typeof value !== "string") throw new Error(`${label} must be a string.`);
|
|
1933
|
+
return value;
|
|
1934
|
+
}
|
|
1935
|
+
/** Validates a finite numeric JSON-RPC parameter. */
|
|
1936
|
+
function requireNumber(value, label) {
|
|
1937
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${label} must be a finite number.`);
|
|
1938
|
+
return value;
|
|
1939
|
+
}
|
|
1940
|
+
/** Validates a non-empty string-array JSON-RPC parameter. */
|
|
1941
|
+
function requireStringArray(value, label) {
|
|
1942
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) throw new Error(`${label} must be a string array.`);
|
|
1943
|
+
if (value.length === 0) throw new Error(`${label} must not be empty.`);
|
|
1944
|
+
return value;
|
|
1945
|
+
}
|
|
1946
|
+
/** Reads HTTP headers from JSON-RPC params, defaulting to an empty header list. */
|
|
1947
|
+
function readHttpHeaders(value) {
|
|
1948
|
+
if (!Array.isArray(value)) return [];
|
|
1949
|
+
return value.map((entry, index) => {
|
|
1950
|
+
const record = requireObject(entry, `header ${index}`);
|
|
1951
|
+
return {
|
|
1952
|
+
name: requireString(record.name, "header name"),
|
|
1953
|
+
value: requireString(record.value, "header value")
|
|
1954
|
+
};
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
/** Sends a JSON-RPC success response over the WebSocket. */
|
|
1958
|
+
function sendResult(socket, id, result) {
|
|
1959
|
+
socket.send(JSON.stringify({
|
|
1960
|
+
jsonrpc: "2.0",
|
|
1961
|
+
id,
|
|
1962
|
+
result: result === void 0 ? {} : result
|
|
1963
|
+
}));
|
|
1964
|
+
}
|
|
1965
|
+
/** Sends a JSON-RPC error response over the WebSocket. */
|
|
1966
|
+
function sendError(socket, id, code, message) {
|
|
1967
|
+
socket.send(JSON.stringify({
|
|
1968
|
+
jsonrpc: "2.0",
|
|
1969
|
+
id: id ?? null,
|
|
1970
|
+
error: {
|
|
1971
|
+
code,
|
|
1972
|
+
message
|
|
1973
|
+
}
|
|
1974
|
+
}));
|
|
1943
1975
|
}
|
|
1944
1976
|
//#endregion
|
|
1945
|
-
//#region extensions/codex/src/app-server/
|
|
1946
|
-
/**
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
}
|
|
1956
|
-
|
|
1977
|
+
//#region extensions/codex/src/app-server/sandbox-exec-server/path-uri.ts
|
|
1978
|
+
/** Converts Codex PathUri protocol values into sandbox-backend path strings. */
|
|
1979
|
+
const WINDOWS_DRIVE_PATH_RE = /^\/[A-Za-z]:(?:\/|$)/u;
|
|
1980
|
+
/** Resolves one Codex exec-server PathUri into a POSIX sandbox path. */
|
|
1981
|
+
function resolveExecServerPath(rawPath, label) {
|
|
1982
|
+
let pathUrl;
|
|
1983
|
+
try {
|
|
1984
|
+
pathUrl = new URL(rawPath);
|
|
1985
|
+
} catch (error) {
|
|
1986
|
+
throw new Error(`${label} must be a valid file URI: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
1987
|
+
}
|
|
1988
|
+
if (pathUrl.protocol !== "file:") throw new Error(`${label} URI must use the file scheme, received ${pathUrl.protocol.slice(0, -1)}.`);
|
|
1989
|
+
if (pathUrl.search || pathUrl.hash) throw new Error(`${label} file URI must not include a query or fragment.`);
|
|
1990
|
+
let resolved;
|
|
1991
|
+
try {
|
|
1992
|
+
resolved = fileURLToPath(pathUrl, { windows: false });
|
|
1993
|
+
} catch (error) {
|
|
1994
|
+
throw new Error(`${label} file URI is not valid for the sandbox: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
1995
|
+
}
|
|
1996
|
+
if (WINDOWS_DRIVE_PATH_RE.test(resolved)) throw new Error(`${label} Windows file URI is not supported by the sandbox.`);
|
|
1997
|
+
if (resolved.includes("\0")) throw new Error(`${label} file URI must not contain a null byte.`);
|
|
1998
|
+
return resolved;
|
|
1957
1999
|
}
|
|
1958
2000
|
//#endregion
|
|
1959
|
-
//#region extensions/codex/src/app-server/
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
2001
|
+
//#region extensions/codex/src/app-server/sandbox-exec-server/fs-policy.ts
|
|
2002
|
+
/**
|
|
2003
|
+
* Resolves Codex filesystem sandbox policy payloads into OpenClaw path/glob
|
|
2004
|
+
* checks for sandbox exec-server filesystem operations.
|
|
2005
|
+
*/
|
|
2006
|
+
/** Resolves request-local sandbox policy and asserts each requested path has the needed access. */
|
|
2007
|
+
function assertFsSandboxAccess(execServer, record, requests) {
|
|
2008
|
+
assertResolvedFsSandboxAccess(resolveFsSandboxPolicy(execServer, record), requests);
|
|
2009
|
+
}
|
|
2010
|
+
/** Parses a Codex managed filesystem sandbox context into normalized access entries. */
|
|
2011
|
+
function resolveFsSandboxPolicy(execServer, record) {
|
|
2012
|
+
if (record.sandbox === void 0 || record.sandbox === null) return;
|
|
2013
|
+
const sandbox = requireObject(record.sandbox, "fs sandbox context");
|
|
2014
|
+
const permissions = requireObject(sandbox.permissions, "fs sandbox permissions");
|
|
2015
|
+
const permissionType = requireString(permissions.type, "fs sandbox permissions type");
|
|
2016
|
+
if (permissionType === "disabled" || permissionType === "external") return {
|
|
2017
|
+
unrestricted: true,
|
|
2018
|
+
entries: []
|
|
2019
|
+
};
|
|
2020
|
+
if (permissionType !== "managed") throw new Error(`Unsupported Codex fs sandbox permission type: ${permissionType}`);
|
|
2021
|
+
const fileSystem = requireObject(permissions.file_system, "fs sandbox file system permissions");
|
|
2022
|
+
const fileSystemType = requireString(fileSystem.type, "fs sandbox file system permissions type");
|
|
2023
|
+
if (fileSystemType === "unrestricted") return {
|
|
2024
|
+
unrestricted: true,
|
|
2025
|
+
entries: []
|
|
2026
|
+
};
|
|
2027
|
+
if (fileSystemType !== "restricted") throw new Error(`Unsupported Codex fs sandbox file system type: ${fileSystemType}`);
|
|
2028
|
+
if (!Array.isArray(fileSystem.entries)) throw new Error("fs sandbox file system entries must be an array.");
|
|
2029
|
+
const cwd = readFsSandboxCwd(execServer, sandbox);
|
|
2030
|
+
return {
|
|
2031
|
+
unrestricted: false,
|
|
2032
|
+
entries: fileSystem.entries.flatMap((entry, index) => {
|
|
2033
|
+
const resolved = resolveFsSandboxEntry(requireObject(entry, `fs sandbox entry ${index}`), cwd);
|
|
2034
|
+
return resolved ? [resolved] : [];
|
|
2035
|
+
})
|
|
2036
|
+
};
|
|
1964
2037
|
}
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
2038
|
+
function readFsSandboxCwd(execServer, sandbox) {
|
|
2039
|
+
if (sandbox.cwd === void 0 || sandbox.cwd === null) return normalizeSandboxAbsolutePath(execServer.sandbox.containerWorkdir, "sandbox cwd");
|
|
2040
|
+
return normalizeSandboxAbsolutePath(resolveExecServerPath(requireString(sandbox.cwd, "sandbox cwd"), "sandbox cwd"), "sandbox cwd");
|
|
2041
|
+
}
|
|
2042
|
+
function resolveFsSandboxEntry(entry, cwd) {
|
|
2043
|
+
const access = readFsAccessMode(entry.access);
|
|
2044
|
+
const pathSpec = requireObject(entry.path, "fs sandbox entry path");
|
|
2045
|
+
const pathType = requireString(pathSpec.type, "fs sandbox entry path type");
|
|
2046
|
+
if (pathType === "path") return {
|
|
2047
|
+
kind: "path",
|
|
2048
|
+
path: normalizeSandboxAbsolutePath(resolveExecServerPath(requireString(pathSpec.path, "fs sandbox path"), "fs sandbox path"), "fs sandbox path"),
|
|
2049
|
+
access
|
|
2050
|
+
};
|
|
2051
|
+
if (pathType === "special") {
|
|
2052
|
+
if (isNonGrantingFsSpecialPath(requireObject(pathSpec.value, "fs sandbox special path"))) return;
|
|
2053
|
+
return {
|
|
2054
|
+
kind: "path",
|
|
2055
|
+
path: resolveFsSpecialPath(requireObject(pathSpec.value, "fs sandbox special path"), cwd),
|
|
2056
|
+
access
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
if (pathType === "glob_pattern") {
|
|
2060
|
+
const pattern = requireString(pathSpec.pattern, "fs sandbox glob pattern");
|
|
2061
|
+
const absolutePattern = normalizeSandboxGlobPattern(pattern.startsWith("/") ? pattern : posix.join(cwd, pattern));
|
|
2062
|
+
return {
|
|
2063
|
+
kind: "glob",
|
|
2064
|
+
pattern: absolutePattern,
|
|
2065
|
+
matcher: compileSandboxGlobPattern(absolutePattern),
|
|
2066
|
+
literalPrefix: sandboxGlobLiteralPrefix(absolutePattern),
|
|
2067
|
+
access
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
throw new Error(`Unsupported Codex fs sandbox path type: ${pathType}`);
|
|
2071
|
+
}
|
|
2072
|
+
function isNonGrantingFsSpecialPath(value) {
|
|
2073
|
+
const kind = requireString(value.kind, "fs sandbox special path kind");
|
|
2074
|
+
return kind === "minimal" || kind === "unknown";
|
|
2075
|
+
}
|
|
2076
|
+
function readFsAccessMode(value) {
|
|
2077
|
+
if (value === "read" || value === "write" || value === "none") return value;
|
|
2078
|
+
if (value === "deny") return "none";
|
|
2079
|
+
throw new Error("fs sandbox entry access must be read, write, none, or deny.");
|
|
2080
|
+
}
|
|
2081
|
+
function resolveFsSpecialPath(value, cwd) {
|
|
2082
|
+
const kind = requireString(value.kind, "fs sandbox special path kind");
|
|
2083
|
+
if (kind === "root") return "/";
|
|
2084
|
+
if (kind === "project_roots" || kind === "current_working_directory") {
|
|
2085
|
+
const subpath = value.subpath === void 0 || value.subpath === null ? void 0 : requireString(value.subpath, "fs sandbox project roots subpath");
|
|
2086
|
+
return normalizeSandboxAbsolutePath(subpath ? posix.join(cwd, subpath) : cwd, "fs sandbox project roots path");
|
|
2087
|
+
}
|
|
2088
|
+
if (kind === "slash_tmp" || kind === "tmpdir") return "/tmp";
|
|
2089
|
+
throw new Error(`Unsupported Codex fs sandbox special path: ${kind}`);
|
|
2090
|
+
}
|
|
2091
|
+
/** Asserts access against an already resolved filesystem sandbox policy. */
|
|
2092
|
+
function assertResolvedFsSandboxAccess(policy, requests) {
|
|
2093
|
+
if (!policy?.unrestricted && policy) for (const request of requests) {
|
|
2094
|
+
const access = resolveFsAccess(policy, request.path);
|
|
2095
|
+
if (request.access === "read" && access === "none") throw new Error(`Codex fs sandbox denied read access to ${request.path}`);
|
|
2096
|
+
if (request.access === "write" && access !== "write") throw new Error(`Codex fs sandbox denied write access to ${request.path}`);
|
|
2097
|
+
}
|
|
1970
2098
|
}
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
2099
|
+
function resolveFsAccess(policy, rawPath) {
|
|
2100
|
+
if (policy.unrestricted) return "write";
|
|
2101
|
+
const target = normalizeSandboxAbsolutePath(rawPath, "fs path");
|
|
2102
|
+
let selected;
|
|
2103
|
+
for (const entry of policy.entries) {
|
|
2104
|
+
if (!fsSandboxEntryMatches(entry, target)) continue;
|
|
2105
|
+
const candidate = {
|
|
2106
|
+
specificity: fsSandboxEntrySpecificity(entry),
|
|
2107
|
+
rank: fsAccessRank(entry.access),
|
|
2108
|
+
access: entry.access
|
|
2109
|
+
};
|
|
2110
|
+
if (!selected || candidate.specificity > selected.specificity || candidate.specificity === selected.specificity && candidate.rank > selected.rank) selected = candidate;
|
|
2111
|
+
}
|
|
2112
|
+
return selected?.access ?? "none";
|
|
2113
|
+
}
|
|
2114
|
+
/** Rejects recursive writes/removes that would cross protected read-only descendants. */
|
|
2115
|
+
function assertNoReadOnlyDescendant(policy, rawPath, operation) {
|
|
2116
|
+
if (!policy || policy.unrestricted) return;
|
|
2117
|
+
const target = normalizeSandboxAbsolutePath(rawPath, "fs path");
|
|
2118
|
+
const protectedDescendant = policy.entries.find((entry) => {
|
|
2119
|
+
if (entry.access === "write" || !fsSandboxEntryCanAffectDescendant(entry, target)) return false;
|
|
2120
|
+
if (entry.kind === "glob") return true;
|
|
2121
|
+
const protectedPath = entry.path;
|
|
2122
|
+
return protectedPath && resolveFsAccess(policy, protectedPath) !== "write";
|
|
2123
|
+
});
|
|
2124
|
+
if (protectedDescendant) {
|
|
2125
|
+
const protectedPath = protectedDescendant.kind === "path" ? protectedDescendant.path : protectedDescendant.pattern;
|
|
2126
|
+
throw new Error(`Codex fs sandbox denied recursive ${operation} of ${rawPath} because ${protectedPath} is not writable.`);
|
|
1978
2127
|
}
|
|
1979
2128
|
}
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
if (
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2129
|
+
/** Normalizes and validates an absolute POSIX path inside the sandbox namespace. */
|
|
2130
|
+
function normalizeSandboxAbsolutePath(rawPath, label) {
|
|
2131
|
+
if (!rawPath || rawPath.includes("\0") || !rawPath.startsWith("/")) throw new Error(`${label} must be an absolute sandbox path.`);
|
|
2132
|
+
const normalized = posix.normalize(rawPath);
|
|
2133
|
+
return normalized === "//" ? "/" : normalized;
|
|
2134
|
+
}
|
|
2135
|
+
/** Returns true when target is root itself or a descendant of root. */
|
|
2136
|
+
function pathContains(root, target) {
|
|
2137
|
+
return root === "/" || target === root || target.startsWith(`${root}/`);
|
|
2138
|
+
}
|
|
2139
|
+
function fsSandboxEntryMatches(entry, target) {
|
|
2140
|
+
if (entry.kind === "path") return pathContains(entry.path, target);
|
|
2141
|
+
return entry.matcher.test(target);
|
|
2142
|
+
}
|
|
2143
|
+
function fsSandboxEntryCanAffectDescendant(entry, target) {
|
|
2144
|
+
if (entry.kind === "path") return pathContains(target, entry.path) && target !== entry.path;
|
|
2145
|
+
return pathContains(target, entry.literalPrefix) || pathContains(entry.literalPrefix, target);
|
|
2146
|
+
}
|
|
2147
|
+
function fsSandboxEntrySpecificity(entry) {
|
|
2148
|
+
return pathSpecificity(entry.kind === "path" ? entry.path : entry.literalPrefix);
|
|
2149
|
+
}
|
|
2150
|
+
function pathSpecificity(filePath) {
|
|
2151
|
+
return filePath === "/" ? 0 : filePath.split("/").filter(Boolean).length;
|
|
2152
|
+
}
|
|
2153
|
+
function fsAccessRank(access) {
|
|
2154
|
+
if (access === "none") return 2;
|
|
2155
|
+
if (access === "write") return 1;
|
|
2156
|
+
return 0;
|
|
2157
|
+
}
|
|
2158
|
+
function normalizeSandboxGlobPattern(pattern) {
|
|
2159
|
+
if (!pattern || pattern.includes("\0") || !pattern.startsWith("/")) throw new Error("fs sandbox glob pattern must be absolute.");
|
|
2160
|
+
return pattern.replace(/\/{2,}/gu, "/");
|
|
2161
|
+
}
|
|
2162
|
+
function compileSandboxGlobPattern(pattern) {
|
|
2163
|
+
let source = "^";
|
|
2164
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
2165
|
+
const char = pattern[index];
|
|
2166
|
+
const next = pattern[index + 1];
|
|
2167
|
+
if (char === "*" && next === "*" && pattern[index + 2] === "/") {
|
|
2168
|
+
source += "(?:.*/)?";
|
|
2169
|
+
index += 2;
|
|
2170
|
+
} else if (char === "*" && next === "*") {
|
|
2171
|
+
source += ".*";
|
|
2172
|
+
index += 1;
|
|
2173
|
+
} else if (char === "*") source += "[^/]*";
|
|
2174
|
+
else if (char === "?") source += "[^/]";
|
|
2175
|
+
else if (char === "[") {
|
|
2176
|
+
const compiledClass = compileSandboxGlobCharacterClass(pattern, index);
|
|
2177
|
+
source += compiledClass.source;
|
|
2178
|
+
index = compiledClass.endIndex;
|
|
2179
|
+
} else source += char?.replace(/[\\^$+?.()|[\]{}]/gu, "\\$&") ?? "";
|
|
2180
|
+
}
|
|
2181
|
+
source += "$";
|
|
2182
|
+
return new RegExp(source, "u");
|
|
2183
|
+
}
|
|
2184
|
+
function compileSandboxGlobCharacterClass(pattern, startIndex) {
|
|
2185
|
+
let index = startIndex + 1;
|
|
2186
|
+
if (index >= pattern.length) throw new Error("fs sandbox glob character class must be closed.");
|
|
2187
|
+
const negated = pattern[index] === "!" || pattern[index] === "^";
|
|
2188
|
+
if (negated) index += 1;
|
|
2189
|
+
let body = "";
|
|
2190
|
+
for (; index < pattern.length; index += 1) {
|
|
2191
|
+
const char = pattern[index];
|
|
2192
|
+
if (char === "]" && body) return {
|
|
2193
|
+
source: `[${negated ? "^" : ""}${body}]`,
|
|
2194
|
+
endIndex: index
|
|
2195
|
+
};
|
|
2196
|
+
if (!char || char === "/") throw new Error("fs sandbox glob character class cannot match path separators.");
|
|
2197
|
+
body += escapeSandboxGlobCharacterClassChar(char, body.length === 0);
|
|
2002
2198
|
}
|
|
2199
|
+
throw new Error("fs sandbox glob character class must be closed.");
|
|
2200
|
+
}
|
|
2201
|
+
function escapeSandboxGlobCharacterClassChar(char, first) {
|
|
2202
|
+
if (char === "\\" || char === "]") return `\\${char}`;
|
|
2203
|
+
if (first && char === "^") return "\\^";
|
|
2204
|
+
return char;
|
|
2205
|
+
}
|
|
2206
|
+
function sandboxGlobLiteralPrefix(pattern) {
|
|
2207
|
+
const wildcardIndex = pattern.search(/[*?[]/u);
|
|
2208
|
+
const prefix = wildcardIndex === -1 ? pattern : pattern.slice(0, wildcardIndex);
|
|
2209
|
+
const slash = prefix.lastIndexOf("/");
|
|
2210
|
+
if (slash <= 0) return "/";
|
|
2211
|
+
return normalizeSandboxAbsolutePath(prefix.slice(0, slash), "fs sandbox glob prefix");
|
|
2212
|
+
}
|
|
2213
|
+
/** Safely joins a single directory entry name onto a sandbox parent path. */
|
|
2214
|
+
function joinSandboxChildPath(parent, child) {
|
|
2215
|
+
if (!child || child === "." || child === ".." || child.includes("/") || child.includes("\0")) throw new Error(`Invalid sandbox directory entry name: ${child}`);
|
|
2216
|
+
return parent.endsWith("/") ? `${parent}${child}` : `${parent}/${child}`;
|
|
2003
2217
|
}
|
|
2004
2218
|
//#endregion
|
|
2005
|
-
//#region extensions/codex/src/app-server/
|
|
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
|
+
//#region extensions/codex/src/app-server/sandbox-exec-server/filesystem.ts
|
|
2006
2234
|
/**
|
|
2007
|
-
*
|
|
2235
|
+
* Implements filesystem JSON-RPC handlers for the Codex sandbox exec-server
|
|
2236
|
+
* with OpenClaw sandbox policy checks before every bridge operation.
|
|
2008
2237
|
*/
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2238
|
+
const CODEX_SANDBOX_EXEC_SERVER_MAX_READ_FILE_BYTES = 512 * 1024 * 1024;
|
|
2239
|
+
const CODEX_SANDBOX_EXEC_SERVER_MAX_OPEN_FILE_READS = 128;
|
|
2240
|
+
const CODEX_SANDBOX_EXEC_SERVER_MAX_BUFFERED_FILE_READ_BYTES = 64 * 1024 * 1024;
|
|
2241
|
+
const CODEX_SANDBOX_EXEC_SERVER_MAX_READ_BLOCK_BYTES = 1024 * 1024;
|
|
2242
|
+
const CODEX_SANDBOX_EXEC_SERVER_MAX_FILE_READ_HANDLE_ID_BYTES = 32;
|
|
2243
|
+
/** Opens a policy-checked sandbox file under a bounded, connection-owned handle. */
|
|
2244
|
+
async function openFile(execServer, handles, params) {
|
|
2245
|
+
const record = requireObject(params, "fs/open params");
|
|
2246
|
+
const handleId = requireFileReadHandleId(record.handleId);
|
|
2247
|
+
if (handles.closed) throw new JsonRpcProtocolError(JSON_RPC_NOT_FOUND, `unknown file read handle \`${handleId}\``);
|
|
2248
|
+
if (handles.has(handleId)) throw new JsonRpcProtocolError(-32600, `file read handle \`${handleId}\` already exists`);
|
|
2249
|
+
if (handles.size >= CODEX_SANDBOX_EXEC_SERVER_MAX_OPEN_FILE_READS) throw new JsonRpcProtocolError(-32600, `at most ${CODEX_SANDBOX_EXEC_SERVER_MAX_OPEN_FILE_READS} file reads may be open per connection`);
|
|
2250
|
+
const filePath = resolveExecServerPath(requireString(record.path, "path"), "read path");
|
|
2251
|
+
assertFsSandboxAccess(execServer, record, [{
|
|
2252
|
+
path: filePath,
|
|
2253
|
+
access: "read"
|
|
2254
|
+
}]);
|
|
2255
|
+
const fsBridge = requireFsBridge(execServer);
|
|
2256
|
+
const handle = {
|
|
2257
|
+
abortController: new AbortController(),
|
|
2258
|
+
closeRequested: false,
|
|
2259
|
+
reservedBytes: 0
|
|
2260
|
+
};
|
|
2261
|
+
handles.set(handleId, handle);
|
|
2262
|
+
try {
|
|
2263
|
+
const stat = await fsBridge.stat({
|
|
2264
|
+
filePath,
|
|
2265
|
+
signal: handle.abortController.signal
|
|
2266
|
+
});
|
|
2267
|
+
if (handles.get(handleId) !== handle || handle.closeRequested || handles.closed) throw new JsonRpcProtocolError(JSON_RPC_NOT_FOUND, `unknown file read handle \`${handleId}\``);
|
|
2268
|
+
if (!stat) throw new JsonRpcProtocolError(JSON_RPC_NOT_FOUND, "file not found");
|
|
2269
|
+
if (stat.type !== "file") throw new JsonRpcProtocolError(-32600, "file read handle requires a regular file");
|
|
2270
|
+
if (!Number.isSafeInteger(stat.size) || stat.size < 0) throw new JsonRpcProtocolError(-32600, "file size must be a non-negative safe integer");
|
|
2271
|
+
if (stat.size > CODEX_SANDBOX_EXEC_SERVER_MAX_BUFFERED_FILE_READ_BYTES - bufferedFileReadBytes(handles)) throw new JsonRpcProtocolError(-32600, "sandbox file read exceeds the per-connection buffered file limit");
|
|
2272
|
+
handle.reservedBytes = stat.size;
|
|
2273
|
+
const data = await fsBridge.readFile({
|
|
2274
|
+
filePath,
|
|
2275
|
+
maxBytes: handle.reservedBytes,
|
|
2276
|
+
signal: handle.abortController.signal
|
|
2277
|
+
});
|
|
2278
|
+
if (handles.get(handleId) !== handle || handle.closeRequested || handles.closed) throw new JsonRpcProtocolError(JSON_RPC_NOT_FOUND, `unknown file read handle \`${handleId}\``);
|
|
2279
|
+
if (data.byteLength > handle.reservedBytes) throw new JsonRpcProtocolError(-32600, "sandbox file read exceeds the per-connection buffered file limit");
|
|
2280
|
+
handle.reservedBytes = data.byteLength;
|
|
2281
|
+
handle.data = data;
|
|
2282
|
+
return { handleId };
|
|
2283
|
+
} catch (error) {
|
|
2284
|
+
if (handles.get(handleId) === handle) handles.delete(handleId);
|
|
2285
|
+
throw error;
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
/** Reads a bounded base64 block from a handle belonging to this connection. */
|
|
2289
|
+
function readFileBlock(handles, params) {
|
|
2290
|
+
const record = requireObject(params, "fs/readBlock params");
|
|
2291
|
+
const handleId = requireFileReadHandleId(record.handleId);
|
|
2292
|
+
const handle = handles.get(handleId);
|
|
2293
|
+
if (!handle?.data) throw new JsonRpcProtocolError(JSON_RPC_NOT_FOUND, `unknown file read handle \`${handleId}\``);
|
|
2294
|
+
const offset = requireNumber(record.offset, "offset");
|
|
2295
|
+
const length = requireNumber(record.len, "len");
|
|
2296
|
+
if (!Number.isSafeInteger(offset) || offset < 0) throw new JsonRpcProtocolError(-32600, "file read offset must be a non-negative safe integer");
|
|
2297
|
+
if (!Number.isSafeInteger(length) || length < 1 || length > CODEX_SANDBOX_EXEC_SERVER_MAX_READ_BLOCK_BYTES) throw new JsonRpcProtocolError(-32600, `file read block length must be between 1 and ${CODEX_SANDBOX_EXEC_SERVER_MAX_READ_BLOCK_BYTES}`);
|
|
2298
|
+
const chunk = handle.data.subarray(offset, Math.min(offset + length, handle.data.byteLength));
|
|
2299
|
+
return {
|
|
2300
|
+
chunk: chunk.toString("base64"),
|
|
2301
|
+
eof: offset + chunk.byteLength >= handle.data.byteLength
|
|
2302
|
+
};
|
|
2303
|
+
}
|
|
2304
|
+
/** Closes one connection-owned file handle; repeated closes are harmless. */
|
|
2305
|
+
function closeFile(handles, params) {
|
|
2306
|
+
closeFileReadHandle(handles, requireFileReadHandleId(requireObject(params, "fs/close params").handleId));
|
|
2307
|
+
return {};
|
|
2308
|
+
}
|
|
2309
|
+
/** Cancels a disconnected socket without releasing unsettled read reservations. */
|
|
2310
|
+
function closeAllFileReads(handles) {
|
|
2311
|
+
handles.closed = true;
|
|
2312
|
+
for (const handleId of handles.keys()) closeFileReadHandle(handles, handleId);
|
|
2313
|
+
}
|
|
2314
|
+
function closeFileReadHandle(handles, handleId) {
|
|
2315
|
+
const handle = handles.get(handleId);
|
|
2316
|
+
if (!handle) return;
|
|
2317
|
+
handle.closeRequested = true;
|
|
2318
|
+
if (handle.data !== void 0) {
|
|
2319
|
+
handles.delete(handleId);
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2322
|
+
handle.abortController.abort();
|
|
2323
|
+
}
|
|
2324
|
+
function bufferedFileReadBytes(handles) {
|
|
2325
|
+
let total = 0;
|
|
2326
|
+
for (const handle of handles.values()) total += handle.reservedBytes;
|
|
2327
|
+
return total;
|
|
2328
|
+
}
|
|
2329
|
+
function requireFileReadHandleId(value) {
|
|
2330
|
+
const handleId = requireString(value, "handleId");
|
|
2331
|
+
if (Buffer.byteLength(handleId, "utf8") > CODEX_SANDBOX_EXEC_SERVER_MAX_FILE_READ_HANDLE_ID_BYTES) throw new JsonRpcProtocolError(-32600, `file read handle ID must not exceed ${CODEX_SANDBOX_EXEC_SERVER_MAX_FILE_READ_HANDLE_ID_BYTES} bytes`);
|
|
2332
|
+
return handleId;
|
|
2333
|
+
}
|
|
2334
|
+
/** Reads a sandbox file as base64 after read-policy and size checks. */
|
|
2335
|
+
async function readFile(execServer, params) {
|
|
2336
|
+
const record = requireObject(params, "fs/readFile params");
|
|
2337
|
+
const filePath = resolveExecServerPath(requireString(record.path, "path"), "read path");
|
|
2338
|
+
assertFsSandboxAccess(execServer, record, [{
|
|
2339
|
+
path: filePath,
|
|
2340
|
+
access: "read"
|
|
2341
|
+
}]);
|
|
2342
|
+
const fsBridge = requireFsBridge(execServer);
|
|
2343
|
+
const stat = await fsBridge.stat({ filePath });
|
|
2344
|
+
if (!stat) throw new JsonRpcProtocolError(JSON_RPC_NOT_FOUND, "file not found");
|
|
2345
|
+
assertSandboxFileReadWithinLimit(stat);
|
|
2346
|
+
return { dataBase64: (await fsBridge.readFile({
|
|
2347
|
+
filePath,
|
|
2348
|
+
maxBytes: CODEX_SANDBOX_EXEC_SERVER_MAX_READ_FILE_BYTES
|
|
2349
|
+
})).toString("base64") };
|
|
2350
|
+
}
|
|
2351
|
+
/** Writes base64 data to an existing sandbox directory after write-policy checks. */
|
|
2352
|
+
async function writeFile(execServer, params) {
|
|
2353
|
+
const record = requireObject(params, "fs/writeFile params");
|
|
2354
|
+
const filePath = resolveExecServerPath(requireString(record.path, "path"), "write path");
|
|
2355
|
+
assertFsSandboxAccess(execServer, record, [{
|
|
2356
|
+
path: filePath,
|
|
2357
|
+
access: "write"
|
|
2358
|
+
}]);
|
|
2359
|
+
const fsBridge = requireFsBridge(execServer);
|
|
2360
|
+
if ((await fsBridge.stat({ filePath: posix.dirname(filePath) }))?.type !== "directory") throw new JsonRpcProtocolError(JSON_RPC_NOT_FOUND, "parent directory not found");
|
|
2361
|
+
await fsBridge.writeFile({
|
|
2362
|
+
filePath,
|
|
2363
|
+
data: Buffer.from(requireBase64String(record.dataBase64, "dataBase64"), "base64"),
|
|
2364
|
+
mkdir: false
|
|
2019
2365
|
});
|
|
2020
2366
|
}
|
|
2021
|
-
/**
|
|
2022
|
-
function
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2367
|
+
/** Creates a sandbox directory, respecting recursive and parent-directory semantics. */
|
|
2368
|
+
async function createDirectory(execServer, params) {
|
|
2369
|
+
const record = requireObject(params, "fs/createDirectory params");
|
|
2370
|
+
const filePath = resolveExecServerPath(requireString(record.path, "path"), "create-directory path");
|
|
2371
|
+
assertFsSandboxAccess(execServer, record, [{
|
|
2372
|
+
path: filePath,
|
|
2373
|
+
access: "write"
|
|
2374
|
+
}]);
|
|
2375
|
+
const fsBridge = requireFsBridge(execServer);
|
|
2376
|
+
if (record.recursive === false) {
|
|
2377
|
+
const parentPath = posix.dirname(filePath);
|
|
2378
|
+
if ((await fsBridge.stat({ filePath: parentPath }))?.type !== "directory") throw new JsonRpcProtocolError(JSON_RPC_NOT_FOUND, "parent directory not found");
|
|
2379
|
+
}
|
|
2380
|
+
await fsBridge.mkdirp({ filePath });
|
|
2381
|
+
}
|
|
2382
|
+
/** Returns normalized metadata for a sandbox path. */
|
|
2383
|
+
async function getMetadata(execServer, params) {
|
|
2384
|
+
const record = requireObject(params, "fs/getMetadata params");
|
|
2385
|
+
const filePath = resolveExecServerPath(requireString(record.path, "path"), "metadata path");
|
|
2386
|
+
assertFsSandboxAccess(execServer, record, [{
|
|
2387
|
+
path: filePath,
|
|
2388
|
+
access: "read"
|
|
2389
|
+
}]);
|
|
2390
|
+
const stat = await requireFsBridge(execServer).stat({ filePath });
|
|
2391
|
+
if (!stat) throw new JsonRpcProtocolError(JSON_RPC_NOT_FOUND, "file not found");
|
|
2392
|
+
return metadataResponse(stat);
|
|
2393
|
+
}
|
|
2394
|
+
/** Lists sandbox directory entries visible under the resolved filesystem policy. */
|
|
2395
|
+
async function readDirectory(execServer, params) {
|
|
2396
|
+
const record = requireObject(params, "fs/readDirectory params");
|
|
2397
|
+
return { entries: await listDirectoryEntries(execServer, resolveExecServerPath(requireString(record.path, "path"), "read-directory path"), resolveFsSandboxPolicy(execServer, record)) };
|
|
2398
|
+
}
|
|
2399
|
+
async function listDirectoryEntries(execServer, filePath, fsSandboxPolicy) {
|
|
2400
|
+
assertResolvedFsSandboxAccess(fsSandboxPolicy, [{
|
|
2401
|
+
path: filePath,
|
|
2402
|
+
access: "read"
|
|
2403
|
+
}]);
|
|
2404
|
+
const fsBridge = requireFsBridge(execServer);
|
|
2405
|
+
const backend = requireBackend(execServer);
|
|
2406
|
+
const resolved = fsBridge.resolvePath({ filePath });
|
|
2407
|
+
if (!resolved) throw new Error(`Cannot resolve sandbox path: ${filePath}`);
|
|
2408
|
+
const result = await backend.runShellCommand({
|
|
2409
|
+
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
|
+
args: [resolved.containerPath],
|
|
2411
|
+
allowFailure: true
|
|
2412
|
+
});
|
|
2413
|
+
if (result.code !== 0) {
|
|
2414
|
+
const stderr = result.stderr.toString("utf8").trim();
|
|
2415
|
+
throw new Error(stderr || `sandbox directory listing failed with code ${result.code}`);
|
|
2416
|
+
}
|
|
2417
|
+
return result.stdout.toString("utf8").split("\n").filter(Boolean).map((line) => {
|
|
2418
|
+
const [kind = "o", fileName = ""] = line.split(" ");
|
|
2419
|
+
return {
|
|
2420
|
+
fileName,
|
|
2421
|
+
isDirectory: kind === "d",
|
|
2422
|
+
isFile: kind === "f"
|
|
2423
|
+
};
|
|
2034
2424
|
});
|
|
2035
2425
|
}
|
|
2036
|
-
/**
|
|
2037
|
-
function
|
|
2038
|
-
const
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2426
|
+
/** Removes a sandbox path after rejecting writes outside policy or under read-only descendants. */
|
|
2427
|
+
async function removePath(execServer, params) {
|
|
2428
|
+
const record = requireObject(params, "fs/remove params");
|
|
2429
|
+
const filePath = resolveExecServerPath(requireString(record.path, "path"), "remove path");
|
|
2430
|
+
const fsSandboxPolicy = resolveFsSandboxPolicy(execServer, record);
|
|
2431
|
+
assertResolvedFsSandboxAccess(fsSandboxPolicy, [{
|
|
2432
|
+
path: filePath,
|
|
2433
|
+
access: "write"
|
|
2434
|
+
}]);
|
|
2435
|
+
if (record.recursive !== false) assertNoReadOnlyDescendant(fsSandboxPolicy, filePath, "remove");
|
|
2436
|
+
await requireFsBridge(execServer).remove({
|
|
2437
|
+
filePath,
|
|
2438
|
+
recursive: record.recursive !== false,
|
|
2439
|
+
force: record.force !== false
|
|
2440
|
+
});
|
|
2441
|
+
}
|
|
2442
|
+
/** Copies sandbox files or recursive directories while enforcing source and destination policy. */
|
|
2443
|
+
async function copyPath(execServer, params) {
|
|
2444
|
+
const record = requireObject(params, "fs/copy params");
|
|
2445
|
+
const sourcePath = resolveExecServerPath(requireString(record.sourcePath ?? record.source, "sourcePath"), "copy source path");
|
|
2446
|
+
const destinationPath = resolveExecServerPath(requireString(record.destinationPath ?? record.destination, "destinationPath"), "copy destination path");
|
|
2447
|
+
const fsSandboxPolicy = resolveFsSandboxPolicy(execServer, record);
|
|
2448
|
+
assertResolvedFsSandboxAccess(fsSandboxPolicy, [{
|
|
2449
|
+
path: sourcePath,
|
|
2450
|
+
access: "read"
|
|
2451
|
+
}, {
|
|
2452
|
+
path: destinationPath,
|
|
2453
|
+
access: "write"
|
|
2454
|
+
}]);
|
|
2455
|
+
await copySandboxPath(execServer, {
|
|
2456
|
+
sourcePath,
|
|
2457
|
+
destinationPath,
|
|
2458
|
+
recursive: record.recursive === true,
|
|
2459
|
+
fsSandboxPolicy
|
|
2460
|
+
});
|
|
2461
|
+
}
|
|
2462
|
+
async function copySandboxPath(execServer, params) {
|
|
2463
|
+
const fsBridge = execServer.sandbox.fsBridge;
|
|
2464
|
+
if (!fsBridge) throw new Error("Sandbox filesystem bridge is unavailable.");
|
|
2465
|
+
assertResolvedFsSandboxAccess(params.fsSandboxPolicy, [{
|
|
2466
|
+
path: params.sourcePath,
|
|
2467
|
+
access: "read"
|
|
2468
|
+
}, {
|
|
2469
|
+
path: params.destinationPath,
|
|
2470
|
+
access: "write"
|
|
2471
|
+
}]);
|
|
2472
|
+
const sourceStat = await fsBridge.stat({ filePath: params.sourcePath });
|
|
2473
|
+
if (!sourceStat) throw new JsonRpcProtocolError(JSON_RPC_NOT_FOUND, "file not found");
|
|
2474
|
+
if (sourceStat?.type === "directory") {
|
|
2475
|
+
if (!params.recursive) throw new Error(`Cannot copy directory without recursive=true: ${params.sourcePath}`);
|
|
2476
|
+
if (pathContains(normalizeSandboxAbsolutePath(params.sourcePath, "copy source path"), normalizeSandboxAbsolutePath(params.destinationPath, "copy destination path"))) throw new Error("Cannot recursively copy a directory into itself.");
|
|
2477
|
+
await fsBridge.mkdirp({ filePath: params.destinationPath });
|
|
2478
|
+
for (const entry of await listDirectoryEntries(execServer, params.sourcePath, params.fsSandboxPolicy)) {
|
|
2479
|
+
if (!entry.isDirectory && !entry.isFile) throw new Error(`Cannot copy unsupported filesystem entry: ${entry.fileName}`);
|
|
2480
|
+
await copySandboxPath(execServer, {
|
|
2481
|
+
sourcePath: joinSandboxChildPath(params.sourcePath, entry.fileName),
|
|
2482
|
+
destinationPath: joinSandboxChildPath(params.destinationPath, entry.fileName),
|
|
2483
|
+
recursive: true,
|
|
2484
|
+
fsSandboxPolicy: params.fsSandboxPolicy
|
|
2485
|
+
});
|
|
2486
|
+
}
|
|
2050
2487
|
return;
|
|
2051
2488
|
}
|
|
2052
|
-
if (
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
sessionId: params.sessionId,
|
|
2058
|
-
sessionKey: params.sessionKey,
|
|
2059
|
-
toolName: params.call.tool,
|
|
2060
|
-
toolCallId: params.call.callId,
|
|
2061
|
-
deniedReason: "plugin-before-tool-call",
|
|
2062
|
-
reason: "Tool call blocked"
|
|
2489
|
+
if (sourceStat.type === "file" && fsBridge.copyFile) {
|
|
2490
|
+
await fsBridge.copyFile({
|
|
2491
|
+
sourcePath: params.sourcePath,
|
|
2492
|
+
destinationPath: params.destinationPath,
|
|
2493
|
+
mkdir: true
|
|
2063
2494
|
});
|
|
2064
2495
|
return;
|
|
2065
2496
|
}
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2497
|
+
assertSandboxFileReadWithinLimit(sourceStat);
|
|
2498
|
+
const data = await fsBridge.readFile({
|
|
2499
|
+
filePath: params.sourcePath,
|
|
2500
|
+
maxBytes: CODEX_SANDBOX_EXEC_SERVER_MAX_READ_FILE_BYTES
|
|
2501
|
+
});
|
|
2502
|
+
await fsBridge.writeFile({
|
|
2503
|
+
filePath: params.destinationPath,
|
|
2504
|
+
data,
|
|
2505
|
+
mkdir: true
|
|
2069
2506
|
});
|
|
2070
2507
|
}
|
|
2508
|
+
function assertSandboxFileReadWithinLimit(stat) {
|
|
2509
|
+
if (stat.type === "file" && stat.size > CODEX_SANDBOX_EXEC_SERVER_MAX_READ_FILE_BYTES) throw new Error(`file is too large to read through Codex sandbox exec-server: ${stat.size} bytes`);
|
|
2510
|
+
}
|
|
2511
|
+
function metadataResponse(stat) {
|
|
2512
|
+
return {
|
|
2513
|
+
isDirectory: stat?.type === "directory",
|
|
2514
|
+
isFile: stat?.type === "file",
|
|
2515
|
+
isSymlink: false,
|
|
2516
|
+
size: stat?.size ?? 0,
|
|
2517
|
+
createdAtMs: 0,
|
|
2518
|
+
modifiedAtMs: stat?.mtimeMs ?? 0
|
|
2519
|
+
};
|
|
2520
|
+
}
|
|
2071
2521
|
//#endregion
|
|
2072
|
-
//#region extensions/codex/src/app-server/
|
|
2522
|
+
//#region extensions/codex/src/app-server/sandbox-exec-server/http.ts
|
|
2073
2523
|
/**
|
|
2074
|
-
*
|
|
2075
|
-
*
|
|
2524
|
+
* Implements sandboxed HTTP requests for Codex native tools by routing network
|
|
2525
|
+
* access through the active OpenClaw sandbox backend.
|
|
2076
2526
|
*/
|
|
2077
|
-
|
|
2078
|
-
const
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
const
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
...params.allowedDecisions ? { allowedDecisions: params.allowedDecisions } : {}
|
|
2099
|
-
}, { expectFinal: false });
|
|
2527
|
+
/** Maximum JSON-line size accepted from the streaming HTTP helper process. */
|
|
2528
|
+
const SANDBOX_HTTP_STREAM_LINE_MAX_CHARS = 256 * 1024;
|
|
2529
|
+
/** Handles one sandbox HTTP JSON-RPC request, optionally streaming response body deltas. */
|
|
2530
|
+
async function httpRequest(execServer, socket, params) {
|
|
2531
|
+
const record = requireObject(params, "http/request params");
|
|
2532
|
+
const requestId = requireString(record.requestId, "requestId");
|
|
2533
|
+
const url = requireString(record.url, "url");
|
|
2534
|
+
assertSandboxHttpRequestTargetAllowed(url);
|
|
2535
|
+
const request = {
|
|
2536
|
+
method: requireString(record.method, "method"),
|
|
2537
|
+
url,
|
|
2538
|
+
headers: readHttpHeaders(record.headers),
|
|
2539
|
+
bodyBase64: typeof record.bodyBase64 === "string" ? record.bodyBase64 : void 0,
|
|
2540
|
+
timeoutMs: typeof record.timeoutMs === "number" && record.timeoutMs > 0 ? Math.floor(record.timeoutMs) : void 0,
|
|
2541
|
+
streamResponse: record.streamResponse === true
|
|
2542
|
+
};
|
|
2543
|
+
if (request.streamResponse) return await runStreamingSandboxHttpRequest(execServer, socket, requestId, request);
|
|
2544
|
+
return await runSandboxHttpRequest(execServer, {
|
|
2545
|
+
...request,
|
|
2546
|
+
streamResponse: false
|
|
2547
|
+
});
|
|
2100
2548
|
}
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
if (result === null || result === void 0 || typeof result !== "object") return false;
|
|
2104
|
-
let descriptor;
|
|
2549
|
+
function assertSandboxHttpRequestTargetAllowed(url) {
|
|
2550
|
+
let parsed;
|
|
2105
2551
|
try {
|
|
2106
|
-
|
|
2552
|
+
parsed = new URL(url);
|
|
2107
2553
|
} catch {
|
|
2108
|
-
|
|
2554
|
+
throw new SsrFBlockedError("Invalid URL supplied to sandbox http/request");
|
|
2109
2555
|
}
|
|
2110
|
-
|
|
2556
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new SsrFBlockedError(`Blocked non-HTTP(S) protocol in sandbox http/request: ${parsed.protocol}`);
|
|
2557
|
+
if (isBlockedHostnameOrIp(parsed.hostname)) throw new SsrFBlockedError(`Blocked hostname or private/internal IP in sandbox http/request: ${parsed.hostname}`);
|
|
2558
|
+
}
|
|
2559
|
+
async function runSandboxHttpRequest(execServer, params) {
|
|
2560
|
+
const result = await requireBackend(execServer).runShellCommand({
|
|
2561
|
+
script: SANDBOX_HTTP_REQUEST_SCRIPT,
|
|
2562
|
+
stdin: JSON.stringify(params),
|
|
2563
|
+
allowFailure: true
|
|
2564
|
+
});
|
|
2565
|
+
if (result.code !== 0) {
|
|
2566
|
+
const stderr = result.stderr.toString("utf8").trim();
|
|
2567
|
+
throw new Error(stderr || `sandbox http/request failed with code ${result.code}`);
|
|
2568
|
+
}
|
|
2569
|
+
const parsed = JSON.parse(result.stdout.toString("utf8"));
|
|
2570
|
+
if (typeof parsed.status !== "number" || !Array.isArray(parsed.headers)) throw new Error("sandbox http/request returned an invalid response envelope");
|
|
2571
|
+
return {
|
|
2572
|
+
status: parsed.status,
|
|
2573
|
+
headers: readHttpHeaders(parsed.headers),
|
|
2574
|
+
bodyBase64: typeof parsed.bodyBase64 === "string" ? parsed.bodyBase64 : ""
|
|
2575
|
+
};
|
|
2111
2576
|
}
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
const
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
if (!params.signal) return bindDecision(await waitPromise);
|
|
2120
|
-
let onAbort;
|
|
2121
|
-
const abortPromise = new Promise((_, reject) => {
|
|
2122
|
-
if (params.signal.aborted) {
|
|
2123
|
-
reject(toLintErrorObject(params.signal.reason, "Non-Error rejection"));
|
|
2124
|
-
return;
|
|
2125
|
-
}
|
|
2126
|
-
onAbort = () => reject(toLintErrorObject(params.signal.reason, "Non-Error rejection"));
|
|
2127
|
-
params.signal.addEventListener("abort", onAbort, { once: true });
|
|
2577
|
+
async function runStreamingSandboxHttpRequest(execServer, socket, requestId, params) {
|
|
2578
|
+
const backend = requireBackend(execServer);
|
|
2579
|
+
const execSpec = await backend.buildExecSpec({
|
|
2580
|
+
command: SANDBOX_HTTP_REQUEST_SCRIPT,
|
|
2581
|
+
workdir: execServer.sandbox.containerWorkdir,
|
|
2582
|
+
env: {},
|
|
2583
|
+
usePty: false
|
|
2128
2584
|
});
|
|
2585
|
+
let child;
|
|
2129
2586
|
try {
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2587
|
+
const [command, ...args] = execSpec.argv;
|
|
2588
|
+
if (!command) throw new Error("OpenClaw sandbox HTTP exec spec did not provide a command.");
|
|
2589
|
+
child = spawn(command, args, {
|
|
2590
|
+
env: execSpec.env,
|
|
2591
|
+
stdio: [
|
|
2592
|
+
"pipe",
|
|
2593
|
+
"pipe",
|
|
2594
|
+
"pipe"
|
|
2595
|
+
]
|
|
2596
|
+
});
|
|
2597
|
+
} catch (error) {
|
|
2598
|
+
try {
|
|
2599
|
+
await backend.finalizeExec?.({
|
|
2600
|
+
status: "failed",
|
|
2601
|
+
exitCode: null,
|
|
2602
|
+
timedOut: false,
|
|
2603
|
+
token: execSpec.finalizeToken
|
|
2604
|
+
});
|
|
2605
|
+
} catch (finalizeError) {
|
|
2606
|
+
embeddedAgentLog.warn("codex sandbox http/request finalize after start failure failed", { error: finalizeError });
|
|
2607
|
+
}
|
|
2608
|
+
throw error;
|
|
2133
2609
|
}
|
|
2610
|
+
const abortOnSocketClose = () => child.kill("SIGTERM");
|
|
2611
|
+
socket.once("close", abortOnSocketClose);
|
|
2612
|
+
child.once("close", () => {
|
|
2613
|
+
socket.off("close", abortOnSocketClose);
|
|
2614
|
+
});
|
|
2615
|
+
child.stdin.on("error", (error) => {
|
|
2616
|
+
if (error.code === "EPIPE" || error.code === "ERR_STREAM_DESTROYED") return;
|
|
2617
|
+
embeddedAgentLog.warn("codex sandbox http/request stdin write failed", { error });
|
|
2618
|
+
});
|
|
2619
|
+
child.stdin.end(JSON.stringify(params));
|
|
2620
|
+
return await readStreamingSandboxHttpResponse({
|
|
2621
|
+
child,
|
|
2622
|
+
execSpec,
|
|
2623
|
+
finalizeExec: backend.finalizeExec,
|
|
2624
|
+
requestId,
|
|
2625
|
+
socket
|
|
2626
|
+
});
|
|
2134
2627
|
}
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2628
|
+
function readStreamingSandboxHttpResponse(params) {
|
|
2629
|
+
return new Promise((resolve, reject) => {
|
|
2630
|
+
let headerResolved = false;
|
|
2631
|
+
let failed = false;
|
|
2632
|
+
let childFailure = null;
|
|
2633
|
+
let lastBodySeq = 0;
|
|
2634
|
+
let stdoutBuffer = "";
|
|
2635
|
+
let stderr = "";
|
|
2636
|
+
const finalize = async (status, exitCode) => {
|
|
2637
|
+
await params.finalizeExec?.({
|
|
2638
|
+
status,
|
|
2639
|
+
exitCode,
|
|
2640
|
+
timedOut: false,
|
|
2641
|
+
token: params.execSpec.finalizeToken
|
|
2642
|
+
});
|
|
2643
|
+
};
|
|
2644
|
+
const fail = (message, exitCode) => {
|
|
2645
|
+
if (failed) return;
|
|
2646
|
+
failed = true;
|
|
2647
|
+
finalize("failed", exitCode).catch((error) => {
|
|
2648
|
+
embeddedAgentLog.warn("codex sandbox http/request finalize failed", { error });
|
|
2649
|
+
});
|
|
2650
|
+
if (headerResolved) {
|
|
2651
|
+
sendHttpBodyDelta(params.socket, {
|
|
2652
|
+
requestId: params.requestId,
|
|
2653
|
+
seq: lastBodySeq + 1,
|
|
2654
|
+
deltaBase64: "",
|
|
2655
|
+
done: true,
|
|
2656
|
+
error: message
|
|
2657
|
+
});
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
reject(new Error(message));
|
|
2661
|
+
};
|
|
2662
|
+
params.child.stdout.setEncoding("utf8");
|
|
2663
|
+
params.child.stdout.on("data", (chunk) => {
|
|
2664
|
+
stdoutBuffer += chunk;
|
|
2665
|
+
let newline = stdoutBuffer.indexOf("\n");
|
|
2666
|
+
while (newline >= 0) {
|
|
2667
|
+
const line = stdoutBuffer.slice(0, newline).trim();
|
|
2668
|
+
stdoutBuffer = stdoutBuffer.slice(newline + 1);
|
|
2669
|
+
if (line) try {
|
|
2670
|
+
const message = requireObject(JSON.parse(line), "http stream message");
|
|
2671
|
+
const type = requireString(message.type, "http stream message type");
|
|
2672
|
+
if (type === "headers") {
|
|
2673
|
+
headerResolved = true;
|
|
2674
|
+
resolve({
|
|
2675
|
+
status: requireNumber(message.status, "http status"),
|
|
2676
|
+
headers: readHttpHeaders(message.headers),
|
|
2677
|
+
bodyBase64: ""
|
|
2678
|
+
});
|
|
2679
|
+
} else if (type === "bodyDelta") {
|
|
2680
|
+
const seq = requireNumber(message.seq, "http body sequence");
|
|
2681
|
+
lastBodySeq = Math.max(lastBodySeq, seq);
|
|
2682
|
+
sendHttpBodyDelta(params.socket, {
|
|
2683
|
+
requestId: params.requestId,
|
|
2684
|
+
seq,
|
|
2685
|
+
deltaBase64: typeof message.deltaBase64 === "string" ? message.deltaBase64 : "",
|
|
2686
|
+
done: message.done === true,
|
|
2687
|
+
error: typeof message.error === "string" ? message.error : null
|
|
2688
|
+
});
|
|
2689
|
+
}
|
|
2690
|
+
} catch (error) {
|
|
2691
|
+
fail(error instanceof Error ? error.message : String(error), null);
|
|
2692
|
+
}
|
|
2693
|
+
newline = stdoutBuffer.indexOf("\n");
|
|
2694
|
+
}
|
|
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
|
+
}
|
|
2699
|
+
});
|
|
2700
|
+
params.child.stderr.setEncoding("utf8");
|
|
2701
|
+
params.child.stderr.on("data", (chunk) => {
|
|
2702
|
+
stderr = sliceUtf16Safe(`${stderr}${chunk}`, -4096);
|
|
2703
|
+
});
|
|
2704
|
+
params.child.once("error", (error) => {
|
|
2705
|
+
childFailure ??= error.message;
|
|
2706
|
+
});
|
|
2707
|
+
params.child.once("close", (code) => {
|
|
2708
|
+
const exitCode = code ?? 1;
|
|
2709
|
+
if (failed) return;
|
|
2710
|
+
if (childFailure) {
|
|
2711
|
+
fail(childFailure, exitCode);
|
|
2712
|
+
return;
|
|
2713
|
+
}
|
|
2714
|
+
if (exitCode === 0) {
|
|
2715
|
+
finalize("completed", exitCode).catch((error) => {
|
|
2716
|
+
embeddedAgentLog.warn("codex sandbox http/request finalize failed", { error });
|
|
2717
|
+
});
|
|
2718
|
+
if (!headerResolved) reject(/* @__PURE__ */ new Error("sandbox http/request exited before returning headers"));
|
|
2719
|
+
return;
|
|
2720
|
+
}
|
|
2721
|
+
fail(stderr.trim() || `sandbox http/request failed with code ${exitCode}`, exitCode);
|
|
2722
|
+
});
|
|
2723
|
+
});
|
|
2144
2724
|
}
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2725
|
+
const SANDBOX_HTTP_REQUEST_SCRIPT = String.raw`
|
|
2726
|
+
tmp=$(mktemp "$TMPDIR/openclaw-http.XXXXXX.py" 2>/dev/null || mktemp "/tmp/openclaw-http.XXXXXX.py") || exit 1
|
|
2727
|
+
trap 'rm -f "$tmp"' EXIT
|
|
2728
|
+
cat > "$tmp" <<'PY'
|
|
2729
|
+
import base64
|
|
2730
|
+
import json
|
|
2731
|
+
import ipaddress
|
|
2732
|
+
import socket
|
|
2733
|
+
import sys
|
|
2734
|
+
import urllib.error
|
|
2735
|
+
import urllib.parse
|
|
2736
|
+
import urllib.request
|
|
2737
|
+
|
|
2738
|
+
def emit(payload):
|
|
2739
|
+
print(json.dumps(payload, separators=(",", ":")), flush=True)
|
|
2740
|
+
|
|
2741
|
+
def response_headers(response):
|
|
2742
|
+
return [{"name": name, "value": value} for name, value in response.headers.items()]
|
|
2743
|
+
|
|
2744
|
+
BLOCKED_HOSTNAMES = {
|
|
2745
|
+
"localhost",
|
|
2746
|
+
"localhost.localdomain",
|
|
2747
|
+
"metadata.google.internal",
|
|
2748
|
+
}
|
|
2749
|
+
CLOUD_METADATA_IP_ADDRESSES = {
|
|
2750
|
+
"100.100.100.200",
|
|
2751
|
+
"fd00:ec2::254",
|
|
2752
|
+
}
|
|
2753
|
+
BLOCKED_IPV4_NETWORKS = tuple(
|
|
2754
|
+
ipaddress.ip_network(network)
|
|
2755
|
+
for network in (
|
|
2756
|
+
"100.64.0.0/10",
|
|
2757
|
+
"198.18.0.0/15",
|
|
2758
|
+
)
|
|
2759
|
+
)
|
|
2760
|
+
BLOCKED_IPV6_NETWORKS = tuple(
|
|
2761
|
+
ipaddress.ip_network(network)
|
|
2762
|
+
for network in (
|
|
2763
|
+
"100::/64",
|
|
2764
|
+
"2001:2::/48",
|
|
2765
|
+
"2001:20::/28",
|
|
2766
|
+
"2001:db8::/32",
|
|
2767
|
+
"fec0::/10",
|
|
2768
|
+
)
|
|
2769
|
+
)
|
|
2770
|
+
PINNED_ADDRESSES = {}
|
|
2771
|
+
|
|
2772
|
+
def normalize_hostname(hostname):
|
|
2773
|
+
return (hostname or "").strip("[]").rstrip(".").lower()
|
|
2774
|
+
|
|
2775
|
+
def is_blocked_hostname(hostname):
|
|
2776
|
+
normalized = normalize_hostname(hostname)
|
|
2777
|
+
return (
|
|
2778
|
+
normalized in BLOCKED_HOSTNAMES
|
|
2779
|
+
or normalized.endswith(".localhost")
|
|
2780
|
+
or normalized.endswith(".local")
|
|
2781
|
+
or normalized.endswith(".internal")
|
|
2782
|
+
)
|
|
2783
|
+
|
|
2784
|
+
def is_blocked_ip(address):
|
|
2785
|
+
try:
|
|
2786
|
+
parsed = ipaddress.ip_address(address)
|
|
2787
|
+
except ValueError:
|
|
2788
|
+
return False
|
|
2789
|
+
embedded_ipv4 = extract_embedded_ipv4(parsed)
|
|
2790
|
+
if embedded_ipv4 is not None and is_blocked_ip(str(embedded_ipv4)):
|
|
2791
|
+
return True
|
|
2792
|
+
if str(parsed).lower() in CLOUD_METADATA_IP_ADDRESSES:
|
|
2793
|
+
return True
|
|
2794
|
+
if isinstance(parsed, ipaddress.IPv4Address):
|
|
2795
|
+
if any(parsed in network for network in BLOCKED_IPV4_NETWORKS):
|
|
2796
|
+
return True
|
|
2797
|
+
else:
|
|
2798
|
+
if any(parsed in network for network in BLOCKED_IPV6_NETWORKS):
|
|
2799
|
+
return True
|
|
2800
|
+
return (
|
|
2801
|
+
parsed.is_loopback
|
|
2802
|
+
or parsed.is_private
|
|
2803
|
+
or parsed.is_link_local
|
|
2804
|
+
or parsed.is_multicast
|
|
2805
|
+
or parsed.is_reserved
|
|
2806
|
+
or parsed.is_unspecified
|
|
2807
|
+
)
|
|
2808
|
+
|
|
2809
|
+
def ipv4_from_int(value):
|
|
2810
|
+
return ipaddress.IPv4Address(value & 0xffffffff)
|
|
2811
|
+
|
|
2812
|
+
def extract_embedded_ipv4(address):
|
|
2813
|
+
if not isinstance(address, ipaddress.IPv6Address):
|
|
2814
|
+
return None
|
|
2815
|
+
if address.ipv4_mapped is not None:
|
|
2816
|
+
return address.ipv4_mapped
|
|
2817
|
+
value = int(address)
|
|
2818
|
+
hextets = [(value >> shift) & 0xffff for shift in range(112, -1, -16)]
|
|
2819
|
+
if hextets[:6] == [0, 0, 0, 0, 0, 0]:
|
|
2820
|
+
return ipv4_from_int(value)
|
|
2821
|
+
if hextets[:6] == [0x64, 0xff9b, 0, 0, 0, 0]:
|
|
2822
|
+
return ipv4_from_int(value)
|
|
2823
|
+
if hextets[:6] == [0x64, 0xff9b, 1, 0, 0, 0]:
|
|
2824
|
+
return ipv4_from_int(value)
|
|
2825
|
+
if hextets[0] == 0x2002:
|
|
2826
|
+
return ipv4_from_int((hextets[1] << 16) | hextets[2])
|
|
2827
|
+
if hextets[0] == 0x2001 and hextets[1] == 0:
|
|
2828
|
+
return ipv4_from_int(((hextets[6] << 16) | hextets[7]) ^ 0xffffffff)
|
|
2829
|
+
if (hextets[4] & 0xfcff) == 0 and hextets[5] == 0x5efe:
|
|
2830
|
+
return ipv4_from_int((hextets[6] << 16) | hextets[7])
|
|
2831
|
+
return None
|
|
2832
|
+
|
|
2833
|
+
def assert_url_allowed(url):
|
|
2834
|
+
parsed = urllib.parse.urlparse(url)
|
|
2835
|
+
if parsed.scheme not in ("http", "https"):
|
|
2836
|
+
raise ValueError("http/request only supports http and https URLs")
|
|
2837
|
+
hostname = normalize_hostname(parsed.hostname)
|
|
2838
|
+
if not hostname or is_blocked_hostname(hostname) or is_blocked_ip(hostname):
|
|
2839
|
+
raise ValueError("Blocked hostname or private/internal/special-use IP address")
|
|
2840
|
+
try:
|
|
2841
|
+
results = socket.getaddrinfo(hostname, parsed.port, proto=socket.IPPROTO_TCP)
|
|
2842
|
+
except socket.gaierror as error:
|
|
2843
|
+
raise ValueError(f"Unable to resolve hostname: {hostname}") from error
|
|
2844
|
+
addresses = {entry[4][0] for entry in results if entry[4]}
|
|
2845
|
+
if not addresses or any(is_blocked_ip(address) for address in addresses):
|
|
2846
|
+
raise ValueError("Blocked: resolves to private/internal/special-use IP address")
|
|
2847
|
+
PINNED_ADDRESSES[hostname] = sorted(addresses)
|
|
2848
|
+
|
|
2849
|
+
class GuardedRedirectHandler(urllib.request.HTTPRedirectHandler):
|
|
2850
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
2851
|
+
assert_url_allowed(newurl)
|
|
2852
|
+
return super().redirect_request(req, fp, code, msg, headers, newurl)
|
|
2853
|
+
|
|
2854
|
+
def pinned_getaddrinfo(original_getaddrinfo):
|
|
2855
|
+
def getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
|
|
2856
|
+
pinned = PINNED_ADDRESSES.get(normalize_hostname(host))
|
|
2857
|
+
if not pinned:
|
|
2858
|
+
return original_getaddrinfo(host, port, family, type, proto, flags)
|
|
2859
|
+
results = []
|
|
2860
|
+
for address in pinned:
|
|
2861
|
+
results.extend(original_getaddrinfo(address, port, family, type, proto, flags))
|
|
2862
|
+
return results
|
|
2863
|
+
return getaddrinfo
|
|
2864
|
+
|
|
2865
|
+
def handle_response(input_data, response):
|
|
2866
|
+
headers = response_headers(response)
|
|
2867
|
+
status = int(getattr(response, "status", getattr(response, "code", 0)))
|
|
2868
|
+
if input_data.get("streamResponse"):
|
|
2869
|
+
emit({"type": "headers", "status": status, "headers": headers})
|
|
2870
|
+
seq = 1
|
|
2871
|
+
while True:
|
|
2872
|
+
chunk = response.read(65536)
|
|
2873
|
+
if not chunk:
|
|
2874
|
+
break
|
|
2875
|
+
emit({
|
|
2876
|
+
"type": "bodyDelta",
|
|
2877
|
+
"seq": seq,
|
|
2878
|
+
"deltaBase64": base64.b64encode(chunk).decode("ascii"),
|
|
2879
|
+
"done": False,
|
|
2880
|
+
})
|
|
2881
|
+
seq += 1
|
|
2882
|
+
emit({"type": "bodyDelta", "seq": seq, "deltaBase64": "", "done": True})
|
|
2883
|
+
return
|
|
2884
|
+
body = response.read()
|
|
2885
|
+
emit({
|
|
2886
|
+
"status": status,
|
|
2887
|
+
"headers": headers,
|
|
2888
|
+
"bodyBase64": base64.b64encode(body).decode("ascii"),
|
|
2889
|
+
})
|
|
2890
|
+
|
|
2891
|
+
def main():
|
|
2892
|
+
input_data = json.load(sys.stdin)
|
|
2893
|
+
url = str(input_data.get("url", ""))
|
|
2894
|
+
assert_url_allowed(url)
|
|
2895
|
+
body_base64 = input_data.get("bodyBase64")
|
|
2896
|
+
data = base64.b64decode(body_base64) if isinstance(body_base64, str) else None
|
|
2897
|
+
request = urllib.request.Request(
|
|
2898
|
+
url,
|
|
2899
|
+
data=data,
|
|
2900
|
+
method=str(input_data.get("method", "GET")),
|
|
2901
|
+
)
|
|
2902
|
+
for header in input_data.get("headers") or []:
|
|
2903
|
+
request.add_header(str(header.get("name", "")), str(header.get("value", "")))
|
|
2904
|
+
timeout_ms = input_data.get("timeoutMs")
|
|
2905
|
+
timeout = None
|
|
2906
|
+
if isinstance(timeout_ms, (int, float)) and timeout_ms > 0:
|
|
2907
|
+
timeout = timeout_ms / 1000
|
|
2908
|
+
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), GuardedRedirectHandler)
|
|
2909
|
+
original_getaddrinfo = socket.getaddrinfo
|
|
2910
|
+
socket.getaddrinfo = pinned_getaddrinfo(original_getaddrinfo)
|
|
2911
|
+
try:
|
|
2912
|
+
with opener.open(request, timeout=timeout) as response:
|
|
2913
|
+
handle_response(input_data, response)
|
|
2914
|
+
except urllib.error.HTTPError as response:
|
|
2915
|
+
handle_response(input_data, response)
|
|
2916
|
+
finally:
|
|
2917
|
+
socket.getaddrinfo = original_getaddrinfo
|
|
2918
|
+
|
|
2919
|
+
if __name__ == "__main__":
|
|
2920
|
+
main()
|
|
2921
|
+
PY
|
|
2922
|
+
python3 "$tmp"
|
|
2923
|
+
`.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
|
+
}));
|
|
2151
2937
|
}
|
|
2152
2938
|
//#endregion
|
|
2153
|
-
//#region extensions/codex/src/app-server/
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
const
|
|
2159
|
-
const
|
|
2160
|
-
const
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
const
|
|
2164
|
-
const
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
const ANSI_OSC_SEQUENCE_RE$1 = new RegExp(String.raw`(?:\u001b]|\u009d)[^\u001b\u009c\u0007]*(?:\u0007|\u001b\\|\u009c)`, "g");
|
|
2199
|
-
const ANSI_CONTROL_SEQUENCE_RE$1 = new RegExp(String.raw`(?:\u001b\[[0-?]*[ -/]*[@-~]|\u009b[0-?]*[ -/]*[@-~]|\u001b[@-Z\\-_])`, "g");
|
|
2200
|
-
const CONTROL_CHARACTER_RE$1 = new RegExp(String.raw`[\u0000-\u001f\u007f-\u009f]+`, "g");
|
|
2201
|
-
const INVISIBLE_FORMATTING_CONTROL_RE$1 = new RegExp(String.raw`[\u00ad\u034f\u061c\u200b-\u200f\u202a-\u202e\u2060-\u206f\ufeff\ufe00-\ufe0f\u{e0100}-\u{e01ef}]`, "gu");
|
|
2202
|
-
const DANGLING_TERMINAL_SEQUENCE_SUFFIX_RE$1 = new RegExp(String.raw`(?:\u001b\][^\u001b\u009c\u0007]*|\u009d[^\u001b\u009c\u0007]*|\u001b\[[0-?]*[ -/]*|\u009b[0-?]*[ -/]*|\u001b)$`);
|
|
2203
|
-
async function handleCodexAppServerElicitationRequest(params) {
|
|
2204
|
-
const requestParams = isJsonObject(params.requestParams) ? params.requestParams : void 0;
|
|
2205
|
-
if (!requestParams) return;
|
|
2206
|
-
if (!matchesCurrentThread(requestParams, params.threadId)) return;
|
|
2207
|
-
if (turnIdMismatches(requestParams, params.turnId)) return;
|
|
2208
|
-
const pluginResolution = resolvePluginElicitation({
|
|
2209
|
-
requestParams,
|
|
2210
|
-
pluginAppPolicyContext: params.pluginAppPolicyContext
|
|
2211
|
-
});
|
|
2212
|
-
if (pluginResolution.kind !== "not_plugin") {
|
|
2213
|
-
if (pluginResolution.kind === "decline") {
|
|
2214
|
-
logPluginElicitationDecline(pluginResolution.reason, requestParams);
|
|
2215
|
-
return declineElicitationResponse();
|
|
2216
|
-
}
|
|
2217
|
-
if (!hasExactTurnId(requestParams, params.turnId)) {
|
|
2218
|
-
logPluginElicitationDecline("missing_active_turn", requestParams);
|
|
2219
|
-
return declineElicitationResponse();
|
|
2939
|
+
//#region extensions/codex/src/app-server/sandbox-exec-server/processes.ts
|
|
2940
|
+
/**
|
|
2941
|
+
* Manages subprocess lifecycle, streaming output buffers, stdin writes, and
|
|
2942
|
+
* termination for Codex sandbox exec-server process RPCs.
|
|
2943
|
+
*/
|
|
2944
|
+
const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
2945
|
+
const RETAINED_PROCESS_OUTPUT_BYTES = 1024 * 1024;
|
|
2946
|
+
const CLOSED_PROCESS_EVICTION_MS = 6e4;
|
|
2947
|
+
/** Starts a sandbox-backed process and registers it in the connection-local process table. */
|
|
2948
|
+
async function startProcess(execServer, processes, socket, params) {
|
|
2949
|
+
const record = requireObject(params, "process/start params");
|
|
2950
|
+
const processId = requireString(record.processId, "processId");
|
|
2951
|
+
if (processes.has(processId)) throw new Error(`process already exists: ${processId}`);
|
|
2952
|
+
const argv = requireStringArray(record.argv, "argv");
|
|
2953
|
+
const cwd = resolveExecServerPath(requireString(record.cwd, "cwd"), "process cwd");
|
|
2954
|
+
rejectUnsupportedArg0(record.arg0);
|
|
2955
|
+
const env = readProcessEnv(record);
|
|
2956
|
+
const managed = {
|
|
2957
|
+
processId,
|
|
2958
|
+
chunks: [],
|
|
2959
|
+
retainedOutputBytes: 0,
|
|
2960
|
+
nextSeq: 1,
|
|
2961
|
+
exited: false,
|
|
2962
|
+
exitCode: null,
|
|
2963
|
+
closed: false,
|
|
2964
|
+
failure: null,
|
|
2965
|
+
tty: record.tty === true,
|
|
2966
|
+
pipeStdin: record.pipeStdin === true,
|
|
2967
|
+
abortController: new AbortController(),
|
|
2968
|
+
child: null,
|
|
2969
|
+
finalized: false,
|
|
2970
|
+
waiters: [],
|
|
2971
|
+
emitNotification: (method, notificationParams) => {
|
|
2972
|
+
if (socket.readyState === 1) socket.send(JSON.stringify({
|
|
2973
|
+
jsonrpc: "2.0",
|
|
2974
|
+
method,
|
|
2975
|
+
params: notificationParams
|
|
2976
|
+
}));
|
|
2977
|
+
},
|
|
2978
|
+
evictProcess: () => {
|
|
2979
|
+
if (managed.evictionTimer) return;
|
|
2980
|
+
managed.evictionTimer = setTimeout(() => {
|
|
2981
|
+
if (processes.get(processId) === managed && managed.closed) processes.delete(processId);
|
|
2982
|
+
}, CLOSED_PROCESS_EVICTION_MS);
|
|
2983
|
+
managed.evictionTimer.unref?.();
|
|
2220
2984
|
}
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2985
|
+
};
|
|
2986
|
+
processes.set(processId, managed);
|
|
2987
|
+
try {
|
|
2988
|
+
await runProcess(execServer, managed, {
|
|
2989
|
+
argv,
|
|
2990
|
+
cwd,
|
|
2991
|
+
env
|
|
2226
2992
|
});
|
|
2993
|
+
} catch (error) {
|
|
2994
|
+
processes.delete(processId);
|
|
2995
|
+
managed.failure = coerceErrorMessage(error);
|
|
2996
|
+
managed.exitCode = null;
|
|
2997
|
+
managed.exited = true;
|
|
2998
|
+
managed.closed = true;
|
|
2999
|
+
notifyProcessWaiters(managed);
|
|
3000
|
+
throw error;
|
|
2227
3001
|
}
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
3002
|
+
return { processId };
|
|
3003
|
+
}
|
|
3004
|
+
async function runProcess(execServer, managed, params) {
|
|
3005
|
+
const backend = execServer.sandbox.backend;
|
|
3006
|
+
if (!backend) throw new Error("OpenClaw sandbox backend is unavailable.");
|
|
3007
|
+
throwIfProcessStartCancelled(managed);
|
|
3008
|
+
const execSpec = await backend.buildExecSpec({
|
|
3009
|
+
command: shellCommandFromArgv(params.argv),
|
|
3010
|
+
workdir: params.cwd,
|
|
3011
|
+
env: params.env,
|
|
3012
|
+
usePty: false
|
|
3013
|
+
});
|
|
3014
|
+
managed.finalizeToken = execSpec.finalizeToken;
|
|
3015
|
+
managed.finalizeExec = backend.finalizeExec;
|
|
3016
|
+
let child;
|
|
3017
|
+
try {
|
|
3018
|
+
if (managed.abortController.signal.aborted) throw new Error("process start cancelled");
|
|
3019
|
+
const [command, ...args] = execSpec.argv;
|
|
3020
|
+
if (!command) throw new Error("OpenClaw sandbox exec spec did not provide a command.");
|
|
3021
|
+
child = spawn(command, args, {
|
|
3022
|
+
env: execSpec.env,
|
|
3023
|
+
stdio: [
|
|
3024
|
+
"pipe",
|
|
3025
|
+
"pipe",
|
|
3026
|
+
"pipe"
|
|
3027
|
+
]
|
|
3028
|
+
});
|
|
3029
|
+
} catch (error) {
|
|
3030
|
+
managed.failure = coerceErrorMessage(error);
|
|
3031
|
+
await finalizeProcess(managed).catch((finalizeError) => {
|
|
3032
|
+
embeddedAgentLog.warn("codex sandbox exec-server finalize after start failure failed", {
|
|
3033
|
+
processId: managed.processId,
|
|
3034
|
+
error: coerceErrorMessage(finalizeError)
|
|
3035
|
+
});
|
|
3036
|
+
});
|
|
3037
|
+
throw error;
|
|
3038
|
+
}
|
|
3039
|
+
managed.child = child;
|
|
3040
|
+
const abortListener = () => child.kill("SIGTERM");
|
|
3041
|
+
managed.abortController.signal.addEventListener("abort", abortListener, { once: true });
|
|
3042
|
+
child.stdout.on("data", (chunk) => appendProcessChunk(managed, managed.tty ? "pty" : "stdout", chunk));
|
|
3043
|
+
child.stderr.on("data", (chunk) => appendProcessChunk(managed, "stderr", chunk));
|
|
3044
|
+
child.once("error", (error) => {
|
|
3045
|
+
managed.failure ??= error.message;
|
|
3046
|
+
notifyProcessWaiters(managed);
|
|
3047
|
+
});
|
|
3048
|
+
child.once("close", (code) => {
|
|
3049
|
+
managed.abortController.signal.removeEventListener("abort", abortListener);
|
|
3050
|
+
emitProcessClosed(managed, code ?? 1);
|
|
3051
|
+
});
|
|
3052
|
+
if (!managed.tty && !managed.pipeStdin) child.stdin.end();
|
|
2245
3053
|
}
|
|
2246
|
-
function
|
|
2247
|
-
|
|
3054
|
+
function throwIfProcessStartCancelled(managed) {
|
|
3055
|
+
if (managed.abortController.signal.aborted) throw new Error("process start cancelled");
|
|
2248
3056
|
}
|
|
2249
|
-
function
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
const pluginEntries = entries.filter(isPluginAppPolicyContextEntry);
|
|
2256
|
-
const appId = readFirstString$1(meta, PLUGIN_APP_ID_META_KEYS) ?? readFirstString$1(requestParams, PLUGIN_APP_ID_META_KEYS);
|
|
2257
|
-
const connectorId = readFirstString$1(meta, PLUGIN_CONNECTOR_ID_META_KEYS);
|
|
2258
|
-
const isCodexConnectorApproval = isCodexConnectorApprovalElicitation(requestParams, meta);
|
|
2259
|
-
if (isCodexConnectorApproval && appId && connectorId && appId !== connectorId) return {
|
|
2260
|
-
kind: "decline",
|
|
2261
|
-
reason: "app_id_connector_id_mismatch"
|
|
3057
|
+
function appendProcessChunk(managed, stream, data) {
|
|
3058
|
+
if (data.length === 0) return;
|
|
3059
|
+
const chunk = {
|
|
3060
|
+
seq: managed.nextSeq,
|
|
3061
|
+
stream,
|
|
3062
|
+
chunk: data.toString("base64")
|
|
2262
3063
|
};
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
if (entry?.source === "account" && !isCodexConnectorApproval) return {
|
|
2270
|
-
kind: "decline",
|
|
2271
|
-
reason: "account_app_source_mismatch"
|
|
2272
|
-
};
|
|
2273
|
-
return uniquePluginMatch(entry ? [entry] : [], "app_id");
|
|
3064
|
+
managed.chunks.push(chunk);
|
|
3065
|
+
managed.retainedOutputBytes += data.length;
|
|
3066
|
+
while (managed.retainedOutputBytes > RETAINED_PROCESS_OUTPUT_BYTES && managed.chunks.length > 1) {
|
|
3067
|
+
const removed = managed.chunks.shift();
|
|
3068
|
+
if (!removed) break;
|
|
3069
|
+
managed.retainedOutputBytes -= Buffer.from(removed.chunk, "base64").byteLength;
|
|
2274
3070
|
}
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
3071
|
+
managed.nextSeq += 1;
|
|
3072
|
+
managed.emitNotification("process/output", {
|
|
3073
|
+
processId: managed.processId,
|
|
3074
|
+
seq: chunk.seq,
|
|
3075
|
+
stream: chunk.stream,
|
|
3076
|
+
chunk: chunk.chunk
|
|
3077
|
+
});
|
|
3078
|
+
notifyProcessWaiters(managed);
|
|
3079
|
+
}
|
|
3080
|
+
function emitProcessClosed(managed, exitCode) {
|
|
3081
|
+
if (!managed.exited) {
|
|
3082
|
+
const exitSeq = managed.nextSeq;
|
|
3083
|
+
managed.nextSeq += 1;
|
|
3084
|
+
managed.exitCode = exitCode;
|
|
3085
|
+
managed.exited = true;
|
|
3086
|
+
if (exitCode !== null) managed.emitNotification("process/exited", {
|
|
3087
|
+
processId: managed.processId,
|
|
3088
|
+
seq: exitSeq,
|
|
3089
|
+
exitCode
|
|
3090
|
+
});
|
|
2282
3091
|
}
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
3092
|
+
if (!managed.closed) {
|
|
3093
|
+
const closeSeq = managed.nextSeq;
|
|
3094
|
+
managed.nextSeq += 1;
|
|
3095
|
+
managed.closed = true;
|
|
3096
|
+
managed.emitNotification("process/closed", {
|
|
3097
|
+
processId: managed.processId,
|
|
3098
|
+
seq: closeSeq
|
|
3099
|
+
});
|
|
2287
3100
|
}
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
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
|
+
});
|
|
2293
3108
|
});
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
3109
|
+
managed.evictProcess();
|
|
3110
|
+
notifyProcessWaiters(managed);
|
|
3111
|
+
}
|
|
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
|
+
function limitProcessChunks(chunks, maxBytes) {
|
|
3124
|
+
if (!maxBytes) return chunks;
|
|
3125
|
+
const retained = [];
|
|
3126
|
+
let retainedBytes = 0;
|
|
3127
|
+
for (const chunk of chunks) {
|
|
3128
|
+
const byteLength = Buffer.from(chunk.chunk, "base64").byteLength;
|
|
3129
|
+
if (retained.length > 0 && retainedBytes + byteLength > maxBytes) break;
|
|
3130
|
+
retained.push(chunk);
|
|
3131
|
+
retainedBytes += byteLength;
|
|
3132
|
+
if (retainedBytes >= maxBytes) break;
|
|
3133
|
+
}
|
|
3134
|
+
return retained;
|
|
3135
|
+
}
|
|
3136
|
+
/** Reads buffered process output, optionally waiting for new output or process close. */
|
|
3137
|
+
async function readProcess(processes, params) {
|
|
3138
|
+
const record = requireObject(params, "process/read params");
|
|
3139
|
+
const managed = requireProcess(processes, requireString(record.processId, "processId"));
|
|
3140
|
+
const afterSeq = typeof record.afterSeq === "number" ? record.afterSeq : 0;
|
|
3141
|
+
const waitMs = typeof record.waitMs === "number" && record.waitMs > 0 ? record.waitMs : 0;
|
|
3142
|
+
if (!managed.exited && !hasChunksAtOrAfter(managed, afterSeq) && waitMs > 0) await waitForProcessUpdate(managed, waitMs);
|
|
3143
|
+
const chunks = limitProcessChunks(managed.chunks.filter((chunk) => chunk.seq > afterSeq), typeof record.maxBytes === "number" && record.maxBytes > 0 ? record.maxBytes : void 0);
|
|
3144
|
+
const lastChunk = chunks.at(-1);
|
|
3145
|
+
return {
|
|
3146
|
+
chunks,
|
|
3147
|
+
nextSeq: lastChunk ? lastChunk.seq + 1 : managed.nextSeq,
|
|
3148
|
+
exited: managed.exited,
|
|
3149
|
+
exitCode: managed.exitCode,
|
|
3150
|
+
closed: managed.closed,
|
|
3151
|
+
failure: managed.failure
|
|
2298
3152
|
};
|
|
2299
|
-
return { kind: "not_plugin" };
|
|
2300
3153
|
}
|
|
2301
|
-
|
|
2302
|
-
|
|
3154
|
+
/** Writes base64 stdin data to a running process when stdin is still open. */
|
|
3155
|
+
function writeProcess(processes, params) {
|
|
3156
|
+
const record = requireObject(params, "process/write params");
|
|
3157
|
+
const processId = requireString(record.processId, "processId");
|
|
3158
|
+
const managed = processes.get(processId);
|
|
3159
|
+
if (!managed) return { status: "unknownProcess" };
|
|
3160
|
+
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);
|
|
3163
|
+
return { status: "accepted" };
|
|
3164
|
+
}
|
|
3165
|
+
/** Requests process termination and reports whether it was running at call time. */
|
|
3166
|
+
function terminateProcess(processes, params) {
|
|
3167
|
+
const processId = requireString(requireObject(params, "process/terminate params").processId, "processId");
|
|
3168
|
+
const managed = processes.get(processId);
|
|
3169
|
+
if (!managed) return { running: false };
|
|
3170
|
+
const running = !managed.exited;
|
|
3171
|
+
managed.abortController.abort();
|
|
3172
|
+
managed.child?.kill("SIGTERM");
|
|
3173
|
+
if (running && !managed.child) emitProcessClosed(managed, null);
|
|
3174
|
+
return { running };
|
|
3175
|
+
}
|
|
3176
|
+
function waitForProcessUpdate(managed, waitMs) {
|
|
3177
|
+
return new Promise((resolve) => {
|
|
3178
|
+
const timer = setTimeout(done, Math.min(waitMs, 3e4));
|
|
3179
|
+
function done() {
|
|
3180
|
+
clearTimeout(timer);
|
|
3181
|
+
managed.waiters = managed.waiters.filter((waiter) => waiter !== done);
|
|
3182
|
+
resolve();
|
|
3183
|
+
}
|
|
3184
|
+
managed.waiters.push(done);
|
|
3185
|
+
});
|
|
2303
3186
|
}
|
|
2304
|
-
function
|
|
2305
|
-
const
|
|
2306
|
-
|
|
2307
|
-
const
|
|
2308
|
-
if (!pluginName && !configKey) return { kind: "not_plugin" };
|
|
2309
|
-
if (!params.context) return {
|
|
2310
|
-
kind: "decline",
|
|
2311
|
-
reason: "missing_policy_context"
|
|
2312
|
-
};
|
|
2313
|
-
return uniquePluginMatch(params.entries.filter((entry) => {
|
|
2314
|
-
if (marketplaceName && entry.marketplaceName !== marketplaceName) return false;
|
|
2315
|
-
if (pluginName && entry.pluginName !== pluginName) return false;
|
|
2316
|
-
if (configKey && entry.configKey !== configKey) return false;
|
|
2317
|
-
return true;
|
|
2318
|
-
}), "metadata");
|
|
3187
|
+
function notifyProcessWaiters(managed) {
|
|
3188
|
+
const waiters = managed.waiters;
|
|
3189
|
+
managed.waiters = [];
|
|
3190
|
+
for (const waiter of waiters) waiter();
|
|
2319
3191
|
}
|
|
2320
|
-
function
|
|
2321
|
-
|
|
2322
|
-
kind: "matched",
|
|
2323
|
-
entry: matches[0]
|
|
2324
|
-
};
|
|
2325
|
-
return {
|
|
2326
|
-
kind: "decline",
|
|
2327
|
-
reason: matches.length === 0 ? `${source}_not_enabled` : `${source}_ambiguous`
|
|
2328
|
-
};
|
|
3192
|
+
function hasChunksAtOrAfter(managed, afterSeq) {
|
|
3193
|
+
return managed.chunks.some((chunk) => chunk.seq > afterSeq);
|
|
2329
3194
|
}
|
|
2330
|
-
function
|
|
2331
|
-
|
|
2332
|
-
if (!connectorName) return false;
|
|
2333
|
-
const normalized = normalizePluginIdentityText(connectorName);
|
|
2334
|
-
return entries.some((entry) => normalizePluginIdentityText(appPolicyDisplayName(entry)) === normalized || isPluginAppPolicyContextEntry(entry) && normalizePluginIdentityText(entry.configKey) === normalized);
|
|
3195
|
+
function shellCommandFromArgv(argv) {
|
|
3196
|
+
return argv.map(shellEscape).join(" ");
|
|
2335
3197
|
}
|
|
2336
|
-
function
|
|
2337
|
-
return
|
|
3198
|
+
function shellEscape(value) {
|
|
3199
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
2338
3200
|
}
|
|
2339
|
-
function
|
|
2340
|
-
|
|
3201
|
+
function requireProcess(processes, processId) {
|
|
3202
|
+
const managed = processes.get(processId);
|
|
3203
|
+
if (!managed) throw new Error(`unknown process: ${processId}`);
|
|
3204
|
+
return managed;
|
|
2341
3205
|
}
|
|
2342
|
-
function
|
|
2343
|
-
|
|
3206
|
+
function rejectUnsupportedArg0(value) {
|
|
3207
|
+
if (value === void 0 || value === null) return;
|
|
3208
|
+
if (typeof value === "string") throw new Error("Codex sandbox exec-server does not support arg0 overrides.");
|
|
3209
|
+
throw new Error("arg0 must be a string or null.");
|
|
2344
3210
|
}
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
}
|
|
2351
|
-
const approvalPrompt = readPluginApprovalElicitation(params.entry, params.requestParams);
|
|
2352
|
-
if (!approvalPrompt) {
|
|
2353
|
-
logPluginElicitationDecline("unsupported_schema", params.requestParams);
|
|
2354
|
-
return declineElicitationResponse();
|
|
2355
|
-
}
|
|
2356
|
-
const response = buildElicitationResponse(approvalPrompt, "approved-once");
|
|
2357
|
-
if (isJsonObject(response) && response.action === "accept") {
|
|
2358
|
-
if (mode === "allow") return response;
|
|
2359
|
-
return buildElicitationResponse(approvalPrompt, oneShotPluginPolicyApprovalOutcome(mode, await requestPluginApprovalOutcome({
|
|
2360
|
-
paramsForRun: params.paramsForRun,
|
|
2361
|
-
title: approvalPrompt.title,
|
|
2362
|
-
description: approvalPrompt.description,
|
|
2363
|
-
allowedDecisions: allowedPluginPolicyApprovalDecisions(mode, approvalPrompt),
|
|
2364
|
-
signal: params.signal
|
|
2365
|
-
})));
|
|
2366
|
-
}
|
|
2367
|
-
logPluginElicitationDecline("unmappable_schema", params.requestParams);
|
|
2368
|
-
return declineElicitationResponse();
|
|
3211
|
+
function readEnv(value) {
|
|
3212
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
3213
|
+
const env = {};
|
|
3214
|
+
for (const [key, rawValue] of Object.entries(value)) if (typeof rawValue === "string" && ENV_KEY_RE.test(key)) env[key] = rawValue;
|
|
3215
|
+
return env;
|
|
2369
3216
|
}
|
|
2370
|
-
function
|
|
2371
|
-
return
|
|
3217
|
+
function readProcessEnv(record) {
|
|
3218
|
+
return sanitizeEnvVars({
|
|
3219
|
+
...buildEnvFromPolicy(record.envPolicy),
|
|
3220
|
+
...readEnv(record.env)
|
|
3221
|
+
}).allowed;
|
|
2372
3222
|
}
|
|
2373
|
-
function
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
3223
|
+
function buildEnvFromPolicy(value) {
|
|
3224
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
3225
|
+
const policy = value;
|
|
3226
|
+
const inheritedEnv = readEnv(policy.set);
|
|
3227
|
+
const includeOnly = readStringList(policy.includeOnly);
|
|
3228
|
+
if (includeOnly.length > 0) filterEnvKeys(inheritedEnv, includeOnly, true);
|
|
3229
|
+
return inheritedEnv;
|
|
2377
3230
|
}
|
|
2378
|
-
function
|
|
2379
|
-
|
|
3231
|
+
function filterEnvKeys(env, patterns, keepMatches) {
|
|
3232
|
+
if (patterns.length === 0) return;
|
|
3233
|
+
const regexes = patterns.map((pattern) => wildcardPatternToRegex(pattern));
|
|
3234
|
+
for (const key of Object.keys(env)) if (regexes.some((regex) => regex.test(key)) !== keepMatches) delete env[key];
|
|
2380
3235
|
}
|
|
2381
|
-
function
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
3236
|
+
function wildcardPatternToRegex(pattern) {
|
|
3237
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/gu, "\\$&");
|
|
3238
|
+
return new RegExp(`^${escaped.replaceAll("*", ".*").replaceAll("?", ".")}$`, "iu");
|
|
3239
|
+
}
|
|
3240
|
+
function readStringList(value) {
|
|
3241
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
3242
|
+
}
|
|
3243
|
+
//#endregion
|
|
3244
|
+
//#region extensions/codex/src/app-server/sandbox-exec-server.ts
|
|
3245
|
+
/**
|
|
3246
|
+
* Hosts the local OpenClaw sandbox exec-server that Codex app-server native
|
|
3247
|
+
* execution can register as an external environment.
|
|
3248
|
+
*/
|
|
3249
|
+
const CODEX_SANDBOX_EXEC_SERVER_MAX_INBOUND_MESSAGE_BYTES = 100 * 1024 * 1024;
|
|
3250
|
+
/** Starts or reuses a sandbox exec-server and registers it with Codex app-server. */
|
|
3251
|
+
async function ensureCodexSandboxExecServerEnvironment(params) {
|
|
3252
|
+
if (!params.sandbox?.enabled || !params.sandbox.backend) return;
|
|
3253
|
+
if (!canExposeLocalExecServerToAppServer(params.appServerStartOptions)) throw new Error("OpenClaw Codex exec-server uses a local loopback URL and cannot be registered with a remote Codex app-server.");
|
|
3254
|
+
const execServer = await acquireOpenClawExecServer(params.sandbox);
|
|
3255
|
+
try {
|
|
3256
|
+
await params.client.request("environment/add", {
|
|
3257
|
+
environmentId: execServer.environmentId,
|
|
3258
|
+
execServerUrl: execServer.url
|
|
3259
|
+
}, {
|
|
3260
|
+
timeoutMs: params.timeoutMs,
|
|
3261
|
+
signal: params.signal
|
|
3262
|
+
});
|
|
3263
|
+
} catch (error) {
|
|
3264
|
+
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
|
+
throw error;
|
|
3270
|
+
}
|
|
2389
3271
|
return {
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
title,
|
|
2393
|
-
meta: descriptionMeta,
|
|
2394
|
-
requestedSchema,
|
|
2395
|
-
serverName: sanitizeOptionalDisplayText(readString$1(requestParams, "serverName"))
|
|
2396
|
-
}),
|
|
2397
|
-
requestedSchema,
|
|
2398
|
-
meta,
|
|
2399
|
-
persistHintsMode: "explicit",
|
|
2400
|
-
allowedDecisions: buildApprovalAllowedDecisions(requestedSchema, meta)
|
|
3272
|
+
environmentId: execServer.environmentId,
|
|
3273
|
+
cwd: params.sandbox.containerWorkdir
|
|
2401
3274
|
};
|
|
2402
3275
|
}
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
] : ["allow-once", "deny"];
|
|
3276
|
+
/** Releases the sandbox exec-server lease associated with a sandbox runtime. */
|
|
3277
|
+
async function releaseCodexSandboxExecServerEnvironment(sandbox) {
|
|
3278
|
+
if (!sandbox?.enabled) return;
|
|
3279
|
+
const server = await sandboxExecServerRegistry.servers.get(sandbox.runtimeId)?.catch(() => void 0);
|
|
3280
|
+
if (server) await releaseOpenClawExecServer(server);
|
|
2409
3281
|
}
|
|
2410
|
-
function
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
const properties = isJsonObject(requestedSchema.properties) ? requestedSchema.properties : {};
|
|
2414
|
-
return Object.entries(properties).some(([name, value]) => {
|
|
2415
|
-
const schema = isJsonObject(value) ? value : void 0;
|
|
2416
|
-
if (!schema) return false;
|
|
2417
|
-
return isPersistField({
|
|
2418
|
-
name,
|
|
2419
|
-
schema,
|
|
2420
|
-
required: false
|
|
2421
|
-
}) && chooseAlwaysPersistOptionValue(readEnumOptions(schema)) !== void 0;
|
|
2422
|
-
});
|
|
3282
|
+
function isEnvironmentAddUnsupported(error) {
|
|
3283
|
+
if (!(error instanceof Error)) return false;
|
|
3284
|
+
return error.message.includes("environment/add") && (error.message.includes("unknown variant") || error.message.includes("Method not found"));
|
|
2423
3285
|
}
|
|
2424
|
-
function
|
|
2425
|
-
return
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
3286
|
+
function canExposeLocalExecServerToAppServer(startOptions) {
|
|
3287
|
+
if (!startOptions || startOptions.transport !== "websocket") return true;
|
|
3288
|
+
if (typeof startOptions.url !== "string") return false;
|
|
3289
|
+
try {
|
|
3290
|
+
const host = new URL(startOptions.url).hostname.toLowerCase();
|
|
3291
|
+
const ipHost = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
|
|
3292
|
+
if (host === "localhost" || ipHost === "::1") return true;
|
|
3293
|
+
return isIP(ipHost) === 4 && ipHost.split(".")[0] === "127";
|
|
3294
|
+
} catch {
|
|
3295
|
+
return false;
|
|
3296
|
+
}
|
|
2430
3297
|
}
|
|
2431
|
-
function
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
3298
|
+
async function acquireOpenClawExecServer(sandbox) {
|
|
3299
|
+
const key = sandbox.runtimeId;
|
|
3300
|
+
while (true) {
|
|
3301
|
+
const promise = sandboxExecServerRegistry.servers.get(key) ?? startAndRememberOpenClawExecServer(sandbox);
|
|
3302
|
+
const server = await promise;
|
|
3303
|
+
if (!server.closed && sandboxExecServerRegistry.servers.get(key) === promise) {
|
|
3304
|
+
server.refCount += 1;
|
|
3305
|
+
return server;
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
2437
3308
|
}
|
|
2438
|
-
function
|
|
2439
|
-
|
|
2440
|
-
const
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
title,
|
|
2447
|
-
meta: requestParams["_meta"],
|
|
2448
|
-
requestedSchema,
|
|
2449
|
-
serverName: sanitizeOptionalDisplayText(readString$1(requestParams, "serverName"))
|
|
2450
|
-
}),
|
|
2451
|
-
requestedSchema,
|
|
2452
|
-
meta: requestParams["_meta"]
|
|
2453
|
-
};
|
|
3309
|
+
function startAndRememberOpenClawExecServer(sandbox) {
|
|
3310
|
+
const created = startOpenClawExecServer(sandbox);
|
|
3311
|
+
const key = sandbox.runtimeId;
|
|
3312
|
+
sandboxExecServerRegistry.servers.set(key, created);
|
|
3313
|
+
created.catch(() => {
|
|
3314
|
+
if (sandboxExecServerRegistry.servers.get(key) === created) sandboxExecServerRegistry.servers.delete(key);
|
|
3315
|
+
});
|
|
3316
|
+
return created;
|
|
2454
3317
|
}
|
|
2455
|
-
function
|
|
2456
|
-
const
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
3318
|
+
async function startOpenClawExecServer(sandbox) {
|
|
3319
|
+
const server = new WebSocketServer({
|
|
3320
|
+
host: "127.0.0.1",
|
|
3321
|
+
port: 0,
|
|
3322
|
+
maxPayload: CODEX_SANDBOX_EXEC_SERVER_MAX_INBOUND_MESSAGE_BYTES
|
|
3323
|
+
});
|
|
3324
|
+
await once(server, "listening");
|
|
3325
|
+
const address = server.address();
|
|
3326
|
+
if (!address || typeof address === "string") throw new Error("OpenClaw Codex exec-server did not bind to a TCP port.");
|
|
3327
|
+
const environmentId = buildEnvironmentId(sandbox);
|
|
3328
|
+
const authPath = `/openclaw-${randomUUID()}`;
|
|
3329
|
+
const execServer = {
|
|
3330
|
+
authPath,
|
|
3331
|
+
closed: false,
|
|
3332
|
+
environmentId,
|
|
3333
|
+
refCount: 0,
|
|
3334
|
+
url: `ws://127.0.0.1:${address.port}${authPath}`,
|
|
3335
|
+
sandbox,
|
|
3336
|
+
server
|
|
2472
3337
|
};
|
|
3338
|
+
server.on("connection", (socket, request) => {
|
|
3339
|
+
socket.on("error", handleExecServerSocketError);
|
|
3340
|
+
if (!isAuthorizedExecServerRequest(execServer, request)) {
|
|
3341
|
+
socket.close(1008, "unauthorized");
|
|
3342
|
+
return;
|
|
3343
|
+
}
|
|
3344
|
+
handleConnection(execServer, socket);
|
|
3345
|
+
});
|
|
3346
|
+
embeddedAgentLog.info("codex sandbox exec-server started", {
|
|
3347
|
+
environmentId,
|
|
3348
|
+
runtimeId: sandbox.runtimeId,
|
|
3349
|
+
backendId: sandbox.backendId
|
|
3350
|
+
});
|
|
3351
|
+
return execServer;
|
|
3352
|
+
}
|
|
3353
|
+
async function releaseOpenClawExecServer(execServer) {
|
|
3354
|
+
if (execServer.closed) return;
|
|
3355
|
+
execServer.refCount = Math.max(0, execServer.refCount - 1);
|
|
3356
|
+
if (execServer.refCount > 0) return;
|
|
3357
|
+
const current = await sandboxExecServerRegistry.servers.get(execServer.sandbox.runtimeId)?.catch(() => void 0);
|
|
3358
|
+
if (execServer.refCount > 0 || execServer.closed) return;
|
|
3359
|
+
if (current === execServer) sandboxExecServerRegistry.servers.delete(execServer.sandbox.runtimeId);
|
|
3360
|
+
await closeOpenClawExecServer(execServer);
|
|
3361
|
+
}
|
|
3362
|
+
async function closeOpenClawExecServer(execServer) {
|
|
3363
|
+
if (execServer.closed) return;
|
|
3364
|
+
execServer.closed = true;
|
|
3365
|
+
for (const client of execServer.server.clients) client.close(1001, "shutdown");
|
|
3366
|
+
await new Promise((resolve) => {
|
|
3367
|
+
execServer.server.close(() => resolve());
|
|
3368
|
+
});
|
|
2473
3369
|
}
|
|
2474
|
-
function
|
|
2475
|
-
|
|
2476
|
-
const toolTitle = sanitizeOptionalDisplayText(readString$1(params.meta, MCP_TOOL_APPROVAL_TOOL_TITLE_KEY));
|
|
2477
|
-
const toolDescription = sanitizeOptionalDisplayText(readString$1(params.meta, MCP_TOOL_APPROVAL_TOOL_DESCRIPTION_KEY));
|
|
2478
|
-
const summaryLines = [
|
|
2479
|
-
connectorName && `App: ${connectorName}`,
|
|
2480
|
-
toolTitle && `Tool: ${toolTitle}`,
|
|
2481
|
-
params.serverName && `MCP server: ${params.serverName}`,
|
|
2482
|
-
toolDescription
|
|
2483
|
-
].filter((line) => Boolean(line));
|
|
2484
|
-
const paramLines = readDisplayParamLines(params.meta);
|
|
2485
|
-
const propertyLines = readPropertyDescriptionLines(params.requestedSchema);
|
|
2486
|
-
return [
|
|
2487
|
-
params.title,
|
|
2488
|
-
summaryLines.join("\n"),
|
|
2489
|
-
paramLines.length > 0 ? ["Parameters:", ...paramLines].join("\n") : "",
|
|
2490
|
-
propertyLines.length > 0 ? ["Fields:", ...propertyLines].join("\n") : ""
|
|
2491
|
-
].filter(Boolean).join("\n\n");
|
|
3370
|
+
function buildEnvironmentId(sandbox) {
|
|
3371
|
+
return `openclaw-sandbox-${createHash("sha256").update(sandbox.runtimeId).digest("hex").slice(0, 16)}`;
|
|
2492
3372
|
}
|
|
2493
|
-
function
|
|
2494
|
-
|
|
2495
|
-
return Object.entries(properties).map(([name, value]) => {
|
|
2496
|
-
const schema = isJsonObject(value) ? value : void 0;
|
|
2497
|
-
if (!schema) return;
|
|
2498
|
-
const propTitle = sanitizeDisplayText(readString$1(schema, "title") ?? "") || sanitizeDisplayText(name) || "field";
|
|
2499
|
-
const description = sanitizeOptionalDisplayText(readString$1(schema, "description"));
|
|
2500
|
-
return description ? `- ${propTitle}: ${description}` : `- ${propTitle}`;
|
|
2501
|
-
}).filter((line) => Boolean(line));
|
|
3373
|
+
function isAuthorizedExecServerRequest(execServer, request) {
|
|
3374
|
+
return new URL(request.url ?? "", "ws://127.0.0.1").pathname === execServer.authPath;
|
|
2502
3375
|
}
|
|
2503
|
-
function
|
|
2504
|
-
const
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
3376
|
+
function handleConnection(execServer, socket) {
|
|
3377
|
+
const processes = /* @__PURE__ */ new Map();
|
|
3378
|
+
const fileReads = /* @__PURE__ */ new Map();
|
|
3379
|
+
socket.on("message", (data) => {
|
|
3380
|
+
handleMessage(execServer, processes, fileReads, socket, data).catch((error) => {
|
|
3381
|
+
embeddedAgentLog.warn("codex sandbox exec-server message failed", { error });
|
|
3382
|
+
});
|
|
3383
|
+
});
|
|
3384
|
+
socket.on("close", () => {
|
|
3385
|
+
closeAllFileReads(fileReads);
|
|
3386
|
+
for (const process of processes.values()) process.abortController.abort();
|
|
3387
|
+
});
|
|
2515
3388
|
}
|
|
2516
|
-
function
|
|
2517
|
-
|
|
3389
|
+
function handleExecServerSocketError(error) {
|
|
3390
|
+
embeddedAgentLog.debug("codex sandbox exec-server websocket failed", { error });
|
|
2518
3391
|
}
|
|
2519
|
-
function
|
|
2520
|
-
|
|
2521
|
-
if (
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
if (depth <= 0) return "[truncated]";
|
|
2525
|
-
const parts = [];
|
|
2526
|
-
const limit = Math.min(value.length, MAX_DISPLAY_VALUE_ARRAY_ITEMS);
|
|
2527
|
-
for (let i = 0; i < limit; i += 1) parts.push(formatDisplayJsonValue(value[i] ?? null, depth - 1));
|
|
2528
|
-
if (value.length > MAX_DISPLAY_VALUE_ARRAY_ITEMS) parts.push("...");
|
|
2529
|
-
return `[${parts.join(",")}]`;
|
|
3392
|
+
async function handleMessage(execServer, processes, fileReads, socket, data) {
|
|
3393
|
+
const request = parseRequest(data);
|
|
3394
|
+
if (!request.method) {
|
|
3395
|
+
sendError(socket, request.id, -32600, "Invalid Request");
|
|
3396
|
+
return;
|
|
2530
3397
|
}
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
count += 1;
|
|
2545
|
-
}
|
|
2546
|
-
if (truncated) parts.push("...");
|
|
2547
|
-
return `{${parts.join(",")}}`;
|
|
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));
|
|
2548
3411
|
}
|
|
2549
|
-
return "null";
|
|
2550
3412
|
}
|
|
2551
|
-
function
|
|
2552
|
-
|
|
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
|
+
}
|
|
2553
3450
|
}
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
const
|
|
2558
|
-
|
|
2559
|
-
return
|
|
3451
|
+
//#endregion
|
|
3452
|
+
//#region extensions/codex/src/app-server/provider-capabilities.ts
|
|
3453
|
+
function resolveOverriddenProviderWebSearchSupport(modelProviderOverride) {
|
|
3454
|
+
const provider = modelProviderOverride?.trim().toLowerCase();
|
|
3455
|
+
if (!provider) return;
|
|
3456
|
+
return provider === "openai" ? "supported" : "unsupported";
|
|
2560
3457
|
}
|
|
2561
|
-
function
|
|
2562
|
-
return
|
|
3458
|
+
async function readConfiguredProviderWebSearchSupport(params) {
|
|
3459
|
+
return (await params.client.request("modelProvider/capabilities/read", {}, {
|
|
3460
|
+
timeoutMs: params.timeoutMs,
|
|
3461
|
+
signal: params.signal
|
|
3462
|
+
})).webSearch ? "supported" : "unsupported";
|
|
2563
3463
|
}
|
|
2564
|
-
async function
|
|
3464
|
+
async function resolveCodexProviderWebSearchSupportForClient(params) {
|
|
3465
|
+
const overrideSupport = resolveOverriddenProviderWebSearchSupport(params.modelProviderOverride);
|
|
3466
|
+
if (overrideSupport) return overrideSupport;
|
|
2565
3467
|
try {
|
|
2566
|
-
|
|
2567
|
-
paramsForRun: params.paramsForRun,
|
|
2568
|
-
title: params.title,
|
|
2569
|
-
description: params.description,
|
|
2570
|
-
severity: "warning",
|
|
2571
|
-
toolName: "codex_mcp_tool_approval",
|
|
2572
|
-
allowedDecisions: params.allowedDecisions
|
|
2573
|
-
});
|
|
2574
|
-
const approvalId = requestResult?.id;
|
|
2575
|
-
if (!approvalId) return "unavailable";
|
|
2576
|
-
return mapExecDecisionToOutcome(approvalRequestExplicitlyUnavailable(requestResult) ? null : await waitForPluginApprovalDecision({
|
|
2577
|
-
approvalId,
|
|
2578
|
-
signal: params.signal
|
|
2579
|
-
}));
|
|
3468
|
+
return await readConfiguredProviderWebSearchSupport(params);
|
|
2580
3469
|
} catch {
|
|
2581
|
-
return
|
|
3470
|
+
return "unknown";
|
|
2582
3471
|
}
|
|
2583
3472
|
}
|
|
2584
|
-
function
|
|
2585
|
-
const
|
|
2586
|
-
if (
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
};
|
|
2596
|
-
const content = buildAcceptedContent(approvalPrompt, outcome);
|
|
2597
|
-
if (!content) {
|
|
2598
|
-
if (hasNoSchemaProperties(requestedSchema)) return {
|
|
2599
|
-
action: "accept",
|
|
2600
|
-
content: null,
|
|
2601
|
-
_meta: buildAcceptedMeta(meta, outcome, approvalPrompt.persistHintsMode ?? "legacy")
|
|
2602
|
-
};
|
|
2603
|
-
embeddedAgentLog.warn("codex MCP approval elicitation approved without a mappable response", {
|
|
2604
|
-
approvalKind: meta[MCP_TOOL_APPROVAL_KIND_KEY],
|
|
2605
|
-
fields: Object.keys(requestedSchema.properties ?? {}),
|
|
2606
|
-
outcome
|
|
3473
|
+
async function resolveCodexProviderWebSearchSupport(params) {
|
|
3474
|
+
const overrideSupport = resolveOverriddenProviderWebSearchSupport(params.modelProviderOverride);
|
|
3475
|
+
if (overrideSupport) return overrideSupport;
|
|
3476
|
+
let client;
|
|
3477
|
+
try {
|
|
3478
|
+
client = await params.clientFactory({
|
|
3479
|
+
startOptions: params.appServer.start,
|
|
3480
|
+
...params.preparedAuth ? { preparedAuth: params.preparedAuth } : { authProfileId: params.authProfileId },
|
|
3481
|
+
agentDir: params.agentDir,
|
|
3482
|
+
config: params.config,
|
|
3483
|
+
timeoutMs: params.appServer.requestTimeoutMs
|
|
2607
3484
|
});
|
|
2608
|
-
return {
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
3485
|
+
return await resolveCodexProviderWebSearchSupportForClient({
|
|
3486
|
+
client,
|
|
3487
|
+
timeoutMs: params.appServer.requestTimeoutMs,
|
|
3488
|
+
modelProviderOverride: params.modelProviderOverride,
|
|
3489
|
+
signal: params.signal
|
|
3490
|
+
});
|
|
3491
|
+
} catch {
|
|
3492
|
+
return "unknown";
|
|
3493
|
+
} finally {
|
|
3494
|
+
if (client) releaseLeasedSharedCodexAppServerClient(client);
|
|
2613
3495
|
}
|
|
2614
|
-
return {
|
|
2615
|
-
action: "accept",
|
|
2616
|
-
content,
|
|
2617
|
-
_meta: buildAcceptedMeta(meta, outcome, approvalPrompt.persistHintsMode ?? "legacy")
|
|
2618
|
-
};
|
|
2619
|
-
}
|
|
2620
|
-
function buildAcceptedContent(approvalPrompt, outcome) {
|
|
2621
|
-
const { requestedSchema, meta } = approvalPrompt;
|
|
2622
|
-
const properties = isJsonObject(requestedSchema.properties) ? requestedSchema.properties : void 0;
|
|
2623
|
-
if (!properties) return;
|
|
2624
|
-
const required = Array.isArray(requestedSchema.required) ? new Set(requestedSchema.required.filter((entry) => typeof entry === "string")) : /* @__PURE__ */ new Set();
|
|
2625
|
-
const content = {};
|
|
2626
|
-
let sawApprovalField = false;
|
|
2627
|
-
for (const [name, value] of Object.entries(properties)) {
|
|
2628
|
-
const schema = isJsonObject(value) ? value : void 0;
|
|
2629
|
-
if (!schema) continue;
|
|
2630
|
-
const property = {
|
|
2631
|
-
name,
|
|
2632
|
-
schema,
|
|
2633
|
-
required: required.has(name)
|
|
2634
|
-
};
|
|
2635
|
-
const next = readApprovalFieldValue(property, outcome) ?? readPersistFieldValue(property, meta, outcome, approvalPrompt.persistHintsMode ?? "legacy") ?? readFallbackFieldValue(property, outcome);
|
|
2636
|
-
if (next === void 0) {
|
|
2637
|
-
if (isApprovalField(property)) sawApprovalField = true;
|
|
2638
|
-
if (property.required) return;
|
|
2639
|
-
continue;
|
|
2640
|
-
}
|
|
2641
|
-
if (isApprovalField(property)) sawApprovalField = true;
|
|
2642
|
-
content[name] = next;
|
|
2643
|
-
}
|
|
2644
|
-
return sawApprovalField ? content : void 0;
|
|
2645
|
-
}
|
|
2646
|
-
function readApprovalFieldValue(property, outcome) {
|
|
2647
|
-
if (!isApprovalField(property)) return;
|
|
2648
|
-
if (readString$1(property.schema, "type") === "boolean") return true;
|
|
2649
|
-
const options = readEnumOptions(property.schema);
|
|
2650
|
-
if (options.length === 0) return;
|
|
2651
|
-
const sessionChoice = options.find((option) => isSessionApprovalOption(option));
|
|
2652
|
-
const acceptChoice = options.find((option) => isPositiveApprovalOption(option));
|
|
2653
|
-
if (outcome === "approved-session") return sessionChoice?.value ?? acceptChoice?.value;
|
|
2654
|
-
return acceptChoice?.value ?? sessionChoice?.value;
|
|
2655
|
-
}
|
|
2656
|
-
function readPersistFieldValue(property, meta, outcome, persistHintsMode) {
|
|
2657
|
-
if (!isPersistField(property) || outcome !== "approved-session") return;
|
|
2658
|
-
const persistHints = readPersistHints(meta, persistHintsMode);
|
|
2659
|
-
const options = readEnumOptions(property.schema);
|
|
2660
|
-
if (options.length === 0) return;
|
|
2661
|
-
const preferred = choosePersistHint(persistHints);
|
|
2662
|
-
if (preferred) return options.find((option) => option.value === preferred || option.label === preferred)?.value;
|
|
2663
|
-
if (persistHintsMode === "explicit") return chooseAlwaysPersistOptionValue(options);
|
|
2664
|
-
}
|
|
2665
|
-
function readDefaultValue(schema) {
|
|
2666
|
-
return schema.default;
|
|
2667
|
-
}
|
|
2668
|
-
function readFallbackFieldValue(property, outcome) {
|
|
2669
|
-
if (outcome === "approved-once" && isPersistField(property)) return;
|
|
2670
|
-
return readDefaultValue(property.schema);
|
|
2671
|
-
}
|
|
2672
|
-
function isApprovalField(property) {
|
|
2673
|
-
const haystack = propertyText(property).toLowerCase();
|
|
2674
|
-
return /\b(approve|approval|allow|accept|decision)\b/.test(haystack);
|
|
2675
|
-
}
|
|
2676
|
-
function isPersistField(property) {
|
|
2677
|
-
const haystack = propertyText(property).toLowerCase();
|
|
2678
|
-
return /\b(persist|session|always|scope)\b/.test(haystack);
|
|
2679
|
-
}
|
|
2680
|
-
function propertyText(property) {
|
|
2681
|
-
return [
|
|
2682
|
-
property.name,
|
|
2683
|
-
readString$1(property.schema, "title"),
|
|
2684
|
-
readString$1(property.schema, "description")
|
|
2685
|
-
].filter(Boolean).join(" ");
|
|
2686
|
-
}
|
|
2687
|
-
function readPersistHints(meta, mode = "legacy") {
|
|
2688
|
-
const raw = meta.persist;
|
|
2689
|
-
if (typeof raw === "string") return [raw];
|
|
2690
|
-
if (Array.isArray(raw)) return raw.filter((entry) => typeof entry === "string");
|
|
2691
|
-
return mode === "legacy" ? ["session", "always"] : [];
|
|
2692
|
-
}
|
|
2693
|
-
function buildAcceptedMeta(meta, outcome, persistHintsMode) {
|
|
2694
|
-
if (outcome !== "approved-session") return null;
|
|
2695
|
-
const persist = choosePersistHint(readPersistHints(meta, persistHintsMode));
|
|
2696
|
-
return persist ? { persist } : null;
|
|
2697
|
-
}
|
|
2698
|
-
function choosePersistHint(persistHints) {
|
|
2699
|
-
if (persistHints.includes("always")) return "always";
|
|
2700
|
-
if (persistHints.includes("session")) return "session";
|
|
2701
|
-
}
|
|
2702
|
-
function chooseAlwaysPersistOptionValue(options) {
|
|
2703
|
-
return options.find((option) => optionMatchesPersist(option, "always"))?.value;
|
|
2704
3496
|
}
|
|
2705
|
-
|
|
2706
|
-
|
|
3497
|
+
//#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
|
+
});
|
|
2707
3520
|
}
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
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;
|
|
2711
3531
|
}
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
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);
|
|
2720
3559
|
}
|
|
2721
|
-
if (Array.isArray(schema.oneOf)) return schema.oneOf.map((entry) => {
|
|
2722
|
-
const option = isJsonObject(entry) ? entry : void 0;
|
|
2723
|
-
const value = readString$1(option, "const");
|
|
2724
|
-
if (!value) return;
|
|
2725
|
-
return {
|
|
2726
|
-
value,
|
|
2727
|
-
label: readString$1(option, "title") ?? value
|
|
2728
|
-
};
|
|
2729
|
-
}).filter((entry) => Boolean(entry));
|
|
2730
|
-
return [];
|
|
2731
3560
|
}
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
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";
|
|
2735
3567
|
}
|
|
2736
|
-
function
|
|
2737
|
-
|
|
2738
|
-
return /\b(session|always|persistent)\b/.test(haystack) && /\b(allow|approve|accept)\b/.test(haystack);
|
|
3568
|
+
function truncateCodexApprovalDisplayText(value, maxLength) {
|
|
3569
|
+
return value.length <= maxLength ? value : `${truncateUtf16Safe(value, maxLength - 3)}...`;
|
|
2739
3570
|
}
|
|
2740
|
-
function
|
|
2741
|
-
|
|
2742
|
-
return typeof value === "string" && value.trim() ? value : void 0;
|
|
3571
|
+
function stripDanglingCodexApprovalTerminalSequence(value) {
|
|
3572
|
+
return value.replace(DANGLING_TERMINAL_SEQUENCE_SUFFIX_RE, "");
|
|
2743
3573
|
}
|
|
2744
|
-
function
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
if (value) return value;
|
|
2748
|
-
}
|
|
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();
|
|
2749
3577
|
}
|
|
2750
3578
|
//#endregion
|
|
2751
3579
|
//#region extensions/codex/src/app-server/approval-bridge.ts
|
|
@@ -2760,11 +3588,6 @@ const COMMAND_PREVIEW_WITH_DETAILS_MAX_LENGTH = 80;
|
|
|
2760
3588
|
const APPROVAL_PREVIEW_SCAN_MAX_LENGTH = 4096;
|
|
2761
3589
|
const APPROVAL_PREVIEW_OMITTED = "[preview truncated or unsafe content omitted]";
|
|
2762
3590
|
const CONCRETE_TOOL_AUTO_APPROVAL_METHODS = /* @__PURE__ */ new Set(["item/commandExecution/requestApproval", "item/fileChange/requestApproval"]);
|
|
2763
|
-
const ANSI_OSC_SEQUENCE_RE = new RegExp(String.raw`(?:\u001b]|\u009d)[^\u001b\u009c\u0007]*(?:\u0007|\u001b\\|\u009c)`, "g");
|
|
2764
|
-
const ANSI_CONTROL_SEQUENCE_RE = new RegExp(String.raw`(?:\u001b\[[0-?]*[ -/]*[@-~]|\u009b[0-?]*[ -/]*[@-~]|\u001b[@-Z\\-_])`, "g");
|
|
2765
|
-
const CONTROL_CHARACTER_RE = new RegExp(String.raw`[\u0000-\u001f\u007f-\u009f]+`, "g");
|
|
2766
|
-
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");
|
|
2767
|
-
const DANGLING_TERMINAL_SEQUENCE_SUFFIX_RE = new RegExp(String.raw`(?:\u001b\][^\u001b\u009c\u0007]*|\u009d[^\u001b\u009c\u0007]*|\u001b\[[0-?]*[ -/]*|\u009b[0-?]*[ -/]*|\u001b)$`);
|
|
2768
3591
|
/**
|
|
2769
3592
|
* Handles one app-server approval request for the active thread/turn, returning
|
|
2770
3593
|
* the app-server response payload when the request belongs to this run.
|
|
@@ -2777,6 +3600,18 @@ async function handleCodexAppServerApprovalRequest(params) {
|
|
|
2777
3600
|
requestParams,
|
|
2778
3601
|
paramsForRun: params.paramsForRun
|
|
2779
3602
|
});
|
|
3603
|
+
const resolvePolicyApproval = (outcome, message = approvalResolutionMessage(outcome)) => {
|
|
3604
|
+
emitApprovalEvent(params.paramsForRun, {
|
|
3605
|
+
phase: "resolved",
|
|
3606
|
+
kind: context.kind,
|
|
3607
|
+
status: outcome === "denied" ? "denied" : "approved",
|
|
3608
|
+
title: context.title,
|
|
3609
|
+
...context.eventDetails,
|
|
3610
|
+
...approvalEventScope(params.method, outcome),
|
|
3611
|
+
message
|
|
3612
|
+
});
|
|
3613
|
+
return buildApprovalResponse(params.method, context.requestParams, outcome);
|
|
3614
|
+
};
|
|
2780
3615
|
try {
|
|
2781
3616
|
const policyOutcome = await runOpenClawToolPolicyForApprovalRequest({
|
|
2782
3617
|
method: params.method,
|
|
@@ -2789,56 +3624,14 @@ async function handleCodexAppServerApprovalRequest(params) {
|
|
|
2789
3624
|
});
|
|
2790
3625
|
if (policyOutcome?.outcome === "denied") {
|
|
2791
3626
|
recordNativeToolFailureDisposition(params, context, policyOutcome.failureDisposition);
|
|
2792
|
-
|
|
2793
|
-
phase: "resolved",
|
|
2794
|
-
kind: context.kind,
|
|
2795
|
-
status: "denied",
|
|
2796
|
-
title: context.title,
|
|
2797
|
-
...context.eventDetails,
|
|
2798
|
-
...approvalEventScope(params.method, "denied"),
|
|
2799
|
-
message: policyOutcome.reason
|
|
2800
|
-
});
|
|
2801
|
-
return buildApprovalResponse(params.method, context.requestParams, "denied");
|
|
2802
|
-
}
|
|
2803
|
-
if (policyOutcome?.outcome === "approved-once" || policyOutcome?.outcome === "approved-session") {
|
|
2804
|
-
emitApprovalEvent(params.paramsForRun, {
|
|
2805
|
-
phase: "resolved",
|
|
2806
|
-
kind: context.kind,
|
|
2807
|
-
status: "approved",
|
|
2808
|
-
title: context.title,
|
|
2809
|
-
...context.eventDetails,
|
|
2810
|
-
...approvalEventScope(params.method, policyOutcome.outcome),
|
|
2811
|
-
message: approvalResolutionMessage(policyOutcome.outcome)
|
|
2812
|
-
});
|
|
2813
|
-
return buildApprovalResponse(params.method, context.requestParams, policyOutcome.outcome);
|
|
3627
|
+
return resolvePolicyApproval("denied", policyOutcome.reason);
|
|
2814
3628
|
}
|
|
3629
|
+
if (policyOutcome?.outcome === "approved-once" || policyOutcome?.outcome === "approved-session") return resolvePolicyApproval(policyOutcome.outcome);
|
|
2815
3630
|
const canAutoApproveConcreteToolCall = CONCRETE_TOOL_AUTO_APPROVAL_METHODS.has(params.method);
|
|
2816
|
-
if (canAutoApproveConcreteToolCall && params.autoApproveOpenClawToolPolicy === true && policyOutcome?.outcome === "allowed")
|
|
2817
|
-
|
|
2818
|
-
phase: "resolved",
|
|
2819
|
-
kind: context.kind,
|
|
2820
|
-
status: "approved",
|
|
2821
|
-
title: context.title,
|
|
2822
|
-
...context.eventDetails,
|
|
2823
|
-
...approvalEventScope(params.method, "approved-once"),
|
|
2824
|
-
message: "Codex app-server approval accepted by OpenClaw tool policy."
|
|
2825
|
-
});
|
|
2826
|
-
return buildApprovalResponse(params.method, context.requestParams, "approved-once");
|
|
2827
|
-
}
|
|
2828
|
-
if (canAutoApproveConcreteToolCall && params.autoApprove === true) {
|
|
2829
|
-
emitApprovalEvent(params.paramsForRun, {
|
|
2830
|
-
phase: "resolved",
|
|
2831
|
-
kind: context.kind,
|
|
2832
|
-
status: "approved",
|
|
2833
|
-
title: context.title,
|
|
2834
|
-
...context.eventDetails,
|
|
2835
|
-
...approvalEventScope(params.method, "approved-session"),
|
|
2836
|
-
message: "Codex app-server approval auto-approved by runtime policy."
|
|
2837
|
-
});
|
|
2838
|
-
return buildApprovalResponse(params.method, context.requestParams, "approved-session");
|
|
2839
|
-
}
|
|
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.");
|
|
2840
3633
|
const requestResult = await requestPluginApproval({
|
|
2841
|
-
|
|
3634
|
+
hostCapabilities: params.paramsForRun.hostCapabilities,
|
|
2842
3635
|
title: context.title,
|
|
2843
3636
|
description: context.description,
|
|
2844
3637
|
severity: context.severity,
|
|
@@ -2872,7 +3665,8 @@ async function handleCodexAppServerApprovalRequest(params) {
|
|
|
2872
3665
|
const requestUnavailable = approvalRequestExplicitlyUnavailable(requestResult);
|
|
2873
3666
|
const decision = requestUnavailable ? null : await waitForPluginApprovalDecision({
|
|
2874
3667
|
approvalId,
|
|
2875
|
-
signal: params.signal
|
|
3668
|
+
signal: params.signal,
|
|
3669
|
+
hostCapabilities: params.paramsForRun.hostCapabilities
|
|
2876
3670
|
});
|
|
2877
3671
|
const approvalExpired = !requestUnavailable && decision === null;
|
|
2878
3672
|
const outcome = params.signal?.aborted ? "cancelled" : mapExecDecisionToOutcome(decision);
|
|
@@ -2900,7 +3694,7 @@ async function handleCodexAppServerApprovalRequest(params) {
|
|
|
2900
3694
|
title: context.title,
|
|
2901
3695
|
...context.eventDetails,
|
|
2902
3696
|
...approvalEventScope(params.method, cancelled ? "cancelled" : "denied"),
|
|
2903
|
-
message: cancelled ? "Codex app-server approval cancelled because the run stopped." : `Codex app-server approval route failed: ${formatCodexDisplayText(
|
|
3697
|
+
message: cancelled ? "Codex app-server approval cancelled because the run stopped." : `Codex app-server approval route failed: ${formatCodexDisplayText(coerceErrorMessage(error))}`
|
|
2904
3698
|
});
|
|
2905
3699
|
return buildApprovalResponse(params.method, context.requestParams, cancelled ? "cancelled" : "denied");
|
|
2906
3700
|
}
|
|
@@ -2929,13 +3723,13 @@ function buildApprovalResponse(method, requestParams, outcome) {
|
|
|
2929
3723
|
}
|
|
2930
3724
|
function matchesCurrentTurn(requestParams, threadId, turnId) {
|
|
2931
3725
|
if (!requestParams) return false;
|
|
2932
|
-
const requestThreadId =
|
|
2933
|
-
const requestTurnId =
|
|
3726
|
+
const requestThreadId = readStringField(requestParams, "threadId");
|
|
3727
|
+
const requestTurnId = readStringField(requestParams, "turnId");
|
|
2934
3728
|
return requestThreadId === threadId && requestTurnId === turnId;
|
|
2935
3729
|
}
|
|
2936
3730
|
function buildApprovalContext(params) {
|
|
2937
|
-
const itemId =
|
|
2938
|
-
const approvalId =
|
|
3731
|
+
const itemId = readStringField(params.requestParams, "itemId") ?? readStringField(params.requestParams, "callId") ?? readStringField(params.requestParams, "approvalId");
|
|
3732
|
+
const approvalId = readStringField(params.requestParams, "approvalId") ?? itemId;
|
|
2939
3733
|
const commandDetailLines = params.method === "item/commandExecution/requestApproval" ? describeCommandApprovalDetails(params.requestParams) : [];
|
|
2940
3734
|
const commandPreview = sanitizeApprovalPreview(readDisplayCommandPreview(params.requestParams), commandDetailLines.length > 0 ? COMMAND_PREVIEW_WITH_DETAILS_MAX_LENGTH : 180);
|
|
2941
3735
|
const reasonPreview = sanitizeApprovalPreview(readStringPreview(params.requestParams, "reason"), 180);
|
|
@@ -2970,7 +3764,7 @@ function buildApprovalContext(params) {
|
|
|
2970
3764
|
async function runOpenClawToolPolicyForApprovalRequest(params) {
|
|
2971
3765
|
const policyRequest = buildOpenClawToolPolicyRequest(params.method, params.requestParams);
|
|
2972
3766
|
if (!policyRequest) return;
|
|
2973
|
-
const cwd =
|
|
3767
|
+
const cwd = readStringField(params.requestParams, "cwd") ?? params.paramsForRun.workspaceDir;
|
|
2974
3768
|
const nativeRelayOutcome = await runNativeRelayToolPolicyForApprovalRequest({
|
|
2975
3769
|
method: params.method,
|
|
2976
3770
|
requestParams: params.requestParams,
|
|
@@ -2978,6 +3772,7 @@ async function runOpenClawToolPolicyForApprovalRequest(params) {
|
|
|
2978
3772
|
policyRequest,
|
|
2979
3773
|
nativeHookRelay: params.nativeHookRelay,
|
|
2980
3774
|
autoApprove: params.autoApprove,
|
|
3775
|
+
assertActive: params.paramsForRun.hostCapabilities.assertActive,
|
|
2981
3776
|
cwd,
|
|
2982
3777
|
signal: params.signal
|
|
2983
3778
|
});
|
|
@@ -2988,37 +3783,12 @@ async function runOpenClawToolPolicyForApprovalRequest(params) {
|
|
|
2988
3783
|
};
|
|
2989
3784
|
if (nativeRelayOutcome?.outcome === "approved-once" || nativeRelayOutcome?.outcome === "approved-session") return { outcome: nativeRelayOutcome.outcome };
|
|
2990
3785
|
if (nativeRelayOutcome?.handled) return { outcome: "allowed" };
|
|
2991
|
-
const
|
|
2992
|
-
sessionKey: params.paramsForRun.sessionKey,
|
|
2993
|
-
messageChannel: params.paramsForRun.messageChannel,
|
|
2994
|
-
messageProvider: params.paramsForRun.messageProvider,
|
|
2995
|
-
currentChannelId: params.paramsForRun.currentChannelId,
|
|
2996
|
-
messageTo: params.paramsForRun.messageTo
|
|
2997
|
-
}).channelId;
|
|
2998
|
-
const requester = buildCodexHookRequester(params.paramsForRun);
|
|
2999
|
-
const outcome = await runBeforeToolCallHook({
|
|
3786
|
+
const outcome = await params.paramsForRun.hostCapabilities.runBeforeToolCall({
|
|
3000
3787
|
toolName: policyRequest.toolName,
|
|
3001
3788
|
params: policyRequest.params,
|
|
3789
|
+
...cwd ? { nativeOperation: { cwd } } : {},
|
|
3002
3790
|
...params.context.approvalId ? { toolCallId: params.context.approvalId } : {},
|
|
3003
|
-
|
|
3004
|
-
signal: params.signal,
|
|
3005
|
-
ctx: {
|
|
3006
|
-
...params.paramsForRun.agentId ? { agentId: params.paramsForRun.agentId } : {},
|
|
3007
|
-
...params.paramsForRun.config ? { config: params.paramsForRun.config } : {},
|
|
3008
|
-
...cwd ? { cwd } : {},
|
|
3009
|
-
workspaceDir: params.paramsForRun.workspaceDir,
|
|
3010
|
-
...params.paramsForRun.sessionKey ? { sessionKey: params.paramsForRun.sessionKey } : {},
|
|
3011
|
-
...params.paramsForRun.sessionId ? { sessionId: params.paramsForRun.sessionId } : {},
|
|
3012
|
-
...params.paramsForRun.runId ? { runId: params.paramsForRun.runId } : {},
|
|
3013
|
-
...hookChannelId ? { channelId: hookChannelId } : {},
|
|
3014
|
-
...requester ? { requester } : {},
|
|
3015
|
-
trigger: params.paramsForRun.trigger,
|
|
3016
|
-
approvalReviewerDeviceId: params.paramsForRun.approvalReviewerDeviceId,
|
|
3017
|
-
turnSourceChannel: params.paramsForRun.messageChannel ?? params.paramsForRun.messageProvider,
|
|
3018
|
-
turnSourceTo: params.paramsForRun.currentMessagingTarget ?? params.paramsForRun.currentChannelId,
|
|
3019
|
-
turnSourceAccountId: params.paramsForRun.agentAccountId,
|
|
3020
|
-
turnSourceThreadId: params.paramsForRun.currentThreadTs
|
|
3021
|
-
}
|
|
3791
|
+
signal: params.signal
|
|
3022
3792
|
});
|
|
3023
3793
|
if (outcome.blocked) return {
|
|
3024
3794
|
outcome: "denied",
|
|
@@ -3033,7 +3803,8 @@ async function runOpenClawToolPolicyForApprovalRequest(params) {
|
|
|
3033
3803
|
return { outcome: "allowed" };
|
|
3034
3804
|
}
|
|
3035
3805
|
async function runNativeRelayToolPolicyForApprovalRequest(params) {
|
|
3036
|
-
|
|
3806
|
+
const nativeHookRelay = params.nativeHookRelay;
|
|
3807
|
+
if (params.method !== "item/commandExecution/requestApproval" || !nativeHookRelay?.allowedEvents.includes("pre_tool_use")) return;
|
|
3037
3808
|
const payload = buildNativeRelayPreToolUsePayload({
|
|
3038
3809
|
requestParams: params.requestParams,
|
|
3039
3810
|
policyRequest: params.policyRequest,
|
|
@@ -3041,33 +3812,34 @@ async function runNativeRelayToolPolicyForApprovalRequest(params) {
|
|
|
3041
3812
|
cwd: params.cwd
|
|
3042
3813
|
});
|
|
3043
3814
|
if (!payload) return;
|
|
3044
|
-
|
|
3045
|
-
relayId: params.nativeHookRelay.relayId,
|
|
3046
|
-
event: "pre_tool_use",
|
|
3047
|
-
toolUseId: params.context.approvalId
|
|
3048
|
-
})) {
|
|
3815
|
+
const resolveDeferredApproval = async () => {
|
|
3049
3816
|
const approvalOutcome = await resolveNativeHookRelayDeferredToolApproval({
|
|
3050
|
-
relayId:
|
|
3817
|
+
relayId: nativeHookRelay.relayId,
|
|
3051
3818
|
toolUseId: params.context.approvalId,
|
|
3052
3819
|
signal: params.signal
|
|
3053
3820
|
});
|
|
3821
|
+
params.assertActive();
|
|
3054
3822
|
if (approvalOutcome?.outcome === "denied") return {
|
|
3055
3823
|
handled: true,
|
|
3056
3824
|
blocked: true,
|
|
3057
3825
|
reason: approvalOutcome.reason,
|
|
3058
3826
|
...approvalOutcome.failureDisposition ? { failureDisposition: approvalOutcome.failureDisposition } : {}
|
|
3059
3827
|
};
|
|
3060
|
-
|
|
3828
|
+
return approvalOutcome?.outcome === "approved-once" ? {
|
|
3061
3829
|
handled: true,
|
|
3062
3830
|
outcome: approvalOutcome.outcome
|
|
3063
|
-
};
|
|
3064
|
-
|
|
3065
|
-
|
|
3831
|
+
} : { handled: true };
|
|
3832
|
+
};
|
|
3833
|
+
if (hasNativeHookRelayInvocation({
|
|
3834
|
+
relayId: nativeHookRelay.relayId,
|
|
3835
|
+
event: "pre_tool_use",
|
|
3836
|
+
toolUseId: params.context.approvalId
|
|
3837
|
+
})) return resolveDeferredApproval();
|
|
3066
3838
|
try {
|
|
3067
3839
|
const decision = readNativeRelayPreToolUseDecision(await invokeNativeHookRelay({
|
|
3068
3840
|
provider: "codex",
|
|
3069
|
-
relayId:
|
|
3070
|
-
generation:
|
|
3841
|
+
relayId: nativeHookRelay.relayId,
|
|
3842
|
+
generation: nativeHookRelay.generation,
|
|
3071
3843
|
event: "pre_tool_use",
|
|
3072
3844
|
rawPayload: payload,
|
|
3073
3845
|
requireGeneration: true
|
|
@@ -3078,40 +3850,25 @@ async function runNativeRelayToolPolicyForApprovalRequest(params) {
|
|
|
3078
3850
|
reason: decision.reason,
|
|
3079
3851
|
...decision.failureDisposition ? { failureDisposition: decision.failureDisposition } : {}
|
|
3080
3852
|
};
|
|
3081
|
-
|
|
3082
|
-
relayId: params.nativeHookRelay.relayId,
|
|
3083
|
-
toolUseId: params.context.approvalId,
|
|
3084
|
-
signal: params.signal
|
|
3085
|
-
});
|
|
3086
|
-
if (approvalOutcome?.outcome === "denied") return {
|
|
3087
|
-
handled: true,
|
|
3088
|
-
blocked: true,
|
|
3089
|
-
reason: approvalOutcome.reason,
|
|
3090
|
-
...approvalOutcome.failureDisposition ? { failureDisposition: approvalOutcome.failureDisposition } : {}
|
|
3091
|
-
};
|
|
3092
|
-
if (approvalOutcome?.outcome === "approved-once") return {
|
|
3093
|
-
handled: true,
|
|
3094
|
-
outcome: approvalOutcome.outcome
|
|
3095
|
-
};
|
|
3096
|
-
return { handled: true };
|
|
3853
|
+
return await resolveDeferredApproval();
|
|
3097
3854
|
} catch (error) {
|
|
3098
3855
|
if (params.autoApprove === true && !hasNativeHookRelayInvocation({
|
|
3099
|
-
relayId:
|
|
3856
|
+
relayId: nativeHookRelay.relayId,
|
|
3100
3857
|
event: "pre_tool_use",
|
|
3101
3858
|
toolUseId: params.context.approvalId
|
|
3102
3859
|
})) return;
|
|
3103
3860
|
return {
|
|
3104
3861
|
handled: true,
|
|
3105
3862
|
blocked: true,
|
|
3106
|
-
reason: `OpenClaw native hook relay unavailable for Codex app-server approval: ${formatCodexDisplayText(
|
|
3863
|
+
reason: `OpenClaw native hook relay unavailable for Codex app-server approval: ${formatCodexDisplayText(coerceErrorMessage(error))}`,
|
|
3107
3864
|
failureDisposition: "failed"
|
|
3108
3865
|
};
|
|
3109
3866
|
}
|
|
3110
3867
|
}
|
|
3111
3868
|
function buildNativeRelayPreToolUsePayload(params) {
|
|
3112
|
-
const command =
|
|
3869
|
+
const command = readStringField(params.policyRequest.params, "command");
|
|
3113
3870
|
if (!command) return;
|
|
3114
|
-
const turnId =
|
|
3871
|
+
const turnId = readStringField(params.requestParams, "turnId");
|
|
3115
3872
|
return {
|
|
3116
3873
|
hook_event_name: "PreToolUse",
|
|
3117
3874
|
openclaw_approval_mode: "report",
|
|
@@ -3138,7 +3895,7 @@ function readNativeRelayPreToolUseDecision(response) {
|
|
|
3138
3895
|
const output = isJsonObject(parsed?.hookSpecificOutput) ? parsed.hookSpecificOutput : void 0;
|
|
3139
3896
|
if (output?.permissionDecision === "deny") return {
|
|
3140
3897
|
blocked: true,
|
|
3141
|
-
reason:
|
|
3898
|
+
reason: readStringField(output, "permissionDecisionReason") || "OpenClaw native hook policy denied Codex app-server approval.",
|
|
3142
3899
|
...response.failureDisposition ? { failureDisposition: response.failureDisposition } : {}
|
|
3143
3900
|
};
|
|
3144
3901
|
return {
|
|
@@ -3168,7 +3925,7 @@ function buildOpenClawToolPolicyRequest(method, requestParams) {
|
|
|
3168
3925
|
toolName: "exec",
|
|
3169
3926
|
params: {
|
|
3170
3927
|
...command ? { command } : {},
|
|
3171
|
-
...
|
|
3928
|
+
...readStringField(requestParams, "cwd") ? { cwd: readStringField(requestParams, "cwd") } : {},
|
|
3172
3929
|
approval: requestParams ?? {}
|
|
3173
3930
|
}
|
|
3174
3931
|
};
|
|
@@ -3194,7 +3951,7 @@ function stableJsonText(value) {
|
|
|
3194
3951
|
const items = value.map((item) => stableJsonText(item));
|
|
3195
3952
|
return items.every((item) => item !== void 0) ? `[${items.join(",")}]` : void 0;
|
|
3196
3953
|
}
|
|
3197
|
-
if (
|
|
3954
|
+
if (isJsonObject(value)) {
|
|
3198
3955
|
const entries = Object.entries(value).toSorted(([left], [right]) => left.localeCompare(right)).map(([key, item]) => {
|
|
3199
3956
|
const text = stableJsonText(item);
|
|
3200
3957
|
return text === void 0 ? void 0 : `${JSON.stringify(key)}:${text}`;
|
|
@@ -3202,9 +3959,6 @@ function stableJsonText(value) {
|
|
|
3202
3959
|
return entries.every((entry) => entry !== void 0) ? `{${entries.join(",")}}` : void 0;
|
|
3203
3960
|
}
|
|
3204
3961
|
}
|
|
3205
|
-
function isPlainRecord(value) {
|
|
3206
|
-
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
3207
|
-
}
|
|
3208
3962
|
function commandApprovalDecision(requestParams, outcome) {
|
|
3209
3963
|
if (outcome === "cancelled") return commandRejectionDecision(requestParams, "cancel");
|
|
3210
3964
|
if (outcome === "denied" || outcome === "unavailable") return commandRejectionDecision(requestParams, "decline");
|
|
@@ -3330,15 +4084,10 @@ function summarizeFileSystemEntries(permission, risks) {
|
|
|
3330
4084
|
return `entries: ${samples.join(", ")}${remainderSuffix}`;
|
|
3331
4085
|
}
|
|
3332
4086
|
function summarizePermissionRecord(permission, risks, descriptors) {
|
|
3333
|
-
|
|
3334
|
-
for (const descriptor of descriptors) {
|
|
3335
|
-
const summary = summarizePermissionArray(permission, descriptor, risks);
|
|
3336
|
-
if (summary) details.push(summary);
|
|
3337
|
-
}
|
|
3338
|
-
return details.length > 0 ? details.join("; ") : void 0;
|
|
4087
|
+
return descriptors.map((descriptor) => summarizePermissionArray(permission, descriptor, risks)).filter(Boolean).join("; ") || void 0;
|
|
3339
4088
|
}
|
|
3340
4089
|
function summarizePermissionArray(record, descriptor, risks) {
|
|
3341
|
-
const values =
|
|
4090
|
+
const values = normalizeTrimmedStringList(record[descriptor.key]);
|
|
3342
4091
|
if (values.length === 0) return;
|
|
3343
4092
|
for (const value of values) for (const risk of descriptor.risksFor(value)) risks.add(risk);
|
|
3344
4093
|
const sampleValues = values.slice(0, PERMISSION_SAMPLE_LIMIT).map(descriptor.sanitize).filter(Boolean);
|
|
@@ -3373,178 +4122,797 @@ function summarizeNetworkPolicyAmendments(value) {
|
|
|
3373
4122
|
const remainderSuffix = remaining > 0 ? ` (+${remaining} more)` : "";
|
|
3374
4123
|
return `Proposed network policy: ${samples.join(", ")}${remainderSuffix}`;
|
|
3375
4124
|
}
|
|
3376
|
-
function readStringArray(record, key) {
|
|
3377
|
-
return normalizeTrimmedStringList(record[key]);
|
|
3378
|
-
}
|
|
3379
4125
|
function sanitizePermissionHostValue(value) {
|
|
3380
4126
|
const withoutScheme = sanitizePermissionScalar(value).toLowerCase().replace(/^[a-z][a-z0-9+.-]*:\/\//, "");
|
|
3381
4127
|
const authority = withoutScheme.split(/[/?#]/, 1)[0] ?? withoutScheme;
|
|
3382
|
-
return
|
|
4128
|
+
return truncateCodexApprovalDisplayText(authority.includes("@") ? authority.slice(authority.lastIndexOf("@") + 1) : authority, PERMISSION_VALUE_MAX_LENGTH);
|
|
3383
4129
|
}
|
|
3384
4130
|
function sanitizePermissionPathValue(value) {
|
|
3385
|
-
return
|
|
4131
|
+
return truncateCodexApprovalDisplayText(formatApprovalDisplayPath(sanitizePermissionScalar(value)), PERMISSION_VALUE_MAX_LENGTH);
|
|
3386
4132
|
}
|
|
3387
4133
|
function sanitizePermissionScalar(value) {
|
|
3388
|
-
return
|
|
4134
|
+
return sanitizeCodexApprovalVisibleText(value);
|
|
4135
|
+
}
|
|
4136
|
+
function permissionHostRisks(value) {
|
|
4137
|
+
const normalized = value.trim().toLowerCase();
|
|
4138
|
+
const risks = [];
|
|
4139
|
+
if (normalized.includes("*")) {
|
|
4140
|
+
risks.push("wildcard hosts");
|
|
4141
|
+
if (isPrivateNetworkHostPattern(normalized)) risks.push("private-network wildcards");
|
|
4142
|
+
}
|
|
4143
|
+
return risks;
|
|
4144
|
+
}
|
|
4145
|
+
function permissionPathRisks(value) {
|
|
4146
|
+
const normalized = sanitizePermissionScalar(value);
|
|
4147
|
+
const risks = [];
|
|
4148
|
+
if (normalized === "/" || normalized === "\\" || /^[A-Za-z]:[\\/]*$/.test(normalized)) risks.push("filesystem root");
|
|
4149
|
+
return risks;
|
|
4150
|
+
}
|
|
4151
|
+
function isPrivateNetworkHostPattern(value) {
|
|
4152
|
+
const wildcardStripped = value.toLowerCase().replace(/^\*\./, "");
|
|
4153
|
+
if (wildcardStripped === "localhost" || wildcardStripped === "local" || wildcardStripped === "internal" || wildcardStripped === "lan" || wildcardStripped === "home" || wildcardStripped === "corp" || wildcardStripped === "private" || wildcardStripped.endsWith(".local") || wildcardStripped.endsWith(".internal") || wildcardStripped.endsWith(".lan") || wildcardStripped.endsWith(".home") || wildcardStripped.endsWith(".corp") || wildcardStripped.endsWith(".private")) return true;
|
|
4154
|
+
if (wildcardStripped.startsWith("10.") || wildcardStripped.startsWith("127.") || wildcardStripped.startsWith("192.168.") || wildcardStripped.startsWith("169.254.")) return true;
|
|
4155
|
+
return /^172\.(1[6-9]|2\d|3[0-1])\./.test(wildcardStripped);
|
|
4156
|
+
}
|
|
4157
|
+
function hasAvailableDecision(requestParams, decision) {
|
|
4158
|
+
const available = requestParams?.availableDecisions;
|
|
4159
|
+
if (!Array.isArray(available)) return true;
|
|
4160
|
+
return available.includes(decision);
|
|
4161
|
+
}
|
|
4162
|
+
function findAvailableCommandAmendmentDecision(requestParams) {
|
|
4163
|
+
const available = requestParams?.availableDecisions;
|
|
4164
|
+
if (!Array.isArray(available)) return;
|
|
4165
|
+
return available.find((entry) => isJsonObject(entry) && (isJsonObject(entry.acceptWithExecpolicyAmendment) || isJsonObject(entry.applyNetworkPolicyAmendment)));
|
|
4166
|
+
}
|
|
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
|
+
function approvalResolutionMessage(outcome) {
|
|
4176
|
+
if (outcome === "approved-session") return "Codex app-server approval granted for the session.";
|
|
4177
|
+
if (outcome === "approved-once") return "Codex app-server approval granted for this turn.";
|
|
4178
|
+
if (outcome === "cancelled") return "Codex app-server approval cancelled.";
|
|
4179
|
+
if (outcome === "unavailable") return "Codex app-server approval unavailable.";
|
|
4180
|
+
return "Codex app-server approval denied.";
|
|
4181
|
+
}
|
|
4182
|
+
function approvalEventScope(method, outcome) {
|
|
4183
|
+
return method === "item/permissions/requestApproval" ? { scope: outcome === "approved-session" ? "session" : "turn" } : {};
|
|
4184
|
+
}
|
|
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
|
+
function emitApprovalEvent(params, data) {
|
|
4191
|
+
params.onAgentEvent?.({
|
|
4192
|
+
stream: "approval",
|
|
4193
|
+
data
|
|
4194
|
+
});
|
|
4195
|
+
}
|
|
4196
|
+
function readDisplayCommandPreview(record) {
|
|
4197
|
+
const actionCommand = readCommandActionsPreview(record);
|
|
4198
|
+
if (actionCommand) return actionCommand;
|
|
4199
|
+
return readCommandPreview(record);
|
|
4200
|
+
}
|
|
4201
|
+
function readPolicyCommand(record) {
|
|
4202
|
+
const command = record?.command;
|
|
4203
|
+
if (typeof command === "string") return command;
|
|
4204
|
+
if (Array.isArray(command) && command.every((part) => typeof part === "string")) return command.join(" ");
|
|
4205
|
+
const actionCommands = readCommandActions(record);
|
|
4206
|
+
if (actionCommands.length > 0) return actionCommands.join(" && ");
|
|
4207
|
+
}
|
|
4208
|
+
function readCommandActions(record) {
|
|
4209
|
+
const actions = record?.commandActions;
|
|
4210
|
+
if (!Array.isArray(actions)) return [];
|
|
4211
|
+
return actions.map((action) => isJsonObject(action) ? readStringField(action, "command") : void 0).filter((command) => Boolean(command));
|
|
4212
|
+
}
|
|
4213
|
+
function readCommandActionsPreview(record) {
|
|
4214
|
+
let source;
|
|
4215
|
+
for (const command of readCommandActions(record)) {
|
|
4216
|
+
source = appendPreviewPart(source, command, " && ");
|
|
4217
|
+
if (source.clipped) break;
|
|
4218
|
+
}
|
|
4219
|
+
return source;
|
|
4220
|
+
}
|
|
4221
|
+
function readCommandPreview(record) {
|
|
4222
|
+
const command = record?.command;
|
|
4223
|
+
if (typeof command === "string") return previewSource(command);
|
|
4224
|
+
if (!Array.isArray(command)) return;
|
|
4225
|
+
let source;
|
|
4226
|
+
for (const part of command) {
|
|
4227
|
+
if (typeof part !== "string") return;
|
|
4228
|
+
source = appendPreviewPart(source, part, " ");
|
|
4229
|
+
if (source.clipped) break;
|
|
4230
|
+
}
|
|
4231
|
+
return source;
|
|
4232
|
+
}
|
|
4233
|
+
function readStringPreview(record, key) {
|
|
4234
|
+
const value = readStringField(record, key);
|
|
4235
|
+
return value === void 0 ? void 0 : previewSource(value);
|
|
4236
|
+
}
|
|
4237
|
+
function previewSource(value) {
|
|
4238
|
+
return {
|
|
4239
|
+
value: sliceUtf16Safe(value, 0, APPROVAL_PREVIEW_SCAN_MAX_LENGTH),
|
|
4240
|
+
clipped: value.length > APPROVAL_PREVIEW_SCAN_MAX_LENGTH
|
|
4241
|
+
};
|
|
4242
|
+
}
|
|
4243
|
+
function appendPreviewPart(source, part, separator) {
|
|
4244
|
+
const value = `${source?.value ? `${source.value}${separator}` : ""}${part}`;
|
|
4245
|
+
const clipped = source?.clipped === true || value.length > APPROVAL_PREVIEW_SCAN_MAX_LENGTH;
|
|
4246
|
+
return {
|
|
4247
|
+
value: sliceUtf16Safe(value, 0, APPROVAL_PREVIEW_SCAN_MAX_LENGTH),
|
|
4248
|
+
clipped
|
|
4249
|
+
};
|
|
4250
|
+
}
|
|
4251
|
+
function sanitizeApprovalPreview(source, maxLength) {
|
|
4252
|
+
if (!source || !source.value) return { omitted: false };
|
|
4253
|
+
const sanitized = sanitizeCodexApprovalVisibleText(stripDanglingCodexApprovalTerminalSequence(source.value));
|
|
4254
|
+
if (!sanitized) return { omitted: true };
|
|
4255
|
+
return {
|
|
4256
|
+
text: formatCodexDisplayText(truncateCodexApprovalDisplayText(sanitized, maxLength)),
|
|
4257
|
+
omitted: source.clipped
|
|
4258
|
+
};
|
|
4259
|
+
}
|
|
4260
|
+
function formatApprovalPreviewSubject(text, omitted) {
|
|
4261
|
+
return omitted ? `${text} ${APPROVAL_PREVIEW_OMITTED}` : text;
|
|
4262
|
+
}
|
|
4263
|
+
function joinDescriptionLinesWithinLimit(lines, maxLength) {
|
|
4264
|
+
let description = "";
|
|
4265
|
+
for (const line of lines) {
|
|
4266
|
+
const prefix = description ? "\n" : "";
|
|
4267
|
+
const next = `${description}${prefix}${line}`;
|
|
4268
|
+
if (next.length <= maxLength) {
|
|
4269
|
+
description = next;
|
|
4270
|
+
continue;
|
|
4271
|
+
}
|
|
4272
|
+
const remaining = maxLength - description.length - prefix.length;
|
|
4273
|
+
if (remaining < 3) break;
|
|
4274
|
+
description += `${prefix}${truncateCodexApprovalDisplayText(line, remaining)}`;
|
|
4275
|
+
break;
|
|
4276
|
+
}
|
|
4277
|
+
return description;
|
|
4278
|
+
}
|
|
4279
|
+
//#endregion
|
|
4280
|
+
//#region extensions/codex/src/app-server/dynamic-tool-diagnostics.ts
|
|
4281
|
+
/**
|
|
4282
|
+
* Trusted diagnostics emitted around Codex dynamic tool execution lifecycle.
|
|
4283
|
+
*/
|
|
4284
|
+
/** Emits a start event for one Codex dynamic tool call. */
|
|
4285
|
+
function emitDynamicToolStartedDiagnostic(params) {
|
|
4286
|
+
emitTrustedDiagnosticEvent({
|
|
4287
|
+
type: "tool.execution.started",
|
|
4288
|
+
agentId: params.agentId,
|
|
4289
|
+
runId: params.runId,
|
|
4290
|
+
sessionId: params.sessionId,
|
|
4291
|
+
sessionKey: params.sessionKey,
|
|
4292
|
+
toolName: params.call.tool,
|
|
4293
|
+
toolCallId: params.call.callId
|
|
4294
|
+
});
|
|
4295
|
+
}
|
|
4296
|
+
/** Emits an error event for one Codex dynamic tool call. */
|
|
4297
|
+
function emitDynamicToolErrorDiagnostic(params) {
|
|
4298
|
+
emitTrustedDiagnosticEvent({
|
|
4299
|
+
type: "tool.execution.error",
|
|
4300
|
+
agentId: params.agentId,
|
|
4301
|
+
runId: params.runId,
|
|
4302
|
+
sessionId: params.sessionId,
|
|
4303
|
+
sessionKey: params.sessionKey,
|
|
4304
|
+
toolName: params.call.tool,
|
|
4305
|
+
toolCallId: params.call.callId,
|
|
4306
|
+
durationMs: params.durationMs,
|
|
4307
|
+
errorCategory: "codex_dynamic_tool_error",
|
|
4308
|
+
terminalReason: params.terminalReason ?? "failed"
|
|
4309
|
+
});
|
|
4310
|
+
}
|
|
4311
|
+
/** Emits the terminal event matching a dynamic tool response's diagnostic type. */
|
|
4312
|
+
function emitDynamicToolTerminalDiagnostic(params) {
|
|
4313
|
+
const terminalType = params.response.diagnosticTerminalType ?? (params.response.success ? "completed" : "error");
|
|
4314
|
+
if (terminalType === "completed") {
|
|
4315
|
+
emitTrustedDiagnosticEvent({
|
|
4316
|
+
type: "tool.execution.completed",
|
|
4317
|
+
agentId: params.agentId,
|
|
4318
|
+
runId: params.runId,
|
|
4319
|
+
sessionId: params.sessionId,
|
|
4320
|
+
sessionKey: params.sessionKey,
|
|
4321
|
+
toolName: params.call.tool,
|
|
4322
|
+
toolCallId: params.call.callId,
|
|
4323
|
+
durationMs: params.durationMs
|
|
4324
|
+
});
|
|
4325
|
+
return;
|
|
4326
|
+
}
|
|
4327
|
+
if (terminalType === "blocked") {
|
|
4328
|
+
emitTrustedDiagnosticEvent({
|
|
4329
|
+
type: "tool.execution.blocked",
|
|
4330
|
+
agentId: params.agentId,
|
|
4331
|
+
runId: params.runId,
|
|
4332
|
+
sessionId: params.sessionId,
|
|
4333
|
+
sessionKey: params.sessionKey,
|
|
4334
|
+
toolName: params.call.tool,
|
|
4335
|
+
toolCallId: params.call.callId,
|
|
4336
|
+
deniedReason: "plugin-before-tool-call",
|
|
4337
|
+
reason: "Tool call blocked"
|
|
4338
|
+
});
|
|
4339
|
+
return;
|
|
4340
|
+
}
|
|
4341
|
+
emitDynamicToolErrorDiagnostic({
|
|
4342
|
+
...params,
|
|
4343
|
+
terminalReason: params.response.diagnosticTerminalReason ?? "failed"
|
|
4344
|
+
});
|
|
4345
|
+
}
|
|
4346
|
+
//#endregion
|
|
4347
|
+
//#region extensions/codex/src/app-server/elicitation-bridge.ts
|
|
4348
|
+
const MCP_TOOL_APPROVAL_KIND = "mcp_tool_call";
|
|
4349
|
+
const MCP_TOOL_APPROVAL_KIND_KEY = "codex_approval_kind";
|
|
4350
|
+
const MCP_TOOL_APPROVAL_CONNECTOR_NAME_KEY = "connector_name";
|
|
4351
|
+
const MCP_TOOL_APPROVAL_TOOL_TITLE_KEY = "tool_title";
|
|
4352
|
+
const MCP_TOOL_APPROVAL_TOOL_DESCRIPTION_KEY = "tool_description";
|
|
4353
|
+
const MCP_TOOL_APPROVAL_TOOL_PARAMS_DISPLAY_KEY = "tool_params_display";
|
|
4354
|
+
const MCP_TOOL_APPROVAL_SOURCE_KEY = "source";
|
|
4355
|
+
const MCP_TOOL_APPROVAL_CONNECTOR_SOURCE = "connector";
|
|
4356
|
+
const CODEX_APPS_SERVER_NAME = "codex_apps";
|
|
4357
|
+
const COMPUTER_USE_APPROVAL_TITLE = "Computer Use approval";
|
|
4358
|
+
const EMPTY_OBJECT_SCHEMA = {
|
|
4359
|
+
type: "object",
|
|
4360
|
+
properties: {}
|
|
4361
|
+
};
|
|
4362
|
+
const PLUGIN_APP_ID_META_KEYS = [
|
|
4363
|
+
"app_id",
|
|
4364
|
+
"appId",
|
|
4365
|
+
"codex_app_id",
|
|
4366
|
+
"codexAppId"
|
|
4367
|
+
];
|
|
4368
|
+
const PLUGIN_CONNECTOR_ID_META_KEYS = ["connector_id", "connectorId"];
|
|
4369
|
+
const PLUGIN_NAME_META_KEYS = [
|
|
4370
|
+
"plugin_name",
|
|
4371
|
+
"pluginName",
|
|
4372
|
+
"codex_plugin_name",
|
|
4373
|
+
"codexPluginName"
|
|
4374
|
+
];
|
|
4375
|
+
const PLUGIN_CONFIG_KEY_META_KEYS = [
|
|
4376
|
+
"config_key",
|
|
4377
|
+
"configKey",
|
|
4378
|
+
"codex_config_key"
|
|
4379
|
+
];
|
|
4380
|
+
const PLUGIN_MARKETPLACE_NAME_META_KEYS = [
|
|
4381
|
+
"marketplace_name",
|
|
4382
|
+
"marketplaceName",
|
|
4383
|
+
"codex_marketplace_name",
|
|
4384
|
+
"codexMarketplaceName"
|
|
4385
|
+
];
|
|
4386
|
+
const MAX_DISPLAY_PARAM_ENTRIES = 8;
|
|
4387
|
+
const MAX_DISPLAY_PARAM_VALUE_LENGTH = 120;
|
|
4388
|
+
const MAX_DISPLAY_VALUE_ARRAY_ITEMS = 8;
|
|
4389
|
+
const MAX_DISPLAY_VALUE_OBJECT_KEYS = 8;
|
|
4390
|
+
const MAX_DISPLAY_VALUE_DEPTH = 3;
|
|
4391
|
+
const DISPLAY_TEXT_SCAN_MAX_LENGTH = 4096;
|
|
4392
|
+
async function handleCodexAppServerElicitationRequest(params) {
|
|
4393
|
+
const requestParams = isJsonObject(params.requestParams) ? params.requestParams : void 0;
|
|
4394
|
+
if (!requestParams || readNonBlankStringField(requestParams, "threadId") !== params.threadId) return;
|
|
4395
|
+
const requestTurnId = requestParams.turnId;
|
|
4396
|
+
if (requestTurnId !== null && requestTurnId !== void 0 && requestTurnId !== params.turnId) return;
|
|
4397
|
+
const pluginResolution = resolvePluginElicitation({
|
|
4398
|
+
requestParams,
|
|
4399
|
+
pluginAppPolicyContext: params.pluginAppPolicyContext
|
|
4400
|
+
});
|
|
4401
|
+
if (pluginResolution.kind !== "not_plugin") {
|
|
4402
|
+
if (params.paramsForRun.trigger === "cron" && params.paramsForRun.scheduledRuntimeAuthority) {
|
|
4403
|
+
logPluginElicitationDecline("scheduled_authority_non_interactive", requestParams);
|
|
4404
|
+
return declineElicitationResponse();
|
|
4405
|
+
}
|
|
4406
|
+
if (pluginResolution.kind === "decline") {
|
|
4407
|
+
logPluginElicitationDecline(pluginResolution.reason, requestParams);
|
|
4408
|
+
return declineElicitationResponse();
|
|
4409
|
+
}
|
|
4410
|
+
if (requestTurnId !== params.turnId) {
|
|
4411
|
+
logPluginElicitationDecline("missing_active_turn", requestParams);
|
|
4412
|
+
return declineElicitationResponse();
|
|
4413
|
+
}
|
|
4414
|
+
return await buildPluginPolicyElicitationResponse({
|
|
4415
|
+
entry: pluginResolution.entry,
|
|
4416
|
+
requestParams,
|
|
4417
|
+
paramsForRun: params.paramsForRun,
|
|
4418
|
+
signal: params.signal
|
|
4419
|
+
});
|
|
4420
|
+
}
|
|
4421
|
+
const approvalPrompt = readComputerUseApprovalElicitation(requestParams, params.computerUseMcpServerName) ?? readBridgeableApprovalElicitation(requestParams);
|
|
4422
|
+
if (!approvalPrompt) return;
|
|
4423
|
+
return buildElicitationResponse(approvalPrompt, await requestPluginApprovalOutcome({
|
|
4424
|
+
paramsForRun: params.paramsForRun,
|
|
4425
|
+
title: approvalPrompt.title,
|
|
4426
|
+
description: approvalPrompt.description,
|
|
4427
|
+
allowedDecisions: approvalPrompt.allowedDecisions,
|
|
4428
|
+
signal: params.signal
|
|
4429
|
+
}));
|
|
4430
|
+
}
|
|
4431
|
+
function resolvePluginElicitation(params) {
|
|
4432
|
+
const requestParams = params.requestParams;
|
|
4433
|
+
const meta = isJsonObject(requestParams["_meta"]) ? requestParams["_meta"] : {};
|
|
4434
|
+
const context = params.pluginAppPolicyContext;
|
|
4435
|
+
const entries = context ? Object.values(context.apps) : [];
|
|
4436
|
+
const pluginEntries = entries.filter(isPluginAppPolicyContextEntry);
|
|
4437
|
+
const appId = readFirstString$1(meta, PLUGIN_APP_ID_META_KEYS) ?? readFirstString$1(requestParams, PLUGIN_APP_ID_META_KEYS);
|
|
4438
|
+
const connectorId = readFirstString$1(meta, PLUGIN_CONNECTOR_ID_META_KEYS);
|
|
4439
|
+
const isCodexConnectorApproval = isCodexConnectorApprovalElicitation(requestParams, meta);
|
|
4440
|
+
if (isCodexConnectorApproval && appId && connectorId && appId !== connectorId) return {
|
|
4441
|
+
kind: "decline",
|
|
4442
|
+
reason: "app_id_connector_id_mismatch"
|
|
4443
|
+
};
|
|
4444
|
+
if (appId) {
|
|
4445
|
+
if (!context) return {
|
|
4446
|
+
kind: "decline",
|
|
4447
|
+
reason: "missing_policy_context"
|
|
4448
|
+
};
|
|
4449
|
+
const entry = context.apps[appId];
|
|
4450
|
+
if (entry?.source === "account" && !isCodexConnectorApproval) return {
|
|
4451
|
+
kind: "decline",
|
|
4452
|
+
reason: "account_app_source_mismatch"
|
|
4453
|
+
};
|
|
4454
|
+
return uniquePluginMatch(entry ? [entry] : [], "app_id");
|
|
4455
|
+
}
|
|
4456
|
+
if (isCodexConnectorApproval && connectorId) {
|
|
4457
|
+
if (!context) return {
|
|
4458
|
+
kind: "decline",
|
|
4459
|
+
reason: "missing_policy_context"
|
|
4460
|
+
};
|
|
4461
|
+
const entry = context.apps[connectorId];
|
|
4462
|
+
return uniquePluginMatch(entry ? [entry] : [], "connector_id");
|
|
4463
|
+
}
|
|
4464
|
+
const serverName = readNonBlankStringField(requestParams, "serverName");
|
|
4465
|
+
if (serverName && context) {
|
|
4466
|
+
const matches = entries.filter((entry) => entry.mcpServerNames.includes(serverName));
|
|
4467
|
+
if (matches.length > 0) return uniquePluginMatch(matches, "server_name");
|
|
4468
|
+
}
|
|
4469
|
+
const metadataResolution = resolvePluginStableMetadataMatch({
|
|
4470
|
+
meta,
|
|
4471
|
+
requestParams,
|
|
4472
|
+
entries: pluginEntries,
|
|
4473
|
+
context
|
|
4474
|
+
});
|
|
4475
|
+
if (metadataResolution.kind !== "not_plugin") return metadataResolution;
|
|
4476
|
+
if (context && hasDisplayNameOnlyPluginMatch(meta, entries)) return {
|
|
4477
|
+
kind: "decline",
|
|
4478
|
+
reason: "display_name_only"
|
|
4479
|
+
};
|
|
4480
|
+
return { kind: "not_plugin" };
|
|
4481
|
+
}
|
|
4482
|
+
function isCodexConnectorApprovalElicitation(requestParams, meta) {
|
|
4483
|
+
return readNonBlankStringField(requestParams, "serverName") === CODEX_APPS_SERVER_NAME && readNonBlankStringField(meta, MCP_TOOL_APPROVAL_KIND_KEY) === MCP_TOOL_APPROVAL_KIND && readNonBlankStringField(meta, MCP_TOOL_APPROVAL_SOURCE_KEY) === MCP_TOOL_APPROVAL_CONNECTOR_SOURCE;
|
|
4484
|
+
}
|
|
4485
|
+
function resolvePluginStableMetadataMatch(params) {
|
|
4486
|
+
const pluginName = readFirstString$1(params.meta, PLUGIN_NAME_META_KEYS) ?? readFirstString$1(params.requestParams, PLUGIN_NAME_META_KEYS);
|
|
4487
|
+
const configKey = readFirstString$1(params.meta, PLUGIN_CONFIG_KEY_META_KEYS) ?? readFirstString$1(params.requestParams, PLUGIN_CONFIG_KEY_META_KEYS);
|
|
4488
|
+
const marketplaceName = readFirstString$1(params.meta, PLUGIN_MARKETPLACE_NAME_META_KEYS) ?? readFirstString$1(params.requestParams, PLUGIN_MARKETPLACE_NAME_META_KEYS);
|
|
4489
|
+
if (!pluginName && !configKey) return { kind: "not_plugin" };
|
|
4490
|
+
if (!params.context) return {
|
|
4491
|
+
kind: "decline",
|
|
4492
|
+
reason: "missing_policy_context"
|
|
4493
|
+
};
|
|
4494
|
+
return uniquePluginMatch(params.entries.filter((entry) => {
|
|
4495
|
+
if (marketplaceName && entry.marketplaceName !== marketplaceName) return false;
|
|
4496
|
+
if (pluginName && entry.pluginName !== pluginName) return false;
|
|
4497
|
+
if (configKey && entry.configKey !== configKey) return false;
|
|
4498
|
+
return true;
|
|
4499
|
+
}), "metadata");
|
|
4500
|
+
}
|
|
4501
|
+
function uniquePluginMatch(matches, source) {
|
|
4502
|
+
if (matches.length === 1 && matches[0]) return {
|
|
4503
|
+
kind: "matched",
|
|
4504
|
+
entry: matches[0]
|
|
4505
|
+
};
|
|
4506
|
+
return {
|
|
4507
|
+
kind: "decline",
|
|
4508
|
+
reason: matches.length === 0 ? `${source}_not_enabled` : `${source}_ambiguous`
|
|
4509
|
+
};
|
|
4510
|
+
}
|
|
4511
|
+
function hasDisplayNameOnlyPluginMatch(meta, entries) {
|
|
4512
|
+
const connectorName = readNonBlankStringField(meta, MCP_TOOL_APPROVAL_CONNECTOR_NAME_KEY);
|
|
4513
|
+
if (!connectorName) return false;
|
|
4514
|
+
const normalized = normalizePluginIdentityText(connectorName);
|
|
4515
|
+
return entries.some((entry) => normalizePluginIdentityText(appPolicyDisplayName(entry)) === normalized || isPluginAppPolicyContextEntry(entry) && normalizePluginIdentityText(entry.configKey) === normalized);
|
|
4516
|
+
}
|
|
4517
|
+
function isPluginAppPolicyContextEntry(entry) {
|
|
4518
|
+
return entry.source !== "account";
|
|
4519
|
+
}
|
|
4520
|
+
function appPolicyDisplayName(entry) {
|
|
4521
|
+
return isPluginAppPolicyContextEntry(entry) ? entry.pluginName : entry.appName;
|
|
4522
|
+
}
|
|
4523
|
+
function normalizePluginIdentityText(value) {
|
|
4524
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
4525
|
+
}
|
|
4526
|
+
async function buildPluginPolicyElicitationResponse(params) {
|
|
4527
|
+
const mode = resolvePluginDestructiveApprovalMode(params.entry);
|
|
4528
|
+
if (mode === "deny") {
|
|
4529
|
+
logPluginElicitationDecline("destructive_actions_disabled", params.requestParams);
|
|
4530
|
+
return declineElicitationResponse();
|
|
4531
|
+
}
|
|
4532
|
+
const approvalPrompt = readPluginApprovalElicitation(params.entry, params.requestParams);
|
|
4533
|
+
if (!approvalPrompt) {
|
|
4534
|
+
logPluginElicitationDecline("unsupported_schema", params.requestParams);
|
|
4535
|
+
return declineElicitationResponse();
|
|
4536
|
+
}
|
|
4537
|
+
const response = buildElicitationResponse(approvalPrompt, "approved-once");
|
|
4538
|
+
if (isJsonObject(response) && response.action === "accept") {
|
|
4539
|
+
if (mode === "allow") return response;
|
|
4540
|
+
return buildElicitationResponse(approvalPrompt, oneShotPluginPolicyApprovalOutcome(mode, await requestPluginApprovalOutcome({
|
|
4541
|
+
paramsForRun: params.paramsForRun,
|
|
4542
|
+
title: approvalPrompt.title,
|
|
4543
|
+
description: approvalPrompt.description,
|
|
4544
|
+
allowedDecisions: allowedPluginPolicyApprovalDecisions(mode, approvalPrompt),
|
|
4545
|
+
signal: params.signal
|
|
4546
|
+
})));
|
|
4547
|
+
}
|
|
4548
|
+
logPluginElicitationDecline("unmappable_schema", params.requestParams);
|
|
4549
|
+
return declineElicitationResponse();
|
|
4550
|
+
}
|
|
4551
|
+
function resolvePluginDestructiveApprovalMode(entry) {
|
|
4552
|
+
return entry.destructiveApprovalMode ?? (entry.allowDestructiveActions ? "allow" : "deny");
|
|
4553
|
+
}
|
|
4554
|
+
function allowedPluginPolicyApprovalDecisions(mode, approvalPrompt) {
|
|
4555
|
+
const allowedDecisions = approvalPrompt.allowedDecisions ?? ["allow-once", "deny"];
|
|
4556
|
+
if (mode !== "ask") return allowedDecisions;
|
|
4557
|
+
return allowedDecisions.filter((decision) => decision !== "allow-always");
|
|
4558
|
+
}
|
|
4559
|
+
function oneShotPluginPolicyApprovalOutcome(mode, outcome) {
|
|
4560
|
+
return mode === "ask" && outcome === "approved-session" ? "approved-once" : outcome;
|
|
4561
|
+
}
|
|
4562
|
+
function readPluginApprovalElicitation(entry, requestParams) {
|
|
4563
|
+
if (readNonBlankStringField(requestParams, "mode") !== "form" || !isJsonObject(requestParams.requestedSchema)) return;
|
|
4564
|
+
const requestedSchema = requestParams.requestedSchema;
|
|
4565
|
+
if (readNonBlankStringField(requestedSchema, "type") !== "object" || !isJsonObject(requestedSchema.properties)) return;
|
|
4566
|
+
const meta = isJsonObject(requestParams["_meta"]) ? requestParams["_meta"] : {};
|
|
4567
|
+
const title = sanitizeDisplayText(readNonBlankStringField(requestParams, "message") ?? "") || "Codex plugin approval";
|
|
4568
|
+
const descriptionMeta = { ...meta };
|
|
4569
|
+
if (!readNonBlankStringField(descriptionMeta, MCP_TOOL_APPROVAL_CONNECTOR_NAME_KEY)) descriptionMeta[MCP_TOOL_APPROVAL_CONNECTOR_NAME_KEY] = appPolicyDisplayName(entry);
|
|
4570
|
+
return {
|
|
4571
|
+
title,
|
|
4572
|
+
description: buildApprovalDescription({
|
|
4573
|
+
title,
|
|
4574
|
+
meta: descriptionMeta,
|
|
4575
|
+
requestedSchema,
|
|
4576
|
+
serverName: sanitizeOptionalDisplayText(readNonBlankStringField(requestParams, "serverName"))
|
|
4577
|
+
}),
|
|
4578
|
+
requestedSchema,
|
|
4579
|
+
meta,
|
|
4580
|
+
persistHintsMode: "explicit",
|
|
4581
|
+
allowedDecisions: buildApprovalAllowedDecisions(requestedSchema, meta)
|
|
4582
|
+
};
|
|
4583
|
+
}
|
|
4584
|
+
function buildApprovalAllowedDecisions(requestedSchema, meta) {
|
|
4585
|
+
return canMapPersistentApproval(requestedSchema, meta) ? [
|
|
4586
|
+
"allow-once",
|
|
4587
|
+
"allow-always",
|
|
4588
|
+
"deny"
|
|
4589
|
+
] : ["allow-once", "deny"];
|
|
4590
|
+
}
|
|
4591
|
+
function canMapPersistentApproval(requestedSchema, meta) {
|
|
4592
|
+
const persistHints = readPersistHints(meta, "explicit");
|
|
4593
|
+
if (persistHints.length > 0) return persistHints.includes("always");
|
|
4594
|
+
const properties = isJsonObject(requestedSchema.properties) ? requestedSchema.properties : {};
|
|
4595
|
+
return Object.entries(properties).some(([name, value]) => {
|
|
4596
|
+
const schema = isJsonObject(value) ? value : void 0;
|
|
4597
|
+
if (!schema) return false;
|
|
4598
|
+
return isPersistField({
|
|
4599
|
+
name,
|
|
4600
|
+
schema,
|
|
4601
|
+
required: false
|
|
4602
|
+
}) && chooseAlwaysPersistOptionValue(readEnumOptions(schema)) !== void 0;
|
|
4603
|
+
});
|
|
4604
|
+
}
|
|
4605
|
+
function declineElicitationResponse() {
|
|
4606
|
+
return {
|
|
4607
|
+
action: "decline",
|
|
4608
|
+
content: null,
|
|
4609
|
+
_meta: null
|
|
4610
|
+
};
|
|
4611
|
+
}
|
|
4612
|
+
function logPluginElicitationDecline(reason, requestParams) {
|
|
4613
|
+
embeddedAgentLog.debug("codex plugin elicitation declined", {
|
|
4614
|
+
reason,
|
|
4615
|
+
serverName: readNonBlankStringField(requestParams, "serverName"),
|
|
4616
|
+
mode: readNonBlankStringField(requestParams, "mode")
|
|
4617
|
+
});
|
|
4618
|
+
}
|
|
4619
|
+
function readBridgeableApprovalElicitation(requestParams) {
|
|
4620
|
+
if (!requestParams || readNonBlankStringField(requestParams, "mode") !== "form" || !isJsonObject(requestParams["_meta"]) || requestParams["_meta"][MCP_TOOL_APPROVAL_KIND_KEY] !== MCP_TOOL_APPROVAL_KIND || !isJsonObject(requestParams.requestedSchema)) return;
|
|
4621
|
+
const requestedSchema = requestParams.requestedSchema;
|
|
4622
|
+
if (readNonBlankStringField(requestedSchema, "type") !== "object" || !isJsonObject(requestedSchema.properties)) return;
|
|
4623
|
+
const title = sanitizeDisplayText(readNonBlankStringField(requestParams, "message") ?? "") || "Codex MCP tool approval";
|
|
4624
|
+
return {
|
|
4625
|
+
title,
|
|
4626
|
+
description: buildApprovalDescription({
|
|
4627
|
+
title,
|
|
4628
|
+
meta: requestParams["_meta"],
|
|
4629
|
+
requestedSchema,
|
|
4630
|
+
serverName: sanitizeOptionalDisplayText(readNonBlankStringField(requestParams, "serverName"))
|
|
4631
|
+
}),
|
|
4632
|
+
requestedSchema,
|
|
4633
|
+
meta: requestParams["_meta"]
|
|
4634
|
+
};
|
|
4635
|
+
}
|
|
4636
|
+
function readComputerUseApprovalElicitation(requestParams, expectedServerName) {
|
|
4637
|
+
const serverName = readNonBlankStringField(requestParams, "serverName");
|
|
4638
|
+
if (!serverName || !expectedServerName || serverName !== expectedServerName || readNonBlankStringField(requestParams, "mode") !== "form") return;
|
|
4639
|
+
const requestedSchema = isJsonObject(requestParams?.requestedSchema) ? requestParams.requestedSchema : EMPTY_OBJECT_SCHEMA;
|
|
4640
|
+
if (readNonBlankStringField(requestedSchema, "type") !== "object" || !isJsonObject(requestedSchema.properties)) return;
|
|
4641
|
+
const meta = isJsonObject(requestParams?.["_meta"]) ? requestParams["_meta"] : {};
|
|
4642
|
+
const title = sanitizeDisplayText(readNonBlankStringField(requestParams, "message") ?? "") || COMPUTER_USE_APPROVAL_TITLE;
|
|
4643
|
+
return {
|
|
4644
|
+
title,
|
|
4645
|
+
description: buildApprovalDescription({
|
|
4646
|
+
title,
|
|
4647
|
+
meta,
|
|
4648
|
+
requestedSchema,
|
|
4649
|
+
serverName: sanitizeOptionalDisplayText(serverName)
|
|
4650
|
+
}),
|
|
4651
|
+
requestedSchema,
|
|
4652
|
+
meta
|
|
4653
|
+
};
|
|
4654
|
+
}
|
|
4655
|
+
function buildApprovalDescription(params) {
|
|
4656
|
+
const connectorName = sanitizeOptionalDisplayText(readNonBlankStringField(params.meta, MCP_TOOL_APPROVAL_CONNECTOR_NAME_KEY));
|
|
4657
|
+
const toolTitle = sanitizeOptionalDisplayText(readNonBlankStringField(params.meta, MCP_TOOL_APPROVAL_TOOL_TITLE_KEY));
|
|
4658
|
+
const toolDescription = sanitizeOptionalDisplayText(readNonBlankStringField(params.meta, MCP_TOOL_APPROVAL_TOOL_DESCRIPTION_KEY));
|
|
4659
|
+
const summaryLines = [
|
|
4660
|
+
connectorName && `App: ${connectorName}`,
|
|
4661
|
+
toolTitle && `Tool: ${toolTitle}`,
|
|
4662
|
+
params.serverName && `MCP server: ${params.serverName}`,
|
|
4663
|
+
toolDescription
|
|
4664
|
+
].filter((line) => Boolean(line));
|
|
4665
|
+
const paramLines = readDisplayParamLines(params.meta);
|
|
4666
|
+
const propertyLines = readPropertyDescriptionLines(params.requestedSchema);
|
|
4667
|
+
return [
|
|
4668
|
+
params.title,
|
|
4669
|
+
summaryLines.join("\n"),
|
|
4670
|
+
paramLines.length > 0 ? ["Parameters:", ...paramLines].join("\n") : "",
|
|
4671
|
+
propertyLines.length > 0 ? ["Fields:", ...propertyLines].join("\n") : ""
|
|
4672
|
+
].filter(Boolean).join("\n\n");
|
|
4673
|
+
}
|
|
4674
|
+
function readPropertyDescriptionLines(requestedSchema) {
|
|
4675
|
+
const properties = isJsonObject(requestedSchema.properties) ? requestedSchema.properties : {};
|
|
4676
|
+
return Object.entries(properties).map(([name, value]) => {
|
|
4677
|
+
const schema = isJsonObject(value) ? value : void 0;
|
|
4678
|
+
if (!schema) return;
|
|
4679
|
+
const propTitle = sanitizeDisplayText(readNonBlankStringField(schema, "title") ?? "") || sanitizeDisplayText(name) || "field";
|
|
4680
|
+
const description = sanitizeOptionalDisplayText(readNonBlankStringField(schema, "description"));
|
|
4681
|
+
return description ? `- ${propTitle}: ${description}` : `- ${propTitle}`;
|
|
4682
|
+
}).filter((line) => Boolean(line));
|
|
4683
|
+
}
|
|
4684
|
+
function readDisplayParamLines(meta) {
|
|
4685
|
+
const displayParams = meta[MCP_TOOL_APPROVAL_TOOL_PARAMS_DISPLAY_KEY];
|
|
4686
|
+
if (!Array.isArray(displayParams)) return [];
|
|
4687
|
+
const lines = displayParams.slice(0, MAX_DISPLAY_PARAM_ENTRIES).map((entry) => {
|
|
4688
|
+
const param = isJsonObject(entry) ? entry : void 0;
|
|
4689
|
+
if (!param) return;
|
|
4690
|
+
const name = sanitizeOptionalDisplayText(readNonBlankStringField(param, "display_name")) ?? sanitizeOptionalDisplayText(readNonBlankStringField(param, "name"));
|
|
4691
|
+
if (!name) return;
|
|
4692
|
+
return `- ${name}: ${formatDisplayParamValue(param.value)}`;
|
|
4693
|
+
}).filter((line) => Boolean(line));
|
|
4694
|
+
const remaining = displayParams.length - MAX_DISPLAY_PARAM_ENTRIES;
|
|
4695
|
+
return remaining > 0 ? [...lines, `- Additional parameters: ${remaining} more`] : lines;
|
|
4696
|
+
}
|
|
4697
|
+
function formatDisplayParamValue(value) {
|
|
4698
|
+
return truncateCodexApprovalDisplayText(sanitizeDisplayText(typeof value === "string" ? value : formatDisplayJsonValue(value ?? null)), MAX_DISPLAY_PARAM_VALUE_LENGTH);
|
|
4699
|
+
}
|
|
4700
|
+
function formatDisplayJsonValue(value, depth = MAX_DISPLAY_VALUE_DEPTH) {
|
|
4701
|
+
if (value === null) return "null";
|
|
4702
|
+
if (typeof value === "string") return JSON.stringify(truncateCodexApprovalDisplayText(sanitizeDisplayText(value), 80));
|
|
4703
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
4704
|
+
if (Array.isArray(value)) {
|
|
4705
|
+
if (depth <= 0) return "[truncated]";
|
|
4706
|
+
const parts = [];
|
|
4707
|
+
const limit = Math.min(value.length, MAX_DISPLAY_VALUE_ARRAY_ITEMS);
|
|
4708
|
+
for (let i = 0; i < limit; i += 1) parts.push(formatDisplayJsonValue(value[i] ?? null, depth - 1));
|
|
4709
|
+
if (value.length > MAX_DISPLAY_VALUE_ARRAY_ITEMS) parts.push("...");
|
|
4710
|
+
return `[${parts.join(",")}]`;
|
|
4711
|
+
}
|
|
4712
|
+
if (typeof value === "object") {
|
|
4713
|
+
if (depth <= 0) return "{truncated}";
|
|
4714
|
+
const parts = [];
|
|
4715
|
+
let count = 0;
|
|
4716
|
+
let truncated = false;
|
|
4717
|
+
for (const key in value) {
|
|
4718
|
+
if (!Object.hasOwn(value, key)) continue;
|
|
4719
|
+
if (count >= MAX_DISPLAY_VALUE_OBJECT_KEYS) {
|
|
4720
|
+
truncated = true;
|
|
4721
|
+
break;
|
|
4722
|
+
}
|
|
4723
|
+
const safeKey = truncateCodexApprovalDisplayText(sanitizeDisplayText(key), 80);
|
|
4724
|
+
parts.push(`${JSON.stringify(safeKey)}:${formatDisplayJsonValue(value[key] ?? null, depth - 1)}`);
|
|
4725
|
+
count += 1;
|
|
4726
|
+
}
|
|
4727
|
+
if (truncated) parts.push("...");
|
|
4728
|
+
return `{${parts.join(",")}}`;
|
|
4729
|
+
}
|
|
4730
|
+
return "null";
|
|
4731
|
+
}
|
|
4732
|
+
function sanitizeOptionalDisplayText(value) {
|
|
4733
|
+
return (value === void 0 ? "" : sanitizeDisplayText(value)) || void 0;
|
|
4734
|
+
}
|
|
4735
|
+
function sanitizeDisplayText(value) {
|
|
4736
|
+
const scanned = sliceUtf16Safe(value, 0, DISPLAY_TEXT_SCAN_MAX_LENGTH);
|
|
4737
|
+
const clipped = value.length > DISPLAY_TEXT_SCAN_MAX_LENGTH;
|
|
4738
|
+
const sanitized = sanitizeCodexApprovalVisibleText(scanned, { stripDanglingTerminalSequence: true });
|
|
4739
|
+
const escaped = sanitized ? formatCodexDisplayText(sanitized) : "";
|
|
4740
|
+
return clipped && escaped ? `${escaped}...` : escaped;
|
|
4741
|
+
}
|
|
4742
|
+
async function requestPluginApprovalOutcome(params) {
|
|
4743
|
+
try {
|
|
4744
|
+
const requestResult = await requestPluginApproval({
|
|
4745
|
+
hostCapabilities: params.paramsForRun.hostCapabilities,
|
|
4746
|
+
title: params.title,
|
|
4747
|
+
description: params.description,
|
|
4748
|
+
severity: "warning",
|
|
4749
|
+
toolName: "codex_mcp_tool_approval",
|
|
4750
|
+
allowedDecisions: params.allowedDecisions
|
|
4751
|
+
});
|
|
4752
|
+
const approvalId = requestResult?.id;
|
|
4753
|
+
if (!approvalId) return "unavailable";
|
|
4754
|
+
return mapExecDecisionToOutcome(approvalRequestExplicitlyUnavailable(requestResult) ? null : await waitForPluginApprovalDecision({
|
|
4755
|
+
hostCapabilities: params.paramsForRun.hostCapabilities,
|
|
4756
|
+
approvalId,
|
|
4757
|
+
signal: params.signal
|
|
4758
|
+
}));
|
|
4759
|
+
} catch {
|
|
4760
|
+
return params.signal?.aborted ? "cancelled" : "denied";
|
|
4761
|
+
}
|
|
3389
4762
|
}
|
|
3390
|
-
function
|
|
3391
|
-
const
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
4763
|
+
function buildElicitationResponse(approvalPrompt, outcome) {
|
|
4764
|
+
const { requestedSchema, meta } = approvalPrompt;
|
|
4765
|
+
if (outcome === "cancelled") return {
|
|
4766
|
+
action: "cancel",
|
|
4767
|
+
content: null,
|
|
4768
|
+
_meta: null
|
|
4769
|
+
};
|
|
4770
|
+
if (outcome === "denied" || outcome === "unavailable") return {
|
|
4771
|
+
action: "decline",
|
|
4772
|
+
content: null,
|
|
4773
|
+
_meta: null
|
|
4774
|
+
};
|
|
4775
|
+
const content = buildAcceptedContent(approvalPrompt, outcome);
|
|
4776
|
+
if (!content && !hasNoSchemaProperties(requestedSchema)) {
|
|
4777
|
+
embeddedAgentLog.warn("codex MCP approval elicitation approved without a mappable response", {
|
|
4778
|
+
approvalKind: meta[MCP_TOOL_APPROVAL_KIND_KEY],
|
|
4779
|
+
fields: Object.keys(requestedSchema.properties ?? {}),
|
|
4780
|
+
outcome
|
|
4781
|
+
});
|
|
4782
|
+
return declineElicitationResponse();
|
|
3396
4783
|
}
|
|
3397
|
-
return
|
|
4784
|
+
return {
|
|
4785
|
+
action: "accept",
|
|
4786
|
+
content: content ?? null,
|
|
4787
|
+
_meta: buildAcceptedMeta(meta, outcome, approvalPrompt.persistHintsMode ?? "legacy")
|
|
4788
|
+
};
|
|
3398
4789
|
}
|
|
3399
|
-
function
|
|
3400
|
-
const
|
|
3401
|
-
const
|
|
3402
|
-
if (
|
|
3403
|
-
|
|
4790
|
+
function buildAcceptedContent(approvalPrompt, outcome) {
|
|
4791
|
+
const { requestedSchema, meta } = approvalPrompt;
|
|
4792
|
+
const properties = isJsonObject(requestedSchema.properties) ? requestedSchema.properties : void 0;
|
|
4793
|
+
if (!properties) return;
|
|
4794
|
+
const required = Array.isArray(requestedSchema.required) ? new Set(requestedSchema.required.filter((entry) => typeof entry === "string")) : /* @__PURE__ */ new Set();
|
|
4795
|
+
const content = {};
|
|
4796
|
+
let sawApprovalField = false;
|
|
4797
|
+
for (const [name, value] of Object.entries(properties)) {
|
|
4798
|
+
const schema = isJsonObject(value) ? value : void 0;
|
|
4799
|
+
if (!schema) continue;
|
|
4800
|
+
const property = {
|
|
4801
|
+
name,
|
|
4802
|
+
schema,
|
|
4803
|
+
required: required.has(name)
|
|
4804
|
+
};
|
|
4805
|
+
const next = readApprovalFieldValue(property, outcome) ?? readPersistFieldValue(property, meta, outcome, approvalPrompt.persistHintsMode ?? "legacy") ?? readFallbackFieldValue(property, outcome);
|
|
4806
|
+
if (next === void 0) {
|
|
4807
|
+
if (isApprovalField(property)) sawApprovalField = true;
|
|
4808
|
+
if (property.required) return;
|
|
4809
|
+
continue;
|
|
4810
|
+
}
|
|
4811
|
+
if (isApprovalField(property)) sawApprovalField = true;
|
|
4812
|
+
content[name] = next;
|
|
4813
|
+
}
|
|
4814
|
+
return sawApprovalField ? content : void 0;
|
|
3404
4815
|
}
|
|
3405
|
-
function
|
|
3406
|
-
|
|
3407
|
-
if (
|
|
3408
|
-
|
|
3409
|
-
|
|
4816
|
+
function readApprovalFieldValue(property, outcome) {
|
|
4817
|
+
if (!isApprovalField(property)) return;
|
|
4818
|
+
if (readNonBlankStringField(property.schema, "type") === "boolean") return true;
|
|
4819
|
+
const options = readEnumOptions(property.schema);
|
|
4820
|
+
if (options.length === 0) return;
|
|
4821
|
+
const sessionChoice = options.find((option) => isSessionApprovalOption(option));
|
|
4822
|
+
const acceptChoice = options.find((option) => isPositiveApprovalOption(option));
|
|
4823
|
+
if (outcome === "approved-session") return sessionChoice?.value ?? acceptChoice?.value;
|
|
4824
|
+
return acceptChoice?.value ?? sessionChoice?.value;
|
|
3410
4825
|
}
|
|
3411
|
-
function
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
4826
|
+
function readPersistFieldValue(property, meta, outcome, persistHintsMode) {
|
|
4827
|
+
if (!isPersistField(property) || outcome !== "approved-session") return;
|
|
4828
|
+
const persistHints = readPersistHints(meta, persistHintsMode);
|
|
4829
|
+
const options = readEnumOptions(property.schema);
|
|
4830
|
+
if (options.length === 0) return;
|
|
4831
|
+
const preferred = choosePersistHint(persistHints);
|
|
4832
|
+
if (preferred) return options.find((option) => option.value === preferred || option.label === preferred)?.value;
|
|
4833
|
+
if (persistHintsMode === "explicit") return chooseAlwaysPersistOptionValue(options);
|
|
3415
4834
|
}
|
|
3416
|
-
function
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
return available.find((entry) => isJsonObject(entry) && (isJsonObject(entry.acceptWithExecpolicyAmendment) || isJsonObject(entry.applyNetworkPolicyAmendment)));
|
|
4835
|
+
function readFallbackFieldValue(property, outcome) {
|
|
4836
|
+
if (outcome === "approved-once" && isPersistField(property)) return;
|
|
4837
|
+
return property.schema.default;
|
|
3420
4838
|
}
|
|
3421
|
-
function
|
|
3422
|
-
const
|
|
3423
|
-
|
|
3424
|
-
if (available.includes(preferred)) return preferred;
|
|
3425
|
-
const alternate = preferred === "decline" ? "cancel" : "decline";
|
|
3426
|
-
if (available.includes(alternate)) return alternate;
|
|
3427
|
-
return preferred;
|
|
4839
|
+
function isApprovalField(property) {
|
|
4840
|
+
const haystack = propertyText(property).toLowerCase();
|
|
4841
|
+
return /\b(approve|approval|allow|accept|decision)\b/.test(haystack);
|
|
3428
4842
|
}
|
|
3429
|
-
function
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
if (outcome === "cancelled") return "Codex app-server approval cancelled.";
|
|
3433
|
-
if (outcome === "unavailable") return "Codex app-server approval unavailable.";
|
|
3434
|
-
return "Codex app-server approval denied.";
|
|
4843
|
+
function isPersistField(property) {
|
|
4844
|
+
const haystack = propertyText(property).toLowerCase();
|
|
4845
|
+
return /\b(persist|session|always|scope)\b/.test(haystack);
|
|
3435
4846
|
}
|
|
3436
|
-
function
|
|
3437
|
-
return
|
|
4847
|
+
function propertyText(property) {
|
|
4848
|
+
return [
|
|
4849
|
+
property.name,
|
|
4850
|
+
readNonBlankStringField(property.schema, "title"),
|
|
4851
|
+
readNonBlankStringField(property.schema, "description")
|
|
4852
|
+
].filter(Boolean).join(" ");
|
|
3438
4853
|
}
|
|
3439
|
-
function
|
|
3440
|
-
|
|
4854
|
+
function readPersistHints(meta, mode = "legacy") {
|
|
4855
|
+
const raw = meta.persist;
|
|
4856
|
+
if (typeof raw === "string") return [raw];
|
|
4857
|
+
if (Array.isArray(raw)) return raw.filter((entry) => typeof entry === "string");
|
|
4858
|
+
return mode === "legacy" ? ["session", "always"] : [];
|
|
3441
4859
|
}
|
|
3442
|
-
function
|
|
3443
|
-
if (
|
|
3444
|
-
|
|
3445
|
-
return
|
|
4860
|
+
function buildAcceptedMeta(meta, outcome, persistHintsMode) {
|
|
4861
|
+
if (outcome !== "approved-session") return null;
|
|
4862
|
+
const persist = choosePersistHint(readPersistHints(meta, persistHintsMode));
|
|
4863
|
+
return persist ? { persist } : null;
|
|
3446
4864
|
}
|
|
3447
|
-
function
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
data
|
|
3451
|
-
});
|
|
4865
|
+
function choosePersistHint(persistHints) {
|
|
4866
|
+
if (persistHints.includes("always")) return "always";
|
|
4867
|
+
if (persistHints.includes("session")) return "session";
|
|
3452
4868
|
}
|
|
3453
|
-
function
|
|
3454
|
-
|
|
3455
|
-
if (actionCommand) return actionCommand;
|
|
3456
|
-
return readCommandPreview(record);
|
|
4869
|
+
function chooseAlwaysPersistOptionValue(options) {
|
|
4870
|
+
return options.find((option) => optionMatchesPersist(option, "always"))?.value;
|
|
3457
4871
|
}
|
|
3458
|
-
function
|
|
3459
|
-
|
|
3460
|
-
if (typeof command === "string") return command;
|
|
3461
|
-
if (Array.isArray(command) && command.every((part) => typeof part === "string")) return command.join(" ");
|
|
3462
|
-
const actionCommands = readCommandActions(record);
|
|
3463
|
-
if (actionCommands.length > 0) return actionCommands.join(" && ");
|
|
4872
|
+
function optionMatchesPersist(option, persist) {
|
|
4873
|
+
return option.value.toLowerCase() === persist || option.label.toLowerCase() === persist;
|
|
3464
4874
|
}
|
|
3465
|
-
function
|
|
3466
|
-
const
|
|
3467
|
-
|
|
3468
|
-
return actions.map((action) => isJsonObject(action) ? readString(action, "command") : void 0).filter((command) => Boolean(command));
|
|
4875
|
+
function hasNoSchemaProperties(requestedSchema) {
|
|
4876
|
+
const properties = isJsonObject(requestedSchema.properties) ? requestedSchema.properties : {};
|
|
4877
|
+
return Object.keys(properties).length === 0;
|
|
3469
4878
|
}
|
|
3470
|
-
function
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
4879
|
+
function readEnumOptions(schema) {
|
|
4880
|
+
if (Array.isArray(schema.enum)) {
|
|
4881
|
+
const values = schema.enum.filter((entry) => typeof entry === "string");
|
|
4882
|
+
const labels = Array.isArray(schema.enumNames) ? schema.enumNames.filter((entry) => typeof entry === "string") : [];
|
|
4883
|
+
return values.map((value, index) => ({
|
|
4884
|
+
value,
|
|
4885
|
+
label: labels[index] ?? value
|
|
4886
|
+
}));
|
|
3475
4887
|
}
|
|
3476
|
-
return
|
|
4888
|
+
if (Array.isArray(schema.oneOf)) return schema.oneOf.map((entry) => {
|
|
4889
|
+
const option = isJsonObject(entry) ? entry : void 0;
|
|
4890
|
+
const value = readNonBlankStringField(option, "const");
|
|
4891
|
+
if (!value) return;
|
|
4892
|
+
return {
|
|
4893
|
+
value,
|
|
4894
|
+
label: readNonBlankStringField(option, "title") ?? value
|
|
4895
|
+
};
|
|
4896
|
+
}).filter((entry) => Boolean(entry));
|
|
4897
|
+
return [];
|
|
3477
4898
|
}
|
|
3478
|
-
function
|
|
3479
|
-
const
|
|
3480
|
-
|
|
3481
|
-
if (!Array.isArray(command)) return;
|
|
3482
|
-
let source;
|
|
3483
|
-
for (const part of command) {
|
|
3484
|
-
if (typeof part !== "string") return;
|
|
3485
|
-
source = appendPreviewPart(source, part, " ");
|
|
3486
|
-
if (source.clipped) break;
|
|
3487
|
-
}
|
|
3488
|
-
return source;
|
|
4899
|
+
function isPositiveApprovalOption(option) {
|
|
4900
|
+
const haystack = `${option.value} ${option.label}`.toLowerCase();
|
|
4901
|
+
return /\b(allow|approve|accept|yes|continue|proceed|true)\b/.test(haystack);
|
|
3489
4902
|
}
|
|
3490
|
-
function
|
|
3491
|
-
const
|
|
3492
|
-
return
|
|
4903
|
+
function isSessionApprovalOption(option) {
|
|
4904
|
+
const haystack = `${option.value} ${option.label}`.toLowerCase();
|
|
4905
|
+
return /\b(session|always|persistent)\b/.test(haystack) && /\b(allow|approve|accept)\b/.test(haystack);
|
|
3493
4906
|
}
|
|
3494
|
-
function
|
|
4907
|
+
function readNonBlankStringField(record, key) {
|
|
3495
4908
|
const value = record?.[key];
|
|
3496
|
-
return typeof value === "string" ? value : void 0;
|
|
3497
|
-
}
|
|
3498
|
-
function truncate(value, maxLength) {
|
|
3499
|
-
return value.length <= maxLength ? value : `${truncateUtf16Safe(value, maxLength - 3)}...`;
|
|
3500
|
-
}
|
|
3501
|
-
function previewSource(value) {
|
|
3502
|
-
return {
|
|
3503
|
-
value: sliceUtf16Safe(value, 0, APPROVAL_PREVIEW_SCAN_MAX_LENGTH),
|
|
3504
|
-
clipped: value.length > APPROVAL_PREVIEW_SCAN_MAX_LENGTH
|
|
3505
|
-
};
|
|
3506
|
-
}
|
|
3507
|
-
function appendPreviewPart(source, part, separator) {
|
|
3508
|
-
const value = `${source?.value ? `${source.value}${separator}` : ""}${part}`;
|
|
3509
|
-
const clipped = source?.clipped === true || value.length > APPROVAL_PREVIEW_SCAN_MAX_LENGTH;
|
|
3510
|
-
return {
|
|
3511
|
-
value: sliceUtf16Safe(value, 0, APPROVAL_PREVIEW_SCAN_MAX_LENGTH),
|
|
3512
|
-
clipped
|
|
3513
|
-
};
|
|
3514
|
-
}
|
|
3515
|
-
function sanitizeApprovalPreview(source, maxLength) {
|
|
3516
|
-
if (!source || !source.value) return { omitted: false };
|
|
3517
|
-
const sanitized = sanitizeVisibleScalar(source.value.replace(DANGLING_TERMINAL_SEQUENCE_SUFFIX_RE, ""));
|
|
3518
|
-
if (!sanitized) return { omitted: true };
|
|
3519
|
-
return {
|
|
3520
|
-
text: formatCodexDisplayText(truncate(sanitized, maxLength)),
|
|
3521
|
-
omitted: source.clipped
|
|
3522
|
-
};
|
|
3523
|
-
}
|
|
3524
|
-
function sanitizeVisibleScalar(value) {
|
|
3525
|
-
return value.replace(ANSI_OSC_SEQUENCE_RE, "").replace(ANSI_CONTROL_SEQUENCE_RE, "").replace(INVISIBLE_FORMATTING_CONTROL_RE, " ").replace(CONTROL_CHARACTER_RE, " ").replace(/\s+/g, " ").trim();
|
|
3526
|
-
}
|
|
3527
|
-
function formatApprovalPreviewSubject(text, omitted) {
|
|
3528
|
-
return omitted ? `${text} ${APPROVAL_PREVIEW_OMITTED}` : text;
|
|
4909
|
+
return typeof value === "string" && value.trim() ? value : void 0;
|
|
3529
4910
|
}
|
|
3530
|
-
function
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
const next = `${description}${prefix}${line}`;
|
|
3535
|
-
if (next.length <= maxLength) {
|
|
3536
|
-
description = next;
|
|
3537
|
-
continue;
|
|
3538
|
-
}
|
|
3539
|
-
const remaining = maxLength - description.length - prefix.length;
|
|
3540
|
-
if (remaining < 3) break;
|
|
3541
|
-
description += `${prefix}${truncate(line, remaining)}`;
|
|
3542
|
-
break;
|
|
4911
|
+
function readFirstString$1(record, keys) {
|
|
4912
|
+
for (const key of keys) {
|
|
4913
|
+
const value = readNonBlankStringField(record, key);
|
|
4914
|
+
if (value) return value;
|
|
3543
4915
|
}
|
|
3544
|
-
return description;
|
|
3545
|
-
}
|
|
3546
|
-
function formatErrorMessage$1(error) {
|
|
3547
|
-
return error instanceof Error ? error.message : String(error);
|
|
3548
4916
|
}
|
|
3549
4917
|
//#endregion
|
|
3550
4918
|
//#region extensions/codex/src/app-server/dynamic-tools.ts
|
|
@@ -3670,6 +5038,16 @@ function hasExplicitNonSourceMessageRoute(args, hookContext, messagingTarget) {
|
|
|
3670
5038
|
if (targetValues.some((value) => !routeTokenMatchesSource(value, hookContext))) return true;
|
|
3671
5039
|
return false;
|
|
3672
5040
|
}
|
|
5041
|
+
function normalizeAcceptedSessionSpawn(result) {
|
|
5042
|
+
const details = asOptionalRecord(asOptionalRecord(result)?.details);
|
|
5043
|
+
if (!details || details.status !== "accepted") return null;
|
|
5044
|
+
const runId = normalizeOptionalString(details.runId);
|
|
5045
|
+
const childSessionKey = normalizeOptionalString(details.childSessionKey);
|
|
5046
|
+
return runId && childSessionKey ? {
|
|
5047
|
+
runId,
|
|
5048
|
+
childSessionKey
|
|
5049
|
+
} : null;
|
|
5050
|
+
}
|
|
3673
5051
|
/** Namespace attached to OpenClaw-owned dynamic tools exposed to Codex. */
|
|
3674
5052
|
const CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE = "openclaw";
|
|
3675
5053
|
const ALWAYS_DIRECT_DYNAMIC_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
@@ -3694,14 +5072,6 @@ const EXPLICIT_MESSAGE_REPLY_KEYS = [
|
|
|
3694
5072
|
"replyToId",
|
|
3695
5073
|
"replyToIdFull"
|
|
3696
5074
|
];
|
|
3697
|
-
const DEFAULT_CODEX_DYNAMIC_TOOL_RESULT_MAX_CHARS = 16e3;
|
|
3698
|
-
const LARGE_CONTEXT_CODEX_DYNAMIC_TOOL_RESULT_MAX_CHARS = 32e3;
|
|
3699
|
-
const XL_CONTEXT_CODEX_DYNAMIC_TOOL_RESULT_MAX_CHARS = 64e3;
|
|
3700
|
-
function resolveCodexDynamicToolResultMaxChars(contextWindowTokens) {
|
|
3701
|
-
if (typeof contextWindowTokens !== "number" || !Number.isFinite(contextWindowTokens) || contextWindowTokens <= 0) return DEFAULT_CODEX_DYNAMIC_TOOL_RESULT_MAX_CHARS;
|
|
3702
|
-
const tokens = Math.floor(contextWindowTokens);
|
|
3703
|
-
return Math.min(tokens >= 2e5 ? XL_CONTEXT_CODEX_DYNAMIC_TOOL_RESULT_MAX_CHARS : tokens >= 1e5 ? LARGE_CONTEXT_CODEX_DYNAMIC_TOOL_RESULT_MAX_CHARS : DEFAULT_CODEX_DYNAMIC_TOOL_RESULT_MAX_CHARS, Math.max(1, Math.floor(tokens * .3) * 4));
|
|
3704
|
-
}
|
|
3705
5075
|
function computerFrameImageIdentity(content) {
|
|
3706
5076
|
if (!Array.isArray(content)) return;
|
|
3707
5077
|
const images = content.filter((block) => block.type === "image");
|
|
@@ -3720,20 +5090,16 @@ function invalidateComputerFrame(contextEpoch) {
|
|
|
3720
5090
|
*/
|
|
3721
5091
|
function createCodexDynamicToolBridge(params) {
|
|
3722
5092
|
const toolResultHookContext = toToolResultHookContext(params.hookContext);
|
|
3723
|
-
const
|
|
3724
|
-
const
|
|
5093
|
+
const contextWindowTokens = params.hookContext?.contextWindowTokens;
|
|
5094
|
+
const toolResultMaxChars = typeof contextWindowTokens === "number" && Number.isFinite(contextWindowTokens) && contextWindowTokens > 0 ? Math.max(1, resolveLiveToolResultMaxChars({ contextWindowTokens })) : DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS;
|
|
5095
|
+
const availableProjection = projectCodexExecutableDynamicToolSurface(params.tools, params.hookContext);
|
|
3725
5096
|
const registeredProjection = params.registeredTools ? projectCodexDynamicTools(params.registeredTools) : availableProjection;
|
|
3726
|
-
const
|
|
3727
|
-
const
|
|
3728
|
-
const quarantinedAvailableToolNames = new Set([...availableProjection.quarantinedTools, ...wrappedAvailableProjection.quarantinedTools].map((tool) => tool.tool));
|
|
5097
|
+
const availableTools = availableProjection.tools;
|
|
5098
|
+
const quarantinedAvailableToolNames = new Set(availableProjection.quarantinedTools.map((tool) => tool.tool));
|
|
3729
5099
|
const registeredSpecTools = (params.registeredTools ? registeredProjection.tools : availableTools).filter((entry) => !quarantinedAvailableToolNames.has(entry.name));
|
|
3730
5100
|
const toolMap = new Map(availableTools.map((entry) => [entry.name, entry]));
|
|
3731
5101
|
const registeredToolNames = new Set(registeredSpecTools.map((entry) => entry.name));
|
|
3732
|
-
const quarantinedTools = dedupeQuarantinedDynamicTools([
|
|
3733
|
-
...availableProjection.quarantinedTools,
|
|
3734
|
-
...registeredProjection.quarantinedTools,
|
|
3735
|
-
...wrappedAvailableProjection.quarantinedTools
|
|
3736
|
-
]);
|
|
5102
|
+
const quarantinedTools = dedupeQuarantinedDynamicTools([...availableProjection.quarantinedTools, ...registeredProjection.quarantinedTools]);
|
|
3737
5103
|
warnQuarantinedDynamicTools(quarantinedTools);
|
|
3738
5104
|
emitQuarantinedDynamicToolDiagnostics(quarantinedTools, params.hookContext);
|
|
3739
5105
|
const telemetry = {
|
|
@@ -3745,6 +5111,7 @@ function createCodexDynamicToolBridge(params) {
|
|
|
3745
5111
|
messagingToolSourceReplyPayloads: [],
|
|
3746
5112
|
toolMediaUrls: [],
|
|
3747
5113
|
toolAudioAsVoice: false,
|
|
5114
|
+
acceptedSessionSpawns: [],
|
|
3748
5115
|
quarantinedTools
|
|
3749
5116
|
};
|
|
3750
5117
|
const middlewareRunner = createAgentToolResultMiddlewareRunner({
|
|
@@ -3761,6 +5128,7 @@ function createCodexDynamicToolBridge(params) {
|
|
|
3761
5128
|
const directToolNames = /* @__PURE__ */ new Set([...ALWAYS_DIRECT_DYNAMIC_TOOL_NAMES, ...params.directToolNames ?? []]);
|
|
3762
5129
|
let readRemoteWorkspaceFile;
|
|
3763
5130
|
return {
|
|
5131
|
+
availableTools: availableTools.map((entry) => entry.tool),
|
|
3764
5132
|
availableSpecs: createCodexDynamicToolSpecs({
|
|
3765
5133
|
entries: availableTools,
|
|
3766
5134
|
loading: params.loading ?? "searchable",
|
|
@@ -3785,7 +5153,7 @@ function createCodexDynamicToolBridge(params) {
|
|
|
3785
5153
|
handleToolCall: async (call, options) => {
|
|
3786
5154
|
const toolEntry = toolMap.get(call.tool);
|
|
3787
5155
|
if (!toolEntry) {
|
|
3788
|
-
const executedArguments =
|
|
5156
|
+
const executedArguments = asNonArrayRecord(call.arguments);
|
|
3789
5157
|
const message = registeredToolNames.has(call.tool) ? `OpenClaw tool is not available for this turn: ${call.tool}` : `Unknown OpenClaw tool: ${call.tool}`;
|
|
3790
5158
|
finalizeToolTerminalPresentation({
|
|
3791
5159
|
toolCallId: call.callId,
|
|
@@ -3803,7 +5171,7 @@ function createCodexDynamicToolBridge(params) {
|
|
|
3803
5171
|
});
|
|
3804
5172
|
}
|
|
3805
5173
|
const { tool, name: toolName } = toolEntry;
|
|
3806
|
-
const args =
|
|
5174
|
+
const args = asNonArrayRecord(call.arguments);
|
|
3807
5175
|
const startedAt = Date.now();
|
|
3808
5176
|
const signal = composeAbortSignals(params.signal, options?.signal);
|
|
3809
5177
|
let didStartExecution = false;
|
|
@@ -3869,6 +5237,8 @@ function createCodexDynamicToolBridge(params) {
|
|
|
3869
5237
|
result: middlewareResult
|
|
3870
5238
|
});
|
|
3871
5239
|
const resultIsError = rawIsError || isToolResultError(result);
|
|
5240
|
+
const acceptedSessionSpawn = toolName === "sessions_spawn" && !rawIsError ? normalizeAcceptedSessionSpawn(telemetryRawResult) : null;
|
|
5241
|
+
if (acceptedSessionSpawn) telemetry.acceptedSessionSpawns.push(acceptedSessionSpawn);
|
|
3872
5242
|
const finalResultFailureKind = resolveToolResultFailureKind(result);
|
|
3873
5243
|
const resultFailureKind = rawResultFailureKind ?? finalResultFailureKind;
|
|
3874
5244
|
const observerResult = rawResultFailureKind && finalResultFailureKind !== rawResultFailureKind ? {
|
|
@@ -3932,10 +5302,9 @@ function createCodexDynamicToolBridge(params) {
|
|
|
3932
5302
|
isError: resultIsError
|
|
3933
5303
|
}) && (replyReceiptMatchesCurrentMessage(rawResult, params.hookContext) || replyReceiptMatchesCurrentMessage(result, params.hookContext));
|
|
3934
5304
|
const toolConfirmedSourceReply = params.hookContext?.sourceReplyDeliveryMode === "message_tool_only" && toolName === "message" && !resultIsError && (rawResult.terminate === true || result.terminate === true);
|
|
3935
|
-
const hasExplicitFinalControl = typeof executedArgs.final === "boolean";
|
|
3936
5305
|
const confirmedSourceReply = params.hookContext?.sourceReplyDeliveryMode === "message_tool_only" && toolName === "message" && (toolConfirmedSourceReply || deliveredSourceReply || receiptConfirmedSourceReply);
|
|
3937
|
-
const sourceReplyFinal = confirmedSourceReply ?
|
|
3938
|
-
|
|
5306
|
+
const sourceReplyFinal = confirmedSourceReply ? executedArgs.final !== false : void 0;
|
|
5307
|
+
collectToolTelemetry({
|
|
3939
5308
|
toolName,
|
|
3940
5309
|
args: executedArgs,
|
|
3941
5310
|
result,
|
|
@@ -3945,13 +5314,9 @@ function createCodexDynamicToolBridge(params) {
|
|
|
3945
5314
|
messagingTarget: confirmedMessagingTarget,
|
|
3946
5315
|
sourceReplyFinal
|
|
3947
5316
|
});
|
|
3948
|
-
if (confirmedSourceReply && sourceReplyRecord) recordCodexSourceReplyDeliveryIntent(telemetry, {
|
|
3949
|
-
record: sourceReplyRecord,
|
|
3950
|
-
final: sourceReplyFinal
|
|
3951
|
-
});
|
|
3952
5317
|
if (deliveredSourceReply || receiptConfirmedSourceReply || toolConfirmedSourceReply) telemetry.didDeliverSourceReplyViaMessageTool = true;
|
|
3953
|
-
const
|
|
3954
|
-
withDynamicToolTermination(response, (rawResult.terminate === true || result.terminate === true) && !
|
|
5318
|
+
const continuesSourceReplyProgress = confirmedSourceReply && sourceReplyFinal === false;
|
|
5319
|
+
withDynamicToolTermination(response, (rawResult.terminate === true || result.terminate === true) && !continuesSourceReplyProgress || isToolResultYield(rawResult) || isToolResultYield(result) || confirmedSourceReply && sourceReplyFinal === true);
|
|
3955
5320
|
const asyncStarted = isAsyncStartedToolResult(rawResult) || isAsyncStartedToolResult(result);
|
|
3956
5321
|
withDynamicToolAsyncStarted(response, asyncStarted);
|
|
3957
5322
|
const replaySafe = executionPrevented || !asyncStarted && isReplaySafeToolInstance(toolEntry.tool) && isReplaySafeToolCall(toolName, executedArgs);
|
|
@@ -4015,6 +5380,22 @@ function createCodexDynamicToolBridge(params) {
|
|
|
4015
5380
|
}
|
|
4016
5381
|
};
|
|
4017
5382
|
}
|
|
5383
|
+
function projectCodexExecutableDynamicToolSurface(tools, hookContext) {
|
|
5384
|
+
const projected = projectCodexDynamicTools(tools);
|
|
5385
|
+
const wrapped = wrapProjectedCodexDynamicTools(projected.tools, hookContext);
|
|
5386
|
+
return {
|
|
5387
|
+
tools: wrapped.tools,
|
|
5388
|
+
quarantinedTools: dedupeQuarantinedDynamicTools([...projected.quarantinedTools, ...wrapped.quarantinedTools])
|
|
5389
|
+
};
|
|
5390
|
+
}
|
|
5391
|
+
/** Applies the exact schema and hook-wrapper projection used by the executable Codex bridge. */
|
|
5392
|
+
function projectCodexExecutableDynamicTools(params) {
|
|
5393
|
+
const projected = projectCodexExecutableDynamicToolSurface(params.tools, params.hookContext);
|
|
5394
|
+
return {
|
|
5395
|
+
availableTools: projected.tools.map((entry) => entry.tool),
|
|
5396
|
+
quarantinedTools: projected.quarantinedTools
|
|
5397
|
+
};
|
|
5398
|
+
}
|
|
4018
5399
|
function notifyAgentToolResult(observer, toolName, result, isError) {
|
|
4019
5400
|
try {
|
|
4020
5401
|
observer?.({
|
|
@@ -4396,15 +5777,16 @@ function withDynamicToolAsyncStarted(response, asyncStarted) {
|
|
|
4396
5777
|
return response;
|
|
4397
5778
|
}
|
|
4398
5779
|
function normalizeToolResultMaxChars(maxChars) {
|
|
4399
|
-
return typeof maxChars === "number" && Number.isFinite(maxChars) && maxChars > 0 ? Math.floor(maxChars) :
|
|
5780
|
+
return typeof maxChars === "number" && Number.isFinite(maxChars) && maxChars > 0 ? Math.floor(maxChars) : DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS;
|
|
4400
5781
|
}
|
|
4401
|
-
function convertToolContents(content, toolResultMaxChars =
|
|
5782
|
+
function convertToolContents(content, toolResultMaxChars = DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS) {
|
|
4402
5783
|
const maxChars = normalizeToolResultMaxChars(toolResultMaxChars);
|
|
4403
5784
|
const totalTextChars = content.reduce((total, item) => total + (item.type === "text" ? item.text.length : 0), 0);
|
|
4404
|
-
if (
|
|
4405
|
-
const noticeText = `...(OpenClaw truncated dynamic tool result: original ${totalTextChars} chars,
|
|
5785
|
+
if (content.reduce((total, item) => total + (item.type === "text" ? estimateToolResultTextChars(item.text) : 0), 0) <= maxChars) return content.flatMap(convertToolContent);
|
|
5786
|
+
const noticeText = `...(OpenClaw truncated dynamic tool result: original ${totalTextChars} chars, weighted budget ${maxChars}; rerun with narrower args.)`;
|
|
4406
5787
|
const notice = `\n${noticeText}`;
|
|
4407
|
-
|
|
5788
|
+
const noticeChars = estimateToolResultTextChars(notice);
|
|
5789
|
+
let remainingTextBudget = Math.max(0, maxChars - noticeChars);
|
|
4408
5790
|
let appendedNotice = false;
|
|
4409
5791
|
const output = [];
|
|
4410
5792
|
for (const item of content) {
|
|
@@ -4413,19 +5795,17 @@ function convertToolContents(content, toolResultMaxChars = DEFAULT_CODEX_DYNAMIC
|
|
|
4413
5795
|
continue;
|
|
4414
5796
|
}
|
|
4415
5797
|
if (appendedNotice) continue;
|
|
4416
|
-
if (
|
|
5798
|
+
if (noticeChars >= maxChars) {
|
|
4417
5799
|
output.push({
|
|
4418
5800
|
type: "inputText",
|
|
4419
|
-
text:
|
|
5801
|
+
text: sliceToolResultTextToBudget(noticeText, maxChars)
|
|
4420
5802
|
});
|
|
4421
5803
|
appendedNotice = true;
|
|
4422
5804
|
continue;
|
|
4423
5805
|
}
|
|
4424
|
-
const
|
|
4425
|
-
remainingTextBudget -=
|
|
4426
|
-
|
|
4427
|
-
const text = truncateUtf16Safe(item.text, sliceLength);
|
|
4428
|
-
if (shouldAppendNotice) {
|
|
5806
|
+
const text = sliceToolResultTextToBudget(item.text, remainingTextBudget);
|
|
5807
|
+
remainingTextBudget -= estimateToolResultTextChars(text);
|
|
5808
|
+
if (remainingTextBudget <= 0 || text.length < item.text.length) {
|
|
4429
5809
|
output.push({
|
|
4430
5810
|
type: "inputText",
|
|
4431
5811
|
text: `${text.trimEnd()}${notice}`
|
|
@@ -4438,7 +5818,7 @@ function convertToolContents(content, toolResultMaxChars = DEFAULT_CODEX_DYNAMIC
|
|
|
4438
5818
|
}
|
|
4439
5819
|
if (!appendedNotice) output.push({
|
|
4440
5820
|
type: "inputText",
|
|
4441
|
-
text:
|
|
5821
|
+
text: sliceToolResultTextToBudget(noticeText, maxChars)
|
|
4442
5822
|
});
|
|
4443
5823
|
return output;
|
|
4444
5824
|
}
|
|
@@ -4457,10 +5837,6 @@ function convertToolContent(content) {
|
|
|
4457
5837
|
imageUrl
|
|
4458
5838
|
}];
|
|
4459
5839
|
}
|
|
4460
|
-
function jsonObjectToRecord(value) {
|
|
4461
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
4462
|
-
return value;
|
|
4463
|
-
}
|
|
4464
5840
|
function readFirstString(record, keys) {
|
|
4465
5841
|
for (const key of keys) {
|
|
4466
5842
|
const value = record[key];
|
|
@@ -4517,4 +5893,4 @@ function isCronAddAction(args) {
|
|
|
4517
5893
|
return typeof action === "string" && action.trim().toLowerCase() === "add";
|
|
4518
5894
|
}
|
|
4519
5895
|
//#endregion
|
|
4520
|
-
export {
|
|
5896
|
+
export { buildCodexNativeHookRelayDisabledConfig as A, resolveDynamicToolCallTimeoutMs as B, shouldWarnCodexDynamicToolBuildStageSummary as C, CODEX_NATIVE_HOOK_RELAY_EVENTS as D, CodexNativeToolLifecycleProjector as E, scheduleCodexNativeHookRelayUnregister as F, toCodexDynamicToolProtocolResponse as G, shouldBlockTerminalReleaseForNonTerminalDynamicToolResult as H, handleDynamicToolCallWithTimeout as I, resolveCodexToolAbortTerminalReason as K, hasPendingDynamicToolTerminalDiagnostic as L, emitCodexNativePreToolUseFailureDiagnostic as M, resolveCodexNativeHookRelayEvents as N, CODEX_NATIVE_HOOK_RELAY_TTL_GRACE_MS as O, resolveCodexNativeHookRelayTtlMs as P, isDynamicToolTerminalDiagnosticEvent as R, shouldRequireCodexSandboxExecServerEnvironment as S, readBoundedCodexRemoteWorkspaceFile as T, shouldReleaseTurnAfterTerminalDynamicTool as U, resolveTerminalDynamicToolBatchAction as V, toCodexDynamicToolProgressResponse as W, resolveCodexAppServerHookChannelId as _, emitDynamicToolStartedDiagnostic as a, resolveCodexSandboxEnvironmentSelection as b, resolveCodexProviderWebSearchSupport as c, releaseCodexSandboxExecServerEnvironment as d, buildDynamicTools as f, resolveCodexAppServerExecutionCwd as g, formatCodexDynamicToolBuildStageSummary as h, emitDynamicToolErrorDiagnostic as i, createCodexNativeHookRelay as j, buildCodexNativeHookRelayConfig as k, resolveCodexProviderWebSearchSupportForClient as l, disableCodexPluginThreadConfig as m, projectCodexExecutableDynamicTools as n, emitDynamicToolTerminalDiagnostic as o, createCodexDynamicToolBuildStageTracker as p, handleCodexAppServerElicitationRequest as r, handleCodexAppServerApprovalRequest as s, createCodexDynamicToolBridge as t, ensureCodexSandboxExecServerEnvironment as u, resolveCodexExternalSandboxPolicyForOpenClawSandbox as v, filterToolsForVisionInputs as w, shouldEnableCodexAppServerNativeToolSurface as x, resolveCodexMessageToolProvider as y, isMatchingDynamicToolTerminalDiagnostic as z };
|