@bitkyc08/opencodex 2.58.0 → 2.59.0

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 (188) hide show
  1. package/README.md +28 -10
  2. package/gui/dist/assets/index-C5IebErG.js +136 -0
  3. package/gui/dist/assets/{index-C5-RdDmD.css → index-OESInAjC.css} +1 -1
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/crusoe.svg +1 -0
  6. package/gui/dist/provider-icons/opper.svg +3 -0
  7. package/package.json +1 -1
  8. package/src/adapters/base.ts +11 -1
  9. package/src/adapters/cursor/catalog.ts +11 -0
  10. package/src/adapters/cursor/effort-map.ts +16 -2
  11. package/src/adapters/cursor/envelope-echo.ts +55 -2
  12. package/src/adapters/cursor/message-mapper.ts +3 -2
  13. package/src/adapters/cursor/protobuf-request.ts +8 -5
  14. package/src/adapters/cursor/request-builder.ts +14 -3
  15. package/src/adapters/cursor/thread-continuity.ts +105 -31
  16. package/src/adapters/cursor/tool-guidance.ts +5 -4
  17. package/src/adapters/cursor.ts +42 -1
  18. package/src/adapters/devin/cloud-direct/chat.ts +11 -2
  19. package/src/adapters/devin/cloud-direct/index.ts +7 -0
  20. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +103 -0
  21. package/src/adapters/devin.ts +75 -13
  22. package/src/adapters/google-antigravity-wire.ts +29 -2
  23. package/src/adapters/google-http.ts +8 -1
  24. package/src/adapters/google.ts +23 -4
  25. package/src/adapters/openai-chat/response-events.ts +61 -0
  26. package/src/adapters/openai-chat.ts +5 -10
  27. package/src/adapters/openai-responses/passthrough.ts +10 -1
  28. package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
  29. package/src/adapters/openai-responses/tool-schema.ts +19 -7
  30. package/src/adapters/responses-tool-schema.ts +76 -46
  31. package/src/adapters/run-turn-queue.ts +17 -4
  32. package/src/bridge/response-json.ts +1 -1
  33. package/src/bridge/sse.ts +165 -24
  34. package/src/claude/context-windows.ts +22 -0
  35. package/src/claude/outbound.ts +35 -4
  36. package/src/cli/account-api.ts +4 -3
  37. package/src/cli/account-extended.ts +22 -2
  38. package/src/cli/account-orca-import.ts +63 -0
  39. package/src/cli/account.ts +32 -4
  40. package/src/cli/capabilities.ts +40 -0
  41. package/src/cli/claude.ts +29 -1
  42. package/src/cli/codex-cli-update.ts +97 -2
  43. package/src/cli/dispatch.ts +54 -0
  44. package/src/cli/doctor.ts +197 -2
  45. package/src/cli/help.ts +4 -1
  46. package/src/cli/index.ts +88 -20
  47. package/src/cli/models-runtime.ts +33 -4
  48. package/src/cli/registry.ts +11 -1
  49. package/src/cli/runtime-api.ts +44 -0
  50. package/src/cli/start-args.ts +94 -0
  51. package/src/cli/system-command.ts +2 -0
  52. package/src/client/machine-api.ts +4 -3
  53. package/src/client/machine-listener.ts +14 -1
  54. package/src/clients/config-export/constants.ts +2 -3
  55. package/src/clients/config-export.ts +5 -5
  56. package/src/codex/account-store.ts +81 -5
  57. package/src/codex/auth-api/pool-quota-probe.ts +14 -3
  58. package/src/codex/auth-api/routes.ts +17 -2
  59. package/src/codex/auth-context.ts +16 -12
  60. package/src/codex/catalog/build-entries.ts +25 -4
  61. package/src/codex/catalog/derive-entry.ts +8 -1
  62. package/src/codex/catalog/effort.ts +10 -6
  63. package/src/codex/catalog/gather-capture.ts +1 -0
  64. package/src/codex/catalog/model-hints.ts +37 -5
  65. package/src/codex/catalog/parsing.ts +83 -5
  66. package/src/codex/catalog/reserve-warn.ts +96 -0
  67. package/src/codex/catalog/retained-sync.ts +19 -0
  68. package/src/codex/catalog/routed-gather.ts +42 -3
  69. package/src/codex/cli-installation-identity.ts +210 -0
  70. package/src/codex/cli-installation-targets.ts +158 -0
  71. package/src/codex/convergence.ts +5 -0
  72. package/src/codex/history-provider.ts +4 -1
  73. package/src/codex/history-state-open.ts +105 -0
  74. package/src/codex/inject/config-toml.ts +44 -2
  75. package/src/codex/inject.ts +3 -2
  76. package/src/codex/lineage.ts +83 -32
  77. package/src/codex/loopback-target.ts +31 -0
  78. package/src/codex/main-account-hard-lock.ts +2 -1
  79. package/src/codex/main-account.ts +10 -3
  80. package/src/codex/main-device-reauth.ts +17 -9
  81. package/src/codex/model-entitlements.ts +60 -1
  82. package/src/codex/observed-model-denials.ts +137 -0
  83. package/src/codex/orca-auth-source.ts +94 -0
  84. package/src/codex/orca-import.ts +219 -0
  85. package/src/codex/prompt-text-probe.ts +282 -12
  86. package/src/codex/quota-401-recovery.ts +12 -0
  87. package/src/codex/quota-types.ts +65 -0
  88. package/src/codex/quota.ts +24 -19
  89. package/src/codex/routing/cooldown-math.ts +8 -47
  90. package/src/codex/routing/pin-drain.ts +57 -0
  91. package/src/codex/routing.ts +13 -15
  92. package/src/codex/subagent-model-fallback.ts +94 -0
  93. package/src/codex/windows-installation-files.ts +224 -0
  94. package/src/combos/failover.ts +122 -5
  95. package/src/config/diagnostics.ts +21 -0
  96. package/src/config/load-degrade.ts +15 -0
  97. package/src/config/pending-teardown.ts +8 -0
  98. package/src/config/process-state.ts +36 -3
  99. package/src/config/provider-relative-send-path.ts +16 -0
  100. package/src/config/proxy-env.ts +23 -5
  101. package/src/config/schema/config-schema.ts +21 -0
  102. package/src/config/schema/leaf-validators.ts +64 -17
  103. package/src/generated/compatibility-version.json +235 -163
  104. package/src/generated/model-metadata.ts +1 -1
  105. package/src/lib/bounded-body.ts +4 -2
  106. package/src/lib/destination-policy.ts +48 -6
  107. package/src/lib/errors.ts +3 -15
  108. package/src/lib/local-destinations.ts +32 -5
  109. package/src/lib/provider-outbound.ts +3 -3
  110. package/src/lib/proxy-env.ts +70 -3
  111. package/src/lib/request-execution-budget.ts +11 -3
  112. package/src/lib/response-body-inactivity.ts +193 -0
  113. package/src/lib/retry-delay.ts +69 -0
  114. package/src/lib/socks5-fetch.ts +631 -0
  115. package/src/lib/spend-reservation-ledger.ts +115 -9
  116. package/src/lib/workflow-budget.ts +145 -8
  117. package/src/oauth/account-quota-rank.ts +72 -15
  118. package/src/oauth/generic-account-failover.ts +40 -27
  119. package/src/oauth/orcarouter.ts +15 -2
  120. package/src/oauth/store.ts +8 -0
  121. package/src/providers/codex-capacity.ts +9 -0
  122. package/src/providers/devin-provider-merge-migration.ts +33 -12
  123. package/src/providers/free-directory.ts +20 -2
  124. package/src/providers/key-failover.ts +261 -7
  125. package/src/providers/model-rename-migration.ts +1 -0
  126. package/src/providers/openai-sidecar.ts +4 -0
  127. package/src/providers/opencode-go-transport.ts +14 -5
  128. package/src/providers/quota/report-cache.ts +3 -0
  129. package/src/providers/registry/entries-extended.ts +96 -0
  130. package/src/providers/registry/model-seeds.ts +78 -21
  131. package/src/responses/apply-patch-envelope.ts +44 -11
  132. package/src/responses/bridge-search-replay-cache.ts +152 -0
  133. package/src/responses/code-mode-helper-compat.ts +26 -16
  134. package/src/responses/custom-tool-compat.ts +1 -1
  135. package/src/responses/hosted-tool-policy.ts +85 -2
  136. package/src/responses/schema.ts +9 -2
  137. package/src/server/auth-cors.ts +26 -0
  138. package/src/server/chat-completions.ts +9 -4
  139. package/src/server/chat-native-sse.ts +26 -9
  140. package/src/server/chat-native.ts +10 -4
  141. package/src/server/claude-messages.ts +24 -2
  142. package/src/server/gui-static.ts +36 -2
  143. package/src/server/inbound-body-admission.ts +187 -0
  144. package/src/server/index.ts +15 -19
  145. package/src/server/management/api-access.ts +3 -4
  146. package/src/server/management/config-routes.ts +31 -6
  147. package/src/server/management/provider-capability-config.ts +35 -7
  148. package/src/server/management/provider-routes.ts +70 -18
  149. package/src/server/proxy-liveness.ts +97 -2
  150. package/src/server/relay.ts +17 -24
  151. package/src/server/request-log.ts +25 -1
  152. package/src/server/responses/adapter-continuation.ts +71 -27
  153. package/src/server/responses/adapter-delivery.ts +39 -8
  154. package/src/server/responses/adapter-dispatch.ts +52 -24
  155. package/src/server/responses/compact.ts +60 -11
  156. package/src/server/responses/core-codex-account.ts +83 -22
  157. package/src/server/responses/core-normalize.ts +12 -5
  158. package/src/server/responses/fetch-helpers.ts +68 -2
  159. package/src/server/responses/passthrough-delivery.ts +10 -1
  160. package/src/server/responses/passthrough-dispatch.ts +113 -48
  161. package/src/server/responses/passthrough-execution.ts +11 -1
  162. package/src/server/responses/request-prepare.ts +29 -0
  163. package/src/server/responses/request-send-budget.ts +84 -7
  164. package/src/server/responses/request-sidecar-auth.ts +16 -8
  165. package/src/server/responses/request-spend.ts +38 -9
  166. package/src/server/responses/request-transport.ts +13 -10
  167. package/src/server/responses/run-turn-execution.ts +20 -5
  168. package/src/server/responses/sidecar-execution.ts +2 -0
  169. package/src/server/responses/ws-upstream.ts +2 -1
  170. package/src/server/responses-custom-tool-repair.ts +2 -2
  171. package/src/server/sse-frame-buffer.ts +12 -10
  172. package/src/server/sse-payload-rewrite.ts +36 -9
  173. package/src/server/system-env-shell.ts +5 -1
  174. package/src/server/system-env.ts +7 -1
  175. package/src/server/workflow-refusal.ts +56 -2
  176. package/src/service/cli.ts +16 -6
  177. package/src/service/guards.ts +10 -0
  178. package/src/service/health.ts +43 -0
  179. package/src/service/state.ts +7 -2
  180. package/src/types/accounts.ts +4 -0
  181. package/src/types/config.ts +100 -3
  182. package/src/types/provider.ts +19 -0
  183. package/src/types/request.ts +7 -1
  184. package/src/types/wire.ts +9 -1
  185. package/src/usage/expected-prices.ts +28 -0
  186. package/src/usage/log.ts +87 -4
  187. package/src/web-search/passthrough-bridge.ts +39 -5
  188. package/gui/dist/assets/index-BbrHOIY0.js +0 -128
