@xultrax-web/agent-memory-mcp 0.12.0 → 0.13.1

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 (2) hide show
  1. package/dist/index.js +112 -18
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ import { CallToolRequestSchema, CreateMessageResultSchema, GetPromptRequestSchem
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
+ import { createHash, createHmac, createPrivateKey, createPublicKey, generateKeyPairSync, randomBytes, sign as cryptoSign, timingSafeEqual, verify as cryptoVerify, } from "node:crypto";
31
31
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync, } from "node:fs";
32
32
  import { homedir } from "node:os";
33
33
  import { join, resolve } from "node:path";
@@ -1333,7 +1333,16 @@ function toolSaveRule(args) {
1333
1333
  // in v0.11.3.
1334
1334
  const KEYRING_DIR = join(MEMORY_DIR, ".keyring");
1335
1335
  const HMAC_KEY_FILE = join(KEYRING_DIR, "hmac-key");
1336
+ const ED25519_PRIV_FILE = join(KEYRING_DIR, "ed25519.priv");
1337
+ const ED25519_PUB_FILE = join(KEYRING_DIR, "ed25519.pub");
1336
1338
  const RECEIPT_DEFAULT_TTL_SECONDS = 60;
1339
+ /**
1340
+ * Signing-mode selector. CRP 1.0 = HMAC-SHA256 with shared symmetric secret.
1341
+ * CRP 1.1 = Ed25519 asymmetric · enables cross-server federation (validators
1342
+ * verify with the issuer's public key, no shared secret needed). Set via
1343
+ * the `CRP_SIGNING_MODE` env var. Default stays `hmac` for back-compat.
1344
+ */
1345
+ const CRP_SIGNING_MODE = process.env.CRP_SIGNING_MODE === "ed25519" ? "ed25519" : "hmac";
1337
1346
  function loadOrCreateHmacKey() {
1338
1347
  if (existsSync(HMAC_KEY_FILE)) {
1339
1348
  return readFileSync(HMAC_KEY_FILE);
@@ -1347,6 +1356,39 @@ function loadOrCreateHmacKey() {
1347
1356
  writeFileSync(HMAC_KEY_FILE, key, { mode: 0o600 });
1348
1357
  return key;
1349
1358
  }
1359
+ /**
1360
+ * Load or lazily create the Ed25519 keypair for CRP 1.1 receipts. Private
1361
+ * key (PEM, mode 0o600) signs · public key (PEM, mode 0o644) verifies and
1362
+ * can be shared with other MCP servers for federation. The public key
1363
+ * is exportable via the `agent-memory export-pubkey` CLI command.
1364
+ */
1365
+ function loadOrCreateEd25519Keys() {
1366
+ if (!existsSync(KEYRING_DIR)) {
1367
+ mkdirSync(KEYRING_DIR, { recursive: true });
1368
+ }
1369
+ if (existsSync(ED25519_PRIV_FILE) && existsSync(ED25519_PUB_FILE)) {
1370
+ return {
1371
+ privateKeyPem: readFileSync(ED25519_PRIV_FILE, "utf8"),
1372
+ publicKeyPem: readFileSync(ED25519_PUB_FILE, "utf8"),
1373
+ };
1374
+ }
1375
+ const { privateKey, publicKey } = generateKeyPairSync("ed25519");
1376
+ const privPem = privateKey.export({ format: "pem", type: "pkcs8" });
1377
+ const pubPem = publicKey.export({ format: "pem", type: "spki" });
1378
+ writeFileSync(ED25519_PRIV_FILE, privPem, { mode: 0o600 });
1379
+ writeFileSync(ED25519_PUB_FILE, pubPem, { mode: 0o644 });
1380
+ return { privateKeyPem: privPem, publicKeyPem: pubPem };
1381
+ }
1382
+ /**
1383
+ * Export the Ed25519 public key (PEM) for sharing with other servers.
1384
+ * Returns null when CRP 1.1 hasn't been initialized yet · the keypair is
1385
+ * created lazily on first Ed25519 receipt issuance.
1386
+ */
1387
+ export function exportEd25519PublicKey() {
1388
+ if (!existsSync(ED25519_PUB_FILE))
1389
+ return null;
1390
+ return readFileSync(ED25519_PUB_FILE, "utf8");
1391
+ }
1350
1392
  /**
1351
1393
  * Compute the rules-version hash · first 16 hex chars of SHA-256 over
1352
1394
  * the concatenated bytes of every type=rule memory file, in sorted
@@ -1377,21 +1419,70 @@ function computeRulesVersion() {
1377
1419
  */
1378
1420
  function canonicalizeReceipt(r) {
1379
1421
  const sortedCaveats = [...r.caveats].sort((a, b) => a.type === b.type ? a.value.localeCompare(b.value) : a.type.localeCompare(b.type));
1380
- return JSON.stringify({
1422
+ // Field ordering is significant for the signed canonical form. The
1423
+ // version field is included only when present so v1.0 and v1.1 receipts
1424
+ // produce distinct signatures (preventing version-downgrade attacks).
1425
+ const base = {
1381
1426
  id: r.id,
1382
1427
  issued_at: r.issued_at,
1383
1428
  expires_at: r.expires_at,
1384
1429
  rules_version: r.rules_version,
1385
1430
  caveats: sortedCaveats,
1386
- });
1431
+ };
1432
+ if (r.version)
1433
+ base.version = r.version;
1434
+ return JSON.stringify(base);
1387
1435
  }
1388
1436
  function signReceipt(r) {
1437
+ const canonical = canonicalizeReceipt(r);
1438
+ if (r.version === "1.1") {
1439
+ const { privateKeyPem } = loadOrCreateEd25519Keys();
1440
+ const privKey = createPrivateKey(privateKeyPem);
1441
+ // Ed25519 signs the raw bytes directly (no separate digest); pass null
1442
+ // as the algorithm per Node's API contract.
1443
+ return cryptoSign(null, Buffer.from(canonical, "utf8"), privKey).toString("hex");
1444
+ }
1445
+ // Default CRP 1.0 · HMAC-SHA256
1389
1446
  const key = loadOrCreateHmacKey();
1390
- return createHmac("sha256", key).update(canonicalizeReceipt(r)).digest("hex");
1447
+ return createHmac("sha256", key).update(canonical).digest("hex");
1448
+ }
1449
+ function verifySignature(receipt) {
1450
+ const canonical = canonicalizeReceipt({
1451
+ id: receipt.id,
1452
+ issued_at: receipt.issued_at,
1453
+ expires_at: receipt.expires_at,
1454
+ rules_version: receipt.rules_version,
1455
+ caveats: receipt.caveats,
1456
+ version: receipt.version,
1457
+ });
1458
+ if (receipt.version === "1.1") {
1459
+ try {
1460
+ const { publicKeyPem } = loadOrCreateEd25519Keys();
1461
+ const pubKey = createPublicKey(publicKeyPem);
1462
+ return cryptoVerify(null, Buffer.from(canonical, "utf8"), pubKey, Buffer.from(receipt.signature, "hex"));
1463
+ }
1464
+ catch {
1465
+ return false;
1466
+ }
1467
+ }
1468
+ // CRP 1.0 · HMAC-SHA256 with constant-time compare
1469
+ const key = loadOrCreateHmacKey();
1470
+ const expected = createHmac("sha256", key).update(canonical).digest();
1471
+ const actual = Buffer.from(receipt.signature, "hex");
1472
+ if (expected.length !== actual.length)
1473
+ return false;
1474
+ return timingSafeEqual(expected, actual);
1391
1475
  }
1392
1476
  /**
1393
- * Issue a fresh Compliance Receipt with the given caveats. The receipt
1394
- * is bound to the current rule-store hash; any rule edit invalidates it.
1477
+ * Issue a fresh Compliance Receipt with the given caveats. The receipt is
1478
+ * bound to the current rule-store hash; any rule edit invalidates it.
1479
+ *
1480
+ * Signing mode selected by the `CRP_SIGNING_MODE` env var:
1481
+ * - `hmac` (default) · CRP 1.0 · HMAC-SHA256 with a 256-bit shared secret
1482
+ * - `ed25519` · CRP 1.1 · Ed25519 asymmetric · enables cross-server
1483
+ * federation since validators verify with the issuer's public key
1484
+ * without sharing any secret. Public key exportable via the
1485
+ * `agent-memory export-pubkey` CLI command.
1395
1486
  */
1396
1487
  export function issueReceipt(opts) {
1397
1488
  const now = Math.floor(Date.now() / 1000);
@@ -1402,6 +1493,7 @@ export function issueReceipt(opts) {
1402
1493
  expires_at: now + ttl,
1403
1494
  rules_version: computeRulesVersion(),
1404
1495
  caveats: opts.caveats,
1496
+ ...(CRP_SIGNING_MODE === "ed25519" ? { version: "1.1" } : {}),
1405
1497
  };
1406
1498
  return { ...base, signature: signReceipt(base) };
1407
1499
  }
@@ -1411,17 +1503,10 @@ export function issueReceipt(opts) {
1411
1503
  * {valid: false, reason: <human-readable>}.
1412
1504
  */
1413
1505
  export function validateReceipt(receipt, opts = {}) {
1414
- // 1. HMAC verification · constant-time compare to avoid timing leaks.
1415
- const expected = signReceipt({
1416
- id: receipt.id,
1417
- issued_at: receipt.issued_at,
1418
- expires_at: receipt.expires_at,
1419
- rules_version: receipt.rules_version,
1420
- caveats: receipt.caveats,
1421
- });
1422
- const expectedBuf = Buffer.from(expected, "hex");
1423
- const actualBuf = Buffer.from(receipt.signature, "hex");
1424
- if (expectedBuf.length !== actualBuf.length || !timingSafeEqual(expectedBuf, actualBuf)) {
1506
+ // 1. Signature verification · routes to HMAC (CRP 1.0) or Ed25519 (CRP 1.1)
1507
+ // based on the receipt's version field. Both paths use constant-time
1508
+ // comparisons internally to avoid timing leaks.
1509
+ if (!verifySignature(receipt)) {
1425
1510
  return { valid: false, reason: "invalid signature" };
1426
1511
  }
1427
1512
  // 2. Expiry · receipts past their expires_at are dead.
@@ -2192,7 +2277,7 @@ function actionColor(action) {
2192
2277
  // -------------------------------------------------------------
2193
2278
  // Server wiring
2194
2279
  // -------------------------------------------------------------
2195
- const server = new Server({ name: "agent-memory", version: "0.12.0" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
2280
+ const server = new Server({ name: "agent-memory", version: "0.13.1" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
2196
2281
  // -------------------------------------------------------------
2197
2282
  // Resource URI scheme
2198
2283
  // -------------------------------------------------------------
@@ -2876,6 +2961,7 @@ const CLI_COMMANDS = new Set([
2876
2961
  "emit-companions",
2877
2962
  "check-action",
2878
2963
  "audit",
2964
+ "export-pubkey",
2879
2965
  "ui",
2880
2966
  "import-claude-code",
2881
2967
  "help",
@@ -3153,6 +3239,14 @@ async function cliMain(command, rest) {
3153
3239
  process.stdout.write(toolAudit({ format: flags.json ? "json" : "pretty" }) + "\n");
3154
3240
  return 0;
3155
3241
  }
3242
+ case "export-pubkey": {
3243
+ // CRP 1.1 · print the Ed25519 public key (PEM) so other MCP servers
3244
+ // can verify receipts issued by this server without sharing the
3245
+ // signing secret. Lazily initializes the keypair if missing.
3246
+ const { publicKeyPem } = loadOrCreateEd25519Keys();
3247
+ process.stdout.write(publicKeyPem);
3248
+ return 0;
3249
+ }
3156
3250
  case "ui": {
3157
3251
  // Dynamic import so Ink + React only load when the TUI runs,
3158
3252
  // keeping cold-start fast for MCP server + every other CLI command.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xultrax-web/agent-memory-mcp",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
4
4
  "mcpName": "io.github.xultrax-web/agent-memory-mcp",
5
5
  "description": "Codify how you work. Every AI tool obeys. Markdown rules + cross-tool companion files (AGENTS.md/CLAUDE.md/.cursor/rules/.gemini) + Compliance Receipts for protocol-level enforcement of destructive ops. Reference implementation of CRP 1.0. Works on every MCP client (no Sampling required).",
6
6
  "type": "module",