punkgo_kernel/runtime/
lifecycle.rs1use crate::state::ActorStore;
15use punkgo_core::actor::{ActorRecord, ActorStatus, ActorType, LifecycleOp};
16use punkgo_core::errors::{KernelError, KernelResult};
17
18pub fn parse_lifecycle_op(
22 target: &str,
23 payload: &serde_json::Value,
24) -> Option<(String, LifecycleOp)> {
25 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
47pub async fn validate_lifecycle_authorization(
58 initiator: &ActorRecord,
59 target: &ActorRecord,
60 _op: &LifecycleOp,
61) -> KernelResult<()> {
62 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 if initiator.actor_id == "root" {
71 return Ok(());
72 }
73
74 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 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
98pub 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 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 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
130pub 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
147pub 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}