diff --git a/crates/tinycomputer-core/tests/process_inventory.rs b/crates/tinycomputer-core/tests/process_inventory.rs new file mode 100644 index 00000000..8b46d354 --- /dev/null +++ b/crates/tinycomputer-core/tests/process_inventory.rs @@ -0,0 +1,64 @@ +//! Every process spawn in this workspace is listed in +//! `docs/technical/specs/process-containment.md`, so a new one cannot slip in +//! unsandboxable without a documented decision. + +#![allow(clippy::unwrap_used, clippy::expect_used)] + +use std::fs; +use std::path::{Path, PathBuf}; + +fn rust_files(dir: &Path, out: &mut Vec) { + let Ok(entries) = fs::read_dir(dir) else { + return; + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + rust_files(&path, out); + } else if path.extension().is_some_and(|e| e == "rs") { + out.push(path); + } + } +} + +#[test] +fn every_spawning_file_is_documented() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let doc = fs::read_to_string(root.join("docs/technical/specs/process-containment.md")) + .expect("process-containment.md exists"); + let mut files = Vec::new(); + rust_files(&root.join("crates"), &mut files); + let mut undocumented = Vec::new(); + for file in files { + let rel = file + .strip_prefix(&root) + .unwrap() + .to_string_lossy() + .replace('\\', "/"); + if rel.contains("/tests/") + || rel.contains("_tests.rs") + || rel.contains("tinycomputer-examples") + { + continue; + } + let Ok(text) = fs::read_to_string(&file) else { + continue; + }; + let spawns = text.contains("Command::new("); + if !spawns { + continue; + } + // The doc names files as `crates//src/`, sometimes with + // `{a,b}` grouping, so match on the file name within its crate. + let name = file.file_stem().unwrap().to_string_lossy().to_string(); + let krate = rel.split('/').nth(1).unwrap(); + let listed = doc.lines().any(|l| l.contains(krate) && l.contains(&name)); + if !listed { + undocumented.push(rel); + } + } + assert!( + undocumented.is_empty(), + "spawn sites missing from process-containment.md: {undocumented:?}" + ); +} diff --git a/docs/technical/specs/README.md b/docs/technical/specs/README.md index 133a0108..789863ec 100644 --- a/docs/technical/specs/README.md +++ b/docs/technical/specs/README.md @@ -20,6 +20,8 @@ After the specification is accepted, create a linked implementation plan in [`../plans/`](../plans/README.md). Keep code snippets small enough to clarify the contract; production code still belongs under `src/`. +See [`process-containment.md`](process-containment.md) for which spawned processes the host cannot contain. + See [`desktop-module-contract.md`](desktop-module-contract.md) for a complete example, [`jev-intent-flows.md`](jev-intent-flows.md) for the flow runtime, and [`unified-agent.md`](unified-agent.md) for browser and desktop behind one task API, diff --git a/docs/technical/specs/process-containment.md b/docs/technical/specs/process-containment.md new file mode 100644 index 00000000..add5d889 --- /dev/null +++ b/docs/technical/specs/process-containment.md @@ -0,0 +1,49 @@ +# Process containment and egress + +Status: Accepted. Owner: tinycomputer maintainers. +Part of tinyhumansai/openhuman#7328 (phase 5, sandbox coverage). + +## Problem + +A host that sandboxes spawned processes (OpenHuman's tinysecurity and tinybox +work) needs to know which processes this module starts, what network it can +reach, and which of those the host can contain. A process that is silently +skipped looks contained when it is not. + +## Decision + +tinycomputer is a native module loaded into the host process. It drives the +user's real desktop and a real Chrome, so it cannot run inside a jail that +hides those. This module therefore **does not expose a sandbox, proxy or egress +contract**: none of it is enforceable from inside the module, and a contract +that is not enforced is worse than none. The host treats the whole module as +unsandboxed-by-design and gates it at the member level (approval, payment stop, +`POLICY_DENIED`). This document is the explicit list of what is not contained. + +## Process inventory + +`crates/tinycomputer-core/tests/process_inventory.rs` fails if a crate under +`crates/` spawns a process from a file that is not named here. + +| Process | Spawned from | Contract | +| --- | --- | --- | +| Chrome (browser sessions) | agent-browser (`vendor/agent-browser`, `native/cdp/chrome.rs`), started by an explicit `launch` | Unsupported. Runs as the user with their network. agent-browser can take a `proxy` launch option, but this module does not set or forward one, so egress is not enforceable here. Xvfb and certutil helpers are agent-browser's. | +| Cursor overlay helper (`tinycomputer-cursor-overlay`) | `crates/tinycomputer-cursor/src/screen/process.rs` | Unsupported. Draws on screen, reads commands on stdin, opens no network. Optional; absent means no drawn cursor. | +| Accessibility helper (Swift, macOS) | `crates/tinycomputer-accessibility/src/helper/process.rs`, `globe.rs`, `build.rs` | Unsupported. Needs the macOS accessibility grant of the host app, so it cannot be jailed. Opens no network. | +| `osascript`, `open`, `cmd`, `xcrun`, `swiftc` | `crates/tinycomputer-accessibility/src/{focus,paste,permissions}.rs`, `helper/process.rs` | Unsupported. Fixed argv, no model-supplied command line. Local desktop actions only. | +| agent-browser plugins, Lightpanda, Safari/Appium drivers, upgrade | vendor/agent-browser | Not used by this module's launch path; owned by agent-browser. | + +## Egress + +The module's own outbound traffic is the decision model, planner, rescue and +output model calls (the `jev`, `planner` config routes), made from the host +process, plus whatever sites the browser is told to open. Host-side egress +policy applies to the first. The second is user-directed browsing and is not +restricted by the module (`origins` checks classify pages, they do not block +network). + +## Revisit when + +agent-browser or the module can pass a launch-time proxy or network namespace +and the module can verify it took effect. Only then add a contract member, in +`tinycomputer-bus`, with a test that the browser's traffic goes through it.