signafaide-mcp 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/README.md CHANGED
@@ -7,7 +7,7 @@ The signafAIde MCP server for coding agents, over stdio. It is a separate packag
7
7
  the SDK's version:
8
8
 
9
9
  ```json
10
- {"command": "npx", "args": ["-y", "signafaide-mcp@0.2.1"]}
10
+ {"command": "npx", "args": ["-y", "signafaide-mcp@0.3.0"]}
11
11
  ```
12
12
 
13
13
  Tools: `signafaide_setup` (previews the install command and code edit, changes nothing),
@@ -15,4 +15,20 @@ Tools: `signafaide_setup` (previews the install command and code edit, changes n
15
15
  (creates the external sandbox; call it only after the user has agreed, with
16
16
  `confirm_user_agreed: true` and `user_words`, what the user wrote; without both it creates nothing).
17
17
 
18
+ ## Recording everything an agent can call
19
+
20
+ The setup this server previews has two parts beyond wrapping tools. The MCP servers the
21
+ application's own agent uses are started through the gateway of the `signafaide` package,
22
+ `npx -y signafaide gateway --agent <name> -- <the server's command>`, which records every tool
23
+ call that passes through it and forwards none it cannot record. And the program gives the full
24
+ list of the tools it hands the agent, with `declareTools([...])` (TypeScript) or
25
+ `signafaide.declare_tools([...])` (Python), so `signafaide_verify` can say which of the agent's
26
+ tools are recorded.
27
+
28
+ The gateway records every call that goes through it; a call the application makes around it is
29
+ still not recorded, so the deployment has to make the gateway the only route to those tools. A
30
+ declaration is the program's own statement of its tools, not proof that the list is complete. A
31
+ tool list is as fresh as its last declaration: a source that stops leaves the list at its clean
32
+ exit, or within 24 hours after a crash.
33
+
18
34
  Apache-2.0. Source: the `js/` folder of the signafAIde SDK repository.
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var VERSION = "0.2.1";
2
+ var VERSION = "0.3.0";
3
3
 
4
4
  // src/telemetry.ts
5
5
  var HEADER = "X-Signafaide-Client";
@@ -7,7 +7,7 @@ import {
7
7
  clientHeader,
8
8
  detectAgent,
9
9
  telemetryEnabled
10
- } from "./chunk-T43S3SPH.js";
10
+ } from "./chunk-2NXHIOEZ.js";
11
11
 
12
12
  // src/mcp.ts
13
13
  import * as fs6 from "fs";
@@ -641,6 +641,8 @@ function fsError(e, home) {
641
641
  }
642
642
  return e;
643
643
  }
644
+ var TOOL_SOURCE_REFRESH_MS = 36e5;
645
+ var TOOL_SOURCE_MAX_AGE_MS = 864e5;
644
646
  var LOCK_STALE_MS = 1e4;
645
647
  var LOCK_WAIT_MS = 15e3;
646
648
  var PENDING = "_pending";
