P6: browser.goto — real Chromium browsing with live web taint
- SandboxSpec gains an egress flag (default false — the kernel suite
still proves zero-network for agent sandboxes). Egress-enabled
containers exist ONLY for the browser: no credentials, no broker
route, bridge network with host-gateway alias for local test pages
- images/agent-browser: Alpine Chromium, uid 10001, setuid bits
stripped — same non-root hardening as agent-base
- browser.goto tool: headless chromium --dump-dom in the agent's
browser container; HTML stripped to readable text (4k cap) and
returned with output_taint=web; viewport screenshot captured,
base64'd out of the container, stored in the blob store
- Taint semantics tightened: the step that PRODUCED untrusted output
now carries its own taint (recorded before the step row), not just
later steps — chat.inbox test updated to the stricter §15 reading
- GET /api/claws/{id}/browser/viewport.png serves the latest capture;
BrowserApp polls it and renders the live viewport (spec §7.1),
keeping the empty state until the agent has browsed
- Proven end to end with REAL Chromium against a REAL local page:
content 'Revenue up 14 percent' returned tainted web; the gated
email.send that follows carries 'web' in its approval taint_sources
(untrusted content can never quietly reach outward); screenshot
verified by PNG magic bytes
152 Rust tests + 63 frontend + 27 Playwright journeys.
Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
7392ce1d08
commit
4f253bec93
@@ -85,19 +85,29 @@ async fn run() -> Result<(), String> {
|
||||
};
|
||||
// Environment tools need a container engine; absence is tolerated
|
||||
// (shell.exec reports it per-call) so the API still serves.
|
||||
let sandboxes = if config.sandbox.enabled {
|
||||
let (sandboxes, browser) = if config.sandbox.enabled {
|
||||
match tc_sandbox::DockerDriver::connect() {
|
||||
Ok(driver) => Some(std::sync::Arc::new(tc_runtime::SandboxManager::new(
|
||||
std::sync::Arc::new(driver),
|
||||
&config.sandbox.image,
|
||||
))),
|
||||
Ok(driver) => {
|
||||
let driver: std::sync::Arc<dyn tc_sandbox::SandboxDriver> =
|
||||
std::sync::Arc::new(driver);
|
||||
(
|
||||
Some(std::sync::Arc::new(tc_runtime::SandboxManager::new(
|
||||
driver.clone(),
|
||||
&config.sandbox.image,
|
||||
))),
|
||||
Some(std::sync::Arc::new(
|
||||
tc_runtime::SandboxManager::new(driver, &config.sandbox.browser_image)
|
||||
.with_egress(),
|
||||
)),
|
||||
)
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!("teamclaw-server: sandbox engine unavailable: {error}");
|
||||
None
|
||||
(None, None)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
(None, None)
|
||||
};
|
||||
let runtime = Runtime::with_blob_store(
|
||||
pool.clone(),
|
||||
@@ -108,6 +118,7 @@ async fn run() -> Result<(), String> {
|
||||
broker_socket: Some(PathBuf::from(&config.broker.socket_path)),
|
||||
slack_base_url: config.slack.base_url.clone(),
|
||||
sandboxes,
|
||||
browser,
|
||||
},
|
||||
blob,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user