use std::{
collections::BTreeMap,
path::{Path, PathBuf},
sync::Arc,
};
#[cfg(feature = "mcp")]
use std::{collections::HashMap, sync::Mutex};
use mentra::{
BuiltinProvider, ModelSelector, ProviderId, RuntimePolicy,
provider_core::{StaticCredentialSource, responses, responses::ResponsesProvider},
};
use crate::{
approval::ApprovalGate, hooks::Interceptor, provider, run::RunError, shell::ShellAccess, store,
tools::SpawnTool,
};
use super::{
Runtime,
dispatch::{DispatchHook, HookDispatch},
environment::EnvironmentExecutor,
};
const SHARED_IDENTIFIER: &str = "basis:runtime";
pub struct RuntimeBuilder {
provider: Option<BuiltinProvider>,
base_url: Option<String>,
api_key: Option<String>,
model: ModelSelector,
history: Option<History>,
interceptors: Vec<Arc<dyn Interceptor>>,
command_environment: BTreeMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum History {
Directory(PathBuf),
Ephemeral,
}
impl std::fmt::Debug for RuntimeBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RuntimeBuilder")
.field("provider", &self.provider)
.field("base_url", &self.base_url)
.field("api_key", &self.api_key.as_ref().map(|_| "<redacted>"))
.field("model", &self.model)
.field("history", &self.history)
.field(
"interceptors",
&self
.interceptors
.iter()
.map(|interceptor| interceptor.name())
.collect::<Vec<_>>(),
)
.field(
"command_environment",
&self.command_environment.keys().collect::<Vec<_>>(),
)
.finish()
}
}
impl Default for RuntimeBuilder {
fn default() -> Self {
Self {
provider: None,
base_url: None,
api_key: None,
model: ModelSelector::NewestAvailable,
history: None,
interceptors: Vec::new(),
command_environment: BTreeMap::new(),
}
}
}
impl RuntimeBuilder {
pub fn with_provider(self, provider: BuiltinProvider) -> Self {
Self {
provider: Some(provider),
..self
}
}
pub fn with_base_url(self, base_url: impl Into<String>) -> Self {
Self {
base_url: Some(base_url.into()),
..self
}
}
pub fn with_api_key(self, api_key: impl Into<String>) -> Self {
Self {
api_key: Some(api_key.into()),
..self
}
}
pub fn with_model(self, model: ModelSelector) -> Self {
Self { model, ..self }
}
pub fn with_store_dir(self, dir: impl Into<PathBuf>) -> Self {
Self {
history: Some(History::Directory(dir.into())),
..self
}
}
pub fn with_ephemeral_history(self) -> Self {
Self {
history: Some(History::Ephemeral),
..self
}
}
pub fn with_interceptor(self, interceptor: impl Interceptor + 'static) -> Self {
Self {
interceptors: {
let mut interceptors = self.interceptors;
interceptors.push(Arc::new(interceptor));
interceptors
},
..self
}
}
pub fn with_command_environment(
mut self,
name: impl Into<String>,
value: impl Into<String>,
) -> Self {
self.command_environment.insert(name.into(), value.into());
self
}
pub fn build(self) -> Result<Runtime, RunError> {
self.build_with(SHARED_IDENTIFIER.to_string(), shared_policy())
}
pub(crate) fn build_for(
self,
workspace: &Path,
shell: ShellAccess,
) -> Result<Runtime, RunError> {
let policy = git_protected(RuntimePolicy::workspace_bounded(workspace), workspace)
.allow_shell_commands(shell.is_granted())
.allow_background_commands(shell.is_granted());
self.build_with(store::runtime_identifier(workspace), policy)
}
fn build_with(self, identifier: String, policy: RuntimePolicy) -> Result<Runtime, RunError> {
let choice = provider::resolve_with(
self.provider,
self.base_url.as_deref(),
self.api_key.as_deref(),
)?;
let dispatch = Arc::new(HookDispatch::new(self.interceptors));
let builder = mentra::Runtime::builder()
.with_runtime_identifier(identifier)
.with_policy(policy)
.with_tool_authorizer(ApprovalGate::new())
.with_tool(SpawnTool::new())
.with_pre_hook(DispatchHook(Arc::clone(&dispatch)));
let builder = if self.command_environment.is_empty() {
builder
} else {
builder.with_executor(EnvironmentExecutor::new(self.command_environment))
};
let builder = match &self.history {
Some(History::Directory(dir)) => builder.with_store(store::store_in(dir)),
Some(History::Ephemeral) => builder.with_store(store::volatile()),
None => builder,
};
let mentra = match &choice.base_url {
Some(base_url) => {
builder.with_registered_provider(compatible_provider(base_url, &choice.api_key))
}
None => builder.with_provider(choice.provider, choice.api_key.clone()),
}
.build()?;
Ok(Runtime {
mentra,
provider: choice.provider,
provider_label: ProviderId::from(choice.provider).to_string(),
model: self.model,
dispatch,
#[cfg(feature = "mcp")]
mcp_claims: Mutex::new(HashMap::new()),
})
}
}
pub(crate) fn shared_policy() -> RuntimePolicy {
RuntimePolicy::default()
.allow_shell_commands(true)
.allow_background_commands(true)
.with_default_command_timeout(std::time::Duration::from_secs(120))
.with_max_command_timeout(std::time::Duration::from_secs(600))
}
fn git_protected(policy: RuntimePolicy, workspace: &Path) -> RuntimePolicy {
let git = workspace.join(".git");
policy
.with_denied_write_root(git.join("hooks"))
.with_denied_write_root(git.join("config"))
}
fn compatible_provider(base_url: &str, api_key: &str) -> ResponsesProvider<StaticCredentialSource> {
let mut definition = responses::openai_definition();
definition.base_url = Some(base_url.to_string());
definition.descriptor.display_name = Some(format!("OpenAI-compatible ({base_url})"));
ResponsesProvider::new(definition, StaticCredentialSource::new(api_key))
.without_hybrid_http_previous_response_id()
}
#[cfg(test)]
mod tests;