use std::collections::BTreeSet;
use std::fmt;
use serde::{Deserialize, Serialize};
use thiserror::Error;
macro_rules! string_id {
($(#[$meta:meta])* $name:ident) => {
$(#[$meta])*
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct $name(pub String);
impl $name {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl From<String> for $name {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for $name {
fn from(s: &str) -> Self {
Self(s.to_owned())
}
}
};
}
string_id! {
OperatorId
}
string_id! {
PlatformId
}
string_id! {
DelegateId
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct ExternalId(pub String);
impl ExternalId {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for ExternalId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl From<String> for ExternalId {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for ExternalId {
fn from(s: &str) -> Self {
Self(s.to_owned())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum TrustRoot {
Oidc { issuer: String },
Ado { realm: String },
GitHub { org: String },
}
impl TrustRoot {
fn to_uri_segments(&self) -> String {
match self {
TrustRoot::Oidc { issuer } => format!("oidc/{}", percent_encode(issuer)),
TrustRoot::Ado { realm } => format!("ado/{}", percent_encode(realm)),
TrustRoot::GitHub { org } => format!("github/{}", percent_encode(org)),
}
}
fn from_uri_segments(kind: &str, rest: &str) -> Result<Self, PrincipalParseError> {
match kind {
"oidc" => Ok(TrustRoot::Oidc {
issuer: percent_decode(rest)?,
}),
"ado" => Ok(TrustRoot::Ado {
realm: percent_decode(rest)?,
}),
"github" => Ok(TrustRoot::GitHub {
org: percent_decode(rest)?,
}),
other => Err(PrincipalParseError::UnknownTrustRoot(other.to_owned())),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct AuthorityScope {
capabilities: BTreeSet<Capability>,
}
impl AuthorityScope {
pub fn empty() -> Self {
Self {
capabilities: BTreeSet::new(),
}
}
pub fn root() -> Self {
Self {
capabilities: BTreeSet::from([Capability::ToolWildcard]),
}
}
pub fn from_capabilities<I: IntoIterator<Item = Capability>>(caps: I) -> Self {
Self {
capabilities: caps.into_iter().collect(),
}
}
pub fn contains(&self, cap: &Capability) -> bool {
self.capabilities.contains(cap)
|| (self.capabilities.contains(&Capability::ToolWildcard) && cap.is_tool())
}
pub fn is_superset_of(&self, other: &AuthorityScope) -> bool {
other.capabilities.iter().all(|c| self.contains(c))
}
pub fn iter(&self) -> impl Iterator<Item = &Capability> {
self.capabilities.iter()
}
pub fn is_empty(&self) -> bool {
self.capabilities.is_empty()
}
pub fn len(&self) -> usize {
self.capabilities.len()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Capability {
ToolApply,
ToolGet,
ToolLogs,
ToolEvents,
ToolWildcard,
}
impl Capability {
pub fn as_token(&self) -> &'static str {
match self {
Capability::ToolApply => "tool:apply",
Capability::ToolGet => "tool:get",
Capability::ToolLogs => "tool:logs",
Capability::ToolEvents => "tool:events",
Capability::ToolWildcard => "tool:*",
}
}
pub fn from_token(token: &str) -> Result<Self, PrincipalParseError> {
match token {
"tool:apply" => Ok(Capability::ToolApply),
"tool:get" => Ok(Capability::ToolGet),
"tool:logs" => Ok(Capability::ToolLogs),
"tool:events" => Ok(Capability::ToolEvents),
"tool:*" => Ok(Capability::ToolWildcard),
other => Err(PrincipalParseError::UnknownCapability(other.to_owned())),
}
}
fn is_tool(&self) -> bool {
matches!(
self,
Capability::ToolApply
| Capability::ToolGet
| Capability::ToolLogs
| Capability::ToolEvents
| Capability::ToolWildcard
)
}
}
impl Serialize for Capability {
fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
ser.serialize_str(self.as_token())
}
}
impl<'de> Deserialize<'de> for Capability {
fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
let s = String::deserialize(de)?;
Capability::from_token(&s).map_err(serde::de::Error::custom)
}
}
impl fmt::Display for Capability {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_token())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error(
"delegate scope not narrowing: requested capability `{missing}` \
not held by authorizing principal"
)]
pub struct AuthorityScopeViolation {
pub missing: Capability,
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum PrincipalParseError {
#[error("principal URI must start with `principal://`, got `{0}`")]
MissingScheme(String),
#[error("principal URI has no variant segment: `{0}`")]
EmptyPath(String),
#[error("unknown principal kind `{0}` (expected operator|platform|delegate|federated)")]
UnknownKind(String),
#[error("malformed `{kind}` principal URI: {reason}")]
Malformed { kind: &'static str, reason: String },
#[error("unknown trust root `{0}` (expected oidc|ado|github)")]
UnknownTrustRoot(String),
#[error("unknown capability token `{0}`")]
UnknownCapability(String),
#[error("invalid percent-encoding in URI segment: `{0}`")]
BadPercentEncoding(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Principal {
Operator { id: OperatorId },
Platform { id: PlatformId },
Delegate {
authorizing: Box<Principal>,
delegate: DelegateId,
scope: AuthorityScope,
},
Federated {
trust_root: TrustRoot,
identity: ExternalId,
},
}
impl Principal {
pub fn root_operator(&self) -> Option<&OperatorId> {
match self {
Principal::Operator { id } => Some(id),
Principal::Delegate { authorizing, .. } => authorizing.root_operator(),
Principal::Platform { .. } | Principal::Federated { .. } => None,
}
}
pub fn effective_scope(&self) -> AuthorityScope {
match self {
Principal::Delegate { scope, .. } => scope.clone(),
_ => AuthorityScope::root(),
}
}
pub fn compose(
authorizing: Principal,
delegate: DelegateId,
requested_scope: AuthorityScope,
) -> Result<Principal, AuthorityScopeViolation> {
let upstream = authorizing.effective_scope();
if let Some(missing) = requested_scope.iter().find(|cap| !upstream.contains(cap)) {
return Err(AuthorityScopeViolation {
missing: (*missing).clone(),
});
}
Ok(Principal::Delegate {
authorizing: Box::new(authorizing),
delegate,
scope: requested_scope,
})
}
pub fn to_source_uri(&self) -> String {
format!("principal://{}", self.uri_body_with_query())
}
fn uri_body_with_query(&self) -> String {
match self {
Principal::Operator { id } => format!("operator/{}", percent_encode(id.as_str())),
Principal::Platform { id } => format!("platform/{}", percent_encode(id.as_str())),
Principal::Federated {
trust_root,
identity,
} => format!(
"federated/{}/identity/{}",
trust_root.to_uri_segments(),
percent_encode(identity.as_str())
),
Principal::Delegate {
authorizing,
delegate,
scope,
} => {
let (auth_body, auth_query) = authorizing.uri_split();
let mut body = format!(
"{}/delegate/{}",
auth_body,
percent_encode(delegate.as_str())
);
let scope_query = if scope.is_empty() {
String::new()
} else {
let tokens: Vec<&str> = scope.iter().map(|c| c.as_token()).collect();
format!("scope={}", tokens.join(","))
};
let merged = match (auth_query.is_empty(), scope_query.is_empty()) {
(true, true) => String::new(),
(false, true) => auth_query,
(true, false) => scope_query,
(false, false) => format!("{}&{}", auth_query, scope_query),
};
if merged.is_empty() {
body
} else {
body.push('?');
body.push_str(&merged);
body
}
}
}
}
fn uri_split(&self) -> (String, String) {
let full = self.uri_body_with_query();
match full.find('?') {
Some(i) => (full[..i].to_owned(), full[i + 1..].to_owned()),
None => (full, String::new()),
}
}
pub fn from_source_uri(uri: &str) -> Result<Principal, PrincipalParseError> {
let rest = uri
.strip_prefix("principal://")
.ok_or_else(|| PrincipalParseError::MissingScheme(uri.to_owned()))?;
if rest.is_empty() {
return Err(PrincipalParseError::EmptyPath(uri.to_owned()));
}
let (path, query) = match rest.find('?') {
Some(i) => (&rest[..i], &rest[i + 1..]),
None => (rest, ""),
};
let mut scope_clauses: Vec<AuthorityScope> = Vec::new();
if !query.is_empty() {
for clause in query.split('&') {
let (k, v) =
clause
.split_once('=')
.ok_or_else(|| PrincipalParseError::Malformed {
kind: "query",
reason: format!("clause `{}` missing `=`", clause),
})?;
if k != "scope" {
return Err(PrincipalParseError::Malformed {
kind: "query",
reason: format!("unknown query parameter `{}`", k),
});
}
let caps: Result<BTreeSet<Capability>, _> = v
.split(',')
.filter(|s| !s.is_empty())
.map(Capability::from_token)
.collect();
scope_clauses.push(AuthorityScope {
capabilities: caps?,
});
}
}
let segments: Vec<&str> = path.split('/').collect();
Self::parse_segments(&segments, &mut scope_clauses.into_iter())
}
fn parse_segments(
segments: &[&str],
scopes: &mut std::vec::IntoIter<AuthorityScope>,
) -> Result<Principal, PrincipalParseError> {
if segments.is_empty() {
return Err(PrincipalParseError::Malformed {
kind: "principal",
reason: "no segments".to_owned(),
});
}
match segments[0] {
"operator" => {
if segments.len() < 2 {
return Err(PrincipalParseError::Malformed {
kind: "operator",
reason: "missing id segment".to_owned(),
});
}
let id = percent_decode(segments[1])?;
if segments.len() > 2 && segments[2] != "delegate" {
return Err(PrincipalParseError::Malformed {
kind: "operator",
reason: format!("unexpected segment `{}` after operator id", segments[2]),
});
}
let principal = Principal::Operator { id: OperatorId(id) };
Self::wrap_delegates(principal, &segments[2..], scopes)
}
"platform" => {
if segments.len() < 2 {
return Err(PrincipalParseError::Malformed {
kind: "platform",
reason: "missing id segment".to_owned(),
});
}
let id = percent_decode(segments[1])?;
if segments.len() > 2 && segments[2] != "delegate" {
return Err(PrincipalParseError::Malformed {
kind: "platform",
reason: format!("unexpected segment `{}` after platform id", segments[2]),
});
}
let principal = Principal::Platform { id: PlatformId(id) };
Self::wrap_delegates(principal, &segments[2..], scopes)
}
"federated" => {
if segments.len() < 5 || segments[3] != "identity" {
return Err(PrincipalParseError::Malformed {
kind: "federated",
reason: format!(
"expected `federated/<root_kind>/<root_id>/identity/<external_id>`, got `{}`",
segments.join("/")
),
});
}
let trust_root = TrustRoot::from_uri_segments(segments[1], segments[2])?;
let identity = ExternalId(percent_decode(segments[4])?);
if segments.len() > 5 && segments[5] != "delegate" {
return Err(PrincipalParseError::Malformed {
kind: "federated",
reason: format!(
"unexpected segment `{}` after federated identity",
segments[5]
),
});
}
let principal = Principal::Federated {
trust_root,
identity,
};
Self::wrap_delegates(principal, &segments[5..], scopes)
}
other => Err(PrincipalParseError::UnknownKind(other.to_owned())),
}
}
fn wrap_delegates(
mut principal: Principal,
mut remaining: &[&str],
scopes: &mut std::vec::IntoIter<AuthorityScope>,
) -> Result<Principal, PrincipalParseError> {
while !remaining.is_empty() {
if remaining[0] != "delegate" {
return Err(PrincipalParseError::Malformed {
kind: "delegate",
reason: format!("expected `delegate`, got `{}`", remaining[0]),
});
}
if remaining.len() < 2 {
return Err(PrincipalParseError::Malformed {
kind: "delegate",
reason: "missing delegate id segment".to_owned(),
});
}
let delegate_id = DelegateId(percent_decode(remaining[1])?);
let scope = scopes.next().unwrap_or_else(AuthorityScope::empty);
principal = Principal::Delegate {
authorizing: Box::new(principal),
delegate: delegate_id,
scope,
};
remaining = &remaining[2..];
}
Ok(principal)
}
}
fn percent_encode(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for &b in s.as_bytes() {
let ok = b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_' | b'~');
if ok {
out.push(b as char);
} else {
out.push('%');
out.push_str(&format!("{:02X}", b));
}
}
out
}
fn percent_decode(s: &str) -> Result<String, PrincipalParseError> {
let bytes = s.as_bytes();
let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' {
if i + 2 >= bytes.len() {
return Err(PrincipalParseError::BadPercentEncoding(s.to_owned()));
}
let hi = hex_nibble(bytes[i + 1])
.ok_or_else(|| PrincipalParseError::BadPercentEncoding(s.to_owned()))?;
let lo = hex_nibble(bytes[i + 2])
.ok_or_else(|| PrincipalParseError::BadPercentEncoding(s.to_owned()))?;
out.push((hi << 4) | lo);
i += 3;
} else {
out.push(bytes[i]);
i += 1;
}
}
String::from_utf8(out).map_err(|_| PrincipalParseError::BadPercentEncoding(s.to_owned()))
}
fn hex_nibble(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(10 + b - b'a'),
b'A'..=b'F' => Some(10 + b - b'A'),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn op(id: &str) -> Principal {
Principal::Operator {
id: OperatorId(id.to_owned()),
}
}
fn plat(id: &str) -> Principal {
Principal::Platform {
id: PlatformId(id.to_owned()),
}
}
fn fed_ado(realm: &str, ext: &str) -> Principal {
Principal::Federated {
trust_root: TrustRoot::Ado {
realm: realm.to_owned(),
},
identity: ExternalId(ext.to_owned()),
}
}
#[test]
fn operator_round_trip() {
let p = op("op-123");
let uri = p.to_source_uri();
assert_eq!(uri, "principal://operator/op-123");
assert_eq!(Principal::from_source_uri(&uri).unwrap(), p);
}
#[test]
fn platform_round_trip() {
let p = plat("hosted-ctrl-plane-prod");
let uri = p.to_source_uri();
assert_eq!(uri, "principal://platform/hosted-ctrl-plane-prod");
assert_eq!(Principal::from_source_uri(&uri).unwrap(), p);
}
#[test]
fn federated_round_trip() {
let p = fed_ado("realm-acme", "user-7842");
let uri = p.to_source_uri();
assert_eq!(
uri,
"principal://federated/ado/realm-acme/identity/user-7842"
);
assert_eq!(Principal::from_source_uri(&uri).unwrap(), p);
}
#[test]
fn federated_oidc_round_trip_with_url_issuer() {
let p = Principal::Federated {
trust_root: TrustRoot::Oidc {
issuer: "https://login.example.com/".to_owned(),
},
identity: ExternalId("user-42".to_owned()),
};
let uri = p.to_source_uri();
assert!(uri.starts_with("principal://federated/oidc/"));
assert!(uri.contains("/identity/user-42"));
assert_eq!(Principal::from_source_uri(&uri).unwrap(), p);
}
#[test]
fn federated_github_round_trip() {
let p = Principal::Federated {
trust_root: TrustRoot::GitHub {
org: "anthropic".to_owned(),
},
identity: ExternalId("octocat".to_owned()),
};
let uri = p.to_source_uri();
assert_eq!(
uri,
"principal://federated/github/anthropic/identity/octocat"
);
assert_eq!(Principal::from_source_uri(&uri).unwrap(), p);
}
#[test]
fn delegate_round_trip_one_level() {
let scope = AuthorityScope::from_capabilities([Capability::ToolApply, Capability::ToolGet]);
let p = Principal::compose(op("op-123"), DelegateId("llm-claude-456".to_owned()), scope)
.unwrap();
let uri = p.to_source_uri();
assert_eq!(
uri,
"principal://operator/op-123/delegate/llm-claude-456?scope=tool:apply,tool:get"
);
assert_eq!(Principal::from_source_uri(&uri).unwrap(), p);
}
#[test]
fn delegate_round_trip_two_levels() {
let outer = Principal::compose(
op("op-123"),
DelegateId("bridge-A".to_owned()),
AuthorityScope::from_capabilities([Capability::ToolApply, Capability::ToolGet]),
)
.unwrap();
let inner = Principal::compose(
outer,
DelegateId("bridge-B".to_owned()),
AuthorityScope::from_capabilities([Capability::ToolGet]),
)
.unwrap();
let uri = inner.to_source_uri();
assert_eq!(Principal::from_source_uri(&uri).unwrap(), inner);
}
#[test]
fn delegate_with_empty_scope_round_trips() {
let p = Principal::compose(
op("op-123"),
DelegateId("noop-bridge".to_owned()),
AuthorityScope::empty(),
)
.unwrap();
let uri = p.to_source_uri();
assert_eq!(uri, "principal://operator/op-123/delegate/noop-bridge");
assert_eq!(Principal::from_source_uri(&uri).unwrap(), p);
}
#[test]
fn compose_narrows_scope_returns_ok() {
let p = Principal::compose(
op("op-123"),
DelegateId("session-1".to_owned()),
AuthorityScope::from_capabilities([Capability::ToolApply]),
);
assert!(p.is_ok());
}
#[test]
fn compose_broadens_scope_returns_err() {
let narrow = Principal::compose(
op("op-123"),
DelegateId("session-A".to_owned()),
AuthorityScope::from_capabilities([Capability::ToolGet]),
)
.unwrap();
let err = Principal::compose(
narrow,
DelegateId("session-B".to_owned()),
AuthorityScope::from_capabilities([Capability::ToolApply]),
)
.unwrap_err();
assert_eq!(err.missing, Capability::ToolApply);
}
#[test]
fn compose_wildcard_authorizes_specific_tool() {
let wide = Principal::compose(
op("op-123"),
DelegateId("session-wild".to_owned()),
AuthorityScope::from_capabilities([Capability::ToolWildcard]),
)
.unwrap();
let narrow = Principal::compose(
wide,
DelegateId("session-narrow".to_owned()),
AuthorityScope::from_capabilities([Capability::ToolApply, Capability::ToolEvents]),
);
assert!(narrow.is_ok());
}
#[test]
fn root_operator_finds_human_at_depth() {
let inner = Principal::compose(
Principal::compose(
op("op-human"),
DelegateId("d1".to_owned()),
AuthorityScope::from_capabilities([Capability::ToolApply]),
)
.unwrap(),
DelegateId("d2".to_owned()),
AuthorityScope::from_capabilities([Capability::ToolApply]),
)
.unwrap();
assert_eq!(
inner.root_operator(),
Some(&OperatorId("op-human".to_owned()))
);
}
#[test]
fn root_operator_returns_none_for_platform() {
assert_eq!(plat("hosted-ctrl-plane-prod").root_operator(), None);
}
#[test]
fn root_operator_returns_none_for_federated() {
assert_eq!(fed_ado("realm-acme", "user-7842").root_operator(), None);
}
#[test]
fn root_operator_returns_none_for_platform_rooted_delegate() {
let p = Principal::compose(
plat("hosted-ctrl-plane-prod"),
DelegateId("compactor".to_owned()),
AuthorityScope::from_capabilities([Capability::ToolApply]),
)
.unwrap();
assert_eq!(p.root_operator(), None);
}
#[test]
fn serde_json_round_trip_operator() {
let p = op("op-123");
let json = serde_json::to_string(&p).unwrap();
let back: Principal = serde_json::from_str(&json).unwrap();
assert_eq!(back, p);
}
#[test]
fn serde_json_round_trip_delegate() {
let p = Principal::compose(
op("op-123"),
DelegateId("session-1".to_owned()),
AuthorityScope::from_capabilities([Capability::ToolApply, Capability::ToolGet]),
)
.unwrap();
let json = serde_json::to_string(&p).unwrap();
let back: Principal = serde_json::from_str(&json).unwrap();
assert_eq!(back, p);
}
#[test]
fn from_source_uri_rejects_missing_scheme() {
let err = Principal::from_source_uri("http://operator/op-123").unwrap_err();
assert!(matches!(err, PrincipalParseError::MissingScheme(_)));
}
#[test]
fn from_source_uri_rejects_unknown_kind() {
let err = Principal::from_source_uri("principal://martian/op-123").unwrap_err();
assert!(matches!(err, PrincipalParseError::UnknownKind(_)));
}
#[test]
fn from_source_uri_rejects_unknown_capability() {
let err =
Principal::from_source_uri("principal://operator/op-123/delegate/d?scope=tool:explode")
.unwrap_err();
assert!(matches!(err, PrincipalParseError::UnknownCapability(_)));
}
#[test]
fn percent_encoding_round_trips_slash_in_id() {
let p = Principal::Operator {
id: OperatorId("ns/op with space".to_owned()),
};
let uri = p.to_source_uri();
assert_eq!(Principal::from_source_uri(&uri).unwrap(), p);
}
}