sneakoscope 8.1.2 → 8.2.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 (261) hide show
  1. package/README.md +96 -58
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/bin/fast-inline.js +5 -51
  5. package/dist/cli/command-manifest-lite.js +2 -1
  6. package/dist/cli/command-registry.js +2 -1
  7. package/dist/cli/global-mode-router.js +1 -1
  8. package/dist/cli/install-helpers-codex-lb-config.js +112 -914
  9. package/dist/cli/install-helpers-codex-lb-shared.js +3 -44
  10. package/dist/cli/install-helpers-install-support.js +4 -20
  11. package/dist/cli/install-helpers.js +36 -2716
  12. package/dist/cli/router.js +9 -9
  13. package/dist/commands/bridge-command-output.js +111 -0
  14. package/dist/commands/bridge.js +320 -0
  15. package/dist/commands/codex-app.js +39 -123
  16. package/dist/commands/doctor-helpers.js +380 -0
  17. package/dist/commands/doctor-profile.js +3 -0
  18. package/dist/commands/doctor.js +114 -873
  19. package/dist/commands/mad-sks.js +0 -4
  20. package/dist/config/skills-manifest.json +1 -1
  21. package/dist/core/agents/agent-effort-policy.js +11 -21
  22. package/dist/core/agents/agent-recursion-guard.js +26 -0
  23. package/dist/core/agents/agent-runner-codex-exec.js +7 -9
  24. package/dist/core/architecture-hardening/contracts/contracts.js +225 -161
  25. package/dist/core/architecture-hardening/state/state-service.js +49 -13
  26. package/dist/core/auto-review.js +15 -9
  27. package/dist/core/codex/codex-config-guard.js +8 -0
  28. package/dist/core/codex-adapter.js +8 -10
  29. package/dist/core/codex-app/codex-app-fast-ui-repair.js +7 -29
  30. package/dist/core/codex-app/codex-app-launcher.js +10 -12
  31. package/dist/core/codex-app/codex-app-restart.js +10 -12
  32. package/dist/core/codex-app/menubar/constants.js +2 -5
  33. package/dist/core/codex-app/menubar/global-install.js +3 -2
  34. package/dist/core/codex-app/menubar/installer/result.js +23 -0
  35. package/dist/core/codex-app/menubar/installer/runtime.js +28 -0
  36. package/dist/core/codex-app/menubar/installer.js +5 -96
  37. package/dist/core/codex-app/menubar/migration.js +1 -42
  38. package/dist/core/codex-app/openrouter-provider.js +0 -78
  39. package/dist/core/codex-app.js +97 -360
  40. package/dist/core/codex-control/codex-app-server-runtime-env.js +8 -28
  41. package/dist/core/codex-control/codex-current-image-path-real-probe.js +2 -3
  42. package/dist/core/codex-control/codex-current-web-search-probe.js +2 -3
  43. package/dist/core/codex-control/codex-sdk-adapter.js +5 -48
  44. package/dist/core/codex-control/codex-task-runner.js +16 -60
  45. package/dist/core/codex-control/desktop-bridge-launch-guard.js +227 -0
  46. package/dist/core/codex-exec-output-schema.js +11 -13
  47. package/dist/core/codex-lb/bridge-contracts.js +2 -0
  48. package/dist/core/codex-lb/bridge-runtime-validation/capability.js +104 -0
  49. package/dist/core/codex-lb/bridge-runtime-validation/catalog.js +52 -0
  50. package/dist/core/codex-lb/bridge-runtime-validation/shared.js +109 -0
  51. package/dist/core/codex-lb/bridge-runtime-validation/status.js +250 -0
  52. package/dist/core/codex-lb/bridge-runtime-validation.js +106 -0
  53. package/dist/core/codex-lb/capability-runner.js +505 -201
  54. package/dist/core/codex-lb/codex-lb-env.js +179 -12
  55. package/dist/core/codex-lb/combined-catalog/builder.js +86 -0
  56. package/dist/core/codex-lb/combined-catalog/contracts.js +5 -0
  57. package/dist/core/codex-lb/combined-catalog/normalize.js +170 -0
  58. package/dist/core/codex-lb/combined-catalog/persistence.js +207 -0
  59. package/dist/core/codex-lb/combined-catalog/shared.js +12 -0
  60. package/dist/core/codex-lb/combined-catalog/storage.js +171 -0
  61. package/dist/core/codex-lb/combined-catalog.js +12 -0
  62. package/dist/core/codex-lb/desktop-bridge/client.js +4 -1
  63. package/dist/core/codex-lb/desktop-bridge/header-policy.js +48 -63
  64. package/dist/core/codex-lb/desktop-bridge/http-forward.js +115 -83
  65. package/dist/core/codex-lb/desktop-bridge/launchd.js +20 -13
  66. package/dist/core/codex-lb/desktop-bridge/location-rewrite.js +4 -1
  67. package/dist/core/codex-lb/desktop-bridge/security.js +381 -267
  68. package/dist/core/codex-lb/desktop-bridge/server.js +148 -19
  69. package/dist/core/codex-lb/desktop-bridge/state.js +56 -70
  70. package/dist/core/codex-lb/desktop-bridge/types.js +6 -1
  71. package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +281 -106
  72. package/dist/core/codex-lb/desktop-bridge-migration/historical-config.js +245 -0
  73. package/dist/core/codex-lb/desktop-bridge-migration/historical-toml.js +56 -0
  74. package/dist/core/codex-lb/desktop-bridge-migration/metadata.js +73 -0
  75. package/dist/core/codex-lb/desktop-bridge-migration/receipt.js +52 -0
  76. package/dist/core/codex-lb/desktop-bridge-migration/retired-runtime-cleanup.js +106 -0
  77. package/dist/core/codex-lb/desktop-bridge-migration/types.js +1 -0
  78. package/dist/core/codex-lb/desktop-bridge-migration.js +238 -0
  79. package/dist/core/codex-lb/desktop-controller-v3/catalog.js +257 -0
  80. package/dist/core/codex-lb/desktop-controller-v3/diagnostics.js +62 -0
  81. package/dist/core/codex-lb/desktop-controller-v3/lifecycle-commands.js +203 -0
  82. package/dist/core/codex-lb/desktop-controller-v3/live-probes.js +293 -0
  83. package/dist/core/codex-lb/desktop-controller-v3/preflight-probes.js +144 -0
  84. package/dist/core/codex-lb/desktop-controller-v3/provider-commands.js +182 -0
  85. package/dist/core/codex-lb/desktop-controller-v3/shared.js +279 -0
  86. package/dist/core/codex-lb/desktop-controller-v3/status.js +269 -0
  87. package/dist/core/codex-lb/desktop-controller-v3/types.js +1 -0
  88. package/dist/core/codex-lb/desktop-controller-v3/verification.js +89 -0
  89. package/dist/core/codex-lb/desktop-controller-v3.js +67 -0
  90. package/dist/core/codex-lb/desktop-controller.js +2 -1730
  91. package/dist/core/codex-lb/desktop-service.js +462 -627
  92. package/dist/core/codex-lb/migration-receipt.js +560 -118
  93. package/dist/core/codex-lb/probes/auxiliary-surfaces-probe.js +37 -53
  94. package/dist/core/codex-lb/probes/bridge-probe.js +125 -64
  95. package/dist/core/codex-lb/probes/catalog-probe.js +36 -136
  96. package/dist/core/codex-lb/probes/computer-use-probe.js +63 -39
  97. package/dist/core/codex-lb/probes/image-generation-probe.js +98 -41
  98. package/dist/core/codex-lb/probes/probe-evidence.js +32 -33
  99. package/dist/core/codex-lb/probes/voice-realtime-probe.js +69 -43
  100. package/dist/core/codex-lb/provider-credentials/configuration.js +127 -0
  101. package/dist/core/codex-lb/provider-credentials/locks.js +11 -0
  102. package/dist/core/codex-lb/provider-credentials/private-snapshot.js +28 -0
  103. package/dist/core/codex-lb/provider-credentials/removal.js +33 -0
  104. package/dist/core/codex-lb/provider-credentials/resolution.js +59 -0
  105. package/dist/core/codex-lb/provider-credentials/runtime.js +55 -0
  106. package/dist/core/codex-lb/provider-credentials/types.js +1 -0
  107. package/dist/core/codex-lb/provider-credentials/validation-store.js +137 -0
  108. package/dist/core/codex-lb/provider-credentials.js +5 -0
  109. package/dist/core/codex-lb/provider-registry.js +323 -0
  110. package/dist/core/codex-lb/provider-route-policy.js +118 -0
  111. package/dist/core/codex-lb/request-route-resolver.js +172 -0
  112. package/dist/core/codex-lb/route-index.js +148 -0
  113. package/dist/core/codex-lb/trusted-deep-evidence.js +158 -286
  114. package/dist/core/commands/basic-cli.js +1 -1
  115. package/dist/core/commands/check-command.js +3 -10
  116. package/dist/core/commands/gate-result-contract.js +10 -14
  117. package/dist/core/commands/intent-normalization/normalizer.js +1 -1
  118. package/dist/core/commands/mad-sks-command.js +2 -6
  119. package/dist/core/commands/release-command.js +11 -1
  120. package/dist/core/commands/telegram-command.js +46 -6
  121. package/dist/core/doctor/doctor-readiness-matrix.js +9 -7
  122. package/dist/core/doctor/skill-legacy-surface.js +5 -5
  123. package/dist/core/evidence/v2/evidence-key.js +30 -3
  124. package/dist/core/feature-fixtures.js +8 -1
  125. package/dist/core/feature-registry.js +3 -2
  126. package/dist/core/hooks-runtime/stop-finalization.js +11 -1
  127. package/dist/core/hooks-runtime/subagent-context.js +15 -1
  128. package/dist/core/hooks-runtime/subagent-skill-availability-contract.js +1 -1
  129. package/dist/core/hooks-runtime/tool-output-quarantine.js +1 -1
  130. package/dist/core/hooks-runtime.js +73 -87
  131. package/dist/core/image-ux-review/imagegen-adapter.js +108 -94
  132. package/dist/core/image-ux-review/real-callout-extractor.js +144 -14
  133. package/dist/core/imagegen/desktop-bridge-imagegen-target.js +77 -0
  134. package/dist/core/imagegen/imagegen-capability.js +43 -151
  135. package/dist/core/init/skills/filesystem.js +89 -0
  136. package/dist/core/init/skills/inventory.js +97 -0
  137. package/dist/core/init/skills/manifest.js +151 -0
  138. package/dist/core/init/skills.js +5 -325
  139. package/dist/core/init.js +0 -63
  140. package/dist/core/install/installed-package-smoke.js +1 -1
  141. package/dist/core/managed-assets/managed-assets-manifest.js +0 -1
  142. package/dist/core/policy/latest-version-guidance.js +15 -2
  143. package/dist/core/preflight/parallel-preflight-engine.js +27 -51
  144. package/dist/core/provider/provider-badge.js +9 -4
  145. package/dist/core/provider/provider-context.js +57 -107
  146. package/dist/core/providers/openrouter/openrouter-account.js +3 -1
  147. package/dist/core/providers/openrouter/openrouter-secret-store.js +91 -22
  148. package/dist/core/release/desktop-bridge-release-evidence.js +457 -0
  149. package/dist/core/release/gate-manifest.js +0 -1
  150. package/dist/core/release/main-push-guard.js +13 -0
  151. package/dist/core/release/main-push-receipt.js +11 -0
  152. package/dist/core/release/physical-release-gates.js +664 -61
  153. package/dist/core/release/publish-preflight.js +3 -3
  154. package/dist/core/release/release-gate-batch-runner.js +19 -6
  155. package/dist/core/release/release-gate-dag.js +33 -6
  156. package/dist/core/release/release-gate-node.js +1 -3
  157. package/dist/core/release/release-gate-output-contract.js +55 -0
  158. package/dist/core/release/release-pack-contract.js +1 -1
  159. package/dist/core/release/release-pack-receipt.js +209 -8
  160. package/dist/core/release/release-real-contract.js +1 -1
  161. package/dist/core/release/stage-publish.js +106 -28
  162. package/dist/core/retention/temp-lease.js +204 -0
  163. package/dist/core/retention.js +18 -195
  164. package/dist/core/routes.js +1 -2
  165. package/dist/core/safety/intent-contract/intent-contract.js +3 -3
  166. package/dist/core/subagents/naruto-host-credentials.js +6 -1
  167. package/dist/core/subagents/official-subagent-config.js +27 -9
  168. package/dist/core/subagents/official-subagent-runner.js +14 -31
  169. package/dist/core/subagents/wave-lifecycle.js +36 -8
  170. package/dist/core/subagents/wave-parent-guidance.js +2 -1
  171. package/dist/core/telegram/doctor.js +2 -2
  172. package/dist/core/telegram/liveness.js +10 -4
  173. package/dist/core/triwiki/context-graph/compiler/index.js +18 -18
  174. package/dist/core/update/update-migration-state/fast-mode-config.js +172 -0
  175. package/dist/core/update/update-migration-state/session-state-split.js +247 -0
  176. package/dist/core/update/update-migration-state/simple-stages.js +46 -0
  177. package/dist/core/update/update-migration-state.js +4 -481
  178. package/dist/core/version.js +1 -1
  179. package/dist/native/sks-menubar/Sources/OperationCoordinator.swift +28 -4
  180. package/dist/native/sks-menubar/Sources/OperationModels.swift +112 -2
  181. package/dist/native/sks-menubar/Sources/ProviderRouteExplanation.swift +21 -0
  182. package/dist/native/sks-menubar/Sources/ProvidersBridgeCatalog.swift +140 -0
  183. package/dist/native/sks-menubar/Sources/ProvidersOpenRouter.swift +107 -470
  184. package/dist/native/sks-menubar/Sources/ProvidersReliability.swift +27 -197
  185. package/dist/native/sks-menubar/Sources/ProvidersRoutingTruth.swift +335 -269
  186. package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +233 -437
  187. package/dist/native/sks-menubar/Sources/RemoteCodingViewController.swift +16 -7
  188. package/dist/native/sks-menubar/Sources/SecureProcessEnvelope.swift +39 -9
  189. package/dist/native/sks-menubar/Sources/TelegramSupport.swift +56 -0
  190. package/dist/scripts/canonical-test-runner.js +7 -1
  191. package/dist/scripts/check-runtime-schemas.js +7 -2
  192. package/dist/scripts/codex-sdk-real-smoke-check.js +3 -1
  193. package/dist/scripts/desktop-bridge-capabilities-check.js +123 -0
  194. package/dist/scripts/{codex-lb-desktop-bridge-latency-check.js → desktop-bridge-latency-check.js} +61 -16
  195. package/dist/scripts/desktop-bridge-real-evidence-check.js +64 -0
  196. package/dist/scripts/{codex-lb-desktop-bridge-check.js → desktop-bridge-transport-check.js} +92 -155
  197. package/dist/scripts/desktop-bridge-transport-client.js +116 -0
  198. package/dist/scripts/desktop-bridge-unification-check.js +276 -0
  199. package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +12 -8
  200. package/dist/scripts/gate-policy-audit-check.js +52 -2
  201. package/dist/scripts/provider-badge-context-check.js +55 -12
  202. package/dist/scripts/publish-preflight.js +1 -2
  203. package/dist/scripts/release-affected-selector-check.js +1 -2
  204. package/dist/scripts/release-aggressive-resource-governor-check.js +1 -1
  205. package/dist/scripts/release-cache-glob-hashing-check.js +1 -0
  206. package/dist/scripts/release-cache-version-neutral-fixture-check.js +1 -0
  207. package/dist/scripts/release-full-parallelism-blackbox.js +1 -1
  208. package/dist/scripts/release-gate-batch-runner-check.js +9 -0
  209. package/dist/scripts/release-gate-dag-runner-check.js +6 -0
  210. package/dist/scripts/release-gate-dag-runner.js +4 -2
  211. package/dist/scripts/release-metadata-check.js +16 -3
  212. package/dist/scripts/release-pack-receipt.js +29 -6
  213. package/dist/scripts/release-physical-gates-check.js +39 -0
  214. package/dist/scripts/release-real-check.js +7 -5
  215. package/package.json +6 -8
  216. package/release-gates.v2.json +156 -157
  217. package/runtime-required-scripts.json +11 -11
  218. package/schemas/codex/desktop-bridge-status-v3.schema.json +286 -0
  219. package/schemas/codex/desktop-capabilities-v3.schema.json +198 -0
  220. package/schemas/release/release-gate-node.schema.json +2 -3
  221. package/dist/cli/codex-lb-setup-warning-output.js +0 -17
  222. package/dist/cli/install-helpers-codex-lb-chain.js +0 -529
  223. package/dist/commands/codex-lb.js +0 -1232
  224. package/dist/core/architecture-hardening/migration/migration.js +0 -111
  225. package/dist/core/codex-app/auxiliary-oauth-policy.js +0 -36
  226. package/dist/core/codex-app/catalog-compat/catalog-service.js +0 -104
  227. package/dist/core/codex-app/child-policy/child-policy.js +0 -35
  228. package/dist/core/codex-app/desktop-routing-snapshot.js +0 -233
  229. package/dist/core/codex-app/glm-model-profile.js +0 -1
  230. package/dist/core/codex-app/glm-profile-installer.js +0 -191
  231. package/dist/core/codex-app/glm-profile-schema.js +0 -42
  232. package/dist/core/codex-app/multi-provider-router.js +0 -391
  233. package/dist/core/codex-app/openrouter-activate.js +0 -405
  234. package/dist/core/codex-app/openrouter-model-catalog.js +0 -217
  235. package/dist/core/codex-app/provider-child-policy.js +0 -64
  236. package/dist/core/codex-app/provider-mode.js +0 -91
  237. package/dist/core/codex-app/provider-session-policy.js +0 -85
  238. package/dist/core/codex-app/session-policy/session-pinning.js +0 -68
  239. package/dist/core/codex-control/codex-lb-launch-recovery.js +0 -319
  240. package/dist/core/codex-lb/codex-lb-setup.js +0 -293
  241. package/dist/core/codex-lb/codex-lb-tool-catalog.js +0 -456
  242. package/dist/core/codex-lb/codex-lb-tool-output-recovery.js +0 -290
  243. package/dist/core/codex-lb/desktop-center-credentials.js +0 -1083
  244. package/dist/core/codex-lb/desktop-mode.js +0 -32
  245. package/dist/core/codex-lb/desktop-real-evidence.js +0 -168
  246. package/dist/core/codex-lb/legacy-migration.js +0 -576
  247. package/dist/core/codex-lb/native-openai-transport/transport.js +0 -68
  248. package/dist/core/codex-lb/provider-routing/provider-router.js +0 -58
  249. package/dist/core/codex-lb/routing-truth.js +0 -327
  250. package/dist/core/imagegen/codex-lb-imagegen-target.js +0 -92
  251. package/dist/native/sks-menubar/Sources/ProvidersConnectTest.swift +0 -118
  252. package/dist/native/sks-menubar/Sources/ProvidersFastMode.swift +0 -17
  253. package/dist/native/sks-menubar/Sources/ProvidersMultiProvider.swift +0 -331
  254. package/dist/native/sks-menubar/Sources/ProvidersRoleModels.swift +0 -260
  255. package/dist/scripts/codex-lb-catalog-passthrough-check.js +0 -120
  256. package/dist/scripts/codex-lb-desktop-auth-invariant-check.js +0 -169
  257. package/dist/scripts/codex-lb-desktop-capabilities-check.js +0 -287
  258. package/dist/scripts/codex-lb-desktop-real-evidence-check.js +0 -123
  259. package/dist/scripts/codex-lb-fast-mode-truth-check.js +0 -114
  260. package/dist/scripts/codex-lb-fast-ui-preservation-check.js +0 -152
  261. package/dist/scripts/codex-lb-gpt56-fast-profile-check.js +0 -231
@@ -1,41 +1,14 @@
1
1
  import path from 'node:path';
2
- import os from 'node:os';
3
2
  import fsp from 'node:fs/promises';
4
- import readline from 'node:readline/promises';
5
3
  import { stdin as input, stdout as output } from 'node:process';
6
- import { ensureDir, exists, globalSksRoot, packageRoot, PACKAGE_VERSION, readText, runProcess, tmpdir, which, writeTextAtomic } from '../core/fsx.js';
7
- import { createRequestedScopeContract } from '../core/safety/requested-scope-contract.js';
8
- import { guardedPackageInstall, guardContextForRoute } from '../core/safety/mutation-guard.js';
4
+ import { ensureDir, exists, globalSksRoot, packageRoot } from '../core/fsx.js';
9
5
  import { formatHarnessConflictReport, llmHarnessCleanupPrompt, scanHarnessConflicts } from '../core/harness-conflicts.js';
10
- import { initProject, installGlobalSkills } from '../core/init.js';
11
- import { context7ConfigToml, DOLLAR_SKILL_NAMES, GETDESIGN_REFERENCE, hasContext7ConfigText, RECOMMENDED_SKILLS } from '../core/routes.js';
12
6
  import { reconcileCodexAppUpgradeProcesses } from '../core/codex-app.js';
