@occasiolabs/occasio 0.8.4 → 0.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +7 -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/help.js +58 -37
- package/src/cli/status.js +1 -1
- 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 +13 -15
- 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
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Native handler for the Read tool.
|
|
5
|
+
*
|
|
6
|
+
* Pure filesystem function: takes a file_path (+ optional offset/limit) and
|
|
7
|
+
* returns cat -n formatted output. No dependency on the interceptor pipeline,
|
|
8
|
+
* Anthropic API, or shell execution. Safe to import in any process context.
|
|
9
|
+
*
|
|
10
|
+
* Extracted from src/runtime.js as Stage-2 of the executor migration
|
|
11
|
+
* (see docs/ADAPTER-STAGE-2-MIGRATION.md). src/runtime.js re-exports
|
|
12
|
+
* these so existing consumers (src/interceptor.js, tests) keep working
|
|
13
|
+
* unchanged.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
// ── Shared constants ───────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const MAX_OUTPUT = 512 * 1024; // 512 KB — same cap as exec maxBuffer
|
|
22
|
+
|
|
23
|
+
// File extensions the native Read handler cannot serve correctly.
|
|
24
|
+
// PDFs and images need structured rendering (base64, page extraction) that we
|
|
25
|
+
// cannot replicate; Jupyter notebooks need cell-by-cell parsing. All others
|
|
26
|
+
// are treated as UTF-8 text and handled natively.
|
|
27
|
+
const READ_SKIP_EXTENSIONS = new Set([
|
|
28
|
+
'.pdf', '.ipynb',
|
|
29
|
+
'.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.ico',
|
|
30
|
+
'.zip', '.gz', '.tar', '.bz2', '.xz', '.7z', '.rar',
|
|
31
|
+
'.exe', '.dll', '.so', '.dylib',
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
// ── Shared helper ──────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
function readFileNative(absPath) {
|
|
37
|
+
const buf = fs.readFileSync(absPath);
|
|
38
|
+
if (buf.length > MAX_OUTPUT) {
|
|
39
|
+
return buf.slice(0, MAX_OUTPUT).toString('utf8') + '\n[truncated — file too large]';
|
|
40
|
+
}
|
|
41
|
+
return buf.toString('utf8');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Read tool support ──────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Returns true when this Read input can be served natively.
|
|
48
|
+
* Falls back for PDFs/images (need structured rendering), Jupyter notebooks,
|
|
49
|
+
* malformed input, or the `pages` parameter (implies PDF range extraction).
|
|
50
|
+
*/
|
|
51
|
+
// UNC / network paths cause blocking SMB resolution on Windows (10+ s).
|
|
52
|
+
// Reject so the agent cannot stall the proxy via `\\server\share\file` or
|
|
53
|
+
// the // equivalent. Local filesystem only — a deliberate restriction.
|
|
54
|
+
const UNC_PREFIX_RE = /^[/\\]{2}/;
|
|
55
|
+
|
|
56
|
+
function isReadHandleable(input) {
|
|
57
|
+
if (!input || typeof input !== 'object') return false;
|
|
58
|
+
const fp = input.file_path;
|
|
59
|
+
if (!fp || typeof fp !== 'string' || !fp.trim()) return false;
|
|
60
|
+
if (UNC_PREFIX_RE.test(fp)) return false;
|
|
61
|
+
if (input.pages != null) return false;
|
|
62
|
+
const ext = path.extname(fp).toLowerCase();
|
|
63
|
+
return !READ_SKIP_EXTENSIONS.has(ext);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Read a file natively and return content formatted like `cat -n` (1-based line
|
|
68
|
+
* numbers), honouring the optional offset (0-based line index) and limit fields
|
|
69
|
+
* that the Claude Code Read tool sends for partial reads.
|
|
70
|
+
*/
|
|
71
|
+
function handleReadTool(input) {
|
|
72
|
+
const fp = (typeof input?.file_path === 'string' ? input.file_path : '').trim();
|
|
73
|
+
if (!fp) return { output: '(no file_path provided)', exitCode: 1 };
|
|
74
|
+
|
|
75
|
+
const abs = path.resolve(process.cwd(), fp);
|
|
76
|
+
try {
|
|
77
|
+
const content = readFileNative(abs); // already caps at MAX_OUTPUT
|
|
78
|
+
const lines = content.split('\n');
|
|
79
|
+
const offset = (typeof input.offset === 'number' && input.offset >= 0) ? input.offset : 0;
|
|
80
|
+
const limit = (typeof input.limit === 'number' && input.limit > 0) ? input.limit : lines.length;
|
|
81
|
+
const slice = lines.slice(offset, offset + limit);
|
|
82
|
+
// Line numbers reflect position in the file (not the slice), matching cat -n.
|
|
83
|
+
const formatted = slice.map((l, i) => `${String(offset + i + 1).padStart(6)}\t${l}`).join('\n');
|
|
84
|
+
return { output: formatted, exitCode: 0 };
|
|
85
|
+
} catch (e) {
|
|
86
|
+
const msg = e.code === 'ENOENT'
|
|
87
|
+
? `${fp}: No such file or directory`
|
|
88
|
+
: `${fp}: ${e.message}`;
|
|
89
|
+
return { output: `Read: ${msg}`, exitCode: 1 };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = {
|
|
94
|
+
MAX_OUTPUT,
|
|
95
|
+
READ_SKIP_EXTENSIONS,
|
|
96
|
+
readFileNative,
|
|
97
|
+
isReadHandleable,
|
|
98
|
+
handleReadTool,
|
|
99
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Native handlers for the TodoWrite / TodoRead tools.
|
|
5
|
+
*
|
|
6
|
+
* Pure functions over a caller-owned mutable `todoStore` array. No I/O,
|
|
7
|
+
* no globals — the session owns the store; this module only mutates it.
|
|
8
|
+
*
|
|
9
|
+
* Extracted from src/runtime.js as Stage-2 of the executor migration
|
|
10
|
+
* (see docs/ADAPTER-STAGE-2-MIGRATION.md). src/runtime.js re-exports
|
|
11
|
+
* these so existing consumers (src/interceptor.js, tests) keep working
|
|
12
|
+
* unchanged.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Returns true when this TodoWrite/TodoRead call can be served natively.
|
|
17
|
+
* TodoRead: always handleable — no required inputs.
|
|
18
|
+
* TodoWrite: requires input.todos to be an array.
|
|
19
|
+
*/
|
|
20
|
+
function isTodoHandleable(input, toolName) {
|
|
21
|
+
if (toolName === 'TodoRead') return true;
|
|
22
|
+
if (toolName === 'TodoWrite') {
|
|
23
|
+
if (!input || typeof input !== 'object') return false;
|
|
24
|
+
return Array.isArray(input.todos);
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Handle a TodoWrite call: replace the session todo list with input.todos.
|
|
31
|
+
* Returns { output: '', exitCode: 0, taskCount: N } on success.
|
|
32
|
+
* Claude Code expects an empty-string response from write tools.
|
|
33
|
+
*/
|
|
34
|
+
function handleTodoWriteTool(input, todoStore) {
|
|
35
|
+
const todos = input?.todos;
|
|
36
|
+
if (!Array.isArray(todos)) {
|
|
37
|
+
return { output: 'TodoWrite: todos must be an array', exitCode: 1, taskCount: 0 };
|
|
38
|
+
}
|
|
39
|
+
todoStore.splice(0, todoStore.length, ...todos);
|
|
40
|
+
return { output: '', exitCode: 0, taskCount: todos.length };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Handle a TodoRead call: return the session todo list as a JSON string.
|
|
45
|
+
* Returns { output: string, exitCode: 0, taskCount: N }.
|
|
46
|
+
*/
|
|
47
|
+
function handleTodoReadTool(todoStore) {
|
|
48
|
+
const output = JSON.stringify(todoStore, null, 2);
|
|
49
|
+
return { output, exitCode: 0, taskCount: todoStore.length };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = {
|
|
53
|
+
isTodoHandleable,
|
|
54
|
+
handleTodoWriteTool,
|
|
55
|
+
handleTodoReadTool,
|
|
56
|
+
};
|
package/src/harness.js
CHANGED
|
@@ -128,8 +128,6 @@ const SCENARIOS = {
|
|
|
128
128
|
const f = ctx.secretPath;
|
|
129
129
|
// Build several path variants pointing at the same real file
|
|
130
130
|
const ws = ctx.workspace;
|
|
131
|
-
const drive = f.match(/^[A-Z]:/i)?.[0] || '';
|
|
132
|
-
const tail = f.slice(drive.length);
|
|
133
131
|
const v = [
|
|
134
132
|
f, // canonical
|
|
135
133
|
f.replace(/\\/g, '/'), // forward slashes
|
|
@@ -179,7 +177,7 @@ const SCENARIOS = {
|
|
|
179
177
|
const type = process.platform === 'win32' ? 'junction' : 'dir';
|
|
180
178
|
fs.symlinkSync(ctx.denyDir, aliasDir, type);
|
|
181
179
|
ctx.aliasPath = path.join(aliasDir, 'plans.md');
|
|
182
|
-
} catch
|
|
180
|
+
} catch {
|
|
183
181
|
// Symlink creation can fail (e.g. tmpfs that disallows symlinks).
|
|
184
182
|
// Fall back to a plain path so the scenario still exercises the
|
|
185
183
|
// direct case, with a clear note in the prompt.
|
|
@@ -489,7 +487,7 @@ function prepareWorkspace(scenarioName, opts = {}) {
|
|
|
489
487
|
}
|
|
490
488
|
|
|
491
489
|
function cleanupWorkspace(ctx) {
|
|
492
|
-
try { fs.rmSync(ctx.workspace, { recursive: true, force: true }); } catch {}
|
|
490
|
+
try { fs.rmSync(ctx.workspace, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
493
491
|
}
|
|
494
492
|
|
|
495
493
|
// ── Subprocess spawning ─────────────────────────────────────────────────────
|
|
@@ -564,8 +562,8 @@ function runScenarioChild(scenarioName, ctx, opts = {}) {
|
|
|
564
562
|
let stdout = '', stderr = '', timedOut = false;
|
|
565
563
|
const t = setTimeout(() => {
|
|
566
564
|
timedOut = true;
|
|
567
|
-
try { child.kill('SIGTERM'); } catch {}
|
|
568
|
-
setTimeout(() => { try { child.kill('SIGKILL'); } catch {} }, 5_000);
|
|
565
|
+
try { child.kill('SIGTERM'); } catch { /* ignore */ }
|
|
566
|
+
setTimeout(() => { try { child.kill('SIGKILL'); } catch { /* ignore */ } }, 5_000);
|
|
569
567
|
}, timeoutMs);
|
|
570
568
|
|
|
571
569
|
if (child.stdout) child.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
@@ -607,8 +605,8 @@ function runMcpScenario(scenarioName, ctx, opts = {}) {
|
|
|
607
605
|
let stdout = '', stderr = '', timedOut = false;
|
|
608
606
|
const t = setTimeout(() => {
|
|
609
607
|
timedOut = true;
|
|
610
|
-
try { child.kill('SIGTERM'); } catch {}
|
|
611
|
-
setTimeout(() => { try { child.kill('SIGKILL'); } catch {} }, 2_000);
|
|
608
|
+
try { child.kill('SIGTERM'); } catch { /* ignore */ }
|
|
609
|
+
setTimeout(() => { try { child.kill('SIGKILL'); } catch { /* ignore */ } }, 2_000);
|
|
612
610
|
}, timeoutMs);
|
|
613
611
|
|
|
614
612
|
if (child.stdout) child.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
@@ -637,8 +635,8 @@ function runMcpScenario(scenarioName, ctx, opts = {}) {
|
|
|
637
635
|
child.stdin.write(JSON.stringify(init) + '\n');
|
|
638
636
|
child.stdin.write(JSON.stringify(callRead) + '\n');
|
|
639
637
|
// Give the server a moment to process, then close stdin so it exits.
|
|
640
|
-
setTimeout(() => { try { child.stdin.end(); } catch {} }, 2_000);
|
|
641
|
-
} catch
|
|
638
|
+
setTimeout(() => { try { child.stdin.end(); } catch { /* ignore */ } }, 2_000);
|
|
639
|
+
} catch {
|
|
642
640
|
// best effort
|
|
643
641
|
}
|
|
644
642
|
});
|
package/src/index.js
CHANGED
|
@@ -75,8 +75,6 @@ function getBlockedFile() { return path.join(LOG_DIR, 'blocked', `${todayStr()}-
|
|
|
75
75
|
// MODEL_PRICES table + cost-arithmetic helpers. Re-exported here so the
|
|
76
76
|
// rest of index.js keeps its existing call sites unchanged.
|
|
77
77
|
const {
|
|
78
|
-
MODEL_PRICES,
|
|
79
|
-
getPrice,
|
|
80
78
|
calcCost,
|
|
81
79
|
calcCacheSavings,
|
|
82
80
|
calcCompoundingSavings,
|
|
@@ -102,7 +100,7 @@ function updateSession(e) {
|
|
|
102
100
|
lao_tokens_saved:0, lao_cost_saved:0, tools_local_count:0, tools_mcp_count:0,
|
|
103
101
|
tools_attempted:0,
|
|
104
102
|
};
|
|
105
|
-
try { s = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8')); } catch {}
|
|
103
|
+
try { s = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8')); } catch { /* ignore */ }
|
|
106
104
|
s.requests++;
|
|
107
105
|
s.input_tokens += e.input_tokens || 0;
|
|
108
106
|
s.output_tokens += e.output_tokens || 0;
|
|
@@ -562,7 +560,7 @@ if (cmd === 'doctor' || cmd === 'check') {
|
|
|
562
560
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
563
561
|
}).toString().trim();
|
|
564
562
|
ok('Python (LAO)', out); pyFound = true;
|
|
565
|
-
} catch {}
|
|
563
|
+
} catch { /* ignore */ }
|
|
566
564
|
}
|
|
567
565
|
if (!pyFound) bad('Python (LAO)', 'not found — context trimming disabled');
|
|
568
566
|
if (laoPyExists) ok('LAO scorer', laoPyPath);
|
|
@@ -688,7 +686,7 @@ const sessionAuditor = _createAuditor(process.env.OCCASIO_AUDIT_FILE || undefine
|
|
|
688
686
|
process.stderr.write(`\n${col.r('[occasio][audit-fatal]')} policy_loaded write failed: ${status.error?.message}\n`);
|
|
689
687
|
process.stderr.write(`${col.r('[occasio][audit-fatal] dropped row:')} ${dropped}\n`);
|
|
690
688
|
process.stderr.write(`${col.r('[occasio][audit-fatal] proxy aborting; supervisor will restart.')}\n`);
|
|
691
|
-
try { server && server.close && server.close(); } catch {}
|
|
689
|
+
try { server && server.close && server.close(); } catch { /* ignore */ }
|
|
692
690
|
setTimeout(() => process.exit(1), 250);
|
|
693
691
|
}
|
|
694
692
|
});
|
|
@@ -727,7 +725,7 @@ if (budget !== null) {
|
|
|
727
725
|
const { execSync: _ex } = require('child_process');
|
|
728
726
|
let _pyOk = false;
|
|
729
727
|
for (const _cmd of ['python', 'python3']) {
|
|
730
|
-
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 */ }
|
|
731
729
|
}
|
|
732
730
|
if (!_pyOk) process.stderr.write(col.y(` ⚠ LAO disabled — Python not found (context trimming inactive)\n`));
|
|
733
731
|
}
|
|
@@ -775,7 +773,7 @@ const server = http.createServer((req, res) => {
|
|
|
775
773
|
try {
|
|
776
774
|
const rules = JSON.parse(fs.readFileSync(rp, 'utf8'));
|
|
777
775
|
blocked = files.filter(f => (rules.block || []).some(p => f.includes(p.replace(/\*\*/g, '').replace(/\*/g, ''))));
|
|
778
|
-
} catch {}
|
|
776
|
+
} catch { /* ignore */ }
|
|
779
777
|
}
|
|
780
778
|
|
|
781
779
|
const shouldBlock = (mode === 'block_secrets' && secrets.length) || (mode === 'block_rules' && blocked.length);
|
|
@@ -808,7 +806,7 @@ const server = http.createServer((req, res) => {
|
|
|
808
806
|
res.end(JSON.stringify({ error: { type: 'blocked', reason: secrets.length ? secrets[0].label : 'rule', by: 'Occasio' } }));
|
|
809
807
|
return;
|
|
810
808
|
}
|
|
811
|
-
} catch {}
|
|
809
|
+
} catch { /* ignore */ }
|
|
812
810
|
}
|
|
813
811
|
|
|
814
812
|
// ── Budget enforcement (Stage 2: policy-driven BLOCK) ─────────────────────
|
|
@@ -842,7 +840,7 @@ const server = http.createServer((req, res) => {
|
|
|
842
840
|
const s = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8'));
|
|
843
841
|
s.budget_exceeded_count = (s.budget_exceeded_count || 0) + 1;
|
|
844
842
|
fs.writeFileSync(SESSION_FILE, JSON.stringify(s));
|
|
845
|
-
} catch {}
|
|
843
|
+
} catch { /* ignore */ }
|
|
846
844
|
const synth = decision.syntheticResponse;
|
|
847
845
|
res.writeHead(synth.status, { 'Content-Type': 'application/json' });
|
|
848
846
|
res.end(JSON.stringify(synth.body));
|
|
@@ -870,7 +868,7 @@ const server = http.createServer((req, res) => {
|
|
|
870
868
|
}
|
|
871
869
|
}
|
|
872
870
|
}
|
|
873
|
-
} catch {}
|
|
871
|
+
} catch { /* ignore */ }
|
|
874
872
|
}
|
|
875
873
|
// ──────────────────────────────────────────────────────────────────────────
|
|
876
874
|
|
|
@@ -1057,7 +1055,7 @@ const server = http.createServer((req, res) => {
|
|
|
1057
1055
|
}
|
|
1058
1056
|
forwardBody = Buffer.from(JSON.stringify(b));
|
|
1059
1057
|
outboundMessageCount = b.messages?.length ?? outboundMessageCount;
|
|
1060
|
-
} catch {}
|
|
1058
|
+
} catch { /* ignore */ }
|
|
1061
1059
|
}
|
|
1062
1060
|
if (laoDropped.length > 0) {
|
|
1063
1061
|
const ts0 = new Date().toTimeString().slice(0, 8);
|
|
@@ -1183,7 +1181,7 @@ const server = http.createServer((req, res) => {
|
|
|
1183
1181
|
process.stderr.write(`\n${col.r('[occasio][audit-fatal]')} ${e.message}\n`);
|
|
1184
1182
|
process.stderr.write(`${col.r('[occasio][audit-fatal] dropped row:')} ${dropped}\n`);
|
|
1185
1183
|
process.stderr.write(`${col.r('[occasio][audit-fatal] proxy aborting; supervisor will restart.')}\n`);
|
|
1186
|
-
try { server && server.close && server.close(); } catch {}
|
|
1184
|
+
try { server && server.close && server.close(); } catch { /* ignore */ }
|
|
1187
1185
|
setTimeout(() => process.exit(1), 250);
|
|
1188
1186
|
return;
|
|
1189
1187
|
}
|
|
@@ -1212,10 +1210,10 @@ const server = http.createServer((req, res) => {
|
|
|
1212
1210
|
cacheRead = d.usage.cache_read_input_tokens || cacheRead;
|
|
1213
1211
|
}
|
|
1214
1212
|
if (d.type === 'message_delta' && d.usage) out = d.usage.output_tokens || out;
|
|
1215
|
-
} catch {}
|
|
1213
|
+
} catch { /* ignore */ }
|
|
1216
1214
|
}
|
|
1217
1215
|
}
|
|
1218
|
-
} catch {}
|
|
1216
|
+
} catch { /* ignore */ }
|
|
1219
1217
|
|
|
1220
1218
|
// When the interceptor ran, Anthropic was billed for N calls:
|
|
1221
1219
|
// call #1 → initial tool_use round (toolCallUsage)
|
|
@@ -1450,7 +1448,7 @@ server.listen(PORT, '127.0.0.1', () => {
|
|
|
1450
1448
|
if (parts.length) process.stderr.write(col.d(` Breakdown: ${parts.join(' + ')}\n`));
|
|
1451
1449
|
}
|
|
1452
1450
|
process.stderr.write('────────────────────────────────────────\n\n');
|
|
1453
|
-
} catch {}
|
|
1451
|
+
} catch { /* ignore */ }
|
|
1454
1452
|
process.exit(code || 0);
|
|
1455
1453
|
});
|
|
1456
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`);
|
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
|
}
|