@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.
@@ -6,16 +6,24 @@
6
6
  *
7
7
  * The assertion engine, capability skip check, and scripted
8
8
  * interceptor/resolver evaluation live in the Rust core (native.ctk*).
9
- * This module keeps only vector globbing, the recording wrapper, and
10
- * the orchestration loop that calls the native `Harness`.
9
+ * This module keeps only vector globbing, the recording wrapper, the
10
+ * host declaration seam (§7.7.9: the CTK registry, the construction
11
+ * path proof and the `load` outcome) and the orchestration loop that
12
+ * calls the native `Harness`.
11
13
  */
12
14
  Object.defineProperty(exports, "__esModule", { value: true });
13
15
  exports.loadVectors = loadVectors;
16
+ exports.redactPaths = redactPaths;
17
+ exports.builderFromValue = builderFromValue;
18
+ exports.provePaths = provePaths;
19
+ exports.assessHarness = assessHarness;
14
20
  exports.runVector = runVector;
15
21
  exports.runVectors = runVectors;
16
22
  const node_fs_1 = require("node:fs");
23
+ const node_os_1 = require("node:os");
17
24
  const node_path_1 = require("node:path");
18
25
  const index_1 = require("../index");
26
+ const declaration_1 = require("../declaration");
19
27
  const native_1 = require("../native");
20
28
  function loadVectors(dir) {
21
29
  const vectors = (0, node_fs_1.readdirSync)(dir)
@@ -83,6 +91,83 @@ class ScriptedResolver {
83
91
  return r;
84
92
  }
85
93
  }
