@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
|
@@ -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 };
|