sneakoscope 8.0.0 → 8.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +13 -2
  2. package/config/skills-hash-ledger.v1.json +63 -0
  3. package/crates/sks-core/Cargo.lock +1 -1
  4. package/crates/sks-core/Cargo.toml +1 -1
  5. package/dist/bin/fast-inline.js +14 -3
  6. package/dist/bin/sks-dispatch.js +1 -1
  7. package/dist/cli/codex-lb-setup-warning-output.js +17 -0
  8. package/dist/cli/command-manifest-lite.js +2 -13
  9. package/dist/cli/install-helpers-codex-lb-config.js +475 -8
  10. package/dist/cli/install-helpers-install-support.js +1 -1
  11. package/dist/cli/install-helpers.js +764 -68
  12. package/dist/cli/router.js +5 -2
  13. package/dist/commands/codex-lb.js +77 -47
  14. package/dist/commands/doctor-profile.js +1 -0
  15. package/dist/commands/doctor.js +165 -58
  16. package/dist/config/skills-manifest.json +193 -97
  17. package/dist/core/codex/codex-config-guard.js +70 -16
  18. package/dist/core/codex/oauth-callback-port-diagnostic.js +121 -0
  19. package/dist/core/codex-app/codex-app-fast-ui-repair.js +7 -2
  20. package/dist/core/codex-app/menubar/constants.js +3 -0
  21. package/dist/core/codex-app/menubar/installer.js +32 -9
  22. package/dist/core/codex-app/menubar/launch-agent.js +58 -6
  23. package/dist/core/codex-app/menubar/migration.js +17 -6
  24. package/dist/core/codex-app/menubar/status.js +15 -3
  25. package/dist/core/codex-app.js +15 -20
  26. package/dist/core/codex-control/codex-lb-launch-recovery.js +26 -0
  27. package/dist/core/codex-lb/capability-runner.js +1 -0
  28. package/dist/core/codex-lb/codex-lb-env.js +52 -41
  29. package/dist/core/codex-lb/codex-lb-setup.js +85 -8
  30. package/dist/core/codex-lb/desktop-center-credentials.js +474 -39
  31. package/dist/core/codex-lb/desktop-controller.js +118 -4
  32. package/dist/core/codex-lb/legacy-migration.js +6 -3
  33. package/dist/core/codex-native/codex-native-feature-broker.js +5 -1
  34. package/dist/core/codex-native/core-skill-manifest.js +1 -1
  35. package/dist/core/codex-native/managed-skill-generation-lock.js +13 -0
  36. package/dist/core/codex-native/sks-skill-paths.js +97 -10
  37. package/dist/core/codex-runtime/codex-desktop-config-policy.js +1 -0
  38. package/dist/core/commands/basic-cli.js +54 -4
  39. package/dist/core/commands/mad-sks-command.js +1 -1
  40. package/dist/core/commands/menubar-command.js +24 -8
  41. package/dist/core/commands/naruto-command.js +42 -217
  42. package/dist/core/doctor/codex-config-syntax-repair.js +368 -0
  43. package/dist/core/doctor/doctor-native-capability-repair.js +12 -5
  44. package/dist/core/doctor/doctor-readiness-matrix.js +51 -4
  45. package/dist/core/doctor/skill-legacy-surface.js +0 -1
  46. package/dist/core/doctor/sks-menubar-doctor.js +21 -0
  47. package/dist/core/fsx.js +7 -14
  48. package/dist/core/hooks-runtime/hook-context.js +30 -16
  49. package/dist/core/hooks-runtime/managed-skill-admission.js +17 -0
  50. package/dist/core/hooks-runtime/managed-skill-generation-heal.js +891 -30
  51. package/dist/core/hooks-runtime/official-subagent-lifecycle.js +99 -6
  52. package/dist/core/hooks-runtime/subagent-skill-availability-contract.js +3 -1
  53. package/dist/core/hooks-runtime/subagent-skill-availability-evidence.js +1 -1
  54. package/dist/core/hooks-runtime/subagent-skill-availability-guards.js +4 -4
  55. package/dist/core/hooks-runtime/subagent-skill-availability.js +9 -7
  56. package/dist/core/hooks-runtime/subagent-stop-hook.js +19 -0
  57. package/dist/core/hooks-runtime.js +41 -14
  58. package/dist/core/init/skills.js +270 -11
  59. package/dist/core/mission.js +20 -0
  60. package/dist/core/naruto/hardware-capacity-probe.js +3 -1
  61. package/dist/core/naruto/naruto-concurrency-governor.js +9 -3
  62. package/dist/core/pipeline-internals/runtime-core.js +68 -9
  63. package/dist/core/pipeline-internals/runtime-gates.js +10 -2
  64. package/dist/core/preflight/parallel-preflight-engine.js +18 -4
  65. package/dist/core/release/package-dist-snapshot.js +50 -8
  66. package/dist/core/routes.js +1 -1
  67. package/dist/core/safety/command-contract/contracts.js +10 -13
  68. package/dist/core/safety/command-contract/json-schema.js +38 -1
  69. package/dist/core/subagents/agent-catalog.js +37 -13
  70. package/dist/core/subagents/model-policy.js +14 -9
  71. package/dist/core/subagents/naruto-command-args.js +237 -0
  72. package/dist/core/subagents/naruto-command-input-contract.js +123 -0
  73. package/dist/core/subagents/naruto-help-contract.js +35 -2
  74. package/dist/core/subagents/naruto-launch-diagnostics.js +35 -0
  75. package/dist/core/subagents/official-subagent-config.js +30 -3
  76. package/dist/core/subagents/official-subagent-preparation.js +30 -5
  77. package/dist/core/subagents/official-subagent-prompt.js +4 -3
  78. package/dist/core/subagents/official-subagent-runner.js +12 -0
  79. package/dist/core/subagents/subagent-evidence.js +10 -0
  80. package/dist/core/subagents/thread-budget.js +1 -1
  81. package/dist/core/subagents/wave-lifecycle.js +41 -11
  82. package/dist/core/subagents/wave-parent-guidance.js +10 -3
  83. package/dist/core/update/update-migration-state.js +694 -63
  84. package/dist/core/update/update-operation.js +498 -32
  85. package/dist/core/update-check.js +990 -509
  86. package/dist/core/version.js +1 -1
  87. package/dist/native/sks-menubar/Sources/AppDelegate.swift +1 -0
  88. package/dist/native/sks-menubar/Sources/DiagnosticsViewController.swift +1 -1
  89. package/dist/native/sks-menubar/Sources/MCPServersViewController.swift +1 -1
  90. package/dist/native/sks-menubar/Sources/OperationCoordinator.swift +127 -12
  91. package/dist/native/sks-menubar/Sources/OverviewViewController.swift +7 -2
  92. package/dist/native/sks-menubar/Sources/ProcessClient.swift +287 -84
  93. package/dist/native/sks-menubar/Sources/ProcessExecutionState.swift +45 -0
  94. package/dist/native/sks-menubar/Sources/ProcessIdentityGuard.swift +196 -0
  95. package/dist/native/sks-menubar/Sources/SecureProcessEnvelope.swift +169 -0
  96. package/dist/native/sks-menubar/Sources/StatusItemController.swift +23 -5
  97. package/dist/native/sks-menubar/Sources/UpdatesViewController.swift +113 -24
  98. package/dist/native/sks-menubar/Sources/main.swift +12 -0
  99. package/dist/native/sks-menubar/Tests/AppRuntimeTestSupport.swift +14 -0
  100. package/dist/native/sks-menubar/Tests/OperationCoordinatorTests.swift +174 -0
  101. package/dist/native/sks-menubar/Tests/ProcessClientTests.swift +4 -0
  102. package/dist/scripts/current-surface-update-e2e-check.js +24 -5
  103. package/dist/scripts/lib/ensure-dist-fresh.js +12 -8
  104. package/dist/scripts/mutation-callsite-analysis.js +381 -0
  105. package/dist/scripts/mutation-callsite-coverage-check.js +127 -62
  106. package/dist/scripts/official-subagent-workflow-check.js +1 -1
  107. package/dist/scripts/skills-manifest-continuity-check.js +19 -1
  108. package/package.json +5 -3
  109. package/runtime-required-scripts.json +5 -0
  110. package/scripts/build-clean-atomic.mjs +323 -0
@@ -14,7 +14,7 @@ import { reconcileCodexAppUpgradeProcesses } from '../core/codex-app.js';
14
14
  import { restartCodexApp } from '../core/codex-app/codex-app-restart.js';
