@responsibleai/agent-hooks 0.1.0-beta.1 → 0.1.0-beta.3
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/README.md +72 -7
- package/binding.d.ts +27 -0
- package/binding.js +59 -54
- package/dist/builder.js +3 -4
- package/dist/ctk/index.d.ts +44 -5
- package/dist/ctk/index.js +6 -10
- package/dist/ctk/reference.d.ts +8 -8
- package/dist/ctk/reference.declaration.json +33 -0
- package/dist/ctk/reference.js +86 -34
- package/dist/ctk/runner.d.ts +30 -10
- package/dist/ctk/runner.js +569 -42
- package/dist/declaration.d.ts +378 -0
- package/dist/declaration.js +752 -0
- package/dist/emitter.d.ts +68 -14
- package/dist/emitter.js +281 -45
- package/dist/index.d.ts +23 -3
- package/dist/index.js +49 -8
- package/dist/native.d.ts +9 -8
- package/dist/native.js +13 -5
- package/package.json +6 -6
package/dist/emitter.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// Copyright (c) Microsoft
|
|
3
|
-
|
|
2
|
+
// Copyright (c) Microsoft. All rights reserved.
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.InterceptionEmitter = exports.EmitterSealed = exports.DEFAULT_TIMEOUT_MS = void 0;
|
|
4
5
|
/**
|
|
5
6
|
* Host-side emitter: dispatch context → interceptors → composition →
|
|
6
7
|
* combined verdict → record (§6–§10).
|
|
@@ -20,8 +21,15 @@
|
|
|
20
21
|
* serial dispatch over isolated snapshots — §7.2: parallel names
|
|
21
22
|
* isolation semantics, not scheduling.
|
|
22
23
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
24
|
+
* An emitter can also be built from a host declaration document (§7.7)
|
|
25
|
+
* through {@link InterceptionEmitter.fromDeclaration} and the path, JSON
|
|
26
|
+
* and value forms. The document is validated and resolved by the Rust
|
|
27
|
+
* core; this module runs the host's kind resolvers (step 11 of §7.7.6),
|
|
28
|
+
* seals the emitter (§7.7.7) and stamps the contract version on every
|
|
29
|
+
* record (§7.7.8).
|
|
30
|
+
*
|
|
31
|
+
* Fail-closed defaults: an `enforce`-mode emission with zero interceptors
|
|
32
|
+
* bound at the point yields `deny host_error:no_interceptor` (§7), and
|
|
25
33
|
* {@link InterceptionEmitter.emit} **throws** {@link InterceptionBlocked}
|
|
26
34
|
* on any block — the ignorable-result variant is the explicitly named
|
|
27
35
|
* {@link InterceptionEmitter.emitUnchecked}.
|
|
@@ -30,9 +38,8 @@
|
|
|
30
38
|
* on the event loop; sequence assignment and record append are atomic on
|
|
31
39
|
* a single JS thread. Sharing one emitter across workers is unsupported.
|
|
32
40
|
*/
|
|
33
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
-
exports.InterceptionEmitter = exports.DEFAULT_TIMEOUT_MS = void 0;
|
|
35
41
|
const index_1 = require("./index");
|
|
42
|
+
const declaration_1 = require("./declaration");
|
|
36
43
|
const native_1 = require("./native");
|
|
37
44
|
/** §7 RECOMMENDED interceptor/resolver timeout (milliseconds). */
|
|
38
45
|
exports.DEFAULT_TIMEOUT_MS = 5000;
|
|
@@ -43,6 +50,24 @@ class InterceptTimeout extends Error {
|
|
|
43
50
|
this.name = "InterceptTimeout";
|
|
44
51
|
}
|
|
45
52
|
}
|
|
53
|
+
/** Thrown when a setter or `register` is called on an emitter built
|
|
54
|
+
* from a host declaration (§7.7.7): the declaration must stay what ran. */
|
|
55
|
+
/** Recursively freeze a JSON-shaped value in place and return it. */
|
|
56
|
+
function deepFreeze(value) {
|
|
57
|
+
if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
|
|
58
|
+
Object.freeze(value);
|
|
59
|
+
for (const v of Object.values(value))
|
|
60
|
+
deepFreeze(v);
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
class EmitterSealed extends Error {
|
|
65
|
+
constructor(what) {
|
|
66
|
+
super(`${what}: this emitter was built from a host declaration and is sealed (see spec §7.7.7)`);
|
|
67
|
+
this.name = "EmitterSealed";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.EmitterSealed = EmitterSealed;
|
|
46
71
|
/** Whether a verdict was synthesized by the host (§11) rather than
|
|
47
72
|
* returned by an interceptor or resolver. */
|
|
48
73
|
function isHostSynthesized(v) {
|
|
@@ -109,19 +134,32 @@ function synthesized(err, detail) {
|
|
|
109
134
|
};
|
|
110
135
|
}
|
|
111
136
|
const NOT_CONSULTED = { consulted: false };
|
|
137
|
+
/** Whether `v` looks like an interceptor (§7): an object with an
|
|
138
|
+
* `intercept` method. */
|
|
139
|
+
function isInterceptor(v) {
|
|
140
|
+
return (v !== null &&
|
|
141
|
+
(typeof v === "object" || typeof v === "function") &&
|
|
142
|
+
typeof v.intercept === "function");
|
|
143
|
+
}
|
|
112
144
|
class InterceptionEmitter {
|
|
113
|
-
mode;
|
|
114
|
-
resolver;
|
|
115
|
-
timeoutMs;
|
|
116
145
|
interceptors = [];
|
|
117
146
|
_records = [];
|
|
118
147
|
composition = index_1.Composition.default();
|
|
119
148
|
identity = index_1.JCS_SHA256;
|
|
120
|
-
names = [];
|
|
121
149
|
approvalRedactor = null;
|
|
122
150
|
recordSink = null;
|
|
123
151
|
maxRecords = null;
|
|
124
152
|
_recordsDropped = 0;
|
|
153
|
+
mode;
|
|
154
|
+
resolver;
|
|
155
|
+
/** §7 emitter-wide interceptor bound; `null` disables enforcement. */
|
|
156
|
+
timeoutMs;
|
|
157
|
+
/** §9 resolver bound (`approval_resolver_ms`, §7.7.3); `null` disables. */
|
|
158
|
+
resolverTimeoutMs;
|
|
159
|
+
/** §7.7.7: a declaration-built emitter refuses reconfiguration. */
|
|
160
|
+
sealed = false;
|
|
161
|
+
/** The resolved declaration this emitter was built from, if any. */
|
|
162
|
+
_declaration = null;
|
|
125
163
|
/**
|
|
126
164
|
* `timeoutMs` bounds each interceptor `intercept()` and resolver
|
|
127
165
|
* `resolve()` call (§7, RECOMMENDED default 5000 ms); breach fails
|
|
@@ -134,17 +172,151 @@ class InterceptionEmitter {
|
|
|
134
172
|
this.mode = mode;
|
|
135
173
|
this.resolver = resolver;
|
|
136
174
|
this.timeoutMs = timeoutMs;
|
|
175
|
+
this.resolverTimeoutMs = timeoutMs;
|
|
137
176
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
177
|
+
// ---- host declaration (§7.7) ----------------------------------------------
|
|
178
|
+
/** Build an emitter from a validated declaration: steps 8 to 11 of
|
|
179
|
+
* §7.7.6 (resolve against the registry's surface and names in the
|
|
180
|
+
* core, then run every binding's kind resolver). The result is
|
|
181
|
+
* sealed (§7.7.7) and stamps `declaration` on every record (§7.7.8).
|
|
182
|
+
* Throws {@link DeclarationError} on refusal; no emitter exists then. */
|
|
183
|
+
static fromDeclaration(declaration, registry) {
|
|
184
|
+
const resolved = (0, declaration_1.resolveDeclaration)(declaration, registry);
|
|
185
|
+
return InterceptionEmitter.fromResolved(resolved, registry);
|
|
186
|
+
}
|
|
187
|
+
/** {@link HostDeclaration.fromPath} then {@link fromDeclaration}. */
|
|
188
|
+
static async fromDeclarationPath(path, registry) {
|
|
189
|
+
return InterceptionEmitter.fromDeclaration(await declaration_1.HostDeclaration.fromPath(path), registry);
|
|
190
|
+
}
|
|
191
|
+
/** {@link HostDeclaration.fromJson} then {@link fromDeclaration}. */
|
|
192
|
+
static fromDeclarationJson(text, registry) {
|
|
193
|
+
return InterceptionEmitter.fromDeclaration(declaration_1.HostDeclaration.fromJson(text), registry);
|
|
194
|
+
}
|
|
195
|
+
/** {@link HostDeclaration.fromValue} then {@link fromDeclaration}. */
|
|
196
|
+
static fromDeclarationValue(value, registry) {
|
|
197
|
+
return InterceptionEmitter.fromDeclaration(declaration_1.HostDeclaration.fromValue(value), registry);
|
|
198
|
+
}
|
|
199
|
+
/** The resolved declaration this emitter runs under, when it was
|
|
200
|
+
* built from one (§7.7.7). Its canonical JSON is the equivalence
|
|
201
|
+
* oracle for the construction paths. The value is frozen all the
|
|
202
|
+
* way down: writing through it has no effect on dispatch or records.
|
|
203
|
+
* `null` for an emitter configured in code. */
|
|
204
|
+
get declaration() {
|
|
205
|
+
return this._declaration;
|
|
206
|
+
}
|
|
207
|
+
/** Step 11 and construction. Every reference is looked up again in
|
|
208
|
+
* the registry's own maps, so bookkeeping drift between the names the
|
|
209
|
+
* core checked and what the registry holds fails closed. */
|
|
210
|
+
static fromResolved(resolved, registry) {
|
|
211
|
+
const cfg = resolved.configuration;
|
|
212
|
+
let resolver = null;
|
|
213
|
+
if (cfg.approval.resolver !== null) {
|
|
214
|
+
const name = cfg.approval.resolver;
|
|
215
|
+
const r = registry.approvalResolverRef(name);
|
|
216
|
+
if (r === undefined) {
|
|
217
|
+
throw declaration_1.DeclarationError.single(declaration_1.DeclarationErrorClass.ReferenceUnresolved, "/configuration/approval/resolver", `approval resolver ${JSON.stringify(name)} vanished from the registry`);
|
|
218
|
+
}
|
|
219
|
+
resolver = r;
|
|
220
|
+
}
|
|
221
|
+
let identity;
|
|
222
|
+
if (cfg.identity_provider === null) {
|
|
223
|
+
identity = null;
|
|
224
|
+
}
|
|
225
|
+
else if (cfg.identity_provider === index_1.JCS_SHA256) {
|
|
226
|
+
identity = index_1.JCS_SHA256;
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
const name = cfg.identity_provider;
|
|
230
|
+
const fn = registry.identityFn(name);
|
|
231
|
+
if (fn === undefined) {
|
|
232
|
+
throw declaration_1.DeclarationError.single(declaration_1.DeclarationErrorClass.ReferenceUnresolved, "/configuration/identity_provider", `identity provider ${JSON.stringify(name)} vanished from the registry`);
|
|
233
|
+
}
|
|
234
|
+
identity = { name, fn };
|
|
235
|
+
}
|
|
236
|
+
let approvalRedactor = null;
|
|
237
|
+
if (cfg.approval.redactor !== null) {
|
|
238
|
+
const name = cfg.approval.redactor;
|
|
239
|
+
const fn = registry.redactorFn(name);
|
|
240
|
+
if (fn === undefined) {
|
|
241
|
+
throw declaration_1.DeclarationError.single(declaration_1.DeclarationErrorClass.ReferenceUnresolved, "/configuration/approval/redactor", `approval redactor ${JSON.stringify(name)} vanished from the registry`);
|
|
242
|
+
}
|
|
243
|
+
approvalRedactor = fn;
|
|
244
|
+
}
|
|
245
|
+
const bound = [];
|
|
246
|
+
resolved.bindings.forEach((b, i) => {
|
|
247
|
+
const pointer = `/bindings/${i}`;
|
|
248
|
+
const resolverFn = registry.kindResolver(b.kind);
|
|
249
|
+
if (resolverFn === undefined) {
|
|
250
|
+
throw declaration_1.DeclarationError.single(declaration_1.DeclarationErrorClass.KindUnknown, `${pointer}/kind`, `kind ${JSON.stringify(b.kind)} vanished from the registry`);
|
|
251
|
+
}
|
|
252
|
+
// The dispatch set is fixed before the resolver runs and the
|
|
253
|
+
// context carries a frozen copy, so a resolver cannot widen its
|
|
254
|
+
// own binding (the Rust core hands out `&BTreeSet` for the same
|
|
255
|
+
// reason).
|
|
256
|
+
const at = new Set(b.at);
|
|
257
|
+
const context = {
|
|
258
|
+
id: b.id,
|
|
259
|
+
kind: b.kind,
|
|
260
|
+
at: Object.freeze([...b.at]),
|
|
261
|
+
timeoutMs: b.timeout_ms,
|
|
262
|
+
host: resolved.host ?? null,
|
|
263
|
+
declarationVersion: resolved.declaration,
|
|
264
|
+
};
|
|
265
|
+
// §7.7.5: a resolver that throws or returns a non-interceptor
|
|
266
|
+
// refuses the document; the message is bounded and the config is
|
|
267
|
+
// never echoed by the loader.
|
|
268
|
+
let interceptor;
|
|
269
|
+
try {
|
|
270
|
+
interceptor = resolverFn(b.config, context);
|
|
271
|
+
}
|
|
272
|
+
catch (e) {
|
|
273
|
+
const msg = e instanceof Error ? e.message : "resolver threw a non-Error value";
|
|
274
|
+
throw declaration_1.DeclarationError.single(declaration_1.DeclarationErrorClass.BindingRejected, pointer, `binding ${JSON.stringify(b.id)} (kind ${JSON.stringify(b.kind)}) rejected: ${msg}`);
|
|
275
|
+
}
|
|
276
|
+
if (!isInterceptor(interceptor)) {
|
|
277
|
+
const why = interceptor instanceof Promise
|
|
278
|
+
? "resolver returned a Promise; kind resolvers are synchronous"
|
|
279
|
+
: "resolver did not return an interceptor";
|
|
280
|
+
throw declaration_1.DeclarationError.single(declaration_1.DeclarationErrorClass.BindingRejected, pointer, `binding ${JSON.stringify(b.id)} (kind ${JSON.stringify(b.kind)}) rejected: ${why}`);
|
|
281
|
+
}
|
|
282
|
+
bound.push({
|
|
283
|
+
interceptor,
|
|
284
|
+
name: b.id,
|
|
285
|
+
at,
|
|
286
|
+
timeoutMs: b.timeout_ms,
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
const em = new InterceptionEmitter(cfg.mode, resolver, cfg.timeouts.interceptor_ms);
|
|
290
|
+
em.resolverTimeoutMs = cfg.timeouts.approval_resolver_ms;
|
|
291
|
+
// The emitter owns its own copy of the composition and the
|
|
292
|
+
// declaration it exposes is frozen all the way down, so the seal
|
|
293
|
+
// cannot be bypassed by writing through `emitter.declaration`.
|
|
294
|
+
em.composition = { ...cfg.composition };
|
|
295
|
+
em.identity = identity;
|
|
296
|
+
em.approvalRedactor = approvalRedactor;
|
|
297
|
+
em.maxRecords = cfg.records.max_buffered;
|
|
298
|
+
em.interceptors.push(...bound);
|
|
299
|
+
em._declaration = deepFreeze(resolved);
|
|
300
|
+
em.sealed = true;
|
|
301
|
+
return em;
|
|
302
|
+
}
|
|
303
|
+
/** §7.7.7: a declaration-built emitter refuses reconfiguration;
|
|
304
|
+
* otherwise the declaration would not be what ran. */
|
|
305
|
+
unsealed(what) {
|
|
306
|
+
if (this.sealed)
|
|
307
|
+
throw new EmitterSealed(what);
|
|
308
|
+
}
|
|
309
|
+
/** Race `fn`'s result against a timeout (§7). `limitMs: null` runs
|
|
310
|
+
* unbounded. */
|
|
311
|
+
async withTimeout(fn, limitMs) {
|
|
312
|
+
if (limitMs === null)
|
|
141
313
|
return fn();
|
|
142
314
|
let timer;
|
|
143
315
|
try {
|
|
144
316
|
return await Promise.race([
|
|
145
317
|
(async () => fn())(),
|
|
146
318
|
new Promise((_, reject) => {
|
|
147
|
-
timer = setTimeout(() => reject(new InterceptTimeout()),
|
|
319
|
+
timer = setTimeout(() => reject(new InterceptTimeout()), limitMs);
|
|
148
320
|
}),
|
|
149
321
|
]);
|
|
150
322
|
}
|
|
@@ -157,10 +329,36 @@ class InterceptionEmitter {
|
|
|
157
329
|
return this._records;
|
|
158
330
|
}
|
|
159
331
|
/** Register an interceptor, optionally with a host-chosen
|
|
160
|
-
* payload-free `name` recorded on `verdicts[].name` (§10.3)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
332
|
+
* payload-free `name` recorded on `verdicts[].name` (§10.3) and the
|
|
333
|
+
* points it runs at (`at`; omitted = every point). At point P the
|
|
334
|
+
* interceptors that run are those bound there, in registration
|
|
335
|
+
* order; `interceptors_registered`, `verdicts[].index` and
|
|
336
|
+
* `decided_by` count and index that list (§7.7.8). Throws
|
|
337
|
+
* {@link EmitterSealed} on a declaration-built emitter. */
|
|
338
|
+
register(interceptor, name, at) {
|
|
339
|
+
this.unsealed("register");
|
|
340
|
+
let points = null;
|
|
341
|
+
if (at !== undefined) {
|
|
342
|
+
// The compile-time type does not protect plain-JS callers: a
|
|
343
|
+
// misspelled point must not bind nowhere without a word.
|
|
344
|
+
points = new Set();
|
|
345
|
+
const known = Object.values(index_1.InterceptionPoint);
|
|
346
|
+
for (const p of at) {
|
|
347
|
+
if (!known.includes(p)) {
|
|
348
|
+
throw new RangeError(`unknown interception point ${JSON.stringify(p)}: the point set is closed (§3)`);
|
|
349
|
+
}
|
|
350
|
+
points.add(p);
|
|
351
|
+
}
|
|
352
|
+
if (points.size === 0) {
|
|
353
|
+
throw new RangeError("at must name at least one interception point; omit it to bind every point (§7.7.5)");
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
this.interceptors.push({
|
|
357
|
+
interceptor,
|
|
358
|
+
name: name ?? null,
|
|
359
|
+
at: points,
|
|
360
|
+
timeoutMs: undefined,
|
|
361
|
+
});
|
|
164
362
|
return this;
|
|
165
363
|
}
|
|
166
364
|
/**
|
|
@@ -178,6 +376,7 @@ class InterceptionEmitter {
|
|
|
178
376
|
* profile. See docs/PRODUCTION.md.
|
|
179
377
|
*/
|
|
180
378
|
setComposition(composition) {
|
|
379
|
+
this.unsealed("setComposition");
|
|
181
380
|
const profiles = Object.values(index_1.CompositionProfile);
|
|
182
381
|
if (!profiles.includes(composition.profile)) {
|
|
183
382
|
throw new RangeError(`unknown composition profile ${JSON.stringify(composition.profile)}: the profile set is closed (§7.2)`);
|
|
@@ -200,6 +399,7 @@ class InterceptionEmitter {
|
|
|
200
399
|
* `^[a-z][a-z0-9_-]*$` and must not begin with `jcs` (reserved so a
|
|
201
400
|
* custom function can never claim golden-vector semantics). */
|
|
202
401
|
setIdentityProvider(provider) {
|
|
402
|
+
this.unsealed("setIdentityProvider");
|
|
203
403
|
if (provider !== null && provider !== index_1.JCS_SHA256) {
|
|
204
404
|
if (!/^[a-z][a-z0-9_-]*$/.test(provider.name) || provider.name.startsWith("jcs")) {
|
|
205
405
|
throw new RangeError("identity provider name must match ^[a-z][a-z0-9_-]*$ and must not begin with 'jcs' (§10.1)");
|
|
@@ -215,13 +415,15 @@ class InterceptionEmitter {
|
|
|
215
415
|
* redactor that throws fails the consultation closed as
|
|
216
416
|
* `host_error:approval_resolver_failed`. */
|
|
217
417
|
setApprovalRedactor(redactor) {
|
|
418
|
+
this.unsealed("setApprovalRedactor");
|
|
218
419
|
this.approvalRedactor = redactor;
|
|
219
420
|
return this;
|
|
220
421
|
}
|
|
221
422
|
/** Register a per-emission record callback (§10.3), invoked
|
|
222
423
|
* synchronously after every emission before buffering; a sink
|
|
223
424
|
* exception is swallowed (audit delivery is the host's liveness
|
|
224
|
-
* concern, not the control plane's).
|
|
425
|
+
* concern, not the control plane's). Allowed on a sealed emitter: it
|
|
426
|
+
* changes where records go, not what they say (§7.7.7). */
|
|
225
427
|
setRecordSink(sink) {
|
|
226
428
|
this.recordSink = sink;
|
|
227
429
|
return this;
|
|
@@ -230,6 +432,7 @@ class InterceptionEmitter {
|
|
|
230
432
|
* is dropped and {@link recordsDropped} increments. Unbounded by
|
|
231
433
|
* default. */
|
|
232
434
|
setMaxRecords(max) {
|
|
435
|
+
this.unsealed("setMaxRecords");
|
|
233
436
|
this.maxRecords = max;
|
|
234
437
|
return this;
|
|
235
438
|
}
|
|
@@ -285,8 +488,9 @@ class InterceptionEmitter {
|
|
|
285
488
|
}
|
|
286
489
|
}
|
|
287
490
|
const identityFailed = outcome !== null;
|
|
491
|
+
const active = this.active(ctx);
|
|
288
492
|
if (outcome === null)
|
|
289
|
-
outcome = await this.dispatch(ctx);
|
|
493
|
+
outcome = await this.dispatch(ctx, active);
|
|
290
494
|
const providerName = this.identity === null ? null : this.identity === index_1.JCS_SHA256 ? index_1.JCS_SHA256 : this.identity.name;
|
|
291
495
|
const meta = {
|
|
292
496
|
input_identity: inputIdentity,
|
|
@@ -300,13 +504,15 @@ class InterceptionEmitter {
|
|
|
300
504
|
: null,
|
|
301
505
|
decided_by: outcome.decidedBy,
|
|
302
506
|
composition: this.composition,
|
|
507
|
+
declaration: this._declaration?.declaration ?? null,
|
|
508
|
+
// §10.3 summaries with the bound names attached positionally.
|
|
303
509
|
verdicts: outcome.verdicts.map((v) => ({
|
|
304
510
|
...v,
|
|
305
|
-
...(
|
|
511
|
+
...(active[v.index]?.name != null ? { name: active[v.index].name } : {}),
|
|
306
512
|
})),
|
|
307
513
|
fold_truncated: outcome.foldTruncated,
|
|
308
514
|
resolved_by: outcome.resolvedBy,
|
|
309
|
-
interceptors_registered:
|
|
515
|
+
interceptors_registered: active.length,
|
|
310
516
|
};
|
|
311
517
|
let record;
|
|
312
518
|
if (identityFailed) {
|
|
@@ -362,10 +568,11 @@ class InterceptionEmitter {
|
|
|
362
568
|
enforced_identity: null,
|
|
363
569
|
decided_by: null,
|
|
364
570
|
composition: this.composition,
|
|
571
|
+
declaration: this._declaration?.declaration ?? null,
|
|
365
572
|
verdicts: null,
|
|
366
573
|
fold_truncated: null,
|
|
367
574
|
resolved_by: null,
|
|
368
|
-
interceptors_registered: this.interceptors.length,
|
|
575
|
+
interceptors_registered: this.interceptors.filter((b) => runsAt(b, point)).length,
|
|
369
576
|
});
|
|
370
577
|
return this.deliver(record);
|
|
371
578
|
}
|
|
@@ -390,23 +597,34 @@ class InterceptionEmitter {
|
|
|
390
597
|
return record;
|
|
391
598
|
}
|
|
392
599
|
// ---------------------------------------------------------------------------
|
|
600
|
+
/** The interceptors bound at the context's point, in dispatch order
|
|
601
|
+
* (§7.7.8). */
|
|
602
|
+
active(ctx) {
|
|
603
|
+
const point = pointOf(ctx);
|
|
604
|
+
return this.interceptors.filter((b) => runsAt(b, point));
|
|
605
|
+
}
|
|
606
|
+
/** The bound a registration runs under: its own, else the emitter's. */
|
|
607
|
+
limit(b) {
|
|
608
|
+
return b.timeoutMs === undefined ? this.timeoutMs : b.timeoutMs;
|
|
609
|
+
}
|
|
393
610
|
/** Profile dispatch (§7.4–§7.5). Returns the combined verdict and
|
|
394
611
|
* its record metadata. */
|
|
395
|
-
async dispatch(ctx) {
|
|
396
|
-
if (
|
|
612
|
+
async dispatch(ctx, active) {
|
|
613
|
+
if (active.length === 0) {
|
|
397
614
|
// §7: zero interceptors fails closed, profile-independent.
|
|
398
615
|
// Register an explicit allow-all interceptor for a deliberate
|
|
399
|
-
// passthrough.
|
|
616
|
+
// passthrough. With per-point bindings (§7.7.5) this is per
|
|
617
|
+
// point: a surface point with no binding denies.
|
|
400
618
|
return synthesized(index_1.HostError.NoInterceptor);
|
|
401
619
|
}
|
|
402
620
|
switch (this.composition.profile) {
|
|
403
621
|
case index_1.CompositionProfile.SequentialFirstDeny:
|
|
404
|
-
return this.dispatchFirstDeny(ctx);
|
|
622
|
+
return this.dispatchFirstDeny(ctx, active);
|
|
405
623
|
case index_1.CompositionProfile.SequentialRunAll:
|
|
406
|
-
return this.dispatchRunAll(ctx);
|
|
624
|
+
return this.dispatchRunAll(ctx, active);
|
|
407
625
|
case index_1.CompositionProfile.ParallelStrictest:
|
|
408
626
|
case index_1.CompositionProfile.ParallelUnanimous:
|
|
409
|
-
return this.dispatchParallel(ctx);
|
|
627
|
+
return this.dispatchParallel(ctx, active);
|
|
410
628
|
default:
|
|
411
629
|
// Unreachable through the public API: setComposition validates
|
|
412
630
|
// against the closed §7.2 set. Fail closed rather than silently
|
|
@@ -417,12 +635,12 @@ class InterceptionEmitter {
|
|
|
417
635
|
/** Invoke one interceptor on its own copy of `ctx` (§7) and pass the
|
|
418
636
|
* result through the §5 gate. Never throws: every failure becomes the
|
|
419
637
|
* §6.3 synthesized deny. */
|
|
420
|
-
async invoke(
|
|
638
|
+
async invoke(bound, ctx) {
|
|
421
639
|
let v;
|
|
422
640
|
try {
|
|
423
641
|
// §7: each interceptor gets its own copy — in-place mutation of
|
|
424
642
|
// the copy cannot alter enforcement.
|
|
425
|
-
v = await this.withTimeout(() => interceptor.intercept(structuredClone(ctx)));
|
|
643
|
+
v = await this.withTimeout(() => bound.interceptor.intercept(structuredClone(ctx)), this.limit(bound));
|
|
426
644
|
}
|
|
427
645
|
catch (e) {
|
|
428
646
|
if (e instanceof InterceptTimeout) {
|
|
@@ -451,11 +669,11 @@ class InterceptionEmitter {
|
|
|
451
669
|
* short-circuits; a liftable deny consults the seam, then `stop` or
|
|
452
670
|
* `resume` per the knob.
|
|
453
671
|
*
|
|
454
|
-
* `perInterceptor` stays index-aligned with
|
|
672
|
+
* `perInterceptor` stays index-aligned with dispatch order (one
|
|
455
673
|
* entry per invoked interceptor, §10.3 summaries); `pool`
|
|
456
674
|
* additionally holds substituted resolutions for the §7.3 unions. */
|
|
457
|
-
async dispatchFirstDeny(ctx) {
|
|
458
|
-
const n =
|
|
675
|
+
async dispatchFirstDeny(ctx, active) {
|
|
676
|
+
const n = active.length;
|
|
459
677
|
const onApproval = this.composition.on_approval ?? "stop";
|
|
460
678
|
const perInterceptor = [];
|
|
461
679
|
const pool = [];
|
|
@@ -463,7 +681,7 @@ class InterceptionEmitter {
|
|
|
463
681
|
let resolvedBy = null;
|
|
464
682
|
const truncated = (i) => i + 1 < n;
|
|
465
683
|
for (let i = 0; i < n; i++) {
|
|
466
|
-
let v = await this.invoke(
|
|
684
|
+
let v = await this.invoke(active[i], ctx);
|
|
467
685
|
perInterceptor.push(v);
|
|
468
686
|
pool.push(v);
|
|
469
687
|
if (isHostSynthesized(v)) {
|
|
@@ -560,12 +778,12 @@ class InterceptionEmitter {
|
|
|
560
778
|
* consulted at most once, only when the winner is liftable (which
|
|
561
779
|
* implies every deny in the emission is liftable — a single plain
|
|
562
780
|
* deny already won the severity order). */
|
|
563
|
-
async dispatchRunAll(ctx) {
|
|
781
|
+
async dispatchRunAll(ctx, active) {
|
|
564
782
|
const all = [];
|
|
565
|
-
for (const
|
|
783
|
+
for (const bound of active) {
|
|
566
784
|
// §6.3 per-interceptor: a malformed verdict becomes that
|
|
567
785
|
// interceptor's synthesized deny; the rest still run.
|
|
568
|
-
const v = await this.invoke(
|
|
786
|
+
const v = await this.invoke(bound, ctx);
|
|
569
787
|
if (v.decision === index_1.Decision.Transform) {
|
|
570
788
|
const folded = this.foldTransform(ctx, v);
|
|
571
789
|
if (!(0, index_1.permits)(folded.decision)) {
|
|
@@ -590,13 +808,13 @@ class InterceptionEmitter {
|
|
|
590
808
|
}
|
|
591
809
|
/** Parallel profiles (§7.5): isolated snapshots, no fold; serial
|
|
592
810
|
* dispatch (isolation semantics, not scheduling). */
|
|
593
|
-
async dispatchParallel(ctx) {
|
|
811
|
+
async dispatchParallel(ctx, active) {
|
|
594
812
|
// Every interceptor receives its own copy of the same untransformed
|
|
595
813
|
// snapshot ({@link invoke} clones per call).
|
|
596
814
|
const snapshot = structuredClone(ctx);
|
|
597
815
|
const all = [];
|
|
598
|
-
for (const
|
|
599
|
-
all.push(await this.invoke(
|
|
816
|
+
for (const bound of active) {
|
|
817
|
+
all.push(await this.invoke(bound, snapshot));
|
|
600
818
|
}
|
|
601
819
|
return this.aggregateAndConsult(ctx, all);
|
|
602
820
|
}
|
|
@@ -685,9 +903,6 @@ class InterceptionEmitter {
|
|
|
685
903
|
}
|
|
686
904
|
return v;
|
|
687
905
|
}
|
|
688
|
-
/** The declared provider's output for `ctx` (§10.1); `null` iff the
|
|
689
|
-
* provider is `null`. Throws {@link AgentHooksCoreError} iff the
|
|
690
|
-
* provider rejected the context (§10.2 fail-closed value domain). */
|
|
691
906
|
/** Custom-provider identity, or null when the provider fails (the
|
|
692
907
|
* emission has already been decided at this point). */
|
|
693
908
|
tryCustomIdentity(ctx) {
|
|
@@ -698,6 +913,9 @@ class InterceptionEmitter {
|
|
|
698
913
|
return null;
|
|
699
914
|
}
|
|
700
915
|
}
|
|
916
|
+
/** The declared provider's output for `ctx` (§10.1); `null` iff the
|
|
917
|
+
* provider is `null`. Throws {@link AgentHooksCoreError} iff the
|
|
918
|
+
* provider rejected the context (§10.2 fail-closed value domain). */
|
|
701
919
|
identityOf(ctx) {
|
|
702
920
|
if (this.identity === null)
|
|
703
921
|
return null;
|
|
@@ -759,7 +977,7 @@ class InterceptionEmitter {
|
|
|
759
977
|
interception_point: ctx.interception_point,
|
|
760
978
|
verdict,
|
|
761
979
|
context: presented,
|
|
762
|
-
}));
|
|
980
|
+
}), this.resolverTimeoutMs);
|
|
763
981
|
}
|
|
764
982
|
catch (e) {
|
|
765
983
|
if (e instanceof InterceptTimeout) {
|
|
@@ -796,6 +1014,24 @@ class InterceptionEmitter {
|
|
|
796
1014
|
}
|
|
797
1015
|
}
|
|
798
1016
|
exports.InterceptionEmitter = InterceptionEmitter;
|
|
1017
|
+
/** The context's interception point when it names one. */
|
|
1018
|
+
function pointOf(ctx) {
|
|
1019
|
+
const p = ctx["interception_point"];
|
|
1020
|
+
return typeof p === "string" && Object.values(index_1.InterceptionPoint).includes(p)
|
|
1021
|
+
? p
|
|
1022
|
+
: null;
|
|
1023
|
+
}
|
|
1024
|
+
/** Whether a registration runs at `point` (§7.7.5). A registration
|
|
1025
|
+
* without an `at` filter runs everywhere. An unparseable point never
|
|
1026
|
+
* reaches dispatch (§4 validation denies first); every binding counts
|
|
1027
|
+
* then so the record is conservative. */
|
|
1028
|
+
function runsAt(b, point) {
|
|
1029
|
+
if (b.at === null)
|
|
1030
|
+
return true;
|
|
1031
|
+
if (point === null)
|
|
1032
|
+
return true;
|
|
1033
|
+
return b.at.has(point);
|
|
1034
|
+
}
|
|
799
1035
|
/** `(code, detail)` of a native failure; anything else maps to
|
|
800
1036
|
* `fallback` with the stringified error as detail. */
|
|
801
1037
|
function codeAndDetail(e, fallback = index_1.HostError.ContextInvalid) {
|
package/dist/index.d.ts
CHANGED
|
@@ -90,6 +90,11 @@ export interface Verdict {
|
|
|
90
90
|
evidence?: Evidence;
|
|
91
91
|
result_labels?: string[];
|
|
92
92
|
}
|
|
93
|
+
/** Optional hosted/cross-process response envelope (§5.5). */
|
|
94
|
+
export interface InterceptorResponse {
|
|
95
|
+
verdict: Verdict;
|
|
96
|
+
extensions?: Record<string, JsonValue>;
|
|
97
|
+
}
|
|
93
98
|
/** Constructor sugar for the three-verdict vocabulary (§5.1). Merges
|
|
94
99
|
* with the `Verdict` interface: `Verdict.warn(...)`, `Verdict.escalate(...)`. */
|
|
95
100
|
export declare const Verdict: Readonly<{
|
|
@@ -137,7 +142,7 @@ export interface CompositionConfig {
|
|
|
137
142
|
on_approval?: OnApproval;
|
|
138
143
|
/** `parallel/unanimous` only. */
|
|
139
144
|
on_disagreement?: SynthesisPolicy;
|
|
140
|
-
/**
|
|
145
|
+
/** `parallel/strictest` only. */
|
|
141
146
|
on_transform_conflict?: SynthesisPolicy;
|
|
142
147
|
}
|
|
143
148
|
/** Constructors for the closed profile set (§7.2). */
|
|
@@ -231,6 +236,11 @@ export interface InterceptionRecord {
|
|
|
231
236
|
decided_by: number | null;
|
|
232
237
|
/** The composition profile and knobs in effect (§7.1). */
|
|
233
238
|
composition: CompositionConfig;
|
|
239
|
+
/** The host declaration contract version the emitter was built from
|
|
240
|
+
* (§7.7.8), e.g. `agent-hooks-declaration/1.0`. Present iff the
|
|
241
|
+
* emitter was constructed from a declaration document; absent for
|
|
242
|
+
* an emitter configured in code. */
|
|
243
|
+
declaration?: string;
|
|
234
244
|
/** Per-interceptor summary; populated in multi-verdict profiles
|
|
235
245
|
* (`sequential/run_all`, `parallel/*`). */
|
|
236
246
|
verdicts?: VerdictSummary[];
|
|
@@ -242,7 +252,7 @@ export interface InterceptionRecord {
|
|
|
242
252
|
* resolution substituted; `"rejection"` iff consulted without a
|
|
243
253
|
* lift; absent iff never consulted. */
|
|
244
254
|
resolved_by?: "approval" | "rejection" | null;
|
|
245
|
-
/** Interceptors
|
|
255
|
+
/** Interceptors bound at the emitted point (§10.3, §7.7.8). */
|
|
246
256
|
interceptors_registered: number;
|
|
247
257
|
}
|
|
248
258
|
/** Whether the guarded action executes (§6, §8). */
|
|
@@ -290,6 +300,10 @@ export declare function canonicalJson(v: JsonValue): string;
|
|
|
290
300
|
export declare function contextIdentity(ctx: AgentContext): string;
|
|
291
301
|
/** §5: validate an interceptor's wire return value. Rust core. */
|
|
292
302
|
export declare function validateVerdict(v: Verdict): void;
|
|
303
|
+
/** Parse a bare Verdict or §5.5 response envelope. */
|
|
304
|
+
export declare function parseInterceptorResponse(value: JsonValue | string): InterceptorResponse;
|
|
305
|
+
/** Emit a bare Verdict when the response carries no transport metadata. */
|
|
306
|
+
export declare function serializeInterceptorResponse(response: InterceptorResponse): JsonValue;
|
|
293
307
|
/** §5.2: apply a `$target`-rooted transform. Returns a new object. Rust core. */
|
|
294
308
|
export declare function applyTransform(target: JsonValue, path: string, value: JsonValue): JsonValue;
|
|
295
309
|
/** §7.4 fold-through: apply one transform to the context's `target` (and
|
|
@@ -315,6 +329,10 @@ export interface FinalizeMeta {
|
|
|
315
329
|
decided_by?: number | null;
|
|
316
330
|
/** REQUIRED: the profile and knobs in effect (§7.1). */
|
|
317
331
|
composition: CompositionConfig;
|
|
332
|
+
/** The declaration contract version the emitter was built from
|
|
333
|
+
* (§7.7.8); absent or `null` for an emitter configured in code. The
|
|
334
|
+
* core never defaults it. */
|
|
335
|
+
declaration?: string | null;
|
|
318
336
|
/** Per-interceptor summaries (multi-verdict profiles, §10.3). */
|
|
319
337
|
verdicts?: VerdictSummary[] | null;
|
|
320
338
|
/** Sequential profiles (§7.4). */
|
|
@@ -340,8 +358,10 @@ export declare function composeAggregate(composition: CompositionConfig, verdict
|
|
|
340
358
|
verdicts: VerdictSummary[];
|
|
341
359
|
};
|
|
342
360
|
export { AgentContextBuilder } from "./builder";
|
|
343
|
-
export { InterceptionEmitter } from "./emitter";
|
|
361
|
+
export { InterceptionEmitter, EmitterSealed } from "./emitter";
|
|
344
362
|
export type { HostFailure } from "./emitter";
|
|
363
|
+
export { CAPABILITIES, DECLARATION_VERSION, DeclarationBuilder, DeclarationError, DeclarationErrorClass, HostDeclaration, HostRegistry, HostRegistryError, HostSurface, MAX_BINDINGS, MAX_DEPTH, MAX_DETAIL_LEN, MAX_DOCUMENT_BYTES, MAX_ID_LEN, MAX_KIND_LEN, MAX_TIMEOUT_MS, SUPPORTED_DECLARATION_VERSIONS, canonicalDeclaration, declarationVersions, fullKnobSupport, validKind, validReference, } from "./declaration";
|
|
364
|
+
export type { BindingContext, Finding, HostInfo, KindResolver, KnobSupport, RegistryNames, ResolvedBinding, ResolvedConfiguration, ResolvedDeclaration, ResolvedSurface, TimeoutSupport, ToolSeamPosture, } from "./declaration";
|
|
345
365
|
/** Raised by a host when a verdict blocks the guarded action (§6). */
|
|
346
366
|
/** Returned by `InterceptionEmitter.emit` on a proceeding emission:
|
|
347
367
|
* the record plus the **effective** (post-composition) target the
|