# ============================================================================ # card.k — Genética del Ente. Esquema KCL para EntityCard. # # Esta es la gramática autoritativa: cualquier Card que se cargue al fractal # debe pasar la validación de este esquema. El boot de PID 1 acepta JSON que # cumple este shape (KCL exporta JSON tras validar `check:`). # # Para validar manualmente: # kcl run examples/my-card.k --schema schema/card.k # # Cada `check:` es invariante de fractal. Romperlo = Card inválida = no boot. # ============================================================================ # ---------- Identidad ---------- schema EntityCard: """Tarjeta de Identidad. Inmutable: cambios = nueva Card con nuevo id.""" schema_version: int = 1 id: str # Ulid (26 chars, Crockford base32) lineage?: str # parent Ulid; None = Ente raíz label: str # legible, no es identificador provides: [Capability] = [] # contrato hacia el grafo requires: [Capability] = [] # contrato del grafo hacia el Ente soma: SomaSpec # cuerpo: aislamiento + recursos payload: Payload # cómo encarnar (Wasm/Native/Virtual) supervision: Supervision # política tras muerte genesis?: [EntityCard] = [] # hijos a instanciar al encarnar check: schema_version == 1, "schema version no soportada" len(label) > 0, "label vacío" len(id) == 26, "id debe ser Ulid (26 caracteres)" # Auto-dependencia: una capacidad no puede estar en requires y provides all c in requires { c not in provides }, "self-dependency: ${c}" # ---------- Capacidades (typed enum) ---------- # KCL no tiene sum types nativos; usamos tagged union: `kind` + campos opcionales # que sólo aplican según el kind. Las invariantes en `check:` aseguran consistencia. schema Capability: """Capacidad tipada del fractal. NUNCA usar strings libres.""" kind: "FilesystemRoot" | "KernelNetlink" | "Endpoint" | "LegacyLogind" | "Device" | "Spawn" | "Journal" netlink_family?: "Uevent" | "Route" | "Generic" | "Audit" endpoint_interface?: str # 32-char hex (UUID 16 bytes) endpoint_version?: int device_class?: "Block" | "Tty" | "Input" | "Drm" | "Net" | "Hidraw" check: kind != "KernelNetlink" or netlink_family is not None, \ "KernelNetlink requiere netlink_family" kind != "Endpoint" or (endpoint_interface is not None and endpoint_version is not None), \ "Endpoint requiere interface + version" kind != "Endpoint" or len(endpoint_interface) == 32, \ "endpoint_interface debe ser hex de 32 chars" kind != "Device" or device_class is not None, \ "Device requiere device_class" # ---------- Soma: cuerpo + restricciones de recursos ---------- schema SomaSpec: """Aislamiento + recursos. Validados por KCL antes de tocar el kernel.""" namespaces: NamespaceSet = NamespaceSet {} rlimits: ResourceLimits = ResourceLimits {} cgroup: CgroupSpec = CgroupSpec {} cpu_affinity?: [int] # CPU pinning check: cpu_affinity is None or all c in cpu_affinity { c >= 0 and c < 1024 }, \ "cpu_affinity fuera de rango [0, 1024)" schema NamespaceSet: mount: bool = False pid: bool = False net: bool = False uts: bool = False ipc: bool = False user: bool = False cgroup: bool = False schema ResourceLimits: """Restricciones nativas validadas en KCL — el kernel sólo ve valores sanos.""" mem_bytes?: int # RLIMIT_AS nproc?: int # RLIMIT_NPROC nofile?: int # RLIMIT_NOFILE energy_budget_mw?: int # presupuesto energético (futuro) check: mem_bytes is None or mem_bytes > 0, "mem_bytes debe ser positivo" mem_bytes is None or mem_bytes <= 1099511627776, "mem_bytes > 1 TiB sospechoso" nproc is None or (nproc > 0 and nproc <= 65535), "nproc fuera de rango" nofile is None or (nofile > 0 and nofile <= 1048576), "nofile fuera de rango" energy_budget_mw is None or energy_budget_mw > 0, "energy_budget_mw debe ser positivo" schema CgroupSpec: """Cgroup v2: path + weights. cpu_weight 1..10000 según kernel docs.""" path: str = "" cpu_weight?: int io_weight?: int check: cpu_weight is None or (cpu_weight >= 1 and cpu_weight <= 10000), \ "cpu_weight 1..10000" io_weight is None or (io_weight >= 1 and io_weight <= 10000), \ "io_weight 1..10000" # ---------- Payload: tagged union de cómo encarnar ---------- schema Payload: """Una variante por Card. Set exactly one of: Wasm, Native, Virtual, Legacy.""" kind: "Wasm" | "Native" | "Virtual" | "Legacy" # Wasm module_sha256?: str # hex 64 chars entry?: str # Native / Legacy exec?: str argv?: [str] = [] envp?: [{str: str}] = [] # Legacy fakes?: ["SystemdLogind" | "SystemdHostnamed" | "SystemdNotify"] = [] check: kind != "Wasm" or (module_sha256 is not None and entry is not None), \ "Wasm requiere module_sha256 + entry" kind != "Wasm" or len(module_sha256) == 64, "module_sha256 debe ser hex de 64 chars" kind != "Native" or exec is not None, "Native requiere exec" kind != "Legacy" or exec is not None, "Legacy requiere exec" # ---------- Supervision ---------- schema Supervision: kind: "Restart" | "OneShot" | "Delegate" initial_ms?: int # ms — backoff inicial para Restart max_ms?: int # ms — backoff máximo check: kind != "Restart" or (initial_ms is not None and max_ms is not None), \ "Restart requiere initial_ms + max_ms" initial_ms is None or initial_ms >= 0, "initial_ms negativo" max_ms is None or max_ms >= initial_ms or max_ms is None, \ "max_ms < initial_ms es contradictorio" # ============================================================================ # Herencia: EnteWeb hereda de EnteBase con campos pre-rellenados. # ============================================================================ schema EnteBase(EntityCard): """Base para Entes managed: declara Spawn provider y Journal por defecto.""" schema_version = 1 supervision = Supervision {kind = "Restart", initial_ms = 100, max_ms = 30000} soma = SomaSpec { rlimits = ResourceLimits {nofile = 4096} cgroup = CgroupSpec {path = "ente.slice/managed", cpu_weight = 100} } schema EnteWeb(EnteBase): """Hereda EnteBase, declara endpoint + cap LegacyLogind como ejemplo.""" provides = [ Capability {kind = "Journal"} Capability { kind = "Endpoint" endpoint_interface = "deadbeefcafe1234deadbeefcafe1234" endpoint_version = 1 } ] soma = SomaSpec { namespaces = NamespaceSet {net = True, mount = True, pid = True} rlimits = ResourceLimits {nofile = 16384, mem_bytes = 536870912} # 512 MiB cgroup = CgroupSpec {path = "ente.slice/web", cpu_weight = 200, io_weight = 100} }