use serde::{Deserialize, Serialize};
use crate::crypto::EventVerificationStatus;
use crate::model::ActorId;
use crate::session::{DelegationMap, PrincipalResolution, is_agent_actor_id};
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ArtifactAvailability {
Available,
Unavailable,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EventVerificationView {
pub verification_status: EventVerificationStatus,
pub artifact_availability: ArtifactAvailability,
}
pub fn verification_view(
verification_status: EventVerificationStatus,
artifact_availability: ArtifactAvailability,
) -> EventVerificationView {
EventVerificationView {
verification_status,
artifact_availability,
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct EventVerificationPolicy {
pub reject_invalid_signatures: bool,
pub require_trusted_signer: bool,
pub allow_unsigned: bool,
}
impl EventVerificationPolicy {
pub fn advisory() -> Self {
Self {
reject_invalid_signatures: false,
require_trusted_signer: false,
allow_unsigned: true,
}
}
pub fn integrity_strict() -> Self {
Self {
reject_invalid_signatures: true,
require_trusted_signer: false,
allow_unsigned: true,
}
}
pub fn trusted_strict() -> Self {
Self {
reject_invalid_signatures: true,
require_trusted_signer: true,
allow_unsigned: false,
}
}
pub fn with_allow_unsigned(mut self, allow_unsigned: bool) -> Self {
self.allow_unsigned = allow_unsigned;
self
}
pub fn rejects(&self, status: EventVerificationStatus) -> bool {
match status {
EventVerificationStatus::Valid => false,
EventVerificationStatus::Invalid => self.reject_invalid_signatures,
EventVerificationStatus::UntrustedKey => self.require_trusted_signer,
EventVerificationStatus::Unsigned => !self.allow_unsigned,
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum PrincipalPolicy {
#[default]
None,
Prefer,
RequireResolvablePrincipal,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum RemovalPolicy {
Advisory,
#[default]
PossessionOrTrusted,
TrustedStrict,
}
pub fn principal_sufficient(
writer_actor: &ActorId,
occurred_at: &str,
delegation_map: Option<&DelegationMap>,
policy: PrincipalPolicy,
) -> bool {
match policy {
PrincipalPolicy::None | PrincipalPolicy::Prefer => true,
PrincipalPolicy::RequireResolvablePrincipal => {
if !is_agent_actor_id(writer_actor.as_str()) {
return true;
}
matches!(
delegation_map.map(|map| map.resolve(writer_actor, occurred_at)),
Some(PrincipalResolution::Resolved(ref principal))
if !is_agent_actor_id(principal.as_str())
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::ActorId;
use crate::session::delegation_map_from_value;
const AGENT: &str = "actor:agent:claude-code";
const KEVIN: &str = "actor:git-email:kevin@swiber.dev";
const ALICE: &str = "actor:git-email:alice@example.com";
const DID_KEY: &str = "did:key:z6MkehRgf7yJbgaGfYsdoAsKdBPE3dj2CYhowQdcjqSJgvVd";
const AT: &str = "2026-06-11T00:00:00Z";
fn actor(id: &str) -> ActorId {
ActorId::new(id)
}
fn resolving_map() -> DelegationMap {
delegation_map_from_value(serde_json::json!({
"delegates": { AGENT: [
{ "principal": KEVIN, "validFrom": "2026-06-01T00:00:00Z", "validUntil": null }
]}
}))
.unwrap()
}
fn ambiguous_map() -> DelegationMap {
delegation_map_from_value(serde_json::json!({
"delegates": { AGENT: [
{ "principal": KEVIN, "validFrom": "2026-06-01T00:00:00Z", "validUntil": null },
{ "principal": ALICE, "validFrom": "2026-06-01T00:00:00Z", "validUntil": null }
]}
}))
.unwrap()
}
#[test]
fn default_principal_policy_is_none() {
assert_eq!(PrincipalPolicy::default(), PrincipalPolicy::None);
}
#[test]
fn default_removal_policy_is_possession_or_trusted() {
assert_eq!(RemovalPolicy::default(), RemovalPolicy::PossessionOrTrusted);
}
#[test]
fn none_and_prefer_are_always_sufficient() {
let map = resolving_map();
for policy in [PrincipalPolicy::None, PrincipalPolicy::Prefer] {
for writer in [AGENT, KEVIN, DID_KEY] {
assert!(
principal_sufficient(&actor(writer), AT, Some(&map), policy),
"{writer} under {policy:?} must be sufficient"
);
assert!(principal_sufficient(&actor(writer), AT, None, policy));
}
}
}
#[test]
fn require_resolvable_principal_truth_table() {
let require = PrincipalPolicy::RequireResolvablePrincipal;
let resolving = resolving_map();
assert!(principal_sufficient(
&actor(KEVIN),
AT,
Some(&resolving),
require
));
assert!(principal_sufficient(
&actor(DID_KEY),
AT,
Some(&resolving),
require
));
assert!(principal_sufficient(
&actor(AGENT),
AT,
Some(&resolving),
require
));
let unknown = delegation_map_from_value(serde_json::json!({ "delegates": {} })).unwrap();
assert!(!principal_sufficient(
&actor(AGENT),
AT,
Some(&unknown),
require
));
assert!(!principal_sufficient(&actor(AGENT), AT, None, require));
assert!(!principal_sufficient(
&actor(AGENT),
AT,
Some(&ambiguous_map()),
require
));
}
}