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/README.md CHANGED
@@ -74,6 +74,35 @@ const list = wrapTools([lookupOrder, refund]); // array
74
74
 
75
75
  None of them is a dependency: `wrapTools` recognises the objects by their shape.
76
76
 
77
+ ## Recording everything an agent can call
78
+
79
+ Wrapping records the tools you wrap. Two things cover the rest: the gateway records every MCP tool call with no wrapper at all, and a tool declaration says in the chain which tools the agent has and which of them are recorded.
80
+
81
+ MCP tools: the gateway. In the agent's MCP configuration, start the gateway in place of the server:
82
+
83
+ ```json
84
+ {"command": "npx", "args": ["-y", "signafaide", "gateway", "--agent", "support-bot", "--", "npx", "-y", "@modelcontextprotocol/server-filesystem", "/data"]}
85
+ ```
86
+
87
+ It runs the server, relays every message unchanged, declares the server's tools as recorded, and writes a `decision` before each `tools/call` is forwarded and an `outcome` when the answer arrives. A call that cannot be recorded is not forwarded. `--strict` (or `--strict-tools a,b`) forwards a call only once the witness has co-signed its decision; `--name-prefix github.` keeps the tool names of several servers apart; `--store-content` sends inputs and outputs too. A remote (HTTP) server goes behind a stdio bridge such as [`mcp-remote`](https://github.com/geelen/mcp-remote):
88
+
89
+ ```bash
90
+ npx -y signafaide gateway --agent support-bot -- npx -y mcp-remote https://mcp.example.com/mcp
91
+ ```
92
+
93
+ Framework tools: tool declarations. `wrapTools` declares the tools it wraps as recorded, with the first recorded call. Give the full list of the tools your code hands the agent, and the ones you did not wrap are declared as not recorded:
94
+
95
+ ```ts
96
+ import { declareTools, wrapTools } from "signafaide";
97
+
98
+ const tools = wrapTools({ refund, lookupOrder }, { agent: "support-bot" });
99
+ declareTools(["refund", "lookupOrder", "web_search"], { agent: "support-bot" }); // web_search is not wrapped
100
+ ```
101
+
102
+ `signafaide verify` then has a coverage line for each agent: `all 3 declared tools are recorded`, or `1 declared tool is not recorded: web_search`, or that no tool list was declared. It is a note, never a failure. A declaration is one signed note for the list and one for each tool in the agent's chain, so n tools are n + 1 records, counted like every other record, each time a process starts or the list changes. `{ declareTools: false }` or `SIGNAFAIDE_DECLARE_TOOLS=0` turns off the one `wrapTools` writes.
103
+
104
+ What this shows, and what it does not. The gateway records every call that goes through it; a call the application makes around it is still not recorded, so the deployment has to make the gateway the only route to those tools. A declaration is the program's own signed statement of its tools: evidence of what it said it had, not proof that the list is complete. A tool list is as fresh as its last declaration: a source that stops (a gateway that is removed, a program that ends) leaves the list at its clean exit, or within 24 hours after a crash.
105
+
77
106
  ## Strict mode: no receipt, no action
78
107
 
79
108
  ```ts
@@ -181,6 +210,7 @@ The chain is verified first; then each recorded decision with a co-signed outcom
181
210
  | `signafaide compare --module file.mjs:fn [--agent X] [--name N] [--from T] [--to T] [--limit N] [--upload [--values]]` | re-runs recorded decisions with a candidate and compares; `--values` sends the values too (stored-content decisions only) |
182
211
  | `signafaide claim` | prints the claim link again |
183
212
  | `signafaide flush` | ships the outbox now (with no key it sends nothing and says so) |
213
+ | `signafaide gateway [--agent X] [--name-prefix P] [--strict \| --strict-tools a,b] [--store-content] -- <command> [args...]` | runs an MCP server behind a recording proxy: every tool call that passes through is recorded, and one that cannot be recorded is not forwarded (see "Recording everything an agent can call") |
184
214
  | `signafaide agent setup [--agent <name>]... [--url <witness>] [--dry-run] [--dir <project>]` | installs the MCP server and the skill in your coding agents; `--url` sets `SIGNAFAIDE_URL` in each MCP entry |
185
215
  | `signafaide mcp` | runs the MCP server over stdio, when the separate `signafaide-mcp` package is installed (`agent setup` configures `npx -y signafaide-mcp@<version>` instead) |
186
216
 
@@ -208,7 +238,7 @@ The MCP tools: `signafaide_setup` (a preview that changes, creates and sends not
208
238
  Every request carries one header:
209
239
 
210
240
  ```
