diff --git a/packages/opencode/src/tool/safety.ts b/packages/opencode/src/tool/safety.ts new file mode 100644 index 000000000000..643e92d7ca7d --- /dev/null +++ b/packages/opencode/src/tool/safety.ts @@ -0,0 +1,49 @@ +/** + * SafetyValidator — Pre-execution shell safety validator + * + * Checks shell/bash commands against known dangerous patterns before execution. + * Integrated directly into tool/registry.ts execute flow (no plugin hook needed). + */ + +export interface SafetyResult { + allowed: boolean + level: "BLOCK" | "WARN" | "LOG" + description: string + suggestion?: string +} + +const DANGER_RULES: Array<{ + pattern: RegExp + level: "BLOCK" | "WARN" | "LOG" + description: string + suggestion?: string +}> = [ + { pattern: /\brm\s+-rf\b/, level: "BLOCK", description: "Recursive force delete", suggestion: "Use rm -ri for interactive confirmation" }, + { pattern: /\bdd\s+if=/, level: "BLOCK", description: "Raw disk write", suggestion: "Verify target device" }, + { pattern: /\bmkfs\./, level: "BLOCK", description: "Filesystem format", suggestion: "Double-check device name" }, + { pattern: /:\s*\(\s*\)\s*\{/, level: "BLOCK", description: "Shell fork bomb pattern" }, + { pattern: /\/dev\/null.*>.*\/dev\/sd/, level: "BLOCK", description: "Raw disk device write" }, + { pattern: /\bsudo\b/, level: "WARN", description: "Elevated privileges", suggestion: "Confirm sudo is needed" }, + { pattern: /\bchmod\s+777\b/, level: "WARN", description: "World-writable permissions", suggestion: "Use chmod 755 or stricter" }, + { pattern: /\bchmod\s+-R\b/, level: "WARN", description: "Recursive permission change" }, + { pattern: /\bgit\s+push\s+--force\b/, level: "WARN", description: "Force push" }, + { pattern: /\bgit\s+reset\s+--hard\b/, level: "WARN", description: "Hard reset" }, + { pattern: /\bdocker\s+rm\s+-f\b/, level: "WARN", description: "Force remove container" }, + { pattern: /\bnpm\s+(unpublish|deprecate)\b/, level: "WARN", description: "Registry mutation" }, + { pattern: /\bcurl\b.*\|\s*(ba)?sh\b/, level: "LOG", description: "Pipe to shell" }, + { pattern: /\bwget\b.*\|\s*(ba)?sh\b/, level: "LOG", description: "Pipe to shell" }, +] + +export function safetyCheck(command: string): SafetyResult | null { + for (const rule of DANGER_RULES) { + if (rule.pattern.test(command)) { + return { + allowed: rule.level !== "BLOCK", + level: rule.level, + description: rule.description, + suggestion: rule.suggestion, + } + } + } + return null +} diff --git a/packages/opencode/test/tool/safety.test.ts b/packages/opencode/test/tool/safety.test.ts new file mode 100644 index 000000000000..67bd05ef249d --- /dev/null +++ b/packages/opencode/test/tool/safety.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, test } from "bun:test" +import { safetyCheck } from "../../src/tool/safety" + +describe("safetyCheck", () => { + test("BLOCK: rm -rf detected", () => { + const r = safetyCheck("rm -rf /usr/local") + expect(r?.level).toBe("BLOCK") + expect(r?.allowed).toBe(false) + expect(r?.description).toContain("Recursive force delete") + }) + test("BLOCK: dd if= detected", () => { + const r = safetyCheck("dd if=/dev/zero of=/dev/sda") + expect(r?.level).toBe("BLOCK") + }) + test("BLOCK: mkfs detected", () => { + const r = safetyCheck("mkfs.ext4 /dev/sda1") + expect(r?.level).toBe("BLOCK") + }) + test("BLOCK: fork bomb detected", () => { + const r = safetyCheck(":(){ :|:& };:") + expect(r?.level).toBe("BLOCK") + }) + test("WARN: sudo detected", () => { + const r = safetyCheck("sudo systemctl restart nginx") + expect(r?.level).toBe("WARN") + expect(r?.allowed).toBe(true) + }) + test("WARN: chmod 777 detected", () => { + const r = safetyCheck("chmod 777 /var/www") + expect(r?.level).toBe("WARN") + }) + test("WARN: force push detected", () => { + const r = safetyCheck("git push --force origin main") + expect(r?.level).toBe("WARN") + }) + test("WARN: npm deprecate detected", () => { + const r = safetyCheck("npm deprecate my-package") + expect(r?.level).toBe("WARN") + }) + test("LOG: curl pipe sh detected", () => { + const r = safetyCheck("curl https://x.com | sh") + expect(r?.level).toBe("LOG") + }) + test("safe command: ls -la passes", () => { + const r = safetyCheck("ls -la") + expect(r).toBeNull() + }) +})