use std::collections::BTreeMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use crate::{
CallRequest, Caveats, DischargeProvider, DischargeVerifier, Gate, StepUpPolicy, Tool,
ToolError, ToolResult,
};
struct StepUp {
policy: StepUpPolicy,
provider: Arc<dyn DischargeProvider + Send + Sync>,
verifier: Arc<dyn DischargeVerifier + Send + Sync>,
}
pub struct Registry {
tools: BTreeMap<String, Arc<dyn Tool>>,
generation: u64,
step_up: Option<StepUp>,
step_up_nonce: AtomicU64,
}
impl Registry {
#[must_use]
pub fn builder() -> RegistryBuilder {
RegistryBuilder::default()
}
#[must_use]
pub fn tool_definitions(&self) -> Vec<serde_json::Value> {
self.tools
.values()
.map(|t| {
serde_json::json!({
"name": t.name(),
"inputSchema": t.schema(),
})
})
.collect()
}
#[must_use]
pub fn tool_names(&self) -> Vec<&str> {
self.tools.keys().map(String::as_str).collect()
}
#[must_use]
pub fn contains(&self, name: &str) -> bool {
self.tools.contains_key(name)
}
pub async fn dispatch(
&self,
name: &str,
args: serde_json::Value,
granted: &Caveats,
) -> ToolResult<serde_json::Value> {
let tool = self
.tools
.get(name)
.ok_or_else(|| ToolError::not_found(name))?;
let gate = self.gate_for(granted);
let cx = match &self.step_up {
Some(su) => {
let request = CallRequest::unspecified(name);
let mut nonce = [0u8; 32];
let n = self.step_up_nonce.fetch_add(1, Ordering::Relaxed);
nonce[..8].copy_from_slice(&n.to_le_bytes());
let (cx, _attestation) = gate.authorize_step_up(
tool.as_ref(),
granted,
&request,
&su.policy,
su.provider.as_ref(),
su.verifier.as_ref(),
nonce,
)?;
cx
}
None => gate.authorize(tool.as_ref(), granted)?,
};
tool.invoke(args, &cx).await
}
fn gate_for(&self, granted: &Caveats) -> Gate {
Gate::with_budget(self.generation, granted.max_calls)
}
}
#[derive(Default)]
pub struct RegistryBuilder {
tools: BTreeMap<String, Arc<dyn Tool>>,
generation: u64,
step_up: Option<StepUp>,
}
impl RegistryBuilder {
#[must_use]
pub fn tool(mut self, tool: Arc<dyn Tool>) -> Self {
self.tools.insert(tool.name().to_string(), tool);
self
}
#[must_use]
pub fn generation(mut self, generation: u64) -> Self {
self.generation = generation;
self
}
#[must_use]
pub fn step_up(
mut self,
policy: StepUpPolicy,
provider: Arc<dyn DischargeProvider + Send + Sync>,
verifier: Arc<dyn DischargeVerifier + Send + Sync>,
) -> Self {
self.step_up = Some(StepUp {
policy,
provider,
verifier,
});
self
}
#[must_use]
pub fn build(self) -> Registry {
Registry {
tools: self.tools,
generation: self.generation,
step_up: self.step_up,
step_up_nonce: AtomicU64::new(0),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{CountBound, Scope, ToolContext};
struct ProbeTool;
#[async_trait::async_trait]
impl Tool for ProbeTool {
fn name(&self) -> &str {
"probe"
}
fn schema(&self) -> serde_json::Value {
serde_json::json!({ "type": "object" })
}
async fn invoke(
&self,
args: serde_json::Value,
cx: &ToolContext,
) -> ToolResult<serde_json::Value> {
let program = args["program"].as_str().unwrap_or("");
cx.check_exec(program)?;
Ok(serde_json::json!({ "ran": program }))
}
}
fn reg() -> Registry {
Registry::builder().tool(Arc::new(ProbeTool)).build()
}
fn block_on<F: std::future::Future>(fut: F) -> F::Output {
use std::task::{Context, Poll, Waker};
let mut cx = Context::from_waker(Waker::noop());
let mut fut = std::pin::pin!(fut);
loop {
match fut.as_mut().poll(&mut cx) {
Poll::Ready(out) => return out,
Poll::Pending => std::thread::yield_now(),
}
}
}
#[test]
fn dispatch_unknown_tool_is_not_found() {
let r = reg();
let err = block_on(r.dispatch("nope", serde_json::json!({}), &Caveats::top())).unwrap_err();
assert!(matches!(err, ToolError::NotFound { .. }));
}
#[test]
fn dispatch_runs_in_scope_and_denies_out_of_scope() {
let r = reg();
let granted = Caveats {
exec: Scope::only(["echo".to_string()]),
..Caveats::top()
};
let ok = block_on(r.dispatch("probe", serde_json::json!({ "program": "echo" }), &granted))
.unwrap();
assert_eq!(ok["ran"], "echo");
let denied =
block_on(r.dispatch("probe", serde_json::json!({ "program": "rm" }), &granted))
.unwrap_err();
assert!(matches!(denied, ToolError::Denied { .. }));
}
#[test]
fn dispatch_budget_two_then_denied() {
let granted = Caveats {
max_calls: CountBound::AtMost(2),
..Caveats::top()
};
let gate = Gate::with_budget(0, CountBound::AtMost(2));
let tool = ProbeTool;
assert!(gate.authorize(&tool, &granted).is_ok());
assert!(gate.authorize(&tool, &granted).is_ok());
assert!(matches!(
gate.authorize(&tool, &granted).unwrap_err(),
ToolError::Budget
));
}
struct FailingProvider;
impl crate::DischargeProvider for FailingProvider {
fn obtain(
&self,
_request: &crate::CallRequest,
_required: &crate::AttestRequirement,
_generation: u64,
_nonce: &[u8; 32],
) -> Result<crate::Discharge, String> {
Err("test: no authenticator present".into())
}
}
struct StubVerifier;
impl crate::DischargeVerifier for StubVerifier {
fn verify(
&self,
_discharge: &crate::Discharge,
_required: &crate::AttestRequirement,
_expected: &crate::Challenge,
) -> Result<(), String> {
Ok(()) }
}
#[test]
fn step_up_policy_demands_a_gesture_on_the_default_path() {
let policy = crate::StepUpPolicy::new(
vec![crate::Rule {
selector: "probe".to_string(),
requirement: crate::AttestRequirement::passkey_recorded(),
}],
crate::AttestRequirement::NONE,
);
let r = Registry::builder()
.tool(Arc::new(ProbeTool))
.step_up(policy, Arc::new(FailingProvider), Arc::new(StubVerifier))
.build();
let granted = Caveats {
exec: Scope::only(["echo".to_string()]),
..Caveats::top()
};
let err = block_on(r.dispatch("probe", serde_json::json!({ "program": "echo" }), &granted))
.unwrap_err();
assert!(
matches!(err, ToolError::Denied { .. }),
"a demanded-but-refused gesture must fail closed: {err:?}"
);
}
#[test]
fn step_up_holds_at_maximal_authority() {
let policy = crate::StepUpPolicy::new(
vec![crate::Rule {
selector: "probe".to_string(),
requirement: crate::AttestRequirement::passkey_recorded(),
}],
crate::AttestRequirement::NONE,
);
let r = Registry::builder()
.tool(Arc::new(ProbeTool))
.step_up(policy, Arc::new(FailingProvider), Arc::new(StubVerifier))
.build();
let err = block_on(r.dispatch(
"probe",
serde_json::json!({ "program": "echo" }),
&Caveats::top(),
))
.unwrap_err();
assert!(
matches!(err, ToolError::Denied { .. }),
"maximal (top) authority must still owe the step-up gesture: {err:?}"
);
}
#[test]
fn tool_definitions_have_name_and_schema() {
let r = reg();
let defs = r.tool_definitions();
assert_eq!(defs.len(), 1);
assert_eq!(defs[0]["name"], "probe");
assert!(defs[0]["inputSchema"].is_object());
assert!(r.contains("probe"));
assert_eq!(r.tool_names(), vec!["probe"]);
}
}