sneakoscope 1.0.5 → 1.0.7

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 (42) hide show
  1. package/README.md +60 -2
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/crates/sks-core/src/main.rs +1 -1
  5. package/dist/bin/sks.js +1 -1
  6. package/dist/build-manifest.json +7 -1
  7. package/dist/cli/install-helpers.d.ts +5 -0
  8. package/dist/cli/install-helpers.js +160 -12
  9. package/dist/commands/codex-lb.js +138 -6
  10. package/dist/commands/wiki.d.ts +1 -1
  11. package/dist/core/codex-compat/codex-compat-report.d.ts +7 -0
  12. package/dist/core/codex-compat/codex-compat-report.js +1 -0
  13. package/dist/core/codex-compat/codex-hook-issues.d.ts +20 -0
  14. package/dist/core/codex-compat/codex-hook-issues.js +93 -0
  15. package/dist/core/codex-compat/codex-hook-schema.d.ts +3 -0
  16. package/dist/core/codex-compat/codex-hook-schema.js +7 -4
  17. package/dist/core/codex-compat/codex-hook-semantic-validator.d.ts +5 -1
  18. package/dist/core/codex-compat/codex-hook-semantic-validator.js +84 -71
  19. package/dist/core/codex-compat/codex-hook-warning-detector.d.ts +6 -0
  20. package/dist/core/codex-compat/codex-hook-warning-detector.js +46 -27
  21. package/dist/core/codex-lb/codex-lb-setup.d.ts +65 -0
  22. package/dist/core/codex-lb/codex-lb-setup.js +170 -0
  23. package/dist/core/commands/computer-use-command.js +39 -2
  24. package/dist/core/commands/wiki-command.d.ts +2 -2
  25. package/dist/core/computer-use-live-evidence.d.ts +109 -0
  26. package/dist/core/computer-use-live-evidence.js +276 -0
  27. package/dist/core/computer-use-status.d.ts +31 -0
  28. package/dist/core/computer-use-status.js +65 -0
  29. package/dist/core/evidence/evidence-router.js +10 -0
  30. package/dist/core/fsx.d.ts +1 -1
  31. package/dist/core/fsx.js +1 -1
  32. package/dist/core/proof/evidence-collector.d.ts +1 -1
  33. package/dist/core/proof/route-finalizer.js +29 -2
  34. package/dist/core/triwiki-wrongness/wrongness-cli.d.ts +2 -2
  35. package/dist/core/triwiki-wrongness/wrongness-ledger.js +3 -3
  36. package/dist/core/triwiki-wrongness/wrongness-proof-linker.d.ts +1 -1
  37. package/dist/core/triwiki-wrongness/wrongness-retrieval.d.ts +1 -1
  38. package/dist/core/triwiki-wrongness/wrongness-schema.d.ts +1 -1
  39. package/dist/core/triwiki-wrongness/wrongness-schema.js +17 -2
  40. package/dist/core/version.d.ts +1 -1
  41. package/dist/core/version.js +1 -1
  42. package/package.json +10 -3
package/README.md CHANGED
@@ -4,7 +4,11 @@ Fast legacy-free proof-first Codex trust layer with image-based Voxel TriWiki.
4
4
 
5
5
  Sneakoscope Codex (`sks`) is a Codex CLI/App harness that makes repeatable Codex work auditable.
6
6
 
7
- SKS **1.0.5** seals the Codex trust harness: hook outputs are validated against both vendored OpenAI Codex CLI `rust-v0.131.0` schemas and runtime semantic parser rules, codex-lb setup survives macOS user-session launches through env-file/Keychain/launchctl-aware repair surfaces, and Computer Use is the preferred macOS visual evidence capability when available.
7
+ SKS **1.0.7** is the Ultimate Final Completion seal for the Codex trust harness: Computer Use live evidence is an opt-in, local-only macOS evidence path with explicit `probe_only`, `live_capture_attempted`, `live_capture_success`, and `live_capture_blocked` modes; `codex-lb setup` reports durable persistence versus `process_only_ephemeral` honestly; and docs/release readiness checks block mock/probe/live overclaims.
8
+
9
+ SKS **1.0.6** is the final precision polish for the Codex trust harness: hook compatibility is classified as upstream schema plus an SKS zero-warning strict subset, `sks codex-lb setup` previews and applies the exact choices the user selected, and Computer Use has an optional live smoke surface for macOS capability/evidence status.
10
+
11
+ SKS **1.0.5** sealed the prior trust harness: hook outputs were validated against both vendored OpenAI Codex CLI `rust-v0.131.0` schemas and runtime semantic parser rules, codex-lb setup survived macOS user-session launches through env-file/Keychain/launchctl-aware repair surfaces, and Computer Use became the preferred macOS visual evidence capability when available.
8
12
 
9
13
  SKS **1.0.4** introduced the `rust-v0.131.0` schema snapshot, guided codex-lb setup path, and Computer Use/MAD-SKS separation that 1.0.5 now hardens into release gates.
10
14
 
@@ -18,8 +22,62 @@ Hybrid-free **`1.0.1`** already delivered the CLI entrypoint/router/registry/Tru
18
22
 
19
23
  SKS does not try to clone every other harness. It focuses on one thing: making Codex work auditable, visual-evidence-bound, safety-gated, and reproducible through Completion Proof.
20
24
 
