Commit Graph

9 Commits

Author SHA1 Message Date
Sergio a97f6b98f3 feat(brahman-handshake): ListMatches endpoint + timeline en broker-explorer
Iter 21. Cierra el loop iniciado en iter 20: ahora se ven sesiones
+ matches actuales + cómo cambian a través del tiempo.

brahman-handshake/messages:
- Frame::ListMatches → Frame::MatchList(Vec<brahman_broker::Match>).

brahman-handshake/server:
- run_post_handshake pasa Option<&SharedBroker> a handle_inbound_frame.
- Sin broker configurado → MatchList vacía (no error).

brahman-handshake/client + brahman-sidecar:
- Client::list_matches() análogo a list_sessions, drena MatchEvents.
- list_matches / list_matches_blocking, mismo patrón.

brahman-broker-explorer:
- Poll-tick agrega list_matches_blocking además de list_sessions.
- last_match_keys: HashSet<MatchKey> para diff entre ticks.
- timeline: VecDeque<TimelineEntry> cap 50.
- diff_matches (free fn): Available para keys nuevas, Lost para
  desaparecidas. Primer tick marca todo Available (boot UX).
- Render: stat_card "Timeline" con HH:MM:SS {+/-} formato compacto.

5 tests broker-explorer (3 nuevos del diff). Stack verde.

Decisión: timeline polled cada POLL_INTERVAL=5s, no push. MatchEvents
del broker son consumer-céntricos (cada session ve sólo SUS matches);
"system-wide timeline" requeriría broker subscribe-all (mucho scope).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:53:38 +00:00
Sergio 99cd685dc1 feat(brahman-handshake): ListSessions endpoint + cliente + UI broker-explorer
Iter 20. Nuevo flujo end-to-end para observabilidad: cualquier módulo
conectado puede pedir al broker la lista de sesiones activas y mostrar
labels + flows in/out por cada una.

brahman-handshake/messages:
- Frame::ListSessions(ListSessions{session}) → Frame::SessionList(SessionList{entries}).
- SessionEntry: session, label, schema_version, outputs, inputs, conscious.

brahman-handshake/server:
- run_post_handshake pasa SessionRegistry a handle_inbound_frame.
- build_session_list helper proyecta el snapshot bajo lock.
- Validación session_id mismatched → Unauthorized.

brahman-handshake/client:
- Client::list_sessions() async, drena MatchEvents intermedios al
  pending_events buffer, mismo patrón que ping().

brahman-sidecar/discovery:
- list_sessions / list_sessions_blocking arman Card observer mínima,
  piden, Farewell.

brahman-broker-explorer:
- Poll-tick agrega list_sessions_blocking cuando broker está UP*.
- stat_card "Sesiones activas" con count + items ordenados por Ulid:
  label · in:[flows] out:[flows]  (wit)?.

Test list_sessions_returns_currently_registered: 3 clientes
conectados, observer pide list, verifica labels + schema_version
+ conscious=false. 24 handshake tests + sidecar + broker-explorer
verde.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:40:33 +00:00
Sergio f9a3c33586 feat(brahman-handshake): multi-key identity — rotacion de session sin perder peer_id logico
Cierra el ultimo 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.

Solucion: separar identity master (Ed25519 persistente forever,
identifica al nodo como entidad logica) de session libp2p (Ed25519
efimera, rotable). El master firma certs de session con expiracion.
La politica de admision se evalua contra el master_peer_id del cert
— el session peer_id puede cambiar libremente sin tocar allowlists.

API nueva en brahman_handshake::identity:
- Identity::from_keypair / master_peer_id / issue_session_cert.
- SessionCert::verify 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).
- CertError tipado: UnknownVersion, DecodeMaster, DecodeSession,
  InvalidSignature, Expired, SessionMismatch, Sign.
- DEFAULT_SESSION_TTL = 24h. SESSION_CERT_VERSION = 1 documenta
  esquema; bump al cambiar canonicalizacion.

Wire:
- Hello.identity_cert: Option<SessionCert> agregado (default None,
  back-compat).
- Client::connect_with_stream_signed_with_cert variante que adjunta
  cert.
- network::connect_libp2p_with_cert paralelo a connect_libp2p.

Server (do_handshake): nuevo paso ANTES del policy gate. Si el
Hello trae cert, verify_against_session(&hello.signature.public_key)
y el logical_peer = master_peer_id derivado. Sin cert (path Fase 3),
logical_peer = expected_peer (compat). Cert invalido -> Unauthorized
antes de evaluar policy.

