main
  1/**
  2 * Utility functions and rule definitions for Guardrails extension
  3 *
  4 * Extracted for testability.
  5 */
  6
  7// ── Command rule types ────────────────────────────────────────
  8
  9export interface CommandRule {
 10	pattern: RegExp;
 11	desc: string;
 12	action: "confirm" | "block";
 13	suggestion?: string;
 14}
 15
 16// ── Dangerous & guarded command patterns ──────────────────────
 17
 18export const commandRules: CommandRule[] = [
 19	// General dangerous commands (confirm)
 20	{ pattern: /\brm\s+(-[^\s]*r|--recursive)/, desc: "recursive delete", action: "confirm" },
 21	{ pattern: /\bsudo\b/, desc: "sudo command", action: "confirm" },
 22	{ pattern: /\b(chmod|chown)\b.*777/, desc: "dangerous permissions", action: "confirm" },
 23	{ pattern: /\bmkfs\b/, desc: "filesystem format", action: "confirm" },
 24	{ pattern: /\bdd\b.*\bof=\/dev\//, desc: "raw device write", action: "confirm" },
 25	{ pattern: />\s*\/dev\/sd[a-z]/, desc: "raw device overwrite", action: "confirm" },
 26	{ pattern: /\bkill\s+-9\s+-1\b/, desc: "kill all processes", action: "confirm" },
 27	{ pattern: /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;/, desc: "fork bomb", action: "confirm" },
 28	{ pattern: /\bkubectl\b/, desc: "kubectl command", action: "confirm" },
 29
 30	// Git: block broad staging (use explicit file paths)
 31	{ pattern: /\bgit\s+add\s+(-[Aau]|--all|--update)\b/, desc: "broad git add (use explicit file paths)", action: "block", suggestion: "Use 'git add <file>...' with explicit paths to avoid staging unrelated changes" },
 32
 33	// Nix commands (block: must use make targets)
 34	{ pattern: /\bnixos-rebuild\s+(switch|boot|test)/, desc: "direct nixos-rebuild", action: "block", suggestion: "Use 'make switch', 'make boot', or 'make host/<hostname>/switch' instead" },
 35	{ pattern: /\bhome-manager\s+switch\b/, desc: "direct home-manager switch", action: "block", suggestion: "Use 'make switch' or appropriate make target instead" },
 36
 37	// gh CLI (confirm: use the github tool instead)
 38	{ pattern: /\bgh\s+(pr|issue|run|release|repo)\b/, desc: "gh CLI (use the `github` tool instead for structured output and approval gates)", action: "confirm", suggestion: "Use the `github` tool with the appropriate resource/action instead of direct gh CLI" },
 39
 40	// Nix commands (confirm)
 41	{ pattern: /\bnix\s+eval\b/, desc: "nix eval (arbitrary code execution)", action: "confirm" },
 42	{ pattern: /\bnix-build\b/, desc: "nix-build (builds derivations)", action: "confirm" },
 43	{ pattern: /\bnix\s+store\s+(delete|gc|optimise)\b/, desc: "nix store modification", action: "confirm" },
 44	{ pattern: /\bnix-collect-garbage\b/, desc: "nix garbage collection", action: "confirm" },
 45	{ pattern: /\bnix-channel\s+(--add|--remove|--update)\b/, desc: "nix-channel modification", action: "confirm" },
 46	{ pattern: /\bnix\s+flake\s+update\b/, desc: "nix flake update", action: "confirm" },
 47	{ pattern: /\bnix\s+flake\s+lock\b/, desc: "nix flake lock", action: "confirm" },
 48	{ pattern: /\bnix\s+(copy|push)\b/, desc: "nix store transfer", action: "confirm" },
 49	{ pattern: /\bnix\s+(develop|shell)\b/, desc: "nix development shell", action: "confirm" },
 50	{ pattern: /\bnix\s+run\b/, desc: "nix run (execute package)", action: "confirm" },
 51	{ pattern: /\bnix-env\s+-[ie]/, desc: "nix-env package installation", action: "confirm", suggestion: "Consider using home-manager or system configuration instead" },
 52	{ pattern: /\bnix\s+profile\s+(install|remove|upgrade)\b/, desc: "nix profile modification", action: "confirm", suggestion: "Consider using home-manager or system configuration instead" },
 53];
 54
 55// ── Protected path patterns (hard block via bash) ─────────────
 56
 57export const dangerousBashWrites = [
 58	/>\s*\.env/,
 59	/>\s*\.dev\.vars/,
 60	/>\s*.*\.pem/,
 61	/>\s*.*\.key/,
 62	/tee\s+.*\.env/,
 63	/tee\s+.*\.dev\.vars/,
 64	/cp\s+.*\s+\.env/,
 65	/mv\s+.*\s+\.env/,
 66];
 67
 68// ── Protected paths for write/edit tools ──────────────────────
 69
 70export const protectedPaths = [
 71	{ pattern: /\.env($|\.(?!example))/, desc: "environment file" },
 72	{ pattern: /\.dev\.vars($|\.[^/]+$)/, desc: "dev vars file" },
 73	{ pattern: /node_modules\//, desc: "node_modules" },
 74	{ pattern: /^\.git\/|\/\.git\//, desc: "git directory" },
 75	{ pattern: /\.pem$|\.key$/, desc: "private key file" },
 76	{ pattern: /id_rsa|id_ed25519|id_ecdsa/, desc: "SSH key" },
 77	{ pattern: /\.ssh\//, desc: ".ssh directory" },
 78	{ pattern: /secrets?\.(json|ya?ml|toml)$/i, desc: "secrets file" },
 79	{ pattern: /credentials/i, desc: "credentials file" },
 80];
 81
 82export const softProtectedPaths = [
 83	{ pattern: /package-lock\.json$/, desc: "package-lock.json" },
 84	{ pattern: /yarn\.lock$/, desc: "yarn.lock" },
 85	{ pattern: /pnpm-lock\.yaml$/, desc: "pnpm-lock.yaml" },
 86];
 87
 88// ── Helpers ───────────────────────────────────────────────────
 89
 90/** Strip content inside quotes to avoid false positives on e.g. commit messages */
 91export function stripQuotedContent(command: string): string {
 92	return command
 93		.replace(/"(?:[^"\\]|\\.)*"/g, '""')
 94		.replace(/'[^']*'/g, "''");
 95}
 96
 97/**
 98 * Match a command string against the command rules.
 99 * Returns the first matching rule or null.
100 * Apply stripQuotedContent before calling this for best results.
101 */
102export function matchCommandRule(command: string): CommandRule | null {
103	for (const rule of commandRules) {
104		if (rule.pattern.test(command)) {
105			return rule;
106		}
107	}
108	return null;
109}
110
111// ── Approval gate reason builders ─────────────────────────────
112
113/**
114 * Build the block reason when user wants to modify the command.
115 * Tells the LLM to ask the user what to change and retry.
116 */
117export function buildModifyReason(desc: string): string {
118	return `User wants to modify this command (${desc}). Ask the user what they would like to change, then retry with the updated command.`;
119}
120
121/**
122 * Build the block reason when user rejects the command.
123 * Tells the LLM NOT to retry.
124 */
125export function buildRejectReason(desc: string): string {
126	return `User rejected this command (${desc}). Do NOT retry or attempt this command again.`;
127}
128
129/**
130 * Build the block reason when user rejects a file write.
131 * Tells the LLM NOT to retry.
132 */
133export function buildWriteModifyReason(desc: string, filePath: string): string {
134	return `User wants to modify this write to ${desc} (${filePath}). Ask the user what they would like to change, then retry with the updated parameters.`;
135}
136
137/**
138 * Build the block reason when user rejects a file write.
139 * Tells the LLM NOT to retry.
140 */
141export function buildWriteRejectReason(desc: string, filePath: string): string {
142	return `User rejected write to ${desc} (${filePath}). Do NOT retry or attempt this write again.`;
143}