diff --git a/docs/isolation.md b/docs/isolation.md index 4d544b7..06248f8 100644 --- a/docs/isolation.md +++ b/docs/isolation.md @@ -14,7 +14,7 @@ A pane starts with your access: its program, an agent included, can read and wri | Only these providers | Only the hosts and ports that the agents you choose name in their manifests, through a proxy nerd runs for that pane alone. The agent must honour `HTTPS_PROXY`; one that does not gets no network. Agents whose manifests name different ports cannot share a pane. | | None | No internet and no name lookups. Services on this Mac (`localhost`) stay reachable. | -The hosts in the agents' `plugin.json` are a starting point: check them against what your agent contacts. +The hosts in the agents' `plugin.json` are a starting point: check them against what your agent contacts. pi's names every provider pi ships with; Bedrock outside us-east-1 and eu-central-1, Vertex and Azure use addresses of your own account and are not in it. **This folder only** also keeps one session from planting code or instructions for another. Inside the places it may write, these stay read-only: shell, git and editor startup files, `~/.local/bin` and other folders ahead on `PATH`, agents' settings and memory, Claude Code's task list, Codex's shared databases, Qwen Code's workflow scripts, what agents source or run later from their own folders, and the caches package managers run code from (pnpm, uv, deno, Go, Playwright, Poetry, Yarn, node-gyp, pip, Cypress, SwiftPM, Copilot). When the pane's folder is your home folder, its dot files, `~/Library` and `~/bin` stay read-only too, except `~/Library/Caches` and the agents' session folders; choose a project folder for a real limit. The **Isolation** window lists what stops working. diff --git a/plugins/pi/plugin.json b/plugins/pi/plugin.json index 4eafbfb..be3f344 100644 --- a/plugins/pi/plugin.json +++ b/plugins/pi/plugin.json @@ -12,6 +12,6 @@ "flag": ["--continue", "-c", "--resume", "-r", "--no-session"] }, "data": ["~/.pi/agent/sessions"], - "network": { "hosts": ["api.anthropic.com", "console.anthropic.com", "claude.ai", "api.openai.com", "auth.openai.com", "chatgpt.com", "generativelanguage.googleapis.com"], "ports": [443] } + "network": { "hosts": ["ai-gateway.vercel.sh", "api.ant-ling.com", "api.anthropic.com", "api.business.githubcopilot.com", "api.cerebras.ai", "api.cloudflare.com", "api.deepseek.com", "api.enterprise.githubcopilot.com", "api.fireworks.ai", "api.github.com", "api.githubcopilot.com", "api.groq.com", "api.individual.githubcopilot.com", "api.kimi.com", "api.meta.ai", "api.minimax.io", "api.minimaxi.com", "api.mistral.ai", "api.moonshot.ai", "api.moonshot.cn", "api.openai.com", "api.together.ai", "api.typesafe.ai", "api.x.ai", "api.xiaomimimo.com", "api.z.ai", "auth.kimi.com", "auth.meta.com", "auth.openai.com", "auth.x.ai", "bedrock-runtime.eu-central-1.amazonaws.com", "bedrock-runtime.us-east-1.amazonaws.com", "chatgpt.com", "claude.ai", "console.anthropic.com", "gateway.ai.cloudflare.com", "generativelanguage.googleapis.com", "github.com", "inference.baseten.co", "integrate.api.nvidia.com", "open.bigmodel.cn", "opencode.ai", "openrouter.ai", "platform.claude.com", "radius.pi.dev", "router.huggingface.co", "token-plan-ams.xiaomimimo.com", "token-plan-cn.xiaomimimo.com", "token-plan-sgp.xiaomimimo.com", "token-plan.ap-southeast-1.maas.aliyuncs.com", "token-plan.cn-beijing.maas.aliyuncs.com", "us.openrouter.ai"], "ports": [443] } } } diff --git a/src/agents.rs b/src/agents.rs index bac9514..e2ae87c 100644 --- a/src/agents.rs +++ b/src/agents.rs @@ -77,6 +77,10 @@ pub enum Network { Internet { hosts: Vec, ports: Vec }, } +/// A plugin's or a user's manifest; a built-in agent may name every host of its providers (pi has dozens). +const HOSTS: usize = 16; +const BUILTIN_HOSTS: usize = 64; + const NETWORK_FORMS: &str = r#"permissions.network is "localhost" or {"hosts": [...], "ports": [...]}"#; impl<'de> Deserialize<'de> for Network { @@ -102,8 +106,8 @@ fn network(v: &serde_json::Value) -> Result { hosts.dedup(); ports.sort_unstable(); ports.dedup(); - if hosts.len() > 16 || ports.len() > 8 { - return Err(format!("{NETWORK_FORMS}; at most 16 hosts and 8 ports")); + if hosts.len() > BUILTIN_HOSTS || ports.len() > 8 { + return Err(format!("{NETWORK_FORMS}; at most {HOSTS} hosts and 8 ports")); } Ok(Network::Internet { hosts, ports }) } @@ -578,6 +582,10 @@ pub fn validate(m: &Manifest, others: &[Manifest]) -> Result<(), String> { if crate::slots::BUILTINS.contains(&m.name.as_str()) { return Err(format!("{} is a built-in's name", m.name)); } + let many = |n: &Network| matches!(n, Network::Internet { hosts, .. } if hosts.len() > HOSTS); + if !m.builtin && (many(&m.permissions.network) || many(&a.network)) { + return Err(format!("{NETWORK_FORMS}; at most {HOSTS} hosts and 8 ports")); + } if let Some(o) = others.iter().find(|o| o.name == m.name) { return Err(format!("{} is already taken by {}", m.name, if o.builtin { "a built-in" } else { "another plugin" })); } @@ -1037,11 +1045,15 @@ mod tests { (json!({ "data": ["~/.fake/*/x"] }), "a * only"), (json!({ "data": ["~/.fake/a\"b"] }), "no quote"), (json!({ "network": "localhost" }), "names its hosts"), + (json!({ "network": { "hosts": (0..17).map(|i| format!("h{i}.fake.dev")).collect::>(), "ports": [443] } }), "at most 16 hosts"), ] { let e = refused(bad.clone()); assert!(e.contains(says), "{bad}: {e}"); } assert!(WORDS.contains(&"mail")); + let mut wide = fake(json!({ "network": { "hosts": (0..64).map(|i| format!("h{i}.fake.dev")).collect::>(), "ports": [443] } })); + wide.builtin = true; + assert!(validate(&wide, &builtins()).is_ok(), "a built-in agent may name all its providers' hosts"); } #[test]