21
- ![Sneakoscope Codex architecture and pipeline](https://raw.githubusercontent.com/mandarange/Sneakoscope-Codex/dev/docs/assets/sneakoscope-architecture-pipeline.jpg)
25
+ ![Sneakoscope Codex Trust Layer](docs/assets/sneakoscope-architecture-pipeline.jpg)
26
+
27
+
28
+ ## 1.0.7 Ultimate Final Completion
29
+
30
+ 1.0.7 does not add a new route, skill, or competing harness. It closes the last trust gaps around live visual evidence, persistence truth, and release documentation.
31
+
32
+ Computer Use live evidence stays optional and explicit:
33
+
34
+ ```bash
35
+ sks computer-use smoke --json
36
+ sks computer-use smoke --real --capture-screenshot --json
37
+ sks computer-use smoke --real --require-real --json
38
+ npm run computer-use:live-evidence
39
+ ```
40
+
41
+ Default smoke is `probe_only` and never attempts screen capture. Real mode records `live_capture_attempted`, `live_capture_success`, or `live_capture_blocked`; if Codex App, macOS permission, or the official Computer Use capture surface is unavailable, SKS writes a structured blocker instead of fabricated evidence. Browser Use evidence and manual screenshots remain separate sources. Computer Use screenshots are local-only by default, carry SHA-256 metadata, and link to Image Voxel only when a mission-local anchor can be made.
42
+
43
+ codex-lb setup now reports the exact persistence truth:
44
+
45
+ ```bash
46
+ sks codex-lb setup --host lb.example.com --api-key-stdin --plan --json
47
+ sks codex-lb setup --host lb.example.com --api-key-stdin --yes --no-env-file --no-keychain --no-launchctl --shell-profile skip --json
48
+ npm run codex-lb:persistence-truth
49
+ ```
50
+
51
+ Durable modes are `durable_env_file`, `durable_keychain`, `durable_launchctl`, and `shell_profile`. If all durable modes are disabled, setup is classified as `process_only_ephemeral`, emits `next_shell_requires_setup_or_env`, and warns that Codex App GUI launches may not see credentials. Use `sks codex-lb setup --write-env-file --keychain --launchctl` to recover durable persistence.
22
52
 
53
+ Documentation truthfulness is now a release invariant:
54
+
55
+ ```bash
56
+ npm run docs:truthfulness
57
+ npm run release:readiness
58
+ ```
59
+
60
+ The hook compatibility surface remains the upstream schema plus the SKS zero-warning strict subset; SKS does not claim to mirror every upstream runtime parser rule or guarantee universal Computer Use availability.
61
+
62
+ ## 1.0.6 Final Precision Polish
63
+
64
+ SKS validates Codex hooks against the OpenAI Codex `rust-v0.131.0` schema and enforces a stricter SKS zero-warning subset. Some fields may be accepted by upstream but are intentionally disallowed by SKS to avoid user-facing hook warnings and release drift. `sks hooks warning-check --json` now reports `schema_violation`, `upstream_semantic_unsupported`, `sks_zero_warning_disallowed`, `legacy_shape`, and `policy_disallowed` category counts.
65
+
66
+ `sks codex-lb setup` is now a two-phase plan/apply wizard. Every question maps to an actual action: provider selection, env file writing, Keychain storage, launchctl sync, shell profile snippets, and health checks.
67
+
68
+ ```bash
69
+ sks codex-lb setup --host lb.example.com --api-key-stdin --plan --json
70
+ sks codex-lb setup --host lb.example.com --api-key-stdin --yes --no-default-provider --no-env-file --json
71
+ npm run codex-lb:setup-truthfulness
72
+ ```
73
+
74
+ Computer Use live validation is optional and opt-in. On macOS, `SKS_TEST_REAL_COMPUTER_USE=1 sks computer-use smoke --real --json` attempts a non-destructive capability/evidence check. If Codex App or macOS denies the capability, SKS records a structured blocker and does not fabricate visual evidence.
75
+
76
+ ```bash
77
+ sks computer-use smoke --json
78
+ SKS_TEST_REAL_COMPUTER_USE=1 sks computer-use smoke --real --json
79
+ npm run computer-use:live-optional
80
+ ```
23
81
 
24
82
  ## 1.0.5 Ultimate Harness Seal
25
83
 
@@ -76,7 +76,7 @@ dependencies = [
76
76
 
77
77
  [[package]]
78
78
  name = "sks-core"
79
- version = "1.0.5"
79
+ version = "1.0.7"
80
80
  dependencies = [
81
81
  "serde_json",
82
82
  ]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "sks-core"
3
- version = "1.0.5"
3
+ version = "1.0.7"
4
4
  edition = "2021"
5
5
 
6
6
  [dependencies]
@@ -4,7 +4,7 @@ use std::io::{self, Read, Seek, SeekFrom};
4
4
  fn main() {
5
5
  let mut args = std::env::args().skip(1);
6
6
  match args.next().as_deref() {
7
- Some("--version") => println!("sks-rs 1.0.5"),
7
+ Some("--version") => println!("sks-rs 1.0.7"),
8
8
  Some("compact-info") => {
9
9
  let mut input = String::new();
10
10
  let _ = io::stdin().read_to_string(&mut input);
package/dist/bin/sks.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const FAST_PACKAGE_VERSION = '1.0.5';
2
+ const FAST_PACKAGE_VERSION = '1.0.7';
3
3
  const args = process.argv.slice(2);
4
4
  try {
5
5
  if (args[0] === '--version' || args[0] === '-v' || args[0] === 'version') {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema": "sks.dist-build.v2",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "typescript": true,
5
5
  "mjs_runtime_files": 0,
6
6
  "files": [
@@ -188,6 +188,8 @@
188
188
  "core/codex-compat/codex-compat-report.js",
189
189
  "core/codex-compat/codex-config-policy.d.ts",
190
190
  "core/codex-compat/codex-config-policy.js",
191
+ "core/codex-compat/codex-hook-issues.d.ts",
192
+ "core/codex-compat/codex-hook-issues.js",
191
193
  "core/codex-compat/codex-hook-output-builders.d.ts",
192
194
  "core/codex-compat/codex-hook-output-builders.js",
193
195
  "core/codex-compat/codex-hook-output-normalizer.d.ts",
@@ -208,6 +210,8 @@
208
210
  "core/codex-lb-circuit.js",
209
211
  "core/codex-lb/codex-lb-env.d.ts",
210
212
  "core/codex-lb/codex-lb-env.js",
213
+ "core/codex-lb/codex-lb-setup.d.ts",
214
+ "core/codex-lb/codex-lb-setup.js",
211
215
  "core/codex-model-guard.d.ts",
212
216
  "core/codex-model-guard.js",
213
217
  "core/commands/autoresearch-command.d.ts",
@@ -282,6 +286,8 @@
282
286
  "core/commands/wiki-command.js",
283
287
  "core/commands/wrongness-command.d.ts",
284
288
  "core/commands/wrongness-command.js",
289
+ "core/computer-use-live-evidence.d.ts",
290
+ "core/computer-use-live-evidence.js",
285
291
  "core/computer-use-status.d.ts",
286
292
  "core/computer-use-status.js",
287
293
  "core/context7-client.d.ts",
@@ -1,3 +1,4 @@
1
+ import { type CodexLbPersistenceSummary } from '../core/codex-lb/codex-lb-setup.js';
1
2
  type CodexLbStatusSnapshot = Awaited<ReturnType<typeof codexLbStatus>>;
2
3
  /** Install-time shim reconciliation; fields vary by `status`. */
3
4
  export type SksPostinstallShimResult = {
@@ -56,6 +57,10 @@ export type CodexLbAuthInstallResult = {
56
57
  export type ConfigureCodexLbResult = {
57
58
  ok?: boolean;
58
59
  status: string;
60
+ plan?: Record<string, unknown>;
61
+ applied_actions?: Array<Record<string, unknown>>;
62
+ drift?: string[];
63
+ persistence?: CodexLbPersistenceSummary;
59
64
  config_path?: string;
60
65
  env_path?: string;
61
66
  metadata_path?: string;
@@ -12,6 +12,7 @@ import { codexLaunchCommand, platformTmuxInstallHint, tmuxReadiness, tmuxReadine
12
12
  import { reconcileCodexAppUpgradeProcesses } from '../core/codex-app.js';
13
13
  import { recordCodexLbHealthEvent } from '../core/codex-lb-circuit.js';
14
14
  import { loadCodexLbEnv, writeCodexLbKeychain, codexLbMetadataPath } from '../core/codex-lb/codex-lb-env.js';
15
+ import { buildCodexLbSetupPlan, codexLbPersistenceSummary, installCodexLbShellProfileSnippet, selectedCodexLbPersistenceModes } from '../core/codex-lb/codex-lb-setup.js';
15
16
  const DEFAULT_CODEX_APP_PLUGINS = [
16
17
  ['browser', 'openai-bundled'],
17
18
  ['chrome', 'openai-bundled'],
@@ -307,8 +308,33 @@ export async function configureCodexLb(opts = {}) {
307
308
  const rawHost = String(opts.host || opts.baseUrl || '');
308
309
  const baseUrl = normalizeCodexLbBaseUrl(rawHost);
309
310
  const apiKey = String(opts.apiKey || '').trim();
311
+ const useDefaultProvider = opts.useDefaultProvider !== false;
312
+ const writeEnvFile = opts.writeEnvFile !== false;
313
+ const storeKeychain = opts.storeKeychain === true || opts.keychain === true;
314
+ const syncLaunchctl = opts.syncLaunchctl !== false && opts.syncLaunchEnv !== false;
315
+ const shellProfile = opts.shellProfile || 'skip';
316
+ const setupAnswers = {
317
+ host_or_base_url: rawHost,
318
+ api_key_source: opts.apiKeySource || 'cli_option',
319
+ use_as_default_provider: useDefaultProvider,
320
+ write_env_file: writeEnvFile,
321
+ store_keychain: storeKeychain,
322
+ sync_launchctl: syncLaunchctl,
323
+ install_shell_profile: shellProfile,
324
+ run_health_check: opts.runHealth === true,
325
+ allow_insecure_localhost: opts.allowInsecureHttp === true || opts.allowInsecureLocalhost === true
326
+ };
327
+ const selectedPersistenceModes = selectedCodexLbPersistenceModes(setupAnswers);
328
+ const plan = buildCodexLbSetupPlan(setupAnswers, {
329
+ home,
330
+ configPath,
331
+ envPath,
332
+ metadataPath: opts.metadataPath || codexLbMetadataPath(home)
333
+ });
310
334
  if (!baseUrl)
311
335
  return { ok: false, status: 'missing_host_or_base_url', config_path: configPath, env_path: envPath };
336
+ if (plan.blockers.length)
337
+ return { ok: false, status: 'plan_blocked', plan: plan, drift: plan.blockers, config_path: configPath, env_path: envPath };
312
338
  if (/[\u0000-\u001f\u007f\s]/.test(rawHost.trim()))
313
339
  return { ok: false, status: 'invalid_host_or_base_url', config_path: configPath, env_path: envPath, error: 'host_or_base_url_contains_whitespace_or_control_character' };
314
340
  if (!apiKey)
@@ -316,12 +342,22 @@ export async function configureCodexLb(opts = {}) {
316
342
  const insecureLocalWarning = /^http:\/\//i.test(baseUrl) && !/^http:\/\/(?:localhost|127\.0\.0\.1|\[::1\])(?::|\/|$)/i.test(baseUrl) && !opts.allowInsecureHttp
317
343
  ? ['codex-lb base URL uses http outside localhost; prefer https or pass an explicit allow flag in the calling surface.']
318
344
  : [];
345
+ const beforeState = await captureCodexLbSetupWriteState({ home, configPath, envPath, shellProfile });
346
+ const appliedActions = [];
319
347
  await ensureDir(path.dirname(configPath));
320
348
  const current = await readText(configPath, '');
321
- const next = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(current, baseUrl));
349
+ const next = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(current, baseUrl, useDefaultProvider));
322
350
  await writeTextAtomic(configPath, next);
323
- await writeTextAtomic(envPath, `export CODEX_LB_BASE_URL=${shellSingleQuote(baseUrl)}\nexport CODEX_LB_API_KEY=${shellSingleQuote(apiKey)}\n`);
324
- await fsp.chmod(envPath, 0o600).catch(() => { });
351
+ appliedActions.push({ type: 'write_config_provider', target: configPath, ok: true });
352
+ if (useDefaultProvider)
353
+ appliedActions.push({ type: 'select_default_provider', target: configPath, ok: true });
354
+ if (writeEnvFile) {
355
+ await writeTextAtomic(envPath, `export CODEX_LB_BASE_URL=${shellSingleQuote(baseUrl)}\nexport CODEX_LB_API_KEY=${shellSingleQuote(apiKey)}\n`);
356
+ await fsp.chmod(envPath, 0o600).catch(() => { });
357
+ appliedActions.push({ type: 'write_env_file', target: envPath, ok: true });
358
+ }
359
+ process.env.CODEX_LB_BASE_URL = baseUrl;
360
+ process.env.CODEX_LB_API_KEY = apiKey;
325
361
  const keyFingerprint = await sha256Text(apiKey);
326
362
  const metadataPath = opts.metadataPath || codexLbMetadataPath(home);
327
363
  await writeTextAtomic(metadataPath, `${JSON.stringify({
@@ -332,23 +368,67 @@ export async function configureCodexLb(opts = {}) {
332
368
  api_key: { redacted: true, sha256: keyFingerprint }
333
369
  }, null, 2)}\n`);
334
370
  await fsp.chmod(metadataPath, 0o600).catch(() => { });
335
- const keychain = opts.keychain ? await writeCodexLbKeychain(apiKey, opts).catch((err) => ({ ok: false, status: 'keychain_store_failed', error: err.message })) : { ok: false, status: 'skipped' };
336
- const codexEnvironment = await syncCodexLbProviderEnvironment({ env_path: envPath, base_url: baseUrl }, { ...opts, home });
371
+ appliedActions.push({ type: 'write_metadata', target: metadataPath, ok: true });
372
+ const keychain = storeKeychain ? await writeCodexLbKeychain(apiKey, opts).catch((err) => ({ ok: false, status: 'keychain_store_failed', error: err.message })) : { ok: false, status: 'skipped' };
373
+ if (storeKeychain)
374
+ appliedActions.push({ type: 'store_keychain', target: 'macOS Keychain service sks-codex-lb', ok: keychain.ok === true, status: keychain.status });
375
+ const codexEnvironment = await syncCodexLbProviderEnvironment({ env_path: envPath, base_url: baseUrl }, { ...opts, home, apiKey, baseUrl, syncLaunchEnv: syncLaunchctl });
376
+ if (syncLaunchctl)
377
+ appliedActions.push({ type: 'sync_launchctl', target: 'macOS launchctl user environment', ok: codexEnvironment.ok === true, status: codexEnvironment.status });
378
+ const shellProfileResult = await installCodexLbShellProfileSnippet({ home, envPath, shellProfile }).catch((err) => ({ ok: false, status: 'failed', files: [], error: err.message }));
379
+ if (shellProfile !== 'skip')
380
+ appliedActions.push({ type: 'install_shell_profile_snippet', target: shellProfileResult.files?.join(', ') || shellProfile, ok: shellProfileResult.ok === true, status: shellProfileResult.status });
337
381
  const codexLogin = await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home, force: true });
338
382
  const codexLb = await codexLbStatus({ ...opts, home, configPath, envPath });
339
383
  const authReconcile = await reconcileCodexLbAuthConflict({ ...opts, home, status: codexLb }).catch((err) => ({ status: 'failed', reason: 'exception', error: err.message }));
340
384
  const finalCodexLb = await codexLbStatus({ ...opts, home, configPath, envPath });
341
385
  const ok = Boolean(codexEnvironment.ok && codexLogin.ok);
386
+ const afterState = await captureCodexLbSetupWriteState({ home, configPath, envPath, shellProfile });
387
+ const drift = detectCodexLbSetupDrift({
388
+ useDefaultProvider,
389
+ writeEnvFile,
390
+ storeKeychain,
391
+ syncLaunchctl,
392
+ shellProfile,
393
+ selected: finalCodexLb.selected,
394
+ envFile: finalCodexLb.env_file,
395
+ keychain,
396
+ codexEnvironment,
397
+ shellProfileResult,
398
+ beforeState,
399
+ afterState
400
+ });
401
+ const appliedPersistenceModes = appliedCodexLbPersistenceModes({
402
+ writeEnvFile,
403
+ storeKeychain,
404
+ syncLaunchctl,
405
+ shellProfile,
406
+ envFile: finalCodexLb.env_file,
407
+ keychain,
408
+ codexEnvironment,
409
+ shellProfileResult,
410
+ apiKeySource: finalCodexLb.env_loader?.api_key?.source || null
411
+ });
412
+ const persistence = codexLbPersistenceSummary({
413
+ selectedModes: selectedPersistenceModes,
414
+ appliedModes: appliedPersistenceModes,
415
+ processOnly: appliedPersistenceModes.includes('process_only_ephemeral')
416
+ });
417
+ const warnings = [...insecureLocalWarning, ...persistence.warnings];
342
418
  return {
343
- ok,
344
- status: ok ? 'configured' : (codexEnvironment.status || codexLogin.status),
419
+ ok: ok && drift.length === 0,
420
+ status: ok && drift.length === 0 ? 'configured' : drift.length ? 'setup_choice_drift' : (codexEnvironment.status || codexLogin.status),
421
+ plan: plan,
422
+ applied_actions: appliedActions,
423
+ drift,
424
+ persistence,
345
425
  config_path: configPath,
346
426
  env_path: envPath,
347
427
  metadata_path: metadataPath,
348
428
  base_url: baseUrl,
349
429
  env_key: 'CODEX_LB_API_KEY',
350
430
  keychain,
351
- warnings: insecureLocalWarning,
431
+ warnings,
352
432
  auth_reconcile: authReconcile,
353
433
  codex_lb: finalCodexLb,
354
434
  codex_environment: codexEnvironment,
@@ -1188,10 +1268,10 @@ async function syncCodexLbProviderEnvironment(status = {}, opts = {}) {
1188
1268
  const home = opts.home || process.env.HOME || os.homedir();
1189
1269
  const envPath = opts.envPath || status.env_path || codexLbEnvPath(home);
1190
1270
  const envText = await readText(envPath, '');
1191
- const apiKey = parseCodexLbEnvKey(envText);
1271
+ const apiKey = String(opts.apiKey || '').trim() || parseCodexLbEnvKey(envText);
1192
1272
  if (!apiKey)
1193
1273
  return { ok: false, status: 'missing_env_key' };
1194
- const baseUrl = status.base_url || parseCodexLbEnvBaseUrl(envText);
1274
+ const baseUrl = status.base_url || opts.baseUrl || parseCodexLbEnvBaseUrl(envText);
1195
1275
  process.env.CODEX_LB_API_KEY = apiKey;
1196
1276
  if (baseUrl)
1197
1277
  process.env.CODEX_LB_BASE_URL = baseUrl;
@@ -1258,8 +1338,10 @@ async function syncCodexApiKeyLogin(apiKey, opts = {}) {
1258
1338
  return { ok: true, status: 'synced' };
1259
1339
  return { ok: false, status: 'login_failed', error: redactSecretText(login.stderr || login.stdout || 'codex login failed', [apiKey]).trim() };
1260
1340
  }
1261
- function upsertCodexLbConfig(text = '', baseUrl) {
1262
- let next = upsertTopLevelTomlString(text, 'model_provider', 'codex-lb');
1341
+ function upsertCodexLbConfig(text = '', baseUrl, selectDefault = true) {
1342
+ let next = selectDefault
1343
+ ? upsertTopLevelTomlString(text, 'model_provider', 'codex-lb')
1344
+ : removeTopLevelTomlKeyIfValue(text, 'model_provider', 'codex-lb');
1263
1345
  const block = [
1264
1346
  '[model_providers.codex-lb]',
1265
1347
  'name = "OpenAI"',
@@ -1272,6 +1354,72 @@ function upsertCodexLbConfig(text = '', baseUrl) {
1272
1354
  next = upsertTomlTable(next, 'model_providers.codex-lb', block);
1273
1355
  return `${next.trim()}\n`;
1274
1356
  }
1357
+ function detectCodexLbSetupDrift(state = {}) {
1358
+ const drift = [];
1359
+ if (state.useDefaultProvider && state.selected !== true)
1360
+ drift.push('default_provider_not_selected');
1361
+ if (!state.useDefaultProvider && state.selected === true)
1362
+ drift.push('default_provider_selected_despite_no_default_provider');
1363
+ if (state.writeEnvFile && state.envFile !== true)
1364
+ drift.push('env_file_not_written');
1365
+ if (!state.writeEnvFile && state.beforeState && state.afterState && state.beforeState.envHash !== state.afterState.envHash)
1366
+ drift.push('env_file_changed_despite_no_env_file');
1367
+ if (!state.writeEnvFile && !state.beforeState && state.envFile === true)
1368
+ drift.push('env_file_written_despite_no_env_file');
1369
+ if (!state.storeKeychain && state.keychain?.status && state.keychain.status !== 'skipped')
1370
+ drift.push('keychain_touched_despite_no_keychain');
1371
+ if (!state.syncLaunchctl && state.codexEnvironment?.launch_environment?.status === 'synced')
1372
+ drift.push('launchctl_synced_despite_no_launchctl');
1373
+ if (state.shellProfile === 'skip' && state.shellProfileResult?.status === 'installed')
1374
+ drift.push('shell_profile_written_despite_skip');
1375
+ if (state.shellProfile === 'skip' && state.beforeState && state.afterState && state.beforeState.profileHash !== state.afterState.profileHash)
1376
+ drift.push('shell_profile_changed_despite_skip');
1377
+ return drift;
1378
+ }
1379
+ async function captureCodexLbSetupWriteState({ home, configPath, envPath, shellProfile } = {}) {
1380
+ const profileFiles = profileFilesForDrift(home, shellProfile);
1381
+ return {
1382
+ configHash: await fileHashOrMissing(configPath),
1383
+ envHash: await fileHashOrMissing(envPath),
1384
+ profileHash: (await Promise.all(profileFiles.map((file) => fileHashOrMissing(file)))).join('|')
1385
+ };
1386
+ }
1387
+ async function fileHashOrMissing(file) {
1388
+ const text = await readText(file, null).catch(() => null);
1389
+ return text === null ? 'missing' : await sha256Text(String(text));
1390
+ }
1391
+ function profileFilesForDrift(home, shellProfile) {
1392
+ const targets = {
1393
+ zsh: path.join(home, '.zshrc'),
1394
+ bash: path.join(home, '.bashrc'),
1395
+ fish: path.join(home, '.config', 'fish', 'config.fish')
1396
+ };
1397
+ if (shellProfile === 'zsh')
1398
+ return [targets.zsh];
1399
+ if (shellProfile === 'bash')
1400
+ return [targets.bash];
1401
+ if (shellProfile === 'fish')
1402
+ return [targets.fish];
1403
+ if (shellProfile === 'all')
1404
+ return [targets.zsh, targets.bash, targets.fish];
1405
+ return [targets.zsh, targets.bash, targets.fish];
1406
+ }
1407
+ function appliedCodexLbPersistenceModes(state = {}) {
1408
+ const modes = [];
1409
+ if (state.writeEnvFile && state.envFile === true)
1410
+ modes.push('durable_env_file');
1411
+ if (state.storeKeychain && state.keychain?.ok === true)
1412
+ modes.push('durable_keychain');
1413
+ if (state.syncLaunchctl && state.codexEnvironment?.launch_environment?.status === 'synced')
1414
+ modes.push('durable_launchctl');
1415
+ if (state.shellProfile !== 'skip' && state.shellProfileResult?.status === 'installed')
1416
+ modes.push('shell_profile');
1417
+ if (!modes.length && state.apiKeySource === 'process.env')
1418
+ modes.push('process_only_ephemeral');
1419
+ if (!modes.length)
1420
+ modes.push('none');
1421
+ return modes;
1422
+ }
1275
1423
  export async function ensureGlobalCodexFastModeDuringInstall(opts = {}) {
1276
1424
  if (process.env.SKS_SKIP_CODEX_FAST_MODE_REPAIR === '1')
1277
1425
  return { status: 'skipped', reason: 'SKS_SKIP_CODEX_FAST_MODE_REPAIR=1' };
@@ -6,6 +6,7 @@ import { flag, readOption } from '../cli/args.js';
6
6
  import { printJson } from '../cli/output.js';
7
7
  import { codexLbMetrics, readCodexLbCircuit, recordCodexLbHealthEvent, resetCodexLbCircuit, codexLbProofEvidence } from '../core/codex-lb-circuit.js';
8
8
  import { checkCodexLbResponseChain, codexLbStatus, configureCodexLb, formatCodexLbStatusText, releaseCodexLbAuthHold, repairCodexLbAuth, unselectCodexLbProvider } from '../cli/install-helpers.js';
9
+ import { buildCodexLbSetupPlan, codexLbPersistenceSummary, renderCodexLbSetupPlan } from '../core/codex-lb/codex-lb-setup.js';
9
10
  export async function run(command, args = []) {
10
11
  const root = await projectRoot();
11
12
  const action = args[0] || 'status';
@@ -77,6 +78,17 @@ export async function run(command, args = []) {
77
78
  }
78
79
  if (action === 'setup' || action === 'reconfigure') {
79
80
  const options = await codexLbSetupOptions(args);
81
+ const plan = buildCodexLbSetupPlan({
82
+ host_or_base_url: options.host || '',
83
+ api_key_source: options.apiKeySource,
84
+ use_as_default_provider: options.useDefaultProvider,
85
+ write_env_file: options.writeEnvFile,
86
+ store_keychain: options.keychain,
87
+ sync_launchctl: options.syncLaunchctl,
88
+ install_shell_profile: options.shellProfile,
89
+ run_health_check: options.health,
90
+ allow_insecure_localhost: options.allowInsecureLocalhost
91
+ });
80
92
  if (!options.host || !options.apiKey) {
81
93
  const result = {
82
94
  schema: 'sks.codex-lb-setup.v1',
@@ -98,13 +110,84 @@ export async function run(command, args = []) {
98
110
  process.exitCode = 1;
99
111
  return;
100
112
  }
101
- const result = await configureCodexLb({ host: options.host, apiKey: options.apiKey, keychain: options.keychain });
113
+ if (flag(args, '--plan')) {
114
+ const result = { schema: 'sks.codex-lb-setup-plan-result.v1', ok: plan.blockers.length === 0, plan, writes: false, expected_actions: plan.expected_actions, persistence: plan.persistence };
115
+ if (flag(args, '--json'))
116
+ return printJson(result);
117
+ process.stdout.write(renderCodexLbSetupPlan(plan));
118
+ if (!result.ok)
119
+ process.exitCode = 1;
120
+ return;
121
+ }
122
+ const processOnly = plan.persistence.effective_mode === 'process_only_ephemeral';
123
+ if (options.interactive && !options.yes) {
124
+ process.stdout.write(renderCodexLbSetupPlan(plan));
125
+ const confirm = (await ask('Apply this codex-lb setup plan? [y/N] ')).trim();
126
+ if (!/^(y|yes|예|네|응)$/i.test(confirm)) {
127
+ const result = { schema: 'sks.codex-lb-setup.v1', ok: false, status: 'cancelled', plan, applied_actions: [] };
128
+ if (flag(args, '--json'))
129
+ return printJson(result);
130
+ console.log('codex-lb setup cancelled.');
131
+ process.exitCode = 1;
132
+ return;
133
+ }
134
+ if (processOnly) {
135
+ const confirmProcessOnly = (await ask('This setup keeps credentials only in the current process. Type process-only to continue: ')).trim();
136
+ if (confirmProcessOnly !== 'process-only') {
137
+ const result = { schema: 'sks.codex-lb-setup.v1', ok: false, status: 'process_only_cancelled', plan, applied_actions: [], persistence: plan.persistence };
138
+ if (flag(args, '--json')) {
139
+ printJson(result);
140
+ process.exitCode = 1;
141
+ return;
142
+ }
143
+ console.log('codex-lb setup cancelled: process-only ephemeral setup was not confirmed.');
144
+ process.exitCode = 1;
145
+ return;
146
+ }
147
+ }
148
+ }
149
+ else if (processOnly && !options.yes) {
150
+ const result = {
151
+ schema: 'sks.codex-lb-setup.v1',
152
+ ok: false,
153
+ status: 'process_only_requires_yes',
154
+ plan,
155
+ applied_actions: [],
156
+ persistence: plan.persistence,
157
+ guidance: ['Pass --yes to acknowledge process_only_ephemeral setup, or enable --write-env-file, --keychain, --launchctl, or --shell-profile.']
158
+ };
159
+ if (flag(args, '--json')) {
160
+ printJson(result);
161
+ process.exitCode = 1;
162
+ return;
163
+ }
164
+ console.error('codex-lb setup would be process-only ephemeral. Pass --yes to acknowledge, or enable a durable persistence mode.');
165
+ process.exitCode = 1;
166
+ return;
167
+ }
168
+ const result = await configureCodexLb({
169
+ host: options.host,
170
+ apiKey: options.apiKey,
171
+ keychain: options.keychain,
172
+ storeKeychain: options.keychain,
173
+ useDefaultProvider: options.useDefaultProvider,
174
+ writeEnvFile: options.writeEnvFile,
175
+ syncLaunchctl: options.syncLaunchctl,
176
+ shellProfile: options.shellProfile,
177
+ runHealth: options.health,
178
+ apiKeySource: options.apiKeySource,
179
+ allowInsecureHttp: options.allowInsecureLocalhost
180
+ });
102
181
  const shaped = { schema: 'sks.codex-lb-setup.v1', ...result, api_key: { present: Boolean(options.apiKey), redacted: true }, env_file_chmod: '0600' };
182
+ if (options.health)
183
+ shaped.applied_actions = [...(shaped.applied_actions || []), { type: 'run_health_check', target: 'codex-lb response chain', ok: true }];
103
184
  if (options.health)
104
185
  shaped.chain_health = result.ok ? await checkCodexLbResponseChain(result, { force: true, root }) : null;
105
186
  if (flag(args, '--json'))
106
187
  return printJson(shaped);
107
188
  console.log(`codex-lb configured: ${result.base_url || result.status}`);
189
+ if (shaped.persistence?.warning)
190
+ console.log(`warning: ${shaped.persistence.warning}`);
108
191
  if (!result.ok)
109
192
  process.exitCode = 1;
110
193
  return;
@@ -143,6 +226,18 @@ export async function run(command, args = []) {
143
226
  process.exitCode = 1;
144
227
  }
145
228
  function shapeCodexLbStatus(status = {}) {
229
+ const mode = status.env_loader?.api_key?.source === 'env-file'
230
+ ? 'durable_env_file'
231
+ : status.env_loader?.api_key?.source === 'keychain'
232
+ ? 'durable_keychain'
233
+ : status.env_loader?.api_key?.source === 'process.env'
234
+ ? 'process_only_ephemeral'
235
+ : 'none';
236
+ const persistence = codexLbPersistenceSummary({
237
+ selectedModes: mode === 'none' ? [] : [mode],
238
+ appliedModes: mode === 'none' ? ['none'] : [mode],
239
+ processOnly: mode === 'process_only_ephemeral'
240
+ });
146
241
  return {
147
242
  schema: 'sks.codex-lb-status.v1',
148
243
  ...status,
@@ -154,6 +249,7 @@ function shapeCodexLbStatus(status = {}) {
154
249
  source: status.env_loader?.api_key?.source || null,
155
250
  redacted: true
156
251
  },
252
+ persistence,
157
253
  env_loader: status.env_loader || null,
158
254
  env_auto_load: Boolean(status.env_file && status.env_key_configured),
159
255
  guidance: status.ok ? [] : [
@@ -167,22 +263,58 @@ async function codexLbSetupOptions(args = []) {
167
263
  const baseUrl = readOption(args, '--base-url', null);
168
264
  let host = baseUrl || readOption(args, '--host', readOption(args, '--domain', null));
169
265
  let apiKey = readOption(args, '--api-key', readOption(args, '--key', null));
266
+ let apiKeySource = apiKey ? 'cli_option' : 'hidden_prompt';
170
267
  let keychain = flag(args, '--keychain');
171
268
  if (flag(args, '--api-key-stdin'))
172
269
  apiKey = (await readStdin()).trim();
173
- let health = flag(args, '--health') || flag(args, '--check');
270
+ if (flag(args, '--api-key-stdin'))
271
+ apiKeySource = 'stdin';
272
+ let health = (flag(args, '--health') || flag(args, '--check')) && !flag(args, '--no-health');
273
+ let useDefaultProvider = flag(args, '--no-default-provider') ? false : true;
274
+ if (flag(args, '--use-default-provider'))
275
+ useDefaultProvider = true;
276
+ let writeEnvFile = flag(args, '--no-env-file') ? false : true;
277
+ if (flag(args, '--write-env-file'))
278
+ writeEnvFile = true;
279
+ if (flag(args, '--no-keychain'))
280
+ keychain = false;
281
+ let syncLaunchctl = flag(args, '--no-launchctl') ? false : true;
282
+ if (flag(args, '--launchctl'))
283
+ syncLaunchctl = true;
284
+ const shellProfile = normalizeShellProfile(readOption(args, '--shell-profile', 'skip'));
285
+ const allowInsecureLocalhost = flag(args, '--allow-insecure-localhost') || flag(args, '--allow-insecure-http');
286
+ const interactive = (!host || !apiKey || canAskInteractive(args)) && canAskInteractive(args);
174
287
  if ((!host || !apiKey) && canAskInteractive(args)) {
175
288
  console.log('SKS codex-lb setup\n');
176
289
  host ||= (await ask('1. codex-lb domain or base URL?\n Example: lb.example.com or https://lb.example.com/backend-api/codex\n> ')).trim();
177
290
  apiKey ||= (await askHidden('2. API key?\n Input hidden. Value will be stored securely and never printed.\n> ')).trim();
178
- await ask('3. Use this codex-lb as default for Codex launches? [Y/n] ');
179
- await ask('4. Write shell env loader to ~/.codex/sks-codex-lb.env? [Y/n] ');
291
+ apiKeySource = 'hidden_prompt';
292
+ useDefaultProvider = parseYesNo(await ask('3. Use this codex-lb as default for Codex launches? [Y/n] '), true);
293
+ writeEnvFile = parseYesNo(await ask('4. Write shell env loader to ~/.codex/sks-codex-lb.env? [Y/n] '), true);
180
294
  const storeKeychain = (await ask('5. Store the key in macOS Keychain when available? [Y/n] ')).trim();
181
295
  keychain = !/^(n|no|아니|아니요|ㄴ)$/i.test(storeKeychain || 'y');
182
- const runHealth = (await ask('6. Run health check now? [Y/n] ')).trim();
296
+ syncLaunchctl = parseYesNo(await ask('6. Sync macOS launchctl environment when available? [Y/n] '), true);
297
+ const profile = (await ask('7. Install shell profile snippet? [zsh/bash/fish/all/skip] ')).trim();
298
+ const interactiveShellProfile = normalizeShellProfile(profile || 'skip');
299
+ const runHealth = (await ask('8. Run health check now? [Y/n] ')).trim();
183
300
  health = !/^(n|no|아니|아니요|ㄴ)$/i.test(runHealth || 'y');
301
+ return { host, apiKey, health, keychain, useDefaultProvider, writeEnvFile, syncLaunchctl, shellProfile: interactiveShellProfile, allowInsecureLocalhost, apiKeySource, interactive: true, yes: flag(args, '--yes') };
184
302
  }
185
- return { host, apiKey, health, keychain };
303
+ return { host, apiKey, health, keychain, useDefaultProvider, writeEnvFile, syncLaunchctl, shellProfile, allowInsecureLocalhost, apiKeySource, interactive, yes: flag(args, '--yes') };
304
+ }
305
+ function normalizeShellProfile(value) {
306
+ const raw = String(value || 'skip').toLowerCase();
307
+ return raw === 'zsh' || raw === 'bash' || raw === 'fish' || raw === 'all' ? raw : 'skip';
308
+ }
309
+ function parseYesNo(value, fallback) {
310
+ const raw = String(value || '').trim();
311
+ if (!raw)
312
+ return fallback;
313
+ if (/^(y|yes|예|네|응)$/i.test(raw))
314
+ return true;
315
+ if (/^(n|no|아니|아니요|ㄴ)$/i.test(raw))
316
+ return false;
317
+ return fallback;
186
318
  }
187
319
  function canAskInteractive(args = []) {
188
320
  return !flag(args, '--json') && !flag(args, '--yes') && Boolean(input.isTTY && output.isTTY && process.env.CI !== 'true');
@@ -15,7 +15,7 @@ export declare function run(_command: any, args?: any): Promise<void | {
15
15
  };
16
16
  active_records: {
17
17
  id: string;
18
- kind: "incorrect_claim" | "overconfident_claim" | "stale_evidence" | "missing_evidence" | "test_failure" | "route_misclassification" | "scout_error" | "visual_anchor_error" | "image_bbox_error" | "db_safety_false_positive" | "db_safety_false_negative" | "hook_policy_mismatch" | "hook_semantic_mismatch" | "codex_lb_health_misread" | "codex_lb_missing_env_raw_message" | "computer_use_policy_misclassification" | "mock_real_confusion" | "user_intent_misread" | "artifact_schema_error" | "trust_status_overclaim";
18
+ kind: "incorrect_claim" | "overconfident_claim" | "stale_evidence" | "missing_evidence" | "test_failure" | "route_misclassification" | "scout_error" | "visual_anchor_error" | "image_bbox_error" | "db_safety_false_positive" | "db_safety_false_negative" | "hook_policy_mismatch" | "hook_semantic_mismatch" | "hook_strict_subset_misclassified" | "codex_lb_health_misread" | "codex_lb_missing_env_raw_message" | "codex_lb_setup_choice_drift" | "codex_lb_env_persistence_failure" | "computer_use_policy_misclassification" | "computer_use_live_smoke_mismatch" | "computer_use_external_block_overclaimed" | "mock_real_confusion" | "user_intent_misread" | "artifact_schema_error" | "trust_status_overclaim";
19
19
  severity: "high" | "low" | "medium" | "critical";
20
20
  route: string | null;
21
21
  claim: string;