@openclaw/codex 2026.7.2-beta.5 → 2026.8.1-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +2 -0
  2. package/dist/{app-server-policy-BmDgseo_.js → app-server-policy-zcBDc1JU.js} +1 -1
  3. package/dist/attempt-client-cleanup-DMLXUfa8.js +735 -0
  4. package/dist/binding-connection-I5Bo-AsX.js +33 -0
  5. package/dist/{bounded-turn-FEbUXoYT.js → bounded-turn-qgZmzy_4.js} +71 -63
  6. package/dist/{capabilities-BHRSI6Cn.js → capabilities-CybGI1oz.js} +8 -2
  7. package/dist/cli-metadata.js +1 -1
  8. package/dist/{command-formatters-ico4ZFnk.js → command-formatters-B1AEz8ZA.js} +18 -18
  9. package/dist/{command-handlers-CRmnwI2N.js → command-handlers-Db8pu_ds.js} +484 -95
  10. package/dist/{compact-dEN49UH6.js → compact-LspQaGfl.js} +213 -199
  11. package/dist/{conversation-binding-data-BHRpYbX3.js → conversation-binding-data-CW2GuupV.js} +6 -10
  12. package/dist/doctor-contract-api.js +40 -43
  13. package/dist/{dynamic-tools-LIt9ojep.js → dynamic-tools-BVdyWkUD.js} +2984 -1221
  14. package/dist/effective-mcp-catalog-DIIhAtOZ.js +119 -0
  15. package/dist/{event-projector-assistant-message-DTgnPPVF.js → event-projector-assistant-message-BwxV4d6N.js} +11 -23
  16. package/dist/{attempt-notifications-CMR2_fmR.js → event-projector-usage-e5huIQWu.js} +113 -29
  17. package/dist/harness.js +83 -13
  18. package/dist/image-payload-sanitizer-CnfoUPCE.js +65 -0
  19. package/dist/index.js +703 -329
  20. package/dist/isolated-completion-C58NbL40.js +75 -0
  21. package/dist/local-runtime-attribution-B_2mnqhb.js +19 -0
  22. package/dist/media-understanding-provider.js +5 -5
  23. package/dist/{models-BA01NEez.js → models-BFlpn1Ex.js} +12 -17
  24. package/dist/native-subagent-monitor-DyTGmTUx.js +3111 -0
  25. package/dist/{plugin-app-cache-key-DSXX-rXO.js → plugin-app-cache-key-C3H2CVp6.js} +135 -42
  26. package/dist/plugin-marketplace-discovery-CzrGJdCR.js +385 -0
  27. package/dist/{computer-use-BjGylO-E.js → plugin-metadata-cache-CNJPj4_b.js} +211 -113
  28. package/dist/protocol-validators-B4F7L8Rf.js +2346 -0
  29. package/dist/{rate-limits-S_PXs0ul.js → rate-limits-DZXmOL8-.js} +17 -35
  30. package/dist/{request-CXpPennK.js → request-BRl5_UfX.js} +4 -4
  31. package/dist/{run-attempt-C5GTPJOE.js → run-attempt-P8BdxC1I.js} +1996 -5728
  32. package/dist/{runtime-artifact-C2ITXdhM.js → runtime-artifact-BVSPdd6g.js} +91 -40
  33. package/dist/{sandbox-guard-D1Wky__S.js → sandbox-guard-Cmwc9eIk.js} +10 -13
  34. package/dist/{session-binding-DlVQ-Pxs.js → session-binding-DPxIvMgE.js} +305 -271
  35. package/dist/{session-catalog-CP2oCcEb.js → session-catalog-xwNi_SDM.js} +418 -144
  36. package/dist/{session-cli-CqZ0Fp2a.js → session-cli-7hgXZDV7.js} +1 -1
  37. package/dist/session-retirement-DoMMJFKm.js +36 -0
  38. package/dist/{session-upstream-marker-QE-m5QFG.js → session-upstream-marker-CrIFX06J.js} +1 -1
  39. package/dist/{settled-turn-finalizer-CA_2QXIE.js → settled-turn-finalizer-BRWBO4MO.js} +36 -15
  40. package/dist/{shared-client-BVKE40c-.js → shared-client-kIBbGjk4.js} +721 -240
  41. package/dist/{side-question-CO12sHxo.js → side-question-9HAL26l9.js} +102 -53
  42. package/dist/{thread-lifecycle-DRg_FdzC.js → thread-lifecycle-Bu1BTDXr.js} +1355 -660
  43. package/dist/thread-ownership-6kOQHlyC.js +92 -0
  44. package/dist/thread-resume-CPrJNQVM.js +30 -0
  45. package/dist/{transcript-mirror-uKA-A4tB.js → transcript-mirror-B9gGgnJS.js} +202 -182
  46. package/dist/{transport-stdio-DXgGZ7Ib.js → transport-stdio-DIImwDVS.js} +11 -0
  47. package/dist/upstream-prompt-provenance-ClRBije0.js +41 -0
  48. package/dist/{upstream-session-fork-_Vrz4WJr.js → upstream-session-fork-CE-_LdnP.js} +5 -5
  49. package/dist/{usage-UpVbdX5G.js → usage-CHlV0e-o.js} +3 -3
  50. package/dist/{web-search-provider.runtime-CaQMYDbp.js → web-search-provider.runtime-DRw2Vl42.js} +8 -9
  51. package/openclaw.plugin.json +18 -3
  52. package/package.json +6 -7
  53. package/dist/attempt-client-cleanup-Ia52yRWc.js +0 -127
  54. package/dist/conversation-control-DE7d9DOj.js +0 -287
  55. package/dist/event-projector-usage-tvgb1NCX.js +0 -69
  56. package/dist/notification-correlation-Bj_Ak_jI.js +0 -67
  57. package/dist/protocol-validators-Dm4eXZmt.js +0 -6337
  58. package/dist/realtime-voice-api-BPcA1uN9.js +0 -494
  59. package/dist/realtime-voice-api.js +0 -2
@@ -1,103 +1,34 @@
1
- import { B as flattenCodexDynamicToolFunctions, E as resolveCodexAppServerClientInstanceId, V as isJsonObject, _ as CodexAppServerRpcError, s as isCodexAppServerStartSelectionChangedError, v as getCodexAppServerClientInstanceId, w as isCodexAppServerPrewriteRequestCancellationError, x as isCodexAppServerConnectionClosedError } from "./shared-client-BVKE40c-.js";
2
- import { A as CODEX_PLUGINS_WORKSPACE_MARKETPLACE_NAME, E as resolveCodexPluginsPolicy, a as hashCodexAppServerBindingFingerprint, c as reclaimCurrentCodexSessionGeneration, f as codexSandboxPolicyForTurn, i as createCodexSessionGenerationSupersededError, k as CODEX_PLUGINS_MARKETPLACE_NAME, l as sessionBindingIdentity, n as assertCodexBindingMayBeReplaced, o as isCodexAppServerNativeAuthProfile, s as normalizeCodexAppServerBindingModelProvider } from "./session-binding-DlVQ-Pxs.js";
3
- import { a as closeCodexStartupClientBestEffort, c as isCodexAppServerUnsafeSubscriptionError, i as assertCodexThreadResumeSubscription, n as CODEX_APP_SERVER_UNSUBSCRIBE_TIMEOUT_MS, r as CodexAppServerUnsafeSubscriptionError, u as unsubscribeCodexThreadBestEffort } from "./attempt-client-cleanup-Ia52yRWc.js";
4
- import { i as assertCodexThreadStartResponse, n as assertCodexThreadForkResponse, r as assertCodexThreadResumeResponse } from "./protocol-validators-Dm4eXZmt.js";
5
- import { a as serializeCodexAppInventoryError, i as defaultCodexAppInventoryCache, t as buildCodexAppServerConnectionFingerprint } from "./plugin-app-cache-key-DSXX-rXO.js";
1
+ import { D as CODEX_PLUGINS_MARKETPLACE_NAME, E as resolveCodexPluginsPolicy, O as CODEX_PLUGINS_WORKSPACE_MARKETPLACE_NAME, a as hashCodexAppServerBindingFingerprint, c as reclaimCurrentCodexSessionGeneration, d as sessionBindingIdentity, i as createCodexSessionGenerationSupersededError, m as codexSandboxPolicyForTurn, n as assertCodexBindingMayBeReplaced, o as isCodexAppServerNativeAuthProfile, s as normalizeCodexAppServerBindingModelProvider } from "./session-binding-DPxIvMgE.js";
2
+ import { M as isCodexAppServerConnectionClosedError, R as resolveCodexAppServerClientInstanceId, V as consumeCodexAppServerLiveThread, ct as isJsonObject, d as retainSharedCodexAppServerClientByInstanceId, k as getCodexAppServerClientInstanceId, s as isCodexAppServerStartSelectionChangedError, st as flattenCodexDynamicToolFunctions, z as CodexAppServerRpcError } from "./shared-client-kIBbGjk4.js";
3
+ import { a as closeCodexStartupClientBestEffort, c as isCodexAppServerUnsafeSubscriptionError, d as unsubscribeCodexThreadBestEffort, n as CODEX_APP_SERVER_UNSUBSCRIBE_TIMEOUT_MS, r as CodexAppServerUnsafeSubscriptionError } from "./attempt-client-cleanup-DMLXUfa8.js";
4
+ import { a as assertCodexThreadStartResponse, r as assertCodexThreadForkResponse } from "./protocol-validators-B4F7L8Rf.js";
5
+ import { a as serializeCodexAppInventoryError, i as defaultCodexAppInventoryCache, t as buildCodexAppServerConnectionFingerprint } from "./plugin-app-cache-key-C3H2CVp6.js";
6
6
  import { t as isIncognitoSessionKey } from "./incognito-session-ET_ufDH-.js";