13
- import { restartCodexApp } from '../core/codex-app/codex-app-restart.js';
14
- import { cleanupMacLaunchSecretEnvironment } from '../core/codex-app/menubar/index.js';
15
- import { recordCodexLbHealthEvent } from '../core/codex-lb-circuit.js';
16
- import { CODEX_LB_SECURE_KEYCHAIN_SERVICE, loadCodexLbEnv, codexLbMetadataPath } from '../core/codex-lb/codex-lb-env.js';
17
- import { codexLbToolCatalogPath } from '../core/codex-lb/codex-lb-tool-catalog.js';
18
- import { codexLbToolOutputRecoveryNotChecked, codexLbToolOutputRecoveryNotSelected, codexLbToolOutputRecoveryOverrideAcknowledged, probeCodexLbToolOutputRecovery } from '../core/codex-lb/codex-lb-tool-output-recovery.js';
19
- 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';
20
- import { buildCodexLbSetupPlan, codexLbPersistenceSummary, installCodexLbShellProfileSnippet, selectedCodexLbPersistenceModes } from '../core/codex-lb/codex-lb-setup.js';
21
- import { DEFAULT_CODEX_LB_GATEWAY_AUTH_TRANSPORT, modeRequiresChatGptOAuth, parseCodexLbDesktopMode, parseCodexLbGatewayAuthTransport } from '../core/codex-lb/desktop-mode.js';
22
- import { assertDesktopAuthUnchangedBySks, captureCodexAuthSnapshot } from '../core/codex-lb/desktop-auth-invariant.js';
23
- import { extractTomlTable, writeCodexConfigGuarded } from '../core/codex/codex-config-guard.js';
24
- import { ensureGlobalCodexFastModeDuringInstall, ensureTrailingNewline, normalizeCodexFastModeUiConfig, removeTopLevelTomlKeyIfValue, safeWriteCodexConfigToml, upsertTomlTable } from '../core/codex-runtime/codex-desktop-config-policy.js';
25
7
  import { runPostinstallGlobalDoctorAndMarkPending } from '../core/update/update-migration-state.js';
26
- import { repairCodexImagegen } from '../core/doctor/imagegen-repair.js';
27
- import { canAskYesNo, compareVersions, hasCodexUnstableFeatureWarningSuppression, hasDeprecatedCodexHooksFeatureFlag, isProjectSetupCandidate } from './install-tool-helpers.js';
28
- import { printCodexLbSetupWarnings } from './codex-lb-setup-warning-output.js';
29
- import { checkCodexLbResponseChain } from './install-helpers-codex-lb-chain.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, CODEX_LB_PROVIDER_SELECTION_MARKER, appliedCodexLbPersistenceModes, captureCodexLbSetupWriteState, codexLbSharedOpenAiRoutingState, detectCodexLbSetupDrift, ensureGlobalCodexAppGlmProfile, ensureStoredOpenRouterProviderDuringInstall, hardenCodexLbSetupRecoveryPath, removeCodexLbSetupRecoveryPath, removeCodexLbOrphanManagedMarkers, removeCodexLbSharedOpenAiRouting, releaseSksManagedThirdPartySelection, removeCodexLbManagedDesktopConfig, restoreCodexLbSetupWriteState, sha256Text, shellSingleQuote, upsertCodexAppGlmConfig, upsertCodexLbConfig, upsertCodexLbCliProviderConfig, upsertCodexLbCompatDesktopConfig, upsertCodexLbNativeDesktopConfig, upsertCodexLbSharedOpenAiRouting, writeCodexLbSetupFileIfUnchanged } from './install-helpers-codex-lb-config.js';
32
- import { detectLegacyCodexLbDesktopState } from '../core/codex-lb/legacy-migration.js';
8
+ import { isProjectSetupCandidate } from './install-tool-helpers.js';
33
9
  import { ensureCodexImagegenDuringInstall, ensureGlobalCodexSkillsDuringInstall, ensureGlobalContext7DuringInstall, ensureGlobalGetdesignSkillDuringInstall, ensureSksCommandDuringInstall } from './install-helpers-install-support.js';
