250 líneas de comentario en 151 scripts. Control verificado: el diff no toca NI UNA línea que no empiece por #, y la sintaxis de los 151 pasa. El barrido saltea heredocs y cadenas triples, y el guardián DISPARÓ 3 veces: las tres eran el MOTD que el script escribe DENTRO de la imagen construida — texto del producto, no comentario del script. Se cambiaron aparte y a propósito, que es rebranding, no limpieza. Y el hallazgo caro: casaba contra , que es el TARGET de tracing — o sea el module_path!, o sea el nombre del crate. La etapa 4 lo movió a y el script quedó casando NADA. No fallaba: imprimía cero atribuciones, indistinguible de un log sin problemas. Comprobado con el binario (RUST_LOG=info sobre zlib), no deducido. Ahora acepta las dos, y tiene que seguir aceptándolas porque los logs viejos en disco dicen la vieja. Además 14 rutas de módulo en docs, que el barrido anterior no tocó porque no es frontera de palabra.
357 lines
28 KiB
Markdown
357 lines
28 KiB
Markdown
# SDD 10 — Roadmap
|
||
|
||
## Estrategia: validar la capa AI-nativa sobre Alpine antes de la distro propia
|
||
|
||
La innovación de `takana` no es la distro — es el substrato AI-nativo (build determinista +
|
||
mutable + diario + `.swm`). Construir bootstrap + init + userland de cero antes de validar esa
|
||
capa sería arriesgar meses contra una hipótesis no probada. Por eso:
|
||
|
||
> **Fase 0–6:** montamos `takana` **sobre Alpine** (musl + busybox + FHS mutable, ya cocidos).
|
||
> Validamos el bucle completo en semanas.
|
||
> **Track posterior:** una vez probado, bajamos a distro propia (init propio, bootstrap propio,
|
||
> userland propio) reusando todo el tooling sin retrabajo.
|
||
|
||
Ver [ADR 0002](adr/0002-alpine-first.md).
|
||
|
||
---
|
||
|
||
## Fases (sobre Alpine)
|
||
|
||
### Fase 0 — Laboratorio de build ✅
|
||
- [x] `takana-core`: tipos `Recipe`, `ArtifactHash`, `Store` (BLAKE3, layout `/store`).
|
||
- [x] `takana-build`: sandbox con `bubblewrap` + `zig cc` → binario musl estático.
|
||
- [x] CAS: hashing de entrada, caché por hash, sellado en el store.
|
||
- [x] `takana build <recipe.toml>` → imprime el hash del artefacto.
|
||
- [x] Fuente git **y** tarball (sha256 fijado), patches opcionales.
|
||
- [x] Heurística de build system: autotools / cmake / meson / make plano.
|
||
- [x] Caché persistente de zig entre builds (~30s/build ahorrados).
|
||
|
||
### Fase 1 — Hidratación ✅
|
||
- [x] `hydrate(hash, target, mode)`: hardlink a FHS, `patchelf` para el caso dinámico.
|
||
- [x] `takana hydrate <hash> --into /`.
|
||
|
||
### 🎯 Primer entregable (Fase 0 + 1) ✅
|
||
**GNU grep 3.12 compilado estático desde el tarball upstream, sellado en el store
|
||
(`b3:b81e893e…`), hidratado por hardlink a `/usr/bin/grep` y ejecutado dentro del rootfs
|
||
Alpine vía `bwrap`** — confirma el corazón "fábrica funcional → FHS mutable" sobre el
|
||
substrato Alpine. La VM/LXC dedicada queda como ejercicio de empaque, no como
|
||
pre-requisito de validación.
|
||
|
||
### Fase 2 — Overlay de experimentación ✅
|
||
- [x] Crate `takana-overlay`: `try` / `commit` / `discard` / `status` sobre `overlayfs`.
|
||
- [x] Manifiesto persistente en `<state_root>/<id>/state.json`; `status` enumera.
|
||
- [x] `commit` promociona archivos y procesa whiteouts (char dev 0/0) como remove.
|
||
- [x] Subcomandos del CLI: `takana try [targets…]`, `commit <id>`, `discard <id>`, `status`.
|
||
- [x] Tests E2E con bwrap+user-ns, gateados en `TAKANA_OVERLAY_TESTS=1` (kernel-dependiente).
|
||
- [x] Hook al diario en `commit` (Fase 3 lo añadió vía `commit_with_journal`).
|
||
- [x] Anidación real de overlays. Un `try` sobre un target ya cubierto apila un overlay nuevo
|
||
(el kernel usa el merged view inferior como `lowerdir`). Orden de apilamiento total y
|
||
determinista vía `stack_key` (`created_at` + `id`, con `seq` por proceso en `fresh_id`
|
||
para evitar colisiones en ráfaga). `commit`/`discard` exigen **LIFO**: `blocking_overlays`
|
||
detecta capas más jóvenes que solapan targets (igualdad o ancestro de path) y la operación
|
||
falla con `Error::Shadowed` si las hay. Guard cubierto por 6 unit tests; el stack real
|
||
(capa 2 ve la 1, LIFO rechaza commit de abajo, commit arriba→abajo fusiona) por
|
||
`overlay_nested_stack_inside_userns`, gated en `TAKANA_OVERLAY_TESTS`. Pendiente menor:
|
||
unicidad de orden entre procesos concurrentes (track posterior).
|
||
|
||
### Fase 3 — Diario de mutaciones ✅
|
||
- [x] Crate `takana-journal`: `MutationEvent`, `Source` (HammerHydrate/HammerCommit/External),
|
||
append/read/tail/follow JSON-líneas. RFC 3339 sin chrono.
|
||
- [x] `hammerd` con `fanotify` (FAN_CLOSE_WRITE) sobre `/bin`,`/sbin`,`/usr/bin`,`/usr/sbin`,
|
||
`/lib`,`/usr/lib`,`/etc`. Filtra eventos bajo overlays activos. Fallback graceful sin
|
||
CAP_SYS_ADMIN.
|
||
- [x] `takana journal [--tail N] [--follow] [--format pretty|json]`.
|
||
- [x] `takana commit` registra cada archivo promocionado vía `commit_with_journal`.
|
||
- [x] Refinar la `op` del watcher. `watcher::classify_op(&journal, path, deleted)` deriva
|
||
`Delete` (el fd resuelve a `" (deleted)"`, que ahora recortamos del path en vez de
|
||
colarlo al diario), `Edit` (el diario ya tenía un evento previo no-`Delete` del path) o
|
||
`Create` (primera mutación observada, o resurrección tras un `Delete`). Es la divergencia
|
||
que el diario atestigua, no la verdad absoluta del FS. La atribución plena vía
|
||
`FAN_REPORT_DFID_NAME` (nombre en eventos de directorio, `rename`-sobre-destino,
|
||
`open_by_handle_at`+`CAP_DAC_READ_SEARCH`) queda para el track posterior: `nix` 0.30 no
|
||
parsea los info-records de FID y el modo FID perdería el fd que hoy hashea el contenido.
|
||
- [x] Hash del contenido tras la mutación (`content_hash`) y de-dup idempotente.
|
||
`takana_journal::content_hash_of`/`hash_file` (blake3 plano, estilo `b3sum`, distinto del
|
||
`of_inputs` de artefactos). `Journal::record_dedup` omite el evento si deja el archivo
|
||
idéntico al último de ese path (o un `Delete` sobre algo ya borrado); `last_for_path` lo
|
||
resuelve. El watcher hashea cada `CLOSE_WRITE` y usa `record_dedup`: una reescritura sin
|
||
cambios ni ensucia el diario ni despierta el bus.
|
||
|
||
### Fase 4 — Formato y flujo `.swm` ✅
|
||
- [x] `Swm` de/serialización YAML estable (roundtrip).
|
||
- [x] `verify_schema` (estructura + invariantes por mutación) y `verify_base` (distro_version
|
||
+ pins) con `BaseRef` / `BaseCompat`.
|
||
- [x] `apply_config_edit` (hunks `-/+` con búsqueda exacta, no-fuzz) y `apply_file_drop`
|
||
(base64 inline + verificación BLAKE3 vs `content_hash`).
|
||
- [x] Bridge `Mutation::SourcePatch` → `Recipe` + `build` + hidratación.
|
||
- [x] CLI: `takana apply [--prefix DIR] [--base-ref base.json]`,
|
||
`takana swm-verify`, `takana export --journal DIR > out.swm`.
|
||
- [x] `patch_url` / `content_url` remotos. `takana_build::download` (curl, sólo fuera del
|
||
sandbox; acepta `file://` para tests offline). `build_source_patch` descarga `patch_url`
|
||
al `swm-recipes/<commit>.patch` antes de compilar; `takana apply` descarga `content_url`
|
||
y **verifica BLAKE3 antes de escribir** (hash erróneo ⇒ nada tocado). Su integridad la
|
||
cubre `content_hash` (file_drop) y el build reproducible + `expected_hash` (patch).
|
||
- [x] Provenance en `export`: mapa artefacto→receta vía sidecar `.hammer/recipe.toml` que
|
||
`takana-build::build` escribe dentro del artefacto antes de sellar. `takana export`
|
||
agrupa eventos por `artifact_hash` y emite UN `source_patch` por grupo cuya receta
|
||
sea recuperable; lo demás cae al fallback `file_drop`. Source `git` **y `tarball`**
|
||
modelados: `Mutation::SourcePatch` (y `RecipeInline`) llevan campos `repo`/`commit`
|
||
**xor** `tarball`/`sha256` (resueltos por `swm::swm_source_kind`, misma regla que
|
||
`recipe::Source::kind`); `takana export` reconstruye ambos sin caer a `file_drop`.
|
||
- [x] Firma `signature` (ed25519) y `TrustStore` local. `takana_core::sign`: `KeyPair`
|
||
(genera/carga/escribe claves), `TrustStore::load(dir)` (lee `*.ed25519.pub`),
|
||
`Swm::verify_signature(&trust) → SigStatus` (`trusted`/`unknown-key`/`bad-sig`/
|
||
`unsigned`) sobre bytes canónicos JSON del manifiesto sin la firma. CLI:
|
||
`takana keygen`, `takana swm-sign`, y `takana swm-verify --trust DIR`. La firma reporta
|
||
autoría/integridad; NO autoriza promover (sigue mandando reproducir + commit). SDD 09 §3,§5.
|
||
- **Hecho cuando:** exportas un cambio, lo aplicas en otra máquina y reproduce idéntico.
|
||
✅ Demostrado en `crates/hammer-cli/tests/swm_roundtrip.rs` para
|
||
`config_edit` + `file_drop`. `source_patch` reusa el camino de Fase 0/1 (gated en
|
||
`TAKANA_NETWORK_TESTS`).
|
||
|
||
### Fase 5 — Bus de agente ✅ *(queda `CRASHED` real, diferido al track posterior)*
|
||
- [x] `/run/init.control` (FIFO humano): `mkfifo`, reader que loguea cada línea, y
|
||
`takana ctl <line>` que escribe al FIFO con error claro si no existe.
|
||
- [x] `/run/agent.sock` (JSON-líneas) con handshake `Hello/Welcome`, auth via
|
||
`SO_PEERCRED` y `CapsPolicy` por UID.
|
||
- [x] Comandos `Compile`/`Inject`/`Query`/`Init`; eventos
|
||
`Welcome`/`BuildReady`/`BuildFailed`/`Injected`/`QueryResult`/`InitAck`/
|
||
`Modified`/`Crashed`/`Error`.
|
||
- [x] EventBus in-process: el watcher publica `Modified` y todas las conexiones lo reciben.
|
||
- [x] Subsistemas independientes en `hammerd::main` (FIFO/watcher/bus en threads); si uno
|
||
falla en init, los demás siguen.
|
||
- [x] Política expresiva leída de `/etc/hammer/agent-caps.toml` (`hammerd --agent-caps`).
|
||
`takana_core::AgentCapsConfig`: `default` + `[[rule]]` por `uid`/`gid` (primera que casa
|
||
gana), `caps_for(uid,gid)`. `bus::policy_from_config` la enchufa; sin fichero o con
|
||
fichero inválido, cae a la política por-UID por defecto (avisando). Ejemplo en
|
||
`examples/agent-caps.toml`. El `default_policy` hardcoded queda como fallback.
|
||
- [ ] `CRASHED` real (requiere supervisión de servicios, que llega con el init propio del
|
||
track posterior).
|
||
- [x] `BuildFailed.log_tail` con cola real del lab. `Sandbox::run` hace *tee* de
|
||
stdout/stderr (sigue viéndose en vivo) y retiene las últimas 50 líneas; al fallar una
|
||
fase devuelve `BuildFailure { reason, log_tail }` envuelto en `Error::Other`. El bus lo
|
||
recupera por `BuildFailure::from_error` (downcast) y separa `reason` del log textual del
|
||
compilador, para que la IA reaccione al error real, no sólo al mensaje de Rust.
|
||
- **Hecho cuando:** un cliente externo dispara un build y recibe el evento de fin por el
|
||
socket. ✅ Camino implementado (`Compile` → `BuildReady`/`BuildFailed`) y la maquinaria
|
||
alrededor cubierta por `crates/hammerd/tests/bus_e2e.rs`: handshake con peer creds,
|
||
gating `no_cap`, `Query`, `Modified` fan-out, `Init`→FIFO. Un `Compile` real reusa el
|
||
camino de Fase 0/1 (gated en `TAKANA_NETWORK_TESTS`).
|
||
|
||
### Fase 6 — Integración de la IA ✅
|
||
- [x] Crate `takana-agent` con tres piezas:
|
||
- `AgentClient`: cliente síncrono del bus (handshake, `compile`/`inject`/`query`/`init`
|
||
bloqueantes, drenado de eventos asíncronos).
|
||
- `IntentTranslator` (trait) + `MockTranslator` cargado desde un `IntentCatalog` YAML
|
||
(intent → `.swm` pre-armado). La integración con un LLM real se enchufa detrás del
|
||
mismo trait sin tocar el bucle.
|
||
- `Orchestrator` con `run(intent) → Proposal`: plan → schema → base → try (overlay
|
||
o prefix) → apply (mutaciones puras + source_patch vía bus opcional) → verify →
|
||
propose.
|
||
- [x] CLI: `takana ai <intent> --catalog F [--prefix DIR --base-ref F --bus SOCK]`.
|
||
- [x] Tipos del protocolo del bus movidos a `takana-core::proto` para que `hammerd` y
|
||
`takana-agent` los compartan.
|
||
- [x] Tests:
|
||
- 10 unit en `takana-agent` (translator + catalog + orchestrator).
|
||
- 3 e2e del bucle agéntico (prefix tmp → archivos esperados en disco).
|
||
- 1 e2e del cliente contra un *stub* del bus (handshake + Compile→BuildReady + Modified
|
||
asíncrono).
|
||
- [x] Traductor LLM real (Claude API u otro), opcional vía feature flag o crate aparte.
|
||
`ClaudeTranslator` detrás de la feature `llm-claude`, con `ureq + rustls`. Modelo
|
||
por defecto `claude-opus-4-8`, adaptive thinking + `effort=high`. Trait `HttpClient`
|
||
inyectable para tests sin red. CLI: `takana ai --llm` (mutuamente excluyente con
|
||
`--catalog`). Sin la feature, el flag falla con mensaje claro.
|
||
- [x] Lenguaje de consulta del sistema (SDD 08 §6) para que la IA refiera servicios y
|
||
archivos sin rutas frágiles. Forma `kind:value` (`bin`, `file`, `pin`, `service`,
|
||
`depends`), evaluable local (`takana query <expr>`) y remoto vía bus
|
||
(`Command::Query{what:"expr"}`). Parser ELF64 mínimo para extraer `DT_NEEDED`.
|
||
- [x] Bucle de auto-reparación (cliente reacciona a `Crashed` con un nuevo plan).
|
||
`Orchestrator::run_with_repair(intent, &RepairPolicy)` drena async events del bus
|
||
tras cada apply; si llega `Crashed`, formatea una nueva intención y reentra hasta
|
||
`max_attempts`. El `Proposal` final lleva el `repair_chain` completo para que el
|
||
humano vea la evolución antes de hacer commit. CLI: `takana ai --repair-max-attempts`.
|
||
- **Hecho cuando:** una intención en lenguaje natural produce un cambio probado en overlay,
|
||
presentado para `commit` humano. ✅ Demostrado por `takana ai` con `MockTranslator`:
|
||
intent → `.swm` → mutaciones aplicadas → `Proposal` con `overlay_id` + checks. El humano
|
||
decide `commit`/`discard`. El upgrade a LLM real reusa todo el bucle.
|
||
|
||
---
|
||
|
||
## Track posterior — distro propia
|
||
|
||
Diseño completo en [SDD 11 — Bootstrap from-scratch](11-bootstrap.md). Resumen:
|
||
|
||
- **Bootstrap from-scratch** en tres etapas ([SDD 11 §3](11-bootstrap.md)):
|
||
- **Stage 0** ✅ — crate `takana-bootstrap` + `takana bootstrap stage0`: ingiere el toolchain
|
||
semilla pinned (`SeedSpec`, hash por identidad), verifica sha256 antes de sellar,
|
||
idempotente. Cubierto por 4 unit + 1 e2e de CLI (offline, `file://`).
|
||
- **Stage 1** ✅ — userland mínimo, **booteado end-to-end en QEMU**. Recetas pinned `musl` 1.2.5 +
|
||
`busybox` 1.36.1 (estáticas, zig cc) + `hammerd` + `arje-zero` (recetas Cargo: repo pinned +
|
||
deps vendoreadas en el fetch, build nativo crt-static vía `cargo rustc`); `takana bootstrap
|
||
stage1` los construye con la semilla, ensambla el rootfs FHS y lo sella. El init es **arje-zero
|
||
como PID 1** (`/sbin/init`→arje-zero) con su seed card `/ente/seed.card.json`; al bootear,
|
||
arje-zero carga+valida la seed y **supervisa hammerd + getty** (el `agent.sock` y el watcher
|
||
fanotify de hammerd levantan). El **`CRASHED` real está demostrado**: `kill hammerd` → arje lo
|
||
detecta (`Killed(SIGTERM)`), programa restart con backoff y lo re-encarna — la supervisión del
|
||
init propio que la Fase 5 difirió ([ADR 0007](adr/0007-arje-como-init-propio.md)). Diseño en
|
||
[SDD 12](12-init-real.md); corrida y fixes en el [runbook](runbooks/stage1-vm-boot.md). Pendiente:
|
||
bus único (B.2: exponer el `CRASHED` a la capa de IA) y atestación (A1/A2).
|
||
- **Stage 2** ✅ — rebuild nativo dentro del rootfs y diff de hashes ⇒ **auto-alojamiento
|
||
bit-reproducible CONFIRMADO end-to-end (host↔VM)**. `KVM=1 MEM=24576
|
||
./scripts/selfhost-verify.sh` reconstruye los 4/4 dentro del rootfs Stage 1 con sólo su
|
||
propio toolchain (arje-zero cu=1 compila en ~27 min in-VM), sella el rootfs y su
|
||
`of_tree(stage1') = b3:0039b2b9…` **iguala** la referencia ⇒ `✓ REPRODUCIBLE: stage1' ==
|
||
stage1`. El último no-determinismo (#3, `arje-zero`/`codegen-units`) se cazó y eliminó
|
||
imponiendo `CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1` en el sandbox (commit `4c9bcc0`).
|
||
Corrida y veredicto en el [runbook](runbooks/stage1-vm-boot.md) §8.
|
||
- Reemplazar el init de Alpine por **tu init** (bus por pipes nativo) — habilita el `CRASHED`
|
||
real que la Fase 5 dejó diferido. **Decisión: adoptar `arje`** (init from-scratch ya existente
|
||
en el monorepo `tawasuyu`, con PID 1 + supervisión real) en lugar de escribir uno nuevo —
|
||
ver [ADR 0007](adr/0007-arje-como-init-propio.md). Trae bus único, CAS BLAKE3 compartido y
|
||
confianza en capas (procedencia takana + atestación arje).
|
||
- Userland propio (busybox/toybox a elección), `/etc/service/*` propios.
|
||
- Decidir particionado/montaje de `/store` y `/var/lib/hammer`.
|
||
- Log de transparencia para compartir en comunidad ([SDD 09](09-trust-model.md) §4): el
|
||
`bootstrap.json` de [SDD 11 §4](11-bootstrap.md) es su embrión.
|
||
- Mini-lenguaje de consulta del sistema para la IA ([SDD 08](08-ai-integration.md) §6).
|
||
|
||
---
|
||
|
||
## Estado actual
|
||
- ✅ **Fases 0–6 cerradas** sobre Alpine: build determinista → hidratación → overlay
|
||
(con anidación LIFO) → diario (con `op` Create/Edit/Delete) → `.swm` firmado → bus de
|
||
agente → bucle agéntico con IA (mock + Claude real tras `llm-claude`).
|
||
- ✅ El bucle completo está validado: una intención en lenguaje natural produce un cambio
|
||
probado en overlay, presentado para `commit` humano.
|
||
- ✅ 214 tests verdes en el workspace (1 e2e de overlayfs gated en `TAKANA_OVERLAY_TESTS`;
|
||
los de red en `TAKANA_NETWORK_TESTS`).
|
||
- ⏳ Único ítem de las fases diferido: `CRASHED` real (necesita supervisión de servicios →
|
||
init propio del track posterior).
|
||
- ✅ **Track posterior arrancado y con su hito mayor cerrado:** bootstrap from-scratch
|
||
Stage 0 → Stage 1 (booteado en QEMU, arje-zero como PID 1, `CRASHED` real) → **Stage 2
|
||
auto-alojamiento bit-a-bit confirmado** (`✓ REPRODUCIBLE`, host↔VM). Baseline reproducible
|
||
4/4: `of_tree(stage1)=b3:0039b2b9…` (cu=1).
|
||
- ✅ **CERRADO — auto-alojamiento *puro* (variante b, [SDD 11 §7.2b](11-bootstrap.md)):** que
|
||
takana construya el toolchain del builder desde fuente (no Alpine), reemplazando piezas una a una
|
||
con Stage 2 verificando cada paso. **Pieza 1 hecha:** GNU make `4.4.1` (`recipes/make.toml`) se
|
||
compila desde fuente con el lab — estática musl, sellada `b3:fbad44ac…`, reproducible bit-a-bit.
|
||
**Swap implementado:** `BuilderSpec.swaps` + el flag `takana bootstrap builder --swap make=<hash>`
|
||
montan el `make` takana sobre `/toolchain/usr/bin/make` (pisando el de Alpine) y lo anclan en el
|
||
**hash lógico** del builder ⇒ la procedencia deja de ser "todo Alpine" y se vuelve auditable
|
||
(pura `b3:8a370f5d…` vs swapped `b3:e7e2282c…` en el host). El verify lo expone con `SWAP_MAKE=1`.
|
||
**Stage 2 in-VM con el swap (2026-06-13): `✗ DIVERGENTE` → causa raíz hallada y arreglada, make
|
||
inocente.** El swap dio `of_tree(stage1')=9adefb82… ≠ 0039b2b9…`, pero el control **sin** swap dio el
|
||
*mismo* `9adefb82…` ⇒ el make no era la causa. Bisección (descartando uno a uno: musl/busybox idénticos
|
||
con takana-make incluso a `-j1`; hammerd reproduce): el único flaky era **arje-zero**, no-determinista
|
||
**incluso host-a-host** (~62 KB de diferencia, mismo rustc/commit/flags) — el backend **paralelo** de
|
||
rustc/LLVM (ThinLTO) que `codegen-units=1` NO serializa. Builds seriales (la VM tiene 1 vCPU, o
|
||
`taskset -c 0`) reproducían bit-a-bit a `9adefb82…`; los paralelos divergían. El viejo `0039b2b9…` era
|
||
un build paralelo no-fiable. **Fix:** `CARGO_BUILD_JOBS=1` en el sandbox (serializa el jobserver ⇒
|
||
backend de 1 hilo, independiente del nº de CPUs) — verificado: arje-zero a todas las CPUs + `JOBS=1`
|
||
da byte-idéntico al serial. Baseline del host refrescado y `EXPECT_REF` re-baseado a `9adefb82…`.
|
||
**Pieza 2 — busybox (sh+coreutils) validada en host:** el `/toolchain` Alpine es mixto (busybox para
|
||
sh/sed/grep/awk/tar/find, GNU coreutils para cp/mkdir/install); el swap monta el busybox estático de
|
||
takana sobre `/toolchain/bin/busybox` (`--swap busybox=<hash>:bin/busybox`, sin código nuevo).
|
||
Con make+busybox de takana los 4/4 (incl. busybox compilándose a sí mismo con takana-busybox de shell)
|
||
reproducen `of_tree=9adefb82…`. Expuesto con `SWAP_BUSYBOX=1`. **✓ REPRODUCIBLE in-VM con ambos swaps
|
||
(2026-06-13):** `KVM=1 MEM=24576 SWAP_MAKE=1 SWAP_BUSYBOX=1 ./scripts/selfhost-verify.sh` en `libre`
|
||
reconstruyó los 4/4 dentro de la VM con el `/toolchain` hammerizado (make `fbad44ac…` + busybox
|
||
`56664d70…` pisando los de Alpine) y el `of_tree(stage1')` **igualó** la referencia `9adefb82…` ⇒
|
||
`✓ REPRODUCIBLE: stage1' == stage1`, `DRIVER_RC=0` (~54 min, arje-zero cu=1 in-VM). **La procedencia
|
||
del builder ya no es "todo Alpine" y el auto-alojamiento sigue bit-a-bit.** **Pieza 3 — linux-headers
|
||
(`recipes/linux-headers.toml`, kernel 6.16.12, validada en host):** los headers UAPI que musl/busybox
|
||
`#include`. Construidos con `make headers` (NO `headers_install`: su `rsync` final falta en el toolchain
|
||
hermético) + `unifdef` vía HOSTCC=zig cc. El swap es de **directorio**, no de binario: se extendió
|
||
`assemble_builder` para reemplazar árboles enteros (los 13 subdirs kernel-owned `usr/include/{linux,asm,
|
||
asm-generic,cxl,fwctl,misc,mtd,rdma,regulator,scsi,sound,video,xen}`), dejando intactos los de musl
|
||
(bits/sys/net…). Hallazgo clave: el paquete de Alpine NO es el `make headers` crudo — trae 5 archivos
|
||
distintos a la salida vainilla; 3 de ellos (`scsi/{scsi,scsi_ioctl,sg}.h`, legacy userspace, NO UAPI)
|
||
son **requeridos** porque el applet `eject` de busybox los incluye y sin ellos no compila. Se aportan
|
||
content-pinned vía `recipes/linux-headers-alpine-compat.patch` (+ 2 cosméticos: kernel.h/if_tunnel.h) y
|
||
se pisan sobre la salida. Criterio de éxito fuerte alcanzado: **`diff -r` del header-tree takana contra
|
||
el de Alpine = vacío** ⇒ el toolchain swapeado es byte-idéntico al baseline ⇒ todo lo que compile abajo
|
||
reproduce de por sí (garantía por construcción, más fuerte que un rebuild). Expuesto con
|
||
`SWAP_LINUX_HEADERS=1`; ensamblado end-to-end en host OK. Falta sólo la corrida in-VM para el sello.
|
||
**Pieza 4 — bwrap (`recipes/bwrap.toml`, bubblewrap 0.11.0, validada en host):** EL sandbox del propio
|
||
lab (`takana-build/src/sandbox.rs`). Binario estático ⇒ swap de archivo sobre `/toolchain/usr/bin/bwrap`.
|
||
Como tool del toolchain (no input del 4/4) NO necesita casar byte-a-byte con Alpine, sólo aislar igual.
|
||
Tres sub-hallazgos que la hicieron más jugosa que make/busybox: (1) el toolchain Alpine no trae meson/
|
||
ninja/python — bypaseamos meson compilando los 4 `.c` de bubblewrap directo con `zig cc` (+`config.h`
|
||
trivial generado a mano); (2) depende de **libcap**, cuyo -dev (libcap.a + `sys/capability.h`) tampoco
|
||
está en el toolchain ⇒ nueva receta `recipes/libcap.toml`; (3) para que el build de bwrap viera libcap se
|
||
agregó **materialización de build-deps** al lab: `deps.build` ahora se construye recursivamente y cada
|
||
artefacto sellado se apila como capa `--overlay-src` BAJO el rootfs del sandbox, dejando sus
|
||
`usr/{include,lib,lib/pkgconfig}` en `/usr` — pkgconf y zig cc los hallan sin plumbing de flags (recetas
|
||
sin deps quedan byte-iguales, baseline intacto). Validación host fuerte: takana-bwrap es estático, corre
|
||
`--version`/sandboxea, y **musl rebuildeó byte-idéntico** usándolo de sandbox (bisección). Expuesto con
|
||
`SWAP_BWRAP=1`; falta sólo correrla in-VM.
|
||
**Pieza 5 — coreutils (`recipes/coreutils.toml`, GNU 9.8, validada en host):** cp/mkdir/ln/chmod/mv/
|
||
install que el `make install` de musl/busybox usa. Empaquetada multicall (`--enable-single-binary`,
|
||
igual layout que Alpine: un `/bin/coreutils` + ~100 symlinks), así **un solo swap del binario** rutea
|
||
todos los applets. Tool, no input ⇒ build vainilla sin patches. Bisección host fuerte: **musl Y busybox
|
||
rebuildearon byte-idéntico** (`57b66a2e`, `56664d70`) con takana-coreutils pisando el de Alpine.
|
||
Expuesto con `SWAP_COREUTILS=1`. Falta correrla in-VM.
|
||
**Herramientas de toolchain extra desde fuente (provenance, no en el camino del 4/4 mínimo):**
|
||
`recipes/{patch,m4,pkgconf}.toml` — GNU patch 2.8 (lo usa `apply_patches`), GNU m4 1.4.20 (base
|
||
autotools) y pkgconf 2.5.1 (lee los `.pc` que materializa el lab). Construidas estáticas con zig cc
|
||
y validadas (corren, versión correcta, funcionales). El 4/4 mínimo (musl/busybox/hammerd/arje-zero)
|
||
no las invoca, así que no llevan flag `SWAP_*` dedicado: son swapeables con el escape `SWAPS="name=hash:rel"`.
|
||
Avanzan el "builder reconstruible al completo, no todo-Alpine" aunque el verify no las ejercite hoy.
|
||
**✅ CORRIDA in-VM acumulada (2026-06-14): los 5 swaps de sandbox juntos `✓ REPRODUCIBLE`.**
|
||
`KVM=1 MEM=... SWAP_MAKE=1 SWAP_BUSYBOX=1 SWAP_LINUX_HEADERS=1 SWAP_BWRAP=1 SWAP_COREUTILS=1
|
||
./scripts/selfhost-verify.sh` reconstruyó los 4/4 dentro de la VM con el `/toolchain` hammerizado y
|
||
el `of_tree(stage1')` igualó `9adefb82…` bit a bit (~46 min, sobrevive presión de memoria). Recetas
|
||
extra de toolchain desde fuente añadidas después: `recipes/binutils.toml` (GNU binutils 2.45.1, CC=gcc
|
||
porque zig miscompila → segfault, link dinámico, inerte al of_tree porque zig ya provee as/ld/ar;
|
||
swapeable vía `SWAPS=`).
|
||
- ✅✅✅ **CERRADO — frente rust/llvm (la última pieza grande del toolchain):** cadena purista
|
||
`mrustc → rustc 1.90.0 → 1.91.0 → 1.91.1` con `x.py` real, reusando un LLVM 20.1.8 externo en todo
|
||
(sin rebuild). `rust-1.91.1-prefix/bin/{rustc,cargo}` corren standalone (rpath, host musl), versión
|
||
EXACTA de Alpine. **Auto-consistencia VERIFICADA in-VM (2026-06-18):** `SWAP_RUST=1 KVM=1
|
||
PRESEED=hammerd ./scripts/selfhost-verify.sh` → `✓ REPRODUCIBLE` bit a bit (host y VM dan
|
||
`of_tree=7fa6cb4e…` = `RUST_EXPECT_REF`, ≠ `9adefb82…` Alpine). Para el rebuild bit-reproducible
|
||
se committeó el `Cargo.lock` de arje-zero en tawasuyu + se bumpeó la receta (`--locked`); `PRESEED=hammerd`
|
||
es OBLIGATORIO (arje-zero vendorea ~1973 crates y un rebuild in-VM completo desborda RAM). Artefactos en
|
||
`scripts/rust-frontier/`.
|
||
- ✅✅✅ **CAPSTONE (2026-06-19): LOS 6 SWAPS JUNTOS `✓ REPRODUCIBLE` IN-VM** — los 5 tools de sandbox +
|
||
`SWAP_RUST` a la vez, `of_tree=7fa6cb4e…` bit a bit (~7 min, `MEM=12288 PRESEED=hammerd`, sin swap en el
|
||
host). El auto-alojamiento del toolchain del builder está COMPLETO; las piezas inertes al 4/4
|
||
(binutils/pkgconf) quedan salteadas por diseño. Capa de arranque restante (status libc, no vale
|
||
de-Alpinizar): gcc, m4, libz/libzstd runtime.
|
||
- ✅✅✅ **CERRADO — kernel-from-source ([SDD 11](11-bootstrap.md), último eslabón de soberanía):** el
|
||
kernel Linux 6.16.12 construido por takana (`recipes/linux.toml`, `CC=gcc`, monolítico
|
||
e1000/overlay/userns=y) **BOOTEA la VM del selfhost-verify** y el rebuild in-VM da `✓ REPRODUCIBLE`
|
||
bit a bit (`TAKANA_KERNEL=1 KERNEL=<bzImage> KVM=1 MEM=16384 PRESEED=hammerd`). Config lean (build
|
||
~35→~14 min, bzImage 13→8.7 MB). El muro `pivot_root` (`/` = rootfs absoluto del mount-ns ⇒ bwrap no
|
||
pivota; quirk que el kernel host enmascaraba) se resolvió con un `/init` wrapper `switch_root` a tmpfs
|
||
(condicional a `TAKANA_KERNEL=1`, no toca el camino del kernel host). **Build-deps del kernel TODOS
|
||
de-Alpinizados:** `recipes/{flex,bison}.toml` (kconfig), `recipes/openssl.toml` (3.5.4, certs/extract-cert),
|
||
`recipes/elfutils.toml` (sólo libelf 0.194 para objtool, con shims musl mínimos). Capa de arranque
|
||
restante: m4, libz/libzstd, gcc (status libc).
|
||
- ✅ **Etapa A — orquestador `bootstrap all` + log de transparencia exportable:** `takana_bootstrap::all`
|
||
encadena stage0→stage1→stage2 en una corrida del host (`takana bootstrap all --url … --sha256 … --version …`)
|
||
y deja el `bootstrap.json` poblado; `takana bootstrap manifest` lo imprime (la superficie de publicación
|
||
del log, SDD 11 §4). El veredicto `✓ REPRODUCIBLE` lo sigue sellando el rebuild in-VM (`selfhost-verify.sh`),
|
||
no el host — `all` ancla la referencia para que ese rebuild la compare. **Pendiente de la Etapa A:** el
|
||
smoke end-to-end de I3 (bus único) contra un init **vivo** (arje+hammerd corriendo) — no reproducible
|
||
sin la VM, mismo status que cualquier sello in-VM.
|
||
- ✅ **B.2 — `CRASHED` real a la capa de IA (cableado end-to-end):** el `Event::Crashed` del
|
||
bus de agente ya tiene fuente real. **Fuente (arje):** `arje-bus` ganó `BusRequest::Subscribe`
|
||
+ `BusPayload::Event(BusEvent)`; arje-zero difunde en `on_death` `EnteCrashed{id,label,status}`
|
||
/ `EnteRestarting{delay_ms}` / `EnteExited` a las conexiones suscritas, purgando las muertas
|
||
(4 tests). **Sink (takana):** `hammerd::crashes` traduce la señal normalizada → `Event::Crashed`
|
||
→ `EventBus` → `/run/agent.sock` (3 tests). **Transporte (takana):** `hammerd::arje_link` se
|
||
suscribe al bus de arje (`$ENTE_BUS_SOCK`) y reenvía cada `BusEvent`. Decisión de wire =
|
||
**relectura del frame postcard** (mirror mínimo del subconjunto de suscriptor; no arrastra el
|
||
crate-graph de arje ⇒ takana sigue standalone). El layout está **verificado byte-a-byte contra
|
||
`arje-bus` real** (mismas versiones ulid 1.2 / postcard 1.1; frame Subscribe = `[00,01,00,0d]`)
|
||
y cubierto por un round-trip local (2 tests). Resta sólo el smoke end-to-end contra un init
|
||
vivo (no reproducible sin arje corriendo).
|
||
- ⏭️ **También pendiente (Stage 1):** atestación arje (A1/A2 — ya cableada en el repo arje,
|
||
resta sólo enchufar su veredicto de boot a este roadmap).
|
||
|
||
## Notas de entorno
|
||
- Desarrollo principal: laptop del autor.
|
||
- Host actual: Proxmox (`gioser.net`). La VM/LXC Alpine de pruebas vivirá aquí o en la laptop.
|
||
- Repo: `https://gitea.gioser.net/sergio/hammer`.
|