use std::ops::ControlFlow;
use std::sync::Arc;
use auths_core::storage::keychain::{IdentityDID, KeyAlias, extract_public_key_bytes};
use auths_id::keri::Event;
use auths_id::keri::delegation::{
DelegatedRole, incept_delegated_device, list_delegated_devices, mark_agent_scope,
mark_delegated_agent, read_agent_scope, revoke_delegated_device,
revoke_delegated_devices_batch, rotate_delegated_device,
};
use auths_id::keri::parse_did_keri;
use auths_keri::AgentScope;
use crate::context::AuthsContext;
use crate::domains::agents::error::AgentError;
use crate::domains::agents::scope::{
DelegatorScope, RequestedScope, validate_delegation_constraints,
};
#[derive(Debug, Clone)]
pub struct AgentDelegationResult {
pub agent_did: String,
pub agent_prefix: String,
}
pub fn add(
ctx: &AuthsContext,
root_alias: &KeyAlias,
agent_alias: &KeyAlias,
agent_curve: auths_crypto::CurveType,
) -> Result<AgentDelegationResult, AgentError> {
add_scoped(ctx, root_alias, agent_alias, agent_curve, &[], None)
}
pub fn add_scoped(
ctx: &AuthsContext,
root_alias: &KeyAlias,
agent_alias: &KeyAlias,
agent_curve: auths_crypto::CurveType,
scope: &[String],
expires_at: Option<i64>,
) -> Result<AgentDelegationResult, AgentError> {
if ctx.key_storage.load_key(agent_alias).is_ok() {
return Err(AgentError::AlreadyDelegated {
alias: agent_alias.as_str().to_string(),
});
}
let managed =
ctx.identity_storage
.load_identity()
.map_err(|e| AgentError::IdentityNotFound {
did: format!("identity load failed: {e}"),
})?;
let root_prefix = parse_did_keri(managed.controller_did.as_str()).map_err(|e| {
AgentError::IdentityNotFound {
did: format!("invalid root did:keri: {e}"),
}
})?;
let (_pk, root_curve) = extract_public_key_bytes(
ctx.key_storage.as_ref(),
root_alias,
ctx.passphrase_provider.as_ref(),
)
.map_err(AgentError::CryptoError)?;
enforce_scope_subset(ctx, &root_prefix, scope)?;
let agent = incept_delegated_device(
Arc::clone(&ctx.registry),
&root_prefix,
root_alias,
root_curve,
agent_alias,
agent_curve,
ctx.passphrase_provider.as_ref(),
ctx.key_storage.as_ref(),
)
.map_err(AgentError::DelegationError)?;
mark_delegated_agent(
ctx.registry.as_ref(),
&root_prefix,
root_alias,
root_curve,
&agent.device_prefix,
ctx.passphrase_provider.as_ref(),
ctx.key_storage.as_ref(),
)
.map_err(AgentError::DelegationError)?;
if !scope.is_empty() || expires_at.is_some() {
mark_agent_scope(
ctx.registry.as_ref(),
&root_prefix,
root_alias,
root_curve,
&agent.device_prefix,
&AgentScope {
capabilities: scope.to_vec(),
expires_at,
},
ctx.passphrase_provider.as_ref(),
ctx.key_storage.as_ref(),
)
.map_err(AgentError::DelegationError)?;
}
Ok(AgentDelegationResult {
agent_did: agent.device_did.as_str().to_string(),
agent_prefix: agent.device_prefix.as_str().to_string(),
})
}
fn collect_kel(ctx: &AuthsContext, prefix: &auths_id::keri::types::Prefix) -> Vec<Event> {
let mut events = Vec::new();
let _ = ctx.registry.visit_events(prefix, 0, &mut |e| {
events.push(e.clone());
ControlFlow::Continue(())
});
events
}
fn enforce_scope_subset(
ctx: &AuthsContext,
root_prefix: &auths_id::keri::types::Prefix,
requested: &[String],
) -> Result<(), AgentError> {
if requested.is_empty() {
return Ok(());
}
let root_kel = collect_kel(ctx, root_prefix);
let Some(delegator_scope) = read_agent_scope(&root_kel, root_prefix) else {
return Ok(()); };
validate_delegation_constraints(
&DelegatorScope {
capabilities: &delegator_scope.capabilities,
remaining_ttl_secs: u64::MAX,
depth: 0,
max_depth: u32::MAX,
},
&RequestedScope {
capabilities: requested,
ttl_secs: 0,
},
)
.map_err(|e| match e {
crate::domains::agents::scope::DelegationError::CapabilityNotGranted(cap) => {
AgentError::OutsideDelegatorScope { capability: cap }
}
other => AgentError::OutsideDelegatorScope {
capability: other.to_string(),
},
})
}
#[derive(Debug, Clone)]
pub struct AgentInfo {
pub agent_did: String,
pub revoked: bool,
}
pub fn list(ctx: &AuthsContext) -> Result<Vec<AgentInfo>, AgentError> {
let managed =
ctx.identity_storage
.load_identity()
.map_err(|e| AgentError::IdentityNotFound {
did: format!("identity load failed: {e}"),
})?;
let root_prefix = parse_did_keri(managed.controller_did.as_str()).map_err(|e| {
AgentError::IdentityNotFound {
did: format!("invalid root did:keri: {e}"),
}
})?;
let delegated = list_delegated_devices(ctx.registry.as_ref(), &root_prefix)
.map_err(AgentError::DelegationError)?;
Ok(delegated
.into_iter()
.filter(|d| d.role == DelegatedRole::Agent)
.map(|d| AgentInfo {
agent_did: format!("did:keri:{}", d.device_prefix),
revoked: d.revoked,
})
.collect())
}
pub fn revoke(
ctx: &AuthsContext,
root_alias: &KeyAlias,
agent_did: &str,
) -> Result<(), AgentError> {
let managed =
ctx.identity_storage
.load_identity()
.map_err(|e| AgentError::IdentityNotFound {
did: format!("identity load failed: {e}"),
})?;
let root_prefix = parse_did_keri(managed.controller_did.as_str()).map_err(|e| {
AgentError::IdentityNotFound {
did: format!("invalid root did:keri: {e}"),
}
})?;
let agent_prefix = parse_did_keri(agent_did).map_err(|_| AgentError::AgentNotFound {
did: agent_did.to_string(),
})?;
let (_pk, root_curve) = extract_public_key_bytes(
ctx.key_storage.as_ref(),
root_alias,
ctx.passphrase_provider.as_ref(),
)
.map_err(AgentError::CryptoError)?;
revoke_delegated_device(
ctx.registry.as_ref(),
&root_prefix,
root_alias,
root_curve,
&agent_prefix,
ctx.passphrase_provider.as_ref(),
ctx.key_storage.as_ref(),
)
.map_err(AgentError::DelegationError)
}
#[derive(Debug, Clone)]
pub struct BatchRevocation {
pub revoked: Vec<String>,
pub anchored_at_seq: Option<u128>,
}
pub fn revoke_batch(
ctx: &AuthsContext,
root_alias: &KeyAlias,
agent_dids: &[String],
) -> Result<BatchRevocation, AgentError> {
let managed =
ctx.identity_storage
.load_identity()
.map_err(|e| AgentError::IdentityNotFound {
did: format!("identity load failed: {e}"),
})?;
let root_prefix = parse_did_keri(managed.controller_did.as_str()).map_err(|e| {
AgentError::IdentityNotFound {
did: format!("invalid root did:keri: {e}"),
}
})?;
let mut prefixes = Vec::with_capacity(agent_dids.len());
for did in agent_dids {
prefixes
.push(parse_did_keri(did).map_err(|_| AgentError::AgentNotFound { did: did.clone() })?);
}
let (_pk, root_curve) = extract_public_key_bytes(
ctx.key_storage.as_ref(),
root_alias,
ctx.passphrase_provider.as_ref(),
)
.map_err(AgentError::CryptoError)?;
let (_newly, ixn) = revoke_delegated_devices_batch(
ctx.registry.as_ref(),
&root_prefix,
root_alias,
root_curve,
&prefixes,
ctx.passphrase_provider.as_ref(),
ctx.key_storage.as_ref(),
)
.map_err(AgentError::DelegationError)?;
Ok(BatchRevocation {
revoked: agent_dids.to_vec(),
anchored_at_seq: ixn.map(|e| e.s.value()),
})
}
pub fn rotate(
ctx: &AuthsContext,
root_alias: &KeyAlias,
agent_did: &str,
) -> Result<(), AgentError> {
let managed =
ctx.identity_storage
.load_identity()
.map_err(|e| AgentError::IdentityNotFound {
did: format!("identity load failed: {e}"),
})?;
let root_prefix = parse_did_keri(managed.controller_did.as_str()).map_err(|e| {
AgentError::IdentityNotFound {
did: format!("invalid root did:keri: {e}"),
}
})?;
let agent_prefix = parse_did_keri(agent_did).map_err(|_| AgentError::AgentNotFound {
did: agent_did.to_string(),
})?;
let delegated = list_delegated_devices(ctx.registry.as_ref(), &root_prefix)
.map_err(AgentError::DelegationError)?;
let info = delegated
.iter()
.find(|d| d.device_prefix.as_str() == agent_prefix.as_str())
.ok_or_else(|| AgentError::AgentNotFound {
did: agent_did.to_string(),
})?;
if info.revoked {
return Err(AgentError::Revoked {
did: agent_did.to_string(),
});
}
#[allow(clippy::disallowed_methods)]
let agent_did_typed = IdentityDID::new_unchecked(agent_did.to_string());
let agent_alias = ctx
.key_storage
.list_aliases_for_identity(&agent_did_typed)
.map_err(AgentError::CryptoError)?
.into_iter()
.find(|a| !a.as_str().contains("--next-"))
.ok_or_else(|| AgentError::AgentNotFound {
did: format!("no local key for agent {agent_did}"),
})?;
let (_pk, agent_curve) = extract_public_key_bytes(
ctx.key_storage.as_ref(),
&agent_alias,
ctx.passphrase_provider.as_ref(),
)
.map_err(AgentError::CryptoError)?;
let (_pk, root_curve) = extract_public_key_bytes(
ctx.key_storage.as_ref(),
root_alias,
ctx.passphrase_provider.as_ref(),
)
.map_err(AgentError::CryptoError)?;
rotate_delegated_device(
ctx.registry.as_ref(),
&root_prefix,
root_alias,
root_curve,
&agent_prefix,
&agent_alias,
agent_curve,
ctx.passphrase_provider.as_ref(),
ctx.key_storage.as_ref(),
)
.map_err(AgentError::DelegationError)
}