@responsibleai/agent-hooks 0.1.0-beta.1 → 0.1.0-beta.3

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