gioser

prueba

d270c5e67457195cd845f783c0b3b7bd66b9add0 · ruwaq Sergio · (5 killa ñawpaq) · tayta b5d8400fd · kay commitpi sach'ata qhaway

Semántico sayk'uy · 14

crates/ente-brain/src/introspect.rs

crates/ente-brain/src/kcl_loader.rs

crates/ente-brain/src/lib.rs

crates/ente-brain/src/loader.rs

crates/ente-zero/src/seed.rs

8 qillqakuna · +93 −354

diff --git a/crates/ente-brain/src/introspect.rs b/crates/ente-brain/src/introspect.rs
index 5f8ad7c..2590a12 100644
--- a/crates/ente-brain/src/introspect.rs
+++ b/crates/ente-brain/src/introspect.rs
@@ -423,7 +423,7 @@ impl IntrospectServer {
                         "ReloadRules sin path y sin rules_out configurado".into()
                     ),
                 };
-                let rules = match crate::kcl_loader::load_rules_file(&path) {
+                let rules = match crate::loader::load_rules_file(&path) {
                     Ok(r) => r,
                     Err(e) => return IntrospectResponse::Error(format!("load: {e}")),
                 };
diff --git a/crates/ente-brain/src/kcl_loader.rs b/crates/ente-brain/src/kcl_loader.rs
deleted file mode 100644
index cc447a6..0000000
--- a/crates/ente-brain/src/kcl_loader.rs
+++ /dev/null
@@ -1,143 +0,0 @@
-//! Loader de reglas desde archivos `.k` vía subprocess al CLI de KCL.
-//!
-//! ## ¿Por qué subprocess y no SDK Rust?
-//!
-//! El SDK Rust de KusionStack KCL (en el monorepo `kcl-lang/kcl`) no se
-//! publica como crate independiente en crates.io. Los crates `kcl-*` que
-//! sí están publicados (kcl-lib, kcl-api, etc.) pertenecen al proyecto
-//! KittyCAD — un lenguaje CAD distinto pese al nombre. Verificado 2026-05.
-//!
-//! Subprocess al CLI `kcl` (instalable vía `go install kcl-lang.io/cli/cmd/kcl@latest`
-//! o desde el release de GitHub) es funcionalmente equivalente al SDK:
-//! produce JSON validado contra el schema KCL declarado, sin dependencia
-//! de Go runtime en el binario final del fractal.
-//!
-//! Si `kcl` no está en PATH, el caller decide: cargar JSON crudo (skip KCL),
-//! o fallar el boot.
-//!
-//! ## Formato esperado del .k file
-//!
-//! ```kcl
-//! import .rule  # schema/rule.k
-//!
-//! rules: [Rule] = [
-//!     Rule { id = "...", priority = 5, when = ..., then = [...] },
-//!     ...
-//! ]
-//! ```
-//!
-//! Salida tras `kcl run --format json`: `{"rules": [...]}`. El loader busca
-//! la primera array en el JSON (top-level o anidada un nivel) y la deserializa.
-
-use crate::rules::Rule;
-use ente_card::EntityCard;
-use std::path::Path;
-use std::process::Command;
-use tracing::{debug, info};
-
-/// Detecta si `kcl` está disponible en PATH. Útil para degradar a JSON-only
-/// en entornos sin la toolchain.
-pub fn kcl_available() -> bool {
-    Command::new("kcl")
-        .arg("version")
-        .output()
-        .map(|o| o.status.success())
-        .unwrap_or(false)
-}
-
-/// Ejecuta `kcl run <path> --format=json` y devuelve el JSON crudo.
-pub fn run_kcl(path: &Path) -> anyhow::Result<String> {
-    let output = Command::new("kcl")
-        .arg("run")
-        .arg(path)
-        .arg("--format=json")
-        .output()
-        .map_err(|e| anyhow::anyhow!("invocando `kcl`: {e}. ¿Instalado en PATH?"))?;
-    if !output.status.success() {
-        anyhow::bail!(
-            "kcl run {} falló: {}",
-            path.display(),
-            String::from_utf8_lossy(&output.stderr)
-        );
-    }
-    debug!(path = %path.display(), out_bytes = output.stdout.len(), "kcl run ok");
-    Ok(String::from_utf8(output.stdout)?)
-}
-
-/// Carga reglas desde un archivo `.k` o JSON. Discrimina por extensión:
-/// `.k` → invoca KCL, `.json` → directo.
-pub fn load_rules_file(path: &Path) -> anyhow::Result<Vec<Rule>> {
-    let raw = match path.extension().and_then(|e| e.to_str()) {
-        Some("k") => {
-            info!(path = %path.display(), "cargando reglas vía kcl");
-            run_kcl(path)?
-        }
-        _ => {
-            info!(path = %path.display(), "cargando reglas como JSON crudo");
-            std::fs::read_to_string(path)?
-        }
-    };
-    extract_rules_from_json(&raw)
-}
-
-/// Extrae un `Vec<Rule>` de JSON que puede ser:
-/// 1. Array directo: `[{...}, {...}]`
-/// 2. Object con un campo array: `{"rules": [...]}`
-pub fn extract_rules_from_json(raw: &str) -> anyhow::Result<Vec<Rule>> {
-    let v: serde_json::Value = serde_json::from_str(raw)?;
-    let arr = match v {
-        serde_json::Value::Array(_) => v,
-        serde_json::Value::Object(map) => {
-            map.into_values()
-                .find(|x| x.is_array())
-                .ok_or_else(|| anyhow::anyhow!("JSON no contiene ningún array"))?
-        }
-        _ => anyhow::bail!("JSON debe ser array o object con campo array"),
-    };
-    let rules: Vec<Rule> = serde_json::from_value(arr)?;
-    Ok(rules)
-}
-
-// ============================================================================
-// Carga de Cards desde KCL/JSON. Cierra la "puerta genética": ninguna Card
-// se acepta sin pasar `validate()` extendido en ente-card.
-// ============================================================================
-
-/// Carga una `EntityCard` desde un archivo `.k` (vía kcl run) o `.json`.
-/// Pasa por `EntityCard::validate()` antes de devolver — falla rápida.
-pub fn load_card_file(path: &Path) -> anyhow::Result<EntityCard> {
-    let raw = match path.extension().and_then(|e| e.to_str()) {
-        Some("k") => {
-            info!(path = %path.display(), "cargando Card vía kcl");
-            run_kcl(path)?
-        }
-        _ => {
-            info!(path = %path.display(), "cargando Card como JSON crudo");
-            std::fs::read_to_string(path)?
-        }
-    };
-    let card = extract_card_from_json(&raw)?;
-    card.validate()
-        .map_err(|e| anyhow::anyhow!("Card inválida ({}): {e}", path.display()))?;
-    Ok(card)
-}
-
-/// Extrae una `EntityCard` de JSON. Acepta:
-/// 1. Object directamente serializable como EntityCard
-/// 2. Object dict con un único valor que sea EntityCard (KCL output típico)
-pub fn extract_card_from_json(raw: &str) -> anyhow::Result<EntityCard> {
-    let v: serde_json::Value = serde_json::from_str(raw)?;
-    // Intento 1: deserializar el value directamente.
-    if let Ok(c) = serde_json::from_value::<EntityCard>(v.clone()) {
-        return Ok(c);
-    }
-    // Intento 2: si es dict, buscar el primer value que parsee como Card.
-    if let serde_json::Value::Object(map) = v {
-        for (_, vv) in map {
-            if let Ok(c) = serde_json::from_value::<EntityCard>(vv) {
-                return Ok(c);
-            }
-        }
-    }
-    anyhow::bail!("JSON no contiene una EntityCard válida")
-}
diff --git a/crates/ente-brain/src/lib.rs b/crates/ente-brain/src/lib.rs
index 3723e1b..d9d3447 100644
--- a/crates/ente-brain/src/lib.rs
+++ b/crates/ente-brain/src/lib.rs
@@ -22,7 +22,7 @@ pub mod crystallize;
 pub mod dispatch;
 pub mod engine;
 pub mod introspect;
-pub mod kcl_loader;
+pub mod loader;
 pub mod metrics;
 pub mod observer;
 pub mod rules;
@@ -32,7 +32,7 @@ pub use crystallize::{detect_crystals, Crystal, CrystallizationParams};
 pub use dispatch::{dispatch_actions, ActionSink, NullSink};
 pub use engine::{EventKindDiscriminant, RuleEngine, SubjectInfo};
 pub use introspect::{IntrospectRequest, IntrospectResponse, IntrospectServer, BrainState};
-pub use kcl_loader::{kcl_available, load_card_file, load_rules_file};
+pub use loader::{load_card_file, load_rules_file};
 pub use metrics::serve_metrics;
 pub use observer::{Observer, TimedEvent};
 pub use rules::{Action, EventKind, EventPattern, LogLevel, Rule, Scope};
diff --git a/crates/ente-brain/src/loader.rs b/crates/ente-brain/src/loader.rs
new file mode 100644
index 0000000..d7b7e48
--- /dev/null
+++ b/crates/ente-brain/src/loader.rs
@@ -0,0 +1,68 @@
+//! Loader de Cards y Reglas desde archivos JSON.
+//!
+//! Sustituye al antiguo `kcl_loader.rs` (eliminado): la rama KCL invocaba
+//! un subprocess al CLI Go `kcl` que ningún target real tenía instalado y
+//! cuya validación duplicaba `EntityCard::validate()`. La fuente de verdad
+//! del shape de la Card es Rust + serde; en disco se guarda JSON crudo.
+//!
+//! Ergonomía de autoría futura (RON, Dhall, etc.) se añade como ramas
+//! adicionales aquí cuando duela escribir JSON a mano. Hoy: una sola rama.
+
+use crate::rules::Rule;
+use ente_card::EntityCard;
+use std::path::Path;
+use tracing::info;
+
+/// Carga una `EntityCard` desde un archivo JSON. Pasa por
+/// `EntityCard::validate()` antes de devolver — falla rápida.
+pub fn load_card_file(path: &Path) -> anyhow::Result<EntityCard> {
+    info!(path = %path.display(), "cargando Card desde JSON");
+    let raw = std::fs::read_to_string(path)?;
+    let card = extract_card_from_json(&raw)?;
+    card.validate()
+        .map_err(|e| anyhow::anyhow!("Card inválida ({}): {e}", path.display()))?;
+    Ok(card)
+}
+
+/// Extrae una `EntityCard` de JSON. Acepta:
+/// 1. Object directamente serializable como EntityCard.
+/// 2. Object dict con un único valor que sea EntityCard (compat con
+///    salidas de generadores que envuelven en `{"seed": {...}}`).
+pub fn extract_card_from_json(raw: &str) -> anyhow::Result<EntityCard> {
+    let v: serde_json::Value = serde_json::from_str(raw)?;
+    if let Ok(c) = serde_json::from_value::<EntityCard>(v.clone()) {
+        return Ok(c);
+    }
+    if let serde_json::Value::Object(map) = v {
+        for (_, vv) in map {
+            if let Ok(c) = serde_json::from_value::<EntityCard>(vv) {
+                return Ok(c);
+            }
+        }
+    }
+    anyhow::bail!("JSON no contiene una EntityCard válida")
+}
+
+/// Carga reglas desde un archivo JSON.
+pub fn load_rules_file(path: &Path) -> anyhow::Result<Vec<Rule>> {
+    info!(path = %path.display(), "cargando reglas desde JSON");
+    let raw = std::fs::read_to_string(path)?;
+    extract_rules_from_json(&raw)
+}
+
+/// Extrae un `Vec<Rule>` de JSON que puede ser:
+/// 1. Array directo: `[{...}, {...}]`
+/// 2. Object con un campo array: `{"rules": [...]}`
+pub fn extract_rules_from_json(raw: &str) -> anyhow::Result<Vec<Rule>> {
+    let v: serde_json::Value = serde_json::from_str(raw)?;
+    let arr = match v {
+        serde_json::Value::Array(_) => v,
+        serde_json::Value::Object(map) => map
+            .into_values()
+            .find(|x| x.is_array())
+            .ok_or_else(|| anyhow::anyhow!("JSON no contiene ningún array"))?,
+        _ => anyhow::bail!("JSON debe ser array o object con campo array"),
+    };
+    let rules: Vec<Rule> = serde_json::from_value(arr)?;
+    Ok(rules)
+}
diff --git a/crates/ente-zero/src/seed.rs b/crates/ente-zero/src/seed.rs
index 2b9c736..376bfcf 100644
--- a/crates/ente-zero/src/seed.rs
+++ b/crates/ente-zero/src/seed.rs
@@ -3,7 +3,7 @@
 //! Tres caminos:
 //!   1. `--restore <path>`: leer `FractalSnapshot` y reconstruir Semilla
 //!      con seed_id preservado + entes anteriores como genesis.
-//!   2. `seed.card` en disco: deserialize directo (prod o dev).
+//!   2. `seed.card.json` en disco: deserialize directo (prod o dev).
 //!   3. Fallback dev: sintetizar Semilla + 6 genesis Entes que ejercitan
 //!      todas las capacidades del fractal.
 
@@ -63,13 +63,14 @@ fn load_from_snapshot(path: &Path) -> anyhow::Result<EntityCard> {
 }
 
 fn load_or_synthesize(dev_mode: bool) -> anyhow::Result<EntityCard> {
-    // Buscamos primero `.k` (KCL canónico, validado por su schema), luego
-    // `.json` para compatibilidad. La puerta genética se cruza vía
-    // `ente_brain::load_card_file` que pasa por `validate()` extendido.
+    // Buscamos primero `.json` (canónico), luego sin extensión por
+    // compatibilidad con instalaciones que dejan el archivo crudo. La puerta
+    // genética se cruza vía `ente_brain::load_card_file` que pasa por
+    // `validate()` extendido.
     let candidates: &[&str] = if dev_mode {
-        &["seed.card.k", SEED_PATH_DEV]
+        &["seed.card.json", SEED_PATH_DEV]
     } else {
-        &["/ente/seed.card.k", SEED_PATH_PROD]
+        &["/ente/seed.card.json", SEED_PATH_PROD]
     };
     for cand in candidates {
         let path = PathBuf::from(cand);
@@ -83,7 +84,7 @@ fn load_or_synthesize(dev_mode: bool) -> anyhow::Result<EntityCard> {
         info!("sin seed.card — sintetizando semilla mínima (dev)");
         return Ok(synthesize_dev_seed());
     }
-    anyhow::bail!("seed.card no encontrada en /ente/seed.card[.k]")
+    anyhow::bail!("seed.card no encontrada en /ente/seed.card.json ni /ente/seed.card")
 }
 
 fn synthesize_dev_seed() -> EntityCard {
diff --git a/docs/gnome-boot-test.md b/docs/gnome-boot-test.md
index bb04f06..c48f5e4 100644
--- a/docs/gnome-boot-test.md
+++ b/docs/gnome-boot-test.md
@@ -35,14 +35,14 @@ escritorio realiza durante la sesión.
    - `ente-echo`, `brainctl`, `busctl`, `ente-journalctl`
 3. Renombra el `/init` original a `/sbin/init.systemd` (backup)
 4. Symlink `/init` → `/usr/local/bin/ente-zero`
-5. Coloca la Card Semilla en `/ente/seed.card.k`
+5. Coloca la Card Semilla en `/ente/seed.card.json`
 6. Desinstala los services systemd que ahora son shims (logind, etc) o
    los enmascara con `systemctl mask` (en la imagen base, antes de
    reescribir `/init`)
 
 ## Card Semilla para el boot test
 
-`/ente/seed.card.k` debe declarar como genesis los Entes esenciales:
+`/ente/seed.card.json` debe declarar como genesis los Entes esenciales:
 - D-Bus daemon (`/usr/bin/dbus-daemon --system`)
 - Los 8 compat-shims
 - NetworkManager
@@ -50,45 +50,13 @@ escritorio realiza durante la sesión.
   udev añade reglas de userspace — opcional)
 - gdm o sddm
 
-Ejemplo mínimo:
-
-```kcl
-import .card
-
-seed = EntityCard {
-    schema_version = 1
-    id = "01KQ_BOOT_SEED_GNOME_TEST_0"
-    label = "boot-gnome-test"
-    provides = [
-        Capability {kind = "Spawn"}
-        Capability {kind = "Journal"}
-    ]
-    soma = SomaSpec {}
-    payload = Payload {kind = "Virtual"}
-    supervision = Supervision {kind = "OneShot"}
-    genesis = [
-        # dbus-daemon — todo lo demás depende de él.
-        EntityCard {
-            schema_version = 1
-            id = "01KQ_BOOT_DBUS_DAEMON__________"
-            label = "dbus-daemon"
-            soma = SomaSpec {}
-            payload = Payload {
-                kind = "Native"
-                exec = "/usr/bin/dbus-daemon"
-                argv = ["--system", "--nofork"]
-            }
-            supervision = Supervision {
-                kind = "Restart"
-                initial_ms = 100
-                max_ms = 30000
-            }
-        }
-        # Aquí los 8 compat-shims (mismo patrón) ...
-        # Aquí gdm o sddm ...
-    ]
-}
-```
+El shape es la serialización serde de `EntityCard` (ver
+`crates/ente-card/src/lib.rs`). Para el primer arranque sin GNOME hay un
+ejemplo defensivo en `docs/seed-vps-min.json` (PID 1 + un `sleep infinity`
+supervisado). Extiéndelo añadiendo entradas a `genesis[]` con `payload` de
+forma `{"Native": {"exec": "...", "argv": [...], "envp": []}}` y
+`supervision` `{"Restart": {"initial": 100, "max": 30000}}` para los
+daemons que sí queremos restart-supervisados.
 
 ## Boot
 
diff --git a/docs/seed-gnome-test.k b/docs/seed-gnome-test.k
deleted file mode 100644
index b68d1ab..0000000
--- a/docs/seed-gnome-test.k
+++ /dev/null
@@ -1,155 +0,0 @@
-# Card Semilla para el boot test de GNOME bajo Ente #0.
-#
-# Este archivo se valida con `kcl run` contra el schema en
-# crates/ente-card/schema/card.k antes de que ente-zero lo cargue.
-#
-# Genesis declara la constelación mínima para que GNOME arranque sin
-# systemd: D-Bus daemon, los 8 compat-shims, NetworkManager, gdm.
-
-import .ente_card.schema.card
-
-# Card "supervisor genérico" reutilizable — dispara un binario con Restart.
-schema NativeRestart(EnteBase):
-    soma = SomaSpec {
-        rlimits = ResourceLimits {nofile = 16384}
-    }
-    supervision = Supervision {
-        kind = "Restart"
-        initial_ms = 100
-        max_ms = 30000
-    }
-
-
-# ----- La Semilla -----
-
-seed = EntityCard {
-    schema_version = 1
-    id = "01KQABOOTTESTSEEDFRACTAL00"
-    label = "boot-gnome-test"
-    provides = [
-        Capability {kind = "Spawn"}
-        Capability {kind = "Journal"}
-    ]
-    soma = SomaSpec {}
-    payload = Payload {kind = "Virtual"}
-    supervision = Supervision {kind = "OneShot"}
-
-    genesis = [
-        # 1. dbus-daemon — pivote del system bus, todos los demás dependen de él.
-        EntityCard {
-            schema_version = 1
-            id = "01KQABOOTTESTDBUSDAEMON___"
-            label = "dbus-daemon"
-            soma = SomaSpec {}
-            payload = Payload {
-                kind = "Native"
-                exec = "/usr/bin/dbus-daemon"
-                argv = ["--system", "--nofork", "--nopidfile"]
-            }
-            supervision = Supervision {
-                kind = "Restart"
-                initial_ms = 100
-                max_ms = 30000
-            }
-        }
-
-        # 2-9. Los 8 compat-shims D-Bus.
-        EntityCard {
-            schema_version = 1
-            id = "01KQABOOTTESTLOGIND_______"
-            label = "compat-logind"
-            provides = [Capability {kind = "LegacyLogind"}]
-            soma = SomaSpec {}
-            payload = Payload {
-                kind = "Native"
-                exec = "/usr/local/bin/ente-logind-compat"
-            }
-            supervision = Supervision {kind = "Restart", initial_ms = 100, max_ms = 30000}
-        }
-        EntityCard {
-            schema_version = 1
-            id = "01KQABOOTTESTHOSTNAMED____"
-            label = "compat-hostnamed"
-            soma = SomaSpec {}
-            payload = Payload {kind = "Native", exec = "/usr/local/bin/ente-hostnamed-compat"}
-            supervision = Supervision {kind = "Restart", initial_ms = 100, max_ms = 30000}
-        }
-        EntityCard {
-            schema_version = 1
-            id = "01KQABOOTTESTTIMEDATED____"
-            label = "compat-timedated"
-            soma = SomaSpec {}
-            payload = Payload {kind = "Native", exec = "/usr/local/bin/ente-timedated-compat"}
-            supervision = Supervision {kind = "Restart", initial_ms = 100, max_ms = 30000}
-        }
-        EntityCard {
-            schema_version = 1
-            id = "01KQABOOTTESTLOCALED______"
-            label = "compat-localed"
-            soma = SomaSpec {}
-            payload = Payload {kind = "Native", exec = "/usr/local/bin/ente-localed-compat"}
-            supervision = Supervision {kind = "Restart", initial_ms = 100, max_ms = 30000}
-        }
-        EntityCard {
-            schema_version = 1
-            id = "01KQABOOTTESTJOURNALD_____"
-            label = "compat-journald"
-            provides = [Capability {kind = "Journal"}]
-            soma = SomaSpec {}
-            payload = Payload {kind = "Native", exec = "/usr/local/bin/ente-journald-compat"}
-            supervision = Supervision {kind = "Restart", initial_ms = 100, max_ms = 30000}
-        }
-        EntityCard {
-            schema_version = 1
-            id = "01KQABOOTTESTRESOLVED_____"
-            label = "compat-resolved"
-            soma = SomaSpec {}
-            payload = Payload {kind = "Native", exec = "/usr/local/bin/ente-resolved-compat"}
-            supervision = Supervision {kind = "Restart", initial_ms = 100, max_ms = 30000}
-        }
-        EntityCard {
-            schema_version = 1
-            id = "01KQABOOTTESTPOLKIT_______"
-            label = "compat-polkit"
-            soma = SomaSpec {}
-            payload = Payload {kind = "Native", exec = "/usr/local/bin/ente-polkit-compat"}
-            supervision = Supervision {kind = "Restart", initial_ms = 100, max_ms = 30000}
-        }
-        EntityCard {
-            schema_version = 1
-            id = "01KQABOOTTESTMACHINED_____"
-            label = "compat-machined"
-            soma = SomaSpec {}
-            payload = Payload {kind = "Native", exec = "/usr/local/bin/ente-machined-compat"}
-            supervision = Supervision {kind = "Restart", initial_ms = 100, max_ms = 30000}
-        }
-
-        # 10. NetworkManager — la mayoría de distros lo prefieren sobre networkd.
-        EntityCard {
-            schema_version = 1
-            id = "01KQABOOTTESTNETWORKMGR___"
-            label = "NetworkManager"
-            soma = SomaSpec {}
-            payload = Payload {
-                kind = "Native"
-                exec = "/usr/sbin/NetworkManager"
-                argv = ["--no-daemon"]
-            }
-            supervision = Supervision {kind = "Restart", initial_ms = 200, max_ms = 30000}
-        }
-
-        # 11. gdm — display manager. GNOME settings panels via gnome-shell.
-        EntityCard {
-            schema_version = 1
-            id = "01KQABOOTTESTGDMDAEMON____"
-            label = "gdm"
-            soma = SomaSpec {}
-            payload = Payload {
-                kind = "Native"
-                exec = "/usr/bin/gdm"
-                argv = ["--no-daemon"]
-            }
-            supervision = Supervision {kind = "Restart", initial_ms = 500, max_ms = 60000}
-        }
-    ]
-}
diff --git a/scripts/build-rootfs.sh b/scripts/build-rootfs.sh
index a99bc96..77ae5b0 100755
--- a/scripts/build-rootfs.sh
+++ b/scripts/build-rootfs.sh
@@ -6,7 +6,7 @@
 #   $2 — path opcional a Card Semilla custom (.k o .json)
 #
 # Output: el rootfs queda con /init → ente-zero, binarios en
-# /usr/local/bin, y la Semilla en /ente/seed.card.k.
+# /usr/local/bin, y la Semilla en /ente/seed.card.json.
 
 set -euo pipefail
 
@@ -76,14 +76,14 @@ echo "==> /init → ente-zero"
 ln -sf /usr/local/bin/ente-zero "$ROOTFS/init"
 ln -sf /usr/local/bin/ente-zero "$ROOTFS/sbin/init"
 
-# 5. Card Semilla
+# 5. Card Semilla — JSON crudo, validado en boot por EntityCard::validate().
 mkdir -p "$ROOTFS/ente"
 if [[ -n "$SEED_CARD" && -f "$SEED_CARD" ]]; then
-    cp "$SEED_CARD" "$ROOTFS/ente/seed.card.k"
+    cp "$SEED_CARD" "$ROOTFS/ente/seed.card.json"
     echo "==> Semilla custom: $SEED_CARD"
 else
-    cp "$WORKSPACE/docs/seed-gnome-test.k" "$ROOTFS/ente/seed.card.k" 2>/dev/null \
-        || echo "WARN: docs/seed-gnome-test.k no existe; ente-zero sintetizará dev seed"
+    cp "$WORKSPACE/docs/seed-vps-min.json" "$ROOTFS/ente/seed.card.json" 2>/dev/null \
+        || echo "WARN: docs/seed-vps-min.json no existe; ente-zero sintetizará dev seed"
 fi
 
 # 6. Mascara servicios systemd que vamos a sustituir