signafaide 0.2.1 → 0.3.0

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/dist/index.cjs CHANGED
@@ -35,7 +35,7 @@ var VERSION;
35
35
  var init_version = __esm({
36
36
  "src/version.ts"() {
37
37
  "use strict";
38
- VERSION = "0.2.1";
38
+ VERSION = "0.3.0";
39
39
  }
40
40
  });
41
41
 
@@ -248,6 +248,7 @@ __export(src_exports, {
248
248
  HEADER: () => HEADER,
249
249
  JsonNumber: () => JsonNumber,
250
250
  KeysRequired: () => KeysRequired,
251
+ MAX_DECLARED_NAME: () => MAX_DECLARED_NAME,
251
252
  MAX_NODES: () => MAX_NODES,
252
253
  MAX_PROOF_BYTES: () => MAX_PROOF_BYTES,
253
254
  MAX_ROUND: () => MAX_ROUND,
@@ -260,11 +261,17 @@ __export(src_exports, {
260
261
  SigningKey: () => SigningKey,
261
262
  State: () => State,
262
263
  StateError: () => StateError,
264
+ TOOLS: () => TOOLS,
265
+ TOOL_PREFIX: () => TOOL_PREFIX,
266
+ TOOL_SOURCE_MAX_AGE_MS: () => TOOL_SOURCE_MAX_AGE_MS,
267
+ TOOL_SOURCE_REFRESH_MS: () => TOOL_SOURCE_REFRESH_MS,
268
+ TOOL_UNRECORDED_PREFIX: () => TOOL_UNRECORDED_PREFIX,
263
269
  VERSION: () => VERSION,
264
270
  VerificationFailed: () => VerificationFailed,
265
271
  agentName: () => agentName,
266
272
  assertPortableNumbers: () => assertPortableNumbers,
267
273
  auditPath: () => auditPath,
274
+ autoDeclareTools: () => autoDeclareTools,
268
275
  b64url: () => b64url,
269
276
  b64urlDecode: () => b64urlDecode,
270
277
  blockAttestations: () => blockAttestations,
@@ -282,6 +289,10 @@ __export(src_exports, {
282
289
  contentFromText: () => contentFromText,
283
290
  contentHash: () => contentHash,
284
291
  countsText: () => countsText,
292
+ coverage: () => coverage,
293
+ coverageLines: () => coverageLines,
294
+ declareTools: () => declareTools,
295
+ declaredName: () => declaredName,
285
296
  detectAgent: () => detectAgent,
286
297
  draftErrors: () => draftErrors,
287
298
  envelopeErrors: () => envelopeErrors,
@@ -337,6 +348,8 @@ __export(src_exports, {
337
348
  testMode: () => testMode,
338
349
  timestampHeights: () => timestampHeights,
339
350
  toJsonable: () => toJsonable,
351
+ toolNote: () => toolNote,
352
+ toolNoteName: () => toolNoteName,
340
353
  validProjectId: () => validProjectId,
341
354
  validTime: () => validTime,
342
355
  verify: () => verify3,
@@ -1468,6 +1481,8 @@ function fsError(e, home) {
1468
1481
  }
1469
1482
  return e;
1470
1483
  }
1484
+ var TOOL_SOURCE_REFRESH_MS = 36e5;
1485
+ var TOOL_SOURCE_MAX_AGE_MS = 864e5;
1471
1486
  var LOCK_STALE_MS = 1e4;
1472
1487
  var LOCK_WAIT_MS = 15e3;
1473
1488
  var PENDING = "_pending";
@@ -2068,18 +2083,97 @@ var State = class _State {
2068
2083
  * and put `wire(record)` in the outbox. Returns null when `stillValid()` is false once
2069
2084
  * the lock is held (the chain was just moved under its project; resolve it again). */
2070
2085
  append(chain, build, wire, stillValid = () => true) {
2086
+ return this.appendRun(chain, [build], wire, stillValid)?.[0] ?? null;
2087
+ }
2088
+ /** `append` for several records that must follow each other with nothing between them (a tool
2089
+ * declaration, §2.6): all of them are built and written under one hold of the chain lock, in
2090
+ * one write, so no other writer's record lands inside the run. */
2091
+ appendRun(chain, builds, wire, stillValid = () => true) {
2071
2092
  this.ensure();
2072
2093
  return this.lock(chain, () => {
2073
2094
  if (!stillValid()) return null;
2074
- const [seq, prev, prevTime] = this.tail(chain);
2075
- const rec = build(seq + 1, prev, prevTime);
2095
+ let [seq, prev, prevTime] = this.tail(chain);
2096
+ const recs = [];
2097
+ for (const build of builds) {
2098
+ const rec = build(seq + 1, prev, prevTime);
2099
+ recs.push(rec);
2100
+ [seq, prev, prevTime] = [rec.envelope.seq, recordHash(rec), rec.envelope.time];
2101
+ }
2076
2102
  try {
2077
- fs.appendFileSync(this.chainPath(chain), canonicalize(rec) + "\n");
2103
+ fs.appendFileSync(this.chainPath(chain), recs.map((r) => canonicalize(r) + "\n").join(""));
2078
2104
  } catch (e) {
2079
2105
  throw fsError(e, this.home);
2080
2106
  }
2081
- if (!rec.draft) this.writeOutbox(chain, wire(rec));
2082
- return rec;
2107
+ for (const rec of recs) if (!rec.draft) this.writeOutbox(chain, wire(rec));
2108
+ return recs;
2109
+ });
2110
+ }
2111
+ // --- tool declarations --------------------------------------------------------
2112
+ /** `tools/<agent>.json` (§8): the tools each source (the SDK in an application, each gateway)
2113
+ * last declared for the agent. */
2114
+ toolsPath(agent) {
2115
+ return path.join(this.home, "tools", `${agent}.json`);
2116
+ }
2117
+ /** The tools every source last declared for `agent`: source -> {tool name: recorded}. */
2118
+ toolSources(agent) {
2119
+ return Object.fromEntries(Object.entries(this.toolEntries(agent)).map(([src, e]) => [src, e.tools]));
2120
+ }
2121
+ /** The entries of `tools/<agent>.json`: source -> when it last declared or refreshed, and its tools. */
2122
+ toolEntries(agent) {
2123
+ const doc = readJson(this.toolsPath(agent));
2124
+ const out = {};
2125
+ const sources = doc?.sources;
2126
+ if (!sources || typeof sources !== "object" || Array.isArray(sources)) return out;
2127
+ for (const [src, entry] of Object.entries(sources)) {
2128
+ const tools = entry?.tools;
2129
+ if (!tools || typeof tools !== "object" || Array.isArray(tools)) continue;
2130
+ out[src] = { at: typeof entry.at === "string" ? entry.at : "", tools: Object.fromEntries(Object.entries(tools).filter(([, v]) => typeof v === "boolean")) };
2131
+ }
2132
+ return out;
2133
+ }
2134
+ /** A source that ends cleanly takes its own entry out of `tools/<agent>.json`, so the next
2135
+ * declaration by any source no longer counts its tools (§8). Nothing is written to the chain. */
2136
+ releaseToolSource(agent, source) {
2137
+ const file = this.toolsPath(agent);
2138
+ if (!fs.existsSync(file)) return;
2139
+ this.lockAt(path.join(this.home, "tools", `${agent}.lock`), () => {
2140
+ const entries = this.toolEntries(agent);
2141
+ if (!(source in entries)) return;
2142
+ delete entries[source];
2143
+ writeJson(file, { sources: entries });
2144
+ });
2145
+ }
2146
+ /** Set the tools `source` declares for `agent` and call `write` with the agent's whole tool
2147
+ * list: the union over every source, a tool recorded when any source records it (§8). Held
2148
+ * under `tools/<agent>.lock`, so two sources never write declarations that miss each other's
2149
+ * tools. The source's own entry gets the time `nowMs`; another source's entry not refreshed for
2150
+ * `TOOL_SOURCE_MAX_AGE_MS` is dropped (that source has stopped), and `expired` tells `write`
2151
+ * whether dropping changed the agent's tool list. The file is updated only when `write` returns. */
2152
+ declareTools(agent, source, tools, write, nowMs = Date.now()) {
2153
+ this.ensure();
2154
+ const file = this.toolsPath(agent);
2155
+ return this.lockAt(path.join(this.home, "tools", `${agent}.lock`), () => {
2156
+ const union = (entries) => {
2157
+ const all2 = {};
2158
+ for (const src of Object.keys(entries).sort()) for (const [name, recorded] of Object.entries(entries[src].tools)) all2[name] = all2[name] === true || recorded;
2159
+ return all2;
2160
+ };
2161
+ const sorted = (all2) => JSON.stringify(Object.keys(all2).sort().map((n) => [n, all2[n]]));
2162
+ const held = { ...this.toolEntries(agent), [source]: { at: new Date(nowMs).toISOString(), tools } };
2163
+ const live = Object.fromEntries(
2164
+ Object.entries(held).filter(([src, e]) => {
2165
+ const at = Date.parse(e.at);
2166
+ return src === source || Number.isFinite(at) && nowMs - at <= TOOL_SOURCE_MAX_AGE_MS;
2167
+ })
2168
+ );
2169
+ const all = union(live);
2170
+ const out = write(all, sorted(all) !== sorted(union(held)));
2171
+ try {
2172
+ writeJson(file, { sources: live });
2173
+ } catch (e) {
2174
+ throw fsError(e, this.home);
2175
+ }
2176
+ return out;
2083
2177
  });
2084
2178
  }
2085
2179
  writeOutbox(chain, rec) {
@@ -2384,6 +2478,71 @@ function wireOf(rec, store) {
2384
2478
 
2385
2479
  // src/runtime.ts
2386
2480
  init_telemetry();
2481
+
2482
+ // src/coverage.ts
2483
+ var TOOLS = "signafaide.tools";
2484
+ var TOOL_PREFIX = "signafaide.tool/";
2485
+ var TOOL_UNRECORDED_PREFIX = "signafaide.tool_unrecorded/";
2486
+ var RESERVED_PREFIX = "signafaide.";
2487
+ var MAX_DECLARED_NAME = 101;
2488
+ var KEPT = MAX_DECLARED_NAME - 9;
2489
+ function declaredName(tool) {
2490
+ const chars = [...tool];
2491
+ if (chars.length <= MAX_DECLARED_NAME) return tool;
2492
+ return `${chars.slice(0, KEPT).join("")}~${sha256Hex(new TextEncoder().encode(tool)).slice(0, 8)}`;
2493
+ }
2494
+ function toolNoteName(tool, recorded) {
2495
+ return (recorded ? TOOL_PREFIX : TOOL_UNRECORDED_PREFIX) + declaredName(tool);
2496
+ }
2497
+ function toolNote(kind, name) {
2498
+ if (kind !== "note" || typeof name !== "string") return null;
2499
+ if (name === TOOLS) return "header";
2500
+ const recorded = name.startsWith(TOOL_PREFIX);
2501
+ if (!recorded && !name.startsWith(TOOL_UNRECORDED_PREFIX)) return null;
2502
+ const rest = name.slice((recorded ? TOOL_PREFIX : TOOL_UNRECORDED_PREFIX).length);
2503
+ return rest ? { name: rest, recorded } : null;
2504
+ }
2505
+ function byCodePoint(a, b) {
2506
+ const [x, y] = [[...a], [...b]];
2507
+ for (let i = 0; i < x.length && i < y.length; i++) {
2508
+ const d = x[i].codePointAt(0) - y[i].codePointAt(0);
2509
+ if (d) return d;
2510
+ }
2511
+ return x.length - y.length;
2512
+ }
2513
+ function coverage(records) {
2514
+ let at = null;
2515
+ let tools = /* @__PURE__ */ new Map();
2516
+ const called = /* @__PURE__ */ new Set();
2517
+ for (const { seq, kind, name } of records) {
2518
+ const t = toolNote(kind, name);
2519
+ if (t === "header") {
2520
+ at = seq;
2521
+ tools = /* @__PURE__ */ new Map();
2522
+ } else if (t) {
2523
+ if (at !== null) tools.set(t.name, t.recorded);
2524
+ } else if (kind === "decision" && typeof name === "string" && !name.startsWith(RESERVED_PREFIX)) called.add(declaredName(name));
2525
+ }
2526
+ if (at === null) return { declared: [], undeclared_recorded: [], declared_at_seq: null };
2527
+ return {
2528
+ declared: [...tools.keys()].sort(byCodePoint).map((name) => ({ name, recorded: tools.get(name) })),
2529
+ undeclared_recorded: [...called].filter((n) => !tools.has(n)).sort(byCodePoint),
2530
+ declared_at_seq: at
2531
+ };
2532
+ }
2533
+ var countIs = (n, one) => `${plural(n, one)} ${n === 1 ? "is" : "are"}`;
2534
+ function coverageLines(agent, c) {
2535
+ if (c.declared_at_seq === null) return [`${agent}: no tool list was declared, so the pack cannot show which of the agent's tools are recorded`];
2536
+ const unrecorded = c.declared.filter((t) => !t.recorded).map((t) => t.name);
2537
+ const out = [];
2538
+ if (!c.declared.length) out.push(`${agent}: the declared tool list is empty`);
2539
+ else if (!unrecorded.length) out.push(`${agent}: ${c.declared.length === 1 ? "the" : "all"} ${countIs(c.declared.length, "declared tool")} recorded`);
2540
+ else out.push(`${agent}: ${countIs(unrecorded.length, "declared tool")} not recorded: ${unrecorded.join(", ")}`);
2541
+ if (c.undeclared_recorded.length) out.push(`${agent}: ${countIs(c.undeclared_recorded.length, "recorded tool")} not in the declared tool list: ${c.undeclared_recorded.join(", ")}`);
2542
+ return out;
2543
+ }
2544
+
2545
+ // src/runtime.ts
2387
2546
  var BATCH_MAX = 1e3;
2388
2547
  var KEY_MAX_AGE_MS = 7 * 864e5 - 36e5;
2389
2548
  var LOCAL_CODES = /* @__PURE__ */ new Set(["too_old", "chain_exists"]);
@@ -2418,6 +2577,9 @@ var Recorder = class {
2418
2577
  /** Agents this process wrote records for since its last stop note. */
2419
2578
  wrote = /* @__PURE__ */ new Set();
2420
2579
  toldStrict = false;
2580
+ /** `<agent>\n<source>` -> the tool set this recorder last declared for it (§2.6), and the header's hash. */
2581
+ declared = /* @__PURE__ */ new Map();
2582
+ toolTimer;
2421
2583
  /** @internal */
2422
2584
  _url;
2423
2585
  /** @internal */
@@ -2639,32 +2801,39 @@ ${scope}`;
2639
2801
  /** Sign and chain one record locally (synchronous) and queue it for shipping.
2640
2802
  * The chain is this API key's `<project>/<env>/<agent>` (§8). */
2641
2803
  write(kind, name, content, opts = {}) {
2804
+ return this.writeRun([{ kind, name, content, ref: opts.ref }], opts)[0];
2805
+ }
2806
+ /** `write` for records that must follow each other with nothing between them (a tool
2807
+ * declaration, §2.6): they are signed and appended under one hold of the chain lock. */
2808
+ writeRun(items, opts = {}) {
2642
2809
  const why = noWrites.getStore();
2643
- if (why) throw new SignafaideError(`${why} must not write records: candidate functions must be side-effect free (it tried to record "${name}")`, { code: "no_writes" });
2810
+ if (why) throw new SignafaideError(`${why} must not write records: candidate functions must be side-effect free (it tried to record "${items[0]?.name}")`, { code: "no_writes" });
2644
2811
  const agent = opts.agent || this.defaultAgent;
2645
2812
  if (!AGENT_RE.test(agent)) throw new Error(`agent "${agent}" is invalid: use lowercase letters, digits, '.', '_' or '-' (max 64)`);
2646
2813
  const sc = opts.storeContent ?? this.storeContent;
2647
2814
  const apiKey = this.apiKey;
2648
- let rec = null;
2649
- while (!rec) {
2815
+ let recs = null;
2816
+ while (!recs) {
2650
2817
  const bucket = this.writeBucket(apiKey, agent);
2651
2818
  const scope = scopeOf(bucket);
2652
2819
  const key = this.key(agent, scope);
2653
2820
  const chain = `${bucket}/${agent}`;
2654
2821
  if (!fs2.existsSync(this.state.chainPath(chain))) for (const e of Object.values(this.adoptInto(bucket, apiKey))) this.report(`signafaide: ${e.message}`);
2655
2822
  const [project, env] = bucket.split("/");
2656
- const build = bucket.startsWith(PENDING + "/") ? (seq, prev, prevTime) => {
2657
- const [time, c] = this.timed(content, prevTime, agent, scope);
2658
- return makeDraft(key, { kind, agent, seq, prev, name, content: c, ref: opts.ref ?? null, time }, sc);
2659
- } : (seq, prev, prevTime) => {
2660
- const [time, c] = this.timed(content, prevTime, agent, scope);
2661
- return makeRecord(key, { kind, project, env, agent, seq, prev, name, content: c, ref: this.state.translateRef(chain, opts.ref), time });
2662
- };
2663
- rec = this.state.append(chain, build, (r) => wireRecord(r, sc), () => this.writeBucket(apiKey, agent) === bucket);
2823
+ const builds = items.map(
2824
+ ({ kind, name, content, ref }) => bucket.startsWith(PENDING + "/") ? (seq, prev, prevTime) => {
2825
+ const [time, c] = this.timed(content, prevTime, agent, scope);
2826
+ return makeDraft(key, { kind, agent, seq, prev, name, content: c, ref: ref ?? null, time }, sc);
2827
+ } : (seq, prev, prevTime) => {
2828
+ const [time, c] = this.timed(content, prevTime, agent, scope);
2829
+ return makeRecord(key, { kind, project, env, agent, seq, prev, name, content: c, ref: this.state.translateRef(chain, ref), time });
2830
+ }
2831
+ );
2832
+ recs = this.state.appendRun(chain, builds, (r) => wireRecord(r, sc), () => this.writeBucket(apiKey, agent) === bucket);
2664
2833
  }
2665
- this.pending++;
2834
+ this.pending += recs.length;
2666
2835
  this.exitFlushed = false;
2667
- if (name !== INSTANCE_STOP) this.wrote.add(agent);
2836
+ if (items.some((i) => i.name !== INSTANCE_STOP)) this.wrote.add(agent);
2668
2837
  if (!apiKey && !this.toldLocal && !this.quiet && !this.test) {
2669
2838
  this.toldLocal = true;
2670
2839
  console.error(
@@ -2673,7 +2842,76 @@ ${scope}`;
2673
2842
  }
2674
2843
  this.start();
2675
2844
  if (this.pending >= this.flushSize) void this.tick();
2676
- return rec;
2845
+ return recs;
2846
+ }
2847
+ /** Declare the tools one source knows for an agent (§2.6): a `signafaide.tools` note followed
2848
+ * by one note per tool, written as one run. `source` names who declares (the SDK in this
2849
+ * program, or one gateway); the notes list the agent's whole tool list, the union over every
2850
+ * source that declared on this machine (§8 `tools/<agent>.json`), a tool recorded when any
2851
+ * source records it. Written at most once per recorder for the same set: the first time, and
2852
+ * again when the set changes. Returns the header's record hash. */
2853
+ declareTools(tools, opts) {
2854
+ const agent = opts.agent || this.defaultAgent;
2855
+ if (!AGENT_RE.test(agent)) throw new Error(`agent "${agent}" is invalid: use lowercase letters, digits, '.', '_' or '-' (max 64)`);
2856
+ const names = Object.keys(tools).sort(byCodePoint);
2857
+ const bad = names.find((n) => !n || /[\u0000-\u001f\u007f]/.test(n));
2858
+ if (bad !== void 0) throw new TypeError(`a tool name must be a non-empty string without control characters (got ${JSON.stringify(bad)})`);
2859
+ const id = `${agent}
2860
+ ${opts.source}`;
2861
+ const sig = JSON.stringify(names.map((n) => [n, tools[n]]));
2862
+ const held = this.declared.get(id);
2863
+ const hash = this.state.declareTools(agent, opts.source, tools, (all, expired) => {
2864
+ if (held?.sig === sig && !expired) return held.hash;
2865
+ const byName = /* @__PURE__ */ new Map();
2866
+ for (const [tool, recorded2] of Object.entries(all)) {
2867
+ const d = declaredName(tool);
2868
+ const prev = byName.get(d);
2869
+ if (!prev || recorded2 && !prev.recorded) byName.set(d, { tool, recorded: recorded2 });
2870
+ }
2871
+ const list = [...byName.keys()].sort(byCodePoint).map((d) => byName.get(d));
2872
+ const recorded = list.filter((t) => t.recorded).length;
2873
+ const items = [
2874
+ { kind: "note", name: TOOLS, content: { source: opts.origin, tools: list.length, recorded, unrecorded: list.length - recorded } },
2875
+ ...list.map((t) => ({ kind: "note", name: toolNoteName(t.tool, t.recorded), content: { tool: t.tool, recorded: t.recorded } }))
2876
+ ];
2877
+ return recordHash(this.writeRun(items, { agent, storeContent: opts.storeContent })[0]);
2878
+ });
2879
+ this.declared.set(id, { sig, hash, agent, source: opts.source, origin: opts.origin, tools, storeContent: opts.storeContent });
2880
+ if (!this.toolTimer && !this.closed) {
2881
+ this.toolTimer = setInterval(() => this.refreshToolSources(), TOOL_SOURCE_REFRESH_MS);
2882
+ this.toolTimer.unref?.();
2883
+ }
2884
+ return hash;
2885
+ }
2886
+ /** Refresh this recorder's entries in `tools/<agent>.json` (§8): each gets the current time,
2887
+ * so other sources keep counting its tools, and entries of sources that stopped more than 24
2888
+ * hours ago drop out. Nothing is written to the chain unless that changed the agent's tool
2889
+ * list. Runs by itself once an hour while the process lives. Never throws. */
2890
+ refreshToolSources() {
2891
+ for (const d of [...this.declared.values()]) {
2892
+ try {
2893
+ this.declareTools(d.tools, { agent: d.agent, source: d.source, origin: d.origin, storeContent: d.storeContent });
2894
+ } catch (e) {
2895
+ this.report(`signafaide: could not refresh the tool list of ${d.agent}: ${e.message}`);
2896
+ }
2897
+ }
2898
+ }
2899
+ /** A clean end of this source (§8): its entries leave `tools/<agent>.json`, so the next
2900
+ * declaration by any source no longer counts its tools. Nothing is written to the chain. With
2901
+ * `source`, only that source ends (a gateway run inside another program). Never throws. */
2902
+ releaseToolSources(source) {
2903
+ for (const [id, d] of [...this.declared]) {
2904
+ if (source !== void 0 && d.source !== source) continue;
2905
+ try {
2906
+ this.state.releaseToolSource(d.agent, d.source);
2907
+ } catch {
2908
+ }
2909
+ this.declared.delete(id);
2910
+ }
2911
+ if (!this.declared.size && this.toolTimer) {
2912
+ clearInterval(this.toolTimer);
2913
+ this.toolTimer = void 0;
2914
+ }
2677
2915
  }
2678
2916
  /** Where `agent`'s next record goes. A known project whose chain for `agent` is not on this
2679
2917
  * disk (an empty `SIGNAFAIDE_HOME` on Lambda or Cloud Run) may already have that chain on the
@@ -3093,10 +3331,22 @@ ${k.keyId}`;
3093
3331
  /** The program has nothing left to do: one last attempt, held open at most `exitFlushMs`.
3094
3332
  * Background requests never keep the process alive, so they are cut off here. */
3095
3333
  onBeforeExit = () => {
3334
+ void this.exitAttempt();
3335
+ };
3336
+ /** The last attempt of a program that ends on its own schedule (the gateway, once its server
3337
+ * has exited): what happens by itself when a program runs out of work. Ships what is left and,
3338
+ * when stop notes are on, writes the signed stop note, all within `exitFlushMs`; then stops the
3339
+ * background timer. Never throws. */
3340
+ async shutdown() {
3341
+ await this.exitAttempt();
3342
+ this.releaseToolSources();
3343
+ this.close();
3344
+ }
3345
+ exitAttempt() {
3096
3346
  const stop2 = this.instanceStop && this.wrote.size > 0 && !this.test;
3097
- if (this.exitFlushed || !this.exitFlushMs || !this.apiKey || !this.waiting() && !stop2) return;
3347
+ if (this.exitFlushed || !this.exitFlushMs || !this.apiKey || !this.waiting() && !stop2) return void 0;
3098
3348
  this.exitFlushed = true;
3099
- if (this.halted && this.halted.fp === this.configFp()) return;
3349
+ if (this.halted && this.halted.fp === this.configFp()) return void 0;
3100
3350
  this.bgAbort.abort();
3101
3351
  this.bgAbort = new AbortController();
3102
3352
  const ctrl = new AbortController();
@@ -3106,15 +3356,16 @@ ${k.keyId}`;
3106
3356
  clearTimeout(timer);
3107
3357
  this.exitFlushed = true;
3108
3358
  };
3109
- if (!stop2) void this.flushLogged(this.exitFlushMs, "exit").finally(done);
3110
- else void this.stop(this.exitFlushMs, "exit").then((r) => {
3359
+ if (!stop2) return this.flushLogged(this.exitFlushMs, "exit").then(done, done);
3360
+ return this.stop(this.exitFlushMs, "exit").then((r) => {
3111
3361
  const errs = Object.entries(r.errors);
3112
3362
  if (errs.length && !ctrl.signal.aborted) this.report("signafaide: " + errs.map(([a, e]) => e.message.includes(a) ? e.message : `${a}: ${e.message}`).join("; "));
3113
3363
  }, (e) => {
3114
3364
  if (!ctrl.signal.aborted) this.report(`signafaide: ${e.message}`);
3115
- }).finally(done);
3116
- };
3365
+ }).then(done, done);
3366
+ }
3117
3367
  onExit = () => {
3368
+ this.releaseToolSources();
3118
3369
  const left = this.waiting();
3119
3370
  if (left && !this.quiet && this.apiKey) {
3120
3371
  console.error(`signafaide: ${plural(left, "record")} kept in ${path2.join(this.state.home, "outbox")}; they ship on the next run or with \`signafaide flush\``);
@@ -3123,6 +3374,8 @@ ${k.keyId}`;
3123
3374
  /** Stop the background timer and exit hooks (records stay in the outbox). */
3124
3375
  close() {
3125
3376
  this.closed = true;
3377
+ if (this.toolTimer) clearInterval(this.toolTimer);
3378
+ this.toolTimer = void 0;
3126
3379
  if (this.timer) clearInterval(this.timer);
3127
3380
  this.timer = void 0;
3128
3381
  this.bgAbort.abort();
@@ -3651,6 +3904,9 @@ var Report = class {
3651
3904
  notSent = 0;
3652
3905
  /** The project's name from the signed export statement, when the pack has one. */
3653
3906
  projectName = null;
3907
+ /** Tool coverage per agent (§7; per chain for local state): the agent's current tool
3908
+ * declaration and the recorded tools it does not list. A note, never a failure. */
3909
+ coverage = {};
3654
3910
  /** e.g. `PASS: export of every agent, all records, generated <t>`, or
3655
3911
  * `PASS (agent keys taken from the pack, not independent): export of ...` (§7). */
3656
3912
  get resultLine() {
@@ -3674,6 +3930,11 @@ var Report = class {
3674
3930
  firstFailed() {
3675
3931
  return this.checks.find((c) => c.status === "fail")?.id ?? null;
3676
3932
  }
3933
+ /** Tool coverage in words (§7), agent by agent: what each agent declared, then the recorded
3934
+ * tools its declaration does not list. */
3935
+ coverageLines() {
3936
+ return Object.entries(this.coverage).flatMap(([agent, c]) => coverageLines(agent, c));
3937
+ }
3677
3938
  lines() {
3678
3939
  const out = [...this.notes];
3679
3940
  if (this.project && this.env) {
@@ -3689,6 +3950,7 @@ var Report = class {
3689
3950
  if (c.failures.length > 20) out.push(` ... and ${c.failures.length - 20} more`);
3690
3951
  }
3691
3952
  if (this.checks.some((c) => c.status === "pending")) out.push(PENDING_MEANS);
3953
+ out.push(...this.coverageLines().map((l) => `coverage: ${l}`));
3692
3954
  out.push(...this.after);
3693
3955
  out.push(...this.blocked.map((b) => `BLOCKED ${b}`));
3694
3956
  out.push(this.resultLine);
@@ -3711,6 +3973,7 @@ var Report = class {
3711
3973
  independent: this.independent,
3712
3974
  notes: this.notes,
3713
3975
  checks: this.checks,
3976
+ coverage: this.coverage,
3714
3977
  ...this.after.length ? { not_yet: this.after } : {},
3715
3978
  ...this.blocked.length ? { blocked: this.blocked } : {}
3716
3979
  };
@@ -3839,6 +4102,7 @@ function verifyPack(pack, serviceKeys, opts = {}) {
3839
4102
  byService
3840
4103
  });
3841
4104
  report.projectName = typeof pack.project_name === "string" ? pack.project_name : null;
4105
+ report.coverage = v.coverage;
3842
4106
  return report;
3843
4107
  }
3844
4108
  function earliest(a, b) {
@@ -3852,6 +4116,7 @@ function verifyLocalState(local, serviceKeys, drand = null, serviceKeysPlainHttp
3852
4116
  const checks = v.run(local.records, [], local.chainIds);
3853
4117
  const report = new Report(checks, null, null, local.records.length, [], [], { service_keys: !serviceKeysPlainHttp, agent_keys: true });
3854
4118
  report.nothingCosigned = v.nothingCosigned;
4119
+ report.coverage = v.coverage;
3855
4120
  return report;
3856
4121
  }
3857
4122
  var isU64 = (x) => typeof x === "number" && Number.isInteger(x) && x >= 0;
@@ -3982,6 +4247,8 @@ var Verifier = class {
3982
4247
  firstKey = /* @__PURE__ */ new Map();
3983
4248
  drafts = 0;
3984
4249
  nothingCosigned = false;
4250
+ /** Tool coverage per chain (§7), filled by `run`. */
4251
+ coverage = {};
3985
4252
  /** Local state: chain id -> the witness's signed head a chain continues after (§8). */
3986
4253
  localBases = {};
3987
4254
  /** A chain may start after seq 1 only when the export's from is after the agent's first key. */
@@ -4168,6 +4435,11 @@ var Verifier = class {
4168
4435
  if (starts.length) acc.notes.push(`${INSTANCE_NOTE}: ${starts.join(", ")}`);
4169
4436
  if (clean.length) acc.notes.push(`${CLEAN_RESTART_NOTE}: ${clean.join(", ")}`);
4170
4437
  if (based.length) acc.notes.push(`continues after a co-signed record this machine does not hold: ${based.join(", ")} (the witness's signed head, see signed heads)`);
4438
+ for (const agent of [...chains.keys()].sort()) {
4439
+ const chain = chains.get(agent);
4440
+ const seqs = [...chain.keys()].sort((x, y) => x - y);
4441
+ this.coverage[agent] = coverage(seqs.map((seq) => ({ seq, kind: chain.get(seq).r.envelope.kind, name: chain.get(seq).r.envelope.name })));
4442
+ }
4171
4443
  return chains;
4172
4444
  }
4173
4445
  // 5
@@ -4592,16 +4864,89 @@ init_version();
4592
4864
  var NotRecorded = class extends SignafaideError {
4593
4865
  name = "NotRecorded";
4594
4866
  };
4867
+ var WRAPPED = /* @__PURE__ */ new Map();
4868
+ var LISTED = /* @__PURE__ */ new Map();
4869
+ var EXPLICIT = /* @__PURE__ */ new Map();
4870
+ var toolsVersion = 0;
4871
+ var checked = /* @__PURE__ */ new WeakMap();
4872
+ var toldDeclare = false;
4873
+ function autoDeclareTools(env = process.env) {
4874
+ return !/^(0|false|no|off)$/i.test((env.SIGNAFAIDE_DECLARE_TOOLS ?? "").trim());
4875
+ }
4876
+ function remember(map, agent, name) {
4877
+ const set = map.get(agent ?? "") ?? /* @__PURE__ */ new Set();
4878
+ map.set(agent ?? "", set);
4879
+ if (!set.has(name)) {
4880
+ set.add(name);
4881
+ toolsVersion++;
4882
+ }
4883
+ }
4884
+ function namesFor(map, agent, isDefault) {
4885
+ return /* @__PURE__ */ new Set([...map.get(agent) ?? [], ...isDefault ? map.get("") ?? [] : []]);
4886
+ }
4887
+ function ownTools(rec, agent) {
4888
+ const isDefault = agent === rec.defaultAgent;
4889
+ const explicit = EXPLICIT.get(agent) ?? (isDefault ? EXPLICIT.get("") : void 0);
4890
+ if (explicit) {
4891
+ const wrapped = namesFor(WRAPPED, agent, isDefault);
4892
+ const tools = {};
4893
+ for (const n of explicit.names) tools[n] = wrapped.has(n) && !explicit.unrecorded.has(n);
4894
+ for (const n of explicit.unrecorded) tools[n] = false;
4895
+ return [tools, "declared"];
4896
+ }
4897
+ const listed = namesFor(LISTED, agent, isDefault);
4898
+ if (!listed.size || !autoDeclareTools()) return null;
4899
+ return [Object.fromEntries([...listed].map((n) => [n, true])), "wrap_tools"];
4900
+ }
4901
+ function declareOwn(rec, agentOpt, storeContent) {
4902
+ const agent = agentOpt || rec.defaultAgent;
4903
+ const own = ownTools(rec, agent);
4904
+ if (!own) return "";
4905
+ return rec.declareTools(own[0], { agent, source: "sdk", origin: own[1], storeContent });
4906
+ }
4907
+ function declareBeforeCall(rec, agentOpt) {
4908
+ const agent = agentOpt || rec.defaultAgent;
4909
+ const seen = checked.get(rec) ?? /* @__PURE__ */ new Map();
4910
+ checked.set(rec, seen);
4911
+ if (seen.get(agent) === toolsVersion || noWrites.getStore()) return;
4912
+ try {
4913
+ declareOwn(rec, agentOpt);
4914
+ seen.set(agent, toolsVersion);
4915
+ } catch (e) {
4916
+ seen.set(agent, toolsVersion);
4917
+ if (!toldDeclare) {
4918
+ toldDeclare = true;
4919
+ console.error(`signafaide: could not write the tool declaration for ${agent}: ${e.message}`);
4920
+ }
4921
+ }
4922
+ }
4923
+ function declareTools(names, opts = {}) {
4924
+ const list = [...names];
4925
+ const unrecorded = [...opts.unrecorded ?? []];
4926
+ for (const n of [...list, ...unrecorded]) if (typeof n !== "string" || !n) throw new TypeError("declareTools needs tool names: non-empty strings");
4927
+ EXPLICIT.set(opts.agent ?? "", { names: list, unrecorded: new Set(unrecorded) });
4928
+ toolsVersion++;
4929
+ if (recordingDisabled()) return "";
4930
+ const rec = getRecorder();
4931
+ const hash = declareOwn(rec, opts.agent);
4932
+ const seen = checked.get(rec) ?? /* @__PURE__ */ new Map();
4933
+ checked.set(rec, seen);
4934
+ seen.set(opts.agent || rec.defaultAgent, toolsVersion);
4935
+ return hash;
4936
+ }
4595
4937
  var Spec = class {
4596
- constructor(name, opts) {
4938
+ constructor(name, opts, listed = false) {
4597
4939
  this.name = name;
4598
4940
  this.opts = opts;
4599
4941
  if (!name) throw new TypeError('record(fn) needs a name: record(fn, { name: "approve_payout" })');
4942
+ remember(WRAPPED, opts.agent, name);
4943
+ if (listed) remember(LISTED, opts.agent, name);
4600
4944
  }
4601
4945
  name;
4602
4946
  opts;
4603
4947
  decide(input) {
4604
4948
  const rec = getRecorder();
4949
+ declareBeforeCall(rec, this.opts.agent);
4605
4950
  const meta = { sdk: `signafaide-js/${VERSION}`, ...this.opts.meta ?? {} };
4606
4951
  const content = { input: toJsonable(input) ?? null, meta: toJsonable(meta) };
4607
4952
  return [rec, rec.write("decision", this.name, content, { agent: this.opts.agent, storeContent: this.opts.storeContent })];
@@ -4655,7 +5000,9 @@ var Spec = class {
4655
5000
  }
4656
5001
  };
4657
5002
  function record(fn, opts = {}) {
4658
- const spec = new Spec(opts.name ?? fn.name, opts);
5003
+ return recordWith(new Spec(opts.name ?? fn.name, opts), fn);
5004
+ }
5005
+ function recordWith(spec, fn) {
4659
5006
  const wrapped = function(...args) {
4660
5007
  return spec.call(args.length === 1 ? args[0] : args, () => fn.apply(this, args));
4661
5008
  };
@@ -4678,7 +5025,8 @@ var isOpenAIAgentsTool = (t) => t?.type === "function" && typeof t.invoke === "f
4678
5025
  var isLangChainTool = (t) => Array.isArray(t?.lc_namespace) && typeof t._call === "function" && typeof t.invoke === "function";
4679
5026
  function wrapTool(tool, opts = {}) {
4680
5027
  const t = tool;
4681
- const spec = (name) => new Spec(opts.name ?? name, opts);
5028
+ const { declareTools: declare = true, ...ropts } = opts;
5029
+ const spec = (name) => new Spec(ropts.name ?? name, ropts, declare);
4682
5030
  if (isOpenAIAgentsTool(t)) {
4683
5031
  const s = spec(t.name);
4684
5032
  ADAPTERS.add("openai-agents");
@@ -4696,7 +5044,7 @@ function wrapTool(tool, opts = {}) {
4696
5044
  };
4697
5045
  return proxy;
4698
5046
  }
4699
- if (typeof t === "function") return record(t, { ...opts, name: opts.name ?? t.name });
5047
+ if (typeof t === "function") return recordWith(spec(t.name), t);
4700
5048
  if (typeof t?.execute === "function") {
4701
5049
  const s = spec(t.name ?? "");
4702
5050
  ADAPTERS.add("vercel-ai");
@@ -5342,6 +5690,7 @@ async function verify3(pack, opts = {}) {
5342
5690
  HEADER,
5343
5691
  JsonNumber,
5344
5692
  KeysRequired,
5693
+ MAX_DECLARED_NAME,
5345
5694
  MAX_NODES,
5346
5695
  MAX_PROOF_BYTES,
5347
5696
  MAX_ROUND,
@@ -5354,11 +5703,17 @@ async function verify3(pack, opts = {}) {
5354
5703
  SigningKey,
5355
5704
  State,
5356
5705
  StateError,
5706
+ TOOLS,
5707
+ TOOL_PREFIX,
5708
+ TOOL_SOURCE_MAX_AGE_MS,
5709
+ TOOL_SOURCE_REFRESH_MS,
5710
+ TOOL_UNRECORDED_PREFIX,
5357
5711
  VERSION,
5358
5712
  VerificationFailed,
5359
5713
  agentName,
5360
5714
  assertPortableNumbers,
5361
5715
  auditPath,
5716
+ autoDeclareTools,
5362
5717
  b64url,
5363
5718
  b64urlDecode,
5364
5719
  blockAttestations,
@@ -5376,6 +5731,10 @@ async function verify3(pack, opts = {}) {
5376
5731
  contentFromText,
5377
5732
  contentHash,
5378
5733
  countsText,
5734
+ coverage,
5735
+ coverageLines,
5736
+ declareTools,
5737
+ declaredName,
5379
5738
  detectAgent,
5380
5739
  draftErrors,
5381
5740
  envelopeErrors,
@@ -5431,6 +5790,8 @@ async function verify3(pack, opts = {}) {
5431
5790
  testMode,
5432
5791
  timestampHeights,
5433
5792
  toJsonable,
5793
+ toolNote,
5794
+ toolNoteName,
5434
5795
  validProjectId,
5435
5796
  validTime,
5436
5797
  verify,