sneakoscope 6.0.3 → 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.
Files changed (219) hide show
  1. package/README.md +3 -3
  2. package/config/codex-releases/{rust-v0.142.0.json → rust-v0.144.1.json} +6 -6
  3. package/crates/sks-core/Cargo.lock +1 -1
  4. package/crates/sks-core/Cargo.toml +1 -1
  5. package/dist/bin/fast-inline.js +0 -5
  6. package/dist/bin/sks-dispatch.js +0 -2
  7. package/dist/cli/install-helpers.js +240 -460
  8. package/dist/cli/router.js +53 -4
  9. package/dist/commands/codex-lb.js +140 -36
  10. package/dist/commands/codex.js +6 -5
  11. package/dist/commands/doctor.js +60 -26
  12. package/dist/commands/versioning.js +7 -0
  13. package/dist/config/skills-manifest.json +57 -57
  14. package/dist/core/agents/agent-central-ledger.js +9 -0
  15. package/dist/core/agents/agent-cleanup-executor.js +334 -41
  16. package/dist/core/agents/agent-cleanup.js +20 -1
  17. package/dist/core/agents/agent-command-surface.js +2 -1
  18. package/dist/core/agents/agent-janitor.js +9 -10
  19. package/dist/core/agents/agent-namespace-safety.js +56 -0
  20. package/dist/core/agents/agent-orchestrator.js +25 -3
  21. package/dist/core/agents/agent-output-validator.js +1 -1
  22. package/dist/core/agents/agent-role-config.js +3 -6
  23. package/dist/core/agents/agent-runner-codex-exec.js +2 -0
  24. package/dist/core/agents/agent-runner-process.js +1 -0
  25. package/dist/core/agents/fast-mode-policy.js +3 -5
  26. package/dist/core/agents/native-worker-backend-router.js +2 -1
  27. package/dist/core/agents/work-partition/repo-inventory.js +68 -7
  28. package/dist/core/code-structure.js +5 -4
  29. package/dist/core/codex/codex-config-guard.js +31 -9
  30. package/dist/core/codex-app/codex-app-fast-ui-repair.js +51 -20
  31. package/dist/core/codex-app/codex-app-ui-state-snapshot.js +11 -3
  32. package/dist/core/codex-app/sks-menubar.js +8 -3
  33. package/dist/core/codex-app.js +35 -14
  34. package/dist/core/codex-compat/codex-hook-warning-detector.js +10 -1
  35. package/dist/core/codex-compat/codex-release-manifest.js +6 -6
  36. package/dist/core/codex-control/codex-0139-doctor-real-probe.js +7 -1
  37. package/dist/core/codex-control/codex-0139-image-path-real-probe.js +14 -8
  38. package/dist/core/codex-control/codex-0139-probe-runner.js +103 -28
  39. package/dist/core/codex-control/codex-0139-real-probe-summary.js +7 -3
  40. package/dist/core/codex-control/codex-0139-real-probes.js +21 -6
  41. package/dist/core/codex-control/codex-0139-sandbox-real-probe.js +39 -13
  42. package/dist/core/codex-control/codex-0139-web-search-probe.js +11 -8
  43. package/dist/core/codex-control/{codex-0142-capability.js → codex-0144-capability.js} +22 -22
  44. package/dist/core/codex-control/codex-0144-collab-agent-real-probe.js +63 -0
  45. package/dist/core/codex-control/codex-multi-agent-event-normalizer.js +5 -1
  46. package/dist/core/codex-control/codex-reliability-shield.js +4 -1
  47. package/dist/core/codex-control/codex-sdk-adapter.js +53 -6
  48. package/dist/core/codex-control/codex-sdk-config-policy.js +2 -1
  49. package/dist/core/codex-control/codex-sdk-sandbox-policy.js +6 -3
  50. package/dist/core/codex-lb/codex-lb-env.js +29 -0
  51. package/dist/core/codex-lb/codex-lb-tool-catalog.js +478 -0
  52. package/dist/core/codex-native/codex-native-feature-broker.js +22 -22
  53. package/dist/core/codex-native/codex-native-feature-matrix.js +2 -2
  54. package/dist/core/codex-native/native-capability-postcheck.js +16 -8
  55. package/dist/core/codex-native/native-capability-repair-matrix.js +76 -27
  56. package/dist/core/codex-plugins/codex-plugin-json.js +67 -21
  57. package/dist/core/codex-plugins/codex-plugin-repair.js +118 -0
  58. package/dist/core/codex-runtime/codex-desktop-config-policy.js +306 -0
  59. package/dist/core/commands/agent-command.js +19 -0
  60. package/dist/core/commands/basic-cli.js +30 -24
  61. package/dist/core/commands/gc-command.js +27 -4
  62. package/dist/core/commands/image-ux-review-command.js +55 -24
  63. package/dist/core/commands/mad-sks-command.js +152 -132
  64. package/dist/core/commands/naruto-command.js +5 -2
  65. package/dist/core/commands/pipeline-command.js +1 -0
  66. package/dist/core/commands/wiki-command.js +179 -83
  67. package/dist/core/computer-use-live-evidence.js +166 -3
  68. package/dist/core/computer-use-status.js +5 -2
  69. package/dist/core/daemon/sksd-hook-daemon-entrypoint.js +2 -2
  70. package/dist/core/daemon/sksd-hook-daemon.js +171 -28
  71. package/dist/core/daemon/sksd-hook-dispatch.js +2 -2
  72. package/dist/core/dfix/verification-selector.js +6 -4
  73. package/dist/core/doctor/browser-use-repair.js +54 -13
  74. package/dist/core/doctor/codex-0139-doctor.js +3 -2
  75. package/dist/core/doctor/computer-use-repair.js +28 -39
  76. package/dist/core/doctor/doctor-codex-startup-repair.js +19 -9
  77. package/dist/core/doctor/doctor-context7-repair.js +1 -1
  78. package/dist/core/doctor/doctor-dirty-planner.js +1 -1
  79. package/dist/core/doctor/doctor-native-capability-repair.js +74 -3
  80. package/dist/core/doctor/imagegen-repair.js +47 -12
  81. package/dist/core/doctor/legacy-global-hook-cleanup.js +179 -0
  82. package/dist/core/feature-registry.js +25 -31
  83. package/dist/core/fsx.js +54 -6
  84. package/dist/core/harness-conflicts.js +1 -1
  85. package/dist/core/hooks-runtime/code-pack-freshness-preflight.js +5 -4
  86. package/dist/core/hooks-runtime/hook-invocation-dedupe.js +147 -0
  87. package/dist/core/hooks-runtime/hook-io.js +6 -0
  88. package/dist/core/hooks-runtime.js +17 -2
  89. package/dist/core/image/image-artifact-path-contract.js +8 -6
  90. package/dist/core/image/image-artifact-registry.js +5 -1
  91. package/dist/core/image-ux-review/imagegen-adapter.js +5 -2
  92. package/dist/core/imagegen/imagegen-capability.js +10 -13
  93. package/dist/core/imagegen/require-imagegen.js +37 -11
  94. package/dist/core/managed-assets/managed-assets-manifest.js +1 -5
  95. package/dist/core/naruto/naruto-work-graph.js +70 -4
  96. package/dist/core/ops/reporting.js +3 -0
  97. package/dist/core/pipeline-internals/runtime-core.js +12 -7
  98. package/dist/core/pipeline-internals/runtime-gates.js +17 -1
  99. package/dist/core/ppt/style-tokens.js +1 -1
  100. package/dist/core/ppt-review/index.js +1 -1
  101. package/dist/core/ppt-review/slide-exporter.js +23 -17
  102. package/dist/core/ppt-review/slide-imagegen-review.js +1 -1
  103. package/dist/core/ppt-review/slide-issue-extraction.js +4 -2
  104. package/dist/core/product-design-plugin.js +2 -2
  105. package/dist/core/proof/route-finalizer.js +1 -1
  106. package/dist/core/proof-field.js +3 -3
  107. package/dist/core/provider/model-router.js +5 -1
  108. package/dist/core/release/gate-manifest.js +18 -12
  109. package/dist/core/release/gate-pack-manifest.js +1 -1
  110. package/dist/core/release/release-gate-affected-selector.js +1 -1
  111. package/dist/core/release/release-gate-dag.js +43 -3
  112. package/dist/core/release/release-gate-hermetic-env.js +28 -17
  113. package/dist/core/release/release-real-contract.js +416 -0
  114. package/dist/core/retention/retention-budget.js +25 -6
  115. package/dist/core/retention.js +980 -101
  116. package/dist/core/routes/evidence.js +4 -4
  117. package/dist/core/routes/ppt-policy.js +1 -1
  118. package/dist/core/routes.js +4 -3
  119. package/dist/core/secret-redaction.js +4 -0
  120. package/dist/core/triwiki/code-pack.js +8 -2
  121. package/dist/core/triwiki/triwiki-cache-key.js +108 -29
  122. package/dist/core/triwiki-attention.js +13 -6
  123. package/dist/core/triwiki-provenance.js +274 -0
  124. package/dist/core/triwiki-runtime.js +21 -7
  125. package/dist/core/triwiki-wrongness/wrongness-ledger.js +178 -59
  126. package/dist/core/triwiki-wrongness/wrongness-retrieval.js +11 -2
  127. package/dist/core/triwiki-wrongness/wrongness-trust-policy.js +2 -0
  128. package/dist/core/update-check.js +23 -15
  129. package/dist/core/version.js +1 -1
  130. package/dist/core/wiki-coordinate.js +128 -12
  131. package/dist/core/work-order-ledger.js +168 -9
  132. package/dist/core/zellij/zellij-command.js +12 -1
  133. package/dist/core/zellij/zellij-dashboard-pane.js +3 -3
  134. package/dist/core/zellij/zellij-launcher.js +15 -14
  135. package/dist/core/zellij/zellij-pane-proof.js +32 -6
  136. package/dist/core/zellij/zellij-worker-pane-manager.js +5 -3
  137. package/dist/scripts/agent-fast-mode-default-check.js +48 -29
  138. package/dist/scripts/agent-role-config-repair-check.js +25 -4
  139. package/dist/scripts/agent-wiki-context-proof-check.js +16 -2
  140. package/dist/scripts/all-feature-deep-completion-check.js +14 -4
  141. package/dist/scripts/build-dist.js +17 -5
  142. package/dist/scripts/canonical-test-runner.js +188 -0
  143. package/dist/scripts/check-command-module-budget.js +38 -16
  144. package/dist/scripts/check-legacy-free.js +1 -1
  145. package/dist/scripts/codex-0139-interrupt-agent-check.js +5 -1
  146. package/dist/scripts/{codex-0142-app-server-v2-check.js → codex-0144-app-server-v2-check.js} +11 -10
  147. package/dist/scripts/{codex-0142-binary-identity-check.js → codex-0144-binary-identity-check.js} +4 -4
  148. package/dist/scripts/codex-0144-capability-check.js +21 -0
  149. package/dist/scripts/{codex-0139-real-probes-check.js → codex-0144-core-real-probes-check.js} +19 -5
  150. package/dist/scripts/{codex-0142-doctor-wiring-check.js → codex-0144-doctor-wiring-check.js} +6 -6
  151. package/dist/scripts/{codex-0142-manifest-check.js → codex-0144-manifest-check.js} +11 -3
  152. package/dist/scripts/{codex-0142-policy-check.js → codex-0144-policy-check.js} +2 -2
  153. package/dist/scripts/{codex-0142-thread-store-check.js → codex-0144-thread-store-check.js} +3 -3
  154. package/dist/scripts/codex-app-provider-model-ui-check.js +53 -19
  155. package/dist/scripts/codex-app-ui-preservation-check.js +35 -4
  156. package/dist/scripts/codex-control-side-effect-scope-check.js +2 -2
  157. package/dist/scripts/codex-effort-auto-discovery-check.js +1 -1
  158. package/dist/scripts/codex-lb-fast-mode-truth-check.js +1 -0
  159. package/dist/scripts/{codex-lb-gpt55-fast-profile-check.js → codex-lb-gpt56-fast-profile-check.js} +56 -5
  160. package/dist/scripts/codex-model-metadata-check.js +1 -1
  161. package/dist/scripts/codex-sdk-sandbox-policy-check.js +5 -1
  162. package/dist/scripts/codex-sdk-version-compat-check.js +2 -1
  163. package/dist/scripts/dfix-fixture-check.js +1 -1
  164. package/dist/scripts/docs-truthfulness-check.js +1 -1
  165. package/dist/scripts/doctor-codex-startup-repair-check.js +35 -10
  166. package/dist/scripts/doctor-context7-repair-check.js +2 -1
  167. package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +34 -7
  168. package/dist/scripts/doctor-imagegen-repair-check.js +14 -4
  169. package/dist/scripts/doctor-startup-config-repair-blackbox.js +1 -1
  170. package/dist/scripts/doctor-startup-config-repair-check.js +1 -1
  171. package/dist/scripts/install-update-preserves-config-check.js +1 -1
  172. package/dist/scripts/legacy-gate-inventory-check.js +7 -5
  173. package/dist/scripts/legacy-strong-inventory-check.js +7 -5
  174. package/dist/scripts/legacy-upgrade-matrix-check.js +4 -4
  175. package/dist/scripts/lib/native-cli-session-swarm-check-lib.js +173 -13
  176. package/dist/scripts/mad-sks-app-ui-no-mutation-check.js +166 -6
  177. package/dist/scripts/mutation-callsite-coverage-check.js +0 -11
  178. package/dist/scripts/package-published-contract-check.js +9 -0
  179. package/dist/scripts/postinstall-safe-side-effects-check.js +8 -3
  180. package/dist/scripts/ppt-no-mock-as-real-check.js +3 -1
  181. package/dist/scripts/product-design-plugin-routing-check.js +1 -1
  182. package/dist/scripts/release-check-stamp.js +182 -2
  183. package/dist/scripts/release-dynamic-presets-check.js +8 -0
  184. package/dist/scripts/release-gate-dag-runner-check.js +3 -2
  185. package/dist/scripts/release-gate-dag-runner.js +24 -11
  186. package/dist/scripts/release-gate-script-parity-check.js +1 -1
  187. package/dist/scripts/release-real-check.js +368 -157
  188. package/dist/scripts/release-runtime-truth-matrix-check.js +22 -0
  189. package/dist/scripts/route-proof-artifact-structure-check.js +1 -0
  190. package/dist/scripts/search-visibility-gate-lib.js +1 -1
  191. package/dist/scripts/seo-geo-route-identity-check.js +30 -4
  192. package/dist/scripts/sizecheck.js +2 -2
  193. package/dist/scripts/sks-1-11-gate-lib.js +53 -6
  194. package/dist/scripts/sks-uninstall-regression-check.js +1 -1
  195. package/dist/scripts/sksd-daemon-check.js +16 -6
  196. package/dist/scripts/write-build-manifest.js +2 -1
  197. package/dist/scripts/zellij-layout-valid-check.js +31 -6
  198. package/dist/scripts/zellij-pane-proof-check.js +33 -2
  199. package/dist/scripts/zellij-real-session-cleanup-check.js +122 -2
  200. package/dist/scripts/zellij-real-session-launch-check.js +76 -5
  201. package/dist/scripts/zellij-slot-pane-renderer-check.js +4 -4
  202. package/package.json +25 -15
  203. package/schemas/codex/{app-server-0.142 → app-server-0.144}/codex_app_server_protocol.v2.schemas.json +1146 -781
  204. package/schemas/codex/codex-0139-real-probe-result.schema.json +39 -5
  205. package/schemas/codex/ppt-slide-issue-ledger.schema.json +2 -1
  206. package/dist/core/codex-control/codex-0139-multi-agent-real-probe.js +0 -107
  207. package/dist/scripts/agent-native-release-gate.js +0 -274
  208. package/dist/scripts/codex-0142-capability-check.js +0 -21
  209. package/dist/scripts/command-performance-scorecard-check.js +0 -204
  210. package/dist/scripts/config-managed-merge-callsite-coverage-check.js +0 -212
  211. package/dist/scripts/dollar-performance-scorecard-check.js +0 -257
  212. package/dist/scripts/gate-timing-check.js +0 -16
  213. package/dist/scripts/ops-maturity-scorecard-check.js +0 -12
  214. package/dist/scripts/performance-baseline-check.js +0 -12
  215. package/dist/scripts/performance-improvement-report-check.js +0 -58
  216. package/dist/scripts/release-parallel-check.js +0 -389
  217. package/dist/scripts/release-runner-efficiency-check.js +0 -15
  218. package/dist/scripts/retention-long-run-smoke-check.js +0 -299
  219. 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 { cleanupCodexConfigBackups, validateCodexConfigRoundTrip } from '../core/codex/codex-config-toml.js';
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 = detectCodexLbSetupDrift({
511
- useDefaultProvider,
512
- writeEnvFile,
513
- storeKeychain,
514
- syncLaunchctl,
515
- shellProfile,
516
- selected: finalCodexLb.selected,
517
- envFile: finalCodexLb.env_file,
518
- keychain,
519
- codexEnvironment,
520
- shellProfileResult,
521
- beforeState,
522
- afterState
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 = [...insecureLocalWarning, ...persistence.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
- if (!hasTopLevelCodexLbSelected(current))
1374
- return { status: 'not_selected', config_path: configPath };
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
- const next = ensureTrailingNewline(removeTopLevelTomlString(current, 'model_provider'));
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
- return { status: safeWrite.ok ? 'unselected' : safeWrite.status, config_path: configPath, backup_path: safeWrite.backup_path };
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,7 @@ 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);
1400
1515
  const currentAuthText = await readText(authPath, '');
1401
1516
  const trimmedCurrent = currentAuthText.trim();
1402
1517
  // Repeated "Use ChatGPT OAuth" is idempotent. If OAuth/browser auth is
@@ -1404,21 +1519,20 @@ export async function releaseCodexLbAuthHold(opts = {}) {
1404
1519
  // codex-lb provider is no longer selected.
1405
1520
  const currentAuthMode = codexAuthModeSummary(currentAuthText);
1406
1521
  if (!opts.force && (currentAuthMode.mode === 'chatgpt_oauth' || currentAuthMode.mode === 'browser_marker')) {
1407
- let providerUnselected = false;
1408
- let providerError = null;
1522
+ let provider = { ok: true, provider_unselected: false, provider_status: 'kept', provider_error: null };
1409
1523
  if (!opts.keepProvider) {
1410
1524
  const unselected = await unselectCodexLbProvider({ ...opts, home, configPath });
1411
- if (unselected.status === 'unselected' || unselected.status === 'not_selected')
1412
- providerUnselected = true;
1413
- else if (unselected.status === 'failed')
1414
- providerError = unselected.error || unselected.reason || 'unselect_failed';
1525
+ provider = providerDeselectionOutcome(unselected);
1415
1526
  }
1416
1527
  return {
1417
- status: providerError ? 'failed' : 'already_chatgpt',
1528
+ ok: provider.ok,
1529
+ status: provider.ok ? 'already_chatgpt' : 'failed',
1530
+ ...(provider.ok ? {} : { reason: 'provider_unselect_failed' }),
1418
1531
  auth_path: authPath,
1419
1532
  backup_path: backupPath,
1420
- provider_unselected: providerUnselected,
1421
- provider_error: providerError
1533
+ provider_unselected: provider.provider_unselected,
1534
+ provider_status: provider.provider_status,
1535
+ provider_error: provider.provider_error
1422
1536
  };
1423
1537
  }
1424
1538
  const backupExists = await exists(backupPath);
@@ -1443,21 +1557,20 @@ export async function releaseCodexLbAuthHold(opts = {}) {
1443
1557
  // If auth.json already looks like ChatGPT OAuth (user re-logged in some other way), don't
1444
1558
  // clobber it — but still honor the deselect request so the OAuth blob takes effect.
1445
1559
  if (trimmedCurrent && hasChatgptOAuthTokens(currentAuthText) && !opts.force) {
1446
- let providerUnselected = false;
1447
- let providerError = null;
1560
+ let provider = { ok: true, provider_unselected: false, provider_status: 'kept', provider_error: null };
1448
1561
  if (!opts.keepProvider) {
1449
1562
  const unselected = await unselectCodexLbProvider({ ...opts, home, configPath });
1450
- if (unselected.status === 'unselected')
1451
- providerUnselected = true;
1452
- else if (unselected.status === 'failed')
1453
- providerError = unselected.error || unselected.reason || 'unselect_failed';
1563
+ provider = providerDeselectionOutcome(unselected);
1454
1564
  }
1455
1565
  return {
1456
- status: 'already_chatgpt',
1566
+ ok: provider.ok,
1567
+ status: provider.ok ? 'already_chatgpt' : 'failed',
1568
+ ...(provider.ok ? {} : { reason: 'provider_unselect_failed' }),
1457
1569
  auth_path: authPath,
1458
1570
  backup_path: backupPath,
1459
- provider_unselected: providerUnselected,
1460
- provider_error: providerError
1571
+ provider_unselected: provider.provider_unselected,
1572
+ provider_status: provider.provider_status,
1573
+ provider_error: provider.provider_error
1461
1574
  };
1462
1575
  }
1463
1576
  // Refuse to clobber unfamiliar auth.json shapes unless forced. We expect either an empty file,
@@ -1478,7 +1591,7 @@ export async function releaseCodexLbAuthHold(opts = {}) {
1478
1591
  try {
1479
1592
  await ensureDir(path.dirname(authPath));
1480
1593
  const restored = backupText.endsWith('\n') ? backupText : `${backupText}\n`;
1481
- await writeTextAtomic(authPath, restored);
1594
+ await writeTextAtomic(authPath, restored, { mode: 0o600 });
1482
1595
  await fsp.chmod(authPath, 0o600).catch(() => { });
1483
1596
  }
1484
1597
  catch (err) {
@@ -1491,6 +1604,28 @@ export async function releaseCodexLbAuthHold(opts = {}) {
1491
1604
  provider_unselected: false
1492
1605
  };
1493
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
+ }
1494
1629
  let backupRemoved = false;
1495
1630
  if (opts.deleteBackup) {
1496
1631
  try {
@@ -1498,27 +1633,36 @@ export async function releaseCodexLbAuthHold(opts = {}) {
1498
1633
  backupRemoved = true;
1499
1634
  }
1500
1635
  catch {
1501
- // Non-fatal: the restore already landed.
1636
+ // Non-fatal: the restore and provider deselection already landed.
1502
1637
  }
1503
1638
  }
1504
- let providerUnselected = false;
1505
- let providerError = null;
1506
- if (!opts.keepProvider) {
1507
- const unselected = await unselectCodexLbProvider({ ...opts, home, configPath });
1508
- if (unselected.status === 'unselected')
1509
- providerUnselected = true;
1510
- else if (unselected.status === 'failed')
1511
- providerError = unselected.error || unselected.reason || 'unselect_failed';
1512
- }
1513
1639
  return {
1640
+ ok: true,
1514
1641
  status: 'released',
1515
1642
  auth_path: authPath,
1516
1643
  backup_path: backupPath,
1517
1644
  backup_removed: backupRemoved,
1518
- provider_unselected: providerUnselected,
1519
- provider_error: providerError
1645
+ auth_restored: true,
1646
+ provider_unselected: provider.provider_unselected,
1647
+ provider_status: provider.provider_status,
1648
+ provider_error: provider.provider_error
1520
1649
  };
1521
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
+ }
1522
1666
  export async function maybePromptCodexLbSetupForLaunch(args = [], opts = {}) {
1523
1667
  if (args.includes('--json') || args.includes('--skip-codex-lb') || process.env.SKS_SKIP_CODEX_LB_PROMPT === '1')
1524
1668
  return { status: 'skipped' };
@@ -1550,6 +1694,15 @@ export async function maybePromptCodexLbSetupForLaunch(args = [], opts = {}) {
1550
1694
  if (codexEnvironment.status === 'synced')
1551
1695
  console.log('codex-lb provider auth synced for this user session.');
1552
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
+ }
1553
1706
  const codexLogin = await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home: opts.home || process.env.HOME || os.homedir() });
1554
1707
  if (codexLogin.status === 'synced')
1555
1708
  console.log('codex-lb auth synced with Codex CLI login cache.');
@@ -1560,7 +1713,7 @@ export async function maybePromptCodexLbSetupForLaunch(args = [], opts = {}) {
1560
1713
  // health probe fails. Keep codex-lb active and just warn.
1561
1714
  if (chainHealth.status === 'previous_response_not_found') {
1562
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.');
1563
- 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 };
1564
1717
  }
1565
1718
  // Hard chain failure (auth rejected, timeout, missing base URL, etc.). Don't silently
1566
1719
  // demote a configured codex-lb to ChatGPT OAuth — surface the failure and let the user
@@ -1568,23 +1721,23 @@ export async function maybePromptCodexLbSetupForLaunch(args = [], opts = {}) {
1568
1721
  console.log(`codex-lb response chain check failed (${chainHealth.status}${chainHealth.error ? `: ${chainHealth.error}` : ''}).`);
1569
1722
  if (process.env.SKS_CODEX_LB_AUTOBYPASS === '1') {
1570
1723
  console.log('SKS_CODEX_LB_AUTOBYPASS=1 set; bypassing codex-lb to ChatGPT OAuth for this launch.');
1571
- 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 };
1572
1725
  }
1573
1726
  if (canAskYesNo()) {
1574
1727
  const answer = (await askPostinstallQuestion('Use codex-lb anyway, or fall back to ChatGPT OAuth? [LB/oauth] ')).trim().toLowerCase();
1575
1728
  if (/^(oauth|o|chatgpt|fall ?back|n|no|아니|아니요|ㄴ)$/.test(answer)) {
1576
1729
  console.log('Falling back to ChatGPT OAuth for this launch. Re-enable codex-lb anytime with `sks codex-lb repair`.');
1577
- 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 };
1578
1731
  }
1579
1732
  console.log('Keeping codex-lb active. To switch back to ChatGPT OAuth: `sks codex-lb release`.');
1580
- 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 };
1581
1734
  }
1582
1735
  // Non-interactive context with no opt-out env var. The user explicitly configured codex-lb,
1583
1736
  // so default to keeping it active rather than silently swapping providers.
1584
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.');
1585
- 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 };
1586
1739
  }