7
- import { a as projectBoundedCodexThreadHistory } from "./transcript-mirror-uKA-A4tB.js";
8
- import { SKILL_WORKSHOP_TOOL_NAME, buildSkillWorkshopPromptSection, embeddedAgentLog, formatErrorMessage, isActiveHarnessContextEngine, isHostScopedAgentToolActive } from "openclaw/plugin-sdk/agent-harness-runtime";
7
+ import { t as resumeCodexAppServerThread } from "./thread-resume-CPrJNQVM.js";
8
+ import { u as projectBoundedCodexThreadHistory } from "./transcript-mirror-B9gGgnJS.js";
9
+ import { r as sanitizeInlineImageDataUrl, t as invalidInlineImageText } from "./image-payload-sanitizer-CnfoUPCE.js";
10
+ import { asOptionalRecord, isRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
11
+ import fs from "node:fs/promises";
12
+ import path from "node:path";
13
+ import { SKILL_WORKSHOP_TOOL_NAME, TRANSCRIPT_CREDENTIAL_SAFETY_PROMPT, buildSkillWorkshopPromptSection, embeddedAgentLog, formatErrorMessage, isActiveHarnessContextEngine, isHostScopedAgentToolActive } from "openclaw/plugin-sdk/agent-harness-runtime";
9
14
  import * as crypto$1 from "node:crypto";
10
15
  import crypto from "node:crypto";
11
- import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
16
+ import os from "node:os";
17
+ import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
12
18
  import { sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
13
19
  import { redactSensitiveFieldValue, redactToolPayloadText } from "openclaw/plugin-sdk/logging-core";
20
+ import { resolveRequiredHomeDir, resolveStateDir } from "openclaw/plugin-sdk/state-paths";
21
+ import { listRegisteredPluginAgentPromptGuidance } from "openclaw/plugin-sdk/plugin-runtime";
14
22
  import { buildCodexUserMcpServersThreadConfigPatchForRuntime } from "openclaw/plugin-sdk/codex-mcp-projection";
15
23
  import { isDiagnosticFlagEnabled } from "openclaw/plugin-sdk/diagnostic-runtime";
16
- import { listRegisteredPluginAgentPromptGuidance } from "openclaw/plugin-sdk/plugin-runtime";
17
24
  import { GPT5_HEARTBEAT_PROMPT_OVERLAY } from "openclaw/plugin-sdk/provider-model-shared";
18
- import { INLINE_IMAGE_DATA_URL_PREFIX, sanitizeInlineImageDataUrl } from "openclaw/plugin-sdk/inline-image-data-url-runtime";
19
- //#region extensions/codex/src/app-server/event-projector-values.ts
20
- function readString(record, key) {
21
- const value = record[key];
22
- return typeof value === "string" ? value : void 0;
23
- }
24
- function normalizeNonEmptyString(value) {
25
- if (typeof value !== "string") return;
26
- return value.trim() || void 0;
27
- }
28
- function readNonEmptyString(record, key) {
29
- return normalizeNonEmptyString(record[key]);
30
- }
31
- function readNonEmptyStringArray(record, key) {
32
- const value = record[key];
33
- if (!Array.isArray(value)) return [];
34
- const entries = [];
35
- for (const entry of value) {
36
- const normalized = normalizeNonEmptyString(entry);
37
- if (normalized) entries.push(normalized);
38
- }
39
- return entries;
40
- }
41
- function readNullableString(record, key) {
42
- const value = record[key];
43
- if (value === null) return null;
44
- return typeof value === "string" ? value : void 0;
45
- }
46
- function readNumber(record, key) {
47
- const value = record[key];
48
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
49
- }
50
- function readNonNegativeInteger(record, key) {
51
- const value = readNumber(record, key);
52
- return value !== void 0 && Number.isInteger(value) && value >= 0 ? value : void 0;
53
- }
54
- function readCodexErrorNotificationMessage(record) {
55
- const error = record.error;
56
- return isJsonObject(error) ? readString(error, "message") : void 0;
57
- }
58
- function readHookOutputEntries(value) {
59
- if (!Array.isArray(value)) return [];
60
- return value.flatMap((entry) => {
61
- if (!isJsonObject(entry)) return [];
62
- const text = readString(entry, "text");
63
- if (!text) return [];
64
- const kind = readString(entry, "kind");
65
- return [{
66
- ...kind ? { kind } : {},
67
- text
68
- }];
69
- });
70
- }
71
- function splitPlanText(text) {
72
- return text.split(/\r?\n/).map((line) => line.trim().replace(/^[-*]\s+/, "")).filter((line) => line.length > 0);
73
- }
74
- function extractRawAssistantText(item) {
75
- return (Array.isArray(item.content) ? item.content : []).flatMap((entry) => {
76
- if (!isJsonObject(entry)) return [];
77
- const type = readString(entry, "type");
78
- if (type !== "output_text" && type !== "text") return [];
79
- const value = readString(entry, "text");
80
- return value ? [value] : [];
81
- }).join("").trim() || void 0;
82
- }
83
- function readItemString(item, key) {
84
- const value = item[key];
85
- return typeof value === "string" ? value : void 0;
86
- }
87
- function readItem(value) {
88
- if (!isJsonObject(value)) return;
89
- const type = typeof value.type === "string" ? value.type : void 0;
90
- const id = typeof value.id === "string" ? value.id : void 0;
91
- if (!type || !id) return;
92
- return value;
93
- }
94
- //#endregion
95
25
  //#region extensions/codex/src/app-server/plugin-inventory.ts
96
26
  /**
97
27
  * Reads Codex plugin marketplace state and app inventory to decide which
98
28
  * plugin-owned apps can be exposed to a native Codex thread.
99
29
  */
100
30
  const CODEX_PLUGINS_REMOTE_MARKETPLACE_NAME = `${CODEX_PLUGINS_MARKETPLACE_NAME}-remote`;
31
+ const CODEX_PLUGINS_API_MARKETPLACE_NAME = "openai-api-curated";
101
32
  /** Reads configured Codex plugin state and maps owned apps to readiness diagnostics. */
102
33
  async function readCodexPluginInventory(params) {
103
34
  const policy = params.policy ?? resolveCodexPluginsPolicy(params.pluginConfig);
@@ -110,21 +41,11 @@ async function readCodexPluginInventory(params) {
110
41
  }]
111
42
  };
112
43
  const appInventory = readCachedAppInventory(params);
113
- const curatedListed = await listCodexPluginMetadata(params, "curated-global", {});
114
- const shouldListWorkspacePlugins = policy.pluginPolicies.some((pluginPolicy) => pluginPolicy.enabled && pluginPolicy.marketplaceName === "workspace-directory");
115
- let workspaceListResult;
116
- if (shouldListWorkspacePlugins) try {
117
- workspaceListResult = {
118
- kind: "listed",
119
- response: await listCodexPluginMetadata(params, "workspace-directory", {
120
- cwds: [],
121
- marketplaceKinds: [CODEX_PLUGINS_WORKSPACE_MARKETPLACE_NAME]
122
- })
123
- };
124
- } catch (error) {
125
- if (!(error instanceof CodexAppServerRpcError)) throw error;
126
- workspaceListResult = { kind: "rejected" };
127
- }
44
+ const installedPlugins = await readInstalledCodexPluginMetadata({
45
+ ...params,
46
+ policy
47
+ });
48
+ const pluginCatalogs = /* @__PURE__ */ new Map();
128
49
  const diagnostics = [];
129
50
  const records = [];
130
51
  if (appInventory?.state === "missing") diagnostics.push({
@@ -136,10 +57,21 @@ async function readCodexPluginInventory(params) {
136
57
  message: "Cached Codex app inventory is stale; using stale app readiness and refreshing."
137
58
  });
138
59
  for (const pluginPolicy of policy.pluginPolicies) {
139
- if (!pluginPolicy.enabled) continue;
140
- const listed = pluginPolicy.marketplaceName === "workspace-directory" ? workspaceListResult?.kind === "listed" ? workspaceListResult.response : void 0 : curatedListed;
141
- const hasMarketplace = pluginPolicy.marketplaceName === "workspace-directory" ? listed?.marketplaces.some((entry) => entry.name === CODEX_PLUGINS_WORKSPACE_MARKETPLACE_NAME) === true : listed?.marketplaces.some(isOpenAiCuratedMarketplace) === true;
142
- if (!listed || !hasMarketplace) {
60
+ if (!pluginPolicy.enabled && !policy.allowAllPlugins) continue;
61
+ let listed = installedPlugins;
62
+ let resolvedPlugin = findConfiguredMarketplacePlugin(listed, pluginPolicy);
63
+ if (!resolvedPlugin && pluginPolicy.enabled && pluginPolicy.marketplaceName !== "workspace-directory") {
64
+ const requestParams = buildPluginCatalogRequestParams(params, pluginPolicy.marketplaceName);
65
+ const catalogKey = JSON.stringify([requestParams, pluginMetadataCatalogScope(pluginPolicy.marketplaceName)]);
66
+ let catalog = pluginCatalogs.get(catalogKey);
67
+ if (!catalog) {
68
+ catalog = listCodexPluginMetadata(params, pluginPolicy.marketplaceName);
69
+ pluginCatalogs.set(catalogKey, catalog);
70
+ }
71
+ listed = await catalog;
72
+ resolvedPlugin = findConfiguredMarketplacePlugin(listed, pluginPolicy);
73
+ }
74
+ if (!listed.marketplaces.some((marketplace) => marketplaceMatchesConfiguredName(marketplace, pluginPolicy.marketplaceName))) {
143
75
  diagnostics.push({
144
76
  code: "marketplace_missing",
145
77
  plugin: pluginPolicy,
@@ -147,7 +79,6 @@ async function readCodexPluginInventory(params) {
147
79
  });
148
80
  continue;
149
81
  }
150
- const resolvedPlugin = pluginPolicy.marketplaceName === "workspace-directory" ? findWorkspaceMarketplacePlugin(listed, pluginPolicy.pluginName) : findOpenAiCuratedMarketplacePlugin(listed, pluginPolicy.pluginName);
151
82
  if (!resolvedPlugin) {
152
83
  diagnostics.push({
153
84
  code: "plugin_missing",
@@ -157,6 +88,15 @@ async function readCodexPluginInventory(params) {
157
88
  continue;
158
89
  }
159
90
  const { summary } = resolvedPlugin;
91
+ const unavailableByMarketplacePolicy = summary.availability === "DISABLED_BY_ADMIN" || summary.installPolicy === "NOT_AVAILABLE";
92
+ if (unavailableByMarketplacePolicy) {
93
+ diagnostics.push({
94
+ code: "plugin_disabled",
95
+ plugin: pluginPolicy,
96
+ message: `${pluginPolicy.pluginName} is unavailable in ${pluginPolicy.marketplaceName}.`
97
+ });
98
+ if (!summary.installed) continue;
99
+ }
160
100
  const detail = await readPluginDetail(params, marketplaceRef(resolvedPlugin.marketplace, pluginPolicy.marketplaceName), pluginPolicy, summary, diagnostics);
161
101
  const ownedAppIds = detail?.apps.map((app) => app.id).filter(Boolean).toSorted() ?? [];
162
102
  const appOwnership = resolveAppOwnership({
@@ -183,7 +123,7 @@ async function readCodexPluginInventory(params) {
183
123
  policy: pluginPolicy,
184
124
  summary,
185
125
  ...detail ? { detail } : {},
186
- activationRequired: !summary.installed || !summary.enabled,
126
+ activationRequired: pluginPolicy.enabled && (unavailableByMarketplacePolicy || !summary.installed || !summary.enabled),
187
127
  authRequired: apps.some((app) => app.needsAuth || !app.accessible),
188
128
  appOwnership,
189
129
  ownedAppIds,
@@ -197,11 +137,14 @@ async function readCodexPluginInventory(params) {
197
137
  ...appInventory ? { appInventory } : {}
198
138
  };
199
139
  }
200
- /** Finds one plugin summary in the OpenAI curated marketplace response. */
201
- function findOpenAiCuratedPluginSummary(listed, pluginName) {
202
- const resolved = findOpenAiCuratedMarketplacePlugin(listed, pluginName);
140
+ /** Finds a configured plugin only in its authorized marketplace identity. */
141
+ function findCodexMarketplacePluginSummary(listed, marketplaceName, pluginName) {
142
+ const resolved = findConfiguredMarketplacePlugin(listed, {
143
+ marketplaceName,
144
+ pluginName
145
+ });
203
146
  return resolved ? {
204
- marketplace: marketplaceRef(resolved.marketplace, CODEX_PLUGINS_MARKETPLACE_NAME),
147
+ marketplace: marketplaceRef(resolved.marketplace, marketplaceName),
205
148
  summary: resolved.summary
206
149
  } : void 0;
207
150
  }
@@ -218,25 +161,52 @@ function resolveRecoverableCodexPluginConfigKeys(params) {
218
161
  return params.policy.pluginPolicies.filter((pluginPolicy) => pluginPolicy.enabled && !isSettledMissingPluginPolicy({
219
162
  pluginPolicy,
220
163
  metadataCache: params.metadataCache,
221
- appCacheKey: params.appCacheKey
164
+ appCacheKey: params.appCacheKey,
165
+ configCwd: params.configCwd
222
166
  })).map((pluginPolicy) => pluginPolicy.configKey).toSorted();
223
167
  }
224
- async function listCodexPluginMetadata(params, queryKind, requestParams) {
225
- if (!params.metadataCache || !params.appCacheKey || queryKind === "workspace-directory") return await params.request("plugin/list", requestParams);
168
+ async function listCodexPluginMetadata(params, marketplaceName) {
169
+ const requestParams = buildPluginCatalogRequestParams(params, marketplaceName);
170
+ if (!params.metadataCache || !params.appCacheKey) return await params.request("plugin/list", requestParams);
226
171
  return (await params.metadataCache.load({
227
172
  appCacheKey: params.appCacheKey,
228
- queryKind,
173
+ queryKind: "curated-global",
229
174
  requestParams,
175
+ catalogScope: pluginMetadataCatalogScope(marketplaceName),
230
176
  request: async (method, listedParams) => await params.request(method, listedParams),
231
- cacheable: (response) => (response.marketplaces ?? []).some((marketplace) => isOpenAiCuratedMarketplace(marketplace))
177
+ cacheable: (response) => response.marketplaces.some((marketplace) => marketplaceMatchesConfiguredName(marketplace, marketplaceName))
178
+ })).response;
179
+ }
180
+ async function readInstalledCodexPluginMetadata(params) {
181
+ const requestParams = params.configCwd ? { cwds: [params.configCwd] } : {};
182
+ if (!params.metadataCache || !params.appCacheKey) return await params.request("plugin/installed", requestParams);
183
+ return (await params.metadataCache.load({
184
+ appCacheKey: params.appCacheKey,
185
+ queryKind: "installed",
186
+ requestParams,
187
+ request: async (method, installedParams) => await params.request(method, installedParams),
188
+ cacheable: (response) => params.policy.pluginPolicies.every((pluginPolicy) => {
189
+ if (!pluginPolicy.enabled && !params.policy.allowAllPlugins) return true;
190
+ return Boolean(findConfiguredMarketplacePlugin(response, pluginPolicy));
191
+ })
232
192
  })).response;
233
193
  }
234
194
  function isSettledMissingPluginPolicy(params) {
235
- const queryKind = params.pluginPolicy.marketplaceName === "workspace-directory" ? "workspace-directory" : "curated-global";
236
- const listed = params.metadataCache.read(params.appCacheKey, queryKind)?.response;
195
+ const queryKind = params.pluginPolicy.marketplaceName === "workspace-directory" ? "installed" : "curated-global";
196
+ const requestParams = queryKind === "installed" ? params.configCwd ? { cwds: [params.configCwd] } : {} : buildPluginCatalogRequestParams(params, params.pluginPolicy.marketplaceName);
197
+ const listed = params.metadataCache.read(params.appCacheKey, queryKind, requestParams, queryKind === "curated-global" ? pluginMetadataCatalogScope(params.pluginPolicy.marketplaceName) : void 0)?.response;
237
198
  if (!listed) return false;
238
- if (queryKind === "workspace-directory") return !findWorkspaceMarketplacePlugin(listed, params.pluginPolicy.pluginName);
239
- return !findOpenAiCuratedMarketplacePlugin(listed, params.pluginPolicy.pluginName);
199
+ return !findConfiguredMarketplacePlugin(listed, params.pluginPolicy);
200
+ }
201
+ function pluginMetadataCatalogScope(marketplaceName) {
202
+ return isOpenAiCuratedMarketplaceName(marketplaceName) ? void 0 : marketplaceName;
203
+ }
204
+ function buildPluginCatalogRequestParams(params, marketplaceName) {
205
+ const marketplaceKinds = marketplaceName === "created-by-me-remote" ? ["created-by-me-remote"] : marketplaceName.startsWith("workspace-shared-with-me") ? ["shared-with-me"] : void 0;
206
+ return {
207
+ ...params.configCwd ? { cwds: [params.configCwd] } : {},
208
+ ...marketplaceKinds ? { marketplaceKinds: [...marketplaceKinds] } : {}
209
+ };
240
210
  }
241
211
  function readCachedAppInventory(params) {
242
212
  if (!params.appCache || !params.appCacheKey) return;
@@ -250,7 +220,7 @@ function readCachedAppInventory(params) {
250
220
  }
251
221
  async function readPluginDetail(params, marketplace, pluginPolicy, summary, diagnostics) {
252
222
  if (params.readPluginDetails === false) return;
253
- if (marketplace.name === "workspace-directory" && marketplace.remoteMarketplaceName && !summary.remotePluginId) {
223
+ if (marketplace.remoteMarketplaceName && !summary.remotePluginId) {
254
224
  diagnostics.push({
255
225
  code: "plugin_detail_unavailable",
256
226
  plugin: pluginPolicy,
@@ -299,23 +269,30 @@ function resolveOwnedApps(params) {
299
269
  name: app.name,
300
270
  accessible: info.isAccessible,
301
271
  enabled: info.isEnabled,
302
- needsAuth: app.needsAuth || !info.isAccessible
272
+ needsAuth: !info.isAccessible
303
273
  };
304
274
  }).toSorted((left, right) => left.id.localeCompare(right.id));
305
275
  }
306
276
  function findPluginSummary(marketplace, pluginName) {
307
- return marketplace.plugins.find((plugin) => plugin.name === pluginName || plugin.id === pluginName || plugin.id === `${pluginName}@${marketplace.name}` || pluginNameFromPluginId(plugin.id, marketplace.name) === pluginName);
277
+ const exact = marketplace.plugins.find((plugin) => plugin.id === pluginName || plugin.id === `${pluginName}@${marketplace.name}`);
278
+ if (exact) return exact;
279
+ const matches = marketplace.plugins.filter((plugin) => plugin.name === pluginName || pluginNameFromPluginId(plugin.id, marketplace.name) === pluginName);
280
+ return matches.length === 1 ? matches[0] : void 0;
308
281
  }
309
- function findOpenAiCuratedMarketplacePlugin(listed, pluginName) {
282
+ function findConfiguredMarketplacePlugin(listed, plugin) {
283
+ if (plugin.marketplaceName === "workspace-directory") return findWorkspaceMarketplacePlugin(listed, plugin.pluginName);
310
284
  for (const marketplace of listed.marketplaces) {
311
- if (!isOpenAiCuratedMarketplace(marketplace)) continue;
312
- const summary = findPluginSummary(marketplace, pluginName);
285
+ if (!marketplaceMatchesConfiguredName(marketplace, plugin.marketplaceName)) continue;
286
+ const summary = findPluginSummary(marketplace, plugin.pluginName);
313
287
  if (summary) return {
314
288
  marketplace,
315
289
  summary
316
290
  };
317
291
  }
318
292
  }
293
+ function marketplaceMatchesConfiguredName(marketplace, configuredMarketplaceName) {
294
+ return isOpenAiCuratedMarketplaceName(configuredMarketplaceName) ? isOpenAiCuratedMarketplace(marketplace) : marketplace.name === configuredMarketplaceName;
295
+ }
319
296
  function findWorkspaceMarketplacePlugin(listed, pluginName) {
320
297
  const marketplace = listed.marketplaces.find((entry) => entry.name === CODEX_PLUGINS_WORKSPACE_MARKETPLACE_NAME);
321
298
  const summary = marketplace?.plugins.find((plugin) => plugin.id === pluginName);
@@ -337,21 +314,35 @@ function marketplaceRef(marketplace, name) {
337
314
  ...!marketplace.path ? { remoteMarketplaceName: marketplace.name } : {}
338
315
  };
339
316
  }
340
- /** True for either supported OpenAI curated marketplace wire name. */
317
+ /** True for any supported OpenAI curated marketplace wire name, matching Codex's own curated predicate. */
341
318
  function isOpenAiCuratedMarketplace(marketplace) {
342
- return marketplace.name === "openai-curated" || marketplace.name === CODEX_PLUGINS_REMOTE_MARKETPLACE_NAME;
319
+ return isOpenAiCuratedMarketplaceName(marketplace.name);
320
+ }
321
+ /** True for all Codex wire aliases of the same OpenAI-curated catalog. */
322
+ function isOpenAiCuratedMarketplaceName(marketplaceName) {
323
+ return marketplaceName === "openai-curated" || marketplaceName === CODEX_PLUGINS_REMOTE_MARKETPLACE_NAME || marketplaceName === CODEX_PLUGINS_API_MARKETPLACE_NAME;
343
324
  }
344
325
  //#endregion
345
326
  //#region extensions/codex/src/app-server/plugin-activation.ts
346
- /** Activates a curated plugin or rejects a workspace plugin that is not already active. */
327
+ /**
328
+ * Activates legacy curated Codex plugins while requiring owner-managed
329
+ * installation for every other marketplace.
330
+ */
331
+ /** Activates legacy curated plugins without granting install authority to other marketplaces. */
347
332
  async function ensureCodexPluginActivation(params) {
348
333
  if (params.identity.marketplaceName === "workspace-directory") return activationFailure(params.identity, "disabled", { message: "workspace-directory plugins must be installed and enabled outside OpenClaw before use." });
334
+ if (!isOpenAiCuratedMarketplaceName(params.identity.marketplaceName)) {
335
+ const target = params.identity.pluginName.endsWith(`@${params.identity.marketplaceName}`) ? params.identity.pluginName : `${params.identity.pluginName}@${params.identity.marketplaceName}`;
336
+ return activationFailure(params.identity, "disabled", { message: `${params.identity.marketplaceName} plugins must be installed and enabled by an owner before use. Run /codex plugins install ${target}.` });
337
+ }
349
338
  const listed = await listCuratedCodexPluginMetadata(params);
350
- const resolved = findOpenAiCuratedPluginSummary(listed, params.identity.pluginName);
339
+ const resolved = findCodexMarketplacePluginSummary(listed, params.identity.marketplaceName, params.identity.pluginName);
351
340
  if (!resolved) {
352
- if (!listed.marketplaces.some(isOpenAiCuratedMarketplace)) return activationFailure(params.identity, "marketplace_missing", { message: `Codex marketplace ${CODEX_PLUGINS_MARKETPLACE_NAME} was not found.` });
341
+ if (!listed.marketplaces.some((marketplace) => isOpenAiCuratedMarketplace(marketplace))) return activationFailure(params.identity, "marketplace_missing", { message: `Codex marketplace ${CODEX_PLUGINS_MARKETPLACE_NAME} was not found.` });
353
342
  return activationFailure(params.identity, "plugin_missing", { message: `${params.identity.pluginName} was not found in ${CODEX_PLUGINS_MARKETPLACE_NAME}.` });
354
343
  }
344
+ 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.` });
345
+ if (resolved.summary.availability === "DISABLED_BY_ADMIN" || resolved.summary.installPolicy === "NOT_AVAILABLE") return activationFailure(params.identity, "disabled", { message: `${params.identity.pluginName} was disabled or made unavailable by its marketplace administrator.` });
355
346
  if (resolved.summary.installed && resolved.summary.enabled && !params.installEvenIfActive) return {
356
347
  identity: params.identity,
357
348
  ok: true,
@@ -360,7 +351,21 @@ async function ensureCodexPluginActivation(params) {
360
351
  marketplace: resolved.marketplace,
361
352
  diagnostics: []
362
353
  };
363
- const installResponse = await params.request("plugin/install", pluginReadParams(resolved.marketplace, resolved.marketplace.remoteMarketplaceName && resolved.summary.remotePluginId ? resolved.summary.remotePluginId : params.identity.pluginName));
354
+ const remotePluginId = resolved.marketplace.remoteMarketplaceName ? resolved.summary.remotePluginId : void 0;
355
+ let installResponse;
356
+ try {
357
+ installResponse = await params.request("plugin/install", pluginReadParams(resolved.marketplace, remotePluginId ?? params.identity.pluginName));
358
+ } catch (error) {
359
+ if (!(error instanceof CodexAppServerRpcError) || error.code !== -32600 || !remotePluginId || error.message !== `remote plugin ${remotePluginId} is disabled by admin` && error.message !== `remote plugin ${remotePluginId} is not available for install`) throw error;
360
+ return {
361
+ identity: params.identity,
362
+ ok: false,
363
+ reason: "install_failed",
364
+ installAttempted: true,
365
+ marketplace: resolved.marketplace,
366
+ diagnostics: [{ message: `Codex plugin install failed: ${coerceErrorMessage(error)}` }]
367
+ };
368
+ }
364
369
  if (params.metadataCache && params.appCacheKey) params.metadataCache.invalidate(params.appCacheKey);
365
370
  const refreshDiagnostics = [];
366
371
  let refreshFailed = false;
@@ -369,13 +374,15 @@ async function ensureCodexPluginActivation(params) {
369
374
  request: params.request,
370
375
  appCache: params.appCache,
371
376
  appCacheKey: params.appCacheKey,
377
+ configCwd: params.configCwd,
372
378
  metadataCache: params.metadataCache,
379
+ deferAppInventoryRefresh: params.deferAppInventoryRefresh,
373
380
  targetAppIds: params.targetAppIds
374
381
  });
375
382
  refreshDiagnostics.push(...refreshResult.diagnostics);
376
383
  } catch (error) {
377
384
  refreshFailed = true;
378
- refreshDiagnostics.push({ message: `Codex plugin runtime refresh failed after install: ${error instanceof Error ? error.message : String(error)}` });
385
+ refreshDiagnostics.push({ message: `Codex plugin runtime refresh failed after install: ${coerceErrorMessage(error)}` });
379
386
  }
380
387
  const authRequired = installResponse.appsNeedingAuth.length > 0;
381
388
  return {
@@ -391,19 +398,20 @@ async function ensureCodexPluginActivation(params) {
391
398
  /** Forces Codex plugin, skill, hook, MCP, and app inventory refreshes after activation. */
392
399
  async function refreshCodexPluginRuntimeState(params) {
393
400
  const diagnostics = [];
394
- await listCuratedCodexPluginMetadata(params);
401
+ await listCuratedCodexPluginMetadata(params, { forceRefetch: true });
395
402
  await params.request("skills/list", {
396
- cwds: [],
403
+ cwds: params.configCwd ? [params.configCwd] : [],
397
404
  forceReload: true
398
405
  });
399
406
  try {
400
- await params.request("hooks/list", { cwds: [] });
407
+ await params.request("hooks/list", { cwds: params.configCwd ? [params.configCwd] : [] });
401
408
  } catch (error) {
402
- diagnostics.push({ message: `Codex hooks refresh skipped: ${error instanceof Error ? error.message : String(error)}` });
409
+ diagnostics.push({ message: `Codex hooks refresh skipped: ${coerceErrorMessage(error)}` });
403
410
  }
404
411
  await params.request("config/mcpServer/reload", void 0);
405
412
  if (params.appCache && params.appCacheKey) {
406
- params.appCache.invalidate(params.appCacheKey, "Codex plugin activation changed app inventory");
413
+ params.appCache.invalidate(params.appCacheKey, "Codex plugin activation changed app inventory", void 0, params.targetAppIds);
414
+ if (params.deferAppInventoryRefresh) return { diagnostics };
407
415
  const request = async (method, requestParams) => await params.request(method, requestParams);
408
416
  try {
409
417
  await params.appCache.refreshNow({
@@ -413,20 +421,23 @@ async function refreshCodexPluginRuntimeState(params) {
413
421
  targetAppIds: params.targetAppIds
414
422
  });
415
423
  } catch (error) {
416
- diagnostics.push({ message: `Codex app inventory refresh skipped: ${error instanceof Error ? error.message : String(error)}` });
424
+ diagnostics.push({ message: `Codex app inventory refresh skipped: ${coerceErrorMessage(error)}` });
417
425
  }
418
426
  }
419
427
  return { diagnostics };
420
428
  }
421
- async function listCuratedCodexPluginMetadata(params) {
422
- const requestParams = {};
429
+ async function listCuratedCodexPluginMetadata(params, options = {}) {
430
+ const requestParams = {
431
+ ...params.configCwd ? { cwds: [params.configCwd] } : {},
432
+ ...options.forceRefetch ? { forceRefetch: true } : {}
433
+ };
423
434
  if (!params.metadataCache || !params.appCacheKey) return await params.request("plugin/list", requestParams);
424
435
  return (await params.metadataCache.load({
425
436
  appCacheKey: params.appCacheKey,
426
437
  queryKind: "curated-global",
427
438
  requestParams,
428
439
  request: async (method, listedParams) => await params.request(method, listedParams),
429
- cacheable: (response) => (response.marketplaces ?? []).some((marketplace) => isOpenAiCuratedMarketplace(marketplace))
440
+ cacheable: (response) => response.marketplaces.some((marketplace) => isOpenAiCuratedMarketplace(marketplace))
430
441
  })).response;
431
442
  }
432
443
  function activationFailure(identity, reason, diagnostic, extraDiagnostics = []) {
@@ -439,6 +450,129 @@ function activationFailure(identity, reason, diagnostic, extraDiagnostics = [])
439
450
  };
440
451
  }
441
452
  //#endregion
453
+ //#region extensions/codex/src/app-server/plugin-thread-app-admission.ts
454
+ async function refreshCodexPluginAppInventory(params, appCache, options = {}) {
455
+ if (!params.appCacheKey) return;
456
+ const request = async (method, requestParams) => await params.request(method, requestParams);
457
+ try {
458
+ return await appCache.refreshNow({
459
+ key: params.appCacheKey,
460
+ request,
461
+ nowMs: params.nowMs,
462
+ forceRefetch: options.forceRefetch,
463
+ targetAppIds: options.targetAppIds
464
+ });
465
+ } catch (error) {
466
+ embeddedAgentLog.warn("codex plugin thread config app inventory refresh failed", {
467
+ reason: options.reason,
468
+ forceRefetch: options.forceRefetch === true,
469
+ error: serializeCodexAppInventoryError(error)
470
+ });
471
+ return;
472
+ }
473
+ }
474
+ function collectCodexPluginOwnedAppIds(inventory) {
475
+ return Array.from(new Set(inventory.records.flatMap((record) => record.ownedAppIds).filter(Boolean))).toSorted();
476
+ }
477
+ function collectCodexReservedPluginAppIds(params) {
478
+ const reserved = new Set(params.inventory.records.flatMap((record) => record.appOwnership === "proven" ? record.ownedAppIds : []));
479
+ const recordsByConfigKey = new Map(params.inventory.records.map((record) => [record.policy.configKey, record]));
480
+ const configuredOwnerNames = new Set(params.policy.pluginPolicies.flatMap((policy) => {
481
+ const record = recordsByConfigKey.get(policy.configKey);
482
+ return [
483
+ policy.configKey,
484
+ policy.pluginName,
485
+ record?.summary.name,
486
+ record?.summary.id
487
+ ].filter((name) => Boolean(name)).map(normalizeCodexPluginOwnerName);
488
+ }));
489
+ for (const app of params.accountApps) if (app.pluginDisplayNames.some((name) => configuredOwnerNames.has(normalizeCodexPluginOwnerName(name)))) reserved.add(app.id);
490
+ return reserved;
491
+ }
492
+ function normalizeCodexPluginOwnerName(name) {
493
+ return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "");
494
+ }
495
+ async function readCodexThreadAdmissibleAccountApps(params, appCache) {
496
+ const request = async (method, requestParams) => await params.request(method, requestParams);
497
+ const cachedInventory = appCache.read({
498
+ key: params.appCacheKey,
499
+ request,
500
+ nowMs: params.nowMs,
501
+ suppressRefresh: true
502
+ });
503
+ const snapshot = cachedInventory.state === "fresh" && !cachedInventory.snapshot?.targetAppIds?.length ? cachedInventory.snapshot : await refreshCodexPluginAppInventory(params, appCache, {
504
+ forceRefetch: false,
505
+ reason: "account_apps_all",
506
+ targetAppIds: []
507
+ });
508
+ if (!snapshot) return {
509
+ apps: [],
510
+ diagnostic: {
511
+ code: "account_app_inventory_unavailable",
512
+ message: "Codex account app inventory was unavailable; account apps were not exposed."
513
+ }
514
+ };
515
+ const installedAppsById = new Map(snapshot.installedApps.map((app) => [app.id, app]));
516
+ return { apps: snapshot.apps.filter((app) => resolveCodexInstalledAppThreadAdmission(toCodexPluginOwnedAccountApp(app), installedAppsById.get(app.id)) !== "blocked").toSorted((left, right) => left.id.localeCompare(right.id)) };
517
+ }
518
+ function toCodexPluginOwnedAccountApp(app) {
519
+ return {
520
+ id: app.id,
521
+ name: app.name,
522
+ accessible: app.isAccessible,
523
+ enabled: app.isEnabled,
524
+ needsAuth: !app.isAccessible
525
+ };
526
+ }
527
+ function resolveCodexThreadConfigAppsForRecord(params) {
528
+ return params.inventory.appInventory?.state === "missing" ? [] : params.record.apps;
529
+ }
530
+ function resolveCodexPluginAppThreadAdmission(app, inventory) {
531
+ const snapshot = inventory.appInventory?.snapshot;
532
+ if (!snapshot) return "blocked";
533
+ return resolveCodexInstalledAppThreadAdmission(app, snapshot.installedApps.find((candidate) => candidate.id === app.id));
534
+ }
535
+ function resolveCodexInstalledAppThreadAdmission(app, installed) {
536
+ if (!app.accessible || app.needsAuth || !installed) return "blocked";
537
+ if (installed.enabled && installed.callable) return "ready";
538
+ return !installed.enabled && !installed.callable ? "provisional" : "blocked";
539
+ }
540
+ async function readCodexConfigForAppAdmission(params) {
541
+ try {
542
+ const response = await params.request("config/read", {
543
+ includeLayers: true,
544
+ ...params.configCwd ? { cwd: params.configCwd } : {}
545
+ });
546
+ if (!isJsonObject(response) || !isJsonObject(response.config) || !Array.isArray(response.layers)) throw new Error("Codex config/read omitted effective config or config layers");
547
+ return {
548
+ config: response.config,
549
+ layers: response.layers.flatMap((layer) => {
550
+ if (!isJsonObject(layer)) throw new Error("Codex config/read returned an invalid config layer");
551
+ if (layer.disabledReason !== void 0 && layer.disabledReason !== null) {
552
+ if (typeof layer.disabledReason !== "string") throw new Error("Codex config/read returned an invalid disabled layer");
553
+ return [];
554
+ }
555
+ if (!isJsonObject(layer.config)) throw new Error("Codex config/read returned an invalid layer config");
556
+ return [layer.config];
557
+ })
558
+ };
559
+ } catch (error) {
560
+ embeddedAgentLog.warn("codex plugin app admission config read failed", { error: serializeCodexAppInventoryError(error) });
561
+ return;
562
+ }
563
+ }
564
+ function resolveCodexExplicitAppEnablement(layersHighestPrecedenceFirst, appId) {
565
+ for (const layer of layersHighestPrecedenceFirst) {
566
+ const apps = layer.apps;
567
+ const app = isJsonObject(apps) ? apps[appId] : void 0;
568
+ if (isJsonObject(app) && Object.hasOwn(app, "enabled")) return app.enabled === true;
569
+ }
570
+ }
571
+ function shouldForceRefreshCodexNotReadyPluginApps(params, policy, inventory) {
572
+ if (!params.appCacheKey || !policy.pluginPolicies.some((plugin) => plugin.enabled) || inventory.appInventory?.state === "missing") return false;
573
+ return inventory.records.some((record) => record.appOwnership === "proven" && record.ownedAppIds.length > 0 && (record.apps.length === 0 || record.apps.some((app) => !app.accessible)));
574
+ }
575
+ //#endregion
442
576
  //#region extensions/codex/src/app-server/plugin-thread-config.ts
443
577
  /**
444
578
  * Builds Codex thread config patches that expose only policy-approved apps
@@ -492,16 +626,17 @@ async function buildCodexPluginThreadConfig(params) {
492
626
  request: params.request,
493
627
  appCache,
494
628
  appCacheKey: params.appCacheKey,
629
+ configCwd: params.configCwd,
495
630
  metadataCache: params.metadataCache,
496
631
  nowMs: params.nowMs,
497
632
  suppressAppInventoryRefresh: true
498
633
  }) : emptyCodexPluginInventory(policy);
499
634
  const appInventoryRefreshDeferredForActivation = inventory.records.some((record) => record.activationRequired) && shouldRefreshMissingAppInventory(params, policy, inventory);
500
635
  if (shouldWaitForInitialAppInventory(params, policy, inventory)) {
501
- await refreshAppInventoryNow(params, appCache, {
636
+ await refreshCodexPluginAppInventory(params, appCache, {
502
637
  forceRefetch: false,
503
638
  reason: "initial_missing",
504
- targetAppIds: collectInventoryOwnedAppIds(inventory)
639
+ targetAppIds: collectCodexPluginOwnedAppIds(inventory)
505
640
  });
506
641
  inventory = await readCodexPluginInventory({
507
642
  pluginConfig: params.pluginConfig,
@@ -509,6 +644,7 @@ async function buildCodexPluginThreadConfig(params) {
509
644
  request: params.request,
510
645
  appCache,
511
646
  appCacheKey: params.appCacheKey,
647
+ configCwd: params.configCwd,
512
648
  metadataCache: params.metadataCache,
513
649
  nowMs: params.nowMs
514
650
  });
@@ -526,7 +662,9 @@ async function buildCodexPluginThreadConfig(params) {
526
662
  request: params.request,
527
663
  appCache,
528
664
  appCacheKey: params.appCacheKey,
665
+ configCwd: params.configCwd,
529
666
  metadataCache: params.metadataCache,
667
+ deferAppInventoryRefresh: true,
530
668
  targetAppIds: record.ownedAppIds
531
669
  });
532
670
  activationResults.push(activation);
@@ -539,10 +677,10 @@ async function buildCodexPluginThreadConfig(params) {
539
677
  const postInstallRefreshRequired = activationResults.some((activation) => activation.ok && activation.installAttempted);
540
678
  const deferredMissingRefreshRequired = appInventoryRefreshDeferredForActivation && !postInstallRefreshRequired && shouldRefreshMissingAppInventory(params, policy, inventory);
541
679
  if (postInstallRefreshRequired || deferredMissingRefreshRequired) {
542
- await refreshAppInventoryNow(params, appCache, {
680
+ await refreshCodexPluginAppInventory(params, appCache, {
543
681
  forceRefetch: true,
544
682
  reason: postInstallRefreshRequired ? "post_install" : "deferred_missing",
545
- targetAppIds: collectInventoryOwnedAppIds(inventory)
683
+ targetAppIds: collectCodexPluginOwnedAppIds(inventory)
546
684
  });
547
685
  inventory = await readCodexPluginInventory({
548
686
  pluginConfig: params.pluginConfig,
@@ -550,6 +688,7 @@ async function buildCodexPluginThreadConfig(params) {
550
688
  request: params.request,
551
689
  appCache,
552
690
  appCacheKey: params.appCacheKey,
691
+ configCwd: params.configCwd,
553
692
  metadataCache: params.metadataCache,
554
693
  nowMs: params.nowMs
555
694
  });
@@ -558,11 +697,11 @@ async function buildCodexPluginThreadConfig(params) {
558
697
  appCacheKey: params.appCacheKey
559
698
  });
560
699
  }
561
- if (shouldForceRefreshForNotReadyPluginApps(params, policy, inventory)) {
562
- await refreshAppInventoryNow(params, appCache, {
700
+ if (shouldForceRefreshCodexNotReadyPluginApps(params, policy, inventory)) {
701
+ await refreshCodexPluginAppInventory(params, appCache, {
563
702
  forceRefetch: true,
564
703
  reason: "not_ready_plugin_apps",
565
- targetAppIds: collectInventoryOwnedAppIds(inventory)
704
+ targetAppIds: collectCodexPluginOwnedAppIds(inventory)
566
705
  });
567
706
  inventory = await readCodexPluginInventory({
568
707
  pluginConfig: params.pluginConfig,
@@ -570,6 +709,7 @@ async function buildCodexPluginThreadConfig(params) {
570
709
  request: params.request,
571
710
  appCache,
572
711
  appCacheKey: params.appCacheKey,
712
+ configCwd: params.configCwd,
573
713
  metadataCache: params.metadataCache,
574
714
  nowMs: params.nowMs
575
715
  });
@@ -578,12 +718,13 @@ async function buildCodexPluginThreadConfig(params) {
578
718
  appCacheKey: params.appCacheKey
579
719
  });
580
720
  }
581
- const accountAppsResult = policy.allowAllPlugins ? await readAccessibleAccountApps(params, appCache) : { apps: [] };
721
+ const accountAppsResult = policy.allowAllPlugins ? await readCodexThreadAdmissibleAccountApps(params, appCache) : { apps: [] };
582
722
  const diagnostics = [
583
723
  ...inventory.diagnostics,
584
724
  ...activationDiagnostics,
585
725
  ...accountAppsResult.diagnostic ? [accountAppsResult.diagnostic] : []
586
726
  ];
727
+ const provisionalAppIds = /* @__PURE__ */ new Set();
587
728
  const apps = { _default: {
588
729
  enabled: false,
589
730
  destructive_enabled: false,
@@ -591,17 +732,35 @@ async function buildCodexPluginThreadConfig(params) {
591
732
  } };
592
733
  const policyApps = {};
593
734
  const pluginAppIds = {};
594
- const pluginOwnedAppIds = new Set(inventory.records.flatMap((record) => record.appOwnership === "proven" ? record.ownedAppIds : []));
735
+ let configForAppAdmission;
736
+ const pluginOwnedAppIds = collectCodexReservedPluginAppIds({
737
+ policy,
738
+ inventory,
739
+ accountApps: accountAppsResult.apps
740
+ });
741
+ const unresolvedDisabledPluginOwnership = policy.allowAllPlugins ? policy.pluginPolicies.find((pluginPolicy) => {
742
+ const record = inventory.records.find((candidate) => candidate.policy.configKey === pluginPolicy.configKey);
743
+ const disabledByMarketplacePolicy = record?.summary.availability === "DISABLED_BY_ADMIN" || record?.summary.installPolicy === "NOT_AVAILABLE";
744
+ const unresolvedPluginIdentity = !record && inventory.diagnostics.some((diagnostic) => diagnostic.plugin?.configKey === pluginPolicy.configKey && (diagnostic.code === "plugin_disabled" || diagnostic.code === "plugin_missing" || diagnostic.code === "marketplace_missing"));
745
+ return (!pluginPolicy.enabled || disabledByMarketplacePolicy || unresolvedPluginIdentity) && !record?.detail;
746
+ }) : void 0;
747
+ if (unresolvedDisabledPluginOwnership) diagnostics.push({
748
+ code: "account_app_ownership_unavailable",
749
+ plugin: unresolvedDisabledPluginOwnership,
750
+ message: `Could not verify disabled Codex plugin app ownership for ${unresolvedDisabledPluginOwnership.pluginName}; account apps were not exposed.`
751
+ });
595
752
  for (const record of inventory.records) {
753
+ if (!record.policy.enabled) continue;
596
754
  const activation = activationResults.find((item) => item.identity.configKey === record.policy.configKey);
597
755
  if (activation?.ok === false || record.activationRequired && !activation?.ok) continue;
598
756
  if (record.appOwnership !== "proven") continue;
599
757
  pluginAppIds[record.policy.configKey] = [...record.ownedAppIds].toSorted();
600
- for (const app of resolveThreadConfigAppsForRecord({
758
+ for (const app of resolveCodexThreadConfigAppsForRecord({
601
759
  record,
602
760
  inventory
603
761
  })) {
604
- if (!isPluginAppReadyForThreadStart(app)) {
762
+ const admissionConfig = resolveCodexPluginAppThreadAdmission(app, inventory) === "blocked" ? void 0 : await (configForAppAdmission ??= readCodexConfigForAppAdmission(params));
763
+ if (!admissionConfig || resolveCodexExplicitAppEnablement(admissionConfig.layers, app.id) === false) {
605
764
  diagnostics.push({
606
765
  code: "app_not_ready",
607
766
  plugin: record.policy,
@@ -612,35 +771,51 @@ async function buildCodexPluginThreadConfig(params) {
612
771
  if (record.policy.destructiveApprovalMode === "ask" && !await clearPersistedAppToolApprovalOverrides({
613
772
  request: params.request,
614
773
  configCwd: params.configCwd,
774
+ config: admissionConfig.config,
615
775
  plugin: record.policy,
616
776
  app,
617
777
  diagnostics
618
778
  })) continue;
779
+ provisionalAppIds.add(app.id);
619
780
  apps[app.id] = buildEnabledAppConfig(record.policy);
620
781
  policyApps[app.id] = {
621
782
  configKey: record.policy.configKey,
622
783
  marketplaceName: record.policy.marketplaceName,
623
784
  pluginName: record.policy.pluginName,
624
785
  allowDestructiveActions: record.policy.allowDestructiveActions,
786
+ allowOpenWorld: true,
625
787
  destructiveApprovalMode: record.policy.destructiveApprovalMode,
626
788
  mcpServerNames: [...record.detail?.mcpServers ?? []].toSorted()
627
789
  };
628
790
  }
629
791
  }
630
- for (const app of accountAppsResult.apps) {
792
+ for (const app of unresolvedDisabledPluginOwnership ? [] : accountAppsResult.apps) {
631
793
  if (pluginOwnedAppIds.has(app.id)) continue;
632
- const accountApp = toOwnedAccountApp(app);
794
+ configForAppAdmission ??= readCodexConfigForAppAdmission(params);
795
+ const admissionConfig = await configForAppAdmission;
796
+ if (!admissionConfig) {
797
+ diagnostics.push({
798
+ code: "account_app_config_unavailable",
799
+ message: "Codex account app configuration was unavailable; account apps were not exposed."
800
+ });
801
+ break;
802
+ }
803
+ if (resolveCodexExplicitAppEnablement(admissionConfig.layers, app.id) === false) continue;
804
+ const accountApp = toCodexPluginOwnedAccountApp(app);
633
805
  if (policy.destructiveApprovalMode === "ask" && !await clearPersistedAppToolApprovalOverrides({
634
806
  request: params.request,
635
807
  configCwd: params.configCwd,
808
+ config: admissionConfig.config,
636
809
  app: accountApp,
637
810
  diagnostics
638
811
  })) continue;
812
+ provisionalAppIds.add(app.id);
639
813
  apps[app.id] = buildEnabledAppConfig(policy);
640
814
  policyApps[app.id] = {
641
815
  source: "account",
642
816
  appName: app.name,
643
817
  allowDestructiveActions: policy.allowDestructiveActions,
818
+ allowOpenWorld: true,
644
819
  destructiveApprovalMode: policy.destructiveApprovalMode,
645
820
  mcpServerNames: []
646
821
  };
@@ -650,6 +825,7 @@ async function buildCodexPluginThreadConfig(params) {
650
825
  return {
651
826
  enabled: true,
652
827
  configPatch,
828
+ ...provisionalAppIds.size > 0 ? { provisionalAppIds: Array.from(provisionalAppIds).toSorted() } : {},
653
829
  fingerprint: fingerprintJson({
654
830
  version: CODEX_PLUGIN_THREAD_CONFIG_FINGERPRINT_VERSION,
655
831
  inputFingerprint,
@@ -719,7 +895,7 @@ function buildCodexPluginAppsConfigPatchFromPolicyContext(policyContext) {
719
895
  for (const [appId, policy] of Object.entries(policyContext.apps).toSorted(([left], [right]) => left.localeCompare(right))) apps[appId] = {
720
896
  enabled: true,
721
897
  destructive_enabled: policy.allowDestructiveActions,
722
- open_world_enabled: true,
898
+ open_world_enabled: policy.allowOpenWorld !== false,
723
899
  default_tools_approval_mode: "auto",
724
900
  ...policy.destructiveApprovalMode === "ask" ? { approvals_reviewer: "user" } : {}
725
901
  };
@@ -738,13 +914,22 @@ function buildPluginAppPolicyContext(apps, pluginAppIds) {
738
914
  }
739
915
  async function clearPersistedAppToolApprovalOverrides(params) {
740
916
  try {
741
- const overrideNames = await readPersistedAppToolApprovalOverrideNames(params);
742
- for (const toolName of overrideNames) if (isOverriddenConfigWriteResponse(await params.request("config/value/write", {
917
+ const overrideNames = readPersistedAppToolApprovalOverrideNames(params.config, params.app);
918
+ if (overrideNames.length === 0) return true;
919
+ const edits = overrideNames.map((toolName) => ({
743
920
  keyPath: `apps.${quoteConfigKeyPathSegment(params.app.id)}.tools.${quoteConfigKeyPathSegment(toolName)}.approval_mode`,
744
921
  value: null,
745
922
  mergeStrategy: "replace"
746
- }))) throw new Error(`approval override for ${toolName} is controlled by another config layer`);
747
- const remainingOverrideNames = await readPersistedAppToolApprovalOverrideNames(params);
923
+ }));
924
+ const response = await params.request("config/batchWrite", { edits });
925
+ if (!isJsonObject(response) || response.status !== "ok" && response.status !== "okOverridden") throw new Error("Codex did not confirm the approval override batch");
926
+ if (response.status === "okOverridden") throw new Error(`approval override for ${overrideNames.join(", ")} is controlled by another config layer`);
927
+ const confirmed = await params.request("config/read", {
928
+ includeLayers: false,
929
+ ...params.configCwd ? { cwd: params.configCwd } : {}
930
+ });
931
+ if (!isJsonObject(confirmed) || !isJsonObject(confirmed.config)) throw new Error("Codex did not confirm effective app approval configuration");
932
+ const remainingOverrideNames = readPersistedAppToolApprovalOverrideNames(confirmed.config, params.app);
748
933
  if (remainingOverrideNames.length > 0) throw new Error(`effective approval overrides remain for ${remainingOverrideNames.join(", ")}`);
749
934
  return true;
750
935
  } catch (error) {
@@ -756,24 +941,15 @@ async function clearPersistedAppToolApprovalOverrides(params) {
756
941
  return false;
757
942
  }
758
943
  }
759
- async function readPersistedAppToolApprovalOverrideNames(params) {
760
- const response = await params.request("config/read", {
761
- includeLayers: false,
762
- ...params.configCwd ? { cwd: params.configCwd } : {}
763
- });
764
- const config = isJsonObject(response) ? response.config : void 0;
765
- const appsRoot = isJsonObject(config) ? config.apps : void 0;
766
- const nestedApps = isJsonObject(appsRoot) ? appsRoot.apps : void 0;
767
- const appConfig = isJsonObject(appsRoot) ? appsRoot[params.app.id] ?? (isJsonObject(nestedApps) ? nestedApps[params.app.id] : void 0) : void 0;
944
+ function readPersistedAppToolApprovalOverrideNames(config, app) {
945
+ const appsRoot = config.apps;
946
+ const appConfig = isJsonObject(appsRoot) ? appsRoot[app.id] : void 0;
768
947
  const tools = isJsonObject(appConfig) ? appConfig.tools : void 0;
769
948
  if (!isJsonObject(tools)) return [];
770
949
  return Object.entries(tools).filter(([, value]) => hasPersistedToolApprovalOverride(value)).map(([toolName]) => toolName).toSorted();
771
950
  }
772
951
  function hasPersistedToolApprovalOverride(value) {
773
- return isJsonObject(value) && (value.approval_mode !== void 0 || value.approvalMode !== void 0);
774
- }
775
- function isOverriddenConfigWriteResponse(response) {
776
- return isJsonObject(response) && response.status === "okOverridden";
952
+ return isJsonObject(value) && value.approval_mode !== void 0;
777
953
  }
778
954
  function quoteConfigKeyPathSegment(segment) {
779
955
  return `"${segment.replace(/["\\]/g, (char) => `\\${char}`)}"`;
@@ -785,30 +961,6 @@ function shouldWaitForInitialAppInventory(params, policy, inventory) {
785
961
  function shouldRefreshMissingAppInventory(params, policy, inventory) {
786
962
  return Boolean(params.appCacheKey && policy.pluginPolicies.some((plugin) => plugin.enabled) && inventory.appInventory?.state === "missing");
787
963
  }
788
- async function refreshAppInventoryNow(params, appCache, options = {}) {
789
- const appCacheKey = params.appCacheKey;
790
- if (!appCacheKey) return;
791
- const request = async (method, requestParams) => await params.request(method, requestParams);
792
- try {
793
- return await appCache.refreshNow({
794
- key: appCacheKey,
795
- request,
796
- nowMs: params.nowMs,
797
- forceRefetch: options.forceRefetch,
798
- targetAppIds: options.targetAppIds
799
- });
800
- } catch (error) {
801
- embeddedAgentLog.warn("codex plugin thread config app inventory refresh failed", {
802
- reason: options.reason,
803
- forceRefetch: options.forceRefetch === true,
804
- error: serializeCodexAppInventoryError(error)
805
- });
806
- return;
807
- }
808
- }
809
- function collectInventoryOwnedAppIds(inventory) {
810
- return Array.from(new Set(inventory.records.flatMap((record) => record.ownedAppIds).filter(Boolean))).toSorted();
811
- }
812
964
  function emptyCodexPluginInventory(policy) {
813
965
  return {
814
966
  policy,
@@ -816,42 +968,6 @@ function emptyCodexPluginInventory(policy) {
816
968
  diagnostics: []
817
969
  };
818
970
  }
819
- async function readAccessibleAccountApps(params, appCache) {
820
- const snapshot = await refreshAppInventoryNow(params, appCache, {
821
- forceRefetch: false,
822
- reason: "account_apps_all",
823
- targetAppIds: []
824
- });
825
- if (!snapshot) return {
826
- apps: [],
827
- diagnostic: {
828
- code: "account_app_inventory_unavailable",
829
- message: "Codex account app inventory was unavailable; account apps were not exposed."
830
- }
831
- };
832
- return { apps: snapshot.apps.filter((app) => app.isAccessible).toSorted((left, right) => left.id.localeCompare(right.id)) };
833
- }
834
- function toOwnedAccountApp(app) {
835
- return {
836
- id: app.id,
837
- name: app.name,
838
- accessible: app.isAccessible,
839
- enabled: app.isEnabled,
840
- needsAuth: !app.isAccessible
841
- };
842
- }
843
- function resolveThreadConfigAppsForRecord(params) {
844
- if (params.inventory.appInventory?.state === "missing") return [];
845
- return params.record.apps;
846
- }
847
- function isPluginAppReadyForThreadStart(app) {
848
- return app.accessible;
849
- }
850
- function shouldForceRefreshForNotReadyPluginApps(params, policy, inventory) {
851
- if (!params.appCacheKey || !policy.pluginPolicies.some((plugin) => plugin.enabled)) return false;
852
- if (inventory.appInventory?.state === "missing") return false;
853
- return inventory.records.some((record) => record.appOwnership === "proven" && record.ownedAppIds.length > 0 && (record.apps.length === 0 || record.apps.some((app) => !app.accessible)));
854
- }
855
971
  function policyFingerprint(policy) {
856
972
  return {
857
973
  enabled: policy.enabled,
@@ -872,13 +988,10 @@ function mergeJsonObjects(left, right) {
872
988
  const merged = { ...left };
873
989
  for (const [key, value] of Object.entries(right)) {
874
990
  const existing = merged[key];
875
- merged[key] = isPlainJsonObject(existing) && isPlainJsonObject(value) ? mergeJsonObjects(existing, value) : value;
991
+ merged[key] = isJsonObject(existing) && isJsonObject(value) ? mergeJsonObjects(existing, value) : value;
876
992
  }
877
993
  return merged;
878
994
  }
879
- function isPlainJsonObject(value) {
880
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
881
- }
882
995
  function fingerprintJson(value) {
883
996
  return crypto.createHash("sha256").update(stableStringify(value)).digest("hex");
884
997
  }
@@ -904,15 +1017,18 @@ const CODEX_TURN_START_TEXT_INPUT_MAX_CHARS = 1 << 20;
904
1017
  const DEFAULT_CODEX_PROJECTION_RESERVE_TOKENS = 2e4;
905
1018
  const MIN_PROMPT_BUDGET_RATIO = .5;
906
1019
  const MIN_PROMPT_BUDGET_TOKENS = 8e3;
1020
+ function neutralizeCodexExplicitMentionSigils(text) {
1021
+ return text.replace(/\$(?=[A-Za-z0-9_:-])/gu, "$").replace(/\[@(?=[A-Za-z0-9_:-]+\]\()/gu, "[@");
1022
+ }
907
1023
  /** Projects assembled OpenClaw context-engine messages into Codex prompt inputs. */
908
1024
  function projectContextEngineAssemblyForCodex(params) {
909
1025
  const prompt = params.prompt.trim();
910
1026
  const contextMessages = dropDuplicateTrailingPrompt(params.assembledMessages, prompt);
911
1027
  const maxRenderedContextChars = normalizeRenderedContextMaxChars(params.maxRenderedContextChars);
912
- const renderedContext = renderMessagesForCodexContext(contextMessages, {
1028
+ const renderedContext = neutralizeCodexExplicitMentionSigils(renderMessagesForCodexContext(contextMessages, {
913
1029
  maxTextPartChars: resolveTextPartMaxChars(maxRenderedContextChars),
914
1030
  toolPayloadMode: params.toolPayloadMode ?? "elide"
915
- });
1031
+ }));
916
1032
  const boundedContext = renderedContext ? truncateOlderContext(renderedContext, maxRenderedContextChars) : void 0;
917
1033
  const promptPrefix = boundedContext ? [
918
1034
  CONTEXT_HEADER,
@@ -1113,8 +1229,8 @@ function hasMessageContent(message) {
1113
1229
  return "content" in message;
1114
1230
  }
1115
1231
  function normalizeRenderedContextMaxChars(value) {
1116
- if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_RENDERED_CONTEXT_CHARS;
1117
- return Math.min(MAX_RENDERED_CONTEXT_CHARS, Math.max(DEFAULT_RENDERED_CONTEXT_CHARS, Math.floor(value)));
1232
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return DEFAULT_RENDERED_CONTEXT_CHARS;
1233
+ return Math.min(MAX_RENDERED_CONTEXT_CHARS, Math.max(1, Math.floor(value)));
1118
1234
  }
1119
1235
  function resolveTextPartMaxChars(maxRenderedContextChars) {
1120
1236
  return Math.min(MAX_TEXT_PART_CHARS, Math.max(DEFAULT_TEXT_PART_CHARS, Math.floor(maxRenderedContextChars / 4)));
@@ -1176,107 +1292,15 @@ function areContextEngineProjectionBindingsCompatible(previous, next) {
1176
1292
  return previous?.schemaVersion === next.schemaVersion && previous.mode === next.mode && previous.epoch === next.epoch && previous.fingerprint === next.fingerprint;
1177
1293
  }
1178
1294
  function resolveContextEngineCitationsMode(config) {
1179
- const rootConfig = isUnknownRecord(config) ? config : void 0;
1180
- const citations = (isUnknownRecord(rootConfig?.memory) ? rootConfig.memory : void 0)?.citations;
1295
+ const rootConfig = isRecord(config) ? config : void 0;
1296
+ const citations = (isRecord(rootConfig?.memory) ? rootConfig.memory : void 0)?.citations;
1181
1297
  return isJsonConfigValue(citations) ? citations : void 0;
1182
1298
  }
1183
- function isUnknownRecord(value) {
1184
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
1185
- }
1186
1299
  function isJsonConfigValue(value) {
1187
1300
  if (value === null || typeof value === "string" || typeof value === "boolean") return true;
1188
1301
  if (typeof value === "number") return Number.isFinite(value);
1189
1302
  if (Array.isArray(value)) return value.every(isJsonConfigValue);
1190
- return isUnknownRecord(value) && Object.values(value).every(isJsonConfigValue);
1191
- }
1192
- //#endregion
1193
- //#region extensions/codex/src/app-server/thread-fingerprints.ts
1194
- function codexDynamicToolsFingerprint(dynamicTools) {
1195
- return fingerprintDynamicTools(dynamicTools);
1196
- }
1197
- function codexLegacyDynamicToolsFingerprint(dynamicTools) {
1198
- return legacyFingerprintDynamicTools(dynamicTools);
1199
- }
1200
- function areCodexDynamicToolFingerprintsCompatible(params) {
1201
- return areDynamicToolFingerprintsCompatible(params.previous, params.next, params.nextLegacy);
1202
- }
1203
- function fingerprintDynamicTools(dynamicTools) {
1204
- return hashCodexAppServerBindingFingerprint(legacyFingerprintDynamicTools(dynamicTools));
1205
- }
1206
- function legacyFingerprintDynamicTools(dynamicTools) {
1207
- return JSON.stringify(dynamicTools.map(fingerprintDynamicToolSpec).toSorted(compareJsonFingerprint));
1208
- }
1209
- function legacyFingerprintUserMcpServersConfigPatch(configPatch) {
1210
- return configPatch ? JSON.stringify(stabilizeJsonValue(configPatch)) : void 0;
1211
- }
1212
- function fingerprintUserMcpServersConfigPatch(configPatch) {
1213
- return configPatch ? hashCodexAppServerBindingFingerprint(JSON.stringify(stabilizeJsonValue(redactUserMcpServersFingerprintSecrets(configPatch)))) : void 0;
1214
- }
1215
- function redactUserMcpServersFingerprintSecrets(value) {
1216
- if (Array.isArray(value)) return value.map(redactUserMcpServersFingerprintSecrets);
1217
- if (!value || typeof value !== "object") return value;
1218
- const next = {};
1219
- for (const [key, entry] of Object.entries(value)) {
1220
- if (key === "http_headers" && entry && typeof entry === "object" && !Array.isArray(entry)) {
1221
- next[key] = Object.fromEntries(Object.entries(entry).map(([header, headerValue]) => [header, header.toLowerCase() === "authorization" ? fingerprintUserMcpServersAuthorizationHeader(headerValue) : headerValue]));
1222
- continue;
1223
- }
1224
- next[key] = redactUserMcpServersFingerprintSecrets(entry);
1225
- }
1226
- return next;
1227
- }
1228
- function fingerprintUserMcpServersAuthorizationHeader(value) {
1229
- return typeof value === "string" && value.length > 0 ? `<redacted:sha256:${crypto$1.createHash("sha256").update(value).digest("hex")}>` : "<redacted>";
1230
- }
1231
- function fingerprintJsonObject(value) {
1232
- return JSON.stringify(stabilizeJsonValue(value));
1233
- }
1234
- function fingerprintEnvironmentSelection(environments) {
1235
- return environments ? JSON.stringify(environments.map(stabilizeJsonValue)) : void 0;
1236
- }
1237
- function fingerprintDynamicToolSpec(tool) {
1238
- return stabilizeDynamicToolFingerprintValue(tool);
1239
- }
1240
- function stabilizeDynamicToolFingerprintValue(value) {
1241
- if (Array.isArray(value)) return value.map(stabilizeDynamicToolFingerprintValue);
1242
- if (!isJsonObject(value)) return value;
1243
- const stable = {};
1244
- for (const [key, child] of Object.entries(value).toSorted(([left], [right]) => left.localeCompare(right))) {
1245
- if (key === "description") continue;
1246
- stable[key] = stabilizeDynamicToolFingerprintValue(child);
1247
- }
1248
- return stable;
1249
- }
1250
- function stabilizeJsonValue(value) {
1251
- if (Array.isArray(value)) return value.map(stabilizeJsonValue);
1252
- if (!isJsonObject(value)) return value;
1253
- const stable = {};
1254
- for (const [key, child] of Object.entries(value).toSorted(([left], [right]) => left.localeCompare(right))) stable[key] = stabilizeJsonValue(child);
1255
- return stable;
1256
- }
1257
- function readActiveCodexTurnIds(thread) {
1258
- return (thread.turns ?? []).filter((turn) => turn.status === "inProgress").map((turn) => typeof turn.id === "string" ? turn.id : "").filter((turnId) => turnId.trim().length > 0);
1259
- }
1260
- function readActiveCodexTurnIdsFromResume(response) {
1261
- const pagedTurns = response.initialTurnsPage?.data;
1262
- return readActiveCodexTurnIds(Array.isArray(pagedTurns) ? { turns: pagedTurns } : response.thread);
1263
- }
1264
- const LEGACY_EMPTY_DYNAMIC_TOOLS_FINGERPRINT = legacyFingerprintDynamicTools([]);
1265
- const EMPTY_DYNAMIC_TOOLS_FINGERPRINT = hashCodexAppServerBindingFingerprint(LEGACY_EMPTY_DYNAMIC_TOOLS_FINGERPRINT);
1266
- function areDynamicToolFingerprintsCompatible(previous, next, nextLegacy) {
1267
- return !previous || previous === next || previous === nextLegacy;
1268
- }
1269
- function areUserMcpServersFingerprintsCompatible(params) {
1270
- return params.previous === params.next || params.previous === params.nextLegacy || params.nextLegacy !== void 0 && params.previous === hashCodexAppServerBindingFingerprint(params.nextLegacy);
1271
- }
1272
- function shouldStartTransientNoToolThread(params) {
1273
- return Boolean(params.previous && !isEmptyDynamicToolsFingerprint(params.previous) && !params.nextHasDynamicTools);
1274
- }
1275
- function isEmptyDynamicToolsFingerprint(fingerprint) {
1276
- return fingerprint === EMPTY_DYNAMIC_TOOLS_FINGERPRINT || fingerprint === LEGACY_EMPTY_DYNAMIC_TOOLS_FINGERPRINT;
1277
- }
1278
- function compareJsonFingerprint(left, right) {
1279
- return JSON.stringify(left).localeCompare(JSON.stringify(right));
1303
+ return isRecord(value) && Object.values(value).every(isJsonConfigValue);
1280
1304
  }
1281
1305
  //#endregion
1282
1306
  //#region extensions/codex/src/app-server/dynamic-tool-profile.ts
@@ -1299,6 +1323,14 @@ const CODEX_NATIVE_GOAL_TOOL_EXCLUDES = [
1299
1323
  "create_goal",
1300
1324
  "update_goal"
1301
1325
  ];
1326
+ const CODEX_APP_SERVER_OWNED_REPLACEABLE_TOOL_EXCLUDES = /* @__PURE__ */ new Set([
1327
+ "read",
1328
+ "write",
1329
+ "edit",
1330
+ "apply_patch",
1331
+ "update_plan",
1332
+ ...CODEX_NATIVE_GOAL_TOOL_EXCLUDES
1333
+ ]);
1302
1334
  const CODEX_APP_SERVER_OWNED_SHELL_TOOL_EXCLUDES = /* @__PURE__ */ new Set(["exec", "process"]);
1303
1335
  const DYNAMIC_TOOL_NAME_ALIASES = {
1304
1336
  bash: "exec",
@@ -1313,6 +1345,10 @@ function normalizeCodexDynamicToolName(name) {
1313
1345
  function isSystemAgentOnlyCodexDynamicToolAllowlist(toolsAllow) {
1314
1346
  return toolsAllow?.length === 1 && normalizeCodexDynamicToolName(toolsAllow[0] ?? "") === "openclaw";
1315
1347
  }
1348
+ /** True when a private source reply may use the message delivery tool only. */
1349
+ function isMessageOnlyCodexSourceReply(params) {
1350
+ return params.sourceReplyDeliveryMode === "message_tool_only" && params.toolsAllow?.length === 1 && normalizeCodexDynamicToolName(params.toolsAllow[0] ?? "") === "message";
1351
+ }
1316
1352
  /** Returns true for private QA runs that force the Codex runtime profile. */
1317
1353
  function isForcedPrivateQaCodexRuntime(env = process.env) {
1318
1354
  return env.OPENCLAW_BUILD_PRIVATE_QA === "1" && env.OPENCLAW_QA_FORCE_RUNTIME?.trim().toLowerCase() === "codex";
@@ -1346,17 +1382,24 @@ function resolveCodexDynamicToolsLoadingForRuntime(config, modelId, options = {}
1346
1382
  }
1347
1383
  /** Filters OpenClaw tools that Codex owns natively or config explicitly excludes. */
1348
1384
  function filterCodexDynamicTools(tools, config, env = process.env) {
1349
- return filterCodexDynamicToolsWithOptions(tools, config, env, { preserveOpenClawShell: false });
1385
+ return filterCodexDynamicToolsWithOptions(tools, config, env, {
1386
+ preserveOpenClawReplacements: false,
1387
+ preserveOpenClawShell: false
1388
+ });
1350
1389
  }
1351
- /** Keeps exec/process only when Codex cannot advertise an environment-backed native shell. */
1352
- function filterCodexDynamicToolsWithOpenClawShell(tools, config, env = process.env) {
1353
- return filterCodexDynamicToolsWithOptions(tools, config, env, { preserveOpenClawShell: true });
1390
+ /** Keeps OpenClaw coding tools that replace a disabled Codex native surface. */
1391
+ function filterCodexDynamicToolsForDisabledNativeSurface(tools, config, options, env = process.env) {
1392
+ return filterCodexDynamicToolsWithOptions(tools, config, env, {
1393
+ preserveOpenClawReplacements: true,
1394
+ preserveOpenClawShell: options.preserveShell
1395
+ });
1354
1396
  }
1355
1397
  function filterCodexDynamicToolsWithOptions(tools, config, env, options) {
1356
1398
  const excludes = /* @__PURE__ */ new Set();
1357
- for (const name of CODEX_NATIVE_GOAL_TOOL_EXCLUDES) excludes.add(name);
1399
+ if (!options.preserveOpenClawReplacements) for (const name of CODEX_NATIVE_GOAL_TOOL_EXCLUDES) excludes.add(name);
1358
1400
  if (isForcedPrivateQaCodexRuntime(env)) excludes.add("apply_patch");
1359
1401
  else for (const name of CODEX_APP_SERVER_OWNED_DYNAMIC_TOOL_EXCLUDES) {
1402
+ if (options.preserveOpenClawReplacements && CODEX_APP_SERVER_OWNED_REPLACEABLE_TOOL_EXCLUDES.has(name)) continue;
1360
1403
  if (options.preserveOpenClawShell && CODEX_APP_SERVER_OWNED_SHELL_TOOL_EXCLUDES.has(name)) continue;
1361
1404
  excludes.add(name);
1362
1405
  }
@@ -1367,13 +1410,6 @@ function filterCodexDynamicToolsWithOptions(tools, config, env, options) {
1367
1410
  return excludes.size === 0 ? tools : tools.filter((tool) => !excludes.has(normalizeCodexDynamicToolName(tool.name)));
1368
1411
  }
1369
1412
  //#endregion
1370
- //#region extensions/codex/src/app-server/profiler-flag.ts
1371
- const PROFILER_FLAGS = ["profiler", "codex.profiler"];
1372
- /** Checks the generic and Codex-specific profiler diagnostic flags. */
1373
- function isCodexAppServerProfilerEnabled(config, env = process.env) {
1374
- return PROFILER_FLAGS.some((flag) => isDiagnosticFlagEnabled(flag, config, env));
1375
- }
1376
- //#endregion
1377
1413
  //#region extensions/codex/src/app-server/thread-binding-policy.ts
1378
1414
  function shouldRotateCodexAppServerBindingForRuntime(params) {
1379
1415
  if (!params.current) return false;
@@ -1424,6 +1460,299 @@ function shouldRecheckRecoverablePluginBinding(params) {
1424
1460
  return bindingContainsSettledPlugin || accountAppRecoveryEnabled && Object.keys(policyContext.apps).length === 0 || recoverablePluginConfigKeys.length > 0;
1425
1461
  }
1426
1462
  //#endregion
1463
+ //#region extensions/codex/src/app-server/thread-fingerprints.ts
1464
+ function codexDynamicToolsFingerprint(dynamicTools) {
1465
+ return fingerprintDynamicTools(dynamicTools);
1466
+ }
1467
+ function codexLegacyDynamicToolsFingerprint(dynamicTools) {
1468
+ return legacyFingerprintDynamicTools(dynamicTools);
1469
+ }
1470
+ function areCodexDynamicToolFingerprintsCompatible(params) {
1471
+ return areDynamicToolFingerprintsCompatible(params.previous, params.next, params.nextLegacy);
1472
+ }
1473
+ function fingerprintDynamicTools(dynamicTools) {
1474
+ return hashCodexAppServerBindingFingerprint(legacyFingerprintDynamicTools(dynamicTools));
1475
+ }
1476
+ function legacyFingerprintDynamicTools(dynamicTools) {
1477
+ return JSON.stringify(dynamicTools.map(fingerprintDynamicToolSpec).toSorted(compareJsonFingerprint));
1478
+ }
1479
+ function legacyFingerprintUserMcpServersConfigPatch(configPatch) {
1480
+ return configPatch ? JSON.stringify(stabilizeJsonValue(configPatch)) : void 0;
1481
+ }
1482
+ function fingerprintUserMcpServersConfigPatch(configPatch) {
1483
+ return configPatch ? hashCodexAppServerBindingFingerprint(JSON.stringify(stabilizeJsonValue(redactUserMcpServersFingerprintSecrets(configPatch)))) : void 0;
1484
+ }
1485
+ function redactUserMcpServersFingerprintSecrets(value) {
1486
+ if (Array.isArray(value)) return value.map(redactUserMcpServersFingerprintSecrets);
1487
+ if (!value || typeof value !== "object") return value;
1488
+ const next = {};
1489
+ for (const [key, entry] of Object.entries(value)) {
1490
+ if (key === "http_headers" && entry && typeof entry === "object" && !Array.isArray(entry)) {
1491
+ next[key] = Object.fromEntries(Object.entries(entry).map(([header, headerValue]) => [header, header.toLowerCase() === "authorization" ? fingerprintUserMcpServersAuthorizationHeader(headerValue) : headerValue]));
1492
+ continue;
1493
+ }
1494
+ next[key] = redactUserMcpServersFingerprintSecrets(entry);
1495
+ }
1496
+ return next;
1497
+ }
1498
+ function fingerprintUserMcpServersAuthorizationHeader(value) {
1499
+ return typeof value === "string" && value.length > 0 ? `<redacted:sha256:${crypto$1.createHash("sha256").update(value).digest("hex")}>` : "<redacted>";
1500
+ }
1501
+ function fingerprintJsonObject(value) {
1502
+ return JSON.stringify(stabilizeJsonValue(value));
1503
+ }
1504
+ /** Hash thread-creation identity; settings already applied by turn/start must not restart Codex. */
1505
+ function fingerprintCodexThreadConfig(request, authProfileId, dynamicToolsFingerprint) {
1506
+ return hashCodexAppServerBindingFingerprint(fingerprintJsonObject({
1507
+ authProfileId: authProfileId ?? null,
1508
+ dynamicToolsFingerprint: dynamicToolsFingerprint ?? null,
1509
+ nativeMultiAgentVersion: resolveCodexGpt56MultiAgentVersion(typeof request.requestedModel === "string" ? request.requestedModel : typeof request.model === "string" ? request.model : void 0) ?? null,
1510
+ modelProvider: request.modelProvider ?? null,
1511
+ requestedModelProvider: request.requestedModelProvider === void 0 ? request.modelProvider ?? null : request.requestedModelProvider,
1512
+ permissions: request.permissions ?? null,
1513
+ baseInstructions: request.baseInstructions ?? null,
1514
+ developerInstructions: request.developerInstructions ?? null,
1515
+ config: request.config ?? {}
1516
+ }));
1517
+ }
1518
+ function fingerprintEnvironmentSelection(environments) {
1519
+ return environments ? JSON.stringify(environments.map(stabilizeJsonValue)) : void 0;
1520
+ }
1521
+ function fingerprintDynamicToolSpec(tool) {
1522
+ return stabilizeJsonValue(tool);
1523
+ }
1524
+ function stabilizeJsonValue(value) {
1525
+ if (Array.isArray(value)) return value.map(stabilizeJsonValue);
1526
+ if (!isJsonObject(value)) return value;
1527
+ const stable = {};
1528
+ for (const [key, child] of Object.entries(value).toSorted(([left], [right]) => left.localeCompare(right))) stable[key] = stabilizeJsonValue(child);
1529
+ return stable;
1530
+ }
1531
+ function readActiveCodexTurnIds(thread) {
1532
+ return (thread.turns ?? []).filter((turn) => turn.status === "inProgress").map((turn) => typeof turn.id === "string" ? turn.id : "").filter((turnId) => turnId.trim().length > 0);
1533
+ }
1534
+ function readActiveCodexTurnIdsFromResume(response) {
1535
+ const pagedTurns = response.initialTurnsPage?.data;
1536
+ return readActiveCodexTurnIds(Array.isArray(pagedTurns) ? { turns: pagedTurns } : response.thread);
1537
+ }
1538
+ const LEGACY_EMPTY_DYNAMIC_TOOLS_FINGERPRINT = legacyFingerprintDynamicTools([]);
1539
+ const EMPTY_DYNAMIC_TOOLS_FINGERPRINT = hashCodexAppServerBindingFingerprint(LEGACY_EMPTY_DYNAMIC_TOOLS_FINGERPRINT);
1540
+ function areDynamicToolFingerprintsCompatible(previous, next, nextLegacy) {
1541
+ return !previous || previous === next || previous === nextLegacy;
1542
+ }
1543
+ function areUserMcpServersFingerprintsCompatible(params) {
1544
+ return params.previous === params.next || params.previous === params.nextLegacy || params.nextLegacy !== void 0 && params.previous === hashCodexAppServerBindingFingerprint(params.nextLegacy);
1545
+ }
1546
+ function shouldStartTransientNoToolThread(params) {
1547
+ return Boolean(params.previous && !isEmptyDynamicToolsFingerprint(params.previous) && !params.nextHasDynamicTools);
1548
+ }
1549
+ function isEmptyDynamicToolsFingerprint(fingerprint) {
1550
+ return fingerprint === EMPTY_DYNAMIC_TOOLS_FINGERPRINT || fingerprint === LEGACY_EMPTY_DYNAMIC_TOOLS_FINGERPRINT;
1551
+ }
1552
+ function compareJsonFingerprint(left, right) {
1553
+ return JSON.stringify(left).localeCompare(JSON.stringify(right));
1554
+ }
1555
+ //#endregion
1556
+ //#region extensions/codex/src/app-server/native-skill-isolation.ts
1557
+ const MAX_PERSONAL_SKILL_DIRECTORIES = 2e3;
1558
+ const MAX_PERSONAL_SKILL_DEPTH = 6;
1559
+ const MAX_PERSONAL_SKILL_ENTRIES = 1e4;
1560
+ const nativeSkillIsolationByClient = /* @__PURE__ */ new WeakMap();
1561
+ function isMissingPathError(error) {
1562
+ return error.code === "ENOENT";
1563
+ }
1564
+ function isPathWithin(root, candidate) {
1565
+ const relative = path.relative(path.resolve(root), path.resolve(candidate));
1566
+ return relative === "" || !path.isAbsolute(relative) && !relative.startsWith(`..${path.sep}`) && relative !== "..";
1567
+ }
1568
+ async function canonicalizeExistingPath(candidate) {
1569
+ try {
1570
+ return await fs.realpath(candidate);
1571
+ } catch {
1572
+ return path.resolve(candidate);
1573
+ }
1574
+ }
1575
+ async function usesDefaultStateDir() {
1576
+ if (!process.env.OPENCLAW_STATE_DIR?.trim()) return true;
1577
+ const home = resolveRequiredHomeDir();
1578
+ const [stateDir, defaultStateDir] = await Promise.all([canonicalizeExistingPath(resolveStateDir()), canonicalizeExistingPath(path.join(home, ".openclaw"))]);
1579
+ return stateDir === defaultStateDir;
1580
+ }
1581
+ async function collectPersonalSkillRealPaths(homes, codexHome) {
1582
+ const realStateDir = await canonicalizeExistingPath(resolveStateDir());
1583
+ const roots = [];
1584
+ for (const home of homes) {
1585
+ for (const dir of [".agents", ".claude"]) roots.push({
1586
+ dir: path.join(home, dir, "skills"),
1587
+ onlyEscapedStateTargets: false
1588
+ });
1589
+ const defaultCodexHome = path.join(home, ".codex");
1590
+ const realDefaultCodexHome = await canonicalizeExistingPath(defaultCodexHome);
1591
+ roots.push({
1592
+ dir: path.join(defaultCodexHome, "skills"),
1593
+ onlyEscapedStateTargets: isPathWithin(realStateDir, realDefaultCodexHome)
1594
+ });
1595
+ }
1596
+ const configuredCodexHome = codexHome?.trim() || process.env.CODEX_HOME?.trim();
1597
+ if (configuredCodexHome) {
1598
+ const stateOwned = isPathWithin(realStateDir, await canonicalizeExistingPath(configuredCodexHome));
1599
+ roots.push({
1600
+ dir: path.join(configuredCodexHome, "skills"),
1601
+ onlyEscapedStateTargets: stateOwned
1602
+ });
1603
+ }
1604
+ const skillPaths = /* @__PURE__ */ new Set();
1605
+ let complete = true;
1606
+ const seenDirectories = /* @__PURE__ */ new Set();
1607
+ const queue = roots.map((root) => ({
1608
+ dir: root.dir,
1609
+ onlyEscapedStateTargets: root.onlyEscapedStateTargets,
1610
+ depth: 0
1611
+ }));
1612
+ let entryCount = 0;
1613
+ const recordSkillFile = async (filePath, onlyEscapedStateTargets) => {
1614
+ try {
1615
+ const skillRealPath = await fs.realpath(filePath);
1616
+ if (!onlyEscapedStateTargets || !isPathWithin(realStateDir, skillRealPath)) skillPaths.add(skillRealPath);
1617
+ } catch (error) {
1618
+ if (!isMissingPathError(error)) complete = false;
1619
+ }
1620
+ };
1621
+ for (const current of queue) {
1622
+ let realDir;
1623
+ try {
1624
+ realDir = await fs.realpath(current.dir);
1625
+ } catch (error) {
1626
+ if (isMissingPathError(error)) continue;
1627
+ complete = false;
1628
+ continue;
1629
+ }
1630
+ if (seenDirectories.has(realDir)) continue;
1631
+ seenDirectories.add(realDir);
1632
+ if (seenDirectories.size > MAX_PERSONAL_SKILL_DIRECTORIES) {
1633
+ complete = false;
1634
+ break;
1635
+ }
1636
+ let directory;
1637
+ try {
1638
+ directory = await fs.opendir(current.dir);
1639
+ } catch (error) {
1640
+ if (!isMissingPathError(error)) complete = false;
1641
+ continue;
1642
+ }
1643
+ try {
1644
+ for await (const entry of directory) {
1645
+ entryCount += 1;
1646
+ if (entryCount > MAX_PERSONAL_SKILL_ENTRIES) {
1647
+ complete = false;
1648
+ queue.length = 0;
1649
+ break;
1650
+ }
1651
+ if (entry.name.startsWith(".")) continue;
1652
+ const entryPath = path.join(current.dir, entry.name);
1653
+ if (entry.name === "SKILL.md" && entry.isFile()) {
1654
+ await recordSkillFile(entryPath, current.onlyEscapedStateTargets);
1655
+ continue;
1656
+ }
1657
+ if (entry.isSymbolicLink()) {
1658
+ try {
1659
+ const stat = await fs.stat(entryPath);
1660
+ if (entry.name === "SKILL.md" && stat.isFile()) await recordSkillFile(entryPath, current.onlyEscapedStateTargets);
1661
+ else if (stat.isDirectory()) if (current.depth < MAX_PERSONAL_SKILL_DEPTH) queue.push({
1662
+ dir: entryPath,
1663
+ depth: current.depth + 1,
1664
+ onlyEscapedStateTargets: current.onlyEscapedStateTargets
1665
+ });
1666
+ else complete = false;
1667
+ } catch (error) {
1668
+ if (!isMissingPathError(error)) complete = false;
1669
+ }
1670
+ continue;
1671
+ }
1672
+ if (current.depth >= MAX_PERSONAL_SKILL_DEPTH) {
1673
+ if (entry.isDirectory()) complete = false;
1674
+ continue;
1675
+ }
1676
+ if (entry.isDirectory()) {
1677
+ queue.push({
1678
+ dir: entryPath,
1679
+ depth: current.depth + 1,
1680
+ onlyEscapedStateTargets: current.onlyEscapedStateTargets
1681
+ });
1682
+ continue;
1683
+ }
1684
+ }
1685
+ } catch (error) {
1686
+ if (!isMissingPathError(error)) complete = false;
1687
+ }
1688
+ }
1689
+ return {
1690
+ complete,
1691
+ skillPaths
1692
+ };
1693
+ }
1694
+ /** Resolves the native user-scope skills that an isolated OpenClaw thread must disable. */
1695
+ async function resolveCodexNativeSkillIsolation(params) {
1696
+ params.signal?.throwIfAborted();
1697
+ if (!process.env.OPENCLAW_STATE_DIR?.trim()) return;
1698
+ const key = JSON.stringify([
1699
+ path.resolve(resolveStateDir()),
1700
+ path.resolve(params.cwd),
1701
+ params.codexHome?.trim() || process.env.CODEX_HOME?.trim() || "",
1702
+ params.home?.trim() || process.env.HOME?.trim() || "",
1703
+ params.userProfile?.trim() || process.env.USERPROFILE?.trim() || ""
1704
+ ]);
1705
+ const cached = nativeSkillIsolationByClient.get(params.client);
1706
+ if (cached?.key === key && (cached.settled || cached.signal === params.signal)) {
1707
+ const isolation = await cached.result;
1708
+ params.signal?.throwIfAborted();
1709
+ return isolation;
1710
+ }
1711
+ const result = resolveUncachedCodexNativeSkillIsolation(params);
1712
+ const entry = {
1713
+ key,
1714
+ result,
1715
+ settled: false,
1716
+ signal: params.signal
1717
+ };
1718
+ nativeSkillIsolationByClient.set(params.client, entry);
1719
+ try {
1720
+ const isolation = await result;
1721
+ entry.settled = true;
1722
+ params.signal?.throwIfAborted();
1723
+ return isolation;
1724
+ } catch (error) {
1725
+ if (nativeSkillIsolationByClient.get(params.client)?.result === result) nativeSkillIsolationByClient.delete(params.client);
1726
+ throw error;
1727
+ }
1728
+ }
1729
+ async function resolveUncachedCodexNativeSkillIsolation(params) {
1730
+ if (await usesDefaultStateDir()) return;
1731
+ const response = await params.client.request("skills/list", {
1732
+ cwds: [params.cwd],
1733
+ forceReload: true
1734
+ }, { signal: params.signal });
1735
+ const homes = [params.home?.trim() || process.env.HOME?.trim() || process.env.USERPROFILE?.trim() || os.homedir()];
1736
+ if (process.platform === "win32") homes.push(params.userProfile?.trim() || os.homedir());
1737
+ const personalSkills = await collectPersonalSkillRealPaths([...new Set(homes.map((home) => path.resolve(home)))], params.codexHome);
1738
+ 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)) };
1739
+ }
1740
+ /** Applies path-exact session rules after caller config so isolated user skills stay disabled. */
1741
+ function applyCodexNativeSkillIsolation(config, isolation) {
1742
+ if (!isolation) return config;
1743
+ const existingRules = config?.["skills.config"];
1744
+ if (existingRules !== void 0 && !Array.isArray(existingRules)) throw new Error("Codex thread skills.config must be an array");
1745
+ const disabledRules = isolation.disabledUserSkillPaths.map((skillPath) => ({
1746
+ path: skillPath,
1747
+ enabled: false
1748
+ }));
1749
+ return {
1750
+ ...config,
1751
+ "skills.include_instructions": false,
1752
+ "skills.config": [...existingRules ?? [], ...disabledRules]
1753
+ };
1754
+ }
1755
+ //#endregion
1427
1756
  //#region extensions/codex/src/app-server/thread-lifecycle-errors.ts
1428
1757
  var CodexThreadStartRequestError = class extends Error {
1429
1758
  constructor(cause) {
@@ -1437,10 +1766,10 @@ var CodexThreadBindingConflictError = class extends Error {
1437
1766
  this.name = "CodexThreadBindingConflictError";
1438
1767
  }
1439
1768
  };
1440
- var CodexRingZeroAttestationError = class extends Error {
1769
+ var CodexRestrictedToolSurfaceAttestationError = class extends Error {
1441
1770
  constructor(cause) {
1442
- super("Codex ring-zero MCP attestation failed", { cause });
1443
- this.name = "CodexRingZeroAttestationError";
1771
+ super("Codex restricted-tool-surface MCP attestation failed", { cause });
1772
+ this.name = "CodexRestrictedToolSurfaceAttestationError";
1444
1773
  }
1445
1774
  };
1446
1775
  var CodexThreadBindingConflictAfterCleanupError = class extends CodexThreadBindingConflictError {};
@@ -1449,7 +1778,62 @@ var CodexAdoptedThreadActiveError = class extends Error {
1449
1778
  super("Codex session became active in another runner; wait for it to finish before continuing");
1450
1779
  this.name = "CodexAdoptedThreadActiveError";
1451
1780
  }
1452
- };
1781
+ };
1782
+ //#endregion
1783
+ //#region extensions/codex/src/app-server/plugin-thread-attestation.ts
1784
+ /**
1785
+ * Confirms admitted plugin and account apps against their actual Codex thread before
1786
+ * OpenClaw commits a binding or starts a turn.
1787
+ */
1788
+ var CodexPluginThreadAppAttestationError = class extends Error {
1789
+ constructor(message, options) {
1790
+ super(message, options);
1791
+ this.name = "CodexPluginThreadAppAttestationError";
1792
+ }
1793
+ };
1794
+ /** Reads the existing runtime snapshot with the started thread's effective app policy. */
1795
+ async function attestCodexPluginThreadApps(params) {
1796
+ const appIds = Array.from(new Set(params.appIds.filter(Boolean))).toSorted();
1797
+ if (appIds.length === 0) return;
1798
+ let response;
1799
+ try {
1800
+ response = await params.client.request("app/installed", {
1801
+ threadId: params.threadId,
1802
+ forceRefresh: false
1803
+ }, { signal: params.signal });
1804
+ } catch (error) {
1805
+ throw new CodexPluginThreadAppAttestationError(`Codex could not confirm admitted apps for thread ${params.threadId}`, { cause: error });
1806
+ }
1807
+ const installedById = new Map(response.apps.map((app) => [app.id, app]));
1808
+ const failures = appIds.flatMap((appId) => {
1809
+ const app = installedById.get(appId);
1810
+ if (!app) return [`${appId}:missing`];
1811
+ if (!app.enabled) return [`${appId}:disabled`];
1812
+ return app.callable ? [] : [`${appId}:not-callable`];
1813
+ });
1814
+ if (failures.length > 0) throw new CodexPluginThreadAppAttestationError(`Codex thread ${params.threadId} did not expose admitted apps: ${failures.join(", ")}`);
1815
+ }
1816
+ /** Deletes a persistent pre-turn thread; ephemeral threads can only be unsubscribed. */
1817
+ async function discardUnattestedCodexPluginThread(params) {
1818
+ if (params.ephemeral) return await unsubscribeCodexThreadBestEffort(params.client, {
1819
+ threadId: params.threadId,
1820
+ timeoutMs: CODEX_APP_SERVER_UNSUBSCRIBE_TIMEOUT_MS
1821
+ });
1822
+ try {
1823
+ await params.client.request("thread/delete", { threadId: params.threadId }, { timeoutMs: CODEX_APP_SERVER_UNSUBSCRIBE_TIMEOUT_MS });
1824
+ return true;
1825
+ } catch (error) {
1826
+ embeddedAgentLog.debug("codex plugin app attestation thread deletion failed", {
1827
+ threadId: params.threadId,
1828
+ error
1829
+ });
1830
+ await unsubscribeCodexThreadBestEffort(params.client, {
1831
+ threadId: params.threadId,
1832
+ timeoutMs: CODEX_APP_SERVER_UNSUBSCRIBE_TIMEOUT_MS
1833
+ });
1834
+ return false;
1835
+ }
1836
+ }
1453
1837
  //#endregion
1454
1838
  //#region extensions/codex/src/app-server/reasoning-effort.ts
1455
1839
  const CODEX_REASONING_EFFORTS = [
@@ -1461,30 +1845,13 @@ const CODEX_REASONING_EFFORTS = [
1461
1845
  "max",
1462
1846
  "ultra"
1463
1847
  ];
1464
- const GPT_56_MAX_REASONING_EFFORTS = [
1465
- "low",
1466
- "medium",
1467
- "high",
1468
- "xhigh",
1469
- "max"
1470
- ];
1471
- const GPT_56_ULTRA_REASONING_EFFORTS = [...GPT_56_MAX_REASONING_EFFORTS, "ultra"];
1472
- const GPT_5_PRO_REASONING_EFFORTS = [
1848
+ const LEGACY_PRO_REASONING_EFFORTS = [
1473
1849
  "medium",
1474
1850
  "high",
1475
1851
  "xhigh"
1476
1852
  ];
1477
- const GPT_56_ULTRA_MODEL_IDS = /* @__PURE__ */ new Set(["gpt-5.6-sol", "gpt-5.6-terra"]);
1478
- const GPT_56_MAX_MODEL_IDS = /* @__PURE__ */ new Set([...GPT_56_ULTRA_MODEL_IDS, "gpt-5.6-luna"]);
1479
- const MODERN_CODEX_MODEL_IDS = /* @__PURE__ */ new Set([
1480
- ...GPT_56_MAX_MODEL_IDS,
1481
- "gpt-5.5",
1482
- "gpt-5.5-pro",
1483
- "gpt-5.4",
1484
- "gpt-5.4-pro",
1485
- "gpt-5.4-mini",
1486
- "gpt-5.3-codex-spark"
1487
- ]);
1853
+ const LEGACY_PRO_MODEL_ID_RE = /^gpt-5\.[45]-pro$/u;
1854
+ const MODERN_GPT_5_MODEL_ID_RE = /^gpt-5\.(?:[3-9]|[1-9]\d)(?:$|-)/u;
1488
1855
  function normalizeCodexReasoningEfforts(efforts) {
1489
1856
  if (!efforts) return [];
1490
1857
  const supported = new Set(efforts.map((effort) => effort.trim().toLowerCase()));
@@ -1504,24 +1871,21 @@ function resolveSupportedReasoningEffort(params) {
1504
1871
  const requestedRank = CODEX_REASONING_EFFORTS.indexOf(params.requested);
1505
1872
  return fallbackEfforts.find((effort) => CODEX_REASONING_EFFORTS.indexOf(effort) >= requestedRank) ?? fallbackEfforts.at(-1);
1506
1873
  }
1507
- function resolveFallbackReasoningEfforts(modelId) {
1508
- const normalized = modelId.trim().toLowerCase();
1509
- if (GPT_56_ULTRA_MODEL_IDS.has(normalized)) return GPT_56_ULTRA_REASONING_EFFORTS;
1510
- if (normalized === "gpt-5.6-luna") return GPT_56_MAX_REASONING_EFFORTS;
1511
- if (normalized === "gpt-5.5-pro" || normalized === "gpt-5.4-pro") return GPT_5_PRO_REASONING_EFFORTS;
1512
- }
1513
- /** Resolve a turn effort from app-server metadata, with exact-name offline fallbacks. */
1874
+ /** Resolve a turn effort from the selected model's provider-owned metadata. */
1514
1875
  function resolveCodexAppServerReasoningEffort(params) {
1515
1876
  if (params.thinkLevel === "off" || params.thinkLevel === "adaptive") return null;
1516
- const supportedReasoningEfforts = params.supportedReasoningEfforts ?? resolveFallbackReasoningEfforts(params.modelId);
1517
- if (supportedReasoningEfforts) return resolveSupportedReasoningEffort({
1877
+ if (params.supportedReasoningEfforts) return resolveSupportedReasoningEffort({
1518
1878
  requested: params.thinkLevel,
1519
- supportedReasoningEfforts
1879
+ supportedReasoningEfforts: params.supportedReasoningEfforts
1880
+ }) ?? null;
1881
+ const modelId = params.modelId.trim().toLowerCase();
1882
+ if (LEGACY_PRO_MODEL_ID_RE.test(modelId)) return resolveSupportedReasoningEffort({
1883
+ requested: params.thinkLevel,
1884
+ supportedReasoningEfforts: LEGACY_PRO_REASONING_EFFORTS
1520
1885
  }) ?? null;
1521
- const normalizedModelId = params.modelId.trim().toLowerCase();
1522
- if (params.thinkLevel === "minimal") return MODERN_CODEX_MODEL_IDS.has(normalizedModelId) ? "low" : "minimal";
1523
- if (params.thinkLevel === "low" || params.thinkLevel === "medium" || params.thinkLevel === "high" || params.thinkLevel === "xhigh") return params.thinkLevel;
1524
- return params.thinkLevel === "max" && GPT_56_MAX_MODEL_IDS.has(normalizedModelId) ? "max" : null;
1886
+ if (params.thinkLevel === "minimal" && MODERN_GPT_5_MODEL_ID_RE.test(modelId)) return "low";
1887
+ if (params.thinkLevel === "minimal" || params.thinkLevel === "low" || params.thinkLevel === "medium" || params.thinkLevel === "high" || params.thinkLevel === "xhigh") return params.thinkLevel;
1888
+ return null;
1525
1889
  }
1526
1890
  //#endregion
1527
1891
  //#region extensions/codex/src/app-server/thread-model-selection.ts
@@ -1601,33 +1965,46 @@ function resolveReasoningEffort(thinkLevel, modelId, supportedReasoningEfforts)
1601
1965
  //#endregion
1602
1966
  //#region extensions/codex/src/app-server/thread-prompt.ts
1603
1967
  function buildDeveloperInstructions(params, options = {}) {
1968
+ const deferredToolNames = /* @__PURE__ */ new Set();
1969
+ let hasSkillWorkshop = false;
1970
+ let hasSessionsSpawn = false;
1971
+ let hasSessionsYield = false;
1972
+ let hasSeenDirectNamespace = false;
1973
+ let messageToolAvailable = options.dynamicTools ? false : params.disableMessageTool !== true;
1974
+ for (const spec of options.dynamicTools ?? []) {
1975
+ const isDirectNamespace = spec.type === "namespace" && !hasSeenDirectNamespace && spec.name.trim() === "openclaw_direct";
1976
+ if (isDirectNamespace) hasSeenDirectNamespace = true;
1977
+ for (const tool of spec.type === "namespace" ? spec.tools : [spec]) {
1978
+ const name = tool.name.trim();
1979
+ if (tool.deferLoading === true && name) deferredToolNames.add(name);
1980
+ hasSkillWorkshop ||= name === SKILL_WORKSHOP_TOOL_NAME;
1981
+ hasSessionsSpawn ||= name === "sessions_spawn";
1982
+ hasSessionsYield ||= isDirectNamespace && name === "sessions_yield";
1983
+ messageToolAvailable ||= name === "message";
1984
+ }
1985
+ }
1604
1986
  const nativeCommandGuidance = listRegisteredPluginAgentPromptGuidance({
1605
1987
  surface: "codex_app_server",
1606
1988
  includeLegacyGlobalGuidance: false
1607
1989
  }).join("\n");
1990
+ const nativeDelegationAvailable = params.disableTools !== true && params.delegationCapability !== "report_only" && !isMessageOnlyCodexSourceReply(params) && !isSystemAgentOnlyCodexDynamicToolAllowlist(params.toolsAllow) && !shouldDisableCodexToolSearchForModel(params.modelId);
1991
+ const deferredToolDiscoveryGuidance = deferredToolNames.size > 0 || nativeDelegationAvailable ? "Deferred tools may be absent from the direct tool list. Use `tool_search` when directly callable. On code-mode-only models, use `exec` instead: filter `ALL_TOOLS` by name and description, then call the matching entry through `tools`." : void 0;
1608
1992
  return [
1609
1993
  "You are a personal agent running inside OpenClaw. OpenClaw has dynamic tools for OpenClaw-owned messaging, cron, sessions, media, gateway, and nodes.",
1610
- buildDeferredDynamicToolManifest(options.dynamicTools),
1611
- buildSkillWorkshopInstruction(options.dynamicTools),
1612
- "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`.",
1613
- buildVisibleReplyInstruction(params, options.dynamicTools),
1994
+ deferredToolNames.size > 0 ? `Deferred searchable OpenClaw dynamic tools available: ${[...deferredToolNames].toSorted((left, right) => left.localeCompare(right)).join(", ")}.` : void 0,
1995
+ deferredToolDiscoveryGuidance,
1996
+ hasSkillWorkshop ? buildSkillWorkshopPromptSection().join("\n") : void 0,
1997
+ nativeDelegationAvailable ? `Use Codex native \`spawn_agent\` for Codex subagents. \`spawn_agent\` and the other native collaboration tools may be deferred.${hasSessionsSpawn ? " Use OpenClaw `sessions_spawn` only for OpenClaw or ACP delegation, never as a substitute for `spawn_agent`." : ""}` : void 0,
1998
+ hasSessionsYield && nativeDelegationAvailable ? "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,
1999
+ buildVisibleReplyInstruction(params, messageToolAvailable),
2000
+ TRANSCRIPT_CREDENTIAL_SAFETY_PROMPT,
1614
2001
  nativeCommandGuidance,
1615
2002
  params.extraSystemPrompt
1616
2003
  ].filter((section) => typeof section === "string" && section.trim()).join("\n\n");
1617
2004
  }
1618
- function buildDeferredDynamicToolManifest(dynamicTools) {
1619
- const deferredToolNames = [...new Set(flattenCodexDynamicToolFunctions(dynamicTools).filter((tool) => tool.deferLoading === true).map((tool) => tool.name.trim()).filter(Boolean))].toSorted((left, right) => left.localeCompare(right));
1620
- if (deferredToolNames.length === 0) return;
1621
- return `Deferred searchable OpenClaw dynamic tools available: ${deferredToolNames.join(", ")}. Use \`tool_search\` to load exact callable specs before use.`;
1622
- }
1623
- function buildSkillWorkshopInstruction(dynamicTools) {
1624
- if (!flattenCodexDynamicToolFunctions(dynamicTools).some((tool) => tool.name.trim() === SKILL_WORKSHOP_TOOL_NAME)) return;
1625
- return buildSkillWorkshopPromptSection().join("\n");
1626
- }
1627
- function buildVisibleReplyInstruction(params, dynamicTools) {
1628
- const messageToolAvailable = dynamicTools ? flattenCodexDynamicToolFunctions(dynamicTools).some((tool) => tool.name.trim() === "message") : params.disableMessageTool !== true;
1629
- 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.";
1630
- 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.";
2005
+ function buildVisibleReplyInstruction(params, messageToolAvailable) {
2006
+ 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`. Set `final=true`, or omit it, for the completed reply to the current source conversation; OpenClaw stops after confirming delivery. Do not repeat visible message content in your final answer.";
2007
+ 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` for supported non-text actions in the current conversation, such as reacting to its current message. Reserve other `message` actions for explicit out-of-band sends or media/file delivery. Reactions are not delivered automatically.";
1631
2008
  return "For the current source conversation, reply normally in your final assistant message; OpenClaw will deliver it through the active source conversation.";
1632
2009
  }
1633
2010
  //#endregion
@@ -1636,16 +2013,13 @@ const CODEX_NATIVE_WEB_SEARCH_DISABLED_CONFIG = {
1636
2013
  "features.standalone_web_search": false,
1637
2014
  web_search: "disabled"
1638
2015
  };
1639
- function normalizeOptionalString$1(value) {
1640
- return typeof value === "string" ? value.trim() || void 0 : void 0;
1641
- }
1642
2016
  function normalizeUniqueStrings(value) {
1643
2017
  if (!Array.isArray(value)) return;
1644
- const normalized = [...new Set(value.map(normalizeOptionalString$1).filter((entry) => Boolean(entry)))];
2018
+ const normalized = [...new Set(value.map(normalizeOptionalString).filter((entry) => Boolean(entry)))];
1645
2019
  return normalized.length > 0 ? normalized : void 0;
1646
2020
  }
1647
2021
  function hasManagedSearchProvider(config) {
1648
- return normalizeOptionalString$1(config?.tools?.web?.search?.provider) !== void 0;
2022
+ return normalizeOptionalString(config?.tools?.web?.search?.provider) !== void 0;
1649
2023
  }
1650
2024
  function hasNativeDomainRestrictions(config) {
1651
2025
  return normalizeUniqueStrings(config?.tools?.web?.search?.openaiCodex?.allowedDomains) !== void 0;
@@ -1660,10 +2034,10 @@ function buildCodexNativeWebSearchThreadConfig(config) {
1660
2034
  if (allowedDomains) threadConfig["tools.web_search.allowed_domains"] = allowedDomains;
1661
2035
  if (nativeConfig?.contextSize) threadConfig["tools.web_search.context_size"] = nativeConfig.contextSize;
1662
2036
  const location = nativeConfig?.userLocation;
1663
- const country = normalizeOptionalString$1(location?.country);
1664
- const region = normalizeOptionalString$1(location?.region);
1665
- const city = normalizeOptionalString$1(location?.city);
1666
- const timezone = normalizeOptionalString$1(location?.timezone);
2037
+ const country = normalizeOptionalString(location?.country);
2038
+ const region = normalizeOptionalString(location?.region);
2039
+ const city = normalizeOptionalString(location?.city);
2040
+ const timezone = normalizeOptionalString(location?.timezone);
1667
2041
  if (country) threadConfig["tools.web_search.location.country"] = country;
1668
2042
  if (region) threadConfig["tools.web_search.location.region"] = region;
1669
2043
  if (city) threadConfig["tools.web_search.location.city"] = city;
@@ -1710,26 +2084,50 @@ const CODEX_CODE_MODE_DISABLED_THREAD_CONFIG = {
1710
2084
  const CODEX_LIGHTWEIGHT_CONTEXT_THREAD_CONFIG = { project_doc_max_bytes: 0 };
1711
2085
  const CODEX_TOOL_SEARCH_UNSUPPORTED_THREAD_CONFIG = { "features.multi_agent": false };
1712
2086
  const CODEX_DELEGATION_DISABLED_THREAD_CONFIG = {
2087
+ "agents.enabled": false,
1713
2088
  "features.multi_agent": false,
1714
2089
  "features.multi_agent_v2": false
1715
2090
  };
2091
+ const CODEX_RING_ZERO_RESTRICTED_FEATURES = /* @__PURE__ */ new Set([
2092
+ "apps",
2093
+ "artifact",
2094
+ "browser_use",
2095
+ "browser_use_external",
2096
+ "browser_use_full_cdp_access",
2097
+ "chronicle",
2098
+ "code_mode",
2099
+ "code_mode_only",
2100
+ "computer_use",
2101
+ "current_time_reminder",
2102
+ "default_mode_request_user_input",
2103
+ "deferred_executor",
2104
+ "goals",
2105
+ "hooks",
2106
+ "image_generation",
2107
+ "memories",
2108
+ "multi_agent",
2109
+ "multi_agent_v2",
2110
+ "plugins",
2111
+ "request_permissions_tool",
2112
+ "skill_search",
2113
+ "shell_tool",
2114
+ "standalone_web_search",
2115
+ "token_budget",
2116
+ "unified_exec",
2117
+ "view_image",
2118
+ "web_search_cached",
2119
+ "web_search_request",
2120
+ "workspace_dependencies"
2121
+ ]);
1716
2122
  const CODEX_RING_ZERO_THREAD_CONFIG = {
1717
- "features.apps": false,
1718
- "features.current_time_reminder": false,
1719
- "features.deferred_executor": false,
1720
- "features.enable_fanout": false,
1721
- "features.goals": false,
1722
- "features.hooks": false,
1723
- "features.image_generation": false,
1724
- "features.memories": false,
1725
- "features.multi_agent": false,
1726
- "features.multi_agent_v2": false,
1727
- "features.plugins": false,
1728
- "features.standalone_web_search": false,
1729
- "features.token_budget": false,
2123
+ ...CODEX_DELEGATION_DISABLED_THREAD_CONFIG,
2124
+ ...Object.fromEntries([...CODEX_RING_ZERO_RESTRICTED_FEATURES].map((feature) => [`features.${feature}`, false])),
1730
2125
  "orchestrator.mcp.enabled": false,
1731
2126
  "orchestrator.skills.enabled": false,
2127
+ "skills.bundled.enabled": false,
2128
+ "skills.include_instructions": false,
1732
2129
  "tools.experimental_request_user_input.enabled": false,
2130
+ "tools.update_plan.enabled": false,
1733
2131
  hooks: {
1734
2132
  PreToolUse: [],
1735
2133
  PermissionRequest: [],
@@ -1746,22 +2144,13 @@ const CODEX_RING_ZERO_THREAD_CONFIG = {
1746
2144
  notify: [],
1747
2145
  web_search: "disabled"
1748
2146
  };
1749
- const CODEX_RING_ZERO_RESTRICTED_FEATURES = /* @__PURE__ */ new Set([
1750
- "apps",
1751
- "code_mode",
1752
- "code_mode_only",
1753
- "current_time_reminder",
1754
- "deferred_executor",
1755
- "enable_fanout",
1756
- "goals",
1757
- "hooks",
1758
- "image_generation",
1759
- "memories",
1760
- "multi_agent",
1761
- "multi_agent_v2",
1762
- "plugins",
1763
- "standalone_web_search",
1764
- "token_budget"
2147
+ const CODEX_RING_ZERO_RESTRICTED_FEATURE_ALIASES = /* @__PURE__ */ new Map([
2148
+ ["connectors", "apps"],
2149
+ ["imagegenext", "image_generation"],
2150
+ ["collab", "multi_agent"],
2151
+ ["memory_tool", "memories"],
2152
+ ["telepathy", "chronicle"],
2153
+ ["codex_hooks", "hooks"]
1765
2154
  ]);
1766
2155
  const CODEX_RING_ZERO_OVERRIDABLE_LAYER_TYPES = /* @__PURE__ */ new Set([
1767
2156
  "mdm",
@@ -1807,7 +2196,7 @@ function buildThreadStartParams(params, options) {
1807
2196
  webSearchAllowed: options.webSearchAllowed,
1808
2197
  appServer: options.appServer,
1809
2198
  hostSystemAgentActive: options.hostSystemAgentActive,
1810
- ringZeroInheritedMcpServerNames: options.ringZeroInheritedMcpServerNames
2199
+ restrictedToolSurfaceInheritedMcpServerNames: options.restrictedToolSurfaceInheritedMcpServerNames
1811
2200
  }),
1812
2201
  ...resolveCodexThreadEnvironmentSelection(options),
1813
2202
  developerInstructions: options.developerInstructions ?? buildDeveloperInstructions(params, { dynamicTools: options.dynamicTools }),
@@ -1856,7 +2245,7 @@ function buildThreadResumeParams(params, options) {
1856
2245
  webSearchAllowed: options.webSearchAllowed,
1857
2246
  appServer: options.appServer,
1858
2247
  hostSystemAgentActive: options.hostSystemAgentActive,
1859
- ringZeroInheritedMcpServerNames: options.ringZeroInheritedMcpServerNames
2248
+ restrictedToolSurfaceInheritedMcpServerNames: options.restrictedToolSurfaceInheritedMcpServerNames
1860
2249
  }),
1861
2250
  developerInstructions: options.developerInstructions ?? buildDeveloperInstructions(params, { dynamicTools: options.dynamicTools })
1862
2251
  };
@@ -1895,9 +2284,15 @@ function resolveDirectOnlyToolNamespaces(dynamicTools) {
1895
2284
  }
1896
2285
  function buildCodexRuntimeThreadConfigForRun(params, config, options = {}) {
1897
2286
  const ringZeroActive = (options.hostSystemAgentActive ?? isHostScopedAgentToolActive("openclaw")) && isSystemAgentOnlyCodexDynamicToolAllowlist(params.toolsAllow);
2287
+ const messageOnlySourceReply = isMessageOnlyCodexSourceReply(params);
2288
+ const restrictedToolSurface = ringZeroActive || messageOnlySourceReply || params.pluginHarnessToolPolicyRestricted === true;
1898
2289
  const configMcpServers = config?.mcp_servers;
1899
- if (ringZeroActive && configMcpServers !== void 0 && !isJsonObject(configMcpServers)) throw new Error("Codex ring-zero received invalid thread mcp_servers config");
1900
- const ringZeroMcpServerNames = [...options.ringZeroInheritedMcpServerNames ?? [], ...isJsonObject(configMcpServers) ? Object.keys(configMcpServers) : []];
2290
+ if (restrictedToolSurface && configMcpServers !== void 0 && !isJsonObject(configMcpServers)) throw new Error("Codex restricted tool surface received invalid thread mcp_servers config");
2291
+ const restrictedToolSurfaceMcpServerNames = [...options.restrictedToolSurfaceInheritedMcpServerNames ?? [], ...isJsonObject(configMcpServers) ? Object.keys(configMcpServers) : []];
2292
+ const restrictedRunConfig = restrictedToolSurface && isJsonObject(configMcpServers) ? {
2293
+ ...config,
2294
+ mcp_servers: {}
2295
+ } : config;
1901
2296
  const webSearchConfig = resolveCodexWebSearchPlan({
1902
2297
  config: params.config,
1903
2298
  disableTools: params.disableTools,
@@ -1905,8 +2300,8 @@ function buildCodexRuntimeThreadConfigForRun(params, config, options = {}) {
1905
2300
  nativeProviderWebSearchSupport: options.nativeProviderWebSearchSupport,
1906
2301
  webSearchAllowed: options.webSearchAllowed
1907
2302
  }).threadConfig;
1908
- const baseConfig = buildCodexRuntimeThreadConfig(mergeCodexThreadConfigs(config, webSearchConfig), options);
1909
- const runtimeConfig = mergeCodexThreadConfigs(baseConfig, options.appServer?.networkProxy?.configPatch, shouldDisableCodexToolSearchForModel(params.modelId) ? CODEX_TOOL_SEARCH_UNSUPPORTED_THREAD_CONFIG : void 0, params.delegationCapability === "report_only" ? CODEX_DELEGATION_DISABLED_THREAD_CONFIG : void 0, buildCodexRingZeroThreadConfigPatch(params, options.hostSystemAgentActive, ringZeroMcpServerNames)) ?? baseConfig;
2303
+ const baseConfig = buildCodexRuntimeThreadConfig(mergeCodexThreadConfigs(restrictedRunConfig, webSearchConfig), options);
2304
+ const runtimeConfig = mergeCodexThreadConfigs(baseConfig, options.appServer?.networkProxy?.configPatch, shouldDisableCodexToolSearchForModel(params.modelId) ? CODEX_TOOL_SEARCH_UNSUPPORTED_THREAD_CONFIG : void 0, params.delegationCapability === "report_only" ? CODEX_DELEGATION_DISABLED_THREAD_CONFIG : void 0, messageOnlySourceReply || params.pluginHarnessToolPolicyRestricted === true ? buildCodexRestrictedToolThreadConfigPatch(restrictedToolSurfaceMcpServerNames) : buildCodexRingZeroThreadConfigPatch(params, options.hostSystemAgentActive, restrictedToolSurfaceMcpServerNames)) ?? baseConfig;
1910
2305
  if (params.bootstrapContextMode !== "lightweight") return runtimeConfig;
1911
2306
  return mergeCodexThreadConfigs(runtimeConfig, CODEX_LIGHTWEIGHT_CONTEXT_THREAD_CONFIG) ?? {
1912
2307
  ...runtimeConfig,
@@ -1915,6 +2310,9 @@ function buildCodexRuntimeThreadConfigForRun(params, config, options = {}) {
1915
2310
  }
1916
2311
  function buildCodexRingZeroThreadConfigPatch(params, hostSystemAgentActive = isHostScopedAgentToolActive("openclaw"), inheritedMcpServerNames = []) {
1917
2312
  if (!hostSystemAgentActive || !isSystemAgentOnlyCodexDynamicToolAllowlist(params.toolsAllow)) return;
2313
+ return buildCodexRestrictedToolThreadConfigPatch(inheritedMcpServerNames);
2314
+ }
2315
+ function buildCodexRestrictedToolThreadConfigPatch(inheritedMcpServerNames) {
1918
2316
  const mcpServers = Object.fromEntries([...new Set(inheritedMcpServerNames)].toSorted().map((name) => [name, { enabled: false }]));
1919
2317
  return {
1920
2318
  ...CODEX_RING_ZERO_THREAD_CONFIG,
@@ -1930,15 +2328,15 @@ async function readCodexInheritedMcpServerNames(client, cwd, signal) {
1930
2328
  if (!Array.isArray(response.layers)) throw new Error("Codex config/read omitted effective config layers");
1931
2329
  for (const layer of response.layers) {
1932
2330
  if (!isJsonObject(layer) || !isJsonObject(layer.name) || typeof layer.name.type !== "string") throw new Error("Codex config/read returned invalid effective config layers");
1933
- if (layer.name.type === "legacyManagedConfigTomlFromFile" || layer.name.type === "legacyManagedConfigTomlFromMdm") throw new Error(`Codex ring-zero cannot override config layer ${layer.name.type}`);
1934
- if (!CODEX_RING_ZERO_OVERRIDABLE_LAYER_TYPES.has(layer.name.type)) throw new Error(`Codex ring-zero does not recognize config layer ${layer.name.type}`);
2331
+ if (layer.name.type === "legacyManagedConfigTomlFromFile" || layer.name.type === "legacyManagedConfigTomlFromMdm") throw new Error(`Codex restricted tool surface cannot override config layer ${layer.name.type}`);
2332
+ if (!CODEX_RING_ZERO_OVERRIDABLE_LAYER_TYPES.has(layer.name.type)) throw new Error(`Codex restricted tool surface does not recognize config layer ${layer.name.type}`);
1935
2333
  }
1936
2334
  const configuredServers = response.config.mcp_servers;
1937
2335
  if (configuredServers === void 0) return [];
1938
2336
  if (!isJsonObject(configuredServers)) throw new Error("Codex config/read returned invalid mcp_servers");
1939
2337
  return Object.keys(configuredServers).toSorted();
1940
2338
  }
1941
- async function assertCodexRingZeroHasNoManagedHooks(client, signal) {
2339
+ async function assertCodexRestrictedToolSurfaceHasNoManagedHooks(client, signal) {
1942
2340
  const response = await client.request("configRequirements/read", void 0, { signal });
1943
2341
  if (!isJsonObject(response) || !Object.hasOwn(response, "requirements")) throw new Error("Codex configRequirements/read returned an invalid response");
1944
2342
  if (response.requirements === null) return;
@@ -1951,7 +2349,7 @@ async function assertCodexRingZeroHasNoManagedHooks(client, signal) {
1951
2349
  const hooks = response.requirements[key];
1952
2350
  if (hooks === void 0 || hooks === null) continue;
1953
2351
  if (!isJsonObject(hooks)) throw new Error("Codex configRequirements/read returned invalid managed hooks");
1954
- if (hasNonEmptyJsonValue(hooks)) throw new Error("Codex ring-zero cannot override managed hooks");
2352
+ if (hasNonEmptyJsonValue(hooks)) throw new Error("Codex restricted tool surface cannot override managed hooks");
1955
2353
  }
1956
2354
  for (const key of ["featureRequirements", "feature_requirements"]) {
1957
2355
  const requirements = response.requirements[key];
@@ -1959,22 +2357,35 @@ async function assertCodexRingZeroHasNoManagedHooks(client, signal) {
1959
2357
  if (!isJsonObject(requirements)) throw new Error("Codex configRequirements/read returned invalid feature requirements");
1960
2358
  for (const [feature, enabled] of Object.entries(requirements)) {
1961
2359
  if (typeof enabled !== "boolean") throw new Error("Codex configRequirements/read returned invalid feature requirements");
1962
- if (enabled && CODEX_RING_ZERO_RESTRICTED_FEATURES.has(feature)) throw new Error(`Codex ring-zero cannot override required feature ${feature}`);
2360
+ const canonicalFeature = CODEX_RING_ZERO_RESTRICTED_FEATURE_ALIASES.get(feature) ?? feature;
2361
+ if (enabled && CODEX_RING_ZERO_RESTRICTED_FEATURES.has(canonicalFeature)) throw new Error(`Codex restricted tool surface cannot override required feature ${feature}`);
1963
2362
  }
1964
2363
  }
1965
2364
  }
1966
- async function attestCodexRingZeroThreadHasNoMcpServers(client, threadId, signal) {
2365
+ async function attestCodexRestrictedToolSurfaceMcpServersDisabled(client, threadId, threadConfig, signal) {
2366
+ const configuredServers = threadConfig?.mcp_servers;
2367
+ if (configuredServers !== void 0 && !isJsonObject(configuredServers)) throw new Error("Codex restricted-tool-surface thread config has invalid mcp_servers");
2368
+ const expectedDisabledServerNames = /* @__PURE__ */ new Set();
2369
+ for (const [name, serverConfig] of Object.entries(configuredServers ?? {})) {
2370
+ if (!isJsonObject(serverConfig) || serverConfig.enabled !== false) throw new Error(`Codex restricted-tool-surface MCP server ${name} is not disabled`);
2371
+ expectedDisabledServerNames.add(name);
2372
+ }
1967
2373
  const response = await client.request("mcpServerStatus/list", {
1968
2374
  threadId,
1969
- limit: 1,
1970
2375
  detail: "toolsAndAuthOnly"
1971
2376
  }, { signal });
1972
- if (!isJsonObject(response) || !Array.isArray(response.data)) throw new Error("Codex mcpServerStatus/list returned an invalid ring-zero attestation");
1973
- if (response.data.length > 0) {
1974
- const first = response.data[0];
1975
- const serverName = isJsonObject(first) && typeof first.name === "string" ? first.name : "unknown";
1976
- throw new Error(`Codex ring-zero MCP attestation found server ${serverName}`);
2377
+ if (!isJsonObject(response) || !Array.isArray(response.data)) throw new Error("Codex mcpServerStatus/list returned an invalid restricted-tool-surface attestation");
2378
+ const observedDisabledServerNames = /* @__PURE__ */ new Set();
2379
+ for (const status of response.data) {
2380
+ if (!isJsonObject(status) || typeof status.name !== "string" || !isJsonObject(status.tools)) throw new Error("Codex mcpServerStatus/list returned an invalid restricted-tool-surface server");
2381
+ if (!expectedDisabledServerNames.has(status.name)) throw new Error(`Codex restricted-tool-surface MCP attestation found unexpected server ${status.name}`);
2382
+ if (observedDisabledServerNames.has(status.name)) throw new Error(`Codex restricted-tool-surface MCP attestation returned duplicate server ${status.name}`);
2383
+ observedDisabledServerNames.add(status.name);
2384
+ if (!Object.hasOwn(status, "serverInfo")) throw new Error(`Codex restricted-tool-surface MCP attestation returned malformed server ${status.name}`);
2385
+ if (status.serverInfo !== null) throw new Error(`Codex restricted-tool-surface MCP attestation found active server ${status.name}`);
2386
+ if (Object.keys(status.tools).length > 0) throw new Error(`Codex restricted-tool-surface MCP attestation found tools for server ${status.name}`);
1977
2387
  }
2388
+ for (const expectedName of expectedDisabledServerNames) if (!observedDisabledServerNames.has(expectedName)) throw new Error(`Codex restricted-tool-surface MCP attestation is missing server ${expectedName}`);
1978
2389
  if (response.nextCursor !== void 0 && response.nextCursor !== null) throw new Error("Codex mcpServerStatus/list returned an invalid empty-page cursor");
1979
2390
  }
1980
2391
  function hasNonEmptyJsonValue(value) {
@@ -1996,47 +2407,26 @@ function resolveCodexThreadEnvironmentSelection(options) {
1996
2407
  return {};
1997
2408
  }
1998
2409
  //#endregion
1999
- //#region extensions/codex/src/app-server/thread-resume.ts
2000
- /** Owns Codex thread/resume subscription safety. */
2001
- /** Resumes one thread and retires the physical client when acceptance is indeterminate. */
2002
- async function resumeCodexAppServerThread(params) {
2003
- const threadId = params.request.threadId;
2004
- let response;
2005
- try {
2006
- response = assertCodexThreadResumeResponse(await params.client.request("thread/resume", params.request, {
2007
- ...params.timeoutMs !== void 0 ? { timeoutMs: params.timeoutMs } : {},
2008
- ...params.signal ? { signal: params.signal } : {}
2009
- }));
2010
- assertCodexThreadResumeSubscription(threadId, response.thread.id);
2011
- } catch (error) {
2012
- if (isCodexAppServerStartSelectionChangedError(error) || isCodexAppServerPrewriteRequestCancellationError(error)) throw error;
2013
- if (error instanceof CodexAppServerRpcError) throw error;
2014
- try {
2015
- await params.abandonClient();
2016
- } catch (abandonError) {
2017
- throw new CodexAppServerUnsafeSubscriptionError(`Codex thread/resume client could not be retired for ${threadId}`, { cause: abandonError });
2018
- }
2019
- if (error instanceof CodexAppServerUnsafeSubscriptionError) throw error;
2020
- throw new CodexAppServerUnsafeSubscriptionError(error instanceof Error ? error.message : `Codex thread/resume outcome is indeterminate for ${threadId}`, { cause: error });
2021
- }
2022
- return response;
2023
- }
2024
- //#endregion
2025
2410
  //#region extensions/codex/src/app-server/thread-lifecycle-io.ts
2411
+ function resolveCodexThreadRolloutPath(thread) {
2412
+ const rolloutPath = thread.path?.trim();
2413
+ if (!rolloutPath || !path.isAbsolute(rolloutPath) || path.extname(rolloutPath) !== ".jsonl" || !path.basename(rolloutPath).includes(thread.id)) return;
2414
+ return rolloutPath;
2415
+ }
2026
2416
  async function resumeExistingCodexThread(params, context) {
2027
- const { binding: resumeBinding, bindingIdentity, startModelSelection, startModelProvider, userMcpServersConfigPatch, dynamicToolsFingerprint, dynamicToolsContainDeferred, webSearchThreadConfigFingerprint, userMcpServersFingerprint, ringZeroConfigFingerprint, ringZeroClientInstanceId, networkProxyConfigFingerprint, contextEngineBinding, environmentSelectionFingerprint, hostSystemAgentActive, ringZeroActive, ringZeroInheritedMcpServerNames, lifecycleTiming, normalizeBindingModelProvider, throwIfAborted, clearCurrentBinding } = context;
2417
+ const { binding: resumeBinding, bindingIdentity, startModelSelection, startModelProvider, userMcpServersConfigPatch, dynamicToolsFingerprint, dynamicToolsContainDeferred, webSearchThreadConfigFingerprint, nativeSkillIsolationFingerprint, userMcpServersFingerprint, ringZeroConfigFingerprint, ringZeroClientInstanceId, networkProxyConfigFingerprint, contextEngineBinding, environmentSelectionFingerprint, hostSystemAgentActive, ringZeroActive, restrictedToolSurface, restrictedToolSurfaceInheritedMcpServerNames, nativeSkillIsolation, lifecycleTiming, normalizeBindingModelProvider, throwIfAborted, clearCurrentBinding } = context;
2028
2418
  let resumeReservation;
2029
2419
  try {
2030
2420
  const authProfileId = resumeBinding.connectionScope === "supervision" ? void 0 : params.params.authProfileId ?? resumeBinding.authProfileId;
2031
- const finalConfigPatch = params.buildFinalConfigPatch?.({
2421
+ const finalConfigPatch = context.prebuiltFinalConfigPatch ?? params.buildFinalConfigPatch?.({
2032
2422
  action: "resume",
2033
2423
  binding: resumeBinding
2034
2424
  }) ?? {
2035
2425
  configPatch: params.finalConfigPatch,
2036
2426
  nativeHookRelayGeneration: params.nativeHookRelayGeneration
2037
2427
  };
2038
- const pluginAppsConfigPatch = params.pluginThreadConfig?.enabled && resumeBinding.pluginAppPolicyContext ? buildCodexPluginAppsConfigPatchFromPolicyContext(resumeBinding.pluginAppPolicyContext) : void 0;
2039
- const resumeConfig = mergeCodexThreadConfigs(params.config, userMcpServersConfigPatch, pluginAppsConfigPatch, finalConfigPatch.configPatch);
2428
+ const pluginAppsConfigPatch = context.prebuiltPluginThreadConfig?.configPatch ?? (params.pluginThreadConfig?.enabled && resumeBinding.pluginAppPolicyContext ? buildCodexPluginAppsConfigPatchFromPolicyContext(resumeBinding.pluginAppPolicyContext) : void 0);
2429
+ const resumeConfig = applyCodexNativeSkillIsolation(mergeCodexThreadConfigs(params.config, userMcpServersConfigPatch, pluginAppsConfigPatch, finalConfigPatch.configPatch), nativeSkillIsolation);
2040
2430
  const resumeParams = lifecycleTiming.measureSync("thread-resume-params", () => buildThreadResumeParams(params.params, {
2041
2431
  threadId: resumeBinding.threadId,
2042
2432
  authProfileId,
@@ -2052,7 +2442,7 @@ async function resumeExistingCodexThread(params, context) {
2052
2442
  nativeCodeModeOnlyEnabled: params.nativeCodeModeOnlyEnabled,
2053
2443
  webSearchAllowed: params.webSearchAllowed,
2054
2444
  hostSystemAgentActive,
2055
- ringZeroInheritedMcpServerNames
2445
+ restrictedToolSurfaceInheritedMcpServerNames
2056
2446
  }));
2057
2447
  const requestModelProvider = typeof resumeParams.modelProvider === "string" && resumeParams.modelProvider.trim() ? resumeParams.modelProvider : void 0;
2058
2448
  throwIfAborted();
@@ -2071,17 +2461,19 @@ async function resumeExistingCodexThread(params, context) {
2071
2461
  request: resumeParams,
2072
2462
  signal: params.signal
2073
2463
  }));
2074
- if (ringZeroActive) try {
2075
- await lifecycleTiming.measure("ring-zero-mcp-attestation", () => attestCodexRingZeroThreadHasNoMcpServers(params.client, response.thread.id, params.signal));
2464
+ if (ringZeroActive || isMessageOnlyCodexSourceReply(params.params) || params.params.pluginHarnessToolPolicyRestricted === true) try {
2465
+ await lifecycleTiming.measure("restricted-tool-surface-mcp-attestation", () => attestCodexRestrictedToolSurfaceMcpServersDisabled(params.client, response.thread.id, resumeParams.config, params.signal));
2076
2466
  } catch (error) {
2077
2467
  await (params.abandonClient ?? (() => closeCodexStartupClientBestEffort(params.client)))();
2078
- throw new CodexRingZeroAttestationError(error);
2468
+ throw new CodexRestrictedToolSurfaceAttestationError(error);
2079
2469
  }
2080
2470
  throwIfAborted();
2081
2471
  const boundAuthProfileId = authProfileId;
2082
2472
  const nextMcpServersFingerprint = params.mcpServersFingerprintEvaluated === true ? params.mcpServersFingerprint : resumeBinding.mcpServersFingerprint;
2083
2473
  const resumePatch = {
2474
+ clientId: resolveCodexAppServerClientInstanceId(params.client),
2084
2475
  cwd: params.cwd,
2476
+ rolloutPath: resolveCodexThreadRolloutPath(response.thread) ?? resumeBinding.rolloutPath,
2085
2477
  authProfileId: boundAuthProfileId,
2086
2478
  model: response.model ?? resumeParams.model ?? params.params.modelId,
2087
2479
  preserveNativeModel: resumeBinding.preserveNativeModel === true ? true : void 0,
@@ -2089,17 +2481,20 @@ async function resumeExistingCodexThread(params, context) {
2089
2481
  dynamicToolsFingerprint,
2090
2482
  dynamicToolsContainDeferred,
2091
2483
  webSearchThreadConfigFingerprint,
2484
+ nativeSkillIsolationFingerprint,
2092
2485
  userMcpServersFingerprint,
2093
2486
  mcpServersFingerprint: nextMcpServersFingerprint,
2487
+ configuredMcpOwnershipVersion: params.configuredMcpOwnershipVersion,
2094
2488
  ringZeroConfigFingerprint,
2095
2489
  ringZeroClientInstanceId,
2490
+ nativeToolPolicyRestricted: restrictedToolSurface ? true : void 0,
2096
2491
  networkProxyProfileName: params.appServer.networkProxy?.profileName,
2097
2492
  networkProxyConfigFingerprint,
2098
2493
  nativeHookRelayGeneration: finalConfigPatch.nativeHookRelayGeneration ?? resumeBinding.nativeHookRelayGeneration,
2099
2494
  appServerRuntimeFingerprint: resumeBinding.connectionScope === "supervision" ? buildCodexAppServerConnectionFingerprint(params.appServer, params.params.agentDir) : params.appServerRuntimeFingerprint,
2100
- pluginAppsFingerprint: resumeBinding.pluginAppsFingerprint,
2101
- pluginAppsInputFingerprint: resumeBinding.pluginAppsInputFingerprint,
2102
- pluginAppPolicyContext: resumeBinding.pluginAppPolicyContext,
2495
+ pluginAppsFingerprint: context.prebuiltPluginThreadConfig?.fingerprint ?? resumeBinding.pluginAppsFingerprint,
2496
+ pluginAppsInputFingerprint: context.prebuiltPluginThreadConfig?.inputFingerprint ?? resumeBinding.pluginAppsInputFingerprint,
2497
+ pluginAppPolicyContext: context.prebuiltPluginThreadConfig?.policyContext ?? resumeBinding.pluginAppPolicyContext,
2103
2498
  contextEngine: contextEngineBinding,
2104
2499
  environmentSelectionFingerprint
2105
2500
  };
@@ -2130,6 +2525,13 @@ async function resumeExistingCodexThread(params, context) {
2130
2525
  ...resumeBinding,
2131
2526
  threadId: response.thread.id,
2132
2527
  ...resumePatch,
2528
+ liveThreadConfigFingerprint: fingerprintCodexThreadConfig({
2529
+ ...resumeParams,
2530
+ model: resumeBinding.preserveNativeModel === true ? null : response.model ?? resumeParams.model ?? null,
2531
+ requestedModel: resumeBinding.preserveNativeModel === true ? null : resumeParams.model ?? null,
2532
+ modelProvider: resumeBinding.preserveNativeModel === true ? null : resumePatch.modelProvider ?? null,
2533
+ requestedModelProvider: resumeBinding.preserveNativeModel === true ? null : resumeParams.modelProvider ?? resumePatch.modelProvider ?? null
2534
+ }, authProfileId, dynamicToolsFingerprint),
2133
2535
  lifecycle: {
2134
2536
  action: "resumed",
2135
2537
  ...activeTurnIds.length ? { activeTurnIds } : {}
@@ -2138,8 +2540,8 @@ async function resumeExistingCodexThread(params, context) {
2138
2540
  } catch (error) {
2139
2541
  resumeReservation?.release();
2140
2542
  if (isCodexAppServerStartSelectionChangedError(error)) throw error;
2141
- if (error instanceof CodexRingZeroAttestationError) {
2142
- await clearCurrentBinding("retiring a failed ring-zero thread attestation");
2543
+ if (error instanceof CodexRestrictedToolSurfaceAttestationError) {
2544
+ await clearCurrentBinding("retiring a failed restricted-tool-surface attestation");
2143
2545
  throw error;
2144
2546
  }
2145
2547
  if (error instanceof CodexAdoptedThreadActiveError) throw error;
@@ -2156,13 +2558,13 @@ async function resumeExistingCodexThread(params, context) {
2156
2558
  }
2157
2559
  async function startFreshCodexThread(params, context) {
2158
2560
  const clientId = resolveCodexAppServerClientInstanceId(params.client);
2159
- const { bindingIdentity, startModelSelection, startModelProvider, userMcpServersConfigPatch, dynamicToolsFingerprint, dynamicToolsContainDeferred, webSearchThreadConfigFingerprint, userMcpServersFingerprint, ringZeroConfigFingerprint, ringZeroClientInstanceId, networkProxyConfigFingerprint, contextEngineBinding, environmentSelectionFingerprint, hostSystemAgentActive, ringZeroActive, ringZeroInheritedMcpServerNames, lifecycleTiming, normalizeBindingModelProvider, throwIfAborted, prebuiltPluginThreadConfig, preserveExistingBinding, rotatedContextEngineBinding } = context;
2561
+ const { bindingIdentity, startModelSelection, startModelProvider, userMcpServersConfigPatch, dynamicToolsFingerprint, dynamicToolsContainDeferred, webSearchThreadConfigFingerprint, nativeSkillIsolationFingerprint, userMcpServersFingerprint, ringZeroConfigFingerprint, ringZeroClientInstanceId, networkProxyConfigFingerprint, contextEngineBinding, environmentSelectionFingerprint, hostSystemAgentActive, ringZeroActive, restrictedToolSurface, restrictedToolSurfaceInheritedMcpServerNames, nativeSkillIsolation, lifecycleTiming, normalizeBindingModelProvider, throwIfAborted, prebuiltPluginThreadConfig, preserveExistingBinding, rotatedContextEngineBinding, replacementPredecessor } = context;
2160
2562
  const pluginThreadConfig = params.pluginThreadConfig?.enabled ? prebuiltPluginThreadConfig ?? await lifecycleTiming.measure("plugin-config-build", () => params.pluginThreadConfig?.build()) : void 0;
2161
2563
  const finalConfigPatch = params.buildFinalConfigPatch?.({ action: "start" }) ?? {
2162
2564
  configPatch: params.finalConfigPatch,
2163
2565
  nativeHookRelayGeneration: params.nativeHookRelayGeneration
2164
2566
  };
2165
- const config = lifecycleTiming.measureSync("merge-thread-config", () => mergeCodexThreadConfigs(params.config, userMcpServersConfigPatch, pluginThreadConfig?.configPatch, finalConfigPatch.configPatch));
2567
+ const config = lifecycleTiming.measureSync("merge-thread-config", () => applyCodexNativeSkillIsolation(mergeCodexThreadConfigs(params.config, userMcpServersConfigPatch, pluginThreadConfig?.configPatch, finalConfigPatch.configPatch), nativeSkillIsolation));
2166
2568
  const startParams = lifecycleTiming.measureSync("thread-start-params", () => buildThreadStartParams(params.params, {
2167
2569
  cwd: params.cwd,
2168
2570
  dynamicTools: params.dynamicTools,
@@ -2177,7 +2579,7 @@ async function startFreshCodexThread(params, context) {
2177
2579
  model: startModelSelection.model,
2178
2580
  modelProvider: startModelProvider,
2179
2581
  hostSystemAgentActive,
2180
- ringZeroInheritedMcpServerNames
2582
+ restrictedToolSurfaceInheritedMcpServerNames
2181
2583
  }));
2182
2584
  const requestModelProvider = typeof startParams.modelProvider === "string" && startParams.modelProvider.trim() ? startParams.modelProvider : void 0;
2183
2585
  const response = assertCodexThreadStartResponse(await lifecycleTiming.measure("thread-start-request", async () => {
@@ -2188,13 +2590,47 @@ async function startFreshCodexThread(params, context) {
2188
2590
  throw error;
2189
2591
  }
2190
2592
  }));
2191
- if (ringZeroActive) try {
2192
- await lifecycleTiming.measure("ring-zero-mcp-attestation", () => attestCodexRingZeroThreadHasNoMcpServers(params.client, response.thread.id, params.signal));
2593
+ const provisionalAppIds = pluginThreadConfig?.provisionalAppIds;
2594
+ if (provisionalAppIds?.length) try {
2595
+ await lifecycleTiming.measure("plugin-app-attestation", () => attestCodexPluginThreadApps({
2596
+ client: params.client,
2597
+ threadId: response.thread.id,
2598
+ appIds: provisionalAppIds,
2599
+ signal: params.signal
2600
+ }));
2601
+ } catch (error) {
2602
+ if (!await discardUnattestedCodexPluginThread({
2603
+ client: params.client,
2604
+ threadId: response.thread.id,
2605
+ ephemeral: startParams.ephemeral === true
2606
+ })) {
2607
+ await (params.abandonClient ?? (() => closeCodexStartupClientBestEffort(params.client)))();
2608
+ throw new CodexAppServerUnsafeSubscriptionError("Codex plugin app attestation cleanup failed", { cause: error });
2609
+ }
2610
+ throw error;
2611
+ }
2612
+ const rolloutPath = resolveCodexThreadRolloutPath(response.thread);
2613
+ if (ringZeroActive || isMessageOnlyCodexSourceReply(params.params) || params.params.pluginHarnessToolPolicyRestricted === true) try {
2614
+ await lifecycleTiming.measure("restricted-tool-surface-mcp-attestation", () => attestCodexRestrictedToolSurfaceMcpServersDisabled(params.client, response.thread.id, startParams.config, params.signal));
2193
2615
  } catch (error) {
2194
2616
  await (params.abandonClient ?? (() => closeCodexStartupClientBestEffort(params.client)))();
2195
2617
  throw error;
2196
2618
  }
2197
- throwIfAborted();
2619
+ try {
2620
+ throwIfAborted();
2621
+ } catch (error) {
2622
+ if (replacementPredecessor) {
2623
+ if (!await discardUnattestedCodexPluginThread({
2624
+ client: params.client,
2625
+ threadId: response.thread.id,
2626
+ ephemeral: startParams.ephemeral === true
2627
+ })) {
2628
+ await (params.abandonClient ?? (() => closeCodexStartupClientBestEffort(params.client)))();
2629
+ throw new CodexAppServerUnsafeSubscriptionError("Codex successor cleanup failed after an aborted binding replacement", { cause: error });
2630
+ }
2631
+ }
2632
+ throw error;
2633
+ }
2198
2634
  const modelProvider = resolveCodexAppServerModelProvider({
2199
2635
  provider: params.params.provider,
2200
2636
  authProfileId: params.params.authProfileId,
@@ -2202,36 +2638,66 @@ async function startFreshCodexThread(params, context) {
2202
2638
  agentDir: params.params.agentDir,
2203
2639
  config: params.params.config
2204
2640
  });
2641
+ const bindingModelProvider = normalizeBindingModelProvider(params.params.authProfileId, response.modelProvider ?? requestModelProvider ?? startModelProvider ?? modelProvider);
2205
2642
  const nextMcpServersFingerprint = params.mcpServersFingerprintEvaluated === true ? params.mcpServersFingerprint : void 0;
2206
2643
  if (!preserveExistingBinding) {
2207
- if (!await lifecycleTiming.measure("thread-start-write-binding", () => params.bindingStore.mutate(bindingIdentity, {
2208
- kind: "set",
2209
- if: { kind: "absent" },
2210
- binding: {
2644
+ const nextBinding = {
2645
+ threadId: response.thread.id,
2646
+ ...clientId ? { clientId } : {},
2647
+ cwd: params.cwd,
2648
+ ...rolloutPath ? { rolloutPath } : {},
2649
+ authProfileId: params.params.authProfileId,
2650
+ model: response.model ?? startParams.model ?? params.params.modelId,
2651
+ modelProvider: bindingModelProvider,
2652
+ dynamicToolsFingerprint,
2653
+ dynamicToolsContainDeferred,
2654
+ webSearchThreadConfigFingerprint,
2655
+ nativeSkillIsolationFingerprint,
2656
+ userMcpServersFingerprint,
2657
+ mcpServersFingerprint: nextMcpServersFingerprint,
2658
+ configuredMcpOwnershipVersion: params.configuredMcpOwnershipVersion,
2659
+ ringZeroConfigFingerprint,
2660
+ ringZeroClientInstanceId,
2661
+ nativeToolPolicyRestricted: restrictedToolSurface ? true : void 0,
2662
+ networkProxyProfileName: params.appServer.networkProxy?.profileName,
2663
+ networkProxyConfigFingerprint,
2664
+ nativeHookRelayGeneration: finalConfigPatch.nativeHookRelayGeneration,
2665
+ appServerRuntimeFingerprint: params.appServerRuntimeFingerprint,
2666
+ pluginAppsFingerprint: pluginThreadConfig?.fingerprint,
2667
+ pluginAppsInputFingerprint: pluginThreadConfig?.inputFingerprint,
2668
+ pluginAppPolicyContext: pluginThreadConfig?.policyContext,
2669
+ contextEngine: contextEngineBinding,
2670
+ environmentSelectionFingerprint
2671
+ };
2672
+ const cleanupUncommittedSuccessor = async (cause) => {
2673
+ if (!await discardUnattestedCodexPluginThread({
2674
+ client: params.client,
2211
2675
  threadId: response.thread.id,
2212
- ...clientId ? { clientId } : {},
2213
- cwd: params.cwd,
2214
- authProfileId: params.params.authProfileId,
2215
- model: response.model ?? startParams.model ?? params.params.modelId,
2216
- modelProvider: normalizeBindingModelProvider(params.params.authProfileId, response.modelProvider ?? requestModelProvider ?? startModelProvider ?? modelProvider),
2217
- dynamicToolsFingerprint,
2218
- dynamicToolsContainDeferred,
2219
- webSearchThreadConfigFingerprint,
2220
- userMcpServersFingerprint,
2221
- mcpServersFingerprint: nextMcpServersFingerprint,
2222
- ringZeroConfigFingerprint,
2223
- ringZeroClientInstanceId,
2224
- networkProxyProfileName: params.appServer.networkProxy?.profileName,
2225
- networkProxyConfigFingerprint,
2226
- nativeHookRelayGeneration: finalConfigPatch.nativeHookRelayGeneration,
2227
- appServerRuntimeFingerprint: params.appServerRuntimeFingerprint,
2228
- pluginAppsFingerprint: pluginThreadConfig?.fingerprint,
2229
- pluginAppsInputFingerprint: pluginThreadConfig?.inputFingerprint,
2230
- pluginAppPolicyContext: pluginThreadConfig?.policyContext,
2231
- contextEngine: contextEngineBinding,
2232
- environmentSelectionFingerprint
2676
+ ephemeral: startParams.ephemeral === true
2677
+ })) {
2678
+ await (params.abandonClient ?? (() => closeCodexStartupClientBestEffort(params.client)))();
2679
+ throw new CodexAppServerUnsafeSubscriptionError("Codex successor cleanup failed after a binding replacement conflict", cause === void 0 ? void 0 : { cause });
2233
2680
  }
2234
- }))) throw new CodexThreadBindingConflictError(response.thread.id, "committing a fresh thread");
2681
+ };
2682
+ let committed;
2683
+ try {
2684
+ committed = await lifecycleTiming.measure("thread-start-write-binding", () => params.bindingStore.mutate(bindingIdentity, replacementPredecessor ? {
2685
+ kind: "replace-thread",
2686
+ expectedThreadId: replacementPredecessor.threadId,
2687
+ binding: nextBinding
2688
+ } : {
2689
+ kind: "set",
2690
+ if: { kind: "absent" },
2691
+ binding: nextBinding
2692
+ }));
2693
+ } catch (error) {
2694
+ if (replacementPredecessor) await cleanupUncommittedSuccessor(error);
2695
+ throw error;
2696
+ }
2697
+ if (!committed) {
2698
+ if (replacementPredecessor) await cleanupUncommittedSuccessor();
2699
+ throw new CodexThreadBindingConflictError(replacementPredecessor?.threadId ?? response.thread.id, "committing a fresh thread");
2700
+ }
2235
2701
  if (contextEngineBinding) embeddedAgentLog.info("codex app-server wrote context-engine thread binding", {
2236
2702
  sessionId: params.params.sessionId,
2237
2703
  sessionKey: params.params.sessionKey,
@@ -2254,13 +2720,16 @@ async function startFreshCodexThread(params, context) {
2254
2720
  threadId: response.thread.id,
2255
2721
  ...clientId ? { clientId } : {},
2256
2722
  cwd: params.cwd,
2723
+ ...rolloutPath ? { rolloutPath } : {},
2257
2724
  authProfileId: params.params.authProfileId,
2258
2725
  model: response.model ?? startParams.model ?? params.params.modelId,
2259
2726
  modelProvider: response.modelProvider ?? requestModelProvider ?? startModelProvider ?? modelProvider,
2260
2727
  dynamicToolsFingerprint,
2261
2728
  dynamicToolsContainDeferred,
2729
+ nativeSkillIsolationFingerprint,
2262
2730
  userMcpServersFingerprint,
2263
2731
  mcpServersFingerprint: nextMcpServersFingerprint,
2732
+ configuredMcpOwnershipVersion: params.configuredMcpOwnershipVersion,
2264
2733
  ringZeroConfigFingerprint,
2265
2734
  ringZeroClientInstanceId,
2266
2735
  networkProxyProfileName: params.appServer.networkProxy?.profileName,
@@ -2272,6 +2741,13 @@ async function startFreshCodexThread(params, context) {
2272
2741
  pluginAppPolicyContext: pluginThreadConfig?.policyContext,
2273
2742
  contextEngine: contextEngineBinding,
2274
2743
  environmentSelectionFingerprint,
2744
+ ...!preserveExistingBinding ? { liveThreadConfigFingerprint: fingerprintCodexThreadConfig({
2745
+ ...startParams,
2746
+ model: response.model ?? startParams.model ?? null,
2747
+ requestedModel: startParams.model ?? null,
2748
+ modelProvider: bindingModelProvider ?? null,
2749
+ requestedModelProvider: startParams.modelProvider ?? bindingModelProvider ?? null
2750
+ }, params.params.authProfileId, dynamicToolsFingerprint) } : {},
2275
2751
  lifecycle: {
2276
2752
  action: "started",
2277
2753
  ...rotatedContextEngineBinding ? { rotatedContextEngineBinding } : {}
@@ -2279,6 +2755,13 @@ async function startFreshCodexThread(params, context) {
2279
2755
  };
2280
2756
  }
2281
2757
  //#endregion
2758
+ //#region extensions/codex/src/app-server/profiler-flag.ts
2759
+ const PROFILER_FLAGS = ["profiler", "codex.profiler"];
2760
+ /** Checks the generic and Codex-specific profiler diagnostic flags. */
2761
+ function isCodexAppServerProfilerEnabled(config, env = process.env) {
2762
+ return PROFILER_FLAGS.some((flag) => isDiagnosticFlagEnabled(flag, config, env));
2763
+ }
2764
+ //#endregion
2282
2765
  //#region extensions/codex/src/app-server/thread-lifecycle-timing.ts
2283
2766
  const CODEX_THREAD_LIFECYCLE_TIMING_WARN_TOTAL_MS = 1e3;
2284
2767
  const CODEX_THREAD_LIFECYCLE_TIMING_WARN_STAGE_MS = 500;
@@ -2368,6 +2851,193 @@ function createCodexThreadLifecycleTimingTracker(options = {}) {
2368
2851
  };
2369
2852
  }
2370
2853
  //#endregion
2854
+ //#region extensions/codex/src/app-server/thread-lifecycle-preflight.ts
2855
+ async function prepareCodexThreadLifecyclePreflight(params) {
2856
+ const lifecycleTiming = createCodexThreadLifecycleTimingTracker({
2857
+ ...params.timing,
2858
+ enabled: params.timing?.enabled ?? isCodexAppServerProfilerEnabled(params.params.config)
2859
+ });
2860
+ const legacyDynamicToolsFingerprint = lifecycleTiming.measureSync("legacy-dynamic-tools-fingerprint", () => codexLegacyDynamicToolsFingerprint(params.dynamicTools));
2861
+ const dynamicToolsFingerprint = lifecycleTiming.measureSync("dynamic-tools-fingerprint", () => hashCodexAppServerBindingFingerprint(legacyDynamicToolsFingerprint));
2862
+ const dynamicToolsContainDeferred = flattenCodexDynamicToolFunctions(params.dynamicTools).some((tool) => tool.deferLoading === true);
2863
+ const webSearchThreadConfigFingerprint = fingerprintJsonObject(lifecycleTiming.measureSync("web-search-plan", () => resolveCodexWebSearchPlan({
2864
+ config: params.params.config,
2865
+ disableTools: params.params.disableTools,
2866
+ nativeToolSurfaceEnabled: params.nativeCodeModeEnabled,
2867
+ nativeProviderWebSearchSupport: params.nativeProviderWebSearchSupport,
2868
+ webSearchAllowed: params.webSearchAllowed
2869
+ })).threadConfig);
2870
+ const networkProxyConfigFingerprint = params.appServer.networkProxy?.configFingerprint;
2871
+ const contextEngineBinding = lifecycleTiming.measureSync("context-engine-binding", () => buildContextEngineBinding(params.params, params.contextEngineProjection));
2872
+ const userMcpServersConfigPatch = params.userMcpServersEnabled === false ? void 0 : await buildCodexUserMcpServersThreadConfigPatchForRuntime(params.params.config, {
2873
+ agentId: params.agentId ?? params.params.agentId,
2874
+ agentDir: params.params.agentDir,
2875
+ allowLiteralOAuthProjection: params.appServer.connectionClass !== "remote",
2876
+ toolOverrides: params.params.toolOverrides,
2877
+ onServerUnavailable: (serverName, error) => embeddedAgentLog.warn("skipping unavailable MCP OAuth server", {
2878
+ serverName,
2879
+ error: formatErrorMessage(error)
2880
+ })
2881
+ });
2882
+ const nativeSkillIsolation = await lifecycleTiming.measure("native-skill-isolation", () => resolveCodexNativeSkillIsolation({
2883
+ client: params.client,
2884
+ codexHome: params.appServer.start.env?.CODEX_HOME,
2885
+ cwd: params.cwd,
2886
+ home: params.appServer.start.env?.HOME,
2887
+ signal: params.signal,
2888
+ userProfile: params.appServer.start.env?.USERPROFILE
2889
+ }));
2890
+ const nativeSkillIsolationFingerprint = nativeSkillIsolation ? fingerprintJsonObject({
2891
+ version: 1,
2892
+ disabledUserSkillPaths: nativeSkillIsolation.disabledUserSkillPaths
2893
+ }) : void 0;
2894
+ const legacyUserMcpServersFingerprint = legacyFingerprintUserMcpServersConfigPatch(userMcpServersConfigPatch);
2895
+ const userMcpServersFingerprint = fingerprintUserMcpServersConfigPatch(userMcpServersConfigPatch);
2896
+ const environmentSelectionFingerprint = fingerprintEnvironmentSelection(params.environmentSelection);
2897
+ const hostSystemAgentActive = params.hostSystemAgentActive ?? isHostScopedAgentToolActive("openclaw");
2898
+ const ringZeroActive = hostSystemAgentActive && isSystemAgentOnlyCodexDynamicToolAllowlist(params.params.toolsAllow);
2899
+ const messageOnlySourceReply = isMessageOnlyCodexSourceReply(params.params);
2900
+ const restrictedToolSurface = ringZeroActive || messageOnlySourceReply || params.params.pluginHarnessToolPolicyRestricted === true;
2901
+ if (restrictedToolSurface && params.nativeCodeModeEnabled !== false) throw new Error("Codex restricted tool surfaces require native code mode to be disabled");
2902
+ const restrictedToolSurfaceInheritedMcpServerNames = restrictedToolSurface ? await lifecycleTiming.measure("restricted-tool-surface-mcp-config-read", () => readCodexInheritedMcpServerNames(params.client, params.cwd, params.signal)) : [];
2903
+ if (restrictedToolSurface) await lifecycleTiming.measure("restricted-tool-surface-config-requirements-read", () => assertCodexRestrictedToolSurfaceHasNoManagedHooks(params.client, params.signal));
2904
+ const ringZeroConfigFingerprint = ringZeroActive ? fingerprintJsonObject({
2905
+ version: 1,
2906
+ baseInstructions: "",
2907
+ config: buildCodexRingZeroThreadConfigPatch(params.params, true, restrictedToolSurfaceInheritedMcpServerNames)
2908
+ }) : void 0;
2909
+ return {
2910
+ contextEngineBinding,
2911
+ dynamicToolsContainDeferred,
2912
+ dynamicToolsFingerprint,
2913
+ environmentSelectionFingerprint,
2914
+ hostSystemAgentActive,
2915
+ legacyDynamicToolsFingerprint,
2916
+ legacyUserMcpServersFingerprint,
2917
+ lifecycleTiming,
2918
+ nativeSkillIsolation,
2919
+ nativeSkillIsolationFingerprint,
2920
+ networkProxyConfigFingerprint,
2921
+ ringZeroActive,
2922
+ ringZeroClientInstanceId: ringZeroActive ? getCodexAppServerClientInstanceId(params.client) : void 0,
2923
+ ringZeroConfigFingerprint,
2924
+ restrictedToolSurface,
2925
+ restrictedToolSurfaceInheritedMcpServerNames,
2926
+ userMcpServersConfigPatch,
2927
+ userMcpServersFingerprint,
2928
+ webSearchThreadConfigFingerprint
2929
+ };
2930
+ }
2931
+ //#endregion
2932
+ //#region extensions/codex/src/app-server/thread-lifecycle-warm.ts
2933
+ /** Preserves the caller's abort reason across thread ownership transitions. */
2934
+ function throwIfCodexThreadLifecycleAborted(signal) {
2935
+ if (!signal?.aborted) return;
2936
+ const reason = signal.reason;
2937
+ if (reason instanceof Error) throw reason;
2938
+ const error = new Error(typeof reason === "string" && reason.length > 0 ? reason : "codex app-server thread lifecycle aborted");
2939
+ error.name = "AbortError";
2940
+ throw error;
2941
+ }
2942
+ /** Releases consumed subscription ownership or retires an unsafe client. */
2943
+ async function releaseCodexConsumedLiveThread(options) {
2944
+ if (await options.lifecycleTiming.measure("retained-thread-unsubscribe", () => unsubscribeCodexThreadBestEffort(options.client, {
2945
+ threadId: options.threadId,
2946
+ timeoutMs: 5e3
2947
+ }))) return;
2948
+ await (options.abandonClient ?? (() => closeCodexStartupClientBestEffort(options.client)))();
2949
+ throw new CodexAppServerUnsafeSubscriptionError(`Codex retained thread subscription could not be released: ${options.threadId}`, options.cause !== void 0 ? { cause: options.cause } : void 0);
2950
+ }
2951
+ /** Transfers retained-slot ownership before unconditionally releasing it. */
2952
+ async function releaseCodexRetainedLiveThread(options) {
2953
+ if (await consumeCodexAppServerLiveThread(options.client, options.threadId)) await releaseCodexConsumedLiveThread(options);
2954
+ }
2955
+ /** Reuses one safely owned, fully matching subscription on its original client. */
2956
+ async function tryReuseCodexLiveThread(options) {
2957
+ const { params, binding, bindingIdentity, clientId, dynamicToolsFingerprint, environmentSelectionFingerprint, hostSystemAgentActive, lifecycleTiming, nativeSkillIsolation, releaseConsumedThread, ringZeroActive, restrictedToolSurfaceInheritedMcpServerNames, startModelProvider, startModelSelection, throwIfAborted, userMcpServersConfigPatch } = options;
2958
+ if (!binding.clientId || binding.clientId !== clientId || binding.preserveNativeModel === true || binding.connectionScope === "supervision" || ringZeroActive) return {};
2959
+ const prebuiltFinalConfigPatch = params.buildFinalConfigPatch?.({
2960
+ action: "resume",
2961
+ binding
2962
+ }) ?? {
2963
+ configPatch: params.finalConfigPatch,
2964
+ nativeHookRelayGeneration: params.nativeHookRelayGeneration
2965
+ };
2966
+ const pluginAppsConfigPatch = params.pluginThreadConfig?.enabled && binding.pluginAppPolicyContext ? buildCodexPluginAppsConfigPatchFromPolicyContext(binding.pluginAppPolicyContext) : void 0;
2967
+ const resumeAuthProfileId = params.params.authProfileId ?? binding.authProfileId;
2968
+ const resumeConfig = mergeCodexThreadConfigs(params.config, userMcpServersConfigPatch, pluginAppsConfigPatch, prebuiltFinalConfigPatch.configPatch);
2969
+ const resumeParams = lifecycleTiming.measureSync("warm-thread-resume-params", () => buildThreadResumeParams(params.params, {
2970
+ threadId: binding.threadId,
2971
+ authProfileId: resumeAuthProfileId,
2972
+ model: startModelSelection.model,
2973
+ modelProvider: startModelProvider,
2974
+ preserveNativeModel: false,
2975
+ appServer: params.appServer,
2976
+ dynamicTools: params.dynamicTools,
2977
+ developerInstructions: params.developerInstructions,
2978
+ config: applyCodexNativeSkillIsolation(resumeConfig, nativeSkillIsolation),
2979
+ nativeCodeModeEnabled: params.nativeCodeModeEnabled,
2980
+ nativeProviderWebSearchSupport: params.nativeProviderWebSearchSupport,
2981
+ nativeCodeModeOnlyEnabled: params.nativeCodeModeOnlyEnabled,
2982
+ webSearchAllowed: params.webSearchAllowed,
2983
+ hostSystemAgentActive,
2984
+ restrictedToolSurfaceInheritedMcpServerNames
2985
+ }));
2986
+ const liveThreadConfigFingerprint = fingerprintCodexThreadConfig({
2987
+ ...resumeParams,
2988
+ model: binding.model ?? resumeParams.model ?? null,
2989
+ requestedModel: resumeParams.model ?? null,
2990
+ modelProvider: binding.modelProvider ?? resumeParams.modelProvider ?? null,
2991
+ requestedModelProvider: resumeParams.modelProvider ?? binding.modelProvider ?? null
2992
+ }, resumeAuthProfileId, dynamicToolsFingerprint);
2993
+ const retainedThread = await consumeCodexAppServerLiveThread(params.client, binding.threadId, liveThreadConfigFingerprint);
2994
+ if (!retainedThread) {
2995
+ const incompatibleOwnership = await consumeCodexAppServerLiveThread(params.client, binding.threadId);
2996
+ if (incompatibleOwnership) await incompatibleOwnership.release(binding.threadId);
2997
+ return { prebuiltFinalConfigPatch };
2998
+ }
2999
+ try {
3000
+ const nativeHookRelayGeneration = prebuiltFinalConfigPatch.nativeHookRelayGeneration ?? binding.nativeHookRelayGeneration;
3001
+ const model = startModelSelection.model;
3002
+ if (!await lifecycleTiming.measure("warm-thread-write-binding", () => params.bindingStore.mutate(bindingIdentity, {
3003
+ kind: "patch",
3004
+ threadId: binding.threadId,
3005
+ patch: {
3006
+ cwd: params.cwd,
3007
+ model,
3008
+ nativeHookRelayGeneration,
3009
+ environmentSelectionFingerprint
3010
+ }
3011
+ }))) throw new CodexThreadBindingConflictError(binding.threadId, "committing a reused thread");
3012
+ throwIfAborted();
3013
+ lifecycleTiming.mark("thread-ready");
3014
+ lifecycleTiming.logSummary({
3015
+ runId: params.params.runId,
3016
+ sessionId: params.params.sessionId,
3017
+ sessionKey: params.params.sessionKey,
3018
+ threadId: binding.threadId,
3019
+ action: "resumed"
3020
+ });
3021
+ return {
3022
+ binding: {
3023
+ ...binding,
3024
+ cwd: params.cwd,
3025
+ model,
3026
+ nativeHookRelayGeneration,
3027
+ environmentSelectionFingerprint,
3028
+ liveThreadConfigFingerprint,
3029
+ liveThreadOwnership: retainedThread,
3030
+ ...retainedThread.serviceTier && resumeParams.serviceTier === void 0 ? { clearInheritedServiceTier: true } : {},
3031
+ lifecycle: { action: "resumed" }
3032
+ },
3033
+ prebuiltFinalConfigPatch
3034
+ };
3035
+ } catch (error) {
3036
+ await releaseConsumedThread(binding.threadId, error);
3037
+ throw error;
3038
+ }
3039
+ }
3040
+ //#endregion
2371
3041
  //#region extensions/codex/src/app-server/thread-supervision.ts
2372
3042
  async function materializePendingSupervisionBranch(params) {
2373
3043
  let pending = params.binding.pendingSupervisionBranch;
@@ -2409,6 +3079,7 @@ async function materializePendingSupervisionBranch(params) {
2409
3079
  pending = await trackPendingSupervisionArtifacts(params, pending, [probeThreadId]);
2410
3080
  params.throwIfAborted();
2411
3081
  const probeResponse = assertCodexThreadForkResponse(rawProbeResponse);
3082
+ if (params.restrictedToolSurface) await params.lifecycleTiming.measure("restricted-tool-surface-mcp-attestation", () => attestCodexRestrictedToolSurfaceMcpServersDisabled(params.client, probeThreadId, probeParams.config ?? void 0, params.signal));
2412
3083
  const nativeModel = requireNonBlankSupervisionValue(probeResponse.model, "native model");
2413
3084
  const nativeModelProvider = requireNativeSupervisionModelProvider({
2414
3085
  responseModelProvider: probeResponse.modelProvider,
@@ -2429,7 +3100,9 @@ async function materializePendingSupervisionBranch(params) {
2429
3100
  webSearchAllowed: params.webSearchAllowed,
2430
3101
  environmentSelection: params.environmentSelection,
2431
3102
  model: nativeModel,
2432
- modelProvider: nativeModelProvider
3103
+ modelProvider: nativeModelProvider,
3104
+ hostSystemAgentActive: params.hostSystemAgentActive,
3105
+ restrictedToolSurfaceInheritedMcpServerNames: params.restrictedToolSurfaceInheritedMcpServerNames
2433
3106
  });
2434
3107
  assertExactSupervisionModelSelection(startParams, {
2435
3108
  model: nativeModel,
@@ -2457,6 +3130,26 @@ async function materializePendingSupervisionBranch(params) {
2457
3130
  modelProvider: nativeModelProvider,
2458
3131
  operation: "thread/start response"
2459
3132
  });
3133
+ if (params.restrictedToolSurface) await params.lifecycleTiming.measure("restricted-tool-surface-mcp-attestation", () => attestCodexRestrictedToolSurfaceMcpServersDisabled(params.client, finalThreadId, startParams.config, params.signal));
3134
+ if (params.provisionalAppIds?.length) try {
3135
+ await params.lifecycleTiming.measure("plugin-app-attestation", () => attestCodexPluginThreadApps({
3136
+ client: params.client,
3137
+ threadId: finalThreadId,
3138
+ appIds: params.provisionalAppIds ?? [],
3139
+ signal: params.signal
3140
+ }));
3141
+ } catch (error) {
3142
+ if (!await discardUnattestedCodexPluginThread({
3143
+ client: params.client,
3144
+ threadId: finalThreadId,
3145
+ ephemeral: startParams.ephemeral === true
3146
+ }) || !await archiveSupervisionArtifact(params.client, probeThreadId)) {
3147
+ provisionalCleanupSafe = false;
3148
+ throw new CodexAppServerUnsafeSubscriptionError("Codex supervised plugin app attestation cleanup failed", { cause: error });
3149
+ }
3150
+ pending = await trackPendingSupervisionArtifacts(params, pending, []);
3151
+ throw error;
3152
+ }
2460
3153
  if (history.responseItems.length > 0) {
2461
3154
  await params.lifecycleTiming.measure("supervision-history-inject", () => params.client.request("thread/inject_items", {
2462
3155
  threadId: finalThreadId,
@@ -2563,7 +3256,9 @@ function buildPendingSupervisionProbeForkParams(params, pending) {
2563
3256
  nativeProviderWebSearchSupport: params.nativeProviderWebSearchSupport,
2564
3257
  nativeCodeModeOnlyEnabled: params.nativeCodeModeOnlyEnabled,
2565
3258
  webSearchAllowed: params.webSearchAllowed,
2566
- appServer: params.appServer
3259
+ appServer: params.appServer,
3260
+ hostSystemAgentActive: params.hostSystemAgentActive,
3261
+ restrictedToolSurfaceInheritedMcpServerNames: params.restrictedToolSurfaceInheritedMcpServerNames
2567
3262
  });
2568
3263
  return {
2569
3264
  threadId: pending.sourceThreadId,
@@ -2700,46 +3395,11 @@ async function startOrResumeThread(params) {
2700
3395
  config: params.params.config
2701
3396
  });
2702
3397
  return await params.bindingStore.withLease(bindingIdentity, async () => {
2703
- const lifecycleTiming = createCodexThreadLifecycleTimingTracker({
2704
- ...params.timing,
2705
- enabled: params.timing?.enabled ?? isCodexAppServerProfilerEnabled(params.params.config)
2706
- });
2707
- const legacyDynamicToolsFingerprint = lifecycleTiming.measureSync("legacy-dynamic-tools-fingerprint", () => codexLegacyDynamicToolsFingerprint(params.dynamicTools));
2708
- const dynamicToolsFingerprint = lifecycleTiming.measureSync("dynamic-tools-fingerprint", () => hashCodexAppServerBindingFingerprint(legacyDynamicToolsFingerprint));
2709
- const dynamicToolsContainDeferred = flattenCodexDynamicToolFunctions(params.dynamicTools).some((tool) => tool.deferLoading === true);
2710
- const webSearchThreadConfigFingerprint = fingerprintJsonObject(lifecycleTiming.measureSync("web-search-plan", () => resolveCodexWebSearchPlan({
2711
- config: params.params.config,
2712
- disableTools: params.params.disableTools,
2713
- nativeToolSurfaceEnabled: params.nativeCodeModeEnabled,
2714
- nativeProviderWebSearchSupport: params.nativeProviderWebSearchSupport,
2715
- webSearchAllowed: params.webSearchAllowed
2716
- })).threadConfig);
2717
- const networkProxyConfigFingerprint = params.appServer.networkProxy?.configFingerprint;
2718
- const contextEngineBinding = lifecycleTiming.measureSync("context-engine-binding", () => buildContextEngineBinding(params.params, params.contextEngineProjection));
2719
- const userMcpServersConfigPatch = params.userMcpServersEnabled === false ? void 0 : await buildCodexUserMcpServersThreadConfigPatchForRuntime(params.params.config, {
2720
- agentId: params.agentId ?? params.params.agentId,
2721
- agentDir: params.params.agentDir,
2722
- allowLiteralOAuthProjection: params.appServer.connectionClass !== "remote",
2723
- onServerUnavailable: (serverName, error) => embeddedAgentLog.warn("skipping unavailable MCP OAuth server", {
2724
- serverName,
2725
- error: formatErrorMessage(error)
2726
- })
2727
- });
2728
- const legacyUserMcpServersFingerprint = legacyFingerprintUserMcpServersConfigPatch(userMcpServersConfigPatch);
2729
- const userMcpServersFingerprint = fingerprintUserMcpServersConfigPatch(userMcpServersConfigPatch);
2730
- const environmentSelectionFingerprint = fingerprintEnvironmentSelection(params.environmentSelection);
2731
- const hostSystemAgentActive = params.hostSystemAgentActive ?? isHostScopedAgentToolActive("openclaw");
2732
- const ringZeroActive = hostSystemAgentActive && isSystemAgentOnlyCodexDynamicToolAllowlist(params.params.toolsAllow);
2733
- if (ringZeroActive && params.nativeCodeModeEnabled !== false) throw new Error("Codex ring-zero requires native code mode to be disabled");
2734
- const ringZeroInheritedMcpServerNames = ringZeroActive ? await lifecycleTiming.measure("ring-zero-mcp-config-read", () => readCodexInheritedMcpServerNames(params.client, params.cwd, params.signal)) : [];
2735
- if (ringZeroActive) await lifecycleTiming.measure("ring-zero-config-requirements-read", () => assertCodexRingZeroHasNoManagedHooks(params.client, params.signal));
2736
- const ringZeroConfigFingerprint = ringZeroActive ? fingerprintJsonObject({
2737
- version: 1,
2738
- baseInstructions: "",
2739
- config: buildCodexRingZeroThreadConfigPatch(params.params, true, ringZeroInheritedMcpServerNames)
2740
- }) : void 0;
2741
- const ringZeroClientInstanceId = ringZeroActive ? getCodexAppServerClientInstanceId(params.client) : void 0;
3398
+ const { contextEngineBinding, dynamicToolsContainDeferred, dynamicToolsFingerprint, environmentSelectionFingerprint, hostSystemAgentActive, legacyDynamicToolsFingerprint, legacyUserMcpServersFingerprint, lifecycleTiming, nativeSkillIsolation, nativeSkillIsolationFingerprint, networkProxyConfigFingerprint, ringZeroActive, ringZeroClientInstanceId, ringZeroConfigFingerprint, restrictedToolSurface, restrictedToolSurfaceInheritedMcpServerNames, userMcpServersConfigPatch, userMcpServersFingerprint, webSearchThreadConfigFingerprint } = await prepareCodexThreadLifecyclePreflight(params);
2742
3399
  let binding = await lifecycleTiming.measure("read-binding", () => params.bindingStore.read(bindingIdentity));
3400
+ let replacementPredecessor;
3401
+ const initialBoundThreadId = binding?.threadId;
3402
+ const initialBoundClientId = binding?.clientId;
2743
3403
  const normalizeBindingModelProvider = (authProfileId, modelProvider) => normalizeCodexAppServerBindingModelProvider({
2744
3404
  authProfileId,
2745
3405
  modelProvider,
@@ -2747,13 +3407,28 @@ async function startOrResumeThread(params) {
2747
3407
  agentDir: params.params.agentDir,
2748
3408
  config: params.params.config
2749
3409
  });
2750
- const throwIfAborted = () => {
2751
- if (!params.signal?.aborted) return;
2752
- const reason = params.signal.reason;
2753
- if (reason instanceof Error) throw reason;
2754
- const error = new Error(typeof reason === "string" && reason.length > 0 ? reason : "codex app-server thread lifecycle aborted");
2755
- error.name = "AbortError";
2756
- throw error;
3410
+ const throwIfAborted = () => throwIfCodexThreadLifecycleAborted(params.signal);
3411
+ const releaseRetainedThread = async (threadId, ownerClientId = initialBoundClientId) => {
3412
+ if (ownerClientId && ownerClientId !== clientId) {
3413
+ const previousClient = retainSharedCodexAppServerClientByInstanceId(ownerClientId);
3414
+ if (!previousClient) return;
3415
+ try {
3416
+ await releaseCodexRetainedLiveThread({
3417
+ client: previousClient.client,
3418
+ lifecycleTiming,
3419
+ threadId
3420
+ });
3421
+ } finally {
3422
+ previousClient.release();
3423
+ }
3424
+ return;
3425
+ }
3426
+ await releaseCodexRetainedLiveThread({
3427
+ client: params.client,
3428
+ abandonClient: params.abandonClient,
3429
+ lifecycleTiming,
3430
+ threadId
3431
+ });
2757
3432
  };
2758
3433
  if (!binding && bindingIdentity.kind === "session" && bindingIdentity.sessionKey) {
2759
3434
  if (!await lifecycleTiming.measure("reclaim-binding-generation", () => reclaimCurrentCodexSessionGeneration({
@@ -2763,13 +3438,14 @@ async function startOrResumeThread(params) {
2763
3438
  }))) throw createCodexSessionGenerationSupersededError(bindingIdentity.sessionId);
2764
3439
  }
2765
3440
  if (binding?.pendingSupervisionBranch) {
3441
+ await releaseRetainedThread(binding.threadId);
2766
3442
  const pendingBinding = binding;
2767
3443
  const pluginThreadConfig = params.pluginThreadConfig?.enabled ? await lifecycleTiming.measure("plugin-config-build", () => params.pluginThreadConfig?.build()) : void 0;
2768
3444
  const finalConfigPatch = params.buildFinalConfigPatch?.({ action: "start" }) ?? {
2769
3445
  configPatch: params.finalConfigPatch,
2770
3446
  nativeHookRelayGeneration: params.nativeHookRelayGeneration
2771
3447
  };
2772
- const config = lifecycleTiming.measureSync("merge-thread-config", () => mergeCodexThreadConfigs(params.config, userMcpServersConfigPatch, pluginThreadConfig?.configPatch, finalConfigPatch.configPatch));
3448
+ const config = lifecycleTiming.measureSync("merge-thread-config", () => applyCodexNativeSkillIsolation(mergeCodexThreadConfigs(params.config, userMcpServersConfigPatch, pluginThreadConfig?.configPatch, finalConfigPatch.configPatch), nativeSkillIsolation));
2773
3449
  return await materializePendingSupervisionBranch({
2774
3450
  client: params.client,
2775
3451
  abandonClient: params.abandonClient ?? (() => closeCodexStartupClientBestEffort(params.client)),
@@ -2786,7 +3462,11 @@ async function startOrResumeThread(params) {
2786
3462
  nativeProviderWebSearchSupport: params.nativeProviderWebSearchSupport,
2787
3463
  nativeCodeModeOnlyEnabled: params.nativeCodeModeOnlyEnabled,
2788
3464
  webSearchAllowed: params.webSearchAllowed,
3465
+ hostSystemAgentActive,
3466
+ restrictedToolSurface,
3467
+ restrictedToolSurfaceInheritedMcpServerNames,
2789
3468
  environmentSelection: params.environmentSelection,
3469
+ provisionalAppIds: pluginThreadConfig?.provisionalAppIds,
2790
3470
  signal: params.signal,
2791
3471
  throwIfAborted,
2792
3472
  lifecycleTiming,
@@ -2799,8 +3479,10 @@ async function startOrResumeThread(params) {
2799
3479
  dynamicToolsFingerprint,
2800
3480
  dynamicToolsContainDeferred,
2801
3481
  webSearchThreadConfigFingerprint,
3482
+ nativeSkillIsolationFingerprint,
2802
3483
  userMcpServersFingerprint,
2803
3484
  mcpServersFingerprint: params.mcpServersFingerprintEvaluated === true ? params.mcpServersFingerprint : pendingBinding.mcpServersFingerprint,
3485
+ configuredMcpOwnershipVersion: params.configuredMcpOwnershipVersion,
2804
3486
  networkProxyProfileName: params.appServer.networkProxy?.profileName,
2805
3487
  networkProxyConfigFingerprint,
2806
3488
  nativeHookRelayGeneration: finalConfigPatch.nativeHookRelayGeneration,
@@ -2824,6 +3506,11 @@ async function startOrResumeThread(params) {
2824
3506
  })) throw new CodexThreadBindingConflictError(current.threadId, operation);
2825
3507
  binding = void 0;
2826
3508
  };
3509
+ if (binding?.threadId && !restrictedToolSurface && binding.nativeToolPolicyRestricted === true) await clearCurrentBinding("rotating a host-policy-restricted thread binding");
3510
+ if (binding?.threadId && binding.nativeSkillIsolationFingerprint !== nativeSkillIsolationFingerprint) {
3511
+ embeddedAgentLog.debug("codex app-server native skill isolation changed; starting a new thread", { threadId: binding.threadId });
3512
+ await clearCurrentBinding("rotating stale native skill isolation");
3513
+ }
2827
3514
  if (binding?.threadId && (binding.ringZeroConfigFingerprint !== ringZeroConfigFingerprint || binding.ringZeroClientInstanceId !== ringZeroClientInstanceId) && (ringZeroActive || binding.ringZeroConfigFingerprint !== void 0)) {
2828
3515
  embeddedAgentLog.debug("codex app-server ring-zero restriction changed; rotating thread", { threadId: binding.threadId });
2829
3516
  await clearCurrentBinding("rotating a ring-zero thread binding");
@@ -2872,6 +3559,14 @@ async function startOrResumeThread(params) {
2872
3559
  const transientWebSearchRestriction = isTransientWebSearchRestriction(params);
2873
3560
  const explicitTransientWebSearchRestriction = params.webSearchAllowed === false && params.persistentWebSearchAllowed !== false && transientWebSearchRestriction;
2874
3561
  const unknownProviderWebSearchSupport = params.nativeProviderWebSearchSupport === "unknown";
3562
+ if (binding?.threadId && (params.configuredMcpOwnershipVersion === 1 && (binding.configuredMcpOwnershipVersion !== 1 || binding.dynamicToolsFingerprint === void 0 || binding.mcpServersFingerprint !== void 0 || binding.userMcpServersFingerprint !== void 0) || params.configuredMcpOwnershipVersion !== 1 && binding.configuredMcpOwnershipVersion === 1) && binding?.threadId) {
3563
+ const predecessorBinding = binding;
3564
+ assertCodexBindingMayBeReplaced(predecessorBinding, "changing configured MCP ownership");
3565
+ embeddedAgentLog.debug("codex app-server configured MCP ownership changed; starting a new thread", { threadId: predecessorBinding.threadId });
3566
+ replacementPredecessor = predecessorBinding;
3567
+ binding = void 0;
3568
+ preserveExistingBinding = false;
3569
+ }
2875
3570
  if (binding?.threadId && params.mcpServersFingerprintEvaluated === true && binding.mcpServersFingerprint !== params.mcpServersFingerprint) {
2876
3571
  assertCodexBindingMayBeReplaced(binding, "changing MCP configuration");
2877
3572
  if (!ringZeroActive && (transientNativeToolRestriction || webSearchBindingChanged && (explicitTransientWebSearchRestriction || unknownProviderWebSearchSupport))) {
@@ -2887,7 +3582,7 @@ async function startOrResumeThread(params) {
2887
3582
  if (binding?.threadId && webSearchBindingChanged && !deferLegacyWebSearchRotationToTransientNativeSurface) {
2888
3583
  assertCodexBindingMayBeReplaced(binding, "changing web-search configuration");
2889
3584
  if (!ringZeroActive && transientWebSearchRestriction) {
2890
- embeddedAgentLog.debug("codex app-server web search restricted for turn; starting transient thread", { threadId: binding.threadId });
3585
+ embeddedAgentLog.debug("codex app-server tool surface restricted for turn; starting transient thread", { threadId: binding.threadId });
2891
3586
  preserveExistingBinding = true;
2892
3587
  } else {
2893
3588
  embeddedAgentLog.debug("codex app-server web search config changed; starting a new thread", { threadId: binding.threadId });
@@ -2933,11 +3628,6 @@ async function startOrResumeThread(params) {
2933
3628
  await clearCurrentBinding("rotating a stale thread binding");
2934
3629
  binding = void 0;
2935
3630
  }
2936
- if (binding?.threadId && binding.environmentSelectionFingerprint !== environmentSelectionFingerprint) {
2937
- embeddedAgentLog.debug("codex app-server environment selection changed; starting a new thread", { threadId: binding.threadId });
2938
- await clearCurrentBinding("rotating a stale thread binding");
2939
- binding = void 0;
2940
- }
2941
3631
  if (binding?.threadId && (binding.networkProxyConfigFingerprint !== networkProxyConfigFingerprint || binding.networkProxyProfileName !== params.appServer.networkProxy?.profileName)) {
2942
3632
  embeddedAgentLog.debug("codex app-server network proxy config changed; starting a new thread", { threadId: binding.threadId });
2943
3633
  await clearCurrentBinding("rotating a stale thread binding");
@@ -2951,13 +3641,15 @@ async function startOrResumeThread(params) {
2951
3641
  currentInputFingerprint: params.pluginThreadConfig?.inputFingerprint,
2952
3642
  hasBindingPolicyContext: Boolean(binding.pluginAppPolicyContext)
2953
3643
  });
2954
- if (!pluginBindingStale && shouldRecheckRecoverablePluginBinding({
3644
+ if (!pluginBindingStale && (params.pluginThreadConfig?.requiresCurrentPolicyCheck || shouldRecheckRecoverablePluginBinding({
2955
3645
  binding,
2956
3646
  pluginThreadConfig: params.pluginThreadConfig
2957
- })) try {
2958
- prebuiltPluginThreadConfig = await lifecycleTiming.measure("plugin-config-recovery", () => params.pluginThreadConfig?.build());
3647
+ }))) try {
3648
+ const bindingThreadId = binding.threadId;
3649
+ prebuiltPluginThreadConfig = await lifecycleTiming.measure("plugin-config-recovery", () => params.pluginThreadConfig?.build({ threadId: bindingThreadId }));
2959
3650
  pluginBindingStale = prebuiltPluginThreadConfig?.fingerprint !== binding.pluginAppsFingerprint;
2960
3651
  } catch (error) {
3652
+ if (params.pluginThreadConfig?.requiresCurrentPolicyCheck) throw error;
2961
3653
  embeddedAgentLog.warn("codex app-server plugin app config recovery check failed", {
2962
3654
  error,
2963
3655
  threadId: binding.threadId
@@ -3010,6 +3702,32 @@ async function startOrResumeThread(params) {
3010
3702
  await clearCurrentBinding("rotating an unavailable ephemeral thread binding");
3011
3703
  binding = void 0;
3012
3704
  } else {
3705
+ const warmReuse = await tryReuseCodexLiveThread({
3706
+ params,
3707
+ binding,
3708
+ bindingIdentity,
3709
+ clientId,
3710
+ dynamicToolsFingerprint,
3711
+ environmentSelectionFingerprint,
3712
+ hostSystemAgentActive,
3713
+ lifecycleTiming,
3714
+ nativeSkillIsolation,
3715
+ releaseConsumedThread: (threadId, cause) => releaseCodexConsumedLiveThread({
3716
+ client: params.client,
3717
+ abandonClient: params.abandonClient,
3718
+ lifecycleTiming,
3719
+ threadId,
3720
+ cause
3721
+ }),
3722
+ ringZeroActive,
3723
+ restrictedToolSurfaceInheritedMcpServerNames,
3724
+ startModelProvider,
3725
+ startModelSelection,
3726
+ throwIfAborted,
3727
+ userMcpServersConfigPatch
3728
+ });
3729
+ if (warmReuse.binding) return warmReuse.binding;
3730
+ await releaseRetainedThread(binding.threadId);
3013
3731
  const resumed = await resumeExistingCodexThread(params, {
3014
3732
  binding,
3015
3733
  bindingIdentity,
@@ -3019,6 +3737,7 @@ async function startOrResumeThread(params) {
3019
3737
  dynamicToolsFingerprint,
3020
3738
  dynamicToolsContainDeferred,
3021
3739
  webSearchThreadConfigFingerprint,
3740
+ nativeSkillIsolationFingerprint,
3022
3741
  userMcpServersFingerprint,
3023
3742
  ringZeroConfigFingerprint,
3024
3743
  ringZeroClientInstanceId,
@@ -3027,15 +3746,20 @@ async function startOrResumeThread(params) {
3027
3746
  environmentSelectionFingerprint,
3028
3747
  hostSystemAgentActive,
3029
3748
  ringZeroActive,
3030
- ringZeroInheritedMcpServerNames,
3749
+ restrictedToolSurface,
3750
+ restrictedToolSurfaceInheritedMcpServerNames,
3751
+ nativeSkillIsolation,
3031
3752
  lifecycleTiming,
3032
3753
  normalizeBindingModelProvider,
3033
3754
  throwIfAborted,
3034
- clearCurrentBinding
3755
+ clearCurrentBinding,
3756
+ prebuiltFinalConfigPatch: warmReuse.prebuiltFinalConfigPatch,
3757
+ prebuiltPluginThreadConfig
3035
3758
  });
3036
3759
  if (resumed) return resumed;
3037
3760
  }
3038
- return await startFreshCodexThread(params, {
3761
+ if (initialBoundThreadId && !preserveExistingBinding && !replacementPredecessor) await releaseRetainedThread(initialBoundThreadId);
3762
+ const started = await startFreshCodexThread(params, {
3039
3763
  bindingIdentity,
3040
3764
  startModelSelection,
3041
3765
  startModelProvider,
@@ -3043,6 +3767,7 @@ async function startOrResumeThread(params) {
3043
3767
  dynamicToolsFingerprint,
3044
3768
  dynamicToolsContainDeferred,
3045
3769
  webSearchThreadConfigFingerprint,
3770
+ nativeSkillIsolationFingerprint,
3046
3771
  userMcpServersFingerprint,
3047
3772
  ringZeroConfigFingerprint,
3048
3773
  ringZeroClientInstanceId,
@@ -3051,84 +3776,27 @@ async function startOrResumeThread(params) {
3051
3776
  environmentSelectionFingerprint,
3052
3777
  hostSystemAgentActive,
3053
3778
  ringZeroActive,
3054
- ringZeroInheritedMcpServerNames,
3779
+ restrictedToolSurface,
3780
+ restrictedToolSurfaceInheritedMcpServerNames,
3781
+ nativeSkillIsolation,
3055
3782
  lifecycleTiming,
3056
3783
  normalizeBindingModelProvider,
3057
3784
  throwIfAborted,
3058
3785
  prebuiltPluginThreadConfig,
3059
3786
  preserveExistingBinding,
3060
- rotatedContextEngineBinding
3787
+ rotatedContextEngineBinding,
3788
+ replacementPredecessor
3061
3789
  });
3790
+ if (replacementPredecessor) await releaseRetainedThread(replacementPredecessor.threadId, replacementPredecessor.clientId);
3791
+ return started;
3062
3792
  });
3063
3793
  }
3064
3794
  //#endregion
3065
- //#region extensions/codex/src/app-server/image-payload-sanitizer.ts
3066
- /**
3067
- * Sanitizes inline image payloads mirrored through Codex history so invalid
3068
- * base64 data becomes readable text instead of poisoning replayed transcripts.
3069
- */
3070
- const IMAGE_OMITTED_TEXT = "omitted image payload: invalid inline image data";
3071
- /** Validates and normalizes an inline image data URL for Codex history payloads. */
3072
- function sanitizeInlineImageDataUrl$1(imageUrl) {
3073
- return sanitizeInlineImageDataUrl(imageUrl);
3074
- }
3075
- /** Builds the replacement text inserted when an inline image payload is invalid. */
3076
- function invalidInlineImageText(label) {
3077
- return `[${label}] ${IMAGE_OMITTED_TEXT}`;
3078
- }
3079
- function sanitizeImageContentRecord(record, label) {
3080
- if (record.type === "image" && typeof record.data === "string") {
3081
- const mimeType = typeof record.mimeType === "string" ? record.mimeType : "image/png";
3082
- const imageUrl = sanitizeInlineImageDataUrl$1(`data:${mimeType};base64,${record.data}`);
3083
- if (!imageUrl) return {
3084
- type: "text",
3085
- text: invalidInlineImageText(label)
3086
- };
3087
- const commaIndex = imageUrl.indexOf(",");
3088
- const mime = imageUrl.slice(INLINE_IMAGE_DATA_URL_PREFIX.length, commaIndex).split(";")[0] ?? mimeType;
3089
- return {
3090
- ...record,
3091
- mimeType: mime,
3092
- data: imageUrl.slice(commaIndex + 1)
3093
- };
3094
- }
3095
- if (record.type === "inputImage" && typeof record.imageUrl === "string") {
3096
- const imageUrl = sanitizeInlineImageDataUrl$1(record.imageUrl);
3097
- return imageUrl ? {
3098
- ...record,
3099
- imageUrl
3100
- } : {
3101
- type: "inputText",
3102
- text: invalidInlineImageText(label)
3103
- };
3104
- }
3105
- if (record.type === "input_image" && typeof record.image_url === "string") {
3106
- const imageUrl = sanitizeInlineImageDataUrl$1(record.image_url);
3107
- return imageUrl ? {
3108
- ...record,
3109
- image_url: imageUrl
3110
- } : {
3111
- type: "input_text",
3112
- text: invalidInlineImageText(label)
3113
- };
3114
- }
3115
- }
3116
- /** Recursively sanitizes all Codex history image shapes while preserving unknown structure. */
3117
- function sanitizeCodexHistoryImagePayloads(value, label) {
3118
- if (Array.isArray(value)) return value.map((entry) => sanitizeCodexHistoryImagePayloads(entry, label));
3119
- if (!isRecord(value)) return value;
3120
- const imageRecord = sanitizeImageContentRecord(value, label);
3121
- if (imageRecord) return imageRecord;
3122
- const next = {};
3123
- for (const [key, child] of Object.entries(value)) next[key] = sanitizeCodexHistoryImagePayloads(child, label);
3124
- return next;
3125
- }
3126
- //#endregion
3127
3795
  //#region extensions/codex/src/app-server/user-input.ts
3128
3796
  /** Builds ordered Codex user input for both new turns and same-turn steering. */
3129
3797
  function buildCodexUserInput(text, images) {
3130
3798
  const imageInputs = (images ?? []).map((image) => {
3131
- const imageUrl = sanitizeInlineImageDataUrl$1(`data:${image.mimeType};base64,${image.data}`);
3799
+ const imageUrl = sanitizeInlineImageDataUrl(`data:${image.mimeType};base64,${image.data}`);
3132
3800
  return imageUrl ? {
3133
3801
  type: "image",
3134
3802
  url: imageUrl
@@ -3146,6 +3814,27 @@ function buildCodexUserInput(text, images) {
3146
3814
  }
3147
3815
  //#endregion
3148
3816
  //#region extensions/codex/src/app-server/turn-params.ts
3817
+ const CODEX_CURRENT_SENDER_FIELD_MAX_CHARS = 256;
3818
+ function buildCodexCurrentSenderContextValue(params) {
3819
+ const metadata = asOptionalRecord(asOptionalRecord(params.userTurnTranscriptRecorder?.message)?.["__openclaw"]);
3820
+ const recorded = [
3821
+ normalizeOptionalString(metadata?.["senderId"]),
3822
+ normalizeOptionalString(metadata?.["senderName"]),
3823
+ normalizeOptionalString(metadata?.["senderUsername"])
3824
+ ];
3825
+ const [id, name, username] = recorded.some(Boolean) ? recorded : [
3826
+ normalizeOptionalString(params.senderId),
3827
+ normalizeOptionalString(params.senderName),
3828
+ normalizeOptionalString(params.senderUsername)
3829
+ ];
3830
+ if (!id && !name && !username) return;
3831
+ const bound = (value) => truncateUtf16Safe(value, CODEX_CURRENT_SENDER_FIELD_MAX_CHARS);
3832
+ return JSON.stringify({ sender: {
3833
+ ...id ? { id: bound(id) } : {},
3834
+ ...name ? { name: bound(name) } : {},
3835
+ ...username ? { username: bound(username) } : {}
3836
+ } });
3837
+ }
3149
3838
  function buildTurnStartParams(params, options) {
3150
3839
  const modelSelection = options.preserveNativeTurnSettings ? void 0 : resolveCodexAppServerRequestModelSelection({
3151
3840
  model: options.model ?? params.modelId,
@@ -3156,9 +3845,15 @@ function buildTurnStartParams(params, options) {
3156
3845
  config: params.config
3157
3846
  });
3158
3847
  const useThreadPermissionProfile = options.appServer.networkProxy && !options.sandboxPolicy;
3848
+ const currentSenderContext = params.trigger === "user" ? buildCodexCurrentSenderContextValue(params) : void 0;
3849
+ const additionalContext = currentSenderContext ? { openclaw_current_sender: {
3850
+ kind: "untrusted",
3851
+ value: currentSenderContext
3852
+ } } : void 0;
3159
3853
  return {
3160
3854
  threadId: options.threadId,
3161
- input: buildCodexUserInput(options.promptText ?? params.prompt, params.images),
3855
+ input: [...buildCodexUserInput(options.promptText ?? params.prompt, params.images), ...options.explicitSkillInputs ?? []],
3856
+ ...additionalContext ? { additionalContext } : {},
3162
3857
  cwd: options.cwd,
3163
3858
  approvalPolicy: options.appServer.approvalPolicy,
3164
3859
  approvalsReviewer: options.appServer.approvalsReviewer,
@@ -3167,7 +3862,7 @@ function buildTurnStartParams(params, options) {
3167
3862
  model: modelSelection.model,
3168
3863
  personality: CODEX_NATIVE_PERSONALITY_NONE
3169
3864
  } : {},
3170
- ...options.appServer.serviceTier !== void 0 ? { serviceTier: options.appServer.serviceTier } : {},
3865
+ ...options.appServer.serviceTier !== void 0 ? { serviceTier: options.appServer.serviceTier } : options.clearInheritedServiceTier ? { serviceTier: null } : {},
3171
3866
  ...modelSelection ? { effort: resolveReasoningEffort(params.thinkLevel, modelSelection.model, readCodexSupportedReasoningEfforts(params.model?.compat)) } : {},
3172
3867
  ...options.environmentSelection ? { environments: options.environmentSelection } : {},
3173
3868
  ...modelSelection ? { collaborationMode: buildTurnCollaborationMode(params, {
@@ -3192,7 +3887,7 @@ function buildTurnCollaborationMode(params, options = {}) {
3192
3887
  function buildTurnScopedCollaborationInstructions(params, options = {}) {
3193
3888
  const contextInstructions = joinPresentSections(options.turnScopedDeveloperInstructions, options.memoryCollaborationInstructions, options.skillsCollaborationInstructions);
3194
3889
  if (params.trigger === "cron") return joinPresentSections(buildCronCollaborationInstructions(), contextInstructions);
3195
- if (params.trigger === "heartbeat" && params.bootstrapContextRunKind !== "commitment-only") return joinPresentSections(buildHeartbeatCollaborationInstructions(), contextInstructions);
3890
+ if (params.trigger === "heartbeat") return joinPresentSections(buildHeartbeatCollaborationInstructions(), contextInstructions);
3196
3891
  if (contextInstructions?.trim()) return joinPresentSections(buildDefaultCollaborationInstructions(), contextInstructions);
3197
3892
  return null;
3198
3893
  }
@@ -3230,4 +3925,4 @@ function joinPresentSections(...sections) {
3230
3925
  return sections.filter((section) => Boolean(section?.trim())).join("\n\n");
3231
3926
  }
3232
3927
  //#endregion
3233
- export { readHookOutputEntries as $, resolveCodexDynamicToolsLoading as A, resolveCodexContextEngineProjectionReserveTokens as B, readCodexSupportedReasoningEfforts as C, isForcedPrivateQaCodexRuntime as D, filterCodexDynamicToolsWithOpenClawShell as E, buildContextEngineBinding as F, mergeCodexThreadConfigs as G, buildCodexPluginThreadConfig as H, isContextEngineBindingCompatible as I, pluginReadParams as J, shouldBuildCodexPluginThreadConfig as K, fitCodexProjectedContextForTurnStart as L, areCodexDynamicToolFingerprintsCompatible as M, codexDynamicToolsFingerprint as N, isSystemAgentOnlyCodexDynamicToolAllowlist as O, codexLegacyDynamicToolsFingerprint as P, readCodexErrorNotificationMessage as Q, projectContextEngineAssemblyForCodex as R, resolveReasoningEffort as S, filterCodexDynamicTools as T, buildCodexPluginThreadConfigInputFingerprint as U, buildCodexPluginAppsConfigPatchFromPolicyContext as V, buildCodexPluginThreadConfigTimeoutFallback as W, extractRawAssistantText as X, resolveRecoverableCodexPluginConfigKeys as Y, normalizeNonEmptyString as Z, CODEX_NATIVE_PERSONALITY_NONE as _, sanitizeCodexHistoryImagePayloads as a, readNullableString as at, resolveCodexAppServerThreadModelSelection as b, resumeCodexAppServerThread as c, splitPlanText as ct, buildCodexRingZeroThreadConfigPatch as d, readItem as et, buildCodexRuntimeThreadConfig as f, buildDeveloperInstructions as g, resolveCodexWebSearchPlan as h, invalidInlineImageText as i, readNonNegativeInteger as it, resolveCodexDynamicToolsLoadingForRuntime as j, normalizeCodexDynamicToolName as k, assertCodexRingZeroHasNoManagedHooks as l, buildCodexNativeWebSearchThreadConfig as m, buildTurnStartParams as n, readNonEmptyString as nt, sanitizeInlineImageDataUrl$1 as o, readNumber as ot, readCodexInheritedMcpServerNames as p, ensureCodexPluginActivation as q, buildCodexUserInput as r, readNonEmptyStringArray as rt, startOrResumeThread as s, readString as st, buildTurnCollaborationMode as t, readItemString as tt, attestCodexRingZeroThreadHasNoMcpServers as u, resolveCodexAppServerModelProvider as v, isCodexAppServerProfilerEnabled as w, resolveCodexBindingModelProviderFallback as x, resolveCodexAppServerRequestModelSelection as y, resolveCodexContextEngineProjectionMaxChars as z };
3928
+ export { resolveCodexDynamicToolsLoadingForRuntime as A, buildCodexPluginThreadConfigInputFingerprint as B, codexLegacyDynamicToolsFingerprint as C, isSystemAgentOnlyCodexDynamicToolAllowlist as D, isForcedPrivateQaCodexRuntime as E, projectContextEngineAssemblyForCodex as F, shouldBuildCodexPluginThreadConfig as G, buildDisabledAppsConfigPatch as H, resolveCodexContextEngineProjectionMaxChars as I, isOpenAiCuratedMarketplace as J, ensureCodexPluginActivation as K, resolveCodexContextEngineProjectionReserveTokens as L, isContextEngineBindingCompatible as M, fitCodexProjectedContextForTurnStart as N, normalizeCodexDynamicToolName as O, neutralizeCodexExplicitMentionSigils as P, buildCodexPluginAppsConfigPatchFromPolicyContext as R, codexDynamicToolsFingerprint as S, filterCodexDynamicToolsForDisabledNativeSurface as T, buildPluginAppPolicyContext as U, buildCodexPluginThreadConfigTimeoutFallback as V, mergeCodexThreadConfigs as W, pluginReadParams as X, isOpenAiCuratedMarketplaceName as Y, resolveRecoverableCodexPluginConfigKeys as Z, resolveCodexAppServerThreadModelSelection as _, isCodexAppServerProfilerEnabled as a, readCodexSupportedReasoningEfforts as b, buildCodexRingZeroThreadConfigPatch as c, buildCodexNativeWebSearchThreadConfig as d, resolveCodexWebSearchPlan as f, resolveCodexAppServerRequestModelSelection as g, resolveCodexAppServerModelProvider as h, startOrResumeThread as i, buildContextEngineBinding as j, resolveCodexDynamicToolsLoading as k, buildCodexRuntimeThreadConfig as l, CODEX_NATIVE_PERSONALITY_NONE as m, buildTurnStartParams as n, assertCodexRestrictedToolSurfaceHasNoManagedHooks as o, buildDeveloperInstructions as p, refreshCodexPluginRuntimeState as q, buildCodexUserInput as r, attestCodexRestrictedToolSurfaceMcpServersDisabled as s, buildTurnCollaborationMode as t, readCodexInheritedMcpServerNames as u, resolveCodexBindingModelProviderFallback as v, filterCodexDynamicTools as w, areCodexDynamicToolFingerprintsCompatible as x, resolveReasoningEffort as y, buildCodexPluginThreadConfig as z };