@graphorin/agent 0.6.0 → 0.7.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 (110) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +31 -11
  3. package/dist/errors/index.d.ts +17 -4
  4. package/dist/errors/index.d.ts.map +1 -1
  5. package/dist/errors/index.js +19 -3
  6. package/dist/errors/index.js.map +1 -1
  7. package/dist/factory.d.ts.map +1 -1
  8. package/dist/factory.js +153 -1930
  9. package/dist/factory.js.map +1 -1
  10. package/dist/fanout/index.d.ts +13 -1
  11. package/dist/fanout/index.d.ts.map +1 -1
  12. package/dist/fanout/index.js +13 -4
  13. package/dist/fanout/index.js.map +1 -1
  14. package/dist/index.d.ts +7 -6
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +11 -9
  17. package/dist/index.js.map +1 -1
  18. package/dist/lateral-leak/index.js +1 -1
  19. package/dist/package.js +6 -0
  20. package/dist/package.js.map +1 -0
  21. package/dist/run-state/index.d.ts +32 -6
  22. package/dist/run-state/index.d.ts.map +1 -1
  23. package/dist/run-state/index.js +46 -22
  24. package/dist/run-state/index.js.map +1 -1
  25. package/dist/runtime/agent-surface.js +122 -0
  26. package/dist/runtime/agent-surface.js.map +1 -0
  27. package/dist/runtime/agent-to-tool.d.ts +51 -0
  28. package/dist/runtime/agent-to-tool.d.ts.map +1 -0
  29. package/dist/runtime/agent-to-tool.js +145 -0
  30. package/dist/runtime/agent-to-tool.js.map +1 -0
  31. package/dist/runtime/approvals.js +0 -0
  32. package/dist/runtime/approvals.js.map +1 -0
  33. package/dist/runtime/dispatch.js +108 -0
  34. package/dist/runtime/dispatch.js.map +1 -0
  35. package/dist/runtime/executor-wiring.js +128 -0
  36. package/dist/runtime/executor-wiring.js.map +1 -0
  37. package/dist/runtime/fallback-chain.js +139 -0
  38. package/dist/runtime/fallback-chain.js.map +1 -0
  39. package/dist/runtime/handoff.js +307 -0
  40. package/dist/runtime/handoff.js.map +1 -0
  41. package/dist/runtime/messages.d.ts +22 -0
  42. package/dist/runtime/messages.d.ts.map +1 -0
  43. package/dist/runtime/messages.js +204 -0
  44. package/dist/runtime/messages.js.map +1 -0
  45. package/dist/runtime/provider-events.js +117 -0
  46. package/dist/runtime/provider-events.js.map +1 -0
  47. package/dist/runtime/run-compaction.js +210 -0
  48. package/dist/runtime/run-compaction.js.map +1 -0
  49. package/dist/runtime/run-finish.js +48 -0
  50. package/dist/runtime/run-finish.js.map +1 -0
  51. package/dist/runtime/run-gates.js +336 -0
  52. package/dist/runtime/run-gates.js.map +1 -0
  53. package/dist/runtime/run-init.js +81 -0
  54. package/dist/runtime/run-init.js.map +1 -0
  55. package/dist/runtime/run-input.js +46 -0
  56. package/dist/runtime/run-input.js.map +1 -0
  57. package/dist/runtime/step-catalogue.js +173 -0
  58. package/dist/runtime/step-catalogue.js.map +1 -0
  59. package/dist/runtime/tool-call-walk.js +189 -0
  60. package/dist/runtime/tool-call-walk.js.map +1 -0
  61. package/dist/runtime/tool-wiring.js +159 -0
  62. package/dist/runtime/tool-wiring.js.map +1 -0
  63. package/dist/tooling/adapters.js +1 -1
  64. package/dist/tooling/dataflow.js +1 -1
  65. package/dist/tooling/policy.js +2 -0
  66. package/dist/tooling/policy.js.map +1 -1
  67. package/dist/types.d.ts +63 -13
  68. package/dist/types.d.ts.map +1 -1
  69. package/package.json +20 -20
  70. package/src/errors/index.ts +320 -0
  71. package/src/evaluator-optimizer/index.ts +212 -0
  72. package/src/factory.ts +957 -0
  73. package/src/fallback/index.ts +108 -0
  74. package/src/fanout/index.ts +523 -0
  75. package/src/filters/index.ts +347 -0
  76. package/src/index.ts +180 -0
  77. package/src/internal/ids.ts +46 -0
  78. package/src/internal/usage-accumulator.ts +90 -0
  79. package/src/lateral-leak/causality-monitor.ts +221 -0
  80. package/src/lateral-leak/index.ts +35 -0
  81. package/src/lateral-leak/merge-guard.ts +151 -0
  82. package/src/lateral-leak/protocol-guard.ts +222 -0
  83. package/src/preferred-model/index.ts +210 -0
  84. package/src/progress/index.ts +238 -0
  85. package/src/run-state/index.ts +607 -0
  86. package/src/runtime/agent-surface.ts +218 -0
  87. package/src/runtime/agent-to-tool.ts +323 -0
  88. package/src/runtime/approvals.ts +0 -0
  89. package/src/runtime/dispatch.ts +183 -0
  90. package/src/runtime/executor-wiring.ts +331 -0
  91. package/src/runtime/fallback-chain.ts +250 -0
  92. package/src/runtime/handoff.ts +428 -0
  93. package/src/runtime/messages.ts +309 -0
  94. package/src/runtime/provider-events.ts +175 -0
  95. package/src/runtime/run-compaction.ts +288 -0
  96. package/src/runtime/run-finish.ts +93 -0
  97. package/src/runtime/run-gates.ts +419 -0
  98. package/src/runtime/run-init.ts +169 -0
  99. package/src/runtime/run-input.ts +102 -0
  100. package/src/runtime/step-catalogue.ts +338 -0
  101. package/src/runtime/tool-call-walk.ts +301 -0
  102. package/src/runtime/tool-wiring.ts +218 -0
  103. package/src/testing/replay-provider.ts +121 -0
  104. package/src/tooling/adapters.ts +403 -0
  105. package/src/tooling/catalogue.ts +36 -0
  106. package/src/tooling/dataflow.ts +171 -0
  107. package/src/tooling/plan.ts +123 -0
  108. package/src/tooling/policy.ts +67 -0
  109. package/src/tooling/registry-build.ts +191 -0
  110. package/src/types.ts +696 -0
