sneakoscope 7.3.0 → 7.6.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 (283) hide show
  1. package/README.md +129 -10
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/cli/codex-app-command.js +1 -1
  5. package/dist/cli/command-manifest-lite.js +4 -1
  6. package/dist/cli/command-registry.js +2 -0
  7. package/dist/cli/install-helpers-codex-lb-chain.js +42 -0
  8. package/dist/cli/install-helpers-codex-lb-config.js +114 -9
  9. package/dist/cli/install-helpers-install-support.js +6 -16
  10. package/dist/cli/install-helpers.js +788 -600
  11. package/dist/cli/router.js +30 -2
  12. package/dist/commands/codex-app.js +37 -1
  13. package/dist/commands/codex-lb.js +491 -300
  14. package/dist/config/skills-manifest.json +57 -71
  15. package/dist/core/align/align-route.js +127 -0
  16. package/dist/core/codex-app/codex-app-fast-ui-repair.js +8 -4
  17. package/dist/core/codex-app/codex-app-restart.js +83 -0
  18. package/dist/core/codex-app/codex-app-ui-state-snapshot.js +8 -4
  19. package/dist/core/codex-app/desktop-routing-snapshot.js +1 -1
  20. package/dist/core/codex-app/menubar/constants.js +5 -0
  21. package/dist/core/codex-app/menubar/generation-transaction-journal.js +1 -3
  22. package/dist/core/codex-app/menubar/installer.js +18 -0
  23. package/dist/core/codex-app/multi-provider-router.js +22 -10
  24. package/dist/core/codex-app/openrouter-activate.js +26 -1
  25. package/dist/core/codex-app.js +118 -26
  26. package/dist/core/codex-control/codex-app-server-runtime-env.js +86 -0
  27. package/dist/core/codex-control/codex-app-server-v2-client.js +78 -8
  28. package/dist/core/codex-control/codex-task-runner.js +2 -2
  29. package/dist/core/codex-lb/capability-runner.js +204 -0
  30. package/dist/core/codex-lb/capability-types.js +1 -0
  31. package/dist/core/codex-lb/codex-lb-env.js +9 -13
  32. package/dist/core/codex-lb/codex-lb-setup.js +45 -8
  33. package/dist/core/codex-lb/codex-lb-tool-catalog.js +52 -74
  34. package/dist/core/codex-lb/codex-lb-tool-output-recovery.js +1 -1
  35. package/dist/core/codex-lb/desktop-auth-invariant.js +185 -0
  36. package/dist/core/codex-lb/desktop-bridge/client.js +34 -0
  37. package/dist/core/codex-lb/desktop-bridge/header-policy.js +106 -0
  38. package/dist/core/codex-lb/desktop-bridge/http-forward.js +101 -0
  39. package/dist/core/codex-lb/desktop-bridge/index.js +10 -0
  40. package/dist/core/codex-lb/desktop-bridge/launchd.js +81 -0
  41. package/dist/core/codex-lb/desktop-bridge/location-rewrite.js +27 -0
  42. package/dist/core/codex-lb/desktop-bridge/security.js +274 -0
  43. package/dist/core/codex-lb/desktop-bridge/server.js +179 -0
  44. package/dist/core/codex-lb/desktop-bridge/state.js +119 -0
  45. package/dist/core/codex-lb/desktop-bridge/types.js +16 -0
  46. package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +151 -0
  47. package/dist/core/codex-lb/desktop-center-credentials.js +189 -0
  48. package/dist/core/codex-lb/desktop-controller.js +1410 -0
  49. package/dist/core/codex-lb/desktop-mode.js +32 -0
  50. package/dist/core/codex-lb/desktop-real-evidence.js +168 -0
  51. package/dist/core/codex-lb/desktop-service.js +613 -0
  52. package/dist/core/codex-lb/legacy-migration.js +575 -0
  53. package/dist/core/codex-lb/migration-receipt.js +246 -0
  54. package/dist/core/codex-lb/probes/auxiliary-surfaces-probe.js +60 -0
  55. package/dist/core/codex-lb/probes/bridge-probe.js +75 -0
  56. package/dist/core/codex-lb/probes/catalog-probe.js +128 -0
  57. package/dist/core/codex-lb/probes/computer-use-probe.js +50 -0
  58. package/dist/core/codex-lb/probes/image-generation-probe.js +47 -0
  59. package/dist/core/codex-lb/probes/probe-evidence.js +39 -0
  60. package/dist/core/codex-lb/probes/voice-realtime-probe.js +44 -0
  61. package/dist/core/codex-lb/trusted-deep-evidence.js +403 -0
  62. package/dist/core/codex-native/core-skill-manifest.js +22 -6
  63. package/dist/core/codex-runtime/codex-desktop-config-policy.js +2 -1
  64. package/dist/core/commands/align-command.js +220 -0
  65. package/dist/core/commands/image-ux-review-command.js +280 -68
  66. package/dist/core/commands/naruto-command.js +257 -8
  67. package/dist/core/commands/ppt-command.js +2 -2
  68. package/dist/core/commands/telegram-command.js +402 -34
  69. package/dist/core/commands/triwiki-command.js +11 -1
  70. package/dist/core/commands/triwiki-graph-command.js +160 -0
  71. package/dist/core/commands/wiki-command.js +100 -20
  72. package/dist/core/doctor/skill-legacy-surface.js +1 -3
  73. package/dist/core/errors/message.js +3 -0
  74. package/dist/core/evidence/evidence-freshness.js +14 -0
  75. package/dist/core/feature-fixtures.js +9 -1
  76. package/dist/core/feature-registry.js +5 -3
  77. package/dist/core/fsx.js +7 -3
  78. package/dist/core/hooks-runtime/codex-commit-hooks-selftest.js +82 -0
  79. package/dist/core/hooks-runtime/context-graph-freshness-preflight.js +113 -0
  80. package/dist/core/hooks-runtime/hook-context.js +15 -2
  81. package/dist/core/hooks-runtime/managed-skill-generation-heal.js +46 -0
  82. package/dist/core/hooks-runtime/naruto-terminal-finalization.js +1 -0
  83. package/dist/core/hooks-runtime/official-subagent-lifecycle.js +1 -1
  84. package/dist/core/hooks-runtime/tool-output-quarantine.js +1 -1
  85. package/dist/core/hooks-runtime.js +33 -89
  86. package/dist/core/image-ux-review/callout-extraction.js +6 -0
  87. package/dist/core/image-ux-review/imagegen-adapter.js +64 -23
  88. package/dist/core/image-ux-review/real-callout-extractor.js +1 -0
  89. package/dist/core/image-ux-review.js +230 -12
  90. package/dist/core/imagegen/codex-lb-imagegen-target.js +21 -7
  91. package/dist/core/imagegen/imagegen-capability.js +10 -4
  92. package/dist/core/imagegen/require-imagegen.js +29 -5
  93. package/dist/core/init/skills.js +25 -13
  94. package/dist/core/init.js +1 -1
  95. package/dist/core/install/installed-package-smoke.js +157 -4
  96. package/dist/core/managed-assets/managed-assets-manifest.js +2 -1
  97. package/dist/core/managed-path-safety.js +8 -10
  98. package/dist/core/mcp-config/backup.js +2 -4
  99. package/dist/core/mcp-config/config-reader.js +2 -4
  100. package/dist/core/mcp-config/scope.js +3 -5
  101. package/dist/core/naruto/context-graph-advisor-scope.js +242 -0
  102. package/dist/core/naruto/context-graph-advisor.js +0 -0
  103. package/dist/core/perf/release-latency-slo.js +12 -6
  104. package/dist/core/pipeline-internals/runtime-core.js +78 -0
  105. package/dist/core/pipeline-internals/runtime-gates.js +5 -2
  106. package/dist/core/policy/latest-version-guidance.js +113 -0
  107. package/dist/core/ppt.js +8 -8
  108. package/dist/core/preflight/parallel-preflight-engine.js +1 -1
  109. package/dist/core/proof/auto-finalize.js +98 -6
  110. package/dist/core/proof/route-proof-gate.js +12 -1
  111. package/dist/core/proof/route-proof-policy.js +8 -8
  112. package/dist/core/release/gate-manifest.js +32 -1
  113. package/dist/core/release/npm-stage-contract.js +21 -0
  114. package/dist/core/release/package-size-budget.js +2 -2
  115. package/dist/core/release/release-gate-contract.js +5 -0
  116. package/dist/core/release/release-proof-io.js +1 -3
  117. package/dist/core/release/release-real-contract.js +5 -4
  118. package/dist/core/release/release-stamp-proof.js +1 -3
  119. package/dist/core/release/stage-publish.js +27 -0
  120. package/dist/core/remote/audit-idempotency.js +50 -18
  121. package/dist/core/remote/codex-public-activity.js +283 -0
  122. package/dist/core/remote/index.js +1 -0
  123. package/dist/core/remote/ssh-worker-client.js +4 -1
  124. package/dist/core/remote/worker.js +206 -27
  125. package/dist/core/research/research-adversarial-review.js +1 -3
  126. package/dist/core/research/research-claim-synthesizer.js +1 -3
  127. package/dist/core/research/research-falsification-runner.js +1 -3
  128. package/dist/core/research/research-source-evidence.js +1 -3
  129. package/dist/core/responses-stream.js +65 -19
  130. package/dist/core/routes/constants.js +1 -1
  131. package/dist/core/routes/design-policy.js +7 -2
  132. package/dist/core/routes/dollar-manifest-lite.js +2 -4
  133. package/dist/core/routes/evidence.js +1 -1
  134. package/dist/core/routes.js +30 -10
  135. package/dist/core/safety/command-contract/contracts.js +2 -0
  136. package/dist/core/safety/command-contract/types.js +1 -1
  137. package/dist/core/search/context-graph-seeds.js +205 -0
  138. package/dist/core/search/context.js +0 -0
  139. package/dist/core/search/provider.js +2 -2
  140. package/dist/core/secret-redaction.js +6 -0
  141. package/dist/core/subagents/naruto-help-contract.js +2 -1
  142. package/dist/core/subagents/naruto-host-credentials.js +161 -0
  143. package/dist/core/subagents/official-subagent-preparation.js +4 -1
  144. package/dist/core/subagents/official-subagent-prompt.js +44 -2
  145. package/dist/core/subagents/official-subagent-runner.js +26 -8
  146. package/dist/core/subagents/triwiki-attention.js +135 -123
  147. package/dist/core/telegram/bot-api.js +85 -10
  148. package/dist/core/telegram/center-snapshot.js +718 -0
  149. package/dist/core/telegram/config.js +158 -31
  150. package/dist/core/telegram/hub.js +288 -33
  151. package/dist/core/telegram/index.js +2 -0
  152. package/dist/core/telegram/ingress-spool.js +419 -0
  153. package/dist/core/telegram/ledgers.js +188 -29
  154. package/dist/core/telegram/messages.js +26 -6
  155. package/dist/core/telegram/owner-lock.js +10 -7
  156. package/dist/core/telegram/runtime-projection.js +38 -3
  157. package/dist/core/telegram/runtime.js +298 -63
  158. package/dist/core/telegram/service.js +137 -8
  159. package/dist/core/telegram/setup.js +291 -73
  160. package/dist/core/text/strings.js +3 -0
  161. package/dist/core/triwiki/code-pack.js +12 -120
  162. package/dist/core/triwiki/context-graph/compiler/cache-key.js +277 -0
  163. package/dist/core/triwiki/context-graph/compiler/extract.js +104 -0
  164. package/dist/core/triwiki/context-graph/compiler/freshness.js +108 -0
  165. package/dist/core/triwiki/context-graph/compiler/index.js +237 -0
  166. package/dist/core/triwiki/context-graph/compiler/merge.js +0 -0
  167. package/dist/core/triwiki/context-graph/compiler/serialize.js +115 -0
  168. package/dist/core/triwiki/context-graph/contracts.js +236 -0
  169. package/dist/core/triwiki/context-graph/extractors/code/builders.js +150 -0
  170. package/dist/core/triwiki/context-graph/extractors/code/declarations.js +161 -0
  171. package/dist/core/triwiki/context-graph/extractors/code/index.js +265 -0
  172. package/dist/core/triwiki/context-graph/extractors/code/inventory.js +237 -0
  173. package/dist/core/triwiki/context-graph/extractors/code/module-graph.js +252 -0
  174. package/dist/core/triwiki/context-graph/extractors/code/modules.js +79 -0
  175. package/dist/core/triwiki/context-graph/extractors/code/references.js +184 -0
  176. package/dist/core/triwiki/context-graph/extractors/code/selection.js +100 -0
  177. package/dist/core/triwiki/context-graph/extractors/code/ts-config.js +161 -0
  178. package/dist/core/triwiki/context-graph/extractors/code/types.js +18 -0
  179. package/dist/core/triwiki/context-graph/extractors/evidence/claims.js +289 -0
  180. package/dist/core/triwiki/context-graph/extractors/evidence/index.js +54 -0
  181. package/dist/core/triwiki/context-graph/extractors/evidence/proof-index.js +213 -0
  182. package/dist/core/triwiki/context-graph/extractors/evidence/proofs.js +266 -0
  183. package/dist/core/triwiki/context-graph/extractors/evidence/redaction.js +177 -0
  184. package/dist/core/triwiki/context-graph/extractors/evidence/shared.js +0 -0
  185. package/dist/core/triwiki/context-graph/extractors/evidence/sources.js +154 -0
  186. package/dist/core/triwiki/context-graph/extractors/index.js +7 -0
  187. package/dist/core/triwiki/context-graph/extractors/topology/commands.js +207 -0
  188. package/dist/core/triwiki/context-graph/extractors/topology/gate-edges.js +201 -0
  189. package/dist/core/triwiki/context-graph/extractors/topology/gates.js +182 -0
  190. package/dist/core/triwiki/context-graph/extractors/topology/globs.js +124 -0
  191. package/dist/core/triwiki/context-graph/extractors/topology/index.js +92 -0
  192. package/dist/core/triwiki/context-graph/extractors/topology/routes.js +135 -0
  193. package/dist/core/triwiki/context-graph/extractors/topology/shared.js +215 -0
  194. package/dist/core/triwiki/context-graph/graph-index.js +143 -0
  195. package/dist/core/triwiki/context-graph/ids.js +0 -0
  196. package/dist/core/triwiki/context-graph/lint/index.js +0 -0
  197. package/dist/core/triwiki/context-graph/lint/rules.js +232 -0
  198. package/dist/core/triwiki/context-graph/lint/warnings.js +93 -0
  199. package/dist/core/triwiki/context-graph/paths.js +116 -0
  200. package/dist/core/triwiki/context-graph/profiles.js +78 -0
  201. package/dist/core/triwiki/context-graph/projections/anchors.js +91 -0
  202. package/dist/core/triwiki/context-graph/projections/attention.js +58 -0
  203. package/dist/core/triwiki/context-graph/projections/code-pack-workspace.js +62 -0
  204. package/dist/core/triwiki/context-graph/projections/code-pack.js +144 -0
  205. package/dist/core/triwiki/context-graph/projections/index.js +7 -0
  206. package/dist/core/triwiki/context-graph/projections/module-view.js +140 -0
  207. package/dist/core/triwiki/context-graph/projections/node-summary.js +118 -0
  208. package/dist/core/triwiki/context-graph/projections/pack-contract.js +6 -0
  209. package/dist/core/triwiki/context-graph/query/explain.js +0 -0
  210. package/dist/core/triwiki/context-graph/query/index.js +178 -0
  211. package/dist/core/triwiki/context-graph/query/load.js +116 -0
  212. package/dist/core/triwiki/context-graph/query/pack.js +117 -0
  213. package/dist/core/triwiki/context-graph/query/rank.js +142 -0
  214. package/dist/core/triwiki/context-graph/query/ranking-config.js +64 -0
  215. package/dist/core/triwiki/context-graph/query/seeds.js +241 -0
  216. package/dist/core/triwiki/context-graph/query/snapshot-cache.js +72 -0
  217. package/dist/core/triwiki/context-graph/query/traverse.js +154 -0
  218. package/dist/core/triwiki/context-graph/query-types.js +29 -0
  219. package/dist/core/triwiki/context-graph/store/compile-lock.js +21 -0
  220. package/dist/core/triwiki/context-graph/store/event-log.js +48 -0
  221. package/dist/core/triwiki/context-graph/store/fragment-cache.js +85 -0
  222. package/dist/core/triwiki/context-graph/store/graph-status.js +85 -0
  223. package/dist/core/triwiki/context-graph/store/snapshot-store.js +155 -0
  224. package/dist/core/triwiki/triwiki-proof-bank-index-repair.js +115 -0
  225. package/dist/core/triwiki/triwiki-proof-bank-index-store.js +290 -0
  226. package/dist/core/triwiki/triwiki-proof-bank-index.js +126 -0
  227. package/dist/core/triwiki/triwiki-proof-bank.js +7 -1
  228. package/dist/core/verification/context-graph-affected.js +271 -0
  229. package/dist/core/version.js +1 -1
  230. package/dist/native/sks-menubar/Sources/AlertFactory.swift +11 -3
  231. package/dist/native/sks-menubar/Sources/AppDelegate.swift +9 -0
  232. package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +25 -3
  233. package/dist/native/sks-menubar/Sources/ControlKit.swift +19 -8
  234. package/dist/native/sks-menubar/Sources/DiagnosticsViewController.swift +1 -2
  235. package/dist/native/sks-menubar/Sources/MCPServersViewController.swift +45 -17
  236. package/dist/native/sks-menubar/Sources/OverviewSummary.swift +162 -0
  237. package/dist/native/sks-menubar/Sources/OverviewViewController.swift +136 -190
  238. package/dist/native/sks-menubar/Sources/ProcessClient.swift +107 -11
  239. package/dist/native/sks-menubar/Sources/ProvidersMultiProvider.swift +5 -1
  240. package/dist/native/sks-menubar/Sources/ProvidersOpenRouter.swift +16 -4
  241. package/dist/native/sks-menubar/Sources/ProvidersRoleModels.swift +4 -1
  242. package/dist/native/sks-menubar/Sources/ProvidersRoutingTruth.swift +131 -0
  243. package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +288 -341
  244. package/dist/native/sks-menubar/Sources/RemoteTelegramCenterState.swift +447 -0
  245. package/dist/native/sks-menubar/Sources/RemoteTelegramRendering.swift +484 -0
  246. package/dist/native/sks-menubar/Sources/RemoteTelegramViewController.swift +364 -115
  247. package/dist/native/sks-menubar/Sources/SettingsViewController.swift +73 -12
  248. package/dist/native/sks-menubar/Sources/SidebarItem.swift +3 -3
  249. package/dist/native/sks-menubar/Sources/StatusItemController.swift +13 -13
  250. package/dist/native/sks-menubar/Sources/TelegramHubSupervisor.swift +469 -0
  251. package/dist/native/sks-menubar/Sources/UpdatesViewController.swift +1 -6
  252. package/dist/native/sks-menubar/Tests/AppRuntimeTestSupport.swift +12 -0
  253. package/dist/native/sks-menubar/Tests/NativeTelegramRegressionRunner.swift +55 -0
  254. package/dist/native/sks-menubar/Tests/ProcessClientTests.swift +33 -0
  255. package/dist/native/sks-menubar/Tests/ProvidersViewControllerTests.swift +142 -0
  256. package/dist/native/sks-menubar/Tests/RemoteTelegramCenterStateTests.swift +56 -0
  257. package/dist/native/sks-menubar/Tests/TelegramHubSupervisorTests.swift +55 -0
  258. package/dist/scripts/codex-lb-catalog-passthrough-check.js +120 -0
  259. package/dist/scripts/codex-lb-desktop-auth-invariant-check.js +169 -0
  260. package/dist/scripts/codex-lb-desktop-bridge-check.js +394 -0
  261. package/dist/scripts/codex-lb-desktop-bridge-latency-check.js +196 -0
  262. package/dist/scripts/codex-lb-desktop-capabilities-check.js +287 -0
  263. package/dist/scripts/codex-lb-desktop-real-evidence-check.js +123 -0
  264. package/dist/scripts/codex-lb-fast-mode-truth-check.js +10 -3
  265. package/dist/scripts/codex-lb-fast-ui-preservation-check.js +81 -40
  266. package/dist/scripts/codex-lb-gpt56-fast-profile-check.js +99 -37
  267. package/dist/scripts/context-graph-check.js +191 -0
  268. package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +1 -1
  269. package/dist/scripts/gate-policy-audit-check.js +1 -1
  270. package/dist/scripts/helper-dedup-check.js +0 -2
  271. package/dist/scripts/installed-package-smoke-check.js +4 -1
  272. package/dist/scripts/latest-version-guidance-check.js +74 -0
  273. package/dist/scripts/release-metadata-check.js +21 -4
  274. package/dist/scripts/release-real-check.js +7 -0
  275. package/dist/scripts/ux-review-imagegen-blackbox-check.js +25 -1
  276. package/dist/scripts/ux-review-run-wires-imagegen-check.js +9 -1
  277. package/package.json +8 -3
  278. package/release-gates.v2.json +176 -1
  279. package/runtime-required-scripts.json +30 -0
  280. package/schemas/triwiki/context-graph.schema.json +119 -0
  281. package/dist/cli/install-helpers-codex-lb-selftest-chain.js +0 -112
  282. package/dist/cli/install-helpers-codex-lb-selftest.js +0 -481
  283. package/dist/core/triwiki/code-index-scanner.js +0 -309
