@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.
Files changed (92) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +10 -0
  3. package/README.md +216 -0
  4. package/bin/occasio-mcp.js +5 -0
  5. package/bin/occasio.js +2 -0
  6. package/bin/supervisor/README.md +90 -0
  7. package/bin/supervisor/com.occasio.proxy.plist.template +36 -0
  8. package/bin/supervisor/install-windows-task.ps1 +48 -0
  9. package/bin/supervisor/occasio.service +18 -0
  10. package/docs/AUDIT.md +120 -0
  11. package/docs/attest_verify.py +283 -0
  12. package/docs/audit_walker.py +65 -0
  13. package/docs/canonicalize.py +99 -0
  14. package/docs/compliance-mapping.md +93 -0
  15. package/docs/demos/mcp-block.md +148 -0
  16. package/docs/edr-calibration.md +73 -0
  17. package/docs/edr-demo.md +83 -0
  18. package/docs/python-verifier.md +74 -0
  19. package/docs/reference-pipeline.md +140 -0
  20. package/package.json +69 -0
  21. package/policy-templates/dev-default.yml +84 -0
  22. package/policy-templates/finance.yml +61 -0
  23. package/policy-templates/strict.yml +49 -0
  24. package/schemas/agent-attestation-v1.json +190 -0
  25. package/schemas/occasio-policy.schema.json +99 -0
  26. package/spec/agent-attestation/v1/README.md +137 -0
  27. package/src/adapters/claude-code.js +518 -0
  28. package/src/adapters/cline.js +161 -0
  29. package/src/adapters/computer-use-cli.js +198 -0
  30. package/src/adapters/computer-use.js +227 -0
  31. package/src/analyzer.js +170 -0
  32. package/src/anomaly/cli.js +143 -0
  33. package/src/anomaly/detectors/deny-rate.js +84 -0
  34. package/src/anomaly/detectors/file-read-volume.js +109 -0
  35. package/src/anomaly/detectors/secret-redact-rate.js +107 -0
  36. package/src/anomaly/detectors/unknown-tool-input.js +83 -0
  37. package/src/anomaly/index.js +169 -0
  38. package/src/attest/canonicalize.js +97 -0
  39. package/src/attest/index.js +355 -0
  40. package/src/attest/run-slice.js +57 -0
  41. package/src/attest/sign.js +186 -0
  42. package/src/attest/verify.js +192 -0
  43. package/src/audit/errors.js +21 -0
  44. package/src/audit/input-normalizer.js +121 -0
  45. package/src/audit/jsonl-auditor.js +178 -0
  46. package/src/audit/verifier.js +152 -0
  47. package/src/baseline.js +507 -0
  48. package/src/boundary.js +238 -0
  49. package/src/budget.js +42 -0
  50. package/src/classifier.js +115 -0
  51. package/src/context-budget.js +77 -0
  52. package/src/core/boundary-event.js +75 -0
  53. package/src/core/decision.js +61 -0
  54. package/src/core/pipeline.js +66 -0
  55. package/src/core/tool-names.js +105 -0
  56. package/src/dashboard.js +892 -0
  57. package/src/demo/README.md +31 -0
  58. package/src/demo/anomalies-demo.js +211 -0
  59. package/src/demo/attest-demo.js +198 -0
  60. package/src/distiller.js +155 -0
  61. package/src/embeddings.json +72 -0
  62. package/src/executor/dispatcher.js +230 -0
  63. package/src/harness.js +817 -0
  64. package/src/index.js +1711 -0
  65. package/src/inspect.js +329 -0
  66. package/src/interceptor.js +1198 -0
  67. package/src/lao.js +185 -0
  68. package/src/lao_prep.py +119 -0
  69. package/src/ledger.js +209 -0
  70. package/src/mcp-experiment.js +140 -0
  71. package/src/mcp-normalize.js +139 -0
  72. package/src/mcp-server.js +320 -0
  73. package/src/outbound-policy.js +433 -0
  74. package/src/policy/built-in-classifiers.js +78 -0
  75. package/src/policy/doctor.js +226 -0
  76. package/src/policy/engine.js +339 -0
  77. package/src/policy/init.js +153 -0
  78. package/src/policy/loader.js +448 -0
  79. package/src/policy/rules-default.js +36 -0
  80. package/src/policy/shell-path.js +135 -0
  81. package/src/policy/show.js +196 -0
  82. package/src/policy/validate.js +310 -0
  83. package/src/preflight/cli.js +164 -0
  84. package/src/preflight/miner.js +329 -0
  85. package/src/proxy/agent-router.js +93 -0
  86. package/src/redteam.js +428 -0
  87. package/src/replay.js +446 -0
  88. package/src/report/index.js +224 -0
  89. package/src/runtime.js +595 -0
  90. package/src/scanner/index.js +49 -0
  91. package/src/selftest.js +192 -0
  92. package/src/session.js +36 -0
@@ -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 };