94
+ /** §9 redaction seam, CTK convention: each listed path is replaced
95
+ * with "[redacted]" via the §5.2/§4.3 transform machinery; a path that
96
+ * does not resolve at the escalating point is left untouched. */
97
+ function redactPaths(ctx, paths) {
98
+ let out = JSON.stringify(ctx);
99
+ for (const path of paths) {
100
+ try {
101
+ out = native_1.native.applyTransformCtx(out, path, '"[redacted]"');
102
+ }
103
+ catch {
104
+ /* unresolvable at this point: skip */
105
+ }
106
+ }
107
+ return JSON.parse(out);
108
+ }
109
+ /** The scripted interceptors and resolver a vector carries, shared by
110
+ * the field-based and the declaration path. */
111
+ class Scripts {
112
+ scripts;
113
+ approval;
114
+ redact;
115
+ /** Script 0 records: `expect.interceptions` describes each emission
116
+ * as the first interceptor saw it. One recorder per vector run, so
117
+ * the declaration path and the field path record alike. */
118
+ recorder;
119
+ constructor(v) {
120
+ // Multi-interceptor vectors (§7.1 fold-through) use
121
+ // interceptor_scripts; single-interceptor vectors use
122
+ // interceptor_script. An empty interceptor_scripts registers zero
123
+ // interceptors (§7 fail-closed vector).
124
+ this.scripts = v.interceptor_scripts ?? [v.interceptor_script];
125
+ this.approval = v.approval_script ?? [];
126
+ this.redact = v.redact_for_approval ?? [];
127
+ this.recorder = this.scripts.length > 0 ? new RecordingInterceptor(this.scripts[0]) : null;
128
+ }
129
+ /** Scripted interceptor `i`; only index 0 records. */
130
+ interceptor(i) {
131
+ if (i >= this.scripts.length)
132
+ return undefined;
133
+ return i === 0 ? this.recorder : new ScriptedInterceptor(this.scripts[i]);
134
+ }
135
+ interceptors() {
136
+ return this.scripts.map((_, i) => this.interceptor(i));
137
+ }
138
+ /** NB: [] is truthy in JS, so an empty approval_script registers NO
139
+ * resolver (matches the Rust/Python runners; exercised by AH-CTK-032). */
140
+ resolver() {
141
+ return this.approval.length > 0 ? new ScriptedResolver(this.approval) : null;
142
+ }
143
+ recorded() {
144
+ return this.recorder?.recorded ?? [];
145
+ }
146
+ /** The CTK registry for a declaration vector (§7.7.9): kind
147
+ * `ctk.scripted` (config `{"script": i}`), identity provider
148
+ * `ctk-fault`, approval resolver `ctk-scripted`, redactor
149
+ * `ctk-redact`. */
150
+ registry(surface) {
151
+ const redact = [...this.redact];
152
+ return index_1.HostRegistry.forConformance(surface)
153
+ .kind("ctk.scripted", (config, ctx) => {
154
+ const script = config?.["script"];
155
+ if (typeof script !== "number" || !Number.isInteger(script) || script < 0) {
156
+ throw new Error("config.script must be an unsigned integer index");
157
+ }
158
+ const i = this.interceptor(script);
159
+ if (i === undefined) {
160
+ throw new Error(`config.script ${script} is out of range for binding ${ctx.id}`);
161
+ }
162
+ return i;
163
+ })
164
+ .identityProvider("ctk-fault", () => {
165
+ throw new Error("ctk scripted provider fault");
166
+ })
167
+ .approvalResolver("ctk-scripted", new ScriptedResolver(this.approval))
168
+ .approvalRedactor("ctk-redact", (ctx) => redactPaths(ctx, redact));
169
+ }
170
+ }
86
171
  function runRecordToWire(rr, postures) {
87
172
  return JSON.stringify({
88
173
  outcome: rr.outcome,
@@ -94,36 +179,412 @@ function runRecordToWire(rr, postures) {
94
179
  // Harness *declarations* (§13.1), not observed behavior: the engine
95
180
  // selects expect.run_outcome_by_posture entries by them.
96
181
  postures,
182
+ ...(rr.load !== undefined ? { load: rr.load } : {}),
183
+ });
184
+ }
185
+ // ---- builder from a value (§7.7.7, the code path) ----------------------------
186
+ const POINTS = Object.values(index_1.InterceptionPoint);
187
+ const PROFILES = Object.values(index_1.CompositionProfile);
188
+ function isObject(v) {
189
+ return v !== null && typeof v === "object" && !Array.isArray(v);
190
+ }
191
+ function pointsOf(v) {
192
+ if (!Array.isArray(v))
193
+ return undefined;
194
+ const out = [];
195
+ for (const p of v) {
196
+ if (typeof p !== "string" || !POINTS.includes(p))
197
+ return undefined;
198
+ out.push(p);
199
+ }
200
+ return out;
201
+ }
202
+ function stringsOf(v) {
203
+ if (!Array.isArray(v))
204
+ return undefined;
205
+ const out = [];
206
+ for (const s of v) {
207
+ if (typeof s !== "string")
208
+ return undefined;
209
+ out.push(s);
210
+ }
211
+ return out;
212
+ }
213
+ function isUint(v) {
214
+ return typeof v === "number" && Number.isInteger(v) && v >= 0;
215
+ }
216
+ /** Rebuild a document through {@link DeclarationBuilder}, member by
217
+ * member (§7.7.7, the code path). Members the typed setters cannot
218
+ * express exactly (an unknown member, a wrong type) go through
219
+ * {@link DeclarationBuilder.raw}, so the result is validated like the
220
+ * file it came from. Mirrors the Rust runner's `builder_from_value`. */
221
+ function builderFromValue(doc) {
222
+ let b = index_1.DeclarationBuilder.empty();
223
+ if (!isObject(doc))
224
+ return b;
225
+ for (const [k, v] of Object.entries(doc)) {
226
+ if (k === "declaration" && typeof v === "string")
227
+ b = b.version(v);
228
+ else if (k === "spec" && typeof v === "string")
229
+ b = b.spec(v);
230
+ else if (k === "id" && typeof v === "string")
231
+ b = b.id(v);
232
+ else if (k === "host" &&
233
+ isObject(v) &&
234
+ Object.keys(v).every((hk) => hk === "name" || hk === "version") &&
235
+ typeof v["name"] === "string" &&
236
+ (v["version"] === undefined || typeof v["version"] === "string")) {
237
+ b = b.host(v["name"], v["version"]);
238
+ }
239
+ else if (k === "configuration" && isObject(v)) {
240
+ b = typedConfiguration(b, v) ?? b.raw(k, v);
241
+ }
242
+ else if (k === "surface" && isObject(v)) {
243
+ b = typedSurface(b, v) ?? b.raw(k, v);
244
+ }
245
+ else if (k === "bindings" && Array.isArray(v)) {
246
+ b = typedBindings(b, v) ?? b.raw(k, v);
247
+ }
248
+ else if (k === "extensions" && isObject(v)) {
249
+ for (const [ek, ev] of Object.entries(v))
250
+ b = b.extension(ek, ev);
251
+ }
252
+ else {
253
+ b = b.raw(k, v);
254
+ }
255
+ }
256
+ return b;
257
+ }
258
+ function typedConfiguration(b, c) {
259
+ for (const [k, v] of Object.entries(c)) {
260
+ if (k === "mode" && typeof v === "string") {
261
+ if (v !== "enforce" && v !== "evaluate_only")
262
+ return undefined;
263
+ b = b.mode(v);
264
+ }
265
+ else if (k === "composition" && isObject(v)) {
266
+ const known = ["profile", "on_approval", "on_disagreement", "on_transform_conflict"];
267
+ if (!Object.keys(v).every((ck) => known.includes(ck)))
268
+ return undefined;
269
+ const cfg = { profile: index_1.CompositionProfile.SequentialFirstDeny };
270
+ const profile = v["profile"];
271
+ if (profile !== undefined) {
272
+ if (typeof profile !== "string" || !PROFILES.includes(profile))
273
+ return undefined;
274
+ cfg.profile = profile;
275
+ }
276
+ for (const [kk, vv] of Object.entries(v)) {
277
+ if (kk === "profile")
278
+ continue;
279
+ if (typeof vv !== "string")
280
+ return undefined;
281
+ if (kk === "on_approval" && (vv === "stop" || vv === "resume"))
282
+ cfg.on_approval = vv;
283
+ else if (kk === "on_disagreement" && (vv === "deny" || vv === "approval")) {
284
+ cfg.on_disagreement = vv;
285
+ }
286
+ else if (kk === "on_transform_conflict" && (vv === "deny" || vv === "approval")) {
287
+ cfg.on_transform_conflict = vv;
288
+ }
289
+ else
290
+ return undefined;
291
+ }
292
+ b = b.composition(cfg);
293
+ }
294
+ else if (k === "identity_provider" && (v === null || typeof v === "string")) {
295
+ b = b.identityProvider(v);
296
+ }
297
+ else if (k === "approval" && isObject(v)) {
298
+ for (const [ak, av] of Object.entries(v)) {
299
+ if (av !== null && typeof av !== "string")
300
+ return undefined;
301
+ if (ak === "resolver")
302
+ b = b.approvalResolver(av);
303
+ else if (ak === "redactor")
304
+ b = b.approvalRedactor(av);
305
+ else
306
+ return undefined;
307
+ }
308
+ }
309
+ else if (k === "posture" && isObject(v)) {
310
+ if (Object.keys(v).length !== 1)
311
+ return undefined;
312
+ const p = v["tool_seam_host_error"];
313
+ if (p !== "continue" && p !== "terminate")
314
+ return undefined;
315
+ b = b.toolSeamHostError(p);
316
+ }
317
+ else if (k === "timeouts" && isObject(v)) {
318
+ for (const [tk, tv] of Object.entries(v)) {
319
+ if (tv !== null && !isUint(tv))
320
+ return undefined;
321
+ if (tk === "interceptor_ms")
322
+ b = b.interceptorTimeoutMs(tv);
323
+ else if (tk === "approval_resolver_ms")
324
+ b = b.approvalResolverTimeoutMs(tv);
325
+ else
326
+ return undefined;
327
+ }
328
+ }
329
+ else if (k === "records" && isObject(v)) {
330
+ if (Object.keys(v).length !== 1)
331
+ return undefined;
332
+ const n = v["max_buffered"];
333
+ if (n !== null && !isUint(n))
334
+ return undefined;
335
+ b = b.maxBufferedRecords(n ?? null);
336
+ }
337
+ else {
338
+ return undefined;
339
+ }
340
+ }
341
+ return b;
342
+ }
343
+ function typedSurface(b, sf) {
344
+ const buffered = sf["buffered_output"];
345
+ if (buffered !== undefined && typeof buffered !== "boolean")
346
+ return undefined;
347
+ const bound = sf["exposure_bound"];
348
+ if (bound !== undefined && typeof bound !== "string")
349
+ return undefined;
350
+ if (buffered !== undefined)
351
+ b = b.bufferedOutput(buffered, bound);
352
+ else if (bound !== undefined)
353
+ return undefined;
354
+ for (const [k, v] of Object.entries(sf)) {
355
+ if (k === "interception_points") {
356
+ const points = pointsOf(v);
357
+ if (points === undefined)
358
+ return undefined;
359
+ b = b.surfacePoints(points);
360
+ }
361
+ else if (k === "capabilities") {
362
+ const caps = stringsOf(v);
363
+ if (caps === undefined)
364
+ return undefined;
365
+ b = b.surfaceCapabilities(caps);
366
+ }
367
+ else if (k === "declaration_versions") {
368
+ const versions = stringsOf(v);
369
+ if (versions === undefined)
370
+ return undefined;
371
+ b = b.surfaceDeclarationVersions(versions);
372
+ }
373
+ else if (k === "profiles") {
374
+ if (!isObject(v))
375
+ return undefined;
376
+ for (const [name, knobs] of Object.entries(v)) {
377
+ if (!PROFILES.includes(name) || !isObject(knobs))
378
+ return undefined;
379
+ const support = {};
380
+ for (const [knob, values] of Object.entries(knobs)) {
381
+ const list = stringsOf(values);
382
+ if (list === undefined)
383
+ return undefined;
384
+ if (knob === "on_approval") {
385
+ if (!list.every((x) => x === "stop" || x === "resume"))
386
+ return undefined;
387
+ support.on_approval = list;
388
+ }
389
+ else if (knob === "on_disagreement" || knob === "on_transform_conflict") {
390
+ if (!list.every((x) => x === "deny" || x === "approval"))
391
+ return undefined;
392
+ support[knob] = list;
393
+ }
394
+ else
395
+ return undefined;
396
+ }
397
+ b = b.surfaceProfile(name, support);
398
+ }
399
+ }
400
+ else if (k === "buffered_output" || k === "exposure_bound") {
401
+ /* handled above */
402
+ }
403
+ else {
404
+ return undefined;
405
+ }
406
+ }
407
+ return b;
408
+ }
409
+ function typedBindings(b, items) {
410
+ const known = ["id", "kind", "config", "at", "timeout_ms"];
411
+ if (items.length === 0) {
412
+ // The written-down empty-deny host (§7.7.5): `bind` is never
413
+ // called, so the member is set explicitly.
414
+ return b.raw("bindings", []);
415
+ }
416
+ for (const item of items) {
417
+ if (!isObject(item))
418
+ return undefined;
419
+ if (!Object.keys(item).every((k) => known.includes(k)))
420
+ return undefined;
421
+ const id = item["id"];
422
+ const kind = item["kind"];
423
+ if (typeof id !== "string" || typeof kind !== "string")
424
+ return undefined;
425
+ // An explicit `null` is a value the spec allows; only an absent
426
+ // member takes the default, as in the Rust runner.
427
+ const config = "config" in item ? item["config"] : {};
428
+ let at;
429
+ if (item["at"] !== undefined) {
430
+ at = pointsOf(item["at"]);
431
+ if (at === undefined)
432
+ return undefined;
433
+ }
434
+ let timeout;
435
+ const t = item["timeout_ms"];
436
+ if (t === undefined)
437
+ timeout = undefined;
438
+ else if (t === null)
439
+ timeout = null;
440
+ else if (isUint(t))
441
+ timeout = t;
442
+ else
443
+ return undefined;
444
+ b = b.bind(id, kind, config, at, timeout);
445
+ }
446
+ return b;
447
+ }
448
+ // ---- construction path proof (§7.7.7) -------------------------------------------
449
+ /** Resolve `doc` through the four construction paths (§7.7.7) and
450
+ * compare: value, JSON text, a temporary file and the builder. Equal
451
+ * canonical forms, or equal refusal classes, prove the paths
452
+ * equivalent. Returns `[equivalent, detail]`. */
453
+ async function provePaths(doc, registry, tag) {
454
+ const text = JSON.stringify(doc);
455
+ // A private directory, not a guessable name in the shared tmpdir: a
456
+ // planted symlink there could redirect the write.
457
+ let dir;
458
+ let path = "";
459
+ const resolveWith = (load) => {
460
+ try {
461
+ return { ok: true, resolved: (0, declaration_1.resolveDeclaration)(load(), registry) };
462
+ }
463
+ catch (e) {
464
+ return { ok: false, error: e };
465
+ }
466
+ };
467
+ let fromFile;
468
+ let fileError;
469
+ try {
470
+ dir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "agent-hooks-ctk-"));
471
+ path = (0, node_path_1.join)(dir, `${tag}.json`);
472
+ (0, node_fs_1.writeFileSync)(path, text, { encoding: "utf8", flag: "wx" });
473
+ try {
474
+ fromFile = await index_1.HostDeclaration.fromPath(path);
475
+ }
476
+ catch (e) {
477
+ fileError = e;
478
+ }
479
+ }
480
+ catch (e) {
481
+ fileError = index_1.DeclarationError.single("declaration_error:unreadable", "", `cannot write temporary file: ${e?.constructor?.name ?? "Error"}`);
482
+ }
483
+ finally {
484
+ if (dir !== undefined) {
485
+ try {
486
+ (0, node_fs_1.rmSync)(dir, { recursive: true, force: true });
487
+ }
488
+ catch {
489
+ /* best effort */
490
+ }
491
+ }
492
+ }
493
+ const outcomes = [
494
+ ["value", resolveWith(() => index_1.HostDeclaration.fromValue(doc))],
495
+ ["json", resolveWith(() => index_1.HostDeclaration.fromJson(text))],
496
+ [
497
+ "file",
498
+ fromFile !== undefined
499
+ ? resolveWith(() => fromFile)
500
+ : { ok: false, error: fileError },
501
+ ],
502
+ ["builder", resolveWith(() => builderFromValue(doc).build())],
503
+ ];
504
+ const keyOf = (r) => {
505
+ if (r.ok)
506
+ return `ok:${(0, index_1.canonicalDeclaration)(r.resolved)}`;
507
+ const e = r.error;
508
+ return `err:${e instanceof index_1.DeclarationError ? e.code : `other:${String(e)}`}`;
509
+ };
510
+ const keys = outcomes.map(([, r]) => keyOf(r));
511
+ if (keys.every((k) => k === keys[0]))
512
+ return [true, ""];
513
+ let detail = "";
514
+ outcomes.forEach(([name, r]) => {
515
+ detail += `${name}: ${r.ok ? "accepted" : String(r.error)}; `;
97
516
  });
