feat(agent): confine agent bash in an OS sandbox (bubblewrap), mode-gated
The SDK's built-in Bash tool ran commands as a bare subprocess with only current_dir set — no OS boundary. On a server that let an agent shell read /etc/shadow, cd /, write anywhere the process uid could, and reach the network; the phase-1 path validation only guards the file tools, not bash. (The SDK's SandboxSettings is dead config: Agent::new never reads it.) Replace it with SandboxedBashTool — same name and input schema, registered via custom_tools so it overrides the built-in by name — that confines the command with bubblewrap, gated by permission mode: - Default / AcceptEdits (review modes): run inside a bwrap namespace with the system mounted read-only, the session sandbox dir bind-mounted rw at its real path and made the cwd, /etc //root //home and all other host paths absent, and --unshare-all (network included). Filesystem escape and network egress are blocked by the kernel, not by convention. - FullAccess (explicitly dangerous): unconfined, same as before, but every command is audit-logged with a warning. Fail-closed: in a review mode, if bwrap is unavailable the command is refused rather than silently run unconfined — no fabricated boundary. Verified end-to-end in tests/agent_bash_sandbox.rs by invoking the tool: under Default mode `cat /etc/passwd` sees no host passwd, `ls /` exposes no etc//root//home, a write to /etc/cron.d/x fails (and the host file is never created), and the network is unreachable; in-sandbox work succeeds and lands in the real session dir; FullAccess can see the host. bwrap 0.11.2 confirmed functional on this host (root, CAP_SYS_ADMIN, user namespaces enabled). Phase-1 suites stay green.
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
use crate::auth::jwt::validate_jwt;
|
||||
use crate::services::agent_workbench::{
|
||||
approval::{self, ApprovalOutcome, SettledState},
|
||||
bash_sandbox::SandboxedBashTool,
|
||||
permission::{classify_tool_risk, decide_permission, PermissionAction, PermissionMode},
|
||||
sandbox,
|
||||
session_store::{self, UsageDelta},
|
||||
@@ -318,6 +319,13 @@ async fn chat_handler(
|
||||
|
||||
let can_use_tool = build_can_use_tool(tx.clone(), cwd.clone(), mode);
|
||||
|
||||
// Replace the SDK's built-in Bash (a bare subprocess) with an
|
||||
// OS-sandboxed one confined to this session's directory. Registered as
|
||||
// a custom tool, it overrides the built-in by name; review modes run it
|
||||
// under bwrap, FullAccess runs it unconfined with audit logging.
|
||||
let sandboxed_bash: Arc<dyn open_agent_sdk::types::Tool> =
|
||||
Arc::new(SandboxedBashTool::new(cwd.clone(), mode));
|
||||
|
||||
let agent = Agent::new(AgentOptions {
|
||||
provider: Some(provider),
|
||||
model: Some(model.clone()),
|
||||
@@ -325,6 +333,7 @@ async fn chat_handler(
|
||||
system_prompt: Some(SYSTEM_PROMPT.to_string()),
|
||||
max_turns: Some(MAX_TURNS),
|
||||
can_use_tool: Some(can_use_tool),
|
||||
custom_tools: vec![sandboxed_bash],
|
||||
session_id: Some(session_id.clone()),
|
||||
..Default::default()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user