use serde::Serialize;
use super::delegates::{DelegationMap, PrincipalResolution};
use super::writer::is_agent_actor_id;
use crate::model::ActorId;
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PrincipalView {
#[serde(skip_serializing_if = "Option::is_none")]
pub actor_id: Option<ActorId>,
pub status: PrincipalStatus,
pub source: PrincipalSource,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PrincipalStatus {
Resolved,
None,
Ambiguous,
Disavowed,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PrincipalSource {
Delegates,
None,
}
pub fn principal_view_for(
writer_actor: &ActorId,
map: Option<&DelegationMap>,
occurred_at: &str,
) -> Option<PrincipalView> {
if !is_agent_actor_id(writer_actor.as_str()) {
return None;
}
let Some(map) = map else {
return Some(PrincipalView {
actor_id: Option::None,
status: PrincipalStatus::None,
source: PrincipalSource::None,
});
};
let view = match map.resolve(writer_actor, occurred_at) {
PrincipalResolution::Resolved(principal) => PrincipalView {
actor_id: Some(principal),
status: PrincipalStatus::Resolved,
source: PrincipalSource::Delegates,
},
PrincipalResolution::None(_) => PrincipalView {
actor_id: Option::None,
status: PrincipalStatus::None,
source: PrincipalSource::Delegates,
},
PrincipalResolution::Ambiguous(_) => PrincipalView {
actor_id: Option::None,
status: PrincipalStatus::Ambiguous,
source: PrincipalSource::Delegates,
},
};
Some(view)
}
pub fn principal_resolution_for_writer(
writer_actor: &ActorId,
map: &DelegationMap,
occurred_at: &str,
) -> Option<PrincipalResolution> {
is_agent_actor_id(writer_actor.as_str()).then(|| map.resolve(writer_actor, occurred_at))
}
pub fn principal_display_label(writer_actor: &ActorId, principal: &PrincipalView) -> String {
let agent_name = writer_actor
.as_str()
.strip_prefix("actor:agent:")
.unwrap_or_else(|| writer_actor.as_str());
match (&principal.actor_id, principal.status) {
(Some(actor_id), PrincipalStatus::Resolved) => {
format!(
"{agent_name} (for {})",
principal_display_name(actor_id.as_str())
)
}
_ => agent_name.to_owned(),
}
}
fn principal_display_name(actor_id: &str) -> &str {
actor_id
.strip_prefix("actor:git-email:")
.or_else(|| actor_id.strip_prefix("actor:git-name:"))
.unwrap_or(actor_id)
}
#[cfg(test)]
mod tests {
use serde_json::{json, to_value};
use super::*;
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";
fn actor(id: &str) -> ActorId {
ActorId::new(id)
}
fn map_with(records: serde_json::Value) -> DelegationMap {
delegation_map_from_value(json!({ "delegates": { AGENT: records } })).unwrap()
}
fn open_window(principal: &str) -> serde_json::Value {
json!([{ "principal": principal, "validFrom": "2026-06-10T00:00:00Z", "validUntil": null }])
}
#[test]
fn principal_view_serializes_resolved_shape() {
let map = map_with(open_window(KEVIN));
let view = principal_view_for(&actor(AGENT), Some(&map), "2026-06-11T12:00:00Z")
.expect("agent-scheme writer gets a principal object");
assert_eq!(
to_value(&view).unwrap(),
json!({ "actorId": KEVIN, "status": "resolved", "source": "delegates" })
);
}
#[test]
fn non_agent_writers_get_no_principal_object() {
let map = map_with(open_window(KEVIN));
for non_agent in [KEVIN, "actor:git-name:Kevin Swiber", "actor:local", DID_KEY] {
assert!(
principal_view_for(&actor(non_agent), Some(&map), "2026-06-11T12:00:00Z").is_none(),
"{non_agent} is its own principal"
);
}
}
#[test]
fn missing_map_degrades_to_none_status() {
let view = principal_view_for(&actor(AGENT), None, "2026-06-11T12:00:00Z").unwrap();
assert_eq!(
to_value(&view).unwrap(),
json!({ "status": "none", "source": "none" })
);
}
#[test]
fn no_covering_window_serializes_status_none_with_delegates_source() {
let map = map_with(json!([{
"principal": KEVIN,
"validFrom": "2026-06-10T00:00:00Z",
"validUntil": "2026-06-12T00:00:00Z"
}]));
let view = principal_view_for(&actor(AGENT), Some(&map), "2026-07-01T00:00:00Z").unwrap();
assert_eq!(
to_value(&view).unwrap(),
json!({ "status": "none", "source": "delegates" })
);
}
#[test]
fn ambiguous_resolution_serializes_status_ambiguous() {
let map = map_with(json!([
{ "principal": KEVIN, "validFrom": "2026-06-10T00:00:00Z", "validUntil": null },
{ "principal": ALICE, "validFrom": "2026-06-15T00:00:00Z", "validUntil": null }
]));
let view = principal_view_for(&actor(AGENT), Some(&map), "2026-06-16T00:00:00Z").unwrap();
assert_eq!(
to_value(&view).unwrap(),
json!({ "status": "ambiguous", "source": "delegates" })
);
}
#[test]
fn display_label_renders_agent_for_principal() {
let map = map_with(open_window(KEVIN));
let view = principal_view_for(&actor(AGENT), Some(&map), "2026-06-11T12:00:00Z").unwrap();
assert_eq!(
principal_display_label(&actor(AGENT), &view),
"claude-code (for kevin@swiber.dev)"
);
let map = map_with(open_window("actor:git-name:Kevin Swiber"));
let view = principal_view_for(&actor(AGENT), Some(&map), "2026-06-11T12:00:00Z").unwrap();
assert_eq!(
principal_display_label(&actor(AGENT), &view),
"claude-code (for Kevin Swiber)"
);
let map = map_with(open_window(DID_KEY));
let view = principal_view_for(&actor(AGENT), Some(&map), "2026-06-11T12:00:00Z").unwrap();
assert_eq!(
principal_display_label(&actor(AGENT), &view),
format!("claude-code (for {DID_KEY})")
);
let view = principal_view_for(&actor(AGENT), None, "2026-06-11T12:00:00Z").unwrap();
assert_eq!(principal_display_label(&actor(AGENT), &view), "claude-code");
}
}