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