@@ -1241,18 +1243,97 @@ var State = class _State {
1241
1243
  * and put `wire(record)` in the outbox. Returns null when `stillValid()` is false once
1242
1244
  * the lock is held (the chain was just moved under its project; resolve it again). */
1243
1245
  append(chain, build, wire, stillValid = () => true) {
1246
+ return this.appendRun(chain, [build], wire, stillValid)?.[0] ?? null;
1247
+ }
1248
+ /** `append` for several records that must follow each other with nothing between them (a tool
1249
+ * declaration, §2.6): all of them are built and written under one hold of the chain lock, in
1250
+ * one write, so no other writer's record lands inside the run. */
1251
+ appendRun(chain, builds, wire, stillValid = () => true) {
1244
1252
  this.ensure();
1245
1253
  return this.lock(chain, () => {
1246
1254
  if (!stillValid()) return null;
1247
- const [seq, prev, prevTime] = this.tail(chain);
1248
- const rec = build(seq + 1, prev, prevTime);
1255
+ let [seq, prev, prevTime] = this.tail(chain);
1256
+ const recs = [];
1257
+ for (const build of builds) {
1258
+ const rec = build(seq + 1, prev, prevTime);
1259
+ recs.push(rec);
1260
+ [seq, prev, prevTime] = [rec.envelope.seq, recordHash(rec), rec.envelope.time];
1261
+ }
1262
+ try {
1263
+ fs.appendFileSync(this.chainPath(chain), recs.map((r) => canonicalize(r) + "\n").join(""));
1264
+ } catch (e) {
1265
+ throw fsError(e, this.home);
1266
+ }
1267
+ for (const rec of recs) if (!rec.draft) this.writeOutbox(chain, wire(rec));
1268
+ return recs;
1269
+ });
1270
+ }
1271
+ // --- tool declarations --------------------------------------------------------
1272
+ /** `tools/<agent>.json` (§8): the tools each source (the SDK in an application, each gateway)
1273
+ * last declared for the agent. */
1274
+ toolsPath(agent) {
1275
+ return path.join(this.home, "tools", `${agent}.json`);
1276
+ }
1277
+ /** The tools every source last declared for `agent`: source -> {tool name: recorded}. */
1278
+ toolSources(agent) {
1279
+ return Object.fromEntries(Object.entries(this.toolEntries(agent)).map(([src, e]) => [src, e.tools]));
1280
+ }
1281
+ /** The entries of `tools/<agent>.json`: source -> when it last declared or refreshed, and its tools. */
1282
+ toolEntries(agent) {
1283
+ const doc = readJson(this.toolsPath(agent));
1284
+ const out = {};
1285
+ const sources = doc?.sources;
1286
+ if (!sources || typeof sources !== "object" || Array.isArray(sources)) return out;
1287
+ for (const [src, entry] of Object.entries(sources)) {
1288
+ const tools = entry?.tools;
1289
+ if (!tools || typeof tools !== "object" || Array.isArray(tools)) continue;
1290
+ out[src] = { at: typeof entry.at === "string" ? entry.at : "", tools: Object.fromEntries(Object.entries(tools).filter(([, v]) => typeof v === "boolean")) };
1291
+ }
1292
+ return out;
1293
+ }
1294
+ /** A source that ends cleanly takes its own entry out of `tools/<agent>.json`, so the next
1295
+ * declaration by any source no longer counts its tools (§8). Nothing is written to the chain. */
1296
+ releaseToolSource(agent, source) {
1297
+ const file = this.toolsPath(agent);
1298
+ if (!fs.existsSync(file)) return;
1299
+ this.lockAt(path.join(this.home, "tools", `${agent}.lock`), () => {
1300
+ const entries = this.toolEntries(agent);
1301
+ if (!(source in entries)) return;
1302
+ delete entries[source];
1303
+ writeJson(file, { sources: entries });
1304
+ });
1305
+ }
1306
+ /** Set the tools `source` declares for `agent` and call `write` with the agent's whole tool
1307
+ * list: the union over every source, a tool recorded when any source records it (§8). Held
1308
+ * under `tools/<agent>.lock`, so two sources never write declarations that miss each other's
1309
+ * tools. The source's own entry gets the time `nowMs`; another source's entry not refreshed for
1310
+ * `TOOL_SOURCE_MAX_AGE_MS` is dropped (that source has stopped), and `expired` tells `write`
1311
+ * whether dropping changed the agent's tool list. The file is updated only when `write` returns. */
1312
+ declareTools(agent, source, tools, write, nowMs = Date.now()) {
1313
+ this.ensure();
1314
+ const file = this.toolsPath(agent);
1315
+ return this.lockAt(path.join(this.home, "tools", `${agent}.lock`), () => {
1316
+ const union = (entries) => {
1317
+ const all2 = {};
1318
+ for (const src of Object.keys(entries).sort()) for (const [name, recorded] of Object.entries(entries[src].tools)) all2[name] = all2[name] === true || recorded;
1319
+ return all2;
1320
+ };
1321
+ const sorted = (all2) => JSON.stringify(Object.keys(all2).sort().map((n) => [n, all2[n]]));
1322
+ const held = { ...this.toolEntries(agent), [source]: { at: new Date(nowMs).toISOString(), tools } };
1323
+ const live = Object.fromEntries(
1324
+ Object.entries(held).filter(([src, e]) => {
1325
+ const at = Date.parse(e.at);
1326
+ return src === source || Number.isFinite(at) && nowMs - at <= TOOL_SOURCE_MAX_AGE_MS;
1327
+ })
1328
+ );
1329
+ const all = union(live);
1330
+ const out = write(all, sorted(all) !== sorted(union(held)));
1249
1331
  try {
1250
- fs.appendFileSync(this.chainPath(chain), canonicalize(rec) + "\n");
1332
+ writeJson(file, { sources: live });
1251
1333
  } catch (e) {
1252
1334
  throw fsError(e, this.home);
1253
1335
  }
1254
- if (!rec.draft) this.writeOutbox(chain, wire(rec));
1255
- return rec;
1336
+ return out;
1256
1337
  });
1257
1338
  }
1258
1339
  writeOutbox(chain, rec) {
@@ -1555,6 +1636,69 @@ function wireOf(rec, store) {
1555
1636
  return out;
1556
1637
  }
1557
1638
 
1639
+ // src/coverage.ts
1640
+ var TOOLS = "signafaide.tools";
1641
+ var TOOL_PREFIX = "signafaide.tool/";
1642
+ var TOOL_UNRECORDED_PREFIX = "signafaide.tool_unrecorded/";
1643
+ var RESERVED_PREFIX = "signafaide.";
1644
+ var MAX_DECLARED_NAME = 101;
1645
+ var KEPT = MAX_DECLARED_NAME - 9;
1646
+ function declaredName(tool) {
1647
+ const chars = [...tool];
1648
+ if (chars.length <= MAX_DECLARED_NAME) return tool;
1649
+ return `${chars.slice(0, KEPT).join("")}~${sha256Hex(new TextEncoder().encode(tool)).slice(0, 8)}`;
1650
+ }
1651
+ function toolNoteName(tool, recorded) {
1652
+ return (recorded ? TOOL_PREFIX : TOOL_UNRECORDED_PREFIX) + declaredName(tool);
1653
+ }
1654
+ function toolNote(kind, name) {
1655
+ if (kind !== "note" || typeof name !== "string") return null;
1656
+ if (name === TOOLS) return "header";
1657
+ const recorded = name.startsWith(TOOL_PREFIX);
1658
+ if (!recorded && !name.startsWith(TOOL_UNRECORDED_PREFIX)) return null;
1659
+ const rest = name.slice((recorded ? TOOL_PREFIX : TOOL_UNRECORDED_PREFIX).length);
1660
+ return rest ? { name: rest, recorded } : null;
1661
+ }
1662
+ function byCodePoint(a, b) {
1663
+ const [x, y] = [[...a], [...b]];
1664
+ for (let i = 0; i < x.length && i < y.length; i++) {
1665
+ const d = x[i].codePointAt(0) - y[i].codePointAt(0);
1666
+ if (d) return d;
1667
+ }
1668
+ return x.length - y.length;
1669
+ }
1670
+ function coverage(records) {
1671
+ let at = null;
1672
+ let tools = /* @__PURE__ */ new Map();
1673
+ const called = /* @__PURE__ */ new Set();
1674
+ for (const { seq, kind, name } of records) {
1675
+ const t = toolNote(kind, name);
1676
+ if (t === "header") {
1677
+ at = seq;
1678
+ tools = /* @__PURE__ */ new Map();
1679
+ } else if (t) {
1680
+ if (at !== null) tools.set(t.name, t.recorded);
1681
+ } else if (kind === "decision" && typeof name === "string" && !name.startsWith(RESERVED_PREFIX)) called.add(declaredName(name));
1682
+ }
1683
+ if (at === null) return { declared: [], undeclared_recorded: [], declared_at_seq: null };
1684
+ return {
1685
+ declared: [...tools.keys()].sort(byCodePoint).map((name) => ({ name, recorded: tools.get(name) })),
1686
+ undeclared_recorded: [...called].filter((n) => !tools.has(n)).sort(byCodePoint),
1687
+ declared_at_seq: at
1688
+ };
1689
+ }
1690
+ var countIs = (n, one) => `${plural(n, one)} ${n === 1 ? "is" : "are"}`;
1691
+ function coverageLines(agent, c) {
1692
+ 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`];
1693
+ const unrecorded = c.declared.filter((t) => !t.recorded).map((t) => t.name);
1694
+ const out = [];
1695
+ if (!c.declared.length) out.push(`${agent}: the declared tool list is empty`);
1696
+ else if (!unrecorded.length) out.push(`${agent}: ${c.declared.length === 1 ? "the" : "all"} ${countIs(c.declared.length, "declared tool")} recorded`);
1697
+ else out.push(`${agent}: ${countIs(unrecorded.length, "declared tool")} not recorded: ${unrecorded.join(", ")}`);
1698
+ 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(", ")}`);
1699
+ return out;
1700
+ }
1701
+
1558
1702
  // src/runtime.ts
1559
1703
  var BATCH_MAX = 1e3;
1560
1704
  var KEY_MAX_AGE_MS = 7 * 864e5 - 36e5;
@@ -1587,6 +1731,9 @@ var Recorder = class {
1587
1731
  /** Agents this process wrote records for since its last stop note. */
1588
1732
  wrote = /* @__PURE__ */ new Set();
1589
1733
  toldStrict = false;
1734
+ /** `<agent>\n<source>` -> the tool set this recorder last declared for it (§2.6), and the header's hash. */
1735
+ declared = /* @__PURE__ */ new Map();
1736
+ toolTimer;
1590
1737
  /** @internal */
1591
1738
  _url;
1592
1739
  /** @internal */
@@ -1808,32 +1955,39 @@ ${scope}`;
1808
1955
  /** Sign and chain one record locally (synchronous) and queue it for shipping.
1809
1956
  * The chain is this API key's `<project>/<env>/<agent>` (§8). */
1810
1957
  write(kind, name, content, opts = {}) {
1958
+ return this.writeRun([{ kind, name, content, ref: opts.ref }], opts)[0];
1959
+ }
1960
+ /** `write` for records that must follow each other with nothing between them (a tool
1961
+ * declaration, §2.6): they are signed and appended under one hold of the chain lock. */
1962
+ writeRun(items, opts = {}) {
1811
1963
  const why = noWrites.getStore();
1812
- 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" });
1964
+ 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" });
1813
1965
  const agent = opts.agent || this.defaultAgent;
1814
1966
  if (!AGENT_RE.test(agent)) throw new Error(`agent "${agent}" is invalid: use lowercase letters, digits, '.', '_' or '-' (max 64)`);
1815
1967
  const sc = opts.storeContent ?? this.storeContent;
1816
1968
  const apiKey = this.apiKey;
1817
- let rec = null;
1818
- while (!rec) {
1969
+ let recs = null;
1970
+ while (!recs) {
1819
1971
  const bucket = this.writeBucket(apiKey, agent);
1820
1972
  const scope = scopeOf(bucket);
1821
1973
  const key = this.key(agent, scope);
1822
1974
  const chain = `${bucket}/${agent}`;
1823
1975
  if (!fs2.existsSync(this.state.chainPath(chain))) for (const e of Object.values(this.adoptInto(bucket, apiKey))) this.report(`signafaide: ${e.message}`);
1824
1976
  const [project, env] = bucket.split("/");
1825
- const build = bucket.startsWith(PENDING + "/") ? (seq, prev, prevTime) => {
1826
- const [time, c] = this.timed(content, prevTime, agent, scope);
1827
- return makeDraft(key, { kind, agent, seq, prev, name, content: c, ref: opts.ref ?? null, time }, sc);
1828
- } : (seq, prev, prevTime) => {
1829
- const [time, c] = this.timed(content, prevTime, agent, scope);
1830
- return makeRecord(key, { kind, project, env, agent, seq, prev, name, content: c, ref: this.state.translateRef(chain, opts.ref), time });
1831
- };
1832
- rec = this.state.append(chain, build, (r) => wireRecord(r, sc), () => this.writeBucket(apiKey, agent) === bucket);
1977
+ const builds = items.map(
1978
+ ({ kind, name, content, ref }) => bucket.startsWith(PENDING + "/") ? (seq, prev, prevTime) => {
1979
+ const [time, c] = this.timed(content, prevTime, agent, scope);
1980
+ return makeDraft(key, { kind, agent, seq, prev, name, content: c, ref: ref ?? null, time }, sc);
1981
+ } : (seq, prev, prevTime) => {
1982
+ const [time, c] = this.timed(content, prevTime, agent, scope);
1983
+ return makeRecord(key, { kind, project, env, agent, seq, prev, name, content: c, ref: this.state.translateRef(chain, ref), time });
1984
+ }
1985
+ );
1986
+ recs = this.state.appendRun(chain, builds, (r) => wireRecord(r, sc), () => this.writeBucket(apiKey, agent) === bucket);
1833
1987
  }
1834
- this.pending++;
1988
+ this.pending += recs.length;
1835
1989
  this.exitFlushed = false;
1836
- if (name !== INSTANCE_STOP) this.wrote.add(agent);
1990
+ if (items.some((i) => i.name !== INSTANCE_STOP)) this.wrote.add(agent);
1837
1991
  if (!apiKey && !this.toldLocal && !this.quiet && !this.test) {
1838
1992
  this.toldLocal = true;
1839
1993
  console.error(
@@ -1842,7 +1996,76 @@ ${scope}`;
1842
1996
  }
1843
1997
  this.start();
1844
1998
  if (this.pending >= this.flushSize) void this.tick();
1845
- return rec;
1999
+ return recs;
2000
+ }
2001
+ /** Declare the tools one source knows for an agent (§2.6): a `signafaide.tools` note followed
2002
+ * by one note per tool, written as one run. `source` names who declares (the SDK in this
2003
+ * program, or one gateway); the notes list the agent's whole tool list, the union over every
2004
+ * source that declared on this machine (§8 `tools/<agent>.json`), a tool recorded when any
2005
+ * source records it. Written at most once per recorder for the same set: the first time, and
2006
+ * again when the set changes. Returns the header's record hash. */
2007
+ declareTools(tools, opts) {
2008
+ const agent = opts.agent || this.defaultAgent;
2009
+ if (!AGENT_RE.test(agent)) throw new Error(`agent "${agent}" is invalid: use lowercase letters, digits, '.', '_' or '-' (max 64)`);
2010
+ const names = Object.keys(tools).sort(byCodePoint);
2011
+ const bad = names.find((n) => !n || /[\u0000-\u001f\u007f]/.test(n));
2012
+ if (bad !== void 0) throw new TypeError(`a tool name must be a non-empty string without control characters (got ${JSON.stringify(bad)})`);
2013
+ const id = `${agent}
2014
+ ${opts.source}`;
2015
+ const sig = JSON.stringify(names.map((n) => [n, tools[n]]));
2016
+ const held = this.declared.get(id);
2017
+ const hash = this.state.declareTools(agent, opts.source, tools, (all, expired) => {
2018
+ if (held?.sig === sig && !expired) return held.hash;
2019
+ const byName = /* @__PURE__ */ new Map();
2020
+ for (const [tool, recorded2] of Object.entries(all)) {
2021
+ const d = declaredName(tool);
2022
+ const prev = byName.get(d);
2023
+ if (!prev || recorded2 && !prev.recorded) byName.set(d, { tool, recorded: recorded2 });
2024
+ }
2025
+ const list2 = [...byName.keys()].sort(byCodePoint).map((d) => byName.get(d));
2026
+ const recorded = list2.filter((t) => t.recorded).length;
2027
+ const items = [
2028
+ { kind: "note", name: TOOLS, content: { source: opts.origin, tools: list2.length, recorded, unrecorded: list2.length - recorded } },
2029
+ ...list2.map((t) => ({ kind: "note", name: toolNoteName(t.tool, t.recorded), content: { tool: t.tool, recorded: t.recorded } }))
2030
+ ];
2031
+ return recordHash(this.writeRun(items, { agent, storeContent: opts.storeContent })[0]);
2032
+ });
2033
+ this.declared.set(id, { sig, hash, agent, source: opts.source, origin: opts.origin, tools, storeContent: opts.storeContent });
2034
+ if (!this.toolTimer && !this.closed) {
2035
+ this.toolTimer = setInterval(() => this.refreshToolSources(), TOOL_SOURCE_REFRESH_MS);
2036
+ this.toolTimer.unref?.();
2037
+ }
2038
+ return hash;
2039
+ }
2040
+ /** Refresh this recorder's entries in `tools/<agent>.json` (§8): each gets the current time,
2041
+ * so other sources keep counting its tools, and entries of sources that stopped more than 24
2042
+ * hours ago drop out. Nothing is written to the chain unless that changed the agent's tool
2043
+ * list. Runs by itself once an hour while the process lives. Never throws. */
2044
+ refreshToolSources() {
2045
+ for (const d of [...this.declared.values()]) {
2046
+ try {
2047
+ this.declareTools(d.tools, { agent: d.agent, source: d.source, origin: d.origin, storeContent: d.storeContent });
2048
+ } catch (e) {
2049
+ this.report(`signafaide: could not refresh the tool list of ${d.agent}: ${e.message}`);
2050
+ }
2051
+ }
2052
+ }
2053
+ /** A clean end of this source (§8): its entries leave `tools/<agent>.json`, so the next
2054
+ * declaration by any source no longer counts its tools. Nothing is written to the chain. With
2055
+ * `source`, only that source ends (a gateway run inside another program). Never throws. */
2056
+ releaseToolSources(source) {
2057
+ for (const [id, d] of [...this.declared]) {
2058
+ if (source !== void 0 && d.source !== source) continue;
2059
+ try {
2060
+ this.state.releaseToolSource(d.agent, d.source);
2061
+ } catch {
2062
+ }
2063
+ this.declared.delete(id);
2064
+ }
2065
+ if (!this.declared.size && this.toolTimer) {
2066
+ clearInterval(this.toolTimer);
2067
+ this.toolTimer = void 0;
2068
+ }
1846
2069
  }
