Files
16gagent/scripts/guard-tool-trace.mjs
2026-06-06 10:40:48 +08:00

133 lines
4.3 KiB
JavaScript

#!/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();