use std::{
path::{Path, PathBuf},
sync::Arc,
};
use crate::runtime::Runtime;
use super::{
manifest::{DeclaredToolError, ToolsSource, layer},
tool::DeclaredTool,
};
#[derive(Debug)]
pub(crate) struct DeclaredTools {
runtime: Arc<Runtime>,
root: PathBuf,
names: Vec<String>,
}
impl DeclaredTools {
pub(crate) fn register(
runtime: Arc<Runtime>,
root: &Path,
sources: &[ToolsSource],
) -> Result<Self, DeclaredToolError> {
let declared = layer(sources);
let mut claimed = Self {
runtime,
root: root.to_path_buf(),
names: Vec::new(),
};
for (path, spec) in &declared {
claimed
.runtime
.claim_declared_tool(&spec.name, root)
.map_err(|reason| DeclaredToolError::NameTaken {
path: path.clone(),
name: spec.name.clone(),
reason,
})?;
claimed.names.push(spec.name.clone());
}
for (_, spec) in declared {
claimed
.runtime
.mentra_runtime()
.register_tool(DeclaredTool::new(spec, root));
}
Ok(claimed)
}
pub(crate) fn names(&self) -> &[String] {
&self.names
}
pub(crate) fn root(&self) -> &Path {
&self.root
}
}
impl Drop for DeclaredTools {
fn drop(&mut self) {
for name in self.names.drain(..) {
self.runtime.release_declared_tool(&name, &self.root);
}
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use crate::{
context::ContextScope,
tools::declared::manifest::{DeclaredToolSpec, SideEffect},
};
use super::*;
fn runtime() -> Arc<Runtime> {
Arc::new(
Runtime::builder()
.with_base_url("http://127.0.0.1:1/v1")
.with_api_key("test-key")
.with_ephemeral_history()
.build()
.expect("builds"),
)
}
fn source(path: &str, names: &[&str]) -> ToolsSource {
ToolsSource {
path: PathBuf::from(path),
scope: ContextScope::Workspace,
tools: names
.iter()
.map(|name| DeclaredToolSpec {
name: (*name).to_string(),
description: "does the thing".to_string(),
input_schema: json!({"type": "object", "properties": {}}),
command: vec!["./x".to_string()],
cwd: None,
env: Vec::new(),
timeout_ms: None,
side_effect: SideEffect::Process,
})
.collect(),
}
}
#[test]
fn a_declared_tool_reaches_the_model_under_the_name_the_file_gave_it() {
let runtime = runtime();
let sources = [source("/repo/.basis/tools.json", &["jenkins_job"])];
let registered =
DeclaredTools::register(Arc::clone(&runtime), Path::new("/repo"), &sources)
.expect("registers");
assert_eq!(registered.names(), ["jenkins_job"]);
assert!(
runtime
.mentra_runtime()
.tools()
.iter()
.any(|tool| tool.provider.name == "jenkins_job")
);
}
#[test]
fn a_manifest_cannot_take_over_the_name_of_basiss_own_tool() {
let runtime = runtime();
let sources = [source("/repo/.basis/tools.json", &[crate::tools::SPAWN])];
let error =
DeclaredTools::register(runtime, Path::new("/repo"), &sources).expect_err("refused");
assert!(
matches!(error, DeclaredToolError::NameTaken { .. }),
"{error}"
);
assert!(error.to_string().contains(crate::tools::SPAWN), "{error}");
}
#[test]
fn a_manifest_cannot_take_over_a_mentra_builtin_either() {
let runtime = runtime();
let sources = [source("/repo/.basis/tools.json", &["files"])];
let error =
DeclaredTools::register(runtime, Path::new("/repo"), &sources).expect_err("refused");
assert!(
matches!(error, DeclaredToolError::NameTaken { .. }),
"{error}"
);
}
#[test]
fn one_name_is_one_program_across_the_workspaces_sharing_a_runtime() {
let runtime = runtime();
let first = DeclaredTools::register(
Arc::clone(&runtime),
Path::new("/repo/one"),
&[source("/repo/one/.basis/tools.json", &["deploy"])],
)
.expect("the first claimant registers");
let error = DeclaredTools::register(
Arc::clone(&runtime),
Path::new("/repo/two"),
&[source("/repo/two/.basis/tools.json", &["deploy"])],
)
.expect_err("refused rather than silently renamed");
assert!(
matches!(error, DeclaredToolError::NameTaken { .. }),
"{error}"
);
assert!(
error.to_string().contains("/repo/two"),
"the refusal names the file to fix: {error}"
);
drop(first);
DeclaredTools::register(
runtime,
Path::new("/repo/two"),
&[source("/repo/two/.basis/tools.json", &["deploy"])],
)
.expect("a released name is claimable again");
}
#[test]
fn a_workspace_can_be_reopened_over_the_entry_its_last_open_left_behind() {
let runtime = runtime();
let sources = [source("/repo/.basis/tools.json", &["deploy"])];
let first = DeclaredTools::register(Arc::clone(&runtime), Path::new("/repo"), &sources)
.expect("registers");
drop(first);
DeclaredTools::register(runtime, Path::new("/repo"), &sources)
.expect("the same workspace registers its own tool again");
}
#[test]
fn two_opens_of_one_workspace_both_keep_their_tools() {
let runtime = runtime();
let sources = [source("/repo/.basis/tools.json", &["deploy"])];
let first = DeclaredTools::register(Arc::clone(&runtime), Path::new("/repo"), &sources)
.expect("registers");
let second = DeclaredTools::register(Arc::clone(&runtime), Path::new("/repo"), &sources)
.expect("registers");
drop(first);
let error = DeclaredTools::register(
Arc::clone(&runtime),
Path::new("/elsewhere"),
&[source("/elsewhere/.basis/tools.json", &["deploy"])],
)
.expect_err("still held");
assert!(
matches!(error, DeclaredToolError::NameTaken { .. }),
"{error}"
);
drop(second);
DeclaredTools::register(
runtime,
Path::new("/elsewhere"),
&[source("/elsewhere/.basis/tools.json", &["deploy"])],
)
.expect("the last holder released it");
}
#[test]
fn a_collision_partway_through_registers_nothing_at_all() {
let runtime = runtime();
let _held = DeclaredTools::register(
Arc::clone(&runtime),
Path::new("/repo/one"),
&[source("/repo/one/.basis/tools.json", &["taken"])],
)
.expect("registers");
let error = DeclaredTools::register(
Arc::clone(&runtime),
Path::new("/repo/two"),
&[source("/repo/two/.basis/tools.json", &["fine", "taken"])],
)
.expect_err("refused");
assert!(
matches!(error, DeclaredToolError::NameTaken { .. }),
"{error}"
);
assert!(
!runtime
.mentra_runtime()
.tools()
.iter()
.any(|tool| tool.provider.name == "fine"),
"a workspace that failed to open must leave nothing in a shared roster"
);
DeclaredTools::register(
runtime,
Path::new("/repo/three"),
&[source("/repo/three/.basis/tools.json", &["fine"])],
)
.expect("and must hold no claim on the name it half-took either");
}
#[test]
fn one_workspaces_tool_is_not_offered_to_another_on_the_same_runtime() {
let runtime = runtime();
let _held = DeclaredTools::register(
Arc::clone(&runtime),
Path::new("/repo/one"),
&[source("/repo/one/.basis/tools.json", &["deploy"])],
)
.expect("registers");
assert_eq!(
runtime.foreign_declared_tools(Path::new("/repo/two")),
vec!["deploy".to_string()],
"a program one repository declared is not the other's to run"
);
assert!(
runtime
.foreign_declared_tools(Path::new("/repo/one"))
.is_empty()
);
}
}