feat(sandokan): CLI de prueba + fix wire serialization

apps/sandokan (binario `sandokan`): CLI para probar el orquestador.
Subcomandos: daemon, run <exec> [args], list, status, telemetry, stop.

Fix: Intent serializaba Card directo, pero Card tiene un campo
`#[serde(flatten)] extensions` incompatible con postcard ("sequence
length must be known"). Intent::card ahora usa #[serde(with)] que
proyecta Card↔WireCard en el límite de serialización (las extensions
locales se descartan al cruzar el wire — comportamiento correcto).

Smoke test verificado end-to-end: daemon + run /bin/sleep + list +
status Running + telemetry + stop + status Killed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-20 15:05:03 +00:00
parent 590572b5bb
commit 94ea0eaa53
6 changed files with 182 additions and 0 deletions
@@ -32,11 +32,30 @@ pub struct ExecContext {
/// Una intención de ejecución: la `Card` a encarnar + su contexto.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Intent {
/// La Card se serializa vía `WireCard` (proyección postcard-friendly):
/// el campo `extensions` de `Card` usa `#[serde(flatten)]`, que no es
/// compatible con formatos no auto-descriptivos como postcard.
#[serde(with = "card_wire")]
pub card: Card,
#[serde(default)]
pub context: ExecContext,
}
/// Serde adapter: `Card` ↔ `WireCard` en el límite de serialización.
/// Las `extensions` locales de la Card se descartan al cruzar el wire.
mod card_wire {
use brahman_card::{Card, WireCard};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub fn serialize<S: Serializer>(card: &Card, s: S) -> Result<S::Ok, S::Error> {
WireCard::from(card.clone()).serialize(s)
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Card, D::Error> {
Ok(Card::from(WireCard::deserialize(d)?))
}
}
impl Intent {
/// Intención mínima: encarnar una Card con el contexto por defecto.
pub fn new(card: Card) -> Self {