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.
|
|
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.
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
clientHeader,
|
|
8
8
|
detectAgent,
|
|
9
9
|
telemetryEnabled
|
|
10
|
-
} from "./chunk-
|
|
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
|
-
|
|
1248
|
-
const
|
|
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
|
-
|
|
1332
|
+
writeJson(file, { sources: live });
|
|
1251
1333
|
} catch (e) {
|
|
1252
1334
|
throw fsError(e, this.home);
|
|
1253
1335
|
}
|
|
1254
|
-
|
|
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
|
|
1818
|
-
while (!
|
|
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
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
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
|
|
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)
|
|
2279
|
-
|
|
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
|
-
}).
|
|
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-
|
|
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.
|
|
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.
|
|
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
package/dist/index.js
CHANGED
package/package.json
CHANGED