diff --git a/Cargo.lock b/Cargo.lock index 5aea1c2..de3b45f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1139,6 +1139,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "brahman-card" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "thiserror 2.0.18", + "toml 0.8.23", + "ulid", +] + [[package]] name = "bs58" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 196b0d2..671bdaa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ # ============================================================ # core/ — Init y compat (arje absorbido) # ============================================================ + "crates/core/brahman-card", "crates/core/ente-card", "crates/core/ente-bus", "crates/core/ente-cas", diff --git a/crates/core/brahman-card/Cargo.toml b/crates/core/brahman-card/Cargo.toml new file mode 100644 index 0000000..ff8c003 --- /dev/null +++ b/crates/core/brahman-card/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "brahman-card" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +publish.workspace = true +description = "Brahman — Tarjeta de Presentación canónica (identidad arje + flujos tipados brahman)." + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +toml = { workspace = true } +thiserror = { workspace = true } +ulid = { workspace = true } diff --git a/crates/core/brahman-card/src/lib.rs b/crates/core/brahman-card/src/lib.rs new file mode 100644 index 0000000..7f19910 --- /dev/null +++ b/crates/core/brahman-card/src/lib.rs @@ -0,0 +1,799 @@ +//! `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::{BTreeSet, HashMap, HashSet}; +use std::path::Path; +use std::time::Duration; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use thiserror::Error; +use ulid::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 desconocidos preservados intactos (forward-compat). + #[serde(flatten, default)] + pub extensions: HashMap, +} + +// ===================================================================== +// 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, + #[serde(flatten, default)] + pub extra: HashMap, +} + +#[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. +#[derive(Debug, Clone, Copy, PartialEq, Eq, 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. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "kind", 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)] +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, + } + } +} + +// ===================================================================== +// 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": { "kind": "primitive", "name": "string" } } + ], + "output": [ + { "name": "dht-results", + "type": { "kind": "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); + } +}