sneakoscope 6.7.0 → 7.0.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 +3 -3
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/cli/install-helpers-codex-lb-config.js +71 -0
- package/dist/cli/install-helpers-codex-lb-selftest.js +11 -14
- package/dist/cli/install-helpers.js +305 -53
- package/dist/config/skills-manifest.json +52 -52
- package/dist/core/agents/agent-orchestrator.js +56 -5
- package/dist/core/agents/agent-roster.js +4 -2
- package/dist/core/codex-app/codex-app-fast-ui-repair.js +18 -1
- package/dist/core/codex-app/codex-app-ui-state-snapshot.js +16 -0
- package/dist/core/codex-app.js +52 -6
- package/dist/core/hooks-runtime.js +77 -3
- package/dist/core/managed-assets/managed-assets-manifest.js +1 -1
- package/dist/core/release/package-size-budget.js +1 -1
- package/dist/core/subagents/official-subagent-config.js +8 -5
- package/dist/core/subagents/official-subagent-preparation.js +24 -1
- package/dist/core/subagents/official-subagent-prompt.js +3 -2
- package/dist/core/subagents/wave-lifecycle.js +20 -0
- package/dist/core/subagents/wave-parent-guidance.js +42 -0
- package/dist/core/version.js +1 -1
- package/dist/native/sks-menubar/Sources/AppIdentity.swift +3 -1
- package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +14 -8
- package/dist/native/sks-menubar/Sources/DiagnosticsViewController.swift +71 -7
- package/dist/native/sks-menubar/Sources/MCPServersViewController.swift +50 -9
- package/dist/native/sks-menubar/Sources/OverviewViewController.swift +61 -8
- package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +120 -29
- package/dist/native/sks-menubar/Sources/RemoteTelegramViewController.swift +58 -23
- package/dist/native/sks-menubar/Sources/SettingsViewController.swift +41 -7
- package/dist/native/sks-menubar/Sources/StatusItemController.swift +34 -3
- package/dist/native/sks-menubar/Sources/UpdatesViewController.swift +38 -8
- package/dist/scripts/docs-truthfulness-check.js +2 -2
- package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +7 -3
- package/package.json +1 -1
|
@@ -28,7 +28,7 @@ import { repairCodexImagegen } from '../core/doctor/imagegen-repair.js';
|
|
|
28
28
|
import { canAskYesNo, compareVersions, hasCodexUnstableFeatureWarningSuppression, hasDeprecatedCodexHooksFeatureFlag, hasTopLevelCodexModeLock, isProjectSetupCandidate } from './install-tool-helpers.js';
|
|
29
29
|
import { checkCodexLbResponseChain } from './install-helpers-codex-lb-chain.js';
|
|
30
30
|
import { CODEX_LB_CANONICAL_FAST_SERVICE_TIER, CODEX_LB_PROVIDER_ENV_KEY, CODEX_LB_PROVIDER_NAME, askPostinstallQuestion, codexAuthChatgptBackupPath, codexAuthPath, codexLbConfigPath, codexLbEnvPath, hasTopLevelCodexLbSelected, normalizeCodexLbBaseUrl, parseCodexLbEnvKey, redactSecretText } from './install-helpers-codex-lb-shared.js';
|
|
31
|
-
import { appliedCodexLbPersistenceModes, captureCodexLbSetupWriteState, detectCodexLbSetupDrift, ensureGlobalCodexAppGlmProfile, parseCodexLbEnvBaseUrl, parseCodexSharedLoginApiKey, sha256Text, shellSingleQuote, upsertCodexAppGlmConfig, upsertCodexLbConfig } from './install-helpers-codex-lb-config.js';
|
|
31
|
+
import { appliedCodexLbPersistenceModes, captureCodexLbSetupWriteState, codexLbSharedOpenAiRoutingState, detectCodexLbSetupDrift, ensureGlobalCodexAppGlmProfile, parseCodexLbEnvBaseUrl, parseCodexSharedLoginApiKey, removeCodexLbSharedOpenAiRouting, sha256Text, shellSingleQuote, upsertCodexAppGlmConfig, upsertCodexLbConfig, upsertCodexLbSharedOpenAiRouting } from './install-helpers-codex-lb-config.js';
|
|
32
32
|
import { ensureCodexImagegenDuringInstall, ensureGlobalCodexSkillsDuringInstall, ensureGlobalContext7DuringInstall, ensureGlobalGetdesignSkillDuringInstall, ensureSksCommandDuringInstall } from './install-helpers-install-support.js';
|
|
33
33
|
function packagedSksEntrypoint() {
|
|
34
34
|
return path.join(packageRoot(), 'dist', 'bin', 'sks.js');
|
|
@@ -383,8 +383,11 @@ async function ensureCodexLbToolCatalogSelection(input, opts = {}) {
|
|
|
383
383
|
const catalogPath = codexLbToolCatalogPath(codexHome);
|
|
384
384
|
const current = await readText(input.configPath, '');
|
|
385
385
|
const selected = hasTopLevelCodexLbSelected(current);
|
|
386
|
+
// Setup writes the catalog pointer before selecting the provider so Desktop can
|
|
387
|
+
// load GPT-5.6/tool metadata the moment model_provider becomes codex-lb.
|
|
388
|
+
const canBindCatalog = selected || opts.allowUnselectedCatalogWrite === true;
|
|
386
389
|
const existingCatalogPath = topLevelTomlString(current, 'model_catalog_json');
|
|
387
|
-
if (
|
|
390
|
+
if (canBindCatalog && existingCatalogPath && path.resolve(existingCatalogPath) !== path.resolve(catalogPath)) {
|
|
388
391
|
return {
|
|
389
392
|
schema: 'sks.codex-lb-tool-catalog-selection.v1',
|
|
390
393
|
ok: false,
|
|
@@ -393,6 +396,7 @@ async function ensureCodexLbToolCatalogSelection(input, opts = {}) {
|
|
|
393
396
|
path: catalogPath,
|
|
394
397
|
configured_path: existingCatalogPath,
|
|
395
398
|
config_changed: false,
|
|
399
|
+
selected,
|
|
396
400
|
blockers: ['codex_lb_user_model_catalog_conflict']
|
|
397
401
|
};
|
|
398
402
|
}
|
|
@@ -410,6 +414,7 @@ async function ensureCodexLbToolCatalogSelection(input, opts = {}) {
|
|
|
410
414
|
status: 'skipped_reserved_host',
|
|
411
415
|
path: catalogPath,
|
|
412
416
|
config_changed: false,
|
|
417
|
+
selected,
|
|
413
418
|
blockers: []
|
|
414
419
|
};
|
|
415
420
|
}
|
|
@@ -421,7 +426,7 @@ async function ensureCodexLbToolCatalogSelection(input, opts = {}) {
|
|
|
421
426
|
timeoutMs: Number(opts.toolCatalogTimeoutMs || 5000),
|
|
422
427
|
force: opts.forceToolCatalog === true
|
|
423
428
|
});
|
|
424
|
-
if (!catalog.ok || !
|
|
429
|
+
if (!catalog.ok || !canBindCatalog) {
|
|
425
430
|
return {
|
|
426
431
|
...catalog,
|
|
427
432
|
schema: 'sks.codex-lb-tool-catalog-selection.v1',
|
|
@@ -431,7 +436,7 @@ async function ensureCodexLbToolCatalogSelection(input, opts = {}) {
|
|
|
431
436
|
}
|
|
432
437
|
const next = ensureTrailingNewline(upsertTopLevelTomlString(current, 'model_catalog_json', catalog.path));
|
|
433
438
|
if (next === ensureTrailingNewline(current)) {
|
|
434
|
-
return { ...catalog, schema: 'sks.codex-lb-tool-catalog-selection.v1', config_changed: false, selected
|
|
439
|
+
return { ...catalog, schema: 'sks.codex-lb-tool-catalog-selection.v1', config_changed: false, selected };
|
|
435
440
|
}
|
|
436
441
|
const safeWrite = await safeWriteCodexConfigToml(input.configPath, current, next, 'codex-lb-tool-catalog');
|
|
437
442
|
return {
|
|
@@ -441,7 +446,7 @@ async function ensureCodexLbToolCatalogSelection(input, opts = {}) {
|
|
|
441
446
|
status: safeWrite.ok ? catalog.status : safeWrite.status,
|
|
442
447
|
config_changed: safeWrite.ok && safeWrite.changed === true,
|
|
443
448
|
backup_path: safeWrite.backup_path,
|
|
444
|
-
selected
|
|
449
|
+
selected,
|
|
445
450
|
blockers: safeWrite.ok ? catalog.blockers : [...new Set([...(catalog.blockers || []), 'codex_lb_tool_catalog_config_write_failed'])]
|
|
446
451
|
};
|
|
447
452
|
}
|
|
@@ -509,16 +514,17 @@ export async function configureCodexLb(opts = {}) {
|
|
|
509
514
|
const beforeState = await captureCodexLbSetupWriteState({ home, configPath, envPath, shellProfile });
|
|
510
515
|
const appliedActions = [];
|
|
511
516
|
await ensureDir(path.dirname(configPath));
|
|
517
|
+
// Write the provider table first without selecting it. Desktop model/tool UI
|
|
518
|
+
// exposure depends on a validated catalog; selecting before that leaves the App
|
|
519
|
+
// on codex-lb with no GPT-5.6 picker surface.
|
|
512
520
|
const current = await readText(configPath, '');
|
|
513
|
-
const
|
|
521
|
+
const providerOnly = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(current, baseUrl, false), {
|
|
514
522
|
forceFastMode: opts.forceFastMode !== false
|
|
515
523
|
});
|
|
516
|
-
const safeWrite = await safeWriteCodexConfigToml(configPath, current,
|
|
524
|
+
const safeWrite = await safeWriteCodexConfigToml(configPath, current, providerOnly, 'codex-lb');
|
|
517
525
|
if (!safeWrite.ok)
|
|
518
526
|
return { ok: false, status: safeWrite.status, config_path: configPath, env_path: envPath, backup_path: safeWrite.backup_path };
|
|
519
527
|
appliedActions.push({ type: 'write_config_provider', target: configPath, ok: true, backup_path: safeWrite.backup_path });
|
|
520
|
-
if (useDefaultProvider)
|
|
521
|
-
appliedActions.push({ type: 'select_default_provider', target: configPath, ok: true });
|
|
522
528
|
if (writeEnvFile) {
|
|
523
529
|
await writeTextAtomic(envPath, `export CODEX_LB_BASE_URL=${shellSingleQuote(baseUrl)}\nexport CODEX_LB_API_KEY=${shellSingleQuote(apiKey)}\n`, { mode: 0o600 });
|
|
524
530
|
await fsp.chmod(envPath, 0o600).catch(() => { });
|
|
@@ -526,10 +532,48 @@ export async function configureCodexLb(opts = {}) {
|
|
|
526
532
|
}
|
|
527
533
|
process.env.CODEX_LB_BASE_URL = baseUrl;
|
|
528
534
|
process.env.CODEX_LB_API_KEY = apiKey;
|
|
529
|
-
|
|
535
|
+
let toolCatalog = await ensureCodexLbToolCatalogSelection({ home, configPath, baseUrl, apiKey }, { ...opts, allowUnselectedCatalogWrite: true });
|
|
530
536
|
if (toolCatalog.config_changed || toolCatalog.status === 'repaired' || toolCatalog.status === 'cached_compatible') {
|
|
531
537
|
appliedActions.push({ type: 'write_model_tool_catalog', target: toolCatalog.path, ok: toolCatalog.ok === true, status: toolCatalog.status });
|
|
532
538
|
}
|
|
539
|
+
const catalogReadyForSelection = toolCatalog.ok === true
|
|
540
|
+
&& (toolCatalog.required === false || ['repaired', 'cached_compatible', 'cached_compatible_after_refresh_failed', 'cached_compatible_after_refresh_rejected', 'skipped_reserved_host', 'present'].includes(String(toolCatalog.status || '')));
|
|
541
|
+
let providerSelected = false;
|
|
542
|
+
if (useDefaultProvider && catalogReadyForSelection) {
|
|
543
|
+
const beforeSelect = await readText(configPath, '');
|
|
544
|
+
const selectedConfig = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(beforeSelect, baseUrl, true), {
|
|
545
|
+
forceFastMode: opts.forceFastMode !== false
|
|
546
|
+
});
|
|
547
|
+
const selectWrite = await safeWriteCodexConfigToml(configPath, beforeSelect, selectedConfig, 'codex-lb-select');
|
|
548
|
+
if (!selectWrite.ok) {
|
|
549
|
+
return {
|
|
550
|
+
ok: false,
|
|
551
|
+
status: selectWrite.status || 'provider_selection_failed',
|
|
552
|
+
config_path: configPath,
|
|
553
|
+
env_path: envPath,
|
|
554
|
+
backup_path: selectWrite.backup_path,
|
|
555
|
+
tool_catalog: toolCatalog,
|
|
556
|
+
tool_output_recovery: toolOutputRecovery,
|
|
557
|
+
error: 'codex_lb_provider_selection_write_failed'
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
providerSelected = true;
|
|
561
|
+
appliedActions.push({ type: 'select_default_provider', target: configPath, ok: true, backup_path: selectWrite.backup_path });
|
|
562
|
+
// Re-bind after selection so model_catalog_json stays attached to the active provider.
|
|
563
|
+
toolCatalog = await ensureCodexLbToolCatalogSelection({ home, configPath, baseUrl, apiKey }, opts);
|
|
564
|
+
if (toolCatalog.config_changed) {
|
|
565
|
+
appliedActions.push({ type: 'write_model_tool_catalog', target: toolCatalog.path, ok: toolCatalog.ok === true, status: toolCatalog.status });
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
else if (useDefaultProvider) {
|
|
569
|
+
appliedActions.push({
|
|
570
|
+
type: 'select_default_provider',
|
|
571
|
+
target: configPath,
|
|
572
|
+
ok: false,
|
|
573
|
+
status: 'deferred_until_tool_catalog_ready',
|
|
574
|
+
blockers: toolCatalog.blockers || ['codex_lb_gpt56_tool_catalog_not_ready']
|
|
575
|
+
});
|
|
576
|
+
}
|
|
533
577
|
const keyFingerprint = await sha256Text(apiKey);
|
|
534
578
|
const metadataPath = opts.metadataPath || codexLbMetadataPath(home);
|
|
535
579
|
await writeTextAtomic(metadataPath, `${JSON.stringify({
|
|
@@ -551,11 +595,18 @@ export async function configureCodexLb(opts = {}) {
|
|
|
551
595
|
if (shellProfile !== 'skip')
|
|
552
596
|
appliedActions.push({ type: 'install_shell_profile_snippet', target: shellProfileResult.files?.join(', ') || shellProfile, ok: shellProfileResult.ok === true, status: shellProfileResult.status });
|
|
553
597
|
const codexLb = await codexLbStatus({ ...opts, home, configPath, envPath });
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
|
|
598
|
+
// Never activate shared sk-clb auth while the Desktop picker is still missing a
|
|
599
|
+
// ready catalog/selection — that leaves App threads calling api.openai.com.
|
|
600
|
+
const wantForceCodexLbApiKeyAuth = opts.forceCodexLbApiKeyAuth === true || opts.authMode === 'codex-lb';
|
|
601
|
+
const forceCodexLbApiKeyAuth = wantForceCodexLbApiKeyAuth && providerSelected;
|
|
602
|
+
const authReconcile = providerSelected
|
|
603
|
+
? await reconcileCodexLbAuthConflict({ ...opts, home, status: codexLb, forceCodexLbApiKeyAuth }).catch((err) => ({ status: 'failed', reason: 'exception', error: err.message }))
|
|
604
|
+
: { status: 'deferred_until_provider_selected', reason: 'tool_catalog_or_selection_incomplete' };
|
|
605
|
+
const codexLogin = !providerSelected
|
|
606
|
+
? { ok: true, status: 'deferred_until_provider_selected', reason: 'tool_catalog_or_selection_incomplete', error: null }
|
|
607
|
+
: forceCodexLbApiKeyAuth
|
|
608
|
+
? { ok: ['apikey_forced', 'apikey_auth_active'].includes(authReconcile.status), status: authReconcile.status, ...(authReconcile.reason ? { reason: authReconcile.reason } : {}), error: authReconcile.error || null }
|
|
609
|
+
: await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home, force: true });
|
|
559
610
|
const finalCodexLb = await codexLbStatus({ ...opts, home, configPath, envPath });
|
|
560
611
|
const ok = Boolean(codexEnvironment.ok && codexLogin.ok);
|
|
561
612
|
const afterState = await captureCodexLbSetupWriteState({ home, configPath, envPath, shellProfile });
|
|
@@ -636,6 +687,7 @@ export async function codexLbStatus(opts = {}) {
|
|
|
636
687
|
const authPath = opts.authPath || codexAuthPath(home);
|
|
637
688
|
const authText = await readText(authPath, '');
|
|
638
689
|
const authMode = codexAuthModeSummary(authText);
|
|
690
|
+
const authApiKey = parseCodexAuthApiKey(authText);
|
|
639
691
|
const envKeyConfigured = Boolean(envLoad.api_key.present);
|
|
640
692
|
const providerConfigured = /\[model_providers\.codex-lb\]/.test(config);
|
|
641
693
|
const selected = hasTopLevelCodexLbSelected(config);
|
|
@@ -646,6 +698,10 @@ export async function codexLbStatus(opts = {}) {
|
|
|
646
698
|
const providerRequiresOpenAiAuth = codexLbProviderRequiresOpenAiAuth(config);
|
|
647
699
|
const providerOpenAiAuthDisabled = codexLbProviderOpenAiAuthDisabled(config);
|
|
648
700
|
const providerEnvKey = codexLbProviderEnvKey(config);
|
|
701
|
+
const sharedOpenAiRouting = codexLbSharedOpenAiRoutingState(config, baseUrl);
|
|
702
|
+
const codexLbKeyInSharedAuth = Boolean(authApiKey && envLoad.secret_api_key && authApiKey === envLoad.secret_api_key);
|
|
703
|
+
const sharedOpenAiRoutingSafe = !codexLbKeyInSharedAuth || sharedOpenAiRouting.status === 'matched';
|
|
704
|
+
const authRoutingCoherent = selected === codexLbKeyInSharedAuth && sharedOpenAiRoutingSafe;
|
|
649
705
|
const providerContractOk = providerConfigured
|
|
650
706
|
&& providerName === CODEX_LB_PROVIDER_NAME
|
|
651
707
|
&& providerWireApi === 'responses'
|
|
@@ -661,7 +717,7 @@ export async function codexLbStatus(opts = {}) {
|
|
|
661
717
|
status: 'inspect_failed',
|
|
662
718
|
error: err.message
|
|
663
719
|
}));
|
|
664
|
-
const providerReady = providerContractOk && envKeyConfigured && Boolean(baseUrl) && authMode.codex_app_usable;
|
|
720
|
+
const providerReady = providerContractOk && envKeyConfigured && Boolean(baseUrl) && authMode.codex_app_usable && authRoutingCoherent;
|
|
665
721
|
const probeToolOutputRecovery = opts.probeToolOutputRecovery === true;
|
|
666
722
|
const toolOutputRecovery = !selected
|
|
667
723
|
? codexLbToolOutputRecoveryNotSelected()
|
|
@@ -704,8 +760,20 @@ export async function codexLbStatus(opts = {}) {
|
|
|
704
760
|
base_url: baseUrl,
|
|
705
761
|
auth_path: authPath,
|
|
706
762
|
auth_mode: authMode.mode,
|
|
707
|
-
|
|
708
|
-
|
|
763
|
+
codex_lb_key_in_shared_auth: codexLbKeyInSharedAuth,
|
|
764
|
+
auth_routing_coherent: authRoutingCoherent,
|
|
765
|
+
shared_openai_routing: {
|
|
766
|
+
status: sharedOpenAiRouting.status,
|
|
767
|
+
safe: sharedOpenAiRoutingSafe,
|
|
768
|
+
managed: sharedOpenAiRouting.managed,
|
|
769
|
+
configured_base_url: sharedOpenAiRouting.configured_base_url
|
|
770
|
+
},
|
|
771
|
+
auth_usable_for_codex_app: authMode.codex_app_usable && authRoutingCoherent,
|
|
772
|
+
auth_summary: !sharedOpenAiRoutingSafe
|
|
773
|
+
? 'codex-lb key is active in shared auth without a verified built-in OpenAI routing guard'
|
|
774
|
+
: !authRoutingCoherent
|
|
775
|
+
? 'Codex provider selection and shared auth mode do not form a coherent codex-lb/OAuth state'
|
|
776
|
+
: codexAppUsableWithCodexLb ? `codex-lb provider uses ${authMode.mode} OpenAI-style auth through Codex App` : authMode.summary,
|
|
709
777
|
fast_mode: fastMode,
|
|
710
778
|
launch_environment: launchEnvironment,
|
|
711
779
|
tool_output_recovery: toolOutputRecovery
|
|
@@ -721,7 +789,9 @@ export function formatCodexLbStatusText(status = {}, opts = {}) {
|
|
|
721
789
|
`Selected: ${status.selected ? 'yes' : 'no'}`,
|
|
722
790
|
`Provider: ${status.provider_contract_ok ? 'codex-lb App contract ok' : status.provider_configured ? 'drifted' : 'missing'}`,
|
|
723
791
|
`Provider OpenAI Auth: ${status.provider_requires_openai_auth ? 'required' : 'not required/drifted'} (${status.provider_name || 'missing'})`,
|
|
724
|
-
`Codex App auth: ${status.auth_usable_for_codex_app ? 'ok' : 'needs sign-in/repair'} (${status.auth_mode || 'unknown'})
|
|
792
|
+
`Codex App auth: ${status.auth_usable_for_codex_app ? 'ok' : 'needs sign-in/repair'} (${status.auth_mode || 'unknown'})`,
|
|
793
|
+
`Shared OpenAI routing: ${status.shared_openai_routing?.safe === false ? 'unsafe' : status.shared_openai_routing?.status || 'unknown'}${status.shared_openai_routing?.managed ? ' (sks-managed)' : ''}`,
|
|
794
|
+
`Auth/routing coherent: ${status.auth_routing_coherent ? 'yes' : 'no'}`
|
|
725
795
|
];
|
|
726
796
|
if (status.tool_output_recovery?.status && status.tool_output_recovery.status !== 'not_selected') {
|
|
727
797
|
const recovery = status.tool_output_recovery;
|
|
@@ -743,7 +813,9 @@ export function formatCodexLbStatusText(status = {}, opts = {}) {
|
|
|
743
813
|
if (status.base_url)
|
|
744
814
|
lines.push(`Base URL: ${status.base_url}`);
|
|
745
815
|
lines.push(`ChatGPT backup: ${backupPresent ? `yes (${backupPath})` : 'no'}`);
|
|
746
|
-
if (status.
|
|
816
|
+
if (status.shared_openai_routing?.safe === false)
|
|
817
|
+
lines.push('', 'Run: sks codex-lb use-codex-lb to pin built-in OpenAI routing to the codex-lb base URL before the shared key can hit api.openai.com.');
|
|
818
|
+
else if (status.provider_configured && !status.provider_contract_ok)
|
|
747
819
|
lines.push('', 'Run: sks codex-lb repair to rewrite the provider block to the current codex-lb App contract.');
|
|
748
820
|
else if (status.ok && !status.auth_usable_for_codex_app && backupPresent)
|
|
749
821
|
lines.push('', 'Run: sks codex-lb use-oauth to restore ChatGPT OAuth, or sks codex-lb use-codex-lb to force codex-lb API-key auth.');
|
|
@@ -754,7 +826,7 @@ export function formatCodexLbStatusText(status = {}, opts = {}) {
|
|
|
754
826
|
else if (status.ok)
|
|
755
827
|
lines.push('', 'Status: codex-lb active; no repair needed.');
|
|
756
828
|
else if (!status.ok && status.base_url && status.env_key_configured)
|
|
757
|
-
lines.push('', 'Run: sks codex-lb repair to restore
|
|
829
|
+
lines.push('', 'Run: sks codex-lb repair / use-codex-lb to restore a coherent provider + shared-auth routing state.');
|
|
758
830
|
else if (!status.ok)
|
|
759
831
|
lines.push('', 'Run: sks codex-lb setup --host <domain> --api-key-stdin');
|
|
760
832
|
if (backupPresent)
|
|
@@ -878,7 +950,10 @@ export async function repairCodexLbAuth(opts = {}) {
|
|
|
878
950
|
configRepaired = safeWrite.ok && safeWrite.changed === true;
|
|
879
951
|
status = await codexLbStatus({ ...opts, probeToolOutputRecovery: true });
|
|
880
952
|
}
|
|
881
|
-
const canRepairAuthMode = opts.forceCodexLbApiKeyAuth === true
|
|
953
|
+
const canRepairAuthMode = (opts.forceCodexLbApiKeyAuth === true || status.codex_lb_key_in_shared_auth === true)
|
|
954
|
+
&& status.provider_contract_ok
|
|
955
|
+
&& status.env_key_configured
|
|
956
|
+
&& Boolean(status.base_url);
|
|
882
957
|
if (!status.ok && !canRepairAuthMode) {
|
|
883
958
|
return {
|
|
884
959
|
ok: false,
|
|
@@ -1064,11 +1139,11 @@ async function migrateCodexAuthKeyFormat(opts = {}) {
|
|
|
1064
1139
|
return { status: 'skipped', reason: 'parse_error' };
|
|
1065
1140
|
}
|
|
1066
1141
|
}
|
|
1067
|
-
//
|
|
1068
|
-
//
|
|
1069
|
-
//
|
|
1070
|
-
//
|
|
1071
|
-
//
|
|
1142
|
+
// Codex App uses the shared OpenAI auth store for providers with
|
|
1143
|
+
// requires_openai_auth=true. Before putting the codex-lb key in that store, SKS
|
|
1144
|
+
// must also guard the built-in `openai` provider with the same LB base URL. This
|
|
1145
|
+
// covers stale App threads and explicit CLI overrides that retain `openai`; the
|
|
1146
|
+
// config guard is written and verified before auth.json changes.
|
|
1072
1147
|
export async function reconcileCodexLbAuthConflict(opts = {}) {
|
|
1073
1148
|
const home = opts.home || process.env.HOME || os.homedir();
|
|
1074
1149
|
const status = opts.status || await codexLbStatus({ ...opts, home });
|
|
@@ -1086,19 +1161,111 @@ export async function reconcileCodexLbAuthConflict(opts = {}) {
|
|
|
1086
1161
|
return { status: 'skipped', reason: 'missing_env_key', auth_path: authPath };
|
|
1087
1162
|
}
|
|
1088
1163
|
const forceCodexLbApiKeyAuth = opts.forceCodexLbApiKeyAuth === true;
|
|
1164
|
+
const currentApiKey = parseCodexAuthApiKey(authText);
|
|
1165
|
+
let routingGuardAdded = false;
|
|
1166
|
+
const ensureSharedOpenAiRouting = async () => {
|
|
1167
|
+
const configPath = opts.configPath || status.config_path || codexLbConfigPath(home);
|
|
1168
|
+
const currentConfig = await readText(configPath, '');
|
|
1169
|
+
const planned = upsertCodexLbSharedOpenAiRouting(currentConfig, status.base_url);
|
|
1170
|
+
if (!planned.ok) {
|
|
1171
|
+
return {
|
|
1172
|
+
ok: false,
|
|
1173
|
+
status: planned.status,
|
|
1174
|
+
config_path: configPath,
|
|
1175
|
+
configured_base_url: planned.configured_base_url
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
if (planned.status === 'present') {
|
|
1179
|
+
return { ok: true, status: 'present', changed: false, config_path: configPath, managed: planned.managed };
|
|
1180
|
+
}
|
|
1181
|
+
const written = await safeWriteCodexConfigToml(configPath, currentConfig, planned.text, 'codex-lb-shared-auth-routing');
|
|
1182
|
+
if (!written.ok)
|
|
1183
|
+
return { ok: false, status: written.status, config_path: configPath, error: 'shared_openai_routing_write_failed' };
|
|
1184
|
+
const after = await readText(configPath, '');
|
|
1185
|
+
const verified = codexLbSharedOpenAiRoutingState(after, status.base_url);
|
|
1186
|
+
if (verified.status !== 'matched' || !verified.managed) {
|
|
1187
|
+
return { ok: false, status: 'readback_failed', config_path: configPath, error: 'shared_openai_routing_readback_failed' };
|
|
1188
|
+
}
|
|
1189
|
+
routingGuardAdded = true;
|
|
1190
|
+
return { ok: true, status: 'added', changed: true, config_path: configPath, managed: true };
|
|
1191
|
+
};
|
|
1192
|
+
const rollbackSharedOpenAiRouting = async () => {
|
|
1193
|
+
if (!routingGuardAdded)
|
|
1194
|
+
return { ok: true, status: 'not_needed' };
|
|
1195
|
+
const configPath = opts.configPath || status.config_path || codexLbConfigPath(home);
|
|
1196
|
+
const currentConfig = await readText(configPath, '');
|
|
1197
|
+
const removal = removeCodexLbSharedOpenAiRouting(currentConfig, status.base_url);
|
|
1198
|
+
if (!removal.changed)
|
|
1199
|
+
return { ok: false, status: 'not_owned_or_changed' };
|
|
1200
|
+
const written = await safeWriteCodexConfigToml(configPath, currentConfig, ensureTrailingNewline(removal.text), 'codex-lb-shared-auth-routing-rollback');
|
|
1201
|
+
if (!written.ok)
|
|
1202
|
+
return { ok: false, status: written.status };
|
|
1203
|
+
const after = codexLbSharedOpenAiRoutingState(await readText(configPath, ''), status.base_url);
|
|
1204
|
+
return { ok: after.status === 'missing' && !after.managed, status: after.status === 'missing' && !after.managed ? 'rolled_back' : 'readback_failed' };
|
|
1205
|
+
};
|
|
1206
|
+
const rollbackAuth = async () => {
|
|
1207
|
+
try {
|
|
1208
|
+
if (authExists)
|
|
1209
|
+
await writeTextAtomic(authPath, authText, { mode: 0o600 });
|
|
1210
|
+
else
|
|
1211
|
+
await fsp.rm(authPath, { force: true });
|
|
1212
|
+
return { ok: true, status: 'rolled_back' };
|
|
1213
|
+
}
|
|
1214
|
+
catch (err) {
|
|
1215
|
+
return { ok: false, status: 'rollback_failed', error: err.message };
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1089
1218
|
const writeApiKeyAuth = async (reason, backupPathForResult = null) => {
|
|
1219
|
+
const routingGuard = await ensureSharedOpenAiRouting();
|
|
1220
|
+
if (!routingGuard.ok) {
|
|
1221
|
+
return {
|
|
1222
|
+
status: 'failed',
|
|
1223
|
+
reason: routingGuard.status === 'conflicting_user_openai_base_url' ? 'shared_openai_base_url_conflict' : 'shared_openai_routing_guard_failed',
|
|
1224
|
+
auth_path: authPath,
|
|
1225
|
+
backup_path: backupPathForResult || backupPath,
|
|
1226
|
+
routing_guard: routingGuard,
|
|
1227
|
+
error: routingGuard.error || routingGuard.status
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1090
1230
|
try {
|
|
1091
1231
|
await writeTextAtomic(authPath, `${JSON.stringify({ auth_mode: 'apikey', OPENAI_API_KEY: apiKey }, null, 2)}\n`, { mode: 0o600 });
|
|
1092
1232
|
await fsp.chmod(authPath, 0o600).catch(() => { });
|
|
1233
|
+
const finalConfig = await readText(opts.configPath || status.config_path || codexLbConfigPath(home), '');
|
|
1234
|
+
const finalAuth = await readText(authPath, '');
|
|
1235
|
+
const finalRouting = codexLbSharedOpenAiRoutingState(finalConfig, status.base_url);
|
|
1236
|
+
if (finalRouting.status !== 'matched' || parseCodexAuthApiKey(finalAuth) !== apiKey) {
|
|
1237
|
+
const authRollback = await rollbackAuth();
|
|
1238
|
+
const routingRollback = await rollbackSharedOpenAiRouting();
|
|
1239
|
+
return {
|
|
1240
|
+
status: 'failed',
|
|
1241
|
+
reason: 'activation_readback_failed',
|
|
1242
|
+
auth_path: authPath,
|
|
1243
|
+
backup_path: backupPathForResult || backupPath,
|
|
1244
|
+
routing_guard: routingGuard,
|
|
1245
|
+
routing_rollback: routingRollback,
|
|
1246
|
+
error: authRollback.ok && routingRollback.ok ? 'activation_readback_failed_rolled_back' : 'activation_readback_failed_rollback_incomplete'
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1093
1249
|
return {
|
|
1094
1250
|
status: 'apikey_forced',
|
|
1095
1251
|
reason,
|
|
1096
1252
|
auth_path: authPath,
|
|
1097
|
-
backup_path: backupPathForResult || backupPath
|
|
1253
|
+
backup_path: backupPathForResult || backupPath,
|
|
1254
|
+
routing_guard: routingGuard
|
|
1098
1255
|
};
|
|
1099
1256
|
}
|
|
1100
1257
|
catch (err) {
|
|
1101
|
-
|
|
1258
|
+
const authRollback = await rollbackAuth();
|
|
1259
|
+
const routingRollback = await rollbackSharedOpenAiRouting();
|
|
1260
|
+
return {
|
|
1261
|
+
status: 'failed',
|
|
1262
|
+
reason: 'write_failed',
|
|
1263
|
+
auth_path: authPath,
|
|
1264
|
+
backup_path: backupPathForResult || backupPath,
|
|
1265
|
+
routing_guard: routingGuard,
|
|
1266
|
+
routing_rollback: routingRollback,
|
|
1267
|
+
error: authRollback.ok && routingRollback.ok ? err.message : `${err.message}; activation rollback incomplete`
|
|
1268
|
+
};
|
|
1102
1269
|
}
|
|
1103
1270
|
};
|
|
1104
1271
|
if (!authExists) {
|
|
@@ -1138,45 +1305,73 @@ export async function reconcileCodexLbAuthConflict(opts = {}) {
|
|
|
1138
1305
|
backup_path: backupPath
|
|
1139
1306
|
};
|
|
1140
1307
|
}
|
|
1141
|
-
|
|
1142
|
-
const replacement = `${JSON.stringify({ auth_mode: 'apikey', OPENAI_API_KEY: apiKey }, null, 2)}\n`;
|
|
1143
|
-
await writeTextAtomic(authPath, replacement, { mode: 0o600 });
|
|
1144
|
-
await fsp.chmod(authPath, 0o600).catch(() => { });
|
|
1145
|
-
}
|
|
1146
|
-
catch (err) {
|
|
1147
|
-
return { status: 'failed', reason: 'write_failed', auth_path: authPath, backup_path: backupPath, error: err.message };
|
|
1148
|
-
}
|
|
1149
|
-
return {
|
|
1150
|
-
status: 'apikey_forced',
|
|
1151
|
-
reason: 'SKS_CODEX_LB_FORCE_APIKEY_AUTH=1',
|
|
1152
|
-
auth_path: authPath,
|
|
1153
|
-
backup_path: backupPath
|
|
1154
|
-
};
|
|
1308
|
+
return writeApiKeyAuth('SKS_CODEX_LB_FORCE_APIKEY_AUTH=1', backupPath);
|
|
1155
1309
|
}
|
|
1156
|
-
const currentApiKey = parseCodexAuthApiKey(authText);
|
|
1157
1310
|
if (forceCodexLbApiKeyAuth) {
|
|
1158
1311
|
if (currentApiKey && currentApiKey === apiKey) {
|
|
1312
|
+
const routingGuard = await ensureSharedOpenAiRouting();
|
|
1313
|
+
if (!routingGuard.ok) {
|
|
1314
|
+
return {
|
|
1315
|
+
status: 'failed',
|
|
1316
|
+
reason: routingGuard.status === 'conflicting_user_openai_base_url' ? 'shared_openai_base_url_conflict' : 'shared_openai_routing_guard_failed',
|
|
1317
|
+
auth_path: authPath,
|
|
1318
|
+
backup_path: backupPath,
|
|
1319
|
+
routing_guard: routingGuard,
|
|
1320
|
+
error: routingGuard.error || routingGuard.status
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1159
1323
|
return {
|
|
1160
1324
|
status: 'apikey_auth_active',
|
|
1161
1325
|
reason: 'codex_lb_auth_selected',
|
|
1162
1326
|
auth_path: authPath,
|
|
1163
|
-
backup_path: backupPath
|
|
1327
|
+
backup_path: backupPath,
|
|
1328
|
+
routing_guard: routingGuard
|
|
1164
1329
|
};
|
|
1165
1330
|
}
|
|
1166
1331
|
return writeApiKeyAuth('codex_lb_auth_selected_replace_existing');
|
|
1167
1332
|
}
|
|
1168
1333
|
if (currentApiKey && currentApiKey === apiKey) {
|
|
1334
|
+
const routingGuard = await ensureSharedOpenAiRouting();
|
|
1335
|
+
if (!routingGuard.ok) {
|
|
1336
|
+
return {
|
|
1337
|
+
status: 'failed',
|
|
1338
|
+
reason: routingGuard.status === 'conflicting_user_openai_base_url' ? 'shared_openai_base_url_conflict' : 'shared_openai_routing_guard_failed',
|
|
1339
|
+
auth_path: authPath,
|
|
1340
|
+
backup_path: backupPath,
|
|
1341
|
+
routing_guard: routingGuard,
|
|
1342
|
+
error: routingGuard.error || routingGuard.status
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1169
1345
|
const backupText = await readText(backupPath, '');
|
|
1170
1346
|
if (hasChatgptOAuthTokens(backupText) && process.env.SKS_CODEX_LB_KEEP_APIKEY_AUTH !== '1') {
|
|
1171
1347
|
try {
|
|
1172
1348
|
const restored = backupText.endsWith('\n') ? backupText : `${backupText}\n`;
|
|
1173
1349
|
await writeTextAtomic(authPath, restored, { mode: 0o600 });
|
|
1174
1350
|
await fsp.chmod(authPath, 0o600).catch(() => { });
|
|
1351
|
+
const unselected = await unselectCodexLbProvider({
|
|
1352
|
+
...opts,
|
|
1353
|
+
home,
|
|
1354
|
+
authPath,
|
|
1355
|
+
allowActiveSharedAuthTransition: true
|
|
1356
|
+
});
|
|
1357
|
+
if (!unselected.ok) {
|
|
1358
|
+
await writeTextAtomic(authPath, authText, { mode: 0o600 });
|
|
1359
|
+
await fsp.chmod(authPath, 0o600).catch(() => { });
|
|
1360
|
+
return {
|
|
1361
|
+
status: 'failed',
|
|
1362
|
+
reason: 'oauth_restore_provider_unselect_failed',
|
|
1363
|
+
auth_path: authPath,
|
|
1364
|
+
backup_path: backupPath,
|
|
1365
|
+
routing_guard: routingGuard,
|
|
1366
|
+
error: unselected.provider_error || unselected.reason || unselected.status
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1175
1369
|
return {
|
|
1176
1370
|
status: 'oauth_restored',
|
|
1177
1371
|
reason: 'restored_chatgpt_oauth_for_codex_app',
|
|
1178
1372
|
auth_path: authPath,
|
|
1179
|
-
backup_path: backupPath
|
|
1373
|
+
backup_path: backupPath,
|
|
1374
|
+
routing_guard: routingGuard
|
|
1180
1375
|
};
|
|
1181
1376
|
}
|
|
1182
1377
|
catch (err) {
|
|
@@ -1187,7 +1382,8 @@ export async function reconcileCodexLbAuthConflict(opts = {}) {
|
|
|
1187
1382
|
status: 'apikey_auth_active',
|
|
1188
1383
|
reason: hasChatgptOAuthTokens(backupText) ? 'SKS_CODEX_LB_KEEP_APIKEY_AUTH=1' : 'chatgpt_oauth_backup_missing',
|
|
1189
1384
|
auth_path: authPath,
|
|
1190
|
-
backup_path: backupPath
|
|
1385
|
+
backup_path: backupPath,
|
|
1386
|
+
routing_guard: routingGuard
|
|
1191
1387
|
};
|
|
1192
1388
|
}
|
|
1193
1389
|
return { status: 'no_oauth_conflict', auth_path: authPath };
|
|
@@ -1224,20 +1420,41 @@ export async function unselectCodexLbProvider(opts = {}) {
|
|
|
1224
1420
|
const current = await readText(configPath, '');
|
|
1225
1421
|
if (!current.trim())
|
|
1226
1422
|
return { ok: true, status: 'not_selected', reason: 'no_config', config_path: configPath };
|
|
1423
|
+
const envPath = opts.envPath || codexLbEnvPath(home);
|
|
1424
|
+
const envLoad = await loadCodexLbEnv({ ...opts, home, envPath });
|
|
1425
|
+
const authPath = opts.authPath || codexAuthPath(home);
|
|
1426
|
+
const authApiKey = parseCodexAuthApiKey(await readText(authPath, ''));
|
|
1427
|
+
const sharedCodexLbAuthActive = Boolean(authApiKey && envLoad.secret_api_key && authApiKey === envLoad.secret_api_key);
|
|
1428
|
+
if (sharedCodexLbAuthActive && opts.allowActiveSharedAuthTransition !== true) {
|
|
1429
|
+
return {
|
|
1430
|
+
ok: false,
|
|
1431
|
+
status: 'failed',
|
|
1432
|
+
reason: 'shared_codex_lb_auth_active',
|
|
1433
|
+
provider_error: 'refusing to unselect codex-lb while its key is active in shared OpenAI auth',
|
|
1434
|
+
config_path: configPath
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1227
1437
|
const managedCatalogPath = codexLbToolCatalogPath(opts.codexHome || path.join(home, '.codex'));
|
|
1228
1438
|
const managedCatalogSelected = topLevelTomlString(current, 'model_catalog_json') === managedCatalogPath;
|
|
1229
|
-
|
|
1439
|
+
const providerBaseUrl = codexLbProviderBaseUrl(current) || envLoad.base_url || '';
|
|
1440
|
+
const sharedOpenAiRouting = codexLbSharedOpenAiRoutingState(current, providerBaseUrl);
|
|
1441
|
+
const managedSharedOpenAiRouting = sharedOpenAiRouting.status === 'matched' && sharedOpenAiRouting.managed;
|
|
1442
|
+
if (!hasTopLevelCodexLbSelected(current) && !managedCatalogSelected && !managedSharedOpenAiRouting)
|
|
1230
1443
|
return { ok: true, status: 'not_selected', config_path: configPath };
|
|
1231
1444
|
try {
|
|
1232
1445
|
let next = removeTopLevelTomlString(current, 'model_provider');
|
|
1233
1446
|
next = removeTopLevelTomlKeyIfValue(next, 'model_catalog_json', managedCatalogPath);
|
|
1447
|
+
const routingRemoval = removeCodexLbSharedOpenAiRouting(next, providerBaseUrl);
|
|
1448
|
+
next = routingRemoval.text;
|
|
1234
1449
|
next = ensureTrailingNewline(next);
|
|
1235
1450
|
const safeWrite = await safeWriteCodexConfigToml(configPath, current, next, 'codex-lb-unselect');
|
|
1236
1451
|
const after = safeWrite.ok ? await readText(configPath, '') : current;
|
|
1452
|
+
const afterRouting = codexLbSharedOpenAiRoutingState(after, providerBaseUrl);
|
|
1237
1453
|
const selectionRemoved = !hasTopLevelCodexLbSelected(after)
|
|
1238
|
-
&& topLevelTomlString(after, 'model_catalog_json') !== managedCatalogPath
|
|
1454
|
+
&& topLevelTomlString(after, 'model_catalog_json') !== managedCatalogPath
|
|
1455
|
+
&& (!managedSharedOpenAiRouting || (afterRouting.status === 'missing' && !afterRouting.managed));
|
|
1239
1456
|
if (safeWrite.ok && selectionRemoved)
|
|
1240
|
-
return { ok: true, status: 'unselected', config_path: configPath, backup_path: safeWrite.backup_path };
|
|
1457
|
+
return { ok: true, status: 'unselected', config_path: configPath, backup_path: safeWrite.backup_path, routing_guard_removed: routingRemoval.changed };
|
|
1241
1458
|
const providerError = safeWrite.ok ? 'provider_selection_remains_after_write' : safeWrite.status || 'provider_config_write_blocked';
|
|
1242
1459
|
return {
|
|
1243
1460
|
ok: false,
|
|
@@ -1282,6 +1499,21 @@ export async function releaseCodexLbAuthHold(opts = {}) {
|
|
|
1282
1499
|
const authExisted = await exists(authPath);
|
|
1283
1500
|
const currentAuthText = await readText(authPath, '');
|
|
1284
1501
|
const trimmedCurrent = currentAuthText.trim();
|
|
1502
|
+
if (opts.keepProvider) {
|
|
1503
|
+
const currentConfig = await readText(configPath, '');
|
|
1504
|
+
const providerBaseUrl = codexLbProviderBaseUrl(currentConfig);
|
|
1505
|
+
const routing = codexLbSharedOpenAiRoutingState(currentConfig, providerBaseUrl);
|
|
1506
|
+
if (hasTopLevelCodexLbSelected(currentConfig) || (routing.status === 'matched' && routing.managed)) {
|
|
1507
|
+
return {
|
|
1508
|
+
ok: false,
|
|
1509
|
+
status: 'failed',
|
|
1510
|
+
reason: 'keep_provider_unsafe_with_shared_auth',
|
|
1511
|
+
auth_path: authPath,
|
|
1512
|
+
backup_path: backupPath,
|
|
1513
|
+
provider_unselected: false
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1285
1517
|
// Repeated "Use ChatGPT OAuth" is idempotent. If OAuth/browser auth is
|
|
1286
1518
|
// already active, a historical backup is unnecessary; only ensure that the
|
|
1287
1519
|
// codex-lb provider is no longer selected.
|
|
@@ -1289,7 +1521,13 @@ export async function releaseCodexLbAuthHold(opts = {}) {
|
|
|
1289
1521
|
if (!opts.force && (currentAuthMode.mode === 'chatgpt_oauth' || currentAuthMode.mode === 'browser_marker')) {
|
|
1290
1522
|
let provider = { ok: true, provider_unselected: false, provider_status: 'kept', provider_error: null };
|
|
1291
1523
|
if (!opts.keepProvider) {
|
|
1292
|
-
const unselected = await unselectCodexLbProvider({
|
|
1524
|
+
const unselected = await unselectCodexLbProvider({
|
|
1525
|
+
...opts,
|
|
1526
|
+
home,
|
|
1527
|
+
configPath,
|
|
1528
|
+
authPath,
|
|
1529
|
+
allowActiveSharedAuthTransition: true
|
|
1530
|
+
});
|
|
1293
1531
|
provider = providerDeselectionOutcome(unselected);
|
|
1294
1532
|
}
|
|
1295
1533
|
return {
|
|
@@ -1327,7 +1565,13 @@ export async function releaseCodexLbAuthHold(opts = {}) {
|
|
|
1327
1565
|
if (trimmedCurrent && hasChatgptOAuthTokens(currentAuthText) && !opts.force) {
|
|
1328
1566
|
let provider = { ok: true, provider_unselected: false, provider_status: 'kept', provider_error: null };
|
|
1329
1567
|
if (!opts.keepProvider) {
|
|
1330
|
-
const unselected = await unselectCodexLbProvider({
|
|
1568
|
+
const unselected = await unselectCodexLbProvider({
|
|
1569
|
+
...opts,
|
|
1570
|
+
home,
|
|
1571
|
+
configPath,
|
|
1572
|
+
authPath,
|
|
1573
|
+
allowActiveSharedAuthTransition: true
|
|
1574
|
+
});
|
|
1331
1575
|
provider = providerDeselectionOutcome(unselected);
|
|
1332
1576
|
}
|
|
1333
1577
|
return {
|
|
@@ -1374,7 +1618,15 @@ export async function releaseCodexLbAuthHold(opts = {}) {
|
|
|
1374
1618
|
}
|
|
1375
1619
|
let provider = { ok: true, provider_unselected: false, provider_status: 'kept', provider_error: null };
|
|
1376
1620
|
if (!opts.keepProvider) {
|
|
1377
|
-
|
|
1621
|
+
// Auth was just restored to ChatGPT OAuth. Allow deselection even if a stale
|
|
1622
|
+
// shared-key read would otherwise refuse, so release cannot leave OAuth + LB pin.
|
|
1623
|
+
const unselected = await unselectCodexLbProvider({
|
|
1624
|
+
...opts,
|
|
1625
|
+
home,
|
|
1626
|
+
configPath,
|
|
1627
|
+
authPath,
|
|
1628
|
+
allowActiveSharedAuthTransition: true
|
|
1629
|
+
});
|
|
1378
1630
|
provider = providerDeselectionOutcome(unselected);
|
|
1379
1631
|
}
|
|
1380
1632
|
if (!provider.ok) {
|