@xultrax-web/agent-memory-mcp 0.12.0 → 0.13.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/dist/index.js +112 -18
- 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
|
-
|
|
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(
|
|
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
|
-
*
|
|
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.
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
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.
|
|
2280
|
+
const server = new Server({ name: "agent-memory", version: "0.13.0" }, { 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.
|
|
3
|
+
"version": "0.13.0",
|
|
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",
|