@@ -0,0 +1,63 @@
1
+ import type { importOrcaAccounts } from "../codex/orca-import";
2
+
3
+ const USAGE = "Usage: ocx account import-orca --source <orca-data-directory> --registry <orca-data.json> [--apply] [--json]";
4
+
5
+ export interface OrcaImportCommandDeps {
6
+ importAccounts?: typeof importOrcaAccounts;
7
+ }
8
+
9
+ /** Local-only: never sends source paths or credentials to a management listener. */
10
+ export async function cmdOrcaImport(args: string[], deps: OrcaImportCommandDeps = {}): Promise<number> {
11
+ let sourceDir: string | undefined;
12
+ let registryPath: string | undefined;
13
+ let apply = false;
14
+ const wantsJson = args.includes("--json");
15
+ const seen = new Set<string>();
16
+ for (let index = 0; index < args.length; index++) {
17
+ const arg = args[index]!;
18
+ if (seen.has(arg) || !["--source", "--registry", "--apply", "--json"].includes(arg)) {
19
+ console.error(USAGE);
20
+ return 1;
21
+ }
22
+ seen.add(arg);
23
+ if (arg === "--source" || arg === "--registry") {
24
+ const value = args[++index];
25
+ if (!value || value.startsWith("--")) {
26
+ console.error(USAGE);
27
+ return 1;
28
+ }
29
+ if (arg === "--source") sourceDir = value;
30
+ else registryPath = value;
31
+ } else if (arg === "--apply") apply = true;
32
+ }
33
+ if (!sourceDir || !registryPath) {
34
+ console.error(USAGE);
35
+ return 1;
36
+ }
37
+ try {
38
+ const run = deps.importAccounts ?? (await import("../codex/orca-import")).importOrcaAccounts;
39
+ const result = await run({ sourceDir, registryPath, apply });
40
+ if (wantsJson) console.log(JSON.stringify({
41
+ mode: result.mode, discovered: result.discovered, eligible: result.eligible,
42
+ imported: result.imported, duplicates: result.duplicates, invalid: result.invalid,
43
+ invalidReasons: result.invalidReasons,
44
+ }));
45
+ else {
46
+ console.log(`Orca: ${result.discovered} discovered, ${result.eligible} eligible, ${result.imported} imported, ${result.duplicates} duplicates, ${result.invalid} invalid.`);
47
+ if (result.invalid > 0) {
48
+ const reasons = Object.entries(result.invalidReasons).map(([reason, count]) => `${reason}=${count}`).join(", ");
49
+ console.log(`Invalid reasons: ${reasons}`);
50
+ }
51
+ console.log(apply
52
+ ? "Start the proxy and use Refresh quotas in Codex Auth to validate new accounts. Orca retains refresh ownership."
53
+ : "Preview only. Stop the proxy before repeating with --apply. Orca authentication files stay read-only.");
54
+ }
55
+ return result.invalid > 0 && result.eligible === 0 ? 1 : 0;
56
+ } catch {
57
+ // Filesystem/JSON exceptions may contain source paths or token fragments.
58
+ const error = "Orca import failed. Check the source, destination config, and stopped proxy; no successful completion is confirmed.";
59
+ if (wantsJson) console.log(JSON.stringify({ error: "orca_import_failed" }));
60
+ else console.error(error);
61
+ return 1;
62
+ }
63
+ }
@@ -57,6 +57,7 @@ const ACCOUNT_USAGE = `Usage:
57
57
  ocx account clear-cooldown <provider> <account-id|main> [--json]
58
58
  ocx account add-key <provider> [--label <label>] [--json]
59
59
  ocx account import <provider> --format <format> (--file <path>|--stdin) [--json]
60
+ ocx account import-orca --source <orca-data-directory> --registry <orca-data.json> [--apply] [--json]
60
61
  ocx account login <provider> [--id <account-id>] [--reauth] [--code -] [--no-wait] [--json]
61
62
  ocx account code <provider> [--flow <flow-id>] [--json] (reads the code from stdin)
62
63
  ocx account cancel <provider> [--flow <flow-id>] [--json]
@@ -323,13 +324,36 @@ async function cmdUse(rest: string[], deps: AccountDeps): Promise<number> {
323
324
  if (res.status === 0) return proxyUnreachable(res.transportError);
324
325
  if (res.status !== 200) return apiError(res.json, `failed to switch ${name}`, res.status);
325
326
 
326
- if (wantsJson) console.log(JSON.stringify({ ok: true, provider: name, type: c.type, activeId }, null, 2));
327
- else console.log(`${name}: active ${c.type === "api-key" ? "key" : "account"} is now ${displayId(activeId)}`);
327
+ // The route reports this only when routing would drop the pin it just recorded, so an
328
+ // absent field means the pin survives (#4521).
329
+ const pinDrainReason = typeof res.json.pinDrainReason === "string" ? res.json.pinDrainReason : undefined;
330
+ if (wantsJson) {
331
+ console.log(JSON.stringify({
332
+ ok: true,
333
+ provider: name,
334
+ type: c.type,
335
+ activeId,
336
+ ...(pinDrainReason !== undefined ? { pinDrained: true, pinDrainReason } : {}),
337
+ }, null, 2));
338
+ } else {
339
+ console.log(`${name}: active ${c.type === "api-key" ? "key" : "account"} is now ${displayId(activeId)}`);
340
+ }
328
341
  if (c.type === "codex") {
329
342
  console.error("Takes effect immediately; running threads move on their next request, and in-flight requests keep the account they captured.");
330
343
  const active = await apiJson(deps, baseUrl, "GET", "/api/codex-auth/active");
331
- if (active.status === 200 && typeof active.json.autoSwitchThreshold === "number" && active.json.autoSwitchThreshold > 0) {
332
- console.error(`Note: auto-switch (threshold ${active.json.autoSwitchThreshold}%) may override this pin.`);
344
+ const threshold = active.status === 200 && typeof active.json.autoSwitchThreshold === "number"
345
+ ? active.json.autoSwitchThreshold
346
+ : undefined;
347
+ if (pinDrainReason !== undefined) {
348
+ // "may override" is the right caveat for a pin that is currently fine and could be
349
+ // overtaken later. It is the wrong sentence for one the next request will discard, and
350
+ // printing only that is what left the operator believing the account was pinned.
351
+ const because = pinDrainReason === "quota_threshold"
352
+ ? `is at or above the auto-switch threshold${threshold !== undefined ? ` (${threshold}%)` : ""}`
353
+ : `cannot currently be selected (${pinDrainReason})`;
354
+ console.error(`Note: ${displayId(activeId)} ${because}, so routing releases this pin on its next request.`);
355
+ } else if (threshold !== undefined && threshold > 0) {
356
+ console.error(`Note: auto-switch (threshold ${threshold}%) may override this pin.`);
333
357
  }
334
358
  }
335
359
  return 0;
@@ -360,6 +384,10 @@ export async function cmdAccount(args: string[], deps: AccountDeps = {}): Promis
360
384
  if (sub === "clear-cooldown") return await cmdClearCooldown(rest, deps);
361
385
  if (sub === "add-key") return await cmdAddKey(rest, deps);
362
386
  if (sub === "import") return await cmdImport(rest, deps);
387
+ if (sub === "import-orca") {
388
+ const { cmdOrcaImport } = await import("./account-orca-import");
389
+ return await cmdOrcaImport(rest);
390
+ }
363
391
  if (sub === "main") {
364
392
  const { cmdNativeMainAccount } = await import("./account-main");
365
393
  return await cmdNativeMainAccount(rest, deps);
@@ -360,6 +360,25 @@ export const CAPABILITIES: readonly Capability[] = [
360
360
  "`--quota` shows cached Codex windows (including 5h); `--refresh` bypasses the server TTL.",
361
361
  ],
362
362
  },
363
+ {
364
+ command: ["account", "import-orca"],
365
+ summary: "Preview or register read-only links to Orca-managed Codex accounts without another login.",
366
+ routes: [],
367
+ flags: [
368
+ { name: "--source", value: "string", required: true, summary: "Orca data directory containing codex-accounts." },
369
+ { name: "--registry", value: "string", required: true, summary: "The chosen Orca profile's orca-data.json account registry." },
370
+ { name: "--apply", value: "boolean", summary: "Register new accounts; requires a stopped proxy. Default is preview." },
371
+ { name: "--json", value: "boolean", summary: "Emit counts and fixed invalid-reason codes without credentials or source paths." },
372
+ ],
373
+ mutates: true,
374
+ json: "envelope",
375
+ details: [
376
+ "Local files only; never copies refresh tokens or changes Orca authentication files.",
377
+ "Skips existing ChatGPT identities. New accounts remain pending until dashboard validation.",
378
+ "Orca must keep the source login available and refreshed; a missing or expired source fails closed.",
379
+ "Mixed eligible and invalid entries exit successfully; an all-invalid result exits nonzero.",
380
+ ],
381
+ },
363
382
  {
364
383
  command: ["account", "refresh"],
365
384
  summary: "Refresh account quotas without model validation; pending Codex accounts require dashboard consent.",
@@ -708,6 +727,27 @@ export const CAPABILITIES: readonly Capability[] = [
708
727
  "Does not execute Codex or npm, install or repair software, control a process, or write configuration or cache state.",
709
728
  ],
710
729
  },
730
+ {
731
+ command: ["system", "codex-cli-update", "attest"],
732
+ summary: "Observe the selected or explicitly named Windows npm Codex installation files without enabling updates.",
733
+ routes: [],
734
+ flags: [
735
+ { name: "--candidate", value: "string", summary: "Absolute npm codex.cmd or package bin/codex.js path; all four paths are all-or-none." },
736
+ { name: "--npm-prefix", value: "string", summary: "Absolute prefix containing node_modules/@openai/codex." },
737
+ { name: "--npm-cli", value: "string", summary: "Absolute node_modules/npm/bin/npm-cli.js path." },
738
+ { name: "--node", value: "string", summary: "Absolute node.exe path; observed, never executed." },
739
+ { name: "--json", value: "boolean", summary: "Emit the path-free installation identity observation." },
740
+ ],
741
+ mutates: false,
742
+ json: "envelope",
743
+ details: [
744
+ "Opt-in Windows x64 local-volume inspection using held native file handles; refuses reparse points, active writers and unsupported layouts.",
745
+ "Without explicit paths, the proof-bound launcher snapshot identifies the selected candidate: the configured CODEX_CLI_PATH or the first codex on the captured PATH, with an OpenCodex wrapper resolving to its codex.opencodex-real backing. Discovery only proposes paths; the held-handle observation remains the authority.",
746
+ "Success binds observed file identities and bytes, not selected-runtime admission or installer ownership.",
747
+ "selectionAttested, managed and applyAllowed remain false. The digest is an observation, not a durable update permit.",
748
+ "Does not run the named Codex/npm/Node files, query a registry, install software, control processes or persist state.",
749
+ ],
750
+ },
711
751
  {
712
752
  command: ["system", "codex-restart"],
713
753
  summary: "Restart the Codex desktop app and app-servers.",
package/src/cli/claude.ts CHANGED
@@ -11,7 +11,7 @@ import { spawn } from "node:child_process";
11
11
  import { loadConfig } from "../config";
12
12
  import { injectClaudeAgentDefs } from "../claude/agents-inject";
13
13
  import { CLAUDE_ALIAS_PREFIX_V1, CLAUDE_ALIAS_PREFIX_V2 } from "../claude/alias";
14
- import { effectiveModelEnv, resolveAutoContext } from "../claude/context-windows";
14
+ import { claudeToolSearchEnv, effectiveModelEnv, resolveAutoContext } from "../claude/context-windows";
15
15
  import { claudeConfigDir, refreshGatewayModelCacheFromProxy } from "../claude/gateway-cache";
16
16
  import { commandInvocation } from "../lib/win-exec";
17
17
  import { isProxyAdmissionSecret } from "../server/auth-cors";
@@ -357,6 +357,22 @@ export function buildClaudeEnv(
357
357
  if (config.claudeCode?.alwaysEnableEffort === true) {
358
358
  setDefault("CLAUDE_CODE_ALWAYS_ENABLE_EFFORT", "1");
359
359
  }
360
+ // Tool-search deferral (#4838). Claude Code disables MCP tool deferral whenever
361
+ // ANTHROPIC_BASE_URL is not a first-party Anthropic host — keyed on the host, not
362
+ // the model — so every routed session inlines all MCP tool schemas. Its own log
363
+ // line states the precondition: "Set ENABLE_TOOL_SEARCH=true (or auto / auto:N)
364
+ // if your proxy forwards tool_reference blocks."
365
+ //
366
+ // We forward them on the native Anthropic passthrough route only. A translated
367
+ // route cannot honour the shape: deferred tools still carry input_schema on the
368
+ // wire (deferral is a server-side context optimisation, not a smaller request),
369
+ // toolsToResponses drops the tool_search server tool and ignores defer_loading,
370
+ // and compatibility.ts lists tool_search/tool_reference/deferred_tools as
371
+ // unsupported. Turning it on there leaves the routed provider holding every
372
+ // schema while Claude Code stops counting them and stops compacting, which is
373
+ // worse than the problem. So this stays opt-in per config rather than
374
+ // unconditional, and setDefault keeps an operator's own export.
375
+ setDefault("ENABLE_TOOL_SEARCH", claudeToolSearchEnv(config.claudeCode?.toolSearch));
360
376
  // Context-window override: the official pair — MAX_CONTEXT_TOKENS alone is ignored
361
377
  // for recognized claude-shaped ids unless DISABLE_COMPACT=1 rides along (devlog 135).
362
378
  const maxCtx = config.claudeCode?.maxContextTokens;
@@ -542,6 +558,18 @@ export function claudeLaunchPreflight(
542
558
  : { kind: "native", notice: CLAUDE_NATIVE_ROUTING_OFF };
543
559
  }
544
560
 
561
+ /**
562
+ * Levers a native (non-routed) launch must shed. The loop below deletes them
563
+ * unconditionally, because each one either points Claude Code at a gateway that
564
+ * is not there or asserts host ownership that a native session does not have.
565
+ *
566
+ * ENABLE_TOOL_SEARCH is deliberately NOT in this list (#4838). It is the only
567
+ * one of these whose value is meaningful to a native session: natively the base
568
+ * URL is first-party, where deferral is already Claude Code's default and the
569
+ * variable is the user's own tuning knob (auto:N, force). Stripping it would
570
+ * delete a preference that works, to protect a session that does not need
571
+ * protecting.
572
+ */
545
573
  const NATIVE_STRIPPED_LEVERS = [
546
574
  "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY",
547
575
  "CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST",
@@ -3,18 +3,53 @@ import {
3
3
  type CodexCliInstallProvenanceDeps,
4
4
  type CodexCliInstallReport,
5
5
  } from "../codex/cli-install-provenance";
6
+ import type {
7
+ CodexCliInstallationIdentityInput,
8
+ CodexCliInstallationIdentityReport,
9
+ } from "../codex/cli-installation-identity";
10
+ import type {
11
+ CodexCliInstallationSnapshot,
12
+ CodexCliInstallationTargetDerivation,
13
+ } from "../codex/cli-installation-targets";
6
14
  import { CliUsageError, isJsonOption, printData, runCliAction } from "./runtime-api";
7
15
  import { trustedNodeLauncherContext } from "./launcher-context";
8
16
 
9
17
  export const CODEX_CLI_UPDATE_USAGE = `Usage:
