From e2559809679c9b5601d9f369abe049a211cd5d12 Mon Sep 17 00:00:00 2001 From: Sebastian Maniak Date: Fri, 18 Sep 2026 17:13:35 -0400 Subject: [PATCH 01/12] Add Pi discovery and managed gateway configuration Detect the pi.dev coding-agent harness (@earendil-works/pi-coding-agent), inventory MCP servers and skills, and reconcile user-level models.json and settings.json so Pi can use the LLM gateway with a request-time credential helper. --- README.md | 1 + crates/agent/src/daemon.rs | 57 ++++ crates/agent/src/provider/README.md | 31 +- crates/agent/src/provider/mod.rs | 1 + crates/agent/src/provider/pi/discovery.rs | 249 ++++++++++++++++ crates/agent/src/provider/pi/mod.rs | 131 +++++++++ crates/agent/src/provider/pi/reconcile.rs | 275 ++++++++++++++++++ crates/agent/src/reconcile/mod.rs | 69 ++++- crates/core/src/config.rs | 149 ++++++++++ examples/claude/claude-code.yaml | 2 +- examples/standalone/config.yaml | 3 + frontend/controller/src/types.ts | 3 +- .../src/views/ConfigurationView.tsx | 10 +- frontend/desktop/src/views/ToolsView.tsx | 2 +- frontend/ui/src/assets/pi.svg | 1 + frontend/ui/src/tools.tsx | 3 + schema/daemon-config.json | 72 +++++ schema/daemon-config.md | 9 + 18 files changed, 1050 insertions(+), 18 deletions(-) create mode 100644 crates/agent/src/provider/pi/discovery.rs create mode 100644 crates/agent/src/provider/pi/mod.rs create mode 100644 crates/agent/src/provider/pi/reconcile.rs create mode 100644 frontend/ui/src/assets/pi.svg diff --git a/README.md b/README.md index 83fcc226..749dff13 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ stage. | OpenCode | Yes | Yes | MCP | — | | VS Code | Yes | — | MCP and skills | — | | Grok Build | Yes | System mode | MCP and skills | — | +| Pi | Yes | User mode | MCP and skills | — | > **Don't see your tool?** We're actively expanding this list and would love > your help. [Open an integration request](https://github.com/agentdesktop-dev/agentdesktop/issues/new) diff --git a/crates/agent/src/daemon.rs b/crates/agent/src/daemon.rs index c8cc4262..c5db9816 100644 --- a/crates/agent/src/daemon.rs +++ b/crates/agent/src/daemon.rs @@ -76,6 +76,7 @@ struct ResolvedDaemonArgs { codex: ResolvedToolConfigPath, open_code: ResolvedOpenCodeStartupConfig, grok: ResolvedToolConfigPath, + pi: ResolvedPiStartupConfig, once: bool, dry_run: bool, } @@ -94,6 +95,11 @@ struct ResolvedOpenCodeStartupConfig { plugin: PathBuf, } +struct ResolvedPiStartupConfig { + models: PathBuf, + settings: PathBuf, +} + impl DaemonArgs { fn resolve( self, @@ -150,6 +156,16 @@ impl DaemonArgs { .config .unwrap_or_else(reconcile::default_grok_managed_config_path), }, + pi: ResolvedPiStartupConfig { + models: startup + .pi + .models + .unwrap_or_else(reconcile::default_pi_models_path), + settings: startup + .pi + .settings + .unwrap_or_else(reconcile::default_pi_settings_path), + }, once: self.once || self.dry_run, dry_run: self.dry_run, }); @@ -218,6 +234,14 @@ impl DaemonArgs { .join("managed_config.toml") }), }, + pi: ResolvedPiStartupConfig { + models: startup.pi.models.unwrap_or_else(|| { + crate::provider::pi::user_pi_agent_dir(&home).join("models.json") + }), + settings: startup.pi.settings.unwrap_or_else(|| { + crate::provider::pi::user_pi_agent_dir(&home).join("settings.json") + }), + }, once: self.once || self.dry_run, dry_run: self.dry_run, }) @@ -299,6 +323,8 @@ where args.open_code.config.clone(), args.open_code.plugin.clone(), args.grok.config.clone(), + args.pi.models.clone(), + args.pi.settings.clone(), agentdesktop_client_executable()?, socket.clone(), ); @@ -911,6 +937,37 @@ mod tests { use agentdesktop_core::model::{Agent, Discovery}; use tokio::sync::watch; + #[test] + fn pi_startup_paths_override_defaults_in_both_scopes() { + for user in [false, true] { + let file = tempfile::NamedTempFile::new().unwrap(); + std::fs::write( + file.path(), + format!( + "daemon:\n user: {user}\n pi:\n models: custom/pi-models.json\n settings: custom/pi-settings.json\n" + ), + ) + .unwrap(); + let config = agentdesktop_core::config::load_daemon(file.path()).unwrap(); + let args = super::DaemonArgs { + user: false, + once: false, + dry_run: false, + config: None, + }; + let resolved = args + .resolve( + config.daemon.unwrap(), + file.path().to_path_buf(), + agentdesktop_core::DEFAULT_SOCKET_PATH.into(), + ) + .unwrap(); + assert_eq!(resolved.user, user); + assert_eq!(resolved.pi.models, Path::new("custom/pi-models.json")); + assert_eq!(resolved.pi.settings, Path::new("custom/pi-settings.json")); + } + } + fn discovery(kinds: &[&str]) -> Discovery { Discovery { agents: kinds diff --git a/crates/agent/src/provider/README.md b/crates/agent/src/provider/README.md index e2417f50..14b3358c 100644 --- a/crates/agent/src/provider/README.md +++ b/crates/agent/src/provider/README.md @@ -8,20 +8,27 @@ Grok Build managed configuration requires system mode. Linux and macOS use Grok's own configuration sync can delete or replace the user-level managed file. +Pi stores configuration in `~/.pi/agent` (or `PI_CODING_AGENT_DIR`). `--user` +merges `providers.agentdesktop` into `models.json` and sets the default model +in `settings.json`. MCP inventory comes from `pi-mcp-adapter` config files +(`~/.pi/agent/mcp.json`, `.pi/mcp.json`, `.mcp.json`, and shared +`~/.config/mcp/mcp.json` / `~/.agents/mcp.json`). Host-specific Cursor/Claude +files are not scanned until the adapter imports them into a Pi-owned file. + ✅ Implemented · ◯ Not implemented · — Not applicable -| Feature | [Claude Code](claude_code/) | [Claude Desktop](claude_desktop/) | [Codex](codex/) | [OpenCode](opencode/) | [VS Code](vscode/) | [Ollama](ollama/) | [Cursor](cursor/) | [Grok Build](grok/) | -| --- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | -| Installation and version discovery | ✅ | ✅ | ✅ | ✅ | ✅ | — | ✅ | ✅ | -| Local model discovery | — | — | — | — | — | ✅ | — | — | -| MCP server discovery | ✅ | ✅ | ✅ | ◯ | ✅ | — | ✅ | ✅ | -| Skill discovery | ✅ | ◯ | ✅ | ◯ | ✅ | — | ✅ | ✅ | -| Managed configuration | ✅ | ✅ | ✅ | ✅ | ◯ | ◯ | ◯ | ✅ | -| LLM gateway routing | ✅ | ✅ | ✅ | ✅ | ◯ | — | ◯ | ✅ | -| Gateway credentials | ✅ | ✅ | ✅ | ✅ | ◯ | — | ◯ | ✅ | -| Sandbox configuration | ✅ | ◯ | ✅ | ◯ | ◯ | — | ◯ | ◯ | -| Tool-use telemetry | ✅ | ◯ | ◯ | ◯ | ◯ | — | ◯ | ◯ | -| Session-start telemetry | ✅ | ◯ | ◯ | ◯ | ◯ | — | ◯ | ◯ | +| Feature | [Claude Code](claude_code/) | [Claude Desktop](claude_desktop/) | [Codex](codex/) | [OpenCode](opencode/) | [VS Code](vscode/) | [Ollama](ollama/) | [Cursor](cursor/) | [Grok Build](grok/) | [Pi](pi/) | +| --- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | +| Installation and version discovery | ✅ | ✅ | ✅ | ✅ | ✅ | — | ✅ | ✅ | ✅ | +| Local model discovery | — | — | — | — | — | ✅ | — | — | — | +| MCP server discovery | ✅ | ✅ | ✅ | ◯ | ✅ | — | ✅ | ✅ | ✅ | +| Skill discovery | ✅ | ◯ | ✅ | ◯ | ✅ | — | ✅ | ✅ | ✅ | +| Managed configuration | ✅ | ✅ | ✅ | ✅ | ◯ | ◯ | ◯ | ✅ | ✅ | +| LLM gateway routing | ✅ | ✅ | ✅ | ✅ | ◯ | — | ◯ | ✅ | ✅ | +| Gateway credentials | ✅ | ✅ | ✅ | ✅ | ◯ | — | ◯ | ✅ | ✅ | +| Sandbox configuration | ✅ | ◯ | ✅ | ◯ | ◯ | — | ◯ | ◯ | ◯ | +| Tool-use telemetry | ✅ | ◯ | ◯ | ◯ | ◯ | — | ◯ | ◯ | ◯ | +| Session-start telemetry | ✅ | ◯ | ◯ | ◯ | ◯ | — | ◯ | ◯ | ◯ | - ◯ describes a gap in Agentdesktop; it does not mean the upstream provider cannot support the feature. diff --git a/crates/agent/src/provider/mod.rs b/crates/agent/src/provider/mod.rs index ebd28c4c..a1e6af30 100644 --- a/crates/agent/src/provider/mod.rs +++ b/crates/agent/src/provider/mod.rs @@ -17,6 +17,7 @@ mod json_merge; mod metadata; pub mod ollama; pub mod opencode; +pub mod pi; pub(crate) mod shared; pub mod vscode; diff --git a/crates/agent/src/provider/pi/discovery.rs b/crates/agent/src/provider/pi/discovery.rs new file mode 100644 index 00000000..91932b3b --- /dev/null +++ b/crates/agent/src/provider/pi/discovery.rs @@ -0,0 +1,249 @@ +use std::{ + collections::BTreeSet, + path::{Path, PathBuf}, +}; + +use agentdesktop_core::model::{Agent, McpServer}; + +use crate::provider::{metadata, vscode::discovery as vscode}; + +use super::Pi; + +pub(super) fn discover() -> Option { + let executable = metadata::find_all_in_path("pi") + .into_iter() + .chain( + executable_candidates() + .into_iter() + .filter(|candidate| candidate.is_file()), + ) + .find(|candidate| is_pi(candidate))?; + Some(Agent { + version: package_version(&executable), + executable, + kind: Pi::ID.to_owned(), + mcp_servers: discover_mcp_servers(), + skills: metadata::discover_skills(skill_roots()), + }) +} + +fn executable_candidates() -> Vec { + let mut candidates = BTreeSet::new(); + for home in metadata::user_home_dirs() { + candidates.insert(home.join(".local/bin/pi")); + candidates.insert(home.join(".npm-global/bin/pi")); + #[cfg(windows)] + { + candidates.insert(home.join(".local/bin/pi.exe")); + candidates.insert(home.join("AppData/Roaming/npm/pi.cmd")); + candidates.insert(home.join("AppData/Roaming/npm/pi.exe")); + candidates.insert(home.join("AppData/Local/pnpm/pi.exe")); + } + } + #[cfg(target_os = "macos")] + candidates.extend([ + PathBuf::from("/opt/homebrew/bin/pi"), + PathBuf::from("/usr/local/bin/pi"), + ]); + #[cfg(target_os = "linux")] + candidates.extend([ + PathBuf::from("/usr/bin/pi"), + PathBuf::from("/usr/local/bin/pi"), + ]); + candidates.into_iter().collect() +} + +fn is_pi(executable: &Path) -> bool { + package_manifest(executable).is_some() +} + +fn package_version(executable: &Path) -> Option { + let manifest = package_manifest(executable)?; + metadata::json_package_version(&manifest, Pi::PACKAGE_NAME) +} + +fn package_manifest(executable: &Path) -> Option { + let mut seen = BTreeSet::new(); + for start in [ + Some(executable.to_path_buf()), + executable.canonicalize().ok(), + ] + .into_iter() + .flatten() + { + let mut directory = start.parent()?.to_path_buf(); + for _ in 0..8 { + let manifest = directory.join("package.json"); + if seen.insert(manifest.clone()) + && metadata::json_package_version(&manifest, Pi::PACKAGE_NAME).is_some() + { + return Some(manifest); + } + if !directory.pop() { + break; + } + } + } + None +} + +fn discover_mcp_servers() -> Vec { + mcp_config_paths() + .into_iter() + .flat_map(|path| vscode::mcp_servers_from_json(&path)) + .collect() +} + +/// Files `pi-mcp-adapter` loads as normal MCP config. +/// +/// Host-specific Cursor/Claude/Codex files are not included: the adapter only +/// imports those after `/mcp setup` writes them into a Pi-owned file. +fn mcp_config_paths() -> Vec { + let mut paths = BTreeSet::new(); + for home in metadata::user_home_dirs() { + let agent_dir = std::env::var_os("PI_CODING_AGENT_DIR") + .filter(|value| !value.is_empty()) + .map(PathBuf::from) + .unwrap_or_else(|| home.join(".pi/agent")); + paths.insert(agent_dir.join("mcp.json")); + paths.insert(home.join(".config/mcp/mcp.json")); + paths.insert(home.join(".agents/mcp.json")); + paths.insert(home.join(".agents/mcp/mcp.json")); + } + paths.extend(metadata::current_dir_ancestors(Path::new(".mcp.json"))); + paths.extend(metadata::current_dir_ancestors(Path::new(".pi/mcp.json"))); + paths.into_iter().collect() +} + +fn skill_roots() -> Vec { + let mut roots = BTreeSet::new(); + for home in metadata::user_home_dirs() { + let agent_dir = std::env::var_os("PI_CODING_AGENT_DIR") + .filter(|value| !value.is_empty()) + .map(PathBuf::from) + .unwrap_or_else(|| home.join(".pi/agent")); + roots.insert(agent_dir.join("skills")); + roots.insert(home.join(".agents/skills")); + } + roots.extend(metadata::current_dir_ancestors(Path::new(".pi/skills"))); + roots.extend(metadata::current_dir_ancestors(Path::new(".agents/skills"))); + roots.into_iter().collect() +} + +#[cfg(test)] +mod tests { + use std::{ + fs, + path::{Path, PathBuf}, + }; + + use serde_json::json; + + use super::{is_pi, mcp_config_paths, package_version}; + use crate::provider::vscode::discovery::mcp_servers_from_value; + + #[test] + fn accepts_earendil_pi_package_and_reads_version() { + let root = temporary("pi-package"); + let package = root.join("node_modules/@earendil-works/pi-coding-agent"); + let bundle = package.join("dist/bundle"); + fs::create_dir_all(&bundle).unwrap(); + fs::write( + package.join("package.json"), + r#"{"name":"@earendil-works/pi-coding-agent","version":"0.85.1"}"#, + ) + .unwrap(); + let executable = bundle.join("cli.js"); + fs::write(&executable, "#!/usr/bin/env node\n").unwrap(); + + assert!(is_pi(&executable)); + assert_eq!(package_version(&executable).as_deref(), Some("0.85.1")); + + let _ = fs::remove_dir_all(root); + } + + #[test] + fn rejects_an_unrelated_pi_binary() { + let root = temporary("pi-unrelated"); + fs::create_dir_all(&root).unwrap(); + fs::write( + root.join("package.json"), + r#"{"name":"raspberry-pi-tools","version":"1.0.0"}"#, + ) + .unwrap(); + let executable = root.join("pi"); + fs::write(&executable, "#!/bin/sh\n").unwrap(); + + assert!(!is_pi(&executable)); + assert_eq!(package_version(&executable), None); + + let _ = fs::remove_dir_all(root); + } + + #[test] + fn reads_adapter_servers_without_secrets_or_arguments() { + let servers = mcp_servers_from_value( + &json!({ + "mcpServers": { + "github": { + "url": "https://api.githubcopilot.com/mcp", + "headers": { "Authorization": "secret" } + }, + "chrome": { + "command": "npx", + "args": ["-y", "chrome-devtools-mcp@1.6.0"], + "env": { "TOKEN": "secret" } + }, + "skipped": { + "command": "npx", + "disabled": true + } + } + }), + Path::new("/home/tester/.pi/agent/mcp.json"), + ); + + assert_eq!(servers.len(), 3); + assert_eq!(servers[0].name, "chrome"); + assert_eq!(servers[0].transport, "stdio"); + assert_eq!(servers[0].command.as_deref(), Some("npx")); + assert!(servers[0].enabled); + assert_eq!(servers[1].name, "github"); + assert_eq!(servers[1].transport, "http"); + assert_eq!( + servers[1].url.as_deref(), + Some("https://api.githubcopilot.com/mcp") + ); + assert!(!servers[2].enabled); + assert!( + servers + .iter() + .all(|server| server.source.ends_with("mcp.json")) + ); + } + + #[test] + fn scans_pi_owned_and_shared_mcp_files() { + let paths = mcp_config_paths(); + assert!( + paths + .iter() + .any(|path| path.ends_with(std::path::Path::new(".pi/agent/mcp.json"))) + ); + assert!( + paths + .iter() + .any(|path| path.ends_with(std::path::Path::new(".config/mcp/mcp.json"))) + ); + assert!(paths.iter().any(|path| path.ends_with(".mcp.json"))); + assert!( + paths + .iter() + .any(|path| path.ends_with(std::path::Path::new(".pi/mcp.json"))) + ); + } + + fn temporary(name: &str) -> PathBuf { + std::env::temp_dir().join(format!("agentdesktop-{name}-{}", std::process::id())) + } +} diff --git a/crates/agent/src/provider/pi/mod.rs b/crates/agent/src/provider/pi/mod.rs new file mode 100644 index 00000000..74686560 --- /dev/null +++ b/crates/agent/src/provider/pi/mod.rs @@ -0,0 +1,131 @@ +use std::path::PathBuf; + +use agentdesktop_core::config::DaemonConfig; +use agentdesktop_core::model::Discovery; + +use super::{Provider, ReconcileContext}; +use crate::reconcile::ReconcilePlan; + +pub(super) mod discovery; +mod reconcile; + +#[derive(Clone)] +pub struct Pi { + pub models_path: PathBuf, + pub settings_path: PathBuf, +} + +impl Default for Pi { + fn default() -> Self { + Self { + models_path: default_pi_models_path(), + settings_path: default_pi_settings_path(), + } + } +} + +impl Pi { + pub const ID: &'static str = "pi"; + pub const DISPLAY_NAME: &'static str = "Pi"; + pub const PACKAGE_NAME: &'static str = "@earendil-works/pi-coding-agent"; +} + +#[async_trait::async_trait] +impl Provider for Pi { + async fn discover(&self) -> Discovery { + Discovery { + agents: discovery::discover().into_iter().collect(), + model_runtimes: Vec::new(), + } + } + + fn plan(&self, ctx: &ReconcileContext, config: &DaemonConfig) -> anyhow::Result { + let configured = config.programs.pi.as_ref().map(|provider| { + let gateway = config + .llm_gateway + .as_ref() + .filter(|_| provider.use_llm_gateway); + (provider, gateway) + }); + let plan = ReconcilePlan::default(); + reconcile::plan( + &self.models_path, + &self.settings_path, + &ctx.credential_helper, + &ctx.socket, + configured, + &plan, + )?; + Ok(plan) + } +} + +/// Returns the system-wide Pi models.json path. +pub fn default_pi_models_path() -> PathBuf { + pi_agent_dir(None).join("models.json") +} + +/// Returns the system-wide Pi settings.json path. +pub fn default_pi_settings_path() -> PathBuf { + pi_agent_dir(None).join("settings.json") +} + +pub fn user_pi_agent_dir(home: &std::path::Path) -> PathBuf { + std::env::var_os("PI_CODING_AGENT_DIR") + .filter(|value| !value.is_empty()) + .map(PathBuf::from) + .unwrap_or_else(|| home.join(".pi/agent")) +} + +fn pi_agent_dir(home: Option<&std::path::Path>) -> PathBuf { + if let Some(home) = home { + return user_pi_agent_dir(home); + } + + #[cfg(windows)] + return windows_system_pi_agent_dir( + std::env::var_os("SystemDrive").or_else(|| std::env::var_os("SYSTEMDRIVE")), + ); + + #[cfg(not(windows))] + PathBuf::from("/etc/pi/agent") +} + +#[cfg(any(windows, test))] +fn windows_system_pi_agent_dir(system_drive: Option) -> PathBuf { + let drive = system_drive + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| "C:".into()); + let mut drive = drive + .to_string_lossy() + .trim_end_matches(['\\', '/']) + .to_owned(); + if drive.len() == 1 && drive.as_bytes()[0].is_ascii_alphabetic() { + drive.push(':'); + } + if drive.is_empty() || !drive.ends_with(':') { + drive = "C:".to_owned(); + } + PathBuf::from(format!(r"{drive}\etc\pi\agent")) +} + +#[cfg(test)] +mod tests { + use super::windows_system_pi_agent_dir; + + #[test] + fn windows_system_pi_agent_dir_uses_system_drive_root() { + assert_eq!( + windows_system_pi_agent_dir(Some("D:".into())), + std::path::PathBuf::from(r"D:\etc\pi\agent") + ); + assert_eq!( + windows_system_pi_agent_dir(Some(r"E:\".into())), + std::path::PathBuf::from(r"E:\etc\pi\agent") + ); + assert_eq!( + windows_system_pi_agent_dir(None), + std::path::PathBuf::from(r"C:\etc\pi\agent") + ); + } +} diff --git a/crates/agent/src/provider/pi/reconcile.rs b/crates/agent/src/provider/pi/reconcile.rs new file mode 100644 index 00000000..ad7141ac --- /dev/null +++ b/crates/agent/src/provider/pi/reconcile.rs @@ -0,0 +1,275 @@ +use super::Pi; + +use std::path::Path; + +use agentdesktop_core::config::{LlmGatewayAuthentication, LlmGatewayConfig, PiConfig}; +use anyhow::Context; +use serde_json::{Map, Value, json}; + +use crate::provider::{ + json_merge, + shared::{CommandSpec, deep_merge, render_command, responses_base_url}, +}; +use crate::reconcile::ReconcilePlan; + +const PROVIDER_NAME: &str = "agentdesktop"; +const DEFAULT_API: &str = "anthropic-messages"; + +pub(super) fn plan( + models_path: &Path, + settings_path: &Path, + credential_helper: &Path, + socket: &Path, + config: Option<(&PiConfig, Option<&LlmGatewayConfig>)>, + plan: &ReconcilePlan, +) -> anyhow::Result<()> { + let models_state = json_merge::state_path(models_path); + let settings_state = json_merge::state_path(settings_path); + let Some((config, gateway)) = config else { + json_merge::plan_remove(models_path, &models_state, "models", Pi::DISPLAY_NAME, plan)?; + json_merge::plan_remove( + settings_path, + &settings_state, + "settings", + Pi::DISPLAY_NAME, + plan, + )?; + return Ok(()); + }; + + json_merge::plan_merge( + models_path, + &models_state, + managed_models(config, gateway, credential_helper, socket)?, + false, + "models", + Pi::DISPLAY_NAME, + plan, + )?; + json_merge::plan_merge( + settings_path, + &settings_state, + managed_settings(config, gateway)?, + false, + "settings", + Pi::DISPLAY_NAME, + plan, + )?; + Ok(()) +} + +fn managed_models( + config: &PiConfig, + gateway: Option<&LlmGatewayConfig>, + credential_helper: &Path, + socket: &Path, +) -> anyhow::Result { + let mut settings = serde_json::to_value(&config.managed_config) + .context("serialize Pi pass-through managed configuration")?; + let Some(gateway) = gateway else { + return Ok(settings); + }; + + let default_model = config + .model + .as_deref() + .filter(|model| !model.trim().is_empty()) + .context("Pi gateway configuration has no model")?; + let api = config + .api + .as_deref() + .map(str::trim) + .filter(|api| !api.is_empty()) + .unwrap_or(DEFAULT_API); + let uses_credential_helper = gateway + .authentication + .as_ref() + .is_some_and(LlmGatewayAuthentication::uses_credential_helper); + let api_key = if uses_credential_helper { + format!( + "!{}", + render_command(&CommandSpec::new( + credential_helper, + [ + "--socket", + &socket.to_string_lossy(), + "credential", + "--client-id", + Pi::ID, + ], + )) + ) + } else { + "agentdesktop-managed".to_owned() + }; + + let mut models = Vec::new(); + for (id, mut entry) in catalog_entries(config, default_model) { + if entry.get("id").and_then(Value::as_str).is_none() { + entry.insert("id".to_owned(), json!(id)); + } + models.push(Value::Object(entry)); + } + + let generated = json!({ + "providers": { + (PROVIDER_NAME): { + "baseUrl": gateway_base_url(gateway, api), + "api": api, + "apiKey": api_key, + "authHeader": true, + "models": models, + } + } + }); + deep_merge(&mut settings, generated); + Ok(settings) +} + +fn managed_settings( + config: &PiConfig, + gateway: Option<&LlmGatewayConfig>, +) -> anyhow::Result { + if gateway.is_none() { + return Ok(json!({})); + } + let model = config + .model + .as_deref() + .filter(|model| !model.trim().is_empty()) + .context("Pi gateway configuration has no model")?; + Ok(json!({ + "defaultProvider": PROVIDER_NAME, + "defaultModel": model, + })) +} + +fn catalog_entries(config: &PiConfig, default_model: &str) -> Vec<(String, Map)> { + if config.models.is_empty() { + return vec![(default_model.to_owned(), Map::new())]; + } + config + .models + .iter() + .map(|(id, value)| { + let object = match value { + Value::Object(object) => object.clone(), + _ => Map::new(), + }; + (id.clone(), object) + }) + .collect() +} + +fn gateway_base_url(gateway: &LlmGatewayConfig, api: &str) -> String { + match api { + "openai-completions" | "openai-responses" => responses_base_url(gateway), + _ => gateway.url.as_str().trim_end_matches('/').to_owned(), + } +} + +#[cfg(test)] +mod tests { + use std::path::Path; + + use agentdesktop_core::config::parse_daemon; + + use super::{managed_models, managed_settings}; + + #[test] + fn pass_through_settings_are_merged_with_managed_gateway_values() { + let config = parse_daemon( + r#" +llmGateway: + url: https://gateway.example.com/proxy + authentication: + type: controllerJwt + audience: agentgateway + allowedClientIds: [pi] +programs: + pi: + model: claude-sonnet-4-5 + models: + claude-sonnet-4-5: + name: Company Claude + contextWindow: 200000 + claude-haiku-4-5: + name: Haiku + managedConfig: + providers: + ollama: + baseUrl: http://127.0.0.1:11434/v1 +"#, + ) + .expect("valid daemon configuration"); + let pi = config.programs.pi.as_ref().unwrap(); + let gateway = config.llm_gateway.as_ref().unwrap(); + + let settings = managed_models( + pi, + Some(gateway), + Path::new("/usr/local/bin/agentdesktop"), + Path::new("/run/agentdesktop/agentdesktop.sock"), + ) + .expect("merged settings"); + + assert_eq!( + settings["providers"]["ollama"]["baseUrl"], + "http://127.0.0.1:11434/v1" + ); + let provider = &settings["providers"]["agentdesktop"]; + assert_eq!(provider["baseUrl"], "https://gateway.example.com/proxy"); + assert_eq!(provider["api"], "anthropic-messages"); + assert_eq!(provider["authHeader"], true); + let api_key = provider["apiKey"].as_str().unwrap(); + assert!(api_key.starts_with('!'), "{api_key}"); + #[cfg(unix)] + { + assert!(api_key.contains("credential"), "{api_key}"); + assert!(api_key.contains("--client-id"), "{api_key}"); + assert!(api_key.contains("/usr/local/bin/agentdesktop"), "{api_key}"); + } + let models = provider["models"].as_array().unwrap(); + assert_eq!(models.len(), 2); + assert_eq!(models[0]["id"], "claude-haiku-4-5"); + assert_eq!(models[1]["id"], "claude-sonnet-4-5"); + assert_eq!(models[1]["name"], "Company Claude"); + assert_eq!(models[1]["contextWindow"], 200000); + + let startup = managed_settings(pi, Some(gateway)).expect("startup settings"); + assert_eq!(startup["defaultProvider"], "agentdesktop"); + assert_eq!(startup["defaultModel"], "claude-sonnet-4-5"); + } + + #[test] + fn openai_compatible_gateways_append_v1() { + let config = parse_daemon( + r#" +llmGateway: + url: https://gateway.example.com +programs: + pi: + model: local-model + api: openai-completions +"#, + ) + .expect("valid daemon configuration"); + let pi = config.programs.pi.as_ref().unwrap(); + let gateway = config.llm_gateway.as_ref().unwrap(); + let settings = managed_models( + pi, + Some(gateway), + Path::new("agentdesktop"), + Path::new("/tmp/agentdesktop.sock"), + ) + .expect("merged settings"); + assert_eq!( + settings["providers"]["agentdesktop"]["baseUrl"], + "https://gateway.example.com/v1" + ); + assert_eq!( + settings["providers"]["agentdesktop"]["api"], + "openai-completions" + ); + } +} diff --git a/crates/agent/src/reconcile/mod.rs b/crates/agent/src/reconcile/mod.rs index 64e71621..0205eba2 100644 --- a/crates/agent/src/reconcile/mod.rs +++ b/crates/agent/src/reconcile/mod.rs @@ -4,7 +4,8 @@ pub use plan::ReconcilePlan; use crate::provider::{ Provider, ReconcileContext, claude_code::ClaudeCode, claude_desktop::ClaudeDesktop, - codex::Codex, cursor::Cursor, grok::Grok, ollama::Ollama, opencode::OpenCode, vscode::VsCode, + codex::Codex, cursor::Cursor, grok::Grok, ollama::Ollama, opencode::OpenCode, pi::Pi, + vscode::VsCode, }; use agentdesktop_core::{config::DaemonConfig, model::Discovery}; use serde_json::Value; @@ -25,6 +26,7 @@ pub use crate::provider::{ codex::default_codex_managed_config_path, grok::default_grok_managed_config_path, opencode::{default_open_code_managed_config_path, default_open_code_plugin_path}, + pi::{default_pi_models_path, default_pi_settings_path}, }; #[derive(Clone)] @@ -44,6 +46,8 @@ impl Reconciler { open_code_managed_config_path: PathBuf, open_code_plugin_path: PathBuf, grok_managed_config_path: PathBuf, + pi_models_path: PathBuf, + pi_settings_path: PathBuf, credential_helper: PathBuf, socket: PathBuf, ) -> Self { @@ -73,6 +77,10 @@ impl Reconciler { Box::new(Grok { managed_config_path: grok_managed_config_path, }), + Box::new(Pi { + models_path: pi_models_path, + settings_path: pi_settings_path, + }), Box::new(Ollama), ]), } @@ -274,6 +282,8 @@ programs: root.join("opencode/config.json"), root.join("opencode/plugin.js"), root.join("grok/managed_config.toml"), + root.join("pi/models.json"), + root.join("pi/settings.json"), root.join("bin/agentdesktop"), root.join("agentdesktop.sock"), ); @@ -312,6 +322,8 @@ programs: root.join("opencode/config.json"), root.join("opencode/plugin.js"), root.join("grok/managed_config.toml"), + root.join("pi/models.json"), + root.join("pi/settings.json"), root.join("bin/agentdesktop"), root.join("agentdesktop.sock"), ); @@ -321,6 +333,49 @@ programs: assert!(!root.exists(), "preflight failure must not write any files"); } + #[test] + fn user_mode_reconciles_pi_models_and_settings() { + let root = std::env::temp_dir().join(format!( + "agentdesktop-reconcile-user-pi-{}-{}", + std::process::id(), + rand::random::() + )); + let config = parse_daemon( + r#" +llmGateway: + url: https://gateway.example.com +programs: + pi: + model: claude-sonnet-4-5 +"#, + ) + .unwrap(); + let reconciler = Reconciler::new( + true, + root.join("claude/settings.json"), + root.join("claude-desktop/settings.json"), + root.join("claude-desktop/helper"), + root.join("codex/config.toml"), + root.join("opencode/config.json"), + root.join("opencode/plugin.js"), + root.join("grok/managed_config.toml"), + root.join("pi/models.json"), + root.join("pi/settings.json"), + root.join("bin/agentdesktop"), + root.join("agentdesktop.sock"), + ); + + reconciler + .apply(&config) + .expect("Pi is supported in user mode"); + let models = String::from_utf8(fs::read(root.join("pi/models.json")).unwrap()).unwrap(); + assert!(models.contains("agentdesktop")); + assert!(models.contains("claude-sonnet-4-5")); + let settings = String::from_utf8(fs::read(root.join("pi/settings.json")).unwrap()).unwrap(); + assert!(settings.contains("defaultProvider")); + fs::remove_dir_all(root).unwrap(); + } + #[cfg(windows)] #[test] fn windows_reconciles_system_grok_managed_config() { @@ -367,6 +422,8 @@ programs: root.join("opencode/config.json"), root.join("opencode/plugin.js"), root.join("grok/managed_config.toml"), + root.join("pi/models.json"), + root.join("pi/settings.json"), root.join("bin/agentdesktop"), root.join("agentdesktop.sock"), ); @@ -408,6 +465,8 @@ programs: root.join("opencode/config.json"), root.join("opencode/plugin.js"), root.join("grok/managed_config.toml"), + root.join("pi/models.json"), + root.join("pi/settings.json"), root.join("bin/agentdesktop"), root.join("agentdesktop.sock"), ); @@ -431,7 +490,7 @@ llmGateway: authentication: type: controllerJwt audience: agentgateway - allowedClientIds: [claude-code, claude-desktop, codex, opencode, grok] + allowedClientIds: [claude-code, claude-desktop, codex, opencode, grok, pi] programs: claudeCode: {} claudeDesktop: {} @@ -442,6 +501,8 @@ programs: company-model: {} grok: model: grok-4.6 + pi: + model: claude-sonnet-4-5 "#, ) .unwrap(); @@ -464,6 +525,10 @@ programs: "opencode/config.json", "opencode/plugin.js", "grok/managed_config.toml", + "pi/models.json", + "pi/.models.json.agentdesktop", + "pi/settings.json", + "pi/.settings.json.agentdesktop", ]; let contents: Vec<_> = paths .iter() diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs index 1ff9cb90..e99ba56c 100644 --- a/crates/core/src/config.rs +++ b/crates/core/src/config.rs @@ -76,6 +76,9 @@ pub struct DaemonStartupConfig { /// Grok Build paths. #[serde(default)] pub grok: ToolConfigPath, + /// Pi paths. + #[serde(default)] + pub pi: PiStartupConfig, } /// Local Claude Desktop paths. @@ -114,6 +117,19 @@ pub struct OpenCodeStartupConfig { pub plugin: Option, } +/// Local Pi paths. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct PiStartupConfig { + /// Model catalog file. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub models: Option, + /// Settings file. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub settings: Option, +} + impl Default for DaemonConfig { fn default() -> Self { Self { @@ -481,6 +497,9 @@ pub struct ProgramsConfig { /// Grok Build managed configuration. #[serde(default, skip_serializing_if = "Option::is_none")] pub grok: Option, + /// Pi coding-agent harness managed configuration. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub pi: Option, } impl ProgramsConfig { @@ -490,6 +509,7 @@ impl ProgramsConfig { && self.codex.is_none() && self.open_code.is_none() && self.grok.is_none() + && self.pi.is_none() } } @@ -616,6 +636,39 @@ pub struct GrokConfig { pub managed_config: BTreeMap, } +/// Settings reconciled into Pi's user `models.json` and `settings.json`. +/// +/// Generated `providers.agentdesktop` values take precedence over overlapping +/// `managedConfig` keys. Pi stores configuration under `~/.pi/agent` (or +/// `PI_CODING_AGENT_DIR`), so `--user` is the supported management mode. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct PiConfig { + /// Whether this program uses the top-level LLM gateway. + #[serde(default = "default_true", skip_serializing_if = "is_true")] + pub use_llm_gateway: bool, + /// Model ID selected when pointing Pi at the LLM gateway. + /// + /// This is required when a top-level `llmGateway` is configured. If `models` + /// is empty, agentdesktop creates a catalog entry with this ID. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub model: Option, + /// Extra Pi model objects, keyed by model ID. + /// + /// Each value is an arbitrary Pi `models.json` model object. Generated + /// gateway `baseUrl` and `apiKey` values take precedence. When this map is + /// non-empty, `model` must name one of its keys. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub models: BTreeMap, + /// Pi API dialect for the managed provider. Defaults to `anthropic-messages`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub api: Option, + /// Arbitrary values merged into Pi's `models.json`. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub managed_config: BTreeMap, +} + /// Upstream authentication selected by a managed agent. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] @@ -758,6 +811,9 @@ fn validate_daemon( if programs.grok.is_some() { anyhow::bail!("sandbox is not supported for Grok Build"); } + if programs.pi.is_some() { + anyhow::bail!("sandbox is not supported for Pi"); + } } if let Some(gateway) = llm_gateway { if !matches!(gateway.url.scheme(), "http" | "https") { @@ -901,6 +957,19 @@ fn validate_daemon( anyhow::bail!("Grok Build model {model} is not declared in models"); } } + if let Some(pi) = &programs.pi + && llm_gateway.is_some() + && pi.use_llm_gateway + { + let model = pi + .model + .as_deref() + .filter(|model| !model.trim().is_empty()) + .context("Pi requires model when llmGateway is configured")?; + if !pi.models.is_empty() && !pi.models.contains_key(model) { + anyhow::bail!("Pi model {model} is not declared in models"); + } + } Ok(()) } @@ -952,6 +1021,22 @@ mod tests { assert!(super::parse_local_daemon("daemon: { stateDr: /tmp/device }").is_err()); } + #[test] + fn pi_startup_paths_are_local_only() { + let yaml = "daemon:\n pi:\n models: custom/pi-models.json\n settings: custom/pi-settings.json\n"; + let config = super::parse_local_daemon(yaml).expect("local Pi paths are supported"); + let value = serde_json::to_value(config).unwrap(); + assert_eq!(value["daemon"]["pi"]["models"], "custom/pi-models.json"); + assert_eq!(value["daemon"]["pi"]["settings"], "custom/pi-settings.json"); + assert!( + parse_daemon(yaml) + .unwrap_err() + .to_string() + .contains("only allowed in the local") + ); + assert!(super::parse_local_daemon("daemon: { pi: { model: wrong } }").is_err()); + } + #[test] fn daemon_configuration_supports_local_and_managed_options() { let document = r#" @@ -1301,4 +1386,68 @@ programs: .contains("Grok Build model missing is not declared in models") ); } + + #[test] + fn pi_parses_gateway_catalog_settings() { + let daemon = parse_daemon( + r#" +llmGateway: + url: https://gateway.example.com +programs: + pi: + model: claude-sonnet-4-5 + api: anthropic-messages + models: + claude-sonnet-4-5: + name: Claude Sonnet +"#, + ) + .expect("valid Pi configuration"); + let pi = daemon.programs.pi.expect("Pi program"); + assert!(pi.use_llm_gateway); + assert_eq!(pi.model.as_deref(), Some("claude-sonnet-4-5")); + assert_eq!(pi.api.as_deref(), Some("anthropic-messages")); + assert!(pi.models.contains_key("claude-sonnet-4-5")); + } + + #[test] + fn pi_requires_a_model_when_using_the_gateway() { + let error = parse_daemon( + r#" +llmGateway: + url: https://gateway.example.com +programs: + pi: {} +"#, + ) + .expect_err("Pi without a model should fail"); + + assert!( + error + .to_string() + .contains("Pi requires model when llmGateway is configured") + ); + } + + #[test] + fn pi_requires_a_declared_gateway_model_when_models_are_listed() { + let error = parse_daemon( + r#" +llmGateway: + url: https://gateway.example.com +programs: + pi: + model: missing + models: + available: {} +"#, + ) + .expect_err("undeclared Pi model should fail"); + + assert!( + error + .to_string() + .contains("Pi model missing is not declared in models") + ); + } } diff --git a/examples/claude/claude-code.yaml b/examples/claude/claude-code.yaml index b43aa366..4f504e58 100644 --- a/examples/claude/claude-code.yaml +++ b/examples/claude/claude-code.yaml @@ -4,7 +4,7 @@ llmGateway: authentication: type: controllerJwt audience: "agentgateway" - allowedClientIds: [claude-code, claude-desktop, codex, opencode] + allowedClientIds: [claude-code, claude-desktop, codex, opencode, pi] telemetry: events: diff --git a/examples/standalone/config.yaml b/examples/standalone/config.yaml index dcbad869..351ffa1f 100644 --- a/examples/standalone/config.yaml +++ b/examples/standalone/config.yaml @@ -12,6 +12,9 @@ programs: claudeCode: useLlmGateway: true companyAnnouncements: ["Using the local Agentgateway through Agentdesktop"] + pi: + useLlmGateway: true + model: claude-sonnet-4-5 # Claude desktop cannot run with --user. Uncomment to enable # claudeDesktop: # useLlmGateway: true diff --git a/frontend/controller/src/types.ts b/frontend/controller/src/types.ts index d3bb0aeb..94625be7 100644 --- a/frontend/controller/src/types.ts +++ b/frontend/controller/src/types.ts @@ -74,7 +74,8 @@ export type AgentKind = | "claudeDesktop" | "codex" | "openCode" - | "grok"; + | "grok" + | "pi"; export type AgentDraft = { kind: AgentKind; diff --git a/frontend/controller/src/views/ConfigurationView.tsx b/frontend/controller/src/views/ConfigurationView.tsx index ad4ad3f3..9ef67768 100644 --- a/frontend/controller/src/views/ConfigurationView.tsx +++ b/frontend/controller/src/views/ConfigurationView.tsx @@ -528,12 +528,20 @@ const configurableAgents: Array<{ placeholder: "model: grok-4.6", initialSettings: "model: grok-4.6", }, + { + kind: "pi", + label: "Pi", + iconKind: "pi", + placeholder: "model: claude-sonnet-4-5", + initialSettings: "model: claude-sonnet-4-5", + }, ]; const sandboxUnsupportedAgents = new Set([ "claudeDesktop", "openCode", "grok", + "pi", ]); function daemonConfigYaml(options: { @@ -558,7 +566,7 @@ function daemonConfigYaml(options: { " authentication:", " type: controllerJwt", ` audience: ${yamlString(options.audience)}`, - " allowedClientIds: [claude-code, claude-desktop, codex, opencode, grok]", + " allowedClientIds: [claude-code, claude-desktop, codex, opencode, grok, pi]", ); } lines.push(""); diff --git a/frontend/desktop/src/views/ToolsView.tsx b/frontend/desktop/src/views/ToolsView.tsx index 0f1c702c..99f3f531 100644 --- a/frontend/desktop/src/views/ToolsView.tsx +++ b/frontend/desktop/src/views/ToolsView.tsx @@ -97,7 +97,7 @@ export function ToolsView({ discovery, unavailable }: ToolsViewProps) { No supported tools found Agent Desktop can inventory VS Code, Claude Code, Claude - Desktop, Codex, OpenCode, and Grok Build. + Desktop, Codex, OpenCode, Grok Build, and Pi. diff --git a/frontend/ui/src/assets/pi.svg b/frontend/ui/src/assets/pi.svg new file mode 100644 index 00000000..046fd463 --- /dev/null +++ b/frontend/ui/src/assets/pi.svg @@ -0,0 +1 @@ +Pi diff --git a/frontend/ui/src/tools.tsx b/frontend/ui/src/tools.tsx index 1fbda87e..71292322 100644 --- a/frontend/ui/src/tools.tsx +++ b/frontend/ui/src/tools.tsx @@ -15,6 +15,7 @@ import cursorIcon from "./assets/cursor.svg"; import grokIcon from "./assets/grok.svg"; import ollamaIcon from "./assets/ollama.svg"; import openCodeIcon from "./assets/opencode.svg"; +import piIcon from "./assets/pi.svg"; export interface ToolMcpServer { name: string; @@ -53,6 +54,7 @@ const toolIcons: Record = { "grok-build": grokIcon, cursor: cursorIcon, opencode: openCodeIcon, + pi: piIcon, vscode: copilotIcon, }; @@ -66,6 +68,7 @@ export function friendlyTool(kind: string) { grok: "Grok Build", "grok-build": "Grok Build", opencode: "OpenCode", + pi: "Pi", vscode: "VS Code", cursor: "Cursor", }; diff --git a/schema/daemon-config.json b/schema/daemon-config.json index 7f7bd3e9..bcb49067 100644 --- a/schema/daemon-config.json +++ b/schema/daemon-config.json @@ -208,6 +208,11 @@ "$ref": "#/$defs/OpenCodeStartupConfig", "default": {} }, + "pi": { + "description": "Pi paths.", + "$ref": "#/$defs/PiStartupConfig", + "default": {} + }, "socket": { "description": "Local API Unix socket or Windows named pipe.", "type": [ @@ -412,6 +417,62 @@ }, "additionalProperties": false }, + "PiConfig": { + "description": "Settings reconciled into Pi's user `models.json` and `settings.json`.\n\nGenerated `providers.agentdesktop` values take precedence over overlapping\n`managedConfig` keys. Pi stores configuration under `~/.pi/agent` (or\n`PI_CODING_AGENT_DIR`), so `--user` is the supported management mode.", + "type": "object", + "properties": { + "api": { + "description": "Pi API dialect for the managed provider. Defaults to `anthropic-messages`.", + "type": [ + "string", + "null" + ] + }, + "managedConfig": { + "description": "Arbitrary values merged into Pi's `models.json`.", + "type": "object", + "additionalProperties": true + }, + "model": { + "description": "Model ID selected when pointing Pi at the LLM gateway.\n\nThis is required when a top-level `llmGateway` is configured. If `models`\nis empty, agentdesktop creates a catalog entry with this ID.", + "type": [ + "string", + "null" + ] + }, + "models": { + "description": "Extra Pi model objects, keyed by model ID.\n\nEach value is an arbitrary Pi `models.json` model object. Generated\ngateway `baseUrl` and `apiKey` values take precedence. When this map is\nnon-empty, `model` must name one of its keys.", + "type": "object", + "additionalProperties": true + }, + "useLlmGateway": { + "description": "Whether this program uses the top-level LLM gateway.", + "type": "boolean" + } + }, + "additionalProperties": false + }, + "PiStartupConfig": { + "description": "Local Pi paths.", + "type": "object", + "properties": { + "models": { + "description": "Model catalog file.", + "type": [ + "string", + "null" + ] + }, + "settings": { + "description": "Settings file.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, "ProgramAuthentication": { "description": "Upstream authentication selected by a managed agent.", "oneOf": [ @@ -480,6 +541,17 @@ "type": "null" } ] + }, + "pi": { + "description": "Pi coding-agent harness managed configuration.", + "anyOf": [ + { + "$ref": "#/$defs/PiConfig" + }, + { + "type": "null" + } + ] } }, "additionalProperties": false diff --git a/schema/daemon-config.md b/schema/daemon-config.md index bd98ee58..a0baf07c 100644 --- a/schema/daemon-config.md +++ b/schema/daemon-config.md @@ -20,6 +20,9 @@ |`daemon.openCode`|object|OpenCode paths.| |`daemon.openCode.config`|string|Configuration file.| |`daemon.openCode.plugin`|string|Credential plugin path.| +|`daemon.pi`|object|Pi paths.| +|`daemon.pi.models`|string|Model catalog file.| +|`daemon.pi.settings`|string|Settings file.| |`daemon.socket`|string|Local API Unix socket or Windows named pipe.| |`daemon.stateDir`|string|Persistent daemon state directory.| |`daemon.user`|boolean|Manage the current user’s tool settings instead of system settings.| @@ -56,6 +59,12 @@ |`programs.openCode.model`|string|Model ID selected from `models` when using the LLM gateway.

This is required when a top-level `llmGateway` is configured.| |`programs.openCode.models`|object|Models exposed by the managed LLM gateway provider, keyed by model ID.

Each value is an arbitrary OpenCode model configuration object. At least
one model is required when a top-level `llmGateway` is configured.| |`programs.openCode.useLlmGateway`|boolean|Whether this program uses the top-level LLM gateway.| +|`programs.pi`|object|Pi coding-agent harness managed configuration.| +|`programs.pi.api`|string|Pi API dialect for the managed provider. Defaults to `anthropic-messages`.| +|`programs.pi.managedConfig`|object|Arbitrary values merged into Pi's `models.json`.| +|`programs.pi.model`|string|Model ID selected when pointing Pi at the LLM gateway.

This is required when a top-level `llmGateway` is configured. If `models`
is empty, agentdesktop creates a catalog entry with this ID.| +|`programs.pi.models`|object|Extra Pi model objects, keyed by model ID.

Each value is an arbitrary Pi `models.json` model object. Generated
gateway `baseUrl` and `apiKey` values take precedence. When this map is
non-empty, `model` must name one of its keys.| +|`programs.pi.useLlmGateway`|boolean|Whether this program uses the top-level LLM gateway.| |`sandbox`|object|Local execution sandbox required for managed developer tools.| |`sandbox.filesystem`|object|Filesystem access available to sandboxed commands.| |`sandbox.filesystem.denied`|[]string|Paths sandboxed commands may neither read nor modify.| From 73f8d2f3446c9e7f7f7b604e73a98782bbb3f9a5 Mon Sep 17 00:00:00 2001 From: Sebastian Maniak Date: Fri, 18 Sep 2026 17:16:12 -0400 Subject: [PATCH 02/12] docs: add Pi inventory screenshots for review Local Agent Desktop Tools view of Pi 0.85.1 with MCP and skills. --- docs/screenshots/pi-inventory.png | Bin 0 -> 20661 bytes docs/screenshots/pi-mcp.png | Bin 0 -> 29517 bytes docs/screenshots/pi-skills.png | Bin 0 -> 55953 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/screenshots/pi-inventory.png create mode 100644 docs/screenshots/pi-mcp.png create mode 100644 docs/screenshots/pi-skills.png diff --git a/docs/screenshots/pi-inventory.png b/docs/screenshots/pi-inventory.png new file mode 100644 index 0000000000000000000000000000000000000000..637a82ddf2e60b7b65c5f148fc83b2a5f3e2c9e6 GIT binary patch literal 20661 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 z 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 L>gTe~DWM4fHtd$ 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®Y6 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®>(7gf@CHdbj~2N)-mH!;15*l3k9US-hlCvm^}0 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

@gCb6`%-L; zEtk_?`Kh@yk!ZK@U{2Hj1?zC`i<_ETPSn%L{x`hy|9%1WAH;)-M3B1R;|YSF$_%{2 z#oWnY*q$<)azvHP#O2F=>K5BWn;utbP?{64%WNafOrY2&cKY12kuI(**(lJI$VF4H zkY#O!1)j8Q4USJq*1ieu3GMQ3BSgZAG|P}FdaWBl8+<<)&hTmMg(TSs$}B`GA2oJz zQ9Gr%)pucSTOT;=sB)0 zW4~kDM*6kpxII<38Qv@l9J|`rr*Jd8SBd(TPY6v$RVW;@B-ai$9W=@9j)jAA|Lp6c z1)(MkhY@jS(;NI!kATHtm3vE}sfRw!;{Utd)HZPYONVR^U36xBm7fut{Q8o70p-1Y z6@PH$E3ceLObXYR^b1)Hlk1Jr_PYD1aMFF2N5l(6utE~~8>a;KoA3N2gi@LNZ(Xkz zU3mBZz#O@;asw!E#XViL3G%DT@7oK2gno9P*Km$!wn1{sZW$f_KU9q6iq*~0&^2(y z!EwQ5yAW@1k{X}>D(mh=D9-C9HSmhX@vJOZsbTVozbt3vRzsV8DBt(PDdDx?M!oIc zF`d57;j-|*dtYNApg&xZe%9)6?>ID*F?0jQo0y5}+d^;pmY_kfLybGsa$iPdhko5d zo;G+z7n-!!r6Xi!+&gFEYWn1) zC7nN2U+^obB6i%W9xB11Ksx1bxa)Ux3^iUb1@`YxdeqUL8=;dtc?QZ-hfcz&aPhq4 z6ohC2rn>5^%z<>{LVqh1e)-K@q|RBj$$rFac(#pAcz})G&t~|)=Os?9FaInClOA&5 zeQ-{M54qg zhpO{)_04vNzg*W#oot*R;`tZMt@DL_0v4lHpm9PhIft?_^c%xbP^_)=s`BpKIF^77 z?SZVC$V;3kQqJ#y?!O(EUn!D%+)mZzi)cobNdka|Fqk6nq!(+aw0iT`D`l7n>+@}E z=J?MS-#@4Dv~Ym->b#fDp5DX(lWy$i7^|!$0dsL6m)#M5awK@UalvmyAQ~+8)Ez}| zpv=&3=9~U#RPM(QSU*{9ypx%TA}NM@BON>xN?5-z(cg|J0FST~|^`#ZMb~$oZ zZ19NbVsYXs>J1E_ua{h~~*juW`2ntIut}=}REKk+8joR=W~V zD&7W&_nW+4*d@RTqmu}-vxPVNNZApUBZSM2;M;B8UfapAsuGZ)N3k$`B@Gh&vXcyz zIsDc*eP=zaaUTC5lKldcU!ZPb;x#uOSAM&bew>VN7e1ud?1=S&g6T;`$c{jwukcG@ zAAsVG@P<>P@2{_iWUe&j*QHuPDcMY%B#H~CsdGa`41WId18H+xQO?S))^asX#Puw8 zF)vDn8k|E!qzRgoh5~JPz5}C6IC6iuW5Y0Iy7ayK?0!+_%+$BoiJ&?e&>!eb!JG0q zgfIt@FVof}izDFAzDCTE4icGI-zaeylH(&LE4H1o4k{q~hkf{-a|j75`_>U6pco~y zI`Uu2a^NX7)+%+Nxk3+IbyYNKJ5}>4WwXOBUvzX$tNr(jOo1@6PkL2Zn1wKagwpYS z+JA5Zyziy|;FY-8lE^JIlPFt*A2ydBxAvChd5b>@J1ZGhq-5gbhJ4-EiV@C4F~A~v zbBQ7`fykY9G!ZTc3J8Y4k3)_^-qXDr;c(U{e zi2cSw+DMF14tdQX=ttETx*obe@kwb1ajs$gi57_dyQnh>w4Y-(kxN+{g)}mH>^!TM_)-@k^uKVBdZFtC*z4esM8DN zso!Z~3%JhavFIc;6+R*l-1&=>!mnTcgC(>qe3yWz8xuI5$XvJ6p|4gK$|enWK^&tw z66gUTrgwa{i#lK^`hld9_}B0e`rPyTAjx4KJGVUex@6JNG0uaJ zp;rV*7-6CYU+GM+5A&7Mq^DCCwZoNvc1!zs#Mp1K?>g;*Y+65X+;(vkgdCd_h4G`S zzw0zEo!xk^xH7ddCGu2EF+&k=GpZxuFw!Fq{CuRVbfmHrZ$;Nn=8o^9-INf-WTk?MJk4Y(JAu+9{hHv(W7y^frnDw@YX1yEP zuA}^dY!S{3jN?>dz(P-4yIquumN^QD$pf@X6J2p33C-zbeF_RchCM-2;MjSf;{!L% zIn~;3ipZDNvu5xh8YGfk+37GfR%GT2nRpwjp@?+W_%v{YNpa7EJYg1Q78ILtnXv!% zM;p9@;ZoD|!)mxvUC>Jz&c=E3W8|Je+0kO7SS(6?5bWM&SiT zVur~j!hZ**Gd@9v&fjp|CWZx91HZ7r`VU@v?;Qi(cyLw~h`A>6myDB!pD4G@1-(41HxDogM;cHf<+a8B(xTVueAx1KV@{Q3=ZiAyJ z*23s%f&bjJ$Rryn^g#)DGabAyNwM_rWG_F?yWbEmsCPQTUjBh93#p7A2RE*CN*?UG zUyQ}&009&j!7i1#gI_rwn7#l}cKun4*FKysNNfN8u~2E%UF9}{(HE2;8fLFHpK9<@ z@&UMg#b^2d-_w zVh==4_Y4deJGgZX|M0daih#I7WTbW4XG)4>f0cBZO|*)X&cp*G-M}xjhqknn%(~?FPg@<)$ZDG`px>f0kWf1` z4Kd)9pz7!H@OiE2p|z6ow4@Y z<+ga)rTQMx`(N+1RM(_!y)VY>68S8Mee#rlreSA$o&^i`)SfTd2SiQeJhu?vzj)g_ zd5Ov8XyZ&0laJ_fzS}M{M>14-n9&~^Zt!v1LW)n#1-=LvQBaB~@y2v4@1OB#(f9sN z?X*Mj3|#1%mYt~6P1grhVeF6cC2F*{5SbGNrpOP?V;noX2Kk;F8hD6!$7bb`=(PO& zg)Yj6@w$FQm@~Vv>h;5D28uG5TY!9EHH>Y39slC+g7!x)42!p1kNk10<_D_T#Eye4<#trtJ!%*}%=AR_otllWL8Bw^TUDEP^OmoML8S z@o#T`FMTg}0{icu97`4-0rwO~Al6sSXfel!q&gj9_jB0VfyKMrWzFw!FcM7YjTVor zn+*k-LR45S84c5X@Jv<*8^km0Kx6LNM}!}_j7h%|OOhHxpwV{aYe~wqL=aQSFEDr9YH(UH`b35-Eqt$fur}>h24KBIfU|(~Wa2A(u2!QNCLglbBKhGq~s01tkiAK4`Ym@I@+m_lpuN z14w4qSI3FkptMiodU)MaWq_}@V8qE5mbKj8^0D4SQRC|;r5WB79Vde}`^qyyb08&X z^dcI*pwNR;_nR)uASI~!uX?ScLTXRUE(L7#RECEoWccBc&yLhP`{ocoetu&PjBdmM8It6{?w`|vr9(&ouRadj;+v$TgcJL`W$ZJVqu9Ni|jYyoKL z*H3*KN|X8Ao|s)1zjB;k2FkT31kZmpZ}zw+A+PoL@f|6M{r>hRt=9dA)mqQAtXQ3d zuo68zEXfKrUUVjBO*)k+r;?Ra6w(hsZmN4W)!b$F7oNhlRfo3wWWw2!;vFXz*sh!9byW;8cpl$3xvg=_A!b{6*2W7)q{H_ z+au!suE4Qpq=clTd2Qe5d}XXiYWS{R+H6bEs(>p8{U);G<}+J1&nAj-qTu78j#D(uu4RUZ^kr-W#oz&}(2 z4yu7OcOI=ho?gdR;hb6}=nzXH^i4#NNEH1iORR|MrD+)%HF{t)7MTvF8;XQ3jceOh ziqM==U=ti0+D^(q2L(0g{)FJMD^sw7N}|NsvnYO7%(Vumg zZ`ps*1I_RA9GxvvN*OjBx8iwd<|xpTaPO?BVBI>DVoVtirR`feKcWLoS6oZsWWx*D zz2A7ST?1PmZ`)t zB~mvE=sQ4?KLTt9zJH6?rZCSjD@xnqdUxY_Js^B9rXX7P!`DaXGV&rIR_0e2)GQ7x z;IHPRyyr!;^}(k$2_&m~LuUUeoXR~~xjoIHR!dqe*!J=M_JQ-ZtJzN;yV1m@^%5v} z_<>PrA5@q#<*X)FMKO0lMIx9lb7pA4r`B6by7Ya6ZZF20eCoNyrqHkf8R_l%h8ySN ztXfx=kerMZSV_cA<@xFa@WAz$k0rR{U?5m>4%@D~)UAeHDC15hsPw(~DP76K?2|dK zx)UX!bSOIt=D_7_jdk!c_%WG@`d(aEjD@NCtS#KPq-d`TPy@v3VZERsx)2R8?wr+bV%f@nRTK_!lliE+@L-L2oti{l)z!c2ze;)O(;i$E{;_wnng3w3Z``Vd z!c~WiXg<~V9yMC*LWwA?gz(p(OlnEe_zUJCYtvI>)M4Q`CIY$3efqg350b9hW)D_%t~Pas?0)~LrB*16YvCOzQ!`0C!m5S#7DC}oQr#i}t!E*K_ z5M@)u=6-Y>D>6g2+zazokk5^XEL;xJ2ReeSk*iA@%z8}3kz4G0>v}o7YBCCWX&l#W zCix`g)_&x7L_hr2{d~~jW-#XQa(cxCtCfVU&NJF%x_z;XI9R0)ae4c5r=0nGUq$C) z@}YRSttH#BYA4*LzzK9<;fH3%R?c0EZi z{d#!o=&S283mOy=hHYO!YOU6C$+4H~i(h+0HBzGONb0Q8t2dXbM`rN%Uri{TrIo^d z&CvgbDn9^{?B5j)I7i)Ivb8HC<8)pO+N^D-@$OZv03zma9Icn!8<-HTi$@6$|Lr`G z^+L23_=zJUkM}cGNkWdWQ)5q$hnhrm#Wqc=87ZQ2SaSAKbI%zEx0IvFOKj{u@a6^_ zFmB84_K2?k>)G;_Ur6IK^j@-zpPt{hU~m%Ll6>G!vdlVj0-I;BIu_hn%$lFoTgl5bU+so{qe2OPG z9m8G^3kkl6rkvkjpicf?3GpHI- zWBa?sK_IOguuH05jPfk4J${#Mh9jqTT>E#km%3@70)3|M!e39N&J-piiLYsVS<1ND zsiub`QA`d{)-7>f{%&+)C~}r2{TRr3s_x*WO@JLs$avP;S zi~TO6JlTuAV*>ApU!>*s2=k0c`ck!5f@!b+6%3z9J%p5%N)i0=S9p$;B93S!Y=Yt!Vgj4^6*BSdBHve3gCbK16wdxXC0IcSOWXfc_sB#A!T;Y!o}>() zsS~Tg{3J{yN#hMq(4VMUm9i?YkoV7-fl&n$K~WX>rAR(J--sH@g!j6Qrc81FkM93p z0)7NCa>U#)VpCTAYn{ZBGKdZ9fMGmVa1coPgu%T-70QFv$%e`HAEW`s?sv6XTpssm zaB74$wmG7De7#JM-cM)C*;c_4pqDR6j;N zPC1LM7C&L`;&|nh>r%`;iEz%rmUPp}0iXrBveK9gs>^~mF?dlx{5Edy&9|-svfFr(6 zq;4GOv;+T6{E>TEEnb5kdpV~-r%hF4w(7YoWOvCzDK)Lr*K1#hF!8o~)3N*fzjNy; z_pmyl^lz?5A8J5D@-K;EFAPTM_wcJ!aV8dx8;0>-unaJ*2noBEsu+BGRQJAJS082s zY2Q+SxTNX9l}}xpB!QvJA@J?U0f7v;;D01dnEZJ#uBj@vqLs=5FQ$0^9RYLj`gr=R zP?JD0p)Pb(+Pjg^2Oyl-rP$U}u(8Ab^a9X00{mae?mMP}@*Xe{`LZOT- zkmQ?8dBX5=lEsGpPASjL0b7JYkaU<#y9&ky;LpXMs@!M0@pg;$dQ)QbaO^3$sXOQR z9%(eCjrjhLrF5s~oTuZ#xqv!&QGoTV77dDd1AccTZqRf0HnQbW>NdUBOg00&g3=o( zjO^`g#KCTa^Syta?TS14#^x7Np&e~hEE;RejPkulKrg$aF`H5fo9J!QM@WOMYt{$0 zAU2qq_%T{|`q}SotC%2RE2gl8F|vPhA0=$ieTIr5q{^SP4c0Rrp`dSc!`O-_yb@*- zNU(iY8`}2igsOR#lIH5*9zaTT^#)N`LuVjmlE4pvUIQ9wf9aoeY%1gG}mE ziiDCp8{HKuE*_97^wifl- zp*D9e&9}cQ3%a8c0M-(8Z*Cw3j%H(cv8&bm)!j%B_}jO=_eQ8rN?aI*GL)=KMyRWl z?Z}TFdr{VVBSRFNe-QgfhsBo9Bhz!swp0t7o#4~M9baIW#(3u&DTNL(QZ7ZuUjICw z-2qYtwXWYI=l|<5bKe!OW#^R8*QMg$^}y}nTvurq%~ic${E4+Jy}8<5GJw5UHRe|A zcIsuB;4$;Bt0F`l7W>rxt@GiX)M3}c+V3e-SeatAog0iaBa-%@g?vt4E>8LY*C+w8 zP2gJ>xS^CJB1(KKVG8Me`y*}(<4@3#^Tyy~Cag8X+7iey`_uXa_W*GdFVMIK)=_oD z(f6I0mb@O&^(=e*#~kwUpWerp@zE0fWJArdp+|oRuDoG|+|%UOX!`AE`+qX6@N$9E z7oJuot`=Rv7N z^&TH2w!?m5$3nEuJJhKC6_^Vi#W>V{8it^|MwwVaB|Jry`XBwmHa6E5 zs`QL@N~2U&*(p!m;Ej>Ra@Z$)U=>{rvrau^IltmU8aOQEzf`?N+dHHQ7i2SR5C@?l zf^)Y~b|?41DLgG||AXfNtuXOnFcW#oXx+!OaazR`?CO=ny)<4s3`$d2yN7T!hGNoi zEX%m~S+7H-ZoRDW;03P;T;8lhNIui}Pd%o5N&6$n=MQ>lx%o+&< z*?3swk2Sk9YO$uwv$Wja=sKzV&pCPJ#iaqE6Q8lzNj(vAF8=Vjjq1G_U~jGlRkq4Oe><~r7AYp9T6(63r9@`(aG;HZc{AXX@Vs^e}AJa4)(sRM0?MJ@`>#Gc~ zihK?tGP*V=5uY#TnZvW`b6&GZH5f4dWE~R0cN;lrmBv${t<>U-Zp^2DkVAU`6F}x$ z!N~*7tZWwDcjjGG`|6?GT7c zF3n%DDnSPbtdjmuKyqJ9HnzkB*~%3RCcn=)Wm2Q5)#LfrR=-0D}~V* zvV<&=EhJ8qLH4!mTXsq&jAh0)rXkB1W{ffX9#_BX+xPqY0iWx+uj_uE`+eV^*ZX}3 zwrNE7SMBTxi`?F*1jXZA7T=dO>n2oL|tL*(bL%^6C8Ej`m{vfNUsk}VX7SpIUj3v5VLbKJ4FpbwRr_`u`4wCRz*Qg;_$De`%@l zTc{K9e7879oj4b)>4mX~D*Ta^x7m$W!TPy{vdA8j7^JjK2dgH8-O%*TL@1FRG<7Qo zoHri>U#*gzyXCt&iJ*NONi=NmbSauXr@qfJZ5@@fGF3sLsJji`>dLJm#t&Anw9FIB z`}SRdt5-Iu2GU^L&*0nvf)0H~+c>5tl5_&IgAAIup*HB)u`xN&j1zk8k?VA(l`3X? zpUS^9$66oZJC@gmhenX-kOb*_%a4poum;Pmek)Srs^q-uhFF78P-fAq>0o_l@^H# zG$*uG356mr!u&v+m4Dmq9m0&uBiTy6b;9^m8n3x&lXA`uTIe9@&v0nC$=QrW5;oy( zw#i@GiNdz_94U|3e$>m)-T1}}AFVIUi?9Dmp>0ba40BxQn@-NmNX0mvoGp!rP0>h0 zol^3_F6#bLEWME&R&OP*r2Hk}5C@o&Qy#c~G!q_h9!R(T^+B}}6F5H2uWYsonWXIg zhnx9XW1n(}vV=Aq72E!=zx<($KJ)wW)1uX36h+?(Fp$M(qkt z$$uF-AyvZq7jT0uC3PnHfy5bBM~-`;1>P1u`Rzv?+C_Cw#A2%c!4(s&FO8Qk^7(+Y z3Q7n?_uQ4L*(a}id@26s@y8z;`%aOtNMcki#bx2~hc-$&K7{2ZmiG3=&yhQR3IqBN z#+$5b1Y#=x_x1qE-1I~kH^bWz2+wtELE`ruXv2&5z2gVjJjz{O&HjA}v0s7sNZ|4S z8LzG2hX|=#&DUzpjNjn!}VJ`)gWUT4NI zM%4i}P~~al(+9aeGnzpMtoiqKQlcCYt(rZw8{hDYWY6(3x%=m$MIO{#l4Y;dR1X6A zsEUoTa})CueEP5IBt5gZ=|0XU)oMNx4x>h4uB{m6;f>7ZIPk3@kUZ z%RA&7v-l4D8`zn(vQZlWQ)zvck9BYS+(${t1NzKNA^eV6srl%kt(f#?2EA6B=uXJ1 zRp<8cY{`JlaDi0R^#H_j=va4!#YqD5`y6LiW~cLgpxIu4x8Rzn%-88{BwkvWY@!-v zUr;8Sb)hRQH+q5NsqxDPOS0#KpL;SCwu3L*erV%LCb#L|@SfZsIAXlBT2IUf^KEqq z*`?@UTI@zSxE_p^?BjuCyqh@(_69JvFzrW5qLkms9B4(CPBY?Wvlk%3i)~*55o@n> z`FfsF8*01DZS(7xlpfIFtv~CXN5T`9P3x>-+iTz$zx{cKzrm+y5U>#N5P!;c11rVb{66pv{&%z|YrnY2~EYk~@-Z<)Co(-7Sqq!GM=zKzL+5CO*}xiZ@U4Sg&*n^sdw8be8pp%QO!BMPMstXOtzWJE zG~=2bki?qb6t)d-nUP+4ip(`KJeN|5%t7`3sBp4?(jU`N%8=C)^33uYErjmFqA}8j zs8tq00lr#o7#h9HyBh!FNfXp{QISNZAP=Te2?5tN{2ThVz7xJx%+E4XlEw8h4y;37 z&kiW?<0n5gNx8Isuksw4*P{JXI=Q{bgwWQeFsVwtsft>XM->nUrncznj#J+hR&Z0J z!Xr&-(#r0D zlcW=2szZN+E89QJSoT*&ptN3WH+NOcXmE`cnW$ZJ(&oPV0av$bXu>1wt;ziV6Nfh9 z3l6QdzAmAsylJtDV%-U6-$jT;i2RjJs&M_Ce`n1KGZ;yA!o_shAB||V{TR zJWXYs1)!IEUIhP@b(j8^M7-j?&)MK1T>rMK;{CFnfRDg=a!x~b=$AJ-i~M{MQNK|G z!yKKN)2Ep&^v!D5F@Y=kQ}pvV<#v{?#YMO@khiD^^%w&KF5wSn#k7xb(q2@F>08Ge zHNANYM$*YkZ%u}cL~-SOt?!@lLe;sZM?|Bt@)KcdpGBQkTn4(*ROHfw?Sjjqf6L+R z6jm{S!->7>48L;hqm792UAPfrb-=4!`!q@R;&#VWlUsW*^gcN@41rQd@Xbqh_g#Db!&4}KwEpXyAZb!L>+Ft?G52{62kY_ ztQ`jhycIMc4ZT;`KI8OVc62wAp5?0IuOPbTE~?Ga0fpETP}|4A*~Nw68gI&14|~Qs ztN2Ye3ImoWP#6t@_qw-&NI|r3RpJsd>uIv)g>UNf1=FC;j+(C-Nu}bKISw}1GJLm% zI0^_kXixmgK-yNmR2gWFA0WV7pqtWDpwK<}l+qBDAV)~YO^M=MuUfM|w}&0CGAJ{8 z&L1LC6@f+*rn}tK=N=;xiRCL}rO@)Gop{0R_hPmNXq`UZDCAU%%J;~UP~x~gAi?`r ztW-W)R(;vq95^SpZb7#3vedIuw8*&5I?<;mC|83f$t<^LUv;oH=hpvP@K0cAO(UK1 z3_`trrzs!w{H#M17s_gJ>;8jse%z_t`g|3=&KIxm3tK<RS8Fw>!?w{`cqwvwzQLqg>_ zsb9t8x{`7(M$JREhc~9j;?Q~~Sbd1B_z+h6N^GFO0!PVx#hLsa-G)@p%v1pmQJh5MYD@GvGU}+biOeO$rjN8< zUT?=Ku-`?l$*+zRatBME)C=$I0_Ur=BASD0u6M(m?DLMiD@Kb;Bm7xjlV_j>ScA+$ zsuVc}P{;L6fUB=#;k2dg#{AszsFB_66;WC@O&6r$s+i%nz6^192%XJ)N=qXfyDw17 ztq@3}3;_{xn^fU-InSb~AUd2^&_z+3} z+(s|F^G(hCvpmGmMoz1bms42#EGAsV`))ye9nS;PH32!?cfoGaDDFF&_3R>K)=6Xz z_A@h4SEi|ZqhJp&VXpCg+-~@_O9-1JU}MwX`$=?2OmMwobphotUOr_+_IfF^P|%Wl|*{~uoPkYoS= literal 0 HcmV?d00001 From f2953e1838d4458a6fc70b816ed62ce0027bb1e5 Mon Sep 17 00:00:00 2001 From: Sebastian Maniak Date: Fri, 18 Sep 2026 17:21:50 -0400 Subject: [PATCH 03/12] Include Pi in the controller-managed example config The controller watches examples/claude/claude-code.yaml. Add programs.pi so the Configuration UI shows Pi next to Claude Code/Desktop. --- examples/claude/claude-code.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/claude/claude-code.yaml b/examples/claude/claude-code.yaml index 4f504e58..b37a4a26 100644 --- a/examples/claude/claude-code.yaml +++ b/examples/claude/claude-code.yaml @@ -16,3 +16,6 @@ programs: companyAnnouncements: ["Managed by Agentdesktop"] claudeDesktop: isLocalDevMcpEnabled: true + pi: + useLlmGateway: true + model: claude-sonnet-4-5 From 30d50680d83181b14ff0e323080ee3ddec778757 Mon Sep 17 00:00:00 2001 From: Sebastian Maniak Date: Fri, 18 Sep 2026 17:25:10 -0400 Subject: [PATCH 04/12] docs: add controller Pi configuration screenshot --- docs/screenshots/pi-controller-config.png | Bin 0 -> 28101 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/screenshots/pi-controller-config.png diff --git a/docs/screenshots/pi-controller-config.png b/docs/screenshots/pi-controller-config.png new file mode 100644 index 0000000000000000000000000000000000000000..34bf6170bf9549fcc35196a13f36aef68f7bdcb2 GIT binary patch literal 28101 zcmdSBbyU>d7d}dfAR-7VuOJ~3A|)*i(v76V3@Y6{#K2GrDoWQ3-5t^}gcx+!Fr-Q| zFf>DRKj<6Zb??7-{nov#H8Y<$xz9QK?7g4o@J?G(iToPvH9R~#ausEH9XvdOTs%Dd z3=(4CN$eTJJ>W*wR!&Y^MNW=M+s)a^*1-}F?_soml!h9?vsYvG!Vkgmi?>{wH*%PJxWfjcy^7@FI9)GM+ET`WZZlv*h{=Tm!?)dCt zPC$oCG+`xKZ(668tR=~1sZ$@J^@iQ?uIizwdqYnmf(+yY!8%y!ei8X{zxdUiH;k_z zypd|1?x)bp*r%)FCCvCoJ(h{r}_5e>MJ3&gcK-6c!Qr-<<#R=Kr2k z-`&zp&e;*D(nIRMk@-9G|GxY?qXh3|&;Lgff5iOzDIjO5YZAQwj+xZ8N9B|lJUkgZ z6?s`bZ~XO{tKPR1=ss_Y#Lq%E=~tN)6l_%uwQp81O;iNm{{BArJ##_&ZL*BFSH$o5 z^6KPxttxK{vE29D)tqp5Ux*>25zBtc!%I%f425h~?oU>?%3iy}#Wf(kYZL6wVO&x_jr*Kk3B_qb zKLpe{#|MSXB}^neKYF?Tm+i!%Du;gKhwJ|485aF13ck{N1zd;r5hc%S9#LPMYUF7$@P^EKD`jP6^){9-78RQ)M`wLs_g%)gM%Ha-f%M4jn~SXvNPHx(cL&|E$!cStpWLX@Oicc9 z3Lg)M}4`Cw-U`Ds9GmpjpUj*(ty)PN|6jTH`X>BT74I<89fe&@DSd(K~5H% zm>$qQs&aY{#)!+Vb{I3D1-Zt5VAwBon5}PnQZ2MR)d`Noh|e*T(+UgYx*6S05BY2Z z&OT-x4D4g<%N^gmk6+BSPOkuKNt|WflZNQc26(+)@knd(u{<7CsoYNYJec$l!3Fm- zO2bAcdsCzomf$Dbwc?w6QdcZSml?bAK3*u#v8M^`Yta^qECeZ~MSW3t-(6?+F(xKg zRV}u-Yz@(nceY5FWkHnGVTQdAL0tIT#*HBTdrDYpHB?fd)0xX0Ah7Ts}1 zv_cKtL;*&u63o-Y_*kpWLoZ}XggCcSY)Q7S4pO^_OZtWTF%#vjaW(b^svC{uIodz7sHmuzqw|~=i^d$|d6jM?TB@BV zNVlIYI52x+M5;$xRG0&%#5lqhrb-2SQ=;Kb0 z$R=dmt4QCr+&g*)!k%Gz;R3=xQbnxu?oG7LxofucoA=~>gwW7-ZqC(-v|$tOijc1h z4wmX{@J|_#52m=Q^q1+x+tref-RbziLP z&|1GIcsMLEdz(Y2mcez<*_7~TL_ zp)vP4Lszm$`aKD6nF#o2u>+41hsSNjjrHL$reo+&Kcg0hPp4K`fwLc(_?fy?$fbTU z9EQ8@pn~ycLbX%V-mNjjmOi^>~!Sq%W1@JxOFmk4J#<-jk~Xnp0P-18l8CBqU4? z%AVUrCUbiEK~v3@T^W^XV`S$J(al(*&IX~r6V`SQy-z-;*i=h0!VQHKNC-Fw`s3-Mi`lsfbN zh5zgbq#@irV1>W0$<7KIOX}x^pb$O3wS4>?P9d*3wZk;*azC-dv+eFoT`;W394|Dzbwh8)Ewl-NngM4*PT=;5fOZ z5b`VdM3gdk_yJd_T1<|S`eKD%zrMMOx}MgXqN^M91nM2YmmuMARUfDEW2rXZ?^qPqq-@Sr1w9paO$oCIhv(j*}jZx@opj^~6S9!N9SDbMxAf zKrv`UO3cjd$D;d9X0diiNTI!n6f7XA!~{9H&Wpq?NiD~UMe4G-jAa>t`uu!p_Qxr1 zBDTlMytvONY$f|1+Cl0ROU2mCO$@+3{z)FSa{RxLg_EA^TyH)!KqNclJ6Mi0WpMLa zL-+AT-4-f9KPLOYjq0KUR$=N10?hnhERdL~59PaUUv%4qp5;0CKsR85UnTci+-Ww7 zP%o)jRTFBB%&k+w_I;Y;Wx8{-BVy}M1zLSmb&RFXRst&hF?W)0^6FIAJBE})&5(zl zo$p}ELt==J<5^{PQdWB;cX7M8+R_HM<>=`prLdF*0o3bYVAeAQ%D558(#NfJQC$3> zpSm*nwMkTJt8@Nd^J$3GOh`DAl0M0cduwjEf1ofBW4 zXu0zQ++RGNEa_f!_vv<37nkB>-}DS_&9~d4XaB1l3hOm#i))$Rj5uUH##iWIl0IIj zf;i-Bid!C(*a{zxeCiZ?{SD;dx5*)JD!;i3p%+L0BoxTga5MZgGK-o<23x(87P>QJ zW#)y0O*FE5Zek|vxe@3wER6dYzx?T&?wv0^A$`x6lQ&v#lUIZ%-6uZ&`834t+T~E; z5pjlIV&5xpd z(dy&RI>?S6Z3p$=9Y}5&AK`lqa^A3I=tMXX7nD~~Sgy)x4@jRCnv3dSfk=%3d2~kF74=6cy&lhYn z6gJ_}^a=8^BZ-1q(sy5&!jL{XGsJ9r-FQP#cXPl$)5zRxWB6p`uGb6^Oa6fl*jjlk8aX)*frt@-2 zh*=ZaL*$+>YRu~d8A*cM;V{iXrS7CZios{nM{9O2|kz2f6-)z8f9urCRP@&<+)+=J1|3#9xRCX z@O$|KUqs1xvbGhVXZ=_IXlEiyNo~T-_BF|1JgE`vT^C&#B09bh8L?Z+BC@O7 z=1MGrg6eJyy{R|gw)}qzi~B-c9&zGW?dQDL;Pju$p3D(B;PVr%8UKlyS!~FGc?usld;yz%Vph zjNT*pN5cQd3wnKG9G#G8-IJ#4w$RCaGa5eZCSXhVr-jR`l)%77Fi5g=MAD0vZ=gwn z_8vx+1rKj+k#xn@WqkXdnh zhvboOqm)NL0ehX9A6!6Bl>SI@9h!D1iNzTw?w0m{kwqhbCv|40H#hOIj}ZiHEV>DJ*95i#}kPg)~x0VZ8^ zG@O-R_fG4YF6qBh=*O2U!D0_3U4IpPYnRVgRtZ5B6i`z^#SAK~`l7QO3;$i(g5!~IG{D+$cUV^?Hbp4 zt8MBozF$+_UFze!dgEUGAloO&OVP^EQrgTG)x>d|w2e@aYkI;C!#2GW_=4~VIH*Y` zGe+n5a#UQ8kdbMUP?hZVj}}Y4aEKSDVM)~Ya2ja)C)fdcS*OsAb#*E~vNv5?x=^n2 z#$~UVK3;pRB(I#;sb1^ZXHuKMtK`RQBJ5nfo2!}_ZvvfWMORwZ?LN!TTJUgipp~RX zypfr_1=P2viX_Ic*%x_okT6B@2VJL*v-?~qjjgooxijF9$ODoX;YfYrzVzW6 z!U=Knf#QUQQh=y&%lVQ$2e_#|l~K$iX|zn&1iLw7Vo+)vKRlz$_*-J+B;Qh{xR0qF z8D2}mPLmdT<4q#&Nm-aANJ4LjLe>jF%cE+98hNB7Ih>bu`dX_}_r!opIBDqUVqX~g z>?}o-2;AXtw`w^N#J?_ZIkaMTR=m$NDt-5_om@a0cH$R$qQ8!8qX#{R2zaAj2J38= zK}DLqh{u}y(^sRSC{NOdLntI!j(T+uG|K9~IMuqEpW=B39LUmNuHv9QR+o0cO9!aB z?gLfg@!oAjY-bzqNf(gQ2`g`&VBHP2-(Sv5V1K*c26+nX zG(opJAIsX>@r$R&hAr1?}~C1F=e{k)ohtia@AC>La^Q@ z*V>V(jsKu64m-14#WD3~?B#+2#>=Cdp^#*0HGeVs+BCKYQ3pi)7TQJlh1793=!Ch( z%HPMTNV>D}j75IO|GoHmK7MWz-41xH5Ls(ELOdWV;$*rBw<5qNI;8>To}i%Ms5MQa z)ZT)hf}W0%&Px2K$nzmEecXb6 zkgEz<^3D6bVjcJE+CGW{HqCc}Cx2`YrXSpoeF)In&eTgc>$QV2$W?E%0UDahe=SE| zN$F_48%I2PxnojA`Sv`H{JkFnUpZ+q+E;VC^V(SPt=48_*6;qKX6pYUIG5~l=Zp9z z^`9=J+`2ELy+#=N{f{EP6mFtsV8-6Ckvq#D?R}Y+5wIepUOfLFJ+w+hqRN{pr1bQk zt@8hq1yN$neGsN8oLc9CXdc=(|A>gm*dOt47k6eKK{_sUFP_Xl_XiPy1`)#;;)%iSe8Us_9aZrXN6+ z6#uqFfj~B$LnB&|ws5-2u$+jy0fR+S_nNK4ON=U&$4iU=&Hsf4idv`mN4#5Vff=KZ z--LzrMBSIOnA}_CpR(X%*GOZon}H->t&j`5!H=z*`-Rd#aA`^H$615y)NRHE-IsdP zKy@}lUj#Pnp9*}TX4i-@sP3p>i?;5F_ z(+yRr0l&J~H=4%FpH=0Z?(GiLf+lOz1CA8B6Sz4Q-rxG<2|MyknNbW_-6C!#c5dB` zu)a(E_pFNI1D0%`XG@i}mhD7A^!jYGNz2i(Jt-Mk{E06v;n(+YwGd)*2TFt_6asf3 zsE&Qco@?F_F^t5W6}p8vOx5;bapP68uJi359RnNvPax?X_NbQTl?ik&ysGp4ZBF?x zcD<74&zPB+S#lT3Ta*HtD0IINH?3M71M)Yr#aX)Q=TUbCWU=+5B^x&ybNm5jADyw;u%VHmAdmRlxLuowN5#P3kn`#Vuzh zJ>7OJ;yhLchp??3sb1Z10c-Vkx0r!>(Yw7rze*>Z^gc&NuqBPUR%@c`yvjZJmNS?4 zqPyP1&e7Ho{oMEQ7Ci~vJ6Pm4ysJ46+zQizXY5BkSr%~_l!4B+Mx2a^t;m_TCiUhg z7}waLOY)_j~qndL=m-n=V>LOLYxZ6FIm;u2R&> z{;16DS$MhoLT7b&TMIcixJo?4n7`1S+5>MYD7Rktpq?zIU|eVWK*(-1sz|@M8~CuS zEvTe-45{ZNw9;@_meVrXy;$W!W#SQPJO-7SHW1yppGgW@Z?_Iu?2P`ZSr~?l<22Je zSVuqTh+v3)_5Hn1;~s!XaC;%}j9Z78M%TT%Vpt(nX>4Tv8=j>b{MOQq;GX3NQX;Y! z8p8!I9~9a)9z9@w;E*RGEIeM=&H1-7wxA5kpitFHd%|HexW>|Ggq)VrRV`xucZ12#3EMSYFFiD7lp1DZRu zj6`-2L&7dPjfxGP3+%*8YoUsbxeFtolZ}^^FMbk0(~dunZn8ybMY{#j16@UaPjU0_uC!@z##!)Ior!y7F#akwQm}LF+pIZoDB5vEBHxHhi}1acetF z+JM1-?^&T?c^W;zWApaw*0;+6Js^XidYFb(cu%M!8B||SN!Lpimsi-MVm_Q9GZ@L? zzXZ>>Y&*SjfR8X(lD$W+Ez~6n_zp*~_2*C(L1?}6oD}uMSmJV80C)!>V?Q}+%RfW( z=~yz)47Q#7TIuzadIoWh*ul=vuwMefSo<{#EuoCkxr~n<4FmZT+3hI-W`lVn?c?OiI7d{)4`Afqy`QV2>IZ13|sC8K{i$=Mt0zL z<%-biAMfY_PAkW(Q+d&^#D0Qlzcj;lmcnmWe z9j0Qo)P4=n)X5iKqp2>}j^%FZwZI7OuAF_}*G%5yj1goS!ESwfFGZP){tWCZthHh< zdiH)kIJ;JSM7GouB{Em7)rq0T(*6bZT#QeHEOw`wXeMtXS|-aC5nUeAEV(_Zh)jwL zL~jMO!8#wNzLI2c#%XYZ+mz0)g{ghEP7iPj^+I+CQxBWOT*VikOk-N^1_4*SXR3y=BuMRSuX| zSl;JV2PqGgBZe6_`CokQ8E#TIMX3U;0uJb=BU}k+*Kml0-G~hhfvqvqSE5|o z<42}1<^>7S$BmeE+Jby}xT<{|sQxp%;WkAxy^|lSmrGk}dq^iI#Abpzp>xQx?8Z)5&$BTnq zi{jX)esA|@FD(f4!fS*aF=&PAp(xmx^og$#aNJ?iMZDq{{vA8(>?ELlEvA6P%uoog zJ{cCu!=wQ|BL@xBLyd;JCcn31ix>FhVl!xM28Zs?B82&{XFwE(_Yu3e+~vpt(WKcR z4hmt**Twy!d9{GSeC>?@oHw{!cXQ{KQ+$PQsn|Yi)lt^@3y#HZ!?>q!-V^n#=5+h$ zm4Y9He96r$*m}bMqGjE0a-nvE>KIS7r`>@6A`~W;3EFW5SfiWK+So#Ir+FP z$M=hPsSUv#@z#g=&$29RZ9PHA@XB(v2I@@+ zbiiyXFJ#nya7#zH%e~|hsR!cTxLyGubV8#gLpYMQ(TemIx6mT7coB5{W}L8{Uwrd7 zMCNt*LR3cd(QfV=Le~`!uijVR)s$0@X%gPu_@)A_n}Whtei$y)nFU;zA9=z?Kw`>* zziXH{fP5XAH)*yfwJH4RK7p+Ulbj!`-dPV_PY!9T+HOK=uFWT^%K16R1F`878Aegd z^HWQB>K@EkRqcuvXc()sGiq4b$gX%|ldkBR0l&5kZ! zX@7RI<$;>16)B-APqBmQUWBMV+qr0e6|Z#`04Te;qo!EHyYQS3ty6Dtf_0>h=d{ty z(0oC=VZBSNJ~}#j$(gP^_GUWl#f3*f+Q59x*4)hHkuc{y_?^epD5pG zT)++yk((0-G2NVi*<{@snE7&=P8*XYF#G~?lBvQm8QM)^As<9`IGrd}zq828)Udn2 z=UhEvIT9MM(4E&OL5>o!BUhfIKWJ5 z)hWWxY)kHmx;;^YL`&B`%D!yaeq7{pX+Gnbag`g}s&r%6xPY&m~A38`=MbK&G!(>tp)u8jbfKN{l^=(%(3 z8QD1XO^~3Cr2;Y~2yZ$yf;UzMyoa&zZt^yA1E7A)9g^b=y(-W%wuPFo%dVKF=8P8&(?W%+YKoxw+3`iyhe35lg7|R$h*ef86%7G0+Ps2gpixAt3T! zDLT1pmXK6p>e4cMUL$^MzGagEBW_)Gt%{COJV~>bQ;3gSN6OZ6xp2jY@bFk^*hv?$ zjiW|jnbBPEn;3?e42);;!&rS#?Vht}ML?@$9#&1jg*LFpDWDmMr>b4MeeT>_n%`!X z^`Q$0q}6F4>D0BK_afrB^c53$L0pR(2ihUu=Q|?1qQ8dadcfU;Wk605iGVXArl)0_ ze&HpiEqYe!;v$%s(fwzPeLD8FM+0-P1ZIVBG>8UZKJ;UD^z-sx%(XNhfDPFD;_Q-( z42oZtL96@TvwO8Z*;u^&J46u_ca6v&*SzcU4w7#7IA5Vkz1WbaZp76!f1~(Z%#onC zqT)hXu)_7droHemk_%L+%;W5l`}%cg=P!lbX2X*dloN~_@y4Bo{KgH2w-mF2-lIHb zUPDYq19DY9v=XBDrQ@U`J=IU91}^Mt?-o_e@Qj%Ce-ixcl9&@w7*wl!EHz~ID$87b zt7XFCk(cT=Rc?y@yOuS|*AD1P=(sJa>JIr+M(CQTc#{mWz%xA`glem5L%%y?Q+DrH z(YQe`!6fc=ZZA+%vovjWs~HRj8{{y!(C6f6xkHCO+(U4oInv zqmVNyee4i!LCSuFP_lJZiP3yjPU8!JKQ@)ga(sWj?1Ul4$K7hup(MW&uDo_!ziEjR z8iRH@Q?5MGI{IciL zUY9Vk2t5pE!@!@v6KXR*557We68{2DtmeulE@XY+A%JyzZ!wVeHKNeNaMGFI2=IL4 zPZ(ieQaZ+#_FV_oJguutXmd1DC1!~!MBSHmO0*olq?@ln5H(}|A5VNr=$C1&gJt>x z+4QUFMIa4h*Wor!$nYx5ND6JC`NBoD5r1juxZpcUdi!?m_6f^;P55UnHQ_0Rasa0x1|}>(+=0Q5Rhlz)m-YOmxB8|-jI8{Sde(mvLwaC)eH~D2ht~l=y-n(?D0JT z{EZDOzcPAu2P_$VRgvXFJ|pd1M@f0Diu7~GnWnC?5&M#52J`lVqIY-9aB6pA#~gh? zCMH?~A%9bgRIfaJe~0tP>~;KbX>6fc*;7w+t))MI&8Z0}OYcp1E${MMe};f|V$rHT z-Tle^Qi}a2mOp;fS+39#Y8dRi?fbPFyZr99|4QCPc=@D={f5rIesx;QDiW#cKQal4<)*AZB#=$z z(1=i*d%cfI6a2AvLrmzT(76IDGX@Rt*;$D1NQcP^W{U!R=*=@zcJE`^bFqNHYtef^Z%kBDXyWJ5(o9( zxUw_xPW#X_ZhR6xJ~>aGzcKO|HCK|0JLr87D6>Tx#X;U;-+ecgGb&RcciAn+Plc&U zE=U5Rnlo%HK4J`Q;vVrmX`1Sh_!G&akz*=IZ)yEYW}Wb#IJ^wgKHvxH&Lutg8+QSk z!14gVLatL5{|~5Q1~6atKZ+9kDd*B_@c(P!51`HXtwe(VB1$rW8FA46 z?wJ06lK`BUp)qyfrcreVtTu1`fSEbR1UHpqyx+4P9LDIQ2ZWImZ9t%;Y2S3Zdz60+pcl6Q z=yCupDlI01|^dF(-291nsn+(2u`I4g&%b7b?Q|dSa zkes^R2U=P5i}Y%CXED?>Kd zp5^$TU@R}r&vwqk(mU(s&RDQ``~7KNXbe;-GA*so543we&}@T|I@D&=0$KkW8EC1% ztclc5^Bg;TOXPpGU}Iggybmx9+-5u-<#7tKf5tS3jHZ)P);70WpT#|a%Rr6rUHG$0 zch>7*g(q^~zY|*~-xcF#^bw(x`HEG1v-$khyz%5cDoaK)AfoC zKnYwRE_SW7DBJ}uLiugy1Am69jZq3R6nIKeQz12mSMg&-PfX)d=6x%3-Lm>$k51N3 zADOiTFLc7c_J@XkyBHziyLwGjd}DM3C1Bm(Ia;Wjqa4kA(1=9w{R3xm(2!^yv8_6X zN6r0f%deKp+{ySWL$3@kvHUfN)=S z2Ri%d#i^>lLzmX<5h?>!tOABVPJz;$u*wm!U! zC$!KCjF*)=1bg@Kx197>SPZCP9H&||uECww0Ux?++|gv=Bg!m(2I+O0*+#K2v zTFNTOrPp%k@cP^L?`Sa{oqF)Zh2s`G!=}O@> zs~v?30b9!-)QTYJdNfDrOa3G%4jV6h{ulfG%=ye!O4TWtlh@)xi+ZJ>C;7a0vyOxFx6G=1fp?qW(NPo1G8xQbU>=+5Ao{4O=M3tqsKk0`>ZF$kH-Ty7+K$%|z zvW#(diM|$j>ilyS^w#}JJhUIG9XwOV2Co;eDFP%ORWSV%nd1YW$3Zb8%_lZ=dk{p?H0zgc25z)}imXUiGw~r2z`GiRx=~Yxz>1UPy{CQKDtiVW) zJZW>&KXPx)Cz)I`)jeQ&qs}6x>p2g&Nx9Oh$BJ-CD>T3cSNGN8W$WW*#I%p-RcVi{ z3<;crv`_VbT&@3@hD-k9wbabKHRQ*@lfjSkY~xa<**(?CbseeADPyo!gHqb`)v1b?&DP11P@FQHf4c+qhL4Z$Kq+6SxY$Ua z8{xB#>vb1K+R!AKMco2fu&TvXPBoo& z7n9bO8rS$%$`MdG{6L7>1ncCdNciY5t816AMs}UHx3?oo)^CcJ%iT$d(W6Th>$hi% z?TQemp5xx+8bRUaph~pDc3hW4w0O3!>v4cc7*HH@{wFo2BP9(UE8flI85GH3iGpvn zRNnF7l;kZ=kMbWcau zE@BGl6*nSD>UunlL}T^U%2w*NHnYt2Yi@B+G1{zE6FZ2F3pDHm`aG3Du0l|~R^{T( zi9n>Vn4vF=KJq!8nAf&pY;I!2PPf&Q(SGE-2mdTY8FR)h1lb0PiV!QptH^>HgP74ZR`q#d^aNp;CO?YR(W0N>T4`9#D`wQUAw`{=yg-&1iW* zL*Iw~Bq0_T?f2vESB zKgE;cejiT;iQzEt9x23Y=!X*FL&t%SBAz$T9%W50L3un+ZQT??#JG={5@7oAyB^*; zQ>%uo{<|BIJHG;hDjhKf;w_ZZ&_g{Wm zP<2C1Lr=B?&KaETf~Ob*-UbZzAT=#4a`qZA;DN{pA;^0vF39>JpLK%a-$48mtxHod zmACc+Ag@bGAUsE~+pm@~+5CU3;O5%lS5uiczDgVQOBG`E&V74rj9Ou-w;t(rmZ8{t zW)xcQbaEIw1H89Oc#p1W#SM2%lLl@7@a{uQGSVZy7y)(^@ZhEJo<&bDV^(;TCeNnb zot>xX87&(-ntSeQ&@^^Kb_JUtD9lzMe);tn)%Zs}X1hz1&esl)@)<4Kf{(JaCUAUO z?KGSQrm&+$rQL-KFf#0}KpuKK?HDg9VWD3QQ$HX8lWHmU@A}p180t0OPA<&E0E6_d z-4z%YDimImnpPN4P$S&E72w9&U|%+`1WN2Py)fI@3hhf4f%Da|OCY@{Xa+y!9lvhf z&7RXQ&aFIBBqHBRTILv&SV#6Dy;s9WAxQ}~pQ?ba&H0E!Ex2gqD|AeA;Z%3{9 z`meA9G+w8~;Xu8*q1|Ej6>2guPdF^m<`u@n#7kzam)#R=+<{yf*%n)uscK4xoxdSh z-ak>8nKz8J%dV2%KOXU@!c3T*@n2MH?|pr91#-f`7m8474KFH@h}?)!*2xRW#_*2<`d4ZOK!ElpDy8QxH8<;=U6!ea%Uxa24R+vqAN5PPx* zyJ9Q-k6%Vf1h_b|E;mK5{7cPhCIh%`I8No0f84nsCQ6cHzTBTbqhmOs;9XpoyQ+ov z5|LR1BM~Z6_cxrVj3HPGr^z5kfwt2OhB1nyca#Yq ze=k;9tR&A-ZsR|`>J5-GFYN)~D8Lz+kT}jEMu={?CvSr@_cI#DMlhlVA$_7Pc9Wz# zLl3#`7&>bDx^c`zxvh!SVrRO%H-Lo9A7yE0fWdTFEh27Gf>BJN)!WwHunv7(YgLhcYS;+x%_zw~VN6 z4sIHa zljK~vOqZGu-^*k>vrzrxyD)JBhd*LkM9{zBE_b%x%e3Di>CX@}H`DEB_{X1B_5#Lw z6&o}3;@`181PE6ln+P|>zin9nTn3I$Xm}-lApVyR*~SjA_5DzB0{=2ehf@K(w-IR} z_*c*_O_HQ_|B-WV!k-dyvhZYA71|x$zTY0s(=cg0nEYg}orv7rSp%Fia*8dS%M z4HKPakW+TKJ#o=fPDl;FHH?|Ig8182cbt5l%mbM0KD<`v%w(pW{w~qUU$B3;u)hg_ zylfYoo*R?;z6WcoC}P^T28B8bVxDU(`xq3?K6&90@J@rl`v<v+xnB0caTSt@x!6x8z!DH)l>@CT4Z6 zFocl3Rb6w`$>;KrzmEb0g>zBDqH!Hq9j3<~E7~T|@pUxE#H0P9yqbfYS$JN5+Sdon zIW9lH!~m|}Ek-859N8I^qsU!}lzbf;s_fuUG7TM&Oq+L{Z9)t55&gX=RRCtAY3U^; zvl;^&M=mieljXz1<5;@krYN1yF!?q`k(chxV95BV))+hKx0c&xYVX*8}iP2xL(xQ)qj2cZn3oR_qS!ZSn&AJU)EG zl=bNbfwq^A^a_}qn~w4p06O5MPYtjXV?~H5G_~13-SrH(2%*SH5slp1auN02E2y@e znXF|O6x0j7?xVIgUTojbr&sGd#exYNsDKzlJY|&=!02*o6~(Qr*c>^n z<8(f!gd~Xco%erBjb>F#PzJcF6P8_Pw0mpCm7W*_j^`CG-1MnSP^~wrT;~<1LWww& z{$6saq=Wv>JW0fMfWK7Prqtr+W4VtQyChIHoJCA+Ec(q5sUYxAEDl}ZVxC3hx15E{ zg=#!j-NMP0J3icf5X-Jnc~Z&Q%S$hfzCfQ>dQKVmVA0l=$vyzh>AZi1lsUuT;fFN$Iqlr<)ix$6~E_TvT zDx1%TZC4ppE#z~q(>_r?Ki$=8I6uaeCRg#AmHQtb#9zDpsGDp++IdP4S$+VS5?~uW^VzRKtv8lf`IRCJ%1n*GB8-$-+2-Y<88l*kJYHm2$S0}IT{#jf> zx0rN&4KB7azwXR<&2>HSKkj_JJ-_kSK%*xHT_dKr1=te_mxn-n$UBW|9XPT9US6I? zGGcv>$7eI(q6&jRe8|+rHtlMPH&|@H5{GyI#6%6617st0vv)D93zsCqmtTK{-QY5=?lekWz+7c);uW*nX~dLK_vzTxeIkZ*#n*03 z!q*q?`iv4q8`CCzs;aR=)s7e@g?$kWY4SkpaOmb$-xKwluyWYsm(3-v@S5|iD_uUb z?~D?W49Y4qsj<@!-gTHQY3NI7k$4d}qgQTTvo=Pxz7(+8!a3jP$z#DED~bI;!IgT= zeYyAcSrnh+vBzEeU>wkSM22q=5U1*cJ%4tU+t+atp@Gz>ue&e zd;p*k4j8sP8S5k41pf?!tpY%OT8|$rGtZZYV{tYksq|KjZ-h?nST*h%mYJ@W)Pe>Z z+&d!axQr@u$cs|qCnZb2;#J!uXcJbpe@uQ-Y&kXVn6{txMv4vgf3^3WQB8FHw}ObE zA|QyOQdK~CC{lv-DAJ@CK{`?b7z0ugAV>+Qs8pp%M_NDxq=sHBASF~G1Oliusi7l; zggZg>eb-&PA8X%Hr zQb5sL4}K=;+hEOc`9|vWC(k*w-V=S@2w(_{_RuM*k)@%Mk7b|V4isbq9Mv@6)Yn@$ z8$az1PvxaxKP^1?EzDy+!_L=LFJoHo=7j6#3`^6ys{Ocvl@A zxmCa|S5geS-;;sjN|qg`(@%O{+fxb~0o8^fH|FrXEjUWuNR~ZFns;C&tvInH>Rg^) z`n7&~mzt=&Jfl;r+~cr!YjT@|SD=GMFW(Nll`9jlzdI~hKQSJ-SPp@~Q;NK2zs}MV zRT-z<76|y02|ehZk#g8YUHYA>6&!Suooqt06?)Q&duE5_$5&5lw%Ri(d@M=V0tdri z>XfI{hkmf)FefsuRoFD#R_=E!hRu6|GEC#l>2G<$NIp2S%v(OZX}|+O;zpB}3@Zlw!mDe6Y2UQtg?2>-nj^JOlgW+4eZ$ z1a!6X-fkK8L;bHGT2yXM+sligzRNkqRrAh{T#dsuI_ClAY~CWjH=?J^RIGYy%x`Mv z{4TS03XIKfUVvjXZ3RgJ*Sbj6KS602v@w4$w2q@lU~ zhi*4!-KmY%iWQp0kGcs&4}i5=txcpy$93b4g*zowm>z&3+Fs8d5B4uPaoHzY&H@3% zdTyj{mp%hmT83(<)=#$O#bHcB(sZ)`+1*JuX2jcWe!#XhGIjI2P(^baNG7~Hn-Ih>+I2uAbV!{a-wZo zRX~5&Jlm#jB9zv|KO--u?WeR`FF|w=O;C?GH)~u{3~<)d6z-)t>~(#$!nmdq@L8c& zOD-SrCoF>_x;&@dV=B%=(geJ@cYAF;C-EhvU7~EzD5WO>6A?0PbZg7eO0a;{PnuCL zm7X^`ZEVCR$VPAk16+H)T`~t0jfzl{=g4+ySh1 zEPMG_^%Bv$Tv-*+!FFJMWGB<#SpW%1S0a6jMuarl7QNv>H>}-1gCxILTSd$%QHHWs z$4ErK1usdWo8d1wp?LaU;dzI;?Wa!wJ=~W?C!S|(a*w;sX6`BpSDBPM_ufRN)SG&h zoa9<*h%cJ^9d6@|&fSl+Ci*mPs5&F6j}17E#BZFrr^kQ!_MK%vo2lD_?^_q(6Nh}K z3jvp<6}Vs$7VrT8R)Vrl={33`wB9{a#(m+5O6=JqbNx#$Z+`liVu$XtyfXs_zKHbx z2LR%nGZOFi!joQVFU;q7Y3IuM1YV9U|3zeT6FU(CnL4V#{_L!zlOeA7mg8y z^ty5AD>KRIr78Gc^G^NYe_G6%@F>EV&olBopzKg-b1k+h9slG|2IwqUuA^>5Q7)xN z`j3HZKMpzuNME{4M71sIZ&lnQ2QoJcn5J(qSYD-($hi`|c{+Sn^p!dKM!XVVTKo(8 z&<5IDHj7`I#PZj^qMQ6rbNMA3)qxC@G+rON7d^-Cx)Ts{x@xXVJ@o!QfR|uTep_TTIjws3t(p zMc&x3NG$@{ax zIYcW;6Kr^fKIT6OC{{qYA1<%1`lm=N4}l@KM*Q`Elw=Iyz>de3Aba)CzBwp}sz-X# zywAu5iUL50wm|U?Pjngl**4SASpv@%Jt6R)1X>_adhV=u|l{AHZw*t&lsr&$wMG~hw;EL^*}`r!PRjabE1PU&l03ujTBdd2Nc^rX*{Wp;bPzRAo~!SG86(vrnmhAOJbu z5WOOF^tWiGidOw34?7bWeqy#oC;5|N@ztHQY@&{N%GOo+kAN~W8o*WcQWWgO7Ly{@ za(uH~9ki-@_or{s=in|&Uj>?c?jFTrvpz^fO?wSC?14qf)?2MU@sq(|ITiqOUSBRb zTxsFZzZq9L(**L(AXymZSo#O|dp%U?yG1Rs;_+LzZVjEt_dF@r=&}YOac2`%{g@vl zFj*wvw}eR6Q^u3PV&;;16RFmlBK@ zcG?{;{r9q(aDDgyM&8s)BrB_ zXI0q-q>V4!vn@@rRTa-8{~P4{c9ZJ-f=w1{Jb|3x$`AClk1~o(kzFipF|}MYL-4Vk z%a(E;`cxKH$t9-=xbP{ix%hslU98Zq9S{b5_3{T7s3aUfpu)7=Vn7j zSYQ@~jvPO&6jg`^^kURya+ny)x=mWK10UZEg%f>?BQ>Fj-lN)M3Q#aX(s7$8!k5~` zC)z=$*tjl|HkULjAdc;^ns1E@M4`BMu`mmnWZa!q%f<$eo%LB``RVPYDxWq(ME};9 zuhB{FA+Z8ekpy?;KrwfB_nF!bc9>~}Inx!B%xuM6*X!#MriSH;zkOI(H1udW`Gs`m zf~DLB%#*A~4L@A`Fx3R`v$h3^9Tb1R9EVwnzQw_`SCXcpR8zii?(KZ0Oack{zB?xK zhDpxVJ5wd-v;@9l=Jy3!i7=aU*FPs6})xWqcn7g{!a_3Wex8GD6I25HPnOfA&lCDHA_lmp^E4O_o|$O`yHW)+$OX0x+) zH3MaU!K{IS!Rjc^=F41M$D-T}4ldAdQXHUyhXxM;j;JGu@SR=z`I&hI?!cWRwbc+8 zt=BM$^>V^`-3*Pd+tB$w!ceS`!EI~5DOn4&dNJve4JCMRfJdIZ{6>Z5tw}Lo5|&Ws zk`=zQph5-hf4+agfvb!I0R549g}#5rYn?E^As7gQYO85n z{n3Alg99>5&TtojZZ+DIKn9Ie`I8L6C(JnSt|~qA3uPRM2dN#IeqHpUGFhM7=a)O< zapnX|fwgmNTq4EHkF0CPkZ4$0W#Cp(>S|jkv0WN#p}GAcZehhH0G4+`inQ2UfT`vX zbo?a9qY)37GbG3c05-Y+GsJ|WD_yveqA)nP1;;M7-wFVTfu`I(lZ^NV|AJ9(yDdsd zmU=dkt$Wx{oyfQ7?vptlwmL&p?zrk{^HaJry)lUV@EY`Kb!(@;)&Zz~nKmJpN8++b z&11M^a!`S9$6%?$%1vMIpSEn4w=`3c2_r$%OC;`iKm~$7^nIu|D|2mZDwNnzO4A$} zi17;s7aH|=ktpJbaDb(bN9ddpy88uTikTnMj29!1G_1_`1j{4GX6m(E>{^(X@by|$ zQ^#pIdbVNFL9#yXs_AJiqhH-S6(I0G77dw9;U7MJ$Wbi-8I~cewl~lvt82{T!}iww zyXLiSOqJ3TGMgh}j@@Zi9s|-Mm-zV&PeDn_^Mmc>SJ)O1GGiXe7vvFN9cJiCPcfUvSQX3e5fJ0=+$lh@AMA8rBUjLNnCPWbZgJiQra${K{NZ&{UG4PE#8 za|0iouXq)ZcNbt=J4mB4#w5*$Z31IruUP2jYVR5eyyk7xu3iNN;oxTBVe}EYZIBii zM6#pYLE2sx{v{YP*tmx%XP3DFj0KK>)-fQ~t>K&tP8v372x!gW*XZ|fukjrG^yF=6qnyze z03I((yu$QKAYu=zl$V|OUOQ1DjNaa?)S<$y>6~^|7=0FcLbs#$qWYvwjH5?;-{mle zd`7Rpy$$tRjn+k%P2Dj18*=n_4qwFPArne$0$pk=p*WaraAYQkH$n<7E&O&0uwZh; zziOO{a#!{R6+%nn%&!{1`{!KUefLOof0Andfy3`(Mng7eEyO{_^=@(Um+vY^VbcX4 znPl3gBlccT1lT3`?a2WSQ-G;9PQtBAI~nEG)rRwZa1E~|QmfNc$m!y31|gI~>#Reb z=7#M_j0`6BqkXzA%huk$^A-=%PL?p>5P2;aaH7WEe*7Yz~_Hq_*C)Y@WNJS`Q!|pa9 z^{tr1ChJKtd}UTCLEG!tHY}y^9aarHozQruD`BdGqr+`lssBM`q%Zuo9gbi&6Hq@j z$PMz_En*b1x3hayw{@l~-KjNW?*H=mJsS339=}tuF3bmE>sSUl_*Q+tbeg2oqvbc^ zfPkh_Nl>Tf4b5ycj{fxt2g!Wrnr{^(ix3&7M?X=ANs~dGT6vuP`fHE^w_OT5_P&dI z<2Cii>O+n-$|A@MT&Hxu@SgZDh&*0h?csi&7VKQ~#0bI&X^Re-lD z5F1y|TTX!zF)Bxyhnog$ zme832e2W>8{8o_;F!ptn#MOwF_yvwFaQ40XTwyp~UakNA!)iy=IDS-HiU%RpQ)Gos z^Ut)Atam$sG&q^a30iXu{&A5$gL*c|g>gVFJO#)=b28g_=S-zX2^%0k z`eCjDJ>K!*30Hh2EX+Cg=f1UX_QciHLy*DnD+1!fPBms)?VA0cXRYjX5SpzUF5Zs} zRe2VAGf}IZvoSih?9kfn(UHH-zPS@hsbJ%IXO@B-;skk0pueKXYCyO5kjV%s3a@*3 zZb8;#XXB2tW<`CgRXyylr*EV?JTid*HJov8rzR6hU!E~zmpLuQ+-2MDw{ll|D zje;{rSReD>HP6;ReuVYLvKrr6t@`)RF!B5q6;D1T=~R}u8s@Df!av~4>pQ+aZ+ zxf9Ifb-_`Qo_~_IhpoIZ7)@pwJq3!h%(28f(~Qs4DV;M(#Jy$pQ6YnAHOQsK5_KNe zNN#^8&mU=|kz~Ol4NgIAzIy^qI>ZK})QmMaPEm@rN6^j0)rQ zVH4g2yl7Po`uIqqpnSu|V%s%52mm=V^ou}PTIsj@!!SeTAE^d}@TPzt zMi@7rwnmK6ky9;_HNVhkU3j;ieFE1+v~Qi+LhO4VSKsPdowPN_6&`0(>RRHly&bGZac`+`b=`k$ZVzk8k`#G<$Tl0Fj}yOzn&@YRip{)_QYYbP>GTx zwMy(xZ=>!_r<-HWJ@bI@LoUK9GU8(cbh9v7>1C_uqYqYdzBv|3DpG3^p2tasq>fuA zEyd-+UjuiW$O$%b2+sWUY5$i76K@bxWW1hi)$WhIJptl~_T{t4fJb{~0o;K^CvGo} zNJm{SwY!oMiZzaV1f8=mEsoNS?{F`haW-D@q6e+D~fFlp7 zJ1ex5Z5tQ%6M~Xzor#|^8Tse?Rg&(pY+2KrEx+QQXeMG3EYOUmi3z_t1OrM$4>>%R z*u$3U-1kdMo~#`31n6Ws7pA!OIDAomd>`i&Wv&Ip4C?ZBOgM{(z2w zg;zYHsfhDRDt%V0zr?BQ&|m-LxaCwxUTnaRX?zhaEau$M29GiMixh%}EgFMf_U%C5 zg)XM*GE4#9kn(6q?UfA94T3CDUW5-E1dgB`5c7Il;=FTfzo#&;h^{wDG# z4|TXonTuiics3k;6iV2 zt+duRcyuTPe1ul@N#8DZOA@_@zfcvB&x8(5CLa$w9Sc4~y(6dziR$FTmRArim$MFu zP%mx}+E$;+J@1Pqz7_P#?Gd@~%Lk2zjZ`{w8REg}7d= zTt7G;)I8`ZpoEsq$%GSiv5Q<4o~+In2TPpk$9tCZ#~jBARsBhX(a)>UKRm zzkTb|UBH0$i;uDb8gaF~eYI#Fb25SNec>Cgsa-sTm{yN%&QWo6cD}s-fIMSf+z-o` zgltUlJyXVJOL6}voK#a38^;&kpXp2p<^mUxWc~t7v_cLE~ zFm*~%EnJp0Z)ClGVzD27RIFW3^*pHULhy7nM9yl-dYECs0ag8^FEu;^_P1$FO>IZq z#%)HIWaa<7Czz4;rVHbpKDFN!6$G9Fj;D?cD*ieC_x!*0^S`@)>>U<+q!NYfh}{GJ Q#Osizs_w0#o3_FK2jlhr)Bpeg literal 0 HcmV?d00001 From 5f2c50df3bd689cd5a776f6bb692441ac25983d9 Mon Sep 17 00:00:00 2001 From: Sebastian Maniak Date: Fri, 18 Sep 2026 17:36:01 -0400 Subject: [PATCH 05/12] style: keep agentdesktop lowercase in Pi copy --- crates/agent/src/provider/README.md | 6 +++--- frontend/desktop/src/views/ToolsView.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/agent/src/provider/README.md b/crates/agent/src/provider/README.md index 14b3358c..1ba2b6ce 100644 --- a/crates/agent/src/provider/README.md +++ b/crates/agent/src/provider/README.md @@ -1,6 +1,6 @@ # Provider support -Features currently implemented by Agentdesktop. +Features currently implemented by agentdesktop. Grok Build managed configuration requires system mode. Linux and macOS use `/etc/grok/managed_config.toml`; Windows uses the system drive's @@ -30,9 +30,9 @@ files are not scanned until the adapter imports them into a Pi-owned file. | Tool-use telemetry | ✅ | ◯ | ◯ | ◯ | ◯ | — | ◯ | ◯ | ◯ | | Session-start telemetry | ✅ | ◯ | ◯ | ◯ | ◯ | — | ◯ | ◯ | ◯ | -- ◯ describes a gap in Agentdesktop; it does not mean the upstream provider +- ◯ describes a gap in agentdesktop; it does not mean the upstream provider cannot support the feature. -- Managed configuration includes dry runs and removal of Agentdesktop-owned +- Managed configuration includes dry runs and removal of agentdesktop-owned settings. Claude Code also supports merging into user settings; Claude Desktop requires system-managed settings. - Discovery is best effort; versions may be unavailable. Ollama discovers models diff --git a/frontend/desktop/src/views/ToolsView.tsx b/frontend/desktop/src/views/ToolsView.tsx index 99f3f531..8d6d78ab 100644 --- a/frontend/desktop/src/views/ToolsView.tsx +++ b/frontend/desktop/src/views/ToolsView.tsx @@ -96,7 +96,7 @@ export function ToolsView({ discovery, unavailable }: ToolsViewProps) {

No supported tools found - Agent Desktop can inventory VS Code, Claude Code, Claude + agentdesktop can inventory VS Code, Claude Code, Claude Desktop, Codex, OpenCode, Grok Build, and Pi.
From f45e0eb938bb9eb73eeae22f7563a36d70c3e783 Mon Sep 17 00:00:00 2001 From: Sebastian Maniak Date: Fri, 18 Sep 2026 20:12:15 -0400 Subject: [PATCH 06/12] style: wrap ToolsView empty-state copy for biome --- frontend/desktop/src/views/ToolsView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/desktop/src/views/ToolsView.tsx b/frontend/desktop/src/views/ToolsView.tsx index 8d6d78ab..48f2586c 100644 --- a/frontend/desktop/src/views/ToolsView.tsx +++ b/frontend/desktop/src/views/ToolsView.tsx @@ -96,8 +96,8 @@ export function ToolsView({ discovery, unavailable }: ToolsViewProps) {
No supported tools found - agentdesktop can inventory VS Code, Claude Code, Claude - Desktop, Codex, OpenCode, Grok Build, and Pi. + agentdesktop can inventory VS Code, Claude Code, Claude Desktop, + Codex, OpenCode, Grok Build, and Pi.
From acd219cb3f88dc42f2d042223a2f12cf9feefc66 Mon Sep 17 00:00:00 2001 From: Sebastian Maniak Date: Fri, 18 Sep 2026 22:38:51 -0400 Subject: [PATCH 07/12] fix: address Pi gateway configuration and discovery issues --- crates/agent/src/daemon.rs | 31 ++ crates/agent/src/provider/README.md | 15 +- .../src/provider/claude_code/reconcile.rs | 2 + .../src/provider/claude_desktop/reconcile.rs | 1 + crates/agent/src/provider/json_merge.rs | 24 +- crates/agent/src/provider/pi/discovery.rs | 299 +++++++++++++++++- crates/agent/src/provider/pi/mod.rs | 66 +++- crates/agent/src/provider/pi/reconcile.rs | 206 +++++++++++- crates/agent/src/reconcile/mod.rs | 45 ++- crates/core/src/config.rs | 9 +- examples/claude/README.md | 13 + examples/claude/claude-code.yaml | 7 +- examples/standalone/README.md | 9 +- examples/standalone/config.yaml | 2 +- schema/daemon-config.json | 4 +- schema/daemon-config.md | 2 +- 16 files changed, 690 insertions(+), 45 deletions(-) diff --git a/crates/agent/src/daemon.rs b/crates/agent/src/daemon.rs index c5db9816..f44d5a57 100644 --- a/crates/agent/src/daemon.rs +++ b/crates/agent/src/daemon.rs @@ -686,6 +686,11 @@ fn validate_one_shot(config: &agentdesktop_core::config::DaemonConfig) -> anyhow .grok .as_ref() .is_some_and(|program| program.use_llm_gateway), + config + .programs + .pi + .as_ref() + .is_some_and(|program| program.use_llm_gateway), ] .into_iter() .any(|used| used); @@ -1374,6 +1379,32 @@ programs: ); } + #[test] + fn one_shot_rejects_authenticated_pi_gateway() { + for authentication in [ + " type: oidc\n issuer: https://login.example.com\n clientId: agentdesktop\n", + " type: controllerJwt\n audience: agentgateway\n allowedClientIds: [pi]\n", + ] { + let config = parse_daemon(&format!( + "llmGateway:\n url: https://gateway.example.com\n authentication:\n{authentication}programs:\n pi:\n model: selected\n" + )).unwrap(); + let error = + validate_one_shot(&config).expect_err("Pi credentials require a running daemon"); + assert!(error.to_string().contains("credential helpers")); + } + } + + #[test] + fn one_shot_accepts_pi_without_runtime_credentials() { + for yaml in [ + "programs:\n pi: {}\n", + "llmGateway:\n url: https://gateway.example.com\nprograms:\n pi:\n model: selected\n", + "llmGateway:\n url: https://gateway.example.com\n authentication:\n type: oidc\n issuer: https://login.example.com\n clientId: agentdesktop\nprograms:\n pi:\n useLlmGateway: false\n", + ] { + validate_one_shot(&parse_daemon(yaml).unwrap()).unwrap(); + } + } + #[test] fn one_shot_accepts_static_settings_and_rejects_runtime_services() { let static_config = parse_daemon( diff --git a/crates/agent/src/provider/README.md b/crates/agent/src/provider/README.md index 1ba2b6ce..a432cecb 100644 --- a/crates/agent/src/provider/README.md +++ b/crates/agent/src/provider/README.md @@ -8,9 +8,18 @@ Grok Build managed configuration requires system mode. Linux and macOS use Grok's own configuration sync can delete or replace the user-level managed file. -Pi stores configuration in `~/.pi/agent` (or `PI_CODING_AGENT_DIR`). `--user` -merges `providers.agentdesktop` into `models.json` and sets the default model -in `settings.json`. MCP inventory comes from `pi-mcp-adapter` config files +Pi stores configuration in `~/.pi/agent` (or `PI_CODING_AGENT_DIR`, with `~` +expanded to the user's home). Management requires `--user`; system mode +rejects `programs.pi` because Pi does not read `/etc/pi/agent`. Authenticated +gateway configuration also requires a running daemon and rejects `--once`. +User mode merges `providers.agentdesktop` into `models.json` and sets the +default model in `settings.json`. Model map keys determine catalog IDs, and +gateway URLs override per-model endpoints, using each model's API dialect. +Commented `models.json` files are accepted and rewritten as standard JSON, +preserving user configuration values. `settings.json` requires standard JSON. +Discovery recognizes npm's Windows command shims as well as symlinked launchers, +verifying the Pi package manifest without executing the launcher. +MCP inventory comes from `pi-mcp-adapter` config files (`~/.pi/agent/mcp.json`, `.pi/mcp.json`, `.mcp.json`, and shared `~/.config/mcp/mcp.json` / `~/.agents/mcp.json`). Host-specific Cursor/Claude files are not scanned until the adapter imports them into a Pi-owned file. diff --git a/crates/agent/src/provider/claude_code/reconcile.rs b/crates/agent/src/provider/claude_code/reconcile.rs index 54727276..19b92f8e 100644 --- a/crates/agent/src/provider/claude_code/reconcile.rs +++ b/crates/agent/src/provider/claude_code/reconcile.rs @@ -52,6 +52,7 @@ pub(super) fn plan( && json_merge::plan_remove( path, &merge_state_path, + json_merge::JsonFormat::Json, "settings", ClaudeCode::DISPLAY_NAME, plan, @@ -68,6 +69,7 @@ pub(super) fn plan( json_merge::plan_merge( path, &merge_state_path, + json_merge::JsonFormat::Json, settings, is_owned(&owner_path, plan)?, "settings", diff --git a/crates/agent/src/provider/claude_desktop/reconcile.rs b/crates/agent/src/provider/claude_desktop/reconcile.rs index 29477c66..41788ed4 100644 --- a/crates/agent/src/provider/claude_desktop/reconcile.rs +++ b/crates/agent/src/provider/claude_desktop/reconcile.rs @@ -32,6 +32,7 @@ pub(super) fn plan( || !json_merge::plan_remove( settings_path, &settings_state, + json_merge::JsonFormat::Json, "managed settings", ClaudeDesktop::DISPLAY_NAME, plan, diff --git a/crates/agent/src/provider/json_merge.rs b/crates/agent/src/provider/json_merge.rs index d9e39c35..38ecd09c 100644 --- a/crates/agent/src/provider/json_merge.rs +++ b/crates/agent/src/provider/json_merge.rs @@ -17,6 +17,24 @@ struct MergeState { after: Value, } +pub(super) enum JsonFormat { + Json, + Jsonc, +} + +impl JsonFormat { + fn parse(&self, contents: &[u8]) -> anyhow::Result { + match self { + Self::Json => Ok(serde_json::from_slice(contents)?), + Self::Jsonc => { + let contents = std::str::from_utf8(contents)?; + // Normalize commented input to ordinary JSON when writing it. + Ok(json5::from_str(contents.trim_start_matches('\u{feff}'))?) + } + } + } +} + pub(super) fn state_path(path: &Path) -> PathBuf { let name = path .file_name() @@ -29,6 +47,7 @@ pub(super) fn state_path(path: &Path) -> PathBuf { pub(super) fn plan_merge( path: &Path, state_path: &Path, + format: JsonFormat, managed: Value, legacy_owned: bool, description: &str, @@ -50,7 +69,7 @@ pub(super) fn plan_merge( .unwrap_or(existing.is_none() || legacy_owned); let mut combined = match existing.as_deref() { - Some(contents) => match serde_json::from_slice::(contents) { + Some(contents) => match format.parse(contents) { Ok(Value::Object(object)) => Value::Object(object), Ok(_) | Err(_) => { plan.record(display_name, description, "conflict", path); @@ -103,6 +122,7 @@ pub(super) fn plan_merge( pub(super) fn plan_remove( path: &Path, state_path: &Path, + format: JsonFormat, description: &str, display_name: &str, plan: &ReconcilePlan, @@ -122,7 +142,7 @@ pub(super) fn plan_remove( .with_context(|| format!("read {display_name} from {}", path.display())); } }; - let settings = match serde_json::from_slice::(&existing) { + let settings = match format.parse(&existing) { Ok(Value::Object(object)) => Value::Object(object), Ok(_) | Err(_) => { plan.record(display_name, description, "conflict", path); diff --git a/crates/agent/src/provider/pi/discovery.rs b/crates/agent/src/provider/pi/discovery.rs index 91932b3b..9c500010 100644 --- a/crates/agent/src/provider/pi/discovery.rs +++ b/crates/agent/src/provider/pi/discovery.rs @@ -1,5 +1,6 @@ use std::{ collections::BTreeSet, + io::Read, path::{Path, PathBuf}, }; @@ -7,7 +8,7 @@ use agentdesktop_core::model::{Agent, McpServer}; use crate::provider::{metadata, vscode::discovery as vscode}; -use super::Pi; +use super::{Pi, user_pi_agent_dir}; pub(super) fn discover() -> Option { let executable = metadata::find_all_in_path("pi") @@ -71,6 +72,9 @@ fn package_manifest(executable: &Path) -> Option { .into_iter() .flatten() { + if let Some(manifest) = npm_shim_manifest(&start) { + return Some(manifest); + } let mut directory = start.parent()?.to_path_buf(); for _ in 0..8 { let manifest = directory.join("package.json"); @@ -87,6 +91,47 @@ fn package_manifest(executable: &Path) -> Option { None } +fn npm_shim_manifest(executable: &Path) -> Option { + let manifest = executable + .parent()? + .join("node_modules") + .join(Pi::PACKAGE_NAME) + .join("package.json"); + metadata::json_package_version(&manifest, Pi::PACKAGE_NAME)?; + + // npm's Windows global launchers point to a sibling package. Verify that + // relationship from the small launcher script without executing it. + const MAX_SHIM_BYTES: usize = 16 * 1024; + let file = std::fs::File::open(executable).ok()?; + if !file.metadata().ok()?.is_file() { + return None; + } + let mut source = String::new(); + file.take((MAX_SHIM_BYTES + 1) as u64) + .read_to_string(&mut source) + .ok()?; + if source.len() > MAX_SHIM_BYTES { + return None; + } + + let target = format!("node_modules/{}/dist/bundle/cli.js\"", Pi::PACKAGE_NAME); + let basedir_target = format!("\"$basedir/{target}"); + let dp0_target = format!("\"%dp0%/{target}"); + source + .lines() + .any(|line| { + let line = line.trim_start().replace('\\', "/"); + ((line.starts_with("exec ") + || line.starts_with("& ") + || line.starts_with("$input | & ")) + && line.contains(&basedir_target)) + || (line.starts_with("endLocal & ") + && line.contains("\"%_prog%\"") + && line.contains(&dp0_target)) + }) + .then_some(manifest) +} + fn discover_mcp_servers() -> Vec { mcp_config_paths() .into_iter() @@ -101,10 +146,7 @@ fn discover_mcp_servers() -> Vec { fn mcp_config_paths() -> Vec { let mut paths = BTreeSet::new(); for home in metadata::user_home_dirs() { - let agent_dir = std::env::var_os("PI_CODING_AGENT_DIR") - .filter(|value| !value.is_empty()) - .map(PathBuf::from) - .unwrap_or_else(|| home.join(".pi/agent")); + let agent_dir = user_pi_agent_dir(&home); paths.insert(agent_dir.join("mcp.json")); paths.insert(home.join(".config/mcp/mcp.json")); paths.insert(home.join(".agents/mcp.json")); @@ -118,10 +160,7 @@ fn mcp_config_paths() -> Vec { fn skill_roots() -> Vec { let mut roots = BTreeSet::new(); for home in metadata::user_home_dirs() { - let agent_dir = std::env::var_os("PI_CODING_AGENT_DIR") - .filter(|value| !value.is_empty()) - .map(PathBuf::from) - .unwrap_or_else(|| home.join(".pi/agent")); + let agent_dir = user_pi_agent_dir(&home); roots.insert(agent_dir.join("skills")); roots.insert(home.join(".agents/skills")); } @@ -139,7 +178,8 @@ mod tests { use serde_json::json; - use super::{is_pi, mcp_config_paths, package_version}; + use super::{is_pi, mcp_config_paths, package_version, skill_roots}; + use crate::provider::pi::user_pi_agent_dir; use crate::provider::vscode::discovery::mcp_servers_from_value; #[test] @@ -180,6 +220,165 @@ mod tests { let _ = fs::remove_dir_all(root); } + // npm-generated Windows global shims: package.json is beside these launchers + // under node_modules, rather than in an ancestor of the launcher itself. + const NPM_CMD_SHIM: &str = r#"@ECHO off +GOTO start +:find_dp0 +SET dp0=%~dp0 +EXIT /b +:start +SETLOCAL +CALL :find_dp0 + +IF EXIST "%dp0%\node.exe" ( + SET "_prog=%dp0%\node.exe" +) ELSE ( + SET "_prog=node" + SET PATHEXT=%PATHEXT:;.JS;=;% +) + +endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\node_modules\@earendil-works\pi-coding-agent\dist\bundle\cli.js" %* +"#; + + const NPM_POWERSHELL_SHIM: &str = r#"#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir/node$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/node$exe" "$basedir/node_modules/@earendil-works/pi-coding-agent/dist/bundle/cli.js" $args + } else { + & "$basedir/node$exe" "$basedir/node_modules/@earendil-works/pi-coding-agent/dist/bundle/cli.js" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "node$exe" "$basedir/node_modules/@earendil-works/pi-coding-agent/dist/bundle/cli.js" $args + } else { + & "node$exe" "$basedir/node_modules/@earendil-works/pi-coding-agent/dist/bundle/cli.js" $args + } + $ret=$LASTEXITCODE +} +exit $ret +"#; + + const NPM_SHELL_SHIM: &str = r#"#!/bin/sh +basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir=`cygpath -w "$basedir"` + fi + ;; +esac + +if [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/node_modules/@earendil-works/pi-coding-agent/dist/bundle/cli.js" "$@" +else + exec node "$basedir/node_modules/@earendil-works/pi-coding-agent/dist/bundle/cli.js" "$@" +fi +"#; + + fn npm_shim_fixture(name: &str, source: &str) -> (tempfile::TempDir, PathBuf) { + let root = tempfile::tempdir().unwrap(); + let package = root + .path() + .join("node_modules/@earendil-works/pi-coding-agent"); + fs::create_dir_all(package.join("dist/bundle")).unwrap(); + fs::write( + package.join("package.json"), + r#"{"name":"@earendil-works/pi-coding-agent","version":"0.85.1"}"#, + ) + .unwrap(); + fs::write(package.join("dist/bundle/cli.js"), "#!/usr/bin/env node\n").unwrap(); + let shim = root.path().join(name); + fs::write(&shim, source).unwrap(); + (root, shim) + } + + #[test] + fn accepts_npm_cmd_shim_and_reads_version() { + let (_root, shim) = npm_shim_fixture("pi.cmd", NPM_CMD_SHIM); + assert!(is_pi(&shim)); + assert_eq!(package_version(&shim).as_deref(), Some("0.85.1")); + } + + #[test] + fn accepts_npm_powershell_shim_and_reads_version() { + let (_root, shim) = npm_shim_fixture("pi.ps1", NPM_POWERSHELL_SHIM); + assert!(is_pi(&shim)); + assert_eq!(package_version(&shim).as_deref(), Some("0.85.1")); + } + + #[test] + fn accepts_npm_shell_shim_and_reads_version() { + let (_root, shim) = npm_shim_fixture("pi", NPM_SHELL_SHIM); + assert!(is_pi(&shim)); + assert_eq!(package_version(&shim).as_deref(), Some("0.85.1")); + } + + #[test] + fn rejects_unrelated_launchers_beside_pi_package() { + for (name, source) in [ + ("pi.cmd", NPM_CMD_SHIM), + ("pi.ps1", NPM_POWERSHELL_SHIM), + ("pi", NPM_SHELL_SHIM), + ] { + let unrelated = source + .replace("@earendil-works/pi-coding-agent", "unrelated-pi") + .replace(r"@earendil-works\pi-coding-agent", "unrelated-pi"); + let (_root, shim) = npm_shim_fixture(name, &unrelated); + assert!(!is_pi(&shim)); + assert_eq!(package_version(&shim), None); + } + } + + #[test] + fn rejects_npm_shim_without_matching_package_metadata() { + let (root, shim) = npm_shim_fixture("pi.cmd", NPM_CMD_SHIM); + let manifest = root + .path() + .join("node_modules/@earendil-works/pi-coding-agent/package.json"); + for contents in [ + r#"{"name":"unrelated-pi","version":"0.85.1"}"#, + r#"{"name":"@earendil-works/pi-coding-agent"}"#, + ] { + fs::write(&manifest, contents).unwrap(); + assert!(!is_pi(&shim)); + assert_eq!(package_version(&shim), None); + } + } + + #[test] + fn rejects_commented_npm_shim_references() { + let source = format!( + "{}\nexec unrelated-pi\n", + NPM_SHELL_SHIM + .lines() + .map(|line| format!("# {line}\n")) + .collect::() + ); + let (_root, shim) = npm_shim_fixture("pi", &source); + assert!(!is_pi(&shim)); + } + + #[test] + fn rejects_oversized_npm_shims() { + let source = format!("{NPM_CMD_SHIM}{}", " ".repeat(16 * 1024)); + let (_root, shim) = npm_shim_fixture("pi.cmd", &source); + assert!(!is_pi(&shim)); + } + #[test] fn reads_adapter_servers_without_secrets_or_arguments() { let servers = mcp_servers_from_value( @@ -225,11 +424,9 @@ mod tests { #[test] fn scans_pi_owned_and_shared_mcp_files() { let paths = mcp_config_paths(); - assert!( - paths - .iter() - .any(|path| path.ends_with(std::path::Path::new(".pi/agent/mcp.json"))) - ); + for home in crate::provider::metadata::user_home_dirs() { + assert!(paths.contains(&user_pi_agent_dir(&home).join("mcp.json"))); + } assert!( paths .iter() @@ -243,6 +440,78 @@ mod tests { ); } + #[test] + fn pi_agent_dir_overrides_match_all_consumers() { + const CHILD_HOME: &str = "AGENTDESKTOP_PI_PATH_TEST_HOME"; + const CHILD_EXPECTED: &str = "AGENTDESKTOP_PI_PATH_TEST_EXPECTED"; + if let Some(home) = std::env::var_os(CHILD_HOME) { + let home = PathBuf::from(home); + let expected = PathBuf::from(std::env::var_os(CHILD_EXPECTED).unwrap()); + assert_eq!(user_pi_agent_dir(&home), expected); + assert!(mcp_config_paths().contains(&expected.join("mcp.json"))); + assert!(skill_roots().contains(&expected.join("skills"))); + return; + } + + // Isolate environment overrides in a subprocess so parallel tests never + // observe changes to HOME, USERPROFILE, or PI_CODING_AGENT_DIR. + let home = tempfile::tempdir().unwrap(); + let absolute = home.path().join("absolute-agent"); + let cases = [ + ( + Some(Path::new("~/.pi/custom-agent")), + home.path().join(".pi/custom-agent"), + ), + (Some(Path::new("~")), home.path().to_owned()), + ( + Some(Path::new("relative-agent")), + PathBuf::from("relative-agent"), + ), + (Some(absolute.as_path()), absolute.clone()), + (Some(Path::new("")), home.path().join(".pi/agent")), + (None, home.path().join(".pi/agent")), + ( + Some(Path::new("~another-user/agent")), + PathBuf::from("~another-user/agent"), + ), + #[cfg(windows)] + ( + Some(Path::new(r"~\.pi\custom-agent")), + home.path().join(".pi/custom-agent"), + ), + #[cfg(unix)] + ( + Some(Path::new(r"~\.pi\custom-agent")), + PathBuf::from(r"~\.pi\custom-agent"), + ), + ]; + for (configured, expected) in cases { + let mut command = std::process::Command::new(std::env::current_exe().unwrap()); + command + .args([ + "--exact", + "provider::pi::discovery::tests::pi_agent_dir_overrides_match_all_consumers", + "--nocapture", + ]) + .env(CHILD_HOME, home.path()) + .env(CHILD_EXPECTED, expected) + .env("HOME", home.path()) + .env("USERPROFILE", home.path()); + if let Some(configured) = configured { + command.env("PI_CODING_AGENT_DIR", configured); + } else { + command.env_remove("PI_CODING_AGENT_DIR"); + } + let output = command.output().unwrap(); + assert!( + output.status.success(), + "PI_CODING_AGENT_DIR={configured:?}:\n{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + } + } + fn temporary(name: &str) -> PathBuf { std::env::temp_dir().join(format!("agentdesktop-{name}-{}", std::process::id())) } diff --git a/crates/agent/src/provider/pi/mod.rs b/crates/agent/src/provider/pi/mod.rs index 74686560..0909e41a 100644 --- a/crates/agent/src/provider/pi/mod.rs +++ b/crates/agent/src/provider/pi/mod.rs @@ -40,6 +40,11 @@ impl Provider for Pi { } fn plan(&self, ctx: &ReconcileContext, config: &DaemonConfig) -> anyhow::Result { + if !ctx.merge_user_settings && config.programs.pi.is_some() { + anyhow::bail!( + "Pi only reads user configuration; run agentdesktop with --user to manage programs.pi, or remove programs.pi from the system configuration" + ); + } let configured = config.programs.pi.as_ref().map(|provider| { let gateway = config .llm_gateway @@ -73,7 +78,15 @@ pub fn default_pi_settings_path() -> PathBuf { pub fn user_pi_agent_dir(home: &std::path::Path) -> PathBuf { std::env::var_os("PI_CODING_AGENT_DIR") .filter(|value| !value.is_empty()) - .map(PathBuf::from) + .map(|value| { + let path = PathBuf::from(value); + // Path components handle ~/ on every platform and ~\ on Windows, + // matching Pi's platform-specific tilde expansion. + match path.strip_prefix("~") { + Ok(relative) => home.join(relative), + Err(_) => path, + } + }) .unwrap_or_else(|| home.join(".pi/agent")) } @@ -111,7 +124,56 @@ fn windows_system_pi_agent_dir(system_drive: Option) -> Path #[cfg(test)] mod tests { - use super::windows_system_pi_agent_dir; + use agentdesktop_core::config::{DaemonConfig, parse_daemon}; + + use super::{Pi, Provider, ReconcileContext, windows_system_pi_agent_dir}; + + #[test] + fn rejects_configured_pi_in_system_mode() { + let root = tempfile::tempdir().unwrap(); + let pi = Pi { + models_path: root.path().join("models.json"), + settings_path: root.path().join("settings.json"), + }; + let ctx = ReconcileContext { + merge_user_settings: false, + credential_helper: root.path().join("agentdesktop"), + socket: root.path().join("agentdesktop.sock"), + }; + let config = + parse_daemon("programs:\n pi:\n managedConfig:\n providers: {}\n").unwrap(); + + let error = pi.plan(&ctx, &config).err().expect("Pi needs user mode"); + assert!(error.to_string().contains("--user")); + assert!(error.to_string().contains("programs.pi")); + assert!(!pi.models_path.exists()); + assert!(!pi.settings_path.exists()); + } + + #[test] + fn allows_absent_pi_cleanup_in_system_mode() { + let root = tempfile::tempdir().unwrap(); + let pi = Pi { + models_path: root.path().join("models.json"), + settings_path: root.path().join("settings.json"), + }; + let mut ctx = ReconcileContext { + merge_user_settings: true, + credential_helper: root.path().join("agentdesktop"), + socket: root.path().join("agentdesktop.sock"), + }; + let config = + parse_daemon("programs:\n pi:\n managedConfig:\n providers: {}\n").unwrap(); + pi.plan(&ctx, &config).unwrap().apply().unwrap(); + assert!(pi.models_path.exists()); + + ctx.merge_user_settings = false; + pi.plan(&ctx, &DaemonConfig::default()) + .unwrap() + .apply() + .unwrap(); + assert!(!pi.models_path.exists()); + } #[test] fn windows_system_pi_agent_dir_uses_system_drive_root() { diff --git a/crates/agent/src/provider/pi/reconcile.rs b/crates/agent/src/provider/pi/reconcile.rs index ad7141ac..f7aeea69 100644 --- a/crates/agent/src/provider/pi/reconcile.rs +++ b/crates/agent/src/provider/pi/reconcile.rs @@ -26,10 +26,18 @@ pub(super) fn plan( let models_state = json_merge::state_path(models_path); let settings_state = json_merge::state_path(settings_path); let Some((config, gateway)) = config else { - json_merge::plan_remove(models_path, &models_state, "models", Pi::DISPLAY_NAME, plan)?; + json_merge::plan_remove( + models_path, + &models_state, + json_merge::JsonFormat::Jsonc, + "models", + Pi::DISPLAY_NAME, + plan, + )?; json_merge::plan_remove( settings_path, &settings_state, + json_merge::JsonFormat::Json, "settings", Pi::DISPLAY_NAME, plan, @@ -40,6 +48,7 @@ pub(super) fn plan( json_merge::plan_merge( models_path, &models_state, + json_merge::JsonFormat::Jsonc, managed_models(config, gateway, credential_helper, socket)?, false, "models", @@ -49,6 +58,7 @@ pub(super) fn plan( json_merge::plan_merge( settings_path, &settings_state, + json_merge::JsonFormat::Json, managed_settings(config, gateway)?, false, "settings", @@ -105,9 +115,10 @@ fn managed_models( let mut models = Vec::new(); for (id, mut entry) in catalog_entries(config, default_model) { - if entry.get("id").and_then(Value::as_str).is_none() { - entry.insert("id".to_owned(), json!(id)); - } + entry.insert("id".to_owned(), json!(id)); + let model_api = entry.get("api").and_then(Value::as_str).unwrap_or(api); + let base_url = gateway_base_url(gateway, model_api); + entry.insert("baseUrl".to_owned(), json!(base_url)); models.push(Value::Object(entry)); } @@ -170,11 +181,196 @@ fn gateway_base_url(gateway: &LlmGatewayConfig, api: &str) -> String { #[cfg(test)] mod tests { + use std::fs; use std::path::Path; use agentdesktop_core::config::parse_daemon; + use serde_json::{Value, json}; + + use super::{managed_models, managed_settings, plan}; + use crate::reconcile::ReconcilePlan; + + #[test] + fn gateway_overrides_per_model_endpoints() { + let config = parse_daemon( + r#" +llmGateway: + url: https://gateway.example.com/proxy +programs: + pi: + model: claude + models: + claude: + baseUrl: https://outside.example.com + chat: + api: openai-completions + baseUrl: https://outside.example.com/v1 + responses: + api: openai-responses +"#, + ) + .unwrap(); + let managed = managed_models( + config.programs.pi.as_ref().unwrap(), + config.llm_gateway.as_ref(), + Path::new("agentdesktop"), + Path::new("agentdesktop.sock"), + ) + .unwrap(); + for model in managed["providers"]["agentdesktop"]["models"] + .as_array() + .unwrap() + { + let expected = if model["id"] == "claude" { + "https://gateway.example.com/proxy" + } else { + "https://gateway.example.com/proxy/v1" + }; + assert_eq!(model["baseUrl"], expected, "model {}", model["id"]); + } + } - use super::{managed_models, managed_settings}; + #[test] + fn model_map_keys_define_catalog_ids() { + let config = parse_daemon( + r#" +llmGateway: + url: https://gateway.example.com +programs: + pi: + model: selected + models: + selected: + id: different + name: Selected Model +"#, + ) + .unwrap(); + let pi = config.programs.pi.as_ref().unwrap(); + let gateway = config.llm_gateway.as_ref(); + let managed = managed_models( + pi, + gateway, + Path::new("agentdesktop"), + Path::new("agentdesktop.sock"), + ) + .unwrap(); + let settings = managed_settings(pi, gateway).unwrap(); + let model = &managed["providers"]["agentdesktop"]["models"][0]; + assert_eq!(model["id"], settings["defaultModel"]); + assert_eq!(model["name"], "Selected Model"); + } + + #[test] + fn commented_models_survive_merge_repeat_and_removal() { + let root = tempfile::tempdir().unwrap(); + let models = root.path().join("models.json"); + let settings = root.path().join("settings.json"); + fs::write( + &models, + "\u{feff}{\n // Personal provider\n \"providers\": {\"local\": {\"baseUrl\": \"http://localhost:11434/v1\",},},\n}\n", + ) + .unwrap(); + fs::write(&settings, r#"{"theme":"dark"}"#).unwrap(); + let config = parse_daemon( + "llmGateway:\n url: https://gateway.example.com\nprograms:\n pi:\n model: selected\n", + ) + .unwrap(); + let configured = Some(( + config.programs.pi.as_ref().unwrap(), + config.llm_gateway.as_ref(), + )); + let helper = root.path().join("agentdesktop"); + let socket = root.path().join("agentdesktop.sock"); + let changes = ReconcilePlan::default(); + plan(&models, &settings, &helper, &socket, configured, &changes).unwrap(); + assert!(!changes.has_conflicts(), "Pi accepts commented models.json"); + changes.apply().unwrap(); + let merged: Value = serde_json::from_slice(&fs::read(&models).unwrap()).unwrap(); + assert_eq!( + merged["providers"]["local"]["baseUrl"], + "http://localhost:11434/v1" + ); + assert_eq!( + merged["providers"]["agentdesktop"]["models"][0]["id"], + "selected" + ); + + let before = fs::read(&models).unwrap(); + let repeated = ReconcilePlan::default(); + plan(&models, &settings, &helper, &socket, configured, &repeated).unwrap(); + assert!(repeated.render().contains("Summary: 0 changes")); + repeated.apply().unwrap(); + assert_eq!(fs::read(&models).unwrap(), before); + + // A user can add comments while agentdesktop manages the file. + fs::write( + &models, + format!("// Updated comment\n{}", String::from_utf8(before).unwrap()), + ) + .unwrap(); + let removal = ReconcilePlan::default(); + plan(&models, &settings, &helper, &socket, None, &removal).unwrap(); + removal.apply().unwrap(); + let restored: Value = serde_json::from_slice(&fs::read(&models).unwrap()).unwrap(); + assert_eq!( + restored, + json!({"providers":{"local":{"baseUrl":"http://localhost:11434/v1"}}}) + ); + let restored_settings: Value = + serde_json::from_slice(&fs::read(&settings).unwrap()).unwrap(); + assert_eq!(restored_settings, json!({"theme":"dark"})); + assert!(!super::json_merge::state_path(&models).exists()); + assert!(!super::json_merge::state_path(&settings).exists()); + } + + #[test] + fn commented_settings_remain_a_conflict() { + let root = tempfile::tempdir().unwrap(); + let models = root.path().join("models.json"); + let settings = root.path().join("settings.json"); + let existing = b"// Pi settings require strict JSON\n{\"theme\":\"dark\"}"; + fs::write(&settings, existing).unwrap(); + let config = parse_daemon("programs:\n pi: {}\n").unwrap(); + let changes = ReconcilePlan::default(); + plan( + &models, + &settings, + Path::new("agentdesktop"), + Path::new("agentdesktop.sock"), + Some((config.programs.pi.as_ref().unwrap(), None)), + &changes, + ) + .unwrap(); + assert!(changes.has_conflicts()); + assert!(changes.apply().is_err()); + assert_eq!(fs::read(&settings).unwrap(), existing); + assert!(!models.exists()); + } + + #[test] + fn malformed_models_prevent_settings_writes() { + let root = tempfile::tempdir().unwrap(); + let models = root.path().join("models.json"); + let settings = root.path().join("settings.json"); + let invalid = b"{\"providers\":"; + fs::write(&models, invalid).unwrap(); + let config = parse_daemon("programs:\n pi: {}\n").unwrap(); + let changes = ReconcilePlan::default(); + plan( + &models, + &settings, + Path::new("agentdesktop"), + Path::new("agentdesktop.sock"), + Some((config.programs.pi.as_ref().unwrap(), None)), + &changes, + ) + .unwrap(); + assert!(changes.has_conflicts()); + assert!(changes.apply().is_err()); + assert_eq!(fs::read(&models).unwrap(), invalid); + assert!(!settings.exists()); + } #[test] fn pass_through_settings_are_merged_with_managed_gateway_values() { diff --git a/crates/agent/src/reconcile/mod.rs b/crates/agent/src/reconcile/mod.rs index 0205eba2..09e355ce 100644 --- a/crates/agent/src/reconcile/mod.rs +++ b/crates/agent/src/reconcile/mod.rs @@ -333,6 +333,21 @@ programs: assert!(!root.exists(), "preflight failure must not write any files"); } + #[test] + fn system_mode_rejects_pi_before_writing_other_settings() { + let fixture = Fixture::new(); + let config = parse_daemon("programs:\n claudeCode: {}\n pi: {}\n").unwrap(); + let error = fixture + .reconciler + .apply(&config) + .expect_err("Pi requires user mode"); + assert!(error.to_string().contains("--user")); + assert!( + !fixture.root.exists(), + "preflight failure must not write any files" + ); + } + #[test] fn user_mode_reconciles_pi_models_and_settings() { let root = std::env::temp_dir().join(format!( @@ -373,6 +388,30 @@ programs: assert!(models.contains("claude-sonnet-4-5")); let settings = String::from_utf8(fs::read(root.join("pi/settings.json")).unwrap()).unwrap(); assert!(settings.contains("defaultProvider")); + + let repeated = reconciler.plan(&config).unwrap(); + assert!(repeated.render().contains("Summary: 0 changes")); + repeated.apply().unwrap(); + assert_eq!( + fs::read_to_string(root.join("pi/models.json")).unwrap(), + models + ); + assert_eq!( + fs::read_to_string(root.join("pi/settings.json")).unwrap(), + settings + ); + + reconciler + .apply(&parse_daemon("programs: {}").unwrap()) + .unwrap(); + for path in [ + "models.json", + ".models.json.agentdesktop", + "settings.json", + ".settings.json.agentdesktop", + ] { + assert!(!root.join("pi").join(path).exists()); + } fs::remove_dir_all(root).unwrap(); } @@ -501,8 +540,6 @@ programs: company-model: {} grok: model: grok-4.6 - pi: - model: claude-sonnet-4-5 "#, ) .unwrap(); @@ -525,10 +562,6 @@ programs: "opencode/config.json", "opencode/plugin.js", "grok/managed_config.toml", - "pi/models.json", - "pi/.models.json.agentdesktop", - "pi/settings.json", - "pi/.settings.json.agentdesktop", ]; let contents: Vec<_> = paths .iter() diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs index e99ba56c..ab7d350f 100644 --- a/crates/core/src/config.rs +++ b/crates/core/src/config.rs @@ -640,7 +640,7 @@ pub struct GrokConfig { /// /// Generated `providers.agentdesktop` values take precedence over overlapping /// `managedConfig` keys. Pi stores configuration under `~/.pi/agent` (or -/// `PI_CODING_AGENT_DIR`), so `--user` is the supported management mode. +/// `PI_CODING_AGENT_DIR`), so management requires `--user`. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -656,9 +656,10 @@ pub struct PiConfig { pub model: Option, /// Extra Pi model objects, keyed by model ID. /// - /// Each value is an arbitrary Pi `models.json` model object. Generated - /// gateway `baseUrl` and `apiKey` values take precedence. When this map is - /// non-empty, `model` must name one of its keys. + /// Each value is an arbitrary Pi `models.json` model object. The map key + /// sets its `id`. Generated gateway `baseUrl` and `apiKey` values take + /// precedence, including per-model URLs. When this map is non-empty, + /// `model` must name one of its keys. #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub models: BTreeMap, /// Pi API dialect for the managed provider. Defaults to `anthropic-messages`. diff --git a/examples/claude/README.md b/examples/claude/README.md index cbc03bfb..97dcdc9b 100644 --- a/examples/claude/README.md +++ b/examples/claude/README.md @@ -70,6 +70,19 @@ agentdesktop Now that agentdesktop is running, Claude Code can be run. It will show the configured `Managed by Agentdesktop` and direct traffic through the gateway. +## Pi + +Pi requires user-mode configuration. To include Pi, remove `programs.claudeDesktop` +from `claude-code.yaml` and uncomment `programs.pi`. Run the daemon as the user +who runs Pi: + +```console +agentdesktop daemon --user --config examples/claude/agentdesktop.yaml +``` + +After enrollment, run `pi`; the managed default model uses the gateway. Keep the +daemon running so Pi can request gateway credentials. + ## Stop the scenario Stop the foreground daemon and controller with Ctrl-C, then stop Dex and diff --git a/examples/claude/claude-code.yaml b/examples/claude/claude-code.yaml index b37a4a26..d5e9a37c 100644 --- a/examples/claude/claude-code.yaml +++ b/examples/claude/claude-code.yaml @@ -16,6 +16,7 @@ programs: companyAnnouncements: ["Managed by Agentdesktop"] claudeDesktop: isLocalDevMcpEnabled: true - pi: - useLlmGateway: true - model: claude-sonnet-4-5 + # Pi requires --user. Remove claudeDesktop before enabling Pi in user mode. + # pi: + # useLlmGateway: true + # model: claude-sonnet-4-5 diff --git a/examples/standalone/README.md b/examples/standalone/README.md index 6198fdcd..120162e4 100644 --- a/examples/standalone/README.md +++ b/examples/standalone/README.md @@ -62,13 +62,20 @@ $ agentdesktop daemon --config examples/standalone/config.yaml --user When we run `claude`, we can now see the message we programmed (`Using the local Agentgateway through Agentdesktop`), and messages we send will traverse Agentgateway. +## Pi + +The user-mode configuration also enables Pi with `claude-sonnet-4-5`. Run `pi` +while the daemon is running to use that model through the gateway. Pi requires +`--user`; authenticated gateway configuration cannot be applied with `--once`. + ## Claude Desktop In the Claude Code example, we ran with `--user`. This runs the daemon as an unprivileged user. Claude Desktop, however, requires root configuration. -Uncomment the Claude Desktop configuration in `config.yaml` and save the file. +Remove `programs.pi` and uncomment the Claude Desktop configuration in +`config.yaml`, then save the file. Then run the daemon as root: ```sh diff --git a/examples/standalone/config.yaml b/examples/standalone/config.yaml index 351ffa1f..2a9316b6 100644 --- a/examples/standalone/config.yaml +++ b/examples/standalone/config.yaml @@ -15,6 +15,6 @@ programs: pi: useLlmGateway: true model: claude-sonnet-4-5 -# Claude desktop cannot run with --user. Uncomment to enable +# Claude Desktop requires system mode. Remove pi above before enabling it. # claudeDesktop: # useLlmGateway: true diff --git a/schema/daemon-config.json b/schema/daemon-config.json index bcb49067..3756ca34 100644 --- a/schema/daemon-config.json +++ b/schema/daemon-config.json @@ -418,7 +418,7 @@ "additionalProperties": false }, "PiConfig": { - "description": "Settings reconciled into Pi's user `models.json` and `settings.json`.\n\nGenerated `providers.agentdesktop` values take precedence over overlapping\n`managedConfig` keys. Pi stores configuration under `~/.pi/agent` (or\n`PI_CODING_AGENT_DIR`), so `--user` is the supported management mode.", + "description": "Settings reconciled into Pi's user `models.json` and `settings.json`.\n\nGenerated `providers.agentdesktop` values take precedence over overlapping\n`managedConfig` keys. Pi stores configuration under `~/.pi/agent` (or\n`PI_CODING_AGENT_DIR`), so management requires `--user`.", "type": "object", "properties": { "api": { @@ -441,7 +441,7 @@ ] }, "models": { - "description": "Extra Pi model objects, keyed by model ID.\n\nEach value is an arbitrary Pi `models.json` model object. Generated\ngateway `baseUrl` and `apiKey` values take precedence. When this map is\nnon-empty, `model` must name one of its keys.", + "description": "Extra Pi model objects, keyed by model ID.\n\nEach value is an arbitrary Pi `models.json` model object. The map key\nsets its `id`. Generated gateway `baseUrl` and `apiKey` values take\nprecedence, including per-model URLs. When this map is non-empty,\n`model` must name one of its keys.", "type": "object", "additionalProperties": true }, diff --git a/schema/daemon-config.md b/schema/daemon-config.md index a0baf07c..20a699c4 100644 --- a/schema/daemon-config.md +++ b/schema/daemon-config.md @@ -63,7 +63,7 @@ |`programs.pi.api`|string|Pi API dialect for the managed provider. Defaults to `anthropic-messages`.| |`programs.pi.managedConfig`|object|Arbitrary values merged into Pi's `models.json`.| |`programs.pi.model`|string|Model ID selected when pointing Pi at the LLM gateway.

This is required when a top-level `llmGateway` is configured. If `models`
is empty, agentdesktop creates a catalog entry with this ID.| -|`programs.pi.models`|object|Extra Pi model objects, keyed by model ID.

Each value is an arbitrary Pi `models.json` model object. Generated
gateway `baseUrl` and `apiKey` values take precedence. When this map is
non-empty, `model` must name one of its keys.| +|`programs.pi.models`|object|Extra Pi model objects, keyed by model ID.

Each value is an arbitrary Pi `models.json` model object. The map key
sets its `id`. Generated gateway `baseUrl` and `apiKey` values take
precedence, including per-model URLs. When this map is non-empty,
`model` must name one of its keys.| |`programs.pi.useLlmGateway`|boolean|Whether this program uses the top-level LLM gateway.| |`sandbox`|object|Local execution sandbox required for managed developer tools.| |`sandbox.filesystem`|object|Filesystem access available to sandboxed commands.| From a248ec50a3694b447f659d2f3fb3eadc05edc321 Mon Sep 17 00:00:00 2001 From: Sebastian Maniak Date: Fri, 18 Sep 2026 22:57:04 -0400 Subject: [PATCH 08/12] fix: discover Pi PowerShell shims on Windows --- crates/agent/src/provider/pi/discovery.rs | 105 +++++++++++++++++++--- 1 file changed, 93 insertions(+), 12 deletions(-) diff --git a/crates/agent/src/provider/pi/discovery.rs b/crates/agent/src/provider/pi/discovery.rs index 9c500010..92a10c98 100644 --- a/crates/agent/src/provider/pi/discovery.rs +++ b/crates/agent/src/provider/pi/discovery.rs @@ -33,13 +33,6 @@ fn executable_candidates() -> Vec { for home in metadata::user_home_dirs() { candidates.insert(home.join(".local/bin/pi")); candidates.insert(home.join(".npm-global/bin/pi")); - #[cfg(windows)] - { - candidates.insert(home.join(".local/bin/pi.exe")); - candidates.insert(home.join("AppData/Roaming/npm/pi.cmd")); - candidates.insert(home.join("AppData/Roaming/npm/pi.exe")); - candidates.insert(home.join("AppData/Local/pnpm/pi.exe")); - } } #[cfg(target_os = "macos")] candidates.extend([ @@ -51,9 +44,32 @@ fn executable_candidates() -> Vec { PathBuf::from("/usr/bin/pi"), PathBuf::from("/usr/local/bin/pi"), ]); + #[cfg(windows)] + { + windows_executable_candidates(candidates) + } + #[cfg(not(windows))] candidates.into_iter().collect() } +#[cfg(any(windows, test))] +fn windows_executable_candidates(mut candidates: BTreeSet) -> Vec { + for home in metadata::user_home_dirs() { + candidates.extend([ + home.join(".local/bin/pi.exe"), + home.join("AppData/Roaming/npm/pi.cmd"), + home.join("AppData/Roaming/npm/pi.exe"), + home.join("AppData/Roaming/npm/pi.ps1"), + home.join("AppData/Local/pnpm/pi.exe"), + ]); + } + // PowerShell shims may be absent from PATHEXT; keep PATH order before fallbacks. + metadata::find_all_in_path("pi.ps1") + .into_iter() + .chain(candidates) + .collect() +} + fn is_pi(executable: &Path) -> bool { package_manifest(executable).is_some() } @@ -291,9 +307,12 @@ fi fn npm_shim_fixture(name: &str, source: &str) -> (tempfile::TempDir, PathBuf) { let root = tempfile::tempdir().unwrap(); - let package = root - .path() - .join("node_modules/@earendil-works/pi-coding-agent"); + let shim = write_npm_shim(root.path(), name, source); + (root, shim) + } + + fn write_npm_shim(prefix: &Path, name: &str, source: &str) -> PathBuf { + let package = prefix.join("node_modules/@earendil-works/pi-coding-agent"); fs::create_dir_all(package.join("dist/bundle")).unwrap(); fs::write( package.join("package.json"), @@ -301,9 +320,71 @@ fi ) .unwrap(); fs::write(package.join("dist/bundle/cli.js"), "#!/usr/bin/env node\n").unwrap(); - let shim = root.path().join(name); + let shim = prefix.join(name); fs::write(&shim, source).unwrap(); - (root, shim) + shim + } + + #[test] + fn discovers_powershell_only_npm_installs_without_pathext() { + const EXPECTED_SHIM: &str = "AGENTDESKTOP_PI_PS1_TEST_SHIM"; + if let Some(expected) = std::env::var_os(EXPECTED_SHIM) { + #[cfg(windows)] + let executable = super::discover() + .expect("Pi should be discovered") + .executable; + // Exercise Windows candidate discovery on other platforms too. + #[cfg(not(windows))] + let executable = super::windows_executable_candidates(Default::default()) + .into_iter() + .filter(|path| path.is_file()) + .find(|path| is_pi(path)) + .expect("PowerShell-only Pi should be discovered"); + assert_eq!(executable, PathBuf::from(expected)); + return; + } + + for use_path in [false, true] { + let home = tempfile::tempdir().unwrap(); + let prefix = home.path().join(if use_path { + "z-custom-npm-prefix" + } else { + "AppData/Roaming/npm" + }); + let shim = write_npm_shim(&prefix, "pi.ps1", NPM_POWERSHELL_SHIM); + let path = if use_path { + // The first PATH match wins over later entries and fallback locations. + let later_prefix = home.path().join("a-older-npm-prefix"); + write_npm_shim(&later_prefix, "pi.ps1", NPM_POWERSHELL_SHIM); + write_npm_shim( + &home.path().join("AppData/Roaming/npm"), + "pi.ps1", + NPM_POWERSHELL_SHIM, + ); + std::env::join_paths([&prefix, &later_prefix]).unwrap() + } else { + Default::default() + }; + let output = std::process::Command::new(std::env::current_exe().unwrap()) + .args([ + "--exact", + "provider::pi::discovery::tests::discovers_powershell_only_npm_installs_without_pathext", + "--nocapture", + ]) + .env(EXPECTED_SHIM, &shim) + .env("HOME", home.path()) + .env("USERPROFILE", home.path()) + .env("PATH", path) + .env("PATHEXT", ".COM;.EXE;.BAT;.CMD") + .output() + .unwrap(); + assert!( + output.status.success(), + "Pi with only pi.ps1 (on PATH: {use_path}):\n{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + } } #[test] From effafbddf7b8e10fe5b7b9b40f9de911862a409b Mon Sep 17 00:00:00 2001 From: Sebastian Maniak Date: Sat, 19 Sep 2026 08:00:24 -0400 Subject: [PATCH 09/12] fix: protect Pi credentials and replace managed model catalogs --- crates/agent/src/provider/README.md | 9 +- .../src/provider/claude_code/reconcile.rs | 4 +- .../src/provider/claude_desktop/reconcile.rs | 2 +- crates/agent/src/provider/json_merge.rs | 94 +++++- crates/agent/src/provider/pi/reconcile.rs | 289 +++++++++++++++++- 5 files changed, 382 insertions(+), 16 deletions(-) diff --git a/crates/agent/src/provider/README.md b/crates/agent/src/provider/README.md index a432cecb..89c1b193 100644 --- a/crates/agent/src/provider/README.md +++ b/crates/agent/src/provider/README.md @@ -15,13 +15,20 @@ gateway configuration also requires a running daemon and rejects `--once`. User mode merges `providers.agentdesktop` into `models.json` and sets the default model in `settings.json`. Model map keys determine catalog IDs, and gateway URLs override per-model endpoints, using each model's API dialect. +The gateway owns the entire `providers.agentdesktop.models` catalog while +enabled; stale entries are replaced, and the prior catalog is restored on +cleanup. Other providers and user additions are preserved. On Unix, +`models.json` is written with owner-only permissions (`0600`), including +cleanup, and reconciliation repairs permissions on existing managed files. Commented `models.json` files are accepted and rewritten as standard JSON, preserving user configuration values. `settings.json` requires standard JSON. Discovery recognizes npm's Windows command shims as well as symlinked launchers, verifying the Pi package manifest without executing the launcher. MCP inventory comes from `pi-mcp-adapter` config files (`~/.pi/agent/mcp.json`, `.pi/mcp.json`, `.mcp.json`, and shared -`~/.config/mcp/mcp.json` / `~/.agents/mcp.json`). Host-specific Cursor/Claude +`~/.config/mcp/mcp.json` / `~/.agents/mcp.json` / `~/.agents/mcp/mcp.json`). +Both `.agents` paths are supported by the [adapter's configuration loader](https://github.com/nicobailon/pi-mcp-adapter/blob/33bdc38d8dd3802f2b51ba1fe37e30ed7ae6a29a/config.ts#L15-L19). +Host-specific Cursor/Claude files are not scanned until the adapter imports them into a Pi-owned file. ✅ Implemented · ◯ Not implemented · — Not applicable diff --git a/crates/agent/src/provider/claude_code/reconcile.rs b/crates/agent/src/provider/claude_code/reconcile.rs index 19b92f8e..6935e6ce 100644 --- a/crates/agent/src/provider/claude_code/reconcile.rs +++ b/crates/agent/src/provider/claude_code/reconcile.rs @@ -52,7 +52,7 @@ pub(super) fn plan( && json_merge::plan_remove( path, &merge_state_path, - json_merge::JsonFormat::Json, + json_merge::MergeOptions::default(), "settings", ClaudeCode::DISPLAY_NAME, plan, @@ -69,7 +69,7 @@ pub(super) fn plan( json_merge::plan_merge( path, &merge_state_path, - json_merge::JsonFormat::Json, + json_merge::MergeOptions::default(), settings, is_owned(&owner_path, plan)?, "settings", diff --git a/crates/agent/src/provider/claude_desktop/reconcile.rs b/crates/agent/src/provider/claude_desktop/reconcile.rs index 41788ed4..3d7ce9cd 100644 --- a/crates/agent/src/provider/claude_desktop/reconcile.rs +++ b/crates/agent/src/provider/claude_desktop/reconcile.rs @@ -32,7 +32,7 @@ pub(super) fn plan( || !json_merge::plan_remove( settings_path, &settings_state, - json_merge::JsonFormat::Json, + json_merge::MergeOptions::default(), "managed settings", ClaudeDesktop::DISPLAY_NAME, plan, diff --git a/crates/agent/src/provider/json_merge.rs b/crates/agent/src/provider/json_merge.rs index 38ecd09c..8ffa4a96 100644 --- a/crates/agent/src/provider/json_merge.rs +++ b/crates/agent/src/provider/json_merge.rs @@ -22,6 +22,23 @@ pub(super) enum JsonFormat { Jsonc, } +pub(super) struct MergeOptions { + pub format: JsonFormat, + pub permissions: u32, + /// JSON pointers to values managed as a whole, rather than merged additively. + pub replace_paths: &'static [&'static str], +} + +impl Default for MergeOptions { + fn default() -> Self { + Self { + format: JsonFormat::Json, + permissions: 0o644, + replace_paths: &[], + } + } +} + impl JsonFormat { fn parse(&self, contents: &[u8]) -> anyhow::Result { match self { @@ -47,7 +64,7 @@ pub(super) fn state_path(path: &Path) -> PathBuf { pub(super) fn plan_merge( path: &Path, state_path: &Path, - format: JsonFormat, + options: MergeOptions, managed: Value, legacy_owned: bool, description: &str, @@ -69,7 +86,7 @@ pub(super) fn plan_merge( .unwrap_or(existing.is_none() || legacy_owned); let mut combined = match existing.as_deref() { - Some(contents) => match format.parse(contents) { + Some(contents) => match options.format.parse(contents) { Ok(Value::Object(object)) => Value::Object(object), Ok(_) | Err(_) => { plan.record(display_name, description, "conflict", path); @@ -85,13 +102,25 @@ pub(super) fn plan_merge( combined = json!({}); } let before = combined.clone(); + // Snapshot the user's original values before replacing any owned subtree. + for pointer in options.replace_paths { + if managed.pointer(pointer).is_some() + && let Some(existing) = combined.pointer_mut(pointer) + { + *existing = Value::Null; + } + } merge_overlay(&mut combined, managed); let mut contents = serde_json::to_vec_pretty(&combined) .with_context(|| format!("serialize merged {display_name}"))?; contents.push(b'\n'); let action = match existing.as_deref() { - Some(existing) if existing == contents => "unchanged", + Some(existing) + if existing == contents && !needs_permission_update(path, options.permissions)? => + { + "unchanged" + } Some(_) => "update", None => "create", }; @@ -105,7 +134,7 @@ pub(super) fn plan_merge( ); if action != "unchanged" { - plan.write_file(path, &contents, 0o644)?; + plan.write_file(path, &contents, options.permissions)?; } let mut state = serde_json::to_vec_pretty(&MergeState { created, @@ -122,7 +151,7 @@ pub(super) fn plan_merge( pub(super) fn plan_remove( path: &Path, state_path: &Path, - format: JsonFormat, + options: MergeOptions, description: &str, display_name: &str, plan: &ReconcilePlan, @@ -142,7 +171,7 @@ pub(super) fn plan_remove( .with_context(|| format!("read {display_name} from {}", path.display())); } }; - let settings = match format.parse(&existing) { + let settings = match options.format.parse(&existing) { Ok(Value::Object(object)) => Value::Object(object), Ok(_) | Err(_) => { plan.record(display_name, description, "conflict", path); @@ -161,7 +190,11 @@ pub(super) fn plan_remove( }; let action = match proposed.as_deref() { None => "remove", - Some(contents) if contents == existing => "unchanged", + Some(contents) + if contents == existing && !needs_permission_update(path, options.permissions)? => + { + "unchanged" + } Some(_) => "update", }; plan.record_diff( @@ -178,13 +211,31 @@ pub(super) fn plan_remove( } else if let Some(contents) = proposed && action == "update" { - plan.write_file(path, &contents, 0o644)?; + plan.write_file(path, &contents, options.permissions)?; } remove_file(state_path, display_name, plan)?; debug!(provider = display_name, action, path = %path.display(), "planned removal of managed values from user settings"); Ok(true) } +fn needs_permission_update(path: &Path, permissions: u32) -> anyhow::Result { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + let existing = std::fs::metadata(path) + .with_context(|| format!("inspect permissions at {}", path.display()))? + .permissions() + .mode(); + Ok(existing & 0o777 & !permissions != 0) + } + #[cfg(not(unix))] + { + let _ = (path, permissions); + Ok(false) + } +} + fn read_state( path: &Path, display_name: &str, @@ -292,6 +343,33 @@ fn rollback_value( let after = after .and_then(Value::as_array) .map_or(&[][..], Vec::as_slice); + // Additive merges keep the original array as a prefix. A replacement + // must restore its order and duplicate entries, then retain user edits. + if !after.starts_with(before) { + let mut result = before.to_vec(); + let mut restored = before.to_vec(); + let mut added = current_array.clone(); + for value in after { + if let Some(index) = restored.iter().position(|entry| entry == value) { + restored.remove(index); + } + if let Some(index) = added.iter().position(|entry| entry == value) { + added.remove(index); + } else if let Some(index) = result.iter().position(|entry| entry == value) { + // Respect a user's removal of a retained original entry. + result.remove(index); + } + } + for value in added { + if let Some(index) = restored.iter().position(|entry| entry == &value) { + // An original entry restored by the user is already present. + restored.remove(index); + } else { + result.push(value); + } + } + return Some(Value::Array(result)); + } let mut result = current_array.clone(); for added in after.iter().filter(|value| !before.contains(value)) { if let Some(index) = result.iter().position(|value| value == added) { diff --git a/crates/agent/src/provider/pi/reconcile.rs b/crates/agent/src/provider/pi/reconcile.rs index f7aeea69..a0e3065c 100644 --- a/crates/agent/src/provider/pi/reconcile.rs +++ b/crates/agent/src/provider/pi/reconcile.rs @@ -14,6 +14,11 @@ use crate::reconcile::ReconcilePlan; const PROVIDER_NAME: &str = "agentdesktop"; const DEFAULT_API: &str = "anthropic-messages"; +const MODELS_OPTIONS: json_merge::MergeOptions = json_merge::MergeOptions { + format: json_merge::JsonFormat::Jsonc, + permissions: 0o600, + replace_paths: &[], +}; pub(super) fn plan( models_path: &Path, @@ -29,7 +34,7 @@ pub(super) fn plan( json_merge::plan_remove( models_path, &models_state, - json_merge::JsonFormat::Jsonc, + MODELS_OPTIONS, "models", Pi::DISPLAY_NAME, plan, @@ -37,7 +42,7 @@ pub(super) fn plan( json_merge::plan_remove( settings_path, &settings_state, - json_merge::JsonFormat::Json, + json_merge::MergeOptions::default(), "settings", Pi::DISPLAY_NAME, plan, @@ -48,7 +53,14 @@ pub(super) fn plan( json_merge::plan_merge( models_path, &models_state, - json_merge::JsonFormat::Jsonc, + json_merge::MergeOptions { + replace_paths: if gateway.is_some() { + &["/providers/agentdesktop/models"] + } else { + &[] + }, + ..MODELS_OPTIONS + }, managed_models(config, gateway, credential_helper, socket)?, false, "models", @@ -58,7 +70,7 @@ pub(super) fn plan( json_merge::plan_merge( settings_path, &settings_state, - json_merge::JsonFormat::Json, + json_merge::MergeOptions::default(), managed_settings(config, gateway)?, false, "settings", @@ -190,6 +202,275 @@ mod tests { use super::{managed_models, managed_settings, plan}; use crate::reconcile::ReconcilePlan; + fn reconcile_fixture(models: &Path, config: Option<&str>) -> ReconcilePlan { + let config = config.map(|yaml| parse_daemon(yaml).unwrap()); + let changes = ReconcilePlan::default(); + plan( + models, + &models.with_file_name("settings.json"), + Path::new("agentdesktop"), + Path::new("agentdesktop.sock"), + config.as_ref().map(|config| { + ( + config.programs.pi.as_ref().unwrap(), + config.llm_gateway.as_ref(), + ) + }), + &changes, + ) + .unwrap(); + changes + } + + #[cfg(unix)] + #[test] + fn models_create_and_merge_use_private_permissions() { + use std::os::unix::fs::PermissionsExt; + + for existing_mode in [None, Some(0o600), Some(0o644)] { + let root = tempfile::tempdir().unwrap(); + let models = root.path().join("models.json"); + if let Some(mode) = existing_mode { + fs::write( + &models, + r#"{"providers":{"personal":{"apiKey":"test-secret"}}}"#, + ) + .unwrap(); + fs::set_permissions(&models, fs::Permissions::from_mode(mode)).unwrap(); + } + let changes = reconcile_fixture(&models, Some("programs:\n pi: {}\n")); + if let Some(mode) = existing_mode { + assert_eq!( + fs::metadata(&models).unwrap().permissions().mode() & 0o777, + mode + ); + } else { + assert!(!models.exists(), "planning must not write files"); + } + changes.apply().unwrap(); + assert_eq!( + fs::metadata(&models).unwrap().permissions().mode() & 0o077, + 0 + ); + if existing_mode.is_some() { + let merged: Value = serde_json::from_slice(&fs::read(&models).unwrap()).unwrap(); + assert_eq!(merged["providers"]["personal"]["apiKey"], "test-secret"); + } + } + } + + #[cfg(unix)] + #[test] + fn models_cleanup_keeps_personal_credentials_private() { + use std::os::unix::fs::PermissionsExt; + + let root = tempfile::tempdir().unwrap(); + let models = root.path().join("models.json"); + let original = json!({"providers":{"personal":{"apiKey":"test-secret"}}}); + fs::write(&models, serde_json::to_vec(&original).unwrap()).unwrap(); + reconcile_fixture(&models, Some("llmGateway:\n url: https://gateway.example.com\nprograms:\n pi:\n model: selected\n")) + .apply().unwrap(); + fs::set_permissions(&models, fs::Permissions::from_mode(0o600)).unwrap(); + + reconcile_fixture(&models, None).apply().unwrap(); + assert_eq!( + fs::metadata(&models).unwrap().permissions().mode() & 0o077, + 0 + ); + let restored: Value = serde_json::from_slice(&fs::read(&models).unwrap()).unwrap(); + assert_eq!(restored, original); + } + + #[cfg(unix)] + #[test] + fn models_reconcile_repairs_permissions_without_content_changes() { + use std::os::unix::fs::PermissionsExt; + + // Cover both an active provider and cleanup of a previous merge. + for cleanup in [false, true] { + let root = tempfile::tempdir().unwrap(); + let models = root.path().join("models.json"); + fs::write( + &models, + r#"{"providers":{"personal":{"apiKey":"test-secret"}}}"#, + ) + .unwrap(); + let config = Some("programs:\n pi: {}\n"); + reconcile_fixture(&models, config).apply().unwrap(); + let before = fs::read(&models).unwrap(); + // Simulate a file written by the old 0644 merge implementation. + fs::set_permissions(&models, fs::Permissions::from_mode(0o644)).unwrap(); + let changes = reconcile_fixture(&models, if cleanup { None } else { config }); + assert!( + changes.render().contains("UPDATE Pi models"), + "{}", + changes.render() + ); + changes.apply().unwrap(); + assert_eq!(fs::read(&models).unwrap(), before); + assert_eq!( + fs::metadata(&models).unwrap().permissions().mode() & 0o077, + 0 + ); + if !cleanup { + let repeated = reconcile_fixture(&models, config); + assert!(repeated.render().contains("Summary: 0 changes")); + repeated.apply().unwrap(); + } + } + } + + #[test] + fn gateway_catalog_replaces_existing_models_and_restores_on_removal() { + let root = tempfile::tempdir().unwrap(); + let models = root.path().join("models.json"); + let original = json!({"providers":{ + "agentdesktop": { + "baseUrl":"https://old.example.com", + "apiKey":"test-secret", + "models":[ + {"id":"selected","baseUrl":"https://gateway.example.com"}, + {"id":"selected","baseUrl":"https://outside.example.com"}, + {"id":"stale","baseUrl":"https://outside.example.com"} + ] + }, + "personal":{"models":[{"id":"personal-model"}],"apiKey":"personal-secret"} + }}); + fs::write(&models, serde_json::to_vec(&original).unwrap()).unwrap(); + + for selected in ["selected", "next"] { + let config = format!( + "llmGateway:\n url: https://gateway.example.com\nprograms:\n pi:\n model: {selected}\n" + ); + reconcile_fixture(&models, Some(&config)).apply().unwrap(); + let merged: Value = serde_json::from_slice(&fs::read(&models).unwrap()).unwrap(); + assert_eq!( + merged["providers"]["agentdesktop"]["models"], + json!([ + {"id":selected,"baseUrl":"https://gateway.example.com"} + ]) + ); + assert_eq!( + merged["providers"]["personal"], + original["providers"]["personal"] + ); + let repeated = reconcile_fixture(&models, Some(&config)); + assert!(repeated.render().contains("Summary: 0 changes")); + repeated.apply().unwrap(); + } + + // Unrelated user edits made while managed must survive cleanup. + let mut edited: Value = serde_json::from_slice(&fs::read(&models).unwrap()).unwrap(); + edited["providers"]["personal"]["apiKey"] = json!("updated-personal-secret"); + fs::write(&models, serde_json::to_vec(&edited).unwrap()).unwrap(); + reconcile_fixture(&models, None).apply().unwrap(); + let restored: Value = serde_json::from_slice(&fs::read(&models).unwrap()).unwrap(); + assert_eq!( + restored["providers"]["agentdesktop"], + original["providers"]["agentdesktop"] + ); + assert_eq!( + restored["providers"]["personal"]["apiKey"], + "updated-personal-secret" + ); + assert!(!super::json_merge::state_path(&models).exists()); + } + + #[test] + fn cleanup_restores_catalog_order_after_user_additions() { + for restore_original in [false, true] { + let root = tempfile::tempdir().unwrap(); + let models = root.path().join("models.json"); + // Pi resolves duplicate IDs using the last entry. The original catalog + // must keep that ordering even if one entry matches the managed model. + let original_catalog = json!([ + {"id":"selected","baseUrl":"https://outside.example.com"}, + {"id":"selected","baseUrl":"https://gateway.example.com"}, + {"id":"selected","baseUrl":"https://gateway.example.com"} + ]); + fs::write( + &models, + serde_json::to_vec(&json!({"providers":{"agentdesktop":{ + "models":original_catalog + }}})) + .unwrap(), + ) + .unwrap(); + reconcile_fixture(&models, Some("llmGateway:\n url: https://gateway.example.com\nprograms:\n pi:\n model: selected\n")) + .apply().unwrap(); + let mut edited: Value = serde_json::from_slice(&fs::read(&models).unwrap()).unwrap(); + let addition = json!({"id":"added","baseUrl":"https://added.example.com"}); + if restore_original { + // A user may restore an original entry before disabling management. + edited["providers"]["agentdesktop"]["models"] + .as_array_mut() + .unwrap() + .push(original_catalog[0].clone()); + } + edited["providers"]["agentdesktop"]["models"] + .as_array_mut() + .unwrap() + .push(addition.clone()); + fs::write(&models, serde_json::to_vec(&edited).unwrap()).unwrap(); + + reconcile_fixture(&models, None).apply().unwrap(); + let restored: Value = serde_json::from_slice(&fs::read(&models).unwrap()).unwrap(); + let mut expected = original_catalog; + expected.as_array_mut().unwrap().push(addition); + assert_eq!(restored["providers"]["agentdesktop"]["models"], expected); + } + } + + #[test] + fn disabling_gateway_restores_catalog_and_preserves_user_additions() { + let root = tempfile::tempdir().unwrap(); + let models = root.path().join("models.json"); + let original_model = json!({"id":"original","baseUrl":"https://original.example.com"}); + fs::write( + &models, + serde_json::to_vec(&json!({"providers":{"agentdesktop":{ + "models":[original_model.clone()] + }}})) + .unwrap(), + ) + .unwrap(); + reconcile_fixture(&models, Some("llmGateway:\n url: https://gateway.example.com\nprograms:\n pi:\n model: selected\n")) + .apply().unwrap(); + let mut edited: Value = serde_json::from_slice(&fs::read(&models).unwrap()).unwrap(); + assert_eq!( + edited["providers"]["agentdesktop"]["models"] + .as_array() + .unwrap() + .len(), + 1 + ); + let added_model = json!({"id":"added","baseUrl":"https://added.example.com"}); + edited["providers"]["agentdesktop"]["models"] + .as_array_mut() + .unwrap() + .push(added_model.clone()); + fs::write(&models, serde_json::to_vec(&edited).unwrap()).unwrap(); + + reconcile_fixture( + &models, + Some("programs:\n pi:\n useLlmGateway: false\n"), + ) + .apply() + .unwrap(); + let restored: Value = serde_json::from_slice(&fs::read(&models).unwrap()).unwrap(); + let catalog = restored["providers"]["agentdesktop"]["models"] + .as_array() + .unwrap(); + assert_eq!(catalog.len(), 2); + assert!(catalog.contains(&original_model)); + assert!(catalog.contains(&added_model)); + reconcile_fixture(&models, None).apply().unwrap(); + assert_eq!( + serde_json::from_slice::(&fs::read(&models).unwrap()).unwrap(), + restored + ); + } + #[test] fn gateway_overrides_per_model_endpoints() { let config = parse_daemon( From b201f7d4d9395b2183682ca54d26f0274a1adac0 Mon Sep 17 00:00:00 2001 From: Sebastian Maniak Date: Tue, 22 Sep 2026 11:48:56 -0400 Subject: [PATCH 10/12] Remove Pi screenshots and format rebased tool icons --- docs/screenshots/pi-controller-config.png | Bin 28101 -> 0 bytes docs/screenshots/pi-inventory.png | Bin 20661 -> 0 bytes docs/screenshots/pi-mcp.png | Bin 29517 -> 0 bytes docs/screenshots/pi-skills.png | Bin 55953 -> 0 bytes frontend/ui/src/tools.tsx | 4 +++- 5 files changed, 3 insertions(+), 1 deletion(-) delete mode 100644 docs/screenshots/pi-controller-config.png delete mode 100644 docs/screenshots/pi-inventory.png delete mode 100644 docs/screenshots/pi-mcp.png delete mode 100644 docs/screenshots/pi-skills.png diff --git a/docs/screenshots/pi-controller-config.png b/docs/screenshots/pi-controller-config.png deleted file mode 100644 index 34bf6170bf9549fcc35196a13f36aef68f7bdcb2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28101 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 Q#Osizs_w0#o3_FK2jlhr)Bpeg diff --git a/docs/screenshots/pi-inventory.png b/docs/screenshots/pi-inventory.png deleted file mode 100644 index 637a82ddf2e60b7b65c5f148fc83b2a5f3e2c9e6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20661 zcmdqJby$^M6E_No0#Zs!cS?7I(p`JgA>ExDq(neKLb|(=?rv%6F6r))J~!|C`pEO0 z>pIu@`}5ixZr8nLtu?c1<~K70D#(c=zr=kB1qFpHDIuZ+1qG7_Ja56j0Dh8rVc|eQ zK}(ql3oA$p3ll5Y+ZdZ!8bLu(fAa{Jm4;Eo@F(gI7Z3_o8P%VK^%0Fjc#r2m^6s0d zUKO7Bn9x28>+7OjPLY;0aDQvjLprr70sU+x{Ct$mrg=7|Q$WYf(NB*de*8D^@lxPB zp`wG?nX{m>rV0z#lsCqoi1m=h>_IG%>XC0h?p%)5cjcbD$z5LGJuI;*(Q(IYC4%Oa zsu7#xY&Uwq3G#kaRyb0>wH)gDV-payt(^}njg{Lu+er%bvHXZWllcj>%y(Dqq@OeU zrg(k_E%gQZXXi|(^)QEiq)_x_Lx|}i*%q`flVj+DmO|sWiOT;a#X%FAL7R-El5fB) zFT&OL38Zy+vq|W_*!!GrH(B7a@m#&OT<^AQa}bu83wf}S#p=|ep*d!0M)RvPq!L=# zB;_(9!28T}39@L>aXNeQ<5!cR<{_fGHucDe$SfdS6(coCV_8`!THqNT3Oc|H3Kn>R z2EMq!7YgcGgg+D<@EZ;Iilo8(c?*-5_Uz9y)YfCgcgn(&lE80e1A8MQYX?&s$BtDs z3!teA8a%-B zV=*Hs@qel~TJV#q$tn;F+t?csb1*P5Fp&zpBqk>2vo|#6Q4$gRA35+peo|9MM_V37 zMi&Tv6M%_%+<(J zUBt`^5D(CY01GP%-+$`=zux@q@d>5cUrG)xrl*uo-u$1GU`L7l8^D9ITLspra(pk1tkO}De?~F3ca5U_c0QT{X_mT z&G)@JeU!TKOnGkk<)ZX+J5uVPRN|nFU!6tN8AvnbhJ9ie;rx9J7}OP&elQD5+%=6P zvXN}iv!9QW?nhk~&hKp^`3~zg7kQpP56JrVT!n@0Yq55n={qTMXy{i^e?O8ZJPGmV)3WI=-i7Sv_3m!! zk@#l+Zo(W!1z~&|Z|Zw76wcq4fi7CZy;F6)muVK5{eL@Amp+3bhWhjQfC}`xpN>8J z;^|Ncpy5FTFZcc#2=Fld3@CHodR__rWYj|9P%z*C7~a3_LBU)ILBUKAxti4QHU+I0vY$kQ4PLtc&qWhtukWgy}2i((AjzY?A^tl@Mgz}P$q670NUsElMOfD^kefbo`?a@oNEJexb?EpWmwVk)U>ZC~U5Zhw1rfmgy@qj}w>UTLG2OA)`rX(@Acik)Ud z_0d|=KC28JhI9p&f8lG2*<=KVjh1+!3d7)dxgycmOf3O{v-^e)){tTpcMI}$Mul*( z=DYens!x>;HH)C)_o63fXQ0D54|nIWCB)7A#s@t9m`;*!2?;-xHI=qcvZ*y{j424X z9f_5jHsnCXO3C%-JEiL*EIqDD6~H(b9fUwJCUkz=LO ztz);QP;HV)Wuu)G{gUjJ!e+)!PH$Clx+r6La zM5myM36=c{C&z$2%%EmvhQ(CzXu9tW{g5H0Q8q1jyYtw9@_B(P1MVsIW=_cNL~K%g z2L8pUFAJ9o!>8t(Ys7jF-bHf`RE^Dg_J`7%-j1E?@HS`IwF;MEY2myN=^vzIWY&f{ zZ&0tUi|XCZgV6Hyq6>zb`5PQ#KOqKC+ZFJ(@e(wgWd%hSjAlG{*c!-3zpc~x#N&8S zypc<%!=xJ6q&A5jyfL&fbTSVrl=IkGID{k|U3K67g2=W|SKJ7AdO9ArL%^XiO!t$r zRc}MXLbct77Z-TVPIJzsM4b0$VwStJfR4CpQM}CNi9ODb5}iN+)v$ zd%1ngZ4>D=t3mQ4*SWyXif?oB+8QQFVAA{Ki57h6vOg=j+8v_2EqHgvYCc>F4KCD4 zez`UzZ#7$;?ecIb$OAda>RKmRUVYa(#~X`_5`WQM;&^2r<<_fg)71xNg$LnW$L3yk z(0$dn7AaI6_meo;MCD}0`H4}o6F){Z5pJv$aop>1b%L4Z;qEeaQIu%UjPRl<3+ z&zldD_X4$KDET~BpMH?G4;6#|<}CNqb6;u(4*!_@rf_kAQE{I7%N^8y1}OI-!g{nPH@_icOY>!MB9NFRp8-ei}#7^8Z9yJt6{ygI?2DP4Y8 z1&bv`d-HVZCK&a_WU?2kjGkl%gJ-Ho!oUgt9!nfn0Tq^;8*R8vFy7_ZdJGN2Z-zn$|&Uu?!z4JLOCZkS-(#&qvMk;2d z@wrrVvuj|)c*9|Zxo+IxtwYdkH{%Ue8p7yd%l+4-{-Fd*TYddc^(vVv3rH~F(5_Bl z8p^|MbNPu3BQM4@{sxyk2hQKDntb`-!YL38m!G&)CPRv9rYg%Pr{!C2+^c&k@=QY& z1kn9_ShFWMnnFhtM{nf2%*@G-gLzkZS0724hwJ-&JVRrZ9F6`~wtJB$KVJn+O884U!o|EHPQ3ZwY z>ol*t_upn15KQfOPcXj6e^kRg!vMj8G$ScmT{MQwjA~OBW z6%@p38Xk=?-}Op`PF+Mi41b)_KOp-uPxWB_i`h@8AD?_FWi^#Pe&n-Ssuqo{=S7r7 zy@;ca;|wk+HQnR=1@VNM&TM}5x$MiZET3U5tHyOP|3toK>ST$gtS0A!G~C60x~eWw zabF}HD#GC~r-@E^F%9ArftD^^u&zhpE0u3pKT`_{t?Ns-X3Nbouk8yuKBdalter()uq!WUsoj_;ZsR_3{|))ErT7vEcC8eH}j%y8M<{I4&( zQhtQuWEZNyE<|)8QfHak?F{E9qOEAey(760s4{44G`PXisJSMhRpGlktnuh(0JXd$ zqxeLDku7^8cP0~Qyyqj(x{i_gkq@7tU5oYgU|`@SQSksX(-a{@A)G^kHydd%OTM5? z;#FJAW{dWCv^Znvk+1Kbg>j=Cl`+=$MfY~0&!4|&L*9IA{swG8*Bmp4=GT zup)H#*UCTOgp~&c+S0kwjrs~&G;vOxHxIY^z=fyO5?s$;R)BXh58ZwR2T@g zO1x@n5SYnT-xmwU80G3_fzdY(f%>nW6|HNQBz0A7DO4QC~3sfQ3uo&9=-8(pOe!$YFr z(b)5-q)50xnn*9jb~(N*BCl&qzZeGL6&j4l`*?PPgjvQPDK+Kz?PM_L?N<^`aw<2q z1B-nIbfn1p`3Y4Ljn#!S$|k0X_X}x_Io+XzB*!27V*AZ5Qi5$U+p-~|(hf;>5P@8N zC@VcZe0(i6h@H#HGkIx3+(=0mQjIRTRK7M{zD3&3EcM_?DUlRjy!7Jz*)nYe-s6qn z+zPEarrB~xW<6yKIi<_aVdUu8;wkA={*(|TX3a)pPib6~J*iHAMhPneVK0O(ov)5w zvnm^P(LF#k+tua3t1QvrLoxq0UkK_%5a(`59V^%!5QqeYu(Zjmyp5gtIs?2S-G&5? zhlX!Be4<8u(NxRSs0^c<(S<6?%y|X2xAt5x*skM!!G7_svg-~i`a;SPk>H-N&nsU7 zpS`1-9M~+bsX=uo>d(K-78GO)AgCY{<&c`dFR+s73HqBQ#eO|#23N2S| zFrrD^rwZ#N6?f>vD+wMmWCaMlXLnx-S&!KyEq4;U47ps`AxE}~Ehf6Zpsqa0g_Y6L z?pJGcw>}Ted|^GipB~C{?NzE&9pW{75ZSBn3rUFy_yM|h>EO|R))h=C+>$UIg&^W` zFu$QbpX~&5PKvr@M!EQv#|h6gz_{k5bmZ%*_T~OnSn(Sfx$v*wHh&h}&Mt)^Wd}yn zVfKA;uA7b3J@Lcs>u_elr;iy}(P%uDywtJ(IWY(>dP)4LOv~@3z0^$GGxun072Zja z;d z>m9m{-)ib?*dkvM42pc-MpWTu)RiNBg)H0riabVHUnL~I6AzEoEVUEIg7QX#x60v1 zwH@CXmC^ml7-}Q?{rc-35*YmximgMUv>VN_z6K{7dw*v-62y}rE{CAianCWi-D)a% zLK9RFJ7Lg3u6~AQrFFm8>d(I5w}IvQ;b3=1Oq{(QU;pOgIVltMNr=sFoki7qKE;tz zL8s?>-FgUnOP8q$I7^kHc(hts@ZDMR;EugnT8A-esBwDIvJ@vf2`uGVc|^IPoT zCoxlGiGD^VZkWc(T5tYLkh2zxfkYrh)2b?G5+n%8t{lC&a(7<2+;YO|W4V&jFQaf2 z;%ivB9fqy+^yB=!?6!i6E^)fimzC*YGNb@2KZ&W?;O2)L9SJtfx{FE3w>Ki}%&hhY z3>c*260Pbu0t5>pr(>+>O-scBd)?GB8#@D#$a({wdu=GrT_i`VMN_TTBjXlQ*~orp zvzUpQ^7vjPV5m{GpO=zhG#b^JCD7f!Zz?PwMno}JU8{2^X8x%|Bjrt&%?T4R>Ps{i z>f_LTu4W|}FC6UG*&(UceXMSU3hIO?97##Ui;cAS9hB&(XB|9-8N+2qWb=ow3uL6d zlGiYy4JC9}6pV_7SN7JUjCRY*pWR#(FqU5s@D{#dS3i+i5kH?=FH$Pvap!(pdYM-g z`U&5KeRF=!4DoF9n5=W3+S@1RJE_F6ECqJYZ^%n4);~wA8wBWYebRQ3&lu{oeTF9uRGi{tl zu^5I-pd`EJ4WX*XR?>KdX>oa_pt(}S9+3q^#h7+1;XHqdmEla#kZ(`=o40M)(0&?` zv8j0R@I;%eUzg&P#Vmo|yv+N;qr-_=3@fpr-mZOy{&yEenyxXP-wzu1dhB&ra~|5#v7VWAHXO2! zfkG9vGyIFIKBJvZ)M=vL|8h}&Z+TPwwWDpfw#5BxM?7ro$+*7FxEh3tPY+OV-pL=h`&px&nt#;B9Pt6%J{h0B1LItspe9Uom;9icc2&> zT|Rt1EN+}C^an}DqDnUlj?{`Id!)0e)yU6Y=fyo7wqIA+TQfe41!GE^Kiu`ua^;hi z)(JNdl*<|Qb!XpFroU8YW||-j?A_=6kVY%+8FFu%G)wew43{NfbIvz;Qdfw~X$;QA zAT2~|ycgqnrj8(t*z$($Hzq|hSoGH77JF&f$@4J8^OAlaqdE)$T6w9fT&~U-ZBntq znLlMe<@oNqcI>hps*Xn{cT4K*v2O}|a|hhz$Y|Fo811gSk`J{U(?)N}WW9mx&PFMw zJ%I<3K`TWmO21)LY#3F<(?VtVv;DS?6(r%Ur>)6hl}70CA}?Bd?aubYlJz>*2<5w8 zcg^-5^2YB*yBCf-iy8&EZ>nDqu&!mTgxPG*L#rUwnN?cq zc(TL1;<)0eQg72lD&bKqaNZQ6Z{jCMh5hzj6U_D0Xxb(e3o#C_g*lkoAPKOv80 zK4oGvY``j<%V|G-u=G0||IcFkZ2oP;bKWvnozt|z3R>N78R>MxtPEq>nBfM5Anbdg zh=)!CQ_?rM9F{%1V4WPyw<^QAsh;L>A}(}BtwOCzF#ggAk^%-q)EiiY*goWC;{Gq} z%q2R@SMT`F`0YCr<6|JBL-g;5k6ONO>BhpL7O?SHEi562wnQ(=riAdSOk>i%`XH`__$?RCh6pM(4hXow(=Ka^p2G9G%42LH*7 z&Gs540&)*ZKfp!N56Up3Zhkc-<4^<;C#Op$3^?VJ+u4?$q$c=lgebx{cmR@TKQ9l5M~f58wm%&#UTRc6W|zCw$*y_oyQjM zJLsn(xERH5tQO&^yJYV5 zD_NS^(I|B~1$1=up25Km5H}a+?Yo+WJGi>JiUAyBXd zVlbLdnQb5jJX_cc-eON(Os0VEhaGMDU*4|gfY+&E>jfLItpD+q36Vkh6ntyNdQ#NN z056m_E6SPn`spIY1&BjntDhD*KdxJbdSFaXhByomo0FG5e^OM62=gk>c?8)M_NmKC z3~+am`OrTt(th+}1zQV|{T&|^EE_kb<`3Gu$T|hB)ofEhg~+@rf8e zU_UzKGVq4spUUh1zX-Le2&8M|iiG*|(1=hBgMHz~%V~j<4p0wvf6)q%YkG3FN zE9_#uluLERO@&%oeS^F0p4q4WuPQv;+y?KXXoCK=t3EJ8#~U&NJ2<{spBCy9h#Q8be6j|6>0e6s$Q%yXeSM>Pd89#iiibKixWpWYF;AmY;(&o=(&E zNB1#5>)O+$-XjJKdF&x<_1;4iqJr6J>SDjhC1 z5oJWd^=nj`DuOSE(5W;)t!qnOx#xHsN*_L?iEcmMXMZ*)exOh&BFF_56_?ADnE&$j zl)G=Fe=K-@eqI9BUM?{*GO|E(r>0X&TdQ|rAw>5ehFl!?uM}*lVYo{s={h%8R`gY5 z5*ipt$0sHx+7no)STQ2}yRW$iv@1lA68lSZ!}c^ICz%*S|ML6Gj^yYUEkoi#sal!- z*{p?dp43D^R6@dg_xm~FCC?jVQBl!+TChto*?O$*Xmo$9 z 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 L>gTe~DWM4fHtd$ z-Tl1#W&b=q&otE~U3IGJyyrcqCPGO;5*_6Y3JeSky0ny-3JeU~C=3kjGo&(wWz3)w5TYl63EfQ+RhvXhBn?ePF@yH`DGXhI8IPFN@L1o5k62n4OxW1h3tL2 zm2o|R}IMh zt2Cf?q&aO5sb?sL(%9q6+~~P9gHtn5^&LJO+gYggaQBdvn&5?!giwSEb}kRr?|;5z z3(oN?Q(GH}2rK+LpEtx7>qDg+{1!Q7h-^;?RIR|=4=aPsb)QuHQ-+H!_6vPBnntlH zo1)meksvj_llvWF@6}-h#)E9Z>$XenrW)gi>YXuo(s$@%ZS3~vwyhm0YYV#3A3W<| zMJ;o#6T$;7tkyhNZF|obAp<|#&2&#twe@KyC&dAlH&tA{BK6nUiucvof=i3!;#clJbMhEO=GKB>pN6{3k$e&lmB~3br*AxsG~j5q^sb+ zlKHFfpAY{k$j|b-=YNdEe~0-$slYf3qVTi)BQrr1z0g=--U!1;i@jI#fIZAc$Rg~Y z?LU!O&TeXL#darih=U<(61q5Z^7QH2IQx!?q)e_U|k3LLtv%7*gZC^EDvL z!b(`in2b^w?%xZu0G>82xNcp`{_he%(ZDXuJfjzAmMJ3tzs!k*A;(ss0@@lT8TzG# zha3m*l~@r?ORLBK z9duzhSPJ28!vB-=|LsuFk+vQdzZ{3{`PODy%D`jYb2yvN9aP$TVVxx$OUOgT z$iMzRZB)DF+ks-w+0@Nmjh8f~Qjvi#QTs?{-?X%>Mx(*A;Y1q6DeNKO~bHp`nE$=vlRlI%Q2sbh_iWq}Qt;co7lNOPw*A&#;HVIdSXs84MA3uoHl~E|Ct4 z#uunfhC@J=4vkD6gQ^sQr3?fVLzcaAY4yYA0s`Ylh2Ebhg-?J$!9t#!;8aEE>&sR; zdTMHkFZucAa}_I)-fN+u{h882`6aHY5_Q>hKKGn5y=L}ib(6cdQZU@L9nYT2u9J(q zs!@n&Fzb@LSjcx4f2ER1PB|91&mh3Q8_8K|HG9a--yBH>Z-+fhl{#8yV7l{O6kf0T z%{;Cw4y6dRE>GrvN}|(z-+>QJClvZH&D%EtkMxq>HIx5oiUIP-Z8*FwPk{3=`*fWT zag73;${fGa`oSw^xk8l2dZ8tz4GIx*V-``n$Zit`r&bN=Kn2#k=C*EDq$fe zwXR~tTQ*cCnaX8*^o@`Xm%SE)DoqTxb*>+jDlm|entFCLhS&2~8eQnvqMs?0C|`ukyvps1nqY~b!F(IE z;1Wc_*tRpCEjx|xy}h3FPHj!NoYnLWtyrx%L9Iehdev`-srfpG46;X)J95)G0Nx(U zEVp`)vxILc_7E3(4ZR&yDf5tSpS(&6M!Y&ex6*CnIGSwD#cCAteX8FaN|$vs3%;OU z6R}Xp{gNnGo6heC^$XE!{UE@+GM1=@x}u@=e1${p@sNVk?_)^F@~;0G%El{6xbnri zM2;fG;aMg+7XgiOjg^n)jzK*6Iv+AP9m!;0v9#=w9cgvDHgOcAy&&QzHAKIct1ys$ zurgKw>*WP?55Gx$kx8|Zg+S_yG zIeMYuQ>)oL&C^V(%9CM8t@#AmU@Y;Z&p>ljC+Id|A&l%Rm`K3f zcD9VyM_mJYvqi(>`K#&Huv|jkrM4c`qRGZ=EJMGcSc$Ka#o^`?0aXZxNP`{Ev#TkQ`lZa3!Qw~c4=HA#xU%@)a}vrDHs ztQ4A;a)BV5$hQuCn$+c&MelnD21M$OR`qOCZ_AWxQ(X6_#+LoJM>EKxUNN)YuUGsi zI)O1=h>VGeM?aVyxpDR{38f3?zw1-IOL+M;h&gzVW@{uh_147_9Br*Czqq*H@x8*- zvisZI*~Z|OBGK()r(QN?Lm<R0IE1ipWEV#{R11( zA{|MwCN!-2gV|3sY0iqm0SK?i@(q4)+Ri28tJ0)HLw$io%=y?EcIB89J}I=^$<`A7 zBpFMXU}@l?%^I2Zx*H9Aw!l#&Z(zJ`j7@|a`b&;hv*Syj6;G}DbdNB*?}(V^esgL; z)74t`(guNPjoC20{^eo-gLpwKs6xA5r1DH#L&G_j%w}}2km9rWmL=z+TB)+`NOC1< z=MyAfvW>v-5qvH2&}YZ#FuPo(SL4)ST0?Ck7c>dlBcE%ZWK(&whF`Z`w&wq&FA@EE z9s2Y`QAqm!>ZnjYlWY6wn{vL{0NUI$%4U0~vxEb5Hr21u2}1sU&E==|3GYex(e^$G zueRNeNfQZp&HI7|V+m8*Zc&;#O`mc1)R(5nE?s38G)I<>uXv?}Uo3Jma}zDUbEfb+ zouq-U!2cw0nS~{P#y-iC>DKgu8?;=7k1d%eUOH=G<-ZlVbsQq#eVrVQ&oK;-t6#-` z-Rcr|uQQg+kc!pc>V7h%98j!M3{o#%tBRLy^;uBC=iA1hXp1LlupCP}9)CrEY0aeD zn5@pA`R%n3$;oR-(ELiPWn;_S1?av`bXUFHKopNDje(&qT0%ZUdd7zHf-FZFvIm|T z!39SzDSj3SHH(k5yP!cMNu5HxBy}@<2fB%S;DXTL#%UI;b+!IVMh@ih-gaH6w$H@6 z{UBr$t>>x+>;G$>%{KsAhF6C}ZEv|U_f1+=`<;J|9HG$o&GqW)m*lL;aG&a5RMGcA z?QtMn`n2%Y`iWQ13@_6wdw7;sB00jh9V%p;rdrcDSs)R^VI~Xt-6!jw-j(o31f7E5d!NBckpH|gA|dMm z*RDpIrw$^m?9DnGL?bGMVET%yG~E+9+FXLbW*|y6+W! z$Dx^wydja93T4!;{T3Bjx_aetW$zEEA5=5VbE;Pa+9-Z zu||1DX1g!k=*m@HM%U#<;*;*4;~6kz6C9VaUHUnc)B8aTozwifBzPtBgocw{W(i0 z_njuWclz=Ntf3MLSWG`af4S(MRAb<68~Q`v&sJ#LmpxUsoyB6QR~3-|Btwv8f+Mcm zVo+oMb@r{}C0Q55<<||HTjzpt$I{bug{tD?j0<>qAt!}6zVJ>dSlx3c_9)1s|KfAA zB2#AabP=XsT*}OAygZp?hNw$mAKx>f(`)+Z`Ewn1wLu@Tn>L(k^LmdKuY*MC{X&Tw zABD~OVnEf0OPR~sEZaWMvBmls8NJ~fLtVE4@O&kG0~R9#A3Q&!qo#TQ1)=79eZ%lv zV1iqO@Rik9g?uJH>3D2Onm*yPn`JRX^O+g!|8Z-KY)evhjD$|WsZpwfuifboa~mtv zF``$_!h*Upp{^YNKGfabej>8QYX&7-(n-9soe3^i<%X4`KoO4BqRU1{m!^9>i%Z*3 z!4}_ipn)lK)ED0}CZ0CroS3EA!g4vc%!_2O9+`o|;1_Aj8QOfO<0pZPg|J*F1KehX z6cr@<=wDrz6graayspPKWt&32*YzknS^KfBJO+NL^=OCJXs5Lrjy4gK%Een?yaU-g zc9&dmv74w>;T(;@6tJ~6SAX>WdVRH`+p+qK#zKe@Tt&K3NYef5S#`8;t%aQ1iL6|Q zHJ!tElt zQyXxdLVlFU>k{4RpES%a<`WBgeD`U`obzWd9TE@lo>*Ss7~_KG2^r0VHM{72;iG3C zxe+mikU}X!!b3CFxmnj=CMJ~yNXJy=X|G!?`@Yy4-whG{s4J&c7vwfRdo8ypN!Gzq zqFxHZO|6E-dwbyLY4jwRh}Z{XG21JSfMZysm~B@h_3^#PW%d60t>;Z#6wy^LC|9~a z2p5}C72yS(f$J%`QjM!MMOyjAgWMDR&x-F72$a+obIR+{sZ0ktJqx*lzKg;`i8PfP z^-;l9bD6?)aH(X=HhB)W2y8s-i+>XwD>GXZA(NWMhf9P zFK;>J@IcNIa2Zh}?){Ns#9V^&L|SnOi=%SNYM75#p@r*EHE+M#^qvW2g;LBhp`TOU zFT2nl*+PrJ#N^8uo44Sy0zd2=d{v6Yuk0<gw&Z z02(ZYxmG{px@~{E4_yZ*s$lL-r(r1D@>~9A%hDyq_ek*PBU|8$_jB*>J-#_}yk8D^ zO{qpQ0VzAlxz@r;5INvLcyvPu~jMb#Oi=G zyJy;udQm;1UYf2>QT8}96B&&hobN|K2TXzlfz}Vj<{nvY^9MAL>+J+>@k)}WSK2`f zoYu;f%Dx)HO>@O--`g+^=A`|qe0o`3s1?31q4t5-t|`Kh8AoPjxSQ)cFbM^l3?(zJ z(ydUp$GP%T+%FC(^6R0{=Z?1~#PTu-ij!Hyj9|P>f^oCi9MVTdgSMvDZQI%9*W)fH za+?DkzW4A?7$kyn)mc&>x(SI)p5w6U?2f$>G}S68vZ^+*(bhBVW42H*O?%b*%L^mr zD!V0ZVfpo{<9t(bwVbdpMV-x$RXMJ1`AYrp@oj2U24WwUleT-oYb9gW^EeoT(!w?8ok#IrB9UtgetOZ#08jx zAGou)?3W1KQNct#IRkTCBVoGFLuhB|!@#``y1s3N{oq%9g4EbOgJ)cBOI6gkI@Toz zVxn2k_Mdm!Peh1n@E_oOfz`0d;-i*We=rhvSglEpfa{Jv7yJxo8mb_|R6JXSbN2=5 z`gusmZ!l`$^iwzb)ob{qs1s<}V3tyPnjP|U`z7}zv~_$l@2q>N6h>`1{ItGwWg7J_ z{9u+DwIKf~bXYATZhI|9A4|6G^NZpeaJlh?apBd@Hq+AXM-?0ncbGYkL1y3t zyMllf(`e3-hJj@o#m~=iS;Ky_cHRd+Y6%Zog=~3sop15=;J@y&i#|JUkv#UlVdLnj zlJcdzK3Xvin~~YWyywxkuu5F58i>7{(Ml?vE3>~_Sr-j=CE6Am9X|e^N#QW1b&mt? zUk~GPUVhCH{HE6$lOhcb?nxdVJQ9}t@^u?d09CbkDOZgQ&9=T8Lz#eHr=Ps0&yD=u zfmmlzj;BHT!0R==wgN5KWAzoU3pEDN^NrFrna8jrSZI}&CmaJSp{C(B#;Whq&2&#a zoa`4Kgu0cOD;EKu^3<=xI0Il`41P+{WB(z_P}+Vm0a^Z@$Y{*+T(-%IhQTWdTo{h& z>vs9ptCj~fZydy5xYlg%U+z4YJrw;UXdK)9gnwBdRI0#zUD!Dg8_$Qh!sL$LF()Q| zI~z%iNigJI%A6hnja9~(eg*5--+}K@iQ!jP@YMC5n&iQAy-;)P>Ir(jDnEN}Wgc)Y zDfLaRFHoxCbr-#ZyaS=niPAZHz&oVM&c}-M`I{4NLzknUYk6cpkF@RdP2XWPnNOFm zb@}jSzkbME?_83pq>>8@g$d2&&?phE+Sy84UGuGVV|VW4JDIf{+@MBgoa>Bv=(aP3 z!uw>hp+wliQKaEqqz9<8^7lfhqGql=1GIUN=itkBF_*R6-@wjsYuM}5IOWt$ruMgc z>q)>yC0|-^+4^3mRN}Hw2{FmK$ZX%b@8JGsyZhP=sHz9d!J3JxG_jOy^}D@CdH0{q zCg6LEFa$2*_#ZER40K|Ynz!AR9E90@3b#l5{EAU+*@e3^F@OSAtB~}K(dR0-yEv-Y zGT&3w(21-cF&MU8ChvV1<;^ zmT5R~H@ogl$y=XIg;LEh8+P2z%g5XEkrH>ohz@>`XrMjgdNhxlzUyOPJdUblpB#9) zU9*I2xKl)b3x|k~N2)B*u@qGE-<>0aFGGDY~ z>y?Dmsh#wmdXfr*;e-o%-}M7R)X;HuCY-X`saBdXR*+DfWk7)SYFo2*k?a0+F=W;I z#8blAUlJfKf3m<%6!ylAOB?aXqg9Utd>nvSe%3Czz1r2VW*i>kONG`Hn;IND zAfl$@peBdKM&gAdm@8VAJI&*}b=5FNvUs>bd-j|ChmREgj${EdoYCT#)IO zmWUd(_h$*It_T=}S{&SCihtZUznwAf;9ia%qWll3{4*HHi1ChJ><|Bp@b7HT{!g|) zyj$ymVosO)Tf%sQHUBzf01=Tcz*`wBaEVy_k(V+q2XNFB?aXd9$N!JR2L{2V28;dK zADn;Sg)NuosEArq zXqfc#Vamx#Wo&$3ee(Q@HF7gJ{|w9#;}O2MZ`Wbz8lz)zrZZGb-rBp$)hWG0>^RaQ z|4%oBM=+bTV+!OtMnmBf#9(H@(d*ijreULgh2ePvPfECxSjw6)>OU$&qBma!Ryk>1 z_eQHPRKmZAC#{>Tn7kpu-NFoR{Y%WG_P}6XS>rY8AMT-VVL2q+`^?iifvPrHxqn}-WORKBR;?@g5#jgDk# zbCnwsy#2z1=J5JXs4#d~)sQQHZ!T-fDxKuN!$gmrXVlQmq}xzdrrW4M?0?8MoXpUe zCFDQ8gZcEz5Dll`bMnG-wusEM?}*)5%K`jfT**nPpFih)$*iZQkj-D*KOh76XtqXX zz@VKxvIOaV(9(zO&AWQr8m}$(+?k-f(M*<3tl5u_p|)?4iTzi zjr=Zfhhtv0pZ2 zs9-p8Lq1IeI{)b@oxNGiq}}V%Y5@OH>7tS8NAhu;i^Szr04OriY+4*#=hQS3I3KW33| zARJ^?*HQM0aKU^a8efZBe$joGG}rGgec4%(8)s04)wxIPw@!E6U)EMA;XE&`?5KIs ze;+~{h&#dIv2=nzG3S2JHi{Zt`O#~14br2&dxtVcuu5j1d8rATE$$ zryZ)OZN37hqYQTItkGOzX&{QYox0xxSGW5nO%dtLb%5T+WUGv<@K6}w~_bX|4rDQZzx`j*GR#_n(WE=r@Kgt zSgIt=Ap9GAGJj~$z$M(Q*~cxEWO{HL&s|UcXTSsloqP}9I$ak9$uW&r@#gZn_1KO( ztqW^4`$UxKHE7L#I~bsI+c?dlMM{kc+}~EN`fd+7$Dk;XnRzo+<*a(U=D&KG!TsW` z?W#=1>Z*+42xw?s6mhGPtrJ{rDoOZ5%a)OkXf%T}({8m+!9pR^BqcH`GWzYLOcEni za*0~w_BZPWUEcmrW7Wqujt4D(w3XSE)Ddyb|jf0laWR~W4B_AhW=eR1_|?ArC}<& z`AE~XDTU>@-oSpe=~X}*$?P5A>(~4GjbO-EG+gEh`fS?k@oU@T6`u`$nHl5MOsQrn zO^%Pqi*HinWzc#AR9rQoB{|4W8rwj4t4+NP=>BMo%5k+#rC5cE4qzFgRRaY#JMIhI zPgdD&rfHkKwn~;=P6R;jLmycvBxL{!e;Pe6FQW0okxsMw5N5G{QA6AL)sjlP1iJM~ zOZ}+Y0Z(76Y8T)V>A5!)G||vLm1U)gORWSw2!zeBvUZ;&>4cDl8Ec1Wtv1e?8w2zp zjY^4}cA%k)+a+7r<#lRq2k!ko{#6aB$z%=@-x?V%y$089nPH<&ED7I!>~q`4fY#&W z$mEg{DYvtCfXmOUI9J^$`^6=h@T4KH+flKId%H8L^^c0$w{}x<1D}Yt#+N$Q!97__ z-XDAcx4zpx=UbfX?Xeud=QjRB89UW&2~sWSVU+vUVX@~zl_sj8lKs$ao$KG7>*eT+ z@_=y9An*uJ!Fhx2eiEI;P)`ipFMvDvF|cTdybbJ+=>>r8qlJxLiZtIz0S9jjPAM-? z+f&(hz+%{dzOkIr_Ggu5BqtZKQL7|NRT~nSAJ5=XWw#tA=d^83;bW-uo1BFhR4D;+ z7^Zwu(L_vtXB@O3;j3F`MJAug$)RC6jbL)!deX@~=<>~~$3H})TnE29+P?3$QYQ zVn7KwnxE`!@y$KpwzC^Y$9ZUQTspk=ln|4!>TF^*2dlH@f>RjdPx3{1@j~D3WX?xF$)ivdf&u8&0G8x9295-U_CqA_GulBF-mJl)#>7xLZ}+lET@hZPVT&BKwqSk-Vc#CJ-` z7aLLmuZy_AXNV&|6a~!tYWV`~pMkqW!wTfw#SDsxjOYfYoSg~-WF?AK%%cgOGlG#Y z1n-g#=Nr`Z*FLC8?D#g>-(%_85)iG&(}A2l6R(%2TYm+^=lP&cD$GIZ{Eih_qOsH4i;wM{#}G za$f{afq7}lK)Cx?`g_&-3k(fWT+b6ONN##AAj6IPs6MPZ7dep9O989xCis zE322cOb9%+VO2jpo`nB4k%2Ia+s&y$*#X4nQ~OZ!C+Onf|eX@~)nOogeDMu9Lf#(7rf=BNk#neku)6QsAX_p#!+M(XQ#9v{ne zYgIJLwWvd9j$prB_ML4U^Us#)80)*-|Z>FU;3jKPi({gS0;bqw+xOEyFSZp;aJlurd4}3)% z#BNIb%ea{$4op&}wLWP}(4Z(IHPY3n(4DIousL9=4M|C^_2D*EQ!i7JJ#@wu3dbPQ zDBUoQQ07J?9Q2n<2nP2g+64MOM9|1NSN-aEfPi`T4ua0m2ga?!)maYRg2BD->-S#} zdK4{lTE!Lf!E?hw{O_~fH!rzYS^WxSQyG&(QD4dEXRCb-;9Yzs#tzRxO+ro#NBs_u zPERbX#PMxP-BPQ2uH0*()vRZ+gNyszz5}kFmou7M8=r(`U1Hd9;F%71etCs`OQS`8 zwRXU&t6taSwQcMx3Cw5E{#1bPbF?X};4I$vpvq{1^UUfpJZ6>AGEmU4Uj*_gi$RN3 z;2BeedQpcoivKQ8?bmesEWTxWyzyKnD%oVNW6W0fqo!^HQ*FI6ztE8RFp>+NM=abX z#k!N}89B|GH)4w77- zhs%bcCoa@l49{1as(yvbK-H-2LLn?tXbx*iOknF{Ec=1knga_RMXJK4<}}(uOA4G} z!A4n_i9`oeK<`nizFG_?=~u;qKq7UKTcvX>g)OAEQ)Qud38|d+>Ulf;V}Fttl5=AbK2)pV2MH*Xo&-ZL0hiou{X026H9@ z*r4WV-_0)_qn<6HAvX96u2eBmuUOuh+h29})5`d3^<&={P(c2+xNk_6+RQv3tC{EY z7)d5oHkBaL(vfOpM`j5b;E`MJlv!*Lbih3lkwojk9c376M8gk?WW>O{{Qd#C>Yapy zeri?p@Ik^51?H@5nP?W9szpST-`Gb{r+yp!TQ_-ocsm-ZE~4iQjs+3{P4ZHV`Fcp& zP4z~eJVl20y;40qmtc34*F0xbDQxL@o~n#|lN*Bnv3LXPWCHLkKBaW`3DtUq>MZy? z$Hm2*f6%teO~UqNRShfS#bR^$@`LQX*d}K)@g4|6O67Tk9K6uVa`X8RbvDdLWCtWI zhZo&T5a(^9xCdla+s67YGBWD;rB-4tGto&s!#=<9(a#iWOes_jgmb2qnW@#`ULsOc zj});ei;r6$zm?YMeRWC{c^e)YhCi3MX@(vJs?$lKOGy-sxBf4QbO;YemzKy4wXfuC zlqN+$64vULi7s4c4>6x|^YL|`w5r#VkTB#vCZtp1uRSG&2_zuEvlZj;dq+hFD+tB} zJIxvWqqHc0>c+rt;8&0D1;O~l=jT(Xvs$v^z_Q_g*CBWh-lUsH-W*9>gC;Ni z!wOkPciq+&O=-nsm5O!~4pM2o8EwXIPW_Xfsz>@xMNvP*Zuoawzqv)%_kd9J-%=cXjAI4{uPDl5Q~H1SEj&OX)8t^EKY7e41R&9MGMXRx&!(oy0n$sy zES=>qJo&pC;TBRLkvSut=KY^d=>Re5s!@#RRDX);6_EHtoC8AnCzYoX1yEr&@#Y=> zj0A84%vl7N7AuJy_&=Mn`8^|;Y6KxffY}FxrOott0E)@%EP+a;Re^|vPOL<&*yNYM z{j!VT#^Va&qd;zFYYt=XzvJ>iCER_;LO1X)KNqN*B`hWNv2O3XxQJ-410D8yrtpvN+@wE$P8rdGaU0 zJE<2Wf_78a2Xl&d7e@?@D<3imd0mGJ(%DSI(1>`|ct7k@0-!Y6>+%2$=o9n4IYV&V z{c$2-58FQU)Y#4`+}`*$8aZ$KL3r=(FnLkeWMBfkwv#OOi;5y zFm;X7v0TK*kFoC9*)3N|*-MQh71>Lp(0gJp6+e>Oa=m6B9)mWc2N6^ky;Aex4E4yD zbmT{Tu5ndWtUSKkb#Lt#)mD`z!^h9Y`A(z&cF26S%~OK#`fx^-*GdomzyP zWV3vfW7Jp7H_;@ToAM+8a!9#W&(kbZQwC5O)9v}IxH$uV{gM)`YnqU-un}FyP9`du zq&Ve#iQ#(NxlQQ($*YgM!~$MJy%<7vHQH@f@o(*>r36*tqg=$HiUN;yW=IpHwU!ep zBB3b5rIqc>KrC@890JN4=JKVw_{aNeaB*T$K_!aqW+s&^o^~@RRPCEm_hz!PA$HmK z5;q1Mp;A!0?RV17iEf#Re>SVYI@vtiPkxY17Zz|pw`Yo>34Bqu${mJv8H@!Yj59chSJGB%16hQc0ib1=V^jJnce9#(7uJ2uO zG_inAj?X^bUZopTIi6!H!eZp$zeD(w!fAigS-Z_B_TtdHNUcmyJ%vS^7Jz8U<+@x$ z18$|}y^HFQX_v|&l1?8M8b!iHARJycBB9Xy!cj=(ijW6B$a~^A8+3e_p7+iZ#p07i zQ0O~eUY!bqc6z=Khf1inuyh7xX|6}J@-41=dpFitQH5>gzQq?%x?S znPuhVqIo8mGH1b`gYU;av_F?Gbc#0t$o$1;KkuVpG!dG(NH@Q8e?DGmMJ<~+oe6cj zO1I4A94r&6#=PJq*_***N<#< z-NjCsQCyvfoLpT<$t+5WQ?K3|Moxt-uh|lH@X;sTHVR!17U4t|6Bn1xX}evfA;HC^^Mq_B12jygFD|`$vbikYx115O&?vmr z^dHnM8(%Nc74FWh$eN71uu&&JN;^327PMx!I1XIwsYqqdk@#(5@%XRKoGSA!2L!w@ z!9eaGWDT_xHfWtbxmi1TSVA*53NqLjh%s-dh=W~Q-+hPb7=3g!%JC^*GWNA|Ozcfh z6w4bh%amz-6p7!B0kwRF8TGEf70`1V=(@k)TLzPdLJjjQ25O!wr+)0Mv3BaM3aL)!q=qxO`I&LYeUF1pu$E&v zp2agp{|XDMf(+xnI;3IUJ9^mvl%NJxzaY328U9lIQn$e;vh0C%9h1R!KD5SdbPvaq z3|#}k_#Y{e2F78|o6N9rC@7n@TQnKg9er2d3D=J|kJ`z+Rmm9`D2QV)sA)S~ZKk7> z#>oz2#(yzHN-5CK{)I}Tjhd^YoqibW^KQY;L~gC+&T81}s}E=l0hH9Fm7CKXZ02mt zf8!J3MH)*GJP;B_wZclWM1mk`+<5n^Lq|En+VARx;sO%P-_+ga3vk zq$`+OwIFS!TvP-Xy>nx$F31Y{KA!p3pIA&K35MpekXFG$;%{jHKF_$--&n2@Z|nAd zupBJ_+g|n$kNyd;rU-zijDtPp^M7f+i;^?mK3%%U9^H zU$f=9D^}nXCI%&_Dn8OAB*?(z8(od#vwGJ^4F|g za!ftEd&c?WtNeKez8@o^FN#o-DU-$tCp*SpofD43$_rhXH3~*tV*)6|+M{OAb3oar z0z}uUkMOnGc!Icr{|0)oW@XOCneY)1U=}i(&Y?-{d%-qHj>CJLw(Gd4UE*rpytC?E zp!8Y9Ullz;)9!MArVxM*Q=R_)#ah)SZKE|7V>Wj(OI-GaPXVwA;`fRsA^;wfzx%Dp z2yiEgae3YMDT6N-PjQc@a;0+l9COr)Rr)pG690uK1M>i1V%CsFuc8&V&GI~(y22yR z5dgY?c<6q@Plz@C4_u_dJCop;XsyMCR(nSxB-1i21KSO zMkG@6wy=8e#nxoLbsz`ydlKpn%h0^D>DErJAG>yVXej>s>1G#8I;Fh4ZEvv-b&2!o zFCS08i(*J0aM{Bjk)-1Q zsHXJ3I)vnHPc|u=4rj7dX7M|#0LK@|0Q3hcI+?4?ZF_()B#@atPv`OmDMn3I8 zBRn_PiWEucH0sjy+CQk{G3(Kiv^{mG0R{ZqA8Pgxfuk{rX}hn}d0f7>l%e4~bYcD{ zhy_>=@EL$5N!)j46rQ_RN$*7p2VgA+lhI6W&5k>pCMez={z57AORpQj6X5l*_u&$H zdf>P{3n!6VHV3DqH+gi4GE%%`)KVHrWvT3;&-A%J;>NBZznIZ9CmF#{G9>ZyqmqBOSx{lJHhy~1ke2sw|cdl`= zU;ze1eL}`Wi`zl*r(=(WFLUX6!XXo$EY{y<{hhuPgK+ zrxc&0bbXDbBjPZxH>m!b_P$pKPk&SH?F-cM+bc|sp5Uk;Y74-nQ|TiFXY(~CP5A|J5p#Ie z*)6t7KSv`pGI*EhGA{SG`7KR_X&6t%nd+MKA`vR6VoM80%{Nn`zGSDHSpR$v6Y$*6 znZ@h$9>S92|3Wh^)d@9bazQzP-mk$$EqxXZ-xR3-3Y=e()hO4aTX#;psL<@cpP9k)BEnriZ#pm7&vi$@fKPmBOJot2(N~nq2V0rq1FtHx9Lcf)Ha2lmeghpVo zyiq`Xmpt!*ao@dF8$oVNhtsV`=heB%ng7$Zze=5zaw;q=qb`;tlwGC#Mo4zOC$vx;oC3FI za=;wLd>)E|m(aN8IY=1ZfBbm8mIzpZnI|{34lBIpYwf+J8-i{KfwYf%MI-{TO&wl* zFXkc#T7pEea}0r>|A@g5tT${9~md&+k6N;|0-iVLfK44c04>LM794yGXvH} zU!t`_zn0n{nVg(_^!nog)&Rn@U$4pdmoGtKAJ#sPGFIEY@OoG%#`A`+TLaEQEYDhh zvU-sD3sw{>1OzQSbFLd>;iBx@$n)z@(8lCY=&l*DfPe^7*yWJ;Jx=qyO-RL_^?ayL zS#@8-98P7))rnXdQdJbP?;E;5Y2RTkF)5Up5~d+CP!lp%v?VOdD4)hP$R6bS^1))UxMwENOTK?4Q#u+N^CY%!^54BYw(p8gCT$UqaC2PpOooqmsxeZ2H&>$1_q z<~c$59mC4X@izL<;j?HTqHqF4YK{u6>K-IhtzyMo!6Q7LG^fyE!>5};A+zJ5ELo|D}odK6AJgV7K(~zWw15cHv(vhAGwV1IMifn zSRMCQx`S^OmQi;p`!}Z_L0PuafDJ3Ji=mPFtO(|SU(XjXp=~@qf11}!)GDSxL$X?u zPNYu1>K_VXIlj8EYD6UVP-0~RRbEL(CjdJ@|0Z%Z8rLqz)lbEhv!Jjz$t4c&D?1b4 zwa*=g9?Gx3`v;uq4p2_?J z;=0jxZdMDm94T2XM7Yan`)S9}=Qu}C(?Fb@+LNq{Jc2~=R)R)kAy}RJ;0N#X_kj`K z`EEf;iAwUj9vb@Vu*YvO?T}j4>ijDl@KV&B=Btbcu;)wNNqn`P%AtYDywtm%y*jbd zK7gj58g{T(|Cz<@S6M~47;LI7~(EASJXq z-o$dH3~Rf)#*u?_y*caK6B03{#B?}Ox-hw)%u)9~_+_!52AYh(SAZE9k&a_>LqGFr}HYxKBh(a6m~ zH&+pJl9I@yYT>!w|7whF6iZ92+ir05zk6&T4EBFqTXv))41YPcFvSap*$sS;IBU)H z9J7THon1s8Lyj1f^QK;}ju&Yt%C+^zwROUhqsgvzbHbz;PsDw9?i=hA(y}p5cO7}= zOq%o48>Lef$wgLa<8_EmARNLnp5>veT>vgy=eDy-_<`?j12wgwe-lmPu~U8^Z5>N~ zY8o%~%ehzAAyd?@{B&-Te2>MY21aH-N%nb@vI3Hgz8C$3JX`aGzc6oe68t7hWI`g= z*eIBuRg<3bpA}2mg#404+u*}V8{0J_=R_FZ_H@BP9Crw=2C);0Q-9SmTUM<1()FdL z;X%f@p*JorGLd9Q)o9Pu9zz$TT`~M28}31=8X^gT)BC@=`|7tUy0BeAN<=n_l)$D$ zq#LAD5Gg@AH_{El1~wg1(jgrp-QC^Y-QC@=5jey9o%hx6PdMk7xwzIfdkr(svu3UJ z+%f8Omr-kLtC16UKp^?b+Y=ZMWH#esTqLr2v~rRhPb4mp`r0Bl3GfaC779??%AM!< zMvwpHl_7-#?+tgO*20$-_}R}fcY0nn2n%<*q(8gR&FZmnt^IxBeP}~P(fW8cI_umonFit|>B6`?%qe(0D0r?!9Z zGhc`UK!SB0e*9nOF5v<$C(Z%v?_~cM5Md1z^07MuivRxkKb?Y~EtHsxpFRfk(?9&| zUn?K*hI4A)Io!y@pY~k*_U6^rUz%K>vE3q1NzvhapF8`r_zvx(NBqS-F^`qj^trey zZ|pAyz%L;xXWTT_1UYA*d}l0r79EdW-^bfqvRht1&9T4c*x?7gvRsd1mf6bdP0#?r zrja+Iw?)LO@|`CqogsEGDRw)E<=w0#2L5E8RXrQ31UIIt5OibzO#23D^&Ie*#J-AHG(SW~aBeoxMd z`{YUjn2J%*IadKWyTpJ7Y%N&p)?8c^S&4(B@0NUm%`Ik(~zL zq{zJK5=xGnmjRZ{&5{$3L+)C6{Q_RnT7c8$oj_;QD%k2zv~MSRa=!v}v}{w^(~{vF zX>r(<|K@7jc7F@O^)l)wQ44;9_2^g0)o1*2>1zFYhC!!v$_?S&sGBR-tQ*}7q{u8* z$LC35mAlA|q%$>|hSjGBH|Z%C_p|C{KkmE^u+glDB+Wk1+1%W=E2}EqH#%)mWZciL zt}L({b%`L#?aO{JTa-3&ie^w>sJq(i)&4XC-7~-dIe_Qxw?;SgCzET)W&G|$Zr~17 z=W&Yp+|TANr3%B&1f|(u==CPGDo4H)h*Bt^)_QIbV}X`NjII1iP_0hXn9CO8R+sE- zm!p(UGk)f?IP9004-n=0jIpK5I{MY(;%P(=TVSmE0O{ia0l?8pr!4$_?0sq=DcZ)~ zS*A37MX*-D2me_7B7JSd`zed@Z+tG-J-@Yq2xacY;aL?S(B3R<1k??vfl^m~u3V6> zk>a{lEYoE%o_h;0nd_!Oaz1R)jGB>LteHD^HY0{j^C$h$B=Np1dDuXuBC5JHypJO-rYL;!GZW59$Twq+x>+}6;GLDZtB&}JAkyO`2&^2Ny$y=cj#Jo zyml0Q5@UL=Qs}YUok7n&&k(cn)_cqXJ{Zl8I4Z7bMgJ);px40=bIc=oGP}%MZ~cmg=J5L zX_T3xCi7Vr*7MpIv>#0i|GY7p8EC&J0<5158r##98*BA8FY;%!Gup_|!Y~sLhv4%D zQz?T#lb1SyU6u;DsS*S0aRPpON5{K~_8P4fbgax>luj%9U&#~@+s%a=64u+pz1Yvk zGw|9%{s-5V?mdFfdRwisv1|~P8Y$Rlq1*MbX`y~hAE0o@dus&8EvQTt$Bb?8PAdc!_M#+s4PM$4w@|B7`%GB|?@Qr!oHQs}EJ4|hfmS6&B(UQ=%!edM@Er8a2B7=NO zmA)B3dIb5_Pbi<787{$N;izCIv0x(Zx0GiKUi?4> z%!0b$^5vOtajNUMTsD?XGPPVu;2qsHPAnJn*OHo7Owfq-EuDN>)m`|i+`NF$KoS>y z(Nqd1n#`tw;v-Lz$v5zEfM}GKk6s<%C=kcyTE@(szy(-Nymtkdp<%`m_aip;4attX zrb?>#Db_Uts-4Hijg1bwsYa%`_asx5;)jIm{aK@~^I}bWPnLmgZpINi32`9E^(>dr6}- zCkOSK{nYX1PNBA_lhcv@y2$1RD!Vo)H-#G2W=Sv|9UVB!n1rycD2o$lVy~dT4Tr96R2jfS=e+3NnqNWG4a&Y)U#JvX`Xe7ddkK1PLp=A1-HnP zj}9!eOb6zw^)vYmo1YJYX_u-Eg8bfT+E-;H!trMKQ+9i4ypk)y2x>9r=9mHGNIJp@ zUtq_c)hs8zg?(N`c&rR7@SwP!Z^dq1EH3WLDG0{_%f%Y-qF0=7<>?B2uj&+sjQ235 z>2+GAFo0q5imXDlok$c15sZ;3Llmv;Mx>)5_KMxy;$jtKAyY*l%+~V075(jHWpZPf zxjLsrWzub55D3IJhJz3l3i$VY%pO?}JU1*^up%>1%Q{CjwifaD@d~y#)$rc&%v$vo z-{6zQV@Oig7vh!Da(=X9Ea9SoUT5p8$IK9Wk>fnpon2LM4AGF|{nb8`zh58|BQ^es zxA(IfZT&|p26kQJPA3Kd9yCIq^Ho8b{f9s|# zjiOf_+wBeyqD9Cx-a)3uvnM|xi)U4c$k4_?RIqVbom;+Cec2`_IGvRZU);bgeXm_G z#LqX@gHegWs~|UVCE$iP>~#Bp@c~NRv-nzUU!asHK?RjnsWEE!*-(kYV6gM;yW_6N zaqP<8utC2zRT_{e3Uw zaGHTeGgCi-s^3k*C}HX;@;DKBU)FQJUL7G6PGYA9Oj4XQ`I&*6W?Sg(=K72NTIm6@MF`4gT!Y zqZ{xw^H^NGp4uSeVacX}bZ^V)Uia&6_8ZV5Nc z^}b_D254B`?52~G(Nh!~wUsyDxwh6r3L8u<58Ey@$8I(@75!k<^?(~v_Olu67Hq~K zWd?2Qy69zaz%FXV1~AXXT-=;zKHo^-*{R1sU<`zvYZ)X_sdKz)ZMmy=I%}w!Lz@8H znAjNgs|1z^yj?+@#7K2zdVP_@byo4j@XloZ?;N$MhHk2j2Hw@t%LHDAxCI`+9P{%> zZ&TfF<;0I@+HIUf2$l(2<|o5Op_}<`*ETMyn@xl2AwcP~f}<1pNdSQbX+sOFe1-j! zb)B(@Qf!XpQuLVTY>|YG?s2itqK@V_MNFODjUX{Jomp!73w8Zf&;HbjvW@(iUWW3s z{e|+@tGBJ(wlZ!s_;(EsA6A7bMCs+x0QZY<8^ggEU(>+``&gH&QhEAzDn-54p6L&} zsLumxB9(h_^2D&5o$L=I#iZ5o<;>@u1#Y%9{j0z_79Weq$#1hGsM3o&yaJFtZxfD) zqtsMXp6{?5nSi^pBA{9u?j&vrJArx^ClmQv#=n;#64dFqAu8o|Y6ysrL1^DrJA>o! zF#R@eZ|ri@FLSa7n{RLWj;-{~D=p#^!EWd|Sq*uhWT>f99K}`J(W#7!hZQ<q)R&09_zTE$8J?T)^8Q6{I+!oCkr|B z3qG5243H_@&LzT!eqEQYGB@}30+n^?nF|y4LizFxwNT+egEcXIt9L&cTjY0a=ciV62%}_q(efeX?BD8fm{vw}%&AnF?4u&b@iGM5 z?x!_aW%LD&CQPTMKGH%ns^sWAfqo46h6jCV1R&pUKxOQ0TRT4_ z^()ZD>W172vu%dHV1EyQ7yNtMe$RRdZ`Y)txT`%uBbY~{j1TV&-9U`uo4dZ*~f32vkRA#z93sp z`s=q>tSpbtLIW#W_Tu6^$2@p;frtG zWP{>^KF4~*+vZ0lMy%;#RIq)DzZ^Jp=T{-t*d!^?P`N)eK3ABT<0tlLu@!Dsr0(92 zaJ!ltqGoOoq~yx;`|~M_?=Cvy5k9kM(=GBP*$ph_v9()50Z$z%_jw_}B)E2QneJV> z$QN?Q-KetjL2G}881?M~u}oY497BT6c`?{XcO1Ri?i~G;sro)Fv)>B=^!{5eP0tYt zNN#Qni;345IkW7d1eE1Xh)X#KskJ>!P^jG3T-o8*GKthv8!ocnbAerQN=R+p!Csn8Ca=ncz=GXG0gY~L+-d`lG z=CP2VnhR&DnlYC1IT=qxDa{9ni)hT*q|x=#VquK$Nx7+VAFNY=6Au91>&Dylz?Yqq zP~8w;Jc&Atgijt2WG8j?n~TGFb014u<@}5lmKHQEOZ_Dw^h@kr0iTR$x&*^HEC?kr z@0t$9QC+%ttdbl1?1maT>jAsfXjgs|dzGSUGqvSz%qA6`)g^|D!h8E`2MVwss>c^( zGj((G{Zjt<$ux9=+LgjFO)E1k#DN5XF>6|} zX?dOS^06Oxn)tm>A+IQ_=P2KC27mhH%iS7Q|Fe+7-L~(#E(?*#``i_mZf(9s!pZJN zzEE(Ko0C;d;Z%r|MW)!Icc`}q;-xVyopy~NeJ%D{K*-{#+%NM9i+X_?473LPDvI5X z^Z>km8x#gttnR8#4`^P62W?vqJ8}OR1|aZ>IBdL|!}n!E{Lb|Xt>_zxAFI4W#__m# z>sgY?xq6z2cI7~O1Ig@`_JZdq)E@h(`u)iz{6qG)Ii?8b6K@>Auv`Q<=}eqI zN8ud7s82-+z)@A*$_fHJ7}2XjD7lz)QK`1?(D0bRfmp(L1If|mG$U3b-c5Fk7%L`A z^!fE>yk<-a{$$b^`Ynw3`hs&pMTH}TQ`e=2L5hRi%vETDSvvCQbeKw61q~Svq%E1i zf(q~IXvJqJN0aKa?4B>-V-2AtOb^uQRM~n6ov;I}Y3F&>va($6Ta^U<^%EgcZYTGl zz5-kV3_RVb%WrCRdz;=>@7z?J^6zCpj+xxEz0G#6<{*_fN;$F#P@Bs@uS2W%+4A)n zv7e9wnDR7?@OodNK6U+EexjDk#Afof)^=+|6Q&MAVMcr=@;zN)O87(Ti6@!5ijLJN zy=qPw!Ci&Yqh=8l26n#?yduJg7GYZz$D%RCT}rT^Y8lvp6f#AE@Jm28fiR$PF7aK= zg>t~BXdZZs9CGi@W^P6+wH4LD&| zDK%%}O(s{KWueYtFzJ9=JGTh8E+M+VJu{`lZ8(^BA~{b4XH_UTuYyAtSJf~!$y z$MstzepY>{xm)4xyzN>1p$wV+*2s`I6|G1*LtlB}%vV{TNVw80j#FS*CGbT78IgvC ze8jA9A}mKd$$6o!X+w##kmnHAbY+$r!Zj~DT$liltLI-Rz}9ezs`bTLR3|y#yH|3) z@>!kf&fF~4A4M^(ygsWdY~#Vdq%6N-ILME>XZik(l7!sta$vvX`p$TJ{5Cw_ZLd{n}3Q}uUjehd8pyrWI zwVdxcM5vgK`z;nHIJ0b@8)_5n0v~&FZgT)>!#qjg)d|d|%?m#w>tMQDmyGU1db<01 ziRXH*ae)o{RP6pDbvl1s!L7>*wvk*Izsw@_y=bAw8XOc~9_eBdB;r1}6KsxyH1SnM z+Pbuj>2==J5ufu$vSkdJ$rv&&`wdqt#u*5_1=_7PEvDJgEFgtKg(KQu8ZS{@hTv(J zhIeIYyE4I4uVJ~R(7BIQGWj?77p>p0dAF?R;Fj&pR99YPD^2~6qRvI&@_N=PDcAs$ z{Xj{Qe1LG1$#~948hd>JWbvYOtdNk_S@5OK3ig~BX~D!U7VjGg9eftyQ}NqnEoRwU zNvCX63}=4pQTuU$2`Q=OETMpj{eCaK(+p^jVRAN$h8hY6IEUYJ5#B%V|5(dsY0|e_ z?}*XI6wT-Kc>){@Txa*j$cZ=5hxh1Pn~_`41SwW*EEGp$3+aKfG4 zNY3xRk9M;T82!AWnG--7!WG+DT|>S}EVnO|)QFfigV3xI7LNt&lXy3@%AGbjY}}ZR z^WLF%-tsZ8E;Y;ggU0SIGl-)T|CeYAF=+&6i;X8cyB#dycsGLux-D3G0ZI5}`Kk2cfAlJ>2LJ&!`J`BrB&G%_(G%R7E3JVkmMGp{seiO6F zbWk5t>51nluiq0ltj?RPlQx}usaGYk-XA^a@~lnYlpk2}XpG9-K1~g~KoR1bJ1;Tp z*;fx21k6Jz&7w_rm>+!Czdw#%)ZF5Wr}!RwA#EQ47fA)3 z&)55ysq?7P&{^9!v8RKu{9Ld&^SnVIwWE<)!8F~Y{RzlS@^#C2+ULrojuUpnj+eA? z*P`?1Tn3^Uw~vtAcbyYz(c5S(EQa=N0>^hM5jamW;jBn9dcbIcSO5 zQT%l;A^BQpjp+d!?E%3jkYn#39qNLrZ%4T~X~9!fR|`cSS7QR!`)?m#SHZ_{PM3;L zibtk=udr8w*^-Dt4VIQ}0(-H1W@e3Mn`#;ynu+Y?F8J4io`^onJH{JJZ{puG9mh_- zs9C9{qHx>sv_-qq;4FxdxZkxTcn3;o+&o|7tEX*nd5c$GW>aCZPj9KR6=+r@Dx!fn z_Lu^kWDZ$3ycb>YhgCV{2Jp#R&dLC8Qw&Oq%>n!c#Agy#?cdQamUdmgJ0!XECtYmp zwynQGPD(=9G;0j5KX)6ny*Ny|#JMRRo2j_u^K0`xgPk{m7B0Hm)irquAV0z}9W8&a z3EWS-W-wTQ6JLmnt*AP90UEl}p`BDCP#NAqK#`d47#4Q%b1Y6~m$8qJCE2w!u$Z#( zI${=G;Yx+fyq6k`T1py4o^pu$-xPiGZWjuSi7v+*rBHrH82khEXs2-?r@59f(Hdwh zVM)rg)-Q;@q$8DShNfC^f7MOq%dafi=ky??){LOpTg*2f1S~igKQA>q2F1r`8i_hv zaz5J?(_i1d_V3nwyO?}>BDH%qUpgRTy{FGb+{vE@3moU$yK|nO(cz$Q(l=ei~390`H#74oq#*TS}Ud2J@+$zS+B}Gp9Ig zL$ZV(Q-LyVPNOg*wvI>`K4Q^Fk2@X^@ZJ9} z09LCO9Wm)JarOEe;ig#V*c z1AiDlVDV}u&nEr_=O20}?SIgb*hsga|7{bKMhSpo#@XyD|2m{RP)I%49zd~v_9y@U z{j(lT=o_J-Y^d%ye+#V8SP&5=A`!6rii??rtBLdy!^Zzecsu|P^|%)^EI>WllZ$+% z2H;)Nf7)#S`oBU(jbQZAs8`V2{mBNvhW?0)z9Cpn+xsU}9=4MIb#mQuUHSqS_YK$A z^{~)KH;G8-Z~i(HP>b;)4uuYd4@C||d7v+Q(7XlYFo1IYbtMip89@tc7hv`KnuSI9 zSL!cBMdT=v|H}?|NC2#FNG&a`lp;QX;q0b%PbLR-#WAx$HfBu&uhTvZzwn@ymVIi8 z!gbzH8jxhQA*K3Ss-5*;hTZ>=P8}$)h;ab?Tl+_@iIulLSLhc4D0&s-0>$&@?~MLH zO8){a24thg3!Cu&3psU1dhw8^SY%+Tzp}I{9kBRW9O)m7_`CIkACRTO-y9PD`aK{d z1dvxrP664ZzgmGo^|J5D^A+m{D(}_(#Q|T&&S~)K6<#%(pC_l)68)E;3^mK3FZo&2 zcPaUj6OP7pW7&xoY8&+Ia+w7yl&Zu^Wu*lb-Ep=%8Z4g;HKJ`z@}v7OGajd;uV;`C zYzqn<6m8yoq9e6ZvH7h!026rfbpV!h>f}1OdpYP!wYVsYgeCY00jW;Sy=iYeI=P*~ z{ShJ>>BFJw1u0YQi(e!N|8oYGU{fwKTA!B7wJPubJ%iNUofZMHl$H$s{GpHkmpVGK zVQ-qh<>%)gp)AWqLiYUt=_EZqKefMnNd)?H!(K=^xisl{bqXI3@p|Wf-G4MU&ri*@ zVPm?9wVIbebgl2>q0&&kap(qxJt`|t6UF(POS2pnI6LLsHgGu4vH)LnxS zG}ylqP58s)T!{U*YD$R+#ux9PpPVa)jvFx&5cR?TKIumu;uJq&ENPN4`SM+jNvkSJ zws}6Py-Yc|x$6tMQ(|sx(SfPt|6S@G_0M$tI7;@w(Zs-8fMzAxu%FA;y1qDoBesklCmt(~+pz(9n?YGdlrie|d9#?Xo#){K_o8 z!O~%A0zUI%K>fzG6?S%h&swo<4-5JC?i_NP>}9t#hWZ_$UGMD~o=k=aRM?#FoIkGqN-51@IrADB z8JVMMhZp;cZo2p-I6Ntgq4EAm1o%ouWFi$Co0OLJO-);1P+0g4G}9uOD;Rz4-{^%4?JdV5pF;jWaLB_*YLzpswN zuXaTPN+`*MzH|gdNZw0@t*z<7zHUX7xO4`KLHg^5)t0-mKEwh>Kx-RoA5>wnZE;L> z&{+=ZDml{oYN(_SCNYhGYDt@`%jib`$w(l9yrQDM*ZEH&uh(QcxqOVop+)RMZ)B6q zLTqklpga}Jbu*PZttfy9Gqf?`9T^!}q8`8?K504Ie&#zpdAiw1KI-;P^Q(=`dKU?% zq=G_ZVlF_C2L-J^{x977Bb6+TDk~x2;}GUI%6Td}_F7OX4xL`yCm0x!zkZQF*`8AcY^*2%CBZMA26JPe zKo_2t_WFUWp2vnz;R>Gbzw2*s8L70?cKYp)P4)5pd+*=2+w#>eHx&R;UDmb2EViv( zI&5rAfq>m4a^DSl{H2zn9?~45-#t=Ol)rfl2nqcr*hy}-D6p#rdz7_Z=#K?LqNzV; zbR~xno7(0qKVQCZk<$rF;BnKEiE+B_^WNxx>YxA#24k5k$Hu`KZ)!6# z#0t{Vr<;g~iRm?^Bs?q1#>!A{Ywh++3eQRA$?k>&82N2W6S||#O<*vXoyPEd?S*|a zr#`7SfP>bfD7vh8PsnQ5JC6J4?+=CMnG2phdN8GWR(5@T%|W*d2+3TFX02U5@C_E}2mNoW`EBvsv3iKQ zu1RM3fU12tG*h{?j^o)Jr_`qj+~>o+2WEIUI6aV3k%gl4jP#FyY|w13N={yC+i!tr zsuO8nxRiCFY@V%ry0`xTsxn!?GPhUKSOW2w_n)XSdN#oJCvPT@XUri7MqtKDbM;lt z5fJ}XV&noN4n1s$-({rzgUxAjl!``!^E8U?tblR&?~@?Tw#7m1H_}P3uP0AQ=8*7S z61`3p4~YV(-!*)6yXkMKsl#5{q^1mnl^KTUxn8FOd$xr7kAU1Otk%Gun}R|F1q7x8 ztl6W@Y$B2-L^#b1`ESQi4C<|qu8vpjP&FTyp)6^~%xw5b@0LIro^?@EQ_FwW)UCCQ ztucA^-+R#A)WgnxR6C!#bck5Z{FNQy-CV7`>_na_y{f7zi}i>otYdB{IyRO%SE-;Z zMKw7xrM7c=!=S0E>fO!!UY?n1qiZ6}Q8bu{ZT+blN6J0*_X%Osp@RJUa(#^RNSq%( z&KXJInif0Z@uq5pdHKKU4ld!)-L<@Iz5~mo9_Icr3cTo-@l>$As799)f8t7=TSp1j z^w)CrPQ9grG7d+Pf$&2TK1T)27%I&z5+p$QT*mn{Nx)uf;rDnv^i;vs3GDCh&*9z~ zCqQ0Sz|Hh_2;~^gf^~y@_h+Itl53)3$+6Ugn_2rgasNujr%|?Howq6CAfkNxwkw#1 zp5EN9?821ZdNh9VOtl?O;<_bslxo2po#cB!!#R0|BKmA^eSN(jIy-pQP3l;nudh!Z zJdBwrE-4Feh;>7zcOcyVF4sH`s|Ax?WxFP0p_d+fJD<%fDG3T7YUn-Zi>BF)BYUK~ zA=v%n8U?~dNvZt$sr1s5vmD`!k?J#FyNl3mwfw2JKA`UsIHLFC$tY4DtLwZLKNktkl^v*pFDYjB>h@k`NJS=cf=k3w3*N6ZiE^IPL z?OwPPGCy|~TG3Kv1)YIw>PKpbIN_km7ONFY@40t5(b!#Z?k<0MbN{%)uFSxbu#>8~ zs9cNCnq;^2O)W(sklF?hbfxRqFp!vnp!d!hVr{C@!PP-l{1!WqB!D7Nux+uoW-t4k z)jz|#M0KS<sHM7~AS~ayMtjy_zJG)gs z6*J4Yj0t*wX1?OKY}I)>f6}wsVyttBpruDMHYPp~Os=|#rnIS?+!H$BJM5FEK^9M- zfp1TN4?ggD^5j{}hbJ$9-{`f&uXTQEbMf&-oh>DoBH1J!+$ic+qouj#} zQ-4S3G|<$vg{r2LrkpIVk*y7rp|R~-6DC(1yPs8_@VoK?Uu{gB3`t#Wtlv5Ex(bm0 z^#(8S{bw;VIq6@oI9UmhYsx8-irG4tkiKGKVPYW{geN5><##YPJoJGaDNt@CKu!+dC&iSH^da6n{7Js~vF@M>(d|U zFlks*IK3l~N`|GS8Yf@{{NrHXhQVV$JXJ!iP*kiykbvHWBtU?l*>xXpmxd^}&cU#y zx~CqC9iz$aKj)%#P1I!A`tG6%7p#$EdqC$xZ!@@o#BZXc>{g%v^giJKM+oSf~ z#wSWQgaRo2+wO3T0 zdAC{i=cwB;q}xkl+-|4A@CXR!cWYX3NGK>OK1YcRZgeB}p)!9;?MvMff$+mkdx$@v=@Ir8GRzBx@|E%vale<6ha!?@&qI^*TZoD(B^khb-dH z(prog|6pRt5Q=!ooDZv7q8d1!r$D0eRp*0Fg@YO>k;!?ZTK1cNSVk_jL};99nW#k6 zGOdQanza+UJhr|zk9&uf2LuMgpS{ERag*9e%NU0$NitCgG@Ml3nf-LWie#@~b|%mx)% zB?6=rHC?%hxW8n2-|SM)fAJRJw4M##2O&<`*a0fo5f{|LV75*{7KFJq#EZ18XB6D-J^cW)kvn(?KGoWh!@Kp!o|k+QBXF) zVcX2gu})_0>*Sbu@DAu}VCZOFj$j^pfx@nigOeDk%(OL!`8TPv&ZMzwuLW$bvZWQs z+&+)?&~=N?ioQ;-TxwnJ4N~gp!oB|Hay-l4u`J2W{S3+ONuErtx2M;$-Mh_RR;O!+ zAKez7&)J_TjK>N{$?q*~Q@Za*Xe2Gj8QBr1r*52eshp?O(5mDQa#(&Z$l%7|Iu3Y( zS)mQaS6q4?VKH?=S#7XfBBgxMb{V8{f=O$B^!_30%%SyR%Oj4~5!?N+w6&ikuD_P3 zQSswjN*d{ymwPQsjcyb}NjK#uw~(xC=p0e;F0YI|G?DG$jBLuVtuaXY0!hfsRG@O+ zfKpI_J2%fqf^17ar za$39Np)Q!RoK_mS9E*NB`Po8dzRoPs{lhvXNF~_;%lzycnmhz!9VHQz?-rww!eOzq zH&wiB@4%%LEOyG}xi*;07Zi%k2sAx}= z2t5z}A$qk=bbj3%N3Wo7Inl~a#OJQrQr+VO-dRi^PUDdf@_FRU1Er_Dc`#G!=60Fp zD7#ZGVzE1fEH$At3-Bs2TGjrTEFKxKWJ_&pBe}kA;MgI^Npz$d{i1}?=z0uwuvjrf z%@0ao5_@rRSf$2hDDrtIVx@mmMonxp_BBbB@TdOOtK%Gwn}Ly?ivt~fA$!#g-AZ#o z9WTG;NCo~bqNkHBBdtetCVeUCqN1Yp@}5`O?_{R}5w_!Fw3>}A<{RDoH`Ff(8{+(S z>)WYI7r^=GlmnyHv=}?zvY+XeZ|A7n@D*L-PZY?i!Y^=6Q|HS)1nr-2s1}02d*dxQ z5qD0wKK%L|&o@u_C5lv?ZYHI%BAs3Lw~)Z_8xs4@>E;7H=hc>*%6YK0hl>%dH=f)} zEg1LNo{C9h7s0a_cBRHCcbxAQHNB8SNNau5rykFa| z5-((v2T?VS9b>hG#itf)Fcr747sxAGmSvId;0J(DzpeQfd9ZLyx_4;zgeP0A4Q_Nx2qYCj2{$am! zrOMSmI32;kKS?Rzl6)}V;Qw%UkMjUqFGj@cl1IKcg^7vT6`|l!H1mZuUC1S&_0c(Q zVnV%CJvQd0TnDxvjQq?%9HRoGPEFnuCLDyVq5Mr5oZ?&QSgO2&M!_NM59>~ur7u$( zSkpMxY(aeyWBd0jn^Vkp@#s#$1g)ZhVWXd5PR)l)cKdCw;p0DBoT|r>8ATs30UI!wn(6=gzynnGd#Y zz1X?mummN&U!_6#%~cnicLoKX?oN8dQkXH#cOe~p57{8PT*G-Ai4pqkahU{!!>pUt z>~^XGt&@p8lqnO2J*)yb-5OBPYdlxoHn_Na(^l+w&HR8d3fi1`^#M_PemCid>)N#bb~3cLYDEk<4(T!jT~;v zAIK8LQpw~ql;iH9b+lOQ&)iFe;S7Qup2^$H3rXydUG3hEsirIx`UQrL&#rjeIhft8 zN!LqWE~m|W){DBEdtK?dD=K4Kvp2bYc_vt@i><|_%^SKFrk`y0aJ@}36&Uf(`YwWq zFQKWDK)J>Jd~&5L1jFJh)b=Ku!vctGP-Om3XjfCx*{);(Ov=zj7tU z&vt+k%JnItpo>DX(ol&tE&Z(TPgRoSv<^?3L~y~mC)miT2-kdavk>s^)Vo9&n^wDn z7V&O&SdaGeT?bP97)9iFKT2QsbApLdojUV_lt;o#zRF76u7qOR$4CwA;dcs`$}&k` zMSx2&p4=^(G2YQ^BtBe*6vD87?JB)hGir`L-a-~$z$qR(>BvvCT?J9SyrHvjo zdY4;foyy<=6@*PL60UbGH_fl96*97fyzfIwed-EzqV#)c;6QVC4OQkN5WUGFxfccY z*;;e(lYa5bRc7*4m)f8(90(R>Fz1+OhO5#Qop&1|)sg z3D8fDp7mip)7bT`Y_spwo+2ejWc(Iexj0I~=N@M&n>u){B)0jnH7W<3<~_*32ZO^P z?zlaum|#XzazlYa0;@u;pn4tm5$PFu6v|T?PoYS>ZqTROY|C(qTSuS7Wy9*dcn z=NneYEBUSl=L7tqRGe!Gf~glpirG*#Rx258M`2&awclWM0b4@Z&KF7vN%>CTIka*` zAl#=WZo5d+WMV-~k2?a?k5l-qCK6%Rdblvs#+g$wMiQ@zd_*8Rjf$%Z<=!6u# zQL4sM!b|aS%f_;lRPtbng`8C-+6JGq;-i$_NhhsF8| zU1-simGZ-?U$xsrCqzNRzU)n%B<&Hff@aS*3l%?pf{whp63$4ZQ&ErTo7_#@h?Q8K zEX0V5i>KFQRvLLL`%-Y0)$@6$HmL=PY-PjNr)9YW1{j^EVV^JAIDM|#=>YXA2hsMa zu0szc3P)4N=AkJFsjoA*`3`5vc(_$Mue8l15TA$Ef^*X{(O|bfEvGK3vY#1ggbsPS zHkQ43izZU6!MK645MPubHcyI_TD{ts_tNWh%1l3Etqx%Z&7MO(fmtv2EVswn)wjge zpd!F8#Ol;k!9_Prl4I;-FFq!O30o7hMi_6>;pWW33%}H;&HFy?F;#cA^~T{Pk$}TX z?X{5YD+S@GQ@LcecrmOT9+KJ4nV zGLx=mloy$H#bhW|M>sP))%bdR3~;`!J2Ddl2>E19BSsl;&22ZO2X-1}GH-qpOrFLFD!95LnZnreAR%E@n1{pli;ZiwO zKTzxTgA>DjEWpZuUCxGQuB<2?#mz|bkQbJNT00I-^33VI_{xO|f)#u+{3Q?>i@U^c z$8y?ljY&J}HD0{DZm+92jhm@RUSvfCZl?t$u8$SvYm%NN%dZXc1+Avw=HVrC*wY}^ zm7}r7r{CI_4p2Fa3snlhahycUL#ZD-Jh+LYLdMPp)n%^$NHOU|De<#R7v$)a|I+cSPq$KKAS-*~u64w_Ee)>`&(>llXi zTDai0zstlluLwZpk7H0%Kj$^4WRD9<$>P5~=5r5;*E_>Eux4vqGc*i*Dn|dc2bNX! ziQe^mQRus!jo!#nCCJ!&27PXg^GHK5M(&DA1p>|~lqaRB1iPRn%XyVVag`u74ijT4 zm+f?ck|6O``3;421Hb)x08Nro{?(OR7eiYMe@`i~i`-j8#Du_hZ4aHRecT8)9DPA3 zJvP_1hUHINj0;85R9B*<+BvUusv~*(W&C@F-1OS}i$0l;H`m;xbc>S?()fHxbQoLW zjv~pcm$R?gtC`I53isuJ$8@K;1@Chihv^Dhj#Wl9+USx)hngJlp1b8+SkMjX6>mMg z{8ALZ#j1xZYfDJ(+sAx+0on2tvJ4+T;OX)CR*_(=U;FB1#2*FN?XZjJSl zdc<6ZgEp>$g>GGq)0~5~4MUH5+Lon5r#_^}h0@&I#d-zQ?UpW3auBlZUxVIEsz{an zI&)Lf?c9fcj!sjUanrBNEcmB^`c>*_59sY2Htc%0t!%g*HDtXjptytIqENbO%kl@~ zvK}l$Uob^*=_f^4yGSwyas}e2vf*<*AoNC)u@W~`Uj0!^Kc*~kAum0S| zPMsreQ_mwj>PEc#xpX5iH2prsq7jmO_=wcKFPMJck17L^*Lq57e3 zX_WLe=1U@hj&TdnRR$d@B*Y^jmqa#53F`10lt_)7J!Odhy z^TnFu*@=}cdaHOMlfH0}+XXYe41+@$6TGLu)5A7rR6nuc4K^HYzstua7UT|iE+KS5UQpPc6$)+?6bF6R@NN`K&T{fu9J zwRU@0$Yjf48oudL0)4#W$-cc&P5U^GP~I%b+cGay)8M+Ddq{*2sHmjV$67DgaleBBhs;$KI`ibVP!a=rjw#kTXO$& z5X=y(X}TNrrd314@Y9Hsz9?La-sV+$JMKdtMsFbm?5O)#sw$WDxORq95&!4O=@Jj7P2eKc-IBvTH8KP8C;KlNwa=}E7bS5T z8jgFoxoT`^%Xdw&Y4SnI-ytaXpE{9BKJ~1a`kXAJJYld?jQN0wzna8q;|wQ(xdCrz z)6VOmo#An;me-zDjJuyAy2iDg=WN9<_^ABG1WbH9hPq)=KFk`14^Px1=1%eD!muH4 z!P-JK z^0?==c$(d`Ga1%Uu9>5$zKtRZxZxqxJsl8KL=RSkJ@1m_kTNTmbCjMNqs7{XBeo!{ zsTEeNcd8Q@L_R!@FREe=nxW(LT$vb?pXwGG^kPE|+<_dOCi+a>D(;FolMtcJ@BBYzKT505`$LZ^F)j;SGm z1$Nbo&848XD^`a#k(YS#K0|(1gv&$rwQL{(hQ+x_5>xzOdwW*`h^keR7rggfi5K-l zFo4NMF+Uyr(DOzWTF;`nAZAA2tB~7`$c_IP6$eqbJ7f*2%GGObqWOL8&v!-JF`$^5 z2GH6MbY^m_mD+JjjQdpFVvy}A{sfd!N8qkXng~bWC|SQR6LCt)BPG@0=InO)H;s=; zAS0NrXMJNFN`nlb{1@fT_=)n~pDAKX7*2A0r}?wR)kxv$={_f%`b8Y5KM?WPNdR^( zn{>^j!1(XmzPOa6Ak*;m`1U`*_h=Tl;Fx5VQ@srBKd^IFkZ{}}Za2Lz!qeZPkQ$<+ zQnDLvHSyQ{0a(9{1Mqm(Pz|U*`WNna33Svk=;CJiyS6ZF7YzXY8)241GyNe_fHcri zK=R?qicFfa&2%p(#*^7pi(F8o|toAqPv*7xt9j@HaJ7x09^`hPleBvh`Ln zevd51GP+()RR@CjE{|N+Dy2=U1K?;ZqM|lb`Z_wC%Agw2zFZQfa$I2 zU4w=232kZhl(p3@XkALncE2q+E;=6qnmt zev$Vk=3Z1Lk;%;(Q_krlwJAb=IgFv?k*4-8?iKP0UjWBg®Y6 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®>(7gf@CHdbj~2N)-mH!;15*l3k9US-hlCvm^}0 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

@gCb6`%-L; zEtk_?`Kh@yk!ZK@U{2Hj1?zC`i<_ETPSn%L{x`hy|9%1WAH;)-M3B1R;|YSF$_%{2 z#oWnY*q$<)azvHP#O2F=>K5BWn;utbP?{64%WNafOrY2&cKY12kuI(**(lJI$VF4H zkY#O!1)j8Q4USJq*1ieu3GMQ3BSgZAG|P}FdaWBl8+<<)&hTmMg(TSs$}B`GA2oJz zQ9Gr%)pucSTOT;=sB)0 zW4~kDM*6kpxII<38Qv@l9J|`rr*Jd8SBd(TPY6v$RVW;@B-ai$9W=@9j)jAA|Lp6c z1)(MkhY@jS(;NI!kATHtm3vE}sfRw!;{Utd)HZPYONVR^U36xBm7fut{Q8o70p-1Y z6@PH$E3ceLObXYR^b1)Hlk1Jr_PYD1aMFF2N5l(6utE~~8>a;KoA3N2gi@LNZ(Xkz zU3mBZz#O@;asw!E#XViL3G%DT@7oK2gno9P*Km$!wn1{sZW$f_KU9q6iq*~0&^2(y z!EwQ5yAW@1k{X}>D(mh=D9-C9HSmhX@vJOZsbTVozbt3vRzsV8DBt(PDdDx?M!oIc zF`d57;j-|*dtYNApg&xZe%9)6?>ID*F?0jQo0y5}+d^;pmY_kfLybGsa$iPdhko5d zo;G+z7n-!!r6Xi!+&gFEYWn1) zC7nN2U+^obB6i%W9xB11Ksx1bxa)Ux3^iUb1@`YxdeqUL8=;dtc?QZ-hfcz&aPhq4 z6ohC2rn>5^%z<>{LVqh1e)-K@q|RBj$$rFac(#pAcz})G&t~|)=Os?9FaInClOA&5 zeQ-{M54qg zhpO{)_04vNzg*W#oot*R;`tZMt@DL_0v4lHpm9PhIft?_^c%xbP^_)=s`BpKIF^77 z?SZVC$V;3kQqJ#y?!O(EUn!D%+)mZzi)cobNdka|Fqk6nq!(+aw0iT`D`l7n>+@}E z=J?MS-#@4Dv~Ym->b#fDp5DX(lWy$i7^|!$0dsL6m)#M5awK@UalvmyAQ~+8)Ez}| zpv=&3=9~U#RPM(QSU*{9ypx%TA}NM@BON>xN?5-z(cg|J0FST~|^`#ZMb~$oZ zZ19NbVsYXs>J1E_ua{h~~*juW`2ntIut}=}REKk+8joR=W~V zD&7W&_nW+4*d@RTqmu}-vxPVNNZApUBZSM2;M;B8UfapAsuGZ)N3k$`B@Gh&vXcyz zIsDc*eP=zaaUTC5lKldcU!ZPb;x#uOSAM&bew>VN7e1ud?1=S&g6T;`$c{jwukcG@ zAAsVG@P<>P@2{_iWUe&j*QHuPDcMY%B#H~CsdGa`41WId18H+xQO?S))^asX#Puw8 zF)vDn8k|E!qzRgoh5~JPz5}C6IC6iuW5Y0Iy7ayK?0!+_%+$BoiJ&?e&>!eb!JG0q zgfIt@FVof}izDFAzDCTE4icGI-zaeylH(&LE4H1o4k{q~hkf{-a|j75`_>U6pco~y zI`Uu2a^NX7)+%+Nxk3+IbyYNKJ5}>4WwXOBUvzX$tNr(jOo1@6PkL2Zn1wKagwpYS z+JA5Zyziy|;FY-8lE^JIlPFt*A2ydBxAvChd5b>@J1ZGhq-5gbhJ4-EiV@C4F~A~v zbBQ7`fykY9G!ZTc3J8Y4k3)_^-qXDr;c(U{e zi2cSw+DMF14tdQX=ttETx*obe@kwb1ajs$gi57_dyQnh>w4Y-(kxN+{g)}mH>^!TM_)-@k^uKVBdZFtC*z4esM8DN zso!Z~3%JhavFIc;6+R*l-1&=>!mnTcgC(>qe3yWz8xuI5$XvJ6p|4gK$|enWK^&tw z66gUTrgwa{i#lK^`hld9_}B0e`rPyTAjx4KJGVUex@6JNG0uaJ zp;rV*7-6CYU+GM+5A&7Mq^DCCwZoNvc1!zs#Mp1K?>g;*Y+65X+;(vkgdCd_h4G`S zzw0zEo!xk^xH7ddCGu2EF+&k=GpZxuFw!Fq{CuRVbfmHrZ$;Nn=8o^9-INf-WTk?MJk4Y(JAu+9{hHv(W7y^frnDw@YX1yEP zuA}^dY!S{3jN?>dz(P-4yIquumN^QD$pf@X6J2p33C-zbeF_RchCM-2;MjSf;{!L% zIn~;3ipZDNvu5xh8YGfk+37GfR%GT2nRpwjp@?+W_%v{YNpa7EJYg1Q78ILtnXv!% zM;p9@;ZoD|!)mxvUC>Jz&c=E3W8|Je+0kO7SS(6?5bWM&SiT zVur~j!hZ**Gd@9v&fjp|CWZx91HZ7r`VU@v?;Qi(cyLw~h`A>6myDB!pD4G@1-(41HxDogM;cHf<+a8B(xTVueAx1KV@{Q3=ZiAyJ z*23s%f&bjJ$Rryn^g#)DGabAyNwM_rWG_F?yWbEmsCPQTUjBh93#p7A2RE*CN*?UG zUyQ}&009&j!7i1#gI_rwn7#l}cKun4*FKysNNfN8u~2E%UF9}{(HE2;8fLFHpK9<@ z@&UMg#b^2d-_w zVh==4_Y4deJGgZX|M0daih#I7WTbW4XG)4>f0cBZO|*)X&cp*G-M}xjhqknn%(~?FPg@<)$ZDG`px>f0kWf1` z4Kd)9pz7!H@OiE2p|z6ow4@Y z<+ga)rTQMx`(N+1RM(_!y)VY>68S8Mee#rlreSA$o&^i`)SfTd2SiQeJhu?vzj)g_ zd5Ov8XyZ&0laJ_fzS}M{M>14-n9&~^Zt!v1LW)n#1-=LvQBaB~@y2v4@1OB#(f9sN z?X*Mj3|#1%mYt~6P1grhVeF6cC2F*{5SbGNrpOP?V;noX2Kk;F8hD6!$7bb`=(PO& zg)Yj6@w$FQm@~Vv>h;5D28uG5TY!9EHH>Y39slC+g7!x)42!p1kNk10<_D_T#Eye4<#trtJ!%*}%=AR_otllWL8Bw^TUDEP^OmoML8S z@o#T`FMTg}0{icu97`4-0rwO~Al6sSXfel!q&gj9_jB0VfyKMrWzFw!FcM7YjTVor zn+*k-LR45S84c5X@Jv<*8^km0Kx6LNM}!}_j7h%|OOhHxpwV{aYe~wqL=aQSFEDr9YH(UH`b35-Eqt$fur}>h24KBIfU|(~Wa2A(u2!QNCLglbBKhGq~s01tkiAK4`Ym@I@+m_lpuN z14w4qSI3FkptMiodU)MaWq_}@V8qE5mbKj8^0D4SQRC|;r5WB79Vde}`^qyyb08&X z^dcI*pwNR;_nR)uASI~!uX?ScLTXRUE(L7#RECEoWccBc&yLhP`{ocoetu&PjBdmM8It6{?w`|vr9(&ouRadj;+v$TgcJL`W$ZJVqu9Ni|jYyoKL z*H3*KN|X8Ao|s)1zjB;k2FkT31kZmpZ}zw+A+PoL@f|6M{r>hRt=9dA)mqQAtXQ3d zuo68zEXfKrUUVjBO*)k+r;?Ra6w(hsZmN4W)!b$F7oNhlRfo3wWWw2!;vFXz*sh!9byW;8cpl$3xvg=_A!b{6*2W7)q{H_ z+au!suE4Qpq=clTd2Qe5d}XXiYWS{R+H6bEs(>p8{U);G<}+J1&nAj-qTu78j#D(uu4RUZ^kr-W#oz&}(2 z4yu7OcOI=ho?gdR;hb6}=nzXH^i4#NNEH1iORR|MrD+)%HF{t)7MTvF8;XQ3jceOh ziqM==U=ti0+D^(q2L(0g{)FJMD^sw7N}|NsvnYO7%(Vumg zZ`ps*1I_RA9GxvvN*OjBx8iwd<|xpTaPO?BVBI>DVoVtirR`feKcWLoS6oZsWWx*D zz2A7ST?1PmZ`)t zB~mvE=sQ4?KLTt9zJH6?rZCSjD@xnqdUxY_Js^B9rXX7P!`DaXGV&rIR_0e2)GQ7x z;IHPRyyr!;^}(k$2_&m~LuUUeoXR~~xjoIHR!dqe*!J=M_JQ-ZtJzN;yV1m@^%5v} z_<>PrA5@q#<*X)FMKO0lMIx9lb7pA4r`B6by7Ya6ZZF20eCoNyrqHkf8R_l%h8ySN ztXfx=kerMZSV_cA<@xFa@WAz$k0rR{U?5m>4%@D~)UAeHDC15hsPw(~DP76K?2|dK zx)UX!bSOIt=D_7_jdk!c_%WG@`d(aEjD@NCtS#KPq-d`TPy@v3VZERsx)2R8?wr+bV%f@nRTK_!lliE+@L-L2oti{l)z!c2ze;)O(;i$E{;_wnng3w3Z``Vd z!c~WiXg<~V9yMC*LWwA?gz(p(OlnEe_zUJCYtvI>)M4Q`CIY$3efqg350b9hW)D_%t~Pas?0)~LrB*16YvCOzQ!`0C!m5S#7DC}oQr#i}t!E*K_ z5M@)u=6-Y>D>6g2+zazokk5^XEL;xJ2ReeSk*iA@%z8}3kz4G0>v}o7YBCCWX&l#W zCix`g)_&x7L_hr2{d~~jW-#XQa(cxCtCfVU&NJF%x_z;XI9R0)ae4c5r=0nGUq$C) z@}YRSttH#BYA4*LzzK9<;fH3%R?c0EZi z{d#!o=&S283mOy=hHYO!YOU6C$+4H~i(h+0HBzGONb0Q8t2dXbM`rN%Uri{TrIo^d z&CvgbDn9^{?B5j)I7i)Ivb8HC<8)pO+N^D-@$OZv03zma9Icn!8<-HTi$@6$|Lr`G z^+L23_=zJUkM}cGNkWdWQ)5q$hnhrm#Wqc=87ZQ2SaSAKbI%zEx0IvFOKj{u@a6^_ zFmB84_K2?k>)G;_Ur6IK^j@-zpPt{hU~m%Ll6>G!vdlVj0-I;BIu_hn%$lFoTgl5bU+so{qe2OPG z9m8G^3kkl6rkvkjpicf?3GpHI- zWBa?sK_IOguuH05jPfk4J${#Mh9jqTT>E#km%3@70)3|M!e39N&J-piiLYsVS<1ND zsiub`QA`d{)-7>f{%&+)C~}r2{TRr3s_x*WO@JLs$avP;S zi~TO6JlTuAV*>ApU!>*s2=k0c`ck!5f@!b+6%3z9J%p5%N)i0=S9p$;B93S!Y=Yt!Vgj4^6*BSdBHve3gCbK16wdxXC0IcSOWXfc_sB#A!T;Y!o}>() zsS~Tg{3J{yN#hMq(4VMUm9i?YkoV7-fl&n$K~WX>rAR(J--sH@g!j6Qrc81FkM93p z0)7NCa>U#)VpCTAYn{ZBGKdZ9fMGmVa1coPgu%T-70QFv$%e`HAEW`s?sv6XTpssm zaB74$wmG7De7#JM-cM)C*;c_4pqDR6j;N zPC1LM7C&L`;&|nh>r%`;iEz%rmUPp}0iXrBveK9gs>^~mF?dlx{5Edy&9|-svfFr(6 zq;4GOv;+T6{E>TEEnb5kdpV~-r%hF4w(7YoWOvCzDK)Lr*K1#hF!8o~)3N*fzjNy; z_pmyl^lz?5A8J5D@-K;EFAPTM_wcJ!aV8dx8;0>-unaJ*2noBEsu+BGRQJAJS082s zY2Q+SxTNX9l}}xpB!QvJA@J?U0f7v;;D01dnEZJ#uBj@vqLs=5FQ$0^9RYLj`gr=R zP?JD0p)Pb(+Pjg^2Oyl-rP$U}u(8Ab^a9X00{mae?mMP}@*Xe{`LZOT- zkmQ?8dBX5=lEsGpPASjL0b7JYkaU<#y9&ky;LpXMs@!M0@pg;$dQ)QbaO^3$sXOQR z9%(eCjrjhLrF5s~oTuZ#xqv!&QGoTV77dDd1AccTZqRf0HnQbW>NdUBOg00&g3=o( zjO^`g#KCTa^Syta?TS14#^x7Np&e~hEE;RejPkulKrg$aF`H5fo9J!QM@WOMYt{$0 zAU2qq_%T{|`q}SotC%2RE2gl8F|vPhA0=$ieTIr5q{^SP4c0Rrp`dSc!`O-_yb@*- zNU(iY8`}2igsOR#lIH5*9zaTT^#)N`LuVjmlE4pvUIQ9wf9aoeY%1gG}mE ziiDCp8{HKuE*_97^wifl- zp*D9e&9}cQ3%a8c0M-(8Z*Cw3j%H(cv8&bm)!j%B_}jO=_eQ8rN?aI*GL)=KMyRWl z?Z}TFdr{VVBSRFNe-QgfhsBo9Bhz!swp0t7o#4~M9baIW#(3u&DTNL(QZ7ZuUjICw z-2qYtwXWYI=l|<5bKe!OW#^R8*QMg$^}y}nTvurq%~ic${E4+Jy}8<5GJw5UHRe|A zcIsuB;4$;Bt0F`l7W>rxt@GiX)M3}c+V3e-SeatAog0iaBa-%@g?vt4E>8LY*C+w8 zP2gJ>xS^CJB1(KKVG8Me`y*}(<4@3#^Tyy~Cag8X+7iey`_uXa_W*GdFVMIK)=_oD z(f6I0mb@O&^(=e*#~kwUpWerp@zE0fWJArdp+|oRuDoG|+|%UOX!`AE`+qX6@N$9E z7oJuot`=Rv7N z^&TH2w!?m5$3nEuJJhKC6_^Vi#W>V{8it^|MwwVaB|Jry`XBwmHa6E5 zs`QL@N~2U&*(p!m;Ej>Ra@Z$)U=>{rvrau^IltmU8aOQEzf`?N+dHHQ7i2SR5C@?l zf^)Y~b|?41DLgG||AXfNtuXOnFcW#oXx+!OaazR`?CO=ny)<4s3`$d2yN7T!hGNoi zEX%m~S+7H-ZoRDW;03P;T;8lhNIui}Pd%o5N&6$n=MQ>lx%o+&< z*?3swk2Sk9YO$uwv$Wja=sKzV&pCPJ#iaqE6Q8lzNj(vAF8=Vjjq1G_U~jGlRkq4Oe><~r7AYp9T6(63r9@`(aG;HZc{AXX@Vs^e}AJa4)(sRM0?MJ@`>#Gc~ zihK?tGP*V=5uY#TnZvW`b6&GZH5f4dWE~R0cN;lrmBv${t<>U-Zp^2DkVAU`6F}x$ z!N~*7tZWwDcjjGG`|6?GT7c zF3n%DDnSPbtdjmuKyqJ9HnzkB*~%3RCcn=)Wm2Q5)#LfrR=-0D}~V* zvV<&=EhJ8qLH4!mTXsq&jAh0)rXkB1W{ffX9#_BX+xPqY0iWx+uj_uE`+eV^*ZX}3 zwrNE7SMBTxi`?F*1jXZA7T=dO>n2oL|tL*(bL%^6C8Ej`m{vfNUsk}VX7SpIUj3v5VLbKJ4FpbwRr_`u`4wCRz*Qg;_$De`%@l zTc{K9e7879oj4b)>4mX~D*Ta^x7m$W!TPy{vdA8j7^JjK2dgH8-O%*TL@1FRG<7Qo zoHri>U#*gzyXCt&iJ*NONi=NmbSauXr@qfJZ5@@fGF3sLsJji`>dLJm#t&Anw9FIB z`}SRdt5-Iu2GU^L&*0nvf)0H~+c>5tl5_&IgAAIup*HB)u`xN&j1zk8k?VA(l`3X? zpUS^9$66oZJC@gmhenX-kOb*_%a4poum;Pmek)Srs^q-uhFF78P-fAq>0o_l@^H# zG$*uG356mr!u&v+m4Dmq9m0&uBiTy6b;9^m8n3x&lXA`uTIe9@&v0nC$=QrW5;oy( zw#i@GiNdz_94U|3e$>m)-T1}}AFVIUi?9Dmp>0ba40BxQn@-NmNX0mvoGp!rP0>h0 zol^3_F6#bLEWME&R&OP*r2Hk}5C@o&Qy#c~G!q_h9!R(T^+B}}6F5H2uWYsonWXIg zhnx9XW1n(}vV=Aq72E!=zx<($KJ)wW)1uX36h+?(Fp$M(qkt z$$uF-AyvZq7jT0uC3PnHfy5bBM~-`;1>P1u`Rzv?+C_Cw#A2%c!4(s&FO8Qk^7(+Y z3Q7n?_uQ4L*(a}id@26s@y8z;`%aOtNMcki#bx2~hc-$&K7{2ZmiG3=&yhQR3IqBN z#+$5b1Y#=x_x1qE-1I~kH^bWz2+wtELE`ruXv2&5z2gVjJjz{O&HjA}v0s7sNZ|4S z8LzG2hX|=#&DUzpjNjn!}VJ`)gWUT4NI zM%4i}P~~al(+9aeGnzpMtoiqKQlcCYt(rZw8{hDYWY6(3x%=m$MIO{#l4Y;dR1X6A zsEUoTa})CueEP5IBt5gZ=|0XU)oMNx4x>h4uB{m6;f>7ZIPk3@kUZ z%RA&7v-l4D8`zn(vQZlWQ)zvck9BYS+(${t1NzKNA^eV6srl%kt(f#?2EA6B=uXJ1 zRp<8cY{`JlaDi0R^#H_j=va4!#YqD5`y6LiW~cLgpxIu4x8Rzn%-88{BwkvWY@!-v zUr;8Sb)hRQH+q5NsqxDPOS0#KpL;SCwu3L*erV%LCb#L|@SfZsIAXlBT2IUf^KEqq z*`?@UTI@zSxE_p^?BjuCyqh@(_69JvFzrW5qLkms9B4(CPBY?Wvlk%3i)~*55o@n> z`FfsF8*01DZS(7xlpfIFtv~CXN5T`9P3x>-+iTz$zx{cKzrm+y5U>#N5P!;c11rVb{66pv{&%z|YrnY2~EYk~@-Z<)Co(-7Sqq!GM=zKzL+5CO*}xiZ@U4Sg&*n^sdw8be8pp%QO!BMPMstXOtzWJE zG~=2bki?qb6t)d-nUP+4ip(`KJeN|5%t7`3sBp4?(jU`N%8=C)^33uYErjmFqA}8j zs8tq00lr#o7#h9HyBh!FNfXp{QISNZAP=Te2?5tN{2ThVz7xJx%+E4XlEw8h4y;37 z&kiW?<0n5gNx8Isuksw4*P{JXI=Q{bgwWQeFsVwtsft>XM->nUrncznj#J+hR&Z0J z!Xr&-(#r0D zlcW=2szZN+E89QJSoT*&ptN3WH+NOcXmE`cnW$ZJ(&oPV0av$bXu>1wt;ziV6Nfh9 z3l6QdzAmAsylJtDV%-U6-$jT;i2RjJs&M_Ce`n1KGZ;yA!o_shAB||V{TR zJWXYs1)!IEUIhP@b(j8^M7-j?&)MK1T>rMK;{CFnfRDg=a!x~b=$AJ-i~M{MQNK|G z!yKKN)2Ep&^v!D5F@Y=kQ}pvV<#v{?#YMO@khiD^^%w&KF5wSn#k7xb(q2@F>08Ge zHNANYM$*YkZ%u}cL~-SOt?!@lLe;sZM?|Bt@)KcdpGBQkTn4(*ROHfw?Sjjqf6L+R z6jm{S!->7>48L;hqm792UAPfrb-=4!`!q@R;&#VWlUsW*^gcN@41rQd@Xbqh_g#Db!&4}KwEpXyAZb!L>+Ft?G52{62kY_ ztQ`jhycIMc4ZT;`KI8OVc62wAp5?0IuOPbTE~?Ga0fpETP}|4A*~Nw68gI&14|~Qs ztN2Ye3ImoWP#6t@_qw-&NI|r3RpJsd>uIv)g>UNf1=FC;j+(C-Nu}bKISw}1GJLm% zI0^_kXixmgK-yNmR2gWFA0WV7pqtWDpwK<}l+qBDAV)~YO^M=MuUfM|w}&0CGAJ{8 z&L1LC6@f+*rn}tK=N=;xiRCL}rO@)Gop{0R_hPmNXq`UZDCAU%%J;~UP~x~gAi?`r ztW-W)R(;vq95^SpZb7#3vedIuw8*&5I?<;mC|83f$t<^LUv;oH=hpvP@K0cAO(UK1 z3_`trrzs!w{H#M17s_gJ>;8jse%z_t`g|3=&KIxm3tK<RS8Fw>!?w{`cqwvwzQLqg>_ zsb9t8x{`7(M$JREhc~9j;?Q~~Sbd1B_z+h6N^GFO0!PVx#hLsa-G)@p%v1pmQJh5MYD@GvGU}+biOeO$rjN8< zUT?=Ku-`?l$*+zRatBME)C=$I0_Ur=BASD0u6M(m?DLMiD@Kb;Bm7xjlV_j>ScA+$ zsuVc}P{;L6fUB=#;k2dg#{AszsFB_66;WC@O&6r$s+i%nz6^192%XJ)N=qXfyDw17 ztq@3}3;_{xn^fU-InSb~AUd2^&_z+3} z+(s|F^G(hCvpmGmMoz1bms42#EGAsV`))ye9nS;PH32!?cfoGaDDFF&_3R>K)=6Xz z_A@h4SEi|ZqhJp&VXpCg+-~@_O9-1JU}MwX`$=?2OmMwobphotUOr_+_IfF^P|%Wl|*{~uoPkYoS= diff --git a/frontend/ui/src/tools.tsx b/frontend/ui/src/tools.tsx index 71292322..945743c0 100644 --- a/frontend/ui/src/tools.tsx +++ b/frontend/ui/src/tools.tsx @@ -126,7 +126,9 @@ export function ModelRuntimeInventory({ export function ToolIcon({ kind }: { kind: string }) { const icon = toolIcons[kind.toLowerCase()]; - const monochrome = ["opencode", "vscode", "cursor"].includes(kind.toLowerCase()); + const monochrome = ["opencode", "vscode", "cursor"].includes( + kind.toLowerCase(), + ); return icon ? ( Date: Thu, 1 Oct 2026 20:02:18 -0400 Subject: [PATCH 11/12] fix: align Pi inventory with the files Pi loads Project MCP files stay in the working directory unless a user-global adapter config opts into ancestorConfigRoots. Skill discovery keeps .pi/skills in the working directory and stops .agents/skills at the git root. Validation rejects Pi combined with Claude Desktop or Grok Build, and rejects empty Pi model IDs before models.json is written. --- crates/agent/src/provider/README.md | 17 +- crates/agent/src/provider/pi/discovery.rs | 402 +++++++++++++++++++++- crates/core/src/config.rs | 55 ++- schema/daemon-config.json | 4 +- schema/daemon-config.md | 4 +- 5 files changed, 462 insertions(+), 20 deletions(-) diff --git a/crates/agent/src/provider/README.md b/crates/agent/src/provider/README.md index 89c1b193..61148aa5 100644 --- a/crates/agent/src/provider/README.md +++ b/crates/agent/src/provider/README.md @@ -10,7 +10,9 @@ file. Pi stores configuration in `~/.pi/agent` (or `PI_CODING_AGENT_DIR`, with `~` expanded to the user's home). Management requires `--user`; system mode -rejects `programs.pi` because Pi does not read `/etc/pi/agent`. Authenticated +rejects `programs.pi` because Pi does not read `/etc/pi/agent`. Pi cannot be +combined with Claude Desktop or Grok Build, because those programs require +system mode. Authenticated gateway configuration also requires a running daemon and rejects `--once`. User mode merges `providers.agentdesktop` into `models.json` and sets the default model in `settings.json`. Model map keys determine catalog IDs, and @@ -25,9 +27,16 @@ preserving user configuration values. `settings.json` requires standard JSON. Discovery recognizes npm's Windows command shims as well as symlinked launchers, verifying the Pi package manifest without executing the launcher. MCP inventory comes from `pi-mcp-adapter` config files -(`~/.pi/agent/mcp.json`, `.pi/mcp.json`, `.mcp.json`, and shared -`~/.config/mcp/mcp.json` / `~/.agents/mcp.json` / `~/.agents/mcp/mcp.json`). -Both `.agents` paths are supported by the [adapter's configuration loader](https://github.com/nicobailon/pi-mcp-adapter/blob/33bdc38d8dd3802f2b51ba1fe37e30ed7ae6a29a/config.ts#L15-L19). +(`~/.pi/agent/mcp.json`, the working directory's `.pi/mcp.json` and +`.mcp.json`, and shared `~/.config/mcp/mcp.json` / `~/.agents/mcp.json` / +`~/.agents/mcp/mcp.json`). Ancestor project files are included only when a +user-global config sets `settings.ancestorConfigRoots` to an existing +directory under the home that contains the working directory. Both `.agents` +paths are supported by the [adapter's configuration loader](https://github.com/nicobailon/pi-mcp-adapter/blob/33bdc38d8dd3802f2b51ba1fe37e30ed7ae6a29a/config.ts#L15-L19). +`PI_MCP_CONFIG_MODE=exclusive` inventories only the Pi agent `mcp.json`. +Skill inventory reads `~/.pi/agent/skills`, `~/.agents/skills`, the working +directory's `.pi/skills`, and `.agents/skills` from the working directory +through the git repository root. Host-specific Cursor/Claude files are not scanned until the adapter imports them into a Pi-owned file. diff --git a/crates/agent/src/provider/pi/discovery.rs b/crates/agent/src/provider/pi/discovery.rs index 92a10c98..cf0ae54f 100644 --- a/crates/agent/src/provider/pi/discovery.rs +++ b/crates/agent/src/provider/pi/discovery.rs @@ -1,10 +1,12 @@ use std::{ collections::BTreeSet, + env, fs, io::Read, path::{Path, PathBuf}, }; use agentdesktop_core::model::{Agent, McpServer}; +use serde_json::Value; use crate::provider::{metadata, vscode::discovery as vscode}; @@ -157,34 +159,268 @@ fn discover_mcp_servers() -> Vec { /// Files `pi-mcp-adapter` loads as normal MCP config. /// +/// Global files are always included. Project files are only the working +/// directory's `.mcp.json` and `.pi/mcp.json`, plus ancestors when a +/// user-global config opts in through `settings.ancestorConfigRoots`. +/// `PI_MCP_CONFIG_MODE=exclusive` keeps only the Pi agent `mcp.json`. /// Host-specific Cursor/Claude/Codex files are not included: the adapter only /// imports those after `/mcp setup` writes them into a Pi-owned file. fn mcp_config_paths() -> Vec { + let current = env::current_dir().ok(); + mcp_config_paths_with( + &metadata::user_home_dirs(), + current.as_deref(), + pi_mcp_exclusive_mode(), + user_pi_agent_dir, + ) +} + +fn mcp_config_paths_with( + homes: &[PathBuf], + cwd: Option<&Path>, + exclusive: bool, + agent_dir: impl Fn(&Path) -> PathBuf, +) -> Vec { let mut paths = BTreeSet::new(); - for home in metadata::user_home_dirs() { - let agent_dir = user_pi_agent_dir(&home); - paths.insert(agent_dir.join("mcp.json")); + for home in homes { + let agent_mcp = agent_dir(home).join("mcp.json"); + if exclusive { + paths.insert(agent_mcp); + continue; + } paths.insert(home.join(".config/mcp/mcp.json")); paths.insert(home.join(".agents/mcp.json")); paths.insert(home.join(".agents/mcp/mcp.json")); + paths.insert(agent_mcp); + } + if exclusive { + return paths.into_iter().collect(); + } + if let Some(cwd) = cwd { + let cwd = canonicalize_dir(cwd).unwrap_or_else(|| cwd.to_path_buf()); + paths.insert(cwd.join(".mcp.json")); + paths.insert(cwd.join(".pi/mcp.json")); + for home in homes { + let Some(root) = configured_ancestor_root(home, &cwd, &agent_dir) else { + continue; + }; + for directory in ancestor_project_dirs(&cwd, &root) { + paths.insert(directory.join(".mcp.json")); + paths.insert(directory.join(".pi/mcp.json")); + } + } } - paths.extend(metadata::current_dir_ancestors(Path::new(".mcp.json"))); - paths.extend(metadata::current_dir_ancestors(Path::new(".pi/mcp.json"))); paths.into_iter().collect() } +fn pi_mcp_exclusive_mode() -> bool { + env::var("PI_MCP_CONFIG_MODE").is_ok_and(|value| value.trim().eq_ignore_ascii_case("exclusive")) +} + +/// Last user-global `settings.ancestorConfigRoots` that pi-mcp-adapter would honor. +/// +/// Later global files replace earlier ones. An empty or invalid value disables +/// ancestor loading. Project files cannot enable it. +fn configured_ancestor_root( + home: &Path, + cwd: &Path, + agent_dir: &impl Fn(&Path) -> PathBuf, +) -> Option { + let mut configured = None; + for path in global_mcp_sources(home, agent_dir) { + match read_ancestor_setting(&path) { + AncestorSetting::Unset => {} + AncestorSetting::Clear => configured = Some(Vec::new()), + AncestorSetting::Roots(roots) => configured = Some(roots), + } + } + let roots = configured.filter(|roots| !roots.is_empty())?; + let home_id = canonicalize_dir(home)?; + let mut valid = Vec::new(); + for entry in roots { + let Some(expanded) = expand_ancestor_root(&entry, home) else { + continue; + }; + let Some(root) = canonicalize_dir(&expanded) else { + continue; + }; + // The deepest existing directory under the user's home that contains + // the working directory wins. A root equal to the working directory + // adds no parent files. + if root.starts_with(&home_id) && root_contains_cwd(&root, cwd) { + valid.push(root); + } + } + valid.sort_by_key(|path| std::cmp::Reverse(path.as_os_str().len())); + valid.into_iter().next() +} + +fn global_mcp_sources(home: &Path, agent_dir: &impl Fn(&Path) -> PathBuf) -> Vec { + let user = agent_dir(home).join("mcp.json"); + let generic = home.join(".config/mcp/mcp.json"); + let mut sources = Vec::new(); + if generic != user { + sources.push(generic.clone()); + } + for relative in [ + Path::new(".agents/mcp.json"), + Path::new(".agents/mcp/mcp.json"), + ] { + let path = home.join(relative); + if path != user && path != generic { + sources.push(path); + } + } + sources.push(user); + sources +} + +enum AncestorSetting { + Unset, + Clear, + Roots(Vec), +} + +fn read_ancestor_setting(path: &Path) -> AncestorSetting { + let Ok(bytes) = fs::read(path) else { + return AncestorSetting::Unset; + }; + let Ok(text) = str::from_utf8(&bytes) else { + return AncestorSetting::Unset; + }; + let Ok(value) = json5::from_str::(text.trim_start_matches('\u{feff}')) else { + return AncestorSetting::Unset; + }; + let Some(roots) = value.pointer("/settings/ancestorConfigRoots") else { + return AncestorSetting::Unset; + }; + let Some(entries) = roots.as_array() else { + return AncestorSetting::Clear; + }; + if entries.is_empty() { + return AncestorSetting::Clear; + } + AncestorSetting::Roots( + entries + .iter() + .filter_map(Value::as_str) + .map(str::to_owned) + .collect(), + ) +} + +fn expand_ancestor_root(entry: &str, home: &Path) -> Option { + entry + .strip_prefix("~/") + .map(|rest| home.join(rest)) + .or_else(|| { + let path = PathBuf::from(entry); + path.is_absolute().then_some(path) + }) +} + +fn ancestor_project_dirs(cwd: &Path, root: &Path) -> Vec { + let Some(cwd) = canonicalize_dir(cwd) else { + return Vec::new(); + }; + let Some(root) = canonicalize_dir(root) else { + return Vec::new(); + }; + let mut dirs = Vec::new(); + let Some(mut current) = cwd.parent().map(Path::to_path_buf) else { + return dirs; + }; + while current.starts_with(&root) { + dirs.insert(0, current.clone()); + if current == root { + break; + } + let Some(parent) = current.parent().map(Path::to_path_buf) else { + break; + }; + if parent == current { + break; + } + current = parent; + } + dirs +} + +fn root_contains_cwd(root: &Path, cwd: &Path) -> bool { + let Some(cwd) = canonicalize_dir(cwd) else { + return false; + }; + let Some(root) = canonicalize_dir(root) else { + return false; + }; + cwd.starts_with(root) +} + +fn canonicalize_dir(path: &Path) -> Option { + let canonical = fs::canonicalize(path).ok()?; + canonical.is_dir().then_some(canonical) +} + +/// Skill directories Pi loads. +/// +/// User skills come from the Pi agent directory and `~/.agents/skills`. +/// Project `.pi/skills` is only the working directory. Project `.agents/skills` +/// walks from the working directory through the git repository root, or the +/// filesystem root when the working directory is not in a repository. fn skill_roots() -> Vec { + let current = env::current_dir().ok(); + skill_roots_in(&metadata::user_home_dirs(), current.as_deref()) +} + +fn skill_roots_in(homes: &[PathBuf], cwd: Option<&Path>) -> Vec { let mut roots = BTreeSet::new(); - for home in metadata::user_home_dirs() { - let agent_dir = user_pi_agent_dir(&home); - roots.insert(agent_dir.join("skills")); + for home in homes { + roots.insert(user_pi_agent_dir(home).join("skills")); roots.insert(home.join(".agents/skills")); } - roots.extend(metadata::current_dir_ancestors(Path::new(".pi/skills"))); - roots.extend(metadata::current_dir_ancestors(Path::new(".agents/skills"))); + if let Some(cwd) = cwd { + let cwd = canonicalize_dir(cwd).unwrap_or_else(|| cwd.to_path_buf()); + roots.insert(cwd.join(".pi/skills")); + roots.extend(agents_skill_dirs(&cwd)); + } roots.into_iter().collect() } +fn agents_skill_dirs(start: &Path) -> Vec { + let start = canonicalize_dir(start).unwrap_or_else(|| start.to_path_buf()); + let git_root = git_repo_root(&start); + let mut dir = start; + let mut dirs = Vec::new(); + loop { + dirs.push(dir.join(".agents/skills")); + if git_root.as_ref().is_some_and(|root| &dir == root) { + break; + } + let Some(parent) = dir.parent().map(Path::to_path_buf) else { + break; + }; + if parent == dir { + break; + } + dir = parent; + } + dirs +} + +fn git_repo_root(start: &Path) -> Option { + let mut dir = start.to_path_buf(); + loop { + if dir.join(".git").exists() { + return Some(dir); + } + let parent = dir.parent()?.to_path_buf(); + if parent == dir { + return None; + } + dir = parent; + } +} + #[cfg(test)] mod tests { use std::{ @@ -194,7 +430,10 @@ mod tests { use serde_json::json; - use super::{is_pi, mcp_config_paths, package_version, skill_roots}; + use super::{ + is_pi, mcp_config_paths, mcp_config_paths_with, package_version, skill_roots, + skill_roots_in, + }; use crate::provider::pi::user_pi_agent_dir; use crate::provider::vscode::discovery::mcp_servers_from_value; @@ -502,6 +741,147 @@ fi ); } + #[test] + fn project_mcp_stays_in_the_working_directory_unless_opted_in() { + let root = tempfile::tempdir().unwrap(); + let home = root.path().join("home"); + let repo = home.join("repo"); + let project = repo.join("child"); + let outside = home.join("outside"); + fs::create_dir_all(project.join(".pi")).unwrap(); + fs::create_dir_all(&outside).unwrap(); + fs::create_dir_all(repo.join(".pi")).unwrap(); + let project_id = fs::canonicalize(&project).unwrap(); + let repo_id = fs::canonicalize(&repo).unwrap(); + let outside_id = fs::canonicalize(&outside).unwrap(); + let home_id = fs::canonicalize(&home).unwrap(); + let agent = |home: &Path| home.join(".pi/agent"); + let paths = + mcp_config_paths_with(std::slice::from_ref(&home), Some(&project), false, agent); + assert!(paths.contains(&project_id.join(".mcp.json"))); + assert!(paths.contains(&project_id.join(".pi/mcp.json"))); + assert!(!paths.contains(&repo_id.join(".mcp.json"))); + assert!(!paths.contains(&repo_id.join(".pi/mcp.json"))); + assert!(!paths.contains(&outside_id.join(".mcp.json"))); + assert!(paths.contains(&home.join(".config/mcp/mcp.json"))); + assert!(paths.contains(&home.join(".agents/mcp.json"))); + assert!(paths.contains(&home.join(".agents/mcp/mcp.json"))); + assert!(paths.contains(&agent(&home).join("mcp.json"))); + + // A project file cannot widen discovery. + fs::write( + project.join(".mcp.json"), + r#"{"settings":{"ancestorConfigRoots":["~/"]}}"#, + ) + .unwrap(); + let paths = + mcp_config_paths_with(std::slice::from_ref(&home), Some(&project), false, agent); + assert!(!paths.contains(&repo_id.join(".mcp.json"))); + + fs::create_dir_all(home.join(".config/mcp")).unwrap(); + fs::write( + home.join(".config/mcp/mcp.json"), + r#"{"settings":{"ancestorConfigRoots":["~/repo"]}}"#, + ) + .unwrap(); + let paths = + mcp_config_paths_with(std::slice::from_ref(&home), Some(&project), false, agent); + assert!(paths.contains(&repo_id.join(".mcp.json"))); + assert!(paths.contains(&repo_id.join(".pi/mcp.json"))); + assert!(!paths.contains(&outside_id.join(".mcp.json"))); + assert!(!paths.contains(&home_id.join(".mcp.json"))); + + // The Pi agent file is the last global source, so it can turn the opt-in off. + fs::create_dir_all(home.join(".pi/agent")).unwrap(); + fs::write( + agent(&home).join("mcp.json"), + r#"{"settings":{"ancestorConfigRoots":[]}}"#, + ) + .unwrap(); + let paths = + mcp_config_paths_with(std::slice::from_ref(&home), Some(&project), false, agent); + assert!(!paths.contains(&repo_id.join(".mcp.json"))); + + let paths = mcp_config_paths_with(std::slice::from_ref(&home), Some(&project), true, agent); + assert_eq!(paths, vec![agent(&home).join("mcp.json")]); + } + + #[test] + fn ancestor_mcp_ignores_roots_outside_the_home_or_working_directory() { + let root = tempfile::tempdir().unwrap(); + let home = root.path().join("home"); + let project = home.join("work/child"); + let elsewhere = home.join("elsewhere"); + fs::create_dir_all(&project).unwrap(); + fs::create_dir_all(&elsewhere).unwrap(); + fs::create_dir_all(home.join(".pi/agent")).unwrap(); + fs::write( + home.join(".pi/agent/mcp.json"), + format!( + r#"{{"settings":{{"ancestorConfigRoots":["{}", "~/elsewhere"]}}}}"#, + root.path().display() + ), + ) + .unwrap(); + let paths = + mcp_config_paths_with(std::slice::from_ref(&home), Some(&project), false, |home| { + home.join(".pi/agent") + }); + let root_id = fs::canonicalize(root.path()).unwrap(); + let elsewhere_id = fs::canonicalize(&elsewhere).unwrap(); + let work_id = fs::canonicalize(home.join("work")).unwrap(); + assert!(!paths.contains(&root_id.join(".mcp.json"))); + assert!(!paths.contains(&elsewhere_id.join(".mcp.json"))); + assert!(!paths.contains(&work_id.join(".mcp.json"))); + } + + #[test] + fn skill_roots_follow_pi_project_scopes() { + let root = tempfile::tempdir().unwrap(); + let home = root.path().join("home"); + let repo = root.path().join("repo"); + let project = repo.join("child"); + fs::create_dir_all(&home).unwrap(); + fs::create_dir_all(&project).unwrap(); + fs::write(repo.join(".git"), "gitdir: /unused").unwrap(); + + let project = fs::canonicalize(&project).unwrap(); + let repo = fs::canonicalize(&repo).unwrap(); + let paths = skill_roots_in(std::slice::from_ref(&home), Some(&project)); + assert!(paths.contains(&project.join(".pi/skills"))); + assert!(paths.contains(&project.join(".agents/skills"))); + assert!(paths.contains(&repo.join(".agents/skills"))); + assert!(!paths.contains(&repo.join(".pi/skills"))); + assert!( + !paths.contains( + &fs::canonicalize(root.path()) + .unwrap() + .join(".agents/skills") + ) + ); + assert!(!paths.contains(&fs::canonicalize(root.path()).unwrap().join(".pi/skills"))); + assert!(paths.contains(&home.join(".agents/skills"))); + assert!(paths.contains(&user_pi_agent_dir(&home).join("skills"))); + + let loose = root.path().join("loose/child"); + fs::create_dir_all(&loose).unwrap(); + let loose = fs::canonicalize(&loose).unwrap(); + let paths = skill_roots_in(&[home], Some(&loose)); + assert!(paths.contains(&loose.join(".pi/skills"))); + assert!(!paths.contains(&loose.parent().unwrap().join(".pi/skills"))); + assert!(paths.contains(&loose.parent().unwrap().join(".agents/skills"))); + assert!( + paths.contains( + &loose + .parent() + .unwrap() + .parent() + .unwrap() + .join(".agents/skills") + ) + ); + } + #[test] fn scans_pi_owned_and_shared_mcp_files() { let paths = mcp_config_paths(); diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs index ab7d350f..84c8787c 100644 --- a/crates/core/src/config.rs +++ b/crates/core/src/config.rs @@ -498,6 +498,9 @@ pub struct ProgramsConfig { #[serde(default, skip_serializing_if = "Option::is_none")] pub grok: Option, /// Pi coding-agent harness managed configuration. + /// + /// Requires `--user`. Cannot be combined with Claude Desktop or Grok Build, + /// which require system mode. #[serde(default, skip_serializing_if = "Option::is_none")] pub pi: Option, } @@ -659,7 +662,8 @@ pub struct PiConfig { /// Each value is an arbitrary Pi `models.json` model object. The map key /// sets its `id`. Generated gateway `baseUrl` and `apiKey` values take /// precedence, including per-model URLs. When this map is non-empty, - /// `model` must name one of its keys. + /// `model` must name one of its keys. Every key must contain a + /// non-whitespace character. #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub models: BTreeMap, /// Pi API dialect for the managed provider. Defaults to `anthropic-messages`. @@ -958,6 +962,12 @@ fn validate_daemon( anyhow::bail!("Grok Build model {model} is not declared in models"); } } + if programs.pi.is_some() && programs.claude_desktop.is_some() { + anyhow::bail!("programs.pi requires --user, which cannot apply programs.claudeDesktop"); + } + if programs.pi.is_some() && programs.grok.is_some() { + anyhow::bail!("programs.pi requires --user, which cannot apply programs.grok"); + } if let Some(pi) = &programs.pi && llm_gateway.is_some() && pi.use_llm_gateway @@ -967,6 +977,9 @@ fn validate_daemon( .as_deref() .filter(|model| !model.trim().is_empty()) .context("Pi requires model when llmGateway is configured")?; + if pi.models.keys().any(|model| model.trim().is_empty()) { + anyhow::bail!("Pi model IDs cannot be empty"); + } if !pi.models.is_empty() && !pi.models.contains_key(model) { anyhow::bail!("Pi model {model} is not declared in models"); } @@ -1451,4 +1464,44 @@ programs: .contains("Pi model missing is not declared in models") ); } + + #[test] + fn pi_rejects_empty_model_ids() { + let error = parse_daemon( + r#" +llmGateway: + url: https://gateway.example.com +programs: + pi: + model: claude-sonnet-4-5 + models: + claude-sonnet-4-5: {} + "": {} +"#, + ) + .expect_err("empty Pi model id should fail"); + + assert!(error.to_string().contains("Pi model IDs cannot be empty")); + } + + #[test] + fn pi_rejects_system_only_program_combinations() { + for (name, message) in [ + ( + "claudeDesktop", + "programs.pi requires --user, which cannot apply programs.claudeDesktop", + ), + ( + "grok", + "programs.pi requires --user, which cannot apply programs.grok", + ), + ] { + let error = parse_daemon(&format!("programs:\n pi: {{}}\n {name}: {{}}\n")) + .expect_err("Pi combined with a system-only program should fail"); + assert!( + error.to_string().contains(message), + "missing {message} in {error}" + ); + } + } } diff --git a/schema/daemon-config.json b/schema/daemon-config.json index 3756ca34..68872d89 100644 --- a/schema/daemon-config.json +++ b/schema/daemon-config.json @@ -441,7 +441,7 @@ ] }, "models": { - "description": "Extra Pi model objects, keyed by model ID.\n\nEach value is an arbitrary Pi `models.json` model object. The map key\nsets its `id`. Generated gateway `baseUrl` and `apiKey` values take\nprecedence, including per-model URLs. When this map is non-empty,\n`model` must name one of its keys.", + "description": "Extra Pi model objects, keyed by model ID.\n\nEach value is an arbitrary Pi `models.json` model object. The map key\nsets its `id`. Generated gateway `baseUrl` and `apiKey` values take\nprecedence, including per-model URLs. When this map is non-empty,\n`model` must name one of its keys. Every key must contain a\nnon-whitespace character.", "type": "object", "additionalProperties": true }, @@ -543,7 +543,7 @@ ] }, "pi": { - "description": "Pi coding-agent harness managed configuration.", + "description": "Pi coding-agent harness managed configuration.\n\nRequires `--user`. Cannot be combined with Claude Desktop or Grok Build,\nwhich require system mode.", "anyOf": [ { "$ref": "#/$defs/PiConfig" diff --git a/schema/daemon-config.md b/schema/daemon-config.md index 20a699c4..c23231c8 100644 --- a/schema/daemon-config.md +++ b/schema/daemon-config.md @@ -59,11 +59,11 @@ |`programs.openCode.model`|string|Model ID selected from `models` when using the LLM gateway.

This is required when a top-level `llmGateway` is configured.| |`programs.openCode.models`|object|Models exposed by the managed LLM gateway provider, keyed by model ID.

Each value is an arbitrary OpenCode model configuration object. At least
one model is required when a top-level `llmGateway` is configured.| |`programs.openCode.useLlmGateway`|boolean|Whether this program uses the top-level LLM gateway.| -|`programs.pi`|object|Pi coding-agent harness managed configuration.| +|`programs.pi`|object|Pi coding-agent harness managed configuration.

Requires `--user`. Cannot be combined with Claude Desktop or Grok Build,
which require system mode.| |`programs.pi.api`|string|Pi API dialect for the managed provider. Defaults to `anthropic-messages`.| |`programs.pi.managedConfig`|object|Arbitrary values merged into Pi's `models.json`.| |`programs.pi.model`|string|Model ID selected when pointing Pi at the LLM gateway.

This is required when a top-level `llmGateway` is configured. If `models`
is empty, agentdesktop creates a catalog entry with this ID.| -|`programs.pi.models`|object|Extra Pi model objects, keyed by model ID.

Each value is an arbitrary Pi `models.json` model object. The map key
sets its `id`. Generated gateway `baseUrl` and `apiKey` values take
precedence, including per-model URLs. When this map is non-empty,
`model` must name one of its keys.| +|`programs.pi.models`|object|Extra Pi model objects, keyed by model ID.

Each value is an arbitrary Pi `models.json` model object. The map key
sets its `id`. Generated gateway `baseUrl` and `apiKey` values take
precedence, including per-model URLs. When this map is non-empty,
`model` must name one of its keys. Every key must contain a
non-whitespace character.| |`programs.pi.useLlmGateway`|boolean|Whether this program uses the top-level LLM gateway.| |`sandbox`|object|Local execution sandbox required for managed developer tools.| |`sandbox.filesystem`|object|Filesystem access available to sandboxed commands.| From 2d797f57be7e9e86222a9bf50a4a5f30936ca45b Mon Sep 17 00:00:00 2001 From: Sebastian Maniak Date: Sun, 18 Oct 2026 17:24:14 -0400 Subject: [PATCH 12/12] Skip programs unsupported by the daemon mode --- README.md | 10 +- crates/agent/src/daemon.rs | 125 +++++--- crates/agent/src/provider/README.md | 19 +- .../agent/src/provider/claude_desktop/mod.rs | 12 +- crates/agent/src/provider/copilot/mod.rs | 23 +- crates/agent/src/provider/copilot/tests.rs | 19 +- crates/agent/src/provider/grok/mod.rs | 12 +- crates/agent/src/provider/mod.rs | 31 +- crates/agent/src/provider/pi/mod.rs | 25 +- crates/agent/src/provider/vscode/mod.rs | 16 +- crates/agent/src/provider/vscode/tests.rs | 19 +- crates/agent/src/reconcile/mod.rs | 294 +++++++++--------- crates/agent/src/reconcile/plan.rs | 20 +- crates/core/src/config.rs | 49 +-- examples/claude/README.md | 5 +- examples/claude/claude-code.yaml | 2 +- examples/standalone/README.md | 3 +- examples/standalone/config.yaml | 2 +- schema/daemon-config.json | 14 +- schema/daemon-config.md | 4 +- 20 files changed, 408 insertions(+), 296 deletions(-) diff --git a/README.md b/README.md index 2ca42437..4fb224fa 100644 --- a/README.md +++ b/README.md @@ -683,7 +683,7 @@ device-wide state. The device page in the controller lists them under | `unchanged` | The program is configured and nothing needed to change. | | `removed` | The program is no longer configured and its managed content was removed. | | `conflict` | A managed file holds configuration the daemon will not overwrite; the detail names the file. | -| `inactive` | The program uses the LLM gateway but the local LLM proxy is not running (see `llmProxy.error` in daemon-info): its entries were left pointing at the loopback port (`whenProxyUnavailable: failClosed`, the default; the detail says when no entries were in place, so the tool keeps its own settings) or removed (`failOpen`). | +| `inactive` | The daemon's mode cannot manage the program, so its files are untouched and the detail names the required mode. Also used when the program uses the LLM gateway but the local LLM proxy is not running (see `llmProxy.error` in daemon-info): its entries were left pointing at the loopback port (`whenProxyUnavailable: failClosed`, the default; the detail says when no entries were in place, so the tool keeps its own settings) or removed (`failOpen`). | | `blocked` | The program had changes, but none were written because another program conflicted or failed; the detail names that program. | | `failed` | Planning or writing this program failed; the detail carries the error. | @@ -830,7 +830,7 @@ runs this setup. | Tool | Discovery | Managed configuration | MCP and skills inventory | Sandbox policy | | --- | --- | --- | --- | --- | | Claude Code | Yes | Yes | MCP and skills | Yes | -| Claude Desktop | Yes | Yes | MCP | — | +| Claude Desktop | Yes | System mode | MCP | — | | Codex | Yes | Yes | MCP and skills | Yes | | Cursor | Yes | — | MCP and skills | — | | OpenCode | Yes | Yes | MCP | — | @@ -839,6 +839,12 @@ runs this setup. | GitHub Copilot CLI | Yes | User mode | — | — | | Pi | Yes | User mode | MCP and skills | — | +A fleet policy can include programs for both user and system mode. Each daemon +skips programs its mode cannot manage and reports them as **inactive** with a +reason; supported programs continue applying. Skipped programs' files are left +untouched, including when the program is removed from the policy. Cleanup requires +a daemon running in the program's supported mode. + > **Don't see your tool?** We're actively expanding this list and would love > your help. [Open an integration request](https://github.com/agentdesktop-dev/agentdesktop/issues/new) > to tell us what you use, or contribute discovery and configuration support diff --git a/crates/agent/src/daemon.rs b/crates/agent/src/daemon.rs index d2ca6836..ff9dc961 100644 --- a/crates/agent/src/daemon.rs +++ b/crates/agent/src/daemon.rs @@ -469,7 +469,7 @@ where .dry_run(&config) .context("preview daemon configuration")?; } else { - validate_one_shot(&config)?; + validate_one_shot(&config, args.user)?; let (report, applied) = reconciler.apply_with_report(&config); report.log(); applied.context("apply daemon configuration")?; @@ -492,6 +492,7 @@ where }; start_gateway_authentication( &config, + args.user, args.state_dir.clone(), args.oidc_callback_listen, proxy_listener.is_some(), @@ -940,8 +941,26 @@ async fn refresh_inventory_with( } } +/// Only programs managed in this mode can request subscription sign-in. +fn uses_subscription(config: &agentdesktop_core::config::DaemonConfig, user: bool) -> bool { + use agentdesktop_core::config::ProgramAuthentication; + + config + .programs + .claude_code + .as_ref() + .is_some_and(|program| program.auth == Some(ProgramAuthentication::Subscription)) + || (!user + && config + .programs + .claude_desktop + .as_ref() + .is_some_and(|program| program.auth == Some(ProgramAuthentication::Subscription))) +} + fn start_gateway_authentication( config: &agentdesktop_core::config::DaemonConfig, + user: bool, state_dir: PathBuf, callback_listen: Option, proxy_enabled: bool, @@ -951,15 +970,7 @@ fn start_gateway_authentication( }; let authentication = gateway.authentication.clone(); let github = gateway.github_oauth.clone().filter(|_| proxy_enabled); - let subscription = config.programs.claude_code.as_ref().is_some_and(|program| { - program.auth == Some(agentdesktop_core::config::ProgramAuthentication::Subscription) - }) || config - .programs - .claude_desktop - .as_ref() - .is_some_and(|program| { - program.auth == Some(agentdesktop_core::config::ProgramAuthentication::Subscription) - }); + let subscription = uses_subscription(config, user); if authentication.is_none() && !subscription { return; } @@ -1058,7 +1069,10 @@ fn validate_dry_run(config: &agentdesktop_core::config::DaemonConfig) -> anyhow: Ok(()) } -fn validate_one_shot(config: &agentdesktop_core::config::DaemonConfig) -> anyhow::Result<()> { +fn validate_one_shot( + config: &agentdesktop_core::config::DaemonConfig, + user: bool, +) -> anyhow::Result<()> { if config.controller.is_some() { bail!( "--once cannot use a controller because controller synchronization requires the daemon to remain running" @@ -1067,12 +1081,12 @@ fn validate_one_shot(config: &agentdesktop_core::config::DaemonConfig) -> anyhow if !config.telemetry.events.is_empty() { bail!("--once cannot collect telemetry because hooks require the daemon to remain running"); } - if config.programs.copilot.is_some() { + if user && config.programs.copilot.is_some() { bail!( "--once cannot manage the GitHub Copilot CLI providers file because its local proxy requires the daemon to remain running" ); } - if config.programs.vscode.is_some() { + if user && config.programs.vscode.is_some() { bail!( "--once cannot manage the VS Code chatLanguageModels.json and settings.json files because their local proxy requires the daemon to remain running" ); @@ -1087,11 +1101,12 @@ fn validate_one_shot(config: &agentdesktop_core::config::DaemonConfig) -> anyhow .claude_code .as_ref() .is_some_and(|program| program.use_llm_gateway), - config - .programs - .claude_desktop - .as_ref() - .is_some_and(|program| program.use_llm_gateway), + !user + && config + .programs + .claude_desktop + .as_ref() + .is_some_and(|program| program.use_llm_gateway), config .programs .codex @@ -1102,22 +1117,13 @@ fn validate_one_shot(config: &agentdesktop_core::config::DaemonConfig) -> anyhow .open_code .as_ref() .is_some_and(|program| program.use_llm_gateway), - config - .programs - .grok - .as_ref() - .is_some_and(|program| program.use_llm_gateway), - config - .programs - .copilot - .as_ref() - .is_some_and(|program| program.use_llm_gateway), - config - .programs - .vscode - .as_ref() - .is_some_and(|program| program.use_llm_gateway), - config + !user + && config + .programs + .grok + .as_ref() + .is_some_and(|program| program.use_llm_gateway), + user && config .programs .pi .as_ref() @@ -2024,6 +2030,20 @@ programs: ); } + #[test] + fn skipped_desktop_does_not_request_subscription_sign_in() { + let mut config = parse_daemon( + "llmGateway:\n url: https://gateway.example.com\n authentication:\n type: oidc\n issuer: https://login.example.com\n clientId: agentdesktop\nprograms:\n pi:\n model: selected\n claudeDesktop:\n auth: subscription\n" + ).unwrap(); + assert!(!super::uses_subscription(&config, true)); + assert!(super::uses_subscription(&config, false)); + + config.programs.claude_code = + Some(serde_json::from_value(serde_json::json!({ "auth": "subscription" })).unwrap()); + assert!(super::uses_subscription(&config, true)); + assert!(super::uses_subscription(&config, false)); + } + #[test] fn one_shot_rejects_authenticated_pi_gateway() { for authentication in [ @@ -2033,8 +2053,8 @@ programs: let config = parse_daemon(&format!( "llmGateway:\n url: https://gateway.example.com\n authentication:\n{authentication}programs:\n pi:\n model: selected\n" )).unwrap(); - let error = - validate_one_shot(&config).expect_err("Pi credentials require a running daemon"); + let error = validate_one_shot(&config, true) + .expect_err("Pi credentials require a running daemon"); assert!(error.to_string().contains("credential helpers")); } } @@ -2046,7 +2066,28 @@ programs: "llmGateway:\n url: https://gateway.example.com\nprograms:\n pi:\n model: selected\n", "llmGateway:\n url: https://gateway.example.com\n authentication:\n type: oidc\n issuer: https://login.example.com\n clientId: agentdesktop\nprograms:\n pi:\n useLlmGateway: false\n", ] { - validate_one_shot(&parse_daemon(yaml).unwrap()).unwrap(); + validate_one_shot(&parse_daemon(yaml).unwrap(), true).unwrap(); + } + } + + #[test] + fn one_shot_ignores_runtime_requirements_of_skipped_programs() { + for (user, programs) in [ + ( + false, + " pi:\n model: selected\n copilot:\n models:\n selected: {}\n vscode:\n models:\n selected: {}\n", + ), + ( + true, + " claudeDesktop:\n model: selected\n grok:\n model: selected\n", + ), + ] { + let config = parse_daemon(&format!( + "llmGateway:\n url: https://gateway.example.com\n authentication:\n type: oidc\n issuer: https://login.example.com\n clientId: agentdesktop\nprograms:\n claudeCode:\n useLlmGateway: false\n{programs}" + )).unwrap(); + validate_one_shot(&config, user).expect("skipped programs need no runtime services"); + validate_one_shot(&config, !user) + .expect_err("supported programs still require runtime services"); } } @@ -2061,7 +2102,7 @@ programs: "#, ) .unwrap(); - validate_one_shot(&static_config).expect("static settings work in one-shot mode"); + validate_one_shot(&static_config, true).expect("static settings work in one-shot mode"); let oidc = parse_daemon( r#" @@ -2077,7 +2118,7 @@ programs: ) .unwrap(); assert!( - validate_one_shot(&oidc) + validate_one_shot(&oidc, true) .unwrap_err() .to_string() .contains("credential helpers") @@ -2091,7 +2132,7 @@ telemetry: ) .unwrap(); assert!( - validate_one_shot(&telemetry) + validate_one_shot(&telemetry, true) .unwrap_err() .to_string() .contains("telemetry") @@ -2112,7 +2153,7 @@ programs: ) .unwrap(); assert!( - validate_one_shot(&copilot) + validate_one_shot(&copilot, true) .unwrap_err() .to_string() .contains("Copilot") @@ -2133,7 +2174,7 @@ programs: ) .unwrap(); assert!( - validate_one_shot(&vscode) + validate_one_shot(&vscode, true) .unwrap_err() .to_string() .contains("VS Code") diff --git a/crates/agent/src/provider/README.md b/crates/agent/src/provider/README.md index 186a6057..32c573ac 100644 --- a/crates/agent/src/provider/README.md +++ b/crates/agent/src/provider/README.md @@ -2,6 +2,14 @@ Features currently implemented by agentdesktop. +A fleet policy can include programs for both daemon modes. Programs the current +mode cannot manage are skipped and reported as **inactive**, with a reason in +configuration status and dry-run output. Supported programs continue applying. +Skipped programs' files and ownership metadata are left untouched, even when +those programs are removed from the policy. Cleanup runs in the mode that manages +the program. Claude Desktop and Grok Build require system mode; Pi, Copilot CLI, +and VS Code require user mode. + VS Code managed configuration (Copilot Chat custom models in the user's `chatLanguageModels.json`, or the Copilot Chat API override in the user's `settings.json` under `copilotChat: githubModels`) and GitHub Copilot CLI @@ -10,16 +18,15 @@ these files point at the daemon's loopback LLM proxy. The Copilot CLI's MCP and `~/.copilot` is read by the VS Code adapter. Grok Build managed configuration requires system mode. Linux and macOS use `/etc/grok/managed_config.toml`; Windows uses the system drive's -`\etc\grok\managed_config.toml`. `--user` rejects `programs.grok` because +`\etc\grok\managed_config.toml`. User daemons skip `programs.grok` because Grok's own configuration sync can delete or replace the user-level managed file. Pi stores configuration in `~/.pi/agent` (or `PI_CODING_AGENT_DIR`, with `~` -expanded to the user's home). Management requires `--user`; system mode -rejects `programs.pi` because Pi does not read `/etc/pi/agent`. Pi cannot be -combined with Claude Desktop or Grok Build, because those programs require -system mode. Authenticated -gateway configuration also requires a running daemon and rejects `--once`. +expanded to the user's home). Management requires `--user`; system daemons +skip `programs.pi` because Pi does not read `/etc/pi/agent`. In user mode, +authenticated gateway configuration also requires a running daemon and +rejects `--once`. Pi executes its credential helper on every request. The daemon caches controller-issued credentials for up to 60 seconds per allowed client, gateway, audience, and enrolled device, with a 30-second expiry margin and diff --git a/crates/agent/src/provider/claude_desktop/mod.rs b/crates/agent/src/provider/claude_desktop/mod.rs index e6a09e58..1b0bd227 100644 --- a/crates/agent/src/provider/claude_desktop/mod.rs +++ b/crates/agent/src/provider/claude_desktop/mod.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use agentdesktop_core::config::DaemonConfig; -use agentdesktop_core::model::Discovery; +use agentdesktop_core::model::{DaemonScope, Discovery}; use super::{Provider, ReconcileContext}; use crate::reconcile::ReconcilePlan; @@ -45,10 +45,12 @@ impl Provider for ClaudeDesktop { } fn plan(&self, ctx: &ReconcileContext, config: &DaemonConfig) -> anyhow::Result { - if ctx.merge_user_settings && config.programs.claude_desktop.is_some() { - anyhow::bail!( - "Claude Desktop does not read inference settings from its user preferences; remove programs.claudeDesktop or run Agentdesktop without --user as root so it can manage its Claude Desktop managed settings" - ); + if let Some(plan) = ctx.skip_unless( + DaemonScope::System, + config.programs.claude_desktop.is_some(), + Self::DISPLAY_NAME, + ) { + return Ok(plan); } let configured = config.programs.claude_desktop.as_ref().map(|provider| { let gateway = config diff --git a/crates/agent/src/provider/copilot/mod.rs b/crates/agent/src/provider/copilot/mod.rs index c461b9a3..c406968a 100644 --- a/crates/agent/src/provider/copilot/mod.rs +++ b/crates/agent/src/provider/copilot/mod.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use agentdesktop_core::config::DaemonConfig; -use agentdesktop_core::model::Discovery; +use agentdesktop_core::model::{DaemonScope, Discovery}; use super::{Provider, ReconcileContext}; use crate::reconcile::ReconcilePlan; @@ -52,6 +52,14 @@ impl Provider for Copilot { } fn plan(&self, ctx: &ReconcileContext, config: &DaemonConfig) -> anyhow::Result { + if let Some(plan) = ctx.skip_unless( + DaemonScope::User, + config.programs.copilot.is_some(), + DISPLAY_NAME, + ) { + return Ok(plan); + } + let configured = config.programs.copilot.as_ref().map(|program| { let gateway = config .llm_gateway @@ -64,18 +72,9 @@ impl Provider for Copilot { .as_ref() .map(|proxy| (proxy.address, &*proxy.pairing)); let plan = ReconcilePlan::default(); - // The inverse of Grok's check: the file lives in the user's home, so a - // system daemon has no providers path and rejects the program before - // any provider writes. - let Some(path) = self - .providers_path - .as_deref() - .filter(|_| ctx.merge_user_settings) - else { + let Some(path) = self.providers_path.as_deref() else { if configured.is_some() { - anyhow::bail!( - "{DISPLAY_NAME} reads its providers from the user's own Copilot directory; remove programs.copilot or run agentdesktop with --user (daemon.user: true) so it can manage that file" - ); + anyhow::bail!("{DISPLAY_NAME} user configuration path is unavailable"); } return Ok(plan); }; diff --git a/crates/agent/src/provider/copilot/tests.rs b/crates/agent/src/provider/copilot/tests.rs index 4de39f1c..f4f47437 100644 --- a/crates/agent/src/provider/copilot/tests.rs +++ b/crates/agent/src/provider/copilot/tests.rs @@ -752,10 +752,10 @@ fn conflict_on_a_non_object_file() { assert_eq!(fs::read(&path).unwrap(), b"[]\n"); } -// --- Provider::plan: system-mode rejection ---------------------------------- +// --- Provider::plan: system-mode skipping ---------------------------------- #[test] -fn system_mode_rejects_a_configured_program_before_writing_anything() { +fn system_mode_skips_a_configured_program_without_writing_anything() { use super::Copilot; use crate::provider::{Provider, ReconcileContext}; let dir = tempfile::tempdir().unwrap(); @@ -779,12 +779,15 @@ fn system_mode_rejects_a_configured_program_before_writing_anything() { providers_path: Some(path.clone()), }, ] { - let error = match provider.plan(&context(false), &config) { - Ok(_) => panic!("system mode with programs.copilot configured must fail"), - Err(error) => error, - }; - assert!(error.to_string().contains("--user"), "{error}"); - assert!(error.to_string().contains("GitHub Copilot CLI"), "{error}"); + let plan = provider + .plan(&context(false), &config) + .expect("system mode skips this program"); + let reason = plan + .inactive_reason() + .expect("configured program is inactive"); + assert!(reason.contains("--user"), "{reason}"); + assert!(reason.contains("GitHub Copilot CLI"), "{reason}"); + plan.apply().unwrap(); } assert!(!path.exists(), "preflight failure must not write any files"); // System mode with the program absent does nothing. diff --git a/crates/agent/src/provider/grok/mod.rs b/crates/agent/src/provider/grok/mod.rs index b91e24e5..522ee8ab 100644 --- a/crates/agent/src/provider/grok/mod.rs +++ b/crates/agent/src/provider/grok/mod.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use agentdesktop_core::config::DaemonConfig; -use agentdesktop_core::model::Discovery; +use agentdesktop_core::model::{DaemonScope, Discovery}; use super::{Provider, ReconcileContext}; use crate::reconcile::ReconcilePlan; @@ -41,10 +41,12 @@ impl Provider for Grok { } fn plan(&self, ctx: &ReconcileContext, config: &DaemonConfig) -> anyhow::Result { - if ctx.merge_user_settings && config.programs.grok.is_some() { - anyhow::bail!( - "Grok Build can delete or replace its user-level managed_config.toml during startup; remove programs.grok or run agentdesktop in system mode so it can manage Grok's system managed_config.toml" - ); + if let Some(plan) = ctx.skip_unless( + DaemonScope::System, + config.programs.grok.is_some(), + Self::DISPLAY_NAME, + ) { + return Ok(plan); } let configured = config.programs.grok.as_ref().map(|provider| { let gateway = config diff --git a/crates/agent/src/provider/mod.rs b/crates/agent/src/provider/mod.rs index 881282e5..a9b88cda 100644 --- a/crates/agent/src/provider/mod.rs +++ b/crates/agent/src/provider/mod.rs @@ -4,7 +4,10 @@ //! contributes no entries. Reconciliation is optional and only builds a plan. use std::path::PathBuf; -use agentdesktop_core::{config::DaemonConfig, model::Discovery}; +use agentdesktop_core::{ + config::DaemonConfig, + model::{DaemonScope, Discovery}, +}; use crate::reconcile::ReconcilePlan; @@ -55,3 +58,29 @@ pub struct ReconcileContext { /// or neutralise any pointer they wrote earlier, never leave a stale one. pub llm_proxy: Option, } + +impl ReconcileContext { + /// Skip providers outside this daemon's scope before they read or clean up + /// any files. Only configured programs need an inactive status. + fn skip_unless( + &self, + required_scope: DaemonScope, + configured: bool, + display_name: &str, + ) -> Option { + if self.merge_user_settings == (required_scope == DaemonScope::User) { + return None; + } + Some(if configured { + let (required, current) = match required_scope { + DaemonScope::User => ("user mode (--user)", "system mode"), + DaemonScope::System => ("system mode", "user mode"), + }; + ReconcilePlan::skipped(&format!( + "{display_name} requires {required}; skipped in {current}" + )) + } else { + ReconcilePlan::default() + }) + } +} diff --git a/crates/agent/src/provider/pi/mod.rs b/crates/agent/src/provider/pi/mod.rs index c12e62e3..ff031466 100644 --- a/crates/agent/src/provider/pi/mod.rs +++ b/crates/agent/src/provider/pi/mod.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use agentdesktop_core::config::DaemonConfig; -use agentdesktop_core::model::Discovery; +use agentdesktop_core::model::{DaemonScope, Discovery}; use super::{Provider, ReconcileContext}; use crate::reconcile::ReconcilePlan; @@ -36,15 +36,14 @@ impl Provider for Pi { } fn plan(&self, ctx: &ReconcileContext, config: &DaemonConfig) -> anyhow::Result { - if !ctx.merge_user_settings && config.programs.pi.is_some() { - anyhow::bail!( - "Pi only reads user configuration; run agentdesktop with --user to manage programs.pi, or remove programs.pi from the system configuration" - ); - } - let plan = ReconcilePlan::default(); - if !ctx.merge_user_settings { + if let Some(plan) = ctx.skip_unless( + DaemonScope::User, + config.programs.pi.is_some(), + Self::DISPLAY_NAME, + ) { return Ok(plan); } + let plan = ReconcilePlan::default(); let (Some(models_path), Some(settings_path)) = (&self.models_path, &self.settings_path) else { anyhow::bail!("Pi user configuration paths are unavailable"); @@ -90,7 +89,7 @@ mod tests { use super::{Pi, Provider, ReconcileContext}; #[test] - fn rejects_configured_pi_in_system_mode() { + fn skips_configured_pi_in_system_mode() { let root = tempfile::tempdir().unwrap(); let pi = Pi { models_path: Some(root.path().join("models.json")), @@ -105,9 +104,11 @@ mod tests { let config = parse_daemon("programs:\n pi:\n managedConfig:\n providers: {}\n").unwrap(); - let error = pi.plan(&ctx, &config).err().expect("Pi needs user mode"); - assert!(error.to_string().contains("--user")); - assert!(error.to_string().contains("programs.pi")); + let plan = pi + .plan(&ctx, &config) + .expect("Pi is skipped in system mode"); + assert!(plan.inactive_reason().unwrap().contains("--user")); + plan.apply().unwrap(); assert!(!pi.models_path.as_ref().unwrap().exists()); assert!(!pi.settings_path.as_ref().unwrap().exists()); } diff --git a/crates/agent/src/provider/vscode/mod.rs b/crates/agent/src/provider/vscode/mod.rs index 8e5c7d97..b6c9fc97 100644 --- a/crates/agent/src/provider/vscode/mod.rs +++ b/crates/agent/src/provider/vscode/mod.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use agentdesktop_core::config::{DaemonConfig, VsCodeCopilotChat}; -use agentdesktop_core::model::Discovery; +use agentdesktop_core::model::{DaemonScope, Discovery}; use super::{Provider, ReconcileContext}; use crate::reconcile::ReconcilePlan; @@ -56,6 +56,14 @@ impl Provider for VsCode { } fn plan(&self, ctx: &ReconcileContext, config: &DaemonConfig) -> anyhow::Result { + if let Some(plan) = ctx.skip_unless( + DaemonScope::User, + config.programs.vscode.is_some(), + Self::DISPLAY_NAME, + ) { + return Ok(plan); + } + let configured = config.programs.vscode.as_ref().map(|program| { let gateway = config .llm_gateway @@ -68,10 +76,8 @@ impl Provider for VsCode { .as_ref() .map(|proxy| (proxy.address, &*proxy.pairing)); let plan = ReconcilePlan::default(); - // Both files live in the user's VS Code profile, so a system daemon has - // no paths and rejects the program before any provider writes. let paths = match (&self.chat_models_path, &self.settings_path) { - (Some(chat_models), Some(settings)) if ctx.merge_user_settings => { + (Some(chat_models), Some(settings)) => { Some((chat_models.as_path(), settings.as_path())) } _ => None, @@ -79,7 +85,7 @@ impl Provider for VsCode { let Some((chat_models_path, settings_path)) = paths else { if configured.is_some() { anyhow::bail!( - "{} Copilot Chat reads its custom models (chatLanguageModels.json) and the gateway override (settings.json) from the user's own VS Code profile; remove programs.vscode or run agentdesktop with --user (daemon.user: true) so it can manage those files", + "{} user configuration paths are unavailable", Self::DISPLAY_NAME ); } diff --git a/crates/agent/src/provider/vscode/tests.rs b/crates/agent/src/provider/vscode/tests.rs index 91d37bbe..acd96b94 100644 --- a/crates/agent/src/provider/vscode/tests.rs +++ b/crates/agent/src/provider/vscode/tests.rs @@ -644,10 +644,10 @@ fn conflict_on_a_non_array_non_object_file() { assert_eq!(fs::read(&path).unwrap(), b"42\n"); } -// --- Provider::plan: system-mode rejection ---------------------------------- +// --- Provider::plan: system-mode skipping ---------------------------------- #[test] -fn system_mode_rejects_a_configured_program_before_writing_anything() { +fn system_mode_skips_a_configured_program_without_writing_anything() { use super::VsCode; use crate::provider::{Provider, ReconcileContext}; let dir = tempfile::tempdir().unwrap(); @@ -673,12 +673,15 @@ fn system_mode_rejects_a_configured_program_before_writing_anything() { settings_path: Some(path.with_file_name("settings.json")), }, ] { - let error = match provider.plan(&context(false), &config) { - Ok(_) => panic!("system mode with programs.vscode configured must fail"), - Err(error) => error, - }; - assert!(error.to_string().contains("--user"), "{error}"); - assert!(error.to_string().contains("VS Code"), "{error}"); + let plan = provider + .plan(&context(false), &config) + .expect("system mode skips this program"); + let reason = plan + .inactive_reason() + .expect("configured program is inactive"); + assert!(reason.contains("--user"), "{reason}"); + assert!(reason.contains("VS Code"), "{reason}"); + plan.apply().unwrap(); } assert!(!path.exists(), "preflight failure must not write any files"); } diff --git a/crates/agent/src/reconcile/mod.rs b/crates/agent/src/reconcile/mod.rs index c1a63069..bfcf623d 100644 --- a/crates/agent/src/reconcile/mod.rs +++ b/crates/agent/src/reconcile/mod.rs @@ -294,8 +294,8 @@ pub enum ProgramState { Removed, /// A managed file held conflicting or invalid existing configuration. Conflict, - /// The program uses the gateway, a gateway is configured, and the - /// loopback proxy is absent. + /// The daemon's mode cannot manage this program, or the program uses a + /// configured gateway but the loopback proxy is absent. Detail explains why. Inactive, /// The program had changes, but none of them was written because the /// apply was refused or stopped. @@ -343,9 +343,9 @@ impl ApplyReport { /// warning. pub fn log(&self) { for outcome in &self.programs { - // Inactive is a warning too: the program is configured for the - // gateway but its files are removed. The providers log the cause - // at debug level, since they plan on every apply. + // Inactive is a warning too: a configured program cannot be + // managed in this mode or cannot use the gateway. Detail explains + // the cause, including any effect on its existing files. let warning = matches!( outcome.state, ProgramState::Failed @@ -621,6 +621,7 @@ pub fn configured_programs(programs: &ProgramsConfig) -> Vec<&'static str> { #[derive(Default)] struct DryRunReport { changes: RefCell>, + inactive: RefCell>, } struct DryRunChange { @@ -668,6 +669,10 @@ impl DryRunReport { .count(); let mut output = String::from("Dry run — no files will be changed\n"); + for reason in self.inactive.borrow().iter() { + let _ = write!(output, "\nINACTIVE {reason}\n"); + } + for change in changes.iter().filter(|change| change.action != "unchanged") { let _ = write!( output, @@ -758,99 +763,147 @@ mod tests { } #[test] - fn user_mode_rejects_claude_desktop_before_writing_other_settings() { - let root = std::env::temp_dir().join(format!( - "agentdesktop-reconcile-user-desktop-{}-{}", - std::process::id(), - rand::random::() - )); + fn mixed_mode_policy_applies_supported_programs_and_preserves_skipped_files() { let config = parse_daemon( r#" +llmGateway: + url: https://gateway.example.com programs: claudeCode: {} - claudeDesktop: {} + claudeDesktop: + model: selected + grok: + model: selected + pi: + model: selected + copilot: + models: + selected: {} + vscode: + models: + selected: {} "#, ) - .expect("valid configuration"); - let reconciler = Reconciler::new( - true, - root.join("claude/settings.json"), - root.join("claude-desktop/settings.json"), - root.join("claude-desktop/helper"), - root.join("codex/config.toml"), - root.join("opencode/config.json"), - root.join("opencode/plugin.js"), - root.join("grok/managed_config.toml"), - Some(root.join("copilot/providers.json")), - None, - None, - Some(root.join("pi/models.json")), - Some(root.join("pi/settings.json")), - root.join("bin/agentdesktop"), - root.join("agentdesktop.sock"), - ); - - let error = reconciler.apply(&config).expect_err("user mode must fail"); + .expect("mixed fleet policy is valid"); + let programs = [ + ("claude-desktop", "claude-desktop", false), + ("grok", "grok", false), + ("pi", "pi", true), + ("copilot", "copilot", true), + ("vscode", "vscode", true), + ]; - assert!( - error - .to_string() - .contains("Claude Desktop managed settings") - ); - assert!(!root.exists(), "preflight failure must not write any files"); - } + fn snapshot(path: &Path) -> std::collections::BTreeMap> { + let mut files = std::collections::BTreeMap::new(); + for entry in fs::read_dir(path).unwrap() { + let path = entry.unwrap().path(); + if path.is_dir() { + files.extend(snapshot(&path)); + } else { + files.insert(path.clone(), fs::read(path).unwrap()); + } + } + files + } - #[test] - fn user_mode_rejects_grok_before_writing_other_settings() { - let root = std::env::temp_dir().join(format!( - "agentdesktop-reconcile-user-grok-{}-{}", - std::process::id(), - rand::random::() - )); - let config = parse_daemon( - r#" -programs: - claudeCode: {} - grok: {} -"#, - ) - .unwrap(); - let reconciler = Reconciler::new( - true, - root.join("claude/settings.json"), - root.join("claude-desktop/settings.json"), - root.join("claude-desktop/helper"), - root.join("codex/config.toml"), - root.join("opencode/config.json"), - root.join("opencode/plugin.js"), - root.join("grok/managed_config.toml"), - Some(root.join("copilot/providers.json")), - None, - None, - Some(root.join("pi/models.json")), - Some(root.join("pi/settings.json")), - root.join("bin/agentdesktop"), - root.join("agentdesktop.sock"), - ); + for user in [false, true] { + let root = tempfile::tempdir().unwrap(); + let mut reconciler = + full_reconciler(root.path()).with_llm_proxy(Some(proxy_context("PAIRING"))); + // Seed owned files in the opposite mode, including ownership sidecars. + reconciler.context.merge_user_settings = !user; + let mut seed = config.clone(); + seed.programs.claude_code = None; + reconciler + .apply(&seed) + .expect("seed the other mode's files"); + let skipped: Vec<_> = programs + .iter() + .filter(|(_, _, required_user)| *required_user != user) + .map(|(id, dir, _)| (*id, snapshot(&root.path().join(dir)))) + .collect(); + assert!(skipped.iter().all(|(_, files)| !files.is_empty())); + + reconciler.context.merge_user_settings = user; + let preview = reconciler.plan(&config).unwrap().render(); + assert!(preview.contains("INACTIVE"), "{preview}"); + assert!(preview.contains(if user { + "requires system mode" + } else { + "requires user mode" + })); + let (report, result) = reconciler.apply_with_report(&config); + result.expect("unsupported programs must not block supported programs"); + assert_eq!( + program_outcome(&report, "claude-code").unwrap().state, + ProgramState::Applied + ); + for (id, _, required_user) in programs { + let outcome = program_outcome(&report, id).unwrap(); + if required_user == user { + assert_eq!(outcome.state, ProgramState::Applied, "{id}"); + } else { + assert_eq!(outcome.state, ProgramState::Inactive, "{id}"); + assert!( + outcome + .detail + .contains(if user { "system mode" } else { "user mode" }), + "{}", + outcome.detail + ); + } + } + for (id, files) in &skipped { + assert_eq!(&snapshot(&root.path().join(id)), files, "{id}"); + } - let error = reconciler.apply(&config).expect_err("user mode must fail"); - assert!(error.to_string().contains("Grok Build")); - assert!(!root.exists(), "preflight failure must not write any files"); + let (report, result) = reconciler.apply_with_report(&parse_daemon("{}").unwrap()); + result.expect("cleanup only affects programs supported in this mode"); + for (id, files) in &skipped { + assert!( + program_outcome(&report, id).is_none(), + "disabled {id} has no status" + ); + assert_eq!(&snapshot(&root.path().join(id)), files, "{id}"); + } + } } #[test] - fn system_mode_rejects_pi_before_writing_other_settings() { - let fixture = Fixture::new(); - let config = parse_daemon("programs:\n claudeCode: {}\n pi: {}\n").unwrap(); - let error = fixture - .reconciler - .apply(&config) - .expect_err("Pi requires user mode"); - assert!(error.to_string().contains("--user")); - assert!( - !fixture.root.exists(), - "preflight failure must not write any files" - ); + fn skipped_programs_do_not_read_unmanageable_paths() { + for (user, yaml, paths) in [ + ( + true, + "programs:\n claudeCode: {}\n claudeDesktop: {}\n grok: {}", + vec!["claude-desktop/settings.json", "grok/managed_config.toml"], + ), + ( + false, + "programs:\n claudeCode: {}\n pi: {}\n copilot: {}\n vscode: {}", + vec![ + "pi/models.json", + "copilot/providers.json", + "vscode/User/settings.json", + ], + ), + ] { + let root = tempfile::tempdir().unwrap(); + // Directories where files are expected would fail planning if read. + for path in &paths { + fs::create_dir_all(root.path().join(path)).unwrap(); + } + let mut reconciler = full_reconciler(root.path()); + reconciler.context.merge_user_settings = user; + let (report, result) = reconciler.apply_with_report(&parse_daemon(yaml).unwrap()); + result.expect("skipped programs do no I/O"); + assert_eq!( + program_outcome(&report, "claude-code").unwrap().state, + ProgramState::Applied + ); + for path in paths { + assert!(root.path().join(path).is_dir()); + } + } } #[test] @@ -1037,50 +1090,6 @@ programs: let _ = std::fs::remove_dir_all(&root); } - #[test] - fn system_mode_rejects_copilot_before_writing_other_settings() { - let root = std::env::temp_dir().join(format!( - "agentdesktop-reconcile-system-copilot-{}-{}", - std::process::id(), - rand::random::() - )); - let config = parse_daemon( - r#" -programs: - claudeCode: {} - copilot: {} -"#, - ) - .unwrap(); - // The Copilot CLI program is the inverse of Grok: it manages a file in - // the user's home, so a system daemon (no providers path) rejects it - // before any provider writes. - let reconciler = Reconciler::new( - false, - root.join("claude/settings.json"), - root.join("claude-desktop/settings.json"), - root.join("claude-desktop/helper"), - root.join("codex/config.toml"), - root.join("opencode/config.json"), - root.join("opencode/plugin.js"), - root.join("grok/managed_config.toml"), - None, - None, - None, - Some(root.join("pi/models.json")), - Some(root.join("pi/settings.json")), - root.join("bin/agentdesktop"), - root.join("agentdesktop.sock"), - ); - - let error = reconciler - .apply(&config) - .expect_err("system mode must fail"); - assert!(error.to_string().contains("GitHub Copilot CLI"), "{error}"); - assert!(error.to_string().contains("--user"), "{error}"); - assert!(!root.exists(), "preflight failure must not write any files"); - } - #[cfg(windows)] #[test] fn windows_reconciles_system_grok_managed_config() { @@ -1589,23 +1598,28 @@ programs: #[test] fn failed_provider_plan_does_not_stop_the_others() { let root = new_root("failed-plan-continues"); - // Grok Build's managed config lives in a system location, so a - // user-mode daemon rejects it in `plan()` (see - // `user_mode_rejects_grok_before_writing_other_settings` above) while - // Claude Code plans normally: a real, deterministic plan failure. + // A directory where Pi expects a file is a real I/O failure. Grok is + // skipped in user mode and must remain inactive even when apply fails. + fs::create_dir_all(root.join("pi/models.json")).unwrap(); let reconciler = full_reconciler(&root); - let config = parse_daemon("programs:\n claudeCode: {}\n grok: {}").unwrap(); + let config = parse_daemon("programs:\n claudeCode: {}\n pi: {}\n grok: {}").unwrap(); let (report, result) = reconciler.apply_with_report(&config); assert!(result.is_err()); - let grok = program_outcome(&report, "grok").expect("grok is reported"); - assert_eq!(grok.state, ProgramState::Failed); - assert!(grok.detail.contains("Grok Build"), "{}", grok.detail); assert_eq!( - program_outcome(&report, "claude-code").map(|outcome| outcome.state), - Some(ProgramState::Blocked), + program_outcome(&report, "pi").unwrap().state, + ProgramState::Failed + ); + assert_eq!( + program_outcome(&report, "grok").unwrap().state, + ProgramState::Inactive + ); + assert_eq!( + program_outcome(&report, "claude-code").unwrap().state, + ProgramState::Blocked, "claude-code was still planned, but nothing is written because the apply fails" ); + assert!(!root.join("claude/settings.json").exists()); let _ = fs::remove_dir_all(&root); } diff --git a/crates/agent/src/reconcile/plan.rs b/crates/agent/src/reconcile/plan.rs index 333a5b4f..5a0a8d81 100644 --- a/crates/agent/src/reconcile/plan.rs +++ b/crates/agent/src/reconcile/plan.rs @@ -16,8 +16,8 @@ pub struct ReconcilePlan { operations: RefCell>, private_dirs: PrivateDirs, report: DryRunReport, - /// Why the program that owns this plan is configured but not pointed at - /// the gateway (the loopback proxy is absent); set by the provider. + /// Why the configured program cannot be managed in this daemon's mode or + /// cannot use the gateway (the loopback proxy is absent). inactive: RefCell>, } @@ -76,12 +76,20 @@ impl ReconcilePlan { self.apply_tracked().map_err(|(error, _)| error) } - /// Records that the program owning this plan uses the gateway, a gateway - /// is configured, and the loopback proxy is absent. + /// Records why the configured program is inactive. pub fn inactive(&self, reason: &str) { *self.inactive.borrow_mut() = Some(reason.to_owned()); } + /// A program outside this daemon's scope has no file operations. Keep its + /// reason in both per-program status and the combined dry-run report. + pub(crate) fn skipped(reason: &str) -> Self { + let plan = Self::default(); + plan.inactive(reason); + plan.report.inactive.borrow_mut().push(reason.to_owned()); + plan + } + /// The reason recorded by [`ReconcilePlan::inactive`], for tests. #[cfg(test)] pub(crate) fn inactive_reason(&self) -> Option { @@ -242,6 +250,10 @@ impl ReconcilePlan { .changes .get_mut() .extend(other.report.changes.into_inner()); + self.report + .inactive + .get_mut() + .extend(other.report.inactive.into_inner()); Ok(()) } diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs index 30ddfee9..0dc60ddd 100644 --- a/crates/core/src/config.rs +++ b/crates/core/src/config.rs @@ -664,6 +664,10 @@ fn default_heartbeat_interval() -> Duration { } /// Settings for developer tools managed by Agentdesktop. +/// +/// A policy can mix user-only and system-only programs. Each daemon skips +/// programs its mode cannot manage, reporting them as inactive with a reason. +/// Skipped programs' files are left untouched, including during cleanup. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -671,7 +675,8 @@ pub struct ProgramsConfig { /// Claude Code managed-settings configuration. Arbitrary keys are passed through directly. #[serde(default, skip_serializing_if = "Option::is_none")] pub claude_code: Option, - /// Claude Desktop managed configuration. Arbitrary keys are passed through directly. + /// Claude Desktop system-managed configuration. User daemons skip it. + /// Arbitrary keys are passed through directly. #[serde(default, skip_serializing_if = "Option::is_none")] pub claude_desktop: Option, /// Codex managed configuration. @@ -692,8 +697,7 @@ pub struct ProgramsConfig { pub vscode: Option, /// Pi coding-agent harness managed configuration. /// - /// Requires `--user`. Cannot be combined with Claude Desktop or Grok Build, - /// which require system mode. + /// Managed in user mode (`--user`); system daemons skip it with an inactive reason. #[serde(default, skip_serializing_if = "Option::is_none")] pub pi: Option, } @@ -803,7 +807,7 @@ pub struct OpenCodeConfig { /// When generated LLM-gateway settings overlap with those values, /// agentdesktop's generated values take precedence. /// Only system mode is supported. Grok can delete or replace the user-level -/// managed file during startup, so `--user` rejects `programs.grok`. +/// managed file during startup, so user daemons skip it with an inactive reason. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -840,8 +844,8 @@ pub struct GrokConfig { /// managed-settings equivalent for providers, and users may have their own /// BYOK entries. See `provider::copilot::reconcile` in the agent crate for /// how this is turned into the merged document. User mode only: the file -/// lives in the user's Copilot directory, so a system daemon rejects the -/// program. +/// lives in the user's Copilot directory, so system daemons skip it with an +/// inactive reason. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -982,7 +986,7 @@ impl CopilotConfig { /// the daemon adds the gateway identity. See `provider::vscode::reconcile` /// and `provider::vscode::settings` in the agent crate for how each is turned /// into its merged document. User mode only: both files live in the user's -/// VS Code profile directory, so a system daemon rejects the program. +/// VS Code profile directory, so system daemons skip it with an inactive reason. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -1153,7 +1157,7 @@ impl VsCodeConfig { /// /// Generated `providers.agentdesktop` values take precedence over overlapping /// `managedConfig` keys. Pi stores configuration under `~/.pi/agent` (or -/// `PI_CODING_AGENT_DIR`), so management requires `--user`. +/// `PI_CODING_AGENT_DIR`), so management requires `--user`. System daemons skip it. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -1589,12 +1593,6 @@ fn validate_daemon( } } } - if programs.pi.is_some() && programs.claude_desktop.is_some() { - anyhow::bail!("programs.pi requires --user, which cannot apply programs.claudeDesktop"); - } - if programs.pi.is_some() && programs.grok.is_some() { - anyhow::bail!("programs.pi requires --user, which cannot apply programs.grok"); - } if let Some(pi) = &programs.pi && llm_gateway.is_some() && pi.use_llm_gateway @@ -2295,23 +2293,10 @@ programs: } #[test] - fn pi_rejects_system_only_program_combinations() { - for (name, message) in [ - ( - "claudeDesktop", - "programs.pi requires --user, which cannot apply programs.claudeDesktop", - ), - ( - "grok", - "programs.pi requires --user, which cannot apply programs.grok", - ), - ] { - let error = parse_daemon(&format!("programs:\n pi: {{}}\n {name}: {{}}\n")) - .expect_err("Pi combined with a system-only program should fail"); - assert!( - error.to_string().contains(message), - "missing {message} in {error}" - ); - } + fn accepts_mixed_user_and_system_programs() { + parse_daemon( + "programs:\n pi: {}\n claudeDesktop: {}\n grok: {}\n copilot: {}\n vscode: {}", + ) + .expect("each daemon manages the programs supported in its mode"); } } diff --git a/examples/claude/README.md b/examples/claude/README.md index dd7d2406..122b73e3 100644 --- a/examples/claude/README.md +++ b/examples/claude/README.md @@ -78,8 +78,9 @@ It will show the configured `Managed by Agentdesktop` and direct traffic through ## Pi -Pi requires user-mode configuration. To include Pi, remove `programs.claudeDesktop` -from `claude-code.yaml` and uncomment `programs.pi`. Run the daemon as the user +To include Pi, uncomment `programs.pi` in `claude-code.yaml`. The same policy can +include Claude Desktop: user daemons manage Pi and skip Claude Desktop, while +system daemons manage Claude Desktop and skip Pi. Run the daemon as the user who runs Pi: ```console diff --git a/examples/claude/claude-code.yaml b/examples/claude/claude-code.yaml index d5e9a37c..93c23178 100644 --- a/examples/claude/claude-code.yaml +++ b/examples/claude/claude-code.yaml @@ -16,7 +16,7 @@ programs: companyAnnouncements: ["Managed by Agentdesktop"] claudeDesktop: isLocalDevMcpEnabled: true - # Pi requires --user. Remove claudeDesktop before enabling Pi in user mode. + # Pi requires --user; system daemons skip it. User daemons skip claudeDesktop. # pi: # useLlmGateway: true # model: claude-sonnet-4-5 diff --git a/examples/standalone/README.md b/examples/standalone/README.md index ffe8646b..5e244bf7 100644 --- a/examples/standalone/README.md +++ b/examples/standalone/README.md @@ -85,7 +85,8 @@ cost by tool and model. Uncomment `programs.pi` in `config.yaml` to enable Pi with `claude-sonnet-4-5`. Run `pi` while the daemon is running to use that model through the gateway. Pi requires `--user`; authenticated gateway configuration cannot be applied -with `--once`. Leave Pi commented out when using Claude Desktop. +with `--once` in user mode. Pi and Claude Desktop can share a policy: user +daemons skip Claude Desktop, and system daemons skip Pi, with an inactive reason. ## Claude Desktop diff --git a/examples/standalone/config.yaml b/examples/standalone/config.yaml index 5db8037f..1783174f 100644 --- a/examples/standalone/config.yaml +++ b/examples/standalone/config.yaml @@ -22,6 +22,6 @@ programs: # pi: # useLlmGateway: true # model: claude-sonnet-4-5 -# Claude Desktop requires system mode; leave Pi disabled when enabling it. +# Claude Desktop requires system mode; user daemons skip it. # claudeDesktop: # useLlmGateway: true diff --git a/schema/daemon-config.json b/schema/daemon-config.json index 15248fcb..caa02cb8 100644 --- a/schema/daemon-config.json +++ b/schema/daemon-config.json @@ -176,7 +176,7 @@ ] }, "CopilotConfig": { - "description": "Settings reconciled into GitHub Copilot CLI's `providers.json`.\n\n`providers.json` is merged, never owned outright: the CLI has no\nmanaged-settings equivalent for providers, and users may have their own\nBYOK entries. See `provider::copilot::reconcile` in the agent crate for\nhow this is turned into the merged document. User mode only: the file\nlives in the user's Copilot directory, so a system daemon rejects the\nprogram.", + "description": "Settings reconciled into GitHub Copilot CLI's `providers.json`.\n\n`providers.json` is merged, never owned outright: the CLI has no\nmanaged-settings equivalent for providers, and users may have their own\nBYOK entries. See `provider::copilot::reconcile` in the agent crate for\nhow this is turned into the merged document. User mode only: the file\nlives in the user's Copilot directory, so system daemons skip it with an\ninactive reason.", "type": "object", "properties": { "models": { @@ -362,7 +362,7 @@ ] }, "GrokConfig": { - "description": "Settings reconciled into Grok Build's organization-managed configuration.\n\nValues under `managedConfig` are written to Grok's `managed_config.toml`.\nWhen generated LLM-gateway settings overlap with those values,\nagentdesktop's generated values take precedence.\nOnly system mode is supported. Grok can delete or replace the user-level\nmanaged file during startup, so `--user` rejects `programs.grok`.", + "description": "Settings reconciled into Grok Build's organization-managed configuration.\n\nValues under `managedConfig` are written to Grok's `managed_config.toml`.\nWhen generated LLM-gateway settings overlap with those values,\nagentdesktop's generated values take precedence.\nOnly system mode is supported. Grok can delete or replace the user-level\nmanaged file during startup, so user daemons skip it with an inactive reason.", "type": "object", "properties": { "managedConfig": { @@ -589,7 +589,7 @@ "additionalProperties": false }, "PiConfig": { - "description": "Settings reconciled into Pi's user `models.json` and `settings.json`.\n\nGenerated `providers.agentdesktop` values take precedence over overlapping\n`managedConfig` keys. Pi stores configuration under `~/.pi/agent` (or\n`PI_CODING_AGENT_DIR`), so management requires `--user`.", + "description": "Settings reconciled into Pi's user `models.json` and `settings.json`.\n\nGenerated `providers.agentdesktop` values take precedence over overlapping\n`managedConfig` keys. Pi stores configuration under `~/.pi/agent` (or\n`PI_CODING_AGENT_DIR`), so management requires `--user`. System daemons skip it.", "type": "object", "properties": { "api": { @@ -655,7 +655,7 @@ ] }, "ProgramsConfig": { - "description": "Settings for developer tools managed by Agentdesktop.", + "description": "Settings for developer tools managed by Agentdesktop.\n\nA policy can mix user-only and system-only programs. Each daemon skips\nprograms its mode cannot manage, reporting them as inactive with a reason.\nSkipped programs' files are left untouched, including during cleanup.", "type": "object", "properties": { "claudeCode": { @@ -670,7 +670,7 @@ ] }, "claudeDesktop": { - "description": "Claude Desktop managed configuration. Arbitrary keys are passed through directly.", + "description": "Claude Desktop system-managed configuration. User daemons skip it.\nArbitrary keys are passed through directly.", "anyOf": [ { "$ref": "#/$defs/ClaudeDesktopConfig" @@ -725,7 +725,7 @@ ] }, "pi": { - "description": "Pi coding-agent harness managed configuration.\n\nRequires `--user`. Cannot be combined with Claude Desktop or Grok Build,\nwhich require system mode.", + "description": "Pi coding-agent harness managed configuration.\n\nManaged in user mode (`--user`); system daemons skip it with an inactive reason.", "anyOf": [ { "$ref": "#/$defs/PiConfig" @@ -886,7 +886,7 @@ ] }, "VsCodeConfig": { - "description": "Settings reconciled into VS Code's Copilot Chat configuration. Under\n`copilotChat: ownModels` this is `chatLanguageModels.json`, pointing\nCopilot Chat's built-in \"Custom Endpoint\" model provider at the local LLM\nproxy's `/vscode-copilot` route; under `copilotChat: githubModels`\nit is the user `settings.json`, pointing Copilot Chat's\nCAPI endpoint at the loopback proxy's `/vscode-copilot-capi/`\nroute so VS Code keeps GitHub's own models and its own Copilot token while\nthe daemon adds the gateway identity. See `provider::vscode::reconcile`\nand `provider::vscode::settings` in the agent crate for how each is turned\ninto its merged document. User mode only: both files live in the user's\nVS Code profile directory, so a system daemon rejects the program.", + "description": "Settings reconciled into VS Code's Copilot Chat configuration. Under\n`copilotChat: ownModels` this is `chatLanguageModels.json`, pointing\nCopilot Chat's built-in \"Custom Endpoint\" model provider at the local LLM\nproxy's `/vscode-copilot` route; under `copilotChat: githubModels`\nit is the user `settings.json`, pointing Copilot Chat's\nCAPI endpoint at the loopback proxy's `/vscode-copilot-capi/`\nroute so VS Code keeps GitHub's own models and its own Copilot token while\nthe daemon adds the gateway identity. See `provider::vscode::reconcile`\nand `provider::vscode::settings` in the agent crate for how each is turned\ninto its merged document. User mode only: both files live in the user's\nVS Code profile directory, so system daemons skip it with an inactive reason.", "type": "object", "properties": { "copilotChat": { diff --git a/schema/daemon-config.md b/schema/daemon-config.md index af410a70..34b29b84 100644 --- a/schema/daemon-config.md +++ b/schema/daemon-config.md @@ -62,7 +62,7 @@ |`programs.claudeCode`|object|Claude Code managed-settings configuration. Arbitrary keys are passed through directly.| |`programs.claudeCode.auth`|enum|Upstream authentication used by this agent.
Possible values: `subscription`.| |`programs.claudeCode.useLlmGateway`|boolean|Whether this program uses the top-level LLM gateway.| -|`programs.claudeDesktop`|object|Claude Desktop managed configuration. Arbitrary keys are passed through directly.| +|`programs.claudeDesktop`|object|Claude Desktop system-managed configuration. User daemons skip it.
Arbitrary keys are passed through directly.| |`programs.claudeDesktop.auth`|enum|Upstream authentication used by this agent.
Possible values: `subscription`.| |`programs.claudeDesktop.useLlmGateway`|boolean|Whether this program uses the top-level LLM gateway.| |`programs.codex`|object|Codex managed configuration.| @@ -83,7 +83,7 @@ |`programs.openCode.model`|string|Model ID selected from `models` when using the LLM gateway.

This is required when a top-level `llmGateway` is configured.| |`programs.openCode.models`|object|Models exposed by the managed LLM gateway provider, keyed by model ID.

Each value is an arbitrary OpenCode model configuration object. At least
one model is required when a top-level `llmGateway` is configured.| |`programs.openCode.useLlmGateway`|boolean|Whether this program uses the top-level LLM gateway.| -|`programs.pi`|object|Pi coding-agent harness managed configuration.

Requires `--user`. Cannot be combined with Claude Desktop or Grok Build,
which require system mode.| +|`programs.pi`|object|Pi coding-agent harness managed configuration.

Managed in user mode (`--user`); system daemons skip it with an inactive reason.| |`programs.pi.api`|string|Pi API dialect for the managed provider. Defaults to `anthropic-messages`.| |`programs.pi.managedConfig`|object|Arbitrary values merged into Pi's `models.json`.| |`programs.pi.model`|string|Model ID selected when pointing Pi at the LLM gateway.

This is required when a top-level `llmGateway` is configured. If `models`
is empty, agentdesktop creates a catalog entry with this ID.|