@@ -0,0 +1,403 @@
1
+ /**
2
+ * Integration adapters (A-E) that let the `@graphorin/tools` executor
3
+ * consume the agent's `@graphorin/security` / `@graphorin/memory` /
4
+ * `@graphorin/provider` surfaces.
5
+ *
6
+ * These are built and unit-tested in isolation (WI-01). The run loop
7
+ * wires them into `createToolExecutor(...)` in a later work item
8
+ * (WI-03); nothing here is exported from the package entrypoint yet.
9
+ *
10
+ * Every adapter is typed against the executor's `ExecutorOptions` via
11
+ * indexed access, so the values they produce are guaranteed assignable
12
+ * to the hooks `createToolExecutor(...)` expects - without depending on
13
+ * which individual hook types `@graphorin/tools` happens to re-export.
14
+ *
15
+ * @packageDocumentation
16
+ */
17
+
18
+ import type { Memory } from '@graphorin/memory';
19
+ import {
20
+ type ApiBoundaryGuardOptions,
21
+ type AuditOnlyGuardOptions,
22
+ createGuard,
23
+ type StrictFullGuardOptions,
24
+ } from '@graphorin/security/guard';
25
+ import {
26
+ createDockerSandbox,
27
+ createIsolatedVMSandbox,
28
+ createWorkerThreadsSandbox,
29
+ type SandboxImpl,
30
+ } from '@graphorin/security/sandbox';
31
+ import { parseSecretRef, resolveSecret } from '@graphorin/security/secrets';
32
+ import type { ExecutorOptions } from '@graphorin/tools/executor';
33
+ import { countTokensHeuristic } from '@graphorin/tools/result';
34
+
35
+ // --- Executor hook shapes (derived from `ExecutorOptions`) ------------------
36
+ // Indexed access keeps these in lock-step with what the executor expects.
37
+
38
+ /** The executor's secret-resolver hook: `{ resolve(key): Promise<SecretValue | null> }`. */
39
+ type SecretResolver = NonNullable<ExecutorOptions['secretResolver']>;
40
+ /** A resolved `SecretValue`, or `null` when the backing store has no value. */
41
+ type SecretValueOrNull = Awaited<ReturnType<SecretResolver['resolve']>>;
42
+ /** The executor's sandbox-dispatch resolver: `(policy) => Sandbox | null`. */
43
+ type SandboxResolver = NonNullable<ExecutorOptions['sandboxResolver']>;
44
+ /** The executor's memory-guard factory: `(tier) => MemoryModificationGuard | null`. */
45
+ type MemoryGuardFactory = NonNullable<ExecutorOptions['memoryGuardFactory']>;
46
+ /** The discriminator the guard factory receives (`'pure' | … | 'untrusted'`). */
47
+ type MemoryGuardTier = Parameters<MemoryGuardFactory>[0];
48
+ /** A guard instance, or `null` when the tier needs no guard / cannot be built. */
49
+ type MemoryModificationGuardOrNull = ReturnType<MemoryGuardFactory>;
50
+ /** The reader the guard hashes pre/post execution: `{ regions; read(region) }`. */
51
+ type MemoryRegionReader = NonNullable<ExecutorOptions['memoryRegionReader']>;
52
+ /** The executor's **synchronous** truncation token counter: `{ count(text): number }`. */
53
+ type ToolTokenCounter = NonNullable<ExecutorOptions['tokenCounter']>;
54
+ /** A value the executor's `streamingSink` receives (a `tool.execute.*` event). */
55
+ type ExecutorEvent = Parameters<NonNullable<ExecutorOptions['streamingSink']>>[0];
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Adapter A - `secretResolver`
59
+ // ---------------------------------------------------------------------------
60
+
61
+ /**
62
+ * Backend that turns a secret reference into a resolved value (or
63
+ * `null` when the backing store represents "absent" as null). Defaults
64
+ * to `@graphorin/security`'s global resolver registry
65
+ * (`resolveSecret(parseSecretRef(key))`), which **rejects** on a
66
+ * malformed ref / unknown scheme / resolution failure and never returns
67
+ * null - those rejections surface as tool errors through the executor's
68
+ * secrets accessor.
69
+ */
70
+ export type SecretBackend = (key: string) => Promise<SecretValueOrNull>;
71
+
72
+ /** Options for {@link buildSecretResolver}. */
73
+ export interface SecretResolverOptions {
74
+ /** Resolution backend. Defaults to `resolveSecret(parseSecretRef(key))`. */
75
+ readonly backend?: SecretBackend;
76
+ }
77
+
78
+ /**
79
+ * Adapter A - build the executor's `secretResolver` hook.
80
+ *
81
+ * This is a thin **value resolver**. The executor's secrets accessor
82
+ * (`@graphorin/tools` `tool-context.ts`) already (1) enforces the
83
+ * per-tool `secretsAllowed` ACL via `enforceSecretAcl(key)` inside a
84
+ * `withChildToolSecretsContext(...)` scope and (2) maps a `null` result
85
+ * onto the optional/required-secret error contract. This adapter must
86
+ * therefore **not** re-implement ACL - it only resolves a key to a
87
+ * value.
88
+ */
89
+ export function buildSecretResolver(options: SecretResolverOptions = {}): SecretResolver {
90
+ const backend: SecretBackend =
91
+ options.backend ?? (async (key) => resolveSecret(parseSecretRef(key)));
92
+ return Object.freeze({ resolve: backend });
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Adapter B - `sandboxResolver`
97
+ // ---------------------------------------------------------------------------
98
+
99
+ /** Isolation kinds the default resolver knows how to construct. */
100
+ type IsolatedKind = 'worker-threads' | 'isolated-vm' | 'docker';
101
+
102
+ /** Factory producing a concrete sandbox for a given isolation kind. */
103
+ export type SandboxFactory = () => SandboxImpl;
104
+
105
+ /** Per-kind sandbox factory overrides (e.g. fakes in tests, custom adapters). */
106
+ export type SandboxFactoryMap = Partial<Record<IsolatedKind, SandboxFactory>>;
107
+
108
+ /** Options for {@link buildSandboxResolver}. */
109
+ export interface SandboxResolverOptions {
110
+ /**
111
+ * Per-kind factory overrides. Defaults wire the three out-of-process
112
+ * `@graphorin/security` adapters. Tests inject fakes so unit runs
113
+ * never spawn real worker threads / containers (offline; R4). The
114
+ * factories are invoked **lazily** - never at build time - so the
115
+ * production default costs nothing until a tool actually needs that
116
+ * isolation kind.
117
+ */
118
+ readonly factories?: SandboxFactoryMap;
119
+ }
120
+
121
+ /**
122
+ * Adapter B - build the executor's `sandboxResolver`.
123
+ *
124
+ * Maps a `ResolvedSandboxPolicy.kind` to a cached `SandboxImpl`,
125
+ * lazily constructing **one instance per kind**. Returns `null` for
126
+ * `'none'` (run the tool inline) and for any kind without a registered
127
+ * factory (custom kinds need a custom resolver).
128
+ */
129
+ export function buildSandboxResolver(options: SandboxResolverOptions = {}): SandboxResolver {
130
+ const factories: Record<IsolatedKind, SandboxFactory> = {
131
+ 'worker-threads': options.factories?.['worker-threads'] ?? (() => createWorkerThreadsSandbox()),
132
+ 'isolated-vm': options.factories?.['isolated-vm'] ?? (() => createIsolatedVMSandbox()),
133
+ docker: options.factories?.docker ?? (() => createDockerSandbox()),
134
+ };
135
+ // Look up factories by the (open) policy kind. `SandboxKind` carries a
136
+ // `(string & {})` member that defeats literal narrowing, so we index a
137
+ // string-keyed view: a miss (incl. `'none'` and custom kinds) runs inline.
138
+ const lookup = factories as Record<string, SandboxFactory | undefined>;
139
+ const cache = new Map<string, SandboxImpl>();
140
+
141
+ return (policy) => {
142
+ const kind = policy.kind;
143
+ const factory = lookup[kind];
144
+ if (factory === undefined) {
145
+ return null;
146
+ }
147
+ let impl = cache.get(kind);
148
+ if (impl === undefined) {
149
+ impl = factory();
150
+ cache.set(kind, impl);
151
+ }
152
+ return impl;
153
+ };
154
+ }
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // Adapter C - `memoryGuardFactory` + `memoryRegionReader`
158
+ // ---------------------------------------------------------------------------
159
+
160
+ /** Options for {@link buildMemoryGuard}. */
161
+ export interface MemoryGuardOptions {
162
+ /**
163
+ * Options for the `'memory-aware'` API-boundary guard. The guard
164
+ * requires an operation allowlist + a recorder of observed
165
+ * `<scope>.<op>` calls, both supplied by the runtime once a run scope
166
+ * exists (WI-03). When omitted, the factory returns `null` for the
167
+ * `'memory-aware'` tier and the executor skips the guard.
168
+ */
169
+ readonly apiBoundary?: ApiBoundaryGuardOptions;
170
+ /** Options for the `'unknown'` (audit-only) guard. */
171
+ readonly auditOnly?: AuditOnlyGuardOptions;
172
+ /** Options for the `'untrusted'` (strict-full) guard. */
173
+ readonly strictFull?: StrictFullGuardOptions;
174
+ /**
175
+ * The region reader the guard hashes pre/post execution. The reader
176
+ * is **scope-bound** (it materialises memory regions for a specific
177
+ * run scope), so the runtime supplies it in WI-03 - the `Memory`
178
+ * facade exposes no scope-free read surface. When omitted, the
179
+ * executor skips the snapshot/verify cycle (it only runs the guard
180
+ * when the reader is present).
181
+ */
182
+ readonly regionReader?: MemoryRegionReader;
183
+ }
184
+
185
+ /** The executor wiring produced by {@link buildMemoryGuard}. */
186
+ export interface MemoryGuardWiring {
187
+ readonly memoryGuardFactory: MemoryGuardFactory;
188
+ readonly memoryRegionReader?: MemoryRegionReader;
189
+ }
190
+
191
+ /**
192
+ * Adapter C - build the executor's `memoryGuardFactory` (+ an optional
193
+ * `memoryRegionReader`) from the agent's configured `Memory`.
194
+ *
195
+ * When `memory` is `undefined`, the factory returns `null` for every
196
+ * tier and no reader is supplied - the executor degrades to its
197
+ * audit-only baseline (the guard step is skipped).
198
+ */
199
+ export function buildMemoryGuard(
200
+ memory: Memory | undefined,
201
+ options: MemoryGuardOptions = {},
202
+ ): MemoryGuardWiring {
203
+ if (memory === undefined) {
204
+ return Object.freeze({ memoryGuardFactory: () => null });
205
+ }
206
+
207
+ const memoryGuardFactory: MemoryGuardFactory = (
208
+ tier: MemoryGuardTier,
209
+ ): MemoryModificationGuardOrNull => {
210
+ switch (tier) {
211
+ case 'pure':
212
+ case 'side-effecting-no-memory':
213
+ return createGuard({ tier });
214
+ case 'unknown':
215
+ return createGuard({ tier, ...(options.auditOnly ?? {}) });
216
+ case 'untrusted':
217
+ return createGuard({ tier, ...(options.strictFull ?? {}) });
218
+ case 'memory-aware':
219
+ return options.apiBoundary === undefined
220
+ ? null
221
+ : createGuard({ tier, ...options.apiBoundary });
222
+ default:
223
+ return null;
224
+ }
225
+ };
226
+
227
+ return Object.freeze(
228
+ options.regionReader === undefined
229
+ ? { memoryGuardFactory }
230
+ : { memoryGuardFactory, memoryRegionReader: options.regionReader },
231
+ );
232
+ }
233
+
234
+ /**
235
+ * Construct a `MemoryRegionReader` from an explicit region list and a
236
+ * read function. WI-03 uses this to bind the agent's `Memory` tiers to
237
+ * named regions once a run scope exists; exposed here so the reader
238
+ * contract is unit-testable in isolation.
239
+ */
240
+ export function createMemoryRegionReader(
241
+ regions: ReadonlyArray<string>,
242
+ read: (region: string) => Promise<Uint8Array | string>,
243
+ ): MemoryRegionReader {
244
+ return Object.freeze({ regions: Object.freeze([...regions]), read });
245
+ }
246
+
247
+ // ---------------------------------------------------------------------------
248
+ // Adapter D - `tokenCounter` (the sync/async impedance mismatch)
249
+ // ---------------------------------------------------------------------------
250
+
251
+ /** A synchronous tokenizer (e.g. js-tiktoken's `encode`) returning a token array. */
252
+ export type SyncTokenize = (text: string) => { readonly length: number };
253
+
254
+ /** Options for {@link buildToolTokenCounter}. */
255
+ export interface ToolTokenCounterOptions {
256
+ /**
257
+ * Optional synchronous tokenizer. When provided, the token count is
258
+ * `tokenize(text).length`. Defaults to the `@graphorin/tools`
259
+ * heuristic (4 chars/token).
260
+ */
261
+ readonly tokenize?: SyncTokenize;
262
+ }
263
+
264
+ /**
265
+ * Adapter D - build the **synchronous** token counter the executor's
266
+ * truncation pipeline requires.
267
+ *
268
+ * DESIGN DECISION (impedance mismatch §1.6.1): `@graphorin/tools`'
269
+ * `TokenCounter.count(text): number` is synchronous, while
270
+ * `@graphorin/provider`'s `createDefaultCounter().count(input): Promise<number>`
271
+ * is asynchronous (and also accepts `Message[]`). The truncation path
272
+ * runs inside `executeBatch` and must not `await`, so we never wire the
273
+ * provider's async counter here. We default to the tools heuristic
274
+ * (deterministic, offline) and allow a *synchronous* tokenizer (e.g.
275
+ * js-tiktoken's `encode`) to be injected when a caller wants
276
+ * vendor-accurate counts without blocking. The async provider counter
277
+ * stays reserved for budget accounting elsewhere (WI-09), not for
278
+ * executor truncation.
279
+ */
280
+ export function buildToolTokenCounter(options: ToolTokenCounterOptions = {}): ToolTokenCounter {
281
+ const { tokenize } = options;
282
+ if (tokenize === undefined) {
283
+ return countTokensHeuristic;
284
+ }
285
+ return Object.freeze({
286
+ count: (text: string): number => tokenize(text).length,
287
+ });
288
+ }
289
+
290
+ // ---------------------------------------------------------------------------
291
+ // Adapter E - `streamingSink` → `AgentEvent` stream bridge
292
+ // ---------------------------------------------------------------------------
293
+
294
+ /** Options for {@link createExecutorEventBridge}. */
295
+ export interface ExecutorEventBridgeOptions {
296
+ /**
297
+ * Maximum buffered events before backpressure drops the **oldest**
298
+ * event (preserving the most-recent window). Defaults to `256`, matching
299
+ * the executor's `streamingEventQueueDepth` default. Under normal
300
+ * operation the consumer drains faster than the producer and nothing
301
+ * is ever dropped; this bound is a safety valve.
302
+ */
303
+ readonly queueDepth?: number;
304
+ }
305
+
306
+ /**
307
+ * Bridges the executor's synchronous `streamingSink` callback into an
308
+ * async iterable the run loop can `for await` over.
309
+ *
310
+ * Single-producer / single-consumer: `sink` is the producer (called by
311
+ * the executor); one `drain()` iterator is the consumer (the loop). It
312
+ * is generic over the event type (defaulting to the executor's
313
+ * `ExecutorEvent`) so the queue mechanics can be exercised in isolation.
314
+ */
315
+ export interface ExecutorEventBridge<T = ExecutorEvent> {
316
+ /**
317
+ * Synchronous sink handed to the executor's `streamingSink`. Never
318
+ * throws and never blocks; events emitted after {@link close} are
319
+ * dropped.
320
+ */
321
+ readonly sink: (event: T) => void;
322
+ /**
323
+ * Async iterable yielding events in arrival order. Completes once
324
+ * {@link close} has been called **and** the buffer is drained.
325
+ */
326
+ drain(): AsyncIterableIterator<T>;
327
+ /** Signal end-of-stream; `drain()` finishes after the buffer drains. */
328
+ close(): void;
329
+ /** Count of events dropped under backpressure (oldest-dropped). */
330
+ readonly dropped: number;
331
+ }
332
+
333
+ /**
334
+ * Adapter E - bridge the executor's `streamingSink` callback to an
335
+ * async iterator via a bounded queue.
336
+ */
337
+ export function createExecutorEventBridge<T = ExecutorEvent>(
338
+ options: ExecutorEventBridgeOptions = {},
339
+ ): ExecutorEventBridge<T> {
340
+ const queueDepth = options.queueDepth ?? 256;
341
+ const buffer: T[] = [];
342
+ let pending: ((result: IteratorResult<T, undefined>) => void) | null = null;
343
+ let closed = false;
344
+ let dropped = 0;
345
+
346
+ const sink = (event: T): void => {
347
+ if (closed) {
348
+ return;
349
+ }
350
+ if (pending !== null) {
351
+ // A consumer is parked - hand the event off directly.
352
+ const resolve = pending;
353
+ pending = null;
354
+ resolve({ value: event, done: false });
355
+ return;
356
+ }
357
+ buffer.push(event);
358
+ if (buffer.length > queueDepth) {
359
+ buffer.shift();
360
+ dropped += 1;
361
+ }
362
+ };
363
+
364
+ async function* drain(): AsyncIterableIterator<T> {
365
+ for (;;) {
366
+ if (buffer.length > 0) {
367
+ const event = buffer.shift();
368
+ if (event !== undefined) {
369
+ yield event;
370
+ }
371
+ continue;
372
+ }
373
+ if (closed) {
374
+ return;
375
+ }
376
+ const next = await new Promise<IteratorResult<T, undefined>>((resolve) => {
377
+ pending = resolve;
378
+ });
379
+ if (next.done === true) {
380
+ return;
381
+ }
382
+ yield next.value;
383
+ }
384
+ }
385
+
386
+ const close = (): void => {
387
+ closed = true;
388
+ if (pending !== null) {
389
+ const resolve = pending;
390
+ pending = null;
391
+ resolve({ value: undefined, done: true });
392
+ }
393
+ };
394
+
395
+ return Object.freeze({
396
+ sink,
397
+ drain,
398
+ close,
399
+ get dropped(): number {
400
+ return dropped;
401
+ },
402
+ });
403
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * A7 (SOTA): prompt-cache-aware tool-catalogue ordering.
3
+ *
4
+ * The per-step catalogue is `[...eagerTools, ...promotedTools, ...handoffTools]`.
5
+ * The eager prefix is already stable (the registry lists in insertion order), so
6
+ * the only source of churn is the promoted section: building it from the
7
+ * registry's `listDeferred` order means a tool promoted on a later step can sort
8
+ * BEFORE one promoted earlier, shifting the serialized suffix and invalidating
9
+ * the provider's prompt-cache prefix on every subsequent step.
10
+ *
11
+ * `orderPromotedTools` instead emits promoted tools in PROMOTION order (the
12
+ * insertion order of the promoted-name set), so a newly promoted tool always
13
+ * joins the END - the catalogue grows append-only and earlier promotions keep
14
+ * their byte position. A long-running assistant re-pays only for the new tail.
15
+ *
16
+ * @packageDocumentation
17
+ */
18
+
19
+ /**
20
+ * Resolve `promotedNames` (insertion-ordered) to the matching `deferred`
21
+ * entries, preserving promotion order. Names absent from the pool (e.g. a tool
22
+ * removed mid-run) are skipped.
23
+ */
24
+ export function orderPromotedTools<T extends { readonly name: string }>(
25
+ promotedNames: Iterable<string>,
26
+ deferred: ReadonlyArray<T>,
27
+ ): ReadonlyArray<T> {
28
+ const byName = new Map<string, T>();
29
+ for (const tool of deferred) byName.set(tool.name, tool);
30
+ const out: T[] = [];
31
+ for (const name of promotedNames) {
32
+ const tool = byName.get(name);
33
+ if (tool !== undefined) out.push(tool);
34
+ }
35
+ return out;
36
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Adapter: build the executor's {@link DataFlowGuard} from a
3
+ * {@link DataFlowPolicyConfig} (WI-12 / P1-3).
4
+ *
5
+ * Bridges `@graphorin/security`'s pure taint engine (policy + per-run
6
+ * ledger + provenance derivation) to the `@graphorin/tools` executor hook.
7
+ * The executor calls {@link DataFlowGuard.inspect} as a sink gate and
8
+ * {@link DataFlowGuard.record} after every successful output; this adapter
9
+ * routes each call to the right run's {@link TaintLedger} and maps the
10
+ * security {@link DataFlowDecision} onto the executor's
11
+ * {@link DataFlowVerdict} (so the executor takes no security dependency).
12
+ *
13
+ * Because the agent builds one executor that serves the whole run (and the
14
+ * code-mode quiet executor too), the guard keeps a per-run ledger map.
15
+ * The live ledger (with its verbatim-probe spans) is in-memory and
16
+ * run-scoped, but its summary survives suspend/resume: the agent persists
17
+ * `snapshotLedger()` onto `RunState.taintSummary` on every exit and
18
+ * re-seeds via `seedLedger()` on resume (AG-19 / agent-08). The summary
19
+ * carries the coarse flags plus ONE-WAY span-tile hashes (C6) - never
20
+ * untrusted text - so the verbatim probe re-arms for pre-suspend content
21
+ * at tile granularity while live spans keep full sensitivity. The map is bounded by
22
+ * insertion-order eviction so a long-lived agent never leaks ledgers.
23
+ *
24
+ * @packageDocumentation
25
+ */
26
+
27
+ import {
28
+ createDataFlowPolicy,
29
+ createTaintLedger,
30
+ type DataFlowPolicyConfig,
31
+ deriveTaintLabel,
32
+ type TaintLedger,
33
+ type TaintLedgerSnapshot,
34
+ } from '@graphorin/security/dataflow';
35
+ import { containsPii } from '@graphorin/security/guardrails';
36
+ import type {
37
+ DataFlowGuard,
38
+ DataFlowInspectInput,
39
+ DataFlowRecordInput,
40
+ DataFlowVerdict,
41
+ } from '@graphorin/tools/executor';
42
+
43
+ /** Max concurrent run ledgers retained before evicting the oldest. */
44
+ const MAX_TRACKED_RUNS = 128;
45
+
46
+ /** Serialize tool-call args to text for the verbatim-carry probe. */
47
+ function stringifyArgs(value: unknown): string {
48
+ if (typeof value === 'string') return value;
49
+ try {
50
+ return JSON.stringify(value) ?? '';
51
+ } catch {
52
+ return '';
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Build a {@link DataFlowGuard} backed by `@graphorin/security`'s taint
58
+ * engine. Returns a guard whose `inspect`/`record` operate on a per-run
59
+ * ledger keyed by `runContext.runId`.
60
+ *
61
+ * @internal
62
+ */
63
+ /**
64
+ * The executor's {@link DataFlowGuard} plus AG-19 ledger-persistence hooks the
65
+ * agent uses to carry the coarse taint summary across a suspend/resume.
66
+ */
67
+ export interface DataFlowGuardWithLedgers extends DataFlowGuard {
68
+ /** Read a run's coarse taint summary to persist on suspend (or `undefined`). */
69
+ snapshotLedger(runId: string): TaintLedgerSnapshot | undefined;
70
+ /** Pre-seed a run's ledger from a persisted summary on resume. */
71
+ seedLedger(runId: string, summary: TaintLedgerSnapshot): void;
72
+ /**
73
+ * C6: record the model's own step output as derived-untrusted (no-op on
74
+ * an untainted run). Makes the verbatim probe catch args the model
75
+ * copied from its own untrusted-derived phrasing, and feeds the
76
+ * `derivedTaint: 'strict'` policy leg.
77
+ */
78
+ recordAssistant(runId: string, text: string): void;
79
+ }
80
+
81
+ export function buildDataFlowGuard(config: DataFlowPolicyConfig): DataFlowGuardWithLedgers {
82
+ const policy = createDataFlowPolicy(config);
83
+ // FIDES-lattice (SDF-8): when treatPiiAsSensitive is on, feed the PII catalogue
84
+ // into the ledger so PII reads arm the trifecta `sensitive` leg. Default off.
85
+ const ledgerOpts = {
86
+ ...(config.minSpanLength !== undefined ? { minSpanLength: config.minSpanLength } : {}),
87
+ ...(config.treatPiiAsSensitive === true ? { piiSensitivity: containsPii } : {}),
88
+ };
89
+ const ledgers = new Map<string, TaintLedger>();
90
+
91
+ function ledgerFor(runId: string): TaintLedger {
92
+ const existing = ledgers.get(runId);
93
+ if (existing !== undefined) return existing;
94
+ const ledger = createTaintLedger(ledgerOpts);
95
+ ledgers.set(runId, ledger);
96
+ while (ledgers.size > MAX_TRACKED_RUNS) {
97
+ const oldest = ledgers.keys().next().value;
98
+ if (oldest === undefined) break;
99
+ ledgers.delete(oldest);
100
+ }
101
+ return ledger;
102
+ }
103
+
104
+ return {
105
+ inspect(input: DataFlowInspectInput): DataFlowVerdict {
106
+ const ledger = ledgerFor(input.runContext.runId);
107
+ const probe = ledger.inspectArgs(stringifyArgs(input.args));
108
+ const decision = policy.evaluate({
109
+ toolName: input.toolName,
110
+ sideEffectClass: input.sideEffectClass,
111
+ carriesUntrustedVerbatim: probe.carriesUntrustedVerbatim,
112
+ untrustedSeen: ledger.untrustedSeen,
113
+ sensitiveSeen: ledger.sensitiveSeen,
114
+ sourceKinds:
115
+ probe.matchedSourceKinds.length > 0
116
+ ? probe.matchedSourceKinds
117
+ : ledger.untrustedSourceKinds,
118
+ });
119
+ if (decision.action === 'allow') return { action: 'allow' };
120
+ return {
121
+ action: decision.action,
122
+ flow: decision.flow,
123
+ reason: decision.reason,
124
+ sourceKinds: decision.sourceKinds,
125
+ };
126
+ },
127
+
128
+ record(input: DataFlowRecordInput): void {
129
+ const ledger = ledgerFor(input.runContext.runId);
130
+ const derived = deriveTaintLabel({
131
+ trustClass: input.trustClass,
132
+ ...(input.source !== undefined ? { source: input.source } : {}),
133
+ ...(input.sensitivity !== undefined ? { sensitivity: input.sensitivity } : {}),
134
+ // SDF-8: widen the trifecta `sensitive` leg to the operator's
135
+ // configured tiers (default secret-only ⇒ byte-identical).
136
+ ...(config.sensitiveTiers !== undefined ? { sensitiveTiers: config.sensitiveTiers } : {}),
137
+ });
138
+ // C6: a ToolReturn taint override only ever WIDENS the derived
139
+ // label - a first-party recall tool can mark quarantined content
140
+ // untrusted, but nothing can launder an untrusted tool's output.
141
+ const override = input.taintOverride;
142
+ const label =
143
+ override === undefined
144
+ ? derived
145
+ : {
146
+ ...derived,
147
+ untrusted: derived.untrusted || override.untrusted === true,
148
+ sensitive: derived.sensitive || override.sensitive === true,
149
+ ...(override.untrusted === true && override.sourceKind !== undefined
150
+ ? { sourceKind: override.sourceKind }
151
+ : {}),
152
+ };
153
+ ledger.recordOutput(label, input.outputText);
154
+ },
155
+
156
+ recordAssistant(runId: string, text: string): void {
157
+ if (text.length === 0) return;
158
+ ledgerFor(runId).recordAssistantOutput?.(text);
159
+ },
160
+
161
+ // AG-19: snapshot/rehydrate the run's coarse taint summary across a
162
+ // suspend/resume so the sink gate is not silently un-gated on the HITL
163
+ // boundary. Only the load-bearing flags cross the boundary - never spans.
164
+ snapshotLedger(runId: string): TaintLedgerSnapshot | undefined {
165
+ return ledgers.get(runId)?.snapshot();
166
+ },
167
+ seedLedger(runId: string, summary: TaintLedgerSnapshot): void {
168
+ ledgers.set(runId, createTaintLedger({ ...ledgerOpts, initial: summary }));
169
+ },
170
+ };
171
+ }