Skip to main content

heddle_object_model/object/
state_attribution.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Attribution types for states.
3
4use serde::{Deserialize, Serialize};
5
6/// Human identity accountable for changes.
7#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
8pub struct Principal {
9    /// Human-readable name bytes, preserved exactly from the source identity.
10    #[serde(with = "serde_bytes")]
11    pub name: Vec<u8>,
12    /// Email address bytes, preserved exactly from the source identity.
13    #[serde(with = "serde_bytes")]
14    pub email: Vec<u8>,
15}
16
17impl Principal {
18    /// Create a new principal.
19    pub fn new(name: impl AsRef<[u8]>, email: impl AsRef<[u8]>) -> Self {
20        Self {
21            name: name.as_ref().to_vec(),
22            email: email.as_ref().to_vec(),
23        }
24    }
25
26    /// Return the name as text for display or text-only API boundaries.
27    pub fn name_lossy(&self) -> std::borrow::Cow<'_, str> {
28        String::from_utf8_lossy(&self.name)
29    }
30
31    /// Return the email as text for display or text-only API boundaries.
32    pub fn email_lossy(&self) -> std::borrow::Cow<'_, str> {
33        String::from_utf8_lossy(&self.email)
34    }
35
36    /// Create from environment variables.
37    pub fn from_env() -> Option<Self> {
38        let name = std::env::var("HEDDLE_PRINCIPAL_NAME").ok()?;
39        let email = std::env::var("HEDDLE_PRINCIPAL_EMAIL").ok()?;
40        if name.trim().is_empty() || email.trim().is_empty() {
41            return None;
42        }
43        Some(Self {
44            name: name.into_bytes(),
45            email: email.into_bytes(),
46        })
47    }
48}
49
50impl std::fmt::Display for Principal {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        write!(f, "{} <{}>", self.name_lossy(), self.email_lossy())
53    }
54}
55
56/// AI agent identity that performed changes on behalf of a principal.
57#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
58pub struct Agent {
59    /// Provider name (e.g., "anthropic", "openai").
60    pub provider: String,
61    /// Model identifier (e.g., "claude-opus-4-5-20250120").
62    pub model: String,
63    /// Session identifier (opt-in, links to Session.id).
64    pub session_id: Option<String>,
65    /// Segment identifier (opt-in, links to SessionSegment.id).
66    pub segment_id: Option<String>,
67    /// Policy or prompt template identifier.
68    pub policy_id: Option<String>,
69}
70
71impl Agent {
72    /// Create a new agent.
73    pub fn new(provider: impl Into<String>, model: impl Into<String>) -> Self {
74        Self {
75            provider: provider.into(),
76            model: model.into(),
77            session_id: None,
78            segment_id: None,
79            policy_id: None,
80        }
81    }
82
83    /// Create with session linkage.
84    pub fn with_session(
85        mut self,
86        session_id: impl Into<String>,
87        segment_id: impl Into<String>,
88    ) -> Self {
89        self.session_id = Some(session_id.into());
90        self.segment_id = Some(segment_id.into());
91        self
92    }
93
94    /// Create with policy ID.
95    pub fn with_policy(mut self, policy_id: impl Into<String>) -> Self {
96        self.policy_id = Some(policy_id.into());
97        self
98    }
99
100    /// Create from environment variables.
101    pub fn from_env() -> Option<Self> {
102        let provider = std::env::var("HEDDLE_AGENT_PROVIDER").ok()?;
103        let model = std::env::var("HEDDLE_AGENT_MODEL").ok()?;
104        let session_id = std::env::var("HEDDLE_SESSION_ID").ok();
105        let segment_id = std::env::var("HEDDLE_SESSION_SEGMENT").ok();
106        let policy_id = std::env::var("HEDDLE_AGENT_POLICY").ok();
107        Some(Self {
108            provider,
109            model,
110            session_id,
111            segment_id,
112            policy_id,
113        })
114    }
115}
116
117impl std::fmt::Display for Agent {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        write!(f, "{}/{}", self.provider, self.model)
120    }
121}
122
123/// Attribution for a change (who did it).
124#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
125pub struct Attribution {
126    /// Human accountable for the change.
127    pub principal: Principal,
128    /// AI agent that performed the change (if any).
129    pub agent: Option<Agent>,
130}
131
132impl Attribution {
133    /// Create attribution for a human-only change.
134    pub fn human(principal: Principal) -> Self {
135        Self {
136            principal,
137            agent: None,
138        }
139    }
140
141    /// Create attribution for an agent-assisted change.
142    pub fn with_agent(principal: Principal, agent: Agent) -> Self {
143        Self {
144            principal,
145            agent: Some(agent),
146        }
147    }
148}
149
150impl std::fmt::Display for Attribution {
151    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152        if let Some(agent) = &self.agent {
153            write!(f, "{} (via {})", self.principal, agent)
154        } else {
155            write!(f, "{}", self.principal)
156        }
157    }
158}