use std::collections::BTreeMap;
use std::path::Path;
use serde_json::{Map, Value};
use super::instant::{parse_event_instant, parse_rfc3339_utc_millis};
use super::writer::{is_agent_actor_id, is_valid_principal_actor_id};
use crate::error::{Result, ShoreError};
use crate::model::ActorId;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct ValidityWindow {
from_ms: i64,
until_ms: Option<i64>,
}
impl ValidityWindow {
fn contains(&self, instant: i64) -> bool {
self.from_ms <= instant && self.until_ms.is_none_or(|until| instant < until)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PrincipalResolution {
Resolved(ActorId),
None(UnresolvedReason),
Ambiguous(Vec<ActorId>),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum UnresolvedReason {
NoDelegationRecord,
NoCoveringWindow,
UnparseableTimestamp,
}
impl UnresolvedReason {
pub fn as_str(self) -> &'static str {
match self {
UnresolvedReason::NoDelegationRecord => "no_delegation_record",
UnresolvedReason::NoCoveringWindow => "no_covering_window",
UnresolvedReason::UnparseableTimestamp => "unparseable_timestamp",
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct DelegationMap {
delegates: BTreeMap<ActorId, Vec<DelegationRecord>>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DelegationRecord {
principal: ActorId,
window: ValidityWindow,
}
impl DelegationMap {
pub fn from_delegates_file(path: impl AsRef<Path>) -> Result<Self> {
let bytes =
std::fs::read(path.as_ref()).map_err(|error| ShoreError::WorkflowInputInvalid {
reason: format!(
"failed to read delegates file {}: {error}",
path.as_ref().display()
),
})?;
delegation_map_from_value(serde_json::from_slice(&bytes)?)
}
pub fn is_empty(&self) -> bool {
self.delegates.is_empty()
}
pub fn with_local_override(mut self, local: DelegationMap) -> DelegationMap {
for (agent, records) in local.delegates {
self.delegates.insert(agent, records);
}
self
}
pub(crate) fn records_for(&self, actor: &ActorId) -> &[DelegationRecord] {
self.delegates.get(actor).map(Vec::as_slice).unwrap_or(&[])
}
pub fn record_count_for(&self, actor: &ActorId) -> usize {
self.records_for(actor).len()
}
pub fn resolve(&self, actor: &ActorId, occurred_at: &str) -> PrincipalResolution {
let Some(instant) = parse_event_instant(occurred_at) else {
return PrincipalResolution::None(UnresolvedReason::UnparseableTimestamp);
};
let records = self.records_for(actor);
if records.is_empty() {
return PrincipalResolution::None(UnresolvedReason::NoDelegationRecord);
}
let mut principals: Vec<ActorId> = records
.iter()
.filter(|record| record.window.contains(instant))
.map(|record| record.principal.clone())
.collect();
if principals.is_empty() {
return PrincipalResolution::None(UnresolvedReason::NoCoveringWindow);
}
principals.sort();
principals.dedup();
if principals.len() == 1 {
PrincipalResolution::Resolved(principals.into_iter().next().expect("len checked"))
} else {
PrincipalResolution::Ambiguous(principals)
}
}
}
pub fn delegation_map_from_value(value: Value) -> Result<DelegationMap> {
let delegates = value
.get("delegates")
.and_then(Value::as_object)
.ok_or_else(|| invalid_delegation_map("missing delegates object"))?;
let mut parsed = BTreeMap::new();
for (actor, records) in delegates {
if !is_agent_actor_id(actor) {
return Err(invalid_delegation_map(format!(
"delegate key {actor} must be an actor:agent:<name> id"
)));
}
let records = records.as_array().ok_or_else(|| {
invalid_delegation_map(format!("delegation records for {actor} must be an array"))
})?;
let mut parsed_records = Vec::with_capacity(records.len());
for record in records {
parsed_records.push(parse_record(actor, record)?);
}
parsed.insert(ActorId::new(actor), parsed_records);
}
Ok(DelegationMap { delegates: parsed })
}
pub const DELEGATES_REL_PATH: &str = ".shore/delegates.json";
pub const DELEGATES_LOCAL_REL_PATH: &str = ".shore/delegates.local.json";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DelegationStageOutcome {
pub added: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DelegationWriteRecord {
principal: ActorId,
valid_from: String,
valid_until: Option<String>,
comment: Option<String>,
}
impl DelegationWriteRecord {
pub fn new(principal: ActorId, valid_from: String) -> Self {
Self {
principal,
valid_from,
valid_until: None,
comment: None,
}
}
pub fn with_valid_until(mut self, until: Option<String>) -> Self {
self.valid_until = until;
self
}
pub fn with_comment(mut self, comment: Option<String>) -> Self {
self.comment = comment;
self
}
}
pub fn stage_delegation(
path: &Path,
agent: &ActorId,
record: &DelegationWriteRecord,
) -> Result<DelegationStageOutcome> {
if !is_agent_actor_id(agent.as_str()) {
return Err(invalid_delegation_map(format!(
"delegate key {} must be an actor:agent:<name> id",
agent.as_str()
)));
}
if !is_valid_principal_actor_id(record.principal.as_str()) {
return Err(invalid_delegation_map(format!(
"principal {} is not a valid actor id",
record.principal.as_str()
)));
}
if is_agent_actor_id(record.principal.as_str()) {
return Err(invalid_delegation_map(format!(
"principal {} must be a non-agent actor id in v1",
record.principal.as_str()
)));
}
if parse_rfc3339_utc_millis(&record.valid_from).is_none() {
return Err(invalid_delegation_map(format!(
"validFrom {} is not an RFC 3339 UTC instant",
record.valid_from
)));
}
if let Some(until) = &record.valid_until
&& parse_rfc3339_utc_millis(until).is_none()
{
return Err(invalid_delegation_map(format!(
"validUntil {until} is not an RFC 3339 UTC instant"
)));
}
let mut root: Value = if path.exists() {
serde_json::from_slice(
&std::fs::read(path)
.map_err(|e| ShoreError::Message(format!("read {}: {e}", path.display())))?,
)?
} else {
Value::Object(Map::new())
};
let root_obj = root
.as_object_mut()
.ok_or_else(|| invalid_delegation_map("delegates file is not an object"))?;
let delegates = root_obj
.entry("delegates".to_owned())
.or_insert_with(|| Value::Object(Map::new()))
.as_object_mut()
.ok_or_else(|| invalid_delegation_map("delegates is not an object"))?;
let array = delegates
.entry(agent.as_str().to_owned())
.or_insert_with(|| Value::Array(Vec::new()))
.as_array_mut()
.ok_or_else(|| invalid_delegation_map("agent records must be an array"))?;
let new_record = delegation_record_value(record);
let added = !array.iter().any(|existing| existing == &new_record);
if added {
array.push(new_record);
}
delegation_map_from_value(root.clone())?;
write_delegates(path, &root)?;
Ok(DelegationStageOutcome { added })
}
fn delegation_record_value(record: &DelegationWriteRecord) -> Value {
let mut obj = Map::new();
obj.insert(
"principal".to_owned(),
Value::String(record.principal.as_str().to_owned()),
);
obj.insert(
"validFrom".to_owned(),
Value::String(record.valid_from.clone()),
);
obj.insert(
"validUntil".to_owned(),
match &record.valid_until {
Some(s) => Value::String(s.clone()),
None => Value::Null,
},
);
if let Some(comment) = &record.comment {
obj.insert("comment".to_owned(), Value::String(comment.clone()));
}
Value::Object(obj)
}
fn write_delegates(path: &Path, root: &Value) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| ShoreError::Message(format!("create {}: {e}", parent.display())))?;
}
let mut bytes = serde_json::to_vec_pretty(root)?;
bytes.push(b'\n');
std::fs::write(path, &bytes)
.map_err(|e| ShoreError::Message(format!("write {}: {e}", path.display())))
}
fn parse_record(actor: &str, record: &Value) -> Result<DelegationRecord> {
let record = record.as_object().ok_or_else(|| {
invalid_delegation_map(format!("delegation record for {actor} must be an object"))
})?;
let principal = record
.get("principal")
.and_then(Value::as_str)
.ok_or_else(|| {
invalid_delegation_map(format!(
"delegation record for {actor} is missing principal"
))
})?;
if !is_valid_principal_actor_id(principal) {
return Err(invalid_delegation_map(format!(
"principal {principal} for {actor} is not a valid actor id"
)));
}
if is_agent_actor_id(principal) {
return Err(invalid_delegation_map(format!(
"principal {principal} for {actor} must be a non-agent actor id in v1"
)));
}
let from_ms = required_instant(actor, record, "validFrom")?;
let until_ms = optional_instant(actor, record, "validUntil")?;
match record.get("comment") {
None | Some(Value::Null) | Some(Value::String(_)) => {}
Some(_) => {
return Err(invalid_delegation_map(format!(
"comment for {actor} must be a string"
)));
}
}
Ok(DelegationRecord {
principal: ActorId::new(principal),
window: ValidityWindow { from_ms, until_ms },
})
}
fn required_instant(
actor: &str,
record: &serde_json::Map<String, Value>,
field: &str,
) -> Result<i64> {
let value = record.get(field).and_then(Value::as_str).ok_or_else(|| {
invalid_delegation_map(format!("delegation record for {actor} is missing {field}"))
})?;
parse_rfc3339_utc_millis(value).ok_or_else(|| {
invalid_delegation_map(format!(
"{field} {value} for {actor} is not an RFC 3339 UTC instant"
))
})
}
fn optional_instant(
actor: &str,
record: &serde_json::Map<String, Value>,
field: &str,
) -> Result<Option<i64>> {
match record.get(field) {
None | Some(Value::Null) => Ok(None),
Some(Value::String(value)) => parse_rfc3339_utc_millis(value).map(Some).ok_or_else(|| {
invalid_delegation_map(format!(
"{field} {value} for {actor} is not an RFC 3339 UTC instant"
))
}),
Some(_) => Err(invalid_delegation_map(format!(
"{field} for {actor} must be a string or null"
))),
}
}
fn invalid_delegation_map(reason: impl Into<String>) -> ShoreError {
ShoreError::WorkflowInputInvalid {
reason: format!("invalid delegation map: {}", reason.into()),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn actor(id: &str) -> ActorId {
ActorId::new(id)
}
#[test]
fn parses_delegates_file_shape() {
let map = delegation_map_from_value(serde_json::json!({
"delegates": {
"actor:agent:claude-code": [{
"principal": "actor:git-email:kevin@swiber.dev",
"validFrom": "2026-06-10T00:00:00Z",
"validUntil": null,
"comment": "claude-code, enrolled by Kevin"
}]
},
"futureTopLevelKey": {"ignored": true}
}))
.unwrap();
let agent = actor("actor:agent:claude-code");
let records = map.records_for(&agent);
assert_eq!(records.len(), 1);
assert_eq!(
records[0].principal,
actor("actor:git-email:kevin@swiber.dev")
);
assert!(!map.is_empty());
assert_eq!(
map.resolve(&agent, "2026-06-11T00:00:00Z"),
PrincipalResolution::Resolved(actor("actor:git-email:kevin@swiber.dev"))
);
}
#[test]
fn rejects_missing_delegates_key() {
let err = delegation_map_from_value(serde_json::json!({ "notDelegates": {} }))
.expect_err("missing delegates key must be rejected");
let message = err.to_string();
assert!(
message.contains("delegates"),
"error names the missing key; got: {message}"
);
}
#[test]
fn rejects_agent_scheme_principal_in_v1() {
let err = delegation_map_from_value(serde_json::json!({
"delegates": {
"actor:agent:claude-code": [{
"principal": "actor:agent:subagent",
"validFrom": "2026-06-10T00:00:00Z",
"validUntil": null
}]
}
}))
.expect_err("agent-scheme principal must be rejected in v1");
let message = err.to_string();
assert!(
message.contains("actor:agent:claude-code"),
"error names the offending agent id; got: {message}"
);
}
#[test]
fn rejects_invalid_principal_actor_id() {
let err = delegation_map_from_value(serde_json::json!({
"delegates": {
"actor:agent:claude-code": [{
"principal": "not-an-actor-id",
"validFrom": "2026-06-10T00:00:00Z",
"validUntil": null
}]
}
}))
.expect_err("malformed principal must be rejected");
assert!(err.to_string().contains("actor:agent:claude-code"));
}
#[test]
fn rejects_missing_valid_from() {
let err = delegation_map_from_value(serde_json::json!({
"delegates": {
"actor:agent:claude-code": [{
"principal": "actor:git-email:kevin@swiber.dev",
"validUntil": null
}]
}
}))
.expect_err("missing validFrom must be rejected");
assert!(err.to_string().contains("validFrom"));
}
#[test]
fn rejects_non_rfc3339_valid_from() {
let err = delegation_map_from_value(serde_json::json!({
"delegates": {
"actor:agent:claude-code": [{
"principal": "actor:git-email:kevin@swiber.dev",
"validFrom": "yesterday",
"validUntil": null
}]
}
}))
.expect_err("non-RFC-3339 validFrom must be rejected");
assert!(err.to_string().contains("validFrom"));
}
#[test]
fn rejects_non_rfc3339_valid_until() {
let err = delegation_map_from_value(serde_json::json!({
"delegates": {
"actor:agent:claude-code": [{
"principal": "actor:git-email:kevin@swiber.dev",
"validFrom": "2026-06-10T00:00:00Z",
"validUntil": "later"
}]
}
}))
.expect_err("non-RFC-3339 validUntil must be rejected");
assert!(err.to_string().contains("validUntil"));
}
#[test]
fn accepts_open_window_and_optional_comment() {
let map = delegation_map_from_value(serde_json::json!({
"delegates": {
"actor:agent:claude-code": [{
"principal": "actor:git-email:kevin@swiber.dev",
"validFrom": "2026-06-10T00:00:00Z",
"validUntil": null
}]
}
}))
.unwrap();
assert_eq!(
map.resolve(&actor("actor:agent:claude-code"), "2099-01-01T00:00:00Z"),
PrincipalResolution::Resolved(actor("actor:git-email:kevin@swiber.dev"))
);
}
#[test]
fn accepts_git_name_principal_with_internal_space() {
let map = map_with(serde_json::json!([{
"principal": "actor:git-name:Kevin Swiber",
"validFrom": "2026-06-10T00:00:00Z",
"validUntil": null
}]));
let records = map.records_for(&actor(AGENT));
assert_eq!(records[0].principal, actor("actor:git-name:Kevin Swiber"));
}
#[test]
fn rejects_non_agent_delegate_key() {
let err = delegation_map_from_value(serde_json::json!({
"delegates": {
"actor:git-email:kevin@swiber.dev": [{
"principal": "actor:git-email:kevin@swiber.dev",
"validFrom": "2026-06-10T00:00:00Z",
"validUntil": null
}]
}
}))
.expect_err("a non-agent delegate key must be rejected");
assert!(err.to_string().contains("actor:git-email:kevin@swiber.dev"));
}
fn map_with(records: Value) -> DelegationMap {
delegation_map_from_value(serde_json::json!({
"delegates": { "actor:agent:claude-code": records }
}))
.unwrap()
}
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";
#[test]
fn resolves_principal_inside_open_window() {
let map = map_with(serde_json::json!([{
"principal": KEVIN, "validFrom": "2026-06-10T00:00:00Z", "validUntil": null
}]));
assert_eq!(
map.resolve(&actor(AGENT), "2026-06-11T12:00:00Z"),
PrincipalResolution::Resolved(actor(KEVIN))
);
}
#[test]
fn unix_ms_event_timestamp_resolves_against_rfc3339_window() {
let map = map_with(serde_json::json!([{
"principal": KEVIN, "validFrom": "2026-06-10T00:00:00Z", "validUntil": null
}]));
assert_eq!(
map.resolve(&actor(AGENT), "unix-ms:1781136000000"),
PrincipalResolution::Resolved(actor(KEVIN))
);
}
#[test]
fn window_boundaries_are_half_open() {
let map = map_with(serde_json::json!([{
"principal": KEVIN,
"validFrom": "2026-06-10T00:00:00Z",
"validUntil": "2026-06-20T00:00:00Z"
}]));
assert_eq!(
map.resolve(&actor(AGENT), "2026-06-10T00:00:00Z"),
PrincipalResolution::Resolved(actor(KEVIN))
);
assert_eq!(
map.resolve(&actor(AGENT), "2026-06-20T00:00:00Z"),
PrincipalResolution::None(UnresolvedReason::NoCoveringWindow)
);
}
#[test]
fn closed_window_keeps_resolving_history_and_rejects_later_events() {
let map = map_with(serde_json::json!([{
"principal": KEVIN,
"validFrom": "2026-06-10T00:00:00Z",
"validUntil": "2026-06-20T00:00:00Z"
}]));
assert_eq!(
map.resolve(&actor(AGENT), "2026-06-15T00:00:00Z"),
PrincipalResolution::Resolved(actor(KEVIN))
);
assert_eq!(
map.resolve(&actor(AGENT), "2026-06-25T00:00:00Z"),
PrincipalResolution::None(UnresolvedReason::NoCoveringWindow)
);
}
#[test]
fn unknown_agent_resolves_none_no_delegation_record() {
let map = map_with(serde_json::json!([{
"principal": KEVIN, "validFrom": "2026-06-10T00:00:00Z", "validUntil": null
}]));
assert_eq!(
map.resolve(&actor("actor:agent:other"), "2026-06-11T00:00:00Z"),
PrincipalResolution::None(UnresolvedReason::NoDelegationRecord)
);
}
#[test]
fn overlapping_windows_with_distinct_principals_are_ambiguous() {
let map = map_with(serde_json::json!([
{ "principal": KEVIN, "validFrom": "2026-06-10T00:00:00Z", "validUntil": null },
{ "principal": ALICE, "validFrom": "2026-06-15T00:00:00Z", "validUntil": null }
]));
assert_eq!(
map.resolve(&actor(AGENT), "2026-06-16T00:00:00Z"),
PrincipalResolution::Ambiguous(vec![actor(ALICE), actor(KEVIN)])
);
}
#[test]
fn overlapping_windows_with_same_principal_resolve() {
let map = map_with(serde_json::json!([
{ "principal": KEVIN, "validFrom": "2026-06-10T00:00:00Z", "validUntil": null },
{ "principal": KEVIN, "validFrom": "2026-06-15T00:00:00Z", "validUntil": null }
]));
assert_eq!(
map.resolve(&actor(AGENT), "2026-06-16T00:00:00Z"),
PrincipalResolution::Resolved(actor(KEVIN))
);
}
#[test]
fn unparseable_event_timestamp_resolves_none_with_reason() {
let map = map_with(serde_json::json!([{
"principal": KEVIN, "validFrom": "2026-06-10T00:00:00Z", "validUntil": null
}]));
assert_eq!(
map.resolve(&actor(AGENT), "garbage"),
PrincipalResolution::None(UnresolvedReason::UnparseableTimestamp)
);
}
#[test]
fn rejects_non_array_records() {
let err = delegation_map_from_value(serde_json::json!({
"delegates": {
"actor:agent:claude-code": {
"principal": "actor:git-email:kevin@swiber.dev",
"validFrom": "2026-06-10T00:00:00Z"
}
}
}))
.expect_err("records for an agent must be an array");
assert!(err.to_string().contains("actor:agent:claude-code"));
}
#[test]
fn local_records_fully_replace_committed_for_same_agent() {
let committed = map_with(serde_json::json!([
{ "principal": KEVIN, "validFrom": "2026-06-10T00:00:00Z", "validUntil": null }
]));
let local = map_with(serde_json::json!([
{ "principal": ALICE, "validFrom": "2026-06-10T00:00:00Z", "validUntil": null }
]));
let merged = committed.with_local_override(local);
assert_eq!(
merged.resolve(&actor(AGENT), "2026-06-12T00:00:00Z"),
PrincipalResolution::Resolved(actor(ALICE))
);
}
#[test]
fn agent_absent_from_local_inherits_committed() {
let committed = delegation_map_from_value(serde_json::json!({
"delegates": {
"actor:agent:claude-code": [
{ "principal": KEVIN, "validFrom": "2026-06-10T00:00:00Z", "validUntil": null }],
"actor:agent:other": [
{ "principal": ALICE, "validFrom": "2026-06-10T00:00:00Z", "validUntil": null }]
}
}))
.unwrap();
let local = map_with(serde_json::json!([
{ "principal": ALICE, "validFrom": "2026-06-10T00:00:00Z", "validUntil": null }
]));
let merged = committed.with_local_override(local);
assert_eq!(
merged.resolve(&actor("actor:agent:other"), "2026-06-12T00:00:00Z"),
PrincipalResolution::Resolved(actor(ALICE))
);
assert_eq!(
merged.resolve(&actor(AGENT), "2026-06-12T00:00:00Z"),
PrincipalResolution::Resolved(actor(ALICE))
);
}
#[test]
fn either_map_alone_round_trips_through_merge() {
let committed = map_with(serde_json::json!([
{ "principal": KEVIN, "validFrom": "2026-06-10T00:00:00Z", "validUntil": null }
]));
assert_eq!(
committed
.clone()
.with_local_override(DelegationMap::default()),
committed
);
assert_eq!(
DelegationMap::default().with_local_override(committed.clone()),
committed
);
}
#[test]
fn local_empty_record_array_disavows_the_agent() {
let committed = map_with(serde_json::json!([
{ "principal": KEVIN, "validFrom": "2026-06-10T00:00:00Z", "validUntil": null }
]));
let local = delegation_map_from_value(serde_json::json!({
"delegates": { "actor:agent:claude-code": [] }
}))
.unwrap();
let merged = committed.with_local_override(local);
assert_eq!(
merged.resolve(&actor(AGENT), "2026-06-12T00:00:00Z"),
PrincipalResolution::None(UnresolvedReason::NoDelegationRecord)
);
}
#[test]
fn stage_delegation_round_trips_through_the_reader() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(".shore/delegates.json");
let agent = ActorId::new("actor:agent:claude-code");
let record = DelegationWriteRecord::new(
ActorId::new("actor:git-email:kevin@swiber.dev"),
"2026-06-10T00:00:00Z".to_owned(),
)
.with_comment(Some("enrolled by Kevin".to_owned()));
let outcome = stage_delegation(&path, &agent, &record).unwrap();
assert!(outcome.added);
let map = DelegationMap::from_delegates_file(&path).unwrap();
assert_eq!(
map.resolve(&agent, "2026-06-11T00:00:00Z"),
PrincipalResolution::Resolved(ActorId::new("actor:git-email:kevin@swiber.dev"))
);
}
#[test]
fn stage_delegation_is_byte_stable_on_identical_restage() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(".shore/delegates.json");
let agent = ActorId::new("actor:agent:claude-code");
let record = DelegationWriteRecord::new(
ActorId::new("actor:git-email:kevin@swiber.dev"),
"2026-06-10T00:00:00Z".to_owned(),
);
let first = stage_delegation(&path, &agent, &record).unwrap();
let before = std::fs::read(&path).unwrap();
let second = stage_delegation(&path, &agent, &record).unwrap();
let after = std::fs::read(&path).unwrap();
assert!(
first.added && !second.added,
"identical re-stage is a no-op"
);
assert_eq!(before, after, "re-stage leaves the file byte-identical");
assert!(
before.ends_with(b"\n"),
"trailing newline like stage_enrollment"
);
}
#[test]
fn stage_delegation_appends_a_second_record_for_the_same_agent() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(".shore/delegates.json");
let agent = ActorId::new("actor:agent:claude-code");
stage_delegation(
&path,
&agent,
&DelegationWriteRecord::new(
ActorId::new("actor:git-email:kevin@swiber.dev"),
"2026-06-10T00:00:00Z".to_owned(),
),
)
.unwrap();
let second = stage_delegation(
&path,
&agent,
&DelegationWriteRecord::new(
ActorId::new("actor:git-email:alice@example.com"),
"2026-06-12T00:00:00Z".to_owned(),
),
)
.unwrap();
assert!(second.added);
let map = DelegationMap::from_delegates_file(&path).unwrap();
assert!(matches!(
map.resolve(&agent, "2026-06-13T00:00:00Z"),
PrincipalResolution::Ambiguous(_)
));
}
#[test]
fn stage_delegation_rejects_agent_principal_depth0_rule() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(".shore/delegates.json");
let agent = ActorId::new("actor:agent:claude-code");
let bad = DelegationWriteRecord::new(
ActorId::new("actor:agent:subagent"),
"2026-06-10T00:00:00Z".to_owned(),
);
assert!(stage_delegation(&path, &agent, &bad).is_err());
assert!(!path.exists(), "a rejected record writes nothing");
}
#[test]
fn stage_delegation_refuses_when_an_existing_sibling_is_malformed() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(".shore/delegates.json");
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(&path, br#"{"delegates":{"actor:agent:claude-code":[{"principal":"actor:agent:bad","validFrom":"2026-06-10T00:00:00Z","validUntil":null}]}}"#).unwrap();
let before = std::fs::read(&path).unwrap();
let result = stage_delegation(
&path,
&ActorId::new("actor:agent:claude-code"),
&DelegationWriteRecord::new(
ActorId::new("actor:git-email:kevin@swiber.dev"),
"2026-06-10T00:00:00Z".to_owned(),
),
);
assert!(
result.is_err(),
"a malformed existing sibling must make the stage fail"
);
assert_eq!(
std::fs::read(&path).unwrap(),
before,
"a failed stage writes nothing"
);
}
#[test]
fn stage_delegation_rejects_non_agent_key_and_non_rfc3339_instants() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(".shore/delegates.json");
let principal = ActorId::new("actor:git-email:kevin@swiber.dev");
assert!(
stage_delegation(
&path,
&ActorId::new("actor:git-email:not-an-agent"),
&DelegationWriteRecord::new(principal.clone(), "2026-06-10T00:00:00Z".to_owned())
)
.is_err()
);
assert!(
stage_delegation(
&path,
&ActorId::new("actor:agent:claude-code"),
&DelegationWriteRecord::new(principal, "yesterday".to_owned())
)
.is_err()
);
}
#[test]
fn record_count_for_returns_array_length() {
let agent = actor(AGENT);
assert_eq!(DelegationMap::default().record_count_for(&agent), 0);
let map = map_with(serde_json::json!([
{ "principal": KEVIN, "validFrom": "2026-06-10T00:00:00Z", "validUntil": null },
{ "principal": ALICE, "validFrom": "2026-06-15T00:00:00Z", "validUntil": null }
]));
assert_eq!(map.record_count_for(&agent), 2);
assert_eq!(map.record_count_for(&actor("actor:agent:absent")), 0);
}
}