tawasuyu

tawasuyu/brahman

Rust 582TOML 222Markdown 73JSON 198635 Definiciones

⎇ 72758e75ce515f355f27615671c95ecaf8c2a8bc crates/modules/shipote/docs/ARCHITECTURE.md Markdown · 159 líneas · 10.4 KB Historia crudo

feat(shipote): audit log persistente + HTTP gateway (fase S) 6596c8127 · sergio ·

1# Arquitectura de shipote
2
3## Diagrama de capas
4
5```
6┌──────────────────────────────────────────────────────────────────┐
7│                       Apps (binarios)                             │
8│                                                                   │
9│   shipote-daemon        shipote (CLI)         shipote-shell      │
10│   crates/apps/...       crates/apps/...       crates/apps/...    │
11└─────────┬─────────────────────┬──────────────────────┬───────────┘
12          │                     │                      │
13          │                  postcard frames (length-prefixed)
14          │                     │                      │
15          └─────────────────────┴──────────────────────┘
16                                │
17                  shipote-protocol — wire types
18                  crates/modules/shipote/shipote-protocol
19                                │
20┌──────────────────────────────────────────────────────────────────┐
21│                  shipote-core — runtime                          │
22│  crates/modules/shipote/shipote-core                             │
23│                                                                  │
24│   WorkspaceManager (Arc<Mutex<Inner>>)                           │
25│     ├ workspaces: HashMap<WorkspaceId, WorkspaceState>          │
26│     ├ saved_pipelines, pipeline_flows, pipeline_supervisors      │
27│     ├ restart_specs, pending_pipeline_restarts                   │
28│     └ dirty: AtomicBool (snapshot incremental)                   │
29│                                                                  │
30│   Modules:                                                       │
31│   - pipeline.rs   (run_pipeline, splitter, merger, TokenBucket) │
32│   - flow_channel.rs (Unix socket + tokio::broadcast + replay)   │
33│   - logbuf.rs     (ring buffer 64 KiB para stdout/stderr)       │
34│   - stats.rs      (WorkspaceStats: RSS/CPU/peak/cores)          │
35│   - persist.rs    (ShipoteSnapshot v4 con JSON atomic write)    │
36└──────────────────────────────────────────────────────────────────┘
37                                │
38┌──────────────────────────────────────────────────────────────────┐
39│         Soporte compartido                                       │
40│                                                                  │
41│   shipote-card                  shipote-discern                  │
42│   - WorkspaceSpec               - DiscernPipeline                │
43│   - PipelineSpec                - MagicBytes, JsonProbe, etc.    │
44│   - CommandRef                  - Detecta brahman:card           │
45│   - DiscernPolicy                                                │
46│                                                                  │
47│   ente-incarnate (crates/shared/) — extraído de ente-soma       │
48│   - Incarnator { caps, env, stdio, pre_exec }                   │
49│   - CapabilitySet::detect (user_ns, cgroup, kernel version)     │
50│   - ChildStdio, ChildSetup (NoNewPrivs, Chdir, ...)              │
51│   - clone(2) + namespaces + cgroup move (post-clone setup)       │
52└──────────────────────────────────────────────────────────────────┘
53                                │
54                  brahman-card · ente-incarnate · nix · libc
55```
56
57## Crates (11 nuevos)
58
59### Core del runtime
60- **`crates/shared/ente-incarnate`** — rutina extraída del Init para encarnar Cards en procesos aislados. Reusable por cualquier supervisor. NO requiere ser PID 1.
61- **`crates/modules/shipote/shipote-card`** — tipos: `WorkspaceSpec`, `PipelineSpec`, `CommandRef`, `FlowEdge`, `DiscernPolicy`, `QuotaEnforcement`. Compilan a `brahman_card::Card`.
62- **`crates/modules/shipote/shipote-protocol`** — wire postcard length-prefixed sobre Unix socket en `$XDG_RUNTIME_DIR/shipote.sock`.
63- **`crates/modules/shipote/shipote-discern`** — `DiscernPipeline` + discerners default. Reusado por `yahweh-provider-fs` y `nouser-core::cluster`.
64- **`crates/modules/shipote/shipote-core`** — `WorkspaceManager` + módulos `pipeline`, `flow_channel`, `logbuf`, `persist`, `stats`.
65
66### Binarios
67- **`crates/apps/shipote-daemon`** — long-running. Escucha admin socket, ejecuta el dispatch, reaper periódico.
68- **`crates/apps/shipote-cli`** — `shipote` (clap). Para administración + scripting.
69- **`crates/apps/shipote-shell`** — GUI con `yahweh_launcher`. Dashboard: estado, workspaces, comandos, flows, quotas, sparkline.
70- **`crates/apps/shipote-gateway`** — HTTP/JSON adapter. `POST /rpc` con body JSON `Request` → traduce a postcard, round-trip al daemon, retorna JSON. Default `127.0.0.1:7378`.
71
72### Consumidores del discerner (externos a shipote pero usándolo)
73- **`crates/modules/ui_engine/libs/providers/fs`** — `FileDataProvider` ahora puebla `mime_type` con `DiscernPipeline`.
74- **`crates/modules/nouser/core/src/cluster.rs`** — `pick_lens` usa el discerner como fallback cuando la extensión cae a `Lens::Grid`.
75
76## Modelo conceptual
77
78### Workspace
79Espacio aislado raíz. Una **Card con `kind: Ente`, `payload: Virtual`** que aloja N comandos hijos. Su `SomaSpec` define el sandbox compartido.
80
81```toml
82label = "demo"
83on_exit = "reap"
84ttl = 60000              # opcional, ms
85
86[soma.rlimits]
87mem_bytes = 10485760     # 10 MiB
88nproc = 4
89
90[soma.cgroup]
91path = "shipote/demo"
92
93[quota_enforce]
94mem = "kill"             # None | Log | Kill
95```
96
97### CommandRef
98Un comando dentro del workspace. Su `SomaSpec` se **intersecta** con el del padre (OR de namespaces, MIN de rlimits).
99
100### PipelineSpec
101DAG de `CommandRef` con edges tipados. Soporta:
102- **1→1**: pipe directo (sin task intermedio).
103- **1→N (fan-out)**: splitter task replica.
104- **N→1 (fan-in)**: merger task con mpsc (una sub-task por reader → channel → escribir al consumer.stdin).
105
106### Flow channel (data plane)
107Unix socket en `$XDG_RUNTIME_DIR/shipote-flow-<pipeline_id>-<edge_idx>.sock`. Cada subscriber recibe primero el replay (cap por chunks y/o bytes), después el broadcast live. Tokio `broadcast::Sender<Arc<Vec<u8>>>` — zero-copy entre subscribers.
108
109### Discerner
110Pipeline ordenado: `MagicBytes` → `CardProbe` → `JsonProbe` → `TomlProbe` → `Utf8Probe`. Cada uno devuelve `Discernment { ty: TypeRef, confidence, mime, lens }`.
111
112## Decisiones clave de arquitectura
113
114### Por qué `ente-incarnate` es separado
115La rutina de aislamiento (clone+ns+cgroup+rlimits) vivía dentro del Init (`ente-soma`). Era global y atada a PID 1. La separamos en Fase 0 para que shipote (y futuros supervisores) puedan reusarla sin implicar privilegio.
116
117Hoy `ente-soma` es un wrapper de ~30 líneas sobre `ente-incarnate` que preserva la semántica histórica (`set_bus_sock` global + `strict_caps=false`). `ente-zero` sigue funcionando intocado.
118
119### Por qué pipeline-level restart relanza el pipeline ENTERO
120Los pipes intermedios de un comando que muere ya están cerrados. Restart parcial no funciona. La identidad ULID del pipeline cambia entre intentos; `restart_count` y `current_backoff_ms` se preservan en el supervisor.
121
122### Por qué replay buffer guarda ANTES del broadcast
123Si guardas después, un subscriber que conecta entre `broadcast::send` y `replay.push_back` puede perder ese chunk. El orden importa: replay → broadcast.
124
125### Por qué `pipe2(O_CLOEXEC)` siempre
126Sin `O_CLOEXEC`, el siguiente comando del pipeline hereda copias del write-end del pipe anterior. El read-end nunca ve EOF y el consumidor cuelga. Bug encontrado y arreglado en Fase F.
127
128### Por qué AsyncFd + non-blocking en taps/mergers
129`tokio::fs::File::from_std` con un FD blocking bloquea el reactor entero. La solución: `fcntl(F_SETFL, O_NONBLOCK)` antes de envolver en `AsyncFd<OwnedFd>`. Patrón usado en splitter, merger y log drainer.
130
131### Por qué token-bucket real reemplazó el sleep simple
132El sleep `chunk_size / rate` era uniforme y no permitía burst legítimo. `TokenBucket` con refill por wall time + `capacity=rate_bps` (1s burst) permite burst real y throttle suave.
133
134### Por qué `dirty: AtomicBool` para snapshot incremental
135Cada SIGTERM hacía full re-serialize aunque no hubiera cambios. `Ordering::Relaxed` está bien porque el peor caso es un save innecesario. `restore_snapshot` resetea dirty=false (acabamos de hidratar, no es mutation).
136
137### Por qué auth con SO_PEERCRED y `target: "audit"`
138SO_PEERCRED es automático en Unix sockets — leer `ucred.uid` del peer y comparar con el propio uid. Sin grupos, sin caps. `SHIPOTE_TRUST_ANYONE=1` es el escape hatch.
139
140`target: "audit"` separa el log de mutaciones del log normal. Filtrable con `EnvFilter`: `SHIPOTE_LOG=warn,audit=info`.
141
142## Versión de snapshot
143
144| Versión | Campos | Forward-compat |
145|---------|--------|----------------|
146| v1 | `workspaces` | ✓ |
147| v2 | + `saved_pipelines` | ✓ (default vacío) |
148| v3 | + `live_pipelines` | ✓ (default vacío) |
149| v4 | + `stats_history` per workspace (cap 16) | ✓ (default vacío) |
150
151`SNAPSHOT_VERSION = 4`. Reader rechaza versiones > 4 (forward-incompat). Todos los campos nuevos usan `#[serde(default)]`.
152
153## Wire protocol
154
155Todos los mensajes son enums tagged externamente, serializados con `postcard`. Framing: u32 BE length-prefix + payload. Max frame: 1 MiB.
156
157### Requests
158- Reads: `Ping`, `Health`, `Capabilities`, `WorkspaceList`, `WorkspaceStats`, `WorkspaceStatsHistory`, `WorkspaceFullSummary`, `WorkspaceQuota`, `CommandList`, `CommandLogs`, `PipelineSavedList`, `FlowList`, `FlowThroughput`, `Discern`.
159- Mutaciones (loguean audit): `WorkspaceCreate`, `WorkspaceStop`, `Run`, `PipelineRun`, `PipelineRunSaved`, `PipelineStop`, `PipelineSave`, `PipelineDrop`, `FlowDrop`.