Migracion gradual: clientes sin cert siguen funcionando contra
servers con policy basada en session peer_ids.

Tests: 8 unit en identity::tests (issue+verify, mismatch, expired,
tampered sig/expires_at, unknown version, rotated_session_with_same_
master_yields_same_master_peer_id — la propiedad fundamental).

E2E definitivo identity_cert_allows_session_rotation_without_policy_
change: A allowlist[master_peer]; B conecta con session1+cert ->
admitido; B rota a session2!=session1 con cert nuevo del MISMO
master -> admitido SIN tocar la allowlist; sanity: session sin cert
es rechazada.

40 tests verdes en brahman-handshake + brahman-net.

Wire en Arje queda como follow-up: ente-zero es server-only y no
necesita identity (su keypair libp2p ya es estable). La API esta
lista para cuando algun modulo haga conexiones salientes con cert.
2026-05-09 15:55:36 +00:00
Sergio c164e9f422 feat(brahman-handshake): Fase 3 — trust remoto via firma Ed25519
Cuarto 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
podia registrar cualquier Card con cualquier label/flow. Fase 3
exige que el Hello via libp2p venga firmado con la misma keypair
Ed25519 que produce el peer_id autenticado por Noise.

Modelo de trust:
- Local Unix: SO_PEERCRED del kernel autentica. Firma opcional pero
  verificada si presente (defensa en profundidad).
- Remoto libp2p: firma obligatoria; public key del Hello debe derivar
  al peer_id autenticado por Noise. Si falta o no coincide ->
  HandshakeError::Unauthorized.

Wire:
- Hello.signature: Option<HelloSignature> (default None, backwards
  compat para path Unix).
- HelloSignature { public_key: Vec<u8>, signature: Vec<u8> } —
  public_key en formato canonico libp2p (encode_protobuf), firma
  Ed25519 sobre (SIGNATURE_VERSION, WireCard, Option<WitInterface>)
  serializado postcard.

Nuevo modulo signature:
- sign_hello / verify_hello con SignatureError tipado.
- 4 unit tests: roundtrip, peer mismatch, card tampered, sig flipped.

Server:
- Session<S> gana expected_peer: Option<PeerId>.
- session_from_libp2p_stream(stream, peer) para path remoto;
  session_from_stream sin peer para Unix.
- do_handshake exige firma + verifica peer match si expected_peer.

Client:
- connect_with_stream_signed(stream, card, wit, &Keypair) (nuevo).
- connect_libp2p ahora requiere &Keypair (breaking change).

BrahmanNet:
- Almacena Arc<Keypair> internamente; expose keypair() accessor.
  Truco: ed25519::Keypair SI es Clone, se duplica para que swarm
  (Noise) y caller (signing) compartan identidad sin copiar bytes.
- with_keypair rechaza no-Ed25519.

Tests: 4 unit signature + 1 E2E negativo nuevo
(libp2p_handshake_rejects_mismatched_signing_key) + E2E positivo
ya pasaba con keypair correcta. 90+ tests verdes en
brahman-handshake/brahman-net/brahman-card/minga-p2p.

Lo que cierra: la cadena completa de discovery + connect + trust
funciona cross-machine sin paths hardcodeados ni confianza
implicita. Brahman-net es una malla publicamente dial-able CON
autenticacion criptografica end-to-end.

Pendientes futuros: stop_providing en cleanup, wire de Arje con
ServerConfig.net configurado, allowlist/denylist de peers,
persistencia de la keypair entre reboots.
2026-05-09 14:50:04 +00:00
Sergio 73dadbb166 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 tambien
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> y Client<S> genericos: ambos dejan de estar atados a
  UnixStream y pasan a ser genericos sobre S: AsyncRead + AsyncWrite
  + Unpin + Send + 'static. El path Unix queda como
  Client = Client<UnixStream> (default generico). 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.stream requeria S: Sync indirectamente, y libp2p::Stream
  no es Sync. Reemplazado por tokio::io::split(stream) -> reader loop
  principal + writer task separada que drena el push channel. Writer
  compartido bajo Arc<Mutex<WriteHalf<S>>> para serializar Pong/Error
  inline con los MatchEvents pusheados. Cleanup garantizado en todas
  las ramas. La logica del post-handshake migra a funciones libres
  (run_post_handshake, handle_inbound_frame, cleanup,
  broadcast_match_diffs, do_handshake, register_session,
  validate_hello).

