sneakoscope 1.0.5 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -1
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/crates/sks-core/src/main.rs +1 -1
- package/dist/bin/sks.js +1 -1
- package/dist/build-manifest.json +5 -1
- package/dist/cli/install-helpers.d.ts +3 -0
- package/dist/cli/install-helpers.js +88 -11
- package/dist/commands/codex-lb.js +89 -6
- package/dist/commands/wiki.d.ts +1 -1
- package/dist/core/codex-compat/codex-compat-report.d.ts +7 -0
- package/dist/core/codex-compat/codex-compat-report.js +1 -0
- package/dist/core/codex-compat/codex-hook-issues.d.ts +20 -0
- package/dist/core/codex-compat/codex-hook-issues.js +93 -0
- package/dist/core/codex-compat/codex-hook-schema.d.ts +3 -0
- package/dist/core/codex-compat/codex-hook-schema.js +7 -4
- package/dist/core/codex-compat/codex-hook-semantic-validator.d.ts +5 -1
- package/dist/core/codex-compat/codex-hook-semantic-validator.js +84 -71
- package/dist/core/codex-compat/codex-hook-warning-detector.d.ts +6 -0
- package/dist/core/codex-compat/codex-hook-warning-detector.js +46 -27
- package/dist/core/codex-lb/codex-lb-setup.d.ts +46 -0
- package/dist/core/codex-lb/codex-lb-setup.js +112 -0
- package/dist/core/commands/computer-use-command.js +22 -1
- package/dist/core/commands/wiki-command.d.ts +2 -2
- package/dist/core/computer-use-status.d.ts +24 -0
- package/dist/core/computer-use-status.js +46 -0
- package/dist/core/fsx.d.ts +1 -1
- package/dist/core/fsx.js +1 -1
- package/dist/core/proof/evidence-collector.d.ts +1 -1
- package/dist/core/proof/route-finalizer.js +3 -1
- package/dist/core/triwiki-wrongness/wrongness-cli.d.ts +2 -2
- package/dist/core/triwiki-wrongness/wrongness-ledger.js +3 -3
- package/dist/core/triwiki-wrongness/wrongness-proof-linker.d.ts +1 -1
- package/dist/core/triwiki-wrongness/wrongness-retrieval.d.ts +1 -1
- package/dist/core/triwiki-wrongness/wrongness-schema.d.ts +1 -1
- package/dist/core/triwiki-wrongness/wrongness-schema.js +17 -2
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -4,7 +4,9 @@ Fast legacy-free proof-first Codex trust layer with image-based Voxel TriWiki.
|
|
|
4
4
|
|
|
5
5
|
Sneakoscope Codex (`sks`) is a Codex CLI/App harness that makes repeatable Codex work auditable.
|
|
6
6
|
|
|
7
|
-
SKS **1.0.
|
|
7
|
+
SKS **1.0.6** is the final precision polish for the Codex trust harness: hook compatibility is classified as upstream schema plus an SKS zero-warning strict subset, `sks codex-lb setup` previews and applies the exact choices the user selected, and Computer Use has an optional live smoke surface for macOS capability/evidence status.
|
|
8
|
+
|
|
9
|
+
SKS **1.0.5** sealed the prior trust harness: hook outputs were validated against both vendored OpenAI Codex CLI `rust-v0.131.0` schemas and runtime semantic parser rules, codex-lb setup survived macOS user-session launches through env-file/Keychain/launchctl-aware repair surfaces, and Computer Use became the preferred macOS visual evidence capability when available.
|
|
8
10
|
|
|
9
11
|
SKS **1.0.4** introduced the `rust-v0.131.0` schema snapshot, guided codex-lb setup path, and Computer Use/MAD-SKS separation that 1.0.5 now hardens into release gates.
|
|
10
12
|
|
|
@@ -21,6 +23,26 @@ SKS does not try to clone every other harness. It focuses on one thing: making C
|
|
|
21
23
|

