@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.
Files changed (55) hide show
  1. package/README.md +2 -0
  2. package/dist/{app-server-policy-BSaO2JIT.js → app-server-policy-zcBDc1JU.js} +1 -1
  3. package/dist/attempt-client-cleanup-DMLXUfa8.js +735 -0
  4. package/dist/{notification-correlation-CU6_AnOb.js → binding-connection-I5Bo-AsX.js} +3 -37
  5. package/dist/{bounded-turn-D2ZAgZVz.js → bounded-turn-qgZmzy_4.js} +50 -49
  6. package/dist/{capabilities-BFDTVYr4.js → capabilities-CybGI1oz.js} +8 -2
  7. package/dist/cli-metadata.js +1 -1
  8. package/dist/{command-formatters-C336eRVS.js → command-formatters-B1AEz8ZA.js} +18 -18
  9. package/dist/{command-handlers-DAHUSry9.js → command-handlers-Db8pu_ds.js} +484 -95
  10. package/dist/{compact-D_uvHurS.js → compact-LspQaGfl.js} +213 -199
  11. package/dist/{conversation-binding-data-BHRpYbX3.js → conversation-binding-data-CW2GuupV.js} +6 -10
  12. package/dist/doctor-contract-api.js +40 -43
  13. package/dist/{dynamic-tools-D2gYkSBO.js → dynamic-tools-BVdyWkUD.js} +2597 -1221
  14. package/dist/{effective-mcp-catalog-Bbd98Inw.js → effective-mcp-catalog-DIIhAtOZ.js} +20 -15
  15. package/dist/{event-projector-assistant-message-DTgnPPVF.js → event-projector-assistant-message-BwxV4d6N.js} +11 -23
  16. package/dist/{attempt-notifications-AP-a-Q5k.js → event-projector-usage-e5huIQWu.js} +113 -29
  17. package/dist/harness.js +80 -14
  18. package/dist/image-payload-sanitizer-CnfoUPCE.js +65 -0
  19. package/dist/index.js +591 -240
  20. package/dist/isolated-completion-C58NbL40.js +75 -0
  21. package/dist/local-runtime-attribution-B_2mnqhb.js +19 -0
  22. package/dist/media-understanding-provider.js +5 -5
  23. package/dist/{models-BBMKU4jA.js → models-BFlpn1Ex.js} +12 -17
  24. package/dist/native-subagent-monitor-DyTGmTUx.js +3111 -0
  25. package/dist/{plugin-app-cache-key-DztPafOA.js → plugin-app-cache-key-C3H2CVp6.js} +5 -5
  26. package/dist/plugin-marketplace-discovery-CzrGJdCR.js +385 -0
  27. package/dist/{computer-use-CYEc8U3w.js → plugin-metadata-cache-CNJPj4_b.js} +105 -5
  28. package/dist/protocol-validators-B4F7L8Rf.js +2346 -0
  29. package/dist/{rate-limits-CU-gVCCS.js → rate-limits-DZXmOL8-.js} +17 -35
  30. package/dist/{request-qKh331yu.js → request-BRl5_UfX.js} +3 -3
  31. package/dist/{run-attempt-CY9WlDLc.js → run-attempt-P8BdxC1I.js} +4837 -9047
  32. package/dist/{runtime-artifact-CmtG_t1L.js → runtime-artifact-BVSPdd6g.js} +8 -23
  33. package/dist/{sandbox-guard-NuhpXmPB.js → sandbox-guard-Cmwc9eIk.js} +6 -13
  34. package/dist/{session-binding-BXpo9ymA.js → session-binding-DPxIvMgE.js} +246 -265
  35. package/dist/{session-catalog-DyOgR4WH.js → session-catalog-xwNi_SDM.js} +159 -109
  36. package/dist/{session-cli-2PZaFLRh.js → session-cli-7hgXZDV7.js} +1 -1
  37. package/dist/session-retirement-DoMMJFKm.js +36 -0
  38. package/dist/{session-upstream-marker-B6U7tLw5.js → session-upstream-marker-CrIFX06J.js} +1 -1
  39. package/dist/{settled-turn-finalizer-CXY2Z9dT.js → settled-turn-finalizer-BRWBO4MO.js} +21 -13
  40. package/dist/{shared-client-DTbvH-XV.js → shared-client-kIBbGjk4.js} +587 -223
  41. package/dist/{side-question-_ulBk1AL.js → side-question-9HAL26l9.js} +101 -52
  42. package/dist/{thread-lifecycle-DF53ogiA.js → thread-lifecycle-Bu1BTDXr.js} +694 -753
  43. package/dist/thread-ownership-6kOQHlyC.js +92 -0
  44. package/dist/thread-resume-CPrJNQVM.js +30 -0
  45. package/dist/transcript-mirror-B9gGgnJS.js +570 -0
  46. package/dist/{transport-stdio-DXgGZ7Ib.js → transport-stdio-DIImwDVS.js} +11 -0
  47. package/dist/upstream-prompt-provenance-ClRBije0.js +41 -0
  48. package/dist/{upstream-session-fork-Dy4Xvuoj.js → upstream-session-fork-CE-_LdnP.js} +5 -5
  49. package/dist/{usage-CQ6GhD5-.js → usage-CHlV0e-o.js} +3 -3
  50. package/dist/{web-search-provider.runtime-CCFhg2z5.js → web-search-provider.runtime-DRw2Vl42.js} +8 -9
  51. package/openclaw.plugin.json +18 -3
  52. package/package.json +5 -5
  53. package/dist/conversation-control-xsIeXbo4.js +0 -287
  54. package/dist/event-projector-usage-Xp4AlR2k.js +0 -86
  55. package/dist/transcript-mirror-DzsdNCf5.js +0 -6924
