//! `brahman-card` — Tarjeta de Presentación canónica de Brahman. //! //! Híbrida del `EntityCard` de arje (identidad ULID, capacidades tipadas, //! `Payload`/`SomaSpec`/`Supervision`/`genesis` recursivo) con flujos tipados, //! permisos enumerados explícitos y nivel de confianza derivado del modelo //! que veníamos diseñando en `core_protocol`. Una sola tarjeta sirve a: //! //! - **El Init** (encarnación): `payload` + `soma` + `supervision` + `genesis`. //! - **El Admin** (matching): `provides`/`requires` + `flow` + `permissions`. //! - **El runtime** (sandbox): `permissions` enumerados → seccomp / namespaces. //! //! Forward-compat: cualquier campo desconocido se preserva en `extensions` //! (raíz) o en `extra` (sub-secciones). //! //! Formatos soportados: JSON (canónico, compatible con arje) y TOML //! (humano-legible). Auto-detección por extensión. #![forbid(unsafe_code)] #![warn(rust_2018_idioms)] use std::collections::{BTreeMap, BTreeSet, HashSet}; use std::path::Path; use std::time::Duration; use serde::{Deserialize, Serialize}; use serde_json::Value as JsonValue; use thiserror::Error; use ulid::Ulid; // Re-export para que los consumidores no necesiten depender de `ulid` // directamente. pub use ::ulid; /// Versión del esquema de la Card. pub const CARD_SCHEMA_VERSION: u16 = 1; /// Versión del protocolo Brahman. pub const PROTOCOL_VERSION: &str = "0.1.0"; /// Errores de parseo o validación de la Card. #[derive(Debug, Error)] pub enum CardError { #[error("schema version mismatch: got {got}, expected {expected}")] SchemaMismatch { got: u16, expected: u16 }, #[error("label vacío")] EmptyLabel, #[error("label demasiado largo: {0} bytes (máx 256)")] LabelTooLong(usize), #[error("capacidad presente en provides Y requires: {0:?}")] SelfDependency(Capability), #[error("payload Native/Legacy con exec vacío")] EmptyExec, #[error("payload Wasm con sha256 sentinela (todo ceros)")] SentinelWasmHash, #[error("rlimit inválido: {0}")] InvalidRlimit(&'static str), #[error("cgroup weight fuera de [1,10000]: {0}")] InvalidCgroupWeight(&'static str), #[error("flujo {section}: nombre duplicado '{name}'")] DuplicateFlowName { section: &'static str, name: String, }, #[error("JSON inválido: {0}")] Json(#[from] serde_json::Error), #[error("TOML inválido: {0}")] Toml(#[from] toml::de::Error), #[error("E/S leyendo card: {0}")] Io(#[from] std::io::Error), #[error("formato desconocido (extensiones esperadas: .json, .toml)")] UnknownFormat, } // ===================================================================== // Card raíz // ===================================================================== /// Tarjeta de Presentación de un módulo Brahman. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Card { /// Versión del esquema. Cambiar = romper compatibilidad del fractal. pub schema_version: u16, /// Identidad opaca, única en el grafo del fractal. pub id: Ulid, /// Ancestro del que esta Card desciende (genealogía). #[serde(default)] pub lineage: Option, /// Nombre humano-legible. Único por convención, no por validación. pub label: String, /// Capacidades del sistema que esta Card ofrece a otros. #[serde(default)] pub provides: BTreeSet, /// Capacidades que necesita resolver el Init antes de encarnarla. #[serde(default)] pub requires: BTreeSet, /// Permisos sandbox declarativos (más alto nivel que `Capability`). /// El Admin los compila a seccomp/namespaces/cgroups concretos. #[serde(default)] pub permissions: Permissions, /// Spec runtime Linux (namespaces, cgroups, rlimits, cpu_affinity). #[serde(default)] pub soma: SomaSpec, /// Qué encarnar: WASM, ELF nativo, virtual, o legacy con shims. pub payload: Payload, /// Política de supervisión (restart con backoff, oneshot, delegada). pub supervision: Supervision, /// Modelo de ejecución (eje ortogonal a `supervision`). #[serde(default)] pub lifecycle: Lifecycle, /// Prioridad de scheduling. #[serde(default)] pub priority: Priority, /// Contratos de flujo de datos: qué consume, qué produce. #[serde(default)] pub flow: Flows, /// Hijas a instanciar inmediatamente al encarnar esta Card. #[serde(default)] pub genesis: Vec, /// Campos JSON/TOML desconocidos preservados durante I/O de archivos /// (forward-compat). **No se transmiten por wire (postcard)** — la /// proyección a [`WireCard`] los descarta porque `serde_json::Value` /// no es postcard-friendly. Sirven para anotaciones locales que /// sobreviven leer/escribir Cards en disco. #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")] pub extensions: BTreeMap, } impl Default for Card { /// Default razonable para `..Default::default()` en struct-literals. /// `id` queda en `Ulid::nil()` y `label` vacío — el consumidor debe /// sobreescribirlos antes de validar. fn default() -> Self { Self { schema_version: CARD_SCHEMA_VERSION, id: Ulid::nil(), lineage: None, label: String::new(), provides: BTreeSet::new(), requires: BTreeSet::new(), permissions: Permissions::default(), soma: SomaSpec::default(), payload: Payload::Virtual, supervision: Supervision::OneShot, lifecycle: Lifecycle::default(), priority: Priority::default(), flow: Flows::default(), genesis: Vec::new(), extensions: BTreeMap::new(), } } } // ===================================================================== // Capacidades — heredadas de arje, tipadas, no strings // ===================================================================== /// Capacidad del sistema. Identificadores tipados, no strings libres. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub enum Capability { /// Provee un punto de montaje root para Cards hijas. FilesystemRoot, /// Acceso a una familia netlink del kernel. KernelNetlink(NetlinkFamily), /// Endpoint del bus interno — equivalente tipado de un nombre D-Bus. Endpoint { interface: InterfaceId, version: u16, }, /// Reemplazo del shim de systemd-logind. Solo el ente compat lo provee. LegacyLogind, /// Acceso crudo a una clase de dispositivo. Capacidad escalada. Device { class: DeviceClass }, /// Permiso de instanciar Cards hijas. Por defecto solo PID 1 lo tiene. Spawn, /// Acceso a logging estructurado del fractal. Journal, } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub enum NetlinkFamily { Uevent, Route, Generic, Audit, } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub enum DeviceClass { Block, Tty, Input, Drm, Net, Hidraw, } /// Identificador de interfaz del bus interno (UUID, no string libre). /// Para extender el protocolo, se genera un UUID nuevo y se versiona. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub struct InterfaceId(pub [u8; 16]); // ===================================================================== // Permisos sandbox — más alto nivel que Capability // ===================================================================== /// Permisos declarativos. El Admin los traduce a seccomp/namespaces. #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct Permissions { #[serde(default)] pub networking: NetworkingPolicy, #[serde(default)] pub filesystem: FsPolicy, #[serde(default)] pub ipc: IpcPolicy, /// Capacidad de spawnear sub-procesos. Implica `TrustLevel::System`. #[serde(default)] pub processes: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum NetworkingPolicy { #[default] None, Loopback, Outbound, Full, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum FsPolicy { #[default] None, ReadOnly, ReadWrite, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct IpcPolicy { /// Protocolos IPC permitidos (p. ej. `"wit-v1"`, `"shm-v1"`). #[serde(default)] pub allow: Vec, } // ===================================================================== // SomaSpec — runtime Linux (heredado de arje sin cambios) // ===================================================================== #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct SomaSpec { pub namespaces: NamespaceSet, pub rlimits: ResourceLimits, pub cgroup: CgroupSpec, pub cpu_affinity: Option>, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct NamespaceSet { pub mount: bool, pub pid: bool, pub net: bool, pub uts: bool, pub ipc: bool, pub user: bool, pub cgroup: bool, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct ResourceLimits { pub mem_bytes: Option, pub nproc: Option, pub nofile: Option, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct CgroupSpec { #[serde(default)] pub path: String, pub cpu_weight: Option, pub io_weight: Option, } // ===================================================================== // Payload — qué encarnar (heredado de arje) // ===================================================================== #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Payload { Wasm { module_sha256: [u8; 32], entry: String, }, Native { exec: String, argv: Vec, envp: Vec<(String, String)>, }, /// Sin proceso. Nodo lógico del grafo (agregadores, mediators). Virtual, /// Wrapper de daemon legacy. `fakes` activa shims D-Bus / sd_notify. Legacy { exec: String, argv: Vec, fakes: BTreeSet, }, } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub enum LegacyFacade { SystemdLogind, SystemdHostnamed, SystemdNotify, } // ===================================================================== // Supervisión (heredada de arje) // ===================================================================== #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Supervision { Restart { #[serde(with = "duration_millis")] initial: Duration, #[serde(with = "duration_millis")] max: Duration, }, OneShot, Delegate, } mod duration_millis { use serde::{Deserialize, Deserializer, Serializer}; use std::time::Duration; pub fn serialize(d: &Duration, s: S) -> Result { s.serialize_u64(d.as_millis() as u64) } pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result { let ms = u64::deserialize(d)?; Ok(Duration::from_millis(ms)) } } // ===================================================================== // Lifecycle / Priority (del modelo brahman) // ===================================================================== /// Modelo de ejecución (rol). Ortogonal a `Supervision` (política de restart). #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum Lifecycle { /// Servicio de larga duración. #[default] Daemon, /// Una sola ejecución; sale al terminar su tarea. Oneshot, /// Componente UI gestionado por el motor de widgets. Widget, } /// Prioridad de scheduling. Orden: `Low < Normal < High < Critical` — /// usable como tiebreaker en el broker (mayor priority gana). #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum Priority { Low, #[default] Normal, High, Critical, } // ===================================================================== // Flujos tipados (del modelo brahman) // ===================================================================== #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct Flows { #[serde(default)] pub input: Vec, #[serde(default)] pub output: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Flow { /// Nombre único dentro de su dirección. pub name: String, /// Tipo de los datos que viajan por el flujo. #[serde(rename = "type")] pub ty: TypeRef, /// Sugerencia de productor/consumidor concreto. El broker la respeta /// como pista; cae en matching por tipo si no es resoluble. #[serde(default)] pub pin_to: Option, } /// Referencia a un tipo, discriminada para distinguir primitivas de tipos WIT. /// /// **Wire format (JSON / TOML / postcard):** externally-tagged. Ejemplo JSON: /// ```json /// { "primitive": { "name": "string" } } /// { "wit": { "package": "brahman:dht", "name": "entity-result" } } /// ``` /// Se eligió externally-tagged por compatibilidad con postcard, que no /// soporta `#[serde(tag = "...")]` (internally-tagged) en formatos no /// self-describing. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum TypeRef { /// Tipo primitivo del runtime. Primitive { name: String }, /// Tipo declarado en un paquete WIT. Wit { package: String, #[serde(default)] interface: Option, name: String, }, } // ===================================================================== // API: parseo y validación // ===================================================================== impl Card { /// Deserializa una Card desde JSON y valida. pub fn from_json(src: &str) -> Result { let c: Self = serde_json::from_str(src)?; c.validate()?; Ok(c) } /// Deserializa una Card desde TOML y valida. pub fn from_toml(src: &str) -> Result { let c: Self = toml::from_str(src)?; c.validate()?; Ok(c) } /// Carga una Card desde disco. Auto-detecta formato por extensión /// (`.json` o `.toml`). pub fn from_path(path: impl AsRef) -> Result { let p = path.as_ref(); let src = std::fs::read_to_string(p)?; match p.extension().and_then(|e| e.to_str()) { Some("json") => Self::from_json(&src), Some("toml") => Self::from_toml(&src), _ => Err(CardError::UnknownFormat), } } /// Re-serializa la Card a JSON con indentación. pub fn to_json_pretty(&self) -> Result { Ok(serde_json::to_string_pretty(self)?) } /// Validación semántica exhaustiva, recursiva sobre `genesis`. pub fn validate(&self) -> Result<(), CardError> { if self.schema_version != CARD_SCHEMA_VERSION { return Err(CardError::SchemaMismatch { got: self.schema_version, expected: CARD_SCHEMA_VERSION, }); } if self.label.is_empty() { return Err(CardError::EmptyLabel); } if self.label.len() > 256 { return Err(CardError::LabelTooLong(self.label.len())); } for cap in &self.requires { if self.provides.contains(cap) { return Err(CardError::SelfDependency(cap.clone())); } } validate_payload(&self.payload)?; validate_rlimits(&self.soma.rlimits)?; validate_cgroup(&self.soma.cgroup)?; check_unique_flow_names(&self.flow.input, "flow.input")?; check_unique_flow_names(&self.flow.output, "flow.output")?; for child in &self.genesis { child.validate()?; } Ok(()) } } fn validate_payload(p: &Payload) -> Result<(), CardError> { match p { Payload::Native { exec, .. } | Payload::Legacy { exec, .. } => { if exec.trim().is_empty() { return Err(CardError::EmptyExec); } } Payload::Wasm { module_sha256, .. } => { if module_sha256.iter().all(|&b| b == 0) { return Err(CardError::SentinelWasmHash); } } Payload::Virtual => {} } Ok(()) } fn validate_rlimits(rl: &ResourceLimits) -> Result<(), CardError> { if let Some(m) = rl.mem_bytes { if m == 0 { return Err(CardError::InvalidRlimit("mem_bytes=0")); } if m > 1u64 << 40 { return Err(CardError::InvalidRlimit("mem_bytes>1TiB")); } } if let Some(n) = rl.nproc { if n == 0 || n > 65535 { return Err(CardError::InvalidRlimit("nproc fuera de [1,65535]")); } } if let Some(n) = rl.nofile { if n == 0 || n > 1_048_576 { return Err(CardError::InvalidRlimit("nofile fuera de [1,1M]")); } } Ok(()) } fn validate_cgroup(cg: &CgroupSpec) -> Result<(), CardError> { if let Some(w) = cg.cpu_weight { if !(1..=10000).contains(&w) { return Err(CardError::InvalidCgroupWeight("cpu_weight")); } } if let Some(w) = cg.io_weight { if !(1..=10000).contains(&w) { return Err(CardError::InvalidCgroupWeight("io_weight")); } } Ok(()) } fn check_unique_flow_names(flows: &[Flow], section: &'static str) -> Result<(), CardError> { let mut seen = HashSet::new(); for f in flows { if !seen.insert(f.name.as_str()) { return Err(CardError::DuplicateFlowName { section, name: f.name.clone(), }); } } Ok(()) } // ===================================================================== // Trust derivado // ===================================================================== /// Nivel de confianza derivado de los permisos. **No es un campo declarado** — /// se calcula. Una sola fuente de verdad: lo que el Admin concede. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum TrustLevel { /// Sin permisos — sandbox total. Untrusted, /// Permisos menores (loopback, FS read-only, IPC). Sandboxed, /// Permisos amplios (red saliente o FS read-write). Privileged, /// Capacidad de spawnear procesos. System, } impl TrustLevel { /// Política de derivación: /// - `processes = true` ⇒ `System`. /// - FS `read-write` o networking `outbound`/`full` ⇒ `Privileged`. /// - FS `read-only`, networking `loopback`, o cualquier IPC ⇒ `Sandboxed`. /// - Sin permisos ⇒ `Untrusted`. pub fn derive(p: &Permissions) -> Self { if p.processes { return Self::System; } if matches!(p.filesystem, FsPolicy::ReadWrite) || matches!( p.networking, NetworkingPolicy::Outbound | NetworkingPolicy::Full ) { return Self::Privileged; } if matches!(p.filesystem, FsPolicy::ReadOnly) || matches!(p.networking, NetworkingPolicy::Loopback) || !p.ipc.allow.is_empty() { return Self::Sandboxed; } Self::Untrusted } } // ===================================================================== // Identidad runtime (Card + WIT extraído + trust) // ===================================================================== /// Resumen de la interfaz WIT extraída del componente WASM/WIT. /// Vacío para módulos agnósticos (sin contrato WIT). #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct WitInterface { pub package: String, pub world: String, pub exports: Vec, pub imports: Vec, } /// Card resuelta a runtime: schema + WIT opcional + trust derivado. /// Es lo que el Admin indexa. #[derive(Debug, Clone)] pub struct ResolvedCard { pub card: Card, /// `Some` si el módulo es consciente (expone WIT), `None` si es agnóstico. pub wit: Option, pub trust: TrustLevel, } impl ResolvedCard { /// Construye una Card resuelta sin información WIT. pub fn from_agnostic(card: Card) -> Self { let trust = TrustLevel::derive(&card.permissions); Self { card, wit: None, trust, } } /// Construye una Card resuelta con interfaz WIT extraída. pub fn from_conscious(card: Card, wit: WitInterface) -> Self { let trust = TrustLevel::derive(&card.permissions); Self { card, wit: Some(wit), trust, } } } // ===================================================================== // WireCard — proyección postcard-friendly de Card // ===================================================================== /// Forma de wire de [`Card`]: idéntica al schema rico **sin** el campo /// `extensions` (incompatible con postcard porque `serde_json::Value` /// usa secuencias/maps de longitud dinámica). /// /// Conversión: /// - `WireCard::from(card)` descarta `extensions` y proyecta `genesis` /// recursivamente. /// - `Card::from(wire)` recupera todos los campos; `extensions` queda /// vacío (la información de extensions no cruza el wire). /// /// Esta separación implementa el contrato: /// - **JSON/TOML**: `Card` directa, con extensiones preservadas. /// - **Wire (postcard)**: `WireCard`, sin extensiones. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WireCard { pub schema_version: u16, pub id: Ulid, #[serde(default)] pub lineage: Option, pub label: String, #[serde(default)] pub provides: BTreeSet, #[serde(default)] pub requires: BTreeSet, #[serde(default)] pub permissions: Permissions, #[serde(default)] pub soma: SomaSpec, pub payload: Payload, pub supervision: Supervision, #[serde(default)] pub lifecycle: Lifecycle, #[serde(default)] pub priority: Priority, #[serde(default)] pub flow: Flows, #[serde(default)] pub genesis: Vec, } impl From for WireCard { fn from(c: Card) -> Self { Self { schema_version: c.schema_version, id: c.id, lineage: c.lineage, label: c.label, provides: c.provides, requires: c.requires, permissions: c.permissions, soma: c.soma, payload: c.payload, supervision: c.supervision, lifecycle: c.lifecycle, priority: c.priority, flow: c.flow, genesis: c.genesis.into_iter().map(WireCard::from).collect(), } } } impl From for Card { fn from(w: WireCard) -> Self { Self { schema_version: w.schema_version, id: w.id, lineage: w.lineage, label: w.label, provides: w.provides, requires: w.requires, permissions: w.permissions, soma: w.soma, payload: w.payload, supervision: w.supervision, lifecycle: w.lifecycle, priority: w.priority, flow: w.flow, genesis: w.genesis.into_iter().map(Card::from).collect(), extensions: BTreeMap::new(), } } } // ===================================================================== // Tests // ===================================================================== #[cfg(test)] mod tests { use super::*; fn sample_card_json() -> &'static str { r#"{ "schema_version": 1, "id": "01HQAR53D4M2NBV8KZTYXFGS01", "lineage": null, "label": "brahman.semantic_dht", "provides": ["Spawn", "Journal"], "requires": [], "permissions": { "networking": "loopback", "filesystem": "read-only", "ipc": { "allow": ["wit-v1"] }, "processes": false }, "soma": { "namespaces": { "mount": false, "pid": false, "net": false, "uts": false, "ipc": false, "user": false, "cgroup": false }, "rlimits": { "mem_bytes": null, "nproc": null, "nofile": null }, "cgroup": { "path": "ente.slice/dht", "cpu_weight": null, "io_weight": null }, "cpu_affinity": null }, "payload": "Virtual", "supervision": { "Restart": { "initial": 100, "max": 30000 } }, "lifecycle": "daemon", "priority": "high", "flow": { "input": [ { "name": "search-query", "type": { "primitive": { "name": "string" } } } ], "output": [ { "name": "dht-results", "type": { "wit": { "package": "brahman:dht", "name": "entity-result" } } } ] }, "genesis": [] }"# } #[test] fn parses_full_json() { let c = Card::from_json(sample_card_json()).unwrap(); assert_eq!(c.label, "brahman.semantic_dht"); assert_eq!(c.lifecycle, Lifecycle::Daemon); assert_eq!(c.priority, Priority::High); assert_eq!(c.permissions.filesystem, FsPolicy::ReadOnly); assert_eq!(c.permissions.networking, NetworkingPolicy::Loopback); assert_eq!(c.permissions.ipc.allow, vec!["wit-v1".to_string()]); assert_eq!(c.flow.input.len(), 1); assert_eq!(c.flow.output.len(), 1); match &c.flow.output[0].ty { TypeRef::Wit { package, name, .. } => { assert_eq!(package, "brahman:dht"); assert_eq!(name, "entity-result"); } _ => panic!("expected Wit"), } } #[test] fn json_roundtrip_preserves_shape() { let c1 = Card::from_json(sample_card_json()).unwrap(); let s = c1.to_json_pretty().unwrap(); let c2 = Card::from_json(&s).unwrap(); assert_eq!(c1.label, c2.label); assert_eq!(c1.flow.input.len(), c2.flow.input.len()); } #[test] fn trust_derivation() { let mut p = Permissions::default(); assert_eq!(TrustLevel::derive(&p), TrustLevel::Untrusted); p.filesystem = FsPolicy::ReadOnly; assert_eq!(TrustLevel::derive(&p), TrustLevel::Sandboxed); p.networking = NetworkingPolicy::Outbound; assert_eq!(TrustLevel::derive(&p), TrustLevel::Privileged); p.processes = true; assert_eq!(TrustLevel::derive(&p), TrustLevel::System); } #[test] fn duplicate_flow_names_rejected() { let mut c: Card = serde_json::from_str(sample_card_json()).unwrap(); c.flow.input.push(c.flow.input[0].clone()); assert!(matches!( c.validate(), Err(CardError::DuplicateFlowName { .. }) )); } #[test] fn self_dependency_rejected() { let mut c: Card = serde_json::from_str(sample_card_json()).unwrap(); c.requires.insert(Capability::Spawn); assert!(matches!(c.validate(), Err(CardError::SelfDependency(_)))); } #[test] fn invalid_genesis_propagates() { let parent_src = r#"{ "schema_version": 1, "id": "01HQAR53D4M2NBV8KZTYXFGS01", "label": "parent", "provides": [], "requires": [], "soma": { "namespaces": {"mount":false,"pid":false,"net":false,"uts":false,"ipc":false,"user":false,"cgroup":false}, "rlimits": {"mem_bytes":null,"nproc":null,"nofile":null}, "cgroup": {"path":"x","cpu_weight":null,"io_weight":null}, "cpu_affinity": null }, "payload": "Virtual", "supervision": "OneShot", "genesis": [{ "schema_version": 1, "id": "01HQAR53D4M2NBV8KZTYXFGS02", "label": "", "provides": [], "requires": [], "soma": { "namespaces": {"mount":false,"pid":false,"net":false,"uts":false,"ipc":false,"user":false,"cgroup":false}, "rlimits": {"mem_bytes":null,"nproc":null,"nofile":null}, "cgroup": {"path":"x","cpu_weight":null,"io_weight":null}, "cpu_affinity": null }, "payload": "Virtual", "supervision": "OneShot", "genesis": [] }] }"#; assert!(matches!( Card::from_json(parent_src), Err(CardError::EmptyLabel) )); } #[test] fn presentation_card_carries_derived_trust() { let c = Card::from_json(sample_card_json()).unwrap(); let resolved = ResolvedCard::from_agnostic(c); assert_eq!(resolved.trust, TrustLevel::Sandboxed); assert!(resolved.wit.is_none()); } #[test] fn arje_seed_format_compatible() { // Reproduce el formato canónico de arje (sin lifecycle/priority/flow, // que son aditivos brahman). Debe parsear con defaults. let src = r#"{ "schema_version": 1, "id": "01HQAR53D4M2NBV8KZTYXFGS01", "lineage": null, "label": "vps-min", "provides": ["Spawn", "Journal"], "requires": [], "soma": { "namespaces": {"mount":false,"pid":false,"net":false,"uts":false,"ipc":false,"user":false,"cgroup":false}, "rlimits": {"mem_bytes":null,"nproc":null,"nofile":null}, "cgroup": {"path":"ente.slice/zero","cpu_weight":null,"io_weight":null}, "cpu_affinity": null }, "payload": "Virtual", "supervision": "OneShot", "genesis": [] }"#; let c = Card::from_json(src).unwrap(); assert_eq!(c.lifecycle, Lifecycle::Daemon); // default assert_eq!(c.priority, Priority::Normal); // default assert_eq!(c.flow.input.len(), 0); } #[test] fn extensions_preserved_in_json_roundtrip() { let src = r#"{ "schema_version": 1, "id": "01HQAR53D4M2NBV8KZTYXFGS01", "label": "x", "soma": { "namespaces": {"mount":false,"pid":false,"net":false,"uts":false,"ipc":false,"user":false,"cgroup":false}, "rlimits": {"mem_bytes":null,"nproc":null,"nofile":null}, "cgroup": {"path":"x","cpu_weight":null,"io_weight":null}, "cpu_affinity": null }, "payload": "Virtual", "supervision": "OneShot", "author": "sergio", "tags": ["draft", "experimental"] }"#; let c = Card::from_json(src).unwrap(); assert_eq!(c.extensions.get("author").and_then(|v| v.as_str()), Some("sergio")); assert!(c.extensions.contains_key("tags")); // Roundtrip JSON: extensions deben re-emitirse. let s = c.to_json_pretty().unwrap(); let c2 = Card::from_json(&s).unwrap(); assert_eq!(c2.extensions.get("author"), c.extensions.get("author")); } #[test] fn wire_card_roundtrip_strips_extensions() { let src = r#"{ "schema_version": 1, "id": "01HQAR53D4M2NBV8KZTYXFGS01", "label": "x", "soma": { "namespaces": {"mount":false,"pid":false,"net":false,"uts":false,"ipc":false,"user":false,"cgroup":false}, "rlimits": {"mem_bytes":null,"nproc":null,"nofile":null}, "cgroup": {"path":"x","cpu_weight":null,"io_weight":null}, "cpu_affinity": null }, "payload": "Virtual", "supervision": "OneShot", "author": "sergio" }"#; let c = Card::from_json(src).unwrap(); assert!(c.extensions.contains_key("author")); // Card → WireCard descarta extensions. let wire: WireCard = c.into(); assert_eq!(wire.label, "x"); // WireCard → Card → extensiones quedan vacías (se perdieron). let c_back: Card = wire.into(); assert_eq!(c_back.label, "x"); assert!(c_back.extensions.is_empty(), "extensions sobreviven al wire"); } #[test] fn wire_card_postcard_friendly() { // Validación: WireCard puede ser postcard-encoded sin error. // Si Card tuviera extensions populadas, el encode rompería con // "length of a sequence must be known". WireCard las descarta. let src = r#"{ "schema_version": 1, "id": "01HQAR53D4M2NBV8KZTYXFGS01", "label": "x", "soma": { "namespaces": {"mount":false,"pid":false,"net":false,"uts":false,"ipc":false,"user":false,"cgroup":false}, "rlimits": {"mem_bytes":null,"nproc":null,"nofile":null}, "cgroup": {"path":"x","cpu_weight":null,"io_weight":null}, "cpu_affinity": null }, "payload": "Virtual", "supervision": "OneShot", "author": "sergio" }"#; let c = Card::from_json(src).unwrap(); let wire: WireCard = c.into(); let bytes = postcard::to_allocvec(&wire).expect("WireCard debe encodear"); let decoded: WireCard = postcard::from_bytes(&bytes).expect("WireCard debe decodear"); assert_eq!(decoded.label, "x"); } }