Cameron Mcloughlinagent: Re-enable sandbox (#61711)12a19dc3 months agoHistory
376 lines · 14.6 KB · Rust
RawDownload
use anyhow::Result;use gpui::SharedString;use handlebars::Handlebars;use rust_embed::RustEmbed;use serde::Serialize;use std::sync::Arc; #[derive(RustEmbed)]#[folder = "src/templates"]#[include = "*.hbs"]struct Assets; pub struct Templates(Handlebars<'static>); impl Templates {    pub fn new() -> Arc<Self> {        let mut handlebars = Handlebars::new();        handlebars.set_strict_mode(true);        handlebars.register_helper("contains", Box::new(contains));        handlebars.register_embed_templates::<Assets>().unwrap();        Arc::new(Self(handlebars))    }} pub trait Template: Sized {    const TEMPLATE_NAME: &'static str;     fn render(&self, templates: &Templates) -> Result<String>    where        Self: Serialize + Sized,    {        Ok(templates.0.render(Self::TEMPLATE_NAME, self)?)    }} #[derive(Serialize)]pub struct SystemPromptTemplate<'a> {    #[serde(flatten)]    pub project: &'a prompt_store::ProjectContext,    pub available_tools: Vec<SharedString>,    pub model_name: Option<String>,    pub date: String,    /// Contents of the user-global `~/.config/zed/AGENTS.md` file (or the    /// platform equivalent), if present and non-empty.    pub user_agents_md: Option<SharedString>,    /// Whether agent-run terminal commands are wrapped in an OS-level    /// sandbox for this thread. When `true` — and the `terminal` tool is    /// in `available_tools` — the rendered prompt describes the sandbox's    /// read/write/network rules and the per-command flags the model can    /// request to relax them. Otherwise the prompt omits the sandbox    /// section entirely.    pub sandboxing: bool,    /// Whether the host is Linux. The writable-temp story differs by    /// platform (Linux exposes an ephemeral `tmpfs` over `/tmp`; other    /// platforms provide a persistent per-thread `$TMPDIR`), so the sandbox    /// section describes the right one rather than advertising a `$TMPDIR`    /// that doesn't behave as stated.    pub is_linux: bool,    /// Whether sandboxed terminal commands run through WSL on Windows.    pub is_windows: bool,} impl Template for SystemPromptTemplate<'_> {    const TEMPLATE_NAME: &'static str = "system_prompt.hbs";} /// Handlebars helper for checking if an item is in a listfn contains(    h: &handlebars::Helper,    _: &handlebars::Handlebars,    _: &handlebars::Context,    _: &mut handlebars::RenderContext,    out: &mut dyn handlebars::Output,) -> handlebars::HelperResult {    let list = h        .param(0)        .and_then(|v| v.value().as_array())        .ok_or_else(|| {            handlebars::RenderError::new("contains: missing or invalid list parameter")        })?;    let query = h.param(1).map(|v| v.value()).ok_or_else(|| {        handlebars::RenderError::new("contains: missing or invalid query parameter")    })?;     if list.contains(query) {        out.write("true")?;    }     Ok(())} #[cfg(test)]mod tests {    use super::*;     #[test]    fn test_system_prompt_template() {        let project = prompt_store::ProjectContext::default();        let template = SystemPromptTemplate {            project: &project,            available_tools: vec!["echo".into()],            model_name: Some("test-model".to_string()),            date: "2026-01-01".to_string(),            user_agents_md: None,            sandboxing: false,            is_linux: false,            is_windows: false,        };        let templates = Templates::new();        let rendered = template.render(&templates).unwrap();        assert!(rendered.contains("You are the Zed coding agent"));        assert!(rendered.contains("Today's Date: 2026-01-01"));        assert!(rendered.contains("## Fixing Diagnostics"));        assert!(rendered.contains("test-model"));    }     #[test]    fn test_system_prompt_renders_user_agents_md_before_project_rules() {        use prompt_store::{ProjectContext, RulesFileContext, WorktreeContext};        use util::rel_path::RelPath;         let worktrees = vec![WorktreeContext {            root_name: "my-project".to_string(),            abs_path: std::path::Path::new("/tmp/my-project").into(),            rules_file: Some(RulesFileContext {                path_in_worktree: RelPath::from_unix_str("AGENTS.md").unwrap().into(),                text: "project-specific guidance".to_string(),                project_entry_id: 1,            }),        }];        let project = ProjectContext::new(worktrees);        let template = SystemPromptTemplate {            project: &project,            available_tools: vec!["echo".into()],            model_name: Some("test-model".to_string()),            date: "2026-01-01".to_string(),            user_agents_md: Some("always be concise".into()),            sandboxing: false,            is_linux: false,            is_windows: false,        };        let templates = Templates::new();        let rendered = template.render(&templates).unwrap();         assert!(rendered.contains("### Personal `AGENTS.md`"));        assert!(rendered.contains("always be concise"));        assert!(rendered.contains("### Project Rules"));        assert!(rendered.contains("project-specific guidance"));         let personal_idx = rendered.find("### Personal `AGENTS.md`").unwrap();        let project_idx = rendered.find("### Project Rules").unwrap();        assert!(            personal_idx < project_idx,            "personal AGENTS.md should render before project rules so project rules can override it"        );    }     #[test]    fn test_system_prompt_omits_sandbox_section_when_sandboxing_disabled() {        let project = prompt_store::ProjectContext::default();        let template = SystemPromptTemplate {            project: &project,            available_tools: vec!["echo".into()],            model_name: Some("test-model".to_string()),            date: "2026-01-01".to_string(),            user_agents_md: None,            sandboxing: false,            is_linux: false,            is_windows: false,        };        let templates = Templates::new();        let rendered = template.render(&templates).unwrap();        assert!(!rendered.contains("## Terminal sandbox"));        assert!(!rendered.contains("allow_hosts"));    }     #[test]    fn test_system_prompt_renders_sandbox_section_with_worktrees_when_enabled() {        use prompt_store::{ProjectContext, WorktreeContext};         let worktrees = vec![            WorktreeContext {                root_name: "alpha".to_string(),                abs_path: std::path::Path::new("/tmp/alpha").into(),                rules_file: None,            },            WorktreeContext {                root_name: "beta".to_string(),                abs_path: std::path::Path::new("/tmp/beta").into(),                rules_file: None,            },        ];        let project = ProjectContext::new(worktrees);        let template = SystemPromptTemplate {            project: &project,            available_tools: vec!["echo".into(), "terminal".into()],            model_name: Some("test-model".to_string()),            date: "2026-01-01".to_string(),            user_agents_md: None,            sandboxing: true,            is_linux: false,            is_windows: false,        };        let templates = Templates::new();        let rendered = template.render(&templates).unwrap();         assert!(rendered.contains("## Terminal sandbox"));        assert!(rendered.contains("`/tmp/alpha`"));        assert!(rendered.contains("`/tmp/beta`"));        assert!(rendered.contains("allow_hosts"));        assert!(rendered.contains("allow_all_hosts: true"));        assert!(rendered.contains("fs_write_paths"));        assert!(rendered.contains("allow_fs_write_all: true"));        assert!(rendered.contains("unsandboxed: true"));        assert!(rendered.contains("`.git` directories remain protected"));        assert!(rendered.contains("Git metadata writes are never grantable inside the sandbox"));        assert!(rendered.contains("request `unsandboxed: true` with a reason"));        assert!(rendered.contains("git --no-optional-locks status"));        assert!(rendered.contains("for the rest of the thread"));        // macOS tolerates granting a not-yet-existing path, so the        // existing-directory requirement must not be stated there; the        // `create_directory` flow is the preferred guidance instead.        assert!(!rendered.contains("Each path must be an existing directory"));        assert!(rendered.contains("first create it with the `create_directory` tool"));    }     #[test]    fn test_system_prompt_linux_sandbox_section_omits_tmpdir() {        use prompt_store::{ProjectContext, WorktreeContext};         let worktrees = vec![WorktreeContext {            root_name: "alpha".to_string(),            abs_path: std::path::Path::new("/tmp/alpha").into(),            rules_file: None,        }];        let project = ProjectContext::new(worktrees);        let template = SystemPromptTemplate {            project: &project,            available_tools: vec!["echo".into(), "terminal".into()],            model_name: Some("test-model".to_string()),            date: "2026-01-01".to_string(),            user_agents_md: None,            sandboxing: true,            is_linux: true,            is_windows: false,        };        let templates = Templates::new();        let rendered = template.render(&templates).unwrap();         assert!(rendered.contains("## Terminal sandbox"));        // On Linux we must not advertise the special persistent `$TMPDIR`.        assert!(!rendered.contains("$TMPDIR"));        assert!(rendered.contains("`/tmp` is writable"));        assert!(rendered.contains("`/tmp/alpha`"));        // Linux write grants must already exist (bwrap binds existing paths).        assert!(rendered.contains("Each path must be an existing directory"));        assert!(rendered.contains("first create it with the `create_directory` tool"));    }     #[test]    fn test_system_prompt_windows_sandbox_section_rejects_host_specific_network() {        use prompt_store::{ProjectContext, WorktreeContext};         let worktrees = vec![WorktreeContext {            root_name: "alpha".to_string(),            abs_path: std::path::Path::new("C:/Users/me/project").into(),            rules_file: None,        }];        let project = ProjectContext::new(worktrees);        let template = SystemPromptTemplate {            project: &project,            available_tools: vec!["echo".into(), "terminal".into()],            model_name: Some("test-model".to_string()),            date: "2026-01-01".to_string(),            user_agents_md: None,            sandboxing: true,            is_linux: false,            is_windows: true,        };        let templates = Templates::new();        let rendered = template.render(&templates).unwrap();         assert!(rendered.contains("commands run inside WSL under Bubblewrap"));        assert!(rendered.contains("Protected Git metadata remains read-only"));        assert!(rendered.contains("do not use this on Windows"));        assert!(rendered.contains("such requests are rejected"));        assert!(rendered.contains("allow_all_hosts: true"));        assert!(rendered.contains("git --no-optional-locks status"));        // Out-of-project `create_directory` grants aren't supported on Windows,        // so the prompt must not recommend that flow; it suggests granting the        // nearest existing parent instead.        assert!(rendered.contains("Each path must be an existing directory"));        assert!(rendered.contains("nearest existing parent directory"));        assert!(!rendered.contains("first create it with the `create_directory` tool"));    }     #[test]    fn test_system_prompt_sandbox_section_handles_zero_worktrees() {        let project = prompt_store::ProjectContext::default();        let template = SystemPromptTemplate {            project: &project,            available_tools: vec!["echo".into(), "terminal".into()],            model_name: Some("test-model".to_string()),            date: "2026-01-01".to_string(),            user_agents_md: None,            sandboxing: true,            is_linux: false,            is_windows: false,        };        let templates = Templates::new();        let rendered = template.render(&templates).unwrap();         assert!(rendered.contains("## Terminal sandbox"));        assert!(rendered.contains("No project directories are currently writable"));    }     #[test]    fn test_system_prompt_omits_sandbox_section_when_terminal_tool_unavailable() {        // A profile can disable the terminal tool entirely; the prompt must not        // describe a sandboxed `terminal` tool the model doesn't have.        let project = prompt_store::ProjectContext::default();        let template = SystemPromptTemplate {            project: &project,            available_tools: vec!["echo".into()],            model_name: Some("test-model".to_string()),            date: "2026-01-01".to_string(),            user_agents_md: None,            sandboxing: true,            is_linux: false,            is_windows: false,        };        let templates = Templates::new();        let rendered = template.render(&templates).unwrap();         assert!(!rendered.contains("## Terminal sandbox"));        assert!(!rendered.contains("allow_hosts"));    }     #[test]    fn test_system_prompt_omits_user_agents_md_section_when_absent() {        let project = prompt_store::ProjectContext::default();        let template = SystemPromptTemplate {            project: &project,            available_tools: vec!["echo".into()],            model_name: Some("test-model".to_string()),            date: "2026-01-01".to_string(),            user_agents_md: None,            sandboxing: false,            is_linux: false,            is_windows: false,        };        let templates = Templates::new();        let rendered = template.render(&templates).unwrap();        assert!(!rendered.contains("### Personal `AGENTS.md`"));    }     #[test]    fn test_system_prompt_does_not_render_legacy_zed_rules_section() {        let project = prompt_store::ProjectContext::default();        let template = SystemPromptTemplate {            project: &project,            available_tools: vec!["echo".into()],            model_name: Some("test-model".to_string()),            date: "2026-01-01".to_string(),            user_agents_md: None,            sandboxing: false,            is_linux: false,            is_windows: false,        };        let templates = Templates::new();        let rendered = template.render(&templates).unwrap();         assert!(!rendered.contains("The user has specified the following rules"));        assert!(!rendered.contains("Rules title:"));    }}