@@ -1,136 +1,35 @@
1
- import { H as CODEX_OPENCLAW_DIRECT_DYNAMIC_TOOL_NAMESPACE, W as isJsonObject, X as resolveCodexGatewayTimeoutWithGraceMs, l as releaseLeasedSharedCodexAppServerClient } from "./shared-client-DTbvH-XV.js";
2
- import { O as readCodexPluginConfig } from "./session-binding-BXpo9ymA.js";
3
- import { D as isForcedPrivateQaCodexRuntime, E as filterCodexDynamicToolsWithOpenClawShell, O as isSystemAgentOnlyCodexDynamicToolAllowlist, T as filterCodexDynamicTools, ct as readString$2, g as resolveCodexWebSearchPlan, i as invalidInlineImageText, k as normalizeCodexDynamicToolName, nt as readItemString, o as sanitizeInlineImageDataUrl, tt as readItem } from "./thread-lifecycle-DF53ogiA.js";
4
- import { S as readCodexTurn } from "./transcript-mirror-DzsdNCf5.js";
5
- import { r as formatCodexDisplayText } from "./command-formatters-C336eRVS.js";
6
- import { a as resolveCodexNodeExecToolOverrides, i as resolveCodexNativeExecutionPolicy } from "./sandbox-guard-NuhpXmPB.js";
7
- import { n as readCodexNotificationThreadId, r as readCodexNotificationTurnId } from "./notification-correlation-CU6_AnOb.js";
8
- import path from "node:path";
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, callGatewayTool, 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, registerNativeHookRelay, resolveAttemptSpawnWorkspaceDir, resolveModelAuthMode, resolveNativeHookRelayDeferredToolApproval, resolveToolExecutionErrorKind, resolveToolResultFailureKind, runAgentHarnessAfterToolCallHook, runBeforeToolCallHook, sanitizeToolResult, setBeforeToolCallDiagnosticsEnabled, supportsModelTools, wrapToolWithBeforeToolCallHook } from "openclaw/plugin-sdk/agent-harness-runtime";
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 { isRecord, normalizeTrimmedStringList } from "openclaw/plugin-sdk/string-coerce-runtime";
14
- import { sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
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
- return registerNativeHookRelay({
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 || readCodexNotificationThreadId(params) !== this.threadId || readCodexNotificationTurnId(params) !== this.turnId) return;
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 (readString$2(item, "type") !== "web_search_call") return;
910
- const toolCallId = readString$2(item, "id");
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 = readString$2(item, "status");
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 codexScopedTools = addCodexMessageToolOnlyFinalControl(createOpenClawCodingTools({
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
- allocateToolOutcomeOrdinal: params.allocateToolOutcomeOrdinal
1644
- }), params.sourceReplyDeliveryMode);
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(codexScopedTools);
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 = shouldKeepOpenClawShellDynamicTools(input, nativeExecutionPolicy) ? filterCodexDynamicToolsWithOpenClawShell(readableAllTools, input.pluginConfig) : filterCodexDynamicTools(readableAllTools, input.pluginConfig);
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 environment && nativeToolSurfaceEnabled ? [environment] : void 0;
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?.backendId !== "docker") return true;
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/source-reply-finality.ts
1922
- const sourceReplyDeliveryIntents = /* @__PURE__ */ new WeakMap();
1923
- /** Retain source-reply intent until the owning Codex turn has an authoritative outcome. */
1924
- function recordCodexSourceReplyDeliveryIntent(owner, intent) {
1925
- const intents = sourceReplyDeliveryIntents.get(owner);
1926
- if (intents) {
1927
- intents.push(intent);
1928
- return;
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
- sourceReplyDeliveryIntents.set(owner, [intent]);
1931
- }
1932
- /** Resolve omitted finality without changing explicit progress or final markers. */
1933
- function settleCodexSourceReplyFinality(owner, turnSucceeded) {
1934
- const intents = sourceReplyDeliveryIntents.get(owner);
1935
- if (!intents) return false;
1936
- const lastIntent = intents.at(-1);
1937
- for (const intent of intents) {
1938
- if (intent.final !== void 0) continue;
1939
- intent.record.sourceReplyFinal = turnSucceeded && intent === lastIntent;
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
- sourceReplyDeliveryIntents.delete(owner);
1942
- return turnSucceeded && intents.some((intent) => intent.record.sourceReplyFinal === true);
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/hook-requester.ts
1946
- /** Rebuilds the host-proven requester identity shared by native and bridged tool hooks. */
1947
- function buildCodexHookRequester(params) {
1948
- const channel = params.messageChannel ?? params.messageProvider;
1949
- const requester = {
1950
- ...channel ? { channel } : {},
1951
- ...params.agentAccountId ? { accountId: params.agentAccountId } : {},
1952
- ...params.senderId ? { senderId: params.senderId } : {},
1953
- ...params.senderIsOwner !== void 0 ? { senderIsOwner: params.senderIsOwner } : {},
1954
- ...params.memberRoleIds?.length ? { roleIds: [...params.memberRoleIds] } : {}
1955
- };
1956
- return Object.keys(requester).length > 0 ? requester : void 0;
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/provider-capabilities.ts
1960
- function resolveOverriddenProviderWebSearchSupport(modelProviderOverride) {
1961
- const provider = modelProviderOverride?.trim().toLowerCase();
1962
- if (!provider) return;
1963
- return provider === "openai" ? "supported" : "unsupported";
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
- async function readConfiguredProviderWebSearchSupport(params) {
1966
- return (await params.client.request("modelProvider/capabilities/read", {}, {
1967
- timeoutMs: params.timeoutMs,
1968
- signal: params.signal
1969
- })).webSearch ? "supported" : "unsupported";
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
- async function resolveCodexProviderWebSearchSupportForClient(params) {
1972
- const overrideSupport = resolveOverriddenProviderWebSearchSupport(params.modelProviderOverride);
1973
- if (overrideSupport) return overrideSupport;
1974
- try {
1975
- return await readConfiguredProviderWebSearchSupport(params);
1976
- } catch {
1977
- return "unknown";
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
- async function resolveCodexProviderWebSearchSupport(params) {
1981
- const overrideSupport = resolveOverriddenProviderWebSearchSupport(params.modelProviderOverride);
1982
- if (overrideSupport) return overrideSupport;
1983
- let client;
1984
- try {
1985
- client = await params.clientFactory({
1986
- startOptions: params.appServer.start,
1987
- ...params.preparedAuth ? { preparedAuth: params.preparedAuth } : { authProfileId: params.authProfileId },
1988
- agentDir: params.agentDir,
1989
- config: params.config,
1990
- timeoutMs: params.appServer.requestTimeoutMs
1991
- });
1992
- return await resolveCodexProviderWebSearchSupportForClient({
1993
- client,
1994
- timeoutMs: params.appServer.requestTimeoutMs,
1995
- modelProviderOverride: params.modelProviderOverride,
1996
- signal: params.signal
1997
- });
1998
- } catch {
1999
- return "unknown";
2000
- } finally {
2001
- if (client) releaseLeasedSharedCodexAppServerClient(client);
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/dynamic-tool-diagnostics.ts
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
- * Trusted diagnostics emitted around Codex dynamic tool execution lifecycle.
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
- /** Emits a start event for one Codex dynamic tool call. */
2010
- function emitDynamicToolStartedDiagnostic(params) {
2011
- emitTrustedDiagnosticEvent({
2012
- type: "tool.execution.started",
2013
- agentId: params.agentId,
2014
- runId: params.runId,
2015
- sessionId: params.sessionId,
2016
- sessionKey: params.sessionKey,
2017
- toolName: params.call.tool,
2018
- toolCallId: params.call.callId
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
- /** Emits an error event for one Codex dynamic tool call. */
2022
- function emitDynamicToolErrorDiagnostic(params) {
2023
- emitTrustedDiagnosticEvent({
2024
- type: "tool.execution.error",
2025
- agentId: params.agentId,
2026
- runId: params.runId,
2027
- sessionId: params.sessionId,
2028
- sessionKey: params.sessionKey,
2029
- toolName: params.call.tool,
2030
- toolCallId: params.call.callId,
2031
- durationMs: params.durationMs,
2032
- errorCategory: "codex_dynamic_tool_error",
2033
- terminalReason: params.terminalReason ?? "failed"
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
- /** Emits the terminal event matching a dynamic tool response's diagnostic type. */
2037
- function emitDynamicToolTerminalDiagnostic(params) {
2038
- const terminalType = params.response.diagnosticTerminalType ?? (params.response.success ? "completed" : "error");
2039
- if (terminalType === "completed") {
2040
- emitTrustedDiagnosticEvent({
2041
- type: "tool.execution.completed",
2042
- agentId: params.agentId,
2043
- runId: params.runId,
2044
- sessionId: params.sessionId,
2045
- sessionKey: params.sessionKey,
2046
- toolName: params.call.tool,
2047
- toolCallId: params.call.callId,
2048
- durationMs: params.durationMs
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 (terminalType === "blocked") {
2053
- emitTrustedDiagnosticEvent({
2054
- type: "tool.execution.blocked",
2055
- agentId: params.agentId,
2056
- runId: params.runId,
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
- emitDynamicToolErrorDiagnostic({
2067
- ...params,
2068
- terminalReason: params.response.diagnosticTerminalReason ?? "failed"
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/plugin-approval-roundtrip.ts
2522
+ //#region extensions/codex/src/app-server/sandbox-exec-server/http.ts
2073
2523
  /**
2074
- * Routes Codex app-server plugin approval prompts through OpenClaw's gateway
2075
- * approval tool and maps gateway decisions back to Codex outcomes.
2524
+ * Implements sandboxed HTTP requests for Codex native tools by routing network
2525
+ * access through the active OpenClaw sandbox backend.
2076
2526
  */
2077
- const DEFAULT_CODEX_APPROVAL_TIMEOUT_MS = 12e4;
2078
- const MAX_PLUGIN_APPROVAL_TITLE_LENGTH = 80;
2079
- const MAX_PLUGIN_APPROVAL_DESCRIPTION_LENGTH = 256;
2080
- /** Starts a two-phase plugin approval request through the OpenClaw gateway. */
2081
- async function requestPluginApproval(params) {
2082
- const timeoutMs = DEFAULT_CODEX_APPROVAL_TIMEOUT_MS;
2083
- return callGatewayTool("plugin.approval.request", { timeoutMs: resolveCodexGatewayTimeoutWithGraceMs(timeoutMs) }, {
2084
- pluginId: "openclaw-codex-app-server",
2085
- title: truncateForGateway(params.title, MAX_PLUGIN_APPROVAL_TITLE_LENGTH),
2086
- description: truncateForGateway(params.description, MAX_PLUGIN_APPROVAL_DESCRIPTION_LENGTH),
2087
- severity: params.severity,
2088
- toolName: params.toolName,
2089
- toolCallId: params.toolCallId,
2090
- agentId: params.paramsForRun.agentId,
2091
- sessionKey: params.paramsForRun.sessionKey,
2092
- turnSourceChannel: params.paramsForRun.messageChannel ?? params.paramsForRun.messageProvider,
2093
- turnSourceTo: params.paramsForRun.currentChannelId,
2094
- turnSourceAccountId: params.paramsForRun.agentAccountId,
2095
- turnSourceThreadId: params.paramsForRun.currentThreadTs,
2096
- timeoutMs,
2097
- twoPhase: true,
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
- /** Detects the gateway's explicit null-decision marker for unavailable approvals. */
2102
- function approvalRequestExplicitlyUnavailable(result) {
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
- descriptor = Object.getOwnPropertyDescriptor(result, "decision");
2552
+ parsed = new URL(url);
2107
2553
  } catch {
2108
- return false;
2554
+ throw new SsrFBlockedError("Invalid URL supplied to sandbox http/request");
2109
2555
  }
2110
- return descriptor !== void 0 && "value" in descriptor && descriptor.value === null;
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
- /** Waits for the gateway's final approval decision, respecting turn aborts. */
2113
- async function waitForPluginApprovalDecision(params) {
2114
- const waitPromise = callGatewayTool("plugin.approval.waitDecision", { timeoutMs: resolveCodexGatewayTimeoutWithGraceMs(DEFAULT_CODEX_APPROVAL_TIMEOUT_MS) }, { id: params.approvalId }).catch((error) => {
2115
- if (isApprovalNotFoundError(error)) return null;
2116
- throw error;
2117
- });
2118
- const bindDecision = (result) => result === null ? null : result?.id === params.approvalId ? result.decision : void 0;
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
- return bindDecision(await Promise.race([waitPromise, abortPromise]));
2131
- } finally {
2132
- if (onAbort) params.signal.removeEventListener("abort", onAbort);
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
- /** Converts a gateway exec approval decision into the app-server approval outcome enum. */
2136
- function mapExecDecisionToOutcome(decision) {
2137
- if (decision === "allow-once") return "approved-once";
2138
- if (decision === "allow-always") return "approved-session";
2139
- if (decision === null || decision === void 0) return "unavailable";
2140
- return "denied";
2141
- }
2142
- function truncateForGateway(value, maxLength) {
2143
- return value.length <= maxLength ? value : `${truncateUtf16Safe(value, maxLength - 3)}...`;
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
- function toLintErrorObject(value, fallbackMessage) {
2146
- if (value instanceof Error) return value;
2147
- if (typeof value === "string") return new Error(value);
2148
- const error = new Error(fallbackMessage, { cause: value });
2149
- if (typeof value === "object" && value !== null || typeof value === "function") Object.assign(error, value);
2150
- return error;
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/elicitation-bridge.ts
2154
- const MCP_TOOL_APPROVAL_KIND = "mcp_tool_call";
2155
- const MCP_TOOL_APPROVAL_KIND_KEY = "codex_approval_kind";
2156
- const MCP_TOOL_APPROVAL_CONNECTOR_NAME_KEY = "connector_name";
2157
- const MCP_TOOL_APPROVAL_TOOL_TITLE_KEY = "tool_title";
2158
- const MCP_TOOL_APPROVAL_TOOL_DESCRIPTION_KEY = "tool_description";
2159
- const MCP_TOOL_APPROVAL_TOOL_PARAMS_DISPLAY_KEY = "tool_params_display";
2160
- const MCP_TOOL_APPROVAL_SOURCE_KEY = "source";
2161
- const MCP_TOOL_APPROVAL_CONNECTOR_SOURCE = "connector";
2162
- const CODEX_APPS_SERVER_NAME = "codex_apps";
2163
- const COMPUTER_USE_APPROVAL_TITLE = "Computer Use approval";
2164
- const EMPTY_OBJECT_SCHEMA = {
2165
- type: "object",
2166
- properties: {}
2167
- };
2168
- const PLUGIN_APP_ID_META_KEYS = [
2169
- "app_id",
2170
- "appId",
2171
- "codex_app_id",
2172
- "codexAppId"
2173
- ];
2174
- const PLUGIN_CONNECTOR_ID_META_KEYS = ["connector_id", "connectorId"];
2175
- const PLUGIN_NAME_META_KEYS = [
2176
- "plugin_name",
2177
- "pluginName",
2178
- "codex_plugin_name",
2179
- "codexPluginName"
2180
- ];
2181
- const PLUGIN_CONFIG_KEY_META_KEYS = [
2182
- "config_key",
2183
- "configKey",
2184
- "codex_config_key"
2185
- ];
2186
- const PLUGIN_MARKETPLACE_NAME_META_KEYS = [
2187
- "marketplace_name",
2188
- "marketplaceName",
2189
- "codex_marketplace_name",
2190
- "codexMarketplaceName"
2191
- ];
2192
- const MAX_DISPLAY_PARAM_ENTRIES = 8;
2193
- const MAX_DISPLAY_PARAM_VALUE_LENGTH = 120;
2194
- const MAX_DISPLAY_VALUE_ARRAY_ITEMS = 8;
2195
- const MAX_DISPLAY_VALUE_OBJECT_KEYS = 8;
2196
- const MAX_DISPLAY_VALUE_DEPTH = 3;
2197
- const DISPLAY_TEXT_SCAN_MAX_LENGTH = 4096;
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
- return await buildPluginPolicyElicitationResponse({
2222
- entry: pluginResolution.entry,
2223
- requestParams,
2224
- paramsForRun: params.paramsForRun,
2225
- signal: params.signal
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
- const approvalPrompt = readComputerUseApprovalElicitation(requestParams, params.computerUseMcpServerName) ?? readBridgeableApprovalElicitation(requestParams);
2229
- if (!approvalPrompt) return;
2230
- return buildElicitationResponse(approvalPrompt, await requestPluginApprovalOutcome({
2231
- paramsForRun: params.paramsForRun,
2232
- title: approvalPrompt.title,
2233
- description: approvalPrompt.description,
2234
- allowedDecisions: approvalPrompt.allowedDecisions,
2235
- signal: params.signal
2236
- }));
2237
- }
2238
- function matchesCurrentThread(requestParams, threadId) {
2239
- if (!requestParams) return false;
2240
- return readString$1(requestParams, "threadId") === threadId;
2241
- }
2242
- function turnIdMismatches(requestParams, turnId) {
2243
- const rawTurnId = requestParams?.turnId;
2244
- return rawTurnId !== null && rawTurnId !== void 0 && rawTurnId !== turnId;
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 hasExactTurnId(requestParams, turnId) {
2247
- return requestParams?.turnId === turnId;
3054
+ function throwIfProcessStartCancelled(managed) {
3055
+ if (managed.abortController.signal.aborted) throw new Error("process start cancelled");
2248
3056
  }
2249
- function resolvePluginElicitation(params) {
2250
- const requestParams = params.requestParams;
2251
- if (!requestParams) return { kind: "not_plugin" };
2252
- const meta = isJsonObject(requestParams["_meta"]) ? requestParams["_meta"] : {};
2253
- const context = params.pluginAppPolicyContext;
2254
- const entries = context ? Object.values(context.apps) : [];
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
- if (appId) {
2264
- if (!context) return {
2265
- kind: "decline",
2266
- reason: "missing_policy_context"
2267
- };
2268
- const entry = context.apps[appId];
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
- if (isCodexConnectorApproval && connectorId) {
2276
- if (!context) return {
2277
- kind: "decline",
2278
- reason: "missing_policy_context"
2279
- };
2280
- const entry = context.apps[connectorId];
2281
- return uniquePluginMatch(entry ? [entry] : [], "connector_id");
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
- const serverName = readString$1(requestParams, "serverName");
2284
- if (serverName && context) {
2285
- const matches = entries.filter((entry) => entry.mcpServerNames.includes(serverName));
2286
- if (matches.length > 0) return uniquePluginMatch(matches, "server_name");
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
- const metadataResolution = resolvePluginStableMetadataMatch({
2289
- meta,
2290
- requestParams,
2291
- entries: pluginEntries,
2292
- context
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
- if (metadataResolution.kind !== "not_plugin") return metadataResolution;
2295
- if (context && hasDisplayNameOnlyPluginMatch(meta, entries)) return {
2296
- kind: "decline",
2297
- reason: "display_name_only"
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
- function isCodexConnectorApprovalElicitation(requestParams, meta) {
2302
- return readString$1(requestParams, "serverName") === CODEX_APPS_SERVER_NAME && readString$1(meta, MCP_TOOL_APPROVAL_KIND_KEY) === MCP_TOOL_APPROVAL_KIND && readString$1(meta, MCP_TOOL_APPROVAL_SOURCE_KEY) === MCP_TOOL_APPROVAL_CONNECTOR_SOURCE;
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 resolvePluginStableMetadataMatch(params) {
2305
- const pluginName = readFirstString$1(params.meta, PLUGIN_NAME_META_KEYS) ?? readFirstString$1(params.requestParams, PLUGIN_NAME_META_KEYS);
2306
- const configKey = readFirstString$1(params.meta, PLUGIN_CONFIG_KEY_META_KEYS) ?? readFirstString$1(params.requestParams, PLUGIN_CONFIG_KEY_META_KEYS);
2307
- const marketplaceName = readFirstString$1(params.meta, PLUGIN_MARKETPLACE_NAME_META_KEYS) ?? readFirstString$1(params.requestParams, PLUGIN_MARKETPLACE_NAME_META_KEYS);
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 uniquePluginMatch(matches, source) {
2321
- if (matches.length === 1 && matches[0]) return {
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 hasDisplayNameOnlyPluginMatch(meta, entries) {
2331
- const connectorName = readString$1(meta, MCP_TOOL_APPROVAL_CONNECTOR_NAME_KEY);
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 isPluginAppPolicyContextEntry(entry) {
2337
- return entry.source !== "account";
3198
+ function shellEscape(value) {
3199
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
2338
3200
  }
2339
- function appPolicyDisplayName(entry) {
2340
- return isPluginAppPolicyContextEntry(entry) ? entry.pluginName : entry.appName;
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 normalizePluginIdentityText(value) {
2343
- return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
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
- async function buildPluginPolicyElicitationResponse(params) {
2346
- const mode = resolvePluginDestructiveApprovalMode(params.entry);
2347
- if (mode === "deny") {
2348
- logPluginElicitationDecline("destructive_actions_disabled", params.requestParams);
2349
- return declineElicitationResponse();
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 resolvePluginDestructiveApprovalMode(entry) {
2371
- return entry.destructiveApprovalMode ?? (entry.allowDestructiveActions ? "allow" : "deny");
3217
+ function readProcessEnv(record) {
3218
+ return sanitizeEnvVars({
3219
+ ...buildEnvFromPolicy(record.envPolicy),
3220
+ ...readEnv(record.env)
3221
+ }).allowed;
2372
3222
  }
2373
- function allowedPluginPolicyApprovalDecisions(mode, approvalPrompt) {
2374
- const allowedDecisions = approvalPrompt.allowedDecisions ?? ["allow-once", "deny"];
2375
- if (mode !== "ask") return allowedDecisions;
2376
- return allowedDecisions.filter((decision) => decision !== "allow-always");
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 oneShotPluginPolicyApprovalOutcome(mode, outcome) {
2379
- return mode === "ask" && outcome === "approved-session" ? "approved-once" : outcome;
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 readPluginApprovalElicitation(entry, requestParams) {
2382
- if (readString$1(requestParams, "mode") !== "form" || !isJsonObject(requestParams.requestedSchema)) return;
2383
- const requestedSchema = requestParams.requestedSchema;
2384
- if (readString$1(requestedSchema, "type") !== "object" || !isJsonObject(requestedSchema.properties)) return;
2385
- const meta = isJsonObject(requestParams["_meta"]) ? requestParams["_meta"] : {};
2386
- const title = sanitizeDisplayText(readString$1(requestParams, "message") ?? "") || "Codex plugin approval";
2387
- const descriptionMeta = { ...meta };
2388
- if (!readString$1(descriptionMeta, MCP_TOOL_APPROVAL_CONNECTOR_NAME_KEY)) descriptionMeta[MCP_TOOL_APPROVAL_CONNECTOR_NAME_KEY] = appPolicyDisplayName(entry);
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
- title,
2391
- description: buildApprovalDescription({
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
- function buildApprovalAllowedDecisions(requestedSchema, meta) {
2404
- return canMapPersistentApproval(requestedSchema, meta) ? [
2405
- "allow-once",
2406
- "allow-always",
2407
- "deny"
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 canMapPersistentApproval(requestedSchema, meta) {
2411
- const persistHints = readPersistHints(meta, "explicit");
2412
- if (persistHints.length > 0) return persistHints.includes("always");
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 declineElicitationResponse() {
2425
- return {
2426
- action: "decline",
2427
- content: null,
2428
- _meta: null
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 logPluginElicitationDecline(reason, requestParams) {
2432
- embeddedAgentLog.debug("codex plugin elicitation declined", {
2433
- reason,
2434
- serverName: readString$1(requestParams, "serverName"),
2435
- mode: readString$1(requestParams, "mode")
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 readBridgeableApprovalElicitation(requestParams) {
2439
- if (!requestParams || readString$1(requestParams, "mode") !== "form" || !isJsonObject(requestParams["_meta"]) || requestParams["_meta"][MCP_TOOL_APPROVAL_KIND_KEY] !== MCP_TOOL_APPROVAL_KIND || !isJsonObject(requestParams.requestedSchema)) return;
2440
- const requestedSchema = requestParams.requestedSchema;
2441
- if (readString$1(requestedSchema, "type") !== "object" || !isJsonObject(requestedSchema.properties)) return;
2442
- const title = sanitizeDisplayText(readString$1(requestParams, "message") ?? "") || "Codex MCP tool approval";
2443
- return {
2444
- title,
2445
- description: buildApprovalDescription({
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 readComputerUseApprovalElicitation(requestParams, expectedServerName) {
2456
- const serverName = readString$1(requestParams, "serverName");
2457
- if (!serverName || !expectedServerName || serverName !== expectedServerName || readString$1(requestParams, "mode") !== "form") return;
2458
- const requestedSchema = isJsonObject(requestParams?.requestedSchema) ? requestParams.requestedSchema : EMPTY_OBJECT_SCHEMA;
2459
- if (readString$1(requestedSchema, "type") !== "object" || !isJsonObject(requestedSchema.properties)) return;
2460
- const meta = isJsonObject(requestParams?.["_meta"]) ? requestParams["_meta"] : {};
2461
- const title = sanitizeDisplayText(readString$1(requestParams, "message") ?? "") || COMPUTER_USE_APPROVAL_TITLE;
2462
- return {
2463
- title,
2464
- description: buildApprovalDescription({
2465
- title,
2466
- meta,
2467
- requestedSchema,
2468
- serverName: sanitizeOptionalDisplayText(serverName)
2469
- }),
2470
- requestedSchema,
2471
- meta
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 buildApprovalDescription(params) {
2475
- const connectorName = sanitizeOptionalDisplayText(readString$1(params.meta, MCP_TOOL_APPROVAL_CONNECTOR_NAME_KEY));
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 readPropertyDescriptionLines(requestedSchema) {
2494
- const properties = isJsonObject(requestedSchema.properties) ? requestedSchema.properties : {};
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 readDisplayParamLines(meta) {
2504
- const displayParams = meta[MCP_TOOL_APPROVAL_TOOL_PARAMS_DISPLAY_KEY];
2505
- if (!Array.isArray(displayParams)) return [];
2506
- const lines = displayParams.slice(0, MAX_DISPLAY_PARAM_ENTRIES).map((entry) => {
2507
- const param = isJsonObject(entry) ? entry : void 0;
2508
- if (!param) return;
2509
- const name = sanitizeOptionalDisplayText(readString$1(param, "display_name")) ?? sanitizeOptionalDisplayText(readString$1(param, "name"));
2510
- if (!name) return;
2511
- return `- ${name}: ${formatDisplayParamValue(param.value)}`;
2512
- }).filter((line) => Boolean(line));
2513
- const remaining = displayParams.length - MAX_DISPLAY_PARAM_ENTRIES;
2514
- return remaining > 0 ? [...lines, `- Additional parameters: ${remaining} more`] : lines;
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 formatDisplayParamValue(value) {
2517
- return truncateDisplayText(sanitizeDisplayText(typeof value === "string" ? value : formatDisplayJsonValue(value ?? null)), MAX_DISPLAY_PARAM_VALUE_LENGTH);
3389
+ function handleExecServerSocketError(error) {
3390
+ embeddedAgentLog.debug("codex sandbox exec-server websocket failed", { error });
2518
3391
  }
2519
- function formatDisplayJsonValue(value, depth = MAX_DISPLAY_VALUE_DEPTH) {
2520
- if (value === null) return "null";
2521
- if (typeof value === "string") return JSON.stringify(truncateDisplayText(sanitizeDisplayText(value), 80));
2522
- if (typeof value === "number" || typeof value === "boolean") return String(value);
2523
- if (Array.isArray(value)) {
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
- if (typeof value === "object") {
2532
- if (depth <= 0) return "{truncated}";
2533
- const parts = [];
2534
- let count = 0;
2535
- let truncated = false;
2536
- for (const key in value) {
2537
- if (!Object.hasOwn(value, key)) continue;
2538
- if (count >= MAX_DISPLAY_VALUE_OBJECT_KEYS) {
2539
- truncated = true;
2540
- break;
2541
- }
2542
- const safeKey = truncateDisplayText(sanitizeDisplayText(key), 80);
2543
- parts.push(`${JSON.stringify(safeKey)}:${formatDisplayJsonValue(value[key] ?? null, depth - 1)}`);
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 sanitizeOptionalDisplayText(value) {
2552
- return (value === void 0 ? "" : sanitizeDisplayText(value)) || void 0;
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
- function sanitizeDisplayText(value) {
2555
- const scanned = sliceUtf16Safe(value, 0, DISPLAY_TEXT_SCAN_MAX_LENGTH);
2556
- const clipped = value.length > DISPLAY_TEXT_SCAN_MAX_LENGTH;
2557
- const sanitized = scanned.replace(ANSI_OSC_SEQUENCE_RE$1, "").replace(ANSI_CONTROL_SEQUENCE_RE$1, "").replace(DANGLING_TERMINAL_SEQUENCE_SUFFIX_RE$1, "").replace(INVISIBLE_FORMATTING_CONTROL_RE$1, " ").replace(CONTROL_CHARACTER_RE$1, " ").replace(/\s+/g, " ").trim();
2558
- const escaped = sanitized ? formatCodexDisplayText(sanitized) : "";
2559
- return clipped && escaped ? `${escaped}...` : escaped;
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 truncateDisplayText(value, maxLength) {
2562
- return value.length <= maxLength ? value : `${truncateUtf16Safe(value, maxLength - 3)}...`;
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 requestPluginApprovalOutcome(params) {
3464
+ async function resolveCodexProviderWebSearchSupportForClient(params) {
3465
+ const overrideSupport = resolveOverriddenProviderWebSearchSupport(params.modelProviderOverride);
3466
+ if (overrideSupport) return overrideSupport;
2565
3467
  try {
2566
- const requestResult = await requestPluginApproval({
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 params.signal?.aborted ? "cancelled" : "denied";
3470
+ return "unknown";
2582
3471
  }
2583
3472
  }
2584
- function buildElicitationResponse(approvalPrompt, outcome) {
2585
- const { requestedSchema, meta } = approvalPrompt;
2586
- if (outcome === "cancelled") return {
2587
- action: "cancel",
2588
- content: null,
2589
- _meta: null
2590
- };
2591
- if (outcome === "denied" || outcome === "unavailable") return {
2592
- action: "decline",
2593
- content: null,
2594
- _meta: null
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
- action: "decline",
2610
- content: null,
2611
- _meta: null
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
- function optionMatchesPersist(option, persist) {
2706
- return option.value.toLowerCase() === persist || option.label.toLowerCase() === persist;
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
- function hasNoSchemaProperties(requestedSchema) {
2709
- const properties = isJsonObject(requestedSchema.properties) ? requestedSchema.properties : {};
2710
- return Object.keys(properties).length === 0;
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
- function readEnumOptions(schema) {
2713
- if (Array.isArray(schema.enum)) {
2714
- const values = schema.enum.filter((entry) => typeof entry === "string");
2715
- const labels = Array.isArray(schema.enumNames) ? schema.enumNames.filter((entry) => typeof entry === "string") : [];
2716
- return values.map((value, index) => ({
2717
- value,
2718
- label: labels[index] ?? value
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
- function isPositiveApprovalOption(option) {
2733
- const haystack = `${option.value} ${option.label}`.toLowerCase();
2734
- return /\b(allow|approve|accept|yes|continue|proceed|true)\b/.test(haystack);
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 isSessionApprovalOption(option) {
2737
- const haystack = `${option.value} ${option.label}`.toLowerCase();
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 readString$1(record, key) {
2741
- const value = record?.[key];
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 readFirstString$1(record, keys) {
2745
- for (const key of keys) {
2746
- const value = readString$1(record, key);
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
- emitApprovalEvent(params.paramsForRun, {
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
- emitApprovalEvent(params.paramsForRun, {
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
- paramsForRun: params.paramsForRun,
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(formatErrorMessage$1(error))}`
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 = readString(requestParams, "threadId");
2933
- const requestTurnId = readString(requestParams, "turnId");
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 = readString(params.requestParams, "itemId") ?? readString(params.requestParams, "callId") ?? readString(params.requestParams, "approvalId");
2938
- const approvalId = readString(params.requestParams, "approvalId") ?? itemId;
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 = readString(params.requestParams, "cwd") ?? params.paramsForRun.workspaceDir;
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 hookChannelId = buildAgentHookContextChannelFields({
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
- approvalMode: "request",
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
- if (params.method !== "item/commandExecution/requestApproval" || !params.nativeHookRelay?.allowedEvents.includes("pre_tool_use")) return;
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
- if (hasNativeHookRelayInvocation({
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: params.nativeHookRelay.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
- if (approvalOutcome?.outcome === "approved-once") return {
3828
+ return approvalOutcome?.outcome === "approved-once" ? {
3061
3829
  handled: true,
3062
3830
  outcome: approvalOutcome.outcome
3063
- };
3064
- return { handled: true };
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: params.nativeHookRelay.relayId,
3070
- generation: params.nativeHookRelay.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
- const approvalOutcome = await resolveNativeHookRelayDeferredToolApproval({
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: params.nativeHookRelay.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(formatErrorMessage$1(error))}`,
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 = readString(params.policyRequest.params, "command");
3869
+ const command = readStringField(params.policyRequest.params, "command");
3113
3870
  if (!command) return;
3114
- const turnId = readString(params.requestParams, "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: readString(output, "permissionDecisionReason") || "OpenClaw native hook policy denied Codex app-server approval.",
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
- ...readString(requestParams, "cwd") ? { cwd: readString(requestParams, "cwd") } : {},
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 (isPlainRecord(value)) {
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
- const details = [];
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 = readStringArray(record, descriptor.key);
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 truncate(authority.includes("@") ? authority.slice(authority.lastIndexOf("@") + 1) : authority, PERMISSION_VALUE_MAX_LENGTH);
4128
+ return truncateCodexApprovalDisplayText(authority.includes("@") ? authority.slice(authority.lastIndexOf("@") + 1) : authority, PERMISSION_VALUE_MAX_LENGTH);
3383
4129
  }
3384
4130
  function sanitizePermissionPathValue(value) {
3385
- return truncate(formatApprovalDisplayPath(sanitizePermissionScalar(value)), PERMISSION_VALUE_MAX_LENGTH);
4131
+ return truncateCodexApprovalDisplayText(formatApprovalDisplayPath(sanitizePermissionScalar(value)), PERMISSION_VALUE_MAX_LENGTH);
3386
4132
  }
3387
4133
  function sanitizePermissionScalar(value) {
3388
- return sanitizeVisibleScalar(value);
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 permissionHostRisks(value) {
3391
- const normalized = value.trim().toLowerCase();
3392
- const risks = [];
3393
- if (normalized.includes("*")) {
3394
- risks.push("wildcard hosts");
3395
- if (isPrivateNetworkHostPattern(normalized)) risks.push("private-network wildcards");
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 risks;
4784
+ return {
4785
+ action: "accept",
4786
+ content: content ?? null,
4787
+ _meta: buildAcceptedMeta(meta, outcome, approvalPrompt.persistHintsMode ?? "legacy")
4788
+ };
3398
4789
  }
3399
- function permissionPathRisks(value) {
3400
- const normalized = sanitizePermissionScalar(value);
3401
- const risks = [];
3402
- if (normalized === "/" || normalized === "\\" || /^[A-Za-z]:[\\/]*$/.test(normalized)) risks.push("filesystem root");
3403
- return risks;
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 isPrivateNetworkHostPattern(value) {
3406
- const wildcardStripped = value.toLowerCase().replace(/^\*\./, "");
3407
- 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;
3408
- if (wildcardStripped.startsWith("10.") || wildcardStripped.startsWith("127.") || wildcardStripped.startsWith("192.168.") || wildcardStripped.startsWith("169.254.")) return true;
3409
- return /^172\.(1[6-9]|2\d|3[0-1])\./.test(wildcardStripped);
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 hasAvailableDecision(requestParams, decision) {
3412
- const available = requestParams?.availableDecisions;
3413
- if (!Array.isArray(available)) return true;
3414
- return available.includes(decision);
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 findAvailableCommandAmendmentDecision(requestParams) {
3417
- const available = requestParams?.availableDecisions;
3418
- if (!Array.isArray(available)) return;
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 commandRejectionDecision(requestParams, preferred) {
3422
- const available = requestParams?.availableDecisions;
3423
- if (!Array.isArray(available)) return preferred;
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 approvalResolutionMessage(outcome) {
3430
- if (outcome === "approved-session") return "Codex app-server approval granted for the session.";
3431
- if (outcome === "approved-once") return "Codex app-server approval granted for this turn.";
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 approvalScopeForOutcome(outcome) {
3437
- return outcome === "approved-session" ? "session" : "turn";
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 approvalEventScope(method, outcome) {
3440
- return method === "item/permissions/requestApproval" ? { scope: approvalScopeForOutcome(outcome) } : {};
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 approvalKindForMethod(method) {
3443
- if (method.includes("commandExecution") || method.includes("execCommand")) return "exec";
3444
- if (method.includes("fileChange") || method.includes("Patch") || method.includes("permissions")) return "plugin";
3445
- return "unknown";
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 emitApprovalEvent(params, data) {
3448
- params.onAgentEvent?.({
3449
- stream: "approval",
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 readDisplayCommandPreview(record) {
3454
- const actionCommand = readCommandActionsPreview(record);
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 readPolicyCommand(record) {
3459
- const command = record?.command;
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 readCommandActions(record) {
3466
- const actions = record?.commandActions;
3467
- if (!Array.isArray(actions)) return [];
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 readCommandActionsPreview(record) {
3471
- let source;
3472
- for (const command of readCommandActions(record)) {
3473
- source = appendPreviewPart(source, command, " && ");
3474
- if (source.clipped) break;
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 source;
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 readCommandPreview(record) {
3479
- const command = record?.command;
3480
- if (typeof command === "string") return previewSource(command);
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 readStringPreview(record, key) {
3491
- const value = readString(record, key);
3492
- return value === void 0 ? void 0 : previewSource(value);
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 readString(record, key) {
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 joinDescriptionLinesWithinLimit(lines, maxLength) {
3531
- let description = "";
3532
- for (const line of lines) {
3533
- const prefix = description ? "\n" : "";
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 toolResultMaxChars = resolveCodexDynamicToolResultMaxChars(params.hookContext?.contextWindowTokens);
3724
- const availableProjection = projectCodexDynamicTools(params.tools);
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 wrappedAvailableProjection = wrapProjectedCodexDynamicTools(availableProjection.tools, params.hookContext);
3727
- const availableTools = wrappedAvailableProjection.tools;
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 = jsonObjectToRecord(call.arguments);
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 = jsonObjectToRecord(call.arguments);
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 ? hasExplicitFinalControl ? executedArgs.final === true : void 0 : void 0;
3938
- const sourceReplyRecord = collectToolTelemetry({
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 defersInferredSourceReplyTermination = confirmedSourceReply && executedArgs.final !== true;
3954
- withDynamicToolTermination(response, (rawResult.terminate === true || result.terminate === true) && !defersInferredSourceReplyTermination || isToolResultYield(rawResult) || isToolResultYield(result) || confirmedSourceReply && executedArgs.final === 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) : DEFAULT_CODEX_DYNAMIC_TOOL_RESULT_MAX_CHARS;
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 = DEFAULT_CODEX_DYNAMIC_TOOL_RESULT_MAX_CHARS) {
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 (totalTextChars <= maxChars) return content.flatMap(convertToolContent);
4405
- const noticeText = `...(OpenClaw truncated dynamic tool result: original ${totalTextChars} chars, showing ${maxChars}; rerun with narrower args.)`;
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
- let remainingTextBudget = Math.max(0, maxChars - notice.length);
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 (notice.length >= maxChars) {
5798
+ if (noticeChars >= maxChars) {
4417
5799
  output.push({
4418
5800
  type: "inputText",
4419
- text: truncateUtf16Safe(noticeText, maxChars)
5801
+ text: sliceToolResultTextToBudget(noticeText, maxChars)
4420
5802
  });
4421
5803
  appendedNotice = true;
4422
5804
  continue;
4423
5805
  }
4424
- const sliceLength = Math.min(item.text.length, remainingTextBudget);
4425
- remainingTextBudget -= sliceLength;
4426
- const shouldAppendNotice = remainingTextBudget <= 0;
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: truncateUtf16Safe(noticeText, maxChars)
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 { shouldClearTerminalPresentationForNativeItem as $, createCodexNativeHookRelay as A, resolveTerminalDynamicToolBatchAction as B, filterToolsForVisionInputs as C, CODEX_NATIVE_HOOK_RELAY_TTL_GRACE_MS as D, CODEX_NATIVE_HOOK_RELAY_EVENTS as E, handleDynamicToolCallWithTimeout as F, resolveCodexToolAbortTerminalReason as G, shouldReleaseTurnAfterTerminalDynamicTool as H, hasPendingDynamicToolTerminalDiagnostic as I, isSideEffectingNativeToolItem as J, isMutatingNativeToolItem as K, isDynamicToolTerminalDiagnosticEvent as L, resolveCodexNativeHookRelayEvents as M, resolveCodexNativeHookRelayTtlMs as N, buildCodexNativeHookRelayConfig as O, scheduleCodexNativeHookRelayUnregister as P, itemTitle as Q, isMatchingDynamicToolTerminalDiagnostic as R, shouldWarnCodexDynamicToolBuildStageSummary as S, CodexNativeToolLifecycleProjector as T, toCodexDynamicToolProgressResponse as U, shouldBlockTerminalReleaseForNonTerminalDynamicToolResult as V, toCodexDynamicToolProtocolResponse as W, itemName as X, itemKind as Y, itemStatus as Z, resolveCodexExternalSandboxPolicyForOpenClawSandbox as _, emitDynamicToolStartedDiagnostic as a, shouldEnableCodexAppServerNativeToolSurface as b, resolveCodexProviderWebSearchSupportForClient as c, buildDynamicTools as d, shouldRecordNativeToolTranscript as et, createCodexDynamicToolBuildStageTracker as f, resolveCodexAppServerHookChannelId as g, resolveCodexAppServerExecutionCwd as h, emitDynamicToolErrorDiagnostic as i, emitCodexNativePreToolUseFailureDiagnostic as j, buildCodexNativeHookRelayDisabledConfig as k, buildCodexHookRequester as l, formatCodexDynamicToolBuildStageSummary as m, handleCodexAppServerApprovalRequest as n, unknownItemStatus as nt, emitDynamicToolTerminalDiagnostic as o, disableCodexPluginThreadConfig as p, isNonSuccessItemStatus as q, handleCodexAppServerElicitationRequest as r, resolveCodexProviderWebSearchSupport as s, createCodexDynamicToolBridge as t, shouldSynthesizeToolProgressForItem as tt, settleCodexSourceReplyFinality as u, resolveCodexMessageToolProvider as v, readBoundedCodexRemoteWorkspaceFile as w, shouldRequireCodexSandboxExecServerEnvironment as x, resolveCodexSandboxEnvironmentSelection as y, resolveDynamicToolCallTimeoutMs as z };
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 };