@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.
Files changed (97) hide show
  1. package/LICENSE +176 -0
  2. package/dist/actions.d.ts +16 -0
  3. package/dist/actions.js +200 -0
  4. package/dist/adapter.d.ts +40 -0
  5. package/dist/adapter.js +31 -0
  6. package/dist/assembly.d.ts +75 -0
  7. package/dist/assembly.js +133 -0
  8. package/dist/audit-collector.d.ts +10 -0
  9. package/dist/audit-collector.js +139 -0
  10. package/dist/auth.d.ts +24 -0
  11. package/{src/auth.ts → dist/auth.js} +13 -19
  12. package/dist/authz-extension.d.ts +46 -0
  13. package/dist/authz-extension.js +184 -0
  14. package/dist/correlation.d.ts +26 -0
  15. package/dist/correlation.js +39 -0
  16. package/dist/default-director.d.ts +111 -0
  17. package/dist/default-director.js +228 -0
  18. package/dist/director.d.ts +6 -0
  19. package/dist/director.js +56 -0
  20. package/dist/errors.d.ts +18 -0
  21. package/dist/errors.js +83 -0
  22. package/dist/gates.d.ts +28 -0
  23. package/dist/gates.js +103 -0
  24. package/dist/harness.d.ts +147 -0
  25. package/dist/harness.js +1407 -0
  26. package/dist/index.d.ts +37 -0
  27. package/dist/index.js +21 -0
  28. package/dist/manifest.d.ts +31 -0
  29. package/dist/manifest.js +44 -0
  30. package/dist/providers/anthropic.d.ts +37 -0
  31. package/dist/providers/anthropic.js +917 -0
  32. package/dist/providers/google-genai-files.d.ts +48 -0
  33. package/dist/providers/google-genai-files.js +205 -0
  34. package/dist/providers/google-genai.d.ts +5 -0
  35. package/dist/providers/google-genai.js +1205 -0
  36. package/dist/providers/index.d.ts +38 -0
  37. package/dist/providers/index.js +56 -0
  38. package/dist/providers/openai.d.ts +9 -0
  39. package/dist/providers/openai.js +903 -0
  40. package/dist/reactor.d.ts +50 -0
  41. package/dist/reactor.js +1233 -0
  42. package/dist/retry-policy.d.ts +31 -0
  43. package/{src/retry-policy.ts → dist/retry-policy.js} +41 -53
  44. package/dist/sse.d.ts +1 -0
  45. package/dist/sse.js +63 -0
  46. package/dist/state.d.ts +23 -0
  47. package/dist/state.js +100 -0
  48. package/dist/tool-name.d.ts +6 -0
  49. package/dist/tool-name.js +110 -0
  50. package/dist/transform.d.ts +11 -0
  51. package/dist/transform.js +132 -0
  52. package/dist/transforms/index.d.ts +2 -0
  53. package/dist/transforms/index.js +1 -0
  54. package/dist/transforms/size-cap.d.ts +12 -0
  55. package/dist/transforms/size-cap.js +80 -0
  56. package/dist/turns.d.ts +21 -0
  57. package/dist/turns.js +135 -0
  58. package/package.json +22 -6
  59. package/src/actions.ts +0 -245
  60. package/src/adapter.ts +0 -57
  61. package/src/assembly.test.ts +0 -728
  62. package/src/assembly.ts +0 -250
  63. package/src/audit-collector.test.ts +0 -332
  64. package/src/audit-collector.ts +0 -172
  65. package/src/auth.test.ts +0 -117
  66. package/src/authz-extension.test.ts +0 -269
  67. package/src/authz-extension.ts +0 -145
  68. package/src/correlation.ts +0 -61
  69. package/src/default-director.test.ts +0 -314
  70. package/src/default-director.ts +0 -344
  71. package/src/director.ts +0 -87
  72. package/src/errors.test.ts +0 -133
  73. package/src/errors.ts +0 -115
  74. package/src/gates.ts +0 -128
  75. package/src/harness.test.ts +0 -655
  76. package/src/harness.ts +0 -1571
  77. package/src/index.ts +0 -76
  78. package/src/providers/anthropic.test.ts +0 -771
  79. package/src/providers/anthropic.ts +0 -810
  80. package/src/providers/google-genai-files.ts +0 -289
  81. package/src/providers/google-genai.ts +0 -1518
  82. package/src/providers/openai.ts +0 -719
  83. package/src/providers/registry.ts +0 -33
  84. package/src/reactor.test.ts +0 -3660
  85. package/src/reactor.ts +0 -1058
  86. package/src/scheduler.test.ts +0 -41
  87. package/src/sse.test.ts +0 -133
  88. package/src/sse.ts +0 -76
  89. package/src/state.ts +0 -135
  90. package/src/transform.test.ts +0 -207
  91. package/src/transform.ts +0 -159
  92. package/src/transforms/index.ts +0 -2
  93. package/src/transforms/size-cap.test.ts +0 -172
  94. package/src/transforms/size-cap.ts +0 -110
  95. package/src/turns.ts +0 -54
  96. package/tsconfig.json +0 -4
  97. 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
- });