@openclaw/codex 2026.7.2-beta.4 → 2026.7.2-beta.7

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 (40) hide show
  1. package/dist/{app-server-policy-BO3jYBkv.js → app-server-policy-BSaO2JIT.js} +1 -1
  2. package/dist/{attempt-notifications-DdmnBMUp.js → attempt-notifications-AP-a-Q5k.js} +1 -1
  3. package/dist/{bounded-turn-CaQL96kg.js → bounded-turn-D2ZAgZVz.js} +54 -24
  4. package/dist/{capabilities-CLICvTxf.js → capabilities-BFDTVYr4.js} +1 -1
  5. package/dist/cli-metadata.js +1 -1
  6. package/dist/{command-formatters-DgADqnHa.js → command-formatters-C336eRVS.js} +2 -2
  7. package/dist/{command-handlers-l_lVPNlB.js → command-handlers-DAHUSry9.js} +1233 -1222
  8. package/dist/{compact-CC5KhBzX.js → compact-D_uvHurS.js} +8 -8
  9. package/dist/{computer-use-Ip1-QGa-.js → computer-use-CYEc8U3w.js} +123 -115
  10. package/dist/{conversation-control-sNPsbTCP.js → conversation-control-xsIeXbo4.js} +6 -6
  11. package/dist/doctor-contract-api.js +24 -86
  12. package/dist/{dynamic-tools-CQI3UwqY.js → dynamic-tools-D2gYkSBO.js} +482 -91
  13. package/dist/effective-mcp-catalog-Bbd98Inw.js +114 -0
  14. package/dist/{event-projector-usage-D6tE7_qD.js → event-projector-usage-Xp4AlR2k.js} +20 -3
  15. package/dist/harness.js +19 -14
  16. package/dist/index.js +292 -328
  17. package/dist/media-understanding-provider.js +6 -1
  18. package/dist/{models-BNeMlHgg.js → models-BBMKU4jA.js} +2 -2
  19. package/dist/{notification-correlation-BGLixv5_.js → notification-correlation-CU6_AnOb.js} +3 -3
  20. package/dist/{plugin-app-cache-key-CvzvmBRz.js → plugin-app-cache-key-DztPafOA.js} +135 -42
  21. package/dist/{rate-limits-D0IJT-a6.js → rate-limits-CU-gVCCS.js} +1 -1
  22. package/dist/{request-BTccUTD3.js → request-qKh331yu.js} +3 -3
  23. package/dist/{run-attempt--858NXDl.js → run-attempt-CY9WlDLc.js} +1060 -402
  24. package/dist/{runtime-artifact-C2ITXdhM.js → runtime-artifact-CmtG_t1L.js} +90 -24
  25. package/dist/{sandbox-guard-D1Wky__S.js → sandbox-guard-NuhpXmPB.js} +4 -0
  26. package/dist/{session-binding-ZSMX1omP.js → session-binding-BXpo9ymA.js} +1006 -891
  27. package/dist/{session-catalog-DsKaezgR.js → session-catalog-DyOgR4WH.js} +289 -56
  28. package/dist/{session-cli-CYJTfRLG.js → session-cli-2PZaFLRh.js} +2 -2
  29. package/dist/{session-upstream-marker-D7XlI9us.js → session-upstream-marker-B6U7tLw5.js} +1 -1
  30. package/dist/{settled-turn-finalizer-DHltNgtF.js → settled-turn-finalizer-CXY2Z9dT.js} +19 -6
  31. package/dist/{shared-client-wmgNFNYp.js → shared-client-DTbvH-XV.js} +1993 -1720
  32. package/dist/{side-question-BfFZCsJ-.js → side-question-_ulBk1AL.js} +12 -10
  33. package/dist/{thread-lifecycle-cqrOlJkq.js → thread-lifecycle-DF53ogiA.js} +980 -346
  34. package/dist/{transcript-mirror-2p7kl6e4.js → transcript-mirror-DzsdNCf5.js} +80 -41
  35. package/dist/{upstream-session-fork-vPjwkT1i.js → upstream-session-fork-Dy4Xvuoj.js} +3 -3
  36. package/dist/{usage-DM_I3mPT.js → usage-CQ6GhD5-.js} +3 -3
  37. package/dist/{web-search-provider.runtime-Bx6HAc_k.js → web-search-provider.runtime-CCFhg2z5.js} +3 -3
  38. package/package.json +6 -8
  39. package/npm-shrinkwrap.json +0 -202
  40. package/dist/{auth-binding-C1czuHPA.js → auth-binding-B4FfbcCg.js} +1 -1
@@ -1,19 +1,147 @@
1
- import { G as isCodexAppServerPrewriteRequestCancellationError, H as isCodexAppServerConnectionClosedError, Q as isJsonObject, R as CodexAppServerRpcError, Z as flattenCodexDynamicToolFunctions, d as retireSharedCodexAppServerClientIfCurrent, n as clearSharedCodexAppServerClientIfCurrentAndUnclaimed, o as isCodexAppServerStartSelectionChangedError, q as resolveCodexAppServerClientInstanceId, t as clearSharedCodexAppServerClientIfCurrent, z as getCodexAppServerClientInstanceId } from "./shared-client-wmgNFNYp.js";
2
- import { E as resolveCodexPluginsPolicy, a as hashCodexAppServerBindingFingerprint, c as reclaimCurrentCodexSessionGeneration, d as CODEX_PLUGINS_MARKETPLACE_NAME, f as CODEX_PLUGINS_WORKSPACE_MARKETPLACE_NAME, g as codexSandboxPolicyForTurn, i as createCodexSessionGenerationSupersededError, l as sessionBindingIdentity, n as assertCodexBindingMayBeReplaced, o as isCodexAppServerNativeAuthProfile, s as normalizeCodexAppServerBindingModelProvider } from "./session-binding-ZSMX1omP.js";
3
- import { _ as assertCodexThreadStartResponse, a as projectBoundedCodexThreadHistory, g as assertCodexThreadResumeResponse, h as assertCodexThreadForkResponse } from "./transcript-mirror-2p7kl6e4.js";
4
- import { a as serializeCodexAppInventoryError, i as defaultCodexAppInventoryCache, t as buildCodexAppServerConnectionFingerprint } from "./plugin-app-cache-key-CvzvmBRz.js";
1
+ import { D as resolveCodexAppServerClientInstanceId, O as consumeCodexAppServerLiveThread, S as isCodexAppServerConnectionClosedError, T as isCodexAppServerPrewriteRequestCancellationError, U as flattenCodexDynamicToolFunctions, W as isJsonObject, n as clearSharedCodexAppServerClientIfCurrentAndUnclaimed, p as retireSharedCodexAppServerClientIfCurrent, s as isCodexAppServerStartSelectionChangedError, t as clearSharedCodexAppServerClientIfCurrent, v as CodexAppServerRpcError, y as getCodexAppServerClientInstanceId } from "./shared-client-DTbvH-XV.js";
2
+ import { M as CODEX_PLUGINS_MARKETPLACE_NAME, N as CODEX_PLUGINS_WORKSPACE_MARKETPLACE_NAME, a as hashCodexAppServerBindingFingerprint, c as reclaimCurrentCodexSessionGeneration, d as sessionBindingIdentity, i as createCodexSessionGenerationSupersededError, k as resolveCodexPluginsPolicy, m as codexSandboxPolicyForTurn, n as assertCodexBindingMayBeReplaced, o as isCodexAppServerNativeAuthProfile, s as normalizeCodexAppServerBindingModelProvider } from "./session-binding-BXpo9ymA.js";
3
+ import { _ as assertCodexThreadStartResponse, a as projectBoundedCodexThreadHistory, g as assertCodexThreadResumeResponse, h as assertCodexThreadForkResponse } from "./transcript-mirror-DzsdNCf5.js";
4
+ import { a as serializeCodexAppInventoryError, i as defaultCodexAppInventoryCache, t as buildCodexAppServerConnectionFingerprint } from "./plugin-app-cache-key-DztPafOA.js";
5
5
  import { t as isIncognitoSessionKey } from "./incognito-session-ET_ufDH-.js";