|
|
22
24
|
|
|
23
25
|
|
|
26
|
+
## 1.0.6 Final Precision Polish
|
|
27
|
+
|
|
28
|
+
SKS validates Codex hooks against the OpenAI Codex `rust-v0.131.0` schema and enforces a stricter SKS zero-warning subset. Some fields may be accepted by upstream but are intentionally disallowed by SKS to avoid user-facing hook warnings and release drift. `sks hooks warning-check --json` now reports `schema_violation`, `upstream_semantic_unsupported`, `sks_zero_warning_disallowed`, `legacy_shape`, and `policy_disallowed` category counts.
|
|
29
|
+
|
|
30
|
+
`sks codex-lb setup` is now a two-phase plan/apply wizard. Every question maps to an actual action: provider selection, env file writing, Keychain storage, launchctl sync, shell profile snippets, and health checks.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
sks codex-lb setup --host lb.example.com --api-key-stdin --plan --json
|
|
34
|
+
sks codex-lb setup --host lb.example.com --api-key-stdin --yes --no-default-provider --no-env-file --json
|
|
35
|
+
npm run codex-lb:setup-truthfulness
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Computer Use live validation is optional and opt-in. On macOS, `SKS_TEST_REAL_COMPUTER_USE=1 sks computer-use smoke --real --json` attempts a non-destructive capability/evidence check. If Codex App or macOS denies the capability, SKS records a structured blocker and does not fabricate visual evidence.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
sks computer-use smoke --json
|
|
42
|
+
SKS_TEST_REAL_COMPUTER_USE=1 sks computer-use smoke --real --json
|
|
43
|
+
npm run computer-use:live-optional
|
|
44
|
+
```
|
|
45
|
+
|
|
24
46
|
## 1.0.5 Ultimate Harness Seal
|
|
25
47
|
|
|
26
48
|
SKS 1.0.5 treats Codex hook semantic compatibility as stricter than schema compatibility. `sks hooks warning-check --json` and `npm run hooks:semantic-check` fail if an output uses `permissionDecision:"ask"`, PreToolUse `allow` without `updatedInput`, Stop `continue:false`, `stopReason`, `suppressOutput`, snake_case keys, unknown fields, or legacy top-level hook decisions.
|
|
@@ -4,7 +4,7 @@ use std::io::{self, Read, Seek, SeekFrom};
|
|
|
4
4
|
fn main() {
|
|
5
5
|
let mut args = std::env::args().skip(1);
|
|
6
6
|
match args.next().as_deref() {
|
|
7
|
-
Some("--version") => println!("sks-rs 1.0.
|
|
7
|
+
Some("--version") => println!("sks-rs 1.0.6"),
|
|
8
8
|
Some("compact-info") => {
|
|
9
9
|
let mut input = String::new();
|
|
10
10
|
let _ = io::stdin().read_to_string(&mut input);
|
package/dist/bin/sks.js
CHANGED
package/dist/build-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": "sks.dist-build.v2",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"typescript": true,
|
|
5
5
|
"mjs_runtime_files": 0,
|
|
6
6
|
"files": [
|
|
@@ -188,6 +188,8 @@
|
|
|
188
188
|
"core/codex-compat/codex-compat-report.js",
|
|
189
189
|
"core/codex-compat/codex-config-policy.d.ts",
|
|
190
190
|
"core/codex-compat/codex-config-policy.js",
|
|
191
|
+
"core/codex-compat/codex-hook-issues.d.ts",
|
|
192
|
+
"core/codex-compat/codex-hook-issues.js",
|
|
191
193
|
"core/codex-compat/codex-hook-output-builders.d.ts",
|
|
192
194
|
"core/codex-compat/codex-hook-output-builders.js",
|
|
193
195
|
"core/codex-compat/codex-hook-output-normalizer.d.ts",
|
|
@@ -208,6 +210,8 @@
|
|
|
208
210
|
"core/codex-lb-circuit.js",
|
|
209
211
|
"core/codex-lb/codex-lb-env.d.ts",
|
|
210
212
|
"core/codex-lb/codex-lb-env.js",
|
|
213
|
+
"core/codex-lb/codex-lb-setup.d.ts",
|
|
214
|
+
"core/codex-lb/codex-lb-setup.js",
|
|
211
215
|
"core/codex-model-guard.d.ts",
|
|
212
216
|
"core/codex-model-guard.js",
|
|
213
217
|
"core/commands/autoresearch-command.d.ts",
|
|
@@ -56,6 +56,9 @@ export type CodexLbAuthInstallResult = {
|
|
|
56
56
|
export type ConfigureCodexLbResult = {
|
|
57
57
|
ok?: boolean;
|
|
58
58
|
status: string;
|
|
59
|
+
plan?: Record<string, unknown>;
|
|
60
|
+
applied_actions?: Array<Record<string, unknown>>;
|
|
61
|
+
drift?: string[];
|
|
59
62
|
config_path?: string;
|
|
60
63
|
env_path?: string;
|
|
61
64
|
metadata_path?: string;
|
|
@@ -12,6 +12,7 @@ import { codexLaunchCommand, platformTmuxInstallHint, tmuxReadiness, tmuxReadine
|
|
|
12
12
|
import { reconcileCodexAppUpgradeProcesses } from '../core/codex-app.js';
|
|
13
13
|
import { recordCodexLbHealthEvent } from '../core/codex-lb-circuit.js';
|
|
14
14
|
import { loadCodexLbEnv, writeCodexLbKeychain, codexLbMetadataPath } from '../core/codex-lb/codex-lb-env.js';
|
|
15
|
+
import { buildCodexLbSetupPlan, installCodexLbShellProfileSnippet } from '../core/codex-lb/codex-lb-setup.js';
|
|
15
16
|
const DEFAULT_CODEX_APP_PLUGINS = [
|
|
16
17
|
['browser', 'openai-bundled'],
|
|
17
18
|
['chrome', 'openai-bundled'],
|
|
@@ -307,8 +308,32 @@ export async function configureCodexLb(opts = {}) {
|
|
|
307
308
|
const rawHost = String(opts.host || opts.baseUrl || '');
|
|
308
309
|
const baseUrl = normalizeCodexLbBaseUrl(rawHost);
|
|
309
310
|
const apiKey = String(opts.apiKey || '').trim();
|
|
311
|
+
const useDefaultProvider = opts.useDefaultProvider !== false;
|
|
312
|
+
const writeEnvFile = opts.writeEnvFile !== false;
|
|
313
|
+
const storeKeychain = opts.storeKeychain === true || opts.keychain === true;
|
|
314
|
+
const syncLaunchctl = opts.syncLaunchctl !== false && opts.syncLaunchEnv !== false;
|
|
315
|
+
const shellProfile = opts.shellProfile || 'skip';
|
|
316
|
+
const setupAnswers = {
|
|
317
|
+
host_or_base_url: rawHost,
|
|
318
|
+
api_key_source: opts.apiKeySource || 'cli_option',
|
|
319
|
+
use_as_default_provider: useDefaultProvider,
|
|
320
|
+
write_env_file: writeEnvFile,
|
|
321
|
+
store_keychain: storeKeychain,
|
|
322
|
+
sync_launchctl: syncLaunchctl,
|
|
323
|
+
install_shell_profile: shellProfile,
|
|
324
|
+
run_health_check: opts.runHealth === true,
|
|
325
|
+
allow_insecure_localhost: opts.allowInsecureHttp === true || opts.allowInsecureLocalhost === true
|
|
326
|
+
};
|
|
327
|
+
const plan = buildCodexLbSetupPlan(setupAnswers, {
|
|
328
|
+
home,
|
|
329
|
+
configPath,
|
|
330
|
+
envPath,
|
|
331
|
+
metadataPath: opts.metadataPath || codexLbMetadataPath(home)
|
|
332
|
+
});
|
|
310
333
|
if (!baseUrl)
|
|
311
334
|
return { ok: false, status: 'missing_host_or_base_url', config_path: configPath, env_path: envPath };
|
|
335
|
+
if (plan.blockers.length)
|
|
336
|
+
return { ok: false, status: 'plan_blocked', plan: plan, drift: plan.blockers, config_path: configPath, env_path: envPath };
|
|
312
337
|
if (/[\u0000-\u001f\u007f\s]/.test(rawHost.trim()))
|
|
313
338
|
return { ok: false, status: 'invalid_host_or_base_url', config_path: configPath, env_path: envPath, error: 'host_or_base_url_contains_whitespace_or_control_character' };
|
|
314
339
|
if (!apiKey)
|
|
@@ -316,12 +341,21 @@ export async function configureCodexLb(opts = {}) {
|
|
|
316
341
|
const insecureLocalWarning = /^http:\/\//i.test(baseUrl) && !/^http:\/\/(?:localhost|127\.0\.0\.1|\[::1\])(?::|\/|$)/i.test(baseUrl) && !opts.allowInsecureHttp
|
|
317
342
|
? ['codex-lb base URL uses http outside localhost; prefer https or pass an explicit allow flag in the calling surface.']
|
|
318
343
|
: [];
|
|
344
|
+
const appliedActions = [];
|
|
319
345
|
await ensureDir(path.dirname(configPath));
|
|
320
346
|
const current = await readText(configPath, '');
|
|
321
|
-
const next = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(current, baseUrl));
|
|
347
|
+
const next = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(current, baseUrl, useDefaultProvider));
|
|
322
348
|
await writeTextAtomic(configPath, next);
|
|
323
|
-
|
|
324
|
-
|
|
349
|
+
appliedActions.push({ type: 'write_config_provider', target: configPath, ok: true });
|
|
350
|
+
if (useDefaultProvider)
|
|
351
|
+
appliedActions.push({ type: 'select_default_provider', target: configPath, ok: true });
|
|
352
|
+
if (writeEnvFile) {
|
|
353
|
+
await writeTextAtomic(envPath, `export CODEX_LB_BASE_URL=${shellSingleQuote(baseUrl)}\nexport CODEX_LB_API_KEY=${shellSingleQuote(apiKey)}\n`);
|
|
354
|
+
await fsp.chmod(envPath, 0o600).catch(() => { });
|
|
355
|
+
appliedActions.push({ type: 'write_env_file', target: envPath, ok: true });
|
|
356
|
+
}
|
|
357
|
+
process.env.CODEX_LB_BASE_URL = baseUrl;
|
|
358
|
+
process.env.CODEX_LB_API_KEY = apiKey;
|
|
325
359
|
const keyFingerprint = await sha256Text(apiKey);
|
|
326
360
|
const metadataPath = opts.metadataPath || codexLbMetadataPath(home);
|
|
327
361
|
await writeTextAtomic(metadataPath, `${JSON.stringify({
|
|
@@ -332,16 +366,39 @@ export async function configureCodexLb(opts = {}) {
|
|
|
332
366
|
api_key: { redacted: true, sha256: keyFingerprint }
|
|
333
367
|
}, null, 2)}\n`);
|
|
334
368
|
await fsp.chmod(metadataPath, 0o600).catch(() => { });
|
|
335
|
-
|
|
336
|
-
const
|
|
369
|
+
appliedActions.push({ type: 'write_metadata', target: metadataPath, ok: true });
|
|
370
|
+
const keychain = storeKeychain ? await writeCodexLbKeychain(apiKey, opts).catch((err) => ({ ok: false, status: 'keychain_store_failed', error: err.message })) : { ok: false, status: 'skipped' };
|
|
371
|
+
if (storeKeychain)
|
|
372
|
+
appliedActions.push({ type: 'store_keychain', target: 'macOS Keychain service sks-codex-lb', ok: keychain.ok === true, status: keychain.status });
|
|
373
|
+
const codexEnvironment = await syncCodexLbProviderEnvironment({ env_path: envPath, base_url: baseUrl }, { ...opts, home, apiKey, baseUrl, syncLaunchEnv: syncLaunchctl });
|
|
374
|
+
if (syncLaunchctl)
|
|
375
|
+
appliedActions.push({ type: 'sync_launchctl', target: 'macOS launchctl user environment', ok: codexEnvironment.ok === true, status: codexEnvironment.status });
|
|
376
|
+
const shellProfileResult = await installCodexLbShellProfileSnippet({ home, envPath, shellProfile }).catch((err) => ({ ok: false, status: 'failed', files: [], error: err.message }));
|
|
377
|
+
if (shellProfile !== 'skip')
|
|
378
|
+
appliedActions.push({ type: 'install_shell_profile_snippet', target: shellProfileResult.files?.join(', ') || shellProfile, ok: shellProfileResult.ok === true, status: shellProfileResult.status });
|
|
337
379
|
const codexLogin = await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home, force: true });
|
|
338
380
|
const codexLb = await codexLbStatus({ ...opts, home, configPath, envPath });
|
|
339
381
|
const authReconcile = await reconcileCodexLbAuthConflict({ ...opts, home, status: codexLb }).catch((err) => ({ status: 'failed', reason: 'exception', error: err.message }));
|
|
340
382
|
const finalCodexLb = await codexLbStatus({ ...opts, home, configPath, envPath });
|
|
341
383
|
const ok = Boolean(codexEnvironment.ok && codexLogin.ok);
|
|
384
|
+
const drift = detectCodexLbSetupDrift({
|
|
385
|
+
useDefaultProvider,
|
|
386
|
+
writeEnvFile,
|
|
387
|
+
storeKeychain,
|
|
388
|
+
syncLaunchctl,
|
|
389
|
+
shellProfile,
|
|
390
|
+
selected: finalCodexLb.selected,
|
|
391
|
+
envFile: finalCodexLb.env_file,
|
|
392
|
+
keychain,
|
|
393
|
+
codexEnvironment,
|
|
394
|
+
shellProfileResult
|
|
395
|
+
});
|
|
342
396
|
return {
|
|
343
|
-
ok,
|
|
344
|
-
status: ok ? 'configured' : (codexEnvironment.status || codexLogin.status),
|
|
397
|
+
ok: ok && drift.length === 0,
|
|
398
|
+
status: ok && drift.length === 0 ? 'configured' : drift.length ? 'setup_choice_drift' : (codexEnvironment.status || codexLogin.status),
|
|
399
|
+
plan: plan,
|
|
400
|
+
applied_actions: appliedActions,
|
|
401
|
+
drift,
|
|
345
402
|
config_path: configPath,
|
|
346
403
|
env_path: envPath,
|
|
347
404
|
metadata_path: metadataPath,
|
|
@@ -1188,10 +1245,10 @@ async function syncCodexLbProviderEnvironment(status = {}, opts = {}) {
|
|
|
1188
1245
|
const home = opts.home || process.env.HOME || os.homedir();
|
|
1189
1246
|
const envPath = opts.envPath || status.env_path || codexLbEnvPath(home);
|
|
1190
1247
|
const envText = await readText(envPath, '');
|
|
1191
|
-
const apiKey = parseCodexLbEnvKey(envText);
|
|
1248
|
+
const apiKey = String(opts.apiKey || '').trim() || parseCodexLbEnvKey(envText);
|
|
1192
1249
|
if (!apiKey)
|
|
1193
1250
|
return { ok: false, status: 'missing_env_key' };
|
|
1194
|
-
const baseUrl = status.base_url || parseCodexLbEnvBaseUrl(envText);
|
|
1251
|
+
const baseUrl = status.base_url || opts.baseUrl || parseCodexLbEnvBaseUrl(envText);
|
|
1195
1252
|
process.env.CODEX_LB_API_KEY = apiKey;
|
|
1196
1253
|
if (baseUrl)
|
|
1197
1254
|
process.env.CODEX_LB_BASE_URL = baseUrl;
|
|
@@ -1258,8 +1315,10 @@ async function syncCodexApiKeyLogin(apiKey, opts = {}) {
|
|
|
1258
1315
|
return { ok: true, status: 'synced' };
|
|
1259
1316
|
return { ok: false, status: 'login_failed', error: redactSecretText(login.stderr || login.stdout || 'codex login failed', [apiKey]).trim() };
|
|
1260
1317
|
}
|
|
1261
|
-
function upsertCodexLbConfig(text = '', baseUrl) {
|
|
1262
|
-
let next =
|
|
1318
|
+
function upsertCodexLbConfig(text = '', baseUrl, selectDefault = true) {
|
|
1319
|
+
let next = selectDefault
|
|
1320
|
+
? upsertTopLevelTomlString(text, 'model_provider', 'codex-lb')
|
|
1321
|
+
: removeTopLevelTomlKeyIfValue(text, 'model_provider', 'codex-lb');
|
|
1263
1322
|
const block = [
|
|
1264
1323
|
'[model_providers.codex-lb]',
|
|
1265
1324
|
'name = "OpenAI"',
|
|
@@ -1272,6 +1331,24 @@ function upsertCodexLbConfig(text = '', baseUrl) {
|
|
|
1272
1331
|
next = upsertTomlTable(next, 'model_providers.codex-lb', block);
|
|
1273
1332
|
return `${next.trim()}\n`;
|
|
1274
1333
|
}
|
|
1334
|
+
function detectCodexLbSetupDrift(state = {}) {
|
|
1335
|
+
const drift = [];
|
|
1336
|
+
if (state.useDefaultProvider && state.selected !== true)
|
|
1337
|
+
drift.push('default_provider_not_selected');
|
|
1338
|
+
if (!state.useDefaultProvider && state.selected === true)
|
|
1339
|
+
drift.push('default_provider_selected_despite_no_default_provider');
|
|
1340
|
+
if (state.writeEnvFile && state.envFile !== true)
|
|
1341
|
+
drift.push('env_file_not_written');
|
|
1342
|
+
if (!state.writeEnvFile && state.envFile === true)
|
|
1343
|
+
drift.push('env_file_written_despite_no_env_file');
|
|
1344
|
+
if (!state.storeKeychain && state.keychain?.status && state.keychain.status !== 'skipped')
|
|
1345
|
+
drift.push('keychain_touched_despite_no_keychain');
|
|
1346
|
+
if (!state.syncLaunchctl && state.codexEnvironment?.launch_environment?.status === 'synced')
|
|
1347
|
+
drift.push('launchctl_synced_despite_no_launchctl');
|
|
1348
|
+
if (state.shellProfile === 'skip' && state.shellProfileResult?.status === 'installed')
|
|
1349
|
+
drift.push('shell_profile_written_despite_skip');
|
|
1350
|
+
return drift;
|
|
1351
|
+
}
|
|
1275
1352
|
export async function ensureGlobalCodexFastModeDuringInstall(opts = {}) {
|
|
1276
1353
|
if (process.env.SKS_SKIP_CODEX_FAST_MODE_REPAIR === '1')
|
|
1277
1354
|
return { status: 'skipped', reason: 'SKS_SKIP_CODEX_FAST_MODE_REPAIR=1' };
|
|
@@ -6,6 +6,7 @@ import { flag, readOption } from '../cli/args.js';
|
|
|
6
6
|
import { printJson } from '../cli/output.js';
|
|
7
7
|
import { codexLbMetrics, readCodexLbCircuit, recordCodexLbHealthEvent, resetCodexLbCircuit, codexLbProofEvidence } from '../core/codex-lb-circuit.js';
|
|
8
8
|
import { checkCodexLbResponseChain, codexLbStatus, configureCodexLb, formatCodexLbStatusText, releaseCodexLbAuthHold, repairCodexLbAuth, unselectCodexLbProvider } from '../cli/install-helpers.js';
|
|
9
|
+
import { buildCodexLbSetupPlan, renderCodexLbSetupPlan } from '../core/codex-lb/codex-lb-setup.js';
|
|
9
10
|
export async function run(command, args = []) {
|
|
10
11
|
const root = await projectRoot();
|
|
11
12
|
const action = args[0] || 'status';
|
|
@@ -77,6 +78,17 @@ export async function run(command, args = []) {
|
|
|
77
78
|
}
|
|
78
79
|
if (action === 'setup' || action === 'reconfigure') {
|
|
79
80
|
const options = await codexLbSetupOptions(args);
|
|
81
|
+
const plan = buildCodexLbSetupPlan({
|
|
82
|
+
host_or_base_url: options.host || '',
|
|
83
|
+
api_key_source: options.apiKeySource,
|
|
84
|
+
use_as_default_provider: options.useDefaultProvider,
|
|
85
|
+
write_env_file: options.writeEnvFile,
|
|
86
|
+
store_keychain: options.keychain,
|
|
87
|
+
sync_launchctl: options.syncLaunchctl,
|
|
88
|
+
install_shell_profile: options.shellProfile,
|
|
89
|
+
run_health_check: options.health,
|
|
90
|
+
allow_insecure_localhost: options.allowInsecureLocalhost
|
|
91
|
+
});
|
|
80
92
|
if (!options.host || !options.apiKey) {
|
|
81
93
|
const result = {
|
|
82
94
|
schema: 'sks.codex-lb-setup.v1',
|
|
@@ -98,8 +110,43 @@ export async function run(command, args = []) {
|
|
|
98
110
|
process.exitCode = 1;
|
|
99
111
|
return;
|
|
100
112
|
}
|
|
101
|
-
|
|
113
|
+
if (flag(args, '--plan')) {
|
|
114
|
+
const result = { schema: 'sks.codex-lb-setup-plan-result.v1', ok: plan.blockers.length === 0, plan, writes: false };
|
|
115
|
+
if (flag(args, '--json'))
|
|
116
|
+
return printJson(result);
|
|
117
|
+
process.stdout.write(renderCodexLbSetupPlan(plan));
|
|
118
|
+
if (!result.ok)
|
|
119
|
+
process.exitCode = 1;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (options.interactive && !options.yes) {
|
|
123
|
+
process.stdout.write(renderCodexLbSetupPlan(plan));
|
|
124
|
+
const confirm = (await ask('Apply this codex-lb setup plan? [y/N] ')).trim();
|
|
125
|
+
if (!/^(y|yes|예|네|응)$/i.test(confirm)) {
|
|
126
|
+
const result = { schema: 'sks.codex-lb-setup.v1', ok: false, status: 'cancelled', plan, applied_actions: [] };
|
|
127
|
+
if (flag(args, '--json'))
|
|
128
|
+
return printJson(result);
|
|
129
|
+
console.log('codex-lb setup cancelled.');
|
|
130
|
+
process.exitCode = 1;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const result = await configureCodexLb({
|
|
135
|
+
host: options.host,
|
|
136
|
+
apiKey: options.apiKey,
|
|
137
|
+
keychain: options.keychain,
|
|
138
|
+
storeKeychain: options.keychain,
|
|
139
|
+
useDefaultProvider: options.useDefaultProvider,
|
|
140
|
+
writeEnvFile: options.writeEnvFile,
|
|
141
|
+
syncLaunchctl: options.syncLaunchctl,
|
|
142
|
+
shellProfile: options.shellProfile,
|
|
143
|
+
runHealth: options.health,
|
|
144
|
+
apiKeySource: options.apiKeySource,
|
|
145
|
+
allowInsecureHttp: options.allowInsecureLocalhost
|
|
146
|
+
});
|
|
102
147
|
const shaped = { schema: 'sks.codex-lb-setup.v1', ...result, api_key: { present: Boolean(options.apiKey), redacted: true }, env_file_chmod: '0600' };
|
|
148
|
+
if (options.health)
|
|
149
|
+
shaped.applied_actions = [...(shaped.applied_actions || []), { type: 'run_health_check', target: 'codex-lb response chain', ok: true }];
|
|
103
150
|
if (options.health)
|
|
104
151
|
shaped.chain_health = result.ok ? await checkCodexLbResponseChain(result, { force: true, root }) : null;
|
|
105
152
|
if (flag(args, '--json'))
|
|
@@ -167,22 +214,58 @@ async function codexLbSetupOptions(args = []) {
|
|
|
167
214
|
const baseUrl = readOption(args, '--base-url', null);
|
|
168
215
|
let host = baseUrl || readOption(args, '--host', readOption(args, '--domain', null));
|
|
169
216
|
let apiKey = readOption(args, '--api-key', readOption(args, '--key', null));
|
|
217
|
+
let apiKeySource = apiKey ? 'cli_option' : 'hidden_prompt';
|
|
170
218
|
let keychain = flag(args, '--keychain');
|
|
171
219
|
if (flag(args, '--api-key-stdin'))
|
|
172
220
|
apiKey = (await readStdin()).trim();
|
|
173
|
-
|
|
221
|
+
if (flag(args, '--api-key-stdin'))
|
|
222
|
+
apiKeySource = 'stdin';
|
|
223
|
+
let health = (flag(args, '--health') || flag(args, '--check')) && !flag(args, '--no-health');
|
|
224
|
+
let useDefaultProvider = flag(args, '--no-default-provider') ? false : true;
|
|
225
|
+
if (flag(args, '--use-default-provider'))
|
|
226
|
+
useDefaultProvider = true;
|
|
227
|
+
let writeEnvFile = flag(args, '--no-env-file') ? false : true;
|
|
228
|
+
if (flag(args, '--write-env-file'))
|
|
229
|
+
writeEnvFile = true;
|
|
230
|
+
if (flag(args, '--no-keychain'))
|
|
231
|
+
keychain = false;
|
|
232
|
+
let syncLaunchctl = flag(args, '--no-launchctl') ? false : true;
|
|
233
|
+
if (flag(args, '--launchctl'))
|
|
234
|
+
syncLaunchctl = true;
|
|
235
|
+
const shellProfile = normalizeShellProfile(readOption(args, '--shell-profile', 'skip'));
|
|
236
|
+
const allowInsecureLocalhost = flag(args, '--allow-insecure-localhost') || flag(args, '--allow-insecure-http');
|
|
237
|
+
const interactive = (!host || !apiKey || canAskInteractive(args)) && canAskInteractive(args);
|
|
174
238
|
if ((!host || !apiKey) && canAskInteractive(args)) {
|
|
175
239
|
console.log('SKS codex-lb setup\n');
|
|
176
240
|
host ||= (await ask('1. codex-lb domain or base URL?\n Example: lb.example.com or https://lb.example.com/backend-api/codex\n> ')).trim();
|
|
177
241
|
apiKey ||= (await askHidden('2. API key?\n Input hidden. Value will be stored securely and never printed.\n> ')).trim();
|
|
178
|
-
|
|
179
|
-
await ask('
|
|
242
|
+
apiKeySource = 'hidden_prompt';
|
|
243
|
+
useDefaultProvider = parseYesNo(await ask('3. Use this codex-lb as default for Codex launches? [Y/n] '), true);
|
|
244
|
+
writeEnvFile = parseYesNo(await ask('4. Write shell env loader to ~/.codex/sks-codex-lb.env? [Y/n] '), true);
|
|
180
245
|
const storeKeychain = (await ask('5. Store the key in macOS Keychain when available? [Y/n] ')).trim();
|
|
181
246
|
keychain = !/^(n|no|아니|아니요|ㄴ)$/i.test(storeKeychain || 'y');
|
|
182
|
-
|
|
247
|
+
syncLaunchctl = parseYesNo(await ask('6. Sync macOS launchctl environment when available? [Y/n] '), true);
|
|
248
|
+
const profile = (await ask('7. Install shell profile snippet? [zsh/bash/fish/all/skip] ')).trim();
|
|
249
|
+
const interactiveShellProfile = normalizeShellProfile(profile || 'skip');
|
|
250
|
+
const runHealth = (await ask('8. Run health check now? [Y/n] ')).trim();
|
|
183
251
|
health = !/^(n|no|아니|아니요|ㄴ)$/i.test(runHealth || 'y');
|
|
252
|
+
return { host, apiKey, health, keychain, useDefaultProvider, writeEnvFile, syncLaunchctl, shellProfile: interactiveShellProfile, allowInsecureLocalhost, apiKeySource, interactive: true, yes: flag(args, '--yes') };
|
|
184
253
|
}
|
|
185
|
-
return { host, apiKey, health, keychain };
|
|
254
|
+
return { host, apiKey, health, keychain, useDefaultProvider, writeEnvFile, syncLaunchctl, shellProfile, allowInsecureLocalhost, apiKeySource, interactive, yes: flag(args, '--yes') };
|
|
255
|
+
}
|
|
256
|
+
function normalizeShellProfile(value) {
|
|
257
|
+
const raw = String(value || 'skip').toLowerCase();
|
|
258
|
+
return raw === 'zsh' || raw === 'bash' || raw === 'fish' || raw === 'all' ? raw : 'skip';
|
|
259
|
+
}
|
|
260
|
+
function parseYesNo(value, fallback) {
|
|
261
|
+
const raw = String(value || '').trim();
|
|
262
|
+
if (!raw)
|
|
263
|
+
return fallback;
|
|
264
|
+
if (/^(y|yes|예|네|응)$/i.test(raw))
|
|
265
|
+
return true;
|
|
266
|
+
if (/^(n|no|아니|아니요|ㄴ)$/i.test(raw))
|
|
267
|
+
return false;
|
|
268
|
+
return fallback;
|
|
186
269
|
}
|
|
187
270
|
function canAskInteractive(args = []) {
|
|
188
271
|
return !flag(args, '--json') && !flag(args, '--yes') && Boolean(input.isTTY && output.isTTY && process.env.CI !== 'true');
|
package/dist/commands/wiki.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export declare function run(_command: any, args?: any): Promise<void | {
|
|
|
15
15
|
};
|
|
16
16
|
active_records: {
|
|
17
17
|
id: string;
|
|
18
|
-
kind: "incorrect_claim" | "overconfident_claim" | "stale_evidence" | "missing_evidence" | "test_failure" | "route_misclassification" | "scout_error" | "visual_anchor_error" | "image_bbox_error" | "db_safety_false_positive" | "db_safety_false_negative" | "hook_policy_mismatch" | "hook_semantic_mismatch" | "codex_lb_health_misread" | "codex_lb_missing_env_raw_message" | "computer_use_policy_misclassification" | "mock_real_confusion" | "user_intent_misread" | "artifact_schema_error" | "trust_status_overclaim";
|
|
18
|
+
kind: "incorrect_claim" | "overconfident_claim" | "stale_evidence" | "missing_evidence" | "test_failure" | "route_misclassification" | "scout_error" | "visual_anchor_error" | "image_bbox_error" | "db_safety_false_positive" | "db_safety_false_negative" | "hook_policy_mismatch" | "hook_semantic_mismatch" | "hook_strict_subset_misclassified" | "codex_lb_health_misread" | "codex_lb_missing_env_raw_message" | "codex_lb_setup_choice_drift" | "codex_lb_env_persistence_failure" | "computer_use_policy_misclassification" | "computer_use_live_smoke_mismatch" | "computer_use_external_block_overclaimed" | "mock_real_confusion" | "user_intent_misread" | "artifact_schema_error" | "trust_status_overclaim";
|
|
19
19
|
severity: "high" | "low" | "medium" | "critical";
|
|
20
20
|
route: string | null;
|
|
21
21
|
claim: string;
|
|
@@ -16,11 +16,13 @@ export declare function codexCompatibilityReport(opts?: any): Promise<{
|
|
|
16
16
|
hooks_semantic: {
|
|
17
17
|
ok: boolean;
|
|
18
18
|
warnings_count: number;
|
|
19
|
+
issues_by_category: Record<"schema_violation" | "upstream_semantic_unsupported" | "sks_zero_warning_disallowed" | "legacy_shape" | "policy_disallowed", number>;
|
|
19
20
|
events: {
|
|
20
21
|
event: "UserPromptSubmit" | "PreToolUse" | "PostToolUse" | "PermissionRequest" | "Stop" | "PreCompact" | "PostCompact" | "SessionStart";
|
|
21
22
|
checked: number;
|
|
22
23
|
ok: boolean;
|
|
23
24
|
warnings: string[];
|
|
25
|
+
issues_by_category: Record<"schema_violation" | "upstream_semantic_unsupported" | "sks_zero_warning_disallowed" | "legacy_shape" | "policy_disallowed", number>;
|
|
24
26
|
}[];
|
|
25
27
|
};
|
|
26
28
|
ok: boolean;
|
|
@@ -49,11 +51,13 @@ export declare function codexDoctorReport(opts?: any): Promise<{
|
|
|
49
51
|
hooks_semantic: {
|
|
50
52
|
ok: boolean;
|
|
51
53
|
warnings_count: number;
|
|
54
|
+
issues_by_category: Record<"schema_violation" | "upstream_semantic_unsupported" | "sks_zero_warning_disallowed" | "legacy_shape" | "policy_disallowed", number>;
|
|
52
55
|
events: {
|
|
53
56
|
event: "UserPromptSubmit" | "PreToolUse" | "PostToolUse" | "PermissionRequest" | "Stop" | "PreCompact" | "PostCompact" | "SessionStart";
|
|
54
57
|
checked: number;
|
|
55
58
|
ok: boolean;
|
|
56
59
|
warnings: string[];
|
|
60
|
+
issues_by_category: Record<"schema_violation" | "upstream_semantic_unsupported" | "sks_zero_warning_disallowed" | "legacy_shape" | "policy_disallowed", number>;
|
|
57
61
|
}[];
|
|
58
62
|
};
|
|
59
63
|
ok: boolean;
|
|
@@ -66,12 +70,15 @@ export declare function codexDoctorReport(opts?: any): Promise<{
|
|
|
66
70
|
ok: boolean;
|
|
67
71
|
baseline: string;
|
|
68
72
|
warnings_count: number;
|
|
73
|
+
issues_by_category: Record<"schema_violation" | "upstream_semantic_unsupported" | "sks_zero_warning_disallowed" | "legacy_shape" | "policy_disallowed", number>;
|
|
74
|
+
issues: import("./codex-hook-issues.js").CodexHookIssue[];
|
|
69
75
|
warnings: string[];
|
|
70
76
|
events: {
|
|
71
77
|
event: "UserPromptSubmit" | "PreToolUse" | "PostToolUse" | "PermissionRequest" | "Stop" | "PreCompact" | "PostCompact" | "SessionStart";
|
|
72
78
|
checked: number;
|
|
73
79
|
ok: boolean;
|
|
74
80
|
warnings: string[];
|
|
81
|
+
issues_by_category: Record<"schema_violation" | "upstream_semantic_unsupported" | "sks_zero_warning_disallowed" | "legacy_shape" | "policy_disallowed", number>;
|
|
75
82
|
}[];
|
|
76
83
|
config: {
|
|
77
84
|
schema: string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const CODEX_HOOK_ISSUE_CATEGORIES: readonly ["schema_violation", "upstream_semantic_unsupported", "sks_zero_warning_disallowed", "legacy_shape", "policy_disallowed"];
|
|
2
|
+
export type CodexHookIssueCategory = typeof CODEX_HOOK_ISSUE_CATEGORIES[number];
|
|
3
|
+
export interface CodexHookIssue {
|
|
4
|
+
category: CodexHookIssueCategory;
|
|
5
|
+
code: string;
|
|
6
|
+
message: string;
|
|
7
|
+
path?: string;
|
|
8
|
+
upstream_supported?: boolean;
|
|
9
|
+
sks_disallowed?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function makeCodexHookIssue(category: CodexHookIssueCategory, code: string, message: string, opts?: {
|
|
12
|
+
path?: string;
|
|
13
|
+
upstream_supported?: boolean;
|
|
14
|
+
sks_disallowed?: boolean;
|
|
15
|
+
}): CodexHookIssue;
|
|
16
|
+
export declare function schemaIssueToCodexHookIssue(issue: string): CodexHookIssue;
|
|
17
|
+
export declare function dedupeCodexHookIssues(issues: readonly CodexHookIssue[]): CodexHookIssue[];
|
|
18
|
+
export declare function codexHookIssuesByCategory(issues: readonly CodexHookIssue[]): Record<CodexHookIssueCategory, number>;
|
|
19
|
+
export declare function codexHookIssueWarningString(issue: CodexHookIssue): string;
|
|
20
|
+
//# sourceMappingURL=codex-hook-issues.d.ts.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export const CODEX_HOOK_ISSUE_CATEGORIES = Object.freeze([
|
|
2
|
+
'schema_violation',
|
|
3
|
+
'upstream_semantic_unsupported',
|
|
4
|
+
'sks_zero_warning_disallowed',
|
|
5
|
+
'legacy_shape',
|
|
6
|
+
'policy_disallowed'
|
|
7
|
+
]);
|
|
8
|
+
export function makeCodexHookIssue(category, code, message, opts = {}) {
|
|
9
|
+
const issue = {
|
|
10
|
+
category,
|
|
11
|
+
code: normalizeIssueCode(code),
|
|
12
|
+
message
|
|
13
|
+
};
|
|
14
|
+
if (opts.path)
|
|
15
|
+
issue.path = opts.path;
|
|
16
|
+
if (opts.upstream_supported !== undefined)
|
|
17
|
+
issue.upstream_supported = opts.upstream_supported;
|
|
18
|
+
if (opts.sks_disallowed !== undefined)
|
|
19
|
+
issue.sks_disallowed = opts.sks_disallowed;
|
|
20
|
+
return issue;
|
|
21
|
+
}
|
|
22
|
+
export function schemaIssueToCodexHookIssue(issue) {
|
|
23
|
+
const path = issue.split(':')[0] || '$';
|
|
24
|
+
const code = issue.includes(':unknown_field') ? 'unknown_field'
|
|
25
|
+
: issue.includes(':required') ? 'required'
|
|
26
|
+
: issue.includes(':type:') ? 'type'
|
|
27
|
+
: issue.includes(':enum') ? 'enum'
|
|
28
|
+
: issue.includes(':const') ? 'const'
|
|
29
|
+
: 'schema_violation';
|
|
30
|
+
return makeCodexHookIssue('schema_violation', code, `Codex hook output schema violation: ${issue}`, {
|
|
31
|
+
path,
|
|
32
|
+
upstream_supported: false,
|
|
33
|
+
sks_disallowed: true
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export function dedupeCodexHookIssues(issues) {
|
|
37
|
+
const seen = new Set();
|
|
38
|
+
const out = [];
|
|
39
|
+
for (const issue of issues) {
|
|
40
|
+
const key = [issue.category, issue.code, issue.path || '', issue.message].join('\u0000');
|
|
41
|
+
if (seen.has(key))
|
|
42
|
+
continue;
|
|
43
|
+
seen.add(key);
|
|
44
|
+
out.push(issue);
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
export function codexHookIssuesByCategory(issues) {
|
|
49
|
+
const summary = {
|
|
50
|
+
schema_violation: 0,
|
|
51
|
+
upstream_semantic_unsupported: 0,
|
|
52
|
+
sks_zero_warning_disallowed: 0,
|
|
53
|
+
legacy_shape: 0,
|
|
54
|
+
policy_disallowed: 0
|
|
55
|
+
};
|
|
56
|
+
for (const issue of issues)
|
|
57
|
+
summary[issue.category] += 1;
|
|
58
|
+
return summary;
|
|
59
|
+
}
|
|
60
|
+
export function codexHookIssueWarningString(issue) {
|
|
61
|
+
if (issue.code.startsWith('legacy_top_level_'))
|
|
62
|
+
return `legacy_top_level:${issue.code.slice('legacy_top_level_'.length)}`;
|
|
63
|
+
if (issue.code.startsWith('permission_request_reserved_'))
|
|
64
|
+
return `permission_request_reserved:${reservedPermissionRequestFieldName(issue.code.slice('permission_request_reserved_'.length))}`;
|
|
65
|
+
if (issue.code === 'snake_case')
|
|
66
|
+
return `${issue.path || '$'}:snake_case`;
|
|
67
|
+
if (issue.category === 'schema_violation')
|
|
68
|
+
return `${issue.path || '$'}:${issue.code}`;
|
|
69
|
+
if (issue.category === 'upstream_semantic_unsupported')
|
|
70
|
+
return `semantic_unsupported:${issue.message}`;
|
|
71
|
+
if (issue.category === 'sks_zero_warning_disallowed')
|
|
72
|
+
return `sks_zero_warning_disallowed:${issue.code}`;
|
|
73
|
+
if (issue.category === 'legacy_shape')
|
|
74
|
+
return `legacy_shape:${issue.code}`;
|
|
75
|
+
return `policy_disallowed:${issue.code}`;
|
|
76
|
+
}
|
|
77
|
+
function reservedPermissionRequestFieldName(value) {
|
|
78
|
+
if (value === 'updated_input')
|
|
79
|
+
return 'updatedInput';
|
|
80
|
+
if (value === 'updated_permissions')
|
|
81
|
+
return 'updatedPermissions';
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
function normalizeIssueCode(value) {
|
|
85
|
+
return String(value || 'issue')
|
|
86
|
+
.trim()
|
|
87
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
88
|
+
.toLowerCase()
|
|
89
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
90
|
+
.replace(/^_+|_+$/g, '')
|
|
91
|
+
|| 'issue';
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=codex-hook-issues.js.map
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { type CodexHookEventName } from './codex-schema-snapshot.js';
|
|
2
2
|
import { type CodexHookSemanticValidation } from './codex-hook-semantic-validator.js';
|
|
3
|
+
import { type CodexHookIssue } from './codex-hook-issues.js';
|
|
3
4
|
export type CodexSchemaValidation = {
|
|
4
5
|
ok: boolean;
|
|
5
6
|
event: CodexHookEventName;
|
|
6
7
|
issues: string[];
|
|
8
|
+
structured_issues: CodexHookIssue[];
|
|
7
9
|
};
|
|
8
10
|
export type CodexHookOutputValidation = CodexSchemaValidation & {
|
|
9
11
|
semantic: CodexHookSemanticValidation;
|
|
@@ -18,6 +20,7 @@ export declare function validateCodexFixtureOutputs(root?: string): Promise<{
|
|
|
18
20
|
semantic: CodexHookSemanticValidation;
|
|
19
21
|
ok: boolean;
|
|
20
22
|
issues: string[];
|
|
23
|
+
structured_issues: CodexHookIssue[];
|
|
21
24
|
event: CodexHookEventName;
|
|
22
25
|
file: string;
|
|
23
26
|
}[];
|
|
@@ -2,11 +2,12 @@ import path from 'node:path';
|
|
|
2
2
|
import { exists, projectRoot, readJson } from '../fsx.js';
|
|
3
3
|
import { CODEX_HOOK_EVENTS, codexHookEventName, readCodexHookSchema } from './codex-schema-snapshot.js';
|
|
4
4
|
import { validateCodexHookSemanticOutput } from './codex-hook-semantic-validator.js';
|
|
5
|
+
import { schemaIssueToCodexHookIssue } from './codex-hook-issues.js';
|
|
5
6
|
export async function validateCodexHookOutput(eventLike, output) {
|
|
6
7
|
const event = codexHookEventName(eventLike) || 'UserPromptSubmit';
|
|
7
8
|
const schema = await readCodexHookSchema(event, 'output');
|
|
8
9
|
const issues = validateJsonValue(output, schema, schema, '$');
|
|
9
|
-
return { ok: issues.length === 0, event, issues };
|
|
10
|
+
return { ok: issues.length === 0, event, issues, structured_issues: issues.map(schemaIssueToCodexHookIssue) };
|
|
10
11
|
}
|
|
11
12
|
export async function validateCodexFixtureOutputs(root) {
|
|
12
13
|
root ||= await projectRoot();
|
|
@@ -25,9 +26,11 @@ export async function validateCodexFixtureOutputs(root) {
|
|
|
25
26
|
ok: validation.ok && semantic.ok,
|
|
26
27
|
issues: [
|
|
27
28
|
...validation.issues,
|
|
28
|
-
...semantic.
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
...semantic.issues.map((issue) => `${issue.category}:${issue.code}:${issue.message}`)
|
|
30
|
+
],
|
|
31
|
+
structured_issues: [
|
|
32
|
+
...validation.structured_issues,
|
|
33
|
+
...semantic.issues
|
|
31
34
|
]
|
|
32
35
|
});
|
|
33
36
|
}
|