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 "update_energy_share" => {
42 let energy_share = payload.get("energy_share")?.as_f64()?;
43 LifecycleOp::UpdateEnergyShare { energy_share }
44 }
45 _ => return None,
46 };
47
48 Some((actor_id.to_string(), op))
49}
50
51pub async fn validate_lifecycle_authorization(
61 initiator: &ActorRecord,
62 target: &ActorRecord,
63 _op: &LifecycleOp,
64) -> KernelResult<()> {
65 if target.actor_type == ActorType::Human {
67 return Err(KernelError::PolicyViolation(
68 "cannot perform lifecycle operations on human actors".to_string(),
69 ));
70 }
71
72 if initiator.actor_type == ActorType::Agent {
74 return Err(KernelError::PolicyViolation(format!(
75 "agent {} cannot perform lifecycle operations — only humans can manage agents (PIP-001 §5)",
76 initiator.actor_id
77 )));
78 }
79
80 if initiator.actor_type == ActorType::Human {
82 if target.lineage.contains(&initiator.actor_id) {
83 return Ok(());
84 }
85 return Err(KernelError::PolicyViolation(format!(
86 "human {} cannot manage actor {} (not in lineage)",
87 initiator.actor_id, target.actor_id
88 )));
89 }
90
91 Err(KernelError::PolicyViolation(
92 "lifecycle authorization denied".to_string(),
93 ))
94}
95
96pub async fn execute_freeze(
102 actor_store: &ActorStore,
103 pool: &sqlx::SqlitePool,
104 target_id: &str,
105) -> KernelResult<Vec<String>> {
106 let mut tx = pool.begin().await?;
107 let mut frozen_ids = Vec::new();
108
109 actor_store
111 .set_status_in_tx(&mut tx, target_id, &ActorStatus::Frozen)
112 .await?;
113 frozen_ids.push(target_id.to_string());
114
115 let descendants = actor_store.list_descendants(target_id).await?;
117 for descendant in descendants {
118 actor_store
119 .set_status_in_tx(&mut tx, &descendant.actor_id, &ActorStatus::Frozen)
120 .await?;
121 frozen_ids.push(descendant.actor_id);
122 }
123
124 tx.commit().await?;
125 Ok(frozen_ids)
126}
127
128pub async fn execute_unfreeze(
133 actor_store: &ActorStore,
134 pool: &sqlx::SqlitePool,
135 target_id: &str,
136) -> KernelResult<()> {
137 let mut tx = pool.begin().await?;
138 actor_store
139 .set_status_in_tx(&mut tx, target_id, &ActorStatus::Active)
140 .await?;
141 tx.commit().await?;
142 Ok(())
143}
144
145pub async fn execute_update_energy_share(
147 actor_store: &ActorStore,
148 pool: &sqlx::SqlitePool,
149 target_id: &str,
150 energy_share: f64,
151) -> KernelResult<()> {
152 let mut tx = pool.begin().await?;
153 actor_store
154 .update_energy_share_in_tx(&mut tx, target_id, energy_share)
155 .await?;
156 tx.commit().await?;
157 Ok(())
158}
159
160pub async fn check_lineage_active(
172 actor_store: &ActorStore,
173 lineage: &[String],
174) -> KernelResult<()> {
175 for ancestor_id in lineage {
176 let is_active = actor_store.is_active(ancestor_id).await?;
177 if !is_active {
178 return Err(KernelError::PolicyViolation(format!(
179 "lineage ancestor {} is not active (PIP-001 §7: delegator absence)",
180 ancestor_id
181 )));
182 }
183 }
184 Ok(())
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190 use serde_json::json;
191
192 #[test]
193 fn parse_update_energy_share_op() {
194 let (id, op) = parse_lifecycle_op(
195 "actor/agent-1",
196 &json!({"op": "update_energy_share", "energy_share": 42.5}),
197 )
198 .expect("should parse");
199 assert_eq!(id, "agent-1");
200 assert!(
201 matches!(op, LifecycleOp::UpdateEnergyShare { energy_share } if (energy_share - 42.5).abs() < f64::EPSILON)
202 );
203 }
204
205 #[test]
206 fn parse_update_energy_share_missing_value() {
207 let result = parse_lifecycle_op("actor/agent-1", &json!({"op": "update_energy_share"}));
208 assert!(
209 result.is_none(),
210 "missing energy_share value should return None"
211 );
212 }
213
214 #[test]
215 fn parse_freeze_op() {
216 let (id, op) =
217 parse_lifecycle_op("actor/agent-1", &json!({"op": "freeze", "reason": "test"}))
218 .expect("should parse");
219 assert_eq!(id, "agent-1");
220 assert!(matches!(op, LifecycleOp::Freeze { reason: Some(r) } if r == "test"));
221 }
222
223 #[test]
224 fn parse_unfreeze_op() {
225 let (id, op) =
226 parse_lifecycle_op("actor/agent-1", &json!({"op": "unfreeze"})).expect("should parse");
227 assert_eq!(id, "agent-1");
228 assert!(matches!(op, LifecycleOp::Unfreeze));
229 }
230
231 #[test]
232 fn parse_non_lifecycle_target() {
233 let result = parse_lifecycle_op("workspace/a", &json!({"op": "freeze"}));
234 assert!(result.is_none(), "non-actor target should return None");
235 }
236
237 #[test]
238 fn parse_unknown_op() {
239 let result = parse_lifecycle_op("actor/agent-1", &json!({"op": "destroy"}));
240 assert!(result.is_none(), "unknown op should return None");
241 }
242}