sneakoscope 0.9.7 → 0.9.9
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 +5 -1
- package/package.json +4 -2
- package/src/cli/feature-commands.mjs +131 -0
- package/src/cli/install-helpers.mjs +246 -44
- package/src/cli/main.mjs +41 -21
- package/src/core/codex-app.mjs +32 -0
- package/src/core/feature-registry.mjs +461 -0
- package/src/core/fsx.mjs +1 -1
- package/src/core/routes.mjs +5 -2
package/README.md
CHANGED
|
@@ -210,6 +210,8 @@ Flags:
|
|
|
210
210
|
|
|
211
211
|
`sks codex-lb status` reports whether a ChatGPT OAuth backup is present and shows the `sks codex-lb release` hint when applicable. `sks doctor` surfaces the same hint.
|
|
212
212
|
|
|
213
|
+
If Codex App shows `access token could not be refreshed` after codex-lb setup or status checks, recover the ChatGPT OAuth side without discarding codex-lb: run `sks codex-lb status`, then `sks codex-lb repair`. Repair restores a ChatGPT OAuth backup when one exists while keeping `model_provider = "codex-lb"` selected and the codex-lb key in `CODEX_LB_API_KEY`. If no OAuth backup exists, sign in again in Codex App/CLI, then rerun `sks codex-lb repair`. Use `sks codex-lb release` only when you want to switch fully away from codex-lb.
|
|
214
|
+
|
|
213
215
|
If you only want to stop routing through codex-lb without touching `auth.json`, use the lighter `sks codex-lb unselect` instead:
|
|
214
216
|
|
|
215
217
|
```sh
|
|
@@ -491,6 +493,8 @@ Run local checks:
|
|
|
491
493
|
npm run repo-audit
|
|
492
494
|
npm run changelog:check
|
|
493
495
|
npm run packcheck
|
|
496
|
+
npm run feature:check
|
|
497
|
+
npm run all-features:selftest
|
|
494
498
|
npm run selftest
|
|
495
499
|
npm run sizecheck
|
|
496
500
|
npm run registry:check
|
|
@@ -498,7 +502,7 @@ npm run release:check
|
|
|
498
502
|
npm run publish:dry
|
|
499
503
|
```
|
|
500
504
|
|
|
501
|
-
`release:check` runs audit, changelog, syntax, selftest, size, and registry checks. `publish:dry` runs that same gate and then performs an npm dry-run publish against the public registry.
|
|
505
|
+
`release:check` runs audit, changelog, syntax, feature-registry coverage, all-features contract selftest, selftest, size, and registry checks. Generate the human-readable registry with `sks features inventory --write-docs`. `publish:dry` runs that same gate and then performs an npm dry-run publish against the public registry.
|
|
502
506
|
|
|
503
507
|
Version bumps are manual. Run `sks versioning bump` only when preparing release metadata; SKS will not create `.git/hooks/pre-commit` or auto-bump during ordinary commits.
|
|
504
508
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sneakoscope",
|
|
3
3
|
"displayName": "ㅅㅋㅅ",
|
|
4
|
-
"version": "0.9.
|
|
4
|
+
"version": "0.9.9",
|
|
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",
|
|
@@ -38,7 +38,9 @@
|
|
|
38
38
|
"changelog:check": "node ./scripts/changelog-check.mjs",
|
|
39
39
|
"sizecheck": "node ./scripts/sizecheck.mjs",
|
|
40
40
|
"registry:check": "node ./scripts/release-registry-check.mjs",
|
|
41
|
-
"
|
|
41
|
+
"feature:check": "node ./bin/sks.mjs features check --json",
|
|
42
|
+
"all-features:selftest": "node ./bin/sks.mjs all-features selftest --mock --json",
|
|
43
|
+
"release:check": "npm run repo-audit && npm run changelog:check && npm run packcheck && npm run feature:check && npm run all-features:selftest && npm run selftest && npm run sizecheck && npm run registry:check",
|
|
42
44
|
"publish:dry": "npm run release:check && npm --cache /tmp/sks-npm-cache publish --dry-run --registry https://registry.npmjs.org/ --access public",
|
|
43
45
|
"publish:npm": "npm --cache /tmp/sks-npm-cache publish --registry https://registry.npmjs.org/ --access public",
|
|
44
46
|
"prepublishOnly": "npm run release:check && node ./scripts/release-registry-check.mjs --require-unpublished"
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { projectRoot } from '../core/fsx.mjs';
|
|
3
|
+
import { CODEX_ACCESS_TOKENS_DOCS_URL } from '../core/codex-app.mjs';
|
|
4
|
+
import { buildAllFeaturesSelftest, buildFeatureRegistry, validateFeatureRegistry, writeFeatureInventoryDocs } from '../core/feature-registry.mjs';
|
|
5
|
+
|
|
6
|
+
const flag = (args, name) => args.includes(name);
|
|
7
|
+
|
|
8
|
+
export async function featuresCommand(sub = 'list', args = []) {
|
|
9
|
+
const action = sub || 'list';
|
|
10
|
+
const root = await projectRoot();
|
|
11
|
+
if (action === 'list' || action === 'status' || action === 'registry') {
|
|
12
|
+
const registry = await buildFeatureRegistry({ root });
|
|
13
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(registry, null, 2));
|
|
14
|
+
printFeatureRegistrySummary(registry);
|
|
15
|
+
if (!registry.coverage.ok) process.exitCode = 1;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (action === 'check') {
|
|
19
|
+
const registry = await buildFeatureRegistry({ root });
|
|
20
|
+
const coverage = validateFeatureRegistry(registry);
|
|
21
|
+
const result = { schema: 'sks.feature-registry-check.v1', generated_at: registry.generated_at, ok: coverage.ok, coverage };
|
|
22
|
+
if (flag(args, '--json')) console.log(JSON.stringify(result, null, 2));
|
|
23
|
+
else printFeatureCoverage(coverage);
|
|
24
|
+
if (!coverage.ok) process.exitCode = 1;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (action === 'inventory') {
|
|
28
|
+
const writeDocs = flag(args, '--write-docs');
|
|
29
|
+
const result = writeDocs
|
|
30
|
+
? await writeFeatureInventoryDocs({ root })
|
|
31
|
+
: { ok: true, registry: await buildFeatureRegistry({ root }), path: path.join(root, 'docs', 'feature-inventory.md') };
|
|
32
|
+
if (flag(args, '--json')) return console.log(JSON.stringify({ ok: result.ok, path: result.path, coverage: result.registry.coverage }, null, 2));
|
|
33
|
+
if (writeDocs) console.log(`Feature inventory written: ${path.relative(root, result.path)}`);
|
|
34
|
+
printFeatureRegistrySummary(result.registry);
|
|
35
|
+
if (!result.registry.coverage.ok) process.exitCode = 1;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
console.error('Usage: sks features list|check|inventory [--json] [--write-docs]');
|
|
39
|
+
process.exitCode = 1;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function allFeaturesCommand(sub = 'selftest', args = []) {
|
|
43
|
+
const action = sub || 'selftest';
|
|
44
|
+
if (action !== 'selftest') {
|
|
45
|
+
console.error('Usage: sks all-features selftest --mock [--json]');
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const root = await projectRoot();
|
|
50
|
+
const registry = await buildFeatureRegistry({ root });
|
|
51
|
+
const result = buildAllFeaturesSelftest(registry);
|
|
52
|
+
if (flag(args, '--json')) console.log(JSON.stringify(result, null, 2));
|
|
53
|
+
else {
|
|
54
|
+
console.log('SKS all-features selftest');
|
|
55
|
+
console.log(`Status: ${result.status}`);
|
|
56
|
+
for (const check of result.checks) console.log(`- ${check.ok ? 'ok' : 'blocked'} ${check.id}${check.blockers.length ? `: ${check.blockers.join(', ')}` : ''}`);
|
|
57
|
+
if (result.note) console.log(`\n${result.note}`);
|
|
58
|
+
}
|
|
59
|
+
if (!result.ok) process.exitCode = 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function hooksCommand(sub = 'explain', args = []) {
|
|
63
|
+
const action = sub || 'explain';
|
|
64
|
+
if (action !== 'explain' && action !== 'status') {
|
|
65
|
+
console.error('Usage: sks hooks explain [--json]');
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const report = hooksExplainReport();
|
|
70
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(report, null, 2));
|
|
71
|
+
console.log('SKS hooks explain\n');
|
|
72
|
+
console.log(`Status: ${report.status}`);
|
|
73
|
+
console.log(`Feature key: ${report.feature_key}`);
|
|
74
|
+
console.log(`Config paths: ${report.config_paths.join(', ')}`);
|
|
75
|
+
console.log(`Events: ${report.events.join(', ')}`);
|
|
76
|
+
console.log(`Handlers: ${report.handlers.supported.join(', ')} supported; ${report.handlers.parsed_but_skipped.join(', ')} parsed but skipped`);
|
|
77
|
+
console.log('\nPolicies:');
|
|
78
|
+
for (const policy of report.sks_policies) console.log(`- ${policy}`);
|
|
79
|
+
console.log('\nSources:');
|
|
80
|
+
for (const source of report.sources) console.log(`- ${source.title}: ${source.url}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function hooksExplainReport() {
|
|
84
|
+
return {
|
|
85
|
+
schema: 'sks.hooks-explain.v1',
|
|
86
|
+
status: 'supported_by_official_docs_and_local_config',
|
|
87
|
+
feature_key: 'features.hooks',
|
|
88
|
+
deprecated_feature_alias: 'features.codex_hooks',
|
|
89
|
+
config_paths: ['~/.codex/hooks.json', '~/.codex/config.toml', '<repo>/.codex/hooks.json', '<repo>/.codex/config.toml'],
|
|
90
|
+
events: ['SessionStart', 'PreToolUse', 'PermissionRequest', 'PostToolUse', 'UserPromptSubmit', 'Stop'],
|
|
91
|
+
handlers: {
|
|
92
|
+
supported: ['command'],
|
|
93
|
+
parsed_but_skipped: ['prompt', 'agent'],
|
|
94
|
+
async_command_hooks: 'parsed_but_not_supported'
|
|
95
|
+
},
|
|
96
|
+
runtime_notes: [
|
|
97
|
+
'Matching hooks from multiple files all run.',
|
|
98
|
+
'Multiple matching command hooks for the same event are launched concurrently.',
|
|
99
|
+
'Project-local hooks require a trusted project .codex layer.',
|
|
100
|
+
'Repo-local hook commands should resolve from git root instead of assuming the session cwd.'
|
|
101
|
+
],
|
|
102
|
+
sks_policies: [
|
|
103
|
+
'secret_scan_policy',
|
|
104
|
+
'directory_rule_policy',
|
|
105
|
+
'db_safety_policy',
|
|
106
|
+
'visual_claim_source_policy',
|
|
107
|
+
'proof_required_policy',
|
|
108
|
+
'codex_lb_health_policy'
|
|
109
|
+
],
|
|
110
|
+
sources: [
|
|
111
|
+
{ title: 'OpenAI Codex Hooks', url: 'https://developers.openai.com/codex/hooks' },
|
|
112
|
+
{ title: 'OpenAI Codex Configuration Reference', url: 'https://developers.openai.com/codex/config-reference' },
|
|
113
|
+
{ title: 'OpenAI Codex Access Tokens', url: CODEX_ACCESS_TOKENS_DOCS_URL }
|
|
114
|
+
]
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function printFeatureRegistrySummary(registry) {
|
|
119
|
+
console.log('SKS feature registry\n');
|
|
120
|
+
console.log(`Schema: ${registry.schema}`);
|
|
121
|
+
console.log(`Features: ${registry.features.length}`);
|
|
122
|
+
printFeatureCoverage(registry.coverage);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function printFeatureCoverage(coverage = {}) {
|
|
126
|
+
console.log(`Coverage: ${coverage.ok ? 'ok' : 'blocked'} (${coverage.status || 'unknown'})`);
|
|
127
|
+
for (const [kind, values] of Object.entries(coverage.unmapped || {})) {
|
|
128
|
+
console.log(`- ${kind}: ${values.length ? values.join(', ') : 'none'}`);
|
|
129
|
+
}
|
|
130
|
+
if (coverage.blockers?.length) console.log(`Blockers: ${coverage.blockers.join(', ')}`);
|
|
131
|
+
}
|
|
@@ -88,8 +88,12 @@ async function reportPostinstallCodexLbAuth() {
|
|
|
88
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.');
|
|
89
89
|
else if (codexLbAuth.status && codexLbAuth.status !== 'not_configured') console.log(`codex-lb auth: repair skipped (${codexLbAuth.status}${codexLbAuth.error ? `: ${codexLbAuth.error}` : ''}).`);
|
|
90
90
|
const reconcile = codexLbAuth.auth_reconcile;
|
|
91
|
-
if (reconcile?.status === '
|
|
92
|
-
console.log(`codex-lb auth:
|
|
91
|
+
if (reconcile?.status === 'oauth_preserved') {
|
|
92
|
+
console.log(`codex-lb auth: ChatGPT OAuth preserved for Codex App; codex-lb key stays in env_key (OAuth backup at ${reconcile.backup_path}).`);
|
|
93
|
+
} else if (reconcile?.status === 'oauth_restored') {
|
|
94
|
+
console.log(`codex-lb auth: restored ChatGPT OAuth from ${reconcile.backup_path} while keeping codex-lb selected.`);
|
|
95
|
+
} else if (reconcile?.status === 'apikey_forced') {
|
|
96
|
+
console.log(`codex-lb auth: forced API-key auth.json for CLI-only use (OAuth backup at ${reconcile.backup_path}).`);
|
|
93
97
|
} else if (reconcile?.status === 'backup_only') {
|
|
94
98
|
console.log(`codex-lb auth: detected ChatGPT OAuth tokens in auth.json. OAuth backup written to ${reconcile.backup_path}; auth.json left untouched because SKS_CODEX_LB_NO_AUTH_RECONCILE=1.`);
|
|
95
99
|
} else if (reconcile?.status === 'failed') {
|
|
@@ -263,6 +267,9 @@ export async function configureCodexLb(opts = {}) {
|
|
|
263
267
|
await fsp.chmod(envPath, 0o600).catch(() => {});
|
|
264
268
|
const codexEnvironment = await syncCodexLbProviderEnvironment({ env_path: envPath, base_url: baseUrl }, { ...opts, home });
|
|
265
269
|
const codexLogin = await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home, force: true });
|
|
270
|
+
const codexLb = await codexLbStatus({ ...opts, home, configPath, envPath });
|
|
271
|
+
const authReconcile = await reconcileCodexLbAuthConflict({ ...opts, home, status: codexLb }).catch((err) => ({ status: 'failed', reason: 'exception', error: err.message }));
|
|
272
|
+
const finalCodexLb = await codexLbStatus({ ...opts, home, configPath, envPath });
|
|
266
273
|
const ok = Boolean(codexEnvironment.ok && codexLogin.ok);
|
|
267
274
|
return {
|
|
268
275
|
ok,
|
|
@@ -271,9 +278,11 @@ export async function configureCodexLb(opts = {}) {
|
|
|
271
278
|
env_path: envPath,
|
|
272
279
|
base_url: baseUrl,
|
|
273
280
|
env_key: 'CODEX_LB_API_KEY',
|
|
281
|
+
auth_reconcile: authReconcile,
|
|
282
|
+
codex_lb: finalCodexLb,
|
|
274
283
|
codex_environment: codexEnvironment,
|
|
275
284
|
codex_login: codexLogin,
|
|
276
|
-
error: codexEnvironment.error || codexLogin.error || null
|
|
285
|
+
error: authReconcile.error || codexEnvironment.error || codexLogin.error || null
|
|
277
286
|
};
|
|
278
287
|
}
|
|
279
288
|
|
|
@@ -284,6 +293,9 @@ export async function codexLbStatus(opts = {}) {
|
|
|
284
293
|
const config = await readText(configPath, '');
|
|
285
294
|
const envExists = await exists(envPath);
|
|
286
295
|
const envText = envExists ? await readText(envPath, '') : '';
|
|
296
|
+
const authPath = opts.authPath || codexAuthPath(home);
|
|
297
|
+
const authText = await readText(authPath, '');
|
|
298
|
+
const authMode = codexAuthModeSummary(authText);
|
|
287
299
|
const envKeyConfigured = Boolean(parseCodexLbEnvKey(envText));
|
|
288
300
|
const providerConfigured = /\[model_providers\.codex-lb\]/.test(config);
|
|
289
301
|
const selected = hasTopLevelCodexLbSelected(config);
|
|
@@ -299,10 +311,52 @@ export async function codexLbStatus(opts = {}) {
|
|
|
299
311
|
env_file: envExists,
|
|
300
312
|
env_key_configured: envKeyConfigured,
|
|
301
313
|
env_base_url_configured: Boolean(parseCodexLbEnvBaseUrl(envText)),
|
|
302
|
-
base_url: baseUrl
|
|
314
|
+
base_url: baseUrl,
|
|
315
|
+
auth_path: authPath,
|
|
316
|
+
auth_mode: authMode.mode,
|
|
317
|
+
auth_usable_for_codex_app: authMode.codex_app_usable,
|
|
318
|
+
auth_summary: authMode.summary
|
|
303
319
|
};
|
|
304
320
|
}
|
|
305
321
|
|
|
322
|
+
export function formatCodexLbStatusText(status = {}, opts = {}) {
|
|
323
|
+
const backupPresent = Boolean(opts.backupPresent);
|
|
324
|
+
const backupPath = opts.backupPath || '';
|
|
325
|
+
const lines = [
|
|
326
|
+
'SKS codex-lb',
|
|
327
|
+
'',
|
|
328
|
+
`Configured: ${status.ok ? 'yes' : 'no'}`,
|
|
329
|
+
`Selected: ${status.selected ? 'yes' : 'no'}`,
|
|
330
|
+
`Provider: ${status.provider_configured ? 'yes' : 'no'}`,
|
|
331
|
+
`Provider requires OpenAI auth: ${status.provider_requires_openai_auth ? 'yes' : 'missing'}`,
|
|
332
|
+
`Codex App auth: ${status.auth_usable_for_codex_app ? 'ok' : 'needs sign-in/repair'} (${status.auth_mode || 'unknown'})`
|
|
333
|
+
];
|
|
334
|
+
if (status.auth_summary) lines.push(`Auth detail: ${status.auth_summary}`);
|
|
335
|
+
lines.push(`Env file: ${status.env_file ? status.env_path : 'missing'}`);
|
|
336
|
+
if (status.base_url) lines.push(`Base URL: ${status.base_url}`);
|
|
337
|
+
lines.push(`ChatGPT backup: ${backupPresent ? `yes (${backupPath})` : 'no'}`);
|
|
338
|
+
if (status.ok && !status.auth_usable_for_codex_app && backupPresent) lines.push('', 'Run: sks codex-lb repair to restore the ChatGPT OAuth backup while keeping codex-lb selected.');
|
|
339
|
+
else if (status.ok && !status.auth_usable_for_codex_app) lines.push('', 'Sign in to Codex App/CLI again, then run: sks codex-lb repair');
|
|
340
|
+
else if (status.ok && !status.selected) lines.push('', 'Run: sks codex-lb repair to activate codex-lb for Codex App.');
|
|
341
|
+
else if (!status.ok && status.base_url && status.env_key_configured) lines.push('', 'Run: sks codex-lb repair to restore the upstream codex-lb provider block.');
|
|
342
|
+
else if (!status.ok) lines.push('', 'Run: sks codex-lb setup --host <domain> --api-key <key>');
|
|
343
|
+
else lines.push('', 'Repair provider auth: sks codex-lb repair');
|
|
344
|
+
if (backupPresent) lines.push('Switch fully away from codex-lb: sks codex-lb release');
|
|
345
|
+
return `${lines.join('\n')}\n`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function formatCodexLbRepairResultText(result = {}) {
|
|
349
|
+
const lines = [
|
|
350
|
+
'codex-lb provider auth repaired for Codex CLI/App environment.',
|
|
351
|
+
`Config: ${result.config_path}`,
|
|
352
|
+
`Key env: ${result.env_path}`
|
|
353
|
+
];
|
|
354
|
+
if (result.auth_reconcile?.status === 'oauth_restored') lines.push(`Codex App auth: ChatGPT OAuth restored from ${result.auth_reconcile.backup_path}.`);
|
|
355
|
+
else if (result.auth_reconcile?.status === 'oauth_preserved') lines.push('Codex App auth: ChatGPT OAuth preserved; codex-lb will use CODEX_LB_API_KEY from env_key.');
|
|
356
|
+
else if (result.auth_reconcile?.status === 'apikey_auth_active') lines.push('Codex App auth: API-key auth.json is still active. Sign in again if the App asks for ChatGPT OAuth.');
|
|
357
|
+
return `${lines.join('\n')}\n`;
|
|
358
|
+
}
|
|
359
|
+
|
|
306
360
|
function codexLbResponsesEndpoint(baseUrl = '') {
|
|
307
361
|
const base = String(baseUrl || '').trim().replace(/\/+$/, '');
|
|
308
362
|
if (!base) return '';
|
|
@@ -313,6 +367,77 @@ function codexLbChainCheckEnabled(env = process.env) {
|
|
|
313
367
|
return env.SKS_CODEX_LB_CHAIN_CHECK !== '0' && env.SKS_SKIP_CODEX_LB_CHAIN_CHECK !== '1';
|
|
314
368
|
}
|
|
315
369
|
|
|
370
|
+
function codexLbChainCachePath(home = process.env.HOME || os.homedir()) {
|
|
371
|
+
return path.join(home, '.codex', 'sks-codex-lb-chain-health.json');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function codexLbChainCacheTtlMs(status = '', env = process.env) {
|
|
375
|
+
const hardFailure = Boolean(status && !['chain_ok', 'previous_response_not_found'].includes(status));
|
|
376
|
+
const key = hardFailure ? 'SKS_CODEX_LB_CHAIN_CHECK_FAILURE_CACHE_TTL_MS' : 'SKS_CODEX_LB_CHAIN_CHECK_CACHE_TTL_MS';
|
|
377
|
+
const fallback = hardFailure ? 30 * 1000 : 5 * 60 * 1000;
|
|
378
|
+
const raw = env[key] ?? env.SKS_CODEX_LB_CHAIN_CHECK_CACHE_TTL_MS;
|
|
379
|
+
if (raw === undefined || raw === '') return fallback;
|
|
380
|
+
const parsed = Number(raw);
|
|
381
|
+
return Number.isFinite(parsed) ? Math.max(0, parsed) : fallback;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function codexLbChainCacheEnabled(opts = {}, env = process.env) {
|
|
385
|
+
if (opts.force || opts.cache === false) return false;
|
|
386
|
+
if (opts.fetch) return false;
|
|
387
|
+
if (env.SKS_CODEX_LB_CHAIN_CHECK_CACHE === '0') return false;
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function readCodexLbChainCache({ endpoint, home, opts = {}, env = process.env } = {}) {
|
|
392
|
+
if (!endpoint || !codexLbChainCacheEnabled(opts, env)) return null;
|
|
393
|
+
const cachePath = opts.cachePath || codexLbChainCachePath(home || env.HOME || os.homedir());
|
|
394
|
+
const text = await readText(cachePath, '');
|
|
395
|
+
if (!text.trim()) return null;
|
|
396
|
+
try {
|
|
397
|
+
const parsed = JSON.parse(text);
|
|
398
|
+
if (parsed?.schema !== 'sks.codex-lb-chain-health.v1' || parsed.endpoint !== endpoint || !parsed.result?.status) return null;
|
|
399
|
+
const now = typeof opts.now === 'function' ? opts.now() : Date.now();
|
|
400
|
+
const checkedAt = Number(parsed.checked_at_ms || 0);
|
|
401
|
+
const ttlMs = codexLbChainCacheTtlMs(parsed.result.status, env);
|
|
402
|
+
if (!checkedAt || ttlMs <= 0 || now - checkedAt > ttlMs) return null;
|
|
403
|
+
return {
|
|
404
|
+
...parsed.result,
|
|
405
|
+
endpoint,
|
|
406
|
+
cached: true,
|
|
407
|
+
cache_path: cachePath,
|
|
408
|
+
cache_age_ms: Math.max(0, now - checkedAt)
|
|
409
|
+
};
|
|
410
|
+
} catch {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async function writeCodexLbChainCache(result = {}, { endpoint, home, opts = {}, env = process.env } = {}) {
|
|
416
|
+
if (!endpoint || !result.status || !codexLbChainCacheEnabled(opts, env)) return result;
|
|
417
|
+
const cachePath = opts.cachePath || codexLbChainCachePath(home || env.HOME || os.homedir());
|
|
418
|
+
const now = typeof opts.now === 'function' ? opts.now() : Date.now();
|
|
419
|
+
const cacheResult = {
|
|
420
|
+
ok: Boolean(result.ok),
|
|
421
|
+
status: result.status,
|
|
422
|
+
chain_unhealthy: result.chain_unhealthy === true,
|
|
423
|
+
http_status: result.http_status || null,
|
|
424
|
+
error: result.error || null
|
|
425
|
+
};
|
|
426
|
+
try {
|
|
427
|
+
await ensureDir(path.dirname(cachePath));
|
|
428
|
+
await writeTextAtomic(cachePath, `${JSON.stringify({
|
|
429
|
+
schema: 'sks.codex-lb-chain-health.v1',
|
|
430
|
+
endpoint,
|
|
431
|
+
checked_at_ms: now,
|
|
432
|
+
result: cacheResult
|
|
433
|
+
}, null, 2)}\n`);
|
|
434
|
+
await fsp.chmod(cachePath, 0o600).catch(() => {});
|
|
435
|
+
} catch {
|
|
436
|
+
// Cache writes are a launch optimization only; never block codex-lb startup.
|
|
437
|
+
}
|
|
438
|
+
return result;
|
|
439
|
+
}
|
|
440
|
+
|
|
316
441
|
function isPreviousResponseNotFound(payload = {}) {
|
|
317
442
|
const error = payload?.error || payload?.response?.error || payload;
|
|
318
443
|
const text = typeof error === 'string'
|
|
@@ -382,8 +507,11 @@ export async function checkCodexLbResponseChain(status = {}, opts = {}) {
|
|
|
382
507
|
if (!codexLbChainCheckEnabled(env) && !opts.force) return { ok: true, status: 'skipped', skipped: true, reason: 'SKS_CODEX_LB_CHAIN_CHECK=0' };
|
|
383
508
|
const endpoint = codexLbResponsesEndpoint(opts.baseUrl || status.base_url);
|
|
384
509
|
if (!endpoint) return { ok: false, status: 'missing_base_url', chain_unhealthy: true };
|
|
385
|
-
const
|
|
510
|
+
const home = opts.home || env.HOME || os.homedir();
|
|
511
|
+
const apiKey = opts.apiKey || parseCodexLbEnvKey(await readText(opts.envPath || status.env_path || codexLbEnvPath(home), ''));
|
|
386
512
|
if (!apiKey) return { ok: false, status: 'missing_env_key', chain_unhealthy: true };
|
|
513
|
+
const cached = await readCodexLbChainCache({ endpoint, home, opts, env });
|
|
514
|
+
if (cached) return cached;
|
|
387
515
|
const fetchImpl = opts.fetch || globalThis.fetch;
|
|
388
516
|
if (typeof fetchImpl !== 'function') return { ok: true, status: 'skipped', skipped: true, reason: 'fetch unavailable' };
|
|
389
517
|
const model = opts.model || env.SKS_CODEX_MODEL || 'gpt-5.5';
|
|
@@ -400,19 +528,19 @@ export async function checkCodexLbResponseChain(status = {}, opts = {}) {
|
|
|
400
528
|
};
|
|
401
529
|
const first = await fetchCodexLbResponse(fetchImpl, endpoint, apiKey, baseBody, timeoutMs);
|
|
402
530
|
if (!first.ok || !first.response_id) {
|
|
403
|
-
return {
|
|
531
|
+
return writeCodexLbChainCache({
|
|
404
532
|
ok: false,
|
|
405
533
|
status: first.ok ? 'missing_response_id' : 'first_request_failed',
|
|
406
534
|
chain_unhealthy: true,
|
|
407
535
|
endpoint,
|
|
408
536
|
http_status: first.status,
|
|
409
537
|
error: redactSecretText(first.error_payload?.error?.message || first.error_payload?.response?.error?.message || first.text || 'codex-lb first Responses request failed', [apiKey])
|
|
410
|
-
};
|
|
538
|
+
}, { endpoint, home, opts, env });
|
|
411
539
|
}
|
|
412
540
|
const second = await fetchCodexLbResponse(fetchImpl, endpoint, apiKey, { ...baseBody, previous_response_id: first.response_id }, timeoutMs);
|
|
413
|
-
if (second.ok) return { ok: true, status: 'chain_ok', endpoint, response_id: first.response_id, chained_response_id: second.response_id || null, http_status: second.status };
|
|
541
|
+
if (second.ok) return writeCodexLbChainCache({ ok: true, status: 'chain_ok', endpoint, response_id: first.response_id, chained_response_id: second.response_id || null, http_status: second.status }, { endpoint, home, opts, env });
|
|
414
542
|
const previousMissing = isPreviousResponseNotFound(second.error_payload || second.json || second.text);
|
|
415
|
-
return {
|
|
543
|
+
return writeCodexLbChainCache({
|
|
416
544
|
ok: false,
|
|
417
545
|
status: previousMissing ? 'previous_response_not_found' : 'second_request_failed',
|
|
418
546
|
chain_unhealthy: true,
|
|
@@ -420,7 +548,7 @@ export async function checkCodexLbResponseChain(status = {}, opts = {}) {
|
|
|
420
548
|
response_id: first.response_id,
|
|
421
549
|
http_status: second.status,
|
|
422
550
|
error: redactSecretText(second.error_payload?.error?.message || second.error_payload?.response?.error?.message || second.text || 'codex-lb chained Responses request failed', [apiKey])
|
|
423
|
-
};
|
|
551
|
+
}, { endpoint, home, opts, env });
|
|
424
552
|
}
|
|
425
553
|
|
|
426
554
|
function hasTopLevelCodexLbSelected(text = '') {
|
|
@@ -473,6 +601,7 @@ export async function repairCodexLbAuth(opts = {}) {
|
|
|
473
601
|
const apiKey = parseCodexLbEnvKey(await readText(status.env_path, ''));
|
|
474
602
|
const codexLogin = await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home: opts.home || process.env.HOME || os.homedir(), force: true });
|
|
475
603
|
const authReconcile = await reconcileCodexLbAuthConflict({ ...opts, status }).catch((err) => ({ status: 'failed', reason: 'exception', error: err.message }));
|
|
604
|
+
const finalStatus = await codexLbStatus(opts);
|
|
476
605
|
const ok = Boolean(codexEnvironment.ok && codexLogin.ok);
|
|
477
606
|
return {
|
|
478
607
|
ok,
|
|
@@ -484,7 +613,7 @@ export async function repairCodexLbAuth(opts = {}) {
|
|
|
484
613
|
legacy_auth_migrated: legacyAuthMigrated,
|
|
485
614
|
legacy_auth_path: legacyAuthPath,
|
|
486
615
|
auth_reconcile: authReconcile,
|
|
487
|
-
codex_lb:
|
|
616
|
+
codex_lb: finalStatus,
|
|
488
617
|
codex_environment: codexEnvironment,
|
|
489
618
|
codex_login: codexLogin
|
|
490
619
|
};
|
|
@@ -504,6 +633,7 @@ export async function ensureCodexLbAuthDuringInstall(opts = {}) {
|
|
|
504
633
|
const apiKey = parseCodexLbEnvKey(await readText(status.env_path, ''));
|
|
505
634
|
const codexLogin = await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home: opts.home || process.env.HOME || os.homedir(), force: true });
|
|
506
635
|
const authReconcile = await reconcileCodexLbAuthConflict({ ...opts, status }).catch((err) => ({ status: 'failed', reason: 'exception', error: err.message }));
|
|
636
|
+
const finalStatus = await codexLbStatus(opts);
|
|
507
637
|
const ok = Boolean(codexEnvironment.ok && codexLogin.ok);
|
|
508
638
|
return {
|
|
509
639
|
ok,
|
|
@@ -511,7 +641,7 @@ export async function ensureCodexLbAuthDuringInstall(opts = {}) {
|
|
|
511
641
|
config_path: status.config_path,
|
|
512
642
|
env_path: status.env_path,
|
|
513
643
|
base_url: status.base_url,
|
|
514
|
-
codex_lb:
|
|
644
|
+
codex_lb: finalStatus,
|
|
515
645
|
codex_environment: codexEnvironment,
|
|
516
646
|
codex_login: codexLogin,
|
|
517
647
|
auth_reconcile: authReconcile,
|
|
@@ -565,6 +695,19 @@ function parseCodexAuthApiKey(text = '') {
|
|
|
565
695
|
}
|
|
566
696
|
}
|
|
567
697
|
|
|
698
|
+
function codexAuthModeSummary(text = '') {
|
|
699
|
+
const raw = String(text || '').trim();
|
|
700
|
+
if (!raw) return { mode: 'missing', codex_app_usable: false, summary: 'missing auth.json' };
|
|
701
|
+
if (hasChatgptOAuthTokens(raw)) return { mode: 'chatgpt_oauth', codex_app_usable: true, summary: 'ChatGPT OAuth token blob present' };
|
|
702
|
+
const apiKey = parseCodexAuthApiKey(raw);
|
|
703
|
+
if (apiKey) return { mode: 'apikey', codex_app_usable: false, summary: 'API-key auth.json; Codex App may require ChatGPT sign-in for requires_openai_auth providers' };
|
|
704
|
+
try {
|
|
705
|
+
const parsed = JSON.parse(raw);
|
|
706
|
+
if (parsed?.auth_mode === 'browser') return { mode: 'browser_marker', codex_app_usable: false, summary: 'browser auth marker without refresh tokens' };
|
|
707
|
+
} catch {}
|
|
708
|
+
return { mode: 'unknown', codex_app_usable: false, summary: 'unrecognized auth.json shape' };
|
|
709
|
+
}
|
|
710
|
+
|
|
568
711
|
// Migrate auth.json from legacy {"auth_mode":"apikey","key":"..."} to the codex 0.130.0+
|
|
569
712
|
// format {"auth_mode":"apikey","OPENAI_API_KEY":"..."}. Safe: preserves key value, only renames field.
|
|
570
713
|
async function migrateCodexAuthKeyFormat(opts = {}) {
|
|
@@ -587,11 +730,10 @@ async function migrateCodexAuthKeyFormat(opts = {}) {
|
|
|
587
730
|
}
|
|
588
731
|
}
|
|
589
732
|
|
|
590
|
-
//
|
|
591
|
-
//
|
|
592
|
-
//
|
|
593
|
-
// auth.json
|
|
594
|
-
// Opt out with SKS_CODEX_LB_NO_AUTH_RECONCILE=1 (the backup is still produced so nothing is lost).
|
|
733
|
+
// Codex App needs a refreshable ChatGPT OAuth blob when a provider declares
|
|
734
|
+
// requires_openai_auth=true. For codex-lb, the proxy key belongs in env_key
|
|
735
|
+
// (CODEX_LB_API_KEY), so SKS preserves or restores OAuth by default and only
|
|
736
|
+
// writes apikey auth.json when explicitly requested for CLI-only legacy use.
|
|
595
737
|
export async function reconcileCodexLbAuthConflict(opts = {}) {
|
|
596
738
|
const home = opts.home || process.env.HOME || os.homedir();
|
|
597
739
|
const status = opts.status || await codexLbStatus({ ...opts, home });
|
|
@@ -607,42 +749,77 @@ export async function reconcileCodexLbAuthConflict(opts = {}) {
|
|
|
607
749
|
if (!authText.trim()) {
|
|
608
750
|
return { status: 'skipped', reason: 'auth_empty', auth_path: authPath };
|
|
609
751
|
}
|
|
610
|
-
if (!hasChatgptOAuthTokens(authText)) {
|
|
611
|
-
return { status: 'no_oauth_conflict', auth_path: authPath };
|
|
612
|
-
}
|
|
613
752
|
const envText = await readText(status.env_path, '');
|
|
614
753
|
const apiKey = parseCodexLbEnvKey(envText);
|
|
615
754
|
if (!apiKey) {
|
|
616
755
|
return { status: 'skipped', reason: 'missing_env_key', auth_path: authPath };
|
|
617
756
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
757
|
+
if (hasChatgptOAuthTokens(authText)) {
|
|
758
|
+
try {
|
|
759
|
+
await ensureDir(path.dirname(backupPath));
|
|
760
|
+
await writeTextAtomic(backupPath, authText);
|
|
761
|
+
await fsp.chmod(backupPath, 0o600).catch(() => {});
|
|
762
|
+
} catch (err) {
|
|
763
|
+
return { status: 'failed', reason: 'backup_failed', auth_path: authPath, backup_path: backupPath, error: err.message };
|
|
764
|
+
}
|
|
765
|
+
if (process.env.SKS_CODEX_LB_NO_AUTH_RECONCILE === '1' && !opts.force) {
|
|
766
|
+
return {
|
|
767
|
+
status: 'backup_only',
|
|
768
|
+
reason: 'SKS_CODEX_LB_NO_AUTH_RECONCILE=1',
|
|
769
|
+
auth_path: authPath,
|
|
770
|
+
backup_path: backupPath
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
if (process.env.SKS_CODEX_LB_FORCE_APIKEY_AUTH !== '1') {
|
|
774
|
+
return {
|
|
775
|
+
status: 'oauth_preserved',
|
|
776
|
+
reason: 'codex_app_requires_refreshable_oauth',
|
|
777
|
+
auth_path: authPath,
|
|
778
|
+
backup_path: backupPath
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
try {
|
|
782
|
+
const replacement = `${JSON.stringify({ auth_mode: 'apikey', OPENAI_API_KEY: apiKey }, null, 2)}\n`;
|
|
783
|
+
await writeTextAtomic(authPath, replacement);
|
|
784
|
+
await fsp.chmod(authPath, 0o600).catch(() => {});
|
|
785
|
+
} catch (err) {
|
|
786
|
+
return { status: 'failed', reason: 'write_failed', auth_path: authPath, backup_path: backupPath, error: err.message };
|
|
787
|
+
}
|
|
627
788
|
return {
|
|
628
|
-
status: '
|
|
629
|
-
reason: '
|
|
789
|
+
status: 'apikey_forced',
|
|
790
|
+
reason: 'SKS_CODEX_LB_FORCE_APIKEY_AUTH=1',
|
|
630
791
|
auth_path: authPath,
|
|
631
792
|
backup_path: backupPath
|
|
632
793
|
};
|
|
633
794
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
await
|
|
638
|
-
|
|
639
|
-
|
|
795
|
+
|
|
796
|
+
const currentApiKey = parseCodexAuthApiKey(authText);
|
|
797
|
+
if (currentApiKey && currentApiKey === apiKey) {
|
|
798
|
+
const backupText = await readText(backupPath, '');
|
|
799
|
+
if (hasChatgptOAuthTokens(backupText) && process.env.SKS_CODEX_LB_KEEP_APIKEY_AUTH !== '1') {
|
|
800
|
+
try {
|
|
801
|
+
const restored = backupText.endsWith('\n') ? backupText : `${backupText}\n`;
|
|
802
|
+
await writeTextAtomic(authPath, restored);
|
|
803
|
+
await fsp.chmod(authPath, 0o600).catch(() => {});
|
|
804
|
+
return {
|
|
805
|
+
status: 'oauth_restored',
|
|
806
|
+
reason: 'restored_chatgpt_oauth_for_codex_app',
|
|
807
|
+
auth_path: authPath,
|
|
808
|
+
backup_path: backupPath
|
|
809
|
+
};
|
|
810
|
+
} catch (err) {
|
|
811
|
+
return { status: 'failed', reason: 'restore_failed', auth_path: authPath, backup_path: backupPath, error: err.message };
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return {
|
|
815
|
+
status: 'apikey_auth_active',
|
|
816
|
+
reason: hasChatgptOAuthTokens(backupText) ? 'SKS_CODEX_LB_KEEP_APIKEY_AUTH=1' : 'chatgpt_oauth_backup_missing',
|
|
817
|
+
auth_path: authPath,
|
|
818
|
+
backup_path: backupPath
|
|
819
|
+
};
|
|
640
820
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
auth_path: authPath,
|
|
644
|
-
backup_path: backupPath
|
|
645
|
-
};
|
|
821
|
+
|
|
822
|
+
return { status: 'no_oauth_conflict', auth_path: authPath };
|
|
646
823
|
}
|
|
647
824
|
|
|
648
825
|
// Expose the ChatGPT OAuth backup path so the CLI can surface it in status / release output.
|
|
@@ -1730,7 +1907,7 @@ export async function selftestCodexLb(tmp) {
|
|
|
1730
1907
|
const codexLbReconcileJson = JSON.parse(codexLbReconcileRepair.stdout);
|
|
1731
1908
|
const codexLbReconcileAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
|
|
1732
1909
|
const codexLbReconcileBackup = await safeReadText(path.join(codexLbHome, '.codex', 'auth.chatgpt-backup.json'));
|
|
1733
|
-
if (codexLbReconcileJson.auth_reconcile?.status !== '
|
|
1910
|
+
if (codexLbReconcileJson.auth_reconcile?.status !== 'oauth_preserved' || !codexLbReconcileAuth.includes('oauth-id') || !codexLbReconcileAuth.includes('oauth-refresh') || codexLbReconcileAuth.includes('sk-test') || !codexLbReconcileBackup.includes('oauth-id') || !codexLbReconcileBackup.includes('oauth-refresh')) throw new Error('selftest: codex-lb oauth reconcile should preserve ChatGPT OAuth and back it up');
|
|
1734
1911
|
// Opt-out path: SKS_CODEX_LB_NO_AUTH_RECONCILE=1 keeps auth.json untouched but still backs up the OAuth blob.
|
|
1735
1912
|
await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), `${oauthAuthJson}\n`);
|
|
1736
1913
|
await fsp.rm(path.join(codexLbHome, '.codex', 'auth.chatgpt-backup.json'), { force: true });
|
|
@@ -1740,6 +1917,16 @@ export async function selftestCodexLb(tmp) {
|
|
|
1740
1917
|
const codexLbReconcileOptOutAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
|
|
1741
1918
|
const codexLbReconcileOptOutBackup = await safeReadText(path.join(codexLbHome, '.codex', 'auth.chatgpt-backup.json'));
|
|
1742
1919
|
if (codexLbReconcileOptOutJson.auth_reconcile?.status !== 'backup_only' || !codexLbReconcileOptOutAuth.includes('oauth-id') || !codexLbReconcileOptOutBackup.includes('oauth-id')) throw new Error('selftest: codex-lb oauth reconcile opt-out should back up but not rewrite auth.json');
|
|
1920
|
+
// Restore path: older SKS versions could leave the codex-lb API key in auth.json. Repair should
|
|
1921
|
+
// restore the ChatGPT OAuth backup while keeping codex-lb selected for provider routing.
|
|
1922
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"apikey","OPENAI_API_KEY":"sk-test"}\n');
|
|
1923
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.chatgpt-backup.json'), `${oauthAuthJson}\n`);
|
|
1924
|
+
const codexLbReconcileRestoreRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'auth', 'repair', '--json'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
1925
|
+
if (codexLbReconcileRestoreRepair.code !== 0) throw new Error(`selftest: codex-lb oauth restore repair exited ${codexLbReconcileRestoreRepair.code}: ${codexLbReconcileRestoreRepair.stderr}`);
|
|
1926
|
+
const codexLbReconcileRestoreJson = JSON.parse(codexLbReconcileRestoreRepair.stdout);
|
|
1927
|
+
const codexLbReconcileRestoreAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
|
|
1928
|
+
const codexLbReconcileRestoreConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
|
|
1929
|
+
if (codexLbReconcileRestoreJson.auth_reconcile?.status !== 'oauth_restored' || !codexLbReconcileRestoreAuth.includes('oauth-id') || codexLbReconcileRestoreAuth.includes('sk-test') || !hasTopLevelCodexLbSelected(codexLbReconcileRestoreConfig)) throw new Error('selftest: codex-lb oauth restore should replace apikey auth.json with ChatGPT OAuth backup while keeping codex-lb selected');
|
|
1743
1930
|
// codex-lb auth: release flow — restore ChatGPT OAuth from backup so the user can return to
|
|
1744
1931
|
// the official ChatGPT account login. Default deselects model_provider; flags control whether
|
|
1745
1932
|
// the provider stays selected and whether the backup file is removed after restore.
|
|
@@ -1932,7 +2119,7 @@ export async function selftestCodexLb(tmp) {
|
|
|
1932
2119
|
});
|
|
1933
2120
|
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');
|
|
1934
2121
|
const codexLbStatusText = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'status'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
1935
|
-
if (!String(codexLbStatusText.stdout || '').includes('
|
|
2122
|
+
if (!String(codexLbStatusText.stdout || '').includes('Codex App auth:') || !String(codexLbStatusText.stdout || '').includes('sks codex-lb repair')) throw new Error('selftest: codex-lb status did not advertise App auth state and repair command');
|
|
1936
2123
|
const nonInteractiveLaunchChainCalls = [];
|
|
1937
2124
|
const nonInteractiveLaunch = await maybePromptCodexLbSetupForLaunch([], {
|
|
1938
2125
|
home: codexLbHome,
|
|
@@ -2023,6 +2210,21 @@ export async function selftestCodexLb(tmp) {
|
|
|
2023
2210
|
}
|
|
2024
2211
|
);
|
|
2025
2212
|
if (!okChain.ok || okChain.status !== 'chain_ok' || chainCalls.length !== 2 || !String(chainCalls[0].url).endsWith('/backend-api/codex/responses') || chainCalls[1].body.previous_response_id !== 'resp_selftest_1') throw new Error('selftest: codex-lb response chain health check did not verify previous_response_id continuity');
|
|
2213
|
+
const previousGlobalFetch = globalThis.fetch;
|
|
2214
|
+
const cacheCalls = [];
|
|
2215
|
+
const cachePath = path.join(codexLbHome, '.codex', 'chain-cache-selftest.json');
|
|
2216
|
+
try {
|
|
2217
|
+
globalThis.fetch = async (url, init) => {
|
|
2218
|
+
cacheCalls.push({ url, body: JSON.parse(init.body) });
|
|
2219
|
+
return new Response(JSON.stringify({ id: cacheCalls.length === 1 ? 'resp_cache_1' : 'resp_cache_2' }), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
2220
|
+
};
|
|
2221
|
+
const cacheStatus = { base_url: 'https://cache.example.test/backend-api/codex', env_path: path.join(codexLbHome, '.codex', 'sks-codex-lb.env') };
|
|
2222
|
+
const firstCache = await checkCodexLbResponseChain(cacheStatus, { home: codexLbHome, apiKey: 'sk-test', timeoutMs: 1000, cachePath, now: () => 1000 });
|
|
2223
|
+
const secondCache = await checkCodexLbResponseChain(cacheStatus, { home: codexLbHome, apiKey: 'sk-test', timeoutMs: 1000, cachePath, now: () => 2000 });
|
|
2224
|
+
if (!firstCache.ok || firstCache.status !== 'chain_ok' || secondCache.cached !== true || secondCache.status !== 'chain_ok' || cacheCalls.length !== 2) throw new Error('selftest: codex-lb response chain cache did not avoid repeated launch preflight calls');
|
|
2225
|
+
} finally {
|
|
2226
|
+
globalThis.fetch = previousGlobalFetch;
|
|
2227
|
+
}
|
|
2026
2228
|
const brokenChain = await checkCodexLbResponseChain(
|
|
2027
2229
|
{ base_url: 'https://lb.example.test/backend-api/codex', env_path: path.join(codexLbHome, '.codex', 'sks-codex-lb.env') },
|
|
2028
2230
|
{
|