1847
2070
  /** Where `agent`'s next record goes. A known project whose chain for `agent` is not on this
1848
2071
  * disk (an empty `SIGNAFAIDE_HOME` on Lambda or Cloud Run) may already have that chain on the
@@ -2262,10 +2485,22 @@ ${k.keyId}`;
2262
2485
  /** The program has nothing left to do: one last attempt, held open at most `exitFlushMs`.
2263
2486
  * Background requests never keep the process alive, so they are cut off here. */
2264
2487
  onBeforeExit = () => {
2488
+ void this.exitAttempt();
2489
+ };
2490
+ /** The last attempt of a program that ends on its own schedule (the gateway, once its server
2491
+ * has exited): what happens by itself when a program runs out of work. Ships what is left and,
2492
+ * when stop notes are on, writes the signed stop note, all within `exitFlushMs`; then stops the
2493
+ * background timer. Never throws. */
2494
+ async shutdown() {
2495
+ await this.exitAttempt();
2496
+ this.releaseToolSources();
2497
+ this.close();
2498
+ }
2499
+ exitAttempt() {
2265
2500
  const stop = this.instanceStop && this.wrote.size > 0 && !this.test;
2266
- if (this.exitFlushed || !this.exitFlushMs || !this.apiKey || !this.waiting() && !stop) return;
2501
+ if (this.exitFlushed || !this.exitFlushMs || !this.apiKey || !this.waiting() && !stop) return void 0;
2267
2502
  this.exitFlushed = true;
2268
- if (this.halted && this.halted.fp === this.configFp()) return;
2503
+ if (this.halted && this.halted.fp === this.configFp()) return void 0;
2269
2504
  this.bgAbort.abort();
2270
2505
  this.bgAbort = new AbortController();
2271
2506
  const ctrl = new AbortController();
@@ -2275,15 +2510,16 @@ ${k.keyId}`;
2275
2510
  clearTimeout(timer);
2276
2511
  this.exitFlushed = true;
2277
2512
  };
2278
- if (!stop) void this.flushLogged(this.exitFlushMs, "exit").finally(done);
2279
- else void this.stop(this.exitFlushMs, "exit").then((r) => {
2513
+ if (!stop) return this.flushLogged(this.exitFlushMs, "exit").then(done, done);
2514
+ return this.stop(this.exitFlushMs, "exit").then((r) => {
2280
2515
  const errs = Object.entries(r.errors);
2281
2516
  if (errs.length && !ctrl.signal.aborted) this.report("signafaide: " + errs.map(([a, e]) => e.message.includes(a) ? e.message : `${a}: ${e.message}`).join("; "));
2282
2517
  }, (e) => {
2283
2518
  if (!ctrl.signal.aborted) this.report(`signafaide: ${e.message}`);
2284
- }).finally(done);
2285
- };
2519
+ }).then(done, done);
2520
+ }
2286
2521
  onExit = () => {
2522
+ this.releaseToolSources();
2287
2523
  const left = this.waiting();
2288
2524
  if (left && !this.quiet && this.apiKey) {
2289
2525
  console.error(`signafaide: ${plural(left, "record")} kept in ${path2.join(this.state.home, "outbox")}; they ship on the next run or with \`signafaide flush\``);