15
15
  import { cleanupMacLaunchSecretEnvironment } from '../core/codex-app/menubar/index.js';
16
16
  import { recordCodexLbHealthEvent } from '../core/codex-lb-circuit.js';
17
- import { loadCodexLbEnv, writeCodexLbKeychain, codexLbMetadataPath } from '../core/codex-lb/codex-lb-env.js';
17
+ import { CODEX_LB_SECURE_KEYCHAIN_SERVICE, loadCodexLbEnv, codexLbMetadataPath } from '../core/codex-lb/codex-lb-env.js';
18
18
  import { codexLbToolCatalogPath } from '../core/codex-lb/codex-lb-tool-catalog.js';
19
19
  import { codexLbToolOutputRecoveryNotChecked, codexLbToolOutputRecoveryNotSelected, codexLbToolOutputRecoveryOverrideAcknowledged, probeCodexLbToolOutputRecovery } from '../core/codex-lb/codex-lb-tool-output-recovery.js';
20
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';
@@ -26,9 +26,10 @@ import { ensureGlobalCodexFastModeDuringInstall, ensureTrailingNewline, normaliz
26
26
  import { runPostinstallGlobalDoctorAndMarkPending } from '../core/update/update-migration-state.js';
27
27
  import { repairCodexImagegen } from '../core/doctor/imagegen-repair.js';
28
28
  import { canAskYesNo, compareVersions, hasCodexUnstableFeatureWarningSuppression, hasDeprecatedCodexHooksFeatureFlag, isProjectSetupCandidate } from './install-tool-helpers.js';
29
+ import { printCodexLbSetupWarnings } from './codex-lb-setup-warning-output.js';
29
30
  import { checkCodexLbResponseChain } from './install-helpers-codex-lb-chain.js';
30
31
  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 { CODEX_LB_DESKTOP_BRIDGE_MARKER, CODEX_LB_DESKTOP_COMPAT_MARKER, appliedCodexLbPersistenceModes, captureCodexLbSetupWriteState, codexLbSharedOpenAiRoutingState, detectCodexLbSetupDrift, ensureGlobalCodexAppGlmProfile, ensureStoredOpenRouterProviderDuringInstall, hardenCodexLbSetupRecoveryPath, parseCodexLbEnvBaseUrl, removeCodexLbSetupRecoveryPath, removeCodexLbSharedOpenAiRouting, removeCodexLbManagedDesktopConfig, restoreCodexLbSetupWriteState, sha256Text, shellSingleQuote, upsertCodexAppGlmConfig, upsertCodexLbConfig, upsertCodexLbCliProviderConfig, upsertCodexLbCompatDesktopConfig, upsertCodexLbNativeDesktopConfig, upsertCodexLbSharedOpenAiRouting, writeCodexLbSetupFileIfUnchanged } from './install-helpers-codex-lb-config.js';
32
33
  import { detectLegacyCodexLbDesktopState } from '../core/codex-lb/legacy-migration.js';
33
34
  import { ensureCodexImagegenDuringInstall, ensureGlobalCodexSkillsDuringInstall, ensureGlobalContext7DuringInstall, ensureGlobalGetdesignSkillDuringInstall, ensureSksCommandDuringInstall } from './install-helpers-install-support.js';
34
35
  import { escapeRegExp } from '../core/text/regex.js';
@@ -261,6 +262,7 @@ async function reportPostinstallCodexLbAuth(snapshot = null) {
261
262
  console.log(`codex-lb key updated: ${result.base_url}`);
262
263
  else
263
264
  console.log(`codex-lb key update failed: ${result.status}${result.error ? `: ${result.error}` : ''}`);
265
+ printCodexLbSetupWarnings(result);
264
266
  }
265
267
  }
266
268
  }
@@ -336,6 +338,17 @@ export async function configureCodexLbDesktopRouting(opts) {
336
338
  auth_path: authPath,
337
339
  auth_before: authBefore
338
340
  };
