use polyc_capability::{
Capability, CapabilitySet, Ceremony, Requirement, ToolOrigin, ToolProfile,
required_capabilities,
};
use polyc_llm::ToolSpec;
use crate::{
ToolRegistry, ask_question, coding, conversation, demote, email_link, invite, list_admins,
memory, paid_fetch, peer, promote, provisional_persona, revoke, routine, unlink_identity,
unlink_self, wallet, web,
};
#[must_use]
pub fn builtin_origin(name: &str) -> Option<ToolOrigin> {
if coding::owns(name) {
return Some(ToolOrigin::LocalSandbox);
}
if ask_question::ALL.contains(&name) {
return Some(ToolOrigin::LocalSandbox);
}
if name == web::WEB_FETCH || name == paid_fetch::TOOL_NAME {
return Some(ToolOrigin::Fetcher);
}
if invite::ALL.contains(&name) {
return Some(ToolOrigin::AccessGrant);
}
if revoke::ALL.contains(&name) {
return Some(ToolOrigin::AccessRevoke);
}
if unlink_identity::ALL.contains(&name)
|| name == provisional_persona::DELETE_PROVISIONAL_PERSONA
{
return Some(ToolOrigin::AccessRevoke);
}
if demote::ALL.contains(&name) {
return Some(ToolOrigin::AdminManage);
}
if promote::ALL.contains(&name) {
return Some(ToolOrigin::AdminGrant);
}
if conversation::ALL.contains(&name)
|| list_admins::ALL.contains(&name)
|| wallet::ALL.contains(&name)
|| email_link::ALL.contains(&name)
|| memory::ALL.contains(&name)
|| routine::ALL.contains(&name)
|| unlink_self::ALL.contains(&name)
|| name == peer::PEER_CALL
|| name == provisional_persona::LIST_PROVISIONAL_PERSONAS
{
return Some(ToolOrigin::FirstParty);
}
None
}
#[must_use]
pub fn builtin_requirements(name: &str) -> Option<&'static [Requirement]> {
if name == wallet::WALLET_LINK || name == wallet::WALLET_UPDATE_LIMIT {
return Some(&[Requirement::CeremonyPage(Ceremony::WalletLink)]);
}
if name == email_link::LINK_EMAIL {
return Some(&[
Requirement::CeremonyPage(Ceremony::EmailMagicLink),
Requirement::MailRelay,
]);
}
if name == paid_fetch::TOOL_NAME {
return Some(&[Requirement::PaymentsProxy]);
}
if wallet::ALL.contains(&name)
|| unlink_self::ALL.contains(&name)
|| coding::owns(name)
|| ask_question::ALL.contains(&name)
|| name == web::WEB_FETCH
|| invite::ALL.contains(&name)
|| revoke::ALL.contains(&name)
|| demote::ALL.contains(&name)
|| promote::ALL.contains(&name)
|| unlink_identity::ALL.contains(&name)
|| name == provisional_persona::DELETE_PROVISIONAL_PERSONA
|| name == provisional_persona::LIST_PROVISIONAL_PERSONAS
|| conversation::ALL.contains(&name)
|| list_admins::ALL.contains(&name)
|| memory::ALL.contains(&name)
|| routine::ALL.contains(&name)
|| name == peer::PEER_CALL
{
return Some(&[]);
}
None
}
#[must_use]
pub fn required_for_spec(spec: &ToolSpec) -> CapabilitySet {
if spec.name == "shell_exec" {
return CapabilitySet::of(Capability::LocalRead)
.with(Capability::LocalWrite)
.with(Capability::ArbitraryEgress);
}
let origin = builtin_origin(&spec.name).unwrap_or(ToolOrigin::Unknown);
required_capabilities(ToolProfile::for_spec(spec, origin))
}
#[must_use]
pub fn required_for_builtin(name: &str, specs: &[ToolSpec]) -> CapabilitySet {
specs
.iter()
.find(|s| s.name == name)
.map_or_else(CapabilitySet::all, required_for_spec)
}
pub mod management {
use std::sync::LazyLock;
use crate::{
demote, email_link, invite, list_admins, memory, promote, provisional_persona, revoke,
routine, unlink_identity, unlink_self, wallet,
};
pub static ALL: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
wallet::ALL
.iter()
.chain(email_link::ALL)
.chain(invite::ALL)
.chain(revoke::ALL)
.chain(demote::ALL)
.chain(promote::ALL)
.chain(list_admins::ALL)
.chain(memory::ALL)
.chain(unlink_identity::ALL)
.chain(unlink_self::ALL)
.chain(provisional_persona::ALL)
.chain(routine::ALL)
.copied()
.collect()
});
#[must_use]
pub fn is_management_builtin(name: &str) -> bool {
ALL.contains(&name)
}
#[must_use]
pub fn all_specs() -> Vec<polyc_llm::ToolSpec> {
wallet::all_specs()
.into_iter()
.chain(email_link::all_specs())
.chain(invite::all_specs())
.chain(revoke::all_specs())
.chain(demote::all_specs())
.chain(promote::all_specs())
.chain(list_admins::all_specs())
.chain(memory::all_specs())
.chain(unlink_identity::all_specs())
.chain(unlink_self::all_specs())
.chain(provisional_persona::all_specs())
.chain(routine::all_specs())
.collect()
}
}
#[must_use]
pub fn floor_requirements(name: &str) -> CapabilitySet {
if name == web::WEB_FETCH || name == paid_fetch::TOOL_NAME {
let spec = if name == web::WEB_FETCH {
web::fetch_spec()
} else {
paid_fetch::spec()
};
required_capabilities(ToolProfile::for_spec(&spec, ToolOrigin::Fetcher))
} else if name == web::NATIVE_SEARCH_GROUNDING {
native_search_grounding_requirements()
} else {
CapabilitySet::EMPTY
}
}
#[must_use]
pub const fn native_search_grounding_requirements() -> CapabilitySet {
CapabilitySet::native_search_grounding_requirements()
}
fn all_builtin_specs() -> Vec<ToolSpec> {
let mut specs = ToolRegistry::specs_with_payments(true);
specs.extend(conversation::all_specs());
specs.extend(management::all_specs());
specs.push(peer::spec(&["peer".to_owned()]));
specs
}
#[must_use]
pub fn unclassified_builtins() -> Vec<String> {
all_builtin_specs()
.iter()
.filter(|s| required_for_spec(s) == CapabilitySet::all())
.map(|s| s.name.clone())
.collect()
}
pub fn assert_builtins_classified() {
let unclassified = unclassified_builtins();
assert!(
unclassified.is_empty(),
"built-in tools failed capability classification (add annotations or \
extend polyc_tools::capability::builtin_origin): {unclassified:?}"
);
}
#[must_use]
pub fn unresolved_builtin_requirements() -> Vec<String> {
all_builtin_specs()
.iter()
.filter(|s| builtin_requirements(&s.name).is_none())
.map(|s| s.name.clone())
.collect()
}
pub fn assert_builtin_requirements_resolved() {
let unresolved = unresolved_builtin_requirements();
assert!(
unresolved.is_empty(),
"built-in tools failed deployment-requirement resolution (extend \
polyc_tools::capability::builtin_requirements): {unresolved:?}"
);
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs)]
use polyc_capability::Capability;
use serde_json::json;
use super::*;
const fn set(caps: &[Capability]) -> CapabilitySet {
let mut s = CapabilitySet::EMPTY;
let mut i = 0;
while i < caps.len() {
s = s.with(caps[i]);
i += 1;
}
s
}
#[test]
fn no_builtin_is_both_approval_required_and_cacheable() {
let both: Vec<String> = all_builtin_specs()
.iter()
.filter(|s| s.needs_approval && s.cacheable_approval)
.map(|s| s.name.clone())
.collect();
assert!(
both.is_empty(),
"these built-ins are both approval_required and cacheable_approval: {both:?}. The \
control plane's trusted-side approval recheck (#1702) matches a brokered gated call \
against an approval bound to that call's own tool-call id, and a remembered session \
grant carries a DIFFERENT id from an earlier turn — so a tool that is both would \
have its legitimate session-approved calls refused. Teach the recheck about session \
grants before making any gated tool cacheable."
);
}
#[test]
fn builtin_derivation_matches_the_capability_table() {
let specs = all_builtin_specs();
for name in ["file_read", "grep", "glob"] {
assert_eq!(
required_for_builtin(name, &specs),
set(&[Capability::LocalRead]),
"{name}"
);
}
for name in ["file_write", "file_edit"] {
assert_eq!(
required_for_builtin(name, &specs),
set(&[Capability::LocalRead, Capability::LocalWrite]),
"{name}"
);
}
assert_eq!(
required_for_builtin("shell_exec", &specs),
set(&[
Capability::LocalRead,
Capability::LocalWrite,
Capability::ArbitraryEgress,
])
);
assert_eq!(
required_for_builtin("web_fetch", &specs),
set(&[Capability::ArbitraryEgress])
);
assert_eq!(
required_for_builtin("paid_fetch", &specs),
set(&[Capability::ArbitraryEgress, Capability::MutateExternal])
);
for name in conversation::ALL {
assert_eq!(
required_for_builtin(name, &specs),
set(&[Capability::FixedConnectorRead]),
"{name}"
);
}
assert_eq!(
required_for_builtin(memory::MEMORY_RECALL, &specs),
set(&[Capability::FixedConnectorRead])
);
for name in [
"wallet_status",
"wallet_history",
"wallet_roster",
routine::ROUTINE_LIST,
] {
assert_eq!(
required_for_builtin(name, &specs),
set(&[Capability::FixedConnectorRead]),
"{name}"
);
}
for name in [
"wallet_link",
"wallet_set_policy",
"wallet_update_limit",
"peer_call",
"link_email",
unlink_self::TOOL_NAME,
"memory_write",
routine::ROUTINE_CREATE,
] {
assert_eq!(
required_for_builtin(name, &specs),
set(&[Capability::FixedConnectorRead, Capability::MutateExternal]),
"{name}"
);
}
assert_eq!(
required_for_builtin("no_such_tool", &specs),
CapabilitySet::all()
);
let stray = ToolSpec::new("mystery", "d", json!({})).read_only();
assert_eq!(required_for_spec(&stray), CapabilitySet::all());
}
#[test]
fn invite_requires_grant_access_and_always_escalates() {
use polyc_capability::{
CallPolicy, GateOutcome, GrantPolicy, TaintState, decide, granted_capabilities,
};
let specs = all_builtin_specs();
let required = required_for_builtin(invite::TOOL_NAME, &specs);
assert_eq!(
required,
set(&[Capability::GrantAccess]),
"the invite tool requires only the never-granted access-grant marker"
);
for taint in [TaintState::Clean, TaintState::Tainted] {
let granted = granted_capabilities(GrantPolicy::default(), taint);
let out = decide(required, granted, &CallPolicy::default(), invite::TOOL_NAME);
assert!(
matches!(out, GateOutcome::Escalate { .. }),
"invite must escalate under {taint:?}, got {out:?}"
);
}
}
#[test]
fn revoke_requires_revoke_access_and_always_escalates() {
use polyc_capability::{
CallPolicy, GateOutcome, GrantPolicy, TaintState, decide, granted_capabilities,
};
let specs = all_builtin_specs();
let required = required_for_builtin(revoke::TOOL_NAME, &specs);
assert_eq!(
required,
set(&[Capability::RevokeAccess]),
"the revoke tool requires only the never-granted revoke-access marker"
);
for taint in [TaintState::Clean, TaintState::Tainted] {
let granted = granted_capabilities(GrantPolicy::default(), taint);
let out = decide(required, granted, &CallPolicy::default(), revoke::TOOL_NAME);
assert!(
matches!(out, GateOutcome::Escalate { .. }),
"revoke must escalate under {taint:?}, got {out:?}"
);
}
}
#[test]
fn demote_requires_manage_admin_and_always_escalates() {
use polyc_capability::{
CallPolicy, GateOutcome, GrantPolicy, TaintState, decide, granted_capabilities,
};
let specs = all_builtin_specs();
let required = required_for_builtin(demote::TOOL_NAME, &specs);
assert_eq!(
required,
set(&[Capability::ManageAdmin]),
"the demote tool requires only the never-granted manage-admin marker"
);
for taint in [TaintState::Clean, TaintState::Tainted] {
let granted = granted_capabilities(GrantPolicy::default(), taint);
let out = decide(required, granted, &CallPolicy::default(), demote::TOOL_NAME);
assert!(
matches!(out, GateOutcome::Escalate { .. }),
"demote must escalate under {taint:?}, got {out:?}"
);
}
}
#[test]
fn promote_requires_grant_admin_and_always_escalates() {
use polyc_capability::{
CallPolicy, GateOutcome, GrantPolicy, TaintState, decide, granted_capabilities,
};
let specs = all_builtin_specs();
let required = required_for_builtin(promote::TOOL_NAME, &specs);
assert_eq!(
required,
set(&[Capability::GrantAdmin]),
"the promote tool requires only the never-granted grant-admin marker"
);
for taint in [TaintState::Clean, TaintState::Tainted] {
let granted = granted_capabilities(GrantPolicy::default(), taint);
let out = decide(
required,
granted,
&CallPolicy::default(),
promote::TOOL_NAME,
);
assert!(
matches!(out, GateOutcome::Escalate { .. }),
"promote must escalate under {taint:?}, got {out:?}"
);
}
}
#[test]
fn unlink_identity_requires_revoke_access_and_always_escalates() {
use polyc_capability::{
CallPolicy, GateOutcome, GrantPolicy, TaintState, decide, granted_capabilities,
};
let specs = all_builtin_specs();
let required = required_for_builtin(unlink_identity::TOOL_NAME, &specs);
assert_eq!(
required,
set(&[Capability::RevokeAccess]),
"unlink_identity requires only the never-granted revoke-access marker"
);
for taint in [TaintState::Clean, TaintState::Tainted] {
let granted = granted_capabilities(GrantPolicy::default(), taint);
let out = decide(
required,
granted,
&CallPolicy::default(),
unlink_identity::TOOL_NAME,
);
assert!(
matches!(out, GateOutcome::Escalate { .. }),
"unlink_identity must escalate under {taint:?}, got {out:?}"
);
}
}
#[test]
fn delete_provisional_persona_requires_revoke_access_and_always_escalates() {
use polyc_capability::{
CallPolicy, GateOutcome, GrantPolicy, TaintState, decide, granted_capabilities,
};
let specs = all_builtin_specs();
let required =
required_for_builtin(provisional_persona::DELETE_PROVISIONAL_PERSONA, &specs);
assert_eq!(
required,
set(&[Capability::RevokeAccess]),
"delete_provisional_persona requires only the never-granted revoke-access marker"
);
for taint in [TaintState::Clean, TaintState::Tainted] {
let granted = granted_capabilities(GrantPolicy::default(), taint);
let out = decide(
required,
granted,
&CallPolicy::default(),
provisional_persona::DELETE_PROVISIONAL_PERSONA,
);
assert!(
matches!(out, GateOutcome::Escalate { .. }),
"delete_provisional_persona must escalate under {taint:?}, got {out:?}"
);
}
}
#[test]
fn list_provisional_personas_does_not_always_escalate() {
use polyc_capability::{
CallPolicy, GateOutcome, GrantPolicy, TaintState, decide, granted_capabilities,
};
let specs = all_builtin_specs();
let required = required_for_builtin(provisional_persona::LIST_PROVISIONAL_PERSONAS, &specs);
assert!(
!required.contains(Capability::RevokeAccess)
&& !required.contains(Capability::GrantAccess)
&& !required.contains(Capability::ManageAdmin)
&& !required.contains(Capability::GrantAdmin),
"list_provisional_personas must not require a never-granted marker: {required:?}"
);
let granted = granted_capabilities(GrantPolicy::default(), TaintState::Clean);
let out = decide(
required,
granted,
&CallPolicy::default(),
provisional_persona::LIST_PROVISIONAL_PERSONAS,
);
assert!(
!matches!(out, GateOutcome::Escalate { .. }),
"list_provisional_personas should not always escalate, got {out:?}"
);
}
#[test]
fn every_builtin_spec_classifies_out_of_the_privileged_bucket() {
assert_eq!(unclassified_builtins(), Vec::<String>::new());
assert_builtins_classified();
}
#[test]
fn floor_pins_the_fetchers_regardless_of_owner_claims() {
assert_eq!(
floor_requirements("web_fetch"),
set(&[Capability::ArbitraryEgress])
);
assert_eq!(
floor_requirements("paid_fetch"),
set(&[Capability::ArbitraryEgress, Capability::MutateExternal])
);
assert_eq!(floor_requirements("file_read"), CapabilitySet::EMPTY);
assert_eq!(floor_requirements(""), CapabilitySet::EMPTY);
}
#[test]
fn ask_question_classifies_as_local_sandbox_with_minimal_capability() {
assert_eq!(
builtin_origin(ask_question::TOOL_NAME),
Some(ToolOrigin::LocalSandbox)
);
assert_eq!(
required_for_spec(&ask_question::spec()),
set(&[Capability::LocalRead]),
"a read-only local-sandbox tool requires only LocalRead, never the \
fail-closed privileged set"
);
}
#[test]
fn native_search_grounding_requires_arbitrary_egress() {
assert_eq!(
native_search_grounding_requirements(),
set(&[Capability::ArbitraryEgress])
);
assert_eq!(
floor_requirements(web::NATIVE_SEARCH_GROUNDING),
set(&[Capability::ArbitraryEgress])
);
}
#[test]
fn management_all_spans_exactly_the_twelve_proxied_families() {
let mut expected = vec![
"demote",
"promote",
"delete_provisional_persona",
"invite",
"link_email",
list_admins::TOOL_NAME,
"list_provisional_personas",
"memory_write",
memory::MEMORY_RECALL,
"revoke",
routine::ROUTINE_LIST,
routine::ROUTINE_CREATE,
routine::ROUTINE_PAUSE,
routine::ROUTINE_RESUME,
routine::ROUTINE_DELETE,
routine::ROUTINE_FIRE,
routine::ROUTINE_DUPLICATE,
routine::ROUTINE_SET_SCOPE,
routine::ROUTINE_ALLOW_DENIAL,
routine::ROUTINE_REVOKE_GRANT,
routine::ROUTINE_REFIRE_ATTENDED,
"unlink_identity",
unlink_self::TOOL_NAME,
"wallet_history",
"wallet_deposit_address",
"wallet_link",
"wallet_roster",
"wallet_set_policy",
"wallet_status",
"wallet_update_limit",
];
expected.sort_unstable();
let mut actual: Vec<&str> = management::ALL.iter().copied().collect();
actual.sort_unstable();
assert_eq!(
actual, expected,
"management::ALL must span exactly these 30 names across the twelve \
proxied families (wallet, email_link, invite, revoke, demote, promote, \
list_admins, memory, unlink_identity, unlink_self, \
provisional_persona, routine) — no more, no less"
);
for name in [
"shell_exec",
"file_read",
"web_fetch",
"paid_fetch",
"peer_call",
]
.into_iter()
.chain(conversation::ALL.iter().copied())
{
assert!(
!management::is_management_builtin(name),
"{name} is not a management built-in — the coding core, fetchers, \
the own-conversation read family, and peer delegation default \
on and are not grant-gated the same way"
);
}
}
#[test]
fn the_own_conversation_read_family_defaults_on() {
for name in conversation::ALL {
assert!(
!management::is_management_builtin(name),
"{name} must not be a management built-in"
);
assert!(
crate::mcp_client::builtin_admits(name, None, &[]),
"{name} must compose for a turn that carried no grant resolution"
);
assert!(
crate::mcp_client::builtin_admits(name, Some(&[(*name).to_owned()]), &[]),
"{name} must compose when a grant names it explicitly"
);
assert!(
!crate::mcp_client::builtin_admits(name, Some(&["something_else".to_owned()]), &[]),
"{name} must still honor an explicit allowlist that omits it"
);
}
}
#[test]
fn memory_recall_never_defaults_on() {
assert!(
management::is_management_builtin(memory::MEMORY_RECALL),
"recall must sit in the management envelope, not the default-on read family"
);
assert!(
!crate::mcp_client::builtin_admits(memory::MEMORY_RECALL, None, &[]),
"a turn that carried no grant resolution must not compose memory_recall"
);
assert!(
crate::mcp_client::builtin_admits(
memory::MEMORY_RECALL,
Some(&[memory::MEMORY_RECALL.to_owned()]),
&[]
),
"an explicit grant is what makes it exist for an agent"
);
assert_eq!(
builtin_origin(memory::MEMORY_RECALL),
Some(ToolOrigin::FirstParty),
"a read of the caller's own memory through the control plane"
);
assert_eq!(
builtin_requirements(memory::MEMORY_RECALL),
Some(&[][..]),
"a read of already-recorded notes needs nothing from this deployment's config"
);
}
#[test]
fn every_builtin_spec_resolves_its_requirements() {
assert_eq!(unresolved_builtin_requirements(), Vec::<String>::new());
assert_builtin_requirements_resolved();
}
#[test]
fn builtin_requirements_matches_the_1412_incident_tools() {
assert_eq!(
builtin_requirements(wallet::WALLET_LINK),
Some(&[Requirement::CeremonyPage(Ceremony::WalletLink)][..])
);
assert_eq!(
builtin_requirements(wallet::WALLET_UPDATE_LIMIT),
Some(&[Requirement::CeremonyPage(Ceremony::WalletLink)][..]),
"shares the same deployed ceremony page as wallet_link"
);
assert_eq!(
builtin_requirements(email_link::LINK_EMAIL),
Some(
&[
Requirement::CeremonyPage(Ceremony::EmailMagicLink),
Requirement::MailRelay
][..]
)
);
assert_eq!(
builtin_requirements(paid_fetch::TOOL_NAME),
Some(&[Requirement::PaymentsProxy][..])
);
}
#[test]
fn read_only_reports_carry_no_requirement() {
for name in [
wallet::WALLET_STATUS,
wallet::WALLET_HISTORY,
wallet::WALLET_ROSTER,
wallet::WALLET_DEPOSIT_ADDRESS,
wallet::WALLET_SET_POLICY,
] {
assert_eq!(
builtin_requirements(name),
Some(&[][..]),
"{name} must carry no deployment requirement"
);
}
}
#[test]
fn builtin_requirements_fails_closed_on_an_unknown_name() {
assert_eq!(builtin_requirements("no_such_tool"), None);
}
}