@graphorin/agent 0.6.1 → 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.
- package/CHANGELOG.md +81 -0
- package/README.md +2 -2
- package/dist/errors/index.d.ts +17 -4
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +19 -3
- package/dist/errors/index.js.map +1 -1
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +153 -1930
- package/dist/factory.js.map +1 -1
- package/dist/fanout/index.d.ts +13 -1
- package/dist/fanout/index.d.ts.map +1 -1
- package/dist/fanout/index.js +13 -4
- package/dist/fanout/index.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -7
- package/dist/index.js.map +1 -1
- package/dist/lateral-leak/index.js +1 -1
- package/dist/package.js +1 -1
- package/dist/package.js.map +1 -1
- package/dist/run-state/index.d.ts +32 -6
- package/dist/run-state/index.d.ts.map +1 -1
- package/dist/run-state/index.js +46 -22
- package/dist/run-state/index.js.map +1 -1
- package/dist/runtime/agent-surface.js +122 -0
- package/dist/runtime/agent-surface.js.map +1 -0
- package/dist/runtime/agent-to-tool.d.ts +51 -0
- package/dist/runtime/agent-to-tool.d.ts.map +1 -0
- package/dist/runtime/agent-to-tool.js +145 -0
- package/dist/runtime/agent-to-tool.js.map +1 -0
- package/dist/runtime/approvals.js +0 -0
- package/dist/runtime/approvals.js.map +1 -0
- package/dist/runtime/dispatch.js +108 -0
- package/dist/runtime/dispatch.js.map +1 -0
- package/dist/runtime/executor-wiring.js +128 -0
- package/dist/runtime/executor-wiring.js.map +1 -0
- package/dist/runtime/fallback-chain.js +139 -0
- package/dist/runtime/fallback-chain.js.map +1 -0
- package/dist/runtime/handoff.js +307 -0
- package/dist/runtime/handoff.js.map +1 -0
- package/dist/runtime/messages.d.ts +22 -0
- package/dist/runtime/messages.d.ts.map +1 -0
- package/dist/runtime/messages.js +204 -0
- package/dist/runtime/messages.js.map +1 -0
- package/dist/runtime/provider-events.js +117 -0
- package/dist/runtime/provider-events.js.map +1 -0
- package/dist/runtime/run-compaction.js +210 -0
- package/dist/runtime/run-compaction.js.map +1 -0
- package/dist/runtime/run-finish.js +48 -0
- package/dist/runtime/run-finish.js.map +1 -0
- package/dist/runtime/run-gates.js +336 -0
- package/dist/runtime/run-gates.js.map +1 -0
- package/dist/runtime/run-init.js +81 -0
- package/dist/runtime/run-init.js.map +1 -0
- package/dist/runtime/run-input.js +46 -0
- package/dist/runtime/run-input.js.map +1 -0
- package/dist/runtime/step-catalogue.js +173 -0
- package/dist/runtime/step-catalogue.js.map +1 -0
- package/dist/runtime/tool-call-walk.js +189 -0
- package/dist/runtime/tool-call-walk.js.map +1 -0
- package/dist/runtime/tool-wiring.js +159 -0
- package/dist/runtime/tool-wiring.js.map +1 -0
- package/dist/tooling/adapters.js +1 -1
- package/dist/tooling/dataflow.js +1 -1
- package/dist/tooling/policy.js +2 -0
- package/dist/tooling/policy.js.map +1 -1
- package/dist/types.d.ts +63 -13
- package/dist/types.d.ts.map +1 -1
- package/package.json +20 -20
- package/src/errors/index.ts +320 -0
- package/src/evaluator-optimizer/index.ts +212 -0
- package/src/factory.ts +957 -0
- package/src/fallback/index.ts +108 -0
- package/src/fanout/index.ts +523 -0
- package/src/filters/index.ts +347 -0
- package/src/index.ts +180 -0
- package/src/internal/ids.ts +46 -0
- package/src/internal/usage-accumulator.ts +90 -0
- package/src/lateral-leak/causality-monitor.ts +221 -0
- package/src/lateral-leak/index.ts +35 -0
- package/src/lateral-leak/merge-guard.ts +151 -0
- package/src/lateral-leak/protocol-guard.ts +222 -0
- package/src/preferred-model/index.ts +210 -0
- package/src/progress/index.ts +238 -0
- package/src/run-state/index.ts +607 -0
- package/src/runtime/agent-surface.ts +218 -0
- package/src/runtime/agent-to-tool.ts +323 -0
- package/src/runtime/approvals.ts +0 -0
- package/src/runtime/dispatch.ts +183 -0
- package/src/runtime/executor-wiring.ts +331 -0
- package/src/runtime/fallback-chain.ts +250 -0
- package/src/runtime/handoff.ts +428 -0
- package/src/runtime/messages.ts +309 -0
- package/src/runtime/provider-events.ts +175 -0
- package/src/runtime/run-compaction.ts +288 -0
- package/src/runtime/run-finish.ts +93 -0
- package/src/runtime/run-gates.ts +419 -0
- package/src/runtime/run-init.ts +169 -0
- package/src/runtime/run-input.ts +102 -0
- package/src/runtime/step-catalogue.ts +338 -0
- package/src/runtime/tool-call-walk.ts +301 -0
- package/src/runtime/tool-wiring.ts +218 -0
- package/src/testing/replay-provider.ts +121 -0
- package/src/tooling/adapters.ts +403 -0
- package/src/tooling/catalogue.ts +36 -0
- package/src/tooling/dataflow.ts +171 -0
- package/src/tooling/plan.ts +123 -0
- package/src/tooling/policy.ts +67 -0
- package/src/tooling/registry-build.ts +191 -0
- 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
|
+
}
|