@openclaw/codex 2026.9.4 → 2026.9.6

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 (160) hide show
  1. package/assets/activity.svg +30 -0
  2. package/assets/icon.png +0 -0
  3. package/dist/.setup/account-usage-runtime-Eem_WGqY.mjs +75 -0
  4. package/dist/{apply-CvT8tW5i.js → .setup/apply-DDOi8uNx.mjs} +24 -30
  5. package/dist/.setup/async-questions-DdhOCwGi.mjs +27 -0
  6. package/dist/{attempt-client-cleanup-DKHzbwt5.js → .setup/attempt-client-cleanup-CEv1cKwF.mjs} +79 -49
  7. package/dist/.setup/attempt-terminal-gZrxQ35N.mjs +5 -0
  8. package/dist/.setup/attempt-workspace-context-DUVEn_CS.mjs +331 -0
  9. package/dist/{auth-binding-CWlKVccJ.js → .setup/auth-binding-DhLWa_7K.mjs} +1 -1
  10. package/dist/{auth-cache-key-7orQae_j.js → .setup/auth-cache-key-B2mpaI9D.mjs} +9 -81
  11. package/dist/{auth-profile-CwzO4_RG.js → .setup/auth-profile-WqZtZfXN.mjs} +1 -1
  12. package/dist/.setup/auth-profile-health-oHizqgye.mjs +53 -0
  13. package/dist/.setup/auth-profile-recovery-BREezV0L.mjs +14 -0
  14. package/dist/.setup/binding-connection-ThoatDcb.mjs +50 -0
  15. package/dist/.setup/binding-connection.runtime-yoHrABt-.mjs +48 -0
  16. package/dist/{bounded-turn-C_Hneydj.js → .setup/bounded-turn-B2HScEgT.mjs} +143 -32
  17. package/dist/{build-state-BCmg9-5p.js → .setup/build-state-C7EnDVgr.mjs} +1 -1
  18. package/dist/{capabilities-8vx68WLH.js → .setup/capabilities-CDXOOFdZ.mjs} +1 -1
  19. package/dist/{client-B57KWPQx.js → .setup/client-Cs08OXVQ.mjs} +705 -512
  20. package/dist/.setup/client-catalog-response-BMgt_4q4.mjs +285 -0
  21. package/dist/{command-dispatch-BpduNGVf.js → .setup/command-dispatch-B4JuOsOk.mjs} +8 -4
  22. package/dist/{command-formatters-lxlkNZgU.js → .setup/command-formatters-Bmqvr9tO.mjs} +8 -4
  23. package/dist/{command-handlers-tiL9iWj7.js → .setup/command-handlers-DW7Lx9oH.mjs} +137 -186
  24. package/dist/{command-rpc-CR5EOEZX.js → .setup/command-rpc-DixIj3HW.mjs} +47 -23
  25. package/dist/{compact-B_H3AOd7.js → .setup/compact-Du5UvtxA.mjs} +29 -51
  26. package/dist/{config-oIORaQ5T.js → .setup/config-BoTP_mrL.mjs} +5 -4
  27. package/dist/{config-options-CXMq1T-C.js → .setup/config-options-BvaRs51b.mjs} +50 -446
  28. package/dist/{protocol-5bh1G-H7.js → .setup/config-parsing-CcB9iPoq.mjs} +9 -51
  29. package/dist/{config-runtime-D0zGNIHu.js → .setup/config-runtime-C8Rw1m1m.mjs} +2 -2
  30. package/dist/.setup/config-security-BEReZ6go.mjs +380 -0
  31. package/dist/{conversation-binding-DsB4-O7U.js → .setup/conversation-binding-DP-gMrBP.mjs} +29 -38
  32. package/dist/{conversation-binding-data-BiodpIzq.js → .setup/conversation-binding-data-CrcyZk6O.mjs} +1 -1
  33. package/dist/{conversation-control-CtUu1b3m.js → .setup/conversation-control-uK9Jomlc.mjs} +32 -67
  34. package/dist/.setup/cyber-failover-DYwbmf1o.mjs +123 -0
  35. package/dist/.setup/cyber-failover-notice-e9G3J9-e.mjs +17 -0
  36. package/dist/{dynamic-tools-CmN5RAip.js → .setup/dynamic-tools-BDXl-8Cw.mjs} +936 -620
  37. package/dist/{effective-mcp-catalog-Bd73bqA8.js → .setup/effective-mcp-catalog-CvnvnGIe.mjs} +3 -3
  38. package/dist/{ephemeral-turn-D78_VmzO.js → .setup/ephemeral-turn-poidXgek.mjs} +9 -7
  39. package/dist/{event-projector-assistant-message-BmBM4qBV.js → .setup/event-projector-assistant-message-BsruvNrH.mjs} +8 -7
  40. package/dist/{event-projector-items-5VhsECCO.js → .setup/event-projector-items-C6L_5ZiT.mjs} +42 -8
  41. package/dist/{attempt-notifications-C9XufG6l.js → .setup/event-projector-usage-CnBS8Wa3.mjs} +102 -9
  42. package/dist/{harness-C7rRa7bm.js → .setup/harness-a_MsE4-_.mjs} +81 -21
  43. package/dist/{inference-context-Dr_mzvk4.js → .setup/inference-context-D96l9-ew.mjs} +1 -1
  44. package/dist/.setup/inference-proxy-b-aJMSxm.mjs +601 -0
  45. package/dist/{isolated-completion-DWVb5U2J.js → .setup/isolated-completion-BIZFhTNG.mjs} +18 -11
  46. package/dist/.setup/launch-args-DbFCehO7.mjs +196 -0
  47. package/dist/{managed-binary-D22rNEcR.js → .setup/managed-binary-BnshlFag.mjs} +14 -3
  48. package/dist/{managed-thread-store-D1t5jh3G.js → .setup/managed-thread-store-BMqgThVL.mjs} +48 -14
  49. package/dist/{model-catalog-Dq2n8f_p.js → .setup/model-catalog-CfXJf9Ay.mjs} +36 -12
  50. package/dist/{models-PCKOim-h.js → .setup/models-KnqBv0hE.mjs} +3 -3
  51. package/dist/.setup/native-auth-rWbGbMn3.mjs +57 -0
  52. package/dist/{native-subagent-history-DjtPloaU.js → .setup/native-subagent-history-0nYYgo0k.mjs} +43 -35
  53. package/dist/.setup/native-subagent-monitor-D3filLY2.mjs +4859 -0
  54. package/dist/{native-subagent-task-ids-BzTdT0Uw.js → .setup/native-subagent-task-ids-wb8CKZGN.mjs} +98 -44
  55. package/dist/{node-cli-sessions-DKaOmVUh.js → .setup/node-cli-sessions-r3avj02l.mjs} +86 -68
  56. package/dist/.setup/node-exec-server.runtime-BDwWXVpK.mjs +247 -0
  57. package/dist/{plan-7uG-Fp36.js → .setup/plan-CN3VwfNU.mjs} +133 -119
  58. package/dist/{plugin-activation-BYQculil.js → .setup/plugin-activation-DT3MNtqj.mjs} +7 -16
  59. package/dist/{plugin-app-cache-key-d0eKxy7b.js → .setup/plugin-app-cache-key-B2CsSdnV.mjs} +32 -44
  60. package/dist/{plugin-approval-roundtrip-Bz3FT8W5.js → .setup/plugin-approval-roundtrip-C0MZzCuP.mjs} +1 -1
  61. package/dist/{plugin-inventory-oL4Na1ZG.js → .setup/plugin-inventory-BoRei8Z4.mjs} +21 -11
  62. package/dist/{plugin-marketplace-discovery-P-Vndfqq.js → .setup/plugin-marketplace-discovery-CDOi8IFs.mjs} +4 -1
  63. package/dist/{plugin-metadata-cache-ClPJpRio.js → .setup/plugin-metadata-cache-Ctmo83k_.mjs} +226 -141
  64. package/dist/.setup/protocol-CANUwXJ3.mjs +17 -0
  65. package/dist/{provider-MQ8PVkuE.js → .setup/provider-BOsxQHOM.mjs} +9 -5
  66. package/dist/{provider-capabilities-BvCUL69A.js → .setup/provider-capabilities-BfE88FZ5.mjs} +31 -3
  67. package/dist/{rate-limits-CBMmWBr9.js → .setup/rate-limits-CMU3DUD0.mjs} +174 -151
  68. package/dist/{request-B7hEkBGq.js → .setup/request-D2L0zMrq.mjs} +130 -42
  69. package/dist/{run-attempt-CvsAAGa0.js → .setup/run-attempt-BHu11r7h.mjs} +1628 -966
  70. package/dist/.setup/run-attempt-lifecycle-B7JbY4-F.mjs +71 -0
  71. package/dist/{runtime-artifact-BJ0rXBZ_.js → .setup/runtime-artifact-CLZ8udh-.mjs} +29 -45
  72. package/dist/{sandbox-guard-BxgyoM8M.js → .setup/sandbox-guard-C2sRMOEY.mjs} +3 -3
  73. package/dist/{session-binding-CI8UuilT.js → .setup/session-binding-Cm0apEbd.mjs} +36 -83
  74. package/dist/{session-binding-record-Bcvslnhf.js → .setup/session-binding-record-BGoz8wOK.mjs} +202 -5
  75. package/dist/.setup/session-binding-sidecars-BlB5EvIS.mjs +546 -0
  76. package/dist/.setup/session-catalog-Pno1xGcl.mjs +2764 -0
  77. package/dist/{session-catalog-adoption-tcZi1IfX.js → .setup/session-catalog-adoption-BI6UTDZT.mjs} +58 -26
  78. package/dist/{session-catalog-archive-CwKg8Xz2.js → .setup/session-catalog-archive-lHBqzhaO.mjs} +6 -6
  79. package/dist/.setup/session-catalog-events-Bj6j94E_.mjs +347 -0
  80. package/dist/.setup/session-catalog-index-B9_VQIUz.mjs +1170 -0
  81. package/dist/.setup/session-catalog-index-order-hcdz07yN.mjs +48 -0
  82. package/dist/.setup/session-catalog-index-state-Dgh8ORJm.mjs +183 -0
  83. package/dist/{thread-history-page-D0JIp48V.js → .setup/session-catalog-native-projection-DowriLid.mjs} +223 -95
  84. package/dist/{session-catalog-node-adoption-B1tgDW-m.js → .setup/session-catalog-node-adoption-ITddWx_X.mjs} +25 -13
  85. package/dist/{session-catalog-node-continue-Ci-DXyJP.js → .setup/session-catalog-node-continue-6g6qi4A9.mjs} +24 -15
  86. package/dist/.setup/session-catalog-projection-zK_PUsiE.mjs +422 -0
  87. package/dist/{session-cli-DNN_jhGc.js → .setup/session-cli-irrD-uo7.mjs} +3 -3
  88. package/dist/{session-history-Cz-tOOYj.js → .setup/session-history-BrqhSCf9.mjs} +19 -19
  89. package/dist/{session-history-import-DwyijFI6.js → .setup/session-history-import-D-3wAgOP.mjs} +4 -4
  90. package/dist/{session-history-read-cxPUxyVV.js → .setup/session-history-read-B3ZfSUbF.mjs} +2 -2
  91. package/dist/{session-history.worker-BeK1YOyT.js → .setup/session-history.worker-CaCh0xF_.mjs} +10 -24
  92. package/dist/{session-initialization-Ct0_Hzmg.js → .setup/session-initialization-rcgbZiid.mjs} +2 -2
  93. package/dist/{session-permission-policy-BBmXA8Eg.js → .setup/session-permission-policy-BI1QK_98.mjs} +2 -2
  94. package/dist/{session-retirement-B2YfbAAT.js → .setup/session-retirement-BH-S6qkJ.mjs} +26 -7
  95. package/dist/{settled-turn-context-Du6gdUDd.js → .setup/settled-turn-context-fmwouT5I.mjs} +2 -2
  96. package/dist/{settled-turn-finalizer-Dn91stmc.js → .setup/settled-turn-finalizer-DiAi6Kx6.mjs} +12 -12
  97. package/dist/{shared-client-CscigXXL.js → .setup/shared-client-DA4VR4Eb.mjs} +332 -221
  98. package/dist/{side-question-BGMJQf4U.js → .setup/side-question-BJ1wSDcB.mjs} +183 -230
  99. package/dist/{source-CElXU0Oy.js → .setup/source-CUzJq5WN.mjs} +11 -10
  100. package/dist/.setup/spawn-error-CL3n6MAa.mjs +57 -0
  101. package/dist/{thread-archive-guard-DfpBr-Sz.js → .setup/thread-archive-guard-C9frICzX.mjs} +10 -3
  102. package/dist/.setup/thread-history-page-DoVpnHim.mjs +83 -0
  103. package/dist/{thread-lifecycle-DXl1-I6b.js → .setup/thread-lifecycle-B3mfz1TG.mjs} +347 -2318
  104. package/dist/{thread-ownership-BsDRTsyp.js → .setup/thread-ownership-DcTtAcXj.mjs} +6 -4
  105. package/dist/{thread-policy-BuEQfE0g.js → .setup/thread-policy-DGuQsuFC.mjs} +19 -3
  106. package/dist/.setup/thread-requests-BLvGkP2R.mjs +2205 -0
  107. package/dist/{thread-resume-DsDOZpFg.js → .setup/thread-resume-DIBXwOpE.mjs} +8 -5
  108. package/dist/.setup/timeout-C910MdAB.mjs +80 -0
  109. package/dist/{transcript-history-projection-v17wXgdK.js → .setup/transcript-history-projection-ZZpkFkJ_.mjs} +96 -61
  110. package/dist/{transcript-mirror-C5CKgNy6.js → .setup/transcript-mirror-BtHDBp1G.mjs} +224 -195
  111. package/dist/{transcript-mirror-attestation-DibE0krH.js → .setup/transcript-mirror-attestation-CzM-94NC.mjs} +4 -1
  112. package/dist/{transport-stdio-BGjHoYLr.js → .setup/transport-stdio-h2VHOskC.mjs} +274 -89
  113. package/dist/{upstream-session-fork-BZerMte_.js → .setup/upstream-session-fork-BUr4OUbC.mjs} +51 -42
  114. package/dist/{usage-tCk-mg_x.js → .setup/usage-BML0r-ua.mjs} +5 -4
  115. package/dist/.setup/usage-limit-error-DUMbefVY.mjs +166 -0
  116. package/dist/{web-search-provider.runtime-B7bGbGWq.js → .setup/web-search-provider.runtime-CDKlN-Rv.mjs} +4 -3
  117. package/dist/api.js +23 -12
  118. package/dist/catalog-page-worker-entrypoint.js +12 -0
  119. package/dist/catalog-page.worker.js +9 -0
  120. package/dist/cli-metadata.js +1 -1
  121. package/dist/doctor-contract-api.js +8 -528
  122. package/dist/harness.js +1 -1
  123. package/dist/index.js +306 -215
  124. package/dist/media-understanding-provider.js +3 -3
  125. package/dist/migration-provider-api.js +1 -1
  126. package/dist/native-tool-policy.js +12 -0
  127. package/dist/provider-discovery.js +18 -0
  128. package/dist/session-history-worker-runtime.js +36 -35
  129. package/dist/session-history.worker.js +2 -2
  130. package/dist/web-search-contract-api.js +1 -1
  131. package/openclaw.plugin.json +354 -69
  132. package/package.json +14 -8
  133. package/dist/attempt-context-D44yeDII.js +0 -636
  134. package/dist/binding-connection-D3udKbIE.js +0 -60
  135. package/dist/event-projector-usage-BfXmpOec.js +0 -102
  136. package/dist/inference-proxy-HGXKUIre.js +0 -311
  137. package/dist/launch-args-RXQwn8zV.js +0 -90
  138. package/dist/native-subagent-history-owner-Pcp7JuVF.js +0 -34
  139. package/dist/native-subagent-monitor-fQ7eIXEf.js +0 -2978
  140. package/dist/node-exec-server.runtime-Cl2vapEi.js +0 -226
  141. package/dist/rpc-error-ttN_QjEm.js +0 -30
  142. package/dist/session-catalog-ct-AH01I.js +0 -1477
  143. package/dist/session-catalog-home-id-CwhLdC03.js +0 -18
  144. package/dist/session-catalog-provenance-D-dIfRAH.js +0 -96
  145. package/dist/timeout-BVw8aaM1.js +0 -33
  146. package/dist/version-omm6IPI3.js +0 -12
  147. /package/dist/{auth-profile-selection-DFu9e167.js → .setup/auth-profile-selection-DFu9e167.mjs} +0 -0
  148. /package/dist/{config-utils-DujwEnhg.js → .setup/config-utils-DujwEnhg.mjs} +0 -0
  149. /package/dist/{context-compaction-activity-CMla5FPJ.js → .setup/context-compaction-activity-CMla5FPJ.mjs} +0 -0
  150. /package/dist/{elicitation-input-DPoK8F7w.js → .setup/elicitation-input-DPoK8F7w.mjs} +0 -0
  151. /package/dist/{history-rejection-B4nOXNXF.js → .setup/history-rejection-B4nOXNXF.mjs} +0 -0
  152. /package/dist/{image-payload-sanitizer-tupAr-2o.js → .setup/image-payload-sanitizer-tupAr-2o.mjs} +0 -0
  153. /package/dist/{incognito-session-uhrBF6wJ.js → .setup/incognito-session-uhrBF6wJ.mjs} +0 -0
  154. /package/dist/{local-runtime-attribution-B_2mnqhb.js → .setup/local-runtime-attribution-B_2mnqhb.mjs} +0 -0
  155. /package/dist/{native-execution-policy-umd0tgh4.js → .setup/native-execution-policy-umd0tgh4.mjs} +0 -0
  156. /package/dist/{rolldown-runtime-8H4AJuhK.js → .setup/rolldown-runtime-8H4AJuhK.mjs} +0 -0
  157. /package/dist/{session-binding-meta-B7aEMU7g.js → .setup/session-binding-meta-B7aEMU7g.mjs} +0 -0
  158. /package/dist/{session-upstream-marker-D15C9NHp.js → .setup/session-upstream-marker-D15C9NHp.mjs} +0 -0
  159. /package/dist/{upstream-prompt-provenance-LphB8slG.js → .setup/upstream-prompt-provenance-LphB8slG.mjs} +0 -0
  160. /package/dist/{web-search-provider.shared-BrZmlqyR.js → .setup/web-search-provider.shared-BrZmlqyR.mjs} +0 -0