@@ -2292,6 +2528,8 @@ ${k.keyId}`;
2292
2528
  /** Stop the background timer and exit hooks (records stay in the outbox). */
2293
2529
  close() {
2294
2530
  this.closed = true;
2531
+ if (this.toolTimer) clearInterval(this.toolTimer);
2532
+ this.toolTimer = void 0;
2295
2533
  if (this.timer) clearInterval(this.timer);
2296
2534
  this.timer = void 0;
2297
2535
  this.bgAbort.abort();
@@ -2814,6 +3052,9 @@ var Report = class {
2814
3052
  notSent = 0;
2815
3053
  /** The project's name from the signed export statement, when the pack has one. */
2816
3054
  projectName = null;
3055
+ /** Tool coverage per agent (§7; per chain for local state): the agent's current tool
3056
+ * declaration and the recorded tools it does not list. A note, never a failure. */
3057
+ coverage = {};
2817
3058
  /** e.g. `PASS: export of every agent, all records, generated <t>`, or
2818
3059
  * `PASS (agent keys taken from the pack, not independent): export of ...` (§7). */
2819
3060
  get resultLine() {
@@ -2837,6 +3078,11 @@ var Report = class {
2837
3078
  firstFailed() {
2838
3079
  return this.checks.find((c) => c.status === "fail")?.id ?? null;
2839
3080
  }
3081
+ /** Tool coverage in words (§7), agent by agent: what each agent declared, then the recorded
3082
+ * tools its declaration does not list. */
3083
+ coverageLines() {
3084
+ return Object.entries(this.coverage).flatMap(([agent, c]) => coverageLines(agent, c));
3085
+ }
2840
3086
  lines() {
2841
3087
  const out = [...this.notes];
2842
3088
  if (this.project && this.env) {
@@ -2852,6 +3098,7 @@ var Report = class {
2852
3098
  if (c.failures.length > 20) out.push(` ... and ${c.failures.length - 20} more`);
2853
3099
  }
2854
3100
  if (this.checks.some((c) => c.status === "pending")) out.push(PENDING_MEANS);
3101
+ out.push(...this.coverageLines().map((l) => `coverage: ${l}`));
2855
3102
  out.push(...this.after);
2856
3103
  out.push(...this.blocked.map((b) => `BLOCKED ${b}`));
2857
3104
  out.push(this.resultLine);
@@ -2874,6 +3121,7 @@ var Report = class {
2874
3121
  independent: this.independent,
2875
3122
  notes: this.notes,
2876
3123
  checks: this.checks,
3124
+ coverage: this.coverage,
2877
3125
  ...this.after.length ? { not_yet: this.after } : {},
2878
3126
  ...this.blocked.length ? { blocked: this.blocked } : {}
2879
3127
  };
@@ -3002,6 +3250,7 @@ function verifyPack(pack, serviceKeys, opts = {}) {
3002
3250
  byService
3003
3251
  });
3004
3252
  report.projectName = typeof pack.project_name === "string" ? pack.project_name : null;
3253
+ report.coverage = v.coverage;
3005
3254
  return report;
3006
3255
  }
3007
3256
  function earliest(a, b) {
@@ -3015,6 +3264,7 @@ function verifyLocalState(local, serviceKeys, drand = null, serviceKeysPlainHttp
3015
3264
  const checks = v.run(local.records, [], local.chainIds);
3016
3265
  const report = new Report(checks, null, null, local.records.length, [], [], { service_keys: !serviceKeysPlainHttp, agent_keys: true });
3017
3266
  report.nothingCosigned = v.nothingCosigned;
3267
+ report.coverage = v.coverage;
3018
3268
  return report;
3019
3269
  }
3020
3270
  var isU64 = (x) => typeof x === "number" && Number.isInteger(x) && x >= 0;
@@ -3145,6 +3395,8 @@ var Verifier = class {
3145
3395
  firstKey = /* @__PURE__ */ new Map();
3146
3396
  drafts = 0;
3147
3397
  nothingCosigned = false;
3398
+ /** Tool coverage per chain (§7), filled by `run`. */
3399
+ coverage = {};
3148
3400
  /** Local state: chain id -> the witness's signed head a chain continues after (§8). */
3149
3401
  localBases = {};
3150
3402
  /** A chain may start after seq 1 only when the export's from is after the agent's first key. */
@@ -3331,6 +3583,11 @@ var Verifier = class {
3331
3583
  if (starts.length) acc.notes.push(`${INSTANCE_NOTE}: ${starts.join(", ")}`);
3332
3584
  if (clean.length) acc.notes.push(`${CLEAN_RESTART_NOTE}: ${clean.join(", ")}`);
3333
3585
  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)`);
