#!/usr/bin/env node /** * Guard 5: Tool Trace Guard * Inventories tool call sites and checks for trace/log coverage. * * → Architecture Freeze v2 (PR-1): docs/architecture-freeze-v2.md * → Future: PR-10 Trace Model enforcement * * SOFT GUARD — Trace model not yet implemented (PR-10). * Currently inventories tool call entry points and reports gaps. * No hard failure at this stage. */ import { readdirSync, readFileSync, existsSync } from "node:fs"; import { join } from "node:path"; import { execSync } from "node:child_process"; const OPENCLAW_HOME = process.env.OPENCLAW_HOME || "/opt/homebrew/lib/node_modules/openclaw"; const DIST_DIR = join(OPENCLAW_HOME, "dist"); function guardToolTrace() { const issues = []; const toolSites = []; let totalToolCallSites = 0; let sitesWithTrace = 0; // Search for tool call patterns in dist const searchTerms = [ "tool_call", "toolCall", "tool.call", "tool_exec", "toolExec", "invokeTool", "runTool", ]; try { const grepResult = execSync( `grep -rln -E "(tool_call|toolCall|tool\\.call|tool_exec|toolExec|executeTool|invokeTool|runTool)" "${DIST_DIR}" 2>/dev/null | grep -v ".d.ts$" | grep -v "node_modules" | grep ".js$" | head -30`, { timeout: 30000, encoding: "utf8", maxBuffer: 10 * 1024 * 1024 } ).trim(); if (grepResult) { const files = grepResult.split("\n").filter(Boolean); totalToolCallSites = files.length; for (const file of files) { const relativePath = file.replace(DIST_DIR + "/", ""); // Check if this file has trace/log instrumentation try { const content = readFileSync(file, "utf8"); const hasTrace = content.includes("trace") || content.includes("Trace") || content.includes("span") || content.includes("log") || content.includes("event") || content.includes("audit") || content.includes("diagnostics"); if (hasTrace) sitesWithTrace++; toolSites.push({ file: relativePath, hasTrace, module: relativePath.split("/")[0] || "unknown", }); } catch { toolSites.push({ file: relativePath, hasTrace: false, module: relativePath.split("/")[0] || "unknown", }); } } } } catch (e) { if (e.status !== 1) { console.log(` Warning: grep failed: ${e.message}`); } } const sitesWithoutTrace = toolSites.filter(s => !s.hasTrace); console.log(""); console.log("═══════════════════════════════════════"); console.log(" Guard 5: Tool Trace Guard"); console.log("═══════════════════════════════════════"); console.log(""); console.log(` Tool call entry points found: ${totalToolCallSites}`); console.log(` Sites with trace/log: ${sitesWithTrace}`); console.log(` Sites without trace: ${sitesWithoutTrace.length}`); // Group by module const moduleSummary = {}; for (const s of toolSites) { if (!moduleSummary[s.module]) { moduleSummary[s.module] = { total: 0, traced: 0 }; } moduleSummary[s.module].total++; if (s.hasTrace) moduleSummary[s.module].traced++; } if (Object.keys(moduleSummary).length > 0) { console.log("\n Trace coverage by module:"); for (const [mod, stats] of Object.entries(moduleSummary)) { const pct = stats.total > 0 ? Math.round(stats.traced / stats.total * 100) : 0; const bar = "█".repeat(Math.round(pct / 10)) + "░".repeat(10 - Math.round(pct / 10)); console.log(` ${bar} ${mod}: ${stats.traced}/${stats.total} (${pct}%)`); } } if (sitesWithoutTrace.length > 0 && sitesWithoutTrace.length <= 10) { console.log("\n Sites without trace:"); for (const s of sitesWithoutTrace) { console.log(` • ${s.file}`); } } console.log(""); console.log(" ⚠ WARN: Trace model not yet enforced"); console.log(` Current coverage: ${sitesWithTrace}/${totalToolCallSites} sites have tracing`); console.log(" After PR-10 (Trace Model), all tool calls will require trace spans."); // No hard fail — soft guard process.exit(0); } guardToolTrace();