- Nuevo modulo brahman-handshake::network:
  - BRAHMAN_HANDSHAKE_PROTOCOL = "/brahman/handshake/1.0.0"
  - LibP2pHandshakeStream = Compat<libp2p::Stream>
  - run_libp2p_accept_loop(server, net): accept loop que delega cada
    stream entrante a session_from_stream(stream.compat()). Sesiones
    libp2p y Unix conviven en el mismo Server — comparten broker,
    push table, last_matches.
  - connect_libp2p(net, peer, card, wit): abre stream libp2p al peer
    y arranca handshake.
  - NetworkError tipado.

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 verdes en el path Unix (sin regresion). Nuevo
tests/network_libp2p.rs E2E que arma server con BrahmanNet, hace
listen TCP, monta accept loop; cliente con su propio BrahmanNet
dial-ea al peer_id, completa handshake remoto, ping, farewell.
Verifica que la sesion se registro durante la conversacion y se
removio tras farewell.
2026-05-09 12:51:43 +00:00
Sergio f19ca723b6 feat(card): WireCard + extensions — forward-compat sin romper postcard
Restaura el campo extensions de Card que había caído al adoptar postcard
(serde_json::Value usa secuencias/maps de longitud dinámica). La
solución es separar dos formas:

- Card (la rica): para JSON/TOML. Tiene extensions: BTreeMap<String,
  serde_json::Value> con #[serde(flatten, skip_serializing_if = is_empty)].
  Los campos desconocidos del archivo sobreviven el roundtrip.
- WireCard (la slim): para postcard. Mismo schema sin extensions y con
  genesis: Vec<WireCard> recursivo. Postcard-friendly por construcción.

Conversiones From<Card> for WireCard (descarta extensions) y
From<WireCard> for Card (extensiones quedan vacías post-wire). El
contrato es explícito: extensions son anotaciones locales que sobreviven
file I/O pero NO cruzan al Init.

brahman-handshake::Hello.card cambia de Card a WireCard. Client hace
card.into() al enviar; Server hace hello.card.into() para volver a
Card antes de validar/registrar.

Tests:
- 3 nuevos en brahman-card: extensions_preserved_in_json_roundtrip,
  wire_card_roundtrip_strips_extensions, wire_card_postcard_friendly
  (verifica que postcard::to_allocvec(&wire) NO falla — caso que
  rompía con Card.extensions populadas).
- 1 ajuste en handshake/tests/handshake.rs (struct-literal de Hello
  ahora con card: sample_card(...).into()).
- brahman-card: postcard como dev-dep.

Tests acumulados: 35 (card 11, broker 11, handshake codec+transport 2 +
integ 7, card-wit 4, admin 0). 0 errores, 0 warnings (vienen del
commit anterior 9420eae).

CHANGELOG.md actualizado con esta entrada y con el commit 9420eae
("probando" del usuario, limpieza de 17 warnings dead-code).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 17:33:15 +00:00
Sergio 354f992c63 feat(sidecar): WIT al sidecar — módulos conscientes vivos
Cierra el ciclo brahman-card-wit ↔ runtime: un módulo que tenga su
.wit lo parsea, lo manda en Hello, y aparece como "consciente" en el
broker y en brahman-status.

Cambios coordinados (un solo commit por la cadena de tipos):

- brahman-card::WitInterface deriva Serialize/Deserialize/Eq.
- brahman-handshake::Hello lleva wit: Option<WitInterface> (#[serde(default)]
  para tolerar Hellos antiguos en formato JSON aunque postcard exige
  presencia explícita).
- Server's register_session enruta a ResolvedCard::from_conscious cuando
  viene wit; from_agnostic cuando no.
- Client::connect queda como wrapper de connect_with(path, card,
  wit: Option<WitInterface>) — backward-compatible.
- Broker::register acepta Option<WitInterface> como tercer arg; BrokeredCard
  guarda el wit. 25 sitios de tests actualizados con `, None` (vía perl).
- brahman-sidecar::SidecarConfig.wit + helpers SidecarConfig::with_wit
  y spawn_conscious(card, wit). Log attached reporta conscious=true|false.
- brahman-status pretty-print con 🧠 + sección wit (package/world +
  imports + exports) por sesión consciente.
- Example nuevo presence-conscious: parsea protocol.wit y se presenta
  consciente.

Validación end-to-end manual:

  $ ente-zero &
  $ 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

Tests: 32/32 (broker 11 + card 8 + handshake codec+transport 2 + integ 7
+ admin 0 + card-wit 4). Workspace: 0 errores.

