| 873 | * @param config Required configuration including working directory |
| 874 | */ |
| 875 | export const createBashTool: ToolFactory = (config: ToolConfiguration) => { |
| 876 | // Select limits based on overflow policy |
| 877 | // truncate = IPC calls (generous limits for UI features, no line limit, no per-line limit) |
| 878 | // tmpfile = AI agent calls (conservative limits for LLM context) |
| 879 | const overflowPolicy = config.overflow_policy ?? "tmpfile"; |
| 880 | const maxTotalBytes = |
| 881 | overflowPolicy === "truncate" ? BASH_TRUNCATE_MAX_TOTAL_BYTES : BASH_MAX_TOTAL_BYTES; |
| 882 | const maxFileBytes = |
| 883 | overflowPolicy === "truncate" ? BASH_TRUNCATE_MAX_FILE_BYTES : BASH_MAX_FILE_BYTES; |
| 884 | const maxLines = overflowPolicy === "truncate" ? Infinity : BASH_HARD_MAX_LINES; |
| 885 | const maxLineBytes = overflowPolicy === "truncate" ? Infinity : BASH_MAX_LINE_BYTES; |
| 886 | |
| 887 | return tool({ |
| 888 | description: buildBashToolDescription(config.cwd, config.projects ?? []), |
| 889 | inputSchema: TOOL_DEFINITIONS.bash.schema, |
| 890 | execute: async ( |
| 891 | { script, timeout_secs, run_in_background, display_name, monitor }, |
| 892 | { abortSignal, toolCallId } |
| 893 | ): Promise<BashToolResult> => { |
| 894 | // Validate script input |
| 895 | |
| 896 | // Treat display_name as untrusted input: it ends up in filesystem paths. |
| 897 | const safeDisplayName = resolveBashDisplayName(script, display_name); |
| 898 | const validationError = validateScript(script, config); |
| 899 | if (validationError) return validationError; |
| 900 | |
| 901 | // Warn when the model appears to be reading files via bash output (cat/rg/grep). |
| 902 | // Reading files via bash output is fragile (may be truncated or auto-filtered); |
| 903 | // file_read supports paging and avoids silent context loss. |
| 904 | const fileReadNotice = |
| 905 | detectCatFileRead(script) || detectRipgrepFileDump(script) || detectGrepFileDump(script) |
| 906 | ? CAT_FILE_READ_NOTICE |
| 907 | : undefined; |
| 908 | |
| 909 | // Look up .mux/tool_env to source before script (for direnv, nvm, venv, etc.) |
| 910 | // Skip for untrusted projects — tool_env is repo-controlled code |
| 911 | const toolEnvPath = |
| 912 | config.trusted && config.runtime ? await getToolEnvPath(config.runtime, config.cwd) : null; |
| 913 | const toolEnvPrelude = buildToolEnvPrelude(toolEnvPath); |
| 914 | |
| 915 | // Neutralize git hooks for untrusted projects — prevent repository-controlled |
| 916 | // hooks from executing when the model runs git subcommands (for example, |
| 917 | // `git commit` or `git am`) through this bash tool. |
| 918 | const hooksEnv = config.trusted !== true ? GIT_NO_HOOKS_ENV : {}; |
| 919 | |
| 920 | // On Windows, models sometimes emit cmd.exe-style `>nul` / `2>nul` redirections. |
| 921 | // Since the bash tool runs via bash, `nul` becomes a real file in the workspace. |
| 922 | // Only rewrite for local Windows runtimes so non-Windows scripts keep their semantics. |
| 923 | const shouldRewriteNullRedirects = |
| 924 | process.platform === "win32" && config.runtime instanceof LocalBaseRuntime; |
| 925 | const nulRedirectRewrite = shouldRewriteNullRedirects |
| 926 | ? rewriteWindowsNullRedirects(script) |
| 927 | : { script, didRewrite: false }; |
| 928 | const scriptWithEnv = toolEnvPrelude + nulRedirectRewrite.script; |
| 929 | |
| 930 | const nulRedirectNote = nulRedirectRewrite.didRewrite |
| 931 | ? "Rewrote `>nul`/`2>nul` → `/dev/null` (bash tool runs in bash; use `/dev/null` to discard output)." |
| 932 | : undefined; |