@@ -1,47 +1,47 @@
1
- import { a as flattenCodexDynamicToolFunctions, o as isJsonObject, r as CODEX_OPENCLAW_DIRECT_DYNAMIC_TOOL_NAMESPACE, x as readCodexPluginConfig, y as isCodexRemoteExecPlacementSandbox } from "./protocol-5bh1G-H7.js";
2
- import { B as withDynamicToolTranscriptDetails, E as createCodexElicitationResponse, L as resolveCodexToolAbortTerminalReason, R as createFailedDynamicToolResponse, w as readCodexTurn, z as withDynamicToolExecutionState } from "./client-B57KWPQx.js";
3
- import { o as formatCodexDisplayText } from "./command-formatters-lxlkNZgU.js";
4
- import { c as itemName, f as shouldClearTerminalPresentationForNativeItem, l as itemStatus, n as auditNativeToolTerminalStatus, r as auditNativeToolUnfinishedStatus, t as auditNativeToolName, x as readItem } from "./event-projector-items-5VhsECCO.js";
5
- import "./config-oIORaQ5T.js";
6
- import { r as sanitizeInlineImageDataUrl, t as invalidInlineImageText } from "./image-payload-sanitizer-tupAr-2o.js";
7
- import { h as isCodexNotificationForTurn } from "./attempt-client-cleanup-DKHzbwt5.js";
8
- import { C as resolveCodexWebSearchPlan, V as emitCodexNativePreToolUseFailureDiagnostic, at as isForcedPrivateQaCodexRuntime, ct as normalizeCodexDynamicToolName, it as filterCodexDynamicToolsForDisabledNativeSurface, rt as filterCodexDynamicTools, st as isSystemAgentOnlyCodexDynamicToolAllowlist } from "./thread-lifecycle-DXl1-I6b.js";
9
- import { n as resolveCodexNativeExecutionPolicy, r as resolveCodexNodeExecToolOverrides } from "./native-execution-policy-umd0tgh4.js";
10
- import { a as CODEX_NODE_EXEC_DYNAMIC_TOOL_NAME, c as createNodeExecAliasDynamicTool, i as CODEX_GATEWAY_PROCESS_DYNAMIC_TOOL_NAME, l as isCodexDynamicToolExcluded, o as createGatewayExecProjection, r as CODEX_GATEWAY_EXEC_DYNAMIC_TOOL_NAME, s as createGatewayProcessProjection } from "./provider-capabilities-BvCUL69A.js";
11
- import { a as requestPluginApprovalOutcome, c as truncateCodexApprovalDisplayText, i as requestPluginApproval, l as waitForPluginApprovalDecision, n as codexApprovalTimeoutText, o as sanitizeCodexApprovalVisibleText, r as mapExecDecisionToOutcome, s as stripDanglingCodexApprovalTerminalSequence, t as approvalRequestExplicitlyUnavailable } from "./plugin-approval-roundtrip-Bz3FT8W5.js";
1
+ import { CODEX_NATIVE_TOOL_REQUIREMENTS } from "../native-tool-policy.js";
2
+ import { d as readCodexPluginConfig, l as isCodexRemoteExecPlacementSandbox } from "./config-parsing-CcB9iPoq.mjs";
3
+ import { i as isJsonObject, r as flattenCodexDynamicToolFunctions, t as CODEX_OPENCLAW_DIRECT_DYNAMIC_TOOL_NAMESPACE } from "./protocol-CANUwXJ3.mjs";
4
+ import { o as formatCodexDisplayText } from "./command-formatters-Bmqvr9tO.mjs";
5
+ import { S as readItem, c as itemName, f as shouldClearTerminalPresentationForNativeItem, l as itemStatus, n as auditNativeToolTerminalStatus, r as auditNativeToolUnfinishedStatus, t as auditNativeToolName } from "./event-projector-items-C6L_5ZiT.mjs";
6
+ import { s as codexAppIdentityKey } from "./plugin-app-cache-key-B2CsSdnV.mjs";
7
+ import "./config-BoTP_mrL.mjs";
8
+ import { r as sanitizeInlineImageDataUrl, t as invalidInlineImageText } from "./image-payload-sanitizer-tupAr-2o.mjs";
9
+ import { V as protectCodexAppServerLiveThread, v as retainSharedCodexAppServerClientIfCurrent } from "./shared-client-DA4VR4Eb.mjs";
10
+ import { E as createCodexElicitationResponse, L as resolveCodexToolAbortTerminalReason, R as createFailedDynamicToolResponse, T as readCodexTurnCompletedNotification, m as CodexServerRequestResolvedError, z as failedToolResult } from "./client-Cs08OXVQ.mjs";
11
+ import { b as readCodexNotificationTurnId, v as isCodexNotificationForTurn, y as readCodexNotificationThreadId } from "./attempt-client-cleanup-CEv1cKwF.mjs";
12
+ import { A as emitCodexNativePreToolUseFailureDiagnostic, at as filterCodexDynamicTools, lt as isSystemAgentOnlyCodexDynamicToolAllowlist, ot as filterCodexDynamicToolsForDisabledNativeSurface, p as resolveCodexWebSearchPlan, st as isForcedPrivateQaCodexRuntime, ut as normalizeCodexDynamicToolName } from "./thread-requests-BLvGkP2R.mjs";
13
+ import { n as resolveCodexNativeExecutionPolicy, r as resolveCodexNodeExecToolOverrides } from "./native-execution-policy-umd0tgh4.mjs";
14
+ import { a as CODEX_NODE_EXEC_DYNAMIC_TOOL_NAME, c as createNodeExecAliasDynamicTool, d as isCodexDynamicToolExcluded, i as CODEX_GATEWAY_PROCESS_DYNAMIC_TOOL_NAME, l as createSandboxExecProjection, o as createGatewayExecProjection, r as CODEX_GATEWAY_EXEC_DYNAMIC_TOOL_NAME, s as createGatewayProcessProjection, u as createSandboxProcessProjection } from "./provider-capabilities-BfE88FZ5.mjs";
15
+ import { a as requestPluginApprovalOutcome, c as truncateCodexApprovalDisplayText, i as requestPluginApproval, l as waitForPluginApprovalDecision, n as codexApprovalTimeoutText, o as sanitizeCodexApprovalVisibleText, r as mapExecDecisionToOutcome, s as stripDanglingCodexApprovalTerminalSequence, t as approvalRequestExplicitlyUnavailable } from "./plugin-approval-roundtrip-C0MZzCuP.mjs";
12
16
  import { createRequire } from "node:module";
13
- import { asNonArrayRecord, asOptionalRecord, isRecord, normalizeOptionalString, normalizeTrimmedStringList, readStringField } from "openclaw/plugin-sdk/string-coerce-runtime";
17
+ import { asNonArrayRecord, asOptionalRecord, isRecord, normalizeTrimmedStringList, readStringField } from "openclaw/plugin-sdk/string-coerce-runtime";
14
18
  import { createHash, randomUUID } from "node:crypto";
15
19
  import { asDateTimestampMs } from "openclaw/plugin-sdk/number-runtime";
16
- import { killProcessTree } from "openclaw/plugin-sdk/process-runtime";
20
+ import { signalProcessTree, spawnTerminalPty } from "openclaw/plugin-sdk/process-runtime";
17
21
  import { validateJsonSchemaValue } from "openclaw/plugin-sdk/json-schema-runtime";
18
22
  import path, { posix } from "node:path";
23
+ import { isPathStrictlyInside, root } from "openclaw/plugin-sdk/file-access-runtime";
19
24
  import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
20
25
  import { once } from "node:events";
21
26
  import { spawn } from "node:child_process";
22
27
  import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
