@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
package/src/inspect.js ADDED
@@ -0,0 +1,329 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * inspect.js โ€” Cloud-boundary manifest for Occasio log entries.
5
+ *
6
+ * Answers per-request:
7
+ * - What exactly reached Anthropic?
8
+ * - What ran locally and why?
9
+ * - What was trimmed or distilled before send?
10
+ * - What was blocked and never sent?
11
+ *
12
+ * All display is derived from existing JSONL log fields plus two new ones:
13
+ * lao_dropped (string[]) โ€” files LAO removed before send
14
+ * outbound_message_count (number) โ€” messages in the forwarded request
15
+ */
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const os = require('os');
20
+ const { readDayLog, readSessionEntries } = require('./ledger');
21
+
22
+ const LOG_DIR = path.join(os.homedir(), '.occasio');
23
+ const SESSION_FILE = path.join(LOG_DIR, 'session.json');
24
+
25
+ const col = {
26
+ r: s => `\x1b[31m${s}\x1b[0m`, g: s => `\x1b[32m${s}\x1b[0m`,
27
+ y: s => `\x1b[33m${s}\x1b[0m`, c: s => `\x1b[36m${s}\x1b[0m`,
28
+ d: s => `\x1b[2m${s}\x1b[0m`, b: s => `\x1b[1m${s}\x1b[0m`,
29
+ };
30
+
31
+ function todayStr() {
32
+ const d = new Date();
33
+ return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
34
+ }
35
+
36
+ function fmtN(n) {
37
+ if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
38
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + 'k';
39
+ return String(n || 0);
40
+ }
41
+
42
+ /**
43
+ * Extract structured cloud-boundary facts from a raw log entry.
44
+ * Pure function โ€” no I/O, fully testable.
45
+ *
46
+ * @param {object} entry A JSONL log entry
47
+ * @returns {BoundaryFacts}
48
+ */
49
+ function buildBoundaryFacts(entry) {
50
+ if (!entry || typeof entry !== 'object') return null;
51
+
52
+ const et = entry.event_type || (entry.intercepted ? 'local_only' : 'cloud_sent');
53
+
54
+ return {
55
+ event_type: et,
56
+ // true for any event type where something reached Anthropic (even after trim/intercept)
57
+ reached_anthropic: et !== 'blocked' && et !== 'budget_exceeded',
58
+ blocked_entirely: et === 'blocked' || et === 'budget_exceeded',
59
+
60
+ // Tokens Anthropic actually reported (exact โ€” from provider response)
61
+ input_tokens: entry.input_tokens || 0,
62
+ output_tokens: entry.output_tokens || 0,
63
+ cost: entry.cost || 0,
64
+ cache_read_tokens: entry.cache_read_tokens || 0,
65
+ cache_write_tokens: entry.cache_write_tokens || 0,
66
+ cache_savings: entry.cache_savings || 0,
67
+
68
+ // Files in context (names + approximate token counts โ€” estimated by Occasio analyzer)
69
+ files_in_context: entry.file_tokens || [],
70
+
71
+ // How many messages were in the outbound request (exact โ€” from request body)
72
+ outbound_message_count: entry.outbound_message_count || null,
73
+
74
+ // What LAO removed before forwarding (exact โ€” recorded at trim time)
75
+ lao_dropped: entry.lao_dropped || [],
76
+ lao_tokens_saved: entry.lao_tokens_saved || 0,
77
+ lao_cost_saved: entry.lao_cost_saved || 0,
78
+
79
+ // Tool runs executed locally (exact โ€” from interceptor)
80
+ tools_local: (entry.tools || []),
81
+ tools_distilled: (entry.tools || []).filter(t => t.distilled),
82
+ distill_tokens_saved: entry.distill_tokens_saved || 0,
83
+
84
+ // Blocked by secret detection (exact โ€” from analyzer)
85
+ secrets_detected: entry.secrets || [],
86
+ files_rule_blocked: entry.blocked || [],
87
+
88
+ // Budget enforcement
89
+ budget_limit: entry.budget_limit ?? null,
90
+ budget_spent: entry.budget_spent ?? null,
91
+ };
92
+ }
93
+
94
+ // โ”€โ”€ Boundary section renderers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
95
+
96
+ const RULE = col.d('โ”€'.repeat(56));
97
+
98
+ function sectionHeader(icon, label, note) {
99
+ const noteStr = note ? col.d(` (${note})`) : '';
100
+ return `\n${icon} ${col.b(label)}${noteStr}`;
101
+ }
102
+
103
+ function renderCloudSent(facts) {
104
+ // LAO trim section (if applicable)
105
+ if (facts.lao_dropped.length > 0 || facts.lao_tokens_saved > 0) {
106
+ console.log(sectionHeader(col.y('โœ‚'), 'TRIMMED BEFORE SEND', 'LAO context optimizer'));
107
+ for (const f of facts.lao_dropped) {
108
+ const name = f.split(/[/\\]/).slice(-2).join('/');
109
+ console.log(` ${col.d(name)} ${col.y('removed')}`);
110
+ }
111
+ if (facts.lao_dropped.length === 0 && facts.lao_tokens_saved > 0) {
112
+ console.log(col.d(` (file list unavailable โ€” log entry predates v0.5.3)`));
113
+ }
114
+ if (facts.lao_tokens_saved > 0) {
115
+ console.log(col.y(` Tokens saved: ~${fmtN(facts.lao_tokens_saved)}`
116
+ + (facts.lao_cost_saved > 0 ? col.g(` ($${facts.lao_cost_saved.toFixed(4)})`) : '')));
117
+ }
118
+ }
119
+
120
+ // Cloud payload
121
+ console.log(sectionHeader(col.c('โ†’'), 'SENT TO ANTHROPIC', 'provider-reported token counts'));
122
+ console.log(` Tokens: ${col.y(fmtN(facts.input_tokens) + ' in')} / ${col.y(fmtN(facts.output_tokens) + ' out')} ${col.g('$' + facts.cost.toFixed(4))}`);
123
+ if (facts.outbound_message_count) {
124
+ console.log(col.d(` Messages: ${facts.outbound_message_count} in request`));
125
+ }
126
+ if (facts.cache_read_tokens > 0) {
127
+ console.log(col.d(` Cache: ${fmtN(facts.cache_read_tokens)} tokens read`
128
+ + (facts.cache_savings > 0 ? ` (saved $${facts.cache_savings.toFixed(4)})` : '')));
129
+ }
130
+
131
+ const fts = facts.files_in_context;
132
+ if (fts.length > 0) {
133
+ console.log(col.d(` Files in context (names from request body โ€” token counts are estimates):`));
134
+ const show = fts.slice(0, 8);
135
+ for (const f of show) {
136
+ const name = (f.name || '?').split(/[/\\]/).slice(-2).join('/');
137
+ console.log(` ${name.padEnd(40)} ${col.d('~' + fmtN(f.tokens || 0) + ' tokens')}`);
138
+ }
139
+ if (fts.length > 8) console.log(col.d(` โ€ฆ and ${fts.length - 8} more files`));
140
+ } else if (facts.input_tokens > 0) {
141
+ console.log(col.d(` (no per-file breakdown โ€” file analysis not available for this request)`));
142
+ }
143
+
144
+ // Distilled tool results note
145
+ const dist = facts.tools_distilled;
146
+ if (dist.length > 0) {
147
+ console.log(sectionHeader(col.y('โœ‚'), 'DISTILLED BEFORE RE-ENTERING MODEL', 'tool output reduced'));
148
+ for (const t of dist) {
149
+ const cmd = (t.cmd || '?').slice(0, 48);
150
+ console.log(` ${cmd.padEnd(50)} ${col.y('โœ‚ ' + (t.distillLabel || 'distilled'))}`);
151
+ }
152
+ console.log(col.d(` Full output saved locally. Run: ${col.b('occasio distill')} to view.`));
153
+ }
154
+ }
155
+
156
+ function renderLocalOnly(facts) {
157
+ const tools = facts.tools_local;
158
+
159
+ if (tools.length > 0) {
160
+ console.log(sectionHeader(col.g('โšก'), 'EXECUTED LOCALLY', 'tool results forwarded to Anthropic in follow-up call'));
161
+ for (const t of tools) {
162
+ const cmd = (t.cmd || '?').slice(0, 46);
163
+ const sz = t.bytes != null ? `${(t.bytes / 1024).toFixed(1)} KB`.padStart(9) : ''.padStart(9);
164
+ const nat = t.native ? col.g(' native') : col.d(' exec ');
165
+ const dis = t.distilled ? col.y(` โœ‚ ${(t.distillLabel || 'distilled').slice(0, 30)}`) : '';
166
+ console.log(` ${cmd.padEnd(48)} ${col.d(sz)}${nat}${dis}`);
167
+ }
168
+ console.log(col.d(`\n Note: "local" means execution happened on this machine (not in Claude Code's`));
169
+ console.log(col.d(` subprocess). Tool results were forwarded to Anthropic in Occasio's follow-up call.`));
170
+ }
171
+
172
+ if (facts.input_tokens > 0 || facts.output_tokens > 0) {
173
+ console.log(sectionHeader(col.c('โ†’'), 'SENT TO ANTHROPIC', 'initial + follow-up calls combined'));
174
+ console.log(` Tokens: ${col.y(fmtN(facts.input_tokens) + ' in')} / ${col.y(fmtN(facts.output_tokens) + ' out')} ${col.g('$' + facts.cost.toFixed(4))}`);
175
+ if (facts.cache_read_tokens > 0) {
176
+ console.log(col.d(` Cache: ${fmtN(facts.cache_read_tokens)} tokens read (saved $${facts.cache_savings.toFixed(4)})`));
177
+ }
178
+ }
179
+
180
+ const dist = facts.tools_distilled;
181
+ if (dist.length > 0) {
182
+ console.log(sectionHeader(col.y('โœ‚'), 'DISTILLED BEFORE FORWARDING', 'tool output reduced before re-entering model'));
183
+ for (const t of dist) {
184
+ console.log(` ${(t.cmd || '?').slice(0, 48).padEnd(50)} ${col.y('โœ‚ ' + (t.distillLabel || ''))}`);
185
+ }
186
+ console.log(col.d(` Run: ${col.b('occasio distill')} to view raw output.`));
187
+ }
188
+ }
189
+
190
+ function renderBlocked(facts) {
191
+ console.log(`\n${col.r('๐Ÿ›‘ BLOCKED')} ${col.d('โ€” nothing was sent to Anthropic')}`);
192
+ const secs = facts.secrets_detected;
193
+ if (secs.length > 0) {
194
+ console.log(col.r(`\n Detected secrets:`));
195
+ for (const s of secs) {
196
+ const line = s.line ? col.d(` line ${s.line}`) : '';
197
+ console.log(` ${col.r('ร—')} ${s.label || 'secret'}${line}`);
198
+ }
199
+ }
200
+ const blk = facts.files_rule_blocked;
201
+ if (blk.length > 0) {
202
+ console.log(col.r(`\n Rule-blocked files:`));
203
+ for (const f of blk) console.log(` ${col.r('ร—')} ${f}`);
204
+ }
205
+ }
206
+
207
+ function renderBudgetExceeded(facts) {
208
+ console.log(`\n${col.r('๐Ÿšซ BUDGET EXCEEDED')} ${col.d('โ€” nothing was sent to Anthropic')}`);
209
+ if (facts.budget_limit != null) {
210
+ console.log(` Budget limit: ${col.y('$' + facts.budget_limit.toFixed(4))}`);
211
+ console.log(` Spent so far: ${col.r('$' + (facts.budget_spent || 0).toFixed(4))}`);
212
+ const overage = (facts.budget_spent || 0) - facts.budget_limit;
213
+ if (overage > 0) console.log(col.d(` Over by: $${overage.toFixed(4)}`));
214
+ }
215
+ console.log(col.d(` Reset: occasio clear | Increase: restart with --budget N`));
216
+ }
217
+
218
+ // โ”€โ”€ Main entry renderer โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
219
+
220
+ const ET_LABEL = {
221
+ cloud_sent: col.c('[cloud_sent]'),
222
+ local_only: col.g('[local_only]'),
223
+ trimmed: col.y('[trimmed]'),
224
+ blocked: col.r('[blocked]'),
225
+ budget_exceeded: col.r('[budget_exceeded]'),
226
+ };
227
+
228
+ /**
229
+ * Print a full boundary manifest for one log entry.
230
+ * @param {object} entry Raw log entry
231
+ * @param {number} idxLabel 1-based display index
232
+ * @param {number} total Total entries in the view (for header)
233
+ */
234
+ function printBoundaryEntry(entry, idxLabel, total) {
235
+ const facts = buildBoundaryFacts(entry);
236
+ if (!facts) return;
237
+
238
+ const ts = entry.ts || (entry.iso ? new Date(entry.iso).toTimeString().slice(0, 8) : '?');
239
+ const model = (entry.model || '').replace('claude-', '').replace(/-\d{8}$/, '');
240
+ const label = ET_LABEL[facts.event_type] || col.d(`[${facts.event_type}]`);
241
+ const ofStr = total > 1 ? ` of ${total}` : '';
242
+
243
+ console.log(`\n${col.b('โšก Request ' + idxLabel + ofStr)} ${label} ${col.d(ts)} ${col.d(model)}`);
244
+ console.log(RULE);
245
+
246
+ switch (facts.event_type) {
247
+ case 'cloud_sent':
248
+ case 'trimmed':
249
+ renderCloudSent(facts);
250
+ break;
251
+ case 'local_only':
252
+ renderLocalOnly(facts);
253
+ break;
254
+ case 'blocked':
255
+ renderBlocked(facts);
256
+ break;
257
+ case 'budget_exceeded':
258
+ renderBudgetExceeded(facts);
259
+ break;
260
+ default:
261
+ console.log(col.d(` (unknown event type: ${facts.event_type})`));
262
+ }
263
+
264
+ console.log('\n' + RULE);
265
+ }
266
+
267
+ // โ”€โ”€ CLI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
268
+
269
+ function runInspectCli(args) {
270
+ let session = null;
271
+ try { session = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8')); } catch {}
272
+
273
+ const todayEntries = readDayLog(todayStr());
274
+
275
+ const scopeIdx = args.indexOf('--scope');
276
+ const scope = scopeIdx >= 0 ? (args[scopeIdx + 1] || 'session') : 'session';
277
+ const entries = scope === 'session'
278
+ ? readSessionEntries(todayEntries, session?.start)
279
+ : todayEntries;
280
+
281
+ if (!entries.length) {
282
+ console.log(col.d(`\n No log entries yet. Run: occasio claude\n`));
283
+ return;
284
+ }
285
+
286
+ // --entry N: single entry by 0-based index
287
+ const entryFlagIdx = args.indexOf('--entry');
288
+ if (entryFlagIdx >= 0) {
289
+ const n = parseInt(args[entryFlagIdx + 1]);
290
+ if (isNaN(n) || n < 0 || n >= entries.length) {
291
+ console.log(col.r(` No entry at index ${n} (${entries.length} total in scope).`));
292
+ return;
293
+ }
294
+ console.log(col.b(`\nโšก Occasio Inspect โ€” Entry ${n + 1} of ${entries.length}`));
295
+ printBoundaryEntry(entries[n], n + 1, entries.length);
296
+ return;
297
+ }
298
+
299
+ // --run <id-prefix>: all entries matching run_id prefix
300
+ const runFlagIdx = args.indexOf('--run');
301
+ if (runFlagIdx >= 0) {
302
+ const runFilter = args[runFlagIdx + 1] || '';
303
+ const runEntries = entries.filter(e => e.run_id && e.run_id.startsWith(runFilter));
304
+ if (!runEntries.length) {
305
+ console.log(col.d(`\n No entries for run: ${runFilter}\n`));
306
+ return;
307
+ }
308
+ const shortId = runFilter.slice(0, 8);
309
+ console.log(col.b(`\nโšก Occasio Inspect โ€” Run ${shortId} (${runEntries.length} event${runEntries.length === 1 ? '' : 's'})`));
310
+ runEntries.forEach((e, i) => printBoundaryEntry(e, i + 1, runEntries.length));
311
+ return;
312
+ }
313
+
314
+ // default / --last N: last N entries (default 1)
315
+ const lastFlagIdx = args.indexOf('--last');
316
+ const limit = lastFlagIdx >= 0 ? (parseInt(args[lastFlagIdx + 1]) || 1) : 1;
317
+ const slice = entries.slice(-limit);
318
+ const offset = entries.length - slice.length;
319
+
320
+ console.log(col.b(`\nโšก Occasio Inspect`));
321
+ console.log(col.d(` scope: ${scope} ยท ${entries.length} entr${entries.length === 1 ? 'y' : 'ies'} ยท showing last ${slice.length}\n`));
322
+
323
+ slice.forEach((e, i) => printBoundaryEntry(e, offset + i + 1, entries.length));
324
+
325
+ console.log(col.d(`\n --last N show last N entries --entry N inspect by index`));
326
+ console.log(col.d(` --run <id> inspect a specific run --scope today full-day scope\n`));
327
+ }
328
+
329
+ module.exports = { buildBoundaryFacts, printBoundaryEntry, runInspectCli };