#!/usr/bin/env node /** * Guard 1: Memory Backend Guard * Prevents proliferation beyond the allowed memory backends. * * → Architecture Freeze v2 (PR-1): docs/architecture-freeze-v2.md * → Core: memory-core | Legacy: memory-wiki * * Allowed plugins (kind="memory" or has memory tools): * - memory-core (primary — Core §2) * - active-memory (recall injector — Core §2) * - memory-wiki (legacy, allowed for now — Legacy §4) * - Builtin/QMD (internal engine, not a plugin) * * FAIL: any new memory plugin appears * PASS: within allowed list */ import { readdirSync, readFileSync, existsSync } from "node:fs"; import { join } from "node:path"; import { homedir } from "node:os"; const OPENCLAW_HOME = process.env.OPENCLAW_HOME || "/opt/homebrew/lib/node_modules/openclaw"; const EXTENSIONS_DIR = join(OPENCLAW_HOME, "dist", "extensions"); // Allowed memory plugin IDs const ALLOWED_MEMORY_PLUGINS = new Set([ "memory-core", "active-memory", "memory-wiki", ]); // Plugin IDs that have memory contracts but are not "kind: memory" const KNOWN_MEMORY_RELATED = new Set([ "memory-lancedb", // legacy, being deprecated ]); function guardMemoryBackends() { const issues = []; const found = []; if (!existsSync(EXTENSIONS_DIR)) { issues.push({ level: "ERROR", msg: `Extensions dir not found: ${EXTENSIONS_DIR}` }); return { pass: false, found, issues }; } const extDirs = readdirSync(EXTENSIONS_DIR, { withFileTypes: true }) .filter(d => d.isDirectory()) .map(d => d.name); for (const extDir of extDirs) { const pluginJsonPath = join(EXTENSIONS_DIR, extDir, "openclaw.plugin.json"); if (!existsSync(pluginJsonPath)) continue; try { const plugin = JSON.parse(readFileSync(pluginJsonPath, "utf8")); const pluginId = plugin.id || extDir; const kind = plugin.kind || ""; const contracts = plugin.contracts || {}; const tools = contracts.tools || []; const memoryTools = tools.filter(t => t.startsWith("memory_") || t.startsWith("wiki_")); const isMemoryPlugin = kind === "memory" || tools.includes("memory_search") || tools.includes("memory_get") || memoryTools.length >= 2; if (isMemoryPlugin) { found.push({ id: pluginId, kind, memoryTools }); if (!ALLOWED_MEMORY_PLUGINS.has(pluginId) && !KNOWN_MEMORY_RELATED.has(pluginId)) { issues.push({ level: "FAIL", msg: `UNEXPECTED memory plugin: ${pluginId} (kind=${kind}, tools=${memoryTools.join(",")})` }); } } } catch (e) { issues.push({ level: "WARN", msg: `Could not parse ${pluginJsonPath}: ${e.message}` }); } } const pass = issues.filter(i => i.level === "FAIL").length === 0; return { pass, found, issues }; } // Run const result = guardMemoryBackends(); console.log(""); console.log("═══════════════════════════════════════"); console.log(" Guard 1: Memory Backend Guard"); console.log("═══════════════════════════════════════"); console.log(""); if (result.found.length === 0) { console.log(" No memory plugins found."); } else { console.log(` Memory plugins found: ${result.found.length}`); for (const f of result.found) { const status = ALLOWED_MEMORY_PLUGINS.has(f.id) ? "✓" : KNOWN_MEMORY_RELATED.has(f.id) ? "⚠ (legacy)" : "✗ UNEXPECTED"; console.log(` ${status} ${f.id} (kind="${f.kind}", tools: ${f.memoryTools.join(", ")})`); } } for (const issue of result.issues) { console.log(` [${issue.level}] ${issue.msg}`); } if (result.pass) { console.log(""); console.log(" ✓ PASS: memory backend count within allowed list"); process.exit(0); } else { console.log(""); console.log(" ✗ FAIL: unexpected memory backend(s) detected"); process.exit(1); }