1587
- 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 };
1588
1741
  }
1589
1742
  if (!canAskYesNo())
1590
1743
  return { status: 'non_interactive', codex_lb: status };
@@ -1870,379 +2023,6 @@ function appliedCodexLbPersistenceModes(state = {}) {
1870
2023
  modes.push('none');
1871
2024
  return modes;
1872
2025
  }
1873
- export async function ensureGlobalCodexFastModeDuringInstall(opts = {}) {
1874
- if (process.env.SKS_SKIP_CODEX_FAST_MODE_REPAIR === '1')
1875
- return { status: 'skipped', reason: 'SKS_SKIP_CODEX_FAST_MODE_REPAIR=1' };
1876
- const home = opts.home || process.env.HOME || os.homedir();
1877
- const configPath = opts.configPath || codexLbConfigPath(home);
1878
- try {
1879
- await ensureDir(path.dirname(configPath));
1880
- const current = await readText(configPath, '');
1881
- // Safety gate 1: never blind-overwrite an unparseable user config — that would
1882
- // entrench corruption on the file Codex actually loads. Back it up and bail.
1883
- if (current.trim()) {
1884
- const currentSmoke = codexConfigParseSmoke(current);
1885
- if (!currentSmoke.ok) {
1886
- const backupPath = await backupCodexConfig(configPath, current, 'unparseable');
1887
- return { status: 'unparseable_config_preserved', config_path: configPath, backup_path: backupPath, parse_smoke: currentSmoke };
1888
- }
1889
- }
1890
- const next = normalizeCodexFastModeUiConfig(current, {
1891
- forceFastMode: opts.forceFastMode === true,
1892
- forceFastModeOff: opts.forceFastModeOff === true
1893
- });
1894
- if (next === ensureTrailingNewline(current))
1895
- return { status: 'present', config_path: configPath };
1896
- const safeWrite = await safeWriteCodexConfigToml(configPath, current, next, 'codex-fast-mode-install', {
1897
- preserveFastUiKeys: opts.forceFastModeOff !== true
1898
- });
1899
- return {
1900
- status: safeWrite.status === 'written' ? 'updated' : safeWrite.status,
1901
- config_path: configPath,
1902
- backup_path: safeWrite.backup_path,
1903
- parse_smoke: safeWrite.ok ? undefined : safeWrite
1904
- };
1905
- }
1906
- catch (err) {
1907
- return { status: 'failed', config_path: configPath, error: err.message };
1908
- }
1909
- }
1910
- export function normalizeCodexFastModeUiConfig(text = '', opts = {}) {
1911
- // Run to a fixed point so a second install is a true no-op (idempotent). The per-pass
1912
- // table/whitespace normalization converges within one extra pass.
1913
- return normalizeCodexFastModeUiConfigOnce(normalizeCodexFastModeUiConfigOnce(text, opts), opts);
1914
- }
1915
- function normalizeCodexFastModeUiConfigOnce(text = '', opts = {}) {
1916
- // 2026-07 Codex App renewal (ChatGPT desktop merge): the config schema removed
1917
- // `default_profile`, `[profiles.<name>]` tables, `[user.fast_mode]`, and
1918
- // `notice.fast_default_opt_out`. The only documented fast-default mechanism is
1919
- // the plain top-level `service_tier = "fast"`. This pass strips every legacy
1920
- // schema stamps SKS wrote and keeps user-authored top-level choices intact.
1921
- let next = String(text || '');
1922
- next = removeLegacyTopLevelCodexModeLocks(next);
1923
- next = removeTopLevelTomlKeyIfValue(next, 'default_profile', 'sks-fast-high');
1924
- next = removeTomlTable(next, 'user.fast_mode');
1925
- next = removeTomlTable(next, 'profiles.sks-fast-high');
1926
- next = removeTomlTableKey(next, 'notice', 'fast_default_opt_out');
1927
- // Feature flags SKS wrote in earlier versions that are not in the current
1928
- // [features] reference (renewal removed/renamed them). They only trigger the
1929
- // under-development warning now — strip our own stamps.
1930
- for (const legacyFlag of [
1931
- 'codex_hooks', 'remote_control', 'fast_mode_ui', 'codex_git_commit', 'computer_use',
1932
- 'browser_use', 'browser_use_external', 'image_generation', 'in_app_browser',
1933
- 'guardian_approval', 'tool_suggest', 'plugins'
1934
- ]) {
1935
- // Earlier SKS releases wrote these flags as `true`. Remove only that
1936
- // managed legacy value; an explicit user `false` remains authoritative.
1937
- next = removeTomlTableKey(next, 'features', legacyFlag, 'true');
1938
- }
1939
- if (opts.forceFastMode === true) {
1940
- next = upsertTopLevelTomlString(next, 'service_tier', 'fast');
1941
- }
1942
- else if (opts.forceFastModeOff === true) {
1943
- // "off" = remove the tier and let Codex's own default apply. The old code
1944
- // wrote service_tier = "default", which is not a documented tier value.
1945
- next = removeTopLevelTomlKey(next, 'service_tier');
1946
- }
1947
- // Documented, currently-valid feature flags are SET-IF-ABSENT: a fresh config
1948
- // gets SKS's defaults, but SKS never re-enables a feature the user disabled.
1949
- next = upsertTopLevelTomlBooleanIfAbsent(next, 'suppress_unstable_features_warning', true);
1950
- for (const featureLine of ['hooks = true', 'multi_agent = true', 'fast_mode = true', 'apps = true']) {
1951
- next = upsertTomlTableKeyIfAbsent(next, 'features', featureLine);
1952
- }
1953
- if (process.env.SKS_ALLOW_HIGH_AGENT_CONCURRENCY !== '1') {
1954
- next = upsertTomlTableKey(next, 'agents', 'max_threads = 4');
1955
- }
1956
- next = removeTomlTable(next, 'features.multi_agent_v2');
1957
- // Plugin auto-enable is OPT-IN only. Force-writing `[plugins."name@marketplace"] enabled =
1958
- // true` for marketplace plugins the App may not have installed (different build/channel)
1959
- // makes the App reference plugins it cannot load -> broken/blocked plugin UI. It also
1960
- // replaced the user's whole plugin table, reverting any `enabled = false` they set. By
1961
- // default SKS leaves the user's [plugins] alone; opt in with SKS_MANAGE_CODEX_APP_PLUGINS=1.
1962
- if (process.env.SKS_MANAGE_CODEX_APP_PLUGINS === '1') {
1963
- for (const [name, marketplace] of DEFAULT_CODEX_APP_PLUGINS) {
1964
- const table = `plugins."${name}@${marketplace}"`;
1965
- if (!hasTomlTable(next, table))
1966
- next = upsertTomlTable(next, table, `[${table}]\nenabled = true`);
1967
- }
1968
- }
1969
- return ensureTrailingNewline(next);
1970
- }
1971
- function removeTopLevelTomlKey(text = '', key = '') {
1972
- const lines = String(text || '').split('\n');
1973
- const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
1974
- const end = firstTable === -1 ? lines.length : firstTable;
1975
- const keyPattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`);
1976
- return lines.filter((line, index) => index >= end || !keyPattern.test(line)).join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
1977
- }
1978
- function removeTomlTable(text, table) {
1979
- const lines = String(text || '').trimEnd().split('\n');
1980
- const header = `[${table}]`;
1981
- const start = lines.findIndex((x) => x.trim() === header);
1982
- if (start === -1)
1983
- return String(text || '');
1984
- let end = lines.length;
1985
- for (let i = start + 1; i < lines.length; i += 1) {
1986
- const ln = lines[i];
1987
- if (ln !== undefined && /^\s*\[.+\]\s*$/.test(ln)) {
1988
- end = i;
1989
- break;
1990
- }
1991
- }
1992
- return lines.filter((_, index) => index < start || index >= end).join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
1993
- }
1994
- function removeLegacyTopLevelCodexModeLocks(text = '') {
1995
- const lines = String(text || '').split('\n');
1996
- const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
1997
- const end = firstTable === -1 ? lines.length : firstTable;
1998
- return lines.filter((line, index) => {
1999
- if (index >= end)
2000
- return true;
2001
- if (!/^\s*(?:model|model_reasoning_effort)\s*=/.test(line))
2002
- return true;
2003
- return ![line, lines[index - 1] || ''].some((candidate) => {
2004
- const trimmed = String(candidate || '').trim();
2005
- const comment = trimmed.startsWith('#') ? trimmed : trimmed.includes('#') ? trimmed.slice(trimmed.indexOf('#')) : '';
2006
- return /(?:SKS|Sneakoscope|codex-lb|sks fast)/i.test(comment);
2007
- });
2008
- }).join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
2009
- }
2010
- function removeTopLevelTomlKeyIfValue(text = '', key = '', value = '') {
2011
- const lines = String(text || '').split('\n');
2012
- const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
2013
- const end = firstTable === -1 ? lines.length : firstTable;
2014
- const keyPattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*"${escapeRegExp(value)}"\\s*(?:#.*)?$`);
2015
- return lines.filter((line, index) => index >= end || !keyPattern.test(line)).join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
2016
- }
2017
- function removeTomlTableKey(text, table, key, expectedValue = null) {
2018
- const lines = String(text || '').trimEnd().split('\n');
2019
- if (lines.length === 1 && lines[0] === '')
2020
- return '';
2021
- const header = `[${table}]`;
2022
- const start = lines.findIndex((x) => x.trim() === header);
2023
- if (start === -1)
2024
- return String(text || '');
2025
- let end = lines.length;
2026
- for (let i = start + 1; i < lines.length; i += 1) {
2027
- const ln = lines[i];
2028
- if (ln === undefined)
2029
- continue;
2030
- if (/^\s*\[.+\]\s*$/.test(ln)) {
2031
- end = i;
2032
- break;
2033
- }
2034
- }
2035
- const valuePattern = expectedValue === null
2036
- ? ''
2037
- : `\\s*${escapeRegExp(String(expectedValue))}\\s*(?:#.*)?$`;
2038
- const keyPattern = new RegExp(`^\\s*${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*=${valuePattern}`);
2039
- return lines.filter((line, index) => index <= start || index >= end || !keyPattern.test(line)).join('\n').replace(/\n{3,}/g, '\n\n');
2040
- }
2041
- function upsertTomlTableKey(text, table, line) {
2042
- const key = String(line).split('=')[0]?.trim() ?? '';
2043
- const lines = String(text || '').trimEnd().split('\n');
2044
- if (lines.length === 1 && lines[0] === '')
2045
- lines.length = 0;
2046
- const header = `[${table}]`;
2047
- const start = lines.findIndex((x) => x.trim() === header);
2048
- if (start === -1)
2049
- return [...lines, ...(lines.length ? [''] : []), header, line].join('\n').replace(/\n{3,}/g, '\n\n');
2050
- let end = lines.length;
2051
- for (let i = start + 1; i < lines.length; i++) {
2052
- const ln = lines[i];
2053
- if (ln === undefined)
2054
- continue;
2055
- if (/^\s*\[.+\]\s*$/.test(ln)) {
2056
- end = i;
2057
- break;
2058
- }
2059
- }
2060
- const keyRe = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`);
2061
- for (let i = start + 1; i < end; i++) {
2062
- const ln = lines[i];
2063
- if (ln === undefined)
2064
- continue;
2065
- if (keyRe.test(ln)) {
2066
- lines[i] = line;
2067
- return lines.join('\n').replace(/\n{3,}/g, '\n\n');
2068
- }
2069
- }
2070
- if (hasTomlTableKey(lines.join('\n'), table, key))
2071
- return lines.join('\n').replace(/\n{3,}/g, '\n\n');
2072
- lines.splice(end, 0, line);
2073
- return lines.join('\n').replace(/\n{3,}/g, '\n\n');
2074
- }
2075
- // True if [table] already declares `key` (so we never override a user's explicit value).
2076
- function hasTomlTableKey(text, table, key) {
2077
- const lines = String(text || '').split('\n');
2078
- const header = `[${table}]`;
2079
- const start = lines.findIndex((x) => x.trim() === header);
2080
- if (start === -1)
2081
- return false;
2082
- const keyRe = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`);
2083
- for (let i = start + 1; i < lines.length; i += 1) {
2084
- const ln = lines[i];
2085
- if (ln === undefined)
2086
- continue;
2087
- if (/^\s*\[.+\]\s*$/.test(ln))
2088
- break;
2089
- if (keyRe.test(ln))
2090
- return true;
2091
- }
2092
- return false;
2093
- }
2094
- // Set a [table] key only when absent — preserves a Codex App feature the user toggled off
2095
- // (so SKS never re-enables / re-surfaces UI the user hid). On a fresh config the key/table
2096
- // is still created, preserving fresh-install enablement.
2097
- function upsertTomlTableKeyIfAbsent(text, table, line) {
2098
- const key = String(line).split('=')[0]?.trim() ?? '';
2099
- return hasTomlTableKey(text, table, key) ? String(text || '') : upsertTomlTableKey(text, table, line);
2100
- }
2101
- function upsertTopLevelTomlBooleanIfAbsent(text, key, value) {
2102
- return hasTopLevelTomlKey(text, key) ? String(text || '') : upsertTopLevelTomlBoolean(text, key, value);
2103
- }
2104
- function ensureTrailingNewline(text = '') {
2105
- const value = String(text || '').trimEnd();
2106
- return value ? `${value}\n` : '';
2107
- }
2108
- function upsertTopLevelTomlString(text, key, value) {
2109
- const line = `${key} = "${value}"`;
2110
- const lines = String(text || '').split('\n');
2111
- const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
2112
- const end = firstTable === -1 ? lines.length : firstTable;
2113
- for (let i = 0; i < end; i++) {
2114
- const ln = lines[i];
2115
- if (ln === undefined)
2116
- continue;
2117
- if (new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(ln)) {
2118
- lines[i] = line;
2119
- return lines.join('\n').replace(/\n{3,}/g, '\n\n');
2120
- }
2121
- }
2122
- lines.splice(end, 0, line);
2123
- return lines.join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
2124
- }
2125
- function hasTopLevelTomlKey(text, key) {
2126
- const lines = String(text || '').split('\n');
2127
- const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
2128
- const end = firstTable === -1 ? lines.length : firstTable;
2129
- const pattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`);
2130
- for (let i = 0; i < end; i += 1) {
2131
- if (typeof lines[i] === 'string' && pattern.test(lines[i]))
2132
- return true;
2133
- }
2134
- return false;
2135
- }
2136
- // Preserve a user's deliberate top-level scalar (model/service_tier/reasoning); only set
2137
- // the SKS default when the key is ABSENT. This is what stops `npm i -g` from clobbering
2138
- // a user's global Codex config on every update.
2139
- function upsertTopLevelTomlStringIfAbsent(text, key, value) {
2140
- return hasTopLevelTomlKey(text, key) ? String(text || '') : upsertTopLevelTomlString(text, key, value);
2141
- }
2142
- // Lightweight safety gate: detect clearly-broken TOML so we never overwrite (or produce)
2143
- // an unparseable config that Codex itself would reject. Mirrors the project-config smoke.
2144
- function codexConfigParseSmoke(text = '') {
2145
- const str = String(text || '');
2146
- const tripleTokens = (str.match(/"""|'''/g) || []).length;
2147
- const unterminatedTriple = tripleTokens % 2 !== 0;
2148
- const invalidHeader = str.split('\n').find((line) => /^\s*\[/.test(line) && !/^\s*\[\[?[^\]]+\]\]?\s*(?:#.*)?$/.test(line)) || null;
2149
- return { ok: !unterminatedTriple && !invalidHeader, unterminated_multiline_string: unterminatedTriple, invalid_table_header: invalidHeader };
2150
- }
2151
- async function backupCodexConfig(configPath, text, tag) {
2152
- try {
2153
- const stamp = `${PACKAGE_VERSION}-${Date.now().toString(36)}`;
2154
- const backupPath = `${configPath}.sks-${tag}-${stamp}.bak`;
2155
- await writeTextAtomic(backupPath, text);
2156
- await cleanupCodexConfigBackups(configPath, { keepPerTag: 3, maxAgeMs: 30 * 24 * 60 * 60 * 1000 }).catch(() => undefined);
2157
- return backupPath;
2158
- }
2159
- catch {
2160
- return null;
2161
- }
2162
- }
2163
- // Single TOML-safe gate for every codex-lb config write. Mirrors the fast-mode safety so the
2164
- // codex-lb path can NEVER corrupt ~/.codex/config.toml on install (esp. a fresh/initial one):
2165
- // - refuse to overwrite an existing config that is already unparseable (back it up, bail),
2166
- // - refuse to WRITE a result that would not parse (e.g. a regex helper mangled a multiline
2167
- // string), leaving the existing config untouched,
2168
- // - otherwise back up the prior config before mutating.
2169
- export async function safeWriteCodexConfigToml(configPath, current, next, tag = 'codex-lb', opts = {}) {
2170
- return writeCodexConfigGuarded({
2171
- configPath,
2172
- before: String(current || ''),
2173
- cause: tag,
2174
- removeTopLevelModeLocks: true,
2175
- ...(opts.preserveFastUiKeys === undefined ? {} : { preserveFastUiKeys: opts.preserveFastUiKeys }),
2176
- mutate: () => String(next || '')
2177
- });
2178
- }
2179
- export function codexFastModeDesktopStatus(text = '') {
2180
- const validation = validateCodexConfigRoundTrip(String(text || ''));
2181
- // Post-renewal contract: the fast default is the plain top-level
2182
- // service_tier = "fast". default_profile/[profiles.*]/[user.fast_mode] are
2183
- // gone from the Codex config schema and are only reported as legacy_keys.
2184
- const globalOn = validation.ok && validation.service_tier === 'fast';
2185
- return {
2186
- schema: 'sks.codex-fast-mode-desktop-status.v2',
2187
- ok: validation.ok,
2188
- on: Boolean(globalOn),
2189
- service_tier: validation.service_tier ?? null,
2190
- model: validation.model ?? null,
2191
- legacy_keys: validation.legacy_keys,
2192
- validation: {
2193
- ok: validation.ok,
2194
- blockers: validation.blockers,
2195
- parse_error: validation.parse_error || null,
2196
- service_tier: validation.service_tier ?? null,
2197
- model: validation.model ?? null,
2198
- model_reasoning_effort: validation.model_reasoning_effort ?? null,
2199
- legacy_keys: validation.legacy_keys
2200
- }
2201
- };
2202
- }
2203
- function upsertTopLevelTomlBoolean(text, key, value) {
2204
- const line = `${key} = ${value ? 'true' : 'false'}`;
2205
- const lines = String(text || '').split('\n');
2206
- const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
2207
- const end = firstTable === -1 ? lines.length : firstTable;
2208
- for (let i = 0; i < end; i += 1) {
2209
- const ln = lines[i];
2210
- if (ln === undefined)
2211
- continue;
2212
- if (new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(ln)) {
2213
- lines[i] = line;
2214
- return lines.join('\n').replace(/\n{3,}/g, '\n\n');
2215
- }
2216
- }
2217
- lines.splice(end, 0, line);
2218
- return lines.join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
2219
- }
2220
- function hasTomlTable(text, table) {
2221
- const header = `[${table}]`;
2222
- return String(text || '').split('\n').some((line) => String(line).trim() === header);
2223
- }
2224
- function upsertTomlTable(text, table, block) {
2225
- let lines = String(text || '').trimEnd().split('\n');
2226
- if (lines.length === 1 && lines[0] === '')
2227
- lines = [];
2228
- const header = `[${table}]`;
2229
- const start = lines.findIndex((x) => x.trim() === header);
2230
- const blockLines = String(block || '').trim().split('\n');
2231
- if (start === -1)
2232
- return [...lines, ...(lines.length ? [''] : []), ...blockLines].join('\n').replace(/\n{3,}/g, '\n\n');
2233
- let end = lines.length;
2234
- for (let i = start + 1; i < lines.length; i++) {
2235
- const ln = lines[i];
2236
- if (ln === undefined)
2237
- continue;
2238
- if (/^\s*\[.+\]\s*$/.test(ln)) {
2239
- end = i;
2240
- break;
2241
- }
2242
- }
2243
- lines.splice(start, end - start, ...blockLines);
2244
- return lines.join('\n').replace(/\n{3,}/g, '\n\n');
2245
- }
2246
2026
  function shellSingleQuote(value) {
2247
2027
  return `'${String(value).replace(/'/g, `'\\''`)}'`;