ARCHITECTURE.md

Arquitectura de shipote

Diagrama de capas

┌──────────────────────────────────────────────────────────────────┐
│                       Apps (binarios)                             │
│                                                                   │
│   shipote-daemon        shipote (CLI)         shipote-shell      │
│   crates/apps/...       crates/apps/...       crates/apps/...    │
└─────────┬─────────────────────┬──────────────────────┬───────────┘
          │                     │                      │
          │                  postcard frames (length-prefixed)
          │                     │                      │
          └─────────────────────┴──────────────────────┘
                                │
                  shipote-protocol — wire types
                  crates/modules/shipote/shipote-protocol
                                │
┌──────────────────────────────────────────────────────────────────┐
│                  shipote-core — runtime                          │
│  crates/modules/shipote/shipote-core                             │
│                                                                  │
│   WorkspaceManager (Arc<Mutex<Inner>>)                           │
│     ├ workspaces: HashMap<WorkspaceId, WorkspaceState>          │
│     ├ saved_pipelines, pipeline_flows, pipeline_supervisors      │
│     ├ restart_specs, pending_pipeline_restarts                   │
│     └ dirty: AtomicBool (snapshot incremental)                   │
│                                                                  │
│   Modules:                                                       │
│   - pipeline.rs   (run_pipeline, splitter, merger, TokenBucket) │
│   - flow_channel.rs (Unix socket + tokio::broadcast + replay)   │
│   - logbuf.rs     (ring buffer 64 KiB para stdout/stderr)       │
│   - stats.rs      (WorkspaceStats: RSS/CPU/peak/cores)          │
│   - persist.rs    (ShipoteSnapshot v4 con JSON atomic write)    │
└──────────────────────────────────────────────────────────────────┘
                                │
