@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.
- package/README.md +72 -7
- package/binding.d.ts +27 -0
- package/binding.js +59 -54
- package/dist/builder.js +3 -4
- package/dist/ctk/index.d.ts +44 -5
- package/dist/ctk/index.js +6 -10
- package/dist/ctk/reference.d.ts +8 -8
- package/dist/ctk/reference.declaration.json +33 -0
- package/dist/ctk/reference.js +86 -34
- package/dist/ctk/runner.d.ts +30 -10
- package/dist/ctk/runner.js +569 -42
- package/dist/declaration.d.ts +378 -0
- package/dist/declaration.js +752 -0
- package/dist/emitter.d.ts +68 -14
- package/dist/emitter.js +281 -45
- package/dist/index.d.ts +23 -3
- package/dist/index.js +49 -8
- package/dist/native.d.ts +9 -8
- package/dist/native.js +13 -5
- package/package.json +6 -6
package/dist/ctk/runner.js
CHANGED
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// Copyright (c) Microsoft
|
|
3
|
-
|
|
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,
|
|
10
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
151
|
-
harness.
|
|
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:
|
|
155
|
-
return JSON.parse(native_1.native.ctkAssert(vectorJson, JSON.stringify(
|
|
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
|
-
|
|
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
|
}
|