sneakoscope 7.5.0 → 7.6.0

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