diff --git a/Cargo.toml b/Cargo.toml index 59364fe..fdb8b6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,6 +63,7 @@ log = "0.4" dirs = "5.0" serde_yaml = "0.9" handlebars = "6.0" +once_cell = "1.19" # Configuration toml = "0.8" diff --git a/crates/owlen-core/Cargo.toml b/crates/owlen-core/Cargo.toml index 8acb01d..500d81a 100644 --- a/crates/owlen-core/Cargo.toml +++ b/crates/owlen-core/Cargo.toml @@ -45,6 +45,7 @@ tokio-stream = { workspace = true } tokio-tungstenite = "0.21" tungstenite = "0.21" ollama-rs = { version = "0.3", features = ["stream", "headers"] } +once_cell = { workspace = true } [dev-dependencies] tokio-test = { workspace = true } diff --git a/crates/owlen-core/src/mcp/presets.rs b/crates/owlen-core/src/mcp/presets.rs index bfbfc72..88a805d 100644 --- a/crates/owlen-core/src/mcp/presets.rs +++ b/crates/owlen-core/src/mcp/presets.rs @@ -6,7 +6,7 @@ use crate::config::McpServerConfig; use crate::tools::tool_identifier_violation; -use anyhow::{anyhow, Result}; +use anyhow::{Result, anyhow}; use std::collections::{HashMap, HashSet}; use std::str::FromStr; diff --git a/crates/owlen-core/src/mode.rs b/crates/owlen-core/src/mode.rs index 1d130fe..fe3f77b 100644 --- a/crates/owlen-core/src/mode.rs +++ b/crates/owlen-core/src/mode.rs @@ -6,6 +6,8 @@ use serde::{Deserialize, Serialize}; use std::str::FromStr; +use crate::tools::{WEB_SEARCH_TOOL_NAME, canonical_tool_name}; + /// Operating mode for Owlen #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)] #[serde(rename_all = "lowercase")] @@ -71,7 +73,7 @@ impl Default for ModeConfig { impl ModeConfig { fn default_chat_tools() -> ModeToolConfig { ModeToolConfig { - allowed_tools: vec!["web_search".to_string()], + allowed_tools: vec![WEB_SEARCH_TOOL_NAME.to_string()], } } @@ -108,7 +110,10 @@ impl ModeToolConfig { } // Check if tool is explicitly listed - self.allowed_tools.iter().any(|t| t == tool_name) + let target = canonical_tool_name(tool_name); + self.allowed_tools + .iter() + .any(|t| canonical_tool_name(t) == target) } } @@ -141,6 +146,7 @@ mod tests { let config = ModeConfig::default(); // Web search should be allowed in chat mode + assert!(config.is_tool_allowed(Mode::Chat, WEB_SEARCH_TOOL_NAME)); assert!(config.is_tool_allowed(Mode::Chat, "web_search")); // Code exec should not be allowed in chat mode @@ -153,6 +159,7 @@ mod tests { let config = ModeConfig::default(); // All tools should be allowed in code mode + assert!(config.is_tool_allowed(Mode::Code, WEB_SEARCH_TOOL_NAME)); assert!(config.is_tool_allowed(Mode::Code, "web_search")); assert!(config.is_tool_allowed(Mode::Code, "code_exec")); assert!(config.is_tool_allowed(Mode::Code, "file_write")); diff --git a/crates/owlen-core/tests/fixtures/ollama_cloud_final.json b/crates/owlen-core/tests/fixtures/ollama_cloud_final.json index ff8edf9..65ae6ca 100644 --- a/crates/owlen-core/tests/fixtures/ollama_cloud_final.json +++ b/crates/owlen-core/tests/fixtures/ollama_cloud_final.json @@ -7,12 +7,10 @@ "tool_calls": [] }, "done": true, - "final_data": { - "total_duration": 2500000000, - "load_duration": 60000000, - "prompt_eval_count": 64, - "prompt_eval_duration": 420000000, - "eval_count": 48, - "eval_duration": 520000000 - } + "total_duration": 2500000000, + "load_duration": 60000000, + "prompt_eval_count": 64, + "prompt_eval_duration": 420000000, + "eval_count": 48, + "eval_duration": 520000000 } diff --git a/crates/owlen-core/tests/fixtures/ollama_local_completion.json b/crates/owlen-core/tests/fixtures/ollama_local_completion.json index edfac1d..d2b50e9 100644 --- a/crates/owlen-core/tests/fixtures/ollama_local_completion.json +++ b/crates/owlen-core/tests/fixtures/ollama_local_completion.json @@ -7,12 +7,10 @@ "tool_calls": [] }, "done": true, - "final_data": { - "total_duration": 1200000000, - "load_duration": 50000000, - "prompt_eval_count": 24, - "prompt_eval_duration": 320000000, - "eval_count": 12, - "eval_duration": 480000000 - } + "total_duration": 1200000000, + "load_duration": 50000000, + "prompt_eval_count": 24, + "prompt_eval_duration": 320000000, + "eval_count": 12, + "eval_duration": 480000000 } diff --git a/crates/owlen-core/tests/mode_tool_filter.rs b/crates/owlen-core/tests/mode_tool_filter.rs index 5b77c6d..5948dda 100644 --- a/crates/owlen-core/tests/mode_tool_filter.rs +++ b/crates/owlen-core/tests/mode_tool_filter.rs @@ -13,7 +13,7 @@ use std::sync::Arc; use owlen_core::config::Config; use owlen_core::mode::{Mode, ModeConfig, ModeToolConfig}; use owlen_core::tools::registry::ToolRegistry; -use owlen_core::tools::{Tool, ToolResult}; +use owlen_core::tools::{Tool, ToolResult, WEB_SEARCH_TOOL_NAME}; use owlen_core::ui::{NoOpUiController, UiController}; use serde_json::json; use tokio::sync::Mutex; @@ -57,7 +57,7 @@ async fn test_tool_allowed_in_chat_mode() { let ui: Arc = Arc::new(NoOpUiController); let mut reg = ToolRegistry::new(cfg.clone(), ui); - reg.register(EchoTool); + reg.register(EchoTool).unwrap(); let args = json!({ "msg": "hello" }); let result = reg @@ -75,11 +75,11 @@ async fn test_tool_not_allowed_in_any_mode() { let cfg = Config { modes: ModeConfig { chat: ModeToolConfig { - allowed_tools: vec!["web_search".to_string()], + allowed_tools: vec![WEB_SEARCH_TOOL_NAME.to_string()], }, code: ModeToolConfig { // Strict denial - only web_search allowed - allowed_tools: vec!["web_search".to_string()], + allowed_tools: vec![WEB_SEARCH_TOOL_NAME.to_string()], }, }, ..Default::default() @@ -88,7 +88,7 @@ async fn test_tool_not_allowed_in_any_mode() { let ui: Arc = Arc::new(NoOpUiController); let mut reg = ToolRegistry::new(cfg.clone(), ui); - reg.register(EchoTool); + reg.register(EchoTool).unwrap(); let args = json!({ "msg": "hello" }); let result = reg diff --git a/crates/owlen-core/tests/ollama_wiremock.rs b/crates/owlen-core/tests/ollama_wiremock.rs index 04ffbec..6e4cb08 100644 --- a/crates/owlen-core/tests/ollama_wiremock.rs +++ b/crates/owlen-core/tests/ollama_wiremock.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use owlen_core::tools::WEB_SEARCH_TOOL_NAME; use owlen_core::types::{ChatParameters, Role}; use owlen_core::{ Config, Provider, @@ -108,6 +109,9 @@ fn configure_cloud(base_url: &str) -> Config { config.privacy.encrypt_local_data = false; config.privacy.require_consent_per_session = false; config.tools.web_search.enabled = true; + unsafe { + std::env::set_var("OWLEN_ALLOW_INSECURE_CLOUD", "1"); + } if let Some(cloud) = config.providers.get_mut("ollama_cloud") { cloud.enabled = true; @@ -117,6 +121,10 @@ fn configure_cloud(base_url: &str) -> Config { "web_search_endpoint".into(), Value::String("/v1/web/search".into()), ); + cloud.extra.insert( + owlen_core::config::OLLAMA_CLOUD_ENDPOINT_KEY.into(), + Value::String(base_url.to_string()), + ); } config @@ -162,6 +170,7 @@ async fn local_provider_happy_path_records_usage() { SessionOutcome::Complete(response) => response, _ => panic!("expected complete outcome"), }; + assert_eq!(response.message.content, "Local response complete."); let snapshot = session @@ -226,6 +235,8 @@ async fn cloud_tool_call_flows_through_web_search() { .get("ollama_cloud") .expect("cloud provider config") .clone(); + assert_eq!(cloud_cfg.api_key.as_deref(), Some("test-key")); + assert_eq!(cloud_cfg.base_url.as_deref(), Some(base_url.as_str())); let provider: Arc = Arc::new( OllamaProvider::from_config("ollama_cloud", &cloud_cfg, Some(&config.general)) .expect("cloud provider"), @@ -233,7 +244,7 @@ async fn cloud_tool_call_flows_through_web_search() { let (mut session, _tmp) = create_session(provider, config).await; session.grant_consent( - "web_search", + WEB_SEARCH_TOOL_NAME, vec!["network".into()], vec![format!("{}/v1/web/search", base_url)], ); @@ -243,12 +254,12 @@ async fn cloud_tool_call_flows_through_web_search() { "What is new in Rust today?".to_string(), ChatParameters::default(), ) - .await - .expect("cloud completion"); + .await; let response = match outcome { - SessionOutcome::Complete(response) => response, - _ => panic!("expected complete outcome"), + Ok(SessionOutcome::Complete(response)) => response, + Ok(_) => panic!("expected complete outcome"), + Err(err) => panic!("cloud completion: {err:?}"), }; assert_eq!( response.message.content, diff --git a/crates/owlen-tui/src/chat_app.rs b/crates/owlen-tui/src/chat_app.rs index 84f3cbd..8161885 100644 --- a/crates/owlen-tui/src/chat_app.rs +++ b/crates/owlen-tui/src/chat_app.rs @@ -6917,7 +6917,7 @@ impl ChatApp { } } "audit" => { - let preset_name = args.get(1).map(|s| *s); + let preset_name = args.get(1).copied(); match self.audit_tool_preset(preset_name).await { Ok(message) => { self.status = message;