sneakoscope 0.8.4 → 0.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -174,9 +174,9 @@ sks codex-lb repair
174
174
  sks
175
175
  ```
176
176
 
177
- Bare `sks` can also prompt for codex-lb auth; SKS stores the base URL/key in `~/.codex/sks-codex-lb.env`, syncs `codex login --with-api-key`, and loads it in tmux. When codex-lb is active, SKS opens a fresh `sks-codex-lb-*` tmux session and sweeps older detached codex-lb sessions for the same repo before launch so stale Responses API chains are not reused. Configured launch paths, including non-interactive runs, verify that codex-lb can continue a Responses API chain with `previous_response_id`; if that check fails, SKS bypasses codex-lb for that launch with `model_provider="openai"` instead of letting the Codex session fail mid-work.
177
+ Bare `sks` can also prompt for codex-lb auth; SKS stores the base URL/key in `~/.codex/sks-codex-lb.env`, loads the provider env key for tmux launches, and syncs the macOS user launch environment so the Codex App can see `CODEX_LB_API_KEY` after restart. npm postinstall upgrades resync that stored env file when postinstall is not stopped by a hard harness conflict, and `sks doctor --fix` does the same during repair. If an older SKS release left the codex-lb dashboard key only in the shared Codex `auth.json` login cache, SKS migrates that key back into `~/.codex/sks-codex-lb.env` when a codex-lb provider or env base URL is already recoverable. It does not rewrite the shared Codex `auth.json` login cache by default; set `SKS_CODEX_LB_SYNC_CODEX_LOGIN=1` only if you intentionally want the old API-key login-cache behavior. When codex-lb is active, SKS opens a fresh `sks-codex-lb-*` tmux session and sweeps older detached codex-lb sessions for the same repo before launch so stale Responses API chains are not reused. Configured launch paths, including non-interactive runs, verify that codex-lb can continue a Responses API chain with `previous_response_id`; if that check fails, SKS bypasses codex-lb for that launch with `model_provider="openai"` instead of letting the Codex session fail mid-work.
178
178
 
179
- If Codex CLI auth drifts after launch/reinstall, run `sks doctor --fix` or `sks codex-lb repair`; to replace it, run `sks codex-lb reconfigure --host <domain> --api-key <key>`.
179
+ If codex-lb provider auth drifts after launch/reinstall, run `sks doctor --fix` or `sks codex-lb repair`; to replace it, run `sks codex-lb reconfigure --host <domain> --api-key <key>`.
180
180
 
181
181
  ### MAD tmux Launch
182
182
 
@@ -185,7 +185,7 @@ sks --mad
185
185
  sks --mad --yes
186
186
  ```
187
187
 
188
- This syncs existing codex-lb/Codex CLI auth, creates/uses the `sks-mad-high` full-access profile, opens the MAD-SKS permission gate for that tmux run, and launches a single Codex CLI pane. The session recreates the named session so stale split-pane MAD sessions collapse back to one pane. Catastrophic database wipe/all-row/project-management safeguards remain active, and the pipeline contract still forbids unrequested fallback implementation code.
188
+ This syncs existing codex-lb provider auth, creates/uses the `sks-mad-high` full-access profile, opens the MAD-SKS permission gate for that tmux run, and launches a single Codex CLI pane. The session recreates the named session so stale split-pane MAD sessions collapse back to one pane. Catastrophic database wipe/all-row/project-management safeguards remain active, and the pipeline contract still forbids unrequested fallback implementation code.
189
189
 
190
190
  Before launching, SKS checks npm for a newer `sneakoscope`; answer `y` to update or `n` to continue. Use `--yes` to approve missing dependency installs automatically.
191
191
 
@@ -407,6 +407,10 @@ codex mcp list
407
407
 
408
408
  Codex App workflows need the app installed. UI/browser evidence requires first-party Codex Computer Use, and generated raster/image-review evidence requires real `$imagegen`/`gpt-image-2` output. After setup/upgrade, start a fresh thread so Codex reloads plugin tools.
409
409
 
410
+ ### Codex App UI looks stale after codex-lb changes
411
+
412
+ If Codex App UI panels or auth-dependent controls still look wrong after codex-lb setup, repair, or upgrade, restart the app first. If the UI still does not recover, sign out of Codex App, sign back in, then run `sks codex-app check` or `sks codex-lb repair` as needed.
413
+
410
414
  ### Setup is blocked by another harness
411
415
 