517
+ const oks = keys.filter((k) => k.startsWith("ok:"));
518
+ if (oks.length >= 2 && oks[0] !== oks[1]) {
519
+ let pos = 0;
520
+ while (pos < oks[0].length && pos < oks[1].length && oks[0][pos] === oks[1][pos])
521
+ pos++;
522
+ detail += `first difference at byte ${pos}`;
523
+ }
524
+ return [false, detail];
98
525
  }
99
- async function runVector(harness, vector) {
526
+ // ---- one vector ---------------------------------------------------------------------
527
+ function fail(v, failures) {
528
+ return {
529
+ id: v.id,
530
+ title: v.title,
531
+ part: v.part ?? undefined,
532
+ status: "fail",
533
+ detail: "",
534
+ failures,
535
+ };
536
+ }
537
+ /** The code surface a harness reports (§13.1), or the one derived from
538
+ * its capabilities and posture. */
539
+ function codeSurfaceOf(harness) {
540
+ return (harness.hostSurface?.() ??
541
+ index_1.HostSurface.fromCapabilities(harness.capabilities, harness.toolSeamHostError ?? "continue"));
542
+ }
543
+ /** Resolve the harness's own declaration against its code surface.
544
+ * Throws {@link DeclarationError} on refusal. {@link runVectors} calls
545
+ * this once before the first vector, as §7.7.9 asks; {@link runVector}
546
+ * calls it when no resolved surface is handed in. */
547
+ function assessHarness(harness) {
548
+ const own = harness.declaration?.();
549
+ if (own === undefined) {
550
+ return {
551
+ capabilities: [...harness.capabilities],
552
+ posture: harness.toolSeamHostError ?? "continue",
553
+ };
554
+ }
555
+ const resolved = (0, declaration_1.resolveSurfaceOnly)(index_1.HostDeclaration.fromValue(own), codeSurfaceOf(harness));
556
+ return {
557
+ capabilities: [...resolved.surface.capabilities],
558
+ posture: resolved.configuration.posture.tool_seam_host_error,
559
+ };
560
+ }
561
+ async function runVector(harness, vector, assessed) {
100
562
  const v = vector;
101
563
  const vectorJson = JSON.stringify(vector);
102
- const capsJson = JSON.stringify([...harness.capabilities].sort());
564
+ // §7.7.9: a harness with its own declaration is assessed against the
565
+ // resolved document, so what ran is what a claim cites.
566
+ const codeSurface = codeSurfaceOf(harness);
567
+ let caps;
568
+ let posture;
569
+ try {
570
+ ({ capabilities: caps, posture } = assessed ?? assessHarness(harness));
571
+ }
572
+ catch (e) {
573
+ return fail(v, [`harness declaration refused: ${e}`]);
574
+ }
575
+ const capsJson = JSON.stringify([...caps].sort());
103
576
  const skip = JSON.parse(native_1.native.ctkShouldSkip(vectorJson, capsJson));
104
577
  if (skip !== null) {
105
578
  return {
106
579
  id: v.id,
107
580
  title: v.title,
581
+ part: v.part ?? undefined,
108
582
  status: "skip",
109
583
  detail: skip,
110
584
  failures: [],
111
585
  };
112
586
  }
113
- // Multi-interceptor vectors (§7.1 fold-through) use interceptor_scripts;
114
- // single-interceptor vectors use interceptor_script. Only the FIRST
115
- // interceptor records: expect.interceptions describes each emission as
116
- // the first-registered interceptor saw it. An empty interceptor_scripts
117
- // registers zero interceptors (§7 fail-closed vector).
118
- const scripts = v.interceptor_scripts ?? [v.interceptor_script];
119
- const first = scripts.length > 0 ? new RecordingInterceptor(scripts[0]) : null;
120
- const interceptors = first ? [first] : [];
121
- for (const s of scripts.slice(1))
122
- interceptors.push(new ScriptedInterceptor(s));
123
- const approval = v.approval_script;
124
- // NB: [] is truthy in JS — an empty approval_script registers NO
125
- // resolver (matches the Rust/Python runners; exercised by AH-CTK-032).
126
- const resolver = approval && approval.length > 0 ? new ScriptedResolver(approval) : null;
587
+ const scripts = new Scripts(v);
127
588
  const mode = (v.mode ?? "enforce");
128
589
  // §13.2: composition vectors carry the profile/knobs they apply to;
129
590
  // absent means the pre-P-003 default (`sequential/first_deny, stop`).
@@ -132,32 +593,99 @@ async function runVector(harness, vector) {
132
593
  const identityProvider = 'identity_provider' in v
133
594
  ? v.identity_provider
134
595
  : 'jcs-sha256';
135
- const redactForApproval = v.redact_for_approval ?? [];
136
- harness.setup(v.scenario, interceptors, resolver, mode, composition, identityProvider, redactForApproval);
137
596
  let rr;
138
- try {
139
- rr = await harness.run();
140
- }
141
- catch (e) {
142
- return {
143
- id: v.id,
144
- title: v.title,
145
- status: "fail",
146
- detail: "",
147
- failures: [`harness.run threw: ${e}`],
148
- };
597
+ const document = v.host_declaration;
598
+ if (document !== undefined) {
599
+ // §7.7.9: a declaration vector builds the emitter through the
600
+ // loader. The runner proves the construction paths itself, then
601
+ // hands the document and the CTK registry to the harness.
602
+ const registry = scripts.registry(codeSurface);
603
+ const [pathsEquivalent, pathDetail] = await provePaths(document, registry, v.id);
604
+ if (harness.setupDeclared === undefined) {
605
+ return fail(v, [
606
+ `harness ${JSON.stringify(harness.name)} declares host_declaration but does not implement setupDeclared`,
607
+ ]);
608
+ }
609
+ let refused = null;
610
+ try {
611
+ harness.setupDeclared(v.scenario, document, registry);
612
+ }
613
+ catch (e) {
614
+ if (!(e instanceof index_1.DeclarationError)) {
615
+ harness.teardown();
616
+ return fail(v, [`harness.setupDeclared threw a non-declaration error: ${e}`]);
617
+ }
618
+ refused = e;
619
+ }
620
+ if (refused !== null) {
621
+ harness.teardown();
622
+ const load = {
623
+ outcome: "refused",
624
+ class: refused.code,
625
+ paths_equivalent: pathsEquivalent,
626
+ detail: pathDetail === "" ? refused.message : `${refused.message}; paths: ${pathDetail}`,
627
+ };
628
+ rr = {
629
+ outcome: "error",
630
+ final_output: null,
631
+ tool_invocations: [],
632
+ error: refused.message,
633
+ identities: [],
634
+ records: [],
635
+ load,
636
+ };
637
+ }
638
+ else {
639
+ try {
640
+ rr = await harness.run();
641
+ }
642
+ catch (e) {
643
+ return fail(v, [`harness.run threw: ${e}`]);
644
+ }
645
+ finally {
646
+ harness.teardown();
647
+ }
648
+ rr.load = {
649
+ outcome: "accepted",
650
+ paths_equivalent: pathsEquivalent,
651
+ ...(pathDetail !== "" ? { detail: pathDetail } : {}),
652
+ };
653
+ }
149
654
  }
150
- finally {
151
- harness.teardown();
655
+ else {
656
+ harness.setup(v.scenario, scripts.interceptors(), scripts.resolver(), mode, composition, identityProvider, scripts.redact);
657
+ try {
658
+ rr = await harness.run();
659
+ }
660
+ catch (e) {
661
+ return fail(v, [`harness.run threw: ${e}`]);
662
+ }
663
+ finally {
664
+ harness.teardown();
665
+ }
152
666
  }
153
667
  // §13.1 posture declaration; absent means the spec default.
154
- const postures = { tool_seam_host_error: harness.toolSeamHostError ?? "continue" };
155
- return JSON.parse(native_1.native.ctkAssert(vectorJson, JSON.stringify(first?.recorded ?? []), runRecordToWire(rr, postures)));
668
+ const postures = { tool_seam_host_error: posture };
669
+ return JSON.parse(native_1.native.ctkAssert(vectorJson, JSON.stringify(scripts.recorded()), runRecordToWire(rr, postures)));
156
670
  }
157
671
  async function runVectors(harnessFactory, vectors) {
158
672
  const out = [];
673
+ // Resolve the harness declaration once, before the first vector
674
+ // (§7.7.9); a refusal fails every vector with the findings.
675
+ let assessed;
676
+ let refusal;
677
+ try {
678
+ assessed = assessHarness(harnessFactory());
679
+ }
680
+ catch (e) {
681
+ refusal = e;
682
+ }
159
683
  for (const v of vectors) {
160
- out.push(await runVector(harnessFactory(), v));
684
+ if (assessed === undefined) {
685
+ out.push(fail(v, [`harness declaration refused: ${refusal}`]));
686
+ continue;
687
+ }
688
+ out.push(await runVector(harnessFactory(), v, assessed));
161
689
  }
162
690
  return out;
163
691
  }