pub mod manifest;
#[cfg(feature = "mcp")]
pub(crate) mod rate;
use serde::{Deserialize, Serialize};
pub use manifest::{AgentAuthorityDescriptor, GrantDescriptor};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum EffectKind {
Write,
UnboundedWrite,
CrossTenant,
Outbound,
Webhook,
Job,
}
impl EffectKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Write => "write",
Self::UnboundedWrite => "unbounded_write",
Self::CrossTenant => "cross_tenant",
Self::Outbound => "outbound",
Self::Webhook => "webhook",
Self::Job => "job",
}
}
}
impl std::fmt::Display for EffectKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum EffectProvenance {
TypeResolved,
Syntactic,
Declared,
}
impl EffectProvenance {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::TypeResolved => "type_resolved",
Self::Syntactic => "syntactic",
Self::Declared => "declared",
}
}
}
impl std::fmt::Display for EffectProvenance {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Effect {
pub kind: EffectKind,
pub subject: &'static str,
pub location: &'static str,
pub provenance: EffectProvenance,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum Reversibility {
Reversible,
Compensable,
Irreversible,
}
impl Reversibility {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Reversible => "reversible",
Self::Compensable => "compensable",
Self::Irreversible => "irreversible",
}
}
#[must_use]
pub const fn rank(self) -> u8 {
match self {
Self::Reversible => 0,
Self::Compensable => 1,
Self::Irreversible => 2,
}
}
}
impl std::fmt::Display for Reversibility {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum TenantScope {
Scoped,
CrossTenant,
None,
}
impl TenantScope {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Scoped => "scoped",
Self::CrossTenant => "cross_tenant",
Self::None => "none",
}
}
}
impl std::fmt::Display for TenantScope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Grant {
pub name: &'static str,
pub writes: &'static [&'static str],
pub unbounded_writes: &'static [&'static str],
pub tenant_scope: TenantScope,
pub outbound: &'static [&'static str],
pub webhooks: &'static [&'static str],
pub jobs: &'static [&'static str],
pub rate: Option<&'static str>,
pub spend: Option<&'static str>,
pub reversibility: Reversibility,
pub location: &'static str,
}
impl Grant {
#[must_use]
pub const fn allows_write(&self, model: &str) -> bool {
list_contains(self.writes, model) || list_contains(self.unbounded_writes, model)
}
#[must_use]
pub const fn allows_writes_all(&self, models: &[&str]) -> bool {
let mut i = 0;
while i < models.len() {
if !self.allows_write(models[i]) {
return false;
}
i += 1;
}
true
}
#[must_use]
pub const fn allows_unbounded_write(&self, model: &str) -> bool {
list_contains(self.unbounded_writes, model)
}
#[must_use]
pub const fn allows_cross_tenant(&self) -> bool {
match self.tenant_scope {
TenantScope::Scoped => false,
TenantScope::CrossTenant | TenantScope::None => true,
}
}
#[must_use]
pub const fn allows_outbound(&self, url: &str) -> bool {
let mut i = 0;
while i < self.outbound.len() {
if prefix_ends_at_boundary(url, self.outbound[i]) {
return true;
}
i += 1;
}
false
}
#[must_use]
pub const fn allows_webhook(&self, topic: &str) -> bool {
list_contains(self.webhooks, topic)
}
#[must_use]
pub const fn allows_job(&self, job: &str) -> bool {
list_contains(self.jobs, job)
}
#[must_use]
pub const fn allows_reversibility_floor(&self, floor: Reversibility) -> bool {
self.reversibility.rank() >= floor.rank()
}
}
#[must_use]
pub const fn reversibility_floor_of(kind: EffectKind) -> Reversibility {
match kind {
EffectKind::Write | EffectKind::CrossTenant => Reversibility::Reversible,
_ => Reversibility::Compensable,
}
}
const fn str_eq(a: &str, b: &str) -> bool {
let a = a.as_bytes();
let b = b.as_bytes();
if a.len() != b.len() {
return false;
}
let mut i = 0;
while i < a.len() {
if a[i] != b[i] {
return false;
}
i += 1;
}
true
}
const fn list_contains(list: &[&str], needle: &str) -> bool {
let mut i = 0;
while i < list.len() {
if str_eq(list[i], needle) {
return true;
}
i += 1;
}
false
}
const fn prefix_ends_at_boundary(value: &str, prefix: &str) -> bool {
let v = value.as_bytes();
let p = prefix.as_bytes();
if p.is_empty() || v.len() < p.len() {
return false;
}
let mut i = 0;
while i < p.len() {
if v[i] != p[i] {
return false;
}
i += 1;
}
if v.len() == p.len() {
return true;
}
let next = v[p.len()];
next == b'/' || next == b'?' || next == b'#'
}
const fn tail_eq(haystack: &[u8], start: usize, needle: &str) -> bool {
let needle = needle.as_bytes();
if haystack.len() - start != needle.len() {
return false;
}
let mut i = 0;
while i < needle.len() {
if haystack[start + i] != needle[i] {
return false;
}
i += 1;
}
true
}
#[doc(hidden)]
#[derive(Debug, Clone, Copy)]
pub struct CascadeLevel {
pub models: &'static [&'static str],
pub more: bool,
}
impl CascadeLevel {
pub const NONE: Self = Self {
models: &[],
more: false,
};
}
#[doc(hidden)]
#[must_use]
pub const fn cascade_len(parts: &[&[&str]]) -> usize {
let mut total = 0;
let mut i = 0;
while i < parts.len() {
total += parts[i].len();
i += 1;
}
total
}
#[doc(hidden)]
#[must_use]
pub const fn cascade_union<const N: usize>(parts: &[&[&'static str]]) -> [&'static str; N] {
assert!(
cascade_len(parts) == N,
"cascade_union: N must equal cascade_len"
);
let mut out = [""; N];
let mut at = 0;
let mut i = 0;
while i < parts.len() {
let mut j = 0;
while j < parts[i].len() {
out[at] = parts[i][j];
at += 1;
j += 1;
}
i += 1;
}
out
}
#[must_use]
pub const fn rate_is_wellformed(rate: &str) -> bool {
let b = rate.as_bytes();
let mut i = 0;
let mut nonzero = false;
while i < b.len() && b[i] >= b'0' && b[i] <= b'9' {
if b[i] != b'0' {
nonzero = true;
}
i += 1;
}
if i == 0 || !nonzero {
return false;
}
if i >= b.len() || b[i] != b'/' {
return false;
}
let unit = i + 1;
tail_eq(b, unit, "s")
|| tail_eq(b, unit, "sec")
|| tail_eq(b, unit, "min")
|| tail_eq(b, unit, "hour")
|| tail_eq(b, unit, "day")
}
#[must_use]
pub const fn spend_is_wellformed(spend: &str) -> bool {
let b = spend.as_bytes();
let mut i = 0;
let mut digits = 0;
while i < b.len() && b[i] >= b'0' && b[i] <= b'9' {
i += 1;
digits += 1;
}
if digits == 0 {
return false;
}
if i < b.len() && b[i] == b'.' {
i += 1;
let mut fraction = 0;
while i < b.len() && b[i] >= b'0' && b[i] <= b'9' {
i += 1;
fraction += 1;
}
if fraction == 0 {
return false;
}
}
if i >= b.len() || b[i] != b' ' {
return false;
}
i += 1;
if b.len() - i != 3 {
return false;
}
let mut k = 0;
while k < 3 {
if b[i + k] < b'A' || b[i + k] > b'Z' {
return false;
}
k += 1;
}
true
}
#[doc(hidden)]
#[must_use]
pub const fn reason_is_blank(reason: &str) -> bool {
crate::cache::coherence::reason_is_blank(reason)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct AssertedEffectFree {
pub location: &'static str,
pub reason: &'static str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AgentAuthority {
pub action: &'static str,
pub module_path: &'static str,
pub location: &'static str,
pub grant: &'static Grant,
pub effects: &'static [Effect],
pub asserted_effect_free_sites: u32,
pub asserted_effect_free: &'static [AssertedEffectFree],
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AgentActor(pub String);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AgentInvocation {
pub correlation_id: String,
pub tool: String,
pub grant: Option<&'static str>,
pub reversibility: Option<Reversibility>,
}
#[doc(hidden)]
#[derive(Debug, Clone, Copy)]
pub struct GrantBuilder {
pub writes: &'static [&'static str],
pub unbounded_writes: &'static [&'static str],
pub tenant_scope: TenantScope,
pub outbound: &'static [&'static str],
pub webhooks: &'static [&'static str],
pub jobs: &'static [&'static str],
pub rate: Option<&'static str>,
pub spend: Option<&'static str>,
pub reversibility: Option<Reversibility>,
}
impl GrantBuilder {
pub const DEFAULT: Self = Self {
writes: &[],
unbounded_writes: &[],
tenant_scope: TenantScope::Scoped,
outbound: &[],
webhooks: &[],
jobs: &[],
rate: None,
spend: None,
reversibility: None,
};
#[doc(hidden)]
#[must_use]
pub const fn reversibility_declared(&self) -> bool {
self.reversibility.is_some()
}
#[doc(hidden)]
#[must_use]
pub const fn rate_is_valid(&self) -> bool {
match self.rate {
Some(rate) => rate_is_wellformed(rate),
None => true,
}
}
#[doc(hidden)]
#[must_use]
pub const fn spend_is_valid(&self) -> bool {
match self.spend {
Some(spend) => spend_is_wellformed(spend),
None => true,
}
}
#[doc(hidden)]
#[must_use]
pub const fn build(self, name: &'static str, location: &'static str) -> Grant {
let reversibility = match self.reversibility {
Some(reversibility) => reversibility,
None => Reversibility::Irreversible,
};
Grant {
name,
writes: self.writes,
unbounded_writes: self.unbounded_writes,
tenant_scope: self.tenant_scope,
outbound: self.outbound,
webhooks: self.webhooks,
jobs: self.jobs,
rate: self.rate,
spend: self.spend,
reversibility,
location,
}
}
}
#[doc(hidden)]
#[macro_export]
macro_rules! __authority_grant_subject {
($subject:literal) => {
$subject
};
($subject:ident) => {
stringify!($subject)
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __authority_grant_tenant_scope {
(scoped) => {
$crate::agent_authority::TenantScope::Scoped
};
(cross_tenant) => {
$crate::agent_authority::TenantScope::CrossTenant
};
(none) => {
$crate::agent_authority::TenantScope::None
};
($other:tt) => {
::core::compile_error!(::core::concat!(
"authority_grant!: `tenant_scope` must be `scoped`, `cross_tenant` or `none`, not `",
::core::stringify!($other),
"`. `none` means the application is single-tenant and the dimension does not apply. \
See docs/guide/agent-authority.md"
))
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __authority_grant_reversibility {
(reversible) => {
$crate::agent_authority::Reversibility::Reversible
};
(compensable) => {
$crate::agent_authority::Reversibility::Compensable
};
(irreversible) => {
$crate::agent_authority::Reversibility::Irreversible
};
(compensatable) => {
::core::compile_error!(
"authority_grant!: `reversibility` is spelled `compensable`, not `compensatable`. \
See docs/guide/agent-authority.md"
)
};
($other:tt) => {
::core::compile_error!(::core::concat!(
"authority_grant!: `reversibility` must be `reversible`, `compensable` or \
`irreversible`, not `",
::core::stringify!($other),
"`. See docs/guide/agent-authority.md"
))
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __authority_grant_parse {
(
@meta [$($meta:tt)*] @vis [$($vis:tt)*] @name [$name:ident]
@fields [$($fields:tt)*] @rest []
) => {
const _: () = {
#[allow(clippy::needless_update)]
let builder = $crate::agent_authority::GrantBuilder {
$($fields)*
..$crate::agent_authority::GrantBuilder::DEFAULT
};
::core::assert!(
builder.rate_is_valid(),
"authority_grant!: `rate` must read `<count>/<unit>`, e.g. \"10/min\" -- the \
count is a positive integer and the unit is one of `s`, `sec`, `min`, `hour`, \
`day`. See docs/guide/agent-authority.md"
);
};
const _: () = {
#[allow(clippy::needless_update)]
let builder = $crate::agent_authority::GrantBuilder {
$($fields)*
..$crate::agent_authority::GrantBuilder::DEFAULT
};
::core::assert!(
builder.spend_is_valid(),
"authority_grant!: `spend` must read `<amount> <CURRENCY>`, e.g. \"500.00 USD\" \
-- a non-negative decimal, one space, and a three-letter uppercase ISO 4217 \
code. See docs/guide/agent-authority.md"
);
};
const _: () = {
#[allow(clippy::needless_update)]
let builder = $crate::agent_authority::GrantBuilder {
$($fields)*
..$crate::agent_authority::GrantBuilder::DEFAULT
};
::core::assert!(
builder.reversibility_declared(),
"authority_grant! requires `reversibility:`. Add one of \
`reversibility: reversible`, `reversibility: compensable` or \
`reversibility: irreversible` to the grant -- how far an agent's action can be \
walked back is the one thing about it nobody should have to infer. \
See docs/guide/agent-authority.md"
);
};
$($meta)*
#[allow(non_upper_case_globals)]
$($vis)* const $name: $crate::agent_authority::Grant = {
#[allow(clippy::needless_update)]
let builder = $crate::agent_authority::GrantBuilder {
$($fields)*
..$crate::agent_authority::GrantBuilder::DEFAULT
};
builder.build(
::core::stringify!($name),
::core::concat!(::core::file!(), ":", ::core::line!()),
)
};
$crate::reexports::inventory::submit! {
$crate::agent_authority::GrantDescriptor(&$name)
}
};
(
@meta $meta:tt @vis $vis:tt @name $name:tt @fields [$($fields:tt)*]
@rest [writes: [$($subject:tt),* $(,)?] $(, $($rest:tt)*)?]
) => {
$crate::__authority_grant_parse! {
@meta $meta @vis $vis @name $name
@fields [$($fields)* writes: &[$($crate::__authority_grant_subject!($subject)),*],]
@rest [$($($rest)*)?]
}
};
(
@meta $meta:tt @vis $vis:tt @name $name:tt @fields [$($fields:tt)*]
@rest [unbounded_writes: [$($subject:tt),* $(,)?] $(, $($rest:tt)*)?]
) => {
$crate::__authority_grant_parse! {
@meta $meta @vis $vis @name $name
@fields [
$($fields)*
unbounded_writes: &[$($crate::__authority_grant_subject!($subject)),*],
]
@rest [$($($rest)*)?]
}
};
(
@meta $meta:tt @vis $vis:tt @name $name:tt @fields [$($fields:tt)*]
@rest [jobs: [$($subject:tt),* $(,)?] $(, $($rest:tt)*)?]
) => {
$crate::__authority_grant_parse! {
@meta $meta @vis $vis @name $name
@fields [$($fields)* jobs: &[$($crate::__authority_grant_subject!($subject)),*],]
@rest [$($($rest)*)?]
}
};
(
@meta $meta:tt @vis $vis:tt @name $name:tt @fields [$($fields:tt)*]
@rest [outbound: [$($url:literal),* $(,)?] $(, $($rest:tt)*)?]
) => {
$crate::__authority_grant_parse! {
@meta $meta @vis $vis @name $name
@fields [$($fields)* outbound: &[$($url),*],]
@rest [$($($rest)*)?]
}
};
(
@meta $meta:tt @vis $vis:tt @name $name:tt @fields $fields:tt
@rest [outbound: $bad:tt $(, $($rest:tt)*)?]
) => {
::core::compile_error!(
"authority_grant!: `outbound` takes a list of string literals, e.g. \
`outbound: [\"https://api.stripe.com/v1/refunds\", \"alias:stripe\"]` -- a URL \
prefix matched at a path boundary, or `alias:<name>` for a named client. \
See docs/guide/agent-authority.md"
);
};
(
@meta $meta:tt @vis $vis:tt @name $name:tt @fields [$($fields:tt)*]
@rest [webhooks: [$($topic:literal),* $(,)?] $(, $($rest:tt)*)?]
) => {
$crate::__authority_grant_parse! {
@meta $meta @vis $vis @name $name
@fields [$($fields)* webhooks: &[$($topic),*],]
@rest [$($($rest)*)?]
}
};
(
@meta $meta:tt @vis $vis:tt @name $name:tt @fields $fields:tt
@rest [webhooks: $bad:tt $(, $($rest:tt)*)?]
) => {
::core::compile_error!(
"authority_grant!: `webhooks` takes a list of string literals, e.g. \
`webhooks: [\"refund.drafted\"]`. See docs/guide/agent-authority.md"
);
};
(
@meta $meta:tt @vis $vis:tt @name $name:tt @fields [$($fields:tt)*]
@rest [tenant_scope: $scope:tt $(, $($rest:tt)*)?]
) => {
$crate::__authority_grant_parse! {
@meta $meta @vis $vis @name $name
@fields [$($fields)* tenant_scope: $crate::__authority_grant_tenant_scope!($scope),]
@rest [$($($rest)*)?]
}
};
(
@meta $meta:tt @vis $vis:tt @name $name:tt @fields [$($fields:tt)*]
@rest [reversibility: $reversibility:tt $(, $($rest:tt)*)?]
) => {
$crate::__authority_grant_parse! {
@meta $meta @vis $vis @name $name
@fields [
$($fields)*
reversibility: ::core::option::Option::Some(
$crate::__authority_grant_reversibility!($reversibility)
),
]
@rest [$($($rest)*)?]
}
};
(
@meta $meta:tt @vis $vis:tt @name $name:tt @fields [$($fields:tt)*]
@rest [rate: $rate:literal $(, $($rest:tt)*)?]
) => {
$crate::__authority_grant_parse! {
@meta $meta @vis $vis @name $name
@fields [$($fields)* rate: ::core::option::Option::Some($rate),]
@rest [$($($rest)*)?]
}
};
(
@meta $meta:tt @vis $vis:tt @name $name:tt @fields $fields:tt
@rest [rate: $bad:tt $(, $($rest:tt)*)?]
) => {
::core::compile_error!(
"authority_grant!: `rate` takes a string literal, e.g. `rate: \"10/min\"`. \
See docs/guide/agent-authority.md"
);
};
(
@meta $meta:tt @vis $vis:tt @name $name:tt @fields [$($fields:tt)*]
@rest [spend: $spend:literal $(, $($rest:tt)*)?]
) => {
$crate::__authority_grant_parse! {
@meta $meta @vis $vis @name $name
@fields [$($fields)* spend: ::core::option::Option::Some($spend),]
@rest [$($($rest)*)?]
}
};
(
@meta $meta:tt @vis $vis:tt @name $name:tt @fields $fields:tt
@rest [spend: $bad:tt $(, $($rest:tt)*)?]
) => {
::core::compile_error!(
"authority_grant!: `spend` takes a string literal, e.g. `spend: \"500.00 USD\"`. \
See docs/guide/agent-authority.md"
);
};
(
@meta $meta:tt @vis $vis:tt @name $name:tt @fields $fields:tt
@rest [$key:ident : $($rest:tt)*]
) => {
::core::compile_error!(::core::concat!(
"authority_grant!: unknown key `",
::core::stringify!($key),
"`. The grammar is `writes: [..]`, `unbounded_writes: [..]`, \
`tenant_scope: scoped | cross_tenant | none`, `outbound: [\"..\"]`, \
`webhooks: [\"..\"]`, `jobs: [..]`, `rate: \"10/min\"`, `spend: \"500.00 USD\"`, \
`reversibility: reversible | compensable | irreversible` (required). The keys may \
appear in any order and all but `reversibility` may be omitted. \
See docs/guide/agent-authority.md"
));
};
(
@meta $meta:tt @vis $vis:tt @name $name:tt @fields $fields:tt
@rest [$($junk:tt)*]
) => {
::core::compile_error!(
"authority_grant!: could not parse the grant body. Each entry reads `key: value,`. \
See docs/guide/agent-authority.md"
);
};
}
#[macro_export]
macro_rules! authority_grant {
(
$(#[$meta:meta])*
pub($($restricted:tt)*) $name:ident { $($body:tt)* }
) => {
$crate::__authority_grant_parse! {
@meta [$(#[$meta])*] @vis [pub($($restricted)*)] @name [$name]
@fields [] @rest [$($body)*]
}
};
(
$(#[$meta:meta])*
pub $name:ident { $($body:tt)* }
) => {
$crate::__authority_grant_parse! {
@meta [$(#[$meta])*] @vis [pub] @name [$name]
@fields [] @rest [$($body)*]
}
};
(
$(#[$meta:meta])*
$name:ident { $($body:tt)* }
) => {
$crate::__authority_grant_parse! {
@meta [$(#[$meta])*] @vis [] @name [$name]
@fields [] @rest [$($body)*]
}
};
}
#[cfg(test)]
mod tests {
#[test]
fn a_grant_allows_a_list_of_writes_only_when_it_allows_each() {
const GRANT: Grant = Grant {
name: "G",
writes: &["Post", "Comment"],
unbounded_writes: &["Tag"],
tenant_scope: TenantScope::Scoped,
outbound: &[],
webhooks: &[],
jobs: &[],
rate: None,
spend: None,
reversibility: Reversibility::Compensable,
location: "test",
};
assert!(GRANT.allows_writes_all(&[]));
assert!(GRANT.allows_writes_all(&["Post", "Tag"]));
assert!(!GRANT.allows_writes_all(&["Post", "Like"]));
}
#[test]
fn cascade_union_joins_slices_in_order() {
const PARTS: &[&[&str]] = &[&["A", "B"], &[], &["C"]];
const N: usize = cascade_len(PARTS);
const JOINED: [&str; N] = cascade_union::<N>(PARTS);
assert_eq!(JOINED, ["A", "B", "C"]);
}
use super::*;
const fn empty_grant() -> Grant {
Grant {
name: "TestGrant",
writes: &[],
unbounded_writes: &[],
tenant_scope: TenantScope::Scoped,
outbound: &[],
webhooks: &[],
jobs: &[],
rate: None,
spend: None,
reversibility: Reversibility::Reversible,
location: "test:0",
}
}
#[test]
fn a_listed_model_is_writable_and_an_unlisted_one_is_not() {
let grant = Grant {
writes: &["Refund", "RefundNote"],
..empty_grant()
};
assert!(grant.allows_write("Refund"));
assert!(grant.allows_write("RefundNote"));
assert!(!grant.allows_write("Payment"));
assert!(!grant.allows_write("RefundNoteRevision"));
assert!(!grant.allows_write("Refun"));
}
#[test]
fn an_empty_write_list_allows_nothing() {
let grant = empty_grant();
assert!(!grant.allows_write("Refund"));
assert!(!grant.allows_write(""));
}
#[test]
fn writes_does_not_imply_unbounded_writes() {
let grant = Grant {
writes: &["Refund"],
..empty_grant()
};
assert!(grant.allows_write("Refund"));
assert!(!grant.allows_unbounded_write("Refund"));
}
#[test]
fn unbounded_writes_implies_the_bounded_form() {
let grant = Grant {
unbounded_writes: &["Refund"],
..empty_grant()
};
assert!(grant.allows_unbounded_write("Refund"));
assert!(grant.allows_write("Refund"));
assert!(!grant.allows_unbounded_write("Payment"));
}
#[test]
fn an_outbound_prefix_matches_only_at_a_path_boundary() {
let grant = Grant {
outbound: &["https://api.stripe.com/v1/refunds"],
..empty_grant()
};
assert!(grant.allows_outbound("https://api.stripe.com/v1/refunds"));
assert!(grant.allows_outbound("https://api.stripe.com/v1/refunds/"));
assert!(grant.allows_outbound("https://api.stripe.com/v1/refunds/re_123"));
assert!(grant.allows_outbound("https://api.stripe.com/v1/refunds?x=1"));
assert!(grant.allows_outbound("https://api.stripe.com/v1/refunds#frag"));
assert!(!grant.allows_outbound("https://api.stripe.com/v1/refunds-evil"));
assert!(!grant.allows_outbound("https://api.stripe.com/v1/refundsevil.com"));
}
#[test]
fn the_outbound_scheme_and_host_must_match_exactly() {
let grant = Grant {
outbound: &["https://api.stripe.com/v1/refunds"],
..empty_grant()
};
assert!(!grant.allows_outbound("http://api.stripe.com/v1/refunds"));
assert!(!grant.allows_outbound("https://api.stripe.com.evil.test/v1/refunds"));
assert!(!grant.allows_outbound("https://api.stripe.com/v1/charges"));
}
#[test]
fn an_empty_outbound_list_allows_no_url() {
assert!(!empty_grant().allows_outbound("https://api.stripe.com/v1/refunds"));
}
#[test]
fn an_alias_entry_matches_exactly() {
let grant = Grant {
outbound: &["alias:stripe"],
..empty_grant()
};
assert!(grant.allows_outbound("alias:stripe"));
assert!(!grant.allows_outbound("alias:stripe2"));
assert!(!grant.allows_outbound("alias:strip"));
assert!(!grant.allows_outbound("https://api.stripe.com/v1/refunds"));
}
#[test]
fn a_webhook_topic_matches_exactly() {
let grant = Grant {
webhooks: &["refund.drafted"],
..empty_grant()
};
assert!(grant.allows_webhook("refund.drafted"));
assert!(!grant.allows_webhook("refund.drafted.v2"));
assert!(!grant.allows_webhook("refund"));
assert!(!empty_grant().allows_webhook("refund.drafted"));
}
#[test]
fn a_job_matches_exactly() {
let grant = Grant {
jobs: &["NotifyFinance", "audit_export"],
..empty_grant()
};
assert!(grant.allows_job("NotifyFinance"));
assert!(grant.allows_job("audit_export"));
assert!(!grant.allows_job("NotifyFinanceLater"));
assert!(!empty_grant().allows_job("NotifyFinance"));
}
#[test]
fn crossing_tenants_needs_cross_tenant_or_a_single_tenant_app() {
assert!(!empty_grant().allows_cross_tenant());
assert!(
Grant {
tenant_scope: TenantScope::CrossTenant,
..empty_grant()
}
.allows_cross_tenant()
);
assert!(
Grant {
tenant_scope: TenantScope::None,
..empty_grant()
}
.allows_cross_tenant()
);
}
#[test]
fn a_reversible_grant_cannot_carry_a_compensable_floor() {
let grant = empty_grant();
assert!(grant.allows_reversibility_floor(Reversibility::Reversible));
assert!(!grant.allows_reversibility_floor(Reversibility::Compensable));
assert!(!grant.allows_reversibility_floor(Reversibility::Irreversible));
}
#[test]
fn a_compensable_grant_accepts_the_compensable_floor() {
let grant = Grant {
reversibility: Reversibility::Compensable,
..empty_grant()
};
assert!(grant.allows_reversibility_floor(Reversibility::Reversible));
assert!(grant.allows_reversibility_floor(Reversibility::Compensable));
assert!(!grant.allows_reversibility_floor(Reversibility::Irreversible));
}
#[test]
fn an_irreversible_grant_accepts_every_floor() {
let grant = Grant {
reversibility: Reversibility::Irreversible,
..empty_grant()
};
assert!(grant.allows_reversibility_floor(Reversibility::Reversible));
assert!(grant.allows_reversibility_floor(Reversibility::Compensable));
assert!(grant.allows_reversibility_floor(Reversibility::Irreversible));
}
#[test]
fn a_bounded_write_and_a_cross_tenant_reach_leave_an_action_reversible() {
for kind in [EffectKind::Write, EffectKind::CrossTenant] {
assert_eq!(
reversibility_floor_of(kind),
Reversibility::Reversible,
"{kind} must leave the action `reversible`"
);
}
for kind in [
EffectKind::UnboundedWrite,
EffectKind::Outbound,
EffectKind::Webhook,
EffectKind::Job,
] {
assert_eq!(
reversibility_floor_of(kind),
Reversibility::Compensable,
"{kind} must floor the action at `compensable`"
);
}
}
#[test]
fn a_well_formed_rate_is_accepted() {
for rate in ["10/min", "100/hour", "5/sec", "1/day", "60/s"] {
assert!(rate_is_wellformed(rate), "`{rate}` must be accepted");
}
}
#[test]
fn a_malformed_rate_is_rejected() {
for rate in [
"ten per minute",
"10/",
"/min",
"0/min",
"10/fortnight",
"10 min",
"",
"-1/min",
"10/min/extra",
] {
assert!(!rate_is_wellformed(rate), "`{rate}` must be rejected");
}
}
#[test]
fn a_well_formed_spend_is_accepted() {
for spend in ["500.00 USD", "12 EUR", "0 JPY", "1000000.99 GBP"] {
assert!(spend_is_wellformed(spend), "`{spend}` must be accepted");
}
}
#[test]
fn a_malformed_spend_is_rejected() {
for spend in [
"500",
"USD 500",
"500.00 usd",
"-1 USD",
"500.00 US",
"500.00 USDD",
"500.00",
" 500.00 USD",
"500.00 USD",
"",
] {
assert!(!spend_is_wellformed(spend), "`{spend}` must be rejected");
}
}
#[test]
fn a_whitespace_only_reason_is_blank() {
assert!(reason_is_blank(""));
assert!(reason_is_blank(" "));
assert!(!reason_is_blank("the helper does the write"));
}
}
#[cfg(test)]
#[allow(clippy::redundant_pub_crate)]
mod grant_syntax {
use super::*;
crate::authority_grant! {
pub(crate) ScrambledOrder {
reversibility: compensable,
spend: "500.00 USD",
jobs: [NotifyFinance, "audit_export"],
tenant_scope: cross_tenant,
outbound: ["https://api.stripe.com/v1/refunds", "alias:stripe"],
rate: "10/min",
webhooks: ["refund.drafted"],
unbounded_writes: [StaleDraft],
writes: [Refund, "refund_notes"],
}
}
crate::authority_grant! {
pub(crate) MinimalGrant {
reversibility: reversible,
}
}
crate::authority_grant! {
SingleTenantSweeper {
writes: [],
tenant_scope: none,
reversibility: irreversible
}
}
#[test]
fn a_scrambled_key_order_expands_to_the_same_envelope() {
assert_eq!(ScrambledOrder.name, "ScrambledOrder");
assert!(ScrambledOrder.allows_write("Refund"));
assert!(ScrambledOrder.allows_write("refund_notes"));
assert!(ScrambledOrder.allows_unbounded_write("StaleDraft"));
assert!(ScrambledOrder.allows_cross_tenant());
assert!(ScrambledOrder.allows_outbound("https://api.stripe.com/v1/refunds/re_1"));
assert!(ScrambledOrder.allows_outbound("alias:stripe"));
assert!(ScrambledOrder.allows_webhook("refund.drafted"));
assert!(ScrambledOrder.allows_job("NotifyFinance"));
assert!(ScrambledOrder.allows_job("audit_export"));
assert_eq!(ScrambledOrder.rate, Some("10/min"));
assert_eq!(ScrambledOrder.spend, Some("500.00 USD"));
assert_eq!(ScrambledOrder.reversibility, Reversibility::Compensable);
let location = ScrambledOrder.location.replace('\\', "/");
assert!(location.contains("agent_authority/mod.rs"), "{location}");
}
#[test]
fn a_grant_with_only_reversibility_denies_every_dimension() {
assert_eq!(MinimalGrant.name, "MinimalGrant");
assert!(!MinimalGrant.allows_write("Refund"));
assert!(!MinimalGrant.allows_unbounded_write("Refund"));
assert!(!MinimalGrant.allows_cross_tenant());
assert!(!MinimalGrant.allows_outbound("https://api.stripe.com/"));
assert!(!MinimalGrant.allows_webhook("refund.drafted"));
assert!(!MinimalGrant.allows_job("NotifyFinance"));
assert_eq!(MinimalGrant.rate, None);
assert_eq!(MinimalGrant.spend, None);
assert_eq!(MinimalGrant.reversibility, Reversibility::Reversible);
}
#[test]
fn a_single_tenant_grant_does_not_treat_crossing_tenants_as_a_violation() {
assert_eq!(SingleTenantSweeper.tenant_scope, TenantScope::None);
assert!(SingleTenantSweeper.allows_cross_tenant());
assert!(SingleTenantSweeper.writes.is_empty());
assert_eq!(
SingleTenantSweeper.reversibility,
Reversibility::Irreversible
);
}
#[test]
fn every_declared_grant_reaches_the_manifest_through_inventory() {
let names: Vec<&str> = inventory::iter::<GrantDescriptor>
.into_iter()
.map(|descriptor| descriptor.0.name)
.collect();
for expected in ["ScrambledOrder", "MinimalGrant", "SingleTenantSweeper"] {
assert!(
names.contains(&expected),
"`authority_grant!` must register `{expected}`: {names:?}"
);
}
}
#[test]
fn the_const_eval_gates_agree_with_the_grammar_they_advertise() {
assert!(rate_is_wellformed("10/min"));
assert!(!rate_is_wellformed("10/fortnight"));
assert!(spend_is_wellformed("500.00 USD"));
assert!(!spend_is_wellformed("500.00 usd"));
}
}