Skip to content

refactor: merge exec tool into shell, add per-command env vars#360

Merged
jamiepine merged 4 commits intomainfrom
refactor/merge-shell-exec
Mar 8, 2026
Merged

refactor: merge exec tool into shell, add per-command env vars#360
jamiepine merged 4 commits intomainfrom
refactor/merge-shell-exec

Conversation

@jamiepine
Copy link
Member

@jamiepine jamiepine commented Mar 8, 2026

Summary

  • Remove the separate exec tool entirely — the shell tool now supports an optional env parameter for per-command environment variables
  • Eliminates tool overlap: shell already handles everything exec did via sh -c, the env parameter covers the one unique exec capability
  • Carries over the DANGEROUS_ENV_VARS blocklist from exec (LD_PRELOAD, NODE_OPTIONS, etc.)

Changes

  • Add EnvVar type and env field to ShellArgs with JSON schema
  • Add DANGEROUS_ENV_VARS validation to shell call()
  • Delete src/tools/exec.rs, prompt description, text.rs registry entry
  • Remove ExecTool from worker and cortex chat ToolServer registrations
  • Update all prompts (worker, channel, branch, fragments, shell description)
  • Update spawn_worker tool definition (drop exec from tools list)
  • Update frontend (remove exec renderer from ToolCall.tsx)
  • Update AGENTS.md module map and tool references

17 files changed, -268 net lines.

Note

Summary for commit 94b2ef6:

This PR consolidates two overlapping execution tools into one. The shell tool now handles both shell commands and subprocess execution, eliminating the need for a separate exec tool. The key addition is the env parameter on shell commands, which allows setting per-command environment variables while maintaining the dangerous environment variable blocklist (LD_PRELOAD, NODE_OPTIONS, etc.) that prevents code injection attacks. Frontend and documentation updates ensure the tool consolidation is reflected throughout the system. Net change removes 268 lines of duplicate code while maintaining security guardrails.

Written by Tembo for commit 94b2ef6. This will update automatically on new commits.

Remove the separate exec tool entirely. The shell tool now supports an
optional `env` parameter for setting per-command environment variables
(with the same DANGEROUS_ENV_VARS blocklist that exec had). This
eliminates tool overlap — shell already handles everything exec did via
sh -c, and the env parameter covers the one capability exec had that
shell lacked.

Changes:
- Add EnvVar type and env field to ShellArgs with JSON schema
- Add DANGEROUS_ENV_VARS validation to shell tool call()
- Remove ExecTool, ExecArgs, ExecOutput, ExecError, ExecResult
- Remove exec.rs source, prompt description, and text.rs registry entry
- Remove exec from worker/cortex ToolServer registrations
- Update all prompts (worker, channel, branch, fragments, shell desc)
- Update spawn_worker tool definition (remove exec from tools list)
- Update frontend (remove exec renderer, update comments)
- Update AGENTS.md module map and tool references
- Replace exec tests with shell env parsing tests
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 8, 2026

Walkthrough

Removed the standalone exec tool and its code; merged subprocess handling into shell with per-command env support and dangerous-env filtering. Updated exports, prompts, docs, UI text, tests, and sandbox.wrap/backends to accept and propagate per-command environment variables.

Changes

