@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/ctk/runner.js
CHANGED
|
@@ -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,
|
|
10
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
151
|
-
harness.
|
|
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:
|
|
155
|
-
return JSON.parse(native_1.native.ctkAssert(vectorJson, JSON.stringify(
|
|
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
|
-
|
|
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
|
}
|