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