From 441fce8752fdd63ecb2d1fe33da7e67d01d27ef4 Mon Sep 17 00:00:00 2001 From: themartto Date: Fri, 29 May 2026 10:35:27 +0300 Subject: [PATCH 01/14] feat: system.md identity, default_skills, and structured prompt --- src/client.rs | 3 +- src/config/client.rs | 1 + src/config/config.toml.default | 4 + src/config/mod.rs | 31 ++++-- src/config/resolve.rs | 2 + src/config/types.rs | 3 + src/rag/mod.rs | 81 ++++++++++++--- src/rag/prompt.rs | 185 +++++++++++++++++++++++++-------- src/rag/system.rs | 34 ++++++ src/transport/stdio.rs | 2 +- src/transport/ws.rs | 2 +- src/tui/app.rs | 24 +++-- 12 files changed, 293 insertions(+), 79 deletions(-) create mode 100644 src/rag/system.rs diff --git a/src/client.rs b/src/client.rs index cdb96ac..67ec990 100644 --- a/src/client.rs +++ b/src/client.rs @@ -336,7 +336,7 @@ impl OpenheimBuilder { app_config.mcp_servers.insert(name, cfg); } - let rag = RagContext::new()?; + let rag = RagContext::new(app_config.default_skills.clone())?; let state = Arc::new(AgentState::new(agent_config, app_config, rag).await?); Ok(OpenheimClient { state }) } @@ -378,6 +378,7 @@ fn build_programmatic( theme_color: None, providers, mcp_servers: BTreeMap::new(), + default_skills: vec![], }; let agent_config = AgentConfig { diff --git a/src/config/client.rs b/src/config/client.rs index dab187a..9a363ea 100644 --- a/src/config/client.rs +++ b/src/config/client.rs @@ -121,6 +121,7 @@ mod tests { theme_color: None, providers, mcp_servers: BTreeMap::new(), + default_skills: vec![], } } diff --git a/src/config/config.toml.default b/src/config/config.toml.default index ab77dd0..e978537 100644 --- a/src/config/config.toml.default +++ b/src/config/config.toml.default @@ -7,6 +7,10 @@ default_provider = "openai" # Maximum number of agent iterations (can be overridden with --max-iterations) max_iterations = 10 +# Skills loaded automatically in every new session (no --skills flag needed). +# Add skill names matching files in ~/.openheim/skills/{name}.md +# default_skills = ["rules"] + # --- Provider Configuration --- # # The provider name determines which API client is used: diff --git a/src/config/mod.rs b/src/config/mod.rs index c0f6646..df56f3f 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -23,20 +23,37 @@ pub fn config_path() -> Result { Ok(config_dir()?.join("config.toml")) } +const DEFAULT_SYSTEM_MD: &str = "You are Openheim, a multipurpose, multiprovider LLM agent."; + /// Initialize the config file at ~/.openheim/config.toml with the default template. -/// Returns the path written to. +/// Also writes ~/.openheim/system.md if it does not already exist. +/// Returns the path of the config file written. +/// +/// Errors if `config.toml` already exists. `system.md` is written regardless — +/// so existing users who already have a config can still run `openheim init` to +/// get their `system.md` created. pub fn init_config() -> Result { let dir = config_dir()?; std::fs::create_dir_all(&dir)?; - let path = dir.join("config.toml"); - if path.exists() { + + // Always write system.md first so existing users who re-run `init` get it + // even though config.toml already exists and will cause an early return below. + let system_path = dir.join("system.md"); + if !system_path.exists() { + std::fs::write(&system_path, DEFAULT_SYSTEM_MD)?; + } + + let config_path = dir.join("config.toml"); + if config_path.exists() { return Err(Error::config(format!( - "Config file already exists at {}", - path.display() + "Config file already exists at {}. system.md has been created at {}.", + config_path.display(), + system_path.display() ))); } - std::fs::write(&path, DEFAULT_CONFIG)?; - Ok(path) + std::fs::write(&config_path, DEFAULT_CONFIG)?; + + Ok(config_path) } /// Load AppConfig from a specific path diff --git a/src/config/resolve.rs b/src/config/resolve.rs index 36243f0..8abaa2a 100644 --- a/src/config/resolve.rs +++ b/src/config/resolve.rs @@ -149,6 +149,7 @@ mod tests { theme_color: None, providers, mcp_servers: BTreeMap::new(), + default_skills: vec![], } } @@ -189,6 +190,7 @@ mod tests { theme_color: None, providers: BTreeMap::new(), mcp_servers: BTreeMap::new(), + default_skills: vec![], }; let err = config.resolve(None).unwrap_err(); assert!(err.to_string().contains("nonexistent")); diff --git a/src/config/types.rs b/src/config/types.rs index e3cfaf3..969abda 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -38,6 +38,9 @@ pub struct AppConfig { pub providers: BTreeMap, #[serde(default)] pub mcp_servers: BTreeMap, + /// Skills loaded automatically in every new session (merged with --skills at runtime). + #[serde(default)] + pub default_skills: Vec, } /// Configuration for a single MCP server connection. diff --git a/src/rag/mod.rs b/src/rag/mod.rs index 284d608..4c703c8 100644 --- a/src/rag/mod.rs +++ b/src/rag/mod.rs @@ -3,10 +3,12 @@ pub mod history; pub mod prompt; pub mod skills; +pub mod system; pub use history::{Conversation, ConversationMeta, HistoryManager}; pub use prompt::PromptBuilder; pub use skills::SkillsManager; +pub use system::SystemLoader; use crate::error::Result; use uuid::Uuid; @@ -18,22 +20,33 @@ pub struct RagContext { pub history: HistoryManager, /// Named skill files loaded from `~/.openheim/skills/`. pub skills: SkillsManager, + /// System identity loaded from `~/.openheim/system.md`. + pub system: SystemLoader, + /// Skills included in every new session (from `default_skills` in config). + default_skills: Vec, } impl RagContext { - /// Initialise history and skills from the default openheim data directory. - pub fn new() -> Result { + /// Initialise history, skills, and system identity from the default openheim data directory. + /// + /// `default_skills` are merged with any per-session skills on each new conversation. + pub fn new(default_skills: Vec) -> Result { Ok(Self { history: HistoryManager::new()?, skills: SkillsManager::new()?, + system: SystemLoader::new()?, + default_skills, }) } /// Load or create a conversation and build the prompt context for an agent turn. /// /// Returns the resolved [`Conversation`] and a [`PromptBuilder`] already populated - /// with any requested skills. When `chat_id` refers to an existing conversation the - /// skills stored on that conversation take precedence over `skill_names`. + /// with the system identity and any requested skills. + /// + /// For **new** conversations, `default_skills` are merged with `skill_names` (defaults + /// first, deduplicated) and persisted on the conversation. For **existing** conversations + /// the stored skill list is used as-is, preserving the state from when the session began. pub fn prepare( &self, chat_id: Option, @@ -41,22 +54,27 @@ impl RagContext { model: Option, provider: Option, ) -> Result<(Conversation, PromptBuilder)> { + // Always pass merged skills to resolve_conversation. For existing conversations + // the parameter is ignored (stored list wins); for new ones it gets persisted. + let merged_skills = merge_skills(&self.default_skills, skill_names); + let conversation = self.history - .resolve_conversation(chat_id, model, provider, skill_names.to_vec())?; + .resolve_conversation(chat_id, model, provider, merged_skills)?; let mut builder = PromptBuilder::new(); - // Load skills: use conversation's stored skills if continuing, otherwise use provided ones - let skills_to_load = if chat_id.is_some() && !conversation.meta.skills.is_empty() { - &conversation.meta.skills - } else { - skill_names - }; + // Always inject the system identity as the first layer. + let system_content = self.system.load()?; + tracing::debug!(chars = system_content.len(), "prepare: loaded system.md"); + builder.set_system(system_content); - if !skills_to_load.is_empty() { - let loaded = self.skills.load_skills(skills_to_load)?; + // Load skills from the conversation's stored list (already contains merged + // defaults for new conversations, or the original set for existing ones). + if !conversation.meta.skills.is_empty() { + let loaded = self.skills.load_skills(&conversation.meta.skills)?; for (name, content) in &loaded { + tracing::debug!(skill = %name, "prepare: loaded skill"); builder.add_skill(name, content); } } @@ -64,3 +82,40 @@ impl RagContext { Ok((conversation, builder)) } } + +/// Merge `defaults` with `session` skills, preserving order and deduplicating. +/// Defaults come first; session skills are appended only if not already present. +fn merge_skills(defaults: &[String], session: &[String]) -> Vec { + let mut merged = defaults.to_vec(); + for s in session { + if !merged.contains(s) { + merged.push(s.clone()); + } + } + merged +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn merge_skills_defaults_first_no_duplicates() { + let defaults = vec!["rules".to_string(), "coding".to_string()]; + let session = vec!["coding".to_string(), "rust".to_string()]; + let merged = merge_skills(&defaults, &session); + assert_eq!(merged, vec!["rules", "coding", "rust"]); + } + + #[test] + fn merge_skills_empty_defaults() { + let merged = merge_skills(&[], &["rust".to_string()]); + assert_eq!(merged, vec!["rust"]); + } + + #[test] + fn merge_skills_empty_session() { + let merged = merge_skills(&["rules".to_string()], &[]); + assert_eq!(merged, vec!["rules"]); + } +} diff --git a/src/rag/prompt.rs b/src/rag/prompt.rs index 072ea4e..e306a4c 100644 --- a/src/rag/prompt.rs +++ b/src/rag/prompt.rs @@ -1,16 +1,18 @@ use crate::core::models::{Message, Role}; -/// Builds an LLM message sequence by prepending skill content as a system message. +/// Builds an LLM message sequence by prepending a structured system message. /// -/// Skills are accumulated with [`add_skill`] and combined into a single system -/// message joined by `---` separators. That message is inserted at position 0, -/// followed by the existing conversation history. +/// The system message is assembled from an optional identity (from `system.md`) +/// and any number of named skills, laid out in a consistent template so the +/// model always understands what it is, what identity it has been given, and +/// what skills it has been asked to master. /// -/// If no skills have been added, [`build`] returns the history unchanged with no -/// system message prepended. +/// If neither identity nor skills have been registered, no system message is +/// prepended and `history` is returned unchanged. #[derive(Default)] pub struct PromptBuilder { - system_parts: Vec, + system_identity: Option, + skills: Vec<(String, String)>, } impl PromptBuilder { @@ -18,35 +20,86 @@ impl PromptBuilder { Self::default() } - /// Registers a skill to be included in the system message. - /// - /// Each call appends a `## Skill: {name}` section. Multiple skills are joined - /// with `\n\n---\n\n` when [`build`] is called. + /// Sets the system identity loaded from `system.md`. + pub fn set_system(&mut self, content: String) { + self.system_identity = Some(content); + } + + /// Registers a named skill to include in the system message. pub fn add_skill(&mut self, name: &str, content: &str) { - self.system_parts - .push(format!("## Skill: {}\n\n{}", name, content)); + self.skills.push((name.to_string(), content.to_string())); } /// Constructs the full message list for an LLM request. /// - /// Prepends a system message containing all registered skill sections to - /// `history`. If no skills have been registered, `history` is returned as-is - /// with no system message added. + /// When there is substantive content (non-blank identity or at least one + /// skill), a `Role::System` message is inserted at position 0 using the + /// template below. Otherwise `history` is returned unchanged. + /// + /// ```text + /// You are a general purpose multiprovider LLM agent. + /// + /// The user has given you the following identity: + /// + /// + /// + /// --- + /// + /// These are the skills you have mastered: + /// + /// ### + /// + /// + /// ``` pub fn build(&self, history: &[Message]) -> Vec { - let mut messages = Vec::new(); - - if !self.system_parts.is_empty() { - let system_content = self.system_parts.join("\n\n---\n\n"); - messages.push(Message { - role: Role::System, - content: Some(system_content), - tool_calls: None, - tool_call_id: None, - tool_name: None, - is_error: false, - }); + let identity = self + .system_identity + .as_deref() + .map(str::trim) + .filter(|s| !s.is_empty()); + let has_content = identity.is_some() || !self.skills.is_empty(); + + if !has_content { + return history.to_vec(); + } + + let mut sections: Vec = Vec::new(); + + sections.push("You are a general purpose multiprovider LLM agent.".to_string()); + + if let Some(id) = identity { + sections.push(format!( + "The user has given you the following identity:\n\n{id}" + )); } + if !self.skills.is_empty() { + let skill_blocks: Vec = self + .skills + .iter() + .map(|(name, content)| format!("### {name}\n\n{content}")) + .collect(); + sections.push(format!( + "These are the skills you have mastered:\n\n{}", + skill_blocks.join("\n\n---\n\n") + )); + } + + let system_content = sections.join("\n\n---\n\n"); + tracing::debug!( + len = system_content.len(), + "build: system message assembled" + ); + + let mut messages = vec![Message { + role: Role::System, + content: Some(system_content), + tool_calls: None, + tool_call_id: None, + tool_name: None, + is_error: false, + }]; + messages.extend_from_slice(history); messages } @@ -57,42 +110,84 @@ mod tests { use super::*; #[test] - fn build_with_no_skills_returns_history_unchanged() { + fn build_with_nothing_returns_history_unchanged() { let builder = PromptBuilder::new(); let history = vec![Message::user("hello".into())]; let result = builder.build(&history); assert_eq!(result.len(), 1); assert_eq!(result[0].role, Role::User); - assert_eq!(result[0].content.as_deref(), Some("hello")); } #[test] - fn build_with_one_skill_prepends_system_message() { + fn empty_system_identity_is_ignored() { let mut builder = PromptBuilder::new(); - builder.add_skill("coding", "You are a coding assistant."); - let history = vec![Message::user("help".into())]; + builder.set_system(" ".into()); + let history = vec![Message::user("hi".into())]; let result = builder.build(&history); + assert_eq!(result.len(), 1); + assert_eq!(result[0].role, Role::User); + } + + #[test] + fn set_system_alone_produces_structured_message() { + let mut builder = PromptBuilder::new(); + builder.set_system("I am a helpful agent.".into()); + let result = builder.build(&[Message::user("hi".into())]); assert_eq!(result.len(), 2); - assert_eq!(result[0].role, Role::System); let content = result[0].content.as_deref().unwrap(); - assert!(content.contains("## Skill: coding")); - assert!(content.contains("You are a coding assistant.")); - assert_eq!(result[1].role, Role::User); + assert!(content.contains("You are a general purpose multiprovider LLM agent.")); + assert!(content.contains("The user has given you the following identity:")); + assert!(content.contains("I am a helpful agent.")); + assert!(!content.contains("skills")); } #[test] - fn build_with_multiple_skills_joins_with_separator() { + fn skill_alone_produces_structured_message() { let mut builder = PromptBuilder::new(); - builder.add_skill("skill_a", "Content A"); - builder.add_skill("skill_b", "Content B"); - let history = vec![Message::user("test".into())]; - let result = builder.build(&history); + builder.add_skill("rust", "Write idiomatic Rust."); + let result = builder.build(&[Message::user("hi".into())]); + + assert_eq!(result.len(), 2); + let content = result[0].content.as_deref().unwrap(); + assert!(content.contains("You are a general purpose multiprovider LLM agent.")); + assert!(content.contains("These are the skills you have mastered:")); + assert!(content.contains("### rust")); + assert!(content.contains("Write idiomatic Rust.")); + assert!(!content.contains("identity")); + } + + #[test] + fn identity_and_skills_are_both_present_in_order() { + let mut builder = PromptBuilder::new(); + builder.set_system("Custom identity.".into()); + builder.add_skill("rust", "Be idiomatic."); + builder.add_skill("testing", "Write tests."); + let result = builder.build(&[Message::user("go".into())]); assert_eq!(result.len(), 2); let content = result[0].content.as_deref().unwrap(); - assert!(content.contains("## Skill: skill_a")); - assert!(content.contains("## Skill: skill_b")); + + let base_pos = content.find("general purpose").unwrap(); + let identity_pos = content.find("Custom identity.").unwrap(); + let skills_pos = content.find("skills you have mastered").unwrap(); + let rust_pos = content.find("### rust").unwrap(); + let testing_pos = content.find("### testing").unwrap(); + + assert!(base_pos < identity_pos); + assert!(identity_pos < skills_pos); + assert!(skills_pos < rust_pos); + assert!(rust_pos < testing_pos); + } + + #[test] + fn multiple_skills_are_separated() { + let mut builder = PromptBuilder::new(); + builder.add_skill("a", "Content A"); + builder.add_skill("b", "Content B"); + let content = builder.build(&[]).remove(0).content.unwrap(); + assert!(content.contains("### a")); + assert!(content.contains("### b")); assert!(content.contains("---")); } @@ -107,7 +202,7 @@ mod tests { ]; let result = builder.build(&history); - assert_eq!(result.len(), 4); // system + 3 history + assert_eq!(result.len(), 4); assert_eq!(result[0].role, Role::System); assert_eq!(result[1].content.as_deref(), Some("first")); assert_eq!(result[2].content.as_deref(), Some("second")); diff --git a/src/rag/system.rs b/src/rag/system.rs new file mode 100644 index 0000000..10f1f76 --- /dev/null +++ b/src/rag/system.rs @@ -0,0 +1,34 @@ +use crate::config::config_dir; +use crate::error::{Error, Result}; +use std::path::PathBuf; + +/// Loads the system identity from `~/.openheim/system.md`. +/// +/// The file must exist — run `openheim init` to create it. Returns an error +/// if the file is absent, mirroring the behaviour of a missing `config.toml`. +#[derive(Clone)] +pub struct SystemLoader { + path: PathBuf, +} + +impl SystemLoader { + /// Creates a `SystemLoader` pointed at `~/.openheim/system.md`. + pub fn new() -> Result { + Ok(Self { + path: config_dir()?.join("system.md"), + }) + } + + /// Returns the contents of `system.md`. + /// + /// Returns an error if the file does not exist. + pub fn load(&self) -> Result { + if !self.path.exists() { + return Err(Error::config(format!( + "system.md not found at {}. Run `openheim init` to create one.", + self.path.display() + ))); + } + Ok(std::fs::read_to_string(&self.path)?) + } +} diff --git a/src/transport/stdio.rs b/src/transport/stdio.rs index c1ec0fd..ff738a6 100644 --- a/src/transport/stdio.rs +++ b/src/transport/stdio.rs @@ -21,7 +21,7 @@ use crate::{ pub async fn run() -> crate::error::Result<()> { let app_config = load_config()?; let agent_config = app_config.resolve(None)?; - let rag = RagContext::new()?; + let rag = RagContext::new(app_config.default_skills.clone())?; let state = Arc::new(AgentState::new(agent_config, app_config, rag).await?); acp::serve(Stdio::new(), state) diff --git a/src/transport/ws.rs b/src/transport/ws.rs index 1d67591..fb5c471 100644 --- a/src/transport/ws.rs +++ b/src/transport/ws.rs @@ -76,7 +76,7 @@ enum WsOutbound { pub async fn serve(host: String, port: u16) -> crate::error::Result<()> { let app_config = load_config()?; let agent_config = app_config.resolve(None)?; - let rag = RagContext::new()?; + let rag = RagContext::new(app_config.default_skills.clone())?; let state = Arc::new(AgentState::new(agent_config, app_config, rag).await?); let cors = CorsLayer::new() diff --git a/src/tui/app.rs b/src/tui/app.rs index 7b64307..e7659e5 100644 --- a/src/tui/app.rs +++ b/src/tui/app.rs @@ -449,17 +449,19 @@ impl App { ↑/↓ scroll · PgUp/PgDn page · Ctrl+C quit" .to_string(), )), - "sessions" => match RagContext::new().and_then(|r| r.history.list_conversations()) { - Ok(metas) if metas.is_empty() => { - self.push(ChatItem::SystemInfo("no sessions yet".to_string())); - } - Ok(metas) => { - self.sessions = metas; - self.picker_selected = 0; - self.push_screen(Screen::SessionPicker); + "sessions" => { + match RagContext::new(vec![]).and_then(|r| r.history.list_conversations()) { + Ok(metas) if metas.is_empty() => { + self.push(ChatItem::SystemInfo("no sessions yet".to_string())); + } + Ok(metas) => { + self.sessions = metas; + self.picker_selected = 0; + self.push_screen(Screen::SessionPicker); + } + Err(e) => self.push(ChatItem::Err(e.to_string())), } - Err(e) => self.push(ChatItem::Err(e.to_string())), - }, + } "config" => { let ac = &self.agent_config; let mut rows = vec![ @@ -624,7 +626,7 @@ impl App { let title = meta.title.as_deref().unwrap_or("(untitled)"); self.push(ChatItem::SystemInfo(format!("─── {title}"))); - match RagContext::new().and_then(|r| r.history.load_conversation(&meta.id)) { + match RagContext::new(vec![]).and_then(|r| r.history.load_conversation(&meta.id)) { Ok(conv) => { for msg in &conv.messages { match msg.role { From 3c8af2df1688d4c030edeb087b18d327d23bde08 Mon Sep 17 00:00:00 2001 From: themartto Date: Fri, 29 May 2026 10:35:33 +0300 Subject: [PATCH 02/14] fix: suppress MCP subprocess stderr from leaking into terminal --- src/mcp/client.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/mcp/client.rs b/src/mcp/client.rs index ec39d61..9e17448 100644 --- a/src/mcp/client.rs +++ b/src/mcp/client.rs @@ -42,7 +42,9 @@ impl McpClient { for (k, v) in &config.env { cmd.env(k, v); } - let transport = TokioChildProcess::new(cmd) + let (transport, _) = TokioChildProcess::builder(cmd) + .stderr(std::process::Stdio::null()) + .spawn() .map_err(|e| Error::Other(format!("MCP spawn '{}' failed: {}", name, e)))?; let service = ().serve(transport).await.map_err(|e| { Error::Other(format!("MCP stdio connect to '{}' failed: {}", name, e)) From c587cecb0a175909436e7014665b65bdf17581c6 Mon Sep 17 00:00:00 2001 From: themartto Date: Fri, 29 May 2026 10:35:38 +0300 Subject: [PATCH 03/14] feat: exit process after run command completes --- src/transport/run.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/transport/run.rs b/src/transport/run.rs index 5726620..db94a61 100644 --- a/src/transport/run.rs +++ b/src/transport/run.rs @@ -30,7 +30,7 @@ use crate::{ pub async fn run_headless(prompt: String, model: Option) -> crate::error::Result<()> { let app_config = load_config()?; let agent_config = app_config.resolve(model.as_deref())?; - let rag = RagContext::new()?; + let rag = RagContext::new(app_config.default_skills.clone())?; let state = Arc::new(AgentState::new(agent_config, app_config, rag).await?); let (server_half, client_half) = tokio::io::duplex(65536); @@ -82,8 +82,6 @@ pub async fn run_headless(prompt: String, model: Option) -> crate::error .await .map_err(|e| crate::error::Error::Other(e.to_string()))?; - server_handle - .await - .map_err(|e| crate::error::Error::Other(e.to_string())) - .and_then(|r| r.map_err(|e| crate::error::Error::Other(e.to_string()))) + server_handle.abort(); + Ok(()) } From e1877e8288a8ca0cfba09547a38880671c332da4 Mon Sep 17 00:00:00 2001 From: themartto Date: Fri, 29 May 2026 10:38:33 +0300 Subject: [PATCH 04/14] docs: document system.md identity, default_skills, and updated prompt structure --- README.md | 52 ++++++++++++++++++++++---- docs/architecture.md | 16 +++++--- docs/configuration.md | 4 ++ docs/skills.md | 87 +++++++++++++++++++++++++++++++------------ 4 files changed, 121 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 47565db..fcba6fa 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,8 @@ Openheim is built in Rust from the ground up: - **Tool execution** — built-in shell, file read, and file write tools. Trait-based, so you can add your own. - **MCP (Model Context Protocol)** — connect external MCP servers (stdio or Streamable HTTP) and their tools are automatically exposed to the LLM as `{server_name}__{tool_name}`. - **Conversation memory** — conversations (including full tool call history) persist to disk and resume across sessions -- **Skills** — drop a markdown file into `~/.openheim/skills/` and it's prepended to the system prompt. ACP clients can also pass skills per-session via `_meta`. +- **System identity** — edit `~/.openheim/system.md` to define how the agent presents itself. Required at startup (created by `openheim init`). +- **Skills** — drop a markdown file into `~/.openheim/skills/` and it's injected into the system prompt. Set `default_skills` in config to auto-load skills every session; pass `--skills` for per-session additions. ACP clients can also pass skills per-session via `_meta`. - **ACP transport** — implements the [Agent Client Protocol](https://github.com/block/agent-client-protocol) over stdio (for editor integrations) and WebSocket (for remote clients), with real-time streaming of message chunks and tool calls - **Unified WebSocket** — single multiplexed `WS /ws` connection carries both ACP agent traffic (sessions, streaming, tool calls) and filesystem operations (file CRUD, live watching) via channel envelopes - **Retry with backoff** — transient failures (429s, 5xx, network errors) are retried automatically with exponential backoff @@ -79,11 +80,12 @@ cargo build --release ### Configure ```bash -# Generate the default config +# Generate the default config and system.md cargo run -- init -# Edit it +# Edit them vim ~/.openheim/config.toml +vim ~/.openheim/system.md ``` Example config: @@ -92,6 +94,9 @@ Example config: default_provider = "anthropic" max_iterations = 10 +# Skills loaded in every new session automatically (no --skills flag needed) +# default_skills = ["rules"] + [providers.anthropic] api_base = "https://api.anthropic.com/v1" default_model = "claude-sonnet-4-6" @@ -170,15 +175,46 @@ Conversations are saved to `~/.openheim/history/` as JSON after every run. --- -## Skills +## Agent identity and skills + +### `~/.openheim/system.md` + +This file defines the agent's base identity. It is loaded on every session and is required — run `openheim init` to create it, then edit it freely. + +```markdown +You are a senior software engineer who writes clean, idiomatic code. +You prefer simple solutions and ask clarifying questions before making large changes. +``` -Skills are markdown files in `~/.openheim/skills/`. When loaded, their content is injected into the system prompt before the conversation starts. +### Skills -Use them to give the agent a persona, a set of coding standards, domain knowledge, or anything you'd otherwise paste into the system prompt every time. +Skills are markdown files in `~/.openheim/skills/`. They are injected into the system prompt after the identity block. ```bash -# Run the REPL with specific skills loaded +# Run with specific skills for this session cargo run -- --skills rust,debugging + +# Always load certain skills (set in config.toml) +# default_skills = ["rules", "concise"] +``` + +The system message the LLM receives is assembled in this order: + +``` +You are a general purpose multiprovider LLM agent. + +--- + +The user has given you the following identity: + + + +--- + +These are the skills you have mastered: + +### rust + ``` ACP clients (Zed, Claude Code, etc.) can pass skills per-session by including a `skills` array in the `_meta` field of the `NewSession` request — no flag needed on the server side. @@ -284,7 +320,7 @@ src/ mcp/ MCP (Model Context Protocol) client integration client.rs MCP server connection (stdio + Streamable HTTP) tool_handler.rs Adapts MCP tools to the ToolHandler trait - rag/ Conversation history, prompt builder, and skills manager + rag/ Conversation history, prompt builder, skills manager, and system identity acp/ ACP agent core — session state and protocol handling transport/ stdio.rs ACP-over-stdio transport (for editor integrations) diff --git a/docs/architecture.md b/docs/architecture.md index 2c64dfc..d4ace71 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -44,10 +44,11 @@ src/ ├── error.rs Unified Error / Result types │ ├── rag/ Retrieval-Augmented Generation utilities -│ ├── mod.rs RagContext — history + skills bundled together +│ ├── mod.rs RagContext — history + skills + system identity │ ├── history.rs HistoryManager — conversation persistence │ ├── skills.rs SkillsManager — Markdown skill files -│ └── prompt.rs PromptBuilder — injects skills as a system message +│ ├── system.rs SystemLoader — reads ~/.openheim/system.md +│ └── prompt.rs PromptBuilder — assembles structured system message │ ├── tools/ Tool abstraction and built-in implementations │ ├── mod.rs ToolHandler / ToolExecutor traits, SystemToolExecutor @@ -108,8 +109,10 @@ User / Client │ rag::RagContext::prepare │ │ │ │ 1. Load conversation from disk (history) │ -│ 2. Load skill files by name │ -│ 3. Build PromptBuilder (system message) │ +│ 2. Load ~/.openheim/system.md (identity) │ +│ 3. Merge default_skills + session skills │ +│ 4. Load skill files by name │ +│ 5. Build PromptBuilder (system message) │ │ │ │ Returns: (Conversation, PromptBuilder) │ └────────────────────┬────────────────────────┘ @@ -160,7 +163,8 @@ All persistence lives under `~/.openheim/` by default. ``` ~/.openheim/ -├── config.toml Agent configuration (providers, MCP servers, …) +├── config.toml Agent configuration (providers, MCP servers, default_skills, …) +├── system.md Agent identity — loaded on every session (required) ├── history/ │ ├── {uuid}.json One file per conversation │ └── … @@ -169,7 +173,7 @@ All persistence lives under `~/.openheim/` by default. └── … ``` -`HistoryManager` reads and writes conversation JSON files. `SkillsManager` reads `.md` files from the skills directory. Both paths are configurable at construction time, which is how the test suite uses temporary directories. +`SystemLoader` reads `system.md` on every `prepare()` call — missing file is a hard error (run `openheim init` to create it). `HistoryManager` reads and writes conversation JSON files. `SkillsManager` reads `.md` files from the skills directory. Both history and skills paths are configurable at construction time, which is how the test suite uses temporary directories. --- diff --git a/docs/configuration.md b/docs/configuration.md index fa89423..c62fb1a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -14,11 +14,15 @@ cargo run -- init |---|---|---|---| | `default_provider` | string | — | Provider to use when no `--model` override is given (must match a key under `[providers]`) | | `max_iterations` | integer | `10` | Maximum number of agent loop iterations per prompt before stopping | +| `default_skills` | string[] | `[]` | Skills loaded automatically in every new session. Merged with per-session `--skills`; defaults appear first, duplicates removed. | | `theme_color` | string | `"white"` | TUI accent color. Valid values: `white`, `gray`, `blue`, `cyan`, `magenta`, `green`, `yellow`, `red`, `pink`. Can also be changed at runtime with `:theme` | ```toml default_provider = "anthropic" max_iterations = 20 + +# Always load these skills without passing --skills each time +default_skills = ["rules", "concise"] ``` --- diff --git a/docs/skills.md b/docs/skills.md index e9dd71b..72cffe3 100644 --- a/docs/skills.md +++ b/docs/skills.md @@ -1,10 +1,33 @@ -# Skills +# Skills & Agent Identity -Skills are Markdown files that inject system-level instructions into every LLM request in a session. They let you shape the agent's behaviour — tone, domain expertise, constraints, output format — without touching code or configuration. +This document covers two related concepts: + +- **System identity** (`~/.openheim/system.md`) — the agent's base character, loaded on every session. +- **Skills** (`~/.openheim/skills/*.md`) — domain-specific instructions injected on demand. + +--- + +## Agent Identity (`system.md`) + +`~/.openheim/system.md` defines who the agent is. It is loaded at the start of every session and is **required** — openheim will error on startup if the file is missing. + +Run `openheim init` to create it with a default starting point, then edit it to match how you want the agent to behave: + +```markdown +You are a senior software engineer specialising in Rust and distributed systems. +You write clean, idiomatic code and prefer simple solutions over clever ones. +Ask clarifying questions before making large changes. +``` + +The identity is placed at the top of the system message, before any skills. --- -## Where skills live +## Skills + +Skills are Markdown files that inject domain-specific instructions into the system prompt. They let you shape the agent's behaviour — tone, domain expertise, constraints, output format — without touching code or configuration. + +### Where skills live Skills are stored as `.md` files in `~/.openheim/skills/`: @@ -19,23 +42,32 @@ The filename (without extension) is the skill name. Names are case-sensitive and --- -## How they work +## How the system message is assembled -When a session starts with one or more skills, `SkillsManager` reads each file and passes the content to `PromptBuilder`. Before each LLM call, the builder prepends a single system message containing all active skills, joined by `---` separators: +When a session has an identity and skills, the LLM receives a single system message structured like this: ``` -[System message] -## Skill: rust +You are a general purpose multiprovider LLM agent. + +--- -You are an expert Rust programmer. Always prefer idiomatic Rust… +The user has given you the following identity: + + --- -## Skill: tdd +These are the skills you have mastered: + +### rust -Write tests first. Every function should have at least one test… + + +--- -[Conversation history follows] +### tdd + + ``` The system message is reconstructed on every LLM call in the session, so it is always present regardless of how long the conversation runs. @@ -72,21 +104,23 @@ This skill is about systems programming. The assistant knows Rust and C. ## Enabling skills -### Via CLI +### Default skills (loaded every session) -Pass `--skills` as a comma-separated list of skill names: +Set `default_skills` in `~/.openheim/config.toml` to load skills automatically in every new session — no `--skills` flag needed: -```bash -openheim --skills rust,tdd -openheim run --skills concise "Summarise the project structure" +```toml +default_skills = ["rules", "concise"] ``` -### Via configuration +Default skills are merged with any per-session skills. Duplicates are deduplicated; defaults always appear first. -Set skills globally in `~/.openheim/config.toml` so they apply to every session: +### Per-session via CLI -```toml -skills = ["rust", "concise"] +Pass `--skills` as a comma-separated list of skill names: + +```bash +openheim --skills rust,tdd +openheim run --skills concise "Summarise the project structure" ``` ### Via the Rust library @@ -108,9 +142,6 @@ Skills are persisted in the conversation metadata (`ConversationMeta.skills`), s ## Listing available skills ```bash -# CLI -openheim --skills "" # lists available skills on startup (TUI) - # API (REST when running openheim serve) curl http://localhost:1217/api/skills # → ["concise","rust","tdd"] @@ -125,7 +156,15 @@ let skills = client.rag().skills.list_skills()?; --- -## Example skills +## Example files + +### `~/.openheim/system.md` + +```markdown +You are a pragmatic software engineer who writes clean, well-tested code. +You prefer simple solutions and always consider the maintenance burden of your suggestions. +When you are uncertain, say so rather than guessing. +``` ### `~/.openheim/skills/concise.md` From d73082ab02efce6406a13211a0f9060fdba8e5fa Mon Sep 17 00:00:00 2001 From: themartto Date: Sat, 30 May 2026 12:06:06 +0300 Subject: [PATCH 05/14] docs: use openheim binary in examples and clarify system.md session wording --- README.md | 28 ++++++++++++++-------------- docs/configuration.md | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index fcba6fa..a76ea8e 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ Openheim is built in Rust from the ground up: - **Tool execution** — built-in shell, file read, and file write tools. Trait-based, so you can add your own. - **MCP (Model Context Protocol)** — connect external MCP servers (stdio or Streamable HTTP) and their tools are automatically exposed to the LLM as `{server_name}__{tool_name}`. - **Conversation memory** — conversations (including full tool call history) persist to disk and resume across sessions -- **System identity** — edit `~/.openheim/system.md` to define how the agent presents itself. Required at startup (created by `openheim init`). +- **System identity** — edit `~/.openheim/system.md` to define how the agent presents itself. Required when preparing a session (created by `openheim init`). - **Skills** — drop a markdown file into `~/.openheim/skills/` and it's injected into the system prompt. Set `default_skills` in config to auto-load skills every session; pass `--skills` for per-session additions. ACP clients can also pass skills per-session via `_meta`. - **ACP transport** — implements the [Agent Client Protocol](https://github.com/block/agent-client-protocol) over stdio (for editor integrations) and WebSocket (for remote clients), with real-time streaming of message chunks and tool calls - **Unified WebSocket** — single multiplexed `WS /ws` connection carries both ACP agent traffic (sessions, streaming, tool calls) and filesystem operations (file CRUD, live watching) via channel envelopes @@ -81,7 +81,7 @@ cargo build --release ```bash # Generate the default config and system.md -cargo run -- init +openheim init # Edit them vim ~/.openheim/config.toml @@ -134,26 +134,26 @@ models = ["llama3", "mistral", "codellama"] ```bash # Interactive REPL (default — no subcommand) -cargo run +openheim # Load skills in the REPL -cargo run -- --skills rust,debugging +openheim --skills rust,debugging # Single headless prompt, streams to stdout -cargo run -- run "List the files in the current directory" +openheim run "List the files in the current directory" # Single headless prompt with a model override -cargo run -- run "Hello" --model gpt-4o +openheim run "Hello" --model gpt-4o # ACP stdio agent (for Zed, Claude Code, and other ACP clients) -cargo run -- acp +openheim acp # ACP-over-WebSocket server -cargo run -- serve -cargo run -- serve --host 0.0.0.0 --port 1217 +openheim serve +openheim serve --host 0.0.0.0 --port 1217 # Initialize config -cargo run -- init +openheim init ``` --- @@ -179,7 +179,7 @@ Conversations are saved to `~/.openheim/history/` as JSON after every run. ### `~/.openheim/system.md` -This file defines the agent's base identity. It is loaded on every session and is required — run `openheim init` to create it, then edit it freely. +This file defines the agent's base identity. It is loaded when preparing each session (via `prepare()` / session setup) and is required — run `openheim init` to create it, then edit it freely. ```markdown You are a senior software engineer who writes clean, idiomatic code. @@ -192,7 +192,7 @@ Skills are markdown files in `~/.openheim/skills/`. They are injected into the s ```bash # Run with specific skills for this session -cargo run -- --skills rust,debugging +openheim --skills rust,debugging # Always load certain skills (set in config.toml) # default_skills = ["rules", "concise"] @@ -223,7 +223,7 @@ ACP clients (Zed, Claude Code, etc.) can pass skills per-session by including a ## Server mode -Start with `cargo run -- serve` (defaults to `0.0.0.0:1217`). +Start with `openheim serve` (defaults to `0.0.0.0:1217`). The server speaks the [Agent Client Protocol](https://github.com/block/agent-client-protocol) over WebSocket and exposes a multiplexed WS endpoint plus REST API routes: @@ -334,7 +334,7 @@ src/ ## Development ```bash -RUST_LOG=debug cargo run -- run "test" +RUST_LOG=debug openheim run "test" cargo test cargo fmt --check cargo clippy diff --git a/docs/configuration.md b/docs/configuration.md index c62fb1a..232fc4a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -3,7 +3,7 @@ Openheim loads its configuration from `~/.openheim/config.toml`. Generate a default file with: ```bash -cargo run -- init +openheim init ``` --- From a5cc9cc09e254622836958debd36cfaa248c7c15 Mon Sep 17 00:00:00 2001 From: themartto Date: Sat, 30 May 2026 12:06:11 +0300 Subject: [PATCH 06/14] fix: report accurate system.md creation status in init error message --- src/config/mod.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index df56f3f..ce9d4da 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -39,16 +39,22 @@ pub fn init_config() -> Result { // Always write system.md first so existing users who re-run `init` get it // even though config.toml already exists and will cause an early return below. let system_path = dir.join("system.md"); - if !system_path.exists() { + let system_written = !system_path.exists(); + if system_written { std::fs::write(&system_path, DEFAULT_SYSTEM_MD)?; } let config_path = dir.join("config.toml"); if config_path.exists() { + let system_note = if system_written { + format!("system.md has been created at {}.", system_path.display()) + } else { + format!("system.md is available at {}.", system_path.display()) + }; return Err(Error::config(format!( - "Config file already exists at {}. system.md has been created at {}.", + "Config file already exists at {}. {}", config_path.display(), - system_path.display() + system_note ))); } std::fs::write(&config_path, DEFAULT_CONFIG)?; From e2ee0a49cb9ea3721abaad03cdd42fadad1be7e0 Mon Sep 17 00:00:00 2001 From: themartto Date: Sat, 30 May 2026 12:06:15 +0300 Subject: [PATCH 07/14] fix: deduplicate within defaults in merge_skills --- src/rag/mod.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/rag/mod.rs b/src/rag/mod.rs index 4c703c8..ad85322 100644 --- a/src/rag/mod.rs +++ b/src/rag/mod.rs @@ -86,8 +86,8 @@ impl RagContext { /// Merge `defaults` with `session` skills, preserving order and deduplicating. /// Defaults come first; session skills are appended only if not already present. fn merge_skills(defaults: &[String], session: &[String]) -> Vec { - let mut merged = defaults.to_vec(); - for s in session { + let mut merged = Vec::new(); + for s in defaults.iter().chain(session.iter()) { if !merged.contains(s) { merged.push(s.clone()); } @@ -118,4 +118,11 @@ mod tests { let merged = merge_skills(&["rules".to_string()], &[]); assert_eq!(merged, vec!["rules"]); } + + #[test] + fn merge_skills_deduplicates_within_defaults() { + let defaults = vec!["rules".to_string(), "rules".to_string()]; + let merged = merge_skills(&defaults, &[]); + assert_eq!(merged, vec!["rules"]); + } } From 3d2e99a7065bf16c87419329527b83b3e0377cdd Mon Sep 17 00:00:00 2001 From: themartto Date: Sat, 30 May 2026 12:06:21 +0300 Subject: [PATCH 08/14] fix: preserve leading/trailing whitespace in system identity prompt --- src/rag/prompt.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/rag/prompt.rs b/src/rag/prompt.rs index e306a4c..928ea26 100644 --- a/src/rag/prompt.rs +++ b/src/rag/prompt.rs @@ -52,11 +52,8 @@ impl PromptBuilder { /// /// ``` pub fn build(&self, history: &[Message]) -> Vec { - let identity = self - .system_identity - .as_deref() - .map(str::trim) - .filter(|s| !s.is_empty()); + let orig = self.system_identity.as_deref(); + let identity = orig.filter(|s| !s.trim().is_empty()); let has_content = identity.is_some() || !self.skills.is_empty(); if !has_content { From 56a83f541f013cc1cd76f22211783a4dce4a476a Mon Sep 17 00:00:00 2001 From: themartto Date: Sat, 30 May 2026 12:06:26 +0300 Subject: [PATCH 09/14] feat: add default_skills field to OpenheimBuilder --- src/client.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 67ec990..39cbdfd 100644 --- a/src/client.rs +++ b/src/client.rs @@ -239,6 +239,7 @@ pub struct OpenheimBuilder { timeout_secs: Option, max_tokens: Option, mcp_servers: BTreeMap, + default_skills: Vec, } impl OpenheimBuilder { @@ -297,6 +298,12 @@ impl OpenheimBuilder { self } + /// Skills loaded automatically in every new session. + pub fn default_skills(mut self, skills: Vec) -> Self { + self.default_skills = skills; + self + } + /// Build the client, connecting to MCP servers and initialising the agent state. pub async fn build(self) -> Result { let (agent_config, mut app_config) = if self.provider.is_some() @@ -312,6 +319,7 @@ impl OpenheimBuilder { self.max_iterations, self.timeout_secs, self.max_tokens, + self.default_skills.clone(), ) } else { let app_config = match self.config_path { @@ -336,12 +344,18 @@ impl OpenheimBuilder { app_config.mcp_servers.insert(name, cfg); } + // Apply builder default_skills for the file-based path (programmatic path sets them directly) + if !self.default_skills.is_empty() { + app_config.default_skills = self.default_skills; + } + let rag = RagContext::new(app_config.default_skills.clone())?; let state = Arc::new(AgentState::new(agent_config, app_config, rag).await?); Ok(OpenheimClient { state }) } } +#[allow(clippy::too_many_arguments)] fn build_programmatic( provider: Option, api_key: Option, @@ -350,6 +364,7 @@ fn build_programmatic( max_iterations: Option, timeout_secs: Option, max_tokens: Option, + default_skills: Vec, ) -> (AgentConfig, AppConfig) { let provider = provider.unwrap_or_else(|| "openai".to_string()); let api_base = api_base.unwrap_or_else(|| default_api_base(&provider)); @@ -378,7 +393,7 @@ fn build_programmatic( theme_color: None, providers, mcp_servers: BTreeMap::new(), - default_skills: vec![], + default_skills, }; let agent_config = AgentConfig { From 516004b06e0d7c729dabee5a7a1071b5d99968a1 Mon Sep 17 00:00:00 2001 From: themartto Date: Sun, 31 May 2026 23:10:27 +0300 Subject: [PATCH 10/14] feat: add work_dir sandbox and allow_shell security controls --- src/acp/mod.rs | 23 +++++- src/config/client.rs | 2 + src/config/config.toml.default | 8 ++ src/config/resolve.rs | 4 + src/config/types.rs | 13 ++++ src/tools/mod.rs | 11 +++ src/tools/sandbox.rs | 113 +++++++++++++++++++++++++++ src/tools/sandboxed_executor.rs | 133 ++++++++++++++++++++++++++++++++ 8 files changed, 304 insertions(+), 3 deletions(-) create mode 100644 src/tools/sandbox.rs create mode 100644 src/tools/sandboxed_executor.rs diff --git a/src/acp/mod.rs b/src/acp/mod.rs index 6a046fc..2135527 100644 --- a/src/acp/mod.rs +++ b/src/acp/mod.rs @@ -31,7 +31,7 @@ use crate::{ error::{Error, Result}, llm::LlmClient, rag::RagContext, - tools::{SystemToolExecutor, ToolExecutor}, + tools::{SandboxedExecutor, SystemToolExecutor, ToolExecutor}, }; use session::SessionState; @@ -45,6 +45,10 @@ pub struct AgentState { pub app_config: AppConfig, pub rag: RagContext, pub mcp_statuses: Vec, + /// Resolved work directory used as the sandbox boundary for every session. + pub work_dir: PathBuf, + /// Whether shell command execution is enabled for the LLM. + pub allow_shell: bool, sessions: Sessions, } @@ -52,8 +56,14 @@ impl AgentState { pub async fn new(config: AgentConfig, app_config: AppConfig, rag: RagContext) -> Result { let http_client = build_http_client(config.timeout_secs)?; let llm = create_client(&config, &http_client); - let (sys_executor, mcp_statuses) = SystemToolExecutor::build(&app_config.mcp_servers).await; + let allow_shell = app_config.allow_shell; + let (sys_executor, mcp_statuses) = + SystemToolExecutor::build(&app_config.mcp_servers, allow_shell).await; let executor = Arc::new(sys_executor) as Arc; + let work_dir = app_config + .work_dir + .clone() + .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))); Ok(Self { llm, executor, @@ -61,6 +71,8 @@ impl AgentState { app_config, rag, mcp_statuses, + work_dir, + allow_shell, sessions: Arc::new(RwLock::new(HashMap::new())), }) } @@ -162,9 +174,14 @@ impl AgentState { } else { self.llm.clone() }; + let sandboxed = Arc::new(SandboxedExecutor::new( + self.executor.clone(), + self.work_dir.clone(), + self.allow_shell, + )) as Arc; ( llm, - self.executor.clone(), + sandboxed, s.config.clone(), s.chat_id, s.skills.clone(), diff --git a/src/config/client.rs b/src/config/client.rs index 9a363ea..2596d90 100644 --- a/src/config/client.rs +++ b/src/config/client.rs @@ -122,6 +122,8 @@ mod tests { providers, mcp_servers: BTreeMap::new(), default_skills: vec![], + work_dir: None, + allow_shell: true, } } diff --git a/src/config/config.toml.default b/src/config/config.toml.default index e978537..005337f 100644 --- a/src/config/config.toml.default +++ b/src/config/config.toml.default @@ -11,6 +11,14 @@ max_iterations = 10 # Add skill names matching files in ~/.openheim/skills/{name}.md # default_skills = ["rules"] +# Root directory the agent is allowed to read/write. +# When unset the directory from which openheim is invoked is used. +# work_dir = "/home/user/projects/myproject" + +# Whether to allow the LLM to execute shell commands via the execute_command tool. +# Set to false to remove the tool from the LLM's view entirely. +# allow_shell = true + # --- Provider Configuration --- # # The provider name determines which API client is used: diff --git a/src/config/resolve.rs b/src/config/resolve.rs index 8abaa2a..6625c4f 100644 --- a/src/config/resolve.rs +++ b/src/config/resolve.rs @@ -150,6 +150,8 @@ mod tests { providers, mcp_servers: BTreeMap::new(), default_skills: vec![], + work_dir: None, + allow_shell: true, } } @@ -191,6 +193,8 @@ mod tests { providers: BTreeMap::new(), mcp_servers: BTreeMap::new(), default_skills: vec![], + work_dir: None, + allow_shell: true, }; let err = config.resolve(None).unwrap_err(); assert!(err.to_string().contains("nonexistent")); diff --git a/src/config/types.rs b/src/config/types.rs index 969abda..e22fde0 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -1,5 +1,6 @@ use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, HashMap}; +use std::path::PathBuf; use std::sync::Arc; /// Public model info for a single provider (no credentials). @@ -41,6 +42,18 @@ pub struct AppConfig { /// Skills loaded automatically in every new session (merged with --skills at runtime). #[serde(default)] pub default_skills: Vec, + /// Root directory the agent is allowed to read/write. Defaults to the + /// directory from which openheim was invoked when not set. + #[serde(default)] + pub work_dir: Option, + /// Whether to expose the `execute_command` shell tool to the LLM. + /// Defaults to `true`. Set to `false` to disable shell access entirely. + #[serde(default = "default_allow_shell")] + pub allow_shell: bool, +} + +fn default_allow_shell() -> bool { + true } /// Configuration for a single MCP server connection. diff --git a/src/tools/mod.rs b/src/tools/mod.rs index 5b43ec1..8685c67 100644 --- a/src/tools/mod.rs +++ b/src/tools/mod.rs @@ -52,6 +52,8 @@ mod execute_command; mod read_file; +pub mod sandbox; +mod sandboxed_executor; mod write_file; use std::collections::{BTreeMap, HashMap}; @@ -62,6 +64,8 @@ use crate::config::McpServerConfig; use crate::core::models::Tool; use crate::error::{Error, Result}; +pub use sandboxed_executor::SandboxedExecutor; + #[async_trait] pub trait ToolHandler: Send + Sync { /// Returns the tool definition (name, description, JSON-schema parameters). @@ -107,13 +111,20 @@ impl SystemToolExecutor { /// Builds a fully-configured executor: registers built-in tools then connects /// to all configured MCP servers and registers their tools. /// + /// When `allow_shell` is `false` the `execute_command` tool is omitted so + /// the LLM never sees it in its tool list. + /// /// Returns the executor alongside [`McpServerStatus`] entries for each server /// so callers can inspect which connections succeeded. pub async fn build( mcp_configs: &BTreeMap, + allow_shell: bool, ) -> (Self, Vec) { let mut executor = Self::new(); executor.register_builtins(); + if !allow_shell { + executor.handlers.remove("execute_command"); + } let (handlers, statuses) = crate::mcp::load_mcp_tools(mcp_configs).await; for handler in handlers { executor.register(handler); diff --git a/src/tools/sandbox.rs b/src/tools/sandbox.rs new file mode 100644 index 0000000..6e27962 --- /dev/null +++ b/src/tools/sandbox.rs @@ -0,0 +1,113 @@ +//! Work-directory path validation for the agent sandbox. + +use std::path::{Path, PathBuf}; + +use crate::error::{Error, Result}; + +/// Validates that `requested` resolves to a path within `work_dir`. +/// +/// Relative paths are resolved against `work_dir`. For paths that already +/// exist symlinks are followed and the canonicalized result is checked. +/// For paths that do not yet exist (e.g. a file about to be written) the +/// nearest existing ancestor is canonicalized and checked instead. +/// +/// Returns the resolved absolute path on success, or an error describing +/// why the path is rejected. +pub fn validate_path(requested: &str, work_dir: &Path) -> Result { + let work_dir_canonical = work_dir.canonicalize().map_err(|_| { + Error::ToolExecutionError(format!( + "work directory '{}' is inaccessible", + work_dir.display() + )) + })?; + + let requested_path = Path::new(requested); + let resolved = if requested_path.is_absolute() { + requested_path.to_path_buf() + } else { + work_dir_canonical.join(requested_path) + }; + + let check = if resolved.exists() { + resolved.canonicalize().map_err(Error::IoError)? + } else { + // Walk up the tree until we find an existing ancestor, canonicalize + // that, and verify it is within the work directory. + let mut ancestor: &Path = &resolved; + loop { + ancestor = ancestor.parent().ok_or_else(|| { + Error::ToolExecutionError(format!( + "path '{}' has no accessible ancestor within the filesystem", + requested + )) + })?; + if ancestor.exists() { + let canonical_ancestor = ancestor.canonicalize().map_err(Error::IoError)?; + if !canonical_ancestor.starts_with(&work_dir_canonical) { + return Err(Error::ToolExecutionError(format!( + "path '{}' is outside the work directory '{}'", + requested, + work_dir.display() + ))); + } + // The non-existing tail of the path is fine; return it as-is + // so the caller (write_file) can create it. + return Ok(resolved); + } + } + }; + + if check.starts_with(&work_dir_canonical) { + Ok(check) + } else { + Err(Error::ToolExecutionError(format!( + "path '{}' is outside the work directory '{}'", + requested, + work_dir.display() + ))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + #[test] + fn allows_existing_file_inside_work_dir() { + let dir = tempfile::tempdir().unwrap(); + let file = dir.path().join("foo.txt"); + fs::write(&file, "x").unwrap(); + assert!(validate_path(file.to_str().unwrap(), dir.path()).is_ok()); + } + + #[test] + fn allows_relative_path_inside() { + let dir = tempfile::tempdir().unwrap(); + fs::create_dir(dir.path().join("sub")).unwrap(); + assert!(validate_path("sub", dir.path()).is_ok()); + } + + #[test] + fn allows_new_file_path_inside_work_dir() { + let dir = tempfile::tempdir().unwrap(); + assert!(validate_path("new_file.txt", dir.path()).is_ok()); + } + + #[test] + fn rejects_absolute_path_outside_work_dir() { + let dir = tempfile::tempdir().unwrap(); + let other = tempfile::tempdir().unwrap(); + let outside = other.path().join("secret.txt"); + fs::write(&outside, "x").unwrap(); + let err = validate_path(outside.to_str().unwrap(), dir.path()).unwrap_err(); + assert!(err.to_string().contains("outside the work directory")); + } + + #[test] + fn rejects_dotdot_traversal() { + let dir = tempfile::tempdir().unwrap(); + let err = validate_path("../../etc/passwd", dir.path()).unwrap_err(); + assert!(err.to_string().contains("outside the work directory")); + } +} diff --git a/src/tools/sandboxed_executor.rs b/src/tools/sandboxed_executor.rs new file mode 100644 index 0000000..557da1b --- /dev/null +++ b/src/tools/sandboxed_executor.rs @@ -0,0 +1,133 @@ +//! Work-directory sandboxing wrapper around any [`ToolExecutor`]. + +use std::{path::PathBuf, sync::Arc}; + +use async_trait::async_trait; +use tokio::{fs, process::Command}; + +use crate::{ + core::models::Tool, + error::{Error, Result}, +}; + +use super::{ToolExecutor, sandbox::validate_path}; + +/// Wraps an inner [`ToolExecutor`] and enforces a work-directory boundary. +/// +/// The three built-in tools are intercepted: +/// - `read_file` / `write_file`: the requested path is validated to be within +/// `work_dir` (following symlinks for existing paths); access outside the +/// boundary is rejected with an error the LLM can read and react to. +/// - `execute_command`: when `allow_shell` is `false` the call is rejected +/// immediately. When `true` the command runs with its working directory set +/// to `work_dir` so relative paths behave correctly. Note that absolute +/// paths inside the shell command are not blocked at the application layer +/// — OS-level sandboxing is required for that. +/// +/// All other tools are forwarded to the inner executor unchanged. +pub struct SandboxedExecutor { + inner: Arc, + work_dir: Arc, + allow_shell: bool, +} + +impl SandboxedExecutor { + pub fn new(inner: Arc, work_dir: PathBuf, allow_shell: bool) -> Self { + Self { + inner, + work_dir: Arc::new(work_dir), + allow_shell, + } + } +} + +#[async_trait] +impl ToolExecutor for SandboxedExecutor { + fn list_tools(&self) -> Vec { + self.inner.list_tools() + } + + async fn execute(&self, name: &str, args_json: &str) -> Result { + match name { + "read_file" => { + let args: serde_json::Value = serde_json::from_str(args_json) + .map_err(|e| Error::ParseError(format!("failed to parse arguments: {}", e)))?; + let path = args["path"] + .as_str() + .ok_or_else(|| Error::ParseError("missing 'path' argument".to_string()))?; + let validated = validate_path(path, &self.work_dir)?; + let content = fs::read_to_string(&validated) + .await + .map_err(Error::IoError)?; + Ok(content) + } + + "write_file" => { + let args: serde_json::Value = serde_json::from_str(args_json) + .map_err(|e| Error::ParseError(format!("failed to parse arguments: {}", e)))?; + let path = args["path"] + .as_str() + .ok_or_else(|| Error::ParseError("missing 'path' argument".to_string()))?; + let content = args["content"] + .as_str() + .ok_or_else(|| Error::ParseError("missing 'content' argument".to_string()))?; + let validated = validate_path(path, &self.work_dir)?; + if let Some(parent) = validated.parent() + && !parent.as_os_str().is_empty() + { + fs::create_dir_all(parent).await.map_err(Error::IoError)?; + } + fs::write(&validated, content) + .await + .map_err(Error::IoError)?; + Ok(format!("Successfully wrote to {}", validated.display())) + } + + "execute_command" => { + if !self.allow_shell { + return Err(Error::ToolExecutionError( + "shell command execution is disabled by configuration".to_string(), + )); + } + let args: serde_json::Value = serde_json::from_str(args_json) + .map_err(|e| Error::ParseError(format!("failed to parse arguments: {}", e)))?; + let command = args["command"] + .as_str() + .ok_or_else(|| Error::ParseError("missing 'command' argument".to_string()))?; + + #[cfg(target_family = "unix")] + let mut cmd = { + let mut c = Command::new("sh"); + c.arg("-c").arg(command); + c + }; + #[cfg(target_family = "windows")] + let mut cmd = { + let mut c = Command::new("cmd"); + c.arg("/C").arg(command); + c + }; + + cmd.current_dir(&*self.work_dir); + + let output = cmd.output().await.map_err(|e| { + Error::ToolExecutionError(format!("failed to execute command: {}", e)) + })?; + + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + + if output.status.success() { + Ok(stdout) + } else { + Err(Error::ToolExecutionError(format!( + "Command failed:\nStdout: {}\nStderr: {}", + stdout, stderr + ))) + } + } + + _ => self.inner.execute(name, args_json).await, + } + } +} From eaf2b24c4dac0fc9e7d0e016e74ea7e2b648086e Mon Sep 17 00:00:00 2001 From: themartto Date: Sun, 31 May 2026 23:10:32 +0300 Subject: [PATCH 11/14] feat: expose work_dir and allow_shell in OpenheimBuilder --- src/client.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/client.rs b/src/client.rs index 39cbdfd..995e5ed 100644 --- a/src/client.rs +++ b/src/client.rs @@ -240,6 +240,8 @@ pub struct OpenheimBuilder { max_tokens: Option, mcp_servers: BTreeMap, default_skills: Vec, + work_dir: Option, + allow_shell: Option, } impl OpenheimBuilder { @@ -304,6 +306,21 @@ impl OpenheimBuilder { self } + /// Root directory the agent is allowed to read/write. + /// Overrides `work_dir` from the config file. When not set, defaults to the + /// directory from which the process was invoked. + pub fn work_dir(mut self, path: impl Into) -> Self { + self.work_dir = Some(path.into()); + self + } + + /// Whether to expose the `execute_command` shell tool to the LLM. + /// Overrides `allow_shell` from the config file. Defaults to `true`. + pub fn allow_shell(mut self, allow: bool) -> Self { + self.allow_shell = Some(allow); + self + } + /// Build the client, connecting to MCP servers and initialising the agent state. pub async fn build(self) -> Result { let (agent_config, mut app_config) = if self.provider.is_some() @@ -349,6 +366,13 @@ impl OpenheimBuilder { app_config.default_skills = self.default_skills; } + if let Some(wd) = self.work_dir { + app_config.work_dir = Some(wd); + } + if let Some(shell) = self.allow_shell { + app_config.allow_shell = shell; + } + let rag = RagContext::new(app_config.default_skills.clone())?; let state = Arc::new(AgentState::new(agent_config, app_config, rag).await?); Ok(OpenheimClient { state }) @@ -394,6 +418,8 @@ fn build_programmatic( providers, mcp_servers: BTreeMap::new(), default_skills, + work_dir: None, + allow_shell: true, }; let agent_config = AgentConfig { From b0036039864af55af47c91c7dd11b37d47ad1c61 Mon Sep 17 00:00:00 2001 From: themartto Date: Sun, 31 May 2026 23:10:36 +0300 Subject: [PATCH 12/14] docs: document work_dir, allow_shell, and update changelog --- CHANGELOG.md | 25 +++++++++++++++++++++++++ README.md | 9 +++++++++ docs/configuration.md | 20 ++++++++++++++++++++ docs/library.md | 20 ++++++++++++++++++++ 4 files changed, 74 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39fb87f..e8f9bee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## [Unreleased] + +### Added + +- **System identity (`system.md`)** — `~/.openheim/system.md` defines the agent's base identity and is injected into the system prompt on every session. `openheim init` creates a default file. The prompt is now structured: identity block first, then skills, separated by clear section headers. +- **`default_skills` in config** — new `default_skills` array in `config.toml` auto-loads a set of skills into every session without passing `--skills` each time. Per-session skills are merged on top; duplicates are removed with defaults appearing first. +- **`default_skills` in `OpenheimBuilder`** — `.default_skills(vec![...])` builder method brings the same control to programmatic embeddings. +- **Work-directory sandbox** — new `work_dir` field in `config.toml` restricts `read_file` and `write_file` to a directory tree. When unset, the directory from which openheim is invoked is used. Symlinks are followed and canonicalized so they cannot be used to escape the boundary. +- **Shell access control** — new `allow_shell` boolean in `config.toml` (default `true`). When `false`, the `execute_command` tool is removed from the tool list entirely — the LLM never sees it and cannot request it. +- **Builder methods for security controls** — `OpenheimClient::builder()` gains `.work_dir(path)` and `.allow_shell(bool)`. Both override the corresponding config-file values. +- **Cross-compilation config** — `Cross.toml` added for building Linux targets from macOS. + +### Fixed + +- **MCP subprocess stderr suppression** — stderr from spawned MCP server processes no longer leaks into the terminal. +- **`run` command exits cleanly** — the process now exits after a headless `openheim run` prompt completes instead of hanging. +- **`merge_skills` deduplication** — skills within the `default_skills` list itself are now deduplicated, not just across the default/session boundary. +- **Whitespace preserved in system identity** — leading and trailing whitespace in `system.md` content is preserved when building the system prompt. +- **Accurate `init` error message** — `openheim init` now correctly reports whether `system.md` was created when the config already exists. + +### Breaking changes (library) + +- `AppConfig` gained two new public fields: `work_dir: Option` and `allow_shell: bool`. Code constructing `AppConfig` via struct literal (rather than TOML or the builder) must now supply these fields. Both have serde defaults so TOML loading is unaffected. +- `SystemToolExecutor::build` takes an additional `allow_shell: bool` argument. + ## [0.2.1] - 2026-05-28 ### Added diff --git a/README.md b/README.md index a76ea8e..b19fcd1 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ Openheim is built in Rust from the ground up: - **Multi-provider** — OpenAI, Anthropic Claude, Google Gemini, and any OpenAI-compatible endpoint (Ollama, vLLM, LM Studio, etc.) - **Tool execution** — built-in shell, file read, and file write tools. Trait-based, so you can add your own. +- **Agent sandboxing** — configurable work-directory boundary restricts file access to a directory tree. Shell execution can be disabled entirely via `allow_shell = false` in config or `.allow_shell(false)` in the builder. - **MCP (Model Context Protocol)** — connect external MCP servers (stdio or Streamable HTTP) and their tools are automatically exposed to the LLM as `{server_name}__{tool_name}`. - **Conversation memory** — conversations (including full tool call history) persist to disk and resume across sessions - **System identity** — edit `~/.openheim/system.md` to define how the agent presents itself. Required when preparing a session (created by `openheim init`). @@ -97,6 +98,12 @@ max_iterations = 10 # Skills loaded in every new session automatically (no --skills flag needed) # default_skills = ["rules"] +# Restrict the agent to a specific directory tree (defaults to invocation directory) +# work_dir = "/home/user/projects/myproject" + +# Set to false to remove the shell tool from the LLM's tool list entirely +# allow_shell = true + [providers.anthropic] api_base = "https://api.anthropic.com/v1" default_model = "claude-sonnet-4-6" @@ -317,6 +324,8 @@ src/ retry.rs Automatic retry with exponential backoff tools/ Tool trait, registry, and built-in tools execute_command.rs / read_file.rs / write_file.rs + sandbox.rs Work-directory path validation + sandboxed_executor.rs Per-session executor wrapper enforcing work_dir and allow_shell mcp/ MCP (Model Context Protocol) client integration client.rs MCP server connection (stdio + Streamable HTTP) tool_handler.rs Adapts MCP tools to the ToolHandler trait diff --git a/docs/configuration.md b/docs/configuration.md index 232fc4a..c1215f6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -16,6 +16,8 @@ openheim init | `max_iterations` | integer | `10` | Maximum number of agent loop iterations per prompt before stopping | | `default_skills` | string[] | `[]` | Skills loaded automatically in every new session. Merged with per-session `--skills`; defaults appear first, duplicates removed. | | `theme_color` | string | `"white"` | TUI accent color. Valid values: `white`, `gray`, `blue`, `cyan`, `magenta`, `green`, `yellow`, `red`, `pink`. Can also be changed at runtime with `:theme` | +| `work_dir` | path | cwd at invocation | Root directory the agent is allowed to read and write. The agent cannot access files outside this tree. When unset, defaults to the directory from which openheim was invoked. | +| `allow_shell` | boolean | `true` | Whether to expose the `execute_command` shell tool to the LLM. Set to `false` to remove the tool entirely — the LLM will not see it in its tool list. | ```toml default_provider = "anthropic" @@ -23,8 +25,20 @@ max_iterations = 20 # Always load these skills without passing --skills each time default_skills = ["rules", "concise"] + +# Restrict the agent to a specific directory tree +work_dir = "/home/user/projects/myproject" + +# Disable shell command execution +allow_shell = false ``` +### Security notes + +**`work_dir`** is enforced at the application layer for `read_file` and `write_file`. Symlinks are followed and canonicalized so they cannot be used to escape the boundary. Shell commands (`execute_command`) are launched with `work_dir` as their working directory so relative paths resolve correctly, but absolute paths inside a shell command are not blocked — OS-level sandboxing (chroot, containers) is required for full shell isolation. + +**`allow_shell`** removes `execute_command` from the tool list sent to the LLM. When `false`, the LLM never sees the tool and cannot request it. + --- ## `[providers.]` @@ -115,6 +129,12 @@ The `name` key is sanitized when building tool names: hyphens and spaces become default_provider = "anthropic" max_iterations = 15 +# Restrict the agent to this directory tree +work_dir = "/home/user/projects/myproject" + +# Disable shell command access +allow_shell = false + [providers.anthropic] api_base = "https://api.anthropic.com/v1" default_model = "claude-sonnet-4-6" diff --git a/docs/library.md b/docs/library.md index 4ae7975..dde7719 100644 --- a/docs/library.md +++ b/docs/library.md @@ -84,6 +84,26 @@ Default models when `.model()` is omitted: - `"gemini"` → `gemini-2.0-flash` - everything else → `gpt-4o` +### Security controls + +Two builder methods control the agent's access boundary. Both override the corresponding `config.toml` fields when set. + +```rust +let client = OpenheimClient::builder() + .provider("openai") + .api_key("sk-...") + // Restrict file access to this directory tree + .work_dir("/home/user/projects/myproject") + // Remove the execute_command tool from the LLM's tool list entirely + .allow_shell(false) + .build() + .await?; +``` + +**`.work_dir(path)`** — sets the root directory the agent may read and write. The agent cannot access files outside this tree. Relative paths in tool arguments are resolved against this directory. Defaults to the directory from which the process was invoked when not set in the builder or config file. + +**`.allow_shell(bool)`** — controls whether the `execute_command` tool is exposed to the LLM. When `false` the tool is removed from the tool list entirely; the LLM never sees it and cannot request it. Defaults to `true`. + ### With MCP servers MCP servers can be added in either mode. Their tools become available to the agent automatically as `{server_name}__{tool_name}`. From 070c39860e29eee043fa0c4034d49ef7b1129907 Mon Sep 17 00:00:00 2001 From: themartto Date: Sun, 31 May 2026 23:52:00 +0300 Subject: [PATCH 13/14] fix: harden sandbox security defaults and symlink handling --- src/acp/mod.rs | 12 ++++++++---- src/client.rs | 21 ++++++++++++++++++--- src/config/types.rs | 4 ++-- src/tools/mod.rs | 10 ++++++++++ src/tools/sandbox.rs | 26 ++++++++++++++++++++++++++ src/tools/sandboxed_executor.rs | 10 +++++++++- 6 files changed, 73 insertions(+), 10 deletions(-) diff --git a/src/acp/mod.rs b/src/acp/mod.rs index 2135527..829d08d 100644 --- a/src/acp/mod.rs +++ b/src/acp/mod.rs @@ -60,10 +60,14 @@ impl AgentState { let (sys_executor, mcp_statuses) = SystemToolExecutor::build(&app_config.mcp_servers, allow_shell).await; let executor = Arc::new(sys_executor) as Arc; - let work_dir = app_config - .work_dir - .clone() - .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))); + let work_dir = match app_config.work_dir.clone() { + Some(wd) => wd, + None => std::env::current_dir().map_err(|e| { + crate::error::Error::Other(format!( + "failed to determine current directory for work_dir: {e}" + )) + })?, + }; Ok(Self { llm, executor, diff --git a/src/client.rs b/src/client.rs index 995e5ed..1bfc198 100644 --- a/src/client.rs +++ b/src/client.rs @@ -315,7 +315,7 @@ impl OpenheimBuilder { } /// Whether to expose the `execute_command` shell tool to the LLM. - /// Overrides `allow_shell` from the config file. Defaults to `true`. + /// Overrides `allow_shell` from the config file. Defaults to `false`. pub fn allow_shell(mut self, allow: bool) -> Self { self.allow_shell = Some(allow); self @@ -367,7 +367,22 @@ impl OpenheimBuilder { } if let Some(wd) = self.work_dir { - app_config.work_dir = Some(wd); + let abs = if wd.is_absolute() { + wd.clone() + } else { + std::env::current_dir() + .map_err(|e| { + crate::error::Error::Other(format!("cannot resolve relative work_dir: {e}")) + })? + .join(&wd) + }; + let canonical = abs.canonicalize().map_err(|e| { + crate::error::Error::Other(format!( + "work_dir '{}' is inaccessible: {e}", + wd.display() + )) + })?; + app_config.work_dir = Some(canonical); } if let Some(shell) = self.allow_shell { app_config.allow_shell = shell; @@ -419,7 +434,7 @@ fn build_programmatic( mcp_servers: BTreeMap::new(), default_skills, work_dir: None, - allow_shell: true, + allow_shell: false, }; let agent_config = AgentConfig { diff --git a/src/config/types.rs b/src/config/types.rs index e22fde0..831dbfb 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -47,13 +47,13 @@ pub struct AppConfig { #[serde(default)] pub work_dir: Option, /// Whether to expose the `execute_command` shell tool to the LLM. - /// Defaults to `true`. Set to `false` to disable shell access entirely. + /// Defaults to `false`. Set to `true` to explicitly opt in to shell access. #[serde(default = "default_allow_shell")] pub allow_shell: bool, } fn default_allow_shell() -> bool { - true + false } /// Configuration for a single MCP server connection. diff --git a/src/tools/mod.rs b/src/tools/mod.rs index 8685c67..de649c4 100644 --- a/src/tools/mod.rs +++ b/src/tools/mod.rs @@ -175,6 +175,8 @@ impl ToolExecutor for SystemToolExecutor { #[cfg(test)] mod tests { + use std::collections::BTreeMap; + use super::*; #[test] @@ -200,4 +202,12 @@ mod tests { assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("Unknown tool")); } + + #[tokio::test] + async fn build_without_shell_omits_execute_command() { + let (executor, _) = SystemToolExecutor::build(&BTreeMap::new(), false).await; + assert!(!executor.handlers.contains_key("execute_command")); + assert!(executor.handlers.contains_key("read_file")); + assert!(executor.handlers.contains_key("write_file")); + } } diff --git a/src/tools/sandbox.rs b/src/tools/sandbox.rs index 6e27962..71d7439 100644 --- a/src/tools/sandbox.rs +++ b/src/tools/sandbox.rs @@ -31,6 +31,19 @@ pub fn validate_path(requested: &str, work_dir: &Path) -> Result { let check = if resolved.exists() { resolved.canonicalize().map_err(Error::IoError)? } else { + // Dangling symlinks look non-existent to exists(); detect them explicitly + // so write_file cannot create the symlink target outside the sandbox. + if resolved + .symlink_metadata() + .ok() + .is_some_and(|m| m.file_type().is_symlink()) + { + return Err(Error::ToolExecutionError(format!( + "path '{}' is a dangling symlink (work directory: '{}')", + requested, + work_dir.display() + ))); + } // Walk up the tree until we find an existing ancestor, canonicalize // that, and verify it is within the work directory. let mut ancestor: &Path = &resolved; @@ -110,4 +123,17 @@ mod tests { let err = validate_path("../../etc/passwd", dir.path()).unwrap_err(); assert!(err.to_string().contains("outside the work directory")); } + + #[test] + fn rejects_dangling_symlink_inside_work_dir() { + let dir = tempfile::tempdir().unwrap(); + let link = dir.path().join("dangling_link"); + // Point the symlink at a path that does not exist so it is dangling. + std::os::unix::fs::symlink("/nonexistent_target_path_12345", &link).unwrap(); + let err = validate_path(link.to_str().unwrap(), dir.path()).unwrap_err(); + assert!( + err.to_string().contains("dangling symlink"), + "unexpected error: {err}" + ); + } } diff --git a/src/tools/sandboxed_executor.rs b/src/tools/sandboxed_executor.rs index 557da1b..39dc4fd 100644 --- a/src/tools/sandboxed_executor.rs +++ b/src/tools/sandboxed_executor.rs @@ -44,7 +44,15 @@ impl SandboxedExecutor { #[async_trait] impl ToolExecutor for SandboxedExecutor { fn list_tools(&self) -> Vec { - self.inner.list_tools() + let tools = self.inner.list_tools(); + if self.allow_shell { + tools + } else { + tools + .into_iter() + .filter(|t| t.function.name != "execute_command") + .collect() + } } async fn execute(&self, name: &str, args_json: &str) -> Result { From 73a43f9f6619a114922609d0d962068af9da6ae5 Mon Sep 17 00:00:00 2001 From: themartto Date: Mon, 1 Jun 2026 07:42:06 +0300 Subject: [PATCH 14/14] feat: v0.3.0 --- CHANGELOG.md | 2 +- Cargo.lock | 901 +++++++++++++++++++++++++++------------------------ Cargo.toml | 2 +- 3 files changed, 485 insertions(+), 420 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8f9bee..655c415 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.3.0] - 2026-06-01 ### Added diff --git a/Cargo.lock b/Cargo.lock index 1a08833..31a3a36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,11 +28,10 @@ dependencies = [ [[package]] name = "agent-client-protocol-derive" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce42c2d3c048c12897eef2e577dfff1e3355c632c9f1625cc953b9df48b44631" +checksum = "cabdc9d845d08ec7ed2d0c9de1ae4a1b198301407d55855261572761be90ec9f" dependencies = [ - "proc-macro2", "quote", "syn", ] @@ -94,9 +93,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -109,15 +108,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -167,9 +166,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "axum" @@ -240,9 +239,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "block-buffer" @@ -253,17 +252,26 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cassowary" @@ -282,9 +290,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.52" +version = "1.2.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", "shlex", @@ -310,9 +318,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -324,9 +332,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.54" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -334,9 +342,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.54" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -346,9 +354,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -358,9 +366,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.7" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "clipboard-win" @@ -373,9 +381,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "colored" @@ -389,9 +397,9 @@ dependencies = [ [[package]] name = "compact_str" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +checksum = "7fd622ebbb56a5b2ccb651b32b911cdeb2a9b4b11776b2473bf26a26a286244e" dependencies = [ "castaway", "cfg-if", @@ -420,6 +428,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -456,10 +474,10 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "crossterm_winapi", "futures-core", - "mio 1.1.1", + "mio 1.2.1", "parking_lot", "rustix 0.38.44", "signal-hook", @@ -486,38 +504,14 @@ dependencies = [ "typenum", ] -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core 0.20.11", - "darling_macro 0.20.11", -] - [[package]] name = "darling" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", + "darling_core", + "darling_macro", ] [[package]] @@ -533,24 +527,13 @@ dependencies = [ "syn", ] -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core 0.20.11", - "quote", - "syn", -] - [[package]] name = "darling_macro" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ - "darling_core 0.23.0", + "darling_core", "quote", "syn", ] @@ -563,9 +546,9 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", "serde_core", @@ -627,9 +610,9 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", @@ -687,9 +670,9 @@ checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "fd-lock" @@ -698,27 +681,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", - "rustix 1.1.3", + "rustix 1.1.4", "windows-sys 0.59.0", ] [[package]] name = "filetime" -version = "0.2.26" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" dependencies = [ "cfg-if", "libc", - "libredox", - "windows-sys 0.60.2", ] [[package]] name = "find-msvc-tools" -version = "0.1.7" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixedbitset" @@ -914,15 +895,28 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + [[package]] name = "h2" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" dependencies = [ "atomic-waker", "bytes", @@ -930,7 +924,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.13.0", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -956,9 +950,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -983,9 +977,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" dependencies = [ "bytes", "itoa", @@ -1028,9 +1022,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.8.1" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", @@ -1043,7 +1037,6 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -1051,15 +1044,14 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", "hyper-util", "rustls", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", @@ -1083,14 +1075,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64", "bytes", "futures-channel", - "futures-core", "futures-util", "http", "http-body", @@ -1133,12 +1124,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -1146,9 +1138,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1159,9 +1151,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1173,15 +1165,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -1193,15 +1185,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -1212,6 +1204,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -1231,9 +1229,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -1252,12 +1250,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -1293,11 +1291,11 @@ dependencies = [ [[package]] name = "instability" -version = "0.3.10" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6778b0196eefee7df739db78758e5cf9b37412268bfa5650bfeed028aed20d9c" +checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" dependencies = [ - "darling 0.20.11", + "darling", "indoc", "proc-macro2", "quote", @@ -1306,19 +1304,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" -dependencies = [ - "memchr", - "serde", -] +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "is_terminal_polyfill" @@ -1337,15 +1325,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.97" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ "cfg-if", "futures-util", @@ -1365,9 +1353,9 @@ dependencies = [ [[package]] name = "kqueue" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +checksum = "273c0752728918e0ac4976f2b275b6fefb9ecd400585dec929419f3844cd87b5" dependencies = [ "kqueue-sys", "libc", @@ -1375,11 +1363,11 @@ dependencies = [ [[package]] name = "kqueue-sys" -version = "1.0.4" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.11.1", "libc", ] @@ -1389,6 +1377,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.186" @@ -1397,13 +1391,11 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" dependencies = [ - "bitflags 2.10.0", "libc", - "redox_syscall 0.7.0", ] [[package]] @@ -1414,15 +1406,15 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "lock_api" @@ -1435,9 +1427,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" [[package]] name = "lru" @@ -1465,9 +1457,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "mime" @@ -1489,9 +1481,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "log", @@ -1501,9 +1493,9 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.14" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" dependencies = [ "libc", "log", @@ -1531,7 +1523,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "cfg-if", "cfg_aliases 0.1.1", "libc", @@ -1539,11 +1531,11 @@ dependencies = [ [[package]] name = "nix" -version = "0.31.2" +version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "cfg-if", "cfg_aliases 0.2.1", "libc", @@ -1555,7 +1547,7 @@ version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "crossbeam-channel", "filetime", "fsevent-sys", @@ -1579,9 +1571,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-traits" @@ -1594,9 +1586,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -1606,7 +1598,7 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "openheim" -version = "0.2.1" +version = "0.3.0" dependencies = [ "agent-client-protocol", "agent-client-protocol-tokio", @@ -1640,15 +1632,14 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.75" +version = "0.10.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "cfg-if", "foreign-types", "libc", - "once_cell", "openssl-macros", "openssl-sys", ] @@ -1666,15 +1657,15 @@ dependencies = [ [[package]] name = "openssl-probe" -version = "0.1.6" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.111" +version = "0.9.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" dependencies = [ "cc", "libc", @@ -1712,7 +1703,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.18", + "redox_syscall", "smallvec", "windows-link", ] @@ -1725,9 +1716,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pastey" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5a797f0e07bdf071d15742978fc3128ec6c22891c31a3a931513263904c982a" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" [[package]] name = "percent-encoding" @@ -1737,18 +1728,18 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", @@ -1757,27 +1748,21 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -1797,11 +1782,21 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" -version = "1.0.105" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -1813,8 +1808,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e842efad9119158434d193c6682e2ebee4b44d6ad801d7b349623b3f57cdf55" dependencies = [ "futures", - "indexmap 2.13.0", - "nix 0.31.2", + "indexmap 2.14.0", + "nix 0.31.3", "tokio", "tracing", "windows", @@ -1822,9 +1817,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.43" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -1835,6 +1830,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radix_trie" version = "0.2.1" @@ -1847,9 +1848,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha", "rand_core", @@ -1880,7 +1881,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "cassowary", "compact_str", "crossterm", @@ -1901,16 +1902,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", -] - -[[package]] -name = "redox_syscall" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" -dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", ] [[package]] @@ -1946,9 +1938,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -1957,9 +1949,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" @@ -2003,9 +1995,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64", "bytes", @@ -2051,9 +2043,9 @@ dependencies = [ [[package]] name = "rmcp" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e12ca9067b5ebfbd5b3fcdc4acfceb81aa7d5ab2a879dff7cb75d22434276aad" +checksum = "0810a9f717d9828f475fe1f629f4c305c8464b7f496c3a854b58d29e65f4058e" dependencies = [ "async-trait", "base64", @@ -2063,7 +2055,7 @@ dependencies = [ "pastey", "pin-project-lite", "process-wrap", - "reqwest 0.13.3", + "reqwest 0.13.4", "rmcp-macros", "schemars 1.2.1", "serde", @@ -2078,11 +2070,11 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7caa6743cc0888e433105fe1bc551a7f607940b126a37bc97b478e86064627eb" +checksum = "6aefac48c364756e97f04c0401ba3231e8607882c7c1d92da0437dc16307904d" dependencies = [ - "darling 0.23.0", + "darling", "proc-macro2", "quote", "serde_json", @@ -2110,7 +2102,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys 0.4.15", @@ -2119,22 +2111,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "errno", "libc", - "linux-raw-sys 0.11.0", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "once_cell", "rustls-pki-types", @@ -2145,18 +2137,18 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.2" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "ring", "rustls-pki-types", @@ -2175,7 +2167,7 @@ version = "14.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7803e8936da37efd9b6d4478277f4b2b9bb5cdb37a113e8d63222e58da647e63" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "cfg-if", "clipboard-win", "fd-lock", @@ -2193,9 +2185,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -2208,9 +2200,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -2261,12 +2253,12 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "2.11.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.10.0", - "core-foundation", + "bitflags 2.11.1", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -2274,9 +2266,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -2284,9 +2276,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -2331,9 +2323,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -2376,15 +2368,16 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.19.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05839ce67618e14a09b286535c0d9c94e85ef25469b0e13cb4f844e5593eb19" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" dependencies = [ "base64", + "bs58", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.13.0", + "indexmap 2.14.0", "schemars 0.9.0", "schemars 1.2.1", "serde_core", @@ -2395,11 +2388,11 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.19.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf2ebbe86054f9b45bc3881e865683ccfaccce97b9b4cb53f3039d67f355a334" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" dependencies = [ - "darling 0.23.0", + "darling", "proc-macro2", "quote", "syn", @@ -2433,9 +2426,9 @@ checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook" @@ -2454,7 +2447,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", - "mio 1.1.1", + "mio 1.2.1", "signal-hook", ] @@ -2470,9 +2463,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -2482,12 +2475,12 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2572,9 +2565,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.114" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -2603,12 +2596,12 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.10.0", - "core-foundation", + "bitflags 2.11.1", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -2624,14 +2617,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.24.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", - "rustix 1.1.3", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -2697,23 +2690,38 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" -version = "1.49.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", - "mio 1.1.1", + "mio 1.2.1", "parking_lot", "pin-project-lite", "signal-hook-registry", @@ -2724,9 +2732,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -2817,7 +2825,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.14.0", "serde", "serde_spanned", "toml_datetime", @@ -2849,20 +2857,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -2923,9 +2931,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -2963,21 +2971,21 @@ dependencies = [ [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-truncate" @@ -3040,11 +3048,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.20.0" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.2", "js-sys", "serde_core", "wasm-bindgen", @@ -3095,18 +3103,27 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.120" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", @@ -3117,9 +3134,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.70" +version = "0.4.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" dependencies = [ "js-sys", "wasm-bindgen", @@ -3127,9 +3144,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.120" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3137,9 +3154,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.120" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ "bumpalo", "proc-macro2", @@ -3150,13 +3167,35 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.120" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.14.0", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" version = "0.5.0" @@ -3170,11 +3209,23 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver", +] + [[package]] name = "web-sys" -version = "0.3.97" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" dependencies = [ "js-sys", "wasm-bindgen", @@ -3350,15 +3401,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -3392,30 +3434,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - [[package]] name = "windows-threading" version = "0.2.1" @@ -3437,12 +3462,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -3455,12 +3474,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -3473,24 +3486,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -3503,12 +3504,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -3521,12 +3516,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -3539,12 +3528,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -3558,37 +3541,119 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" +name = "winnow" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] [[package]] -name = "winnow" -version = "0.7.14" +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" dependencies = [ - "memchr", + "wit-bindgen-rust-macro", ] [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.14.0", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap 2.14.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.14.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -3597,9 +3662,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -3609,18 +3674,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.33" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.33" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" dependencies = [ "proc-macro2", "quote", @@ -3629,18 +3694,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -3656,9 +3721,9 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -3667,9 +3732,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -3678,9 +3743,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", @@ -3689,6 +3754,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.14" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 32bbbd0..548a543 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openheim" -version = "0.2.1" +version = "0.3.0" edition = "2024" description = "A fast, multi-provider LLM agent runtime written in Rust" license = "MIT"