@occasiolabs/occasio 0.8.4 → 0.8.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 +4 -3
- package/docs/ADAPTER-STAGE-2-MIGRATION.md +59 -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 +8 -3
- 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/boundary.js +1 -1
- package/src/classifier.js +1 -1
- package/src/cli/clear.js +4 -4
- package/src/cli/conversation.js +121 -0
- package/src/cli/help.js +62 -38
- package/src/cli/recap.js +367 -0
- package/src/cli/status.js +1 -1
- package/src/dashboard.js +2 -3
- package/src/demo/audit-demo.js +330 -0
- 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 +118 -30
- 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
|
@@ -56,10 +56,11 @@ const VERSION = (() => {
|
|
|
56
56
|
catch { return '0.0.0-unknown'; }
|
|
57
57
|
})();
|
|
58
58
|
const LOG_SCHEMA_VERSION = 2;
|
|
59
|
-
// Port
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
let PORT = parseInt(process.env.OCCASIO_PORT, 10) ||
|
|
59
|
+
// Port: defaults to 0 (auto-assigned by the OS) so multiple `occasio claude`
|
|
60
|
+
// sessions can run in parallel. Explicit overrides via OCCASIO_PORT env or
|
|
61
|
+
// --port flag still pin a fixed port (and surface EADDRINUSE if taken).
|
|
62
|
+
let PORT = parseInt(process.env.OCCASIO_PORT, 10) || 0;
|
|
63
|
+
let PORT_EXPLICIT = PORT !== 0;
|
|
63
64
|
const ANTHROPIC_REAL = 'api.anthropic.com';
|
|
64
65
|
const LOG_DIR = path.join(os.homedir(), '.occasio');
|
|
65
66
|
const SESSION_FILE = path.join(LOG_DIR, 'session.json');
|
|
@@ -75,8 +76,6 @@ function getBlockedFile() { return path.join(LOG_DIR, 'blocked', `${todayStr()}-
|
|
|
75
76
|
// MODEL_PRICES table + cost-arithmetic helpers. Re-exported here so the
|
|
76
77
|
// rest of index.js keeps its existing call sites unchanged.
|
|
77
78
|
const {
|
|
78
|
-
MODEL_PRICES,
|
|
79
|
-
getPrice,
|
|
80
79
|
calcCost,
|
|
81
80
|
calcCacheSavings,
|
|
82
81
|
calcCompoundingSavings,
|
|
@@ -102,7 +101,7 @@ function updateSession(e) {
|
|
|
102
101
|
lao_tokens_saved:0, lao_cost_saved:0, tools_local_count:0, tools_mcp_count:0,
|
|
103
102
|
tools_attempted:0,
|
|
104
103
|
};
|
|
105
|
-
try { s = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8')); } catch {}
|
|
104
|
+
try { s = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8')); } catch { /* ignore */ }
|
|
106
105
|
s.requests++;
|
|
107
106
|
s.input_tokens += e.input_tokens || 0;
|
|
108
107
|
s.output_tokens += e.output_tokens || 0;
|
|
@@ -133,6 +132,60 @@ const col = {
|
|
|
133
132
|
d: s => `\x1b[2m${s}\x1b[0m`, b: s => `\x1b[1m${s}\x1b[0m`,
|
|
134
133
|
};
|
|
135
134
|
|
|
135
|
+
// Print a compact recap banner just before claude starts — gives the user
|
|
136
|
+
// (and the agent, via on-screen context) a memory anchor for the previous
|
|
137
|
+
// session in this cwd. Silent if no prior data exists.
|
|
138
|
+
function printSessionRecapBanner() {
|
|
139
|
+
let conv = null;
|
|
140
|
+
try { conv = require('./cli/conversation').readLastConversation(process.cwd()); }
|
|
141
|
+
catch { /* module load issue → skip */ }
|
|
142
|
+
|
|
143
|
+
// Last run summary from the audit chain.
|
|
144
|
+
let runLine = null;
|
|
145
|
+
try {
|
|
146
|
+
const chainFile = path.join(LOG_DIR, 'pipeline-events.jsonl');
|
|
147
|
+
if (fs.existsSync(chainFile)) {
|
|
148
|
+
const text = fs.readFileSync(chainFile, 'utf8');
|
|
149
|
+
const rows = [];
|
|
150
|
+
for (const line of text.split('\n')) {
|
|
151
|
+
if (!line) continue;
|
|
152
|
+
try { rows.push(JSON.parse(line)); } catch { /* skip */ }
|
|
153
|
+
}
|
|
154
|
+
const byRun = new Map();
|
|
155
|
+
for (const r of rows) {
|
|
156
|
+
const id = r.run_id || 'legacy';
|
|
157
|
+
if (!byRun.has(id)) byRun.set(id, []);
|
|
158
|
+
byRun.get(id).push(r);
|
|
159
|
+
}
|
|
160
|
+
const newest = [...byRun.entries()].map(([id, rs]) => ({
|
|
161
|
+
id, rs, end: rs[rs.length - 1]?.ts || '',
|
|
162
|
+
})).sort((a, b) => a.end < b.end ? 1 : -1)[0];
|
|
163
|
+
if (newest) {
|
|
164
|
+
const tc = newest.rs.filter(r => r.kind === 'tool_call').length;
|
|
165
|
+
const blocked = newest.rs.filter(r => r.action === 'BLOCK').length;
|
|
166
|
+
const date = newest.end ? new Date(newest.end).toISOString().slice(0, 16).replace('T', ' ') : 'unknown';
|
|
167
|
+
runLine = `${tc} tool calls, ${blocked} blocked · ${date}`;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} catch { /* skip */ }
|
|
171
|
+
|
|
172
|
+
if (!conv && !runLine) return;
|
|
173
|
+
|
|
174
|
+
const oneLine = (s, max = 160) => {
|
|
175
|
+
const flat = String(s).replace(/\s+/g, ' ').trim();
|
|
176
|
+
return flat.length > max ? flat.slice(0, max - 1) + '…' : flat;
|
|
177
|
+
};
|
|
178
|
+
process.stderr.write('\n' + col.b('── previous session in this directory ──') + '\n');
|
|
179
|
+
if (runLine) process.stderr.write(' ' + col.d(runLine) + '\n');
|
|
180
|
+
if (conv && conv.lastUser) {
|
|
181
|
+
process.stderr.write(' ' + col.y('You: ') + oneLine(conv.lastUser) + '\n');
|
|
182
|
+
}
|
|
183
|
+
if (conv && conv.lastAssistant) {
|
|
184
|
+
process.stderr.write(' ' + col.g('Agent: ') + oneLine(conv.lastAssistant) + '\n');
|
|
185
|
+
}
|
|
186
|
+
process.stderr.write(col.d(' (enabled via --recap; suppress next time by omitting it)') + '\n\n');
|
|
187
|
+
}
|
|
188
|
+
|
|
136
189
|
// ── Pre-send manifest ─────────────────────────────────────────────────────────
|
|
137
190
|
|
|
138
191
|
function fmtTok(n) {
|
|
@@ -286,6 +339,10 @@ if (cmd === 'replay') {
|
|
|
286
339
|
process.exit(0);
|
|
287
340
|
}
|
|
288
341
|
|
|
342
|
+
if (cmd === 'recap') {
|
|
343
|
+
process.exit(require('./cli/recap').run(args.slice(1)));
|
|
344
|
+
}
|
|
345
|
+
|
|
289
346
|
if (cmd === 'distill') {
|
|
290
347
|
runDistillCli(args.slice(1));
|
|
291
348
|
process.exit(0);
|
|
@@ -406,6 +463,13 @@ if (cmd === 'demo' && (args[1] === 'anomalies' || args[1] === 'anomaly')) {
|
|
|
406
463
|
process.exit(runAnomaliesDemoCli(args.slice(2)));
|
|
407
464
|
}
|
|
408
465
|
|
|
466
|
+
if (cmd === 'demo' && (args[1] === 'audit' || args[1] === 'auditor')) {
|
|
467
|
+
const { runAuditDemoCli } = require('./demo/audit-demo');
|
|
468
|
+
runAuditDemoCli(args.slice(2)).then(code => process.exit(code))
|
|
469
|
+
.catch(e => { process.stderr.write(`[demo audit] ${e.message}\n`); process.exit(1); });
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
409
473
|
if (cmd === 'demo') {
|
|
410
474
|
const { scanSecrets } = require('../src/analyzer');
|
|
411
475
|
// Realistic-looking but synthetic credentials. Each hits exactly one pattern.
|
|
@@ -539,16 +603,22 @@ if (cmd === 'doctor' || cmd === 'check') {
|
|
|
539
603
|
ok('log dir', LOG_DIR);
|
|
540
604
|
} catch (e) { bad('log dir', e.message); }
|
|
541
605
|
|
|
542
|
-
// 4. Port availability (async)
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
606
|
+
// 4. Port availability (async) — only probe when an explicit port is pinned.
|
|
607
|
+
// With auto-assignment (default), the OS picks a free port at listen-time
|
|
608
|
+
// so there's nothing meaningful to probe in advance.
|
|
609
|
+
if (PORT_EXPLICIT) {
|
|
610
|
+
await new Promise(resolve => {
|
|
611
|
+
const srv = net.createServer();
|
|
612
|
+
srv.once('error', () => {
|
|
613
|
+
bad(`port ${PORT}`, `already in use — kill it: netstat -ano | findstr :${PORT}`);
|
|
614
|
+
resolve();
|
|
615
|
+
});
|
|
616
|
+
srv.once('listening', () => { srv.close(); ok(`port ${PORT}`, 'available'); resolve(); });
|
|
617
|
+
srv.listen(PORT, '127.0.0.1');
|
|
548
618
|
});
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
}
|
|
619
|
+
} else {
|
|
620
|
+
ok('port', 'auto-assigned at runtime');
|
|
621
|
+
}
|
|
552
622
|
|
|
553
623
|
// 5. Python + LAO scorer script
|
|
554
624
|
const laoPyPath = path.join(__dirname, 'lao_prep.py');
|
|
@@ -562,7 +632,7 @@ if (cmd === 'doctor' || cmd === 'check') {
|
|
|
562
632
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
563
633
|
}).toString().trim();
|
|
564
634
|
ok('Python (LAO)', out); pyFound = true;
|
|
565
|
-
} catch {}
|
|
635
|
+
} catch { /* ignore */ }
|
|
566
636
|
}
|
|
567
637
|
if (!pyFound) bad('Python (LAO)', 'not found — context trimming disabled');
|
|
568
638
|
if (laoPyExists) ok('LAO scorer', laoPyPath);
|
|
@@ -634,8 +704,15 @@ if (prIdx >= 0) {
|
|
|
634
704
|
const di = claudeArgs.indexOf('--dashboard');
|
|
635
705
|
const useDashboard = di >= 0;
|
|
636
706
|
if (di >= 0) claudeArgs.splice(di, 1);
|
|
707
|
+
const recapIdx = claudeArgs.indexOf('--recap');
|
|
708
|
+
const showRecap = recapIdx >= 0;
|
|
709
|
+
if (recapIdx >= 0) claudeArgs.splice(recapIdx, 1);
|
|
637
710
|
const pi = claudeArgs.indexOf('--port');
|
|
638
|
-
if (pi >= 0) {
|
|
711
|
+
if (pi >= 0) {
|
|
712
|
+
const parsed = parseInt(claudeArgs[pi+1], 10);
|
|
713
|
+
if (parsed > 0) { PORT = parsed; PORT_EXPLICIT = true; }
|
|
714
|
+
claudeArgs.splice(pi, 2);
|
|
715
|
+
}
|
|
639
716
|
|
|
640
717
|
// Budget flag: --budget <N> sets a session dollar limit.
|
|
641
718
|
const bgtIdx = claudeArgs.indexOf('--budget');
|
|
@@ -688,7 +765,7 @@ const sessionAuditor = _createAuditor(process.env.OCCASIO_AUDIT_FILE || undefine
|
|
|
688
765
|
process.stderr.write(`\n${col.r('[occasio][audit-fatal]')} policy_loaded write failed: ${status.error?.message}\n`);
|
|
689
766
|
process.stderr.write(`${col.r('[occasio][audit-fatal] dropped row:')} ${dropped}\n`);
|
|
690
767
|
process.stderr.write(`${col.r('[occasio][audit-fatal] proxy aborting; supervisor will restart.')}\n`);
|
|
691
|
-
try { server && server.close && server.close(); } catch {}
|
|
768
|
+
try { server && server.close && server.close(); } catch { /* ignore */ }
|
|
692
769
|
setTimeout(() => process.exit(1), 250);
|
|
693
770
|
}
|
|
694
771
|
});
|
|
@@ -727,7 +804,7 @@ if (budget !== null) {
|
|
|
727
804
|
const { execSync: _ex } = require('child_process');
|
|
728
805
|
let _pyOk = false;
|
|
729
806
|
for (const _cmd of ['python', 'python3']) {
|
|
730
|
-
try { _ex(`${_cmd} --version`, { shell: process.platform === 'win32', timeout: 3000, stdio: 'pipe' }); _pyOk = true; break; } catch {}
|
|
807
|
+
try { _ex(`${_cmd} --version`, { shell: process.platform === 'win32', timeout: 3000, stdio: 'pipe' }); _pyOk = true; break; } catch { /* ignore */ }
|
|
731
808
|
}
|
|
732
809
|
if (!_pyOk) process.stderr.write(col.y(` ⚠ LAO disabled — Python not found (context trimming inactive)\n`));
|
|
733
810
|
}
|
|
@@ -775,7 +852,7 @@ const server = http.createServer((req, res) => {
|
|
|
775
852
|
try {
|
|
776
853
|
const rules = JSON.parse(fs.readFileSync(rp, 'utf8'));
|
|
777
854
|
blocked = files.filter(f => (rules.block || []).some(p => f.includes(p.replace(/\*\*/g, '').replace(/\*/g, ''))));
|
|
778
|
-
} catch {}
|
|
855
|
+
} catch { /* ignore */ }
|
|
779
856
|
}
|
|
780
857
|
|
|
781
858
|
const shouldBlock = (mode === 'block_secrets' && secrets.length) || (mode === 'block_rules' && blocked.length);
|
|
@@ -808,7 +885,7 @@ const server = http.createServer((req, res) => {
|
|
|
808
885
|
res.end(JSON.stringify({ error: { type: 'blocked', reason: secrets.length ? secrets[0].label : 'rule', by: 'Occasio' } }));
|
|
809
886
|
return;
|
|
810
887
|
}
|
|
811
|
-
} catch {}
|
|
888
|
+
} catch { /* ignore */ }
|
|
812
889
|
}
|
|
813
890
|
|
|
814
891
|
// ── Budget enforcement (Stage 2: policy-driven BLOCK) ─────────────────────
|
|
@@ -842,7 +919,7 @@ const server = http.createServer((req, res) => {
|
|
|
842
919
|
const s = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8'));
|
|
843
920
|
s.budget_exceeded_count = (s.budget_exceeded_count || 0) + 1;
|
|
844
921
|
fs.writeFileSync(SESSION_FILE, JSON.stringify(s));
|
|
845
|
-
} catch {}
|
|
922
|
+
} catch { /* ignore */ }
|
|
846
923
|
const synth = decision.syntheticResponse;
|
|
847
924
|
res.writeHead(synth.status, { 'Content-Type': 'application/json' });
|
|
848
925
|
res.end(JSON.stringify(synth.body));
|
|
@@ -870,7 +947,7 @@ const server = http.createServer((req, res) => {
|
|
|
870
947
|
}
|
|
871
948
|
}
|
|
872
949
|
}
|
|
873
|
-
} catch {}
|
|
950
|
+
} catch { /* ignore */ }
|
|
874
951
|
}
|
|
875
952
|
// ──────────────────────────────────────────────────────────────────────────
|
|
876
953
|
|
|
@@ -1057,7 +1134,7 @@ const server = http.createServer((req, res) => {
|
|
|
1057
1134
|
}
|
|
1058
1135
|
forwardBody = Buffer.from(JSON.stringify(b));
|
|
1059
1136
|
outboundMessageCount = b.messages?.length ?? outboundMessageCount;
|
|
1060
|
-
} catch {}
|
|
1137
|
+
} catch { /* ignore */ }
|
|
1061
1138
|
}
|
|
1062
1139
|
if (laoDropped.length > 0) {
|
|
1063
1140
|
const ts0 = new Date().toTimeString().slice(0, 8);
|
|
@@ -1183,7 +1260,7 @@ const server = http.createServer((req, res) => {
|
|
|
1183
1260
|
process.stderr.write(`\n${col.r('[occasio][audit-fatal]')} ${e.message}\n`);
|
|
1184
1261
|
process.stderr.write(`${col.r('[occasio][audit-fatal] dropped row:')} ${dropped}\n`);
|
|
1185
1262
|
process.stderr.write(`${col.r('[occasio][audit-fatal] proxy aborting; supervisor will restart.')}\n`);
|
|
1186
|
-
try { server && server.close && server.close(); } catch {}
|
|
1263
|
+
try { server && server.close && server.close(); } catch { /* ignore */ }
|
|
1187
1264
|
setTimeout(() => process.exit(1), 250);
|
|
1188
1265
|
return;
|
|
1189
1266
|
}
|
|
@@ -1212,10 +1289,10 @@ const server = http.createServer((req, res) => {
|
|
|
1212
1289
|
cacheRead = d.usage.cache_read_input_tokens || cacheRead;
|
|
1213
1290
|
}
|
|
1214
1291
|
if (d.type === 'message_delta' && d.usage) out = d.usage.output_tokens || out;
|
|
1215
|
-
} catch {}
|
|
1292
|
+
} catch { /* ignore */ }
|
|
1216
1293
|
}
|
|
1217
1294
|
}
|
|
1218
|
-
} catch {}
|
|
1295
|
+
} catch { /* ignore */ }
|
|
1219
1296
|
|
|
1220
1297
|
// When the interceptor ran, Anthropic was billed for N calls:
|
|
1221
1298
|
// call #1 → initial tool_use round (toolCallUsage)
|
|
@@ -1355,7 +1432,7 @@ const server = http.createServer((req, res) => {
|
|
|
1355
1432
|
|
|
1356
1433
|
server.on('error', e => {
|
|
1357
1434
|
if (e.code === 'EADDRINUSE') {
|
|
1358
|
-
process.stderr.write(col.r(`\n❌ Port ${PORT} already in use. Kill the old process first:\n`));
|
|
1435
|
+
process.stderr.write(col.r(`\n❌ Port ${PORT} already in use. Kill the old process first or omit --port / OCCASIO_PORT to let the OS auto-assign:\n`));
|
|
1359
1436
|
process.stderr.write(col.d(` netstat -ano | findstr :${PORT} → then: taskkill /PID <pid> /F\n\n`));
|
|
1360
1437
|
} else {
|
|
1361
1438
|
process.stderr.write(col.r(`\n❌ ${e.message}\n`));
|
|
@@ -1364,6 +1441,17 @@ server.on('error', e => {
|
|
|
1364
1441
|
});
|
|
1365
1442
|
|
|
1366
1443
|
server.listen(PORT, '127.0.0.1', () => {
|
|
1444
|
+
PORT = server.address().port;
|
|
1445
|
+
process.stderr.write(col.d(`occasio proxy listening on 127.0.0.1:${PORT}\n`));
|
|
1446
|
+
|
|
1447
|
+
// Best-effort: print a 3-line recap of the previous session so the user
|
|
1448
|
+
// (and via screen-context, the agent itself) has a memory anchor before
|
|
1449
|
+
// typing the next prompt. Silent if no prior session in this cwd.
|
|
1450
|
+
// Disable with --no-recap.
|
|
1451
|
+
if (showRecap) {
|
|
1452
|
+
try { printSessionRecapBanner(); } catch { /* never block startup */ }
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1367
1455
|
const env = { ...process.env, ANTHROPIC_BASE_URL: `http://localhost:${PORT}` };
|
|
1368
1456
|
// On Windows, npm installs binaries as .cmd wrappers (claude.cmd).
|
|
1369
1457
|
// spawn() without shell:true calls CreateProcess directly, which won't
|
|
@@ -1450,7 +1538,7 @@ server.listen(PORT, '127.0.0.1', () => {
|
|
|
1450
1538
|
if (parts.length) process.stderr.write(col.d(` Breakdown: ${parts.join(' + ')}\n`));
|
|
1451
1539
|
}
|
|
1452
1540
|
process.stderr.write('────────────────────────────────────────\n\n');
|
|
1453
|
-
} catch {}
|
|
1541
|
+
} catch { /* ignore */ }
|
|
1454
1542
|
process.exit(code || 0);
|
|
1455
1543
|
});
|
|
1456
1544
|
|
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`);
|
package/src/policy/loader.js
CHANGED
|
@@ -264,7 +264,7 @@ function normalize(parsed) {
|
|
|
264
264
|
try {
|
|
265
265
|
const regex = new RegExp(rawPattern);
|
|
266
266
|
patterns.push(Object.freeze({ label, regex }));
|
|
267
|
-
} catch
|
|
267
|
+
} catch {
|
|
268
268
|
process.stderr.write(`[Occasio] policy.yml: deny_patterns.${label} — invalid RegExp "${rawPattern}", entry skipped\n`);
|
|
269
269
|
}
|
|
270
270
|
}
|
|
@@ -276,7 +276,7 @@ function normalize(parsed) {
|
|
|
276
276
|
// module load (loader.js is imported by other code paths that don't
|
|
277
277
|
// need the registry).
|
|
278
278
|
let toolNames;
|
|
279
|
-
try { toolNames = require('../core/tool-names'); } catch {}
|
|
279
|
+
try { toolNames = require('../core/tool-names'); } catch { /* ignore */ }
|
|
280
280
|
const tools = {};
|
|
281
281
|
for (const name of Object.keys(parsed.tools)) {
|
|
282
282
|
const entry = normalizeToolEntry(parsed.tools[name]);
|
|
@@ -351,7 +351,7 @@ function _firePolicyChange(filePath, policy, hash, fileWasPresent) {
|
|
|
351
351
|
});
|
|
352
352
|
} catch (e) {
|
|
353
353
|
// Listener crash must not break the proxy — surface to stderr only.
|
|
354
|
-
try { process.stderr.write(`[occasio] policy-change listener threw: ${e.message}\n`); } catch {}
|
|
354
|
+
try { process.stderr.write(`[occasio] policy-change listener threw: ${e.message}\n`); } catch { /* ignore */ }
|
|
355
355
|
}
|
|
356
356
|
}
|
|
357
357
|
}
|
package/src/policy/show.js
CHANGED
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
const fs = require('fs');
|
|
15
|
-
const path = require('path');
|
|
16
15
|
|
|
17
16
|
// Transforms currently implemented in the dispatcher.
|
|
18
17
|
const KNOWN_TRANSFORMS = new Set(['redact-secrets', 'distill-output']);
|
|
@@ -88,7 +87,7 @@ function runPolicyCli(args, opts = {}) {
|
|
|
88
87
|
const text = fs.readFileSync(filePath, 'utf8');
|
|
89
88
|
fileExists = true;
|
|
90
89
|
userParsed = loader.parse(text);
|
|
91
|
-
} catch {}
|
|
90
|
+
} catch { /* ignore */ }
|
|
92
91
|
|
|
93
92
|
const active = loader.load();
|
|
94
93
|
const defaults = loader.DEFAULT_POLICY;
|
package/src/preflight/cli.js
CHANGED
package/src/preflight/miner.js
CHANGED
|
@@ -122,10 +122,10 @@ function readRecentEntries(days, logsDir) {
|
|
|
122
122
|
for (const line of raw.split('\n')) {
|
|
123
123
|
const trimmed = line.trim();
|
|
124
124
|
if (!trimmed) continue;
|
|
125
|
-
try { entries.push(JSON.parse(trimmed)); } catch {}
|
|
125
|
+
try { entries.push(JSON.parse(trimmed)); } catch { /* ignore */ }
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
|
-
} catch {}
|
|
128
|
+
} catch { /* ignore */ }
|
|
129
129
|
return entries;
|
|
130
130
|
}
|
|
131
131
|
|
|
@@ -231,14 +231,11 @@ function mine(opts) {
|
|
|
231
231
|
|
|
232
232
|
// project_root → { projectRoot, sessions: Set, toolCounts: Map, legacySessions: number }
|
|
233
233
|
const projects = new Map();
|
|
234
|
-
let totalLegacy = 0;
|
|
235
234
|
|
|
236
235
|
for (const [, runEntries] of runsMap) {
|
|
237
236
|
const cwd = runCwd(runEntries);
|
|
238
237
|
if (!cwd) {
|
|
239
|
-
// Pre-schema run
|
|
240
|
-
// but we can't know which project it belongs to — just count globally.
|
|
241
|
-
totalLegacy++;
|
|
238
|
+
// Pre-schema run with no cwd — can't attribute to a project, skip.
|
|
242
239
|
continue;
|
|
243
240
|
}
|
|
244
241
|
|
package/src/redteam.js
CHANGED
|
@@ -33,7 +33,6 @@
|
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
35
|
const fs = require('fs');
|
|
36
|
-
const path = require('path');
|
|
37
36
|
const harness = require('./harness');
|
|
38
37
|
|
|
39
38
|
const C = (() => {
|
|
@@ -411,7 +410,7 @@ async function runRedteamCli(args = []) {
|
|
|
411
410
|
return result;
|
|
412
411
|
} finally {
|
|
413
412
|
if (!keepScratch && !process.env.OCC_REDTEAM_KEEP) {
|
|
414
|
-
try { fs.rmSync(ctx.workspace, { recursive: true, force: true }); } catch {}
|
|
413
|
+
try { fs.rmSync(ctx.workspace, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
415
414
|
}
|
|
416
415
|
}
|
|
417
416
|
}
|
package/src/replay.js
CHANGED
package/src/report/index.js
CHANGED
|
@@ -25,10 +25,6 @@ const LOGS_DIR = path.join(LOG_DIR, 'logs');
|
|
|
25
25
|
|
|
26
26
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
27
27
|
|
|
28
|
-
function todayStr(d) {
|
|
29
|
-
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
28
|
function cutoffMs(days) {
|
|
33
29
|
const d = new Date();
|
|
34
30
|
d.setDate(d.getDate() - days);
|