Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
124 lines
3.4 KiB
JavaScript
124 lines
3.4 KiB
JavaScript
/**
|
|
* CommandClassifier - ADR-076: Terminal Command Risk Classification
|
|
*
|
|
* Classifies shell commands into risk levels:
|
|
* - safe: read-only commands (ls, cat, grep, git status)
|
|
* - medium: write commands that are reversible (git commit, npm install, mkdir)
|
|
* - dangerous: destructive/irreversible commands (rm -rf, sudo, git push --force)
|
|
*/
|
|
|
|
import { logger } from '../utils/logger.js';
|
|
|
|
/** @typedef {'safe' | 'medium' | 'dangerous'} RiskLevel */
|
|
|
|
/**
|
|
* Patterns for dangerous commands
|
|
* These require explicit approval before execution
|
|
*/
|
|
const DANGEROUS_PATTERNS = [
|
|
// Destructive file operations
|
|
/\brm\s+(-[a-zA-Z]*r[a-zA-Z]*f|--recursive|--force)/,
|
|
/\brm\s+-rf\b/,
|
|
// Elevated privileges
|
|
/\bsudo\b/,
|
|
/\bsu\s+-?\s*$/,
|
|
/\bsu\s+\w/,
|
|
// Git destructive
|
|
/\bgit\s+push\s+.*--force\b/,
|
|
/\bgit\s+push\s+-f\b/,
|
|
/\bgit\s+reset\s+--hard\b/,
|
|
/\bgit\s+clean\s+-[a-zA-Z]*f/,
|
|
// Database destructive
|
|
/\bDROP\s+(TABLE|DATABASE|SCHEMA)\b/i,
|
|
/\bTRUNCATE\b/i,
|
|
/\bDELETE\s+FROM\s+\w+\s*;?\s*$/i,
|
|
// System control
|
|
/\bsystemctl\s+(restart|stop|disable)\b/,
|
|
/\bservice\s+\w+\s+(restart|stop)\b/,
|
|
/\bkill\s+(-9|-KILL)\b/,
|
|
/\bkillall\b/,
|
|
/\bpkill\b/,
|
|
// Publishing/deploying
|
|
/\bnpm\s+publish\b/,
|
|
/\bdeploy\b/,
|
|
// Permissions
|
|
/\bchmod\s+777\b/,
|
|
/\bchown\s+-R\b/,
|
|
// Shell bombs and pipes to shell
|
|
/:\s*\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;/,
|
|
/\bcurl\b.*\|\s*(bash|sh|zsh)\b/,
|
|
/\bwget\b.*\|\s*(bash|sh|zsh)\b/,
|
|
// Format/wipe
|
|
/\bmkfs\b/,
|
|
/\bdd\s+if=/,
|
|
].filter(Boolean);
|
|
|
|
/**
|
|
* Patterns for medium-risk commands
|
|
* These execute immediately but are logged
|
|
*/
|
|
const MEDIUM_PATTERNS = [
|
|
// Git write operations
|
|
/\bgit\s+(commit|push|merge|rebase|checkout|branch\s+-[dD])\b/,
|
|
/\bgit\s+stash\s+(drop|clear)\b/,
|
|
// Package management
|
|
/\bnpm\s+(install|update|uninstall|ci)\b/,
|
|
/\byarn\s+(add|remove|upgrade)\b/,
|
|
/\bpnpm\s+(install|add|remove|update)\b/,
|
|
// File write operations
|
|
/\bmv\b/,
|
|
/\bcp\s+-r\b/,
|
|
/\bmkdir\b/,
|
|
/\btouch\b/,
|
|
// Text processing that modifies files
|
|
/\bsed\s+-i\b/,
|
|
/\bawk\b.*>>/,
|
|
// File write redirects
|
|
/>\s*[^>]/,
|
|
/>>/,
|
|
// Build operations
|
|
/\bnpm\s+run\s+build\b/,
|
|
/\bnpx\b/,
|
|
];
|
|
|
|
/**
|
|
* Classify a shell command by risk level
|
|
* @param {string} command - The command to classify
|
|
* @returns {{ riskLevel: RiskLevel, matchedPattern: string | null }}
|
|
*/
|
|
export function classifyCommand(command) {
|
|
if (!command || typeof command !== 'string') {
|
|
return { riskLevel: 'safe', matchedPattern: null };
|
|
}
|
|
|
|
const trimmed = command.trim();
|
|
|
|
// Check dangerous first
|
|
for (const pattern of DANGEROUS_PATTERNS) {
|
|
if (pattern && pattern.test(trimmed)) {
|
|
logger.debug({ command: trimmed, pattern: pattern.toString() }, 'Command classified as dangerous');
|
|
return { riskLevel: 'dangerous', matchedPattern: pattern.toString() };
|
|
}
|
|
}
|
|
|
|
// Check medium
|
|
for (const pattern of MEDIUM_PATTERNS) {
|
|
if (pattern.test(trimmed)) {
|
|
logger.debug({ command: trimmed, pattern: pattern.toString() }, 'Command classified as medium');
|
|
return { riskLevel: 'medium', matchedPattern: pattern.toString() };
|
|
}
|
|
}
|
|
|
|
// Default: safe
|
|
return { riskLevel: 'safe', matchedPattern: null };
|
|
}
|
|
|
|
/**
|
|
* Check if a command needs approval
|
|
* @param {string} command
|
|
* @returns {boolean}
|
|
*/
|
|
export function needsApproval(command) {
|
|
const { riskLevel } = classifyCommand(command);
|
|
return riskLevel === 'dangerous';
|
|
}
|