Cohort / File(s) Summary
Tool implementation
src/tools/shell.rs, src/tools/exec.rs
Added EnvVar and per-command env handling plus dangerous-var validation to shell.rs; removed the entire src/tools/exec.rs.
Module exports & tooling registry
src/tools.rs, src/tools/spawn_worker.rs, src/prompts/text.rs
Removed public exec exports/re-exports and references; re-exported EnvVar from shell; removed exec from builtin tool lists and language-key mappings.
Sandbox plumbing
src/sandbox.rs
Changed wrap and backend wrappers to accept/propagate command_env; added DANGEROUS_ENV_VARS and logic to filter/block dangerous names; updated signatures.
Interface / UI
interface/src/components/ToolCall.tsx, interface/src/routes/AgentConfig.tsx
Added a legacy exec renderer for rendering old transcripts; updated UI wording to reference only shell subprocesses.
Prompts & templates
prompts/en/..., prompts/en/tools/exec_description.md.j2
Removed exec descriptions/templates; expanded shell docs to document env parameter and examples; updated sandbox messaging.
Docs & comments
AGENTS.md, src/agent/cortex_chat.rs, src/opencode/worker.rs, src/prompts/...
Replaced "shell/exec" phrasing with "shell" across docs, comments, and headers.
Tests
src/... (tests in src/tools/related)
Removed exec tests; added/updated shell env parsing and validation tests (empty keys, '=' in key, null bytes, blocked vars, case-insensitive checks).
Text & prompt fragments
prompts/en/branch.md.j2, prompts/en/channel.md.j2, prompts/en/fragments/worker_capabilities.md.j2, prompts/en/worker.md.j2, prompts/en/branch.md.j2
Adjusted prompt wording to remove exec, clarify sandbox semantics, and mention per-command env in shell descriptions.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: merging the exec tool into shell and adding per-command environment variable support.
Description check ✅ Passed The description clearly outlines the purpose, changes, and rationale for consolidating the exec tool into shell with new env parameter support.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/merge-shell-exec

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/tools/shell.rs`:
- Around line 209-212: The current loop in src/tools/shell.rs that applies
per-command env vars via cmd.env(...) sets them on the outer bwrap process and
therefore doesn't propagate into the bubblewrap sandbox; change the wrap()
function signature to accept per-command environment variables (e.g., a Vec of
EnvVar or the same args.env type) and remove the post-wrap cmd.env(...) usage,
then propagate that new env parameter into the sandbox backends so bubblewrap
injects them using "--setenv" when building the bwrap command; specifically
update wrap_sandbox_exec and wrap_passthrough (and any callers of wrap()) to
pass the per-command env list and, in sandbox.rs where bwrap args are assembled,
add cmd.arg("--setenv").arg(key).arg(value) (or the existing pattern used there)
for each env entry instead of relying on outer process env.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ae1d1d17-439e-4d23-9507-720d54ff73cd

📥 Commits

Reviewing files that changed from the base of the PR and between e81b99b and 94b2ef6.

📒 Files selected for processing (17)
  • AGENTS.md
  • interface/src/components/ToolCall.tsx
  • interface/src/routes/AgentConfig.tsx
  • prompts/en/branch.md.j2
  • prompts/en/channel.md.j2
  • prompts/en/fragments/worker_capabilities.md.j2
  • prompts/en/tools/exec_description.md.j2
  • prompts/en/tools/shell_description.md.j2
  • prompts/en/worker.md.j2
  • src/agent/cortex_chat.rs
  • src/opencode/worker.rs
  • src/prompts/text.rs
  • src/sandbox.rs
  • src/tools.rs
  • src/tools/exec.rs
  • src/tools/shell.rs
  • src/tools/spawn_worker.rs
💤 Files with no reviewable changes (3)
  • prompts/en/tools/exec_description.md.j2
  • src/tools/exec.rs
  • src/prompts/text.rs

},
},

set_status: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we persist tool call history (DB logs, old transcripts), dropping the exec renderer makes those older calls render as “unknown”. Might be worth keeping a legacy renderer here even though the tool is removed.

Suggested change
set_status: {
exec: {
summary(pair) {
const command = pair.args?.command;
if (!command) return null;
if (pair.result && typeof pair.result.exit_code === "number") {
const code = pair.result.exit_code;
const cmdStr = truncate(String(command), 50);
return code === 0 ? cmdStr : `${cmdStr} (exit ${code})`;
}
return truncate(String(command), 60);
},
resultView(pair) {
if (!pair.resultRaw) return null;
return <ShellResultView pair={pair} />;
},
},
set_status: {

Comment on lines +183 to +212
@@ -136,6 +206,11 @@ impl Tool for ShellTool {
.wrap("sh", &["-c", &args.command], &working_dir)
};

// Apply user-specified env vars after sandbox wrapping
for env_var in args.env {
cmd.env(env_var.key, env_var.value);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that shell supports caller-provided env, it’d be nice to (a) validate env var names early so we don’t surface a spawn-time error, and (b) scrub inherited DANGEROUS_ENV_VARS too (so a contaminated parent env can’t inject into the sandbox wrapper even if the user didn’t explicitly pass it).

Suggested change
// Block env vars that enable library injection or alter runtime
// loading behavior — these allow arbitrary code execution regardless
// of filesystem sandbox state.
for env_var in &args.env {
if env_var.key.is_empty() || env_var.key.contains('=') || env_var.key.contains('\0') {
return Err(ShellError {
message: format!("Invalid env var name: {}", env_var.key),
exit_code: -1,
});
}
if DANGEROUS_ENV_VARS
.iter()
.any(|blocked| env_var.key.eq_ignore_ascii_case(blocked))
{
return Err(ShellError {
message: format!(
"Cannot set {}: this environment variable enables code injection.",
env_var.key
),
exit_code: -1,
});
}
}
let mut cmd = if cfg!(target_os = "windows") {
self.sandbox
.wrap("cmd", &["/C", &args.command], &working_dir)
} else {
self.sandbox
.wrap("sh", &["-c", &args.command], &working_dir)
};
for blocked in DANGEROUS_ENV_VARS {
cmd.env_remove(*blocked);
}
// Apply user-specified env vars after sandbox wrapping
for env_var in args.env {
cmd.env(env_var.key, env_var.value);
}

jamiepine and others added 2 commits March 7, 2026 19:39
…derer

Address CodeRabbit review findings on PR #360:

- Fix bubblewrap env propagation: Sandbox::wrap() now accepts per-command
  env vars and injects them via --setenv (bwrap) or .env() (sandbox-exec/
  passthrough), so env vars always reach the inner sandboxed process.
- Add env var name validation: reject empty names, names containing '=',
  and names/values containing null bytes.
- Add defense-in-depth DANGEROUS_ENV_VARS check at the sandbox layer,
  so dangerous env vars are dropped even if tool-level validation is
  bypassed by a future caller.
- Re-add legacy 'exec' renderer in ToolCall.tsx for old transcript
  rendering (maps exec's program+args to shell-style display).
- Add 5 new tests for env validation and dangerous env blocking.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/sandbox.rs (1)

88-104: Defense-in-depth constant duplicated from shell.rs.

This constant is intentionally duplicated from src/tools/shell.rs to provide defense-in-depth at the sandbox layer. The duplication is acceptable here since:

  1. The sandbox layer must not depend on tool-layer definitions
  2. It ensures protection even if tool-level validation is bypassed
  3. Both lists are identical and documented as serving different purposes (tool returns error vs sandbox silently drops)

Consider adding a test that verifies both lists remain in sync to catch accidental divergence.

🧪 Optional: Add a sync test in src/sandbox.rs tests
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn dangerous_env_vars_matches_shell_tool() {
        // Ensure defense-in-depth lists stay in sync
        let shell_list = crate::tools::shell::DANGEROUS_ENV_VARS; // requires pub
        assert_eq!(DANGEROUS_ENV_VARS, shell_list);
    }
}

Note: This would require making the shell constant pub(crate).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sandbox.rs` around lines 88 - 104, Add a unit test to ensure the sandbox
DANGEROUS_ENV_VARS stays identical to the tool-level list and make the shell
constant accessible; specifically, expose the tool-level constant by changing
the visibility of crate::tools::shell::DANGEROUS_ENV_VARS to pub(crate) and add
a #[cfg(test)] mod tests in src/sandbox.rs that imports
crate::tools::shell::DANGEROUS_ENV_VARS and asserts equality with the sandbox
DANGEROUS_ENV_VARS (e.g., assert_eq!(DANGEROUS_ENV_VARS,
crate::tools::shell::DANGEROUS_ENV_VARS)); this will catch accidental divergence
while preserving the documented defense-in-depth behavior.
src/tools/shell.rs (1)