@@ -6,7 +6,6 @@ import { stdin as input, stdout as output } from 'node:process';
6
6
  import { ensureDir, exists, globalSksRoot, packageRoot, PACKAGE_VERSION, readText, runProcess, tmpdir, which, writeTextAtomic } from '../core/fsx.js';
7
7
  import { createRequestedScopeContract } from '../core/safety/requested-scope-contract.js';
8
8
  import { guardedPackageInstall, guardContextForRoute } from '../core/safety/mutation-guard.js';
9
- import { EMPTY_CODEX_INFO, getCodexInfo } from '../core/codex-adapter.js';
10
9
  import { formatHarnessConflictReport, llmHarnessCleanupPrompt, scanHarnessConflicts } from '../core/harness-conflicts.js';
11
10
  import { initProject, installGlobalSkills } from '../core/init.js';
12
11
  import { context7ConfigToml, DOLLAR_SKILL_NAMES, GETDESIGN_REFERENCE, hasContext7ConfigText, RECOMMENDED_SKILLS } from '../core/routes.js';
@@ -16,18 +15,21 @@ import { restartCodexApp } from '../core/codex-app/codex-app-restart.js';
16
15
  import { cleanupMacLaunchSecretEnvironment } from '../core/codex-app/menubar/index.js';
17
16
  import { recordCodexLbHealthEvent } from '../core/codex-lb-circuit.js';
18
17
  import { loadCodexLbEnv, writeCodexLbKeychain, codexLbMetadataPath } from '../core/codex-lb/codex-lb-env.js';
19
- import { codexLbToolCatalogPath, ensureCodexLbToolCatalog } from '../core/codex-lb/codex-lb-tool-catalog.js';
18
+ import { codexLbToolCatalogPath } from '../core/codex-lb/codex-lb-tool-catalog.js';
20
19
  import { codexLbToolOutputRecoveryNotChecked, codexLbToolOutputRecoveryNotSelected, codexLbToolOutputRecoveryOverrideAcknowledged, probeCodexLbToolOutputRecovery } from '../core/codex-lb/codex-lb-tool-output-recovery.js';
21
20
  import { GLM_CODEX_CONFIG_PROFILE_ID, GLM_CODEX_CONFIG_PROVIDER_ID, GLM_CODEX_CONFIG_REASONING_PROFILES, GLM_52_OPENROUTER_MODEL } from '../core/codex-app/openrouter-provider.js';
22
21
  import { buildCodexLbSetupPlan, codexLbPersistenceSummary, installCodexLbShellProfileSnippet, selectedCodexLbPersistenceModes } from '../core/codex-lb/codex-lb-setup.js';
22
+ import { DEFAULT_CODEX_LB_GATEWAY_AUTH_TRANSPORT, modeRequiresChatGptOAuth, parseCodexLbDesktopMode, parseCodexLbGatewayAuthTransport } from '../core/codex-lb/desktop-mode.js';
23
+ import { assertDesktopAuthUnchangedBySks, captureCodexAuthSnapshot } from '../core/codex-lb/desktop-auth-invariant.js';
23
24
  import { extractTomlTable, writeCodexConfigGuarded } from '../core/codex/codex-config-guard.js';
24
- import { ensureGlobalCodexFastModeDuringInstall, ensureTrailingNewline, normalizeCodexFastModeUiConfig, removeTopLevelTomlKeyIfValue, safeWriteCodexConfigToml, upsertTopLevelTomlString, upsertTomlTable } from '../core/codex-runtime/codex-desktop-config-policy.js';
25
+ import { ensureGlobalCodexFastModeDuringInstall, ensureTrailingNewline, normalizeCodexFastModeUiConfig, removeTopLevelTomlKeyIfValue, safeWriteCodexConfigToml, upsertTomlTable } from '../core/codex-runtime/codex-desktop-config-policy.js';
25
26
  import { runPostinstallGlobalDoctorAndMarkPending } from '../core/update/update-migration-state.js';
26
27
  import { repairCodexImagegen } from '../core/doctor/imagegen-repair.js';
27
- import { canAskYesNo, compareVersions, hasCodexUnstableFeatureWarningSuppression, hasDeprecatedCodexHooksFeatureFlag, hasTopLevelCodexModeLock, isProjectSetupCandidate } from './install-tool-helpers.js';
28
+ import { canAskYesNo, compareVersions, hasCodexUnstableFeatureWarningSuppression, hasDeprecatedCodexHooksFeatureFlag, isProjectSetupCandidate } from './install-tool-helpers.js';
28
29
  import { checkCodexLbResponseChain } from './install-helpers-codex-lb-chain.js';
29
- import { CODEX_LB_CANONICAL_FAST_SERVICE_TIER, CODEX_LB_PROVIDER_ENV_KEY, CODEX_LB_PROVIDER_NAME, askPostinstallQuestion, codexAuthChatgptBackupPath, codexAuthPath, codexLbConfigPath, codexLbEnvPath, hasTopLevelCodexLbSelected, normalizeCodexLbBaseUrl, parseCodexLbEnvKey, redactSecretText } from './install-helpers-codex-lb-shared.js';
30
- import { appliedCodexLbPersistenceModes, captureCodexLbSetupWriteState, codexLbSharedOpenAiRoutingState, detectCodexLbSetupDrift, ensureGlobalCodexAppGlmProfile, ensureStoredOpenRouterProviderDuringInstall, parseCodexLbEnvBaseUrl, parseCodexSharedLoginApiKey, removeCodexLbSharedOpenAiRouting, sha256Text, shellSingleQuote, upsertCodexAppGlmConfig, upsertCodexLbConfig, upsertCodexLbSharedOpenAiRouting } from './install-helpers-codex-lb-config.js';
30
+ import { CODEX_LB_CANONICAL_FAST_SERVICE_TIER, CODEX_LB_PROVIDER_ENV_KEY, askPostinstallQuestion, codexAuthChatgptBackupPath, codexAuthPath, codexLbConfigPath, codexLbEnvPath, hasTopLevelCodexLbSelected, normalizeCodexLbBaseUrl, parseCodexLbEnvKey, redactSecretText } from './install-helpers-codex-lb-shared.js';
31
+ import { CODEX_LB_DESKTOP_BRIDGE_MARKER, CODEX_LB_DESKTOP_COMPAT_MARKER, appliedCodexLbPersistenceModes, captureCodexLbSetupWriteState, codexLbSharedOpenAiRoutingState, detectCodexLbSetupDrift, ensureGlobalCodexAppGlmProfile, ensureStoredOpenRouterProviderDuringInstall, parseCodexLbEnvBaseUrl, removeCodexLbSharedOpenAiRouting, removeCodexLbManagedDesktopConfig, sha256Text, shellSingleQuote, upsertCodexAppGlmConfig, upsertCodexLbConfig, upsertCodexLbCliProviderConfig, upsertCodexLbCompatDesktopConfig, upsertCodexLbNativeDesktopConfig, upsertCodexLbSharedOpenAiRouting } from './install-helpers-codex-lb-config.js';
32
+ import { detectLegacyCodexLbDesktopState } from '../core/codex-lb/legacy-migration.js';
31
33
  import { ensureCodexImagegenDuringInstall, ensureGlobalCodexSkillsDuringInstall, ensureGlobalContext7DuringInstall, ensureGlobalGetdesignSkillDuringInstall, ensureSksCommandDuringInstall } from './install-helpers-install-support.js';
32
34
  import { escapeRegExp } from '../core/text/regex.js';