211
- X-Signafaide-Client: signafaide-js/0.2.1 (node 22.22.0; adapter=vercel-ai; agent=cursor; via=sdk)
241
+ X-Signafaide-Client: signafaide-js/0.3.0 (node 22.22.0; adapter=vercel-ai; agent=cursor; via=sdk)
212
242
  ```
213
243
 
214
244
  It holds the SDK and its version, the JavaScript runtime and its version, and the framework adapter in use (`langchain`, `openai-agents`, `vercel-ai` or `none`). It also holds the coding agent that set the project up (`claude-code`, `codex`, `cursor`, `gemini-cli`, `copilot`, `windsurf` or `unknown`) and how the call was made (`sdk`, `cli` or `mcp`). The agent is found from that tool's environment variables when the project is set up (`signafaide init`, `signafaide agent setup`) and kept in `.signafaide/config.json`; it is never guessed per process, so a program started from a terminal inside a coding agent is not labelled as that agent. `signafaide verify` and `signafaide export` also send `POST /v1/events {"name": "verify"}` (or `"export"`) when there is an API key. If that fails, the command is not affected. With no API key nothing at all is sent.
@@ -228,6 +258,7 @@ The header and the events hold no content, file paths, hostnames or user names.
228
258
  | `SIGNAFAIDE_EXIT_FLUSH_MS` | `3000`: the longest a finished program is kept open to send; `0` never |
229
259
  | `SIGNAFAIDE_TEST` | test mode is on by itself under Vitest, Jest and `node --test` (records in a temporary folder, nothing sent); `1` forces it, `0` turns it off |
230
260
  | `SIGNAFAIDE_DISABLED` | `1`: wrapped functions run and nothing is recorded |
261
+ | `SIGNAFAIDE_DECLARE_TOOLS` | `0`: `wrapTools` does not declare the tools it wraps (`declareTools()` still does) |
231
262
  | `SIGNAFAIDE_SEGMENT_BYTES` | `67108864`: a shipped chain file larger than this rolls into a sealed segment |
232
263
  | `SIGNAFAIDE_TELEMETRY` | `0` turns off the adapter, agent and strict fields and the events |
233
264
 
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-S2RSID5F.js";
3
+ } from "./chunk-DRRRZNIH.js";
4
4
 
5
5
  // src/jcs.ts
6
6
  var LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
@@ -546,6 +546,72 @@ function manifestErrors(doc) {
546
546
  return [];
547
547
  }
548
548
 
549
+ // src/plural.ts
550
+ var plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
551
+
552
+ // src/coverage.ts
553
+ var TOOLS = "signafaide.tools";
554
+ var TOOL_PREFIX = "signafaide.tool/";
555
+ var TOOL_UNRECORDED_PREFIX = "signafaide.tool_unrecorded/";
556
+ var RESERVED_PREFIX = "signafaide.";
557
+ var MAX_DECLARED_NAME = 101;
558
+ var KEPT = MAX_DECLARED_NAME - 9;
559
+ function declaredName(tool) {
560
+ const chars = [...tool];
561
+ if (chars.length <= MAX_DECLARED_NAME) return tool;
562
+ return `${chars.slice(0, KEPT).join("")}~${sha256Hex(new TextEncoder().encode(tool)).slice(0, 8)}`;
563
+ }
564
+ function toolNoteName(tool, recorded) {
565
+ return (recorded ? TOOL_PREFIX : TOOL_UNRECORDED_PREFIX) + declaredName(tool);
566
+ }
567
+ function toolNote(kind, name) {
568
+ if (kind !== "note" || typeof name !== "string") return null;
569
+ if (name === TOOLS) return "header";
570
+ const recorded = name.startsWith(TOOL_PREFIX);
571
+ if (!recorded && !name.startsWith(TOOL_UNRECORDED_PREFIX)) return null;
572
+ const rest = name.slice((recorded ? TOOL_PREFIX : TOOL_UNRECORDED_PREFIX).length);
573
+ return rest ? { name: rest, recorded } : null;
574
+ }
575
+ function byCodePoint(a, b) {
576
+ const [x, y] = [[...a], [...b]];
577
+ for (let i = 0; i < x.length && i < y.length; i++) {
578
+ const d = x[i].codePointAt(0) - y[i].codePointAt(0);
579
+ if (d) return d;
580
+ }
581
+ return x.length - y.length;
582
+ }
583
+ function coverage(records) {
584
+ let at = null;
585
+ let tools = /* @__PURE__ */ new Map();
586
+ const called = /* @__PURE__ */ new Set();
587
+ for (const { seq, kind, name } of records) {
588
+ const t = toolNote(kind, name);
589
+ if (t === "header") {
590
+ at = seq;
591
+ tools = /* @__PURE__ */ new Map();
592
+ } else if (t) {
593
+ if (at !== null) tools.set(t.name, t.recorded);
594
+ } else if (kind === "decision" && typeof name === "string" && !name.startsWith(RESERVED_PREFIX)) called.add(declaredName(name));
595
+ }
596
+ if (at === null) return { declared: [], undeclared_recorded: [], declared_at_seq: null };
597
+ return {
598
+ declared: [...tools.keys()].sort(byCodePoint).map((name) => ({ name, recorded: tools.get(name) })),
599
+ undeclared_recorded: [...called].filter((n) => !tools.has(n)).sort(byCodePoint),
600
+ declared_at_seq: at
601
+ };
602
+ }
603
+ var countIs = (n, one) => `${plural(n, one)} ${n === 1 ? "is" : "are"}`;
604
+ function coverageLines(agent, c) {
605
+ 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`];
606
+ const unrecorded = c.declared.filter((t) => !t.recorded).map((t) => t.name);
607
+ const out = [];
608
+ if (!c.declared.length) out.push(`${agent}: the declared tool list is empty`);
609
+ else if (!unrecorded.length) out.push(`${agent}: ${c.declared.length === 1 ? "the" : "all"} ${countIs(c.declared.length, "declared tool")} recorded`);
610
+ else out.push(`${agent}: ${countIs(unrecorded.length, "declared tool")} not recorded: ${unrecorded.join(", ")}`);
611
+ 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(", ")}`);
612
+ return out;
613
+ }
614
+
549
615
  // src/ots.ts
550
616
  import { ripemd160, sha1 } from "@noble/hashes/legacy";
551
617
  import { sha256 as sha2562 } from "@noble/hashes/sha2";
@@ -768,11 +834,6 @@ function rootsFromVector(m) {
768
834
 
769
835
  // src/verifier.ts
770
836
  import { hexToBytes as hexToBytes3 } from "@noble/hashes/utils";
771
-
772
- // src/plural.ts
773
- var plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
774
-
775
- // src/verifier.ts
776
837
  var CHECKS = [
777
838
  ["schema", "Envelope schema"],
778
839
  ["record_hash", "Record hashes"],
@@ -901,6 +962,9 @@ var Report = class {
901
962
  notSent = 0;
902
963
  /** The project's name from the signed export statement, when the pack has one. */
903
964
  projectName = null;
965
+ /** Tool coverage per agent (§7; per chain for local state): the agent's current tool
966
+ * declaration and the recorded tools it does not list. A note, never a failure. */
967
+ coverage = {};
904
968
  /** e.g. `PASS: export of every agent, all records, generated <t>`, or
905
969
  * `PASS (agent keys taken from the pack, not independent): export of ...` (§7). */
906
970
  get resultLine() {
@@ -924,6 +988,11 @@ var Report = class {
924
988
  firstFailed() {
925
989
  return this.checks.find((c) => c.status === "fail")?.id ?? null;
926
990
  }
991
+ /** Tool coverage in words (§7), agent by agent: what each agent declared, then the recorded
992
+ * tools its declaration does not list. */
993
+ coverageLines() {
994
+ return Object.entries(this.coverage).flatMap(([agent, c]) => coverageLines(agent, c));
995
+ }
927
996
  lines() {
928
997
  const out = [...this.notes];
929
998
  if (this.project && this.env) {
@@ -939,6 +1008,7 @@ var Report = class {
939
1008
  if (c.failures.length > 20) out.push(` ... and ${c.failures.length - 20} more`);
940
1009
  }
941
1010
  if (this.checks.some((c) => c.status === "pending")) out.push(PENDING_MEANS);
1011
+ out.push(...this.coverageLines().map((l) => `coverage: ${l}`));
942
1012
  out.push(...this.after);
943
1013
  out.push(...this.blocked.map((b) => `BLOCKED ${b}`));
944
1014
  out.push(this.resultLine);
@@ -961,6 +1031,7 @@ var Report = class {
961
1031
  independent: this.independent,
962
1032
  notes: this.notes,
963
1033
  checks: this.checks,
1034
+ coverage: this.coverage,
964
1035
  ...this.after.length ? { not_yet: this.after } : {},
965
1036
  ...this.blocked.length ? { blocked: this.blocked } : {}
966
1037
  };
@@ -1089,6 +1160,7 @@ function verifyPack(pack, serviceKeys, opts = {}) {
1089
1160
  byService
1090
1161
  });
1091
1162
  report.projectName = typeof pack.project_name === "string" ? pack.project_name : null;
1163
+ report.coverage = v.coverage;
1092
1164
  return report;
1093
1165
  }
1094
1166
  function earliest(a, b) {
@@ -1102,6 +1174,7 @@ function verifyLocalState(local, serviceKeys, drand = null, serviceKeysPlainHttp
1102
1174
  const checks = v.run(local.records, [], local.chainIds);
1103
1175
  const report = new Report(checks, null, null, local.records.length, [], [], { service_keys: !serviceKeysPlainHttp, agent_keys: true });
1104
1176
  report.nothingCosigned = v.nothingCosigned;
1177
+ report.coverage = v.coverage;
1105
1178
  return report;
1106
1179
  }
1107
1180
  var isU64 = (x) => typeof x === "number" && Number.isInteger(x) && x >= 0;
@@ -1232,6 +1305,8 @@ var Verifier = class {
1232
1305
  firstKey = /* @__PURE__ */ new Map();
1233
1306
  drafts = 0;
1234
1307
  nothingCosigned = false;
1308
+ /** Tool coverage per chain (§7), filled by `run`. */
1309
+ coverage = {};
1235
1310
  /** Local state: chain id -> the witness's signed head a chain continues after (§8). */
1236
1311
  localBases = {};
1237
1312
  /** A chain may start after seq 1 only when the export's from is after the agent's first key. */
@@ -1418,6 +1493,11 @@ var Verifier = class {
1418
1493
  if (starts.length) acc.notes.push(`${INSTANCE_NOTE}: ${starts.join(", ")}`);
1419
1494
  if (clean.length) acc.notes.push(`${CLEAN_RESTART_NOTE}: ${clean.join(", ")}`);
1420
1495
  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)`);
1496
+ for (const agent of [...chains.keys()].sort()) {
1497
+ const chain = chains.get(agent);
1498
+ const seqs = [...chain.keys()].sort((x, y) => x - y);
1499
+ this.coverage[agent] = coverage(seqs.map((seq) => ({ seq, kind: chain.get(seq).r.envelope.kind, name: chain.get(seq).r.envelope.name })));
1500
+ }
1421
1501
  return chains;
1422
1502
  }
1423
1503
  // 5
@@ -1882,6 +1962,16 @@ export {
1882
1962
  registration,
1883
1963
  manifestEntry,
1884
1964
  manifestErrors,
1965
+ TOOLS,
1966
+ TOOL_PREFIX,
1967
+ TOOL_UNRECORDED_PREFIX,
1968
+ MAX_DECLARED_NAME,
1969
+ declaredName,
1970
+ toolNoteName,
1971
+ toolNote,
1972
+ byCodePoint,
1973
+ coverage,
1974
+ coverageLines,
1885
1975
  DEFAULT_TIMESTAMP_URL,
1886
1976
  target,
1887
1977
  checkHeader,
@@ -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";