1use std::path::PathBuf;
2
3use serde::{Deserialize, Serialize};
4use serde_json::{Value, json};
5use tracing::{info, warn};
6use uuid::Uuid;
7
8use crate::audit::AuditLog;
9use punkgo_core::action::{Action, ActionType, payload_hash_hex, quote_cost};
10use punkgo_core::actor::{
11 ActorType, CreateActorSpec, WritableTarget, build_lineage, derive_agent_id,
12};
13use punkgo_core::boundary::{check_writable_boundary, validate_child_targets};
14use punkgo_core::consent::{self, AuthorizationMode, CheckpointLevel, EnvelopeSpec, HoldRule};
15use punkgo_core::errors::{KernelError, KernelResult};
16use punkgo_core::policy::{check_read_access, validate_action};
17
18use super::lifecycle;
19use crate::state::{
20 ActorStore, EnergyLedger, EnergyReservation, EnvelopeStore, EventLog, EventRecord,
21 NewHoldRequest, StateStore,
22};
23use punkgo_core::protocol::{RequestEnvelope, RequestType, ResponseEnvelope};
24use punkgo_core::stellar::{StellarConfig, load_stellar_config};
25
26#[derive(Debug, Clone)]
28pub struct KernelConfig {
29 pub state_dir: PathBuf,
30 pub ipc_endpoint: String,
31}
32
33impl Default for KernelConfig {
34 fn default() -> Self {
35 let state_dir = std::env::var("PUNKGO_STATE_DIR")
36 .map(PathBuf::from)
37 .unwrap_or_else(|_| default_state_dir());
38 let ipc_endpoint =
39 std::env::var("PUNKGO_IPC_ENDPOINT").unwrap_or_else(|_| default_ipc_endpoint());
40 Self {
41 state_dir,
42 ipc_endpoint,
43 }
44 }
45}
46
47fn default_ipc_endpoint() -> String {
58 let pid = std::process::id();
59 if cfg!(windows) {
60 format!(r"\\.\pipe\punkgo-kernel-{pid}")
61 } else {
62 let state_dir = std::env::var("PUNKGO_STATE_DIR")
63 .map(std::path::PathBuf::from)
64 .unwrap_or_else(|_| default_state_dir());
65 state_dir
66 .join(format!("daemon-{pid}.sock"))
67 .to_string_lossy()
68 .into_owned()
69 }
70}
71
72fn default_state_dir() -> PathBuf {
73 if let Some(home) = home_dir() {
74 return home.join(".punkgo").join("state");
75 }
76 PathBuf::from("state")
77}
78
79fn home_dir() -> Option<PathBuf> {
80 if let Some(home) = std::env::var_os("HOME") {
82 return Some(PathBuf::from(home));
83 }
84 if let Some(profile) = std::env::var_os("USERPROFILE") {
86 return Some(PathBuf::from(profile));
87 }
88 let drive = std::env::var_os("HOMEDRIVE")?;
90 let path = std::env::var_os("HOMEPATH")?;
91 let mut p = PathBuf::from(drive);
92 p.push(path);
93 Some(p)
94}
95
96#[derive(Debug, Clone, Serialize)]
102pub struct SubmitReceipt {
103 pub event_id: String,
104 pub log_index: i64,
105 pub event_hash: String,
106 pub reserved_cost: i64,
107 pub settled_cost: i64,
108 pub artifact_hash: Option<String>,
109}
110
111#[derive(Debug, Deserialize)]
112struct ReadQuery {
113 kind: String,
114 actor_id: Option<String>,
115 #[serde(default)]
117 hold_id: Option<String>,
118 limit: Option<i64>,
119 log_index: Option<i64>,
121 tree_size: Option<i64>,
123 old_size: Option<i64>,
125 #[serde(default)]
128 before_index: Option<i64>,
129 #[serde(default)]
132 after_index: Option<i64>,
133 #[serde(default)]
137 requester_id: Option<String>,
138}
139
140pub struct Kernel {
148 state_store: StateStore,
149 energy_ledger: EnergyLedger,
150 event_log: EventLog,
151 audit_log: AuditLog,
152 actor_store: ActorStore,
153 envelope_store: EnvelopeStore,
154 stellar_config: StellarConfig,
155 signing_key: crate::signing::SigningKey,
156}
157
158impl Kernel {
159 pub async fn bootstrap(config: &KernelConfig) -> KernelResult<Self> {
163 let state_store = StateStore::bootstrap(&config.state_dir).await?;
164 let energy_ledger = EnergyLedger::new(state_store.pool());
165 let event_log = EventLog::new(state_store.pool());
166 let signing_key_path = config.state_dir.join("signing_key");
168 let signing_key = crate::signing::SigningKey::load_or_generate(&signing_key_path)
169 .map_err(|e| KernelError::Audit(format!("signing key: {e}")))?;
170 tracing::info!(pubkey = %signing_key.public_key_hex(), "checkpoint signing key loaded");
171 let audit_log = AuditLog::new(
172 state_store.pool(),
173 "punkgo/kernel",
174 Some(signing_key.clone()),
175 );
176 let actor_store = ActorStore::new(state_store.pool());
177 let envelope_store = EnvelopeStore::new(state_store.pool());
178
179 let stellar_config_path = config.state_dir.join("stellar.toml");
181 let stellar_config = load_stellar_config(&stellar_config_path)?;
182 info!(
183 energy_per_tick = stellar_config.effective_energy_per_tick(),
184 int8_tops = stellar_config.int8_tops,
185 tick_interval_ms = stellar_config.tick_interval_ms,
186 "stellar configuration loaded"
187 );
188
189 Ok(Self {
190 state_store,
191 energy_ledger,
192 event_log,
193 audit_log,
194 actor_store,
195 envelope_store,
196 stellar_config,
197 signing_key,
198 })
199 }
200
201 pub fn stellar_config(&self) -> &StellarConfig {
204 &self.stellar_config
205 }
206
207 pub fn energy_ledger(&self) -> &EnergyLedger {
209 &self.energy_ledger
210 }
211
212 pub fn actor_store(&self) -> &ActorStore {
214 &self.actor_store
215 }
216
217 pub fn envelope_store(&self) -> &EnvelopeStore {
219 &self.envelope_store
220 }
221
222 pub fn pool(&self) -> sqlx::SqlitePool {
224 self.state_store.pool()
225 }
226
227 pub async fn handle_request(&self, req: RequestEnvelope) -> ResponseEnvelope {
229 let request_id = req.request_id.clone();
230 info!(
231 request_id = %request_id,
232 request_type = ?req.request_type,
233 "received request"
234 );
235 match self.dispatch(req).await {
236 Ok(payload) => {
237 info!(request_id = %request_id, "request completed");
238 ResponseEnvelope::ok(request_id, payload)
239 }
240 Err(err) => {
241 warn!(error = %err, "request failed");
242 ResponseEnvelope::err_structured(request_id, &err)
243 }
244 }
245 }
246
247 async fn dispatch(&self, req: RequestEnvelope) -> KernelResult<Value> {
248 match req.request_type {
249 RequestType::Quote => {
250 let action: Action = serde_json::from_value(req.payload)?;
251 validate_action(&action)?;
252 let cost = quote_cost(&action);
253 Ok(json!({ "cost": cost }))
254 }
255 RequestType::Submit => {
256 let action: Action = serde_json::from_value(req.payload)?;
257 let receipt = self.submit_action(action).await?;
258 Ok(serde_json::to_value(receipt)?)
259 }
260 RequestType::Read => {
261 let query: ReadQuery = serde_json::from_value(req.payload)?;
262 self.read_query(query).await
263 }
264 }
265 }
266
267 async fn submit_action(&self, action: Action) -> KernelResult<SubmitReceipt> {
268 validate_action(&action)?;
270 if !self.state_store.actor_exists(&action.actor_id).await? {
271 return Err(KernelError::ActorNotFound(action.actor_id.clone()));
272 }
273
274 if let Some(actor) = self.actor_store.get(&action.actor_id).await? {
278 if actor.status == punkgo_core::actor::ActorStatus::Frozen
279 && action.action_type.is_state_changing()
280 {
281 return Err(KernelError::ActorFrozen(format!(
282 "actor {} is frozen and cannot perform state-changing actions",
283 action.actor_id
284 )));
285 }
286 check_writable_boundary(&actor, &action.target, &action.action_type)?;
288
289 if actor.actor_type == punkgo_core::actor::ActorType::Agent
292 && action.action_type.is_state_changing()
293 {
294 lifecycle::check_lineage_active(&self.actor_store, &actor.lineage).await?;
295 }
296
297 if action.action_type.is_state_changing() {
300 let envelope = self
301 .envelope_store
302 .get_active_for_actor(&action.actor_id)
303 .await?;
304
305 let envelope = if let Some(env) = envelope {
307 if consent::is_envelope_expired(&env, now_millis_u64()) {
308 self.envelope_store
309 .set_status(&env.envelope_id, &consent::EnvelopeStatus::Expired)
310 .await?;
311 None
312 } else {
313 Some(env)
314 }
315 } else {
316 None
317 };
318
319 let auth_mode = consent::check_authorization(&actor, envelope.as_ref())?;
320
321 if auth_mode == AuthorizationMode::ManOnTheLoop
323 && let Some(ref env) = envelope
324 {
325 consent::check_envelope_covers(
326 env,
327 &action.target,
328 action.action_type.as_str(),
329 )?;
330
331 if !env.hold_on.is_empty() {
333 if let Some(timeout_secs) = env.hold_timeout_secs {
334 if timeout_secs > 0 {
335 self.expire_timed_out_holds(&env.envelope_id, timeout_secs)
336 .await?;
337 }
338 }
339 }
340
341 let hold_approved = action
346 .payload
347 .get("_hold_approved")
348 .and_then(|v| v.as_bool())
349 .unwrap_or(false);
350 if !hold_approved
351 && consent::check_hold_trigger(
352 &env.hold_on,
353 &action.target,
354 action.action_type.as_str(),
355 )
356 {
357 let hold_id = Uuid::new_v4().to_string();
358
359 let reserved_cost = quote_cost(&action) as i64;
362
363 let hold_payload = json!({
364 "hold_id": &hold_id,
365 "agent_id": &action.actor_id,
366 "trigger": {
367 "target": &action.target,
368 "action_type": action.action_type.as_str()
369 },
370 "pending_action": {
371 "target": &action.target,
372 "action_type": action.action_type.as_str(),
373 "payload": &action.payload
374 },
375 "reserved_cost": reserved_cost,
376 "triggered_at": now_millis_string()
377 });
378
379 let hold_action = Action {
381 actor_id: action.actor_id.clone(),
382 action_type: ActionType::Mutate,
383 target: format!("ledger/hold/{hold_id}"),
384 payload: hold_payload.clone(),
385 timestamp: None,
386 };
387 let mut hold_event = EventRecord {
388 id: Uuid::new_v4().to_string(),
389 log_index: 0,
390 event_hash: String::new(),
391 actor_id: action.actor_id.clone(),
392 action_type: "hold_request".to_string(),
393 target: format!("ledger/hold/{hold_id}"),
394 payload: hold_payload.clone(),
395 payload_hash: payload_hash_hex(&hold_action)?,
396 artifact_hash: None,
397 reserved_energy: reserved_cost,
398 settled_energy: 0,
399 timestamp: now_millis_string(),
400 };
401
402 let pool = self.state_store.pool();
404 let mut tx = pool.begin().await?;
405 if reserved_cost > 0 {
406 self.energy_ledger
407 .reserve_in_tx(&mut tx, &action.actor_id, reserved_cost)
408 .await?;
409 }
410 self.event_log
411 .append_in_tx(&mut tx, &mut hold_event)
412 .await?;
413 self.envelope_store
414 .create_hold_request_in_tx(
415 &mut tx,
416 &NewHoldRequest {
417 hold_id: &hold_id,
418 envelope_id: &env.envelope_id,
419 agent_id: &action.actor_id,
420 trigger_target: &action.target,
421 trigger_action: action.action_type.as_str(),
422 pending_payload: &json!({
423 "target": &action.target,
424 "action_type": action.action_type.as_str(),
425 "payload": &action.payload,
426 "reserved_cost": reserved_cost
427 }),
428 },
429 )
430 .await?;
431 let log_index = hold_event.log_index as u64;
435 self.audit_log
436 .append_leaf_in_tx(&mut tx, log_index, &hold_event.event_hash)
437 .await
438 .map_err(|e| KernelError::Audit(e.to_string()))?;
439 tx.commit().await?;
441
442 return Err(KernelError::HoldTriggered {
443 hold_id,
444 agent_id: action.actor_id.clone(),
445 });
446 }
447 }
448 }
449 }
450
451 let hold_response = if matches!(action.action_type, ActionType::Mutate) {
454 parse_hold_response(&action.target, &action.payload)
455 } else {
456 None
457 };
458
459 if let Some((ref hold_id, ref decision, ref instruction)) = hold_response {
461 return self
462 .execute_hold_response(&action, hold_id, decision, instruction.as_deref())
463 .await;
464 }
465
466 let lifecycle_op = if matches!(action.action_type, ActionType::Mutate) {
468 lifecycle::parse_lifecycle_op(&action.target, &action.payload)
469 } else {
470 None
471 };
472
473 if let Some((ref target_actor_id, ref op)) = lifecycle_op {
475 let initiator = self
476 .actor_store
477 .get(&action.actor_id)
478 .await?
479 .ok_or_else(|| KernelError::ActorNotFound(action.actor_id.clone()))?;
480 let target_actor = self
481 .actor_store
482 .get(target_actor_id)
483 .await?
484 .ok_or_else(|| KernelError::ActorNotFound(target_actor_id.clone()))?;
485 lifecycle::validate_lifecycle_authorization(&initiator, &target_actor, op).await?;
486 }
487
488 let create_actor_spec = parse_create_actor_spec(&action, &self.actor_store).await?;
489 let create_envelope_spec =
490 parse_create_envelope_spec(&action, &self.envelope_store).await?;
491 let policy_version = parse_policy_version(&action);
492
493 let (reserved_cost, reservation) = if let Some(hold_reserved) = action
499 .payload
500 .get("_hold_reserved_cost")
501 .and_then(|v| v.as_i64())
502 {
503 let phantom = if hold_reserved > 0 {
504 Some(EnergyReservation {
505 actor_id: action.actor_id.clone(),
506 reserved: hold_reserved,
507 })
508 } else {
509 None
510 };
511 (hold_reserved, phantom)
512 } else {
513 let cost = quote_cost(&action) as i64;
516 let res = if cost > 0 {
517 Some(self.energy_ledger.reserve(&action.actor_id, cost).await?)
518 } else {
519 None
520 };
521 (cost, res)
522 };
523
524 let artifact_hash = if matches!(action.action_type, ActionType::Execute) {
528 Some(Self::validate_execute_payload(&action.payload)?)
529 } else {
530 None
531 };
532
533 let settled_cost = reserved_cost;
536
537 let mut event = EventRecord {
539 id: Uuid::new_v4().to_string(),
540 log_index: 0,
541 event_hash: String::new(),
542 actor_id: action.actor_id.clone(),
543 action_type: action.action_type.as_str().to_string(),
544 target: action.target.clone(),
545 payload: action.payload.clone(),
546 payload_hash: payload_hash_hex(&action)?,
547 artifact_hash: artifact_hash.clone(),
548 reserved_energy: reserved_cost,
549 settled_energy: settled_cost,
550 timestamp: now_millis_string(),
551 };
552 self.finalize_energy_and_event(
553 reservation.as_ref(),
554 settled_cost,
555 create_actor_spec.as_ref(),
556 create_envelope_spec.as_ref(),
557 &mut event,
558 )
559 .await?;
560
561 if let Some(ref version) = policy_version {
563 if let Err(err) = self.state_store.set_policy_version(version).await {
564 warn!(error = %err, "failed to record policy version after commit");
565 } else {
566 info!(policy_version = %version, event_id = %event.id, "policy version updated");
567 }
568 }
569
570 if action.action_type.is_state_changing()
572 && settled_cost > 0
573 && let Ok(Some(actor)) = self.actor_store.get(&action.actor_id).await
574 && actor.actor_type == ActorType::Agent
575 && let Ok(Some(envelope)) = self
576 .envelope_store
577 .get_active_for_actor(&action.actor_id)
578 .await
579 {
580 let checkpoint = consent::check_checkpoint(&envelope, settled_cost);
582
583 let pool = self.state_store.pool();
585 let mut tx = pool.begin().await?;
586 match self
587 .envelope_store
588 .consume_budget_in_tx(&mut tx, &envelope.envelope_id, settled_cost)
589 .await
590 {
591 Ok(_new_consumed) => {
592 match checkpoint {
594 Some(CheckpointLevel::Halt) => {
595 self.envelope_store
596 .set_status_in_tx(
597 &mut tx,
598 &envelope.envelope_id,
599 &consent::EnvelopeStatus::Expired,
600 )
601 .await?;
602 info!(
603 envelope_id = %envelope.envelope_id,
604 actor_id = %action.actor_id,
605 "checkpoint: HALT — envelope budget exhausted"
606 );
607 }
608 Some(CheckpointLevel::Report) => {
609 info!(
610 envelope_id = %envelope.envelope_id,
611 actor_id = %action.actor_id,
612 budget = envelope.budget,
613 consumed = envelope.budget_consumed + settled_cost,
614 "checkpoint: REPORT — summary logged"
615 );
616 }
617 None => {}
618 }
619 tx.commit().await?;
620 }
621 Err(err) => {
622 warn!(
623 error = %err,
624 "envelope budget consumption failed (event already committed)"
625 );
626 }
628 }
629 }
630
631 if let Some((ref target_actor_id, ref op)) = lifecycle_op {
633 let pool = self.state_store.pool();
634 match op {
635 punkgo_core::actor::LifecycleOp::Freeze { .. } => {
636 match lifecycle::execute_freeze(&self.actor_store, &pool, target_actor_id).await
637 {
638 Ok(frozen_ids) => {
639 info!(
640 target = %target_actor_id,
641 cascade_count = frozen_ids.len(),
642 "lifecycle: freeze executed"
643 );
644 }
645 Err(err) => {
646 warn!(error = %err, "lifecycle: freeze failed (event already committed)");
647 }
648 }
649 }
650 punkgo_core::actor::LifecycleOp::Unfreeze => {
651 match lifecycle::execute_unfreeze(&self.actor_store, &pool, target_actor_id)
652 .await
653 {
654 Ok(()) => {
655 info!(target = %target_actor_id, "lifecycle: unfreeze executed");
656 }
657 Err(err) => {
658 warn!(error = %err, "lifecycle: unfreeze failed (event already committed)");
659 }
660 }
661 }
662 punkgo_core::actor::LifecycleOp::Terminate { .. } => {
663 match lifecycle::execute_freeze(&self.actor_store, &pool, target_actor_id).await
666 {
667 Ok(frozen_ids) => {
668 info!(
669 target = %target_actor_id,
670 cascade_count = frozen_ids.len(),
671 "lifecycle: terminate executed (cascade frozen)"
672 );
673 }
674 Err(err) => {
675 warn!(error = %err, "lifecycle: terminate failed (event already committed)");
676 }
677 }
678 }
679 punkgo_core::actor::LifecycleOp::UpdateEnergyShare { energy_share } => {
680 match lifecycle::execute_update_energy_share(
681 &self.actor_store,
682 &pool,
683 target_actor_id,
684 *energy_share,
685 )
686 .await
687 {
688 Ok(()) => {
689 info!(
690 target = %target_actor_id,
691 energy_share,
692 "lifecycle: energy_share updated"
693 );
694 }
695 Err(err) => {
696 warn!(error = %err, "lifecycle: update_energy_share failed (event already committed)");
697 }
698 }
699 }
700 }
701 }
702
703 Ok(SubmitReceipt {
704 event_id: event.id,
705 log_index: event.log_index,
706 event_hash: event.event_hash,
707 reserved_cost,
708 settled_cost,
709 artifact_hash,
710 })
711 }
712
713 async fn finalize_energy_and_event(
714 &self,
715 reservation: Option<&EnergyReservation>,
716 settled_cost: i64,
717 create_actor: Option<&CreateActorSpec>,
718 create_envelope: Option<&(String, EnvelopeSpec, Option<String>)>,
719 event: &mut EventRecord,
720 ) -> KernelResult<()> {
721 let pool = self.state_store.pool();
722 let mut tx = pool.begin().await?;
723
724 if let Some(res) = reservation {
726 self.energy_ledger
727 .settle_in_tx(&mut tx, &res.actor_id, res.reserved, settled_cost)
728 .await?;
729 }
730
731 if let Some(spec) = create_actor {
733 self.energy_ledger
734 .create_actor_in_tx(&mut tx, &spec.actor_id, spec.energy_balance)
735 .await?;
736 self.actor_store.create_in_tx(&mut tx, spec).await?;
737 info!(
738 created_actor = %spec.actor_id,
739 actor_type = spec.actor_type.as_str(),
740 energy_balance = spec.energy_balance,
741 "actor created in transaction"
742 );
743 }
744
745 if let Some((envelope_id, spec, parent_id)) = create_envelope {
747 self.envelope_store
748 .create_in_tx(&mut tx, envelope_id, spec, parent_id.as_deref())
749 .await?;
750 info!(
751 envelope_id = %envelope_id,
752 actor_id = %spec.actor_id,
753 grantor_id = %spec.grantor_id,
754 budget = spec.budget,
755 "envelope created in transaction"
756 );
757 }
758
759 self.event_log.append_in_tx(&mut tx, event).await?;
761
762 let log_index = event.log_index as u64;
764 self.audit_log
765 .append_leaf_in_tx(&mut tx, log_index, &event.event_hash)
766 .await
767 .map_err(|e| KernelError::Audit(e.to_string()))?;
768
769 tx.commit().await?;
774 info!(event_id = %event.id, log_index = event.log_index, "event committed");
775 Ok(())
776 }
777
778 fn validate_execute_payload(payload: &Value) -> KernelResult<String> {
783 Self::require_valid_oid(payload, "input_oid")?;
784 Self::require_valid_oid(payload, "output_oid")?;
785
786 if payload.get("exit_code").and_then(|v| v.as_i64()).is_none() {
787 return Err(KernelError::ExecutePayloadInvalid(
788 "missing or invalid exit_code (must be integer)".to_string(),
789 ));
790 }
791
792 let artifact_hash = Self::require_valid_oid(payload, "artifact_hash")?;
793 Ok(artifact_hash)
794 }
795
796 fn require_valid_oid(payload: &Value, field: &str) -> KernelResult<String> {
798 let val = payload
799 .get(field)
800 .and_then(|v| v.as_str())
801 .ok_or_else(|| KernelError::ExecutePayloadInvalid(format!("missing {field}")))?;
802
803 if !val.starts_with("sha256:") || val.len() != 71 {
804 return Err(KernelError::ExecutePayloadInvalid(format!(
805 "{field} must be sha256:<64 hex chars>, got: {val}"
806 )));
807 }
808 let hex_part = &val[7..];
809 if !hex_part.chars().all(|c| c.is_ascii_hexdigit()) {
810 return Err(KernelError::ExecutePayloadInvalid(format!(
811 "{field} contains non-hex characters: {val}"
812 )));
813 }
814
815 Ok(val.to_string())
816 }
817
818 async fn read_query(&self, query: ReadQuery) -> KernelResult<Value> {
819 let requester = query.requester_id.as_deref().unwrap_or("anonymous");
824 check_read_access(requester, &query.kind)?;
825
826 match query.kind.as_str() {
827 "health" => Ok(json!({ "status": "ok" })),
828 "actor_energy" => {
829 let actor_id = query.actor_id.ok_or_else(|| {
830 KernelError::InvalidRequest("actor_id is required for actor_energy".to_string())
831 })?;
832 let (energy_balance, reserved_energy) =
833 self.energy_ledger.balance_view(&actor_id).await?;
834 Ok(json!({
835 "actor_id": actor_id,
836 "energy_balance": energy_balance,
837 "reserved_energy": reserved_energy
838 }))
839 }
840 "events" => {
841 let limit = query.limit.unwrap_or(20).clamp(1, 500);
842 let events = self
843 .event_log
844 .query(
845 query.actor_id.as_deref(),
846 query.before_index,
847 query.after_index,
848 limit,
849 )
850 .await?;
851 let next_cursor = events.last().map(|e| e.log_index);
854 let has_more = events.len() as i64 == limit;
855 Ok(json!({
856 "events": events,
857 "has_more": has_more,
858 "next_cursor": next_cursor
859 }))
860 }
861 "stats" => {
862 let event_count = self.event_log.count().await?;
863 Ok(json!({ "event_count": event_count }))
864 }
865 "snapshot" => {
866 let event_count = self.event_log.count().await?;
869 self.audit_log
870 .ensure_checkpoint(event_count as u64)
871 .await
872 .map_err(|e| KernelError::Audit(e.to_string()))?;
873 let cp = self
874 .audit_log
875 .latest_checkpoint()
876 .await
877 .map_err(|e| KernelError::Audit(e.to_string()))?;
878 Ok(json!({
879 "event_count": event_count,
880 "snapshot_hash": cp.root_hash,
881 "generated_at": cp.created_at
882 }))
883 }
884 "paths" => {
885 let paths = self.state_store.paths();
886 Ok(json!({
887 "root": paths.root.display().to_string(),
888 "workspace_root": paths.workspace_root.display().to_string(),
889 "quarantine_root": paths.quarantine_root.display().to_string(),
890 "db_path": paths.db_path.display().to_string()
891 }))
892 }
893 "audit_checkpoint" => {
894 let event_count = self.event_log.count().await?;
895 self.audit_log
896 .ensure_checkpoint(event_count as u64)
897 .await
898 .map_err(|e| KernelError::Audit(e.to_string()))?;
899 let cp = self
900 .audit_log
901 .latest_checkpoint()
902 .await
903 .map_err(|e| KernelError::Audit(e.to_string()))?;
904 Ok(serde_json::to_value(cp)?)
905 }
906 "audit_inclusion_proof" => {
907 let log_index = query.log_index.ok_or_else(|| {
908 KernelError::InvalidRequest(
909 "log_index is required for audit_inclusion_proof".to_string(),
910 )
911 })? as u64;
912 let tree_size = match query.tree_size {
913 Some(s) => s as u64,
914 None => {
915 let event_count = self.event_log.count().await? as u64;
917 self.audit_log
918 .ensure_checkpoint(event_count)
919 .await
920 .map_err(|e| KernelError::Audit(e.to_string()))?;
921 self.audit_log
922 .tree_size()
923 .await
924 .map_err(|e| KernelError::Audit(e.to_string()))?
925 }
926 };
927 let proof = self
928 .audit_log
929 .inclusion_proof(log_index, tree_size)
930 .await
931 .map_err(|e| KernelError::Audit(e.to_string()))?;
932 Ok(json!({
933 "log_index": log_index,
934 "tree_size": tree_size,
935 "proof": proof
936 }))
937 }
938 "audit_consistency_proof" => {
939 let old_size = query.old_size.ok_or_else(|| {
940 KernelError::InvalidRequest(
941 "old_size is required for audit_consistency_proof".to_string(),
942 )
943 })? as u64;
944 let new_size = query.tree_size.ok_or_else(|| {
945 KernelError::InvalidRequest(
946 "tree_size is required for audit_consistency_proof".to_string(),
947 )
948 })? as u64;
949 let proof = self
950 .audit_log
951 .consistency_proof(old_size, new_size)
952 .await
953 .map_err(|e| KernelError::Audit(e.to_string()))?;
954 Ok(json!({
955 "old_size": old_size,
956 "new_size": new_size,
957 "proof": proof
958 }))
959 }
960 "stellar_info" => {
962 let config = &self.stellar_config;
963 Ok(json!({
964 "gpu_model": config.gpu_model,
965 "cpu_model": config.cpu_model,
966 "int8_tops": config.int8_tops,
967 "energy_per_tick": config.effective_energy_per_tick(),
968 "tick_interval_ms": config.tick_interval_ms,
969 "luminosity_source": config.luminosity_source
970 }))
971 }
972 "envelope_info" => {
974 let actor_id = query.actor_id.ok_or_else(|| {
975 KernelError::InvalidRequest(
976 "actor_id is required for envelope_info".to_string(),
977 )
978 })?;
979 let envelope = self.envelope_store.get_active_for_actor(&actor_id).await?;
980 match envelope {
981 Some(record) => Ok(serde_json::to_value(record)?),
982 None => Ok(json!({ "actor_id": actor_id, "envelope": null })),
983 }
984 }
985 "actor_info" => {
987 let actor_id = query.actor_id.ok_or_else(|| {
988 KernelError::InvalidRequest("actor_id is required for actor_info".to_string())
989 })?;
990 let actor = self.actor_store.get(&actor_id).await?;
991 match actor {
992 Some(record) => Ok(serde_json::to_value(record)?),
993 None => Err(KernelError::ActorNotFound(actor_id)),
994 }
995 }
996 "hold_info" => {
998 let hold_id = query.hold_id.ok_or_else(|| {
999 KernelError::InvalidRequest("hold_id is required for hold_info".to_string())
1000 })?;
1001 let hold = self.envelope_store.get_hold_request(&hold_id).await?;
1002 match hold {
1003 Some(record) => Ok(record),
1004 None => Err(KernelError::InvalidRequest(format!(
1005 "hold_request not found: {hold_id}"
1006 ))),
1007 }
1008 }
1009 "holds_pending" => {
1011 let holds = self
1012 .envelope_store
1013 .list_pending_holds(query.actor_id.as_deref())
1014 .await?;
1015 Ok(json!({ "holds": holds }))
1016 }
1017 "signing_pubkey" => Ok(json!({
1019 "pubkey_hex": self.signing_key.public_key_hex(),
1020 "algorithm": "ed25519"
1021 })),
1022 other => Err(KernelError::InvalidRequest(format!(
1023 "unsupported read query kind: {other}"
1024 ))),
1025 }
1026 }
1027}
1028
1029fn now_millis_string() -> String {
1030 let now = std::time::SystemTime::now()
1031 .duration_since(std::time::UNIX_EPOCH)
1032 .unwrap_or_default();
1033 now.as_millis().to_string()
1034}
1035
1036fn now_millis_u64() -> u64 {
1037 std::time::SystemTime::now()
1038 .duration_since(std::time::UNIX_EPOCH)
1039 .unwrap_or_default()
1040 .as_millis() as u64
1041}
1042
1043fn parse_policy_version(action: &Action) -> Option<String> {
1046 if !matches!(action.action_type, ActionType::Create) || action.target != "system/policy" {
1047 return None;
1048 }
1049 action
1050 .payload
1051 .get("version")
1052 .and_then(|v| v.as_str())
1053 .map(|s| s.to_string())
1054}
1055
1056async fn parse_create_actor_spec(
1063 action: &Action,
1064 actor_store: &ActorStore,
1065) -> KernelResult<Option<CreateActorSpec>> {
1066 if !matches!(action.action_type, ActionType::Create) || action.target != "ledger/actor" {
1067 return Ok(None);
1068 }
1069
1070 let payload = action.payload.as_object().ok_or_else(|| {
1071 KernelError::InvalidRequest("seed actor payload must be an object".to_string())
1072 })?;
1073
1074 let explicit_id = payload.get("actor_id").and_then(Value::as_str);
1076 let purpose = payload
1077 .get("purpose")
1078 .and_then(Value::as_str)
1079 .unwrap_or("legacy");
1080 let energy_balance = payload
1081 .get("energy_balance")
1082 .and_then(Value::as_i64)
1083 .unwrap_or(1000);
1084
1085 let actor_type_str = payload
1087 .get("actor_type")
1088 .and_then(Value::as_str)
1089 .unwrap_or("agent");
1090 let actor_type = ActorType::parse(actor_type_str).ok_or_else(|| {
1091 KernelError::InvalidRequest(format!("invalid actor_type: {actor_type_str}"))
1092 })?;
1093
1094 let actor_id = if let Some(id) = explicit_id {
1096 id.to_string()
1097 } else {
1098 if purpose == "legacy" {
1099 return Err(KernelError::InvalidRequest(
1100 "actor_id or purpose is required to create an actor".to_string(),
1101 ));
1102 }
1103 let seq = actor_store.next_sequence(&action.actor_id, purpose).await?;
1104 derive_agent_id(&action.actor_id, purpose, seq)
1105 };
1106
1107 let creator_record = actor_store.get(&action.actor_id).await?;
1109 let (creator_type, creator_lineage) = match &creator_record {
1110 Some(record) => (record.actor_type.clone(), record.lineage.clone()),
1111 None => (ActorType::Human, vec![]),
1113 };
1114
1115 if creator_type == ActorType::Agent && actor_type == ActorType::Agent {
1117 return Err(KernelError::PolicyViolation(
1118 "agents cannot create agents — creation right belongs to humans (PIP-001 §5)"
1119 .to_string(),
1120 ));
1121 }
1122
1123 let lineage = build_lineage(&creator_type, &action.actor_id, &creator_lineage);
1124
1125 let writable_targets: Vec<WritableTarget> = if let Some(targets_val) =
1127 payload.get("writable_targets")
1128 {
1129 serde_json::from_value(targets_val.clone())
1130 .map_err(|e| KernelError::InvalidRequest(format!("invalid writable_targets: {e}")))?
1131 } else {
1132 vec![]
1133 };
1134
1135 if !writable_targets.is_empty()
1137 && let Some(ref creator) = creator_record
1138 {
1139 validate_child_targets(creator, &writable_targets)?;
1140 }
1141
1142 let energy_share = payload
1144 .get("energy_share")
1145 .and_then(Value::as_f64)
1146 .unwrap_or(0.0);
1147
1148 let reduction_policy = payload
1149 .get("reduction_policy")
1150 .and_then(Value::as_str)
1151 .unwrap_or("none")
1152 .to_string();
1153
1154 Ok(Some(CreateActorSpec {
1155 actor_id,
1156 actor_type,
1157 creator_id: action.actor_id.clone(),
1158 lineage,
1159 purpose: Some(purpose.to_string()),
1160 writable_targets,
1161 energy_balance,
1162 energy_share,
1163 reduction_policy,
1164 }))
1165}
1166
1167async fn parse_create_envelope_spec(
1176 action: &Action,
1177 envelope_store: &EnvelopeStore,
1178) -> KernelResult<Option<(String, EnvelopeSpec, Option<String>)>> {
1179 if !matches!(action.action_type, ActionType::Create) || action.target != "ledger/envelope" {
1180 return Ok(None);
1181 }
1182
1183 let payload = action.payload.as_object().ok_or_else(|| {
1184 KernelError::InvalidRequest("envelope payload must be an object".to_string())
1185 })?;
1186
1187 let actor_id = payload
1188 .get("actor_id")
1189 .and_then(Value::as_str)
1190 .ok_or_else(|| {
1191 KernelError::InvalidRequest("actor_id is required to create an envelope".to_string())
1192 })?
1193 .to_string();
1194
1195 let budget = payload
1196 .get("budget")
1197 .and_then(Value::as_i64)
1198 .ok_or_else(|| {
1199 KernelError::InvalidRequest("budget is required to create an envelope".to_string())
1200 })?;
1201
1202 if budget <= 0 {
1203 return Err(KernelError::InvalidRequest(
1204 "envelope budget must be positive".to_string(),
1205 ));
1206 }
1207
1208 let targets: Vec<String> = if let Some(targets_val) = payload.get("targets") {
1209 serde_json::from_value(targets_val.clone())
1210 .map_err(|e| KernelError::InvalidRequest(format!("invalid envelope targets: {e}")))?
1211 } else {
1212 return Err(KernelError::InvalidRequest(
1213 "targets are required to create an envelope".to_string(),
1214 ));
1215 };
1216
1217 let actions: Vec<String> = if let Some(actions_val) = payload.get("actions") {
1218 serde_json::from_value(actions_val.clone())
1219 .map_err(|e| KernelError::InvalidRequest(format!("invalid envelope actions: {e}")))?
1220 } else {
1221 return Err(KernelError::InvalidRequest(
1222 "actions are required to create an envelope".to_string(),
1223 ));
1224 };
1225
1226 let duration_secs = payload.get("duration_secs").and_then(Value::as_i64);
1227 let report_every = payload.get("report_every").and_then(Value::as_i64);
1228 let hold_timeout_secs = payload.get("hold_timeout_secs").and_then(Value::as_i64);
1229
1230 let hold_on: Vec<HoldRule> = if let Some(hold_on_val) = payload.get("hold_on") {
1232 serde_json::from_value(hold_on_val.clone())
1233 .map_err(|e| KernelError::InvalidRequest(format!("invalid hold_on rules: {e}")))?
1234 } else {
1235 vec![]
1236 };
1237
1238 let spec = EnvelopeSpec {
1239 actor_id,
1240 grantor_id: action.actor_id.clone(),
1241 budget,
1242 targets,
1243 actions,
1244 duration_secs,
1245 report_every,
1246 hold_on,
1247 hold_timeout_secs,
1248 };
1249
1250 let parent_envelope_id = if let Some(grantor_envelope) = envelope_store
1252 .get_active_for_actor(&action.actor_id)
1253 .await?
1254 {
1255 consent::validate_envelope_reduction(&grantor_envelope, &spec)?;
1256 Some(grantor_envelope.envelope_id)
1257 } else {
1258 None
1259 };
1260
1261 let envelope_id = Uuid::new_v4().to_string();
1262
1263 Ok(Some((envelope_id, spec, parent_envelope_id)))
1264}
1265
1266fn parse_hold_response(target: &str, payload: &Value) -> Option<(String, String, Option<String>)> {
1274 let rest = target.strip_prefix("ledger/hold/")?;
1276 if rest.is_empty() || rest.contains('/') {
1278 return None;
1279 }
1280 let hold_id = rest.to_string();
1281
1282 let decision = payload.get("decision").and_then(Value::as_str)?.to_string();
1283
1284 if decision != "approve" && decision != "reject" {
1286 return None;
1287 }
1288
1289 let instruction = payload
1290 .get("instruction")
1291 .and_then(Value::as_str)
1292 .map(|s| s.to_string());
1293
1294 Some((hold_id, decision, instruction))
1295}
1296
1297impl Kernel {
1298 async fn execute_hold_response(
1308 &self,
1309 human_action: &Action,
1310 hold_id: &str,
1311 decision: &str,
1312 instruction: Option<&str>,
1313 ) -> KernelResult<SubmitReceipt> {
1314 let caller = self
1316 .actor_store
1317 .get(&human_action.actor_id)
1318 .await?
1319 .ok_or_else(|| KernelError::ActorNotFound(human_action.actor_id.clone()))?;
1320
1321 if caller.actor_type != punkgo_core::actor::ActorType::Human {
1322 return Err(KernelError::PolicyViolation(format!(
1323 "only Human actors can resolve holds; caller {} is {:?}",
1324 human_action.actor_id, caller.actor_type
1325 )));
1326 }
1327
1328 let hold_record = self
1330 .envelope_store
1331 .get_hold_request(hold_id)
1332 .await?
1333 .ok_or_else(|| {
1334 KernelError::PolicyViolation(format!("hold_request not found: {hold_id}"))
1335 })?;
1336
1337 let _envelope_id = hold_record
1338 .get("envelope_id")
1339 .and_then(Value::as_str)
1340 .ok_or_else(|| {
1341 KernelError::PolicyViolation("hold_request missing envelope_id".to_string())
1342 })?
1343 .to_string();
1344
1345 let agent_id = hold_record
1346 .get("agent_id")
1347 .and_then(Value::as_str)
1348 .ok_or_else(|| {
1349 KernelError::PolicyViolation("hold_request missing agent_id".to_string())
1350 })?
1351 .to_string();
1352
1353 let pending_payload = hold_record
1354 .get("pending_payload")
1355 .cloned()
1356 .unwrap_or(Value::Null);
1357
1358 let trigger_target = hold_record
1359 .get("trigger_target")
1360 .and_then(Value::as_str)
1361 .unwrap_or("")
1362 .to_string();
1363
1364 let trigger_action = hold_record
1365 .get("trigger_action")
1366 .and_then(Value::as_str)
1367 .unwrap_or("")
1368 .to_string();
1369
1370 let status = hold_record
1371 .get("status")
1372 .and_then(Value::as_str)
1373 .unwrap_or("");
1374 if status != "pending" {
1375 return Err(KernelError::PolicyViolation(format!(
1376 "hold_request {hold_id} is already resolved (status={status})"
1377 )));
1378 }
1379
1380 let envelope_id_str = hold_record
1383 .get("envelope_id")
1384 .and_then(Value::as_str)
1385 .unwrap_or("")
1386 .to_string();
1387 if let Ok(Some(env)) = self.envelope_store.get(&envelope_id_str).await {
1388 if let Some(timeout_secs) = env.hold_timeout_secs {
1389 if timeout_secs > 0 {
1390 let triggered_at: u64 = hold_record
1391 .get("triggered_at")
1392 .and_then(|v| v.as_str())
1393 .and_then(|s| s.parse().ok())
1394 .unwrap_or(0);
1395 let now = now_millis_u64();
1396 if now.saturating_sub(triggered_at) > (timeout_secs as u64) * 1000 {
1397 self.expire_timed_out_holds(&envelope_id_str, timeout_secs)
1399 .await?;
1400 return Err(KernelError::PolicyViolation(format!(
1401 "hold_request {hold_id} has timed out and was auto-rejected"
1402 )));
1403 }
1404 }
1405 }
1406 }
1407
1408 let response_payload = json!({
1410 "hold_id": hold_id,
1411 "agent_id": &agent_id,
1412 "decision": decision,
1413 "instruction": instruction,
1414 "trigger": {
1415 "target": &trigger_target,
1416 "action_type": &trigger_action
1417 },
1418 "resolved_by": &human_action.actor_id,
1419 "resolved_at": now_millis_string()
1420 });
1421
1422 let response_action = Action {
1423 actor_id: human_action.actor_id.clone(),
1424 action_type: ActionType::Mutate,
1425 target: format!("ledger/hold/{hold_id}"),
1426 payload: response_payload.clone(),
1427 timestamp: None,
1428 };
1429
1430 let pool = self.state_store.pool();
1433 let mut tx = pool.begin().await?;
1434
1435 self.envelope_store
1436 .resolve_hold_request_in_tx(&mut tx, hold_id, decision, instruction)
1437 .await?;
1438
1439 let hold_reserved_cost = pending_payload
1442 .get("reserved_cost")
1443 .and_then(|v| v.as_i64())
1444 .unwrap_or(0);
1445
1446 let commitment_cost = if decision == "reject" && hold_reserved_cost > 0 {
1447 let cost = ((hold_reserved_cost as f64) * 0.2).ceil() as i64;
1448 self.energy_ledger
1449 .settle_in_tx(&mut tx, &agent_id, hold_reserved_cost, cost)
1450 .await?;
1451 cost
1452 } else {
1453 0
1454 };
1455
1456 let mut response_event = EventRecord {
1457 id: Uuid::new_v4().to_string(),
1458 log_index: 0,
1459 event_hash: String::new(),
1460 actor_id: human_action.actor_id.clone(),
1461 action_type: "hold_response".to_string(),
1462 target: format!("ledger/hold/{hold_id}"),
1463 payload: response_payload.clone(),
1464 payload_hash: payload_hash_hex(&response_action)?,
1465 artifact_hash: None,
1466 reserved_energy: hold_reserved_cost,
1467 settled_energy: commitment_cost,
1468 timestamp: now_millis_string(),
1469 };
1470 self.event_log
1471 .append_in_tx(&mut tx, &mut response_event)
1472 .await?;
1473
1474 let log_index = response_event.log_index as u64;
1476 self.audit_log
1477 .append_leaf_in_tx(&mut tx, log_index, &response_event.event_hash)
1478 .await
1479 .map_err(|e| KernelError::Audit(e.to_string()))?;
1480 tx.commit().await?;
1482
1483 info!(
1484 hold_id = %hold_id,
1485 decision = %decision,
1486 agent_id = %agent_id,
1487 resolved_by = %human_action.actor_id,
1488 "PIP-001 §11d: hold resolved"
1489 );
1490
1491 if decision == "approve" {
1493 let orig_target = pending_payload
1495 .get("target")
1496 .and_then(Value::as_str)
1497 .unwrap_or(&trigger_target)
1498 .to_string();
1499 let orig_action_type_str = pending_payload
1500 .get("action_type")
1501 .and_then(Value::as_str)
1502 .unwrap_or(&trigger_action)
1503 .to_string();
1504 let mut orig_payload = pending_payload
1505 .get("payload")
1506 .cloned()
1507 .unwrap_or(Value::Null);
1508
1509 if let Some(obj) = orig_payload.as_object_mut() {
1516 obj.insert("_hold_approved".to_string(), Value::Bool(true));
1517 obj.insert(
1518 "_hold_reserved_cost".to_string(),
1519 Value::Number(hold_reserved_cost.into()),
1520 );
1521 if let Some(instr) = instruction {
1523 obj.insert(
1524 "_hold_instruction".to_string(),
1525 Value::String(instr.to_string()),
1526 );
1527 }
1528 } else {
1529 let mut obj = serde_json::Map::new();
1531 obj.insert("_hold_approved".to_string(), Value::Bool(true));
1532 obj.insert(
1533 "_hold_reserved_cost".to_string(),
1534 Value::Number(hold_reserved_cost.into()),
1535 );
1536 if let Some(instr) = instruction {
1537 obj.insert(
1538 "_hold_instruction".to_string(),
1539 Value::String(instr.to_string()),
1540 );
1541 }
1542 orig_payload = Value::Object(obj);
1543 }
1544
1545 let orig_action_type = match orig_action_type_str.as_str() {
1546 "observe" => ActionType::Observe,
1547 "create" => ActionType::Create,
1548 "mutate" => ActionType::Mutate,
1549 _ => ActionType::Execute,
1550 };
1551
1552 let pending_action = Action {
1553 actor_id: agent_id.clone(),
1554 action_type: orig_action_type,
1555 target: orig_target,
1556 payload: orig_payload,
1557 timestamp: None,
1558 };
1559
1560 info!(
1561 hold_id = %hold_id,
1562 agent_id = %agent_id,
1563 "PIP-001 §11d: re-executing approved pending action"
1564 );
1565
1566 return Box::pin(self.submit_action(pending_action)).await;
1570 }
1571
1572 Ok(SubmitReceipt {
1574 event_id: response_event.id,
1575 log_index: response_event.log_index,
1576 event_hash: response_event.event_hash,
1577 reserved_cost: hold_reserved_cost,
1578 settled_cost: commitment_cost,
1579 artifact_hash: None,
1580 })
1581 }
1582
1583 async fn expire_timed_out_holds(
1590 &self,
1591 envelope_id: &str,
1592 hold_timeout_secs: i64,
1593 ) -> KernelResult<()> {
1594 let holds = self
1595 .envelope_store
1596 .list_pending_holds_for_envelope(envelope_id)
1597 .await?;
1598 let now = now_millis_u64();
1599
1600 for hold in holds {
1601 let triggered_at: u64 = hold
1602 .get("triggered_at")
1603 .and_then(|v| v.as_str())
1604 .and_then(|s| s.parse().ok())
1605 .unwrap_or(0);
1606
1607 if now.saturating_sub(triggered_at) <= (hold_timeout_secs as u64) * 1000 {
1608 continue;
1609 }
1610
1611 let hold_id = hold.get("hold_id").and_then(|v| v.as_str()).unwrap_or("");
1613 let agent_id = hold.get("agent_id").and_then(|v| v.as_str()).unwrap_or("");
1614 let pending_payload = hold.get("pending_payload").cloned().unwrap_or(Value::Null);
1615 let reserved_cost = pending_payload
1616 .get("reserved_cost")
1617 .and_then(|v| v.as_i64())
1618 .unwrap_or(0);
1619
1620 let commitment_cost = if reserved_cost > 0 {
1621 ((reserved_cost as f64) * 0.2).ceil() as i64
1622 } else {
1623 0
1624 };
1625
1626 let timeout_payload = json!({
1627 "hold_id": hold_id,
1628 "agent_id": agent_id,
1629 "decision": "timed_out",
1630 "reserved_cost": reserved_cost,
1631 "commitment_cost": commitment_cost,
1632 "triggered_at": triggered_at.to_string(),
1633 "expired_at": now.to_string()
1634 });
1635 let timeout_action = Action {
1636 actor_id: agent_id.to_string(),
1637 action_type: ActionType::Mutate,
1638 target: format!("ledger/hold/{hold_id}"),
1639 payload: timeout_payload.clone(),
1640 timestamp: None,
1641 };
1642 let mut timeout_event = EventRecord {
1643 id: Uuid::new_v4().to_string(),
1644 log_index: 0,
1645 event_hash: String::new(),
1646 actor_id: agent_id.to_string(),
1647 action_type: "hold_timeout".to_string(),
1648 target: format!("ledger/hold/{hold_id}"),
1649 payload: timeout_payload,
1650 payload_hash: payload_hash_hex(&timeout_action)?,
1651 artifact_hash: None,
1652 reserved_energy: reserved_cost,
1653 settled_energy: commitment_cost,
1654 timestamp: now_millis_string(),
1655 };
1656
1657 let pool = self.state_store.pool();
1658 let mut tx = pool.begin().await?;
1659 self.envelope_store
1660 .resolve_hold_request_in_tx(&mut tx, hold_id, "timed_out", None)
1661 .await?;
1662 if reserved_cost > 0 {
1663 self.energy_ledger
1664 .settle_in_tx(&mut tx, agent_id, reserved_cost, commitment_cost)
1665 .await?;
1666 }
1667 self.event_log
1668 .append_in_tx(&mut tx, &mut timeout_event)
1669 .await?;
1670
1671 let t_log_index = timeout_event.log_index as u64;
1673 self.audit_log
1674 .append_leaf_in_tx(&mut tx, t_log_index, &timeout_event.event_hash)
1675 .await
1676 .map_err(|e| KernelError::Audit(e.to_string()))?;
1677 tx.commit().await?;
1679
1680 info!(
1681 hold_id = %hold_id,
1682 agent_id = %agent_id,
1683 commitment_cost = commitment_cost,
1684 "PIP-001 §11e: hold auto-expired (timeout)"
1685 );
1686 }
1687 Ok(())
1688 }
1689}