412
416
  ```sh
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "0.8.4",
4
+ "version": "0.8.5",
5
5
  "description": "Sneakoscope Codex: database-safe Codex CLI/App harness with Team, Goal, AutoResearch, TriWiki, and Honest Mode.",
6
6
  "type": "module",
7
7
  "homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
@@ -81,7 +81,8 @@ export async function postinstall({ bootstrap }) {
81
81
 
82
82
  async function reportPostinstallCodexLbAuth() {
83
83
  const codexLbAuth = await ensureCodexLbAuthDuringInstall();
84
- if (codexLbAuth.status === 'synced' || codexLbAuth.status === 'present' || codexLbAuth.status === 'repaired') console.log(`codex-lb auth: preserved from ${codexLbAuth.env_path}.`);
84
+ if (codexLbAuth.legacy_auth_migrated) console.log(`codex-lb auth: restored from existing Codex login cache into ${codexLbAuth.env_path}.`);
85
+ else if (codexLbAuth.status === 'synced' || codexLbAuth.status === 'present' || codexLbAuth.status === 'repaired') console.log(`codex-lb auth: preserved from ${codexLbAuth.env_path}.`);
85
86
  else if (codexLbAuth.status === 'skipped') console.log(`codex-lb auth: skipped (${codexLbAuth.reason}).`);
86
87
  else if (codexLbAuth.status === 'missing_env_key') console.log('codex-lb auth: stored key missing. Run `sks codex-lb setup --host <domain> --api-key <key>` to repair.');
87
88
  else if (codexLbAuth.status === 'missing_base_url') console.log('codex-lb auth: stored key has no recoverable base URL. Run `sks codex-lb reconfigure --host <domain> --api-key <key>` once.');
@@ -155,6 +156,10 @@ export function codexLbEnvPath(home = process.env.HOME || os.homedir()) {
155
156
  return path.join(home, '.codex', 'sks-codex-lb.env');
156
157
  }
157
158
 
159
+ function codexAuthPath(home = process.env.HOME || os.homedir()) {
160
+ return path.join(home, '.codex', 'auth.json');
161
+ }
162
+
158
163
  async function capturePostinstallCodexLbConfigSnapshot(home = process.env.HOME || os.homedir()) {
159
164
  const configPath = codexLbConfigPath(home);
160
165
  const envPath = codexLbEnvPath(home);
@@ -198,9 +203,20 @@ export async function configureCodexLb(opts = {}) {
198
203
  await writeTextAtomic(configPath, next);
199
204
  await writeTextAtomic(envPath, `export CODEX_LB_BASE_URL=${shellSingleQuote(baseUrl)}\nexport CODEX_LB_API_KEY=${shellSingleQuote(apiKey)}\n`);
200
205
  await fsp.chmod(envPath, 0o600).catch(() => {});
201
- process.env.CODEX_LB_API_KEY = apiKey;
202
- const codexLogin = await syncCodexApiKeyLogin(apiKey, { home, force: true });
203
- return { ok: true, status: 'configured', config_path: configPath, env_path: envPath, base_url: baseUrl, env_key: 'CODEX_LB_API_KEY', codex_login: codexLogin };
206
+ const codexEnvironment = await syncCodexLbProviderEnvironment({ env_path: envPath, base_url: baseUrl }, { ...opts, home });
207
+ const codexLogin = await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home, force: true });
208
+ const ok = Boolean(codexEnvironment.ok && codexLogin.ok);
209
+ return {
210
+ ok,
211
+ status: ok ? 'configured' : (codexEnvironment.status || codexLogin.status),
212
+ config_path: configPath,
213
+ env_path: envPath,
214
+ base_url: baseUrl,
215
+ env_key: 'CODEX_LB_API_KEY',
216
+ codex_environment: codexEnvironment,
217
+ codex_login: codexLogin,
218
+ error: codexEnvironment.error || codexLogin.error || null
219
+ };
204
220
  }
205
221
 
206
222
  export async function codexLbStatus(opts = {}) {
@@ -360,8 +376,18 @@ function codexLbProviderBaseUrl(text = '') {
360
376
  export async function repairCodexLbAuth(opts = {}) {
361
377
  let status = await codexLbStatus(opts);
362
378
  let configRepaired = false;
379
+ let legacyAuthMigrated = false;
380
+ let legacyAuthPath = null;
363
381
  const currentConfig = await readText(status.config_path, '');
364
- if (status.env_key_configured && status.base_url && (!status.ok || status.selected || hasTopLevelCodexModeLock(currentConfig))) {
382
+ if (!status.env_key_configured && status.base_url && (status.provider_configured || status.selected || status.env_base_url_configured)) {
383
+ const legacyAuth = await restoreCodexLbEnvFromSharedLogin(status, opts);
384
+ if (legacyAuth.ok) {
385
+ legacyAuthMigrated = true;
386
+ legacyAuthPath = legacyAuth.auth_path;
387
+ status = await codexLbStatus(opts);
388
+ }
389
+ }
390
+ if (status.env_key_configured && status.base_url && (!status.ok || status.selected || legacyAuthMigrated || hasTopLevelCodexModeLock(currentConfig))) {
365
391
  await ensureDir(path.dirname(status.config_path));
366
392
  const next = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(currentConfig, status.base_url));
367
393
  await writeTextAtomic(status.config_path, next);
@@ -377,15 +403,21 @@ export async function repairCodexLbAuth(opts = {}) {
377
403
  codex_lb: status
378
404
  };
379
405
  }
380
- const codexLogin = await ensureCodexLbLoginFromEnv(status, opts);
406
+ const codexEnvironment = await syncCodexLbProviderEnvironment(status, opts);
407
+ const apiKey = parseCodexLbEnvKey(await readText(status.env_path, ''));
408
+ const codexLogin = await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home: opts.home || process.env.HOME || os.homedir(), force: true });
409
+ const ok = Boolean(codexEnvironment.ok && codexLogin.ok);
381
410
  return {
382
- ok: Boolean(codexLogin.ok),
383
- status: codexLogin.ok ? 'repaired' : codexLogin.status,
411
+ ok,
412
+ status: ok ? 'repaired' : (codexEnvironment.status || codexLogin.status),
384
413
  config_path: status.config_path,
385
414
  env_path: status.env_path,
386
415
  base_url: status.base_url,
387
416
  config_repaired: configRepaired,
417
+ legacy_auth_migrated: legacyAuthMigrated,
418
+ legacy_auth_path: legacyAuthPath,
388
419
  codex_lb: status,
420
+ codex_environment: codexEnvironment,
389
421
  codex_login: codexLogin
390
422
  };
391
423
  }
@@ -395,34 +427,56 @@ export async function ensureCodexLbAuthDuringInstall(opts = {}) {
395
427
  const status = await codexLbStatus(opts);
396
428
  if (!status.selected && !status.provider_configured && !status.env_file) return { status: 'not_configured', codex_lb: status };
397
429
  if (!status.ok) {
398
- if (status.env_key_configured && status.base_url) return repairCodexLbAuth(opts);
430
+ if (status.base_url && (status.env_key_configured || status.provider_configured || status.selected || status.env_base_url_configured)) return repairCodexLbAuth(opts);
399
431
  return { status: status.env_key_configured ? 'missing_base_url' : 'missing_env_key', codex_lb: status, config_path: status.config_path, env_path: status.env_path };
400
432
  }
401
- const codexLogin = await ensureCodexLbLoginFromEnv(status, { ...opts, force: true });
433
+ const codexEnvironment = await syncCodexLbProviderEnvironment(status, opts);
434
+ const apiKey = parseCodexLbEnvKey(await readText(status.env_path, ''));
435
+ const codexLogin = await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home: opts.home || process.env.HOME || os.homedir(), force: true });
436
+ const ok = Boolean(codexEnvironment.ok && codexLogin.ok);
402
437
  return {
403
- ok: Boolean(codexLogin.ok),
404
- status: codexLogin.status,
438
+ ok,
439
+ status: ok ? 'present' : (codexEnvironment.status || codexLogin.status),
405
440
  config_path: status.config_path,
406
441
  env_path: status.env_path,
407
442
  base_url: status.base_url,
408
443
  codex_lb: status,
444
+ codex_environment: codexEnvironment,
409
445
  codex_login: codexLogin,
410
- error: codexLogin.error || null
446
+ error: codexEnvironment.error || codexLogin.error || null
411
447
  };
412
448
  }
413
449
 
450
+ async function restoreCodexLbEnvFromSharedLogin(status = {}, opts = {}) {
451
+ const home = opts.home || process.env.HOME || os.homedir();
452
+ const authPath = opts.authPath || codexAuthPath(home);
453
+ const envPath = opts.envPath || status.env_path || codexLbEnvPath(home);
454
+ const authText = await readText(authPath, '');
455
+ const apiKey = parseCodexSharedLoginApiKey(authText);
456
+ if (!apiKey) return { ok: false, status: 'missing_legacy_login_key', auth_path: authPath, env_path: envPath };
457
+ const baseUrl = status.base_url || parseCodexLbEnvBaseUrl(await readText(envPath, ''));
458
+ if (!baseUrl) return { ok: false, status: 'missing_base_url', auth_path: authPath, env_path: envPath };
459
+ await ensureDir(path.dirname(envPath));
460
+ await writeTextAtomic(envPath, `export CODEX_LB_BASE_URL=${shellSingleQuote(normalizeCodexLbBaseUrl(baseUrl))}\nexport CODEX_LB_API_KEY=${shellSingleQuote(apiKey)}\n`);
461
+ await fsp.chmod(envPath, 0o600).catch(() => {});
462
+ return { ok: true, status: 'migrated_login_cache', auth_path: authPath, env_path: envPath, base_url: normalizeCodexLbBaseUrl(baseUrl) };
463
+ }
464
+
414
465
  export async function maybePromptCodexLbSetupForLaunch(args = [], opts = {}) {
415
466
  if (args.includes('--json') || args.includes('--skip-codex-lb') || process.env.SKS_SKIP_CODEX_LB_PROMPT === '1') return { status: 'skipped' };
416
467
  const status = await codexLbStatus(opts);
417
468
  if (status.ok) {
418
- const codexLogin = await ensureCodexLbLoginFromEnv(status, opts);
419
- if (codexLogin.status === 'synced') console.log('codex-lb auth synced with Codex CLI.');
469
+ const codexEnvironment = await syncCodexLbProviderEnvironment(status, opts);
470
+ if (codexEnvironment.status === 'synced') console.log('codex-lb provider auth synced for this user session.');
471
+ const apiKey = parseCodexLbEnvKey(await readText(status.env_path, ''));
472
+ const codexLogin = await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home: opts.home || process.env.HOME || os.homedir() });
473
+ if (codexLogin.status === 'synced') console.log('codex-lb auth synced with Codex CLI login cache.');
420
474
  const chainHealth = await checkCodexLbResponseChain(status, opts);
421
475
  if (!chainHealth.ok && chainHealth.chain_unhealthy) {
422
476
  console.log(`codex-lb response chain check failed (${chainHealth.status}); bypassing codex-lb for this launch.`);
423
- return { status: 'chain_unhealthy', ...status, ok: false, codex_login: codexLogin, chain_health: chainHealth, bypass_codex_lb: true };
477
+ return { status: 'chain_unhealthy', ...status, ok: false, codex_environment: codexEnvironment, codex_login: codexLogin, chain_health: chainHealth, bypass_codex_lb: true };
424
478
  }
425
- return { status: 'present', ...status, codex_login: codexLogin, chain_health: chainHealth };
479
+ return { status: 'present', ...status, codex_environment: codexEnvironment, codex_login: codexLogin, chain_health: chainHealth };
426
480
  }
427
481
  if (!canAskYesNo()) return { status: 'non_interactive', codex_lb: status };
428
482
  const useCodexLb = (await askPostinstallQuestion('\nAuthenticate and route Codex through codex-lb? [y/N] ')).trim();
@@ -435,12 +489,57 @@ export async function maybePromptCodexLbSetupForLaunch(args = [], opts = {}) {
435
489
  return configured;
436
490
  }
437
491
 
438
- async function ensureCodexLbLoginFromEnv(status = {}, opts = {}) {
492
+ async function syncCodexLbProviderEnvironment(status = {}, opts = {}) {
439
493
  const home = opts.home || process.env.HOME || os.homedir();
440
494
  const envPath = opts.envPath || status.env_path || codexLbEnvPath(home);
441
- const apiKey = parseCodexLbEnvKey(await readText(envPath, ''));
495
+ const envText = await readText(envPath, '');
496
+ const apiKey = parseCodexLbEnvKey(envText);
497
+ if (!apiKey) return { ok: false, status: 'missing_env_key' };
498
+ const baseUrl = status.base_url || parseCodexLbEnvBaseUrl(envText);
499
+ process.env.CODEX_LB_API_KEY = apiKey;
500
+ if (baseUrl) process.env.CODEX_LB_BASE_URL = baseUrl;
501
+ const launchEnv = await syncCodexLbMacLaunchEnvironment({ CODEX_LB_API_KEY: apiKey, ...(baseUrl ? { CODEX_LB_BASE_URL: baseUrl } : {}) }, opts);
502
+ const ok = launchEnv.ok || launchEnv.skipped || launchEnv.status === 'not_macos';
503
+ return {
504
+ ok,
505
+ status: launchEnv.status === 'synced' ? 'synced' : ok ? 'process_env' : launchEnv.status,
506
+ env_path: envPath,
507
+ base_url: baseUrl || null,
508
+ launch_environment: launchEnv,
509
+ error: launchEnv.error || null
510
+ };
511
+ }
512
+
513
+ async function syncCodexLbMacLaunchEnvironment(values = {}, opts = {}) {
514
+ if (opts.syncLaunchEnv === false || process.env.SKS_SKIP_CODEX_LB_LAUNCH_ENV === '1') return { ok: true, status: 'skipped', skipped: true, reason: 'SKS_SKIP_CODEX_LB_LAUNCH_ENV=1' };
515
+ if (process.platform !== 'darwin' && !opts.forceLaunchEnv) return { ok: true, status: 'not_macos', skipped: true };
516
+ const launchctl = opts.launchctlBin || await which('launchctl').catch(() => null) || await exists('/bin/launchctl').then((ok) => ok ? '/bin/launchctl' : null).catch(() => null);
517
+ if (!launchctl) return { ok: false, status: 'launchctl_missing', error: 'launchctl not found on PATH' };
518
+ const variables = Object.entries(values).filter(([, value]) => value);
519
+ const results = [];
520
+ for (const [key, value] of variables) {
521
+ const result = await runProcess(launchctl, ['setenv', key, value], { timeoutMs: 5000, maxOutputBytes: 8192 });
522
+ results.push({
523
+ key,
524
+ ok: result.code === 0,
525
+ error: result.code === 0 ? null : redactSecretText(result.stderr || result.stdout || 'launchctl setenv failed', [value]).trim()
526
+ });
527
+ }
528
+ const failed = results.filter((result) => !result.ok);
529
+ if (failed.length) return { ok: false, status: 'launch_env_failed', variables: results.map((result) => result.key), failed, error: failed.map((result) => `${result.key}: ${result.error}`).join('; ') };
530
+ return { ok: true, status: 'synced', variables: results.map((result) => result.key) };
531
+ }
532
+
533
+ async function maybeSyncCodexLbSharedLogin(apiKey, opts = {}) {
442
534
  if (!apiKey) return { ok: false, status: 'missing_env_key' };
443
- return syncCodexApiKeyLogin(apiKey, { ...opts, home, force: true });
535
+ if (!shouldSyncCodexLbSharedLogin(opts)) {
536
+ return { ok: true, status: 'skipped', reason: 'codex-lb uses provider env_key auth; set SKS_CODEX_LB_SYNC_CODEX_LOGIN=1 to also rewrite Codex shared login cache.' };
537
+ }
538
+ return syncCodexApiKeyLogin(apiKey, opts);
539
+ }
540
+
541
+ function shouldSyncCodexLbSharedLogin(opts = {}) {
542
+ return opts.syncCodexLogin === true || process.env.SKS_CODEX_LB_SYNC_CODEX_LOGIN === '1';
444
543
  }
445
544
 
446
545
  async function syncCodexApiKeyLogin(apiKey, opts = {}) {
@@ -663,6 +762,19 @@ function parseCodexLbEnvBaseUrl(text = '') {
663
762
  return value ? normalizeCodexLbBaseUrl(value) : '';
664
763
  }
665
764
 
765
+ function parseCodexSharedLoginApiKey(text = '') {
766
+ try {
767
+ const parsed = JSON.parse(String(text || ''));
768
+ const authMode = String(parsed?.auth_mode || parsed?.authMode || parsed?.mode || '').toLowerCase();
769
+ const key = parsed?.key || parsed?.api_key || parsed?.apiKey || parsed?.openai_api_key || parsed?.OPENAI_API_KEY;
770
+ if (!key || typeof key !== 'string') return '';
771
+ if (authMode && !/api[-_]?key|apikey/.test(authMode)) return '';
772
+ return key.trim();
773
+ } catch {
774
+ return '';
775
+ }
776
+ }
777
+
666
778
  function parseShellEnvValue(text = '', key = '') {
667
779
  const re = new RegExp(`^\\s*(?:export\\s+)?${escapeRegExp(key)}\\s*=\\s*(.+?)\\s*$`, 'm');
668
780
  const envMatch = String(text || '').match(re);
@@ -1082,6 +1194,24 @@ async function safeReadText(file, fallback = '') {
1082
1194
  }
1083
1195
  }
1084
1196
 
1197
+ async function codexLbLoginCallCount(home) {
1198
+ return (await safeReadText(path.join(home, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
1199
+ }
1200
+
1201
+ function codexLbPostinstallEnv(baseEnv, overrides = {}) {
1202
+ return {
1203
+ ...baseEnv,
1204
+ SKS_POSTINSTALL_NO_BOOTSTRAP: '1',
1205
+ SKS_SKIP_POSTINSTALL_SHIM: '1',
1206
+ SKS_SKIP_POSTINSTALL_CONTEXT7: '1',
1207
+ SKS_SKIP_POSTINSTALL_GETDESIGN: '1',
1208
+ SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1',
1209
+ SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0',
1210
+ SKS_SKIP_CODEX_LB_LAUNCH_ENV: '1',
1211
+ ...overrides
1212
+ };
1213
+ }
1214
+
1085
1215
  export async function selftestCodexLb(tmp) {
1086
1216
  const codexLbHome = path.join(tmp, 'codex-lb-home');
1087
1217
  await ensureDir(path.join(codexLbHome, '.codex'));
@@ -1091,7 +1221,7 @@ export async function selftestCodexLb(tmp) {
1091
1221
  await writeTextAtomic(codexLbFakeCodex, "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo \"codex-cli 99.0.0\"; exit 0; fi\nif [ \"$1\" = \"login\" ] && [ \"$2\" = \"status\" ]; then echo \"logged in with browser auth\"; exit 0; fi\nif [ \"$1\" = \"login\" ] && [ \"$2\" = \"--with-api-key\" ]; then read key; mkdir -p \"$HOME/.codex\"; printf '{\\\"auth_mode\\\":\\\"apikey\\\",\\\"key\\\":\\\"%s\\\"}\\n' \"$key\" > \"$HOME/.codex/auth.json\"; printf '%s\\n' \"$key\" >> \"$HOME/.codex/login-calls.log\"; exit 0; fi\necho \"fake codex unsupported\" >&2\nexit 1\n");
1092
1222
  await fsp.chmod(codexLbFakeCodex, 0o755);
1093
1223
  await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nmodel_reasoning_effort = "low"\nservice_tier = "fast"\n\n[profiles.custom]\nmodel_reasoning_effort = "low"\n\n[notice]\nfast_default_opt_out = true\n\n[features]\ncodex_hooks = true\n');
1094
- const codexLbEnvForSelftest = { HOME: codexLbHome, SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-global'), PATH: `${codexLbFakeBin}${path.delimiter}${process.env.PATH || ''}` };
1224
+ const codexLbEnvForSelftest = { HOME: codexLbHome, SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-global'), PATH: `${codexLbFakeBin}${path.delimiter}${process.env.PATH || ''}`, SKS_SKIP_CODEX_LB_LAUNCH_ENV: '1' };
1095
1225
  const codexLbSetup = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'setup', '--host', 'lb.example.test', '--api-key', 'sk-test', '--json'], {
1096
1226
  cwd: tmp,
1097
1227
  env: codexLbEnvForSelftest,
@@ -1103,7 +1233,12 @@ export async function selftestCodexLb(tmp) {
1103
1233
  const codexLbConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
1104
1234
  const codexLbEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
1105
1235
  const codexLbAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1106
- if (!codexLbSetupJson.ok || codexLbSetupJson.base_url !== 'https://lb.example.test/backend-api/codex' || hasTopLevelCodexLbSelected(codexLbConfig) || !codexLbConfig.includes('[model_providers.codex-lb]') || !codexLbEnv.includes("CODEX_LB_BASE_URL='https://lb.example.test/backend-api/codex'") || !codexLbEnv.includes("CODEX_LB_API_KEY='sk-test'") || !/(\"auth_mode\"\s*:\s*\"apikey\")/.test(codexLbAuth)) throw new Error('selftest: codex-lb setup');
1236
+ if (!codexLbSetupJson.ok || codexLbSetupJson.base_url !== 'https://lb.example.test/backend-api/codex' || hasTopLevelCodexLbSelected(codexLbConfig) || !codexLbConfig.includes('[model_providers.codex-lb]') || !codexLbEnv.includes("CODEX_LB_BASE_URL='https://lb.example.test/backend-api/codex'") || !codexLbEnv.includes("CODEX_LB_API_KEY='sk-test'") || codexLbSetupJson.codex_environment?.ok !== true || codexLbSetupJson.codex_login?.status !== 'skipped' || codexLbAuth.trim()) throw new Error('selftest: codex-lb setup');
1237
+ const codexLbFailLaunchctl = path.join(codexLbFakeBin, 'launchctl-fail');
1238
+ await writeTextAtomic(codexLbFailLaunchctl, '#!/bin/sh\necho "launchctl denied" >&2\nexit 7\n');
1239
+ await fsp.chmod(codexLbFailLaunchctl, 0o755);
1240
+ const codexLbFailedLaunchEnv = await configureCodexLb({ home: path.join(tmp, 'codex-lb-launch-fail-home'), host: 'lb.example.test', apiKey: 'sk-fail', forceLaunchEnv: true, launchctlBin: codexLbFailLaunchctl });
1241
+ if (codexLbFailedLaunchEnv.ok || codexLbFailedLaunchEnv.status !== 'launch_env_failed' || !/launchctl denied/.test(codexLbFailedLaunchEnv.error || '')) throw new Error('selftest: codex-lb setup must expose launch-env failure');
1107
1242
  if (!hasCodexUnstableFeatureWarningSuppression(codexLbConfig)) throw new Error('selftest: codex-lb setup did not suppress Codex unstable feature warning');
1108
1243
  await initProject(codexLbHome, { installScope: 'global', force: true, repair: true });
1109
1244
  const codexLbRepairSetupConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
@@ -1121,30 +1256,27 @@ export async function selftestCodexLb(tmp) {
1121
1256
  if (codexLbRepair.code !== 0) throw new Error(`selftest: codex-lb repair exited ${codexLbRepair.code}: ${codexLbRepair.stderr}`);
1122
1257
  const codexLbRepairJson = JSON.parse(codexLbRepair.stdout);
1123
1258
  const codexLbRepairedAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1124
- if (!codexLbRepairJson.ok || codexLbRepairJson.status !== 'repaired' || !codexLbRepairedAuth.includes('"auth_mode":"apikey"') || !codexLbRepairedAuth.includes('sk-test')) throw new Error('selftest: codex-lb repair');
1125
- const codexLbLoginCallsBeforePostinstall = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
1259
+ if (!codexLbRepairJson.ok || codexLbRepairJson.status !== 'repaired' || codexLbRepairJson.codex_environment?.ok !== true || codexLbRepairJson.codex_login?.status !== 'skipped' || !codexLbRepairedAuth.includes('"auth_mode":"browser"') || codexLbRepairedAuth.includes('sk-test')) throw new Error('selftest: codex-lb repair');
1260
+ const codexLbLegacyRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'auth', 'repair', '--json'], { cwd: tmp, env: { ...codexLbEnvForSelftest, SKS_CODEX_LB_SYNC_CODEX_LOGIN: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
1261
+ if (codexLbLegacyRepair.code !== 0) throw new Error(`selftest: codex-lb legacy login repair exited ${codexLbLegacyRepair.code}: ${codexLbLegacyRepair.stderr}`);
1262
+ const codexLbLegacyRepairJson = JSON.parse(codexLbLegacyRepair.stdout);
1263
+ const codexLbLegacyAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1264
+ if (!codexLbLegacyRepairJson.ok || codexLbLegacyRepairJson.codex_login?.status !== 'synced' || !codexLbLegacyAuth.includes('"auth_mode":"apikey"') || !codexLbLegacyAuth.includes('sk-test')) throw new Error('selftest: codex-lb legacy login repair');
1265
+ const codexLbLoginCallsBeforePostinstall = await codexLbLoginCallCount(codexLbHome);
1126
1266
  await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"browser"}\n');
1127
1267
  const codexLbPostinstall = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], {
1128
1268
  cwd: tmp,
1129
- env: {
1130
- ...codexLbEnvForSelftest,
1131
- SKS_POSTINSTALL_NO_BOOTSTRAP: '1',
1132
- SKS_SKIP_POSTINSTALL_SHIM: '1',
1133
- SKS_SKIP_POSTINSTALL_CONTEXT7: '1',
1134
- SKS_SKIP_POSTINSTALL_GETDESIGN: '1',
1135
- SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1',
1136
- SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0'
1137
- },
1269
+ env: codexLbPostinstallEnv(codexLbEnvForSelftest),
1138
1270
  timeoutMs: 15000,
1139
1271
  maxOutputBytes: 128 * 1024
1140
1272
  });
1141
1273
  if (codexLbPostinstall.code !== 0) throw new Error(`selftest: codex-lb postinstall auth preservation exited ${codexLbPostinstall.code}: ${codexLbPostinstall.stderr}`);
1142
1274
  const codexLbPostinstallAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1143
- const codexLbLoginCallsAfterPostinstall = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
1144
- if (!String(codexLbPostinstall.stdout || '').includes('codex-lb auth: preserved') || !codexLbPostinstallAuth.includes('"auth_mode":"apikey"') || !codexLbPostinstallAuth.includes('sk-test') || codexLbLoginCallsAfterPostinstall <= codexLbLoginCallsBeforePostinstall) throw new Error('selftest: postinstall auth');
1145
- const postinstallEnvKeys = ['HOME', 'PATH', 'INIT_CWD', 'SKS_GLOBAL_ROOT', 'SKS_POSTINSTALL_BOOTSTRAP', 'SKS_POSTINSTALL_NO_BOOTSTRAP', 'SKS_SKIP_POSTINSTALL_SHIM', 'SKS_SKIP_POSTINSTALL_CONTEXT7', 'SKS_SKIP_POSTINSTALL_GETDESIGN', 'SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS', 'SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH'];
1275
+ const codexLbLoginCallsAfterPostinstall = await codexLbLoginCallCount(codexLbHome);
1276
+ if (!String(codexLbPostinstall.stdout || '').includes('codex-lb auth: preserved') || !codexLbPostinstallAuth.includes('"auth_mode":"browser"') || codexLbPostinstallAuth.includes('sk-test') || codexLbLoginCallsAfterPostinstall !== codexLbLoginCallsBeforePostinstall) throw new Error('selftest: postinstall auth');
1277
+ const postinstallEnvKeys = ['HOME', 'PATH', 'INIT_CWD', 'SKS_GLOBAL_ROOT', 'SKS_POSTINSTALL_BOOTSTRAP', 'SKS_POSTINSTALL_NO_BOOTSTRAP', 'SKS_SKIP_POSTINSTALL_SHIM', 'SKS_SKIP_POSTINSTALL_CONTEXT7', 'SKS_SKIP_POSTINSTALL_GETDESIGN', 'SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS', 'SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH', 'SKS_SKIP_CODEX_LB_LAUNCH_ENV', 'SKS_CODEX_LB_SYNC_CODEX_LOGIN'];
1146
1278
  const postinstallEnvBefore = Object.fromEntries(postinstallEnvKeys.map((key) => [key, process.env[key]]));
1147
- const codexLbLoginCallsBeforeBootstrap = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
1279
+ const codexLbLoginCallsBeforeBootstrap = await codexLbLoginCallCount(codexLbHome);
1148
1280
  try {
1149
1281
  for (const key of postinstallEnvKeys) delete process.env[key];
1150
1282
  Object.assign(process.env, {
@@ -1157,7 +1289,8 @@ export async function selftestCodexLb(tmp) {
1157
1289
  SKS_SKIP_POSTINSTALL_CONTEXT7: '1',
1158
1290
  SKS_SKIP_POSTINSTALL_GETDESIGN: '1',
1159
1291
  SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1',
1160
- SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0'
1292
+ SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0',
1293
+ SKS_SKIP_CODEX_LB_LAUNCH_ENV: '1'
1161
1294
  });
1162
1295
  await postinstall({
1163
1296
  bootstrap: async () => {
@@ -1173,8 +1306,8 @@ export async function selftestCodexLb(tmp) {
1173
1306
  }
1174
1307
  const codexLbPostBootstrapAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1175
1308
  const codexLbPostBootstrapConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
1176
- const codexLbLoginCallsAfterBootstrap = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
1177
- if (!codexLbPostBootstrapAuth.includes('"auth_mode":"apikey"') || !codexLbPostBootstrapAuth.includes('sk-test') || codexLbLoginCallsAfterBootstrap <= codexLbLoginCallsBeforeBootstrap) throw new Error('selftest: postinstall drift auth');
1309
+ const codexLbLoginCallsAfterBootstrap = await codexLbLoginCallCount(codexLbHome);
1310
+ if (!codexLbPostBootstrapAuth.includes('"auth_mode":"browser"') || codexLbPostBootstrapAuth.includes('sk-test') || codexLbLoginCallsAfterBootstrap !== codexLbLoginCallsBeforeBootstrap) throw new Error('selftest: postinstall drift auth');
1178
1311
  if (hasTopLevelCodexLbSelected(codexLbPostBootstrapConfig) || !codexLbPostBootstrapConfig.includes('[model_providers.codex-lb]') || !codexLbPostBootstrapConfig.includes('https://lb.example.test/backend-api/codex') || codexLbPostBootstrapConfig.includes('sk-test')) throw new Error('selftest: postinstall drift config');
1179
1312
  const doctorProject = tmpdir();
1180
1313
  await ensureDir(path.join(doctorProject, '.git'));
@@ -1192,7 +1325,7 @@ export async function selftestCodexLb(tmp) {
1192
1325
  const codexLbDoctorJson = JSON.parse(codexLbDoctorRepair.stdout);
1193
1326
  const codexLbDoctorAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1194
1327
  const codexLbDoctorConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
1195
- if (!codexLbDoctorJson.repair?.codex_lb?.ok || !codexLbDoctorJson.repair.codex_lb.config_repaired || !codexLbDoctorJson.codex_lb?.ok || !codexLbDoctorAuth.includes('"auth_mode":"apikey"') || !codexLbDoctorAuth.includes('sk-test') || hasTopLevelCodexLbSelected(codexLbDoctorConfig) || !codexLbDoctorConfig.includes('https://lb.example.test/backend-api/codex') || !hasCodexUnstableFeatureWarningSuppression(codexLbDoctorConfig)) throw new Error('selftest: doctor codex-lb');
1328
+ if (!codexLbDoctorJson.repair?.codex_lb?.ok || !codexLbDoctorJson.repair.codex_lb.config_repaired || !codexLbDoctorJson.codex_lb?.ok || !codexLbDoctorAuth.includes('"auth_mode":"browser"') || codexLbDoctorAuth.includes('sk-test') || hasTopLevelCodexLbSelected(codexLbDoctorConfig) || !codexLbDoctorConfig.includes('https://lb.example.test/backend-api/codex') || !hasCodexUnstableFeatureWarningSuppression(codexLbDoctorConfig)) throw new Error('selftest: doctor codex-lb');
1196
1329
  const codexLbContext7Bin = path.join(tmp, 'codex-lb-context7-bin');
1197
1330
  await ensureDir(codexLbContext7Bin);
1198
1331
  await writeTextAtomic(path.join(codexLbContext7Bin, 'codex'), '#!/bin/sh\nif [ "$1" = "--version" ]; then echo "codex-cli 99.0.0"; exit 0; fi\nif [ "$CODEX_LB_API_KEY" ]; then echo "context7 leaked CODEX_LB_API_KEY" >&2; exit 77; fi\nif [ "$1" = "mcp" ] && [ "$2" = "list" ]; then echo ""; exit 0; fi\nif [ "$1" = "mcp" ] && [ "$2" = "add" ]; then echo "context7 added"; exit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
@@ -1214,23 +1347,82 @@ export async function selftestCodexLb(tmp) {
1214
1347
  });
1215
1348
  if (codexLbContext7Postinstall.code !== 0 || String(`${codexLbContext7Postinstall.stdout}\n${codexLbContext7Postinstall.stderr}`).includes('leaked CODEX_LB_API_KEY')) throw new Error('selftest: Context7 key leak');
1216
1349
  await writeTextAtomic(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), "export CODEX_LB_API_KEY='unterminated\n");
1217
- const codexLbLoginCallsBeforeMalformed = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
1350
+ const codexLbLoginCallsBeforeMalformed = await codexLbLoginCallCount(codexLbHome);
1218
1351
  const codexLbMalformedPostinstall = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], {
1219
1352
  cwd: tmp,
1220
- env: {
1221
- ...codexLbEnvForSelftest,
1222
- SKS_POSTINSTALL_NO_BOOTSTRAP: '1',
1223
- SKS_SKIP_POSTINSTALL_SHIM: '1',
1224
- SKS_SKIP_POSTINSTALL_CONTEXT7: '1',
1225
- SKS_SKIP_POSTINSTALL_GETDESIGN: '1',
1226
- SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1',
1227
- SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0'
1228
- },
1353
+ env: codexLbPostinstallEnv(codexLbEnvForSelftest),
1229
1354
  timeoutMs: 15000,
1230
1355
  maxOutputBytes: 128 * 1024
1231
1356
  });
1232
- const codexLbLoginCallsAfterMalformed = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
1357
+ const codexLbLoginCallsAfterMalformed = await codexLbLoginCallCount(codexLbHome);
1233
1358
  if (codexLbMalformedPostinstall.code !== 0 || !String(codexLbMalformedPostinstall.stdout || '').includes('codex-lb auth: stored key missing') || codexLbLoginCallsAfterMalformed !== codexLbLoginCallsBeforeMalformed) throw new Error('selftest: bad codex-lb env');
1359
+ await fsp.rm(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), { force: true });
1360
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), '[model_providers.codex-lb]\nname = "OpenAI"\nbase_url = "https://lb.example.test/backend-api/codex"\nwire_api = "responses"\nsupports_websockets = true\nrequires_openai_auth = true\n');
1361
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"apikey","key":"sk-legacy"}\n');
1362
+ const codexLbLoginCallsBeforeLegacyPostinstall = await codexLbLoginCallCount(codexLbHome);
1363
+ const codexLbLegacyPostinstall = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], {
1364
+ cwd: tmp,
1365
+ env: codexLbPostinstallEnv(codexLbEnvForSelftest),
1366
+ timeoutMs: 15000,
1367
+ maxOutputBytes: 128 * 1024
1368
+ });
1369
+ if (codexLbLegacyPostinstall.code !== 0) throw new Error(`selftest: legacy codex-lb postinstall restore exited ${codexLbLegacyPostinstall.code}: ${codexLbLegacyPostinstall.stderr}`);
1370
+ const codexLbLegacyPostinstallEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
1371
+ const codexLbLegacyPostinstallAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1372
+ const codexLbLoginCallsAfterLegacyPostinstall = await codexLbLoginCallCount(codexLbHome);
1373
+ if (!String(codexLbLegacyPostinstall.stdout || '').includes('codex-lb auth: restored from existing Codex login cache') || !codexLbLegacyPostinstallEnv.includes("CODEX_LB_API_KEY='sk-legacy'") || !codexLbLegacyPostinstallEnv.includes("CODEX_LB_BASE_URL='https://lb.example.test/backend-api/codex'") || !codexLbLegacyPostinstallAuth.includes('"auth_mode":"apikey"') || !codexLbLegacyPostinstallAuth.includes('sk-legacy') || codexLbLoginCallsAfterLegacyPostinstall !== codexLbLoginCallsBeforeLegacyPostinstall) throw new Error('selftest: legacy codex-lb postinstall restore');
1374
+ await fsp.rm(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), { force: true });
1375
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model_provider = "codex-lb"\n\n[model_providers.codex-lb]\nname = "OpenAI"\nbase_url = "https://lb.example.test/backend-api/codex"\nwire_api = "responses"\nsupports_websockets = true\nrequires_openai_auth = true\n');
1376
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"apikey","key":"sk-legacy-doctor"}\n');
1377
+ const codexLbLegacyDoctorProject = tmpdir();
1378
+ await ensureDir(path.join(codexLbLegacyDoctorProject, '.git'));
1379
+ await writeTextAtomic(path.join(codexLbLegacyDoctorProject, 'package.json'), '{"name":"codex-lb-legacy-doctor-project","version":"0.0.0"}\n');
1380
+ const codexLbLegacyDoctorRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'doctor', '--fix', '--json'], {
1381
+ cwd: codexLbLegacyDoctorProject,
1382
+ env: { ...codexLbEnvForSelftest, SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-legacy-doctor-global'), SKS_SKIP_CODEX_LB_LAUNCH_ENV: '1' },
1383
+ timeoutMs: 30000,
1384
+ maxOutputBytes: 256 * 1024
1385
+ });
1386
+ if (codexLbLegacyDoctorRepair.code !== 0) throw new Error(`selftest: legacy doctor --fix codex-lb restore exited ${codexLbLegacyDoctorRepair.code}: ${codexLbLegacyDoctorRepair.stderr}`);
1387
+ const codexLbLegacyDoctorJson = JSON.parse(codexLbLegacyDoctorRepair.stdout);
1388
+ const codexLbLegacyDoctorEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
1389
+ const codexLbLegacyDoctorConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
1390
+ const codexLbLegacyDoctorAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1391
+ if (!codexLbLegacyDoctorJson.repair?.codex_lb?.ok || !codexLbLegacyDoctorJson.repair.codex_lb.legacy_auth_migrated || !codexLbLegacyDoctorEnv.includes("CODEX_LB_API_KEY='sk-legacy-doctor'") || !codexLbLegacyDoctorAuth.includes('"auth_mode":"apikey"') || !codexLbLegacyDoctorAuth.includes('sk-legacy-doctor') || hasTopLevelCodexLbSelected(codexLbLegacyDoctorConfig) || !codexLbLegacyDoctorConfig.includes('env_key = "CODEX_LB_API_KEY"')) throw new Error('selftest: legacy doctor codex-lb restore');
1392
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), "export CODEX_LB_BASE_URL='https://lb.example.test/backend-api/codex'\n");
1393
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nservice_tier = "fast"\n');
1394
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"apikey","key":"sk-env-only"}\n');
1395
+ const codexLbLoginCallsBeforeEnvOnlyPostinstall = await codexLbLoginCallCount(codexLbHome);
1396
+ const codexLbEnvOnlyPostinstall = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], {
1397
+ cwd: tmp,
1398
+ env: codexLbPostinstallEnv(codexLbEnvForSelftest),
1399
+ timeoutMs: 15000,
1400
+ maxOutputBytes: 128 * 1024
1401
+ });
1402
+ if (codexLbEnvOnlyPostinstall.code !== 0) throw new Error(`selftest: env-only codex-lb postinstall restore exited ${codexLbEnvOnlyPostinstall.code}: ${codexLbEnvOnlyPostinstall.stderr}`);
1403
+ const codexLbEnvOnlyPostinstallEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
1404
+ const codexLbEnvOnlyPostinstallConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
1405
+ const codexLbEnvOnlyPostinstallAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1406
+ const codexLbLoginCallsAfterEnvOnlyPostinstall = await codexLbLoginCallCount(codexLbHome);
1407
+ if (!String(codexLbEnvOnlyPostinstall.stdout || '').includes('codex-lb auth: restored from existing Codex login cache') || !codexLbEnvOnlyPostinstallEnv.includes("CODEX_LB_API_KEY='sk-env-only'") || !codexLbEnvOnlyPostinstallConfig.includes('env_key = "CODEX_LB_API_KEY"') || !codexLbEnvOnlyPostinstallAuth.includes('sk-env-only') || codexLbLoginCallsAfterEnvOnlyPostinstall !== codexLbLoginCallsBeforeEnvOnlyPostinstall) throw new Error('selftest: env-only codex-lb postinstall restore');
1408
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), "export CODEX_LB_BASE_URL='https://lb.example.test/backend-api/codex'\n");
1409
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nservice_tier = "fast"\n');
1410
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"apikey","key":"sk-env-only-doctor"}\n');
1411
+ const codexLbEnvOnlyDoctorProject = tmpdir();
1412
+ await ensureDir(path.join(codexLbEnvOnlyDoctorProject, '.git'));
1413
+ await writeTextAtomic(path.join(codexLbEnvOnlyDoctorProject, 'package.json'), '{"name":"codex-lb-env-only-doctor-project","version":"0.0.0"}\n');
1414
+ const codexLbEnvOnlyDoctorRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'doctor', '--fix', '--json'], {
1415
+ cwd: codexLbEnvOnlyDoctorProject,
1416
+ env: { ...codexLbEnvForSelftest, SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-env-only-doctor-global'), SKS_SKIP_CODEX_LB_LAUNCH_ENV: '1' },
1417
+ timeoutMs: 30000,
1418
+ maxOutputBytes: 256 * 1024
1419
+ });
1420
+ if (codexLbEnvOnlyDoctorRepair.code !== 0) throw new Error(`selftest: env-only doctor --fix codex-lb restore exited ${codexLbEnvOnlyDoctorRepair.code}: ${codexLbEnvOnlyDoctorRepair.stderr}`);
1421
+ const codexLbEnvOnlyDoctorJson = JSON.parse(codexLbEnvOnlyDoctorRepair.stdout);
1422
+ const codexLbEnvOnlyDoctorEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
1423
+ const codexLbEnvOnlyDoctorConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
1424
+ const codexLbEnvOnlyDoctorAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1425
+ if (!codexLbEnvOnlyDoctorJson.repair?.codex_lb?.ok || !codexLbEnvOnlyDoctorJson.repair.codex_lb.legacy_auth_migrated || !codexLbEnvOnlyDoctorEnv.includes("CODEX_LB_API_KEY='sk-env-only-doctor'") || !codexLbEnvOnlyDoctorConfig.includes('env_key = "CODEX_LB_API_KEY"') || !codexLbEnvOnlyDoctorAuth.includes('sk-env-only-doctor')) throw new Error('selftest: env-only doctor codex-lb restore');
1234
1426
  await writeTextAtomic(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), "export CODEX_LB_API_KEY='sk-test'\n");
1235
1427
  const codexLbMissingCli = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], {
1236
1428
  cwd: tmp,
@@ -1243,12 +1435,13 @@ export async function selftestCodexLb(tmp) {
1243
1435
  SKS_SKIP_POSTINSTALL_CONTEXT7: '1',
1244
1436
  SKS_SKIP_POSTINSTALL_GETDESIGN: '1',
1245
1437
  SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1',
1246
- SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0'
1438
+ SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0',
1439
+ SKS_SKIP_CODEX_LB_LAUNCH_ENV: '1'
1247
1440
  },
1248
1441
  timeoutMs: 15000,
1249
1442
  maxOutputBytes: 128 * 1024
1250
1443
  });
1251
- if (codexLbMissingCli.code !== 0 || !String(codexLbMissingCli.stdout || '').includes('codex-lb auth: repair skipped (codex_missing')) throw new Error('selftest: codex missing');
1444
+ if (codexLbMissingCli.code !== 0 || !String(codexLbMissingCli.stdout || '').includes('codex-lb auth: preserved') || String(codexLbMissingCli.stdout || '').includes('codex_missing')) throw new Error('selftest: codex-lb provider auth should not require Codex CLI login');
1252
1445
  const codexLbNotConfiguredHome = path.join(tmp, 'codex-lb-not-configured-home');
1253
1446
  const codexLbNotConfigured = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], {
1254
1447
  cwd: tmp,
@@ -1268,7 +1461,7 @@ export async function selftestCodexLb(tmp) {
1268
1461
  });
1269
1462
  if (codexLbNotConfigured.code !== 0 || String(codexLbNotConfigured.stdout || '').includes('codex-lb auth:')) throw new Error('selftest: postinstall should stay quiet when codex-lb is not configured');
1270
1463
  const codexLbStatusText = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'status'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
1271
- if (!String(codexLbStatusText.stdout || '').includes('Repair auth: sks codex-lb repair')) throw new Error('selftest: codex-lb status did not advertise repair command');
1464
+ if (!String(codexLbStatusText.stdout || '').includes('Repair provider auth: sks codex-lb repair')) throw new Error('selftest: codex-lb status did not advertise repair command');
1272
1465
  const nonInteractiveLaunchChainCalls = [];
1273
1466
  const nonInteractiveLaunch = await maybePromptCodexLbSetupForLaunch([], {
1274
1467
  home: codexLbHome,
package/src/cli/main.mjs CHANGED
@@ -1149,7 +1149,7 @@ async function codexLbCommand(action = 'status', args = []) {
1149
1149
  console.log(`Env file: ${status.env_file ? status.env_path : 'missing'}`);
1150
1150
  if (status.base_url) console.log(`Base URL: ${status.base_url}`);
1151
1151
  if (!status.ok) console.log('\nRun: sks codex-lb setup --host <domain> --api-key <key>');
1152
- else console.log('\nRepair auth: sks codex-lb repair');
1152
+ else console.log('\nRepair provider auth: sks codex-lb repair');
1153
1153
  return;
1154
1154
  }
1155
1155
  if (sub === 'health' || sub === 'verify-chain' || sub === 'chain') {
@@ -1176,7 +1176,7 @@ async function codexLbCommand(action = 'status', args = []) {
1176
1176
  process.exitCode = 1;
1177
1177
  return;
1178
1178
  }
1179
- console.log('codex-lb auth repaired for Codex CLI.');
1179
+ console.log('codex-lb provider auth repaired for Codex CLI/App environment.');
1180
1180
  console.log(`Config: ${result.config_path}`);
1181
1181
  console.log(`Key env: ${result.env_path}`);
1182
1182
  return;
@@ -1193,7 +1193,7 @@ async function codexLbCommand(action = 'status', args = []) {
1193
1193
  const result = await configureCodexLb({ host, apiKey });
1194
1194
  if (json) return console.log(JSON.stringify(result, null, 2));
1195
1195
  if (!result.ok) {
1196
- console.error(`codex-lb setup failed: ${result.status}`);
1196
+ console.error(`codex-lb setup failed: ${result.status}${result.error ? `: ${result.error}` : ''}`);
1197
1197
  process.exitCode = 1;
1198
1198
  return;
1199
1199
  }
@@ -1779,7 +1779,7 @@ async function doctor(args) {
1779
1779
  const cleanup = removed.length ? ` removed stale generated skill shadow(s): ${removed.join(', ')}` : '';
1780
1780
  console.log(`Global $ repair: ${globalSkillsRepair.status} ${globalSkillsRepair.root || ''}${cleanup}`.trimEnd());
1781
1781
  }
1782
- if (codexLbRepair?.ok) console.log(`codex-lb repair: ${codexLbRepair.config_repaired ? 'config+auth resynced' : 'auth resynced'} from stored env`);
1782
+ if (codexLbRepair?.ok) console.log(`codex-lb repair: ${codexLbRepair.config_repaired ? 'config+provider auth resynced' : 'provider auth resynced'} from stored env`);
1783
1783
  else if (codexLbRepair && codexLbRepair.status !== 'missing_env_key') console.log(`codex-lb repair: skipped (${codexLbRepair.status})`);
1784
1784
  if (flag(args, '--fix') && result.harness_conflicts.hard_block) console.log('Repair: skipped because another Codex harness needs human-approved removal first');
1785
1785
  console.log(`Rust acc.: ${rust.available ? rust.version : 'optional-missing'}`);
package/src/core/fsx.mjs CHANGED
@@ -5,7 +5,7 @@ import os from 'node:os';
5
5
  import crypto from 'node:crypto';
6
6
  import { spawn } from 'node:child_process';
7
7
 
8
- export const PACKAGE_VERSION = '0.8.4';
8
+ export const PACKAGE_VERSION = '0.8.5';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
 
@@ -523,8 +523,8 @@ export const COMMAND_CATALOG = [
523
523
  { name: 'root', usage: 'sks root [--json]', description: 'Show whether SKS is using a project root or the per-user global SKS runtime root.' },
524
524
  { name: 'deps', usage: 'sks deps check|install [tmux|codex|context7|all] [--yes]', description: 'Check or guided-install Node/npm PATH, Codex CLI/App, Context7, Browser tooling, Computer Use, tmux, and Homebrew on macOS.' },
525
525
  { name: 'codex-app', usage: 'sks codex-app [check|open|remote-control]', description: 'Check Codex App install and first-party MCP/plugin readiness, then show app setup files, examples, and Codex CLI 0.130.0+ remote-control availability.' },
526
- { name: 'codex-lb', usage: 'sks codex-lb status|health|repair|setup --host <domain> --api-key <key>', description: 'Configure, health-check, or repair codex-lb Codex CLI auth by writing ~/.codex/config.toml, syncing auth.json, and loading the CODEX_LB_API_KEY env file.' },
527
- { name: 'auth', usage: 'sks auth status|health|repair|setup --host <domain> --api-key <key>', description: 'Shortcut for codex-lb auth status, health, repair, and setup commands.' },
526
+ { name: 'codex-lb', usage: 'sks codex-lb status|health|repair|setup --host <domain> --api-key <key>', description: 'Configure, health-check, or repair codex-lb provider auth by writing ~/.codex/config.toml, restoring CODEX_LB_API_KEY env auth from stored or legacy login-cache state, and preserving the shared Codex login cache unless explicitly requested.' },
527
+ { name: 'auth', usage: 'sks auth status|health|repair|setup --host <domain> --api-key <key>', description: 'Shortcut for codex-lb provider auth status, health, repair, and setup commands.' },
528
528
  { name: 'openclaw', usage: 'sks openclaw install|path|print [--dir path] [--force] [--json]', description: 'Generate an OpenClaw skill package so OpenClaw agents can discover and use local SKS workflows.' },
529
529
  { name: 'tmux', usage: 'sks | sks tmux open|check|status [--workspace name]', description: 'Open the default SKS tmux runtime with bare sks, or use tmux subcommands for explicit launch/check/status.' },
530
530
  { name: 'mad', usage: 'sks --mad [--high]', description: 'Open a one-shot tmux Codex CLI workspace with the SKS MAD full-access auto-review profile.' },