3586
+ for (const agent of [...chains.keys()].sort()) {
3587
+ const chain = chains.get(agent);
3588
+ const seqs = [...chain.keys()].sort((x, y) => x - y);
3589
+ this.coverage[agent] = coverage(seqs.map((seq) => ({ seq, kind: chain.get(seq).r.envelope.kind, name: chain.get(seq).r.envelope.name })));
3590
+ }
3334
3591
  return chains;
3335
3592
  }
3336
3593
  // 5
@@ -3787,7 +4044,7 @@ async function readInput(src, flag) {
3787
4044
  }
3788
4045
  console.error(`WARNING: ${flag} ${src} uses plain http:// on this machine; the keys do not count as independent`);
3789
4046
  }
3790
- return parseJson(JSON.stringify(await (await import("./http-2GGI355E.js")).fetchJson(src)));
4047
+ return parseJson(JSON.stringify(await (await import("./http-ME5BOUS6.js")).fetchJson(src)));
3791
4048
  }
3792
4049
  return parseJson(fs3.readFileSync(src, "utf8"));
3793
4050
  }
@@ -3941,7 +4198,8 @@ Use it when the user asks for an audit trail, tamper-evident records or complian
3941
4198
  A reason or choice that is not a tool call: \`signafaide.note("chose_escalation", {"reason": ...})\` (Python) or \`note(...)\` (TypeScript).
3942
4199
 
3943
4200
  Tools that move money, freeze or close accounts, delete data or send messages: wrap them in strict mode, \`wrap_tools([...], agent="claims-bot", strict=True)\` or \`wrapTools({...}, { agent: "claims-bot", strict: true })\`, so the tool runs only once the witness has co-signed its decision record, and not at all when it cannot be reached. Strict mode needs an API key: until a person opts in, a strict call fails with \`no_key\` and its tool does not run, so say so in your final message.
3944
- 3. Run the agent once, then \`signafaide verify\` (\`uv run signafaide verify\`, \`.venv/bin/signafaide verify\` or \`npx signafaide verify\`). It checks signatures, chain links and content hashes on this machine, and ends with a line such as \`PASS: 4 of 12 checks passed, 8 not checked (nothing co-signed yet)\`. The other checks need the witness.
4201
+ 3. Record what you did not wrap. MCP servers the agent uses: start each one through the gateway in the agent's MCP configuration, \`{"command": "npx", "args": ["-y", "signafaide", "gateway", "--agent", "claims-bot", "--", <the server's command and arguments>]}\`; it records every tool call that passes through it and forwards none it cannot record. Then give the full list of the tools your code hands the agent, once at start-up: \`signafaide.declare_tools(["approve_payout", "escalate", "web_search"], agent="claims-bot")\` (Python) or \`declareTools([...], { agent: "claims-bot" })\` (TypeScript). A tool you did not wrap is declared as not recorded, and \`signafaide verify\` names it. The gateway records only the calls that go through it, so make it the only route to those tools.
4202
+ 4. Run the agent once, then \`signafaide verify\` (\`uv run signafaide verify\`, \`.venv/bin/signafaide verify\` or \`npx signafaide verify\`). It checks signatures, chain links and content hashes on this machine, and ends with a line such as \`PASS: 4 of 12 checks passed, 8 not checked (nothing co-signed yet)\`. The other checks need the witness.
3945
4203
 
3946
4204
  ## Sending records to the witness
3947
4205
 
@@ -4107,6 +4365,12 @@ var ERRORS = [
4107
4365
  means: "Strict mode: the decision got no receipt from the service, so the function did not run.",
4108
4366
  fix: "Fix the cause in the message (often `network`), then call again. Use strict mode only where an action must never run unrecorded."
4109
4367
  },
4368
+ {
4369
+ code: "not_written",
4370
+ status: 0,
4371
+ means: "The gateway could not write the decision record on this machine, so it did not forward the tool call to the MCP server (it fails closed).",
4372
+ fix: "Fix the cause in the message, usually a folder that cannot be written: set SIGNAFAIDE_HOME to a writable folder for the gateway, then call the tool again."
4373
+ },
4110
4374
  {
4111
4375
  code: "no_receipts",
4112
4376
  status: 0,
@@ -4386,7 +4650,8 @@ function steps(language, framework, install, verifyCmd) {
4386
4650
  snippet(language, framework),
4387
4651
  "```",