2248
2028
  }
@@ -3435,14 +3215,14 @@ export async function selftestCodexLb(tmp) {
3435
3215
  });
3436
3216
  if (brokenChain.ok || brokenChain.status !== 'previous_response_not_found' || brokenChain.chain_unhealthy !== true)
3437
3217
  throw new Error('selftest: codex-lb response chain health check did not detect previous_response_not_found');
3438
- // 2026-07 renewal contract: only documented [features] flags remain; the flags,
3439
- // [user.fast_mode], [profiles.sks-fast-high], and notice.fast_default_opt_out SKS
3440
- // used to write must be STRIPPED by setup. User-authored tables ([profiles.custom])
3441
- // are preserved untouched even though the schema dropped profile tables.
3442
- 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'];
3443
3221
  const survivingLegacy = legacyStamps.filter((stamp) => codexLbConfig.includes(stamp));
3444
- if (!codexLbConfig.includes('hooks = true') || hasDeprecatedCodexHooksFeatureFlag(codexLbConfig) || !codexLbConfig.includes('multi_agent = true') || !codexLbConfig.includes('fast_mode = true') || !codexLbConfig.includes('apps = true') || survivingLegacy.length || !/\[profiles\.custom\][\s\S]*?model_reasoning_effort = "low"/.test(codexLbConfig) || hasTopLevelCodexModeLock(codexLbConfig))
3445
- throw new Error(`selftest: codex-lb setup did not enforce the renewed feature-flag contract (documented flags present, legacy stamps stripped, user profile tables preserved)${survivingLegacy.length ? ` — surviving legacy stamps: ${survivingLegacy.join(', ')}` : ''}`);
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(', ')}` : ''}`);
3446
3226
  if (!hasCodexUnstableFeatureWarningSuppression(codexLbConfig))
3447
3227
  throw new Error('selftest: codex-lb setup did not suppress Codex unstable feature warning');
3448
3228
  const codexLbLaunch = `source ${path.join(tmp, '.codex', 'sks-codex-lb.env')} && codex`;