23
28
  import { DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS, estimateToolResultTextChars, resolveLiveToolResultMaxChars, sliceToolResultTextToBudget, sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
29
+ import { emitTrustedDiagnosticEvent } from "openclaw/plugin-sdk/diagnostic-runtime";
24
30
  import { createDeferred } from "openclaw/plugin-sdk/extension-shared";
25
31
  import { createStageTimingTracker, formatStageTimings } from "openclaw/plugin-sdk/time-runtime";
26
- import { isPathStrictlyInside, root } from "openclaw/plugin-sdk/file-access-runtime";
27
32
  import { fileURLToPath, pathToFileURL } from "node:url";
28
33
  import { resolveAgentDir } from "openclaw/plugin-sdk/agent-runtime";
29
- import { HEARTBEAT_RESPONSE_TOOL_NAME, buildAgentHookContextChannelFields, buildEmbeddedAttemptToolRunContext, consumeAdjustedParamsForToolCall, consumePreExecutionBlockedToolCall, createAgentToolResultMiddlewareRunner, createCodexAppServerToolResultExtensionRunner, embeddedAgentLog, extractMessagingToolSend, extractMessagingToolSendResult, extractToolResultMediaArtifact, filterProviderNormalizableTools, filterToolResultMediaUrls, finalizeAgentToolAvailability, finalizeToolTerminalPresentation, formatApprovalDisplayPath, formatToolExecutionErrorMessage, getBeforeToolCallFailureDisposition, getChannelAgentToolMeta, getPluginToolMeta, getPluginToolSideEffectOwnerKey, hasNativeHookRelayInvocation, invokeNativeHookRelay, isDeliveredMessageToolOnlySourceReplyResult, isDeliveredMessagingToolResult, isHostScopedAgentToolActive, isMessagingTool, isMessagingToolSendAction, isReplaySafeToolCall, isSubagentSessionKey, isToolResultError, isToolWrappedWithBeforeToolCallHook, normalizeAgentRuntimeTools, normalizeHeartbeatToolResponse, projectRuntimeToolInputSchema, resolveAttemptSpawnWorkspaceDir, resolveModelAuthMode, resolveNativeHookRelayDeferredToolApproval, resolveToolExecutionErrorKind, resolveToolResultFailureKind, runAgentHarnessAfterToolCallHook, sanitizeToolResult, setBeforeToolCallDiagnosticsEnabled, supportsModelTools, wrapToolWithBeforeToolCallHook } from "openclaw/plugin-sdk/agent-harness-runtime";
34
+ import { HEARTBEAT_RESPONSE_TOOL_NAME, applyEmbeddedAttemptToolsAllow, buildAgentHookContextChannelFields, buildEmbeddedAttemptToolRunContext, consumeAdjustedParamsForToolCall, consumePreExecutionBlockedToolCall, createAgentToolResultMiddlewareRunner, createCodexAppServerToolResultExtensionRunner, embeddedAgentLog, extractMessagingToolSend, extractMessagingToolSendResult, extractMessagingToolSourceReplyPayload, extractToolResultMediaArtifact, filterProviderNormalizableTools, filterToolResultMediaUrls, finalizeAgentToolAvailability, finalizeToolTerminalPresentation, formatApprovalDisplayPath, formatToolExecutionErrorMessage, getBeforeToolCallFailureDisposition, getChannelAgentToolMeta, getPluginToolMeta, getPluginToolSideEffectOwnerKey, hasNativeHookRelayInvocation, invokeNativeHookRelay, isDeliveredMessageToolOnlySourceReplyResult, isDeliveredMessagingToolResult, isDeliveredMessagingToolSendToCurrentSource, isHostScopedAgentToolActive, isMessagingTool, isReplaySafeToolCall, isSubagentSessionKey, isToolResultError, isToolWrappedWithBeforeToolCallHook, normalizeAgentRuntimeTools, normalizeHeartbeatToolResponse, projectPluginMessageDeliveryFact, projectRuntimeToolInputSchema, readEmbeddedMessageDeliveryFact, readToolOperatorHint, resolveAttemptSpawnWorkspaceDir, resolveModelAuthMode, resolveNativeHookRelayDeferredToolApproval, resolveToolExecutionErrorKind, resolveToolResultFailureKind, runAgentHarnessAfterToolCallHook, sanitizeToolResult, setBeforeToolCallDiagnosticsEnabled, supportsModelTools, wrapToolWithBeforeToolCallHook } from "openclaw/plugin-sdk/agent-harness-runtime";
30
35
  import { formatErrorMessage as formatErrorMessage$2, redactSensitiveText } from "openclaw/plugin-sdk/security-runtime";
31
- import { emitTrustedDiagnosticEvent } from "openclaw/plugin-sdk/diagnostic-runtime";
36
+ import { consumeTrustedToolNoStartError, copyInternalToolResultState, getCoreTtsToolResultMediaUrls, normalizeAcceptedSessionSpawnResult } from "openclaw/plugin-sdk/agent-harness-tool-runtime";
32
37
  import { isIP } from "node:net";
33
38
  import { redactToolPayloadText } from "openclaw/plugin-sdk/logging-core";
34
39
  import { formatMcpCodexApprovalRemedy, requiresMcpCodexToolApproval, resolveCodexScheduledToolProjectionFactory, resolveProjectedMcpCodexToolApprovalMode, runWithCronCreatorAuthorityCapabilityResolver } from "openclaw/plugin-sdk/codex-mcp-projection";
35
- import { buildRemoteCommand, isToolAllowed, sanitizeEnvVars } from "openclaw/plugin-sdk/sandbox";
40
+ import { SANDBOX_COMMAND_MAX_BUFFER_BYTES, buildRemoteCommand, isToolAllowed, prepareSandboxProcessCleanup, sanitizeEnvVars } from "openclaw/plugin-sdk/sandbox";
36
41
  import { SsrFBlockedError, isBlockedHostnameOrIp } from "openclaw/plugin-sdk/ssrf-runtime";
37
- import { saveMediaBuffer } from "openclaw/plugin-sdk/media-store";
42
+ import { normalizeMediaReferenceForComparison, saveMediaBuffer } from "openclaw/plugin-sdk/media-store";
38
43
  import { getMediaDir } from "openclaw/plugin-sdk/media-runtime";
39
- import { consumeTrustedToolNoStartError, copyInternalToolResultState, getCoreTtsToolResultMediaUrls } from "openclaw/plugin-sdk/agent-harness-tool-runtime";
40
44
  import { normalizeOpenAIStrictCompatSchema } from "openclaw/plugin-sdk/provider-tools";
41
- //#region extensions/codex/src/app-server/dynamic-tool-build-state.ts
42
- /** Mutable dependency seam shared by dynamic-tool construction and its behavioral tests. */
43
- const dynamicToolBuildState = {};
44
- //#endregion
45
45
  //#region extensions/codex/src/app-server/remote-workspace-path.ts
46
46
  /** Projects a gateway workspace path into the remote Codex execution workspace. */
47
47
  function mapCodexAppServerRemoteWorkspacePath(params) {
@@ -106,14 +106,6 @@ function filterCodexVisionTools(tools, params) {
106
106
  * OpenClaw-owned tools, Codex native-tool fallback rules, sandbox shell shims,
107
107
  * and provider allowlist normalization.
108
108
  */
109
- const CODEX_NATIVE_SANDBOX_TOOL_REQUIREMENTS = [
110
- "exec",
111
- "process",
112
- "read",
113
- "write",
114
- "edit",
115
- "apply_patch"
116
- ];
117
109
  const CODEX_MEMORY_FLUSH_DYNAMIC_TOOL_ALLOW = /* @__PURE__ */ new Set(["read", "write"]);
118
110
  const CODEX_DISABLED_NATIVE_SHELL_DYNAMIC_TOOLS = /* @__PURE__ */ new Set([
119
111
  "exec",
@@ -198,7 +190,6 @@ async function buildDynamicTools(input) {
198
190
  const toolBuildStages = createCodexDynamicToolBuildStageTracker();
199
191
  const modelHasVision = params.model.input?.includes("image") ?? false;
200
192
  const agentDir = params.agentDir ?? resolveAgentDir(params.config ?? {}, input.sessionAgentId);
201
- const injectedOpenClawCodingToolsFactory = dynamicToolBuildState.openClawCodingToolsFactory;
202
193
  const nativeExecutionPolicy = resolveCodexNativeExecutionPolicyForRun(params, {
203
194
  agentId: input.policyAgentId,
204
195
  runtimeSessionKey: input.sandboxSessionKey,
@@ -210,6 +201,7 @@ async function buildDynamicTools(input) {
210
201
  nativeToolSurfaceEnabled: input.nativeToolSurfaceEnabled,
211
202
  nativeProviderWebSearchSupport: input.nativeProviderWebSearchSupport
212
203
  });
204
+ const messageToolProvider = resolveCodexMessageToolProvider(params);
213
205
  const webFetchHostnameAllowlistRef = {};
214
206
  const toolConstructionPlan = resolveCodexNodePlacementToolConstructionPlan(input.sandbox, input.nativeToolSurfaceEnabled);
215
207
  const options = {
@@ -229,11 +221,11 @@ async function buildDynamicTools(input) {
229
221
  } : void 0,
230
222
  sandbox: input.sandbox,
231
223
  ...toolConstructionPlan ? { toolConstructionPlan } : {},
232
- messageProvider: resolveCodexMessageToolProvider(params),
224
+ messageProvider: messageToolProvider,
233
225
  toolPolicyMessageProvider: params.messageProvider ?? params.messageChannel,
234
226
  ...params.onToolResult ? { questionPrompt: {
235
227
  send: params.onToolResult,
236
- ...params.messageChannel ? { messageChannel: params.messageChannel } : {}
228
+ messageChannel: messageToolProvider
237
229
  } } : {},
238
230
  inputProvenance: params.inputProvenance,
239
231
  trustedInternalHandoff: params.trustedInternalHandoff,
@@ -253,7 +245,6 @@ async function buildDynamicTools(input) {
253
245
  config: params.config,
254
246
  skillsSnapshot: params.skillsSnapshot,
255
247
  ...params.skillLibraryAuthoring ? { skillWorkshop: { libraryAuthoring: params.skillLibraryAuthoring } } : {},
256
- githubPublicationAvailable: params.githubPublicationAvailable,
257
248
  authProfileStore: params.toolAuthProfileStore ?? params.authProfileStore,
258
249
  abortSignal: input.runAbortController.signal,
259
250
  emitBeforeToolCallDiagnostics: false,
@@ -300,7 +291,6 @@ async function buildDynamicTools(input) {
300
291
  input.onMessageToolTargetResolved?.(options.requireExplicitMessageTarget === true);
301
292
  const buildOpenClawCodingTools = () => {
302
293
  const bindingOptions = { cwd: input.effectiveCwd ?? input.effectiveWorkspace };
303
- if (injectedOpenClawCodingToolsFactory) return params.hostCapabilities.bindToolSurface(injectedOpenClawCodingToolsFactory(options), bindingOptions);
304
294
  const createToolSurface = params.hostCapabilities.createToolSurface;
305
295
  if (!createToolSurface) throw new Error("Codex tool construction requires a current host capability");
306
296
  return createToolSurface(options, bindingOptions);
@@ -336,7 +326,7 @@ async function buildDynamicTools(input) {
336
326
  agentId: input.policyAgentId,
337
327
  sessionKey: input.sandboxSessionKey,
338
328
  sandboxToolPolicy: input.sandbox?.tools,
339
- messageProvider: resolveCodexMessageToolProvider(params),
329
+ messageProvider: messageToolProvider,
340
330
  agentAccountId: params.agentAccountId,
341
331
  groupId: params.groupId,
342
332
  groupChannel: params.groupChannel,
@@ -457,7 +447,7 @@ function canCodexAppServerNativeToolSurfaceHonorSandbox(sandbox, options = {}) {
457
447
  return false;
458
448
  }
459
449
  function canSandboxToolPolicyExposeCodexNativeToolSurface(sandbox) {
460
- return CODEX_NATIVE_SANDBOX_TOOL_REQUIREMENTS.every((toolName) => isToolAllowed(sandbox.tools, toolName));
450
+ return CODEX_NATIVE_TOOL_REQUIREMENTS.every((toolName) => isToolAllowed(sandbox.tools, toolName));
461
451
  }
462
452
  function isCodexMemoryFlushRun(params) {
463
453
  return params?.trigger === "memory" && Boolean(params.memoryFlushWritePath?.trim());
@@ -513,29 +503,9 @@ function addSandboxShellDynamicToolsIfAvailable(filteredTools, allTools, input,
513
503
  if (isCodexMemoryFlushRun(input.params) || !executionPolicy.nativeToolSurfaceAllowed || !input.sandbox?.enabled || !input.sandbox.backendId.trim() || input.nativeToolSurfaceEnabled !== false || isSandboxShellDynamicToolExcluded(input.pluginConfig)) return filteredTools;
514
504
  const execTool = allTools.find((tool) => normalizeCodexDynamicToolName(tool.name) === "exec");
515
505
  const processTool = allTools.find((tool) => normalizeCodexDynamicToolName(tool.name) === "process");
516
- if (!execTool || !processTool) return filteredTools;
517
- const sandboxExecTool = {
518
- ...execTool,
519
- name: "sandbox_exec",
520
- description: "Run a shell command through OpenClaw's configured sandbox backend for this session. Use when OpenClaw sandboxing is active or when a command must execute in the sandbox backend, such as an SSH-backed sandbox or Docker container-path bind layout. Use Codex's native shell only when no OpenClaw sandbox is active and native Code Mode is available.",
521
- execute: async (toolCallId, args, signal, onUpdate) => {
522
- const result = await execTool.execute(toolCallId, args, signal, onUpdate);
523
- return {
524
- ...result,
525
- content: result.content.map((item) => item.type === "text" ? Object.assign({}, item, { text: item.text.replace("Use process (list/poll/log/write/send-keys/submit/paste/kill/clear/remove) for follow-up.", "Use sandbox_process (list/poll/log/write/send-keys/submit/paste/kill/clear/remove) for follow-up.") }) : item)
526
- };
527
- }
528
- };
529
- const sandboxProcessTool = {
530
- ...processTool,
531
- name: "sandbox_process",
532
- description: "Manage background shell sessions through OpenClaw's configured sandbox backend for this session: list, poll, log, write, send-keys, submit, paste, kill, clear, or remove. Use only for sandbox follow-up; use Codex's native shell session handling only when no OpenClaw sandbox is active and native Code Mode is available."
533
- };
534
- return [
535
- ...filteredTools,
536
- sandboxExecTool,
537
- sandboxProcessTool
538
- ];
506
+ if (!execTool) return filteredTools;
507
+ const sandboxTools = [...filteredTools, createSandboxExecProjection(execTool)];
508
+ return processTool ? [...sandboxTools, createSandboxProcessProjection(processTool)] : sandboxTools;
539
509
  }
540
510
  function isSandboxShellDynamicToolExcluded(config) {
541
511
  return isCodexDynamicToolExcluded(config, [
@@ -566,13 +536,12 @@ function placeDisabledNativeShellToolsInDirectNamespace(tools, nativeToolSurface
566
536
  }
567
537
  /** Applies a normalized tool allowlist while preserving shell aliases for exec/process. */
568
538
  function filterCodexDynamicToolsForAllowlist(tools, toolsAllow) {
569
- if (!toolsAllow) return tools;
570
- if (toolsAllow.length === 0) return [];
571
- if (hasWildcardCodexToolsAllow(toolsAllow)) return tools;
572
- const allowSet = new Set(toolsAllow.map((name) => normalizeCodexDynamicToolName(name)).filter(Boolean));
573
- return tools.filter((tool) => {
574
- const normalized = normalizeCodexDynamicToolName(tool.name);
575
- return allowSet.has(normalized) || normalized === "sandbox_exec" && allowSet.has("exec") || normalized === "sandbox_process" && (allowSet.has("exec") || allowSet.has("process")) || normalized === "gateway_exec" && allowSet.has("exec") || normalized === "gateway_process" && (allowSet.has("exec") || allowSet.has("process")) || normalized === "node_exec" && allowSet.has("exec");
539
+ return applyEmbeddedAttemptToolsAllow(tools, toolsAllow, {
540
+ toolMeta: getPluginToolMeta,
541
+ toolAliases: (tool) => {
542
+ const normalized = normalizeCodexDynamicToolName(tool.name);
543
+ return normalized === "sandbox_exec" || normalized === "gateway_exec" || normalized === "node_exec" ? ["exec"] : normalized === "sandbox_process" || normalized === "gateway_process" ? ["exec", "process"] : [];
544
+ }
576
545
  });
577
546
  }
578
547
  /** Detects the wildcard allowlist marker after Codex tool-name normalization. */
@@ -645,17 +614,17 @@ var CodexNativeToolLifecycleProjector = class {
645
614
  const params = isJsonObject(notification.params) ? notification.params : void 0;
646
615
  if (!params || !isCodexNotificationForTurn(params, this.threadId, this.turnId)) return;
647
616
  if (isMcpToolCallItemNotification(notification.method, params)) this.pendingMcpNotifications += 1;
648
- else if (notification.method === "turn/completed" && readCodexTurn(params.turn)?.id === this.turnId) this.turnCompleted = true;
617
+ else if (notification.method === "turn/completed" && readCodexTurnCompletedNotification(params)?.turn.id === this.turnId) this.turnCompleted = true;
649
618
  }
650
619
  handleNotification(notification) {
651
620
  const params = isJsonObject(notification.params) ? notification.params : void 0;
652
621
  if (!params || !isCodexNotificationForTurn(params, this.threadId, this.turnId)) return;
653
622
  if (this.pendingMcpNotifications > 0 && isMcpToolCallItemNotification(notification.method, params)) this.pendingMcpNotifications -= 1;
654
623
  if (notification.method === "turn/completed") {
655
- const turn = readCodexTurn(params.turn);
624
+ const turn = readCodexTurnCompletedNotification(params)?.turn;
656
625
  if (!turn || turn.id !== this.turnId) return;
657
626
  this.turnCompleted = true;
658
- for (const item of turn.items ?? []) this.recordSnapshotItem(item);
627
+ for (const item of turn.items) this.recordSnapshotItem(item);
659
628
  return;
660
629
  }
661
630
  if (notification.method === "rawResponseItem/completed") {
@@ -899,6 +868,22 @@ const ATTACHMENT_MEDIA_KEYS = [
899
868
  "fileUrl",
900
869
  "url"
901
870
  ];
871
+ function collectCodexMessageMediaUrls(record) {
872
+ const urls = [];
873
+ const push = (value) => {
874
+ if (typeof value === "string" && value.trim()) urls.push(value.trim());
875
+ };
876
+ for (const key of MESSAGE_MEDIA_KEYS) push(record[key]);
877
+ for (const key of MESSAGE_MEDIA_ARRAY_KEYS) {
878
+ const value = record[key];
879
+ if (Array.isArray(value)) for (const entry of value) push(entry);
880
+ }
881
+ if (Array.isArray(record.attachments)) for (const attachment of record.attachments) {
882
+ if (!isRecord(attachment)) continue;
883
+ for (const key of ATTACHMENT_MEDIA_KEYS) push(attachment[key]);
884
+ }
885
+ return urls;
886
+ }
902
887
  /** Reads actual remote bytes with a cap enforced by Codex before transport. */
903
888
  async function readBoundedCodexRemoteWorkspaceFile(params) {
904
889
  if (!Number.isSafeInteger(params.maxBytes) || params.maxBytes < 0) throw new Error("Codex remote workspace upload requires a valid media byte limit.");
@@ -907,10 +892,10 @@ async function readBoundedCodexRemoteWorkspaceFile(params) {
907
892
  let offset = 0;
908
893
  let expectedSize;
909
894
  let expectedRevision;
910
- const startedAt = Date.now();
895
+ const startedAt = performance.now();
911
896
  do {
912
897
  params.signal?.throwIfAborted();
913
- const timeoutMs = params.timeoutMs === void 0 ? void 0 : params.timeoutMs - (Date.now() - startedAt);
898
+ const timeoutMs = params.timeoutMs === void 0 ? void 0 : Math.floor(params.timeoutMs - (performance.now() - startedAt));
914
899
  if (timeoutMs !== void 0 && timeoutMs <= 0) throw new Error("Codex remote workspace file transfer timed out.");
915
900
  let response;
916
901
  try {
@@ -971,7 +956,11 @@ async function readBoundedCodexRemoteWorkspaceFile(params) {
971
956
  /** Stages authoritative bounded remote bytes into immutable Gateway-owned media. */
972
957
  async function prepareCodexRemoteWorkspaceMessageMedia(params) {
973
958
  const { localWorkspaceRoot, remoteWorkspaceRoot } = params;
974
- if (!localWorkspaceRoot || !remoteWorkspaceRoot) return params.args;
959
+ const sourcePathsByStagedPath = /* @__PURE__ */ new Map();
960
+ if (!localWorkspaceRoot || !remoteWorkspaceRoot) return {
961
+ args: params.args,
962
+ sourcePathsByStagedPath
963
+ };
975
964
  const remotePathsByLocalPath = /* @__PURE__ */ new Map();
976
965
  const gatewayManagedPaths = /* @__PURE__ */ new Set();
977
966
  const gatewayMediaRoot = getMediaDir();
@@ -990,11 +979,18 @@ async function prepareCodexRemoteWorkspaceMessageMedia(params) {
990
979
  });
991
980
  if (value.trim() && !isCodexPassThroughMediaSource(value)) {
992
981
  attachmentEntries += 1;
993
- remotePathsByLocalPath.set(mapped, mapCodexAppServerRemoteWorkspacePath({
982
+ const remotePath = mapCodexAppServerRemoteWorkspacePath({
994
983
  value: mapped,
995
984
  localWorkspaceRoot,
996
985
  remoteWorkspaceRoot
997
- }));
986
+ });
987
+ const sourcePaths = remotePathsByLocalPath.get(mapped)?.sourcePaths ?? /* @__PURE__ */ new Set();
988
+ sourcePaths.add(value);
989
+ sourcePaths.add(remotePath);
990
+ remotePathsByLocalPath.set(mapped, {
991
+ remotePath,
992
+ sourcePaths
993
+ });
998
994
  }
999
995
  return mapped;
1000
996
  };
@@ -1031,18 +1027,21 @@ async function prepareCodexRemoteWorkspaceMessageMedia(params) {
1031
1027
  }
1032
1028
  if (attachmentEntries > REMOTE_WORKSPACE_MEDIA_MAX_ATTACHMENTS) throw new Error(`Codex remote workspace upload exceeds the ${REMOTE_WORKSPACE_MEDIA_MAX_ATTACHMENTS}-attachment limit.`);
1033
1029
  for (const managedPath of gatewayManagedPaths) await assertGatewayManagedMediaPath(managedPath, gatewayMediaRoot);
1034
- if (remotePathsByLocalPath.size === 0) return mappedArgs;
1030
+ if (remotePathsByLocalPath.size === 0) return {
1031
+ args: mappedArgs,
1032
+ sourcePathsByStagedPath
1033
+ };
1035
1034
  const readRemoteFile = params.readRemoteFile;
1036
1035
  if (!readRemoteFile) throw new Error("Codex remote workspace file transfer requires an active app-server client.");
1037
1036
  const maxBytes = params.maxBytes ?? REMOTE_WORKSPACE_MEDIA_MAX_BYTES;
1038
1037
  const timeoutMs = params.timeoutMs ?? REMOTE_WORKSPACE_MEDIA_TIMEOUT_MS;
1039
- const deadline = Date.now() + timeoutMs;
1038
+ const deadline = performance.now() + timeoutMs;
1040
1039
  const stagedPaths = /* @__PURE__ */ new Map();
1041
1040
  let totalBytes = 0;
1042
- for (const [localPath, remotePath] of remotePathsByLocalPath) {
1041
+ for (const [localPath, { remotePath, sourcePaths }] of remotePathsByLocalPath) {
1043
1042
  params.signal?.throwIfAborted();
1044
1043
  const remainingBytes = maxBytes - totalBytes;
1045
- const remainingMs = deadline - Date.now();
1044
+ const remainingMs = Math.floor(deadline - performance.now());
1046
1045
  if (remainingMs <= 0) throw new Error("Codex remote workspace attachment batch timed out.");
1047
1046
  const response = await readRemoteFile({
1048
1047
  path: remotePath,
@@ -1058,8 +1057,15 @@ async function prepareCodexRemoteWorkspaceMessageMedia(params) {
1058
1057
  totalBytes += remoteBuffer.byteLength;
1059
1058
  const saved = await saveMediaBuffer(remoteBuffer, void 0, "outbound", maxBytes, path.basename(remotePath));
1060
1059
  stagedPaths.set(localPath, saved.path);
1060
+ sourcePathsByStagedPath.set(normalizeMediaReferenceForComparison(saved.path), [...sourcePaths]);
1061
1061
  }
1062
- return mapMessageMediaValues(mappedArgs, (value) => stagedPaths.get(value) ?? value);
1062
+ return {
1063
+ args: mapMessageMediaValues(mappedArgs, (value) => stagedPaths.get(value) ?? value),
1064
+ sourcePathsByStagedPath
1065
+ };
1066
+ }
1067
+ function resolveCodexMediaSourceUrls(mediaUrls, sourcePathsByStagedPath) {
1068
+ return [...new Set(mediaUrls.flatMap((url) => [url, ...sourcePathsByStagedPath?.get(normalizeMediaReferenceForComparison(url)) ?? []]))];
1063
1069
  }
1064
1070
  async function assertGatewayManagedMediaPath(value, mediaRoot) {
1065
1071
  const opened = await (await root(mediaRoot, { symlinks: "reject" })).open(path.relative(mediaRoot, value), { symlinks: "reject" });
@@ -1091,6 +1097,209 @@ function mapMessageMediaValues(args, mapValue) {
1091
1097
  return mapped;
1092
1098
  }
1093
1099
  //#endregion
1100
+ //#region extensions/codex/src/app-server/native-process-authority.ts
1101
+ const clients = /* @__PURE__ */ new WeakMap();
1102
+ /** A physical client joins native call receipts to its concrete sandbox processes. */
1103
+ function getCodexNativeProcessClient(client) {
1104
+ let owner = clients.get(client);
1105
+ if (!owner) {
1106
+ owner = new CodexNativeProcessClient(client);
1107
+ clients.set(client, owner);
1108
+ }
1109
+ return owner;
1110
+ }
1111
+ function hasCodexNativeBackgroundProcesses(client, threadId) {
1112
+ return clients.get(client)?.hasProcesses(threadId) ?? false;
1113
+ }
1114
+ var CodexNativeProcessClient = class {
1115
+ constructor(client) {
1116
+ this.client = client;
1117
+ this.id = randomUUID();
1118
+ this.authPath = `/openclaw-${randomUUID()}`;
1119
+ this.threads = /* @__PURE__ */ new Map();
1120
+ this.closed = false;
1121
+ client.addNotificationHandler((notification) => {
1122
+ if (!isJsonObject(notification.params)) return;
1123
+ const threadId = readCodexNotificationThreadId(notification.params);
1124
+ const turnId = readCodexNotificationTurnId(notification.params);
1125
+ const commands = threadId ? this.threads.get(threadId) : void 0;
1126
+ if (!commands || !turnId) return;
1127
+ if (notification.method === "turn/completed") {
1128
+ for (const command of commands.values()) if (command.turnId === turnId) this.closeAdmission(command);
1129
+ } else if (notification.method === "item/completed") {
1130
+ const item = notification.params.item;
1131
+ const command = isJsonObject(item) && typeof item.id === "string" ? commands.get(item.id) : void 0;
1132
+ if (command?.turnId === turnId) this.closeAdmission(command);
1133
+ }
1134
+ });
1135
+ client.addCloseHandler(() => {
1136
+ this.closed = true;
1137
+ const owners = /* @__PURE__ */ new Set();
1138
+ for (const commands of this.threads.values()) for (const command of commands.values()) owners.add(command.owner);
1139
+ for (const owner of owners) owner.cancelClient(this);
1140
+ });
1141
+ }
1142
+ admit(owner, receipt, parentTurn, assertActive) {
1143
+ if (this.closed) throw new Error("Codex process source client is closed");
1144
+ let commands = this.threads.get(receipt.threadId);
1145
+ const existing = commands?.get(receipt.itemId);
1146
+ if (existing) {
1147
+ if (existing.owner === owner && existing.turnId === receipt.turnId && existing.accepting) return;
1148
+ throw new Error("Codex reused an unsettled native command identity");
1149
+ }
1150
+ if (!commands) {
1151
+ commands = /* @__PURE__ */ new Map();
1152
+ this.threads.set(receipt.threadId, commands);
1153
+ }
1154
+ const command = {
1155
+ ...receipt,
1156
+ owner,
1157
+ client: this,
1158
+ parentTurn,
1159
+ accepting: true,
1160
+ processes: /* @__PURE__ */ new Set(),
1161
+ assertActive,
1162
+ releaseClient: retainSharedCodexAppServerClientIfCurrent(this.client),
1163
+ releaseThread: protectCodexAppServerLiveThread(this.client, receipt.threadId)
1164
+ };
1165
+ commands.set(receipt.itemId, command);
1166
+ owner.commands.add(command);
1167
+ }
1168
+ hasProcesses(threadId) {
1169
+ return [...this.threads.get(threadId)?.values() ?? []].some((command) => command.processes.size > 0);
1170
+ }
1171
+ claim(metadata, terminate) {
1172
+ if (this.closed || !isJsonObject(metadata) || typeof metadata.threadId !== "string" || typeof metadata.toolCallId !== "string") throw new Error("Codex process start requires its admitted native command");
1173
+ const command = this.threads.get(metadata.threadId)?.get(metadata.toolCallId);
1174
+ if (!command) throw new Error("Codex process start has no admitted native command");
1175
+ const assertAdmission = () => {
1176
+ command.owner.assertCurrent();
1177
+ command.assertActive();
1178
+ if (this.closed || !command.accepting || this.threads.get(command.threadId)?.get(command.itemId) !== command) throw new Error("Codex native command admission has ended");
1179
+ };
1180
+ assertAdmission();
1181
+ const process = { terminate };
1182
+ command.processes.add(process);
1183
+ let settled = false;
1184
+ return {
1185
+ assertAdmission,
1186
+ assertCurrent: () => {
1187
+ command.owner.assertCurrent();
1188
+ if (this.closed || settled) throw new Error("Codex native process authority has ended");
1189
+ },
1190
+ settle: () => {
1191
+ if (settled) return;
1192
+ settled = true;
1193
+ command.processes.delete(process);
1194
+ this.forgetSettled(command);
1195
+ },
1196
+ fail: (error) => command.owner.reportSettlementFailure(error)
1197
+ };
1198
+ }
1199
+ closeAdmission(command) {
1200
+ command.accepting = false;
1201
+ this.forgetSettled(command);
1202
+ }
1203
+ forgetSettled(command) {
1204
+ if (command.accepting || command.processes.size > 0) return;
1205
+ const commands = this.threads.get(command.threadId);
1206
+ if (commands?.get(command.itemId) === command) {
1207
+ commands.delete(command.itemId);
1208
+ if (commands.size === 0) this.threads.delete(command.threadId);
1209
+ }
1210
+ command.owner.commands.delete(command);
1211
+ command.releaseThread();
1212
+ command.releaseClient?.();
1213
+ command.owner.releaseIfSettled();
1214
+ }
1215
+ };
1216
+ /** Original-source custody outlives foreground authority, never permitting new admission after release. */
1217
+ var CodexNativeProcessAuthority = class {
1218
+ constructor(host, onCleanupFailure) {
1219
+ this.onCleanupFailure = onCleanupFailure;
1220
+ this.commands = /* @__PURE__ */ new Set();
1221
+ this.holds = 1;
1222
+ this.cancelled = false;
1223
+ this.released = false;
1224
+ this.onAbort = () => {
1225
+ if (!this.cancelled) this.cancel().catch(this.onCleanupFailure);
1226
+ };
1227
+ this.source = host.retainSourceAuthority?.();
1228
+ this.source?.signal?.addEventListener("abort", this.onAbort, { once: true });
1229
+ if (this.source?.signal?.aborted) this.onAbort();
1230
+ }
1231
+ assertCurrent() {
1232
+ if (this.cancelled || this.released) throw new Error("Codex native process source has ended");
1233
+ try {
1234
+ this.source?.assertCurrent();
1235
+ if (this.cancelled || this.released) throw new Error("Codex native process source has ended");
1236
+ } catch (error) {
1237
+ this.onAbort();
1238
+ throw error;
1239
+ }
1240
+ }
1241
+ bindTurn(client, threadId, turnId) {
1242
+ this.parentTurn = {
1243
+ client,
1244
+ threadId,
1245
+ turnId
1246
+ };
1247
+ }
1248
+ admit(client, receipt, assertAdmissionCurrent, childParentThreadId) {
1249
+ this.assertCurrent();
1250
+ if (this.holds === 0) throw new Error("Codex native process admission is closed");
1251
+ const parent = this.parentTurn;
1252
+ if (parent?.client !== client || (childParentThreadId ? childParentThreadId !== parent.threadId : receipt.threadId !== parent.threadId || receipt.turnId !== parent.turnId)) throw new Error("Codex native command does not belong to its admitted turn");
1253
+ assertAdmissionCurrent();
1254
+ getCodexNativeProcessClient(client).admit(this, receipt, parent, assertAdmissionCurrent);
1255
+ }
1256
+ retainAdmission() {
1257
+ this.assertCurrent();
1258
+ this.holds += 1;
1259
+ let held = true;
1260
+ return () => {
1261
+ if (held) {
1262
+ held = false;
1263
+ this.release();
1264
+ }
1265
+ };
1266
+ }
1267
+ release() {
1268
+ if (this.holds === 0) return;
1269
+ this.holds -= 1;
1270
+ if (this.holds === 0) for (const command of this.commands) command.client.closeAdmission(command);
1271
+ this.releaseIfSettled();
1272
+ }
1273
+ releaseIfSettled() {
1274
+ if (this.released || this.holds > 0 || this.commands.size > 0) return;
1275
+ this.released = true;
1276
+ this.source?.signal?.removeEventListener("abort", this.onAbort);
1277
+ this.source?.release();
1278
+ }
1279
+ cancelTurn(client, threadId, turnId) {
1280
+ const owner = getCodexNativeProcessClient(client);
1281
+ return this.terminate([...this.commands].filter((command) => command.client === owner && command.parentTurn.threadId === threadId && command.parentTurn.turnId === turnId));
1282
+ }
1283
+ cancelClient(client) {
1284
+ this.terminate([...this.commands].filter((command) => command.client === client)).catch(this.onCleanupFailure);
1285
+ }
1286
+ reportSettlementFailure(error) {
1287
+ this.onCleanupFailure(new AggregateError([error], "Codex native process cleanup failed; background work remains unsettled"));
1288
+ }
1289
+ cancel() {
1290
+ this.cancelled = true;
1291
+ return this.cancellation ??= this.terminate([...this.commands]);
1292
+ }
1293
+ async terminate(commands) {
1294
+ const processes = commands.flatMap((command) => {
1295
+ command.client.closeAdmission(command);
1296
+ return [...command.processes];
1297
+ });
1298
+ const failures = (await Promise.allSettled(processes.map((process) => process.terminate()))).flatMap((result) => result.status === "rejected" ? [result.reason] : []);
1299
+ if (failures.length > 0) throw new AggregateError(failures, "Codex native process cleanup failed; background work remains unsettled");
1300
+ }
1301
+ };
1302
+ //#endregion
1094
1303
  //#region extensions/codex/src/app-server/sandbox-exec-server-node-relay.ts
1095
1304
  const CODEX_NODE_EXEC_SERVER_MAX_MESSAGE_BYTES$1 = 67108864;
1096
1305
  const CODEX_NODE_EXEC_SERVER_MAX_FAILURE_DETAIL_CHARS = 240;
@@ -1855,32 +2064,50 @@ async function readFile(execServer, params) {
1855
2064
  async function writeFile(execServer, params) {
1856
2065
  const record = requireObject(params, "fs/writeFile params");
1857
2066
  const filePath = resolveExecServerPath(requireString(record.path, "path"), "write path");
2067
+ const fsBridge = execServer.fsBridge;
2068
+ const canonicalDestination = await fsBridge.resolvePinnedMutationTarget?.({
2069
+ filePath,
2070
+ action: "write"
2071
+ });
1858
2072
  assertFsSandboxAccess(execServer, record, [{
1859
2073
  path: filePath,
1860
2074
  access: "write"
1861
- }]);
1862
- const fsBridge = execServer.fsBridge;
2075
+ }, ...canonicalDestination ? [{
2076
+ path: canonicalDestination.policyPath,
2077
+ access: "write"
2078
+ }] : []]);
1863
2079
  if ((await fsBridge.stat({ filePath: posix.dirname(filePath) }))?.type !== "directory") throw new JsonRpcProtocolError(JSON_RPC_NOT_FOUND, "parent directory not found");
1864
2080
  await fsBridge.writeFile({
1865
2081
  filePath,
1866
2082
  data: Buffer.from(requireBase64String(record.dataBase64, "dataBase64"), "base64"),
1867
- mkdir: false
2083
+ mkdir: false,
2084
+ pinnedPath: canonicalDestination?.pinnedPath
1868
2085
  });
1869
2086
  }
1870
2087
  /** Creates a sandbox directory, respecting recursive and parent-directory semantics. */
1871
2088
  async function createDirectory(execServer, params) {
1872
2089
  const record = requireObject(params, "fs/createDirectory params");
1873
2090
  const filePath = resolveExecServerPath(requireString(record.path, "path"), "create-directory path");
2091
+ const fsBridge = execServer.fsBridge;
2092
+ const canonicalDestination = await fsBridge.resolvePinnedMutationTarget?.({
2093
+ filePath,
2094
+ action: "mkdir"
2095
+ });
1874
2096
  assertFsSandboxAccess(execServer, record, [{
1875
2097
  path: filePath,
1876
2098
  access: "write"
1877
- }]);
1878
- const fsBridge = execServer.fsBridge;
2099
+ }, ...canonicalDestination ? [{
2100
+ path: canonicalDestination.policyPath,
2101
+ access: "write"
2102
+ }] : []]);
1879
2103
  if (record.recursive === false) {
1880
2104
  const parentPath = posix.dirname(filePath);
1881
2105
  if ((await fsBridge.stat({ filePath: parentPath }))?.type !== "directory") throw new JsonRpcProtocolError(JSON_RPC_NOT_FOUND, "parent directory not found");
1882
2106
  }
1883
- await fsBridge.mkdirp({ filePath });
2107
+ await fsBridge.mkdirp({
2108
+ filePath,
2109
+ pinnedPath: canonicalDestination?.pinnedPath
2110
+ });
1884
2111
  }
1885
2112
  /** Returns normalized metadata for a sandbox path. */
1886
2113
  async function getMetadata(execServer, params) {
@@ -1929,15 +2156,26 @@ async function removePath(execServer, params) {
1929
2156
  const record = requireObject(params, "fs/remove params");
1930
2157
  const filePath = resolveExecServerPath(requireString(record.path, "path"), "remove path");
1931
2158
  const fsSandboxPolicy = resolveFsSandboxPolicy(execServer, record);
2159
+ const canonicalDestination = await execServer.fsBridge.resolvePinnedMutationTarget?.({
2160
+ filePath,
2161
+ action: "remove"
2162
+ });
1932
2163
  assertResolvedFsSandboxAccess(fsSandboxPolicy, [{
1933
2164
  path: filePath,
1934
2165
  access: "write"
1935
- }]);
1936
- if (record.recursive !== false) assertNoReadOnlyDescendant(fsSandboxPolicy, filePath, "remove");
2166
+ }, ...canonicalDestination ? [{
2167
+ path: canonicalDestination.policyPath,
2168
+ access: "write"
2169
+ }] : []]);
2170
+ if (record.recursive !== false) {
2171
+ assertNoReadOnlyDescendant(fsSandboxPolicy, filePath, "remove");
2172
+ if (canonicalDestination) assertNoReadOnlyDescendant(fsSandboxPolicy, canonicalDestination.policyPath, "remove");
2173
+ }
1937
2174
  await execServer.fsBridge.remove({
1938
2175
  filePath,
1939
2176
  recursive: record.recursive !== false,
1940
- force: record.force !== false
2177
+ force: record.force !== false,
2178
+ pinnedPath: canonicalDestination?.pinnedPath
1941
2179
  });
1942
2180
  }
1943
2181
  /** Copies sandbox files or recursive directories while enforcing source and destination policy. */
@@ -1971,10 +2209,25 @@ async function copySandboxPath(execServer, params) {
1971
2209
  }]);
1972
2210
  const sourceStat = await fsBridge.stat({ filePath: params.sourcePath });
1973
2211
  if (!sourceStat) throw new JsonRpcProtocolError(JSON_RPC_NOT_FOUND, "file not found");
1974
- if (sourceStat?.type === "directory") {
2212
+ const canonicalDestination = await fsBridge.resolvePinnedMutationTarget?.({
2213
+ filePath: params.destinationPath,
2214
+ action: sourceStat.type === "directory" ? "mkdir" : "copy-destination"
2215
+ });
2216
+ const canonicalPolicyEntries = canonicalDestination ? [{
2217
+ path: canonicalDestination.policyPath,
2218
+ access: "write"
2219
+ }] : [];
2220
+ assertResolvedFsSandboxAccess(params.fsSandboxPolicy, canonicalPolicyEntries);
2221
+ if (sourceStat.type === "directory") {
1975
2222
  if (!params.recursive) throw new Error(`Cannot copy directory without recursive=true: ${params.sourcePath}`);
1976
- if (pathContains(normalizeSandboxAbsolutePath(params.sourcePath, "copy source path"), normalizeSandboxAbsolutePath(params.destinationPath, "copy destination path"))) throw new Error("Cannot recursively copy a directory into itself.");
1977
- await fsBridge.mkdirp({ filePath: params.destinationPath });
2223
+ if (pathContains(normalizeSandboxAbsolutePath((await fsBridge.resolvePinnedMutationTarget?.({
2224
+ filePath: params.sourcePath,
2225
+ action: "mkdir"
2226
+ }))?.policyPath ?? params.sourcePath, "copy source path"), normalizeSandboxAbsolutePath(canonicalDestination?.policyPath ?? params.destinationPath, "copy destination path"))) throw new Error("Cannot recursively copy a directory into itself.");
2227
+ await fsBridge.mkdirp({
2228
+ filePath: params.destinationPath,
2229
+ pinnedPath: canonicalDestination?.pinnedPath
2230
+ });
1978
2231
  for (const entry of await listDirectoryEntries(execServer, params.sourcePath, params.fsSandboxPolicy)) {
1979
2232
  if (!entry.isDirectory && !entry.isFile) throw new Error(`Cannot copy unsupported filesystem entry: ${entry.fileName}`);
1980
2233
  await copySandboxPath(execServer, {
@@ -1990,7 +2243,8 @@ async function copySandboxPath(execServer, params) {
1990
2243
  await fsBridge.copyFile({
1991
2244
  sourcePath: params.sourcePath,
1992
2245
  destinationPath: params.destinationPath,
1993
- mkdir: true
2246
+ mkdir: true,
2247
+ pinnedPath: canonicalDestination?.pinnedPath
1994
2248
  });
1995
2249
  return;
1996
2250
  }
@@ -2002,7 +2256,8 @@ async function copySandboxPath(execServer, params) {
2002
2256
  await fsBridge.writeFile({
2003
2257
  filePath: params.destinationPath,
2004
2258
  data,
2005
- mkdir: true
2259
+ mkdir: true,
2260
+ pinnedPath: canonicalDestination?.pinnedPath
2006
2261
  });
2007
2262
  }
2008
2263
  function assertSandboxFileReadWithinLimit(stat) {
@@ -2023,7 +2278,7 @@ function metadataResponse(stat) {
2023
2278
  /** Owns one sandbox subprocess tree through close, reaping, and backend finalization. */
2024
2279
  const SANDBOX_CHILD_TERM_GRACE_MS = 1e3;
2025
2280
  const SANDBOX_CHILD_REAP_TIMEOUT_MS = 4500;
2026
- const SANDBOX_EXEC_MARKER = "CODEX_SANDBOX_EXEC_ID";
2281
+ const SANDBOX_CHILD_INTERRUPT_POLL_MS = 50;
2027
2282
  async function spawnSandboxChild(params) {
2028
2283
  const [command, ...args] = params.argv;
2029
2284
  const finalize = async (status, exitCode) => await params.finalizeExec?.({
@@ -2037,54 +2292,88 @@ async function spawnSandboxChild(params) {
2037
2292
  throw new Error("OpenClaw sandbox exec spec did not provide a command.");
2038
2293
  }
2039
2294
  let child;
2040
- try {
2041
- child = spawn(command, args, {
2042
- detached: process.platform !== "win32",
2043
- env: params.env,
2044
- stdio: [
2045
- "pipe",
2046
- "pipe",
2047
- "pipe"
2048
- ]
2049
- });
2050
- } catch (error) {
2051
- await finalize("failed", null).catch(params.onFinalizeError);
2052
- throw error;
2053
- }
2295
+ let pty;
2296
+ let exitOutcome;
2054
2297
  let outcome;
2055
- const closed = new Promise((resolve) => {
2056
- child.once("close", (code, signal) => resolve(outcome = {
2057
- exitCode: code ?? 1,
2298
+ let escalation;
2299
+ const ready = createDeferred();
2300
+ const exited = createDeferred();
2301
+ const closed = createDeferred();
2302
+ const recordExit = (exitCode, signal) => {
2303
+ if (!exitOutcome) {
2304
+ clearTimeout(escalation);
2305
+ exited.resolve(exitOutcome = {
2306
+ exitCode,
2307
+ signal
2308
+ });
2309
+ }
2310
+ };
2311
+ const recordClose = (exitCode, signal) => {
2312
+ recordExit(exitCode, signal);
2313
+ closed.resolve(outcome = {
2314
+ exitCode,
2058
2315
  signal
2059
- }));
2060
- });
2061
- let finalizePromise;
2316
+ });
2317
+ };
2318
+ let startFailed = false;
2319
+ let startupPending = true;
2320
+ let terminationRequested = false;
2062
2321
  let terminationCleanup;
2063
2322
  let terminationError;
2064
- const settled = closed.then(async (result) => {
2323
+ let settlementStarted = false;
2324
+ const interruptions = /* @__PURE__ */ new Set();
2325
+ const localSignals = [];
2326
+ const settled = closed.promise.then(async (result) => {
2327
+ settlementStarted = true;
2065
2328
  await terminationCleanup;
2066
- child.stdin.destroy();
2067
- await (finalizePromise ??= finalize(params.finalizeStatus(result), result.exitCode));
2329
+ if (interruptions.size > 0) await Promise.allSettled(interruptions);
2330
+ await Promise.all(localSignals);
2331
+ child?.stdin?.destroy();
2332
+ await finalize(startFailed ? "failed" : params.finalizeStatus(result), startFailed ? null : result.exitCode);
2068
2333
  return result;
2069
2334
  });
2070
2335
  settled.catch(params.onFinalizeError);
2336
+ const assertCurrent = () => {
2337
+ params.assertCurrent?.();
2338
+ if (terminationRequested) throw new Error("Sandbox child process start cancelled");
2339
+ };
2071
2340
  let terminationPromise;
2072
2341
  const owner = {
2073
- process: child,
2342
+ assertCurrent,
2343
+ exited: exited.promise,
2344
+ closed: closed.promise,
2074
2345
  settled,
2075
2346
  terminate: () => terminationPromise ??= (async () => {
2076
- child.stdin.destroy();
2347
+ terminationRequested = true;
2348
+ if (startupPending) await ready.promise;
2349
+ if (startFailed || settlementStarted) return await settled;
2350
+ child?.stdin?.destroy();
2077
2351
  terminationCleanup = params.terminateRemote?.().catch((error) => {
2078
2352
  terminationError = error instanceof Error ? error : new Error(String(error));
2079
2353
  });
2080
2354
  await terminationCleanup;
2081
2355
  if (!outcome) {
2082
- if (child.pid) killProcessTree(child.pid, {
2083
- detached: process.platform !== "win32",
2084
- graceMs: SANDBOX_CHILD_TERM_GRACE_MS
2085
- });
2086
- else child.kill("SIGTERM");
2087
- if (!await Promise.race([closed.then(() => true), delay(SANDBOX_CHILD_REAP_TIMEOUT_MS).then(() => false)])) throw new Error(`Sandbox child process tree ${child.pid ?? "unknown"} survived SIGKILL; tear down the sandbox environment and inspect the surviving process tree before retrying.`);
2356
+ const signalLocal = (signal) => {
2357
+ if (exitOutcome) return;
2358
+ if (pty) pty.kill(signal);
2359
+ else if (child?.pid) {
2360
+ const pid = child.pid;
2361
+ localSignals.push(new Promise((resolve) => {
2362
+ signalProcessTree(pid, signal, {
2363
+ detached: process.platform !== "win32",
2364
+ onComplete: resolve
2365
+ });
2366
+ }));
2367
+ } else child?.kill(signal);
2368
+ };
2369
+ signalLocal("SIGTERM");
2370
+ if (!exitOutcome) {
2371
+ escalation = setTimeout(() => signalLocal("SIGKILL"), SANDBOX_CHILD_TERM_GRACE_MS);
2372
+ escalation.unref?.();
2373
+ }
2374
+ const reaped = await Promise.race([closed.promise.then(() => true), delay(SANDBOX_CHILD_REAP_TIMEOUT_MS).then(() => false)]).finally(() => clearTimeout(escalation));
2375
+ await Promise.all(localSignals);
2376
+ if (!reaped) throw new Error(`Sandbox child process tree ${pty?.pid ?? child?.pid ?? "unknown"} survived SIGKILL; tear down the sandbox environment and inspect the surviving process tree before retrying.`);
2088
2377
  }
2089
2378
  const result = await settled;
2090
2379
  if (terminationError) throw terminationError;
@@ -2093,46 +2382,85 @@ async function spawnSandboxChild(params) {
2093
2382
  };
2094
2383
  params.owners.add(owner);
2095
2384
  settled.then(() => params.owners.delete(owner), () => params.owners.delete(owner));
2096
- return owner;
2097
- }
2098
- function prepareSandboxChildExec(backend, env) {
2099
- const marker = randomUUID();
2100
- return {
2101
- env: {
2102
- ...env,
2103
- [SANDBOX_EXEC_MARKER]: marker
2104
- },
2105
- terminate: async () => {
2106
- const result = await backend.runShellCommand({
2107
- script: SANDBOX_REMOTE_TERMINATE_SCRIPT,
2108
- args: [`${SANDBOX_EXEC_MARKER}=${marker}`],
2109
- allowFailure: true,
2110
- signal: AbortSignal.timeout(SANDBOX_CHILD_REAP_TIMEOUT_MS)
2111
- });
2112
- if (result.code !== 0) {
2113
- const detail = result.stderr.toString("utf8").trim() || result.stdout.toString("utf8").trim();
2114
- throw new Error(detail || `Sandbox process tree cleanup failed with code ${result.code}; tear down the sandbox environment and inspect surviving processes before retrying.`);
2385
+ const interrupt = async () => {
2386
+ await ready.promise;
2387
+ const interruptRemote = params.interruptRemote;
2388
+ if (exitOutcome || terminationRequested || !interruptRemote) return;
2389
+ const interruption = (async () => {
2390
+ const deadline = performance.now() + SANDBOX_CHILD_REAP_TIMEOUT_MS;
2391
+ while (true) {
2392
+ if (exitOutcome || terminationRequested) return;
2393
+ const remainingMs = Math.ceil(deadline - performance.now());
2394
+ if (remainingMs <= 0) throw new Error("Sandbox process interrupt timed out waiting for remote process admission");
2395
+ assertCurrent();
2396
+ if (await interruptRemote(remainingMs)) return;
2397
+ await Promise.race([delay(Math.min(SANDBOX_CHILD_INTERRUPT_POLL_MS, Math.max(0, deadline - performance.now()))), exited.promise]);
2115
2398
  }
2399
+ })();
2400
+ interruptions.add(interruption);
2401
+ try {
2402
+ await interruption;
2403
+ } finally {
2404
+ interruptions.delete(interruption);
2116
2405
  }
2117
2406
  };
2407
+ try {
2408
+ if (params.usePty) {
2409
+ const env = Object.fromEntries(Object.entries(params.env).filter((entry) => entry[1] !== void 0));
2410
+ pty = await spawnTerminalPty({
2411
+ file: command,
2412
+ args,
2413
+ cwd: params.cwd,
2414
+ env,
2415
+ cols: 80,
2416
+ rows: 24
2417
+ }, { assertCurrent });
2418
+ pty.onExit(({ exitCode, signal }) => {
2419
+ recordClose(signal ? 1 : exitCode, signal || null);
2420
+ });
2421
+ return {
2422
+ ...owner,
2423
+ pty,
2424
+ interrupt
2425
+ };
2426
+ }
2427
+ assertCurrent();
2428
+ child = spawn(command, args, {
2429
+ detached: process.platform !== "win32",
2430
+ env: params.env,
2431
+ cwd: params.cwd,
2432
+ stdio: [
2433
+ "pipe",
2434
+ "pipe",
2435
+ "pipe"
2436
+ ]
2437
+ });
2438
+ child.once("exit", (code, signal) => recordExit(code ?? 1, signal));
2439
+ child.once("close", (code, signal) => recordClose(code ?? 1, signal));
2440
+ const markStartFailed = () => {
2441
+ startFailed = true;
2442
+ };
2443
+ child.once("error", markStartFailed);
2444
+ try {
2445
+ await once(child, "spawn");
2446
+ } finally {
2447
+ child.off("error", markStartFailed);
2448
+ }
2449
+ return {
2450
+ ...owner,
2451
+ process: child,
2452
+ interrupt
2453
+ };
2454
+ } catch (error) {
2455
+ startFailed = true;
2456
+ if (!child) recordClose(1, null);
2457
+ await settled.catch(() => void 0);
2458
+ throw error;
2459
+ } finally {
2460
+ startupPending = false;
2461
+ ready.resolve();
2462
+ }
2118
2463
  }
2119
- const SANDBOX_REMOTE_TERMINATE_SCRIPT = String.raw`
2120
- find_owned_pids() {
2121
- for env_file in /proc/[0-9]*/environ; do
2122
- if [ -r "$env_file" ] && tr '\0' '\n' < "$env_file" 2>/dev/null | grep -Fqx "$1"; then
2123
- basename "$(dirname "$env_file")"
2124
- fi
2125
- done
2126
- }
2127
- owned="$(find_owned_pids "$1")"
2128
- [ -z "$owned" ] || kill -TERM $owned 2>/dev/null || true
2129
- sleep 1
2130
- owned="$(find_owned_pids "$1")"
2131
- [ -z "$owned" ] || kill -KILL $owned 2>/dev/null || true
2132
- sleep 1
2133
- owned="$(find_owned_pids "$1")"
2134
- [ -z "$owned" ] || { echo "Sandbox process IDs survived SIGKILL: $owned" >&2; exit 1; }
2135
- `.trim();
2136
2464
  function delay(ms) {
2137
2465
  return new Promise((resolve) => {
2138
2466
  setTimeout(resolve, ms).unref?.();
@@ -2147,7 +2475,7 @@ function delay(ms) {
2147
2475
  /** Maximum JSON-line size accepted from the streaming HTTP helper process. */
2148
2476
  const SANDBOX_HTTP_STREAM_LINE_MAX_CHARS = 262144;
2149
2477
  /** Handles one sandbox HTTP JSON-RPC request, optionally streaming response body deltas. */
2150
- async function httpRequest(execServer, notifications, params) {
2478
+ async function httpRequest(execServer, notifications, params, operations) {
2151
2479
  const record = requireObject(params, "http/request params");
2152
2480
  const requestId = requireString(record.requestId, "requestId");
2153
2481
  const url = requireString(record.url, "url");
@@ -2163,11 +2491,14 @@ async function httpRequest(execServer, notifications, params) {
2163
2491
  redirectPolicy,
2164
2492
  streamResponse: record.streamResponse === true
2165
2493
  };
2166
- if (request.streamResponse) return await runStreamingSandboxHttpRequest(execServer, notifications, requestId, request);
2167
- return await runSandboxHttpRequest(execServer, {
2168
- ...request,
2169
- streamResponse: false
2494
+ const response = createDeferred();
2495
+ const operation = runSandboxHttpRequest(execServer, notifications, requestId, request, response);
2496
+ operations.add(operation);
2497
+ operation.then(() => operations.delete(operation), (error) => {
2498
+ operations.delete(operation);
2499
+ response.reject(error);
2170
2500
  });
2501
+ return await response.promise;
2171
2502
  }
2172
2503
  function assertSandboxHttpRequestTargetAllowed(url) {
2173
2504
  let parsed;
@@ -2179,50 +2510,54 @@ function assertSandboxHttpRequestTargetAllowed(url) {
2179
2510
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new SsrFBlockedError(`Blocked non-HTTP(S) protocol in sandbox http/request: ${parsed.protocol}`);
2180
2511
  if (isBlockedHostnameOrIp(parsed.hostname)) throw new SsrFBlockedError(`Blocked hostname or private/internal IP in sandbox http/request: ${parsed.hostname}`);
2181
2512
  }
2182
- async function runSandboxHttpRequest(execServer, params) {
2183
- const result = await execServer.backend.runShellCommand({
2184
- script: SANDBOX_HTTP_REQUEST_SCRIPT,
2185
- stdin: JSON.stringify(params),
2186
- allowFailure: true
2187
- });
2188
- if (result.code !== 0) {
2189
- const stderr = result.stderr.toString("utf8").trim();
2190
- throw new Error(stderr || `sandbox http/request failed with code ${result.code}`);
2191
- }
2192
- const parsed = JSON.parse(result.stdout.toString("utf8"));
2193
- if (typeof parsed.status !== "number" || !Array.isArray(parsed.headers)) throw new Error("sandbox http/request returned an invalid response envelope");
2194
- return {
2195
- status: parsed.status,
2196
- headers: readHttpHeaders(parsed.headers),
2197
- bodyBase64: typeof parsed.bodyBase64 === "string" ? parsed.bodyBase64 : ""
2198
- };
2199
- }
2200
- async function runStreamingSandboxHttpRequest(execServer, notifications, requestId, params) {
2201
- const backend = execServer.backend;
2202
- const remoteExec = prepareSandboxChildExec(backend, {});
2203
- const execSpec = await backend.buildExecSpec({
2204
- command: SANDBOX_HTTP_REQUEST_SCRIPT,
2205
- workdir: execServer.sandbox.containerWorkdir,
2206
- env: remoteExec.env,
2207
- usePty: false
2208
- });
2513
+ async function runSandboxHttpRequest(execServer, notifications, requestId, params, response) {
2209
2514
  const lifecycle = { failed: false };
2210
- const owner = await spawnSandboxChild({
2211
- argv: execSpec.argv,
2212
- env: execSpec.env,
2213
- finalizeExec: backend.finalizeExec,
2214
- finalizeToken: execSpec.finalizeToken,
2215
- finalizeStatus: (outcome) => lifecycle.failed || outcome.exitCode !== 0 ? "failed" : "completed",
2216
- onFinalizeError: (error) => {
2217
- embeddedAgentLog.warn("codex sandbox http/request finalize failed", { error });
2218
- },
2219
- owners: execServer.children,
2220
- terminateRemote: remoteExec.terminate
2221
- });
2515
+ let owner;
2516
+ try {
2517
+ notifications.signal.throwIfAborted();
2518
+ const backend = execServer.backend;
2519
+ const remoteExec = prepareSandboxProcessCleanup(backend, {});
2520
+ const execSpec = await backend.buildExecSpec({
2521
+ command: SANDBOX_HTTP_REQUEST_SCRIPT,
2522
+ workdir: execServer.sandbox.containerWorkdir,
2523
+ env: remoteExec.env,
2524
+ usePty: false
2525
+ });
2526
+ owner = await spawnSandboxChild({
2527
+ argv: execSpec.argv,
2528
+ env: execSpec.env,
2529
+ cwd: execSpec.cwd,
2530
+ assertCurrent: () => {
2531
+ notifications.signal.throwIfAborted();
2532
+ execSpec.assertCurrent?.();
2533
+ },
2534
+ finalizeExec: backend.finalizeExec,
2535
+ finalizeToken: execSpec.finalizeToken,
2536
+ finalizeStatus: (outcome) => lifecycle.failed || outcome.exitCode !== 0 ? "failed" : "completed",
2537
+ onFinalizeError: (error) => {
2538
+ embeddedAgentLog.warn("codex sandbox http/request finalize failed", { error });
2539
+ },
2540
+ owners: execServer.children,
2541
+ terminateRemote: remoteExec.terminate
2542
+ });
2543
+ } catch (error) {
2544
+ response.reject(error);
2545
+ return;
2546
+ }
2222
2547
  const child = owner.process;
2548
+ const completion = createDeferred();
2549
+ owner.settled.then(() => completion.resolve(), completion.reject);
2550
+ let termination;
2551
+ const terminate = () => {
2552
+ if (!termination) {
2553
+ termination = owner.terminate().then(() => void 0);
2554
+ termination.then(completion.resolve, completion.reject);
2555
+ }
2556
+ return termination;
2557
+ };
2223
2558
  const abortOnSessionClose = () => {
2224
2559
  lifecycle.failed = true;
2225
- owner.terminate().catch((error) => {
2560
+ terminate().catch((error) => {
2226
2561
  embeddedAgentLog.warn("codex sandbox http/request cleanup failed", { error });
2227
2562
  });
2228
2563
  };
@@ -2230,21 +2565,38 @@ async function runStreamingSandboxHttpRequest(execServer, notifications, request
2230
2565
  child.once("close", () => {
2231
2566
  notifications.signal.removeEventListener("abort", abortOnSessionClose);
2232
2567
  });
2233
- if (notifications.signal.aborted) abortOnSessionClose();
2234
2568
  child.stdin.on("error", (error) => {
2235
2569
  if (error.code === "EPIPE" || error.code === "ERR_STREAM_DESTROYED") return;
2236
2570
  embeddedAgentLog.warn("codex sandbox http/request stdin write failed", { error });
2237
2571
  });
2238
- child.stdin.end(JSON.stringify(params));
2239
- return await readStreamingSandboxHttpResponse({
2572
+ readSandboxHttpResponse({
2240
2573
  child,
2241
2574
  lifecycle,
2242
- owner,
2575
+ terminate,
2243
2576
  requestId,
2244
- notifications
2245
- });
2577
+ notifications,
2578
+ streamResponse: params.streamResponse
2579
+ }).then(response.resolve, response.reject);
2580
+ try {
2581
+ if (notifications.signal.aborted) abortOnSessionClose();
2582
+ else {
2583
+ owner.assertCurrent();
2584
+ child.stdin.end(JSON.stringify(params));
2585
+ }
2586
+ await completion.promise;
2587
+ await termination;
2588
+ } catch (error) {
2589
+ lifecycle.failed = true;
2590
+ response.reject(error);
2591
+ await terminate().catch((cleanupError) => {
2592
+ embeddedAgentLog.warn("codex sandbox http/request cleanup failed", { error: cleanupError });
2593
+ });
2594
+ throw error;
2595
+ } finally {
2596
+ notifications.signal.removeEventListener("abort", abortOnSessionClose);
2597
+ }
2246
2598
  }
2247
- function readStreamingSandboxHttpResponse(params) {
2599
+ function readSandboxHttpResponse(params) {
2248
2600
  return new Promise((resolve, reject) => {
2249
2601
  let headerResolved = false;
2250
2602
  let failed = false;
@@ -2252,11 +2604,21 @@ function readStreamingSandboxHttpResponse(params) {
2252
2604
  let lastBodySeq = 0;
2253
2605
  let stdoutBuffer = "";
2254
2606
  let stderr = "";
2255
- const fail = (message, _exitCode) => {
2607
+ const buffered = {
2608
+ stdout: {
2609
+ chunks: [],
2610
+ bytes: 0
2611
+ },
2612
+ stderr: {
2613
+ chunks: [],
2614
+ bytes: 0
2615
+ }
2616
+ };
2617
+ const fail = (message) => {
2256
2618
  if (failed) return;
2257
2619
  failed = true;
2258
2620
  params.lifecycle.failed = true;
2259
- params.owner.terminate().catch((error) => {
2621
+ params.terminate().catch((error) => {
2260
2622
  embeddedAgentLog.warn("codex sandbox http/request cleanup failed", { error });
2261
2623
  });
2262
2624
  if (headerResolved) {
@@ -2271,9 +2633,28 @@ function readStreamingSandboxHttpResponse(params) {
2271
2633
  }
2272
2634
  reject(new Error(message));
2273
2635
  };
2274
- params.child.stdout.setEncoding("utf8");
2636
+ const bufferOutput = (stream, chunk) => {
2637
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
2638
+ const output = buffered[stream];
2639
+ output.bytes += buffer.byteLength;
2640
+ if (output.bytes > SANDBOX_COMMAND_MAX_BUFFER_BYTES) {
2641
+ fail(`sandbox http/request ${stream} exceeded ${SANDBOX_COMMAND_MAX_BUFFER_BYTES} bytes`);
2642
+ return;
2643
+ }
2644
+ output.chunks.push(buffer);
2645
+ };
2646
+ if (params.streamResponse) {
2647
+ params.child.stdout.setEncoding("utf8");
2648
+ params.child.stderr.setEncoding("utf8");
2649
+ }
2275
2650
  params.child.stdout.on("data", (chunk) => {
2276
- stdoutBuffer += chunk;
2651
+ if (failed) return;
2652
+ if (!params.streamResponse) {
2653
+ bufferOutput("stdout", chunk);
2654
+ return;
2655
+ }
2656
+ const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
2657
+ stdoutBuffer += text;
2277
2658
  let newline = stdoutBuffer.indexOf("\n");
2278
2659
  while (newline >= 0) {
2279
2660
  const line = stdoutBuffer.slice(0, newline).trim();
@@ -2300,15 +2681,20 @@ function readStreamingSandboxHttpResponse(params) {
2300
2681
  });
2301
2682
  }
2302
2683
  } catch (error) {
2303
- fail(error instanceof Error ? error.message : String(error), null);
2684
+ fail(error instanceof Error ? error.message : String(error));
2304
2685
  }
2305
2686
  newline = stdoutBuffer.indexOf("\n");
2306
2687
  }
2307
- if (stdoutBuffer.length > SANDBOX_HTTP_STREAM_LINE_MAX_CHARS) fail(`sandbox http/request produced an unterminated stdout line longer than ${SANDBOX_HTTP_STREAM_LINE_MAX_CHARS} characters`, null);
2688
+ if (stdoutBuffer.length > SANDBOX_HTTP_STREAM_LINE_MAX_CHARS) fail(`sandbox http/request produced an unterminated stdout line longer than ${SANDBOX_HTTP_STREAM_LINE_MAX_CHARS} characters`);
2308
2689
  });
2309
- params.child.stderr.setEncoding("utf8");
2310
2690
  params.child.stderr.on("data", (chunk) => {
2311
- stderr = sliceUtf16Safe(`${stderr}${chunk}`, -4096);
2691
+ if (failed) return;
2692
+ if (!params.streamResponse) {
2693
+ bufferOutput("stderr", chunk);
2694
+ return;
2695
+ }
2696
+ const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
2697
+ stderr = sliceUtf16Safe(`${stderr}${text}`, -4096);
2312
2698
  });
2313
2699
  params.child.once("error", (error) => {
2314
2700
  childFailure ??= error.message;
@@ -2318,17 +2704,32 @@ function readStreamingSandboxHttpResponse(params) {
2318
2704
  const exitCode = code ?? 1;
2319
2705
  if (failed) return;
2320
2706
  if (childFailure) {
2321
- fail(childFailure, exitCode);
2707
+ fail(childFailure);
2322
2708
  return;
2323
2709
  }
2324
2710
  if (exitCode === 0) {
2711
+ if (!params.streamResponse) {
2712
+ try {
2713
+ const parsed = JSON.parse(Buffer.concat(buffered.stdout.chunks).toString("utf8"));
2714
+ if (typeof parsed.status !== "number" || !Array.isArray(parsed.headers)) throw new Error("sandbox http/request returned an invalid response envelope");
2715
+ resolve({
2716
+ status: parsed.status,
2717
+ headers: readHttpHeaders(parsed.headers),
2718
+ bodyBase64: typeof parsed.bodyBase64 === "string" ? parsed.bodyBase64 : ""
2719
+ });
2720
+ } catch (error) {
2721
+ fail(error instanceof Error ? error.message : String(error));
2722
+ }
2723
+ return;
2724
+ }
2325
2725
  if (!headerResolved) {
2326
2726
  params.lifecycle.failed = true;
2327
2727
  reject(/* @__PURE__ */ new Error("sandbox http/request exited before returning headers"));
2328
2728
  }
2329
2729
  return;
2330
2730
  }
2331
- fail(stderr.trim() || `sandbox http/request failed with code ${exitCode}`, exitCode);
2731
+ if (!params.streamResponse) stderr = Buffer.concat(buffered.stderr.chunks).toString("utf8");
2732
+ fail(stderr.trim() || `sandbox http/request failed with code ${exitCode}`);
2332
2733
  });
2333
2734
  });
2334
2735
  }
@@ -2576,7 +2977,7 @@ const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
2576
2977
  const RETAINED_PROCESS_OUTPUT_BYTES = 1048576;
2577
2978
  const CLOSED_PROCESS_EVICTION_MS = 6e4;
2578
2979
  /** Starts a sandbox-backed process and registers it in the connection-local process table. */
2579
- async function startProcess(execServer, processes, notify, params) {
2980
+ async function startProcess(execServer, processes, notify, params, processAuthority) {
2580
2981
  const record = requireObject(params, "process/start params");
2581
2982
  const processId = requireString(record.processId, "processId");
2582
2983
  if (processes.has(processId)) throw new Error(`process already exists: ${processId}`);
@@ -2608,18 +3009,39 @@ async function startProcess(execServer, processes, notify, params) {
2608
3009
  managed.evictionTimer.unref?.();
2609
3010
  }
2610
3011
  };
3012
+ const source = processAuthority?.claim(record.metadata, async () => {
3013
+ await terminateManagedProcess(managed);
3014
+ });
2611
3015
  processes.set(processId, managed);
2612
3016
  const startPromise = runProcess(execServer, managed, {
2613
3017
  argv,
2614
3018
  cwd,
2615
- env
3019
+ env,
3020
+ source
2616
3021
  });
2617
3022
  managed.startPromise = startPromise;
3023
+ startPromise.catch(() => void 0).then(async () => {
3024
+ await managed.child?.settled;
3025
+ if (managed.terminationRequested) await managed.child?.terminate();
3026
+ source?.settle();
3027
+ }).catch((error) => {
3028
+ source?.fail(error);
3029
+ embeddedAgentLog.warn("codex sandbox process settlement failed", {
3030
+ processId,
3031
+ error: coerceErrorMessage(error)
3032
+ });
3033
+ });
2618
3034
  try {
2619
3035
  await startPromise;
2620
3036
  } catch (error) {
2621
- processes.delete(processId);
2622
3037
  managed.failure = coerceErrorMessage(error);
3038
+ await managed.child?.terminate().catch((cleanupError) => {
3039
+ embeddedAgentLog.warn("codex sandbox failed-start cleanup failed", {
3040
+ processId,
3041
+ error: coerceErrorMessage(cleanupError)
3042
+ });
3043
+ });
3044
+ processes.delete(processId);
2623
3045
  managed.exitCode = null;
2624
3046
  managed.exited = true;
2625
3047
  managed.closed = true;
@@ -2643,13 +3065,14 @@ function assertSupportedProcessSandbox(execServer, record) {
2643
3065
  }
2644
3066
  async function runProcess(execServer, managed, params) {
2645
3067
  const backend = execServer.backend;
3068
+ params.source?.assertAdmission();
2646
3069
  throwIfProcessStartCancelled(managed);
2647
- const remoteExec = prepareSandboxChildExec(backend, params.env);
3070
+ const remoteExec = prepareSandboxProcessCleanup(backend, params.env);
2648
3071
  const execSpec = await backend.buildExecSpec({
2649
- command: buildRemoteCommand(params.argv),
3072
+ command: `exec ${buildRemoteCommand(params.argv)}`,
2650
3073
  workdir: params.cwd,
2651
3074
  env: remoteExec.env,
2652
- usePty: false
3075
+ usePty: managed.tty
2653
3076
  });
2654
3077
  if (managed.terminationRequested) {
2655
3078
  await backend.finalizeExec?.({
@@ -2660,9 +3083,18 @@ async function runProcess(execServer, managed, params) {
2660
3083
  });
2661
3084
  throw new Error("process start cancelled");
2662
3085
  }
3086
+ let spawned = false;
2663
3087
  const owner = await spawnSandboxChild({
2664
3088
  argv: execSpec.argv,
2665
3089
  env: execSpec.env,
3090
+ cwd: execSpec.cwd,
3091
+ usePty: managed.tty,
3092
+ assertCurrent: () => {
3093
+ if (spawned) params.source?.assertCurrent();
3094
+ else params.source?.assertAdmission();
3095
+ execSpec.assertCurrent?.();
3096
+ throwIfProcessStartCancelled(managed);
3097
+ },
2666
3098
  finalizeExec: backend.finalizeExec,
2667
3099
  finalizeToken: execSpec.finalizeToken,
2668
3100
  finalizeStatus: () => managed.failure ? "failed" : "completed",
@@ -2675,19 +3107,29 @@ async function runProcess(execServer, managed, params) {
2675
3107
  });
2676
3108
  },
2677
3109
  owners: execServer.children,
2678
- terminateRemote: remoteExec.terminate
3110
+ terminateRemote: remoteExec.terminate,
3111
+ interruptRemote: remoteExec.interrupt
2679
3112
  });
3113
+ spawned = true;
2680
3114
  managed.child = owner;
3115
+ owner.exited.then(({ exitCode }) => emitProcessExited(managed, exitCode));
3116
+ owner.closed.then(({ exitCode }) => emitProcessClosed(managed, exitCode));
3117
+ if ("pty" in owner) {
3118
+ owner.pty.onData((chunk) => appendProcessChunk(managed, "pty", Buffer.from(chunk)));
3119
+ return;
3120
+ }
2681
3121
  const child = owner.process;
2682
- child.stdout.on("data", (chunk) => appendProcessChunk(managed, managed.tty ? "pty" : "stdout", chunk));
3122
+ child.stdout.on("data", (chunk) => appendProcessChunk(managed, "stdout", chunk));
2683
3123
  child.stderr.on("data", (chunk) => appendProcessChunk(managed, "stderr", chunk));
2684
3124
  child.once("error", (error) => {
2685
3125
  managed.failure ??= error.message;
2686
3126
  notifyProcessWaiters(managed);
2687
3127
  });
2688
- child.once("close", (code) => {
2689
- emitProcessClosed(managed, code ?? 1);
3128
+ child.stdin.on("error", (error) => {
3129
+ managed.failure ??= error.message;
3130
+ notifyProcessWaiters(managed);
2690
3131
  });
3132
+ owner.assertCurrent();
2691
3133
  if (!managed.tty && !managed.pipeStdin) child.stdin.end();
2692
3134
  }
2693
3135
  function throwIfProcessStartCancelled(managed) {
@@ -2716,7 +3158,7 @@ function appendProcessChunk(managed, stream, data) {
2716
3158
  });
2717
3159
  notifyProcessWaiters(managed);
2718
3160
  }
2719
- function emitProcessClosed(managed, exitCode) {
3161
+ function emitProcessExited(managed, exitCode) {
2720
3162
  if (!managed.exited) {
2721
3163
  const exitSeq = managed.nextSeq;
2722
3164
  managed.nextSeq += 1;
@@ -2725,9 +3167,14 @@ function emitProcessClosed(managed, exitCode) {
2725
3167
  if (exitCode !== null) managed.emitNotification("process/exited", {
2726
3168
  processId: managed.processId,
2727
3169
  seq: exitSeq,
2728
- exitCode
3170
+ exitCode,
3171
+ sandboxDenied: false
2729
3172
  });
2730
3173
  }
3174
+ notifyProcessWaiters(managed);
3175
+ }
3176
+ function emitProcessClosed(managed, exitCode) {
3177
+ emitProcessExited(managed, exitCode);
2731
3178
  if (!managed.closed) {
2732
3179
  const closeSeq = managed.nextSeq;
2733
3180
  managed.nextSeq += 1;
@@ -2759,7 +3206,7 @@ async function readProcess(processes, params) {
2759
3206
  const managed = requireProcess(processes, requireString(record.processId, "processId"));
2760
3207
  const afterSeq = typeof record.afterSeq === "number" ? record.afterSeq : 0;
2761
3208
  const waitMs = typeof record.waitMs === "number" && record.waitMs > 0 ? record.waitMs : 0;
2762
- if (!managed.exited && !hasChunksAtOrAfter(managed, afterSeq) && waitMs > 0) await waitForProcessUpdate(managed, waitMs);
3209
+ if (!managed.closed && managed.nextSeq - 1 <= afterSeq && waitMs > 0) await waitForProcessUpdate(managed, waitMs);
2763
3210
  const chunks = limitProcessChunks(managed.chunks.filter((chunk) => chunk.seq > afterSeq), typeof record.maxBytes === "number" && record.maxBytes > 0 ? record.maxBytes : void 0);
2764
3211
  const lastChunk = chunks.at(-1);
2765
3212
  return {
@@ -2778,15 +3225,37 @@ function writeProcess(processes, params) {
2778
3225
  const managed = processes.get(processId);
2779
3226
  if (!managed) return { status: "unknownProcess" };
2780
3227
  const chunk = Buffer.from(requireString(record.chunk, "chunk"), "base64");
2781
- if (!managed.tty && !managed.pipeStdin || managed.closed || !managed.child?.process.stdin.writable) return { status: "stdinClosed" };
2782
- managed.child.process.stdin.write(chunk);
3228
+ if (!managed.tty && !managed.pipeStdin || managed.closed || !managed.child) return { status: "stdinClosed" };
3229
+ if ("pty" in managed.child) {
3230
+ managed.child.assertCurrent();
3231
+ managed.child.pty.write(chunk);
3232
+ } else {
3233
+ if (!managed.child.process.stdin.writable) return { status: "stdinClosed" };
3234
+ managed.child.assertCurrent();
3235
+ managed.child.process.stdin.write(chunk);
3236
+ }
2783
3237
  return { status: "accepted" };
2784
3238
  }
3239
+ async function signalProcess(processes, params) {
3240
+ const record = requireObject(params, "process/signal params");
3241
+ const processId = requireString(record.processId, "processId");
3242
+ if (record.signal !== "interrupt") throw new Error("process/signal only supports interrupt");
3243
+ const managed = processes.get(processId);
3244
+ if (managed && !managed.exited) {
3245
+ await managed.startPromise;
3246
+ managed.child?.assertCurrent();
3247
+ await managed.child?.interrupt();
3248
+ }
3249
+ return {};
3250
+ }
2785
3251
  /** Requests process termination and reports whether it was running at call time. */
2786
3252
  async function terminateProcess(processes, params) {
2787
3253
  const processId = requireString(requireObject(params, "process/terminate params").processId, "processId");
2788
3254
  const managed = processes.get(processId);
2789
3255
  if (!managed) return { running: false };
3256
+ return await terminateManagedProcess(managed);
3257
+ }
3258
+ async function terminateManagedProcess(managed) {
2790
3259
  const running = !managed.exited;
2791
3260
  managed.terminationRequested = true;
2792
3261
  await managed.startPromise?.catch(() => void 0);
@@ -2810,9 +3279,6 @@ function notifyProcessWaiters(managed) {
2810
3279
  managed.waiters = [];
2811
3280
  for (const waiter of waiters) waiter();
2812
3281
  }
2813
- function hasChunksAtOrAfter(managed, afterSeq) {
2814
- return managed.chunks.some((chunk) => chunk.seq > afterSeq);
2815
- }
2816
3282
  function requireProcess(processes, processId) {
2817
3283
  const managed = processes.get(processId);
2818
3284
  if (!managed) throw new Error(`unknown process: ${processId}`);
@@ -2861,11 +3327,13 @@ function readStringList(value) {
2861
3327
  /** Owns the JSON-RPC protocol and resources of one sandbox execution connection. */
2862
3328
  /** Connection-local execution state; closing it never enables session resumption. */
2863
3329
  var CodexSandboxExecSession = class {
2864
- constructor(execServer, transport) {
3330
+ constructor(execServer, transport, processAuthority) {
2865
3331
  this.execServer = execServer;
2866
3332
  this.transport = transport;
3333
+ this.processAuthority = processAuthority;
2867
3334
  this.processes = /* @__PURE__ */ new Map();
2868
3335
  this.fileReads = /* @__PURE__ */ new Map();
3336
+ this.httpRequests = /* @__PURE__ */ new Set();
2869
3337
  this.closeController = new AbortController();
2870
3338
  this.notifications = {
2871
3339
  isOpen: transport.isOpen,
@@ -2900,7 +3368,10 @@ var CodexSandboxExecSession = class {
2900
3368
  if (!this.cleanup) {
2901
3369
  this.closeController.abort();
2902
3370
  closeAllFileReads(this.fileReads);
2903
- this.cleanup = Promise.all([...this.processes.keys()].map(async (processId) => terminateProcess(this.processes, { processId }))).then(() => void 0);
3371
+ this.cleanup = Promise.allSettled([...this.httpRequests, ...[...this.processes.keys()].map(async (processId) => terminateProcess(this.processes, { processId }))]).then((results) => {
3372
+ const failures = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
3373
+ if (failures.length > 0) throw new AggregateError(failures, "Codex sandbox execution cleanup failed");
3374
+ });
2904
3375
  }
2905
3376
  return this.cleanup;
2906
3377
  }
@@ -2916,9 +3387,10 @@ var CodexSandboxExecSession = class {
2916
3387
  capabilities: { networkProxyLaunch: false }
2917
3388
  };
2918
3389
  case "environment/status": return { status: "ready" };
2919
- case "process/start": return startProcess(this.execServer, this.processes, this.notifications.send, params);
3390
+ case "process/start": return startProcess(this.execServer, this.processes, this.notifications.send, params, this.processAuthority);
2920
3391
  case "process/read": return await readProcess(this.processes, params);
2921
3392
  case "process/write": return writeProcess(this.processes, params);
3393
+ case "process/signal": return await signalProcess(this.processes, params);
2922
3394
  case "process/terminate": return await terminateProcess(this.processes, params);
2923
3395
  case "fs/open": return await openFile(this.execServer, this.fileReads, params);
2924
3396
  case "fs/readBlock": return readFileBlock(this.fileReads, params);
@@ -2938,7 +3410,7 @@ var CodexSandboxExecSession = class {
2938
3410
  case "fs/copy":
2939
3411
  await copyPath(this.execServer, params);
2940
3412
  return {};
2941
- case "http/request": return await httpRequest(this.execServer, this.notifications, params);
3413
+ case "http/request": return await httpRequest(this.execServer, this.notifications, params, this.httpRequests);
2942
3414
  default: throw new JsonRpcProtocolError(JSON_RPC_METHOD_NOT_FOUND, `Unsupported OpenClaw sandbox exec-server method: ${method}`);
2943
3415
  }
2944
3416
  }
@@ -2965,9 +3437,17 @@ async function ensureCodexSandboxExecServerEnvironment(params) {
2965
3437
  signal: params.signal,
2966
3438
  onExecutionDisconnect: params.onExecutionDisconnect
2967
3439
  });
2968
- const environmentId = nodeLease ? `openclaw-node-${nodeLease.id}` : execServer.environmentId;
3440
+ const processAuthority = params.requireProcessAuthority && !("node" in execServer) ? getCodexNativeProcessClient(params.client) : void 0;
3441
+ if (processAuthority && !("node" in execServer)) {
3442
+ const authorities = execServer.processAuthorities ??= /* @__PURE__ */ new Map();
3443
+ if (!authorities.has(processAuthority.authPath)) {
3444
+ authorities.set(processAuthority.authPath, processAuthority);
3445
+ params.client.addCloseHandler(() => authorities.delete(processAuthority.authPath));
3446
+ }
3447
+ }
3448
+ const environmentId = nodeLease ? `openclaw-node-${nodeLease.id}` : processAuthority ? `${execServer.environmentId}-${processAuthority.id}` : execServer.environmentId;
2969
3449
  try {
2970
- const execServerUrl = nodeLease ? `${execServer.url}?lease=${nodeLease.id}` : execServer.url;
3450
+ const execServerUrl = nodeLease ? `${execServer.url}?lease=${nodeLease.id}` : processAuthority ? new URL(processAuthority.authPath, execServer.url).href : execServer.url;
2971
3451
  await params.client.request("environment/add", {
2972
3452
  environmentId,
2973
3453
  execServerUrl
@@ -3139,7 +3619,8 @@ async function startOpenClawExecServer(sandbox) {
3139
3619
  handleNodeConnection(execServer, socket, request);
3140
3620
  return;
3141
3621
  }
3142
- handleConnection(execServer, socket);
3622
+ const requestPath = new URL(request.url ?? "", "ws://127.0.0.1").pathname;
3623
+ handleConnection(execServer, socket, execServer.processAuthorities?.get(requestPath));
3143
3624
  });
3144
3625
  embeddedAgentLog.info("codex sandbox exec-server started", {
3145
3626
  environmentId,
@@ -3161,7 +3642,8 @@ function buildEnvironmentId(sandbox) {
3161
3642
  return `openclaw-sandbox-${createHash("sha256").update(sandbox.runtimeId).digest("hex").slice(0, 16)}`;
3162
3643
  }
3163
3644
  function isAuthorizedExecServerRequest(execServer, request) {
3164
- return new URL(request.url ?? "", "ws://127.0.0.1").pathname === execServer.authPath;
3645
+ const url = new URL(request.url ?? "", "ws://127.0.0.1");
3646
+ return url.pathname === execServer.authPath || !("node" in execServer) && execServer.processAuthorities?.has(url.pathname) === true;
3165
3647
  }
3166
3648
  function readCodexPlacementNodeId(sandbox) {
3167
3649
  if (!("placementExecutionMode" in sandbox) || sandbox.placementExecutionMode !== "remote-exec" || !("placementNodeId" in sandbox) || typeof sandbox.placementNodeId !== "string" || !sandbox.placementNodeId) return;
@@ -3214,11 +3696,11 @@ function handleClosedCodexNodeExecServerLease(execServer, lease, result) {
3214
3696
  closeCodexNodeExecServerLease(execServer, lease);
3215
3697
  }
3216
3698
  }
3217
- function handleConnection(execServer, socket) {
3699
+ function handleConnection(execServer, socket, processAuthority) {
3218
3700
  const session = new CodexSandboxExecSession(execServer, {
3219
3701
  isOpen: () => socket.readyState === socket.OPEN,
3220
3702
  send: (message) => socket.send(JSON.stringify(message))
3221
- });
3703
+ }, processAuthority);
3222
3704
  socket.on("message", (data) => {
3223
3705
  handleMessage(session, data).catch((error) => {
3224
3706
  embeddedAgentLog.warn("codex sandbox exec-server message failed", { error });
@@ -3241,6 +3723,73 @@ async function handleMessage(session, data) {
3241
3723
  await session.handleRequest(parseRequest(buffer.toString("utf8")));
3242
3724
  }
3243
3725
  //#endregion
3726
+ //#region extensions/codex/src/app-server/native-command-approval.ts
3727
+ const PERSISTENT_APPROVAL_TARGET_MAX_LENGTH = 256;
3728
+ function resolveCommandApproval(requestParams, outcome) {
3729
+ if (outcome === "cancelled") return {
3730
+ scope: "cancelled",
3731
+ decision: "cancel"
3732
+ };
3733
+ if (outcome === "denied" || outcome === "unavailable") return {
3734
+ scope: "denied",
3735
+ decision: "decline"
3736
+ };
3737
+ const capabilities = commandApprovalCapabilities(requestParams);
3738
+ if (outcome === "approved-session" && capabilities.repeated) return capabilities.repeated;
3739
+ return capabilities.once ? {
3740
+ scope: "once",
3741
+ decision: "accept"
3742
+ } : {
3743
+ scope: "denied",
3744
+ decision: "decline"
3745
+ };
3746
+ }
3747
+ function commandApprovalAllowedDecisions(requestParams, requiresOneShot) {
3748
+ if (!Array.isArray(requestParams?.availableDecisions)) return;
3749
+ const capabilities = commandApprovalCapabilities(requestParams);
3750
+ const decisions = [];
3751
+ if (capabilities.once) decisions.push("allow-once");
3752
+ if (!requiresOneShot && capabilities.repeated) decisions.push("allow-always");
3753
+ decisions.push("deny");
3754
+ return decisions;
3755
+ }
3756
+ function commandApprovalCapabilities(requestParams) {
3757
+ const available = requestParams?.availableDecisions;
3758
+ if (!Array.isArray(available)) return {
3759
+ once: true,
3760
+ repeated: {
3761
+ scope: "session",
3762
+ decision: "acceptForSession"
3763
+ }
3764
+ };
3765
+ return {
3766
+ once: available.includes("accept"),
3767
+ repeated: available.includes("acceptForSession") ? {
3768
+ scope: "session",
3769
+ decision: "acceptForSession"
3770
+ } : findAvailableCommandPersistentApproval(available)
3771
+ };
3772
+ }
3773
+ function findAvailableCommandPersistentApproval(available) {
3774
+ for (const decision of available) {
3775
+ if (!isJsonObject(decision)) continue;
3776
+ const exec = decision.acceptWithExecpolicyAmendment;
3777
+ const network = decision.applyNetworkPolicyAmendment;
3778
+ const prefix = isJsonObject(exec) ? exec.execpolicy_amendment : void 0;
3779
+ const amendment = isJsonObject(network) ? network.network_policy_amendment : void 0;
3780
+ let target;
3781
+ if (Array.isArray(prefix) && prefix.length > 0 && prefix.every((part) => typeof part === "string")) target = `Command prefix: ${JSON.stringify(prefix)}`;
3782
+ else if (isJsonObject(amendment) && amendment.action === "allow" && typeof amendment.host === "string" && amendment.host.length > 0) target = `Network host: ${JSON.stringify(amendment.host)}`;
3783
+ else continue;
3784
+ if (target.length > PERSISTENT_APPROVAL_TARGET_MAX_LENGTH || sanitizeCodexApprovalVisibleText(target) !== target) continue;
3785
+ return {
3786
+ scope: "persistent",
3787
+ decision,
3788
+ description: `Allow Always requests a persistent allow rule for future sessions.\n${target}`
3789
+ };
3790
+ }
3791
+ }
3792
+ //#endregion
3244
3793
  //#region extensions/codex/src/app-server/approval-bridge.ts
3245
3794
  /**
3246
3795
  * Bridges Codex app-server approval requests into OpenClaw policy hooks and
@@ -3266,12 +3815,14 @@ async function handleCodexAppServerApprovalRequest(params) {
3266
3815
  paramsForRun: params.paramsForRun
3267
3816
  });
3268
3817
  if (params.signal?.aborted) {
3818
+ if (params.signal.reason instanceof CodexServerRequestResolvedError) return;
3269
3819
  recordNativeToolFailureDisposition(params, context, "cancelled");
3270
3820
  return buildApprovalResponse(params.method, context.requestParams, "cancelled");
3271
3821
  }
3272
3822
  let revalidateMutableFileApproval;
3273
3823
  let mutableFileApprovalRequiresOneShot = false;
3274
- const resolvePolicyApproval = async (outcome, message = approvalResolutionMessage(outcome), approvalId) => {
3824
+ let approvalId;
3825
+ const resolvePolicyApproval = async (outcome, message = approvalResolutionMessage(outcome), resolvedApprovalId) => {
3275
3826
  let resolvedOutcome = outcome;
3276
3827
  let resolvedMessage = message;
3277
3828
  if (outcome !== "denied" && revalidateMutableFileApproval) {
@@ -3285,6 +3836,16 @@ async function handleCodexAppServerApprovalRequest(params) {
3285
3836
  resolvedOutcome = "approved-once";
3286
3837
  resolvedMessage = "Codex app-server approval granted for this byte-bound command only.";
3287
3838
  }
3839
+ if (params.method === "item/commandExecution/requestApproval" && resolvedOutcome !== "denied") {
3840
+ const resolution = resolveCommandApproval(context.requestParams, resolvedOutcome);
3841
+ if (resolution.scope === "denied") {
3842
+ resolvedOutcome = "denied";
3843
+ resolvedMessage = "Codex app-server request does not offer the approved scope.";
3844
+ } else if (resolvedOutcome === "approved-session" && resolution.scope === "once") {
3845
+ resolvedOutcome = "approved-once";
3846
+ resolvedMessage = approvalResolutionMessage(resolvedOutcome);
3847
+ } else if (resolution.scope === "persistent") resolvedMessage = "Codex app-server approval granted; persistent policy amendment requested for future sessions.";
3848
+ }
3288
3849
  params.signal?.throwIfAborted();
3289
3850
  if (resolvedOutcome !== "denied") params.paramsForRun.hostCapabilities.assertActive();
3290
3851
  emitApprovalEvent(params.paramsForRun, {
@@ -3292,9 +3853,9 @@ async function handleCodexAppServerApprovalRequest(params) {
3292
3853
  kind: context.kind,
3293
3854
  status: resolvedOutcome === "denied" ? "denied" : "approved",
3294
3855
  title: context.title,
3295
- ...approvalId ? {
3296
- approvalId,
3297
- approvalSlug: approvalId
3856
+ ...resolvedApprovalId ? {
3857
+ approvalId: resolvedApprovalId,
3858
+ approvalSlug: resolvedApprovalId
3298
3859
  } : {},
3299
3860
  ...context.eventDetails,
3300
3861
  ...approvalEventScope(params.method, resolvedOutcome),
@@ -3331,22 +3892,25 @@ async function handleCodexAppServerApprovalRequest(params) {
3331
3892
  return await resolvePolicyApproval("denied", policyOutcome.reason);
3332
3893
  }
3333
3894
  if (policyOutcome?.outcome === "approved-once" || policyOutcome?.outcome === "approved-session") return await resolvePolicyApproval(policyOutcome.outcome);
3334
- if (CONCRETE_TOOL_AUTO_APPROVAL_METHODS.has(params.method) && !readNetworkApprovalContext(requestParams) && params.autoApprove === true) return await resolvePolicyApproval("approved-session", "Codex app-server approval auto-approved by runtime policy.");
3895
+ if (CONCRETE_TOOL_AUTO_APPROVAL_METHODS.has(params.method) && !readNetworkApprovalContext(requestParams) && params.autoApprove === true) return await resolvePolicyApproval("approved-once", "Codex app-server approval auto-approved by runtime policy.");
3896
+ const allowedDecisions = nativeApprovalAllowedDecisions({
3897
+ method: params.method,
3898
+ requestParams,
3899
+ requiresOneShot: mutableFileApprovalRequiresOneShot
3900
+ });
3901
+ const repeatedApproval = params.method === "item/commandExecution/requestApproval" && allowedDecisions?.includes("allow-always") ? commandApprovalCapabilities(requestParams).repeated : void 0;
3335
3902
  const requestResult = await requestPluginApproval({
3336
3903
  hostCapabilities: params.paramsForRun.hostCapabilities,
3337
3904
  signal: params.signal,
3338
3905
  title: context.title,
3339
- description: context.description,
3906
+ description: repeatedApproval?.scope === "persistent" ? `${repeatedApproval.description}\n${context.description}` : context.description,
3340
3907
  severity: context.severity,
3341
3908
  toolName: context.toolName,
3342
3909
  toolCallId: context.approvalId,
3343
- allowedDecisions: nativeApprovalAllowedDecisions({
3344
- method: params.method,
3345
- requestParams,
3346
- requiresOneShot: mutableFileApprovalRequiresOneShot
3347
- })
3910
+ allowedDecisions
3348
3911
  });
3349
- const approvalId = requestResult?.id;
3912
+ approvalId = requestResult?.id;
3913
+ params.signal?.throwIfAborted();
3350
3914
  if (!approvalId) {
3351
3915
  recordNativeToolFailureDisposition(params, context, "failed");
3352
3916
  emitApprovalEvent(params.paramsForRun, {
@@ -3375,16 +3939,16 @@ async function handleCodexAppServerApprovalRequest(params) {
3375
3939
  signal: params.signal,
3376
3940
  hostCapabilities: params.paramsForRun.hostCapabilities
3377
3941
  });
3378
- const approvalTimedOut = !params.signal?.aborted && approvalResult?.terminalReason === "timeout";
3379
- const outcome = params.signal?.aborted ? "cancelled" : mapExecDecisionToOutcome(approvalResult?.decision);
3942
+ params.signal?.throwIfAborted();
3943
+ const approvalTimedOut = approvalResult?.terminalReason === "timeout";
3944
+ const outcome = mapExecDecisionToOutcome(approvalResult?.decision);
3380
3945
  if (approvalTimedOut) recordNativeToolFailureDisposition(params, context, "timed_out", context.approvalKind);
3381
- else if (outcome === "cancelled") recordNativeToolFailureDisposition(params, context, params.signal?.aborted ? resolveCodexToolAbortTerminalReason(params.signal) : "cancelled");
3382
3946
  else if (outcome === "unavailable") recordNativeToolFailureDisposition(params, context, "failed");
3383
3947
  if (outcome === "approved-once" || outcome === "approved-session") return await resolvePolicyApproval(outcome, approvalResolutionMessage(outcome), approvalId);
3384
3948
  emitApprovalEvent(params.paramsForRun, {
3385
3949
  phase: "resolved",
3386
3950
  kind: context.kind,
3387
- status: outcome === "denied" ? "denied" : outcome === "unavailable" ? "unavailable" : outcome === "cancelled" ? "failed" : "approved",
3951
+ status: outcome,
3388
3952
  title: context.title,
3389
3953
  approvalId,
3390
3954
  approvalSlug: approvalId,
@@ -3394,6 +3958,7 @@ async function handleCodexAppServerApprovalRequest(params) {
3394
3958
  });
3395
3959
  return buildApprovalResponse(params.method, context.requestParams, outcome);
3396
3960
  } catch (error) {
3961
+ if (params.signal?.reason instanceof CodexServerRequestResolvedError) return;
3397
3962
  const cancelled = params.signal?.aborted === true;
3398
3963
  recordNativeToolFailureDisposition(params, context, cancelled && params.signal ? resolveCodexToolAbortTerminalReason(params.signal) : "failed");
3399
3964
  emitApprovalEvent(params.paramsForRun, {
@@ -3401,6 +3966,10 @@ async function handleCodexAppServerApprovalRequest(params) {
3401
3966
  kind: context.kind,
3402
3967
  status: cancelled ? "failed" : "unavailable",
3403
3968
  title: context.title,
3969
+ ...approvalId ? {
3970
+ approvalId,
3971
+ approvalSlug: approvalId
3972
+ } : {},
3404
3973
  ...context.eventDetails,
3405
3974
  ...approvalEventScope(params.method, cancelled ? "cancelled" : "denied"),
3406
3975
  message: cancelled ? "Codex app-server approval cancelled because the run stopped." : `Codex app-server approval route failed: ${formatCodexDisplayText(coerceErrorMessage(error))}`
@@ -3417,7 +3986,7 @@ function recordNativeToolFailureDisposition(params, context, disposition, approv
3417
3986
  }
3418
3987
  /** Converts an OpenClaw approval outcome into the app-server method response. */
3419
3988
  function buildApprovalResponse(method, requestParams, outcome) {
3420
- if (method === "item/commandExecution/requestApproval") return { decision: commandApprovalDecision(requestParams, outcome) };
3989
+ if (method === "item/commandExecution/requestApproval") return { decision: resolveCommandApproval(requestParams, outcome).decision };
3421
3990
  if (method === "item/fileChange/requestApproval") return { decision: fileChangeApprovalDecision(outcome) };
3422
3991
  if (method === "item/permissions/requestApproval") {
3423
3992
  if (outcome === "approved-session" || outcome === "approved-once") return {
@@ -3675,28 +4244,13 @@ function stableJsonText(value) {
3675
4244
  return entries.every((entry) => entry !== void 0) ? `{${entries.join(",")}}` : void 0;
3676
4245
  }
3677
4246
  }
3678
- function commandApprovalDecision(requestParams, outcome) {
3679
- if (outcome === "cancelled") return "cancel";
3680
- if (outcome === "denied" || outcome === "unavailable") return "decline";
3681
- const capabilities = commandApprovalCapabilities(requestParams);
3682
- if (outcome === "approved-session" && capabilities.sessionDecision !== void 0) return capabilities.sessionDecision;
3683
- return capabilities.once ? "accept" : "decline";
3684
- }
3685
4247
  function nativeApprovalAllowedDecisions(params) {
3686
4248
  if (params.method === "item/fileChange/requestApproval") return [
3687
4249
  "allow-once",
3688
4250
  "allow-always",
3689
4251
  "deny"
3690
4252
  ];
3691
- if (params.method !== "item/commandExecution/requestApproval") return;
3692
- const available = params.requestParams?.availableDecisions;
3693
- if (!Array.isArray(available)) return;
3694
- const capabilities = commandApprovalCapabilities(params.requestParams);
3695
- const decisions = [];
3696
- if (capabilities.once) decisions.push("allow-once");
3697
- if (!params.requiresOneShot && capabilities.sessionDecision !== void 0) decisions.push("allow-always");
3698
- decisions.push("deny");
3699
- return decisions;
4253
+ return params.method === "item/commandExecution/requestApproval" ? commandApprovalAllowedDecisions(params.requestParams, params.requiresOneShot) : void 0;
3700
4254
  }
3701
4255
  function fileChangeApprovalDecision(outcome) {
3702
4256
  if (outcome === "cancelled") return "cancel";
@@ -3878,22 +4432,6 @@ function isPrivateNetworkHostPattern(value) {
3878
4432
  if (wildcardStripped.startsWith("10.") || wildcardStripped.startsWith("127.") || wildcardStripped.startsWith("192.168.") || wildcardStripped.startsWith("169.254.")) return true;
3879
4433
  return /^172\.(1[6-9]|2\d|3[0-1])\./.test(wildcardStripped);
3880
4434
  }
3881
- function commandApprovalCapabilities(requestParams) {
3882
- const available = requestParams?.availableDecisions;
3883
- if (!Array.isArray(available)) return {
3884
- once: true,
3885
- sessionDecision: "acceptForSession"
3886
- };
3887
- return {
3888
- once: available.includes("accept"),
3889
- ...available.includes("acceptForSession") ? { sessionDecision: "acceptForSession" } : { sessionDecision: findAvailableCommandAmendmentDecision(requestParams) }
3890
- };
3891
- }
3892
- function findAvailableCommandAmendmentDecision(requestParams) {
3893
- const available = requestParams?.availableDecisions;
3894
- if (!Array.isArray(available)) return;
3895
- return available.find((entry) => isJsonObject(entry) && (isJsonObject(entry.acceptWithExecpolicyAmendment) || isJsonObject(entry.applyNetworkPolicyAmendment)));
3896
- }
3897
4435
  function approvalResolutionMessage(outcome) {
3898
4436
  return {
3899
4437
  "approved-session": "Codex app-server approval granted for the session.",
@@ -4221,7 +4759,7 @@ function resolvePluginElicitation(params) {
4221
4759
  const appId = readFirstString$1(meta, PLUGIN_APP_ID_META_KEYS) ?? readFirstString$1(requestParams, PLUGIN_APP_ID_META_KEYS);
4222
4760
  const connectorId = readFirstString$1(meta, PLUGIN_CONNECTOR_ID_META_KEYS);
4223
4761
  const isCodexConnectorApproval = isCodexConnectorApprovalElicitation(requestParams, meta);
4224
- if (isCodexConnectorApproval && appId && connectorId && appId !== connectorId) return {
4762
+ if (isCodexConnectorApproval && appId && connectorId && codexAppIdentityKey(appId) !== codexAppIdentityKey(connectorId)) return {
4225
4763
  kind: "decline",
4226
4764
  reason: "app_id_connector_id_mismatch"
4227
4765
  };
@@ -4230,20 +4768,19 @@ function resolvePluginElicitation(params) {
4230
4768
  kind: "decline",
4231
4769
  reason: "missing_policy_context"
4232
4770
  };
4233
- const entry = context.apps[appId];
4234
- if (entry?.source === "account" && !isCodexConnectorApproval) return {
4771
+ const matches = Object.entries(context.apps).filter(([id]) => codexAppIdentityKey(id) === codexAppIdentityKey(appId)).map(([, entry]) => entry);
4772
+ if (matches.some((entry) => entry.source === "account") && !isCodexConnectorApproval) return {
4235
4773
  kind: "decline",
4236
4774
  reason: "account_app_source_mismatch"
4237
4775
  };
4238
- return uniquePluginMatch(entry ? [entry] : [], "app_id");
4776
+ return uniquePluginMatch(matches, "app_id");
4239
4777
  }
4240
4778
  if (isCodexConnectorApproval && connectorId) {
4241
4779
  if (!context) return {
4242
4780
  kind: "decline",
4243
4781
  reason: "missing_policy_context"
4244
4782
  };
4245
- const entry = context.apps[connectorId];
4246
- return uniquePluginMatch(entry ? [entry] : [], "connector_id");
4783
+ return uniquePluginMatch(Object.entries(context.apps).filter(([id]) => codexAppIdentityKey(id) === codexAppIdentityKey(connectorId)).map(([, entry]) => entry), "connector_id");
4247
4784
  }
4248
4785
  const serverName = readNonBlankStringField(requestParams, "serverName");
4249
4786
  if (serverName && context) {
@@ -4979,142 +5516,6 @@ function applyCurrentMessageProvider(toolName, args, currentProvider) {
4979
5516
  provider
4980
5517
  };
4981
5518
  }
4982
- function normalizeRouteToken(value) {
4983
- if (typeof value === "number") return Number.isFinite(value) ? String(value) : void 0;
4984
- const normalized = value?.trim().toLowerCase();
4985
- return normalized ? normalized : void 0;
4986
- }
4987
- function sourceRouteTokens(hookContext) {
4988
- const tokens = /* @__PURE__ */ new Set();
4989
- const currentTarget = normalizeRouteToken(hookContext?.currentMessagingTarget);
4990
- const currentChannel = normalizeRouteToken(hookContext?.currentChannelId);
4991
- const currentProvider = normalizeRouteToken(hookContext?.currentChannelProvider);
4992
- if (currentTarget) tokens.add(currentTarget);
4993
- if (currentChannel) tokens.add(currentChannel);
4994
- const channelPrefixIndex = currentChannel?.indexOf(":") ?? -1;
4995
- if (channelPrefixIndex >= 0 && currentChannel) {
4996
- const unprefixedChannel = currentChannel.slice(channelPrefixIndex + 1);
4997
- if (unprefixedChannel) {
4998
- tokens.add(unprefixedChannel);
4999
- for (const segment of unprefixedChannel.split(/[;,]/u)) {
5000
- const token = normalizeRouteToken(segment);
5001
- if (token) tokens.add(token);
5002
- }
5003
- }
5004
- }
5005
- if (currentProvider && currentChannel?.startsWith(`${currentProvider}:`)) {
5006
- const unprefixedChannel = currentChannel.slice(currentProvider.length + 1);
5007
- if (unprefixedChannel) tokens.add(unprefixedChannel);
5008
- }
5009
- return tokens;
5010
- }
5011
- function routeTokenMatchesSource(token, hookContext) {
5012
- const normalized = normalizeRouteToken(token);
5013
- return normalized !== void 0 && sourceRouteTokens(hookContext).has(normalized);
5014
- }
5015
- function routeProviderMatchesSource(provider, hookContext) {
5016
- const normalized = normalizeRouteToken(provider);
5017
- if (!normalized) return false;
5018
- const currentProvider = normalizeRouteToken(hookContext?.currentChannelProvider);
5019
- const currentChannel = normalizeRouteToken(hookContext?.currentChannelId);
5020
- return currentProvider === normalized || currentChannel?.startsWith(`${normalized}:`) === true;
5021
- }
5022
- function routeTokenMatchesCurrentMessage(token, hookContext) {
5023
- const normalized = normalizeRouteToken(token);
5024
- return normalized !== void 0 && normalized === normalizeRouteToken(hookContext?.currentMessageId);
5025
- }
5026
- function readRouteToken(record, key) {
5027
- const value = record[key];
5028
- return typeof value === "string" || typeof value === "number" ? value : void 0;
5029
- }
5030
- function explicitRouteTokensMismatchCurrent(args, keys, currentToken) {
5031
- const normalizedCurrent = normalizeRouteToken(currentToken);
5032
- if (!normalizedCurrent) return false;
5033
- return keys.some((key) => {
5034
- const normalized = normalizeRouteToken(readRouteToken(args, key));
5035
- return normalized !== void 0 && normalized !== normalizedCurrent;
5036
- });
5037
- }
5038
- function explicitThreadRouteTargetsNonSource(args, hookContext, messagingTarget) {
5039
- const normalizedCurrentThread = normalizeRouteToken(hookContext?.currentThreadId);
5040
- const explicitThreadTokens = [...EXPLICIT_MESSAGE_THREAD_KEYS.map((key) => normalizeRouteToken(readRouteToken(args, key))), normalizeRouteToken(messagingTarget?.threadId)].filter((value) => value !== void 0);
5041
- if (explicitThreadTokens.length === 0) return false;
5042
- return normalizedCurrentThread === void 0 || explicitThreadTokens.some((value) => value !== normalizedCurrentThread);
5043
- }
5044
- function replyReceiptMatchesCurrentMessage(value, hookContext, depth = 0) {
5045
- if (depth > 4 || value === null) return false;
5046
- if (typeof value === "string") {
5047
- const trimmed = value.trim();
5048
- if (!trimmed || !["{", "["].includes(trimmed[0] ?? "")) return false;
5049
- try {
5050
- return replyReceiptMatchesCurrentMessage(JSON.parse(trimmed), hookContext, depth + 1);
5051
- } catch {
5052
- return false;
5053
- }
5054
- }
5055
- if (typeof value !== "object") return false;
5056
- if (Array.isArray(value)) return value.some((item) => replyReceiptMatchesCurrentMessage(item, hookContext, depth + 1));
5057
- const record = value;
5058
- for (const key of [
5059
- "repliedTo",
5060
- "replyTo",
5061
- "replyToId",
5062
- "replyToIdFull"
5063
- ]) if (routeTokenMatchesCurrentMessage(typeof record[key] === "string" ? record[key] : void 0, hookContext)) return true;
5064
- for (const key of [
5065
- "content",
5066
- "details",
5067
- "payload",
5068
- "receipt",
5069
- "result",
5070
- "results",
5071
- "sendResult",
5072
- "text"
5073
- ]) if (replyReceiptMatchesCurrentMessage(record[key], hookContext, depth + 1)) return true;
5074
- return false;
5075
- }
5076
- function hasExplicitNonSourceMessageRoute(args, hookContext, messagingTarget) {
5077
- const currentProvider = normalizeRouteToken(hookContext?.currentChannelProvider);
5078
- for (const key of EXPLICIT_MESSAGE_PROVIDER_KEYS) {
5079
- const provider = normalizeRouteToken(typeof args[key] === "string" ? args[key] : void 0);
5080
- if (provider && currentProvider !== provider && !routeProviderMatchesSource(provider, hookContext)) return true;
5081
- }
5082
- const targetValues = [...EXPLICIT_MESSAGE_TARGET_KEYS.map((key) => typeof args[key] === "string" ? args[key] : void 0), ...Array.isArray(args.targets) ? args.targets.map((value) => typeof value === "string" ? value : void 0) : []].filter((value) => normalizeRouteToken(value) !== void 0);
5083
- if (explicitThreadRouteTargetsNonSource(args, hookContext, messagingTarget)) return true;
5084
- if (explicitRouteTokensMismatchCurrent(args, EXPLICIT_MESSAGE_REPLY_KEYS, hookContext?.currentMessageId)) return true;
5085
- if (messagingTarget?.to !== void 0 && !routeTokenMatchesSource(messagingTarget.to, hookContext)) return true;
5086
- if (messagingTarget?.to !== void 0) return false;
5087
- if (targetValues.length === 0) return false;
5088
- if (targetValues.some((value) => !routeTokenMatchesSource(value, hookContext))) return true;
5089
- return false;
5090
- }
5091
- function normalizeAcceptedSessionSpawn(result) {
5092
- const details = asOptionalRecord(asOptionalRecord(result)?.details);
5093
- if (!details || details.status !== "accepted") return null;
5094
- const runId = normalizeOptionalString(details.runId);
5095
- const childSessionKey = normalizeOptionalString(details.childSessionKey);
5096
- return runId && childSessionKey ? {
5097
- runId,
5098
- childSessionKey
5099
- } : null;
5100
- }
5101
- const EXPLICIT_MESSAGE_PROVIDER_KEYS = ["channel", "provider"];
5102
- const EXPLICIT_MESSAGE_TARGET_KEYS = [
5103
- "target",
5104
- "to",
5105
- "channelId"
5106
- ];
5107
- const EXPLICIT_MESSAGE_THREAD_KEYS = [
5108
- "threadId",
5109
- "thread_id",
5110
- "messageThreadId",
5111
- "topicId"
5112
- ];
5113
- const EXPLICIT_MESSAGE_REPLY_KEYS = [
5114
- "replyTo",
5115
- "replyToId",
5116
- "replyToIdFull"
5117
- ];
5118
5519
  function computerFrameImageIdentity(content) {
5119
5520
  if (!Array.isArray(content)) return;
5120
5521
  const images = content.filter((block) => block.type === "image");
@@ -5142,6 +5543,11 @@ function createCodexDynamicToolBridge(params) {
5142
5543
  const registrationNames = inheritedNames ?? new Set(registeredProjection.tools.map((entry) => entry.name));
5143
5544
  const finalized = finalizeCodexToolAvailability(availableProjection.tools.filter((entry) => registrationNames.has(entry.name)));
5144
5545
  const availableTools = finalized.tools;
5546
+ const pluginLocalMediaTrustByToolName = /* @__PURE__ */ new Map();
5547
+ for (const { name, tool } of availableTools) {
5548
+ const pluginMeta = getPluginToolMeta(tool);
5549
+ if (pluginMeta) pluginLocalMediaTrustByToolName.set(name, new Set(pluginMeta.trustedLocalMedia === true ? [name] : []));
5550
+ }
5145
5551
  availableProjection.quarantinedTools.push(...finalized.quarantinedTools);
5146
5552
  const toolMap = new Map(availableTools.map((entry) => [entry.name, entry]));
5147
5553
  const quarantinedAvailableToolNames = new Set(availableProjection.quarantinedTools.map((tool) => tool.tool));
@@ -5161,6 +5567,7 @@ function createCodexDynamicToolBridge(params) {
5161
5567
  messagingToolSentMediaUrls: [],
5162
5568
  messagingToolSentTargets: [],
5163
5569
  messagingToolSourceReplyPayloads: [],
5570
+ confirmedMediaDeliveries: [],
5164
5571
  toolMediaUrls: [],
5165
5572
  toolAutoDeliveryMediaUrls: [],
5166
5573
  coreTtsToolResults: [],
@@ -5264,16 +5671,16 @@ function createCodexDynamicToolBridge(params) {
5264
5671
  try {
5265
5672
  const prepare = tool.prepareArguments;
5266
5673
  const toolArgs = prepare ? Reflect.apply(prepare, tool, [rawArguments]) : args;
5267
- const preparedArgs = toolName === "message" && isRecord(toolArgs) ? await prepareCodexRemoteWorkspaceMessageMedia({
5674
+ const preparedMessageMedia = toolName === "message" && isRecord(toolArgs) ? await prepareCodexRemoteWorkspaceMessageMedia({
5268
5675
  args: toolArgs,
5269
5676
  localWorkspaceRoot: params.hookContext?.workspaceDir,
5270
5677
  remoteWorkspaceRoot: params.hookContext?.remoteWorkspaceRoot,
5271
5678
  readRemoteFile: readRemoteWorkspaceFile,
5272
5679
  timeoutMs: params.hookContext?.remoteWorkspaceRequestTimeoutMs,
5273
5680
  signal
5274
- }) : toolArgs;
5275
- const telemetryArgs = isRecord(preparedArgs) ? preparedArgs : args;
5276
- executedArgs = structuredClone(telemetryArgs);
5681
+ }) : void 0;
5682
+ const preparedArgs = preparedMessageMedia?.args ?? toolArgs;
5683
+ executedArgs = structuredClone(isRecord(preparedArgs) ? preparedArgs : args);
5277
5684
  const messagingContext = {
5278
5685
  config: params.hookContext?.config,
5279
5686
  currentChannelId: params.hookContext?.currentChannelId,
@@ -5298,10 +5705,41 @@ function createCodexDynamicToolBridge(params) {
5298
5705
  }));
5299
5706
  const rawResult = await Reflect.apply(tool.execute, tool, executionArgs);
5300
5707
  captureExecutionBoundary();
5301
- if (toolName === "message" && asOptionalRecord(asOptionalRecord(rawResult.details)?.messageDelivery)?.sourceReplyDelivered === true) telemetry.sourceReplyDelivered = true;
5302
- const telemetryRawResult = sanitizeToolResult(rawResult);
5303
5708
  const rawIsError = isToolResultError(rawResult);
5709
+ const messageDelivery = readEmbeddedMessageDeliveryFact(asOptionalRecord(asOptionalRecord(rawResult)?.details)?.messageDelivery);
5710
+ const messagingDelivered = isMessagingTool(toolName) && (messageDelivery ? messageDelivery.status === "settled" && (!rawIsError || messageDelivery.partialDelivery) : isDeliveredMessagingToolResult({
5711
+ toolName,
5712
+ args: executedArgs,
5713
+ result: rawResult,
5714
+ isError: rawIsError
5715
+ }));
5716
+ const mediaDelivery = messagingDelivered ? messageDelivery ?? projectPluginMessageDeliveryFact(rawResult) : void 0;
5717
+ const mediaDeliveryConfirmed = messagingDelivered && !rawIsError && executedArgs.dryRun !== true && mediaDelivery?.status === "settled" && !mediaDelivery.partialDelivery;
5718
+ const messagingTelemetryArgs = applyCurrentMessageProvider(toolName, executedArgs, params.hookContext?.currentChannelProvider);
5719
+ const messagingTarget = isMessagingTool(toolName) ? extractMessagingToolSend(toolName, messagingTelemetryArgs, messagingContext) : void 0;
5720
+ const confirmedMessagingTarget = messagingDelivered && messagingTarget ? extractMessagingToolSendResult(messagingTarget, rawResult) : messagingTarget;
5721
+ const deliveredSourceReply = messagingDelivered && isDeliveredMessageToolOnlySourceReplyResult({
5722
+ sourceReplyDeliveryMode: params.hookContext?.sourceReplyDeliveryMode,
5723
+ toolName,
5724
+ args: executedArgs,
5725
+ result: rawResult,
5726
+ isError: rawIsError,
5727
+ deliveryConfirmed: true,
5728
+ allowExplicitSourceRoute: isDeliveredMessagingToolSendToCurrentSource({
5729
+ send: confirmedMessagingTarget,
5730
+ config: params.hookContext?.config,
5731
+ currentProvider: params.hookContext?.currentChannelProvider,
5732
+ currentAccountId: params.hookContext?.turnSourceAccountId,
5733
+ currentChannelId: params.hookContext?.currentChannelId,
5734
+ currentMessagingTarget: params.hookContext?.currentMessagingTarget,
5735
+ currentThreadId: params.hookContext?.currentThreadId,
5736
+ sessionKey: params.hookContext?.sessionKey,
5737
+ deliveredPayload: rawResult
5738
+ })
5739
+ });
5740
+ if (toolName === "message" && messageDelivery?.sourceReplyDelivered) telemetry.sourceReplyDelivered = true;
5304
5741
  const rawResultFailureKind = resolveToolResultFailureKind(rawResult);
5742
+ const telemetryRawResult = sanitizeToolResult(rawResult);
5305
5743
  const middlewareResult = await middlewareRunner.applyToolResultMiddleware({
5306
5744
  threadId: call.threadId,
5307
5745
  turnId: call.turnId,
@@ -5320,7 +5758,7 @@ function createCodexDynamicToolBridge(params) {
5320
5758
  result: middlewareResult
5321
5759
  });
5322
5760
  const resultIsError = rawIsError || isToolResultError(result);
5323
- const acceptedSessionSpawn = toolName === "sessions_spawn" && !rawIsError ? normalizeAcceptedSessionSpawn(telemetryRawResult) : null;
5761
+ const acceptedSessionSpawn = toolName === "sessions_spawn" && !rawIsError ? normalizeAcceptedSessionSpawnResult(telemetryRawResult) : null;
5324
5762
  if (acceptedSessionSpawn) telemetry.acceptedSessionSpawns.push(acceptedSessionSpawn);
5325
5763
  const finalResultFailureKind = resolveToolResultFailureKind(result);
5326
5764
  const resultFailureKind = rawResultFailureKind ?? finalResultFailureKind;
@@ -5353,39 +5791,20 @@ function createCodexDynamicToolBridge(params) {
5353
5791
  toolName,
5354
5792
  toolCallOrdinal: options?.toolCallOrdinal
5355
5793
  });
5356
- const messagingTelemetryArgs = applyCurrentMessageProvider(toolName, executedArgs, params.hookContext?.currentChannelProvider);
5357
- const messagingTarget = isMessagingTool(toolName) ? extractMessagingToolSend(toolName, messagingTelemetryArgs, messagingContext) : void 0;
5358
- const confirmedMessagingTarget = !rawIsError && messagingTarget ? extractMessagingToolSendResult(messagingTarget, telemetryRawResult) : messagingTarget;
5359
- const terminalType = resultFailureKind === "blocked" ? "blocked" : resultIsError ? "error" : "completed";
5794
+ const terminalType = resultFailureKind === "blocked" ? "blocked" : resultIsError || resultFailureKind ? "error" : "completed";
5360
5795
  const contentItems = convertToolContents(result.content, toolResultMaxChars);
5361
5796
  const deliveredFrameImages = contentItems.filter((item) => item.type === "inputImage");
5362
5797
  const finalFrameImageIdentity = computerFrameImageIdentity(result.content);
5363
5798
  if (toolName === "computer" && params.computerContextEpoch?.frameToolCallId === call.callId && (deliveredFrameImages.length !== 1 || finalFrameImageIdentity === void 0 || finalFrameImageIdentity !== params.computerContextEpoch.frameImageIdentity)) invalidateComputerFrame(params.computerContextEpoch);
5364
- const response = withDiagnosticTerminalType({
5799
+ const response = {
5365
5800
  contentItems,
5366
- success: !resultIsError
5367
- }, terminalType);
5368
- withDynamicToolTranscriptDetails(response, asOptionalRecord(sanitizeToolResult(result))?.details);
5369
- withDiagnosticFailureDisposition(response, resultFailureKind);
5370
- const blocksSourceReplyTermination = hasExplicitNonSourceMessageRoute(executedArgs, params.hookContext, confirmedMessagingTarget);
5371
- const deliveredSourceReply = isDeliveredMessageToolOnlySourceReplyResult({
5372
- sourceReplyDeliveryMode: params.hookContext?.sourceReplyDeliveryMode,
5373
- toolName,
5374
- args: executedArgs,
5375
- result,
5376
- hookResult: rawResult,
5377
- isError: resultIsError,
5378
- allowExplicitSourceRoute: !blocksSourceReplyTermination
5379
- });
5380
- const receiptConfirmedSourceReply = params.hookContext?.sourceReplyDeliveryMode === "message_tool_only" && toolName === "message" && normalizeRouteToken(typeof executedArgs.action === "string" ? executedArgs.action : void 0) === "reply" && !resultIsError && !blocksSourceReplyTermination && isDeliveredMessagingToolResult({
5381
- toolName,
5382
- args: executedArgs,
5383
- result,
5384
- hookResult: rawResult,
5385
- isError: resultIsError
5386
- }) && (replyReceiptMatchesCurrentMessage(rawResult, params.hookContext) || replyReceiptMatchesCurrentMessage(result, params.hookContext));
5801
+ success: !resultIsError,
5802
+ diagnosticTerminalType: terminalType,
5803
+ diagnosticTerminalReason: resultFailureKind === "blocked" ? void 0 : resultFailureKind,
5804
+ transcriptDetails: asOptionalRecord(sanitizeToolResult(result))?.details
5805
+ };
5387
5806
  const toolConfirmedSourceReply = params.hookContext?.sourceReplyDeliveryMode === "message_tool_only" && toolName === "message" && !resultIsError && (rawResult.terminate === true || result.terminate === true);
5388
- const confirmedSourceReply = params.hookContext?.sourceReplyDeliveryMode === "message_tool_only" && toolName === "message" && (toolConfirmedSourceReply || deliveredSourceReply || receiptConfirmedSourceReply);
5807
+ const confirmedSourceReply = params.hookContext?.sourceReplyDeliveryMode === "message_tool_only" && toolName === "message" && (toolConfirmedSourceReply || deliveredSourceReply);
5389
5808
  const sourceReplyFinal = confirmedSourceReply ? executedArgs.final !== false : void 0;
5390
5809
  const autoDeliveryTtsMediaUrls = getCoreTtsToolResultMediaUrls(rawResult);
5391
5810
  collectToolTelemetry({
@@ -5396,23 +5815,27 @@ function createCodexDynamicToolBridge(params) {
5396
5815
  telemetry,
5397
5816
  signal,
5398
5817
  isError: resultIsError,
5818
+ messagingDelivered,
5819
+ mediaDeliveryConfirmed,
5820
+ sourcePathsByStagedPath: preparedMessageMedia?.sourcePathsByStagedPath,
5399
5821
  autoDeliveryTtsMediaUrls,
5400
5822
  coreTtsToolResult: autoDeliveryTtsMediaUrls?.length ? rawResult : void 0,
5401
5823
  messagingTarget: confirmedMessagingTarget,
5402
- sourceReplyFinal
5824
+ sourceReplyFinal,
5825
+ trustedLocalMediaToolNames: pluginLocalMediaTrustByToolName.get(toolName)
5403
5826
  });
5404
- if (deliveredSourceReply || receiptConfirmedSourceReply || toolConfirmedSourceReply) telemetry.didDeliverSourceReplyViaMessageTool = true;
5827
+ if (deliveredSourceReply || toolConfirmedSourceReply) telemetry.didDeliverSourceReplyViaMessageTool = true;
5405
5828
  const continuesSourceReplyProgress = confirmedSourceReply && sourceReplyFinal === false;
5406
- withDynamicToolTermination(response, (rawResult.terminate === true || result.terminate === true) && !continuesSourceReplyProgress || isToolResultYield(rawResult) || isToolResultYield(result) || confirmedSourceReply && sourceReplyFinal === true);
5829
+ response.terminate = (rawResult.terminate === true || result.terminate === true) && !continuesSourceReplyProgress || isToolResultYield(rawResult) || isToolResultYield(result) || confirmedSourceReply && sourceReplyFinal === true || void 0;
5407
5830
  const asyncStarted = isAsyncStartedToolResult(rawResult) || isAsyncStartedToolResult(result);
5408
- withDynamicToolAsyncStarted(response, asyncStarted);
5831
+ response.asyncStarted = asyncStarted || void 0;
5409
5832
  const replaySafe = executionPrevented || !asyncStarted && isReplaySafeToolInstance(toolEntry.tool) && isReplaySafeToolCall(toolName, executedArgs);
5410
5833
  copyInternalToolResultState(rawResult, response);
5411
- return withDynamicToolExecutionState(response, {
5412
- executedArguments: executedArgs,
5413
- executionStarted: didStartExecution && !executionPrevented,
5414
- sideEffectEvidence: !replaySafe
5415
- });
5834
+ response.executedArguments = executedArgs;
5835
+ response.executionStarted = didStartExecution && !executionPrevented;
5836
+ response.replaySafe = replaySafe;
5837
+ response.sideEffectEvidence = !replaySafe || void 0;
5838
+ return response;
5416
5839
  } catch (error) {
5417
5840
  const trustedNoStart = consumeTrustedToolNoStartError(error);
5418
5841
  executionPrevented ||= trustedNoStart;
@@ -5420,6 +5843,8 @@ function createCodexDynamicToolBridge(params) {
5420
5843
  if (toolName === "computer" && params.computerContextEpoch?.frameToolCallId === call.callId) invalidateComputerFrame(params.computerContextEpoch);
5421
5844
  const executionDisposition = getBeforeToolCallFailureDisposition(error) ?? (signal.aborted ? resolveCodexToolAbortTerminalReason(signal) : resolveToolExecutionErrorKind(error));
5422
5845
  const errorMessage = formatToolExecutionErrorMessage(error, "OpenClaw dynamic tool call failed.");
5846
+ const operatorHint = readToolOperatorHint(error);
5847
+ if (operatorHint) embeddedAgentLog.error(`[tools] ${toolName} failed: ${errorMessage} ${operatorHint}`);
5423
5848
  executionPrevented = executionPrevented || consumePreExecutionBlockedToolCall(call.callId, toolResultHookContext.runId);
5424
5849
  const failedResult = failedToolResult(errorMessage, executionDisposition);
5425
5850
  finalizeToolTerminalPresentation({
@@ -5445,17 +5870,19 @@ function createCodexDynamicToolBridge(params) {
5445
5870
  startedAt
5446
5871
  });
5447
5872
  const replaySafe = !didStartExecution || executionPrevented || isReplaySafeToolInstance(toolEntry.tool) && isReplaySafeToolCall(toolName, executedArgs);
5448
- return withDynamicToolExecutionState(withDiagnosticFailureDisposition({
5873
+ return {
5449
5874
  contentItems: [{
5450
5875
  type: "inputText",
5451
5876
  text: errorMessage
5452
5877
  }],
5453
- success: false
5454
- }, executionDisposition), {
5878
+ success: false,
5879
+ diagnosticTerminalType: executionDisposition === "blocked" ? "blocked" : "error",
5880
+ diagnosticTerminalReason: executionDisposition === "blocked" ? void 0 : executionDisposition,
5455
5881
  executedArguments: executedArgs,
5456
5882
  executionStarted: didStartExecution && !executionPrevented,
5457
- sideEffectEvidence: didStartExecution && !replaySafe
5458
- });
5883
+ replaySafe,
5884
+ sideEffectEvidence: didStartExecution && !replaySafe || void 0
5885
+ };
5459
5886
  } finally {
5460
5887
  if (executionSnapshotStates.get(call.callId) === executionSnapshotState && (executionSnapshotState.consumed || !executionSnapshotState.retainAfterCompletion)) executionSnapshotStates.delete(call.callId);
5461
5888
  consumeAdjustedParamsForToolCall(call.callId, toolResultHookContext.runId);
@@ -5508,18 +5935,6 @@ function notifyAgentToolResult(observer, toolName, result, isError) {
5508
5935
  embeddedAgentLog.warn(`onAgentToolResult handler failed: tool=${toolName} error=${message}`);
5509
5936
  }
5510
5937
  }
5511
- function failedToolResult(message, status = "failed") {
5512
- return {
5513
- content: [{
5514
- type: "text",
5515
- text: message
5516
- }],
5517
- details: {
5518
- status,
5519
- error: message
5520
- }
5521
- };
5522
- }
5523
5938
  function reportQuarantinedDynamicTools(params) {
5524
5939
  if (params.tools.length === 0) return;
5525
5940
  embeddedAgentLog.warn(`codex app-server quarantined ${params.tools.length} unsupported dynamic tool ${params.tools.length === 1 ? "definition" : "definitions"}: ${params.tools.map((tool) => tool.tool).join(", ")}; retained ${params.availableToolCount} available and ${params.registeredToolCount} registered tools`, {
@@ -5564,16 +5979,15 @@ function composeAbortSignals(...signals) {
5564
5979
  return AbortSignal.any(activeSignals);
5565
5980
  }
5566
5981
  function collectToolTelemetry(params) {
5567
- if (params.isError) return;
5568
5982
  if (!params.isError && params.toolName === "cron" && isCronAddAction(params.args)) params.telemetry.successfulCronAdds = (params.telemetry.successfulCronAdds ?? 0) + 1;
5569
5983
  if (!params.isError && params.toolName === HEARTBEAT_RESPONSE_TOOL_NAME) {
5570
5984
  const response = normalizeHeartbeatToolResponse(params.result?.details);
5571
5985
  if (response) params.telemetry.heartbeatToolResponse = response;
5572
5986
  }
5573
- if (params.result && !params.signal.aborted) {
5987
+ if (!params.isError && params.result && !params.signal.aborted) {
5574
5988
  const media = extractToolResultMediaArtifact(params.result);
5575
5989
  if (media) {
5576
- const mediaUrls = filterToolResultMediaUrls(params.toolName, media.mediaUrls, params.mediaTrustResult ?? params.result);
5990
+ const mediaUrls = filterToolResultMediaUrls(params.toolName, media.mediaUrls, params.coreTtsToolResult ?? params.mediaTrustResult ?? params.result, params.trustedLocalMediaToolNames);
5577
5991
  const seen = new Set(params.telemetry.toolMediaUrls);
5578
5992
  const autoDeliveryMediaUrls = new Set(params.telemetry.toolAutoDeliveryMediaUrls);
5579
5993
  const rawAutoDeliveryMediaUrls = new Set(params.autoDeliveryTtsMediaUrls);
@@ -5593,24 +6007,20 @@ function collectToolTelemetry(params) {
5593
6007
  if (media.audioAsVoice) params.telemetry.toolAudioAsVoice = true;
5594
6008
  }
5595
6009
  }
5596
- if (!isMessagingTool(params.toolName)) return;
5597
- const isMessagingSendAction = isMessagingToolSendAction(params.toolName, params.args);
5598
- if (!isMessagingSendAction && !params.messagingTarget) return;
5599
- if (!isMessagingSendAction && !isDeliveredMessagingToolResult({
5600
- toolName: params.toolName,
5601
- args: params.args,
5602
- result: params.result,
5603
- hookResult: params.mediaTrustResult,
5604
- isError: params.isError
5605
- })) return;
6010
+ if (!params.messagingDelivered) return;
5606
6011
  params.telemetry.didSendViaMessagingTool = true;
5607
- const sourceReplyPayload = extractInternalSourceReplyPayload(params.result?.details);
5608
- if (sourceReplyPayload) {
6012
+ if (asOptionalRecord(asOptionalRecord(params.mediaTrustResult)?.details)?.sourceReplySink === "internal-ui") {
6013
+ const sourceReplyPayload = extractMessagingToolSourceReplyPayload(params.result);
6014
+ if (!sourceReplyPayload) return;
5609
6015
  const record = {
5610
6016
  ...sourceReplyPayload,
5611
6017
  ...params.sourceReplyFinal !== void 0 ? { sourceReplyFinal: params.sourceReplyFinal } : {}
5612
6018
  };
5613
6019
  params.telemetry.messagingToolSourceReplyPayloads.push(record);
6020
+ if (params.mediaDeliveryConfirmed) params.telemetry.confirmedMediaDeliveries.push({
6021
+ kind: "sourceReply",
6022
+ sourceUrls: resolveCodexMediaSourceUrls(collectCodexMessageMediaUrls(record), params.sourcePathsByStagedPath)
6023
+ });
5614
6024
  return record;
5615
6025
  }
5616
6026
  const text = readFirstString(params.args, [
@@ -5620,7 +6030,7 @@ function collectToolTelemetry(params) {
5620
6030
  "content"
5621
6031
  ]);
5622
6032
  if (text) params.telemetry.messagingToolSentTexts.push(text);
5623
- const mediaUrls = collectMediaUrls(params.args);
6033
+ const mediaUrls = params.mediaDeliveryConfirmed ? collectCodexMessageMediaUrls(params.args) : [];
5624
6034
  params.telemetry.messagingToolSentMediaUrls.push(...mediaUrls);
5625
6035
  const record = {
5626
6036
  ...params.messagingTarget ?? {
@@ -5643,27 +6053,13 @@ function collectToolTelemetry(params) {
5643
6053
  ...params.sourceReplyFinal !== void 0 ? { sourceReplyFinal: params.sourceReplyFinal } : {}
5644
6054
  };
5645
6055
  params.telemetry.messagingToolSentTargets.push(record);
6056
+ if (mediaUrls.length > 0) params.telemetry.confirmedMediaDeliveries.push({
6057
+ kind: "outbound",
6058
+ target: record,
6059
+ sourceUrls: resolveCodexMediaSourceUrls(mediaUrls, params.sourcePathsByStagedPath)
6060
+ });
5646
6061
  return record;
5647
6062
  }
5648
- function extractInternalSourceReplyPayload(details) {
5649
- if (!isRecord(details) || details.sourceReplySink !== "internal-ui") return;
5650
- const rawPayload = details.sourceReply;
5651
- if (!isRecord(rawPayload)) return;
5652
- const text = readFirstString(rawPayload, ["text", "message"]);
5653
- const mediaUrls = collectMediaUrls(rawPayload);
5654
- const mediaUrl = typeof rawPayload.mediaUrl === "string" && rawPayload.mediaUrl.trim() ? rawPayload.mediaUrl.trim() : mediaUrls[0];
5655
- const payload = {
5656
- ...text ? { text } : {},
5657
- ...mediaUrl ? { mediaUrl } : {},
5658
- ...mediaUrls.length > 0 ? { mediaUrls } : {},
5659
- ...rawPayload.audioAsVoice === true ? { audioAsVoice: true } : {},
5660
- ...isRecord(rawPayload.presentation) ? { presentation: rawPayload.presentation } : {},
5661
- ...isRecord(rawPayload.interactive) ? { interactive: rawPayload.interactive } : {},
5662
- ...isRecord(rawPayload.channelData) ? { channelData: rawPayload.channelData } : {},
5663
- ...typeof details.idempotencyKey === "string" && details.idempotencyKey.trim() ? { idempotencyKey: details.idempotencyKey.trim() } : {}
5664
- };
5665
- return text || mediaUrls.length > 0 || payload.presentation || payload.interactive ? payload : void 0;
5666
- }
5667
6063
  function isToolResultYield(result) {
5668
6064
  const details = result.details;
5669
6065
  if (!isRecord(details) || typeof details.status !== "string") return false;
@@ -5673,42 +6069,6 @@ function isAsyncStartedToolResult(result) {
5673
6069
  const details = result.details;
5674
6070
  return isRecord(details) && details.async === true && details.status === "started";
5675
6071
  }
5676
- function withDiagnosticTerminalType(response, terminalType) {
5677
- Object.defineProperty(response, "diagnosticTerminalType", {
5678
- configurable: true,
5679
- enumerable: false,
5680
- value: terminalType
5681
- });
5682
- return response;
5683
- }
5684
- function withDiagnosticFailureDisposition(response, disposition) {
5685
- if (!disposition) return response;
5686
- withDiagnosticTerminalType(response, disposition === "blocked" ? "blocked" : "error");
5687
- if (disposition !== "blocked") Object.defineProperty(response, "diagnosticTerminalReason", {
5688
- configurable: true,
5689
- enumerable: false,
5690
- value: disposition
5691
- });
5692
- return response;
5693
- }
5694
- function withDynamicToolTermination(response, terminate) {
5695
- if (!terminate) return response;
5696
- Object.defineProperty(response, "terminate", {
5697
- configurable: true,
5698
- enumerable: false,
5699
- value: true
5700
- });
5701
- return response;
5702
- }
5703
- function withDynamicToolAsyncStarted(response, asyncStarted) {
5704
- if (!asyncStarted) return response;
5705
- Object.defineProperty(response, "asyncStarted", {
5706
- configurable: true,
5707
- enumerable: false,
5708
- value: true
5709
- });
5710
- return response;
5711
- }
5712
6072
  function normalizeToolResultMaxChars(maxChars) {
5713
6073
  return typeof maxChars === "number" && Number.isFinite(maxChars) && maxChars > 0 ? Math.floor(maxChars) : DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS;
5714
6074
  }
@@ -5806,53 +6166,9 @@ function readFirstString(record, keys) {
5806
6166
  if (typeof value === "number" && Number.isFinite(value)) return String(value);
5807
6167
  }
5808
6168
  }
5809
- function collectMediaUrls(record) {
5810
- const urls = [];
5811
- const pushMediaUrl = (value) => {
5812
- if (typeof value === "string" && value.trim()) urls.push(value.trim());
5813
- };
5814
- const pushAttachment = (value) => {
5815
- if (!value || typeof value !== "object" || Array.isArray(value)) return;
5816
- const attachment = value;
5817
- for (const key of [
5818
- "media",
5819
- "mediaUrl",
5820
- "path",
5821
- "filePath",
5822
- "fileUrl",
5823
- "url"
5824
- ]) pushMediaUrl(attachment[key]);
5825
- };
5826
- for (const key of [
5827
- "media",
5828
- "mediaUrl",
5829
- "media_url",
5830
- "path",
5831
- "filePath",
5832
- "fileUrl",
5833
- "imageUrl",
5834
- "image_url"
5835
- ]) {
5836
- const value = record[key];
5837
- pushMediaUrl(value);
5838
- }
5839
- for (const key of [
5840
- "mediaUrls",
5841
- "media_urls",
5842
- "imageUrls",
5843
- "image_urls"
5844
- ]) {
5845
- const value = record[key];
5846
- if (!Array.isArray(value)) continue;
5847
- for (const entry of value) pushMediaUrl(entry);
5848
- }
5849
- const attachments = record.attachments;
5850
- if (Array.isArray(attachments)) for (const attachment of attachments) pushAttachment(attachment);
5851
- return urls;
5852
- }
5853
6169
  function isCronAddAction(args) {
5854
6170
  const action = args.action;
5855
6171
  return typeof action === "string" && action.trim().toLowerCase() === "add";
5856
6172
  }
5857
6173
  //#endregion
5858
- export { shouldRequireCodexSandboxExecServerEnvironment as C, shouldEnableCodexAppServerNativeToolSurface as S, filterCodexVisionTools as T, resolveCodexAppServerHookChannelId as _, emitDynamicToolStartedDiagnostic as a, resolveCodexNodePlacementToolConstructionPlan as b, ensureCodexSandboxExecServerEnvironment as c, CodexNativeToolLifecycleProjector as d, buildDynamicTools as f, resolveCodexAppServerExecutionCwd as g, formatCodexDynamicToolBuildStageSummary as h, emitDynamicToolErrorDiagnostic as i, releaseCodexSandboxExecServerEnvironment as l, disableCodexPluginThreadConfig as m, projectCodexExecutableDynamicTools as n, emitDynamicToolTerminalDiagnostic as o, createCodexDynamicToolBuildStageTracker as p, routeCodexAppServerElicitationRequest as r, handleCodexAppServerApprovalRequest as s, createCodexDynamicToolBridge as t, readBoundedCodexRemoteWorkspaceFile as u, resolveCodexExternalSandboxPolicyForOpenClawSandbox as v, shouldWarnCodexDynamicToolBuildStageSummary as w, resolveCodexSandboxEnvironmentSelection as x, resolveCodexMessageToolProvider as y };
6174
+ export { shouldEnableCodexAppServerNativeToolSurface as C, mapCodexAppServerRemoteWorkspacePath as E, resolveCodexSandboxEnvironmentSelection as S, shouldWarnCodexDynamicToolBuildStageSummary as T, formatCodexDynamicToolBuildStageSummary as _, emitDynamicToolStartedDiagnostic as a, resolveCodexExternalSandboxPolicyForOpenClawSandbox as b, ensureCodexSandboxExecServerEnvironment as c, hasCodexNativeBackgroundProcesses as d, readBoundedCodexRemoteWorkspaceFile as f, disableCodexPluginThreadConfig as g, createCodexDynamicToolBuildStageTracker as h, emitDynamicToolErrorDiagnostic as i, releaseCodexSandboxExecServerEnvironment as l, buildDynamicTools as m, projectCodexExecutableDynamicTools as n, emitDynamicToolTerminalDiagnostic as o, CodexNativeToolLifecycleProjector as p, routeCodexAppServerElicitationRequest as r, handleCodexAppServerApprovalRequest as s, createCodexDynamicToolBridge as t, CodexNativeProcessAuthority as u, resolveCodexAppServerExecutionCwd as v, shouldRequireCodexSandboxExecServerEnvironment as w, resolveCodexMessageToolProvider as x, resolveCodexAppServerHookChannelId as y };