33
35
  function packagedSksEntrypoint() {
@@ -35,24 +37,31 @@ function packagedSksEntrypoint() {
35
37
  }
36
38
  export async function postinstall({ bootstrap, args = [] }) {
37
39
  const installRoot = path.resolve(process.env.INIT_CWD || process.cwd());
38
- const conflictScan = await scanHarnessConflicts(installRoot);
39
- if (conflictScan.hard_block) {
40
- await postinstallHarnessConflictNotice(conflictScan);
40
+ console.log('\nSKS installed.');
41
+ await restoreInstalledPackageBuildStamp();
42
+ if (!postinstallExternalMutationsAllowed(process.env)) {
43
+ console.log('Automatic bootstrap was not run; npm install leaves project, HOME, Codex, and global SKS state unchanged by default.');
44
+ console.log('Next: run `sks bootstrap` when you are ready to initialize SKS.');
45
+ console.log('Dependency diagnostics remain explicit: sks deps check');
46
+ console.log('Explicit lifecycle opt-in: SKS_POSTINSTALL_BOOTSTRAP=1 npm i -g sneakoscope');
47
+ console.log('Optional Homebrew/npm-global tool repair remains off unless SKS_POSTINSTALL_AUTO_INSTALL_CLI_TOOLS=1 is also set.');
48
+ const reason = process.env.SKS_POSTINSTALL_NO_BOOTSTRAP === '1'
49
+ ? 'SKS_POSTINSTALL_NO_BOOTSTRAP=1'
50
+ : 'explicit opt-in required (SKS_POSTINSTALL_BOOTSTRAP=1)';
51
+ console.log(`Reason: ${reason}.`);
41
52
  return;
42
53
  }
43
- const codexLbConfigSnapshot = await capturePostinstallCodexLbConfigSnapshot();
54
+ let codexLbConfigSnapshot = null;
55
+ let reconcileCodexLb = false;
44
56
  try {
45
- console.log('\nSKS installed.');
46
- try {
47
- const stampLib = await import('../scripts/lib/ensure-dist-fresh.js');
48
- await fsp.access(stampLib.distStampPath).catch(async () => {
49
- await fsp.writeFile(stampLib.distStampPath, `${JSON.stringify(stampLib.buildStampPayload(), null, 2)}\n`);
50
- console.log('SKS build stamp: restored for update self-verification (npm packages ship without it).');
51
- });
52
- }
53
- catch (err) {
54
- console.log(`SKS build stamp: could not restore (${err?.message || err}); \`sks update\` self-verification may report dist_stamp missing.`);
57
+ const bootstrapDecision = await postinstallBootstrapDecision(installRoot);
58
+ const conflictScan = await scanHarnessConflicts(installRoot);
59
+ if (conflictScan.hard_block) {
60
+ await postinstallHarnessConflictNotice(conflictScan);
61
+ return;
55
62
  }
63
+ codexLbConfigSnapshot = await capturePostinstallCodexLbConfigSnapshot();
64
+ reconcileCodexLb = true;
56
65
  const shim = await ensureSksCommandDuringInstall();
57
66
  if (shim.status === 'present')
58
67
  console.log(`SKS command: available (${shim.command ?? 'unknown'}).`);
@@ -77,19 +86,7 @@ export async function postinstall({ bootstrap, args = [] }) {
77
86
  console.log(`Context7 MCP: skipped (${context7Install.reason}).`);
78
87
  else if (context7Install.status === 'failed')
79
88
  console.log(`Context7 MCP: auto setup failed. Run \`sks context7 setup --scope global\` or \`sks setup\`. ${context7Install.error || ''}`.trim());
80
- const fastModeRepair = await ensureGlobalCodexFastModeDuringInstall();
81
- if (fastModeRepair.status === 'updated')
82
- console.log(`Codex App Fast mode: updated ${fastModeRepair.config_path}${fastModeRepair.backup_path ? ` (backup ${fastModeRepair.backup_path})` : ''}.`);
83
- else if (fastModeRepair.status === 'present')
84
- console.log('Codex App Fast mode: config already compatible.');
85
- else if (fastModeRepair.status === 'unparseable_config_preserved')
86
- console.log(`Codex App Fast mode: existing ${fastModeRepair.config_path} is not valid TOML — left untouched, backed up to ${fastModeRepair.backup_path}. Run \`sks doctor --fix\` to recover it.`);
87
- else if (fastModeRepair.status === 'skipped_unsafe_rewrite')
88
- console.log(`Codex App Fast mode: skipped (managed rewrite would not parse; ${fastModeRepair.config_path} left untouched).`);
89
- else if (fastModeRepair.status === 'skipped')
90
- console.log(`Codex App Fast mode: skipped (${fastModeRepair.reason}).`);
91
- else if (fastModeRepair.status === 'failed')
92
- console.log(`Codex App Fast mode: auto repair failed. Run \`sks setup\`. ${fastModeRepair.error || ''}`.trim());
89
+ console.log('Codex App Fast mode: left unchanged during install; use the explicit Fast-mode command to change it.');
93
90
  const openRouterProviderRepair = await ensureStoredOpenRouterProviderDuringInstall();
94
91
  if (openRouterProviderRepair.status === 'updated')
95
92
  console.log('OpenRouter provider: repaired for the stored key (credentials and active model were preserved).');
@@ -108,7 +105,12 @@ export async function postinstall({ bootstrap, args = [] }) {
108
105
  console.log(`Codex App Image Gen: blocked; run \`sks doctor --fix\`. ${(imagegenRepair.blockers || []).join(', ')}`.trim());
109
106
  else if (imagegenRepair.status === 'skipped')
110
107
  console.log(`Codex App Image Gen: skipped (${imagegenRepair.reason}).`);
111
- const postinstallDoctor = await runPostinstallGlobalDoctorAndMarkPending().catch((err) => ({
108
+ const postinstallDoctor = await runPostinstallGlobalDoctorAndMarkPending({
109
+ env: {
110
+ ...process.env,
111
+ SKS_POSTINSTALL_GLOBAL_DOCTOR: '0'
112
+ }
113
+ }).catch((err) => ({
112
114
  ok: false,
113
115
  doctor: null,
114
116
  pending: null,
@@ -116,7 +118,7 @@ export async function postinstall({ bootstrap, args = [] }) {
116
118
  warnings: []
117
119
  }));
118
120
  if (postinstallDoctor.ok)
119
- console.log('SKS update migration: global Doctor ran; project receipt will be finalized on first normal command.');
121
+ console.log('SKS update migration: pending receipt recorded; no global Doctor repair ran during install.');
120
122
  else
121
123
  console.log(`SKS update migration: global Doctor did not complete; first normal command will retry. ${(postinstallDoctor.blockers || []).join(', ')}`.trim());
122
124
  const postinstallRetention = await runPostinstallProjectRetentionCleanup(installRoot);
@@ -150,35 +152,43 @@ export async function postinstall({ bootstrap, args = [] }) {
150
152
  else if (globalSkills.status === 'failed')
151
153
  console.log(`Codex App global $ skills: auto setup failed. Run \`sks doctor --fix\`. ${globalSkills.error || ''}`.trim());
152
154
  const getdesignSkill = await ensureGlobalGetdesignSkillDuringInstall();
153
- if (getdesignSkill.status === 'installed')
154
- console.log('getdesign Codex skill: installed.');
155
- else if (getdesignSkill.status === 'present')
156
- console.log('getdesign Codex skill: already available.');
157
- else if (getdesignSkill.status === 'skills_cli_missing')
158
- console.log(`getdesign Codex skill: skills CLI missing; generated getdesign-reference skill is installed. Later run \`${getdesignSkill.install}\` if the skills CLI is available.`);
159
- else if (getdesignSkill.status === 'skipped')
160
- console.log(`getdesign Codex skill: skipped (${getdesignSkill.reason}).`);
161
- else if (getdesignSkill.status === 'failed')
162
- console.log(`getdesign Codex skill: auto setup failed; generated getdesign-reference skill remains available. ${getdesignSkill.error || ''}`.trim());
163
- const bootstrapDecision = await postinstallBootstrapDecision(installRoot);
164
- if (bootstrapDecision.run) {
165
- console.log(`SKS bootstrap: ${bootstrapDecision.reason}.`);
166
- await runPostinstallBootstrap(installRoot, bootstrap);
167
- return;
168
- }
169
- console.log('\nNext:');
170
- console.log(' sks bootstrap');
171
- console.log(`\nSKS bootstrap was not run automatically: ${bootstrapDecision.reason}.`);
172
- console.log('This initializes the current project, installs SKS Codex App skills, verifies Codex App/Context7 readiness, and checks Zellij runtime dependencies.');
173
- console.log('Dependency repair: sks bootstrap --yes, sks deps check --yes, or sks --mad --yes. Postinstall reports missing CLI tools but does not mutate Homebrew/npm globals unless SKS_POSTINSTALL_AUTO_INSTALL_CLI_TOOLS=1 is set.');
174
- console.log('Open runtime after readiness is green: sks\n');
155
+ console.log(`getdesign Codex skill: not installed automatically; generated getdesign-reference skill is available. To install the upstream skill manually, review commit ${getdesignSkill.reviewed_ref} and run \`${getdesignSkill.install}\`.`);
156
+ console.log(`SKS bootstrap: ${bootstrapDecision.reason}.`);
157
+ await runPostinstallBootstrap(installRoot, bootstrap, bootstrapDecision);
158
+ return;
175
159
  }
176
160
  catch (err) {
177
161
  console.log(`\nSKS postinstall: a setup step did not complete; installation continues. Run \`sks doctor --fix\` afterward. (${err?.message || err})`);
178
162
  }
179
163
  finally {
180
- await restorePostinstallCodexLbConfigSnapshot(codexLbConfigSnapshot).catch(() => { });
181
- await reportPostinstallCodexLbAuth(codexLbConfigSnapshot).catch(() => { });
164
+ if (reconcileCodexLb) {
165
+ await restorePostinstallCodexLbConfigSnapshot(codexLbConfigSnapshot).catch(() => { });
166
+ await reportPostinstallCodexLbAuth(codexLbConfigSnapshot).catch(() => { });
167
+ }
168
+ }
169
+ }
170
+ function postinstallExternalMutationsAllowed(env) {
171
+ return env.SKS_POSTINSTALL_BOOTSTRAP === '1' && env.SKS_POSTINSTALL_NO_BOOTSTRAP !== '1';
172
+ }
173
+ async function restoreInstalledPackageBuildStamp() {
174
+ try {
175
+ const stampLib = await import('../scripts/lib/ensure-dist-fresh.js');
176
+ const root = path.resolve(packageRoot());
177
+ const rawStamp = String(stampLib.distStampPath || '').trim();
178
+ if (!rawStamp)
179
+ throw new Error('dist_stamp_path_missing');
180
+ const stamp = path.resolve(rawStamp);
181
+ const relative = path.relative(root, stamp);
182
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
183
+ throw new Error('dist_stamp_outside_package_root');
184
+ }
185
+ await fsp.access(stamp).catch(async () => {
186
+ await fsp.writeFile(stamp, `${JSON.stringify(stampLib.buildStampPayload(), null, 2)}\n`);
187
+ console.log('SKS build stamp: restored inside the installed package for update self-verification.');
188
+ });
189
+ }
190
+ catch (err) {
191
+ console.log(`SKS build stamp: could not restore (${err?.message || err}); \`sks update\` self-verification may report dist_stamp missing.`);
182
192
  }
183
193
  }
184
194
  async function runPostinstallProjectRetentionCleanup(root) {
@@ -212,9 +222,7 @@ async function runPostinstallProjectRetentionCleanup(root) {
212
222
  }
213
223
  }
214
224
  async function reportPostinstallCodexLbAuth(snapshot = null) {
215
- const preserveActiveCodexLb = snapshot?.selected === true && snapshot?.codex_lb_key_in_shared_auth === true;
216
225
  const codexLbAuth = await ensureCodexLbAuthDuringInstall({
217
- forceCodexLbApiKeyAuth: preserveActiveCodexLb,
218
226
  ...(snapshot?.secret_api_key ? { apiKey: snapshot.secret_api_key } : {}),
219
227
  ...(snapshot?.base_url ? { baseUrl: snapshot.base_url } : {})
220
228
  });
@@ -230,25 +238,18 @@ async function reportPostinstallCodexLbAuth(snapshot = null) {
230
238
  console.log('codex-lb auth: stored key missing. Run `sks codex-lb setup --host <domain> --api-key-stdin` to repair.');
231
239
  else if (codexLbAuth.status === 'missing_base_url')
232
240
  console.log('codex-lb auth: stored key has no recoverable base URL. Run `sks codex-lb reconfigure --host <domain> --api-key-stdin` once.');
241
+ else if (codexLbAuth.status === 'legacy_migration_required')
242
+ console.log('codex-lb legacy Desktop auth routing was left unchanged. Migrate explicitly with `sks codex-lb migrate-legacy-desktop --restart-app`.');
233
243
  else if (codexLbAuth.status === 'not_configured')
234
- console.log('codex-lb (optional multi-account load balancer): not configured — opt in anytime with `sks codex-lb setup` (your choice; never applied automatically, never edits your Codex config without it). Swap key later: `sks codex-lb set-key`; switch auth: `sks codex-lb use-oauth` / `use-codex-lb`.');
244
+ console.log('codex-lb (optional multi-account load balancer): not configured — use `sks codex-lb setup`, then choose `use-desktop-full`, `use-cli`, or `disable` explicitly.');
235
245
  else if (codexLbAuth.status && codexLbAuth.status !== 'not_configured')
236
246
  console.log(`codex-lb auth: repair skipped (${codexLbAuth.status}${codexLbAuth.error ? `: ${codexLbAuth.error}` : ''}).`);
237
247
  const reconcile = codexLbAuth.auth_reconcile;
238
- if (reconcile?.status === 'oauth_preserved') {
239
- console.log(`codex-lb auth: ChatGPT OAuth preserved as backup; run \`sks codex-lb use-codex-lb\` to switch the App to codex-lb API-key auth (backup at ${reconcile.backup_path ?? 'unknown'}).`);
240
- }
241
- else if (reconcile?.status === 'oauth_restored') {
242
- console.log(`codex-lb auth: restored ChatGPT OAuth from ${reconcile.backup_path ?? 'unknown'} and cleared codex-lb provider selection.`);
248
+ if (reconcile?.status === 'oauth_untouched') {
249
+ console.log('codex-lb auth: ChatGPT OAuth/shared Codex auth was left byte-for-byte unchanged.');
243
250
  }
244
- else if (reconcile?.status === 'apikey_forced') {
245
- console.log(`codex-lb auth: forced API-key auth.json for CLI-only use (OAuth backup at ${reconcile.backup_path ?? 'unknown'}).`);
246
- }
247
- else if (reconcile?.status === 'backup_only') {
248
- console.log(`codex-lb auth: detected ChatGPT OAuth tokens in auth.json. OAuth backup written to ${reconcile.backup_path ?? 'unknown'}; auth.json left untouched because SKS_CODEX_LB_NO_AUTH_RECONCILE=1.`);
249
- }
250
- else if (reconcile?.status === 'failed') {
251
- console.log(`codex-lb auth: ChatGPT OAuth reconciliation could not complete (${reconcile.reason || 'unknown'}${reconcile.error ? `: ${reconcile.error}` : ''}). Run \`sks codex-lb repair\` to retry.`);
251
+ else if (reconcile?.status === 'legacy_migration_required') {
252
+ console.log('codex-lb auth: legacy shared-auth routing requires `sks codex-lb migrate-legacy-desktop --restart-app`; background install will not migrate it.');
252
253
  }
253
254
  if (codexLbAuth.base_url && codexLbAuth.codex_lb?.env_key_configured && canAskYesNo() && process.env.SKS_SKIP_CODEX_LB_KEY_PROMPT !== '1') {
254
255
  const changeKey = (await askPostinstallQuestion('codex-lb key changed? Update now? [y/N] ')).trim();
@@ -294,23 +295,17 @@ function shouldAskPostinstallQuestion() {
294
295
  export async function postinstallBootstrapDecision(root) {
295
296
  if (process.env.SKS_POSTINSTALL_NO_BOOTSTRAP === '1')
296
297
  return { run: false, reason: 'SKS_POSTINSTALL_NO_BOOTSTRAP=1' };
297
- if (process.env.SKS_POSTINSTALL_BOOTSTRAP === '0')
298
- return { run: false, reason: 'SKS_POSTINSTALL_BOOTSTRAP=0' };
298
+ if (process.env.SKS_POSTINSTALL_BOOTSTRAP !== '1') {
299
+ return { run: false, reason: 'explicit opt-in required (SKS_POSTINSTALL_BOOTSTRAP=1)' };
300
+ }
299
301
  const installRoot = path.resolve(root || process.cwd());
300
302
  const candidate = await isProjectSetupCandidate(installRoot);
301
303
  const target = candidate ? installRoot : globalSksRoot();
302
- if (process.env.SKS_POSTINSTALL_BOOTSTRAP === '1')
303
- return { run: true, target, reason: 'forced by SKS_POSTINSTALL_BOOTSTRAP=1' };
304
- if (process.env.npm_config_global === 'true' && candidate) {
305
- return { run: true, target: globalSksRoot(), reason: 'global install: bootstrapping global SKS runtime only (run `sks setup` inside a project to initialize it)' };
306
- }
307
- if (candidate)
308
- return { run: true, target, reason: 'auto-running sks setup --bootstrap --install-scope global --force' };
309
- return { run: true, target, reason: 'no project marker found; auto-running global SKS runtime bootstrap' };
304
+ return { run: true, target, reason: 'forced by SKS_POSTINSTALL_BOOTSTRAP=1' };
310
305
  }
311
- async function runPostinstallBootstrap(root, bootstrap) {
306
+ async function runPostinstallBootstrap(root, bootstrap, selectedDecision) {
312
307
  const previousCwd = process.cwd();
313
- const decision = await postinstallBootstrapDecision(root);
308
+ const decision = selectedDecision || await postinstallBootstrapDecision(root);
314
309
  const target = path.resolve(decision.target || root || previousCwd);
315
310
  await ensureDir(target);
316
311
  process.chdir(target);
@@ -321,147 +316,324 @@ async function runPostinstallBootstrap(root, bootstrap) {
321
316
  process.chdir(previousCwd);
322
317
  }
323
318
  }
319
+ export async function configureCodexLbDesktopRouting(opts) {
320
+ const home = opts.home || process.env.HOME || os.homedir();
321
+ const configPath = opts.configPath || codexLbConfigPath(home);
322
+ const authPath = opts.authPath || codexAuthPath(home);
323
+ const mode = parseCodexLbDesktopMode(opts.mode);
324
+ const gatewayAuthTransport = parseCodexLbGatewayAuthTransport(opts.gatewayAuthTransport || DEFAULT_CODEX_LB_GATEWAY_AUTH_TRANSPORT);
325
+ const authBefore = await captureCodexAuthSnapshot({ home, authPath });
326
+ const identityPlane = authBefore.mode === 'chatgpt_oauth' || authBefore.mode === 'mixed'
327
+ ? 'chatgpt_oauth'
328
+ : 'unavailable';
329
+ const base = {
330
+ schema: 'sks.codex-lb-desktop-routing.v1',
331
+ mode,
332
+ gateway_auth_transport: gatewayAuthTransport,
333
+ identity_plane: identityPlane,
334
+ auth_mutated: false,
335
+ config_path: configPath,
336
+ auth_path: authPath,
337
+ auth_before: authBefore
338
+ };
339
+ if (mode === 'cli-provider') {
340
+ return {
341
+ ...base,
342
+ ok: false,
343
+ status: 'invalid_desktop_routing_input',
344
+ routing_plane: 'unchanged',
345
+ oauth_preserved: true,
346
+ auth_after: authBefore,
347
+ blockers: ['cli_provider_is_not_a_desktop_routing_mode']
348
+ };
349
+ }
350
+ if (modeRequiresChatGptOAuth(mode) && identityPlane !== 'chatgpt_oauth') {
351
+ return {
352
+ ...base,
353
+ ok: false,
354
+ status: 'desktop_oauth_required',
355
+ routing_plane: 'unchanged',
356
+ oauth_preserved: false,
357
+ auth_after: authBefore,
358
+ blockers: ['chatgpt_oauth_required_for_desktop']
359
+ };
360
+ }
361
+ if (mode === 'desktop-dual-auth-compat' && gatewayAuthTransport !== 'x-codex-lb-api-key') {
362
+ return {
363
+ ...base,
364
+ ok: false,
365
+ status: 'desktop_gateway_auth_transport_unsupported',
366
+ routing_plane: 'unchanged',
367
+ oauth_preserved: true,
368
+ auth_after: authBefore,
369
+ blockers: ['desktop_compat_cannot_use_authorization_bearer_without_replacing_oauth']
370
+ };
371
+ }
372
+ if (mode === 'desktop-native-bridge' && (!opts.bridgeBaseUrl || !opts.remoteBaseUrl)) {
373
+ return {
374
+ ...base,
375
+ ok: false,
376
+ status: 'invalid_desktop_routing_input',
377
+ routing_plane: 'unchanged',
378
+ oauth_preserved: true,
379
+ auth_after: authBefore,
380
+ blockers: [
381
+ ...(!opts.bridgeBaseUrl ? ['missing_bridge_base_url'] : []),
382
+ ...(!opts.remoteBaseUrl ? ['missing_remote_base_url'] : [])
383
+ ]
384
+ };
385
+ }
386
+ if (mode === 'desktop-dual-auth-compat' && !opts.remoteBaseUrl) {
387
+ return {
388
+ ...base,
389
+ ok: false,
390
+ status: 'invalid_desktop_routing_input',
391
+ routing_plane: 'unchanged',
392
+ oauth_preserved: true,
393
+ auth_after: authBefore,
394
+ blockers: ['missing_remote_base_url']
395
+ };
396
+ }
397
+ const current = await readText(configPath, '');
398
+ let next;
399
+ try {
400
+ next = mode === 'desktop-native-bridge'
401
+ ? upsertCodexLbNativeDesktopConfig(current, {
402
+ bridgeBaseUrl: String(opts.bridgeBaseUrl),
403
+ remoteBaseUrl: String(opts.remoteBaseUrl)
404
+ })
405
+ : mode === 'desktop-dual-auth-compat'
406
+ ? upsertCodexLbCompatDesktopConfig(current, { remoteBaseUrl: String(opts.remoteBaseUrl) })
407
+ : removeCodexLbManagedDesktopConfig(current);
408
+ }
409
+ catch (error) {
410
+ return {
411
+ ...base,
412
+ ok: false,
413
+ status: 'failed',
414
+ routing_plane: 'unchanged',
415
+ oauth_preserved: true,
416
+ auth_after: authBefore,
417
+ blockers: ['desktop_config_conflict'],
418
+ error: error instanceof Error ? error.message : String(error)
419
+ };
420
+ }
421
+ const write = await safeWriteCodexConfigToml(configPath, current, next, 'codex-lb-desktop-routing', { verifyUnchangedBeforeWrite: true });
422
+ if (!write.ok) {
423
+ return {
424
+ ...base,
425
+ ok: false,
426
+ status: 'failed',
427
+ routing_plane: 'unchanged',
428
+ oauth_preserved: true,
429
+ auth_after: authBefore,
430
+ backup_path: write.backup_path,
431
+ blockers: ['desktop_config_write_failed'],
432
+ error: write.status
433
+ };
434
+ }
435
+ const authAfter = await captureCodexAuthSnapshot({ home, authPath });
436
+ try {
437
+ await assertDesktopAuthUnchangedBySks(authBefore, authAfter);
438
+ }
439
+ catch (error) {
440
+ const writtenText = await readText(configPath, '');
441
+ const rollbackWrite = await safeWriteCodexConfigToml(configPath, writtenText, current, 'codex-lb-desktop-routing-auth-invariant-rollback', { verifyUnchangedBeforeWrite: true });
442
+ return {
443
+ ...base,
444
+ ok: false,
445
+ status: 'failed',
446
+ routing_plane: 'unchanged',
447
+ oauth_preserved: false,
448
+ auth_after: authAfter,
449
+ backup_path: write.backup_path,
450
+ rollback: {
451
+ ok: rollbackWrite.ok,
452
+ status: rollbackWrite.status,
453
+ config_restored: rollbackWrite.ok
454
+ },
455
+ blockers: ['desktop_auth_byte_invariant_failed', ...(rollbackWrite.ok ? [] : ['desktop_config_rollback_failed'])],
456
+ error: error instanceof Error ? error.message : String(error)
457
+ };
458
+ }
459
+ const routingPlane = mode === 'desktop-native-bridge'
460
+ ? 'desktop_native_bridge'
461
+ : mode === 'desktop-dual-auth-compat'
462
+ ? 'desktop_compat_provider'
463
+ : 'disabled';
464
+ return {
465
+ ...base,
466
+ ok: true,
467
+ status: mode === 'disabled'
468
+ ? 'desktop_routing_disabled'
469
+ : write.changed === true
470
+ ? 'desktop_routing_configured'
471
+ : 'desktop_routing_present',
472
+ routing_plane: routingPlane,
473
+ oauth_preserved: true,
474
+ auth_after: authAfter,
475
+ backup_path: write.backup_path,
476
+ blockers: []
477
+ };
478
+ }
479
+ export async function configureCodexLbCliProvider(opts) {
480
+ const home = opts.home || process.env.HOME || os.homedir();
481
+ const configPath = opts.configPath || codexLbConfigPath(home);
482
+ const authPath = opts.authPath || codexAuthPath(home);
483
+ const authBefore = await captureCodexAuthSnapshot({ home, authPath });
484
+ const current = await readText(configPath, '');
485
+ const next = upsertCodexLbCliProviderConfig(current, {
486
+ remoteBaseUrl: opts.remoteBaseUrl,
487
+ ...(opts.selectGlobally === undefined ? {} : { selectGlobally: opts.selectGlobally })
488
+ });
489
+ const write = await safeWriteCodexConfigToml(configPath, current, next, 'codex-lb-cli-provider', { verifyUnchangedBeforeWrite: true });
490
+ const authAfter = await captureCodexAuthSnapshot({ home, authPath });
491
+ try {
492
+ await assertDesktopAuthUnchangedBySks(authBefore, authAfter);
493
+ }
494
+ catch {
495
+ return {
496
+ schema: 'sks.codex-lb-cli-provider.v1',
497
+ ok: false,
498
+ status: 'auth_changed_during_cli_provider_write',
499
+ mode: 'cli-provider',
500
+ identity_plane: 'unchanged',
501
+ routing_plane: 'cli_provider',
502
+ oauth_preserved: false,
503
+ auth_mutated: false,
504
+ config_path: configPath,
505
+ auth_path: authPath,
506
+ auth_before: authBefore,
507
+ auth_after: authAfter,
508
+ blockers: ['shared_auth_byte_invariant_failed'],
509
+ backup_path: write.backup_path
510
+ };
511
+ }
512
+ return {
513
+ schema: 'sks.codex-lb-cli-provider.v1',
514
+ ok: write.ok,
515
+ status: write.ok ? (write.changed ? 'configured' : 'present') : write.status,
516
+ mode: 'cli-provider',
517
+ identity_plane: 'unchanged',
518
+ routing_plane: 'cli_provider',
519
+ oauth_preserved: true,
520
+ auth_mutated: false,
521
+ config_path: configPath,
522
+ auth_path: authPath,
523
+ auth_before: authBefore,
524
+ auth_after: authAfter,
525
+ blockers: write.ok ? [] : ['cli_provider_config_write_failed'],
526
+ backup_path: write.backup_path
527
+ };
528
+ }
529
+ function hasTopLevelMarker(text, marker) {
530
+ const topLevel = String(text || '').split(/\n\s*\[/)[0] || '';
531
+ return topLevel.split(/\r?\n/).some((line) => line.trim() === marker);
532
+ }
533
+ function managedCodexLbDesktopMode(text) {
534
+ if (hasTopLevelMarker(text, CODEX_LB_DESKTOP_BRIDGE_MARKER))
535
+ return 'desktop-native-bridge';
536
+ if (hasTopLevelMarker(text, CODEX_LB_DESKTOP_COMPAT_MARKER))
537
+ return 'desktop-dual-auth-compat';
538
+ return null;
539
+ }
540
+ function codexLbRoutingPlane(mode) {
541
+ if (mode === 'desktop-native-bridge')
542
+ return 'desktop_native_bridge';
543
+ if (mode === 'desktop-dual-auth-compat')
544
+ return 'desktop_compat_provider';
545
+ return 'cli_provider';
546
+ }
324
547
  async function capturePostinstallCodexLbConfigSnapshot(home = process.env.HOME || os.homedir()) {
325
548
  const configPath = codexLbConfigPath(home);
326
549
  const envPath = codexLbEnvPath(home);
327
550
  const authPath = codexAuthPath(home);
328
551
  const config = await readText(configPath, '');
329
552
  const envText = await readText(envPath, '');
330
- const authExisted = await exists(authPath);
331
- const authText = authExisted ? await readText(authPath, '') : '';
553
+ const auth = await captureCodexAuthSnapshot({ home, authPath });
332
554
  const envLoad = await loadCodexLbEnv({ home, envPath });
333
555
  const envKey = envLoad.secret_api_key || '';
334
556
  const providerConfigured = /\[model_providers\.codex-lb\]/.test(config);
335
557
  const baseUrl = envLoad.base_url || codexLbProviderBaseUrl(config) || parseCodexLbEnvBaseUrl(envText);
336
- if (!envKey && !providerConfigured && !authExisted)
558
+ const desktopMode = managedCodexLbDesktopMode(config);
559
+ const bridgeBaseUrl = desktopMode === 'desktop-native-bridge'
560
+ ? topLevelTomlString(config, 'openai_base_url')
561
+ : '';
562
+ if (!envKey && !providerConfigured && !baseUrl && !desktopMode)
337
563
  return null;
338
564
  return {
339
565
  config_path: configPath,
340
566
  env_path: envPath,
341
567
  auth_path: authPath,
342
568
  base_url: baseUrl ? normalizeCodexLbBaseUrl(baseUrl) : null,
569
+ bridge_base_url: bridgeBaseUrl || null,
570
+ desktop_mode: desktopMode,
343
571
  selected: hasTopLevelCodexLbSelected(config),
344
- auth_mode: codexAuthModeSummary(authText).mode,
345
- codex_lb_key_in_shared_auth: Boolean(envKey && parseCodexAuthApiKey(authText) === envKey),
572
+ auth_mode: auth.mode,
573
+ auth_sha256: auth.sha256,
346
574
  credential_source: envLoad.source,
347
575
  credential_fingerprint: envLoad.api_key.fingerprint,
348
576
  credential_binding: envLoad.credential_binding,
349
- secret_api_key: envKey || null,
350
- auth_existed: authExisted,
351
- auth_text: authText
577
+ secret_api_key: envKey || null
352
578
  };
353
579
  }
354
580
  async function restorePostinstallCodexLbConfigSnapshot(snapshot) {
355
581
  if (!snapshot)
356
582
  return { status: 'skipped', reason: 'no_snapshot' };
357
583
  let configRestored = false;
584
+ let configStatus = 'present';
358
585
  if (snapshot.base_url) {
359
586
  const current = await readText(snapshot.config_path, '');
360
- const next = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(current, snapshot.base_url, snapshot.selected === true));
361
- const alreadyOk = next === ensureTrailingNewline(current) && codexLbProviderBaseUrl(current);
362
- if (!alreadyOk) {
363
- const safeWrite = await safeWriteCodexConfigToml(snapshot.config_path, current, next, 'codex-lb-restore');
364
- configRestored = safeWrite.ok && safeWrite.changed === true;
587
+ let next = current;
588
+ try {
589
+ if (snapshot.desktop_mode === 'desktop-native-bridge' && snapshot.bridge_base_url) {
590
+ next = upsertCodexLbNativeDesktopConfig(current, {
591
+ bridgeBaseUrl: snapshot.bridge_base_url,
592
+ remoteBaseUrl: snapshot.base_url
593
+ });
594
+ }
595
+ else if (snapshot.desktop_mode === 'desktop-dual-auth-compat') {
596
+ next = upsertCodexLbCompatDesktopConfig(current, { remoteBaseUrl: snapshot.base_url });
597
+ }
598
+ else if (snapshot.selected !== true) {
599
+ next = upsertCodexLbCliProviderConfig(current, {
600
+ remoteBaseUrl: snapshot.base_url,
601
+ selectGlobally: false
602
+ });
603
+ }
604
+ else {
605
+ configStatus = 'legacy_migration_required';
606
+ }
365
607
  }
366
- }
367
- let authRestored = false;
368
- if (snapshot.auth_existed && snapshot.auth_text && snapshot.auth_text.trim()) {
369
- const currentAuthExists = await exists(snapshot.auth_path);
370
- const currentAuthText = currentAuthExists ? await readText(snapshot.auth_path, '') : '';
371
- const activeCodexLbAuthChanged = snapshot.selected === true
372
- && snapshot.codex_lb_key_in_shared_auth === true
373
- && snapshot.secret_api_key
374
- && parseCodexAuthApiKey(currentAuthText) !== snapshot.secret_api_key;
375
- if (!currentAuthExists || !currentAuthText.trim() || activeCodexLbAuthChanged) {
376
- await ensureDir(path.dirname(snapshot.auth_path));
377
- await writeTextAtomic(snapshot.auth_path, snapshot.auth_text, { mode: 0o600 });
378
- await fsp.chmod(snapshot.auth_path, 0o600).catch(() => { });
379
- authRestored = true;
608
+ catch (error) {
609
+ return {
610
+ status: 'failed',
611
+ reason: 'managed_routing_restore_conflict',
612
+ config_path: snapshot.config_path,
613
+ auth_path: snapshot.auth_path,
614
+ config_restored: false,
615
+ auth_restored: false,
616
+ error: error instanceof Error ? error.message : String(error)
617
+ };
618
+ }
619
+ if (configStatus !== 'legacy_migration_required' && ensureTrailingNewline(next) !== ensureTrailingNewline(current)) {
620
+ const safeWrite = await safeWriteCodexConfigToml(snapshot.config_path, current, next, 'codex-lb-safe-routing-restore', { verifyUnchangedBeforeWrite: true });
621
+ configRestored = safeWrite.ok && safeWrite.changed === true;
622
+ configStatus = safeWrite.ok ? (configRestored ? 'restored' : 'present') : safeWrite.status;
380
623
  }
381
624
  }
625
+ const authAfter = await captureCodexAuthSnapshot({
626
+ home: path.dirname(path.dirname(snapshot.auth_path)),
627
+ authPath: snapshot.auth_path
628
+ });
629
+ const authUnchanged = snapshot.auth_sha256 === authAfter.sha256;
382
630
  return {
383
- status: configRestored || authRestored ? 'restored' : 'present',
631
+ status: configStatus,
384
632
  config_path: snapshot.config_path,
385
633
  auth_path: snapshot.auth_path,
386
634
  config_restored: configRestored,
387
- auth_restored: authRestored
388
- };
389
- }
390
- async function ensureCodexLbToolCatalogSelection(input, opts = {}) {
391
- const codexHome = opts.codexHome || path.join(input.home, '.codex');
392
- const catalogPath = codexLbToolCatalogPath(codexHome);
393
- const current = await readText(input.configPath, '');
394
- const selected = hasTopLevelCodexLbSelected(current);
395
- const canBindCatalog = selected || opts.allowUnselectedCatalogWrite === true;
396
- const existingCatalogPath = topLevelTomlString(current, 'model_catalog_json');
397
- const { isSksManagedCatalogPath, resolveCatalogPath } = await import('../core/codex-app/multi-provider-router-support.js');
398
- const existingCatalogResolved = existingCatalogPath
399
- ? resolveCatalogPath(existingCatalogPath, { home: input.home, configPath: input.configPath })
400
- : null;
401
- const existingCatalogReplaceable = !existingCatalogResolved
402
- || (isSksManagedCatalogPath(existingCatalogResolved, { home: input.home })
403
- && (selected || opts.willSelectProvider === true));
404
- if (canBindCatalog && existingCatalogResolved && existingCatalogResolved !== path.resolve(catalogPath) && !existingCatalogReplaceable) {
405
- return {
406
- schema: 'sks.codex-lb-tool-catalog-selection.v1',
407
- ok: false,
408
- required: true,
409
- status: 'user_catalog_conflict',
410
- path: catalogPath,
411
- configured_path: existingCatalogPath,
412
- config_changed: false,
413
- selected,
414
- blockers: ['codex_lb_user_model_catalog_conflict']
415
- };
416
- }
417
- let hostname = '';
418
- try {
419
- hostname = new URL(input.baseUrl).hostname.toLowerCase();
420
- }
421
- catch { }
422
- const reservedFixtureHost = /(?:^|\.)(?:test|invalid|example)$/.test(hostname);
423
- if (reservedFixtureHost && typeof opts.toolCatalogFetch !== 'function') {
424
- return {
425
- schema: 'sks.codex-lb-tool-catalog-selection.v1',
426
- ok: true,
427
- required: false,
428
- status: 'skipped_reserved_host',
429
- path: catalogPath,
430
- config_changed: false,
431
- selected,
432
- blockers: []
433
- };
434
- }
435
- const catalog = await ensureCodexLbToolCatalog({
436
- codexHome,
437
- baseUrl: input.baseUrl,
438
- apiKey: input.apiKey,
439
- ...(typeof opts.toolCatalogFetch === 'function' ? { fetchImpl: opts.toolCatalogFetch } : {}),
440
- timeoutMs: Number(opts.toolCatalogTimeoutMs || 5000),
441
- force: opts.forceToolCatalog === true
442
- });
443
- if (!catalog.ok || !canBindCatalog) {
444
- return {
445
- ...catalog,
446
- schema: 'sks.codex-lb-tool-catalog-selection.v1',
447
- config_changed: false,
448
- selected
449
- };
450
- }
451
- const next = ensureTrailingNewline(upsertTopLevelTomlString(current, 'model_catalog_json', catalog.path));
452
- if (next === ensureTrailingNewline(current)) {
453
- return { ...catalog, schema: 'sks.codex-lb-tool-catalog-selection.v1', config_changed: false, selected };
454
- }
455
- const safeWrite = await safeWriteCodexConfigToml(input.configPath, current, next, 'codex-lb-tool-catalog');
456
- return {
457
- ...catalog,
458
- schema: 'sks.codex-lb-tool-catalog-selection.v1',
459
- ok: catalog.ok && safeWrite.ok,
460
- status: safeWrite.ok ? catalog.status : safeWrite.status,
461
- config_changed: safeWrite.ok && safeWrite.changed === true,
462
- backup_path: safeWrite.backup_path,
463
- selected,
464
- blockers: safeWrite.ok ? catalog.blockers : [...new Set([...(catalog.blockers || []), 'codex_lb_tool_catalog_config_write_failed'])]
635
+ auth_restored: false,
636
+ auth_unchanged: authUnchanged
465
637
  };
466
638
  }
467
639
  export async function configureCodexLb(opts = {}) {
@@ -471,7 +643,12 @@ export async function configureCodexLb(opts = {}) {
471
643
  const rawHost = String(opts.host || opts.baseUrl || '');
472
644
  const baseUrl = normalizeCodexLbBaseUrl(rawHost);
473
645
  const apiKey = String(opts.apiKey || '').trim();
474
- const useDefaultProvider = opts.useDefaultProvider !== false;
646
+ const initialConfig = await readText(configPath, '');
647
+ const preservedDesktopMode = managedCodexLbDesktopMode(initialConfig);
648
+ const preservedLegacySelection = hasTopLevelCodexLbSelected(initialConfig) && !preservedDesktopMode;
649
+ const requestedDesktopMode = parseCodexLbDesktopMode(opts.desktopMode || 'cli-provider');
650
+ const desktopMode = preservedDesktopMode || 'cli-provider';
651
+ const useDefaultProvider = desktopMode === 'desktop-dual-auth-compat' || preservedLegacySelection;
475
652
  const writeEnvFile = opts.writeEnvFile !== false;
476
653
  const storeKeychain = opts.storeKeychain === true || opts.keychain === true;
477
654
  const syncLaunchctl = opts.syncLaunchctl === true || opts.syncLaunchEnv === true;
@@ -479,7 +656,8 @@ export async function configureCodexLb(opts = {}) {
479
656
  const setupAnswers = {
480
657
  host_or_base_url: rawHost,
481
658
  api_key_source: opts.apiKeySource || 'stdin',
482
- use_as_default_provider: useDefaultProvider,
659
+ desktop_mode: 'cli-provider',
660
+ gateway_auth_transport: (opts.gatewayAuthTransport || DEFAULT_CODEX_LB_GATEWAY_AUTH_TRANSPORT),
483
661
  write_env_file: writeEnvFile,
484
662
  store_keychain: storeKeychain,
485
663
  sync_launchctl: syncLaunchctl,
@@ -526,16 +704,63 @@ export async function configureCodexLb(opts = {}) {
526
704
  ? ['codex-lb base URL uses http outside localhost; prefer https or pass an explicit allow flag in the calling surface.']
527
705
  : [];
528
706
  const beforeState = await captureCodexLbSetupWriteState({ home, configPath, envPath, shellProfile });
707
+ const authBefore = await captureCodexAuthSnapshot({ home, authPath: opts.authPath || codexAuthPath(home) });
529
708
  const appliedActions = [];
530
709
  await ensureDir(path.dirname(configPath));
531
710
  const current = await readText(configPath, '');
532
- const providerOnly = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(current, baseUrl, false), {
533
- forceFastMode: opts.forceFastMode !== false
534
- });
711
+ let providerOnly = current;
712
+ try {
713
+ if (desktopMode === 'desktop-native-bridge') {
714
+ const bridgeBaseUrl = topLevelTomlString(current, 'openai_base_url');
715
+ if (!bridgeBaseUrl) {
716
+ return {
717
+ ok: false,
718
+ status: 'managed_desktop_bridge_missing_base_url',
719
+ mode: desktopMode,
720
+ config_path: configPath,
721
+ env_path: envPath
722
+ };
723
+ }
724
+ providerOnly = upsertCodexLbNativeDesktopConfig(current, {
725
+ bridgeBaseUrl,
726
+ remoteBaseUrl: baseUrl
727
+ });
728
+ }
729
+ else if (desktopMode === 'desktop-dual-auth-compat') {
730
+ providerOnly = upsertCodexLbCompatDesktopConfig(current, { remoteBaseUrl: baseUrl });
731
+ }
732
+ else if (!preservedLegacySelection) {
733
+ providerOnly = upsertCodexLbCliProviderConfig(current, {
734
+ remoteBaseUrl: baseUrl,
735
+ selectGlobally: false
736
+ });
737
+ }
738
+ }
739
+ catch (error) {
740
+ return {
741
+ ok: false,
742
+ status: 'managed_desktop_routing_conflict',
743
+ mode: desktopMode,
744
+ config_path: configPath,
745
+ env_path: envPath,
746
+ error: error instanceof Error ? error.message : String(error)
747
+ };
748
+ }
535
749
  const safeWrite = await safeWriteCodexConfigToml(configPath, current, providerOnly, 'codex-lb');
536
750
  if (!safeWrite.ok)
537
751
  return { ok: false, status: safeWrite.status, config_path: configPath, env_path: envPath, backup_path: safeWrite.backup_path };
538
- appliedActions.push({ type: 'write_config_provider', target: configPath, ok: true, backup_path: safeWrite.backup_path });
752
+ appliedActions.push({
753
+ type: preservedLegacySelection
754
+ ? 'preserve_legacy_desktop_routing'
755
+ : desktopMode === 'desktop-native-bridge'
756
+ ? 'preserve_desktop_native_bridge'
757
+ : desktopMode === 'desktop-dual-auth-compat'
758
+ ? 'preserve_desktop_compat_provider'
759
+ : 'write_cli_provider',
760
+ target: configPath,
761
+ ok: true,
762
+ backup_path: safeWrite.backup_path
763
+ });
539
764
  if (writeEnvFile) {
540
765
  await writeTextAtomic(envPath, `export CODEX_LB_BASE_URL=${shellSingleQuote(baseUrl)}\nexport CODEX_LB_API_KEY=${shellSingleQuote(apiKey)}\n`, { mode: 0o600 });
541
766
  await fsp.chmod(envPath, 0o600).catch(() => { });
@@ -543,47 +768,20 @@ export async function configureCodexLb(opts = {}) {
543
768
  }
544
769
  process.env.CODEX_LB_BASE_URL = baseUrl;
545
770
  process.env.CODEX_LB_API_KEY = apiKey;
546
- let toolCatalog = await ensureCodexLbToolCatalogSelection({ home, configPath, baseUrl, apiKey }, { ...opts, allowUnselectedCatalogWrite: true, willSelectProvider: useDefaultProvider === true });
547
- if (toolCatalog.config_changed || toolCatalog.status === 'repaired' || toolCatalog.status === 'cached_compatible') {
548
- appliedActions.push({ type: 'write_model_tool_catalog', target: toolCatalog.path, ok: toolCatalog.ok === true, status: toolCatalog.status });
549
- }
550
- const catalogReadyForSelection = toolCatalog.ok === true
551
- && (toolCatalog.required === false || ['repaired', 'cached_compatible', 'cached_compatible_after_refresh_failed', 'cached_compatible_after_refresh_rejected', 'skipped_reserved_host', 'present'].includes(String(toolCatalog.status || '')));
552
- let providerSelected = false;
553
- if (useDefaultProvider && catalogReadyForSelection) {
554
- const beforeSelect = await readText(configPath, '');
555
- const selectedConfig = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(beforeSelect, baseUrl, true), {
556
- forceFastMode: opts.forceFastMode !== false
557
- });
558
- const selectWrite = await safeWriteCodexConfigToml(configPath, beforeSelect, selectedConfig, 'codex-lb-select');
559
- if (!selectWrite.ok) {
560
- return {
561
- ok: false,
562
- status: selectWrite.status || 'provider_selection_failed',
563
- config_path: configPath,
564
- env_path: envPath,
565
- backup_path: selectWrite.backup_path,
566
- tool_catalog: toolCatalog,
567
- tool_output_recovery: toolOutputRecovery,
568
- error: 'codex_lb_provider_selection_write_failed'
569
- };
570
- }
571
- providerSelected = true;
572
- appliedActions.push({ type: 'select_default_provider', target: configPath, ok: true, backup_path: selectWrite.backup_path });
573
- toolCatalog = await ensureCodexLbToolCatalogSelection({ home, configPath, baseUrl, apiKey }, opts);
574
- if (toolCatalog.config_changed) {
575
- appliedActions.push({ type: 'write_model_tool_catalog', target: toolCatalog.path, ok: toolCatalog.ok === true, status: toolCatalog.status });
576
- }
577
- }
578
- else if (useDefaultProvider) {
579
- appliedActions.push({
580
- type: 'select_default_provider',
581
- target: configPath,
582
- ok: false,
583
- status: 'deferred_until_tool_catalog_ready',
584
- blockers: toolCatalog.blockers || ['codex_lb_gpt56_tool_catalog_not_ready']
585
- });
586
- }
771
+ const toolCatalog = {
772
+ schema: 'sks.codex-lb-tool-catalog-selection.v1',
773
+ ok: true,
774
+ required: false,
775
+ status: desktopMode === 'desktop-native-bridge'
776
+ ? 'not_bound_for_desktop_native'
777
+ : desktopMode === 'desktop-dual-auth-compat'
778
+ ? 'not_bound_for_desktop_compat'
779
+ : 'not_bound_for_cli_provider',
780
+ path: codexLbToolCatalogPath(path.join(home, '.codex')),
781
+ config_changed: false,
782
+ selected: desktopMode === 'desktop-dual-auth-compat',
783
+ blockers: []
784
+ };
587
785
  const keyFingerprint = await sha256Text(apiKey);
588
786
  const metadataPath = opts.metadataPath || codexLbMetadataPath(home);
589
787
  await writeTextAtomic(metadataPath, `${JSON.stringify({
@@ -591,6 +789,9 @@ export async function configureCodexLb(opts = {}) {
591
789
  base_url: baseUrl,
592
790
  updated_at: new Date().toISOString(),
593
791
  source: opts.source || 'setup',
792
+ desktop_mode: desktopMode,
793
+ requested_desktop_mode: requestedDesktopMode,
794
+ gateway_auth_transport: opts.gatewayAuthTransport || DEFAULT_CODEX_LB_GATEWAY_AUTH_TRANSPORT,
594
795
  api_key: { redacted: true, sha256: keyFingerprint }
595
796
  }, null, 2)}\n`);
596
797
  await fsp.chmod(metadataPath, 0o600).catch(() => { });
@@ -598,25 +799,54 @@ export async function configureCodexLb(opts = {}) {
598
799
  const keychain = storeKeychain ? await writeCodexLbKeychain(apiKey, opts).catch((err) => ({ ok: false, status: 'keychain_store_failed', error: err.message })) : { ok: false, status: 'skipped' };
599
800
  if (storeKeychain)
600
801
  appliedActions.push({ type: 'store_keychain', target: 'macOS Keychain service sks-codex-lb', ok: keychain.ok === true, status: keychain.status });
601
- const codexEnvironment = await syncCodexLbProviderEnvironment({ env_path: envPath, base_url: baseUrl }, { ...opts, home, apiKey, baseUrl, syncLaunchEnv: syncLaunchctl });
602
- if (syncLaunchctl)
802
+ const { syncDesktopCenterLaunchCredentials } = await import('../core/codex-lb/desktop-center-credentials.js');
803
+ const centerCredentials = await syncDesktopCenterLaunchCredentials({
804
+ mode: desktopMode,
805
+ home,
806
+ loadedEnv: await loadCodexLbEnv({ home, processEnv: {}, envPath, metadataPath })
807
+ });
808
+ appliedActions.push({
809
+ type: 'sync_center_desktop_credentials',
810
+ target: 'launchctl from official sks-codex-lb store',
811
+ ok: centerCredentials.ok === true,
812
+ status: centerCredentials.status
813
+ });
814
+ const codexEnvironment = await syncCodexLbProviderEnvironment({ env_path: envPath, base_url: baseUrl }, {
815
+ ...opts,
816
+ home,
817
+ apiKey,
818
+ baseUrl,
819
+ syncLaunchEnv: syncLaunchctl && desktopMode !== 'desktop-dual-auth-compat'
820
+ });
821
+ if (syncLaunchctl && desktopMode !== 'desktop-dual-auth-compat') {
603
822
  appliedActions.push({ type: 'sync_launchctl', target: 'macOS launchctl user environment (base URL only; API-key env removed)', ok: codexEnvironment.ok === true, status: codexEnvironment.status });
823
+ }
604
824
  const shellProfileResult = await installCodexLbShellProfileSnippet({ home, envPath, shellProfile }).catch((err) => ({ ok: false, status: 'failed', files: [], error: err.message }));
605
825
  if (shellProfile !== 'skip')
606
826
  appliedActions.push({ type: 'install_shell_profile_snippet', target: shellProfileResult.files?.join(', ') || shellProfile, ok: shellProfileResult.ok === true, status: shellProfileResult.status });
607
827
  const codexLb = await codexLbStatus({ ...opts, home, configPath, envPath });
608
- const wantForceCodexLbApiKeyAuth = opts.forceCodexLbApiKeyAuth === true || opts.authMode === 'codex-lb';
609
- const forceCodexLbApiKeyAuth = wantForceCodexLbApiKeyAuth && providerSelected;
610
- const authReconcile = providerSelected
611
- ? await reconcileCodexLbAuthConflict({ ...opts, home, status: codexLb, forceCodexLbApiKeyAuth }).catch((err) => ({ status: 'failed', reason: 'exception', error: err.message }))
612
- : { status: 'deferred_until_provider_selected', reason: 'tool_catalog_or_selection_incomplete' };
613
- const codexLogin = !providerSelected
614
- ? { ok: true, status: 'deferred_until_provider_selected', reason: 'tool_catalog_or_selection_incomplete', error: null }
615
- : forceCodexLbApiKeyAuth
616
- ? { ok: ['apikey_forced', 'apikey_auth_active'].includes(authReconcile.status), status: authReconcile.status, ...(authReconcile.reason ? { reason: authReconcile.reason } : {}), error: authReconcile.error || null }
617
- : await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home, force: true });
828
+ const authAfter = await captureCodexAuthSnapshot({ home, authPath: opts.authPath || codexAuthPath(home) });
829
+ let authInvariantError = null;
830
+ try {
831
+ await assertDesktopAuthUnchangedBySks(authBefore, authAfter);
832
+ }
833
+ catch (error) {
834
+ authInvariantError = error instanceof Error ? error.message : String(error);
835
+ }
836
+ const authReconcile = {
837
+ status: authInvariantError ? 'failed' : 'oauth_untouched',
838
+ reason: authInvariantError || 'credential_setup_does_not_mutate_desktop_auth',
839
+ auth_path: authBefore.path,
840
+ ...(authInvariantError ? { error: authInvariantError } : {})
841
+ };
842
+ const codexLogin = {
843
+ ok: !authInvariantError,
844
+ status: authInvariantError ? 'desktop_auth_byte_invariant_failed' : 'not_required',
845
+ reason: authInvariantError || 'cli_gateway_key_is_separate_from_chatgpt_oauth',
846
+ error: authInvariantError
847
+ };
618
848
  const finalCodexLb = await codexLbStatus({ ...opts, home, configPath, envPath });
619
- const ok = Boolean(codexEnvironment.ok && codexLogin.ok);
849
+ const ok = Boolean(codexEnvironment.ok && codexLogin.ok && centerCredentials.ok !== false);
620
850
  const afterState = await captureCodexLbSetupWriteState({ home, configPath, envPath, shellProfile });
621
851
  const drift = [
622
852
  ...detectCodexLbSetupDrift({
@@ -633,6 +863,7 @@ export async function configureCodexLb(opts = {}) {
633
863
  beforeState,
634
864
  afterState
635
865
  }),
866
+ ...(authInvariantError ? ['desktop_auth_byte_invariant_failed'] : []),
636
867
  ...(toolCatalog.required !== false && toolCatalog.ok !== true ? ['codex_lb_gpt56_tool_catalog_not_ready'] : [])
637
868
  ];
638
869
  const appliedPersistenceModes = appliedCodexLbPersistenceModes({
@@ -654,20 +885,30 @@ export async function configureCodexLb(opts = {}) {
654
885
  const warnings = [
655
886
  ...insecureLocalWarning,
656
887
  ...persistence.warnings,
888
+ ...(preservedLegacySelection ? ['legacy_desktop_auth_routing_requires_explicit_migrate_legacy_desktop'] : []),
657
889
  ...(toolCatalog.required !== false && toolCatalog.ok !== true ? ['codex_lb_gpt56_tool_catalog_not_ready'] : [])
658
890
  ];
659
- const failureStatus = codexEnvironment.ok !== true
660
- ? (codexEnvironment.status || 'environment_failed')
661
- : codexLogin.ok !== true
662
- ? (codexLogin.status || 'login_failed')
663
- : null;
891
+ const failureStatus = centerCredentials.ok !== true
892
+ ? (centerCredentials.status || 'center_desktop_credentials_failed')
893
+ : codexEnvironment.ok !== true
894
+ ? (codexEnvironment.status || 'environment_failed')
895
+ : codexLogin.ok !== true
896
+ ? (codexLogin.status || 'login_failed')
897
+ : null;
664
898
  return {
665
899
  ok: ok && drift.length === 0,
666
900
  status: ok && drift.length === 0 ? 'configured' : failureStatus || (drift.length ? 'setup_choice_drift' : 'configuration_failed'),
901
+ mode: desktopMode,
902
+ identity_plane: 'unchanged',
903
+ routing_plane: codexLbRoutingPlane(desktopMode),
904
+ gateway_auth_transport: opts.gatewayAuthTransport || DEFAULT_CODEX_LB_GATEWAY_AUTH_TRANSPORT,
905
+ oauth_preserved: !authInvariantError,
906
+ auth_mutated: false,
667
907
  plan: plan,
668
908
  applied_actions: appliedActions,
669
909
  drift,
670
910
  persistence,
911
+ center_credentials: centerCredentials,
671
912
  config_path: configPath,
672
913
  env_path: envPath,
673
914
  metadata_path: metadataPath,
@@ -709,19 +950,44 @@ export async function codexLbStatus(opts = {}) {
709
950
  const providerRequiresOpenAiAuth = codexLbProviderRequiresOpenAiAuth(config);
710
951
  const providerOpenAiAuthDisabled = codexLbProviderOpenAiAuthDisabled(config);
711
952
  const providerEnvKey = codexLbProviderEnvKey(config);
953
+ const providerHasSeparateGatewayAuth = codexLbProviderHasSeparateGatewayAuth(config);
954
+ const desktopMode = managedCodexLbDesktopMode(config);
955
+ const bridgeBaseUrl = desktopMode === 'desktop-native-bridge'
956
+ ? topLevelTomlString(config, 'openai_base_url')
957
+ : '';
712
958
  const sharedOpenAiRouting = codexLbSharedOpenAiRoutingState(config, baseUrl);
713
959
  const codexLbKeyInSharedAuth = Boolean(authApiKey && envLoad.secret_api_key && authApiKey === envLoad.secret_api_key);
714
960
  const sharedOpenAiRoutingSafe = !codexLbKeyInSharedAuth || sharedOpenAiRouting.status === 'matched';
715
- const authRoutingCoherent = selected === codexLbKeyInSharedAuth && sharedOpenAiRoutingSafe;
716
- const providerContractOk = providerConfigured
961
+ const cliProviderContractOk = providerConfigured
717
962
  && providerBaseUrlMatchesCredential
718
- && providerName === CODEX_LB_PROVIDER_NAME
963
+ && providerName === 'codex-lb'
719
964
  && providerWireApi === 'responses'
720
965
  && providerEnvKey === CODEX_LB_PROVIDER_ENV_KEY
721
966
  && providerSupportsWebsockets === true
722
- && providerRequiresOpenAiAuth === true;
967
+ && providerOpenAiAuthDisabled;
968
+ const compatProviderContractOk = providerConfigured
969
+ && providerBaseUrlMatchesCredential
970
+ && providerName === 'OpenAI'
971
+ && providerWireApi === 'responses'
972
+ && providerSupportsWebsockets === true
973
+ && providerRequiresOpenAiAuth === true
974
+ && !providerEnvKey
975
+ && providerHasSeparateGatewayAuth;
976
+ const providerContractOk = desktopMode === 'desktop-dual-auth-compat'
977
+ ? compatProviderContractOk
978
+ : cliProviderContractOk;
723
979
  const providerUsesCodexLbEnvAuth = providerConfigured && providerEnvKey === CODEX_LB_PROVIDER_ENV_KEY && providerOpenAiAuthDisabled;
724
- const codexAppUsableWithCodexLb = providerContractOk && envLoad.configured && Boolean(baseUrl) && authMode.codex_app_usable;
980
+ const oauthAvailable = authMode.mode === 'chatgpt_oauth' || authMode.mode === 'browser_marker';
981
+ const authRoutingCoherent = desktopMode === 'desktop-native-bridge'
982
+ ? !selected && oauthAvailable && !codexLbKeyInSharedAuth
983
+ : desktopMode === 'desktop-dual-auth-compat'
984
+ ? selected && oauthAvailable && !codexLbKeyInSharedAuth
985
+ : !codexLbKeyInSharedAuth && (!selected || providerUsesCodexLbEnvAuth);
986
+ const codexAppUsableWithCodexLb = desktopMode === 'desktop-native-bridge'
987
+ ? Boolean(bridgeBaseUrl && oauthAvailable)
988
+ : desktopMode === 'desktop-dual-auth-compat'
989
+ ? compatProviderContractOk && oauthAvailable
990
+ : authMode.codex_app_usable;
725
991
  const fastMode = codexLbFastModeConfigStatus(config);
726
992
  const launchEnvironment = await inspectCodexLbMacLaunchEnvironment(baseUrl, opts).catch((err) => ({
727
993
  checked: true,
@@ -729,7 +995,11 @@ export async function codexLbStatus(opts = {}) {
729
995
  status: 'inspect_failed',
730
996
  error: err.message
731
997
  }));
732
- const providerReady = providerContractOk && envLoad.configured && Boolean(baseUrl) && authMode.codex_app_usable && authRoutingCoherent;
998
+ const providerReady = providerContractOk
999
+ && envLoad.configured
1000
+ && Boolean(baseUrl)
1001
+ && authRoutingCoherent
1002
+ && (desktopMode === 'desktop-native-bridge' ? Boolean(bridgeBaseUrl) : true);
733
1003
  const probeToolOutputRecovery = opts.probeToolOutputRecovery === true;
734
1004
  const toolOutputRecovery = !selected
735
1005
  ? codexLbToolOutputRecoveryNotSelected()
@@ -758,7 +1028,14 @@ export async function codexLbStatus(opts = {}) {
758
1028
  provider_requires_openai_auth: providerRequiresOpenAiAuth,
759
1029
  provider_openai_auth_disabled: providerOpenAiAuthDisabled,
760
1030
  provider_env_key: providerEnvKey || null,
1031
+ provider_has_separate_gateway_auth: providerHasSeparateGatewayAuth,
761
1032
  provider_uses_codex_lb_env_auth: providerUsesCodexLbEnvAuth,
1033
+ desktop_mode: desktopMode || (selected ? 'legacy-or-manual-selected-provider' : 'cli-provider'),
1034
+ bridge_base_url: bridgeBaseUrl || null,
1035
+ legacy_migration_required: Boolean(!desktopMode
1036
+ && selected
1037
+ && authMode.mode === 'apikey'
1038
+ && sharedOpenAiRouting.managed),
762
1039
  selected,
763
1040
  env_file: envExists,
764
1041
  env_key_configured: envKeyConfigured,
@@ -784,7 +1061,7 @@ export async function codexLbStatus(opts = {}) {
784
1061
  managed: sharedOpenAiRouting.managed,
785
1062
  configured_base_url: sharedOpenAiRouting.configured_base_url
786
1063
  },
787
- auth_usable_for_codex_app: authMode.codex_app_usable && authRoutingCoherent,
1064
+ auth_usable_for_codex_app: codexAppUsableWithCodexLb && authRoutingCoherent,
788
1065
  auth_summary: !sharedOpenAiRoutingSafe
789
1066
  ? 'codex-lb key is active in shared auth without a verified built-in OpenAI routing guard'
790
1067
  : !authRoutingCoherent
@@ -829,38 +1106,36 @@ export function formatCodexLbStatusText(status = {}, opts = {}) {
829
1106
  if (status.base_url)
830
1107
  lines.push(`Base URL: ${status.base_url}`);
831
1108
  lines.push(`ChatGPT backup: ${backupPresent ? `yes (${backupPath})` : 'no'}`);
832
- if (status.shared_openai_routing?.safe === false)
833
- lines.push('', 'Run: sks codex-lb use-codex-lb to pin built-in OpenAI routing to the codex-lb base URL before the shared key can hit api.openai.com.');
1109
+ if (status.legacy_migration_required || status.shared_openai_routing?.safe === false)
1110
+ lines.push('', 'Run: sks codex-lb migrate-legacy-desktop --restart-app. Ordinary repair will not rewrite shared Codex auth.');
834
1111
  else if (status.provider_configured && !status.provider_contract_ok)
835
1112
  lines.push('', 'Run: sks codex-lb repair to rewrite the provider block to the current codex-lb App contract.');
836
- else if (status.ok && !status.auth_usable_for_codex_app && backupPresent)
837
- lines.push('', 'Run: sks codex-lb use-oauth to restore ChatGPT OAuth, or sks codex-lb use-codex-lb to force codex-lb API-key auth.');
838
- else if (status.ok && !status.auth_usable_for_codex_app)
839
- lines.push('', 'Run: sks codex-lb use-codex-lb, or sign in to Codex App/CLI again for ChatGPT OAuth.');
840
- else if (status.ok && !status.selected)
841
- lines.push('', 'Run: sks codex-lb repair to activate codex-lb for Codex App.');
1113
+ else if (status.desktop_mode === 'desktop-native-bridge' && !status.auth_usable_for_codex_app)
1114
+ lines.push('', 'Sign in with ChatGPT OAuth, then run: sks codex-lb use-desktop-full.');
1115
+ else if (status.ok && status.desktop_mode === 'cli-provider' && !status.selected)
1116
+ lines.push('', 'CLI provider is stored but unselected. Run `sks codex-lb use-cli` for CLI use or `sks codex-lb use-desktop-full` for managed Desktop routing.');
1117
+ else if (status.ok && status.desktop_mode === 'desktop-dual-auth-compat')
1118
+ lines.push('', 'Compatibility routing is active. Prefer `sks codex-lb use-desktop-full`; use `sks codex-lb disable` to remove managed Desktop routing.');
842
1119
  else if (status.ok)
843
- lines.push('', 'Status: codex-lb active; no repair needed.');
1120
+ lines.push('', 'Status: configured; no ordinary repair needed.');
844
1121
  else if (!status.ok && status.base_url && status.env_key_configured)
845
- lines.push('', 'Run: sks codex-lb repair / use-codex-lb to restore a coherent provider + shared-auth routing state.');
1122
+ lines.push('', 'Run: sks codex-lb repair. To change routing explicitly, use `use-desktop-full`, `use-cli`, or `disable`.');
846
1123
  else if (!status.ok)
847
1124
  lines.push('', 'Run: sks codex-lb setup --host <domain> --api-key-stdin');
848
1125
  if (backupPresent)
849
- lines.push('Switch fully away from codex-lb: sks codex-lb release');
1126
+ lines.push('Legacy OAuth backup detected; use `sks codex-lb migrate-legacy-desktop --restart-app` instead of background auth switching.');
850
1127
  return `${lines.join('\n')}\n`;
851
1128
  }
852
1129
  export function formatCodexLbRepairResultText(result = {}) {
853
1130
  const lines = [
854
- 'codex-lb provider auth repaired for Codex CLI/App environment.',
1131
+ 'codex-lb credential/provider configuration repaired without changing Codex shared auth.',
855
1132
  `Config: ${result.config_path}`,
856
1133
  `Key env: ${result.env_path}`
857
1134
  ];
858
- if (result.auth_reconcile?.status === 'oauth_restored')
859
- lines.push(`Codex App auth: ChatGPT OAuth restored from ${result.auth_reconcile.backup_path}.`);
860
- else if (result.auth_reconcile?.status === 'oauth_preserved')
861
- lines.push('Codex App auth: ChatGPT OAuth preserved as backup; run `sks codex-lb use-codex-lb` to force codex-lb API-key auth.');
862
- else if (['apikey_auth_active', 'apikey_forced'].includes(result.auth_reconcile?.status))
863
- lines.push('Codex App auth: codex-lb API-key auth.json is active.');
1135
+ if (result.auth_reconcile?.status === 'oauth_untouched')
1136
+ lines.push('Codex App auth: unchanged.');
1137
+ else if (result.auth_reconcile?.status === 'legacy_migration_required')
1138
+ lines.push('Legacy Desktop auth routing: run `sks codex-lb migrate-legacy-desktop --restart-app` explicitly.');
864
1139
  return `${lines.join('\n')}\n`;
865
1140
  }
866
1141
  function codexLbProviderBaseUrl(text = '') {
@@ -895,6 +1170,10 @@ function codexLbProviderEnvKey(text = '') {
895
1170
  const block = String(text || '').match(/(^|\n)\[model_providers\.codex-lb\]([\s\S]*?)(?=\n\[[^\]]+\]|\s*$)/)?.[2] || '';
896
1171
  return block.match(/(^|\n)\s*env_key\s*=\s*"([^"]+)"/)?.[2] || '';
897
1172
  }
1173
+ function codexLbProviderHasSeparateGatewayAuth(text = '') {
1174
+ const block = String(text || '').match(/(^|\n)\[model_providers\.codex-lb\]([\s\S]*?)(?=\n\[[^\]]+\]|\s*$)/)?.[2] || '';
1175
+ return /X-Codex-LB-API-Key/.test(block) && /CODEX_LB_API_KEY/.test(block);
1176
+ }
898
1177
  function codexLbFastModeConfigStatus(text = '') {
899
1178
  const globalServiceTier = topLevelTomlString(text, 'service_tier');
900
1179
  const profileBlock = String(text || '').match(/(^|\n)\[profiles\.sks-fast-high\]([\s\S]*?)(?=\n\[[^\]]+\]|\s*$)/)?.[2] || '';
@@ -921,111 +1200,214 @@ function tomlTableString(text = '', table, key) {
921
1200
  return block.match(new RegExp(`(^|\\n)\\s*${escapeRegExp(key)}\\s*=\\s*"([^"]+)"\\s*(?:#.*)?(?=\\n|$)`))?.[2] || '';
922
1201
  }
923
1202
  export async function repairCodexLbAuth(opts = {}) {
924
- let status = await codexLbStatus({
925
- ...opts,
926
- probeToolOutputRecovery: true,
927
- allowUnverifiedToolOutputRecovery: opts.allowUnverifiedToolOutputRecovery === true
1203
+ const home = opts.home || process.env.HOME || os.homedir();
1204
+ let status = await codexLbStatus({ ...opts, home });
1205
+ const authPath = opts.authPath || status.auth_path || codexAuthPath(home);
1206
+ const authBefore = await captureCodexAuthSnapshot({ home, authPath });
1207
+ const currentConfig = await readText(status.config_path, '');
1208
+ const legacyEnv = await loadCodexLbEnv({ ...opts, home });
1209
+ const legacyDetection = await detectLegacyCodexLbDesktopState({
1210
+ home,
1211
+ configPath: status.config_path,
1212
+ authPath,
1213
+ ...(status.base_url ? { remoteBaseUrl: status.base_url } : {}),
1214
+ ...(legacyEnv.secret_api_key
1215
+ ? { expectedGatewayApiKey: legacyEnv.secret_api_key }
1216
+ : {})
928
1217
  });
929
- let configRepaired = false;
930
- let legacyAuthMigrated = false;
931
- let legacyAuthPath = null;
932
- const toolOutputRecovery = status.base_url
933
- ? await probeCodexLbToolOutputRecovery({
934
- baseUrl: status.base_url,
935
- ...(typeof opts.toolOutputRecoveryFetch === 'function' ? { fetchImpl: opts.toolOutputRecoveryFetch } : {}),
936
- timeoutMs: Number(opts.toolOutputRecoveryTimeoutMs || 4_000),
937
- allowUnverified: opts.allowUnverifiedToolOutputRecovery === true
938
- || codexLbToolOutputRecoveryOverrideAcknowledged({ env: opts.env || process.env })
939
- })
940
- : status.tool_output_recovery;
941
- if (status.base_url && toolOutputRecovery?.ok !== true) {
1218
+ const legacySharedAuthSelected = status.selected === true
1219
+ && status.auth_mode === 'apikey'
1220
+ && status.shared_openai_routing?.managed === true;
1221
+ if (legacyDetection.legacy_destructive_mode || legacySharedAuthSelected) {
942
1222
  return {
943
1223
  ok: false,
944
- status: 'tool_output_recovery_blocked',
1224
+ status: 'legacy_migration_required',
1225
+ reason: 'legacy_desktop_auth_routing_requires_explicit_migration',
1226
+ config_path: status.config_path,
1227
+ env_path: status.env_path,
1228
+ base_url: status.base_url,
945
1229
  codex_lb: status,
946
- tool_output_recovery: toolOutputRecovery,
947
- error: toolOutputRecovery?.blockers?.join(', ') || 'codex-lb interrupted tool-output recovery is unverified'
1230
+ auth_reconcile: {
1231
+ status: 'legacy_migration_required',
1232
+ reason: 'run_sks_codex_lb_migrate_legacy_desktop',
1233
+ auth_path: authPath
1234
+ },
1235
+ codex_login: {
1236
+ ok: true,
1237
+ status: 'not_required',
1238
+ reason: 'ordinary_repair_never_changes_shared_codex_auth'
1239
+ },
1240
+ tool_catalog: {
1241
+ ok: true,
1242
+ required: false,
1243
+ status: 'not_bound_during_repair',
1244
+ config_changed: false
1245
+ }
948
1246
  };
949
1247
  }
950
- const currentConfig = await readText(status.config_path, '');
951
1248
  if (!status.env_key_configured && status.base_url && (status.provider_configured || status.selected || status.env_base_url_configured)) {
952
- const legacyAuth = await restoreCodexLbEnvFromSharedLogin(status, opts);
953
- if (legacyAuth.ok) {
954
- legacyAuthMigrated = true;
955
- legacyAuthPath = legacyAuth.auth_path;
956
- status = await codexLbStatus({ ...opts, probeToolOutputRecovery: true });
1249
+ if (status.auth_mode === 'apikey') {
1250
+ return {
1251
+ ok: false,
1252
+ status: 'legacy_migration_required',
1253
+ reason: 'shared_codex_api_key_cannot_be_assumed_to_be_the_codex_lb_gateway_key',
1254
+ config_path: status.config_path,
1255
+ env_path: status.env_path,
1256
+ base_url: status.base_url,
1257
+ codex_lb: status,
1258
+ auth_reconcile: {
1259
+ status: 'legacy_migration_required',
1260
+ reason: 'run_sks_codex_lb_setup_with_a_separate_gateway_key_or_migrate_legacy_desktop',
1261
+ auth_path: authPath
1262
+ },
1263
+ codex_login: {
1264
+ ok: true,
1265
+ status: 'not_required',
1266
+ reason: 'ordinary_repair_never_reads_gateway_credentials_from_shared_codex_auth'
1267
+ },
1268
+ tool_catalog: {
1269
+ ok: true,
1270
+ required: false,
1271
+ status: 'not_bound_during_repair',
1272
+ config_changed: false
1273
+ }
1274
+ };
957
1275
  }
958
1276
  }
959
- const repairCurrentProvider = topLevelTomlString(currentConfig, 'model_provider');
960
- const repairPreserveThirdParty = Boolean(repairCurrentProvider)
961
- && repairCurrentProvider !== 'codex-lb'
962
- && opts.forceCodexLbApiKeyAuth !== true
963
- && opts.authMode !== 'codex-lb';
964
- if (status.env_key_configured && status.base_url && (!status.provider_contract_ok || (!status.selected && !repairPreserveThirdParty) || legacyAuthMigrated || hasTopLevelCodexModeLock(currentConfig) || (opts.forceCodexLbApiKeyAuth === true && !status.ok))) {
965
- await ensureDir(path.dirname(status.config_path));
966
- let next = upsertCodexLbConfig(currentConfig, status.base_url, !repairPreserveThirdParty);
967
- next = normalizeCodexFastModeUiConfig(next, {
968
- forceFastMode: opts.forceFastMode === true || opts.forceCodexLbApiKeyAuth === true
969
- });
970
- const safeWrite = await safeWriteCodexConfigToml(status.config_path, currentConfig, next, 'codex-lb-repair');
971
- configRepaired = safeWrite.ok && safeWrite.changed === true;
972
- status = await codexLbStatus({ ...opts, probeToolOutputRecovery: true });
973
- }
974
- const canRepairAuthMode = (opts.forceCodexLbApiKeyAuth === true || status.codex_lb_key_in_shared_auth === true)
975
- && status.provider_contract_ok
976
- && status.env_key_configured
977
- && Boolean(status.base_url);
978
- if (!status.ok && !canRepairAuthMode) {
1277
+ if (!status.env_key_configured || !status.base_url) {
979
1278
  return {
980
1279
  ok: false,
981
- status: !status.env_key_configured ? 'missing_env_key' : !status.base_url ? 'missing_base_url' : 'not_configured',
1280
+ status: !status.env_key_configured ? 'missing_env_key' : 'missing_base_url',
982
1281
  config_path: status.config_path,
983
1282
  env_path: status.env_path,
984
- codex_lb: status
1283
+ codex_lb: status,
1284
+ auth_reconcile: {
1285
+ status: 'oauth_untouched',
1286
+ reason: 'ordinary_repair_never_changes_shared_codex_auth',
1287
+ auth_path: authPath
1288
+ }
1289
+ };
1290
+ }
1291
+ const configBeforeRepair = await readText(status.config_path, '');
1292
+ const desktopMode = managedCodexLbDesktopMode(configBeforeRepair);
1293
+ let nextConfig = configBeforeRepair;
1294
+ try {
1295
+ if (desktopMode === 'desktop-native-bridge') {
1296
+ const bridgeBaseUrl = topLevelTomlString(configBeforeRepair, 'openai_base_url');
1297
+ if (!bridgeBaseUrl)
1298
+ throw new Error('managed_desktop_bridge_missing_base_url');
1299
+ nextConfig = upsertCodexLbNativeDesktopConfig(configBeforeRepair, {
1300
+ bridgeBaseUrl,
1301
+ remoteBaseUrl: status.base_url
1302
+ });
1303
+ }
1304
+ else if (desktopMode === 'desktop-dual-auth-compat') {
1305
+ nextConfig = upsertCodexLbCompatDesktopConfig(configBeforeRepair, {
1306
+ remoteBaseUrl: status.base_url
1307
+ });
1308
+ }
1309
+ else {
1310
+ nextConfig = upsertCodexLbCliProviderConfig(configBeforeRepair, {
1311
+ remoteBaseUrl: status.base_url,
1312
+ selectGlobally: status.selected === true
1313
+ });
1314
+ }
1315
+ }
1316
+ catch (error) {
1317
+ return {
1318
+ ok: false,
1319
+ status: 'managed_desktop_routing_conflict',
1320
+ config_path: status.config_path,
1321
+ env_path: status.env_path,
1322
+ base_url: status.base_url,
1323
+ codex_lb: status,
1324
+ error: error instanceof Error ? error.message : String(error)
1325
+ };
1326
+ }
1327
+ await ensureDir(path.dirname(status.config_path));
1328
+ const configWrite = await safeWriteCodexConfigToml(status.config_path, configBeforeRepair, nextConfig, 'codex-lb-safe-repair', { verifyUnchangedBeforeWrite: true });
1329
+ if (!configWrite.ok) {
1330
+ return {
1331
+ ok: false,
1332
+ status: configWrite.status,
1333
+ config_path: status.config_path,
1334
+ env_path: status.env_path,
1335
+ base_url: status.base_url,
1336
+ codex_lb: status,
1337
+ error: 'codex_lb_config_repair_failed'
985
1338
  };
986
1339
  }
987
- await migrateCodexAuthKeyFormat({ home: opts.home });
988
1340
  const codexEnvironment = await syncCodexLbProviderEnvironment(status, opts);
989
- const loadedEnv = await loadCodexLbEnv({
990
- ...opts,
991
- home: opts.home || process.env.HOME || os.homedir(),
992
- envPath: status.env_path
993
- });
994
- const apiKey = String(opts.apiKey || loadedEnv.secret_api_key || '').trim();
995
- const toolCatalog = await ensureCodexLbToolCatalogSelection({
996
- home: opts.home || process.env.HOME || os.homedir(),
997
- configPath: status.config_path,
998
- baseUrl: String(status.base_url || ''),
999
- apiKey
1000
- }, opts);
1001
- const forceCodexLbApiKeyAuth = opts.forceCodexLbApiKeyAuth === true || opts.authMode === 'codex-lb';
1002
- const authReconcile = await reconcileCodexLbAuthConflict({ ...opts, status, forceCodexLbApiKeyAuth }).catch((err) => ({ status: 'failed', reason: 'exception', error: err.message }));
1003
- const codexLogin = forceCodexLbApiKeyAuth
1004
- ? { ok: ['apikey_forced', 'apikey_auth_active'].includes(authReconcile.status), status: authReconcile.status, ...(authReconcile.reason ? { reason: authReconcile.reason } : {}), error: authReconcile.error || null }
1005
- : await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home: opts.home || process.env.HOME || os.homedir(), force: true });
1006
- const finalStatus = await codexLbStatus({ ...opts, probeToolOutputRecovery: true });
1007
- const finalReady = finalStatus.selected === true
1008
- && finalStatus.provider_ready === true
1009
- && finalStatus.auth_routing_coherent === true
1010
- && finalStatus.shared_openai_routing?.safe === true;
1011
- const baseOk = Boolean(codexEnvironment.ok
1012
- && codexLogin.ok
1013
- && (toolCatalog.required === false || toolCatalog.ok === true));
1014
- const ok = baseOk && finalReady;
1341
+ const authAfter = await captureCodexAuthSnapshot({ home, authPath });
1342
+ let authInvariantError = null;
1343
+ try {
1344
+ await assertDesktopAuthUnchangedBySks(authBefore, authAfter);
1345
+ }
1346
+ catch (error) {
1347
+ authInvariantError = error instanceof Error ? error.message : String(error);
1348
+ }
1349
+ if (authInvariantError) {
1350
+ const writtenConfig = await readText(status.config_path, '');
1351
+ await safeWriteCodexConfigToml(status.config_path, writtenConfig, configBeforeRepair, 'codex-lb-safe-repair-auth-invariant-rollback', { verifyUnchangedBeforeWrite: true });
1352
+ }
1353
+ const finalStatus = await codexLbStatus({ ...opts, home });
1354
+ const toolOutputRecovery = opts.probeToolOutputRecovery === true && status.selected
1355
+ ? await probeCodexLbToolOutputRecovery({
1356
+ baseUrl: status.base_url,
1357
+ ...(typeof opts.toolOutputRecoveryFetch === 'function' ? { fetchImpl: opts.toolOutputRecoveryFetch } : {}),
1358
+ timeoutMs: Number(opts.toolOutputRecoveryTimeoutMs || 4_000),
1359
+ allowUnverified: opts.allowUnverifiedToolOutputRecovery === true
1360
+ || codexLbToolOutputRecoveryOverrideAcknowledged({ env: opts.env || process.env })
1361
+ })
1362
+ : codexLbToolOutputRecoveryNotChecked(status.selected === true);
1363
+ const authReconcile = {
1364
+ status: authInvariantError ? 'failed' : 'oauth_untouched',
1365
+ reason: authInvariantError || 'ordinary_repair_never_changes_shared_codex_auth',
1366
+ auth_path: authPath,
1367
+ ...(authInvariantError ? { error: authInvariantError } : {})
1368
+ };
1369
+ const codexLogin = {
1370
+ ok: !authInvariantError,
1371
+ status: authInvariantError ? 'desktop_auth_byte_invariant_failed' : 'not_required',
1372
+ reason: authInvariantError || 'gateway_credentials_are_independent_from_codex_login',
1373
+ error: authInvariantError
1374
+ };
1375
+ const toolCatalog = {
1376
+ schema: 'sks.codex-lb-tool-catalog-selection.v1',
1377
+ ok: true,
1378
+ required: false,
1379
+ status: desktopMode === 'desktop-native-bridge'
1380
+ ? 'not_bound_for_desktop_native'
1381
+ : 'not_bound_during_repair',
1382
+ config_changed: false,
1383
+ selected: finalStatus.selected,
1384
+ blockers: []
1385
+ };
1386
+ const ok = Boolean(configWrite.ok && codexEnvironment.ok && !authInvariantError);
1015
1387
  return {
1016
1388
  ok,
1017
- status: ok ? 'repaired' : baseOk ? 'final_readiness_failed' : (codexEnvironment.status || codexLogin.status),
1389
+ status: ok
1390
+ ? configWrite.changed
1391
+ ? 'repaired'
1392
+ : finalStatus.selected
1393
+ ? 'present'
1394
+ : 'present_unselected'
1395
+ : authInvariantError
1396
+ ? 'desktop_auth_byte_invariant_failed'
1397
+ : codexEnvironment.status,
1018
1398
  config_path: status.config_path,
1019
1399
  env_path: status.env_path,
1020
1400
  base_url: status.base_url,
1021
- config_repaired: configRepaired,
1022
- legacy_auth_migrated: legacyAuthMigrated,
1023
- legacy_auth_path: legacyAuthPath,
1401
+ config_repaired: configWrite.changed === true,
1402
+ legacy_auth_migrated: false,
1403
+ legacy_auth_path: null,
1024
1404
  auth_reconcile: authReconcile,
1025
1405
  codex_lb: finalStatus,
1026
1406
  codex_environment: codexEnvironment,
1027
1407
  codex_login: codexLogin,
1028
- tool_catalog: toolCatalog
1408
+ tool_catalog: toolCatalog,
1409
+ tool_output_recovery: toolOutputRecovery,
1410
+ error: authInvariantError || codexEnvironment.error || null
1029
1411
  };
1030
1412
  }
1031
1413
  export async function ensureCodexLbAuthDuringInstall(opts = {}) {
@@ -1034,87 +1416,10 @@ export async function ensureCodexLbAuthDuringInstall(opts = {}) {
1034
1416
  const status = await codexLbStatus(opts);
1035
1417
  if (!status.selected && !status.provider_configured && !status.env_key_configured)
1036
1418
  return { status: 'not_configured', codex_lb: status };
1037
- if (status.ok && !status.selected && status.auth_mode === 'chatgpt_oauth') {
1038
- return {
1039
- ok: true,
1040
- status: 'present_unselected',
1041
- reason: 'chatgpt_oauth_active_codex_lb_unselected',
1042
- config_path: status.config_path,
1043
- env_path: status.env_path,
1044
- base_url: status.base_url,
1045
- codex_lb: status
1046
- };
1047
- }
1048
- await migrateCodexAuthKeyFormat({ home: opts.home });
1049
- if (status.ok && (!status.selected || !status.provider_contract_ok))
1050
- return repairCodexLbAuth(opts);
1051
- if (!status.ok) {
1052
- if (status.base_url && (status.env_key_configured || status.provider_configured || status.selected || status.env_base_url_configured))
1053
- return repairCodexLbAuth(opts);
1054
- return { status: status.env_key_configured ? 'missing_base_url' : 'missing_env_key', codex_lb: status, config_path: status.config_path, env_path: status.env_path };
1055
- }
1056
- const codexEnvironment = await syncCodexLbProviderEnvironment(status, opts);
1057
- const loadedEnv = await loadCodexLbEnv({
1419
+ return repairCodexLbAuth({
1058
1420
  ...opts,
1059
- home: opts.home || process.env.HOME || os.homedir(),
1060
- envPath: status.env_path
1421
+ probeToolOutputRecovery: false
1061
1422
  });
1062
- const apiKey = String(opts.apiKey || loadedEnv.secret_api_key || '').trim();
1063
- const toolCatalog = await ensureCodexLbToolCatalogSelection({
1064
- home: opts.home || process.env.HOME || os.homedir(),
1065
- configPath: status.config_path,
1066
- baseUrl: String(status.base_url || ''),
1067
- apiKey
1068
- }, opts);
1069
- const codexLogin = await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home: opts.home || process.env.HOME || os.homedir(), force: true });
1070
- const authReconcile = await reconcileCodexLbAuthConflict({ ...opts, status }).catch((err) => ({ status: 'failed', reason: 'exception', error: err.message }));
1071
- const finalStatus = await codexLbStatus(opts);
1072
- const finalReady = finalStatus.selected === true
1073
- && finalStatus.provider_ready === true
1074
- && finalStatus.auth_routing_coherent === true
1075
- && finalStatus.shared_openai_routing?.safe === true;
1076
- const baseOk = Boolean(codexEnvironment.ok
1077
- && codexLogin.ok
1078
- && (toolCatalog.required === false || toolCatalog.ok === true));
1079
- const ok = baseOk && finalReady;
1080
- return {
1081
- ok,
1082
- status: ok ? 'present' : baseOk ? 'final_readiness_failed' : (codexEnvironment.status || codexLogin.status),
1083
- config_path: status.config_path,
1084
- env_path: status.env_path,
1085
- base_url: status.base_url,
1086
- codex_lb: finalStatus,
1087
- codex_environment: codexEnvironment,
1088
- codex_login: codexLogin,
1089
- tool_catalog: toolCatalog,
1090
- auth_reconcile: authReconcile,
1091
- error: codexEnvironment.error || codexLogin.error || null
1092
- };
1093
- }
1094
- async function restoreCodexLbEnvFromSharedLogin(status = {}, opts = {}) {
1095
- const home = opts.home || process.env.HOME || os.homedir();
1096
- const authPath = opts.authPath || codexAuthPath(home);
1097
- const envPath = opts.envPath || status.env_path || codexLbEnvPath(home);
1098
- const authText = await readText(authPath, '');
1099
- const apiKey = parseCodexSharedLoginApiKey(authText);
1100
- if (!apiKey)
1101
- return { ok: false, status: 'missing_legacy_login_key', auth_path: authPath, env_path: envPath };
1102
- const baseUrl = status.base_url || parseCodexLbEnvBaseUrl(await readText(envPath, ''));
1103
- if (!baseUrl)
1104
- return { ok: false, status: 'missing_base_url', auth_path: authPath, env_path: envPath };
1105
- await ensureDir(path.dirname(envPath));
1106
- await writeTextAtomic(envPath, `export CODEX_LB_BASE_URL=${shellSingleQuote(normalizeCodexLbBaseUrl(baseUrl))}\nexport CODEX_LB_API_KEY=${shellSingleQuote(apiKey)}\n`, { mode: 0o600 });
1107
- await fsp.chmod(envPath, 0o600).catch(() => { });
1108
- const metadataPath = opts.metadataPath || codexLbMetadataPath(home);
1109
- await writeTextAtomic(metadataPath, `${JSON.stringify({
1110
- schema: 'sks.codex-lb-metadata.v1',
1111
- base_url: normalizeCodexLbBaseUrl(baseUrl),
1112
- updated_at: new Date().toISOString(),
1113
- source: 'legacy-login-migration',
1114
- api_key: { redacted: true, sha256: await sha256Text(apiKey) }
1115
- }, null, 2)}\n`, { mode: 0o600 });
1116
- await fsp.chmod(metadataPath, 0o600).catch(() => { });
1117
- return { ok: true, status: 'migrated_login_cache', auth_path: authPath, env_path: envPath, base_url: normalizeCodexLbBaseUrl(baseUrl) };
1118
1423
  }
1119
1424
  function hasChatgptOAuthTokens(text = '') {
1120
1425
  try {
@@ -1164,30 +1469,6 @@ function codexAuthModeSummary(text = '') {
1164
1469
  catch { }
1165
1470
  return { mode: 'unknown', codex_app_usable: false, summary: 'unrecognized auth.json shape' };
1166
1471
  }
1167
- async function migrateCodexAuthKeyFormat(opts = {}) {
1168
- const home = opts.home || process.env.HOME || os.homedir();
1169
- const authPath = opts.authPath || codexAuthPath(home);
1170
- const text = await readText(authPath, '');
1171
- if (!text.trim())
1172
- return { status: 'skipped', reason: 'empty' };
1173
- try {
1174
- const parsed = JSON.parse(text);
1175
- if (parsed?.auth_mode !== 'apikey')
1176
- return { status: 'skipped', reason: 'not_apikey' };
1177
- if (parsed.OPENAI_API_KEY)
1178
- return { status: 'skipped', reason: 'already_migrated' };
1179
- const legacyKey = parsed.key || parsed.api_key || parsed.apiKey || parsed.openai_api_key;
1180
- if (!legacyKey)
1181
- return { status: 'skipped', reason: 'no_key_found' };
1182
- const replacement = `${JSON.stringify({ auth_mode: 'apikey', OPENAI_API_KEY: legacyKey })}\n`;
1183
- await writeTextAtomic(authPath, replacement, { mode: 0o600 });
1184
- await fsp.chmod(authPath, 0o600).catch(() => { });
1185
- return { status: 'migrated', auth_path: authPath };
1186
- }
1187
- catch {
1188
- return { status: 'skipped', reason: 'parse_error' };
1189
- }
1190
- }
1191
1472
  export async function reconcileCodexLbAuthConflict(opts = {}) {
1192
1473
  const home = opts.home || process.env.HOME || os.homedir();
1193
1474
  const status = opts.status || await codexLbStatus({ ...opts, home });
@@ -1394,8 +1675,7 @@ export async function reconcileCodexLbAuthConflict(opts = {}) {
1394
1675
  const unselected = await unselectCodexLbProvider({
1395
1676
  ...opts,
1396
1677
  home,
1397
- authPath,
1398
- allowActiveSharedAuthTransition: true
1678
+ authPath
1399
1679
  });
1400
1680
  if (!unselected.ok) {
1401
1681
  await writeTextAtomic(authPath, authText, { mode: 0o600 });
@@ -1462,21 +1742,24 @@ export async function unselectCodexLbProvider(opts = {}) {
1462
1742
  const envLoad = await loadCodexLbEnv({ ...opts, home, envPath });
1463
1743
  const authPath = opts.authPath || codexAuthPath(home);
1464
1744
  const authApiKey = parseCodexAuthApiKey(await readText(authPath, ''));
1465
- const sharedCodexLbAuthActive = Boolean(authApiKey && envLoad.secret_api_key && authApiKey === envLoad.secret_api_key);
1466
- if (sharedCodexLbAuthActive && opts.allowActiveSharedAuthTransition !== true) {
1745
+ const managedCatalogPath = codexLbToolCatalogPath(opts.codexHome || path.join(home, '.codex'));
1746
+ const managedCatalogSelected = topLevelTomlString(current, 'model_catalog_json') === managedCatalogPath;
1747
+ const providerBaseUrl = codexLbProviderBaseUrl(current) || envLoad.base_url || '';
1748
+ const sharedOpenAiRouting = codexLbSharedOpenAiRoutingState(current, providerBaseUrl);
1749
+ const managedSharedOpenAiRouting = sharedOpenAiRouting.status === 'matched' && sharedOpenAiRouting.managed;
1750
+ const codexLbRoutingOwned = hasTopLevelCodexLbSelected(current)
1751
+ || /\[model_providers\.codex-lb\]/.test(current)
1752
+ || managedCatalogSelected
1753
+ || managedSharedOpenAiRouting;
1754
+ if (authApiKey && codexLbRoutingOwned) {
1467
1755
  return {
1468
1756
  ok: false,
1469
1757
  status: 'failed',
1470
1758
  reason: 'shared_codex_lb_auth_active',
1471
- provider_error: 'refusing to unselect codex-lb while its key is active in shared OpenAI auth',
1759
+ provider_error: 'refusing to unselect codex-lb while API-key auth could fall through to built-in OpenAI',
1472
1760
  config_path: configPath
1473
1761
  };
1474
1762
  }
1475
- const managedCatalogPath = codexLbToolCatalogPath(opts.codexHome || path.join(home, '.codex'));
1476
- const managedCatalogSelected = topLevelTomlString(current, 'model_catalog_json') === managedCatalogPath;
1477
- const providerBaseUrl = codexLbProviderBaseUrl(current) || envLoad.base_url || '';
1478
- const sharedOpenAiRouting = codexLbSharedOpenAiRoutingState(current, providerBaseUrl);
1479
- const managedSharedOpenAiRouting = sharedOpenAiRouting.status === 'matched' && sharedOpenAiRouting.managed;
1480
1763
  if (!hasTopLevelCodexLbSelected(current) && !managedCatalogSelected && !managedSharedOpenAiRouting)
1481
1764
  return { ok: true, status: 'not_selected', config_path: configPath };
1482
1765
  try {
@@ -1549,8 +1832,7 @@ export async function releaseCodexLbAuthHold(opts = {}) {
1549
1832
  ...opts,
1550
1833
  home,
1551
1834
  configPath,
1552
- authPath,
1553
- allowActiveSharedAuthTransition: true
1835
+ authPath
1554
1836
  });
1555
1837
  provider = providerDeselectionOutcome(unselected);
1556
1838
  }
@@ -1591,8 +1873,7 @@ export async function releaseCodexLbAuthHold(opts = {}) {
1591
1873
  ...opts,
1592
1874
  home,
1593
1875
  configPath,
1594
- authPath,
1595
- allowActiveSharedAuthTransition: true
1876
+ authPath
1596
1877
  });
1597
1878
  provider = providerDeselectionOutcome(unselected);
1598
1879
  }
@@ -1642,8 +1923,7 @@ export async function releaseCodexLbAuthHold(opts = {}) {
1642
1923
  ...opts,
1643
1924
  home,
1644
1925
  configPath,
1645
- authPath,
1646
- allowActiveSharedAuthTransition: true
1926
+ authPath
1647
1927
  });
1648
1928
  provider = providerDeselectionOutcome(unselected);
1649
1929
  }
@@ -1705,108 +1985,46 @@ export async function maybePromptCodexLbSetupForLaunch(args = [], opts = {}) {
1705
1985
  return { status: 'skipped' };
1706
1986
  const allowUnverifiedToolOutputRecovery = opts.allowUnverifiedToolOutputRecovery === true
1707
1987
  || codexLbToolOutputRecoveryOverrideAcknowledged({ args, env: opts.env || process.env });
1708
- let status = await codexLbStatus({
1709
- ...opts,
1710
- probeToolOutputRecovery: true,
1711
- allowUnverifiedToolOutputRecovery
1712
- });
1713
- if (status.selected && status.tool_output_recovery?.ok !== true) {
1988
+ let status = await codexLbStatus({ ...opts, probeToolOutputRecovery: false });
1989
+ if (status.legacy_migration_required) {
1714
1990
  return {
1715
- status: 'tool_output_recovery_blocked',
1991
+ status: 'legacy_migration_required',
1716
1992
  ok: false,
1717
1993
  codex_lb: status,
1718
- tool_output_recovery: status.tool_output_recovery,
1719
- blockers: status.tool_output_recovery.blockers,
1720
- operator_actions: status.tool_output_recovery.operator_actions,
1721
- bypass_codex_lb: false
1994
+ blockers: ['run_sks_codex_lb_migrate_legacy_desktop']
1722
1995
  };
1723
1996
  }
1724
- if (status.env_key_configured && status.base_url && !status.selected && status.auth_mode === 'chatgpt_oauth') {
1725
- return { status: 'continued_to_codex', ok: false, chain_health: null, codex_lb: status, reason: 'chatgpt_oauth_active_codex_lb_unselected' };
1726
- }
1727
- if (status.env_key_configured && status.base_url && (!status.provider_configured || !status.selected || !status.provider_contract_ok)) {
1728
- let promptedRestore = false;
1729
- if (!status.provider_configured && canAskYesNo()) {
1730
- promptedRestore = true;
1731
- const restore = (await askPostinstallQuestion('\ncodex-lb provider section is missing, but stored auth exists. Restore and route Codex through codex-lb? [Y/n] ')).trim();
1732
- if (/^(n|no|아니|아니요|ㄴ)$/i.test(restore))
1733
- return { status: 'continued_to_codex', codex_lb: status };
1734
- }
1735
- const repaired = await repairCodexLbAuth({ ...opts, allowUnverifiedToolOutputRecovery });
1736
- status = await codexLbStatus({ ...opts, probeToolOutputRecovery: true, allowUnverifiedToolOutputRecovery });
1737
- if (status.selected && status.tool_output_recovery?.ok !== true) {
1738
- return {
1739
- status: 'tool_output_recovery_blocked',
1740
- ok: false,
1741
- repair: repaired,
1742
- codex_lb: status,
1743
- tool_output_recovery: status.tool_output_recovery,
1744
- blockers: status.tool_output_recovery.blockers,
1745
- operator_actions: status.tool_output_recovery.operator_actions,
1746
- bypass_codex_lb: false
1747
- };
1748
- }
1749
- if (!status.ok)
1750
- return { status: 'repair_failed', ok: false, repair: repaired, codex_lb: status };
1751
- if (!repaired.ok && repaired.error && promptedRestore)
1752
- console.log(`codex-lb provider restored, but launch environment sync reported: ${repaired.error}`);
1753
- else if (!repaired.ok && promptedRestore)
1754
- console.log(`codex-lb provider restored, but provider auth sync reported: ${repaired.status}`);
1755
- else if (repaired.config_repaired && promptedRestore)
1756
- console.log(`codex-lb provider restored: ${status.base_url}`);
1757
- }
1758
- if (status.ok) {
1759
- const codexEnvironment = await syncCodexLbProviderEnvironment(status, opts);
1760
- if (codexEnvironment.status === 'synced')
1761
- console.log('codex-lb provider auth synced for this user session.');
1762
- const apiKey = parseCodexLbEnvKey(await readText(status.env_path, ''));
1763
- const toolCatalog = await ensureCodexLbToolCatalogSelection({
1764
- home: opts.home || process.env.HOME || os.homedir(),
1765
- configPath: status.config_path,
1766
- baseUrl: String(status.base_url || ''),
1767
- apiKey
1768
- }, opts);
1769
- if (toolCatalog.required !== false && toolCatalog.ok !== true) {
1770
- return { status: 'repair_failed', ok: false, codex_lb: status, codex_environment: codexEnvironment, tool_catalog: toolCatalog };
1771
- }
1772
- const codexLogin = await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home: opts.home || process.env.HOME || os.homedir() });
1773
- if (codexLogin.status === 'synced')
1774
- console.log('codex-lb auth synced with Codex CLI login cache.');
1775
- const chainHealth = await checkCodexLbResponseChain(status, opts);
1776
- if (!chainHealth.ok && chainHealth.chain_unhealthy) {
1777
- if (chainHealth.status === 'previous_response_not_found') {
1778
- console.log('codex-lb response chain check: previous_response_id not persisted by the load balancer (this is normal for stateless deployments). Keeping codex-lb active.');
1779
- return { status: 'present', ...status, codex_environment: codexEnvironment, codex_login: codexLogin, tool_catalog: toolCatalog, chain_health: chainHealth };
1780
- }
1781
- console.log(`codex-lb response chain check failed (${chainHealth.status}${chainHealth.error ? `: ${chainHealth.error}` : ''}).`);
1782
- if (process.env.SKS_CODEX_LB_AUTOBYPASS === '1') {
1783
- console.log('SKS_CODEX_LB_AUTOBYPASS=1 set; bypassing codex-lb to ChatGPT OAuth for this launch.');
1784
- return { status: 'chain_unhealthy', ...status, ok: false, codex_environment: codexEnvironment, codex_login: codexLogin, tool_catalog: toolCatalog, chain_health: chainHealth, bypass_codex_lb: true };
1785
- }
1786
- if (canAskYesNo()) {
1787
- const answer = (await askPostinstallQuestion('Use codex-lb anyway, or fall back to ChatGPT OAuth? [LB/oauth] ')).trim().toLowerCase();
1788
- if (/^(oauth|o|chatgpt|fall ?back|n|no|아니|아니요|ㄴ)$/.test(answer)) {
1789
- console.log('Falling back to ChatGPT OAuth for this launch. Re-enable codex-lb anytime with `sks codex-lb repair`.');
1790
- return { status: 'chain_unhealthy', ...status, ok: false, codex_environment: codexEnvironment, codex_login: codexLogin, tool_catalog: toolCatalog, chain_health: chainHealth, bypass_codex_lb: true };
1791
- }
1792
- console.log('Keeping codex-lb active. To switch back to ChatGPT OAuth: `sks codex-lb release`.');
1793
- return { status: 'present', ...status, codex_environment: codexEnvironment, codex_login: codexLogin, tool_catalog: toolCatalog, chain_health: chainHealth };
1794
- }
1795
- console.log('Non-interactive launch + chain check failure. Keeping codex-lb active. Set SKS_CODEX_LB_AUTOBYPASS=1 to auto-bypass to ChatGPT OAuth.');
1796
- return { status: 'present', ...status, codex_environment: codexEnvironment, codex_login: codexLogin, tool_catalog: toolCatalog, chain_health: chainHealth };
1797
- }
1798
- return { status: 'present', ...status, codex_environment: codexEnvironment, codex_login: codexLogin, tool_catalog: toolCatalog, chain_health: chainHealth };
1997
+ if (status.env_key_configured && status.base_url) {
1998
+ const repaired = await repairCodexLbAuth({
1999
+ ...opts,
2000
+ allowUnverifiedToolOutputRecovery,
2001
+ probeToolOutputRecovery: false
2002
+ });
2003
+ status = await codexLbStatus({ ...opts, probeToolOutputRecovery: false });
2004
+ if (!repaired.ok)
2005
+ return { status: repaired.status, ok: false, repair: repaired, codex_lb: status };
2006
+ const codexEnvironment = repaired.codex_environment || await syncCodexLbProviderEnvironment(status, opts);
2007
+ return {
2008
+ status: status.desktop_mode === 'cli-provider' && !status.selected ? 'continued_to_codex' : 'present',
2009
+ ...status,
2010
+ codex_environment: codexEnvironment,
2011
+ ...(repaired.codex_login ? { codex_login: repaired.codex_login } : {}),
2012
+ ...(repaired.tool_catalog ? { tool_catalog: repaired.tool_catalog } : {}),
2013
+ reason: status.desktop_mode === 'cli-provider' && !status.selected
2014
+ ? 'unselected_cli_provider_available_for_explicit_use'
2015
+ : 'explicit_managed_desktop_routing_preserved'
2016
+ };
1799
2017
  }
1800
2018
  if (!canAskYesNo())
1801
2019
  return { status: 'non_interactive', codex_lb: status };
1802
- const useCodexLb = (await askPostinstallQuestion('\ncodex-lb is not configured for this Codex App profile. Configure and route Codex through codex-lb now? [y/N] ')).trim();
2020
+ const useCodexLb = (await askPostinstallQuestion('\ncodex-lb is not configured. Store credentials and an unselected CLI provider now? [y/N] ')).trim();
1803
2021
  if (!/^(y|yes|예|네|응)$/i.test(useCodexLb))
1804
2022
  return { status: 'continued_to_codex' };
1805
2023
  const host = (await askPostinstallQuestion('codex-lb host domain [http://127.0.0.1:2455]: ')).trim() || 'http://127.0.0.1:2455';
1806
2024
  const apiKey = (await askPostinstallQuestion('codex-lb API key: ')).trim();
1807
2025
  const configured = await configureCodexLb({ ...opts, host, apiKey, allowUnverifiedToolOutputRecovery });
1808
2026
  if (configured.ok)
1809
- console.log(`codex-lb configured: ${configured.base_url}`);
2027
+ console.log(`codex-lb credentials stored: ${configured.base_url}. Use \`sks codex-lb use-cli\`, \`use-desktop-full\`, or \`disable\` explicitly.`);
1810
2028
  else
1811
2029
  console.log('codex-lb setup skipped: API key was empty.');
1812
2030
  return configured;
@@ -1913,39 +2131,9 @@ async function inspectCodexLbMacLaunchEnvironment(baseUrl = '', opts = {}) {
1913
2131
  : []
1914
2132
  };
1915
2133
  }
1916
- async function maybeSyncCodexLbSharedLogin(apiKey, opts = {}) {
1917
- if (!apiKey)
1918
- return { ok: false, status: 'missing_env_key' };
1919
- if (!shouldSyncCodexLbSharedLogin(opts)) {
1920
- return { ok: true, status: 'skipped', reason: 'codex-lb repair preserved the current Codex App auth; run `sks codex-lb use-codex-lb` to switch the app to codex-lb API-key auth.' };
1921
- }
1922
- return syncCodexApiKeyLogin(apiKey, opts);
1923
- }
1924
- function shouldSyncCodexLbSharedLogin(opts = {}) {
1925
- return opts.syncCodexLogin === true || process.env.SKS_CODEX_LB_SYNC_CODEX_LOGIN === '1';
1926
- }
1927
- async function syncCodexApiKeyLogin(apiKey, opts = {}) {
1928
- const home = opts.home || process.env.HOME || os.homedir();
1929
- const codexHome = opts.codexHome || path.join(home, '.codex');
1930
- const codexBin = opts.codexBin || (await getCodexInfo().catch(() => EMPTY_CODEX_INFO)).bin || await which('codex').catch(() => null);
1931
- if (!codexBin)
1932
- return { ok: false, status: 'codex_missing' };
1933
- await ensureDir(codexHome);
1934
- const env = { HOME: home, CODEX_HOME: codexHome, CODEX_LB_API_KEY: apiKey };
1935
- if (!opts.force) {
1936
- const current = await runProcess(codexBin, ['login', 'status'], { env, timeoutMs: 10000, maxOutputBytes: 8192 });
1937
- if (current.code === 0 && !/not logged in/i.test(`${current.stdout}\n${current.stderr}`))
1938
- return { ok: true, status: 'present' };
1939
- }
1940
- const login = await runProcess(codexBin, ['login', '--with-api-key'], { input: `${apiKey}\n`, env, timeoutMs: 15000, maxOutputBytes: 8192 });
1941
- if (login.code === 0)
1942
- return { ok: true, status: 'synced' };
1943
- return { ok: false, status: 'login_failed', error: redactSecretText(login.stderr || login.stdout || 'codex login failed', [apiKey]).trim() };
1944
- }
1945
2134
  export { checkContext7, ensureCodexCliTool, ensureMadLaunchDependencies, ensureRelatedCliTools, ensureZellijCliTool, formatMadLaunchDependencyAction, maybePromptCodexUpdateForLaunch, maybePromptSksUpdateForLaunch, shouldAutoApproveInstall } from './install-tool-helpers.js';
1946
2135
  export { codexFastModeDesktopStatus, ensureGlobalCodexFastModeDuringInstall, normalizeCodexFastModeUiConfig, safeWriteCodexConfigToml } from '../core/codex-runtime/codex-desktop-config-policy.js';
1947
2136
  export { checkRequiredSkills, context7GlobalMcpStatus, ensureCodexImagegenDuringInstall, ensureGlobalCodexSkillsDuringInstall, ensureProjectContext7Config, ensureSksCommandDuringInstall, globalCodexSkillsRoot, selftestSksShimRepair } from './install-helpers-install-support.js';
1948
2137
  export { askPostinstallQuestion, codexLbConfigPath, codexLbEnvPath, normalizeCodexLbBaseUrl } from './install-helpers-codex-lb-shared.js';
1949
2138
  export { ensureGlobalCodexAppGlmProfile, ensureStoredOpenRouterProviderDuringInstall, upsertCodexAppGlmConfig, upsertCodexLbConfig } from './install-helpers-codex-lb-config.js';
1950
2139
  export { checkCodexLbResponseChain } from './install-helpers-codex-lb-chain.js';
1951
- export { selftestCodexLb } from './install-helpers-codex-lb-selftest.js';