┌──────────────────────────────────────────────────────────────────┐
│         Soporte compartido                                       │
│                                                                  │
│   shipote-card                  shipote-discern                  │
│   - WorkspaceSpec               - DiscernPipeline                │
│   - PipelineSpec                - MagicBytes, JsonProbe, etc.    │
│   - CommandRef                  - Detecta brahman:card           │
│   - DiscernPolicy                                                │
│                                                                  │
│   ente-incarnate (crates/shared/) — extraído de ente-soma       │
│   - Incarnator { caps, env, stdio, pre_exec }                   │
│   - CapabilitySet::detect (user_ns, cgroup, kernel version)     │
│   - ChildStdio, ChildSetup (NoNewPrivs, Chdir, ...)              │
│   - clone(2) + namespaces + cgroup move (post-clone setup)       │
└──────────────────────────────────────────────────────────────────┘
                                │
                  brahman-card · ente-incarnate · nix · libc

Crates (11 nuevos)

Core del runtime

Binarios

Consumidores del discerner (externos a shipote pero usándolo)

Modelo conceptual

Workspace

Espacio aislado raíz. Una Card con kind: Ente, payload: Virtual que aloja N comandos hijos. Su SomaSpec define el sandbox compartido.

label = "demo"
on_exit = "reap"
ttl = 60000              # opcional, ms

