sneakoscope 6.0.2 → 6.1.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 +5 -3
- package/config/codex-releases/{rust-v0.142.0.json → rust-v0.144.1.json} +6 -6
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/cli/install-helpers.js +255 -440
- package/dist/cli/router.js +53 -4
- package/dist/commands/codex-app.js +8 -3
- package/dist/commands/codex-lb.js +169 -40
- package/dist/commands/codex.js +6 -5
- package/dist/commands/doctor.js +60 -26
- package/dist/commands/versioning.js +7 -0
- package/dist/config/skills-manifest.json +57 -57
- package/dist/core/agents/agent-central-ledger.js +9 -0
- package/dist/core/agents/agent-cleanup-executor.js +334 -41
- package/dist/core/agents/agent-cleanup.js +20 -1
- package/dist/core/agents/agent-command-surface.js +2 -1
- package/dist/core/agents/agent-effort-policy.js +46 -33
- package/dist/core/agents/agent-janitor.js +9 -10
- package/dist/core/agents/agent-namespace-safety.js +56 -0
- package/dist/core/agents/agent-orchestrator.js +26 -4
- package/dist/core/agents/agent-output-validator.js +1 -1
- package/dist/core/agents/agent-proof-evidence.js +16 -2
- package/dist/core/agents/agent-role-config.js +3 -6
- package/dist/core/agents/agent-roster.js +2 -2
- package/dist/core/agents/agent-runner-codex-exec.js +2 -0
- package/dist/core/agents/agent-runner-process.js +1 -0
- package/dist/core/agents/fast-mode-policy.js +3 -5
- package/dist/core/agents/native-cli-session-swarm.js +18 -1
- package/dist/core/agents/native-worker-backend-router.js +180 -108
- package/dist/core/agents/parallel-write-fixture.js +15 -10
- package/dist/core/agents/runtime-proof-summary.js +21 -3
- package/dist/core/agents/work-partition/repo-inventory.js +68 -7
- package/dist/core/code-structure.js +5 -4
- package/dist/core/codex/codex-config-guard.js +31 -9
- package/dist/core/codex-app/codex-app-fast-ui-repair.js +51 -20
- package/dist/core/codex-app/codex-app-restart.js +38 -8
- package/dist/core/codex-app/codex-app-ui-state-snapshot.js +11 -3
- package/dist/core/codex-app/sks-menubar.js +100 -40
- package/dist/core/codex-app.js +60 -17
- package/dist/core/codex-compat/codex-hook-warning-detector.js +10 -1
- package/dist/core/codex-compat/codex-release-manifest.js +6 -6
- package/dist/core/codex-control/codex-0139-doctor-real-probe.js +7 -1
- package/dist/core/codex-control/codex-0139-image-path-real-probe.js +14 -8
- package/dist/core/codex-control/codex-0139-probe-runner.js +103 -28
- package/dist/core/codex-control/codex-0139-real-probe-summary.js +7 -3
- package/dist/core/codex-control/codex-0139-real-probes.js +21 -6
- package/dist/core/codex-control/codex-0139-sandbox-real-probe.js +39 -13
- package/dist/core/codex-control/codex-0139-web-search-probe.js +11 -8
- package/dist/core/codex-control/{codex-0142-capability.js → codex-0144-capability.js} +22 -22
- package/dist/core/codex-control/codex-0144-collab-agent-real-probe.js +63 -0
- package/dist/core/codex-control/codex-model-metadata.js +8 -4
- package/dist/core/codex-control/codex-multi-agent-event-normalizer.js +5 -1
- package/dist/core/codex-control/codex-reliability-shield.js +4 -1
- package/dist/core/codex-control/codex-sdk-adapter.js +53 -6
- package/dist/core/codex-control/codex-sdk-config-policy.js +2 -1
- package/dist/core/codex-control/codex-sdk-sandbox-policy.js +6 -3
- package/dist/core/codex-lb/codex-lb-env.js +107 -0
- package/dist/core/codex-lb/codex-lb-tool-catalog.js +478 -0
- package/dist/core/codex-native/codex-native-feature-broker.js +22 -22
- package/dist/core/codex-native/codex-native-feature-matrix.js +2 -2
- package/dist/core/codex-native/native-capability-postcheck.js +16 -8
- package/dist/core/codex-native/native-capability-repair-matrix.js +76 -27
- package/dist/core/codex-plugins/codex-plugin-json.js +67 -21
- package/dist/core/codex-plugins/codex-plugin-repair.js +118 -0
- package/dist/core/codex-runtime/codex-desktop-config-policy.js +306 -0
- package/dist/core/commands/agent-command.js +19 -0
- package/dist/core/commands/basic-cli.js +45 -24
- package/dist/core/commands/gc-command.js +27 -4
- package/dist/core/commands/image-ux-review-command.js +55 -24
- package/dist/core/commands/mad-sks-command.js +152 -132
- package/dist/core/commands/naruto-command.js +41 -10
- package/dist/core/commands/pipeline-command.js +1 -0
- package/dist/core/commands/wiki-command.js +179 -83
- package/dist/core/computer-use-live-evidence.js +166 -3
- package/dist/core/computer-use-status.js +5 -2
- package/dist/core/daemon/sksd-hook-daemon-entrypoint.js +2 -2
- package/dist/core/daemon/sksd-hook-daemon.js +171 -28
- package/dist/core/daemon/sksd-hook-dispatch.js +2 -2
- package/dist/core/dfix/verification-selector.js +6 -4
- package/dist/core/doctor/browser-use-repair.js +54 -13
- package/dist/core/doctor/codex-0139-doctor.js +3 -2
- package/dist/core/doctor/computer-use-repair.js +28 -39
- package/dist/core/doctor/doctor-codex-startup-repair.js +19 -9
- package/dist/core/doctor/doctor-context7-repair.js +1 -1
- package/dist/core/doctor/doctor-dirty-planner.js +1 -1
- package/dist/core/doctor/doctor-native-capability-repair.js +74 -3
- package/dist/core/doctor/imagegen-repair.js +47 -12
- package/dist/core/doctor/legacy-global-hook-cleanup.js +179 -0
- package/dist/core/feature-registry.js +25 -31
- package/dist/core/fsx.js +54 -6
- package/dist/core/harness-conflicts.js +1 -1
- package/dist/core/hooks-runtime/code-pack-freshness-preflight.js +5 -4
- package/dist/core/hooks-runtime/hook-invocation-dedupe.js +147 -0
- package/dist/core/hooks-runtime/hook-io.js +6 -0
- package/dist/core/hooks-runtime.js +17 -2
- package/dist/core/image/image-artifact-path-contract.js +8 -6
- package/dist/core/image/image-artifact-registry.js +5 -1
- package/dist/core/image-ux-review/imagegen-adapter.js +5 -2
- package/dist/core/imagegen/imagegen-capability.js +10 -13
- package/dist/core/imagegen/require-imagegen.js +37 -11
- package/dist/core/managed-assets/managed-assets-manifest.js +1 -5
- package/dist/core/naruto/naruto-work-graph.js +70 -4
- package/dist/core/naruto/naruto-write-e2e.js +3 -4
- package/dist/core/ops/reporting.js +3 -0
- package/dist/core/pipeline-internals/runtime-core.js +12 -7
- package/dist/core/pipeline-internals/runtime-gates.js +17 -1
- package/dist/core/ppt/style-tokens.js +1 -1
- package/dist/core/ppt-review/index.js +1 -1
- package/dist/core/ppt-review/slide-exporter.js +23 -17
- package/dist/core/ppt-review/slide-imagegen-review.js +1 -1
- package/dist/core/ppt-review/slide-issue-extraction.js +4 -2
- package/dist/core/product-design-plugin.js +2 -2
- package/dist/core/proof/route-finalizer.js +1 -1
- package/dist/core/proof-field.js +3 -3
- package/dist/core/provider/model-router.js +68 -3
- package/dist/core/release/gate-manifest.js +18 -12
- package/dist/core/release/gate-pack-manifest.js +1 -1
- package/dist/core/release/package-size-budget.js +2 -0
- package/dist/core/release/release-gate-affected-selector.js +1 -1
- package/dist/core/release/release-gate-dag.js +43 -3
- package/dist/core/release/release-gate-hermetic-env.js +28 -17
- package/dist/core/release/release-real-contract.js +416 -0
- package/dist/core/retention/retention-budget.js +25 -6
- package/dist/core/retention.js +980 -101
- package/dist/core/routes/constants.js +1 -1
- package/dist/core/routes/evidence.js +4 -4
- package/dist/core/routes/model-mode-router.js +3 -0
- package/dist/core/routes/ppt-policy.js +1 -1
- package/dist/core/routes.js +11 -12
- package/dist/core/secret-redaction.js +4 -0
- package/dist/core/triwiki/code-pack.js +8 -2
- package/dist/core/triwiki/triwiki-cache-key.js +108 -29
- package/dist/core/triwiki-attention.js +13 -6
- package/dist/core/triwiki-provenance.js +274 -0
- package/dist/core/triwiki-runtime.js +21 -7
- package/dist/core/triwiki-wrongness/wrongness-ledger.js +178 -59
- package/dist/core/triwiki-wrongness/wrongness-retrieval.js +11 -2
- package/dist/core/triwiki-wrongness/wrongness-trust-policy.js +2 -0
- package/dist/core/update/update-migration-state.js +1 -1
- package/dist/core/update/update-notice.js +23 -0
- package/dist/core/update-check.js +69 -25
- package/dist/core/version.js +1 -1
- package/dist/core/wiki-coordinate.js +128 -12
- package/dist/core/work-order-ledger.js +168 -9
- package/dist/core/zellij/zellij-command.js +12 -1
- package/dist/core/zellij/zellij-dashboard-pane.js +3 -3
- package/dist/core/zellij/zellij-launcher.js +15 -14
- package/dist/core/zellij/zellij-pane-proof.js +32 -6
- package/dist/core/zellij/zellij-worker-pane-manager.js +5 -3
- package/dist/scripts/agent-fast-mode-default-check.js +48 -29
- package/dist/scripts/agent-role-config-repair-check.js +25 -4
- package/dist/scripts/agent-wiki-context-proof-check.js +16 -2
- package/dist/scripts/all-feature-deep-completion-check.js +14 -4
- package/dist/scripts/build-dist.js +17 -5
- package/dist/scripts/canonical-test-runner.js +188 -0
- package/dist/scripts/check-command-module-budget.js +38 -16
- package/dist/scripts/check-legacy-free.js +1 -1
- package/dist/scripts/codex-0139-interrupt-agent-check.js +5 -1
- package/dist/scripts/{codex-0142-app-server-v2-check.js → codex-0144-app-server-v2-check.js} +11 -10
- package/dist/scripts/{codex-0142-binary-identity-check.js → codex-0144-binary-identity-check.js} +4 -4
- package/dist/scripts/codex-0144-capability-check.js +21 -0
- package/dist/scripts/{codex-0139-real-probes-check.js → codex-0144-core-real-probes-check.js} +19 -5
- package/dist/scripts/{codex-0142-doctor-wiring-check.js → codex-0144-doctor-wiring-check.js} +6 -6
- package/dist/scripts/{codex-0142-manifest-check.js → codex-0144-manifest-check.js} +11 -3
- package/dist/scripts/{codex-0142-policy-check.js → codex-0144-policy-check.js} +2 -2
- package/dist/scripts/{codex-0142-thread-store-check.js → codex-0144-thread-store-check.js} +3 -3
- package/dist/scripts/codex-app-provider-model-ui-check.js +57 -8
- package/dist/scripts/codex-app-ui-preservation-check.js +35 -4
- package/dist/scripts/codex-control-side-effect-scope-check.js +2 -2
- package/dist/scripts/codex-effort-auto-discovery-check.js +1 -1
- package/dist/scripts/codex-lb-fast-mode-truth-check.js +1 -0
- package/dist/scripts/{codex-lb-gpt55-fast-profile-check.js → codex-lb-gpt56-fast-profile-check.js} +56 -5
- package/dist/scripts/codex-model-metadata-check.js +1 -1
- package/dist/scripts/codex-sdk-sandbox-policy-check.js +5 -1
- package/dist/scripts/codex-sdk-version-compat-check.js +2 -1
- package/dist/scripts/dfix-fixture-check.js +1 -1
- package/dist/scripts/docs-truthfulness-check.js +1 -1
- package/dist/scripts/doctor-codex-startup-repair-check.js +35 -10
- package/dist/scripts/doctor-context7-repair-check.js +2 -1
- package/dist/scripts/doctor-fix-proves-codex-read-check.js +1 -1
- package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +34 -7
- package/dist/scripts/doctor-imagegen-repair-check.js +14 -4
- package/dist/scripts/doctor-startup-config-repair-blackbox.js +1 -1
- package/dist/scripts/doctor-startup-config-repair-check.js +1 -1
- package/dist/scripts/gate-policy-audit-check.js +2 -2
- package/dist/scripts/install-update-preserves-config-check.js +1 -1
- package/dist/scripts/legacy-gate-inventory-check.js +7 -5
- package/dist/scripts/legacy-strong-inventory-check.js +7 -5
- package/dist/scripts/legacy-upgrade-matrix-check.js +5 -5
- package/dist/scripts/lib/native-cli-session-swarm-check-lib.js +175 -14
- package/dist/scripts/mad-sks-app-ui-no-mutation-check.js +166 -6
- package/dist/scripts/mutation-callsite-coverage-check.js +0 -11
- package/dist/scripts/naruto-shadow-clone-swarm-check.js +88 -50
- package/dist/scripts/npm-publish-performance-check.js +2 -1
- package/dist/scripts/package-published-contract-check.js +9 -0
- package/dist/scripts/packlist-performance-check.js +5 -2
- package/dist/scripts/postinstall-safe-side-effects-check.js +8 -3
- package/dist/scripts/ppt-no-mock-as-real-check.js +3 -1
- package/dist/scripts/product-design-plugin-routing-check.js +1 -1
- package/dist/scripts/release-check-stamp.js +182 -2
- package/dist/scripts/release-dag-full-coverage-check.js +7 -7
- package/dist/scripts/release-dynamic-presets-check.js +8 -0
- package/dist/scripts/release-gate-dag-runner-check.js +3 -2
- package/dist/scripts/release-gate-dag-runner.js +24 -11
- package/dist/scripts/release-gate-existence-audit.js +8 -9
- package/dist/scripts/release-gate-script-parity-check.js +1 -1
- package/dist/scripts/release-real-check.js +368 -157
- package/dist/scripts/release-runtime-truth-matrix-check.js +22 -0
- package/dist/scripts/route-proof-artifact-structure-check.js +1 -0
- package/dist/scripts/scheduler-batch-dispatch-check.js +1 -1
- package/dist/scripts/search-visibility-gate-lib.js +1 -1
- package/dist/scripts/seo-geo-route-identity-check.js +30 -4
- package/dist/scripts/sizecheck.js +7 -2
- package/dist/scripts/sks-1-11-gate-lib.js +53 -6
- package/dist/scripts/sks-menubar-install-check.js +7 -0
- package/dist/scripts/sks-uninstall-regression-check.js +1 -1
- package/dist/scripts/sksd-daemon-check.js +16 -6
- package/dist/scripts/update-default-command-check.js +4 -2
- package/dist/scripts/write-build-manifest.js +2 -1
- package/dist/scripts/zellij-layout-valid-check.js +31 -6
- package/dist/scripts/zellij-pane-proof-check.js +33 -2
- package/dist/scripts/zellij-real-session-cleanup-check.js +122 -2
- package/dist/scripts/zellij-real-session-launch-check.js +76 -5
- package/dist/scripts/zellij-slot-pane-renderer-check.js +4 -4
- package/package.json +27 -30
- package/schemas/codex/{app-server-0.142 → app-server-0.144}/codex_app_server_protocol.v2.schemas.json +1146 -781
- package/schemas/codex/codex-0139-real-probe-result.schema.json +39 -5
- package/schemas/codex/ppt-slide-issue-ledger.schema.json +2 -1
- package/dist/core/codex-control/codex-0139-multi-agent-real-probe.js +0 -107
- package/dist/scripts/agent-native-release-gate.js +0 -274
- package/dist/scripts/codex-0142-capability-check.js +0 -21
- package/dist/scripts/command-performance-scorecard-check.js +0 -204
- package/dist/scripts/config-managed-merge-callsite-coverage-check.js +0 -212
- package/dist/scripts/dollar-performance-scorecard-check.js +0 -257
- package/dist/scripts/gate-timing-check.js +0 -16
- package/dist/scripts/ops-maturity-scorecard-check.js +0 -12
- package/dist/scripts/performance-baseline-check.js +0 -12
- package/dist/scripts/performance-improvement-report-check.js +0 -58
- package/dist/scripts/release-parallel-check.js +0 -389
- package/dist/scripts/release-runner-efficiency-check.js +0 -15
- package/dist/scripts/retention-long-run-smoke-check.js +0 -299
- package/docs/assets/sneakoscope-architecture-pipeline.jpg +0 -0
|
@@ -16,25 +16,18 @@ import { restartCodexApp } from '../core/codex-app/codex-app-restart.js';
|
|
|
16
16
|
import { cleanupMacLaunchSecretEnvironment } from '../core/codex-app/sks-menubar.js';
|
|
17
17
|
import { recordCodexLbHealthEvent } from '../core/codex-lb-circuit.js';
|
|
18
18
|
import { loadCodexLbEnv, writeCodexLbKeychain, codexLbMetadataPath } from '../core/codex-lb/codex-lb-env.js';
|
|
19
|
+
import { codexLbToolCatalogPath, ensureCodexLbToolCatalog } from '../core/codex-lb/codex-lb-tool-catalog.js';
|
|
19
20
|
import { GLM_CODEX_CONFIG_PROFILE_ID, GLM_CODEX_CONFIG_PROVIDER_ID, GLM_CODEX_CONFIG_REASONING_PROFILES } from '../core/providers/glm/glm-52-profile.js';
|
|
20
21
|
import { GLM_52_OPENROUTER_MODEL } from '../core/providers/glm/glm-52-settings.js';
|
|
21
22
|
import { buildCodexLbSetupPlan, codexLbPersistenceSummary, installCodexLbShellProfileSnippet, selectedCodexLbPersistenceModes } from '../core/codex-lb/codex-lb-setup.js';
|
|
22
23
|
import { extractTomlTable, writeCodexConfigGuarded } from '../core/codex/codex-config-guard.js';
|
|
23
|
-
import {
|
|
24
|
+
import { ensureGlobalCodexFastModeDuringInstall, ensureTrailingNewline, normalizeCodexFastModeUiConfig, removeTopLevelTomlKeyIfValue, safeWriteCodexConfigToml, upsertTopLevelTomlString, upsertTomlTable } from '../core/codex-runtime/codex-desktop-config-policy.js';
|
|
24
25
|
import { runPostinstallGlobalDoctorAndMarkPending } from '../core/update/update-migration-state.js';
|
|
25
26
|
import { repairCodexImagegen } from '../core/doctor/imagegen-repair.js';
|
|
27
|
+
export { codexFastModeDesktopStatus, ensureGlobalCodexFastModeDuringInstall, normalizeCodexFastModeUiConfig, safeWriteCodexConfigToml } from '../core/codex-runtime/codex-desktop-config-policy.js';
|
|
26
28
|
const CODEX_LB_PROVIDER_NAME = 'openai';
|
|
27
29
|
const CODEX_LB_PROVIDER_ENV_KEY = 'CODEX_LB_API_KEY';
|
|
28
30
|
const CODEX_LB_CANONICAL_FAST_SERVICE_TIER = 'priority';
|
|
29
|
-
const DEFAULT_CODEX_APP_PLUGINS = [
|
|
30
|
-
['browser', 'openai-bundled'],
|
|
31
|
-
['chrome', 'openai-bundled'],
|
|
32
|
-
['computer-use', 'openai-bundled'],
|
|
33
|
-
['latex', 'openai-bundled'],
|
|
34
|
-
['documents', 'openai-primary-runtime'],
|
|
35
|
-
['presentations', 'openai-primary-runtime'],
|
|
36
|
-
['spreadsheets', 'openai-primary-runtime']
|
|
37
|
-
];
|
|
38
31
|
function packagedSksEntrypoint() {
|
|
39
32
|
return path.join(packageRoot(), 'dist', 'bin', 'sks.js');
|
|
40
33
|
}
|
|
@@ -112,7 +105,7 @@ export async function postinstall({ bootstrap, args = [] }) {
|
|
|
112
105
|
if (imagegenRepair.status === 'ready')
|
|
113
106
|
console.log('Codex App Image Gen: ready ($imagegen/gpt-image-2 detected).');
|
|
114
107
|
else if (imagegenRepair.status === 'recovered')
|
|
115
|
-
console.log('Codex App Image Gen: recovered and re-detected.');
|
|
108
|
+
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.');
|
|
116
109
|
else if (imagegenRepair.status === 'blocked')
|
|
117
110
|
console.log(`Codex App Image Gen: blocked; run \`sks doctor --fix\`. ${(imagegenRepair.blockers || []).join(', ')}`.trim());
|
|
118
111
|
else if (imagegenRepair.status === 'skipped')
|
|
@@ -395,7 +388,7 @@ async function restorePostinstallCodexLbConfigSnapshot(snapshot) {
|
|
|
395
388
|
const currentAuthText = currentAuthExists ? await readText(snapshot.auth_path, '') : '';
|
|
396
389
|
if (!currentAuthExists || !currentAuthText.trim()) {
|
|
397
390
|
await ensureDir(path.dirname(snapshot.auth_path));
|
|
398
|
-
await writeTextAtomic(snapshot.auth_path, snapshot.auth_text);
|
|
391
|
+
await writeTextAtomic(snapshot.auth_path, snapshot.auth_text, { mode: 0o600 });
|
|
399
392
|
await fsp.chmod(snapshot.auth_path, 0o600).catch(() => { });
|
|
400
393
|
authRestored = true;
|
|
401
394
|
}
|
|
@@ -417,6 +410,73 @@ export function normalizeCodexLbBaseUrl(input = '') {
|
|
|
417
410
|
host = host.replace(/\/+$/, '');
|
|
418
411
|
return /\/backend-api\/codex$/i.test(host) ? host : `${host}/backend-api/codex`;
|
|
419
412
|
}
|
|
413
|
+
async function ensureCodexLbToolCatalogSelection(input, opts = {}) {
|
|
414
|
+
const codexHome = opts.codexHome || path.join(input.home, '.codex');
|
|
415
|
+
const catalogPath = codexLbToolCatalogPath(codexHome);
|
|
416
|
+
const current = await readText(input.configPath, '');
|
|
417
|
+
const selected = hasTopLevelCodexLbSelected(current);
|
|
418
|
+
const existingCatalogPath = topLevelTomlString(current, 'model_catalog_json');
|
|
419
|
+
if (selected && existingCatalogPath && path.resolve(existingCatalogPath) !== path.resolve(catalogPath)) {
|
|
420
|
+
return {
|
|
421
|
+
schema: 'sks.codex-lb-tool-catalog-selection.v1',
|
|
422
|
+
ok: false,
|
|
423
|
+
required: true,
|
|
424
|
+
status: 'user_catalog_conflict',
|
|
425
|
+
path: catalogPath,
|
|
426
|
+
configured_path: existingCatalogPath,
|
|
427
|
+
config_changed: false,
|
|
428
|
+
blockers: ['codex_lb_user_model_catalog_conflict']
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
let hostname = '';
|
|
432
|
+
try {
|
|
433
|
+
hostname = new URL(input.baseUrl).hostname.toLowerCase();
|
|
434
|
+
}
|
|
435
|
+
catch { }
|
|
436
|
+
const reservedFixtureHost = /(?:^|\.)(?:test|invalid|example)$/.test(hostname);
|
|
437
|
+
if (reservedFixtureHost && typeof opts.toolCatalogFetch !== 'function') {
|
|
438
|
+
return {
|
|
439
|
+
schema: 'sks.codex-lb-tool-catalog-selection.v1',
|
|
440
|
+
ok: true,
|
|
441
|
+
required: false,
|
|
442
|
+
status: 'skipped_reserved_host',
|
|
443
|
+
path: catalogPath,
|
|
444
|
+
config_changed: false,
|
|
445
|
+
blockers: []
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
const catalog = await ensureCodexLbToolCatalog({
|
|
449
|
+
codexHome,
|
|
450
|
+
baseUrl: input.baseUrl,
|
|
451
|
+
apiKey: input.apiKey,
|
|
452
|
+
...(typeof opts.toolCatalogFetch === 'function' ? { fetchImpl: opts.toolCatalogFetch } : {}),
|
|
453
|
+
timeoutMs: Number(opts.toolCatalogTimeoutMs || 5000),
|
|
454
|
+
force: opts.forceToolCatalog === true
|
|
455
|
+
});
|
|
456
|
+
if (!catalog.ok || !selected) {
|
|
457
|
+
return {
|
|
458
|
+
...catalog,
|
|
459
|
+
schema: 'sks.codex-lb-tool-catalog-selection.v1',
|
|
460
|
+
config_changed: false,
|
|
461
|
+
selected
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
const next = ensureTrailingNewline(upsertTopLevelTomlString(current, 'model_catalog_json', catalog.path));
|
|
465
|
+
if (next === ensureTrailingNewline(current)) {
|
|
466
|
+
return { ...catalog, schema: 'sks.codex-lb-tool-catalog-selection.v1', config_changed: false, selected: true };
|
|
467
|
+
}
|
|
468
|
+
const safeWrite = await safeWriteCodexConfigToml(input.configPath, current, next, 'codex-lb-tool-catalog');
|
|
469
|
+
return {
|
|
470
|
+
...catalog,
|
|
471
|
+
schema: 'sks.codex-lb-tool-catalog-selection.v1',
|
|
472
|
+
ok: catalog.ok && safeWrite.ok,
|
|
473
|
+
status: safeWrite.ok ? catalog.status : safeWrite.status,
|
|
474
|
+
config_changed: safeWrite.ok && safeWrite.changed === true,
|
|
475
|
+
backup_path: safeWrite.backup_path,
|
|
476
|
+
selected: true,
|
|
477
|
+
blockers: safeWrite.ok ? catalog.blockers : [...new Set([...(catalog.blockers || []), 'codex_lb_tool_catalog_config_write_failed'])]
|
|
478
|
+
};
|
|
479
|
+
}
|
|
420
480
|
export async function configureCodexLb(opts = {}) {
|
|
421
481
|
const home = opts.home || process.env.HOME || os.homedir();
|
|
422
482
|
const configPath = opts.configPath || codexLbConfigPath(home);
|
|
@@ -472,12 +532,16 @@ export async function configureCodexLb(opts = {}) {
|
|
|
472
532
|
if (useDefaultProvider)
|
|
473
533
|
appliedActions.push({ type: 'select_default_provider', target: configPath, ok: true });
|
|
474
534
|
if (writeEnvFile) {
|
|
475
|
-
await writeTextAtomic(envPath, `export CODEX_LB_BASE_URL=${shellSingleQuote(baseUrl)}\nexport CODEX_LB_API_KEY=${shellSingleQuote(apiKey)}\n
|
|
535
|
+
await writeTextAtomic(envPath, `export CODEX_LB_BASE_URL=${shellSingleQuote(baseUrl)}\nexport CODEX_LB_API_KEY=${shellSingleQuote(apiKey)}\n`, { mode: 0o600 });
|
|
476
536
|
await fsp.chmod(envPath, 0o600).catch(() => { });
|
|
477
537
|
appliedActions.push({ type: 'write_env_file', target: envPath, ok: true });
|
|
478
538
|
}
|
|
479
539
|
process.env.CODEX_LB_BASE_URL = baseUrl;
|
|
480
540
|
process.env.CODEX_LB_API_KEY = apiKey;
|
|
541
|
+
const toolCatalog = await ensureCodexLbToolCatalogSelection({ home, configPath, baseUrl, apiKey }, opts);
|
|
542
|
+
if (toolCatalog.config_changed || toolCatalog.status === 'repaired' || toolCatalog.status === 'cached_compatible') {
|
|
543
|
+
appliedActions.push({ type: 'write_model_tool_catalog', target: toolCatalog.path, ok: toolCatalog.ok === true, status: toolCatalog.status });
|
|
544
|
+
}
|
|
481
545
|
const keyFingerprint = await sha256Text(apiKey);
|
|
482
546
|
const metadataPath = opts.metadataPath || codexLbMetadataPath(home);
|
|
483
547
|
await writeTextAtomic(metadataPath, `${JSON.stringify({
|
|
@@ -507,20 +571,23 @@ export async function configureCodexLb(opts = {}) {
|
|
|
507
571
|
const finalCodexLb = await codexLbStatus({ ...opts, home, configPath, envPath });
|
|
508
572
|
const ok = Boolean(codexEnvironment.ok && codexLogin.ok);
|
|
509
573
|
const afterState = await captureCodexLbSetupWriteState({ home, configPath, envPath, shellProfile });
|
|
510
|
-
const drift =
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
574
|
+
const drift = [
|
|
575
|
+
...detectCodexLbSetupDrift({
|
|
576
|
+
useDefaultProvider,
|
|
577
|
+
writeEnvFile,
|
|
578
|
+
storeKeychain,
|
|
579
|
+
syncLaunchctl,
|
|
580
|
+
shellProfile,
|
|
581
|
+
selected: finalCodexLb.selected,
|
|
582
|
+
envFile: finalCodexLb.env_file,
|
|
583
|
+
keychain,
|
|
584
|
+
codexEnvironment,
|
|
585
|
+
shellProfileResult,
|
|
586
|
+
beforeState,
|
|
587
|
+
afterState
|
|
588
|
+
}),
|
|
589
|
+
...(toolCatalog.required !== false && toolCatalog.ok !== true ? ['codex_lb_gpt56_tool_catalog_not_ready'] : [])
|
|
590
|
+
];
|
|
524
591
|
const appliedPersistenceModes = appliedCodexLbPersistenceModes({
|
|
525
592
|
writeEnvFile,
|
|
526
593
|
storeKeychain,
|
|
@@ -537,7 +604,11 @@ export async function configureCodexLb(opts = {}) {
|
|
|
537
604
|
appliedModes: appliedPersistenceModes,
|
|
538
605
|
processOnly: appliedPersistenceModes.includes('process_only_ephemeral')
|
|
539
606
|
});
|
|
540
|
-
const warnings = [
|
|
607
|
+
const warnings = [
|
|
608
|
+
...insecureLocalWarning,
|
|
609
|
+
...persistence.warnings,
|
|
610
|
+
...(toolCatalog.required !== false && toolCatalog.ok !== true ? ['codex_lb_gpt56_tool_catalog_not_ready'] : [])
|
|
611
|
+
];
|
|
541
612
|
return {
|
|
542
613
|
ok: ok && drift.length === 0,
|
|
543
614
|
status: ok && drift.length === 0 ? 'configured' : drift.length ? 'setup_choice_drift' : (codexEnvironment.status || codexLogin.status),
|
|
@@ -556,6 +627,7 @@ export async function configureCodexLb(opts = {}) {
|
|
|
556
627
|
codex_lb: finalCodexLb,
|
|
557
628
|
codex_environment: codexEnvironment,
|
|
558
629
|
codex_login: codexLogin,
|
|
630
|
+
tool_catalog: toolCatalog,
|
|
559
631
|
error: authReconcile.error || codexEnvironment.error || codexLogin.error || null
|
|
560
632
|
};
|
|
561
633
|
}
|
|
@@ -1052,13 +1124,19 @@ export async function repairCodexLbAuth(opts = {}) {
|
|
|
1052
1124
|
await migrateCodexAuthKeyFormat({ home: opts.home });
|
|
1053
1125
|
const codexEnvironment = await syncCodexLbProviderEnvironment(status, opts);
|
|
1054
1126
|
const apiKey = parseCodexLbEnvKey(await readText(status.env_path, ''));
|
|
1127
|
+
const toolCatalog = await ensureCodexLbToolCatalogSelection({
|
|
1128
|
+
home: opts.home || process.env.HOME || os.homedir(),
|
|
1129
|
+
configPath: status.config_path,
|
|
1130
|
+
baseUrl: String(status.base_url || ''),
|
|
1131
|
+
apiKey
|
|
1132
|
+
}, opts);
|
|
1055
1133
|
const forceCodexLbApiKeyAuth = opts.forceCodexLbApiKeyAuth === true || opts.authMode === 'codex-lb';
|
|
1056
1134
|
const authReconcile = await reconcileCodexLbAuthConflict({ ...opts, status, forceCodexLbApiKeyAuth }).catch((err) => ({ status: 'failed', reason: 'exception', error: err.message }));
|
|
1057
1135
|
const codexLogin = forceCodexLbApiKeyAuth
|
|
1058
1136
|
? { ok: ['apikey_forced', 'apikey_auth_active'].includes(authReconcile.status), status: authReconcile.status, ...(authReconcile.reason ? { reason: authReconcile.reason } : {}), error: authReconcile.error || null }
|
|
1059
1137
|
: await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home: opts.home || process.env.HOME || os.homedir(), force: true });
|
|
1060
1138
|
const finalStatus = await codexLbStatus(opts);
|
|
1061
|
-
const ok = Boolean(codexEnvironment.ok && codexLogin.ok);
|
|
1139
|
+
const ok = Boolean(codexEnvironment.ok && codexLogin.ok && (toolCatalog.required === false || toolCatalog.ok === true));
|
|
1062
1140
|
return {
|
|
1063
1141
|
ok,
|
|
1064
1142
|
status: ok ? 'repaired' : (codexEnvironment.status || codexLogin.status),
|
|
@@ -1071,7 +1149,8 @@ export async function repairCodexLbAuth(opts = {}) {
|
|
|
1071
1149
|
auth_reconcile: authReconcile,
|
|
1072
1150
|
codex_lb: finalStatus,
|
|
1073
1151
|
codex_environment: codexEnvironment,
|
|
1074
|
-
codex_login: codexLogin
|
|
1152
|
+
codex_login: codexLogin,
|
|
1153
|
+
tool_catalog: toolCatalog
|
|
1075
1154
|
};
|
|
1076
1155
|
}
|
|
1077
1156
|
export async function ensureCodexLbAuthDuringInstall(opts = {}) {
|
|
@@ -1101,10 +1180,16 @@ export async function ensureCodexLbAuthDuringInstall(opts = {}) {
|
|
|
1101
1180
|
}
|
|
1102
1181
|
const codexEnvironment = await syncCodexLbProviderEnvironment(status, opts);
|
|
1103
1182
|
const apiKey = parseCodexLbEnvKey(await readText(status.env_path, ''));
|
|
1183
|
+
const toolCatalog = await ensureCodexLbToolCatalogSelection({
|
|
1184
|
+
home: opts.home || process.env.HOME || os.homedir(),
|
|
1185
|
+
configPath: status.config_path,
|
|
1186
|
+
baseUrl: String(status.base_url || ''),
|
|
1187
|
+
apiKey
|
|
1188
|
+
}, opts);
|
|
1104
1189
|
const codexLogin = await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home: opts.home || process.env.HOME || os.homedir(), force: true });
|
|
1105
1190
|
const authReconcile = await reconcileCodexLbAuthConflict({ ...opts, status }).catch((err) => ({ status: 'failed', reason: 'exception', error: err.message }));
|
|
1106
1191
|
const finalStatus = await codexLbStatus(opts);
|
|
1107
|
-
const ok = Boolean(codexEnvironment.ok && codexLogin.ok);
|
|
1192
|
+
const ok = Boolean(codexEnvironment.ok && codexLogin.ok && (toolCatalog.required === false || toolCatalog.ok === true));
|
|
1108
1193
|
return {
|
|
1109
1194
|
ok,
|
|
1110
1195
|
status: ok ? 'present' : (codexEnvironment.status || codexLogin.status),
|
|
@@ -1114,6 +1199,7 @@ export async function ensureCodexLbAuthDuringInstall(opts = {}) {
|
|
|
1114
1199
|
codex_lb: finalStatus,
|
|
1115
1200
|
codex_environment: codexEnvironment,
|
|
1116
1201
|
codex_login: codexLogin,
|
|
1202
|
+
tool_catalog: toolCatalog,
|
|
1117
1203
|
auth_reconcile: authReconcile,
|
|
1118
1204
|
error: codexEnvironment.error || codexLogin.error || null
|
|
1119
1205
|
};
|
|
@@ -1130,7 +1216,7 @@ async function restoreCodexLbEnvFromSharedLogin(status = {}, opts = {}) {
|
|
|
1130
1216
|
if (!baseUrl)
|
|
1131
1217
|
return { ok: false, status: 'missing_base_url', auth_path: authPath, env_path: envPath };
|
|
1132
1218
|
await ensureDir(path.dirname(envPath));
|
|
1133
|
-
await writeTextAtomic(envPath, `export CODEX_LB_BASE_URL=${shellSingleQuote(normalizeCodexLbBaseUrl(baseUrl))}\nexport CODEX_LB_API_KEY=${shellSingleQuote(apiKey)}\n
|
|
1219
|
+
await writeTextAtomic(envPath, `export CODEX_LB_BASE_URL=${shellSingleQuote(normalizeCodexLbBaseUrl(baseUrl))}\nexport CODEX_LB_API_KEY=${shellSingleQuote(apiKey)}\n`, { mode: 0o600 });
|
|
1134
1220
|
await fsp.chmod(envPath, 0o600).catch(() => { });
|
|
1135
1221
|
return { ok: true, status: 'migrated_login_cache', auth_path: authPath, env_path: envPath, base_url: normalizeCodexLbBaseUrl(baseUrl) };
|
|
1136
1222
|
}
|
|
@@ -1203,7 +1289,7 @@ async function migrateCodexAuthKeyFormat(opts = {}) {
|
|
|
1203
1289
|
if (!legacyKey)
|
|
1204
1290
|
return { status: 'skipped', reason: 'no_key_found' };
|
|
1205
1291
|
const replacement = `${JSON.stringify({ auth_mode: 'apikey', OPENAI_API_KEY: legacyKey })}\n`;
|
|
1206
|
-
await writeTextAtomic(authPath, replacement);
|
|
1292
|
+
await writeTextAtomic(authPath, replacement, { mode: 0o600 });
|
|
1207
1293
|
await fsp.chmod(authPath, 0o600).catch(() => { });
|
|
1208
1294
|
return { status: 'migrated', auth_path: authPath };
|
|
1209
1295
|
}
|
|
@@ -1234,7 +1320,7 @@ export async function reconcileCodexLbAuthConflict(opts = {}) {
|
|
|
1234
1320
|
const forceCodexLbApiKeyAuth = opts.forceCodexLbApiKeyAuth === true;
|
|
1235
1321
|
const writeApiKeyAuth = async (reason, backupPathForResult = null) => {
|
|
1236
1322
|
try {
|
|
1237
|
-
await writeTextAtomic(authPath, `${JSON.stringify({ auth_mode: 'apikey', OPENAI_API_KEY: apiKey }, null, 2)}\n
|
|
1323
|
+
await writeTextAtomic(authPath, `${JSON.stringify({ auth_mode: 'apikey', OPENAI_API_KEY: apiKey }, null, 2)}\n`, { mode: 0o600 });
|
|
1238
1324
|
await fsp.chmod(authPath, 0o600).catch(() => { });
|
|
1239
1325
|
return {
|
|
1240
1326
|
status: 'apikey_forced',
|
|
@@ -1260,7 +1346,7 @@ export async function reconcileCodexLbAuthConflict(opts = {}) {
|
|
|
1260
1346
|
if (hasChatgptOAuthTokens(authText)) {
|
|
1261
1347
|
try {
|
|
1262
1348
|
await ensureDir(path.dirname(backupPath));
|
|
1263
|
-
await writeTextAtomic(backupPath, authText);
|
|
1349
|
+
await writeTextAtomic(backupPath, authText, { mode: 0o600 });
|
|
1264
1350
|
await fsp.chmod(backupPath, 0o600).catch(() => { });
|
|
1265
1351
|
}
|
|
1266
1352
|
catch (err) {
|
|
@@ -1286,7 +1372,7 @@ export async function reconcileCodexLbAuthConflict(opts = {}) {
|
|
|
1286
1372
|
}
|
|
1287
1373
|
try {
|
|
1288
1374
|
const replacement = `${JSON.stringify({ auth_mode: 'apikey', OPENAI_API_KEY: apiKey }, null, 2)}\n`;
|
|
1289
|
-
await writeTextAtomic(authPath, replacement);
|
|
1375
|
+
await writeTextAtomic(authPath, replacement, { mode: 0o600 });
|
|
1290
1376
|
await fsp.chmod(authPath, 0o600).catch(() => { });
|
|
1291
1377
|
}
|
|
1292
1378
|
catch (err) {
|
|
@@ -1316,7 +1402,7 @@ export async function reconcileCodexLbAuthConflict(opts = {}) {
|
|
|
1316
1402
|
if (hasChatgptOAuthTokens(backupText) && process.env.SKS_CODEX_LB_KEEP_APIKEY_AUTH !== '1') {
|
|
1317
1403
|
try {
|
|
1318
1404
|
const restored = backupText.endsWith('\n') ? backupText : `${backupText}\n`;
|
|
1319
|
-
await writeTextAtomic(authPath, restored);
|
|
1405
|
+
await writeTextAtomic(authPath, restored, { mode: 0o600 });
|
|
1320
1406
|
await fsp.chmod(authPath, 0o600).catch(() => { });
|
|
1321
1407
|
return {
|
|
1322
1408
|
status: 'oauth_restored',
|
|
@@ -1369,18 +1455,46 @@ export async function unselectCodexLbProvider(opts = {}) {
|
|
|
1369
1455
|
const configPath = opts.configPath || codexLbConfigPath(home);
|
|
1370
1456
|
const current = await readText(configPath, '');
|
|
1371
1457
|
if (!current.trim())
|
|
1372
|
-
return { status: 'not_selected', reason: 'no_config', config_path: configPath };
|
|
1373
|
-
|
|
1374
|
-
|
|
1458
|
+
return { ok: true, status: 'not_selected', reason: 'no_config', config_path: configPath };
|
|
1459
|
+
const managedCatalogPath = codexLbToolCatalogPath(opts.codexHome || path.join(home, '.codex'));
|
|
1460
|
+
const managedCatalogSelected = topLevelTomlString(current, 'model_catalog_json') === managedCatalogPath;
|
|
1461
|
+
if (!hasTopLevelCodexLbSelected(current) && !managedCatalogSelected)
|
|
1462
|
+
return { ok: true, status: 'not_selected', config_path: configPath };
|
|
1375
1463
|
try {
|
|
1376
|
-
|
|
1464
|
+
let next = removeTopLevelTomlString(current, 'model_provider');
|
|
1465
|
+
next = removeTopLevelTomlKeyIfValue(next, 'model_catalog_json', managedCatalogPath);
|
|
1466
|
+
next = ensureTrailingNewline(next);
|
|
1377
1467
|
const safeWrite = await safeWriteCodexConfigToml(configPath, current, next, 'codex-lb-unselect');
|
|
1378
|
-
|
|
1468
|
+
const after = safeWrite.ok ? await readText(configPath, '') : current;
|
|
1469
|
+
const selectionRemoved = !hasTopLevelCodexLbSelected(after)
|
|
1470
|
+
&& topLevelTomlString(after, 'model_catalog_json') !== managedCatalogPath;
|
|
1471
|
+
if (safeWrite.ok && selectionRemoved)
|
|
1472
|
+
return { ok: true, status: 'unselected', config_path: configPath, backup_path: safeWrite.backup_path };
|
|
1473
|
+
const providerError = safeWrite.ok ? 'provider_selection_remains_after_write' : safeWrite.status || 'provider_config_write_blocked';
|
|
1474
|
+
return {
|
|
1475
|
+
ok: false,
|
|
1476
|
+
status: 'failed',
|
|
1477
|
+
reason: 'provider_config_write_blocked',
|
|
1478
|
+
provider_error: providerError,
|
|
1479
|
+
write_status: safeWrite.status || 'failed',
|
|
1480
|
+
config_path: configPath,
|
|
1481
|
+
backup_path: safeWrite.backup_path,
|
|
1482
|
+
config_preserved: safeWrite.changed !== true
|
|
1483
|
+
};
|
|
1379
1484
|
}
|
|
1380
1485
|
catch (err) {
|
|
1381
|
-
return { status: 'failed', reason: 'write_failed', config_path: configPath, error: err.message };
|
|
1486
|
+
return { ok: false, status: 'failed', reason: 'write_failed', provider_error: err.message || 'write_failed', config_path: configPath, error: err.message };
|
|
1382
1487
|
}
|
|
1383
1488
|
}
|
|
1489
|
+
function providerDeselectionOutcome(result) {
|
|
1490
|
+
const ok = result?.status === 'unselected' || result?.status === 'not_selected';
|
|
1491
|
+
return {
|
|
1492
|
+
ok,
|
|
1493
|
+
provider_unselected: ok,
|
|
1494
|
+
provider_status: result?.status || 'failed',
|
|
1495
|
+
provider_error: ok ? null : String(result?.provider_error || result?.error || result?.reason || result?.status || 'unselect_failed')
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1384
1498
|
// Reverse of reconcileCodexLbAuthConflict: restore the ChatGPT OAuth blob from the backup file
|
|
1385
1499
|
// so the user can return to the official ChatGPT account login. Also deselects codex-lb at the
|
|
1386
1500
|
// model_provider level by default so the restored OAuth blob actually wins; pass keepProvider
|
|
@@ -1397,6 +1511,30 @@ export async function releaseCodexLbAuthHold(opts = {}) {
|
|
|
1397
1511
|
const authPath = opts.authPath || codexAuthPath(home);
|
|
1398
1512
|
const backupPath = opts.backupPath || codexAuthChatgptBackupPath(home);
|
|
1399
1513
|
const configPath = opts.configPath || codexLbConfigPath(home);
|
|
1514
|
+
const authExisted = await exists(authPath);
|
|
1515
|
+
const currentAuthText = await readText(authPath, '');
|
|
1516
|
+
const trimmedCurrent = currentAuthText.trim();
|
|
1517
|
+
// Repeated "Use ChatGPT OAuth" is idempotent. If OAuth/browser auth is
|
|
1518
|
+
// already active, a historical backup is unnecessary; only ensure that the
|
|
1519
|
+
// codex-lb provider is no longer selected.
|
|
1520
|
+
const currentAuthMode = codexAuthModeSummary(currentAuthText);
|
|
1521
|
+
if (!opts.force && (currentAuthMode.mode === 'chatgpt_oauth' || currentAuthMode.mode === 'browser_marker')) {
|
|
1522
|
+
let provider = { ok: true, provider_unselected: false, provider_status: 'kept', provider_error: null };
|
|
1523
|
+
if (!opts.keepProvider) {
|
|
1524
|
+
const unselected = await unselectCodexLbProvider({ ...opts, home, configPath });
|
|
1525
|
+
provider = providerDeselectionOutcome(unselected);
|
|
1526
|
+
}
|
|
1527
|
+
return {
|
|
1528
|
+
ok: provider.ok,
|
|
1529
|
+
status: provider.ok ? 'already_chatgpt' : 'failed',
|
|
1530
|
+
...(provider.ok ? {} : { reason: 'provider_unselect_failed' }),
|
|
1531
|
+
auth_path: authPath,
|
|
1532
|
+
backup_path: backupPath,
|
|
1533
|
+
provider_unselected: provider.provider_unselected,
|
|
1534
|
+
provider_status: provider.provider_status,
|
|
1535
|
+
provider_error: provider.provider_error
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1400
1538
|
const backupExists = await exists(backupPath);
|
|
1401
1539
|
const backupText = backupExists ? await readText(backupPath, '') : '';
|
|
1402
1540
|
if (!backupExists || !backupText.trim()) {
|
|
@@ -1416,26 +1554,23 @@ export async function releaseCodexLbAuthHold(opts = {}) {
|
|
|
1416
1554
|
provider_unselected: false
|
|
1417
1555
|
};
|
|
1418
1556
|
}
|
|
1419
|
-
const currentAuthText = await readText(authPath, '');
|
|
1420
|
-
const trimmedCurrent = currentAuthText.trim();
|
|
1421
1557
|
// If auth.json already looks like ChatGPT OAuth (user re-logged in some other way), don't
|
|
1422
1558
|
// clobber it — but still honor the deselect request so the OAuth blob takes effect.
|
|
1423
1559
|
if (trimmedCurrent && hasChatgptOAuthTokens(currentAuthText) && !opts.force) {
|
|
1424
|
-
let
|
|
1425
|
-
let providerError = null;
|
|
1560
|
+
let provider = { ok: true, provider_unselected: false, provider_status: 'kept', provider_error: null };
|
|
1426
1561
|
if (!opts.keepProvider) {
|
|
1427
1562
|
const unselected = await unselectCodexLbProvider({ ...opts, home, configPath });
|
|
1428
|
-
|
|
1429
|
-
providerUnselected = true;
|
|
1430
|
-
else if (unselected.status === 'failed')
|
|
1431
|
-
providerError = unselected.error || unselected.reason || 'unselect_failed';
|
|
1563
|
+
provider = providerDeselectionOutcome(unselected);
|
|
1432
1564
|
}
|
|
1433
1565
|
return {
|
|
1434
|
-
|
|
1566
|
+
ok: provider.ok,
|
|
1567
|
+
status: provider.ok ? 'already_chatgpt' : 'failed',
|
|
1568
|
+
...(provider.ok ? {} : { reason: 'provider_unselect_failed' }),
|
|
1435
1569
|
auth_path: authPath,
|
|
1436
1570
|
backup_path: backupPath,
|
|
1437
|
-
provider_unselected:
|
|
1438
|
-
|
|
1571
|
+
provider_unselected: provider.provider_unselected,
|
|
1572
|
+
provider_status: provider.provider_status,
|
|
1573
|
+
provider_error: provider.provider_error
|
|
1439
1574
|
};
|
|
1440
1575
|
}
|
|
1441
1576
|
// Refuse to clobber unfamiliar auth.json shapes unless forced. We expect either an empty file,
|
|
@@ -1456,7 +1591,7 @@ export async function releaseCodexLbAuthHold(opts = {}) {
|
|
|
1456
1591
|
try {
|
|
1457
1592
|
await ensureDir(path.dirname(authPath));
|
|
1458
1593
|
const restored = backupText.endsWith('\n') ? backupText : `${backupText}\n`;
|
|
1459
|
-
await writeTextAtomic(authPath, restored);
|
|
1594
|
+
await writeTextAtomic(authPath, restored, { mode: 0o600 });
|
|
1460
1595
|
await fsp.chmod(authPath, 0o600).catch(() => { });
|
|
1461
1596
|
}
|
|
1462
1597
|
catch (err) {
|
|
@@ -1469,6 +1604,28 @@ export async function releaseCodexLbAuthHold(opts = {}) {
|
|
|
1469
1604
|
provider_unselected: false
|
|
1470
1605
|
};
|
|
1471
1606
|
}
|
|
1607
|
+
let provider = { ok: true, provider_unselected: false, provider_status: 'kept', provider_error: null };
|
|
1608
|
+
if (!opts.keepProvider) {
|
|
1609
|
+
const unselected = await unselectCodexLbProvider({ ...opts, home, configPath });
|
|
1610
|
+
provider = providerDeselectionOutcome(unselected);
|
|
1611
|
+
}
|
|
1612
|
+
if (!provider.ok) {
|
|
1613
|
+
const rollback = await rollbackCodexAuthRestore({ authPath, authExisted, currentAuthText });
|
|
1614
|
+
return {
|
|
1615
|
+
ok: false,
|
|
1616
|
+
status: 'failed',
|
|
1617
|
+
reason: 'provider_unselect_failed',
|
|
1618
|
+
auth_path: authPath,
|
|
1619
|
+
backup_path: backupPath,
|
|
1620
|
+
backup_removed: false,
|
|
1621
|
+
auth_restored: rollback.ok !== true,
|
|
1622
|
+
auth_rollback: rollback,
|
|
1623
|
+
rollback_safe: rollback.ok === true,
|
|
1624
|
+
provider_unselected: false,
|
|
1625
|
+
provider_status: provider.provider_status,
|
|
1626
|
+
provider_error: provider.provider_error
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1472
1629
|
let backupRemoved = false;
|
|
1473
1630
|
if (opts.deleteBackup) {
|
|
1474
1631
|
try {
|
|
@@ -1476,27 +1633,36 @@ export async function releaseCodexLbAuthHold(opts = {}) {
|
|
|
1476
1633
|
backupRemoved = true;
|
|
1477
1634
|
}
|
|
1478
1635
|
catch {
|
|
1479
|
-
// Non-fatal: the restore already landed.
|
|
1636
|
+
// Non-fatal: the restore and provider deselection already landed.
|
|
1480
1637
|
}
|
|
1481
1638
|
}
|
|
1482
|
-
let providerUnselected = false;
|
|
1483
|
-
let providerError = null;
|
|
1484
|
-
if (!opts.keepProvider) {
|
|
1485
|
-
const unselected = await unselectCodexLbProvider({ ...opts, home, configPath });
|
|
1486
|
-
if (unselected.status === 'unselected')
|
|
1487
|
-
providerUnselected = true;
|
|
1488
|
-
else if (unselected.status === 'failed')
|
|
1489
|
-
providerError = unselected.error || unselected.reason || 'unselect_failed';
|
|
1490
|
-
}
|
|
1491
1639
|
return {
|
|
1640
|
+
ok: true,
|
|
1492
1641
|
status: 'released',
|
|
1493
1642
|
auth_path: authPath,
|
|
1494
1643
|
backup_path: backupPath,
|
|
1495
1644
|
backup_removed: backupRemoved,
|
|
1496
|
-
|
|
1497
|
-
|
|
1645
|
+
auth_restored: true,
|
|
1646
|
+
provider_unselected: provider.provider_unselected,
|
|
1647
|
+
provider_status: provider.provider_status,
|
|
1648
|
+
provider_error: provider.provider_error
|
|
1498
1649
|
};
|
|
1499
1650
|
}
|
|
1651
|
+
async function rollbackCodexAuthRestore(input) {
|
|
1652
|
+
try {
|
|
1653
|
+
if (input.authExisted) {
|
|
1654
|
+
await writeTextAtomic(input.authPath, input.currentAuthText, { mode: 0o600 });
|
|
1655
|
+
await fsp.chmod(input.authPath, 0o600).catch(() => { });
|
|
1656
|
+
}
|
|
1657
|
+
else {
|
|
1658
|
+
await fsp.rm(input.authPath, { force: true });
|
|
1659
|
+
}
|
|
1660
|
+
return { ok: true, status: 'restored_previous_auth' };
|
|
1661
|
+
}
|
|
1662
|
+
catch (err) {
|
|
1663
|
+
return { ok: false, status: 'rollback_failed', error: err.message };
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1500
1666
|
export async function maybePromptCodexLbSetupForLaunch(args = [], opts = {}) {
|
|
1501
1667
|
if (args.includes('--json') || args.includes('--skip-codex-lb') || process.env.SKS_SKIP_CODEX_LB_PROMPT === '1')
|
|
1502
1668
|
return { status: 'skipped' };
|
|
@@ -1528,6 +1694,15 @@ export async function maybePromptCodexLbSetupForLaunch(args = [], opts = {}) {
|
|
|
1528
1694
|
if (codexEnvironment.status === 'synced')
|
|
1529
1695
|
console.log('codex-lb provider auth synced for this user session.');
|
|
1530
1696
|
const apiKey = parseCodexLbEnvKey(await readText(status.env_path, ''));
|
|
1697
|
+
const toolCatalog = await ensureCodexLbToolCatalogSelection({
|
|
1698
|
+
home: opts.home || process.env.HOME || os.homedir(),
|
|
1699
|
+
configPath: status.config_path,
|
|
1700
|
+
baseUrl: String(status.base_url || ''),
|
|
1701
|
+
apiKey
|
|
1702
|
+
}, opts);
|
|
1703
|
+
if (toolCatalog.required !== false && toolCatalog.ok !== true) {
|
|
1704
|
+
return { status: 'repair_failed', ok: false, codex_lb: status, codex_environment: codexEnvironment, tool_catalog: toolCatalog };
|
|
1705
|
+
}
|
|
1531
1706
|
const codexLogin = await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home: opts.home || process.env.HOME || os.homedir() });
|
|
1532
1707
|
if (codexLogin.status === 'synced')
|
|
1533
1708
|
console.log('codex-lb auth synced with Codex CLI login cache.');
|
|
@@ -1538,7 +1713,7 @@ export async function maybePromptCodexLbSetupForLaunch(args = [], opts = {}) {
|
|
|
1538
1713
|
// health probe fails. Keep codex-lb active and just warn.
|
|
1539
1714
|
if (chainHealth.status === 'previous_response_not_found') {
|
|
1540
1715
|
console.log('codex-lb response chain check: previous_response_id not persisted by the load balancer (this is normal for stateless deployments). Keeping codex-lb active.');
|
|
1541
|
-
return { status: 'present', ...status, codex_environment: codexEnvironment, codex_login: codexLogin, chain_health: chainHealth };
|
|
1716
|
+
return { status: 'present', ...status, codex_environment: codexEnvironment, codex_login: codexLogin, tool_catalog: toolCatalog, chain_health: chainHealth };
|
|
1542
1717
|
}
|
|
1543
1718
|
// Hard chain failure (auth rejected, timeout, missing base URL, etc.). Don't silently
|
|
1544
1719
|
// demote a configured codex-lb to ChatGPT OAuth — surface the failure and let the user
|
|
@@ -1546,23 +1721,23 @@ export async function maybePromptCodexLbSetupForLaunch(args = [], opts = {}) {
|
|
|
1546
1721
|
console.log(`codex-lb response chain check failed (${chainHealth.status}${chainHealth.error ? `: ${chainHealth.error}` : ''}).`);
|
|
1547
1722
|
if (process.env.SKS_CODEX_LB_AUTOBYPASS === '1') {
|
|
1548
1723
|
console.log('SKS_CODEX_LB_AUTOBYPASS=1 set; bypassing codex-lb to ChatGPT OAuth for this launch.');
|
|
1549
|
-
return { status: 'chain_unhealthy', ...status, ok: false, codex_environment: codexEnvironment, codex_login: codexLogin, chain_health: chainHealth, bypass_codex_lb: true };
|
|
1724
|
+
return { status: 'chain_unhealthy', ...status, ok: false, codex_environment: codexEnvironment, codex_login: codexLogin, tool_catalog: toolCatalog, chain_health: chainHealth, bypass_codex_lb: true };
|
|
1550
1725
|
}
|
|
1551
1726
|
if (canAskYesNo()) {
|
|
1552
1727
|
const answer = (await askPostinstallQuestion('Use codex-lb anyway, or fall back to ChatGPT OAuth? [LB/oauth] ')).trim().toLowerCase();
|
|
1553
1728
|
if (/^(oauth|o|chatgpt|fall ?back|n|no|아니|아니요|ㄴ)$/.test(answer)) {
|
|
1554
1729
|
console.log('Falling back to ChatGPT OAuth for this launch. Re-enable codex-lb anytime with `sks codex-lb repair`.');
|
|
1555
|
-
return { status: 'chain_unhealthy', ...status, ok: false, codex_environment: codexEnvironment, codex_login: codexLogin, chain_health: chainHealth, bypass_codex_lb: true };
|
|
1730
|
+
return { status: 'chain_unhealthy', ...status, ok: false, codex_environment: codexEnvironment, codex_login: codexLogin, tool_catalog: toolCatalog, chain_health: chainHealth, bypass_codex_lb: true };
|
|
1556
1731
|
}
|
|
1557
1732
|
console.log('Keeping codex-lb active. To switch back to ChatGPT OAuth: `sks codex-lb release`.');
|
|
1558
|
-
return { status: 'present', ...status, codex_environment: codexEnvironment, codex_login: codexLogin, chain_health: chainHealth };
|
|
1733
|
+
return { status: 'present', ...status, codex_environment: codexEnvironment, codex_login: codexLogin, tool_catalog: toolCatalog, chain_health: chainHealth };
|
|
1559
1734
|
}
|
|
1560
1735
|
// Non-interactive context with no opt-out env var. The user explicitly configured codex-lb,
|
|
1561
1736
|
// so default to keeping it active rather than silently swapping providers.
|
|
1562
1737
|
console.log('Non-interactive launch + chain check failure. Keeping codex-lb active. Set SKS_CODEX_LB_AUTOBYPASS=1 to auto-bypass to ChatGPT OAuth.');
|
|
1563
|
-
return { status: 'present', ...status, codex_environment: codexEnvironment, codex_login: codexLogin, chain_health: chainHealth };
|
|
1738
|
+
return { status: 'present', ...status, codex_environment: codexEnvironment, codex_login: codexLogin, tool_catalog: toolCatalog, chain_health: chainHealth };
|
|
1564
1739
|
}
|
|
1565
|
-
return { status: 'present', ...status, codex_environment: codexEnvironment, codex_login: codexLogin, chain_health: chainHealth };
|
|
1740
|
+
return { status: 'present', ...status, codex_environment: codexEnvironment, codex_login: codexLogin, tool_catalog: toolCatalog, chain_health: chainHealth };
|
|
1566
1741
|
}
|
|
1567
1742
|
if (!canAskYesNo())
|
|
1568
1743
|
return { status: 'non_interactive', codex_lb: status };
|
|
@@ -1848,366 +2023,6 @@ function appliedCodexLbPersistenceModes(state = {}) {
|
|
|
1848
2023
|
modes.push('none');
|
|
1849
2024
|
return modes;
|
|
1850
2025
|
}
|
|
1851
|
-
export async function ensureGlobalCodexFastModeDuringInstall(opts = {}) {
|
|
1852
|
-
if (process.env.SKS_SKIP_CODEX_FAST_MODE_REPAIR === '1')
|
|
1853
|
-
return { status: 'skipped', reason: 'SKS_SKIP_CODEX_FAST_MODE_REPAIR=1' };
|
|
1854
|
-
const home = opts.home || process.env.HOME || os.homedir();
|
|
1855
|
-
const configPath = opts.configPath || codexLbConfigPath(home);
|
|
1856
|
-
try {
|
|
1857
|
-
await ensureDir(path.dirname(configPath));
|
|
1858
|
-
const current = await readText(configPath, '');
|
|
1859
|
-
// Safety gate 1: never blind-overwrite an unparseable user config — that would
|
|
1860
|
-
// entrench corruption on the file Codex actually loads. Back it up and bail.
|
|
1861
|
-
if (current.trim()) {
|
|
1862
|
-
const currentSmoke = codexConfigParseSmoke(current);
|
|
1863
|
-
if (!currentSmoke.ok) {
|
|
1864
|
-
const backupPath = await backupCodexConfig(configPath, current, 'unparseable');
|
|
1865
|
-
return { status: 'unparseable_config_preserved', config_path: configPath, backup_path: backupPath, parse_smoke: currentSmoke };
|
|
1866
|
-
}
|
|
1867
|
-
}
|
|
1868
|
-
const next = normalizeCodexFastModeUiConfig(current, {
|
|
1869
|
-
forceFastMode: opts.forceFastMode === true,
|
|
1870
|
-
forceFastModeOff: opts.forceFastModeOff === true
|
|
1871
|
-
});
|
|
1872
|
-
if (next === ensureTrailingNewline(current))
|
|
1873
|
-
return { status: 'present', config_path: configPath };
|
|
1874
|
-
const safeWrite = await safeWriteCodexConfigToml(configPath, current, next, 'codex-fast-mode-install', {
|
|
1875
|
-
preserveFastUiKeys: opts.forceFastModeOff !== true
|
|
1876
|
-
});
|
|
1877
|
-
return {
|
|
1878
|
-
status: safeWrite.status === 'written' ? 'updated' : safeWrite.status,
|
|
1879
|
-
config_path: configPath,
|
|
1880
|
-
backup_path: safeWrite.backup_path,
|
|
1881
|
-
parse_smoke: safeWrite.ok ? undefined : safeWrite
|
|
1882
|
-
};
|
|
1883
|
-
}
|
|
1884
|
-
catch (err) {
|
|
1885
|
-
return { status: 'failed', config_path: configPath, error: err.message };
|
|
1886
|
-
}
|
|
1887
|
-
}
|
|
1888
|
-
export function normalizeCodexFastModeUiConfig(text = '', opts = {}) {
|
|
1889
|
-
// Run to a fixed point so a second install is a true no-op (idempotent). The per-pass
|
|
1890
|
-
// table/whitespace normalization converges within one extra pass.
|
|
1891
|
-
return normalizeCodexFastModeUiConfigOnce(normalizeCodexFastModeUiConfigOnce(text, opts), opts);
|
|
1892
|
-
}
|
|
1893
|
-
function normalizeCodexFastModeUiConfigOnce(text = '', opts = {}) {
|
|
1894
|
-
// 2026-07 Codex App renewal (ChatGPT desktop merge): the config schema removed
|
|
1895
|
-
// `default_profile`, `[profiles.<name>]` tables, `[user.fast_mode]`, and
|
|
1896
|
-
// `notice.fast_default_opt_out`. The only documented fast-default mechanism is
|
|
1897
|
-
// the plain top-level `service_tier = "fast"`. This pass strips every legacy
|
|
1898
|
-
// schema stamps SKS wrote and keeps user-authored top-level choices intact.
|
|
1899
|
-
let next = String(text || '');
|
|
1900
|
-
next = removeLegacyTopLevelCodexModeLocks(next);
|
|
1901
|
-
next = removeTopLevelTomlKeyIfValue(next, 'default_profile', 'sks-fast-high');
|
|
1902
|
-
next = removeTomlTable(next, 'user.fast_mode');
|
|
1903
|
-
next = removeTomlTable(next, 'profiles.sks-fast-high');
|
|
1904
|
-
next = removeTomlTableKey(next, 'notice', 'fast_default_opt_out');
|
|
1905
|
-
// Feature flags SKS wrote in earlier versions that are not in the current
|
|
1906
|
-
// [features] reference (renewal removed/renamed them). They only trigger the
|
|
1907
|
-
// under-development warning now — strip our own stamps.
|
|
1908
|
-
for (const legacyFlag of [
|
|
1909
|
-
'codex_hooks', 'remote_control', 'fast_mode_ui', 'codex_git_commit', 'computer_use',
|
|
1910
|
-
'browser_use', 'browser_use_external', 'image_generation', 'in_app_browser',
|
|
1911
|
-
'guardian_approval', 'tool_suggest', 'plugins'
|
|
1912
|
-
]) {
|
|
1913
|
-
next = removeTomlTableKey(next, 'features', legacyFlag);
|
|
1914
|
-
}
|
|
1915
|
-
if (opts.forceFastMode === true) {
|
|
1916
|
-
next = upsertTopLevelTomlString(next, 'service_tier', 'fast');
|
|
1917
|
-
}
|
|
1918
|
-
else if (opts.forceFastModeOff === true) {
|
|
1919
|
-
// "off" = remove the tier and let Codex's own default apply. The old code
|
|
1920
|
-
// wrote service_tier = "default", which is not a documented tier value.
|
|
1921
|
-
next = removeTopLevelTomlKey(next, 'service_tier');
|
|
1922
|
-
}
|
|
1923
|
-
// Documented, currently-valid feature flags are SET-IF-ABSENT: a fresh config
|
|
1924
|
-
// gets SKS's defaults, but SKS never re-enables a feature the user disabled.
|
|
1925
|
-
next = upsertTopLevelTomlBooleanIfAbsent(next, 'suppress_unstable_features_warning', true);
|
|
1926
|
-
for (const featureLine of ['hooks = true', 'multi_agent = true', 'fast_mode = true', 'apps = true']) {
|
|
1927
|
-
next = upsertTomlTableKeyIfAbsent(next, 'features', featureLine);
|
|
1928
|
-
}
|
|
1929
|
-
if (process.env.SKS_ALLOW_HIGH_AGENT_CONCURRENCY !== '1') {
|
|
1930
|
-
next = upsertTomlTableKey(next, 'agents', 'max_threads = 4');
|
|
1931
|
-
}
|
|
1932
|
-
next = removeTomlTable(next, 'features.multi_agent_v2');
|
|
1933
|
-
// Plugin auto-enable is OPT-IN only. Force-writing `[plugins."name@marketplace"] enabled =
|
|
1934
|
-
// true` for marketplace plugins the App may not have installed (different build/channel)
|
|
1935
|
-
// makes the App reference plugins it cannot load -> broken/blocked plugin UI. It also
|
|
1936
|
-
// replaced the user's whole plugin table, reverting any `enabled = false` they set. By
|
|
1937
|
-
// default SKS leaves the user's [plugins] alone; opt in with SKS_MANAGE_CODEX_APP_PLUGINS=1.
|
|
1938
|
-
if (process.env.SKS_MANAGE_CODEX_APP_PLUGINS === '1') {
|
|
1939
|
-
for (const [name, marketplace] of DEFAULT_CODEX_APP_PLUGINS) {
|
|
1940
|
-
const table = `plugins."${name}@${marketplace}"`;
|
|
1941
|
-
if (!hasTomlTable(next, table))
|
|
1942
|
-
next = upsertTomlTable(next, table, `[${table}]\nenabled = true`);
|
|
1943
|
-
}
|
|
1944
|
-
}
|
|
1945
|
-
return ensureTrailingNewline(next);
|
|
1946
|
-
}
|
|
1947
|
-
function removeTopLevelTomlKey(text = '', key = '') {
|
|
1948
|
-
const lines = String(text || '').split('\n');
|
|
1949
|
-
const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
|
|
1950
|
-
const end = firstTable === -1 ? lines.length : firstTable;
|
|
1951
|
-
const keyPattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`);
|
|
1952
|
-
return lines.filter((line, index) => index >= end || !keyPattern.test(line)).join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
|
|
1953
|
-
}
|
|
1954
|
-
function removeTomlTable(text, table) {
|
|
1955
|
-
const lines = String(text || '').trimEnd().split('\n');
|
|
1956
|
-
const header = `[${table}]`;
|
|
1957
|
-
const start = lines.findIndex((x) => x.trim() === header);
|
|
1958
|
-
if (start === -1)
|
|
1959
|
-
return String(text || '');
|
|
1960
|
-
let end = lines.length;
|
|
1961
|
-
for (let i = start + 1; i < lines.length; i += 1) {
|
|
1962
|
-
const ln = lines[i];
|
|
1963
|
-
if (ln !== undefined && /^\s*\[.+\]\s*$/.test(ln)) {
|
|
1964
|
-
end = i;
|
|
1965
|
-
break;
|
|
1966
|
-
}
|
|
1967
|
-
}
|
|
1968
|
-
return lines.filter((_, index) => index < start || index >= end).join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
|
|
1969
|
-
}
|
|
1970
|
-
function removeLegacyTopLevelCodexModeLocks(text = '') {
|
|
1971
|
-
const lines = String(text || '').split('\n');
|
|
1972
|
-
const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
|
|
1973
|
-
const end = firstTable === -1 ? lines.length : firstTable;
|
|
1974
|
-
return lines.filter((line, index) => {
|
|
1975
|
-
if (index >= end)
|
|
1976
|
-
return true;
|
|
1977
|
-
if (!/^\s*(?:model|model_reasoning_effort)\s*=/.test(line))
|
|
1978
|
-
return true;
|
|
1979
|
-
return ![line, lines[index - 1] || ''].some((candidate) => {
|
|
1980
|
-
const trimmed = String(candidate || '').trim();
|
|
1981
|
-
const comment = trimmed.startsWith('#') ? trimmed : trimmed.includes('#') ? trimmed.slice(trimmed.indexOf('#')) : '';
|
|
1982
|
-
return /(?:SKS|Sneakoscope|codex-lb|sks fast)/i.test(comment);
|
|
1983
|
-
});
|
|
1984
|
-
}).join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
|
|
1985
|
-
}
|
|
1986
|
-
function removeTopLevelTomlKeyIfValue(text = '', key = '', value = '') {
|
|
1987
|
-
const lines = String(text || '').split('\n');
|
|
1988
|
-
const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
|
|
1989
|
-
const end = firstTable === -1 ? lines.length : firstTable;
|
|
1990
|
-
const keyPattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*"${escapeRegExp(value)}"\\s*(?:#.*)?$`);
|
|
1991
|
-
return lines.filter((line, index) => index >= end || !keyPattern.test(line)).join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
|
|
1992
|
-
}
|
|
1993
|
-
function removeTomlTableKey(text, table, key) {
|
|
1994
|
-
const lines = String(text || '').trimEnd().split('\n');
|
|
1995
|
-
if (lines.length === 1 && lines[0] === '')
|
|
1996
|
-
return '';
|
|
1997
|
-
const header = `[${table}]`;
|
|
1998
|
-
const start = lines.findIndex((x) => x.trim() === header);
|
|
1999
|
-
if (start === -1)
|
|
2000
|
-
return String(text || '');
|
|
2001
|
-
let end = lines.length;
|
|
2002
|
-
for (let i = start + 1; i < lines.length; i += 1) {
|
|
2003
|
-
const ln = lines[i];
|
|
2004
|
-
if (ln === undefined)
|
|
2005
|
-
continue;
|
|
2006
|
-
if (/^\s*\[.+\]\s*$/.test(ln)) {
|
|
2007
|
-
end = i;
|
|
2008
|
-
break;
|
|
2009
|
-
}
|
|
2010
|
-
}
|
|
2011
|
-
const keyPattern = new RegExp(`^\\s*${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*=`);
|
|
2012
|
-
return lines.filter((line, index) => index <= start || index >= end || !keyPattern.test(line)).join('\n').replace(/\n{3,}/g, '\n\n');
|
|
2013
|
-
}
|
|
2014
|
-
function upsertTomlTableKey(text, table, line) {
|
|
2015
|
-
const key = String(line).split('=')[0]?.trim() ?? '';
|
|
2016
|
-
const lines = String(text || '').trimEnd().split('\n');
|
|
2017
|
-
if (lines.length === 1 && lines[0] === '')
|
|
2018
|
-
lines.length = 0;
|
|
2019
|
-
const header = `[${table}]`;
|
|
2020
|
-
const start = lines.findIndex((x) => x.trim() === header);
|
|
2021
|
-
if (start === -1)
|
|
2022
|
-
return [...lines, ...(lines.length ? [''] : []), header, line].join('\n').replace(/\n{3,}/g, '\n\n');
|
|
2023
|
-
let end = lines.length;
|
|
2024
|
-
for (let i = start + 1; i < lines.length; i++) {
|
|
2025
|
-
const ln = lines[i];
|
|
2026
|
-
if (ln === undefined)
|
|
2027
|
-
continue;
|
|
2028
|
-
if (/^\s*\[.+\]\s*$/.test(ln)) {
|
|
2029
|
-
end = i;
|
|
2030
|
-
break;
|
|
2031
|
-
}
|
|
2032
|
-
}
|
|
2033
|
-
const keyRe = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`);
|
|
2034
|
-
for (let i = start + 1; i < end; i++) {
|
|
2035
|
-
const ln = lines[i];
|
|
2036
|
-
if (ln === undefined)
|
|
2037
|
-
continue;
|
|
2038
|
-
if (keyRe.test(ln)) {
|
|
2039
|
-
lines[i] = line;
|
|
2040
|
-
return lines.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
2041
|
-
}
|
|
2042
|
-
}
|
|
2043
|
-
if (hasTomlTableKey(lines.join('\n'), table, key))
|
|
2044
|
-
return lines.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
2045
|
-
lines.splice(end, 0, line);
|
|
2046
|
-
return lines.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
2047
|
-
}
|
|
2048
|
-
// True if [table] already declares `key` (so we never override a user's explicit value).
|
|
2049
|
-
function hasTomlTableKey(text, table, key) {
|
|
2050
|
-
const lines = String(text || '').split('\n');
|
|
2051
|
-
const header = `[${table}]`;
|
|
2052
|
-
const start = lines.findIndex((x) => x.trim() === header);
|
|
2053
|
-
if (start === -1)
|
|
2054
|
-
return false;
|
|
2055
|
-
const keyRe = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`);
|
|
2056
|
-
for (let i = start + 1; i < lines.length; i += 1) {
|
|
2057
|
-
const ln = lines[i];
|
|
2058
|
-
if (ln === undefined)
|
|
2059
|
-
continue;
|
|
2060
|
-
if (/^\s*\[.+\]\s*$/.test(ln))
|
|
2061
|
-
break;
|
|
2062
|
-
if (keyRe.test(ln))
|
|
2063
|
-
return true;
|
|
2064
|
-
}
|
|
2065
|
-
return false;
|
|
2066
|
-
}
|
|
2067
|
-
// Set a [table] key only when absent — preserves a Codex App feature the user toggled off
|
|
2068
|
-
// (so SKS never re-enables / re-surfaces UI the user hid). On a fresh config the key/table
|
|
2069
|
-
// is still created, preserving fresh-install enablement.
|
|
2070
|
-
function upsertTomlTableKeyIfAbsent(text, table, line) {
|
|
2071
|
-
const key = String(line).split('=')[0]?.trim() ?? '';
|
|
2072
|
-
return hasTomlTableKey(text, table, key) ? String(text || '') : upsertTomlTableKey(text, table, line);
|
|
2073
|
-
}
|
|
2074
|
-
function upsertTopLevelTomlBooleanIfAbsent(text, key, value) {
|
|
2075
|
-
return hasTopLevelTomlKey(text, key) ? String(text || '') : upsertTopLevelTomlBoolean(text, key, value);
|
|
2076
|
-
}
|
|
2077
|
-
function ensureTrailingNewline(text = '') {
|
|
2078
|
-
const value = String(text || '').trimEnd();
|
|
2079
|
-
return value ? `${value}\n` : '';
|
|
2080
|
-
}
|
|
2081
|
-
function upsertTopLevelTomlString(text, key, value) {
|
|
2082
|
-
const line = `${key} = "${value}"`;
|
|
2083
|
-
const lines = String(text || '').split('\n');
|
|
2084
|
-
const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
|
|
2085
|
-
const end = firstTable === -1 ? lines.length : firstTable;
|
|
2086
|
-
for (let i = 0; i < end; i++) {
|
|
2087
|
-
const ln = lines[i];
|
|
2088
|
-
if (ln === undefined)
|
|
2089
|
-
continue;
|
|
2090
|
-
if (new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(ln)) {
|
|
2091
|
-
lines[i] = line;
|
|
2092
|
-
return lines.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
2093
|
-
}
|
|
2094
|
-
}
|
|
2095
|
-
lines.splice(end, 0, line);
|
|
2096
|
-
return lines.join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
|
|
2097
|
-
}
|
|
2098
|
-
function hasTopLevelTomlKey(text, key) {
|
|
2099
|
-
const lines = String(text || '').split('\n');
|
|
2100
|
-
const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
|
|
2101
|
-
const end = firstTable === -1 ? lines.length : firstTable;
|
|
2102
|
-
const pattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`);
|
|
2103
|
-
for (let i = 0; i < end; i += 1) {
|
|
2104
|
-
if (typeof lines[i] === 'string' && pattern.test(lines[i]))
|
|
2105
|
-
return true;
|
|
2106
|
-
}
|
|
2107
|
-
return false;
|
|
2108
|
-
}
|
|
2109
|
-
// Preserve a user's deliberate top-level scalar (model/service_tier/reasoning); only set
|
|
2110
|
-
// the SKS default when the key is ABSENT. This is what stops `npm i -g` from clobbering
|
|
2111
|
-
// a user's global Codex config on every update.
|
|
2112
|
-
function upsertTopLevelTomlStringIfAbsent(text, key, value) {
|
|
2113
|
-
return hasTopLevelTomlKey(text, key) ? String(text || '') : upsertTopLevelTomlString(text, key, value);
|
|
2114
|
-
}
|
|
2115
|
-
// Lightweight safety gate: detect clearly-broken TOML so we never overwrite (or produce)
|
|
2116
|
-
// an unparseable config that Codex itself would reject. Mirrors the project-config smoke.
|
|
2117
|
-
function codexConfigParseSmoke(text = '') {
|
|
2118
|
-
const str = String(text || '');
|
|
2119
|
-
const tripleTokens = (str.match(/"""|'''/g) || []).length;
|
|
2120
|
-
const unterminatedTriple = tripleTokens % 2 !== 0;
|
|
2121
|
-
const invalidHeader = str.split('\n').find((line) => /^\s*\[/.test(line) && !/^\s*\[\[?[^\]]+\]\]?\s*(?:#.*)?$/.test(line)) || null;
|
|
2122
|
-
return { ok: !unterminatedTriple && !invalidHeader, unterminated_multiline_string: unterminatedTriple, invalid_table_header: invalidHeader };
|
|
2123
|
-
}
|
|
2124
|
-
async function backupCodexConfig(configPath, text, tag) {
|
|
2125
|
-
try {
|
|
2126
|
-
const stamp = `${PACKAGE_VERSION}-${Date.now().toString(36)}`;
|
|
2127
|
-
const backupPath = `${configPath}.sks-${tag}-${stamp}.bak`;
|
|
2128
|
-
await writeTextAtomic(backupPath, text);
|
|
2129
|
-
await cleanupCodexConfigBackups(configPath, { keepPerTag: 3, maxAgeMs: 30 * 24 * 60 * 60 * 1000 }).catch(() => undefined);
|
|
2130
|
-
return backupPath;
|
|
2131
|
-
}
|
|
2132
|
-
catch {
|
|
2133
|
-
return null;
|
|
2134
|
-
}
|
|
2135
|
-
}
|
|
2136
|
-
// Single TOML-safe gate for every codex-lb config write. Mirrors the fast-mode safety so the
|
|
2137
|
-
// codex-lb path can NEVER corrupt ~/.codex/config.toml on install (esp. a fresh/initial one):
|
|
2138
|
-
// - refuse to overwrite an existing config that is already unparseable (back it up, bail),
|
|
2139
|
-
// - refuse to WRITE a result that would not parse (e.g. a regex helper mangled a multiline
|
|
2140
|
-
// string), leaving the existing config untouched,
|
|
2141
|
-
// - otherwise back up the prior config before mutating.
|
|
2142
|
-
export async function safeWriteCodexConfigToml(configPath, current, next, tag = 'codex-lb', opts = {}) {
|
|
2143
|
-
return writeCodexConfigGuarded({
|
|
2144
|
-
configPath,
|
|
2145
|
-
before: String(current || ''),
|
|
2146
|
-
cause: tag,
|
|
2147
|
-
removeTopLevelModeLocks: true,
|
|
2148
|
-
...(opts.preserveFastUiKeys === undefined ? {} : { preserveFastUiKeys: opts.preserveFastUiKeys }),
|
|
2149
|
-
mutate: () => String(next || '')
|
|
2150
|
-
});
|
|
2151
|
-
}
|
|
2152
|
-
export function codexFastModeDesktopStatus(text = '') {
|
|
2153
|
-
const validation = validateCodexConfigRoundTrip(String(text || ''));
|
|
2154
|
-
// Post-renewal contract: the fast default is the plain top-level
|
|
2155
|
-
// service_tier = "fast". default_profile/[profiles.*]/[user.fast_mode] are
|
|
2156
|
-
// gone from the Codex config schema and are only reported as legacy_keys.
|
|
2157
|
-
const globalOn = validation.ok && validation.service_tier === 'fast';
|
|
2158
|
-
return {
|
|
2159
|
-
schema: 'sks.codex-fast-mode-desktop-status.v2',
|
|
2160
|
-
ok: validation.ok,
|
|
2161
|
-
on: Boolean(globalOn),
|
|
2162
|
-
service_tier: validation.service_tier ?? null,
|
|
2163
|
-
model: validation.model ?? null,
|
|
2164
|
-
legacy_keys: validation.legacy_keys,
|
|
2165
|
-
validation
|
|
2166
|
-
};
|
|
2167
|
-
}
|
|
2168
|
-
function upsertTopLevelTomlBoolean(text, key, value) {
|
|
2169
|
-
const line = `${key} = ${value ? 'true' : 'false'}`;
|
|
2170
|
-
const lines = String(text || '').split('\n');
|
|
2171
|
-
const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
|
|
2172
|
-
const end = firstTable === -1 ? lines.length : firstTable;
|
|
2173
|
-
for (let i = 0; i < end; i += 1) {
|
|
2174
|
-
const ln = lines[i];
|
|
2175
|
-
if (ln === undefined)
|
|
2176
|
-
continue;
|
|
2177
|
-
if (new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(ln)) {
|
|
2178
|
-
lines[i] = line;
|
|
2179
|
-
return lines.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
2180
|
-
}
|
|
2181
|
-
}
|
|
2182
|
-
lines.splice(end, 0, line);
|
|
2183
|
-
return lines.join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
|
|
2184
|
-
}
|
|
2185
|
-
function hasTomlTable(text, table) {
|
|
2186
|
-
const header = `[${table}]`;
|
|
2187
|
-
return String(text || '').split('\n').some((line) => String(line).trim() === header);
|
|
2188
|
-
}
|
|
2189
|
-
function upsertTomlTable(text, table, block) {
|
|
2190
|
-
let lines = String(text || '').trimEnd().split('\n');
|
|
2191
|
-
if (lines.length === 1 && lines[0] === '')
|
|
2192
|
-
lines = [];
|
|
2193
|
-
const header = `[${table}]`;
|
|
2194
|
-
const start = lines.findIndex((x) => x.trim() === header);
|
|
2195
|
-
const blockLines = String(block || '').trim().split('\n');
|
|
2196
|
-
if (start === -1)
|
|
2197
|
-
return [...lines, ...(lines.length ? [''] : []), ...blockLines].join('\n').replace(/\n{3,}/g, '\n\n');
|
|
2198
|
-
let end = lines.length;
|
|
2199
|
-
for (let i = start + 1; i < lines.length; i++) {
|
|
2200
|
-
const ln = lines[i];
|
|
2201
|
-
if (ln === undefined)
|
|
2202
|
-
continue;
|
|
2203
|
-
if (/^\s*\[.+\]\s*$/.test(ln)) {
|
|
2204
|
-
end = i;
|
|
2205
|
-
break;
|
|
2206
|
-
}
|
|
2207
|
-
}
|
|
2208
|
-
lines.splice(start, end - start, ...blockLines);
|
|
2209
|
-
return lines.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
2210
|
-
}
|
|
2211
2026
|
function shellSingleQuote(value) {
|
|
2212
2027
|
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
2213
2028
|
}
|
|
@@ -3400,14 +3215,14 @@ export async function selftestCodexLb(tmp) {
|
|
|
3400
3215
|
});
|
|
3401
3216
|
if (brokenChain.ok || brokenChain.status !== 'previous_response_not_found' || brokenChain.chain_unhealthy !== true)
|
|
3402
3217
|
throw new Error('selftest: codex-lb response chain health check did not detect previous_response_not_found');
|
|
3403
|
-
//
|
|
3404
|
-
//
|
|
3405
|
-
|
|
3406
|
-
// are preserved untouched even though the schema dropped profile tables.
|
|
3407
|
-
const legacyStamps = ['remote_control = true', 'fast_mode_ui = true', 'codex_git_commit = true', 'computer_use = true', 'browser_use = true', 'browser_use_external = true', 'guardian_approval = true', 'tool_suggest = true', 'plugins = true', '[user.fast_mode]', '[profiles.sks-fast-high]', 'fast_default_opt_out = true'];
|
|
3218
|
+
// 0.144 contract: removed feature stamps and legacy fast profile tables are
|
|
3219
|
+
// stripped, while stable Computer Use/Browser/ImageGen/plugin flags remain.
|
|
3220
|
+
const legacyStamps = ['remote_control = true', 'fast_mode_ui = true', 'codex_git_commit = true', '[user.fast_mode]', '[profiles.sks-fast-high]', 'fast_default_opt_out = true'];
|
|
3408
3221
|
const survivingLegacy = legacyStamps.filter((stamp) => codexLbConfig.includes(stamp));
|
|
3409
|
-
|
|
3410
|
-
|
|
3222
|
+
const stableCapabilityStamps = ['computer_use = true', 'browser_use = true', 'browser_use_external = true', 'image_generation = true', 'in_app_browser = true', 'guardian_approval = true', 'tool_suggest = true', 'plugins = true'];
|
|
3223
|
+
const missingStableCapabilities = stableCapabilityStamps.filter((stamp) => !codexLbConfig.includes(stamp));
|
|
3224
|
+
if (!codexLbConfig.includes('hooks = true') || hasDeprecatedCodexHooksFeatureFlag(codexLbConfig) || !codexLbConfig.includes('multi_agent = true') || !codexLbConfig.includes('fast_mode = true') || !codexLbConfig.includes('apps = true') || survivingLegacy.length || missingStableCapabilities.length || !/\[profiles\.custom\][\s\S]*?model_reasoning_effort = "low"/.test(codexLbConfig) || hasTopLevelCodexModeLock(codexLbConfig))
|
|
3225
|
+
throw new Error(`selftest: codex-lb setup did not enforce the current feature-flag contract${survivingLegacy.length ? ` — surviving legacy stamps: ${survivingLegacy.join(', ')}` : ''}${missingStableCapabilities.length ? ` — missing stable capabilities: ${missingStableCapabilities.join(', ')}` : ''}`);
|
|
3411
3226
|
if (!hasCodexUnstableFeatureWarningSuppression(codexLbConfig))
|
|
3412
3227
|
throw new Error('selftest: codex-lb setup did not suppress Codex unstable feature warning');
|
|
3413
3228
|
const codexLbLaunch = `source ${path.join(tmp, '.codex', 'sks-codex-lb.env')} && codex`;
|