@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.
273 lines • 11.7 kB
JavaScript
/**
* Check whether the agent's deny mechanism blocks git commit and/or git push.
*
* @param fs - project filesystem adapter used to read settings or hook scripts
* @param agent - agent profile whose deny mechanism should be inspected
* @returns git operation coverage detected from the agent's configured guardrail path
*/
export function checkDenyPatterns(fs, agent) {
/** Deny mechanism configuration for this agent */
const deny = agent.denyMechanism;
if (deny === null) {
return { gitCommitBlocked: false, gitPushBlocked: false };
}
if (deny.type === "settings-deny") {
/** Parsed JSON from the settings deny file */
const parsed = fs.readJson(deny.path);
if (parsed == null)
return { gitCommitBlocked: false, gitPushBlocked: false };
/** Permissions object from the parsed settings */
const permissions = parsed.permissions;
/** Raw deny array from permissions */
const rawDeny = permissions?.deny;
/** Deny patterns as a string array, filtering non-strings for safety */
const denyList = Array.isArray(rawDeny)
? rawDeny.filter((p) => typeof p === "string")
: [];
return {
gitCommitBlocked: denyList.some((p) => p.includes("git commit")),
gitPushBlocked: denyList.some((p) => p.includes("git push")),
};
}
if (deny.type === "deny-script") {
/** Content of the deny hook script */
const content = fs.readFile(deny.path);
if (content == null)
return { gitCommitBlocked: false, gitPushBlocked: false };
return {
gitCommitBlocked: /git\s+commit/i.test(content),
gitPushBlocked: /git\s+push/i.test(content),
};
}
// The 'both' mechanism blocks when either the settings file or the deny
// script matches.
/** Deny results from the settings-based mechanism */
const settings = checkDenyPatterns(fs, {
...agent,
denyMechanism: { type: "settings-deny", path: deny.settingsPath },
});
/** Deny results from the script-based mechanism */
const script = checkDenyPatterns(fs, {
...agent,
denyMechanism: { type: "deny-script", path: deny.scriptPath },
});
return {
gitCommitBlocked: settings.gitCommitBlocked || script.gitCommitBlocked,
gitPushBlocked: settings.gitPushBlocked || script.gitPushBlocked,
};
}
/** Extract settings facts from supported agent config formats. */
// eslint-disable-next-line complexity -- intentional multi-format settings extraction requires branching.
export function extractSettingsFacts(fs, agent) {
/** Whether the agent's settings file exists on disk */
const exists = agent.settingsFile ? fs.exists(agent.settingsFile) : false;
let isSettingsValid = false;
let parsed = null;
let hasDenyPatterns = false;
if (agent.settingsFile) {
if (agent.settingsFile.endsWith(".toml")) {
// TOML (Codex config.toml) -- parse key=value pairs into a flattened object
const tomlContent = fs.readFile(agent.settingsFile);
if (tomlContent) {
const tomlObj = {};
let currentSection = "";
for (const line of tomlContent.split("\n")) {
const trimmed = line.trim();
if (trimmed.startsWith("#") || trimmed === "")
continue;
const sectionMatch = trimmed.match(/^\[(.+)\]$/);
if (sectionMatch?.[1]) {
currentSection = normalizeTomlDottedKey(sectionMatch[1]);
continue;
}
const kvMatch = trimmed.match(/^((?:"(?:\\.|[^"\\])*")|[\w.-]+)\s*=\s*(.+)$/);
if (kvMatch?.[1] && kvMatch[2]) {
const key = currentSection
? `${currentSection}.${normalizeTomlKey(kvMatch[1])}`
: normalizeTomlKey(kvMatch[1]);
const val = parseTomlScalar(kvMatch[2]);
tomlObj[key] = val;
}
}
isSettingsValid = Object.keys(tomlObj).length > 0;
parsed = tomlObj;
}
}
else {
parsed = fs.readJson(agent.settingsFile);
isSettingsValid = parsed !== null;
}
if (isSettingsValid && parsed) {
/** Permissions object from the parsed settings */
const perms = parsed.permissions;
/** Raw deny array from permissions */
const denyArr = perms?.deny;
hasDenyPatterns =
Array.isArray(denyArr) && denyArr.length > 0;
}
}
// Require deny coverage for the common secret-bearing paths goat-flow cares about.
const readDenyCoversSecrets = checkReadDenyCoversSecrets(parsed, hasDenyPatterns, fs);
return {
exists,
valid: isSettingsValid,
parsed,
hasDenyPatterns,
readDenyCoversSecrets,
};
}
/** Remove simple TOML string-key quoting for keys goat-flow needs to inspect. */
function normalizeTomlKey(rawKey) {
const key = rawKey.trim();
if (key.startsWith('"') && key.endsWith('"'))
return key.slice(1, -1);
return key;
}
/** Normalize a dotted TOML table key while preserving quoted path/glob parts. */
function normalizeTomlDottedKey(rawKey) {
return rawKey.split(".").map(normalizeTomlKey).join(".");
}
/** Parse the simple scalar values used in Codex config.toml. */
function parseTomlScalar(rawValue) {
const value = rawValue.trim();
if (value === "true")
return true;
if (value === "false")
return false;
if (/^-?\d+$/u.test(value))
return Number(value);
if (value.startsWith('"') && value.endsWith('"'))
return value.slice(1, -1);
return value;
}
/** Require settings-based read denies for the main secret and credential path families. */
function checkReadDenyCoversSecrets(parsed, hasDenyPatterns, fs) {
if (checkCodexPermissionProfileCoversSecrets(parsed, fs))
return true;
if (!hasDenyPatterns || !parsed)
return false;
/** Permissions object from the parsed settings */
const perms = parsed.permissions;
/** Raw deny array from permissions */
const denyArr = perms?.deny;
if (!Array.isArray(denyArr))
return false;
/** All deny patterns concatenated into a single string for regex matching */
const denyStr = denyArr.join(" ");
/** Whether .env paths are covered by deny rules */
const hasEnv = /Read\(.*\.env/.test(denyStr);
/** Whether .ssh paths are covered by deny rules */
const hasSsh = /Read\(.*\.ssh/.test(denyStr);
/** Whether .aws paths are covered by deny rules */
const hasAws = /Read\(.*\.aws/.test(denyStr);
/** Whether key/credential paths are covered by deny rules */
const hasKeys = /Read\(.*\.(pem|key|pfx)\b/.test(denyStr) ||
/Read\(.*credentials/.test(denyStr);
return hasEnv && hasSsh && hasAws && hasKeys;
}
/** Detect Codex TOML permission profiles that deny the main secret path families. */
function checkCodexPermissionProfileCoversSecrets(parsed, fs) {
if (!parsed || typeof parsed !== "object")
return false;
const defaultPermissions = parsed
.default_permissions;
if (typeof defaultPermissions !== "string" || defaultPermissions === "") {
return false;
}
const denied = collectCodexDeniedWorkspaceRootPatterns(parsed, defaultPermissions);
if (denied.size === 0)
return false;
return (hasCodexEnvDeny(denied, fs) &&
hasAnyCodexPattern(denied, ["secrets/**", "**/secrets/**"]) &&
hasAnyCodexPattern(denied, [".ssh/**", "**/.ssh/**"]) &&
hasAnyCodexPattern(denied, [".aws/**", "**/.aws/**"]) &&
hasCodexCredentialRootDeny(denied, fs));
}
function collectCodexWorkspaceRootEntry(key, value, inlineTableKey, prefix) {
if (key === inlineTableKey && typeof value === "string") {
return parseTomlInlineStringTable(value).map(([pattern, mode]) => ({
pattern,
mode,
}));
}
if (typeof value !== "string" || !key.startsWith(prefix))
return [];
const pattern = key.slice(prefix.length);
return pattern ? [{ pattern, mode: value }] : [];
}
export function collectCodexWorkspaceRootEntries(parsed, profileName) {
if (!parsed || typeof parsed !== "object")
return [];
const rootToken = ":workspace_roots";
const inlineTableKey = `permissions.${profileName}.filesystem.${rootToken}`;
const prefix = `${inlineTableKey}.`;
return Object.entries(parsed).flatMap(([key, value]) => collectCodexWorkspaceRootEntry(key, value, inlineTableKey, prefix));
}
function collectCodexDeniedWorkspaceRootPatterns(parsed, profileName) {
const denied = new Set();
for (const { pattern, mode } of collectCodexWorkspaceRootEntries(parsed, profileName)) {
if (isCodexDenyMode(mode))
denied.add(pattern);
}
return denied;
}
/** Parse the single-line TOML inline string table shape Codex accepts. */
function parseTomlInlineStringTable(rawValue) {
const value = rawValue.trim();
if (!value.startsWith("{") || !value.endsWith("}"))
return [];
const entries = [];
const entryPattern = /"((?:\\.|[^"\\])*)"\s*=\s*"((?:\\.|[^"\\])*)"/gu;
for (const match of value.matchAll(entryPattern)) {
const [, key, mode] = match;
if (key && mode)
entries.push([key, mode]);
}
return entries;
}
/** Return whether any required Codex filesystem pattern is denied. */
function hasAnyCodexPattern(denied, patterns) {
return patterns.some((pattern) => denied.has(pattern));
}
/** Return whether every required Codex filesystem pattern is denied. */
function hasEveryCodexPattern(denied, patterns) {
return patterns.every((pattern) => denied.has(pattern));
}
/** Recognize both old goat-flow "none" entries and current Codex "deny" entries. */
function isCodexDenyMode(mode) {
return mode === "none" || mode === "deny";
}
/** Detect root .env files and variants. Codex broad denies also deny .env.example. */
function existingExactPathsAreDenied(denied, fs, patterns) {
return patterns
.filter((pattern) => fs.exists(pattern))
.every((pattern) => denied.has(pattern));
}
/** Confirm Codex denies every root env file that actually exists in the target project. */
function hasCodexEnvDeny(denied, fs) {
const exactEnvPaths = [
".env",
".env.local",
".env.development",
".env.production",
".env.staging",
".env.test",
".envrc",
];
return (hasAnyCodexPattern(denied, ["**/.env*"]) ||
hasEveryCodexPattern(denied, exactEnvPaths.map((pattern) => `**/${pattern}`)) ||
existingExactPathsAreDenied(denied, fs, exactEnvPaths));
}
/** Detect exact root/subtree credential surfaces Codex can express on 0.131+. */
function hasCodexCredentialRootDeny(denied, fs) {
return ([
[".docker/**", "**/.docker/**"],
[".gnupg/**", "**/.gnupg/**"],
[".kube/**", "**/.kube/**"],
].every((patternGroup) => hasAnyCodexPattern(denied, patternGroup)) &&
hasAnyCodexPattern(denied, ["**/credentials*"]) &&
(hasEveryCodexPattern(denied, ["**/.npmrc", "**/.pypirc"]) ||
existingExactPathsAreDenied(denied, fs, [".npmrc", ".pypirc"])) &&
["pem", "key", "pfx"].every((extension) => hasAnyCodexPattern(denied, [`**/*.${extension}`, `*.${extension}`])));
}
//# sourceMappingURL=settings.js.map