[soma.rlimits]
mem_bytes = 10485760     # 10 MiB
nproc = 4

[soma.cgroup]
path = "shipote/demo"

[quota_enforce]
mem = "kill"             # None | Log | Kill

CommandRef

Un comando dentro del workspace. Su SomaSpec se intersecta con el del padre (OR de namespaces, MIN de rlimits).

PipelineSpec

DAG de CommandRef con edges tipados. Soporta:

Flow channel (data plane)

Unix socket en $XDG_RUNTIME_DIR/shipote-flow-<pipeline_id>-<edge_idx>.sock. Cada subscriber recibe primero el replay (cap por chunks y/o bytes), después el broadcast live. Tokio broadcast::Sender<Arc<Vec<u8>>> — zero-copy entre subscribers.

Discerner

Pipeline ordenado: MagicBytes → CardProbe → JsonProbe → TomlProbe → Utf8Probe. Cada uno devuelve Discernment { ty: TypeRef, confidence, mime, lens }.

Decisiones clave de arquitectura

Por qué ente-incarnate es separado

La rutina de aislamiento (clone+ns+cgroup+rlimits) vivía dentro del Init (ente-soma). Era global y atada a PID 1. La separamos en Fase 0 para que shipote (y futuros supervisores) puedan reusarla sin implicar privilegio.

Hoy ente-soma es un wrapper de ~30 líneas sobre ente-incarnate que preserva la semántica histórica (set_bus_sock global + strict_caps=false). ente-zero sigue funcionando intocado.

Por qué pipeline-level restart relanza el pipeline ENTERO

Los pipes intermedios de un comando que muere ya están cerrados. Restart parcial no funciona. La identidad ULID del pipeline cambia entre intentos; restart_count y current_backoff_ms se preservan en el supervisor.

Por qué replay buffer guarda ANTES del broadcast

Si guardas después, un subscriber que conecta entre broadcast::send y replay.push_back puede perder ese chunk. El orden importa: replay → broadcast.

Por qué pipe2(O_CLOEXEC) siempre

Sin O_CLOEXEC, el siguiente comando del pipeline hereda copias del write-end del pipe anterior. El read-end nunca ve EOF y el consumidor cuelga. Bug encontrado y arreglado en Fase F.

Por qué AsyncFd + non-blocking en taps/mergers

tokio::fs::File::from_std con un FD blocking bloquea el reactor entero. La solución: fcntl(F_SETFL, O_NONBLOCK) antes de envolver en AsyncFd<OwnedFd>. Patrón usado en splitter, merger y log drainer.

Por qué token-bucket real reemplazó el sleep simple

El sleep chunk_size / rate era uniforme y no permitía burst legítimo. TokenBucket con refill por wall time + capacity=rate_bps (1s burst) permite burst real y throttle suave.

Por qué dirty: AtomicBool para snapshot incremental

Cada SIGTERM hacía full re-serialize aunque no hubiera cambios. Ordering::Relaxed está bien porque el peor caso es un save innecesario. restore_snapshot resetea dirty=false (acabamos de hidratar, no es mutation).

Por qué auth con SO_PEERCRED y target: "audit"

SO_PEERCRED es automático en Unix sockets — leer ucred.uid del peer y comparar con el propio uid. Sin grupos, sin caps. SHIPOTE_TRUST_ANYONE=1 es el escape hatch.

target: "audit" separa el log de mutaciones del log normal. Filtrable con EnvFilter: SHIPOTE_LOG=warn,audit=info.

Versión de snapshot

VersiónCamposForward-compat
v1workspaces✓
v2+ saved_pipelines✓ (default vacío)
v3+ live_pipelines✓ (default vacío)
v4+ stats_history per workspace (cap 16)✓ (default vacío)

SNAPSHOT_VERSION = 4. Reader rechaza versiones > 4 (forward-incompat). Todos los campos nuevos usan #[serde(default)].

Wire protocol

Todos los mensajes son enums tagged externamente, serializados con postcard. Framing: u32 BE length-prefix + payload. Max frame: 1 MiB.

Requests