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

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 +50 -2
  2. package/dist/index.js +427 -7
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -43,6 +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
+ ### `check_action` · the protocol enforcement point (v0.11.3)
47
+
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
+
46
75
  ### Compliance Receipts (v0.11.2 · primitive · tool wiring in v0.11.3)
47
76
 
48
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).
@@ -68,10 +97,29 @@ if (!v.valid) throw new Error(v.reason);
68
97
 
69
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.
70
99
 
100
+ ### `audit` command (v0.11.4)
101
+
102
+ Daily operational health report for the rule store:
103
+
104
+ ```bash
105
+ agent-memory audit # pretty colored terminal output
106
+ agent-memory audit --json # structured JSON for tooling
107
+ ```
108
+
109
+ Surfaces:
110
+
111
+ - Rule count broken down by severity (hard / soft / unspecified)
112
+ - **Stale rules** · `last_verified` > 90 days ago, or never verified
113
+ - **Pattern conflicts** · two rules sharing an `enforce_on` category AND an identical regex in their `matches` arrays
114
+ - **Recent denials** · `check_action` calls that blocked an action (helps spot over-aggressive rules)
115
+ - **Unreceipted destructive ops** · `delete_memory` calls that bypassed the receipt path (back-compat in v0.11.x · v0.12 will remove the path)
116
+
117
+ The `healthy` flag is true iff no stale rules, no conflicts, no unreceipted ops in the recent log.
118
+
71
119
  ### Roadmap for the v0.11.x series:
72
120
 
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
121
+ - Compliance Receipt Protocol 1.0 spec doc on GitHub so other MCP servers can adopt the pattern
122
+ - Sampling-enriched Tier-2 `check_action` for clients that support it
75
123
 
76
124
  ---
77
125
 
package/dist/index.js CHANGED
@@ -190,6 +190,20 @@ function parseStringArray(input) {
190
190
  const out = input.filter((x) => typeof x === "string" && x.length > 0);
191
191
  return out.length > 0 ? out : undefined;
192
192
  }
193
+ /**
194
+ * js-yaml (gray-matter's parser) auto-coerces ISO-8601 date strings into
195
+ * JavaScript Date objects unless quoted. Normalize to YYYY-MM-DD string
196
+ * regardless of whether the YAML gave us a string or a Date.
197
+ */
198
+ function parseLastVerified(input) {
199
+ if (input instanceof Date) {
200
+ return input.toISOString().slice(0, 10);
201
+ }
202
+ if (typeof input === "string" && /^\d{4}-\d{2}-\d{2}$/.test(input)) {
203
+ return input;
204
+ }
205
+ return undefined;
206
+ }
193
207
  export function readMemory(name) {
194
208
  const fp = memoryFilePath(name);
195
209
  if (!existsSync(fp))
@@ -210,9 +224,7 @@ export function readMemory(name) {
210
224
  applies_when: parseStringArray(fm.applies_when),
211
225
  matches: parseStringArray(fm.matches),
212
226
  enforce_on: parseStringArray(fm.enforce_on),
213
- last_verified: typeof fm.last_verified === "string" && /^\d{4}-\d{2}-\d{2}$/.test(fm.last_verified)
214
- ? fm.last_verified
215
- : undefined,
227
+ last_verified: parseLastVerified(fm.last_verified),
216
228
  };
217
229
  }