6
- import { sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
6
+ import fs from "node:fs/promises";
7
+ import path from "node:path";
8
+ import { SKILL_WORKSHOP_TOOL_NAME, buildSkillWorkshopPromptSection, embeddedAgentLog, formatErrorMessage, isActiveHarnessContextEngine, isHostScopedAgentToolActive } from "openclaw/plugin-sdk/agent-harness-runtime";
7
9
  import * as crypto$1 from "node:crypto";
8
10
  import crypto from "node:crypto";
11
+ import os from "node:os";
9
12
  import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
10
- import { SKILL_WORKSHOP_TOOL_NAME, buildSkillWorkshopPromptSection, embeddedAgentLog, formatErrorMessage, isActiveHarnessContextEngine, isHostScopedAgentToolActive } from "openclaw/plugin-sdk/agent-harness-runtime";
13
+ import { sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
11
14
  import { redactSensitiveFieldValue, redactToolPayloadText } from "openclaw/plugin-sdk/logging-core";
15
+ import { resolveRequiredHomeDir, resolveStateDir } from "openclaw/plugin-sdk/state-paths";
16
+ import { listRegisteredPluginAgentPromptGuidance } from "openclaw/plugin-sdk/plugin-runtime";
12
17
  import { buildCodexUserMcpServersThreadConfigPatchForRuntime } from "openclaw/plugin-sdk/codex-mcp-projection";
13
18
  import { isDiagnosticFlagEnabled } from "openclaw/plugin-sdk/diagnostic-runtime";
14
- import { listRegisteredPluginAgentPromptGuidance } from "openclaw/plugin-sdk/plugin-runtime";
15
19
  import { GPT5_HEARTBEAT_PROMPT_OVERLAY } from "openclaw/plugin-sdk/provider-model-shared";
16
20
  import { INLINE_IMAGE_DATA_URL_PREFIX, sanitizeInlineImageDataUrl } from "openclaw/plugin-sdk/inline-image-data-url-runtime";
21
+ //#region extensions/codex/src/app-server/attempt-client-cleanup.ts
22
+ /**
23
+ * Best-effort cleanup helpers for Codex app-server startup attempts and turns.
24
+ */
25
+ /** Timeout for best-effort app-server turn interruption during cleanup. */
26
+ const CODEX_APP_SERVER_INTERRUPT_TIMEOUT_MS = 5e3;
27
+ /** Timeout for best-effort thread unsubscribe during cleanup. */
28
+ const CODEX_APP_SERVER_UNSUBSCRIBE_TIMEOUT_MS = 5e3;
29
+ /** Raised when a thread subscription may be live on a client OpenClaw no longer controls. */
30
+ var CodexAppServerUnsafeSubscriptionError = class extends Error {
31
+ constructor(message, options) {
32
+ super(message, options);
33
+ this.name = "CodexAppServerUnsafeSubscriptionError";
34
+ }
35
+ };
36
+ function isCodexAppServerUnsafeSubscriptionError(error) {
37
+ return error instanceof CodexAppServerUnsafeSubscriptionError;
38
+ }
39
+ /** Asserts Codex resumed the exact thread this attempt subscribed to. */
40
+ function assertCodexThreadResumeSubscription(requestedThreadId, returnedThreadId) {
41
+ if (returnedThreadId !== requestedThreadId) throw new CodexAppServerUnsafeSubscriptionError(`Codex thread/resume returned ${returnedThreadId} for ${requestedThreadId}`);
42
+ }
43
+ async function closeClientAndWaitIfAvailable(client) {
44
+ const closeable = client;
45
+ if (typeof closeable.closeAndWait === "function") {
46
+ await closeable.closeAndWait();
47
+ return;
48
+ }
49
+ closeable.close?.();
50
+ }
51
+ async function closeCodexStartupClientBestEffort(client) {
52
+ if (!client) return;
53
+ const unclaimedSharedClient = clearSharedCodexAppServerClientIfCurrentAndUnclaimed(client);
54
+ if (unclaimedSharedClient.closed) {
55
+ await closeClientAndWaitIfAvailable(client);
56
+ return;
57
+ }
58
+ if (unclaimedSharedClient.found) {
59
+ if (retireSharedCodexAppServerClientIfCurrent(client)?.closed) await closeClientAndWaitIfAvailable(client);
60
+ return;
61
+ }
62
+ const retiredSharedClient = retireSharedCodexAppServerClientIfCurrent(client);
63
+ if (retiredSharedClient) {
64
+ if (retiredSharedClient.closed) await closeClientAndWaitIfAvailable(client);
65
+ return;
66
+ }
67
+ if (clearSharedCodexAppServerClientIfCurrent(client)) {
68
+ await closeClientAndWaitIfAvailable(client);
69
+ return;
70
+ }
71
+ await closeClientAndWaitIfAvailable(client);
72
+ }
73
+ /** Sends a turn interrupt without blocking abort cleanup on app-server errors. */
74
+ function interruptCodexTurnBestEffort(client, params) {
75
+ interruptCodexTurnAndWaitBestEffort(client, params);
76
+ }
77
+ /** Sends a bounded turn interrupt and waits for Codex to confirm terminal abort handling. */
78
+ async function interruptCodexTurnAndWaitBestEffort(client, params) {
79
+ const requestOptions = params.timeoutMs && Number.isFinite(params.timeoutMs) && params.timeoutMs > 0 ? { timeoutMs: params.timeoutMs } : void 0;
80
+ const requestParams = {
81
+ threadId: params.threadId,
82
+ turnId: params.turnId
83
+ };
84
+ try {
85
+ await (requestOptions ? client.request("turn/interrupt", requestParams, requestOptions) : client.request("turn/interrupt", requestParams));
86
+ } catch (error) {
87
+ embeddedAgentLog.debug("codex app-server turn interrupt failed during abort", { error });
88
+ }
89
+ }
90
+ /** Unsubscribes from a thread while swallowing cleanup-only failures. */
91
+ async function unsubscribeCodexThreadBestEffort(client, params) {
92
+ try {
93
+ await client.request("thread/unsubscribe", { threadId: params.threadId }, { timeoutMs: params.timeoutMs });
94
+ return true;
95
+ } catch (error) {
96
+ embeddedAgentLog.debug("codex app-server thread unsubscribe cleanup failed", {
97
+ threadId: params.threadId,
98
+ error
99
+ });
100
+ return false;
101
+ }
102
+ }
103
+ /**
104
+ * Retires the shared client after a timed-out turn so later runs do not reuse a
105
+ * potentially wedged app-server connection.
106
+ */
107
+ async function retireCodexAppServerClientAfterTimedOutTurn(client, params) {
108
+ const retiredSharedClient = retireSharedCodexAppServerClientIfCurrent(client, { failActiveLeases: params.suspectPhysicalClient });
109
+ const detachedSharedClient = Boolean(retiredSharedClient);
110
+ if (!(params.suspectPhysicalClient && (retiredSharedClient?.closed ?? false))) {
111
+ interruptCodexTurnBestEffort(client, {
112
+ threadId: params.threadId,
113
+ turnId: params.turnId,
114
+ timeoutMs: CODEX_APP_SERVER_INTERRUPT_TIMEOUT_MS
115
+ });
116
+ await unsubscribeCodexThreadBestEffort(client, {
117
+ threadId: params.threadId,
118
+ timeoutMs: CODEX_APP_SERVER_UNSUBSCRIBE_TIMEOUT_MS
119
+ });
120
+ }
121
+ let closedClient = retiredSharedClient?.closed ?? false;
122
+ if (!detachedSharedClient) {
123
+ const close = client.close;
124
+ if (typeof close === "function") try {
125
+ close.call(client);
126
+ closedClient = true;
127
+ } catch (error) {
128
+ embeddedAgentLog.debug("codex app-server client close failed during timeout cleanup", {
129
+ threadId: params.threadId,
130
+ turnId: params.turnId,
131
+ error
132
+ });
133
+ }
134
+ }
135
+ embeddedAgentLog.warn("codex app-server client retired after timed-out turn", {
136
+ threadId: params.threadId,
137
+ turnId: params.turnId,
138
+ reason: params.reason,
139
+ detachedSharedClient,
140
+ closedClient,
141
+ activeSharedClientLeases: retiredSharedClient?.activeLeases ?? 0
142
+ });
143
+ }
144
+ //#endregion
17
145
  //#region extensions/codex/src/app-server/event-projector-values.ts
18
146
  function readString(record, key) {
19
147
  const value = record[key];
@@ -96,6 +224,7 @@ function readItem(value) {
96
224
  * plugin-owned apps can be exposed to a native Codex thread.
97
225
  */
98
226
  const CODEX_PLUGINS_REMOTE_MARKETPLACE_NAME = `${CODEX_PLUGINS_MARKETPLACE_NAME}-remote`;
227
+ const CODEX_PLUGINS_API_MARKETPLACE_NAME = "openai-api-curated";
99
228
  /** Reads configured Codex plugin state and maps owned apps to readiness diagnostics. */
100
229
  async function readCodexPluginInventory(params) {
101
230
  const policy = params.policy ?? resolveCodexPluginsPolicy(params.pluginConfig);
@@ -108,21 +237,11 @@ async function readCodexPluginInventory(params) {
108
237
  }]
109
238
  };
110
239
  const appInventory = readCachedAppInventory(params);
111
- const curatedListed = await listCodexPluginMetadata(params, "curated-global", {});
112
- const shouldListWorkspacePlugins = policy.pluginPolicies.some((pluginPolicy) => pluginPolicy.enabled && pluginPolicy.marketplaceName === "workspace-directory");
113
- let workspaceListResult;
114
- if (shouldListWorkspacePlugins) try {
115
- workspaceListResult = {
116
- kind: "listed",
117
- response: await listCodexPluginMetadata(params, "workspace-directory", {
118
- cwds: [],
119
- marketplaceKinds: [CODEX_PLUGINS_WORKSPACE_MARKETPLACE_NAME]
120
- })
121
- };
122
- } catch (error) {
123
- if (!(error instanceof CodexAppServerRpcError)) throw error;
124
- workspaceListResult = { kind: "rejected" };
125
- }
240
+ const installedPlugins = await readInstalledCodexPluginMetadata({
241
+ ...params,
242
+ policy
243
+ });
244
+ let curatedCatalog;
126
245
  const diagnostics = [];
127
246
  const records = [];
128
247
  if (appInventory?.state === "missing") diagnostics.push({
@@ -134,10 +253,15 @@ async function readCodexPluginInventory(params) {
134
253
  message: "Cached Codex app inventory is stale; using stale app readiness and refreshing."
135
254
  });
136
255
  for (const pluginPolicy of policy.pluginPolicies) {
137
- if (!pluginPolicy.enabled) continue;
138
- const listed = pluginPolicy.marketplaceName === "workspace-directory" ? workspaceListResult?.kind === "listed" ? workspaceListResult.response : void 0 : curatedListed;
139
- const hasMarketplace = pluginPolicy.marketplaceName === "workspace-directory" ? listed?.marketplaces.some((entry) => entry.name === CODEX_PLUGINS_WORKSPACE_MARKETPLACE_NAME) === true : listed?.marketplaces.some(isOpenAiCuratedMarketplace) === true;
140
- if (!listed || !hasMarketplace) {
256
+ if (!pluginPolicy.enabled && !policy.allowAllPlugins) continue;
257
+ let listed = installedPlugins;
258
+ let resolvedPlugin = pluginPolicy.marketplaceName === "workspace-directory" ? findWorkspaceMarketplacePlugin(listed, pluginPolicy.pluginName) : findOpenAiCuratedMarketplacePlugin(listed, pluginPolicy.pluginName);
259
+ if (!resolvedPlugin && pluginPolicy.enabled && pluginPolicy.marketplaceName === "openai-curated") {
260
+ curatedCatalog ??= listCodexPluginMetadata(params);
261
+ listed = await curatedCatalog;
262
+ resolvedPlugin = findOpenAiCuratedMarketplacePlugin(listed, pluginPolicy.pluginName);
263
+ }
264
+ if (!(pluginPolicy.marketplaceName === "workspace-directory" ? listed.marketplaces.some((entry) => entry.name === "workspace-directory") : listed.marketplaces.some(isOpenAiCuratedMarketplace))) {
141
265
  diagnostics.push({
142
266
  code: "marketplace_missing",
143
267
  plugin: pluginPolicy,
@@ -145,7 +269,6 @@ async function readCodexPluginInventory(params) {
145
269
  });
146
270
  continue;
147
271
  }
148
- const resolvedPlugin = pluginPolicy.marketplaceName === "workspace-directory" ? findWorkspaceMarketplacePlugin(listed, pluginPolicy.pluginName) : findOpenAiCuratedMarketplacePlugin(listed, pluginPolicy.pluginName);
149
272
  if (!resolvedPlugin) {
150
273
  diagnostics.push({
151
274
  code: "plugin_missing",
@@ -181,7 +304,7 @@ async function readCodexPluginInventory(params) {
181
304
  policy: pluginPolicy,
182
305
  summary,
183
306
  ...detail ? { detail } : {},
184
- activationRequired: !summary.installed || !summary.enabled,
307
+ activationRequired: pluginPolicy.enabled && (!summary.installed || !summary.enabled),
185
308
  authRequired: apps.some((app) => app.needsAuth || !app.accessible),
186
309
  appOwnership,
187
310
  ownedAppIds,
@@ -216,24 +339,41 @@ function resolveRecoverableCodexPluginConfigKeys(params) {
216
339
  return params.policy.pluginPolicies.filter((pluginPolicy) => pluginPolicy.enabled && !isSettledMissingPluginPolicy({
217
340
  pluginPolicy,
218
341
  metadataCache: params.metadataCache,
219
- appCacheKey: params.appCacheKey
342
+ appCacheKey: params.appCacheKey,
343
+ configCwd: params.configCwd
220
344
  })).map((pluginPolicy) => pluginPolicy.configKey).toSorted();
221
345
  }
222
- async function listCodexPluginMetadata(params, queryKind, requestParams) {
223
- if (!params.metadataCache || !params.appCacheKey || queryKind === "workspace-directory") return await params.request("plugin/list", requestParams);
346
+ async function listCodexPluginMetadata(params) {
347
+ const requestParams = {};
348
+ if (!params.metadataCache || !params.appCacheKey) return await params.request("plugin/list", requestParams);
224
349
  return (await params.metadataCache.load({
225
350
  appCacheKey: params.appCacheKey,
226
- queryKind,
351
+ queryKind: "curated-global",
227
352
  requestParams,
228
353
  request: async (method, listedParams) => await params.request(method, listedParams),
229
- cacheable: (response) => (response.marketplaces ?? []).some((marketplace) => isOpenAiCuratedMarketplace(marketplace))
354
+ cacheable: (response) => response.marketplaces.some((marketplace) => isOpenAiCuratedMarketplace(marketplace))
355
+ })).response;
356
+ }
357
+ async function readInstalledCodexPluginMetadata(params) {
358
+ const requestParams = params.configCwd ? { cwds: [params.configCwd] } : {};
359
+ if (!params.metadataCache || !params.appCacheKey) return await params.request("plugin/installed", requestParams);
360
+ return (await params.metadataCache.load({
361
+ appCacheKey: params.appCacheKey,
362
+ queryKind: "installed",
363
+ requestParams,
364
+ request: async (method, installedParams) => await params.request(method, installedParams),
365
+ cacheable: (response) => params.policy.pluginPolicies.every((pluginPolicy) => {
366
+ if (!pluginPolicy.enabled && !params.policy.allowAllPlugins) return true;
367
+ return pluginPolicy.marketplaceName === "workspace-directory" ? findWorkspaceMarketplacePlugin(response, pluginPolicy.pluginName) !== void 0 : findOpenAiCuratedMarketplacePlugin(response, pluginPolicy.pluginName) !== void 0;
368
+ })
230
369
  })).response;
231
370
  }
232
371
  function isSettledMissingPluginPolicy(params) {
233
- const queryKind = params.pluginPolicy.marketplaceName === "workspace-directory" ? "workspace-directory" : "curated-global";
234
- const listed = params.metadataCache.read(params.appCacheKey, queryKind)?.response;
372
+ const queryKind = params.pluginPolicy.marketplaceName === "workspace-directory" ? "installed" : "curated-global";
373
+ const requestParams = queryKind === "installed" && params.configCwd ? { cwds: [params.configCwd] } : {};
374
+ const listed = params.metadataCache.read(params.appCacheKey, queryKind, requestParams)?.response;
235
375
  if (!listed) return false;
236
- if (queryKind === "workspace-directory") return !findWorkspaceMarketplacePlugin(listed, params.pluginPolicy.pluginName);
376
+ if (queryKind === "installed") return !findWorkspaceMarketplacePlugin(listed, params.pluginPolicy.pluginName);
237
377
  return !findOpenAiCuratedMarketplacePlugin(listed, params.pluginPolicy.pluginName);
238
378
  }
239
379
  function readCachedAppInventory(params) {
@@ -248,7 +388,7 @@ function readCachedAppInventory(params) {
248
388
  }
249
389
  async function readPluginDetail(params, marketplace, pluginPolicy, summary, diagnostics) {
250
390
  if (params.readPluginDetails === false) return;
251
- if (marketplace.name === "workspace-directory" && marketplace.remoteMarketplaceName && !summary.remotePluginId) {
391
+ if (marketplace.remoteMarketplaceName && !summary.remotePluginId) {
252
392
  diagnostics.push({
253
393
  code: "plugin_detail_unavailable",
254
394
  plugin: pluginPolicy,
@@ -297,7 +437,7 @@ function resolveOwnedApps(params) {
297
437
  name: app.name,
298
438
  accessible: info.isAccessible,
299
439
  enabled: info.isEnabled,
300
- needsAuth: app.needsAuth || !info.isAccessible
440
+ needsAuth: !info.isAccessible
301
441
  };
302
442
  }).toSorted((left, right) => left.id.localeCompare(right.id));
303
443
  }
@@ -335,9 +475,9 @@ function marketplaceRef(marketplace, name) {
335
475
  ...!marketplace.path ? { remoteMarketplaceName: marketplace.name } : {}
336
476
  };
337
477
  }
338
- /** True for either supported OpenAI curated marketplace wire name. */
478
+ /** True for any supported OpenAI curated marketplace wire name, matching Codex's own curated predicate. */
339
479
  function isOpenAiCuratedMarketplace(marketplace) {
340
- return marketplace.name === "openai-curated" || marketplace.name === CODEX_PLUGINS_REMOTE_MARKETPLACE_NAME;
480
+ return marketplace.name === "openai-curated" || marketplace.name === CODEX_PLUGINS_REMOTE_MARKETPLACE_NAME || marketplace.name === CODEX_PLUGINS_API_MARKETPLACE_NAME;
341
481
  }
342
482
  //#endregion
343
483
  //#region extensions/codex/src/app-server/plugin-activation.ts
@@ -350,6 +490,7 @@ async function ensureCodexPluginActivation(params) {
350
490
  if (!listed.marketplaces.some(isOpenAiCuratedMarketplace)) return activationFailure(params.identity, "marketplace_missing", { message: `Codex marketplace ${CODEX_PLUGINS_MARKETPLACE_NAME} was not found.` });
351
491
  return activationFailure(params.identity, "plugin_missing", { message: `${params.identity.pluginName} was not found in ${CODEX_PLUGINS_MARKETPLACE_NAME}.` });
352
492
  }
493
+ if (resolved.marketplace.remoteMarketplaceName && !resolved.summary.remotePluginId) return activationFailure(params.identity, "plugin_missing", { message: `${params.identity.pluginName} detail unavailable: Codex did not return a remote plugin id.` });
353
494
  if (resolved.summary.installed && resolved.summary.enabled && !params.installEvenIfActive) return {
354
495
  identity: params.identity,
355
496
  ok: true,
@@ -368,6 +509,7 @@ async function ensureCodexPluginActivation(params) {
368
509
  appCache: params.appCache,
369
510
  appCacheKey: params.appCacheKey,
370
511
  metadataCache: params.metadataCache,
512
+ deferAppInventoryRefresh: params.deferAppInventoryRefresh,
371
513
  targetAppIds: params.targetAppIds
372
514
  });
373
515
  refreshDiagnostics.push(...refreshResult.diagnostics);
@@ -389,7 +531,7 @@ async function ensureCodexPluginActivation(params) {
389
531
  /** Forces Codex plugin, skill, hook, MCP, and app inventory refreshes after activation. */
390
532
  async function refreshCodexPluginRuntimeState(params) {
391
533
  const diagnostics = [];
392
- await listCuratedCodexPluginMetadata(params);
534
+ await listCuratedCodexPluginMetadata(params, { forceRefetch: true });
393
535
  await params.request("skills/list", {
394
536
  cwds: [],
395
537
  forceReload: true
@@ -401,7 +543,8 @@ async function refreshCodexPluginRuntimeState(params) {
401
543
  }
402
544
  await params.request("config/mcpServer/reload", void 0);
403
545
  if (params.appCache && params.appCacheKey) {
404
- params.appCache.invalidate(params.appCacheKey, "Codex plugin activation changed app inventory");
546
+ params.appCache.invalidate(params.appCacheKey, "Codex plugin activation changed app inventory", void 0, params.targetAppIds);
547
+ if (params.deferAppInventoryRefresh) return { diagnostics };
405
548
  const request = async (method, requestParams) => await params.request(method, requestParams);
406
549
  try {
407
550
  await params.appCache.refreshNow({
@@ -416,15 +559,15 @@ async function refreshCodexPluginRuntimeState(params) {
416
559
  }
417
560
  return { diagnostics };
418
561
  }
419
- async function listCuratedCodexPluginMetadata(params) {
420
- const requestParams = {};
562
+ async function listCuratedCodexPluginMetadata(params, options = {}) {
563
+ const requestParams = options.forceRefetch ? { forceRefetch: true } : {};
421
564
  if (!params.metadataCache || !params.appCacheKey) return await params.request("plugin/list", requestParams);
422
565
  return (await params.metadataCache.load({
423
566
  appCacheKey: params.appCacheKey,
424
567
  queryKind: "curated-global",
425
568
  requestParams,
426
569
  request: async (method, listedParams) => await params.request(method, listedParams),
427
- cacheable: (response) => (response.marketplaces ?? []).some((marketplace) => isOpenAiCuratedMarketplace(marketplace))
570
+ cacheable: (response) => response.marketplaces.some((marketplace) => isOpenAiCuratedMarketplace(marketplace))
428
571
  })).response;
429
572
  }
430
573
  function activationFailure(identity, reason, diagnostic, extraDiagnostics = []) {
@@ -437,6 +580,129 @@ function activationFailure(identity, reason, diagnostic, extraDiagnostics = [])
437
580
  };
438
581
  }
439
582
  //#endregion
583
+ //#region extensions/codex/src/app-server/plugin-thread-app-admission.ts
584
+ async function refreshCodexPluginAppInventory(params, appCache, options = {}) {
585
+ if (!params.appCacheKey) return;
586
+ const request = async (method, requestParams) => await params.request(method, requestParams);
587
+ try {
588
+ return await appCache.refreshNow({
589
+ key: params.appCacheKey,
590
+ request,
591
+ nowMs: params.nowMs,
592
+ forceRefetch: options.forceRefetch,
593
+ targetAppIds: options.targetAppIds
594
+ });
595
+ } catch (error) {
596
+ embeddedAgentLog.warn("codex plugin thread config app inventory refresh failed", {
597
+ reason: options.reason,
598
+ forceRefetch: options.forceRefetch === true,
599
+ error: serializeCodexAppInventoryError(error)
600
+ });
601
+ return;
602
+ }
603
+ }
604
+ function collectCodexPluginOwnedAppIds(inventory) {
605
+ return Array.from(new Set(inventory.records.flatMap((record) => record.ownedAppIds).filter(Boolean))).toSorted();
606
+ }
607
+ function collectCodexReservedPluginAppIds(params) {
608
+ const reserved = new Set(params.inventory.records.flatMap((record) => record.appOwnership === "proven" ? record.ownedAppIds : []));
609
+ const recordsByConfigKey = new Map(params.inventory.records.map((record) => [record.policy.configKey, record]));
610
+ const configuredOwnerNames = new Set(params.policy.pluginPolicies.flatMap((policy) => {
611
+ const record = recordsByConfigKey.get(policy.configKey);
612
+ return [
613
+ policy.configKey,
614
+ policy.pluginName,
615
+ record?.summary.name,
616
+ record?.summary.id
617
+ ].filter((name) => Boolean(name)).map(normalizeCodexPluginOwnerName);
618
+ }));
619
+ for (const app of params.accountApps) if (app.pluginDisplayNames.some((name) => configuredOwnerNames.has(normalizeCodexPluginOwnerName(name)))) reserved.add(app.id);
620
+ return reserved;
621
+ }
622
+ function normalizeCodexPluginOwnerName(name) {
623
+ return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "");
624
+ }
625
+ async function readCodexThreadAdmissibleAccountApps(params, appCache) {
626
+ const request = async (method, requestParams) => await params.request(method, requestParams);
627
+ const cachedInventory = appCache.read({
628
+ key: params.appCacheKey,
629
+ request,
630
+ nowMs: params.nowMs,
631
+ suppressRefresh: true
632
+ });
633
+ const snapshot = cachedInventory.state === "fresh" && !cachedInventory.snapshot?.targetAppIds?.length ? cachedInventory.snapshot : await refreshCodexPluginAppInventory(params, appCache, {
634
+ forceRefetch: false,
635
+ reason: "account_apps_all",
636
+ targetAppIds: []
637
+ });
638
+ if (!snapshot) return {
639
+ apps: [],
640
+ diagnostic: {
641
+ code: "account_app_inventory_unavailable",
642
+ message: "Codex account app inventory was unavailable; account apps were not exposed."
643
+ }
644
+ };
645
+ const installedAppsById = new Map(snapshot.installedApps.map((app) => [app.id, app]));
646
+ return { apps: snapshot.apps.filter((app) => resolveCodexInstalledAppThreadAdmission(toCodexPluginOwnedAccountApp(app), installedAppsById.get(app.id)) !== "blocked").toSorted((left, right) => left.id.localeCompare(right.id)) };
647
+ }
648
+ function toCodexPluginOwnedAccountApp(app) {
649
+ return {
650
+ id: app.id,
651
+ name: app.name,
652
+ accessible: app.isAccessible,
653
+ enabled: app.isEnabled,
654
+ needsAuth: !app.isAccessible
655
+ };
656
+ }
657
+ function resolveCodexThreadConfigAppsForRecord(params) {
658
+ return params.inventory.appInventory?.state === "missing" ? [] : params.record.apps;
659
+ }
660
+ function resolveCodexPluginAppThreadAdmission(app, inventory) {
661
+ const snapshot = inventory.appInventory?.snapshot;
662
+ if (!snapshot) return "blocked";
663
+ return resolveCodexInstalledAppThreadAdmission(app, snapshot.installedApps.find((candidate) => candidate.id === app.id));
664
+ }
665
+ function resolveCodexInstalledAppThreadAdmission(app, installed) {
666
+ if (!app.accessible || app.needsAuth || !installed) return "blocked";
667
+ if (installed.enabled && installed.callable) return "ready";
668
+ return !installed.enabled && !installed.callable ? "provisional" : "blocked";
669
+ }
670
+ async function readCodexConfigForAppAdmission(params) {
671
+ try {
672
+ const response = await params.request("config/read", {
673
+ includeLayers: true,
674
+ ...params.configCwd ? { cwd: params.configCwd } : {}
675
+ });
676
+ if (!isJsonObject(response) || !isJsonObject(response.config) || !Array.isArray(response.layers)) throw new Error("Codex config/read omitted effective config or config layers");
677
+ return {
678
+ config: response.config,
679
+ layers: response.layers.flatMap((layer) => {
680
+ if (!isJsonObject(layer)) throw new Error("Codex config/read returned an invalid config layer");
681
+ if (layer.disabledReason !== void 0 && layer.disabledReason !== null) {
682
+ if (typeof layer.disabledReason !== "string") throw new Error("Codex config/read returned an invalid disabled layer");
683
+ return [];
684
+ }
685
+ if (!isJsonObject(layer.config)) throw new Error("Codex config/read returned an invalid layer config");
686
+ return [layer.config];
687
+ })
688
+ };
689
+ } catch (error) {
690
+ embeddedAgentLog.warn("codex plugin app admission config read failed", { error: serializeCodexAppInventoryError(error) });
691
+ return;
692
+ }
693
+ }
694
+ function resolveCodexExplicitAppEnablement(layersHighestPrecedenceFirst, appId) {
695
+ for (const layer of layersHighestPrecedenceFirst) {
696
+ const apps = layer.apps;
697
+ const app = isJsonObject(apps) ? apps[appId] : void 0;
698
+ if (isJsonObject(app) && Object.hasOwn(app, "enabled")) return app.enabled === true;
699
+ }
700
+ }
701
+ function shouldForceRefreshCodexNotReadyPluginApps(params, policy, inventory) {
702
+ if (!params.appCacheKey || !policy.pluginPolicies.some((plugin) => plugin.enabled) || inventory.appInventory?.state === "missing") return false;
703
+ return inventory.records.some((record) => record.appOwnership === "proven" && record.ownedAppIds.length > 0 && (record.apps.length === 0 || record.apps.some((app) => !app.accessible)));
704
+ }
705
+ //#endregion
440
706
  //#region extensions/codex/src/app-server/plugin-thread-config.ts
441
707
  /**
442
708
  * Builds Codex thread config patches that expose only policy-approved apps
@@ -490,16 +756,17 @@ async function buildCodexPluginThreadConfig(params) {
490
756
  request: params.request,
491
757
  appCache,
492
758
  appCacheKey: params.appCacheKey,
759
+ configCwd: params.configCwd,
493
760
  metadataCache: params.metadataCache,
494
761
  nowMs: params.nowMs,
495
762
  suppressAppInventoryRefresh: true
496
763
  }) : emptyCodexPluginInventory(policy);
497
764
  const appInventoryRefreshDeferredForActivation = inventory.records.some((record) => record.activationRequired) && shouldRefreshMissingAppInventory(params, policy, inventory);
498
765
  if (shouldWaitForInitialAppInventory(params, policy, inventory)) {
499
- await refreshAppInventoryNow(params, appCache, {
766
+ await refreshCodexPluginAppInventory(params, appCache, {
500
767
  forceRefetch: false,
501
768
  reason: "initial_missing",
502
- targetAppIds: collectInventoryOwnedAppIds(inventory)
769
+ targetAppIds: collectCodexPluginOwnedAppIds(inventory)
503
770
  });
504
771
  inventory = await readCodexPluginInventory({
505
772
  pluginConfig: params.pluginConfig,
@@ -507,6 +774,7 @@ async function buildCodexPluginThreadConfig(params) {
507
774
  request: params.request,
508
775
  appCache,
509
776
  appCacheKey: params.appCacheKey,
777
+ configCwd: params.configCwd,
510
778
  metadataCache: params.metadataCache,
511
779
  nowMs: params.nowMs
512
780
  });
@@ -525,6 +793,7 @@ async function buildCodexPluginThreadConfig(params) {
525
793
  appCache,
526
794
  appCacheKey: params.appCacheKey,
527
795
  metadataCache: params.metadataCache,
796
+ deferAppInventoryRefresh: true,
528
797
  targetAppIds: record.ownedAppIds
529
798
  });
530
799
  activationResults.push(activation);
@@ -537,10 +806,10 @@ async function buildCodexPluginThreadConfig(params) {
537
806
  const postInstallRefreshRequired = activationResults.some((activation) => activation.ok && activation.installAttempted);
538
807
  const deferredMissingRefreshRequired = appInventoryRefreshDeferredForActivation && !postInstallRefreshRequired && shouldRefreshMissingAppInventory(params, policy, inventory);
539
808
  if (postInstallRefreshRequired || deferredMissingRefreshRequired) {
540
- await refreshAppInventoryNow(params, appCache, {
809
+ await refreshCodexPluginAppInventory(params, appCache, {
541
810
  forceRefetch: true,
542
811
  reason: postInstallRefreshRequired ? "post_install" : "deferred_missing",
543
- targetAppIds: collectInventoryOwnedAppIds(inventory)
812
+ targetAppIds: collectCodexPluginOwnedAppIds(inventory)
544
813
  });
545
814
  inventory = await readCodexPluginInventory({
546
815
  pluginConfig: params.pluginConfig,
@@ -548,6 +817,7 @@ async function buildCodexPluginThreadConfig(params) {
548
817
  request: params.request,
549
818
  appCache,
550
819
  appCacheKey: params.appCacheKey,
820
+ configCwd: params.configCwd,
551
821
  metadataCache: params.metadataCache,
552
822
  nowMs: params.nowMs
553
823
  });
@@ -556,11 +826,11 @@ async function buildCodexPluginThreadConfig(params) {
556
826
  appCacheKey: params.appCacheKey
557
827
  });
558
828
  }
559
- if (shouldForceRefreshForNotReadyPluginApps(params, policy, inventory)) {
560
- await refreshAppInventoryNow(params, appCache, {
829
+ if (shouldForceRefreshCodexNotReadyPluginApps(params, policy, inventory)) {
830
+ await refreshCodexPluginAppInventory(params, appCache, {
561
831
  forceRefetch: true,
562
832
  reason: "not_ready_plugin_apps",
563
- targetAppIds: collectInventoryOwnedAppIds(inventory)
833
+ targetAppIds: collectCodexPluginOwnedAppIds(inventory)
564
834
  });
565
835
  inventory = await readCodexPluginInventory({
566
836
  pluginConfig: params.pluginConfig,
@@ -568,6 +838,7 @@ async function buildCodexPluginThreadConfig(params) {
568
838
  request: params.request,
569
839
  appCache,
570
840
  appCacheKey: params.appCacheKey,
841
+ configCwd: params.configCwd,
571
842
  metadataCache: params.metadataCache,
572
843
  nowMs: params.nowMs
573
844
  });
@@ -576,12 +847,13 @@ async function buildCodexPluginThreadConfig(params) {
576
847
  appCacheKey: params.appCacheKey
577
848
  });
578
849
  }
579
- const accountAppsResult = policy.allowAllPlugins ? await readAccessibleAccountApps(params, appCache) : { apps: [] };
850
+ const accountAppsResult = policy.allowAllPlugins ? await readCodexThreadAdmissibleAccountApps(params, appCache) : { apps: [] };
580
851
  const diagnostics = [
581
852
  ...inventory.diagnostics,
582
853
  ...activationDiagnostics,
583
854
  ...accountAppsResult.diagnostic ? [accountAppsResult.diagnostic] : []
584
855
  ];
856
+ const provisionalAppIds = /* @__PURE__ */ new Set();
585
857
  const apps = { _default: {
586
858
  enabled: false,
587
859
  destructive_enabled: false,
@@ -589,17 +861,30 @@ async function buildCodexPluginThreadConfig(params) {
589
861
  } };
590
862
  const policyApps = {};
591
863
  const pluginAppIds = {};
592
- const pluginOwnedAppIds = new Set(inventory.records.flatMap((record) => record.appOwnership === "proven" ? record.ownedAppIds : []));
864
+ let configForAppAdmission;
865
+ const pluginOwnedAppIds = collectCodexReservedPluginAppIds({
866
+ policy,
867
+ inventory,
868
+ accountApps: accountAppsResult.apps
869
+ });
870
+ const unresolvedDisabledPluginOwnership = policy.allowAllPlugins ? policy.pluginPolicies.find((pluginPolicy) => !pluginPolicy.enabled && !inventory.records.some((record) => record.policy.configKey === pluginPolicy.configKey && record.detail)) : void 0;
871
+ if (unresolvedDisabledPluginOwnership) diagnostics.push({
872
+ code: "account_app_ownership_unavailable",
873
+ plugin: unresolvedDisabledPluginOwnership,
874
+ message: `Could not verify disabled Codex plugin app ownership for ${unresolvedDisabledPluginOwnership.pluginName}; account apps were not exposed.`
875
+ });
593
876
  for (const record of inventory.records) {
877
+ if (!record.policy.enabled) continue;
594
878
  const activation = activationResults.find((item) => item.identity.configKey === record.policy.configKey);
595
879
  if (activation?.ok === false || record.activationRequired && !activation?.ok) continue;
596
880
  if (record.appOwnership !== "proven") continue;
597
881
  pluginAppIds[record.policy.configKey] = [...record.ownedAppIds].toSorted();
598
- for (const app of resolveThreadConfigAppsForRecord({
882
+ for (const app of resolveCodexThreadConfigAppsForRecord({
599
883
  record,
600
884
  inventory
601
885
  })) {
602
- if (!isPluginAppReadyForThreadStart(app)) {
886
+ const admissionConfig = resolveCodexPluginAppThreadAdmission(app, inventory) === "blocked" ? void 0 : await (configForAppAdmission ??= readCodexConfigForAppAdmission(params));
887
+ if (!admissionConfig || resolveCodexExplicitAppEnablement(admissionConfig.layers, app.id) === false) {
603
888
  diagnostics.push({
604
889
  code: "app_not_ready",
605
890
  plugin: record.policy,
@@ -610,10 +895,12 @@ async function buildCodexPluginThreadConfig(params) {
610
895
  if (record.policy.destructiveApprovalMode === "ask" && !await clearPersistedAppToolApprovalOverrides({
611
896
  request: params.request,
612
897
  configCwd: params.configCwd,
898
+ config: admissionConfig.config,
613
899
  plugin: record.policy,
614
900
  app,
615
901
  diagnostics
616
902
  })) continue;
903
+ provisionalAppIds.add(app.id);
617
904
  apps[app.id] = buildEnabledAppConfig(record.policy);
618
905
  policyApps[app.id] = {
619
906
  configKey: record.policy.configKey,
@@ -625,15 +912,27 @@ async function buildCodexPluginThreadConfig(params) {
625
912
  };
626
913
  }
627
914
  }
628
- for (const app of accountAppsResult.apps) {
915
+ for (const app of unresolvedDisabledPluginOwnership ? [] : accountAppsResult.apps) {
629
916
  if (pluginOwnedAppIds.has(app.id)) continue;
630
- const accountApp = toOwnedAccountApp(app);
917
+ configForAppAdmission ??= readCodexConfigForAppAdmission(params);
918
+ const admissionConfig = await configForAppAdmission;
919
+ if (!admissionConfig) {
920
+ diagnostics.push({
921
+ code: "account_app_config_unavailable",
922
+ message: "Codex account app configuration was unavailable; account apps were not exposed."
923
+ });
924
+ break;
925
+ }
926
+ if (resolveCodexExplicitAppEnablement(admissionConfig.layers, app.id) === false) continue;
927
+ const accountApp = toCodexPluginOwnedAccountApp(app);
631
928
  if (policy.destructiveApprovalMode === "ask" && !await clearPersistedAppToolApprovalOverrides({
632
929
  request: params.request,
633
930
  configCwd: params.configCwd,
931
+ config: admissionConfig.config,
634
932
  app: accountApp,
635
933
  diagnostics
636
934
  })) continue;
935
+ provisionalAppIds.add(app.id);
637
936
  apps[app.id] = buildEnabledAppConfig(policy);
638
937
  policyApps[app.id] = {
639
938
  source: "account",
@@ -648,6 +947,7 @@ async function buildCodexPluginThreadConfig(params) {
648
947
  return {
649
948
  enabled: true,
650
949
  configPatch,
950
+ ...provisionalAppIds.size > 0 ? { provisionalAppIds: Array.from(provisionalAppIds).toSorted() } : {},
651
951
  fingerprint: fingerprintJson({
652
952
  version: CODEX_PLUGIN_THREAD_CONFIG_FINGERPRINT_VERSION,
653
953
  inputFingerprint,
@@ -736,13 +1036,22 @@ function buildPluginAppPolicyContext(apps, pluginAppIds) {
736
1036
  }
737
1037
  async function clearPersistedAppToolApprovalOverrides(params) {
738
1038
  try {
739
- const overrideNames = await readPersistedAppToolApprovalOverrideNames(params);
740
- for (const toolName of overrideNames) if (isOverriddenConfigWriteResponse(await params.request("config/value/write", {
1039
+ const overrideNames = readPersistedAppToolApprovalOverrideNames(params.config, params.app);
1040
+ if (overrideNames.length === 0) return true;
1041
+ const edits = overrideNames.map((toolName) => ({
741
1042
  keyPath: `apps.${quoteConfigKeyPathSegment(params.app.id)}.tools.${quoteConfigKeyPathSegment(toolName)}.approval_mode`,
742
1043
  value: null,
743
1044
  mergeStrategy: "replace"
744
- }))) throw new Error(`approval override for ${toolName} is controlled by another config layer`);
745
- const remainingOverrideNames = await readPersistedAppToolApprovalOverrideNames(params);
1045
+ }));
1046
+ const response = await params.request("config/batchWrite", { edits });
1047
+ if (!isJsonObject(response) || response.status !== "ok" && response.status !== "okOverridden") throw new Error("Codex did not confirm the approval override batch");
1048
+ if (response.status === "okOverridden") throw new Error(`approval override for ${overrideNames.join(", ")} is controlled by another config layer`);
1049
+ const confirmed = await params.request("config/read", {
1050
+ includeLayers: false,
1051
+ ...params.configCwd ? { cwd: params.configCwd } : {}
1052
+ });
1053
+ if (!isJsonObject(confirmed) || !isJsonObject(confirmed.config)) throw new Error("Codex did not confirm effective app approval configuration");
1054
+ const remainingOverrideNames = readPersistedAppToolApprovalOverrideNames(confirmed.config, params.app);
746
1055
  if (remainingOverrideNames.length > 0) throw new Error(`effective approval overrides remain for ${remainingOverrideNames.join(", ")}`);
747
1056
  return true;
748
1057
  } catch (error) {
@@ -754,24 +1063,15 @@ async function clearPersistedAppToolApprovalOverrides(params) {
754
1063
  return false;
755
1064
  }
756
1065
  }
757
- async function readPersistedAppToolApprovalOverrideNames(params) {
758
- const response = await params.request("config/read", {
759
- includeLayers: false,
760
- ...params.configCwd ? { cwd: params.configCwd } : {}
761
- });
762
- const config = isJsonObject(response) ? response.config : void 0;
763
- const appsRoot = isJsonObject(config) ? config.apps : void 0;
764
- const nestedApps = isJsonObject(appsRoot) ? appsRoot.apps : void 0;
765
- const appConfig = isJsonObject(appsRoot) ? appsRoot[params.app.id] ?? (isJsonObject(nestedApps) ? nestedApps[params.app.id] : void 0) : void 0;
1066
+ function readPersistedAppToolApprovalOverrideNames(config, app) {
1067
+ const appsRoot = config.apps;
1068
+ const appConfig = isJsonObject(appsRoot) ? appsRoot[app.id] : void 0;
766
1069
  const tools = isJsonObject(appConfig) ? appConfig.tools : void 0;
767
1070
  if (!isJsonObject(tools)) return [];
768
1071
  return Object.entries(tools).filter(([, value]) => hasPersistedToolApprovalOverride(value)).map(([toolName]) => toolName).toSorted();
769
1072
  }
770
1073
  function hasPersistedToolApprovalOverride(value) {
771
- return isJsonObject(value) && (value.approval_mode !== void 0 || value.approvalMode !== void 0);
772
- }
773
- function isOverriddenConfigWriteResponse(response) {
774
- return isJsonObject(response) && response.status === "okOverridden";
1074
+ return isJsonObject(value) && value.approval_mode !== void 0;
775
1075
  }
776
1076
  function quoteConfigKeyPathSegment(segment) {
777
1077
  return `"${segment.replace(/["\\]/g, (char) => `\\${char}`)}"`;
@@ -783,30 +1083,6 @@ function shouldWaitForInitialAppInventory(params, policy, inventory) {
783
1083
  function shouldRefreshMissingAppInventory(params, policy, inventory) {
784
1084
  return Boolean(params.appCacheKey && policy.pluginPolicies.some((plugin) => plugin.enabled) && inventory.appInventory?.state === "missing");
785
1085
  }
786
- async function refreshAppInventoryNow(params, appCache, options = {}) {
787
- const appCacheKey = params.appCacheKey;
788
- if (!appCacheKey) return;
789
- const request = async (method, requestParams) => await params.request(method, requestParams);
790
- try {
791
- return await appCache.refreshNow({
792
- key: appCacheKey,
793
- request,
794
- nowMs: params.nowMs,
795
- forceRefetch: options.forceRefetch,
796
- targetAppIds: options.targetAppIds
797
- });
798
- } catch (error) {
799
- embeddedAgentLog.warn("codex plugin thread config app inventory refresh failed", {
800
- reason: options.reason,
801
- forceRefetch: options.forceRefetch === true,
802
- error: serializeCodexAppInventoryError(error)
803
- });
804
- return;
805
- }
806
- }
807
- function collectInventoryOwnedAppIds(inventory) {
808
- return Array.from(new Set(inventory.records.flatMap((record) => record.ownedAppIds).filter(Boolean))).toSorted();
809
- }
810
1086
  function emptyCodexPluginInventory(policy) {
811
1087
  return {
812
1088
  policy,
@@ -814,42 +1090,6 @@ function emptyCodexPluginInventory(policy) {
814
1090
  diagnostics: []
815
1091
  };
816
1092
  }
817
- async function readAccessibleAccountApps(params, appCache) {
818
- const snapshot = await refreshAppInventoryNow(params, appCache, {
819
- forceRefetch: false,
820
- reason: "account_apps_all",
821
- targetAppIds: []
822
- });
823
- if (!snapshot) return {
824
- apps: [],
825
- diagnostic: {
826
- code: "account_app_inventory_unavailable",
827
- message: "Codex account app inventory was unavailable; account apps were not exposed."
828
- }
829
- };
830
- return { apps: snapshot.apps.filter((app) => app.isAccessible).toSorted((left, right) => left.id.localeCompare(right.id)) };
831
- }
832
- function toOwnedAccountApp(app) {
833
- return {
834
- id: app.id,
835
- name: app.name,
836
- accessible: app.isAccessible,
837
- enabled: app.isEnabled,
838
- needsAuth: !app.isAccessible
839
- };
840
- }
841
- function resolveThreadConfigAppsForRecord(params) {
842
- if (params.inventory.appInventory?.state === "missing") return [];
843
- return params.record.apps;
844
- }
845
- function isPluginAppReadyForThreadStart(app) {
846
- return app.accessible;
847
- }
848
- function shouldForceRefreshForNotReadyPluginApps(params, policy, inventory) {
849
- if (!params.appCacheKey || !policy.pluginPolicies.some((plugin) => plugin.enabled)) return false;
850
- if (inventory.appInventory?.state === "missing") return false;
851
- return inventory.records.some((record) => record.appOwnership === "proven" && record.ownedAppIds.length > 0 && (record.apps.length === 0 || record.apps.some((app) => !app.accessible)));
852
- }
853
1093
  function policyFingerprint(policy) {
854
1094
  return {
855
1095
  enabled: policy.enabled,
@@ -1111,8 +1351,8 @@ function hasMessageContent(message) {
1111
1351
  return "content" in message;
1112
1352
  }
1113
1353
  function normalizeRenderedContextMaxChars(value) {
1114
- if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_RENDERED_CONTEXT_CHARS;
1115
- return Math.min(MAX_RENDERED_CONTEXT_CHARS, Math.max(DEFAULT_RENDERED_CONTEXT_CHARS, Math.floor(value)));
1354
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return DEFAULT_RENDERED_CONTEXT_CHARS;
1355
+ return Math.min(MAX_RENDERED_CONTEXT_CHARS, Math.max(1, Math.floor(value)));
1116
1356
  }
1117
1357
  function resolveTextPartMaxChars(maxRenderedContextChars) {
1118
1358
  return Math.min(MAX_TEXT_PART_CHARS, Math.max(DEFAULT_TEXT_PART_CHARS, Math.floor(maxRenderedContextChars / 4)));
@@ -1229,6 +1469,26 @@ function fingerprintUserMcpServersAuthorizationHeader(value) {
1229
1469
  function fingerprintJsonObject(value) {
1230
1470
  return JSON.stringify(stabilizeJsonValue(value));
1231
1471
  }
1472
+ /** Hash every resume-visible setting without retaining config, credentials, or instructions. */
1473
+ function fingerprintCodexThreadConfig(request, authProfileId, dynamicToolsFingerprint) {
1474
+ return hashCodexAppServerBindingFingerprint(fingerprintJsonObject({
1475
+ authProfileId: authProfileId ?? null,
1476
+ dynamicToolsFingerprint: dynamicToolsFingerprint ?? null,
1477
+ model: request.model ?? null,
1478
+ requestedModel: request.requestedModel === void 0 ? request.model ?? null : request.requestedModel,
1479
+ modelProvider: request.modelProvider ?? null,
1480
+ requestedModelProvider: request.requestedModelProvider === void 0 ? request.modelProvider ?? null : request.requestedModelProvider,
1481
+ approvalPolicy: request.approvalPolicy ?? null,
1482
+ approvalsReviewer: request.approvalsReviewer ?? null,
1483
+ sandbox: request.sandbox ?? null,
1484
+ permissions: request.permissions ?? null,
1485
+ personality: request.personality ?? null,
1486
+ serviceTier: request.serviceTier === void 0 ? "<omitted>" : request.serviceTier,
1487
+ baseInstructions: request.baseInstructions ?? null,
1488
+ developerInstructions: request.developerInstructions ?? null,
1489
+ config: request.config ?? {}
1490
+ }));
1491
+ }
1232
1492
  function fingerprintEnvironmentSelection(environments) {
1233
1493
  return environments ? JSON.stringify(environments.map(stabilizeJsonValue)) : void 0;
1234
1494
  }
@@ -1259,145 +1519,222 @@ function readActiveCodexTurnIdsFromResume(response) {
1259
1519
  const pagedTurns = response.initialTurnsPage?.data;
1260
1520
  return readActiveCodexTurnIds(Array.isArray(pagedTurns) ? { turns: pagedTurns } : response.thread);
1261
1521
  }
1262
- const LEGACY_EMPTY_DYNAMIC_TOOLS_FINGERPRINT = legacyFingerprintDynamicTools([]);
1263
- const EMPTY_DYNAMIC_TOOLS_FINGERPRINT = hashCodexAppServerBindingFingerprint(LEGACY_EMPTY_DYNAMIC_TOOLS_FINGERPRINT);
1264
- function areDynamicToolFingerprintsCompatible(previous, next, nextLegacy) {
1265
- return !previous || previous === next || previous === nextLegacy;
1266
- }
1267
- function areUserMcpServersFingerprintsCompatible(params) {
1268
- return params.previous === params.next || params.previous === params.nextLegacy || params.nextLegacy !== void 0 && params.previous === hashCodexAppServerBindingFingerprint(params.nextLegacy);
1269
- }
1270
- function shouldStartTransientNoToolThread(params) {
1271
- return Boolean(params.previous && !isEmptyDynamicToolsFingerprint(params.previous) && !params.nextHasDynamicTools);
1272
- }
1273
- function isEmptyDynamicToolsFingerprint(fingerprint) {
1274
- return fingerprint === EMPTY_DYNAMIC_TOOLS_FINGERPRINT || fingerprint === LEGACY_EMPTY_DYNAMIC_TOOLS_FINGERPRINT;
1275
- }
1276
- function compareJsonFingerprint(left, right) {
1277
- return JSON.stringify(left).localeCompare(JSON.stringify(right));
1278
- }
1279
- //#endregion
1280
- //#region extensions/codex/src/app-server/attempt-client-cleanup.ts
1281
- /**
1282
- * Best-effort cleanup helpers for Codex app-server startup attempts and turns.
1283
- */
1284
- /** Timeout for best-effort app-server turn interruption during cleanup. */
1285
- const CODEX_APP_SERVER_INTERRUPT_TIMEOUT_MS = 5e3;
1286
- /** Timeout for best-effort thread unsubscribe during cleanup. */
1287
- const CODEX_APP_SERVER_UNSUBSCRIBE_TIMEOUT_MS = 5e3;
1288
- /** Raised when a thread subscription may be live on a client OpenClaw no longer controls. */
1289
- var CodexAppServerUnsafeSubscriptionError = class extends Error {
1290
- constructor(message, options) {
1291
- super(message, options);
1292
- this.name = "CodexAppServerUnsafeSubscriptionError";
1293
- }
1294
- };
1295
- function isCodexAppServerUnsafeSubscriptionError(error) {
1296
- return error instanceof CodexAppServerUnsafeSubscriptionError;
1297
- }
1298
- /** Asserts Codex resumed the exact thread this attempt subscribed to. */
1299
- function assertCodexThreadResumeSubscription(requestedThreadId, returnedThreadId) {
1300
- if (returnedThreadId !== requestedThreadId) throw new CodexAppServerUnsafeSubscriptionError(`Codex thread/resume returned ${returnedThreadId} for ${requestedThreadId}`);
1301
- }
1302
- async function closeClientAndWaitIfAvailable(client) {
1303
- const closeable = client;
1304
- if (typeof closeable.closeAndWait === "function") {
1305
- await closeable.closeAndWait();
1306
- return;
1307
- }
1308
- closeable.close?.();
1309
- }
1310
- async function closeCodexStartupClientBestEffort(client) {
1311
- if (!client) return;
1312
- const unclaimedSharedClient = clearSharedCodexAppServerClientIfCurrentAndUnclaimed(client);
1313
- if (unclaimedSharedClient.closed) {
1314
- await closeClientAndWaitIfAvailable(client);
1315
- return;
1316
- }
1317
- if (unclaimedSharedClient.found) {
1318
- if (retireSharedCodexAppServerClientIfCurrent(client)?.closed) await closeClientAndWaitIfAvailable(client);
1319
- return;
1320
- }
1321
- const retiredSharedClient = retireSharedCodexAppServerClientIfCurrent(client);
1322
- if (retiredSharedClient) {
1323
- if (retiredSharedClient.closed) await closeClientAndWaitIfAvailable(client);
1324
- return;
1325
- }
1326
- if (clearSharedCodexAppServerClientIfCurrent(client)) {
1327
- await closeClientAndWaitIfAvailable(client);
1328
- return;
1329
- }
1330
- await closeClientAndWaitIfAvailable(client);
1331
- }
1332
- /** Sends a turn interrupt without blocking abort cleanup on app-server errors. */
1333
- function interruptCodexTurnBestEffort(client, params) {
1334
- const requestOptions = params.timeoutMs && Number.isFinite(params.timeoutMs) && params.timeoutMs > 0 ? { timeoutMs: params.timeoutMs } : void 0;
1335
- const requestParams = {
1336
- threadId: params.threadId,
1337
- turnId: params.turnId
1338
- };
1339
- try {
1340
- const interrupt = requestOptions ? client.request("turn/interrupt", requestParams, requestOptions) : client.request("turn/interrupt", requestParams);
1341
- Promise.resolve(interrupt).catch((error) => {
1342
- embeddedAgentLog.debug("codex app-server turn interrupt failed during abort", { error });
1343
- });
1344
- } catch (error) {
1345
- embeddedAgentLog.debug("codex app-server turn interrupt failed during abort", { error });
1346
- }
1522
+ const LEGACY_EMPTY_DYNAMIC_TOOLS_FINGERPRINT = legacyFingerprintDynamicTools([]);
1523
+ const EMPTY_DYNAMIC_TOOLS_FINGERPRINT = hashCodexAppServerBindingFingerprint(LEGACY_EMPTY_DYNAMIC_TOOLS_FINGERPRINT);
1524
+ function areDynamicToolFingerprintsCompatible(previous, next, nextLegacy) {
1525
+ return !previous || previous === next || previous === nextLegacy;
1347
1526
  }
1348
- /** Unsubscribes from a thread while swallowing cleanup-only failures. */
1349
- async function unsubscribeCodexThreadBestEffort(client, params) {
1527
+ function areUserMcpServersFingerprintsCompatible(params) {
1528
+ return params.previous === params.next || params.previous === params.nextLegacy || params.nextLegacy !== void 0 && params.previous === hashCodexAppServerBindingFingerprint(params.nextLegacy);
1529
+ }
1530
+ function shouldStartTransientNoToolThread(params) {
1531
+ return Boolean(params.previous && !isEmptyDynamicToolsFingerprint(params.previous) && !params.nextHasDynamicTools);
1532
+ }
1533
+ function isEmptyDynamicToolsFingerprint(fingerprint) {
1534
+ return fingerprint === EMPTY_DYNAMIC_TOOLS_FINGERPRINT || fingerprint === LEGACY_EMPTY_DYNAMIC_TOOLS_FINGERPRINT;
1535
+ }
1536
+ function compareJsonFingerprint(left, right) {
1537
+ return JSON.stringify(left).localeCompare(JSON.stringify(right));
1538
+ }
1539
+ //#endregion
1540
+ //#region extensions/codex/src/app-server/native-skill-isolation.ts
1541
+ const MAX_PERSONAL_SKILL_DIRECTORIES = 2e3;
1542
+ const MAX_PERSONAL_SKILL_DEPTH = 6;
1543
+ const MAX_PERSONAL_SKILL_ENTRIES = 1e4;
1544
+ const nativeSkillIsolationByClient = /* @__PURE__ */ new WeakMap();
1545
+ function isMissingPathError(error) {
1546
+ return error.code === "ENOENT";
1547
+ }
1548
+ function isPathWithin(root, candidate) {
1549
+ const relative = path.relative(path.resolve(root), path.resolve(candidate));
1550
+ return relative === "" || !path.isAbsolute(relative) && !relative.startsWith(`..${path.sep}`) && relative !== "..";
1551
+ }
1552
+ async function canonicalizeExistingPath(candidate) {
1350
1553
  try {
1351
- await client.request("thread/unsubscribe", { threadId: params.threadId }, { timeoutMs: params.timeoutMs });
1352
- return true;
1353
- } catch (error) {
1354
- embeddedAgentLog.debug("codex app-server thread unsubscribe cleanup failed", {
1355
- threadId: params.threadId,
1356
- error
1554
+ return await fs.realpath(candidate);
1555
+ } catch {
1556
+ return path.resolve(candidate);
1557
+ }
1558
+ }
1559
+ async function usesDefaultStateDir() {
1560
+ if (!process.env.OPENCLAW_STATE_DIR?.trim()) return true;
1561
+ const home = resolveRequiredHomeDir();
1562
+ const [stateDir, defaultStateDir] = await Promise.all([canonicalizeExistingPath(resolveStateDir()), canonicalizeExistingPath(path.join(home, ".openclaw"))]);
1563
+ return stateDir === defaultStateDir;
1564
+ }
1565
+ async function collectPersonalSkillRealPaths(homes, codexHome) {
1566
+ const realStateDir = await canonicalizeExistingPath(resolveStateDir());
1567
+ const roots = [];
1568
+ for (const home of homes) {
1569
+ for (const dir of [".agents", ".claude"]) roots.push({
1570
+ dir: path.join(home, dir, "skills"),
1571
+ onlyEscapedStateTargets: false
1357
1572
  });
1358
- return false;
1359
- }
1360
- }
1361
- /**
1362
- * Retires the shared client after a timed-out turn so later runs do not reuse a
1363
- * potentially wedged app-server connection.
1364
- */
1365
- async function retireCodexAppServerClientAfterTimedOutTurn(client, params) {
1366
- const retiredSharedClient = retireSharedCodexAppServerClientIfCurrent(client, { failActiveLeases: params.suspectPhysicalClient });
1367
- const detachedSharedClient = Boolean(retiredSharedClient);
1368
- if (!(params.suspectPhysicalClient && (retiredSharedClient?.closed ?? false))) {
1369
- interruptCodexTurnBestEffort(client, {
1370
- threadId: params.threadId,
1371
- turnId: params.turnId,
1372
- timeoutMs: CODEX_APP_SERVER_INTERRUPT_TIMEOUT_MS
1573
+ const defaultCodexHome = path.join(home, ".codex");
1574
+ const realDefaultCodexHome = await canonicalizeExistingPath(defaultCodexHome);
1575
+ roots.push({
1576
+ dir: path.join(defaultCodexHome, "skills"),
1577
+ onlyEscapedStateTargets: isPathWithin(realStateDir, realDefaultCodexHome)
1373
1578
  });
1374
- await unsubscribeCodexThreadBestEffort(client, {
1375
- threadId: params.threadId,
1376
- timeoutMs: CODEX_APP_SERVER_UNSUBSCRIBE_TIMEOUT_MS
1579
+ }
1580
+ const configuredCodexHome = codexHome?.trim() || process.env.CODEX_HOME?.trim();
1581
+ if (configuredCodexHome) {
1582
+ const stateOwned = isPathWithin(realStateDir, await canonicalizeExistingPath(configuredCodexHome));
1583
+ roots.push({
1584
+ dir: path.join(configuredCodexHome, "skills"),
1585
+ onlyEscapedStateTargets: stateOwned
1377
1586
  });
1378
1587
  }
1379
- let closedClient = retiredSharedClient?.closed ?? false;
1380
- if (!detachedSharedClient) {
1381
- const close = client.close;
1382
- if (typeof close === "function") try {
1383
- close.call(client);
1384
- closedClient = true;
1588
+ const skillPaths = /* @__PURE__ */ new Set();
1589
+ let complete = true;
1590
+ const seenDirectories = /* @__PURE__ */ new Set();
1591
+ const queue = roots.map((root) => ({
1592
+ dir: root.dir,
1593
+ onlyEscapedStateTargets: root.onlyEscapedStateTargets,
1594
+ depth: 0
1595
+ }));
1596
+ let entryCount = 0;
1597
+ const recordSkillFile = async (filePath, onlyEscapedStateTargets) => {
1598
+ try {
1599
+ const skillRealPath = await fs.realpath(filePath);
1600
+ if (!onlyEscapedStateTargets || !isPathWithin(realStateDir, skillRealPath)) skillPaths.add(skillRealPath);
1385
1601
  } catch (error) {
1386
- embeddedAgentLog.debug("codex app-server client close failed during timeout cleanup", {
1387
- threadId: params.threadId,
1388
- turnId: params.turnId,
1389
- error
1390
- });
1602
+ if (!isMissingPathError(error)) complete = false;
1603
+ }
1604
+ };
1605
+ for (const current of queue) {
1606
+ let realDir;
1607
+ try {
1608
+ realDir = await fs.realpath(current.dir);
1609
+ } catch (error) {
1610
+ if (isMissingPathError(error)) continue;
1611
+ complete = false;
1612
+ continue;
1613
+ }
1614
+ if (seenDirectories.has(realDir)) continue;
1615
+ seenDirectories.add(realDir);
1616
+ if (seenDirectories.size > MAX_PERSONAL_SKILL_DIRECTORIES) {
1617
+ complete = false;
1618
+ break;
1619
+ }
1620
+ let directory;
1621
+ try {
1622
+ directory = await fs.opendir(current.dir);
1623
+ } catch (error) {
1624
+ if (!isMissingPathError(error)) complete = false;
1625
+ continue;
1626
+ }
1627
+ try {
1628
+ for await (const entry of directory) {
1629
+ entryCount += 1;
1630
+ if (entryCount > MAX_PERSONAL_SKILL_ENTRIES) {
1631
+ complete = false;
1632
+ queue.length = 0;
1633
+ break;
1634
+ }
1635
+ if (entry.name.startsWith(".")) continue;
1636
+ const entryPath = path.join(current.dir, entry.name);
1637
+ if (entry.name === "SKILL.md" && entry.isFile()) {
1638
+ await recordSkillFile(entryPath, current.onlyEscapedStateTargets);
1639
+ continue;
1640
+ }
1641
+ if (entry.isSymbolicLink()) {
1642
+ try {
1643
+ const stat = await fs.stat(entryPath);
1644
+ if (entry.name === "SKILL.md" && stat.isFile()) await recordSkillFile(entryPath, current.onlyEscapedStateTargets);
1645
+ else if (stat.isDirectory()) if (current.depth < MAX_PERSONAL_SKILL_DEPTH) queue.push({
1646
+ dir: entryPath,
1647
+ depth: current.depth + 1,
1648
+ onlyEscapedStateTargets: current.onlyEscapedStateTargets
1649
+ });
1650
+ else complete = false;
1651
+ } catch (error) {
1652
+ if (!isMissingPathError(error)) complete = false;
1653
+ }
1654
+ continue;
1655
+ }
1656
+ if (current.depth >= MAX_PERSONAL_SKILL_DEPTH) {
1657
+ if (entry.isDirectory()) complete = false;
1658
+ continue;
1659
+ }
1660
+ if (entry.isDirectory()) {
1661
+ queue.push({
1662
+ dir: entryPath,
1663
+ depth: current.depth + 1,
1664
+ onlyEscapedStateTargets: current.onlyEscapedStateTargets
1665
+ });
1666
+ continue;
1667
+ }
1668
+ }
1669
+ } catch (error) {
1670
+ if (!isMissingPathError(error)) complete = false;
1391
1671
  }
1392
1672
  }
1393
- embeddedAgentLog.warn("codex app-server client retired after timed-out turn", {
1394
- threadId: params.threadId,
1395
- turnId: params.turnId,
1396
- reason: params.reason,
1397
- detachedSharedClient,
1398
- closedClient,
1399
- activeSharedClientLeases: retiredSharedClient?.activeLeases ?? 0
1400
- });
1673
+ return {
1674
+ complete,
1675
+ skillPaths
1676
+ };
1677
+ }
1678
+ /** Resolves the native user-scope skills that an isolated OpenClaw thread must disable. */
1679
+ async function resolveCodexNativeSkillIsolation(params) {
1680
+ params.signal?.throwIfAborted();
1681
+ if (!process.env.OPENCLAW_STATE_DIR?.trim()) return;
1682
+ const key = JSON.stringify([
1683
+ path.resolve(resolveStateDir()),
1684
+ path.resolve(params.cwd),
1685
+ params.codexHome?.trim() || process.env.CODEX_HOME?.trim() || "",
1686
+ params.home?.trim() || process.env.HOME?.trim() || "",
1687
+ params.userProfile?.trim() || process.env.USERPROFILE?.trim() || ""
1688
+ ]);
1689
+ const cached = nativeSkillIsolationByClient.get(params.client);
1690
+ if (cached?.key === key && (cached.settled || cached.signal === params.signal)) {
1691
+ const isolation = await cached.result;
1692
+ params.signal?.throwIfAborted();
1693
+ return isolation;
1694
+ }
1695
+ const result = resolveUncachedCodexNativeSkillIsolation(params);
1696
+ const entry = {
1697
+ key,
1698
+ result,
1699
+ settled: false,
1700
+ signal: params.signal
1701
+ };
1702
+ nativeSkillIsolationByClient.set(params.client, entry);
1703
+ try {
1704
+ const isolation = await result;
1705
+ entry.settled = true;
1706
+ params.signal?.throwIfAborted();
1707
+ return isolation;
1708
+ } catch (error) {
1709
+ if (nativeSkillIsolationByClient.get(params.client)?.result === result) nativeSkillIsolationByClient.delete(params.client);
1710
+ throw error;
1711
+ }
1712
+ }
1713
+ async function resolveUncachedCodexNativeSkillIsolation(params) {
1714
+ if (await usesDefaultStateDir()) return;
1715
+ const response = await params.client.request("skills/list", {
1716
+ cwds: [params.cwd],
1717
+ forceReload: true
1718
+ }, { signal: params.signal });
1719
+ const homes = [params.home?.trim() || process.env.HOME?.trim() || process.env.USERPROFILE?.trim() || os.homedir()];
1720
+ if (process.platform === "win32") homes.push(params.userProfile?.trim() || os.homedir());
1721
+ const personalSkills = await collectPersonalSkillRealPaths([...new Set(homes.map((home) => path.resolve(home)))], params.codexHome);
1722
+ return { disabledUserSkillPaths: [...personalSkills.complete ? personalSkills.skillPaths : /* @__PURE__ */ new Set([...personalSkills.skillPaths, ...response.data.flatMap((entry) => entry.skills.filter((skill) => skill.scope === "user").map((skill) => skill.path))])].toSorted((left, right) => left.localeCompare(right)) };
1723
+ }
1724
+ /** Applies path-exact session rules after caller config so isolated user skills stay disabled. */
1725
+ function applyCodexNativeSkillIsolation(config, isolation) {
1726
+ if (!isolation) return config;
1727
+ const existingRules = config?.["skills.config"];
1728
+ if (existingRules !== void 0 && !Array.isArray(existingRules)) throw new Error("Codex thread skills.config must be an array");
1729
+ const disabledRules = isolation.disabledUserSkillPaths.map((skillPath) => ({
1730
+ path: skillPath,
1731
+ enabled: false
1732
+ }));
1733
+ return {
1734
+ ...config,
1735
+ "skills.include_instructions": false,
1736
+ "skills.config": [...existingRules ?? [], ...disabledRules]
1737
+ };
1401
1738
  }
1402
1739
  //#endregion
1403
1740
  //#region extensions/codex/src/app-server/dynamic-tool-profile.ts
@@ -1476,7 +1813,8 @@ function filterCodexDynamicToolsWithOpenClawShell(tools, config, env = process.e
1476
1813
  function filterCodexDynamicToolsWithOptions(tools, config, env, options) {
1477
1814
  const excludes = /* @__PURE__ */ new Set();
1478
1815
  for (const name of CODEX_NATIVE_GOAL_TOOL_EXCLUDES) excludes.add(name);
1479
- if (!isForcedPrivateQaCodexRuntime(env)) for (const name of CODEX_APP_SERVER_OWNED_DYNAMIC_TOOL_EXCLUDES) {
1816
+ if (isForcedPrivateQaCodexRuntime(env)) excludes.add("apply_patch");
1817
+ else for (const name of CODEX_APP_SERVER_OWNED_DYNAMIC_TOOL_EXCLUDES) {
1480
1818
  if (options.preserveOpenClawShell && CODEX_APP_SERVER_OWNED_SHELL_TOOL_EXCLUDES.has(name)) continue;
1481
1819
  excludes.add(name);
1482
1820
  }
@@ -1487,13 +1825,6 @@ function filterCodexDynamicToolsWithOptions(tools, config, env, options) {
1487
1825
  return excludes.size === 0 ? tools : tools.filter((tool) => !excludes.has(normalizeCodexDynamicToolName(tool.name)));
1488
1826
  }
1489
1827
  //#endregion
1490
- //#region extensions/codex/src/app-server/profiler-flag.ts
1491
- const PROFILER_FLAGS = ["profiler", "codex.profiler"];
1492
- /** Checks the generic and Codex-specific profiler diagnostic flags. */
1493
- function isCodexAppServerProfilerEnabled(config, env = process.env) {
1494
- return PROFILER_FLAGS.some((flag) => isDiagnosticFlagEnabled(flag, config, env));
1495
- }
1496
- //#endregion
1497
1828
  //#region extensions/codex/src/app-server/thread-binding-policy.ts
1498
1829
  function shouldRotateCodexAppServerBindingForRuntime(params) {
1499
1830
  if (!params.current) return false;
@@ -1571,6 +1902,61 @@ var CodexAdoptedThreadActiveError = class extends Error {
1571
1902
  }
1572
1903
  };
1573
1904
  //#endregion
1905
+ //#region extensions/codex/src/app-server/plugin-thread-attestation.ts
1906
+ /**
1907
+ * Confirms admitted plugin and account apps against their actual Codex thread before
1908
+ * OpenClaw commits a binding or starts a turn.
1909
+ */
1910
+ var CodexPluginThreadAppAttestationError = class extends Error {
1911
+ constructor(message, options) {
1912
+ super(message, options);
1913
+ this.name = "CodexPluginThreadAppAttestationError";
1914
+ }
1915
+ };
1916
+ /** Reads the existing runtime snapshot with the started thread's effective app policy. */
1917
+ async function attestCodexPluginThreadApps(params) {
1918
+ const appIds = Array.from(new Set(params.appIds.filter(Boolean))).toSorted();
1919
+ if (appIds.length === 0) return;
1920
+ let response;
1921
+ try {
1922
+ response = await params.client.request("app/installed", {
1923
+ threadId: params.threadId,
1924
+ forceRefresh: false
1925
+ }, { signal: params.signal });
1926
+ } catch (error) {
1927
+ throw new CodexPluginThreadAppAttestationError(`Codex could not confirm admitted apps for thread ${params.threadId}`, { cause: error });
1928
+ }
1929
+ const installedById = new Map(response.apps.map((app) => [app.id, app]));
1930
+ const failures = appIds.flatMap((appId) => {
1931
+ const app = installedById.get(appId);
1932
+ if (!app) return [`${appId}:missing`];
1933
+ if (!app.enabled) return [`${appId}:disabled`];
1934
+ return app.callable ? [] : [`${appId}:not-callable`];
1935
+ });
1936
+ if (failures.length > 0) throw new CodexPluginThreadAppAttestationError(`Codex thread ${params.threadId} did not expose admitted apps: ${failures.join(", ")}`);
1937
+ }
1938
+ /** Deletes a persistent pre-turn thread; ephemeral threads can only be unsubscribed. */
1939
+ async function discardUnattestedCodexPluginThread(params) {
1940
+ if (params.ephemeral) return await unsubscribeCodexThreadBestEffort(params.client, {
1941
+ threadId: params.threadId,
1942
+ timeoutMs: CODEX_APP_SERVER_UNSUBSCRIBE_TIMEOUT_MS
1943
+ });
1944
+ try {
1945
+ await params.client.request("thread/delete", { threadId: params.threadId }, { timeoutMs: CODEX_APP_SERVER_UNSUBSCRIBE_TIMEOUT_MS });
1946
+ return true;
1947
+ } catch (error) {
1948
+ embeddedAgentLog.debug("codex plugin app attestation thread deletion failed", {
1949
+ threadId: params.threadId,
1950
+ error
1951
+ });
1952
+ await unsubscribeCodexThreadBestEffort(params.client, {
1953
+ threadId: params.threadId,
1954
+ timeoutMs: CODEX_APP_SERVER_UNSUBSCRIBE_TIMEOUT_MS
1955
+ });
1956
+ return false;
1957
+ }
1958
+ }
1959
+ //#endregion
1574
1960
  //#region extensions/codex/src/app-server/reasoning-effort.ts
1575
1961
  const CODEX_REASONING_EFFORTS = [
1576
1962
  "minimal",
@@ -1721,31 +2107,38 @@ function resolveReasoningEffort(thinkLevel, modelId, supportedReasoningEfforts)
1721
2107
  //#endregion
1722
2108
  //#region extensions/codex/src/app-server/thread-prompt.ts
1723
2109
  function buildDeveloperInstructions(params, options = {}) {
2110
+ const deferredToolNames = /* @__PURE__ */ new Set();
2111
+ let hasSkillWorkshop = false;
2112
+ let hasSessionsYield = false;
2113
+ let hasSeenDirectNamespace = false;
2114
+ let messageToolAvailable = options.dynamicTools ? false : params.disableMessageTool !== true;
2115
+ for (const spec of options.dynamicTools ?? []) {
2116
+ const isDirectNamespace = spec.type === "namespace" && !hasSeenDirectNamespace && spec.name.trim() === "openclaw_direct";
2117
+ if (isDirectNamespace) hasSeenDirectNamespace = true;
2118
+ for (const tool of spec.type === "namespace" ? spec.tools : [spec]) {
2119
+ const name = tool.name.trim();
2120
+ if (tool.deferLoading === true && name) deferredToolNames.add(name);
2121
+ hasSkillWorkshop ||= name === SKILL_WORKSHOP_TOOL_NAME;
2122
+ hasSessionsYield ||= isDirectNamespace && name === "sessions_yield";
2123
+ messageToolAvailable ||= name === "message";
2124
+ }
2125
+ }
1724
2126
  const nativeCommandGuidance = listRegisteredPluginAgentPromptGuidance({
1725
2127
  surface: "codex_app_server",
1726
2128
  includeLegacyGlobalGuidance: false
1727
2129
  }).join("\n");
1728
2130
  return [
1729
2131
  "You are a personal agent running inside OpenClaw. OpenClaw has dynamic tools for OpenClaw-owned messaging, cron, sessions, media, gateway, and nodes.",
1730
- buildDeferredDynamicToolManifest(options.dynamicTools),
1731
- buildSkillWorkshopInstruction(options.dynamicTools),
2132
+ deferredToolNames.size > 0 ? `Deferred searchable OpenClaw dynamic tools available: ${[...deferredToolNames].toSorted((left, right) => left.localeCompare(right)).join(", ")}. Use \`tool_search\` to load exact callable specs before use.` : void 0,
2133
+ hasSkillWorkshop ? buildSkillWorkshopPromptSection().join("\n") : void 0,
1732
2134
  "Use Codex native `spawn_agent` for Codex subagents. `spawn_agent` and the other native collaboration tools may be deferred: when `spawn_agent` is not directly listed, load it with `tool_search` before spawning. Use OpenClaw `sessions_spawn` only for OpenClaw or ACP delegation, never as a substitute for `spawn_agent`.",
1733
- buildVisibleReplyInstruction(params, options.dynamicTools),
2135
+ hasSessionsYield ? "When a native child's result belongs in a later turn, end the current turn with `openclaw_direct.sessions_yield`; the completion arrives as the next model-visible input. Use native `wait_agent` only for an intentional same-turn wait when the immediate next step is blocked on the child. Never loop-poll for native child completion." : void 0,
2136
+ buildVisibleReplyInstruction(params, messageToolAvailable),
1734
2137
  nativeCommandGuidance,
1735
2138
  params.extraSystemPrompt
1736
2139
  ].filter((section) => typeof section === "string" && section.trim()).join("\n\n");
1737
2140
  }
1738
- function buildDeferredDynamicToolManifest(dynamicTools) {
1739
- const deferredToolNames = [...new Set(flattenCodexDynamicToolFunctions(dynamicTools).filter((tool) => tool.deferLoading === true).map((tool) => tool.name.trim()).filter(Boolean))].toSorted((left, right) => left.localeCompare(right));
1740
- if (deferredToolNames.length === 0) return;
1741
- return `Deferred searchable OpenClaw dynamic tools available: ${deferredToolNames.join(", ")}. Use \`tool_search\` to load exact callable specs before use.`;
1742
- }
1743
- function buildSkillWorkshopInstruction(dynamicTools) {
1744
- if (!flattenCodexDynamicToolFunctions(dynamicTools).some((tool) => tool.name.trim() === SKILL_WORKSHOP_TOOL_NAME)) return;
1745
- return buildSkillWorkshopPromptSection().join("\n");
1746
- }
1747
- function buildVisibleReplyInstruction(params, dynamicTools) {
1748
- const messageToolAvailable = dynamicTools ? flattenCodexDynamicToolFunctions(dynamicTools).some((tool) => tool.name.trim() === "message") : params.disableMessageTool !== true;
2141
+ function buildVisibleReplyInstruction(params, messageToolAvailable) {
1749
2142
  if (params.sourceReplyDeliveryMode === "message_tool_only" && messageToolAvailable) return "Visible source replies are not automatically delivered for this run. Use `message(action=send)` for user-visible source-channel output. For progress, set `final=false`. When the message is the completed reply to the current source conversation, set `final=true`; OpenClaw stops after confirming delivery. If `final` is omitted, OpenClaw continues and resolves the latest omitted source reply only when the turn ends successfully. Do not repeat visible message content in your final answer.";
1750
2143
  if (messageToolAvailable) return "For the current source conversation, reply normally in your final assistant message; OpenClaw will deliver it through the active source conversation. Use `message` only for explicit out-of-band sends, media/file sends, or sends to a different target.";
1751
2144
  return "For the current source conversation, reply normally in your final assistant message; OpenClaw will deliver it through the active source conversation.";
@@ -2143,12 +2536,17 @@ async function resumeCodexAppServerThread(params) {
2143
2536
  }
2144
2537
  //#endregion
2145
2538
  //#region extensions/codex/src/app-server/thread-lifecycle-io.ts
2539
+ function resolveCodexThreadRolloutPath(thread) {
2540
+ const rolloutPath = thread.path?.trim();
2541
+ if (!rolloutPath || !path.isAbsolute(rolloutPath) || path.extname(rolloutPath) !== ".jsonl" || !path.basename(rolloutPath).includes(thread.id)) return;
2542
+ return rolloutPath;
2543
+ }
2146
2544
  async function resumeExistingCodexThread(params, context) {
2147
- const { binding: resumeBinding, bindingIdentity, startModelSelection, startModelProvider, userMcpServersConfigPatch, dynamicToolsFingerprint, dynamicToolsContainDeferred, webSearchThreadConfigFingerprint, userMcpServersFingerprint, ringZeroConfigFingerprint, ringZeroClientInstanceId, networkProxyConfigFingerprint, contextEngineBinding, environmentSelectionFingerprint, hostSystemAgentActive, ringZeroActive, ringZeroInheritedMcpServerNames, lifecycleTiming, normalizeBindingModelProvider, throwIfAborted, clearCurrentBinding } = context;
2545
+ const { binding: resumeBinding, bindingIdentity, startModelSelection, startModelProvider, userMcpServersConfigPatch, dynamicToolsFingerprint, dynamicToolsContainDeferred, webSearchThreadConfigFingerprint, nativeSkillIsolationFingerprint, userMcpServersFingerprint, ringZeroConfigFingerprint, ringZeroClientInstanceId, networkProxyConfigFingerprint, contextEngineBinding, environmentSelectionFingerprint, hostSystemAgentActive, ringZeroActive, ringZeroInheritedMcpServerNames, nativeSkillIsolation, lifecycleTiming, normalizeBindingModelProvider, throwIfAborted, clearCurrentBinding } = context;
2148
2546
  let resumeReservation;
2149
2547
  try {
2150
2548
  const authProfileId = resumeBinding.connectionScope === "supervision" ? void 0 : params.params.authProfileId ?? resumeBinding.authProfileId;
2151
- const finalConfigPatch = params.buildFinalConfigPatch?.({
2549
+ const finalConfigPatch = context.prebuiltFinalConfigPatch ?? params.buildFinalConfigPatch?.({
2152
2550
  action: "resume",
2153
2551
  binding: resumeBinding
2154
2552
  }) ?? {
@@ -2156,7 +2554,7 @@ async function resumeExistingCodexThread(params, context) {
2156
2554
  nativeHookRelayGeneration: params.nativeHookRelayGeneration
2157
2555
  };
2158
2556
  const pluginAppsConfigPatch = params.pluginThreadConfig?.enabled && resumeBinding.pluginAppPolicyContext ? buildCodexPluginAppsConfigPatchFromPolicyContext(resumeBinding.pluginAppPolicyContext) : void 0;
2159
- const resumeConfig = mergeCodexThreadConfigs(params.config, userMcpServersConfigPatch, pluginAppsConfigPatch, finalConfigPatch.configPatch);
2557
+ const resumeConfig = applyCodexNativeSkillIsolation(mergeCodexThreadConfigs(params.config, userMcpServersConfigPatch, pluginAppsConfigPatch, finalConfigPatch.configPatch), nativeSkillIsolation);
2160
2558
  const resumeParams = lifecycleTiming.measureSync("thread-resume-params", () => buildThreadResumeParams(params.params, {
2161
2559
  threadId: resumeBinding.threadId,
2162
2560
  authProfileId,
@@ -2202,6 +2600,7 @@ async function resumeExistingCodexThread(params, context) {
2202
2600
  const nextMcpServersFingerprint = params.mcpServersFingerprintEvaluated === true ? params.mcpServersFingerprint : resumeBinding.mcpServersFingerprint;
2203
2601
  const resumePatch = {
2204
2602
  cwd: params.cwd,
2603
+ rolloutPath: resolveCodexThreadRolloutPath(response.thread) ?? resumeBinding.rolloutPath,
2205
2604
  authProfileId: boundAuthProfileId,
2206
2605
  model: response.model ?? resumeParams.model ?? params.params.modelId,
2207
2606
  preserveNativeModel: resumeBinding.preserveNativeModel === true ? true : void 0,
@@ -2209,6 +2608,7 @@ async function resumeExistingCodexThread(params, context) {
2209
2608
  dynamicToolsFingerprint,
2210
2609
  dynamicToolsContainDeferred,
2211
2610
  webSearchThreadConfigFingerprint,
2611
+ nativeSkillIsolationFingerprint,
2212
2612
  userMcpServersFingerprint,
2213
2613
  mcpServersFingerprint: nextMcpServersFingerprint,
2214
2614
  ringZeroConfigFingerprint,
@@ -2250,6 +2650,13 @@ async function resumeExistingCodexThread(params, context) {
2250
2650
  ...resumeBinding,
2251
2651
  threadId: response.thread.id,
2252
2652
  ...resumePatch,
2653
+ liveThreadConfigFingerprint: fingerprintCodexThreadConfig({
2654
+ ...resumeParams,
2655
+ model: resumeBinding.preserveNativeModel === true ? null : response.model ?? resumeParams.model ?? null,
2656
+ requestedModel: resumeBinding.preserveNativeModel === true ? null : resumeParams.model ?? null,
2657
+ modelProvider: resumeBinding.preserveNativeModel === true ? null : resumePatch.modelProvider ?? null,
2658
+ requestedModelProvider: resumeBinding.preserveNativeModel === true ? null : resumeParams.modelProvider ?? resumePatch.modelProvider ?? null
2659
+ }, authProfileId, dynamicToolsFingerprint),
2253
2660
  lifecycle: {
2254
2661
  action: "resumed",
2255
2662
  ...activeTurnIds.length ? { activeTurnIds } : {}
@@ -2276,13 +2683,13 @@ async function resumeExistingCodexThread(params, context) {
2276
2683
  }
2277
2684
  async function startFreshCodexThread(params, context) {
2278
2685
  const clientId = resolveCodexAppServerClientInstanceId(params.client);
2279
- const { bindingIdentity, startModelSelection, startModelProvider, userMcpServersConfigPatch, dynamicToolsFingerprint, dynamicToolsContainDeferred, webSearchThreadConfigFingerprint, userMcpServersFingerprint, ringZeroConfigFingerprint, ringZeroClientInstanceId, networkProxyConfigFingerprint, contextEngineBinding, environmentSelectionFingerprint, hostSystemAgentActive, ringZeroActive, ringZeroInheritedMcpServerNames, lifecycleTiming, normalizeBindingModelProvider, throwIfAborted, prebuiltPluginThreadConfig, preserveExistingBinding, rotatedContextEngineBinding } = context;
2686
+ const { bindingIdentity, startModelSelection, startModelProvider, userMcpServersConfigPatch, dynamicToolsFingerprint, dynamicToolsContainDeferred, webSearchThreadConfigFingerprint, nativeSkillIsolationFingerprint, userMcpServersFingerprint, ringZeroConfigFingerprint, ringZeroClientInstanceId, networkProxyConfigFingerprint, contextEngineBinding, environmentSelectionFingerprint, hostSystemAgentActive, ringZeroActive, ringZeroInheritedMcpServerNames, nativeSkillIsolation, lifecycleTiming, normalizeBindingModelProvider, throwIfAborted, prebuiltPluginThreadConfig, preserveExistingBinding, rotatedContextEngineBinding } = context;
2280
2687
  const pluginThreadConfig = params.pluginThreadConfig?.enabled ? prebuiltPluginThreadConfig ?? await lifecycleTiming.measure("plugin-config-build", () => params.pluginThreadConfig?.build()) : void 0;
2281
2688
  const finalConfigPatch = params.buildFinalConfigPatch?.({ action: "start" }) ?? {
2282
2689
  configPatch: params.finalConfigPatch,
2283
2690
  nativeHookRelayGeneration: params.nativeHookRelayGeneration
2284
2691
  };
2285
- const config = lifecycleTiming.measureSync("merge-thread-config", () => mergeCodexThreadConfigs(params.config, userMcpServersConfigPatch, pluginThreadConfig?.configPatch, finalConfigPatch.configPatch));
2692
+ const config = lifecycleTiming.measureSync("merge-thread-config", () => applyCodexNativeSkillIsolation(mergeCodexThreadConfigs(params.config, userMcpServersConfigPatch, pluginThreadConfig?.configPatch, finalConfigPatch.configPatch), nativeSkillIsolation));
2286
2693
  const startParams = lifecycleTiming.measureSync("thread-start-params", () => buildThreadStartParams(params.params, {
2287
2694
  cwd: params.cwd,
2288
2695
  dynamicTools: params.dynamicTools,
@@ -2308,6 +2715,26 @@ async function startFreshCodexThread(params, context) {
2308
2715
  throw error;
2309
2716
  }
2310
2717
  }));
2718
+ const provisionalAppIds = pluginThreadConfig?.provisionalAppIds;
2719
+ if (provisionalAppIds?.length) try {
2720
+ await lifecycleTiming.measure("plugin-app-attestation", () => attestCodexPluginThreadApps({
2721
+ client: params.client,
2722
+ threadId: response.thread.id,
2723
+ appIds: provisionalAppIds,
2724
+ signal: params.signal
2725
+ }));
2726
+ } catch (error) {
2727
+ if (!await discardUnattestedCodexPluginThread({
2728
+ client: params.client,
2729
+ threadId: response.thread.id,
2730
+ ephemeral: startParams.ephemeral === true
2731
+ })) {
2732
+ await (params.abandonClient ?? (() => closeCodexStartupClientBestEffort(params.client)))();
2733
+ throw new CodexAppServerUnsafeSubscriptionError("Codex plugin app attestation cleanup failed", { cause: error });
2734
+ }
2735
+ throw error;
2736
+ }
2737
+ const rolloutPath = resolveCodexThreadRolloutPath(response.thread);
2311
2738
  if (ringZeroActive) try {
2312
2739
  await lifecycleTiming.measure("ring-zero-mcp-attestation", () => attestCodexRingZeroThreadHasNoMcpServers(params.client, response.thread.id, params.signal));
2313
2740
  } catch (error) {
@@ -2322,6 +2749,7 @@ async function startFreshCodexThread(params, context) {
2322
2749
  agentDir: params.params.agentDir,
2323
2750
  config: params.params.config
2324
2751
  });
2752
+ const bindingModelProvider = normalizeBindingModelProvider(params.params.authProfileId, response.modelProvider ?? requestModelProvider ?? startModelProvider ?? modelProvider);
2325
2753
  const nextMcpServersFingerprint = params.mcpServersFingerprintEvaluated === true ? params.mcpServersFingerprint : void 0;
2326
2754
  if (!preserveExistingBinding) {
2327
2755
  if (!await lifecycleTiming.measure("thread-start-write-binding", () => params.bindingStore.mutate(bindingIdentity, {
@@ -2331,12 +2759,14 @@ async function startFreshCodexThread(params, context) {
2331
2759
  threadId: response.thread.id,
2332
2760
  ...clientId ? { clientId } : {},
2333
2761
  cwd: params.cwd,
2762
+ ...rolloutPath ? { rolloutPath } : {},
2334
2763
  authProfileId: params.params.authProfileId,
2335
2764
  model: response.model ?? startParams.model ?? params.params.modelId,
2336
- modelProvider: normalizeBindingModelProvider(params.params.authProfileId, response.modelProvider ?? requestModelProvider ?? startModelProvider ?? modelProvider),
2765
+ modelProvider: bindingModelProvider,
2337
2766
  dynamicToolsFingerprint,
2338
2767
  dynamicToolsContainDeferred,
2339
2768
  webSearchThreadConfigFingerprint,
2769
+ nativeSkillIsolationFingerprint,
2340
2770
  userMcpServersFingerprint,
2341
2771
  mcpServersFingerprint: nextMcpServersFingerprint,
2342
2772
  ringZeroConfigFingerprint,
@@ -2374,11 +2804,13 @@ async function startFreshCodexThread(params, context) {
2374
2804
  threadId: response.thread.id,
2375
2805
  ...clientId ? { clientId } : {},
2376
2806
  cwd: params.cwd,
2807
+ ...rolloutPath ? { rolloutPath } : {},
2377
2808
  authProfileId: params.params.authProfileId,
2378
2809
  model: response.model ?? startParams.model ?? params.params.modelId,
2379
2810
  modelProvider: response.modelProvider ?? requestModelProvider ?? startModelProvider ?? modelProvider,
2380
2811
  dynamicToolsFingerprint,
2381
2812
  dynamicToolsContainDeferred,
2813
+ nativeSkillIsolationFingerprint,
2382
2814
  userMcpServersFingerprint,
2383
2815
  mcpServersFingerprint: nextMcpServersFingerprint,
2384
2816
  ringZeroConfigFingerprint,
@@ -2392,6 +2824,13 @@ async function startFreshCodexThread(params, context) {
2392
2824
  pluginAppPolicyContext: pluginThreadConfig?.policyContext,
2393
2825
  contextEngine: contextEngineBinding,
2394
2826
  environmentSelectionFingerprint,
2827
+ ...!preserveExistingBinding ? { liveThreadConfigFingerprint: fingerprintCodexThreadConfig({
2828
+ ...startParams,
2829
+ model: response.model ?? startParams.model ?? null,
2830
+ requestedModel: startParams.model ?? null,
2831
+ modelProvider: bindingModelProvider ?? null,
2832
+ requestedModelProvider: startParams.modelProvider ?? bindingModelProvider ?? null
2833
+ }, params.params.authProfileId, dynamicToolsFingerprint) } : {},
2395
2834
  lifecycle: {
2396
2835
  action: "started",
2397
2836
  ...rotatedContextEngineBinding ? { rotatedContextEngineBinding } : {}
@@ -2399,6 +2838,13 @@ async function startFreshCodexThread(params, context) {
2399
2838
  };
2400
2839
  }
2401
2840
  //#endregion
2841
+ //#region extensions/codex/src/app-server/profiler-flag.ts
2842
+ const PROFILER_FLAGS = ["profiler", "codex.profiler"];
2843
+ /** Checks the generic and Codex-specific profiler diagnostic flags. */
2844
+ function isCodexAppServerProfilerEnabled(config, env = process.env) {
2845
+ return PROFILER_FLAGS.some((flag) => isDiagnosticFlagEnabled(flag, config, env));
2846
+ }
2847
+ //#endregion
2402
2848
  //#region extensions/codex/src/app-server/thread-lifecycle-timing.ts
2403
2849
  const CODEX_THREAD_LIFECYCLE_TIMING_WARN_TOTAL_MS = 1e3;
2404
2850
  const CODEX_THREAD_LIFECYCLE_TIMING_WARN_STAGE_MS = 500;
@@ -2488,6 +2934,174 @@ function createCodexThreadLifecycleTimingTracker(options = {}) {
2488
2934
  };
2489
2935
  }
2490
2936
  //#endregion
2937
+ //#region extensions/codex/src/app-server/thread-lifecycle-preflight.ts
2938
+ async function prepareCodexThreadLifecyclePreflight(params) {
2939
+ const lifecycleTiming = createCodexThreadLifecycleTimingTracker({
2940
+ ...params.timing,
2941
+ enabled: params.timing?.enabled ?? isCodexAppServerProfilerEnabled(params.params.config)
2942
+ });
2943
+ const legacyDynamicToolsFingerprint = lifecycleTiming.measureSync("legacy-dynamic-tools-fingerprint", () => codexLegacyDynamicToolsFingerprint(params.dynamicTools));
2944
+ const dynamicToolsFingerprint = lifecycleTiming.measureSync("dynamic-tools-fingerprint", () => hashCodexAppServerBindingFingerprint(legacyDynamicToolsFingerprint));
2945
+ const dynamicToolsContainDeferred = flattenCodexDynamicToolFunctions(params.dynamicTools).some((tool) => tool.deferLoading === true);
2946
+ const webSearchThreadConfigFingerprint = fingerprintJsonObject(lifecycleTiming.measureSync("web-search-plan", () => resolveCodexWebSearchPlan({
2947
+ config: params.params.config,
2948
+ disableTools: params.params.disableTools,
2949
+ nativeToolSurfaceEnabled: params.nativeCodeModeEnabled,
2950
+ nativeProviderWebSearchSupport: params.nativeProviderWebSearchSupport,
2951
+ webSearchAllowed: params.webSearchAllowed
2952
+ })).threadConfig);
2953
+ const networkProxyConfigFingerprint = params.appServer.networkProxy?.configFingerprint;
2954
+ const contextEngineBinding = lifecycleTiming.measureSync("context-engine-binding", () => buildContextEngineBinding(params.params, params.contextEngineProjection));
2955
+ const userMcpServersConfigPatch = params.userMcpServersEnabled === false ? void 0 : await buildCodexUserMcpServersThreadConfigPatchForRuntime(params.params.config, {
2956
+ agentId: params.agentId ?? params.params.agentId,
2957
+ agentDir: params.params.agentDir,
2958
+ allowLiteralOAuthProjection: params.appServer.connectionClass !== "remote",
2959
+ toolOverrides: params.params.toolOverrides,
2960
+ onServerUnavailable: (serverName, error) => embeddedAgentLog.warn("skipping unavailable MCP OAuth server", {
2961
+ serverName,
2962
+ error: formatErrorMessage(error)
2963
+ })
2964
+ });
2965
+ const nativeSkillIsolation = await lifecycleTiming.measure("native-skill-isolation", () => resolveCodexNativeSkillIsolation({
2966
+ client: params.client,
2967
+ codexHome: params.appServer.start.env?.CODEX_HOME,
2968
+ cwd: params.cwd,
2969
+ home: params.appServer.start.env?.HOME,
2970
+ signal: params.signal,
2971
+ userProfile: params.appServer.start.env?.USERPROFILE
2972
+ }));
2973
+ const nativeSkillIsolationFingerprint = nativeSkillIsolation ? fingerprintJsonObject({
2974
+ version: 1,
2975
+ disabledUserSkillPaths: nativeSkillIsolation.disabledUserSkillPaths
2976
+ }) : void 0;
2977
+ const legacyUserMcpServersFingerprint = legacyFingerprintUserMcpServersConfigPatch(userMcpServersConfigPatch);
2978
+ const userMcpServersFingerprint = fingerprintUserMcpServersConfigPatch(userMcpServersConfigPatch);
2979
+ const environmentSelectionFingerprint = fingerprintEnvironmentSelection(params.environmentSelection);
2980
+ const hostSystemAgentActive = params.hostSystemAgentActive ?? isHostScopedAgentToolActive("openclaw");
2981
+ const ringZeroActive = hostSystemAgentActive && isSystemAgentOnlyCodexDynamicToolAllowlist(params.params.toolsAllow);
2982
+ if (ringZeroActive && params.nativeCodeModeEnabled !== false) throw new Error("Codex ring-zero requires native code mode to be disabled");
2983
+ const ringZeroInheritedMcpServerNames = ringZeroActive ? await lifecycleTiming.measure("ring-zero-mcp-config-read", () => readCodexInheritedMcpServerNames(params.client, params.cwd, params.signal)) : [];
2984
+ if (ringZeroActive) await lifecycleTiming.measure("ring-zero-config-requirements-read", () => assertCodexRingZeroHasNoManagedHooks(params.client, params.signal));
2985
+ const ringZeroConfigFingerprint = ringZeroActive ? fingerprintJsonObject({
2986
+ version: 1,
2987
+ baseInstructions: "",
2988
+ config: buildCodexRingZeroThreadConfigPatch(params.params, true, ringZeroInheritedMcpServerNames)
2989
+ }) : void 0;
2990
+ return {
2991
+ contextEngineBinding,
2992
+ dynamicToolsContainDeferred,
2993
+ dynamicToolsFingerprint,
2994
+ environmentSelectionFingerprint,
2995
+ hostSystemAgentActive,
2996
+ legacyDynamicToolsFingerprint,
2997
+ legacyUserMcpServersFingerprint,
2998
+ lifecycleTiming,
2999
+ nativeSkillIsolation,
3000
+ nativeSkillIsolationFingerprint,
3001
+ networkProxyConfigFingerprint,
3002
+ ringZeroActive,
3003
+ ringZeroClientInstanceId: ringZeroActive ? getCodexAppServerClientInstanceId(params.client) : void 0,
3004
+ ringZeroConfigFingerprint,
3005
+ ringZeroInheritedMcpServerNames,
3006
+ userMcpServersConfigPatch,
3007
+ userMcpServersFingerprint,
3008
+ webSearchThreadConfigFingerprint
3009
+ };
3010
+ }
3011
+ //#endregion
3012
+ //#region extensions/codex/src/app-server/thread-lifecycle-warm.ts
3013
+ /** Preserves the caller's abort reason across thread ownership transitions. */
3014
+ function throwIfCodexThreadLifecycleAborted(signal) {
3015
+ if (!signal?.aborted) return;
3016
+ const reason = signal.reason;
3017
+ if (reason instanceof Error) throw reason;
3018
+ const error = new Error(typeof reason === "string" && reason.length > 0 ? reason : "codex app-server thread lifecycle aborted");
3019
+ error.name = "AbortError";
3020
+ throw error;
3021
+ }
3022
+ /** Releases consumed subscription ownership or retires an unsafe client. */
3023
+ async function releaseCodexConsumedLiveThread(options) {
3024
+ if (await options.lifecycleTiming.measure("retained-thread-unsubscribe", () => unsubscribeCodexThreadBestEffort(options.client, {
3025
+ threadId: options.threadId,
3026
+ timeoutMs: 5e3
3027
+ }))) return;
3028
+ await (options.abandonClient ?? (() => closeCodexStartupClientBestEffort(options.client)))();
3029
+ throw new CodexAppServerUnsafeSubscriptionError(`Codex retained thread subscription could not be released: ${options.threadId}`, options.cause !== void 0 ? { cause: options.cause } : void 0);
3030
+ }
3031
+ /** Transfers retained-slot ownership before unconditionally releasing it. */
3032
+ async function releaseCodexRetainedLiveThread(options) {
3033
+ if (await consumeCodexAppServerLiveThread(options.client, options.threadId)) await releaseCodexConsumedLiveThread(options);
3034
+ }
3035
+ /** Reuses one safely owned, fully matching subscription on its original client. */
3036
+ async function tryReuseCodexLiveThread(options) {
3037
+ const { params, binding, bindingIdentity, clientId, contextEngineBinding, dynamicToolsFingerprint, hostSystemAgentActive, lifecycleTiming, nativeSkillIsolation, releaseConsumedThread, ringZeroActive, ringZeroInheritedMcpServerNames, startModelProvider, startModelSelection, throwIfAborted, userMcpServersConfigPatch } = options;
3038
+ if (!binding.clientId || binding.clientId !== clientId || binding.preserveNativeModel === true || binding.connectionScope === "supervision" || ringZeroActive || contextEngineBinding) return {};
3039
+ const prebuiltFinalConfigPatch = params.buildFinalConfigPatch?.({
3040
+ action: "resume",
3041
+ binding
3042
+ }) ?? {
3043
+ configPatch: params.finalConfigPatch,
3044
+ nativeHookRelayGeneration: params.nativeHookRelayGeneration
3045
+ };
3046
+ const pluginAppsConfigPatch = params.pluginThreadConfig?.enabled && binding.pluginAppPolicyContext ? buildCodexPluginAppsConfigPatchFromPolicyContext(binding.pluginAppPolicyContext) : void 0;
3047
+ const resumeAuthProfileId = params.params.authProfileId ?? binding.authProfileId;
3048
+ const resumeConfig = mergeCodexThreadConfigs(params.config, userMcpServersConfigPatch, pluginAppsConfigPatch, prebuiltFinalConfigPatch.configPatch);
3049
+ const resumeParams = lifecycleTiming.measureSync("warm-thread-resume-params", () => buildThreadResumeParams(params.params, {
3050
+ threadId: binding.threadId,
3051
+ authProfileId: resumeAuthProfileId,
3052
+ model: startModelSelection.model,
3053
+ modelProvider: startModelProvider,
3054
+ preserveNativeModel: false,
3055
+ appServer: params.appServer,
3056
+ dynamicTools: params.dynamicTools,
3057
+ developerInstructions: params.developerInstructions,
3058
+ config: applyCodexNativeSkillIsolation(resumeConfig, nativeSkillIsolation),
3059
+ nativeCodeModeEnabled: params.nativeCodeModeEnabled,
3060
+ nativeProviderWebSearchSupport: params.nativeProviderWebSearchSupport,
3061
+ nativeCodeModeOnlyEnabled: params.nativeCodeModeOnlyEnabled,
3062
+ webSearchAllowed: params.webSearchAllowed,
3063
+ hostSystemAgentActive,
3064
+ ringZeroInheritedMcpServerNames
3065
+ }));
3066
+ const liveThreadConfigFingerprint = fingerprintCodexThreadConfig({
3067
+ ...resumeParams,
3068
+ model: binding.model ?? resumeParams.model ?? null,
3069
+ requestedModel: resumeParams.model ?? null,
3070
+ modelProvider: binding.modelProvider ?? resumeParams.modelProvider ?? null,
3071
+ requestedModelProvider: resumeParams.modelProvider ?? binding.modelProvider ?? null
3072
+ }, resumeAuthProfileId, dynamicToolsFingerprint);
3073
+ if (!await consumeCodexAppServerLiveThread(params.client, binding.threadId, liveThreadConfigFingerprint)) return { prebuiltFinalConfigPatch };
3074
+ try {
3075
+ const nativeHookRelayGeneration = prebuiltFinalConfigPatch.nativeHookRelayGeneration ?? binding.nativeHookRelayGeneration;
3076
+ if (!await lifecycleTiming.measure("warm-thread-write-binding", () => params.bindingStore.mutate(bindingIdentity, {
3077
+ kind: "patch",
3078
+ threadId: binding.threadId,
3079
+ patch: { nativeHookRelayGeneration }
3080
+ }))) throw new CodexThreadBindingConflictError(binding.threadId, "committing a reused thread");
3081
+ throwIfAborted();
3082
+ lifecycleTiming.mark("thread-ready");
3083
+ lifecycleTiming.logSummary({
3084
+ runId: params.params.runId,
3085
+ sessionId: params.params.sessionId,
3086
+ sessionKey: params.params.sessionKey,
3087
+ threadId: binding.threadId,
3088
+ action: "resumed"
3089
+ });
3090
+ return {
3091
+ binding: {
3092
+ ...binding,
3093
+ nativeHookRelayGeneration,
3094
+ liveThreadConfigFingerprint,
3095
+ lifecycle: { action: "resumed" }
3096
+ },
3097
+ prebuiltFinalConfigPatch
3098
+ };
3099
+ } catch (error) {
3100
+ await releaseConsumedThread(binding.threadId, error);
3101
+ throw error;
3102
+ }
3103
+ }
3104
+ //#endregion
2491
3105
  //#region extensions/codex/src/app-server/thread-supervision.ts
2492
3106
  async function materializePendingSupervisionBranch(params) {
2493
3107
  let pending = params.binding.pendingSupervisionBranch;
@@ -2577,6 +3191,25 @@ async function materializePendingSupervisionBranch(params) {
2577
3191
  modelProvider: nativeModelProvider,
2578
3192
  operation: "thread/start response"
2579
3193
  });
3194
+ if (params.provisionalAppIds?.length) try {
3195
+ await params.lifecycleTiming.measure("plugin-app-attestation", () => attestCodexPluginThreadApps({
3196
+ client: params.client,
3197
+ threadId: finalThreadId,
3198
+ appIds: params.provisionalAppIds ?? [],
3199
+ signal: params.signal
3200
+ }));
3201
+ } catch (error) {
3202
+ if (!await discardUnattestedCodexPluginThread({
3203
+ client: params.client,
3204
+ threadId: finalThreadId,
3205
+ ephemeral: startParams.ephemeral === true
3206
+ }) || !await archiveSupervisionArtifact(params.client, probeThreadId)) {
3207
+ provisionalCleanupSafe = false;
3208
+ throw new CodexAppServerUnsafeSubscriptionError("Codex supervised plugin app attestation cleanup failed", { cause: error });
3209
+ }
3210
+ pending = await trackPendingSupervisionArtifacts(params, pending, []);
3211
+ throw error;
3212
+ }
2580
3213
  if (history.responseItems.length > 0) {
2581
3214
  await params.lifecycleTiming.measure("supervision-history-inject", () => params.client.request("thread/inject_items", {
2582
3215
  threadId: finalThreadId,
@@ -2820,46 +3453,9 @@ async function startOrResumeThread(params) {
2820
3453
  config: params.params.config
2821
3454
  });
2822
3455
  return await params.bindingStore.withLease(bindingIdentity, async () => {
2823
- const lifecycleTiming = createCodexThreadLifecycleTimingTracker({
2824
- ...params.timing,
2825
- enabled: params.timing?.enabled ?? isCodexAppServerProfilerEnabled(params.params.config)
2826
- });
2827
- const legacyDynamicToolsFingerprint = lifecycleTiming.measureSync("legacy-dynamic-tools-fingerprint", () => codexLegacyDynamicToolsFingerprint(params.dynamicTools));
2828
- const dynamicToolsFingerprint = lifecycleTiming.measureSync("dynamic-tools-fingerprint", () => hashCodexAppServerBindingFingerprint(legacyDynamicToolsFingerprint));
2829
- const dynamicToolsContainDeferred = flattenCodexDynamicToolFunctions(params.dynamicTools).some((tool) => tool.deferLoading === true);
2830
- const webSearchThreadConfigFingerprint = fingerprintJsonObject(lifecycleTiming.measureSync("web-search-plan", () => resolveCodexWebSearchPlan({
2831
- config: params.params.config,
2832
- disableTools: params.params.disableTools,
2833
- nativeToolSurfaceEnabled: params.nativeCodeModeEnabled,
2834
- nativeProviderWebSearchSupport: params.nativeProviderWebSearchSupport,
2835
- webSearchAllowed: params.webSearchAllowed
2836
- })).threadConfig);
2837
- const networkProxyConfigFingerprint = params.appServer.networkProxy?.configFingerprint;
2838
- const contextEngineBinding = lifecycleTiming.measureSync("context-engine-binding", () => buildContextEngineBinding(params.params, params.contextEngineProjection));
2839
- const userMcpServersConfigPatch = params.userMcpServersEnabled === false ? void 0 : await buildCodexUserMcpServersThreadConfigPatchForRuntime(params.params.config, {
2840
- agentId: params.agentId ?? params.params.agentId,
2841
- agentDir: params.params.agentDir,
2842
- allowLiteralOAuthProjection: params.appServer.connectionClass !== "remote",
2843
- onServerUnavailable: (serverName, error) => embeddedAgentLog.warn("skipping unavailable MCP OAuth server", {
2844
- serverName,
2845
- error: formatErrorMessage(error)
2846
- })
2847
- });
2848
- const legacyUserMcpServersFingerprint = legacyFingerprintUserMcpServersConfigPatch(userMcpServersConfigPatch);
2849
- const userMcpServersFingerprint = fingerprintUserMcpServersConfigPatch(userMcpServersConfigPatch);
2850
- const environmentSelectionFingerprint = fingerprintEnvironmentSelection(params.environmentSelection);
2851
- const hostSystemAgentActive = params.hostSystemAgentActive ?? isHostScopedAgentToolActive("openclaw");
2852
- const ringZeroActive = hostSystemAgentActive && isSystemAgentOnlyCodexDynamicToolAllowlist(params.params.toolsAllow);
2853
- if (ringZeroActive && params.nativeCodeModeEnabled !== false) throw new Error("Codex ring-zero requires native code mode to be disabled");
2854
- const ringZeroInheritedMcpServerNames = ringZeroActive ? await lifecycleTiming.measure("ring-zero-mcp-config-read", () => readCodexInheritedMcpServerNames(params.client, params.cwd, params.signal)) : [];
2855
- if (ringZeroActive) await lifecycleTiming.measure("ring-zero-config-requirements-read", () => assertCodexRingZeroHasNoManagedHooks(params.client, params.signal));
2856
- const ringZeroConfigFingerprint = ringZeroActive ? fingerprintJsonObject({
2857
- version: 1,
2858
- baseInstructions: "",
2859
- config: buildCodexRingZeroThreadConfigPatch(params.params, true, ringZeroInheritedMcpServerNames)
2860
- }) : void 0;
2861
- const ringZeroClientInstanceId = ringZeroActive ? getCodexAppServerClientInstanceId(params.client) : void 0;
3456
+ const { contextEngineBinding, dynamicToolsContainDeferred, dynamicToolsFingerprint, environmentSelectionFingerprint, hostSystemAgentActive, legacyDynamicToolsFingerprint, legacyUserMcpServersFingerprint, lifecycleTiming, nativeSkillIsolation, nativeSkillIsolationFingerprint, networkProxyConfigFingerprint, ringZeroActive, ringZeroClientInstanceId, ringZeroConfigFingerprint, ringZeroInheritedMcpServerNames, userMcpServersConfigPatch, userMcpServersFingerprint, webSearchThreadConfigFingerprint } = await prepareCodexThreadLifecyclePreflight(params);
2862
3457
  let binding = await lifecycleTiming.measure("read-binding", () => params.bindingStore.read(bindingIdentity));
3458
+ const initialBoundThreadId = binding?.threadId;
2863
3459
  const normalizeBindingModelProvider = (authProfileId, modelProvider) => normalizeCodexAppServerBindingModelProvider({
2864
3460
  authProfileId,
2865
3461
  modelProvider,
@@ -2867,14 +3463,13 @@ async function startOrResumeThread(params) {
2867
3463
  agentDir: params.params.agentDir,
2868
3464
  config: params.params.config
2869
3465
  });
2870
- const throwIfAborted = () => {
2871
- if (!params.signal?.aborted) return;
2872
- const reason = params.signal.reason;
2873
- if (reason instanceof Error) throw reason;
2874
- const error = new Error(typeof reason === "string" && reason.length > 0 ? reason : "codex app-server thread lifecycle aborted");
2875
- error.name = "AbortError";
2876
- throw error;
2877
- };
3466
+ const throwIfAborted = () => throwIfCodexThreadLifecycleAborted(params.signal);
3467
+ const releaseRetainedThread = (threadId) => releaseCodexRetainedLiveThread({
3468
+ client: params.client,
3469
+ abandonClient: params.abandonClient,
3470
+ lifecycleTiming,
3471
+ threadId
3472
+ });
2878
3473
  if (!binding && bindingIdentity.kind === "session" && bindingIdentity.sessionKey) {
2879
3474
  if (!await lifecycleTiming.measure("reclaim-binding-generation", () => reclaimCurrentCodexSessionGeneration({
2880
3475
  bindingStore: params.bindingStore,
@@ -2883,13 +3478,14 @@ async function startOrResumeThread(params) {
2883
3478
  }))) throw createCodexSessionGenerationSupersededError(bindingIdentity.sessionId);
2884
3479
  }
2885
3480
  if (binding?.pendingSupervisionBranch) {
3481
+ await releaseRetainedThread(binding.threadId);
2886
3482
  const pendingBinding = binding;
2887
3483
  const pluginThreadConfig = params.pluginThreadConfig?.enabled ? await lifecycleTiming.measure("plugin-config-build", () => params.pluginThreadConfig?.build()) : void 0;
2888
3484
  const finalConfigPatch = params.buildFinalConfigPatch?.({ action: "start" }) ?? {
2889
3485
  configPatch: params.finalConfigPatch,
2890
3486
  nativeHookRelayGeneration: params.nativeHookRelayGeneration
2891
3487
  };
2892
- const config = lifecycleTiming.measureSync("merge-thread-config", () => mergeCodexThreadConfigs(params.config, userMcpServersConfigPatch, pluginThreadConfig?.configPatch, finalConfigPatch.configPatch));
3488
+ const config = lifecycleTiming.measureSync("merge-thread-config", () => applyCodexNativeSkillIsolation(mergeCodexThreadConfigs(params.config, userMcpServersConfigPatch, pluginThreadConfig?.configPatch, finalConfigPatch.configPatch), nativeSkillIsolation));
2893
3489
  return await materializePendingSupervisionBranch({
2894
3490
  client: params.client,
2895
3491
  abandonClient: params.abandonClient ?? (() => closeCodexStartupClientBestEffort(params.client)),
@@ -2907,6 +3503,7 @@ async function startOrResumeThread(params) {
2907
3503
  nativeCodeModeOnlyEnabled: params.nativeCodeModeOnlyEnabled,
2908
3504
  webSearchAllowed: params.webSearchAllowed,
2909
3505
  environmentSelection: params.environmentSelection,
3506
+ provisionalAppIds: pluginThreadConfig?.provisionalAppIds,
2910
3507
  signal: params.signal,
2911
3508
  throwIfAborted,
2912
3509
  lifecycleTiming,
@@ -2919,6 +3516,7 @@ async function startOrResumeThread(params) {
2919
3516
  dynamicToolsFingerprint,
2920
3517
  dynamicToolsContainDeferred,
2921
3518
  webSearchThreadConfigFingerprint,
3519
+ nativeSkillIsolationFingerprint,
2922
3520
  userMcpServersFingerprint,
2923
3521
  mcpServersFingerprint: params.mcpServersFingerprintEvaluated === true ? params.mcpServersFingerprint : pendingBinding.mcpServersFingerprint,
2924
3522
  networkProxyProfileName: params.appServer.networkProxy?.profileName,
@@ -2944,6 +3542,10 @@ async function startOrResumeThread(params) {
2944
3542
  })) throw new CodexThreadBindingConflictError(current.threadId, operation);
2945
3543
  binding = void 0;
2946
3544
  };
3545
+ if (binding?.threadId && binding.nativeSkillIsolationFingerprint !== nativeSkillIsolationFingerprint) {
3546
+ embeddedAgentLog.debug("codex app-server native skill isolation changed; starting a new thread", { threadId: binding.threadId });
3547
+ await clearCurrentBinding("rotating stale native skill isolation");
3548
+ }
2947
3549
  if (binding?.threadId && (binding.ringZeroConfigFingerprint !== ringZeroConfigFingerprint || binding.ringZeroClientInstanceId !== ringZeroClientInstanceId) && (ringZeroActive || binding.ringZeroConfigFingerprint !== void 0)) {
2948
3550
  embeddedAgentLog.debug("codex app-server ring-zero restriction changed; rotating thread", { threadId: binding.threadId });
2949
3551
  await clearCurrentBinding("rotating a ring-zero thread binding");
@@ -3130,6 +3732,32 @@ async function startOrResumeThread(params) {
3130
3732
  await clearCurrentBinding("rotating an unavailable ephemeral thread binding");
3131
3733
  binding = void 0;
3132
3734
  } else {
3735
+ const warmReuse = await tryReuseCodexLiveThread({
3736
+ params,
3737
+ binding,
3738
+ bindingIdentity,
3739
+ clientId,
3740
+ contextEngineBinding,
3741
+ dynamicToolsFingerprint,
3742
+ hostSystemAgentActive,
3743
+ lifecycleTiming,
3744
+ nativeSkillIsolation,
3745
+ releaseConsumedThread: (threadId, cause) => releaseCodexConsumedLiveThread({
3746
+ client: params.client,
3747
+ abandonClient: params.abandonClient,
3748
+ lifecycleTiming,
3749
+ threadId,
3750
+ cause
3751
+ }),
3752
+ ringZeroActive,
3753
+ ringZeroInheritedMcpServerNames,
3754
+ startModelProvider,
3755
+ startModelSelection,
3756
+ throwIfAborted,
3757
+ userMcpServersConfigPatch
3758
+ });
3759
+ if (warmReuse.binding) return warmReuse.binding;
3760
+ await releaseRetainedThread(binding.threadId);
3133
3761
  const resumed = await resumeExistingCodexThread(params, {
3134
3762
  binding,
3135
3763
  bindingIdentity,
@@ -3139,6 +3767,7 @@ async function startOrResumeThread(params) {
3139
3767
  dynamicToolsFingerprint,
3140
3768
  dynamicToolsContainDeferred,
3141
3769
  webSearchThreadConfigFingerprint,
3770
+ nativeSkillIsolationFingerprint,
3142
3771
  userMcpServersFingerprint,
3143
3772
  ringZeroConfigFingerprint,
3144
3773
  ringZeroClientInstanceId,
@@ -3148,13 +3777,16 @@ async function startOrResumeThread(params) {
3148
3777
  hostSystemAgentActive,
3149
3778
  ringZeroActive,
3150
3779
  ringZeroInheritedMcpServerNames,
3780
+ nativeSkillIsolation,
3151
3781
  lifecycleTiming,
3152
3782
  normalizeBindingModelProvider,
3153
3783
  throwIfAborted,
3154
- clearCurrentBinding
3784
+ clearCurrentBinding,
3785
+ prebuiltFinalConfigPatch: warmReuse.prebuiltFinalConfigPatch
3155
3786
  });
3156
3787
  if (resumed) return resumed;
3157
3788
  }
3789
+ if (initialBoundThreadId) await releaseRetainedThread(initialBoundThreadId);
3158
3790
  return await startFreshCodexThread(params, {
3159
3791
  bindingIdentity,
3160
3792
  startModelSelection,
@@ -3163,6 +3795,7 @@ async function startOrResumeThread(params) {
3163
3795
  dynamicToolsFingerprint,
3164
3796
  dynamicToolsContainDeferred,
3165
3797
  webSearchThreadConfigFingerprint,
3798
+ nativeSkillIsolationFingerprint,
3166
3799
  userMcpServersFingerprint,
3167
3800
  ringZeroConfigFingerprint,
3168
3801
  ringZeroClientInstanceId,
@@ -3172,6 +3805,7 @@ async function startOrResumeThread(params) {
3172
3805
  hostSystemAgentActive,
3173
3806
  ringZeroActive,
3174
3807
  ringZeroInheritedMcpServerNames,
3808
+ nativeSkillIsolation,
3175
3809
  lifecycleTiming,
3176
3810
  normalizeBindingModelProvider,
3177
3811
  throwIfAborted,
@@ -3282,7 +3916,7 @@ function buildTurnStartParams(params, options) {
3282
3916
  cwd: options.cwd,
3283
3917
  approvalPolicy: options.appServer.approvalPolicy,
3284
3918
  approvalsReviewer: options.appServer.approvalsReviewer,
3285
- ...useThreadPermissionProfile ? {} : { sandboxPolicy: options.sandboxPolicy ?? codexSandboxPolicyForTurn(options.appServer.sandbox, options.cwd) },
3919
+ ...useThreadPermissionProfile ? {} : { sandboxPolicy: options.sandboxPolicy ?? codexSandboxPolicyForTurn(options.appServer.sandbox, options.cwd, options.appServer.start?.args) },
3286
3920
  ...modelSelection ? {
3287
3921
  model: modelSelection.model,
3288
3922
  personality: CODEX_NATIVE_PERSONALITY_NONE
@@ -3350,4 +3984,4 @@ function joinPresentSections(...sections) {
3350
3984
  return sections.filter((section) => Boolean(section?.trim())).join("\n\n");
3351
3985
  }
3352
3986
  //#endregion
3353
- export { shouldBuildCodexPluginThreadConfig as $, resolveCodexDynamicToolsLoading as A, codexDynamicToolsFingerprint as B, readCodexSupportedReasoningEfforts as C, isForcedPrivateQaCodexRuntime as D, filterCodexDynamicToolsWithOpenClawShell as E, closeCodexStartupClientBestEffort as F, projectContextEngineAssemblyForCodex as G, buildContextEngineBinding as H, interruptCodexTurnBestEffort as I, buildCodexPluginAppsConfigPatchFromPolicyContext as J, resolveCodexContextEngineProjectionMaxChars as K, retireCodexAppServerClientAfterTimedOutTurn as L, CODEX_APP_SERVER_INTERRUPT_TIMEOUT_MS as M, CODEX_APP_SERVER_UNSUBSCRIBE_TIMEOUT_MS as N, isSystemAgentOnlyCodexDynamicToolAllowlist as O, CodexAppServerUnsafeSubscriptionError as P, mergeCodexThreadConfigs as Q, unsubscribeCodexThreadBestEffort as R, resolveReasoningEffort as S, filterCodexDynamicTools as T, isContextEngineBindingCompatible as U, codexLegacyDynamicToolsFingerprint as V, fitCodexProjectedContextForTurnStart as W, buildCodexPluginThreadConfigInputFingerprint as X, buildCodexPluginThreadConfig as Y, buildCodexPluginThreadConfigTimeoutFallback as Z, CODEX_NATIVE_PERSONALITY_NONE as _, sanitizeCodexHistoryImagePayloads as a, readCodexErrorNotificationMessage as at, resolveCodexAppServerThreadModelSelection as b, resumeCodexAppServerThread as c, readItemString as ct, buildCodexRingZeroThreadConfigPatch as d, readNonNegativeInteger as dt, ensureCodexPluginActivation as et, buildCodexRuntimeThreadConfig as f, readNullableString as ft, buildDeveloperInstructions as g, resolveCodexWebSearchPlan as h, splitPlanText as ht, invalidInlineImageText as i, normalizeNonEmptyString as it, resolveCodexDynamicToolsLoadingForRuntime as j, normalizeCodexDynamicToolName as k, assertCodexRingZeroHasNoManagedHooks as l, readNonEmptyString as lt, buildCodexNativeWebSearchThreadConfig as m, readString as mt, buildTurnStartParams as n, resolveRecoverableCodexPluginConfigKeys as nt, sanitizeInlineImageDataUrl$1 as o, readHookOutputEntries as ot, readCodexInheritedMcpServerNames as p, readNumber as pt, resolveCodexContextEngineProjectionReserveTokens as q, buildCodexUserInput as r, extractRawAssistantText as rt, startOrResumeThread as s, readItem as st, buildTurnCollaborationMode as t, pluginReadParams as tt, attestCodexRingZeroThreadHasNoMcpServers as u, readNonEmptyStringArray as ut, resolveCodexAppServerModelProvider as v, isCodexAppServerProfilerEnabled as w, resolveCodexBindingModelProviderFallback as x, resolveCodexAppServerRequestModelSelection as y, areCodexDynamicToolFingerprintsCompatible as z };
3987
+ export { readCodexErrorNotificationMessage as $, resolveCodexDynamicToolsLoading as A, resolveCodexContextEngineProjectionReserveTokens as B, resolveReasoningEffort as C, isForcedPrivateQaCodexRuntime as D, filterCodexDynamicToolsWithOpenClawShell as E, buildContextEngineBinding as F, mergeCodexThreadConfigs as G, buildCodexPluginThreadConfig as H, isContextEngineBindingCompatible as I, isOpenAiCuratedMarketplace as J, shouldBuildCodexPluginThreadConfig as K, fitCodexProjectedContextForTurnStart as L, areCodexDynamicToolFingerprintsCompatible as M, codexDynamicToolsFingerprint as N, isSystemAgentOnlyCodexDynamicToolAllowlist as O, codexLegacyDynamicToolsFingerprint as P, normalizeNonEmptyString as Q, projectContextEngineAssemblyForCodex as R, resolveCodexBindingModelProviderFallback as S, filterCodexDynamicTools as T, buildCodexPluginThreadConfigInputFingerprint as U, buildCodexPluginAppsConfigPatchFromPolicyContext as V, buildCodexPluginThreadConfigTimeoutFallback as W, resolveRecoverableCodexPluginConfigKeys as X, pluginReadParams as Y, extractRawAssistantText as Z, buildDeveloperInstructions as _, unsubscribeCodexThreadBestEffort as _t, sanitizeCodexHistoryImagePayloads as a, readNonNegativeInteger as at, resolveCodexAppServerRequestModelSelection as b, isCodexAppServerProfilerEnabled as c, readString as ct, attestCodexRingZeroThreadHasNoMcpServers as d, CODEX_APP_SERVER_UNSUBSCRIBE_TIMEOUT_MS as dt, readHookOutputEntries as et, buildCodexRingZeroThreadConfigPatch as f, CodexAppServerUnsafeSubscriptionError as ft, resolveCodexWebSearchPlan as g, retireCodexAppServerClientAfterTimedOutTurn as gt, buildCodexNativeWebSearchThreadConfig as h, interruptCodexTurnBestEffort as ht, invalidInlineImageText as i, readNonEmptyStringArray as it, resolveCodexDynamicToolsLoadingForRuntime as j, normalizeCodexDynamicToolName as k, resumeCodexAppServerThread as l, splitPlanText as lt, readCodexInheritedMcpServerNames as m, interruptCodexTurnAndWaitBestEffort as mt, buildTurnStartParams as n, readItemString as nt, sanitizeInlineImageDataUrl$1 as o, readNullableString as ot, buildCodexRuntimeThreadConfig as p, closeCodexStartupClientBestEffort as pt, ensureCodexPluginActivation as q, buildCodexUserInput as r, readNonEmptyString as rt, startOrResumeThread as s, readNumber as st, buildTurnCollaborationMode as t, readItem as tt, assertCodexRingZeroHasNoManagedHooks as u, CODEX_APP_SERVER_INTERRUPT_TIMEOUT_MS as ut, CODEX_NATIVE_PERSONALITY_NONE as v, readCodexSupportedReasoningEfforts as w, resolveCodexAppServerThreadModelSelection as x, resolveCodexAppServerModelProvider as y, resolveCodexContextEngineProjectionMaxChars as z };