godcrm/backend/services/agent-permissions/critical-denies.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

165 lines
6.8 KiB
JavaScript

/**
* ADR-0053 Phase C1 — code-level CRITICAL_DENIES.
*
* These patterns are immutable: not editable via API, never overridden by any
* DB rule, never bypassed by `--dangerously-skip-permissions`. They guard the
* cases where a single mistaken tool-call could destroy the cluster:
*
* - filesystem nukes (rm -rf /)
* - fork bombs (resource exhaustion)
* - irreversible DB ops (DROP DATABASE)
* - irreversible git ops (force-push to main)
* - PROD restarts (kills the very process the hook runs in)
* - PROD-only writes (`make prod`, edits to dist/ or .env on the PROD host)
* - direct PROD DB access from a dev shell
*
* Matched against either `tool_name` alone (for MCP-style tools) or
* `tool_name:command-or-path` (for Bash / Edit / Write). The hook builds the
* subject string once and runs all patterns against it.
*
* NEVER edit a deny pattern out of this file to "unblock" an agent. If an
* agent legitimately needs to run something here, the answer is a human
* running it, not a deny removal.
*/
// Each entry: { id, subject, pattern (RegExp), reason }.
// `subject` selects what string we test:
// - 'bash' → tool_input.command (Bash tool only)
// - 'path' → tool_input.file_path (Edit/Write)
// - 'tool' → tool_name itself (for catch-all by tool)
export const CRITICAL_DENIES = Object.freeze([
// ── 1. Filesystem nukes ──────────────────────────────────────────────────
{
id: 'fs-rm-rf-root',
subject: 'bash',
// Matches: rm -rf / ; rm -rf /foo ; rm -rf ~ ; rm -rf ~/x ; rm -rf $HOME
// Avoids matching rm -rf /tmp/foo by anchoring `/` to end-of-string or
// requiring it to NOT be followed by `tmp/`, `home/`, etc — too brittle;
// instead allow any `/` target except a clearly-safe subtree.
pattern: /(^|[\s;&|])rm\s+(-[rRfv]+\s+)*(--no-preserve-root\s+)?(\/(\s|$|;|&)|\/(?!tmp\/|var\/log\/|var\/cache\/)[^\s]|\/\*|\/\.|\$HOME($|[\s/;&|])|~($|[\s/;&|]))/,
reason: 'Refused: rm -rf targeting root, $HOME, or ~ — destroys the host.',
},
// ── 2. Fork bombs / resource exhaustion ──────────────────────────────────
{
id: 'fs-fork-bomb',
subject: 'bash',
pattern: /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/,
reason: 'Refused: classic shell fork bomb (:(){ :|:& };:) — exhausts host.',
},
// ── 3. Irreversible DB ops ───────────────────────────────────────────────
{
id: 'db-drop-database',
subject: 'bash',
pattern: /\bDROP\s+DATABASE\b/i,
reason: 'Refused: DROP DATABASE — destroys the godcrm_prod cluster.',
},
{
id: 'db-drop-schema-cascade',
subject: 'bash',
pattern: /\bDROP\s+SCHEMA\s+\S+\s+CASCADE\b/i,
reason: 'Refused: DROP SCHEMA … CASCADE — cluster-wide data loss.',
},
// ── 4. Irreversible git ops on main ──────────────────────────────────────
{
id: 'git-force-push-main',
subject: 'bash',
pattern: /\bgit\s+push\s+([^&|;]*\s)?(--force\b|-f\b|--force-with-lease\b).*\bmain\b/,
reason: 'Refused: force-push to main — rewrites shared history.',
},
{
id: 'git-push-mirror',
subject: 'bash',
pattern: /\bgit\s+push\s+([^&|;]*\s)?--mirror\b/,
reason: 'Refused: git push --mirror — wipes remote refs.',
},
// ── 5. PROD process restarts (kills our own process group) ───────────────
{
id: 'pm2-restart-godcrm-prod',
subject: 'bash',
// Match pm2 restart/reload/stop/delete targeting godcrm (the running process)
pattern: /\bpm2\s+(restart|reload|stop|delete|kill)\b.*\bgodcrm\b/,
reason: 'Refused: pm2 restart/reload/stop/delete of godcrm — kills the worker that spawned this agent.',
},
{
id: 'systemctl-godcrm',
subject: 'bash',
pattern: /\bsystemctl\s+(restart|reload|stop|disable)\b.*\b(godcrm|business-crm|nginx|postgresql)\b/,
reason: 'Refused: systemctl restart of godcrm/nginx/postgres — service-level outage.',
},
// ── 6. PROD deploys & PROD-only writes ───────────────────────────────────
{
id: 'make-prod',
subject: 'bash',
pattern: /\bmake\s+prod\b/,
reason: 'Refused: `make prod` rebuilds + restarts PROD — only humans run this.',
},
{
id: 'edit-env',
subject: 'path',
pattern: /(^|\/)\.env(\.[a-z]+)?$/,
reason: 'Refused: editing .env — secrets/keys belong in Settings → Secrets (vault).',
},
{
id: 'edit-dist',
subject: 'path',
pattern: /(^|\/)dist\//,
reason: 'Refused: editing dist/ — build artifact, regenerated by `npm run build`.',
},
// ── 7. Dependency mutation in deploy posture ─────────────────────────────
{
id: 'npm-install-production',
subject: 'bash',
pattern: /\bnpm\s+(install|i|ci)\b[^&|;]*--production\b/,
reason: 'Refused: `npm install --production` — strips vite/plugin-react needed for build.',
},
// ── 8. Direct PROD DB access (use the API; never bypass with raw psql) ───
{
id: 'psql-prod-host',
subject: 'bash',
// `\b` doesn't match between space and `-` (both non-word), so use \s.
pattern: /(^|[\s;&|])psql\b[^&|;]*(\s-h|--host[= ])\s*(109\.107\.184\.205|crm\.hltrn\.cc)\b/,
reason: 'Refused: direct psql to PROD host — schema/data writes must go through /api/v3.',
},
]);
/**
* Build the test subject for a given (tool_name, tool_input) pair.
* Returns one of `{ bash, path, tool }` keys → string to match.
*
* For tools we don't know how to read, we still expose `tool` so a
* catch-all by tool name (e.g. mcp__claude_ai_*) can deny.
*/
export function buildSubjects(toolName, toolInput) {
const subjects = { tool: String(toolName || '') };
if (!toolInput || typeof toolInput !== 'object') return subjects;
if (typeof toolInput.command === 'string') subjects.bash = toolInput.command;
if (typeof toolInput.file_path === 'string') subjects.path = toolInput.file_path;
return subjects;
}
/**
* Test all CRITICAL_DENIES against a (tool_name, tool_input).
* Returns the first match or null. Order matches the array above.
*
* @param {string} toolName
* @param {object} toolInput
* @returns {{ id: string, reason: string }|null}
*/
export function matchCriticalDeny(toolName, toolInput) {
const subjects = buildSubjects(toolName, toolInput);
for (const rule of CRITICAL_DENIES) {
const subject = subjects[rule.subject];
if (subject == null) continue;
if (rule.pattern.test(subject)) {
return { id: rule.id, reason: rule.reason };
}
}
return null;
}