4388
4652
  "",
4389
- `3. Run the agent once so a wrapped function is called, then: ${verifyCmd} (or the signafaide_verify tool). Its last line must start with PASS.`
4653
+ `3. Record what you did not wrap. For each MCP server the agent uses, start it through the gateway in the agent's MCP configuration: {"command": "npx", "args": ["-y", "signafaide", "gateway", "--agent", "<agent name>", "--", <the server's command and arguments>]}. Then give the full list of the tools your code hands the agent, once at start-up: ${language === "python" ? 'signafaide.declare_tools(["tool_a", "tool_b"], agent="<agent name>")' : 'declareTools(["tool_a", "tool_b"], { agent: "<agent name>" })'}; a tool you did not wrap is declared as not recorded, and verify names it. The gateway records only the calls that go through it, so make it the only route to those tools.`,
4654
+ `4. Run the agent once so a wrapped function is called, then: ${verifyCmd} (or the signafaide_verify tool). Its last line must start with PASS.`
4390
4655
  ].join("\n");
4391
4656
  }
4392
4657
  function createServer(root = process.cwd()) {
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  serve
4
- } from "./chunk-YVYCQPVG.js";
5
- import "./chunk-T43S3SPH.js";
4
+ } from "./chunk-AZJ6Z2QX.js";
5
+ import "./chunk-2NXHIOEZ.js";
6
6
 
7
7
  // src/mcp-bin.ts
8
8
  await serve();
@@ -5,7 +5,7 @@ import {
5
5
  SignafaideError,
6
6
  fetchJson,
7
7
  fetchTransport
8
- } from "./chunk-T43S3SPH.js";
8
+ } from "./chunk-2NXHIOEZ.js";
9
9
  export {
10
10
  Client,
11
11
  DEFAULT_URL,
package/dist/index.js CHANGED
@@ -2,8 +2,8 @@ import {
2
2
  createServer,
3
3
  inside,
4
4
  serve
5
- } from "./chunk-YVYCQPVG.js";
6
- import "./chunk-T43S3SPH.js";
5
+ } from "./chunk-AZJ6Z2QX.js";
6
+ import "./chunk-2NXHIOEZ.js";
7
7
  export {
8
8
  createServer,
9
9
  inside,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signafaide-mcp",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "The signafAIde MCP server for coding agents (Claude Code, Codex, Cursor, Gemini CLI, Copilot, Windsurf).",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",