From 2ce26d08b51c0f0a8cb1930125ee45e3b293967d Mon Sep 17 00:00:00 2001 From: Evgeniy Kovrov <7254010+nyxene@users.noreply.github.com> Date: Sun, 18 Oct 2026 23:07:50 +0200 Subject: [PATCH] fix: pi starts with its models in a pane set to this folder only pi takes a lock directory beside settings.json, auth.json and models-store.json before it reads them. Its manifest now lists ~/.pi/agent/*.lock as data, so only those lock directories become writable; the files themselves, extensions, skills, prompts, npm packages, install and mcp.json stay read-only. Closes #59 Co-Authored-By: Claude --- plugins/pi/plugin.json | 2 +- src/isolation.rs | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/plugins/pi/plugin.json b/plugins/pi/plugin.json index be3f344..73a489d 100644 --- a/plugins/pi/plugin.json +++ b/plugins/pi/plugin.json @@ -11,7 +11,7 @@ "value": ["--session", "--session-id", "--fork", "--session-dir"], "flag": ["--continue", "-c", "--resume", "-r", "--no-session"] }, - "data": ["~/.pi/agent/sessions"], + "data": ["~/.pi/agent/sessions", "~/.pi/agent/*.lock"], "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/isolation.rs b/src/isolation.rs index 1410338..b7723ce 100644 --- a/src/isolation.rs +++ b/src/isolation.rs @@ -935,6 +935,31 @@ mod tests { let _ = std::fs::remove_dir_all(&d); } + #[test] + fn folder_only_lets_pi_take_its_locks_but_not_change_what_it_reads() { + let d = scratch("pi"); + let home = d.join("home"); + let pi = crate::agents::builtins().into_iter().find(|m| m.name == "pi").unwrap(); + let mut p = places(&d); + p.data = pi.data_paths(&home); + p.roots = pi.dir(&home).into_iter().chain(pi.agent.data.iter().filter_map(|d| crate::agents::data_root(d)).map(|r| home.join(r))).flat_map(|r| [r.with_extension("json"), r]).collect(); + p.hook_files = pi.config(&home).into_iter().collect(); + std::fs::create_dir_all(home.join(".pi/agent/sessions")).unwrap(); + let profile = text(&p, &Files::Folder, &Network::None, &d.join("work"), Path::new("/tmp/none.msock"), None).unwrap(); + let agent = home.join(".pi/agent"); + // pi takes `.lock` as a directory before it reads the file. + for lock in ["settings.json.lock", "auth.json.lock", "models-store.json.lock"] { + let Some(rc) = sandboxed(&profile, &format!("mkdir '{0}' && rmdir '{0}'", agent.join(lock).display())) else { return }; + assert_eq!(rc, 0, "{lock}"); + } + assert_eq!(sandboxed(&profile, &format!("echo x > '{}'", agent.join("sessions/s.jsonl").display())), Some(0), "sessions"); + for no in ["settings.json", "auth.json", "models-store.json", "mcp.json", "AGENTS.md", "extensions/x.ts", "skills/x/SKILL.md", "npm/x/index.js", "install/x", "bin/pi", "x.lock/y"] { + assert_ne!(sandboxed(&profile, &format!("mkdir -p \"$(dirname '{0}')\" 2>/dev/null; echo x > '{0}'", agent.join(no).display())), Some(0), "{no}"); + assert!(!agent.join(no).exists(), "{no}"); + } + let _ = std::fs::remove_dir_all(&d); + } + #[test] fn folder_only_cannot_read_ssh() { let d = scratch("read");