218
230
  export function listMemoryFiles() {
@@ -603,6 +615,27 @@ export function toolDeleteMemory(args) {
603
615
  const fp = memoryFilePath(name);
604
616
  if (!existsSync(fp))
605
617
  return `Memory "${name}" not found.`;
618
+ // v0.11.3 · receipt-gated path. If a receipt is supplied, validate it
619
+ // against the current rule set + required caveats. If validation fails,
620
+ // refuse the delete with a clear reason. If no receipt is supplied,
621
+ // proceed (back-compat) but log so audit can surface the gap. v0.12
622
+ // will require receipts unconditionally for destructive ops.
623
+ const receipt = parseReceiptArg(args.receipt);
624
+ if (receipt) {
625
+ const v = validateReceipt(receipt, {
626
+ required_caveats: [{ type: "action_type", value: "deletions" }],
627
+ });
628
+ if (!v.valid) {
629
+ logEvent("delete_denied", { name, reason: v.reason, receipt_id: receipt.id });
630
+ throw new Error(`delete_memory refused · receipt invalid (${v.reason}). ` +
631
+ `Call check_action({action: 'delete memory ${name}', action_type: 'deletions'}) ` +
632
+ `to get a fresh receipt.`);
633
+ }
634
+ logEvent("delete_approved_via_receipt", { name, receipt_id: receipt.id });
635
+ }
636
+ else {
637
+ logEvent("delete_without_receipt", { name });
638
+ }
606
639
  return withLock(() => {
607
640
  ensureTrash();
608
641
  // Trash filename: <unix-ms>-<name>.md so restore can pick the
@@ -611,9 +644,12 @@ export function toolDeleteMemory(args) {
611
644
  const trashPath = join(TRASH_DIR, `${ts}-${name}.md`);
612
645
  renameSync(fp, trashPath);
613
646
  removeIndexEntryUnlocked(name);
614
- logEvent("delete", { name, trash: `${ts}-${name}.md` });
647
+ logEvent("delete", { name, trash: `${ts}-${name}.md`, gated: !!receipt });
615
648
  log("debug", "delete_memory", { name });
616
- return `Moved "${name}" to trash. Restore with: agent-memory restore ${name}`;
649
+ const gateMsg = receipt
650
+ ? ` (gated by receipt ${receipt.id})`
651
+ : " (no receipt · v0.11.3 back-compat path)";
652
+ return `Moved "${name}" to trash${gateMsg}. Restore with: agent-memory restore ${name}`;
617
653
  });
618
654
  }
619
655
  function toolRestoreMemory(args) {
@@ -1418,6 +1454,317 @@ export function validateReceipt(receipt, opts = {}) {
1418
1454
  }
1419
1455
  return { valid: true };
1420
1456
  }
1457
+ function safeRegex(pattern) {
1458
+ try {
1459
+ return new RegExp(pattern, "i");
1460
+ }
1461
+ catch {
1462
+ return null;
1463
+ }
1464
+ }
1465
+ /**
1466
+ * Deterministic rule check. For each type=rule memory:
1467
+ * 1. Skip if rule.enforce_on is non-empty and doesn't include actionType.
1468
+ * 2. For each pattern in rule.matches, test against the proposed action.
1469
+ * 3. If any pattern matches → violation entry.
1470
+ * Hard violations block; soft violations warn.
1471
+ */
1472
+ export function checkActionAgainstRules(action, actionType) {
1473
+ const rules = loadAllRules();
1474
+ const hard = [];
1475
+ const soft = [];
1476
+ for (const rule of rules) {
1477
+ // Scope filter · if enforce_on is specified, the action's type must be
1478
+ // listed. Empty enforce_on means "applies everywhere".
1479
+ if (rule.enforce_on && rule.enforce_on.length > 0) {
1480
+ if (!rule.enforce_on.includes(actionType))
1481
+ continue;
1482
+ }
1483
+ // No matches array → this rule has no deterministic pattern, only
1484
+ // applies_when (Sampling-only). Skip in Tier 1.
1485
+ if (!rule.matches || rule.matches.length === 0)
1486
+ continue;
1487
+ for (const pat of rule.matches) {
1488
+ const re = safeRegex(pat);
1489
+ if (!re)
1490
+ continue;
1491
+ if (re.test(action)) {
1492
+ const violation = {
1493
+ rule: rule.name,
1494
+ severity: rule.severity ?? "soft",
1495
+ reason: `matched pattern '${pat}' on action of type '${actionType}'`,
1496
+ };
1497
+ if (violation.severity === "hard")
1498
+ hard.push(violation);
1499
+ else
1500
+ soft.push(violation);
1501
+ break; // one match per rule is enough
1502
+ }
1503
+ }
1504
+ }
1505
+ return { hard, soft, rules_evaluated: rules.length };
1506
+ }
1507
+ function toolCheckAction(args) {
1508
+ const action = String(args.action ?? "").trim();
1509
+ const actionType = String(args.action_type ?? "").trim();
1510
+ const sessionId = typeof args.session_id === "string" ? args.session_id.trim() : "";
1511
+ if (!action)
1512
+ throw new Error("action is required (the proposed action description)");
1513
+ if (!actionType)
1514
+ throw new Error("action_type is required (e.g. 'deletions', 'commits', 'file_writes', 'chat_responses')");
1515
+ const { hard, soft, rules_evaluated } = checkActionAgainstRules(action, actionType);
1516
+ if (hard.length > 0) {
1517
+ const result = {
1518
+ approved: false,
1519
+ hard_violations: hard,
1520
+ soft_warnings: soft,
1521
+ rules_evaluated,
1522
+ };
1523
+ logEvent("check_action_denied", {
1524
+ proposed_action: action,
1525
+ action_type: actionType,
1526
+ hard_count: hard.length,
1527
+ soft_count: soft.length,
1528
+ });
1529
+ return JSON.stringify(result, null, 2);
1530
+ }
1531
+ // Approved · issue a receipt that downstream tools can require.
1532
+ const caveats = [
1533
+ { type: "action_type", value: actionType },
1534
+ { type: "action_hash", value: createHash("sha256").update(action).digest("hex").slice(0, 16) },
1535
+ ];
1536
+ if (sessionId)
1537
+ caveats.push({ type: "session", value: sessionId });
1538
+ const receipt = issueReceipt({ caveats });
1539
+ const result = {
1540
+ approved: true,
1541
+ receipt,
1542
+ hard_violations: [],
1543
+ soft_warnings: soft,
1544
+ rules_evaluated,
1545
+ };
1546
+ logEvent("check_action_approved", {
1547
+ proposed_action: action,
1548
+ action_type: actionType,
1549
+ receipt_id: receipt.id,
1550
+ soft_count: soft.length,
1551
+ });
1552
+ return JSON.stringify(result, null, 2);
1553
+ }
1554
+ /**
1555
+ * Parse a receipt argument · accepts either an already-decoded object or
1556
+ * a JSON string (the form check_action returns when the agent calls it).
1557
+ * Returns null if missing/unparseable.
1558
+ */
1559
+ function parseReceiptArg(input) {
1560
+ if (!input)
1561
+ return null;
1562
+ if (typeof input === "object")
1563
+ return input;
1564
+ if (typeof input === "string") {
1565
+ try {
1566
+ return JSON.parse(input);
1567
+ }
1568
+ catch {
1569
+ return null;
1570
+ }
1571
+ }
1572
+ return null;
1573
+ }
1574
+ // -------------------------------------------------------------
1575
+ // audit · v0.11.4 · daily-rhythm operational health command
1576
+ // -------------------------------------------------------------
1577
+ //
1578
+ // Surfaces what's working and what's drifting in the rule store:
1579
+ // - Rule count, broken down by severity
1580
+ // - Stale rules (last_verified > 90 days ago, or never verified)
1581
+ // - Pattern conflicts (two rules sharing an enforce_on category AND
1582
+ // an identical regex in their matches arrays · likely contradiction)
1583
+ // - Recent check_action denials (the system blocked something — was
1584
+ // that the right call? is a rule too aggressive?)
1585
+ // - Recent unreceipted destructive ops (back-compat path · v0.12 will
1586
+ // remove it but for v0.11.x audit just flags them)
1587
+ const STALE_THRESHOLD_DAYS = 90;
1588
+ const AUDIT_EVENT_TAIL = 50;
1589
+ function summarizeRules(rules) {
1590
+ let hard = 0;
1591
+ let soft = 0;
1592
+ let noSev = 0;
1593
+ for (const r of rules) {
1594
+ if (r.severity === "hard")
1595
+ hard++;
1596
+ else if (r.severity === "soft")
1597
+ soft++;
1598
+ else
1599
+ noSev++;
1600
+ }
1601
+ return { total: rules.length, hard, soft, no_severity: noSev };
1602
+ }
1603
+ function findStaleRules(rules, thresholdDays) {
1604
+ const now = Date.now();
1605
+ const stale = [];
1606
+ for (const r of rules) {
1607
+ if (!r.last_verified) {
1608
+ stale.push({
1609
+ name: r.name,
1610
+ last_verified: null,
1611
+ age_days: null,
1612
+ description: r.description,
1613
+ });
1614
+ continue;
1615
+ }
1616
+ const verifiedAt = new Date(r.last_verified).getTime();
1617
+ if (Number.isNaN(verifiedAt)) {
1618
+ stale.push({
1619
+ name: r.name,
1620
+ last_verified: r.last_verified,
1621
+ age_days: null,
1622
+ description: r.description,
1623
+ });
1624
+ continue;
1625
+ }
1626
+ const ageDays = Math.floor((now - verifiedAt) / 86400_000);
1627
+ if (ageDays > thresholdDays) {
1628
+ stale.push({
1629
+ name: r.name,
1630
+ last_verified: r.last_verified,
1631
+ age_days: ageDays,
1632
+ description: r.description,
1633
+ });
1634
+ }
1635
+ }
1636
+ return stale;
1637
+ }
1638
+ function findPatternConflicts(rules) {
1639
+ const conflicts = [];
1640
+ for (let i = 0; i < rules.length; i++) {
1641
+ const a = rules[i];
1642
+ if (!a.matches || a.matches.length === 0)
1643
+ continue;
1644
+ for (let j = i + 1; j < rules.length; j++) {
1645
+ const b = rules[j];
1646
+ if (!b.matches || b.matches.length === 0)
1647
+ continue;
1648
+ // Need overlap in enforce_on (or both empty → universal)
1649
+ const aCategories = a.enforce_on ?? [];
1650
+ const bCategories = b.enforce_on ?? [];
1651
+ const sharedCategories = aCategories.length === 0 || bCategories.length === 0
1652
+ ? ["(universal)"]
1653
+ : aCategories.filter((c) => bCategories.includes(c));
1654
+ if (sharedCategories.length === 0)
1655
+ continue;
1656
+ // Need at least one identical pattern · low-false-positive heuristic
1657
+ const sharedPatterns = a.matches.filter((p) => b.matches.includes(p));
1658
+ if (sharedPatterns.length === 0)
1659
+ continue;
1660
+ for (const cat of sharedCategories) {
1661
+ for (const pat of sharedPatterns) {
1662
+ conflicts.push({
1663
+ rule_a: a.name,
1664
+ rule_b: b.name,
1665
+ shared_pattern: pat,
1666
+ shared_enforce_on: cat,
1667
+ });
1668
+ }
1669
+ }
1670
+ }
1671
+ }
1672
+ return conflicts;
1673
+ }
1674
+ function recentDenials() {
1675
+ const records = readEventLog({ tail: AUDIT_EVENT_TAIL, action: "check_action_denied" });
1676
+ return records.map((r) => ({
1677
+ ts: String(r.ts),
1678
+ action: String(r.proposed_action ?? ""),
1679
+ action_type: String(r.action_type ?? ""),
1680
+ hard_count: typeof r.hard_count === "number" ? r.hard_count : 0,
1681
+ }));
1682
+ }
1683
+ function recentUnreceiptedDeletes() {
1684
+ const records = readEventLog({ tail: AUDIT_EVENT_TAIL, action: "delete_without_receipt" });
1685
+ return records.map((r) => ({
1686
+ ts: String(r.ts),
1687
+ name: String(r.name ?? ""),
1688
+ }));
1689
+ }
1690
+ export function runAudit() {
1691
+ const rules = loadAllRules();
1692
+ const stale = findStaleRules(rules, STALE_THRESHOLD_DAYS);
1693
+ const conflicts = findPatternConflicts(rules);
1694
+ const denials = recentDenials();
1695
+ const unreceipted = recentUnreceiptedDeletes();
1696
+ return {
1697
+ rules: summarizeRules(rules),
1698
+ stale_rules: stale,
1699
+ pattern_conflicts: conflicts,
1700
+ recent_denials: denials,
1701
+ recent_unreceipted_deletes: unreceipted,
1702
+ healthy: stale.length === 0 && conflicts.length === 0 && unreceipted.length === 0,
1703
+ };
1704
+ }
1705
+ function formatAuditPretty(r) {
1706
+ const lines = [];
1707
+ lines.push(c(ANSI.bold, "agent-memory audit"));
1708
+ lines.push("");
1709
+ lines.push(` Rules: ${c(ANSI.bold, String(r.rules.total))} ` +
1710
+ `(${c(ANSI.red, String(r.rules.hard) + " hard")} · ` +
1711
+ `${c(ANSI.yellow, String(r.rules.soft) + " soft")} · ` +
1712
+ `${r.rules.no_severity} unspecified)`);
1713
+ lines.push("");
1714
+ if (r.stale_rules.length > 0) {
1715
+ lines.push(c(ANSI.yellow, ` ${r.stale_rules.length} stale rule(s):`));
1716
+ for (const s of r.stale_rules) {
1717
+ const age = s.age_days === null
1718
+ ? c(ANSI.dim, "(never verified)")
1719
+ : c(ANSI.dim, `(${s.age_days}d since last_verified)`);
1720
+ lines.push(` ${s.name} ${age}`);
1721
+ lines.push(` ${c(ANSI.dim, s.description)}`);
1722
+ }
1723
+ lines.push("");
1724
+ }
1725
+ else {
1726
+ lines.push(c(ANSI.green, " All rules verified within 90 days."));
1727
+ lines.push("");
1728
+ }
1729
+ if (r.pattern_conflicts.length > 0) {
1730
+ lines.push(c(ANSI.red, ` ${r.pattern_conflicts.length} pattern conflict(s):`));
1731
+ for (const conf of r.pattern_conflicts) {
1732
+ lines.push(` ${conf.rule_a} <-> ${conf.rule_b} ` +
1733
+ c(ANSI.dim, `(share pattern '${conf.shared_pattern}' on '${conf.shared_enforce_on}')`));
1734
+ }
1735
+ lines.push("");
1736
+ }
1737
+ else {
1738
+ lines.push(c(ANSI.green, " No pattern conflicts detected."));
1739
+ lines.push("");
1740
+ }
1741
+ if (r.recent_denials.length > 0) {
1742
+ lines.push(` Recent denials (${r.recent_denials.length}):`);
1743
+ for (const d of r.recent_denials.slice(-5)) {
1744
+ lines.push(` ${c(ANSI.dim, d.ts.slice(0, 19))} ${d.action_type} ` +
1745
+ c(ANSI.red, `(${d.hard_count} hard violation${d.hard_count === 1 ? "" : "s"})`));
1746
+ lines.push(` ${c(ANSI.dim, d.action)}`);
1747
+ }
1748
+ lines.push("");
1749
+ }
1750
+ if (r.recent_unreceipted_deletes.length > 0) {
1751
+ lines.push(c(ANSI.yellow, ` ${r.recent_unreceipted_deletes.length} unreceipted delete(s):`));
1752
+ for (const u of r.recent_unreceipted_deletes.slice(-5)) {
1753
+ lines.push(` ${c(ANSI.dim, u.ts.slice(0, 19))} ${u.name}`);
1754
+ }
1755
+ lines.push(c(ANSI.dim, ` (v0.11.x back-compat path; v0.12 will require receipts for delete_memory)`));
1756
+ lines.push("");
1757
+ }
1758
+ lines.push(r.healthy
1759
+ ? c(ANSI.green, " HEALTHY · no action required")
1760
+ : c(ANSI.yellow, " ATTENTION · review the items above"));
1761
+ return lines.join("\n");
1762
+ }
1763
+ function toolAudit(args) {
1764
+ const json = args.json === true || args.format === "json";
1765
+ const report = runAudit();
1766
+ return json ? JSON.stringify(report, null, 2) : formatAuditPretty(report);
1767
+ }
1421
1768
  // -------------------------------------------------------------
1422
1769
  // Git sync · multi-machine memory via git remote
1423
1770
  // -------------------------------------------------------------
@@ -1740,7 +2087,7 @@ function actionColor(action) {
1740
2087
  // -------------------------------------------------------------
1741
2088
  // Server wiring
1742
2089
  // -------------------------------------------------------------
1743
- const server = new Server({ name: "agent-memory", version: "0.11.2" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
2090
+ const server = new Server({ name: "agent-memory", version: "0.11.4" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
1744
2091
  // -------------------------------------------------------------
1745
2092
  // Resource URI scheme
1746
2093
  // -------------------------------------------------------------
@@ -2063,11 +2410,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2063
2410
  },
2064
2411
  {
2065
2412
  name: "delete_memory",
2066
- 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/.",
2413
+ 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/. " +
2414
+ "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. " +
2415
+ "Receipts must carry the caveat {type: 'action_type', value: 'deletions'} or the delete refuses. " +
2416
+ "Receipts not supplied are accepted (back-compat) but logged · v0.12 will require them.",
2067
2417
  inputSchema: {
2068
2418
  type: "object",
2069
2419
  properties: {
2070
2420
  name: { type: "string", description: "The memory's name slug" },
2421
+ receipt: {
2422
+ description: "Optional Compliance Receipt (object or JSON string) from check_action. v0.11.3 logs unreceipted deletes but doesn't block them yet.",
2423
+ },
2071
2424
  },
2072
2425
  required: ["name"],
2073
2426
  },
@@ -2236,6 +2589,48 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2236
2589
  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.",
2237
2590
  inputSchema: { type: "object", properties: {} },
2238
2591
  },
2592
+ {
2593
+ name: "audit",
2594
+ description: "v0.11.4 · operational health report for the rule store. Surfaces: rule count by severity, stale rules (last_verified > 90 days or null), pattern conflicts (two rules sharing an enforce_on category AND an identical regex pattern), recent check_action denials, and recent unreceipted destructive ops. " +
2595
+ "Default returns pretty-printed text; pass {format: 'json'} for structured output. " +
2596
+ "Run daily-ish · the report is fast and gives the operator a single view of what's drifting.",
2597
+ inputSchema: {
2598
+ type: "object",
2599
+ properties: {
2600
+ format: {
2601
+ type: "string",
2602
+ enum: ["pretty", "json"],
2603
+ description: "Output format. Default 'pretty' (human-readable colored text).",
2604
+ },
2605
+ },
2606
+ },
2607
+ },
2608
+ {
2609
+ name: "check_action",
2610
+ 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" +
2611
+ " - 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" +
2612
+ " - DENIES: returns structured hard_violations (severity:hard rules that block) and/or soft_warnings (severity:soft rules that warn but allow).\n\n" +
2613
+ "Tier 1 (deterministic) matches the action against rule.matches regexes + rule.enforce_on category filter. Works on every MCP client.\n" +
2614
+ "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).",
2615
+ inputSchema: {
2616
+ type: "object",
2617
+ properties: {
2618
+ action: {
2619
+ type: "string",
2620
+ description: "Description of the proposed action. Plain prose · 'delete the memory called X', 'push to main branch', 'commit with message Y', etc.",
2621
+ },
2622
+ action_type: {
2623
+ type: "string",
2624
+ description: "Action category for rule.enforce_on matching. Examples: 'deletions', 'commits', 'pushes', 'file_writes', 'chat_responses', 'tool_calls'.",
2625
+ },
2626
+ session_id: {
2627
+ type: "string",
2628
+ description: "Optional session identifier · binds the issued receipt to this session via a caveat.",
2629
+ },
2630
+ },
2631
+ required: ["action", "action_type"],
2632
+ },
2633
+ },
2239
2634
  {
2240
2635
  name: "emit_companions",
2241
2636
  description: "Regenerate companion rule files from the current rule memories. " +
@@ -2325,6 +2720,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2325
2720
  case "emit_companions":
2326
2721
  result = toolEmitCompanions(args);
2327
2722
  break;
2723
+ case "check_action":
2724
+ result = toolCheckAction(args);
2725
+ break;
2726
+ case "audit":
2727
+ result = toolAudit(args);
2728
+ break;
2328
2729
  default:
2329
2730
  throw new Error(`Unknown tool: ${name}`);
2330
2731
  }
@@ -2364,6 +2765,8 @@ const CLI_COMMANDS = new Set([
2364
2765
  "save-rule",
2365
2766
  "list-rules",
2366
2767
  "emit-companions",
2768
+ "check-action",
2769
+ "audit",
2367
2770
  "ui",
2368
2771
  "import-claude-code",
2369
2772
  "help",
@@ -2609,6 +3012,23 @@ async function cliMain(command, rest) {
2609
3012
  process.stdout.write(toolEmitCompanions({ out_dir: out, targets }) + "\n");
2610
3013
  return 0;
2611
3014
  }
3015
+ case "check-action": {
3016
+ const action = positional[0];
3017
+ const actionType = String(flags.type ?? flags["action-type"] ?? "");
3018
+ if (!action || !actionType) {
3019
+ throw new Error("Usage: agent-memory check-action '<action description>' --type <action_type> [--session <id>]");
3020
+ }
3021
+ process.stdout.write(toolCheckAction({
3022
+ action,
3023
+ action_type: actionType,
3024
+ session_id: flags.session ? String(flags.session) : undefined,
3025
+ }) + "\n");
3026
+ return 0;
3027
+ }
3028
+ case "audit": {
3029
+ process.stdout.write(toolAudit({ format: flags.json ? "json" : "pretty" }) + "\n");
3030
+ return 0;
3031
+ }
2612
3032
  case "ui": {
2613
3033
  // Dynamic import so Ink + React only load when the TUI runs,
2614
3034
  // 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.2",
3
+ "version": "0.11.4",
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",