10
- ocx system codex-cli-update check [--json]`;
18
+ ocx system codex-cli-update check [--json]
19
+ ocx system codex-cli-update attest [--json]
20
+ ocx system codex-cli-update attest --candidate <absolute-path> --npm-prefix <absolute-path> --npm-cli <absolute-path> --node <absolute-path> [--json]`;
11
21
 
12
22
  export type ParsedCodexCliUpdateArgs = Readonly<{
13
23
  json: boolean;
24
+ }> | Readonly<{
25
+ json: boolean;
26
+ attest: CodexCliInstallationIdentityInput | "selected";
14
27
  }>;
15
28
 
16
29
  export interface CodexCliUpdateCommandDeps {
17
30
  readonly inspectInstall?: (deps: CodexCliInstallProvenanceDeps) => Promise<CodexCliInstallReport>;
31
+ readonly inspectIdentity?: (input: CodexCliInstallationIdentityInput) => Promise<CodexCliInstallationIdentityReport>;
32
+ readonly deriveInstallationInput?: (
33
+ snapshot: CodexCliInstallationSnapshot,
34
+ ) => CodexCliInstallationTargetDerivation;
35
+ }
36
+
37
+ function identitySummary(report: CodexCliInstallationIdentityReport): string[] {
38
+ return [
39
+ `status: ${report.status}`,
40
+ `reason: ${report.reason}`,
41
+ `candidate-source: ${report.candidateSource}`,
42
+ `installation-identity-observed: ${report.installationIdentityObserved ? "yes" : "no"}`,
43
+ `selection-attested: ${report.selectionAttested ? "yes" : "no"}`,
44
+ `managed: ${report.managed ? "yes" : "no"}`,
45
+ `apply-allowed: ${report.applyAllowed ? "yes" : "no"}`,
46
+ `package-version: ${report.packageVersion ?? "unavailable"}`,
47
+ `npm-version: ${report.npmVersion ?? "unavailable"}`,
48
+ `identity-digest: ${report.identityDigest ?? "unavailable"}`,
49
+ `proof: ${report.proof ?? "unavailable"}`,
50
+ `toolchain: ${report.toolchain}`,
51
+ "scope: installation identity only; runtime selection and update ownership are not attested",
52
+ ];
18
53
  }
