⎇ 94ea0eaa539cf93d58167ea1687b5a242b196573
crates/runtime/sandokan-core/src/intent.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
feat(sandokan): CLI de prueba + fix wire serialization
1//! Qué orquestar: la intención de ejecución y su contexto.
2
3use brahman_card::Card;
4use serde::{Deserialize, Serialize};
5use std::time::{Duration, SystemTime};
6use ulid::Ulid;
7
8/// Nivel de aislamiento pedido para una encarnación.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
10pub enum IsolationLevel {
11 /// Sin sandbox — mismo namespace que el orquestador.
12 None,
13 /// Namespaces estándar (pid/mount/net/...) según `Card.soma`.
14 #[default]
15 Standard,
16 /// Namespaces + rootfs aislado (`pivot_root` + OverlayFS).
17 Sealed,
18}
19
20/// Contexto de ejecución: ajustes sobre cómo encarnar la Card.
21#[derive(Debug, Clone, Default, Serialize, Deserialize)]
22pub struct ExecContext {
23 /// Aislamiento pedido. `None` = derivar de `Card.soma`.
24 pub isolation: Option<IsolationLevel>,
25 /// Variables de entorno adicionales (sobre las del Card).
26 pub env: Vec<(String, String)>,
27 /// Time-to-live opcional: si se setea, el orquestador detiene la
28 /// entidad al vencer.
29 pub ttl: Option<Duration>,
30}
31
32/// Una intención de ejecución: la `Card` a encarnar + su contexto.
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct Intent {
35 /// La Card se serializa vía `WireCard` (proyección postcard-friendly):
36 /// el campo `extensions` de `Card` usa `#[serde(flatten)]`, que no es
37 /// compatible con formatos no auto-descriptivos como postcard.
38 #[serde(with = "card_wire")]
39 pub card: Card,
40 #[serde(default)]
41 pub context: ExecContext,
42}
43
44/// Serde adapter: `Card` ↔ `WireCard` en el límite de serialización.
45/// Las `extensions` locales de la Card se descartan al cruzar el wire.
46mod card_wire {
47 use brahman_card::{Card, WireCard};
48 use serde::{Deserialize, Deserializer, Serialize, Serializer};
49
50 pub fn serialize<S: Serializer>(card: &Card, s: S) -> Result<S::Ok, S::Error> {
51 WireCard::from(card.clone()).serialize(s)
52 }
53
54 pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Card, D::Error> {
55 Ok(Card::from(WireCard::deserialize(d)?))
56 }
57}
58
59impl Intent {
60 /// Intención mínima: encarnar una Card con el contexto por defecto.
61 pub fn new(card: Card) -> Self {
62 Self { card, context: ExecContext::default() }
63 }
64
65 /// Builder: fija el nivel de aislamiento.
66 pub fn with_isolation(mut self, level: IsolationLevel) -> Self {
67 self.context.isolation = Some(level);
68 self
69 }
70
71 /// Builder: fija un TTL.
72 pub fn with_ttl(mut self, ttl: Duration) -> Self {
73 self.context.ttl = Some(ttl);
74 self
75 }
76
77 /// El id de la Card que esta intención encarna.
78 pub fn card_id(&self) -> Ulid {
79 self.card.id
80 }
81}
82
83/// Referencia a una entidad encarnada por el orquestador.
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85pub struct ExecHandle {
86 /// Id de la Card encarnada (identidad estable en el fractal).
87 pub card_id: Ulid,
88 /// Label humano-legible (copiado de `Card.label`).
89 pub label: String,
90 /// Cuándo arrancó.
91 pub started_at: SystemTime,
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn isolation_default_is_standard() {
100 assert_eq!(IsolationLevel::default(), IsolationLevel::Standard);
101 }
102
103 #[test]
104 fn intent_builders_compose() {
105 let card = Card::new("demo");
106 let intent = Intent::new(card)
107 .with_isolation(IsolationLevel::Sealed)
108 .with_ttl(Duration::from_secs(30));
109 assert_eq!(intent.context.isolation, Some(IsolationLevel::Sealed));
110 assert_eq!(intent.context.ttl, Some(Duration::from_secs(30)));
111 }
112}