sneakoscope 1.0.4 → 1.0.6

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 (49) hide show
  1. package/README.md +50 -1
  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 +11 -1
  7. package/dist/cli/install-helpers.d.ts +60 -0
  8. package/dist/cli/install-helpers.js +129 -15
  9. package/dist/commands/codex-lb.js +97 -8
  10. package/dist/commands/wiki.d.ts +1 -1
  11. package/dist/core/codex-compat/codex-compat-report.d.ts +39 -0
  12. package/dist/core/codex-compat/codex-compat-report.js +16 -3
  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-output-builders.d.ts +45 -0
  16. package/dist/core/codex-compat/codex-hook-output-builders.js +122 -0
  17. package/dist/core/codex-compat/codex-hook-output-normalizer.d.ts +1 -1
  18. package/dist/core/codex-compat/codex-hook-output-normalizer.js +25 -48
  19. package/dist/core/codex-compat/codex-hook-schema.d.ts +9 -1
  20. package/dist/core/codex-compat/codex-hook-schema.js +18 -2
  21. package/dist/core/codex-compat/codex-hook-semantic-validator.d.ts +23 -0
  22. package/dist/core/codex-compat/codex-hook-semantic-validator.js +221 -0
  23. package/dist/core/codex-compat/codex-hook-warning-detector.d.ts +7 -0
  24. package/dist/core/codex-compat/codex-hook-warning-detector.js +50 -23
  25. package/dist/core/codex-compat/codex-schema-snapshot.d.ts +1 -0
  26. package/dist/core/codex-compat/codex-schema-snapshot.js +10 -2
  27. package/dist/core/codex-lb/codex-lb-env.d.ts +42 -0
  28. package/dist/core/codex-lb/codex-lb-env.js +150 -0
  29. package/dist/core/codex-lb/codex-lb-setup.d.ts +46 -0
  30. package/dist/core/codex-lb/codex-lb-setup.js +112 -0
  31. package/dist/core/commands/computer-use-command.js +22 -1
  32. package/dist/core/commands/wiki-command.d.ts +2 -2
  33. package/dist/core/computer-use-status.d.ts +24 -0
  34. package/dist/core/computer-use-status.js +46 -0
  35. package/dist/core/fsx.d.ts +1 -1
  36. package/dist/core/fsx.js +1 -1
  37. package/dist/core/hooks-runtime.js +17 -48
  38. package/dist/core/proof/evidence-collector.d.ts +1 -1
  39. package/dist/core/proof/route-finalizer.js +18 -1
  40. package/dist/core/triwiki-wrongness/wrongness-cli.d.ts +2 -2
  41. package/dist/core/triwiki-wrongness/wrongness-ledger.js +6 -5
  42. package/dist/core/triwiki-wrongness/wrongness-proof-linker.d.ts +1 -1
  43. package/dist/core/triwiki-wrongness/wrongness-retrieval.d.ts +1 -1
  44. package/dist/core/triwiki-wrongness/wrongness-schema.d.ts +1 -1
  45. package/dist/core/triwiki-wrongness/wrongness-schema.js +27 -1
  46. package/dist/core/version.d.ts +1 -1
  47. package/dist/core/version.js +1 -1
  48. package/dist/vendor/openai-codex/rust-v0.131.0/hooks/snapshot-metadata.json +2 -0
  49. package/package.json +9 -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.4** targets OpenAI Codex CLI `rust-v0.131.0`: hook outputs are validated against vendored upstream schemas, `sks codex-lb setup` is a guided/redacted setup path, and macOS Computer Use is treated as a Codex App visual evidence capability independent from MAD-SKS.
7
+ 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.
8
+
9
+ 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.
10
+
11
+ 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.
8
12
 
9
13
  SKS **1.0.3** adds git-collaboration hygiene for shared TriWiki memory: `sks git ...`, tracked shared shards under `.sneakoscope/wiki/**`, runtime-only ignores, shared wrongness publish/sync, and Codex App hook trust-state generation for current hook trust syntax.
10
14
 
