@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,230 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Dispatcher — given a BoundaryEvent and a Decision, performs the action and
5
+ * returns a Result. Pure dispatch: knows no policy, no audit, no protocol.
6
+ *
7
+ * Stage 1 mapping:
8
+ * PASS → { passThrough: true } (caller forwards)
9
+ * LOCAL → executes via legacy native handlers, returns { output, exitCode }
10
+ * TRANSFORM → reserved (Stage 2: redact / distill paths)
11
+ * BLOCK → returns { blocked: true, response: decision.syntheticResponse }
12
+ *
13
+ * The LOCAL branch maps event.toolName to the legacy handler. Tool-name
14
+ * literals here are a Stage 1 leak: this is acknowledged in ARCHITECTURE.md
15
+ * and resolved in Stage 3 by introducing a canonical-name registry that the
16
+ * adapter populates.
17
+ */
18
+
19
+ const {
20
+ handleReadTool,
21
+ handleGlobTool,
22
+ handleGrepTool,
23
+ handleTodoWriteTool,
24
+ handleTodoReadTool,
25
+ } = require('../runtime');
26
+
27
+ const { scanSecrets, redactSecrets } = require('../analyzer');
28
+ const { distill } = require('../distiller');
29
+
30
+ const { nativeHandle, expandPsEnvVars, runLocally } = require('../interceptor');
31
+ const { CANONICAL } = require('../core/tool-names');
32
+
33
+ // Stage 3: NATIVE_HANDLERS keyed on canonical tool names. The dispatcher is
34
+ // agent-agnostic; agent-specific names live in adapters and are translated
35
+ // to canonical names at the BoundaryEvent boundary.
36
+ const NATIVE_HANDLERS = {
37
+ [CANONICAL.READ_FILE]: (input) => handleReadTool(input),
38
+ [CANONICAL.FIND_FILES]: (input) => handleGlobTool(input),
39
+ [CANONICAL.GREP]: (input) => handleGrepTool(input),
40
+ [CANONICAL.TODO_WRITE]: (input, ctx) => handleTodoWriteTool(input, ctx?.todoStore || []),
41
+ [CANONICAL.TODO_READ]: (input, ctx) => handleTodoReadTool(ctx?.todoStore || []),
42
+
43
+ // Bash: native handler first; if isInterceptable said routeLocally was OK
44
+ // but nativeHandle returned null, fall back to the exec subprocess. The
45
+ // returned `native` field tells the caller which path was taken.
46
+ [CANONICAL.SHELL_BASH]: async (input) => {
47
+ const cmd = (input?.command || '').trim();
48
+ if (!cmd) return null;
49
+ const nr = nativeHandle(cmd);
50
+ if (nr !== null) {
51
+ return { output: nr.output, exitCode: nr.exitCode, native: true };
52
+ }
53
+ const r = await runLocally(cmd);
54
+ const rawOutput = (r.stdout + (r.stderr ? `\nstderr: ${r.stderr}` : '')).trimEnd();
55
+ return {
56
+ output: rawOutput || `(exit ${r.exitCode})`,
57
+ exitCode: r.exitCode,
58
+ native: false,
59
+ };
60
+ },
61
+
62
+ // PowerShell: expand $env:VAR (mirrors isPowerShellNativeHandleable's check)
63
+ // then native-only execution. expandedCmd is returned so the caller can
64
+ // record the actually-executed command in toolsRun.
65
+ [CANONICAL.SHELL_POWERSHELL]: (input) => {
66
+ const rawCmd = (input?.command || '').trim();
67
+ if (!rawCmd) return null;
68
+ const cmd = expandPsEnvVars(rawCmd);
69
+ const nr = nativeHandle(cmd);
70
+ if (nr === null) {
71
+ return { output: '(not handled natively)', exitCode: 1, native: false, expandedCmd: cmd };
72
+ }
73
+ const rawOutput = nr.output.trimEnd() || `(exit ${nr.exitCode})`;
74
+ return { output: rawOutput, exitCode: nr.exitCode, native: true, expandedCmd: cmd };
75
+ },
76
+ };
77
+
78
+ /**
79
+ * Dispatch a Decision against an Event.
80
+ *
81
+ * @param {object} event BoundaryEvent
82
+ * @param {object} decision Decision
83
+ * @param {object} [ctx] { todoStore } per-session state
84
+ * @returns {Promise<object>} Result
85
+ */
86
+ async function dispatch(event, decision, ctx = {}) {
87
+ if (!decision) throw new Error('dispatcher.dispatch requires a Decision');
88
+
89
+ switch (decision.action) {
90
+ case 'PASS':
91
+ return { passThrough: true, reason: decision.reason };
92
+
93
+ case 'BLOCK':
94
+ return { blocked: true, response: decision.syntheticResponse, reason: decision.reason };
95
+
96
+ case 'TRANSFORM': {
97
+ // ── Chained transforms ─────────────────────────────────────────────────
98
+ // When the decision carries a `transforms` array (from TRANSFORM_CHAIN),
99
+ // execute the handler once and apply each transform in sequence. The
100
+ // output of one step becomes the input of the next. Security-first
101
+ // ordering (redact before distill) is enforced by the engine.
102
+ if (Array.isArray(decision.transforms) && decision.transforms.length > 1) {
103
+ const chainToolNames = require('../core/tool-names');
104
+ let chainHandler = NATIVE_HANDLERS[event.toolName];
105
+ if (!chainHandler && !chainToolNames.isCanonical(event.toolName)) {
106
+ const canonical = chainToolNames.firstCanonicalFor(event.toolName);
107
+ if (canonical) chainHandler = NATIVE_HANDLERS[canonical];
108
+ }
109
+ if (!chainHandler) {
110
+ return { passThrough: true, reason: `transform-chain-no-handler:${event.toolName}` };
111
+ }
112
+ const chainRaw = await chainHandler(event.toolInput, ctx);
113
+ if (!chainRaw) return { passThrough: true, reason: 'transform-chain-handler-returned-null' };
114
+ if (chainRaw.passThrough) return chainRaw;
115
+
116
+ let chainOutput = typeof chainRaw.output === 'string' ? chainRaw.output : String(chainRaw.output || '');
117
+ let chainSecretsRedacted = [];
118
+ let chainDistilled = false, chainSavedTokens = 0, chainLabel = null, chainRawContent = null;
119
+
120
+ for (const tf of decision.transforms) {
121
+ if (tf === 'redact-secrets') {
122
+ chainSecretsRedacted = scanSecrets(chainOutput);
123
+ chainOutput = redactSecrets(chainOutput);
124
+ } else if (tf === 'distill-output') {
125
+ const cmd = event.toolInput?.command || event.toolInput?.script || event.toolName || '';
126
+ const dr = distill(cmd, chainOutput);
127
+ chainOutput = dr.content;
128
+ chainDistilled = dr.distilled;
129
+ chainSavedTokens = dr.savedTokens;
130
+ chainLabel = dr.label;
131
+ chainRawContent = dr.rawContent;
132
+ }
133
+ // Unknown transform names: skip silently (forward-compatible).
134
+ }
135
+
136
+ return {
137
+ transformed: true,
138
+ transform: decision.transform, // 'redact-secrets+distill-output'
139
+ transforms: decision.transforms, // array for structured consumers
140
+ output: chainOutput,
141
+ secretsRedacted: chainSecretsRedacted.length ? chainSecretsRedacted : undefined,
142
+ distilled: chainDistilled,
143
+ savedTokens: chainSavedTokens,
144
+ label: chainLabel,
145
+ rawContent: chainRawContent,
146
+ exitCode: chainRaw.exitCode,
147
+ };
148
+ }
149
+
150
+ const { transform } = decision;
151
+ if (transform === 'redact-secrets') {
152
+ const toolNames = require('../core/tool-names');
153
+ let handler = NATIVE_HANDLERS[event.toolName];
154
+ if (!handler && !toolNames.isCanonical(event.toolName)) {
155
+ const canonical = toolNames.firstCanonicalFor(event.toolName);
156
+ if (canonical) handler = NATIVE_HANDLERS[canonical];
157
+ }
158
+ if (!handler) {
159
+ return { passThrough: true, reason: `transform-no-handler:${event.toolName}` };
160
+ }
161
+ const raw = await handler(event.toolInput, ctx);
162
+ if (!raw) return { passThrough: true, reason: 'transform-handler-returned-null' };
163
+ if (raw.passThrough) return raw;
164
+
165
+ const output = typeof raw.output === 'string' ? raw.output : String(raw.output || '');
166
+ const secretsRedacted = scanSecrets(output);
167
+ const redacted = redactSecrets(output);
168
+ return {
169
+ transformed: true,
170
+ transform,
171
+ output: redacted,
172
+ secretsRedacted,
173
+ exitCode: raw.exitCode,
174
+ };
175
+ }
176
+ if (transform === 'distill-output') {
177
+ const toolNames = require('../core/tool-names');
178
+ let handler = NATIVE_HANDLERS[event.toolName];
179
+ if (!handler && !toolNames.isCanonical(event.toolName)) {
180
+ const canonical = toolNames.firstCanonicalFor(event.toolName);
181
+ if (canonical) handler = NATIVE_HANDLERS[canonical];
182
+ }
183
+ if (!handler) {
184
+ return { passThrough: true, reason: `transform-no-handler:${event.toolName}` };
185
+ }
186
+ const raw = await handler(event.toolInput, ctx);
187
+ if (!raw) return { passThrough: true, reason: 'transform-handler-returned-null' };
188
+ if (raw.passThrough) return raw;
189
+
190
+ const output = typeof raw.output === 'string' ? raw.output : String(raw.output || '');
191
+ const cmd = event.toolInput?.command || event.toolInput?.script || event.toolName || '';
192
+ const dr = distill(cmd, output);
193
+ return {
194
+ transformed: true,
195
+ transform,
196
+ output: dr.content,
197
+ distilled: dr.distilled,
198
+ savedTokens: dr.savedTokens,
199
+ label: dr.label,
200
+ rawContent: dr.rawContent,
201
+ exitCode: raw.exitCode,
202
+ };
203
+ }
204
+
205
+ return { transformed: false, reason: `transform-not-implemented:${transform}` };
206
+ }
207
+
208
+ case 'LOCAL': {
209
+ // Direct canonical-name lookup. If the event's toolName is an
210
+ // agent-specific alias (e.g., a test passes 'Read'), reverse-resolve
211
+ // through the registry.
212
+ const toolNames = require('../core/tool-names');
213
+ let handler = NATIVE_HANDLERS[event.toolName];
214
+ if (!handler && !toolNames.isCanonical(event.toolName)) {
215
+ const canonical = toolNames.firstCanonicalFor(event.toolName);
216
+ if (canonical) handler = NATIVE_HANDLERS[canonical];
217
+ }
218
+ if (!handler) {
219
+ return { passThrough: true, reason: `unknown-local-tool:${event.toolName}` };
220
+ }
221
+ const out = await handler(event.toolInput, ctx);
222
+ return out || { passThrough: true, reason: 'handler-returned-null' };
223
+ }
224
+
225
+ default:
226
+ throw new Error(`dispatcher.dispatch: unknown decision.action ${decision.action}`);
227
+ }
228
+ }
229
+
230
+ module.exports = { dispatch, NATIVE_HANDLERS };