19
54
 
20
55
  function installSummary(report: CodexCliInstallReport): string[] {
@@ -47,8 +82,38 @@ export function parseCodexCliUpdateArgs(argv: readonly string[]): ParsedCodexCli
47
82
  }
48
83
  positional.push(token);
49
84
  }
85
+ if (positional[0] === "attest") {
86
+ // No options: attest the selected candidate identified from the proof-bound
87
+ // launcher snapshot. The four explicit paths remain all-or-none.
88
+ if (positional.length === 1) {
89
+ return Object.freeze({ json, attest: "selected" as const });
90
+ }
91
+ const options = new Map<string, keyof CodexCliInstallationIdentityInput>([
92
+ ["--candidate", "candidate"], ["--npm-prefix", "npmPrefix"],
93
+ ["--npm-cli", "npmCli"], ["--node", "node"],
94
+ ]);
95
+ const input: Partial<Record<keyof CodexCliInstallationIdentityInput, string>> = {};
96
+ for (let index = 1; index < positional.length; index += 2) {
97
+ const key = options.get(positional[index]!);
98
+ if (!key || input[key] !== undefined) {
99
+ throw new CliUsageError("unsupported or duplicate attest option", CODEX_CLI_UPDATE_USAGE);
100
+ }
101
+ const value = positional[index + 1];
102
+ if (!value || !value.trim() || /[\0\r\n]/.test(value)
103
+ || !(value.startsWith("/") || /^[a-z]:[\\/]/i.test(value))) {
104
+ throw new CliUsageError("attest options require explicit absolute paths", CODEX_CLI_UPDATE_USAGE);
105
+ }
106
+ input[key] = value;
107
+ }
108
+ if (!input.candidate || !input.npmPrefix || !input.npmCli || !input.node) {
109
+ throw new CliUsageError("attest requires --candidate, --npm-prefix, --npm-cli and --node", CODEX_CLI_UPDATE_USAGE);
110
+ }
111
+ return Object.freeze({ json, attest: Object.freeze({
112
+ candidate: input.candidate, npmPrefix: input.npmPrefix, npmCli: input.npmCli, node: input.node,
113
+ }) });
114
+ }
50
115
  if (positional[0] !== "check") {
51
- throw new CliUsageError("codex-cli-update action must be check", CODEX_CLI_UPDATE_USAGE);
116
+ throw new CliUsageError("codex-cli-update action must be check or attest", CODEX_CLI_UPDATE_USAGE);
52
117
  }
