use async_trait::async_trait;
use polyc_agent::ToolExecutor;
use polyc_llm::ToolSpec;
pub mod approval;
pub mod ask_question;
pub mod capability;
pub mod coding;
pub mod connection_pool;
pub mod connector_error;
pub mod conversation;
pub mod demote;
pub mod email_link;
pub mod invite;
pub mod list_admins;
pub mod mcp_client;
pub mod mcp_server;
pub mod memory;
pub mod paid_fetch;
pub mod peer;
pub mod provisional_persona;
pub mod revoke;
pub mod routine;
pub mod unlink_identity;
pub mod unlink_self;
pub mod wallet;
pub mod web;
pub use approval::{ApprovalMode, RiskTier, auto_review_eligible, classify_tier, classify_tool};
pub use capability::{
assert_builtin_requirements_resolved, assert_builtins_classified, builtin_requirements,
management, unclassified_builtins,
};
pub use coding::{SandboxMode, current_sandbox_mode, sandbox_would_deny};
pub use connection_pool::ConnectionPool;
pub use connector_error::{
CallRetryPolicy, ConnectorErrorKind, dial_failure_message, failure_json,
transport_failure_message,
};
pub use mcp_client::{
ApprovalPolicy, AudienceBoundToken, CALLER_HEADER, CONNECTOR_TOOL_SEPARATOR, CompositeRegistry,
ConnectOptions, ConnectorProvenance, DEFAULT_CONNECT_TIMEOUT, McpClientError, McpToolSource,
SpecSource, builtin_admits, core_admits_builtin, is_valid_connector_label,
};
pub use mcp_server::serve;
#[derive(Clone, Default, Debug)]
pub struct ToolRegistry {
allowed: Option<std::sync::Arc<std::collections::BTreeSet<String>>>,
workspace_root: Option<std::path::PathBuf>,
}
impl ToolRegistry {
#[must_use]
pub fn scoped(names: impl IntoIterator<Item = String>) -> Self {
Self {
allowed: Some(std::sync::Arc::new(names.into_iter().collect())),
workspace_root: None,
}
}
#[must_use]
pub const fn rooted_at(
root: std::path::PathBuf,
allowed: Option<std::sync::Arc<std::collections::BTreeSet<String>>>,
) -> Self {
Self {
allowed,
workspace_root: Some(root),
}
}
fn effective_root(&self) -> std::path::PathBuf {
self.workspace_root
.clone()
.unwrap_or_else(coding::workspace::root)
}
fn permits(&self, name: &str) -> bool {
self.allowed.as_ref().is_none_or(|a| a.contains(name))
}
#[must_use]
pub fn all_specs() -> Vec<ToolSpec> {
Self::specs_with_payments(payments_configured())
}
fn specs_with_payments(payments: bool) -> Vec<ToolSpec> {
let mut specs = coding::specs();
specs.push(web::fetch_spec());
specs.push(ask_question::spec());
if payments {
specs.push(paid_fetch::spec());
}
specs
}
}
fn payments_configured() -> bool {
static CONFIGURED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*CONFIGURED
.get_or_init(|| std::env::var("TEMPO_SIGNER_KEY").is_ok_and(|v| !v.trim().is_empty()))
}
pub const TOOL_NEEDS_APPROVAL_ENV: &str = "POLYCHROME_TOOLS_NEEDS_APPROVAL";
fn parse_needs_approval_list(raw: &str) -> Vec<String> {
raw.split(',')
.map(str::trim)
.filter(|t| !t.is_empty())
.map(str::to_owned)
.collect()
}
fn needs_approval_with(specs: &[ToolSpec], env_list: &[String], name: &str) -> bool {
let intrinsic = specs
.iter()
.find(|s| s.name == name)
.is_some_and(|s| s.needs_approval);
intrinsic || env_list.iter().any(|n| n == name)
}
fn sandbox_gated(specs: &[ToolSpec], name: &str, mode: coding::SandboxMode) -> bool {
mode == coding::SandboxMode::ReadOnly
&& specs
.iter()
.find(|s| s.name == name)
.is_some_and(|s| s.destructive)
}
fn needs_approval_set() -> Vec<String> {
std::env::var(TOOL_NEEDS_APPROVAL_ENV)
.ok()
.map(|s| parse_needs_approval_list(&s))
.unwrap_or_default()
}
#[async_trait]
impl ToolExecutor for ToolRegistry {
fn specs(&self) -> Vec<ToolSpec> {
let mut specs = Self::all_specs();
specs.retain(|s| self.permits(&s.name));
specs
}
fn needs_approval(&self, name: &str) -> bool {
let specs = Self::all_specs();
needs_approval_with(&specs, &needs_approval_set(), name)
|| sandbox_gated(&specs, name, coding::SandboxMode::from_env())
}
fn sandbox_would_deny(&self, name: &str, args_json: &str) -> bool {
self.permits(name) && coding::sandbox_would_deny(name, args_json)
}
fn required_capabilities(&self, name: &str) -> polyc_capability::CapabilitySet {
if !self.permits(name) {
return polyc_capability::CapabilitySet::all();
}
capability::required_for_builtin(name, &Self::all_specs())
}
fn for_worker(
&self,
scope: &polyc_agent::delegate::WorkerScope<'_>,
) -> Option<Result<polyc_agent::delegate::WorkerHandoff, polyc_agent::delegate::ShareInError>>
{
let parent_root = self.effective_root();
let worker_root = coding::workspace::worker_root(&parent_root, scope.worker_id);
if let Err(err) = std::fs::create_dir_all(&worker_root) {
tracing::warn!(
worker_id = scope.worker_id,
root = %worker_root.display(),
error = %err,
"could not create delegated worker's workspace subtree; re-rooting anyway so the worker cannot reach the shared root"
);
}
let seeded =
match coding::share_in::seed(&parent_root, &worker_root, scope.share_in, scope.ceiling)
{
Ok(seeded) => seeded,
Err(err) => return Some(Err(err)),
};
Some(Ok(polyc_agent::delegate::WorkerHandoff {
tools: std::sync::Arc::new(Self {
allowed: self.allowed.clone(),
workspace_root: Some(worker_root),
}),
seeded,
}))
}
async fn execute(&self, name: &str, args_json: &str) -> String {
if !self.permits(name) {
return serde_json::json!({
"error": format!("tool not available to this agent: {name}"),
})
.to_string();
}
if let Some(out) = coding::execute_rooted(&self.effective_root(), name, args_json).await {
return out;
}
connector_error::failure_json(
connector_error::ConnectorErrorKind::Application,
&format!("unknown tool: {name}"),
)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs)]
use super::*;
use serde_json::Value;
#[test]
fn specs_always_include_ask_question_regardless_of_payments() {
for payments in [true, false] {
let specs = ToolRegistry::specs_with_payments(payments);
assert!(
specs.iter().any(|s| s.name == ask_question::TOOL_NAME),
"ask_question must be advertised regardless of payments={payments}"
);
}
}
#[test]
fn specs_include_paid_fetch_last_when_payments_configured() {
let specs = ToolRegistry::specs_with_payments(true);
assert!(
specs.iter().any(|s| s.name == "paid_fetch"),
"paid_fetch is advertised when a wallet is configured"
);
assert_eq!(
specs.last().map(|s| s.name.as_str()),
Some("paid_fetch"),
"paid_fetch must be the last spec"
);
}
#[test]
fn every_payment_capable_tool_spec_is_destructive() {
assert!(
paid_fetch::spec().destructive,
"paid_fetch settles a real payment and must be destructive"
);
assert!(
unlink_self::unlink_self_spec().destructive,
"unlink_self can remove the caller's own linked wallet and must be destructive"
);
const MUTATES_SPEND_AUTHORITY: &[&str] = &[wallet::WALLET_SET_POLICY];
for spec in wallet::all_specs() {
let should_be_destructive = MUTATES_SPEND_AUTHORITY.contains(&spec.name.as_str());
assert_eq!(
spec.destructive, should_be_destructive,
"{}: destructive={}, expected {should_be_destructive} — a wallet tool that \
moves money or spend authority must be destructive, and a new wallet tool \
must be added to MUTATES_SPEND_AUTHORITY here if it does",
spec.name, spec.destructive
);
}
}
#[test]
fn specs_omit_paid_fetch_without_a_wallet() {
let specs = ToolRegistry::specs_with_payments(false);
assert!(
!specs.iter().any(|s| s.name == "paid_fetch"),
"paid_fetch must NOT be advertised without a wallet"
);
assert!(specs.iter().any(|s| s.name == "shell_exec"));
assert!(specs.iter().any(|s| s.name == "file_read"));
}
#[test]
fn cacheable_approval_marks_idempotent_reads_not_paid_fetch() {
let registry = ToolRegistry::default();
assert!(registry.cacheable_approval("file_read"));
assert!(registry.cacheable_approval("grep"));
assert!(!registry.cacheable_approval("paid_fetch"));
assert!(!registry.cacheable_approval("shell_exec"));
assert!(!registry.cacheable_approval("does_not_exist"));
}
#[test]
fn only_ask_question_is_interactive() {
let interactive: Vec<String> = ToolRegistry::all_specs()
.into_iter()
.filter(|s| s.interactive)
.map(|s| s.name)
.collect();
assert_eq!(interactive, vec![ask_question::TOOL_NAME.to_owned()]);
}
#[test]
fn unscoped_registry_advertises_the_full_builtin_set() {
let names: Vec<String> = ToolRegistry::default()
.specs()
.into_iter()
.map(|s| s.name)
.collect();
assert_eq!(
names,
ToolRegistry::all_specs()
.into_iter()
.map(|s| s.name)
.collect::<Vec<_>>()
);
assert!(names.iter().any(|n| n == "grep"));
assert!(names.iter().any(|n| n == "shell_exec"));
}
#[test]
fn scoped_registry_advertises_only_allowed_builtins() {
let names: Vec<String> = ToolRegistry::scoped(["grep".to_owned()])
.specs()
.into_iter()
.map(|s| s.name)
.collect();
assert_eq!(names, vec!["grep".to_owned()]);
}
#[test]
fn empty_scope_advertises_no_builtins() {
assert!(ToolRegistry::scoped(std::iter::empty()).specs().is_empty());
}
#[tokio::test]
async fn scoped_registry_refuses_a_disallowed_builtin() {
let registry = ToolRegistry::scoped(["grep".to_owned()]);
let out = registry
.execute("shell_exec", r#"{"command":"echo hi"}"#)
.await;
let v: Value = serde_json::from_str(&out).expect("output must be JSON");
assert!(
v["error"]
.as_str()
.unwrap_or_default()
.contains("not available to this agent"),
"a disallowed built-in must be refused, got: {out}"
);
}
#[test]
fn composite_registry_delegates_cacheable_approval() {
use crate::mcp_client::CompositeRegistry;
use std::sync::Arc;
let composite = CompositeRegistry::new().with(Arc::new(ToolRegistry::default()));
assert!(
composite.cacheable_approval("file_read"),
"composite must delegate cacheable_approval to the owning source"
);
assert!(composite.cacheable_approval("grep"));
assert!(!composite.cacheable_approval("does_not_exist"));
}
#[test]
fn composite_registry_classifies_through_the_owner() {
use crate::mcp_client::CompositeRegistry;
use polyc_agent::ToolExecutor as _;
use polyc_capability::{Capability, CapabilitySet};
use std::sync::Arc;
let composite = CompositeRegistry::new().with(Arc::new(ToolRegistry::default()));
assert_eq!(
composite.required_capabilities("web_fetch"),
CapabilitySet::of(Capability::ArbitraryEgress)
);
assert_eq!(
composite.required_capabilities("file_read"),
CapabilitySet::of(Capability::LocalRead)
);
assert_eq!(
composite.required_capabilities("shell_exec"),
CapabilitySet::of(Capability::LocalRead)
.with(Capability::LocalWrite)
.with(Capability::ArbitraryEgress)
);
assert_eq!(
composite.required_capabilities("does_not_exist"),
CapabilitySet::all()
);
}
#[test]
fn own_conversation_reads_classify_taint_immune_through_their_proxy() {
use crate::mcp_client::CompositeRegistry;
use polyc_agent::ToolExecutor as _;
use polyc_capability::{Capability, CapabilitySet};
use std::sync::Arc;
struct ReadProxyStub;
#[async_trait::async_trait]
impl polyc_agent::ToolExecutor for ReadProxyStub {
fn specs(&self) -> Vec<polyc_llm::ToolSpec> {
crate::conversation::all_specs()
}
fn required_capabilities(&self, name: &str) -> CapabilitySet {
self.specs()
.iter()
.find(|s| s.name == name)
.map_or_else(CapabilitySet::all, crate::capability::required_for_spec)
}
async fn execute(&self, _name: &str, _args: &str) -> String {
"{}".to_owned()
}
}
let composite = CompositeRegistry::new().with(Arc::new(ReadProxyStub));
for tool in crate::conversation::ALL {
assert_eq!(
composite.required_capabilities(tool),
CapabilitySet::of(Capability::FixedConnectorRead),
"{tool} is an own-history read: taint must not revoke it"
);
}
}
#[test]
fn registry_classification_matches_the_builtin_table() {
use polyc_agent::ToolExecutor as _;
use polyc_capability::{Capability, CapabilitySet};
let registry = ToolRegistry::default();
assert_eq!(
registry.required_capabilities("web_fetch"),
CapabilitySet::of(Capability::ArbitraryEgress)
);
assert_eq!(
registry.required_capabilities("file_read"),
CapabilitySet::of(Capability::LocalRead)
);
assert_eq!(
registry.required_capabilities("shell_exec"),
CapabilitySet::of(Capability::LocalRead)
.with(Capability::LocalWrite)
.with(Capability::ArbitraryEgress)
);
let scoped = ToolRegistry::scoped(["grep".to_owned()]);
assert_eq!(
scoped.required_capabilities("web_fetch"),
CapabilitySet::all()
);
assert_eq!(
scoped.required_capabilities("grep"),
CapabilitySet::of(Capability::LocalRead)
);
}
#[test]
fn registry_flags_open_world_builtins_as_untrusted_ingress() {
use polyc_agent::ToolExecutor as _;
let registry = ToolRegistry::default();
assert!(registry.ingests_untrusted_content("web_fetch"));
assert!(!registry.ingests_untrusted_content("file_read"));
assert!(!registry.ingests_untrusted_content("grep"));
assert!(!registry.ingests_untrusted_content("shell_exec"));
let closed = ToolSpec::new("remote_read", "d", serde_json::json!({}));
assert!(!closed.open_world);
let open = ToolSpec::new("remote_read", "d", serde_json::json!({})).open_world();
assert!(open.open_world);
}
#[test]
fn composite_registry_delegates_sandbox_would_deny() {
use crate::mcp_client::CompositeRegistry;
use std::sync::Arc;
let composite = CompositeRegistry::new().with(Arc::new(ToolRegistry::default()));
assert!(
composite.sandbox_would_deny("file_write", r#"{"path":"../etc/passwd","content":"x"}"#),
"composite must delegate sandbox_would_deny to the owning source"
);
assert!(
!composite.sandbox_would_deny("file_write", r#"{"path":"src/main.rs","content":"x"}"#)
);
assert!(!composite.sandbox_would_deny("does_not_exist", r#"{"path":"../x"}"#));
}
#[tokio::test]
async fn composite_registry_delegates_for_worker() {
use crate::mcp_client::CompositeRegistry;
use std::sync::Arc;
let root = coding::tmp_dir("composite-for-worker");
let composite =
CompositeRegistry::new().with(Arc::new(ToolRegistry::rooted_at(root.clone(), None)));
let rerooted = composite
.for_worker(&polyc_agent::delegate::WorkerScope::bare("call-a"))
.expect("composite must delegate for_worker to the owning source")
.expect("a bare scope requests no seeding, so nothing can be refused")
.tools;
let out = rerooted
.execute("file_write", r#"{"path":"out.txt","content":"x"}"#)
.await;
assert!(
!out.contains("\"error\""),
"write through re-rooted composite failed: {out}"
);
assert!(
!root.join("out.txt").exists(),
"the write landed at the shared conversation root — for_worker was not honored"
);
let nested: Vec<_> = std::fs::read_dir(&root)
.expect("conversation root is readable")
.flatten()
.filter(|e| e.path().is_dir())
.collect();
assert_eq!(
nested.len(),
1,
"expected exactly one per-worker subdirectory"
);
assert!(nested[0].path().join("out.txt").exists());
let empty = CompositeRegistry::new();
assert!(
empty
.for_worker(&polyc_agent::delegate::WorkerScope::bare("call-a"))
.is_none(),
"a composite with no re-rootable source must return None"
);
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn needs_approval_via_env_paid_fetch() {
let list = parse_needs_approval_list("hash, paid_fetch ,calculator");
assert!(list.iter().any(|n| n == "paid_fetch"));
assert!(parse_needs_approval_list("").is_empty());
assert!(
!parse_needs_approval_list("calculator")
.iter()
.any(|n| n == "paid_fetch")
);
}
#[test]
fn paid_fetch_is_intrinsically_approval_required() {
let specs = ToolRegistry::specs_with_payments(true);
assert!(
needs_approval_with(&specs, &[], "paid_fetch"),
"paid_fetch must need approval even with no env list"
);
assert!(
!needs_approval_with(&specs, &[], "calculator"),
"pure tools must not be approval-gated by default"
);
let extra = parse_needs_approval_list("calculator");
assert!(
needs_approval_with(&specs, &extra, "calculator"),
"env list must still add extra tools"
);
assert!(
needs_approval_with(&specs, &extra, "paid_fetch"),
"intrinsic paid_fetch gate must survive a non-empty env list"
);
assert!(!needs_approval_with(&specs, &extra, "hash"));
}
#[test]
fn gate_reads_per_spec_property() {
let gated = ToolSpec::new("delete_file", "d", serde_json::json!({})).approval_required();
let pure = ToolSpec::new("calculator", "d", serde_json::json!({}));
let specs = [gated, pure];
assert!(needs_approval_with(&specs, &[], "delete_file"));
assert!(!needs_approval_with(&specs, &[], "calculator"));
let env = parse_needs_approval_list("calculator");
assert!(needs_approval_with(&specs, &env, "calculator"));
assert!(!needs_approval_with(&specs, &[], "mystery"));
assert!(needs_approval_with(
&specs,
&parse_needs_approval_list("mystery"),
"mystery"
));
}
#[test]
fn sandbox_gate_reads_destructive_annotation_per_mode() {
use coding::SandboxMode::{DangerFullAccess, ReadOnly, WorkspaceWrite};
let writer = ToolSpec::new("file_write", "d", serde_json::json!({})).destructive();
let reader = ToolSpec::new("file_read", "d", serde_json::json!({})).read_only();
let specs = [writer, reader];
assert!(sandbox_gated(&specs, "file_write", ReadOnly));
assert!(!sandbox_gated(&specs, "file_write", WorkspaceWrite));
assert!(!sandbox_gated(&specs, "file_write", DangerFullAccess));
assert!(!sandbox_gated(&specs, "file_read", ReadOnly));
assert!(!sandbox_gated(&specs, "unknown", ReadOnly));
}
}