@@ -19,6 +23,51 @@ SKS does not try to clone every other harness. It focuses on one thing: making C
19
23
  ![Sneakoscope Codex architecture and pipeline](https://raw.githubusercontent.com/mandarange/Sneakoscope-Codex/dev/docs/assets/sneakoscope-architecture-pipeline.jpg)
20
24
 
21
25
 
26
+ ## 1.0.6 Final Precision Polish
27
+
28
+ 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.
29
+
30
+ `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.
31
+
32
+ ```bash
33
+ sks codex-lb setup --host lb.example.com --api-key-stdin --plan --json
34
+ sks codex-lb setup --host lb.example.com --api-key-stdin --yes --no-default-provider --no-env-file --json
35
+ npm run codex-lb:setup-truthfulness
36
+ ```
37
+
38
+ 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.
39
+
40
+ ```bash
41
+ sks computer-use smoke --json
42
+ SKS_TEST_REAL_COMPUTER_USE=1 sks computer-use smoke --real --json
43
+ npm run computer-use:live-optional
44
+ ```
45
+
46
+ ## 1.0.5 Ultimate Harness Seal
47
+
48
+ SKS 1.0.5 treats Codex hook semantic compatibility as stricter than schema compatibility. `sks hooks warning-check --json` and `npm run hooks:semantic-check` fail if an output uses `permissionDecision:"ask"`, PreToolUse `allow` without `updatedInput`, Stop `continue:false`, `stopReason`, `suppressOutput`, snake_case keys, unknown fields, or legacy top-level hook decisions.
49
+
50
+ codex-lb setup now has a durable setup/repair path:
51
+
52
+ ```bash
53
+ sks codex-lb setup --host lb.example.com --api-key-stdin --yes --json
54
+ sks codex-lb status --json
55
+ sks codex-lb doctor --deep --json
56
+ npm run codex-lb:missing-env-regression
57
+ ```
58
+
59
+ The API key is written only to redacted status surfaces. The env file is `~/.codex/sks-codex-lb.env`, metadata is `~/.codex/sks-codex-lb.json`, and reports expose only a redacted presence state plus a fingerprint. Raw `CODEX_LB_API_KEY` missing-env errors are release failures.
60
+
61
+ Computer Use is a Codex App/macOS capability, not a MAD-SKS or DB permission. Visual routes use:
62
+
63
+ ```bash
64
+ sks computer-use status --json
65
+ sks computer-use require --route '$QA-LOOP' --json
66
+ npm run computer-use:visual-route-fixture
67
+ ```
68
+
69
+ If Codex App or macOS blocks the capability, SKS records `external_capability_blocked`, `codex_app_missing`, `macos_permission_missing`, or the closest structured status and does not fabricate UI evidence.
70
+
22
71
  ## 1.0.4 Codex CLI Compatibility
23
72
 
24
73
  SKS 1.0.4 targets OpenAI Codex CLI `rust-v0.131.0`. Hook outputs are validated against vendored upstream schemas, so SKS fails release checks if it emits deprecated hook shapes or unknown fields. `sks codex-lb setup` now guides users through domain/base URL and API key setup, stores secrets securely, and prevents raw `CODEX_LB_API_KEY` missing messages. On macOS, Computer Use is treated as a first-class Codex App visual evidence capability and is never blocked by MAD-SKS or a generic SKS safety policy.
@@ -76,7 +76,7 @@ dependencies = [
76
76
 
77
77
  [[package]]
78
78
  name = "sks-core"
79
- version = "1.0.4"
79
+ version = "1.0.6"
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.4"
3
+ version = "1.0.6"
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.4"),
7
+ Some("--version") => println!("sks-rs 1.0.6"),
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.4';
2
+ const FAST_PACKAGE_VERSION = '1.0.6';
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.4",
3
+ "version": "1.0.6",
4
4
  "typescript": true,
5
5
  "mjs_runtime_files": 0,
6
6
  "files": [
@@ -188,10 +188,16 @@
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",
193
+ "core/codex-compat/codex-hook-output-builders.d.ts",
194
+ "core/codex-compat/codex-hook-output-builders.js",
191
195
  "core/codex-compat/codex-hook-output-normalizer.d.ts",
192
196
  "core/codex-compat/codex-hook-output-normalizer.js",
193
197
  "core/codex-compat/codex-hook-schema.d.ts",
194
198
  "core/codex-compat/codex-hook-schema.js",
199
+ "core/codex-compat/codex-hook-semantic-validator.d.ts",
200
+ "core/codex-compat/codex-hook-semantic-validator.js",
195
201
  "core/codex-compat/codex-hook-warning-detector.d.ts",
196
202
  "core/codex-compat/codex-hook-warning-detector.js",
197
203
  "core/codex-compat/codex-schema-snapshot.d.ts",
@@ -202,6 +208,10 @@
202
208
  "core/codex-compat/codex-version.js",
203
209
  "core/codex-lb-circuit.d.ts",
204
210
  "core/codex-lb-circuit.js",
211
+ "core/codex-lb/codex-lb-env.d.ts",
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",
205
215
  "core/codex-model-guard.d.ts",
206
216
  "core/codex-model-guard.js",
207
217
  "core/commands/autoresearch-command.d.ts",
@@ -56,10 +56,16 @@ export type CodexLbAuthInstallResult = {
56
56
  export type ConfigureCodexLbResult = {
57
57
  ok?: boolean;
58
58
  status: string;
59
+ plan?: Record<string, unknown>;
60
+ applied_actions?: Array<Record<string, unknown>>;
61
+ drift?: string[];
59
62
  config_path?: string;
60
63
  env_path?: string;
64
+ metadata_path?: string;
61
65
  base_url?: string;
62
66
  env_key?: string;
67
+ keychain?: Record<string, unknown>;
68
+ warnings?: string[];
63
69
  auth_reconcile?: CodexLbAuthReconcileResult;
64
70
  codex_lb?: CodexLbStatusSnapshot;
65
71
  codex_environment?: CodexLbEnvSyncResult;
@@ -97,6 +103,24 @@ export declare function codexLbStatus(opts?: any): Promise<{
97
103
  env_file: boolean;
98
104
  env_key_configured: boolean;
99
105
  env_base_url_configured: boolean;
106
+ env_loader: {
107
+ configured: boolean;
108
+ missing: string[];
109
+ source: import("../core/codex-lb/codex-lb-env.js").CodexLbEnvSource;
110
+ source_priority: import("../core/codex-lb/codex-lb-env.js").CodexLbEnvSource[];
111
+ api_key: {
112
+ present: boolean;
113
+ source: import("../core/codex-lb/codex-lb-env.js").CodexLbEnvSource | null;
114
+ redacted: true;
115
+ fingerprint: string | null;
116
+ };
117
+ keychain: {
118
+ checked: boolean;
119
+ available: boolean;
120
+ status: string;
121
+ };
122
+ env_paths: string[];
123
+ };
100
124
  base_url: string | null;
101
125
  auth_path: any;
102
126
  auth_mode: string;
@@ -185,6 +209,24 @@ export declare function maybePromptCodexLbSetupForLaunch(args?: any, opts?: any)
185
209
  env_file: boolean;
186
210
  env_key_configured: boolean;
187
211
  env_base_url_configured: boolean;
212
+ env_loader: {
213
+ configured: boolean;
214
+ missing: string[];
215
+ source: import("../core/codex-lb/codex-lb-env.js").CodexLbEnvSource;
216
+ source_priority: import("../core/codex-lb/codex-lb-env.js").CodexLbEnvSource[];
217
+ api_key: {
218
+ present: boolean;
219
+ source: import("../core/codex-lb/codex-lb-env.js").CodexLbEnvSource | null;
220
+ redacted: true;
221
+ fingerprint: string | null;
222
+ };
223
+ keychain: {
224
+ checked: boolean;
225
+ available: boolean;
226
+ status: string;
227
+ };
228
+ env_paths: string[];
229
+ };
188
230
  base_url: string | null;
189
231
  auth_path: any;
190
232
  auth_mode: string;
@@ -205,6 +247,24 @@ export declare function maybePromptCodexLbSetupForLaunch(args?: any, opts?: any)
205
247
  env_file: boolean;
206
248
  env_key_configured: boolean;
207
249
  env_base_url_configured: boolean;
250
+ env_loader: {
251
+ configured: boolean;
252
+ missing: string[];
253
+ source: import("../core/codex-lb/codex-lb-env.js").CodexLbEnvSource;
254
+ source_priority: import("../core/codex-lb/codex-lb-env.js").CodexLbEnvSource[];
255
+ api_key: {
256
+ present: boolean;
257
+ source: import("../core/codex-lb/codex-lb-env.js").CodexLbEnvSource | null;
258
+ redacted: true;
259
+ fingerprint: string | null;
260
+ };
261
+ keychain: {
262
+ checked: boolean;
263
+ available: boolean;
264
+ status: string;
265
+ };
266
+ env_paths: string[];
267
+ };
208
268
  base_url: string | null;
209
269
  auth_path: any;
210
270
  auth_mode: string;
@@ -11,6 +11,8 @@ import { context7ConfigToml, DOLLAR_SKILL_NAMES, GETDESIGN_REFERENCE, hasContext
11
11
  import { codexLaunchCommand, platformTmuxInstallHint, tmuxReadiness, tmuxReadinessCatchFallback } from '../core/tmux-ui.js';
12
12
  import { reconcileCodexAppUpgradeProcesses } from '../core/codex-app.js';
13
13
  import { recordCodexLbHealthEvent } from '../core/codex-lb-circuit.js';
14
+ import { loadCodexLbEnv, writeCodexLbKeychain, codexLbMetadataPath } from '../core/codex-lb/codex-lb-env.js';
15
+ import { buildCodexLbSetupPlan, installCodexLbShellProfileSnippet } from '../core/codex-lb/codex-lb-setup.js';
14
16
  const DEFAULT_CODEX_APP_PLUGINS = [
15
17
  ['browser', 'openai-bundled'],
16
18
  ['chrome', 'openai-bundled'],
@@ -293,7 +295,7 @@ async function restorePostinstallCodexLbConfigSnapshot(snapshot) {
293
295
  export function normalizeCodexLbBaseUrl(input = '') {
294
296
  let host = String(input || '').trim();
295
297
  if (!host)
296
- host = 'http://127.0.0.1:2455';
298
+ return '';
297
299
  if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(host))
298
300
  host = `https://${host}`;
299
301
  host = host.replace(/\/+$/, '');
@@ -303,29 +305,107 @@ export async function configureCodexLb(opts = {}) {
303
305
  const home = opts.home || process.env.HOME || os.homedir();
304
306
  const configPath = opts.configPath || codexLbConfigPath(home);
305
307
  const envPath = opts.envPath || codexLbEnvPath(home);
306
- const baseUrl = normalizeCodexLbBaseUrl(opts.host || opts.baseUrl);
308
+ const rawHost = String(opts.host || opts.baseUrl || '');
309
+ const baseUrl = normalizeCodexLbBaseUrl(rawHost);
307
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 plan = buildCodexLbSetupPlan(setupAnswers, {
328
+ home,
329
+ configPath,
330
+ envPath,
331
+ metadataPath: opts.metadataPath || codexLbMetadataPath(home)
332
+ });
333
+ if (!baseUrl)
334
+ return { ok: false, status: 'missing_host_or_base_url', config_path: configPath, env_path: envPath };
335
+ if (plan.blockers.length)
336
+ return { ok: false, status: 'plan_blocked', plan: plan, drift: plan.blockers, config_path: configPath, env_path: envPath };
337
+ if (/[\u0000-\u001f\u007f\s]/.test(rawHost.trim()))
338
+ 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' };
308
339
  if (!apiKey)
309
340
  return { ok: false, status: 'missing_api_key', config_path: configPath, env_path: envPath };
341
+ const insecureLocalWarning = /^http:\/\//i.test(baseUrl) && !/^http:\/\/(?:localhost|127\.0\.0\.1|\[::1\])(?::|\/|$)/i.test(baseUrl) && !opts.allowInsecureHttp
342
+ ? ['codex-lb base URL uses http outside localhost; prefer https or pass an explicit allow flag in the calling surface.']
343
+ : [];
344
+ const appliedActions = [];
310
345
  await ensureDir(path.dirname(configPath));
311
346
  const current = await readText(configPath, '');
312
- const next = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(current, baseUrl));
347
+ const next = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(current, baseUrl, useDefaultProvider));
313
348
  await writeTextAtomic(configPath, next);
