use std::path::PathBuf;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use serde_json::Value;
use crate::{
CredentialSpec, Harness, HarnessCapabilities, HarnessError, HarnessInfo, HarnessModel,
HarnessReadiness, InstallCallback, InstalledModel, ModelManagement, PullProgressCallback,
RunCallback, RunHandle, RunRequest,
};
mod instructions;
mod ollama;
mod run;
mod session;
mod skills;
mod tools;
mod wire;
pub use session::SessionRecord;
pub use tools::mcp::{McpPrompt, McpPromptArg, McpServer, McpTransport, PromptMessage};
const OLLAMA_CTX_CEILING: u64 = 32_768;
const OLLAMA_CTX_DEFAULT: u64 = 8_192;
enum Discovery {
OllamaTags,
Static(Vec<HarnessModel>),
ModelsDev(String),
}
#[derive(Debug, Clone)]
pub struct AgentDef {
pub description: String,
pub system_prompt: Option<String>,
pub model: Option<String>,
}
#[derive(Debug, Clone, Copy)]
pub struct ModelCost {
pub input_per_mtok: f64,
pub output_per_mtok: f64,
pub cache_read_per_mtok: Option<f64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Permission {
Allow,
Deny,
Ask,
}
#[derive(Debug, Clone)]
pub struct PermissionRule {
pub tool: Option<String>,
pub pattern: Option<String>,
pub effect: Permission,
}
impl PermissionRule {
pub fn deny(tool: impl Into<String>) -> Self {
Self { tool: Some(tool.into()), pattern: None, effect: Permission::Deny }
}
pub fn deny_matching(tool: impl Into<String>, pattern: impl Into<String>) -> Self {
Self { tool: Some(tool.into()), pattern: Some(pattern.into()), effect: Permission::Deny }
}
pub fn allow_matching(tool: impl Into<String>, pattern: impl Into<String>) -> Self {
Self { tool: Some(tool.into()), pattern: Some(pattern.into()), effect: Permission::Allow }
}
pub fn ask_matching(tool: impl Into<String>, pattern: impl Into<String>) -> Self {
Self { tool: Some(tool.into()), pattern: Some(pattern.into()), effect: Permission::Ask }
}
pub fn ask(tool: impl Into<String>) -> Self {
Self { tool: Some(tool.into()), pattern: None, effect: Permission::Ask }
}
}
#[derive(Debug, Clone)]
pub struct PermissionRequest {
pub tool: String,
pub subject: Option<String>,
}
pub type PermissionPrompt = std::sync::Arc<dyn Fn(&PermissionRequest) -> bool + Send + Sync>;
pub struct OpenHarness {
id: String,
display_name: String,
description: String,
base_url: String,
api_key_env: Option<String>,
discovery: Discovery,
default_model: Option<String>,
session_dir: Option<PathBuf>,
context_tokens: Option<u64>,
agents: Vec<(String, AgentDef)>,
mcp_servers: Vec<McpServer>,
model_costs: Vec<(String, ModelCost)>,
permissions: Vec<PermissionRule>,
permission_prompt: Option<PermissionPrompt>,
reasoning_tag: Option<String>,
}
#[derive(Clone, Debug, Default)]
pub struct OpenHarnessConfig {
pub id: String,
pub display_name: String,
pub base_url: String,
pub api_key_env: Option<String>,
pub models: Vec<HarnessModel>,
}
impl OpenHarness {
pub fn ollama() -> Self {
Self {
id: "ollama".to_owned(),
display_name: "Ollama".to_owned(),
description: "Local models served by Ollama via its OpenAI-compatible API.".to_owned(),
base_url: "http://localhost:11434".to_owned(),
api_key_env: None,
discovery: Discovery::OllamaTags,
default_model: None,
session_dir: None,
context_tokens: None,
agents: Vec::new(),
mcp_servers: Vec::new(),
model_costs: Vec::new(),
permissions: Vec::new(),
permission_prompt: None,
reasoning_tag: Some("think".to_owned()),
}
}
pub fn custom(config: OpenHarnessConfig) -> Self {
let OpenHarnessConfig { id, display_name, base_url, api_key_env, models } = config;
Self {
id,
description: format!("{display_name} via its OpenAI-compatible API."),
display_name,
base_url,
api_key_env,
default_model: models.first().map(|m| m.value.clone()),
discovery: Discovery::Static(models),
session_dir: None,
context_tokens: None,
agents: Vec::new(),
mcp_servers: Vec::new(),
model_costs: Vec::new(),
permissions: Vec::new(),
permission_prompt: None,
reasoning_tag: Some("think".to_owned()),
}
}
pub fn with_models_dev(mut self, provider: impl Into<String>) -> Self {
self.discovery = Discovery::ModelsDev(provider.into());
self
}
fn require_ollama_management(&self) -> Result<(), HarnessError> {
match &self.discovery {
Discovery::OllamaTags => Ok(()),
Discovery::Static(_) | Discovery::ModelsDev(_) => Err(HarnessError::Other(format!(
"{} does not support managing models.",
self.display_name
))),
}
}
fn api_key(&self) -> Option<String> {
self.api_key_env
.as_ref()
.and_then(|env| std::env::var(env).ok())
.filter(|v| !v.trim().is_empty())
}
fn resolve_context(&self, model: &str) -> (Option<u64>, Option<u64>) {
match &self.discovery {
Discovery::OllamaTags => {
let effective = self
.context_tokens
.or_else(|| ollama::context_length(&self.base_url, model).map(|n| n.min(OLLAMA_CTX_CEILING)))
.unwrap_or(OLLAMA_CTX_DEFAULT);
(Some(effective), Some(effective))
}
Discovery::Static(_) | Discovery::ModelsDev(_) => (self.context_tokens, None),
}
}
fn model_cost_for(&self, model: &str) -> Option<ModelCost> {
self.model_costs.iter().find(|(m, _)| m == model).map(|(_, c)| *c)
}
pub fn with_session_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.session_dir = Some(dir.into());
self
}
pub fn with_context_tokens(mut self, tokens: u64) -> Self {
self.context_tokens = Some(tokens);
self
}
pub fn with_agent(mut self, name: impl Into<String>, def: AgentDef) -> Self {
self.agents.push((name.into(), def));
self
}
pub fn with_mcp_server(mut self, server: McpServer) -> Self {
self.mcp_servers.push(server);
self
}
pub fn with_model_cost(mut self, model: impl Into<String>, cost: ModelCost) -> Self {
self.model_costs.push((model.into(), cost));
self
}
pub fn with_permission_rule(mut self, rule: PermissionRule) -> Self {
self.permissions.push(rule);
self
}
pub fn with_permission_prompt(
mut self,
prompt: impl Fn(&PermissionRequest) -> bool + Send + Sync + 'static,
) -> Self {
self.permission_prompt = Some(std::sync::Arc::new(prompt));
self
}
pub fn with_reasoning_tag(mut self, tag: impl Into<String>) -> Self {
self.reasoning_tag = Some(tag.into());
self
}
pub fn without_reasoning_extraction(mut self) -> Self {
self.reasoning_tag = None;
self
}
pub fn sessions(&self) -> Result<Vec<SessionRecord>, HarnessError> {
match &self.session_dir {
Some(dir) => session::FileStore::new(dir.clone())
.list_records()
.map_err(HarnessError::Other),
None => Ok(Vec::new()),
}
}
pub fn mcp_prompts(&self) -> Vec<McpPrompt> {
let cwd = std::env::current_dir().unwrap_or_default();
tools::mcp::list_prompts(&self.mcp_servers, &cwd)
}
pub fn get_mcp_prompt(
&self,
server: &str,
name: &str,
arguments: &[(String, String)],
) -> Result<Vec<PromptMessage>, HarnessError> {
let cwd = std::env::current_dir().unwrap_or_default();
tools::mcp::get_prompt(&self.mcp_servers, server, name, arguments, &cwd).map_err(HarnessError::Other)
}
}
impl Harness for OpenHarness {
fn info(&self) -> HarnessInfo {
HarnessInfo {
id: self.id.clone(),
display_name: self.display_name.clone(),
description: self.description.clone(),
requires_install: false,
capabilities: HarnessCapabilities {
credential_required: self.api_key_env.is_some(),
previews_edits: false,
models: match &self.discovery {
Discovery::Static(m) => m.clone(),
Discovery::OllamaTags | Discovery::ModelsDev(_) => Vec::new(),
},
allows_custom_model: true,
supports_effort: false,
supports_max_turns: true,
supports_login: false,
supports_custom_instructions: true,
},
}
}
fn readiness(&self) -> HarnessReadiness {
let base = |ready: bool, error: Option<String>| HarnessReadiness {
harness_id: self.id.clone(),
ready,
installed: true,
version: None,
auth_configured: ready,
error,
details: Value::Null,
};
match &self.discovery {
Discovery::OllamaTags => match ollama::list_tags(&self.base_url) {
Ok(_) => base(true, None),
Err(e) => base(
false,
Some(format!(
"Ollama is not reachable at {} — is it running (`ollama serve`)? ({e})",
self.base_url
)),
),
},
Discovery::Static(_) | Discovery::ModelsDev(_) => match &self.api_key_env {
Some(env) if self.api_key().is_none() => base(
false,
Some(format!("Set {env} to use {}.", self.display_name)),
),
_ => base(true, None),
},
}
}
fn install(&self, _on_event: InstallCallback) -> Result<(), HarnessError> {
Ok(())
}
fn run(&self, request: RunRequest, on_event: RunCallback) -> Result<RunHandle, HarnessError> {
let RunRequest { run_id, prompt, cwd, mode, tuning, resume, attachments } = request;
let model = tuning
.model
.as_deref()
.map(str::trim)
.filter(|m| !m.is_empty())
.map(str::to_owned)
.or_else(|| self.default_model.clone())
.ok_or_else(|| {
HarnessError::Other(format!(
"{}: no model selected and no default — set RunTuning.model",
self.id
))
})?;
let (context_tokens, ollama_num_ctx) = self.resolve_context(&model);
let model_cost = self.model_cost_for(&model);
let image_data_uris: Vec<String> =
attachments.iter().map(|a| wire::image_data_uri(&a.mime_type, &a.data)).collect();
let cfg = run::LoopConfig {
run_id,
base_url: self.base_url.clone(),
api_key: self.api_key(),
model,
prompt,
cwd: cwd.unwrap_or_else(|| std::env::current_dir().unwrap_or_default()),
mode,
max_turns: run::LoopConfig::max_turns_or_default(tuning.max_turns),
resume,
store: self.session_dir.clone().map(session::FileStore::new),
context_tokens,
ollama_num_ctx,
agents: self.agents.clone(),
mcp_servers: self.mcp_servers.clone(),
output_schema: tuning.output_schema,
model_cost,
image_data_uris,
permissions: self.permissions.clone(),
permission_prompt: self.permission_prompt.clone(),
reasoning_tag: self.reasoning_tag.clone(),
extra_instructions: tuning.extra_instructions,
};
let cancel = Arc::new(AtomicBool::new(false));
let thread_cancel = Arc::clone(&cancel);
std::thread::spawn(move || run::drive(cfg, thread_cancel, on_event));
Ok(Box::new(run::OpenAiRun::new(cancel)))
}
fn credential(&self) -> CredentialSpec {
match &self.api_key_env {
Some(env) => CredentialSpec {
label: format!("{} API key", self.display_name),
keychain_service: self.id.clone(),
keychain_account: env.clone(),
required: true,
},
None => CredentialSpec {
label: format!("{} (no key required)", self.display_name),
keychain_service: self.id.clone(),
keychain_account: String::new(),
required: false,
},
}
}
fn list_models(&self) -> Result<Vec<HarnessModel>, HarnessError> {
match &self.discovery {
Discovery::OllamaTags => ollama::list_tags(&self.base_url).map_err(HarnessError::Other),
Discovery::Static(_) => Ok(self.info().capabilities.models),
Discovery::ModelsDev(provider) => Ok(crate::models_dev::provider_models(provider)),
}
}
fn model_management(&self) -> Option<ModelManagement> {
match &self.discovery {
Discovery::OllamaTags => Some(ModelManagement { base_url: self.base_url.clone() }),
Discovery::Static(_) | Discovery::ModelsDev(_) => None,
}
}
fn list_installed_models(&self) -> Result<Vec<InstalledModel>, HarnessError> {
self.require_ollama_management()?;
ollama::list_installed(&self.base_url).map_err(HarnessError::Other)
}
fn pull_model(
&self,
model: &str,
cancel: &std::sync::atomic::AtomicBool,
on_progress: PullProgressCallback<'_>,
) -> Result<(), HarnessError> {
self.require_ollama_management()?;
ollama::pull(&self.base_url, model, cancel, on_progress).map_err(HarnessError::Other)
}
fn delete_model(&self, model: &str) -> Result<(), HarnessError> {
self.require_ollama_management()?;
ollama::delete(&self.base_url, model).map_err(HarnessError::Other)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ollama_is_keyless_dynamic_and_editing() {
let h = OpenHarness::ollama();
let info = h.info();
assert_eq!(info.id, "ollama");
assert!(!info.requires_install);
assert!(!info.capabilities.credential_required);
assert!(!info.capabilities.previews_edits);
assert!(info.capabilities.allows_custom_model);
assert!(info.capabilities.models.is_empty());
assert!(!h.credential().required);
}
#[test]
fn only_ollama_exposes_model_management() {
let ollama = OpenHarness::ollama();
let mgmt = ollama.model_management().expect("Ollama manages models");
assert_eq!(mgmt.base_url, "http://localhost:11434");
let remote = OpenHarness::custom(OpenHarnessConfig {
id: "openrouter".to_owned(),
display_name: "OpenRouter".to_owned(),
base_url: "https://openrouter.ai/api".to_owned(),
api_key_env: Some("OPENROUTER_API_KEY".to_owned()),
models: Vec::new(),
});
assert!(remote.model_management().is_none());
assert!(remote.list_installed_models().is_err());
assert!(remote.delete_model("whatever").is_err());
let cancel = std::sync::atomic::AtomicBool::new(false);
assert!(remote.pull_model("whatever", &cancel, &mut |_| {}).is_err());
}
#[test]
fn custom_requires_its_key_and_lists_static_models() {
let h = OpenHarness::custom(OpenHarnessConfig {
id: "openrouter".to_owned(),
display_name: "OpenRouter".to_owned(),
base_url: "https://openrouter.ai/api".to_owned(),
api_key_env: Some("OPENROUTER_API_KEY".to_owned()),
models: vec![HarnessModel { value: "x-ai/grok".to_owned(), label: "Grok".to_owned() }],
});
assert!(h.info().capabilities.credential_required);
assert!(h.credential().required);
assert_eq!(h.credential().keychain_account, "OPENROUTER_API_KEY");
assert_eq!(h.list_models().unwrap().len(), 1);
assert_eq!(h.default_model.as_deref(), Some("x-ai/grok"));
}
}