Skip to main content

tf_types/
actor_id.rs

1//! Actor-URI parser and formatter mirroring `tools/tf-types-ts/src/core/actor-id.ts`.
2
3use crate::generated::common::ActorType;
4use sha2::{Digest, Sha256};
5
6#[derive(Debug, thiserror::Error, PartialEq, Eq)]
7pub enum ActorIdParseError {
8    #[error("expected tf:actor:<type>:<path>, got {0:?}")]
9    MalformedScheme(String),
10    #[error("expected scheme 'tf:actor:', got 'tf:{0}:'")]
11    WrongKind(String),
12    #[error("unknown actor type: {0}")]
13    UnknownType(String),
14    #[error("actor id path is empty")]
15    EmptyPath,
16}
17
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct ParsedActorId {
20    pub actor_type: ActorType,
21    pub path: String,
22    pub raw: String,
23}
24
25pub const ACTOR_TYPE_STRS: &[&str] = &[
26    "human",
27    "agent",
28    "device",
29    "service",
30    "site",
31    "organization",
32    "relay",
33    "plugin",
34    "process",
35    "tool",
36    "model-provider",
37    "policy-engine",
38    "proof-anchor",
39    "emergency-authority",
40];
41
42pub fn parse_actor_id(s: &str) -> Result<ParsedActorId, ActorIdParseError> {
43    let parts = split_scheme(s).ok_or_else(|| ActorIdParseError::MalformedScheme(s.to_owned()))?;
44    if parts.kind != "actor" {
45        return Err(ActorIdParseError::WrongKind(parts.kind.to_owned()));
46    }
47    let actor_type = parse_actor_type(parts.type_segment)
48        .ok_or_else(|| ActorIdParseError::UnknownType(parts.type_segment.to_owned()))?;
49    if parts.path.is_empty() {
50        return Err(ActorIdParseError::EmptyPath);
51    }
52    Ok(ParsedActorId {
53        actor_type,
54        path: parts.path.to_owned(),
55        raw: s.to_owned(),
56    })
57}
58
59pub fn format_actor_id(actor_type: &ActorType, path: &str) -> Result<String, ActorIdParseError> {
60    if path.is_empty() {
61        return Err(ActorIdParseError::EmptyPath);
62    }
63    Ok(format!(
64        "tf:actor:{}:{}",
65        actor_type_to_str(actor_type),
66        path
67    ))
68}
69
70pub fn actor_id_equals(a: &str, b: &str) -> bool {
71    match (parse_actor_id(a), parse_actor_id(b)) {
72        (Ok(pa), Ok(pb)) => pa.actor_type == pb.actor_type && pa.path == pb.path,
73        _ => false,
74    }
75}
76
77/// Derive the canonical actor URI for a peer identified by an ed25519 public
78/// key. Mirrors `derivePeerActor` in `tools/tf-types-ts/src/core/actor-id.ts`.
79/// Returns `tf:actor:process:key/<hex>` where `<hex>` is the lowercase hex of
80/// the first 8 bytes of `sha256(ident_pub)`.
81pub fn derive_peer_actor(ident_pub: &[u8]) -> Result<String, ActorIdParseError> {
82    if ident_pub.len() != 32 {
83        return Err(ActorIdParseError::EmptyPath);
84    }
85    let digest = Sha256::digest(ident_pub);
86    let thumbprint = digest[..8]
87        .iter()
88        .map(|b| format!("{:02x}", b))
89        .collect::<String>();
90    Ok(format!("tf:actor:process:key/{}", thumbprint))
91}
92
93pub(crate) struct SchemeParts<'a> {
94    pub kind: &'a str,
95    pub type_segment: &'a str,
96    pub path: &'a str,
97}
98
99pub(crate) fn split_scheme(s: &str) -> Option<SchemeParts<'_>> {
100    let rest = s.strip_prefix("tf:")?;
101    let first = rest.find(':')?;
102    let kind = &rest[..first];
103    let remainder = &rest[first + 1..];
104    let second = remainder.find(':')?;
105    let type_segment = &remainder[..second];
106    let path = &remainder[second + 1..];
107    Some(SchemeParts {
108        kind,
109        type_segment,
110        path,
111    })
112}
113
114pub(crate) fn parse_actor_type(s: &str) -> Option<ActorType> {
115    Some(match s {
116        "human" => ActorType::Human,
117        "agent" => ActorType::Agent,
118        "device" => ActorType::Device,
119        "service" => ActorType::Service,
120        "site" => ActorType::Site,
121        "organization" => ActorType::Organization,
122        "relay" => ActorType::Relay,
123        "plugin" => ActorType::Plugin,
124        "process" => ActorType::Process,
125        "tool" => ActorType::Tool,
126        "model-provider" => ActorType::ModelProvider,
127        "policy-engine" => ActorType::PolicyEngine,
128        "proof-anchor" => ActorType::ProofAnchor,
129        "emergency-authority" => ActorType::EmergencyAuthority,
130        _ => return None,
131    })
132}
133
134pub(crate) fn actor_type_to_str(t: &ActorType) -> &'static str {
135    match t {
136        ActorType::Human => "human",
137        ActorType::Agent => "agent",
138        ActorType::Device => "device",
139        ActorType::Service => "service",
140        ActorType::Site => "site",
141        ActorType::Organization => "organization",
142        ActorType::Relay => "relay",
143        ActorType::Plugin => "plugin",
144        ActorType::Process => "process",
145        ActorType::Tool => "tool",
146        ActorType::ModelProvider => "model-provider",
147        ActorType::PolicyEngine => "policy-engine",
148        ActorType::ProofAnchor => "proof-anchor",
149        ActorType::EmergencyAuthority => "emergency-authority",
150    }
151}