57-65: Consider deriving Clone for EnvVar.

The struct is consumed via into_iter() on line 235, which works, but deriving Clone would make the type more flexible if needed elsewhere (e.g., logging, retry logic).

♻️ Optional: Add Clone derive
 /// A key-value environment variable pair.
-#[derive(Debug, Deserialize, JsonSchema)]
+#[derive(Debug, Clone, Deserialize, JsonSchema)]
 pub struct EnvVar {
     /// The variable name.
     pub key: String,
     /// The variable value.
     pub value: String,
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tools/shell.rs` around lines 57 - 65, The EnvVar struct lacks Clone which
reduces flexibility; update the struct declaration to derive Clone alongside
Debug, Deserialize, and JsonSchema (i.e., add Clone to the derive list for
struct EnvVar) so instances can be cheaply cloned when needed (e.g., for logging
or retry paths) while keeping existing into_iter() usage unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/sandbox.rs`:
- Around line 88-104: Add a unit test to ensure the sandbox DANGEROUS_ENV_VARS
stays identical to the tool-level list and make the shell constant accessible;
specifically, expose the tool-level constant by changing the visibility of
crate::tools::shell::DANGEROUS_ENV_VARS to pub(crate) and add a #[cfg(test)] mod
tests in src/sandbox.rs that imports crate::tools::shell::DANGEROUS_ENV_VARS and
asserts equality with the sandbox DANGEROUS_ENV_VARS (e.g.,
assert_eq!(DANGEROUS_ENV_VARS, crate::tools::shell::DANGEROUS_ENV_VARS)); this
will catch accidental divergence while preserving the documented
defense-in-depth behavior.

In `@src/tools/shell.rs`:
- Around line 57-65: The EnvVar struct lacks Clone which reduces flexibility;
update the struct declaration to derive Clone alongside Debug, Deserialize, and
JsonSchema (i.e., add Clone to the derive list for struct EnvVar) so instances
can be cheaply cloned when needed (e.g., for logging or retry paths) while
keeping existing into_iter() usage unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 64c7b4a9-a986-45bb-b7a1-8b3996dbf756

📥 Commits

Reviewing files that changed from the base of the PR and between 94b2ef6 and 86b5d8f.

📒 Files selected for processing (4)
  • interface/src/components/ToolCall.tsx
  • src/sandbox.rs
  • src/tools.rs
  • src/tools/shell.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • interface/src/components/ToolCall.tsx

Shell tool runs without stdin, so interactive prompts from npm/npx/apt-get
always hang until the 60s timeout kills the process. Setting CI=true
prevents most Node.js tooling prompts (npm, npx, yarn) and
DEBIAN_FRONTEND=noninteractive prevents apt-get prompts.

Both vars are added to RESERVED_ENV_VARS so they cannot be overridden by
passthrough_env or per-command env vars.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/sandbox.rs (1)

602-614: Consider extracting filtered env vars to reduce duplication (optional).

The per-command env filtering logic is repeated across all three backends. You could extract a helper that filters the HashMap and logs appropriately:

fn filter_command_env<'a>(
    command_env: &'a HashMap<String, String>,
) -> impl Iterator<Item = (&'a String, &'a String)> {
    command_env.iter().filter(|(name, _)| {
        if is_reserved_env_var(name) {
            tracing::debug!(%name, "skipping reserved per-command env var");
            return false;
        }
        if is_dangerous_env_var(name) {
            tracing::warn!(%name, "dropping dangerous per-command env var");
            return false;
        }
        true
    })
}

