sneakoscope 1.0.5 → 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 (39) hide show
  1. package/README.md +23 -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 +5 -1
  7. package/dist/cli/install-helpers.d.ts +3 -0
  8. package/dist/cli/install-helpers.js +88 -11
  9. package/dist/commands/codex-lb.js +89 -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 +46 -0
  22. package/dist/core/codex-lb/codex-lb-setup.js +112 -0
  23. package/dist/core/commands/computer-use-command.js +22 -1
  24. package/dist/core/commands/wiki-command.d.ts +2 -2
  25. package/dist/core/computer-use-status.d.ts +24 -0
  26. package/dist/core/computer-use-status.js +46 -0
  27. package/dist/core/fsx.d.ts +1 -1
  28. package/dist/core/fsx.js +1 -1
  29. package/dist/core/proof/evidence-collector.d.ts +1 -1
  30. package/dist/core/proof/route-finalizer.js +3 -1
  31. package/dist/core/triwiki-wrongness/wrongness-cli.d.ts +2 -2
  32. package/dist/core/triwiki-wrongness/wrongness-ledger.js +3 -3
  33. package/dist/core/triwiki-wrongness/wrongness-proof-linker.d.ts +1 -1
  34. package/dist/core/triwiki-wrongness/wrongness-retrieval.d.ts +1 -1
  35. package/dist/core/triwiki-wrongness/wrongness-schema.d.ts +1 -1
  36. package/dist/core/triwiki-wrongness/wrongness-schema.js +17 -2
  37. package/dist/core/version.d.ts +1 -1
  38. package/dist/core/version.js +1 -1
  39. package/package.json +6 -3
package/README.md CHANGED
@@ -4,7 +4,9 @@ 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.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.
8
10
 
9
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.
10
12
 
@@ -21,6 +23,26 @@ SKS does not try to clone every other harness. It focuses on one thing: making C
21
23
  ![Sneakoscope Codex architecture and pipeline](https://raw.githubusercontent.com/mandarange/Sneakoscope-Codex/dev/docs/assets/sneakoscope-architecture-pipeline.jpg)
22
24
 
23
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
+
24
46
  ## 1.0.5 Ultimate Harness Seal
25
47
 
26
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.
@@ -76,7 +76,7 @@ dependencies = [
76
76
 
77
77
  [[package]]
78
78
  name = "sks-core"
79
- version = "1.0.5"
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.5"
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.5"),
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.5';
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.5",
3
+ "version": "1.0.6",
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",
@@ -56,6 +56,9 @@ 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;
61
64
  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, installCodexLbShellProfileSnippet } 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,32 @@ 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 plan = buildCodexLbSetupPlan(setupAnswers, {
328
+ home,
329
+ configPath,
330
+ envPath,
331
+ metadataPath: opts.metadataPath || codexLbMetadataPath(home)
332
+ });
310
333
  if (!baseUrl)
311
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 };
312
337
  if (/[\u0000-\u001f\u007f\s]/.test(rawHost.trim()))
313
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' };
314
339
  if (!apiKey)
@@ -316,12 +341,21 @@ export async function configureCodexLb(opts = {}) {
316
341
  const insecureLocalWarning = /^http:\/\//i.test(baseUrl) && !/^http:\/\/(?:localhost|127\.0\.0\.1|\[::1\])(?::|\/|$)/i.test(baseUrl) && !opts.allowInsecureHttp
317
342
  ? ['codex-lb base URL uses http outside localhost; prefer https or pass an explicit allow flag in the calling surface.']
318
343
  : [];
344
+ const appliedActions = [];
319
345
  await ensureDir(path.dirname(configPath));
320
346
  const current = await readText(configPath, '');
321
- const next = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(current, baseUrl));
347
+ const next = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(current, baseUrl, useDefaultProvider));
322
348
  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(() => { });
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;
325
359
  const keyFingerprint = await sha256Text(apiKey);
326
360
  const metadataPath = opts.metadataPath || codexLbMetadataPath(home);
