@dreb/coding-agent 2.20.0 → 2.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"forbidden-commands.d.ts","sourceRoot":"","sources":["../../src/core/forbidden-commands.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAkOH;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAgEhG;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAqC5D;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CACjC,OAAO,EAAE,MAAM,EACf,aAAa,CAAC,EAAE,MAAM,EAAE,GACtB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAgB7D","sourcesContent":["/**\n * Forbidden-commands guard — blocks bash commands matching dangerous patterns\n * before they reach the shell.\n *\n * Hardcoded default patterns are ALWAYS active regardless of settings.\n * Users can add additional patterns via settings.forbiddenCommands.\n *\n * Commands are split on shell operators (&&, ||, ;, |, &) and each segment\n * is checked independently. Default patterns are anchored to the start of\n * each segment (^) so they only match commands that *begin with* the dangerous\n * command, not commands that merely *mention* the pattern in string literals\n * or arguments.\n *\n * To avoid false positives from operators inside quoted strings, content\n * within single/double quotes is masked before splitting. To catch subshell\n * wrappers like $(cmd) and (cmd), leading wrapper characters are stripped\n * from each segment before pattern matching.\n */\n\n/** Hardcoded patterns that are always active. Always anchored with ^. */\nconst DEFAULT_FORBIDDEN_PATTERNS: string[] = [\n\t\"^gh pr merge.*--admin\", // bypass branch protection\n\t\"^git push.*(-f\\\\b|--force)\", // force push (includes --force-with-lease)\n\t\"^gh api.*bypass\", // API calls with bypass flag\n\t\"^(?:export\\\\s+)?HUSKY=0\", // bypass pre-commit hooks (anchored with optional export prefix)\n\t\"^git\\\\s+commit.*--no-verify\", // bypass pre-commit hooks via --no-verify flag\n\t\"^(?:export\\\\s+)?SKIP_?VALIDATION=1\", // bypass pre-commit hooks via SKIP_VALIDATION env var\n\t\"^rm\\\\s+.*--no-preserve-root\", // rm with explicit safety override\n\t\"^rm\\\\s+.*\\\\s[\\\"']?/(\\\\*|[\\\\w.-]+/?)?[\\\"']?(\\\\s|$)\", // rm targeting root or top-level dirs (/, /*, /home, /etc)\n\t\"^dd\\\\s+.*of=/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\", // dd writing to block devices\n\t\"^mkfs\", // format filesystem (mkfs.ext4, mkfs.xfs, etc.)\n\t\"^>>?\\\\s*/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\", // redirect to block device (> and >>)\n\t// Sensitive file access — block reading credential files via bash\n\t// Matches bare commands AND absolute-path invocations (/bin/cat, /usr/bin/cat, etc.)\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*(?:~|\\\\.ssh)/id_(?!.*\\\\.pub\\\\b)\", // SSH private keys (not .pub)\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.dreb/secrets/\", // dreb credential store\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.dreb/agent/auth\\\\.json\", // dreb auth storage\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.aws/credentials\", // AWS credentials\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.gnupg/private-keys\", // GPG private keys\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.config/gcloud/credentials\\\\.db\", // GCloud credentials\n];\n\n/**\n * Patterns checked against the full (quote-masked) command string before\n * splitting into segments. These catch dangerous constructs that span\n * shell operators and would be fragmented by the segment splitter.\n *\n * Matched against the masked string so quoted content doesn't trigger\n * false positives (e.g., `echo \":(){ :|:& };:\"` is safe).\n */\nconst FULL_COMMAND_PATTERNS: string[] = [\n\t\":\\\\(\\\\)\\\\s*\\\\{\", // fork bomb :(){ :|:& };:\n];\n\n/**\n * Patterns also checked against content extracted from within quoted strings.\n * Catches commands like `echo \"rm -rf /\"` where the quoted content is a\n * destructive command that could be piped to execution via `| bash`.\n *\n * These are intentionally limited to destructive/dangerous patterns — env var\n * patterns like HUSKY=0 are excluded because they appear legitimately in\n * contexts like `git log --grep=\"HUSKY=0\"`.\n *\n * The fork bomb pattern from FULL_COMMAND_PATTERNS is included here because\n * it also needs to be caught when quoted (e.g., `echo \":(){ :|:& };:\"`).\n */\nconst QUOTED_CONTENT_PATTERNS: string[] = [\n\t\"^rm\\\\s+.*--no-preserve-root\",\n\t\"^rm\\\\s+.*\\\\s/(\\\\*|[\\\\w.-]+/?)?(\\\\s|$)\",\n\t\"^dd\\\\s+.*of=/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\",\n\t\"^mkfs\",\n\t\"^>>?\\\\s*/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\",\n\t\"^gh pr merge.*--admin\",\n\t\"^git push.*(-f\\\\b|--force)\",\n\t\"^gh api.*bypass\",\n\t\"^git\\\\s+commit.*--no-verify\",\n\t\":\\\\(\\\\)\\\\s*\\\\{\", // fork bomb\n\t// Sensitive file access in quoted content\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*(?:~|\\\\.ssh)/id_(?!.*\\\\.pub\\\\b)\",\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.dreb/secrets/\",\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.dreb/agent/auth\\\\.json\",\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.aws/credentials\",\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.gnupg/private-keys\",\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.config/gcloud/credentials\\\\.db\",\n];\n\n/**\n * Mask content inside single and double-quoted strings by replacing\n * characters within quotes with underscores. This prevents shell operators\n * inside quoted strings from causing false splits.\n *\n * Handles escaped quotes (\\\", \\') within strings. Correctly counts\n * consecutive backslashes before a quote — an even count means the quote\n * is real (e.g. `\\\\\"` is escaped-backslash + closing quote).\n */\nfunction maskQuotedContent(command: string): string {\n\tlet result = \"\";\n\tlet inSingle = false;\n\tlet inDouble = false;\n\n\tfor (let i = 0; i < command.length; i++) {\n\t\tconst ch = command[i];\n\n\t\tif (ch === \"'\" && !inDouble) {\n\t\t\t// In bash, single-quoted strings are completely literal — backslashes\n\t\t\t// have no escape function inside single quotes. Always toggle.\n\t\t\tinSingle = !inSingle;\n\t\t\tresult += ch;\n\t\t} else if (ch === '\"' && !inSingle) {\n\t\t\tif (!isEscaped(command, i)) {\n\t\t\t\tinDouble = !inDouble;\n\t\t\t}\n\t\t\tresult += ch;\n\t\t} else if (inSingle || inDouble) {\n\t\t\t// Replace content inside quotes with a safe character\n\t\t\t// that won't match shell operators\n\t\t\tresult += ch === \"\\n\" ? \"\\n\" : \"_\";\n\t\t} else {\n\t\t\tresult += ch;\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Check if the character at position `i` is escaped by counting consecutive\n * trailing backslashes. If the count is odd, the character is escaped.\n * If even (including zero), it is not escaped.\n *\n * e.g. `\\\\\"` → 2 backslashes → even → `\"` is NOT escaped (real quote)\n * `\\\\\\\"` → 3 backslashes → odd → `\"` IS escaped (literal quote)\n */\nfunction isEscaped(str: string, i: number): boolean {\n\tlet count = 0;\n\tlet j = i - 1;\n\twhile (j >= 0 && str[j] === \"\\\\\") {\n\t\tcount++;\n\t\tj--;\n\t}\n\treturn count % 2 === 1;\n}\n\n/**\n * Extract text content from within quoted strings in a segment.\n * Used to catch commands like `echo \"rm -rf /\" | bash` where dangerous\n * content is hidden inside quotes. The normal segment check won't catch\n * this because `echo` (not `rm`) starts the segment. By extracting the\n * quoted content and checking it separately, we block segments that\n * contain forbidden commands in their quoted arguments.\n */\nfunction extractQuotedContent(text: string): string[] {\n\tconst results: string[] = [];\n\tlet inQuote: string | null = null;\n\tlet start = -1;\n\n\tfor (let i = 0; i < text.length; i++) {\n\t\tconst ch = text[i];\n\t\tif ((ch === '\"' || ch === \"'\") && (ch === \"'\" || !isEscaped(text, i))) {\n\t\t\tif (inQuote === null) {\n\t\t\t\tinQuote = ch;\n\t\t\t\tstart = i + 1;\n\t\t\t} else if (ch === inQuote) {\n\t\t\t\tconst content = text.substring(start, i).trim();\n\t\t\t\tif (content.length > 0) {\n\t\t\t\t\tresults.push(content);\n\t\t\t\t}\n\t\t\t\tinQuote = null;\n\t\t\t}\n\t\t}\n\t}\n\treturn results;\n}\n\n/**\n * Split a command string into individual segments on shell operators.\n *\n * Handles: &&, ||, ;, |, & (background), and newlines.\n * Content inside single/double quotes is masked before splitting so that\n * operators inside quoted strings don't cause false splits.\n * Each segment is trimmed of leading whitespace.\n */\nfunction splitCommandSegments(command: string): string[] {\n\t// Mask quoted content to avoid splitting on operators inside strings\n\tconst masked = maskQuotedContent(command);\n\n\t// Split on shell operators: &&, ||, ;, |, &, and newlines\n\tconst splits = masked.split(/\\s*(?:&&|\\|\\||[;&|]|\\n)\\s*/);\n\n\t// Map split positions back to original command segments.\n\t// We split the masked string to find operator positions, but return\n\t// the original (unmasked) segments so pattern matching sees real text.\n\tconst originalSegments: string[] = [];\n\tlet maskedIdx = 0;\n\n\tfor (const part of splits) {\n\t\t// Find the start of this part in the masked string\n\t\tconst startInMasked = masked.indexOf(part, maskedIdx);\n\t\tif (startInMasked === -1) {\n\t\t\t// Fallback: use the part as-is (shouldn't happen)\n\t\t\toriginalSegments.push(command.substring(maskedIdx, maskedIdx + part.length).trim());\n\t\t} else {\n\t\t\toriginalSegments.push(command.substring(startInMasked, startInMasked + part.length).trim());\n\t\t}\n\t\tmaskedIdx = startInMasked + part.length;\n\t}\n\n\treturn originalSegments.filter((s) => s.length > 0);\n}\n\n/**\n * Strip leading subshell/command-substitution wrappers from a segment\n * so that $(cmd), (cmd), and `cmd` are checked against patterns too.\n *\n * Handles both full-segment wrappers ($(cmd)) and inline substitutions\n * (result=$(cmd)) by extracting inner commands.\n */\nfunction stripSubshellWrapper(segment: string): string {\n\t// Strip $(...) wrapper when it's the whole segment\n\tif (/^\\$\\(/.test(segment) && segment.endsWith(\")\")) {\n\t\treturn segment.slice(2, -1).trim();\n\t}\n\t// Strip (...) wrapper (subshell) when it's the whole segment\n\tif (/^\\(/.test(segment) && segment.endsWith(\")\")) {\n\t\treturn segment.slice(1, -1).trim();\n\t}\n\t// Strip backtick wrapper when it's the whole segment\n\tif (/^`/.test(segment) && segment.endsWith(\"`\")) {\n\t\treturn segment.slice(1, -1).trim();\n\t}\n\t// Extract inner command from inline $() or backtick substitutions\n\t// e.g., \"result=$(git push --force)\" → \"git push --force\"\n\tconst inlineMatch = segment.match(/\\$\\(([^)]+)\\)/);\n\tif (inlineMatch) {\n\t\treturn inlineMatch[1].trim();\n\t}\n\tconst backtickMatch = segment.match(/`([^`]+)`/);\n\tif (backtickMatch) {\n\t\treturn backtickMatch[1].trim();\n\t}\n\treturn segment;\n}\n\n/**\n * Check whether a command matches any forbidden pattern.\n *\n * The command is split on shell operators (&&, ||, ;, |) with quoted content\n * masked to avoid false splits. Each segment is then stripped of subshell\n * wrappers ($(...), (...), `...`) and checked against patterns. Default\n * patterns are ^-anchored so they only match commands that start with the\n * dangerous command prefix.\n *\n * @returns The first matching pattern, or `undefined` if the command is allowed.\n */\nexport function isForbiddenCommand(command: string, extraPatterns?: string[]): string | undefined {\n\t// Guard against misconfigured settings (string instead of array)\n\tconst validatedExtras = Array.isArray(extraPatterns) ? extraPatterns : undefined;\n\tconst allPatterns = validatedExtras\n\t\t? [...DEFAULT_FORBIDDEN_PATTERNS, ...validatedExtras]\n\t\t: DEFAULT_FORBIDDEN_PATTERNS;\n\n\t// Pre-split check: match full-command patterns against the quote-masked\n\t// string to catch constructs that span shell operators (e.g., fork bombs).\n\t// Using the masked string prevents false positives from quoted content.\n\tconst masked = maskQuotedContent(command);\n\tfor (const pattern of FULL_COMMAND_PATTERNS) {\n\t\ttry {\n\t\t\tconst re = new RegExp(pattern);\n\t\t\tif (re.test(masked)) {\n\t\t\t\treturn pattern;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Invalid regex — skip\n\t\t}\n\t}\n\n\tconst segments = splitCommandSegments(command);\n\n\t// Combine quoted-content patterns with any user extras for quoted checking\n\tconst allQuotedPatterns = validatedExtras\n\t\t? [...QUOTED_CONTENT_PATTERNS, ...validatedExtras]\n\t\t: QUOTED_CONTENT_PATTERNS;\n\n\tfor (const segment of segments) {\n\t\t// Check both the raw segment and the subshell-unwrapped version\n\t\tconst toCheck = [segment, stripSubshellWrapper(segment)];\n\t\tfor (const text of toCheck) {\n\t\t\tfor (const pattern of allPatterns) {\n\t\t\t\ttry {\n\t\t\t\t\tconst re = new RegExp(pattern);\n\t\t\t\t\tif (re.test(text)) {\n\t\t\t\t\t\treturn pattern;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Invalid regex in user settings — skip it\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Check content within quotes for embedded dangerous commands.\n\t\t// There is no legitimate reason for an agent to output/echo forbidden\n\t\t// commands, and quoted content could be piped to execution via | bash.\n\t\tconst quotedContent = extractQuotedContent(segment);\n\t\tfor (const content of quotedContent) {\n\t\t\tfor (const pattern of allQuotedPatterns) {\n\t\t\t\ttry {\n\t\t\t\t\tconst re = new RegExp(pattern);\n\t\t\t\t\tif (re.test(content)) {\n\t\t\t\t\t\treturn pattern;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Invalid regex — skip\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Extract file paths from a command that executes a script file.\n * Detects: `bash file`, `sh file`, `source file`, `. file`, and input\n * redirects like `bash < file`.\n *\n * Returns an array of file paths (usually 0 or 1). Does not check whether\n * the files exist — the caller handles that.\n *\n * @returns Array of script file paths referenced by the command.\n */\nexport function extractScriptPaths(command: string): string[] {\n\tconst paths: string[] = [];\n\tconst segments = splitCommandSegments(command);\n\n\tfor (const segment of segments) {\n\t\tconst trimmed = segment.trim();\n\n\t\t// bash < file.sh (input redirect) — check before shell exec to avoid\n\t\t// the shell exec regex matching \"<\" as a filename\n\t\tconst redirectMatch = trimmed.match(/^(?:bash|sh|zsh|ksh)(?:\\s+-\\S+)*\\s+<\\s*(\\S+)/);\n\t\tif (redirectMatch?.[1]) {\n\t\t\tpaths.push(redirectMatch[1]);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// bash [flags] file.sh, sh [flags] file.sh\n\t\t// Flags are short options like -x, -e, -ex, etc.\n\t\t// Exclude -c (inline command — handled by quoted content check)\n\t\tif (/^(?:bash|sh|zsh|ksh)\\s+-c\\b/.test(trimmed)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst shellExecMatch = trimmed.match(/^(?:bash|sh|zsh|ksh)\\s+(?:-\\S+\\s+)*(\\S+)/);\n\t\tif (shellExecMatch) {\n\t\t\tconst filePath = shellExecMatch[1];\n\t\t\tif (filePath && !filePath.startsWith(\"-\")) {\n\t\t\t\tpaths.push(filePath);\n\t\t\t}\n\t\t}\n\n\t\t// source file.sh, . file.sh\n\t\tconst sourceMatch = trimmed.match(/^(?:source|\\.)\\s+(\\S+)/);\n\t\tif (sourceMatch?.[1]) {\n\t\t\tpaths.push(sourceMatch[1]);\n\t\t}\n\t}\n\n\treturn [...new Set(paths)]; // deduplicate\n}\n\n/**\n * Check file content line-by-line for forbidden commands.\n * Each non-empty, non-comment line is passed through `isForbiddenCommand`.\n *\n * This is a pure function — the caller is responsible for reading the file\n * and passing the content string.\n *\n * @returns The first match with pattern, line number, and line text, or undefined.\n */\nexport function checkScriptContent(\n\tcontent: string,\n\textraPatterns?: string[],\n): { pattern: string; line: number; text: string } | undefined {\n\tconst lines = content.split(\"\\n\");\n\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst line = lines[i].trim();\n\n\t\t// Skip empty lines and comments\n\t\tif (!line || line.startsWith(\"#\")) continue;\n\n\t\tconst pattern = isForbiddenCommand(line, extraPatterns);\n\t\tif (pattern) {\n\t\t\treturn { pattern, line: i + 1, text: line };\n\t\t}\n\t}\n\n\treturn undefined;\n}\n"]}
1
+ {"version":3,"file":"forbidden-commands.d.ts","sourceRoot":"","sources":["../../src/core/forbidden-commands.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAiSH;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAqEhG;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAqC5D;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CACjC,OAAO,EAAE,MAAM,EACf,aAAa,CAAC,EAAE,MAAM,EAAE,GACtB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAgB7D","sourcesContent":["/**\n * Forbidden-commands guard — blocks bash commands matching dangerous patterns\n * before they reach the shell.\n *\n * Hardcoded default patterns are ALWAYS active regardless of settings.\n * Users can add additional patterns via settings.forbiddenCommands.\n *\n * Commands are split on shell operators (&&, ||, ;, |, &) and each segment\n * is checked independently. Default patterns are anchored to the start of\n * each segment (^) so they only match commands that *begin with* the dangerous\n * command, not commands that merely *mention* the pattern in string literals\n * or arguments.\n *\n * To avoid false positives from operators inside quoted strings, content\n * within single/double quotes is masked before splitting. To catch subshell\n * wrappers like $(cmd) and (cmd), leading wrapper characters are stripped\n * from each segment before pattern matching.\n */\n\n/** Hardcoded patterns that are always active. Always anchored with ^. */\nconst DEFAULT_FORBIDDEN_PATTERNS: string[] = [\n\t\"^(?:/\\\\S+/)?sudo\\\\b\", // privilege escalation — sudo (bare or absolute path)\n\t\"^(?:/\\\\S+/)?doas\\\\b\", // privilege escalation — doas (bare or absolute path)\n\t\"^(?:/\\\\S+/)?su\\\\b\", // privilege escalation — switch user (bare or absolute path)\n\t\"^gh pr merge.*--admin\", // bypass branch protection\n\t\"^git push.*(-f\\\\b|--force)\", // force push (includes --force-with-lease)\n\t\"^gh api.*bypass\", // API calls with bypass flag\n\t\"^(?:export\\\\s+)?HUSKY=0\", // bypass pre-commit hooks (anchored with optional export prefix)\n\t\"^git\\\\s+commit.*--no-verify\", // bypass pre-commit hooks via --no-verify flag\n\t\"^(?:export\\\\s+)?SKIP_?VALIDATION=1\", // bypass pre-commit hooks via SKIP_VALIDATION env var\n\t\"^rm\\\\s+.*--no-preserve-root\", // rm with explicit safety override\n\t\"^rm\\\\s+.*\\\\s[\\\"']?/(\\\\*|[\\\\w.-]+/?)?[\\\"']?(\\\\s|$)\", // rm targeting root or top-level dirs (/, /*, /home, /etc)\n\t\"^dd\\\\s+.*of=/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\", // dd writing to block devices\n\t\"^mkfs\", // format filesystem (mkfs.ext4, mkfs.xfs, etc.)\n\t\"^>>?\\\\s*/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\", // redirect to block device (> and >>)\n\t// Sensitive file access — block reading credential files via bash\n\t// Matches bare commands AND absolute-path invocations (/bin/cat, /usr/bin/cat, etc.)\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*(?:~|\\\\.ssh)/id_(?!.*\\\\.pub\\\\b)\", // SSH private keys (not .pub)\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.dreb/secrets/\", // dreb credential store\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.dreb/agent/auth\\\\.json\", // dreb auth storage\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.aws/credentials\", // AWS credentials\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.gnupg/private-keys\", // GPG private keys\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.config/gcloud/credentials\\\\.db\", // GCloud credentials\n];\n\n/**\n * Patterns checked against the full (quote-masked) command string before\n * splitting into segments. These catch dangerous constructs that span\n * shell operators and would be fragmented by the segment splitter.\n *\n * Matched against the masked string so quoted content doesn't trigger\n * false positives (e.g., `echo \":(){ :|:& };:\"` is safe).\n */\nconst FULL_COMMAND_PATTERNS: string[] = [\n\t\":\\\\(\\\\)\\\\s*\\\\{\", // fork bomb :(){ :|:& };:\n];\n\n/**\n * Patterns also checked against content extracted from within quoted strings.\n * Catches commands like `echo \"rm -rf /\"` where the quoted content is a\n * destructive command that could be piped to execution via `| bash`.\n *\n * These are intentionally limited to destructive/dangerous patterns — env var\n * patterns like HUSKY=0 are excluded because they appear legitimately in\n * contexts like `git log --grep=\"HUSKY=0\"`.\n *\n * The fork bomb pattern from FULL_COMMAND_PATTERNS is included here because\n * it also needs to be caught when quoted (e.g., `echo \":(){ :|:& };:\"`).\n */\nconst QUOTED_CONTENT_PATTERNS: string[] = [\n\t\"^(?:/\\\\S+/)?sudo\\\\b\", // privilege escalation\n\t\"^(?:/\\\\S+/)?doas\\\\b\", // privilege escalation\n\t\"^(?:/\\\\S+/)?su\\\\b\", // privilege escalation\n\t\"^rm\\\\s+.*--no-preserve-root\",\n\t\"^rm\\\\s+.*\\\\s/(\\\\*|[\\\\w.-]+/?)?(\\\\s|$)\",\n\t\"^dd\\\\s+.*of=/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\",\n\t\"^mkfs\",\n\t\"^>>?\\\\s*/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\",\n\t\"^gh pr merge.*--admin\",\n\t\"^git push.*(-f\\\\b|--force)\",\n\t\"^gh api.*bypass\",\n\t\"^git\\\\s+commit.*--no-verify\",\n\t\":\\\\(\\\\)\\\\s*\\\\{\", // fork bomb\n\t// Sensitive file access in quoted content\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*(?:~|\\\\.ssh)/id_(?!.*\\\\.pub\\\\b)\",\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.dreb/secrets/\",\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.dreb/agent/auth\\\\.json\",\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.aws/credentials\",\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.gnupg/private-keys\",\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.config/gcloud/credentials\\\\.db\",\n];\n\n/**\n * Mask content inside single and double-quoted strings by replacing\n * characters within quotes with underscores. This prevents shell operators\n * inside quoted strings from causing false splits.\n *\n * Handles escaped quotes (\\\", \\') within strings. Correctly counts\n * consecutive backslashes before a quote — an even count means the quote\n * is real (e.g. `\\\\\"` is escaped-backslash + closing quote).\n */\nfunction maskQuotedContent(command: string): string {\n\tlet result = \"\";\n\tlet inSingle = false;\n\tlet inDouble = false;\n\n\tfor (let i = 0; i < command.length; i++) {\n\t\tconst ch = command[i];\n\n\t\tif (ch === \"'\" && !inDouble) {\n\t\t\t// In bash, single-quoted strings are completely literal — backslashes\n\t\t\t// have no escape function inside single quotes. Always toggle.\n\t\t\tinSingle = !inSingle;\n\t\t\tresult += ch;\n\t\t} else if (ch === '\"' && !inSingle) {\n\t\t\tif (!isEscaped(command, i)) {\n\t\t\t\tinDouble = !inDouble;\n\t\t\t}\n\t\t\tresult += ch;\n\t\t} else if (inSingle || inDouble) {\n\t\t\t// Replace content inside quotes with a safe character\n\t\t\t// that won't match shell operators\n\t\t\tresult += ch === \"\\n\" ? \"\\n\" : \"_\";\n\t\t} else {\n\t\t\tresult += ch;\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Check if the character at position `i` is escaped by counting consecutive\n * trailing backslashes. If the count is odd, the character is escaped.\n * If even (including zero), it is not escaped.\n *\n * e.g. `\\\\\"` → 2 backslashes → even → `\"` is NOT escaped (real quote)\n * `\\\\\\\"` → 3 backslashes → odd → `\"` IS escaped (literal quote)\n */\nfunction isEscaped(str: string, i: number): boolean {\n\tlet count = 0;\n\tlet j = i - 1;\n\twhile (j >= 0 && str[j] === \"\\\\\") {\n\t\tcount++;\n\t\tj--;\n\t}\n\treturn count % 2 === 1;\n}\n\n/**\n * Extract text content from within quoted strings in a segment.\n * Used to catch commands like `echo \"rm -rf /\" | bash` where dangerous\n * content is hidden inside quotes. The normal segment check won't catch\n * this because `echo` (not `rm`) starts the segment. By extracting the\n * quoted content and checking it separately, we block segments that\n * contain forbidden commands in their quoted arguments.\n */\nfunction extractQuotedContent(text: string): string[] {\n\tconst results: string[] = [];\n\tlet inQuote: string | null = null;\n\tlet start = -1;\n\n\tfor (let i = 0; i < text.length; i++) {\n\t\tconst ch = text[i];\n\t\tif ((ch === '\"' || ch === \"'\") && (ch === \"'\" || !isEscaped(text, i))) {\n\t\t\tif (inQuote === null) {\n\t\t\t\tinQuote = ch;\n\t\t\t\tstart = i + 1;\n\t\t\t} else if (ch === inQuote) {\n\t\t\t\tconst content = text.substring(start, i).trim();\n\t\t\t\tif (content.length > 0) {\n\t\t\t\t\tresults.push(content);\n\t\t\t\t}\n\t\t\t\tinQuote = null;\n\t\t\t}\n\t\t}\n\t}\n\treturn results;\n}\n\n/**\n * Split a command string into individual segments on shell operators.\n *\n * Handles: &&, ||, ;, |, & (background), and newlines.\n * Content inside single/double quotes is masked before splitting so that\n * operators inside quoted strings don't cause false splits.\n * Each segment is trimmed of leading whitespace.\n */\nfunction splitCommandSegments(command: string): string[] {\n\t// Mask quoted content to avoid splitting on operators inside strings\n\tconst masked = maskQuotedContent(command);\n\n\t// Split on shell operators: &&, ||, ;, |, &, and newlines\n\tconst splits = masked.split(/\\s*(?:&&|\\|\\||[;&|]|\\n)\\s*/);\n\n\t// Map split positions back to original command segments.\n\t// We split the masked string to find operator positions, but return\n\t// the original (unmasked) segments so pattern matching sees real text.\n\tconst originalSegments: string[] = [];\n\tlet maskedIdx = 0;\n\n\tfor (const part of splits) {\n\t\t// Find the start of this part in the masked string\n\t\tconst startInMasked = masked.indexOf(part, maskedIdx);\n\t\tif (startInMasked === -1) {\n\t\t\t// Fallback: use the part as-is (shouldn't happen)\n\t\t\toriginalSegments.push(command.substring(maskedIdx, maskedIdx + part.length).trim());\n\t\t} else {\n\t\t\toriginalSegments.push(command.substring(startInMasked, startInMasked + part.length).trim());\n\t\t}\n\t\tmaskedIdx = startInMasked + part.length;\n\t}\n\n\treturn originalSegments.filter((s) => s.length > 0);\n}\n\n/**\n * Strip shell prefix commands that pass through to the underlying command.\n * These are common bypass vectors for start-anchored patterns:\n * - `env sudo ...` (env runs a command with modified environment)\n * - `env -i VAR=value sudo ...` (env with flags/assignments before the command)\n * - `exec sudo ...` (exec replaces the shell process)\n * - `command sudo ...` (command bypasses shell functions/aliases)\n * - `builtin` (run a shell builtin directly)\n * - `\\sudo ...` (backslash escapes aliases but still runs the command)\n *\n * After stripping `env`, also consumes env-style arguments (flags like `-i`\n * and variable assignments like `VAR=value`) that precede the actual command.\n *\n * Strips iteratively to handle stacking (e.g., `env command sudo`).\n */\nfunction stripShellPrefixes(segment: string): string {\n\tlet result = segment;\n\n\t// Strip leading backslash (alias escape)\n\tif (result.startsWith(\"\\\\\")) {\n\t\tresult = result.slice(1);\n\t}\n\n\t// Strip leading absolute path prefix (e.g., /usr/bin/sudo → sudo)\n\tresult = result.replace(/^\\/\\S+\\//, \"\");\n\n\t// Iteratively strip known pass-through prefixes (bare or with remaining path fragments)\n\tconst prefixes = /^(?:env|exec|command|builtin)\\s+/;\n\tlet prev = \"\";\n\twhile (prev !== result) {\n\t\tprev = result;\n\t\tresult = result.replace(prefixes, \"\");\n\t}\n\n\t// After stripping env prefix, consume env-style arguments:\n\t// - Flags starting with `-` (e.g., -i, -u, -0, --)\n\t// - Variable assignments matching IDENTIFIER=... (e.g., VAR=value, PATH=/usr/bin)\n\t// - Bare uppercase identifiers (e.g., PATH as argument to -u flag)\n\tconst envFlag = /^-\\S*\\s+/;\n\tconst envAssignment = /^[A-Za-z_][A-Za-z0-9_]*=\\S*\\s+/;\n\tconst envBareVar = /^[A-Z_][A-Z0-9_]*\\s+/;\n\tlet envPrev = \"\";\n\twhile (envPrev !== result) {\n\t\tenvPrev = result;\n\t\tresult = result.replace(envFlag, \"\");\n\t\tresult = result.replace(envAssignment, \"\");\n\t\tresult = result.replace(envBareVar, \"\");\n\t}\n\n\t// Strip leading backslash again (in case it's after a prefix: `env \\sudo`)\n\tif (result.startsWith(\"\\\\\")) {\n\t\tresult = result.slice(1);\n\t}\n\n\treturn result.trim();\n}\n\n/**\n * Strip leading subshell/command-substitution wrappers from a segment\n * so that $(cmd), (cmd), and `cmd` are checked against patterns too.\n *\n * Handles both full-segment wrappers ($(cmd)) and inline substitutions\n * (result=$(cmd)) by extracting inner commands.\n */\nfunction stripSubshellWrapper(segment: string): string {\n\t// Strip $(...) wrapper when it's the whole segment\n\tif (/^\\$\\(/.test(segment) && segment.endsWith(\")\")) {\n\t\treturn segment.slice(2, -1).trim();\n\t}\n\t// Strip (...) wrapper (subshell) when it's the whole segment\n\tif (/^\\(/.test(segment) && segment.endsWith(\")\")) {\n\t\treturn segment.slice(1, -1).trim();\n\t}\n\t// Strip backtick wrapper when it's the whole segment\n\tif (/^`/.test(segment) && segment.endsWith(\"`\")) {\n\t\treturn segment.slice(1, -1).trim();\n\t}\n\t// Extract inner command from inline $() or backtick substitutions\n\t// e.g., \"result=$(git push --force)\" → \"git push --force\"\n\tconst inlineMatch = segment.match(/\\$\\(([^)]+)\\)/);\n\tif (inlineMatch) {\n\t\treturn inlineMatch[1].trim();\n\t}\n\tconst backtickMatch = segment.match(/`([^`]+)`/);\n\tif (backtickMatch) {\n\t\treturn backtickMatch[1].trim();\n\t}\n\treturn segment;\n}\n\n/**\n * Check whether a command matches any forbidden pattern.\n *\n * The command is split on shell operators (&&, ||, ;, |) with quoted content\n * masked to avoid false splits. Each segment is then stripped of subshell\n * wrappers ($(...), (...), `...`) and checked against patterns. Default\n * patterns are ^-anchored so they only match commands that start with the\n * dangerous command prefix.\n *\n * @returns The first matching pattern, or `undefined` if the command is allowed.\n */\nexport function isForbiddenCommand(command: string, extraPatterns?: string[]): string | undefined {\n\t// Guard against misconfigured settings (string instead of array)\n\tconst validatedExtras = Array.isArray(extraPatterns) ? extraPatterns : undefined;\n\tconst allPatterns = validatedExtras\n\t\t? [...DEFAULT_FORBIDDEN_PATTERNS, ...validatedExtras]\n\t\t: DEFAULT_FORBIDDEN_PATTERNS;\n\n\t// Pre-split check: match full-command patterns against the quote-masked\n\t// string to catch constructs that span shell operators (e.g., fork bombs).\n\t// Using the masked string prevents false positives from quoted content.\n\tconst masked = maskQuotedContent(command);\n\tfor (const pattern of FULL_COMMAND_PATTERNS) {\n\t\ttry {\n\t\t\tconst re = new RegExp(pattern);\n\t\t\tif (re.test(masked)) {\n\t\t\t\treturn pattern;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Invalid regex — skip\n\t\t}\n\t}\n\n\tconst segments = splitCommandSegments(command);\n\n\t// Combine quoted-content patterns with any user extras for quoted checking\n\tconst allQuotedPatterns = validatedExtras\n\t\t? [...QUOTED_CONTENT_PATTERNS, ...validatedExtras]\n\t\t: QUOTED_CONTENT_PATTERNS;\n\n\tfor (const segment of segments) {\n\t\t// Check the segment after various normalizations:\n\t\t// - Raw segment (e.g., \"sudo apt install\")\n\t\t// - Subshell-unwrapped (e.g., \"$(sudo ...)\" → \"sudo ...\")\n\t\t// - Shell-prefix-stripped (e.g., \"env sudo ...\" → \"sudo ...\")\n\t\t// - Both combined (e.g., \"$(env sudo ...)\" → \"sudo ...\")\n\t\tconst unwrapped = stripSubshellWrapper(segment);\n\t\tconst toCheck = new Set([segment, unwrapped, stripShellPrefixes(segment), stripShellPrefixes(unwrapped)]);\n\t\tfor (const text of toCheck) {\n\t\t\tfor (const pattern of allPatterns) {\n\t\t\t\ttry {\n\t\t\t\t\tconst re = new RegExp(pattern);\n\t\t\t\t\tif (re.test(text)) {\n\t\t\t\t\t\treturn pattern;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Invalid regex in user settings — skip it\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Check content within quotes for embedded dangerous commands.\n\t\t// There is no legitimate reason for an agent to output/echo forbidden\n\t\t// commands, and quoted content could be piped to execution via | bash.\n\t\tconst quotedContent = extractQuotedContent(segment);\n\t\tfor (const content of quotedContent) {\n\t\t\tfor (const pattern of allQuotedPatterns) {\n\t\t\t\ttry {\n\t\t\t\t\tconst re = new RegExp(pattern);\n\t\t\t\t\tif (re.test(content)) {\n\t\t\t\t\t\treturn pattern;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Invalid regex — skip\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Extract file paths from a command that executes a script file.\n * Detects: `bash file`, `sh file`, `source file`, `. file`, and input\n * redirects like `bash < file`.\n *\n * Returns an array of file paths (usually 0 or 1). Does not check whether\n * the files exist — the caller handles that.\n *\n * @returns Array of script file paths referenced by the command.\n */\nexport function extractScriptPaths(command: string): string[] {\n\tconst paths: string[] = [];\n\tconst segments = splitCommandSegments(command);\n\n\tfor (const segment of segments) {\n\t\tconst trimmed = segment.trim();\n\n\t\t// bash < file.sh (input redirect) — check before shell exec to avoid\n\t\t// the shell exec regex matching \"<\" as a filename\n\t\tconst redirectMatch = trimmed.match(/^(?:bash|sh|zsh|ksh)(?:\\s+-\\S+)*\\s+<\\s*(\\S+)/);\n\t\tif (redirectMatch?.[1]) {\n\t\t\tpaths.push(redirectMatch[1]);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// bash [flags] file.sh, sh [flags] file.sh\n\t\t// Flags are short options like -x, -e, -ex, etc.\n\t\t// Exclude -c (inline command — handled by quoted content check)\n\t\tif (/^(?:bash|sh|zsh|ksh)\\s+-c\\b/.test(trimmed)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst shellExecMatch = trimmed.match(/^(?:bash|sh|zsh|ksh)\\s+(?:-\\S+\\s+)*(\\S+)/);\n\t\tif (shellExecMatch) {\n\t\t\tconst filePath = shellExecMatch[1];\n\t\t\tif (filePath && !filePath.startsWith(\"-\")) {\n\t\t\t\tpaths.push(filePath);\n\t\t\t}\n\t\t}\n\n\t\t// source file.sh, . file.sh\n\t\tconst sourceMatch = trimmed.match(/^(?:source|\\.)\\s+(\\S+)/);\n\t\tif (sourceMatch?.[1]) {\n\t\t\tpaths.push(sourceMatch[1]);\n\t\t}\n\t}\n\n\treturn [...new Set(paths)]; // deduplicate\n}\n\n/**\n * Check file content line-by-line for forbidden commands.\n * Each non-empty, non-comment line is passed through `isForbiddenCommand`.\n *\n * This is a pure function — the caller is responsible for reading the file\n * and passing the content string.\n *\n * @returns The first match with pattern, line number, and line text, or undefined.\n */\nexport function checkScriptContent(\n\tcontent: string,\n\textraPatterns?: string[],\n): { pattern: string; line: number; text: string } | undefined {\n\tconst lines = content.split(\"\\n\");\n\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst line = lines[i].trim();\n\n\t\t// Skip empty lines and comments\n\t\tif (!line || line.startsWith(\"#\")) continue;\n\n\t\tconst pattern = isForbiddenCommand(line, extraPatterns);\n\t\tif (pattern) {\n\t\t\treturn { pattern, line: i + 1, text: line };\n\t\t}\n\t}\n\n\treturn undefined;\n}\n"]}
@@ -18,6 +18,9 @@
18
18
  */
19
19
  /** Hardcoded patterns that are always active. Always anchored with ^. */
20
20
  const DEFAULT_FORBIDDEN_PATTERNS = [
21
+ "^(?:/\\S+/)?sudo\\b", // privilege escalation — sudo (bare or absolute path)
22
+ "^(?:/\\S+/)?doas\\b", // privilege escalation — doas (bare or absolute path)
23
+ "^(?:/\\S+/)?su\\b", // privilege escalation — switch user (bare or absolute path)
21
24
  "^gh pr merge.*--admin", // bypass branch protection
22
25
  "^git push.*(-f\\b|--force)", // force push (includes --force-with-lease)
23
26
  "^gh api.*bypass", // API calls with bypass flag
@@ -62,6 +65,9 @@ const FULL_COMMAND_PATTERNS = [
62
65
  * it also needs to be caught when quoted (e.g., `echo ":(){ :|:& };:"`).
63
66
  */
64
67
  const QUOTED_CONTENT_PATTERNS = [
68
+ "^(?:/\\S+/)?sudo\\b", // privilege escalation
69
+ "^(?:/\\S+/)?doas\\b", // privilege escalation
70
+ "^(?:/\\S+/)?su\\b", // privilege escalation
65
71
  "^rm\\s+.*--no-preserve-root",
66
72
  "^rm\\s+.*\\s/(\\*|[\\w.-]+/?)?(\\s|$)",
67
73
  "^dd\\s+.*of=/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)",
@@ -197,6 +203,56 @@ function splitCommandSegments(command) {
197
203
  }
198
204
  return originalSegments.filter((s) => s.length > 0);
199
205
  }
206
+ /**
207
+ * Strip shell prefix commands that pass through to the underlying command.
208
+ * These are common bypass vectors for start-anchored patterns:
209
+ * - `env sudo ...` (env runs a command with modified environment)
210
+ * - `env -i VAR=value sudo ...` (env with flags/assignments before the command)
211
+ * - `exec sudo ...` (exec replaces the shell process)
212
+ * - `command sudo ...` (command bypasses shell functions/aliases)
213
+ * - `builtin` (run a shell builtin directly)
214
+ * - `\sudo ...` (backslash escapes aliases but still runs the command)
215
+ *
216
+ * After stripping `env`, also consumes env-style arguments (flags like `-i`
217
+ * and variable assignments like `VAR=value`) that precede the actual command.
218
+ *
219
+ * Strips iteratively to handle stacking (e.g., `env command sudo`).
220
+ */
221
+ function stripShellPrefixes(segment) {
222
+ let result = segment;
223
+ // Strip leading backslash (alias escape)
224
+ if (result.startsWith("\\")) {
225
+ result = result.slice(1);
226
+ }
227
+ // Strip leading absolute path prefix (e.g., /usr/bin/sudo → sudo)
228
+ result = result.replace(/^\/\S+\//, "");
229
+ // Iteratively strip known pass-through prefixes (bare or with remaining path fragments)
230
+ const prefixes = /^(?:env|exec|command|builtin)\s+/;
231
+ let prev = "";
232
+ while (prev !== result) {
233
+ prev = result;
234
+ result = result.replace(prefixes, "");
235
+ }
236
+ // After stripping env prefix, consume env-style arguments:
237
+ // - Flags starting with `-` (e.g., -i, -u, -0, --)
238
+ // - Variable assignments matching IDENTIFIER=... (e.g., VAR=value, PATH=/usr/bin)
239
+ // - Bare uppercase identifiers (e.g., PATH as argument to -u flag)
240
+ const envFlag = /^-\S*\s+/;
241
+ const envAssignment = /^[A-Za-z_][A-Za-z0-9_]*=\S*\s+/;
242
+ const envBareVar = /^[A-Z_][A-Z0-9_]*\s+/;
243
+ let envPrev = "";
244
+ while (envPrev !== result) {
245
+ envPrev = result;
246
+ result = result.replace(envFlag, "");
247
+ result = result.replace(envAssignment, "");
248
+ result = result.replace(envBareVar, "");
249
+ }
250
+ // Strip leading backslash again (in case it's after a prefix: `env \sudo`)
251
+ if (result.startsWith("\\")) {
252
+ result = result.slice(1);
253
+ }
254
+ return result.trim();
255
+ }
200
256
  /**
201
257
  * Strip leading subshell/command-substitution wrappers from a segment
202
258
  * so that $(cmd), (cmd), and `cmd` are checked against patterns too.
@@ -267,8 +323,13 @@ export function isForbiddenCommand(command, extraPatterns) {
267
323
  ? [...QUOTED_CONTENT_PATTERNS, ...validatedExtras]
268
324
  : QUOTED_CONTENT_PATTERNS;
269
325
  for (const segment of segments) {
270
- // Check both the raw segment and the subshell-unwrapped version
271
- const toCheck = [segment, stripSubshellWrapper(segment)];
326
+ // Check the segment after various normalizations:
327
+ // - Raw segment (e.g., "sudo apt install")
328
+ // - Subshell-unwrapped (e.g., "$(sudo ...)" → "sudo ...")
329
+ // - Shell-prefix-stripped (e.g., "env sudo ..." → "sudo ...")
330
+ // - Both combined (e.g., "$(env sudo ...)" → "sudo ...")
331
+ const unwrapped = stripSubshellWrapper(segment);
332
+ const toCheck = new Set([segment, unwrapped, stripShellPrefixes(segment), stripShellPrefixes(unwrapped)]);
272
333
  for (const text of toCheck) {
273
334
  for (const pattern of allPatterns) {
274
335
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"forbidden-commands.js","sourceRoot":"","sources":["../../src/core/forbidden-commands.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Forbidden-commands guard — blocks bash commands matching dangerous patterns\n * before they reach the shell.\n *\n * Hardcoded default patterns are ALWAYS active regardless of settings.\n * Users can add additional patterns via settings.forbiddenCommands.\n *\n * Commands are split on shell operators (&&, ||, ;, |, &) and each segment\n * is checked independently. Default patterns are anchored to the start of\n * each segment (^) so they only match commands that *begin with* the dangerous\n * command, not commands that merely *mention* the pattern in string literals\n * or arguments.\n *\n * To avoid false positives from operators inside quoted strings, content\n * within single/double quotes is masked before splitting. To catch subshell\n * wrappers like $(cmd) and (cmd), leading wrapper characters are stripped\n * from each segment before pattern matching.\n */\n\n/** Hardcoded patterns that are always active. Always anchored with ^. */\nconst DEFAULT_FORBIDDEN_PATTERNS: string[] = [\n\t\"^gh pr merge.*--admin\", // bypass branch protection\n\t\"^git push.*(-f\\\\b|--force)\", // force push (includes --force-with-lease)\n\t\"^gh api.*bypass\", // API calls with bypass flag\n\t\"^(?:export\\\\s+)?HUSKY=0\", // bypass pre-commit hooks (anchored with optional export prefix)\n\t\"^git\\\\s+commit.*--no-verify\", // bypass pre-commit hooks via --no-verify flag\n\t\"^(?:export\\\\s+)?SKIP_?VALIDATION=1\", // bypass pre-commit hooks via SKIP_VALIDATION env var\n\t\"^rm\\\\s+.*--no-preserve-root\", // rm with explicit safety override\n\t\"^rm\\\\s+.*\\\\s[\\\"']?/(\\\\*|[\\\\w.-]+/?)?[\\\"']?(\\\\s|$)\", // rm targeting root or top-level dirs (/, /*, /home, /etc)\n\t\"^dd\\\\s+.*of=/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\", // dd writing to block devices\n\t\"^mkfs\", // format filesystem (mkfs.ext4, mkfs.xfs, etc.)\n\t\"^>>?\\\\s*/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\", // redirect to block device (> and >>)\n\t// Sensitive file access — block reading credential files via bash\n\t// Matches bare commands AND absolute-path invocations (/bin/cat, /usr/bin/cat, etc.)\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*(?:~|\\\\.ssh)/id_(?!.*\\\\.pub\\\\b)\", // SSH private keys (not .pub)\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.dreb/secrets/\", // dreb credential store\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.dreb/agent/auth\\\\.json\", // dreb auth storage\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.aws/credentials\", // AWS credentials\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.gnupg/private-keys\", // GPG private keys\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.config/gcloud/credentials\\\\.db\", // GCloud credentials\n];\n\n/**\n * Patterns checked against the full (quote-masked) command string before\n * splitting into segments. These catch dangerous constructs that span\n * shell operators and would be fragmented by the segment splitter.\n *\n * Matched against the masked string so quoted content doesn't trigger\n * false positives (e.g., `echo \":(){ :|:& };:\"` is safe).\n */\nconst FULL_COMMAND_PATTERNS: string[] = [\n\t\":\\\\(\\\\)\\\\s*\\\\{\", // fork bomb :(){ :|:& };:\n];\n\n/**\n * Patterns also checked against content extracted from within quoted strings.\n * Catches commands like `echo \"rm -rf /\"` where the quoted content is a\n * destructive command that could be piped to execution via `| bash`.\n *\n * These are intentionally limited to destructive/dangerous patterns — env var\n * patterns like HUSKY=0 are excluded because they appear legitimately in\n * contexts like `git log --grep=\"HUSKY=0\"`.\n *\n * The fork bomb pattern from FULL_COMMAND_PATTERNS is included here because\n * it also needs to be caught when quoted (e.g., `echo \":(){ :|:& };:\"`).\n */\nconst QUOTED_CONTENT_PATTERNS: string[] = [\n\t\"^rm\\\\s+.*--no-preserve-root\",\n\t\"^rm\\\\s+.*\\\\s/(\\\\*|[\\\\w.-]+/?)?(\\\\s|$)\",\n\t\"^dd\\\\s+.*of=/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\",\n\t\"^mkfs\",\n\t\"^>>?\\\\s*/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\",\n\t\"^gh pr merge.*--admin\",\n\t\"^git push.*(-f\\\\b|--force)\",\n\t\"^gh api.*bypass\",\n\t\"^git\\\\s+commit.*--no-verify\",\n\t\":\\\\(\\\\)\\\\s*\\\\{\", // fork bomb\n\t// Sensitive file access in quoted content\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*(?:~|\\\\.ssh)/id_(?!.*\\\\.pub\\\\b)\",\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.dreb/secrets/\",\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.dreb/agent/auth\\\\.json\",\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.aws/credentials\",\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.gnupg/private-keys\",\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.config/gcloud/credentials\\\\.db\",\n];\n\n/**\n * Mask content inside single and double-quoted strings by replacing\n * characters within quotes with underscores. This prevents shell operators\n * inside quoted strings from causing false splits.\n *\n * Handles escaped quotes (\\\", \\') within strings. Correctly counts\n * consecutive backslashes before a quote — an even count means the quote\n * is real (e.g. `\\\\\"` is escaped-backslash + closing quote).\n */\nfunction maskQuotedContent(command: string): string {\n\tlet result = \"\";\n\tlet inSingle = false;\n\tlet inDouble = false;\n\n\tfor (let i = 0; i < command.length; i++) {\n\t\tconst ch = command[i];\n\n\t\tif (ch === \"'\" && !inDouble) {\n\t\t\t// In bash, single-quoted strings are completely literal — backslashes\n\t\t\t// have no escape function inside single quotes. Always toggle.\n\t\t\tinSingle = !inSingle;\n\t\t\tresult += ch;\n\t\t} else if (ch === '\"' && !inSingle) {\n\t\t\tif (!isEscaped(command, i)) {\n\t\t\t\tinDouble = !inDouble;\n\t\t\t}\n\t\t\tresult += ch;\n\t\t} else if (inSingle || inDouble) {\n\t\t\t// Replace content inside quotes with a safe character\n\t\t\t// that won't match shell operators\n\t\t\tresult += ch === \"\\n\" ? \"\\n\" : \"_\";\n\t\t} else {\n\t\t\tresult += ch;\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Check if the character at position `i` is escaped by counting consecutive\n * trailing backslashes. If the count is odd, the character is escaped.\n * If even (including zero), it is not escaped.\n *\n * e.g. `\\\\\"` → 2 backslashes → even → `\"` is NOT escaped (real quote)\n * `\\\\\\\"` → 3 backslashes → odd → `\"` IS escaped (literal quote)\n */\nfunction isEscaped(str: string, i: number): boolean {\n\tlet count = 0;\n\tlet j = i - 1;\n\twhile (j >= 0 && str[j] === \"\\\\\") {\n\t\tcount++;\n\t\tj--;\n\t}\n\treturn count % 2 === 1;\n}\n\n/**\n * Extract text content from within quoted strings in a segment.\n * Used to catch commands like `echo \"rm -rf /\" | bash` where dangerous\n * content is hidden inside quotes. The normal segment check won't catch\n * this because `echo` (not `rm`) starts the segment. By extracting the\n * quoted content and checking it separately, we block segments that\n * contain forbidden commands in their quoted arguments.\n */\nfunction extractQuotedContent(text: string): string[] {\n\tconst results: string[] = [];\n\tlet inQuote: string | null = null;\n\tlet start = -1;\n\n\tfor (let i = 0; i < text.length; i++) {\n\t\tconst ch = text[i];\n\t\tif ((ch === '\"' || ch === \"'\") && (ch === \"'\" || !isEscaped(text, i))) {\n\t\t\tif (inQuote === null) {\n\t\t\t\tinQuote = ch;\n\t\t\t\tstart = i + 1;\n\t\t\t} else if (ch === inQuote) {\n\t\t\t\tconst content = text.substring(start, i).trim();\n\t\t\t\tif (content.length > 0) {\n\t\t\t\t\tresults.push(content);\n\t\t\t\t}\n\t\t\t\tinQuote = null;\n\t\t\t}\n\t\t}\n\t}\n\treturn results;\n}\n\n/**\n * Split a command string into individual segments on shell operators.\n *\n * Handles: &&, ||, ;, |, & (background), and newlines.\n * Content inside single/double quotes is masked before splitting so that\n * operators inside quoted strings don't cause false splits.\n * Each segment is trimmed of leading whitespace.\n */\nfunction splitCommandSegments(command: string): string[] {\n\t// Mask quoted content to avoid splitting on operators inside strings\n\tconst masked = maskQuotedContent(command);\n\n\t// Split on shell operators: &&, ||, ;, |, &, and newlines\n\tconst splits = masked.split(/\\s*(?:&&|\\|\\||[;&|]|\\n)\\s*/);\n\n\t// Map split positions back to original command segments.\n\t// We split the masked string to find operator positions, but return\n\t// the original (unmasked) segments so pattern matching sees real text.\n\tconst originalSegments: string[] = [];\n\tlet maskedIdx = 0;\n\n\tfor (const part of splits) {\n\t\t// Find the start of this part in the masked string\n\t\tconst startInMasked = masked.indexOf(part, maskedIdx);\n\t\tif (startInMasked === -1) {\n\t\t\t// Fallback: use the part as-is (shouldn't happen)\n\t\t\toriginalSegments.push(command.substring(maskedIdx, maskedIdx + part.length).trim());\n\t\t} else {\n\t\t\toriginalSegments.push(command.substring(startInMasked, startInMasked + part.length).trim());\n\t\t}\n\t\tmaskedIdx = startInMasked + part.length;\n\t}\n\n\treturn originalSegments.filter((s) => s.length > 0);\n}\n\n/**\n * Strip leading subshell/command-substitution wrappers from a segment\n * so that $(cmd), (cmd), and `cmd` are checked against patterns too.\n *\n * Handles both full-segment wrappers ($(cmd)) and inline substitutions\n * (result=$(cmd)) by extracting inner commands.\n */\nfunction stripSubshellWrapper(segment: string): string {\n\t// Strip $(...) wrapper when it's the whole segment\n\tif (/^\\$\\(/.test(segment) && segment.endsWith(\")\")) {\n\t\treturn segment.slice(2, -1).trim();\n\t}\n\t// Strip (...) wrapper (subshell) when it's the whole segment\n\tif (/^\\(/.test(segment) && segment.endsWith(\")\")) {\n\t\treturn segment.slice(1, -1).trim();\n\t}\n\t// Strip backtick wrapper when it's the whole segment\n\tif (/^`/.test(segment) && segment.endsWith(\"`\")) {\n\t\treturn segment.slice(1, -1).trim();\n\t}\n\t// Extract inner command from inline $() or backtick substitutions\n\t// e.g., \"result=$(git push --force)\" → \"git push --force\"\n\tconst inlineMatch = segment.match(/\\$\\(([^)]+)\\)/);\n\tif (inlineMatch) {\n\t\treturn inlineMatch[1].trim();\n\t}\n\tconst backtickMatch = segment.match(/`([^`]+)`/);\n\tif (backtickMatch) {\n\t\treturn backtickMatch[1].trim();\n\t}\n\treturn segment;\n}\n\n/**\n * Check whether a command matches any forbidden pattern.\n *\n * The command is split on shell operators (&&, ||, ;, |) with quoted content\n * masked to avoid false splits. Each segment is then stripped of subshell\n * wrappers ($(...), (...), `...`) and checked against patterns. Default\n * patterns are ^-anchored so they only match commands that start with the\n * dangerous command prefix.\n *\n * @returns The first matching pattern, or `undefined` if the command is allowed.\n */\nexport function isForbiddenCommand(command: string, extraPatterns?: string[]): string | undefined {\n\t// Guard against misconfigured settings (string instead of array)\n\tconst validatedExtras = Array.isArray(extraPatterns) ? extraPatterns : undefined;\n\tconst allPatterns = validatedExtras\n\t\t? [...DEFAULT_FORBIDDEN_PATTERNS, ...validatedExtras]\n\t\t: DEFAULT_FORBIDDEN_PATTERNS;\n\n\t// Pre-split check: match full-command patterns against the quote-masked\n\t// string to catch constructs that span shell operators (e.g., fork bombs).\n\t// Using the masked string prevents false positives from quoted content.\n\tconst masked = maskQuotedContent(command);\n\tfor (const pattern of FULL_COMMAND_PATTERNS) {\n\t\ttry {\n\t\t\tconst re = new RegExp(pattern);\n\t\t\tif (re.test(masked)) {\n\t\t\t\treturn pattern;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Invalid regex — skip\n\t\t}\n\t}\n\n\tconst segments = splitCommandSegments(command);\n\n\t// Combine quoted-content patterns with any user extras for quoted checking\n\tconst allQuotedPatterns = validatedExtras\n\t\t? [...QUOTED_CONTENT_PATTERNS, ...validatedExtras]\n\t\t: QUOTED_CONTENT_PATTERNS;\n\n\tfor (const segment of segments) {\n\t\t// Check both the raw segment and the subshell-unwrapped version\n\t\tconst toCheck = [segment, stripSubshellWrapper(segment)];\n\t\tfor (const text of toCheck) {\n\t\t\tfor (const pattern of allPatterns) {\n\t\t\t\ttry {\n\t\t\t\t\tconst re = new RegExp(pattern);\n\t\t\t\t\tif (re.test(text)) {\n\t\t\t\t\t\treturn pattern;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Invalid regex in user settings — skip it\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Check content within quotes for embedded dangerous commands.\n\t\t// There is no legitimate reason for an agent to output/echo forbidden\n\t\t// commands, and quoted content could be piped to execution via | bash.\n\t\tconst quotedContent = extractQuotedContent(segment);\n\t\tfor (const content of quotedContent) {\n\t\t\tfor (const pattern of allQuotedPatterns) {\n\t\t\t\ttry {\n\t\t\t\t\tconst re = new RegExp(pattern);\n\t\t\t\t\tif (re.test(content)) {\n\t\t\t\t\t\treturn pattern;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Invalid regex — skip\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Extract file paths from a command that executes a script file.\n * Detects: `bash file`, `sh file`, `source file`, `. file`, and input\n * redirects like `bash < file`.\n *\n * Returns an array of file paths (usually 0 or 1). Does not check whether\n * the files exist — the caller handles that.\n *\n * @returns Array of script file paths referenced by the command.\n */\nexport function extractScriptPaths(command: string): string[] {\n\tconst paths: string[] = [];\n\tconst segments = splitCommandSegments(command);\n\n\tfor (const segment of segments) {\n\t\tconst trimmed = segment.trim();\n\n\t\t// bash < file.sh (input redirect) — check before shell exec to avoid\n\t\t// the shell exec regex matching \"<\" as a filename\n\t\tconst redirectMatch = trimmed.match(/^(?:bash|sh|zsh|ksh)(?:\\s+-\\S+)*\\s+<\\s*(\\S+)/);\n\t\tif (redirectMatch?.[1]) {\n\t\t\tpaths.push(redirectMatch[1]);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// bash [flags] file.sh, sh [flags] file.sh\n\t\t// Flags are short options like -x, -e, -ex, etc.\n\t\t// Exclude -c (inline command — handled by quoted content check)\n\t\tif (/^(?:bash|sh|zsh|ksh)\\s+-c\\b/.test(trimmed)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst shellExecMatch = trimmed.match(/^(?:bash|sh|zsh|ksh)\\s+(?:-\\S+\\s+)*(\\S+)/);\n\t\tif (shellExecMatch) {\n\t\t\tconst filePath = shellExecMatch[1];\n\t\t\tif (filePath && !filePath.startsWith(\"-\")) {\n\t\t\t\tpaths.push(filePath);\n\t\t\t}\n\t\t}\n\n\t\t// source file.sh, . file.sh\n\t\tconst sourceMatch = trimmed.match(/^(?:source|\\.)\\s+(\\S+)/);\n\t\tif (sourceMatch?.[1]) {\n\t\t\tpaths.push(sourceMatch[1]);\n\t\t}\n\t}\n\n\treturn [...new Set(paths)]; // deduplicate\n}\n\n/**\n * Check file content line-by-line for forbidden commands.\n * Each non-empty, non-comment line is passed through `isForbiddenCommand`.\n *\n * This is a pure function — the caller is responsible for reading the file\n * and passing the content string.\n *\n * @returns The first match with pattern, line number, and line text, or undefined.\n */\nexport function checkScriptContent(\n\tcontent: string,\n\textraPatterns?: string[],\n): { pattern: string; line: number; text: string } | undefined {\n\tconst lines = content.split(\"\\n\");\n\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst line = lines[i].trim();\n\n\t\t// Skip empty lines and comments\n\t\tif (!line || line.startsWith(\"#\")) continue;\n\n\t\tconst pattern = isForbiddenCommand(line, extraPatterns);\n\t\tif (pattern) {\n\t\t\treturn { pattern, line: i + 1, text: line };\n\t\t}\n\t}\n\n\treturn undefined;\n}\n"]}
1
+ {"version":3,"file":"forbidden-commands.js","sourceRoot":"","sources":["../../src/core/forbidden-commands.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Forbidden-commands guard — blocks bash commands matching dangerous patterns\n * before they reach the shell.\n *\n * Hardcoded default patterns are ALWAYS active regardless of settings.\n * Users can add additional patterns via settings.forbiddenCommands.\n *\n * Commands are split on shell operators (&&, ||, ;, |, &) and each segment\n * is checked independently. Default patterns are anchored to the start of\n * each segment (^) so they only match commands that *begin with* the dangerous\n * command, not commands that merely *mention* the pattern in string literals\n * or arguments.\n *\n * To avoid false positives from operators inside quoted strings, content\n * within single/double quotes is masked before splitting. To catch subshell\n * wrappers like $(cmd) and (cmd), leading wrapper characters are stripped\n * from each segment before pattern matching.\n */\n\n/** Hardcoded patterns that are always active. Always anchored with ^. */\nconst DEFAULT_FORBIDDEN_PATTERNS: string[] = [\n\t\"^(?:/\\\\S+/)?sudo\\\\b\", // privilege escalation — sudo (bare or absolute path)\n\t\"^(?:/\\\\S+/)?doas\\\\b\", // privilege escalation — doas (bare or absolute path)\n\t\"^(?:/\\\\S+/)?su\\\\b\", // privilege escalation — switch user (bare or absolute path)\n\t\"^gh pr merge.*--admin\", // bypass branch protection\n\t\"^git push.*(-f\\\\b|--force)\", // force push (includes --force-with-lease)\n\t\"^gh api.*bypass\", // API calls with bypass flag\n\t\"^(?:export\\\\s+)?HUSKY=0\", // bypass pre-commit hooks (anchored with optional export prefix)\n\t\"^git\\\\s+commit.*--no-verify\", // bypass pre-commit hooks via --no-verify flag\n\t\"^(?:export\\\\s+)?SKIP_?VALIDATION=1\", // bypass pre-commit hooks via SKIP_VALIDATION env var\n\t\"^rm\\\\s+.*--no-preserve-root\", // rm with explicit safety override\n\t\"^rm\\\\s+.*\\\\s[\\\"']?/(\\\\*|[\\\\w.-]+/?)?[\\\"']?(\\\\s|$)\", // rm targeting root or top-level dirs (/, /*, /home, /etc)\n\t\"^dd\\\\s+.*of=/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\", // dd writing to block devices\n\t\"^mkfs\", // format filesystem (mkfs.ext4, mkfs.xfs, etc.)\n\t\"^>>?\\\\s*/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\", // redirect to block device (> and >>)\n\t// Sensitive file access — block reading credential files via bash\n\t// Matches bare commands AND absolute-path invocations (/bin/cat, /usr/bin/cat, etc.)\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*(?:~|\\\\.ssh)/id_(?!.*\\\\.pub\\\\b)\", // SSH private keys (not .pub)\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.dreb/secrets/\", // dreb credential store\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.dreb/agent/auth\\\\.json\", // dreb auth storage\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.aws/credentials\", // AWS credentials\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.gnupg/private-keys\", // GPG private keys\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.config/gcloud/credentials\\\\.db\", // GCloud credentials\n];\n\n/**\n * Patterns checked against the full (quote-masked) command string before\n * splitting into segments. These catch dangerous constructs that span\n * shell operators and would be fragmented by the segment splitter.\n *\n * Matched against the masked string so quoted content doesn't trigger\n * false positives (e.g., `echo \":(){ :|:& };:\"` is safe).\n */\nconst FULL_COMMAND_PATTERNS: string[] = [\n\t\":\\\\(\\\\)\\\\s*\\\\{\", // fork bomb :(){ :|:& };:\n];\n\n/**\n * Patterns also checked against content extracted from within quoted strings.\n * Catches commands like `echo \"rm -rf /\"` where the quoted content is a\n * destructive command that could be piped to execution via `| bash`.\n *\n * These are intentionally limited to destructive/dangerous patterns — env var\n * patterns like HUSKY=0 are excluded because they appear legitimately in\n * contexts like `git log --grep=\"HUSKY=0\"`.\n *\n * The fork bomb pattern from FULL_COMMAND_PATTERNS is included here because\n * it also needs to be caught when quoted (e.g., `echo \":(){ :|:& };:\"`).\n */\nconst QUOTED_CONTENT_PATTERNS: string[] = [\n\t\"^(?:/\\\\S+/)?sudo\\\\b\", // privilege escalation\n\t\"^(?:/\\\\S+/)?doas\\\\b\", // privilege escalation\n\t\"^(?:/\\\\S+/)?su\\\\b\", // privilege escalation\n\t\"^rm\\\\s+.*--no-preserve-root\",\n\t\"^rm\\\\s+.*\\\\s/(\\\\*|[\\\\w.-]+/?)?(\\\\s|$)\",\n\t\"^dd\\\\s+.*of=/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\",\n\t\"^mkfs\",\n\t\"^>>?\\\\s*/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\",\n\t\"^gh pr merge.*--admin\",\n\t\"^git push.*(-f\\\\b|--force)\",\n\t\"^gh api.*bypass\",\n\t\"^git\\\\s+commit.*--no-verify\",\n\t\":\\\\(\\\\)\\\\s*\\\\{\", // fork bomb\n\t// Sensitive file access in quoted content\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*(?:~|\\\\.ssh)/id_(?!.*\\\\.pub\\\\b)\",\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.dreb/secrets/\",\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.dreb/agent/auth\\\\.json\",\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.aws/credentials\",\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.gnupg/private-keys\",\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.config/gcloud/credentials\\\\.db\",\n];\n\n/**\n * Mask content inside single and double-quoted strings by replacing\n * characters within quotes with underscores. This prevents shell operators\n * inside quoted strings from causing false splits.\n *\n * Handles escaped quotes (\\\", \\') within strings. Correctly counts\n * consecutive backslashes before a quote — an even count means the quote\n * is real (e.g. `\\\\\"` is escaped-backslash + closing quote).\n */\nfunction maskQuotedContent(command: string): string {\n\tlet result = \"\";\n\tlet inSingle = false;\n\tlet inDouble = false;\n\n\tfor (let i = 0; i < command.length; i++) {\n\t\tconst ch = command[i];\n\n\t\tif (ch === \"'\" && !inDouble) {\n\t\t\t// In bash, single-quoted strings are completely literal — backslashes\n\t\t\t// have no escape function inside single quotes. Always toggle.\n\t\t\tinSingle = !inSingle;\n\t\t\tresult += ch;\n\t\t} else if (ch === '\"' && !inSingle) {\n\t\t\tif (!isEscaped(command, i)) {\n\t\t\t\tinDouble = !inDouble;\n\t\t\t}\n\t\t\tresult += ch;\n\t\t} else if (inSingle || inDouble) {\n\t\t\t// Replace content inside quotes with a safe character\n\t\t\t// that won't match shell operators\n\t\t\tresult += ch === \"\\n\" ? \"\\n\" : \"_\";\n\t\t} else {\n\t\t\tresult += ch;\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Check if the character at position `i` is escaped by counting consecutive\n * trailing backslashes. If the count is odd, the character is escaped.\n * If even (including zero), it is not escaped.\n *\n * e.g. `\\\\\"` → 2 backslashes → even → `\"` is NOT escaped (real quote)\n * `\\\\\\\"` → 3 backslashes → odd → `\"` IS escaped (literal quote)\n */\nfunction isEscaped(str: string, i: number): boolean {\n\tlet count = 0;\n\tlet j = i - 1;\n\twhile (j >= 0 && str[j] === \"\\\\\") {\n\t\tcount++;\n\t\tj--;\n\t}\n\treturn count % 2 === 1;\n}\n\n/**\n * Extract text content from within quoted strings in a segment.\n * Used to catch commands like `echo \"rm -rf /\" | bash` where dangerous\n * content is hidden inside quotes. The normal segment check won't catch\n * this because `echo` (not `rm`) starts the segment. By extracting the\n * quoted content and checking it separately, we block segments that\n * contain forbidden commands in their quoted arguments.\n */\nfunction extractQuotedContent(text: string): string[] {\n\tconst results: string[] = [];\n\tlet inQuote: string | null = null;\n\tlet start = -1;\n\n\tfor (let i = 0; i < text.length; i++) {\n\t\tconst ch = text[i];\n\t\tif ((ch === '\"' || ch === \"'\") && (ch === \"'\" || !isEscaped(text, i))) {\n\t\t\tif (inQuote === null) {\n\t\t\t\tinQuote = ch;\n\t\t\t\tstart = i + 1;\n\t\t\t} else if (ch === inQuote) {\n\t\t\t\tconst content = text.substring(start, i).trim();\n\t\t\t\tif (content.length > 0) {\n\t\t\t\t\tresults.push(content);\n\t\t\t\t}\n\t\t\t\tinQuote = null;\n\t\t\t}\n\t\t}\n\t}\n\treturn results;\n}\n\n/**\n * Split a command string into individual segments on shell operators.\n *\n * Handles: &&, ||, ;, |, & (background), and newlines.\n * Content inside single/double quotes is masked before splitting so that\n * operators inside quoted strings don't cause false splits.\n * Each segment is trimmed of leading whitespace.\n */\nfunction splitCommandSegments(command: string): string[] {\n\t// Mask quoted content to avoid splitting on operators inside strings\n\tconst masked = maskQuotedContent(command);\n\n\t// Split on shell operators: &&, ||, ;, |, &, and newlines\n\tconst splits = masked.split(/\\s*(?:&&|\\|\\||[;&|]|\\n)\\s*/);\n\n\t// Map split positions back to original command segments.\n\t// We split the masked string to find operator positions, but return\n\t// the original (unmasked) segments so pattern matching sees real text.\n\tconst originalSegments: string[] = [];\n\tlet maskedIdx = 0;\n\n\tfor (const part of splits) {\n\t\t// Find the start of this part in the masked string\n\t\tconst startInMasked = masked.indexOf(part, maskedIdx);\n\t\tif (startInMasked === -1) {\n\t\t\t// Fallback: use the part as-is (shouldn't happen)\n\t\t\toriginalSegments.push(command.substring(maskedIdx, maskedIdx + part.length).trim());\n\t\t} else {\n\t\t\toriginalSegments.push(command.substring(startInMasked, startInMasked + part.length).trim());\n\t\t}\n\t\tmaskedIdx = startInMasked + part.length;\n\t}\n\n\treturn originalSegments.filter((s) => s.length > 0);\n}\n\n/**\n * Strip shell prefix commands that pass through to the underlying command.\n * These are common bypass vectors for start-anchored patterns:\n * - `env sudo ...` (env runs a command with modified environment)\n * - `env -i VAR=value sudo ...` (env with flags/assignments before the command)\n * - `exec sudo ...` (exec replaces the shell process)\n * - `command sudo ...` (command bypasses shell functions/aliases)\n * - `builtin` (run a shell builtin directly)\n * - `\\sudo ...` (backslash escapes aliases but still runs the command)\n *\n * After stripping `env`, also consumes env-style arguments (flags like `-i`\n * and variable assignments like `VAR=value`) that precede the actual command.\n *\n * Strips iteratively to handle stacking (e.g., `env command sudo`).\n */\nfunction stripShellPrefixes(segment: string): string {\n\tlet result = segment;\n\n\t// Strip leading backslash (alias escape)\n\tif (result.startsWith(\"\\\\\")) {\n\t\tresult = result.slice(1);\n\t}\n\n\t// Strip leading absolute path prefix (e.g., /usr/bin/sudo → sudo)\n\tresult = result.replace(/^\\/\\S+\\//, \"\");\n\n\t// Iteratively strip known pass-through prefixes (bare or with remaining path fragments)\n\tconst prefixes = /^(?:env|exec|command|builtin)\\s+/;\n\tlet prev = \"\";\n\twhile (prev !== result) {\n\t\tprev = result;\n\t\tresult = result.replace(prefixes, \"\");\n\t}\n\n\t// After stripping env prefix, consume env-style arguments:\n\t// - Flags starting with `-` (e.g., -i, -u, -0, --)\n\t// - Variable assignments matching IDENTIFIER=... (e.g., VAR=value, PATH=/usr/bin)\n\t// - Bare uppercase identifiers (e.g., PATH as argument to -u flag)\n\tconst envFlag = /^-\\S*\\s+/;\n\tconst envAssignment = /^[A-Za-z_][A-Za-z0-9_]*=\\S*\\s+/;\n\tconst envBareVar = /^[A-Z_][A-Z0-9_]*\\s+/;\n\tlet envPrev = \"\";\n\twhile (envPrev !== result) {\n\t\tenvPrev = result;\n\t\tresult = result.replace(envFlag, \"\");\n\t\tresult = result.replace(envAssignment, \"\");\n\t\tresult = result.replace(envBareVar, \"\");\n\t}\n\n\t// Strip leading backslash again (in case it's after a prefix: `env \\sudo`)\n\tif (result.startsWith(\"\\\\\")) {\n\t\tresult = result.slice(1);\n\t}\n\n\treturn result.trim();\n}\n\n/**\n * Strip leading subshell/command-substitution wrappers from a segment\n * so that $(cmd), (cmd), and `cmd` are checked against patterns too.\n *\n * Handles both full-segment wrappers ($(cmd)) and inline substitutions\n * (result=$(cmd)) by extracting inner commands.\n */\nfunction stripSubshellWrapper(segment: string): string {\n\t// Strip $(...) wrapper when it's the whole segment\n\tif (/^\\$\\(/.test(segment) && segment.endsWith(\")\")) {\n\t\treturn segment.slice(2, -1).trim();\n\t}\n\t// Strip (...) wrapper (subshell) when it's the whole segment\n\tif (/^\\(/.test(segment) && segment.endsWith(\")\")) {\n\t\treturn segment.slice(1, -1).trim();\n\t}\n\t// Strip backtick wrapper when it's the whole segment\n\tif (/^`/.test(segment) && segment.endsWith(\"`\")) {\n\t\treturn segment.slice(1, -1).trim();\n\t}\n\t// Extract inner command from inline $() or backtick substitutions\n\t// e.g., \"result=$(git push --force)\" → \"git push --force\"\n\tconst inlineMatch = segment.match(/\\$\\(([^)]+)\\)/);\n\tif (inlineMatch) {\n\t\treturn inlineMatch[1].trim();\n\t}\n\tconst backtickMatch = segment.match(/`([^`]+)`/);\n\tif (backtickMatch) {\n\t\treturn backtickMatch[1].trim();\n\t}\n\treturn segment;\n}\n\n/**\n * Check whether a command matches any forbidden pattern.\n *\n * The command is split on shell operators (&&, ||, ;, |) with quoted content\n * masked to avoid false splits. Each segment is then stripped of subshell\n * wrappers ($(...), (...), `...`) and checked against patterns. Default\n * patterns are ^-anchored so they only match commands that start with the\n * dangerous command prefix.\n *\n * @returns The first matching pattern, or `undefined` if the command is allowed.\n */\nexport function isForbiddenCommand(command: string, extraPatterns?: string[]): string | undefined {\n\t// Guard against misconfigured settings (string instead of array)\n\tconst validatedExtras = Array.isArray(extraPatterns) ? extraPatterns : undefined;\n\tconst allPatterns = validatedExtras\n\t\t? [...DEFAULT_FORBIDDEN_PATTERNS, ...validatedExtras]\n\t\t: DEFAULT_FORBIDDEN_PATTERNS;\n\n\t// Pre-split check: match full-command patterns against the quote-masked\n\t// string to catch constructs that span shell operators (e.g., fork bombs).\n\t// Using the masked string prevents false positives from quoted content.\n\tconst masked = maskQuotedContent(command);\n\tfor (const pattern of FULL_COMMAND_PATTERNS) {\n\t\ttry {\n\t\t\tconst re = new RegExp(pattern);\n\t\t\tif (re.test(masked)) {\n\t\t\t\treturn pattern;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Invalid regex — skip\n\t\t}\n\t}\n\n\tconst segments = splitCommandSegments(command);\n\n\t// Combine quoted-content patterns with any user extras for quoted checking\n\tconst allQuotedPatterns = validatedExtras\n\t\t? [...QUOTED_CONTENT_PATTERNS, ...validatedExtras]\n\t\t: QUOTED_CONTENT_PATTERNS;\n\n\tfor (const segment of segments) {\n\t\t// Check the segment after various normalizations:\n\t\t// - Raw segment (e.g., \"sudo apt install\")\n\t\t// - Subshell-unwrapped (e.g., \"$(sudo ...)\" → \"sudo ...\")\n\t\t// - Shell-prefix-stripped (e.g., \"env sudo ...\" → \"sudo ...\")\n\t\t// - Both combined (e.g., \"$(env sudo ...)\" → \"sudo ...\")\n\t\tconst unwrapped = stripSubshellWrapper(segment);\n\t\tconst toCheck = new Set([segment, unwrapped, stripShellPrefixes(segment), stripShellPrefixes(unwrapped)]);\n\t\tfor (const text of toCheck) {\n\t\t\tfor (const pattern of allPatterns) {\n\t\t\t\ttry {\n\t\t\t\t\tconst re = new RegExp(pattern);\n\t\t\t\t\tif (re.test(text)) {\n\t\t\t\t\t\treturn pattern;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Invalid regex in user settings — skip it\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Check content within quotes for embedded dangerous commands.\n\t\t// There is no legitimate reason for an agent to output/echo forbidden\n\t\t// commands, and quoted content could be piped to execution via | bash.\n\t\tconst quotedContent = extractQuotedContent(segment);\n\t\tfor (const content of quotedContent) {\n\t\t\tfor (const pattern of allQuotedPatterns) {\n\t\t\t\ttry {\n\t\t\t\t\tconst re = new RegExp(pattern);\n\t\t\t\t\tif (re.test(content)) {\n\t\t\t\t\t\treturn pattern;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Invalid regex — skip\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Extract file paths from a command that executes a script file.\n * Detects: `bash file`, `sh file`, `source file`, `. file`, and input\n * redirects like `bash < file`.\n *\n * Returns an array of file paths (usually 0 or 1). Does not check whether\n * the files exist — the caller handles that.\n *\n * @returns Array of script file paths referenced by the command.\n */\nexport function extractScriptPaths(command: string): string[] {\n\tconst paths: string[] = [];\n\tconst segments = splitCommandSegments(command);\n\n\tfor (const segment of segments) {\n\t\tconst trimmed = segment.trim();\n\n\t\t// bash < file.sh (input redirect) — check before shell exec to avoid\n\t\t// the shell exec regex matching \"<\" as a filename\n\t\tconst redirectMatch = trimmed.match(/^(?:bash|sh|zsh|ksh)(?:\\s+-\\S+)*\\s+<\\s*(\\S+)/);\n\t\tif (redirectMatch?.[1]) {\n\t\t\tpaths.push(redirectMatch[1]);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// bash [flags] file.sh, sh [flags] file.sh\n\t\t// Flags are short options like -x, -e, -ex, etc.\n\t\t// Exclude -c (inline command — handled by quoted content check)\n\t\tif (/^(?:bash|sh|zsh|ksh)\\s+-c\\b/.test(trimmed)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst shellExecMatch = trimmed.match(/^(?:bash|sh|zsh|ksh)\\s+(?:-\\S+\\s+)*(\\S+)/);\n\t\tif (shellExecMatch) {\n\t\t\tconst filePath = shellExecMatch[1];\n\t\t\tif (filePath && !filePath.startsWith(\"-\")) {\n\t\t\t\tpaths.push(filePath);\n\t\t\t}\n\t\t}\n\n\t\t// source file.sh, . file.sh\n\t\tconst sourceMatch = trimmed.match(/^(?:source|\\.)\\s+(\\S+)/);\n\t\tif (sourceMatch?.[1]) {\n\t\t\tpaths.push(sourceMatch[1]);\n\t\t}\n\t}\n\n\treturn [...new Set(paths)]; // deduplicate\n}\n\n/**\n * Check file content line-by-line for forbidden commands.\n * Each non-empty, non-comment line is passed through `isForbiddenCommand`.\n *\n * This is a pure function — the caller is responsible for reading the file\n * and passing the content string.\n *\n * @returns The first match with pattern, line number, and line text, or undefined.\n */\nexport function checkScriptContent(\n\tcontent: string,\n\textraPatterns?: string[],\n): { pattern: string; line: number; text: string } | undefined {\n\tconst lines = content.split(\"\\n\");\n\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst line = lines[i].trim();\n\n\t\t// Skip empty lines and comments\n\t\tif (!line || line.startsWith(\"#\")) continue;\n\n\t\tconst pattern = isForbiddenCommand(line, extraPatterns);\n\t\tif (pattern) {\n\t\t\treturn { pattern, line: i + 1, text: line };\n\t\t}\n\t}\n\n\treturn undefined;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"system-prompt.d.ts","sourceRoot":"","sources":["../../src/core/system-prompt.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAyB,KAAK,KAAK,EAAE,MAAM,aAAa,CAAC;AAEhE,MAAM,WAAW,wBAAwB;IACxC,+CAA+C;IAC/C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,0DAA0D;IAC1D,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,qFAAqF;IACrF,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,uCAAuC;IACvC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACxD,2CAA2C;IAC3C,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,yBAAyB;IACzB,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC;IACjB,qFAAqF;IACrF,YAAY,CAAC,EAAE,YAAY,CAAC;CAC5B;AAyBD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,EAAE,GAAG,CAAC,EAAE,IAAI,GAAG,MAAM,CAkB9E;AA2ED,kEAAkE;AAClE,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,wBAA6B,GAAG,MAAM,CAuLhF","sourcesContent":["/**\n * System prompt construction and project context loading\n */\n\nimport { getDocsPath, getExamplesPath, getReadmePath } from \"../config.js\";\nimport type { GitRepoState } from \"./git-repo-state.js\";\nimport { getMemoryInstructions } from \"./memory-prompt.js\";\nimport type { MemoryIndexes } from \"./resource-loader.js\";\nimport { formatSkillsForPrompt, type Skill } from \"./skills.js\";\n\nexport interface BuildSystemPromptOptions {\n\t/** Custom system prompt (replaces default). */\n\tcustomPrompt?: string;\n\t/** Tools to include in prompt. Default: [read, bash, edit, write] */\n\tselectedTools?: string[];\n\t/** Optional one-line tool snippets keyed by tool name. */\n\ttoolSnippets?: Record<string, string>;\n\t/** Additional guideline bullets appended to the default system prompt guidelines. */\n\tpromptGuidelines?: string[];\n\t/** Text to append to system prompt. */\n\tappendSystemPrompt?: string;\n\t/** UI type the agent is communicating through (e.g. \"tui\", \"telegram\", \"rpc\"). */\n\tuiType?: string;\n\t/** Working directory. Default: process.cwd() */\n\tcwd?: string;\n\t/** Pre-loaded context files. */\n\tcontextFiles?: Array<{ path: string; content: string }>;\n\t/** Memory indexes (global and project). */\n\tmemoryIndexes?: MemoryIndexes;\n\t/** Pre-loaded skills. */\n\tskills?: Skill[];\n\t/** Git repo state snapshot (branch, dirty count, recent commits, tags, open PRs). */\n\tgitRepoState?: GitRepoState;\n}\n\nfunction formatMemoryScope(sources: readonly import(\"./resource-loader.js\").MemorySource[], heading: string): string {\n\tif (sources.length === 0) return \"\";\n\n\tconst drebSources = sources.filter((s) => s.source === \"dreb\");\n\tconst claudeSources = sources.filter((s) => s.source === \"claude\");\n\n\tlet out = `\\n### ${heading}\\n`;\n\n\tfor (const source of drebSources) {\n\t\tout += `\\n#### dreb memory (${source.dir}/)\\n\\n${source.content}\\n`;\n\t}\n\n\tif (claudeSources.length > 0) {\n\t\tout += `\\n#### Claude Code memory (read-only)\\n`;\n\t\tout += `> **Note:** These memories were written by Claude Code and may reference Claude Code-specific features, tools, or conventions that don't exist in dreb. Treat the content as useful context, but verify any tool names or workflow references.\\n`;\n\t\tfor (const source of claudeSources) {\n\t\t\tout += `\\nSource: ${source.dir}/\\n\\n${source.content}\\n`;\n\t\t}\n\t}\n\n\treturn out;\n}\n\n/**\n * Format a dream last-run ISO timestamp as a human-readable relative age.\n * Returns \"Never\" if timestamp is null.\n * Uses hours for <24h, days otherwise.\n * @param isoTimestamp ISO timestamp string or null\n * @param now Reference date for computing age (default: current time)\n */\nexport function formatDreamAge(isoTimestamp: string | null, now?: Date): string {\n\tif (!isoTimestamp) return \"Never\";\n\n\tconst then = new Date(isoTimestamp);\n\tif (Number.isNaN(then.getTime())) return \"Never\";\n\n\tconst reference = now ?? new Date();\n\tconst diffMs = reference.getTime() - then.getTime();\n\n\t// Future or zero — treat as just now\n\tif (diffMs <= 0) return \"just now\";\n\n\tconst diffHours = Math.floor(diffMs / (1000 * 60 * 60));\n\tif (diffHours < 1) return \"less than an hour ago\";\n\tif (diffHours < 24) return `${diffHours} ${diffHours === 1 ? \"hour\" : \"hours\"} ago`;\n\n\tconst diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));\n\treturn `${diffDays} ${diffDays === 1 ? \"day\" : \"days\"} ago`;\n}\n\nfunction buildMemorySection(memoryIndexes?: MemoryIndexes): string {\n\tif (!memoryIndexes) return \"\";\n\n\t// Always include memory instructions so the agent knows the convention\n\tlet section = `\\n\\n${getMemoryInstructions({ globalMemoryDir: memoryIndexes.globalMemoryDir, projectMemoryDir: memoryIndexes.projectMemoryDir })}`;\n\n\tconst { global: globalSources, project: projectSources } = memoryIndexes;\n\n\t// Append dream last-run age indicator\n\tconst dreamAge = formatDreamAge(memoryIndexes.dreamLastRun);\n\tif (dreamAge === \"Never\") {\n\t\tsection += \"\\n\\nMemory last consolidated: Never\";\n\t} else {\n\t\tconst dateStr = memoryIndexes.dreamLastRun!.slice(0, 10);\n\t\tsection += `\\n\\nMemory last consolidated: ${dateStr} (${dreamAge})`;\n\t}\n\n\t// Append the actual memory indexes if any exist\n\tif (globalSources.length > 0 || projectSources.length > 0) {\n\t\tsection += \"\\n\\n## Current Memory Indexes\\n\";\n\t\tsection += formatMemoryScope(globalSources, \"Global Memory\");\n\t\tsection += formatMemoryScope(projectSources, \"Project Memory\");\n\t}\n\n\treturn section;\n}\n\n/** UI type descriptions for system prompt context */\nconst UI_DESCRIPTIONS: Record<string, string> = {\n\ttui: \"Terminal UI (interactive terminal with rich rendering)\",\n\ttelegram:\n\t\t\"Telegram (mobile messaging app — the user is on their phone so messages may be shorter or have typos, but this doesn't reflect less thought or intent. The user sees tool names and arguments but not tool output/results, so summarize key findings or changes when relevant)\",\n\trpc: \"RPC (programmatic interface — another application is consuming your output)\",\n\tcli: \"CLI (non-interactive command line — output will be printed and the process exits)\",\n\tagent: \"Subagent (running as a child agent — focus on the task, report results concisely)\",\n};\n\n/** Format the UI context section for the system prompt */\nfunction formatUiSection(uiType: string): string {\n\tconst description = UI_DESCRIPTIONS[uiType] || uiType;\n\treturn `\\nUI: ${description}`;\n}\n\n/** Format the git repo state section for the system prompt */\nfunction formatGitStateSection(state: GitRepoState): string {\n\tlet section = \"\\n\\n## Project state (true at session start only)\\n\\n\";\n\tsection += `- Branch: \\`${state.branch}\\`\\n`;\n\tsection += `- Status: ${state.dirtyCount === 0 ? \"clean\" : `${state.dirtyCount} uncommitted ${state.dirtyCount === 1 ? \"change\" : \"changes\"}`}\\n`;\n\n\tif (state.recentCommits.length > 0) {\n\t\tsection += \"- Recent commits:\\n\";\n\t\tfor (const commit of state.recentCommits) {\n\t\t\tsection += ` - \\`${commit.hash} — ${commit.subject}\\`\\n`;\n\t\t}\n\t}\n\n\tif (state.recentTags.length > 0) {\n\t\tsection += \"- Recent releases:\\n\";\n\t\tfor (const tag of state.recentTags) {\n\t\t\tsection += ` - \\`${tag.name}\\` (${tag.date})\\n`;\n\t\t}\n\t}\n\n\tif (state.openPRs.length > 0) {\n\t\tsection += \"- Open PRs on this branch:\\n\";\n\t\tfor (const pr of state.openPRs) {\n\t\t\tsection += ` - PR ${pr.number} — ${pr.title} (${pr.url})\\n`;\n\t\t}\n\t}\n\n\treturn section;\n}\n\n/** Build the system prompt with tools, guidelines, and context */\nexport function buildSystemPrompt(options: BuildSystemPromptOptions = {}): string {\n\tconst {\n\t\tcustomPrompt,\n\t\tselectedTools,\n\t\ttoolSnippets,\n\t\tpromptGuidelines,\n\t\tappendSystemPrompt,\n\t\tcwd,\n\t\tcontextFiles: providedContextFiles,\n\t\tskills: providedSkills,\n\t} = options;\n\tconst resolvedCwd = cwd ?? process.cwd();\n\tconst promptCwd = resolvedCwd.replace(/\\\\/g, \"/\");\n\n\tconst date = new Date().toISOString().slice(0, 10);\n\n\tconst appendSection = appendSystemPrompt ? `\\n\\n${appendSystemPrompt}` : \"\";\n\n\tconst contextFiles = providedContextFiles ?? [];\n\tconst skills = providedSkills ?? [];\n\n\tif (customPrompt) {\n\t\tlet prompt = customPrompt;\n\n\t\tif (appendSection) {\n\t\t\tprompt += appendSection;\n\t\t}\n\n\t\t// Append project context files\n\t\tif (contextFiles.length > 0) {\n\t\t\tprompt += \"\\n\\n# Project Context\\n\\n\";\n\t\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\t\tprompt += `## ${filePath}\\n\\n${content}\\n\\n`;\n\t\t\t}\n\t\t}\n\n\t\t// Append skills section (when skill or read tool is available)\n\t\tconst customPromptHasSkillAccess =\n\t\t\t!selectedTools || selectedTools.includes(\"skill\") || selectedTools.includes(\"read\");\n\t\tif (customPromptHasSkillAccess && skills.length > 0) {\n\t\t\tprompt += formatSkillsForPrompt(skills);\n\t\t}\n\n\t\t// Append memory indexes\n\t\tprompt += buildMemorySection(options.memoryIndexes);\n\n\t\t// Append git repo state\n\t\tif (options.gitRepoState) {\n\t\t\tprompt += formatGitStateSection(options.gitRepoState);\n\t\t}\n\n\t\t// Add date and working directory last\n\t\tprompt += `\\nCurrent date: ${date}`;\n\t\tprompt += `\\nCurrent working directory: ${promptCwd}`;\n\t\tif (options.uiType) {\n\t\t\tprompt += formatUiSection(options.uiType);\n\t\t}\n\n\t\treturn prompt;\n\t}\n\n\t// Get absolute paths to documentation and examples\n\tconst readmePath = getReadmePath();\n\tconst docsPath = getDocsPath();\n\tconst examplesPath = getExamplesPath();\n\n\t// Build tools list based on selected tools.\n\t// A tool appears in Available tools only when the caller provides a one-line snippet.\n\tconst tools = selectedTools || [\n\t\t\"read\",\n\t\t\"bash\",\n\t\t\"edit\",\n\t\t\"write\",\n\t\t\"grep\",\n\t\t\"find\",\n\t\t\"ls\",\n\t\t\"web_search\",\n\t\t\"web_fetch\",\n\t\t\"subagent\",\n\t\t\"wait\",\n\t];\n\tconst visibleTools = tools.filter((name) => !!toolSnippets?.[name]);\n\tconst toolsList =\n\t\tvisibleTools.length > 0 ? visibleTools.map((name) => `- ${name}: ${toolSnippets![name]}`).join(\"\\n\") : \"(none)\";\n\n\t// Build guidelines based on which tools are actually available\n\tconst guidelinesList: string[] = [];\n\tconst guidelinesSet = new Set<string>();\n\tconst addGuideline = (guideline: string): void => {\n\t\tif (guidelinesSet.has(guideline)) {\n\t\t\treturn;\n\t\t}\n\t\tguidelinesSet.add(guideline);\n\t\tguidelinesList.push(guideline);\n\t};\n\n\tconst hasBash = tools.includes(\"bash\");\n\tconst hasGrep = tools.includes(\"grep\");\n\tconst hasFind = tools.includes(\"find\");\n\tconst hasLs = tools.includes(\"ls\");\n\tconst hasRead = tools.includes(\"read\");\n\tconst hasSearch = tools.includes(\"search\");\n\n\t// File exploration guidelines\n\tif (hasBash && !hasGrep && !hasFind && !hasLs) {\n\t\taddGuideline(\"Use bash for file operations like ls, rg, find\");\n\t} else if (hasBash && (hasGrep || hasFind || hasLs)) {\n\t\tif (hasSearch) {\n\t\t\taddGuideline(\n\t\t\t\t\"Start with `search` to explore and understand the codebase. Use grep/find/ls for exact text matches and specific file lookups. Prefer all of these over bash.\",\n\t\t\t);\n\t\t} else {\n\t\t\taddGuideline(\"Prefer grep/find/ls tools over bash for file exploration (faster, respects .gitignore)\");\n\t\t}\n\t}\n\n\tfor (const guideline of promptGuidelines ?? []) {\n\t\tconst normalized = guideline.trim();\n\t\tif (normalized.length > 0) {\n\t\t\taddGuideline(normalized);\n\t\t}\n\t}\n\n\t// Always include these\n\taddGuideline(\"Be concise in your responses\");\n\taddGuideline(\"Show file paths clearly when working with files\");\n\n\tconst guidelines = guidelinesList.map((g) => `- ${g}`).join(\"\\n\");\n\n\tlet prompt = `You are an expert coding assistant operating inside dreb, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.\n\nAvailable tools:\n${toolsList}\n\nIn addition to the tools above, you may have access to other custom tools depending on the project.\n\nGuidelines:\n${guidelines}\n\nDreb documentation (read only when the user asks about dreb itself, its SDK, extensions, themes, skills, or TUI):\n- Main documentation: ${readmePath}\n- Additional docs: ${docsPath}\n- Examples: ${examplesPath} (extensions, custom tools, SDK)\n- When asked about: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), dreb packages (docs/packages.md)\n- When working on dreb topics, read the docs and examples, and follow .md cross-references before implementing\n- Always read dreb .md files completely and follow links to related docs (e.g., tui.md for TUI API details)`;\n\n\tif (appendSection) {\n\t\tprompt += appendSection;\n\t}\n\n\t// Append project context files\n\tif (contextFiles.length > 0) {\n\t\tprompt += \"\\n\\n# Project Context\\n\\n\";\n\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\tprompt += `## ${filePath}\\n\\n${content}\\n\\n`;\n\t\t}\n\t}\n\n\t// Append skills section (when skill or read tool is available)\n\tconst hasSkillAccess = hasRead || tools.includes(\"skill\");\n\tif (hasSkillAccess && skills.length > 0) {\n\t\tprompt += formatSkillsForPrompt(skills);\n\t}\n\n\t// Append memory indexes\n\tprompt += buildMemorySection(options.memoryIndexes);\n\n\t// Append git repo state\n\tif (options.gitRepoState) {\n\t\tprompt += formatGitStateSection(options.gitRepoState);\n\t}\n\n\t// Add date and working directory last\n\tprompt += `\\nCurrent date: ${date}`;\n\tprompt += `\\nCurrent working directory: ${promptCwd}`;\n\tif (options.uiType) {\n\t\tprompt += formatUiSection(options.uiType);\n\t}\n\n\treturn prompt;\n}\n"]}
1
+ {"version":3,"file":"system-prompt.d.ts","sourceRoot":"","sources":["../../src/core/system-prompt.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAyB,KAAK,KAAK,EAAE,MAAM,aAAa,CAAC;AAEhE,MAAM,WAAW,wBAAwB;IACxC,+CAA+C;IAC/C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,0DAA0D;IAC1D,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,qFAAqF;IACrF,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,uCAAuC;IACvC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACxD,2CAA2C;IAC3C,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,yBAAyB;IACzB,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC;IACjB,qFAAqF;IACrF,YAAY,CAAC,EAAE,YAAY,CAAC;CAC5B;AAyBD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,EAAE,GAAG,CAAC,EAAE,IAAI,GAAG,MAAM,CAkB9E;AA0FD,kEAAkE;AAClE,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,wBAA6B,GAAG,MAAM,CA6LhF","sourcesContent":["/**\n * System prompt construction and project context loading\n */\n\nimport { getDocsPath, getExamplesPath, getReadmePath } from \"../config.js\";\nimport type { GitRepoState } from \"./git-repo-state.js\";\nimport { getMemoryInstructions } from \"./memory-prompt.js\";\nimport type { MemoryIndexes } from \"./resource-loader.js\";\nimport { formatSkillsForPrompt, type Skill } from \"./skills.js\";\n\nexport interface BuildSystemPromptOptions {\n\t/** Custom system prompt (replaces default). */\n\tcustomPrompt?: string;\n\t/** Tools to include in prompt. Default: [read, bash, edit, write] */\n\tselectedTools?: string[];\n\t/** Optional one-line tool snippets keyed by tool name. */\n\ttoolSnippets?: Record<string, string>;\n\t/** Additional guideline bullets appended to the default system prompt guidelines. */\n\tpromptGuidelines?: string[];\n\t/** Text to append to system prompt. */\n\tappendSystemPrompt?: string;\n\t/** UI type the agent is communicating through (e.g. \"tui\", \"telegram\", \"rpc\"). */\n\tuiType?: string;\n\t/** Working directory. Default: process.cwd() */\n\tcwd?: string;\n\t/** Pre-loaded context files. */\n\tcontextFiles?: Array<{ path: string; content: string }>;\n\t/** Memory indexes (global and project). */\n\tmemoryIndexes?: MemoryIndexes;\n\t/** Pre-loaded skills. */\n\tskills?: Skill[];\n\t/** Git repo state snapshot (branch, dirty count, recent commits, tags, open PRs). */\n\tgitRepoState?: GitRepoState;\n}\n\nfunction formatMemoryScope(sources: readonly import(\"./resource-loader.js\").MemorySource[], heading: string): string {\n\tif (sources.length === 0) return \"\";\n\n\tconst drebSources = sources.filter((s) => s.source === \"dreb\");\n\tconst claudeSources = sources.filter((s) => s.source === \"claude\");\n\n\tlet out = `\\n### ${heading}\\n`;\n\n\tfor (const source of drebSources) {\n\t\tout += `\\n#### dreb memory (${source.dir}/)\\n\\n${source.content}\\n`;\n\t}\n\n\tif (claudeSources.length > 0) {\n\t\tout += `\\n#### Claude Code memory (read-only)\\n`;\n\t\tout += `> **Note:** These memories were written by Claude Code and may reference Claude Code-specific features, tools, or conventions that don't exist in dreb. Treat the content as useful context, but verify any tool names or workflow references.\\n`;\n\t\tfor (const source of claudeSources) {\n\t\t\tout += `\\nSource: ${source.dir}/\\n\\n${source.content}\\n`;\n\t\t}\n\t}\n\n\treturn out;\n}\n\n/**\n * Format a dream last-run ISO timestamp as a human-readable relative age.\n * Returns \"Never\" if timestamp is null.\n * Uses hours for <24h, days otherwise.\n * @param isoTimestamp ISO timestamp string or null\n * @param now Reference date for computing age (default: current time)\n */\nexport function formatDreamAge(isoTimestamp: string | null, now?: Date): string {\n\tif (!isoTimestamp) return \"Never\";\n\n\tconst then = new Date(isoTimestamp);\n\tif (Number.isNaN(then.getTime())) return \"Never\";\n\n\tconst reference = now ?? new Date();\n\tconst diffMs = reference.getTime() - then.getTime();\n\n\t// Future or zero — treat as just now\n\tif (diffMs <= 0) return \"just now\";\n\n\tconst diffHours = Math.floor(diffMs / (1000 * 60 * 60));\n\tif (diffHours < 1) return \"less than an hour ago\";\n\tif (diffHours < 24) return `${diffHours} ${diffHours === 1 ? \"hour\" : \"hours\"} ago`;\n\n\tconst diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));\n\treturn `${diffDays} ${diffDays === 1 ? \"day\" : \"days\"} ago`;\n}\n\nfunction buildMemorySection(memoryIndexes?: MemoryIndexes): string {\n\tif (!memoryIndexes) return \"\";\n\n\t// Always include memory instructions so the agent knows the convention\n\tlet section = `\\n\\n${getMemoryInstructions({ globalMemoryDir: memoryIndexes.globalMemoryDir, projectMemoryDir: memoryIndexes.projectMemoryDir })}`;\n\n\tconst { global: globalSources, project: projectSources } = memoryIndexes;\n\n\t// Append dream last-run age indicator\n\tconst dreamAge = formatDreamAge(memoryIndexes.dreamLastRun);\n\tif (dreamAge === \"Never\") {\n\t\tsection += \"\\n\\nMemory last consolidated: Never\";\n\t} else {\n\t\tconst dateStr = memoryIndexes.dreamLastRun!.slice(0, 10);\n\t\tsection += `\\n\\nMemory last consolidated: ${dateStr} (${dreamAge})`;\n\t}\n\n\t// Append the actual memory indexes if any exist\n\tif (globalSources.length > 0 || projectSources.length > 0) {\n\t\tsection += \"\\n\\n## Current Memory Indexes\\n\";\n\t\tsection += formatMemoryScope(globalSources, \"Global Memory\");\n\t\tsection += formatMemoryScope(projectSources, \"Project Memory\");\n\t}\n\n\treturn section;\n}\n\n/** Build the root security warning section if running as UID 0 */\nfunction buildRootSecuritySection(): string {\n\tif (process.getuid?.() !== 0) return \"\";\n\n\treturn `\n\n## ⚠️ Security: Running as Root\n\nYou are running as root (UID 0). You have unrestricted system access. This means:\n- **Never** attempt privilege escalation (sudo, doas, su) — you already have full privileges\n- Be extremely conservative with file modifications, package installations, and system commands\n- Prefer the least-destructive approach for every operation\n- If a task could affect system stability, inform the user before proceeding`;\n}\n\n/** UI type descriptions for system prompt context */\nconst UI_DESCRIPTIONS: Record<string, string> = {\n\ttui: \"Terminal UI (interactive terminal with rich rendering)\",\n\ttelegram:\n\t\t\"Telegram (mobile messaging app — the user is on their phone so messages may be shorter or have typos, but this doesn't reflect less thought or intent. The user sees tool names and arguments but not tool output/results, so summarize key findings or changes when relevant)\",\n\trpc: \"RPC (programmatic interface — another application is consuming your output)\",\n\tcli: \"CLI (non-interactive command line — output will be printed and the process exits)\",\n\tagent: \"Subagent (running as a child agent — focus on the task, report results concisely)\",\n};\n\n/** Format the UI context section for the system prompt */\nfunction formatUiSection(uiType: string): string {\n\tconst description = UI_DESCRIPTIONS[uiType] || uiType;\n\treturn `\\nUI: ${description}`;\n}\n\n/** Format the git repo state section for the system prompt */\nfunction formatGitStateSection(state: GitRepoState): string {\n\tlet section = \"\\n\\n## Project state (true at session start only)\\n\\n\";\n\tsection += `- Branch: \\`${state.branch}\\`\\n`;\n\tsection += `- Status: ${state.dirtyCount === 0 ? \"clean\" : `${state.dirtyCount} uncommitted ${state.dirtyCount === 1 ? \"change\" : \"changes\"}`}\\n`;\n\n\tif (state.recentCommits.length > 0) {\n\t\tsection += \"- Recent commits:\\n\";\n\t\tfor (const commit of state.recentCommits) {\n\t\t\tsection += ` - \\`${commit.hash} — ${commit.subject}\\`\\n`;\n\t\t}\n\t}\n\n\tif (state.recentTags.length > 0) {\n\t\tsection += \"- Recent releases:\\n\";\n\t\tfor (const tag of state.recentTags) {\n\t\t\tsection += ` - \\`${tag.name}\\` (${tag.date})\\n`;\n\t\t}\n\t}\n\n\tif (state.openPRs.length > 0) {\n\t\tsection += \"- Open PRs on this branch:\\n\";\n\t\tfor (const pr of state.openPRs) {\n\t\t\tsection += ` - PR ${pr.number} — ${pr.title} (${pr.url})\\n`;\n\t\t}\n\t}\n\n\treturn section;\n}\n\n/** Build the system prompt with tools, guidelines, and context */\nexport function buildSystemPrompt(options: BuildSystemPromptOptions = {}): string {\n\tconst {\n\t\tcustomPrompt,\n\t\tselectedTools,\n\t\ttoolSnippets,\n\t\tpromptGuidelines,\n\t\tappendSystemPrompt,\n\t\tcwd,\n\t\tcontextFiles: providedContextFiles,\n\t\tskills: providedSkills,\n\t} = options;\n\tconst resolvedCwd = cwd ?? process.cwd();\n\tconst promptCwd = resolvedCwd.replace(/\\\\/g, \"/\");\n\n\tconst date = new Date().toISOString().slice(0, 10);\n\n\tconst appendSection = appendSystemPrompt ? `\\n\\n${appendSystemPrompt}` : \"\";\n\n\tconst contextFiles = providedContextFiles ?? [];\n\tconst skills = providedSkills ?? [];\n\n\tif (customPrompt) {\n\t\tlet prompt = customPrompt;\n\n\t\tif (appendSection) {\n\t\t\tprompt += appendSection;\n\t\t}\n\n\t\t// Append project context files\n\t\tif (contextFiles.length > 0) {\n\t\t\tprompt += \"\\n\\n# Project Context\\n\\n\";\n\t\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\t\tprompt += `## ${filePath}\\n\\n${content}\\n\\n`;\n\t\t\t}\n\t\t}\n\n\t\t// Append skills section (when skill or read tool is available)\n\t\tconst customPromptHasSkillAccess =\n\t\t\t!selectedTools || selectedTools.includes(\"skill\") || selectedTools.includes(\"read\");\n\t\tif (customPromptHasSkillAccess && skills.length > 0) {\n\t\t\tprompt += formatSkillsForPrompt(skills);\n\t\t}\n\n\t\t// Append memory indexes\n\t\tprompt += buildMemorySection(options.memoryIndexes);\n\n\t\t// Append git repo state\n\t\tif (options.gitRepoState) {\n\t\t\tprompt += formatGitStateSection(options.gitRepoState);\n\t\t}\n\n\t\t// Append root security warning if running as UID 0\n\t\tprompt += buildRootSecuritySection();\n\n\t\t// Add date and working directory last\n\t\tprompt += `\\nCurrent date: ${date}`;\n\t\tprompt += `\\nCurrent working directory: ${promptCwd}`;\n\t\tif (options.uiType) {\n\t\t\tprompt += formatUiSection(options.uiType);\n\t\t}\n\n\t\treturn prompt;\n\t}\n\n\t// Get absolute paths to documentation and examples\n\tconst readmePath = getReadmePath();\n\tconst docsPath = getDocsPath();\n\tconst examplesPath = getExamplesPath();\n\n\t// Build tools list based on selected tools.\n\t// A tool appears in Available tools only when the caller provides a one-line snippet.\n\tconst tools = selectedTools || [\n\t\t\"read\",\n\t\t\"bash\",\n\t\t\"edit\",\n\t\t\"write\",\n\t\t\"grep\",\n\t\t\"find\",\n\t\t\"ls\",\n\t\t\"web_search\",\n\t\t\"web_fetch\",\n\t\t\"subagent\",\n\t\t\"wait\",\n\t];\n\tconst visibleTools = tools.filter((name) => !!toolSnippets?.[name]);\n\tconst toolsList =\n\t\tvisibleTools.length > 0 ? visibleTools.map((name) => `- ${name}: ${toolSnippets![name]}`).join(\"\\n\") : \"(none)\";\n\n\t// Build guidelines based on which tools are actually available\n\tconst guidelinesList: string[] = [];\n\tconst guidelinesSet = new Set<string>();\n\tconst addGuideline = (guideline: string): void => {\n\t\tif (guidelinesSet.has(guideline)) {\n\t\t\treturn;\n\t\t}\n\t\tguidelinesSet.add(guideline);\n\t\tguidelinesList.push(guideline);\n\t};\n\n\tconst hasBash = tools.includes(\"bash\");\n\tconst hasGrep = tools.includes(\"grep\");\n\tconst hasFind = tools.includes(\"find\");\n\tconst hasLs = tools.includes(\"ls\");\n\tconst hasRead = tools.includes(\"read\");\n\tconst hasSearch = tools.includes(\"search\");\n\n\t// File exploration guidelines\n\tif (hasBash && !hasGrep && !hasFind && !hasLs) {\n\t\taddGuideline(\"Use bash for file operations like ls, rg, find\");\n\t} else if (hasBash && (hasGrep || hasFind || hasLs)) {\n\t\tif (hasSearch) {\n\t\t\taddGuideline(\n\t\t\t\t\"Start with `search` to explore and understand the codebase. Use grep/find/ls for exact text matches and specific file lookups. Prefer all of these over bash.\",\n\t\t\t);\n\t\t} else {\n\t\t\taddGuideline(\"Prefer grep/find/ls tools over bash for file exploration (faster, respects .gitignore)\");\n\t\t}\n\t}\n\n\tfor (const guideline of promptGuidelines ?? []) {\n\t\tconst normalized = guideline.trim();\n\t\tif (normalized.length > 0) {\n\t\t\taddGuideline(normalized);\n\t\t}\n\t}\n\n\t// Always include these\n\taddGuideline(\"Be concise in your responses\");\n\taddGuideline(\"Show file paths clearly when working with files\");\n\n\tconst guidelines = guidelinesList.map((g) => `- ${g}`).join(\"\\n\");\n\n\tlet prompt = `You are an expert coding assistant operating inside dreb, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.\n\nAvailable tools:\n${toolsList}\n\nIn addition to the tools above, you may have access to other custom tools depending on the project.\n\nGuidelines:\n${guidelines}\n\nDreb documentation (read only when the user asks about dreb itself, its SDK, extensions, themes, skills, or TUI):\n- Main documentation: ${readmePath}\n- Additional docs: ${docsPath}\n- Examples: ${examplesPath} (extensions, custom tools, SDK)\n- When asked about: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), dreb packages (docs/packages.md)\n- When working on dreb topics, read the docs and examples, and follow .md cross-references before implementing\n- Always read dreb .md files completely and follow links to related docs (e.g., tui.md for TUI API details)`;\n\n\tif (appendSection) {\n\t\tprompt += appendSection;\n\t}\n\n\t// Append project context files\n\tif (contextFiles.length > 0) {\n\t\tprompt += \"\\n\\n# Project Context\\n\\n\";\n\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\tprompt += `## ${filePath}\\n\\n${content}\\n\\n`;\n\t\t}\n\t}\n\n\t// Append skills section (when skill or read tool is available)\n\tconst hasSkillAccess = hasRead || tools.includes(\"skill\");\n\tif (hasSkillAccess && skills.length > 0) {\n\t\tprompt += formatSkillsForPrompt(skills);\n\t}\n\n\t// Append memory indexes\n\tprompt += buildMemorySection(options.memoryIndexes);\n\n\t// Append git repo state\n\tif (options.gitRepoState) {\n\t\tprompt += formatGitStateSection(options.gitRepoState);\n\t}\n\n\t// Append root security warning if running as UID 0\n\tprompt += buildRootSecuritySection();\n\n\t// Add date and working directory last\n\tprompt += `\\nCurrent date: ${date}`;\n\tprompt += `\\nCurrent working directory: ${promptCwd}`;\n\tif (options.uiType) {\n\t\tprompt += formatUiSection(options.uiType);\n\t}\n\n\treturn prompt;\n}\n"]}
@@ -71,6 +71,20 @@ function buildMemorySection(memoryIndexes) {
71
71
  }
72
72
  return section;
73
73
  }
74
+ /** Build the root security warning section if running as UID 0 */
75
+ function buildRootSecuritySection() {
76
+ if (process.getuid?.() !== 0)
77
+ return "";
78
+ return `
79
+
80
+ ## ⚠️ Security: Running as Root
81
+
82
+ You are running as root (UID 0). You have unrestricted system access. This means:
83
+ - **Never** attempt privilege escalation (sudo, doas, su) — you already have full privileges
84
+ - Be extremely conservative with file modifications, package installations, and system commands
85
+ - Prefer the least-destructive approach for every operation
86
+ - If a task could affect system stability, inform the user before proceeding`;
87
+ }
74
88
  /** UI type descriptions for system prompt context */
75
89
  const UI_DESCRIPTIONS = {
76
90
  tui: "Terminal UI (interactive terminal with rich rendering)",
@@ -142,6 +156,8 @@ export function buildSystemPrompt(options = {}) {
142
156
  if (options.gitRepoState) {
143
157
  prompt += formatGitStateSection(options.gitRepoState);
144
158
  }
159
+ // Append root security warning if running as UID 0
160
+ prompt += buildRootSecuritySection();
145
161
  // Add date and working directory last
146
162
  prompt += `\nCurrent date: ${date}`;
147
163
  prompt += `\nCurrent working directory: ${promptCwd}`;
@@ -248,6 +264,8 @@ Dreb documentation (read only when the user asks about dreb itself, its SDK, ext
248
264
  if (options.gitRepoState) {
249
265
  prompt += formatGitStateSection(options.gitRepoState);
250
266
  }
267
+ // Append root security warning if running as UID 0
268
+ prompt += buildRootSecuritySection();
251
269
  // Add date and working directory last
252
270
  prompt += `\nCurrent date: ${date}`;
253
271
  prompt += `\nCurrent working directory: ${promptCwd}`;
@@ -1 +1 @@
1
- {"version":3,"file":"system-prompt.js","sourceRoot":"","sources":["../../src/core/system-prompt.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * System prompt construction and project context loading\n */\n\nimport { getDocsPath, getExamplesPath, getReadmePath } from \"../config.js\";\nimport type { GitRepoState } from \"./git-repo-state.js\";\nimport { getMemoryInstructions } from \"./memory-prompt.js\";\nimport type { MemoryIndexes } from \"./resource-loader.js\";\nimport { formatSkillsForPrompt, type Skill } from \"./skills.js\";\n\nexport interface BuildSystemPromptOptions {\n\t/** Custom system prompt (replaces default). */\n\tcustomPrompt?: string;\n\t/** Tools to include in prompt. Default: [read, bash, edit, write] */\n\tselectedTools?: string[];\n\t/** Optional one-line tool snippets keyed by tool name. */\n\ttoolSnippets?: Record<string, string>;\n\t/** Additional guideline bullets appended to the default system prompt guidelines. */\n\tpromptGuidelines?: string[];\n\t/** Text to append to system prompt. */\n\tappendSystemPrompt?: string;\n\t/** UI type the agent is communicating through (e.g. \"tui\", \"telegram\", \"rpc\"). */\n\tuiType?: string;\n\t/** Working directory. Default: process.cwd() */\n\tcwd?: string;\n\t/** Pre-loaded context files. */\n\tcontextFiles?: Array<{ path: string; content: string }>;\n\t/** Memory indexes (global and project). */\n\tmemoryIndexes?: MemoryIndexes;\n\t/** Pre-loaded skills. */\n\tskills?: Skill[];\n\t/** Git repo state snapshot (branch, dirty count, recent commits, tags, open PRs). */\n\tgitRepoState?: GitRepoState;\n}\n\nfunction formatMemoryScope(sources: readonly import(\"./resource-loader.js\").MemorySource[], heading: string): string {\n\tif (sources.length === 0) return \"\";\n\n\tconst drebSources = sources.filter((s) => s.source === \"dreb\");\n\tconst claudeSources = sources.filter((s) => s.source === \"claude\");\n\n\tlet out = `\\n### ${heading}\\n`;\n\n\tfor (const source of drebSources) {\n\t\tout += `\\n#### dreb memory (${source.dir}/)\\n\\n${source.content}\\n`;\n\t}\n\n\tif (claudeSources.length > 0) {\n\t\tout += `\\n#### Claude Code memory (read-only)\\n`;\n\t\tout += `> **Note:** These memories were written by Claude Code and may reference Claude Code-specific features, tools, or conventions that don't exist in dreb. Treat the content as useful context, but verify any tool names or workflow references.\\n`;\n\t\tfor (const source of claudeSources) {\n\t\t\tout += `\\nSource: ${source.dir}/\\n\\n${source.content}\\n`;\n\t\t}\n\t}\n\n\treturn out;\n}\n\n/**\n * Format a dream last-run ISO timestamp as a human-readable relative age.\n * Returns \"Never\" if timestamp is null.\n * Uses hours for <24h, days otherwise.\n * @param isoTimestamp ISO timestamp string or null\n * @param now Reference date for computing age (default: current time)\n */\nexport function formatDreamAge(isoTimestamp: string | null, now?: Date): string {\n\tif (!isoTimestamp) return \"Never\";\n\n\tconst then = new Date(isoTimestamp);\n\tif (Number.isNaN(then.getTime())) return \"Never\";\n\n\tconst reference = now ?? new Date();\n\tconst diffMs = reference.getTime() - then.getTime();\n\n\t// Future or zero — treat as just now\n\tif (diffMs <= 0) return \"just now\";\n\n\tconst diffHours = Math.floor(diffMs / (1000 * 60 * 60));\n\tif (diffHours < 1) return \"less than an hour ago\";\n\tif (diffHours < 24) return `${diffHours} ${diffHours === 1 ? \"hour\" : \"hours\"} ago`;\n\n\tconst diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));\n\treturn `${diffDays} ${diffDays === 1 ? \"day\" : \"days\"} ago`;\n}\n\nfunction buildMemorySection(memoryIndexes?: MemoryIndexes): string {\n\tif (!memoryIndexes) return \"\";\n\n\t// Always include memory instructions so the agent knows the convention\n\tlet section = `\\n\\n${getMemoryInstructions({ globalMemoryDir: memoryIndexes.globalMemoryDir, projectMemoryDir: memoryIndexes.projectMemoryDir })}`;\n\n\tconst { global: globalSources, project: projectSources } = memoryIndexes;\n\n\t// Append dream last-run age indicator\n\tconst dreamAge = formatDreamAge(memoryIndexes.dreamLastRun);\n\tif (dreamAge === \"Never\") {\n\t\tsection += \"\\n\\nMemory last consolidated: Never\";\n\t} else {\n\t\tconst dateStr = memoryIndexes.dreamLastRun!.slice(0, 10);\n\t\tsection += `\\n\\nMemory last consolidated: ${dateStr} (${dreamAge})`;\n\t}\n\n\t// Append the actual memory indexes if any exist\n\tif (globalSources.length > 0 || projectSources.length > 0) {\n\t\tsection += \"\\n\\n## Current Memory Indexes\\n\";\n\t\tsection += formatMemoryScope(globalSources, \"Global Memory\");\n\t\tsection += formatMemoryScope(projectSources, \"Project Memory\");\n\t}\n\n\treturn section;\n}\n\n/** UI type descriptions for system prompt context */\nconst UI_DESCRIPTIONS: Record<string, string> = {\n\ttui: \"Terminal UI (interactive terminal with rich rendering)\",\n\ttelegram:\n\t\t\"Telegram (mobile messaging app — the user is on their phone so messages may be shorter or have typos, but this doesn't reflect less thought or intent. The user sees tool names and arguments but not tool output/results, so summarize key findings or changes when relevant)\",\n\trpc: \"RPC (programmatic interface — another application is consuming your output)\",\n\tcli: \"CLI (non-interactive command line — output will be printed and the process exits)\",\n\tagent: \"Subagent (running as a child agent — focus on the task, report results concisely)\",\n};\n\n/** Format the UI context section for the system prompt */\nfunction formatUiSection(uiType: string): string {\n\tconst description = UI_DESCRIPTIONS[uiType] || uiType;\n\treturn `\\nUI: ${description}`;\n}\n\n/** Format the git repo state section for the system prompt */\nfunction formatGitStateSection(state: GitRepoState): string {\n\tlet section = \"\\n\\n## Project state (true at session start only)\\n\\n\";\n\tsection += `- Branch: \\`${state.branch}\\`\\n`;\n\tsection += `- Status: ${state.dirtyCount === 0 ? \"clean\" : `${state.dirtyCount} uncommitted ${state.dirtyCount === 1 ? \"change\" : \"changes\"}`}\\n`;\n\n\tif (state.recentCommits.length > 0) {\n\t\tsection += \"- Recent commits:\\n\";\n\t\tfor (const commit of state.recentCommits) {\n\t\t\tsection += ` - \\`${commit.hash} — ${commit.subject}\\`\\n`;\n\t\t}\n\t}\n\n\tif (state.recentTags.length > 0) {\n\t\tsection += \"- Recent releases:\\n\";\n\t\tfor (const tag of state.recentTags) {\n\t\t\tsection += ` - \\`${tag.name}\\` (${tag.date})\\n`;\n\t\t}\n\t}\n\n\tif (state.openPRs.length > 0) {\n\t\tsection += \"- Open PRs on this branch:\\n\";\n\t\tfor (const pr of state.openPRs) {\n\t\t\tsection += ` - PR ${pr.number} — ${pr.title} (${pr.url})\\n`;\n\t\t}\n\t}\n\n\treturn section;\n}\n\n/** Build the system prompt with tools, guidelines, and context */\nexport function buildSystemPrompt(options: BuildSystemPromptOptions = {}): string {\n\tconst {\n\t\tcustomPrompt,\n\t\tselectedTools,\n\t\ttoolSnippets,\n\t\tpromptGuidelines,\n\t\tappendSystemPrompt,\n\t\tcwd,\n\t\tcontextFiles: providedContextFiles,\n\t\tskills: providedSkills,\n\t} = options;\n\tconst resolvedCwd = cwd ?? process.cwd();\n\tconst promptCwd = resolvedCwd.replace(/\\\\/g, \"/\");\n\n\tconst date = new Date().toISOString().slice(0, 10);\n\n\tconst appendSection = appendSystemPrompt ? `\\n\\n${appendSystemPrompt}` : \"\";\n\n\tconst contextFiles = providedContextFiles ?? [];\n\tconst skills = providedSkills ?? [];\n\n\tif (customPrompt) {\n\t\tlet prompt = customPrompt;\n\n\t\tif (appendSection) {\n\t\t\tprompt += appendSection;\n\t\t}\n\n\t\t// Append project context files\n\t\tif (contextFiles.length > 0) {\n\t\t\tprompt += \"\\n\\n# Project Context\\n\\n\";\n\t\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\t\tprompt += `## ${filePath}\\n\\n${content}\\n\\n`;\n\t\t\t}\n\t\t}\n\n\t\t// Append skills section (when skill or read tool is available)\n\t\tconst customPromptHasSkillAccess =\n\t\t\t!selectedTools || selectedTools.includes(\"skill\") || selectedTools.includes(\"read\");\n\t\tif (customPromptHasSkillAccess && skills.length > 0) {\n\t\t\tprompt += formatSkillsForPrompt(skills);\n\t\t}\n\n\t\t// Append memory indexes\n\t\tprompt += buildMemorySection(options.memoryIndexes);\n\n\t\t// Append git repo state\n\t\tif (options.gitRepoState) {\n\t\t\tprompt += formatGitStateSection(options.gitRepoState);\n\t\t}\n\n\t\t// Add date and working directory last\n\t\tprompt += `\\nCurrent date: ${date}`;\n\t\tprompt += `\\nCurrent working directory: ${promptCwd}`;\n\t\tif (options.uiType) {\n\t\t\tprompt += formatUiSection(options.uiType);\n\t\t}\n\n\t\treturn prompt;\n\t}\n\n\t// Get absolute paths to documentation and examples\n\tconst readmePath = getReadmePath();\n\tconst docsPath = getDocsPath();\n\tconst examplesPath = getExamplesPath();\n\n\t// Build tools list based on selected tools.\n\t// A tool appears in Available tools only when the caller provides a one-line snippet.\n\tconst tools = selectedTools || [\n\t\t\"read\",\n\t\t\"bash\",\n\t\t\"edit\",\n\t\t\"write\",\n\t\t\"grep\",\n\t\t\"find\",\n\t\t\"ls\",\n\t\t\"web_search\",\n\t\t\"web_fetch\",\n\t\t\"subagent\",\n\t\t\"wait\",\n\t];\n\tconst visibleTools = tools.filter((name) => !!toolSnippets?.[name]);\n\tconst toolsList =\n\t\tvisibleTools.length > 0 ? visibleTools.map((name) => `- ${name}: ${toolSnippets![name]}`).join(\"\\n\") : \"(none)\";\n\n\t// Build guidelines based on which tools are actually available\n\tconst guidelinesList: string[] = [];\n\tconst guidelinesSet = new Set<string>();\n\tconst addGuideline = (guideline: string): void => {\n\t\tif (guidelinesSet.has(guideline)) {\n\t\t\treturn;\n\t\t}\n\t\tguidelinesSet.add(guideline);\n\t\tguidelinesList.push(guideline);\n\t};\n\n\tconst hasBash = tools.includes(\"bash\");\n\tconst hasGrep = tools.includes(\"grep\");\n\tconst hasFind = tools.includes(\"find\");\n\tconst hasLs = tools.includes(\"ls\");\n\tconst hasRead = tools.includes(\"read\");\n\tconst hasSearch = tools.includes(\"search\");\n\n\t// File exploration guidelines\n\tif (hasBash && !hasGrep && !hasFind && !hasLs) {\n\t\taddGuideline(\"Use bash for file operations like ls, rg, find\");\n\t} else if (hasBash && (hasGrep || hasFind || hasLs)) {\n\t\tif (hasSearch) {\n\t\t\taddGuideline(\n\t\t\t\t\"Start with `search` to explore and understand the codebase. Use grep/find/ls for exact text matches and specific file lookups. Prefer all of these over bash.\",\n\t\t\t);\n\t\t} else {\n\t\t\taddGuideline(\"Prefer grep/find/ls tools over bash for file exploration (faster, respects .gitignore)\");\n\t\t}\n\t}\n\n\tfor (const guideline of promptGuidelines ?? []) {\n\t\tconst normalized = guideline.trim();\n\t\tif (normalized.length > 0) {\n\t\t\taddGuideline(normalized);\n\t\t}\n\t}\n\n\t// Always include these\n\taddGuideline(\"Be concise in your responses\");\n\taddGuideline(\"Show file paths clearly when working with files\");\n\n\tconst guidelines = guidelinesList.map((g) => `- ${g}`).join(\"\\n\");\n\n\tlet prompt = `You are an expert coding assistant operating inside dreb, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.\n\nAvailable tools:\n${toolsList}\n\nIn addition to the tools above, you may have access to other custom tools depending on the project.\n\nGuidelines:\n${guidelines}\n\nDreb documentation (read only when the user asks about dreb itself, its SDK, extensions, themes, skills, or TUI):\n- Main documentation: ${readmePath}\n- Additional docs: ${docsPath}\n- Examples: ${examplesPath} (extensions, custom tools, SDK)\n- When asked about: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), dreb packages (docs/packages.md)\n- When working on dreb topics, read the docs and examples, and follow .md cross-references before implementing\n- Always read dreb .md files completely and follow links to related docs (e.g., tui.md for TUI API details)`;\n\n\tif (appendSection) {\n\t\tprompt += appendSection;\n\t}\n\n\t// Append project context files\n\tif (contextFiles.length > 0) {\n\t\tprompt += \"\\n\\n# Project Context\\n\\n\";\n\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\tprompt += `## ${filePath}\\n\\n${content}\\n\\n`;\n\t\t}\n\t}\n\n\t// Append skills section (when skill or read tool is available)\n\tconst hasSkillAccess = hasRead || tools.includes(\"skill\");\n\tif (hasSkillAccess && skills.length > 0) {\n\t\tprompt += formatSkillsForPrompt(skills);\n\t}\n\n\t// Append memory indexes\n\tprompt += buildMemorySection(options.memoryIndexes);\n\n\t// Append git repo state\n\tif (options.gitRepoState) {\n\t\tprompt += formatGitStateSection(options.gitRepoState);\n\t}\n\n\t// Add date and working directory last\n\tprompt += `\\nCurrent date: ${date}`;\n\tprompt += `\\nCurrent working directory: ${promptCwd}`;\n\tif (options.uiType) {\n\t\tprompt += formatUiSection(options.uiType);\n\t}\n\n\treturn prompt;\n}\n"]}
1
+ {"version":3,"file":"system-prompt.js","sourceRoot":"","sources":["../../src/core/system-prompt.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * System prompt construction and project context loading\n */\n\nimport { getDocsPath, getExamplesPath, getReadmePath } from \"../config.js\";\nimport type { GitRepoState } from \"./git-repo-state.js\";\nimport { getMemoryInstructions } from \"./memory-prompt.js\";\nimport type { MemoryIndexes } from \"./resource-loader.js\";\nimport { formatSkillsForPrompt, type Skill } from \"./skills.js\";\n\nexport interface BuildSystemPromptOptions {\n\t/** Custom system prompt (replaces default). */\n\tcustomPrompt?: string;\n\t/** Tools to include in prompt. Default: [read, bash, edit, write] */\n\tselectedTools?: string[];\n\t/** Optional one-line tool snippets keyed by tool name. */\n\ttoolSnippets?: Record<string, string>;\n\t/** Additional guideline bullets appended to the default system prompt guidelines. */\n\tpromptGuidelines?: string[];\n\t/** Text to append to system prompt. */\n\tappendSystemPrompt?: string;\n\t/** UI type the agent is communicating through (e.g. \"tui\", \"telegram\", \"rpc\"). */\n\tuiType?: string;\n\t/** Working directory. Default: process.cwd() */\n\tcwd?: string;\n\t/** Pre-loaded context files. */\n\tcontextFiles?: Array<{ path: string; content: string }>;\n\t/** Memory indexes (global and project). */\n\tmemoryIndexes?: MemoryIndexes;\n\t/** Pre-loaded skills. */\n\tskills?: Skill[];\n\t/** Git repo state snapshot (branch, dirty count, recent commits, tags, open PRs). */\n\tgitRepoState?: GitRepoState;\n}\n\nfunction formatMemoryScope(sources: readonly import(\"./resource-loader.js\").MemorySource[], heading: string): string {\n\tif (sources.length === 0) return \"\";\n\n\tconst drebSources = sources.filter((s) => s.source === \"dreb\");\n\tconst claudeSources = sources.filter((s) => s.source === \"claude\");\n\n\tlet out = `\\n### ${heading}\\n`;\n\n\tfor (const source of drebSources) {\n\t\tout += `\\n#### dreb memory (${source.dir}/)\\n\\n${source.content}\\n`;\n\t}\n\n\tif (claudeSources.length > 0) {\n\t\tout += `\\n#### Claude Code memory (read-only)\\n`;\n\t\tout += `> **Note:** These memories were written by Claude Code and may reference Claude Code-specific features, tools, or conventions that don't exist in dreb. Treat the content as useful context, but verify any tool names or workflow references.\\n`;\n\t\tfor (const source of claudeSources) {\n\t\t\tout += `\\nSource: ${source.dir}/\\n\\n${source.content}\\n`;\n\t\t}\n\t}\n\n\treturn out;\n}\n\n/**\n * Format a dream last-run ISO timestamp as a human-readable relative age.\n * Returns \"Never\" if timestamp is null.\n * Uses hours for <24h, days otherwise.\n * @param isoTimestamp ISO timestamp string or null\n * @param now Reference date for computing age (default: current time)\n */\nexport function formatDreamAge(isoTimestamp: string | null, now?: Date): string {\n\tif (!isoTimestamp) return \"Never\";\n\n\tconst then = new Date(isoTimestamp);\n\tif (Number.isNaN(then.getTime())) return \"Never\";\n\n\tconst reference = now ?? new Date();\n\tconst diffMs = reference.getTime() - then.getTime();\n\n\t// Future or zero — treat as just now\n\tif (diffMs <= 0) return \"just now\";\n\n\tconst diffHours = Math.floor(diffMs / (1000 * 60 * 60));\n\tif (diffHours < 1) return \"less than an hour ago\";\n\tif (diffHours < 24) return `${diffHours} ${diffHours === 1 ? \"hour\" : \"hours\"} ago`;\n\n\tconst diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));\n\treturn `${diffDays} ${diffDays === 1 ? \"day\" : \"days\"} ago`;\n}\n\nfunction buildMemorySection(memoryIndexes?: MemoryIndexes): string {\n\tif (!memoryIndexes) return \"\";\n\n\t// Always include memory instructions so the agent knows the convention\n\tlet section = `\\n\\n${getMemoryInstructions({ globalMemoryDir: memoryIndexes.globalMemoryDir, projectMemoryDir: memoryIndexes.projectMemoryDir })}`;\n\n\tconst { global: globalSources, project: projectSources } = memoryIndexes;\n\n\t// Append dream last-run age indicator\n\tconst dreamAge = formatDreamAge(memoryIndexes.dreamLastRun);\n\tif (dreamAge === \"Never\") {\n\t\tsection += \"\\n\\nMemory last consolidated: Never\";\n\t} else {\n\t\tconst dateStr = memoryIndexes.dreamLastRun!.slice(0, 10);\n\t\tsection += `\\n\\nMemory last consolidated: ${dateStr} (${dreamAge})`;\n\t}\n\n\t// Append the actual memory indexes if any exist\n\tif (globalSources.length > 0 || projectSources.length > 0) {\n\t\tsection += \"\\n\\n## Current Memory Indexes\\n\";\n\t\tsection += formatMemoryScope(globalSources, \"Global Memory\");\n\t\tsection += formatMemoryScope(projectSources, \"Project Memory\");\n\t}\n\n\treturn section;\n}\n\n/** Build the root security warning section if running as UID 0 */\nfunction buildRootSecuritySection(): string {\n\tif (process.getuid?.() !== 0) return \"\";\n\n\treturn `\n\n## ⚠️ Security: Running as Root\n\nYou are running as root (UID 0). You have unrestricted system access. This means:\n- **Never** attempt privilege escalation (sudo, doas, su) — you already have full privileges\n- Be extremely conservative with file modifications, package installations, and system commands\n- Prefer the least-destructive approach for every operation\n- If a task could affect system stability, inform the user before proceeding`;\n}\n\n/** UI type descriptions for system prompt context */\nconst UI_DESCRIPTIONS: Record<string, string> = {\n\ttui: \"Terminal UI (interactive terminal with rich rendering)\",\n\ttelegram:\n\t\t\"Telegram (mobile messaging app — the user is on their phone so messages may be shorter or have typos, but this doesn't reflect less thought or intent. The user sees tool names and arguments but not tool output/results, so summarize key findings or changes when relevant)\",\n\trpc: \"RPC (programmatic interface — another application is consuming your output)\",\n\tcli: \"CLI (non-interactive command line — output will be printed and the process exits)\",\n\tagent: \"Subagent (running as a child agent — focus on the task, report results concisely)\",\n};\n\n/** Format the UI context section for the system prompt */\nfunction formatUiSection(uiType: string): string {\n\tconst description = UI_DESCRIPTIONS[uiType] || uiType;\n\treturn `\\nUI: ${description}`;\n}\n\n/** Format the git repo state section for the system prompt */\nfunction formatGitStateSection(state: GitRepoState): string {\n\tlet section = \"\\n\\n## Project state (true at session start only)\\n\\n\";\n\tsection += `- Branch: \\`${state.branch}\\`\\n`;\n\tsection += `- Status: ${state.dirtyCount === 0 ? \"clean\" : `${state.dirtyCount} uncommitted ${state.dirtyCount === 1 ? \"change\" : \"changes\"}`}\\n`;\n\n\tif (state.recentCommits.length > 0) {\n\t\tsection += \"- Recent commits:\\n\";\n\t\tfor (const commit of state.recentCommits) {\n\t\t\tsection += ` - \\`${commit.hash} — ${commit.subject}\\`\\n`;\n\t\t}\n\t}\n\n\tif (state.recentTags.length > 0) {\n\t\tsection += \"- Recent releases:\\n\";\n\t\tfor (const tag of state.recentTags) {\n\t\t\tsection += ` - \\`${tag.name}\\` (${tag.date})\\n`;\n\t\t}\n\t}\n\n\tif (state.openPRs.length > 0) {\n\t\tsection += \"- Open PRs on this branch:\\n\";\n\t\tfor (const pr of state.openPRs) {\n\t\t\tsection += ` - PR ${pr.number} — ${pr.title} (${pr.url})\\n`;\n\t\t}\n\t}\n\n\treturn section;\n}\n\n/** Build the system prompt with tools, guidelines, and context */\nexport function buildSystemPrompt(options: BuildSystemPromptOptions = {}): string {\n\tconst {\n\t\tcustomPrompt,\n\t\tselectedTools,\n\t\ttoolSnippets,\n\t\tpromptGuidelines,\n\t\tappendSystemPrompt,\n\t\tcwd,\n\t\tcontextFiles: providedContextFiles,\n\t\tskills: providedSkills,\n\t} = options;\n\tconst resolvedCwd = cwd ?? process.cwd();\n\tconst promptCwd = resolvedCwd.replace(/\\\\/g, \"/\");\n\n\tconst date = new Date().toISOString().slice(0, 10);\n\n\tconst appendSection = appendSystemPrompt ? `\\n\\n${appendSystemPrompt}` : \"\";\n\n\tconst contextFiles = providedContextFiles ?? [];\n\tconst skills = providedSkills ?? [];\n\n\tif (customPrompt) {\n\t\tlet prompt = customPrompt;\n\n\t\tif (appendSection) {\n\t\t\tprompt += appendSection;\n\t\t}\n\n\t\t// Append project context files\n\t\tif (contextFiles.length > 0) {\n\t\t\tprompt += \"\\n\\n# Project Context\\n\\n\";\n\t\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\t\tprompt += `## ${filePath}\\n\\n${content}\\n\\n`;\n\t\t\t}\n\t\t}\n\n\t\t// Append skills section (when skill or read tool is available)\n\t\tconst customPromptHasSkillAccess =\n\t\t\t!selectedTools || selectedTools.includes(\"skill\") || selectedTools.includes(\"read\");\n\t\tif (customPromptHasSkillAccess && skills.length > 0) {\n\t\t\tprompt += formatSkillsForPrompt(skills);\n\t\t}\n\n\t\t// Append memory indexes\n\t\tprompt += buildMemorySection(options.memoryIndexes);\n\n\t\t// Append git repo state\n\t\tif (options.gitRepoState) {\n\t\t\tprompt += formatGitStateSection(options.gitRepoState);\n\t\t}\n\n\t\t// Append root security warning if running as UID 0\n\t\tprompt += buildRootSecuritySection();\n\n\t\t// Add date and working directory last\n\t\tprompt += `\\nCurrent date: ${date}`;\n\t\tprompt += `\\nCurrent working directory: ${promptCwd}`;\n\t\tif (options.uiType) {\n\t\t\tprompt += formatUiSection(options.uiType);\n\t\t}\n\n\t\treturn prompt;\n\t}\n\n\t// Get absolute paths to documentation and examples\n\tconst readmePath = getReadmePath();\n\tconst docsPath = getDocsPath();\n\tconst examplesPath = getExamplesPath();\n\n\t// Build tools list based on selected tools.\n\t// A tool appears in Available tools only when the caller provides a one-line snippet.\n\tconst tools = selectedTools || [\n\t\t\"read\",\n\t\t\"bash\",\n\t\t\"edit\",\n\t\t\"write\",\n\t\t\"grep\",\n\t\t\"find\",\n\t\t\"ls\",\n\t\t\"web_search\",\n\t\t\"web_fetch\",\n\t\t\"subagent\",\n\t\t\"wait\",\n\t];\n\tconst visibleTools = tools.filter((name) => !!toolSnippets?.[name]);\n\tconst toolsList =\n\t\tvisibleTools.length > 0 ? visibleTools.map((name) => `- ${name}: ${toolSnippets![name]}`).join(\"\\n\") : \"(none)\";\n\n\t// Build guidelines based on which tools are actually available\n\tconst guidelinesList: string[] = [];\n\tconst guidelinesSet = new Set<string>();\n\tconst addGuideline = (guideline: string): void => {\n\t\tif (guidelinesSet.has(guideline)) {\n\t\t\treturn;\n\t\t}\n\t\tguidelinesSet.add(guideline);\n\t\tguidelinesList.push(guideline);\n\t};\n\n\tconst hasBash = tools.includes(\"bash\");\n\tconst hasGrep = tools.includes(\"grep\");\n\tconst hasFind = tools.includes(\"find\");\n\tconst hasLs = tools.includes(\"ls\");\n\tconst hasRead = tools.includes(\"read\");\n\tconst hasSearch = tools.includes(\"search\");\n\n\t// File exploration guidelines\n\tif (hasBash && !hasGrep && !hasFind && !hasLs) {\n\t\taddGuideline(\"Use bash for file operations like ls, rg, find\");\n\t} else if (hasBash && (hasGrep || hasFind || hasLs)) {\n\t\tif (hasSearch) {\n\t\t\taddGuideline(\n\t\t\t\t\"Start with `search` to explore and understand the codebase. Use grep/find/ls for exact text matches and specific file lookups. Prefer all of these over bash.\",\n\t\t\t);\n\t\t} else {\n\t\t\taddGuideline(\"Prefer grep/find/ls tools over bash for file exploration (faster, respects .gitignore)\");\n\t\t}\n\t}\n\n\tfor (const guideline of promptGuidelines ?? []) {\n\t\tconst normalized = guideline.trim();\n\t\tif (normalized.length > 0) {\n\t\t\taddGuideline(normalized);\n\t\t}\n\t}\n\n\t// Always include these\n\taddGuideline(\"Be concise in your responses\");\n\taddGuideline(\"Show file paths clearly when working with files\");\n\n\tconst guidelines = guidelinesList.map((g) => `- ${g}`).join(\"\\n\");\n\n\tlet prompt = `You are an expert coding assistant operating inside dreb, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.\n\nAvailable tools:\n${toolsList}\n\nIn addition to the tools above, you may have access to other custom tools depending on the project.\n\nGuidelines:\n${guidelines}\n\nDreb documentation (read only when the user asks about dreb itself, its SDK, extensions, themes, skills, or TUI):\n- Main documentation: ${readmePath}\n- Additional docs: ${docsPath}\n- Examples: ${examplesPath} (extensions, custom tools, SDK)\n- When asked about: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), dreb packages (docs/packages.md)\n- When working on dreb topics, read the docs and examples, and follow .md cross-references before implementing\n- Always read dreb .md files completely and follow links to related docs (e.g., tui.md for TUI API details)`;\n\n\tif (appendSection) {\n\t\tprompt += appendSection;\n\t}\n\n\t// Append project context files\n\tif (contextFiles.length > 0) {\n\t\tprompt += \"\\n\\n# Project Context\\n\\n\";\n\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\tprompt += `## ${filePath}\\n\\n${content}\\n\\n`;\n\t\t}\n\t}\n\n\t// Append skills section (when skill or read tool is available)\n\tconst hasSkillAccess = hasRead || tools.includes(\"skill\");\n\tif (hasSkillAccess && skills.length > 0) {\n\t\tprompt += formatSkillsForPrompt(skills);\n\t}\n\n\t// Append memory indexes\n\tprompt += buildMemorySection(options.memoryIndexes);\n\n\t// Append git repo state\n\tif (options.gitRepoState) {\n\t\tprompt += formatGitStateSection(options.gitRepoState);\n\t}\n\n\t// Append root security warning if running as UID 0\n\tprompt += buildRootSecuritySection();\n\n\t// Add date and working directory last\n\tprompt += `\\nCurrent date: ${date}`;\n\tprompt += `\\nCurrent working directory: ${promptCwd}`;\n\tif (options.uiType) {\n\t\tprompt += formatUiSection(options.uiType);\n\t}\n\n\treturn prompt;\n}\n"]}
@@ -97,7 +97,9 @@ export type SubagentModelResolution = {
97
97
  error: string;
98
98
  skippedModels: SkippedFallbackModel[];
99
99
  };
100
- export declare function resolveModelForSubagentSpawn(models: string | string[], parentProvider: string | undefined, registry: ModelRegistry | undefined, parentModel?: string, signal?: AbortSignal): Promise<SubagentModelResolution>;
100
+ export declare function resolveModelForSubagentSpawn(models: string | string[], parentProvider: string | undefined, registry: ModelRegistry | undefined, parentModel?: string, signal?: AbortSignal,
101
+ /** Optional log prefix for warning messages (defaults to "[subagent]") */
102
+ logPrefix?: string): Promise<SubagentModelResolution>;
101
103
  export declare function formatModelFallbackSummary(skippedModels: SkippedFallbackModel[], selectedModel: string | undefined): string | undefined;
102
104
  export declare function prependModelFallbackSummary(output: string, skippedModels: SkippedFallbackModel[], selectedModel: string | undefined): string;
103
105
  export declare function executeSingle(agents: Map<string, AgentTypeConfig>, agentName: string | undefined, task: string, cwd: string, signal?: AbortSignal, onProgress?: (event: string) => void, modelOverride?: string, parentProvider?: string, registry?: ModelRegistry, sessionDir?: string, parentModel?: string): Promise<SubagentResult>;