Skip to main content

punkgo_core/
actor.rs

1//! Actor types and identity derivation.
2//!
3//! Covers: PIP-001 §5 (actor type dichotomy), §7 (agent conditional existence),
4//! §10 (root wildcard boundary).
5
6use serde::{Deserialize, Serialize};
7
8// ---------------------------------------------------------------------------
9// Enums
10// ---------------------------------------------------------------------------
11
12/// PIP-001 §5: Two immutable actor types, exhaustive and mutually exclusive.
13/// - Human: direct human representative, created at kernel init or via identity verification.
14/// - Agent: delegated executor, created **only by human** via `create` action (PIP-001 §5/§6).
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "lowercase")]
17pub enum ActorType {
18    Human,
19    Agent,
20}
21
22impl ActorType {
23    pub fn as_str(&self) -> &'static str {
24        match self {
25            Self::Human => "human",
26            Self::Agent => "agent",
27        }
28    }
29
30    pub fn parse(s: &str) -> Option<Self> {
31        match s {
32            "human" => Some(Self::Human),
33            "agent" => Some(Self::Agent),
34            _ => None,
35        }
36    }
37}
38
39/// Actor lifecycle status.
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41#[serde(rename_all = "lowercase")]
42pub enum ActorStatus {
43    Active,
44    Frozen,
45}
46
47impl ActorStatus {
48    pub fn as_str(&self) -> &'static str {
49        match self {
50            Self::Active => "active",
51            Self::Frozen => "frozen",
52        }
53    }
54
55    pub fn parse(s: &str) -> Option<Self> {
56        match s {
57            "active" => Some(Self::Active),
58            "frozen" => Some(Self::Frozen),
59            _ => None,
60        }
61    }
62}
63
64// ---------------------------------------------------------------------------
65// Structs
66// ---------------------------------------------------------------------------
67
68/// PIP-001 §8: Writability declaration — a glob pattern + allowed action types.
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct WritableTarget {
71    pub target: String,
72    pub actions: Vec<String>,
73}
74
75/// Full actor record as persisted in the `actors` table.
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct ActorRecord {
78    pub actor_id: String,
79    pub actor_type: ActorType,
80    pub creator_id: Option<String>,
81    pub lineage: Vec<String>,
82    pub purpose: Option<String>,
83    pub status: ActorStatus,
84    pub writable_targets: Vec<WritableTarget>,
85    pub energy_share: f64,
86    pub reduction_policy: String,
87    pub created_at: String,
88    pub updated_at: String,
89}
90
91/// Specification for creating a new actor (used in submit pipeline).
92#[derive(Debug, Clone)]
93pub struct CreateActorSpec {
94    pub actor_id: String,
95    pub actor_type: ActorType,
96    pub creator_id: String,
97    pub lineage: Vec<String>,
98    pub purpose: Option<String>,
99    pub writable_targets: Vec<WritableTarget>,
100    pub energy_balance: i64,
101    pub energy_share: f64,
102    pub reduction_policy: String,
103}
104
105// ---------------------------------------------------------------------------
106// Lifecycle operations (PIP-001 §5/§6/§7)
107// ---------------------------------------------------------------------------
108
109/// Phase 4a: Lifecycle operations that can be performed on actors.
110#[derive(Debug, Clone, PartialEq)]
111pub enum LifecycleOp {
112    /// Freeze an actor — suspends all state-changing actions.
113    Freeze { reason: Option<String> },
114    /// Unfreeze — restores an actor to active.
115    Unfreeze,
116    /// Terminate — permanently removes an agent (creates orphans).
117    Terminate { reason: Option<String> },
118    /// Update an actor's energy_share for tick-based distribution.
119    UpdateEnergyShare { energy_share: f64 },
120}
121
122// ---------------------------------------------------------------------------
123// Identity derivation
124// ---------------------------------------------------------------------------
125
126/// Derive an agent ID that encodes the creation relationship.
127///
128/// Format: `{creator_id}/{sanitized_purpose}/{seq}`
129/// Example: `root/doc-organizer/1`, `root/doc-organizer/1/sub-worker/1`
130pub fn derive_agent_id(creator_id: &str, purpose: &str, seq: i64) -> String {
131    let sanitized = sanitize_purpose(purpose);
132    format!("{creator_id}/{sanitized}/{seq}")
133}
134
135/// Sanitize a purpose string for use in an actor ID.
136/// Replace non-alphanumeric/non-hyphen characters with hyphens, lowercase.
137fn sanitize_purpose(purpose: &str) -> String {
138    let s: String = purpose
139        .chars()
140        .map(|c| {
141            if c.is_ascii_alphanumeric() || c == '-' {
142                c.to_ascii_lowercase()
143            } else {
144                '-'
145            }
146        })
147        .collect();
148    // Collapse consecutive hyphens
149    let mut result = String::with_capacity(s.len());
150    let mut prev_hyphen = false;
151    for c in s.chars() {
152        if c == '-' {
153            if !prev_hyphen {
154                result.push(c);
155            }
156            prev_hyphen = true;
157        } else {
158            result.push(c);
159            prev_hyphen = false;
160        }
161    }
162    result.trim_matches('-').to_string()
163}
164
165// ---------------------------------------------------------------------------
166// Lineage construction (PIP-001 §7)
167// ---------------------------------------------------------------------------
168
169/// Build a creation lineage for a new actor (PIP-001 §7).
170///
171/// Since only Humans can create Agents (PIP-001 §5/§6), lineage is always
172/// a single element: `\[human_creator_id\]`.
173///
174/// The `creator_type` and `creator_lineage` parameters are kept for
175/// backward compatibility but the function enforces Human-only creation.
176pub fn build_lineage(
177    creator_type: &ActorType,
178    creator_id: &str,
179    _creator_lineage: &[String],
180) -> Vec<String> {
181    // PIP-001 §5/§6: Only Humans can create Agents.
182    // If creator is Human -> lineage = [human_id] (the only valid case).
183    // If creator is Agent -> still returns [creator_id] but kernel.rs should
184    // reject this before we get here.
185    match creator_type {
186        ActorType::Human => vec![creator_id.to_string()],
187        ActorType::Agent => {
188            // This path should never be reached after PIP-001 §5 enforcement in kernel.rs.
189            // Return creator_id as fallback for safety.
190            vec![creator_id.to_string()]
191        }
192    }
193}
194
195// ---------------------------------------------------------------------------
196// Tests
197// ---------------------------------------------------------------------------
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    #[test]
204    fn derive_agent_id_basic() {
205        assert_eq!(
206            derive_agent_id("root", "doc-organizer", 1),
207            "root/doc-organizer/1"
208        );
209    }
210
211    #[test]
212    fn derive_agent_id_second_agent() {
213        // PIP-001 §5/§6: Only humans create agents, so creator_id is always a human id.
214        // This tests creating a second agent by the same human.
215        assert_eq!(
216            derive_agent_id("root", "tiktok-downloader", 1),
217            "root/tiktok-downloader/1"
218        );
219    }
220
221    #[test]
222    fn derive_agent_id_sanitizes_spaces() {
223        assert_eq!(
224            derive_agent_id("root", "My Cool Agent", 1),
225            "root/my-cool-agent/1"
226        );
227    }
228
229    #[test]
230    fn build_lineage_human_creator() {
231        let lineage = build_lineage(&ActorType::Human, "root", &[]);
232        assert_eq!(lineage, vec!["root"]);
233    }
234
235    #[test]
236    fn build_lineage_agent_creator_fallback() {
237        // PIP-001 §5: Agent-creates-Agent is rejected at kernel level.
238        // build_lineage still returns a safe fallback if called with Agent creator.
239        let creator_lineage = vec!["root".to_string()];
240        let lineage = build_lineage(&ActorType::Agent, "root/worker/1", &creator_lineage);
241        // Fallback: returns [creator_id] (not the old recursive chain)
242        assert_eq!(lineage, vec!["root/worker/1"]);
243    }
244
245    #[test]
246    fn actor_type_roundtrip() {
247        assert_eq!(ActorType::parse("human"), Some(ActorType::Human));
248        assert_eq!(ActorType::parse("agent"), Some(ActorType::Agent));
249        assert_eq!(ActorType::parse("unknown"), None);
250    }
251
252    #[test]
253    fn actor_status_roundtrip() {
254        assert_eq!(ActorStatus::parse("active"), Some(ActorStatus::Active));
255        assert_eq!(ActorStatus::parse("frozen"), Some(ActorStatus::Frozen));
256    }
257}