@openclaw/codex 2026.8.1-beta.2 → 2026.8.1-beta.3

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