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.

331 lines 15.5 kB
import { pass, fail } from "./helpers.js"; const VERIFIED_ON = "2026-04-19"; /** Return the constraints provenance. */ function constraintsProvenance(type, paths, sourceType = "spec") { return { source_type: sourceType, source_urls: [], verified_on: VERIFIED_ON, normative_level: type === "integrity" ? "MUST" : type === "advisory" ? "SHOULD" : "BEST_PRACTICE", evidence_paths: paths, }; } /** Classify each agent by whether BOTH its file-read deny rules AND its Bash * deny hook block direct literal secret-path access. Settings/Codex permission * profile file-read rules do not cover Bash shell reads (cat/source/base64/etc.), * so Bash-side direct-path coverage is required for any non-script-only agent. * * Uncovered agents are split by deny mechanism so remediation guidance stays * accurate: script-only agents (Copilot etc.) have no file-read deny layer, so * they must be told to extend the Bash hook only. */ function classifySecretDeny(ctx) { const covered = []; const scriptOnly = []; const uncoveredSettings = []; const uncoveredScript = []; for (const agentFacts of ctx.agents) { // Capability-limited agents (no deny mechanism documented upstream) are // skipped from secret-deny classification entirely. if (agentFacts.agent.denyMechanism === null) continue; const bashOk = agentFacts.hooks.bashDenyCoversSecrets; const readOk = agentFacts.hooks.readDenyCoversSecrets; const isScriptOnly = agentFacts.agent.denyMechanism.type === "deny-script"; if (isScriptOnly) { // Script-only agents (e.g. Copilot) rely entirely on the Bash hook. if (bashOk) { scriptOnly.push(agentFacts.agent.id); } else { uncoveredScript.push(agentFacts.agent.id); } } else { // Settings-based agents need BOTH file-read deny AND Bash hook coverage. if (readOk && bashOk) { covered.push(agentFacts.agent.id); } else { uncoveredSettings.push(agentFacts.agent.id); } } } return { covered, scriptOnly, uncoveredSettings, uncoveredScript }; } function secretDenyDetails(agents) { return { denyMatrix: agents.map((agentFacts) => { const missingPatterns = []; const isScriptOnly = agentFacts.agent.denyMechanism?.type === "deny-script"; if (agentFacts.agent.denyMechanism !== null && !isScriptOnly && !agentFacts.hooks.readDenyCoversSecrets) { missingPatterns.push("file-read-secret-paths"); } if (!agentFacts.hooks.bashDenyCoversSecrets) { missingPatterns.push("bash-secret-paths"); } return { agent: agentFacts.agent.id, missingPatterns, extraPatterns: [], hookRegistered: agentFacts.hooks.denyIsRegistered, }; }), }; } function pipeToShellDetails(agents) { return { denyMatrix: agents.map((agentFacts) => ({ agent: agentFacts.agent.id, missingPatterns: agentFacts.hooks.denyBlocksPipeToShell ? [] : ["pipe-to-shell"], extraPatterns: [], hookRegistered: agentFacts.hooks.denyIsRegistered, })), }; } function denyRegistrationDetails(agents, unregistered, noDeny, pathMismatch) { return { denyMatrix: agents.map((agentFacts) => { const missingPatterns = []; if (unregistered.includes(agentFacts.agent.id)) { missingPatterns.push("deny-hook-registration"); } if (noDeny.includes(agentFacts.agent.id)) missingPatterns.push("deny-hook"); if (pathMismatch.includes(agentFacts.agent.id)) { missingPatterns.push("deny-hook-path"); } return { agent: agentFacts.agent.id, missingPatterns, extraPatterns: [], hookRegistered: agentFacts.hooks.denyIsRegistered, }; }), }; } const denyCoversSecrets = { id: "deny-covers-secrets", name: "Deny blocks direct literal secret paths", concern: "constraints", type: "integrity", provenance: constraintsProvenance("integrity", [ "docs/harness-audit.md", ".goat-flow/learning-loop/footguns/auditor.md", ]), /** Run the Deny blocks direct literal secret paths check. */ run: (ctx) => { const { covered, scriptOnly, uncoveredSettings, uncoveredScript } = classifySecretDeny(ctx); const details = secretDenyDetails(ctx.agents); const anyUncovered = uncoveredSettings.length > 0 || uncoveredScript.length > 0; if (covered.length === 0 && !anyUncovered) { // All agents are script-only and covered - platform limitation, not a failure return { ...pass([ "Limited assurance: no agents support file-read deny patterns", ...scriptOnly.map((id) => `${id}: limited assurance - script-only deny; Bash hook blocks direct literal secret paths, but file-read deny is unavailable`), ], details), displayStatus: "info", assurance: "limited", }; } const findings = []; if (covered.length > 0) { findings.push(`${covered.join(", ")}: file-read deny + Bash hook both block direct literal secret paths`); } if (scriptOnly.length > 0) { findings.push(...scriptOnly.map((id) => `${id}: limited assurance - script-only deny; Bash hook blocks direct literal secret paths, but file-read deny is unavailable`)); } if (!anyUncovered) { const result = pass(findings, details); if (scriptOnly.length === 0) return result; return { ...result, displayStatus: "info", assurance: "limited" }; } const recs = []; const fixes = []; if (uncoveredSettings.length > 0) { findings.push(`${uncoveredSettings.join(", ")}: direct literal secret-path blocking incomplete (file-read deny and/or Bash hook pattern is missing)`); recs.push(`Add direct literal secret-path blocking to ${uncoveredSettings.join(", ")}: settings/Codex permission file-read patterns for .env / .ssh / .aws / .pem / .key, AND the Bash deny hook must block cat/source/base64/etc. on the same literal paths.`); fixes.push(`${uncoveredSettings.join(", ")}: extend the agent file-read deny layer with .env, .ssh, .aws, credentials, *.key, *.pem AND add an is_secret_path_touch (or equivalent) check in the Bash deny hook. File-read deny alone does not bind Bash shell reads.`); } if (uncoveredScript.length > 0) { findings.push(`${uncoveredScript.join(", ")}: Bash deny hook does not block direct literal secret paths (script-only agent - no file-read deny layer applies)`); recs.push(`Add direct literal secret-path blocking to the Bash deny hook for ${uncoveredScript.join(", ")}: block cat/source/base64/etc. on .env, .ssh, .aws, credentials, *.key, *.pem.`); fixes.push(`${uncoveredScript.join(", ")}: add an is_secret_path_touch (or equivalent) check in the Bash deny hook. Script-only agents have no file-read deny surface; the Bash hook is the only enforcement layer.`); } return fail(findings, recs, fixes, details); }, }; const denyBlocksDangerous = { id: "deny-blocks-dangerous", name: "Deny blocks dangerous commands", concern: "constraints", type: "integrity", provenance: constraintsProvenance("integrity", [ "docs/harness-audit.md", ".goat-flow/learning-loop/footguns/auditor.md", ".goat-flow/learning-loop/footguns/hooks.md", ]), /** Run the Deny blocks dangerous commands check. */ run: (ctx) => { if (ctx.agents.length === 0) { return fail(["No agents to check"], ["Configure at least one agent"]); } const findings = []; const recs = []; const fixes = []; const denyMatrix = []; let hasDangerousDenyGap = false; for (const agentFacts of ctx.agents) { const { denyBlocksRmRf, denyBlocksGitPush, denyBlocksChmod } = agentFacts.hooks; const missingPatterns = []; if (!denyBlocksRmRf) missingPatterns.push("broad rm -r"); if (!denyBlocksGitPush) missingPatterns.push("git-push"); if (!denyBlocksChmod) missingPatterns.push("chmod"); denyMatrix.push({ agent: agentFacts.agent.id, missingPatterns, extraPatterns: [], hookRegistered: agentFacts.hooks.denyIsRegistered, }); if (missingPatterns.length === 0) { findings.push(`${agentFacts.agent.id}: deny blocks broad rm -r, git-push, chmod`); } else { hasDangerousDenyGap = true; findings.push(`${agentFacts.agent.id}: deny missing coverage for ${missingPatterns.join(", ")}`); recs.push(`Add deny patterns for ${missingPatterns.join(", ")} to ${agentFacts.agent.id}`); fixes.push(`Add deny patterns for ${missingPatterns.join(", ")} in ${agentFacts.agent.id} agent configuration.`); } } if (hasDangerousDenyGap) return fail(findings, recs, fixes, { denyMatrix }); return pass(findings, { denyMatrix }); }, }; const denyBlocksPipeToShell = { id: "deny-blocks-pipe-to-shell", name: "Deny blocks pipe-to-shell", concern: "constraints", type: "advisory", provenance: constraintsProvenance("advisory", [ "docs/harness-audit.md", ".goat-flow/learning-loop/footguns/auditor.md", ".goat-flow/learning-loop/footguns/hooks.md", ], "incident"), /** Run the Deny blocks pipe-to-shell check. */ run: (ctx) => { const covered = []; const uncovered = []; const details = pipeToShellDetails(ctx.agents); for (const agentFacts of ctx.agents) { if (agentFacts.hooks.denyBlocksPipeToShell) { covered.push(agentFacts.agent.id); } else { uncovered.push(agentFacts.agent.id); } } if (uncovered.length === 0) { return pass([`${covered.join(", ")}: deny blocks pipe-to-shell (curl | bash)`], details); } if (covered.length === 0) { return fail(["No agents block pipe-to-shell pattern (curl | bash)"], ["Add deny pattern for pipe-to-shell commands"], [ "Add a deny pattern matching curl|bash and wget|sh in agent deny configuration.", ], details); } return fail([`${uncovered.join(", ")}: pipe-to-shell not blocked`], [`Add pipe-to-shell deny pattern to ${uncovered.join(", ")}`], [ `Add deny patterns for curl|bash and wget|sh to ${uncovered.join(", ")} agent configuration.`, ], details); }, }; function findAgent(agents, id) { return agents.find((agentFacts) => agentFacts.agent.id === id); } /** * Group deny-hook registration states for remediation. * * A missing hook, an unregistered hook, and a registered-but-wrong path all need * different fix text; keeping these buckets separate avoids telling users to * register a hook that does not exist or to recreate one that is only miswired. */ function classifyDenyRegistration(agents) { const registered = []; const unregistered = []; const noDeny = []; const pathMismatch = []; for (const agentFacts of agents) { if (!agentFacts.hooks.denyExists && !agentFacts.hooks.denyIsConfigBased) { noDeny.push(agentFacts.agent.id); continue; } if (agentFacts.hooks.denyIsRegistered) { registered.push(agentFacts.agent.id); const expected = agentFacts.agent.denyHookFile; const actual = agentFacts.hooks.denyRegisteredPath; if (expected && actual && !actual.endsWith(expected)) { pathMismatch.push(agentFacts.agent.id); } } else { unregistered.push(agentFacts.agent.id); } } return { registered, unregistered, noDeny, pathMismatch }; } function buildDenyRegistrationFailure(agents, registered, unregistered, noDeny, pathMismatch) { const findings = [ ...registered .filter((id) => !pathMismatch.includes(id)) .map((id) => `${id}: deny hook registered as ${findAgent(agents, id)?.agent.hookEvents?.preTool ?? "pre-tool"} hook`), ...pathMismatch.map((id) => { const agentFacts = findAgent(agents, id); return `${id}: registered hook path "${agentFacts?.hooks.denyRegisteredPath}" does not match expected deny hook "${agentFacts?.agent.denyHookFile}"`; }), ...unregistered.map((id) => `${id}: deny hook exists but is NOT registered as a ${findAgent(agents, id)?.agent.hookEvents?.preTool ?? "pre-tool"} hook`), ]; const actions = [ ...unregistered.map((id) => `Register the deny hook in ${id} agent settings`), ...pathMismatch.map((id) => `Fix ${id} hook registration to point at the canonical deny hook (${findAgent(agents, id)?.agent.denyHookFile})`), ]; return fail(findings, actions, [ ...unregistered.map((id) => `Add a ${findAgent(agents, id)?.agent.hookEvents?.preTool ?? "PreToolUse"} hook entry in ${id} agent settings that runs deny-dangerous.sh.`), ...pathMismatch.map((id) => `Update the ${findAgent(agents, id)?.agent.hookEvents?.preTool ?? "PreToolUse"} hook in ${id} to reference ${findAgent(agents, id)?.agent.denyHookFile}.`), ], denyRegistrationDetails(agents, unregistered, noDeny, pathMismatch)); } const denyHookRegistered = { id: "deny-hook-registered", name: "Deny hook registered in agent settings", concern: "constraints", type: "integrity", provenance: constraintsProvenance("integrity", ["docs/harness-audit.md", ".goat-flow/learning-loop/footguns/auditor.md"], "incident"), run: (ctx) => { const { registered, unregistered, noDeny, pathMismatch } = classifyDenyRegistration(ctx.agents); if (unregistered.length > 0 || pathMismatch.length > 0) { return buildDenyRegistrationFailure(ctx.agents, registered, unregistered, noDeny, pathMismatch); } const findings = [ ...registered.map((id) => `${id}: deny hook registered as ${findAgent(ctx.agents, id)?.agent.hookEvents?.preTool ?? "pre-tool"} hook`), ...noDeny.map((id) => `${id}: no deny mechanism (registration check skipped)`), ]; return pass(findings.length > 0 ? findings : ["No agents with deny hooks to check"], denyRegistrationDetails(ctx.agents, unregistered, noDeny, pathMismatch)); }, }; export const CONSTRAINTS_CHECKS = [ denyCoversSecrets, denyBlocksDangerous, denyBlocksPipeToShell, denyHookRegistered, ]; //# sourceMappingURL=check-constraints.js.map