@occasiolabs/occasio 0.8.3 → 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 +1 -0
- package/docs/ADAPTER-STAGE-2-MIGRATION.md +59 -0
- package/docs/ARCHITECTURE.md +171 -0
- package/docs/STAGE-2-STEP-5-SHELL-PLAN.md +107 -0
- package/docs/THREAT-MODEL.md +195 -0
- package/docs/edr-calibration.md +29 -0
- package/package.json +12 -2
- package/src/adapters/claude-code.js +1 -2
- package/src/adapters/computer-use.js +1 -1
- package/src/anomaly/cli.js +4 -1
- package/src/anomaly/detectors/deny-rate.js +2 -1
- package/src/anomaly/detectors/file-read-volume.js +2 -1
- package/src/anomaly/index.js +5 -0
- package/src/attest/check-summary.js +1 -1
- package/src/attest/index.js +14 -1
- package/src/audit/jsonl-auditor.js +180 -14
- package/src/audit/repair.js +118 -0
- package/src/audit/verifier.js +36 -2
- package/src/boundary.js +1 -1
- package/src/classifier.js +1 -1
- package/src/cli/clear.js +55 -0
- package/src/cli/help.js +102 -0
- package/src/cli/register.js +90 -0
- package/src/cli/status.js +94 -0
- package/src/cost/prices.js +106 -0
- package/src/dashboard.js +2 -3
- package/src/distiller.js +1 -1
- package/src/executor/dispatcher.js +2 -2
- package/src/executor/native-handlers/glob.js +173 -0
- package/src/executor/native-handlers/grep.js +258 -0
- package/src/executor/native-handlers/read.js +99 -0
- package/src/executor/native-handlers/todo.js +56 -0
- package/src/harness.js +8 -10
- package/src/index.js +26 -283
- package/src/inspect.js +1 -1
- package/src/interceptor.js +9 -29
- package/src/ledger.js +2 -3
- package/src/mcp-experiment.js +4 -4
- package/src/mcp-server.js +3 -3
- package/src/policy/doctor.js +2 -2
- package/src/policy/engine.js +0 -1
- package/src/policy/init.js +1 -1
- package/src/policy/loader.js +3 -3
- package/src/policy/show.js +1 -2
- package/src/preflight/cli.js +0 -1
- package/src/preflight/miner.js +3 -6
- package/src/redteam.js +1 -2
- package/src/replay.js +1 -1
- package/src/report/index.js +0 -4
- package/src/runtime.js +42 -444
- package/src/selftest.js +1 -1
- package/src/session.js +1 -1
package/src/index.js
CHANGED
|
@@ -71,70 +71,14 @@ function todayStr() { const d = new Date(); return `${d.getFullYear()}-${String(
|
|
|
71
71
|
function getLogFile() { return path.join(LOG_DIR, 'logs', `${todayStr()}.jsonl`); }
|
|
72
72
|
function getBlockedFile() { return path.join(LOG_DIR, 'blocked', `${todayStr()}-secrets.log`); }
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
function getPrice(model) {
|
|
85
|
-
if (!model) return MODEL_PRICES.default;
|
|
86
|
-
for (const [k, v] of Object.entries(MODEL_PRICES)) {
|
|
87
|
-
if (k !== 'default' && model.includes(k)) return v;
|
|
88
|
-
}
|
|
89
|
-
return MODEL_PRICES.default;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function calcCost(model, inp, out, cacheWrite = 0, cacheRead = 0) {
|
|
93
|
-
const p = getPrice(model);
|
|
94
|
-
return (inp / 1e6 * p.in) + (out / 1e6 * p.out)
|
|
95
|
-
+ (cacheWrite / 1e6 * p.cache_write) + (cacheRead / 1e6 * p.cache_read);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Savings from Anthropic prompt caching: cache reads are 10× cheaper than fresh input.
|
|
99
|
-
function calcCacheSavings(model, cacheReadTokens) {
|
|
100
|
-
if (!cacheReadTokens) return 0;
|
|
101
|
-
const p = getPrice(model);
|
|
102
|
-
return (cacheReadTokens / 1e6) * (p.in - p.cache_read);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Cross-request compounding savings: reads the run's JSONL entries in sequence order
|
|
106
|
-
// and weights each distilled batch by the exact number of subsequent API calls that
|
|
107
|
-
// carry the smaller result in their conversation history.
|
|
108
|
-
// Formula: Σ (distill_tokens_saved[i] / 1M × price_in × (N - i - 1))
|
|
109
|
-
// Returns { savings: float, carryInstances: int }
|
|
110
|
-
// carryInstances = total sum of subsequent-call counts across all distilled batches
|
|
111
|
-
// — the actual multiplier used — so the display is self-explanatory.
|
|
112
|
-
// Assumption: tool results accumulate in Claude Code's message history for all
|
|
113
|
-
// subsequent requests in the session (true for normal sessions; may not hold if
|
|
114
|
-
// Claude Code resets context mid-session).
|
|
115
|
-
function calcCompoundingSavings(runId, logFile, model) {
|
|
116
|
-
if (!runId) return { savings: 0, carryInstances: 0 };
|
|
117
|
-
let entries;
|
|
118
|
-
try {
|
|
119
|
-
const lines = fs.readFileSync(logFile, 'utf8').trim().split('\n').filter(Boolean);
|
|
120
|
-
entries = lines
|
|
121
|
-
.map(l => { try { return JSON.parse(l); } catch { return null; } })
|
|
122
|
-
.filter(e => e && e.run_id === runId);
|
|
123
|
-
} catch { return { savings: 0, carryInstances: 0 }; }
|
|
124
|
-
const N = entries.length;
|
|
125
|
-
if (N < 2) return { savings: 0, carryInstances: 0 };
|
|
126
|
-
const p = getPrice(model);
|
|
127
|
-
let savings = 0, carryInstances = 0;
|
|
128
|
-
for (let i = 0; i < N; i++) {
|
|
129
|
-
const dt = entries[i].distill_tokens_saved || 0;
|
|
130
|
-
if (dt > 0) {
|
|
131
|
-
const subsequent = N - i - 1;
|
|
132
|
-
savings += (dt / 1e6) * p.in * subsequent;
|
|
133
|
-
carryInstances += subsequent;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return { savings, carryInstances };
|
|
137
|
-
}
|
|
74
|
+
// Pricing extracted to its own module — see src/cost/prices.js for the
|
|
75
|
+
// MODEL_PRICES table + cost-arithmetic helpers. Re-exported here so the
|
|
76
|
+
// rest of index.js keeps its existing call sites unchanged.
|
|
77
|
+
const {
|
|
78
|
+
calcCost,
|
|
79
|
+
calcCacheSavings,
|
|
80
|
+
calcCompoundingSavings,
|
|
81
|
+
} = require('./cost/prices');
|
|
138
82
|
|
|
139
83
|
// ── Persistence ────────────────────────────────────────────────────────────────
|
|
140
84
|
|
|
@@ -156,7 +100,7 @@ function updateSession(e) {
|
|
|
156
100
|
lao_tokens_saved:0, lao_cost_saved:0, tools_local_count:0, tools_mcp_count:0,
|
|
157
101
|
tools_attempted:0,
|
|
158
102
|
};
|
|
159
|
-
try { s = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8')); } catch {}
|
|
103
|
+
try { s = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8')); } catch { /* ignore */ }
|
|
160
104
|
s.requests++;
|
|
161
105
|
s.input_tokens += e.input_tokens || 0;
|
|
162
106
|
s.output_tokens += e.output_tokens || 0;
|
|
@@ -311,223 +255,22 @@ const cmd = args[0];
|
|
|
311
255
|
if (cmd === '--version' || cmd === '-v') { console.log(`occasio v${VERSION}`); process.exit(0); }
|
|
312
256
|
|
|
313
257
|
if (cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
314
|
-
|
|
315
|
-
${col.b(`⚡ Occasio v${VERSION}`)}
|
|
316
|
-
|
|
317
|
-
${col.b('Usage:')}
|
|
318
|
-
occasio claude [args...] Start Claude with local proxy (intercept + log)
|
|
319
|
-
occasio demo 10-second proof: see Occasio block real secrets
|
|
320
|
-
occasio demo attest End-to-end attestation pipeline against a synthetic audit chain
|
|
321
|
-
occasio demo anomalies End-to-end EDR test: synthetic adversarial chain → all 4 detectors
|
|
322
|
-
occasio dashboard Open live dashboard for the running session
|
|
323
|
-
occasio register Register shell alias (type 'claude' directly)
|
|
324
|
-
occasio status Show session stats and savings breakdown
|
|
325
|
-
occasio doctor Check setup: Node, claude CLI, port, Python, profile
|
|
326
|
-
occasio clear Reset today's log and session data
|
|
327
|
-
occasio clear --history Wipe all historical logs
|
|
328
|
-
occasio ledger Inspect token ledger (--last N, --summary, --scope session|today)
|
|
329
|
-
occasio replay Replay run audit (--last N, --detail, --run <id>, --attribute)
|
|
330
|
-
occasio distill Inspect distilled outputs (--last N, --entry <N> for raw)
|
|
331
|
-
occasio inspect Cloud-boundary manifest (--last N, --entry N, --run <id>)
|
|
332
|
-
occasio boundary Per-request three-column view: produced / re-entered / prevented
|
|
333
|
-
occasio baseline Behavior baseline: [learn|show|compare|reset] (per project cwd)
|
|
334
|
-
occasio harness Run a real Claude Code session against scratch fixtures and verify governance claims (needs ANTHROPIC_API_KEY)
|
|
335
|
-
occasio redteam Autonomous adversarial test — tester LLM probes a subject Claude Code session under Occasio (needs ANTHROPIC_API_KEY + @anthropic-ai/sdk)
|
|
336
|
-
occasio policy [show] Show active policy: flags, tool routing, overrides
|
|
337
|
-
occasio policy show --diff Only values that differ from defaults
|
|
338
|
-
occasio policy validate Validate policy.yml and report errors/warnings
|
|
339
|
-
occasio policy init Create a starter policy.yml (safe, non-destructive)
|
|
340
|
-
Use --template strict|finance for a non-default starter
|
|
341
|
-
occasio policy doctor Cross-reference session logs with policy; surface suggestions
|
|
342
|
-
occasio audit [verify] Verify tamper-evident hash chain in pipeline-events.jsonl
|
|
343
|
-
occasio report Governance export: file access log, blocked paths, secret events
|
|
344
|
-
occasio anomalies Live anomaly detection over the audit chain (--window 15m, --json)
|
|
345
|
-
occasio computer-use Apply a Computer-Use policy to a JSONL of tool_use blocks (--dry-run --example)
|
|
346
|
-
occasio attest --run-id <uuid> AI-Agent Behavioral Attestation v1: hash-chain commitment + execution summary for one run
|
|
347
|
-
Add --sign in GitHub Actions (with permissions: id-token: write) for Sigstore keyless signing
|
|
348
|
-
occasio attest verify <file> Re-verify a signed attestation: Sigstore bundle + DSSE payload match + audit chain integrity
|
|
349
|
-
occasio selftest Run governance self-checks on a scratch chain (does not touch your audit log)
|
|
350
|
-
occasio report --format csv CSV export for auditors / SIEM import
|
|
351
|
-
occasio mcp-experiment MCP vs. built-in tool adoption stats (experiment)
|
|
352
|
-
|
|
353
|
-
${col.b('Presets:')}
|
|
354
|
-
--preset balanced (default) Intercept safe reads locally, log all requests
|
|
355
|
-
--preset strict Block requests that contain detected secrets
|
|
356
|
-
--preset off Log only — no interception, no blocking
|
|
357
|
-
|
|
358
|
-
${col.b('Flags:')}
|
|
359
|
-
--budget <N> Block requests once session cost exceeds $N (e.g. --budget 1.00)
|
|
360
|
-
--hardened Route Read/Glob/Grep through unified runtime (distill + secret scan)
|
|
361
|
-
--block-secrets Alias for --preset strict
|
|
362
|
-
--log-only Alias for --preset off
|
|
363
|
-
--dashboard Open live dashboard at http://localhost:3001
|
|
364
|
-
--port <N> Proxy port (default: 8081)
|
|
365
|
-
--verbose Print live per-request chatter (off by default — quiet for Claude Code's TUI)
|
|
366
|
-
|
|
367
|
-
${col.b('Multi-agent routing:')}
|
|
368
|
-
Default → Claude Code adapter
|
|
369
|
-
Header x-occasio-agent: cline → Cline adapter (synthetic; live validation pending)
|
|
370
|
-
|
|
371
|
-
${col.b('Logs:')} ~/.occasio/logs/YYYY-MM-DD.jsonl
|
|
372
|
-
`);
|
|
258
|
+
require('./cli/help').run();
|
|
373
259
|
process.exit(0);
|
|
374
260
|
}
|
|
375
261
|
|
|
376
262
|
if (cmd === 'register') {
|
|
377
|
-
|
|
378
|
-
const isWindows = process.platform === 'win32';
|
|
379
|
-
|
|
380
|
-
if (isWindows) {
|
|
381
|
-
const profileDir = path.join(os.homedir(), 'Documents', 'PowerShell');
|
|
382
|
-
const profileFile = path.join(profileDir, 'Microsoft.PowerShell_profile.ps1');
|
|
383
|
-
const snippet = `\n# Occasio — intercept Claude Code traffic\nfunction claude { occasio claude @args }\n`;
|
|
384
|
-
const alreadyMarker = 'occasio claude @args';
|
|
385
|
-
const legacyMarker = 'occasio --intercept @args';
|
|
386
|
-
try {
|
|
387
|
-
if (!fs.existsSync(profileDir)) fs.mkdirSync(profileDir, { recursive: true });
|
|
388
|
-
const existing = fs.existsSync(profileFile) ? fs.readFileSync(profileFile, 'utf8') : '';
|
|
389
|
-
if (existing.includes(alreadyMarker)) {
|
|
390
|
-
console.log(col.g('✓ Already registered (PowerShell)'));
|
|
391
|
-
console.log(col.d(' Type: claude'));
|
|
392
|
-
} else if (existing.includes(legacyMarker)) {
|
|
393
|
-
// Upgrade old --intercept form to canonical `occasio claude`
|
|
394
|
-
const updated = existing.replace(
|
|
395
|
-
/function claude \{ occasio --intercept @args \}/g,
|
|
396
|
-
'function claude { occasio claude @args }'
|
|
397
|
-
);
|
|
398
|
-
fs.writeFileSync(profileFile, updated);
|
|
399
|
-
console.log(col.g('✓ Updated to canonical form (occasio claude)'));
|
|
400
|
-
console.log('');
|
|
401
|
-
console.log(col.y(` ⚠ Restart PowerShell — the 'claude' alias is not active yet.`));
|
|
402
|
-
console.log(col.d(` Open a new terminal, or run: . $PROFILE`));
|
|
403
|
-
console.log('');
|
|
404
|
-
} else {
|
|
405
|
-
fs.appendFileSync(profileFile, snippet);
|
|
406
|
-
console.log(col.g(`✓ Registered in ${profileFile}`));
|
|
407
|
-
console.log('');
|
|
408
|
-
console.log(col.y(` ⚠ Restart PowerShell — the 'claude' alias is not active yet.`));
|
|
409
|
-
console.log(col.d(` Open a new terminal, or run: . $PROFILE`));
|
|
410
|
-
console.log('');
|
|
411
|
-
}
|
|
412
|
-
} catch (e) {
|
|
413
|
-
console.log(col.r(`✗ Could not write profile: ${e.message}`));
|
|
414
|
-
console.log(col.d(` Add manually to your PowerShell profile:\n function claude { occasio claude @args }`));
|
|
415
|
-
}
|
|
416
|
-
} else {
|
|
417
|
-
const rcFile = (process.env.SHELL || '').includes('zsh')
|
|
418
|
-
? path.join(os.homedir(), '.zshrc')
|
|
419
|
-
: path.join(os.homedir(), '.bashrc');
|
|
420
|
-
const snippet = `\n# Occasio — intercept Claude Code traffic\nclaude() { occasio claude "$@"; }\n`;
|
|
421
|
-
const alreadyMarker = 'occasio claude "$@"';
|
|
422
|
-
const legacyMarker = 'occasio --intercept "$@"';
|
|
423
|
-
try {
|
|
424
|
-
const existing = fs.existsSync(rcFile) ? fs.readFileSync(rcFile, 'utf8') : '';
|
|
425
|
-
if (existing.includes(alreadyMarker)) {
|
|
426
|
-
console.log(col.g(`✓ Already registered (${rcFile})`));
|
|
427
|
-
} else if (existing.includes(legacyMarker)) {
|
|
428
|
-
const updated = existing.replace(
|
|
429
|
-
/claude\(\) \{ occasio --intercept "\$@"; \}/g,
|
|
430
|
-
'claude() { occasio claude "$@"; }'
|
|
431
|
-
);
|
|
432
|
-
fs.writeFileSync(rcFile, updated);
|
|
433
|
-
console.log(col.g(`✓ Updated to canonical form in ${rcFile}`));
|
|
434
|
-
} else {
|
|
435
|
-
fs.appendFileSync(rcFile, snippet);
|
|
436
|
-
console.log(col.g(`✓ Registered in ${rcFile}`));
|
|
437
|
-
}
|
|
438
|
-
console.log(col.d(' Run: source ' + rcFile + ' — then type: claude'));
|
|
439
|
-
} catch (e) {
|
|
440
|
-
console.log(col.r(`✗ Could not write ${rcFile}: ${e.message}`));
|
|
441
|
-
console.log(col.d(` Add manually:\n claude() { occasio claude "$@"; }`));
|
|
442
|
-
}
|
|
443
|
-
}
|
|
263
|
+
require('./cli/register').run();
|
|
444
264
|
process.exit(0);
|
|
445
265
|
}
|
|
446
266
|
|
|
447
267
|
if (cmd === 'status' || cmd === 'stats') {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
if (!s) { console.log(col.d(' No session data yet. Run: occasio claude\n')); process.exit(0); }
|
|
451
|
-
|
|
452
|
-
const cacheSav = s.cache_savings || 0;
|
|
453
|
-
const laoSav = s.lao_cost_saved || 0;
|
|
454
|
-
const distSav = s.distill_cost_saved || 0;
|
|
455
|
-
const payload = laoSav + distSav;
|
|
456
|
-
const { savings: context } =
|
|
457
|
-
calcCompoundingSavings(s.run_id, s.log_file || getLogFile(), s.model || '');
|
|
458
|
-
const totalSav = payload + context + cacheSav;
|
|
459
|
-
const broaderCf = (s.cost || 0) + totalSav;
|
|
460
|
-
const savedPct = broaderCf > 0.00001 ? Math.round(totalSav / broaderCf * 100) : 0;
|
|
461
|
-
|
|
462
|
-
// Headline
|
|
463
|
-
if (totalSav > 0.00001) {
|
|
464
|
-
console.log(col.g(` Saved: $${totalSav.toFixed(4)}`) +
|
|
465
|
-
col.d(` (${savedPct}% off — would have cost $${broaderCf.toFixed(4)})`));
|
|
466
|
-
} else {
|
|
467
|
-
console.log(col.d(` Saved: $0.0000 (no interceptable tool calls in this session yet)`));
|
|
468
|
-
}
|
|
469
|
-
console.log(col.y(` Cost: $${s.cost.toFixed(4)}`));
|
|
470
|
-
|
|
471
|
-
// Plain-English coverage. Defensive: legacy sessions (pre-multi-round-fix)
|
|
472
|
-
// may have tools_attempted undercounted relative to tools_local_count.
|
|
473
|
-
// We clamp the denominator to at least the numerator so the displayed
|
|
474
|
-
// ratio is always 0–100% and never reads "X of Y < X (>100%)".
|
|
475
|
-
const localCnt = s.tools_local_count || 0;
|
|
476
|
-
const mcpCnt = s.tools_mcp_count || 0;
|
|
477
|
-
const attempted = s.tools_attempted || 0;
|
|
478
|
-
const totalLocal = localCnt + mcpCnt;
|
|
479
|
-
const denom = Math.max(attempted, totalLocal);
|
|
480
|
-
if (denom > 0) {
|
|
481
|
-
const cpct = Math.round(totalLocal / denom * 100);
|
|
482
|
-
const cColor = cpct >= 80 ? col.g : cpct >= 50 ? col.y : col.r;
|
|
483
|
-
console.log(cColor(` Ran locally: ${totalLocal} of ${denom} tool calls (${cpct}%)`));
|
|
484
|
-
}
|
|
485
|
-
if (s.blocked) console.log(col.r(` Blocked: ${s.blocked} secrets`));
|
|
486
|
-
if (s.secrets_redacted) console.log(col.c(` Redacted: ${s.secrets_redacted} secret${s.secrets_redacted !== 1 ? 's' : ''} in tool results`));
|
|
487
|
-
if (s.tools_transformed) console.log(col.c(` Transforms: ${s.tools_transformed} tool result${s.tools_transformed !== 1 ? 's' : ''} shaped`));
|
|
488
|
-
if (s.budget != null) {
|
|
489
|
-
const pct = Math.min(999, Math.round((s.cost || 0) / s.budget * 100));
|
|
490
|
-
const budgetStr = fmtBudget(s.cost || 0, s.budget);
|
|
491
|
-
const budgetColor = pct >= 100 ? col.r : pct >= 80 ? col.y : col.g;
|
|
492
|
-
console.log(budgetColor(` Budget: ${budgetStr}`));
|
|
493
|
-
if (s.budget_exceeded_count) console.log(col.r(` BudgetBlk: ${s.budget_exceeded_count} request(s) blocked`));
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// Detail
|
|
497
|
-
console.log(col.d(` ────`));
|
|
498
|
-
console.log(col.d(` Requests: ${s.requests} · ${(s.input_tokens/1000).toFixed(1)}k tokens in · ${(s.output_tokens/1000).toFixed(1)}k out`));
|
|
499
|
-
if (totalSav > 0.00001) {
|
|
500
|
-
const parts = [];
|
|
501
|
-
if (payload > 0.00001) parts.push(`$${payload.toFixed(4)} payload`);
|
|
502
|
-
if (context > 0.00001) parts.push(`$${context.toFixed(4)} context`);
|
|
503
|
-
if (cacheSav > 0.00001) parts.push(`$${cacheSav.toFixed(4)} cache`);
|
|
504
|
-
if (parts.length) console.log(col.d(` Breakdown: ${parts.join(' + ')}`));
|
|
505
|
-
}
|
|
506
|
-
const tail = [];
|
|
507
|
-
if (s.mode) tail.push(`Mode: ${s.mode}`);
|
|
508
|
-
if (s.start) tail.push(`Since: ${new Date(s.start).toLocaleString()}`);
|
|
509
|
-
if (tail.length) console.log(col.d(` ${tail.join(' · ')}`));
|
|
510
|
-
console.log(''); process.exit(0);
|
|
268
|
+
require('./cli/status').run();
|
|
269
|
+
process.exit(0);
|
|
511
270
|
}
|
|
512
271
|
|
|
513
272
|
if (cmd === 'clear') {
|
|
514
|
-
|
|
515
|
-
const clearAll = args.slice(1).includes('--history');
|
|
516
|
-
if (clearAll) {
|
|
517
|
-
const logsDir = path.join(LOG_DIR, 'logs');
|
|
518
|
-
const blockedDir = path.join(LOG_DIR, 'blocked');
|
|
519
|
-
let n = 0;
|
|
520
|
-
for (const dir of [logsDir, blockedDir]) {
|
|
521
|
-
try { for (const f of fs.readdirSync(dir)) { fs.unlinkSync(path.join(dir, f)); n++; } } catch {}
|
|
522
|
-
}
|
|
523
|
-
try { fs.unlinkSync(SESSION_FILE); } catch {}
|
|
524
|
-
console.log(col.g(`✓ Cleared all history (${n} log files) and session data`));
|
|
525
|
-
} else {
|
|
526
|
-
try { fs.unlinkSync(getLogFile()); } catch {}
|
|
527
|
-
try { fs.unlinkSync(SESSION_FILE); } catch {}
|
|
528
|
-
console.log(col.g("✓ Cleared today's log and session data"));
|
|
529
|
-
console.log(col.d(' Use --history to wipe all historical logs'));
|
|
530
|
-
}
|
|
273
|
+
require('./cli/clear').run(args.slice(1));
|
|
531
274
|
process.exit(0);
|
|
532
275
|
}
|
|
533
276
|
|
|
@@ -817,7 +560,7 @@ if (cmd === 'doctor' || cmd === 'check') {
|
|
|
817
560
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
818
561
|
}).toString().trim();
|
|
819
562
|
ok('Python (LAO)', out); pyFound = true;
|
|
820
|
-
} catch {}
|
|
563
|
+
} catch { /* ignore */ }
|
|
821
564
|
}
|
|
822
565
|
if (!pyFound) bad('Python (LAO)', 'not found — context trimming disabled');
|
|
823
566
|
if (laoPyExists) ok('LAO scorer', laoPyPath);
|
|
@@ -943,7 +686,7 @@ const sessionAuditor = _createAuditor(process.env.OCCASIO_AUDIT_FILE || undefine
|
|
|
943
686
|
process.stderr.write(`\n${col.r('[occasio][audit-fatal]')} policy_loaded write failed: ${status.error?.message}\n`);
|
|
944
687
|
process.stderr.write(`${col.r('[occasio][audit-fatal] dropped row:')} ${dropped}\n`);
|
|
945
688
|
process.stderr.write(`${col.r('[occasio][audit-fatal] proxy aborting; supervisor will restart.')}\n`);
|
|
946
|
-
try { server && server.close && server.close(); } catch {}
|
|
689
|
+
try { server && server.close && server.close(); } catch { /* ignore */ }
|
|
947
690
|
setTimeout(() => process.exit(1), 250);
|
|
948
691
|
}
|
|
949
692
|
});
|
|
@@ -982,7 +725,7 @@ if (budget !== null) {
|
|
|
982
725
|
const { execSync: _ex } = require('child_process');
|
|
983
726
|
let _pyOk = false;
|
|
984
727
|
for (const _cmd of ['python', 'python3']) {
|
|
985
|
-
try { _ex(`${_cmd} --version`, { shell: process.platform === 'win32', timeout: 3000, stdio: 'pipe' }); _pyOk = true; break; } catch {}
|
|
728
|
+
try { _ex(`${_cmd} --version`, { shell: process.platform === 'win32', timeout: 3000, stdio: 'pipe' }); _pyOk = true; break; } catch { /* ignore */ }
|
|
986
729
|
}
|
|
987
730
|
if (!_pyOk) process.stderr.write(col.y(` ⚠ LAO disabled — Python not found (context trimming inactive)\n`));
|
|
988
731
|
}
|
|
@@ -1030,7 +773,7 @@ const server = http.createServer((req, res) => {
|
|
|
1030
773
|
try {
|
|
1031
774
|
const rules = JSON.parse(fs.readFileSync(rp, 'utf8'));
|
|
1032
775
|
blocked = files.filter(f => (rules.block || []).some(p => f.includes(p.replace(/\*\*/g, '').replace(/\*/g, ''))));
|
|
1033
|
-
} catch {}
|
|
776
|
+
} catch { /* ignore */ }
|
|
1034
777
|
}
|
|
1035
778
|
|
|
1036
779
|
const shouldBlock = (mode === 'block_secrets' && secrets.length) || (mode === 'block_rules' && blocked.length);
|
|
@@ -1063,7 +806,7 @@ const server = http.createServer((req, res) => {
|
|
|
1063
806
|
res.end(JSON.stringify({ error: { type: 'blocked', reason: secrets.length ? secrets[0].label : 'rule', by: 'Occasio' } }));
|
|
1064
807
|
return;
|
|
1065
808
|
}
|
|
1066
|
-
} catch {}
|
|
809
|
+
} catch { /* ignore */ }
|
|
1067
810
|
}
|
|
1068
811
|
|
|
1069
812
|
// ── Budget enforcement (Stage 2: policy-driven BLOCK) ─────────────────────
|
|
@@ -1097,7 +840,7 @@ const server = http.createServer((req, res) => {
|
|
|
1097
840
|
const s = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8'));
|
|
1098
841
|
s.budget_exceeded_count = (s.budget_exceeded_count || 0) + 1;
|
|
1099
842
|
fs.writeFileSync(SESSION_FILE, JSON.stringify(s));
|
|
1100
|
-
} catch {}
|
|
843
|
+
} catch { /* ignore */ }
|
|
1101
844
|
const synth = decision.syntheticResponse;
|
|
1102
845
|
res.writeHead(synth.status, { 'Content-Type': 'application/json' });
|
|
1103
846
|
res.end(JSON.stringify(synth.body));
|
|
@@ -1125,7 +868,7 @@ const server = http.createServer((req, res) => {
|
|
|
1125
868
|
}
|
|
1126
869
|
}
|
|
1127
870
|
}
|
|
1128
|
-
} catch {}
|
|
871
|
+
} catch { /* ignore */ }
|
|
1129
872
|
}
|
|
1130
873
|
// ──────────────────────────────────────────────────────────────────────────
|
|
1131
874
|
|
|
@@ -1312,7 +1055,7 @@ const server = http.createServer((req, res) => {
|
|
|
1312
1055
|
}
|
|
1313
1056
|
forwardBody = Buffer.from(JSON.stringify(b));
|
|
1314
1057
|
outboundMessageCount = b.messages?.length ?? outboundMessageCount;
|
|
1315
|
-
} catch {}
|
|
1058
|
+
} catch { /* ignore */ }
|
|
1316
1059
|
}
|
|
1317
1060
|
if (laoDropped.length > 0) {
|
|
1318
1061
|
const ts0 = new Date().toTimeString().slice(0, 8);
|
|
@@ -1438,7 +1181,7 @@ const server = http.createServer((req, res) => {
|
|
|
1438
1181
|
process.stderr.write(`\n${col.r('[occasio][audit-fatal]')} ${e.message}\n`);
|
|
1439
1182
|
process.stderr.write(`${col.r('[occasio][audit-fatal] dropped row:')} ${dropped}\n`);
|
|
1440
1183
|
process.stderr.write(`${col.r('[occasio][audit-fatal] proxy aborting; supervisor will restart.')}\n`);
|
|
1441
|
-
try { server && server.close && server.close(); } catch {}
|
|
1184
|
+
try { server && server.close && server.close(); } catch { /* ignore */ }
|
|
1442
1185
|
setTimeout(() => process.exit(1), 250);
|
|
1443
1186
|
return;
|
|
1444
1187
|
}
|
|
@@ -1467,10 +1210,10 @@ const server = http.createServer((req, res) => {
|
|
|
1467
1210
|
cacheRead = d.usage.cache_read_input_tokens || cacheRead;
|
|
1468
1211
|
}
|
|
1469
1212
|
if (d.type === 'message_delta' && d.usage) out = d.usage.output_tokens || out;
|
|
1470
|
-
} catch {}
|
|
1213
|
+
} catch { /* ignore */ }
|
|
1471
1214
|
}
|
|
1472
1215
|
}
|
|
1473
|
-
} catch {}
|
|
1216
|
+
} catch { /* ignore */ }
|
|
1474
1217
|
|
|
1475
1218
|
// When the interceptor ran, Anthropic was billed for N calls:
|
|
1476
1219
|
// call #1 → initial tool_use round (toolCallUsage)
|
|
@@ -1705,7 +1448,7 @@ server.listen(PORT, '127.0.0.1', () => {
|
|
|
1705
1448
|
if (parts.length) process.stderr.write(col.d(` Breakdown: ${parts.join(' + ')}\n`));
|
|
1706
1449
|
}
|
|
1707
1450
|
process.stderr.write('────────────────────────────────────────\n\n');
|
|
1708
|
-
} catch {}
|
|
1451
|
+
} catch { /* ignore */ }
|
|
1709
1452
|
process.exit(code || 0);
|
|
1710
1453
|
});
|
|
1711
1454
|
|
package/src/inspect.js
CHANGED
|
@@ -268,7 +268,7 @@ function printBoundaryEntry(entry, idxLabel, total) {
|
|
|
268
268
|
|
|
269
269
|
function runInspectCli(args) {
|
|
270
270
|
let session = null;
|
|
271
|
-
try { session = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8')); } catch {}
|
|
271
|
+
try { session = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8')); } catch { /* ignore */ }
|
|
272
272
|
|
|
273
273
|
const todayEntries = readDayLog(todayStr());
|
|
274
274
|
|
package/src/interceptor.js
CHANGED
|
@@ -24,14 +24,13 @@
|
|
|
24
24
|
const fs = require('fs');
|
|
25
25
|
const path = require('path');
|
|
26
26
|
const { exec } = require('child_process');
|
|
27
|
-
const https = require('https');
|
|
28
27
|
const { routeLocally } = require('./classifier');
|
|
29
28
|
const { distill } = require('./distiller');
|
|
30
29
|
const { scanSecrets } = require('./analyzer');
|
|
31
30
|
|
|
32
31
|
const {
|
|
33
32
|
MAX_OUTPUT,
|
|
34
|
-
readFileNative,
|
|
33
|
+
readFileNative, READ_SKIP_EXTENSIONS,
|
|
35
34
|
isReadHandleable, handleReadTool,
|
|
36
35
|
isGlobHandleable, handleGlobTool, globToRegex,
|
|
37
36
|
isGrepHandleable, handleGrepTool,
|
|
@@ -455,7 +454,7 @@ function nativeHandle(cmd) {
|
|
|
455
454
|
}
|
|
456
455
|
}
|
|
457
456
|
}
|
|
458
|
-
} catch {}
|
|
457
|
+
} catch { /* skip unreadable dir */ }
|
|
459
458
|
}
|
|
460
459
|
walk(abs);
|
|
461
460
|
return { output: results.join('\n') || '', exitCode: 0 };
|
|
@@ -487,7 +486,7 @@ function nativeHandle(cmd) {
|
|
|
487
486
|
if (!filePart) return null;
|
|
488
487
|
const abs = path.resolve(cwd, filePart);
|
|
489
488
|
let exists = false;
|
|
490
|
-
try { fs.statSync(abs); exists = true; } catch {}
|
|
489
|
+
try { fs.statSync(abs); exists = true; } catch { /* missing → exists stays false */ }
|
|
491
490
|
return { output: exists ? 'True' : 'False', exitCode: exists ? 0 : 1 };
|
|
492
491
|
}
|
|
493
492
|
|
|
@@ -689,11 +688,11 @@ function isInterceptable(block) {
|
|
|
689
688
|
if (block.name === 'TodoWrite') return isTodoHandleable(block.input, 'TodoWrite');
|
|
690
689
|
if (block.name === 'TodoRead') return isTodoHandleable(block.input, 'TodoRead');
|
|
691
690
|
if (block.name === 'PowerShell') {
|
|
692
|
-
const cmd = (block.input?.command
|
|
691
|
+
const cmd = (typeof block.input?.command === 'string' ? block.input.command : '').trim();
|
|
693
692
|
return cmd ? isPowerShellNativeHandleable(cmd) : false;
|
|
694
693
|
}
|
|
695
694
|
if (block.name !== 'Bash') return false;
|
|
696
|
-
const cmd = (block.input?.command
|
|
695
|
+
const cmd = (typeof block.input?.command === 'string' ? block.input.command : '').trim();
|
|
697
696
|
if (!cmd) return false;
|
|
698
697
|
if (isNativeHandleable(cmd)) return true;
|
|
699
698
|
if (SHELL_META.test(cmd)) return false;
|
|
@@ -738,7 +737,7 @@ function classifyBlock(block) {
|
|
|
738
737
|
}
|
|
739
738
|
|
|
740
739
|
if (block.name === 'PowerShell') {
|
|
741
|
-
const rawCmd = (block.input?.command
|
|
740
|
+
const rawCmd = (typeof block.input?.command === 'string' ? block.input.command : '').trim();
|
|
742
741
|
if (!rawCmd) return { handled: false, reason: FALLBACK_REASONS.BASH_EMPTY_CMD };
|
|
743
742
|
const expanded = expandPsEnvVars(rawCmd);
|
|
744
743
|
let normalized = expanded.trim();
|
|
@@ -760,7 +759,7 @@ function classifyBlock(block) {
|
|
|
760
759
|
}
|
|
761
760
|
|
|
762
761
|
// Bash
|
|
763
|
-
const cmd = (block.input?.command
|
|
762
|
+
const cmd = (typeof block.input?.command === 'string' ? block.input.command : '').trim();
|
|
764
763
|
if (!cmd) return { handled: false, reason: FALLBACK_REASONS.BASH_EMPTY_CMD };
|
|
765
764
|
if (isNativeHandleable(cmd)) return { handled: true, reason: 'ok' };
|
|
766
765
|
if (SHELL_META.test(cmd)) return { handled: false, reason: FALLBACK_REASONS.BASH_SHELL_META };
|
|
@@ -891,27 +890,6 @@ function buildFollowUpHeaders(authHeaders, payloadLength) {
|
|
|
891
890
|
return h;
|
|
892
891
|
}
|
|
893
892
|
|
|
894
|
-
function anthropicRequest(body, authHeaders) {
|
|
895
|
-
return new Promise((resolve, reject) => {
|
|
896
|
-
const payload = JSON.stringify({ ...body, stream: false });
|
|
897
|
-
const headers = buildFollowUpHeaders(authHeaders, Buffer.byteLength(payload));
|
|
898
|
-
|
|
899
|
-
const req = https.request(
|
|
900
|
-
{ hostname: 'api.anthropic.com', port: 443, path: '/v1/messages', method: 'POST', headers },
|
|
901
|
-
res => {
|
|
902
|
-
const chunks = [];
|
|
903
|
-
res.on('data', c => chunks.push(c));
|
|
904
|
-
res.on('end', () => {
|
|
905
|
-
try { resolve({ status: res.statusCode, body: JSON.parse(Buffer.concat(chunks).toString()) }); }
|
|
906
|
-
catch (e) { reject(e); }
|
|
907
|
-
});
|
|
908
|
-
}
|
|
909
|
-
);
|
|
910
|
-
req.on('error', reject);
|
|
911
|
-
req.end(payload);
|
|
912
|
-
});
|
|
913
|
-
}
|
|
914
|
-
|
|
915
893
|
// ── Main export ────────────────────────────────────────────────────────────────
|
|
916
894
|
|
|
917
895
|
/**
|
|
@@ -1185,6 +1163,8 @@ module.exports = {
|
|
|
1185
1163
|
isGrepHandleable,
|
|
1186
1164
|
isTodoHandleable,
|
|
1187
1165
|
nativeHandle,
|
|
1166
|
+
readFileNative,
|
|
1167
|
+
READ_SKIP_EXTENSIONS,
|
|
1188
1168
|
handleReadTool,
|
|
1189
1169
|
handleGlobTool,
|
|
1190
1170
|
globToRegex,
|
package/src/ledger.js
CHANGED
|
@@ -26,7 +26,7 @@ function readDayLog(dateStr) {
|
|
|
26
26
|
for (const raw of lines) {
|
|
27
27
|
const line = raw.trim();
|
|
28
28
|
if (!line) continue;
|
|
29
|
-
try { result.push(JSON.parse(line)); } catch {}
|
|
29
|
+
try { result.push(JSON.parse(line)); } catch { /* ignore */ }
|
|
30
30
|
}
|
|
31
31
|
return result;
|
|
32
32
|
}
|
|
@@ -119,7 +119,6 @@ function printEntry(e, idx) {
|
|
|
119
119
|
|
|
120
120
|
function printSummary(totals, scope, runId) {
|
|
121
121
|
const { requests, cloud_sent = 0, local_only = 0, blocked = 0, trimmed = 0,
|
|
122
|
-
budget_exceeded = 0,
|
|
123
122
|
input_tokens, output_tokens, cost,
|
|
124
123
|
cache_savings, lao_cost_saved, distill_cost_saved = 0,
|
|
125
124
|
distill_tokens_saved = 0, tools_local_count } = totals;
|
|
@@ -167,7 +166,7 @@ function runLedgerCli(args) {
|
|
|
167
166
|
if (args.includes('--summary')) showSummary = true;
|
|
168
167
|
|
|
169
168
|
let session = null;
|
|
170
|
-
try { session = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8')); } catch {}
|
|
169
|
+
try { session = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8')); } catch { /* ignore */ }
|
|
171
170
|
|
|
172
171
|
const todayEntries = readDayLog(todayStr());
|
|
173
172
|
const entries = scope === 'session'
|
package/src/mcp-experiment.js
CHANGED
|
@@ -33,7 +33,7 @@ function runStats() {
|
|
|
33
33
|
try {
|
|
34
34
|
mcpEntries = fs.readFileSync(MCP_LOG, 'utf8').trim().split('\n')
|
|
35
35
|
.filter(Boolean).map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
36
|
-
} catch {}
|
|
36
|
+
} catch { /* ignore */ }
|
|
37
37
|
|
|
38
38
|
// ── Built-in path: read today's session log, count intercepted Read/Glob/Grep ─
|
|
39
39
|
let builtinTools = [];
|
|
@@ -44,9 +44,9 @@ function runStats() {
|
|
|
44
44
|
const entry = JSON.parse(line);
|
|
45
45
|
const tools = (entry.tools || []).filter(t => ['Read', 'Glob', 'Grep'].includes(t.tool));
|
|
46
46
|
builtinTools.push(...tools);
|
|
47
|
-
} catch {}
|
|
47
|
+
} catch { /* ignore */ }
|
|
48
48
|
}
|
|
49
|
-
} catch {}
|
|
49
|
+
} catch { /* ignore */ }
|
|
50
50
|
|
|
51
51
|
const mcpTotal = mcpEntries.length;
|
|
52
52
|
const builtinTotal = builtinTools.length;
|
|
@@ -116,7 +116,7 @@ function runStats() {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
function runClear() {
|
|
119
|
-
try { fs.unlinkSync(MCP_LOG); console.log(col.g('✓ mcp-experiment.jsonl cleared')); } catch {}
|
|
119
|
+
try { fs.unlinkSync(MCP_LOG); console.log(col.g('✓ mcp-experiment.jsonl cleared')); } catch { /* ignore */ }
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
function runRaw() {
|
package/src/mcp-server.js
CHANGED
|
@@ -86,7 +86,7 @@ function logCall(entry) {
|
|
|
86
86
|
const dir = path.dirname(LOG_FILE);
|
|
87
87
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
88
88
|
fs.appendFileSync(LOG_FILE, JSON.stringify(entry) + '\n');
|
|
89
|
-
} catch {}
|
|
89
|
+
} catch { /* ignore */ }
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
// ── Tool definitions (lao-compatible schemas) ──────────────────────────────────
|
|
@@ -274,7 +274,7 @@ async function handleRequest(req) {
|
|
|
274
274
|
content: [{ type: 'text', text: 'audit-fatal: MCP server aborting' }],
|
|
275
275
|
isError: true,
|
|
276
276
|
});
|
|
277
|
-
} catch {}
|
|
277
|
+
} catch { /* ignore */ }
|
|
278
278
|
setTimeout(() => process.exit(1), 250);
|
|
279
279
|
return;
|
|
280
280
|
}
|
|
@@ -301,7 +301,7 @@ process.stdin.on('data', chunk => {
|
|
|
301
301
|
for (const line of lines) {
|
|
302
302
|
const trimmed = line.trim();
|
|
303
303
|
if (!trimmed) continue;
|
|
304
|
-
try { handleRequest(JSON.parse(trimmed)); } catch
|
|
304
|
+
try { handleRequest(JSON.parse(trimmed)); } catch { /* malformed JSON-RPC frame */ }
|
|
305
305
|
}
|
|
306
306
|
});
|
|
307
307
|
process.stdin.on('end', () => process.exit(0));
|
package/src/policy/doctor.js
CHANGED
|
@@ -56,10 +56,10 @@ function readRecentLogs(days, logsDir) {
|
|
|
56
56
|
for (const f of files) {
|
|
57
57
|
for (const line of fs.readFileSync(path.join(dir, f), 'utf8').split('\n')) {
|
|
58
58
|
if (!line.trim()) continue;
|
|
59
|
-
try { entries.push(JSON.parse(line)); } catch {}
|
|
59
|
+
try { entries.push(JSON.parse(line)); } catch { /* ignore */ }
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
-
} catch {}
|
|
62
|
+
} catch { /* ignore */ }
|
|
63
63
|
return entries;
|
|
64
64
|
}
|
|
65
65
|
|
package/src/policy/engine.js
CHANGED
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
const fs = require('fs');
|
|
17
17
|
const path = require('path');
|
|
18
18
|
const os = require('os');
|
|
19
|
-
const adapter = require('../adapters/claude-code');
|
|
20
19
|
const { PASS, LOCAL, BLOCK, TRANSFORM, TRANSFORM_CHAIN } = require('../core/decision');
|
|
21
20
|
const loader = require('./loader');
|
|
22
21
|
const builtIn = require('./built-in-classifiers');
|
package/src/policy/init.js
CHANGED
|
@@ -101,7 +101,7 @@ function runInitCli(args, opts = {}) {
|
|
|
101
101
|
|
|
102
102
|
// Guard: refuse to overwrite without --force
|
|
103
103
|
let exists = false;
|
|
104
|
-
try { fsMod.statSync(filePath); exists = true; } catch {}
|
|
104
|
+
try { fsMod.statSync(filePath); exists = true; } catch { /* ignore */ }
|
|
105
105
|
|
|
106
106
|
if (exists && !force) {
|
|
107
107
|
console.log(` File: ${filePath} ${col.y('(already exists)')}\n`);
|