314
- await writeTextAtomic(envPath, `export CODEX_LB_BASE_URL=${shellSingleQuote(baseUrl)}\nexport CODEX_LB_API_KEY=${shellSingleQuote(apiKey)}\n`);
315
- await fsp.chmod(envPath, 0o600).catch(() => { });
316
- const codexEnvironment = await syncCodexLbProviderEnvironment({ env_path: envPath, base_url: baseUrl }, { ...opts, home });
349
+ appliedActions.push({ type: 'write_config_provider', target: configPath, ok: true });
350
+ if (useDefaultProvider)
351
+ appliedActions.push({ type: 'select_default_provider', target: configPath, ok: true });
352
+ if (writeEnvFile) {
353
+ await writeTextAtomic(envPath, `export CODEX_LB_BASE_URL=${shellSingleQuote(baseUrl)}\nexport CODEX_LB_API_KEY=${shellSingleQuote(apiKey)}\n`);
354
+ await fsp.chmod(envPath, 0o600).catch(() => { });
355
+ appliedActions.push({ type: 'write_env_file', target: envPath, ok: true });
356
+ }
357
+ process.env.CODEX_LB_BASE_URL = baseUrl;
358
+ process.env.CODEX_LB_API_KEY = apiKey;
359
+ const keyFingerprint = await sha256Text(apiKey);
360
+ const metadataPath = opts.metadataPath || codexLbMetadataPath(home);
361
+ await writeTextAtomic(metadataPath, `${JSON.stringify({
362
+ schema: 'sks.codex-lb-metadata.v1',
363
+ base_url: baseUrl,
364
+ updated_at: new Date().toISOString(),
365
+ source: opts.source || 'setup',
366
+ api_key: { redacted: true, sha256: keyFingerprint }
367
+ }, null, 2)}\n`);
368
+ await fsp.chmod(metadataPath, 0o600).catch(() => { });
369
+ appliedActions.push({ type: 'write_metadata', target: metadataPath, ok: true });
370
+ const keychain = storeKeychain ? await writeCodexLbKeychain(apiKey, opts).catch((err) => ({ ok: false, status: 'keychain_store_failed', error: err.message })) : { ok: false, status: 'skipped' };
371
+ if (storeKeychain)
372
+ appliedActions.push({ type: 'store_keychain', target: 'macOS Keychain service sks-codex-lb', ok: keychain.ok === true, status: keychain.status });
373
+ const codexEnvironment = await syncCodexLbProviderEnvironment({ env_path: envPath, base_url: baseUrl }, { ...opts, home, apiKey, baseUrl, syncLaunchEnv: syncLaunchctl });
374
+ if (syncLaunchctl)
375
+ appliedActions.push({ type: 'sync_launchctl', target: 'macOS launchctl user environment', ok: codexEnvironment.ok === true, status: codexEnvironment.status });
376
+ const shellProfileResult = await installCodexLbShellProfileSnippet({ home, envPath, shellProfile }).catch((err) => ({ ok: false, status: 'failed', files: [], error: err.message }));
377
+ if (shellProfile !== 'skip')
378
+ appliedActions.push({ type: 'install_shell_profile_snippet', target: shellProfileResult.files?.join(', ') || shellProfile, ok: shellProfileResult.ok === true, status: shellProfileResult.status });
317
379
  const codexLogin = await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home, force: true });
