Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,7 @@ rustls-platform-verifier = "0.5.0"
# WARNING: If you change this, you must also publish a new version of zed-scap to crates.io
scap = { git = "https://github.com/zed-industries/scap", rev = "4afea48c3b002197176fb19cd0f9b180dd36eaac", default-features = false, package = "zed-scap", version = "0.0.8-zed" }
schemars = { version = "1.0", features = ["indexmap2"] }
seccompiler = "0.5"
semver = { version = "1.0", features = ["serde"] }
serde = { version = "1.0.221", features = ["derive", "rc"] }
serde_json = { version = "1.0.144", features = ["preserve_order", "raw_value"] }
Expand Down
5 changes: 5 additions & 0 deletions crates/acp_thread/src/acp_thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,11 @@ pub struct SandboxFallbackAuthorizationDetails {
/// whether to run the command without a sandbox.
#[serde(default)]
pub reason: String,
/// Slug of the sandboxing docs section that best explains how to fix this
/// failure (see [`crate::LinuxWslSandboxError::docs_section`]), rendered as a
/// "Learn more" link. `None` when the cause is unknown.
#[serde(default)]
pub docs_section: Option<String>,
}

pub fn meta_with_sandbox_fallback_authorization(
Expand Down
45 changes: 39 additions & 6 deletions crates/acp_thread/src/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,31 @@ impl LinuxWslSandboxError {
LinuxWslSandboxError::Other(message) => message.clone(),
}
}

/// The slug of the sandboxing docs section that best explains how to resolve
/// this failure, for deep-linking from the UI. Pair with
/// `client::zed_urls::sandboxing_docs`.
pub fn docs_section(&self) -> &'static str {
match self {
// Both "no bwrap" and "only a setuid-root bwrap" are resolved by
// installing a non-setuid Bubblewrap.
LinuxWslSandboxError::BwrapNotFound | LinuxWslSandboxError::SetuidRejected => {
"installing-bubblewrap"
}
// A failed probe on Linux is almost always disabled unprivileged
// user namespaces, which the Ubuntu-specific section covers.
LinuxWslSandboxError::SandboxProbeFailed => "installing-bubblewrap-ubuntu",
// Catch-all (includes WSL/Windows messages): point at the platform
// overview for the current OS.
LinuxWslSandboxError::Other(_) => {
if cfg!(target_os = "windows") {
"windows"
} else {
"linux"
}
}
}
}
}

