feat(shipote): data plane + DAG fan-in/out + stats + lifecycle (fases F-I)

Pipeline runtime:
- Fan-out 1→N (splitter task replica al N consumers) y fan-in N→1 (merger
  task con mpsc + reader-per-input). DAGs no lineales soportados.
- Flow channels: Unix socket + tokio broadcast con replay buffer
  configurable por pipeline (DiscernPolicy.replay_chunks). Subscribers
  externos vía `shipote flow tail <socket>`.
- Templating en specs con `${KEY}` (CLI `--var KEY=VALUE`). Walk
  recursivo sobre serde_json::Value, soporta todos los strings del schema.
- Pipelines guardados (`pipeline save/saved-list/drop/run-saved`)
  persisten con el snapshot.

Lifecycle de comandos:
- Log capture per-stream (stdout/stderr separados) via pipe O_CLOEXEC +
  AsyncFd. CLI `shipote logs <ws> <cmd> --stream {stdout,stderr,both}`.
- Stop graceful con tiempo configurable: SIGTERM → grace → SIGKILL.
  Tanto a nivel workspace como pipeline individual.
- TTL auto-stop ya existente (Fase C) sigue funcionando.

ente-incarnate:
- ChildStdio declarativo (Fase C) + ChildPreExec declarativo nuevo:
  NoNewPrivs, ParentDeathSig, Dumpable, NewSession, Chdir, Umask.
- Aplicación pre-execve async-signal-safe en ambos paths (plain via
  Command::pre_exec, namespaced via callback del clone(2)).

Observabilidad:
- WorkspaceStats: RSS + RSS peak (VmHWM o memory.peak cgroup) + CPU usec
  + uptime. Fuente per-proc o cgroup según delegation.
- shipote-shell con sparkline ASCII por workspace (history cap 24),
  card de flow channels activos, vista de comandos + saved pipelines.
- Tap → broker: cada edge enriquecido con TypeRef se anuncia como Card
  efímera vía SidecarPool (graceful si broker no corre).

Discern:
- Integrado en yahweh-provider-fs (mime_type en EntityNode).
- Integrado en nouser-core::cluster::pick_lens como fallback cuando la
  extensión cae a Lens::Grid.

79 tests pasan: ente-incarnate (16), nouser-core (27), shipote-card (8),
shipote-core (20), shipote-discern (5), yahweh-provider-fs (3).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-11 00:29:46 +00:00
parent c22d2480b9
commit 36dac00c8d
13 changed files with 2187 additions and 253 deletions
@@ -24,6 +24,7 @@ use crate::child::{apply_rlimits, make_root_private};
use crate::cgroup::{ensure_cgroup, move_to_cgroup};
use crate::env::{build_env, EnvSpec};
use crate::error::{Degradation, IncarnateError};
use crate::pre_exec::{apply_unchecked, ChildSetup};
use crate::ChildStdio;
use brahman_card::{Card, NamespaceSet, Payload};
use nix::fcntl::OFlag;
@@ -53,6 +54,7 @@ pub fn incarnate_namespaced(
card: &Card,
env_spec: &EnvSpec,
stdio: &ChildStdio,
setup: &ChildSetup,
degradations: &mut Vec<Degradation>,
) -> Result<Pid, IncarnateError> {
let flags = build_clone_flags(&card.soma.namespaces);
@@ -96,6 +98,7 @@ pub fn incarnate_namespaced(
let stdin_fd = stdio.stdin_fd;
let stdout_fd = stdio.stdout_fd;
let stderr_fd = stdio.stderr_fd;
let setup_ops = setup.ops.clone();
// SAFETY: la clausura corre en stack nuevo dentro de un proceso recién
// clonado, COW del padre. Sólo syscalls async-signal-safe; sin allocator,
@@ -142,6 +145,14 @@ pub fn incarnate_namespaced(
}
}
// Aplica las ops declarativas pre-execve (NoNewPrivs, chdir, etc.).
if !setup_ops.is_empty() {
let r = unsafe { apply_unchecked(&setup_ops) };
if r != 0 {
unsafe { libc::_exit(r) };
}
}
unsafe {
libc::execve(exec_c.as_ptr(), argv_ptrs.as_ptr(), envp_ptrs.as_ptr());
libc::_exit(102);