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:
Generated
+12
@@ -10127,6 +10127,18 @@ dependencies = [
|
|||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sandokan-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"brahman-card",
|
||||||
|
"sandokan-lifecycle",
|
||||||
|
"serde",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"ulid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sandokan-lifecycle"
|
name = "sandokan-lifecycle"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ members = [
|
|||||||
"crates/runtime/arje-brain",
|
"crates/runtime/arje-brain",
|
||||||
"crates/runtime/arje-echo",
|
"crates/runtime/arje-echo",
|
||||||
"crates/runtime/sandokan-lifecycle",
|
"crates/runtime/sandokan-lifecycle",
|
||||||
|
"crates/runtime/sandokan-core",
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# compat/ — Shims D-Bus para correr software systemd-aware
|
# compat/ — Shims D-Bus para correr software systemd-aware
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "sandokan-core"
|
||||||
|
description = "Contrato del orquestador sandokan: trait Engine + tipos (Intent, ExecHandle, eventos, telemetría). Sin transporte ni impl concreta."
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
brahman-card = { path = "../../protocol/brahman-card" }
|
||||||
|
sandokan-lifecycle = { path = "../sandokan-lifecycle" }
|
||||||
|
serde = { workspace = true }
|
||||||
|
ulid = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
async-trait = { workspace = true }
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
//! El trait `Engine` — contrato uniforme del orquestador.
|
||||||
|
|
||||||
|
use crate::error::EngineError;
|
||||||
|
use crate::event::TelemetryFrame;
|
||||||
|
use crate::intent::{ExecHandle, Intent};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use sandokan_lifecycle::LifecycleState;
|
||||||
|
use std::time::Duration;
|
||||||
|
use ulid::Ulid;
|
||||||
|
|
||||||
|
/// El orquestador. Tres implementaciones lo cumplen de forma intercambiable:
|
||||||
|
///
|
||||||
|
/// - `LocalEngine` — encarna in-process (`arje-incarnate` + `arje-brain-rules`).
|
||||||
|
/// - `DaemonEngine` — delega a otro proceso vía Unix socket (postcard).
|
||||||
|
/// - `RemoteEngine` — delega a otro host vía `brahman-ssh-multiplex`.
|
||||||
|
///
|
||||||
|
/// Un helper `Engine::auto()` (en `sandokan-local`) prueba si hay un
|
||||||
|
/// daemon escuchando y elige `DaemonEngine`; si no, `LocalEngine`.
|
||||||
|
///
|
||||||
|
/// El contrato es poll-based (sin streams) para que las tres impls lo
|
||||||
|
/// cumplan uniformemente sin complejidad de `Stream` sobre trait objects.
|
||||||
|
#[async_trait]
|
||||||
|
pub trait Engine: Send + Sync {
|
||||||
|
/// Encarna una intención. Devuelve un handle a la entidad corriendo.
|
||||||
|
async fn run(&self, intent: Intent) -> Result<ExecHandle, EngineError>;
|
||||||
|
|
||||||
|
/// Detiene una entidad con período de gracia (SIGTERM → espera →
|
||||||
|
/// SIGKILL). `grace == 0` = kill inmediato.
|
||||||
|
async fn stop(&self, card_id: Ulid, grace: Duration) -> Result<(), EngineError>;
|
||||||
|
|
||||||
|
/// Lista las entidades actualmente activas.
|
||||||
|
async fn list(&self) -> Result<Vec<ExecHandle>, EngineError>;
|
||||||
|
|
||||||
|
/// Estado actual de una entidad.
|
||||||
|
async fn status(&self, card_id: Ulid) -> Result<LifecycleState, EngineError>;
|
||||||
|
|
||||||
|
/// Telemetría puntual de una entidad.
|
||||||
|
async fn telemetry(&self, card_id: Ulid) -> Result<TelemetryFrame, EngineError>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
//! Errores del orquestador.
|
||||||
|
|
||||||
|
use ulid::Ulid;
|
||||||
|
|
||||||
|
/// Falla de una operación del `Engine`. Las impls concretas mapean sus
|
||||||
|
/// errores internos (encarnación, IPC, SSH) a estas variantes.
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum EngineError {
|
||||||
|
/// No existe ninguna entidad activa con ese `card_id`.
|
||||||
|
#[error("card `{0}` no encontrada")]
|
||||||
|
NotFound(Ulid),
|
||||||
|
|
||||||
|
/// La encarnación falló (clone/namespaces/exec).
|
||||||
|
#[error("encarnación falló: {0}")]
|
||||||
|
Incarnate(String),
|
||||||
|
|
||||||
|
/// Falla de transporte (Unix socket del daemon, túnel SSH).
|
||||||
|
#[error("transporte: {0}")]
|
||||||
|
Transport(String),
|
||||||
|
|
||||||
|
/// La intención es inconsistente (Card inválida, contexto imposible).
|
||||||
|
#[error("intención inválida: {0}")]
|
||||||
|
InvalidIntent(String),
|
||||||
|
|
||||||
|
/// La operación excedió su deadline.
|
||||||
|
#[error("timeout")]
|
||||||
|
Timeout,
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
//! Observabilidad: eventos de ciclo de vida y frames de telemetría.
|
||||||
|
|
||||||
|
use sandokan_lifecycle::LifecycleState;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::time::SystemTime;
|
||||||
|
use ulid::Ulid;
|
||||||
|
|
||||||
|
/// Un evento en la vida de una entidad encarnada. El orquestador los
|
||||||
|
/// emite; los consumidores (shells, paneles) reaccionan.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum LifecycleEvent {
|
||||||
|
/// La entidad fue encarnada. `pid` es `None` si no aplica (Wasm, virtual).
|
||||||
|
Spawned { card_id: Ulid, pid: Option<i32> },
|
||||||
|
/// El estado de la entidad cambió.
|
||||||
|
StateChanged { card_id: Ulid, state: LifecycleState },
|
||||||
|
/// La entidad terminó (estado terminal).
|
||||||
|
Exited { card_id: Ulid, state: LifecycleState },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LifecycleEvent {
|
||||||
|
/// El `card_id` al que refiere el evento.
|
||||||
|
pub fn card_id(&self) -> Ulid {
|
||||||
|
match self {
|
||||||
|
LifecycleEvent::Spawned { card_id, .. }
|
||||||
|
| LifecycleEvent::StateChanged { card_id, .. }
|
||||||
|
| LifecycleEvent::Exited { card_id, .. } => *card_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Una medición puntual de recursos de una entidad. Los campos se
|
||||||
|
/// inlinean (en vez de reusar `sandokan_lifecycle::ResourceUsage`) para
|
||||||
|
/// que el frame sea un wire type estable e independiente.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct TelemetryFrame {
|
||||||
|
pub card_id: Ulid,
|
||||||
|
pub at: SystemTime,
|
||||||
|
pub mem_bytes: u64,
|
||||||
|
pub nproc: u32,
|
||||||
|
/// Porcentaje de CPU (100.0 = 1 core saturado).
|
||||||
|
pub cpu_pct: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn event_card_id_accessor() {
|
||||||
|
let id = Ulid::new();
|
||||||
|
let ev = LifecycleEvent::Exited {
|
||||||
|
card_id: id,
|
||||||
|
state: LifecycleState::Exited { code: 0 },
|
||||||
|
};
|
||||||
|
assert_eq!(ev.card_id(), id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
//! sandokan-core — el contrato del orquestador.
|
||||||
|
//!
|
||||||
|
//! `sandokan` es el orquestador del ecosistema brahman, diseñado como
|
||||||
|
//! **library horizontal embebible**, no como daemon supremo. Cualquier
|
||||||
|
//! binario (shuma, nahual-shell, matilda, un agente SSH) embebe un
|
||||||
|
//! `Engine` y decide si lo corre in-process o delega a otro.
|
||||||
|
//!
|
||||||
|
//! Este crate define SOLO el contrato:
|
||||||
|
//! - [`Intent`] — qué orquestar (una Card + contexto de ejecución).
|
||||||
|
//! - [`ExecHandle`] — referencia a una entidad encarnada.
|
||||||
|
//! - [`LifecycleEvent`] / [`TelemetryFrame`] — observabilidad.
|
||||||
|
//! - [`Engine`] — el trait que `LocalEngine`/`DaemonEngine`/`RemoteEngine`
|
||||||
|
//! implementan.
|
||||||
|
//!
|
||||||
|
//! Las implementaciones concretas viven en crates separados
|
||||||
|
//! (`sandokan-local`, `sandokan-daemon`, `sandokan-remote`).
|
||||||
|
|
||||||
|
pub mod engine;
|
||||||
|
pub mod error;
|
||||||
|
pub mod event;
|
||||||
|
pub mod intent;
|
||||||
|
|
||||||
|
pub use engine::Engine;
|
||||||
|
pub use error::EngineError;
|
||||||
|
pub use event::{LifecycleEvent, TelemetryFrame};
|
||||||
|
pub use intent::{ExecContext, ExecHandle, Intent, IsolationLevel};
|
||||||
Reference in New Issue
Block a user