327
361
  await writeTextAtomic(metadataPath, `${JSON.stringify({
@@ -332,16 +366,39 @@ export async function configureCodexLb(opts = {}) {
332
366
  api_key: { redacted: true, sha256: keyFingerprint }
333
367
  }, null, 2)}\n`);
334
368
  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 });
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 });
337
379
  const codexLogin = await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home, force: true });
338
380
  const codexLb = await codexLbStatus({ ...opts, home, configPath, envPath });
339
381
  const authReconcile = await reconcileCodexLbAuthConflict({ ...opts, home, status: codexLb }).catch((err) => ({ status: 'failed', reason: 'exception', error: err.message }));
340
382
  const finalCodexLb = await codexLbStatus({ ...opts, home, configPath, envPath });
341
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
+ });
342
396
  return {
343
- ok,
344
- 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,
345
402
  config_path: configPath,
346
403
  env_path: envPath,
347
404
  metadata_path: metadataPath,
@@ -1188,10 +1245,10 @@ async function syncCodexLbProviderEnvironment(status = {}, opts = {}) {
1188
1245
  const home = opts.home || process.env.HOME || os.homedir();
1189
1246
  const envPath = opts.envPath || status.env_path || codexLbEnvPath(home);
1190
1247
  const envText = await readText(envPath, '');
1191
- const apiKey = parseCodexLbEnvKey(envText);
1248
+ const apiKey = String(opts.apiKey || '').trim() || parseCodexLbEnvKey(envText);
1192
1249
  if (!apiKey)
1193
1250
  return { ok: false, status: 'missing_env_key' };
1194
- const baseUrl = status.base_url || parseCodexLbEnvBaseUrl(envText);
1251
+ const baseUrl = status.base_url || opts.baseUrl || parseCodexLbEnvBaseUrl(envText);
1195
1252
  process.env.CODEX_LB_API_KEY = apiKey;
1196
1253
  if (baseUrl)
1197
1254
  process.env.CODEX_LB_BASE_URL = baseUrl;
@@ -1258,8 +1315,10 @@ async function syncCodexApiKeyLogin(apiKey, opts = {}) {
1258
1315
  return { ok: true, status: 'synced' };
1259
1316
  return { ok: false, status: 'login_failed', error: redactSecretText(login.stderr || login.stdout || 'codex login failed', [apiKey]).trim() };
1260
1317
  }
1261
- function upsertCodexLbConfig(text = '', baseUrl) {
1262
- 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');
1263
1322
  const block = [
1264
1323
  '[model_providers.codex-lb]',
1265
1324
  'name = "OpenAI"',
@@ -1272,6 +1331,24 @@ function upsertCodexLbConfig(text = '', baseUrl) {
1272
1331
  next = upsertTomlTable(next, 'model_providers.codex-lb', block);
1273
1332
  return `${next.trim()}\n`;
1274
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
+ }
1275
1352
  export async function ensureGlobalCodexFastModeDuringInstall(opts = {}) {
1276
1353
  if (process.env.SKS_SKIP_CODEX_FAST_MODE_REPAIR === '1')
1277
1354
  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, 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,8 +110,43 @@ 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 };
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
+ });
102
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 }];
103
150
  if (options.health)
104
151
  shaped.chain_health = result.ok ? await checkCodexLbResponseChain(result, { force: true, root }) : null;
105
152
  if (flag(args, '--json'))
@@ -167,22 +214,58 @@ async function codexLbSetupOptions(args = []) {
167
214
  const baseUrl = readOption(args, '--base-url', null);
168
215
  let host = baseUrl || readOption(args, '--host', readOption(args, '--domain', null));
169
216
  let apiKey = readOption(args, '--api-key', readOption(args, '--key', null));
217
+ let apiKeySource = apiKey ? 'cli_option' : 'hidden_prompt';
170
218
  let keychain = flag(args, '--keychain');
171
219
  if (flag(args, '--api-key-stdin'))
172
220
  apiKey = (await readStdin()).trim();
173
- 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);
174
238
  if ((!host || !apiKey) && canAskInteractive(args)) {
175
239
  console.log('SKS codex-lb setup\n');
176
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();
177
241
  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] ');
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);
180
245
  const storeKeychain = (await ask('5. Store the key in macOS Keychain when available? [Y/n] ')).trim();
