feat(sandokan-core): B1.1 — contrato del orquestador

Primer crate de la Fase B. Define SOLO el contrato del orquestador
sandokan (library horizontal embebible, no daemon supremo):

- Intent / ExecContext / IsolationLevel — qué orquestar
- ExecHandle — referencia a una entidad encarnada
- LifecycleEvent / TelemetryFrame — observabilidad (wire types)
- EngineError — taxonomía de fallas
- trait Engine — run/stop/list/status/telemetry (poll-based, sin
  streams sobre trait objects, para que las 3 impls lo cumplan
  uniformemente)

Las impls concretas (LocalEngine, DaemonEngine, RemoteEngine) vendrán
en crates separados (sandokan-local, sandokan-daemon, sandokan-remote).

3 tests verdes. cargo check --workspace verde.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-20 00:38:22 +00:00
parent f8a2547b45
commit af5d4a1f22
8 changed files with 272 additions and 0 deletions
@@ -0,0 +1,93 @@
//! Qué orquestar: la intención de ejecución y su contexto.
use brahman_card::Card;
use serde::{Deserialize, Serialize};
use std::time::{Duration, SystemTime};
use ulid::Ulid;
/// Nivel de aislamiento pedido para una encarnación.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum IsolationLevel {
/// Sin sandbox — mismo namespace que el orquestador.
None,
/// Namespaces estándar (pid/mount/net/...) según `Card.soma`.
#[default]
Standard,
/// Namespaces + rootfs aislado (`pivot_root` + OverlayFS).
Sealed,
}
/// Contexto de ejecución: ajustes sobre cómo encarnar la Card.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ExecContext {
/// Aislamiento pedido. `None` = derivar de `Card.soma`.
pub isolation: Option<IsolationLevel>,
/// Variables de entorno adicionales (sobre las del Card).
pub env: Vec<(String, String)>,
/// Time-to-live opcional: si se setea, el orquestador detiene la
/// entidad al vencer.
pub ttl: Option<Duration>,
}
/// Una intención de ejecución: la `Card` a encarnar + su contexto.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Intent {
pub card: Card,
#[serde(default)]
pub context: ExecContext,
}
impl Intent {
/// Intención mínima: encarnar una Card con el contexto por defecto.
pub fn new(card: Card) -> Self {
Self { card, context: ExecContext::default() }
}
/// Builder: fija el nivel de aislamiento.
pub fn with_isolation(mut self, level: IsolationLevel) -> Self {
self.context.isolation = Some(level);
self
}
/// Builder: fija un TTL.
pub fn with_ttl(mut self, ttl: Duration) -> Self {
self.context.ttl = Some(ttl);
self
}
/// El id de la Card que esta intención encarna.
pub fn card_id(&self) -> Ulid {
self.card.id
}
}
/// Referencia a una entidad encarnada por el orquestador.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExecHandle {
/// Id de la Card encarnada (identidad estable en el fractal).
pub card_id: Ulid,
/// Label humano-legible (copiado de `Card.label`).
pub label: String,
/// Cuándo arrancó.
pub started_at: SystemTime,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn isolation_default_is_standard() {
assert_eq!(IsolationLevel::default(), IsolationLevel::Standard);
}
#[test]
fn intent_builders_compose() {
let card = Card::new("demo");
let intent = Intent::new(card)
.with_isolation(IsolationLevel::Sealed)
.with_ttl(Duration::from_secs(30));
assert_eq!(intent.context.isolation, Some(IsolationLevel::Sealed));
assert_eq!(intent.context.ttl, Some(Duration::from_secs(30)));
}
}