Skip to main content

punkgo_kernel/runtime/
lifecycle.rs

1//! Actor lifecycle operations — freeze, unfreeze, terminate.
2//!
3//! Covers: PIP-001 §7 (agent conditional existence), §5/§6 (actor types),
4//! whitepaper §3 invariant 6 (governance auditable).
5//!
6//! PIP-001 §5/§6: Only humans create agents. Agents cannot manage other agents.
7//! §6 corollary: Orphan problem eliminated (human creators cannot be terminated).
8//!
9//! Lifecycle operations are triggered by submitting actions with:
10//!   target = "actor/{actor_id}"
11//!   action_type = Mutate
12//!   payload.op = "freeze" | "unfreeze" | "terminate"
13
14use crate::state::ActorStore;
15use punkgo_core::actor::{ActorRecord, ActorStatus, ActorType, LifecycleOp};
16use punkgo_core::errors::{KernelError, KernelResult};
17
18/// Parse a lifecycle operation from an action target + payload.
19///
20/// Returns None if this is not a lifecycle operation.
21pub fn parse_lifecycle_op(
22    target: &str,
23    payload: &serde_json::Value,
24) -> Option<(String, LifecycleOp)> {
25    // target must be "actor/{actor_id}"
26    let actor_id = target.strip_prefix("actor/")?;
27    if actor_id.is_empty() {
28        return None;
29    }
30
31    let op_str = payload.get("op")?.as_str()?;
32    let reason = payload
33        .get("reason")
34        .and_then(|v| v.as_str())
35        .map(|s| s.to_string());
36
37    let op = match op_str {
38        "freeze" => LifecycleOp::Freeze { reason },
39        "unfreeze" => LifecycleOp::Unfreeze,
40        "terminate" => LifecycleOp::Terminate { reason },
41        _ => return None,
42    };
43
44    Some((actor_id.to_string(), op))
45}
46
47/// Validate that the initiator is authorized to perform a lifecycle operation
48/// on the target actor (PIP-001 §5/§6 authorization rules).
49///
50/// PIP-001 §5/§6: Only Humans can create Agents, so only Humans can manage Agents.
51///
52/// | Initiator  | Target     | Allowed |
53/// |-----------|------------|---------|
54/// | human     | own agent  | yes     |
55/// | agent     | any        | no      |
56/// | root      | any agent  | yes     |
57pub async fn validate_lifecycle_authorization(
58    initiator: &ActorRecord,
59    target: &ActorRecord,
60    _op: &LifecycleOp,
61) -> KernelResult<()> {
62    // Cannot perform lifecycle ops on humans
63    if target.actor_type == ActorType::Human {
64        return Err(KernelError::PolicyViolation(
65            "cannot perform lifecycle operations on human actors".to_string(),
66        ));
67    }
68
69    // Root can do anything
70    if initiator.actor_id == "root" {
71        return Ok(());
72    }
73
74    // PIP-001 §5: Agents cannot manage other agents.
75    if initiator.actor_type == ActorType::Agent {
76        return Err(KernelError::PolicyViolation(format!(
77            "agent {} cannot perform lifecycle operations — only humans can manage agents (PIP-001 §5)",
78            initiator.actor_id
79        )));
80    }
81
82    // Human initiator: target must be created by this human (lineage contains the human)
83    if initiator.actor_type == ActorType::Human {
84        if target.lineage.contains(&initiator.actor_id) {
85            return Ok(());
86        }
87        return Err(KernelError::PolicyViolation(format!(
88            "human {} cannot manage actor {} (not in lineage)",
89            initiator.actor_id, target.actor_id
90        )));
91    }
92
93    Err(KernelError::PolicyViolation(
94        "lifecycle authorization denied".to_string(),
95    ))
96}
97
98/// Execute a freeze operation: set target to frozen status, cascade to dependents.
99///
100/// Freezing suspends all state-changing actions.
101/// PIP-001 §5/§6: Agents have no children, but when a Human freezes,
102/// all agents they created (whose lineage contains the human_id) are also frozen.
103pub async fn execute_freeze(
104    actor_store: &ActorStore,
105    pool: &sqlx::SqlitePool,
106    target_id: &str,
107) -> KernelResult<Vec<String>> {
108    let mut tx = pool.begin().await?;
109    let mut frozen_ids = Vec::new();
110
111    // Freeze the target
112    actor_store
113        .set_status_in_tx(&mut tx, target_id, &ActorStatus::Frozen)
114        .await?;
115    frozen_ids.push(target_id.to_string());
116
117    // Cascade: freeze all descendants (actors whose lineage contains target_id)
118    let descendants = actor_store.list_descendants(target_id).await?;
119    for descendant in descendants {
120        actor_store
121            .set_status_in_tx(&mut tx, &descendant.actor_id, &ActorStatus::Frozen)
122            .await?;
123        frozen_ids.push(descendant.actor_id);
124    }
125
126    tx.commit().await?;
127    Ok(frozen_ids)
128}
129
130/// Execute an unfreeze operation: set target to active status.
131///
132/// Note: unfreeze does NOT cascade — each agent must be individually unfrozen.
133/// This is deliberate: the human must consciously decide to restore each agent.
134pub async fn execute_unfreeze(
135    actor_store: &ActorStore,
136    pool: &sqlx::SqlitePool,
137    target_id: &str,
138) -> KernelResult<()> {
139    let mut tx = pool.begin().await?;
140    actor_store
141        .set_status_in_tx(&mut tx, target_id, &ActorStatus::Active)
142        .await?;
143    tx.commit().await?;
144    Ok(())
145}
146
147/// Check existence conditions for an actor (PIP-001 §7).
148///
149/// Four conditions that must hold for an actor to exist:
150/// 1. Energy >= 0 — checked naturally by reserve/settle
151/// 2. Creator active — the human creator must be active (PIP-001 §7)
152/// 3. Writable boundary non-empty — checked by Phase 3 boundary enforcement
153/// 4. Not frozen — checked by Phase 1 frozen status
154///
155/// This function checks condition 2 (creator activity).
156/// PIP-001 §5/§6: lineage is always single-element \[human_creator_id\],
157/// so this is effectively "is my creator active?"
158pub async fn check_lineage_active(
159    actor_store: &ActorStore,
160    lineage: &[String],
161) -> KernelResult<()> {
162    for ancestor_id in lineage {
163        let is_active = actor_store.is_active(ancestor_id).await?;
164        if !is_active {
165            return Err(KernelError::PolicyViolation(format!(
166                "lineage ancestor {} is not active (PIP-001 §7: delegator absence)",
167                ancestor_id
168            )));
169        }
170    }
171    Ok(())
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use serde_json::json;
178
179    #[test]
180    fn parse_freeze_op() {
181        let (id, op) =
182            parse_lifecycle_op("actor/agent-1", &json!({"op": "freeze", "reason": "test"}))
183                .expect("should parse");
184        assert_eq!(id, "agent-1");
185        assert!(matches!(op, LifecycleOp::Freeze { reason: Some(r) } if r == "test"));
186    }
187
188    #[test]
189    fn parse_unfreeze_op() {
190        let (id, op) =
191            parse_lifecycle_op("actor/agent-1", &json!({"op": "unfreeze"})).expect("should parse");
192        assert_eq!(id, "agent-1");
193        assert!(matches!(op, LifecycleOp::Unfreeze));
194    }
195
196    #[test]
197    fn parse_non_lifecycle_target() {
198        let result = parse_lifecycle_op("workspace/a", &json!({"op": "freeze"}));
199        assert!(result.is_none(), "non-actor target should return None");
200    }
201
202    #[test]
203    fn parse_unknown_op() {
204        let result = parse_lifecycle_op("actor/agent-1", &json!({"op": "destroy"}));
205        assert!(result.is_none(), "unknown op should return None");
206    }
207}