@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
JavaScript
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