@occasiolabs/occasio 0.8.1
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/LICENSE +202 -0
- package/NOTICE +10 -0
- package/README.md +216 -0
- package/bin/occasio-mcp.js +5 -0
- package/bin/occasio.js +2 -0
- package/bin/supervisor/README.md +90 -0
- package/bin/supervisor/com.occasio.proxy.plist.template +36 -0
- package/bin/supervisor/install-windows-task.ps1 +48 -0
- package/bin/supervisor/occasio.service +18 -0
- package/docs/AUDIT.md +120 -0
- package/docs/attest_verify.py +283 -0
- package/docs/audit_walker.py +65 -0
- package/docs/canonicalize.py +99 -0
- package/docs/compliance-mapping.md +93 -0
- package/docs/demos/mcp-block.md +148 -0
- package/docs/edr-calibration.md +73 -0
- package/docs/edr-demo.md +83 -0
- package/docs/python-verifier.md +74 -0
- package/docs/reference-pipeline.md +140 -0
- package/package.json +69 -0
- package/policy-templates/dev-default.yml +84 -0
- package/policy-templates/finance.yml +61 -0
- package/policy-templates/strict.yml +49 -0
- package/schemas/agent-attestation-v1.json +190 -0
- package/schemas/occasio-policy.schema.json +99 -0
- package/spec/agent-attestation/v1/README.md +137 -0
- package/src/adapters/claude-code.js +518 -0
- package/src/adapters/cline.js +161 -0
- package/src/adapters/computer-use-cli.js +198 -0
- package/src/adapters/computer-use.js +227 -0
- package/src/analyzer.js +170 -0
- package/src/anomaly/cli.js +143 -0
- package/src/anomaly/detectors/deny-rate.js +84 -0
- package/src/anomaly/detectors/file-read-volume.js +109 -0
- package/src/anomaly/detectors/secret-redact-rate.js +107 -0
- package/src/anomaly/detectors/unknown-tool-input.js +83 -0
- package/src/anomaly/index.js +169 -0
- package/src/attest/canonicalize.js +97 -0
- package/src/attest/index.js +355 -0
- package/src/attest/run-slice.js +57 -0
- package/src/attest/sign.js +186 -0
- package/src/attest/verify.js +192 -0
- package/src/audit/errors.js +21 -0
- package/src/audit/input-normalizer.js +121 -0
- package/src/audit/jsonl-auditor.js +178 -0
- package/src/audit/verifier.js +152 -0
- package/src/baseline.js +507 -0
- package/src/boundary.js +238 -0
- package/src/budget.js +42 -0
- package/src/classifier.js +115 -0
- package/src/context-budget.js +77 -0
- package/src/core/boundary-event.js +75 -0
- package/src/core/decision.js +61 -0
- package/src/core/pipeline.js +66 -0
- package/src/core/tool-names.js +105 -0
- package/src/dashboard.js +892 -0
- package/src/demo/README.md +31 -0
- package/src/demo/anomalies-demo.js +211 -0
- package/src/demo/attest-demo.js +198 -0
- package/src/distiller.js +155 -0
- package/src/embeddings.json +72 -0
- package/src/executor/dispatcher.js +230 -0
- package/src/harness.js +817 -0
- package/src/index.js +1711 -0
- package/src/inspect.js +329 -0
- package/src/interceptor.js +1198 -0
- package/src/lao.js +185 -0
- package/src/lao_prep.py +119 -0
- package/src/ledger.js +209 -0
- package/src/mcp-experiment.js +140 -0
- package/src/mcp-normalize.js +139 -0
- package/src/mcp-server.js +320 -0
- package/src/outbound-policy.js +433 -0
- package/src/policy/built-in-classifiers.js +78 -0
- package/src/policy/doctor.js +226 -0
- package/src/policy/engine.js +339 -0
- package/src/policy/init.js +153 -0
- package/src/policy/loader.js +448 -0
- package/src/policy/rules-default.js +36 -0
- package/src/policy/shell-path.js +135 -0
- package/src/policy/show.js +196 -0
- package/src/policy/validate.js +310 -0
- package/src/preflight/cli.js +164 -0
- package/src/preflight/miner.js +329 -0
- package/src/proxy/agent-router.js +93 -0
- package/src/redteam.js +428 -0
- package/src/replay.js +446 -0
- package/src/report/index.js +224 -0
- package/src/runtime.js +595 -0
- package/src/scanner/index.js +49 -0
- package/src/selftest.js +192 -0
- package/src/session.js +36 -0
package/src/selftest.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* occasio selftest — exercise the highest-risk governance claims against a
|
|
5
|
+
* scratch temp setup. Never touches the user's real ~/.occasio chain.
|
|
6
|
+
*
|
|
7
|
+
* Scope (deliberately small):
|
|
8
|
+
* 1. read_file vs deny_paths → BLOCK path-denied
|
|
9
|
+
* 2. shell_bash cat vs deny_paths → BLOCK path-denied
|
|
10
|
+
* 3. shell_powershell Get-Content vs deny_paths → BLOCK path-denied
|
|
11
|
+
* 4. read_file allowed path → LOCAL, bytes returned
|
|
12
|
+
* 5. secret in tool_result (block mode) → BLOCK secret-in-tool-result
|
|
13
|
+
* 6. redact-secrets TRANSFORM → bytes returned, secret absent
|
|
14
|
+
* 7. audit verify on the scratch chain → ok=true
|
|
15
|
+
* 8. scratch chain shape → ≥1 BLOCK + ≥1 TRANSFORM rows
|
|
16
|
+
*
|
|
17
|
+
* Exit codes (via runSelfTestCli): 0 = all pass, 1 = any fail.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const os = require('os');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
const { makeBoundaryEvent } = require('./core/boundary-event');
|
|
25
|
+
const { processToolEvent } = require('./core/pipeline');
|
|
26
|
+
const loader = require('./policy/loader');
|
|
27
|
+
const engine = require('./policy/engine');
|
|
28
|
+
const { createAuditor } = require('./audit/jsonl-auditor');
|
|
29
|
+
const { verifyFile } = require('./audit/verifier');
|
|
30
|
+
|
|
31
|
+
const AKIA_FIXTURE = 'AKIA' + 'ABCDEFGHIJKLMNOP'; // 20 chars; matches scanner pattern
|
|
32
|
+
|
|
33
|
+
function mkScratch() {
|
|
34
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'lf-selftest-'));
|
|
35
|
+
const denyDir = path.join(root, 'denied');
|
|
36
|
+
const allowDir = path.join(root, 'allowed');
|
|
37
|
+
fs.mkdirSync(denyDir);
|
|
38
|
+
fs.mkdirSync(allowDir);
|
|
39
|
+
const denyFile = path.join(denyDir, 'secret.txt');
|
|
40
|
+
const allowFile = path.join(allowDir, 'public.txt');
|
|
41
|
+
const akiaFile = path.join(allowDir, 'with-akia.txt');
|
|
42
|
+
fs.writeFileSync(denyFile, 'TOP-SECRET-DO-NOT-LEAK\n');
|
|
43
|
+
fs.writeFileSync(allowFile, 'public content\n');
|
|
44
|
+
fs.writeFileSync(akiaFile, `header\n${AKIA_FIXTURE}\nfooter\n`);
|
|
45
|
+
return { root, denyDir, allowDir, denyFile, allowFile, akiaFile };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function mkEvent(toolName, toolInput) {
|
|
49
|
+
return makeBoundaryEvent({
|
|
50
|
+
direction: 'inbound', kind: 'tool_call',
|
|
51
|
+
agent: 'claude-code', protocol: 'anthropic-http',
|
|
52
|
+
sessionId: 'selftest', runId: 'selftest',
|
|
53
|
+
toolName, toolInput,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function runSelfTest(opts = {}) {
|
|
58
|
+
const silent = !!opts.silent;
|
|
59
|
+
const log = silent ? () => {} : (s) => process.stdout.write(s + '\n');
|
|
60
|
+
|
|
61
|
+
const fixtures = mkScratch();
|
|
62
|
+
const auditPath = path.join(fixtures.root, 'events.jsonl');
|
|
63
|
+
const auditor = createAuditor(auditPath);
|
|
64
|
+
|
|
65
|
+
const scenarios = [];
|
|
66
|
+
const record = (id, name, ok, detail) => {
|
|
67
|
+
scenarios.push({ id, name, status: ok ? 'pass' : 'fail', detail: detail || null });
|
|
68
|
+
log(` ${ok ? '✓' : '✗'} ${String(id).padStart(2)}. ${name}${ok ? '' : ` — ${detail}`}`);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
log('Occasio self-test');
|
|
72
|
+
log(` scratch root: ${fixtures.root}`);
|
|
73
|
+
log(` audit file: ${auditPath}`);
|
|
74
|
+
log('');
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
// Policy A: deny_paths covers the denied directory.
|
|
78
|
+
loader._setOverrideForTests({
|
|
79
|
+
deny_paths: [fixtures.denyDir],
|
|
80
|
+
allow_paths: [],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// 1. read_file vs deny_paths
|
|
84
|
+
{
|
|
85
|
+
const r = await processToolEvent(
|
|
86
|
+
mkEvent('read_file', { file_path: fixtures.denyFile }),
|
|
87
|
+
{ auditor });
|
|
88
|
+
const ok = r.decision.action === 'BLOCK' && r.decision.reason === 'path-denied';
|
|
89
|
+
record(1, 'read_file <denied> → BLOCK path-denied', ok,
|
|
90
|
+
ok ? null : `got ${r.decision.action} / ${r.decision.reason}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 2. shell_bash cat vs deny_paths
|
|
94
|
+
{
|
|
95
|
+
const r = await processToolEvent(
|
|
96
|
+
mkEvent('shell_bash', { command: `cat ${fixtures.denyFile}` }),
|
|
97
|
+
{ auditor });
|
|
98
|
+
const ok = r.decision.action === 'BLOCK' && r.decision.reason === 'path-denied';
|
|
99
|
+
record(2, 'shell_bash cat <denied> → BLOCK path-denied', ok,
|
|
100
|
+
ok ? null : `got ${r.decision.action} / ${r.decision.reason}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 3. shell_powershell Get-Content vs deny_paths
|
|
104
|
+
{
|
|
105
|
+
const r = await processToolEvent(
|
|
106
|
+
mkEvent('shell_powershell', { command: `Get-Content ${fixtures.denyFile}` }),
|
|
107
|
+
{ auditor });
|
|
108
|
+
const ok = r.decision.action === 'BLOCK' && r.decision.reason === 'path-denied';
|
|
109
|
+
record(3, 'shell_powershell Get-Content <denied> → BLOCK path-denied', ok,
|
|
110
|
+
ok ? null : `got ${r.decision.action} / ${r.decision.reason}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 4. read_file on an allowed path runs LOCAL and returns bytes
|
|
114
|
+
{
|
|
115
|
+
const r = await processToolEvent(
|
|
116
|
+
mkEvent('read_file', { file_path: fixtures.allowFile }),
|
|
117
|
+
{ auditor });
|
|
118
|
+
const okAct = r.decision.action === 'LOCAL';
|
|
119
|
+
const bytes = typeof r.result?.output === 'string' ? r.result.output : '';
|
|
120
|
+
const okBytes = bytes.includes('public content');
|
|
121
|
+
record(4, 'read_file <allowed> → LOCAL with bytes', okAct && okBytes,
|
|
122
|
+
!okAct ? `decision=${r.decision.action}` : (!okBytes ? 'bytes missing' : null));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 5. Secret in tool_result under block mode → BLOCK
|
|
126
|
+
{
|
|
127
|
+
const decision = engine.evaluateToolResults(
|
|
128
|
+
[{ tool_use_id: 'sst-5', content: `noise\n${AKIA_FIXTURE}\nmore noise\n` }],
|
|
129
|
+
{ mode: 'block_secrets' });
|
|
130
|
+
const ok = decision.action === 'BLOCK' && /secret in tool result/.test(decision.reason || '');
|
|
131
|
+
record(5, 'secret in tool_result (block_secrets) → BLOCK', ok,
|
|
132
|
+
ok ? null : `got ${decision.action} / ${decision.reason}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 6. redact-secrets TRANSFORM: same file, but policy redacts. Secret bytes
|
|
136
|
+
// must be absent from the returned output.
|
|
137
|
+
loader._setOverrideForTests({
|
|
138
|
+
deny_paths: [],
|
|
139
|
+
allow_paths: [],
|
|
140
|
+
redact_secrets_in_tool_results: true,
|
|
141
|
+
});
|
|
142
|
+
{
|
|
143
|
+
const r = await processToolEvent(
|
|
144
|
+
mkEvent('read_file', { file_path: fixtures.akiaFile }),
|
|
145
|
+
{ auditor });
|
|
146
|
+
const out = typeof r.result?.output === 'string' ? r.result.output : '';
|
|
147
|
+
const okAct = r.decision.action === 'TRANSFORM' && r.result?.transformed === true;
|
|
148
|
+
const okHide = out.length > 0 && !out.includes(AKIA_FIXTURE);
|
|
149
|
+
record(6, 'redact-secrets TRANSFORM → secret absent from output', okAct && okHide,
|
|
150
|
+
!okAct ? `decision=${r.decision.action}` : (!okHide ? 'secret leaked in output' : null));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 7. Audit verify on the scratch chain
|
|
154
|
+
{
|
|
155
|
+
const v = verifyFile(auditPath);
|
|
156
|
+
const ok = v.ok === true && v.chained >= 5;
|
|
157
|
+
record(7, `audit verify on scratch chain → ok (${v.chained} chained rows)`, ok,
|
|
158
|
+
ok ? null : `ok=${v.ok} chained=${v.chained} errors=${(v.errors || []).length}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 8. Scratch chain shape: at least one BLOCK row and one TRANSFORM row
|
|
162
|
+
{
|
|
163
|
+
const rows = fs.readFileSync(auditPath, 'utf8')
|
|
164
|
+
.split('\n').filter(Boolean).map(JSON.parse);
|
|
165
|
+
const nBlock = rows.filter(r => r.action === 'BLOCK').length;
|
|
166
|
+
const nTransform = rows.filter(r => r.action === 'TRANSFORM').length;
|
|
167
|
+
const ok = nBlock >= 3 && nTransform >= 1;
|
|
168
|
+
record(8, `scratch chain rows: ${nBlock} BLOCK + ${nTransform} TRANSFORM`, ok,
|
|
169
|
+
ok ? null : `expected ≥3 BLOCK + ≥1 TRANSFORM`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
} finally {
|
|
173
|
+
loader._resetCache();
|
|
174
|
+
try { fs.rmSync(fixtures.root, { recursive: true, force: true }); } catch {}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const failed = scenarios.filter(s => s.status === 'fail').length;
|
|
178
|
+
const ok = failed === 0;
|
|
179
|
+
log('');
|
|
180
|
+
log(ok
|
|
181
|
+
? `All ${scenarios.length} self-tests passed.`
|
|
182
|
+
: `${failed} of ${scenarios.length} self-tests FAILED.`);
|
|
183
|
+
|
|
184
|
+
return { ok, scenarios };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function runSelfTestCli() {
|
|
188
|
+
const result = await runSelfTest({});
|
|
189
|
+
process.exit(result.ok ? 0 : 1);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
module.exports = { runSelfTest, runSelfTestCli };
|
package/src/session.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* session.js — Shared session.json helpers for cross-process accounting.
|
|
5
|
+
*
|
|
6
|
+
* index.js owns the main session lifecycle (init, updateSession, read).
|
|
7
|
+
* mcp-server.js runs in a separate process and cannot call index.js directly.
|
|
8
|
+
* This module provides the single shared function that mcp-server.js needs to
|
|
9
|
+
* increment tools_mcp_count without touching any other session field.
|
|
10
|
+
*
|
|
11
|
+
* Pattern: read-modify-write, same as updateSession() in index.js.
|
|
12
|
+
* No file locking — same accepted tradeoff as the rest of the codebase.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
|
|
19
|
+
const SESSION_FILE = path.join(os.homedir(), '.occasio', 'session.json');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Increment tools_mcp_count in session.json by n (default 1).
|
|
23
|
+
* No-ops if session.json does not exist or has no run_id (no active session).
|
|
24
|
+
* Silent on any I/O error.
|
|
25
|
+
*/
|
|
26
|
+
function incrementSessionMcpCount(n = 1) {
|
|
27
|
+
try {
|
|
28
|
+
let s;
|
|
29
|
+
try { s = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8')); } catch { return; }
|
|
30
|
+
if (!s || !s.run_id) return; // no active proxy session — don't create a stale entry
|
|
31
|
+
s.tools_mcp_count = (s.tools_mcp_count || 0) + n;
|
|
32
|
+
fs.writeFileSync(SESSION_FILE, JSON.stringify(s));
|
|
33
|
+
} catch {}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { SESSION_FILE, incrementSessionMcpCount };
|