use stellar_agent_toolsets::Capability;
pub const READ_BALANCE_GRANTS: &[&str] = &["stellar_balances"];
pub const PROPOSE_TRANSACTION_GRANTS: &[&str] =
&["stellar_pay", "stellar_claim", "stellar_rule_create"];
pub const SUGGEST_DESTINATION_GRANTS: &[&str] = &[
"stellar_sep47_discover",
"stellar_sep48_preview_invocation",
"stellar_sep7_parse_uri",
];
pub const OBSERVE_EVENT_GRANTS: &[&str] = &[];
pub const READ_RULES_GRANTS: &[&str] = &["stellar_rules_list", "stellar_rules_get"];
pub const ALL_MATRIX_ENTRIES: &[(&str, Capability)] = &[
("stellar_balances", Capability::ReadBalance),
("stellar_pay", Capability::ProposeTransaction),
("stellar_claim", Capability::ProposeTransaction),
("stellar_rule_create", Capability::ProposeTransaction),
("stellar_sep47_discover", Capability::SuggestDestination),
(
"stellar_sep48_preview_invocation",
Capability::SuggestDestination,
),
("stellar_sep7_parse_uri", Capability::SuggestDestination),
("stellar_rules_list", Capability::ReadRules),
("stellar_rules_get", Capability::ReadRules),
];
pub const ALL_MATRIX_TOOL_NAMES: &[&str] = &[
"stellar_balances",
"stellar_pay",
"stellar_claim",
"stellar_rule_create",
"stellar_sep47_discover",
"stellar_sep48_preview_invocation",
"stellar_sep7_parse_uri",
"stellar_rules_list",
"stellar_rules_get",
];
pub const SIGN_PAYMENT_GATED_TOOLS: &[&str] = &["stellar_pay_commit"];
pub const SIGN_RULE_CREATE_ASSET_SENTINEL: &str =
"RULECREATE:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";
pub const SIGN_RULE_CREATE_AMOUNT_SENTINEL: i64 = 1;
pub const SIGN_RULE_CREATE_GATED_TOOLS: &[&str] = &["stellar_rule_create_commit"];
pub const GATED_MATRIX_ENTRIES: &[(Capability, &[&str])] = &[
(Capability::SignPayment, SIGN_PAYMENT_GATED_TOOLS),
(Capability::SignRuleCreate, SIGN_RULE_CREATE_GATED_TOOLS),
];
#[must_use]
pub fn gated_grants_for_capability(cap: Capability) -> Option<&'static [&'static str]> {
match cap {
Capability::SignPayment => Some(SIGN_PAYMENT_GATED_TOOLS),
Capability::SignRuleCreate => Some(SIGN_RULE_CREATE_GATED_TOOLS),
_ => None,
}
}
pub const SIGNING_DENYLIST: &[&str] = &[
"stellar_sep43_sign_transaction",
"stellar_sep43_sign_and_submit_transaction",
"stellar_sep43_sign_auth_entry",
"stellar_sep43_sign_message",
"stellar_sep53_sign_message",
"stellar_pay_commit",
"stellar_create_account_commit",
"stellar_claim_commit",
"stellar_rule_create_commit",
"stellar_x402_create_payment",
"stellar_x402_parse_receipt",
"stellar_x402_authenticated_payment",
"stellar_toolset_list",
"stellar_toolset_invoke",
];
#[must_use]
pub fn grants_for_capability(cap: Capability) -> &'static [&'static str] {
match cap {
Capability::ReadBalance => READ_BALANCE_GRANTS,
Capability::ProposeTransaction => PROPOSE_TRANSACTION_GRANTS,
Capability::SuggestDestination => SUGGEST_DESTINATION_GRANTS,
Capability::ObserveEvent => OBSERVE_EVENT_GRANTS,
Capability::SignPayment => &[],
Capability::ReadRules => READ_RULES_GRANTS,
Capability::SignRuleCreate => &[],
_ => &[],
}
}
pub fn resolve_action(
action: &str,
) -> Result<(&'static str, Capability), crate::ToolsetRuntimeError> {
for (tool, cap) in ALL_MATRIX_ENTRIES {
if *tool == action {
return Ok((tool, *cap));
}
}
Err(crate::ToolsetRuntimeError::UnknownToolsetAction {
action: stellar_agent_toolsets::sanitise_display(action, 128),
})
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
reason = "test-only; panics acceptable in unit tests"
)]
mod tests {
use super::*;
#[test]
fn matrix_and_denylist_are_disjoint() {
use std::collections::HashSet;
let denylist: HashSet<&str> = SIGNING_DENYLIST.iter().copied().collect();
for (tool, cap) in ALL_MATRIX_ENTRIES {
assert!(
!denylist.contains(tool),
"matrix tool '{tool}' (granting {cap:?}) appears in SIGNING_DENYLIST — \
this violates the disjoint invariant"
);
}
}
#[test]
fn all_matrix_entries_covers_every_per_capability_grant() {
use std::collections::HashSet;
let mut from_grants: HashSet<&str> = HashSet::new();
for t in READ_BALANCE_GRANTS {
from_grants.insert(t);
}
for t in PROPOSE_TRANSACTION_GRANTS {
from_grants.insert(t);
}
for t in SUGGEST_DESTINATION_GRANTS {
from_grants.insert(t);
}
for t in OBSERVE_EVENT_GRANTS {
from_grants.insert(t);
}
for t in READ_RULES_GRANTS {
from_grants.insert(t);
}
let from_entries: HashSet<&str> = ALL_MATRIX_ENTRIES.iter().map(|(t, _)| *t).collect();
for t in &from_grants {
assert!(
from_entries.contains(t),
"tool '{t}' is in a per-capability grant array but NOT in \
ALL_MATRIX_ENTRIES — add it to keep single source of truth"
);
}
for t in &from_entries {
assert!(
from_grants.contains(t),
"tool '{t}' is in ALL_MATRIX_ENTRIES but NOT in any per-capability \
grant array — ALL_MATRIX_ENTRIES must be derived from the grant arrays"
);
}
}
#[test]
fn all_matrix_tool_names_covers_every_matrix_entry() {
use std::collections::HashSet;
let from_entries: HashSet<&str> = ALL_MATRIX_ENTRIES.iter().map(|(t, _)| *t).collect();
let from_names: HashSet<&str> = ALL_MATRIX_TOOL_NAMES.iter().copied().collect();
for t in &from_entries {
assert!(
from_names.contains(t),
"tool '{t}' is in ALL_MATRIX_ENTRIES but NOT in ALL_MATRIX_TOOL_NAMES"
);
}
for t in &from_names {
assert!(
from_entries.contains(t),
"tool '{t}' is in ALL_MATRIX_TOOL_NAMES but NOT in ALL_MATRIX_ENTRIES"
);
}
}
#[test]
fn resolve_read_balance() {
let (tool, cap) = resolve_action("stellar_balances").unwrap();
assert_eq!(tool, "stellar_balances");
assert_eq!(cap, Capability::ReadBalance);
}
#[test]
fn resolve_stellar_pay() {
let (tool, cap) = resolve_action("stellar_pay").unwrap();
assert_eq!(tool, "stellar_pay");
assert_eq!(cap, Capability::ProposeTransaction);
}
#[test]
fn resolve_stellar_rule_create() {
let (tool, cap) = resolve_action("stellar_rule_create").unwrap();
assert_eq!(tool, "stellar_rule_create");
assert_eq!(cap, Capability::ProposeTransaction);
}
#[test]
fn resolve_suggest_destination_tools() {
for tool in [
"stellar_sep47_discover",
"stellar_sep48_preview_invocation",
"stellar_sep7_parse_uri",
] {
let (resolved, cap) = resolve_action(tool).unwrap();
assert_eq!(resolved, tool);
assert_eq!(cap, Capability::SuggestDestination);
}
}
#[test]
fn resolve_read_rules_tools() {
for tool in ["stellar_rules_list", "stellar_rules_get"] {
let (resolved, cap) = resolve_action(tool).unwrap();
assert_eq!(resolved, tool);
assert_eq!(cap, Capability::ReadRules);
}
}
#[test]
fn signing_tools_not_in_matrix() {
for tool in SIGNING_DENYLIST {
let result = resolve_action(tool);
assert!(
result.is_err(),
"signing/denylist tool '{tool}' must NOT resolve via the matrix"
);
}
}
#[test]
fn observe_event_grant_is_empty() {
assert!(
OBSERVE_EVENT_GRANTS.is_empty(),
"ObserveEvent grant must be empty (no event-stream tool exists yet)"
);
}
#[test]
fn grants_for_read_balance() {
let grants = grants_for_capability(Capability::ReadBalance);
assert_eq!(grants, READ_BALANCE_GRANTS);
}
#[test]
fn grants_for_propose_transaction() {
let grants = grants_for_capability(Capability::ProposeTransaction);
assert_eq!(grants, PROPOSE_TRANSACTION_GRANTS);
}
#[test]
fn grants_for_suggest_destination() {
let grants = grants_for_capability(Capability::SuggestDestination);
assert_eq!(grants, SUGGEST_DESTINATION_GRANTS);
}
#[test]
fn grants_for_observe_event() {
let grants = grants_for_capability(Capability::ObserveEvent);
assert!(grants.is_empty());
}
#[test]
fn grants_for_read_rules() {
let grants = grants_for_capability(Capability::ReadRules);
assert_eq!(grants, READ_RULES_GRANTS);
assert_eq!(grants, &["stellar_rules_list", "stellar_rules_get"]);
}
#[test]
fn grants_for_sign_payment_ungated_is_empty() {
let grants = grants_for_capability(Capability::SignPayment);
assert!(
grants.is_empty(),
"SignPayment ungated grant must be empty (gated tier only)"
);
}
#[test]
fn grants_for_sign_rule_create_ungated_is_empty() {
let grants = grants_for_capability(Capability::SignRuleCreate);
assert!(
grants.is_empty(),
"SignRuleCreate ungated grant must be empty (gated tier only)"
);
}
fn flattened_gated_tools() -> Vec<&'static str> {
GATED_MATRIX_ENTRIES
.iter()
.flat_map(|(_, tools)| tools.iter().copied())
.collect()
}
#[test]
fn gated_tool_not_in_ungated_matrix() {
use std::collections::HashSet;
let ungated: HashSet<&str> = ALL_MATRIX_ENTRIES.iter().map(|(t, _)| *t).collect();
for tool in flattened_gated_tools() {
assert!(
!ungated.contains(tool),
"gated tool '{tool}' must NOT appear in the ungated ALL_MATRIX_ENTRIES \
(would bypass the first-invoke gate)"
);
}
}
#[test]
fn gated_tool_is_in_signing_denylist() {
use std::collections::HashSet;
let denylist: HashSet<&str> = SIGNING_DENYLIST.iter().copied().collect();
for tool in flattened_gated_tools() {
assert!(
denylist.contains(tool),
"gated tool '{tool}' MUST be in SIGNING_DENYLIST — this is the structural \
proof that the ungated resolve_action path is permanently blocked"
);
}
}
#[test]
fn gated_matrix_entries_subset_of_signing_denylist() {
use std::collections::HashSet;
let denylist: HashSet<&str> = SIGNING_DENYLIST.iter().copied().collect();
for (cap, tools) in GATED_MATRIX_ENTRIES {
for tool in *tools {
assert!(
denylist.contains(tool),
"gated tool '{tool}' (capability {cap:?}) is in GATED_MATRIX_ENTRIES \
but NOT in SIGNING_DENYLIST — add it to SIGNING_DENYLIST so the \
ungated resolve_action path is permanently blocked"
);
}
}
}
#[test]
fn sign_payment_gated_tools_do_not_include_bare_sign_tools() {
let bare_sign_tools = [
"stellar_sep43_sign_transaction",
"stellar_sep43_sign_and_submit_transaction",
"stellar_sep43_sign_auth_entry",
"stellar_sep43_sign_message",
"stellar_sep53_sign_message",
];
for bare in &bare_sign_tools {
assert!(
!SIGN_PAYMENT_GATED_TOOLS.contains(bare),
"SignPayment gated tools must NOT include bare-sign tool '{bare}'"
);
}
assert!(
SIGN_PAYMENT_GATED_TOOLS.contains(&"stellar_pay_commit"),
"stellar_pay_commit must be in SIGN_PAYMENT_GATED_TOOLS"
);
}
#[test]
fn resolve_action_does_not_resolve_gated_tools() {
for tool in flattened_gated_tools() {
let result = resolve_action(tool);
assert!(
result.is_err(),
"gated tool '{tool}' must NOT resolve via the ungated resolve_action path \
(gated resolver is the sole admission)"
);
}
}
#[test]
fn gated_grants_for_sign_payment() {
let grants = gated_grants_for_capability(Capability::SignPayment);
assert!(grants.is_some(), "SignPayment must have gated grants");
let grants = grants.unwrap();
assert!(grants.contains(&"stellar_pay_commit"));
}
#[test]
fn gated_grants_for_ungated_capabilities_is_none() {
for cap in [
Capability::ReadBalance,
Capability::ProposeTransaction,
Capability::SuggestDestination,
Capability::ObserveEvent,
] {
assert!(
gated_grants_for_capability(cap).is_none(),
"capability {cap:?} must NOT have gated grants"
);
}
}
#[test]
fn gated_grants_for_sign_rule_create() {
let grants = gated_grants_for_capability(Capability::SignRuleCreate);
assert!(grants.is_some(), "SignRuleCreate must have gated grants");
let grants = grants.unwrap();
assert!(grants.contains(&"stellar_rule_create_commit"));
}
#[test]
fn sign_rule_create_gated_tools_do_not_include_bare_sign_tools() {
let bare_sign_tools = [
"stellar_sep43_sign_transaction",
"stellar_sep43_sign_and_submit_transaction",
"stellar_sep43_sign_auth_entry",
"stellar_sep43_sign_message",
"stellar_sep53_sign_message",
];
for bare in &bare_sign_tools {
assert!(
!SIGN_RULE_CREATE_GATED_TOOLS.contains(bare),
"SignRuleCreate gated tools must NOT include bare-sign tool '{bare}'"
);
}
assert!(
SIGN_RULE_CREATE_GATED_TOOLS.contains(&"stellar_rule_create_commit"),
"stellar_rule_create_commit must be in SIGN_RULE_CREATE_GATED_TOOLS"
);
}
#[test]
fn resolve_action_does_not_resolve_stellar_rule_create_commit() {
assert!(resolve_action("stellar_rule_create_commit").is_err());
}
}