@xultrax-web/agent-memory-mcp 0.11.1 → 0.11.2

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.
Files changed (3) hide show
  1. package/README.md +28 -4
  2. package/dist/index.js +150 -1
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -43,11 +43,35 @@ Companion file targets (v0.11.1):
43
43
 
44
44
  Set `AGENT_MEMORY_AUTO_EMIT_DIR=/path/to/project` to auto-regenerate all companions on every rule save.
45
45
 
46
- Roadmap for the v0.11.x series:
46
+ ### Compliance Receipts (v0.11.2 · primitive · tool wiring in v0.11.3)
47
+
48
+ Receipts are short-lived, HMAC-signed bearer tokens with caveats (Macaroon pattern · [Birgisson et al., NDSS 2014](https://research.google/pubs/pub41892/)). The novel protocol primitive in agent-memory-mcp: server-issued tokens that bind to action + session + rules-version-hash + expiry. Tampering breaks the HMAC. Rule changes invalidate stale receipts (because `rules_version` is part of the signed payload).
49
+
50
+ ```typescript
51
+ import { issueReceipt, validateReceipt } from "@xultrax-web/agent-memory-mcp";
52
+
53
+ // Server-internal: issue a receipt for a destructive action
54
+ const r = issueReceipt({
55
+ caveats: [
56
+ { type: "action", value: "delete_memory" },
57
+ { type: "session", value: "sess_abc123" },
58
+ ],
59
+ ttl_seconds: 60,
60
+ });
61
+
62
+ // Later: validate before executing the destructive op
63
+ const v = validateReceipt(r, {
64
+ required_caveats: [{ type: "action", value: "delete_memory" }],
65
+ });
66
+ if (!v.valid) throw new Error(v.reason);
67
+ ```
68
+
69
+ HMAC key lives at `<MEMORY_DIR>/.keyring/hmac-key` · 32 random bytes · mode `0600`. v0.11.3 wires receipts into `delete_memory` + other destructive tools and adds the `check_action` MCP tool.
70
+
71
+ ### Roadmap for the v0.11.x series:
47
72
 
48
- - Compliance Receipts (Macaroon-style HMAC tokens · protocol-level enforcement of our own destructive tools)
49
- - `check_action` tool (deterministic rule matching · optional Sampling enrichment where clients support it)
50
- - `audit` command (rule conflicts · staleness · receipt-denial log)
73
+ - `check_action` tool · deterministic rule matching · optional Sampling enrichment where clients support it · issues Compliance Receipts when proposed action passes
74
+ - `audit` command · rule conflicts · staleness · receipt-denial log
51
75
 
52
76
  ---
53
77
 
package/dist/index.js CHANGED
@@ -27,6 +27,7 @@ import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema
27
27
  import Fuse from "fuse.js";
28
28
  import matter from "gray-matter";
29
29
  import { spawnSync } from "node:child_process";
30
+ import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
30
31
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync, } from "node:fs";
31
32
  import { homedir } from "node:os";
32
33
  import { join, resolve } from "node:path";
@@ -1270,6 +1271,154 @@ function toolSaveRule(args) {
1270
1271
  });
1271
1272
  }
1272
1273
  // -------------------------------------------------------------