341
+ if (desktopModeRequiresGlobalSecretEnvironment(mode)) {
342
+ return {
343
+ ...base,
344
+ ok: false,
345
+ status: 'desktop_dual_auth_compat_unavailable',
346
+ routing_plane: 'unchanged',
347
+ oauth_preserved: true,
348
+ auth_after: authBefore,
349
+ blockers: ['desktop_dual_auth_compat_requires_global_secret_environment']
350
+ };
351
+ }
339
352
  if (mode === 'cli-provider') {
340
353
  return {
341
354
  ...base,
@@ -640,19 +653,93 @@ export async function configureCodexLb(opts = {}) {
640
653
  const home = opts.home || process.env.HOME || os.homedir();
641
654
  const configPath = opts.configPath || codexLbConfigPath(home);
642
655
  const envPath = opts.envPath || codexLbEnvPath(home);
656
+ const metadataPath = opts.metadataPath || codexLbMetadataPath(home);
643
657
  const rawHost = String(opts.host || opts.baseUrl || '');
644
658
  const baseUrl = normalizeCodexLbBaseUrl(rawHost);
645
659
  const apiKey = String(opts.apiKey || '').trim();
646
- const initialConfig = await readText(configPath, '');
647
- const preservedDesktopMode = managedCodexLbDesktopMode(initialConfig);
648
- const preservedLegacySelection = hasTopLevelCodexLbSelected(initialConfig) && !preservedDesktopMode;
649
660
  const requestedDesktopMode = parseCodexLbDesktopMode(opts.desktopMode || 'cli-provider');
650
- const desktopMode = preservedDesktopMode || 'cli-provider';
651
- const useDefaultProvider = desktopMode === 'desktop-dual-auth-compat' || preservedLegacySelection;
652
661
  const writeEnvFile = opts.writeEnvFile !== false;
653
662
  const storeKeychain = opts.storeKeychain === true || opts.keychain === true;
663
+ const keychainStoreImpl = typeof opts.keychainStoreImpl === 'function'
664
+ ? opts.keychainStoreImpl
665
+ : null;
654
666
  const syncLaunchctl = opts.syncLaunchctl === true || opts.syncLaunchEnv === true;
655
667
  const shellProfile = opts.shellProfile || 'skip';
668
+ if (storeKeychain && !keychainStoreImpl) {
669
+ return {
670
+ ok: false,
671
+ status: 'keychain_acl_helper_unavailable',
672
+ config_path: configPath,
673
+ env_path: envPath,
674
+ metadata_path: metadataPath,
675
+ keychain: {
676
+ ok: false,
677
+ status: 'keychain_acl_helper_unavailable',
678
+ keychain_state_verified: true,
679
+ keychain_state_status: 'unchanged'
680
+ },
681
+ error: 'Dedicated signed Keychain helper unavailable; use the owner-only env file.'
682
+ };
683
+ }
684
+ const beforeState = await captureCodexLbSetupWriteState({
685
+ home,
686
+ configPath,
687
+ envPath,
688
+ metadataPath,
689
+ shellProfile
690
+ });
691
+ await opts.testHooks?.afterBeforeStateCapture?.({ configPath });
692
+ const configBeforeEntry = beforeState.files.find((entry) => entry?.path === configPath);
693
+ if (!configBeforeEntry) {
694
+ return {
695
+ ok: false,
696
+ status: 'setup_snapshot_failed',
697
+ config_path: configPath,
698
+ env_path: envPath,
699
+ metadata_path: metadataPath
700
+ };
701
+ }
702
+ const setupMutationPaths = new Set([
703
+ configPath,
704
+ metadataPath,
705
+ ...(writeEnvFile ? [envPath] : []),
706
+ ...(shellProfile === 'skip'
707
+ ? []
708
+ : beforeState.files
709
+ .map((entry) => String(entry?.path || ''))
710
+ .filter((file) => file && file !== configPath && file !== envPath && file !== metadataPath))
711
+ ]);
712
+ const unsafeSetupTargets = beforeState.files
713
+ .filter((entry) => setupMutationPaths.has(String(entry?.path || ''))
714
+ && entry?.existed === true
715
+ && entry?.kind !== 'regular')
716
+ .map((entry) => `unsafe_setup_write_target:${entry.path}:${entry.kind}`);
717
+ if (unsafeSetupTargets.length > 0) {
718
+ return {
719
+ ok: false,
720
+ status: 'unsafe_setup_write_target',
721
+ config_path: configPath,
722
+ env_path: envPath,
723
+ metadata_path: metadataPath,
724
+ drift: unsafeSetupTargets
725
+ };
726
+ }
727
+ const initialConfig = configBeforeEntry.existed === true
728
+ ? Buffer.from(String(configBeforeEntry.bytes_base64 || ''), 'base64').toString('utf8')
729
+ : '';
730
+ const preservedDesktopMode = managedCodexLbDesktopMode(initialConfig);
731
+ const preservedLegacySelection = hasTopLevelCodexLbSelected(initialConfig) && !preservedDesktopMode;
732
+ const desktopMode = preservedDesktopMode || 'cli-provider';
733
+ if (desktopModeRequiresGlobalSecretEnvironment(desktopMode)) {
734
+ return {
735
+ ok: false,
736
+ status: 'desktop_dual_auth_compat_unavailable',
737
+ config_path: configPath,
738
+ env_path: envPath,
739
+ error: 'desktop_dual_auth_compat_requires_global_secret_environment'
740
+ };
741
+ }
742
+ const useDefaultProvider = desktopMode === 'desktop-dual-auth-compat' || preservedLegacySelection;
656
743
  const setupAnswers = {
657
744
  host_or_base_url: rawHost,
658
745
  api_key_source: opts.apiKeySource || 'stdin',
@@ -660,6 +747,7 @@ export async function configureCodexLb(opts = {}) {
660
747
  gateway_auth_transport: (opts.gatewayAuthTransport || DEFAULT_CODEX_LB_GATEWAY_AUTH_TRANSPORT),
661
748
  write_env_file: writeEnvFile,
662
749
  store_keychain: storeKeychain,
750
+ keychain_helper_verified: Boolean(keychainStoreImpl),
663
751
  sync_launchctl: syncLaunchctl,
664
752
  install_shell_profile: shellProfile,
665
753
  run_health_check: opts.runHealth === true,
@@ -703,11 +791,14 @@ export async function configureCodexLb(opts = {}) {
703
791
  const insecureLocalWarning = /^http:\/\//i.test(baseUrl) && !/^http:\/\/(?:localhost|127\.0\.0\.1|\[::1\])(?::|\/|$)/i.test(baseUrl) && !opts.allowInsecureHttp
704
792
  ? ['codex-lb base URL uses http outside localhost; prefer https or pass an explicit allow flag in the calling surface.']
705
793
  : [];
706
- const beforeState = await captureCodexLbSetupWriteState({ home, configPath, envPath, shellProfile });
794
+ const processEnvBefore = {
795
+ baseUrl: process.env.CODEX_LB_BASE_URL,
796
+ apiKey: process.env.CODEX_LB_API_KEY
797
+ };
707
798
  const authBefore = await captureCodexAuthSnapshot({ home, authPath: opts.authPath || codexAuthPath(home) });
708
799
  const appliedActions = [];
709
800
  await ensureDir(path.dirname(configPath));
710
- const current = await readText(configPath, '');
801
+ const current = initialConfig;
711
802
  let providerOnly = current;
712
803
  try {
713
804
  if (desktopMode === 'desktop-native-bridge') {
@@ -746,7 +837,14 @@ export async function configureCodexLb(opts = {}) {
746
837
  error: error instanceof Error ? error.message : String(error)
747
838
  };
748
839
  }
749
- const safeWrite = await safeWriteCodexConfigToml(configPath, current, providerOnly, 'codex-lb');
840
+ await opts.testHooks?.beforeInitialConfigWrite?.({ configPath });
841
+ const safeWrite = await safeWriteCodexConfigToml(configPath, current, providerOnly, 'codex-lb', {
842
+ verifyUnchangedBeforeWrite: true,
843
+ expectedBeforeExists: configBeforeEntry.existed === true,
844
+ ...(configBeforeEntry.existed === true
845
+ ? { expectedBeforeMode: Number(configBeforeEntry.mode) }
846
+ : {})
847
+ });
750
848
  if (!safeWrite.ok)
751
849
  return { ok: false, status: safeWrite.status, config_path: configPath, env_path: envPath, backup_path: safeWrite.backup_path };
752
850
  appliedActions.push({
@@ -761,13 +859,205 @@ export async function configureCodexLb(opts = {}) {
761
859
  ok: true,
762
860
  backup_path: safeWrite.backup_path
763
861
  });
764
- if (writeEnvFile) {
765
- await writeTextAtomic(envPath, `export CODEX_LB_BASE_URL=${shellSingleQuote(baseUrl)}\nexport CODEX_LB_API_KEY=${shellSingleQuote(apiKey)}\n`, { mode: 0o600 });
766
- await fsp.chmod(envPath, 0o600).catch(() => { });
767
- appliedActions.push({ type: 'write_env_file', target: envPath, ok: true });
862
+ const configExpectedAfter = safeWrite.expected_after;
863
+ if (!configExpectedAfter || typeof configExpectedAfter.exists !== 'boolean') {
864
+ return {
865
+ ok: false,
866
+ status: 'partial_configuration_phase_snapshot_failed',
867
+ mode: desktopMode,
868
+ identity_plane: 'unchanged',
869
+ routing_plane: codexLbRoutingPlane(desktopMode),
870
+ oauth_preserved: true,
871
+ auth_mutated: false,
872
+ plan: plan,
873
+ applied_actions: appliedActions,
874
+ drift: ['setup_phase_snapshot_failed:after_config_write'],
875
+ config_path: configPath,
876
+ env_path: envPath,
877
+ metadata_path: metadataPath,
878
+ backup_path: safeWrite.backup_path,
879
+ error: 'guarded config writer did not return an owned post-write snapshot'
880
+ };
768
881
  }
769
- process.env.CODEX_LB_BASE_URL = baseUrl;
770
- process.env.CODEX_LB_API_KEY = apiKey;
882
+ let setupExpectedState = {
883
+ ...beforeState,
884
+ files: beforeState.files.map((entry) => entry.path === configPath
885
+ ? configExpectedAfter.exists
886
+ ? {
887
+ path: configPath,
888
+ existed: true,
889
+ kind: 'regular',
890
+ bytes_base64: Buffer.from(String(configExpectedAfter.text || '')).toString('base64'),
891
+ mode: Number(configExpectedAfter.mode)
892
+ }
893
+ : { path: configPath, existed: false, kind: 'missing', bytes_base64: '', mode: null }
894
+ : entry)
895
+ };
896
+ const recordExpectedRegularFile = (file, text, mode) => {
897
+ setupExpectedState = {
898
+ ...setupExpectedState,
899
+ files: setupExpectedState.files.map((entry) => entry.path === file
900
+ ? {
901
+ path: file,
902
+ existed: true,
903
+ kind: 'regular',
904
+ bytes_base64: Buffer.from(text).toString('base64'),
905
+ mode: mode & 0o777
906
+ }
907
+ : entry)
908
+ };
909
+ };
910
+ const setupWriteRecoveryPaths = [];
911
+ const writeSetupFile = async (input) => {
912
+ const expected = beforeState.files.find((entry) => path.resolve(String(entry?.path || '')) === path.resolve(input.file));
913
+ if (!expected) {
914
+ throw new Error(`setup_snapshot_missing:${input.file}`);
915
+ }
916
+ const result = await writeCodexLbSetupFileIfUnchanged({
917
+ file: input.file,
918
+ expected,
919
+ text: input.text,
920
+ mode: input.mode,
921
+ ...(input.beforeReplacement ? { beforeReplacement: input.beforeReplacement } : {})
922
+ });
923
+ if (result.recovery_path)
924
+ setupWriteRecoveryPaths.push(result.recovery_path);
925
+ if (result.ok || result.installed) {
926
+ recordExpectedRegularFile(input.file, input.text, input.mode);
927
+ }
928
+ if (!result.ok) {
929
+ throw new Error(`${result.status}:${input.file}${result.error ? `:${result.error}` : ''}`);
930
+ }
931
+ return result;
932
+ };
933
+ const rollbackSetupAfterFailure = async (input) => {
934
+ const rollback = await restoreCodexLbSetupWriteState(beforeState, input.expectedCurrentState, {
935
+ ...(typeof opts.testHooks?.beforeRollbackFileReplacement === 'function'
936
+ ? { beforeReplacement: opts.testHooks.beforeRollbackFileReplacement }
937
+ : {})
938
+ });
939
+ const rollbackBlockers = [...rollback.blockers];
940
+ const processRollbackBlockers = [];
941
+ if (input.restoreProcessEnvironment) {
942
+ if (process.env.CODEX_LB_BASE_URL === processEnvBefore.baseUrl) {
943
+ }
944
+ else if (process.env.CODEX_LB_BASE_URL === baseUrl) {
945
+ if (processEnvBefore.baseUrl === undefined)
946
+ delete process.env.CODEX_LB_BASE_URL;
947
+ else
948
+ process.env.CODEX_LB_BASE_URL = processEnvBefore.baseUrl;
949
+ }
950
+ else {
951
+ processRollbackBlockers.push('setup_rollback_conflict:process.env.CODEX_LB_BASE_URL');
952
+ }
953
+ if (process.env.CODEX_LB_API_KEY === processEnvBefore.apiKey) {
954
+ }
955
+ else if (process.env.CODEX_LB_API_KEY === apiKey) {
956
+ if (processEnvBefore.apiKey === undefined)
957
+ delete process.env.CODEX_LB_API_KEY;
958
+ else
959
+ process.env.CODEX_LB_API_KEY = processEnvBefore.apiKey;
960
+ }
961
+ else {
962
+ processRollbackBlockers.push('setup_rollback_conflict:process.env.CODEX_LB_API_KEY');
963
+ }
964
+ }
965
+ rollbackBlockers.push(...processRollbackBlockers);
966
+ const afterRollback = await captureCodexLbSetupWriteState({
967
+ home,
968
+ configPath,
969
+ envPath,
970
+ metadataPath,
971
+ shellProfile
972
+ });
973
+ if (beforeState.stateHash !== afterRollback.stateHash) {
974
+ rollbackBlockers.push('setup_rollback_state_verification_failed');
975
+ }
976
+ const recoveryPaths = [
977
+ ...(safeWrite.backup_path ? [safeWrite.backup_path] : []),
978
+ ...setupWriteRecoveryPaths,
979
+ ...rollback.recovery
980
+ ];
981
+ const secretRecoveryPaths = [...new Set(recoveryPaths)]
982
+ .filter((recoveryPath) => isRecoveryPathForFile(recoveryPath, envPath));
983
+ for (const recoveryPath of secretRecoveryPaths) {
984
+ if (!await hardenCodexLbSetupRecoveryPath(recoveryPath)) {
985
+ rollbackBlockers.push(`setup_secret_recovery_mode_unverified:${recoveryPath}`);
986
+ }
987
+ }
988
+ if (secretRecoveryPaths.length > 0) {
989
+ rollbackBlockers.push('setup_secret_recovery_retained');
990
+ }
991
+ const filesystemRollbackOk = rollbackBlockers.length === 0;
992
+ const keychain = input.keychain || { ok: false, status: 'not_written' };
993
+ return {
994
+ ok: false,
995
+ status: input.keychainRetained
996
+ ? 'partial_configuration_keychain_retained'
997
+ : input.externalStateMayBeMutated
998
+ ? 'partial_configuration_external_state_unknown'
999
+ : filesystemRollbackOk
1000
+ ? 'setup_failed_rolled_back'
1001
+ : 'setup_failed_rollback_incomplete',
1002
+ mode: desktopMode,
1003
+ identity_plane: 'unchanged',
1004
+ routing_plane: codexLbRoutingPlane(desktopMode),
1005
+ oauth_preserved: true,
1006
+ auth_mutated: false,
1007
+ plan: plan,
1008
+ applied_actions: appliedActions,
1009
+ drift: [
1010
+ `setup_stage_failed:${input.stage}`,
1011
+ ...(input.keychainRetained ? ['codex_lb_keychain_replacement_retained'] : []),
1012
+ ...(input.externalStateMayBeMutated ? ['codex_lb_external_environment_requires_inspection'] : []),
1013
+ ...rollbackBlockers
1014
+ ],
1015
+ rollback: {
1016
+ ...rollback,
1017
+ ok: filesystemRollbackOk,
1018
+ blockers: rollbackBlockers,
1019
+ byte_and_mode_verified: filesystemRollbackOk,
1020
+ keychain_retained: input.keychainRetained,
1021
+ secret_recovery_paths: secretRecoveryPaths,
1022
+ config_backup_path: safeWrite.backup_path || null,
1023
+ config_backup_status: safeWrite.backup_path ? 'retained_for_recovery' : 'not_created',
1024
+ recovery_paths: recoveryPaths
1025
+ },
1026
+ partial_configuration: {
1027
+ schema: 'sks.codex-lb-partial-configuration.v1',
1028
+ failure_stage: input.stage,
1029
+ filesystem_state: filesystemRollbackOk ? 'restored' : 'indeterminate',
1030
+ process_environment_state: input.restoreProcessEnvironment
1031
+ ? (processRollbackBlockers.length === 0 ? 'restored' : 'indeterminate')
1032
+ : 'unchanged',
1033
+ keychain_state: input.keychainRetained ? 'replacement_retained' : 'unchanged',
1034
+ external_environment_state: input.externalStateMayBeMutated ? 'inspect_with_status' : 'unchanged',
1035
+ durable_applied_state: [
1036
+ ...(input.keychainRetained
1037
+ ? [`macOS Keychain service ${CODEX_LB_SECURE_KEYCHAIN_SERVICE}`]
1038
+ : []),
1039
+ ...(input.externalStateMayBeMutated ? ['Center/launch environment may retain a partial update'] : [])
1040
+ ],
1041
+ recovery_actions: [
1042
+ 'Run: sks codex-lb status --json',
1043
+ 'Rerun setup with --api-key-stdin after resolving the failed stage.',
1044
+ `Inspect ${CODEX_LB_SECURE_KEYCHAIN_SERVICE} before removing any retained Keychain credential.`,
1045
+ ...(secretRecoveryPaths.length > 0
1046
+ ? ['Review and securely remove secret_recovery_paths after preserving any concurrent user edits.']
1047
+ : [])
1048
+ ],
1049
+ recovery_paths: recoveryPaths,
1050
+ secret_recovery_paths: secretRecoveryPaths
1051
+ },
1052
+ config_path: configPath,
1053
+ env_path: envPath,
1054
+ metadata_path: metadataPath,
1055
+ keychain,
1056
+ ...(input.centerCredentials ? { center_credentials: input.centerCredentials } : {}),
1057
+ ...(input.codexEnvironment ? { codex_environment: input.codexEnvironment } : {}),
1058
+ error: redactSecretText(input.error instanceof Error ? input.error.message : String(input.error || input.stage), [apiKey])
1059
+ };
1060
+ };
771
1061
  const toolCatalog = {
772
1062
  schema: 'sks.codex-lb-tool-catalog-selection.v1',
773
1063
  ok: true,
@@ -783,8 +1073,7 @@ export async function configureCodexLb(opts = {}) {
783
1073
  blockers: []
784
1074
  };
785
1075
  const keyFingerprint = await sha256Text(apiKey);
786
- const metadataPath = opts.metadataPath || codexLbMetadataPath(home);
787
- await writeTextAtomic(metadataPath, `${JSON.stringify({
1076
+ const metadataText = `${JSON.stringify({
788
1077
  schema: 'sks.codex-lb-metadata.v1',
789
1078
  base_url: baseUrl,
790
1079
  updated_at: new Date().toISOString(),
@@ -793,38 +1082,302 @@ export async function configureCodexLb(opts = {}) {
793
1082
  requested_desktop_mode: requestedDesktopMode,
794
1083
  gateway_auth_transport: opts.gatewayAuthTransport || DEFAULT_CODEX_LB_GATEWAY_AUTH_TRANSPORT,
795
1084
  api_key: { redacted: true, sha256: keyFingerprint }
796
- }, null, 2)}\n`);
797
- await fsp.chmod(metadataPath, 0o600).catch(() => { });
798
- appliedActions.push({ type: 'write_metadata', target: metadataPath, ok: true });
799
- const keychain = storeKeychain ? await writeCodexLbKeychain(apiKey, opts).catch((err) => ({ ok: false, status: 'keychain_store_failed', error: err.message })) : { ok: false, status: 'skipped' };
1085
+ }, null, 2)}\n`;
1086
+ try {
1087
+ await opts.testHooks?.beforeMetadataWrite?.({ metadataPath });
1088
+ const metadataWrite = await writeSetupFile({
1089
+ file: metadataPath,
1090
+ text: metadataText,
1091
+ mode: 0o600,
1092
+ ...(typeof opts.testHooks?.beforeSetupFileReplacement === 'function'
1093
+ ? { beforeReplacement: opts.testHooks.beforeSetupFileReplacement }
1094
+ : {})
1095
+ });
1096
+ appliedActions.push({
1097
+ type: 'write_metadata',
1098
+ target: metadataPath,
1099
+ ok: true,
1100
+ status: metadataWrite.status,
1101
+ recovery_path: metadataWrite.recovery_path || null
1102
+ });
1103
+ }
1104
+ catch (error) {
1105
+ return rollbackSetupAfterFailure({
1106
+ stage: 'write_metadata',
1107
+ error,
1108
+ keychainRetained: false,
1109
+ restoreProcessEnvironment: false,
1110
+ expectedCurrentState: setupExpectedState
1111
+ });
1112
+ }
1113
+ const keychainWriteState = setupExpectedState;
1114
+ const keychain = storeKeychain
1115
+ ? await keychainStoreImpl(apiKey, opts).catch((err) => ({
1116
+ ok: false,
1117
+ status: 'keychain_store_failed',
1118
+ error: err.message
1119
+ }))
1120
+ : { ok: false, status: 'skipped' };
800
1121
  if (storeKeychain)
801
- appliedActions.push({ type: 'store_keychain', target: 'macOS Keychain service sks-codex-lb', ok: keychain.ok === true, status: keychain.status });
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
- });
1122
+ appliedActions.push({ type: 'store_keychain', target: `macOS Keychain service ${CODEX_LB_SECURE_KEYCHAIN_SERVICE}`, ok: keychain.ok === true, status: keychain.status });
1123
+ if (storeKeychain && keychain.ok !== true) {
1124
+ await opts.testHooks?.beforeRollbackStart?.({ configPath });
1125
+ const rollback = await restoreCodexLbSetupWriteState(beforeState, keychainWriteState, {
1126
+ ...(typeof opts.testHooks?.beforeRollbackFileReplacement === 'function'
1127
+ ? { beforeReplacement: opts.testHooks.beforeRollbackFileReplacement }
1128
+ : {})
1129
+ });
1130
+ const rollbackBlockers = [...rollback.blockers];
1131
+ if (keychain.keychain_state_verified !== true) {
1132
+ rollbackBlockers.push('setup_rollback_keychain_state_indeterminate');
1133
+ }
1134
+ const backupStatus = safeWrite.backup_path ? 'retained_for_recovery' : 'not_created';
1135
+ const afterRollback = await captureCodexLbSetupWriteState({ home, configPath, envPath, metadataPath, shellProfile });
1136
+ if (beforeState.stateHash !== afterRollback.stateHash) {
1137
+ rollbackBlockers.push('setup_rollback_state_verification_failed');
1138
+ }
1139
+ const recoveryPaths = [
1140
+ ...(safeWrite.backup_path ? [safeWrite.backup_path] : []),
1141
+ ...setupWriteRecoveryPaths,
1142
+ ...rollback.recovery
1143
+ ];
1144
+ const secretRecoveryPaths = [...new Set(recoveryPaths)]
1145
+ .filter((recoveryPath) => isRecoveryPathForFile(recoveryPath, envPath));
1146
+ for (const recoveryPath of secretRecoveryPaths) {
1147
+ if (!await hardenCodexLbSetupRecoveryPath(recoveryPath)) {
1148
+ rollbackBlockers.push(`setup_secret_recovery_mode_unverified:${recoveryPath}`);
1149
+ }
1150
+ }
1151
+ if (secretRecoveryPaths.length > 0) {
1152
+ rollbackBlockers.push('setup_secret_recovery_retained');
1153
+ }
1154
+ const finalRollbackOk = rollbackBlockers.length === 0;
1155
+ return {
1156
+ ok: false,
1157
+ status: finalRollbackOk
1158
+ ? 'keychain_store_failed_rolled_back'
1159
+ : keychain.keychain_state_verified !== true
1160
+ ? 'keychain_state_indeterminate'
1161
+ : 'keychain_store_failed_rollback_incomplete',
1162
+ mode: desktopMode,
1163
+ identity_plane: 'unchanged',
1164
+ routing_plane: codexLbRoutingPlane(desktopMode),
1165
+ oauth_preserved: true,
1166
+ auth_mutated: false,
1167
+ plan: plan,
1168
+ applied_actions: appliedActions,
1169
+ drift: ['codex_lb_keychain_store_failed', ...rollbackBlockers],
1170
+ rollback: {
1171
+ ...rollback,
1172
+ ok: finalRollbackOk,
1173
+ blockers: rollbackBlockers,
1174
+ byte_and_mode_verified: finalRollbackOk,
1175
+ config_backup_path: safeWrite.backup_path || null,
1176
+ config_backup_status: backupStatus,
1177
+ recovery_paths: recoveryPaths,
1178
+ secret_recovery_paths: secretRecoveryPaths
1179
+ },
1180
+ config_path: configPath,
1181
+ env_path: envPath,
1182
+ metadata_path: metadataPath,
1183
+ keychain,
1184
+ error: keychain.error || keychain.status
1185
+ };
1186
+ }
1187
+ const writeSetupEnvFile = async () => {
1188
+ await opts.testHooks?.beforeEnvWrite?.({ envPath });
1189
+ const envText = `export CODEX_LB_BASE_URL=${shellSingleQuote(baseUrl)}\nexport CODEX_LB_API_KEY=${shellSingleQuote(apiKey)}\n`;
1190
+ const envWrite = await writeSetupFile({
1191
+ file: envPath,
1192
+ text: envText,
1193
+ mode: 0o600,
1194
+ ...(typeof opts.testHooks?.beforeSetupFileReplacement === 'function'
1195
+ ? { beforeReplacement: opts.testHooks.beforeSetupFileReplacement }
1196
+ : {})
1197
+ });
1198
+ appliedActions.push({
1199
+ type: 'write_env_file',
1200
+ target: envPath,
1201
+ ok: true,
1202
+ status: envWrite.status,
1203
+ recovery_path: envWrite.recovery_path || null
1204
+ });
1205
+ await opts.testHooks?.afterEnvWrite?.({ envPath });
1206
+ };
1207
+ if (writeEnvFile && !storeKeychain) {
1208
+ try {
1209
+ await writeSetupEnvFile();
1210
+ }
1211
+ catch (error) {
1212
+ return rollbackSetupAfterFailure({
1213
+ stage: 'write_env_file',
1214
+ error,
1215
+ keychain,
1216
+ keychainRetained: false,
1217
+ restoreProcessEnvironment: false,
1218
+ expectedCurrentState: setupExpectedState
1219
+ });
1220
+ }
1221
+ }
1222
+ let shellProfileResult = { ok: true, status: 'skipped', files: [] };
1223
+ try {
1224
+ await opts.testHooks?.beforeCenterSync?.();
1225
+ }
1226
+ catch (error) {
1227
+ return rollbackSetupAfterFailure({
1228
+ stage: 'sync_center_desktop_credentials',
1229
+ error,
1230
+ keychain,
1231
+ keychainRetained: storeKeychain && keychain.ok === true,
1232
+ restoreProcessEnvironment: false,
1233
+ expectedCurrentState: setupExpectedState
1234
+ });
1235
+ }
1236
+ const skipLaunchEnvironment = opts.syncLaunchEnv === false
1237
+ || process.env.SKS_SKIP_CODEX_LB_LAUNCH_ENV === '1';
1238
+ const { purgeStaleCodexLbCredentialTwins, syncDesktopCenterLaunchCredentials, verifyCodexLbLegacyKeychainReplacementStore } = await import('../core/codex-lb/desktop-center-credentials.js');
1239
+ const centerCredentials = skipLaunchEnvironment
1240
+ ? {
1241
+ schema: 'sks.codex-lb-desktop-center-credentials.v1',
1242
+ ok: true,
1243
+ status: 'skipped',
1244
+ mode: desktopMode,
1245
+ api_key_fingerprint: keyFingerprint,
1246
+ base_url_present: true,
1247
+ launch_env: { api_key: 'skipped', base_url: 'skipped' },
1248
+ stale_twins_removed: [],
1249
+ stale_twins_quarantined: [],
1250
+ stale_keychain_cleared: [],
1251
+ blockers: []
1252
+ }
1253
+ : await syncDesktopCenterLaunchCredentials({
1254
+ mode: desktopMode,
1255
+ home,
1256
+ ...(opts.platform ? { platform: opts.platform } : {}),
1257
+ ...(opts.launchctlBin ? { launchctlBin: opts.launchctlBin } : {}),
1258
+ ...(opts.securityBin ? { securityBin: opts.securityBin } : {}),
1259
+ ...(opts.runProcessImpl ? { runProcessImpl: opts.runProcessImpl } : {}),
1260
+ deferLegacyKeychainCleanup: writeEnvFile && !storeKeychain,
1261
+ loadedEnv: await loadCodexLbEnv({ ...opts, home, processEnv: {}, envPath, metadataPath })
1262
+ }).catch((error) => ({
1263
+ ok: false,
1264
+ status: 'center_desktop_credentials_failed',
1265
+ error: error instanceof Error ? error.message : String(error)
1266
+ }));
808
1267
  appliedActions.push({
809
1268
  type: 'sync_center_desktop_credentials',
810
1269
  target: 'launchctl from official sks-codex-lb store',
811
1270
  ok: centerCredentials.ok === true,
812
1271
  status: centerCredentials.status
813
1272
  });
1273
+ if (centerCredentials.ok !== true) {
1274
+ return rollbackSetupAfterFailure({
1275
+ stage: 'sync_center_desktop_credentials',
1276
+ error: centerCredentials.error || centerCredentials.status,
1277
+ keychain,
1278
+ keychainRetained: storeKeychain && keychain.ok === true,
1279
+ restoreProcessEnvironment: false,
1280
+ externalStateMayBeMutated: true,
1281
+ expectedCurrentState: setupExpectedState,
1282
+ centerCredentials
1283
+ });
1284
+ }
814
1285
  const codexEnvironment = await syncCodexLbProviderEnvironment({ env_path: envPath, base_url: baseUrl }, {
815
1286
  ...opts,
816
1287
  home,
817
1288
  apiKey,
818
1289
  baseUrl,
819
1290
  syncLaunchEnv: syncLaunchctl && desktopMode !== 'desktop-dual-auth-compat'
820
- });
1291
+ }).catch((error) => ({
1292
+ ok: false,
1293
+ status: 'environment_failed',
1294
+ error: error instanceof Error ? error.message : String(error)
1295
+ }));
821
1296
  if (syncLaunchctl && desktopMode !== 'desktop-dual-auth-compat') {
822
1297
  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
1298
  }
824
- const shellProfileResult = await installCodexLbShellProfileSnippet({ home, envPath, shellProfile }).catch((err) => ({ ok: false, status: 'failed', files: [], error: err.message }));
825
- if (shellProfile !== 'skip')
826
- appliedActions.push({ type: 'install_shell_profile_snippet', target: shellProfileResult.files?.join(', ') || shellProfile, ok: shellProfileResult.ok === true, status: shellProfileResult.status });
827
- const codexLb = await codexLbStatus({ ...opts, home, configPath, envPath });
1299
+ if (codexEnvironment.ok !== true) {
1300
+ return rollbackSetupAfterFailure({
1301
+ stage: 'sync_codex_environment',
1302
+ error: codexEnvironment.error || codexEnvironment.status,
1303
+ keychain,
1304
+ keychainRetained: storeKeychain && keychain.ok === true,
1305
+ restoreProcessEnvironment: true,
1306
+ externalStateMayBeMutated: true,
1307
+ expectedCurrentState: setupExpectedState,
1308
+ centerCredentials,
1309
+ codexEnvironment
1310
+ });
1311
+ }
1312
+ if (writeEnvFile && storeKeychain) {
1313
+ try {
1314
+ await writeSetupEnvFile();
1315
+ }
1316
+ catch (error) {
1317
+ return rollbackSetupAfterFailure({
1318
+ stage: 'write_env_file',
1319
+ error,
1320
+ keychain,
1321
+ keychainRetained: true,
1322
+ restoreProcessEnvironment: true,
1323
+ externalStateMayBeMutated: true,
1324
+ expectedCurrentState: setupExpectedState,
1325
+ centerCredentials,
1326
+ codexEnvironment
1327
+ });
1328
+ }
1329
+ }
1330
+ process.env.CODEX_LB_BASE_URL = baseUrl;
1331
+ process.env.CODEX_LB_API_KEY = apiKey;
1332
+ shellProfileResult = await installCodexLbShellProfileSnippet({
1333
+ home,
1334
+ envPath,
1335
+ shellProfile,
1336
+ expectedFiles: beforeState.files,
1337
+ writeFileIfUnchanged: async ({ file, text, mode }) => {
1338
+ try {
1339
+ await opts.testHooks?.beforeShellProfileWrite?.({ file });
1340
+ return await writeSetupFile({
1341
+ file,
1342
+ text,
1343
+ mode,
1344
+ ...(typeof opts.testHooks?.beforeSetupFileReplacement === 'function'
1345
+ ? { beforeReplacement: opts.testHooks.beforeSetupFileReplacement }
1346
+ : {})
1347
+ });
1348
+ }
1349
+ catch (error) {
1350
+ const message = error instanceof Error ? error.message : String(error);
1351
+ return {
1352
+ ok: false,
1353
+ status: message.split(':', 1)[0] || 'write_failed',
1354
+ error: message
1355
+ };
1356
+ }
1357
+ }
1358
+ })
1359
+ .catch((err) => ({ ok: false, status: 'failed', files: [], error: err.message }));
1360
+ if (shellProfile !== 'skip') {
1361
+ appliedActions.push({
1362
+ type: 'install_shell_profile_snippet',
1363
+ target: shellProfileResult.files?.join(', ') || shellProfile,
1364
+ ok: shellProfileResult.ok === true,
1365
+ status: shellProfileResult.status
1366
+ });
1367
+ }
1368
+ if (shellProfileResult.ok !== true) {
1369
+ return rollbackSetupAfterFailure({
1370
+ stage: 'install_shell_profile_snippet',
1371
+ error: shellProfileResult.error || shellProfileResult.status,
1372
+ keychain,
1373
+ keychainRetained: storeKeychain && keychain.ok === true,
1374
+ restoreProcessEnvironment: true,
1375
+ externalStateMayBeMutated: true,
1376
+ expectedCurrentState: setupExpectedState,
1377
+ centerCredentials,
1378
+ codexEnvironment
1379
+ });
1380
+ }
828
1381
  const authAfter = await captureCodexAuthSnapshot({ home, authPath: opts.authPath || codexAuthPath(home) });
829
1382
  let authInvariantError = null;
830
1383
  try {
@@ -845,9 +1398,23 @@ export async function configureCodexLb(opts = {}) {
845
1398
  reason: authInvariantError || 'cli_gateway_key_is_separate_from_chatgpt_oauth',
846
1399
  error: authInvariantError
847
1400
  };
1401
+ if (authInvariantError) {
1402
+ return rollbackSetupAfterFailure({
1403
+ stage: 'verify_desktop_auth_invariant',
1404
+ error: authInvariantError,
1405
+ keychain,
1406
+ keychainRetained: storeKeychain && keychain.ok === true,
1407
+ restoreProcessEnvironment: true,
1408
+ externalStateMayBeMutated: true,
1409
+ expectedCurrentState: setupExpectedState,
1410
+ centerCredentials,
1411
+ codexEnvironment
1412
+ });
1413
+ }
848
1414
  const finalCodexLb = await codexLbStatus({ ...opts, home, configPath, envPath });
849
- const ok = Boolean(codexEnvironment.ok && codexLogin.ok && centerCredentials.ok !== false);
850
- const afterState = await captureCodexLbSetupWriteState({ home, configPath, envPath, shellProfile });
1415
+ const keychainOk = !storeKeychain || keychain.ok === true;
1416
+ const ok = Boolean(codexEnvironment.ok && codexLogin.ok && keychainOk);
1417
+ const afterState = await captureCodexLbSetupWriteState({ home, configPath, envPath, metadataPath, shellProfile });
851
1418
  const drift = [
852
1419
  ...detectCodexLbSetupDrift({
853
1420
  useDefaultProvider,
@@ -864,6 +1431,7 @@ export async function configureCodexLb(opts = {}) {
864
1431
  afterState
865
1432
  }),
866
1433
  ...(authInvariantError ? ['desktop_auth_byte_invariant_failed'] : []),
1434
+ ...(!keychainOk ? ['codex_lb_keychain_store_failed'] : []),
867
1435
  ...(toolCatalog.required !== false && toolCatalog.ok !== true ? ['codex_lb_gpt56_tool_catalog_not_ready'] : [])
868
1436
  ];
869
1437
  const appliedPersistenceModes = appliedCodexLbPersistenceModes({
@@ -888,16 +1456,103 @@ export async function configureCodexLb(opts = {}) {
888
1456
  ...(preservedLegacySelection ? ['legacy_desktop_auth_routing_requires_explicit_migrate_legacy_desktop'] : []),
889
1457
  ...(toolCatalog.required !== false && toolCatalog.ok !== true ? ['codex_lb_gpt56_tool_catalog_not_ready'] : [])
890
1458
  ];
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;
1459
+ if (!ok || drift.length > 0) {
1460
+ return rollbackSetupAfterFailure({
1461
+ stage: 'verify_final_setup_state',
1462
+ error: drift.join(',') || 'configuration_failed',
1463
+ keychain,
1464
+ keychainRetained: storeKeychain && keychain.ok === true,
1465
+ restoreProcessEnvironment: true,
1466
+ externalStateMayBeMutated: true,
1467
+ expectedCurrentState: setupExpectedState,
1468
+ centerCredentials,
1469
+ codexEnvironment
1470
+ });
1471
+ }
1472
+ const retainedSetupRecoveryPaths = [];
1473
+ for (const recoveryPath of [...new Set(setupWriteRecoveryPaths)]) {
1474
+ if (await removeCodexLbSetupRecoveryPath(recoveryPath)) {
1475
+ for (const action of appliedActions) {
1476
+ if (action.recovery_path === recoveryPath) {
1477
+ action.recovery_path = null;
1478
+ action.recovery_claim_cleanup = 'removed_after_final_verification';
1479
+ }
1480
+ }
1481
+ continue;
1482
+ }
1483
+ retainedSetupRecoveryPaths.push(recoveryPath);
1484
+ await hardenCodexLbSetupRecoveryPath(recoveryPath);
1485
+ }
1486
+ const retainedSecretRecoveryPaths = retainedSetupRecoveryPaths
1487
+ .filter((recoveryPath) => isRecoveryPathForFile(recoveryPath, envPath));
1488
+ let legacyKeychainCleanup = {
1489
+ ok: true,
1490
+ status: 'not_applicable',
1491
+ keychain_cleared: [],
1492
+ blockers: []
1493
+ };
1494
+ if ((opts.platform || process.platform) === 'darwin' && writeEnvFile && !storeKeychain) {
1495
+ await opts.testHooks?.beforeLegacyKeychainCleanup?.({ envPath, metadataPath });
1496
+ const replacement = await verifyCodexLbLegacyKeychainReplacementStore({
1497
+ home,
1498
+ envPath,
1499
+ metadataPath,
1500
+ account: opts.account || process.env.USER || 'sks',
1501
+ expectedApiKeySha256: keyFingerprint
1502
+ });
1503
+ const cleanup = await purgeStaleCodexLbCredentialTwins({
1504
+ home,
1505
+ account: opts.account || process.env.USER || 'sks',
1506
+ platform: 'darwin',
1507
+ skipStaleEnvTwins: true,
1508
+ ...(opts.securityBin ? { securityBin: opts.securityBin } : {}),
1509
+ ...(opts.runProcessImpl ? { runProcessImpl: opts.runProcessImpl } : {}),
1510
+ ...(replacement.receipt
1511
+ ? { legacyKeychainMigrationReceipt: replacement.receipt }
1512
+ : {})
1513
+ });
1514
+ const cleanupBlockers = [
1515
+ ...(!replacement.ok && replacement.blockers.length === 0
1516
+ ? ['legacy_keychain_replacement_store_unverified']
1517
+ : replacement.blockers),
1518
+ ...cleanup.blockers
1519
+ ];
1520
+ const cleanupOk = replacement.ok && cleanupBlockers.length === 0;
1521
+ legacyKeychainCleanup = {
1522
+ ok: cleanupOk,
1523
+ status: !replacement.ok
1524
+ ? 'legacy_keychain_cleanup_blocked_replacement_store_unverified'
1525
+ : cleanupBlockers.length === 0
1526
+ ? cleanup.keychain_cleared.length > 0
1527
+ ? 'legacy_keychain_removed'
1528
+ : 'legacy_keychain_absent'
1529
+ : 'legacy_keychain_cleanup_failed_secure_store_retained',
1530
+ replacement_store_verified: replacement.ok,
1531
+ keychain_cleared: cleanup.keychain_cleared,
1532
+ blockers: [...new Set(cleanupBlockers)]
1533
+ };
1534
+ appliedActions.push({
1535
+ type: 'migrate_legacy_keychain',
1536
+ target: 'macOS Keychain service sks-codex-lb',
1537
+ ok: legacyKeychainCleanup.ok,
1538
+ status: legacyKeychainCleanup.status
1539
+ });
1540
+ }
1541
+ const finalWarnings = [
1542
+ ...warnings,
1543
+ ...(retainedSetupRecoveryPaths.length > 0 ? ['setup_recovery_claim_cleanup_required'] : []),
1544
+ ...(retainedSecretRecoveryPaths.length > 0 ? ['setup_secret_recovery_retained'] : []),
1545
+ ...(!legacyKeychainCleanup.ok
1546
+ ? ['legacy_keychain_cleanup_indeterminate_rotate_provider_key']
1547
+ : legacyKeychainCleanup.keychain_cleared.length > 0
1548
+ ? ['legacy_keychain_removed_rotate_provider_key_if_not_already_rotated']
1549
+ : [])
1550
+ ];
898
1551
  return {
899
- ok: ok && drift.length === 0,
900
- status: ok && drift.length === 0 ? 'configured' : failureStatus || (drift.length ? 'setup_choice_drift' : 'configuration_failed'),
1552
+ ok: legacyKeychainCleanup.ok,
1553
+ status: legacyKeychainCleanup.ok
1554
+ ? 'configured'
1555
+ : 'legacy_keychain_cleanup_failed_secure_store_retained',
901
1556
  mode: desktopMode,
902
1557
  identity_plane: 'unchanged',
903
1558
  routing_plane: codexLbRoutingPlane(desktopMode),
@@ -906,16 +1561,25 @@ export async function configureCodexLb(opts = {}) {
906
1561
  auth_mutated: false,
907
1562
  plan: plan,
908
1563
  applied_actions: appliedActions,
909
- drift,
1564
+ drift: [...drift, ...legacyKeychainCleanup.blockers],
910
1565
  persistence,
911
1566
  center_credentials: centerCredentials,
912
1567
  config_path: configPath,
913
1568
  env_path: envPath,
914
1569
  metadata_path: metadataPath,
1570
+ backup_path: safeWrite.backup_path || null,
1571
+ recovery_paths: [
1572
+ ...new Set([
1573
+ ...(safeWrite.backup_path ? [safeWrite.backup_path] : []),
1574
+ ...retainedSetupRecoveryPaths
1575
+ ])
1576
+ ],
1577
+ secret_recovery_paths: retainedSecretRecoveryPaths,
915
1578
  base_url: baseUrl,
916
1579
  env_key: 'CODEX_LB_API_KEY',
917
1580
  keychain,
918
- warnings,
1581
+ legacy_keychain_cleanup: legacyKeychainCleanup,
1582
+ warnings: finalWarnings,
919
1583
  auth_reconcile: authReconcile,
920
1584
  codex_lb: finalCodexLb,
921
1585
  codex_environment: codexEnvironment,
@@ -925,6 +1589,14 @@ export async function configureCodexLb(opts = {}) {
925
1589
  error: authReconcile.error || codexEnvironment.error || codexLogin.error || null
926
1590
  };
927
1591
  }
1592
+ function isRecoveryPathForFile(recoveryPath, file) {
1593
+ const target = path.resolve(file);
1594
+ const candidate = path.resolve(String(recoveryPath || ''));
1595
+ return candidate.startsWith(`${target}.sks-`);
1596
+ }
1597
+ function desktopModeRequiresGlobalSecretEnvironment(mode) {
1598
+ return mode === 'desktop-dual-auth-compat';
1599
+ }
928
1600
  export async function codexLbStatus(opts = {}) {
929
1601
  const home = opts.home || process.env.HOME || os.homedir();
930
1602
  const configPath = opts.configPath || codexLbConfigPath(home);
@@ -965,17 +1637,8 @@ export async function codexLbStatus(opts = {}) {
965
1637
  && providerEnvKey === CODEX_LB_PROVIDER_ENV_KEY
966
1638
  && providerSupportsWebsockets === true
967
1639
  && 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;
1640
+ const retiredCompatConfigured = desktopMode === 'desktop-dual-auth-compat';
1641
+ const providerContractOk = !retiredCompatConfigured && cliProviderContractOk;
979
1642
  const providerUsesCodexLbEnvAuth = providerConfigured && providerEnvKey === CODEX_LB_PROVIDER_ENV_KEY && providerOpenAiAuthDisabled;
980
1643
  const oauthAvailable = authMode.mode === 'chatgpt_oauth' || authMode.mode === 'browser_marker';
981
1644
  const authRoutingCoherent = desktopMode === 'desktop-native-bridge'
@@ -985,8 +1648,8 @@ export async function codexLbStatus(opts = {}) {
985
1648
  : !codexLbKeyInSharedAuth && (!selected || providerUsesCodexLbEnvAuth);
986
1649
  const codexAppUsableWithCodexLb = desktopMode === 'desktop-native-bridge'
987
1650
  ? Boolean(bridgeBaseUrl && oauthAvailable)
988
- : desktopMode === 'desktop-dual-auth-compat'
989
- ? compatProviderContractOk && oauthAvailable
1651
+ : retiredCompatConfigured
1652
+ ? false
990
1653
  : authMode.codex_app_usable;
991
1654
  const fastMode = codexLbFastModeConfigStatus(config);
992
1655
  const launchEnvironment = await inspectCodexLbMacLaunchEnvironment(baseUrl, opts).catch((err) => ({
@@ -999,7 +1662,14 @@ export async function codexLbStatus(opts = {}) {
999
1662
  && envLoad.configured
1000
1663
  && Boolean(baseUrl)
1001
1664
  && authRoutingCoherent
1665
+ && !retiredCompatConfigured
1002
1666
  && (desktopMode === 'desktop-native-bridge' ? Boolean(bridgeBaseUrl) : true);
1667
+ const blockers = [
1668
+ ...(retiredCompatConfigured ? ['desktop_dual_auth_compat_unavailable'] : []),
1669
+ ...(!retiredCompatConfigured && providerConfigured && !providerContractOk
1670
+ ? ['codex_lb_provider_contract_drift']
1671
+ : [])
1672
+ ];
1003
1673
  const probeToolOutputRecovery = opts.probeToolOutputRecovery === true;
1004
1674
  const toolOutputRecovery = !selected
1005
1675
  ? codexLbToolOutputRecoveryNotSelected()
@@ -1014,6 +1684,7 @@ export async function codexLbStatus(opts = {}) {
1014
1684
  });
1015
1685
  return {
1016
1686
  ok: providerReady && (!selected || !probeToolOutputRecovery || toolOutputRecovery.ok),
1687
+ blockers,
1017
1688
  provider_ready: providerReady,
1018
1689
  config_path: configPath,
1019
1690
  env_path: envPath,
@@ -1114,8 +1785,8 @@ export function formatCodexLbStatusText(status = {}, opts = {}) {
1114
1785
  lines.push('', 'Sign in with ChatGPT OAuth, then run: sks codex-lb use-desktop-full.');
1115
1786
  else if (status.ok && status.desktop_mode === 'cli-provider' && !status.selected)
1116
1787
  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.');
1788
+ else if (status.desktop_mode === 'desktop-dual-auth-compat')
1789
+ lines.push('', 'The retired compatibility route is blocked. Run `sks codex-lb use-desktop-full` or `sks codex-lb disable`.');
1119
1790
  else if (status.ok)
1120
1791
  lines.push('', 'Status: configured; no ordinary repair needed.');
1121
1792
  else if (!status.ok && status.base_url && status.env_key_configured)
@@ -2027,8 +2698,15 @@ export async function maybePromptCodexLbSetupForLaunch(args = [], opts = {}) {
2027
2698
  console.log(`codex-lb credentials stored: ${configured.base_url}. Use \`sks codex-lb use-cli\`, \`use-desktop-full\`, or \`disable\` explicitly.`);
2028
2699
  else
2029
2700
  console.log('codex-lb setup skipped: API key was empty.');
2701
+ printCodexLbSetupWarnings(configured);
2030
2702
  return configured;
2031
2703
  }
2704
+ function scrubCodexLbToolEnvironment(env = process.env) {
2705
+ const sanitized = { ...env };
2706
+ delete sanitized.CODEX_LB_API_KEY;
2707
+ delete sanitized.OPENROUTER_API_KEY;
2708
+ return sanitized;
2709
+ }
2032
2710
  async function syncCodexLbProviderEnvironment(status = {}, opts = {}) {
2033
2711
  const home = opts.home || process.env.HOME || os.homedir();
2034
2712
  const envPath = opts.envPath || status.env_path || codexLbEnvPath(home);
@@ -2056,10 +2734,16 @@ async function syncCodexLbMacLaunchEnvironment(values = {}, opts = {}) {
2056
2734
  return { ok: true, status: 'skipped', skipped: true, reason: 'SKS_SKIP_CODEX_LB_LAUNCH_ENV=1' };
2057
2735
  if (process.platform !== 'darwin' && !opts.forceLaunchEnv)
2058
2736
  return { ok: true, status: 'not_macos', skipped: true };
2059
- const launchctl = opts.launchctlBin || await which('launchctl').catch(() => null) || await exists('/bin/launchctl').then((ok) => ok ? '/bin/launchctl' : null).catch(() => null);
2737
+ const launchctl = opts.launchctlBin
2738
+ || await exists('/bin/launchctl').then((ok) => ok ? '/bin/launchctl' : null).catch(() => null);
2060
2739
  if (!launchctl)
2061
- return { ok: false, status: 'launchctl_missing', error: 'launchctl not found on PATH' };
2062
- const secretCleanup = await cleanupMacLaunchSecretEnvironment({ force: opts.forceLaunchEnv === true }).catch((err) => ({
2740
+ return { ok: false, status: 'launchctl_missing', error: '/bin/launchctl not found' };
2741
+ const childEnv = scrubCodexLbToolEnvironment();
2742
+ const secretCleanup = await cleanupMacLaunchSecretEnvironment({
2743
+ force: opts.forceLaunchEnv === true,
2744
+ launchctlBin: launchctl,
2745
+ env: childEnv
2746
+ }).catch((err) => ({
2063
2747
  ok: false,
2064
2748
  status: 'partial',
2065
2749
  variables: ['CODEX_LB_API_KEY', 'OPENROUTER_API_KEY'],
@@ -2070,7 +2754,12 @@ async function syncCodexLbMacLaunchEnvironment(values = {}, opts = {}) {
2070
2754
  const variables = Object.entries(values).filter(([key, value]) => value && !['CODEX_LB_API_KEY', 'OPENROUTER_API_KEY'].includes(String(key)));
2071
2755
  const results = [];
2072
2756
  for (const [key, value] of variables) {
2073
- const result = await runProcess(launchctl, ['setenv', key, String(value)], { timeoutMs: 5000, maxOutputBytes: 8192 });
2757
+ const result = await runProcess(launchctl, ['setenv', key, String(value)], {
2758
+ timeoutMs: 5000,
2759
+ maxOutputBytes: 8192,
2760
+ env: childEnv,
2761
+ envMode: 'replace'
2762
+ });
2074
2763
  results.push({
2075
2764
  key,
2076
2765
  ok: result.code === 0,
@@ -2091,11 +2780,18 @@ async function syncCodexLbMacLaunchEnvironment(values = {}, opts = {}) {
2091
2780
  async function inspectCodexLbMacLaunchEnvironment(baseUrl = '', opts = {}) {
2092
2781
  if (process.platform !== 'darwin' && !opts.forceLaunchEnv)
2093
2782
  return { checked: false, status: 'not_macos', skipped: true };
2094
- const launchctl = opts.launchctlBin || await which('launchctl').catch(() => null) || await exists('/bin/launchctl').then((ok) => ok ? '/bin/launchctl' : null).catch(() => null);
2783
+ const launchctl = opts.launchctlBin
2784
+ || await exists('/bin/launchctl').then((ok) => ok ? '/bin/launchctl' : null).catch(() => null);
2095
2785
  if (!launchctl)
2096
2786
  return { checked: true, available: false, status: 'launchctl_missing' };
2787
+ const childEnv = scrubCodexLbToolEnvironment();
2097
2788
  const readVar = async (key) => {
2098
- const result = await runProcess(launchctl, ['getenv', key], { timeoutMs: 3000, maxOutputBytes: 8192 });
2789
+ const result = await runProcess(launchctl, ['getenv', key], {
2790
+ timeoutMs: 3000,
2791
+ maxOutputBytes: 8192,
2792
+ env: childEnv,
2793
+ envMode: 'replace'
2794
+ });
2099
2795
  return result.code === 0 ? String(result.stdout || '').trim() : '';
2100
2796
  };
2101
2797
  const [currentBaseUrl, currentApiKey, currentOpenRouterKey] = await Promise.all([