@xultrax-web/agent-memory-mcp 0.11.1 → 0.11.3
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 +57 -4
- package/dist/index.js +343 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,11 +43,64 @@ 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
|
-
|
|
46
|
+
### `check_action` · the protocol enforcement point (v0.11.3)
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
-
|
|
48
|
+
```bash
|
|
49
|
+
# Agent proposes an action · server matches against rule store
|
|
50
|
+
agent-memory check-action "delete the memory called old-project-notes" --type deletions
|
|
51
|
+
|
|
52
|
+
# → On approval: returns a Compliance Receipt the agent passes back to destructive tools
|
|
53
|
+
# → On deny: returns structured hard_violations + soft_warnings
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
MCP shape:
|
|
57
|
+
|
|
58
|
+
```jsonc
|
|
59
|
+
{
|
|
60
|
+
"name": "check_action",
|
|
61
|
+
"arguments": {
|
|
62
|
+
"action": "delete the memory called old-project-notes",
|
|
63
|
+
"action_type": "deletions",
|
|
64
|
+
"session_id": "sess_abc",
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Tier 1 (deterministic, every client): action is matched against `rule.matches` regexes, filtered by `rule.enforce_on` categories. Hard violations block; soft violations warn. Approved actions get a fresh receipt with 60s TTL.
|
|
70
|
+
|
|
71
|
+
Tier 2 (Sampling-enriched LLM judgment on `rule.applies_when`) lands in v0.11.3.x for clients that support Sampling.
|
|
72
|
+
|
|
73
|
+
**Receipt-gated delete_memory:** v0.11.3 accepts an optional `receipt` argument on `delete_memory`. Pass a receipt with `{type: 'action_type', value: 'deletions'}` and the delete validates against the rule store. v0.12 will make this required.
|
|
74
|
+
|
|
75
|
+
### Compliance Receipts (v0.11.2 · primitive · tool wiring in v0.11.3)
|
|
76
|
+
|
|
77
|
+
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).
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { issueReceipt, validateReceipt } from "@xultrax-web/agent-memory-mcp";
|
|
81
|
+
|
|
82
|
+
// Server-internal: issue a receipt for a destructive action
|
|
83
|
+
const r = issueReceipt({
|
|
84
|
+
caveats: [
|
|
85
|
+
{ type: "action", value: "delete_memory" },
|
|
86
|
+
{ type: "session", value: "sess_abc123" },
|
|
87
|
+
],
|
|
88
|
+
ttl_seconds: 60,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Later: validate before executing the destructive op
|
|
92
|
+
const v = validateReceipt(r, {
|
|
93
|
+
required_caveats: [{ type: "action", value: "delete_memory" }],
|
|
94
|
+
});
|
|
95
|
+
if (!v.valid) throw new Error(v.reason);
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
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.
|
|
99
|
+
|
|
100
|
+
### Roadmap for the v0.11.x series:
|
|
101
|
+
|
|
102
|
+
- `check_action` tool · deterministic rule matching · optional Sampling enrichment where clients support it · issues Compliance Receipts when proposed action passes
|
|
103
|
+
- `audit` command · rule conflicts · staleness · receipt-denial log
|
|
51
104
|
|
|
52
105
|
---
|
|
53
106
|
|
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";
|
|
@@ -602,6 +603,27 @@ export function toolDeleteMemory(args) {
|
|
|
602
603
|
const fp = memoryFilePath(name);
|
|
603
604
|
if (!existsSync(fp))
|
|
604
605
|
return `Memory "${name}" not found.`;
|
|
606
|
+
// v0.11.3 · receipt-gated path. If a receipt is supplied, validate it
|
|
607
|
+
// against the current rule set + required caveats. If validation fails,
|
|
608
|
+
// refuse the delete with a clear reason. If no receipt is supplied,
|
|
609
|
+
// proceed (back-compat) but log so audit can surface the gap. v0.12
|
|
610
|
+
// will require receipts unconditionally for destructive ops.
|
|
611
|
+
const receipt = parseReceiptArg(args.receipt);
|
|
612
|
+
if (receipt) {
|
|
613
|
+
const v = validateReceipt(receipt, {
|
|
614
|
+
required_caveats: [{ type: "action_type", value: "deletions" }],
|
|
615
|
+
});
|
|
616
|
+
if (!v.valid) {
|
|
617
|
+
logEvent("delete_denied", { name, reason: v.reason, receipt_id: receipt.id });
|
|
618
|
+
throw new Error(`delete_memory refused · receipt invalid (${v.reason}). ` +
|
|
619
|
+
`Call check_action({action: 'delete memory ${name}', action_type: 'deletions'}) ` +
|
|
620
|
+
`to get a fresh receipt.`);
|
|
621
|
+
}
|
|
622
|
+
logEvent("delete_approved_via_receipt", { name, receipt_id: receipt.id });
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
logEvent("delete_without_receipt", { name });
|
|
626
|
+
}
|
|
605
627
|
return withLock(() => {
|
|
606
628
|
ensureTrash();
|
|
607
629
|
// Trash filename: <unix-ms>-<name>.md so restore can pick the
|
|
@@ -610,9 +632,12 @@ export function toolDeleteMemory(args) {
|
|
|
610
632
|
const trashPath = join(TRASH_DIR, `${ts}-${name}.md`);
|
|
611
633
|
renameSync(fp, trashPath);
|
|
612
634
|
removeIndexEntryUnlocked(name);
|
|
613
|
-
logEvent("delete", { name, trash: `${ts}-${name}.md
|
|
635
|
+
logEvent("delete", { name, trash: `${ts}-${name}.md`, gated: !!receipt });
|
|
614
636
|
log("debug", "delete_memory", { name });
|
|
615
|
-
|
|
637
|
+
const gateMsg = receipt
|
|
638
|
+
? ` (gated by receipt ${receipt.id})`
|
|
639
|
+
: " (no receipt · v0.11.3 back-compat path)";
|
|
640
|
+
return `Moved "${name}" to trash${gateMsg}. Restore with: agent-memory restore ${name}`;
|
|
616
641
|
});
|
|
617
642
|
}
|
|
618
643
|
function toolRestoreMemory(args) {
|
|
@@ -1270,6 +1295,271 @@ function toolSaveRule(args) {
|
|
|
1270
1295
|
});
|
|
1271
1296
|
}
|
|
1272
1297
|
// -------------------------------------------------------------
|
|
1298
|
+
// Compliance Receipts · v0.11.2 · the novel protocol primitive
|
|
1299
|
+
// -------------------------------------------------------------
|
|
1300
|
+
//
|
|
1301
|
+
// Receipts are short-lived, HMAC-signed bearer tokens with caveats
|
|
1302
|
+
// (attenuations). Macaroon-style. Issued by `check_action` (v0.11.3),
|
|
1303
|
+
// validated before our own destructive tools execute. Prior art:
|
|
1304
|
+
//
|
|
1305
|
+
// Birgisson et al · "Macaroons: Cookies with Contextual Caveats for
|
|
1306
|
+
// Decentralized Authorization in the Cloud" · Google Research,
|
|
1307
|
+
// NDSS 2014 · https://research.google/pubs/pub41892/
|
|
1308
|
+
//
|
|
1309
|
+
// Why receipts work where MCP Sampling doesn't:
|
|
1310
|
+
// - MCP Sampling is unsupported on Claude Code / Cursor / Cline /
|
|
1311
|
+
// Codex CLI (the primary coding clients) per the MCP client matrix.
|
|
1312
|
+
// - Receipts are server-issued protocol artifacts — they work on every
|
|
1313
|
+
// client because the server controls both ends (issue + validate).
|
|
1314
|
+
// - Receipts bind to: action + session + rules-version-hash + expiry.
|
|
1315
|
+
// Tampering breaks the HMAC. Rule changes invalidate stale receipts.
|
|
1316
|
+
//
|
|
1317
|
+
// Storage:
|
|
1318
|
+
// HMAC key lives at <MEMORY_DIR>/.keyring/hmac-key · 32 random bytes
|
|
1319
|
+
// created on first use with mode 0o600 (owner read/write only).
|
|
1320
|
+
// Caller-rotatable via `agent-memory rotate-key` (a v0.11.x follow-up).
|
|
1321
|
+
//
|
|
1322
|
+
// v0.11.2 ships the PRIMITIVE only — issuance + validation +
|
|
1323
|
+
// canonicalization. Tool wiring (delete_memory + check_action) lands
|
|
1324
|
+
// in v0.11.3.
|
|
1325
|
+
const KEYRING_DIR = join(MEMORY_DIR, ".keyring");
|
|
1326
|
+
const HMAC_KEY_FILE = join(KEYRING_DIR, "hmac-key");
|
|
1327
|
+
const RECEIPT_DEFAULT_TTL_SECONDS = 60;
|
|
1328
|
+
function loadOrCreateHmacKey() {
|
|
1329
|
+
if (existsSync(HMAC_KEY_FILE)) {
|
|
1330
|
+
return readFileSync(HMAC_KEY_FILE);
|
|
1331
|
+
}
|
|
1332
|
+
if (!existsSync(KEYRING_DIR)) {
|
|
1333
|
+
mkdirSync(KEYRING_DIR, { recursive: true });
|
|
1334
|
+
}
|
|
1335
|
+
const key = randomBytes(32); // 256 bits · plenty for HMAC-SHA256
|
|
1336
|
+
// mode 0o600 is owner-only on POSIX; Windows ignores mode but ACLs
|
|
1337
|
+
// default to the user, so practically equivalent for our threat model.
|
|
1338
|
+
writeFileSync(HMAC_KEY_FILE, key, { mode: 0o600 });
|
|
1339
|
+
return key;
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* Compute the rules-version hash · first 16 hex chars of SHA-256 over
|
|
1343
|
+
* the concatenated bytes of every type=rule memory file, in sorted
|
|
1344
|
+
* filename order. Any rule add / edit / remove changes this hash,
|
|
1345
|
+
* which invalidates outstanding receipts (they were issued against a
|
|
1346
|
+
* different rule set).
|
|
1347
|
+
*/
|
|
1348
|
+
function computeRulesVersion() {
|
|
1349
|
+
const rules = loadAllRules();
|
|
1350
|
+
const sortedPaths = rules.map((r) => r.filePath).sort();
|
|
1351
|
+
const hash = createHash("sha256");
|
|
1352
|
+
for (const fp of sortedPaths) {
|
|
1353
|
+
try {
|
|
1354
|
+
hash.update(readFileSync(fp));
|
|
1355
|
+
}
|
|
1356
|
+
catch {
|
|
1357
|
+
// File disappeared between listMemoryFiles + read; skip it.
|
|
1358
|
+
// Next computation will reflect the change.
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
return hash.digest("hex").slice(0, 16);
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* Deterministic canonical form for HMAC input · caveats sorted by
|
|
1365
|
+
* (type, value) so the order in which the caller listed them doesn't
|
|
1366
|
+
* change the signature. JSON with no whitespace (single line) so the
|
|
1367
|
+
* exact byte sequence is reproducible across platforms.
|
|
1368
|
+
*/
|
|
1369
|
+
function canonicalizeReceipt(r) {
|
|
1370
|
+
const sortedCaveats = [...r.caveats].sort((a, b) => a.type === b.type ? a.value.localeCompare(b.value) : a.type.localeCompare(b.type));
|
|
1371
|
+
return JSON.stringify({
|
|
1372
|
+
id: r.id,
|
|
1373
|
+
issued_at: r.issued_at,
|
|
1374
|
+
expires_at: r.expires_at,
|
|
1375
|
+
rules_version: r.rules_version,
|
|
1376
|
+
caveats: sortedCaveats,
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
function signReceipt(r) {
|
|
1380
|
+
const key = loadOrCreateHmacKey();
|
|
1381
|
+
return createHmac("sha256", key).update(canonicalizeReceipt(r)).digest("hex");
|
|
1382
|
+
}
|
|
1383
|
+
/**
|
|
1384
|
+
* Issue a fresh Compliance Receipt with the given caveats. The receipt
|
|
1385
|
+
* is bound to the current rule-store hash; any rule edit invalidates it.
|
|
1386
|
+
*/
|
|
1387
|
+
export function issueReceipt(opts) {
|
|
1388
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1389
|
+
const ttl = Math.max(1, opts.ttl_seconds ?? RECEIPT_DEFAULT_TTL_SECONDS);
|
|
1390
|
+
const base = {
|
|
1391
|
+
id: "rcpt_" + randomBytes(8).toString("hex"),
|
|
1392
|
+
issued_at: now,
|
|
1393
|
+
expires_at: now + ttl,
|
|
1394
|
+
rules_version: computeRulesVersion(),
|
|
1395
|
+
caveats: opts.caveats,
|
|
1396
|
+
};
|
|
1397
|
+
return { ...base, signature: signReceipt(base) };
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* Validate a Compliance Receipt against the current rule store + caller's
|
|
1401
|
+
* required caveats. Returns {valid: true} on success, otherwise
|
|
1402
|
+
* {valid: false, reason: <human-readable>}.
|
|
1403
|
+
*/
|
|
1404
|
+
export function validateReceipt(receipt, opts = {}) {
|
|
1405
|
+
// 1. HMAC verification · constant-time compare to avoid timing leaks.
|
|
1406
|
+
const expected = signReceipt({
|
|
1407
|
+
id: receipt.id,
|
|
1408
|
+
issued_at: receipt.issued_at,
|
|
1409
|
+
expires_at: receipt.expires_at,
|
|
1410
|
+
rules_version: receipt.rules_version,
|
|
1411
|
+
caveats: receipt.caveats,
|
|
1412
|
+
});
|
|
1413
|
+
const expectedBuf = Buffer.from(expected, "hex");
|
|
1414
|
+
const actualBuf = Buffer.from(receipt.signature, "hex");
|
|
1415
|
+
if (expectedBuf.length !== actualBuf.length || !timingSafeEqual(expectedBuf, actualBuf)) {
|
|
1416
|
+
return { valid: false, reason: "invalid signature" };
|
|
1417
|
+
}
|
|
1418
|
+
// 2. Expiry · receipts past their expires_at are dead.
|
|
1419
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1420
|
+
if (now > receipt.expires_at) {
|
|
1421
|
+
return { valid: false, reason: "receipt expired" };
|
|
1422
|
+
}
|
|
1423
|
+
// 3. Rules-version binding · any rule edit since issuance invalidates.
|
|
1424
|
+
const currentRulesVersion = opts.current_rules_version ?? computeRulesVersion();
|
|
1425
|
+
if (receipt.rules_version !== currentRulesVersion) {
|
|
1426
|
+
return {
|
|
1427
|
+
valid: false,
|
|
1428
|
+
reason: `rules changed since receipt issued (was ${receipt.rules_version}, now ${currentRulesVersion})`,
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
// 4. Required-caveat check · each required pair must appear on the receipt.
|
|
1432
|
+
if (opts.required_caveats) {
|
|
1433
|
+
for (const required of opts.required_caveats) {
|
|
1434
|
+
const found = receipt.caveats.find((c) => c.type === required.type && c.value === required.value);
|
|
1435
|
+
if (!found) {
|
|
1436
|
+
return {
|
|
1437
|
+
valid: false,
|
|
1438
|
+
reason: `missing required caveat: ${required.type}=${required.value}`,
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
return { valid: true };
|
|
1444
|
+
}
|
|
1445
|
+
function safeRegex(pattern) {
|
|
1446
|
+
try {
|
|
1447
|
+
return new RegExp(pattern, "i");
|
|
1448
|
+
}
|
|
1449
|
+
catch {
|
|
1450
|
+
return null;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* Deterministic rule check. For each type=rule memory:
|
|
1455
|
+
* 1. Skip if rule.enforce_on is non-empty and doesn't include actionType.
|
|
1456
|
+
* 2. For each pattern in rule.matches, test against the proposed action.
|
|
1457
|
+
* 3. If any pattern matches → violation entry.
|
|
1458
|
+
* Hard violations block; soft violations warn.
|
|
1459
|
+
*/
|
|
1460
|
+
export function checkActionAgainstRules(action, actionType) {
|
|
1461
|
+
const rules = loadAllRules();
|
|
1462
|
+
const hard = [];
|
|
1463
|
+
const soft = [];
|
|
1464
|
+
for (const rule of rules) {
|
|
1465
|
+
// Scope filter · if enforce_on is specified, the action's type must be
|
|
1466
|
+
// listed. Empty enforce_on means "applies everywhere".
|
|
1467
|
+
if (rule.enforce_on && rule.enforce_on.length > 0) {
|
|
1468
|
+
if (!rule.enforce_on.includes(actionType))
|
|
1469
|
+
continue;
|
|
1470
|
+
}
|
|
1471
|
+
// No matches array → this rule has no deterministic pattern, only
|
|
1472
|
+
// applies_when (Sampling-only). Skip in Tier 1.
|
|
1473
|
+
if (!rule.matches || rule.matches.length === 0)
|
|
1474
|
+
continue;
|
|
1475
|
+
for (const pat of rule.matches) {
|
|
1476
|
+
const re = safeRegex(pat);
|
|
1477
|
+
if (!re)
|
|
1478
|
+
continue;
|
|
1479
|
+
if (re.test(action)) {
|
|
1480
|
+
const violation = {
|
|
1481
|
+
rule: rule.name,
|
|
1482
|
+
severity: rule.severity ?? "soft",
|
|
1483
|
+
reason: `matched pattern '${pat}' on action of type '${actionType}'`,
|
|
1484
|
+
};
|
|
1485
|
+
if (violation.severity === "hard")
|
|
1486
|
+
hard.push(violation);
|
|
1487
|
+
else
|
|
1488
|
+
soft.push(violation);
|
|
1489
|
+
break; // one match per rule is enough
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
return { hard, soft, rules_evaluated: rules.length };
|
|
1494
|
+
}
|
|
1495
|
+
function toolCheckAction(args) {
|
|
1496
|
+
const action = String(args.action ?? "").trim();
|
|
1497
|
+
const actionType = String(args.action_type ?? "").trim();
|
|
1498
|
+
const sessionId = typeof args.session_id === "string" ? args.session_id.trim() : "";
|
|
1499
|
+
if (!action)
|
|
1500
|
+
throw new Error("action is required (the proposed action description)");
|
|
1501
|
+
if (!actionType)
|
|
1502
|
+
throw new Error("action_type is required (e.g. 'deletions', 'commits', 'file_writes', 'chat_responses')");
|
|
1503
|
+
const { hard, soft, rules_evaluated } = checkActionAgainstRules(action, actionType);
|
|
1504
|
+
if (hard.length > 0) {
|
|
1505
|
+
const result = {
|
|
1506
|
+
approved: false,
|
|
1507
|
+
hard_violations: hard,
|
|
1508
|
+
soft_warnings: soft,
|
|
1509
|
+
rules_evaluated,
|
|
1510
|
+
};
|
|
1511
|
+
logEvent("check_action_denied", {
|
|
1512
|
+
action,
|
|
1513
|
+
action_type: actionType,
|
|
1514
|
+
hard_count: hard.length,
|
|
1515
|
+
soft_count: soft.length,
|
|
1516
|
+
});
|
|
1517
|
+
return JSON.stringify(result, null, 2);
|
|
1518
|
+
}
|
|
1519
|
+
// Approved · issue a receipt that downstream tools can require.
|
|
1520
|
+
const caveats = [
|
|
1521
|
+
{ type: "action_type", value: actionType },
|
|
1522
|
+
{ type: "action_hash", value: createHash("sha256").update(action).digest("hex").slice(0, 16) },
|
|
1523
|
+
];
|
|
1524
|
+
if (sessionId)
|
|
1525
|
+
caveats.push({ type: "session", value: sessionId });
|
|
1526
|
+
const receipt = issueReceipt({ caveats });
|
|
1527
|
+
const result = {
|
|
1528
|
+
approved: true,
|
|
1529
|
+
receipt,
|
|
1530
|
+
hard_violations: [],
|
|
1531
|
+
soft_warnings: soft,
|
|
1532
|
+
rules_evaluated,
|
|
1533
|
+
};
|
|
1534
|
+
logEvent("check_action_approved", {
|
|
1535
|
+
action,
|
|
1536
|
+
action_type: actionType,
|
|
1537
|
+
receipt_id: receipt.id,
|
|
1538
|
+
soft_count: soft.length,
|
|
1539
|
+
});
|
|
1540
|
+
return JSON.stringify(result, null, 2);
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Parse a receipt argument · accepts either an already-decoded object or
|
|
1544
|
+
* a JSON string (the form check_action returns when the agent calls it).
|
|
1545
|
+
* Returns null if missing/unparseable.
|
|
1546
|
+
*/
|
|
1547
|
+
function parseReceiptArg(input) {
|
|
1548
|
+
if (!input)
|
|
1549
|
+
return null;
|
|
1550
|
+
if (typeof input === "object")
|
|
1551
|
+
return input;
|
|
1552
|
+
if (typeof input === "string") {
|
|
1553
|
+
try {
|
|
1554
|
+
return JSON.parse(input);
|
|
1555
|
+
}
|
|
1556
|
+
catch {
|
|
1557
|
+
return null;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
return null;
|
|
1561
|
+
}
|
|
1562
|
+
// -------------------------------------------------------------
|
|
1273
1563
|
// Git sync · multi-machine memory via git remote
|
|
1274
1564
|
// -------------------------------------------------------------
|
|
1275
1565
|
//
|
|
@@ -1591,7 +1881,7 @@ function actionColor(action) {
|
|
|
1591
1881
|
// -------------------------------------------------------------
|
|
1592
1882
|
// Server wiring
|
|
1593
1883
|
// -------------------------------------------------------------
|
|
1594
|
-
const server = new Server({ name: "agent-memory", version: "0.11.
|
|
1884
|
+
const server = new Server({ name: "agent-memory", version: "0.11.3" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
|
|
1595
1885
|
// -------------------------------------------------------------
|
|
1596
1886
|
// Resource URI scheme
|
|
1597
1887
|
// -------------------------------------------------------------
|
|
@@ -1914,11 +2204,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1914
2204
|
},
|
|
1915
2205
|
{
|
|
1916
2206
|
name: "delete_memory",
|
|
1917
|
-
description: "Move a memory to .trash/ (soft delete). The file is removed from the index but recoverable via restore_memory until you manually empty .trash/."
|
|
2207
|
+
description: "Move a memory to .trash/ (soft delete). The file is removed from the index but recoverable via restore_memory until you manually empty .trash/. " +
|
|
2208
|
+
"v0.11.3+ accepts an optional `receipt` argument · pass a Compliance Receipt from check_action({action_type: 'deletions'}) to gate the delete against the rule store. " +
|
|
2209
|
+
"Receipts must carry the caveat {type: 'action_type', value: 'deletions'} or the delete refuses. " +
|
|
2210
|
+
"Receipts not supplied are accepted (back-compat) but logged · v0.12 will require them.",
|
|
1918
2211
|
inputSchema: {
|
|
1919
2212
|
type: "object",
|
|
1920
2213
|
properties: {
|
|
1921
2214
|
name: { type: "string", description: "The memory's name slug" },
|
|
2215
|
+
receipt: {
|
|
2216
|
+
description: "Optional Compliance Receipt (object or JSON string) from check_action. v0.11.3 logs unreceipted deletes but doesn't block them yet.",
|
|
2217
|
+
},
|
|
1922
2218
|
},
|
|
1923
2219
|
required: ["name"],
|
|
1924
2220
|
},
|
|
@@ -2087,6 +2383,32 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2087
2383
|
description: "List every active rule memory with severity, scope, and staleness markers (>90 days since last_verified). Use this to audit which rules are currently constraining the agent.",
|
|
2088
2384
|
inputSchema: { type: "object", properties: {} },
|
|
2089
2385
|
},
|
|
2386
|
+
{
|
|
2387
|
+
name: "check_action",
|
|
2388
|
+
description: "v0.11.3 · the protocol enforcement point. Pass a proposed action description + its category; the server matches against rule memories (type=rule) and either:\n" +
|
|
2389
|
+
" - APPROVES: returns a short-lived Compliance Receipt (HMAC-signed, 60s default) the agent can pass to destructive tools (e.g. delete_memory) as proof of compliance.\n" +
|
|
2390
|
+
" - DENIES: returns structured hard_violations (severity:hard rules that block) and/or soft_warnings (severity:soft rules that warn but allow).\n\n" +
|
|
2391
|
+
"Tier 1 (deterministic) matches the action against rule.matches regexes + rule.enforce_on category filter. Works on every MCP client.\n" +
|
|
2392
|
+
"Tier 2 (Sampling-enriched LLM judgment on rule.applies_when) ships in v0.11.3.x for clients that support Sampling (Claude Desktop, VS Code Copilot).",
|
|
2393
|
+
inputSchema: {
|
|
2394
|
+
type: "object",
|
|
2395
|
+
properties: {
|
|
2396
|
+
action: {
|
|
2397
|
+
type: "string",
|
|
2398
|
+
description: "Description of the proposed action. Plain prose · 'delete the memory called X', 'push to main branch', 'commit with message Y', etc.",
|
|
2399
|
+
},
|
|
2400
|
+
action_type: {
|
|
2401
|
+
type: "string",
|
|
2402
|
+
description: "Action category for rule.enforce_on matching. Examples: 'deletions', 'commits', 'pushes', 'file_writes', 'chat_responses', 'tool_calls'.",
|
|
2403
|
+
},
|
|
2404
|
+
session_id: {
|
|
2405
|
+
type: "string",
|
|
2406
|
+
description: "Optional session identifier · binds the issued receipt to this session via a caveat.",
|
|
2407
|
+
},
|
|
2408
|
+
},
|
|
2409
|
+
required: ["action", "action_type"],
|
|
2410
|
+
},
|
|
2411
|
+
},
|
|
2090
2412
|
{
|
|
2091
2413
|
name: "emit_companions",
|
|
2092
2414
|
description: "Regenerate companion rule files from the current rule memories. " +
|
|
@@ -2176,6 +2498,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2176
2498
|
case "emit_companions":
|
|
2177
2499
|
result = toolEmitCompanions(args);
|
|
2178
2500
|
break;
|
|
2501
|
+
case "check_action":
|
|
2502
|
+
result = toolCheckAction(args);
|
|
2503
|
+
break;
|
|
2179
2504
|
default:
|
|
2180
2505
|
throw new Error(`Unknown tool: ${name}`);
|
|
2181
2506
|
}
|
|
@@ -2215,6 +2540,7 @@ const CLI_COMMANDS = new Set([
|
|
|
2215
2540
|
"save-rule",
|
|
2216
2541
|
"list-rules",
|
|
2217
2542
|
"emit-companions",
|
|
2543
|
+
"check-action",
|
|
2218
2544
|
"ui",
|
|
2219
2545
|
"import-claude-code",
|
|
2220
2546
|
"help",
|
|
@@ -2460,6 +2786,19 @@ async function cliMain(command, rest) {
|
|
|
2460
2786
|
process.stdout.write(toolEmitCompanions({ out_dir: out, targets }) + "\n");
|
|
2461
2787
|
return 0;
|
|
2462
2788
|
}
|
|
2789
|
+
case "check-action": {
|
|
2790
|
+
const action = positional[0];
|
|
2791
|
+
const actionType = String(flags.type ?? flags["action-type"] ?? "");
|
|
2792
|
+
if (!action || !actionType) {
|
|
2793
|
+
throw new Error("Usage: agent-memory check-action '<action description>' --type <action_type> [--session <id>]");
|
|
2794
|
+
}
|
|
2795
|
+
process.stdout.write(toolCheckAction({
|
|
2796
|
+
action,
|
|
2797
|
+
action_type: actionType,
|
|
2798
|
+
session_id: flags.session ? String(flags.session) : undefined,
|
|
2799
|
+
}) + "\n");
|
|
2800
|
+
return 0;
|
|
2801
|
+
}
|
|
2463
2802
|
case "ui": {
|
|
2464
2803
|
// Dynamic import so Ink + React only load when the TUI runs,
|
|
2465
2804
|
// 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.11.
|
|
3
|
+
"version": "0.11.3",
|
|
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",
|