impl SandboxWrap {
Expand All @@ -151,6 +176,15 @@ impl SandboxWrap {
/// grant as a [`sandbox::HostFilesystemLocation`] (pinning the inode / canonical
/// path) rather than passing a re-resolvable path. A location that can't be
/// captured (e.g. it doesn't exist) is dropped from the grant — fail-closed.
///
/// This function has **no filesystem side effects**: it never creates paths.
/// It is used both by the side-effect-free [`Self::can_create_sandbox`] probe
/// and by real sandbox construction, and must behave identically. On Linux a
/// writable grant that doesn't exist yet simply can't be captured (bwrap
/// can't bind a missing path), so it's dropped here — the sanctioned way to
/// get a grant to a new directory is the `create_directory` tool, which
/// creates it (pinning the inode) before the grant is recorded. On macOS a
/// missing leaf still canonicalizes, so such grants are captured directly.
fn to_policy(&self) -> sandbox::SandboxPolicy {
let protected_paths = self
.protected_paths
Expand All @@ -164,12 +198,11 @@ impl SandboxWrap {
.writable_paths
.iter()
.chain(self.extra_write_paths.iter())
.filter_map(|path| {
// Create not-yet-existing writable grants (e.g. an approved
// scratch dir) so they can be captured and bound; best-effort.
let _ = std::fs::create_dir_all(path);
sandbox::HostFilesystemLocation::new(path).ok()
})
// Capture only — never create anything here (see the doc comment):
// materializing an approved-but-missing grant is deferred to
// `Sandbox::new` so it can never happen during the `can_create`
// probe, before the user has approved the grant.
.filter_map(|path| sandbox::HostFilesystemLocation::new(path).ok())
.collect();
sandbox::SandboxFsPolicy::Restricted {
writable_paths,
Expand Down
34 changes: 12 additions & 22 deletions crates/agent/src/sandboxing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
//! caller see the same answer (and so the `target_os` gate lives in one
//! place instead of scattered across the agent crate).
//!
//! The current policy is: enabled iff the user has the `sandboxing` feature
//! flag turned on, the project is local, the platform has an integration, and
//! the user has not persistently allowed unsandboxed execution (the
//! `allow_unsandboxed` sandbox setting). Setting `allow_unsandboxed`
//! The current policy is: enabled iff the project is local, the platform has an
//! integration, and the user has not persistently allowed unsandboxed execution
//! (the `allow_unsandboxed` sandbox setting). Setting `allow_unsandboxed`
//! persistently turns sandboxing off for the model-facing surface entirely:
//! the plain (non-sandboxed) `terminal` tool is exposed and the system prompt
//! omits the sandbox section, since every command would run without a wrap
Expand All @@ -19,14 +18,12 @@
//!
//! macOS (Seatbelt), Linux (Bubblewrap), and Windows (Bubblewrap via WSL)
//! have real sandbox integrations; on platforms without one the per-command
//! wrap is a no-op, so commands run with the agent's ambient permissions even
//! when the flag is on.
//! wrap is a no-op, so commands run with the agent's ambient permissions.
//!
//! Naming note: this module is about agent terminal sandboxing specifically.
//! Other agent operations (e.g. file edits) are gated separately.

use agent_settings::{AgentSettings, SandboxPermissions};
use feature_flags::{FeatureFlagAppExt as _, SandboxingFeatureFlag};
use gpui::App;
use http_proxy::HostPattern;
use project::Project;
Expand Down Expand Up @@ -176,12 +173,6 @@ pub fn settings_sandbox_policy(persistent: &SandboxPermissions) -> SandboxPolicy
SandboxPolicy { fs, network }
}

/// Whether agent-run terminal commands should be wrapped in an OS-level
/// sandbox for this process. See module docs for the policy.
pub(crate) fn sandboxing_enabled(cx: &App) -> bool {
cx.has_flag::<SandboxingFeatureFlag>()
}

/// Whether the sandboxed terminal can be exposed for this project.
///
/// The persistent `allow_unsandboxed` setting turns sandboxing off for the
Expand All @@ -193,20 +184,19 @@ pub(crate) fn sandboxing_enabled(cx: &App) -> bool {
/// prompt in place, since the model is still operating in the sandbox model and
/// only escaping individual commands (tracked in `ThreadSandboxGrants`).
pub(crate) fn sandboxing_enabled_for_project(project: &Project, cx: &App) -> bool {
sandboxing_available_for_project(project, cx)
sandboxing_available_for_project(project)
&& !AgentSettings::get_global(cx)
.sandbox_permissions
.allow_unsandboxed
}

/// Whether sandboxing is *applicable* for this project at all — the feature is
/// enabled, the project is local, and the platform has a sandbox integration —
/// independent of the persistent `allow_unsandboxed` setting. Used by the UI to
/// distinguish "sandboxing isn't relevant here" (don't show the indicator) from
/// "sandboxing is available but turned off in settings" (show it, struck out).
pub(crate) fn sandboxing_available_for_project(project: &Project, cx: &App) -> bool {
sandboxing_enabled(cx)
&& project.is_local()
/// Whether sandboxing is *applicable* for this project at all — the project is
/// local and the platform has a sandbox integration — independent of the
/// persistent `allow_unsandboxed` setting. Used by the UI to distinguish
/// "sandboxing isn't relevant here" (don't show the indicator) from "sandboxing
/// is available but turned off in settings" (show it, struck out).
pub(crate) fn sandboxing_available_for_project(project: &Project) -> bool {
project.is_local()
&& cfg!(any(
target_os = "macos",
target_os = "linux",
Expand Down
2 changes: 1 addition & 1 deletion crates/agent/src/templates/system_prompt.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ You can request elevated permissions on individual `terminal` calls:
- `allow_hosts: ["github.com", "*.npmjs.org"]` — allow outbound HTTP/HTTPS to specific hosts (exact hostnames or leading-`*.` subdomain wildcards; no IP literals). Prefer this whenever you know which hosts the command needs.
- `allow_all_hosts: true` — lift the network restriction entirely: outbound access to any host over any protocol, so SSH, FTP, and raw sockets work too (unlike `allow_hosts`, which is HTTP/HTTPS-only). Use only when the specific hosts can't be enumerated up front.
{{/if}}
- `fs_write_paths: ["/abs/or/worktree-relative/path", ...]` — allow writes to specific paths (each directory grants its whole subtree). Prefer this whenever you know which paths the command needs to write. Git metadata paths cannot be requested and will never be made writable while sandboxed.
- `fs_write_paths: ["/abs/or/worktree-relative/path", ...]` — allow writes to specific paths (each directory grants its whole subtree). Prefer this whenever you know which paths the command needs to write. Each path must be an existing directory. To write into a directory that doesn't exist yet, first create it with the `create_directory` tool (which creates it and grants write access to exactly that directory) rather than requesting write access to a broad existing parent. Git metadata paths cannot be requested and will never be made writable while sandboxed.
- `allow_fs_write_all: true` — allow unrestricted filesystem writes except protected Git metadata. Only use this when the specific paths can't be enumerated up front.
- `unsandboxed: true` — run the command with no sandbox at all. Use only when none of the above suffice, including when a command must write Git metadata.

Expand Down
Loading
Loading