UNPKG

@blundergoat/goat-flow

Version:

AI coding agent harness and local dashboard for Claude Code, OpenAI Codex, Google Antigravity, and GitHub Copilot - setup audits, guardrails, structured skills, deny hooks, and persistent learning loops.

231 lines 10.9 kB
import { isPackagedInstall } from "../paths.js"; import { runAudit, runAuditBatch } from "../audit/audit.js"; import { loadConfig } from "../config/reader.js"; import { recordEvidenceEvent } from "../evidence/envelope.js"; import { redactEvidenceText } from "../evidence/redaction.js"; import { createFS } from "../facts/fs.js"; import { extractProjectFacts } from "../facts/orchestrator.js"; import { KNOWN_AGENT_IDS, KNOWN_AGENT_LIST, VALID_AGENTS, } from "./dashboard-route-types.js"; import { appendAuditProfile, buildAuditCacheSignature, buildDashboardReport, createDashboardAuditProfiler, enrichDashboardReport, readAuditCache, shouldProfileAuditRequest, writeAuditCache, } from "./dashboard-reporting.js"; import { buildSetupDetectPayload } from "./setup-detect.js"; function isCacheEligible(agentFilter, includeHarness) { return !agentFilter && includeHarness && isPackagedInstall(); } /** Resolve the managed agent list for dashboard aggregate audits. */ function resolveDashboardManagedAgentIds(agentFilter) { return agentFilter === null ? [...KNOWN_AGENT_IDS] : [agentFilter]; } function buildDashboardAuditReport(projectPath, agentFilter, includeHarness, profiler) { const fs = createFS(projectPath); const configAgents = profiler.span("managed-agent resolution", () => resolveDashboardManagedAgentIds(agentFilter)); const auditFactProfile = agentFilter === null ? "dashboard-summary" : "full"; const batch = profiler.span("runAuditBatch", () => runAuditBatch(fs, projectPath, { agentFilter, harness: includeHarness, // Summary cards only need to know whether the deny mechanism is // installed. Explicit per-agent audits and quality flows still run the // slower runtime self-test. denyMechanismEvidenceLevel: agentFilter === null ? "present-only" : "full", factProfile: auditFactProfile, profile: profiler, }, configAgents)); return profiler.span("dashboard report build", () => buildDashboardReport(batch.aggregate, batch.perAgent, projectPath, profiler)); } /** Convert unknown exceptions to the JSON-safe error message used by dashboard routes. */ function routeErrorMessage(err) { return err instanceof Error ? err.message : String(err); } /** Send a standard dashboard JSON error response for non-profiled routes. */ function jsonErrorResponse(ctx, res, err) { ctx.jsonResponse(res, ctx.responseStatusForError(err, 500), { error: routeErrorMessage(err), }); } function readCachedDashboardAudit(ctx, projectPath, fresh, signature, profiler) { if (fresh || signature === null) return null; return profiler.span("cache read", () => readAuditCache(projectPath, ctx.packageVersion, signature)); } /** Record the dashboard audit event after either cache hit or fresh audit run. */ function recordAuditRunEvent(ctx, projectPath, includeHarness, agentFilter, report, cached) { ctx.recordDashboardEvent(projectPath, "audit.run", { cached, harness: includeHarness, agent: agentFilter ?? "all", status: report.status, }); } /** Send an audit report response with cache fields and optional profiling metadata. */ function sendAuditReport(ctx, res, report, profiler, cacheState) { ctx.jsonResponse(res, 200, appendAuditProfile({ ...report, cached: cacheState.cached, cachedAt: cacheState.cachedAt, }, profiler)); } /** Send a profiled audit error response so the dashboard can still render spans. */ function sendAuditError(ctx, res, err, profiler) { ctx.jsonResponse(res, ctx.responseStatusForError(err, 500), appendAuditProfile({ error: routeErrorMessage(err) }, profiler)); } /** Read, enrich, record, and return a cached audit response when the cache matches. */ function respondWithCachedAudit(ctx, res, cached, projectPath, includeHarness, agentFilter, profiler) { const report = profiler.span("learning-loop enrichment", () => enrichDashboardReport(cached.report, projectPath)); recordAuditRunEvent(ctx, projectPath, includeHarness, agentFilter, report, true); sendAuditReport(ctx, res, report, profiler, { cached: true, cachedAt: cached.cachedAt, }); } /** Write the fresh audit result to the dashboard cache when the request is eligible. */ function writeFreshAuditCache(ctx, projectPath, signature, report, profiler) { if (signature === null) return; profiler.span("cache write", () => { writeAuditCache(projectPath, ctx.packageVersion, signature, report); }); } /** Parse optional agent filters without rejecting dashboard-wide requests. */ function parseAgentFilter(param) { return param && VALID_AGENTS.has(param) ? param : null; } function recordSetupPrompt(projectPath, agent, renderedOutput) { recordEvidenceEvent({ producer: "dashboard-session-trace", actor: "server", eventType: "setup.prompt", projectRoot: projectPath, payload: { agent, output: redactEvidenceText("setup prompt", renderedOutput), }, }); } /** Build the `/api/audit` handler bound to one dashboard route context. */ function createHandleAuditRequest(ctx) { return function handleAuditRequest(url, res) { if (url.pathname !== "/api/audit") return false; const includeHarness = url.searchParams.get("quality") === "true"; const agentFilter = parseAgentFilter(url.searchParams.get("agent")); const fresh = url.searchParams.get("fresh") === "true"; const profiler = createDashboardAuditProfiler(shouldProfileAuditRequest(url, ctx.devMode)); try { const projectPath = ctx.validatedPath(url.searchParams.get("path"), "project-read"); const auditCacheSignature = isCacheEligible(agentFilter, includeHarness) ? profiler.span("cache signature", () => buildAuditCacheSignature(projectPath, ctx.packageVersion)) : null; const cached = readCachedDashboardAudit(ctx, projectPath, fresh, auditCacheSignature, profiler); if (cached) { respondWithCachedAudit(ctx, res, cached, projectPath, includeHarness, agentFilter, profiler); return true; } const report = buildDashboardAuditReport(projectPath, agentFilter, includeHarness, profiler); writeFreshAuditCache(ctx, projectPath, auditCacheSignature, report, profiler); recordAuditRunEvent(ctx, projectPath, includeHarness, agentFilter, report, false); sendAuditReport(ctx, res, report, profiler, { cached: false, cachedAt: null, }); } catch (err) { sendAuditError(ctx, res, err, profiler); } return true; }; } /** Build the `/api/setup/detect` handler bound to one dashboard route context. */ function createHandleSetupDetectRequest(ctx) { return function handleSetupDetectRequest(url, res) { if (url.pathname !== "/api/setup/detect") return false; try { const projectPath = ctx.validatedPath(url.searchParams.get("path"), "project-read"); ctx.jsonResponse(res, 200, buildSetupDetectPayload(projectPath)); } catch (err) { jsonErrorResponse(ctx, res, err); } return true; }; } /** Validate the setup agent parameter and send the route-owned 400 response when invalid. */ function validateSetupAgentParam(ctx, res, agentParam) { if (!agentParam) { ctx.jsonResponse(res, 400, { error: `Missing required parameter: agent. Valid: ${KNOWN_AGENT_LIST}`, }); return null; } if (!VALID_AGENTS.has(agentParam)) { ctx.jsonResponse(res, 400, { error: `Invalid agent: ${agentParam}. Valid: ${KNOWN_AGENT_LIST}`, }); return null; } return agentParam; } /** Compose the setup prompt output using dashboard-summary facts and static deny evidence. */ async function composeDashboardSetupOutput(projectPath, agent) { const fs = createFS(projectPath); const configState = loadConfig(projectPath, fs); const facts = extractProjectFacts(fs, { agentFilter: agent, projectPath, configState, includeStack: false, }); const auditReport = runAudit(fs, projectPath, { agentFilter: agent, harness: true, factProfile: "dashboard-summary", denyMechanismEvidenceLevel: "static", }); const { composeSetup } = await import("../prompt/compose-setup.js"); const output = composeSetup(auditReport, facts, agent, { denyMechanismEvidenceLevel: "static", }); return output ?? "No setup output generated."; } /** Build the `/api/setup` handler bound to one dashboard route context. */ function createHandleSetupRequest(ctx) { return async function handleSetupRequest(url, res) { if (url.pathname !== "/api/setup") return false; const agent = validateSetupAgentParam(ctx, res, url.searchParams.get("agent")); if (agent === null) return true; try { const projectPath = ctx.validatedPath(url.searchParams.get("path"), "project-read"); const renderedOutput = await composeDashboardSetupOutput(projectPath, agent); recordSetupPrompt(projectPath, agent, renderedOutput); ctx.jsonResponse(res, 200, { output: renderedOutput, }); } catch (err) { jsonErrorResponse(ctx, res, err); } return true; }; } /** * Bind the audit/setup route handlers to one server's request context so each closure can reach the * validated-path resolver, evidence recorder, and JSON responder without per-request wiring. The * closure shape is intentional because the context is resolved once per server, and binding it here * lets the handlers be registered as plain `(url, res)` callbacks. Each handler reports validation, * audit, and cache failures back to the client as a JSON error body instead of throwing, so a failed * request never crashes the server. The aggregate audit route also folds a disk cache over * `runAuditBatch` to avoid paying a full re-audit on every fresh Home load. * * @param ctx - per-server dashboard route context carrying path validation, the audit cache, and IO hooks * @returns the three audit/setup handlers; each returns true once it has owned and answered a matching * request, or false to let the next handler try the URL */ export function createAuditRouteHandlers(ctx) { return { handleAuditRequest: createHandleAuditRequest(ctx), handleSetupDetectRequest: createHandleSetupDetectRequest(ctx), handleSetupRequest: createHandleSetupRequest(ctx), }; } //# sourceMappingURL=dashboard-audit-routes.js.map