@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.
@@ -0,0 +1,752 @@
1
+ "use strict";
2
+ // Copyright (c) Microsoft. All rights reserved.
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.DeclarationBuilder = exports.HostDeclaration = exports.HostRegistry = exports.HostSurface = exports.HostRegistryError = exports.DeclarationError = exports.DeclarationErrorClass = exports.CAPABILITIES = exports.MAX_TIMEOUT_MS = exports.MAX_DETAIL_LEN = exports.MAX_KIND_LEN = exports.MAX_ID_LEN = exports.MAX_BINDINGS = exports.MAX_DEPTH = exports.MAX_DOCUMENT_BYTES = exports.SUPPORTED_DECLARATION_VERSIONS = exports.DECLARATION_VERSION = void 0;
5
+ exports.fullKnobSupport = fullKnobSupport;
6
+ exports.validReference = validReference;
7
+ exports.validKind = validKind;
8
+ exports.canonicalDeclaration = canonicalDeclaration;
9
+ exports.resolveDeclaration = resolveDeclaration;
10
+ exports.resolveSurfaceOnly = resolveSurfaceOnly;
11
+ exports.declarationVersions = declarationVersions;
12
+ /**
13
+ * Host declaration document (spec §7.7): the loader, the registry it
14
+ * resolves against, and the builder for the code path.
15
+ *
16
+ * A declaration is one JSON object that fixes a host's configuration
17
+ * (§7.1, §8, §10.1, §13.1), its declared surface (§13.1) and its
18
+ * interceptor bindings. It is a versioned contract of its own
19
+ * (`agent-hooks-declaration/<major>.<minor>`, §7.7.2), separate from
20
+ * the wire version `SPEC_VERSION` and from the package version.
21
+ *
22
+ * Every normative check lives in the Rust core. This module does the
23
+ * two things only the wrapper can do: read a file (step 1 of §7.7.6)
24
+ * and run the host's kind resolvers (step 11, in `emitter.ts`). Steps
25
+ * 2 to 10 cross the native boundary as JSON text, so a document yields
26
+ * the same refusal class here as in every other SDK.
27
+ */
28
+ const promises_1 = require("node:fs/promises");
29
+ const index_1 = require("./index");
30
+ const native_1 = require("./native");
31
+ /** The declaration contract version this SDK writes and accepts
32
+ * (§7.7.2). A test pins it to the core's `declaration_versions()`. */
33
+ exports.DECLARATION_VERSION = "agent-hooks-declaration/1.0";
34
+ /** Every contract version the loader accepts (§7.7.2). Any other
35
+ * `declaration` value is refused as `declaration_error:version_unsupported`. */
36
+ exports.SUPPORTED_DECLARATION_VERSIONS = Object.freeze([
37
+ exports.DECLARATION_VERSION,
38
+ ]);
39
+ /** Largest document the text and file paths accept, in bytes (§7.7.3). */
40
+ exports.MAX_DOCUMENT_BYTES = 1 << 20;
41
+ /** Deepest nesting the text paths accept (§7.7.3). */
42
+ exports.MAX_DEPTH = 32;
43
+ /** Most entries in `bindings` (§7.7.3). */
44
+ exports.MAX_BINDINGS = 256;
45
+ /** Longest binding `id`, registered name and document `id` (§7.7.3). */
46
+ exports.MAX_ID_LEN = 64;
47
+ /** Longest binding `kind` (§7.7.5). */
48
+ exports.MAX_KIND_LEN = 128;
49
+ /** Longest refusal detail (§7.7.3). */
50
+ exports.MAX_DETAIL_LEN = 512;
51
+ /** Largest `timeout_ms`, `interceptor_ms` and `approval_resolver_ms` (§7.7.3). */
52
+ exports.MAX_TIMEOUT_MS = 3_600_000;
53
+ /** The closed capability vocabulary a surface may name (§7.7.4). */
54
+ exports.CAPABILITIES = Object.freeze([
55
+ "model_calls",
56
+ "tool_calls",
57
+ "parallel_tool_calls",
58
+ "streaming",
59
+ "multi_turn",
60
+ "int64_json",
61
+ "bigint_json",
62
+ "incremental_output",
63
+ "host_declaration",
64
+ ]);
65
+ /** §3 lifecycle order; the order sets of points are written in. */
66
+ const POINT_ORDER = Object.freeze([
67
+ "agent_startup",
68
+ "input",
69
+ "pre_model_call",
70
+ "post_model_call",
71
+ "pre_tool_call",
72
+ "post_tool_call",
73
+ "output",
74
+ "agent_shutdown",
75
+ ]);
76
+ /** The §3.2 floor every surface carries. */
77
+ const FLOOR_POINTS = Object.freeze([
78
+ "agent_startup",
79
+ "input",
80
+ "output",
81
+ "agent_shutdown",
82
+ ]);
83
+ const RESERVED_KIND_SEGMENTS = Object.freeze(["agent_hooks", "ctk"]);
84
+ // ---- errors -----------------------------------------------------------------
85
+ /** The eleven refusal classes (§7.7.6), in pipeline order. */
86
+ exports.DeclarationErrorClass = Object.freeze({
87
+ Unreadable: "declaration_error:unreadable",
88
+ Malformed: "declaration_error:malformed",
89
+ VersionUnsupported: "declaration_error:version_unsupported",
90
+ SpecUnsupported: "declaration_error:spec_unsupported",
91
+ UnknownField: "declaration_error:unknown_field",
92
+ InvalidField: "declaration_error:invalid_field",
93
+ Inconsistent: "declaration_error:inconsistent",
94
+ SurfaceUnsupported: "declaration_error:surface_unsupported",
95
+ ReferenceUnresolved: "declaration_error:reference_unresolved",
96
+ KindUnknown: "declaration_error:kind_unknown",
97
+ BindingRejected: "declaration_error:binding_rejected",
98
+ });
99
+ function truncate(s) {
100
+ const chars = Array.from(s);
101
+ if (chars.length <= exports.MAX_DETAIL_LEN)
102
+ return s;
103
+ return chars.slice(0, exports.MAX_DETAIL_LEN - 1).join("") + "…";
104
+ }
105
+ /** A refused declaration (§7.7.6). Carries one class, every finding
106
+ * that step produced and, for `version_unsupported`, the accepted
107
+ * version set. Thrown by every construction path; never a verdict. */
108
+ class DeclarationError extends Error {
109
+ code;
110
+ findings;
111
+ accepted;
112
+ constructor(
113
+ /** The namespaced code, `declaration_error:<class>`. */
114
+ code, findings, accepted = []) {
115
+ // Bound the findings before anything reads them: the message, the
116
+ // `findings` list and every log line built from either carry the
117
+ // same 512-character detail (§7.7.6), so a resolver cannot push an
118
+ // unbounded string into a host's log through `.message`.
119
+ const bounded = DeclarationError.bound(findings);
120
+ super(DeclarationError.describe(code, bounded));
121
+ this.code = code;
122
+ this.name = "DeclarationError";
123
+ this.findings = bounded;
124
+ this.accepted = [...accepted];
125
+ }
126
+ /** One finding under `code`. */
127
+ static single(code, pointer, detail) {
128
+ return new DeclarationError(code, [{ pointer, detail }]);
129
+ }
130
+ /** Rebuild the error the core reported across the native boundary:
131
+ * `code` is the `declaration_error:*` string and `detail` the JSON
132
+ * `{"findings": [...], "accepted": [...]}`. */
133
+ static fromCore(code, detail) {
134
+ let findings = [];
135
+ let accepted = [];
136
+ try {
137
+ const parsed = JSON.parse(detail);
138
+ findings = parsed.findings ?? [];
139
+ accepted = parsed.accepted ?? [];
140
+ }
141
+ catch {
142
+ findings = [{ pointer: "", detail }];
143
+ }
144
+ return new DeclarationError(code, findings, accepted);
145
+ }
146
+ /** Copy `findings` with every detail truncated to {@link MAX_DETAIL_LEN}. */
147
+ static bound(findings) {
148
+ return findings.map((f) => ({ pointer: f.pointer, detail: truncate(f.detail) }));
149
+ }
150
+ /** `code: pointer: detail; pointer: detail` (the Rust `Display`). */
151
+ static describe(code, findings) {
152
+ let out = code;
153
+ findings.forEach((f, i) => {
154
+ out += i === 0 ? ": " : "; ";
155
+ out += f.pointer === "" ? f.detail : `${f.pointer}: ${f.detail}`;
156
+ });
157
+ return out;
158
+ }
159
+ }
160
+ exports.DeclarationError = DeclarationError;
161
+ /** A host registry programming error: a duplicate or reserved name, or
162
+ * a code surface the core rejects. Distinct from
163
+ * {@link DeclarationError}: the document is not at fault. */
164
+ class HostRegistryError extends Error {
165
+ constructor(detail) {
166
+ super(`host registry: ${detail}`);
167
+ this.name = "HostRegistryError";
168
+ }
169
+ }
170
+ exports.HostRegistryError = HostRegistryError;
171
+ /** Run a native declaration call and retype its failures: a
172
+ * `declaration_error:*` code becomes {@link DeclarationError};
173
+ * `marshal_error` (the wrapper described its own surface wrongly)
174
+ * becomes {@link HostRegistryError}. */
175
+ function declarationCall(fn) {
176
+ try {
177
+ return fn();
178
+ }
179
+ catch (e) {
180
+ if (e instanceof native_1.AgentHooksCoreError) {
181
+ if (e.code.startsWith("declaration_error:")) {
182
+ throw DeclarationError.fromCore(e.code, e.detail);
183
+ }
184
+ if (e.code === "marshal_error") {
185
+ throw new HostRegistryError(e.detail);
186
+ }
187
+ }
188
+ throw e;
189
+ }
190
+ }
191
+ /** Every value of every knob the profile consults (§7.2). */
192
+ function fullKnobSupport(profile) {
193
+ switch (profile) {
194
+ case "sequential/first_deny":
195
+ return { on_approval: ["resume", "stop"] };
196
+ case "parallel/strictest":
197
+ return { on_transform_conflict: ["approval", "deny"] };
198
+ case "parallel/unanimous":
199
+ return { on_disagreement: ["approval", "deny"] };
200
+ default:
201
+ return {};
202
+ }
203
+ }
204
+ function sortedPoints(points) {
205
+ const set = new Set(points);
206
+ return POINT_ORDER.filter((p) => set.has(p));
207
+ }
208
+ function sortedStrings(items) {
209
+ return [...new Set(items)].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
210
+ }
211
+ /** Constructors for {@link HostSurface}. The core validates the
212
+ * surface (floor, pairs, closed vocabularies) when a document is
213
+ * resolved against it; a surface that fails there is a
214
+ * {@link HostRegistryError}, never a refusal of the document. */
215
+ exports.HostSurface = Object.freeze({
216
+ /** The smallest honest surface for this SDK: the lifecycle floor,
217
+ * `host_declaration`, every profile with every knob value, posture
218
+ * `continue`, buffered output, bounded timeouts and every accepted
219
+ * contract version. A host adds what its runtime does. */
220
+ sdkDefault() {
221
+ return {
222
+ interception_points: [...FLOOR_POINTS],
223
+ capabilities: ["host_declaration"],
224
+ profiles: {
225
+ "sequential/first_deny": fullKnobSupport("sequential/first_deny"),
226
+ "sequential/run_all": {},
227
+ "parallel/strictest": fullKnobSupport("parallel/strictest"),
228
+ "parallel/unanimous": fullKnobSupport("parallel/unanimous"),
229
+ },
230
+ tool_seam_host_error: "continue",
231
+ streams_unbuffered: false,
232
+ interceptor_timeout: "bounded",
233
+ declaration_versions: [...exports.SUPPORTED_DECLARATION_VERSIONS],
234
+ };
235
+ },
236
+ /** The surface the CTK derives from a harness's capability list and
237
+ * posture (§7.7.9): the floor plus the model points iff
238
+ * `model_calls` plus the tool points iff `tool_calls`. A list naming
239
+ * `incremental_output` needs {@link HostSurface.withExposureBound}
240
+ * on top, or the core refuses the surface. */
241
+ fromCapabilities(caps, posture = "continue") {
242
+ const s = exports.HostSurface.sdkDefault();
243
+ s.tool_seam_host_error = posture;
244
+ const points = new Set(FLOOR_POINTS);
245
+ const capabilities = new Set();
246
+ for (const c of caps) {
247
+ if (c === "model_calls") {
248
+ points.add("pre_model_call");
249
+ points.add("post_model_call");
250
+ }
251
+ else if (c === "tool_calls") {
252
+ points.add("pre_tool_call");
253
+ points.add("post_tool_call");
254
+ }
255
+ else if (c === "incremental_output") {
256
+ s.streams_unbuffered = true;
257
+ }
258
+ capabilities.add(c);
259
+ }
260
+ s.interception_points = sortedPoints(points);
261
+ s.capabilities = sortedStrings(capabilities);
262
+ return s;
263
+ },
264
+ /** Add points (the model pair, the tool pair, or both). */
265
+ withPoints(surface, points) {
266
+ return {
267
+ ...surface,
268
+ interception_points: sortedPoints([...surface.interception_points, ...points]),
269
+ };
270
+ },
271
+ /** Add capabilities. */
272
+ withCapabilities(surface, caps) {
273
+ return { ...surface, capabilities: sortedStrings([...surface.capabilities, ...caps]) };
274
+ },
275
+ /** State the §12.1a exposure bound an incremental host enforces. Also
276
+ * marks the host as able to declare `buffered_output: false`. */
277
+ withExposureBound(surface, bound) {
278
+ return { ...surface, streams_unbuffered: true, exposure_bound: bound };
279
+ },
280
+ });
281
+ /** Reference grammar for resolver and redactor names and binding ids:
282
+ * `^[a-z][a-z0-9_-]{0,63}$`. */
283
+ function validReference(s) {
284
+ return /^[a-z][a-z0-9_-]*$/.test(s) && s.length <= exports.MAX_ID_LEN;
285
+ }
286
+ /** Binding kind grammar (§7.7.5): dot-separated lowercase segments, at
287
+ * least two, at most 128 characters. */
288
+ function validKind(s) {
289
+ if (s.length > exports.MAX_KIND_LEN)
290
+ return false;
291
+ const segs = s.split(".");
292
+ return segs.length >= 2 && segs.every((seg) => /^[a-z][a-z0-9_-]*$/.test(seg));
293
+ }
294
+ /** Everything a host registers in code for a declaration to reference
295
+ * (§7.7.5): the code surface, kind resolvers, custom identity
296
+ * providers, approval resolvers and approval redactors. One registry
297
+ * can build many emitters. */
298
+ class HostRegistry {
299
+ surface;
300
+ allowReserved;
301
+ kinds = new Map();
302
+ identityProviders = new Map();
303
+ approvalResolvers = new Map();
304
+ approvalRedactors = new Map();
305
+ /** A registry over the given code surface. Kinds under the reserved
306
+ * `agent_hooks` and `ctk` segments are refused. */
307
+ constructor(surface, allowReserved = false) {
308
+ this.surface = surface;
309
+ this.allowReserved = allowReserved;
310
+ }
311
+ /** The conformance kit's registry: as the constructor, but the `ctk`
312
+ * kind segment may be registered. */
313
+ static forConformance(surface) {
314
+ return new HostRegistry(surface, true);
315
+ }
316
+ /** Register a kind resolver. */
317
+ kind(kind, resolver) {
318
+ if (!validKind(kind)) {
319
+ throw new HostRegistryError(`kind ${JSON.stringify(kind)} does not match the kind grammar (see spec §7.7.5)`);
320
+ }
321
+ const head = kind.split(".")[0];
322
+ if (RESERVED_KIND_SEGMENTS.includes(head) && !(this.allowReserved && head === "ctk")) {
323
+ throw new HostRegistryError(`kind ${JSON.stringify(kind)} uses the reserved segment ${JSON.stringify(head)} (see spec §7.7.5)`);
324
+ }
325
+ if (this.kinds.has(kind)) {
326
+ throw new HostRegistryError(`kind ${JSON.stringify(kind)} registered twice`);
327
+ }
328
+ if (typeof resolver !== "function") {
329
+ throw new HostRegistryError(`kind ${JSON.stringify(kind)} resolver is not a function`);
330
+ }
331
+ this.kinds.set(kind, resolver);
332
+ return this;
333
+ }
334
+ /** Register a custom identity provider (§10.1 name rules apply). */
335
+ identityProvider(name, fn) {
336
+ if (!/^[a-z][a-z0-9_-]*$/.test(name) || name.startsWith("jcs")) {
337
+ throw new HostRegistryError(`identity provider name ${JSON.stringify(name)} must match ^[a-z][a-z0-9_-]*$ and must not begin with 'jcs' (see spec §10.1)`);
338
+ }
339
+ if (name.length > exports.MAX_ID_LEN) {
340
+ throw new HostRegistryError(`identity provider name ${JSON.stringify(name)} exceeds ${exports.MAX_ID_LEN} characters`);
341
+ }
342
+ if (this.identityProviders.has(name)) {
343
+ throw new HostRegistryError(`identity provider ${JSON.stringify(name)} registered twice`);
344
+ }
345
+ this.identityProviders.set(name, fn);
346
+ return this;
347
+ }
348
+ /** Register an approval resolver under a reference name. */
349
+ approvalResolver(name, resolver) {
350
+ HostRegistry.checkReference(name, "approval resolver");
351
+ if (this.approvalResolvers.has(name)) {
352
+ throw new HostRegistryError(`approval resolver ${JSON.stringify(name)} registered twice`);
353
+ }
354
+ this.approvalResolvers.set(name, resolver);
355
+ return this;
356
+ }
357
+ /** Register an approval redactor under a reference name. */
358
+ approvalRedactor(name, fn) {
359
+ HostRegistry.checkReference(name, "approval redactor");
360
+ if (this.approvalRedactors.has(name)) {
361
+ throw new HostRegistryError(`approval redactor ${JSON.stringify(name)} registered twice`);
362
+ }
363
+ this.approvalRedactors.set(name, fn);
364
+ return this;
365
+ }
366
+ /** The registered names, derived (§7.7.5). */
367
+ names() {
368
+ return {
369
+ identity_providers: sortedStrings(this.identityProviders.keys()),
370
+ approval_resolvers: sortedStrings(this.approvalResolvers.keys()),
371
+ approval_redactors: sortedStrings(this.approvalRedactors.keys()),
372
+ kinds: sortedStrings(this.kinds.keys()),
373
+ };
374
+ }
375
+ /** The `host_json` the core's `declaration_resolve` takes: the surface
376
+ * plus the registered names. */
377
+ hostDescription() {
378
+ return JSON.stringify({ surface: this.surface, ...this.names() });
379
+ }
380
+ /** @internal */
381
+ kindResolver(kind) {
382
+ return this.kinds.get(kind);
383
+ }
384
+ /** @internal */
385
+ identityFn(name) {
386
+ return this.identityProviders.get(name);
387
+ }
388
+ /** @internal */
389
+ approvalResolverRef(name) {
390
+ return this.approvalResolvers.get(name);
391
+ }
392
+ /** @internal */
393
+ redactorFn(name) {
394
+ return this.approvalRedactors.get(name);
395
+ }
396
+ static checkReference(name, what) {
397
+ if (!validReference(name)) {
398
+ throw new HostRegistryError(`${what} name ${JSON.stringify(name)} does not match ^[a-z][a-z0-9_-]{0,63}$`);
399
+ }
400
+ }
401
+ }
402
+ exports.HostRegistry = HostRegistry;
403
+ // ---- document ---------------------------------------------------------------
404
+ /** A declaration that passed steps 2 to 7 (§7.7.6): parsed, of an
405
+ * accepted version, closed, well-typed and internally consistent as
406
+ * stated. Not yet checked against any host; `InterceptionEmitter.
407
+ * fromDeclaration` does that. */
408
+ class HostDeclaration {
409
+ doc;
410
+ text;
411
+ /** `text` is the core's serialization of the validated document and
412
+ * is what every later core call receives. The loader refuses any
413
+ * integer token beyond 2^53 - 1 anywhere in the document (§7.7.6),
414
+ * so `doc`, the JavaScript view of the same text, carries every
415
+ * number exactly. */
416
+ constructor(doc, text) {
417
+ this.doc = doc;
418
+ this.text = text;
419
+ }
420
+ /** Step 1 then {@link HostDeclaration.fromJson}: open exactly `path`,
421
+ * require a regular file of at most {@link MAX_DOCUMENT_BYTES},
422
+ * strict UTF-8 without a byte-order mark, read once. */
423
+ static async fromPath(path) {
424
+ const unreadable = (detail) => DeclarationError.single(exports.DeclarationErrorClass.Unreadable, "", detail);
425
+ let size;
426
+ try {
427
+ const st = await (0, promises_1.stat)(path);
428
+ if (!st.isFile())
429
+ throw unreadable("not a regular file");
430
+ size = st.size;
431
+ }
432
+ catch (e) {
433
+ if (e instanceof DeclarationError)
434
+ throw e;
435
+ throw unreadable(`cannot stat: ${ioClass(e)}`);
436
+ }
437
+ if (size > exports.MAX_DOCUMENT_BYTES) {
438
+ throw unreadable(`document is ${size} bytes; the bound is ${exports.MAX_DOCUMENT_BYTES}`);
439
+ }
440
+ // Read through a bounded loop: a file that grows between the stat
441
+ // and the read is still loaded only up to the bound plus one byte,
442
+ // then refused.
443
+ const buf = Buffer.alloc(exports.MAX_DOCUMENT_BYTES + 1);
444
+ let total = 0;
445
+ try {
446
+ const fh = await (0, promises_1.open)(path, "r");
447
+ try {
448
+ while (total < buf.length) {
449
+ const { bytesRead } = await fh.read(buf, total, buf.length - total, total);
450
+ if (bytesRead === 0)
451
+ break;
452
+ total += bytesRead;
453
+ }
454
+ }
455
+ finally {
456
+ await fh.close();
457
+ }
458
+ }
459
+ catch (e) {
460
+ throw unreadable(`cannot read: ${ioClass(e)}`);
461
+ }
462
+ if (total > exports.MAX_DOCUMENT_BYTES) {
463
+ throw unreadable(`document is ${total} bytes; the bound is ${exports.MAX_DOCUMENT_BYTES}`);
464
+ }
465
+ const bytes = buf.subarray(0, total);
466
+ if (bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) {
467
+ throw unreadable("document starts with a byte-order mark");
468
+ }
469
+ let text;
470
+ try {
471
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
472
+ }
473
+ catch {
474
+ throw unreadable("document is not valid UTF-8");
475
+ }
476
+ return HostDeclaration.fromJson(text);
477
+ }
478
+ /** Steps 2 to 7 over JSON text, in the core. */
479
+ static fromJson(text) {
480
+ const validated = declarationCall(() => native_1.native.declarationValidate(text));
481
+ return new HostDeclaration(JSON.parse(validated), validated);
482
+ }
483
+ /** Steps 2 to 7 over an in-memory value: serialized and handed to
484
+ * {@link HostDeclaration.fromJson}, so size, depth and shape checks
485
+ * are the same code on every path. A non-finite number, which
486
+ * `JSON.stringify` would corrupt to `null`, is refused as
487
+ * `malformed` first. */
488
+ static fromValue(value) {
489
+ const hit = (0, index_1.findNonFinite)(value);
490
+ if (hit !== null) {
491
+ throw DeclarationError.single(exports.DeclarationErrorClass.Malformed, "", `cannot serialize: ${(0, index_1.nonFiniteDetail)(hit)}`);
492
+ }
493
+ let text;
494
+ try {
495
+ text = JSON.stringify(value);
496
+ }
497
+ catch (e) {
498
+ throw DeclarationError.single(exports.DeclarationErrorClass.Malformed, "", `cannot serialize: ${e?.constructor?.name ?? "Error"}`);
499
+ }
500
+ if (text === undefined) {
501
+ throw DeclarationError.single(exports.DeclarationErrorClass.Malformed, "", "cannot serialize: value has no JSON form");
502
+ }
503
+ return HostDeclaration.fromJson(text);
504
+ }
505
+ /** A builder for the code path (§7.7.7). */
506
+ static builder() {
507
+ return new DeclarationBuilder();
508
+ }
509
+ /** The document's own `declaration` value. */
510
+ get version() {
511
+ const v = this.doc["declaration"];
512
+ return typeof v === "string" ? v : "";
513
+ }
514
+ /** The validated document, verbatim (including `$schema`). A copy.
515
+ * Every integer in a validated document is within ±(2^53 - 1), so
516
+ * this view loses nothing against {@link HostDeclaration.toJson}. */
517
+ toValue() {
518
+ return JSON.parse(this.text);
519
+ }
520
+ /** @internal The validated document as JSON text, as the core
521
+ * serialized it. */
522
+ toJson() {
523
+ return this.text;
524
+ }
525
+ }
526
+ exports.HostDeclaration = HostDeclaration;
527
+ function ioClass(e) {
528
+ const err = e;
529
+ if (typeof err?.code === "string")
530
+ return err.code;
531
+ if (typeof err?.name === "string")
532
+ return err.name;
533
+ return "Error";
534
+ }
535
+ /** RFC 8785 canonical JSON of a resolved declaration (the core's
536
+ * `canonical_json`). */
537
+ function canonicalDeclaration(resolved) {
538
+ return native_1.native.canonicalJson(JSON.stringify(resolved));
539
+ }
540
+ /** Steps 8 to 10 of §7.7.6 (after steps 2 to 7 again, in the core):
541
+ * resolve a validated declaration against the registry's surface and
542
+ * names. Throws {@link DeclarationError} on refusal. */
543
+ function resolveDeclaration(declaration, registry) {
544
+ const resolved = declarationCall(() => native_1.native.declarationResolve(declaration.toJson(), registry.hostDescription()));
545
+ return JSON.parse(resolved);
546
+ }
547
+ /** Steps 7 (on the filled surface) and 8 against a code surface alone,
548
+ * without a registry: what the CTK runner uses on a harness's own
549
+ * document to learn the surface a run is assessed against (§7.7.9). */
550
+ function resolveSurfaceOnly(declaration, surface) {
551
+ const resolved = declarationCall(() => native_1.native.declarationResolveSurface(declaration.toJson(), JSON.stringify(surface)));
552
+ return JSON.parse(resolved);
553
+ }
554
+ /** The contract versions the core writes and accepts (§7.7.2). */
555
+ function declarationVersions() {
556
+ return JSON.parse(native_1.native.declarationVersions());
557
+ }
558
+ // ---- builder (code path) ----------------------------------------------------
559
+ /** A null-prototype object: every key, `__proto__` included, is an
560
+ * own member that serializes, so the builder path refuses exactly what
561
+ * the value path refuses. */
562
+ function plain() {
563
+ return Object.create(null);
564
+ }
565
+ /** Builds a declaration document in code, one setter per member
566
+ * (§7.7.7). {@link DeclarationBuilder.build} hands the document to
567
+ * {@link HostDeclaration.fromValue}, so the code path is validated by
568
+ * the same function, with the same classes, as a file. */
569
+ class DeclarationBuilder {
570
+ doc;
571
+ /** A builder with `declaration` set to {@link DECLARATION_VERSION}
572
+ * and an empty `bindings` array. */
573
+ constructor() {
574
+ this.doc = plain();
575
+ this.doc["declaration"] = exports.DECLARATION_VERSION;
576
+ this.doc["bindings"] = [];
577
+ }
578
+ /** A builder with no member set at all, not even `declaration` or
579
+ * `bindings`. For harnesses that must express an incomplete
580
+ * document; a host wants the constructor. */
581
+ static empty() {
582
+ const b = new DeclarationBuilder();
583
+ delete b.doc["declaration"];
584
+ delete b.doc["bindings"];
585
+ return b;
586
+ }
587
+ configuration() {
588
+ return this.sub(this.doc, "configuration");
589
+ }
590
+ configurationSub(key) {
591
+ return this.sub(this.configuration(), key);
592
+ }
593
+ surface() {
594
+ return this.sub(this.doc, "surface");
595
+ }
596
+ sub(parent, key) {
597
+ const existing = parent[key];
598
+ if (existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
599
+ return existing;
600
+ }
601
+ const fresh = plain();
602
+ parent[key] = fresh;
603
+ return fresh;
604
+ }
605
+ /** `declaration` (defaults to {@link DECLARATION_VERSION}). */
606
+ version(v) {
607
+ this.doc["declaration"] = v;
608
+ return this;
609
+ }
610
+ spec(v) {
611
+ this.doc["spec"] = v;
612
+ return this;
613
+ }
614
+ id(v) {
615
+ this.doc["id"] = v;
616
+ return this;
617
+ }
618
+ host(name, version) {
619
+ const h = plain();
620
+ h["name"] = name;
621
+ if (version !== undefined)
622
+ h["version"] = version;
623
+ this.doc["host"] = h;
624
+ return this;
625
+ }
626
+ mode(mode) {
627
+ this.configuration()["mode"] = mode;
628
+ return this;
629
+ }
630
+ /** The composition as the host states it. Knobs the profile does not
631
+ * consult are written out and refused by {@link build}, exactly as
632
+ * in a file. */
633
+ composition(c) {
634
+ const v = plain();
635
+ v["profile"] = c.profile;
636
+ if (c.on_approval !== undefined)
637
+ v["on_approval"] = c.on_approval;
638
+ if (c.on_disagreement !== undefined)
639
+ v["on_disagreement"] = c.on_disagreement;
640
+ if (c.on_transform_conflict !== undefined)
641
+ v["on_transform_conflict"] = c.on_transform_conflict;
642
+ this.configuration()["composition"] = v;
643
+ return this;
644
+ }
645
+ /** A name for `jcs-sha256` or a custom provider, `null` for
646
+ * identity-unbound. */
647
+ identityProvider(name) {
648
+ this.configuration()["identity_provider"] = name;
649
+ return this;
650
+ }
651
+ approvalResolver(name) {
652
+ this.configurationSub("approval")["resolver"] = name;
653
+ return this;
654
+ }
655
+ approvalRedactor(name) {
656
+ this.configurationSub("approval")["redactor"] = name;
657
+ return this;
658
+ }
659
+ toolSeamHostError(posture) {
660
+ this.configurationSub("posture")["tool_seam_host_error"] = posture;
661
+ return this;
662
+ }
663
+ /** `null` writes `null` (unbounded). */
664
+ interceptorTimeoutMs(ms) {
665
+ this.configurationSub("timeouts")["interceptor_ms"] = ms;
666
+ return this;
667
+ }
668
+ /** `null` writes `null` (unbounded). */
669
+ approvalResolverTimeoutMs(ms) {
670
+ this.configurationSub("timeouts")["approval_resolver_ms"] = ms;
671
+ return this;
672
+ }
673
+ /** `null` writes `null` (unbounded). */
674
+ maxBufferedRecords(n) {
675
+ this.configurationSub("records")["max_buffered"] = n;
676
+ return this;
677
+ }
678
+ surfacePoints(points) {
679
+ this.surface()["interception_points"] = sortedPoints(points);
680
+ return this;
681
+ }
682
+ surfaceCapabilities(caps) {
683
+ this.surface()["capabilities"] = sortedStrings(caps);
684
+ return this;
685
+ }
686
+ /** Declare support for one profile and its knob values. */
687
+ surfaceProfile(profile, knobs) {
688
+ const profiles = this.sub(this.surface(), "profiles");
689
+ const k = {};
690
+ if (knobs.on_approval && knobs.on_approval.length > 0)
691
+ k["on_approval"] = [...knobs.on_approval];
692
+ if (knobs.on_disagreement && knobs.on_disagreement.length > 0) {
693
+ k["on_disagreement"] = [...knobs.on_disagreement];
694
+ }
695
+ if (knobs.on_transform_conflict && knobs.on_transform_conflict.length > 0) {
696
+ k["on_transform_conflict"] = [...knobs.on_transform_conflict];
697
+ }
698
+ profiles[profile] = k;
699
+ return this;
700
+ }
701
+ /** `buffered_output` and, when `false`, the required exposure bound. */
702
+ bufferedOutput(buffered, exposureBound) {
703
+ const s = this.surface();
704
+ s["buffered_output"] = buffered;
705
+ if (exposureBound !== undefined)
706
+ s["exposure_bound"] = exposureBound;
707
+ else
708
+ delete s["exposure_bound"];
709
+ return this;
710
+ }
711
+ surfaceDeclarationVersions(versions) {
712
+ this.surface()["declaration_versions"] = sortedStrings(versions);
713
+ return this;
714
+ }
715
+ /** Append one binding. `at` omitted binds at every surface point;
716
+ * `timeoutMs` omitted inherits the configured interceptor timeout,
717
+ * `null` writes `null` (unbounded), a number bounds. */
718
+ bind(id, kind, config = {}, at, timeoutMs) {
719
+ const b = { id, kind, config };
720
+ if (at !== undefined)
721
+ b["at"] = sortedPoints(at);
722
+ if (timeoutMs !== undefined)
723
+ b["timeout_ms"] = timeoutMs;
724
+ const bindings = this.doc["bindings"];
725
+ if (Array.isArray(bindings))
726
+ bindings.push(b);
727
+ else
728
+ this.doc["bindings"] = [b];
729
+ return this;
730
+ }
731
+ /** One `extensions` entry, kept verbatim. */
732
+ extension(key, value) {
733
+ this.sub(this.doc, "extensions")[key] = value;
734
+ return this;
735
+ }
736
+ /** Set a top-level member verbatim. For members this builder has no
737
+ * setter for; the result is validated like any other document (an
738
+ * unknown member is refused as `unknown_field`). */
739
+ raw(key, value) {
740
+ this.doc[key] = value;
741
+ return this;
742
+ }
743
+ /** The document as built, before validation. A copy. */
744
+ toValue() {
745
+ return JSON.parse(JSON.stringify(this.doc));
746
+ }
747
+ /** Validate (steps 2 to 7) through {@link HostDeclaration.fromValue}. */
748
+ build() {
749
+ return HostDeclaration.fromValue(this.doc);
750
+ }
751
+ }
752
+ exports.DeclarationBuilder = DeclarationBuilder;