181
246
  keychain = !/^(n|no|아니|아니요|ㄴ)$/i.test(storeKeychain || 'y');
182
- const runHealth = (await ask('6. Run health check now? [Y/n] ')).trim();
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();
183
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') };
184
253
  }
185
- return { host, apiKey, health, keychain };
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;
186
269
  }
187
270
  function canAskInteractive(args = []) {
188
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" | "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;
@@ -16,11 +16,13 @@ export declare function codexCompatibilityReport(opts?: any): Promise<{
16
16
  hooks_semantic: {
17
17
  ok: boolean;
18
18
  warnings_count: number;
19
+ issues_by_category: Record<"schema_violation" | "upstream_semantic_unsupported" | "sks_zero_warning_disallowed" | "legacy_shape" | "policy_disallowed", number>;
19
20
  events: {
20
21
  event: "UserPromptSubmit" | "PreToolUse" | "PostToolUse" | "PermissionRequest" | "Stop" | "PreCompact" | "PostCompact" | "SessionStart";
21
22
  checked: number;
22
23
  ok: boolean;
23
24
  warnings: string[];
25
+ issues_by_category: Record<"schema_violation" | "upstream_semantic_unsupported" | "sks_zero_warning_disallowed" | "legacy_shape" | "policy_disallowed", number>;
24
26
  }[];
25
27
  };
26
28
  ok: boolean;
@@ -49,11 +51,13 @@ export declare function codexDoctorReport(opts?: any): Promise<{
49
51
  hooks_semantic: {
50
52
  ok: boolean;
51
53
  warnings_count: number;
54
+ issues_by_category: Record<"schema_violation" | "upstream_semantic_unsupported" | "sks_zero_warning_disallowed" | "legacy_shape" | "policy_disallowed", number>;
52
55
  events: {
53
56
  event: "UserPromptSubmit" | "PreToolUse" | "PostToolUse" | "PermissionRequest" | "Stop" | "PreCompact" | "PostCompact" | "SessionStart";
54
57
  checked: number;
55
58
  ok: boolean;
56
59
  warnings: string[];
60
+ issues_by_category: Record<"schema_violation" | "upstream_semantic_unsupported" | "sks_zero_warning_disallowed" | "legacy_shape" | "policy_disallowed", number>;
57
61
  }[];
58
62
  };
59
63
  ok: boolean;
@@ -66,12 +70,15 @@ export declare function codexDoctorReport(opts?: any): Promise<{
66
70
  ok: boolean;
67
71
  baseline: string;
68
72
  warnings_count: number;
73
+ issues_by_category: Record<"schema_violation" | "upstream_semantic_unsupported" | "sks_zero_warning_disallowed" | "legacy_shape" | "policy_disallowed", number>;
74
+ issues: import("./codex-hook-issues.js").CodexHookIssue[];
69
75
  warnings: string[];
70
76
  events: {
71
77
  event: "UserPromptSubmit" | "PreToolUse" | "PostToolUse" | "PermissionRequest" | "Stop" | "PreCompact" | "PostCompact" | "SessionStart";
72
78
  checked: number;
73
79
  ok: boolean;
74
80
  warnings: string[];
81
+ issues_by_category: Record<"schema_violation" | "upstream_semantic_unsupported" | "sks_zero_warning_disallowed" | "legacy_shape" | "policy_disallowed", number>;
75
82
  }[];
76
83
  config: {
77
84
  schema: string;
@@ -27,6 +27,7 @@ export async function codexCompatibilityReport(opts = {}) {
27
27
  hooks_semantic: {
28
28
  ok: hooks.ok,
29
29
  warnings_count: hooks.warnings_count,
30
+ issues_by_category: hooks.issues_by_category,
30
31
  events: hooks.events
31
32
  },
32
33
  ok,
@@ -0,0 +1,20 @@
1
+ export declare const CODEX_HOOK_ISSUE_CATEGORIES: readonly ["schema_violation", "upstream_semantic_unsupported", "sks_zero_warning_disallowed", "legacy_shape", "policy_disallowed"];
2
+ export type CodexHookIssueCategory = typeof CODEX_HOOK_ISSUE_CATEGORIES[number];
3
+ export interface CodexHookIssue {
4
+ category: CodexHookIssueCategory;
5
+ code: string;
6
+ message: string;
7
+ path?: string;
8
+ upstream_supported?: boolean;
9
+ sks_disallowed?: boolean;
10
+ }
11
+ export declare function makeCodexHookIssue(category: CodexHookIssueCategory, code: string, message: string, opts?: {
12
+ path?: string;
13
+ upstream_supported?: boolean;
14
+ sks_disallowed?: boolean;
15
+ }): CodexHookIssue;
16
+ export declare function schemaIssueToCodexHookIssue(issue: string): CodexHookIssue;
17
+ export declare function dedupeCodexHookIssues(issues: readonly CodexHookIssue[]): CodexHookIssue[];
18
+ export declare function codexHookIssuesByCategory(issues: readonly CodexHookIssue[]): Record<CodexHookIssueCategory, number>;
19
+ export declare function codexHookIssueWarningString(issue: CodexHookIssue): string;
20
+ //# sourceMappingURL=codex-hook-issues.d.ts.map
@@ -0,0 +1,93 @@
1
+ export const CODEX_HOOK_ISSUE_CATEGORIES = Object.freeze([
2
+ 'schema_violation',
3
+ 'upstream_semantic_unsupported',
4
+ 'sks_zero_warning_disallowed',
5
+ 'legacy_shape',
6
+ 'policy_disallowed'
7
+ ]);
8
+ export function makeCodexHookIssue(category, code, message, opts = {}) {
9
+ const issue = {
10
+ category,
11
+ code: normalizeIssueCode(code),
12
+ message
13
+ };
14
+ if (opts.path)
15
+ issue.path = opts.path;
16
+ if (opts.upstream_supported !== undefined)
17
+ issue.upstream_supported = opts.upstream_supported;
18
+ if (opts.sks_disallowed !== undefined)
19
+ issue.sks_disallowed = opts.sks_disallowed;
20
+ return issue;
21
+ }
22
+ export function schemaIssueToCodexHookIssue(issue) {
23
+ const path = issue.split(':')[0] || '$';
24
+ const code = issue.includes(':unknown_field') ? 'unknown_field'
25
+ : issue.includes(':required') ? 'required'
26
+ : issue.includes(':type:') ? 'type'
27
+ : issue.includes(':enum') ? 'enum'
28
+ : issue.includes(':const') ? 'const'
29
+ : 'schema_violation';
30
+ return makeCodexHookIssue('schema_violation', code, `Codex hook output schema violation: ${issue}`, {
31
+ path,
32
+ upstream_supported: false,
33
+ sks_disallowed: true
34
+ });
35
+ }
36
+ export function dedupeCodexHookIssues(issues) {
37
+ const seen = new Set();
38
+ const out = [];
39
+ for (const issue of issues) {
40
+ const key = [issue.category, issue.code, issue.path || '', issue.message].join('\u0000');
41
+ if (seen.has(key))
42
+ continue;
43
+ seen.add(key);
44
+ out.push(issue);
45
+ }
46
+ return out;
47
+ }
48
+ export function codexHookIssuesByCategory(issues) {
49
+ const summary = {
50
+ schema_violation: 0,
51
+ upstream_semantic_unsupported: 0,
52
+ sks_zero_warning_disallowed: 0,
53
+ legacy_shape: 0,
54
+ policy_disallowed: 0
55
+ };
56
+ for (const issue of issues)
57
+ summary[issue.category] += 1;
58
+ return summary;
59
+ }
60
+ export function codexHookIssueWarningString(issue) {
61
+ if (issue.code.startsWith('legacy_top_level_'))
62
+ return `legacy_top_level:${issue.code.slice('legacy_top_level_'.length)}`;
63
+ if (issue.code.startsWith('permission_request_reserved_'))
64
+ return `permission_request_reserved:${reservedPermissionRequestFieldName(issue.code.slice('permission_request_reserved_'.length))}`;
65
+ if (issue.code === 'snake_case')
66
+ return `${issue.path || '$'}:snake_case`;
67
+ if (issue.category === 'schema_violation')
68
+ return `${issue.path || '$'}:${issue.code}`;
69
+ if (issue.category === 'upstream_semantic_unsupported')
70
+ return `semantic_unsupported:${issue.message}`;
71
+ if (issue.category === 'sks_zero_warning_disallowed')
72
+ return `sks_zero_warning_disallowed:${issue.code}`;
73
+ if (issue.category === 'legacy_shape')
74
+ return `legacy_shape:${issue.code}`;
75
+ return `policy_disallowed:${issue.code}`;
76
+ }
77
+ function reservedPermissionRequestFieldName(value) {
78
+ if (value === 'updated_input')
79
+ return 'updatedInput';
80
+ if (value === 'updated_permissions')
81
+ return 'updatedPermissions';
82
+ return value;
83
+ }
84
+ function normalizeIssueCode(value) {
85
+ return String(value || 'issue')
86
+ .trim()
87
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
88
+ .toLowerCase()
89
+ .replace(/[^a-z0-9]+/g, '_')
90
+ .replace(/^_+|_+$/g, '')
91
+ || 'issue';
92
+ }
93
+ //# sourceMappingURL=codex-hook-issues.js.map
@@ -1,9 +1,11 @@
1
1
  import { type CodexHookEventName } from './codex-schema-snapshot.js';
2
2
  import { type CodexHookSemanticValidation } from './codex-hook-semantic-validator.js';
3
+ import { type CodexHookIssue } from './codex-hook-issues.js';
3
4
  export type CodexSchemaValidation = {
4
5
  ok: boolean;
5
6
  event: CodexHookEventName;
6
7
  issues: string[];
8
+ structured_issues: CodexHookIssue[];
7
9
  };
8
10
  export type CodexHookOutputValidation = CodexSchemaValidation & {
9
11
  semantic: CodexHookSemanticValidation;
@@ -18,6 +20,7 @@ export declare function validateCodexFixtureOutputs(root?: string): Promise<{
18
20
  semantic: CodexHookSemanticValidation;
19
21
  ok: boolean;
20
22
  issues: string[];
23
+ structured_issues: CodexHookIssue[];
21
24
  event: CodexHookEventName;
22
25
  file: string;
23
26
  }[];
@@ -2,11 +2,12 @@ import path from 'node:path';
2
2
  import { exists, projectRoot, readJson } from '../fsx.js';
3
3
  import { CODEX_HOOK_EVENTS, codexHookEventName, readCodexHookSchema } from './codex-schema-snapshot.js';
4
4
  import { validateCodexHookSemanticOutput } from './codex-hook-semantic-validator.js';
5
+ import { schemaIssueToCodexHookIssue } from './codex-hook-issues.js';
5
6
  export async function validateCodexHookOutput(eventLike, output) {
6
7
  const event = codexHookEventName(eventLike) || 'UserPromptSubmit';
7
8
  const schema = await readCodexHookSchema(event, 'output');
8
9
  const issues = validateJsonValue(output, schema, schema, '$');
9
- return { ok: issues.length === 0, event, issues };
10
+ return { ok: issues.length === 0, event, issues, structured_issues: issues.map(schemaIssueToCodexHookIssue) };
10
11
  }
11
12
  export async function validateCodexFixtureOutputs(root) {
12
13
  root ||= await projectRoot();
@@ -25,9 +26,11 @@ export async function validateCodexFixtureOutputs(root) {
25
26
  ok: validation.ok && semantic.ok,
26
27
  issues: [
27
28
  ...validation.issues,
28
- ...semantic.warnings.map((issue) => `semantic_warning:${issue}`),
29
- ...semantic.unsupported.map((issue) => `semantic_unsupported:${issue}`),
30
- ...semantic.fatal.map((issue) => `semantic_fatal:${issue}`)
29
+ ...semantic.issues.map((issue) => `${issue.category}:${issue.code}:${issue.message}`)
30
+ ],
31
+ structured_issues: [
32
+ ...validation.structured_issues,
33
+ ...semantic.issues
31
34
  ]
32
35
  });
33
36
  }