@intx/inference 0.1.2 → 0.3.0
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 +176 -0
- package/dist/actions.d.ts +16 -0
- package/dist/actions.js +200 -0
- package/dist/adapter.d.ts +40 -0
- package/dist/adapter.js +31 -0
- package/dist/assembly.d.ts +75 -0
- package/dist/assembly.js +133 -0
- package/dist/audit-collector.d.ts +10 -0
- package/dist/audit-collector.js +139 -0
- package/dist/auth.d.ts +24 -0
- package/{src/auth.ts → dist/auth.js} +13 -19
- package/dist/authz-extension.d.ts +46 -0
- package/dist/authz-extension.js +184 -0
- package/dist/correlation.d.ts +26 -0
- package/dist/correlation.js +39 -0
- package/dist/default-director.d.ts +111 -0
- package/dist/default-director.js +228 -0
- package/dist/director.d.ts +6 -0
- package/dist/director.js +56 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.js +83 -0
- package/dist/gates.d.ts +28 -0
- package/dist/gates.js +103 -0
- package/dist/harness.d.ts +147 -0
- package/dist/harness.js +1407 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +21 -0
- package/dist/manifest.d.ts +31 -0
- package/dist/manifest.js +44 -0
- package/dist/providers/anthropic.d.ts +37 -0
- package/dist/providers/anthropic.js +917 -0
- package/dist/providers/google-genai-files.d.ts +48 -0
- package/dist/providers/google-genai-files.js +205 -0
- package/dist/providers/google-genai.d.ts +5 -0
- package/dist/providers/google-genai.js +1205 -0
- package/dist/providers/index.d.ts +38 -0
- package/dist/providers/index.js +56 -0
- package/dist/providers/openai.d.ts +9 -0
- package/dist/providers/openai.js +903 -0
- package/dist/reactor.d.ts +50 -0
- package/dist/reactor.js +1233 -0
- package/dist/retry-policy.d.ts +31 -0
- package/{src/retry-policy.ts → dist/retry-policy.js} +41 -53
- package/dist/sse.d.ts +1 -0
- package/dist/sse.js +63 -0
- package/dist/state.d.ts +23 -0
- package/dist/state.js +100 -0
- package/dist/tool-name.d.ts +6 -0
- package/dist/tool-name.js +110 -0
- package/dist/transform.d.ts +11 -0
- package/dist/transform.js +132 -0
- package/dist/transforms/index.d.ts +2 -0
- package/dist/transforms/index.js +1 -0
- package/dist/transforms/size-cap.d.ts +12 -0
- package/dist/transforms/size-cap.js +80 -0
- package/dist/turns.d.ts +21 -0
- package/dist/turns.js +135 -0
- package/package.json +22 -6
- package/src/actions.ts +0 -245
- package/src/adapter.ts +0 -57
- package/src/assembly.test.ts +0 -728
- package/src/assembly.ts +0 -250
- package/src/audit-collector.test.ts +0 -332
- package/src/audit-collector.ts +0 -172
- package/src/auth.test.ts +0 -117
- package/src/authz-extension.test.ts +0 -269
- package/src/authz-extension.ts +0 -145
- package/src/correlation.ts +0 -61
- package/src/default-director.test.ts +0 -314
- package/src/default-director.ts +0 -344
- package/src/director.ts +0 -87
- package/src/errors.test.ts +0 -133
- package/src/errors.ts +0 -115
- package/src/gates.ts +0 -128
- package/src/harness.test.ts +0 -655
- package/src/harness.ts +0 -1571
- package/src/index.ts +0 -76
- package/src/providers/anthropic.test.ts +0 -771
- package/src/providers/anthropic.ts +0 -810
- package/src/providers/google-genai-files.ts +0 -289
- package/src/providers/google-genai.ts +0 -1518
- package/src/providers/openai.ts +0 -719
- package/src/providers/registry.ts +0 -33
- package/src/reactor.test.ts +0 -3660
- package/src/reactor.ts +0 -1058
- package/src/scheduler.test.ts +0 -41
- package/src/sse.test.ts +0 -133
- package/src/sse.ts +0 -76
- package/src/state.ts +0 -135
- package/src/transform.test.ts +0 -207
- package/src/transform.ts +0 -159
- package/src/transforms/index.ts +0 -2
- package/src/transforms/size-cap.test.ts +0 -172
- package/src/transforms/size-cap.ts +0 -110
- package/src/turns.ts +0 -54
- package/tsconfig.json +0 -4
- package/tsconfig.tsbuildinfo +0 -1
package/src/assembly.ts
DELETED
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
// Reactor assembly helper.
|
|
2
|
-
//
|
|
3
|
-
// `createReactorAssembly` is the canonical way to construct a reactor when the
|
|
4
|
-
// caller wants the standard wiring: a default size-cap tool-result transform,
|
|
5
|
-
// authz as a before-tool extension, an audit collector that flushes at
|
|
6
|
-
// checkpoint and shutdown boundaries, and a `BlobReader` over the supplied
|
|
7
|
-
// context store. Future reactor consumers (the harness today; in-process agent
|
|
8
|
-
// runtimes tomorrow) should use this helper rather than calling `createReactor`
|
|
9
|
-
// directly so the wiring stays consistent across composition points.
|
|
10
|
-
|
|
11
|
-
import { getLogger } from "@intx/log";
|
|
12
|
-
import {
|
|
13
|
-
createBlobReader,
|
|
14
|
-
type BlobReader,
|
|
15
|
-
type AuditStore,
|
|
16
|
-
type BeforeToolExtension,
|
|
17
|
-
type Compactor,
|
|
18
|
-
type ContextStore,
|
|
19
|
-
type ContextTransform,
|
|
20
|
-
type InferenceSource,
|
|
21
|
-
type ReactorDirector,
|
|
22
|
-
type ToolResultTransform,
|
|
23
|
-
type ToolRunner,
|
|
24
|
-
} from "@intx/types/runtime";
|
|
25
|
-
|
|
26
|
-
import { createAuditCollector, type AuditCollector } from "./audit-collector";
|
|
27
|
-
import {
|
|
28
|
-
createAuthzExtension,
|
|
29
|
-
type AuthzExtensionOptions,
|
|
30
|
-
} from "./authz-extension";
|
|
31
|
-
import type { CorrelationValidator } from "./correlation";
|
|
32
|
-
import { createDefaultDependencies, type Dependencies } from "./harness";
|
|
33
|
-
import {
|
|
34
|
-
createReactor,
|
|
35
|
-
type Reactor,
|
|
36
|
-
type ReactorConfig,
|
|
37
|
-
type ReactorEmittedEvent,
|
|
38
|
-
} from "./reactor";
|
|
39
|
-
import { createSizeCapTransform } from "./transforms";
|
|
40
|
-
|
|
41
|
-
const logger = getLogger(["interchange", "assembly"]);
|
|
42
|
-
|
|
43
|
-
const DEFAULT_SIZE_CAP_MAX_CHARS = 10_000;
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Configuration for `createReactorAssembly`. Required fields mirror
|
|
47
|
-
* `ReactorConfig`. Optional fields toggle the composed extensions: the helper
|
|
48
|
-
* builds an authz before-tool extension when `authorize` is supplied, builds
|
|
49
|
-
* and wires an audit collector when `auditStore` is supplied, and always
|
|
50
|
-
* prepends a default size-cap tool-result transform.
|
|
51
|
-
*
|
|
52
|
-
* The helper does NOT wrap the supplied `contextStore`; callers that need
|
|
53
|
-
* additional behavior (the harness wraps for connector-thread state) layer
|
|
54
|
-
* that on themselves before passing the store in.
|
|
55
|
-
*/
|
|
56
|
-
export type ReactorAssemblyConfig = {
|
|
57
|
-
sessionId: string;
|
|
58
|
-
director: ReactorDirector;
|
|
59
|
-
source: InferenceSource;
|
|
60
|
-
toolRunner: ToolRunner;
|
|
61
|
-
contextStore: ContextStore;
|
|
62
|
-
onEvent: (event: ReactorEmittedEvent) => void;
|
|
63
|
-
|
|
64
|
-
authorize?: AuthzExtensionOptions["authorize"];
|
|
65
|
-
auditStore?: AuditStore;
|
|
66
|
-
beforeToolExtensions?: BeforeToolExtension[];
|
|
67
|
-
toolResultTransforms?: ToolResultTransform[];
|
|
68
|
-
contextTransforms?: ContextTransform[];
|
|
69
|
-
compactors?: Record<string, Compactor>;
|
|
70
|
-
sizeCapMaxChars?: number;
|
|
71
|
-
|
|
72
|
-
afterCheckpoint?: () => Promise<void>;
|
|
73
|
-
onShutdown?: () => Promise<void>;
|
|
74
|
-
|
|
75
|
-
deps?: Dependencies;
|
|
76
|
-
correlationValidator?: CorrelationValidator;
|
|
77
|
-
inferenceRunner?: ReactorConfig["inferenceRunner"];
|
|
78
|
-
gateTimeout?: number;
|
|
79
|
-
shutdownTimeoutMs?: number;
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Output of `createReactorAssembly`. The `reactor` is started by the caller as
|
|
84
|
-
* usual. `blobReader` is exposed so the caller can pass it to tool factories
|
|
85
|
-
* that resolve `tool-output:///{callId}` URIs against the same context store
|
|
86
|
-
* the reactor commits to. `auditCollector` is `undefined` when no `auditStore`
|
|
87
|
-
* was supplied.
|
|
88
|
-
*/
|
|
89
|
-
export type ReactorAssembly = {
|
|
90
|
-
reactor: Reactor;
|
|
91
|
-
blobReader: BlobReader;
|
|
92
|
-
auditCollector: AuditCollector | undefined;
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Build the standard reactor wiring. This is the canonical reactor-assembly
|
|
97
|
-
* path: any consumer that needs the default size-cap transform, authz, audit
|
|
98
|
-
* collection, and blob reader should call this helper instead of constructing
|
|
99
|
-
* a `ReactorConfig` by hand. Direct `createReactor` use is reserved for
|
|
100
|
-
* reactor-internal tests and any future consumer that genuinely needs a
|
|
101
|
-
* different composition.
|
|
102
|
-
*/
|
|
103
|
-
export function createReactorAssembly(
|
|
104
|
-
config: ReactorAssemblyConfig,
|
|
105
|
-
): ReactorAssembly {
|
|
106
|
-
const {
|
|
107
|
-
sessionId,
|
|
108
|
-
director,
|
|
109
|
-
source,
|
|
110
|
-
toolRunner,
|
|
111
|
-
contextStore,
|
|
112
|
-
onEvent,
|
|
113
|
-
authorize,
|
|
114
|
-
auditStore,
|
|
115
|
-
beforeToolExtensions: callerBeforeToolExtensions,
|
|
116
|
-
toolResultTransforms: callerToolResultTransforms,
|
|
117
|
-
contextTransforms,
|
|
118
|
-
compactors,
|
|
119
|
-
sizeCapMaxChars,
|
|
120
|
-
afterCheckpoint: callerAfterCheckpoint,
|
|
121
|
-
onShutdown: callerOnShutdown,
|
|
122
|
-
deps,
|
|
123
|
-
correlationValidator,
|
|
124
|
-
inferenceRunner,
|
|
125
|
-
gateTimeout,
|
|
126
|
-
shutdownTimeoutMs,
|
|
127
|
-
} = config;
|
|
128
|
-
|
|
129
|
-
// Audit collector is created up-front so the authz extension can route its
|
|
130
|
-
// decisions through `onDecision`. When no auditStore is supplied, no
|
|
131
|
-
// collector is created and authz runs without decision recording.
|
|
132
|
-
const auditCollector: AuditCollector | undefined =
|
|
133
|
-
auditStore !== undefined ? createAuditCollector(sessionId) : undefined;
|
|
134
|
-
|
|
135
|
-
// When an audit collector is present, intercept the reactor's event stream
|
|
136
|
-
// to feed it tool.start / tool.done events (the collector correlates these
|
|
137
|
-
// with authz decisions by callId). message.received is reactor-internal and
|
|
138
|
-
// is forwarded to the caller but not to the collector. Without a collector,
|
|
139
|
-
// the caller's onEvent is used directly.
|
|
140
|
-
const composedOnEvent =
|
|
141
|
-
auditCollector !== undefined
|
|
142
|
-
? (event: ReactorEmittedEvent) => {
|
|
143
|
-
if (event.type !== "message.received") {
|
|
144
|
-
auditCollector.onEvent(event);
|
|
145
|
-
}
|
|
146
|
-
onEvent(event);
|
|
147
|
-
}
|
|
148
|
-
: onEvent;
|
|
149
|
-
|
|
150
|
-
// Authz is composed in front of any caller-supplied before-tool extensions
|
|
151
|
-
// so policy enforcement runs first. Without authz, the caller's list (if
|
|
152
|
-
// any) is passed through unchanged.
|
|
153
|
-
const authzExtension =
|
|
154
|
-
authorize !== undefined
|
|
155
|
-
? createAuthzExtension({
|
|
156
|
-
authorize,
|
|
157
|
-
...(auditCollector !== undefined
|
|
158
|
-
? { onDecision: (d) => auditCollector.onDecision(d) }
|
|
159
|
-
: {}),
|
|
160
|
-
})
|
|
161
|
-
: undefined;
|
|
162
|
-
|
|
163
|
-
const composedBeforeToolExtensions: BeforeToolExtension[] | undefined =
|
|
164
|
-
authzExtension !== undefined
|
|
165
|
-
? [authzExtension, ...(callerBeforeToolExtensions ?? [])]
|
|
166
|
-
: callerBeforeToolExtensions;
|
|
167
|
-
|
|
168
|
-
// The size-cap transform is always prepended so oversized payloads spill
|
|
169
|
-
// before any caller transform sees them. Caller transforms run after and
|
|
170
|
-
// can rely on the inline content already being bounded.
|
|
171
|
-
const sizeCapTransform = createSizeCapTransform({
|
|
172
|
-
maxChars: sizeCapMaxChars ?? DEFAULT_SIZE_CAP_MAX_CHARS,
|
|
173
|
-
contextStore,
|
|
174
|
-
});
|
|
175
|
-
const composedToolResultTransforms: ToolResultTransform[] = [
|
|
176
|
-
sizeCapTransform,
|
|
177
|
-
...(callerToolResultTransforms ?? []),
|
|
178
|
-
];
|
|
179
|
-
|
|
180
|
-
// Audit flush wraps the caller's lifecycle hooks: the helper's flush runs
|
|
181
|
-
// first so the records produced by the just-completed cycle are persisted
|
|
182
|
-
// before the caller's hook observes the checkpoint or shutdown boundary.
|
|
183
|
-
async function flushAudit(): Promise<void> {
|
|
184
|
-
if (auditCollector === undefined || auditStore === undefined) return;
|
|
185
|
-
const records = auditCollector.flush();
|
|
186
|
-
if (records.length > 0) {
|
|
187
|
-
await auditStore.commitAudit(records);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const composedAfterCheckpoint: (() => Promise<void>) | undefined =
|
|
192
|
-
auditCollector !== undefined
|
|
193
|
-
? async () => {
|
|
194
|
-
await flushAudit();
|
|
195
|
-
if (callerAfterCheckpoint !== undefined) {
|
|
196
|
-
await callerAfterCheckpoint();
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
: callerAfterCheckpoint;
|
|
200
|
-
|
|
201
|
-
const composedOnShutdown: (() => Promise<void>) | undefined =
|
|
202
|
-
auditCollector !== undefined
|
|
203
|
-
? async () => {
|
|
204
|
-
const inflight = auditCollector.pending();
|
|
205
|
-
if (inflight > 0) {
|
|
206
|
-
logger.warn`${inflight} audit records in flight at shutdown, these tool calls will not be recorded`;
|
|
207
|
-
}
|
|
208
|
-
await flushAudit();
|
|
209
|
-
if (callerOnShutdown !== undefined) {
|
|
210
|
-
await callerOnShutdown();
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
: callerOnShutdown;
|
|
214
|
-
|
|
215
|
-
// ReactorConfig.deps is required; default to createDefaultDependencies()
|
|
216
|
-
// when the caller did not supply one so we never pass `undefined`.
|
|
217
|
-
const resolvedDeps: Dependencies = deps ?? createDefaultDependencies();
|
|
218
|
-
|
|
219
|
-
// exactOptionalPropertyTypes is on: only set optional keys when defined.
|
|
220
|
-
const reactorConfig: ReactorConfig = {
|
|
221
|
-
sessionId,
|
|
222
|
-
director,
|
|
223
|
-
source,
|
|
224
|
-
toolRunner,
|
|
225
|
-
contextStore,
|
|
226
|
-
onEvent: composedOnEvent,
|
|
227
|
-
deps: resolvedDeps,
|
|
228
|
-
toolResultTransforms: composedToolResultTransforms,
|
|
229
|
-
...(composedBeforeToolExtensions !== undefined
|
|
230
|
-
? { beforeToolExtensions: composedBeforeToolExtensions }
|
|
231
|
-
: {}),
|
|
232
|
-
...(contextTransforms !== undefined ? { contextTransforms } : {}),
|
|
233
|
-
...(compactors !== undefined ? { compactors } : {}),
|
|
234
|
-
...(composedAfterCheckpoint !== undefined
|
|
235
|
-
? { afterCheckpoint: composedAfterCheckpoint }
|
|
236
|
-
: {}),
|
|
237
|
-
...(composedOnShutdown !== undefined
|
|
238
|
-
? { onShutdown: composedOnShutdown }
|
|
239
|
-
: {}),
|
|
240
|
-
...(correlationValidator !== undefined ? { correlationValidator } : {}),
|
|
241
|
-
...(inferenceRunner !== undefined ? { inferenceRunner } : {}),
|
|
242
|
-
...(gateTimeout !== undefined ? { gateTimeout } : {}),
|
|
243
|
-
...(shutdownTimeoutMs !== undefined ? { shutdownTimeoutMs } : {}),
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
const reactor = createReactor(reactorConfig);
|
|
247
|
-
const blobReader = createBlobReader(contextStore);
|
|
248
|
-
|
|
249
|
-
return { reactor, blobReader, auditCollector };
|
|
250
|
-
}
|
|
@@ -1,332 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import { createAuditCollector } from "./audit-collector";
|
|
4
|
-
import type { AuthzDecision } from "./authz-extension";
|
|
5
|
-
import type { InferenceEvent } from "@intx/types/runtime";
|
|
6
|
-
|
|
7
|
-
function makeDecision(overrides: Partial<AuthzDecision> = {}): AuthzDecision {
|
|
8
|
-
return {
|
|
9
|
-
callId: "call-1",
|
|
10
|
-
tool: "bash",
|
|
11
|
-
resource: "tool:bash",
|
|
12
|
-
action: "invoke",
|
|
13
|
-
effect: "allow",
|
|
14
|
-
resolvedBy: null,
|
|
15
|
-
matchingGrants: [],
|
|
16
|
-
blocked: false,
|
|
17
|
-
blockReason: undefined,
|
|
18
|
-
error: undefined,
|
|
19
|
-
...overrides,
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function toolStart(
|
|
24
|
-
callId: string,
|
|
25
|
-
name: string,
|
|
26
|
-
args: Record<string, unknown> = {},
|
|
27
|
-
seq = 1,
|
|
28
|
-
): InferenceEvent {
|
|
29
|
-
return {
|
|
30
|
-
type: "tool.start",
|
|
31
|
-
seq,
|
|
32
|
-
data: { call: { id: callId, name, arguments: args } },
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function toolDone(
|
|
37
|
-
callId: string,
|
|
38
|
-
content: string | Record<string, unknown>,
|
|
39
|
-
seq = 2,
|
|
40
|
-
isError = false,
|
|
41
|
-
): InferenceEvent {
|
|
42
|
-
return {
|
|
43
|
-
type: "tool.done",
|
|
44
|
-
seq,
|
|
45
|
-
data: { result: { callId, content, isError } },
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
describe("createAuditCollector", () => {
|
|
50
|
-
test("collects a complete allowed tool invocation", () => {
|
|
51
|
-
const collector = createAuditCollector("session-1");
|
|
52
|
-
|
|
53
|
-
collector.onDecision(makeDecision({ callId: "c1" }));
|
|
54
|
-
collector.onEvent(toolStart("c1", "bash", { cmd: "ls" }));
|
|
55
|
-
collector.onEvent(toolDone("c1", "file.txt", 5));
|
|
56
|
-
|
|
57
|
-
const records = collector.flush();
|
|
58
|
-
expect(records.length).toBe(1);
|
|
59
|
-
const r = records[0];
|
|
60
|
-
if (r === undefined) throw new Error("expected record");
|
|
61
|
-
|
|
62
|
-
expect(r.callId).toBe("c1");
|
|
63
|
-
expect(r.tool).toBe("bash");
|
|
64
|
-
expect(r.arguments).toEqual({ cmd: "ls" });
|
|
65
|
-
expect(r.result.content).toBe("file.txt");
|
|
66
|
-
expect(r.result.isError).toBe(false);
|
|
67
|
-
expect(r.sessionId).toBe("session-1");
|
|
68
|
-
expect(r.seq).toBe(5);
|
|
69
|
-
expect(r.authz).not.toBeNull();
|
|
70
|
-
if (r.authz === null) throw new Error("expected authz");
|
|
71
|
-
expect(r.authz.effect).toBe("allow");
|
|
72
|
-
expect(r.authz.blocked).toBe(false);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test("collects a blocked tool invocation (no tool.start)", () => {
|
|
76
|
-
const collector = createAuditCollector("session-1");
|
|
77
|
-
|
|
78
|
-
collector.onDecision(
|
|
79
|
-
makeDecision({
|
|
80
|
-
callId: "c1",
|
|
81
|
-
effect: "deny",
|
|
82
|
-
blocked: true,
|
|
83
|
-
blockReason: "Denied by policy: tool:bash/invoke",
|
|
84
|
-
}),
|
|
85
|
-
);
|
|
86
|
-
collector.onEvent(
|
|
87
|
-
toolDone("c1", "Denied by policy: tool:bash/invoke", 3, true),
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
const records = collector.flush();
|
|
91
|
-
expect(records.length).toBe(1);
|
|
92
|
-
const r = records[0];
|
|
93
|
-
if (r === undefined) throw new Error("expected record");
|
|
94
|
-
|
|
95
|
-
expect(r.callId).toBe("c1");
|
|
96
|
-
expect(r.tool).toBe("bash");
|
|
97
|
-
expect(r.result.isError).toBe(true);
|
|
98
|
-
expect(r.result.content).toBe("Denied by policy: tool:bash/invoke");
|
|
99
|
-
expect(r.authz).not.toBeNull();
|
|
100
|
-
if (r.authz === null) throw new Error("expected authz");
|
|
101
|
-
expect(r.authz.effect).toBe("deny");
|
|
102
|
-
expect(r.authz.blocked).toBe(true);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test("collects tool invocation without authz extension", () => {
|
|
106
|
-
const collector = createAuditCollector("session-1");
|
|
107
|
-
|
|
108
|
-
collector.onEvent(toolStart("c1", "curl", { url: "http://x" }));
|
|
109
|
-
collector.onEvent(toolDone("c1", { status: 200 }, 4));
|
|
110
|
-
|
|
111
|
-
const records = collector.flush();
|
|
112
|
-
expect(records.length).toBe(1);
|
|
113
|
-
const r = records[0];
|
|
114
|
-
if (r === undefined) throw new Error("expected record");
|
|
115
|
-
|
|
116
|
-
expect(r.authz).toBeNull();
|
|
117
|
-
expect(r.result.content).toEqual({ status: 200 });
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test("handles parallel tool calls with same name", () => {
|
|
121
|
-
const collector = createAuditCollector("session-1");
|
|
122
|
-
|
|
123
|
-
collector.onDecision(makeDecision({ callId: "c1", tool: "bash" }));
|
|
124
|
-
collector.onDecision(makeDecision({ callId: "c2", tool: "bash" }));
|
|
125
|
-
collector.onEvent(toolStart("c1", "bash", { cmd: "ls" }, 1));
|
|
126
|
-
collector.onEvent(toolStart("c2", "bash", { cmd: "pwd" }, 2));
|
|
127
|
-
collector.onEvent(toolDone("c1", "/home", 3));
|
|
128
|
-
collector.onEvent(toolDone("c2", "/tmp", 4));
|
|
129
|
-
|
|
130
|
-
const records = collector.flush();
|
|
131
|
-
expect(records.length).toBe(2);
|
|
132
|
-
|
|
133
|
-
const r1 = records.find((r) => r.callId === "c1");
|
|
134
|
-
const r2 = records.find((r) => r.callId === "c2");
|
|
135
|
-
if (r1 === undefined || r2 === undefined)
|
|
136
|
-
throw new Error("expected both records");
|
|
137
|
-
|
|
138
|
-
expect(r1.arguments).toEqual({ cmd: "ls" });
|
|
139
|
-
expect(r1.result.content).toBe("/home");
|
|
140
|
-
expect(r2.arguments).toEqual({ cmd: "pwd" });
|
|
141
|
-
expect(r2.result.content).toBe("/tmp");
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
test("parallel mixed batch: one allowed, one blocked", () => {
|
|
145
|
-
const collector = createAuditCollector("session-1");
|
|
146
|
-
|
|
147
|
-
collector.onDecision(
|
|
148
|
-
makeDecision({ callId: "c1", effect: "allow", blocked: false }),
|
|
149
|
-
);
|
|
150
|
-
collector.onDecision(
|
|
151
|
-
makeDecision({
|
|
152
|
-
callId: "c2",
|
|
153
|
-
effect: "deny",
|
|
154
|
-
blocked: true,
|
|
155
|
-
blockReason: "denied",
|
|
156
|
-
}),
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
collector.onEvent(toolStart("c1", "bash", {}, 1));
|
|
160
|
-
collector.onEvent(toolDone("c2", "denied", 2, true));
|
|
161
|
-
collector.onEvent(toolDone("c1", "ok", 3));
|
|
162
|
-
|
|
163
|
-
const records = collector.flush();
|
|
164
|
-
expect(records.length).toBe(2);
|
|
165
|
-
|
|
166
|
-
const allowed = records.find((r) => r.callId === "c1");
|
|
167
|
-
const blocked = records.find((r) => r.callId === "c2");
|
|
168
|
-
if (allowed === undefined || blocked === undefined)
|
|
169
|
-
throw new Error("expected both");
|
|
170
|
-
|
|
171
|
-
expect(allowed.result.isError).toBe(false);
|
|
172
|
-
expect(blocked.result.isError).toBe(true);
|
|
173
|
-
if (blocked.authz === null) throw new Error("expected authz");
|
|
174
|
-
expect(blocked.authz.blocked).toBe(true);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test("flush clears completed records", () => {
|
|
178
|
-
const collector = createAuditCollector("session-1");
|
|
179
|
-
|
|
180
|
-
collector.onEvent(toolStart("c1", "bash", {}, 1));
|
|
181
|
-
collector.onEvent(toolDone("c1", "ok", 2));
|
|
182
|
-
|
|
183
|
-
const first = collector.flush();
|
|
184
|
-
expect(first.length).toBe(1);
|
|
185
|
-
|
|
186
|
-
const second = collector.flush();
|
|
187
|
-
expect(second.length).toBe(0);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
test("flush does not return in-flight records", () => {
|
|
191
|
-
const collector = createAuditCollector("session-1");
|
|
192
|
-
|
|
193
|
-
collector.onDecision(makeDecision({ callId: "c1" }));
|
|
194
|
-
collector.onEvent(toolStart("c1", "bash", {}, 1));
|
|
195
|
-
// No tool.done yet
|
|
196
|
-
|
|
197
|
-
const records = collector.flush();
|
|
198
|
-
expect(records.length).toBe(0);
|
|
199
|
-
expect(collector.pending()).toBe(1);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
test("pending counts buffered decisions and in-flight records", () => {
|
|
203
|
-
const collector = createAuditCollector("session-1");
|
|
204
|
-
expect(collector.pending()).toBe(0);
|
|
205
|
-
|
|
206
|
-
collector.onDecision(makeDecision({ callId: "c1" }));
|
|
207
|
-
expect(collector.pending()).toBe(1);
|
|
208
|
-
|
|
209
|
-
collector.onEvent(toolStart("c1", "bash", {}, 1));
|
|
210
|
-
expect(collector.pending()).toBe(1);
|
|
211
|
-
|
|
212
|
-
collector.onEvent(toolDone("c1", "ok", 2));
|
|
213
|
-
expect(collector.pending()).toBe(0);
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
test("records across multiple flush cycles accumulate correctly", () => {
|
|
217
|
-
const collector = createAuditCollector("session-1");
|
|
218
|
-
|
|
219
|
-
collector.onEvent(toolStart("c1", "bash", {}, 1));
|
|
220
|
-
collector.onEvent(toolDone("c1", "a", 2));
|
|
221
|
-
expect(collector.flush().length).toBe(1);
|
|
222
|
-
|
|
223
|
-
collector.onEvent(toolStart("c2", "curl", {}, 3));
|
|
224
|
-
collector.onEvent(toolDone("c2", "b", 4));
|
|
225
|
-
collector.onEvent(toolStart("c3", "node", {}, 5));
|
|
226
|
-
collector.onEvent(toolDone("c3", "c", 6));
|
|
227
|
-
expect(collector.flush().length).toBe(2);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
test("authz decision with resolvedBy and matchingGrants", () => {
|
|
231
|
-
const collector = createAuditCollector("session-1");
|
|
232
|
-
|
|
233
|
-
collector.onDecision(
|
|
234
|
-
makeDecision({
|
|
235
|
-
callId: "c1",
|
|
236
|
-
effect: "allow",
|
|
237
|
-
resolvedBy: {
|
|
238
|
-
id: "grant-1",
|
|
239
|
-
resource: "tool:bash",
|
|
240
|
-
action: "invoke",
|
|
241
|
-
effect: "allow",
|
|
242
|
-
origin: "creator",
|
|
243
|
-
specificity: 1009,
|
|
244
|
-
},
|
|
245
|
-
matchingGrants: [
|
|
246
|
-
{
|
|
247
|
-
id: "grant-1",
|
|
248
|
-
resource: "tool:bash",
|
|
249
|
-
action: "invoke",
|
|
250
|
-
effect: "allow",
|
|
251
|
-
origin: "creator",
|
|
252
|
-
specificity: 1009,
|
|
253
|
-
},
|
|
254
|
-
],
|
|
255
|
-
}),
|
|
256
|
-
);
|
|
257
|
-
collector.onEvent(toolStart("c1", "bash", {}, 1));
|
|
258
|
-
collector.onEvent(toolDone("c1", "ok", 2));
|
|
259
|
-
|
|
260
|
-
const records = collector.flush();
|
|
261
|
-
const r = records[0];
|
|
262
|
-
if (r === undefined) throw new Error("expected record");
|
|
263
|
-
if (r.authz === null) throw new Error("expected authz");
|
|
264
|
-
if (r.authz.resolvedBy === undefined || r.authz.resolvedBy === null)
|
|
265
|
-
throw new Error("expected resolvedBy");
|
|
266
|
-
|
|
267
|
-
expect(r.authz.resolvedBy.id).toBe("grant-1");
|
|
268
|
-
expect(r.authz.resolvedBy.specificity).toBe(1009);
|
|
269
|
-
expect(r.authz.matchingGrants.length).toBe(1);
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
test("ignores non-tool events", () => {
|
|
273
|
-
const collector = createAuditCollector("session-1");
|
|
274
|
-
|
|
275
|
-
collector.onEvent({
|
|
276
|
-
type: "inference.start",
|
|
277
|
-
seq: 1,
|
|
278
|
-
data: { model: "test" },
|
|
279
|
-
});
|
|
280
|
-
collector.onEvent({
|
|
281
|
-
type: "reactor.start",
|
|
282
|
-
seq: 2,
|
|
283
|
-
data: {},
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
expect(collector.flush().length).toBe(0);
|
|
287
|
-
expect(collector.pending()).toBe(0);
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
test("emits degraded record on orphaned tool.done with no prior context", () => {
|
|
291
|
-
const collector = createAuditCollector("session-1");
|
|
292
|
-
|
|
293
|
-
collector.onEvent(toolDone("unknown-call", "result", 1));
|
|
294
|
-
|
|
295
|
-
const records = collector.flush();
|
|
296
|
-
expect(records.length).toBe(1);
|
|
297
|
-
const r = records[0];
|
|
298
|
-
if (r === undefined) throw new Error("expected record");
|
|
299
|
-
|
|
300
|
-
expect(r.callId).toBe("unknown-call");
|
|
301
|
-
expect(r.tool).toBe("$orphaned");
|
|
302
|
-
expect(r.arguments).toEqual({});
|
|
303
|
-
expect(r.authz).toBeNull();
|
|
304
|
-
expect(r.result.content).toBe("result");
|
|
305
|
-
expect(r.sessionId).toBe("session-1");
|
|
306
|
-
expect(r.seq).toBe(1);
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
test("orphaned tool.done propagates isError into degraded record", () => {
|
|
310
|
-
const collector = createAuditCollector("session-1");
|
|
311
|
-
|
|
312
|
-
collector.onEvent(toolDone("orphan-err", "denied", 1, true));
|
|
313
|
-
|
|
314
|
-
const records = collector.flush();
|
|
315
|
-
expect(records.length).toBe(1);
|
|
316
|
-
const r = records[0];
|
|
317
|
-
if (r === undefined) throw new Error("expected record");
|
|
318
|
-
|
|
319
|
-
expect(r.tool).toBe("$orphaned");
|
|
320
|
-
expect(r.result.isError).toBe(true);
|
|
321
|
-
expect(r.result.content).toBe("denied");
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
test("orphaned tool.done does not leak into pending count", () => {
|
|
325
|
-
const collector = createAuditCollector("session-1");
|
|
326
|
-
|
|
327
|
-
collector.onEvent(toolDone("orphan-1", "result", 1));
|
|
328
|
-
|
|
329
|
-
expect(collector.pending()).toBe(0);
|
|
330
|
-
expect(collector.flush().length).toBe(1);
|
|
331
|
-
});
|
|
332
|
-
});
|