@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/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
- * Fail-closed defaults: an `enforce`-mode emission with zero registered
24
- * interceptors yields `deny host_error:no_interceptor` (§7), and
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
- /** Race `fn`'s result against the §7 timeout. */
139
- async withTimeout(fn) {
140
- if (this.timeoutMs === null)
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()), this.timeoutMs);
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
- register(interceptor, name) {
162
- this.interceptors.push(interceptor);
163
- this.names.push(name ?? null);
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
- ...(this.names[v.index] != null ? { name: this.names[v.index] } : {}),
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: this.interceptors.length,
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 (this.interceptors.length === 0) {
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(interceptor, ctx) {
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 registration order (one
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 = this.interceptors.length;
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(this.interceptors[i], ctx);
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 interceptor of this.interceptors) {
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(interceptor, ctx);
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 interceptor of this.interceptors) {
599
- all.push(await this.invoke(interceptor, snapshot));
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
- /** Parallel profiles only. */
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 registered at emission time (§10.3). */
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
  };