use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Write as _;
use serde::{Deserialize, Serialize};
use super::{
AgentAuthority, AssertedEffectFree, Effect, EffectKind, EffectProvenance, Grant, Reversibility,
TenantScope,
};
pub const MANIFEST_SCHEMA_VERSION: u32 = 1;
pub const AGENT_AUTHORITY_MANIFEST_MARKER: &str = "[autumn:agent-authority] ";
pub const DUMP_ENV: &str = "AUTUMN_DUMP_AGENT_AUTHORITY";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AgentAuthorityDescriptor(pub &'static AgentAuthority);
inventory::collect!(AgentAuthorityDescriptor);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GrantDescriptor(pub &'static Grant);
inventory::collect!(GrantDescriptor);
#[derive(Debug, Clone)]
pub struct RouteSummary {
pub method: String,
pub path: String,
pub handler: &'static str,
pub operation_id: &'static str,
pub module_path: &'static str,
pub mcp_tool: bool,
pub exposed_by: Option<McpExposedBy>,
pub agent_authority: Option<&'static AgentAuthority>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum McpExposedBy {
Attribute,
Hatch,
}
impl std::fmt::Display for McpExposedBy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Attribute => "attribute",
Self::Hatch => "hatch",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(clippy::struct_excessive_bools)] pub struct McpExposureInput<'a> {
pub method: &'a str,
pub hidden: bool,
pub mcp_tool: bool,
pub mcp_exclude: bool,
pub mcp_stream: bool,
pub has_response_schema: bool,
pub success_status: u16,
pub expose_all: bool,
}
#[must_use]
pub fn mcp_exposure(input: &McpExposureInput<'_>) -> Option<McpExposedBy> {
if input.hidden || input.mcp_exclude {
return None;
}
let hatch = || {
if input.expose_all && method_is_read_only(input.method) {
Some(McpExposedBy::Hatch)
} else {
None
}
};
if input.mcp_stream {
if input.mcp_tool {
return Some(McpExposedBy::Attribute);
}
return hatch();
}
if !input.has_response_schema && !has_empty_body_contract(input.success_status) {
return None;
}
if input.mcp_tool {
return Some(McpExposedBy::Attribute);
}
hatch()
}
#[must_use]
pub fn method_is_read_only(method: &str) -> bool {
!method_is_mutating(method)
}
const fn has_empty_body_contract(status: u16) -> bool {
matches!(status, 204 | 205)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RouteRef {
pub method: String,
pub path: String,
pub mcp_tool: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GrantSummary {
pub name: String,
pub reversibility: Reversibility,
pub tenant_scope: TenantScope,
#[serde(skip_serializing_if = "Option::is_none")]
pub rate: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub spend: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct EffectRow {
pub kind: EffectKind,
pub subject: String,
pub provenance: EffectProvenance,
pub location: String,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct AssertedEffectFreeRow {
pub location: String,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ActionRow {
pub action: String,
pub module_path: String,
pub location: String,
pub exposure: String,
pub provenance: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub route: Option<RouteRef>,
pub grant: GrantSummary,
pub effects: Vec<EffectRow>,
pub unused_grant_entries: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub asserted_effect_free: Vec<AssertedEffectFreeRow>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GrantRow {
pub name: String,
pub location: String,
pub reversibility: Reversibility,
pub tenant_scope: TenantScope,
pub writes: Vec<String>,
pub unbounded_writes: Vec<String>,
pub outbound: Vec<String>,
pub webhooks: Vec<String>,
pub jobs: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rate: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub spend: Option<String>,
pub used: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UngovernedTool {
pub tool: String,
#[serde(default)]
pub handler: String,
pub method: String,
pub path: String,
pub module_path: String,
pub mutating: bool,
#[serde(default = "default_exposed_by")]
pub exposed_by: McpExposedBy,
}
const fn default_exposed_by() -> McpExposedBy {
McpExposedBy::Attribute
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UnregisteredAuthority {
pub action: String,
pub module_path: String,
pub handler: String,
pub method: String,
pub path: String,
pub mcp_tool: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Excluded {
pub dimension: String,
pub runtime_caveat: String,
#[serde(default = "default_eventual_provenance")]
pub eventual_provenance: String,
}
fn default_eventual_provenance() -> String {
"declared".to_string()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuditStatus {
pub sink_configured: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AgentAuthorityManifest {
pub schema_version: u32,
pub provenance: String,
pub audit: AuditStatus,
pub actions: Vec<ActionRow>,
pub grants: Vec<GrantRow>,
pub ungoverned_tools: Vec<UngovernedTool>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub unregistered_authorities: Vec<UnregisteredAuthority>,
pub excluded: Vec<Excluded>,
}
impl AgentAuthorityManifest {
#[must_use]
pub fn from_parts(
authorities: &[&'static AgentAuthority],
grants: &[&'static Grant],
routes: &[RouteSummary],
audit_sink_configured: bool,
) -> Self {
let mut route_for: BTreeMap<(&str, &str), &RouteSummary> = BTreeMap::new();
for route in routes {
if let Some(authority) = route.agent_authority {
match route_for.entry((authority.module_path, authority.action)) {
std::collections::btree_map::Entry::Vacant(slot) => {
slot.insert(route);
}
std::collections::btree_map::Entry::Occupied(mut slot) => {
if route.mcp_tool && !slot.get().mcp_tool {
slot.insert(route);
}
}
}
}
}
let mut actions: BTreeMap<(&str, &str), ActionRow> = BTreeMap::new();
let mut used_grants: BTreeSet<(&str, &str)> = BTreeSet::new();
let mut all_grants: BTreeMap<(&str, &str), &'static Grant> = BTreeMap::new();
for authority in authorities {
let key = (authority.module_path, authority.action);
used_grants.insert((authority.grant.name, authority.grant.location));
all_grants.insert(
(authority.grant.name, authority.grant.location),
authority.grant,
);
if actions.contains_key(&key) {
continue;
}
let route = route_for.get(&key).copied();
let mut effects: Vec<EffectRow> = authority.effects.iter().map(effect_row).collect();
effects.sort();
effects.dedup();
actions.insert(
key,
ActionRow {
action: authority.action.to_string(),
module_path: authority.module_path.to_string(),
location: authority.location.to_string(),
exposure: exposure_of(route).to_string(),
provenance: row_provenance(authority).to_string(),
route: route.map(|route| RouteRef {
method: route.method.clone(),
path: route.path.clone(),
mcp_tool: route.mcp_tool,
}),
grant: GrantSummary {
name: authority.grant.name.to_string(),
reversibility: authority.grant.reversibility,
tenant_scope: authority.grant.tenant_scope,
rate: authority.grant.rate.map(ToString::to_string),
spend: authority.grant.spend.map(ToString::to_string),
},
effects,
unused_grant_entries: unused_entries(authority.grant, authority.effects),
asserted_effect_free: authority
.asserted_effect_free
.iter()
.map(asserted_effect_free_row)
.collect(),
},
);
}
for grant in grants {
all_grants.insert((grant.name, grant.location), grant);
}
let grants: Vec<GrantRow> = all_grants
.values()
.map(|grant| GrantRow {
name: grant.name.to_string(),
location: grant.location.to_string(),
reversibility: grant.reversibility,
tenant_scope: grant.tenant_scope,
writes: to_owned(grant.writes),
unbounded_writes: to_owned(grant.unbounded_writes),
outbound: to_owned(grant.outbound),
webhooks: to_owned(grant.webhooks),
jobs: to_owned(grant.jobs),
rate: grant.rate.map(ToString::to_string),
spend: grant.spend.map(ToString::to_string),
used: used_grants.contains(&(grant.name, grant.location)),
})
.collect();
let ungoverned_tools = ungoverned_tools_of(routes);
let unregistered_authorities = unregistered_authorities_of(routes, &actions);
Self {
schema_version: MANIFEST_SCHEMA_VERSION,
provenance: "provable".to_string(),
audit: AuditStatus {
sink_configured: audit_sink_configured,
},
actions: actions.into_values().collect(),
grants,
ungoverned_tools,
unregistered_authorities,
excluded: excluded_dimensions(),
}
}
#[must_use]
pub fn to_json(&self) -> String {
serde_json::to_string_pretty(self).unwrap_or_else(|_| "{}".to_string())
}
#[must_use]
pub fn to_dump_line(&self) -> String {
format!(
"{AGENT_AUTHORITY_MANIFEST_MARKER}{}",
serde_json::to_string(self).unwrap_or_else(|_| "{}".to_string())
)
}
#[must_use]
pub fn summary(&self) -> String {
let mut out = format!(
"{} agent-operable action{} under {} declared grant{}; {} MCP tool{} with no envelope.\n\
Agent audit sink: {}.",
self.actions.len(),
plural(self.actions.len()),
self.grants.len(),
plural(self.grants.len()),
self.ungoverned_tools.len(),
plural(self.ungoverned_tools.len()),
if self.audit.sink_configured {
"configured"
} else {
"NOT configured -- invocations are traced, not recorded"
},
);
for row in &self.actions {
write_action(&mut out, row);
}
if !self.ungoverned_tools.is_empty() {
let _ = write!(out, "\n\nMCP tools with no envelope (ungoverned):");
for tool in &self.ungoverned_tools {
let _ = write!(
out,
"\n {} {} ({}) [{}]{}",
tool.method,
tool.path,
tool.tool,
tool.exposed_by,
if tool.mutating {
" -- MUTATING"
} else {
" -- read-only"
}
);
}
}
if !self.unregistered_authorities.is_empty() {
let _ = write!(
out,
"\n\nRoutes naming an authority nothing registered (in no list above):"
);
for row in &self.unregistered_authorities {
let _ = write!(
out,
"\n {} {} ({}) -> {}::{}{}",
row.method,
row.path,
row.handler,
row.module_path,
row.action,
if row.mcp_tool { " [mcp tool]" } else { "" }
);
}
}
let _ = write!(out, "\n\nRecorded, not enforced by this slice:");
for entry in &self.excluded {
let _ = write!(
out,
"\n {} ({}): {}",
entry.dimension, entry.eventual_provenance, entry.runtime_caveat
);
}
out
}
#[must_use]
pub fn ungoverned_mutating_tools(&self) -> Vec<&UngovernedTool> {
self.ungoverned_tools
.iter()
.filter(|tool| tool.mutating)
.collect()
}
#[must_use]
pub fn non_reversible_actions(&self) -> Vec<&ActionRow> {
self.actions
.iter()
.filter(|row| {
row.exposure == MCP_TOOL_EXPOSURE
&& row.grant.reversibility != Reversibility::Reversible
})
.collect()
}
#[must_use]
pub fn unaudited_and_unrecoverable(&self) -> bool {
!self.audit.sink_configured
&& (!self.non_reversible_actions().is_empty()
|| !self.ungoverned_mutating_tools().is_empty())
}
}
fn write_action(out: &mut String, row: &ActionRow) {
let _ = write!(
out,
"\n {}::{} -> {} ({}, {}) {}",
row.module_path,
row.action,
row.grant.name,
row.grant.reversibility,
row.grant.tenant_scope,
match &row.route {
Some(route) if route.mcp_tool =>
format!("at {} {} [mcp tool]", route.method, route.path),
Some(route) => format!("at {} {}", route.method, route.path),
None => "-- not exposed by any route in this binary".to_string(),
}
);
if row.effects.is_empty() {
let _ = write!(out, "\n no proven effects");
}
for effect in &row.effects {
let _ = write!(
out,
"\n {} {} ({})",
effect.kind, effect.subject, effect.provenance
);
}
for site in &row.asserted_effect_free {
let _ = write!(
out,
"\n asserted effect-free at {}: {}",
site.location, site.reason
);
}
if !row.unused_grant_entries.is_empty() {
let _ = write!(
out,
"\n granted but unused: {}",
row.unused_grant_entries.join(", ")
);
}
}
fn ungoverned_tools_of(routes: &[RouteSummary]) -> Vec<UngovernedTool> {
let mut tools: Vec<UngovernedTool> = routes
.iter()
.filter(|route| route.mcp_tool && route.agent_authority.is_none())
.map(|route| UngovernedTool {
tool: route.operation_id.to_string(),
handler: route.handler.to_string(),
method: route.method.clone(),
path: route.path.clone(),
module_path: route.module_path.to_string(),
mutating: method_is_mutating(&route.method),
exposed_by: route.exposed_by.unwrap_or(McpExposedBy::Attribute),
})
.collect();
tools.sort_by(|a, b| (&a.path, &a.method, &a.tool).cmp(&(&b.path, &b.method, &b.tool)));
tools.dedup();
tools
}
fn unregistered_authorities_of(
routes: &[RouteSummary],
actions: &BTreeMap<(&str, &str), ActionRow>,
) -> Vec<UnregisteredAuthority> {
let mut rows: Vec<UnregisteredAuthority> = routes
.iter()
.filter_map(|route| {
let authority = route.agent_authority?;
if actions.contains_key(&(authority.module_path, authority.action)) {
return None;
}
Some(UnregisteredAuthority {
action: authority.action.to_string(),
module_path: authority.module_path.to_string(),
handler: route.handler.to_string(),
method: route.method.clone(),
path: route.path.clone(),
mcp_tool: route.mcp_tool,
})
})
.collect();
rows.sort_by(|a, b| {
(&a.module_path, &a.action, &a.path, &a.method).cmp(&(
&b.module_path,
&b.action,
&b.path,
&b.method,
))
});
rows.dedup();
rows
}
#[must_use]
pub fn excluded_dimensions() -> Vec<Excluded> {
vec![
Excluded {
dimension: "rate".to_string(),
eventual_provenance: "runtime-only".to_string(),
runtime_caveat: "declared, not enforced in this slice: the cap is checked for grammar \
at compile time and recorded here, but nothing meters calls at \
runtime. How often a running process is called is not a fact a \
compiler can ever settle."
.to_string(),
},
Excluded {
dimension: "spend".to_string(),
eventual_provenance: "runtime-only".to_string(),
runtime_caveat: "declared, not enforced in this slice: the cap is checked for grammar \
at compile time and recorded here, but nothing meters spend at \
runtime. What an outbound call costs is known only once it is made."
.to_string(),
},
Excluded {
dimension: "outbound".to_string(),
eventual_provenance: "provable".to_string(),
runtime_caveat: "literal URL prefixes are proven at compile time; a host resolved at \
runtime, a named-client `alias:` entry and any `#[agent_effect]` \
declaration are `declared` provenance, not proven."
.to_string(),
},
Excluded {
dimension: "jobs".to_string(),
eventual_provenance: "provable".to_string(),
runtime_caveat: "the enqueue is proven and the job name is checked against the grant; \
what the job itself then does is outside this envelope and carries \
its own."
.to_string(),
},
Excluded {
dimension: "cascading_deletes".to_string(),
eventual_provenance: "provable".to_string(),
runtime_caveat: "a `dependent(...)` cascade is not folded into the write set in this \
slice, so deleting a parent may write child models the grant does \
not list."
.to_string(),
},
Excluded {
dimension: "generated_repository_tools".to_string(),
eventual_provenance: "provable".to_string(),
runtime_caveat: "tools generated by `#[repository(api, mcp)]`, and read-only routes \
swept in by `expose_all_as_mcp`, have no annotation site in this \
slice. They are not gated -- they surface under `ungoverned_tools`, \
each row naming whether an attribute or the hatch exposed it."
.to_string(),
},
]
}
#[must_use]
pub fn build(routes: &[RouteSummary], audit_sink_configured: bool) -> AgentAuthorityManifest {
let authorities: Vec<&'static AgentAuthority> = inventory::iter::<AgentAuthorityDescriptor>
.into_iter()
.map(|descriptor| descriptor.0)
.collect();
let grants: Vec<&'static Grant> = inventory::iter::<GrantDescriptor>
.into_iter()
.map(|descriptor| descriptor.0)
.collect();
AgentAuthorityManifest::from_parts(&authorities, &grants, routes, audit_sink_configured)
}
#[must_use]
pub fn is_dump_mode() -> bool {
std::env::var(DUMP_ENV).as_deref() == Ok("1")
}
pub fn print_manifest_dump(manifest: &AgentAuthorityManifest) {
println!("{}", manifest.to_dump_line());
}
#[must_use]
pub fn parse_manifest_dump(stdout: &str) -> Option<AgentAuthorityManifest> {
stdout
.lines()
.filter_map(|line| line.split_once(AGENT_AUTHORITY_MANIFEST_MARKER))
.filter_map(|(_, json)| serde_json::from_str::<AgentAuthorityManifest>(json.trim()).ok())
.next_back()
}
#[must_use]
pub fn method_is_mutating(method: &str) -> bool {
!method.eq_ignore_ascii_case("GET") && !method.eq_ignore_ascii_case("HEAD")
}
pub const MCP_TOOL_EXPOSURE: &str = "mcp-tool";
const fn exposure_of(route: Option<&RouteSummary>) -> &'static str {
match route {
Some(route) if route.mcp_tool => MCP_TOOL_EXPOSURE,
Some(_) => "http-route",
None => "not-exposed",
}
}
fn row_provenance(authority: &AgentAuthority) -> &'static str {
let declared = !authority.asserted_effect_free.is_empty()
|| authority.asserted_effect_free_sites > 0
|| authority
.effects
.iter()
.any(|effect| effect.provenance == EffectProvenance::Declared);
if declared { "declared" } else { "provable" }
}
fn effect_row(effect: &Effect) -> EffectRow {
EffectRow {
kind: effect.kind,
subject: effect.subject.to_string(),
provenance: effect.provenance,
location: effect.location.to_string(),
}
}
fn asserted_effect_free_row(site: &AssertedEffectFree) -> AssertedEffectFreeRow {
AssertedEffectFreeRow {
location: site.location.to_string(),
reason: site.reason.to_string(),
}
}
fn to_owned(entries: &[&str]) -> Vec<String> {
entries.iter().map(ToString::to_string).collect()
}
const fn plural(n: usize) -> &'static str {
if n == 1 { "" } else { "s" }
}
fn unused_entries(grant: &Grant, effects: &[Effect]) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
let used = |kind: EffectKind, entry: &str| {
effects
.iter()
.any(|effect| effect.kind == kind && effect.subject == entry)
};
for entry in grant.writes {
if !used(EffectKind::Write, entry) && !used(EffectKind::UnboundedWrite, entry) {
out.push(format!("writes: {entry}"));
}
}
for entry in grant.unbounded_writes {
if !used(EffectKind::UnboundedWrite, entry) {
out.push(format!("unbounded_writes: {entry}"));
}
}
for entry in grant.outbound {
if !effects
.iter()
.any(|effect| effect.kind == EffectKind::Outbound && covers(entry, effect.subject))
{
out.push(format!("outbound: {entry}"));
}
}
for entry in grant.webhooks {
if !used(EffectKind::Webhook, entry) {
out.push(format!("webhooks: {entry}"));
}
}
for entry in grant.jobs {
if !used(EffectKind::Job, entry) {
out.push(format!("jobs: {entry}"));
}
}
out
}
fn covers(entry: &str, url: &str) -> bool {
url.strip_prefix(entry)
.is_some_and(|rest| rest.is_empty() || rest.starts_with(['/', '?', '#']))
}
#[cfg(test)]
mod tests {
use super::*;
static REFUND_GRANT: Grant = Grant {
name: "RefundDrafter",
writes: &["Refund", "Payment"],
unbounded_writes: &["StaleDraft"],
tenant_scope: TenantScope::Scoped,
outbound: &["https://api.stripe.com/v1/refunds", "alias:stripe"],
webhooks: &["refund.drafted"],
jobs: &["NotifyFinance", "audit_export"],
rate: Some("10/min"),
spend: Some("500.00 USD"),
reversibility: Reversibility::Compensable,
location: "billing/refunds.rs:10",
};
static UNUSED_GRANT: Grant = Grant {
name: "NobodyUsesThis",
writes: &["Ghost"],
unbounded_writes: &[],
tenant_scope: TenantScope::Scoped,
outbound: &[],
webhooks: &[],
jobs: &[],
rate: None,
spend: None,
reversibility: Reversibility::Reversible,
location: "billing/dead.rs:1",
};
static REFUND_EFFECTS: &[Effect] = &[
Effect {
kind: EffectKind::Outbound,
subject: "https://api.stripe.com/v1/refunds",
location: "billing/refunds.rs:42",
provenance: EffectProvenance::Syntactic,
},
Effect {
kind: EffectKind::Write,
subject: "Refund",
location: "billing/refunds.rs:30",
provenance: EffectProvenance::TypeResolved,
},
Effect {
kind: EffectKind::Write,
subject: "Refund",
location: "billing/refunds.rs:30",
provenance: EffectProvenance::TypeResolved,
},
];
static DECLARED_EFFECTS: &[Effect] = &[Effect {
kind: EffectKind::Write,
subject: "Refund",
location: "billing/sweep.rs:12",
provenance: EffectProvenance::Declared,
}];
static REVERSIBLE_GRANT: Grant = Grant {
name: "NoteEditor",
writes: &["Note"],
unbounded_writes: &[],
tenant_scope: TenantScope::Scoped,
outbound: &[],
webhooks: &[],
jobs: &[],
rate: None,
spend: None,
reversibility: Reversibility::Reversible,
location: "notes.rs:1",
};
static EDIT_NOTE: AgentAuthority = AgentAuthority {
action: "edit_note",
module_path: "notes",
location: "notes.rs:20",
grant: &REVERSIBLE_GRANT,
effects: &[],
asserted_effect_free_sites: 0,
asserted_effect_free: &[],
};
static DRAFT_REFUND: AgentAuthority = AgentAuthority {
action: "draft_refund",
module_path: "billing::refunds",
location: "billing/refunds.rs:28",
grant: &REFUND_GRANT,
effects: REFUND_EFFECTS,
asserted_effect_free_sites: 0,
asserted_effect_free: &[],
};
static SWEEP_DRAFTS: AgentAuthority = AgentAuthority {
action: "sweep_drafts",
module_path: "admin::sweep",
location: "admin/sweep.rs:9",
grant: &REFUND_GRANT,
effects: DECLARED_EFFECTS,
asserted_effect_free_sites: 0,
asserted_effect_free: &[],
};
fn route(
method: &str,
path: &str,
handler: &'static str,
module_path: &'static str,
mcp_tool: bool,
authority: Option<&'static AgentAuthority>,
) -> RouteSummary {
RouteSummary {
method: method.to_string(),
path: path.to_string(),
handler,
operation_id: handler,
module_path,
mcp_tool,
exposed_by: mcp_tool.then_some(McpExposedBy::Attribute),
agent_authority: authority,
}
}
fn governed_route() -> RouteSummary {
route(
"POST",
"/refunds",
"draft_refund",
"billing::refunds",
true,
Some(&DRAFT_REFUND),
)
}
fn find<'a>(manifest: &'a AgentAuthorityManifest, action: &str) -> &'a ActionRow {
manifest
.actions
.iter()
.find(|row| row.action == action)
.unwrap_or_else(|| panic!("no `{action}` row in {:?}", manifest.actions))
}
#[test]
fn the_document_carries_its_schema_version_and_provenance() {
let manifest =
AgentAuthorityManifest::from_parts(&[&DRAFT_REFUND], &[&REFUND_GRANT], &[], true);
assert_eq!(manifest.schema_version, MANIFEST_SCHEMA_VERSION);
assert_eq!(manifest.provenance, "provable");
let json = manifest.to_json();
assert!(json.contains("\"schema_version\": 1"), "{json}");
assert!(json.contains("draft_refund"), "{json}");
}
#[test]
fn the_audit_sink_status_is_recorded_in_the_document() {
let with_sink = AgentAuthorityManifest::from_parts(&[], &[], &[], true);
assert!(with_sink.audit.sink_configured);
let without = AgentAuthorityManifest::from_parts(&[], &[], &[], false);
assert!(!without.audit.sink_configured);
assert!(without.to_json().contains("sink_configured"));
}
#[test]
fn the_dump_line_round_trips_through_the_marker() {
let manifest = AgentAuthorityManifest::from_parts(
&[&DRAFT_REFUND],
&[&REFUND_GRANT],
&[governed_route()],
true,
);
let stdout = format!("booting\n{}\ndone\n", manifest.to_dump_line());
assert!(
stdout.contains(AGENT_AUTHORITY_MANIFEST_MARKER),
"the dump line must carry the marker: {stdout}"
);
let parsed = parse_manifest_dump(&stdout).expect("manifest parses");
assert_eq!(parsed, manifest);
}
#[test]
fn the_last_marker_line_wins() {
let first = AgentAuthorityManifest::from_parts(&[], &[], &[], false);
let last = AgentAuthorityManifest::from_parts(
&[&DRAFT_REFUND],
&[&REFUND_GRANT],
&[governed_route()],
true,
);
let stdout = format!(
"booting\n{}\nmore output\n{}\ndone\n",
first.to_dump_line(),
last.to_dump_line()
);
assert_eq!(parse_manifest_dump(&stdout).expect("parses"), last);
}
#[test]
fn a_marker_in_the_middle_of_a_line_still_parses() {
let manifest = AgentAuthorityManifest::from_parts(&[], &[], &[], true);
let stdout = format!(
"2026-09-02T00:00:00Z INFO app: {}\n",
manifest.to_dump_line()
);
assert_eq!(parse_manifest_dump(&stdout).expect("parses"), manifest);
}
#[test]
fn a_trailing_carriage_return_does_not_break_the_parse() {
let manifest = AgentAuthorityManifest::from_parts(&[], &[], &[], true);
let stdout = format!("{}\r\n", manifest.to_dump_line());
assert_eq!(parse_manifest_dump(&stdout).expect("parses"), manifest);
}
#[test]
fn two_routes_reaching_one_action_keep_the_first() {
let manifest = AgentAuthorityManifest::from_parts(
&[&DRAFT_REFUND],
&[&REFUND_GRANT],
&[
governed_route(),
route(
"POST",
"/v2/refunds",
"draft_refund",
"billing::refunds",
true,
Some(&DRAFT_REFUND),
),
],
true,
);
assert_eq!(manifest.actions.len(), 1, "one handler is one action");
let row = find(&manifest, "draft_refund");
assert_eq!(row.route.as_ref().expect("a route").path, "/refunds");
assert!(manifest.unregistered_authorities.is_empty());
}
#[test]
fn an_mcp_route_wins_the_join_over_a_plain_http_one_registered_first() {
let http_first = AgentAuthorityManifest::from_parts(
&[&DRAFT_REFUND],
&[&REFUND_GRANT],
&[
route(
"POST",
"/internal/refunds",
"draft_refund",
"billing::refunds",
false,
Some(&DRAFT_REFUND),
),
governed_route(),
],
false,
);
let row = find(&http_first, "draft_refund");
assert_eq!(row.exposure, MCP_TOOL_EXPOSURE);
assert_eq!(
row.route.as_ref().expect("a route").path,
"/refunds",
"the row must name the route an agent actually calls"
);
assert_eq!(http_first.non_reversible_actions().len(), 1);
assert!(
http_first.unaudited_and_unrecoverable(),
"an agent can reach this and nothing records it"
);
let mcp_first = AgentAuthorityManifest::from_parts(
&[&DRAFT_REFUND],
&[&REFUND_GRANT],
&[
governed_route(),
route(
"POST",
"/internal/refunds",
"draft_refund",
"billing::refunds",
false,
Some(&DRAFT_REFUND),
),
],
false,
);
assert_eq!(find(&mcp_first, "draft_refund").exposure, MCP_TOOL_EXPOSURE);
assert_eq!(
find(&mcp_first, "draft_refund").route,
find(&http_first, "draft_refund").route,
"registration order must not change the document"
);
}
#[test]
fn stdout_without_a_marker_parses_to_nothing() {
assert!(parse_manifest_dump("booting\ndone\n").is_none());
assert!(
parse_manifest_dump(&format!("{AGENT_AUTHORITY_MANIFEST_MARKER}not json")).is_none()
);
}
#[test]
fn rows_are_sorted_by_module_path_then_action() {
let manifest = AgentAuthorityManifest::from_parts(
&[&DRAFT_REFUND, &SWEEP_DRAFTS],
&[&REFUND_GRANT],
&[governed_route()],
true,
);
let keys: Vec<(String, String)> = manifest
.actions
.iter()
.map(|row| (row.module_path.clone(), row.action.clone()))
.collect();
let mut sorted = keys.clone();
sorted.sort();
assert_eq!(keys, sorted, "rows must be sorted by (module_path, action)");
assert_eq!(manifest.actions[0].module_path, "admin::sweep");
}
#[test]
fn effects_are_sorted_and_deduplicated() {
let manifest = AgentAuthorityManifest::from_parts(
&[&DRAFT_REFUND],
&[&REFUND_GRANT],
&[governed_route()],
true,
);
let row = find(&manifest, "draft_refund");
assert_eq!(row.effects.len(), 2, "{:?}", row.effects);
let mut sorted = row.effects.clone();
sorted.sort();
assert_eq!(row.effects, sorted, "effects must be sorted");
assert_eq!(row.effects[0].kind, EffectKind::Write);
assert_eq!(row.effects[0].subject, "Refund");
assert_eq!(row.effects[1].kind, EffectKind::Outbound);
}
#[test]
fn unused_grant_entries_name_the_authority_nothing_exercises() {
let manifest = AgentAuthorityManifest::from_parts(
&[&DRAFT_REFUND],
&[&REFUND_GRANT],
&[governed_route()],
true,
);
let row = find(&manifest, "draft_refund");
let unused = &row.unused_grant_entries;
assert!(
unused.contains(&"writes: Payment".to_string()),
"{unused:?}"
);
assert!(
unused.contains(&"unbounded_writes: StaleDraft".to_string()),
"{unused:?}"
);
assert!(
unused.contains(&"outbound: alias:stripe".to_string()),
"{unused:?}"
);
assert!(
unused.contains(&"webhooks: refund.drafted".to_string()),
"{unused:?}"
);
assert!(
unused.contains(&"jobs: NotifyFinance".to_string()),
"{unused:?}"
);
assert!(
!unused.contains(&"writes: Refund".to_string()),
"{unused:?}"
);
assert!(
!unused.contains(&"outbound: https://api.stripe.com/v1/refunds".to_string()),
"{unused:?}"
);
}
#[test]
fn an_action_no_route_reaches_is_marked_not_exposed() {
let manifest = AgentAuthorityManifest::from_parts(
&[&DRAFT_REFUND, &SWEEP_DRAFTS],
&[&REFUND_GRANT],
&[governed_route()],
true,
);
let exposed = find(&manifest, "draft_refund");
assert_eq!(exposed.exposure, "mcp-tool");
assert_eq!(
exposed.route.as_ref().map(|r| r.path.as_str()),
Some("/refunds")
);
assert!(exposed.route.as_ref().expect("route").mcp_tool);
let orphan = find(&manifest, "sweep_drafts");
assert_eq!(orphan.exposure, "not-exposed");
assert!(orphan.route.is_none());
}
#[test]
fn a_governed_route_that_is_not_an_mcp_tool_is_still_an_http_route() {
let manifest = AgentAuthorityManifest::from_parts(
&[&DRAFT_REFUND],
&[&REFUND_GRANT],
&[route(
"POST",
"/refunds",
"draft_refund",
"billing::refunds",
false,
Some(&DRAFT_REFUND),
)],
true,
);
assert_eq!(find(&manifest, "draft_refund").exposure, "http-route");
}
#[test]
fn a_hand_declared_effect_downgrades_the_row_provenance() {
let manifest = AgentAuthorityManifest::from_parts(
&[&DRAFT_REFUND, &SWEEP_DRAFTS],
&[&REFUND_GRANT],
&[governed_route()],
true,
);
assert_eq!(find(&manifest, "draft_refund").provenance, "provable");
assert_eq!(find(&manifest, "sweep_drafts").provenance, "declared");
}
#[test]
fn an_asserted_effect_free_site_downgrades_the_row_provenance() {
static DISCHARGED: AgentAuthority = AgentAuthority {
action: "discharged",
module_path: "billing::discharged",
location: "billing/discharged.rs:3",
grant: &REFUND_GRANT,
effects: &[],
asserted_effect_free_sites: 1,
asserted_effect_free: &[AssertedEffectFree {
location: "billing/discharged.rs:9",
reason: "the helper only formats a string",
}],
};
let manifest =
AgentAuthorityManifest::from_parts(&[&DISCHARGED], &[&REFUND_GRANT], &[], true);
assert_eq!(find(&manifest, "discharged").provenance, "declared");
}
#[test]
fn each_asserted_effect_free_site_reaches_the_row_with_its_reason() {
static HATCHED: AgentAuthority = AgentAuthority {
action: "hatched",
module_path: "billing::hatched",
location: "billing/hatched.rs:3",
grant: &REFUND_GRANT,
effects: &[],
asserted_effect_free_sites: 2,
asserted_effect_free: &[
AssertedEffectFree {
location: "billing/hatched.rs:11",
reason: "pure formatting helper",
},
AssertedEffectFree {
location: "billing/hatched.rs:19",
reason: "the metrics counter takes no lock",
},
],
};
let manifest = AgentAuthorityManifest::from_parts(&[&HATCHED], &[&REFUND_GRANT], &[], true);
let row = find(&manifest, "hatched");
assert_eq!(
row.asserted_effect_free,
vec![
AssertedEffectFreeRow {
location: "billing/hatched.rs:11".to_string(),
reason: "pure formatting helper".to_string(),
},
AssertedEffectFreeRow {
location: "billing/hatched.rs:19".to_string(),
reason: "the metrics counter takes no lock".to_string(),
},
]
);
let json = manifest.to_json();
assert!(json.contains("pure formatting helper"), "{json}");
let parsed: AgentAuthorityManifest = serde_json::from_str(&json).expect("round trip");
assert_eq!(parsed, manifest);
let summary = manifest.summary();
assert!(summary.contains("asserted effect-free"), "{summary}");
assert!(summary.contains("billing/hatched.rs:19"), "{summary}");
assert!(
summary.contains("the metrics counter takes no lock"),
"{summary}"
);
}
#[test]
fn a_manifest_written_before_the_hatch_list_existed_still_parses() {
let json = r#"{
"schema_version": 1,
"provenance": "provable",
"audit": { "sink_configured": true },
"actions": [{
"action": "draft_refund",
"module_path": "billing::refunds",
"location": "billing/refunds.rs:20",
"exposure": "not-exposed",
"provenance": "provable",
"grant": {
"name": "RefundDrafter",
"reversibility": "compensable",
"tenant_scope": "scoped"
},
"effects": [],
"unused_grant_entries": []
}],
"grants": [],
"ungoverned_tools": [],
"excluded": []
}"#;
let parsed: AgentAuthorityManifest = serde_json::from_str(json).expect("parses");
assert!(parsed.actions[0].asserted_effect_free.is_empty());
assert!(parsed.unregistered_authorities.is_empty());
assert!(parsed.audit.sink_configured);
}
#[test]
fn a_manifest_missing_its_audit_posture_is_rejected_not_defaulted() {
let json = r#"{
"schema_version": 1,
"provenance": "provable",
"actions": [],
"grants": [],
"ungoverned_tools": [],
"excluded": []
}"#;
let error = serde_json::from_str::<AgentAuthorityManifest>(json)
.expect_err("a manifest with no audit posture must not parse");
assert!(error.to_string().contains("audit"), "{error}");
}
#[test]
fn an_ungoverned_tool_is_named_by_its_operation_id_not_its_handler() {
let mut renamed = route(
"POST",
"/widgets",
"create_widget_handler",
"shop::widgets",
true,
None,
);
renamed.operation_id = "createWidget";
let manifest = AgentAuthorityManifest::from_parts(&[], &[], &[renamed], true);
let tool = &manifest.ungoverned_tools[0];
assert_eq!(tool.tool, "createWidget");
assert_eq!(tool.handler, "create_widget_handler");
assert!(tool.mutating);
}
#[test]
fn the_row_carries_the_declared_only_caps_verbatim() {
let manifest =
AgentAuthorityManifest::from_parts(&[&DRAFT_REFUND], &[&REFUND_GRANT], &[], true);
let grant = &find(&manifest, "draft_refund").grant;
assert_eq!(grant.name, "RefundDrafter");
assert_eq!(grant.reversibility, Reversibility::Compensable);
assert_eq!(grant.tenant_scope, TenantScope::Scoped);
assert_eq!(grant.rate.as_deref(), Some("10/min"));
assert_eq!(grant.spend.as_deref(), Some("500.00 USD"));
}
#[test]
fn every_declared_grant_appears_even_when_nothing_uses_it() {
let manifest = AgentAuthorityManifest::from_parts(
&[&DRAFT_REFUND],
&[&REFUND_GRANT, &UNUSED_GRANT],
&[governed_route()],
true,
);
let names: Vec<&str> = manifest.grants.iter().map(|g| g.name.as_str()).collect();
assert!(names.contains(&"RefundDrafter"), "{names:?}");
assert!(names.contains(&"NobodyUsesThis"), "{names:?}");
let dead = manifest
.grants
.iter()
.find(|g| g.name == "NobodyUsesThis")
.expect("row");
assert!(!dead.used, "an envelope nothing uses must say so");
assert_eq!(dead.writes, vec!["Ghost".to_string()]);
let live = manifest
.grants
.iter()
.find(|g| g.name == "RefundDrafter")
.expect("row");
assert!(live.used);
}
#[test]
fn an_mcp_exposed_route_with_no_envelope_is_reported_as_ungoverned() {
let manifest = AgentAuthorityManifest::from_parts(
&[&DRAFT_REFUND],
&[&REFUND_GRANT],
&[
governed_route(),
route(
"DELETE",
"/widgets/{id}",
"destroy_widget",
"shop::widgets",
true,
None,
),
route(
"GET",
"/widgets",
"list_widgets",
"shop::widgets",
true,
None,
),
route(
"POST",
"/internal/sync",
"sync",
"shop::internal",
false,
None,
),
],
true,
);
let names: Vec<&str> = manifest
.ungoverned_tools
.iter()
.map(|t| t.tool.as_str())
.collect();
assert!(names.contains(&"destroy_widget"), "{names:?}");
assert!(names.contains(&"list_widgets"), "{names:?}");
assert!(!names.contains(&"draft_refund"), "{names:?}");
assert!(!names.contains(&"sync"), "{names:?}");
let destroy = manifest
.ungoverned_tools
.iter()
.find(|t| t.tool == "destroy_widget")
.expect("row");
assert!(destroy.mutating, "DELETE changes state");
assert_eq!(destroy.method, "DELETE");
assert_eq!(destroy.path, "/widgets/{id}");
assert_eq!(destroy.module_path, "shop::widgets");
let list = manifest
.ungoverned_tools
.iter()
.find(|t| t.tool == "list_widgets")
.expect("row");
assert!(!list.mutating, "GET is read-only: warn, never fail");
let mutating: Vec<&str> = manifest
.ungoverned_mutating_tools()
.iter()
.map(|t| t.tool.as_str())
.collect();
assert_eq!(mutating, ["destroy_widget"]);
}
#[test]
fn only_get_and_head_are_read_only() {
assert!(!method_is_mutating("GET"));
assert!(!method_is_mutating("HEAD"));
for method in ["POST", "PUT", "PATCH", "DELETE"] {
assert!(
method_is_mutating(method),
"{method} must count as mutating"
);
}
}
#[test]
fn the_document_names_what_it_does_not_enforce() {
let manifest = AgentAuthorityManifest::from_parts(&[], &[], &[], true);
let dimensions: Vec<&str> = manifest
.excluded
.iter()
.map(|e| e.dimension.as_str())
.collect();
for expected in [
"rate",
"spend",
"outbound",
"jobs",
"cascading_deletes",
"generated_repository_tools",
] {
assert!(
dimensions.contains(&expected),
"`{expected}` must be named in `excluded`: {dimensions:?}"
);
}
for entry in &manifest.excluded {
assert!(
!entry.runtime_caveat.trim().is_empty(),
"`{}` needs a caveat a reader can act on",
entry.dimension
);
}
assert_eq!(manifest.excluded, excluded_dimensions());
}
static INVENTORY_GRANT: Grant = Grant {
name: "__AgentAuthorityManifestTestGrant",
writes: &["__ManifestTestWidget", "__ManifestTestUnused"],
unbounded_writes: &[],
tenant_scope: TenantScope::Scoped,
outbound: &[],
webhooks: &[],
jobs: &[],
rate: None,
spend: None,
reversibility: Reversibility::Reversible,
location: "manifest.rs:inventory",
};
static INVENTORY_ACTION: AgentAuthority = AgentAuthority {
action: "__manifest_test_action",
module_path: "__manifest_test",
location: "manifest.rs:inventory",
grant: &INVENTORY_GRANT,
effects: &[Effect {
kind: EffectKind::Write,
subject: "__ManifestTestWidget",
location: "manifest.rs:inventory",
provenance: EffectProvenance::TypeResolved,
}],
asserted_effect_free_sites: 0,
asserted_effect_free: &[],
};
inventory::submit! { GrantDescriptor(&INVENTORY_GRANT) }
inventory::submit! { AgentAuthorityDescriptor(&INVENTORY_ACTION) }
#[test]
fn the_collected_manifest_is_byte_identical_across_builds() {
let routes = vec![route(
"POST",
"/__manifest-test",
"__manifest_test_action",
"__manifest_test",
true,
Some(&INVENTORY_ACTION),
)];
let first = build(&routes, true);
let second = build(&routes, true);
assert_eq!(first, second);
assert_eq!(first.to_json(), second.to_json());
}
#[test]
fn the_inventory_registered_action_reaches_the_document() {
let routes = vec![route(
"POST",
"/__manifest-test",
"__manifest_test_action",
"__manifest_test",
true,
Some(&INVENTORY_ACTION),
)];
let manifest = build(&routes, false);
let row = find(&manifest, "__manifest_test_action");
assert_eq!(row.module_path, "__manifest_test");
assert_eq!(row.grant.name, "__AgentAuthorityManifestTestGrant");
assert_eq!(row.effects.len(), 1);
assert!(
row.unused_grant_entries
.contains(&"writes: __ManifestTestUnused".to_string()),
"{:?}",
row.unused_grant_entries
);
assert!(
manifest
.grants
.iter()
.any(|g| g.name == "__AgentAuthorityManifestTestGrant"),
"the collected grant must appear"
);
assert!(!manifest.audit.sink_configured);
}
#[test]
fn the_summary_names_each_action_its_grant_and_what_is_ungoverned() {
let manifest = AgentAuthorityManifest::from_parts(
&[&DRAFT_REFUND],
&[&REFUND_GRANT],
&[
governed_route(),
route(
"DELETE",
"/widgets/{id}",
"destroy_widget",
"shop::widgets",
true,
None,
),
],
true,
);
let summary = manifest.summary();
assert!(summary.contains("draft_refund"), "{summary}");
assert!(summary.contains("RefundDrafter"), "{summary}");
assert!(summary.contains("compensable"), "{summary}");
assert!(summary.contains("destroy_widget"), "{summary}");
assert!(summary.contains("ungoverned"), "{summary}");
}
#[test]
fn a_used_webhook_topic_is_not_reported_unused() {
static FIRES_WEBHOOK: AgentAuthority = AgentAuthority {
action: "fire_webhook",
module_path: "billing::hooks",
location: "billing/hooks.rs:3",
grant: &REFUND_GRANT,
effects: &[Effect {
kind: EffectKind::Webhook,
subject: "refund.drafted",
location: "billing/hooks.rs:9",
provenance: EffectProvenance::Syntactic,
}],
asserted_effect_free_sites: 0,
asserted_effect_free: &[],
};
let manifest =
AgentAuthorityManifest::from_parts(&[&FIRES_WEBHOOK], &[&REFUND_GRANT], &[], true);
let unused = &find(&manifest, "fire_webhook").unused_grant_entries;
assert!(
!unused.contains(&"webhooks: refund.drafted".to_string()),
"a fired topic is not unused: {unused:?}"
);
}
fn doc(method: &str) -> McpExposureInput<'_> {
McpExposureInput {
method,
hidden: false,
mcp_tool: false,
mcp_exclude: false,
mcp_stream: false,
has_response_schema: true,
success_status: 200,
expose_all: false,
}
}
#[test]
fn the_whole_api_hatch_exposes_read_only_verbs_and_nothing_else() {
let hatch = |method| {
mcp_exposure(&McpExposureInput {
expose_all: true,
..doc(method)
})
};
assert_eq!(hatch("GET"), Some(McpExposedBy::Hatch));
assert_eq!(hatch("HEAD"), Some(McpExposedBy::Hatch));
assert_eq!(hatch("POST"), None);
assert_eq!(hatch("DELETE"), None);
assert_eq!(hatch("FROBNICATE"), None);
}
#[test]
fn an_explicit_opt_in_beats_the_verb_and_an_opt_out_beats_everything() {
assert_eq!(
mcp_exposure(&McpExposureInput {
mcp_tool: true,
..doc("POST")
}),
Some(McpExposedBy::Attribute)
);
for input in [
McpExposureInput {
mcp_tool: true,
mcp_exclude: true,
..doc("POST")
},
McpExposureInput {
mcp_tool: true,
hidden: true,
..doc("POST")
},
McpExposureInput {
expose_all: true,
mcp_exclude: true,
..doc("GET")
},
] {
assert_eq!(mcp_exposure(&input), None, "{input:?}");
}
}
#[test]
fn an_html_route_tagged_mcp_is_not_reported_as_a_tool() {
assert_eq!(
mcp_exposure(&McpExposureInput {
mcp_tool: true,
has_response_schema: false,
..doc("GET")
}),
None
);
assert_eq!(
mcp_exposure(&McpExposureInput {
mcp_tool: true,
has_response_schema: false,
success_status: 204,
..doc("DELETE")
}),
Some(McpExposedBy::Attribute)
);
assert_eq!(
mcp_exposure(&McpExposureInput {
mcp_tool: true,
mcp_stream: true,
has_response_schema: false,
..doc("GET")
}),
Some(McpExposedBy::Attribute)
);
}
#[test]
fn a_hatch_exposed_tool_reaches_ungoverned_tools_and_says_so() {
let mut hatched = route(
"GET",
"/reports",
"list_reports",
"shop::reports",
true,
None,
);
hatched.exposed_by = Some(McpExposedBy::Hatch);
let manifest = AgentAuthorityManifest::from_parts(&[], &[], &[hatched], true);
let tool = &manifest.ungoverned_tools[0];
assert_eq!(tool.tool, "list_reports");
assert_eq!(tool.exposed_by, McpExposedBy::Hatch);
assert!(!tool.mutating);
let summary = manifest.summary();
assert!(summary.contains("hatch"), "{summary}");
}
#[cfg(feature = "mcp")]
#[test]
fn replicated_predicate_matches_the_mcp_projector() {
use crate::openapi::{ApiDoc, SchemaEntry, SchemaKind};
const THING: SchemaEntry = SchemaEntry {
name: "Thing",
kind: SchemaKind::Ref,
identity: None,
};
for method in ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"] {
for hidden in [false, true] {
for mcp_tool in [false, true] {
for mcp_exclude in [false, true] {
for mcp_stream in [false, true] {
for has_response in [false, true] {
for status in [200u16, 204, 205] {
for expose_all in [false, true] {
let doc = ApiDoc {
method,
hidden,
mcp_tool,
mcp_exclude,
mcp_stream,
response: has_response.then_some(THING),
success_status: status,
..ApiDoc::default()
};
let input = McpExposureInput {
method,
hidden,
mcp_tool,
mcp_exclude,
mcp_stream,
has_response_schema: has_response,
success_status: status,
expose_all,
};
assert_eq!(
mcp_exposure(&input).is_some(),
crate::mcp::should_expose(&doc, expose_all),
"{input:?}"
);
}
}
}
}
}
}
}
}
}
#[test]
fn a_route_whose_authority_never_registered_is_surfaced_not_lost() {
let manifest =
AgentAuthorityManifest::from_parts(&[], &[&REFUND_GRANT], &[governed_route()], true);
assert!(manifest.actions.is_empty());
assert!(manifest.ungoverned_tools.is_empty());
assert_eq!(
manifest.unregistered_authorities,
vec![UnregisteredAuthority {
action: "draft_refund".to_string(),
module_path: "billing::refunds".to_string(),
handler: "draft_refund".to_string(),
method: "POST".to_string(),
path: "/refunds".to_string(),
mcp_tool: true,
}]
);
let summary = manifest.summary();
assert!(summary.contains("nothing registered"), "{summary}");
}
#[test]
fn a_registered_authority_is_not_reported_unregistered() {
let manifest = AgentAuthorityManifest::from_parts(
&[&DRAFT_REFUND],
&[&REFUND_GRANT],
&[governed_route()],
true,
);
assert!(manifest.unregistered_authorities.is_empty());
assert_eq!(manifest.actions.len(), 1);
}
#[test]
fn unaudited_and_unrecoverable_needs_both_halves() {
let non_reversible = |sink| {
AgentAuthorityManifest::from_parts(
&[&DRAFT_REFUND],
&[&REFUND_GRANT],
&[governed_route()],
sink,
)
.unaudited_and_unrecoverable()
};
assert!(non_reversible(false), "compensable tool, no sink");
assert!(!non_reversible(true), "compensable tool, sink present");
let reversible =
AgentAuthorityManifest::from_parts(&[&EDIT_NOTE], &[&REVERSIBLE_GRANT], &[], false);
assert!(!reversible.unaudited_and_unrecoverable());
assert!(reversible.non_reversible_actions().is_empty());
let with_tool = AgentAuthorityManifest::from_parts(
&[&EDIT_NOTE],
&[&REVERSIBLE_GRANT],
&[route(
"DELETE",
"/widgets/{id}",
"destroy_widget",
"shop::widgets",
true,
None,
)],
false,
);
assert!(with_tool.unaudited_and_unrecoverable());
}
#[test]
fn only_an_agent_reachable_action_trips_the_audit_gate() {
let gate = |routes: &[RouteSummary]| {
AgentAuthorityManifest::from_parts(&[&DRAFT_REFUND], &[&REFUND_GRANT], routes, false)
};
let unmounted = gate(&[]);
assert_eq!(find(&unmounted, "draft_refund").exposure, "not-exposed");
assert!(unmounted.non_reversible_actions().is_empty());
assert!(!unmounted.unaudited_and_unrecoverable());
let http_only = gate(&[route(
"POST",
"/refunds",
"draft_refund",
"billing::refunds",
false,
Some(&DRAFT_REFUND),
)]);
assert_eq!(find(&http_only, "draft_refund").exposure, "http-route");
assert!(http_only.non_reversible_actions().is_empty());
assert!(!http_only.unaudited_and_unrecoverable());
let as_tool = gate(&[governed_route()]);
assert_eq!(
find(&as_tool, "draft_refund").exposure,
MCP_TOOL_EXPOSURE,
"the fixture must actually be a tool, or this test proves nothing"
);
assert_eq!(as_tool.non_reversible_actions().len(), 1);
assert!(as_tool.unaudited_and_unrecoverable());
}
#[test]
fn every_excluded_dimension_says_whether_it_could_ever_be_proved() {
let excluded = excluded_dimensions();
for entry in &excluded {
assert!(
["provable", "declared", "runtime-only"]
.contains(&entry.eventual_provenance.as_str()),
"{entry:?}"
);
}
let of = |name: &str| {
excluded
.iter()
.find(|e| e.dimension == name)
.unwrap_or_else(|| panic!("no `{name}` dimension"))
.eventual_provenance
.clone()
};
assert_eq!(of("rate"), "runtime-only");
assert_eq!(of("spend"), "runtime-only");
assert_eq!(of("outbound"), "provable");
assert_eq!(of("jobs"), "provable");
assert_eq!(of("cascading_deletes"), "provable");
assert_eq!(of("generated_repository_tools"), "provable");
}
#[test]
fn the_hatch_caveat_describes_what_the_document_actually_does() {
let caveat = excluded_dimensions()
.into_iter()
.find(|e| e.dimension == "generated_repository_tools")
.expect("dimension")
.runtime_caveat;
assert!(caveat.contains("ungoverned_tools"), "{caveat}");
assert!(caveat.contains("expose_all_as_mcp"), "{caveat}");
let mut hatched = route(
"GET",
"/reports",
"list_reports",
"shop::reports",
true,
None,
);
hatched.exposed_by = Some(McpExposedBy::Hatch);
let manifest = AgentAuthorityManifest::from_parts(&[], &[], &[hatched], true);
assert_eq!(
manifest.ungoverned_tools.len(),
1,
"the caveat must be true"
);
}
}