App GPUI con app_id carmen.greeter: formulario usuario+contraseña que autentica con brahman-auth en un hilo de fondo y, en éxito, emite un SessionTicket por stdout para que el compositor haga el traspaso a modo sesión. Backend mock (MIRADA_GREETER_MOCK) o PAM. Incluye brahman-auth::SessionTicket (contrato de tiquet greeter→compositor, serializado a una línea con prefijo versionado) y el modo enmascarado de nahual-widget-text-input (TextInput::with_mask para contraseñas). 18 tests nuevos; greeter verificado por compilación + clippy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
77 KiB
Changelog — protocol/
Contratos canónicos + routing entre módulos. Antes: core/brahman-* + shared/brahman-*.
feat(brahman-auth): SessionTicket — el tiquet greeter→compositor
brahman_auth::SessionTicket { user: UserInfo, session: String }: lo
que el greeter le entrega al compositor tras un login exitoso.
to_line/from_line lo serializan a una línea única (campos por
tabulador, prefijo versionado MIRADA-SESSION-TICKET-v1) — el greeter
la imprime a stdout y el compositor escanea sus líneas buscando el
prefijo. 5 tests de round-trip y rechazo de líneas malformadas.
feat(brahman-auth): autenticación del escritorio — contrato + PAM + mock
Crate nuevo crates/protocol/brahman-auth: la base del DM/greeter de
carmen (mirada). Contrato Authenticator agnóstico del backend:
authenticate(usuario, secreto) -> UserInfo, donde UserInfo lleva
uid/gid/home/shell — lo que el compositor necesita para arrancar la
sesión.
PamAuthenticator— verifica contra PAM (/etc/pam.d/<servicio>, por defectocarmen), el mismo subsistema delogin/sudo. Un handle PAM nuevo por intento;authenticate()cubre credenciales + estado de la cuenta.map_pam_errortraduce losPamReturnCodea la taxonomía gruesa deAuthError.MockAuthenticator— credenciales fijas en memoria, para tests y para iterar el greeter en cajas sin PAM.AuthErrordeliberadamente grueso:BadCredentials(reintentar) vsAccountUnavailable(cuenta vetada); usuario inexistente y contraseña errada dan el mismo error (no filtra existencia de cuentas).resolve_uservíagetpwnam(nix).UserInfo::syntheticpara dev.data/carmen— archivo de servicio PAM. Ejemploauth-probepara verificar PAM en una máquina real.- 11 tests; el camino PAM real se ejercita (falla limpio sin servicio).
feat(brahman-demo): bootstrap script reproducible — broker + producer + consumer + 4 explorers
Iter 22. Cierra el set de iteraciones de hoy: cualquier persona (o future-me retomando el repo) puede levantar el escenario completo con un comando.
Crate nuevo crates/apps/brahman-demo/ con 3 binarios:
brahman-demo-broker: standaloneServer::bindcon un Broker configurado, escucha en el socket default. Reemplaza aente-zeropara fines de demo (ente-zero pesa kernel surface + child subreaper + bus + brain + audit; el demo no lo necesita).brahman-demo-producer: registra una Card conflow.output[demo-stream:json]y queda pingueando.brahman-demo-consumer: registra una Card conflow.input[demo-feed:json](mismo type → matchea con el producer) y queda escuchandoMatchEvents.
Variables de entorno respetadas en los 3: BRAHMAN_INIT_SOCKET,
BRAHMAN_BROKER_CONTEXT (sólo broker), BRAHMAN_DEMO_LABEL/FLOW/TYPE,
RUST_LOG.
Script nuevo scripts/bootstrap-demo.sh:
- Modos:
all(default — broker + producer + consumer + 4 explorers),broker(sin GUIs, sólo backend),only(sólo broker, sin producer/consumer ni GUIs). - Cleanup-safe: trap
EXIT INT TERMmata todos los procesos spawneados (con SIGTERM grace + SIGKILL fallback) y borra el socket. - Espera activa hasta 5s a que el socket aparezca antes de spawnear los siguientes (evita ENOENT en el handshake).
- Logs separados por proceso bajo
$BRAHMAN_DEMO_LOG_DIR(default/tmp/brahman-demo). Re-invocaciones limpian los logs viejos. - Re-build automático opcional (comentado por default — asume
cargo build --workspaceya hecho).
Smoke verificado end-to-end (sin DISPLAY, sólo backend):
- Broker arranca, bind del socket OK.
- Consumer conecta, asigna session.
- Producer conecta, asigna session.
- Consumer recibe
MatchEvent { Available, demo-feed ← demo-stream, via: Exact, pinned: false }automáticamente — el broker computó el match y lo pusheó por el push channel.
Stack tests: brahman-demo (0 unit), workspace verde.
feat(brahman-handshake): ListMatches endpoint + timeline en broker-explorer
Iter 21. Cierra el loop de observabilidad iniciado en iter 20: ahora se ven no sólo las sesiones registradas sino también qué matches consumer↔producer está computando el broker en cada momento, y la historia de cómo cambian.
brahman-handshake/messages.rs:
Frame::ListMatches(ListMatches{session}): pedido (mismo patrón de validación session-id).Frame::MatchList(MatchList{matches: Vec<brahman_broker::Match>}): respuesta. CadaMatchya es serializable y llevaconsumer,consumer_label,producer,producer_label,ty,via,pinned.
brahman-handshake/server.rs:
run_post_handshakeahora pasa tambiénbroker_for_match: Option<&SharedBroker>alhandle_inbound_frame.- Si el server tiene broker configurado,
ListMatchesresponde conbroker.all_matches(). Si no (server sin broker), respondeMatchList { matches: vec![] }— refleja "no hay matching activo", no es un error.
brahman-handshake/client.rs: Client::list_matches() análogo a
list_sessions(), drena MatchEvents intermedios al buffer.
brahman-sidecar/discovery.rs: list_matches y list_matches_blocking
con la misma forma de Card observer minimalista.
brahman-broker-explorer:
- Cada poll-tick ahora pide TANTO
list_sessionsCOMOlist_matches. Explorer.last_match_keys: HashSet<MatchKey>mantiene el estado del último snapshot. La key es(consumer.session, consumer.flow, producer.session, producer.flow).Explorer.timeline: VecDeque<TimelineEntry>con capTIMELINE_CAP=50.- Función pura
diff_matches(last_keys, list) -> (entries, new_keys): emiteAvailablepara keys nuevas yLostpara keys desaparecidas. Primer tick (last_keys vacío) marca todo como Available — cubre el boot sin que la UI quede vacía. - Render:
stat_card"Timeline de matches" con count + 20 entries formateadas comoHH:MM:SS {+/-} consumer.flow ← producer.flow [via]. Más reciente arriba.
Tests broker-explorer: 5 totales.
diff_matches_first_snapshot_marks_everything_availablediff_matches_emits_lost_when_match_disappearsdiff_matches_no_change_emits_nothingpending_is_default_state_at_boot(existente)poll_and_probe_constants_are_sane(existente)
Decisión: timeline polled (cada POLL_INTERVAL=5s), no push.
Razón: los MatchEvent push del broker son consumer-céntricos
(cada session sólo ve sus propios matches). Para "system-wide
timeline" haría falta una API broker-level "subscribe a todos" —
mucho más scope. Polling cada 5s es suficiente para observabilidad.
feat(brahman-handshake): ListSessions endpoint + cliente + UI broker-explorer
Iter 20. Nuevo flujo end-to-end para observabilidad: cualquier módulo conectado puede preguntar al broker la lista de sesiones activas y mostrar labels + flows in/out por cada una.
brahman-handshake/messages.rs:
Frame::ListSessions(ListSessions { session }): request del cliente (server valida quesessioncoincida con la sesión vigente, mismo patrón que Ping/Farewell).Frame::SessionList(SessionList { entries }): respuesta. CadaSessionEntrylleva:session,label,schema_version,outputs(nombres de flow.output),inputs(nombres de flow.input),conscious(truesi la Card vino con WIT).
brahman-handshake/server.rs:
run_post_handshakeahora pasaSessionRegistryahandle_inbound_frame(necesario para consultar el snapshot de sesiones en respuesta aListSessions).- Helper
build_session_list(sessions)que toma el snapshot bajo el lock, lo proyecta aSessionList, y suelta el lock antes de escribir el frame al wire. - Validación
session_idmismatched →HandshakeError::Unauthorized.
brahman-handshake/client.rs:
Client::list_sessions()async: envía el request, drenaMatchEvents intermedios alpending_eventsbuffer (mismo patrón queping), retorna elSessionList.
brahman-sidecar/discovery.rs:
pub async fn list_sessions(observer_label)ypub fn list_sessions_blocking(observer_label): arman una Card observer mínima (sin flow.input/output), conectan, piden la lista, Farewell. Para CLIs y módulos std-thread.
brahman-broker-explorer:
- Cada poll-tick (cuando el broker está UP*) ahora también pide
list_sessions_blockingy guarda el snapshot enExplorer.sessions. - Render extiende el body con un
stat_card"Sesiones activas" que muestra el count + lista ordenada porsession(Ulid temporal), cada item:label · in:[flows] out:[flows] (wit?).
Tests:
list_sessions_returns_currently_registered: levanta server con broker, conecta 3 clientes (alpha, beta, observer), observer pidelist_sessions, verifica los 3 labels presentes y que la entry del observer reporteconscious=falsey elschema_versionesperado.- Stack: handshake suite (24 tests), sidecar (3+8 unit + integ), broker-explorer (4 tests). Todo verde.
feat(brahman-broker-explorer): nueva app probe del broker brahman
Iter 14. Cierra otro frente: visibilidad del broker brahman (el broker handshake que matchea Cards consumer/producer). Hasta ahora no había forma de "ver" si el broker estaba up sin invocar otro binario CLI. Ahora hay una app GUI que probe cada 5s y reporta 3 estados claros.
Crate nuevo crates/apps/brahman-broker-explorer/:
- Deps:
brahman-handshake,brahman-sidecar+ el stack yahweh themed (theme + 3 widgets). Consume el mismoawait_provider_blockingque usanouser-explorer. ProbeStateenum con 4 variants:Pending(estado inicial al boot, antes del primer probe).Down { reason }— connect failed, broker no escucha.UpNoProvider { flow }— broker reachable + sin productor para el flow probado dentro del timeout.UpWithProvider { flow, producer_socket }— broker reachable- matcheó algo, devuelve el socket del provider.
- Polling loop en
cx.spawncada 5s; el probe (que es bloqueante porque internamente usa tokio runtime) se ejecuta encx.background_executor().spawn(...)para no congelar el main thread del UI. - Configuración via env:
BRAHMAN_INIT_SOCKET— path del broker (default resuelto porbrahman_handshake::transport).BRAHMAN_BROKER_PROBE_FLOW— flow del Card observer (defaultbroker-health).BRAHMAN_BROKER_PROBE_TYPE— type name (defaultping).
- UI: header con probe info + theme switcher; banner permanente (Error/Warning/Success/none según estado) debajo del header; stat card con accent color por estado y descripción.
- 2 tests sanity (default state es Pending; constants coherentes: PROBE_TIMEOUT < POLL_INTERVAL >= 2s).
Smoke run del binario verificado: bootstrap completo OK, panic esperado en open_window por falta de display.
Beneficio operativo:
- Si tenés un broker corriendo en
~/.local/share/brahman/init.sock, el explorer lo detecta + reporta estado verde con su socket. - Si no hay broker, banner rojo + msg claro indicando el path probado.
- Si hay broker pero ningún Card produce el flow probado, banner amber — útil para distinguir "broker down" de "broker up, no productor del tipo X".
- Apuntando el flow/type via env, podés monitor productores
específicos: ej.
BRAHMAN_BROKER_PROBE_FLOW=monad-list BRAHMAN_BROKER_PROBE_TYPE=jsonpara ver si nouser está sirviendo.
Apps GUI integradas al stack themed: 5 (nakui-ui, nakui-explorer, nouser-explorer, minga-explorer, brahman-broker-explorer).
Limitaciones documentadas:
- El observer registra una Card temporal en cada probe (cada 5s). Eso ensucia un poco las estadísticas del broker (Cards registradas/desregistradas). No impacta funcionalidad pero inflama el log si el broker tiene observability habilitada.
- No muestra la lista global de Cards registradas en el broker
— el protocolo handshake actual no expone esa API. Para eso
habría que agregar un endpoint
ListSessionsal broker server. - No mantiene un buffer de MatchEvents. Cada probe es independiente. Para timeline de matches, hace falta mantener el Client vivo entre probes — scope futuro.
feat(brahman-cards): templates Nickel canónicos para cada body kind
Materializa el patrón "import + override" del brazo: hasta ahora
BRAHMAN_CARDS_TEMPLATES_DIR existía como mecanismo pero el repo
no shippeaba ningún template. Ahora hay 3 templates basic (uno
por body kind del CardBody) bajo
crates/core/brahman-cards/templates/:
ente_basic.ncl— Card runtime mínima:payload="Virtual",supervision="OneShot",schema_version=1. Override típico:id+label.monad_basic.ncl— agrupación semántica de archivos (Mónada Nouser): metadata vacía,dominant_lens="grid"(lowercase por convención serde rename_all). Override típico:id,label,members,cardinality.ui_module_basic.ncl— descriptor UI conentities=[],menu=[],views={}. Override típico:id,labely los 3 payloads.
Cada field override-able marcada | default (sin eso Nickel
rebota merge de strings/numbers no-iguales).
API nueva en lib.rs:
pub fn canonical_templates_dir() -> PathBuf: devuelve el directorio de templates del crate (resuelto viaCARGO_MANIFEST_DIR). Útil para apuntar el envBRAHMAN_CARDS_TEMPLATES_DIRen runtime/tests sin hardcoding del path.- Doc explica que para distribución del binary standalone (cuando
emerja), incluir templates como recursos via
include_dir!o instalar el directorio junto al ejecutable.
5 tests E2E (tests/templates.rs) que cubren:
ente_basicimport + overrideid+label→ Card body Ente conpayload=Virtual(default preserved).monad_basicimport + overrideid+label+cardinality→ Card body Monad con members=[] y summary="" (defaults).ui_module_basicimport + override deid+label+menu+views → Card body UiModule con entities=[] (default).- Sanity: import sin override → defaults
"TEMPLATE_ID"/"TEMPLATE_LABEL"pasan al wrapper sin error. - Sanity: el path de
canonical_templates_dir()apunta a un directorio existente con los 3 archivos esperados.
Helper de test with_canonical_templates(F) setea/restaura el
env localmente; cada test single-thread-safe.
Tests suite brahman-cards: 26 → 31 verdes (+5). El resto del workspace intacto.
Beneficio operativo:
- Un usuario que quiera declarar un Card nuevo puede empezar con
un override de 2 líneas (
id+label) en lugar de copiar el shape full desde cero. - Templates auto-documentan la convención
| defaultpara que copiar uno y agregar fields propios "just works" en merge. - El brazo sigue siendo agnostic — los templates son sólo
archivos
.nclresueltos via el import resolver Nickel; nada hardcoded en código Rust.
Limitaciones:
- No hay templates "ricos" tipo
crud_basic.nclque parametricen por entity name. Nickel no expone funciones-templates de la forma típica de templating engines; lo más cercano sería un template con un fieldentity_name | Stringy references internas viame.entity_name. Cuando aparezca el caso de uso real (e.g., un módulo donde el patrón list+form es repetitivo), se diseña el template paramétrico. canonical_templates_dir()resuelve viaCARGO_MANIFEST_DIR— funciona encargo(test/run/build) pero no para un binary instalado fuera del workspace. Para release distribution la API necesitará un fallback (resources embedded o convención de install path).
feat(brahman-cards): Nickel reader + templates con merge nativo (V2)
Sigue al V1 (readers JSON). Ahora el brazo acepta inputs .ncl:
los evalúa via nickel-lang 2.0, exporta a JSON, y dispatcha por
los mismos readers JSON estándar. Un .ncl puede producir
cualquier CardBody siempre que su shape sea reconocida. Los
templates funcionan con los import + & merge nativos de
Nickel — el brazo no inventa una mecánica paralela.
Cambios:
- Dep
nickel-lang = "2.0.0"(interfaz estable, nonickel-lang-coreque es internal/inestable). Compila clean pero suma ~1 min al build cold del crate. - Nuevo módulo
nickel_eval.rsconeval_nickel_file(path) -> Result<Value, NickelEvalError>. Errores tipados:Io,Eval,Export,JsonReparse— el mensaje de Nickel se formatea como texto plano (sin ANSI) para que sea legible en logs y toasts. load_card_withañade"ncl": lee archivo → eval Nickel → exporta a JSON → parsea de vuelta a Value → dispatch a los readers JSON. Pipeline simétrico a"json".CardLoadError::Nickel(NickelEvalError): el error de Nickel se propaga limpio al error público del brazo.- Resolución de imports:
- El parent dir del input se agrega como import path →
import "./template.ncl"resuelve sin config. - El env
BRAHMAN_CARDS_TEMPLATES_DIR(constante exportadaBRAHMAN_CARDS_TEMPLATES_ENV) agrega un registry global →import "ui_module_minimal.ncl"desde cualquier ubicación. - No hay magic resolución por kind. El autor del Card decide qué template importa.
- El parent dir del input se agrega como import path →
Convención obligatoria de templates (documentada en
nickel_eval.rs): las fields que el usuario va a sobrescribir
deben marcarse | default (o | optional). Sin ese marker
Nickel rechaza el merge de strings/numbers no-iguales con la
misma prioridad. Patrón canónico:
# template ui_module_basic.ncl
{
id | String | default = "TEMPLATE_ID",
label | String | default = "TEMPLATE_LABEL",
...
}
# uso concreto
let base = import "ui_module_basic.ncl" in
base & { id = "my_id", label = "Mi Label" }
9 tests nuevos en tests/nickel.rs:
eval_nickel_file_returns_value_for_valid_input— happy path.eval_nickel_file_surfaces_evaluation_error— variantEvalcon path + message.load_card_dispatches_ncl_to_ui_module_variant— pipeline e2e a UiModule.load_card_dispatches_ncl_to_ente_variant— pipeline e2e a Ente.template_merge_overrides_id_and_label_only— el caso del user: template + override de id+label, resto del template intacto.template_resolves_via_env_registry— uso del env como registry global.load_card_wraps_nickel_error_in_card_load_error— wrap limpio del error.nickel_contract_violation_caught_at_eval_time— value-add concreto:id | String = 42falla en eval, no en deserialize ni aguas abajo.ncl_evaluating_to_unknown_shape_returns_no_matching_reader— sanity de coherencia con dispatcher JSON.
22 tests en total en brahman-cards (13 JSON V1 + 9 Nickel V2).
Workspace build verde tras la dep nueva.
Lo que NO hace V2 (sigue pendiente):
- No migra consumers —
nakui-uisigue cargando connakui_ui_schema::load_modules_from_dir. La migración abrahman_cards::load_cardqueda para después. - No define un set canonical de templates en el repo (algo
como
templates/ente_basic.ncl,templates/ui_module_minimal.ncl). Eso emerge cuando aparezcan los primeros casos de uso reales donde dos cards comparten estructura. - No hace cross-validation entre template + override (ej: detectar que un override saca un campo required del template). Nickel ya lo hace via contracts si el template tiene un schema.
- No expone una API streaming (load N cards en paralelo). El use case actual es one-shot al boot.
Pendientes para próximos commits (orden):
- Migrar consumers (
nakui-uiconsumebrahman_cards::load_card). - Yahweh refactor: lift del MetaUi runtime a
crates/modules/ui_engine/. - KCL → Nickel: kcl_wrapper reemplazado por evaluación de Nickel contracts; los 3 schemas .k de nakui modules pasan a .ncl.
- card.k eliminado (es REFERENCE ONLY documentado).
feat(brahman-cards): brazo unificado V1 — readers JSON + estructura canónica
Pivote arquitectónico decidido en charla: Brahman maneja varios formatos legítimos de "Card" (cada formato vive en su crate origen y conserva su shape público), y un único brazo los lee, completa desde templates si vienen simplificados, y los proyecta a UNA sola estructura interna canónica que consumen UI runtime / storage / DHT / wire. Agregar un formato nuevo = agregar un reader, sin tocar consumers.
V1 en este commit: estructura canónica + readers para los 3 formatos JSON existentes en el monorepo. Sin Nickel todavía (aislado para próximo commit).
Crate nuevo crates/core/brahman-cards/:
Card { id, schema_version, lineage, label, extensions, body }: wrapper común con identidad legible + extensiones forward-compat.idcomo String (noUlid) porque cada body variant usa un tipo de id distinto (Ulid para Ente/Monad, slug human-friendly para UiModule). PartialEq omitido del derive porqueMonadManifestynakui_ui_schema::Moduleno lo implementan en sus crates origen.CardBodyenum etiquetadokind:Ente(brahman_card::Card)— entidad runtime con payload/soma/supervision.Monad(nouser_card::MonadManifest)— agrupación semántica de archivos.UiModule(nakui_ui_schema::Module)— descriptor de UI con entities/views/menu.- Convención: agregar variant nuevo + reader; los consumers que
sólo manejen algunos hacen
match { Ente(..) => ..., _ => skip }.
trait CardReader:name()+can_read(&Value) -> bool+read(Value) -> Result<Card>. El dispatcher prueba en orden y delega al primero que matchee.- 3 readers concretos (en
readers.rs):EnteJsonReader— heurística:payloadYsupervisionpresentes simultáneamente.MonadJsonReader— heurística:membersYcardinality.UiModuleJsonReader— heurística:entitiesYviewsYmenu. El más específico, va primero endefault_readers().
- Entry points:
load_card(path)— abre archivo, dispatcha por extensión, dentro de JSON prueba los readers default.load_card_with(path, readers)— variante con set custom para apps que quieren restringir formatos.
- Errores tipados vía
CardLoadError:Io,JsonParse,NoMatchingReader,ReaderFailed { reader, message },UnsupportedExtension { ext, supported }.
13 tests integration:
- 3 detection tests (cada reader matchea sólo su shape, rechaza los otros 2 + non-object).
- 3 dispatch+projection tests (cada formato JSON cargado produce el variant esperado con campos del wrapper bien derivados).
- 2 negative cases (NoMatchingReader, non-object input).
- 1 sanity de orden (UiModule gana cuando el shape acepta múltiples readers — defiende el contrato de orden documentado).
- 1 e2e desde disco con
load_card_with. - 1 unsupported extension.
- 1 custom reader set (restringir a sólo Ente).
- 1 documented invariant (extensions vacío en V1; si cambia, este test se rompe como signal).
13/13 verdes. Workspace build verde tras agregar el crate al
members[] del workspace Cargo.toml.
Lo que NO hace V1 (explícito):
- No carga Nickel — próximo commit. La dep
nickel-lang-corequeda aislada para no inflar este commit. - No define templates — los templates Nickel se diseñan junto al
reader Nickel (necesitan
mergenativo de Nickel para fusionar override + base). - No migra consumers.
nakui-uisigue cargandomodule.jsonconnakui_ui_schema::load_modules_from_dirdirecto. La migración abrahman_cards::load_cardviene cuando V1 + Nickel + templates estén estables. - No mueve los
extensionsdel input aCard.extensions— los crates origen ya tienen sus propiosextensionsinternos (#[serde(flatten)]). Documentado como decisión consciente.
Pendientes para próximos commits (orden):
- Reader Nickel + template merge.
- Migrar consumers (
nakui-uiconsumebrahman_cards::load_card). - Yahweh refactor: lift del MetaUi runtime a
crates/modules/ui_engine/(esperando hasta que el brazo + canónico estén estables). - KCL → Nickel: kcl_wrapper reemplazado por evaluación de Nickel contracts; los 3 schemas .k de nakui modules pasan a .ncl.
- card.k eliminado (es REFERENCE ONLY documentado).
feat(brahman-handshake): multi-key identity — rotación de session sin perder peer_id lógico
Cierra el último pendiente del plan de red P2P. Hasta ahora, rotar
la keypair libp2p de un nodo cambiaba su peer_id, lo que
invalidaba todas las allowlists/denylists remotas que lo
referenciaban. Imposible rotar sin coordinar con todos los pares.
Solución: separar identity master (Ed25519 persistente forever,
identifica al nodo como entidad lógica) de session libp2p
(Ed25519 efímera, rotable). El master firma certs de session con
expiración. La política de admisión se evalúa contra el
master_peer_id del cert — el session peer_id puede cambiar
libremente sin tocar las allowlists.
API nueva en brahman_handshake::identity:
Identity::from_keypair(master)— wrapper sobre la master kp.Identity::master_peer_id()— el peer_id estable del nodo.Identity::issue_session_cert(session_kp, ttl) -> SessionCert— firma un cert que vincula session_pubkey + expires_at_ms.SessionCert::verify()— chequea versión, firma criptográfica, no expiración. Devuelve(master_peer_id, session_peer_id).SessionCert::verify_against_session(expected_session_pk)— verify- exige que el cert vincule esa session pubkey (previene reuso de certs ajenos con keypairs distintas).
CertErrortipado:UnknownVersion,DecodeMaster,DecodeSession,InvalidSignature,Expired,SessionMismatch,Sign.DEFAULT_SESSION_TTL = 24h.
Wire:
Hello.identity_cert: Option<SessionCert>agregado (default None, back-compat).Client::connect_with_stream_signed_with_cert(stream, card, wit, session_kp, cert)— variante que adjunta el cert.network::connect_libp2p_with_cert(net, peer, card, wit, session_kp, cert)— paralelo aconnect_libp2p.
Server (do_handshake):
- Nuevo paso ANTES del policy gate: si
Hello.identity_cert.is_some(), se verifica converify_against_session(&hello.signature.public_key). Ellogical_peerque se evalúa contra la policy es elmaster_peer_idderivado, NO el session peer_id. - Sin cert (path Fase 3):
logical_peer = expected_peer(compat). - Si el cert es inválido (firma rota, expirado, session mismatch),
rechazo con
Unauthorizedantes de evaluar policy. - Migración gradual: clientes sin cert siguen funcionando contra servers con policy basada en session peer_ids.
Canonicalización del payload firmado:
[u8 version][b"sess"][u32 LE session_pubkey_len][session_pubkey][u64 LE expires_at_ms]
SESSION_CERT_VERSION = 1 documenta el esquema; cualquier cambio
fuerza bump (clientes viejos no validan certs nuevos).
Sobre el swarm-level deny:
- El
block_listdel swarm sigue operando con session peer_ids (Noise sólo conoce eso). Si la operatoria lista master_peer_ids en deny, el handshake-level gate los para; el swarm-level no. El operador elige granularity: listar masters = robust a rotaciones; listar sessions = rechazo más temprano.
Tests: 8 unit en identity::tests:
issue_and_verify_cert— roundtrip básico, peer_ids derivados.verify_against_session_admits_matchingy_rejects_mismatch— el cert vincula 1 sola session pubkey.cert_with_zero_ttl_is_expired— expiración chequeada con tiempo real.tampered_signature_rejectedytampered_expires_at_rejected— cualquier mutación del cert post-firma falla.unknown_version_rejected— schema versionado defensivamente.rotated_session_with_same_master_yields_same_master_peer_id— la propiedad fundamental: rotar session NO cambia master_peer_id.
Plus 1 E2E definitivo en network_libp2p.rs:
identity_cert_allows_session_rotation_without_policy_change.
- A configura
policy = allowlist[B.master_peer_id](master, no session). - B se conecta con session1 + cert(master, session1) → admitido. Sesión registrada, farewell limpio.
- B "rota": genera session2 ≠ session1, mismo master, emite cert2.
- B se conecta con session2 + cert2 → admitido también, sin que A toque su allowlist.
- Sanity: una session sin cert (cuyo session_peer NO está en allow) es rechazada.
40 tests verdes en brahman-handshake + brahman-net (24 unit incluyendo identity + 7 handshake + 3 discovery + 6 libp2p incluyendo rotation E2E). Ningún regreso.
Wire en Arje queda como follow-up: ente-zero hoy es server-only y
no necesita identity (su keypair libp2p ya es estable vía
keypair_store). Cuando algún módulo de Arje haga conexiones
salientes con cert, se cargará la identity master separada de la
session vía nueva env BRAHMAN_IDENTITY_PATH. La API ya está
lista.
feat(brahman-net+handshake): swarm-level deny — la denylist se proyecta al block_list de libp2p
Optimización de seguridad: la denylist ya no espera al handshake
brahman para rechazar — ahora se proyecta al block_list behaviour
del swarm libp2p. Conexiones desde peers baneados son rechazadas
antes del Noise handshake, ahorrando el round-trip TCP+Noise
por cada intento denegado.
Wire de bajo nivel (brahman-net):
- Nuevo behaviour
block_list: allow_block_list::Behaviour<BlockedPeers>añadido alBrahmanBehaviourderivado. Vive junto astream,kad,identify. Default vacío al construir. - Nuevos comandos
BlockPeer(PeerId)yUnblockPeer(PeerId)en el enum interno + handlers que llamanswarm.behaviour_mut().block_list.{block_peer,unblock_peer}. - API pública:
BrahmanNet::block_peer(peer)yBrahmanNet::unblock_peer(peer). Idempotentes. - Dep nueva:
libp2p-allow-block-list = "0.6"(sub-crate, no es feature delibp2pen 0.56).
Wire en la política (brahman_handshake::peer_policy):
PeerPolicygana campo opcionalnet: Arc<RwLock<Option<Arc<BrahmanNet>>>>. DefaultNonepara preservar callers existentes.- Nuevo método
attach_to_net(net: Arc<BrahmanNet>):- Sincronización inicial: itera la deny actual y llama
net.block_peer(p)por cada uno. - Guarda el net para diff-sync en cada
reload.
- Sincronización inicial: itera la deny actual y llama
reload()extendido: snapshot deprev_denyANTES de mutar el inner. Tras la mutación, llamasync_deny_to_swarm(prev, new)que aplicablock_peerpor cada added yunblock_peerpor cada removed.- Atomicidad preservada: si un archivo falla al parsear, el sync no ocurre y la versión anterior persiste tanto en la policy como en el block_list del swarm.
Wire en Arje (ente-zero):
- Tras setup_brahman_net + setup_brahman_policy, si AMBOS están
presentes se llama
policy.attach_to_net(net.clone())con un log informativo. Sin policy o sin net, no hay attach (modo abierto o solo gate-level deny).
Tests: 1 nuevo E2E en network_libp2p.rs:
swarm_level_deny_blocks_before_noise. A configura policy con
deny de un peer + attach_to_net. Cliente baneado intenta
connect_libp2p; en lugar del HandshakeError::Unauthorized que
recibíamos antes (que requería completar Noise primero), ahora
falla con error de transporte/stream (o timeout, según timing) —
el dial nunca completa porque el swarm rechaza la conexión.
5 tests verdes en network_libp2p.rs (roundtrip, mismatched signing,
allowlist, denylist handshake-level, denylist swarm-level). 31 tests
totales en brahman-handshake + brahman-net. Sin regresión en
ente-zero.
Trade-offs:
- Más eficiente contra DoS: un atacante que prueba miles de peer_ids no consume CPU del Noise handshake.
- Misma fuente de verdad: la denylist sigue viviendo en
PeerPolicy(un solo archivo, hot-reloadable). El swarm es un cache derivado que se actualiza vía diff. No hay drift posible — cada reload re-sincroniza atómicamente. - El handshake-level gate sigue activo como segunda línea: si
por alguna razón un peer baneado pasa el block_list (race entre
reload y nueva conexión, o bug del crate), el handshake brahman
igual lo rechaza con
Unauthorized. Defensa en profundidad.
Pendientes futuros del changelog:
- Rotación de keypair sin perder peer_id (multi-key identity).
feat(brahman-handshake+ente-zero): denylist + hot reload de la política de peers
Consolida PeerAllowlist + nueva PeerDenylist en un único
PeerPolicy con allow + deny + hot reload vía notify. Cubre los
dos pendientes documentados en el commit anterior y simplifica la
API hacia un sólo punto de entrada.
API consolidada en brahman_handshake::peer_policy:
PeerPolicy::open()— todo permitido (default).PeerPolicy::from_sets(allow: Option<BTreeSet<PeerId>>, deny: BTreeSet<PeerId>)— política inline para tests.PeerPolicy::from_files(allow_path?, deny_path?)— carga ambos archivos opcionales.PeerPolicy::evaluate(peer) -> Decision—Admit | DeniedByDenylist | NotInAllowlist. Decision lleva sureason()para logging consistente.PeerPolicy::reload()— recarga atómica desde los paths asociados. Si un archivo falla, conserva la versión anterior (un typo no debe tirar al Init en modo inseguro).PeerPolicy::spawn_watcher() -> JoinHandle— vigila los archivos víanotify, debounce 250ms (coalesce de los varios eventos típicos de un save), recarga atómica al detectar cambio.
Orden de evaluación (deny-first):
- Si
peer ∈ denylist→DeniedByDenylist. - Si hay allowlist y
peer ∉ allowlist→NotInAllowlist. - Resto →
Admit.
Esto significa que deny gana sobre allow: un peer en ambas listas es rechazado. Diseño explícito para que la denylist sea la primitiva de "kill switch" — agregar un peer al deny lo banea inmediatamente sin importar dónde más esté listado.
Watcher: vigila el directorio padre del archivo, no el archivo mismo. Razón: editores típicos hacen rename-and-replace (escriben a tmp y rename al destino), lo que rompe el watch del archivo pero no el del dir. Filtra eventos por path al procesar.
Wire en server:
ServerConfig.allowlist→ServerConfig.policy: Option<PeerPolicy>(breaking rename, scope local al monorepo). Gate endo_handshakellamapolicy.evaluate(&peer)y usadecision.reason()para el mensaje de error tipado.
Wire en Arje (ente-zero):
- Nueva env
BRAHMAN_PEER_DENYLISTcomplementaBRAHMAN_PEER_ALLOWLIST. Cualquiera (o ambas) activa la política. setup_brahman_policy()carga + arranca watcher. Devuelve(policy, JoinHandle); el handle se guarda en main para que el thread no se aborte.- Failure modes degradan a "modo abierto" (sin política) con log, preservando la doctrina PID 1.
Activación end-to-end con todas las capas activas:
BRAHMAN_LISTEN_MULTIADDR=/ip4/0.0.0.0/tcp/4101 \
BRAHMAN_PEER_ALLOWLIST=/etc/brahman/allow.txt \
BRAHMAN_PEER_DENYLIST=/etc/brahman/deny.txt \
ente-zero
# El operador puede editar deny.txt en caliente y la nueva regla
# entra en efecto en ~250ms sin restart del Init.
Tests: 10 unit en peer_policy::tests:
open_admits_anyone,allow_only_admits_listed,deny_overrides_open,deny_overrides_allow(deny-first semantics).from_files_with_both_lists,from_files_only_deny,invalid_file_rejected_at_load.reload_picks_up_changes— manualmente recarga y verifica.reload_failure_preserves_previous_state— invariante de seguridad: archivo roto NO baja la política activa.watcher_reloads_on_file_change— E2E del watcher con notify real: muta archivo, espera < debounce + margen, verifica que la política refleja el cambio sin haber llamado reload manualmente.
Plus 1 E2E nuevo en network_libp2p.rs:
libp2p_handshake_denylist_blocks_listed_peer — A configura
policy = PeerPolicy::from_sets(None, [banned_peer]). Cliente
con keypair baneada es rechazado; cliente con keypair distinta
pasa el handshake.
30 tests verdes en brahman-handshake (16 unit + 7 handshake + 3 discovery + 4 libp2p incluyendo allowlist + denylist E2E). Sin regresión en ente-zero.
Lo que cierra esta entrega:
- Política completa de admisión: open / allow-only / deny-only / allow+deny.
- Hot reload sin restart del Init — el operador puede banear/admitir peers en caliente editando archivos.
- Atomicidad: la recarga es del paquete
(allow, deny)completo, no de cada lista por separado. No hay momento donde una lista esté vieja y la otra nueva. - Resiliencia: errores de parseo NO bajan la política activa.
Pendientes futuros del changelog:
- Aplicar la política a nivel de swarm vía
libp2p_allow_block_list:: Behaviour(rechazar ANTES del Noise handshake, ahorrar el round-trip TCP+Noise por intento denegado). - Rotación de keypair sin perder peer_id (multi-key identity).
feat(brahman-handshake+ente-zero): allowlist explícita de peers libp2p
Capa de política sobre el trust criptográfico de Fase 3. Hasta ahora cualquier peer con keypair Ed25519 válida pasaba el handshake remoto; con allowlist activa, sólo los peers explícitamente listados. Aplica únicamente al path libp2p — el path Unix sigue usando SO_PEERCRED del kernel, que es autenticación de proceso local, no de red.
API nueva en brahman_handshake::peer_allowlist:
PeerAllowlist::from_iter([peer_id, ...])para tests/inline.PeerAllowlist::from_file(path)parsea texto plano: un PeerId base58 por línea,#para comentarios (línea entera o inline), líneas vacías ignoradas. Errores de parseo incluyen número de línea para debug rápido.is_allowed(peer),len(),is_empty(),iter().AllowlistError { Io, InvalidPeerId }.
Wire en el server:
ServerConfig.allowlist: Option<PeerAllowlist>.None= modo abierto (compat con todo lo anterior).Some= sólo los listados.- Gate en
do_handshakeANTES de la verificación de firma — la comparación O(log n) en BTreeSet es más barata que crypto, así que rechazamos peers inválidos antes de gastar ciclos. Se devuelveHandshakeError::Unauthorized("peer X no está en la allowlist").
Wire en Arje (ente-zero):
- Nueva env var
BRAHMAN_PEER_ALLOWLISTapuntando a un archivo. setup_brahman_allowlist()carga al startup; degrada aNone(modo abierto) si el archivo falla, consistente con la doctrina PID 1 de no romper por subsistemas opcionales.
Ejemplo de archivo de allowlist:
# Peers permitidos en la malla brahman de prod-eu-1
# Generados con: ente-zero (peer_id loggeado al arrancar)
12D3KooWFooBarBazFooBarBazFooBarBazFooBarBazFooBarBaz
12D3KooWQuxQuxQuxQuxQuxQuxQuxQuxQuxQuxQuxQuxQuxQuxQux # operador 2
Activación end-to-end:
BRAHMAN_LISTEN_MULTIADDR=/ip4/0.0.0.0/tcp/4101 \\
BRAHMAN_PEER_ALLOWLIST=/etc/brahman/allowlist.txt \\
ente-zero
Tests:
- 6 unit en
peer_allowlist::tests: from_iter, parse limpio, parse con comentarios inline, parse rechaza PeerId inválido (y reporta número de línea), I/O error en archivo faltante, empty list rechaza todo. - 1 E2E en
network_libp2p.rs:libp2p_handshake_allowlist_admits_listed_rejects_others. A configuraallowlist = [allowed_peer]. Cliente con keypair permitida pasa el handshake (sesión registrada, farewell limpio). Segundo cliente con keypair distinta es rechazado con error ANTES de que se le verifique la firma. Sanidad: el conteo de sesiones del server queda en 0 tras el rechazo.
25 tests verdes en brahman-handshake (12 unit + 7 handshake legacy
- 3 discovery + 3 libp2p). Ningún regreso en ente-zero (4/4 keypair_store).
Pendiente futuro:
- Denylist explícita (negada — banear peers específicos sin tener que listar a todos los demás).
- Hot reload de la allowlist sin restart del Init (signal SIGHUP o watch del archivo).
- Aplicar la política a nivel de swarm vía
libp2p_allow_block_list::Behaviourpara rechazar conexiones ANTES del Noise handshake (hoy se rechaza después, gastando un round-trip TCP+Noise por cada intento denegado).
feat(brahman-net+handshake): stop_providing automático en cleanup de sesión
Cierra el pendiente conocido del DHT: hasta ahora cuando una sesión con outputs cerraba (Farewell, EOF, error), el record que la anunciaba en el DHT seguía vivo hasta su TTL natural (~24h en kad default). Consumers remotos podían descubrir un peer "vivo" que ya no servía nada.
Cambios:
BrahmanNet::stop_providing(key)(nuevo): contraparte simétrica destart_providing. MandaCommand::StopProvidingal swarm que llamakad.stop_providing(&key). Borra el record del provider store local al instante; replicas en peers remotos siguen expirando por TTL (kad no expone retracción cross-peer, simétrico al hecho de questart_providingtambién propaga eventualmente).brahman_handshake::network::withdraw_outputs(net, card)(nuevo): contraparte deannounce_outputs. Iteracard.flow.outputy llamanet.stop_providing(flow_dht_key(...))por cada uno.server::cleanup: extrae laResolvedCardremovida del registro de sesiones (en lugar de descartarla conremove) y, siconfig.netestá set, llamawithdraw_outputs(net, &card)antes debroadcast_match_diffs.
Tests: nuevo E2E dht_discovery_withdraws_on_session_cleanup:
- A registra Card con
flow.output = monad-list:json. - B descubre a A vía
find_remote_providers— confirmabefore.contains(&a_peer). - Cliente local de A hace
farewell→ cleanup → withdraw_outputs. - Espera a que la sesión salga del registro (señal de cleanup completado) + 100ms para que el swarm procese el Command.
- Nueva query desde B:
afterNO debe contenera_peer.
3 tests verdes en network_discovery.rs (positivo, negativo,
withdraw). 18 tests totales en handshake + net.
Pendiente futuro: retracción cross-peer en kad (requeriría extensión del protocolo libp2p, no soportada hoy). Aceptable: simétrico al modelo de propagación eventual del DHT.
feat(brahman-handshake): Fase 3 — trust remoto vía firma Ed25519 anclada al peer libp2p
Cuarto y último paso del plan "el encuentro entre Entes no se
restringe a local". Cierra la falla de seguridad que dejaba la red
P2P abierta: hasta ahora, un atacante que pudiera dial-ar al
multiaddr del Init podía registrar cualquier Card con cualquier
label/flow. Fase 3 cierra esto exigiendo que el Hello vía libp2p
venga firmado con la misma keypair Ed25519 que produce el
peer_id autenticado por Noise.
Modelo:
- Local Unix: SO_PEERCRED del kernel autentica al cliente. Firma opcional. Si está presente, igual se verifica (defensa en profundidad).
- Remoto libp2p: firma obligatoria. La public key del Hello debe
derivar al
peer_idque Noise ya autenticó. Si falta o no coincide →HandshakeError::Unauthorized.
Wire (brahman_handshake::messages):
Hello.signature: Option<HelloSignature>(nuevo, default None).HelloSignature { public_key: Vec<u8>, signature: Vec<u8> }— public_key en formato canónico libp2p (encode_protobuf), firma Ed25519 sobre(SIGNATURE_VERSION, WireCard, Option<WitInterface>)serializado postcard.SIGNATURE_VERSION = 1documenta el esquema del payload firmado; bump al cambiar.
Nuevo módulo brahman_handshake::signature:
sign_hello(keypair, card, wit) -> HelloSignature.verify_hello(sig, card, wit, expected_peer) -> Result<(), SignatureError>.SignatureErrortipado (DecodeKey,EncodePayload,Invalid,PeerMismatch,Missing,Unexpected).
Server:
Session<S>ganaexpected_peer: Option<PeerId>.Server::session_from_libp2p_stream(stream, peer)(nuevo) construye Session conexpected_peer = Some(peer).session_from_stream(Unix/in-memory) sigue conNone.do_handshakeexige firma + verifica peer match cuandoexpected_peer.is_some(). Si no, verifica firma presente por consistencia interna pero no exige que esté.network::run_libp2p_accept_loopahora usasession_from_libp2p_stream(stream.compat(), peer)para propagar la identidad libp2p al gate de trust.
Client:
Client::connect_with_stream_signed(stream, card, wit, &Keypair)(nuevo) firma el Hello antes de mandarlo.Client::connect_with_streamsigue existiendo sin firma (path Unix / tests).Client::connect/connect_with(Unix) no cambian — siguen sin firma porque SO_PEERCRED autentica.network::connect_libp2p(net, peer, card, wit, keypair)breaking change: gana parámetrokeypair: &Keypair.
BrahmanNet:
- Almacena la
KeypairenArc<Keypair>(libp2p Keypair no es Clone; el truco es duplicar eled25519::Keypairinterno que sí es Clone, una copia para Noise/swarm y otra para signing). BrahmanNet::keypair() -> Arc<Keypair>accessor para que callers puedan firmar con la misma identidad libp2p del nodo sin tener que mantener la keypair por separado.with_keypairrechaza keypairs no-Ed25519 (RSA/ECDSA/Secp256k1 vendrían a futuro si se necesitan).
Tests:
- 4 unit en
signature::tests: roundtrip propio, peer mismatch, card tampered, signature flipped. - 1 E2E nuevo en
tests/network_libp2p.rs:libp2p_handshake_rejects_mismatched_signing_key— el cliente intenta firmar con keypair distinta a la del net; server rechaza. - E2E positivo (
libp2p_handshake_roundtrip) ahora pasa la keypair del client_net y debe verificar OK. - Discovery + handshake legacy + signature: 90+ tests verdes en brahman-handshake/brahman-net/brahman-card/minga-p2p.
Lo que esto cierra:
- Brahman-net es una malla públicamente dial-able con autenticación criptográfica end-to-end: Noise para el transport, Ed25519 para las Cards.
- La cadena completa de discovery + connect + trust funciona cross-machine sin paths hardcodeados ni confianza implícita.
- El plan original ("el encuentro entre Entes no se restringe a local, la ejecución remota está pensada desde el principio") está implementado y testeado.
Pendientes (futuro, no de hoy):
stop_providingal cleanup de sesión (records DHT viven hasta TTL ~24h).- Wire de Arje (
ente-zero) para arrancar opcionalmente conBrahmanNetconfigurado yServerConfig.net = Some(...). - Allowlist/Denylist de peers (hoy cualquier peer Ed25519-válido pasa el trust gate; producción podría querer un PKI explícito).
- Persistencia de la keypair de identidad del nodo entre reboots.
feat(brahman-handshake): Fase 2 — discovery remoto vía DHT por flow type
Tercer paso del plan "el encuentro entre Entes no se restringe a
local". Cuando un Init local acepta una sesión cuya Card declara
outputs, anuncia al DHT (Kademlia, vía brahman-net) que él provee
esos flow types. Cualquier nodo conectado al mismo DHT puede
consultar y obtener la lista de PeerIds que sirven el flow.
API nueva en brahman_handshake::network:
flow_dht_key(flow_name, type_ref) -> [u8; 32]: blake3 hash de"brahman-flow|v1|{flow}|{type_canon}". Determinístico cross-host. Cambiar la canonicalización rompe compatibilidad — el prefijov1documenta la versión del esquema y obliga a bump al modificar.announce_outputs(net, card): llamastart_providingen el DHT por cadaFlowencard.flow.output. Idempotente, fire-and-forget.find_remote_providers(net, flow_name, type_ref) -> Vec<PeerId>: query DHT por la key derivada. Lista vacía si nadie anuncia o si la query no resuelve dentro del timeout interno de Kad.
Wire en el server:
ServerConfigganapub net: Option<Arc<BrahmanNet>>. Si está set, cada Card registrada con outputs se anuncia automáticamente al DHT desderegister_session.None= server "ciego al DHT" (correcto cuando no hay conectividad o el operador no quiere exponer).ServerConfigahora tieneDebugmanual (BrahmanNet no implementa Debug; loggeamos sólo presencia/ausencia).
Canonicalización del TypeRef:
Primitive { name }→prim:{name}Wit { package, interface, name }→wit:{package}#{interface_or_empty}#{name}
Tests: 2 nuevos en tests/network_discovery.rs:
dht_discovery_finds_remote_provider: dos nodos, A registra Card conflow.output = monad-list:json, B dial-ea a A y descubre elpeer_idde A víafind_remote_providers. Asserts contains.dht_discovery_negative_unknown_flow: B busca un flow que nadie anunció, devuelve lista vacía sin colgarse.
Lo que esto desbloquea:
- Un
nouser daemoncorriendo en máquina A puede ser descubierto por unnouser-exploreren máquina B sin conocimiento previo del peer — sólo necesitan compartir DHT (vía bootstrap inicial). - La cadena completa "explorer → daemon → llm-provider" puede cruzar máquinas, no sólo procesos.
Lo que queda para Fase 3 (trust):
- Cards remotas se aceptan hoy sin verificación. Para producción se necesita firma Ed25519 sobre la Card y verificación antes de aceptar el Hello remoto. Local sigue confiando en SO_PEERCRED.
- Stop-providing al cleanup de sesión (hoy records DHT viven hasta TTL ~24h aunque la sesión cierre).
feat(brahman-handshake): Fase 1 — handshake brahman sobre stream libp2p
Segundo paso del plan "el encuentro entre Entes no se restringe a
local". El protocolo brahman (Hello / HelloAck / Ping / Pong /
MatchEvent / Farewell, frames postcard length-prefixed) ahora también
viaja sobre streams libp2p de la malla brahman-net — el mismo Init
acepta sesiones por Unix socket Y por libp2p indistintamente, y un
consumer remoto puede dial-ar al multiaddr y completar handshake.
Cambios:
Session<S>yClient<S>genéricos: ambos dejan de estar atados aUnixStreamy pasan a ser genéricos sobreS: AsyncRead + AsyncWrite + Unpin + Send + 'static. El path Unix queda comoClient = Client<UnixStream>(default genérico). Constructores nuevos:Server::session_from_stream(stream),Client::connect_with_stream(stream, card, wit).- Refactor del post-handshake con split:
tokio::select!sobre&mut self.streamrequeríaS: Syncindirectamente, ylibp2p::Streamno es Sync. Reemplazado portokio::io::split(stream)→ reader loop principal + writer task separada que drena el push channel. Writer compartido bajoArc<Mutex<WriteHalf<S>>>para serializar Pong/Error inline con los MatchEvents pusheados. Cleanup garantizado en todas las ramas. La lógica del post-handshake migra a funciones libres (run_post_handshake,handle_inbound_frame,cleanup,broadcast_match_diffs,do_handshake,register_session,validate_hello). - Nuevo módulo
brahman-handshake::network:BRAHMAN_HANDSHAKE_PROTOCOL = "/brahman/handshake/1.0.0".LibP2pHandshakeStream = Compat<libp2p::Stream>(alias del stream una vez convertido al mundotokio::io).run_libp2p_accept_loop(server, net): bucle accept sobre el protocolo que delega cada stream entrante a unaSessionconstruida víaserver.session_from_stream(stream.compat()). Sesiones libp2p y Unix conviven en el mismoServer— comparten broker, push table, last_matches.connect_libp2p(net, peer, card, wit): abre stream libp2p alpeery arranca handshake.NetworkErrortipado (OpenStream,Handshake,AcceptStream).
Deps: brahman-handshake gana brahman-net, futures, tokio-util.
brahman-net re-exporta Multiaddr, PeerId, Stream,
StreamProtocol, Protocol, OpenStreamError para que callers no
necesiten dep directa a libp2p.
Tests:
- 9 tests unit + integration verdes (sin regresión en el path Unix).
- Nuevo
tests/network_libp2p.rs: test E2E que arma server con Unix socket + BrahmanNet, hace listen TCP, monta el accept loop; cliente con su propio BrahmanNet dial-ea al peer_id, completa handshake remoto, pinguea, farewell. Verifica que la sesión se registró durante la conversación y se removió tras farewell.
Próximo: Fase 2 (discovery remoto vía DHT — anunciar Cards bajo flow type, broker query local + remoto).
feat(brahman-net): capa P2P compartida — Fase 0 (extracción del swarm libp2p)
Primer paso del plan "el encuentro entre Entes no se restringe a local".
El swarm libp2p que vivía dentro de minga-p2p::network (282 LOC) sale
a una crate compartida brahman-net para que cualquier protocolo de la
familia (handshake brahman remoto en Fase 1, sync minga, futuros) reuse
una sola malla TCP+Noise+Yamux+Kad+Identify+Stream.
Diseño:
BrahmanNet::{new, with_keypair}arma el swarm con DHT en modo Server, Identify auto-poblando el routing table de Kad, y unstream::Controlaccesible para que cada protocolo registre suStreamProtocoly abra/acepte streams sin acoplarse al swarm.- API de comandos uniforme:
dial,listen,add_dht_peer,find_closest_peers,start_providing,find_providers. - Pública:
peer_id(libp2p) +control(stream::Control). - Re-exporta
StreamyStreamProtocolpara que callers no necesiten importar libp2p directo.
Migración:
minga-p2p::networkreduce de 282 LOC a 22: ahora sólo re-exportaBrahmanNetbajo el alias históricoLibP2pNode(zero churn enMingaPeer) y declara la constSYNC_PROTOCOL = "/minga/sync/1.0.0"específica del sub-protocolo Minga.- Cualquier consumer que necesite armar un nodo P2P puede importar
brahman_net::BrahmanNetdirecto sin pasar por minga. - Deps de
minga-p2pgananbrahman-net; el resto del grafo (libp2p, libp2p-stream, futures, tokio-util) sigue igual porqueMingaPeeraún los usa para la lógica específica de sync.
Aclaración semántica anclada por el usuario: Arje es el init (PID 1, runtime, ente-zero/kernel/soma); Brahman es el encuentro entre Entes (handshake/broker/card/sidecar/ahora también net). El nombre de la crate refleja que la malla pertenece al encuentro, no al runtime — Arje puede usar la malla, Minga usa la malla, cualquier futuro módulo (Nakui remoto, p.ej.) la usa, sin acoplarse a Minga.
Tests: minga-p2p completo verde (58 tests, sin regresión). Behavior verificado idéntico — sólo se movió código, ningún cambio funcional. Próximo: Fase 1 (handshake brahman sobre libp2p stream).
refactor(explorer+card): independencia jerárquica enforced — cliente con los wire types + fallback al default path
Cierra el único debt estructural detectado en el audit de
independencia: nouser-explorer ya no arrastra nouser-core
(que aportaba notify/walkdir/sled/blake3 al grafo de
compilación de una UI que sólo habla JSON contra un socket).
Cambios:
- Cliente movido:
engine_socket::client::list_monads(~60 LOC, std + serde_json puros) emigra denouser_core::engine_socketanouser_card::query::client. Vive donde viven los wire types, consistente con el principio "un consumer importa el contrato, no el runtime del productor". - Drop dep:
nouser-explorerdeja de dependener denouser-core. Verificado concargo tree:notify,sled,blake3desaparecen del grafo del binario. (walkdirsigue pero llega víagpui_util→rust-embed, fuera de nuestro control y pre-existente.) - Fallback "falla hacia la simplicidad": nueva función
resolve_socket()en el explorer intenta primero broker discovery; si el broker no responde / no hay init vivo, fallback directo anouser_card::query::transport::default_socket_path(). El explorer queda funcional contra un daemon "huérfano" (corriendo standalone sin init) — completa la cadena "consciente cuando hay ecosistema, soberano cuando está solo". socket_sourceen el header gana un tercer estado"default-path"para que el usuario vea por dónde se conectó.
Audit estructural confirmó que el resto del ecosistema ya
respeta el principio: todos los yahweh-* viewers, minga-cli,
minga-core, nouser-card, nouser-nous, los providers
nouser-nous-{mock,real} y nakui-core corren standalone con
soft-fail hacia infra brahman cuando está ausente. Brahman es
"pegamento opcional, no chasis obligatorio" — y ahora el grafo
de Cargo lo enforcea, no sólo la convención.
Tests: 4 (sidecar) + 10 (nouser-card) + 27 (nouser-core) verdes.
El cliente movido se ejercita end-to-end por los 3 tests integración
de engine_socket (importa ahora nouser_card::query::client).
feat(card): Card::new(label) — alternativa segura a Default::default()
Cierra la trap documentada de Card::default() que devuelve id = Ulid::nil(). Usar Card::default() "viva" colisionaba con cualquier
otra Card default-construida bajo el mismo id 00000000…. La fix no
es romper Default (sigue siendo determinista, requerido por callers
que lo usan como template para deserialización), sino agregar un
constructor explícito:
let card = Card { kind: CardKind::Data, payload: Payload::Embedded(json), ..Card::new("mi-modulo.algo") };
Card::new(label) asigna id = Ulid::new() (único) + label
provisto, dejando el resto en defaults seguros (Virtual / OneShot /
Ente). Pensado para usarse en struct-literals con override parcial,
igual sintaxis que el patrón viejo pero sin la trap.
Refactor de call sites:
brahman_sidecar::discovery::build_consumer_card→..Card::new(label)nouser daemon::build_engine_card→..Card::new("brahman.nouser_engine")
Default se mantiene tal cual con docstring expandida que advierte
explícitamente sobre el uso "vivo" y apunta a Card::new. Tests
existentes y el patrón nouser_card::MonadManifest::to_brahman_card
(que asigna el id estable de la Mónada, no uno fresco) NO se
modifican — Default sigue siendo correcto cuando el caller
sobreescribe id explícitamente.
Tests: 3 unitarios nuevos en brahman-card (new_assigns_real_ulid_and_label,
new_yields_distinct_ids_per_call, default_keeps_nil_id_for_struct_update_pattern).
15 tests verdes (era 12).
feat(sidecar): API reusable de discovery vía broker
Promueve el patrón ad-hoc discover_producer_socket (que vivía
inline en nouser attract --remote) a un módulo público
brahman_sidecar::discovery. Cualquier consumer puede ahora
preguntar al broker "¿quién provee este TypeRef?" con dos llamadas:
// Construir un consumer Card mínimo (Ente, Oneshot, Virtual) let card = brahman_sidecar::build_consumer_card( "mi-cli", "embed-result", // flow.input.name "json", // TypeRef::Primitive { name } );
// Bloqueante (CLIs, std-thread loops): let socket: PathBuf = brahman_sidecar::await_provider_blocking( card, Duration::from_secs(3), )?; // O async (módulos con runtime tokio propio): let socket = brahman_sidecar::await_provider(card, timeout).await?;
API:
build_consumer_card(label, flow_name, type_name) -> Cardabstrae la verbosidad del struct-literal repetido en cada caller. Genera unid: Ulid::new()real (no nil → seguro contra colisiones en el broker).await_provider(card, timeout) -> Result<PathBuf, ConsumerError>conecta al init, esperaMatchEvent::Available, devuelveproducer_service_socket, manda Farewell. Ignora eventosLostdurante el await (no aplican al arranque).await_provider_blocking(card, timeout)arma su propio runtimecurrent_threadpara mundos no-async.ConsumerErrorcon variantes tipadas:Connect { socket, source },NoProvider { flow, type_ref, timeout },Client(ClientError),Runtime(String). Adiós alBox<dyn Error>de antes.
Refactor en nouser daemon:
discover_producer_socket(60 LOC inline enbin/nouser.rs) → 5 líneas que delegan en el helper.remote_embedya no construye su propio runtime tokio.
Próximo consumer natural: nouser-explorer. Hoy renderea
StatusSnapshot vía socket admin (introspección pura). El día que
quiera interactuar con un Ente — p. ej., disparar un re-embed
desde la UI — usa este helper para resolver el socket del provider
sin hardcodear paths.
Nota sobre identidad: este commit fuerza Ulid::new() para los
consumer Cards generados, evitando la trampa documentada del
Card::default() que devuelve Ulid::nil(). La fijación global de
Default queda como cleanup separado (requiere auditar que ningún
caller dependa del determinismo de nil).
Tests: 4 unitarios nuevos en discovery::tests (id no-nil, id
único por llamada, formateo de TypeRef::Wit, fallback sin input).
Workspace verde.
feat(sidecar): Phase B-3 — SidecarPool consolida en un runtime
Antes: cada spawn(card) creaba un thread + tokio runtime propio.
Para módulos que publican muchas sesiones (nouser daemon con 50+
Mónadas) eso es 50 threads + 50 runtimes. Ahora: un thread + un
runtime tokio current_thread que hostea N tasks de sidecar.
API nueva (aditiva, no rompe spawn/spawn_with_handle):
let pool = SidecarPool::new()?; pool.spawn(card1); pool.spawn(card2); pool.spawn_conscious(card_wit, wit); pool.spawn_with_config(SidecarConfig::new(c).with_wit(w)); // pool drop = todas las sesiones cierran.
run_client se hace pública para que el pool pueda enqueuar tasks
externos al runtime con handle.spawn(run_client(config)).
nouser daemon migrado al pool. Verificación con ps -L:
$ ps -L -p $(pidof nouser) LWP CMD 28817 nouser # main thread 28819 brahman-sidecar # pool thread (todas las sesiones)
Antes serían 6+ LWP (1 main + N sesiones); ahora 2 fijos sin importar cuántas Mónadas se publiquen.
feat: Crossreferencia — Card.references como grafo del fractal
Las Cards ahora declaran sus relaciones con otras Cards. El Engine posee Mónadas; las Mónadas declaran que son poseídas por el Engine. La UI puede cruzar el grafo sin discovery especial.
brahman-card:RelationshipKind { Owns, OwnedBy, Processes, ProcessedBy, Sibling }.CardReference { kind, target_id, target_label }—target_labeles cache del label en el momento de declarar (la UI puede pintar sin resolver).Card.references: Vec<CardReference>y espejo enWireCard. ConversionesFrompropagan.
brahman-broker::BrokeredCardpropagareferences.brahman-statusimprime cada referencia:ref OwnedBy → label (id).- nouser daemon: cada Mónada que publica añade
RelationshipKind::OwnedByapuntando al engine. La declaración es unilateral; el engine no necesita conocer las IDs de antemano.
Validación end-to-end:
$ ente-zero & nouser daemon crates/core $ brahman-status Sessions (6): [ente] ... brahman.nouser_engine [data] ... brahman-handshake/src ref OwnedBy → brahman.nouser_engine (01K...) summary: 6 archivos... [data] ... ente-brain/src ref OwnedBy → brahman.nouser_engine (01K...) ...
feat: Phase D-3 + D-4 — service_socket en Card, providers coexisten
Cierra el ciclo del swap automático de Nous (mock↔real):
- Schema (
brahman-card):Card.service_socket: Option<PathBuf>y espejo enWireCard. ConversionesFrompropagan. Es el path del data plane (distinto del socket del Init); cualquier consumer que matchee con esta Card puede conectar directo sin discovery adicional. - Broker (
brahman-broker):BrokeredCardpropagaservice_socketdesde la Card. Sin participación en el matching — sólo metadata para los observadores. - MatchEvent (
brahman-handshake): nuevo campoproducer_service_socket: Option<PathBuf>. Cuando el server emiteAvailable, busca laBrokeredCarddel productor en el broker y copia suservice_socket. El consumer recibe la ruta completa para conectar. - Transport (
nouser-nous):provider_socket_path(provider: &str)devuelvenouser-nous-{provider}.sockpor default — mock y real coexisten en sockets distintos (Phase D-4).default_socket_path()conserva el comportamiento single-provider. - Providers: mock declara
service_socket = /run/user/X/nouser-nous-mock.sock; real declaranouser-nous-real.sock. La Card se construye DESPUÉS del bind para que el path declarado sea el real. - Status:
brahman-statusimprimesocket:por sesión cuando está presente.
Validación end-to-end:
$ ente-zero & nouser-nous-mock & nouser-nous-real & $ ls /run/user/1001/nouser-nous-*.sock nouser-nous-mock.sock nouser-nous-real.sock
$ brahman-status Sessions (2): [ente] ... nouser.nous_real socket: /run/user/1001/nouser-nous-real.sock in embed-request: Primitive { name: "json" } out embed-result: Primitive { name: "json" } [ente] ... nouser.nous_mock socket: /run/user/1001/nouser-nous-mock.sock in embed-request, out embed-result
Pendientes para futuro (no críticos):
- nouser-core attract --remote todavía usa NOUSER_NOUS_SOCKET hardcoded
o
default_socket_path(). El siguiente paso es subscribirse al MatchEvent del broker y usarproducer_service_socketdirecto — con esoBRAHMAN_BROKER_CONTEXT=test/prodswapea provider sin tocar al consumer.
feat(broker): priority contexts — biases per-contexto operativo
brahman-card::ContextBias { pin_to: Option<String>, priority_offset: i8 }declara un override per-contexto.Card.priority_contexts: BTreeMap<String, ContextBias>y mismo enWireCard(cruza el wire). Las conversionesFromlo propagan.BrokerConfig.current_context: Option<String>. Cuando el broker corre bajo un contexto y una Card declara biases para ese nombre, se aplican:- Como consumidor:
pin_tosobreescribe elFlow.pin_toestático. - Como productor:
priority_offsetse suma a la priority base (clamp en[Low=0, Critical=3]) para el ranking.
- Como consumidor:
BrokeredCardpropagapriority_contexts.find_producer_forusaeffective_priority(card)yeffective_pin(card, input)antes de los tiebreaks.brahman-admin::AdminConfig.current_context+StatusSnapshot.current_contextespejan el contexto activo.brahman-statuslo imprime comoContext: <nombre>justo debajo deInit: ....ente-zeroleeBRAHMAN_BROKER_CONTEXTenv var y la propaga al broker y al admin. Sin var, biases per-contexto inactivos.- 4 tests nuevos en brahman-broker:
context_priority_offset_lifts_producer_above_alphabetic_winner,context_pin_to_overrides_static_pin,unknown_context_no_op,priority_offset_clamps_to_critical. - Validación end-to-end:
BRAHMAN_BROKER_CONTEXT=test ente-zero→brahman-statusmuestraContext: test.
feat(card): WireCard + extensions — forward-compat sin romper postcard
Card.extensions: BTreeMap<String, serde_json::Value>restaurado con#[serde(flatten, default, skip_serializing_if = is_empty)]. Los campos JSON/TOML desconocidos sobreviven el roundtrip de archivos.- Nuevo
WireCard: proyección postcard-friendly (sinextensions,genesis: Vec<WireCard>recursivo). ConversionesFrom<Card>yFrom<WireCard>con descarte/recreación de extensions. brahman-handshake::Hello.cardpasa deCardaWireCard. Client hacecard.into()antes de enviar; Server hacehello.card.into()para volver a Card antes de validar/registrar.- 3 tests nuevos en brahman-card:
extensions_preserved_in_json_roundtrip,wire_card_roundtrip_strips_extensions,wire_card_postcard_friendly(postcard encode/decode efectivo). - brahman-card gana
postcardcomo dev-dep para el último test. - Contrato documentado: extensions = anotaciones locales que NO cruzan al Init; sólo viven en archivos.
9420eae chore: limpia warnings dead-code en arje (commit del usuario)
ente-zero/src/events.rs:#![allow(dead_code)]a nivel módulo — es vocabulario de eventos con variantes/campos reservados para flujos no cableados aún (CapabilityRequested, ShutdownReason::Signal, CapabilityGrant::{Granted, Denied, QuotaExceeded}, ExitStatus fields).ente-zero/src/graph/mod.rs: comentado el re-export ahora innecesario deSHUTDOWN_GRACE.DEFAULT_GRANT_TTLcon#[allow(dead_code)]- nota "reservado para capability granting".
ente-zero/src/graph/capabilities.rs:renew_grantcon#[allow(dead_code)](capability renewal pendiente).ente-kernel/src/surface.rs: drop deuse anyhow::Context(no se usaba).ente-hostnamed-compat/src/main.rs: drop deConnection(no se usaba).ente-polkit-compat/src/main.rs:PolicyDecision.sourcecon#[allow(dead_code)](sólo aparece enDebugpara logging).cargo check --workspace: 17 warnings → 0.
feat(sidecar): WIT al sidecar — módulos conscientes vivos
brahman-card::WitInterfacederivaSerialize,Deserialize,PartialEq,Eqpara cruzar el wire postcard.brahman-handshake::Hellollevawit: Option<WitInterface>. Server usaResolvedCard::from_consciouscuando viene presente,from_agnosticcuando no.brahman-handshake::Client::connectqueda como wrapper agnóstico deconnect_with(path, card, wit: Option<WitInterface>).brahman-broker::Broker::registerahora tomaOption<WitInterface>como tercer arg.BrokeredCardguarda el wit. 25 sitios de tests actualizados con, None.brahman-sidecar::SidecarConfigcon campowit. Helpers nuevos:SidecarConfig::new(card).with_wit(wit)yspawn_conscious(card, wit). El logattachedreportaconscious=true|false.brahman-statusmuestra marker 🧠 + secciónwit:(package/world, imports, exports) por sesión consciente.- Example nuevo
crates/shared/brahman-sidecar/examples/presence-conscious.rs: toma label + path .wit (defaultshared_wit/protocol.wit), parsea con brahman-card-wit, spawna sidecar consciente. - Validado end-to-end:
$ presence-conscious demo.conscious shared_wit/protocol.wit & $ brahman-status Sessions (1): 01K... demo.conscious 🧠 lifecycle=Daemon wit: brahman:protocol@0.1.0 / module imports: types, handshake, lifecycle exports: run
feat(core): brahman-card-wit — extractor opcional de contratos WIT
- Crate nuevo
crates/core/brahman-card-witconwit-parser = "0.230". - API:
parse_wit(source)yparse_wit_file(path)devuelvenVec<WitInterface>(uno porworlddeclarado). - Interfaces importadas/exportadas (no sólo funciones) se resuelven
por nombre via
resolve.interfaces[id].name. - Example
crates/core/brahman-card-wit/examples/brahman-wit-info.rsCLI:brahman-wit-info shared_wit/protocol.wit→ lista paquete, worlds, imports y exports. - 4 tests: inline, archivo real (
shared_wit/protocol.wit), parse error, world vacío. - Validado contra
protocol.wit: detecta worldsmoduleyadmin-hostcon sus imports/exports correctos.
7b589b8 chore: agrega CHANGELOG.md retroactivo
CHANGELOG.mden la raíz con los 11 commits previos documentados acción por acción. A partir de este punto, cada cambio sustantivo actualiza también este archivo en el mismo commit.
8a83a26 feat(handshake): notificación push de matches
- Frame
MatchEvent { kind: Available | Lost, ... }añadido al protocolo. Session::run_post_handshakeusatokio::select!para multiplexar reads del cliente y un canalmpscpush del server.- Server:
SessionTxTable(Arc<Mutex<HashMap<SessionId, Sender>>>) yLastMatchespara diff por sesión.broadcast_match_diffscorre tras cadaregisteryunregister, emite sólo los cambios. - Capacity del canal push: 32 (ephemeral,
try_sendnon-blocking). - Client:
VecDeque<MatchEvent>interno,take_event()(non-blocking) yawait_event(timeout).ping()ahora drena MatchEvents intermedios hasta encontrar el Pong. - Example
crates/core/brahman-handshake/examples/subscriber.rs. - Test
match_event_pushed_on_producer_arrival(handshake integ 6→7).
70a7a0d feat: segundo módulo (nakui) + admin API + brahman-status
- Crate nuevo
crates/shared/brahman-sidecar(DRY del thread + tokio + ping loop). API:spawn(card)/spawn_with_handle(config). nakuicmd_run llamabrahman_sidecar::spawnantes derun_server. Card: lifecycle Daemon, supervision Restart, flowcommand(json) /report(json).- Crate nuevo
crates/core/brahman-adminconStatusSnapshotJSON line-delim,AdminServeryclient::query. - ente-zero levanta también el AdminServer en
primordial_loop. - Example
crates/shared/brahman-sidecar/examples/presence.rs(módulo dummy long-lived parametrizable por label). - Example
crates/core/brahman-admin/examples/brahman-status.rs(CLI que pretty-printa el snapshot). brahman-broker:BrokeredCardahora incluyelifecycle.EndpointyMatchderivanSerialize/Deserialize. NuevoBroker::cards()iterador.brahman-card:pub use ::ulidpara que módulos no dependan de ulid.- yahweh-shell migrado al sidecar compartido (96→53 LOC).
595f68e feat(yahweh-shell): primer módulo brahman vivo
- yahweh-shell spawnea sidecar antes de
Application::new(). - Card declarada: label
brahman.ui_engine, lifecycle Widget, supervision Delegate, payload Virtual, flow inputrender-data(json) / outputuser-intent(json). - Sidecar en thread aparte con tokio current_thread runtime, desacoplado del runtime GPUI.
df9d10c feat(ente-zero): enchufa el handshake server al Init real
- ente-zero levanta
brahman_handshake::server::Server::bindenprimordial_loopdespués del ente-bus, con degradación grácil si bind falla (mismo patrón que uevents). - Nuevo módulo
brahman-handshake/src/transport.rs: helperdefault_socket_path()con resoluciónBRAHMAN_INIT_SOCKET→XDG_RUNTIME_DIR→TMPDIR. - Example
crates/core/brahman-handshake/examples/probe.rs. - Validación end-to-end manual: probe contra ente-zero vivo
imprime
HelloAck: session=... init_attached=true.
07d77a3 feat(handshake): integra el broker con el ciclo de sesiones
ServerConfigaceptaOption<Arc<Mutex<Broker>>>.register_sessionindexa la Card en el broker y laSessionRegistryantes de emitir HelloAck.Session::handlerefactor ado_handshake → run_post_handshake → cleanupcon cleanup unificado (broker + sessions).- Tests integ nuevos:
broker_registers_and_unregisters_with_sessionybroker_matches_two_live_modules. - Fix colateral:
brahman-card::TypeRefpasa de internally-tagged (#[serde(tag = "kind")]) a externally-tagged. Postcard no soporta internally-tagged en formatos no self-describing. JSON cambia de{"kind":"primitive","name":"x"}a{"primitive":{"name":"x"}}.
5091106 feat(core): brahman-broker — matching híbrido
- Crate nuevo
crates/core/brahman-broker. - 3 estrategias de matching:
Exact,Structural,ExactThenStructural(default). DevuelvenMatch::viacon la estrategia que ganó. - Override
pin_to: el consumer pide un productor por label; si la pista no resuelve, cae en type-search. - Tiebreak por
Card.prioritydesc, luegolabelasc (estable y determinista). - API:
register,unregister,find_producer_for,all_matches,cards,sessions,len,is_empty. - 11 tests (matching, pin_to, priority, no-self-loops, all-matches).
814390f feat(core): brahman-handshake — protocolo runtime
- Crate nuevo
crates/core/brahman-handshakecon server y client Rust↔Rust sobre Unix socket. - Frames length-prefixed (4 bytes LE) + cuerpo postcard.
- Mensajes:
Hello,HelloAck,Ping,Pong,Farewell,Error. MAX_FRAME_BYTES = 4 MiBpara evitar reservas absurdas.- Tradeoff: drop
extensions/extrade Card por incompat postcard ↔serde_json::Value. Forward-compat queda enschema_version+protocol_versionnegotiation. - 4 tests integ + 1 unit en codec.
ed0e973 refactor(arje): migra ente-card a re-export de brahman-card
ente-card/src/lib.rsreescrito como crate-shim de re-export (327 LOC → 25 LOC).EntityCard≡brahman_card::Cardpor type alias.ente-card/Cargo.toml: deps reducidas abrahman-card.CardimplDefault(Ulid::nil(), label vacío) para que..Default::default()funcione en struct-literals.- 4 sitios en
ente-zero/src/seed.rsactualizados con..Default::default()para los campos aditivos. - Los 21 consumidores arje compilan sin tocar fuente.
0feba74 feat(core): brahman-card — Tarjeta canónica híbrida
- Crate nuevo
crates/core/brahman-card. - Hereda de arje:
id: Ulid,lineage,Capabilitytipado,Payload::{Wasm, Native, Virtual, Legacy},SomaSpec(namespaces, cgroups, rlimits, cpu_affinity),Supervision(Restart con backoff, OneShot, Delegate),genesisrecursivo. - Aditivo brahman:
Permissionsenumerados (NetworkingPolicy,FsPolicy,IpcPolicy),Lifecycleortogonal a Supervision,Priorityde scheduling,FlowsconTypeRefdiscriminado (Primitive | Wit),pin_toopcional. TrustLevelderivado dePermissions(no declarado).ResolvedCard { card, wit: Option<WitInterface>, trust }.- Soporta JSON (canónico) + TOML (auto-detectado por extensión).
- 8 tests incluido
arje_seed_format_compatibleque valida que el JSON de arje sigue parseando con defaults para los aditivos.
4d50bfc chore: absorbe nakui (ERP matemático) en modules/nakui
~/nakui→crates/modules/nakui/{core,modules}.core/: el cratenakui-corecon 4 bins (nakui, demo, inventory_demo, sales_demo) y tests.modules/{inventory,sales,treasury}/: data declarativa (nsmc.json,schema.k,morphisms/) que el crate consume. No son crates Cargo.- Deps directas (no
workspace = true): thiserror v1, surrealdb, rhai, petgraph. No conflicto con el resto del workspace.
53dbdf0 chore: monorepo inicial con arje + minga + yahweh absorbidos
- 45 crates absorbidos en 4 ejes:
crates/core/: 24 crates de arje (Init systemd-compatible:ente-card,ente-zero,ente-kernel,ente-bus,ente-cas,ente-soma,ente-wasm,ente-snapshot,ente-brain,ente-echo,ente-policy-provider, + 12*-compat).crates/modules/semantic_dht/: 5 crates de minga (minga-corecon AST/CAS/MST,minga-p2pcon libp2p Kad,minga-store,minga-vfs,minga-cli).crates/modules/ui_engine/: 11 crates de yahweh (libs/{core, theme, bus, providers}, widgets/{tree, splitter, tabs, tiled, container_core, text_input}).crates/apps/: 5 crates de yahweh (file_explorer, database_explorer, text_viewer, image_viewer, yahweh-shell).
shared_wit/protocol.witcon handshake/lifecycle inicial.Cargo.tomlunificado: thiserror bumped a 2 (transparente para arje), tokio "full", paths intra-workspace de yahweh redirigidos.cargo check --workspace: 0 errores (sólo dead-code warnings preexistentes en ente-zero).