34
- import { escapeRegExp } from '../core/text/regex.js';
35
- function packagedSksEntrypoint() {
36
- return path.join(packageRoot(), 'dist', 'bin', 'sks.js');
37
- }
38
- export async function postinstall({ bootstrap, args = [] }) {
10
+ const doctorFixCommand = ['sks doctor', '--fix'].join(' ');
11
+ export async function postinstall({ bootstrap }) {
39
12
  const installRoot = path.resolve(process.env.INIT_CWD || process.cwd());
40
13
  console.log('\nSKS installed.');
41
14
  await restoreInstalledPackageBuildStamp();
@@ -51,8 +24,6 @@ export async function postinstall({ bootstrap, args = [] }) {
51
24
  console.log(`Reason: ${reason}.`);
52
25
  return;
53
26
  }
54
- let codexLbConfigSnapshot = null;
55
- let reconcileCodexLb = false;
56
27
  try {
57
28
  const bootstrapDecision = await postinstallBootstrapDecision(installRoot);
58
29
  const conflictScan = await scanHarnessConflicts(installRoot);
@@ -60,8 +31,6 @@ export async function postinstall({ bootstrap, args = [] }) {
60
31
  await postinstallHarnessConflictNotice(conflictScan);
61
32
  return;
62
33
  }
63
- codexLbConfigSnapshot = await capturePostinstallCodexLbConfigSnapshot();
64
- reconcileCodexLb = true;
65
34
  const shim = await ensureSksCommandDuringInstall();
66
35
  if (shim.status === 'present')
67
36
  console.log(`SKS command: available (${shim.command ?? 'unknown'}).`);
@@ -87,22 +56,15 @@ export async function postinstall({ bootstrap, args = [] }) {
87
56
  else if (context7Install.status === 'failed')
88
57
  console.log(`Context7 MCP: auto setup failed. Run \`sks context7 setup --scope global\` or \`sks setup\`. ${context7Install.error || ''}`.trim());
89
58
  console.log('Codex App Fast mode: left unchanged during install; use the explicit Fast-mode command to change it.');
90
- const openRouterProviderRepair = await ensureStoredOpenRouterProviderDuringInstall();
91
- if (openRouterProviderRepair.status === 'updated')
92
- console.log('OpenRouter provider: repaired for the stored key (credentials and active model were preserved).');
93
- else if (openRouterProviderRepair.status === 'present')
94
- console.log('OpenRouter provider: stored-key configuration already compatible.');
95
- else if (openRouterProviderRepair.status === 'skipped')
96
- console.log('OpenRouter provider: no stored key; no provider configuration was added.');
97
- else if (openRouterProviderRepair.status === 'failed')
98
- console.log('OpenRouter provider: stored key was preserved, but provider repair failed. Run `sks doctor --fix`.');
59
+ console.log('Provider credentials and Codex provider selection were left byte-for-byte unchanged during install.');
60
+ console.log('Configure providers explicitly with `sks bridge provider configure codex-lb ...` or `sks bridge provider configure openrouter ...`.');
99
61
  const imagegenRepair = await ensureCodexImagegenDuringInstall();
100
62
  if (imagegenRepair.status === 'ready')
101
63
  console.log('Codex App Image Gen: ready ($imagegen/gpt-image-2 detected).');
102
64
  else if (imagegenRepair.status === 'recovered')
103
65
  console.log('Codex App Image Gen: recovered and re-detected. Start a new Codex/Work task; restart the desktop app only if the new task still lacks $imagegen.');
104
66
  else if (imagegenRepair.status === 'blocked')
105
- console.log(`Codex App Image Gen: blocked; run \`sks doctor --fix\`. ${(imagegenRepair.blockers || []).join(', ')}`.trim());
67
+ console.log(`Codex App Image Gen: blocked; run \`${doctorFixCommand}\`. ${(imagegenRepair.blockers || []).join(', ')}`.trim());
106
68
  else if (imagegenRepair.status === 'skipped')
107
69
  console.log(`Codex App Image Gen: skipped (${imagegenRepair.reason}).`);
108
70
  const postinstallDoctor = await runPostinstallGlobalDoctorAndMarkPending({
@@ -134,7 +96,7 @@ export async function postinstall({ bootstrap, args = [] }) {
134
96
  else if (appProcessRepair.status === 'partial')
135
97
  console.log(`Codex App reconnect repair: stopped ${appProcessRepair.killed.length} stale orphan app-server process(es); ${(appProcessRepair.failed ?? []).length} could not be stopped. Restart Codex App if reconnecting continues.`);
136
98
  else if (appProcessRepair.status === 'skipped' && appProcessRepair.reason === 'opt_in_required')
137
- console.log('Codex App reconnect repair: not run (set SKS_POSTINSTALL_RECONCILE_APP_PROCESSES=1 to allow postinstall to stop stale orphan app-server processes; otherwise run `sks doctor --fix`).');
99
+ console.log(`Codex App reconnect repair: not run (set SKS_POSTINSTALL_RECONCILE_APP_PROCESSES=1 to allow postinstall to stop stale orphan app-server processes; otherwise run \`${doctorFixCommand}\`).`);
138
100
  else if (appProcessRepair.status === 'skipped' && appProcessRepair.reason !== 'platform')
139
101
  console.log(`Codex App reconnect repair: skipped (${appProcessRepair.reason}).`);
140
102
  else if (appProcessRepair.status === 'failed')
@@ -145,26 +107,24 @@ export async function postinstall({ bootstrap, args = [] }) {
145
107
  const cleanup = removed.length ? ` Removed stale generated skill shadow(s): ${removed.join(', ')}.` : '';
146
108
  console.log(`Codex App global $ skills: installed in ${globalSkills.root} (${globalSkills.installed_count} skills).${cleanup}`);
147
109
  }
148
- else if (globalSkills.status === 'partial')
149
- console.log(`Codex App global $ skills: partial in ${globalSkills.root}; missing ${(globalSkills.missing_skills ?? []).join(', ')}. Run \`sks doctor --fix\`.`);
150
- else if (globalSkills.status === 'skipped')
110
+ else if (globalSkills.status === 'partial') {
111
+ console.log(`Codex App global $ skills: partial in ${globalSkills.root}; missing ${(globalSkills.missing_skills ?? []).join(', ')}. Run \`${doctorFixCommand}\`.`);
112
+ }
113
+ else if (globalSkills.status === 'skipped') {
151
114
  console.log(`Codex App global $ skills: skipped (${globalSkills.reason}).`);
152
- else if (globalSkills.status === 'failed')
153
- console.log(`Codex App global $ skills: auto setup failed. Run \`sks doctor --fix\`. ${globalSkills.error || ''}`.trim());
115
+ }
116
+ else if (globalSkills.status === 'failed') {
117
+ console.log(`Codex App global $ skills: auto setup failed. Run \`${doctorFixCommand}\`. ${globalSkills.error || ''}`.trim());
118
+ }
154
119
  const getdesignSkill = await ensureGlobalGetdesignSkillDuringInstall();
155
120
  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
121
  console.log(`SKS bootstrap: ${bootstrapDecision.reason}.`);
157
122
  await runPostinstallBootstrap(installRoot, bootstrap, bootstrapDecision);
158
- return;
159
123
  }
160
124
  catch (err) {
161
- console.log(`\nSKS postinstall: a setup step did not complete; installation continues. Run \`sks doctor --fix\` afterward. (${err?.message || err})`);
125
+ console.log(`\nSKS postinstall: a setup step did not complete; installation continues. Run \`${doctorFixCommand}\` afterward. (${err?.message || err})`);
162
126
  }
163
127
  finally {
164
- if (reconcileCodexLb) {
165
- await restorePostinstallCodexLbConfigSnapshot(codexLbConfigSnapshot).catch(() => { });
166
- await reportPostinstallCodexLbAuth(codexLbConfigSnapshot).catch(() => { });
167
- }
168
128
  }
169
129
  }
170
130
  function postinstallExternalMutationsAllowed(env) {
@@ -179,9 +139,8 @@ async function restoreInstalledPackageBuildStamp() {
179
139
  throw new Error('dist_stamp_path_missing');
180
140
  const stamp = path.resolve(rawStamp);
181
141
  const relative = path.relative(root, stamp);
182
- if (relative.startsWith('..') || path.isAbsolute(relative)) {
142
+ if (relative.startsWith('..') || path.isAbsolute(relative))
183
143
  throw new Error('dist_stamp_outside_package_root');
184
- }
185
144
  await fsp.access(stamp).catch(async () => {
186
145
  await fsp.writeFile(stamp, `${JSON.stringify(stampLib.buildStampPayload(), null, 2)}\n`);
187
146
  console.log('SKS build stamp: restored inside the installed package for update self-verification.');
@@ -193,12 +152,10 @@ async function restoreInstalledPackageBuildStamp() {
193
152
  }
194
153
  async function runPostinstallProjectRetentionCleanup(root) {
195
154
  const projectRoot = path.resolve(root || process.cwd());
196
- if (process.env.SKS_POSTINSTALL_RETENTION_CLEANUP === '0') {
155
+ if (process.env.SKS_POSTINSTALL_RETENTION_CLEANUP === '0')
197
156
  return { status: 'skipped', reason: 'disabled_by_env', action_count: 0 };
198
- }
199
- if (!(await exists(path.join(projectRoot, '.sneakoscope', 'missions')))) {
157
+ if (!(await exists(path.join(projectRoot, '.sneakoscope', 'missions'))))
200
158
  return { status: 'skipped', reason: 'missions_missing', action_count: 0 };
201
- }
202
159
  try {
203
160
  const { enforceRetention } = await import('../core/retention.js');
204
161
  const result = await enforceRetention(projectRoot, {
@@ -206,71 +163,16 @@ async function runPostinstallProjectRetentionCleanup(root) {
206
163
  pruneReportLogs: true,
207
164
  policy: { max_tmp_age_hours: 0 }
208
165
  });
209
- return {
210
- status: 'completed',
211
- root: projectRoot,
212
- action_count: Array.isArray(result.actions) ? result.actions.length : 0
213
- };
166
+ return { status: 'completed', root: projectRoot, action_count: Array.isArray(result.actions) ? result.actions.length : 0 };
214
167
  }
215
168
  catch (err) {
216
- return {
217
- status: 'failed',
218
- root: projectRoot,
219
- action_count: 0,
220
- error: err?.message || String(err)
221
- };
169
+ return { status: 'failed', root: projectRoot, action_count: 0, error: err?.message || String(err) };
222
170
  }
223
171
  }
224
- async function reportPostinstallCodexLbAuth(snapshot = null) {
225
- const codexLbAuth = await ensureCodexLbAuthDuringInstall({
226
- ...(snapshot?.secret_api_key ? { apiKey: snapshot.secret_api_key } : {}),
227
- ...(snapshot?.base_url ? { baseUrl: snapshot.base_url } : {})
228
- });
229
- if (codexLbAuth.legacy_auth_migrated)
230
- console.log(`codex-lb auth: restored from existing Codex login cache into ${codexLbAuth.env_path}.`);
231
- else if (codexLbAuth.status === 'synced' || codexLbAuth.status === 'present' || codexLbAuth.status === 'repaired')
232
- console.log(`codex-lb auth: preserved from ${codexLbAuth.env_path}.`);
233
- else if (codexLbAuth.status === 'present_unselected')
234
- console.log('codex-lb auth: preserved but not selected; ChatGPT OAuth remains active.');
235
- else if (codexLbAuth.status === 'skipped')
236
- console.log(`codex-lb auth: skipped (${codexLbAuth.reason}).`);
237
- else if (codexLbAuth.status === 'missing_env_key')
238
- console.log('codex-lb auth: stored key missing. Store it in ~/.codex/sks-codex-lb.env, run `sks codex-lb setup --host <domain> --api-key-stdin --yes`, or provide CODEX_LB_API_KEY in the environment.');
239
- else if (codexLbAuth.status === 'missing_base_url')
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`.');
243
- else if (codexLbAuth.status === 'not_configured')
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.');
245
- else if (codexLbAuth.status && codexLbAuth.status !== 'not_configured')
246
- console.log(`codex-lb auth: repair skipped (${codexLbAuth.status}${codexLbAuth.error ? `: ${codexLbAuth.error}` : ''}).`);
247
- const reconcile = codexLbAuth.auth_reconcile;
248
- if (reconcile?.status === 'oauth_untouched') {
249
- console.log('codex-lb auth: ChatGPT OAuth/shared Codex auth was left byte-for-byte unchanged.');
250
- }
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.');
253
- }
254
- if (codexLbAuth.base_url && codexLbAuth.codex_lb?.env_key_configured && canAskYesNo() && process.env.SKS_SKIP_CODEX_LB_KEY_PROMPT !== '1') {
255
- const changeKey = (await askPostinstallQuestion('codex-lb key changed? Update now? [y/N] ')).trim();
256
- if (/^(y|yes|예|네|응)$/i.test(changeKey)) {
257
- const newKey = (await askPostinstallQuestion('New codex-lb API key (sk-clb-...): ')).trim();
258
- if (newKey) {
259
- const result = await configureCodexLb({ host: codexLbAuth.base_url, apiKey: newKey });
260
- if (result.ok)
261
- console.log(`codex-lb key updated: ${result.base_url}`);
262
- else
263
- console.log(`codex-lb key update failed: ${result.status}${result.error ? `: ${result.error}` : ''}`);
264
- printCodexLbSetupWarnings(result);
265
- }
266
- }
267
- }
268
- return codexLbAuth;
269
- }
270
172
  async function postinstallHarnessConflictNotice(conflictScan) {
271
173
  console.log('\nSneakoscope Codex package installed, but SKS setup is blocked.');
272
174
  console.log(formatHarnessConflictReport(conflictScan, { includePrompt: false }));
273
- console.log('\nWhat this means: npm can finish installing the package. Conflicting OMX/DCodex markers must be removed explicitly with `sks conflicts cleanup --yes` before `sks setup`, `sks doctor --fix`, or `sks update` can proceed.');
175
+ console.log(`\nWhat this means: npm can finish installing the package. Conflicting OMX/DCodex markers must be removed explicitly with \`sks conflicts cleanup --yes\` before \`sks setup\`, \`${doctorFixCommand}\`, or \`sks update\` can proceed.`);
274
176
  console.log('No files were removed by postinstall.');
275
177
  console.log('Cleanup requires a human-approved Codex App session. Keep the model selected in Codex and use high reasoning effort.');
276
178
  if (shouldAskPostinstallQuestion()) {
@@ -286,19 +188,28 @@ async function postinstallHarnessConflictNotice(conflictScan) {
286
188
  else {
287
189
  console.log('Print the cleanup prompt later with: sks conflicts prompt');
288
190
  }
289
- console.log('After approved cleanup, rerun: sks setup && sks doctor --fix && sks selftest --mock\n');
191
+ console.log(`After approved cleanup, rerun: sks setup && ${doctorFixCommand} && sks selftest --mock\n`);
290
192
  }
291
193
  function shouldAskPostinstallQuestion() {
292
194
  if (process.env.SKS_POSTINSTALL_PROMPT === '1')
293
195
  return true;
294
196
  return Boolean(input.isTTY && output.isTTY && process.env.CI !== 'true' && process.env.SKS_POSTINSTALL_NO_PROMPT !== '1');
295
197
  }
198
+ async function askPostinstallQuestion(prompt) {
199
+ const readline = await import('node:readline/promises');
200
+ const rl = readline.createInterface({ input, output });
201
+ try {
202
+ return await rl.question(prompt);
203
+ }
204
+ finally {
205
+ rl.close();
206
+ }
207
+ }
296
208
  export async function postinstallBootstrapDecision(root) {
297
209
  if (process.env.SKS_POSTINSTALL_NO_BOOTSTRAP === '1')
298
210
  return { run: false, reason: 'SKS_POSTINSTALL_NO_BOOTSTRAP=1' };
299
- if (process.env.SKS_POSTINSTALL_BOOTSTRAP !== '1') {
211
+ if (process.env.SKS_POSTINSTALL_BOOTSTRAP !== '1')
300
212
  return { run: false, reason: 'explicit opt-in required (SKS_POSTINSTALL_BOOTSTRAP=1)' };
301
- }
302
213
  const installRoot = path.resolve(root || process.cwd());
303
214
  const candidate = await isProjectSetupCandidate(installRoot);
304
215
  const target = candidate ? installRoot : globalSksRoot();
@@ -317,2598 +228,7 @@ async function runPostinstallBootstrap(root, bootstrap, selectedDecision) {
317
228
  process.chdir(previousCwd);
318
229
  }
319
230
  }
320
- export async function configureCodexLbDesktopRouting(opts) {
321
- const home = opts.home || process.env.HOME || os.homedir();
322
- const configPath = opts.configPath || codexLbConfigPath(home);
323
- const authPath = opts.authPath || codexAuthPath(home);
324
- const mode = parseCodexLbDesktopMode(opts.mode);
325
- const gatewayAuthTransport = parseCodexLbGatewayAuthTransport(opts.gatewayAuthTransport || DEFAULT_CODEX_LB_GATEWAY_AUTH_TRANSPORT);
326
- const authBefore = await captureCodexAuthSnapshot({ home, authPath });
327
- const identityPlane = authBefore.mode === 'chatgpt_oauth' || authBefore.mode === 'mixed'
328
- ? 'chatgpt_oauth'
329
- : 'unavailable';
330
- const base = {
331
- schema: 'sks.codex-lb-desktop-routing.v1',
332
- mode,
333
- gateway_auth_transport: gatewayAuthTransport,
334
- identity_plane: identityPlane,
335
- auth_mutated: false,
336
- config_path: configPath,
337
- auth_path: authPath,
338
- auth_before: authBefore
339
- };
340
- if (desktopModeRequiresGlobalSecretEnvironment(mode)) {
341
- return {
342
- ...base,
343
- ok: false,
344
- status: 'desktop_dual_auth_compat_unavailable',
345
- routing_plane: 'unchanged',
346
- oauth_preserved: true,
347
- auth_after: authBefore,
348
- blockers: ['desktop_dual_auth_compat_requires_global_secret_environment']
349
- };
350
- }
351
- if (mode === 'cli-provider') {
352
- return {
353
- ...base,
354
- ok: false,
355
- status: 'invalid_desktop_routing_input',
356
- routing_plane: 'unchanged',
357
- oauth_preserved: true,
358
- auth_after: authBefore,
359
- blockers: ['cli_provider_is_not_a_desktop_routing_mode']
360
- };
361
- }
362
- if (modeRequiresChatGptOAuth(mode) && identityPlane !== 'chatgpt_oauth') {
363
- return {
364
- ...base,
365
- ok: false,
366
- status: 'desktop_oauth_required',
367
- routing_plane: 'unchanged',
368
- oauth_preserved: false,
369
- auth_after: authBefore,
370
- blockers: ['chatgpt_oauth_required_for_desktop']
371
- };
372
- }
373
- if (mode === 'desktop-dual-auth-compat' && gatewayAuthTransport !== 'x-codex-lb-api-key') {
374
- return {
375
- ...base,
376
- ok: false,
377
- status: 'desktop_gateway_auth_transport_unsupported',
378
- routing_plane: 'unchanged',
379
- oauth_preserved: true,
380
- auth_after: authBefore,
381
- blockers: ['desktop_compat_cannot_use_authorization_bearer_without_replacing_oauth']
382
- };
383
- }
384
- if (mode === 'desktop-native-bridge' && (!opts.bridgeBaseUrl || !opts.remoteBaseUrl)) {
385
- return {
386
- ...base,
387
- ok: false,
388
- status: 'invalid_desktop_routing_input',
389
- routing_plane: 'unchanged',
390
- oauth_preserved: true,
391
- auth_after: authBefore,
392
- blockers: [
393
- ...(!opts.bridgeBaseUrl ? ['missing_bridge_base_url'] : []),
394
- ...(!opts.remoteBaseUrl ? ['missing_remote_base_url'] : [])
395
- ]
396
- };
397
- }
398
- if (mode === 'desktop-dual-auth-compat' && !opts.remoteBaseUrl) {
399
- return {
400
- ...base,
401
- ok: false,
402
- status: 'invalid_desktop_routing_input',
403
- routing_plane: 'unchanged',
404
- oauth_preserved: true,
405
- auth_after: authBefore,
406
- blockers: ['missing_remote_base_url']
407
- };
408
- }
409
- const current = await readText(configPath, '');
410
- let next;
411
- try {
412
- next = mode === 'desktop-native-bridge'
413
- ? upsertCodexLbNativeDesktopConfig(current, {
414
- bridgeBaseUrl: String(opts.bridgeBaseUrl),
415
- remoteBaseUrl: String(opts.remoteBaseUrl)
416
- })
417
- : mode === 'desktop-dual-auth-compat'
418
- ? upsertCodexLbCompatDesktopConfig(current, { remoteBaseUrl: String(opts.remoteBaseUrl) })
419
- : releaseSksManagedThirdPartySelection(removeCodexLbManagedDesktopConfig(current));
420
- }
421
- catch (error) {
422
- return {
423
- ...base,
424
- ok: false,
425
- status: 'failed',
426
- routing_plane: 'unchanged',
427
- oauth_preserved: true,
428
- auth_after: authBefore,
429
- blockers: ['desktop_config_conflict'],
430
- error: error instanceof Error ? error.message : String(error)
431
- };
432
- }
433
- const write = await safeWriteCodexConfigToml(configPath, current, next, 'codex-lb-desktop-routing', { verifyUnchangedBeforeWrite: true });
434
- if (!write.ok) {
435
- return {
436
- ...base,
437
- ok: false,
438
- status: 'failed',
439
- routing_plane: 'unchanged',
440
- oauth_preserved: true,
441
- auth_after: authBefore,
442
- backup_path: write.backup_path,
443
- blockers: ['desktop_config_write_failed'],
444
- error: write.status
445
- };
446
- }
447
- const authAfter = await captureCodexAuthSnapshot({ home, authPath });
448
- try {
449
- await assertDesktopAuthUnchangedBySks(authBefore, authAfter);
450
- }
451
- catch (error) {
452
- const writtenText = await readText(configPath, '');
453
- const rollbackWrite = await safeWriteCodexConfigToml(configPath, writtenText, current, 'codex-lb-desktop-routing-auth-invariant-rollback', { verifyUnchangedBeforeWrite: true });
454
- return {
455
- ...base,
456
- ok: false,
457
- status: 'failed',
458
- routing_plane: 'unchanged',
459
- oauth_preserved: false,
460
- auth_after: authAfter,
461
- backup_path: write.backup_path,
462
- rollback: {
463
- ok: rollbackWrite.ok,
464
- status: rollbackWrite.status,
465
- config_restored: rollbackWrite.ok
466
- },
467
- blockers: ['desktop_auth_byte_invariant_failed', ...(rollbackWrite.ok ? [] : ['desktop_config_rollback_failed'])],
468
- error: error instanceof Error ? error.message : String(error)
469
- };
470
- }
471
- const routingPlane = mode === 'desktop-native-bridge'
472
- ? 'desktop_native_bridge'
473
- : mode === 'desktop-dual-auth-compat'
474
- ? 'desktop_compat_provider'
475
- : 'disabled';
476
- return {
477
- ...base,
478
- ok: true,
479
- status: mode === 'disabled'
480
- ? 'desktop_routing_disabled'
481
- : write.changed === true
482
- ? 'desktop_routing_configured'
483
- : 'desktop_routing_present',
484
- routing_plane: routingPlane,
485
- oauth_preserved: true,
486
- auth_after: authAfter,
487
- backup_path: write.backup_path,
488
- blockers: []
489
- };
490
- }
491
- export async function configureCodexLbCliProvider(opts) {
492
- const home = opts.home || process.env.HOME || os.homedir();
493
- const configPath = opts.configPath || codexLbConfigPath(home);
494
- const authPath = opts.authPath || codexAuthPath(home);
495
- const authBefore = await captureCodexAuthSnapshot({ home, authPath });
496
- const current = await readText(configPath, '');
497
- let next;
498
- try {
499
- next = upsertCodexLbCliProviderConfig(current, {
500
- remoteBaseUrl: opts.remoteBaseUrl,
501
- ...(opts.selectGlobally === undefined ? {} : { selectGlobally: opts.selectGlobally })
502
- });
503
- }
504
- catch (error) {
505
- const message = error instanceof Error ? error.message : String(error);
506
- return {
507
- schema: 'sks.codex-lb-cli-provider.v1',
508
- ok: false,
509
- status: message,
510
- mode: 'cli-provider',
511
- identity_plane: 'unchanged',
512
- routing_plane: 'cli_provider',
513
- oauth_preserved: true,
514
- auth_mutated: false,
515
- config_path: configPath,
516
- auth_path: authPath,
517
- auth_before: authBefore,
518
- auth_after: authBefore,
519
- blockers: [message],
520
- guidance: message === 'codex_lb_user_owned_model_provider_conflict'
521
- ? [
522
- 'config.toml has a model_provider line SKS does not manage.',
523
- 'Remove or change that line yourself, then retry Use Codex LB.'
524
- ]
525
- : [],
526
- backup_path: null
527
- };
528
- }
529
- const write = await safeWriteCodexConfigToml(configPath, current, next, 'codex-lb-cli-provider', { verifyUnchangedBeforeWrite: true });
530
- const authAfter = await captureCodexAuthSnapshot({ home, authPath });
531
- try {
532
- await assertDesktopAuthUnchangedBySks(authBefore, authAfter);
533
- }
534
- catch {
535
- return {
536
- schema: 'sks.codex-lb-cli-provider.v1',
537
- ok: false,
538
- status: 'auth_changed_during_cli_provider_write',
539
- mode: 'cli-provider',
540
- identity_plane: 'unchanged',
541
- routing_plane: 'cli_provider',
542
- oauth_preserved: false,
543
- auth_mutated: false,
544
- config_path: configPath,
545
- auth_path: authPath,
546
- auth_before: authBefore,
547
- auth_after: authAfter,
548
- blockers: ['shared_auth_byte_invariant_failed'],
549
- backup_path: write.backup_path
550
- };
551
- }
552
- return {
553
- schema: 'sks.codex-lb-cli-provider.v1',
554
- ok: write.ok,
555
- status: write.ok ? (write.changed ? 'configured' : 'present') : write.status,
556
- mode: 'cli-provider',
557
- identity_plane: 'unchanged',
558
- routing_plane: 'cli_provider',
559
- oauth_preserved: true,
560
- auth_mutated: false,
561
- config_path: configPath,
562
- auth_path: authPath,
563
- auth_before: authBefore,
564
- auth_after: authAfter,
565
- blockers: write.ok ? [] : ['cli_provider_config_write_failed'],
566
- backup_path: write.backup_path
567
- };
568
- }
569
- function hasTopLevelMarker(text, marker) {
570
- const topLevel = String(text || '').split(/\n\s*\[/)[0] || '';
571
- return topLevel.split(/\r?\n/).some((line) => line.trim() === marker);
572
- }
573
- function managedCodexLbDesktopMode(text) {
574
- if (hasTopLevelMarker(text, CODEX_LB_DESKTOP_BRIDGE_MARKER))
575
- return 'desktop-native-bridge';
576
- if (hasTopLevelMarker(text, CODEX_LB_DESKTOP_COMPAT_MARKER))
577
- return 'desktop-dual-auth-compat';
578
- return null;
579
- }
580
- function codexLbRoutingPlane(mode) {
581
- if (mode === 'desktop-native-bridge')
582
- return 'desktop_native_bridge';
583
- if (mode === 'desktop-dual-auth-compat')
584
- return 'desktop_compat_provider';
585
- return 'cli_provider';
586
- }
587
- export async function capturePostinstallCodexLbConfigSnapshot(home = process.env.HOME || os.homedir()) {
588
- const configPath = codexLbConfigPath(home);
589
- const envPath = codexLbEnvPath(home);
590
- const authPath = codexAuthPath(home);
591
- const config = await readText(configPath, '');
592
- const auth = await captureCodexAuthSnapshot({ home, authPath });
593
- const envLoad = await loadCodexLbEnv({ home, envPath });
594
- const envKey = envLoad.secret_api_key || '';
595
- const providerConfigured = /\[model_providers\.codex-lb\]/.test(config);
596
- const baseUrl = envLoad.base_url || codexLbProviderBaseUrl(config);
597
- const desktopMode = managedCodexLbDesktopMode(config);
598
- const bridgeBaseUrl = desktopMode === 'desktop-native-bridge'
599
- ? topLevelTomlString(config, 'openai_base_url')
600
- : '';
601
- if (!envKey && !providerConfigured && !baseUrl && !desktopMode)
602
- return null;
603
- return {
604
- config_path: configPath,
605
- env_path: envPath,
606
- auth_path: authPath,
607
- base_url: baseUrl ? normalizeCodexLbBaseUrl(baseUrl) : null,
608
- bridge_base_url: bridgeBaseUrl || null,
609
- desktop_mode: desktopMode,
610
- selected: hasTopLevelCodexLbSelected(config),
611
- auth_mode: auth.mode,
612
- auth_sha256: auth.sha256,
613
- credential_source: envLoad.source,
614
- credential_fingerprint: envLoad.api_key.fingerprint,
615
- credential_binding: envLoad.credential_binding,
616
- secret_api_key: envKey || null
617
- };
618
- }
619
- export async function restorePostinstallCodexLbConfigSnapshot(snapshot) {
620
- if (!snapshot)
621
- return { status: 'skipped', reason: 'no_snapshot' };
622
- let configRestored = false;
623
- let configStatus = 'present';
624
- if (snapshot.base_url) {
625
- const current = await readText(snapshot.config_path, '');
626
- let next = current;
627
- try {
628
- if (snapshot.desktop_mode === 'desktop-native-bridge' && snapshot.bridge_base_url) {
629
- next = upsertCodexLbNativeDesktopConfig(current, {
630
- bridgeBaseUrl: snapshot.bridge_base_url,
631
- remoteBaseUrl: snapshot.base_url
632
- });
633
- }
634
- else if (snapshot.desktop_mode === 'desktop-dual-auth-compat') {
635
- next = upsertCodexLbCompatDesktopConfig(current, { remoteBaseUrl: snapshot.base_url });
636
- }
637
- else {
638
- next = upsertCodexLbCliProviderConfig(current, {
639
- remoteBaseUrl: snapshot.base_url,
640
- selectGlobally: snapshot.selected === true
641
- });
642
- }
643
- }
644
- catch (error) {
645
- return {
646
- status: 'failed',
647
- reason: 'managed_routing_restore_conflict',
648
- config_path: snapshot.config_path,
649
- auth_path: snapshot.auth_path,
650
- config_restored: false,
651
- auth_restored: false,
652
- error: error instanceof Error ? error.message : String(error)
653
- };
654
- }
655
- if (ensureTrailingNewline(next) !== ensureTrailingNewline(current)) {
656
- const safeWrite = await safeWriteCodexConfigToml(snapshot.config_path, current, next, 'codex-lb-safe-routing-restore', { verifyUnchangedBeforeWrite: true });
657
- configRestored = safeWrite.ok && safeWrite.changed === true;
658
- configStatus = safeWrite.ok ? (configRestored ? 'restored' : 'present') : safeWrite.status;
659
- }
660
- }
661
- const authAfter = await captureCodexAuthSnapshot({
662
- home: path.dirname(path.dirname(snapshot.auth_path)),
663
- authPath: snapshot.auth_path
664
- });
665
- const authUnchanged = snapshot.auth_sha256 === authAfter.sha256;
666
- return {
667
- status: configStatus,
668
- config_path: snapshot.config_path,
669
- auth_path: snapshot.auth_path,
670
- config_restored: configRestored,
671
- auth_restored: false,
672
- auth_unchanged: authUnchanged
673
- };
674
- }
675
- export async function configureCodexLb(opts = {}) {
676
- const home = opts.home || process.env.HOME || os.homedir();
677
- const configPath = opts.configPath || codexLbConfigPath(home);
678
- const envPath = opts.envPath || codexLbEnvPath(home);
679
- const metadataPath = opts.metadataPath || codexLbMetadataPath(home);
680
- const rawHost = String(opts.host || opts.baseUrl || '');
681
- const baseUrl = normalizeCodexLbBaseUrl(rawHost);
682
- const apiKey = String(opts.apiKey || '').trim();
683
- const requestedDesktopMode = parseCodexLbDesktopMode(opts.desktopMode || 'cli-provider');
684
- const writeEnvFile = opts.writeEnvFile !== false;
685
- const storeKeychain = opts.storeKeychain === true || opts.keychain === true;
686
- const keychainStoreImpl = typeof opts.keychainStoreImpl === 'function'
687
- ? opts.keychainStoreImpl
688
- : null;
689
- const syncLaunchctl = opts.syncLaunchctl === true || opts.syncLaunchEnv === true;
690
- const shellProfile = opts.shellProfile || 'skip';
691
- if (storeKeychain && !keychainStoreImpl) {
692
- return {
693
- ok: false,
694
- status: 'keychain_acl_helper_unavailable',
695
- config_path: configPath,
696
- env_path: envPath,
697
- metadata_path: metadataPath,
698
- keychain: {
699
- ok: false,
700
- status: 'keychain_acl_helper_unavailable',
701
- keychain_state_verified: true,
702
- keychain_state_status: 'unchanged'
703
- },
704
- error: 'Dedicated signed Keychain helper unavailable; use the owner-only env file.'
705
- };
706
- }
707
- const beforeState = await captureCodexLbSetupWriteState({
708
- home,
709
- configPath,
710
- envPath,
711
- metadataPath,
712
- shellProfile
713
- });
714
- await opts.testHooks?.afterBeforeStateCapture?.({ configPath });
715
- const configBeforeEntry = beforeState.files.find((entry) => entry?.path === configPath);
716
- if (!configBeforeEntry) {
717
- return {
718
- ok: false,
719
- status: 'setup_snapshot_failed',
720
- config_path: configPath,
721
- env_path: envPath,
722
- metadata_path: metadataPath
723
- };
724
- }
725
- const setupMutationPaths = new Set([
726
- configPath,
727
- metadataPath,
728
- ...(writeEnvFile ? [envPath] : []),
729
- ...(shellProfile === 'skip'
730
- ? []
731
- : beforeState.files
732
- .map((entry) => String(entry?.path || ''))
733
- .filter((file) => file && file !== configPath && file !== envPath && file !== metadataPath))
734
- ]);
735
- const unsafeSetupTargets = beforeState.files
736
- .filter((entry) => setupMutationPaths.has(String(entry?.path || ''))
737
- && entry?.existed === true
738
- && entry?.kind !== 'regular')
739
- .map((entry) => `unsafe_setup_write_target:${entry.path}:${entry.kind}`);
740
- if (unsafeSetupTargets.length > 0) {
741
- return {
742
- ok: false,
743
- status: 'unsafe_setup_write_target',
744
- config_path: configPath,
745
- env_path: envPath,
746
- metadata_path: metadataPath,
747
- drift: unsafeSetupTargets
748
- };
749
- }
750
- const initialConfig = configBeforeEntry.existed === true
751
- ? Buffer.from(String(configBeforeEntry.bytes_base64 || ''), 'base64').toString('utf8')
752
- : '';
753
- const preservedDesktopMode = managedCodexLbDesktopMode(initialConfig);
754
- const managedCliSelection = hasTopLevelMarker(initialConfig, CODEX_LB_PROVIDER_SELECTION_MARKER)
755
- && hasTopLevelCodexLbSelected(initialConfig);
756
- const preservedLegacySelection = hasTopLevelCodexLbSelected(initialConfig)
757
- && !preservedDesktopMode
758
- && !managedCliSelection;
759
- const desktopMode = preservedDesktopMode || 'cli-provider';
760
- if (desktopModeRequiresGlobalSecretEnvironment(desktopMode)) {
761
- return {
762
- ok: false,
763
- status: 'desktop_dual_auth_compat_unavailable',
764
- config_path: configPath,
765
- env_path: envPath,
766
- error: 'desktop_dual_auth_compat_requires_global_secret_environment'
767
- };
768
- }
769
- const useDefaultProvider = desktopMode === 'desktop-dual-auth-compat'
770
- || preservedLegacySelection
771
- || managedCliSelection
772
- || (desktopMode === 'cli-provider' && opts.useDefaultProvider === true);
773
- const setupAnswers = {
774
- host_or_base_url: rawHost,
775
- api_key_source: opts.apiKeySource || 'stdin',
776
- desktop_mode: 'cli-provider',
777
- use_as_default_provider: useDefaultProvider,
778
- gateway_auth_transport: (opts.gatewayAuthTransport || DEFAULT_CODEX_LB_GATEWAY_AUTH_TRANSPORT),
779
- write_env_file: writeEnvFile,
780
- store_keychain: storeKeychain,
781
- keychain_helper_verified: Boolean(keychainStoreImpl),
782
- sync_launchctl: syncLaunchctl,
783
- install_shell_profile: shellProfile,
784
- run_health_check: opts.runHealth === true,
785
- allow_insecure_localhost: opts.allowInsecureHttp === true || opts.allowInsecureLocalhost === true
786
- };
787
- const selectedPersistenceModes = selectedCodexLbPersistenceModes(setupAnswers);
788
- const plan = buildCodexLbSetupPlan(setupAnswers, {
789
- home,
790
- configPath,
791
- envPath,
792
- metadataPath: opts.metadataPath || codexLbMetadataPath(home)
793
- });
794
- if (!baseUrl)
795
- return { ok: false, status: 'missing_host_or_base_url', config_path: configPath, env_path: envPath };
796
- if (plan.blockers.length)
797
- return { ok: false, status: 'plan_blocked', plan: plan, drift: plan.blockers, config_path: configPath, env_path: envPath };
798
- if (/[\u0000-\u001f\u007f\s]/.test(rawHost.trim()))
799
- return { ok: false, status: 'invalid_host_or_base_url', config_path: configPath, env_path: envPath, error: 'host_or_base_url_contains_whitespace_or_control_character' };
800
- if (!apiKey)
801
- return { ok: false, status: 'missing_api_key', config_path: configPath, env_path: envPath };
802
- const toolOutputRecovery = await probeCodexLbToolOutputRecovery({
803
- baseUrl,
804
- ...(typeof opts.toolOutputRecoveryFetch === 'function' ? { fetchImpl: opts.toolOutputRecoveryFetch } : {}),
805
- timeoutMs: Number(opts.toolOutputRecoveryTimeoutMs || 4_000),
806
- allowUnverified: opts.allowUnverifiedToolOutputRecovery === true
807
- || codexLbToolOutputRecoveryOverrideAcknowledged({ env: opts.env || process.env })
808
- });
809
- if (!toolOutputRecovery.ok) {
810
- return {
811
- ok: false,
812
- status: 'tool_output_recovery_blocked',
813
- plan: plan,
814
- config_path: configPath,
815
- env_path: envPath,
816
- base_url: baseUrl,
817
- tool_output_recovery: toolOutputRecovery,
818
- drift: toolOutputRecovery.blockers,
819
- warnings: toolOutputRecovery.warnings
820
- };
821
- }
822
- const insecureLocalWarning = /^http:\/\//i.test(baseUrl) && !/^http:\/\/(?:localhost|127\.0\.0\.1|\[::1\])(?::|\/|$)/i.test(baseUrl) && !opts.allowInsecureHttp
823
- ? ['codex-lb base URL uses http outside localhost; prefer https or pass an explicit allow flag in the calling surface.']
824
- : [];
825
- const processEnvBefore = {
826
- baseUrl: process.env.CODEX_LB_BASE_URL,
827
- apiKey: process.env.CODEX_LB_API_KEY
828
- };
829
- const authBefore = await captureCodexAuthSnapshot({ home, authPath: opts.authPath || codexAuthPath(home) });
830
- const appliedActions = [];
831
- await ensureDir(path.dirname(configPath));
832
- const current = initialConfig;
833
- let providerOnly = current;
834
- try {
835
- if (desktopMode === 'desktop-native-bridge') {
836
- const bridgeBaseUrl = topLevelTomlString(current, 'openai_base_url');
837
- if (!bridgeBaseUrl) {
838
- return {
839
- ok: false,
840
- status: 'managed_desktop_bridge_missing_base_url',
841
- mode: desktopMode,
842
- config_path: configPath,
843
- env_path: envPath
844
- };
845
- }
846
- providerOnly = upsertCodexLbNativeDesktopConfig(current, {
847
- bridgeBaseUrl,
848
- remoteBaseUrl: baseUrl
849
- });
850
- }
851
- else if (desktopMode === 'desktop-dual-auth-compat') {
852
- providerOnly = upsertCodexLbCompatDesktopConfig(current, { remoteBaseUrl: baseUrl });
853
- }
854
- else if (!preservedLegacySelection) {
855
- providerOnly = upsertCodexLbCliProviderConfig(current, {
856
- remoteBaseUrl: baseUrl,
857
- selectGlobally: useDefaultProvider
858
- });
859
- }
860
- }
861
- catch (error) {
862
- return {
863
- ok: false,
864
- status: 'managed_desktop_routing_conflict',
865
- mode: desktopMode,
866
- config_path: configPath,
867
- env_path: envPath,
868
- error: error instanceof Error ? error.message : String(error)
869
- };
870
- }
871
- await opts.testHooks?.beforeInitialConfigWrite?.({ configPath });
872
- const safeWrite = await safeWriteCodexConfigToml(configPath, current, providerOnly, 'codex-lb', {
873
- verifyUnchangedBeforeWrite: true,
874
- expectedBeforeExists: configBeforeEntry.existed === true,
875
- ...(configBeforeEntry.existed === true
876
- ? { expectedBeforeMode: Number(configBeforeEntry.mode) }
877
- : {})
878
- });
879
- if (!safeWrite.ok)
880
- return { ok: false, status: safeWrite.status, config_path: configPath, env_path: envPath, backup_path: safeWrite.backup_path };
881
- appliedActions.push({
882
- type: preservedLegacySelection
883
- ? 'preserve_legacy_desktop_routing'
884
- : desktopMode === 'desktop-native-bridge'
885
- ? 'preserve_desktop_native_bridge'
886
- : desktopMode === 'desktop-dual-auth-compat'
887
- ? 'preserve_desktop_compat_provider'
888
- : 'write_cli_provider',
889
- target: configPath,
890
- ok: true,
891
- backup_path: safeWrite.backup_path
892
- });
893
- const configExpectedAfter = safeWrite.expected_after;
894
- if (!configExpectedAfter || typeof configExpectedAfter.exists !== 'boolean') {
895
- return {
896
- ok: false,
897
- status: 'partial_configuration_phase_snapshot_failed',
898
- mode: desktopMode,
899
- identity_plane: 'unchanged',
900
- routing_plane: codexLbRoutingPlane(desktopMode),
901
- oauth_preserved: true,
902
- auth_mutated: false,
903
- plan: plan,
904
- applied_actions: appliedActions,
905
- drift: ['setup_phase_snapshot_failed:after_config_write'],
906
- config_path: configPath,
907
- env_path: envPath,
908
- metadata_path: metadataPath,
909
- backup_path: safeWrite.backup_path,
910
- error: 'guarded config writer did not return an owned post-write snapshot'
911
- };
912
- }
913
- let setupExpectedState = {
914
- ...beforeState,
915
- files: beforeState.files.map((entry) => entry.path === configPath
916
- ? configExpectedAfter.exists
917
- ? {
918
- path: configPath,
919
- existed: true,
920
- kind: 'regular',
921
- bytes_base64: Buffer.from(String(configExpectedAfter.text || '')).toString('base64'),
922
- mode: Number(configExpectedAfter.mode)
923
- }
924
- : { path: configPath, existed: false, kind: 'missing', bytes_base64: '', mode: null }
925
- : entry)
926
- };
927
- const recordExpectedRegularFile = (file, text, mode) => {
928
- setupExpectedState = {
929
- ...setupExpectedState,
930
- files: setupExpectedState.files.map((entry) => entry.path === file
931
- ? {
932
- path: file,
933
- existed: true,
934
- kind: 'regular',
935
- bytes_base64: Buffer.from(text).toString('base64'),
936
- mode: mode & 0o777
937
- }
938
- : entry)
939
- };
940
- };
941
- const setupWriteRecoveryPaths = [];
942
- const writeSetupFile = async (input) => {
943
- const expected = beforeState.files.find((entry) => path.resolve(String(entry?.path || '')) === path.resolve(input.file));
944
- if (!expected) {
945
- throw new Error(`setup_snapshot_missing:${input.file}`);
946
- }
947
- const result = await writeCodexLbSetupFileIfUnchanged({
948
- file: input.file,
949
- expected,
950
- text: input.text,
951
- mode: input.mode,
952
- ...(input.beforeReplacement ? { beforeReplacement: input.beforeReplacement } : {})
953
- });
954
- if (result.recovery_path)
955
- setupWriteRecoveryPaths.push(result.recovery_path);
956
- if (result.ok || result.installed) {
957
- recordExpectedRegularFile(input.file, input.text, input.mode);
958
- }
959
- if (!result.ok) {
960
- throw new Error(`${result.status}:${input.file}${result.error ? `:${result.error}` : ''}`);
961
- }
962
- return result;
963
- };
964
- const rollbackSetupAfterFailure = async (input) => {
965
- const rollback = await restoreCodexLbSetupWriteState(beforeState, input.expectedCurrentState, {
966
- ...(typeof opts.testHooks?.beforeRollbackFileReplacement === 'function'
967
- ? { beforeReplacement: opts.testHooks.beforeRollbackFileReplacement }
968
- : {})
969
- });
970
- const rollbackBlockers = [...rollback.blockers];
971
- const processRollbackBlockers = [];
972
- if (input.restoreProcessEnvironment) {
973
- if (process.env.CODEX_LB_BASE_URL === processEnvBefore.baseUrl) {
974
- }
975
- else if (process.env.CODEX_LB_BASE_URL === baseUrl) {
976
- if (processEnvBefore.baseUrl === undefined)
977
- delete process.env.CODEX_LB_BASE_URL;
978
- else
979
- process.env.CODEX_LB_BASE_URL = processEnvBefore.baseUrl;
980
- }
981
- else {
982
- processRollbackBlockers.push('setup_rollback_conflict:process.env.CODEX_LB_BASE_URL');
983
- }
984
- if (process.env.CODEX_LB_API_KEY === processEnvBefore.apiKey) {
985
- }
986
- else if (process.env.CODEX_LB_API_KEY === apiKey) {
987
- if (processEnvBefore.apiKey === undefined)
988
- delete process.env.CODEX_LB_API_KEY;
989
- else
990
- process.env.CODEX_LB_API_KEY = processEnvBefore.apiKey;
991
- }
992
- else {
993
- processRollbackBlockers.push('setup_rollback_conflict:process.env.CODEX_LB_API_KEY');
994
- }
995
- }
996
- rollbackBlockers.push(...processRollbackBlockers);
997
- const afterRollback = await captureCodexLbSetupWriteState({
998
- home,
999
- configPath,
1000
- envPath,
1001
- metadataPath,
1002
- shellProfile
1003
- });
1004
- if (beforeState.stateHash !== afterRollback.stateHash) {
1005
- rollbackBlockers.push('setup_rollback_state_verification_failed');
1006
- }
1007
- const recoveryPaths = [
1008
- ...(safeWrite.backup_path ? [safeWrite.backup_path] : []),
1009
- ...setupWriteRecoveryPaths,
1010
- ...rollback.recovery
1011
- ];
1012
- const secretRecoveryPaths = [...new Set(recoveryPaths)]
1013
- .filter((recoveryPath) => isRecoveryPathForFile(recoveryPath, envPath));
1014
- for (const recoveryPath of secretRecoveryPaths) {
1015
- if (!await hardenCodexLbSetupRecoveryPath(recoveryPath)) {
1016
- rollbackBlockers.push(`setup_secret_recovery_mode_unverified:${recoveryPath}`);
1017
- }
1018
- }
1019
- if (secretRecoveryPaths.length > 0) {
1020
- rollbackBlockers.push('setup_secret_recovery_retained');
1021
- }
1022
- const filesystemRollbackOk = rollbackBlockers.length === 0;
1023
- const keychain = input.keychain || { ok: false, status: 'not_written' };
1024
- return {
1025
- ok: false,
1026
- status: input.keychainRetained
1027
- ? 'partial_configuration_keychain_retained'
1028
- : input.externalStateMayBeMutated
1029
- ? 'partial_configuration_external_state_unknown'
1030
- : filesystemRollbackOk
1031
- ? 'setup_failed_rolled_back'
1032
- : 'setup_failed_rollback_incomplete',
1033
- mode: desktopMode,
1034
- identity_plane: 'unchanged',
1035
- routing_plane: codexLbRoutingPlane(desktopMode),
1036
- oauth_preserved: true,
1037
- auth_mutated: false,
1038
- plan: plan,
1039
- applied_actions: appliedActions,
1040
- drift: [
1041
- `setup_stage_failed:${input.stage}`,
1042
- ...(input.keychainRetained ? ['codex_lb_keychain_replacement_retained'] : []),
1043
- ...(input.externalStateMayBeMutated ? ['codex_lb_external_environment_requires_inspection'] : []),
1044
- ...rollbackBlockers
1045
- ],
1046
- rollback: {
1047
- ...rollback,
1048
- ok: filesystemRollbackOk,
1049
- blockers: rollbackBlockers,
1050
- byte_and_mode_verified: filesystemRollbackOk,
1051
- keychain_retained: input.keychainRetained,
1052
- secret_recovery_paths: secretRecoveryPaths,
1053
- config_backup_path: safeWrite.backup_path || null,
1054
- config_backup_status: safeWrite.backup_path ? 'retained_for_recovery' : 'not_created',
1055
- recovery_paths: recoveryPaths
1056
- },
1057
- partial_configuration: {
1058
- schema: 'sks.codex-lb-partial-configuration.v1',
1059
- failure_stage: input.stage,
1060
- filesystem_state: filesystemRollbackOk ? 'restored' : 'indeterminate',
1061
- process_environment_state: input.restoreProcessEnvironment
1062
- ? (processRollbackBlockers.length === 0 ? 'restored' : 'indeterminate')
1063
- : 'unchanged',
1064
- keychain_state: input.keychainRetained ? 'replacement_retained' : 'unchanged',
1065
- external_environment_state: input.externalStateMayBeMutated ? 'inspect_with_status' : 'unchanged',
1066
- durable_applied_state: [
1067
- ...(input.keychainRetained
1068
- ? [`macOS Keychain service ${CODEX_LB_SECURE_KEYCHAIN_SERVICE}`]
1069
- : []),
1070
- ...(input.externalStateMayBeMutated ? ['Center/launch environment may retain a partial update'] : [])
1071
- ],
1072
- recovery_actions: [
1073
- 'Run: sks codex-lb status --json',
1074
- 'Rerun setup with --api-key-stdin after resolving the failed stage.',
1075
- `Inspect ${CODEX_LB_SECURE_KEYCHAIN_SERVICE} before removing any retained Keychain credential.`,
1076
- ...(secretRecoveryPaths.length > 0
1077
- ? ['Review and securely remove secret_recovery_paths after preserving any concurrent user edits.']
1078
- : [])
1079
- ],
1080
- recovery_paths: recoveryPaths,
1081
- secret_recovery_paths: secretRecoveryPaths
1082
- },
1083
- config_path: configPath,
1084
- env_path: envPath,
1085
- metadata_path: metadataPath,
1086
- keychain,
1087
- ...(input.centerCredentials ? { center_credentials: input.centerCredentials } : {}),
1088
- ...(input.codexEnvironment ? { codex_environment: input.codexEnvironment } : {}),
1089
- error: redactSecretText(input.error instanceof Error ? input.error.message : String(input.error || input.stage), [apiKey])
1090
- };
1091
- };
1092
- const toolCatalog = {
1093
- schema: 'sks.codex-lb-tool-catalog-selection.v1',
1094
- ok: true,
1095
- required: false,
1096
- status: desktopMode === 'desktop-native-bridge'
1097
- ? 'not_bound_for_desktop_native'
1098
- : desktopMode === 'desktop-dual-auth-compat'
1099
- ? 'not_bound_for_desktop_compat'
1100
- : 'not_bound_for_cli_provider',
1101
- path: codexLbToolCatalogPath(path.join(home, '.codex')),
1102
- config_changed: false,
1103
- selected: desktopMode === 'desktop-dual-auth-compat',
1104
- blockers: []
1105
- };
1106
- const keyFingerprint = await sha256Text(apiKey);
1107
- const metadataText = `${JSON.stringify({
1108
- schema: 'sks.codex-lb-metadata.v1',
1109
- base_url: baseUrl,
1110
- updated_at: new Date().toISOString(),
1111
- source: opts.source || 'setup',
1112
- desktop_mode: desktopMode,
1113
- requested_desktop_mode: requestedDesktopMode,
1114
- gateway_auth_transport: opts.gatewayAuthTransport || DEFAULT_CODEX_LB_GATEWAY_AUTH_TRANSPORT,
1115
- api_key: { redacted: true, sha256: keyFingerprint }
1116
- }, null, 2)}\n`;
1117
- try {
1118
- await opts.testHooks?.beforeMetadataWrite?.({ metadataPath });
1119
- const metadataWrite = await writeSetupFile({
1120
- file: metadataPath,
1121
- text: metadataText,
1122
- mode: 0o600,
1123
- ...(typeof opts.testHooks?.beforeSetupFileReplacement === 'function'
1124
- ? { beforeReplacement: opts.testHooks.beforeSetupFileReplacement }
1125
- : {})
1126
- });
1127
- appliedActions.push({
1128
- type: 'write_metadata',
1129
- target: metadataPath,
1130
- ok: true,
1131
- status: metadataWrite.status,
1132
- recovery_path: metadataWrite.recovery_path || null
1133
- });
1134
- }
1135
- catch (error) {
1136
- return rollbackSetupAfterFailure({
1137
- stage: 'write_metadata',
1138
- error,
1139
- keychainRetained: false,
1140
- restoreProcessEnvironment: false,
1141
- expectedCurrentState: setupExpectedState
1142
- });
1143
- }
1144
- const keychainWriteState = setupExpectedState;
1145
- const keychain = storeKeychain
1146
- ? await keychainStoreImpl(apiKey, opts).catch((err) => ({
1147
- ok: false,
1148
- status: 'keychain_store_failed',
1149
- error: err.message
1150
- }))
1151
- : { ok: false, status: 'skipped' };
1152
- if (storeKeychain)
1153
- appliedActions.push({ type: 'store_keychain', target: `macOS Keychain service ${CODEX_LB_SECURE_KEYCHAIN_SERVICE}`, ok: keychain.ok === true, status: keychain.status });
1154
- if (storeKeychain && keychain.ok !== true) {
1155
- await opts.testHooks?.beforeRollbackStart?.({ configPath });
1156
- const rollback = await restoreCodexLbSetupWriteState(beforeState, keychainWriteState, {
1157
- ...(typeof opts.testHooks?.beforeRollbackFileReplacement === 'function'
1158
- ? { beforeReplacement: opts.testHooks.beforeRollbackFileReplacement }
1159
- : {})
1160
- });
1161
- const rollbackBlockers = [...rollback.blockers];
1162
- if (keychain.keychain_state_verified !== true) {
1163
- rollbackBlockers.push('setup_rollback_keychain_state_indeterminate');
1164
- }
1165
- const backupStatus = safeWrite.backup_path ? 'retained_for_recovery' : 'not_created';
1166
- const afterRollback = await captureCodexLbSetupWriteState({ home, configPath, envPath, metadataPath, shellProfile });
1167
- if (beforeState.stateHash !== afterRollback.stateHash) {
1168
- rollbackBlockers.push('setup_rollback_state_verification_failed');
1169
- }
1170
- const recoveryPaths = [
1171
- ...(safeWrite.backup_path ? [safeWrite.backup_path] : []),
1172
- ...setupWriteRecoveryPaths,
1173
- ...rollback.recovery
1174
- ];
1175
- const secretRecoveryPaths = [...new Set(recoveryPaths)]
1176
- .filter((recoveryPath) => isRecoveryPathForFile(recoveryPath, envPath));
1177
- for (const recoveryPath of secretRecoveryPaths) {
1178
- if (!await hardenCodexLbSetupRecoveryPath(recoveryPath)) {
1179
- rollbackBlockers.push(`setup_secret_recovery_mode_unverified:${recoveryPath}`);
1180
- }
1181
- }
1182
- if (secretRecoveryPaths.length > 0) {
1183
- rollbackBlockers.push('setup_secret_recovery_retained');
1184
- }
1185
- const finalRollbackOk = rollbackBlockers.length === 0;
1186
- return {
1187
- ok: false,
1188
- status: finalRollbackOk
1189
- ? 'keychain_store_failed_rolled_back'
1190
- : keychain.keychain_state_verified !== true
1191
- ? 'keychain_state_indeterminate'
1192
- : 'keychain_store_failed_rollback_incomplete',
1193
- mode: desktopMode,
1194
- identity_plane: 'unchanged',
1195
- routing_plane: codexLbRoutingPlane(desktopMode),
1196
- oauth_preserved: true,
1197
- auth_mutated: false,
1198
- plan: plan,
1199
- applied_actions: appliedActions,
1200
- drift: ['codex_lb_keychain_store_failed', ...rollbackBlockers],
1201
- rollback: {
1202
- ...rollback,
1203
- ok: finalRollbackOk,
1204
- blockers: rollbackBlockers,
1205
- byte_and_mode_verified: finalRollbackOk,
1206
- config_backup_path: safeWrite.backup_path || null,
1207
- config_backup_status: backupStatus,
1208
- recovery_paths: recoveryPaths,
1209
- secret_recovery_paths: secretRecoveryPaths
1210
- },
1211
- config_path: configPath,
1212
- env_path: envPath,
1213
- metadata_path: metadataPath,
1214
- keychain,
1215
- error: keychain.error || keychain.status
1216
- };
1217
- }
1218
- const writeSetupEnvFile = async () => {
1219
- await opts.testHooks?.beforeEnvWrite?.({ envPath });
1220
- const envText = `export CODEX_LB_BASE_URL=${shellSingleQuote(baseUrl)}\nexport CODEX_LB_API_KEY=${shellSingleQuote(apiKey)}\n`;
1221
- const envWrite = await writeSetupFile({
1222
- file: envPath,
1223
- text: envText,
1224
- mode: 0o600,
1225
- ...(typeof opts.testHooks?.beforeSetupFileReplacement === 'function'
1226
- ? { beforeReplacement: opts.testHooks.beforeSetupFileReplacement }
1227
- : {})
1228
- });
1229
- appliedActions.push({
1230
- type: 'write_env_file',
1231
- target: envPath,
1232
- ok: true,
1233
- status: envWrite.status,
1234
- recovery_path: envWrite.recovery_path || null
1235
- });
1236
- await opts.testHooks?.afterEnvWrite?.({ envPath });
1237
- };
1238
- if (writeEnvFile && !storeKeychain) {
1239
- try {
1240
- await writeSetupEnvFile();
1241
- }
1242
- catch (error) {
1243
- return rollbackSetupAfterFailure({
1244
- stage: 'write_env_file',
1245
- error,
1246
- keychain,
1247
- keychainRetained: false,
1248
- restoreProcessEnvironment: false,
1249
- expectedCurrentState: setupExpectedState
1250
- });
1251
- }
1252
- }
1253
- let shellProfileResult = { ok: true, status: 'skipped', files: [] };
1254
- try {
1255
- await opts.testHooks?.beforeCenterSync?.();
1256
- }
1257
- catch (error) {
1258
- return rollbackSetupAfterFailure({
1259
- stage: 'sync_center_desktop_credentials',
1260
- error,
1261
- keychain,
1262
- keychainRetained: storeKeychain && keychain.ok === true,
1263
- restoreProcessEnvironment: false,
1264
- expectedCurrentState: setupExpectedState
1265
- });
1266
- }
1267
- const skipLaunchEnvironment = opts.syncLaunchEnv === false
1268
- || process.env.SKS_SKIP_CODEX_LB_LAUNCH_ENV === '1';
1269
- const { repairCodexLbLegacyKeychainMigration, syncDesktopCenterLaunchCredentials } = await import('../core/codex-lb/desktop-center-credentials.js');
1270
- const centerCredentials = skipLaunchEnvironment
1271
- ? {
1272
- schema: 'sks.codex-lb-desktop-center-credentials.v1',
1273
- ok: true,
1274
- status: 'skipped',
1275
- mode: desktopMode,
1276
- api_key_fingerprint: keyFingerprint,
1277
- base_url_present: true,
1278
- launch_env: { api_key: 'skipped', base_url: 'skipped' },
1279
- stale_twins_removed: [],
1280
- stale_twins_quarantined: [],
1281
- stale_keychain_cleared: [],
1282
- blockers: []
1283
- }
1284
- : await syncDesktopCenterLaunchCredentials({
1285
- mode: desktopMode,
1286
- home,
1287
- ...(opts.platform ? { platform: opts.platform } : {}),
1288
- ...(opts.launchctlBin ? { launchctlBin: opts.launchctlBin } : {}),
1289
- ...(opts.securityBin ? { securityBin: opts.securityBin } : {}),
1290
- ...(opts.runProcessImpl ? { runProcessImpl: opts.runProcessImpl } : {}),
1291
- deferLegacyKeychainCleanup: writeEnvFile && !storeKeychain,
1292
- loadedEnv: await loadCodexLbEnv({ ...opts, home, processEnv: {}, envPath, metadataPath })
1293
- }).catch((error) => ({
1294
- ok: false,
1295
- status: 'center_desktop_credentials_failed',
1296
- error: error instanceof Error ? error.message : String(error)
1297
- }));
1298
- appliedActions.push({
1299
- type: 'sync_center_desktop_credentials',
1300
- target: 'launchctl from official sks-codex-lb store',
1301
- ok: centerCredentials.ok === true,
1302
- status: centerCredentials.status
1303
- });
1304
- if (centerCredentials.ok !== true) {
1305
- return rollbackSetupAfterFailure({
1306
- stage: 'sync_center_desktop_credentials',
1307
- error: centerCredentials.error || centerCredentials.status,
1308
- keychain,
1309
- keychainRetained: storeKeychain && keychain.ok === true,
1310
- restoreProcessEnvironment: false,
1311
- externalStateMayBeMutated: true,
1312
- expectedCurrentState: setupExpectedState,
1313
- centerCredentials
1314
- });
1315
- }
1316
- const codexEnvironment = await syncCodexLbProviderEnvironment({ env_path: envPath, base_url: baseUrl }, {
1317
- ...opts,
1318
- home,
1319
- apiKey,
1320
- baseUrl,
1321
- syncLaunchEnv: syncLaunchctl && desktopMode !== 'desktop-dual-auth-compat'
1322
- }).catch((error) => ({
1323
- ok: false,
1324
- status: 'environment_failed',
1325
- error: error instanceof Error ? error.message : String(error)
1326
- }));
1327
- if (syncLaunchctl && desktopMode !== 'desktop-dual-auth-compat') {
1328
- appliedActions.push({ type: 'sync_launchctl', target: 'macOS launchctl user environment (base URL only; API-key env removed)', ok: codexEnvironment.ok === true, status: codexEnvironment.status });
1329
- }
1330
- if (codexEnvironment.ok !== true) {
1331
- return rollbackSetupAfterFailure({
1332
- stage: 'sync_codex_environment',
1333
- error: codexEnvironment.error || codexEnvironment.status,
1334
- keychain,
1335
- keychainRetained: storeKeychain && keychain.ok === true,
1336
- restoreProcessEnvironment: true,
1337
- externalStateMayBeMutated: true,
1338
- expectedCurrentState: setupExpectedState,
1339
- centerCredentials,
1340
- codexEnvironment
1341
- });
1342
- }
1343
- if (writeEnvFile && storeKeychain) {
1344
- try {
1345
- await writeSetupEnvFile();
1346
- }
1347
- catch (error) {
1348
- return rollbackSetupAfterFailure({
1349
- stage: 'write_env_file',
1350
- error,
1351
- keychain,
1352
- keychainRetained: true,
1353
- restoreProcessEnvironment: true,
1354
- externalStateMayBeMutated: true,
1355
- expectedCurrentState: setupExpectedState,
1356
- centerCredentials,
1357
- codexEnvironment
1358
- });
1359
- }
1360
- }
1361
- process.env.CODEX_LB_BASE_URL = baseUrl;
1362
- process.env.CODEX_LB_API_KEY = apiKey;
1363
- shellProfileResult = await installCodexLbShellProfileSnippet({
1364
- home,
1365
- envPath,
1366
- shellProfile,
1367
- expectedFiles: beforeState.files,
1368
- writeFileIfUnchanged: async ({ file, text, mode }) => {
1369
- try {
1370
- await opts.testHooks?.beforeShellProfileWrite?.({ file });
1371
- return await writeSetupFile({
1372
- file,
1373
- text,
1374
- mode,
1375
- ...(typeof opts.testHooks?.beforeSetupFileReplacement === 'function'
1376
- ? { beforeReplacement: opts.testHooks.beforeSetupFileReplacement }
1377
- : {})
1378
- });
1379
- }
1380
- catch (error) {
1381
- const message = error instanceof Error ? error.message : String(error);
1382
- return {
1383
- ok: false,
1384
- status: message.split(':', 1)[0] || 'write_failed',
1385
- error: message
1386
- };
1387
- }
1388
- }
1389
- })
1390
- .catch((err) => ({ ok: false, status: 'failed', files: [], error: err.message }));
1391
- if (shellProfile !== 'skip') {
1392
- appliedActions.push({
1393
- type: 'install_shell_profile_snippet',
1394
- target: shellProfileResult.files?.join(', ') || shellProfile,
1395
- ok: shellProfileResult.ok === true,
1396
- status: shellProfileResult.status
1397
- });
1398
- }
1399
- if (shellProfileResult.ok !== true) {
1400
- return rollbackSetupAfterFailure({
1401
- stage: 'install_shell_profile_snippet',
1402
- error: shellProfileResult.error || shellProfileResult.status,
1403
- keychain,
1404
- keychainRetained: storeKeychain && keychain.ok === true,
1405
- restoreProcessEnvironment: true,
1406
- externalStateMayBeMutated: true,
1407
- expectedCurrentState: setupExpectedState,
1408
- centerCredentials,
1409
- codexEnvironment
1410
- });
1411
- }
1412
- const authAfter = await captureCodexAuthSnapshot({ home, authPath: opts.authPath || codexAuthPath(home) });
1413
- let authInvariantError = null;
1414
- try {
1415
- await assertDesktopAuthUnchangedBySks(authBefore, authAfter);
1416
- }
1417
- catch (error) {
1418
- authInvariantError = error instanceof Error ? error.message : String(error);
1419
- }
1420
- const authReconcile = {
1421
- status: authInvariantError ? 'failed' : 'oauth_untouched',
1422
- reason: authInvariantError || 'credential_setup_does_not_mutate_desktop_auth',
1423
- auth_path: authBefore.path,
1424
- ...(authInvariantError ? { error: authInvariantError } : {})
1425
- };
1426
- const codexLogin = {
1427
- ok: !authInvariantError,
1428
- status: authInvariantError ? 'desktop_auth_byte_invariant_failed' : 'not_required',
1429
- reason: authInvariantError || 'cli_gateway_key_is_separate_from_chatgpt_oauth',
1430
- error: authInvariantError
1431
- };
1432
- if (authInvariantError) {
1433
- return rollbackSetupAfterFailure({
1434
- stage: 'verify_desktop_auth_invariant',
1435
- error: authInvariantError,
1436
- keychain,
1437
- keychainRetained: storeKeychain && keychain.ok === true,
1438
- restoreProcessEnvironment: true,
1439
- externalStateMayBeMutated: true,
1440
- expectedCurrentState: setupExpectedState,
1441
- centerCredentials,
1442
- codexEnvironment
1443
- });
1444
- }
1445
- const finalCodexLb = await codexLbStatus({ ...opts, home, configPath, envPath });
1446
- const keychainOk = !storeKeychain || keychain.ok === true;
1447
- const ok = Boolean(codexEnvironment.ok && codexLogin.ok && keychainOk);
1448
- const afterState = await captureCodexLbSetupWriteState({ home, configPath, envPath, metadataPath, shellProfile });
1449
- const drift = [
1450
- ...detectCodexLbSetupDrift({
1451
- useDefaultProvider,
1452
- writeEnvFile,
1453
- storeKeychain,
1454
- syncLaunchctl,
1455
- shellProfile,
1456
- selected: finalCodexLb.selected,
1457
- envFile: finalCodexLb.env_file,
1458
- keychain,
1459
- codexEnvironment,
1460
- shellProfileResult,
1461
- beforeState,
1462
- afterState
1463
- }),
1464
- ...(authInvariantError ? ['desktop_auth_byte_invariant_failed'] : []),
1465
- ...(!keychainOk ? ['codex_lb_keychain_store_failed'] : []),
1466
- ...(toolCatalog.required !== false && toolCatalog.ok !== true ? ['codex_lb_gpt56_tool_catalog_not_ready'] : [])
1467
- ];
1468
- const appliedPersistenceModes = appliedCodexLbPersistenceModes({
1469
- writeEnvFile,
1470
- storeKeychain,
1471
- syncLaunchctl,
1472
- shellProfile,
1473
- envFile: finalCodexLb.env_file,
1474
- keychain,
1475
- codexEnvironment,
1476
- shellProfileResult,
1477
- apiKeySource: finalCodexLb.env_loader?.api_key?.source || null
1478
- });
1479
- const persistence = codexLbPersistenceSummary({
1480
- selectedModes: selectedPersistenceModes,
1481
- appliedModes: appliedPersistenceModes,
1482
- processOnly: appliedPersistenceModes.includes('process_only_ephemeral')
1483
- });
1484
- const warnings = [
1485
- ...insecureLocalWarning,
1486
- ...persistence.warnings,
1487
- ...(preservedLegacySelection ? ['legacy_desktop_auth_routing_requires_explicit_migrate_legacy_desktop'] : []),
1488
- ...(toolCatalog.required !== false && toolCatalog.ok !== true ? ['codex_lb_gpt56_tool_catalog_not_ready'] : [])
1489
- ];
1490
- if (!ok || drift.length > 0) {
1491
- return rollbackSetupAfterFailure({
1492
- stage: 'verify_final_setup_state',
1493
- error: drift.join(',') || 'configuration_failed',
1494
- keychain,
1495
- keychainRetained: storeKeychain && keychain.ok === true,
1496
- restoreProcessEnvironment: true,
1497
- externalStateMayBeMutated: true,
1498
- expectedCurrentState: setupExpectedState,
1499
- centerCredentials,
1500
- codexEnvironment
1501
- });
1502
- }
1503
- const retainedSetupRecoveryPaths = [];
1504
- for (const recoveryPath of [...new Set(setupWriteRecoveryPaths)]) {
1505
- if (await removeCodexLbSetupRecoveryPath(recoveryPath)) {
1506
- for (const action of appliedActions) {
1507
- if (action.recovery_path === recoveryPath) {
1508
- action.recovery_path = null;
1509
- action.recovery_claim_cleanup = 'removed_after_final_verification';
1510
- }
1511
- }
1512
- continue;
1513
- }
1514
- retainedSetupRecoveryPaths.push(recoveryPath);
1515
- await hardenCodexLbSetupRecoveryPath(recoveryPath);
1516
- }
1517
- const retainedSecretRecoveryPaths = retainedSetupRecoveryPaths
1518
- .filter((recoveryPath) => isRecoveryPathForFile(recoveryPath, envPath));
1519
- let legacyKeychainCleanup = {
1520
- ok: true,
1521
- status: 'not_applicable',
1522
- keychain_cleared: [],
1523
- blockers: []
1524
- };
1525
- if ((opts.platform || process.platform) === 'darwin' && writeEnvFile && !storeKeychain) {
1526
- await opts.testHooks?.beforeLegacyKeychainCleanup?.({ envPath, metadataPath });
1527
- const migration = await repairCodexLbLegacyKeychainMigration({
1528
- home,
1529
- baseUrl,
1530
- envPath,
1531
- metadataPath,
1532
- account: opts.account || process.env.USER || 'sks',
1533
- platform: opts.platform || process.platform,
1534
- ...(opts.securityBin ? { securityBin: opts.securityBin } : {}),
1535
- ...(opts.runProcessImpl ? { runProcessImpl: opts.runProcessImpl } : {}),
1536
- expectedApiKeySha256: keyFingerprint
1537
- });
1538
- const cleanupBlockers = [...new Set(migration.blockers)];
1539
- const replacementStoreVerified = migration.env_key_valid
1540
- && migration.status !== 'replacement_store_unverified'
1541
- && migration.status !== 'env_file_unsafe';
1542
- const cleanupOk = replacementStoreVerified
1543
- && migration.ok
1544
- && cleanupBlockers.length === 0;
1545
- legacyKeychainCleanup = {
1546
- ok: cleanupOk,
1547
- status: !replacementStoreVerified
1548
- ? 'legacy_keychain_cleanup_blocked_replacement_store_unverified'
1549
- : cleanupOk
1550
- ? migration.keychain_cleared.length > 0
1551
- ? 'legacy_keychain_removed'
1552
- : 'legacy_keychain_absent'
1553
- : 'legacy_keychain_cleanup_failed_secure_store_retained',
1554
- replacement_store_verified: replacementStoreVerified,
1555
- keychain_cleared: migration.keychain_cleared,
1556
- blockers: cleanupBlockers
1557
- };
1558
- appliedActions.push({
1559
- type: 'migrate_legacy_keychain',
1560
- target: 'macOS Keychain service sks-codex-lb',
1561
- ok: legacyKeychainCleanup.ok,
1562
- status: legacyKeychainCleanup.status
1563
- });
1564
- }
1565
- const finalWarnings = [
1566
- ...warnings,
1567
- ...(retainedSetupRecoveryPaths.length > 0 ? ['setup_recovery_claim_cleanup_required'] : []),
1568
- ...(retainedSecretRecoveryPaths.length > 0 ? ['setup_secret_recovery_retained'] : []),
1569
- ...(!legacyKeychainCleanup.ok
1570
- ? ['legacy_keychain_cleanup_indeterminate_rotate_provider_key']
1571
- : legacyKeychainCleanup.keychain_cleared.length > 0
1572
- ? ['legacy_keychain_removed_rotate_provider_key_if_not_already_rotated']
1573
- : [])
1574
- ];
1575
- return {
1576
- ok: legacyKeychainCleanup.ok,
1577
- status: legacyKeychainCleanup.ok
1578
- ? 'configured'
1579
- : 'legacy_keychain_cleanup_failed_secure_store_retained',
1580
- mode: desktopMode,
1581
- identity_plane: 'unchanged',
1582
- routing_plane: codexLbRoutingPlane(desktopMode),
1583
- gateway_auth_transport: opts.gatewayAuthTransport || DEFAULT_CODEX_LB_GATEWAY_AUTH_TRANSPORT,
1584
- oauth_preserved: !authInvariantError,
1585
- auth_mutated: false,
1586
- plan: plan,
1587
- applied_actions: appliedActions,
1588
- drift: [...drift, ...legacyKeychainCleanup.blockers],
1589
- persistence,
1590
- center_credentials: centerCredentials,
1591
- config_path: configPath,
1592
- env_path: envPath,
1593
- metadata_path: metadataPath,
1594
- backup_path: safeWrite.backup_path || null,
1595
- recovery_paths: [
1596
- ...new Set([
1597
- ...(safeWrite.backup_path ? [safeWrite.backup_path] : []),
1598
- ...retainedSetupRecoveryPaths
1599
- ])
1600
- ],
1601
- secret_recovery_paths: retainedSecretRecoveryPaths,
1602
- base_url: baseUrl,
1603
- env_key: 'CODEX_LB_API_KEY',
1604
- keychain,
1605
- legacy_keychain_cleanup: legacyKeychainCleanup,
1606
- warnings: finalWarnings,
1607
- auth_reconcile: authReconcile,
1608
- codex_lb: finalCodexLb,
1609
- codex_environment: codexEnvironment,
1610
- codex_login: codexLogin,
1611
- tool_catalog: toolCatalog,
1612
- tool_output_recovery: toolOutputRecovery,
1613
- error: authReconcile.error || codexEnvironment.error || codexLogin.error || null
1614
- };
1615
- }
1616
- function isRecoveryPathForFile(recoveryPath, file) {
1617
- const target = path.resolve(file);
1618
- const candidate = path.resolve(String(recoveryPath || ''));
1619
- return candidate.startsWith(`${target}.sks-`);
1620
- }
1621
- function desktopModeRequiresGlobalSecretEnvironment(mode) {
1622
- return mode === 'desktop-dual-auth-compat';
1623
- }
1624
- export async function codexLbStatus(opts = {}) {
1625
- const home = opts.home || process.env.HOME || os.homedir();
1626
- const configPath = opts.configPath || codexLbConfigPath(home);
1627
- const envPath = opts.envPath || codexLbEnvPath(home);
1628
- const rawConfig = await readText(configPath, '');
1629
- const orphanManagedMarkerCleanup = removeCodexLbOrphanManagedMarkers(rawConfig);
1630
- const config = orphanManagedMarkerCleanup.text;
1631
- const envExists = await exists(envPath);
1632
- const envLoad = await loadCodexLbEnv({ ...opts, home, envPath });
1633
- const authPath = opts.authPath || codexAuthPath(home);
1634
- const authText = await readText(authPath, '');
1635
- const authMode = codexAuthModeSummary(authText);
1636
- const authApiKey = parseCodexAuthApiKey(authText);
1637
- const envKeyConfigured = Boolean(envLoad.secret_api_key);
1638
- const providerConfigured = /\[model_providers\.codex-lb\]/.test(config);
1639
- const selected = hasTopLevelCodexLbSelected(config);
1640
- const providerBaseUrl = normalizeCodexLbBaseUrl(codexLbProviderBaseUrl(config));
1641
- const credentialBaseUrl = envLoad.base_url;
1642
- const baseUrl = credentialBaseUrl || providerBaseUrl || null;
1643
- const providerBaseUrlMatchesCredential = Boolean(providerBaseUrl && credentialBaseUrl && providerBaseUrl === credentialBaseUrl);
1644
- const providerName = codexLbProviderName(config);
1645
- const providerWireApi = codexLbProviderWireApi(config);
1646
- const providerSupportsWebsockets = codexLbProviderSupportsWebsockets(config);
1647
- const providerRequiresOpenAiAuth = codexLbProviderRequiresOpenAiAuth(config);
1648
- const providerOpenAiAuthDisabled = codexLbProviderOpenAiAuthDisabled(config);
1649
- const providerEnvKey = codexLbProviderEnvKey(config);
1650
- const providerHasSeparateGatewayAuth = codexLbProviderHasSeparateGatewayAuth(config);
1651
- const desktopMode = managedCodexLbDesktopMode(config);
1652
- const bridgeBaseUrl = desktopMode === 'desktop-native-bridge'
1653
- ? topLevelTomlString(config, 'openai_base_url')
1654
- : '';
1655
- const sharedOpenAiRouting = codexLbSharedOpenAiRoutingState(config, baseUrl);
1656
- const codexLbKeyInSharedAuth = Boolean(authApiKey && envLoad.secret_api_key && authApiKey === envLoad.secret_api_key);
1657
- const sharedOpenAiRoutingSafe = !codexLbKeyInSharedAuth || sharedOpenAiRouting.status === 'matched';
1658
- const cliProviderContractOk = providerConfigured
1659
- && providerBaseUrlMatchesCredential
1660
- && providerName === 'codex-lb'
1661
- && providerWireApi === 'responses'
1662
- && providerEnvKey === CODEX_LB_PROVIDER_ENV_KEY
1663
- && providerHasSeparateGatewayAuth
1664
- && providerSupportsWebsockets === true
1665
- && providerOpenAiAuthDisabled;
1666
- const retiredCompatConfigured = desktopMode === 'desktop-dual-auth-compat';
1667
- const definedButNotSelected = providerConfigured && !selected && desktopMode === null;
1668
- const providerContractOk = !retiredCompatConfigured && cliProviderContractOk;
1669
- const providerUsesCodexLbEnvAuth = providerConfigured
1670
- && providerEnvKey === CODEX_LB_PROVIDER_ENV_KEY
1671
- && providerHasSeparateGatewayAuth
1672
- && providerOpenAiAuthDisabled;
1673
- const oauthAvailable = authMode.mode === 'chatgpt_oauth' || authMode.mode === 'browser_marker';
1674
- const authRoutingCoherent = desktopMode === 'desktop-native-bridge'
1675
- ? !selected && oauthAvailable && !codexLbKeyInSharedAuth
1676
- : desktopMode === 'desktop-dual-auth-compat'
1677
- ? selected && oauthAvailable && !codexLbKeyInSharedAuth
1678
- : !codexLbKeyInSharedAuth && (!selected || providerUsesCodexLbEnvAuth);
1679
- const codexAppUsableWithCodexLb = desktopMode === 'desktop-native-bridge'
1680
- ? Boolean(bridgeBaseUrl && oauthAvailable)
1681
- : retiredCompatConfigured
1682
- ? false
1683
- : authMode.codex_app_usable;
1684
- const fastMode = codexLbFastModeConfigStatus(config);
1685
- const launchEnvironment = await inspectCodexLbMacLaunchEnvironment(baseUrl, opts).catch((err) => ({
1686
- checked: true,
1687
- available: false,
1688
- status: 'inspect_failed',
1689
- error: err.message
1690
- }));
1691
- const providerReady = providerContractOk
1692
- && envLoad.configured
1693
- && Boolean(baseUrl)
1694
- && authRoutingCoherent
1695
- && !retiredCompatConfigured
1696
- && (desktopMode === 'desktop-native-bridge' ? Boolean(bridgeBaseUrl) : true);
1697
- const blockers = [
1698
- ...(retiredCompatConfigured ? ['desktop_dual_auth_compat_unavailable'] : []),
1699
- ...(!retiredCompatConfigured && providerConfigured && !providerContractOk
1700
- ? ['codex_lb_provider_contract_drift']
1701
- : [])
1702
- ];
1703
- const probeToolOutputRecovery = opts.probeToolOutputRecovery === true;
1704
- const toolOutputRecovery = !selected
1705
- ? codexLbToolOutputRecoveryNotSelected()
1706
- : !probeToolOutputRecovery || !providerContractOk || !envLoad.configured
1707
- ? codexLbToolOutputRecoveryNotChecked(true)
1708
- : await probeCodexLbToolOutputRecovery({
1709
- baseUrl,
1710
- ...(typeof opts.toolOutputRecoveryFetch === 'function' ? { fetchImpl: opts.toolOutputRecoveryFetch } : {}),
1711
- timeoutMs: Number(opts.toolOutputRecoveryTimeoutMs || 4_000),
1712
- allowUnverified: opts.allowUnverifiedToolOutputRecovery === true
1713
- || codexLbToolOutputRecoveryOverrideAcknowledged({ env: opts.env || process.env })
1714
- });
1715
- const secretResolution = {
1716
- source: envLoad.source,
1717
- path: envLoad.source === 'env-file' ? envLoad.env_paths[0] || envPath : null,
1718
- prompt_risk: 'none'
1719
- };
1720
- return {
1721
- ok: providerReady && (!selected || !probeToolOutputRecovery || toolOutputRecovery.ok),
1722
- blockers,
1723
- warnings: definedButNotSelected ? ['codex_lb_defined_but_not_selected'] : [],
1724
- orphan_managed_markers: orphanManagedMarkerCleanup.orphan_markers,
1725
- managed_marker_cleanup_required: orphanManagedMarkerCleanup.changed,
1726
- activation_guidance: definedButNotSelected
1727
- ? [
1728
- 'Run `sks codex-lb use-cli` to select the CLI provider.',
1729
- 'Run `sks codex-lb use-desktop-full` to activate managed Desktop bridge mode.'
1730
- ]
1731
- : [],
1732
- provider_ready: providerReady,
1733
- config_path: configPath,
1734
- env_path: envPath,
1735
- provider_configured: providerConfigured,
1736
- provider_base_url: providerBaseUrl || null,
1737
- credential_base_url: credentialBaseUrl || null,
1738
- provider_base_url_matches_credential: providerBaseUrlMatchesCredential,
1739
- provider_name: providerName || null,
1740
- provider_wire_api: providerWireApi || null,
1741
- provider_supports_websockets: providerSupportsWebsockets,
1742
- provider_contract_ok: providerContractOk,
1743
- provider_requires_openai_auth: providerRequiresOpenAiAuth,
1744
- provider_openai_auth_disabled: providerOpenAiAuthDisabled,
1745
- provider_env_key: providerEnvKey || null,
1746
- provider_has_separate_gateway_auth: providerHasSeparateGatewayAuth,
1747
- provider_uses_codex_lb_env_auth: providerUsesCodexLbEnvAuth,
1748
- desktop_mode: desktopMode || (selected ? 'legacy-or-manual-selected-provider' : 'cli-provider'),
1749
- bridge_base_url: bridgeBaseUrl || null,
1750
- legacy_migration_required: Boolean(!desktopMode
1751
- && selected
1752
- && authMode.mode === 'apikey'
1753
- && sharedOpenAiRouting.managed),
1754
- selected,
1755
- env_file: envExists,
1756
- env_key_configured: envKeyConfigured,
1757
- env_base_url_configured: Boolean(envLoad.base_url),
1758
- env_loader: {
1759
- configured: envLoad.configured,
1760
- missing: envLoad.missing,
1761
- source: envLoad.source,
1762
- source_priority: envLoad.source_priority,
1763
- api_key: envLoad.api_key,
1764
- blockers: envLoad.blockers,
1765
- guidance: envLoad.guidance,
1766
- credential_binding: envLoad.credential_binding,
1767
- keychain: envLoad.keychain,
1768
- env_paths: envLoad.env_paths
1769
- },
1770
- secret_resolution: secretResolution,
1771
- base_url: baseUrl,
1772
- auth_path: authPath,
1773
- auth_mode: authMode.mode,
1774
- codex_lb_key_in_shared_auth: codexLbKeyInSharedAuth,
1775
- auth_routing_coherent: authRoutingCoherent,
1776
- shared_openai_routing: {
1777
- status: sharedOpenAiRouting.status,
1778
- safe: sharedOpenAiRoutingSafe,
1779
- managed: sharedOpenAiRouting.managed,
1780
- configured_base_url: sharedOpenAiRouting.configured_base_url
1781
- },
1782
- auth_usable_for_codex_app: codexAppUsableWithCodexLb && authRoutingCoherent,
1783
- auth_summary: !sharedOpenAiRoutingSafe
1784
- ? 'codex-lb key is active in shared auth without a verified built-in OpenAI routing guard'
1785
- : !authRoutingCoherent
1786
- ? 'Codex provider selection and shared auth mode do not form a coherent codex-lb/OAuth state'
1787
- : codexAppUsableWithCodexLb ? `codex-lb provider uses ${authMode.mode} OpenAI-style auth through Codex App` : authMode.summary,
1788
- fast_mode: fastMode,
1789
- launch_environment: launchEnvironment,
1790
- tool_output_recovery: toolOutputRecovery
1791
- };
1792
- }
1793
- export function formatCodexLbStatusText(status = {}, opts = {}) {
1794
- const backupPresent = Boolean(opts.backupPresent);
1795
- const backupPath = opts.backupPath || '';
1796
- const resolution = status.secret_resolution || {};
1797
- const resolutionSource = String(resolution.source || status.env_loader?.source || 'missing');
1798
- const resolutionPath = resolution.path
1799
- || (resolutionSource === 'env-file' ? status.env_loader?.env_paths?.[0] || status.env_path : null);
1800
- const displayResolutionPath = resolutionPath
1801
- ? formatCodexLbStatusPath(String(resolutionPath), opts.home || os.homedir())
1802
- : '';
1803
- const resolutionLabel = displayResolutionPath
1804
- ? `${resolutionSource} (${displayResolutionPath})`
1805
- : resolutionSource;
1806
- const keychainUsage = resolutionSource === 'keychain' ? 'used' : 'not used';
1807
- const promptRisk = String(resolution.prompt_risk
1808
- || (resolutionSource === 'keychain' ? 'possible' : 'none'));
1809
- const lines = [
1810
- 'SKS codex-lb',
1811
- '',
1812
- `Configured: ${status.ok ? 'yes' : 'no'}`,
1813
- `Selected: ${status.selected ? 'yes' : 'no'}`,
1814
- `Provider: ${status.provider_contract_ok ? 'codex-lb App contract ok' : status.provider_configured ? 'drifted' : 'missing'}`,
1815
- `Provider OpenAI Auth: ${status.provider_requires_openai_auth ? 'required' : 'not required/drifted'} (${status.provider_name || 'missing'})`,
1816
- `Codex App auth: ${status.auth_usable_for_codex_app ? 'ok' : 'needs sign-in/repair'} (${status.auth_mode || 'unknown'})`,
1817
- `Shared OpenAI routing: ${status.shared_openai_routing?.safe === false ? 'unsafe' : status.shared_openai_routing?.status || 'unknown'}${status.shared_openai_routing?.managed ? ' (sks-managed)' : ''}`,
1818
- `Auth/routing coherent: ${status.auth_routing_coherent ? 'yes' : 'no'}`,
1819
- `Key source: ${resolutionLabel} · keychain: ${keychainUsage} · prompt risk: ${promptRisk}`
1820
- ];
1821
- if (status.tool_output_recovery?.status && status.tool_output_recovery.status !== 'not_selected') {
1822
- const recovery = status.tool_output_recovery;
1823
- lines.push(`Interrupted tool-output recovery: ${recovery.ok ? 'ready' : 'blocked'} (${recovery.observed_version || recovery.status}; minimum ${recovery.minimum_version})`);
1824
- if (!recovery.ok) {
1825
- for (const action of recovery.operator_actions || [])
1826
- lines.push(` action: ${action}`);
1827
- }
1828
- }
1829
- if (status.auth_summary)
1830
- lines.push(`Auth detail: ${status.auth_summary}`);
1831
- if (status.fast_mode) {
1832
- const fast = status.fast_mode;
1833
- lines.push(`Fast Mode: ${fast.configured ? `configured request=${fast.codex_request_service_tier} upstream=${fast.codex_lb_upstream_service_tier}` : 'not configured'}`);
1834
- if (!fast.actual_service_tier_verified)
1835
- lines.push(`Fast proof: unverified until ${fast.proof_required}. Run: ${fast.verification_command}`);
1836
- }
1837
- lines.push(`Env file: ${status.env_file ? status.env_path : 'missing'}`);
1838
- if (status.base_url)
1839
- lines.push(`Base URL: ${status.base_url}`);
1840
- lines.push(`ChatGPT backup: ${backupPresent ? `yes (${backupPath})` : 'no'}`);
1841
- if ((status.warnings || []).includes('codex_lb_defined_but_not_selected')) {
1842
- lines.push('', 'Warning [codex_lb_defined_but_not_selected]: the codex-lb provider is defined but not selected.');
1843
- for (const action of status.activation_guidance || [])
1844
- lines.push(` ${action}`);
1845
- }
1846
- if (status.legacy_migration_required || status.shared_openai_routing?.safe === false)
1847
- lines.push('', 'Run: sks codex-lb migrate-legacy-desktop --restart-app. Ordinary repair will not rewrite shared Codex auth.');
1848
- else if (status.provider_configured && !status.provider_contract_ok)
1849
- lines.push('', 'Run: sks codex-lb repair to rewrite the provider block to the current codex-lb App contract.');
1850
- else if (status.desktop_mode === 'desktop-native-bridge' && !status.auth_usable_for_codex_app)
1851
- lines.push('', 'Sign in with ChatGPT OAuth, then run: sks codex-lb use-desktop-full.');
1852
- else if (status.ok && status.desktop_mode === 'cli-provider' && !status.selected && !(status.warnings || []).includes('codex_lb_defined_but_not_selected'))
1853
- 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.');
1854
- else if (status.desktop_mode === 'desktop-dual-auth-compat')
1855
- lines.push('', 'The retired compatibility route is blocked. Run `sks codex-lb use-desktop-full` or `sks codex-lb disable`.');
1856
- else if (status.ok)
1857
- lines.push('', 'Status: configured; no ordinary repair needed.');
1858
- else if (!status.ok && status.base_url && status.env_key_configured)
1859
- lines.push('', 'Run: sks codex-lb repair. To change routing explicitly, use `use-desktop-full`, `use-cli`, or `disable`.');
1860
- else if (!status.ok && !status.env_key_configured)
1861
- lines.push('', 'Gateway key missing. Store CODEX_LB_API_KEY in ~/.codex/sks-codex-lb.env, run `sks codex-lb setup --host <domain> --api-key-stdin --yes`, or provide CODEX_LB_API_KEY in the environment.');
1862
- else if (!status.ok)
1863
- lines.push('', 'Run: sks codex-lb setup --host <domain> --api-key-stdin');
1864
- if (backupPresent)
1865
- lines.push('Legacy OAuth backup detected; use `sks codex-lb migrate-legacy-desktop --restart-app` instead of background auth switching.');
1866
- return `${lines.join('\n')}\n`;
1867
- }
1868
- function formatCodexLbStatusPath(file, home) {
1869
- const resolvedFile = path.resolve(file);
1870
- const resolvedHome = path.resolve(String(home || ''));
1871
- const relative = path.relative(resolvedHome, resolvedFile);
1872
- return relative && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)
1873
- ? `~/${relative.split(path.sep).join('/')}`
1874
- : file;
1875
- }
1876
- export function formatCodexLbRepairResultText(result = {}) {
1877
- const lines = [
1878
- 'codex-lb credential/provider configuration repaired without changing Codex shared auth.',
1879
- `Config: ${result.config_path}`,
1880
- `Key env: ${result.env_path}`
1881
- ];
1882
- if (result.auth_reconcile?.status === 'oauth_untouched')
1883
- lines.push('Codex App auth: unchanged.');
1884
- else if (result.auth_reconcile?.status === 'legacy_migration_required')
1885
- lines.push('Legacy Desktop auth routing: run `sks codex-lb migrate-legacy-desktop --restart-app` explicitly.');
1886
- return `${lines.join('\n')}\n`;
1887
- }
1888
- function codexLbProviderBaseUrl(text = '') {
1889
- const block = String(text || '').match(/(^|\n)\[model_providers\.codex-lb\]([\s\S]*?)(?=\n\[[^\]]+\]|\s*$)/)?.[2] || '';
1890
- return block.match(/(^|\n)\s*base_url\s*=\s*"([^"]+)"/)?.[2] || '';
1891
- }
1892
- function codexLbProviderName(text = '') {
1893
- const block = String(text || '').match(/(^|\n)\[model_providers\.codex-lb\]([\s\S]*?)(?=\n\[[^\]]+\]|\s*$)/)?.[2] || '';
1894
- return (block.match(/(^|\n)\s*name\s*=\s*"([^"]+)"/)?.[2] || '').trim();
1895
- }
1896
- function codexLbProviderWireApi(text = '') {
1897
- const block = String(text || '').match(/(^|\n)\[model_providers\.codex-lb\]([\s\S]*?)(?=\n\[[^\]]+\]|\s*$)/)?.[2] || '';
1898
- return (block.match(/(^|\n)\s*wire_api\s*=\s*"([^"]+)"/)?.[2] || '').trim();
1899
- }
1900
- function codexLbProviderSupportsWebsockets(text = '') {
1901
- const block = String(text || '').match(/(^|\n)\[model_providers\.codex-lb\]([\s\S]*?)(?=\n\[[^\]]+\]|\s*$)/)?.[2] || '';
1902
- if (/(^|\n)\s*supports_websockets\s*=\s*true\s*(?:#.*)?(?=\n|$)/.test(block))
1903
- return true;
1904
- if (/(^|\n)\s*supports_websockets\s*=\s*false\s*(?:#.*)?(?=\n|$)/.test(block))
1905
- return false;
1906
- return null;
1907
- }
1908
- function codexLbProviderRequiresOpenAiAuth(text = '') {
1909
- const block = String(text || '').match(/(^|\n)\[model_providers\.codex-lb\]([\s\S]*?)(?=\n\[[^\]]+\]|\s*$)/)?.[2] || '';
1910
- return /(^|\n)\s*requires_openai_auth\s*=\s*true\s*(?:#.*)?(?=\n|$)/.test(block);
1911
- }
1912
- function codexLbProviderOpenAiAuthDisabled(text = '') {
1913
- const block = String(text || '').match(/(^|\n)\[model_providers\.codex-lb\]([\s\S]*?)(?=\n\[[^\]]+\]|\s*$)/)?.[2] || '';
1914
- return /(^|\n)\s*requires_openai_auth\s*=\s*false\s*(?:#.*)?(?=\n|$)/.test(block);
1915
- }
1916
- function codexLbProviderEnvKey(text = '') {
1917
- const block = String(text || '').match(/(^|\n)\[model_providers\.codex-lb\]([\s\S]*?)(?=\n\[[^\]]+\]|\s*$)/)?.[2] || '';
1918
- return block.match(/(^|\n)\s*env_key\s*=\s*"([^"]+)"/)?.[2]
1919
- || block.match(/env_http_headers\s*=\s*\{[^}]*"X-Codex-LB-API-Key"\s*=\s*"([^"]+)"[^}]*\}/)?.[1]
1920
- || '';
1921
- }
1922
- function codexLbProviderHasSeparateGatewayAuth(text = '') {
1923
- const block = String(text || '').match(/(^|\n)\[model_providers\.codex-lb\]([\s\S]*?)(?=\n\[[^\]]+\]|\s*$)/)?.[2] || '';
1924
- return /(^|\n)\s*env_key\s*=\s*"CODEX_LB_API_KEY"\s*(?:#.*)?(?=\n|$)/.test(block)
1925
- && !/X-Codex-LB-API-Key/.test(block);
1926
- }
1927
- function codexLbFastModeConfigStatus(text = '') {
1928
- const globalServiceTier = topLevelTomlString(text, 'service_tier');
1929
- const profileBlock = String(text || '').match(/(^|\n)\[profiles\.sks-fast-high\]([\s\S]*?)(?=\n\[[^\]]+\]|\s*$)/)?.[2] || '';
1930
- const profileServiceTier = profileBlock.match(/(^|\n)\s*service_tier\s*=\s*"([^"]+)"/)?.[2] || '';
1931
- const configured = globalServiceTier === 'fast' || globalServiceTier === CODEX_LB_CANONICAL_FAST_SERVICE_TIER;
1932
- return {
1933
- schema: 'sks.codex-lb-fast-mode-config.v1',
1934
- configured,
1935
- top_level_service_tier: globalServiceTier || null,
1936
- legacy_profile_service_tier: profileServiceTier || null,
1937
- codex_request_service_tier: configured ? 'fast' : null,
1938
- codex_lb_upstream_service_tier: configured ? CODEX_LB_CANONICAL_FAST_SERVICE_TIER : null,
1939
- actual_service_tier_verified: false,
1940
- verification_command: 'sks codex-lb fast-check --json',
1941
- proof_required: 'codex-lb request log must show requestedServiceTier=priority and actualServiceTier/serviceTier=priority'
1942
- };
1943
- }
1944
- function topLevelTomlString(text = '', key) {
1945
- const topLevel = String(text || '').split(/\n\s*\[/)[0] || '';
1946
- return topLevel.match(new RegExp(`(^|\\n)\\s*${escapeRegExp(key)}\\s*=\\s*"([^"]+)"\\s*(?:#.*)?(?=\\n|$)`))?.[2] || '';
1947
- }
1948
- function tomlTableString(text = '', table, key) {
1949
- const block = String(text || '').match(new RegExp(`(^|\\n)\\[${escapeRegExp(table)}\\]([\\s\\S]*?)(?=\\n\\[[^\\]]+\\]|\\s*$)`))?.[2] || '';
1950
- return block.match(new RegExp(`(^|\\n)\\s*${escapeRegExp(key)}\\s*=\\s*"([^"]+)"\\s*(?:#.*)?(?=\\n|$)`))?.[2] || '';
1951
- }
1952
- export async function repairCodexLbAuth(opts = {}) {
1953
- const home = opts.home || process.env.HOME || os.homedir();
1954
- let status = await codexLbStatus({ ...opts, home });
1955
- const authPath = opts.authPath || status.auth_path || codexAuthPath(home);
1956
- const authBefore = await captureCodexAuthSnapshot({ home, authPath });
1957
- const currentConfig = await readText(status.config_path, '');
1958
- const legacyEnv = await loadCodexLbEnv({ ...opts, home });
1959
- const legacyDetection = await detectLegacyCodexLbDesktopState({
1960
- home,
1961
- configPath: status.config_path,
1962
- authPath,
1963
- ...(status.base_url ? { remoteBaseUrl: status.base_url } : {}),
1964
- ...(legacyEnv.secret_api_key
1965
- ? { expectedGatewayApiKey: legacyEnv.secret_api_key }
1966
- : {})
1967
- });
1968
- const legacySharedAuthSelected = status.selected === true
1969
- && status.auth_mode === 'apikey'
1970
- && status.shared_openai_routing?.managed === true;
1971
- if (legacyDetection.legacy_destructive_mode || legacySharedAuthSelected) {
1972
- return {
1973
- ok: false,
1974
- status: 'legacy_migration_required',
1975
- reason: 'legacy_desktop_auth_routing_requires_explicit_migration',
1976
- config_path: status.config_path,
1977
- env_path: status.env_path,
1978
- base_url: status.base_url,
1979
- codex_lb: status,
1980
- auth_reconcile: {
1981
- status: 'legacy_migration_required',
1982
- reason: 'run_sks_codex_lb_migrate_legacy_desktop',
1983
- auth_path: authPath
1984
- },
1985
- codex_login: {
1986
- ok: true,
1987
- status: 'not_required',
1988
- reason: 'ordinary_repair_never_changes_shared_codex_auth'
1989
- },
1990
- tool_catalog: {
1991
- ok: true,
1992
- required: false,
1993
- status: 'not_bound_during_repair',
1994
- config_changed: false
1995
- }
1996
- };
1997
- }
1998
- if (!status.env_key_configured && status.base_url && (status.provider_configured || status.selected || status.env_base_url_configured)) {
1999
- if (status.auth_mode === 'apikey') {
2000
- return {
2001
- ok: false,
2002
- status: 'legacy_migration_required',
2003
- reason: 'shared_codex_api_key_cannot_be_assumed_to_be_the_codex_lb_gateway_key',
2004
- config_path: status.config_path,
2005
- env_path: status.env_path,
2006
- base_url: status.base_url,
2007
- codex_lb: status,
2008
- auth_reconcile: {
2009
- status: 'legacy_migration_required',
2010
- reason: 'run_sks_codex_lb_setup_with_a_separate_gateway_key_or_migrate_legacy_desktop',
2011
- auth_path: authPath
2012
- },
2013
- codex_login: {
2014
- ok: true,
2015
- status: 'not_required',
2016
- reason: 'ordinary_repair_never_reads_gateway_credentials_from_shared_codex_auth'
2017
- },
2018
- tool_catalog: {
2019
- ok: true,
2020
- required: false,
2021
- status: 'not_bound_during_repair',
2022
- config_changed: false
2023
- }
2024
- };
2025
- }
2026
- }
2027
- if (!status.env_key_configured || !status.base_url) {
2028
- return {
2029
- ok: false,
2030
- status: !status.env_key_configured ? 'missing_env_key' : 'missing_base_url',
2031
- config_path: status.config_path,
2032
- env_path: status.env_path,
2033
- codex_lb: status,
2034
- auth_reconcile: {
2035
- status: 'oauth_untouched',
2036
- reason: 'ordinary_repair_never_changes_shared_codex_auth',
2037
- auth_path: authPath
2038
- }
2039
- };
2040
- }
2041
- const configBeforeRepair = await readText(status.config_path, '');
2042
- const desktopMode = managedCodexLbDesktopMode(configBeforeRepair);
2043
- let nextConfig = configBeforeRepair;
2044
- try {
2045
- if (desktopMode === 'desktop-native-bridge') {
2046
- const bridgeBaseUrl = topLevelTomlString(configBeforeRepair, 'openai_base_url');
2047
- if (!bridgeBaseUrl)
2048
- throw new Error('managed_desktop_bridge_missing_base_url');
2049
- nextConfig = upsertCodexLbNativeDesktopConfig(configBeforeRepair, {
2050
- bridgeBaseUrl,
2051
- remoteBaseUrl: status.base_url
2052
- });
2053
- }
2054
- else if (desktopMode === 'desktop-dual-auth-compat') {
2055
- nextConfig = upsertCodexLbCompatDesktopConfig(configBeforeRepair, {
2056
- remoteBaseUrl: status.base_url
2057
- });
2058
- }
2059
- else {
2060
- nextConfig = upsertCodexLbCliProviderConfig(configBeforeRepair, {
2061
- remoteBaseUrl: status.base_url,
2062
- selectGlobally: status.selected === true
2063
- });
2064
- }
2065
- }
2066
- catch (error) {
2067
- return {
2068
- ok: false,
2069
- status: 'managed_desktop_routing_conflict',
2070
- config_path: status.config_path,
2071
- env_path: status.env_path,
2072
- base_url: status.base_url,
2073
- codex_lb: status,
2074
- error: error instanceof Error ? error.message : String(error)
2075
- };
2076
- }
2077
- await ensureDir(path.dirname(status.config_path));
2078
- const configWrite = await safeWriteCodexConfigToml(status.config_path, configBeforeRepair, nextConfig, 'codex-lb-safe-repair', { verifyUnchangedBeforeWrite: true });
2079
- if (!configWrite.ok) {
2080
- return {
2081
- ok: false,
2082
- status: configWrite.status,
2083
- config_path: status.config_path,
2084
- env_path: status.env_path,
2085
- base_url: status.base_url,
2086
- codex_lb: status,
2087
- error: 'codex_lb_config_repair_failed'
2088
- };
2089
- }
2090
- const codexEnvironment = await syncCodexLbProviderEnvironment(status, opts);
2091
- const authAfter = await captureCodexAuthSnapshot({ home, authPath });
2092
- let authInvariantError = null;
2093
- try {
2094
- await assertDesktopAuthUnchangedBySks(authBefore, authAfter);
2095
- }
2096
- catch (error) {
2097
- authInvariantError = error instanceof Error ? error.message : String(error);
2098
- }
2099
- if (authInvariantError) {
2100
- const writtenConfig = await readText(status.config_path, '');
2101
- await safeWriteCodexConfigToml(status.config_path, writtenConfig, configBeforeRepair, 'codex-lb-safe-repair-auth-invariant-rollback', { verifyUnchangedBeforeWrite: true });
2102
- }
2103
- const finalStatus = await codexLbStatus({ ...opts, home });
2104
- const toolOutputRecovery = opts.probeToolOutputRecovery === true && status.selected
2105
- ? await probeCodexLbToolOutputRecovery({
2106
- baseUrl: status.base_url,
2107
- ...(typeof opts.toolOutputRecoveryFetch === 'function' ? { fetchImpl: opts.toolOutputRecoveryFetch } : {}),
2108
- timeoutMs: Number(opts.toolOutputRecoveryTimeoutMs || 4_000),
2109
- allowUnverified: opts.allowUnverifiedToolOutputRecovery === true
2110
- || codexLbToolOutputRecoveryOverrideAcknowledged({ env: opts.env || process.env })
2111
- })
2112
- : codexLbToolOutputRecoveryNotChecked(status.selected === true);
2113
- const authReconcile = {
2114
- status: authInvariantError ? 'failed' : 'oauth_untouched',
2115
- reason: authInvariantError || 'ordinary_repair_never_changes_shared_codex_auth',
2116
- auth_path: authPath,
2117
- ...(authInvariantError ? { error: authInvariantError } : {})
2118
- };
2119
- const codexLogin = {
2120
- ok: !authInvariantError,
2121
- status: authInvariantError ? 'desktop_auth_byte_invariant_failed' : 'not_required',
2122
- reason: authInvariantError || 'gateway_credentials_are_independent_from_codex_login',
2123
- error: authInvariantError
2124
- };
2125
- const toolCatalog = {
2126
- schema: 'sks.codex-lb-tool-catalog-selection.v1',
2127
- ok: true,
2128
- required: false,
2129
- status: desktopMode === 'desktop-native-bridge'
2130
- ? 'not_bound_for_desktop_native'
2131
- : 'not_bound_during_repair',
2132
- config_changed: false,
2133
- selected: finalStatus.selected,
2134
- blockers: []
2135
- };
2136
- const ok = Boolean(configWrite.ok && codexEnvironment.ok && !authInvariantError);
2137
- return {
2138
- ok,
2139
- status: ok
2140
- ? configWrite.changed
2141
- ? 'repaired'
2142
- : finalStatus.selected
2143
- ? 'present'
2144
- : 'present_unselected'
2145
- : authInvariantError
2146
- ? 'desktop_auth_byte_invariant_failed'
2147
- : codexEnvironment.status,
2148
- config_path: status.config_path,
2149
- env_path: status.env_path,
2150
- base_url: status.base_url,
2151
- config_repaired: configWrite.changed === true,
2152
- legacy_auth_migrated: false,
2153
- legacy_auth_path: null,
2154
- auth_reconcile: authReconcile,
2155
- codex_lb: finalStatus,
2156
- codex_environment: codexEnvironment,
2157
- codex_login: codexLogin,
2158
- tool_catalog: toolCatalog,
2159
- tool_output_recovery: toolOutputRecovery,
2160
- error: authInvariantError || codexEnvironment.error || null
2161
- };
2162
- }
2163
- export async function ensureCodexLbAuthDuringInstall(opts = {}) {
2164
- if (process.env.SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH === '1' && !opts.force)
2165
- return { status: 'skipped', reason: 'SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH=1' };
2166
- const status = await codexLbStatus(opts);
2167
- if (!status.selected && !status.provider_configured && !status.env_key_configured)
2168
- return { status: 'not_configured', codex_lb: status };
2169
- return repairCodexLbAuth({
2170
- ...opts,
2171
- probeToolOutputRecovery: false
2172
- });
2173
- }
2174
- function hasChatgptOAuthTokens(text = '') {
2175
- try {
2176
- const parsed = JSON.parse(String(text || ''));
2177
- if (!parsed || typeof parsed !== 'object')
2178
- return false;
2179
- const authMode = String(parsed.auth_mode || parsed.authMode || parsed.mode || '').toLowerCase();
2180
- const tokens = parsed.tokens || parsed.oauth || parsed.oauth_tokens;
2181
- if (tokens && typeof tokens === 'object') {
2182
- if (tokens.id_token || tokens.access_token || tokens.refresh_token)
2183
- return true;
2184
- }
2185
- if (authMode && /chatgpt|oauth|browser/.test(authMode)) {
2186
- if (parsed.last_refresh || parsed.expires_at || parsed.refresh_token || parsed.access_token || parsed.id_token)
2187
- return true;
2188
- }
2189
- return false;
2190
- }
2191
- catch {
2192
- return false;
2193
- }
2194
- }
2195
- function parseCodexAuthApiKey(text = '') {
2196
- try {
2197
- const parsed = JSON.parse(String(text || ''));
2198
- const key = parsed?.key || parsed?.api_key || parsed?.apiKey || parsed?.openai_api_key || parsed?.OPENAI_API_KEY;
2199
- return typeof key === 'string' ? key.trim() : '';
2200
- }
2201
- catch {
2202
- return '';
2203
- }
2204
- }
2205
- function codexAuthModeSummary(text = '') {
2206
- const raw = String(text || '').trim();
2207
- if (!raw)
2208
- return { mode: 'missing', codex_app_usable: false, summary: 'missing auth.json' };
2209
- if (hasChatgptOAuthTokens(raw))
2210
- return { mode: 'chatgpt_oauth', codex_app_usable: true, summary: 'ChatGPT OAuth token blob present' };
2211
- const apiKey = parseCodexAuthApiKey(raw);
2212
- if (apiKey)
2213
- return { mode: 'apikey', codex_app_usable: true, summary: 'API-key auth.json available for requires_openai_auth providers' };
2214
- try {
2215
- const parsed = JSON.parse(raw);
2216
- if (parsed?.auth_mode === 'browser')
2217
- return { mode: 'browser_marker', codex_app_usable: true, summary: 'browser auth marker present; token storage is not inspectable' };
2218
- }
2219
- catch { }
2220
- return { mode: 'unknown', codex_app_usable: false, summary: 'unrecognized auth.json shape' };
2221
- }
2222
- export async function reconcileCodexLbAuthConflict(opts = {}) {
2223
- const home = opts.home || process.env.HOME || os.homedir();
2224
- const status = opts.status || await codexLbStatus({ ...opts, home });
2225
- const authPath = opts.authPath || codexAuthPath(home);
2226
- const backupPath = opts.backupPath || codexAuthChatgptBackupPath(home);
2227
- if (!status.env_key_configured || !status.base_url) {
2228
- return { status: 'skipped', reason: 'codex_lb_not_ready', auth_path: authPath };
2229
- }
2230
- const authExists = await exists(authPath);
2231
- const authText = authExists ? await readText(authPath, '') : '';
2232
- const envLoad = await loadCodexLbEnv({ ...opts, home, envPath: status.env_path });
2233
- const apiKey = String(opts.apiKey || envLoad.secret_api_key || '').trim();
2234
- if (!apiKey) {
2235
- return { status: 'skipped', reason: 'missing_env_key', auth_path: authPath };
2236
- }
2237
- const forceCodexLbApiKeyAuth = opts.forceCodexLbApiKeyAuth === true;
2238
- const currentApiKey = parseCodexAuthApiKey(authText);
2239
- let routingGuardAdded = false;
2240
- const ensureSharedOpenAiRouting = async () => {
2241
- const configPath = opts.configPath || status.config_path || codexLbConfigPath(home);
2242
- const currentConfig = await readText(configPath, '');
2243
- const planned = upsertCodexLbSharedOpenAiRouting(currentConfig, status.base_url);
2244
- if (!planned.ok) {
2245
- return {
2246
- ok: false,
2247
- status: planned.status,
2248
- config_path: configPath,
2249
- configured_base_url: planned.configured_base_url
2250
- };
2251
- }
2252
- if (planned.status === 'present') {
2253
- return { ok: true, status: 'present', changed: false, config_path: configPath, managed: planned.managed };
2254
- }
2255
- const written = await safeWriteCodexConfigToml(configPath, currentConfig, planned.text, 'codex-lb-shared-auth-routing');
2256
- if (!written.ok)
2257
- return { ok: false, status: written.status, config_path: configPath, error: 'shared_openai_routing_write_failed' };
2258
- const after = await readText(configPath, '');
2259
- const verified = codexLbSharedOpenAiRoutingState(after, status.base_url);
2260
- if (verified.status !== 'matched' || !verified.managed) {
2261
- return { ok: false, status: 'readback_failed', config_path: configPath, error: 'shared_openai_routing_readback_failed' };
2262
- }
2263
- routingGuardAdded = true;
2264
- return { ok: true, status: 'added', changed: true, config_path: configPath, managed: true };
2265
- };
2266
- const rollbackSharedOpenAiRouting = async () => {
2267
- if (!routingGuardAdded)
2268
- return { ok: true, status: 'not_needed' };
2269
- const configPath = opts.configPath || status.config_path || codexLbConfigPath(home);
2270
- const currentConfig = await readText(configPath, '');
2271
- const removal = removeCodexLbSharedOpenAiRouting(currentConfig, status.base_url);
2272
- if (!removal.changed)
2273
- return { ok: false, status: 'not_owned_or_changed' };
2274
- const written = await safeWriteCodexConfigToml(configPath, currentConfig, ensureTrailingNewline(removal.text), 'codex-lb-shared-auth-routing-rollback');
2275
- if (!written.ok)
2276
- return { ok: false, status: written.status };
2277
- const after = codexLbSharedOpenAiRoutingState(await readText(configPath, ''), status.base_url);
2278
- return { ok: after.status === 'missing' && !after.managed, status: after.status === 'missing' && !after.managed ? 'rolled_back' : 'readback_failed' };
2279
- };
2280
- const rollbackAuth = async () => {
2281
- try {
2282
- if (authExists)
2283
- await writeTextAtomic(authPath, authText, { mode: 0o600 });
2284
- else
2285
- await fsp.rm(authPath, { force: true });
2286
- return { ok: true, status: 'rolled_back' };
2287
- }
2288
- catch (err) {
2289
- return { ok: false, status: 'rollback_failed', error: err.message };
2290
- }
2291
- };
2292
- const writeApiKeyAuth = async (reason, backupPathForResult = null) => {
2293
- const routingGuard = await ensureSharedOpenAiRouting();
2294
- if (!routingGuard.ok) {
2295
- return {
2296
- status: 'failed',
2297
- reason: routingGuard.status === 'conflicting_user_openai_base_url' ? 'shared_openai_base_url_conflict' : 'shared_openai_routing_guard_failed',
2298
- auth_path: authPath,
2299
- backup_path: backupPathForResult || backupPath,
2300
- routing_guard: routingGuard,
2301
- error: routingGuard.error || routingGuard.status
2302
- };
2303
- }
2304
- try {
2305
- await writeTextAtomic(authPath, `${JSON.stringify({ auth_mode: 'apikey', OPENAI_API_KEY: apiKey }, null, 2)}\n`, { mode: 0o600 });
2306
- await fsp.chmod(authPath, 0o600).catch(() => { });
2307
- const finalConfig = await readText(opts.configPath || status.config_path || codexLbConfigPath(home), '');
2308
- const finalAuth = await readText(authPath, '');
2309
- const finalRouting = codexLbSharedOpenAiRoutingState(finalConfig, status.base_url);
2310
- if (finalRouting.status !== 'matched' || parseCodexAuthApiKey(finalAuth) !== apiKey) {
2311
- const authRollback = await rollbackAuth();
2312
- const routingRollback = await rollbackSharedOpenAiRouting();
2313
- return {
2314
- status: 'failed',
2315
- reason: 'activation_readback_failed',
2316
- auth_path: authPath,
2317
- backup_path: backupPathForResult || backupPath,
2318
- routing_guard: routingGuard,
2319
- routing_rollback: routingRollback,
2320
- error: authRollback.ok && routingRollback.ok ? 'activation_readback_failed_rolled_back' : 'activation_readback_failed_rollback_incomplete'
2321
- };
2322
- }
2323
- return {
2324
- status: 'apikey_forced',
2325
- reason,
2326
- auth_path: authPath,
2327
- backup_path: backupPathForResult || backupPath,
2328
- routing_guard: routingGuard
2329
- };
2330
- }
2331
- catch (err) {
2332
- const authRollback = await rollbackAuth();
2333
- const routingRollback = await rollbackSharedOpenAiRouting();
2334
- return {
2335
- status: 'failed',
2336
- reason: 'write_failed',
2337
- auth_path: authPath,
2338
- backup_path: backupPathForResult || backupPath,
2339
- routing_guard: routingGuard,
2340
- routing_rollback: routingRollback,
2341
- error: authRollback.ok && routingRollback.ok ? err.message : `${err.message}; activation rollback incomplete`
2342
- };
2343
- }
2344
- };
2345
- if (!authExists) {
2346
- if (forceCodexLbApiKeyAuth)
2347
- return writeApiKeyAuth('codex_lb_auth_selected_missing_auth');
2348
- return { status: 'skipped', reason: 'auth_missing', auth_path: authPath };
2349
- }
2350
- if (!authText.trim()) {
2351
- if (forceCodexLbApiKeyAuth)
2352
- return writeApiKeyAuth('codex_lb_auth_selected_empty_auth');
2353
- return { status: 'skipped', reason: 'auth_empty', auth_path: authPath };
2354
- }
2355
- if (hasChatgptOAuthTokens(authText)) {
2356
- try {
2357
- await ensureDir(path.dirname(backupPath));
2358
- await writeTextAtomic(backupPath, authText, { mode: 0o600 });
2359
- await fsp.chmod(backupPath, 0o600).catch(() => { });
2360
- }
2361
- catch (err) {
2362
- return { status: 'failed', reason: 'backup_failed', auth_path: authPath, backup_path: backupPath, error: err.message };
2363
- }
2364
- if (process.env.SKS_CODEX_LB_NO_AUTH_RECONCILE === '1' && !opts.force) {
2365
- return {
2366
- status: 'backup_only',
2367
- reason: 'SKS_CODEX_LB_NO_AUTH_RECONCILE=1',
2368
- auth_path: authPath,
2369
- backup_path: backupPath
2370
- };
2371
- }
2372
- if (forceCodexLbApiKeyAuth)
2373
- return writeApiKeyAuth('codex_lb_auth_selected', backupPath);
2374
- if (process.env.SKS_CODEX_LB_FORCE_APIKEY_AUTH !== '1') {
2375
- return {
2376
- status: 'oauth_preserved',
2377
- reason: 'chatgpt_oauth_preserved_until_use_codex_lb_auth',
2378
- auth_path: authPath,
2379
- backup_path: backupPath
2380
- };
2381
- }
2382
- return writeApiKeyAuth('SKS_CODEX_LB_FORCE_APIKEY_AUTH=1', backupPath);
2383
- }
2384
- if (forceCodexLbApiKeyAuth) {
2385
- if (currentApiKey && currentApiKey === apiKey) {
2386
- const routingGuard = await ensureSharedOpenAiRouting();
2387
- if (!routingGuard.ok) {
2388
- return {
2389
- status: 'failed',
2390
- reason: routingGuard.status === 'conflicting_user_openai_base_url' ? 'shared_openai_base_url_conflict' : 'shared_openai_routing_guard_failed',
2391
- auth_path: authPath,
2392
- backup_path: backupPath,
2393
- routing_guard: routingGuard,
2394
- error: routingGuard.error || routingGuard.status
2395
- };
2396
- }
2397
- return {
2398
- status: 'apikey_auth_active',
2399
- reason: 'codex_lb_auth_selected',
2400
- auth_path: authPath,
2401
- backup_path: backupPath,
2402
- routing_guard: routingGuard
2403
- };
2404
- }
2405
- return writeApiKeyAuth('codex_lb_auth_selected_replace_existing');
2406
- }
2407
- if (currentApiKey && currentApiKey === apiKey) {
2408
- const routingGuard = await ensureSharedOpenAiRouting();
2409
- if (!routingGuard.ok) {
2410
- return {
2411
- status: 'failed',
2412
- reason: routingGuard.status === 'conflicting_user_openai_base_url' ? 'shared_openai_base_url_conflict' : 'shared_openai_routing_guard_failed',
2413
- auth_path: authPath,
2414
- backup_path: backupPath,
2415
- routing_guard: routingGuard,
2416
- error: routingGuard.error || routingGuard.status
2417
- };
2418
- }
2419
- const backupText = await readText(backupPath, '');
2420
- if (hasChatgptOAuthTokens(backupText) && process.env.SKS_CODEX_LB_KEEP_APIKEY_AUTH !== '1') {
2421
- try {
2422
- const restored = backupText.endsWith('\n') ? backupText : `${backupText}\n`;
2423
- await writeTextAtomic(authPath, restored, { mode: 0o600 });
2424
- await fsp.chmod(authPath, 0o600).catch(() => { });
2425
- const unselected = await unselectCodexLbProvider({
2426
- ...opts,
2427
- home,
2428
- authPath
2429
- });
2430
- if (!unselected.ok) {
2431
- await writeTextAtomic(authPath, authText, { mode: 0o600 });
2432
- await fsp.chmod(authPath, 0o600).catch(() => { });
2433
- return {
2434
- status: 'failed',
2435
- reason: 'oauth_restore_provider_unselect_failed',
2436
- auth_path: authPath,
2437
- backup_path: backupPath,
2438
- routing_guard: routingGuard,
2439
- error: unselected.provider_error || unselected.reason || unselected.status
2440
- };
2441
- }
2442
- return {
2443
- status: 'oauth_restored',
2444
- reason: 'restored_chatgpt_oauth_for_codex_app',
2445
- auth_path: authPath,
2446
- backup_path: backupPath,
2447
- routing_guard: routingGuard
2448
- };
2449
- }
2450
- catch (err) {
2451
- return { status: 'failed', reason: 'restore_failed', auth_path: authPath, backup_path: backupPath, error: err.message };
2452
- }
2453
- }
2454
- return {
2455
- status: 'apikey_auth_active',
2456
- reason: hasChatgptOAuthTokens(backupText) ? 'SKS_CODEX_LB_KEEP_APIKEY_AUTH=1' : 'chatgpt_oauth_backup_missing',
2457
- auth_path: authPath,
2458
- backup_path: backupPath,
2459
- routing_guard: routingGuard
2460
- };
2461
- }
2462
- return { status: 'no_oauth_conflict', auth_path: authPath };
2463
- }
2464
- export function codexLbChatgptBackupPath(home = process.env.HOME || os.homedir()) {
2465
- return codexAuthChatgptBackupPath(home);
2466
- }
2467
- function removeTopLevelTomlString(text, key) {
2468
- const lines = String(text || '').split('\n');
2469
- const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
2470
- const end = firstTable === -1 ? lines.length : firstTable;
2471
- let removed = false;
2472
- for (let i = end - 1; i >= 0; i--) {
2473
- const line = lines[i];
2474
- if (line === undefined)
2475
- continue;
2476
- if (new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(line)) {
2477
- lines.splice(i, 1);
2478
- removed = true;
2479
- }
2480
- }
2481
- if (!removed)
2482
- return text;
2483
- return lines.join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
2484
- }
2485
- export async function unselectCodexLbProvider(opts = {}) {
2486
- const home = opts.home || process.env.HOME || os.homedir();
2487
- const configPath = opts.configPath || codexLbConfigPath(home);
2488
- const current = await readText(configPath, '');
2489
- if (!current.trim())
2490
- return { ok: true, status: 'not_selected', reason: 'no_config', config_path: configPath };
2491
- const envPath = opts.envPath || codexLbEnvPath(home);
2492
- const envLoad = await loadCodexLbEnv({ ...opts, home, envPath });
2493
- const authPath = opts.authPath || codexAuthPath(home);
2494
- const authApiKey = parseCodexAuthApiKey(await readText(authPath, ''));
2495
- const managedCatalogPath = codexLbToolCatalogPath(opts.codexHome || path.join(home, '.codex'));
2496
- const managedCatalogSelected = topLevelTomlString(current, 'model_catalog_json') === managedCatalogPath;
2497
- const providerBaseUrl = codexLbProviderBaseUrl(current) || envLoad.base_url || '';
2498
- const sharedOpenAiRouting = codexLbSharedOpenAiRoutingState(current, providerBaseUrl);
2499
- const managedSharedOpenAiRouting = sharedOpenAiRouting.status === 'matched' && sharedOpenAiRouting.managed;
2500
- const codexLbRoutingOwned = hasTopLevelCodexLbSelected(current)
2501
- || /\[model_providers\.codex-lb\]/.test(current)
2502
- || managedCatalogSelected
2503
- || managedSharedOpenAiRouting;
2504
- if (authApiKey && codexLbRoutingOwned) {
2505
- return {
2506
- ok: false,
2507
- status: 'failed',
2508
- reason: 'shared_codex_lb_auth_active',
2509
- provider_error: 'refusing to unselect codex-lb while API-key auth could fall through to built-in OpenAI',
2510
- config_path: configPath
2511
- };
2512
- }
2513
- if (!hasTopLevelCodexLbSelected(current) && !managedCatalogSelected && !managedSharedOpenAiRouting)
2514
- return { ok: true, status: 'not_selected', config_path: configPath };
2515
- try {
2516
- let next = removeTopLevelTomlKeyIfValue(current, 'model_provider', 'codex-lb');
2517
- next = removeTopLevelTomlKeyIfValue(next, 'model_catalog_json', managedCatalogPath);
2518
- const routingRemoval = removeCodexLbSharedOpenAiRouting(next, providerBaseUrl);
2519
- next = routingRemoval.text;
2520
- next = ensureTrailingNewline(next);
2521
- const safeWrite = await safeWriteCodexConfigToml(configPath, current, next, 'codex-lb-unselect');
2522
- const after = safeWrite.ok ? await readText(configPath, '') : current;
2523
- const afterRouting = codexLbSharedOpenAiRoutingState(after, providerBaseUrl);
2524
- const selectionRemoved = !hasTopLevelCodexLbSelected(after)
2525
- && topLevelTomlString(after, 'model_catalog_json') !== managedCatalogPath
2526
- && (!managedSharedOpenAiRouting || (afterRouting.status === 'missing' && !afterRouting.managed));
2527
- if (safeWrite.ok && selectionRemoved)
2528
- return { ok: true, status: 'unselected', config_path: configPath, backup_path: safeWrite.backup_path, routing_guard_removed: routingRemoval.changed };
2529
- const providerError = safeWrite.ok ? 'provider_selection_remains_after_write' : safeWrite.status || 'provider_config_write_blocked';
2530
- return {
2531
- ok: false,
2532
- status: 'failed',
2533
- reason: 'provider_config_write_blocked',
2534
- provider_error: providerError,
2535
- write_status: safeWrite.status || 'failed',
2536
- config_path: configPath,
2537
- backup_path: safeWrite.backup_path,
2538
- config_preserved: safeWrite.changed !== true
2539
- };
2540
- }
2541
- catch (err) {
2542
- return { ok: false, status: 'failed', reason: 'write_failed', provider_error: err.message || 'write_failed', config_path: configPath, error: err.message };
2543
- }
2544
- }
2545
- function providerDeselectionOutcome(result) {
2546
- const ok = result?.status === 'unselected' || result?.status === 'not_selected';
2547
- return {
2548
- ok,
2549
- provider_unselected: ok,
2550
- provider_status: result?.status || 'failed',
2551
- provider_error: ok ? null : String(result?.provider_error || result?.error || result?.reason || result?.status || 'unselect_failed')
2552
- };
2553
- }
2554
- export async function releaseCodexLbAuthHold(opts = {}) {
2555
- const home = opts.home || process.env.HOME || os.homedir();
2556
- const authPath = opts.authPath || codexAuthPath(home);
2557
- const backupPath = opts.backupPath || codexAuthChatgptBackupPath(home);
2558
- const configPath = opts.configPath || codexLbConfigPath(home);
2559
- const authExisted = await exists(authPath);
2560
- const currentAuthText = await readText(authPath, '');
2561
- const trimmedCurrent = currentAuthText.trim();
2562
- if (opts.keepProvider) {
2563
- const currentConfig = await readText(configPath, '');
2564
- const providerBaseUrl = codexLbProviderBaseUrl(currentConfig);
2565
- const routing = codexLbSharedOpenAiRoutingState(currentConfig, providerBaseUrl);
2566
- if (hasTopLevelCodexLbSelected(currentConfig) || (routing.status === 'matched' && routing.managed)) {
2567
- return {
2568
- ok: false,
2569
- status: 'failed',
2570
- reason: 'keep_provider_unsafe_with_shared_auth',
2571
- auth_path: authPath,
2572
- backup_path: backupPath,
2573
- provider_unselected: false
2574
- };
2575
- }
2576
- }
2577
- const currentAuthMode = codexAuthModeSummary(currentAuthText);
2578
- if (!opts.force && (currentAuthMode.mode === 'chatgpt_oauth' || currentAuthMode.mode === 'browser_marker')) {
2579
- let provider = { ok: true, provider_unselected: false, provider_status: 'kept', provider_error: null };
2580
- if (!opts.keepProvider) {
2581
- const unselected = await unselectCodexLbProvider({
2582
- ...opts,
2583
- home,
2584
- configPath,
2585
- authPath
2586
- });
2587
- provider = providerDeselectionOutcome(unselected);
2588
- }
2589
- return {
2590
- ok: provider.ok,
2591
- status: provider.ok ? 'already_chatgpt' : 'failed',
2592
- ...(provider.ok ? {} : { reason: 'provider_unselect_failed' }),
2593
- auth_path: authPath,
2594
- backup_path: backupPath,
2595
- provider_unselected: provider.provider_unselected,
2596
- provider_status: provider.provider_status,
2597
- provider_error: provider.provider_error
2598
- };
2599
- }
2600
- const backupExists = await exists(backupPath);
2601
- const backupText = backupExists ? await readText(backupPath, '') : '';
2602
- if (!backupExists || !backupText.trim()) {
2603
- return {
2604
- status: 'no_backup',
2605
- auth_path: authPath,
2606
- backup_path: backupPath,
2607
- provider_unselected: false
2608
- };
2609
- }
2610
- if (!hasChatgptOAuthTokens(backupText)) {
2611
- return {
2612
- status: 'no_backup',
2613
- reason: 'backup_not_oauth',
2614
- auth_path: authPath,
2615
- backup_path: backupPath,
2616
- provider_unselected: false
2617
- };
2618
- }
2619
- if (trimmedCurrent && hasChatgptOAuthTokens(currentAuthText) && !opts.force) {
2620
- let provider = { ok: true, provider_unselected: false, provider_status: 'kept', provider_error: null };
2621
- if (!opts.keepProvider) {
2622
- const unselected = await unselectCodexLbProvider({
2623
- ...opts,
2624
- home,
2625
- configPath,
2626
- authPath
2627
- });
2628
- provider = providerDeselectionOutcome(unselected);
2629
- }
2630
- return {
2631
- ok: provider.ok,
2632
- status: provider.ok ? 'already_chatgpt' : 'failed',
2633
- ...(provider.ok ? {} : { reason: 'provider_unselect_failed' }),
2634
- auth_path: authPath,
2635
- backup_path: backupPath,
2636
- provider_unselected: provider.provider_unselected,
2637
- provider_status: provider.provider_status,
2638
- provider_error: provider.provider_error
2639
- };
2640
- }
2641
- if (!opts.force && trimmedCurrent) {
2642
- const looksApikey = /"auth_mode"\s*:\s*"apikey"/.test(currentAuthText) && Boolean(parseCodexAuthApiKey(currentAuthText));
2643
- const looksBrowserMarker = /^\{\s*"auth_mode"\s*:\s*"browser"\s*\}\s*$/.test(currentAuthText);
2644
- if (!looksApikey && !looksBrowserMarker) {
2645
- return {
2646
- status: 'auth_in_use',
2647
- reason: 'unfamiliar_auth_json',
2648
- auth_path: authPath,
2649
- backup_path: backupPath,
2650
- provider_unselected: false
2651
- };
2652
- }
2653
- }
2654
- try {
2655
- await ensureDir(path.dirname(authPath));
2656
- const restored = backupText.endsWith('\n') ? backupText : `${backupText}\n`;
2657
- await writeTextAtomic(authPath, restored, { mode: 0o600 });
2658
- await fsp.chmod(authPath, 0o600).catch(() => { });
2659
- }
2660
- catch (err) {
2661
- return {
2662
- status: 'failed',
2663
- reason: 'restore_failed',
2664
- auth_path: authPath,
2665
- backup_path: backupPath,
2666
- error: err.message,
2667
- provider_unselected: false
2668
- };
2669
- }
2670
- let provider = { ok: true, provider_unselected: false, provider_status: 'kept', provider_error: null };
2671
- if (!opts.keepProvider) {
2672
- const unselected = await unselectCodexLbProvider({
2673
- ...opts,
2674
- home,
2675
- configPath,
2676
- authPath
2677
- });
2678
- provider = providerDeselectionOutcome(unselected);
2679
- }
2680
- if (!provider.ok) {
2681
- const rollback = await rollbackCodexAuthRestore({ authPath, authExisted, currentAuthText });
2682
- return {
2683
- ok: false,
2684
- status: 'failed',
2685
- reason: 'provider_unselect_failed',
2686
- auth_path: authPath,
2687
- backup_path: backupPath,
2688
- backup_removed: false,
2689
- auth_restored: rollback.ok !== true,
2690
- auth_rollback: rollback,
2691
- rollback_safe: rollback.ok === true,
2692
- provider_unselected: false,
2693
- provider_status: provider.provider_status,
2694
- provider_error: provider.provider_error
2695
- };
2696
- }
2697
- let backupRemoved = false;
2698
- if (opts.deleteBackup) {
2699
- try {
2700
- await fsp.rm(backupPath, { force: true });
2701
- backupRemoved = true;
2702
- }
2703
- catch {
2704
- }
2705
- }
2706
- return {
2707
- ok: true,
2708
- status: 'released',
2709
- auth_path: authPath,
2710
- backup_path: backupPath,
2711
- backup_removed: backupRemoved,
2712
- auth_restored: true,
2713
- provider_unselected: provider.provider_unselected,
2714
- provider_status: provider.provider_status,
2715
- provider_error: provider.provider_error
2716
- };
2717
- }
2718
- async function rollbackCodexAuthRestore(input) {
2719
- try {
2720
- if (input.authExisted) {
2721
- await writeTextAtomic(input.authPath, input.currentAuthText, { mode: 0o600 });
2722
- await fsp.chmod(input.authPath, 0o600).catch(() => { });
2723
- }
2724
- else {
2725
- await fsp.rm(input.authPath, { force: true });
2726
- }
2727
- return { ok: true, status: 'restored_previous_auth' };
2728
- }
2729
- catch (err) {
2730
- return { ok: false, status: 'rollback_failed', error: err.message };
2731
- }
2732
- }
2733
- export async function maybePromptCodexLbSetupForLaunch(args = [], opts = {}) {
2734
- if (args.includes('--json') || args.includes('--skip-codex-lb') || process.env.SKS_SKIP_CODEX_LB_PROMPT === '1')
2735
- return { status: 'skipped' };
2736
- const allowUnverifiedToolOutputRecovery = opts.allowUnverifiedToolOutputRecovery === true
2737
- || codexLbToolOutputRecoveryOverrideAcknowledged({ args, env: opts.env || process.env });
2738
- let status = await codexLbStatus({ ...opts, probeToolOutputRecovery: false });
2739
- if (status.legacy_migration_required) {
2740
- return {
2741
- status: 'legacy_migration_required',
2742
- ok: false,
2743
- codex_lb: status,
2744
- blockers: ['run_sks_codex_lb_migrate_legacy_desktop']
2745
- };
2746
- }
2747
- if (status.env_key_configured && status.base_url) {
2748
- const repaired = await repairCodexLbAuth({
2749
- ...opts,
2750
- allowUnverifiedToolOutputRecovery,
2751
- probeToolOutputRecovery: false
2752
- });
2753
- status = await codexLbStatus({ ...opts, probeToolOutputRecovery: false });
2754
- if (!repaired.ok)
2755
- return { status: repaired.status, ok: false, repair: repaired, codex_lb: status };
2756
- const codexEnvironment = repaired.codex_environment || await syncCodexLbProviderEnvironment(status, opts);
2757
- return {
2758
- status: status.desktop_mode === 'cli-provider' && !status.selected ? 'continued_to_codex' : 'present',
2759
- ...status,
2760
- codex_environment: codexEnvironment,
2761
- ...(repaired.codex_login ? { codex_login: repaired.codex_login } : {}),
2762
- ...(repaired.tool_catalog ? { tool_catalog: repaired.tool_catalog } : {}),
2763
- reason: status.desktop_mode === 'cli-provider' && !status.selected
2764
- ? 'unselected_cli_provider_available_for_explicit_use'
2765
- : 'explicit_managed_desktop_routing_preserved'
2766
- };
2767
- }
2768
- if (!canAskYesNo())
2769
- return { status: 'non_interactive', codex_lb: status };
2770
- const useCodexLb = (await askPostinstallQuestion('\ncodex-lb is not configured. Store credentials and an unselected CLI provider now? [y/N] ')).trim();
2771
- if (!/^(y|yes|예|네|응)$/i.test(useCodexLb))
2772
- return { status: 'continued_to_codex' };
2773
- const host = (await askPostinstallQuestion('codex-lb host domain [http://127.0.0.1:2455]: ')).trim() || 'http://127.0.0.1:2455';
2774
- const apiKey = (await askPostinstallQuestion('codex-lb API key: ')).trim();
2775
- const configured = await configureCodexLb({ ...opts, host, apiKey, allowUnverifiedToolOutputRecovery });
2776
- if (configured.ok)
2777
- console.log(`codex-lb credentials stored: ${configured.base_url}. Use \`sks codex-lb use-cli\`, \`use-desktop-full\`, or \`disable\` explicitly.`);
2778
- else
2779
- console.log('codex-lb setup skipped: API key was empty.');
2780
- printCodexLbSetupWarnings(configured);
2781
- return configured;
2782
- }
2783
- function scrubCodexLbToolEnvironment(env = process.env) {
2784
- const sanitized = { ...env };
2785
- delete sanitized.CODEX_LB_API_KEY;
2786
- delete sanitized.OPENROUTER_API_KEY;
2787
- return sanitized;
2788
- }
2789
- async function syncCodexLbProviderEnvironment(status = {}, opts = {}) {
2790
- const home = opts.home || process.env.HOME || os.homedir();
2791
- const envPath = opts.envPath || status.env_path || codexLbEnvPath(home);
2792
- const envLoad = await loadCodexLbEnv({ ...opts, home, envPath });
2793
- const apiKey = String(opts.apiKey || envLoad.secret_api_key || '').trim();
2794
- if (!apiKey)
2795
- return { ok: false, status: 'missing_env_key' };
2796
- const baseUrl = envLoad.base_url || opts.baseUrl || status.base_url;
2797
- process.env.CODEX_LB_API_KEY = apiKey;
2798
- if (baseUrl)
2799
- process.env.CODEX_LB_BASE_URL = baseUrl;
2800
- const launchEnv = await syncCodexLbMacLaunchEnvironment(baseUrl ? { CODEX_LB_BASE_URL: baseUrl } : {}, opts);
2801
- const ok = launchEnv.ok || launchEnv.skipped || launchEnv.status === 'not_macos';
2802
- return {
2803
- ok,
2804
- status: launchEnv.status === 'synced' ? 'launch_base_url_synced_secret_env_removed' : ok ? 'process_env' : launchEnv.status,
2805
- env_path: envPath,
2806
- base_url: baseUrl || null,
2807
- launch_environment: launchEnv,
2808
- error: launchEnv.error || null
2809
- };
2810
- }
2811
- async function syncCodexLbMacLaunchEnvironment(values = {}, opts = {}) {
2812
- if (opts.syncLaunchEnv === false || process.env.SKS_SKIP_CODEX_LB_LAUNCH_ENV === '1')
2813
- return { ok: true, status: 'skipped', skipped: true, reason: 'SKS_SKIP_CODEX_LB_LAUNCH_ENV=1' };
2814
- if (process.platform !== 'darwin' && !opts.forceLaunchEnv)
2815
- return { ok: true, status: 'not_macos', skipped: true };
2816
- const launchctl = opts.launchctlBin
2817
- || await exists('/bin/launchctl').then((ok) => ok ? '/bin/launchctl' : null).catch(() => null);
2818
- if (!launchctl)
2819
- return { ok: false, status: 'launchctl_missing', error: '/bin/launchctl not found' };
2820
- const childEnv = scrubCodexLbToolEnvironment();
2821
- const secretCleanup = await cleanupMacLaunchSecretEnvironment({
2822
- force: opts.forceLaunchEnv === true,
2823
- launchctlBin: launchctl,
2824
- env: childEnv
2825
- }).catch((err) => ({
2826
- ok: false,
2827
- status: 'partial',
2828
- variables: ['CODEX_LB_API_KEY', 'OPENROUTER_API_KEY'],
2829
- cleaned: [],
2830
- failed: [{ key: 'CODEX_LB_API_KEY', error: err?.message || String(err) }, { key: 'OPENROUTER_API_KEY', error: err?.message || String(err) }],
2831
- next_actions: ['Run launchctl unsetenv for CODEX_LB_API_KEY and OPENROUTER_API_KEY']
2832
- }));
2833
- const variables = Object.entries(values).filter(([key, value]) => value && !['CODEX_LB_API_KEY', 'OPENROUTER_API_KEY'].includes(String(key)));
2834
- const results = [];
2835
- for (const [key, value] of variables) {
2836
- const result = await runProcess(launchctl, ['setenv', key, String(value)], {
2837
- timeoutMs: 5000,
2838
- maxOutputBytes: 8192,
2839
- env: childEnv,
2840
- envMode: 'replace'
2841
- });
2842
- results.push({
2843
- key,
2844
- ok: result.code === 0,
2845
- error: result.code === 0 ? null : redactSecretText(result.stderr || result.stdout || 'launchctl setenv failed', [value]).trim()
2846
- });
2847
- }
2848
- const failed = results.filter((result) => !result.ok);
2849
- if (failed.length)
2850
- return { ok: false, status: 'launch_env_failed', variables: results.map((result) => result.key), failed, secret_env_cleanup: secretCleanup, error: failed.map((result) => `${result.key}: ${result.error}`).join('; ') };
2851
- return {
2852
- ok: secretCleanup.ok !== false,
2853
- status: variables.length ? 'synced' : 'secret_env_removed',
2854
- variables: results.map((result) => result.key),
2855
- skipped_secret_variables: ['CODEX_LB_API_KEY', 'OPENROUTER_API_KEY'],
2856
- secret_env_cleanup: secretCleanup
2857
- };
2858
- }
2859
- async function inspectCodexLbMacLaunchEnvironment(baseUrl = '', opts = {}) {
2860
- if (process.platform !== 'darwin' && !opts.forceLaunchEnv)
2861
- return { checked: false, status: 'not_macos', skipped: true };
2862
- const launchctl = opts.launchctlBin
2863
- || await exists('/bin/launchctl').then((ok) => ok ? '/bin/launchctl' : null).catch(() => null);
2864
- if (!launchctl)
2865
- return { checked: true, available: false, status: 'launchctl_missing' };
2866
- const childEnv = scrubCodexLbToolEnvironment();
2867
- const readVar = async (key) => {
2868
- const result = await runProcess(launchctl, ['getenv', key], {
2869
- timeoutMs: 3000,
2870
- maxOutputBytes: 8192,
2871
- env: childEnv,
2872
- envMode: 'replace'
2873
- });
2874
- return result.code === 0 ? String(result.stdout || '').trim() : '';
2875
- };
2876
- const [currentBaseUrl, currentApiKey, currentOpenRouterKey] = await Promise.all([
2877
- readVar('CODEX_LB_BASE_URL'),
2878
- readVar('CODEX_LB_API_KEY'),
2879
- readVar('OPENROUTER_API_KEY')
2880
- ]);
2881
- const baseMatches = !baseUrl || currentBaseUrl === String(baseUrl || '').trim();
2882
- const basePresent = Boolean(currentBaseUrl);
2883
- const keyPresent = Boolean(currentApiKey);
2884
- const openRouterKeyPresent = Boolean(currentOpenRouterKey);
2885
- return {
2886
- checked: true,
2887
- available: true,
2888
- status: keyPresent || openRouterKeyPresent
2889
- ? 'secret_env_present'
2890
- : basePresent && baseMatches
2891
- ? 'base_url_only'
2892
- : basePresent
2893
- ? 'partial'
2894
- : 'missing',
2895
- variables: [
2896
- ...(keyPresent ? ['CODEX_LB_API_KEY'] : []),
2897
- ...(openRouterKeyPresent ? ['OPENROUTER_API_KEY'] : []),
2898
- ...(basePresent ? ['CODEX_LB_BASE_URL'] : [])
2899
- ],
2900
- base_url_present: basePresent,
2901
- base_url_matches: baseMatches,
2902
- api_key_present: keyPresent,
2903
- openrouter_api_key_present: openRouterKeyPresent,
2904
- next_actions: keyPresent || openRouterKeyPresent
2905
- ? ['Run: sks doctor --fix', 'Rotate CODEX_LB_API_KEY and OPENROUTER_API_KEY if they were exposed in launchd.']
2906
- : []
2907
- };
2908
- }
2909
231
  export { checkContext7, ensureCodexCliTool, ensureRelatedCliTools, maybePromptCodexUpdateForLaunch, maybePromptSksUpdateForLaunch, shouldAutoApproveInstall } from './install-tool-helpers.js';
2910
232
  export { codexFastModeDesktopStatus, ensureGlobalCodexFastModeDuringInstall, normalizeCodexFastModeUiConfig, safeWriteCodexConfigToml } from '../core/codex-runtime/codex-desktop-config-policy.js';
2911
233
  export { checkRequiredSkills, context7GlobalMcpStatus, ensureCodexImagegenDuringInstall, ensureGlobalCodexSkillsDuringInstall, ensureProjectContext7Config, ensureSksCommandDuringInstall, globalCodexSkillsRoot, selftestSksShimRepair } from './install-helpers-install-support.js';
2912
- export { askPostinstallQuestion, codexLbConfigPath, codexLbEnvPath, normalizeCodexLbBaseUrl } from './install-helpers-codex-lb-shared.js';
2913
- export { ensureGlobalCodexAppGlmProfile, ensureStoredOpenRouterProviderDuringInstall, upsertCodexAppGlmConfig, upsertCodexLbConfig } from './install-helpers-codex-lb-config.js';
2914
- export { checkCodexLbResponseChain } from './install-helpers-codex-lb-chain.js';
234
+ export { codexLbConfigPath, codexLbEnvPath } from './install-helpers-codex-lb-shared.js';