//! The browser tool, end to end with REAL Chromium in the egress-enabled //! browser container: navigate to a real local page, return its text //! tainted `web`, store a viewport screenshot — and prove the §15 chain: //! a gated action AFTER browsing carries the web taint into its approval. use std::process::Command; use std::sync::Arc; use cm_domain::{ AccessPolicy, Agent, AgentId, AgentStatus, Role, User, UserId, Workspace, WorkspaceId, }; use cm_llm::ScriptedProvider; use cm_runtime::{RunEventBody, Runtime, RuntimeConfig, SandboxManager}; use cm_sandbox::DockerDriver; const BROWSER_IMAGE: &str = "clawmates/agent-browser:dev"; fn scenario(url: &str) -> String { format!( r#" [[scenario]] marker = "[[scenario:research]]" [[scenario.turns]] events = [ {{ type = "tool_use", name = "browser.goto", input = {{ url = "{url}" }} }}, ] [[scenario.turns]] events = [ {{ type = "tool_use", name = "email.send", input = {{ to = "ceo@example.com", subject = "Findings", body = "Summary of the page." }} }}, ] [[scenario.turns]] events = [ {{ type = "text", text = "Done." }}, ] "# ) } fn ensure_browser_image() { let exists = Command::new("docker") .args(["image", "inspect", BROWSER_IMAGE]) .output() .expect("docker available") .status .success(); if !exists { let root = env!("CARGO_MANIFEST_DIR"); let status = Command::new("docker") .args([ "build", "-t", BROWSER_IMAGE, &format!("{root}/../../images/agent-browser"), ]) .status() .expect("docker build runs"); assert!(status.success(), "agent-browser image build failed"); } } /// A real page served on all interfaces so the container can reach it. async fn spawn_page_server() -> String { let app = axum::Router::new().route( "/page", axum::routing::get(|| async { axum::response::Html( "
Revenue up 14 percent.
", ) }), ); let listener = tokio::net::TcpListener::bind("0.0.0.0:0").await.unwrap(); let port = listener.local_addr().unwrap().port(); tokio::spawn(async move { axum::serve(listener, app).await.unwrap(); }); format!("http://host.docker.internal:{port}/page") } #[tokio::test] async fn browsing_returns_web_tainted_content_and_taints_later_gated_actions() { ensure_browser_image(); let pool = cm_testkit::test_pool().await; let url = spawn_page_server().await; let ws = Workspace { id: WorkspaceId::new(), name: "Acme".into(), plan: "team".into(), }; cm_db::repo::workspaces::insert(&pool, &ws).await.unwrap(); let owner = User { id: UserId::new(), workspace_id: ws.id, email: format!("{}@acme.test", UserId::new()), role: Role::Owner, display_name: "Owner".into(), created_at: time::OffsetDateTime::UNIX_EPOCH, }; cm_db::repo::users::insert(&pool, &owner).await.unwrap(); let agent = Agent { id: AgentId::new(), workspace_id: ws.id, name: "Scout".into(), job_title: "Analyst".into(), system_prompt: String::new(), avatar: String::new(), accent: String::new(), wallpaper: String::new(), managed_by: owner.id, status: AgentStatus::Online, }; cm_db::repo::agents::insert(&pool, &agent, &AccessPolicy::default()) .await .unwrap(); let driver: Arc