CHANGELOG.md actualizado.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 17:22:48 +00:00
Sergio 8a83a26de0 feat(handshake): notificación push de matches del broker al cliente
El servidor empuja MatchEvent (Available | Lost) a los consumers cuando
sus inputs cambian de match — sea porque un productor llegó, porque
otro mejor lo desplazó, o porque desapareció.

Mecánica:

- Frame::MatchEvent con MatchEventKind { Available, Lost } y los datos
  del match (consumer_flow, producer_session/label/flow, ty, via, pinned).
- Server: SessionTxTable (Arc<Mutex<HashMap<SessionId, mpsc::Sender>>>)
  + LastMatches (último match conocido por consumer/input). En cada
  register/unregister, broadcast_match_diffs recomputa con el broker
  y emite SOLO los diffs respecto al estado anterior.
- Session::run_post_handshake usa tokio::select! para multiplexar
  read_frame del cliente y rx.recv() de su tx push.
- Cleanup ahora también limpia push_table y last_matches y dispara un
  broadcast (para notificar a quienes pierden el match).
- Client: VecDeque<MatchEvent> bufferea eventos que llegan mezclados
  con respuestas a Ping. API:
    - take_event() — non-blocking, drena buffer
    - await_event(timeout) — bloquea hasta evento o timeout
- ping() ahora drena MatchEvents intermedios hasta encontrar el Pong.

Capacity del canal push por sesión: 32 frames (try_send no-blocking;
si se llena, los eventos extra se descartan — se documenta como
ephemeral, el cliente puede re-consultar via brahman-status).

Test nuevo en brahman-handshake/tests/handshake.rs:
- match_event_pushed_on_producer_arrival: consumer espera, no recibe
  evento → llega productor → recibe Available → productor se va →
  recibe Lost.

Example nuevo: brahman-handshake/examples/subscriber.rs — cliente que
loguea cada MatchEvent en tiempo real. Útil para ver la dinámica del
broker. Pings cada 25s para keepalive.

Demo end-to-end verificada (4 eventos, 3 ya cubren el ciclo completo):

  T+0.3  alpha llega    → Available ← demo.alpha.out
  T+0.8  beta llega     → (sin evento: alpha gana por orden alfabético)
  T+1.3  alpha killed   → Available ← demo.beta.out (re-evaluación)
  T+1.8  beta killed    → Lost ← <none>

El broker emite diff: ningún evento cuando un nuevo productor llega
sin desplazar al ganador actual.

Tests: 28/28 (handshake integ 6→7). cargo check --workspace: 0 errores.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 15:43:41 +00:00
Sergio 814390feec feat(core): brahman-handshake — protocolo runtime Init↔módulo
Crate nuevo en crates/core/brahman-handshake que implementa el handshake
real del shared_wit/protocol.wit como wire format Rust↔Rust sobre Unix
socket con frames length-prefixed + cuerpo postcard.

Componentes:

- src/messages.rs: Hello, HelloAck, Ping, Pong, Farewell, HandshakeError
  y Frame (enum-suma). HandshakeError ya implementa thiserror::Error y
  cruza el wire.
- src/codec.rs: write_frame / read_frame asíncronos con MAX_FRAME_BYTES
  de 4 MiB. Test interno de roundtrip.
- src/server.rs: Server::bind crea el listener en Unix socket; emite
  ResolvedCard tras validar la Card y devuelve ULID como SessionId.
  ServerConfig.init_attached se reporta en HelloAck.
- src/client.rs: Client::connect hace pre-validación local de la Card
  (fail fast), envía Hello, parsea HelloAck. ping() y farewell() expuestos.
- tests/handshake.rs: 4 tests de integración:
   * full_handshake_roundtrip — happy path con 3 pings + farewell
   * rejects_invalid_card_client_side — label vacío rechazado pre-envío
   * server_rejects_protocol_mismatch — protocol_version 999.0.0 → Error
   * ping_before_hello_rejected — Ping sin Hello previo → Rejected

Limitación conocida: postcard no serializa serde_json::Value (variantes
Array/Object con length dinámico). Se removieron por eso los campos
`extensions` (Card) y `extra` (Permissions). Forward-compat queda
cubierta por schema_version + protocol_version negotiation; si más
adelante necesitamos preservar campos JSON desconocidos, irá en un
WireCard separado o un envelope.

13/13 tests verdes (brahman-card 8 + brahman-handshake codec 1 + integ 4).
cargo check --workspace: 0 errores.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 13:57:49 +00:00