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 +32 -1
- package/dist/{chunk-MBXHRQ2J.js → chunk-6UDL5LZL.js} +96 -6
- package/dist/{chunk-S2RSID5F.js → chunk-DRRRZNIH.js} +1 -1
- package/dist/{chunk-4C2JEYAU.js → chunk-P3GJDXSQ.js} +291 -32
- package/dist/cli.js +403 -6
- package/dist/{core-BMfko_ex.d.ts → core-BTJGPYVW.d.cts} +64 -2
- package/dist/{core-BMfko_ex.d.cts → core-BTJGPYVW.d.ts} +64 -2
- package/dist/core.cjs +104 -6
- package/dist/core.d.cts +1 -1
- package/dist/core.d.ts +1 -1
- package/dist/core.js +20 -2
- package/dist/{http-YWG535HP.js → http-BZI6VZM3.js} +1 -1
- package/dist/index.cjs +391 -30
- package/dist/index.d.cts +98 -5
- package/dist/index.d.ts +98 -5
- package/dist/index.js +29 -3
- package/package.json +1 -1
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.
|
|
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-
|
|
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,
|