actionqueue_core/platform.rs
1//! Platform domain types for multi-tenant isolation, RBAC, and ledger entries.
2//!
3//! These types are consumed by `actionqueue-platform` (for enforcement) and
4//! `actionqueue-storage` (for WAL events and snapshot persistence). They live in
5//! `actionqueue-core` so all crates share a single canonical definition.
6
7use crate::ids::{ActorId, LedgerEntryId, TenantId};
8
9/// Organizational role for an actor within a tenant.
10///
11/// Roles map to capability sets via the RBAC enforcer. The `Custom` variant
12/// allows org-specific extension beyond the standard triad.
13#[derive(Debug, Clone, PartialEq, Eq, Hash)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15pub enum Role {
16 /// Operator: submits work plans, manages tasks.
17 Operator,
18 /// Auditor: reviews plans, produces approval/rejection decisions.
19 Auditor,
20 /// Gatekeeper: executes privileged actions after approval.
21 Gatekeeper,
22 /// Extension role. The inner string must be non-empty.
23 Custom(String),
24}
25
26impl Role {
27 /// Creates a validated custom role.
28 ///
29 /// # Errors
30 ///
31 /// Returns an error if `name` is empty.
32 pub fn custom(name: impl Into<String>) -> Result<Self, String> {
33 let name = name.into();
34 if name.is_empty() {
35 return Err("custom role name must be non-empty".to_string());
36 }
37 Ok(Role::Custom(name))
38 }
39}
40
41/// Typed permission for an actor within a tenant.
42#[derive(Debug, Clone, PartialEq, Eq, Hash)]
43#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
44pub enum Capability {
45 /// Actor may submit new tasks.
46 CanSubmit,
47 /// Actor may execute (claim and complete) tasks.
48 CanExecute,
49 /// Actor may review tasks and produce approval/rejection outcomes.
50 CanReview,
51 /// Actor may approve proposed actions.
52 CanApprove,
53 /// Actor may cancel tasks and runs.
54 CanCancel,
55 /// Extension capability. The inner string must be non-empty.
56 Custom(String),
57}
58
59impl Capability {
60 /// Creates a validated custom capability.
61 ///
62 /// # Errors
63 ///
64 /// Returns an error if `name` is empty.
65 pub fn custom(name: impl Into<String>) -> Result<Self, String> {
66 let name = name.into();
67 if name.is_empty() {
68 return Err("custom capability name must be non-empty".to_string());
69 }
70 Ok(Capability::Custom(name))
71 }
72}
73
74/// Tenant registration record.
75#[derive(Debug, Clone, PartialEq, Eq)]
76#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
77pub struct TenantRegistration {
78 tenant_id: TenantId,
79 name: String,
80}
81
82impl TenantRegistration {
83 /// Creates a new tenant registration.
84 ///
85 /// # Panics
86 ///
87 /// Panics if `name` is empty.
88 pub fn new(tenant_id: TenantId, name: impl Into<String>) -> Self {
89 let name = name.into();
90 assert!(!name.is_empty(), "tenant name must be non-empty");
91 TenantRegistration { tenant_id, name }
92 }
93
94 /// Returns the tenant identifier.
95 pub fn tenant_id(&self) -> TenantId {
96 self.tenant_id
97 }
98
99 /// Returns the tenant name.
100 pub fn name(&self) -> &str {
101 &self.name
102 }
103}
104
105/// Generic ledger entry for append-only platform ledgers.
106///
107/// Ledger keys identify the logical ledger (e.g. `"audit"`, `"decision"`,
108/// `"relationship"`, `"incident"`, `"reality"`). The payload is opaque bytes
109/// whose schema is defined by the consumer (Caelum, Digicorp).
110#[derive(Debug, Clone, PartialEq, Eq)]
111#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
112pub struct LedgerEntry {
113 entry_id: LedgerEntryId,
114 tenant_id: TenantId,
115 /// The logical ledger name (e.g. `"audit"`, `"decision"`).
116 ledger_key: String,
117 /// The actor that produced this entry, if any.
118 actor_id: Option<ActorId>,
119 /// Opaque payload bytes. Schema is consumer-defined.
120 payload: Vec<u8>,
121 /// Unix epoch seconds when this entry was recorded.
122 timestamp: u64,
123}
124
125impl LedgerEntry {
126 /// Creates a new ledger entry.
127 ///
128 /// `actor_id` can be set via [`with_actor`](Self::with_actor) after construction.
129 ///
130 /// # Panics
131 ///
132 /// Panics if `ledger_key` is empty.
133 pub fn new(
134 entry_id: LedgerEntryId,
135 tenant_id: TenantId,
136 ledger_key: impl Into<String>,
137 payload: Vec<u8>,
138 timestamp: u64,
139 ) -> Self {
140 let ledger_key = ledger_key.into();
141 assert!(!ledger_key.is_empty(), "ledger_key must be non-empty");
142 LedgerEntry { entry_id, tenant_id, ledger_key, actor_id: None, payload, timestamp }
143 }
144
145 /// Attaches an actor identifier, returning the modified entry.
146 pub fn with_actor(mut self, actor_id: ActorId) -> Self {
147 self.actor_id = Some(actor_id);
148 self
149 }
150
151 /// Returns the entry identifier.
152 pub fn entry_id(&self) -> LedgerEntryId {
153 self.entry_id
154 }
155
156 /// Returns the tenant identifier.
157 pub fn tenant_id(&self) -> TenantId {
158 self.tenant_id
159 }
160
161 /// Returns the logical ledger key.
162 pub fn ledger_key(&self) -> &str {
163 &self.ledger_key
164 }
165
166 /// Returns the actor identifier, if any.
167 pub fn actor_id(&self) -> Option<ActorId> {
168 self.actor_id
169 }
170
171 /// Returns the opaque payload bytes.
172 pub fn payload(&self) -> &[u8] {
173 &self.payload
174 }
175
176 /// Returns the entry timestamp (Unix epoch seconds).
177 pub fn timestamp(&self) -> u64 {
178 self.timestamp
179 }
180}