agent_capability_token/lib.rs
1//! Canonical wire format for the Agent Capability Token (ACT).
2//!
3//! ANVIL section 5 requires every compliant agent to operate under a scoped
4//! ACT, but no document defined the token's byte encoding. Three
5//! implementations filled that gap independently and disagreed: an issuer whose
6//! tokens no verifier could read, and two verifiers that agreed with each other
7//! and with nothing that was actually minted. Each declared in its own
8//! documentation that it matched the others.
9//!
10//! This crate is the single normative definition. It is vendor-neutral and
11//! depends on no product crate, so an issuer and a verifier can share one
12//! format without either depending on the other's implementation.
13//!
14//! # Format
15//!
16//! See [`wire`] for the envelope, and [`SPECIFICATION.md`] for the normative
17//! description.
18//!
19//! [`SPECIFICATION.md`]: https://github.com/OpenAgentID/act/blob/main/SPECIFICATION.md
20//!
21//! Encoding choices follow CWT (RFC 8392) conventions where they apply -
22//! integer epoch seconds for the temporal claims, a flat array for scopes, text
23//! for identifiers. This is **not** an RFC 8392 implementation: claim keys are
24//! text rather than the registered integer keys, and signing is raw Ed25519
25//! over the claims byte string rather than COSE_Sign1. Callers needing wire
26//! compatibility with a CWT library will not find it here.
27//!
28//! # Verifying
29//!
30//! ```
31//! use agent_capability_token::{verify, ActError, Verifier};
32//!
33//! # fn main() -> Result<(), ActError> {
34//! let verifier = Verifier::new(
35//! vec![[0u8; 32]], // in production, loaded from the issuer's key endpoint
36//! "arsenal:broker:prod-1",
37//! "omerta",
38//! )?
39//! .requiring_scopes(["tools:calendar:invoke".parse()?])
40//! .with_leeway_seconds(30);
41//!
42//! let token: &[u8] = &[]; // received from the gateway
43//! match verify(token, &verifier) {
44//! Ok(claims) => println!("authorized {}", claims.sub),
45//! Err(ActError::Expired { .. }) => println!("token expired"),
46//! Err(other) => println!("rejected: {other}"),
47//! }
48//! # Ok(())
49//! # }
50//! ```
51//!
52//! # What this crate does not do
53//!
54//! It carries claims and checks a token's integrity, temporal window, issuer,
55//! audience and scope grant. It does not evaluate issuer policy. Rate limits,
56//! spend budgets, device and network bindings and audit trace travel in
57//! [`ActClaims::ext`] as opaque signed CBOR, and the components that own their
58//! meaning interpret them. Restating those schemas here is exactly what caused
59//! the divergence this format replaces.
60
61#![forbid(unsafe_code)]
62
63pub mod claims;
64pub mod error;
65pub mod scope;
66pub mod wire;
67
68pub use claims::{ActClaims, Confirmation, Delegation};
69pub use error::{ActError, ActResult};
70pub use scope::{Scope, SCOPE_SEGMENTS, WILDCARD};
71pub use wire::{
72 claims_to_signing_payload, envelope_from_parts, verify, ActEnvelope, PublicKeyBytes, Verifier,
73 ALGORITHM_ED25519, FORMAT_VERSION, MAX_ACT_BYTES,
74};
75
76#[cfg(feature = "sign")]
77pub use wire::sign;