318
380
  const codexLb = await codexLbStatus({ ...opts, home, configPath, envPath });
319
381
  const authReconcile = await reconcileCodexLbAuthConflict({ ...opts, home, status: codexLb }).catch((err) => ({ status: 'failed', reason: 'exception', error: err.message }));
320
382
  const finalCodexLb = await codexLbStatus({ ...opts, home, configPath, envPath });
321
383
  const ok = Boolean(codexEnvironment.ok && codexLogin.ok);
384
+ const drift = detectCodexLbSetupDrift({
385
+ useDefaultProvider,
386
+ writeEnvFile,
387
+ storeKeychain,
388
+ syncLaunchctl,
389
+ shellProfile,
390
+ selected: finalCodexLb.selected,
391
+ envFile: finalCodexLb.env_file,
392
+ keychain,
393
+ codexEnvironment,
394
+ shellProfileResult
395
+ });
322
396
  return {
323
- ok,
324
- status: ok ? 'configured' : (codexEnvironment.status || codexLogin.status),
397
+ ok: ok && drift.length === 0,
398
+ status: ok && drift.length === 0 ? 'configured' : drift.length ? 'setup_choice_drift' : (codexEnvironment.status || codexLogin.status),
399
+ plan: plan,
400
+ applied_actions: appliedActions,
401
+ drift,
325
402
  config_path: configPath,
326
403
  env_path: envPath,
404
+ metadata_path: metadataPath,
327
405
  base_url: baseUrl,
328
406
  env_key: 'CODEX_LB_API_KEY',
407
+ keychain,
408
+ warnings: insecureLocalWarning,
329
409
  auth_reconcile: authReconcile,
330
410
  codex_lb: finalCodexLb,
331
411
  codex_environment: codexEnvironment,
@@ -340,13 +420,14 @@ export async function codexLbStatus(opts = {}) {
340
420
  const config = await readText(configPath, '');
341
421
  const envExists = await exists(envPath);
342
422
  const envText = envExists ? await readText(envPath, '') : '';
423
+ const envLoad = await loadCodexLbEnv({ ...opts, home, envPath });
343
424
  const authPath = opts.authPath || codexAuthPath(home);
344
425
  const authText = await readText(authPath, '');
345
426
  const authMode = codexAuthModeSummary(authText);
346
- const envKeyConfigured = Boolean(parseCodexLbEnvKey(envText));
427
+ const envKeyConfigured = Boolean(envLoad.api_key.present);
347
428
  const providerConfigured = /\[model_providers\.codex-lb\]/.test(config);
348
429
  const selected = hasTopLevelCodexLbSelected(config);
349
- const baseUrl = codexLbProviderBaseUrl(config) || parseCodexLbEnvBaseUrl(envText) || null;
430
+ const baseUrl = codexLbProviderBaseUrl(config) || envLoad.base_url || null;
350
431
  const providerRequiresOpenAiAuth = codexLbProviderRequiresOpenAiAuth(config);
351
432
  return {
352
433
  ok: providerConfigured && envKeyConfigured && Boolean(baseUrl) && providerRequiresOpenAiAuth,
@@ -357,7 +438,16 @@ export async function codexLbStatus(opts = {}) {
357
438
  selected,
358
439
  env_file: envExists,
359
440
  env_key_configured: envKeyConfigured,
360
- env_base_url_configured: Boolean(parseCodexLbEnvBaseUrl(envText)),
441
+ env_base_url_configured: Boolean(envLoad.base_url),
442
+ env_loader: {
443
+ configured: envLoad.configured,
444
+ missing: envLoad.missing,
445
+ source: envLoad.source,
446
+ source_priority: envLoad.source_priority,
447
+ api_key: envLoad.api_key,
448
+ keychain: envLoad.keychain,
449
+ env_paths: envLoad.env_paths
450
+ },
361
451
  base_url: baseUrl,
362
452
  auth_path: authPath,
363
453
  auth_mode: authMode.mode,
@@ -1155,10 +1245,10 @@ async function syncCodexLbProviderEnvironment(status = {}, opts = {}) {
1155
1245
  const home = opts.home || process.env.HOME || os.homedir();
1156
1246
  const envPath = opts.envPath || status.env_path || codexLbEnvPath(home);
1157
1247
  const envText = await readText(envPath, '');
1158
- const apiKey = parseCodexLbEnvKey(envText);
1248
+ const apiKey = String(opts.apiKey || '').trim() || parseCodexLbEnvKey(envText);
1159
1249
  if (!apiKey)
1160
1250
  return { ok: false, status: 'missing_env_key' };
1161
- const baseUrl = status.base_url || parseCodexLbEnvBaseUrl(envText);
1251
+ const baseUrl = status.base_url || opts.baseUrl || parseCodexLbEnvBaseUrl(envText);
1162
1252
  process.env.CODEX_LB_API_KEY = apiKey;
1163
1253
  if (baseUrl)
1164
1254
  process.env.CODEX_LB_BASE_URL = baseUrl;
@@ -1225,8 +1315,10 @@ async function syncCodexApiKeyLogin(apiKey, opts = {}) {
1225
1315
  return { ok: true, status: 'synced' };
1226
1316
  return { ok: false, status: 'login_failed', error: redactSecretText(login.stderr || login.stdout || 'codex login failed', [apiKey]).trim() };
1227
1317
  }
1228
- function upsertCodexLbConfig(text = '', baseUrl) {
1229
- let next = upsertTopLevelTomlString(text, 'model_provider', 'codex-lb');
1318
+ function upsertCodexLbConfig(text = '', baseUrl, selectDefault = true) {
1319
+ let next = selectDefault
1320
+ ? upsertTopLevelTomlString(text, 'model_provider', 'codex-lb')
1321
+ : removeTopLevelTomlKeyIfValue(text, 'model_provider', 'codex-lb');
1230
1322
  const block = [
1231
1323
  '[model_providers.codex-lb]',
1232
1324
  'name = "OpenAI"',
@@ -1239,6 +1331,24 @@ function upsertCodexLbConfig(text = '', baseUrl) {
1239
1331
  next = upsertTomlTable(next, 'model_providers.codex-lb', block);
1240
1332
  return `${next.trim()}\n`;
1241
1333
  }
1334
+ function detectCodexLbSetupDrift(state = {}) {
1335
+ const drift = [];
1336
+ if (state.useDefaultProvider && state.selected !== true)
1337
+ drift.push('default_provider_not_selected');
1338
+ if (!state.useDefaultProvider && state.selected === true)
1339
+ drift.push('default_provider_selected_despite_no_default_provider');
1340
+ if (state.writeEnvFile && state.envFile !== true)
1341
+ drift.push('env_file_not_written');
1342
+ if (!state.writeEnvFile && state.envFile === true)
1343
+ drift.push('env_file_written_despite_no_env_file');
1344
+ if (!state.storeKeychain && state.keychain?.status && state.keychain.status !== 'skipped')
1345
+ drift.push('keychain_touched_despite_no_keychain');
1346
+ if (!state.syncLaunchctl && state.codexEnvironment?.launch_environment?.status === 'synced')
1347
+ drift.push('launchctl_synced_despite_no_launchctl');
1348
+ if (state.shellProfile === 'skip' && state.shellProfileResult?.status === 'installed')
1349
+ drift.push('shell_profile_written_despite_skip');
1350
+ return drift;
1351
+ }
1242
1352
  export async function ensureGlobalCodexFastModeDuringInstall(opts = {}) {
1243
1353
  if (process.env.SKS_SKIP_CODEX_FAST_MODE_REPAIR === '1')
1244
1354
  return { status: 'skipped', reason: 'SKS_SKIP_CODEX_FAST_MODE_REPAIR=1' };
@@ -1482,6 +1592,10 @@ function redactSecretText(text = '', secrets = []) {
1482
1592
  }
1483
1593
  return out;
1484
1594
  }
1595
+ async function sha256Text(value = '') {
1596
+ const { createHash } = await import('node:crypto');
1597
+ return createHash('sha256').update(String(value || '')).digest('hex');
1598
+ }
1485
1599
  function escapeRegExp(value) {
1486
1600
  return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1487
1601
  }
@@ -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, 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';
@@ -39,7 +40,8 @@ export async function run(command, args = []) {
39
40
  }
40
41
  if (action === 'health' || action === 'verify-chain' || action === 'chain') {
41
42
  const status = await codexLbStatus();
42
- const result = status.ok ? await checkCodexLbResponseChain(status, { force: true, root }) : { ok: false, status: 'not_configured', codex_lb: status };
43
+ const blocker = !status.env_key_configured ? 'missing_env_key' : !status.base_url ? 'missing_base_url' : 'not_configured';
44
+ const result = status.ok ? await checkCodexLbResponseChain(status, { force: true, root }) : { ok: false, status: blocker, codex_lb: status };
43
45
  if (flag(args, '--json'))
44
46
  return printJson(result);
45
47
  console.log(`codex-lb response chain: ${result.ok ? 'ok' : `failed (${result.status})`}`);
@@ -76,6 +78,17 @@ export async function run(command, args = []) {
76
78
  }
77
79
  if (action === 'setup' || action === 'reconfigure') {
78
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
+ });
79
92
  if (!options.host || !options.apiKey) {
80
93
  const result = {
81
94
  schema: 'sks.codex-lb-setup.v1',
@@ -97,8 +110,43 @@ export async function run(command, args = []) {
97
110
  process.exitCode = 1;
98
111
  return;
99
112
  }
100
- const result = await configureCodexLb({ host: options.host, apiKey: options.apiKey });
113
+ if (flag(args, '--plan')) {
114
+ const result = { schema: 'sks.codex-lb-setup-plan-result.v1', ok: plan.blockers.length === 0, plan, writes: false };
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
+ if (options.interactive && !options.yes) {
123
+ process.stdout.write(renderCodexLbSetupPlan(plan));
124
+ const confirm = (await ask('Apply this codex-lb setup plan? [y/N] ')).trim();
125
+ if (!/^(y|yes|예|네|응)$/i.test(confirm)) {
126
+ const result = { schema: 'sks.codex-lb-setup.v1', ok: false, status: 'cancelled', plan, applied_actions: [] };
127
+ if (flag(args, '--json'))
128
+ return printJson(result);
129
+ console.log('codex-lb setup cancelled.');
130
+ process.exitCode = 1;
131
+ return;
132
+ }
133
+ }
134
+ const result = await configureCodexLb({
135
+ host: options.host,
136
+ apiKey: options.apiKey,
137
+ keychain: options.keychain,
138
+ storeKeychain: options.keychain,
139
+ useDefaultProvider: options.useDefaultProvider,
140
+ writeEnvFile: options.writeEnvFile,
141
+ syncLaunchctl: options.syncLaunchctl,
142
+ shellProfile: options.shellProfile,
143
+ runHealth: options.health,
144
+ apiKeySource: options.apiKeySource,
145
+ allowInsecureHttp: options.allowInsecureLocalhost
146
+ });
101
147
  const shaped = { schema: 'sks.codex-lb-setup.v1', ...result, api_key: { present: Boolean(options.apiKey), redacted: true }, env_file_chmod: '0600' };
148
+ if (options.health)
149
+ shaped.applied_actions = [...(shaped.applied_actions || []), { type: 'run_health_check', target: 'codex-lb response chain', ok: true }];
102
150
  if (options.health)
103
151
  shaped.chain_health = result.ok ? await checkCodexLbResponseChain(result, { force: true, root }) : null;
104
152
  if (flag(args, '--json'))
@@ -147,11 +195,13 @@ function shapeCodexLbStatus(status = {}) {
147
195
  ...status,
148
196
  configured: Boolean(status.ok),
149
197
  setup_needed: !status.ok,
198
+ repair_available: !status.ok,
150
199
  api_key: {
151
200
  present: Boolean(status.env_key_configured),
152
- source: status.env_key_configured ? 'env-file' : null,
201
+ source: status.env_loader?.api_key?.source || null,
153
202
  redacted: true
154
203
  },
204
+ env_loader: status.env_loader || null,
155
205
  env_auto_load: Boolean(status.env_file && status.env_key_configured),
156
206
  guidance: status.ok ? [] : [
157
207
  'codex-lb API key is not configured.',
@@ -164,19 +214,58 @@ async function codexLbSetupOptions(args = []) {
164
214
  const baseUrl = readOption(args, '--base-url', null);
165
215
  let host = baseUrl || readOption(args, '--host', readOption(args, '--domain', null));
166
216
  let apiKey = readOption(args, '--api-key', readOption(args, '--key', null));
217
+ let apiKeySource = apiKey ? 'cli_option' : 'hidden_prompt';
218
+ let keychain = flag(args, '--keychain');
167
219
  if (flag(args, '--api-key-stdin'))
168
220
  apiKey = (await readStdin()).trim();
169
- let health = flag(args, '--health') || flag(args, '--check');
221
+ if (flag(args, '--api-key-stdin'))
222
+ apiKeySource = 'stdin';
223
+ let health = (flag(args, '--health') || flag(args, '--check')) && !flag(args, '--no-health');
224
+ let useDefaultProvider = flag(args, '--no-default-provider') ? false : true;
225
+ if (flag(args, '--use-default-provider'))
226
+ useDefaultProvider = true;
227
+ let writeEnvFile = flag(args, '--no-env-file') ? false : true;
228
+ if (flag(args, '--write-env-file'))
229
+ writeEnvFile = true;
230
+ if (flag(args, '--no-keychain'))
231
+ keychain = false;
232
+ let syncLaunchctl = flag(args, '--no-launchctl') ? false : true;
233
+ if (flag(args, '--launchctl'))
234
+ syncLaunchctl = true;
235
+ const shellProfile = normalizeShellProfile(readOption(args, '--shell-profile', 'skip'));
236
+ const allowInsecureLocalhost = flag(args, '--allow-insecure-localhost') || flag(args, '--allow-insecure-http');
237
+ const interactive = (!host || !apiKey || canAskInteractive(args)) && canAskInteractive(args);
170
238
  if ((!host || !apiKey) && canAskInteractive(args)) {
171
239
  console.log('SKS codex-lb setup\n');
172
240
  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();
173
241
  apiKey ||= (await askHidden('2. API key?\n Input hidden. Value will be stored securely and never printed.\n> ')).trim();
174
- await ask('3. Use this codex-lb as default for Codex launches? [Y/n] ');
175
- await ask('4. Write shell env loader to ~/.codex/sks-codex-lb.env? [Y/n] ');
176
- const runHealth = (await ask('5. Run health check now? [Y/n] ')).trim();
242
+ apiKeySource = 'hidden_prompt';
243
+ useDefaultProvider = parseYesNo(await ask('3. Use this codex-lb as default for Codex launches? [Y/n] '), true);
244
+ writeEnvFile = parseYesNo(await ask('4. Write shell env loader to ~/.codex/sks-codex-lb.env? [Y/n] '), true);
245
+ const storeKeychain = (await ask('5. Store the key in macOS Keychain when available? [Y/n] ')).trim();
246
+ keychain = !/^(n|no|아니|아니요|ㄴ)$/i.test(storeKeychain || 'y');
247
+ syncLaunchctl = parseYesNo(await ask('6. Sync macOS launchctl environment when available? [Y/n] '), true);
248
+ const profile = (await ask('7. Install shell profile snippet? [zsh/bash/fish/all/skip] ')).trim();
249
+ const interactiveShellProfile = normalizeShellProfile(profile || 'skip');
250
+ const runHealth = (await ask('8. Run health check now? [Y/n] ')).trim();
177
251
  health = !/^(n|no|아니|아니요|ㄴ)$/i.test(runHealth || 'y');
252
+ return { host, apiKey, health, keychain, useDefaultProvider, writeEnvFile, syncLaunchctl, shellProfile: interactiveShellProfile, allowInsecureLocalhost, apiKeySource, interactive: true, yes: flag(args, '--yes') };
178
253
  }
179
- return { host, apiKey, health };
254
+ return { host, apiKey, health, keychain, useDefaultProvider, writeEnvFile, syncLaunchctl, shellProfile, allowInsecureLocalhost, apiKeySource, interactive, yes: flag(args, '--yes') };
255
+ }
256
+ function normalizeShellProfile(value) {
257
+ const raw = String(value || 'skip').toLowerCase();
258
+ return raw === 'zsh' || raw === 'bash' || raw === 'fish' || raw === 'all' ? raw : 'skip';
259
+ }
260
+ function parseYesNo(value, fallback) {
261
+ const raw = String(value || '').trim();
262
+ if (!raw)
263
+ return fallback;
264
+ if (/^(y|yes|예|네|응)$/i.test(raw))
265
+ return true;
266
+ if (/^(n|no|아니|아니요|ㄴ)$/i.test(raw))
267
+ return false;
268
+ return fallback;
180
269
  }
181
270
  function canAskInteractive(args = []) {
182
271
  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" | "codex_lb_health_misread" | "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;