53
118
  if (positional.length > 1) {
54
119
  throw new CliUsageError("unsupported codex-cli-update argument", CODEX_CLI_UPDATE_USAGE);
@@ -72,6 +137,36 @@ export async function handleCodexCliUpdateCommand(
72
137
  throw error;
73
138
  }
74
139
  return runCliAction(async () => {
140
+ if ("attest" in parsed) {
141
+ let report: CodexCliInstallationIdentityReport;
142
+ try {
143
+ const identityModule = await import("../codex/cli-installation-identity");
144
+ const inspectIdentity = deps.inspectIdentity ?? identityModule.inspectCodexCliInstallationIdentity;
145
+ if (parsed.attest === "selected") {
146
+ // The proof-bound launcher snapshot is the only trusted source for the
147
+ // selected candidate; a direct Bun/source launch has none and refuses.
148
+ const snapshot = trustedNodeLauncherContext()?.codexCliInspectionEnv;
149
+ const derive = deps.deriveInstallationInput
150
+ ?? (await import("../codex/cli-installation-targets")).deriveCodexCliInstallationInput;
151
+ const derived = derive({
152
+ codexCliPath: snapshot?.codexCliPath ?? null,
153
+ path: snapshot?.path ?? null,
154
+ pathExt: snapshot?.pathExt ?? null,
155
+ });
156
+ report = derived.kind === "derived"
157
+ ? await inspectIdentity(derived.input)
158
+ : identityModule.codexCliInstallationRefusal(derived.reason, "selected");
159
+ } else {
160
+ report = await inspectIdentity(parsed.attest);
161
+ }
162
+ } catch {
163
+ // Filesystem/native errors can contain the explicit private paths. The
164
+ // read-only inspector normally returns a refusal; unexpected errors stay redacted.
165
+ throw new Error("Installation identity inspection failed");
166
+ }
167
+ printData(report, parsed.json, identitySummary(report));
168
+ return;
169
+ }
75
170
  const trustedInspectionEnv = trustedNodeLauncherContext()?.codexCliInspectionEnv;
76
171
  const inspectionDeps: CodexCliInstallProvenanceDeps = trustedInspectionEnv
77
172
  && trustedInspectionEnv.managerRoots !== null ? {
@@ -965,6 +965,60 @@ export function decideStartWithLiveOwner(input: {
965
965
  return input.ocxService === "1" ? "service-stay-out" : "refuse";
966
966
  }
967
967
 
968
+ /** What `chooseListenPort` does when the preferred port stayed busy through prefer-retry. */
969
+ export type BusyPreferredPortDecision =
970
+ | "hop"
971
+ | "refuse-live-proxy"
972
+ | "service-stay-out"
973
+ | "refuse-unidentified-holder";
974
+
975
+ /**
976
+ * Pure decision for a soft `start` whose preferred port is busy and whose only remaining
977
+ * option is an ephemeral port.
978
+ *
979
+ * The hop exists so a first start is not defeated by a port this machine happens to be
980
+ * using. What it must never be is a silent answer to "someone is already here": a start
981
+ * that hops takes over this home's pid and runtime-port records and re-points Codex at
982
+ * itself, so hopping past a live opencodex leaves two proxies running and the editor
983
+ * talking to the one the user did not mean (#5004). The hop path never asked who held the
984
+ * port, and `findLiveProxy` returning null — a stale record, a probe that lost a race, a
985
+ * loopback family split — was enough to reach it.
986
+ *
987
+ * So the decision is made from the holder's own answer rather than from this home's
988
+ * bookkeeping, and both outcomes stop the start. An opencodex answer is the duplicate this
989
+ * closes. A holder that does not answer as opencodex is deliberately NOT called foreign:
990
+ * an identity probe returns the same nothing for a foreign server, an unreachable one, and
991
+ * one that lost a race, so all the start can honestly say is that the port it was told to
992
+ * use is taken by something it could not identify — and moving to an arbitrary port is the
993
+ * one response that hides that from the user while re-pointing Codex. An explicit
994
+ * `--port` never reaches here (`findAvailablePort` refuses the fallback instead), and a
995
+ * configured port of 0 is a request for an ephemeral port, not a collision.
996
+ *
997
+ * Service-wrapper context keeps the semantics `decideStartWithLiveOwner` gives it: a
998
+ * healthy proxy on the port means the port is served, and the wrapper's
999
+ * `if %ERRORLEVEL% NEQ 0` loop must see a zero exit rather than respawn every 5 seconds.
1000
+ */
1001
+ export function decideBusyPreferredPort(input: {
1002
+ preferredPort: number;
1003
+ selectedPort: number;
1004
+ hardPin: boolean;
1005
+ holderIsOpencodex: boolean;
1006
+ ocxService: string | undefined;
1007
+ }): BusyPreferredPortDecision {
1008
+ // Port 0 (or an unusable preference) asked the OS to choose; nothing was taken away.
1009
+ if (input.preferredPort <= 0) return "hop";
1010
+ // The preferred port was obtained — no hop happened, nothing to decide.
1011
+ if (input.selectedPort === input.preferredPort) return "hop";
1012
+ // Defensive: a hard pin cannot reach a different port, and if it ever did, the pin is
1013
+ // the user's explicit instruction and not something to answer with a refusal here.
1014
+ if (input.hardPin) return "hop";
1015
+ if (input.holderIsOpencodex) {
1016
+ // Same sentinel rule as decideStartWithLiveOwner: only the exact "1" is service context.
1017
+ return input.ocxService === "1" ? "service-stay-out" : "refuse-live-proxy";
1018
+ }
1019
+ return "refuse-unidentified-holder";
1020
+ }
1021
+
968
1022
  export function resolveDispatchCommand(command: string | undefined): string | undefined {
969
1023
  if (command === undefined) return undefined;
970
1024
  if (Object.prototype.hasOwnProperty.call(commandRunners, command)) return command;