1274
+ // Compliance Receipts · v0.11.2 · the novel protocol primitive
1275
+ // -------------------------------------------------------------
1276
+ //
1277
+ // Receipts are short-lived, HMAC-signed bearer tokens with caveats
1278
+ // (attenuations). Macaroon-style. Issued by `check_action` (v0.11.3),
1279
+ // validated before our own destructive tools execute. Prior art:
1280
+ //
1281
+ // Birgisson et al · "Macaroons: Cookies with Contextual Caveats for
1282
+ // Decentralized Authorization in the Cloud" · Google Research,
1283
+ // NDSS 2014 · https://research.google/pubs/pub41892/
1284
+ //
1285
+ // Why receipts work where MCP Sampling doesn't:
1286
+ // - MCP Sampling is unsupported on Claude Code / Cursor / Cline /
1287
+ // Codex CLI (the primary coding clients) per the MCP client matrix.
1288
+ // - Receipts are server-issued protocol artifacts — they work on every
1289
+ // client because the server controls both ends (issue + validate).
1290
+ // - Receipts bind to: action + session + rules-version-hash + expiry.
1291
+ // Tampering breaks the HMAC. Rule changes invalidate stale receipts.
1292
+ //
1293
+ // Storage:
1294
+ // HMAC key lives at <MEMORY_DIR>/.keyring/hmac-key · 32 random bytes
1295
+ // created on first use with mode 0o600 (owner read/write only).
1296
+ // Caller-rotatable via `agent-memory rotate-key` (a v0.11.x follow-up).
1297
+ //
1298
+ // v0.11.2 ships the PRIMITIVE only — issuance + validation +
1299
+ // canonicalization. Tool wiring (delete_memory + check_action) lands
1300
+ // in v0.11.3.
1301
+ const KEYRING_DIR = join(MEMORY_DIR, ".keyring");
1302
+ const HMAC_KEY_FILE = join(KEYRING_DIR, "hmac-key");
1303
+ const RECEIPT_DEFAULT_TTL_SECONDS = 60;
1304
+ function loadOrCreateHmacKey() {
1305
+ if (existsSync(HMAC_KEY_FILE)) {
1306
+ return readFileSync(HMAC_KEY_FILE);
1307
+ }
1308
+ if (!existsSync(KEYRING_DIR)) {
1309
+ mkdirSync(KEYRING_DIR, { recursive: true });
1310
+ }
1311
+ const key = randomBytes(32); // 256 bits · plenty for HMAC-SHA256
1312
+ // mode 0o600 is owner-only on POSIX; Windows ignores mode but ACLs
1313
+ // default to the user, so practically equivalent for our threat model.
1314
+ writeFileSync(HMAC_KEY_FILE, key, { mode: 0o600 });
1315
+ return key;
1316
+ }
1317
+ /**
1318
+ * Compute the rules-version hash · first 16 hex chars of SHA-256 over
1319
+ * the concatenated bytes of every type=rule memory file, in sorted
1320
+ * filename order. Any rule add / edit / remove changes this hash,
1321
+ * which invalidates outstanding receipts (they were issued against a
1322
+ * different rule set).
1323
+ */
1324
+ function computeRulesVersion() {
1325
+ const rules = loadAllRules();
1326
+ const sortedPaths = rules.map((r) => r.filePath).sort();
1327
+ const hash = createHash("sha256");
1328
+ for (const fp of sortedPaths) {
1329
+ try {
1330
+ hash.update(readFileSync(fp));
1331
+ }
1332
+ catch {
1333
+ // File disappeared between listMemoryFiles + read; skip it.
1334
+ // Next computation will reflect the change.
1335
+ }
1336
+ }
1337
+ return hash.digest("hex").slice(0, 16);
1338
+ }
1339
+ /**
1340
+ * Deterministic canonical form for HMAC input · caveats sorted by
1341
+ * (type, value) so the order in which the caller listed them doesn't
1342
+ * change the signature. JSON with no whitespace (single line) so the
1343
+ * exact byte sequence is reproducible across platforms.
1344
+ */
1345
+ function canonicalizeReceipt(r) {
1346
+ const sortedCaveats = [...r.caveats].sort((a, b) => a.type === b.type ? a.value.localeCompare(b.value) : a.type.localeCompare(b.type));
1347
+ return JSON.stringify({
1348
+ id: r.id,
1349
+ issued_at: r.issued_at,
1350
+ expires_at: r.expires_at,
1351
+ rules_version: r.rules_version,
1352
+ caveats: sortedCaveats,
1353
+ });
1354
+ }
1355
+ function signReceipt(r) {
1356
+ const key = loadOrCreateHmacKey();
1357
+ return createHmac("sha256", key).update(canonicalizeReceipt(r)).digest("hex");
1358
+ }
1359
+ /**
1360
+ * Issue a fresh Compliance Receipt with the given caveats. The receipt
1361
+ * is bound to the current rule-store hash; any rule edit invalidates it.
1362
+ */
1363
+ export function issueReceipt(opts) {
1364
+ const now = Math.floor(Date.now() / 1000);
1365
+ const ttl = Math.max(1, opts.ttl_seconds ?? RECEIPT_DEFAULT_TTL_SECONDS);
1366
+ const base = {
1367
+ id: "rcpt_" + randomBytes(8).toString("hex"),
1368
+ issued_at: now,
1369
+ expires_at: now + ttl,
1370
+ rules_version: computeRulesVersion(),
1371
+ caveats: opts.caveats,
1372
+ };
1373
+ return { ...base, signature: signReceipt(base) };
1374
+ }
1375
+ /**
1376
+ * Validate a Compliance Receipt against the current rule store + caller's
1377
+ * required caveats. Returns {valid: true} on success, otherwise
1378
+ * {valid: false, reason: <human-readable>}.
1379
+ */
1380
+ export function validateReceipt(receipt, opts = {}) {
1381
+ // 1. HMAC verification · constant-time compare to avoid timing leaks.
1382
+ const expected = signReceipt({
1383
+ id: receipt.id,
1384
+ issued_at: receipt.issued_at,
1385
+ expires_at: receipt.expires_at,
1386
+ rules_version: receipt.rules_version,
1387
+ caveats: receipt.caveats,
1388
+ });
1389
+ const expectedBuf = Buffer.from(expected, "hex");
1390
+ const actualBuf = Buffer.from(receipt.signature, "hex");
1391
+ if (expectedBuf.length !== actualBuf.length || !timingSafeEqual(expectedBuf, actualBuf)) {
1392
+ return { valid: false, reason: "invalid signature" };
1393
+ }
1394
+ // 2. Expiry · receipts past their expires_at are dead.
1395
+ const now = Math.floor(Date.now() / 1000);
1396
+ if (now > receipt.expires_at) {
1397
+ return { valid: false, reason: "receipt expired" };
1398
+ }
1399
+ // 3. Rules-version binding · any rule edit since issuance invalidates.
1400
+ const currentRulesVersion = opts.current_rules_version ?? computeRulesVersion();
1401
+ if (receipt.rules_version !== currentRulesVersion) {
1402
+ return {
1403
+ valid: false,
1404
+ reason: `rules changed since receipt issued (was ${receipt.rules_version}, now ${currentRulesVersion})`,
1405
+ };
1406
+ }
1407
+ // 4. Required-caveat check · each required pair must appear on the receipt.
1408
+ if (opts.required_caveats) {
1409
+ for (const required of opts.required_caveats) {
1410
+ const found = receipt.caveats.find((c) => c.type === required.type && c.value === required.value);
1411
+ if (!found) {
1412
+ return {
1413
+ valid: false,
1414
+ reason: `missing required caveat: ${required.type}=${required.value}`,
1415
+ };
1416
+ }
1417
+ }
1418
+ }
1419
+ return { valid: true };
1420
+ }
1421
+ // -------------------------------------------------------------
1273
1422
  // Git sync · multi-machine memory via git remote
1274
1423
  // -------------------------------------------------------------
1275
1424
  //
@@ -1591,7 +1740,7 @@ function actionColor(action) {
1591
1740
  // -------------------------------------------------------------
1592
1741
  // Server wiring
1593
1742
  // -------------------------------------------------------------
1594
- const server = new Server({ name: "agent-memory", version: "0.11.1" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
1743
+ const server = new Server({ name: "agent-memory", version: "0.11.2" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
1595
1744
  // -------------------------------------------------------------
1596
1745
  // Resource URI scheme
1597
1746
  // -------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xultrax-web/agent-memory-mcp",
3
- "version": "0.11.1",
3
+ "version": "0.11.2",
4
4
  "mcpName": "io.github.xultrax-web/agent-memory-mcp",
5
5
  "description": "Markdown memory for AI agents. Plain files you can read, edit, grep, and commit. Operator-grade storage with atomic writes, file locking, tags, [[wiki-links]], find_related, git-backed multi-machine sync, and an Ink-based TUI.",
6
6
  "type": "module",