orcs_types/principal.rs
1//! Principal (actor identity) types.
2//!
3//! A [`Principal`] represents the actor performing an action,
4//! separating "who is acting" from "what they are allowed to do".
5//!
6//! # Design Rationale
7//!
8//! Principal is placed in `orcs-types` (not `orcs-auth`) because:
9//!
10//! 1. **Plugin SDK boundary**: Plugins receive Signals containing Principal
11//! 2. **No auth logic dependency**: Principal is pure identity, no permission logic
12//! 3. **Avoid circular dependency**: Event -> Auth -> Types would create issues
13//!
14//! Permission checking (Session, PrivilegeLevel) remains in the runtime layer.
15
16use crate::{ComponentId, PrincipalId};
17use serde::{Deserialize, Serialize};
18
19/// The actor performing an action.
20///
21/// A Principal represents identity only, not permission level.
22/// Permission is determined by the runtime layer's Session.
23///
24/// # Variants
25///
26/// | Variant | Description | Typical Use |
27/// |---------|-------------|-------------|
28/// | `User` | Human user | Interactive CLI usage |
29/// | `Component` | Autonomous component | Background processing |
30/// | `System` | Internal operations | Lifecycle, cleanup |
31///
32/// # Why Not Just Use ComponentId?
33///
34/// [`ComponentId`] identifies *what* is executing (LLM, Tool, etc.).
35/// [`Principal`] identifies *who* initiated the action.
36///
37/// A Tool component may execute a file write, but the Principal
38/// is the human user who requested it. This distinction enables:
39///
40/// - Audit trails attributing actions to users
41/// - Permission checks based on who initiated
42/// - Rate limiting per user, not per component
43///
44/// # Example
45///
46/// ```
47/// use orcs_types::{Principal, ComponentId, PrincipalId};
48///
49/// // Human user
50/// let user = Principal::User(PrincipalId::new());
51///
52/// // Component acting autonomously (e.g., scheduled task)
53/// let component = Principal::Component(ComponentId::builtin("scheduler"));
54///
55/// // System internal operation
56/// let system = Principal::System;
57/// ```
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59pub enum Principal {
60 /// Human user identified by [`PrincipalId`].
61 ///
62 /// Represents interactive CLI usage or API calls from
63 /// authenticated users.
64 User(PrincipalId),
65
66 /// Component acting autonomously.
67 ///
68 /// Used when a component initiates actions without direct
69 /// human involvement, such as:
70 ///
71 /// - Scheduled background tasks
72 /// - Event-driven reactions
73 /// - Plugin self-maintenance
74 Component(ComponentId),
75
76 /// System internal operations.
77 ///
78 /// Used for operations that are not attributable to any
79 /// specific user or component:
80 ///
81 /// - Lifecycle management (startup, shutdown)
82 /// - Garbage collection
83 /// - Internal housekeeping
84 System,
85}
86
87impl Principal {
88 /// Returns `true` if this is a [`Principal::User`].
89 #[must_use]
90 pub fn is_user(&self) -> bool {
91 matches!(self, Self::User(_))
92 }
93
94 /// Returns `true` if this is a [`Principal::Component`].
95 #[must_use]
96 pub fn is_component(&self) -> bool {
97 matches!(self, Self::Component(_))
98 }
99
100 /// Returns `true` if this is [`Principal::System`].
101 #[must_use]
102 pub fn is_system(&self) -> bool {
103 matches!(self, Self::System)
104 }
105
106 /// Returns the [`PrincipalId`] if this is a User, otherwise `None`.
107 #[must_use]
108 pub fn user_id(&self) -> Option<&PrincipalId> {
109 match self {
110 Self::User(id) => Some(id),
111 _ => None,
112 }
113 }
114
115 /// Returns the [`ComponentId`] if this is a Component, otherwise `None`.
116 #[must_use]
117 pub fn component_id(&self) -> Option<&ComponentId> {
118 match self {
119 Self::Component(id) => Some(id),
120 _ => None,
121 }
122 }
123}
124
125impl std::fmt::Display for Principal {
126 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127 match self {
128 Self::User(id) => write!(f, "user:{}", id.uuid()),
129 Self::Component(id) => write!(f, "component:{}", id.fqn()),
130 Self::System => write!(f, "system"),
131 }
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn principal_user() {
141 let id = PrincipalId::new();
142 let principal = Principal::User(id);
143
144 assert!(principal.is_user());
145 assert!(!principal.is_component());
146 assert!(!principal.is_system());
147 assert!(principal.user_id().is_some());
148 assert!(principal.component_id().is_none());
149 }
150
151 #[test]
152 fn principal_component() {
153 let id = ComponentId::builtin("llm");
154 let principal = Principal::Component(id);
155
156 assert!(!principal.is_user());
157 assert!(principal.is_component());
158 assert!(!principal.is_system());
159 assert!(principal.user_id().is_none());
160 assert!(principal.component_id().is_some());
161 }
162
163 #[test]
164 fn principal_system() {
165 let principal = Principal::System;
166
167 assert!(!principal.is_user());
168 assert!(!principal.is_component());
169 assert!(principal.is_system());
170 assert!(principal.user_id().is_none());
171 assert!(principal.component_id().is_none());
172 }
173
174 #[test]
175 fn principal_display() {
176 let user = Principal::User(PrincipalId::new());
177 assert!(format!("{user}").starts_with("user:"));
178
179 let component = Principal::Component(ComponentId::builtin("llm"));
180 assert_eq!(format!("{component}"), "component:builtin::llm");
181
182 let system = Principal::System;
183 assert_eq!(format!("{system}"), "system");
184 }
185
186 #[test]
187 fn principal_equality() {
188 let id1 = PrincipalId::new();
189 let id2 = PrincipalId::new();
190
191 let p1 = Principal::User(id1);
192 let p2 = Principal::User(id1);
193 let p3 = Principal::User(id2);
194
195 assert_eq!(p1, p2);
196 assert_ne!(p1, p3);
197 assert_ne!(Principal::System, p1);
198 }
199}