Then each backend could simply iterate: for (name, value) in filter_command_env(command_env).

This is a minor DRY improvement—the current approach is acceptable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sandbox.rs` around lines 602 - 614, The per-command env var filtering in
sandbox.rs is duplicated across backends; extract a helper named
filter_command_env that takes &HashMap<String, String> and returns an iterator
over (&String, &String) applying is_reserved_env_var and is_dangerous_env_var
with the same tracing::debug!/tracing::warn! logging, then replace each
duplicate loop with `for (name, value) in filter_command_env(command_env)` so
all three backends reuse the single filtering function.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/sandbox.rs`:
- Around line 602-614: The per-command env var filtering in sandbox.rs is
duplicated across backends; extract a helper named filter_command_env that takes
&HashMap<String, String> and returns an iterator over (&String, &String)
applying is_reserved_env_var and is_dangerous_env_var with the same
tracing::debug!/tracing::warn! logging, then replace each duplicate loop with
`for (name, value) in filter_command_env(command_env)` so all three backends
reuse the single filtering function.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: eff089b9-340a-44a2-80b1-691a0d39effa

📥 Commits

Reviewing files that changed from the base of the PR and between 86b5d8f and b92bf6b.

📒 Files selected for processing (1)
  • src/sandbox.rs

@jamiepine jamiepine merged commit 857e29b into main Mar 8, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant