merge: origin/master into local

This commit is contained in:
2026-05-27 18:48:28 +02:00
195 changed files with 24562 additions and 129 deletions
+274
View File
@@ -0,0 +1,274 @@
---
name: agentes-dispositivos-mesh
id: 0009
status: pending
created: 2026-05-23
updated: 2026-05-23
priority: high
risk: high
related_issues: [0134, 0135, 0136, 0137, 0138, 0139, 0140, 0141, 0142, 0143]
apps: [agents_dashboard, agents_and_robots, wg_hub, device_agent]
projects: [element_agents]
vaults: []
capability_groups: [wireguard, device-agent, docker-agent]
trigger: manual
schedule: ""
expected_runtime_s: 300
tags: [mesh, wireguard, matrix, e2ee, agents, devices, docker, sandboxing]
---
## Goal
Hablar desde Element con dispositivos completos (PCs, moviles, raspberry, IoT) y con
contenedores Docker como si fueran agentes Matrix. Cada device/container ejecuta sus
capabilities declaradas (shell/fs/camera/docker/sensores) bajo:
1. **Mesh WireGuard** anclado en `organic-machine.com` — sin abrir puertos en los devices.
2. **Matrix E2EE** como bus de control y chat — un room por device/container.
3. **Capability manifest firmado** ed25519 — el device rechaza lo que no este firmado.
## Pre-requisitos
- VPS `organic-machine.com` con root SSH (alias `vps` en `~/.ssh/config`).
- `agents_and_robots` y `agents_dashboard` desplegados (ya OK).
- `pass` con clave operador ed25519 (`pass insert operator/ed25519` — crear si falta).
- `apt-get install wireguard wireguard-tools` permitido en el VPS.
- Devices Linux/WSL: sudo sin password para `wg`, `wg-quick`, `systemctl`.
- Devices Android: Termux + WireGuard app + `pkg install golang openssh-client`.
## Funciones del registry recomendadas
| Rol | Funcion candidata | Estado |
|---|---|---|
| WG install (host) | `wg_install_bash_infra` | FALTA: crear |
| WG keygen | `wg_keygen_go_infra` | FALTA: crear |
| WG hub setup | `wg_hub_setup_bash_infra` | FALTA: crear |
| WG peer add (hub) | `wg_peer_add_go_infra` | FALTA: crear |
| WG peer remove (hub) | `wg_peer_remove_go_infra` | FALTA: crear |
| WG peer revoke (kill switch) | `wg_peer_revoke_go_infra` | FALTA: crear |
| WG client config gen | `wg_client_config_go_infra` | FALTA: crear |
| WG client install (device) | `wg_client_install_bash_infra` | FALTA: crear |
| WG status (parse `wg show`) | `wg_status_bash_infra` | FALTA: crear |
| Docker list (host) | `docker_container_list_go_infra` | FALTA: crear |
| Docker exec capability | `docker_container_exec_go_infra` | FALTA: crear |
| Docker logs tail | `docker_container_logs_go_infra` | FALTA: crear |
| Docker container enroll | `docker_container_enroll_go_infra` | FALTA: crear |
| Capability sign | `capability_manifest_sign_go_infra` | FALTA: crear |
| Capability verify | `capability_manifest_verify_go_infra` | FALTA: crear |
| Enrollment token gen | `enrollment_token_create_go_infra` | FALTA: crear |
| Enrollment token verify | `enrollment_token_verify_go_infra` | FALTA: crear |
| Matrix room per device | `matrix_room_for_device_py_browser` (extender) | OK base, EXTENDER |
| Provision hub pipeline | `provision_wg_hub_bash_pipelines` | FALTA: crear |
| Enroll device pipeline | `enroll_device_bash_pipelines` | FALTA: crear |
| Sink audit log | `device_audit_append_go_infra` | FALTA: crear |
| Notify approval | `matrix_send_message_py_browser` (existente) | OK |
## Apps tocadas
- `agents_dashboard` (cockpit ImGui) — panel "Mesh" + "Devices" + "Containers" + approval queue.
- `agents_and_robots` (hub Matrix VPS) — listener Matrix por device/container.
- `wg_hub` (nuevo service Go en VPS) — enrollment endpoint, peer CRUD, SSE stream.
- `device_agent` (nuevo binario per-host) — capability dispatcher con sandbox.
- `container_agent_sidecar` (opcional, nuevo) — sidecar para containers que necesitan WG-peer propio.
## Projects relacionados
- `element_agents` (parent project — agents Matrix).
## Vaults / storage
- `apps/wg_hub/operations.db` — tabla `wg_peers`, `wg_enrollment_tokens`, `device_audit`.
- `apps/agents_dashboard/local_files/agents_dashboard.db` — cache devices + capabilities.
- `pass operator/ed25519` — clave maestra del operador (firma manifests).
- `pass wg/preshared/<device_id>` — PSK por peer.
## Capability groups consultados
- `wireguard` (nuevo, ver `docs/capabilities/wireguard.md`).
- `device-agent` (nuevo, capability dispatcher + sandbox + audit).
- `docker-agent` (nuevo, capabilities sobre containers locales).
## Flow
### Fase A — registry primero (delegar a fn-constructor en paralelo)
1. `function: wg_install_bash_infra` (delegada).
2. `function: wg_keygen_go_infra` (delegada).
3. `function: wg_hub_setup_bash_infra` (delegada).
4. `function: wg_peer_add_go_infra` (delegada).
5. `function: wg_peer_remove_go_infra` (delegada).
6. `function: wg_peer_revoke_go_infra` (delegada).
7. `function: wg_client_config_go_infra` (delegada).
8. `function: wg_client_install_bash_infra` (delegada).
9. `function: wg_status_bash_infra` (delegada).
10. `cmd: ./fn index` — registra las 9 nuevas.
11. `cmd: fn doctor unused | grep wg_` — confirma que estan listas y no huerfanas (se usan en pasos C).
### Fase C — POC manual end-to-end
12. `function: wg_install_bash_infra` (sobre `organic-machine.com` via SSH).
13. `function: wg_keygen_go_infra` → key par hub.
14. `function: wg_hub_setup_bash_infra` — wg0, 10.42.0.1/24, ufw 51820/udp, persistencia.
15. `function: wg_keygen_go_infra` → key par device `home-wsl`.
16. `function: wg_peer_add_go_infra` (en hub) → asigna 10.42.0.10.
17. `function: wg_client_config_go_infra` → genera client.conf.
18. `function: wg_client_install_bash_infra` (en `home-wsl`).
19. `cmd: ping -c3 10.42.0.1` desde `home-wsl` — verifica handshake.
20. `cmd: curl http://10.42.0.1:8080/healthz` — agents_and_robots accesible por IP privada.
21. Repetir 15-19 para `pc-aurgi`.
### Fase B — spec + capability manifest + bot Matrix
22. Issue 0134 spec protocol: envelope JSON `{request_id, capability, args, signature, nonce}`,
error model, approval flow, audit chain hash.
23. `function: capability_manifest_sign_go_infra` (operator firma).
24. `function: capability_manifest_verify_go_infra` (device verifica antes de aceptar request).
25. `function: enrollment_token_create_go_infra` (token QR firmado, TTL 10min).
26. `function: enrollment_token_verify_go_infra` (hub valida en `/enroll`).
27. Implementar `apps/device_agent/` (Go cross-compile) — Matrix client + capability dispatcher + sandbox firejail.
28. Panel "Devices" en `agents_dashboard` — lista + capability matrix + approval queue + boton revoke.
29. Bot Matrix por device: cuando hablas en el room `#dev-aurgi:organic-machine.com`,
`agents_and_robots` parsea, valida capability, despacha a device_agent, devuelve resultado al room.
### Fase D — agentes-contenedores docker
30. `function: docker_container_list_go_infra` — corre en host con docker socket access.
31. `function: docker_container_exec_go_infra` — exec en container con whitelist binarios.
32. `function: docker_container_logs_go_infra` — tail logs SSE.
33. Modo "light": container expuesto via host's `device_agent` capability `docker.*`.
Element room: `#host-aurgi:organic-machine.com` con comando `!docker exec mycontainer ps`.
34. Modo "deep": container = peer WG propio. `container_agent_sidecar` corre WG dentro del container
(privileged) o sidecar gluetun-wg. Manifest firmado mapea `agent_X` → container_id.
35. Sub-bot Matrix por container: `#cont-mycontainer:organic-machine.com` (opcional, modo deep).
## Acceptance
- [ ] 9 funciones `wg_*` creadas + indexadas + sin huerfanas.
- [ ] Hub WG corriendo en `organic-machine.com`, `wg show` muestra interface wg0.
- [ ] `home-wsl` y `pc-aurgi` con IP estable 10.42.0.10/11, `ping` OK.
- [ ] `agents_and_robots` accesible solo desde subnet 10.42.0.0/24 (publico = DROP en :8080).
- [ ] `agents_dashboard` panel "Mesh" muestra peers vivos via SSE.
- [ ] Chat en `#dev-aurgi` ejecuta capability (ej. `!ls /home/lucas`) y devuelve resultado.
- [ ] Capability fuera del manifest rechazada con error en room.
- [ ] Capability `requires_approval=true` espera confirmacion en `#operator-approvals` antes de ejecutar.
- [ ] `docker.container.list` invocado desde Element devuelve containers del host.
- [ ] `docker.container.exec` con binario fuera de whitelist rechazado.
- [ ] Revoke device desde `agents_dashboard` → device pierde acceso en <5s.
- [ ] Audit log append-only inviolable (hash chain) sobrevive reinicio.
## Definition of Done
Triada obligatoria (ver `.claude/rules/dod_quality.md`). Sin las 3 capas + 0 anti-criterios el flow NO se mueve a `completed/`.
### Mecanica (pre-requisito)
- [ ] **Build device_agent**: `cd apps/device_agent && CGO_ENABLED=0 GOOS=linux go build -o device_agent .` exit 0; cross-compile `GOOS=windows` + `GOOS=android GOARCH=arm64` tambien verdes.
- [ ] **Build agents_and_robots + agents_dashboard**: `./fn run redeploy_cpp_app_windows agents_dashboard apps/agents_dashboard --build` + Go build de `agents_and_robots` exit 0.
- [ ] **Tests unitarios funciones nuevas verdes**: `CGO_ENABLED=1 go test -tags fts5 -count=1 ./functions/infra/...` cubriendo wg_*, capability_*, enrollment_*, device_audit_*. Lista de IDs en `## Notas`.
- [ ] **`./fn index`** sin warnings nuevos tras anadir las ~20 funciones.
- [ ] **`./fn doctor unused --json | jq '.[]|select(.id|startswith("wg_"))'`** vacio (las wg_* tienen consumidores reales).
- [ ] **`./fn doctor uses-functions`** verde para `apps/device_agent/app.md`, `apps/wg_hub/app.md`, `apps/agents_dashboard/app.md`.
- [ ] **`./fn doctor services-spec`** verde para `wg_hub.service` y `device_agent.service` con bloque service: completo.
### Cobertura de comportamiento
Minimo: golden + 8 edge/error documentados aqui con assert ejecutable. Cada uno deja entry en `e2e_runs` de la app afectada (`apps/device_agent/operations.db`, `apps/wg_hub/operations.db`).
| Escenario | Tipo | Comando / evidencia | Resultado esperado |
|---|---|---|---|
| Golden: comando whitelist OK | e2e | Element `!exec ls /home/lucas` en `#dev-home-wsl` | output `ls` en <3s, entry en `device_audit` con hash valido |
| Edge: comando NO whitelist rechazado | e2e | Element `!exec rm -rf /` | reply `capability rejected: shell.exec.rm not in manifest`; entry `device_audit` status=`rejected_capability` |
| Edge: capability fuera de manifest | e2e | Element `!camera.snapshot` en device sin esa capability | reply `capability not in manifest`; alerta a `#operator-approvals` |
| Edge: replay nonce viejo | e2e | reenviar mismo envelope con nonce ya visto (cmd test: `device_agent --replay-test <envelope.json>`) | rechazo + log `nonce_replay`; entry `device_audit` status=`rejected_nonce` |
| Edge: ed25519 manifest invalido | e2e | servir manifest firmado por clave que no es operator; `device_agent` lo recibe en enrollment | `device_agent` rechaza + no instala wg_peer; hub log muestra `manifest_invalid_signature` |
| Edge: token enrollment expirado | e2e | `enrollment_token_create` con TTL=1s, esperar 5s, `POST /enroll` | hub responde 401 `token_expired`; cmd `curl ...` exit != 0 |
| Approval flow honrado | e2e | Element `!fs.write /tmp/x hello` (requires_approval=true); operador hace 👍 en `#operator-approvals` | exec ocurre SOLO tras approval; sin approval no escribe; entry `device_audit` con `approval_msg_id` |
| Approval flow no se salta | e2e | Forzar via API directa salto del approval queue (test negativo: cmd `curl --data ...` directo al device) | device rechaza + log; sin approval_msg_id en envelope = rechazo |
| Mesh-down handled | e2e | `wg-quick down wg0` en hub mientras device manda comando | device entra en `degraded`, comando encolado o respuesta `mesh_unreachable`; al volver hub: handshake reanuda, cola se vacia |
| Dos devices simultaneos sin interferencia | e2e | `home-wsl` y `pc-aurgi` ejecutan capabilities en paralelo (script python con 2 threads) | cada audit chain es independiente, sin cross-contamination; `device_audit` muestra 2 chains separadas, hash chain valido en cada una |
| Audit chain valida tras restart | e2e | matar `device_agent` mid-flight (`kill -9`) + relanzar; `cmd: device_audit_verify_chain --device home-wsl` | chain integra, hash anterior coincide, sin huecos |
| Revoke device <5s | e2e | desde `agents_dashboard` panel "Mesh" boton "Revoke home-wsl"; medir tiempo hasta `wg show` no liste peer | peer ausente en <5s; siguientes comandos a `#dev-home-wsl` -> `peer_revoked` |
**Regla**: cada fila genera `e2e_check` en `app.md` correspondiente (issue 0068). `fn-analizador` los corre periodicamente.
### Vida util validada
| Metrica | Umbral | Donde se observa | Ventana |
|---|---|---|---|
| Peers vivos en mesh | `>=2` constantes (home-wsl + pc-aurgi) | `agents_dashboard` panel "Mesh" (last_handshake < 3min) | 7 dias |
| Crashes `device_agent` | `0` | `journalctl --user -u device_agent.service` en cada device | 7 dias |
| Crashes `wg_hub` | `0` | `ssh vps journalctl -u wg_hub.service` | 7 dias |
| Huecos en audit chain | `0` | `cmd: device_audit_verify_chain --all` | continuo |
| Rollback de wg config | `0 ocurrencias` | hub: `git -C /etc/wireguard status` debe ser clean; sin restore manual | 7 dias |
| Handshake fail rate | `<5%` | `wg show all dump` parseado por `agents_dashboard` | 7 dias |
| Approval queue stuck | `0 pendientes >24h` | `agents_dashboard` panel "Approvals" | continuo |
| Comandos exec latencia p95 | `<3s` | `call_monitor.function_stats` para `capability.shell.exec` | 7 dias |
| Replay attacks bloqueados | `>=1 detectado y bloqueado` (pen-test real) | `device_audit` status=`rejected_nonce` count | 30 dias |
### User-facing (reforzado)
- [ ] **User-facing surface**: humano abre Element en movil/web (`element.organic-machine.com`), entra a `#dev-<nombre>` y escribe comandos. Output en el mismo room. NO en una BD, NO en un log.
- [ ] **User-facing usage real**: el operador (humano) usa Element con `home-wsl` Y `pc-aurgi` (>=2 maquinas reales), **>=1 sesion/dia durante >=7 dias consecutivos**, **>=20 comandos totales** repartidos entre devices.
- [ ] **User-facing variado**: cubre capabilities de **>=4 tipos**: read (`!fs.read`, `!ls`), write (`!fs.write`), exec (`!exec`), approval-required (`!fs.write` en path sensible), docker (`!docker exec`).
- [ ] **User-facing onboarding**: parrafo en `## Notas` con pasos numerados: abrir Element -> entrar a room -> `!help` -> ejemplo de comando. Sin leer el flow entero.
- [ ] **User-facing latencia**: tras enviar mensaje en Element, output visible en <3s (read/exec) o <5s (con approval) — medido y registrado en `## Notas`.
### Anti-criterios (invalidan DoD aunque checkboxes verdes)
- [ ] **Solo-en-home-wsl**: el flow funciona en mi WSL pero falla en `pc-aurgi` u otro device fisico.
- [ ] **device_agent muere cada noche**: cualquier crash recurrente del proceso device_agent en los 7 dias de validacion.
- [ ] **Approval flow se salta**: alguna entrada en `device_audit` con capability `requires_approval=true` ejecutada sin `approval_msg_id` valido.
- [ ] **Audit chain rota**: `device_audit_verify_chain` reporta huecos o hash mismatch en algun device.
- [ ] **wg config drift**: cambios manuales en `/etc/wireguard/wg0.conf` del hub sin pasar por `wg_peer_add/remove/revoke`. Git status muestra cambios sin trackear.
- [ ] **Dashboard fantasma**: `agents_dashboard` declarado pero el operador no lo abre durante la ventana de 7 dias. Telemetria muerta.
- [ ] **Pen-test no ejercitado**: replay attack / capability fuera de manifest / token expirado declarados pero sin entry real en `device_audit` con status `rejected_*` en los 7 dias.
- [ ] **Silent-fail**: peer cae >24h y nadie se entera (sin alerta a `#operator-approvals` ni badge rojo en dashboard).
- [ ] **Secrets en repo**: cualquier hit de `git grep -E 'PrivateKey|PSK|operator/ed25519' -- ':!*.md'` en cualquier rama.
### Custom (security-specific, deben tener evidencia en `device_audit`)
- [ ] _(custom)_ Pen-test capability fuera de manifest: entry `device_audit` status=`rejected_capability` ejercitado intencionalmente >=1 vez.
- [ ] _(custom)_ Pen-test replay: entry `device_audit` status=`rejected_nonce` ejercitado >=1 vez con cmd reproducible.
- [ ] _(custom)_ Stale device: forzar `home-wsl` offline >24h, verificar badge `stale` en `agents_dashboard` + mensaje en `#operator-approvals`.
- [ ] _(custom)_ Operator key rotation: ejecutar rollover de la clave ed25519 maestra + revoke-all + re-enroll, sin perder audit chain historica. Documentado en `## Notas`.
## Telemetria esperada
- `call_monitor.calls`: cada `wg_*`, `capability.*`, `docker.*` con duration_ms, success.
- `apps/wg_hub/operations.db`: tabla `wg_peers` + `device_audit` (hash-chained append-only).
- `apps/agents_and_robots/operations.db`: tabla `matrix_capability_dispatches`.
- `apps/agents_dashboard/local_files/agents_dashboard.db`: cache devices + approval queue.
- Dashboards visibles: `agents_dashboard` panel "Mesh" (peers vivos + last handshake + bytes rx/tx).
- Matrix room `#operator-approvals` recibe cada approval_request.
- Element en movil aprueba/rechaza con reacciones (👍/👎) o comando `!approve <id>`.
## Riesgos / gotchas
- **VPS UDP/51820**: firewall del proveedor del VPS puede bloquearlo. Verificar con `nc -u -v vps 51820`.
- **NAT carrier-grade (4G/5G)**: device tras NAT estricto → `PersistentKeepalive = 25` obligatorio.
- **Sleep laptop / android doze**: handshake muere. Auto-reconnect via `systemd-networkd-wait-online` + script.
- **Privilegio sudo**: `wg-quick` requiere root. Devices necesitan sudo-NOPASSWD para `wg-quick@wg0`.
- **Clock skew**: tokens enrollment + nonces dependen de NTP. Forzar `chrony` en VPS y devices.
- **Container privileged**: modo "deep" docker requiere `--cap-add NET_ADMIN`. Riesgo si container compromised.
Mitigacion: solo modo "deep" para containers de tu propio control (ej. `agents_and_robots` self-hosted), no third-party.
- **Operator key compromise**: si tu ed25519 leaks → cualquiera firma manifests. Plan B: rotacion + revoke-all + re-enroll.
- **Matrix homeserver compromise**: chat E2EE protege contenido, pero metadata (quien habla con quien) leak.
Aceptable porque homeserver es tuyo en `organic-machine.com`.
## Notas
(rellenar tras ejecutar fases A/C/B/D)
### Para hablar con un device desde Element (onboarding)
1. Abre Element en movil o web (`element.organic-machine.com`).
2. Entra al room `#dev-<nombre>` (un room por device).
3. Escribe `!help` → bot del room (`agents_and_robots`) responde con capability matrix del device.
4. Escribe comando, ej. `!exec ls /home/lucas` o `!fs.read /var/log/syslog`.
5. Si capability requiere approval, te llega notification a `#operator-approvals` → reaccionas 👍 → ejecuta.
6. Output aparece en el mismo room del device.
### Para hablar con un container docker
1. Si el host del container ya esta en la mesh: room `#dev-<host>` con `!docker exec <container> <cmd>`.
2. Modo deep: room dedicado `#cont-<container>` (solo containers enrolled).
+157
View File
@@ -0,0 +1,157 @@
---
name: matrix-client-pc
id: 0010
status: pending
created: 2026-05-24
updated: 2026-05-24
priority: high
risk: medium
related_issues: [0147, 0148, 0149, 0150, 0151, 0152, 0153, 0162, 0163]
related_flows: [0009, 0011]
apps: [matrix_client_pc]
projects: [element_agents]
vaults: []
capability_groups: [matrix-client, livekit-calls, e2ee, widgets]
trigger: manual
schedule: ""
expected_runtime_s: 0
tags: [matrix, element, wails, react, mantine, livekit, e2ee, widgets, agents]
---
## Goal
Cliente Matrix propio para PC (Win/Linux/macOS) construido con Wails (Go backend) + React+Mantine+`@fn_library` frontend. Replica capacidades actuales de Element Web (chat, E2EE, calls LiveKit) y se abre a mejoras propias: mini-webapps embebidas en conversaciones gestionadas por agentes del project `element_agents`, paneles especiales para llamadas, integracion directa con `agents_and_robots` + `agents_dashboard` + `device_agent` + futuro mesh WireGuard (flow 0009).
## Pre-requisitos
- Synapse + MAS + LiveKit funcionando en `organic-machine.com` (app `element_matrix_chat` ya desplegada, 5+ semanas uptime).
- `livekit-jwt` container vivo para generar tokens (ver `docker-compose.livekit.yml`).
- Sygnal push gateway (Synapse) — TBD si no existe, anadir container para push notifs PC + Android.
- Cuenta Matrix de test (`@dev-pc:matrix-af2f3d.organic-machine.com`) registrada via MAS.
- Go 1.22+ + Wails CLI v2 instalado (`go install github.com/wailsapp/wails/v2/cmd/wails@latest`).
- pnpm + Node 20+ (ya en el repo para `frontend/`).
## Funciones del registry recomendadas
| Rol | Funcion candidata | Estado |
|---|---|---|
| Matrix client init (Go) | `matrix_client_init_go_infra` | FALTA: wrapper sobre `mautrix-go` (login MAS OIDC, sync, store SQLite) |
| LiveKit token gen (Go) | `livekit_token_gen_go_infra` | FALTA: JWT con `livekit-server-sdk-go` |
| Matrix room subscribe SSE (Go) | `matrix_room_subscribe_go_infra` | FALTA: stream eventos Synapse -> frontend Wails via SSE/IPC |
| Matrix message send (Go) | `matrix_message_send_go_infra` | FALTA: text + markdown + reply + edit + reaction |
| Matrix E2EE bootstrap (Go) | `matrix_e2ee_bootstrap_go_infra` | FALTA: cross-signing keys, recovery passphrase |
| Matrix device verify (Go) | `matrix_device_verify_go_infra` | FALTA: SAS verification flow |
| LiveKit room hook (TS) | `livekit_room_ts_ui` | FALTA: hook React wrapper sobre `livekit-client` |
| Widget host iframe (TS) | `widget_host_ts_ui` | FALTA: iframe sandbox + postMessage Matrix Widget API v2 |
| Matrix timeline hook (TS) | `useMatrixTimeline_ts_ui` | FALTA: hook React con pagination, dedupe, optimistic UI |
| Markdown render (TS) | reuse existing `markdown_render_ts_ui` si existe, sino crear | check |
| HTTP client (Go) | `http_json_client_go_infra` | OK (reusar) |
| SQLite open (Go) | `sqlite_open_go_infra` | OK (reusar) |
| HTTP server SSE | `http_sse_server_go_infra` | OK (reusar) |
| Notify (impure) | `notify_desktop_go_infra` | FALTA: Win/Linux/mac notifications nativas |
## Apps tocadas
- `projects/element_agents/apps/matrix_client_pc` (nueva — Wails + React).
- `projects/element_agents/apps/element_matrix_chat` (backend ya activo; quiza anadir sygnal container).
- `projects/element_agents/apps/agents_and_robots` (consumidor — el cliente PC dialoga con agentes via rooms Matrix).
- `projects/element_agents/apps/agents_dashboard` (referencia UI — algunos paneles se reusan).
## Projects relacionados
- `element_agents` (root project — agrupa todo).
## Vaults / storage
- Local del PC: `~/.matrix_client_pc/store.db` (sync state + crypto store SQLite).
- Cache media: `~/.matrix_client_pc/media/`.
## Capability groups consultados
- `matrix-client` (a crear: documenta wrappers `mautrix-go`).
- `livekit-calls` (a crear: token gen + room join + UI calls).
- `e2ee` (a crear: bootstrap + verification + recovery).
- `widgets` (a crear: Matrix Widget API v2 host + sandbox + permisos).
## Flow
Pasos numerados. Cada paso = issue propio (ver `related_issues`).
1. **0147 — Scaffold Wails + login MAS.** Crear app `matrix_client_pc/` con Wails init, conectar a Synapse via MAS OIDC, mostrar perfil del usuario logueado. Persistencia tokens en `pass` o keychain del SO.
2. **0148 — Rooms list + timeline.** Sidebar con rooms (DMs + spaces + grupos), panel central timeline con pagination scroll-up, dedupe, optimistic UI. Reusar layout `AppShell` Mantine.
3. **0149 — Composer + interacciones.** Composer markdown, replies, edits, reactions, threads, upload media (imagenes, files, voice msg). Drag&drop. Slash commands placeholder.
4. **0150 — E2EE.** `mautrix-go` con crypto store SQLite. Cross-signing setup, recovery passphrase, SAS verification de devices, key backup. UI para verificar otros usuarios.
5. **0151 — Calls LiveKit.** Boton call en room -> token JWT desde Go backend -> join LiveKit room -> UI con tiles participantes, mute/cam/screen/hangup. 1:1 + grupales hasta 16 (limite actual del config).
6. **0152 — Mini-webapps embebidas.** Implementar Matrix Widget API v2: iframe sandbox + postMessage handshake + permisos (capabilities `m.always_on_screen`, `org.matrix.msc2762.send.event`, etc.). Lanzar webapps desde slash command `/widget <url>` o desde state event `m.widget`. Agentes pueden publicar widgets en su room (ej. dashboard de telemetria, formulario, kanban inline).
7. **0153 — Agent integration.** Paneles especiales para rooms operados por agentes de `agents_and_robots`: timeline + panel lateral con estado del agente (uptime, cola de tasks, last_error). Reusar SSE del `agents_dashboard`.
## Acceptance
- [ ] App Wails compila y arranca en Win+Linux con binario standalone.
- [ ] Login MAS OIDC completo, token persistido entre arranques.
- [ ] Sync incremental con Synapse funciona; reconexion automatica tras red caida.
- [ ] E2EE: enviar/recibir mensajes cifrados con otro cliente (Element Web o Android).
- [ ] Call 1:1 con video+audio funcional via LiveKit.
- [ ] Widget de prueba (HTML estatico servido por `agents_and_robots`) se carga en iframe sandbox y postMessage handshake completa.
## Definition of Done
### Mecanica (pre-requisito)
- `go build -tags wails` verde para Win + Linux.
- `pnpm build` frontend verde.
- `fn doctor cpp-apps` no aplica; `fn doctor services` confirma backend Matrix sano.
- `app.md` con `uses_functions` declarando todas las dependencias del registry.
### Cobertura de comportamiento
| Escenario | Tipo | Comando / evidencia | Resultado esperado |
|---|---|---|---|
| Golden: login + recibir mensaje E2EE | e2e | `e2e/test_login_and_receive.sh` | mensaje aparece en timeline en <2s, descifrado OK |
| Edge: red cae 30s, vuelve | e2e | `e2e/test_reconnect.sh` | sync se reanuda sin perder mensajes |
| Edge: 2000 mensajes en 1 room | e2e | `e2e/test_perf_timeline.sh` | scroll a 60fps, memoria <500MB |
| Edge: device nuevo no verificado envia msg | e2e | `e2e/test_unverified_device.sh` | warning visible en UI, msg cifra a este device solo si user confirma |
| Error: token MAS expira | e2e | `e2e/test_token_refresh.sh` | refresh automatico, sin logout visible |
| Error: LiveKit SFU caido | e2e | `e2e/test_livekit_down.sh` | error claro en UI, no crash de la app |
### Vida util validada (>=7 dias uso real)
| Metrica | Umbral | Donde se observa | Ventana |
|---|---|---|---|
| Crashes proceso PC | `0` | `journalctl --user -u matrix_client_pc` (Linux) / Event Viewer (Win) | 7 dias |
| Latencia send msg | `p95 < 500ms` | panel propio de la app + `call_monitor` | 7 dias |
| Calls fallidas | `< 5%` | counter en app + logs LiveKit | 7 dias |
| Uso real diario | `>= 4 dias/semana` | `last_active_at` en store local | 7 dias |
| Onboarding nuevo usuario | `< 5min hasta primer msg E2EE` | screencast operador | 1 sesion |
### Anti-criterios
- NO marcar done si E2EE se silent-falla (mensajes no se descifran y la UI no lo dice).
- NO marcar done si la app solo funciona en `home-wsl` y peta en `aurgi-pc`.
- NO marcar done si widget host carga `javascript:` URLs (XSS).
- NO marcar done si calls grupales >3 participantes lagean con audio cortado.
## Notas
**Onboarding rapido:**
1. `cd projects/element_agents/apps/matrix_client_pc`
2. `wails dev` para desarrollo con hot-reload.
3. `wails build -platform linux/amd64,windows/amd64` para release.
4. Tokens MAS guardados via `keyring` (Go bindings al keychain del SO).
5. Para probar E2EE: crear segundo usuario en Synapse Admin, abrir Element Web como segundo cliente, intercambiar verifications.
**Camino futuro (post-DoD):**
- Push notifs nativas via `sygnal` + APNs/FCM-equivalent desktop (Win Action Center, Linux notify-send).
- Mini-webapp catalog: registry de widgets internos (`projects/element_agents/widgets/`) publicables a rooms con un comando.
- Threads UI mejorado (vs Element que es plano).
- Integracion `agents_and_robots`: panel embebido que muestra logs del agente del room actual.
- Cuando flow 0009 (mesh wireguard) este vivo: este cliente PC habla con `device_agent` de cada PC del mesh via su room Matrix.
**Decisiones clave (justificacion en hilo Claude 2026-05-24):**
- Wails > Tauri: Go es stack principal del registry, reusa funciones existentes, `mautrix-go` es el SDK Matrix mas maduro en Go.
- React+Vite+Mantine+`@fn_library`: defaults del proyecto, ver `frontend_theming.md`.
- 2 codebases (PC Wails + Android Kotlin nativo): tradeoff aceptado por calidad nativa Android + reuso Go en PC. Contrato compartido en `docs/client_contract.md` (TBD).
## Capability growth log
- v0.1.0 (2026-05-24) — baseline (flow creado).
+165
View File
@@ -0,0 +1,165 @@
---
name: matrix-client-android
id: 0011
status: pending
created: 2026-05-24
updated: 2026-05-24
priority: high
risk: medium
related_issues: [0154, 0155, 0156, 0157, 0158, 0159, 0160, 0161, 0162, 0163]
related_flows: [0009, 0010]
apps: [matrix_client_android]
projects: [element_agents]
vaults: []
capability_groups: [matrix-client, livekit-calls, e2ee, widgets, android-native]
trigger: manual
schedule: ""
expected_runtime_s: 0
tags: [matrix, element, android, kotlin, compose, livekit, e2ee, widgets, agents, fcm, push]
---
## Goal
Cliente Matrix Android nativo (Kotlin + Jetpack Compose) que comparte contrato con el cliente PC (flow 0010) pero usa SDKs nativos para calidad superior: `matrix-rust-sdk` Kotlin bindings (E2EE rust, mejor), `livekit-android` (codecs HW, audio focus, AEC), FCM push directo via `sygnal`, foreground service para calls en background. Replica capacidades de Element Android + abre mini-webapps embebidas (Matrix Widget API v2 dentro de WebView) gestionadas por agentes del project `element_agents`.
## Pre-requisitos
- Stack Synapse + MAS + LiveKit ya activo en `organic-machine.com` (flow 0010 compartido).
- Container `sygnal` corriendo en VPS (anadir si no existe — issue 0159 lo cubre).
- Firebase project con FCM activado + service account JSON. Hosting gratuito.
- Android Studio Iguana+, NDK r26+, Kotlin 1.9+.
- `init_kotlin_app_bash_pipelines` (ya existe, ver issues 0073/0074/0075/0078 completados) para scaffold inicial.
- Device fisico o emulator Android 9+ (API 28+) para test.
- Capability del usuario operador: instalar APK debug + microphone/camera/notification grants.
## Funciones del registry recomendadas
| Rol | Funcion candidata | Estado |
|---|---|---|
| Kotlin app scaffold | `init_kotlin_app_bash_pipelines` | OK (reusar) |
| Matrix rust-sdk wrapper (Kotlin) | `matrix_client_kotlin_infra` | FALTA: facade sobre `matrix-rust-sdk` Kotlin bindings |
| LiveKit Android wrapper | `livekit_call_kotlin_infra` | FALTA: wrapper `io.livekit:livekit-android` |
| FCM token register | `fcm_register_kotlin_infra` | FALTA: registrar device en sygnal via Synapse pusher API |
| Sygnal pusher add | `sygnal_pusher_add_go_infra` | FALTA: Go helper para configurar push gateway |
| Compose Room list | `RoomListScreen_kotlin_ui` | FALTA |
| Compose Timeline | `TimelineScreen_kotlin_ui` | FALTA |
| Compose Composer | `Composer_kotlin_ui` | FALTA |
| Compose CallScreen | `CallScreen_kotlin_ui` | FALTA |
| Compose WidgetHost | `WidgetHost_kotlin_ui` | FALTA: WebView + JS bridge Widget API |
| Foreground service call | `CallForegroundService_kotlin_infra` | FALTA |
| ICE permissions helper | `permissions_request_kotlin_core` | FALTA: mic/cam/notif/foreground service grants |
| Local DB Room | reusar `androidx.room` directo | OK |
## Apps tocadas
- `projects/element_agents/apps/matrix_client_android` (nueva — Kotlin+Compose).
- `projects/element_agents/apps/element_matrix_chat` (anadir sygnal container — issue 0159).
- `projects/element_agents/apps/agents_and_robots` (consumidor agent panels).
## Projects relacionados
- `element_agents`.
## Vaults / storage
- Local Android: `/data/data/com.fnregistry.matrix_client_android/databases/` (room DB encriptada via SQLCipher).
- Crypto store de matrix-rust-sdk: gestionado por el SDK en `files/matrix/<userId>/`.
## Capability groups consultados
- `matrix-client` (compartido con flow 0010).
- `livekit-calls` (compartido).
- `e2ee` (compartido).
- `widgets` (compartido — contrato Widget API igual).
- `android-native` (a crear: foreground service, FCM, MediaSession para calls).
## Flow
1. **0154 — Scaffold Kotlin + Compose + login MAS.** App `matrix_client_android/` con `init_kotlin_app`, Material 3 + tema propio acorde a `frontend_theming.md` (paleta equivalente). Login MAS OIDC via Chrome Custom Tabs. Tokens persistidos en EncryptedSharedPreferences.
2. **0155 — Rooms list + Timeline.** Compose UI con `LazyColumn` virtualizado, sync via `matrix-rust-sdk` (corrutinas). Pagination, optimistic UI, swipe-to-react.
3. **0156 — Composer.** Markdown, replies, edits, reactions, media (camara + galeria + voice msg con `MediaRecorder` opus).
4. **0157 — E2EE rust-sdk.** Cross-signing setup, SAS verification (emoji), recovery passphrase, key backup. UI dialog verificacion.
5. **0158 — Calls LiveKit Android nativo.** `livekit-android` SDK con codecs HW (H.264/VP9 hardware decoder), audio focus, echo cancellation, noise suppression. PiP mode Android nativo.
6. **0159 — Push FCM via sygnal.** Anadir container `sygnal` al stack `element_matrix_chat`. Registrar FCM token via Synapse Pusher API. Handle push payload -> open room / wake up para incoming call.
7. **0160 — Mini-webapps en WebView.** `WebView` con `WebViewClient` + JS bridge implementando Matrix Widget API v2. Sandbox via `setAllowFileAccess(false)`, `setAllowContentAccess(false)`, CSP estricta. Mismo contrato widgets que cliente PC.
8. **0161 — Foreground service para calls + lifecycle.** `CallForegroundService` con notification ongoing, audio routing (speaker/earpiece/bluetooth), MediaSession para controls en lockscreen, wakelock controlado.
## Acceptance
- [ ] APK debug instala + arranca en Android 9+ (API 28).
- [ ] Login MAS via Chrome Custom Tabs, token persistido en EncryptedSharedPreferences.
- [ ] Sync incremental funciona; reconexion automatica tras avion mode toggle.
- [ ] E2EE: mensaje enviado desde PC (Wails) se descifra en Android (y al reves).
- [ ] Call 1:1 con video+audio nativos, calidad superior a WebView.
- [ ] Push FCM despierta app para incoming msg / call.
- [ ] Widget de prueba se carga en WebView sandbox con bridge funcional.
- [ ] Foreground service mantiene call viva con app en background + pantalla bloqueada.
## Definition of Done
### Mecanica (pre-requisito)
- `./gradlew assembleDebug` verde.
- `./gradlew test` verde.
- `./gradlew connectedAndroidTest` verde en emulator API 31+ (instrumented).
- `app.md` con `uses_functions` declarando dependencias del registry.
### Cobertura de comportamiento
| Escenario | Tipo | Comando / evidencia | Resultado esperado |
|---|---|---|---|
| Golden: login + E2EE msg | instrumented | `./gradlew connectedAndroidTest --tests *LoginE2EE*` | msg descifrado en <2s, shield green |
| Edge: avion mode 30s | instrumented | `./gradlew connectedAndroidTest --tests *Reconnect*` | sync resume, sin perder msgs |
| Edge: 1000 msgs en room | benchmark | `./gradlew :app:benchmark` | scroll a 60fps, RAM <300MB |
| Edge: incoming call, pantalla apagada | manual + screencast | apagar pantalla + recibir call desde PC | notif full-screen + ring, accept funciona |
| Error: FCM token rotation | instrumented | `./gradlew connectedAndroidTest --tests *FCMRotation*` | re-register automatico en sygnal |
| Error: WebView widget malicioso | instrumented | `./gradlew connectedAndroidTest --tests *WidgetSandbox*` | bloqueado, no escape |
| Battery: call 30min | manual + dumpsys batterystats | call 30min | drain <15%, sin OOM |
### Vida util validada (>=7 dias uso real)
| Metrica | Umbral | Donde se observa | Ventana |
|---|---|---|---|
| Crashes (ANRs/forced close) | `0` | `adb logcat -e FATAL` + Play Console (si publicado) | 7 dias |
| Push latency (msg enviado -> notif visible) | `p95 < 3s` | log custom en app + sygnal | 7 dias |
| Call drops in-pocket (lockscreen) | `< 5%` | counter app | 7 dias |
| Battery drain idle | `< 2%/h` | dumpsys batterystats | 7 dias |
| Uso real diario | `>= 5 dias/semana` | last_active en local DB | 7 dias |
### Anti-criterios
- NO marcar done si E2EE silent-falla.
- NO marcar done si call con pantalla bloqueada se corta a los <5min (battery optimization mata el service).
- NO marcar done si WebView de widget permite acceso a `file://` o cookies del browser host.
- NO marcar done si la app solo funciona en el device del operador y peta en Android < 11.
- NO marcar done sin probar en Android 9 (legacy, muchos dispositivos antiguos siguen vivos).
## Notas
**Onboarding rapido:**
1. `cd projects/element_agents/apps/matrix_client_android`
2. `./gradlew assembleDebug && adb install -r app/build/outputs/apk/debug/app-debug.apk`
3. Para hot-reload UI: `./gradlew :app:installDebug` + Android Studio Compose preview.
4. Para test push: enviar msg desde Element Web a la cuenta del Android; debe llegar notif via FCM en <3s.
**Decisiones clave:**
- `matrix-rust-sdk` Kotlin bindings > matrix-android-sdk2 (deprecated). Rust-sdk es el futuro oficial de matrix.org.
- `livekit-android` nativo > WebRTC.org directo. SDK oficial mantiene mejor performance + features.
- Jetpack Compose > XML views. Encaja mejor con reactive model + menos boilerplate.
- EncryptedSharedPreferences para tokens MAS. NO usar SharedPreferences plain.
- Material 3 con tema propio (paleta similar a Mantine accent del cliente PC para coherencia visual).
**Camino futuro (post-DoD):**
- Wear OS companion app (notifs + quick reply).
- Android Auto integration (read msgs voice + reply voice).
- Conversation shortcuts API (Android 11+) para que cada room aparezca en share sheet.
- Bubble notifications (Android 11+) para conversaciones favoritas.
**Compartido con flow 0010:**
- Contrato `m.widget` y Widget API v2 IDENTICO. Mismo widget html funciona en ambos.
- Contrato `m.agent.metadata` para detectar rooms de agentes IDENTICO.
- Cuando flow 0009 (mesh) este vivo, ambos clientes hablan a `device_agent` igual.
## Capability growth log
- v0.1.0 (2026-05-24) — baseline.
+1
View File
@@ -12,6 +12,7 @@ Tabla de casos de uso multi-app. Mantenida por `/flow create` y `/flow done`.
| [0006](0006-metabase-versioning.md) | metabase-versioning | gitops | auto_metabase, dag_engine | pending | medium | 0% | 2026-05-16 |
| [0007](0007-matrix-telemetry-bot.md) | matrix-telemetry-bot | event-driven | data_factory, dag_engine, call_monitor, agents_and_robots | pending | low | 0% | 2026-05-16 |
| [0008](0008-kanban-cpp-and-agent-workflows.md) | kanban-cpp-and-agent-workflows | realtime-loop | kanban_cpp, kanban, skill_tree, agent_runner_api | pending | medium | 0% | 2026-05-18 |
| [0009](0009-agentes-dispositivos-mesh.md) | agentes-dispositivos-mesh | event-driven | agents_dashboard, agents_and_robots, wg_hub, device_agent | pending | high | 0% | 2026-05-23 |
## Leyenda
+92 -25
View File
@@ -12,49 +12,116 @@ Un flow describe una secuencia de pasos que atraviesa varias apps (`navegator_da
- **Definition of Done OBLIGATORIA** — ver seccion abajo. Sin DoD el flow NO puede crearse.
- Cerrados se mueven a `completed/`.
## Definition of Done (OBLIGATORIA)
## Definition of Done (OBLIGATORIA — triada)
Cada flow al crearse DEBE declarar un bloque `## Definition of Done` distinto de `## Acceptance`. Sin el, `/flow create` rechaza el scaffold y `/flow done` rechaza el cierre.
**Diferencia:**
**Regla absoluta**: DoD no es checkbox que se marca a mano. Cada item lleva **evidencia ejecutable** (comando, e2e_check, dashboard URL con datos frescos, log query, screenshot link). Si no puedes probarlo, no es DoD: es deseo. Ver `.claude/rules/dod_quality.md` para la regla completa.
**Diferencia con `## Acceptance`:**
| `## Acceptance` | `## Definition of Done` |
|---|---|
| Checks task-level del flow (ejecucion concreta) | Contrato global de calidad para considerar el flow CERRADO |
| Pueden quedar `[ ]` mientras iteras | TODOS deben estar `[x]` antes de mover a `completed/` |
| Verifica que el flow CORRE | Verifica que el flow es REPETIBLE, OBSERVABLE y MANTENIBLE |
| Checks task-level del flow (el flow corre una vez) | Contrato global de calidad: el flow sobrevive uso real |
| Pueden quedar `[ ]` mientras iteras | TODAS las capas verdes con evidencia antes de mover a `completed/` |
| Verifica que el flow CORRE | Verifica que el flow es REPETIBLE, OBSERVABLE, MANTENIBLE y USADO |
**Plantilla minima de DoD** (anadir/ajustar segun flow):
### Las 3 capas obligatorias
**1. Mecanica** (pre-requisito, NO es DoD por si misma):
Build verde, tests verdes, `fn index` limpio, `fn doctor` verde, `uses_functions` sin drift. Hacer compilar la cosa NO es haberla terminado.
**2. Cobertura de comportamiento**:
Tabla `escenario | tipo | comando | resultado esperado`. Minimo 1 golden + 2 edge + 1 error path con assert real, no smoke "no peto". Cuando aplique, las pruebas dejan entry en `e2e_runs` de la app afectada.
**3. Vida util validada**:
Tabla `metrica | umbral | dashboard | ventana`. El flow sobrevive **>=7 dias de uso real** sin romperse silenciosamente. Crashes = 0, huecos en audit chains = 0, error_rate < umbral declarado, dashboard observable abierto periodicamente. **El humano usa la cosa en su PC, en su contexto real, >=N veces variadas, no en sandbox aislado**.
### Plantilla obligatoria
Ver `template.md` para el esqueleto completo. Bloques:
```markdown
## Definition of Done
- [ ] **Repetibilidad**: el flow corre N veces consecutivas (N declarado en el flow, default 3) sin intervencion manual.
- [ ] **Observabilidad**: queda trazado en `call_monitor.calls` + `data_factory.runs` + dashboard correspondiente.
- [ ] **Error-path**: al menos 1 modo de fallo probado y manejado (no crash silencioso).
- [ ] **Idempotencia**: re-ejecutar no duplica datos ni rompe estado (clave en sinks).
- [ ] **Secrets**: cero credenciales en disco fuera de `pass`/vaults; cero datos sensibles fuera de `risk` declarado.
- [ ] **Docs**: `## Notas` rellenado con hallazgos reales + comandos para reproducir.
- [ ] **Registry-first**: todas las piezas reutilizables existen como funciones del registry (no inline en apps).
- [ ] **INDEX + status**: `status: done` en frontmatter + fila actualizada en `INDEX.md` + archivo movido a `completed/`.
### Mecanica
- [ ] Build verde (`cmd: ...`)
- [ ] Tests verdes (`cmd: ...`)
- [ ] fn index limpio
- [ ] fn doctor verde
- [ ] uses_functions auditado
### Cobertura de comportamiento
| Escenario | Tipo | Comando | Resultado esperado |
|---|---|---|---|
| Golden: ... | unit/e2e | `cmd` | output concreto |
| Edge 1: ... | unit/e2e | `cmd` | comportamiento concreto |
| Error 1: ... | e2e | `cmd que rompe` | fallo manejado, no crash |
| Error 2: ... | e2e | `cmd` | degradacion graceful + log |
### Vida util validada
| Metrica | Umbral | Donde se observa | Ventana |
|---|---|---|---|
| <metrica> | `>=N` | `<dashboard URL>` | 7 dias |
| crashes | `0` | `journalctl ...` | 7 dias |
### User-facing (reforzado)
- [ ] User-facing surface (lugar concreto, NO BD ni log).
- [ ] User-facing usage real: >=N veces en >=7 dias, en PC real, con inputs reales.
- [ ] User-facing variado: >=3 capabilities/casos distintos.
- [ ] User-facing onboarding (parrafo en `## Notas`).
- [ ] User-facing latencia <X medida.
### Anti-criterios (invalidan DoD aunque checkboxes verdes)
- [ ] solo-en-mi-PC
- [ ] solo-en-sandbox-vacio
- [ ] camino feliz unico (error paths declarados pero nunca ejercitados)
- [ ] dashboard fantasma (no abierto en >30 dias)
- [ ] self-test sin asserts
- [ ] silent-fail
- [ ] approval saltado
```
Cada flow puede anadir DoD especificos al dominio (ej. `bbva-movimientos`: "datos NUNCA cruzan a registry.organic-machine"). El bloque DoD se **versiona con el flow** — un cambio de DoD requiere bump de `updated:` en frontmatter.
### Reglas duras para marcar `status: done`
### User-facing surface (sub-bloque OBLIGATORIO dentro de DoD)
`/flow done` rechaza el cierre si:
"DoD verde" sin valor visible al humano = plumbing limpio sin razon de existir. Cada DoD DEBE incluir, al menos, estos cuatro checks tipo `User-facing`:
1. Falta alguna de las 3 capas (mecanica + cobertura + vida).
2. En Cobertura: <1 golden, <2 edge, <1 error path con evidencia.
3. En Vida util: tabla vacia o sin dashboard observable real.
4. User-facing usage real <7 dias o <N usos declarados.
5. Cualquier anti-criterio marcado como cierto.
6. `## Notas` sin parrafo onboarding.
7. Algun item sin comando/URL/log query — solo texto.
```markdown
- [ ] **User-facing**: <accion concreta del humano + lugar exacto donde ve/usa el output>.
- [ ] **User-facing repeat**: el humano vuelve manana al mismo lugar y ve datos frescos sin conocer el flow.
- [ ] **User-facing onboarding**: parrafo en `## Notas` que explica "para ver/usar esto: hacer X" — sin leer el flow.
- [ ] **User-facing latencia**: el humano percibe el cambio en <X segundos|minutos> tras el evento (X declarado por flow).
```
Cada flow puede anadir DoD especificos al dominio. El bloque DoD se **versiona con el flow** — un cambio de DoD requiere bump de `updated:` en frontmatter.
Regla: si la respuesta a "donde lo ves" es "en una BD" o "en un log" -> NO vale. Tiene que ser una superficie usada por el humano (UI de una app, sala Matrix, dashboard, Metabase card, repo Gitea, archivo en vault abierto a mano). Si el output solo lo consume otra app/flow, esa app/flow es quien debe declarar su propia user-facing surface.
### User-facing surface (regla complementaria)
`/flow done` rechaza el cierre si falta alguno de los 4 user-facing checks o si `## Notas` no contiene parrafo onboarding.
Si la respuesta a "donde lo ves" es "en una BD" o "en un log" -> NO vale. Tiene que ser una superficie usada por el humano (UI de app, sala Matrix, dashboard, Metabase card, repo Gitea, archivo en vault abierto a mano). Si el output solo lo consume otra app/flow, esa app/flow declara su propia user-facing surface.
### Antipatrones documentados
| Antipatron | Por que es malo |
|---|---|
| Marcar `done` porque pasa una vez | Tarea "hecha" se rompe al primer uso real |
| Checkbox sin evidencia ejecutable | DoD se convierte en placebo, no en gate |
| Test que solo verifica camino feliz | El error path es donde se pierden datos en produccion |
| Observabilidad declarada pero dashboard no abierto en 30 dias | Telemetria muerta = ceguera |
| "Repetible 3 veces consecutivas" con BD efimera | No prueba comportamiento sobre datos reales acumulados |
| Aprobacion saltada en algun camino | Security gate roto pero invisible |
| Error path manejado solo "en teoria" | Cuando ocurra en produccion el manejo no existe |
### Validacion programatica de DoD (TBD)
`/flow done` ejecuta checks programaticos:
- Parsea bloques `### Mecanica`, `### Cobertura`, `### Vida util`, `### User-facing`, `### Anti-criterios`.
- Verifica que cada item tiene `cmd:` / URL / log query / e2e_check_id asociado.
- Cuenta filas en Cobertura: >=1 golden + >=2 edge + >=1 error.
- Cruza con `e2e_runs` y `call_monitor.calls` para confirmar evidencias en BDs reales.
- Aborta cierre si falta cobertura o algun anti-criterio esta marcado.
Hoy parte de esto es manual (revision humana). Ver `audit_dod_schema_go_infra` (issue 0114) + `fn doctor dod`.
### DoD evidence schema (issue 0114, opcional)
+60 -14
View File
@@ -68,28 +68,74 @@ Pasos numerados. Cada paso puede ser:
## Acceptance
Checks task-level del flow — verifican que el flow CORRE una vez. Pueden quedar `[ ]` mientras iteras. NO sustituyen a la DoD.
- [ ] Criterio 1.
- [ ] Criterio 2.
## Definition of Done
Contrato global de cierre. TODOS marcados antes de mover a `completed/`. Ver README.md seccion "Definition of Done".
**Filosofia triada (ver `.claude/rules/dod_quality.md`):** DoD no es checkbox que se marca a mano. Cada item debe llevar **evidencia ejecutable** (comando, e2e_check, screenshot link, dashboard URL con datos frescos, log query). Si no puedes probarlo, no es DoD: es deseo. Las 3 capas son obligatorias.
- [ ] **Repetibilidad**: corre 3 veces consecutivas sin intervencion manual.
- [ ] **Observabilidad**: trazado en `call_monitor.calls` + `data_factory.runs` + dashboard relevante.
- [ ] **Error-path**: >=1 modo de fallo probado y manejado.
- [ ] **Idempotencia**: re-ejecucion no duplica ni corrompe sinks.
- [ ] **Secrets**: cero credenciales fuera de `pass`/vaults; risk declarado coincide con datos reales.
- [ ] **Docs**: `## Notas` con hallazgos + comandos reproducibles.
- [ ] **Registry-first**: piezas reutilizables viven como funciones del registry.
- [ ] **INDEX + status**: `status: done` + `INDEX.md` actualizado + movido a `completed/`.
### Mecanica (pre-requisito, NO sustituye al resto)
### User-facing (obligatorio)
Construir verde no es estar hecho. Es la base para empezar a probar.
- [ ] **User-facing**: <accion concreta del humano + lugar exacto donde ve/usa el output>.
- [ ] **User-facing repeat**: humano vuelve manana al mismo lugar, ve datos frescos sin conocer el flow.
- [ ] **User-facing onboarding**: parrafo en `## Notas` explica "para ver/usar esto: hacer X" sin leer el flow.
- [ ] **User-facing latencia**: humano percibe el cambio en <Xs|Xmin> tras el evento (X declarado).
- [ ] **Build verde** (`cmd: <comando build>`).
- [ ] **Tests unitarios verdes** (`cmd: <comando test>`, listar IDs/paths).
- [ ] **`fn index` limpio** (sin warnings nuevos).
- [ ] **`fn doctor` verde** en artefactos tocados (`cmd: ./fn doctor --json | jq ...`).
- [ ] **`uses_functions` auditado** (sin drift en `app.md` vs imports reales).
### Cobertura de comportamiento
Cada escenario debe tener una prueba ejecutable con assert real (no smoke "no peto"). Anadir tantas filas como casos relevantes — golden path + edge cases + error paths.
| Escenario | Tipo de prueba | Comando / evidencia | Resultado esperado |
|---|---|---|---|
| Golden path: <descripcion> | unit / e2e / manual | `<cmd>` | <output concreto, no "ok"> |
| Edge case 1: <input limite> | unit / e2e | `<cmd>` | <comportamiento concreto> |
| Edge case 2: <estado raro> | unit / e2e | `<cmd>` | <comportamiento concreto> |
| Error path 1: <fallo esperado> | e2e | `<cmd que provoca fallo>` | <fallo manejado, no crash> |
| Error path 2: <recursos caidos> | e2e | `<cmd>` | <degradacion graceful + log> |
**Regla**: al menos 1 golden + 2 edge + 1 error path. Tests inscritos en `e2e_runs` de la app correspondiente cuando aplique.
### Vida util validada
El flow no esta hecho hasta que sobrevive **uso real** durante >=7 dias sin romperse silenciosamente. Cada metrica tiene umbral medible y dashboard observable.
| Metrica | Umbral | Donde se observa | Ventana |
|---|---|---|---|
| <metrica 1, ej. handshakes vivos> | `>=N` | `<dashboard URL / app panel>` | 7 dias |
| <metrica 2, ej. error_rate> | `<X%` | `<dashboard URL>` | 7 dias |
| <metrica 3, ej. duracion p95> | `<Xms` | `call_monitor.function_stats` | 30 dias |
| crashes del proceso | `0` | `journalctl -u <unit>` | 7 dias |
| huecos en audit chain | `0` | `cmd: <verify chain>` | continuo |
**Regla**: las metricas NO se autoreportan en el flow; las lee el operador del dashboard real. Si el dashboard no existe, el item se invalida.
### User-facing (reforzado)
"DoD verde" sin uso humano real = plumbing limpio sin razon de existir.
- [ ] **User-facing surface**: <accion concreta del humano + lugar exacto donde ve/usa el output (NO una BD, NO un log)>.
- [ ] **User-facing usage real**: el humano (operador) usa la cosa en **su PC, en su contexto real**, **>=N veces en >=7 dias**, con inputs reales (no demo, no sandbox).
- [ ] **User-facing variado**: cubre >=3 capabilities/casos distintos (no solo "abre dashboard y mira").
- [ ] **User-facing onboarding**: parrafo en `## Notas` que explica "para ver/usar esto: hacer X" sin leer el flow.
- [ ] **User-facing latencia**: percepcion del cambio <Xs|Xmin> tras el evento (X declarado, medido).
### Anti-criterios (invalidan la DoD aunque los checkboxes esten verdes)
Marca el flow como **NO done** si cualquiera de estas condiciones es cierta:
- [ ] **Solo-en-mi-PC**: el flow funciona en `home-wsl` pero falla en `pc-aurgi` u otro PC del operador.
- [ ] **Repetible-en-sandbox-vacio**: solo pasa con BD limpia / cuenta limpia / sin datos historicos.
- [ ] **Camino feliz unico**: los error paths fueron declarados pero NUNCA se ejercitaron (sin entry en `e2e_runs` o logs reales).
- [ ] **Dashboard fantasma**: el dashboard declarado en "Vida util" no se ha abierto en >30 dias.
- [ ] **Self-test que no apesta**: el `e2e_check` retorna `pass` sin verificar nada material (no asserts).
- [ ] **Silent-fail**: el proceso muere/degrada sin alerta visible al operador.
- [ ] **Approval saltado**: alguna capability con `requires_approval=true` fue ejecutada sin el approval flow.
### Custom (opcional, dominio-especifico)
+121
View File
@@ -0,0 +1,121 @@
---
id: "0130"
title: Kanban C++ v2 — gestor de dev/issues y dev/flows con backend Go + frontend ImGui
status: pendiente
type: epic
domain:
- cpp-stack
- apps-infra
- dev-ux
scope: multi-app
priority: alta
depends: []
blocks: []
related:
- "0112"
- "0119"
tags:
- kanban
- cpp
- imgui
- dev_ux
- issues
- flows
created: "2026-05-22"
updated: "2026-05-22"
---
# 0130 — Kanban C++ v2
**Status:** pendiente
## Por que
La v1 (`apps/kanban_cpp` borrada el 2026-05-22) mezclaba paneles ajenos al dominio kanban (agent runs, DoD, worktrees, calendar) y un backend que no era reutilizable. Para gestionar los 98 issues activos + 12 flows del proyecto necesitamos una vista board nativa, sin web, con edicion bidireccional de los archivos markdown.
## Que entrega
App kanban_cpp v2 con dos piezas:
1. **Backend Go** (`apps/kanban_cpp/backend/`) — service HTTP en puerto 8487.
- Parser bidireccional MD <-> SQLite (cache).
- Watcher fsnotify sobre `dev/issues/` (+ `completed/`) y `dev/flows/`.
- Endpoints REST: `/api/issues`, `/api/issues/{id}` (GET/PATCH), `/api/flows`, `/api/flows/{id}`, `/api/meta`, `/api/sse`.
- PATCH a issue reescribe el frontmatter en disco preservando body + orden de campos.
2. **Frontend C++ ImGui** (`apps/kanban_cpp/`) sobre el framework `fn::run_app`.
- Panel **Board**: columnas por status (pendiente / in-progress / bloqueado / completado). Drag-drop = PATCH status.
- Panel **Flows**: lista de flows con detalle.
- Panel **Filtros** (Aside): multi-select domain, scope, priority, tags.
- Panel **Detalle**: edicion de campos frontmatter de un issue (status, priority, scope, tags, depends, blocks).
- SSE para refrescar tras cambios externos en disco.
## Sub-issues
- **0130a** — parser MD + scan dirs (funciones registry).
- **0130b** — backend Go: schema + handlers + watcher + SSE.
- **0130c** — frontend C++: paneles + http client.
Cada sub-issue mergeable independiente en su rama corta TBD.
## Reusa del registry
Backend Go:
- `sqlite_open_go_infra`, `sqlite_apply_migrations_go_infra`
- `http_router_go_infra`, `http_serve_go_infra`, `http_middleware_chain_go_infra`
- `http_cors_middleware_go_infra`, `http_logger_middleware_go_infra`
- `http_json_response_go_infra`, `http_error_response_go_infra`, `http_parse_body_go_infra`
- `random_hex_id_go_core`
Frontend C++:
- `http_request_cpp_core`
- `sse_client_cpp_core`
- `data_table_cpp_viz` (lista flows)
- `kpi_card_cpp_viz` (contadores por status)
## Crea (delegadas a fn-constructor en 0130a)
- `parse_issue_md_go_infra` — lee .md → struct (frontmatter YAML + body).
- `write_issue_md_go_infra` — escribe struct → .md preservando body + orden de campos.
- `scan_issues_dir_go_infra` — walk `dev/issues/` + `dev/issues/completed/`.
- `scan_flows_dir_go_infra` — walk `dev/flows/`.
- `watch_dir_fsnotify_go_infra` (si no existe) — events channel.
## DoD
- `fn doctor` verde para ambas apps (artefacts + e2e).
- `e2e_checks` en ambos `app.md` (build + health + self-test).
- Drag-drop en frontend reescribe el `.md` correspondiente y `git diff` lo muestra (solo frontmatter, body intacto).
- Trio obligatorio (`description` + `icon.phosphor` + `icon.accent`) en ambos `app.md`.
- Sub-repos Gitea creados (`dataforge/kanban_cpp` reactivado o nuevo, mismo nombre).
dod_evidence_schema:
- id: backend_health
kind: cmd
expected: "curl -fsS http://localhost:8487/api/health == 200"
required: true
- id: api_issues_count
kind: cmd
expected: "curl -fsS http://localhost:8487/api/issues | jq 'length' >= 90"
required: true
- id: patch_writes_md
kind: cmd
expected: "PATCH /api/issues/0130 status=in-progress reescribe dev/issues/0130-*.md (git diff muestra solo status)"
required: true
- id: frontend_self_test
kind: cmd
expected: "./cpp/build/linux/apps/kanban_cpp/kanban_cpp --self-test exit 0"
required: true
- id: board_screenshot
kind: screenshot
expected: "kanban_cpp Board panel con 4 columnas pobladas con issues reales"
required: true
## Anti-scope
NO incluye en esta version:
- Grafo de dependencias (depends/blocks/related visual).
- Edicion de body MD desde la app (solo frontmatter).
- Multi-PC sync (backend es local).
- Crear issues nuevos desde la UI (solo editar existentes).
- DoD evidence panel, agent runs, calendar, worktrees (la v1 los mezclaba — fuera).
+75
View File
@@ -0,0 +1,75 @@
---
id: 0130a
title: 'Funciones registry: parser MD + scan dirs + writer + watcher'
status: pendiente
type: infra
domain:
- registry-quality
- dev-ux
scope: registry-only
priority: alta
depends: []
blocks:
- 0130b
related:
- "0130"
tags:
- registry
- go
- parser
- frontmatter
- fsnotify
flow: "0130"
created: "2026-05-22"
updated: "2026-05-22"
---
# 0130a — Funciones registry para kanban_cpp v2
**Status:** pendiente
## Por que
El backend de kanban_cpp v2 necesita parsear/escribir frontmatter YAML de los `.md` de `dev/issues/` y `dev/flows/`. Estas piezas son reusables (cualquier app del registry puede operar sobre issues/flows), asi que viven en el registry, no en el backend de la app.
## Funciones a crear (delegar a fn-constructor en paralelo)
| ID | Firma | Pureza |
|---|---|---|
| `parse_issue_md_go_infra` | `(path string) (Issue, []byte body, error)` | impure (FS) |
| `write_issue_md_go_infra` | `(path string, issue Issue, body []byte) error` | impure (FS) |
| `scan_issues_dir_go_infra` | `(root string) ([]Issue, error)` | impure (FS) |
| `scan_flows_dir_go_infra` | `(root string) ([]Flow, error)` | impure (FS) |
| `watch_dir_fsnotify_go_infra` | `(ctx, root) (<-chan FsEvent, error)` | impure (FS, async) |
Tipos:
- `Issue_go_infra` — struct con campos del frontmatter (id, title, status, type, domain, scope, priority, depends, blocks, related, flow, tags, created, updated, file_path, mtime_ns).
- `Flow_go_infra` — struct equivalente para flows.
- `FsEvent_go_infra``{path, op}` con `op in {create, write, remove, rename}`.
## Notas de implementacion
- Usar `gopkg.in/yaml.v3` para parsing (preserva orden de keys via `yaml.Node`).
- Writer DEBE preservar:
- Orden de campos del frontmatter original.
- Body MD intacto (todo lo que va despues del segundo `---`).
- Comentarios YAML (libre, best-effort).
- `parse_issue_md` debe ser tolerante: si falta un campo opcional, default empty.
- `watch_dir_fsnotify` recursivo, debounce 200ms.
## DoD
- 5 pares `.go` + `.md` en `functions/infra/`.
- Tests unitarios:
- parse → write → parse round-trip preserva struct.
- scan_issues_dir devuelve >=90 issues actuales.
- watcher detecta creacion + modificacion + borrado.
- `fn index` registra los 5 IDs + 3 tipos.
- `fn doctor uses-functions` limpio.
## Anti-scope
NO incluye en esta tanda:
- Markdown rendering del body (eso lo hace el frontend si quiere).
- Validacion contra TAXONOMY (existe `fn doctor issues`).
- CRUD de issues nuevos (write_issue cubre el caso, pero crear file = scope del backend).
+114
View File
@@ -0,0 +1,114 @@
---
id: "0130b"
title: "Backend Go kanban_cpp v2: schema + handlers + watcher + SSE"
status: pendiente
type: app
domain:
- apps-infra
- dev-ux
scope: app-scoped
priority: alta
depends:
- "0130a"
blocks:
- "0130c"
related:
- "0130"
created: 2026-05-22
updated: 2026-05-22
tags: [service, kanban, go, sqlite, sse]
flow: "0130"
---
# 0130b — Backend Go kanban_cpp v2
**Status:** pendiente
## Por que
Servicio HTTP local que sirve los issues + flows del proyecto al frontend C++. Es un wrapper fino sobre las funciones del registry de 0130a + SQLite cache + watcher.
## Estructura
```
apps/kanban_cpp/backend/
app.md # tag service
go.mod
main.go # entry: flags + run
db.go # open + apply migrations + upsert helpers
handlers.go # endpoints REST
sse_hub.go # broadcaster
watcher.go # bind a watch_dir_fsnotify + re-ingesta + emit SSE
ingest.go # scan → upsert; usa 0130a
migrations/
001_init.sql
operations.db # creada en runtime
```
## Endpoints
| Verbo | Path | Notas |
|---|---|---|
| GET | `/api/health` | `{ok:true, version, count_issues, count_flows}` |
| GET | `/api/issues` | filtros: `status`, `domain`, `priority`, `tag`, `scope` |
| GET | `/api/issues/{id}` | issue + body |
| PATCH | `/api/issues/{id}` | partial update frontmatter → `write_issue_md` + re-ingesta + SSE |
| GET | `/api/flows` | filtros: `status`, `kind` |
| GET | `/api/flows/{id}` | flow + body |
| GET | `/api/meta` | enums leidos de `dev/TAXONOMY.md` |
| GET | `/api/sse` | stream `{type, id, path}` |
CORS abierto local (`*`). Logger middleware.
## Schema (migrations/001_init.sql)
```sql
CREATE TABLE IF NOT EXISTS issues (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
status TEXT NOT NULL,
type TEXT,
scope TEXT,
priority TEXT,
domain_json TEXT NOT NULL DEFAULT '[]',
tags_json TEXT NOT NULL DEFAULT '[]',
depends_json TEXT NOT NULL DEFAULT '[]',
blocks_json TEXT NOT NULL DEFAULT '[]',
related_json TEXT NOT NULL DEFAULT '[]',
flow_id TEXT,
body TEXT NOT NULL DEFAULT '',
file_path TEXT NOT NULL,
mtime_ns INTEGER NOT NULL,
created_at TEXT,
updated_at TEXT,
completed INTEGER NOT NULL DEFAULT 0 -- 1 si vive en completed/
);
CREATE INDEX IF NOT EXISTS idx_issues_status ON issues(status);
CREATE INDEX IF NOT EXISTS idx_issues_priority ON issues(priority);
CREATE TABLE IF NOT EXISTS flows (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
status TEXT,
kind TEXT,
tags_json TEXT NOT NULL DEFAULT '[]',
body TEXT NOT NULL DEFAULT '',
file_path TEXT NOT NULL,
mtime_ns INTEGER NOT NULL
);
```
## DoD
- `curl http://localhost:8487/api/health` devuelve 200 + counts.
- `curl http://localhost:8487/api/issues | jq 'length' >= 90`.
- `curl -X PATCH /api/issues/0130 -d '{"status":"in-progress"}'` reescribe `dev/issues/0130-*.md` (status updated, body intacto).
- Despues del PATCH, suscriptor SSE recibe evento `{type:"updated", id:"0130"}`.
- Tras `mv dev/issues/0130-*.md dev/issues/completed/`, watcher actualiza fila (`completed=1`).
- `go test ./...` verde.
## Anti-scope
- No expone proposals ni capabilities (eso es MCP registry).
- No autentica (local-only por ahora).
- No persiste estado UI (eso lo hace el frontend).
@@ -0,0 +1,86 @@
---
id: "0130c"
title: "Frontend C++ ImGui kanban_cpp v2: board + flows + filtros + detalle"
status: pendiente
type: app
domain:
- cpp-stack
- dev-ux
scope: app-scoped
priority: alta
depends:
- "0130b"
blocks: []
related:
- "0130"
created: 2026-05-22
updated: 2026-05-22
tags: [cpp, imgui, kanban, frontend]
flow: "0130"
---
# 0130c — Frontend C++ ImGui kanban_cpp v2
**Status:** pendiente
## Por que
UI nativa sobre el backend 0130b. Aprovecha el framework `fn::run_app` (menubar, layouts, settings, about, log) y los componentes del registry (`data_table`, `kpi_card`, `http_request`, `sse_client`).
## Estructura
```
apps/kanban_cpp/
app.md
appicon.ico
CMakeLists.txt
main.cpp # fn::run_app + cfg.panels
data.h / data.cpp # http client + state global (issues, flows, filters)
panel_board.cpp # 4 columnas + drag-drop
panel_flows.cpp # tabla via data_table_cpp_viz
panel_filters.cpp # Aside con multi-select
panel_detail.cpp # form editable del issue seleccionado
panels.h
```
## Trio obligatorio (`app.md`)
```yaml
description: "Kanban C++ v2 para gestionar dev/issues y dev/flows del registry"
icon:
phosphor: "kanban"
accent: "#a855f7"
```
## Paneles
1. **Board** (`TI_KANBAN " Board"`) — 4 columnas (pendiente / in-progress / bloqueado / completado). Cada card: id + title (trunc 60) + priority badge + first domain chip. Drag-drop con `ImGui::BeginDragDropSource/Target` -> PATCH status.
2. **Flows** (`TI_FLOW " Flows"`) — `data_table_cpp_viz` con columnas id/title/status/kind. Click fila → carga detail.
3. **Filters** (`TI_FUNNEL " Filters"`) — AppShell.Aside-equivalente (panel lateral fijo). Multi-select por domain, scope, priority, tags. Estado local; rebuild request query.
4. **Detail** (`TI_INFO " Detail"`) — modal/panel lateral con form: status (combo), priority (combo), scope (combo), tags (chips editables), depends/blocks (listas), body (read-only multiline).
## HTTP client (data.cpp)
- `fetch_issues(filters)` → GET con query string → parse JSON → vector<Issue>.
- `fetch_flows()` → similar.
- `patch_issue(id, partial)` → PATCH JSON → recibe issue actualizado.
- `subscribe_sse()` thread aparte → push events a queue mutex → consumir en main loop → re-fetch afectados.
Usa `http_request_cpp_core` + `sse_client_cpp_core`. JSON via `nlohmann/json` (ya en cpp/vendor o sacar al header-only).
## DoD
- `cmake --build cpp/build/linux --target kanban_cpp -j` verde.
- `./cpp/build/linux/apps/kanban_cpp/kanban_cpp --self-test` exit 0:
- inicializa contexto ImGui sin display.
- parsea respuesta JSON sintetica.
- no toca red salvo si `--backend http://...` se pasa.
- e2e_checks en `app.md`: build + self_test + backend_health (corre backend en background) + smoke (drag-drop reescribe MD).
- Captura screenshot board con 4 columnas pobladas → guardar en `dod_evidence/board_screenshot.png`.
## Anti-scope
- Sin grafo de dependencias (epic 0130 lo describe como anti-scope v1).
- Sin crear issues nuevos (solo editar existentes).
- Sin edicion de body MD (solo frontmatter).
- Sin syntax highlighting markdown.
+90
View File
@@ -0,0 +1,90 @@
---
id: "0131"
title: "Modulo C++ chat_panel — panel ImGui para chat con agentes"
status: pendiente
type: app
domain:
- cpp-stack
- agents
- dev-ux
scope: cross-stack
priority: alta
depends:
- "0113"
blocks: []
related:
- "0130"
created: 2026-05-22
updated: 2026-05-22
tags: [cpp, imgui, agents, chat, module, sse]
flow: ""
---
# 0131 — Modulo C++ chat_panel
**Status:** pendiente
## Por que
Tras lanzar un agente desde kanban_cpp (issue 0130), no hay forma de interactuar con el desde la propia app. Hoy el flujo es: lanzar agente, abrir terminal aparte, `tail -f /tmp/wt-.../agent.log`. Queremos un panel C++ reutilizable que cualquier app embebra para chatear con un agente (Claude headless o futuros) y ver su output en streaming.
## Que entrega
Modulo `cpp/functions/viz/chat_panel/` (paquete del registry, kind: function, lang: cpp, domain: viz). API:
```cpp
namespace fn_chat {
struct ChatPanel {
// run_id del agent_runner_api; null = panel vacio "no agent attached"
std::string run_id;
std::string backend_url = "http://127.0.0.1:8486"; // agent_runner_api
bool auto_scroll = true;
};
void render(ChatPanel& panel);
}
```
Comportamiento:
- Conecta SSE `/api/runs/<run_id>/sse` en background thread (reusa `sse_client_cpp_core`).
- Parsea eventos `state`, `log`, `evidence`, `finish` y los renderiza:
- `log` → linea cruda en buffer scrollable.
- `state` → badge superior con status (`pending/running/done/aborted/failed`).
- `evidence` → chip lateral con kind + payload_url.
- `finish` → marca run terminada, deja conexion para ver historico.
- Input box inferior (multiline) + boton "Send". POST a `/api/runs/<run_id>/message` (endpoint A IMPLEMENTAR en agent_runner_api — extension paralela; si no existe, boton se deshabilita).
- Toolbar: `Abort run`, `Clear buffer`, `Show evidence panel`.
## Estructura
```
cpp/functions/viz/chat_panel/
chat_panel.h
chat_panel.cpp
chat_panel.md
chat_panel_test.cpp
```
## Reusa del registry
- `sse_client_cpp_core` — SSE async.
- `http_request_cpp_core` — POST mensajes / abort.
- `selectable_text_cpp_viz` — copy log lines.
- `data_table_cpp_viz` — opcional para tabla de evidencias.
## DoD
- Modulo compila en Linux + Windows.
- Demo en `primitives_gallery` o app dedicada `agent_chat_demo` con run_id fijo + mock SSE feeder.
- Integracion en kanban_cpp v2: nuevo panel "Chat" que se abre al hacer click en card con agent_active, run_id se pasa automatico.
- `e2e_checks`: smoke con mock SSE; assertion: tras emitir 3 eventos de log, panel los muestra en orden.
## Anti-scope (v1)
- No persiste history local (refresh = perdemos buffer; agent.log es la fuente).
- No syntax highlight markdown / codigo.
- Sin multi-run (un panel = un run).
- Sin file diff inline (kind:evidence con kind:diff queda como link a `git show`).
## Notas
Si el endpoint POST `/api/runs/:id/message` no existe en agent_runner_api, abrir issue paralelo `0131b` para anadirlo (claude headless aceptara mensajes via stdin del subprocess — el runner debe forwardearlos). Para v1 se acepta panel read-only.
@@ -0,0 +1,92 @@
---
id: "0132"
title: "Modulo C++ terminal_panel — emulador TTY ImGui embebible"
status: pendiente
type: app
domain:
- cpp-stack
- dev-ux
- apps-infra
scope: cross-stack
priority: alta
depends: []
blocks: []
related:
- "0130"
- "0131"
created: 2026-05-22
updated: 2026-05-22
tags: [cpp, imgui, terminal, pty, module]
flow: ""
---
# 0132 — Modulo C++ terminal_panel
**Status:** pendiente
## Por que
Apps del ecosistema (kanban_cpp, services_monitor, agents_dashboard) necesitan ver output crudo de comandos shell sin abrir un terminal externo. Tipico: tail de un log, watch de un curl, ejecutar `git status` rapido. Solucion estandar: modulo `terminal_panel` reusable que arranca un shell hijo via PTY y lo renderiza en ImGui.
## Que entrega
Modulo `cpp/functions/viz/terminal_panel/`:
```cpp
namespace fn_term {
struct TerminalPanel {
std::string shell; // "/bin/bash" linux, "powershell.exe" windows; default auto
std::string cwd; // working dir; default = current
std::vector<std::string> env; // KEY=VAL extras
int scrollback_lines = 5000;
bool readonly = false; // true = no input forwarding (tail-only)
};
void open(TerminalPanel& panel); // crea proceso hijo + PTY
void render(TerminalPanel& panel);
void send(TerminalPanel& panel, const std::string& text); // stdin
void close(TerminalPanel& panel);
}
```
Implementacion:
- Linux: `forkpty` + `read/write` non-blocking en background thread.
- Windows: ConPTY (CreatePseudoConsole) + ReadFile en thread.
- Buffer circular `scrollback_lines` filas; render con `ImGui::TextUnformatted` por chunk para minimizar costo.
- Soporte minimo de ANSI: cursor pos, color FG/BG basico (16 colores), clear screen. NO soporte completo (no Vim, no top, no curses pesado).
- Toolbar: clear, copy selection, reset shell, scroll-to-bottom.
## Estructura
```
cpp/functions/viz/terminal_panel/
terminal_panel.h
terminal_panel.cpp
terminal_panel.md
terminal_panel_linux.cpp // forkpty path
terminal_panel_windows.cpp // ConPTY path
terminal_panel_test.cpp
```
## Reusa del registry
- `logger_cpp_core` (fn_log) — log errores spawn/io.
- `ansi_parser_cpp_core` — si existe, parsear secuencias ANSI. Si no, delegar a `fn-constructor` para crearlo dentro de este issue (sub-deliverable).
## DoD
- Compila Linux + Windows.
- Demo: `primitives_gallery` muestra terminal corriendo `bash -i` (linux) / `cmd.exe` (windows).
- Smoke test: spawn `echo hello && exit 0` → buffer contiene "hello".
- Integracion en kanban_cpp v2: panel "Logs" que toma `run_id` de issue activa y arranca `tail -f /tmp/wt-<slug>-<runid>/agent.log` (readonly=true).
- FPS sin caida bajo carga de `yes "x"` (saturado): 60fps target con scrollback truncado.
## Anti-scope (v1)
- Sin soporte completo ANSI (no italics, no 256 colores, no Unicode wide).
- Sin Vim / programas curses-pesados (cursor visible solo).
- Sin SSH remoto (solo shell local).
- Sin tabs multiples en un panel (un panel = un proceso).
## Notas
ConPTY requiere Windows 10 v1809+. Si target inferior, fallback a CreatePipe sin PTY (sin redimensionado).
@@ -0,0 +1,74 @@
---
id: "0133"
title: "data_table: optimizar para 10M filas sin caida de FPS (finalize modulo)"
status: pendiente
type: refactor
domain:
- cpp-stack
- data-ingest
scope: app-scoped
priority: alta
depends: []
blocks: []
related:
- "0081"
- "0097"
created: 2026-05-22
updated: 2026-05-22
tags: [cpp, imgui, performance, data_table, finalize]
flow: ""
---
# 0133 — data_table 10M rows sin caida FPS
**Status:** pendiente
## Por que
`data_table_cpp_viz` (modulo `fn_module_data_table` / `fn_table_viz`) actualmente maneja decenas de miles de filas con `ImGuiListClipper` y rinde bien. Apps reales (call_monitor con telemetria, services_monitor con escalado, futuro graph_explorer con nodos) ya nos llevan a millones de filas. Objetivo: cerrar el modulo con benchmark estable de **10M filas a >=60fps** en hardware tipico (Ryzen 5 / i5 8th gen + 16GB).
## Que entrega
Refactor del modulo manteniendo API publica + un benchmark suite.
### Cambios tecnicos
1. **Storage columnar** — hoy `std::vector<std::vector<Cell>>` row-major. Cambiar a column-major (`Column { type; vector<T> data }`) para localidad de cache + iteracion. Las celdas se materializan solo para las filas visibles.
2. **String interning** — columnas de tipo string usan tabla de strings global con `uint32_t` indices. 10M filas con 50% strings repetidas → ahorra 60-70% RAM.
3. **Lazy filter/sort indices** — en vez de re-ordenar el storage, mantener `vector<uint32_t> visible_rows` que apunta al storage subyacente. Filter/sort solo reescribe ese vector.
4. **Computed columns en bloques**`compute_stage_cpp_core` ahora corre por cell; cambiar a procesar bloques de 1024 filas con SIMD via `OpenMP` (ya esta linkeado en fn_framework).
5. **Render path**`ImGuiListClipper` sigue siendo el frontend, pero el callback de render no debe asignar memoria por fila. Pre-formatear strings de display en `column.display_cache[row_idx]` con LRU de 100k entradas; resto se formatea on-the-fly.
6. **Color rules**`data_table_color_rules_cpp_viz` se evalua hoy por celda visible. Cachear el rule_id resuelto por row_idx tras primer paint.
7. **Stats**`compute_column_stats_cpp_core` solo se recalcula cuando cambia el filtro, no cada frame.
### Benchmark suite
`cpp/apps/data_table_bench/`:
- Genera dataset sintetico 10M filas x 20 cols (mix int/float/string/timestamp).
- Mide FPS sostenido durante:
- scroll lineal full range (down → bottom).
- filter por string match (`LIKE %foo%`).
- sort por columna numerica.
- color rule `value > p95`.
- Output: `fps_p50`, `fps_p1`, `mem_rss_mb`, `cpu_pct`.
- Asercion DoD: `fps_p1 >= 60` en cada escenario.
## DoD
- Refactor entregado sin romper apps consumidoras (call_monitor, services_monitor, graph_explorer, navegator_dashboard, kanban_cpp future).
- Benchmark suite ejecutable: `./data_table_bench --rows 10000000 --duration 30`.
- Resultados de benchmark guardados en `apps/data_table_bench/operations.db` con assertion `fps_p1 >= 60`.
- `e2e_checks` corriendo benchmark con dataset reducido (100k filas) en CI; full bench manual.
- Modulo marcado `version: 1.0.0` y `tags: [stable]` en su `.md`.
- Guia "porting old call sites" si la API publica cambia (en `cpp/functions/viz/data_table/MIGRATION.md`).
## Anti-scope
- Sin GPU rendering (sigue siendo CPU + ImGui).
- Sin paginacion remota (sigue todo in-memory).
- Sin streaming append-while-rendering (snapshot al frame inicio).
- Sin virtualizacion horizontal (todas las cols se renderizan; assumed N_cols <= 100).
## Notas
Issue 0081 introdujo la migracion inline → modulo. Issue 0097 cerro el wrapping en fn_module/fn_table_viz. Esta issue es el **finalize**: lo deja `1.0.0` con benchmark + suficiente performance para que las apps de telemetria/graph no necesiten paginar manual.
+979
View File
@@ -0,0 +1,979 @@
---
id: "0134"
title: "Mesh protocol spec: capability manifests, ed25519 envelopes, enrollment, audit chain"
status: pending
type: spec
domain:
- infra
- cybersecurity
- protocols
scope: cross-app
priority: high
depends: []
blocks:
- "0135"
- "0136"
- "0137"
- "0138"
- "0139"
- "0140"
- "0141"
- "0142"
- "0143"
related:
- "0069"
related_flows:
- "0009"
created: 2026-05-24
updated: 2026-05-24
tags: [mesh, wireguard, matrix, e2ee, ed25519, manifest, audit-chain, security, spec, agents, devices]
flow: "0009"
dependencies: []
---
# 0134 — Mesh protocol spec
**Status:** pending
## Por que
Flow 0009 (`agentes-dispositivos-mesh`) introduce un bus de control multi-device sobre WireGuard + Matrix donde cada dispositivo (PC, movil, raspberry, container Docker) ejecuta capabilities firmadas por el operador. Sin un protocolo formal compartido, cada implementacion (device_agent en Go, bot dispatcher en agents_and_robots, panel Mesh en agents_dashboard, hub en wg_hub) va a derivar.
Este issue cierra Fase B del flow: define el contrato exacto que **toda** implementacion debe respetar — wire format, firmas, replay protection, approval flow, audit chain, error model, threat model. Las issues 0135-0143 implementan lo que aqui se define.
Una vez aceptado este spec, ningun cambio en el wire format se acepta sin un nuevo issue + bump de `protocol_version`.
## Anti-scope
- NO define como provision el WG hub (ver 0136).
- NO define UI del panel Mesh (ver 0138).
- NO define implementacion concreta del bot Matrix (ver 0142).
- NO entra en como se persiste `pass operator/ed25519` mas alla de su uso.
- NO define schema de la `operations.db` de cada app — solo el subset estrictamente compartido (`audit_log`, `room_devices`, `seen_nonces`).
## Conventions
- `protocol_version` (string): **"mesh/1"** — incluido en todo envelope.
- Todo timestamp es Unix epoch **segundos** (`int64`).
- Todo `*_id` es `[a-z0-9_-]+` lowercase, 4-64 chars.
- Todo nonce es 16 bytes random (`crypto/rand`), serializado como **base64url sin padding**.
- Todo hash es SHA-256, serializado como **hex lowercase** (64 chars).
- Toda firma ed25519 es 64 bytes, serializada como **base64url sin padding** (86 chars).
- Toda clave publica ed25519 es 32 bytes, serializada como **base64url sin padding** (43 chars).
- Fingerprint de clave publica = primeros 16 bytes hex de `SHA-256(pubkey_raw_32_bytes)`.
- JSON canonical: claves ordenadas alfabeticamente, sin espacios, UTF-8, sin BOM. Para firmas siempre usar la forma canonica.
---
## 1. JSON envelope
Toda invocacion de capability viaja como par request/response, ya sea sobre Matrix (eventos `m.room.message` con `msgtype = m.capability.*`) o sobre HTTP dentro del mesh WG (`POST /capability`).
### 1.1 Request
```json
{
"protocol_version": "mesh/1",
"request_id": "req_01J9XYZABCDEF",
"manifest_id": "manifest_home-wsl_v3",
"capability": "fs.read",
"args": {
"path": "/var/log/syslog",
"max_bytes": 4096
},
"ts": 1748131200,
"nonce": "Yk9p6Xs_3hZQk4mB7lWcvA",
"signature": "u2vh...QkA"
}
```
- `request_id`: ULID generado por el caller (agents_and_robots o el operador). Idempotency key — si la misma request llega 2x, device_agent debe devolver el mismo response sin re-ejecutar.
- `manifest_id`: id del capability manifest contra el cual se evalua. El device debe tener este manifest activo o rechazar `manifest_invalid`.
- `capability`: dotted name, ej. `shell.exec`, `fs.read`, `docker.container.list`. Debe estar en `manifest.capabilities[].name`.
- `args`: objeto JSON especifico de la capability. Schema validado por device_agent contra el manifest.
- `ts`: Unix seconds. Edad maxima 60s (ver §5).
- `nonce`: 16 bytes random, base64url. Unico por request (ver §5).
- `signature`: ed25519 sobre canonical bytes (ver 1.3).
### 1.2 Response
```json
{
"protocol_version": "mesh/1",
"request_id": "req_01J9XYZABCDEF",
"ok": true,
"result": {
"stdout": "May 24 12:00:00 localhost systemd[1]: Started session-1.scope.\n",
"stderr": "",
"exit_code": 0,
"truncated": false
},
"error": null,
"duration_ms": 42,
"audit_hash": "a3f5...09bc"
}
```
- `ok`: boolean. Si false, `result` ausente y `error` poblado.
- `error`: objeto `{code, message, details?}` cuando `ok=false`. `code` debe estar en §10.
- `duration_ms`: tiempo de ejecucion en device_agent (no incluye latencia Matrix).
- `audit_hash`: `this_hash` del registro en `audit_log` (ver §7). Permite al caller verificar la cadena.
Response NO va firmado por defecto — viaja sobre canal autenticado (Matrix E2EE o WG mesh). Si en el futuro se requiere firma de response (audit externo), se anade campo opcional `response_signature` con el mismo esquema canonical.
### 1.3 Canonical bytes para firma del request
```
canonical = "mesh/1\n" +
request_id + "\n" +
manifest_id + "\n" +
capability + "\n" +
sha256_hex(json_canonical(args)) + "\n" +
int_to_string(ts) + "\n" +
nonce
```
Bytes UTF-8, separador `\n` (0x0A). No trailing newline. Hash del args para no exponer args grandes a la firma (la firma se valida contra el hash, y `args` se reentrega tal cual; cualquier modificacion rompe la firma).
`json_canonical(args)`:
1. Si `args` es null o ausente, `json_canonical = "null"`.
2. Si `args` es objeto, recursivo: ordenar claves alfabeticamente, valores serializados sin espacios, strings con escape JSON estandar.
3. Si `args` es array, recursivo sobre cada elemento, sin reordenar.
### 1.4 Transport binding
| Transport | Request encoding | Response encoding |
|---|---|---|
| Matrix room event | `content.body` = JSON string, `msgtype` = `m.capability.request` | `m.capability.response` event en el mismo room |
| HTTP intra-mesh (`https://10.42.0.10:7777/capability`) | `POST` body JSON | response body JSON |
| SSE (streaming logs, `docker.logs --follow`) | request via POST | response inicial JSON `{ok, result: {stream_id}}` + SSE `event: chunk` |
Matrix es default. HTTP solo lo activa el operador con `mesh_http=true` en el manifest para casos de baja latencia (`docker.logs` tail interactivo, transferencias >1MB que Matrix limita).
---
## 2. Capability manifest
Documento firmado por el operador que autoriza a un device a ejecutar un set acotado de capabilities. Sin manifest valido, device_agent rechaza todo.
### 2.1 Schema YAML (legible) — fuente de verdad
```yaml
# manifest_home-wsl_v3.yaml
protocol_version: mesh/1
manifest_id: manifest_home-wsl_v3
device_id: home-wsl
operator: egutierrez@aurgi.com
operator_pubkey_fingerprint: "a1b2c3d4e5f60718"
issued_at: 1748131200
expires_at: 1779667200 # 1 year later
capabilities:
- name: shell.exec
requires_approval: false
constraints:
binaries_whitelist: [ls, cat, head, tail, grep, ps, df, du, uname, uptime]
max_duration_s: 10
max_output_bytes: 65536
cwd_allowed: ["/home/lucas", "/tmp"]
- name: fs.read
requires_approval: false
constraints:
paths_allowed: ["/home/lucas/**", "/var/log/syslog", "/etc/os-release"]
paths_denied: ["/home/lucas/.ssh/**", "/home/lucas/.password-store/**"]
max_bytes: 1048576
- name: fs.write
requires_approval: true
constraints:
paths_allowed: ["/home/lucas/inbox/**"]
max_bytes: 1048576
- name: docker.container.list
requires_approval: false
- name: docker.container.exec
requires_approval: true
constraints:
containers_allowed: ["agents_and_robots", "registry_api"]
binaries_whitelist: [ls, cat, ps]
max_duration_s: 30
```
### 2.2 JSON canonical (lo que se firma)
```json
{
"capabilities": [
{"constraints": {"binaries_whitelist": ["ls","cat","head","tail","grep","ps","df","du","uname","uptime"], "cwd_allowed":["/home/lucas","/tmp"], "max_duration_s": 10, "max_output_bytes": 65536}, "name": "shell.exec", "requires_approval": false},
{"constraints": {"max_bytes": 1048576, "paths_allowed": ["/home/lucas/**","/var/log/syslog","/etc/os-release"], "paths_denied": ["/home/lucas/.ssh/**","/home/lucas/.password-store/**"]}, "name": "fs.read", "requires_approval": false},
{"constraints": {"max_bytes": 1048576, "paths_allowed":["/home/lucas/inbox/**"]}, "name": "fs.write", "requires_approval": true},
{"name": "docker.container.list", "requires_approval": false},
{"constraints": {"binaries_whitelist": ["ls","cat","ps"], "containers_allowed": ["agents_and_robots","registry_api"], "max_duration_s": 30}, "name": "docker.container.exec", "requires_approval": true}
],
"device_id": "home-wsl",
"expires_at": 1779667200,
"issued_at": 1748131200,
"manifest_id": "manifest_home-wsl_v3",
"operator": "egutierrez@aurgi.com",
"operator_pubkey_fingerprint": "a1b2c3d4e5f60718",
"protocol_version": "mesh/1"
}
```
Producido por `capability_manifest_canonicalize_go_infra` (function 0135 entrega).
### 2.3 Canonical bytes para firma del manifest
```
manifest_canonical = "mesh/1/manifest\n" + json_canonical(manifest_without_signature)
```
Donde `manifest_without_signature` es el JSON canonical de §2.2. El prefijo `mesh/1/manifest\n` es domain separator — evita que una firma de manifest pueda interpretarse como firma de envelope.
### 2.4 Manifest signed envelope (lo que se entrega al device)
```json
{
"manifest": { /* §2.2 */ },
"signature": "k7Yp...QwE"
}
```
Persistido en device como `~/.config/device_agent/manifests/manifest_home-wsl_v3.json`.
### 2.5 Reglas de verificacion (device_agent al arrancar y al recibir request)
1. Parsear `manifest` y `signature`.
2. Computar `manifest_canonical`.
3. Verificar `ed25519.Verify(operator_pubkey, manifest_canonical, signature)`.
4. Rechazar si `expires_at < now()``manifest_invalid` con `details.reason = "expired"`.
5. Rechazar si `issued_at > now() + 300` (clock skew) → `manifest_invalid` con `details.reason = "future_issued"`.
6. Rechazar si `device_id` no coincide con `~/.config/device_agent/device_id``manifest_invalid`.
7. Rechazar si `operator_pubkey_fingerprint` no coincide con la pubkey conocida → `manifest_invalid`.
### 2.6 Rotacion
Un manifest nuevo coexiste con el anterior hasta su `expires_at`. Para forzar revocacion inmediata: el hub publica un evento `manifest_revoked` (en room `#operator-broadcast`) firmado por el operador con la lista de `manifest_id` revocados. device_agent mantiene `~/.config/device_agent/revoked_manifests.json` y lo consulta antes de aceptar.
---
## 3. ed25519 signing flow
### 3.1 Keypair
- **Privada** del operador: `pass operator/ed25519`. Linea 1 = base64url de los 32 bytes del seed ed25519. Lineas siguientes = metadata (operador email, created_at, fingerprint).
- **Publica** del operador: `~/.fn_operator.pub`. Contenido = base64url de los 32 bytes raw + `\n`. Distribuida a cada device en su `~/.config/device_agent/operator.pub` durante enrollment.
### 3.2 Generacion (una sola vez en la vida del operador, idempotente)
```bash
# Funcion del registry: operator_keygen_bash_infra (0135 entrega)
operator_keygen() {
if pass show operator/ed25519 >/dev/null 2>&1; then
echo "operator key already exists; skipping"
return 0
fi
local seed pub fp
seed=$(openssl rand 32 | base64 -w0 | tr '+/' '-_' | tr -d '=')
pub=$(echo "$seed" | go_ed25519_derive_pub) # helper Go: seed → pub
fp=$(echo -n "$(echo "$pub" | base64url -d)" | sha256sum | cut -c1-32)
pass insert -m operator/ed25519 <<EOF
$seed
operator: $(git config user.email)
created_at: $(date +%s)
fingerprint: $fp
pubkey: $pub
EOF
echo "$pub" > ~/.fn_operator.pub
chmod 600 ~/.fn_operator.pub
}
```
### 3.3 Sign
```go
// capability_manifest_sign_go_infra (issue 0135)
func SignManifest(m Manifest, seed []byte) ([]byte, error) {
if len(seed) != ed25519.SeedSize {
return nil, ErrInvalidSeed
}
canonical, err := canonicalizeManifest(m)
if err != nil { return nil, err }
msg := append([]byte("mesh/1/manifest\n"), canonical...)
priv := ed25519.NewKeyFromSeed(seed)
return ed25519.Sign(priv, msg), nil
}
```
Para envelopes:
```go
func SignRequest(r Request, seed []byte) ([]byte, error) {
canonical, err := canonicalRequestBytes(r)
if err != nil { return nil, err }
priv := ed25519.NewKeyFromSeed(seed)
return ed25519.Sign(priv, canonical), nil
}
```
`canonicalRequestBytes` implementa §1.3.
### 3.4 Verify (device_agent)
```go
// capability_manifest_verify_go_infra (issue 0135)
func VerifyManifest(signed SignedManifest, pubkey []byte) error {
if len(pubkey) != ed25519.PublicKeySize {
return ErrInvalidPubkey
}
canonical, err := canonicalizeManifest(signed.Manifest)
if err != nil { return err }
msg := append([]byte("mesh/1/manifest\n"), canonical...)
if !ed25519.Verify(pubkey, msg, signed.Signature) {
return ErrSignatureInvalid
}
return nil
}
```
### 3.5 Domain separators (criticos)
Cada tipo de firma usa prefix unico para evitar cross-protocol attacks:
| Tipo | Prefix |
|---|---|
| Manifest | `"mesh/1/manifest\n"` |
| Request envelope | `"mesh/1/request\n"` (implicito en §1.3 — usa el `\n` join) |
| Enrollment token | `"mesh/1/enroll\n"` |
| Approval token | `"mesh/1/approval\n"` |
| Manifest revocation | `"mesh/1/revoke\n"` |
---
## 4. Enrollment token
Token corto firmado por el operador que un device usa **una sola vez** para registrarse contra `wg_hub` y obtener su config WireGuard + manifest inicial.
### 4.1 Payload JSON (canonical)
```json
{
"allowed_capabilities": ["shell.exec", "fs.read", "docker.container.list"],
"device_id": "home-wsl",
"expires_at": 1748131800,
"issued_at": 1748131200,
"nonce": "Tk9NbjVxV3JLcF9j",
"operator_pubkey_fingerprint": "a1b2c3d4e5f60718",
"protocol_version": "mesh/1",
"purpose": "enroll"
}
```
- `expires_at - issued_at` <= 600s (10 min). Hub rechaza si excede.
- `allowed_capabilities`: subset que el operador autoriza a este enrollment. El manifest final puede ser mas restrictivo pero no mas amplio.
- `nonce` previene replay incluso si el operador reusa el token por error.
- `purpose: "enroll"` es discriminator interno; los otros valores reservados son `"approval"` (§6) y `"revoke"`.
### 4.2 Wire format
```
base64url(json_canonical(payload)) + "." + base64url(ed25519_signature)
```
Ejemplo (truncado):
```
eyJhbGxvd2VkX2NhcGFiaWxpdGllcyI6WyJzaGVsbC5leGVjIiwiZnMucmVhZCJdLCJkZXZpY2VfaWQiOiJob21lLXdzbCIsLi4u.k7YpQwE9Vh...
```
Aproximadamente 280-320 bytes. Cabe en un QR Code v6 (error correction M).
### 4.3 Generacion (operador)
```bash
# enroll_device pipeline (issue 0139) llama:
./fn run enrollment_token_create home-wsl \
--capabilities "shell.exec,fs.read,docker.container.list" \
--ttl 600
# stdout: token base64url
```
### 4.4 Verificacion (wg_hub `POST /enroll`)
```go
// enrollment_token_verify_go_infra (issue 0135)
func VerifyEnrollToken(raw string, pubkey []byte) (*EnrollPayload, error) {
parts := strings.Split(raw, ".")
if len(parts) != 2 { return nil, ErrTokenMalformed }
payloadBytes, err := base64.RawURLEncoding.DecodeString(parts[0])
if err != nil { return nil, ErrTokenMalformed }
sig, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil { return nil, ErrTokenMalformed }
msg := append([]byte("mesh/1/enroll\n"), payloadBytes...)
if !ed25519.Verify(pubkey, msg, sig) {
return nil, ErrSignatureInvalid
}
var p EnrollPayload
if err := json.Unmarshal(payloadBytes, &p); err != nil {
return nil, ErrTokenMalformed
}
now := time.Now().Unix()
if p.ExpiresAt < now { return nil, ErrTokenExpired }
if p.IssuedAt > now + 300 { return nil, ErrTokenFutureIssued }
if p.Purpose != "enroll" { return nil, ErrTokenWrongPurpose }
return &p, nil
}
```
### 4.5 POST /enroll (wg_hub)
```
POST https://organic-machine.com/enroll
Content-Type: application/json
{
"enrollment_token": "eyJhbGxv...QwE",
"device_pubkey_wg": "K3v8...j0c=", // WG public key del device, generada por wg_keygen
"device_hostname": "home-wsl",
"device_os": "linux-wsl2",
"device_arch": "x86_64"
}
```
Response:
```json
{
"ok": true,
"wg_config": "[Interface]\nPrivateKey = <keep on device>\nAddress = 10.42.0.10/24\n[Peer]\nPublicKey = ...\nEndpoint = organic-machine.com:51820\nAllowedIPs = 10.42.0.0/24\nPersistentKeepalive = 25\n",
"manifest": { /* signed manifest */ },
"matrix_room": "!abc123:organic-machine.com",
"matrix_invite_url": "https://matrix.to/#/!abc123:organic-machine.com?via=organic-machine.com"
}
```
Hub marca el token como `consumed` en `wg_enrollment_tokens` (token_nonce as PK) — segundo uso rechazado con `nonce_replay`.
---
## 5. Replay protection
### 5.1 Nonces
- 16 bytes `crypto/rand` por request.
- Server (device_agent O hub) mantiene tabla `seen_nonces`:
```sql
CREATE TABLE IF NOT EXISTS seen_nonces (
nonce TEXT PRIMARY KEY, -- base64url
seen_at INTEGER NOT NULL, -- unix seconds
request_id TEXT NOT NULL,
expires_at INTEGER NOT NULL -- seen_at + 300
);
CREATE INDEX IF NOT EXISTS idx_seen_nonces_expires ON seen_nonces(expires_at);
```
- TTL = 300s. Job periodico (cada 60s) borra entradas con `expires_at < now()`.
### 5.2 Timestamp
- `ts` debe estar en `[now-60, now+30]`. Mas viejo → `nonce_replay`. Mas futuro → `signature_invalid` con `details.reason="clock_skew"`.
- Asume devices con NTP sync (`chrony`/`systemd-timesyncd`). Si un device tiene clock drift >30s, el operador recibe alerta en `#operator-approvals`.
### 5.3 Algoritmo
```go
// Pseudo
func AcceptNonce(db *sql.DB, nonce string, ts int64, requestID string) error {
now := time.Now().Unix()
if ts < now - 60 { return ErrNonceReplay }
if ts > now + 30 { return ErrSignatureInvalid /* clock_skew */ }
_, err := db.Exec(
`INSERT INTO seen_nonces(nonce, seen_at, request_id, expires_at) VALUES(?,?,?,?)`,
nonce, now, requestID, now+300,
)
if isUniqueViolation(err) { return ErrNonceReplay }
return err
}
```
`AcceptNonce` se llama **antes** de ejecutar la capability, despues de verificar la firma. Si la firma es invalida pero el nonce es nuevo, NO se inserta (evita amplificar log spam).
---
## 6. Approval flow
Para capabilities con `requires_approval: true`, device_agent NO ejecuta sin recibir un approval token firmado por el operador.
### 6.1 Secuencia
```
[operator] [agents_and_robots] [device_agent] [#operator-approvals]
| | | |
| !exec rm -rf /tmp/x in #dev-home-wsl | |
|------------------->| | |
| |--- request envelope ->| |
| | |--- decide: approval needed
| | | |
| |<--- approval_request -| |
| |--- post to #op-approvals ----------------->|
| | | |
|<--- notification --| | |
| |
|--- reacts 👍 OR posts !approve req_01J9... ------------------->|
| |
| |<--- captures reaction/cmd -----------------|
| |--- signs approval_token (via operator key)
| |--- posts approval_token to #dev-home-wsl
| | | |
| |--- approval_token --->| |
| | |--- verifies token |
| | |--- executes |
| |<--- response ---------| |
|<-- output in #dev-home-wsl |
```
### 6.2 Approval token payload
```json
{
"protocol_version": "mesh/1",
"purpose": "approval",
"request_id": "req_01J9XYZABCDEF",
"manifest_id": "manifest_home-wsl_v3",
"capability": "shell.exec",
"args_hash": "a3f5...09bc",
"approver": "egutierrez@aurgi.com",
"approved_at": 1748131245,
"expires_at": 1748131305,
"nonce": "Yk9p6Xs_3hZQk4mB7lWcvA"
}
```
`args_hash` debe coincidir con `sha256_hex(json_canonical(args))` del request original — evita que el operador apruebe `ls /tmp` y el bot reemplace por `rm -rf /tmp`.
### 6.3 Wire format
Igual que enrollment token: `base64url(payload) + "." + base64url(signature)`, domain separator `"mesh/1/approval\n"`.
### 6.4 Captura por agents_and_robots
`agents_and_robots` corre como bot Matrix con la operator key cargada (a traves de `pass operator/ed25519` montado en `/etc/agents_and_robots/operator.key` con permisos 400, owned by service user). Cuando detecta:
- Reaccion `m.reaction` con key `👍` (U+1F44D) sobre el evento `approval_request` en `#operator-approvals`, **emitida por el matrix_id del operador** (configurado en `apps/agents_and_robots/config.yaml::operator_matrix_id`).
- O mensaje `!approve <request_id>` en `#operator-approvals` desde el mismo matrix_id.
Entonces firma el approval token y lo envia a device_agent (via Matrix event `m.capability.approval` en el room del device).
### 6.5 Timeout
Si device_agent no recibe approval token en 60s tras enviar approval_request, responde al room con error `approval_timeout`. El operador puede re-emitir el comando original (genera nuevo `request_id`, nuevo nonce, nueva approval).
### 6.6 Approval denegada
Reaccion `👎` (U+1F44E) o comando `!deny <request_id>` → bot firma un `approval_denied_token` (mismo formato + `denied=true`). device_agent responde `approval_denied`.
### 6.7 Verificacion en device_agent
```go
// Pseudo
func VerifyApproval(token string, req Request, pubkey []byte) error {
payload, err := decodeApprovalToken(token, pubkey)
if err != nil { return err }
if payload.RequestID != req.RequestID { return ErrApprovalMismatch }
if payload.Capability != req.Capability { return ErrApprovalMismatch }
if payload.ArgsHash != sha256Hex(canonicalJSON(req.Args)) { return ErrApprovalMismatch }
if payload.ExpiresAt < time.Now().Unix() { return ErrApprovalExpired }
if payload.Denied { return ErrApprovalDenied }
return nil
}
```
---
## 7. Audit log hash chain
Append-only log local a cada device_agent con hash chain que detecta tampering. Replicado periodicamente al hub para archivo tamper-evident off-device.
### 7.1 Schema (`apps/device_agent/audit.db`)
```sql
CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts INTEGER NOT NULL,
request_id TEXT NOT NULL,
manifest_id TEXT NOT NULL,
capability TEXT NOT NULL,
args_hash TEXT NOT NULL,
approval_id TEXT, -- nullable, request_id del approval token usado
exit_code INTEGER, -- nullable mientras no haya respuesta
ok INTEGER NOT NULL, -- 0/1
error_code TEXT, -- nullable
duration_ms INTEGER NOT NULL,
prev_hash TEXT NOT NULL, -- hex 64 chars
this_hash TEXT NOT NULL, -- hex 64 chars
UNIQUE(request_id)
);
CREATE INDEX IF NOT EXISTS idx_audit_log_ts ON audit_log(ts);
```
Migracion vive en `apps/device_agent/migrations/001_init.sql`. Regla `db_migrations.md`.
### 7.2 Hash chain
```
record_canonical = ts + "|" + request_id + "|" + manifest_id + "|" +
capability + "|" + args_hash + "|" + approval_id_or_empty + "|" +
exit_code_str + "|" + ok_str + "|" + error_code_or_empty + "|" +
duration_ms_str
this_hash = sha256_hex(prev_hash + "\n" + record_canonical)
```
Para el primer registro `prev_hash = "0000...0000"` (64 zeros).
`wg_peer_revoke_go_infra` (ya existente) hace algo similar para revocations; este spec usa el mismo patron para todas las invocaciones.
### 7.3 Append helper
```go
// device_audit_append_go_infra (issue 0135)
func AppendAudit(db *sql.DB, rec Record) (string, error) {
var prev string
err := db.QueryRow(`SELECT this_hash FROM audit_log ORDER BY id DESC LIMIT 1`).Scan(&prev)
if err == sql.ErrNoRows {
prev = strings.Repeat("0", 64)
} else if err != nil {
return "", err
}
canonical := canonicalRecord(rec)
h := sha256.Sum256([]byte(prev + "\n" + canonical))
this := hex.EncodeToString(h[:])
_, err = db.Exec(`INSERT INTO audit_log(...) VALUES(...)`, /* fields, prev, this */)
if err != nil { return "", err }
return this, nil
}
```
Transaccion con `BEGIN IMMEDIATE` para evitar carrera entre prev_hash select y insert.
### 7.4 Verificacion (cualquiera con copia del db)
```go
// device_audit_verify_go_infra (issue 0135)
func VerifyChain(db *sql.DB) error {
rows, _ := db.Query(`SELECT id, prev_hash, this_hash, /* fields */ FROM audit_log ORDER BY id`)
expected := strings.Repeat("0", 64)
for rows.Next() {
var rec Record
rows.Scan(&rec.ID, &rec.PrevHash, &rec.ThisHash /* ... */)
if rec.PrevHash != expected { return fmt.Errorf("chain broken at id %d", rec.ID) }
canonical := canonicalRecord(rec)
h := sha256.Sum256([]byte(rec.PrevHash + "\n" + canonical))
if hex.EncodeToString(h[:]) != rec.ThisHash {
return fmt.Errorf("hash mismatch at id %d", rec.ID)
}
expected = rec.ThisHash
}
return nil
}
```
### 7.5 Replicacion al hub
Cada 60s device_agent hace `POST /audit/replicate` al hub con el bloque de registros nuevos (delta sobre el ultimo replicado). El hub valida la cadena, anade su propio `replicated_at`, y almacena en `apps/wg_hub/operations.db::device_audit` (tabla espejo + meta `last_replicated_id` por device).
Si el hub detecta `chain_broken` o `hash_mismatch`, emite evento a `#operator-approvals` con severity=critical y marca device como `status='compromised'` en `wg_peers`.
---
## 8. Room ↔ device mapping
### 8.1 Schema (`apps/agents_and_robots/operations.db`)
```sql
CREATE TABLE IF NOT EXISTS room_devices (
room_id TEXT PRIMARY KEY, -- !abc123:organic-machine.com
device_id TEXT NOT NULL,
manifest_id TEXT NOT NULL,
role TEXT NOT NULL, -- 'device' | 'container' | 'approval' | 'broadcast'
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
active INTEGER NOT NULL DEFAULT 1
);
CREATE INDEX IF NOT EXISTS idx_room_devices_device_id ON room_devices(device_id);
CREATE INDEX IF NOT EXISTS idx_room_devices_role ON room_devices(role);
```
Migracion: `apps/agents_and_robots/migrations/NNN_room_devices.sql`.
### 8.2 Roles especiales
- `role='approval'`: hay exactamente UN room con este rol, default alias `#operator-approvals:organic-machine.com`. Bot publica `approval_request` aqui y escucha reacciones del operador.
- `role='broadcast'`: alias `#operator-broadcast:organic-machine.com`. Bot publica eventos de control firmados (revocations, manifest rotations).
- `role='device'`: room 1:1 por device. Alias por convencion `#dev-<device_id>:organic-machine.com`.
- `role='container'`: para modo "deep" docker (containers como peers WG). Alias `#cont-<container_id>:organic-machine.com`.
### 8.3 Resolucion al dispatchar
Cuando el bot recibe un `!cmd` en cualquier room:
1. Busca `SELECT device_id, manifest_id, role FROM room_devices WHERE room_id=? AND active=1`.
2. Si no existe → ignora (o responde "this room is not bound to a device").
3. Si `role='device'`, dispatcha al `device_agent` correspondiente.
4. Si `role='approval'` o `role='broadcast'`, NO acepta `!exec`/`!fs.*`/`!docker.*` — solo `!approve`, `!deny`, `!revoke`, `!help`.
---
## 9. Element commands
Comandos que el bot de `agents_and_robots` parsea y traduce a envelopes. Estos viven en rooms `role='device'` o `role='container'` (excepto `!approve`/`!deny` que viven en `role='approval'`).
| Command | Capability | Args | Notas |
|---|---|---|---|
| `!help` | (meta) | none | Bot responde con capability matrix del manifest del device del room |
| `!exec <argv...>` | `shell.exec` | `{argv: [...], cwd?: string}` | argv splits por shlex, sin shell wrapping |
| `!fs.read <path> [bytes]` | `fs.read` | `{path, max_bytes?}` | default max_bytes = manifest.max_bytes |
| `!fs.write <path> <<<content` | `fs.write` | `{path, content_base64}` | content viene en heredoc o quoted; bot codifica base64 |
| `!fs.ls <path>` | `fs.list` | `{path}` | output: array de {name,type,size} |
| `!docker exec <container> <argv...>` | `docker.container.exec` | `{container, argv}` | container debe estar en `containers_allowed` |
| `!docker logs <container> [tail]` | `docker.container.logs` | `{container, tail?, follow?}` | `--follow` activa SSE |
| `!docker ps` | `docker.container.list` | `{}` | output: tabla containers vivos |
| `!approve <req_id>` | (meta) | `{request_id}` | solo en `role='approval'`, solo del operator_matrix_id |
| `!deny <req_id>` | (meta) | `{request_id, reason?}` | idem |
| `!revoke <device_id>` | (meta) | `{device_id, reason?}` | solo del operator_matrix_id; emite `manifest_revoked` + `wg_peer_revoke` |
| `!status` | (meta) | none | bot responde: device IP WG, last_handshake, manifest_id activo, capabilities count |
### 9.1 Parsing rules
- Lexer shlex-style (`shlex.split` Python o equivalente). Quoted strings respetan espacios.
- Si parsing falla → `!help` corto en la misma linea + abort.
- Args desconocidos para una capability → `manifest_invalid` con `details.unknown_args: [...]`.
### 9.2 Output rendering
El bot formatea responses para Matrix:
- `ok=true`, output corto (<2KB): formatted text con `<pre><code>...</code></pre>` (Matrix `formatted_body`).
- `ok=true`, output largo: trim a 2KB + link al artifact subido (Matrix media repo si el homeserver lo permite, sino paste a `paste.organic-machine.com`).
- `ok=false`: render como `[ERROR error_code] message\n<details>` con codigo de color rojo en clientes que soportan colored text.
---
## 10. Error model
Todos los `error.code` son strings snake_case. El cliente NO debe parsear `message` — solo `code`. `details` es objeto libre por code.
| code | meaning | details fields | retry? |
|---|---|---|---|
| `manifest_invalid` | Manifest no firma, expirado, device_id mismatch, o no tiene la capability | `reason`, `manifest_id`, `expires_at?` | no — pedir manifest nuevo |
| `capability_denied` | Capability esta en manifest pero los args violan constraints | `constraint_violated`, `value` | no — ajustar args |
| `binary_not_whitelisted` | shell.exec con binario fuera de `binaries_whitelist` | `binary`, `whitelist` | no |
| `path_not_allowed` | fs.* con path fuera de `paths_allowed` o en `paths_denied` | `path`, `allowed_globs`, `denied_globs` | no |
| `container_not_allowed` | docker.* sobre container fuera de `containers_allowed` | `container`, `allowed_list` | no |
| `approval_timeout` | requires_approval=true y no llego token en 60s | `waited_s` | si — re-enviar |
| `approval_denied` | operador denego | `approver`, `reason?` | no |
| `approval_mismatch` | approval token args_hash != request args_hash | `expected_hash`, `got_hash` | no — posible MITM |
| `nonce_replay` | nonce ya visto en ventana TTL=300s | `nonce`, `first_seen_at` | no — generar nonce nuevo |
| `signature_invalid` | firma ed25519 no verifica | `reason` (e.g. `clock_skew`, `bad_pubkey`, `corrupted`) | no |
| `token_expired` | enrollment o approval token expirado | `expires_at`, `now` | no |
| `token_consumed` | enrollment token ya usado (nonce en `wg_enrollment_tokens`) | `first_use_at` | no |
| `device_revoked` | device esta en revocation list | `revoked_at`, `reason` | no |
| `capability_not_found` | capability name no existe en device_agent | `name`, `available` | no |
| `execution_failed` | la capability ejecuto y devolvio exit != 0 | `exit_code`, `stderr` (trimmed) | depende — semantica de la capability |
| `output_too_large` | output > `max_output_bytes` | `bytes`, `limit` | no — pedir con tail/head |
| `duration_exceeded` | timeout `max_duration_s` excedido | `limit_s`, `killed_signal` | no |
| `transport_error` | error de Matrix/HTTP transport debajo del envelope | `transport`, `inner_error` | si con backoff |
| `internal` | bug en device_agent/hub/bot; NO leakear stack al room | `incident_id` | no — operator debe ver logs |
### 10.1 Mapping a HTTP status (transport HTTP intra-mesh)
| code | HTTP |
|---|---|
| `manifest_invalid`, `signature_invalid`, `token_*` | 401 |
| `capability_denied`, `binary_not_whitelisted`, `path_not_allowed`, `container_not_allowed`, `device_revoked` | 403 |
| `capability_not_found` | 404 |
| `nonce_replay`, `approval_mismatch` | 409 |
| `approval_timeout`, `duration_exceeded` | 408 |
| `output_too_large` | 413 |
| `approval_denied` | 403 |
| `execution_failed` | 200 (con ok=false, exit_code en body) |
| `transport_error`, `internal` | 500 |
Matrix transport ignora HTTP status — el `ok=false` y `error.code` son suficientes.
---
## 11. Security threat model
Top 10 ataques + mitigaciones. Listadas por probabilidad x impacto. Cada item refiere al control que lo mitiga.
### T1. Operator ed25519 key leak
- **Attack**: laptop comprometida, `pass operator/ed25519` exfiltrado.
- **Impact**: atacante firma manifests + approvals — control total de devices.
- **Mitigation**:
- GPG-encrypted at rest via `pass` (depende de GPG subkey).
- Rotacion forzada: `!revoke-all` en `#operator-broadcast` → todos los devices reciben `manifest_revoked` con `device_id="*"` → entran en modo enroll.
- Hardware-backed key (YubiKey, ssh-agent con touch policy) — out of scope v1, candidato a issue futuro.
- Detection: hub registra todas las firmas (mensaje `signed_by_operator_at`); operador revisa diariamente.
### T2. Compromised device executes unauthorized capabilities
- **Attack**: device_agent comprometido, atacante quiere ejecutar capabilities fuera del manifest.
- **Impact**: limitado a las capabilities del manifest (asumiendo verify es correcto).
- **Mitigation**:
- Manifest verify obligatorio antes de cada request (§2.5).
- Sandbox: `firejail` (Linux) o equivalente — ver issue 0140.
- Whitelist binarios + paths + containers (§2).
- Audit chain replicado a hub (§7.5) — atacante no puede borrar audit.
### T3. Replay attack
- **Attack**: atacante captura request firmado valido (de un log, de Matrix federation leak), lo reenvia.
- **Impact**: capability ejecutada 2x.
- **Mitigation**: §5. Nonce TTL=300s + ts window 60s. SQLite UNIQUE constraint.
### T4. MITM despite WireGuard
- **Attack**: alguien dentro del mesh WG (otro device comprometido) intercepta requests entre bot y device.
- **Impact**: leer args/output; modificar args si firma se ignora.
- **Mitigation**:
- HTTP intra-mesh sobre TLS (cert auto-firmado mesh-only, pinned).
- Matrix transport: E2EE via Olm/Megolm — bot debe verificar device keys del room antes de aceptar.
- Firma del envelope (§1.3) — args modificados → `signature_invalid`.
### T5. Container escape
- **Attack**: container con `docker.container.exec` activado escapa a host.
- **Impact**: host comprometido, no mas que T2.
- **Mitigation**:
- `binaries_whitelist` estricta en `docker.container.exec` (sin `bash`, `sh`, `nsenter`, `unshare`).
- Modo "deep" (container con WG-peer propio) solo para containers de propia infra (`agents_and_robots`, `registry_api`).
- Docker socket NUNCA expuesto via capability (capability solo via `docker_container_exec_go_infra` que NO usa `--privileged` en exec).
- Detection: container con syscalls anomalas → logged por seccomp profile (out of scope v1).
### T6. Malicious manifest with crafted globs
- **Attack**: operador firma manifest con `paths_allowed: ["/home/lucas/**"]` pero device_agent tiene bug en glob matcher que permite `..` traversal.
- **Impact**: fs.read fuera del directorio.
- **Mitigation**:
- Implementacion glob: `filepath.Match` Go + canonicalizar path con `filepath.Clean` + verificar `strings.HasPrefix(cleaned, allowed_prefix)`.
- Reject paths con `..` antes de glob match.
- Test suite con corpus de path traversal (`../../etc/passwd`, `/home/lucas/../etc/passwd`, symlinks).
### T7. Enrollment token theft
- **Attack**: token QR fotografiado por tercero.
- **Impact**: tercero hace POST /enroll con su propia WG pubkey → device fantasma en la mesh.
- **Mitigation**:
- TTL=600s.
- Single-use (nonce consumed en hub).
- Operador recibe alerta en `#operator-approvals` cada vez que un device hace POST /enroll exitoso — si no esperabas un enroll, `!revoke` inmediato.
### T8. Matrix homeserver compromise
- **Attack**: atacante root en `organic-machine.com` modifica eventos Matrix.
- **Impact**: si E2EE roto, todo el contenido leak. Si E2EE OK, solo metadata.
- **Mitigation**:
- Megolm E2EE entre operador y bot — keys nunca en disco del homeserver.
- Envelope firmado (§1.3) — atacante no puede inyectar requests sin operator key.
- Hub WG segregado: `wg_hub` corre en mismo VPS pero NO confia en Matrix para autorizacion (solo para transport).
### T9. Clock skew abuse
- **Attack**: device con clock muy adelantado firma requests con `ts` futuro grande, los almacena, los reenvia cuando `ts` cae en ventana.
- **Impact**: replay extendido mas alla de TTL.
- **Mitigation**:
- Ventana ts: `[now-60, now+30]` — clock skew tolerado pequeño.
- Devices forzados a sync NTP (chrony) — el provision check verifica `chronyc tracking` reporta `Leap status: Normal`.
- Hub alerta a `#operator-approvals` si recibe replicacion de audit con `ts` que difiere >30s del `received_at`.
### T10. Denial of service via approval flooding
- **Attack**: atacante con manifest valido pero capabilities limitadas spam `requires_approval=true` requests para inundar `#operator-approvals`.
- **Impact**: operador pierde signal en noise; legitimo approval enterrado.
- **Mitigation**:
- Rate limit en agents_and_robots: por device_id, max 10 approval_requests / 5min.
- Excedente → silently dropped + audit entry + `#operator-approvals` recibe resumen agregado (`device home-wsl: 47 approval requests in 5min, throttled`).
- Si pattern repetido, operador `!revoke home-wsl`.
---
## 12. Implementation order
Las issues 0135-0143 implementan lo definido en este spec. Dependencias y orden:
| # | Issue | Que entrega | Depende de | Bloquea |
|---|---|---|---|---|
| 0135 | capability manifest sign/verify funcs | `capability_manifest_sign_go_infra`, `capability_manifest_verify_go_infra`, `enrollment_token_create_go_infra`, `enrollment_token_verify_go_infra`, `device_audit_append_go_infra`, `device_audit_verify_go_infra`, `operator_keygen_bash_infra` | 0134 (spec) | 0136, 0137, 0140, 0142 |
| 0136 | provision_wg_hub pipeline | `provision_wg_hub_bash_pipelines` que compone las 9 funciones `wg_*` (ver flow 0009 Fase C) | 0135 (audit funcs solamente; resto independiente) | 0137 |
| 0137 | wg_hub Go service | `apps/wg_hub/` con endpoints `POST /enroll`, `GET /peers`, `POST /peers/:id/revoke`, `POST /audit/replicate`, SSE `/events` | 0135, 0136 | 0138, 0139 |
| 0138 | agents_dashboard Mesh panel | Panel ImGui en `apps/agents_dashboard/` con lista de peers, last_handshake, bytes rx/tx, approval queue, boton revoke | 0137 | — |
| 0139 | enroll_device pipeline | `enroll_device_bash_pipelines` que el operador corre en su laptop para enroll un device nuevo (genera token, lo muestra como QR, hace POST /enroll en nombre del device si tiene SSH) | 0135, 0137 | 0140, 0141 |
| 0140 | device_agent Go binary | `apps/device_agent/` — Matrix client + capability dispatcher + sandbox firejail + audit chain. Cross-compile linux/amd64, linux/arm64, windows/amd64 | 0135, 0139 | 0142 |
| 0141 | Android Termux variant | `apps/device_agent_android/` — variante para Termux con WG via wg-go (userspace) y capabilities limitadas (no firejail) | 0140 | — |
| 0142 | Matrix bot dispatcher routes | Extender `apps/agents_and_robots/` con dispatcher `m.capability.*` → device, parse de comandos §9, room_devices table | 0135, 0137, 0140 | 0143 |
| 0143 | Operator approval flow | Capturar reactions en `#operator-approvals`, firmar approval tokens, enviar a device, registrar timeout. En `apps/agents_and_robots/` | 0142 | — |
### 12.1 Paralelismo
- 0135 secuencial (todo lo demas depende).
- 0136 + 0140 paralelos tras 0135.
- 0137 espera 0136 (necesita las funciones `wg_*`).
- 0138 + 0139 + 0140 paralelos tras 0137.
- 0141 tras 0140.
- 0142 + 0143 ultimo bloque.
Skill `parallel-fix-issues` puede orquestar 0136/0140 y 0138/0139 en worktrees aislados (ojo: 0140 crea sub-repo `apps/device_agent/`, requiere `git init` dentro como dice `apps_subrepo.md`).
### 12.2 Acceptance gate para cerrar 0134
- [ ] Este documento mergeado en `dev/issues/`.
- [ ] Issues 0135-0143 creados con frontmatter coherente (`dependencies` apuntando aqui, `related_flows: [0009]`).
- [ ] Capabilities groups `wireguard`, `device-agent`, `docker-agent` con stubs en `docs/capabilities/` referenciando este spec.
- [ ] No changes en wire format hasta que todos los issues 0135-0143 cierren — cambios posteriores requieren nuevo issue + bump `protocol_version`.
---
## Notas
### Wire format evolution policy
`protocol_version: mesh/1` es immutable durante el ciclo de vida de las issues 0135-0143. Cualquier cambio breaking (renombrar campo, cambiar canonical bytes, anadir campo obligatorio) requiere bump a `mesh/2` con un issue nuevo que documente migracion y compat layer.
Cambios non-breaking aceptados sin bump:
- Anadir nuevos `error.code` (clientes los manejan via fallback a `internal`).
- Anadir nuevas capabilities (devices viejos las rechazan con `capability_not_found`).
- Anadir campos opcionales con default backwards-compatible.
### Test corpus
Cada funcion de §3 y §4 entrega test fixtures en `cpp/functions/*/testdata/` o equivalente:
- `manifest_valid.json` + `manifest_valid.sig` — par validable.
- `manifest_expired.json` — para test de §2.5 paso 4.
- `manifest_tampered.json` + sig original — para test de signature_invalid.
- `enroll_token_valid.txt`, `enroll_token_expired.txt`, `enroll_token_wrong_purpose.txt`.
- `path_traversal_corpus.txt` con 50+ paths maliciosos para test T6.
### Capability groups stubs
Como parte de este issue se crean stubs minimos en:
- `docs/capabilities/wireguard.md` — lista de las 9 funciones `wg_*` (referenciadas desde flow 0009).
- `docs/capabilities/device-agent.md` — capability dispatcher + sandbox + audit chain.
- `docs/capabilities/docker-agent.md` — capabilities sobre containers.
Cada uno con seccion `## Ejemplo canonico` + `## Fronteras` segun regla `capability_groups.md`. Los stubs se llenan a medida que las funciones se crean en 0135-0142.
### Observabilidad
- Cada envelope request/response loggea en `call_monitor.calls` con `function_id = capability_<name>_<lang>_<domain>` cuando la implementacion exista. Si la capability es solo metadata (`!help`), no se loggea.
- `audit_log` (§7) es separado de `call_monitor` — el primero es tamper-evident del operator, el segundo es telemetria del agente Claude.
- Panel "Mesh" de `agents_dashboard` (issue 0138) consume:
- `wg_hub::wg_peers` (peers vivos + last_handshake + tx/rx).
- `wg_hub::device_audit` (replica de audit chains — para verificacion offline).
- `agents_and_robots::room_devices` (mapping rooms ↔ devices).
- `agents_and_robots::approvals_pending` (queue de approvals pendientes).
### Capability growth log
`v0.1.0 (2026-05-24)` — initial spec mesh/1.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,232 @@
---
id: "0146"
title: "add-pc one-shot: añade PC al mesh + agente LLM en <2min desde movil"
status: pending
priority: high
created: 2026-05-24
related_flows: ["0009"]
related_issues: ["0134", "0144", "0145"]
dependencies: []
tags: [mesh, wireguard, ssh, scaffolder, agents, llm, scaling, dx]
---
## Objetivo
Reducir de 8 pasos manuales (~15min) a **1 comando (<2min)** el flujo de añadir un PC nuevo al mesh con su propio agente LLM conversacional. Goal final: chatear desde Element movil con cualquier PC del usuario tras un `./fn run add_pc <name>`.
## Estado actual (post-0145)
Pipeline manual funcional pero verboso:
1. Instalar wireguard en PC nuevo.
2. wg_keygen.
3. wg_peer_add en hub (organic-machine.com).
4. wg_client_config + wg_client_install.
5. Build/scp device_agent binario.
6. Manifest YAML local.
7. systemd unit.
8. provision-agent-user.sh + edit launcher main.go + rebuild + restart agents_and_robots.
Solo agent-wsl-lucas existe. Bloqueado por friccion de pasos para escalar a aurgi-pc, windows-lucas, raspberry, etc.
## Vision
```
operador$ ./fn run add_pc aurgi-pc --via wg
[1/9] generating WG keypair...
[2/9] enrolling peer at hub (10.42.0.21)...
[3/9] cross-compiling device_agent for linux/amd64...
[4/9] uploading binary + manifest + systemd unit via SSH...
[5/9] starting WG + device_agent on remote...
[6/9] provisioning Matrix user @agent-aurgi-pc...
[7/9] generating agent config + system prompt...
[8/9] wiring launcher + rebuild...
[9/9] restarting agents_and_robots.service...
✓ agent-aurgi-pc live. Send a DM from your Matrix client.
```
Y para hosts sin posibilidad de instalar binary:
```
operador$ ./fn run add_pc customer-vps-01 --via ssh --ssh-alias customer-prod
✓ agent-customer-vps-01 live (ssh-backed). Send a DM.
```
## Arquitectura
Dos backends para un mismo UX:
### Backend A — WG + device_agent (mesh nativo)
- PC tiene WG client + binary device_agent corriendo.
- Comandos viajan VPS → WG → device_agent → exec local → audit chain LOCAL.
- 14 capabilities completas (fs.*, git.*, docker.*, pkg.*, proc.*, shell.exec, shell.eval).
- Para tus PCs (laptop, desktop, raspberry, mac, movil rooted).
### Backend B — SSH-only (sin binary remoto)
- PC tiene solo SSH server. VPS tiene SSH key autorizada.
- Comandos viajan VPS → ssh.Executor → exec remoto → audit en VPS.
- Tools reducidos: `ssh_exec(argv)`, `ssh_fs_read`, `ssh_fs_list`. Sin docker/git/pkg salvo wrapper.
- Para customer servers, VPS terceros, throwaway boxes.
LLM agent ve diferentes tool sets segun backend. Mismo system prompt template.
## Tareas
### Fase 1 — Pipeline `add_pc_wg_bash_pipelines`
1.1. Cross-compile device_agent matrices:
- `GOOS=linux GOARCH=amd64` (default)
- `GOOS=linux GOARCH=arm64` (raspberry pi4+, mac M-series via Linux)
- `GOOS=windows GOARCH=amd64`
- `GOOS=darwin GOARCH=arm64`
- Reusa `nohup` cgo-free build (swap mattn/go-sqlite3 → modernc.org/sqlite si no esta hecho ya).
- Output: `cpp/build/cross/device_agent.<os>-<arch>`.
1.2. Funcion `cross_compile_device_agent_bash_infra(target_os, target_arch)` que devuelve path al binario.
1.3. Funcion `add_pc_wg_bash_pipelines(name, ssh_alias, target_os?, target_arch?)`. Compone:
- wg_keygen_go_infra (hub side: priv hub + psk; client side: priv cliente)
- wg_peer_add_go_infra (en hub via SSH al VPS)
- wg_client_config_go_infra (genera client.conf)
- ensure_remote_wireguard_installed (SSH al target, apt/dnf install wireguard si falta)
- wg_client_install_bash_infra (en target via SSH push)
- cross_compile_device_agent_bash_infra (local)
- rsync_device_agent_bundle (binary + manifest template + systemd unit → target ~/.local/bin/ + ~/.config/device_agent/)
- start_device_agent_service (systemctl --user enable --now device_agent)
- provision_agent_user (ssh al VPS, ejecuta dev-scripts/agent/provision-agent-user.sh con --mode user)
- wire_launcher_import (edita cmd/launcher/main.go en VPS, anade blank import, git commit + rebuild + restart service)
- assert_dm_received (espera 30s a que el bot mande "hola" via notify-developer.sh)
1.4. Manifest template Y matrix per-OS: paths_allowed difieren (`/home/<user>/**` en Linux, `C:\Users\<user>\**` en Windows). Templates en `dev-scripts/agent/templates/manifest.<os>.yaml.tmpl`.
1.5. Idempotente: re-run con mismo name → no-op + verificar state. Si peer existe pero device_agent caido, restart.
1.6. Rollback: si paso N falla, deshacer 1..N-1. Estado parcial NO debe quedar (peer huerfano, Matrix user sin agent, etc).
### Fase 2 — Pipeline `add_pc_ssh_bash_pipelines` (backend B)
2.1. Funcion `ssh_exec_capability_go_infra` — wrapper que recibe `{argv, host}` y hace `ssh <host> -- <argv...>`. Whitelist binaries opcional. Audit en VPS (`apps/agents_and_robots/ssh_audit.db` o similar).
2.2. Funcion `ssh_fs_read_capability_go_infra`, `ssh_fs_list_capability_go_infra` (read-only, no write para evitar accidentes en customer boxes).
2.3. Tool registry adapter: cuando agent config tiene `device_mesh.backend: ssh`, el adapter no apunta a HTTP device_agent — apunta a las funciones `ssh_*` directamente. Mantener interface ToolRegistry pero swap implementation.
2.4. `add_pc_ssh_bash_pipelines(name, ssh_alias)` compone:
- assert_ssh_reachable (BatchMode yes connect test)
- provision_agent_user --mode user --backend ssh
- generate agent config con `device_mesh.backend: ssh, ssh_alias: <alias>`
- wire launcher + restart
NO toca el remote — solo VPS.
### Fase 3 — Cross-compile device_agent CGO-free
3.1. Swap mattn/go-sqlite3 (CGO) → modernc.org/sqlite (pure Go) en device_agent. Tests verde tras swap.
3.2. `cross_compile_device_agent_bash_infra` produce 4 binarios en <30s.
3.3. Bundle script `make-bundle.sh <os> <arch>` empaqueta zip con binario + manifest.template + systemd-unit/launchd-plist/Task-Scheduler.xml segun OS.
### Fase 4 — agents_dashboard "Add device" panel C++
4.1. Modal nuevo en panel "Devices" con:
- Input: nombre del PC.
- Dropdown backend: WG mesh / SSH-only.
- Si WG: SSH alias para upload + OS/arch detect via uname remote.
- Si SSH: solo alias.
- Boton "Add". Spawn pipeline en background. Stream logs en TextLog.
4.2. Grid de status: device_id, IP mesh, last handshake, capabilities count, last command ts, audit chain integrity.
4.3. Boton "Revoke" por device → llama wg_peer_revoke + deactivate Matrix user + remove launcher import + restart. Confirmacion doble.
### Fase 5 — Health monitor cron + alertas
5.1. Cron 5min `monitor_mesh_health_bash_pipelines`:
- wg_status → cada peer con last_handshake > 600s → mark stale.
- HTTP GET /health a cada device_agent IP del mesh → si falla → mark unreachable.
- verify_hash_chain por device → si rota → mark corrupted.
5.2. Alertas Matrix a `#operator-alerts` (room a crear) con mensaje formato:
```
[ALERT] device_id=aurgi-pc status=stale (handshake 8min ago)
[ALERT] device_id=home-wsl status=hash_chain_corrupted (id=47 broken)
```
5.3. Dashboard tab "Health" muestra el feed SSE.
### Fase 6 — Movil UX validation
6.1. Test en Element movil iOS/Android:
- Lista de rooms con 1 per device.
- Notifications activas → push cuando agent responde.
- Smoke tests de capabilities mas comunes via voice-to-text.
6.2. Documentar `docs/mobile-control.md` con flujo recomendado:
- Como agrupar rooms por device en Element favorites.
- Comandos comunes ("status", "deploy X", "que esta caido").
- Tiempos esperados (claude-code latency 3-5s + tool exec 0.1-2s).
## Aceptacion (DoD triada)
### Mecanica
- `./fn run add_pc <name> --via wg` exit 0 + agent live en <2min en wallclock.
- `./fn run add_pc <name> --via ssh` exit 0 + agent live en <30s.
- Tests unit + integration verde en `bash/functions/pipelines/add_pc_*`.
### Cobertura
- Smoke matrix: 4 target OS (linux/amd64, linux/arm64, windows/amd64, darwin/arm64) cada uno con add_pc_wg flujo end-to-end.
- Rollback: simular falla en paso 5 (binary upload corrupted) → assert estado limpio (no peer huerfano, no Matrix user, no entry en launcher).
- SSH backend: target solo con SSH + sin sudo → agent funciona con tools ssh_exec read-only.
- Anti-criterio A3 (heredado de 0009): tras add_pc, smoke real via Matrix → audit DB en device tiene entries reales (no bot hallucination).
### Vida util
- 5 PCs reales añadidos durante 7 dias.
- 0 revokes manuales por error de provision.
- Operador usa Element movil >=1 sesion/dia interactuando con >=2 devices distintos.
- Health monitor detecta peer caido en <10min (test con `wg-quick down` aleatorio).
### Anti-criterios
- Si add_pc deja estado parcial (peer en wg0 + no agent en launcher) → invalida.
- Si SSH backend ejecuta comandos sin audit en VPS → invalida (no fake "ssh OK" sin log).
- Si dashboard muestra device "online" pero ultimo handshake >24h → invalida (false positive grave).
## Sub-issues planificados
| ID | Titulo | Esfuerzo |
|---|---|---|
| 0146a | cross_compile_device_agent + CGO-free swap a modernc.org/sqlite | 2h ✅ |
| 0146b | add_pc_wg_bash_pipelines (Fase 1) | 4h |
| 0146c | add_pc_ssh_bash_pipelines + ssh_exec_capability (Fase 2) | 3h |
| 0146d | Bundle script multi-OS + manifest templates (Fase 3) | 2h |
| 0146e | agents_dashboard panel "Add device" + status grid (Fase 4) | 4h |
| 0146f | monitor_mesh_health pipeline + alertas Matrix (Fase 5) | 1.5h |
| 0146g | Movil UX doc + smoke real con 4 devices fisicos (Fase 6) | 1h+observacion 7d |
Total: ~17h dev + 7d observacion.
## Decisiones de diseño
1. **Pipeline en bash compose funciones del registry**, no codigo Go monolitico. Permite que cada paso sea trazable + reusable individualmente.
2. **modernc.org/sqlite** vs mattn/go-sqlite3: pure Go elimina CGO + cross-compile trivial. Performance es comparable (modernc benchmarks dentro del 10% para nuestro workload de audit append).
3. **Backend SSH NO replica el manifest enforcement remoto** — el manifest vive en VPS y filtra antes de SSH. Trade-off aceptable: SSH backend = "trust the VPS sudo enforcement". Para PCs propios usa WG backend.
4. **Cada device = un agent Matrix separado** (NO un agent multi-device). Razon: aislamiento blast radius + room por device = UX claro en Element + capability manifest distinto por device. Coste: mas Matrix users + mas claude-code subprocesses.
5. **NO usar Ansible/Terraform** para este flujo. Pipeline bash + funciones del registry es suficiente y evita la dep externa. Si crece a >50 PCs, reconsiderar.
## Riesgos
- **Cross-compile + CGO-free**: el swap a modernc puede romper audit en runtime si schemas no migran. Mitigar con test golden DB + WAL mode check.
- **Windows systemd equivalente**: Task Scheduler es feo. Considera nssm.exe para autostart fiable. Documentar bien en bundle.
- **SSH key trust amplification**: backend B requiere SSH agent del VPS confiable a TODOS los target hosts. Si VPS comprometido → todos los SSH targets caen. Reforzar con SSH key per-host + revocacion centralizada.
- **Mac iCloud signing**: device_agent.app necesitaria notarization para auto-launch en macOS reciente. Skip para POC, abordar si añadimos Mac al mesh real.
- **Movil notifications**: Element push depends on FCM (Android) / APNs (iOS). Sin push, el operador puede perderse approvals time-sensitive. Doc sobre alternativas (NTFY, Gotify).
## Notas
- **2026-05-24 — 0146a done**: swap mattn/go-sqlite3 → modernc.org/sqlite v1.50.1 (pure Go). 4 binarios cross-compile OK (linux-amd64 11MB, linux-arm64 10MB, windows-amd64 11MB, darwin-arm64 10MB), todos stripped + statically linked. Build script idempotente en `apps/device_agent/build_all.sh`. Self-test pass en linux-amd64 nativo. Quedan smoke tests reales en windows/darwin/arm cuando 0146b despliegue a peers fisicos.
- `./fn run add_pc` deberia llamar via mcp__registry__fn_run para que telemetria de issue 0085 quede registrada.
- Aprovechar 0144b provision-agent-user.sh que ya esta hecho — solo compone, no reescribe.
- Sub-issue 0146g (UX movil) cierra el flow 0009 completo al fin: "humano controla N maquinas desde movil".
- Si esto funciona, abrir issue 0147 para "voice control" — Element soporta voice messages; usar transcripcion (Whisper local en VPS) → inyectar como texto al agent.
@@ -0,0 +1,54 @@
---
id: "0147"
title: "matrix-client-pc scaffold: Wails + React+Mantine + login MAS"
status: pending
priority: high
created: 2026-05-24
related_flows: ["0010"]
related_issues: ["0148", "0162"]
dependencies: ["0162"]
tags: [matrix, wails, react, mantine, mas, oidc, scaffold]
---
## Objetivo
Crear el esqueleto de la app `projects/element_agents/apps/matrix_client_pc/` con Wails v2 (Go) + React+Vite+Mantine+`@fn_library` y dejar funcionando el login MAS OIDC contra `mas-...organic-machine.com`. Resultado: arrancar binario -> redirect navegador a MAS -> volver con token -> mostrar perfil del usuario.
## Tareas
1. `wails init -n matrix_client_pc -t react-ts` dentro de `projects/element_agents/apps/`.
2. Sub-repo Gitea: `git init -b master` + crear repo `dataforge/matrix_client_pc` + push inicial.
3. `app.md` con frontmatter (lang=go, framework=wails, tags incluyen `matrix` + `service`? — NO, es app cliente, sin tag service).
4. `go.mod` con deps: `wails/v2`, `mautrix-go`, `keyring`.
5. Reemplazar template frontend por React+Mantine+`@fn_library`. Symlink `frontend/src/fn_library` -> `../../../../../frontend/functions/ui/` (o copia si symlink no funciona en build).
6. Backend Go (`backend/`):
- `wails.json` con `bindings` para `MatrixService`.
- `MatrixService.Login() -> URL` (devuelve URL MAS OIDC).
- `MatrixService.HandleCallback(code) -> User`.
- `MatrixService.GetSession() -> *Session` (lee de keyring).
- `MatrixService.Logout()`.
7. Frontend React: layout `AppShell` Mantine, pagina `Login.tsx` con boton "Sign in with Matrix" -> abre URL MAS en navegador del SO.
8. Persistencia tokens en keyring SO (`github.com/zalando/go-keyring`).
9. Loopback HTTP local (`127.0.0.1:0`, puerto libre aleatorio) para recibir callback OIDC.
10. Test e2e basico: arrancar app, login con `@dev-pc:matrix-af2f3d.organic-machine.com`, ver perfil.
## Funciones del registry a crear (delegar a fn-constructor)
- `matrix_client_init_go_infra``mautrix.NewClient(homeserver, userID, accessToken) -> *Client, error`. Wrapper que configura SQLite store + crypto store.
- `mas_oidc_flow_go_infra``StartFlow(masURL) -> authURL, codeVerifier, state`. `ExchangeCode(code, codeVerifier) -> *Token`.
- `keyring_save_token_go_infra` / `keyring_load_token_go_infra` — wrappers `go-keyring`.
## Acceptance
- [ ] Binario Wails compila para linux/amd64 + windows/amd64.
- [ ] `wails dev` arranca con hot-reload.
- [ ] Login MAS OIDC end-to-end: boton -> navegador -> consent -> callback -> perfil visible.
- [ ] Token persistido entre re-arranques (no re-login si token vigente).
- [ ] `app.md` con `uses_functions` que apunta a las 3 funciones nuevas.
- [ ] Sub-repo `dataforge/matrix_client_pc` creado con commit inicial.
## Notas
- MAS URL: leerla de `.well-known/matrix/client` del homeserver para no hardcodear.
- Refresh token: MAS usa OAuth 2.0 estandar — implementar refresh proactivo (~5min antes de expiry).
- Gotcha: en Windows, `wails dev` requiere WebView2 instalado.
@@ -0,0 +1,57 @@
---
id: "0148"
title: "matrix-client-pc rooms list + timeline con sync incremental"
status: pending
priority: high
created: 2026-05-24
related_flows: ["0010"]
related_issues: ["0147", "0149"]
dependencies: ["0147"]
tags: [matrix, sync, timeline, rooms, react, mantine, sse]
---
## Objetivo
Sidebar con rooms (DMs + spaces + grupos) + panel central con timeline del room activo. Sync incremental con Synapse via long-poll `/sync`. Stream eventos backend -> frontend via SSE (`http_sse_server_go_infra`). Pagination scroll-up (cargar mensajes anteriores). Optimistic UI al enviar.
## Tareas
1. Backend Go:
- `MatrixService.StartSync()` — long-poll `/sync` con since token persistido.
- `MatrixService.SubscribeEvents() -> chan Event` — broadcaster events a frontend.
- SSE endpoint `http://127.0.0.1:<puerto>/events` (autenticado con cookie session local).
- Persistir state en SQLite (`store.db`): rooms, members, last_event_id por room.
2. Frontend React:
- Hook `useMatrixRooms()` — devuelve `Room[]` ordenadas por last_activity.
- Hook `useMatrixTimeline(roomId, limit=50)` — devuelve eventos + `loadMore()`.
- Componente `RoomList` (sidebar con avatar, nombre, last_msg preview, unread badge).
- Componente `Timeline` con `react-virtuoso` para scroll perf con miles de msgs.
- Componente `EventBubble` (text, image, file, redacted, reaction agregada).
- Reconnect automatico si SSE/sync cae (exponential backoff).
3. Tests:
- `e2e/test_sync_basic.sh` — login + verificar que 3 rooms aparecen en sidebar.
- `e2e/test_pagination.sh` — scroll-up carga mensajes anteriores sin gap.
## Funciones del registry a crear
- `matrix_room_subscribe_go_infra` — SSE wrapper: subscribe events de Synapse y push a clientes.
- `useMatrixTimeline_ts_ui` — hook React con dedupe + pagination + optimistic.
- `useMatrixRooms_ts_ui` — hook React rooms list.
- `RoomList_ts_ui` — componente sidebar Mantine.
- `EventBubble_ts_ui` — componente burbuja msg.
## Acceptance
- [ ] Sidebar lista rooms del usuario test, ordenados por actividad.
- [ ] Click en room muestra timeline ultimos 50 msgs.
- [ ] Scroll arriba carga msgs anteriores sin duplicar.
- [ ] Mensaje enviado desde Element Web aparece en <2s en la timeline.
- [ ] Cerrar app + abrir: state restaurado desde SQLite, no re-sync completo.
- [ ] Network kill + restore: sync se reanuda sin perder mensajes.
## Notas
- DMs vs rooms grupales: detectar via `m.direct` account data.
- Spaces (`m.space`): mostrar como grupos colapsables en sidebar.
- Edits + redactions: aplicar in-place, no duplicar bubble.
- Read receipts: TBD en otro issue, no bloquea este.
@@ -0,0 +1,60 @@
---
id: "0149"
title: "matrix-client-pc composer: markdown, reply, edit, reactions, media"
status: pending
priority: high
created: 2026-05-24
related_flows: ["0010"]
related_issues: ["0148", "0150"]
dependencies: ["0148"]
tags: [matrix, composer, markdown, media, reactions, threads]
---
## Objetivo
Composer del room: markdown rendering, replies con quote, edits, reactions emoji, threads (Matrix MSC3440), upload de media (imagenes, files, voice msg). Drag&drop archivos. Slash commands placeholder (`/me`, `/shrug`, `/widget` — este ultimo para issue 0152).
## Tareas
1. Backend Go:
- `MatrixService.SendMessage(roomID, body, format)` — text + markdown -> HTML via `goldmark`.
- `MatrixService.SendReply(roomID, parentEventID, body)`.
- `MatrixService.EditMessage(roomID, eventID, newBody)`.
- `MatrixService.SendReaction(roomID, eventID, key)`.
- `MatrixService.UploadMedia(roomID, filePath) -> mxc://`.
- `MatrixService.SendThreadReply(roomID, threadRootID, body)`.
2. Frontend React:
- Componente `Composer` con Mantine `Textarea` + toolbar markdown.
- Hotkeys: Cmd+B/I/K, Cmd+Enter para enviar, Esc cancel edit.
- Drag&drop zone over Composer + paste image desde clipboard.
- `EmojiPicker` (reusar `@emoji-mart/react` o componente propio `@fn_library`).
- `ReactionBar` debajo de EventBubble con aggregates.
- Thread panel lateral (abrir click en evento "X replies").
- Voice messages: graba con `MediaRecorder` (opus codec), upload + send con `org.matrix.msc3245.voice` flag.
3. Tests:
- `e2e/test_send_markdown.sh``**bold**` aparece negrita en otro cliente.
- `e2e/test_edit_message.sh` — edicion aparece in-place en Element Web.
- `e2e/test_reaction.sh` — reaccion emoji propagada bidireccional.
## Funciones del registry a crear
- `markdown_to_matrix_html_go_core``goldmark` con sanitizer Matrix-compatible.
- `Composer_ts_ui` — componente Mantine + dropzone.
- `EmojiPicker_ts_ui` — wrapper picker emoji.
- `ReactionBar_ts_ui` — componente reactions aggregadas.
## Acceptance
- [ ] Mensaje markdown `**negrita** _cursiva_` se ve formateado en Element Web.
- [ ] Reply quote aparece referenciando el msg padre.
- [ ] Edit cambia el msg in-place en ambos clientes.
- [ ] Reaccion emoji con click aparece como counter agregado.
- [ ] Upload imagen (PNG 2MB) se ve thumbnail + click abre full.
- [ ] Voice msg grabado 5s reproduce OK en Element Web.
- [ ] Thread: 5 replies anidados se muestran en panel lateral.
## Notas
- Sanitizer HTML: usar allowlist Matrix (b, i, em, strong, a[href], code, pre, blockquote, ul, ol, li, br, p, h1-h6). NO permitir `<script>`, `<iframe>`, event handlers.
- mxc:// uploads: validar size limit (Synapse default 50MB).
- Voice msg: encode opus 32kbps, max 5min.
+73
View File
@@ -0,0 +1,73 @@
---
id: "0150"
title: "matrix-client-pc E2EE: cross-signing, SAS verification, recovery"
status: pending
priority: critical
created: 2026-05-24
related_flows: ["0010"]
related_issues: ["0149", "0151"]
dependencies: ["0149"]
tags: [matrix, e2ee, olm, megolm, cross-signing, recovery, security]
---
## Objetivo
Encriptacion end-to-end con `mautrix-go` (Olm/Megolm). Cross-signing keys (master/self-signing/user-signing), SAS verification de devices (emoji + decimal), recovery passphrase + key backup en Synapse, manejo de devices no verificados con warning visible. Mensajes en rooms encriptados se envian y descifran correctamente.
## Tareas
1. Backend Go:
- `MatrixService.BootstrapCrossSigning(passphrase)` — genera master/self/user keys, sube a Synapse cifradas con passphrase-derived key.
- `MatrixService.RecoverFromPassphrase(passphrase)` — descarga keys de Synapse y descifra.
- `MatrixService.StartVerification(userID, deviceID) -> *VerificationSession`.
- `MatrixService.VerifyEmoji(sessionID, accepted bool)`.
- `MatrixService.ListDevices() -> []Device` (con verified flag).
- `MatrixService.BackupMegolmKeys()` — key backup server-side.
- Crypto store SQLite separado del state store (mejor para integridad).
2. Frontend React:
- Wizard onboarding E2EE: pasos (1) generar passphrase, (2) backup, (3) verificar device.
- Panel `Settings > Security & Privacy`:
- Lista devices propios con verified state.
- Boton "Verify new device" + dialog SAS con emoji grid.
- "Reset cross-signing" (destructive, requiere confirmacion).
- "Restore from passphrase" (login en device nuevo).
- `EventBubble` muestra shield: green (verified), amber (encrypted, device unverified), red (decryption failed).
- Banner room: "X devices are not verified" si algun miembro tiene devices unverified.
3. Tests:
- `e2e/test_e2ee_send_receive.sh` — msg enviado en room encriptado se descifra en Element Web.
- `e2e/test_cross_signing.sh` — bootstrap + verificar device desde Element Web.
- `e2e/test_recovery.sh` — login en device nuevo + recover keys con passphrase.
- `e2e/test_unverified_warning.sh` — device nuevo aparece como warning en otros clientes.
## Funciones del registry a crear
- `matrix_e2ee_bootstrap_go_infra` — wrapper cross-signing bootstrap.
- `matrix_device_verify_go_infra` — SAS verification flow.
- `matrix_key_backup_go_infra` — server-side key backup wrapper.
- `passphrase_derive_key_go_infra` — PBKDF2/scrypt para derivar key de passphrase.
- `VerificationDialog_ts_ui` — componente emoji grid SAS.
## Acceptance
- [ ] Bootstrap cross-signing crea 3 keys + las sube a Synapse cifradas.
- [ ] Msg enviado a room encriptado se descifra en Element Web (y al reves).
- [ ] SAS verification con emoji grid funciona contra Element Web (ambos lados muestran 7 emojis iguales).
- [ ] Login en device nuevo + restore con passphrase recupera msgs historicos.
- [ ] Device no verificado dispara shield amber en EventBubble.
- [ ] Decryption failure (key no disponible) muestra shield rojo + boton "Request key".
## Notas
**Critico — anti-criterio:**
- NO marcar done si E2EE silent-falla (msg muestra "** Unable to decrypt **" sin shield rojo claro).
- NO marcar done si recovery passphrase queda en plain text en disco (debe vivir solo en keyring/memoria).
**Decisiones:**
- Olm/Megolm via `mautrix-go/crypto` (Go port estable de libolm).
- Alternativa rust-crypto via CGo: descartada, mantiene complejidad build.
- Passphrase format: 4 palabras Diceware o 12-byte base32. Usuario elige al bootstrap.
**Gotchas:**
- Key rotation: rooms encriptados rotan megolm cada 1 semana o 100 msgs (default). Manejar refresh.
- Olm sessions max 100 mensajes: rotar prekey bundles automaticamente.
- Cuando arrancas device nuevo sin passphrase, los msgs pre-existentes NO se descifran — UI debe ser clara.
@@ -0,0 +1,69 @@
---
id: "0151"
title: "matrix-client-pc calls LiveKit: 1:1 + grupales, mic/cam/screen"
status: pending
priority: high
created: 2026-05-24
related_flows: ["0010"]
related_issues: ["0150", "0152"]
dependencies: ["0150"]
tags: [matrix, livekit, calls, webrtc, video, audio, screen-share]
---
## Objetivo
Llamadas via LiveKit SFU (ya activo en `organic-machine.com:7880-7882`). Backend Go genera JWT con `livekit-server-sdk-go`. Frontend React usa `livekit-client` JS para join room, manejar tracks (mic/cam/screen), UI con tiles participantes, controles. Soporta 1:1 + grupales hasta 16 (limite config actual).
## Tareas
1. Backend Go:
- `MatrixService.RequestCallToken(matrixRoomID) -> (token, livekitRoomURL)`.
- Mapea Matrix roomID -> LiveKit room name (hash determinista).
- Genera JWT con claim `room`, `identity` (matrix userID), `ttl 30min`.
- Permisos: `canPublish=true, canSubscribe=true, canPublishData=true`.
- Publicar event Matrix `m.call.member` para sincronizar quien esta en call (MSC3401).
2. Frontend React:
- Hook `useLiveKitCall(matrixRoomID)`:
- Pide token al backend.
- Conecta `Room` de `livekit-client`.
- Expone participants, tracks, localTracks, state.
- Auto-publish microfono on connect (mute default).
- Componente `CallPanel`:
- Grid tiles participantes (1, 2, 4, 9, 16 layout).
- Tile principal con speaker activo (active-speaker detection del SDK).
- Controles bottom: mic, cam, screen share, raise hand, leave.
- PiP mode: cuando minimizado, tile flotante en esquina.
- Boton "Start call" en header del room (icono telefono).
- Boton "Join call" si hay call activa (segun `m.call.member` events).
- Notifs ring incoming call: audio + desktop notif.
3. Backend ICE/TURN:
- Verificar LiveKit config tiene TURN configurado (NAT traversal). Si no, anadir coturn container.
4. Tests:
- `e2e/test_call_1to1.sh` — 2 clientes (Wails + Element Web), 30s call, audio+video flow.
- `e2e/test_call_screen_share.sh` — compartir pantalla, otro cliente ve el track.
- `e2e/test_call_4_participants.sh` — 4 clientes simultaneos, no crash.
## Funciones del registry a crear
- `livekit_token_gen_go_infra` — JWT generator con `livekit-server-sdk-go`.
- `matrix_call_member_go_infra` — wrapper para publicar/leer `m.call.member` state events.
- `useLiveKitCall_ts_ui` — hook React.
- `CallPanel_ts_ui` — componente UI completo de call.
- `CallTile_ts_ui` — tile individual con video + nombre + speaker indicator.
## Acceptance
- [ ] Boton "Start call" en room DM con otro user.
- [ ] Otro cliente (Element Web) ve ring + acepta -> 2 tiles con video+audio.
- [ ] Mute mic + apagar cam funciona y se refleja en el otro lado.
- [ ] Screen share: tile separado aparece para todos los participantes.
- [ ] 4 participantes simultaneos sin crash ni audio cortado.
- [ ] Hangup limpia recursos (no tracks fantasma, no peer connections abiertas).
## Notas
- LiveKit room name: `sha256(matrix_room_id + secret)` truncado a 32 chars. Asi cualquier cliente que conozca el matrix_room_id puede computar el room name (no es secret).
- Token TTL 30min, refresh proactivo a los 25min.
- Codecs: H.264 + VP8 fallback para compatibilidad navegadores. Audio: Opus 32kbps.
- E2EE en calls: LiveKit soporta E2EE simetrico (insertable streams API). TBD para version posterior — flow inicial usa SRTP only (cifrado SFU<->client, no e2e).
- Sygnal push para incoming calls: enviar VoIP push con TTL bajo para wake-up moviles (relevante para issue 0158 Android).
@@ -0,0 +1,81 @@
---
id: "0152"
title: "matrix-client-pc mini-webapps embebidas: Matrix Widget API v2"
status: pending
priority: high
created: 2026-05-24
related_flows: ["0010"]
related_issues: ["0151", "0153"]
dependencies: ["0151"]
tags: [matrix, widgets, webapps, iframe, sandbox, agents, postmessage]
---
## Objetivo
Implementar host de widgets segun Matrix Widget API v2 (MSC2762, MSC2871, MSC2974). Cada room puede tener widgets activos publicados como state events `m.widget`. Los widgets son URLs cargadas en iframes sandboxed con bridge postMessage que da capabilities controladas (leer eventos del room, enviar eventos, mostrar UI overlay, etc.). Agentes de `agents_and_robots` pueden publicar widgets en sus rooms (ej. dashboard telemetria, formulario, kanban inline, panel de control del agente).
## Tareas
1. Backend Go:
- `MatrixService.ListWidgets(roomID) -> []Widget` — lee state events `m.widget` del room.
- `MatrixService.AddWidget(roomID, widget Widget)` — publica state event.
- `MatrixService.RemoveWidget(roomID, widgetID)`.
- `MatrixService.GenerateWidgetURL(widget Widget, userID) -> string` — substituye `$matrix_user_id`, `$matrix_room_id`, `$matrix_display_name`, `$matrix_avatar_url`, `$matrix_widget_id`, `$theme` en la URL del widget.
- Slash command `/widget <url>` handler en composer (issue 0149) que crea state event con widget temporal.
- `MatrixService.MintWidgetScopedToken(widgetID, userID) -> string` — token efimero con scope reducido (solo el room donde esta el widget).
2. Frontend React:
- Hook `useWidgets(roomID)` — lista widgets activos.
- Componente `WidgetPanel`:
- Tabs por widget activo + boton "+" para anadir.
- Cada widget en iframe con `sandbox="allow-scripts allow-same-origin allow-forms allow-popups-to-escape-sandbox"`.
- `iframe.referrerpolicy="no-referrer"`.
- CSP: `frame-src https: data: blob:`.
- `WidgetBridge` — clase JS que escucha `postMessage` del iframe e implementa Widget API v2:
- `capabilities` handshake: el widget declara que necesita, el host pide consentimiento usuario (dialog Mantine).
- `read_events`, `send_event`, `send_to_device`, `get_openid`, `m.always_on_screen`, etc.
- Whitelist estricta de capabilities concedidas. Audit log de mensajes en `store.db`.
- Layout: widgets se abren en panel lateral derecho (toggleable) o en modal fullscreen.
3. Widgets internos primer batch (proof of concept):
- `widget-jitsi-fallback` — si LiveKit falla, fallback a Jitsi via widget (URL config).
- `widget-agent-panel` — panel de control de agente: estado, ultima ejecucion, restart, view logs. Servido por `agents_and_robots` HTTP API (issue 0113 ya creando agent runner API).
- `widget-kanban` — kanban inline embebido para tasks del room. Reusa `apps/kanban` (Go) servido en LAN.
- `widget-issue-tracker` — widget que abre issue API (`0109m`).
4. Tests:
- `e2e/test_widget_capabilities.sh` — widget pide capability, dialog aparece, deniega/acepta funciona.
- `e2e/test_widget_send_event.sh` — widget con capability `send_event` envia msg al room.
- `e2e/test_widget_sandbox.sh` — widget malicioso (intenta `top.location =`) es bloqueado por sandbox.
## Funciones del registry a crear
- `matrix_widget_state_go_infra` — CRUD state events `m.widget`.
- `widget_url_template_go_core` — substituye placeholders en URL.
- `widget_token_mint_go_infra` — token scoped a un widget+room+user.
- `WidgetBridge_ts_ui` — clase postMessage bridge Widget API v2 completa.
- `WidgetPanel_ts_ui` — UI tabs + iframes + permisos.
- `CapabilityConsentDialog_ts_ui` — dialog Mantine para consentimiento.
## Acceptance
- [ ] `/widget https://my.app` crea state event y abre iframe.
- [ ] Widget declara capability `m.send_event` -> dialog Mantine pide consentimiento.
- [ ] Widget concedido envia msg al room que aparece en timeline.
- [ ] Widget malicioso `<script>top.location='evil.com'</script>` bloqueado por sandbox.
- [ ] `agents_and_robots` publica widget panel y se ve embebido en el room del agente.
- [ ] Widget kanban inline funciona: drag&drop card persiste en DB del kanban.
## Notas
**Anti-criterios:**
- NO permitir `javascript:` ni `data:text/html` URLs (XSS).
- NO conceder capabilities sin consentimiento explicito del usuario (auditable).
- NO compartir el access_token Matrix del usuario al widget — usar siempre tokens scoped efimeros.
**Decisiones:**
- Widget API v2 (no v1) — soporta capabilities + tokens scoped.
- iframe sandbox sin `allow-top-navigation` (previene escape).
- CSP `frame-src https:` + permitir `data:`/`blob:` solo para widgets internos firmados.
**Roadmap post-DoD:**
- Widget marketplace interno: `widget-catalog` en `agents_and_robots` con widgets internos descubribles.
- Widget templates: un agente publica un widget HTML estatico subido al room (`mxc://`) y el cliente lo renderiza desde la URL `mxc -> http`.
- Cross-room widgets: widget que persiste entre rooms (TBD, requiere MSC propio).
@@ -0,0 +1,61 @@
---
id: "0153"
title: "matrix-client-pc agent integration: paneles para rooms operados por agentes"
status: pending
priority: medium
created: 2026-05-24
related_flows: ["0010", "0009"]
related_issues: ["0152"]
dependencies: ["0152"]
tags: [matrix, agents, agents_and_robots, dashboard, sse, device_agent]
---
## Objetivo
Integracion nativa con `agents_and_robots` + `agents_dashboard` + futuro `device_agent` (flow 0009 mesh). Detectar que un room esta operado por un agente Matrix conocido (via state event custom `m.agent.metadata`) y mostrar panel lateral con info del agente: uptime, ultima ejecucion, cola de tasks, last_error, boton restart, view logs en vivo (SSE). Atajos: enviar slash commands del agente (`/agent restart`, `/agent skill <name>`).
## Tareas
1. Backend Go:
- `MatrixService.GetAgentMetadata(roomID) -> *AgentMetadata` — lee state event `m.agent.metadata` que el agente publica al arrancar.
- `MatrixService.SubscribeAgentLogs(agentID) -> chan LogLine` — SSE proxy al endpoint `agents_and_robots /api/agents/<id>/logs` ya existente (issue 0113).
- Llamadas REST proxy a `agents_and_robots`: `RestartAgent(agentID)`, `ListSkills(agentID)`, `TriggerSkill(agentID, skill, args)`.
2. Frontend React:
- Hook `useAgentMetadata(roomID)` — devuelve `null` si no es room de agente.
- Componente `AgentPanel` (panel lateral colapsable, solo visible si hay agentMetadata):
- Card con avatar, nombre, version, uptime, status (running/stopped/error).
- Tabs: "Logs" (live SSE), "Skills" (lista de skills disponibles + boton trigger), "Config" (read-only del config.yaml del agente).
- Boton restart con confirmacion.
- Componente `LogStream` — termtinal-like log viewer con auto-scroll + filtro grep.
- Slash commands custom: `/agent restart`, `/agent skill <name> <args>`, `/agent logs`.
3. Cuando flow 0009 (mesh) este vivo:
- Detectar `device_agent` rooms (state event `m.device.metadata` con tipo `device_agent`).
- Panel especifico `DevicePanel`: hostname, OS, kernel, IP mesh WG, capabilities firmadas, ultimo heartbeat.
- Slash commands: `/device shell <cmd>` (si capability permite), `/device fs ls <path>`, `/device camera capture`.
4. Tests:
- `e2e/test_agent_panel_basic.sh` — entrar a room de `welcome-bot`, panel agente visible con info correcta.
- `e2e/test_agent_logs_live.sh` — boton "view logs" stream logs en tiempo real (5s).
- `e2e/test_agent_restart.sh` — restart desde panel + verificar agente vuelve online.
## Funciones del registry a crear
- `matrix_agent_metadata_go_infra` — leer/publicar state event `m.agent.metadata`.
- `agents_and_robots_client_go_infra` — wrapper REST + SSE del API de `agents_and_robots`.
- `AgentPanel_ts_ui` — panel lateral Mantine con tabs.
- `LogStream_ts_ui` — viewer logs SSE.
- `DevicePanel_ts_ui` — panel device_agent (cuando flow 0009 vivo).
## Acceptance
- [ ] Room operado por agente conocido muestra `AgentPanel` automatico.
- [ ] Logs en vivo del agente aparecen en panel (SSE).
- [ ] Restart desde panel funciona end-to-end.
- [ ] Slash `/agent skill greet` ejecuta skill remota y respuesta llega como msg al room.
- [ ] Room NO operado por agente: panel oculto (no clutter).
## Notas
- State event `m.agent.metadata` format: `{ agent_id, version, capabilities[], owner, repo_url }`. Documentar en `projects/element_agents/docs/agent_metadata.md`.
- SSE proxy: el cliente PC habla a `agents_and_robots` via su DNS publica (`agents.organic-machine.com`) con auth Bearer (token del usuario Matrix + scope `agent_panel`).
- Permisos: solo el `owner` declarado en el agente puede ejecutar restart/trigger. Otros users del room solo leen.
- Gotcha: si el agente se rebuilds y cambia `agent_id`, el state event queda obsoleto — necesita TTL o heartbeat.
@@ -0,0 +1,65 @@
---
id: "0154"
title: "matrix-client-android scaffold: Kotlin + Compose + login MAS"
status: pending
priority: high
created: 2026-05-24
related_flows: ["0011"]
related_issues: ["0155", "0162"]
dependencies: ["0162"]
tags: [matrix, android, kotlin, compose, mas, oidc, scaffold]
---
## Objetivo
Crear `projects/element_agents/apps/matrix_client_android/` con `init_kotlin_app` (pipeline ya existente del registry). Configurar Compose + Material 3 + tema propio. Implementar login MAS OIDC via Chrome Custom Tabs. Tokens persistidos en EncryptedSharedPreferences. Resultado: APK debug que abre Custom Tab al MAS, retorna con token y muestra perfil del usuario.
## Tareas
1. `./fn run init_kotlin_app matrix_client_android` — usa pipeline existente del registry (ver issues completados 0073-0078).
2. Sub-repo Gitea: `git init -b master` + crear `dataforge/matrix_client_android` + push inicial. **Antes** de salir del worktree (ver `apps_subrepo.md`).
3. `app.md` con frontmatter:
- `lang: kotlin`, `framework: jetpack-compose`, `dir_path: projects/element_agents/apps/matrix_client_android`.
- `tags: [matrix, android, kotlin, compose]`.
- `uses_functions: []` (irlo rellenando issue a issue).
4. `build.gradle.kts`:
- `compileSdk = 34`, `minSdk = 28`, `targetSdk = 34`.
- Compose BOM `2024.x`.
- `matrix-rust-sdk` Kotlin bindings (`org.matrix.rustcomponents:sdk-android:0.x`).
- `androidx.security:security-crypto` para EncryptedSharedPreferences.
- `androidx.browser:browser` para Chrome Custom Tabs.
5. Login MAS:
- `LoginActivity` con boton "Sign in with Matrix".
- Generar PKCE code_verifier + state.
- Abrir Chrome Custom Tab a `<mas_url>/oauth/authorize?...`.
- `MainActivity` con intent-filter para `matrix-client-android://callback` redirect.
- Intercambiar code -> access_token + refresh_token.
- Guardar en EncryptedSharedPreferences (`SecurityCryptoUserPrefs`).
6. `HomeScreen` Compose con `Text("Hola @<userId>")` + boton Logout.
7. Tema Material 3 propio (paleta accent acorde a flow 0010 cliente PC para coherencia).
8. Test instrumented: `LoginInstrumentedTest` que mocka MAS y verifica flow callback -> token saved.
## Funciones del registry a crear
- `matrix_client_kotlin_infra` — facade sobre `matrix-rust-sdk` (init, login, sync, logout).
- `mas_oidc_kotlin_infra` — Chrome Custom Tabs + PKCE + callback handler.
- `encrypted_prefs_kotlin_core` — wrapper EncryptedSharedPreferences (idempotente, generic put/get).
- `LoginScreen_kotlin_ui` — Compose screen Material 3.
- `HomeScreen_kotlin_ui` — Compose screen perfil + logout.
## Acceptance
- [ ] `./gradlew assembleDebug` produce APK valido.
- [ ] APK instala en Android 9+ y arranca.
- [ ] Login: boton -> Custom Tab MAS -> consent -> callback -> perfil visible.
- [ ] Token persiste entre re-aperturas (no re-login si vigente).
- [ ] `app.md` con frontmatter completo + 5 `uses_functions`.
- [ ] Sub-repo `dataforge/matrix_client_android` con commit inicial.
- [ ] Test instrumented `LoginInstrumentedTest` pasa en emulator API 31.
## Notas
- Chrome Custom Tabs > WebView para OAuth (security: comparte cookies con browser principal del user, mejor UX).
- Refresh token: implementar refresh proactivo 5min antes de expiry (corutina + WorkManager periodic).
- Gotcha conocido (ver issue 0074): `local.properties` con `sdk.dir` obligatorio en setup nuevo. El scaffolder lo crea.
- Gotcha (issue 0075): Material 3 sin AppCompat — usar `MaterialTheme` directamente, no `Theme.AppCompat.*`.
@@ -0,0 +1,63 @@
---
id: "0155"
title: "matrix-client-android rooms list + timeline Compose"
status: pending
priority: high
created: 2026-05-24
related_flows: ["0011"]
related_issues: ["0154", "0156"]
dependencies: ["0154"]
tags: [matrix, android, compose, sync, timeline, rooms]
---
## Objetivo
UI Compose con `Scaffold` que muestre sidebar drawer con rooms y panel principal con timeline. Sync via `matrix-rust-sdk` (corrutinas + Flow). `LazyColumn` virtualizado para timeline (perf con miles de mensajes). Swipe-to-react en mensajes. Optimistic UI al enviar (en issue 0156).
## Tareas
1. ViewModels:
- `RoomsViewModel(matrixClient)` — expone `StateFlow<List<RoomSummary>>`. Ordenado por `lastActivity`.
- `TimelineViewModel(matrixClient, roomId)` — expone `StateFlow<List<TimelineEvent>>` + `loadMore()`.
- Persistencia local con Room DB (`androidx.room`) — store rooms + last sync token.
2. Compose:
- `MainScreen` con `ModalNavigationDrawer`:
- Drawer: `RoomList` (LazyColumn con `RoomItem`: avatar, name, last preview, unread badge).
- Content: `TimelineScreen(roomId)`.
- `TimelineScreen`:
- `LazyColumn` con `reverseLayout = true` (mensajes recientes abajo).
- `key = { it.eventId }` para evitar re-composiciones.
- `LaunchedEffect` con `LazyListState` -> al llegar al top, `viewModel.loadMore()`.
- `EventBubble` composables segun tipo (text, image, file, redacted).
- `Avatar` composable reusable con cache de imagenes (`Coil`).
3. Sync engine:
- `MatrixSyncService` (corrutina supervisor scope) que mantiene `client.syncStream()`.
- Si pasa a background sin call activa, sync se pausa hasta que vuelve foreground (lifecycle-aware).
- Errores de red: backoff exponencial (1s, 2s, 4s ... 60s max).
4. Tests:
- Instrumented `RoomsListTest` — 3 rooms aparecen en drawer.
- Instrumented `TimelinePaginationTest` — scroll-up carga 50 msgs anteriores.
## Funciones del registry a crear
- `matrix_room_summary_kotlin_infra` — extract `RoomSummary` de matrix-rust-sdk.
- `matrix_timeline_kotlin_infra` — Flow de eventos paginados.
- `RoomListScreen_kotlin_ui` — Compose drawer rooms.
- `TimelineScreen_kotlin_ui` — Compose timeline virtualizado.
- `EventBubble_kotlin_ui` — composable burbuja msg.
## Acceptance
- [ ] Drawer lista rooms del usuario test.
- [ ] Click en room muestra timeline ultimos 50 msgs.
- [ ] Swipe arriba carga msgs anteriores sin gap.
- [ ] Msg enviado desde PC (Wails) aparece en Android en <2s.
- [ ] Avion mode + restore: sync resume, no msgs perdidos.
- [ ] Cerrar app + reopen: state restaurado desde Room DB, no full re-sync.
## Notas
- `matrix-rust-sdk` ya gestiona persistencia interna (SQLite + crypto store). Room DB local solo para datos UI-rapidos (room summaries, unread counters).
- Read receipts: TBD otro issue.
- DMs detectados via `m.direct` account data.
- Spaces: `RoomItem` con icono diferente, colapsable.
@@ -0,0 +1,64 @@
---
id: "0156"
title: "matrix-client-android composer: markdown, replies, edits, reactions, media"
status: pending
priority: high
created: 2026-05-24
related_flows: ["0011"]
related_issues: ["0155", "0157"]
dependencies: ["0155"]
tags: [matrix, android, compose, composer, markdown, media, voice]
---
## Objetivo
Composer Compose con markdown shortcuts, replies, edits, reactions emoji, threads, upload media (camara nativa, galeria, voice msg con `MediaRecorder` opus). Drag&drop archivos compartidos via share sheet Android.
## Tareas
1. ViewModel:
- `ComposerViewModel(matrixClient, roomId)` — methods `sendText`, `sendReply`, `editMessage`, `sendReaction`, `uploadMedia`, `recordVoice`.
2. Compose:
- `Composer` con `OutlinedTextField` + toolbar (markdown shortcuts B/I/code).
- Hotkeys soft keyboard: Send action en IME.
- `AttachmentMenu`: botones camara, galeria, file, voice.
- `EmojiPicker` overlay (reusar libreria existente o componente propio).
- `ReactionBar` debajo de `EventBubble` con aggregates.
- `ThreadScreen` — nueva pantalla full para thread (no panel lateral como en PC, por screen real estate movil).
- Voice recording UI: hold-to-record con waveform preview + cancelar al deslizar.
3. Backend:
- Upload media: comprimir imagenes si >2MB antes de upload (`androidx.exifinterface` para preservar orientacion).
- Voice: `MediaRecorder` con OPUS, 32kbps, ogg container.
- Markdown -> HTML local con `markwon` library (lightweight, no Goldmark equivalente).
4. Share intent:
- `IntentFilter` para `android.intent.action.SEND` + tipos image/video/text/file -> abre composer del room seleccionado.
5. Tests:
- Instrumented `SendMarkdownTest``**bold**` formateado en Element Web.
- Instrumented `EditMessageTest` — edicion in-place propagada.
- Instrumented `VoiceMsgTest` — graba 5s + upload + play en Element Web.
## Funciones del registry a crear
- `markdown_to_matrix_html_kotlin_core` — wrapper markwon con sanitizer.
- `image_compress_kotlin_core` — resize + recompress JPEG.
- `voice_record_kotlin_infra` — MediaRecorder opus wrapper.
- `Composer_kotlin_ui` — Compose composer + toolbar + attachment menu.
- `ReactionBar_kotlin_ui` — composable reactions.
- `ThreadScreen_kotlin_ui` — pantalla thread.
## Acceptance
- [ ] Mensaje markdown se ve formateado en Element Web.
- [ ] Reply con quote del msg padre.
- [ ] Edit in-place propagado en ambos clientes.
- [ ] Reaccion emoji bidireccional.
- [ ] Upload imagen 5MB -> compresion a ~1MB -> envio + thumbnail OK.
- [ ] Voice msg 5s reproducible en Element Web.
- [ ] Share intent desde galeria abre composer con imagen pre-cargada.
## Notas
- Sanitizer HTML server-side delegado a matrix-rust-sdk (mismo allowlist que cliente PC).
- Voice msg: encode opus 32kbps, max 5min.
- Markwon vs goldmark: ambos cumplen el rol equivalente en su stack. Salida HTML compatible Matrix.
- Drag&drop: en Android = share sheet o picker, no drag&drop nativo como en PC.
@@ -0,0 +1,76 @@
---
id: "0157"
title: "matrix-client-android E2EE rust-sdk: cross-signing, SAS, recovery"
status: pending
priority: critical
created: 2026-05-24
related_flows: ["0011"]
related_issues: ["0156", "0158"]
dependencies: ["0156"]
tags: [matrix, android, e2ee, rust-sdk, cross-signing, sas, security]
---
## Objetivo
Encriptacion end-to-end con `matrix-rust-sdk` Kotlin bindings (mejor impl Olm/Megolm disponible). Cross-signing keys, SAS verification con emoji, recovery passphrase, key backup server-side. UI para verificar otros usuarios + manejar devices propios.
## Tareas
1. ViewModel:
- `SecurityViewModel(matrixClient)`:
- `bootstrapCrossSigning(passphrase)`.
- `recoverFromPassphrase(passphrase)`.
- `startVerification(userId, deviceId) -> VerificationSession`.
- `verifyEmoji(sessionId, accepted)`.
- `listOwnDevices() -> Flow<List<Device>>`.
- `backupMegolmKeys()`.
2. Compose:
- `OnboardingE2EEScreen` — wizard 3 pasos: generar passphrase, backup, verify primer device.
- `SettingsSecurityScreen`:
- Lista devices propios con badge verified/unverified.
- Dialog SAS con emoji grid 7x1 cuando hay verificacion en curso.
- Boton "Reset cross-signing" (destructive, requiere typing "RESET").
- Boton "Restore from passphrase".
- `EventBubble` con icono shield (green/amber/red).
- Banner room con "X devices not verified" si aplica.
3. Crypto store:
- `matrix-rust-sdk` gestiona internamente. Solo asegurar que `applicationContext.filesDir` es persistente entre upgrades.
- Backup local del store (export encriptado) antes de uninstall: feature opcional via "Export to file" en settings.
4. Tests:
- Instrumented `BootstrapCrossSigningTest`.
- Instrumented `VerificationSASTest` con mock peer.
- Instrumented `RecoveryFromPassphraseTest`.
- E2E manual con Element Web: enviar/recibir msg E2EE, verificar device cross-platform.
## Funciones del registry a crear
- `matrix_e2ee_kotlin_infra` — wrapper rust-sdk encryption module.
- `passphrase_derive_key_kotlin_core` — PBKDF2 wrapper.
- `VerificationDialog_kotlin_ui` — Compose emoji grid SAS.
- `OnboardingE2EEScreen_kotlin_ui` — wizard.
- `SettingsSecurityScreen_kotlin_ui` — devices + verification UI.
## Acceptance
- [ ] Bootstrap crea cross-signing keys + sube cifradas.
- [ ] Msg enviado en room E2EE se descifra en Element Web + cliente PC Wails (y al reves).
- [ ] SAS verification con emoji grid vs Element Web: ambos 7 emojis iguales, accept funciona.
- [ ] Login device nuevo + restore passphrase recupera msgs historicos.
- [ ] Device no verificado dispara shield amber en EventBubble.
- [ ] Decryption failure muestra shield rojo + boton "Request key".
## Notas
**Anti-criterios:**
- NO marcar done si E2EE silent-falla (mensaje no descifrado pero sin warning visible).
- NO marcar done si passphrase queda en plain text en disco.
- NO marcar done si cross-signing no funciona contra cliente PC Wails (interop critica).
**Decisiones:**
- `matrix-rust-sdk` >> matrix-android-sdk2 (deprecated). Olm/Megolm en Rust = mejor perf + sin memory leaks.
- Passphrase format igual que cliente PC (4 palabras Diceware o 12-byte base32).
**Gotchas:**
- Key rotation Megolm: rust-sdk lo gestiona, pero monitorizar logs en primera semana de uso real.
- Olm sessions max: rust-sdk auto-rotate, no accion manual.
- Devices nuevos sin passphrase: msgs pre-existentes NO se descifran. UI debe ser clara.
@@ -0,0 +1,73 @@
---
id: "0158"
title: "matrix-client-android calls LiveKit nativo: mic/cam/screen + PiP"
status: pending
priority: high
created: 2026-05-24
related_flows: ["0011"]
related_issues: ["0157", "0159", "0161"]
dependencies: ["0157"]
tags: [matrix, android, livekit, calls, webrtc, pip, audio-focus]
---
## Objetivo
Llamadas nativas via `io.livekit:livekit-android` SDK oficial. Codecs HW (H.264/VP9 hardware decoder), audio focus + AEC/NS nativos, MediaSession para controls en lockscreen, Picture-in-Picture mode Android nativo. Soporta 1:1 + grupales (limite 16 del LiveKit config actual).
## Tareas
1. Backend (compartido con cliente PC):
- Reusar `livekit_token_gen_go_infra` que esta en flow 0010.
- Cliente Android pide token al mismo endpoint `/api/call/token` que el cliente PC.
2. ViewModel:
- `CallViewModel(matrixClient, roomId)`:
- `joinCall()` — pide token + conecta `Room.connect()`.
- `toggleMic()`, `toggleCamera()`, `toggleScreenShare()`.
- `hangup()`.
- `Flow<CallState>` con participants, tracks, connection state.
3. Compose:
- `CallScreen` fullscreen:
- Grid tiles participantes (`Flow` layout responsive 1/2/4/9/16).
- Tile principal: active speaker (track audio level del SDK).
- Controles bottom: mic, cam, screen, raise hand, hangup.
- `IncomingCallScreen` fullscreen con accept/decline (system overlay activity).
- `CallTile` composable con `VideoView` (SurfaceViewRenderer del SDK).
4. PiP (Picture-in-Picture):
- `Activity` con `setPictureInPictureParams()`.
- Auto-enter PiP al minimizar la app durante call.
- PiP tile: video remoto + boton hangup.
5. Audio routing:
- `AudioFocusRequest` (Android 8+) — focus exclusivo durante call.
- Switch speaker/earpiece/bluetooth via `AudioManager.setSpeakerphoneOn()` + connection state listeners para audifonos BT.
- Echo cancellation + noise suppression: SDK los habilita por defecto, verificar.
6. ICE/TURN: igual que cliente PC, depende del LiveKit config server-side.
7. Tests:
- Instrumented `Call1to1Test` con emulator + segundo cliente (PC) — connect, video, hangup.
- Manual `ScreenShareTest` con device fisico.
- Manual `4ParticipantsTest`.
- Manual `PiPTest` — call activa + Home button -> PiP aparece.
## Funciones del registry a crear
- `livekit_call_kotlin_infra` — wrapper `Room` SDK + permission helpers.
- `audio_routing_kotlin_infra` — speaker/earpiece/BT switching.
- `CallScreen_kotlin_ui` — fullscreen call UI.
- `CallTile_kotlin_ui` — tile con VideoView.
- `IncomingCallScreen_kotlin_ui` — accept/decline overlay activity.
## Acceptance
- [ ] Start call desde Android -> PC Wails recibe y conecta.
- [ ] 30s call con video+audio nativo (verificar HW codec via `adb shell dumpsys media.codec`).
- [ ] Mute mic + apagar cam refleja en otro cliente.
- [ ] Screen share desde Android (con `MediaProjection`) visible en PC.
- [ ] PiP: minimizar app durante call -> tile flotante con video remoto.
- [ ] Bluetooth headphones: cambio automatico al conectar/desconectar.
- [ ] Battery: call 30min con AC + WiFi <15% drain.
## Notas
- Permissions runtime: `RECORD_AUDIO`, `CAMERA`, `POST_NOTIFICATIONS` (Android 13+), `FOREGROUND_SERVICE`, `FOREGROUND_SERVICE_MEDIA_PROJECTION` (Android 14+).
- Foreground service requerido para mantener call con app en background (issue 0161).
- E2EE en call (insertable streams): TBD post-DoD, igual que en cliente PC.
- Connection service Android (sistema): TBD, opcional. Permite integracion con dialer system + Bluetooth Car. Valorar coste/beneficio.
@@ -0,0 +1,80 @@
---
id: "0159"
title: "matrix-client-android push FCM via sygnal + Firebase setup"
status: pending
priority: high
created: 2026-05-24
related_flows: ["0011"]
related_issues: ["0158", "0160"]
dependencies: ["0154"]
tags: [matrix, android, push, fcm, firebase, sygnal, infra]
---
## Objetivo
Notificaciones push moviles via FCM (Firebase Cloud Messaging) usando `sygnal` (push gateway oficial de Matrix). Sygnal recibe push events de Synapse, traduce a payload FCM, enviado a Firebase, entregado al device. La app despierta para mostrar notificacion del mensaje, o trigger ringer para incoming calls. App en background o muerta tambien recibe.
## Tareas
1. Infra (modifica `element_matrix_chat` app):
- Anadir container `sygnal` al `docker-compose.yml`. Config en `configs/sygnal.yaml`.
- Service account JSON de Firebase en `configs/firebase-sa.json` (gitignored, instalado en VPS via secrets).
- Synapse config: pushers habilitados (ya por defecto).
- Reverse proxy: `https://push-<hash>.organic-machine.com/_matrix/push/v1/notify` -> sygnal:5000.
- Documentar setup en `projects/element_agents/apps/element_matrix_chat/docs/sygnal_setup.md`.
2. Firebase:
- Crear proyecto `fn-registry-matrix-push` en Firebase console.
- Habilitar Cloud Messaging.
- Generar service account JSON.
- Anadir `google-services.json` al modulo Android (`app/google-services.json`).
3. Android app:
- `build.gradle`: `com.google.gms:google-services`, `com.google.firebase:firebase-messaging`.
- `FirebaseMessagingService` subclass:
- `onNewToken(token)` -> registrar en sygnal via Synapse Pusher API `POST /_matrix/client/v3/pushers/set`.
- `onMessageReceived(message)` -> parse data payload + mostrar notif.
- Notification channels (Android 8+):
- `messages` — IMPORTANCE_HIGH, sonido.
- `calls` — IMPORTANCE_HIGH, full-screen intent (despertar pantalla).
- `silent` — IMPORTANCE_LOW.
- VoIP push para calls: payload con `prio=high`, `event_id_only=false` (incluir event para mostrar caller info sin sync completo).
4. Tests:
- Instrumented `FCMTokenRegistrationTest` — mock Firebase, verificar pusher creado en Synapse.
- Manual `PushDeliveryTest` — enviar msg desde Element Web a Android offline -> push aparece <3s.
- Manual `PushCallTest` — start call desde PC -> Android offline despierta + ring.
- Manual `PushBatterySaverTest` — Android en battery saver + Doze mode + push sigue llegando.
## Funciones del registry a crear
- `sygnal_setup_bash_infra` — script setup container sygnal en VPS.
- `sygnal_config_template_go_infra` — generador `sygnal.yaml` con Firebase SA.
- `fcm_register_kotlin_infra` — onNewToken + register en Synapse Pusher API.
- `synapse_pusher_set_go_infra` — Go helper REST `POST /pushers/set` (reutilizable PC + Android).
- `NotificationBuilder_kotlin_ui` — helper notification channels + actions.
## Acceptance
- [ ] Container `sygnal` activo en VPS, health check `:5000/_matrix/push/v1/notify` HEAD 200.
- [ ] Firebase project creado + SA JSON instalada en VPS.
- [ ] App Android registra FCM token + crea pusher en Synapse al primer login.
- [ ] Msg desde Element Web a Android (app cerrada por user) -> push notif en <3s.
- [ ] Start call desde cliente PC -> Android offline despierta + ring 30s.
- [ ] Battery saver activo: push sigue llegando (FCM high priority bypasses Doze).
- [ ] Multiple users: pusher por device, no se cruzan.
## Notas
**Gotcha critico:** FCM no entrega push si:
- App ha sido force-stopped por user (system requirement).
- Device tiene "Restricted background usage" en battery settings.
- Account Google no esta sincronizada en el device.
Documentar en onboarding para que el user lo entienda.
**Privacy:** payload FCM no debe contener contenido del msg en claro (Synapse E2EE). Solo: `room_id`, `event_id`, `unread_count`, `prio`. App hace sync interno al recibir push para obtener msg cifrado y descifrar local.
**Coste:** FCM gratis para hosting Firebase. Sygnal CPU/RAM despreciable (<50MB).
**Alternativas exploradas:**
- UnifiedPush + ntfy: open-source, sin Google. Pro: privacy. Con: requiere infraestructura propia + onboarding mas duro. Post-DoD considerar como segunda opcion para users sin Google Play.
**Decisiones futuras (post-DoD):**
- iOS equivalent: APNs via sygnal mismo gateway. Cuando llegue cliente iOS.
@@ -0,0 +1,83 @@
---
id: "0160"
title: "matrix-client-android mini-webapps: WebView + Widget API v2 bridge"
status: pending
priority: medium
created: 2026-05-24
related_flows: ["0011"]
related_issues: ["0159", "0161"]
dependencies: ["0159"]
tags: [matrix, android, webview, widgets, agents, sandbox]
---
## Objetivo
Host de widgets en Android equivalente al cliente PC (issue 0152). Mismo contrato Widget API v2. WebView con sandbox estricto + bridge JS-Kotlin implementa capabilities API. Widgets de los rooms operados por agentes (`agents_and_robots`) se ven embebidos: dashboard, formulario, kanban inline, control del agente.
## Tareas
1. ViewModel:
- `WidgetsViewModel(matrixClient, roomId)`:
- `Flow<List<Widget>>` desde state events `m.widget` del room.
- `addWidget(widget)`, `removeWidget(widgetId)`.
- `generateUrl(widget) -> String` — substituye placeholders Matrix Widget API.
- `mintScopedToken(widgetId) -> String` — token efimero scope room+widget.
2. Compose:
- `WidgetsPanel` (drawer lateral o bottom sheet en movil):
- Tabs con widgets activos del room.
- Cada tab = `WidgetView` que envuelve un `WebView`.
- `WidgetView` composable:
- `WebView` configurado:
- `settings.javaScriptEnabled = true`.
- `settings.allowFileAccess = false`.
- `settings.allowContentAccess = false`.
- `settings.allowFileAccessFromFileURLs = false`.
- `settings.allowUniversalAccessFromFileURLs = false`.
- `settings.mixedContentMode = MIXED_CONTENT_NEVER_ALLOW`.
- `webViewClient` con CSP injection + URL allowlist.
- `addJavascriptInterface(WidgetBridge, "MatrixWidgetBridge")` — bridge expone Widget API v2.
- `CapabilityConsentDialog` Compose — pide consentimiento usuario para capabilities.
3. WidgetBridge (Kotlin):
- Implementa capabilities handshake postMessage (igual contrato que cliente PC):
- `read_events`, `send_event`, `send_to_device`, `get_openid`, `m.always_on_screen`.
- Audit log mensajes JS<->Kotlin en local DB.
- Whitelist estricta de capabilities concedidas.
4. Widgets internos primer batch (compartidos con cliente PC):
- `widget-agent-panel` — control del agente.
- `widget-kanban` — kanban inline.
- `widget-issue-tracker`.
5. Tests:
- Instrumented `WidgetCapabilitiesTest` — dialog aparece + accept/decline funciona.
- Instrumented `WidgetSandboxTest` — widget malicioso (intenta `window.location='file:///etc/passwd'`) bloqueado.
- Instrumented `WidgetSendEventTest` — widget con capability envia msg.
## Funciones del registry a crear
- `WidgetView_kotlin_ui` — Compose WebView wrapper sandboxed.
- `widget_bridge_kotlin_infra` — JavascriptInterface implementando Widget API v2.
- `widget_url_template_kotlin_core` — substituyente placeholders (puede compartirse logica con la Go version del PC, contrato identico).
- `CapabilityConsentDialog_kotlin_ui` — Compose dialog.
- `widget_audit_log_kotlin_infra` — append-only audit log en Room DB.
## Acceptance
- [ ] Widget publicado desde cliente PC se ve embebido en Android (mismo room).
- [ ] Capability handshake: widget pide `send_event` -> dialog Compose -> accept -> widget envia msg.
- [ ] Sandbox: widget intenta `XMLHttpRequest` a `file:///` -> bloqueado.
- [ ] Widget agent-panel funcional: muestra logs en vivo del agente + boton restart.
- [ ] Audit log persiste en Room DB con timestamp + capability + accept/deny.
## Notas
**Critico:**
- Mismo contrato Widget API v2 que cliente PC. Widget HTML escrito una vez funciona en ambos.
- WebView Android moderno (Chromium 100+) soporta WebRTC + WebGL + service workers. Suficiente para widgets ricos.
**Gotcha:**
- `WebView.addJavascriptInterface` solo seguro en Android 4.2+ (API 17+, ya minSdk=28). Pero validar todo input desde JS — nunca confiar.
- `setAllowFileAccessFromFileURLs(false)` solo aplica si la URL del widget es `file://`. Nuestros widgets son `https://` -> hardcode CSP estricta.
- Memory: WebView por tab + 5 widgets activos = ~200MB facil. Limitar a max 3 widgets simultaneos activos.
**Roadmap post-DoD:**
- Widget marketplace catalog accesible via menu.
- "Add to home screen" PWA mode para widgets favoritos (Android shortcut + launcher icon dedicado).
@@ -0,0 +1,89 @@
---
id: "0161"
title: "matrix-client-android foreground service: calls + lifecycle + lockscreen"
status: pending
priority: high
created: 2026-05-24
related_flows: ["0011"]
related_issues: ["0158", "0160"]
dependencies: ["0158"]
tags: [matrix, android, foreground-service, lifecycle, mediasession, wakelock]
---
## Objetivo
`CallForegroundService` que mantiene call activa con app en background o pantalla bloqueada. Notification ongoing visible mientras dura la call. `MediaSession` para integrar con lockscreen controls + Bluetooth Car (mute, hangup desde audio device). Wakelock controlado para evitar drain excesivo. Notificaciones full-screen intent para incoming calls (despiertan pantalla).
## Tareas
1. `CallForegroundService` (`android.app.Service`):
- `START_FOREGROUND_SERVICE` con type `MEDIA_PROJECTION` o `PHONE_CALL` (Android 14+ requiere type explicito).
- `Notification.Builder` channel `calls` con:
- Custom view con caller name, duration, mute/hangup buttons.
- `setOngoing(true)`.
- `setCategory(CATEGORY_CALL)`.
- Lifecycle: `START_STICKY` para reiniciar si OS lo mata (raro con foreground).
2. `MediaSession` integration:
- `MediaSessionCompat` con play/pause/stop actions mapeados a mute/unmute/hangup.
- Bluetooth Car media controls.
- Lockscreen controls visibles si dispositivo lo soporta.
3. Wakelock:
- `PowerManager.PARTIAL_WAKE_LOCK` durante call activa.
- `WAKE_LOCK_KEY = "matrix_client:call"` para audit en `dumpsys power`.
- Liberar inmediato al hangup.
- Proximity wakelock (`PROXIMITY_SCREEN_OFF_WAKE_LOCK`) si call solo audio + telefono pegado a oreja.
4. Incoming call full-screen intent:
- `Notification` con `setFullScreenIntent(pendingIntent, true)`.
- Activity `IncomingCallActivity` con `showWhenLocked(true)` + `turnScreenOn(true)`.
- Compose UI fullscreen con accept/decline.
5. Doze mode handling:
- `ACTION_IGNORE_BATTERY_OPTIMIZATIONS` solicitar al user en onboarding (no obligatorio, solo para calls fiables).
- Documentar tradeoff en pantalla onboarding.
6. Battery monitoring:
- Log custom: call duration + battery_drain_pct al hangup.
- Visible en `Settings > Diagnostics` para debug.
7. Tests:
- Manual `CallBackgroundTest` — start call + Home button -> notif visible + audio sigue.
- Manual `CallLockscreenTest` — call + power button -> pantalla apaga + audio sigue + lockscreen controls visibles.
- Manual `IncomingFullScreenTest` — device en lockscreen + incoming call -> pantalla despierta + UI accept/decline.
- Manual `BluetoothCarTest` — Bluetooth Car connected + call active + mute desde steering wheel funciona.
- Manual `BatteryTest` — call 30min en background + WiFi + AC -> drain <15%.
## Funciones del registry a crear
- `CallForegroundService_kotlin_infra` — service completo.
- `media_session_kotlin_infra` — wrapper MediaSessionCompat.
- `wakelock_manager_kotlin_infra` — adquirir/liberar wakelocks de forma idempotente.
- `IncomingCallActivity_kotlin_ui` — Compose fullscreen activity.
- `battery_monitor_kotlin_infra` — log drain por session.
## Acceptance
- [ ] Call activa + Home -> notif ongoing visible + audio sigue 30s.
- [ ] Call + power button -> lockscreen muestra controls + audio sigue.
- [ ] Incoming call con pantalla apagada -> despierta + UI accept/decline.
- [ ] Bluetooth Car: mute/hangup desde steering wheel funciona.
- [ ] Hangup libera wakelocks (verificar con `dumpsys power | grep matrix_client`).
- [ ] Battery saver activo: call no se corta (foreground service exempt).
- [ ] Call 30min background: drain <15% con WiFi+AC.
## Notas
**Anti-criterios:**
- NO marcar done si call se corta a los 5min en background (battery optimization kill).
- NO marcar done si wakelock queda colgado tras hangup (battery leak).
- NO marcar done si lockscreen no muestra controls (UX critico para calls largas).
**Gotchas Android 14+:**
- Foreground service type DEBE declararse en manifest + runtime: `phoneCall|mediaProjection`.
- `POST_NOTIFICATIONS` runtime permission (Android 13+).
- `USE_FULL_SCREEN_INTENT` runtime permission (Android 14+) — pedir explicito.
**Decisiones:**
- Telecom framework (ConnectionService): NO en esta iteracion. Pro: integracion dialer nativo. Con: bug-prone, requiere CALL_PHONE permission con justificacion Play Store. Post-DoD considerar.
- Audio focus exclusivo durante call (issue 0158 ya lo cubre).
**Battery optimization onboarding:**
- Pantalla en primer launch: explicar por que pedimos exempt battery optimization (calls fiables).
- Boton "Open settings" -> `Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS`.
- Si user declina: app funciona pero documentar que calls largas pueden cortarse.
@@ -0,0 +1,197 @@
---
id: "0162"
title: "Matrix: migrar Synapse a MAS como unico auth provider (MSC3861)"
status: pending
priority: critical
created: 2026-05-24
related_flows: ["0010", "0011"]
related_issues: ["0147", "0154", "0163"]
dependencies: []
tags: [matrix, mas, synapse, msc3861, auth, oidc, migration, infra]
---
## Objetivo
Activar `matrix_authentication_service` en Synapse para que TODO login pase por MAS (Matrix Authentication Service) via MSC3861. Estado actual: MAS corre 6 semanas pero esta en pie sin clients registrados. Synapse usa login password legacy + application_service. Element Web, Synapse-Admin y clientes nuevos (flows 0010 + 0011) deben autenticarse exclusivamente contra MAS via OIDC.
Bloquea flows 0010 (matrix-client-pc) + 0011 (matrix-client-android) porque ambos asumen MAS funcional.
## Estado actual
```yaml
# synapse_data/homeserver.yaml — comentado, NO activo:
# matrix_authentication_service:
# enabled: true
# endpoint: "http://mas:8080/"
# secret: "<shared_secret>"
experimental_features:
msc3266_enabled: true
msc4222_enabled: true
msc4354_enabled: true
# msc4108_delegation_endpoint: "https://auth-af2f3d.organic-machine.com/_matrix/client/unstable/org.matrix.msc4108/rendezvous"
```
```yaml
# mas/config.yaml
clients: [] # vacio
public_base: https://auth-af2f3d.organic-machine.com/
```
```
GET /_matrix/client/v3/login -> {"flows":[{"type":"m.login.password"},{"type":"m.login.application_service"}]}
GET /.well-known/matrix/client -> sin org.matrix.msc2965.authentication
```
## Tareas
1. **Pre-migracion: backup completo**
- Snapshot postgres Synapse: `docker exec element_matrix_chat-postgres-1 pg_dump -U synapse synapse > /backup/synapse_$(date +%Y%m%d).sql`.
- Snapshot postgres MAS: idem `mas-postgres`.
- Snapshot `synapse_data/` + `mas/config.yaml`.
- Guardar backups en VPS local + descargar copia a PC.
2. **Registrar clients en MAS** (`mas/config.yaml`):
- Cliente para Synapse (admin/internal): `client_id` + `client_secret` o `client_auth_method: client_secret_basic`.
- Cliente para Element Web: `redirect_uris: [https://element-a05ae4.organic-machine.com/]`.
- Cliente para nuevo admin panel (issue 0163): `redirect_uris: [<admin_panel_url>]`.
- Cliente para matrix_client_pc (flow 0010): `redirect_uris: [http://127.0.0.1:*]` (loopback dinamico).
- Cliente para matrix_client_android (flow 0011): `redirect_uris: [matrix-client-android://callback]`.
- Aplicar: `docker exec element_matrix_chat-mas-1 mas-cli config sync`.
3. **Activar MSC3861 en Synapse**:
- Editar `synapse_data/homeserver.yaml`:
```yaml
matrix_authentication_service:
enabled: true
endpoint: "http://mas:8080/"
secret: "<shared_secret_matching_mas_config>"
experimental_features:
msc3861:
enabled: true
msc3266_enabled: true
msc4222_enabled: true
msc4354_enabled: true
msc4108_delegation_endpoint: "https://auth-af2f3d.organic-machine.com/_matrix/client/unstable/org.matrix.msc4108/rendezvous"
# Disable legacy password login:
password_config:
enabled: false
```
4. **Migrar usuarios existentes Synapse -> MAS**:
- `docker exec element_matrix_chat-mas-1 mas-cli syn2mas --synapse-config /data/homeserver.yaml --dry-run` primero.
- Revisar log (conflictos, usuarios huerfanos).
- Ejecutar real: `mas-cli syn2mas --synapse-config /data/homeserver.yaml`.
- Verificar: contar usuarios `mas-postgres` vs `synapse-postgres`, deben coincidir.
5. **Actualizar well-known** (`/.well-known/matrix/client`):
- Servido por `element_matrix_chat-wellknown-1` (nginx).
- Anadir:
```json
"org.matrix.msc2965.authentication": {
"issuer": "https://auth-af2f3d.organic-machine.com/",
"account": "https://auth-af2f3d.organic-machine.com/account"
}
```
- Reload nginx.
6. **Restart ordenado**:
- `docker compose restart mas` -> verificar logs sin errores 30s.
- `docker compose restart synapse` -> verificar `_matrix/client/v3/login` ahora devuelve `m.login.sso` con `identity_providers` apuntando a MAS.
- `docker compose restart element` (recarga config).
7. **Reconfigurar Element Web** (`element-config.json`):
- Activar `oidc_native_flow: true` (Element Web soporta MSC3861 desde v1.11.50+).
- Verificar version Element Web (`docker exec element_matrix_chat-element-1 cat /etc/nginx/conf.d/element.json | head` o image tag) >= v1.11.50.
- Si version vieja: bump container image.
8. **Verificar end-to-end**:
- Logout completo navegador.
- Abrir Element Web -> debe redirigir a MAS para login.
- Login con cuenta existente migrada -> redirect back a Element -> sesion activa.
- Comprobar rooms historicos siguen visibles + msgs E2EE descifrados (las cross-signing keys NO se re-bootstrappean si la migracion va bien).
9. **Plan rollback** (escribir en `docs/mas_migration_rollback.md`):
- Restaurar postgres Synapse desde dump.
- Comentar bloque `matrix_authentication_service:` en homeserver.yaml.
- `password_config.enabled: true`.
- Restart Synapse.
- MAS sigue vivo idle (no destruir).
## Funciones del registry a crear
- `mas_client_register_bash_infra` — `mas-cli config sync` wrapper + validacion idempotente.
- `synapse_msc3861_enable_go_infra` — edita `homeserver.yaml` con bloque MAS + experimental_features.
- `mas_syn2mas_migration_bash_infra` — wrapper migracion con dry-run obligatorio + log archive.
- `wellknown_oidc_patch_go_infra` — anade `org.matrix.msc2965.authentication` al well-known JSON servido por nginx.
- `synapse_login_flows_check_go_infra` — health-check post-migracion (espera ver `m.login.sso` en flows).
## Acceptance
- [ ] `GET /_matrix/client/v3/login` devuelve `m.login.sso` con identity provider MAS.
- [ ] `GET /.well-known/matrix/client` contiene `org.matrix.msc2965.authentication.issuer`.
- [ ] Element Web redirige a MAS para login (no muestra form propio).
- [ ] Login con cuenta existente funciona post-migracion.
- [ ] Rooms historicos + msgs E2EE siguen visibles tras re-login.
- [ ] `password_config.enabled: false` no rompe nada (todo va por MAS).
- [ ] Backup pre-migracion subido + documentado.
- [ ] `docs/mas_migration_rollback.md` escrito + probado en staging (ver Notas).
## Definition of Done
### Mecanica
- `docker compose ps` muestra todos los containers healthy.
- `mas-cli config check` exit 0.
- `synapse curl /health` 200.
- Tests humo: login + send msg + recibe msg propagado a otra cuenta.
### Cobertura
| Escenario | Comando / evidencia | Resultado |
|---|---|---|
| Golden: login Element Web via MAS | navegador Incognito -> ` element-a05ae4.organic-machine.com` | redirect MAS -> login -> sesion activa |
| Edge: usuario migrado con E2EE setup previo | post-login en Element Web | rooms cifrados se descifran sin re-bootstrap |
| Edge: app servicio (bot) usa application_service token | bot envia msg | sigue funcionando (AS no pasa por MAS) |
| Edge: device verification cross-platform | Element Web verifica device PC Wails (post flow 0010) | OK |
| Error: token MAS expira mid-session | esperar TTL (default 5min refresh) | refresh automatico, no logout |
| Error: MAS cae (kill container) | matar `mas-1` 60s | Synapse rechaza nuevos logins; sessiones activas siguen (access_token cached); restart MAS -> recovery |
### Vida util validada (7 dias post-migracion)
| Metrica | Umbral | Donde | Ventana |
|---|---|---|---|
| Login failures (causa MAS) | `< 1%` | `mas` logs + sentry-like | 7 dias |
| Latency `/oauth2/token` | `p95 < 500ms` | nginx access log VPS | 7 dias |
| Crashes MAS / Synapse | `0` | `docker logs --since` | 7 dias |
| Users migrados activos | `>= 95%` | `mas-cli admin user list` vs sesiones activas | 7 dias |
### Anti-criterios
- NO marcar done si algun usuario migrado pierde acceso a rooms cifrados.
- NO marcar done si Element Web sigue mostrando form de password (legacy flow).
- NO marcar done si rollback documentado no se ha probado al menos una vez en staging.
## Notas
**Staging recomendado:** levantar stack identico en VPS test o WSL local con docker-compose + datos fake antes de tocar prod. organic-machine.com lleva 6 semanas viva.
**Element Call (LiveKit):** ya usa OIDC del homeserver para tokens via `livekit-jwt` container -> migracion debe verificar que tokens siguen emitiendose contra el MAS auth.
**Synapse-Admin compat:** synapse-admin v0.10+ soporta MSC3861. Verificar version corriendo. Si vieja, bump O reemplazar por panel propio (issue 0163).
**Gotcha critico — shared_secret:**
- `mas/config.yaml` tiene `matrix.secret` que debe matchear `homeserver.yaml.matrix_authentication_service.secret`.
- Generar con `openssl rand -hex 32` si no existe.
- Si no matchean: Synapse rechaza requests MAS con 401.
**Gotcha — application_service tokens:**
- Los AS (bridges, bots) NO pasan por MAS. Siguen usando `as_token`/`hs_token` de su registration.
- `agents_and_robots` usa application_service? Verificar antes — si SI, no afecta. Si usa password login normal, tendra que pasar por MAS (re-config).
**Roadmap post-DoD:**
- Habilitar `device_code` grant en MAS para login CLI futuro.
- Habilitar QR-code login (MSC4108) ya pre-config con `msc4108_delegation_endpoint`.
- Multi-factor (TOTP) en MAS — config available.
## Capability growth log
- v0.1.0 (2026-05-24) — issue creada.
@@ -0,0 +1,189 @@
---
id: "0163"
title: "Matrix admin panel propio: users, rooms, devices, sessions (sustituye synapse-admin)"
status: pending
priority: medium
created: 2026-05-24
related_flows: ["0010", "0011"]
related_issues: ["0162", "0147"]
dependencies: ["0162"]
tags: [matrix, admin, panel, react, mantine, mas, synapse, infra]
---
## Objetivo
Panel admin propio que reemplaza `https://admin-0cc4d3.organic-machine.com/#/users` (synapse-admin actual). Funciones equivalentes: gestionar usuarios (crear, deactivate, reset password, list devices, list rooms), gestionar rooms (list, members, kick, force-leave, delete), ver sesiones activas + revoke, ver media (storage usage por user). Auth via MAS OIDC con scope admin. Stack: React+Vite+Mantine+`@fn_library` (consistente con flows 0010/0011 + resto del registry).
## Por que reemplazar synapse-admin
- **Auth legacy**: synapse-admin usa admin token + password admin directo. Tras issue 0162 (MAS obligatorio) esto chirria. Mejor consume MAS OIDC + Synapse Admin API.
- **UI ajena**: stack distinto al resto del registry. Sin theming propio, sin `@fn_library`, sin coherencia visual con cliente PC (flow 0010).
- **Sin agentes**: no podemos integrar paneles especiales para `agents_and_robots`, devices del mesh (flow 0009), policies de widgets.
- **No extensible**: anadir "ver telemetria de calls LiveKit" o "audit log MAS" requiere fork pesado.
## Tareas
1. **Scaffold app**:
- `projects/element_agents/apps/matrix_admin_panel/`.
- Stack: React+Vite+TS+Mantine+`@fn_library`+`@tabler/icons-react`.
- Backend: Go con `mautrix-go` admin client + MAS OIDC client + `livekit-server-sdk-go` (para sesiones de call).
- Empaquetado: backend Go sirve frontend estatico embebido (`embed.FS`).
- Deploy: container Docker en `element_matrix_chat` stack o como service standalone via `deploy_server`.
2. **Auth flow MAS**:
- Cliente registrado en MAS (issue 0162 paso 2) con scope `urn:synapse:admin:*`.
- Login Web: OIDC redirect a MAS.
- Token guardado en httpOnly cookie + CSRF token.
3. **Modulos UI**:
- **Users**:
- Tabla virtualizada con `data-table` (cuando exista TS equivalente) o `mantine-react-table`.
- Columnas: localpart, displayname, avatar, admin, deactivated, last_seen, device_count.
- Acciones por row: view detail, deactivate/reactivate, reset password (force MAS link), list devices.
- Filtros: deactivated, admin, search.
- **User detail**:
- Sub-tabs: Profile, Devices (list + revoke individual), Rooms (membership list), Media (uploads + size), Sessions (MAS active sessions + revoke), Audit log (MAS).
- **Rooms**:
- Tabla: room_id, name, alias, members_count, encrypted, public, federated, state_events.
- Acciones: view detail, force-leave usuarios, delete room (purge), shutdown notif.
- **Room detail**:
- Members + roles, state events viewer (read-only JSON), media in room, widgets activos (interop con flow 0010 widget API).
- **Sessions** (MAS):
- Lista sesiones activas global.
- Filtro por user, IP, device, last_used.
- Revoke individual o bulk.
- **Federation**:
- Estado federation (Synapse `federation_handler`).
- Allowlist/blocklist servers.
- **Stats**:
- Resumen: users count, rooms count, mensajes/dia (ultima semana), media storage, calls activas (via LiveKit `RoomService.ListRooms`).
- Graficas con `@mantine/charts` o `recharts`.
4. **Capability groups en panel**:
- Reusa `AgentPanel` (flow 0010 issue 0153) para mostrar info de agentes registrados.
- Reusa `DevicePanel` (cuando flow 0009 vivo) para devices del mesh.
- Slot "Widgets policy": ver/aprobar capabilities concedidas globalmente, audit log.
5. **API endpoints backend Go**:
- `GET /api/users` -> proxy a Synapse `/_synapse/admin/v2/users` con auth MAS.
- `POST /api/users/<id>/deactivate`.
- `GET /api/rooms`, `POST /api/rooms/<id>/delete`.
- `GET /api/mas/sessions`, `POST /api/mas/sessions/<id>/revoke` (MAS admin API).
- `GET /api/livekit/rooms` (active calls).
- `GET /api/stats/summary`.
6. **Permisos**:
- Solo users con flag `admin: true` (Synapse) o scope MAS admin claim.
- Backend valida claim/flag en cada request.
- UI muestra "Access denied" si user logueado no es admin.
7. **Deploy**:
- Anadir container al `docker-compose.yml` de `element_matrix_chat`.
- O bien standalone via `deploy_server` (registry function existente).
- URL: `admin-af2f3d.organic-machine.com` o reusar `admin-0cc4d3.organic-machine.com` cuando se retire synapse-admin.
8. **Migracion synapse-admin -> panel propio**:
- Coexistencia 2 semanas: ambos vivos, MAS audita uso de cada uno.
- Cuando uso de synapse-admin = 0 durante 7 dias seguidos: detener container.
- Documentar en `docs/admin_panel_migration.md`.
9. **Tests**:
- `e2e/test_admin_login.sh` — MAS OIDC + scope admin valido -> acceso.
- `e2e/test_admin_login_denied.sh` — user no-admin recibe 403.
- `e2e/test_user_deactivate.sh` — flow completo deactivate + verify can't login.
- `e2e/test_room_purge.sh` — purge room + verify gone en Synapse.
- `e2e/test_session_revoke.sh` — revoke sesion MAS + user perdiendo acceso en <30s.
## Funciones del registry a crear
- `synapse_admin_client_go_infra` — wrapper Synapse Admin API.
- `mas_admin_client_go_infra` — wrapper MAS admin API (`/api/admin/v1/...`).
- `livekit_admin_client_go_infra``RoomService.ListRooms`, kick participant, etc.
- `oidc_admin_middleware_go_infra` — middleware Go que valida scope admin en cookie/Bearer.
- `UsersTable_ts_ui` — componente Mantine con virtualization + filtros.
- `RoomDetail_ts_ui` — componente con tabs Members/State/Media/Widgets.
- `SessionsList_ts_ui` — lista sesiones + revoke action.
- `StatsSummary_ts_ui` — componente con `@mantine/charts`.
- `FederationStatusPanel_ts_ui` — componente federation diag.
## Acceptance
- [ ] App compila + arranca como container Docker.
- [ ] Login via MAS OIDC con scope admin funciona.
- [ ] User no-admin recibe 403 al intentar entrar.
- [ ] Tabla users con 50+ rows + filtros + actions.
- [ ] Deactivate user end-to-end (verify cannot login despues).
- [ ] Room detail muestra members + state events JSON.
- [ ] Sessions MAS listadas + revoke individual.
- [ ] Stats: counts + media usage + active calls visibles.
- [ ] Tema visual coherente con cliente PC (flow 0010).
## Definition of Done
### Mecanica
- `go build` + `pnpm build` verde.
- Container Docker `<150MB` (Alpine + binary + static).
- Health endpoint `/health` 200.
- E2E suite pasa.
### Cobertura
| Escenario | Evidencia | Resultado |
|---|---|---|
| Golden: admin login + ver users | `e2e/test_admin_full_flow.sh` | tabla con users reales, actions visibles |
| Edge: 5000 users en tabla | benchmark scroll | 60fps, <300MB RAM |
| Edge: user sin admin entra | request directo | 403 + audit log |
| Edge: room con 200 members | view detail | render < 1s, paginacion OK |
| Error: Synapse Admin API caida | mock 500 | UI muestra error claro, no crash |
| Error: MAS session revoke fails | mock 500 | retry + toast error |
### Vida util (>=7 dias)
| Metrica | Umbral | Donde | Ventana |
|---|---|---|---|
| Crashes container | `0` | docker logs | 7 dias |
| Uso real | `>= 2 sesiones/semana` (operador) | nginx access log | 7 dias |
| Latency p95 endpoint /api/users | `< 800ms` (Synapse Admin paginado) | metrics | 7 dias |
| Acciones destructivas auditadas | `100%` (cada delete/revoke con audit row) | local audit DB | continuo |
### Anti-criterios
- NO marcar done si admin panel acepta token sin claim/flag admin.
- NO marcar done si delete room no purga media en DB Synapse.
- NO marcar done si UI deja al operador sin confirmacion en acciones destructivas (deactivate, purge, revoke).
- NO marcar done si lookalike de synapse-admin sin features propias (mejor mantener synapse-admin entonces).
## Notas
**Ventajas reales sobre synapse-admin:**
1. Coherencia visual + Mantine + theme propio.
2. Integracion con `agents_and_robots` (panel agente embedded).
3. Integracion con widgets policy (audit + override capabilities).
4. Integracion con LiveKit calls (ver rooms activos, force-end).
5. Audit log local SQLite con todas las acciones admin (synapse-admin no lo tiene).
6. Extensible — anadir tabs para mesh devices (flow 0009), telemetria, etc.
**Onboarding:**
1. `cd projects/element_agents/apps/matrix_admin_panel`.
2. `make dev` (Go backend + Vite frontend hot reload).
3. Visitar `http://127.0.0.1:8090` -> login MAS dev.
4. Deploy prod: ver `deploy/README.md`.
**Decisiones:**
- Backend Go > Python/Node: alinea con `mautrix-go` + reusa funciones del registry. Binario pequeno, deploy facil.
- Embedded static (Go `embed.FS`): un binario, sin docker multi-stage compleja.
- Audit log local SQLite > Postgres: panel admin no necesita HA, suficiente con SQLite local + backup periodico.
**Gotchas:**
- Synapse Admin API requiere `Bearer <admin_token>` — el panel intercambia OIDC token + admin claim por admin_token (con MAS admin API o con cuenta admin shared).
- MAS admin API esta en `/api/admin/v1/` — version unstable, monitorizar breaking changes.
- Federation tab: si federation deshabilitada (caso actual, ver `homeserver.yaml`), tab muestra "disabled" en vez de error.
**Roadmap post-DoD:**
- Bulk actions (mass deactivate, mass invite).
- Export reports CSV.
- Slack/email alerts en eventos criticos (server cae, MAS down, federation block).
- Multi-tenancy si llegan mas homeservers.
## Capability growth log
- v0.1.0 (2026-05-24) — issue creada.
@@ -0,0 +1,129 @@
---
id: "0164"
title: "Bots agents_and_robots: cryptohelper.Init() cuelga al habilitar encryption=true"
status: pending
priority: high
created: 2026-05-24
related_flows: ["0009"]
related_issues: ["0144", "0162"]
dependencies: ["0162"]
tags: [matrix, e2ee, mautrix, cryptohelper, agents, hang, debug]
---
## Objetivo
Que los agents de `agents_and_robots` (`agent-wsl-lucas`, `agent-windows-lucas`, y futuros) puedan operar con `encryption.enabled=true` en su `config.yaml` y **leer + responder en DMs encrypted** (megolm) con el operator. Hoy todos corren con `enabled=false` para no colgarse; consecuencia: bot puede ENVIAR a room encrypted (cleartext que Element marca como warning) pero NO LEE replies del operator (megolm cifra, bot no descifra) → chat bidireccional roto.
Bloquea Flow 0009 DoD ("Element → PC interaction working") en el camino encrypted.
## Contexto
- mautrix-go v0.21.1 con cryptohelper (tag `goolm` pure-Go).
- Synapse en VPS organic-machine.com con MSC3861/MAS activo (issue 0162 done 2026-05-24).
- `encryption_enabled_by_default_for_room_type` activo en Synapse → TODA DM nueva nace con `m.megolm.v1.aes-sha2` (no override client-side).
- Bots usan password tokens (no application_service). Tokens emitidos pre-migracion siguen validos (verificado: `/account/whoami` OK con bot token post-MAS).
- `verify.sh agent-windows-lucas` corrio OK: genero crypto.db, upload cross-signing keys, escribio `SSSS_RECOVERY_KEY_AGENT_WINDOWS_LUCAS` en `.env`.
## Reproduccion
```bash
# En VPS, agent-windows-lucas:
sudo sed -i 's/enabled: false/enabled: true/' agents/agent-windows-lucas/config.yaml
sudo systemctl restart agents_and_robots
sleep 30
# Bot stuck:
sudo tail logs/agent-windows-lucas/2026-05-24.jsonl
# Last line forever: "initializing e2ee" — runner nunca llega a "starting matrix sync"
# /agents API endpoint reports running=false
```
## Diagnostico actual (incompleto)
SIGQUIT al proceso launcher revelo bots NO-encrypted en `Listener.Run → SyncWithContext` (normal). NO se pudo aislar la stack de **windows-lucas** durante hang — necesita pprof targeted o log adicional dentro de `InitCrypto`.
Hipotesis (ordenadas):
| ID | Hipotesis | Evidencia que la apoya | Como confirmar |
|---|---|---|---|
| H1 | `cryptohelper.Init()` bloquea en primer `/keys/device_signing/upload` por UIA — MAS no acepta el formato auth heredado | MAS recien activo, password_config disabled, mautrix-go usa UIA password flow | inyectar log antes/despues de cada llamada en `cryptohelper.Init` |
| H2 | `cryptohelper.Init()` bloquea en `OlmMachine.Load` por `crypto.db` schema mismatch | crypto.db generado por `cmd/verify` puede tener schema distinto al que cryptohelper espera | reset crypto.db + dejar que cryptohelper bootstrap solo (sin verify.sh) |
| H3 | El listener trata de hacer initial sync ANTES de e2ee init terminar, deadlock en mutex | "starting matrix sync" NUNCA aparece post-`initializing e2ee` | revisar order en `devagents/runtime.go` |
| H4 | Pickle key mismatch entre verify.sh (lo recibe en hex) y runtime (lo decodifica diferente) | Provision-script genero base64; nosotros pusimos hex; runtime acepta hex? | log de pickle key length en runtime |
## Tareas
### Fase 1 — Diagnostico
1.1. Inyectar logging EN `shell/matrix/client.go::InitCrypto` antes/despues de cada paso (cryptohelper construct, Init, OlmMachine.Load, etc) para identificar la linea que bloquea.
1.2. Reproducir hang en agent test aislado (`agent-e2ee-test`):
- Crear bot fresh con provision-agent-user.sh
- Activar encryption=true
- Restart launcher
- Capturar stack
1.3. Con stack identificado, decidir cual hipotesis (H1-H4) aplica.
### Fase 2 — Fix segun hipotesis
- **Si H1 (MAS UIA)**: investigar si mautrix-go v0.21.1 soporta MSC3861 UIA. Si no: bump a v0.22+ que soporta o usar `device_signing/upload` con SSSS-protected path.
- **Si H2 (schema mismatch)**: dejar cryptohelper bootstrap solo, NO usar verify.sh primero. Verify.sh queda como "post-bootstrap repair".
- **Si H3 (sync deadlock)**: refactor `devagents/runtime.go` para que e2ee init complete antes de spawn listener.
- **Si H4 (pickle key)**: arreglar provision-agent-user.sh para generar pickle key como hex.
### Fase 3 — Validacion (DoD triada)
#### Mecanica
- Bot con `encryption.enabled=true` start OK (running=true en /agents API).
- No hang en logs (paso de "initializing e2ee" → "starting matrix sync" en < 30s).
- Build limpio `go build -tags goolm`.
#### Cobertura
| Escenario | Cmd / evidencia | Resultado |
|---|---|---|
| Golden: operator envia mensaje encrypted en DM, bot lee + responde encrypted | Element web → `#agent-windows-lucas` DM → "hola" | bot responde en < 15s, log muestra decrypted msg + claude_code_response + encrypted send |
| Edge: bot reinicia, crypto.db persiste, re-key OK | `sudo systemctl restart agents_and_robots` mid-conversation | bot continua descifrando mensajes anteriores + nuevos sin re-bootstrap |
| Edge: operator reverify device | Element → device list → forget device → re-verify | bot detecta cambio, sigue cifrando OK |
| Error: crypto.db corrupto | `rm crypto.db` mid-run | bot detecta + auto-recovery (per `docs/e2ee.md`) + re-bootstrap < 60s |
| Error: token revoked | revocar via admin API | bot logout limpio + restart picks up nuevo token |
#### Vida util validada (7 dias)
| Metrica | Umbral | Donde | Ventana |
|---|---|---|---|
| Bot uptime con encryption=true | `> 99%` | `/agents/<id>` API | 7 dias |
| Mensajes encrypted leidos | `>= 10` real conversation | `logs/agent-*/...jsonl` decrypted lines | 7 dias |
| Crashes cryptohelper | `0` | journalctl `agents_and_robots` | 7 dias |
| Latency decrypt msg | `p95 < 2s` | log timestamps | 7 dias |
### Anti-criterios
- NO marcar done si bot solo escribe pero no lee.
- NO marcar done si hang reaparece tras reinicio del servicio.
- NO marcar done si solo funciona en 1 bot (debe replicarse: wsl-lucas + windows-lucas + 1 mas).
## Estado actual workaround
- `agent-wsl-lucas`: `encryption.enabled=false`. DM con operator es UNencrypted (probablemente porque fue creada antes de Synapse activar default-encrypt). Funciona bidireccional.
- `agent-windows-lucas`: `encryption.enabled=false`. DM con operator (room `!ymFSupZVqYpOWunuHI` o `!qeuqopdkeYHWdAfMaN`) es ENCRYPTED (Synapse forced). Bot envia clear-text → operator ve mensaje + warning. Operator reply encrypted → bot NO lee.
## Funciones del registry candidatas (post-fix)
- `mautrix_cryptohelper_init_with_timeout_go_infra` — wrapper con context.WithTimeout para evitar hang infinito.
- `agent_e2ee_bootstrap_bash_pipelines` — pipeline: provision agent → set encryption=true → verify.sh → restart + wait healthy.
## Notas
**Pickle key format bug**: `provision-agent-user.sh` genera base64 (`openssl rand -base64 32`). `cmd/verify` espera hex. Fix in scope de este issue o nuevo issue (`0165-provision-pickle-key-hex.md`).
**Subagent investigation report** (2026-05-24) confirmo:
- E2EE machinery YA existe end-to-end (InitCrypto, FetchCrossSigningKeys, SignOwnDevice, verify.sh).
- docs/e2ee.md cubre failure modes conocidos.
- mautrix-go v0.21.1 puede tener bug pre-MSC3861-aware con MAS.
**Pendiente upstream check**: mautrix-go release notes v0.22+ para MSC3861 support. Si esta soportado, bump version es probablemente el fix.
## Capability growth log
- v0.1.0 (2026-05-24) — issue creado tras reproducir hang post-MAS migration con verify.sh OK pero cryptohelper.Init aun cuelga.
@@ -0,0 +1,65 @@
---
id: "0165"
title: "Cifrar media_store/ Synapse con LUKS at-rest"
status: pendiente
type: infra
domain:
- matrix
scope: app:element_matrix_chat
priority: media
depends: []
blocks: []
related: ["0162"]
created: 2026-05-24
updated: 2026-05-24
tags: [matrix, synapse, encryption, security, luks]
---
# 0165 — Cifrar media_store/ Synapse con LUKS at-rest
**Status:** pendiente
**Created:** 2026-05-24
**Type:** infra
**Priority:** media
**Domain:** matrix
**Scope:** app:element_matrix_chat
**Depends:**
**Blocks:**
## Problema
`synapse_data/media_store/` contiene archivos subidos (fotos, voice messages, attachments) + thumbnails. Rooms NO-E2EE: media cleartext en disco. Tabla `media_repository` Postgres: filename/mime/uploader/room_id siempre cleartext. Riesgo: VPS provider snapshot disk, backups desencriptados, disco fisico.
## Objetivo
`media_store/` cifrado at-rest. Synapse arranca y sirve media normal. Decrypt automatico via keyfile en TPM o passphrase al boot.
## Plan
1. Decidir estrategia: LUKS container file-based (loop device) vs LUKS sobre volumen Docker dedicado.
2. Crear LUKS container 50GB (ajustar segun crecimiento previsto).
3. Montar como `/home/ubuntu/CodeProyects/element_matrix_chat/synapse_data/media_store_encrypted/`.
4. Stop Synapse → rsync `media_store/``media_store_encrypted/` → swap mountpoint.
5. Verificar Synapse sirve thumbnails + uploads OK.
6. Configurar auto-unlock via keyfile en `/root/.luks-media.key` con permisos 0400.
7. Documentar recovery passphrase en `pass` (entry `matrix/luks-media-passphrase`).
## Acceptance
- [ ] `media_store/` montado sobre LUKS, `lsblk -f` muestra crypto_LUKS.
- [ ] Synapse arranca tras reboot completo del VPS sin intervencion manual.
- [ ] Test: subir imagen via Element, verificar thumb generado.
- [ ] Test: leer media_store via `dd if=/dev/sdX` directo retorna basura cifrada.
- [ ] Passphrase backed up en `pass`.
## Definition of Done
- [ ] Repetibilidad: reboot VPS, media accesible sin intervencion.
- [ ] Observabilidad: log entry en `journalctl -u systemd-cryptsetup@*`.
- [ ] User-facing: clientes Element no notan diferencia.
- [ ] Recovery probado: detach LUKS y reattach con passphrase.
## Notas
LUKS solo protege at-rest. VPS provider con acceso a RAM viva ve plaintext via memory dump. Sin TPM atestado, utilidad real = anti-snapshot/anti-backup-leak/anti-physical-theft.
Caveat: si keyfile vive en mismo disco que LUKS device, no protege contra disk theft. Mover keyfile a USB removible o TPM2 (`systemd-cryptenroll`).
@@ -0,0 +1,152 @@
---
id: "0128"
title: "agents_and_robots: HTTP API + SSE + apikey + TLS subdominio"
status: pendiente
type: feature
domain:
- agents
- infra
- deploy
scope: app
priority: alta
depends: []
blocks:
- "0129"
related: []
created: 2026-05-22
updated: 2026-05-22
tags: [agents_and_robots, http, sse, apikey, traefik, systemd]
dod_evidence_schema:
- id: build_ok
kind: cmd
expected: "cd projects/element_agents/apps/agents_and_robots && go build -tags goolm ./cmd/launcher → exit 0"
required: true
- id: api_list_authorized
kind: cmd
expected: "curl -fsS -H 'Authorization: Bearer $AGENTS_API_KEY' https://agents.organic-machine.com/agents devuelve JSON con N>=7 agentes"
required: true
- id: api_list_unauthorized_401
kind: cmd
expected: "curl -s -o /dev/null -w '%{http_code}' https://agents.organic-machine.com/agents == 401"
required: true
- id: api_start_stop_roundtrip
kind: cmd
expected: "POST /agents/test-bot/stop → POST /agents/test-bot/start: status running confirmado via GET /agents/test-bot tras 2s"
required: true
- id: sse_logs_streaming
kind: cmd
expected: "curl -N -H 'Authorization: Bearer $KEY' https://agents.organic-machine.com/sse/agents/assistant-bot/logs entrega >=1 line en 5s con agente activo"
required: true
- id: sse_status_broadcast
kind: cmd
expected: "curl -N /sse/status recibe evento {agent_id, old_status, new_status} tras stop/start manual"
required: true
- id: systemd_active
kind: cmd
expected: "ssh organic-machine.com 'systemctl is-active agents_and_robots.service' == active"
required: true
- id: traefik_route
kind: url
expected: "agents.organic-machine.com resuelve y devuelve cert LE valido (curl -vI muestra subject CN=agents.organic-machine.com)"
required: true
- id: app_md_drift_fixed
kind: cmd
expected: "fn doctor services-spec apps/element_agents/apps/agents_and_robots reporta OK (sin drift runtime/systemd)"
required: true
---
# 0128 — agents_and_robots HTTP API + SSE + apikey + TLS
## Contexto
Hoy `agents_and_robots` solo expone control via `agentctl` CLI local (filesystem-based, `shell/process.Manager`). No hay forma remota de gestionar agentes.
Necesitamos backend HTTP seguro para que un frontend local C++ (issue 0129) pueda listar, start/stop/restart agentes, y streamear logs/status en vivo.
## Decision
**Integrar daemon HTTP DENTRO de `cmd/launcher`** como goroutine. Comparte `process.Manager` + acceso a `shell/memory/*.db` + Matrix clients. Un solo proceso, sin drift entre daemon y supervisor.
**Auth:** `Authorization: Bearer <AGENTS_API_KEY>` con `subtle.ConstantTimeCompare`. Clave 32 bytes hex en `.env` (`AGENTS_API_KEY`). 401 sin header o key invalida.
**TLS:** Traefik en VPS organic-machine.com con LE cert auto. Subdominio `agents.organic-machine.com` (DNS A record nuevo → IP del VPS). Ruta Traefik `agents.organic-machine.com → 127.0.0.1:8487`.
**SSE in-memory pubsub.** NATS OFF de momento (1 cliente local, broker = overhead). Documentar TODO en app.md para anadir bus si llega 2do consumidor.
## Scope v0.1 (lean)
| Verbo | Path | Wrap |
|---|---|---|
| GET | `/health` | 200 OK sin auth (liveness) |
| GET | `/agents` | `Scan` + `StatusAll` + `msg_count_24h` (query `shell/memory/*.db`) |
| GET | `/agents/{id}` | detail + config + `LogTail(200)` |
| POST | `/agents/{id}/start` | `Manager.Start` |
| POST | `/agents/{id}/stop` | `Manager.Stop` |
| POST | `/agents/{id}/restart` | Stop+Start con espera health |
| GET | `/agents/{id}/logs?n=200` | `LogTail` snapshot |
**SSE:**
- `GET /sse/status` — broadcast cambios de status (poll cada 2s + diff)
- `GET /sse/agents/{id}/logs` — tail -f del logfile, emite line events
**Fuera de scope v0.1** (queda v0.2):
- POST `/agents/{id}/message` (send Matrix message)
- PUT `/agents/{id}/config` (config edit)
- SSE messages stream
## Tareas
1. **Nuevo paquete `internal/api`** con server HTTP (stdlib `net/http`, sin gin/echo).
- `api.New(mgr *process.Manager, apiKey string, port int) *Server`
- `Server.Run(ctx) error` arranca y bloquea hasta ctx done.
- Middleware: log + auth + recover.
2. **Handlers REST** sobre `process.Manager`. Tests unitarios con mock manager.
3. **SSE pubsub in-memory** (`internal/api/pubsub.go`):
- `Bus` con `Subscribe(topic) <-chan event` + `Publish(topic, event)`.
- Poller goroutine que llama `StatusAll` cada 2s y publica diffs.
- Tail goroutine por logfile (`file_tail_follow` — buscar en registry o crear).
4. **Integrar en launcher**`cmd/launcher/main.go` arranca `api.Server` en goroutine si `--api-port > 0`.
5. **Crear systemd unit** `/etc/systemd/system/agents_and_robots.service` con `Restart=always`, `EnvironmentFile=.env`, `ExecStart=.../bin/launcher --log-level info --api-port 8487`.
6. **Traefik route + DNS:**
- Anadir `agents.organic-machine.com` en DNS (A record).
- Anadir config Traefik (label en docker-compose del stack o file provider) apuntando a `127.0.0.1:8487`.
7. **Fix drift app.md**`runtime: systemd-system` ahora es verdad. Verificar con `fn doctor services-spec`.
8. **Tests:**
- Go: pkg `internal/api` con httptest.
- e2e: `e2e_checks` en `app.md` con curl smoke.
9. **Deploy:**
- `rsync_deploy_bash_infra` o `deploy_server` target nuevo.
- Generar `AGENTS_API_KEY` con `openssl rand -hex 32` y escribir `.env` remoto.
- `systemctl enable --now agents_and_robots.service`.
## Funciones del registry a usar / proponer
Buscar antes de codear:
- `mcp__registry__fn_search query="tail follow file" lang="go"` — ¿existe `file_tail_follow_go_infra`? Si no, delegar a fn-constructor.
- `mcp__registry__fn_search query="http auth bearer" lang="go"` — middleware auth.
- `mcp__registry__fn_search query="sse server" lang="go"` — helper SSE.
- `systemd_generate_unit_go_infra` + `systemd_install_go_infra` — generar/instalar unit.
## Acceptance
- [ ] `curl -fsS -H 'Authorization: Bearer $KEY' https://agents.organic-machine.com/agents` devuelve lista correcta.
- [ ] Sin header → 401. Con key invalida → 401. Key valida → 200.
- [ ] Start/Stop/Restart cambian estado real del proceso (verificable con `ps`).
- [ ] SSE logs entrega lineas en menos de 1s de aparecer en el archivo.
- [ ] SSE status broadcast tras stop/start manual.
- [ ] systemd unit activo y reinicia tras kill -9.
- [ ] `fn doctor services-spec` reporta OK.
- [ ] Tests Go pasan.
## DoD humano
- **Donde:** terminal local → `curl https://agents.organic-machine.com/agents`. SSE verificable con `curl -N`.
- **Latencia:** SSE log lag < 1s. REST list < 200ms.
- **Onboarding:** README de agents_and_robots actualizado con seccion "HTTP API" + ejemplos curl.
## Riesgos
- DNS propagation puede tardar (configurar con TTL bajo).
- Traefik en este VPS: verificar si esta gestionado por Coolify o standalone — anadir ruta donde corresponda.
- `LogTail` actual solo lee snapshot — necesitamos `tail -f` real para SSE. Si no existe en el registry, ronda previa.
@@ -0,0 +1,180 @@
---
id: "0129"
title: "agents_dashboard: C++ ImGui frontend para gestionar agentes Matrix"
status: pendiente
type: feature
domain:
- agents
- tui
scope: app
priority: alta
depends:
- "0128"
blocks: []
related: []
created: 2026-05-22
updated: 2026-05-22
tags: [cpp, imgui, agents, dashboard, sse, http-client]
dod_evidence_schema:
- id: scaffold_ok
kind: cmd
expected: "ls projects/element_agents/apps/agents_dashboard/{app.md,main.cpp,CMakeLists.txt,.git} todos existen"
required: true
- id: build_windows
kind: cmd
expected: "cmake --build cpp/build/windows --target agents_dashboard -j → exit 0"
required: true
- id: appicon_embedded
kind: cmd
expected: "x86_64-w64-mingw32-objdump -h cpp/build/windows/apps/agents_dashboard/agents_dashboard.exe | grep .rsrc"
required: true
- id: hub_card_visible
kind: screenshot
expected: "App Hub muestra tarjeta agents_dashboard con icono robot violeta + description correcta"
required: true
- id: connection_flow
kind: screenshot
expected: "Panel Connection con base_url + apikey input, LED verde tras handshake exitoso con backend"
required: true
- id: agents_table_populated
kind: screenshot
expected: "Tabla Agents muestra >=7 filas con id/status/uptime/msg_24h + botones accion"
required: true
- id: start_stop_works
kind: screenshot
expected: "Click stop sobre test-bot lo apaga (status cambia a stopped en menos de 2s); click start lo reinicia"
required: true
- id: logs_sse_streaming
kind: screenshot
expected: "Panel Logs streamea lineas en vivo de assistant-bot (lineas nuevas aparecen sin pulsar refresh)"
required: true
- id: apikey_encrypted_local
kind: cmd
expected: "strings cpp/build/windows/apps/agents_dashboard/local_files/agents_dashboard.db | grep -v '<plaintext apikey>' (apikey no aparece en claro)"
required: true
- id: e2e_self_test
kind: cmd
expected: "agents_dashboard.exe --self-test exit 0 (verifica subsistemas: GL loader, http client, SSE client, DB local)"
required: true
---
# 0129 — agents_dashboard C++ ImGui frontend
## Contexto
Cuando 0128 cierre, el backend `agents_and_robots` expondra HTTPS API + SSE en `agents.organic-machine.com` con apikey. Necesitamos frontend local C++ ImGui que consuma esa API y permita gestionar agentes sin SSH ni terminal.
## Decision
C++ ImGui app en `projects/element_agents/apps/agents_dashboard/`. Sub-repo Gitea `dataforge/agents_dashboard`. Integrada en App Hub con icono propio.
Scope v0.1 = lo que 0128 expone: list + start/stop/restart + logs SSE. v0.2 anade send-message + config-edit cuando backend los exponga.
## Tareas
### 1. Scaffold (REGLA: scaffolder canonico, NUNCA a mano)
```bash
./fn run init_cpp_app agents_dashboard \
--project element_agents \
--desc "Frontend C++ ImGui para gestionar agentes Matrix de agents_and_robots via HTTPS+apikey, SSE para logs/status en vivo"
```
Tras scaffold:
- `git init` dentro de `projects/element_agents/apps/agents_dashboard/` (regla `apps_subrepo.md`).
- Trio `app.md`: `description` + `icon.phosphor: "robot"` + `icon.accent: "#8b5cf6"`.
- `./fn run regenerate_app_icons agents_dashboard`.
- `./fn run refresh_app_hub` para que aparezca en el hub.
### 2. Funciones del registry — buscar primero
| Necesidad | Buscar en registry | Si falta |
|---|---|---|
| HTTP client C++ (sync GET/POST + Bearer + JSON body) | `mcp__registry__fn_search query="http client" lang="cpp"` | Delegar `fn-constructor`: `http_client_cpp_infra` con libcurl |
| SSE client C++ | `sse_client_cpp_core` (FRESH 7d) | ✓ reuso directo |
| JSON parse/serialize C++ | buscar nlohmann wrapper | Si falta, vendoring `cpp/vendor/json.hpp` (single-header) |
| Data table | `data_table_cpp_viz` | ✓ reuso |
| Secret store local (DPAPI Windows) | buscar | Si falta: `secret_store_cpp_infra` (DPAPI wrap, base64 fallback Linux) |
| Ring buffer C++ | buscar | Si falta: `ring_buffer_cpp_core` |
Delegacion paralela: **una sola llamada Agent con N tool_use blocks paralelos** para las que falten (regla `delegation.md`).
### 3. Paneles UI
- **Connection** — `base_url` input + apikey input (mask) + boton "Test" → GET /health + GET /agents. LED estado SSE (gris/amarillo/verde/rojo). Save credentials en `local_files/agents_dashboard.db` encriptadas via secret_store.
- **Agents** — `data_table_cpp_viz` con cols:
- id (texto)
- status (icono colored: running=green, stopped=gray, crashed=red)
- uptime (humanized)
- msg_24h (numero)
- actions (botones `▶ ⏹ ↻` por fila)
- Filtro por substring + sort por col.
- **Logs** — selector agente (combo) + tail viewport (ring buffer 5000 lineas) + autoscroll toggle + boton "Pause". Stream via `/sse/agents/{id}/logs`.
- **Status feed** — panel collapsible con eventos del `/sse/status` (timeline reciente).
### 4. Persistencia local
- `<exe_dir>/local_files/agents_dashboard.db` (SQLite via funciones del registry o sqlite3 directo).
- Schema migraciones en `migrations/001_init.sql`:
```sql
CREATE TABLE connections (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
base_url TEXT NOT NULL,
apikey_encrypted BLOB NOT NULL,
last_used DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE app_state (
key TEXT PRIMARY KEY,
value TEXT
);
```
- `app_settings.ini` via `fn_ui::settings_*` (theme, layout).
- apikey cifrada con DPAPI Windows (clave nunca abandona la maquina).
### 5. Build + deploy local
- CMake target `agents_dashboard` en `cpp/CMakeLists.txt` (auto via scaffolder).
- Build Windows: `cmake --build cpp/build/windows --target agents_dashboard -j`.
- Deploy local: `./fn run redeploy_cpp_app_windows agents_dashboard projects/element_agents/apps/agents_dashboard --build`.
- Icono via windres (gestionado por `add_imgui_app`).
### 6. Tests + e2e_checks
```yaml
e2e_checks:
- id: build
cmd: "cmake --build cpp/build/windows --target agents_dashboard -j"
timeout_s: 180
- id: self_test
cmd: "./cpp/build/windows/apps/agents_dashboard/agents_dashboard.exe --self-test"
timeout_s: 30
- id: pytest_mock
cmd: "cd projects/element_agents/apps/agents_dashboard/tests && python3 -m pytest -x -q"
timeout_s: 60
```
Mock server pytest emula 0128 (list/start/stop + SSE) y verifica que la app C++ conecta + popula tabla + start/stop funciona en headless con `--capture` mode.
## Acceptance
- [ ] App arranca, muestra Connection panel.
- [ ] Tras meter apikey valida → tabla Agents populated con datos reales de VPS.
- [ ] Stop/Start desde UI cambia estado real del agente en VPS.
- [ ] Logs streamea lineas nuevas sin polling.
- [ ] Cerrar y reabrir app → credentials persisten (cifradas).
- [ ] Sin red / apikey invalida → error visible, app no crashea.
- [ ] `--self-test` exit 0.
- [ ] Visible en App Hub con icono + description correctos.
## DoD humano
- **Donde:** Windows Desktop → App Hub → Click "agents_dashboard".
- **Latencia:** logs SSE < 1s lag. Lista agents < 200ms tras handshake.
- **Onboarding:** First-run wizard pide base_url + apikey; tooltip explica donde obtener la key (gestor de secretos del VPS).
## Riesgos
- libcurl en Windows mingw-w64: cross-compile setup. Si `http_client_cpp_infra` no existe, dedicar tiempo al wrapper antes de UI.
- DPAPI solo Windows: fallback Linux puede ser texto plano con permisos 0600 + warning visible en UI.
- SSE reconnect logic: backoff exponencial + indicador de estado claro.
@@ -0,0 +1,235 @@
---
id: "0131"
title: "agents v0.2: control per-agent unified mode + uptime/msg_24h + data_table_cpp_viz + clear/cache actions"
status: pendiente
type: feature
domain:
- agents
- tui
- infra
scope: app
priority: alta
depends:
- "0128"
- "0129"
blocks: []
related: []
created: 2026-05-22
updated: 2026-05-22
tags: [agents_and_robots, agents_dashboard, http, unified-mode, data-table, control]
dod_evidence_schema:
# Backend: agents_and_robots
- id: build_backend
kind: cmd
expected: "cd projects/element_agents/apps/agents_and_robots && go build -tags goolm ./... → exit 0"
required: true
- id: tests_backend
kind: cmd
expected: "cd projects/element_agents/apps/agents_and_robots && go test -tags goolm -count=1 ./internal/api/... → exit 0"
required: true
- id: stop_unified_works
kind: cmd
expected: "POST /agents/test-bot/stop devuelve {status:stopped}; GET /agents/test-bot → running=false en <2s"
required: true
- id: start_unified_works
kind: cmd
expected: "POST /agents/test-bot/start tras stop devuelve {status:started}; GET /agents/test-bot → running=true en <5s"
required: true
- id: restart_unified_works
kind: cmd
expected: "POST /agents/test-bot/restart sobre agente running deja running=true en <8s sin error"
required: true
- id: clear_memory_endpoint
kind: cmd
expected: "POST /agents/test-bot/clear_memory devuelve {status:cleared, messages_deleted:N}; SELECT COUNT(*) FROM messages WHERE agent_id='test-bot' == 0"
required: true
- id: delete_cache_endpoint
kind: cmd
expected: "POST /agents/test-bot/delete_cache devuelve {status:cleared, paths_deleted:[...]}; verificar que crypto.db cache borrado"
required: true
- id: uptime_exposed
kind: cmd
expected: "GET /agents incluye campo uptime_seconds:int >0 para agents running"
required: true
- id: msg_24h_exposed
kind: cmd
expected: "GET /agents incluye campo messages_24h:int (puede ser 0) calculado de tabla messages"
required: true
# Frontend: agents_dashboard
- id: build_frontend
kind: cmd
expected: "cmake --build cpp/build/windows --target agents_dashboard -j → exit 0"
required: true
- id: data_table_cpp_viz_used
kind: cmd
expected: "grep -E 'BeginTable|EndTable' projects/element_agents/apps/agents_dashboard/main.cpp devuelve 0 lineas (migrado a data_table_cpp_viz); grep data_table_cpp_viz app.md uses_functions = 1"
required: true
- id: per_agent_buttons_rendered
kind: screenshot
expected: "Tabla Agents muestra >=5 botones por fila: Start, Stop, Restart, Clear Memory, Delete Cache (puede iconos+tooltip)"
required: true
- id: uptime_visible
kind: screenshot
expected: "Tabla Agents columna uptime muestra valor humanizado (ej 12h, 3d) para agents running"
required: true
- id: msg_24h_visible
kind: screenshot
expected: "Tabla Agents columna msg/24h muestra contador real (no 'instances' como hack)"
required: true
# E2E: pytest
- id: e2e_tests_pass
kind: cmd
expected: "AGENTS_API_KEY=... pytest tests/test_connect_e2e.py → todos PASS (>=20 tests)"
required: true
- id: e2e_control_roundtrip
kind: cmd
expected: "Nuevo test_control_roundtrip: stop → poll running=false → start → poll running=true → restart → poll running=true. Todo dentro de 30s."
required: true
- id: e2e_clear_memory
kind: cmd
expected: "Nuevo test_clear_memory: insert filas en messages → POST /clear_memory → COUNT == 0"
required: true
---
# 0131 — agents v0.2: full per-agent control + data_table + nuevos botones
## Contexto
v0.1 (issues 0128+0129) entrego:
- HTTP API + apikey + TLS + SSE
- C++ frontend con Connection/Agents/Logs/Status feed
- Tabla agents con `running` derivado de backend
**Gaps detectados durante uso real:**
1. **Control individual roto en unified mode** — Manager.Start/Stop esperan PID files por agente; en unified mode no existen → endpoints devuelven errores confusos ("not running" sobre agente que SI corre).
2. **No hay uptime ni msg_24h reales** — backend no expone esos campos. UI muestra `instances` como hack para msg_24h.
3. **Faltan acciones de gestion** — clear memory (mensajes en SQLite), delete cache (crypto E2EE), reset state.
4. **Tabla manual**`ImGui::BeginTable` inline en main.cpp. El registry tiene `data_table_cpp_viz` (funcion canonica). Migrar.
## Scope v0.2
### Backend (`projects/element_agents/apps/agents_and_robots/`)
**1. Control per-agent en unified mode**
Hoy launcher arranca todos los agents como goroutines bajo 1 PID via mode "unified". `Manager.Start/Stop/Restart` actuales solo funcionan en mode multi-process (PID por agente).
Anadir registro de cancel-context por agente en el launcher:
- Por cada agente que arranca como goroutine, guardar `context.CancelFunc` en `Manager.unifiedCancels map[string]context.CancelFunc`.
- `Manager.StopUnifiedAgent(id)` llama cancel del agente especifico.
- `Manager.StartUnifiedAgent(id)` re-arranca solo ese agente sin restart del launcher entero.
- `Manager.RestartUnifiedAgent(id)` = Stop + Start.
Handlers `handleStart/Stop/Restart` autodetectan via `IsUnifiedRunning()` y delegan a las nuevas variantes unified.
**2. Uptime real**
- `Manager.startedAt map[string]time.Time` poblado al arrancar cada goroutine.
- En `AgentStatus.UptimeSeconds`, calcular `time.Since(startedAt[id]).Seconds()` si running, else 0.
- Exponer en `agentResponse` como `uptime_seconds: int`.
**3. Messages_24h**
Cada agent persiste mensajes en su SQLite (`agents/<id>/data/memory.db`). El handler `handleListAgents` debe agregar por agente:
- Abrir DB del agente readonly
- `SELECT COUNT(*) FROM messages WHERE created_at > datetime('now', '-24 hours')`
- Cache 30s para no abrir DB en cada request
Exponer como `messages_24h: int`.
**4. Endpoint `POST /agents/{id}/clear_memory`**
- Stop agent (si running)
- Open agent's memory.db
- `DELETE FROM messages` + `DELETE FROM facts`
- Optionally start back si estaba running (deber `?restart=true` opcional)
- Return `{status:"cleared", messages_deleted:N, facts_deleted:M}`
**5. Endpoint `POST /agents/{id}/delete_cache`**
- Stop agent (si running)
- Delete `agents/<id>/data/crypto/` directory (E2EE cache; agent re-init on next start)
- Delete `agents/<id>/data/cache/*` si existe
- Return `{status:"cleared", paths_deleted:[...]}`
- Optionally start back si estaba running (`?restart=true`)
NOTA: delete_cache fuerza re-verificacion E2EE. El agente debe re-autenticarse via SSSS recovery key on next start. Documentar.
### Frontend (`projects/element_agents/apps/agents_dashboard/`)
**1. Migrar a `data_table_cpp_viz`**
Hoy main.cpp usa `ImGui::BeginTable` inline. Sustituir por `data_table::Table` del registry (funcion `data_table_cpp_viz`). Anadir a `app.md::uses_functions`. Verificar via `fn doctor cpp-apps` que la app pasa de `CANDIDATE` a limpio.
**2. Columnas tabla:**
- id
- status icon (running=green, stopped=gray, disabled=yellow, crashed=red)
- uptime (humanized via `human_duration_secs`)
- msg/24h (numero real, NO instances)
- actions (5 botones agrupados):
- `▶ Start` (disabled si running)
- `⏹ Stop` (disabled si !running)
- `↻ Restart`
- `🧠 Clear Memory` (confirmacion modal)
- `🗑 Delete Cache` (confirmacion modal)
**3. Sort + filter** mantener via data_table_cpp_viz API.
### E2E (`tests/`)
Anadir 7 tests nuevos:
- `test_control_roundtrip` — stop → poll → start → poll → restart → poll. Usa `test-bot`.
- `test_clear_memory` — POST clear_memory, verifica COUNT(*) FROM messages == 0.
- `test_delete_cache` — POST delete_cache, verifica crypto/ borrado.
- `test_uptime_field_present` — /agents response incluye uptime_seconds key
- `test_msg_24h_field_present` — /agents response incluye messages_24h key
- `test_unified_stop_does_not_kill_launcher` — tras stop de 1 agente, otros siguen running.
- `test_clear_memory_requires_apikey` — sin Bearer → 401
## Tareas
### Fase A — Backend (agents_and_robots)
1. Agregar `unifiedCancels map[string]context.CancelFunc` + `startedAt map[string]time.Time` + mutex a `shell/process.Manager`.
2. Hook en `launcher` runtime para registrar/desregistrar cancels al arrancar/parar cada agent goroutine.
3. Implementar `StopUnifiedAgent`, `StartUnifiedAgent`, `RestartUnifiedAgent` (Stop+Start).
4. Refactor handlers `handleStartAgent/Stop/Restart` para autodetect unified vs multi.
5. Anadir `uptime_seconds` y `messages_24h` a `AgentResponse`. Implementar query 24h con cache 30s.
6. Implementar handlers `handleClearMemory`, `handleDeleteCache`.
7. Anadir rutas en `server.go`.
8. Tests Go unit `internal/api/*_test.go`.
### Fase B — Frontend (agents_dashboard)
1. Cambiar `parse_agents` para leer `uptime_seconds` y `messages_24h` del backend.
2. Migrar tabla a `data_table_cpp_viz`. Mantener filter + sort.
3. Anadir 5 botones por fila (Start/Stop/Restart/Clear/Delete).
4. Confirmacion modal para Clear/Delete.
5. Actualizar app.md::uses_functions con `data_table_cpp_viz`.
### Fase C — E2E + verify
1. Anadir 7 pytest tests.
2. Run all e2e from registry venv. >=20 tests pass.
3. Rebuild .exe + redeploy Windows.
4. Visual confirm: botones, uptime, msg_24h.
## Acceptance
- [ ] All 14 DoD items green (cmd + screenshots).
- [ ] >=20 e2e tests passing.
- [ ] App C++ deployed to Windows Desktop, visible buttons + working roundtrip.
- [ ] Backend unit tests pass.
- [ ] No regression: 0128 + 0129 funcionalidad existente intacta (curl smoke del v0.1 sigue green).
## DoD humano
- **Donde**: Windows Desktop → agents_dashboard.exe → tabla Agents.
- **Latencia**: stop → running=false reflected in UI within 2s (via SSE status diff). msg/24h refresh cada 30s ok.
- **Onboarding**: tooltip en boton "Clear Memory" explica que borra mensajes; "Delete Cache" explica que el agente tendra que re-autenticar via SSSS al volver a arrancar.
## Riesgos
- Refactor de Manager unified-mode toca el ciclo de vida del launcher (paso ~7 del create_agent pipeline). Tests existentes deben pasar.
- delete_cache borra crypto store; agente debe poder re-verify via env var `SSSS_RECOVERY_KEY_<NORM>`. Si esa env var no esta, agente queda en estado degradado. Validar antes de borrar.
- data_table_cpp_viz puede tener limites de API que ImGui inline no tiene (sort custom, alignment). Verificar antes de migrar.
@@ -0,0 +1,115 @@
---
id: "0166"
title: "Desplegar TURN para LiveKit (coturn o integrado)"
status: done
type: infra
domain:
- matrix
scope: app:element_matrix_chat
priority: alta
depends: []
blocks: []
related: ["0167", "0168"]
created: 2026-05-24
updated: 2026-05-24
tags: [matrix, livekit, webrtc, turn, nat]
---
# 0166 — Desplegar TURN para LiveKit (coturn o integrado)
**Status:** pendiente
**Created:** 2026-05-24
**Type:** infra
**Priority:** alta
**Domain:** matrix
**Scope:** app:element_matrix_chat
**Depends:**
**Blocks:**
## Problema
LiveKit corre sin TURN (`turn.enabled: false` en `configs/livekit/livekit.yaml`). Usuarios detras de NAT simetrico (CGNAT movil 4G/5G, redes corporativas con firewall estricto, hotel WiFi) NO pueden establecer call — WebRTC ICE direct/reflexive falla. Calls fallan silenciosos para ~10-20% usuarios.
## Objetivo
Calls funcionan en cualquier red. Element X movil sobre 4G CGNAT completa handshake.
## Plan
1. Decidir: coturn standalone vs LiveKit TURN integrado (recomendado: integrado, menos moving parts).
2. Anadir subdominio `turn.organic-machine.com` con Let's Encrypt cert (Traefik).
3. Activar bloque `turn:` en `livekit.yaml`:
```yaml
turn:
enabled: true
domain: "turn.organic-machine.com"
tls_port: 5349
udp_port: 443
external_tls: true
```
4. Abrir puertos VPS firewall: TCP+UDP 443 (best practice — bypassea firewalls corp), TCP 5349.
5. Rotar shared secret TURN.
6. Test: navegador en red corp con `force-tcp` flag → call establecida.
## Acceptance
- [ ] `nc -vz turn.organic-machine.com 443` UDP+TCP OK.
- [ ] Test call Element Web detras de NAT simetrico (movil hotspot tethering) → audio/video pasa.
- [ ] LiveKit logs muestran `TURN allocation` requests servidas.
- [ ] `.well-known/matrix/client` sigue apuntando al `livekit_service_url` JWT correcto.
## Definition of Done
- [ ] Repetibilidad: 5 calls consecutivas desde 5 redes distintas (incluido CGNAT) sin fallo.
- [ ] Observabilidad: dashboard LiveKit muestra TURN vs direct ratio.
- [ ] User-facing: usuario movil 4G inicia call → conecta < 3s.
## Notas
UDP 443 es trick conocido: la mayoria de firewalls corporativos solo dejan 443 (HTTPS) — TURN sobre UDP 443 bypassea sin requerir TCP relay que aumenta latencia.
Alternativa coturn standalone si LiveKit integrado tiene gaps de gestion: `docker run -d coturn/coturn` + config compartida con shared secret de LiveKit.
## Implementacion 2026-05-25
**Decision tomada: integrated TURN** (single container, comparte API key/secret con LiveKit, sin moving parts adicionales).
**Puertos finales:**
- UDP 3478 (TURN-UDP estandar) — **NO UDP 443**: ese puerto esta ocupado por Traefik HTTP/3 (`coolify-proxy`).
- TCP 5349 (TURN-TLS estandar) — libre.
- Cert TLS: wildcard `*.organic-machine.com` extraido de Traefik `acme.json` (DNS-01 LE).
**Subdomain:** `turn-matrix-rtc-320bd4.organic-machine.com` (cubierto por wildcard DNS + wildcard cert; no requiere DNS manual).
**Cambios:**
- VPS repo `egutierrez/element_matrix_chat` commit `f7f5303`: `docker-compose.livekit.yml` expone puertos TURN + monta certs.
- `configs/livekit/livekit.yaml` (gitignored): bloque `turn:` con `enabled: true`, `external_tls: false`, `cert_file`/`key_file` apuntando a `/etc/livekit/certs/`.
- `configs/livekit/certs/{turn-cert.pem,turn-key.pem}` (gitignored): extraidos de `/data/coolify/proxy/acme.json` via `jq | base64 -d`.
- UFW: `3478/udp` + `5349/tcp` ALLOW.
**Verificacion:**
- `nc -vz organic-machine.com 5349` -> succeeded
- `nc -vzu organic-machine.com 3478` -> succeeded
- `openssl s_client -connect turn-matrix-rtc-320bd4.organic-machine.com:5349` -> Verify return code: 0 (ok), wildcard cert servido
- `docker logs livekit` -> `Starting TURN server {portTLS: 5349, portUDP: 3478, externalTLS: false}`
**TODO operador (follow-up, no bloquea cierre):**
1. **Rotacion cert**: Traefik renueva wildcard automaticamente, pero los PEM extraidos a `configs/livekit/certs/` quedan obsoletos. Anadir cron (mensual) o post-renew hook que re-extraiga desde `acme.json` + `docker compose restart livekit`. Script sugerido:
```bash
#!/bin/bash
set -e
ACME=/data/coolify/proxy/acme.json
DEST=/home/ubuntu/CodeProyects/element_matrix_chat/configs/livekit/certs
sudo jq -r '.letsencrypt.Certificates[0].certificate' $ACME | base64 -d > $DEST/turn-cert.pem
sudo jq -r '.letsencrypt.Certificates[0].key' $ACME | base64 -d > $DEST/turn-key.pem
chmod 644 $DEST/turn-cert.pem && chmod 600 $DEST/turn-key.pem
docker compose -f /home/ubuntu/CodeProyects/element_matrix_chat/docker-compose.yml -f /home/ubuntu/CodeProyects/element_matrix_chat/docker-compose.livekit.yml restart livekit
```
2. **DoD usage real** (capa 3 DoD Quality): pendiente test desde CGNAT movil + 5 redes distintas. Acceptance items 1-2 verificables solo con calls reales. Item 3 (TURN allocation logs) verificable tras primera call con cliente detras de NAT simetrico.
3. **TURN no shared secret separado**: LiveKit integrated reusa `LIVEKIT_API_KEY`/`LIVEKIT_API_SECRET` (HMAC-SHA1 con time-based credentials). No requiere rotacion adicional sobre la del API key. Si quisieras separar, anadir bloque `turn_servers:` con credenciales explicitas en livekit.yaml.
4. **Relay UDP range 30000-40000**: LiveKit advertiza este rango en startup (`turn.relay_range_start/end`). Hoy NO esta expuesto en docker-compose. Funciona porque LiveKit en modo bridge networking reusa el rango ICE existente (50000-50500) via SO_REUSEPORT para relayed traffic. Si hay problemas con relays, exponer 30000-40000/udp.
**Backups:** `configs/livekit/livekit.yaml.bak.20260524_224254` + `docker-compose.livekit.yml.bak.20260524_224254` en el VPS.
@@ -0,0 +1,62 @@
---
id: "0167"
title: "Eliminar STUN leak a Google en LiveKit (hardcode external_ip)"
status: pendiente
type: infra
domain:
- matrix
scope: app:element_matrix_chat
priority: baja
depends: []
blocks: []
related: ["0166"]
created: 2026-05-24
updated: 2026-05-24
tags: [matrix, livekit, privacy, stun]
---
# 0167 — Eliminar STUN leak a Google en LiveKit (hardcode external_ip)
**Status:** pendiente
**Created:** 2026-05-24
**Type:** infra
**Priority:** baja
**Domain:** matrix
**Scope:** app:element_matrix_chat
**Depends:**
**Blocks:**
## Problema
`rtc.use_external_ip: true` con `external_ip` vacio → LiveKit hace STUN query a `stun.l.google.com:19302` cada arranque para descubrir IP publica. Leak metadata server (IP del VPS) a Google. Contradice premisa "self-host privacy first".
## Objetivo
LiveKit conoce su IP publica sin contactar STUN externos.
## Plan
1. Determinar IP publica VPS: `curl -s ifconfig.me`.
2. Editar `configs/livekit/livekit.yaml`:
```yaml
rtc:
use_external_ip: false
node_ip: "<IP_PUBLICA>"
```
3. Si TURN propio desplegado (issue 0166), usar coturn como STUN propio.
4. Restart `element_matrix_chat-livekit-1`.
5. Test: call funciona igual.
6. Auditar: `docker logs element_matrix_chat-livekit-1 | grep -i stun` no muestra queries a google.
## Acceptance
- [ ] `tcpdump -i eth0 dst stun.l.google.com` no captura paquetes tras restart.
- [ ] Calls Element Call siguen funcionando 1:1 y grupo.
## Definition of Done
- [ ] Repetibilidad: reboot VPS, 0 paquetes a stun.l.google.com.
- [ ] Observabilidad: log LiveKit confirma IP hardcoded.
## Notas
Bajo impacto operacional pero alta consistencia con doctrina self-host. Si IP del VPS cambia (rara vez con VPS estatico), actualizar config manual o automatizar con script de healthcheck.
@@ -0,0 +1,58 @@
---
id: "0168"
title: "Ampliar UDP range LiveKit de 200 a 500 ports"
status: pendiente
type: infra
domain:
- matrix
scope: app:element_matrix_chat
priority: baja
depends: []
blocks: []
related: ["0166"]
created: 2026-05-24
updated: 2026-05-24
tags: [matrix, livekit, scaling, webrtc]
---
# 0168 — Ampliar UDP range LiveKit de 200 a 500 ports
**Status:** pendiente
**Created:** 2026-05-24
**Type:** infra
**Priority:** baja
**Domain:** matrix
**Scope:** app:element_matrix_chat
**Depends:**
**Blocks:**
## Problema
LiveKit configurado con `port_range_start: 50000`, `port_range_end: 50200` (200 ports UDP). Cada participante usa ~2 ports → cap **~100 participantes concurrentes** sumando TODAS las calls del server. OK para uso personal hoy, justo si se anaden grupos simultaneos o reuniones >10 personas.
## Objetivo
Sostener al menos 250 participantes concurrentes sin port exhaustion.
## Plan
1. Editar `configs/livekit/livekit.yaml`: `port_range_end: 50500`.
2. Actualizar `docker-compose.yml` para exponer rango ampliado (300 puertos UDP adicionales).
3. Abrir rango en firewall VPS (UFW/iptables).
4. Restart stack LiveKit.
5. Smoke test: call funciona.
## Acceptance
- [ ] `docker port element_matrix_chat-livekit-1` muestra 50000-50500 UDP.
- [ ] `ss -lun | grep -c "0.0.0.0:50"` >= 500 tras restart.
- [ ] Call test OK.
## Definition of Done
- [ ] Repetibilidad: stack reinicia limpio.
## Notas
`docker-compose.yml` actualmente lista los 200 ports uno a uno (verboso pero explicito). Considerar usar sintaxis `"50000-50500:50000-50500/udp"` para legibilidad.
NO incrementar a >1000 sin medir consumo memoria LiveKit — cada port asignado tiene overhead minimo pero acumula.
@@ -0,0 +1,60 @@
---
id: "0169"
title: "Rotar LIVEKIT_SECRET (expuesto en sesion auditoria)"
status: pendiente
type: bugfix
domain:
- matrix
scope: app:element_matrix_chat
priority: alta
depends: []
blocks: []
related: []
created: 2026-05-24
updated: 2026-05-24
tags: [matrix, livekit, security, secret-rotation]
---
# 0169 — Rotar LIVEKIT_SECRET (expuesto en sesion auditoria)
**Status:** pendiente
**Created:** 2026-05-24
**Type:** bugfix
**Priority:** alta
**Domain:** matrix
**Scope:** app:element_matrix_chat
**Depends:**
**Blocks:**
## Problema
Durante auditoria 2026-05-24 (sesion Claude), `docker inspect element_matrix_chat-livekit-jwt-1` volco `LIVEKIT_SECRET=b00e98f70722bc...` cleartext en stdout de la sesion. Aunque la sesion es del operador, el secret quedo en log de conversacion + potencialmente en backups del log + transcripts. Rotacion necesaria por higiene.
## Objetivo
Nuevo secret 32 bytes hex, mismo `api_key` (o regenerar ambos), stack restart sin perdida sesion.
## Plan
1. Generar nuevo secret: `openssl rand -hex 32`.
2. Editar `configs/livekit/livekit.yaml` → bloque `keys:` con nuevo valor.
3. Editar `.env` de docker-compose (var `LIVEKIT_SECRET` consumida por `livekit-jwt`).
4. Restart `element_matrix_chat-livekit-1` y `element_matrix_chat-livekit-jwt-1` en orden.
5. Test call Element Call → handshake JWT OK.
6. Guardar secret antiguo + nuevo en `pass` con timestamp rotacion.
## Acceptance
- [ ] `docker inspect ... --format "{{.Config.Env}}"` muestra secret nuevo.
- [ ] Element Call inicia call sin error "invalid token".
- [ ] Entry `pass matrix/livekit-secret` actualizada.
## Definition of Done
- [ ] Repetibilidad: rotacion documentada como funcion del registry (candidato `livekit_secret_rotate_bash_infra`).
- [ ] Observabilidad: rotation log con timestamp.
## Notas
Considerar promover el procedimiento a funcion del registry: `livekit_secret_rotate_bash_infra(ssh_host, compose_dir)` que automatiza pasos 1-5 y guarda en pass via `gpg_pass_write`.
Patron similar para otros secrets del stack (Synapse macaroon, MAS encryption key, postgres passwords) → capability group nuevo `secret-rotation`.
@@ -0,0 +1,55 @@
---
id: "0170"
title: "Renombrar livekit.example.yaml -> livekit.yaml en bind mount"
status: pendiente
type: chore
domain:
- matrix
scope: app:element_matrix_chat
priority: baja
depends: []
blocks: []
related: []
created: 2026-05-24
updated: 2026-05-24
tags: [matrix, livekit, hygiene]
---
# 0170 — Renombrar livekit.example.yaml -> livekit.yaml en bind mount
**Status:** pendiente
**Created:** 2026-05-24
**Type:** chore
**Priority:** baja
**Domain:** matrix
**Scope:** app:element_matrix_chat
**Depends:**
**Blocks:**
## Problema
`configs/livekit/livekit.yaml` mantiene los comentarios "Copy this file..." del template original. Funciona pero confunde: parece config sin completar. El bind mount apunta directo a este archivo, asi que renombrar limpiamente el archivo template y mantener `livekit.yaml` limpio para mantenimiento.
## Objetivo
`livekit.yaml` limpio sin comentarios de "example", `livekit.example.yaml` separado como referencia template inicial en repo.
## Plan
1. Crear `configs/livekit/livekit.example.yaml` con plantilla limpia (placeholders).
2. Eliminar comentarios "Copy this file..." del `livekit.yaml` actual.
3. Verificar `.gitignore` cubre `livekit.yaml` real pero no `livekit.example.yaml`.
4. Commit en `egutierrez/element_matrix_chat`.
## Acceptance
- [ ] `head -3 configs/livekit/livekit.yaml` NO menciona "example".
- [ ] `configs/livekit/livekit.example.yaml` versionado.
- [ ] Stack restart sin cambios funcionales.
## Definition of Done
- [ ] PR mergeado en `dataforge/element_matrix_chat`.
## Notas
Tarea de higiene puro. Cero impacto runtime. Mejora onboarding futuro si otro operador clona el repo.