feat: import agents_and_robots platform as unibots (Matrix-out, unibus transport)
Reemplaza el scaffold del echobot por la plataforma completa de bots traida desde ~/DataProyects/Github/agents_and_robots tras la operacion Matrix-out: los bots ya no hablan por Matrix sino por el bus unibus (modelo todo-rooms + E2E via shell/transportunibus sobre github.com/enmanuel/unibus/pkg/client). - go.mod: replace de unibus -> ../unibus y de fn-registry -> ../../../.. (paths relativos reajustados a la nueva ubicacion dentro de fn_registry). - app.md: bump a 0.2.0, descripcion + arquitectura + comandos + gotchas reales. - modulo Go conservado como github.com/enmanuel/agents (sin reescribir imports). agents_and_robots queda archivado como museo de la era Matrix.
This commit is contained in:
@@ -0,0 +1,424 @@
|
||||
# Guía completa: Crear un nuevo agente
|
||||
|
||||
Esta guía documenta todos los pasos para crear, registrar, configurar y poner en marcha un nuevo bot/agente en el sistema.
|
||||
|
||||
## Requisitos previos
|
||||
|
||||
- Go 1.23+ instalado (`/usr/local/go/bin`)
|
||||
- Acceso al homeserver Matrix (`MATRIX_HOMESERVER` y `MATRIX_ADMIN_TOKEN` en `.env`)
|
||||
- Variables de entorno cargadas (`.env` con todos los secretos)
|
||||
|
||||
## Paso 1: Crear el scaffold del agente
|
||||
|
||||
### Opción A: Script automático
|
||||
|
||||
```bash
|
||||
./dev-scripts/agent/new-agent.sh <agent-id> "Display Name"
|
||||
# Ejemplo: ./dev-scripts/agent/new-agent.sh mi-bot "Mi Bot"
|
||||
```
|
||||
|
||||
Esto crea la estructura base en `agents/<agent-id>/`.
|
||||
|
||||
### Opción B: Manual
|
||||
|
||||
Crear la estructura de directorios:
|
||||
|
||||
```
|
||||
agents/<agent-id>/
|
||||
├── agent.go # Reglas puras de decisión
|
||||
├── config.yaml # Configuración completa del agente
|
||||
├── prompts/
|
||||
│ └── system.md # System prompt para el LLM
|
||||
└── data/ # Runtime (auto-generado, en .gitignore)
|
||||
└── crypto/ # Store E2EE
|
||||
```
|
||||
|
||||
### 1.1 Crear `agents/<agent-id>/agent.go`
|
||||
|
||||
Archivo de reglas puras. El package debe exportar una función `Rules() []decision.Rule`.
|
||||
|
||||
```go
|
||||
package mibot
|
||||
|
||||
import (
|
||||
"github.com/enmanuel/agents/pkg/decision"
|
||||
)
|
||||
|
||||
// Rules returns the decision rules for this agent.
|
||||
func Rules() []decision.Rule {
|
||||
return []decision.Rule{
|
||||
// !help — comando de ayuda explícito
|
||||
{
|
||||
Name: "help",
|
||||
Match: decision.MatchCommand("help"),
|
||||
Actions: []decision.Action{{
|
||||
Kind: decision.ActionKindReply,
|
||||
Reply: &decision.ReplyAction{
|
||||
Content: "Soy mi-bot. Escríbeme lo que necesitas.",
|
||||
},
|
||||
}},
|
||||
},
|
||||
|
||||
// Catch-all: DMs y menciones → LLM
|
||||
{
|
||||
Name: "llm-all",
|
||||
Match: func(ctx decision.MessageContext) bool {
|
||||
return ctx.IsDirectMsg || ctx.IsMention
|
||||
},
|
||||
Actions: []decision.Action{{
|
||||
Kind: decision.ActionKindLLM,
|
||||
LLM: &decision.LLMAction{},
|
||||
}},
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Reglas importantes:**
|
||||
- Este archivo es **puro** — sin imports de I/O, sin side effects
|
||||
- Solo usa types de `pkg/decision`
|
||||
- Las reglas se evalúan en orden; la primera que matchea gana
|
||||
|
||||
### 1.2 Crear `agents/<agent-id>/config.yaml`
|
||||
|
||||
Configuración completa del agente. Referencia: `internal/config/schema.go`.
|
||||
|
||||
Secciones principales:
|
||||
|
||||
| Sección | Descripción |
|
||||
|---------|-------------|
|
||||
| `agent` | Identidad: id, name, version, enabled, description, tags |
|
||||
| `personality` | Tono, verbosidad, idioma, templates, comportamiento |
|
||||
| `llm` | Provider (openai/anthropic), modelo, tokens, temperature, tool_use |
|
||||
| `tools` | SSH, HTTP, scripts, file_ops, MCP — cada uno con su enabled/config |
|
||||
| `matrix` | Homeserver, user_id, token, device_id, encryption, rooms, filters |
|
||||
| `agents` | Peers conocidos, delegación, protocolo inter-agente |
|
||||
| `ssh` | Configuración SSH (solo si aplica) |
|
||||
| `security` | Roles, audit, secrets provider |
|
||||
| `schedules` | Tareas programadas (cron) |
|
||||
| `observability` | Logging, metrics, health, tracing |
|
||||
| `resilience` | Circuit breaker, retry, shutdown, queue |
|
||||
| `storage` | State backend, cache, history |
|
||||
|
||||
**Campos críticos en `matrix`:**
|
||||
|
||||
```yaml
|
||||
matrix:
|
||||
homeserver: "https://matrix-af2f3d.organic-machine.com"
|
||||
user_id: "@<agent-id>:matrix-af2f3d.organic-machine.com"
|
||||
access_token_env: MATRIX_TOKEN_<AGENT_UPPER> # nombre de la env var
|
||||
device_id: "<se obtiene al registrar>"
|
||||
|
||||
encryption:
|
||||
enabled: true
|
||||
store_path: "./agents/<agent-id>/data/crypto/"
|
||||
pickle_key_env: PICKLE_KEY_<AGENT_UPPER>
|
||||
trust_mode: tofu
|
||||
```
|
||||
|
||||
**Para habilitar tool-use:**
|
||||
|
||||
```yaml
|
||||
llm:
|
||||
tool_use:
|
||||
enabled: true # DEBE ser true
|
||||
max_iterations: 5
|
||||
parallel_calls: false
|
||||
```
|
||||
|
||||
### 1.3 Crear `agents/<agent-id>/prompts/system.md`
|
||||
|
||||
System prompt que recibe el LLM. Debe incluir:
|
||||
- Identidad y rol del bot
|
||||
- Capacidades disponibles
|
||||
- Herramientas disponibles (si tool_use está habilitado)
|
||||
- Estilo de respuesta
|
||||
- Limitaciones
|
||||
|
||||
Usar como referencia: `agents/assistant-bot/prompts/assistant-system.md` o `agents/asistente-2/prompts/system.md`.
|
||||
|
||||
## Paso 2: Registrar el agente en el launcher
|
||||
|
||||
Editar `cmd/launcher/main.go`:
|
||||
|
||||
1. Añadir import del package del agente:
|
||||
```go
|
||||
mibotAgent "github.com/enmanuel/agents/agents/mibot"
|
||||
```
|
||||
|
||||
2. Añadir entrada en `rulesRegistry`:
|
||||
```go
|
||||
var rulesRegistry = map[string]func() []decision.Rule{
|
||||
"assistant-bot": assistantagent.Rules,
|
||||
"mi-bot": mibotAgent.Rules, // ← nuevo
|
||||
}
|
||||
```
|
||||
|
||||
**Nota:** El ID aquí debe coincidir exactamente con `agent.id` en el `config.yaml`.
|
||||
|
||||
## Paso 3: Registrar en Matrix
|
||||
|
||||
```bash
|
||||
./dev-scripts/agent/register.sh <agent-id> "Display Name"
|
||||
```
|
||||
|
||||
Este comando:
|
||||
1. Crea el usuario en Synapse via admin API
|
||||
2. Genera una contraseña aleatoria
|
||||
3. Hace login para obtener un access token
|
||||
4. Guarda `MATRIX_TOKEN_<AGENT>` en `.env`
|
||||
|
||||
**Guardar la contraseña manualmente** — se necesita para la verificación E2EE:
|
||||
|
||||
```bash
|
||||
# Añadir al .env manualmente si no se guardó:
|
||||
MATRIX_PASSWORD_<AGENT>=<password_generada>
|
||||
```
|
||||
|
||||
**Importante:** El script `register.sh` imprime la password en la salida. Copiarla y guardarla.
|
||||
|
||||
### Actualizar device_id en config.yaml
|
||||
|
||||
El registro crea un `device_id` nuevo. Actualizarlo en `agents/<agent-id>/config.yaml`:
|
||||
|
||||
```yaml
|
||||
matrix:
|
||||
device_id: "<DEVICE_ID del output de register>"
|
||||
```
|
||||
|
||||
## Paso 4: Configurar avatar y display name
|
||||
|
||||
Colocar la imagen del bot en `static/`:
|
||||
|
||||
```bash
|
||||
# Subir avatar y sincronizar displayname desde el config
|
||||
./dev-scripts/agent/avatar.sh <agent-id> static/<imagen>.jpg
|
||||
```
|
||||
|
||||
Esto hace:
|
||||
1. Sube la imagen al homeserver Matrix (obtiene una URL `mxc://`)
|
||||
2. Establece el avatar del usuario bot
|
||||
3. Sincroniza el displayname desde `agent.name` del `config.yaml`
|
||||
|
||||
**Formatos soportados:** JPG, PNG. Recomendado: cuadrado, 256x256 o superior.
|
||||
|
||||
## Paso 5: Verificación E2EE (cross-signing)
|
||||
|
||||
Sin este paso, los mensajes del bot mostrarán: **"Encrypted by a device not verified by its owner"**.
|
||||
|
||||
```bash
|
||||
./bin/verify \
|
||||
--homeserver "$MATRIX_HOMESERVER" \
|
||||
--username "<agent-id>" \
|
||||
--password "$MATRIX_PASSWORD_<AGENT>" \
|
||||
--token "$MATRIX_TOKEN_<AGENT>" \
|
||||
--store "./agents/<agent-id>/data/crypto/" \
|
||||
--pickle-key "$PICKLE_KEY_<AGENT>"
|
||||
```
|
||||
|
||||
**Qué hace:**
|
||||
1. Inicializa el crypto helper de mautrix (usando el mismo store y pickle key que el agente)
|
||||
2. Genera claves de cross-signing (master + self-signing + user-signing)
|
||||
3. Las sube al homeserver usando UIA con la password del bot
|
||||
4. Las almacena cifradas en SSSS (Server-Side Secret Storage) en el servidor
|
||||
5. Imprime un **recovery key** (base58) que permite recuperar las claves privadas
|
||||
|
||||
### 5.1 Guardar el recovery key
|
||||
|
||||
El comando imprime algo como:
|
||||
|
||||
```
|
||||
─── IMPORTANT: Save the recovery key ───
|
||||
SSSS_RECOVERY_KEY_MI_BOT=EsXX YYYY ZZZZ ...
|
||||
```
|
||||
|
||||
**Añadir al `.env`** (con comillas, el recovery key tiene espacios):
|
||||
|
||||
```bash
|
||||
SSSS_RECOVERY_KEY_MI_BOT="EsXX YYYY ZZZZ ..."
|
||||
```
|
||||
|
||||
### 5.2 Configurar recovery_key_env en config.yaml
|
||||
|
||||
```yaml
|
||||
encryption:
|
||||
enabled: true
|
||||
store_path: "./agents/<agent-id>/data/crypto/"
|
||||
pickle_key_env: PICKLE_KEY_<AGENT>
|
||||
trust_mode: tofu
|
||||
recovery_key_env: SSSS_RECOVERY_KEY_<AGENT> # ← NUEVO
|
||||
```
|
||||
|
||||
Esto permite que el agente recupere automáticamente las cross-signing private keys desde SSSS cada vez que arranca. Sin esto, las keys solo existen en memoria durante la sesión de verify.
|
||||
|
||||
**Logs esperados al arrancar con recovery key configurado:**
|
||||
```
|
||||
INFO cross-signing private keys fetched from SSSS
|
||||
INFO e2ee ready
|
||||
```
|
||||
|
||||
### 5.3 Si se cambia la password del bot
|
||||
|
||||
Cambiar la password (admin API) invalida el token anterior. Hay que:
|
||||
1. Re-login para obtener nuevo token
|
||||
2. Actualizar `MATRIX_TOKEN_<AGENT>` y `MATRIX_PASSWORD_<AGENT>` en `.env`
|
||||
3. Actualizar `device_id` en `config.yaml`
|
||||
4. Borrar el crypto store viejo (`agents/<id>/data/crypto/crypto.db`)
|
||||
5. Re-ejecutar `cmd/verify` → obtener nuevo recovery key
|
||||
6. Actualizar `SSSS_RECOVERY_KEY_<AGENT>` en `.env`
|
||||
|
||||
**Nota:** El pickle key (`PICKLE_KEY_<AGENT>`) NO cambia al rotar el token. Solo se regenera si se pierde.
|
||||
|
||||
## Paso 6: Arrancar el agente
|
||||
|
||||
```bash
|
||||
./dev-scripts/server/start.sh <agent-id>
|
||||
```
|
||||
|
||||
Verificar que arrancó correctamente:
|
||||
|
||||
```bash
|
||||
# Ver logs
|
||||
tail -f run/<agent-id>.log
|
||||
|
||||
# Verificar proceso
|
||||
./dev-scripts/server/ps.sh <agent-id>
|
||||
|
||||
# Estado general
|
||||
./dev-scripts/agent/list.sh
|
||||
```
|
||||
|
||||
**Logs esperados al arrancar correctamente:**
|
||||
```
|
||||
{"level":"INFO","msg":"initializing e2ee","store":"agents/<id>/data/crypto/crypto.db"}
|
||||
{"level":"INFO","msg":"e2ee ready"}
|
||||
{"level":"INFO","msg":"agent starting","id":"<agent-id>","tools":["current_time","matrix_send"]}
|
||||
{"level":"INFO","msg":"starting matrix sync"}
|
||||
```
|
||||
|
||||
## Paso 7: Verificar funcionamiento
|
||||
|
||||
1. Abrir Element u otro cliente Matrix
|
||||
2. Enviar un DM al bot: `@<agent-id>:matrix-af2f3d.organic-machine.com`
|
||||
3. Verificar que responde
|
||||
4. Verificar que no aparece el warning de "device not verified"
|
||||
5. Si tiene tools, probar que las usa (e.g., preguntar la hora)
|
||||
|
||||
## Resumen de comandos (en orden)
|
||||
|
||||
```bash
|
||||
# 1. Crear scaffold
|
||||
./dev-scripts/agent/new-agent.sh <id> "Nombre"
|
||||
|
||||
# 2. Editar reglas, config, prompt
|
||||
# agents/<id>/agent.go
|
||||
# agents/<id>/config.yaml
|
||||
# agents/<id>/prompts/system.md
|
||||
|
||||
# 3. Registrar en launcher
|
||||
# Editar cmd/launcher/main.go → import + rulesRegistry
|
||||
|
||||
# 4. Registrar en Matrix
|
||||
./dev-scripts/agent/register.sh <id> "Nombre"
|
||||
|
||||
# 5. Avatar y displayname
|
||||
./dev-scripts/agent/avatar.sh <id> static/<imagen>.jpg
|
||||
|
||||
# 6. Generar pickle key (si no existe)
|
||||
openssl rand -hex 32 # → guardar como PICKLE_KEY_<AGENT> en .env
|
||||
|
||||
# 7. Verificación E2EE + recovery key
|
||||
./bin/verify \
|
||||
--homeserver "$MATRIX_HOMESERVER" \
|
||||
--username "<id>" \
|
||||
--password "$MATRIX_PASSWORD_<AGENT>" \
|
||||
--token "$MATRIX_TOKEN_<AGENT>" \
|
||||
--store "./agents/<id>/data/crypto/" \
|
||||
--pickle-key "$PICKLE_KEY_<AGENT>"
|
||||
# → Guardar SSSS_RECOVERY_KEY_<AGENT> en .env (con comillas)
|
||||
# → Añadir recovery_key_env al config.yaml
|
||||
|
||||
# 8. Arrancar
|
||||
./dev-scripts/server/start.sh <id>
|
||||
|
||||
# 9. Verificar
|
||||
tail -f run/<id>.log
|
||||
```
|
||||
|
||||
## Control de acceso
|
||||
|
||||
El sistema de control de acceso permite restringir quién puede interactuar con cada agente. Tiene tres niveles independientes:
|
||||
|
||||
### Nivel 1 — Allowlist de usuarios
|
||||
|
||||
Restringe qué usuarios pueden enviar mensajes al bot. Si la lista está vacía, todos pueden hablar (comportamiento por defecto).
|
||||
|
||||
```yaml
|
||||
matrix:
|
||||
filters:
|
||||
allowed_users:
|
||||
- "@admin:matrix-af2f3d.organic-machine.com"
|
||||
- "@enmanuel:matrix-af2f3d.organic-machine.com"
|
||||
unauthorized_response: silent # silent (default) | explicit
|
||||
```
|
||||
|
||||
- `silent`: ignora mensajes de usuarios no autorizados (como si el bot no existiera)
|
||||
- `explicit`: responde con "No tienes permisos para interactuar con este agente"
|
||||
|
||||
### Nivel 2 — Invite gating
|
||||
|
||||
Si `allowed_users` está configurado, el bot solo acepta invites a salas de usuarios en la lista. Invites de usuarios no autorizados se ignoran silenciosamente.
|
||||
|
||||
### Nivel 3 — RBAC por acción
|
||||
|
||||
Conecta los roles de `security.roles` para controlar qué acciones puede ejecutar cada usuario:
|
||||
|
||||
```yaml
|
||||
security:
|
||||
roles:
|
||||
admin:
|
||||
users: ["@admin:matrix-af2f3d.organic-machine.com"]
|
||||
actions: ["*"] # acceso total
|
||||
user:
|
||||
users: ["*"] # todos los demás
|
||||
actions: ["ask", "help", "command:help", "command:ping"]
|
||||
```
|
||||
|
||||
**Acciones disponibles:**
|
||||
|
||||
| Acción | Qué protege |
|
||||
|--------|-------------|
|
||||
| `*` | Todo (wildcard) |
|
||||
| `ask` | Hablar con el LLM (mensajes de texto libre) |
|
||||
| `command:*` | Todos los comandos `!xxx` |
|
||||
| `command:<name>` | Un comando específico (ej: `command:tool`) |
|
||||
| `tool:*` | Todas las tools vía LLM |
|
||||
| `tool:<name>` | Una tool específica (ej: `tool:ssh_command`) |
|
||||
| `help` | Comandos informativos (`!help`, `!info`, `!status`) |
|
||||
|
||||
**Retrocompatibilidad:** si no se configura `allowed_users` ni `security.roles`, el agente funciona en modo abierto (como siempre).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problema | Causa | Solución |
|
||||
|----------|-------|----------|
|
||||
| `env var ... is not set` | La regex del `.env` loader no matchea | Verificar que el nombre de la var solo usa `[A-Z0-9_]` |
|
||||
| `M_UNKNOWN_TOKEN` | Token invalidado (password cambiada) | Re-login, actualizar `.env` |
|
||||
| `mismatching device ID` | Crypto store con device viejo | Borrar `agents/<id>/data/crypto/crypto.db`, actualizar `device_id` en config |
|
||||
| `olm account not marked as shared` | Crypto store inconsistente | Auto-recovery lo resuelve al reiniciar. Si persiste: borrar crypto.db |
|
||||
| `"Encrypted by device not verified"` | Falta cross-signing | Ejecutar `cmd/verify` con `--store` y `--pickle-key` del agente, guardar recovery key en `.env` |
|
||||
| `cross-signing private keys not available` | Recovery key no configurada | Ejecutar `cmd/verify`, guardar recovery key, configurar `recovery_key_env` |
|
||||
| `verify recovery key: invalid` | Recovery key incorrecta | Re-ejecutar `cmd/verify` para generar nueva recovery key |
|
||||
| Bot no responde | Reglas no matchean | Verificar que hay regla catch-all para DMs/mentions |
|
||||
| `no rules registered for agent` | ID no está en `rulesRegistry` | Añadir en `cmd/launcher/main.go` |
|
||||
| Bot muere al arrancar | Revisar logs | `tail -f run/<id>.log` |
|
||||
|
||||
## Compilación
|
||||
|
||||
Siempre usar la tag `goolm` para soporte E2EE puro (sin CGO):
|
||||
|
||||
```bash
|
||||
go build -tags goolm ./cmd/launcher
|
||||
go build -tags goolm ./cmd/verify
|
||||
go run -tags goolm ./cmd/verify --help
|
||||
```
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
# E2EE (End-to-End Encryption) en agents_and_robots
|
||||
|
||||
## Resumen
|
||||
|
||||
Los bots Matrix usan E2EE via mautrix-go + cryptohelper para comunicarse de forma cifrada.
|
||||
La implementación usa Olm puro en Go (`-tags goolm`, sin CGO).
|
||||
|
||||
## Arquitectura
|
||||
|
||||
```
|
||||
config.yaml (encryption section)
|
||||
↓
|
||||
agents/runtime.go → Agent.New() llama a InitCrypto()
|
||||
↓
|
||||
shell/matrix/client.go → InitCrypto() configura cryptohelper
|
||||
↓
|
||||
crypto.db (SQLite) — estado persistente de claves
|
||||
↓
|
||||
mautrix sync loop → cifrado/descifrado transparente
|
||||
```
|
||||
|
||||
## Qué guarda la crypto store (`crypto.db`)
|
||||
|
||||
| Dato | Qué es | Cuándo se crea | Cuándo rota |
|
||||
|------|--------|----------------|-------------|
|
||||
| **Olm Account** | Par de claves Curve25519 del dispositivo | Al primer `Init()` | Nunca — es la identidad del dispositivo |
|
||||
| **One-time keys** | Claves efímeras para sesiones 1:1 | Al `Init()` y cuando se agotan | Automáticamente cuando hay < 50% disponibles |
|
||||
| **Megolm sessions** | Claves de grupo para rooms | Al unirse a un room E2EE | Cada ~100 mensajes o ~1 semana |
|
||||
| **Device list cache** | Claves públicas de otros dispositivos | Al hacer sync | Se actualiza con cada sync |
|
||||
| **Cross-signing keys** | Master, self-signing, user-signing | Al ejecutar `cmd/verify` | Manualmente |
|
||||
|
||||
## Pickle key
|
||||
|
||||
El pickle key cifra el material criptográfico en la SQLite. Se configura por agente en `.env`:
|
||||
|
||||
```env
|
||||
PICKLE_KEY_ASSISTANT_BOT=<hex random 32 bytes>
|
||||
```
|
||||
|
||||
Y se referencia en `config.yaml`:
|
||||
```yaml
|
||||
encryption:
|
||||
enabled: true
|
||||
store_path: "./agents/assistant-bot/data/crypto/"
|
||||
pickle_key: "${PICKLE_KEY_ASSISTANT_BOT}"
|
||||
trust_mode: tofu
|
||||
```
|
||||
|
||||
### Generar un pickle key
|
||||
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
### Por qué NO derivar del access token
|
||||
|
||||
Si el token cambia (re-registro, nuevo login), el pickle key cambia y la DB existente
|
||||
se vuelve ilegible. Esto causa el error:
|
||||
```
|
||||
olm account is not marked as shared, but there are keys on the server
|
||||
```
|
||||
|
||||
Un pickle key fijo por agente en `.env` evita este problema.
|
||||
|
||||
## Crypto store por agente
|
||||
|
||||
Cada agente debe tener su propia crypto.db para evitar corrupción cruzada:
|
||||
|
||||
```
|
||||
agents/assistant-bot/data/crypto/crypto.db
|
||||
agents/asistente-2/data/crypto/crypto.db
|
||||
```
|
||||
|
||||
**No compartir** la crypto store entre agentes.
|
||||
|
||||
## Auto-recovery
|
||||
|
||||
Si `cryptohelper.Init()` falla por inconsistencia (ej: "not marked as shared"),
|
||||
el runtime borra automáticamente la crypto.db y reintenta. Esto regenera las claves
|
||||
Olm y requiere re-verificar cross-signing.
|
||||
|
||||
## Cross-signing y verificación
|
||||
|
||||
Elimina warnings de "Encrypted by a device not verified by its owner".
|
||||
|
||||
```bash
|
||||
go run -tags goolm ./cmd/verify \
|
||||
--homeserver https://matrix-af2f3d.organic-machine.com \
|
||||
--username <bot-id> \
|
||||
--password <password> \
|
||||
--token <access_token>
|
||||
```
|
||||
|
||||
Esto genera y sube cross-signing keys al servidor. Si las claves ya existen,
|
||||
firma el dispositivo actual con la clave existente.
|
||||
|
||||
## Trust mode
|
||||
|
||||
Configurado en `config.yaml` como `trust_mode`:
|
||||
|
||||
- **tofu** (Trust-on-First-Use): confía en un dispositivo la primera vez que lo ve.
|
||||
Cambios posteriores generan warnings.
|
||||
- **cross-signing**: requiere verificación explícita (no implementado aún).
|
||||
- **manual**: cada dispositivo debe verificarse manualmente (no implementado aún).
|
||||
|
||||
## Build
|
||||
|
||||
Siempre compilar con `-tags goolm`:
|
||||
```bash
|
||||
go build -tags goolm -o bin/launcher ./cmd/launcher
|
||||
```
|
||||
|
||||
El driver SQLite se registra como `"sqlite3"` via `modernc.org/sqlite` en
|
||||
`cmd/launcher/sqlite.go` y `cmd/verify/sqlite.go`.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "olm account is not marked as shared, but there are keys on the server"
|
||||
|
||||
La crypto store local está desincronizada con el servidor.
|
||||
|
||||
**Solución**: El runtime intenta auto-recovery. Si falla manualmente:
|
||||
```bash
|
||||
rm agents/<id>/data/crypto/crypto.db
|
||||
# Reiniciar el bot
|
||||
# Re-verificar cross-signing
|
||||
```
|
||||
|
||||
### "database is locked (SQLITE_BUSY)"
|
||||
|
||||
Dos procesos están accediendo la misma crypto.db simultáneamente.
|
||||
|
||||
**Solución**: Asegurar que cada agente use su propia `store_path` y no corran
|
||||
múltiples instancias del mismo agente con E2EE habilitado sin coordinación.
|
||||
|
||||
### "unable to decrypt message"
|
||||
|
||||
Las claves Megolm de la sesión se perdieron (DB borrada o corrupta).
|
||||
|
||||
**Solución**: Los mensajes cifrados antes del reset son irrecuperables.
|
||||
Los nuevos mensajes se descifrarán normalmente tras regenerar las claves.
|
||||
|
||||
## Archivos clave
|
||||
|
||||
| Archivo | Propósito |
|
||||
|---------|-----------|
|
||||
| `agents/runtime.go` | Inicializa E2EE por agente |
|
||||
| `shell/matrix/client.go` | `InitCrypto()` — setup de cryptohelper |
|
||||
| `cmd/verify/main.go` | Herramienta de cross-signing |
|
||||
| `cmd/launcher/sqlite.go` | Registro driver SQLite |
|
||||
| `internal/config/schema.go` | Schema de `EncryptionCfg` |
|
||||
| `agents/*/config.yaml` | Configuración E2EE por agente |
|
||||
@@ -0,0 +1,237 @@
|
||||
# Seguridad — Protecciones contra prompt injection y abuso de tools
|
||||
|
||||
Este documento describe las capas de defensa implementadas para proteger los agentes Matrix contra ataques de prompt injection y abuso de herramientas.
|
||||
|
||||
## Capas de defensa
|
||||
|
||||
La estrategia es **defensa en profundidad**: multiples capas independientes, ninguna es la unica barrera.
|
||||
|
||||
```
|
||||
Mensaje del usuario
|
||||
|
|
||||
v
|
||||
1. Input sanitization (pkg/sanitize/) — detecta patrones de injection
|
||||
|
|
||||
v
|
||||
2. System prompt hardening — instrucciones anti-manipulation al LLM
|
||||
|
|
||||
v
|
||||
3. Tool validation (deny-by-default) — cada tool valida sus inputs
|
||||
|
|
||||
v
|
||||
4. Rate limiting (tools/registry.go) — limite de tool calls por room
|
||||
|
|
||||
v
|
||||
5. RBAC (pkg/acl/) — control de acceso por usuario/rol
|
||||
```
|
||||
|
||||
## 1. Sanitizacion de input (`pkg/sanitize/`)
|
||||
|
||||
Funciones puras que detectan patrones de prompt injection en mensajes entrantes.
|
||||
|
||||
**Configuracion:**
|
||||
|
||||
```yaml
|
||||
security:
|
||||
sanitize:
|
||||
enabled: true
|
||||
mode: warn # warn | strip | reject
|
||||
min_severity: medium # low | medium | high
|
||||
disabled_patterns: [] # nombres de patrones a ignorar
|
||||
```
|
||||
|
||||
**Modos:**
|
||||
- `warn`: loguea warnings pero no modifica el mensaje (default)
|
||||
- `strip`: elimina las secciones sospechosas del mensaje
|
||||
- `reject`: rechaza el mensaje completamente con respuesta de error
|
||||
|
||||
**Patrones detectados:**
|
||||
- Delimitadores de sistema: `<|system|>`, `<|assistant|>`, `[INST]`
|
||||
- Frases de override: "ignore previous instructions", "you are now", etc.
|
||||
- Intentos de exfiltracion: "repeat your instructions", "show me your prompt"
|
||||
|
||||
Los patrones estan en `pkg/sanitize/patterns.go`. Son extensibles.
|
||||
|
||||
## 2. Hardening de system prompts
|
||||
|
||||
Todos los system prompts deben incluir una seccion de seguridad obligatoria.
|
||||
Template en `.claude/templates/security-prompt.md`.
|
||||
|
||||
Las instrucciones cubren:
|
||||
- Rechazo de acciones fuera del rol
|
||||
- Proteccion del system prompt (no revelar)
|
||||
- Rechazo de comandos destructivos
|
||||
- Validacion de coherencia contextual
|
||||
- Resistencia a redefinicion de identidad
|
||||
|
||||
## 3. Validacion en tools (deny-by-default)
|
||||
|
||||
Cada tool que hace I/O valida sus inputs de forma independiente.
|
||||
|
||||
### `tools/file/` — read_file
|
||||
- **Deny-by-default**: si `AllowedPaths` esta vacio, todo denegado
|
||||
- **Path traversal**: resuelve symlinks con `filepath.EvalSymlinks`, valida que el path este dentro de los permitidos
|
||||
- **Prefix confusion**: usa separador de directorio para evitar que `/allowed/path` matchee `/allowed/pathevil`
|
||||
|
||||
### `tools/ssh/` — ssh_command
|
||||
- **AllowedCommands**: allowlist de prefijos de comandos. Si esta definida, solo los comandos que matcheen se ejecutan
|
||||
- **ForbiddenCommands**: blocklist como segunda capa
|
||||
- **Validacion de sintaxis**: detecta pipes `|`, subshells `$()`, redirects `>`, chains `&&`/`||`/`;`
|
||||
- **AllowedTargets**: solo los hosts configurados
|
||||
|
||||
### `tools/http/` — http_get, http_post
|
||||
- **AllowedDomains**: solo los dominios configurados
|
||||
- **SSRF protection**: resuelve DNS y bloquea IPs privadas (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), link-local (169.254.0.0/16) y metadata (169.254.169.254)
|
||||
|
||||
### `tools/matrix/` — matrix_send
|
||||
- **AllowedRooms**: si esta configurado, solo permite enviar a rooms especificos
|
||||
|
||||
## 4. Rate limiting
|
||||
|
||||
Limite de tool calls por room por minuto. Previene abuso repetitivo.
|
||||
|
||||
**Configuracion:**
|
||||
|
||||
```yaml
|
||||
security:
|
||||
tool_rate_limit:
|
||||
enabled: true
|
||||
max_calls_per_min: 10 # default 10
|
||||
cleanup_interval_s: 60 # limpieza de entries expiradas
|
||||
```
|
||||
|
||||
Implementado en `tools/ratelimit.go` como sliding window per room. El registry verifica antes de ejecutar cada tool.
|
||||
|
||||
## 5. Aislamiento de filesystem
|
||||
|
||||
`storage.base_path` permite mover datos de runtime fuera del arbol del proyecto:
|
||||
|
||||
```yaml
|
||||
storage:
|
||||
base_path: /var/lib/agents/mi-bot # o via $AGENTS_DATA_DIR
|
||||
```
|
||||
|
||||
Prioridad: config `base_path` > `$AGENTS_DATA_DIR/<id>` > `agents/<id>/data/` (default).
|
||||
|
||||
Esto previene que tools como `read_file` accedan accidentalmente a codigo fuente, `.env`, o configs del proyecto.
|
||||
|
||||
## 6. Aislamiento de claude -p (provider claude-code)
|
||||
|
||||
Cuando un agente usa el provider `claude-code`, el subproceso `claude -p` se ejecuta en un directorio de trabajo aislado, no en la raiz del repositorio.
|
||||
|
||||
**Configuracion:**
|
||||
|
||||
```yaml
|
||||
llm:
|
||||
primary:
|
||||
claude_code:
|
||||
working_dir: "/tmp/claude-agents/mi-bot" # directorio aislado
|
||||
```
|
||||
|
||||
**Comportamiento:**
|
||||
- Si `working_dir` esta configurado: se crea el directorio automaticamente con `MkdirAll` y se usa como CWD del subproceso
|
||||
- Si `working_dir` esta vacio: se crea un directorio temporal (`os.MkdirTemp`) y se loguea un WARN para que el operador lo note
|
||||
- **Nunca** se hereda el CWD del launcher (raiz del repo)
|
||||
|
||||
Esto evita que el subproceso `claude -p` tenga acceso de lectura/escritura al codigo fuente del proyecto, incluso con `permission_mode: bypassPermissions`.
|
||||
|
||||
Implementado en `shell/llm/claudecode.go` → `resolveWorkDir()`.
|
||||
|
||||
## 7. Sistema de grupos centralizados (`security/`)
|
||||
|
||||
Control de acceso centralizado: quien puede hablar con que agentes, y que puede hacer.
|
||||
Reemplaza los campos per-agente `security.roles` y `matrix.filters.allowed_users` (ahora deprecados).
|
||||
|
||||
### Estructura de archivos
|
||||
|
||||
```
|
||||
security/
|
||||
user-groups.yaml # grupos de usuarios Matrix
|
||||
agent-groups.yaml # grupos de agentes
|
||||
permissions.yaml # politicas: que grupo de usuarios tiene que acciones en que agentes
|
||||
```
|
||||
|
||||
### user-groups.yaml
|
||||
|
||||
```yaml
|
||||
groups:
|
||||
admins:
|
||||
members: ["@admin:matrix-af2f3d.organic-machine.com"]
|
||||
developers:
|
||||
members: ["@dev1:homeserver.com", "@dev2:homeserver.com"]
|
||||
everyone:
|
||||
members: ["*"] # wildcard: todos los usuarios
|
||||
```
|
||||
|
||||
### agent-groups.yaml
|
||||
|
||||
```yaml
|
||||
groups:
|
||||
all:
|
||||
agents: ["*"] # wildcard: todos los agentes
|
||||
production:
|
||||
agents: ["assistant-bot", "asistente-2"]
|
||||
```
|
||||
|
||||
### permissions.yaml
|
||||
|
||||
```yaml
|
||||
policies:
|
||||
- agent_group: all # aplica a todos los agentes
|
||||
permissions:
|
||||
- user_group: admins
|
||||
actions: ["*"] # admins pueden hacer todo
|
||||
- user_group: everyone
|
||||
actions: ["ask"] # todos pueden chatear
|
||||
- agent_group: production # solo agentes de produccion
|
||||
permissions:
|
||||
- user_group: developers
|
||||
actions: ["ask", "command:deploy", "tool:ssh_command"]
|
||||
```
|
||||
|
||||
### Acciones disponibles
|
||||
|
||||
| Accion | Descripcion |
|
||||
|--------|-------------|
|
||||
| `*` | Todo permitido |
|
||||
| `ask` | Puede chatear con el agente (mensajes normales) |
|
||||
| `command:<name>` | Puede ejecutar el comando `!<name>` |
|
||||
| `tool:<name>` | El LLM puede llamar la tool `<name>` para este usuario |
|
||||
|
||||
### Resolucion de ACL
|
||||
|
||||
Al arrancar, el launcher:
|
||||
1. Carga todos los YAMLs con `shellsecurity.Load("security/")`
|
||||
2. Para cada agente, llama `pksecurity.ResolveACL(agentID, policy)`
|
||||
3. Inyecta la `acl.ACL` resultante en `agents.New()`
|
||||
|
||||
**Comportamiento cuando la politica esta vacia**: si `security/` no existe o no hay politicas que apliquen al agente, la ACL esta vacia y el acceso es abierto (sin restricciones). Preferible a denegar todo por defecto en produccion.
|
||||
|
||||
### Campos deprecados
|
||||
|
||||
Los siguientes campos en `config.yaml` del agente estan deprecated y no tienen efecto desde la activacion del sistema centralizado:
|
||||
|
||||
- `security.roles` — reemplazado por `security/permissions.yaml`
|
||||
- `matrix.filters.allowed_users` — reemplazado por `security/user-groups.yaml`
|
||||
|
||||
Los campos siguen en el schema para compatibilidad con configs existentes y se eliminarán en un issue futuro.
|
||||
|
||||
## Activacion
|
||||
|
||||
Para activar todas las protecciones, añadir al `config.yaml` del agente:
|
||||
|
||||
```yaml
|
||||
security:
|
||||
sanitize:
|
||||
enabled: true
|
||||
mode: warn
|
||||
tool_rate_limit:
|
||||
enabled: true
|
||||
max_calls_per_min: 10
|
||||
```
|
||||
|
||||
Y asegurarse de que:
|
||||
- Las tools tienen allowlists configuradas (no vacias si se quieren usar)
|
||||
- El system prompt incluye la seccion de seguridad
|
||||
- `storage.base_path` apunta fuera del proyecto en produccion
|
||||
- `claude_code.working_dir` apunta fuera del repo si se usa el provider claude-code
|
||||
@@ -0,0 +1,274 @@
|
||||
# Flujo del sistema de agentes — Diagrama de funciones
|
||||
|
||||
## 1. Arranque del sistema (Launcher)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
START["cmd/launcher/main()"] --> NEWLOGGER["newLogger(level)"]
|
||||
START --> GLOB["Glob: agents/*/config.yaml"]
|
||||
GLOB --> LOAD["config.Load(path)<br/>→ os.ExpandEnv + validate()"]
|
||||
LOAD --> RULESFOR["rulesFor(agentID)<br/>→ rulesRegistry[id]()"]
|
||||
RULESFOR --> AGENTNEW["agents.New(cfg, rules, logger)"]
|
||||
|
||||
subgraph "agents.New() — Ensamblado"
|
||||
AGENTNEW --> MATRIXNEW["matrix.New(cfg.Matrix)<br/>→ crea mautrix.Client"]
|
||||
MATRIXNEW --> CRYPTO{"encryption.enabled?"}
|
||||
CRYPTO -->|sí| INITCRYPTO["client.InitCrypto()<br/>→ initCryptoCore()<br/>→ initHelper()<br/>→ resolvePickleKey()<br/>→ logCryptoDiagnostics()"]
|
||||
INITCRYPTO --> FETCHKEYS{"recovery_key?"}
|
||||
FETCHKEYS -->|sí| CROSSSIGN["client.FetchCrossSigningKeys()<br/>→ fetchCrossSigningKeysCore()"]
|
||||
FETCHKEYS -->|no| SSHEXEC
|
||||
CROSSSIGN --> SSHEXEC
|
||||
CRYPTO -->|no| SSHEXEC
|
||||
SSHEXEC["ssh.NewExecutor(cfg.SSH)"]
|
||||
SSHEXEC --> LLMFACTORY["llm.FromConfig(cfg.LLM.Primary)<br/>→ NewAnthropicComplete() /<br/> NewOpenAIComplete()"]
|
||||
LLMFACTORY --> FALLBACK{"fallback?"}
|
||||
FALLBACK -->|sí| WITHFALLBACK["llm.WithFallback(primary, fallback)"]
|
||||
FALLBACK -->|no| TOOLREG
|
||||
WITHFALLBACK --> TOOLREG
|
||||
TOOLREG["buildToolRegistry(cfg, ssh, matrix)<br/>→ NewHTTPGet/Post()<br/>→ NewSSHCommand()<br/>→ NewReadFile()<br/>→ NewCurrentTime()<br/>→ NewMatrixSend()"]
|
||||
TOOLREG --> RUNNER["effects.NewRunner(matrix, ssh)"]
|
||||
RUNNER --> LISTENER["matrix.NewListener(client, cfg, handleEvent)"]
|
||||
end
|
||||
|
||||
AGENTNEW --> RUN["agent.Run(ctx)<br/>→ listener.Run(ctx)<br/>→ mautrix.SyncWithContext()"]
|
||||
START --> SIGNAL["Espera SIGINT / SIGTERM<br/>→ cancel ctx → shutdown"]
|
||||
```
|
||||
|
||||
## 2. Procesamiento de eventos (flujo principal)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
SYNC["mautrix SyncWithContext()"] --> EVENT["Evento Matrix recibido<br/>EventMessage / StateMember"]
|
||||
|
||||
EVENT --> AUTOJOIN{"StateMember<br/>invite?"}
|
||||
AUTOJOIN -->|sí| JOIN["Auto-join room"]
|
||||
AUTOJOIN -->|no| SHOULD["listener.shouldHandle(evt)<br/>→ filtra propios, bots, blocked, rooms"]
|
||||
|
||||
SHOULD -->|rechazado| DROP["Descartado"]
|
||||
SHOULD -->|aceptado| ISDM["listener.checkIsDM(roomID)<br/>→ cache de rooms con 2 miembros"]
|
||||
|
||||
ISDM --> PARSE["message.Parse(body, sender, room, ...)<br/>→ detecta mentions<br/>→ parsea command + args<br/>→ retorna MessageContext"]
|
||||
|
||||
PARSE --> GOROUTINE["goroutine: agent.handleEvent()"]
|
||||
|
||||
subgraph "handleEvent() — Decisión y ejecución"
|
||||
GOROUTINE --> TYPING["matrix.SendTyping(room, true)"]
|
||||
TYPING --> EVALUATE["decision.Evaluate(msgCtx, rules)<br/>→ recorre reglas, Match() → []Action"]
|
||||
|
||||
EVALUATE --> HASACTIONS{"¿acciones?"}
|
||||
HASACTIONS -->|sí| CHECKLLM{"¿contiene<br/>ActionKindLLM?"}
|
||||
HASACTIONS -->|no| FALLBACKLLM{"¿es DM o<br/>mención?"}
|
||||
|
||||
FALLBACKLLM -->|sí| RUNLLM["agent.runLLM(ctx, msgCtx)"]
|
||||
FALLBACKLLM -->|no| NOOP["Sin acción"]
|
||||
|
||||
CHECKLLM -->|sí| EXPANDLLM["Expande LLM actions:<br/>runLLM() → ReplyAction"]
|
||||
CHECKLLM -->|no| EXECUTE
|
||||
|
||||
EXPANDLLM --> EXECUTE
|
||||
RUNLLM --> EXECUTE["runner.Execute(ctx, roomID, actions)"]
|
||||
end
|
||||
```
|
||||
|
||||
## 3. Loop de herramientas del LLM (tool-use)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
RUNLLM["agent.runLLM()"] --> BUILD["Construir CompletionRequest<br/>→ SystemPrompt desde archivo<br/>→ Messages: historial + user<br/>→ Tools: registry.ToLLMSpecs()"]
|
||||
|
||||
BUILD --> CALL["CompleteFunc(ctx, request)<br/>→ Anthropic API / OpenAI API"]
|
||||
|
||||
subgraph "shell/llm — Proveedores"
|
||||
CALL --> ANTHROPIC["NewAnthropicComplete()<br/>→ toAnthropicRequest()<br/>→ HTTP POST /v1/messages<br/>→ fromAnthropicResponse()"]
|
||||
CALL --> OPENAI["NewOpenAIComplete()<br/>→ toOpenAIMessage()<br/>→ toOpenAITools()<br/>→ SDK CreateChatCompletion"]
|
||||
end
|
||||
|
||||
ANTHROPIC --> RESPONSE["CompletionResponse<br/>{Content, ToolCalls, Usage}"]
|
||||
OPENAI --> RESPONSE
|
||||
|
||||
RESPONSE --> HASTOOLS{"¿ToolCalls<br/>en respuesta?"}
|
||||
HASTOOLS -->|no| RETURN["Retorna Content como texto"]
|
||||
HASTOOLS -->|sí| EXECTOOLS["Por cada ToolCall:<br/>registry.Execute(name, argsJSON)"]
|
||||
|
||||
subgraph "tools/ — Ejecución de herramientas"
|
||||
EXECTOOLS --> TOOLSWITCH{"tool name"}
|
||||
TOOLSWITCH --> HTTP_GET["http_get<br/>→ validateDomain()<br/>→ GET request"]
|
||||
TOOLSWITCH --> HTTP_POST["http_post<br/>→ validateDomain()<br/>→ POST request"]
|
||||
TOOLSWITCH --> SSH_CMD["ssh_command<br/>→ validateTarget()<br/>→ validateCommand()<br/>→ ssh.Executor.Execute()"]
|
||||
TOOLSWITCH --> READ_FILE["read_file<br/>→ validatePath()<br/>→ os.ReadFile()"]
|
||||
TOOLSWITCH --> MATRIX_SEND["matrix_send<br/>→ client.SendText()"]
|
||||
TOOLSWITCH --> CURRENT_TIME["current_time<br/>→ time.Now().Format()"]
|
||||
end
|
||||
|
||||
EXECTOOLS --> APPEND["Append assistant msg + tool results<br/>a Messages del request"]
|
||||
APPEND --> ITER{"iteración <<br/>maxIter?"}
|
||||
ITER -->|sí| CALL
|
||||
ITER -->|no| RETURN
|
||||
```
|
||||
|
||||
## 4. Ejecución de efectos (Runner)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
EXECUTE["runner.Execute(ctx, roomID, actions)"] --> LOOP["Por cada Action en []Action"]
|
||||
|
||||
LOOP --> EXECONE["runner.executeOne(ctx, roomID, action)"]
|
||||
|
||||
EXECONE --> KIND{"action.Kind"}
|
||||
|
||||
KIND -->|ActionKindReply| REPLY["matrix.SendText(ctx, roomID, content)<br/>→ envío auto-encriptado si E2EE"]
|
||||
KIND -->|ActionKindSSH| SSHEXEC["ssh.Executor.Execute(ctx, spec)"]
|
||||
KIND -->|otro| UNHANDLED["Result{Err: unhandled}"]
|
||||
|
||||
subgraph "shell/ssh — Ejecución SSH"
|
||||
SSHEXEC --> LOOKUP["Buscar target en config<br/>→ resolver user/port/key"]
|
||||
LOOKUP --> LOADSIGNER["loadSigner(keyFileEnv)<br/>→ leer clave privada"]
|
||||
LOADSIGNER --> DIAL["gossh.Dial(tcp, host:port)"]
|
||||
DIAL --> SESSION["client.NewSession()"]
|
||||
SESSION --> RUNCMD["session.CombinedOutput(cmd)"]
|
||||
RUNCMD --> SSHRESULT["Result{Stdout, Stderr, ExitCode}"]
|
||||
end
|
||||
|
||||
REPLY --> RESULT["Result{Action, Output, Err}"]
|
||||
SSHRESULT --> RESULT
|
||||
UNHANDLED --> RESULT
|
||||
```
|
||||
|
||||
## 5. Motor de reglas puro (decision engine)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
EVAL["decision.Evaluate(ctx, rules)"] --> LOOP["Por cada Rule"]
|
||||
LOOP --> MATCH["rule.Match(ctx) → bool"]
|
||||
|
||||
subgraph "MatchFuncs disponibles (pure)"
|
||||
MATCH --> CMD["MatchCommand(cmd)<br/>ctx.Command == cmd"]
|
||||
MATCH --> PREFIX["MatchPrefix(prefix)<br/>strings.HasPrefix(ctx.Content)"]
|
||||
MATCH --> ANY["MatchAny()<br/>→ true siempre"]
|
||||
MATCH --> POWER["MatchMinPowerLevel(n)<br/>ctx.PowerLevel >= n"]
|
||||
MATCH --> COMPOSE["And(...) / Or(...)<br/>composición lógica"]
|
||||
end
|
||||
|
||||
MATCH -->|true| COLLECT["Agregar rule.Actions a resultado"]
|
||||
MATCH -->|false| NEXT["Siguiente regla"]
|
||||
COLLECT --> NEXT
|
||||
NEXT --> LOOP
|
||||
LOOP -->|fin| ACTIONS["Retorna []Action acumuladas"]
|
||||
```
|
||||
|
||||
## 6. Gestión de procesos (agentctl / dev-scripts)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
CLI["cmd/agentctl/main()"] --> MGR["process.NewManager(runDir, glob, bin)"]
|
||||
|
||||
MGR --> LISTCMD["listCmd → mgr.StatusAll()"]
|
||||
MGR --> STARTCMD["startCmd → mgr.Start(info)"]
|
||||
MGR --> STOPCMD["stopCmd → mgr.Stop(id)"]
|
||||
MGR --> REMOVECMD["removeCmd → mgr.Stop + setEnabled(false)"]
|
||||
|
||||
subgraph "process.Manager — Ciclo de vida"
|
||||
LISTCMD --> SCAN["mgr.Scan()<br/>→ glob configs<br/>→ config.LoadMeta()"]
|
||||
SCAN --> STATUS["mgr.Status(info)<br/>→ findProcessPIDs()<br/>→ resolveRunningPID()"]
|
||||
|
||||
STARTCMD --> STARTCHECK{"¿ya running?"}
|
||||
STARTCHECK -->|sí| REJECT["Error: already running"]
|
||||
STARTCHECK -->|no| LAUNCH["Abrir log file<br/>→ buildEnv()<br/>→ os/exec.Start()<br/>→ escribir PID file"]
|
||||
|
||||
STOPCMD --> SIGTERM["SIGTERM a todos los PIDs"]
|
||||
SIGTERM --> WAIT["Esperar 5s (polls cada 500ms)"]
|
||||
WAIT --> ALIVE{"¿todavía vivo?"}
|
||||
ALIVE -->|sí| SIGKILL["SIGKILL"]
|
||||
ALIVE -->|no| CLEAN["removePID()"]
|
||||
SIGKILL --> CLEAN
|
||||
end
|
||||
|
||||
subgraph "Monitoreo"
|
||||
STATUS --> STATS["mgr.Stats(id)<br/>→ statsForPID()<br/>→ /proc/pid/stat (uptime)<br/>→ /proc/pid/status (RSS)<br/>→ ps -o pcpu (CPU)"]
|
||||
STATUS --> LOGS["mgr.LogTail(id, n)<br/>→ últimas N líneas del log"]
|
||||
end
|
||||
```
|
||||
|
||||
## 7. Dashboard TUI (Bubbletea — pure core / impure shell)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
MAIN["cmd/dashboard/main()"] --> BRIDGE["newBridge(adapter)"]
|
||||
BRIDGE --> TEA["tea.NewProgram(bridge)"]
|
||||
|
||||
subgraph "Pure Core — pkg/tui"
|
||||
INIT["bridge.Init()<br/>→ IntentLoadAgents"]
|
||||
UPDATE["puretui.Update(model, msg)<br/>→ (Model, []Intent)"]
|
||||
VIEW["puretui.View(model)<br/>→ string renderizado"]
|
||||
|
||||
UPDATE --> SCREENS{"Screen actual"}
|
||||
SCREENS --> MAIN_MENU["updateMainScreen()"]
|
||||
SCREENS --> AGENT_LIST["updateAgentList()"]
|
||||
SCREENS --> AGENT_ACTIONS["updateAgentActions()<br/>→ executeAction()"]
|
||||
SCREENS --> LOGS_VIEW["updateLogs()"]
|
||||
SCREENS --> SERVER_VIEW["updateServerScreen()<br/>→ executeServerAction()"]
|
||||
end
|
||||
|
||||
subgraph "Impure Shell — shell/tui"
|
||||
ADAPTER["Adapter.RunIntent(intent)"]
|
||||
ADAPTER --> LOAD["loadAgents()<br/>→ mgr.StatusAll() + Stats()"]
|
||||
ADAPTER --> START["startAgent(id)<br/>→ mgr.Start()"]
|
||||
ADAPTER --> STOP["stopAgent(id)<br/>→ mgr.Stop()"]
|
||||
ADAPTER --> KILL["killAgent(id)<br/>→ mgr.Kill()"]
|
||||
ADAPTER --> RESTART["restartAgent(id)<br/>→ Stop + Start"]
|
||||
ADAPTER --> STARTALL["startAll() / stopAll()<br/>restartAll() / killAll()"]
|
||||
ADAPTER --> LOADLOGS["loadLogs(id)<br/>→ mgr.LogTail()"]
|
||||
end
|
||||
|
||||
TEA --> INIT
|
||||
INIT --> ADAPTER
|
||||
TEA --> UPDATE
|
||||
UPDATE -->|"[]Intent"| ADAPTER
|
||||
ADAPTER -->|"tea.Cmd → Msg"| UPDATE
|
||||
TEA --> VIEW
|
||||
```
|
||||
|
||||
## 8. Registro y verificación E2EE de bots
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph "cmd/register — Registro en Matrix"
|
||||
REG["main()"] --> CREATE["createUser(homeserver, token, userID, name, pass)<br/>→ PUT /_synapse/admin/v2/users/"]
|
||||
CREATE --> LOGIN["loginAs(homeserver, user, pass)<br/>→ POST /_matrix/client/v3/login"]
|
||||
LOGIN --> TOKEN["Imprime access_token + device_id<br/>→ exportar como MATRIX_TOKEN_BOT"]
|
||||
REG --> GENPASS["generatePassword()<br/>→ 24 bytes /dev/urandom → hex"]
|
||||
end
|
||||
|
||||
subgraph "cmd/verify — Cross-signing E2EE"
|
||||
VER["main()"] --> MAUTRIX["Crear mautrix.Client"]
|
||||
MAUTRIX --> INITCRYPTO["cryptohelper.Init()<br/>→ mismo store que el agente"]
|
||||
INITCRYPTO --> GENKEYS["GenerateAndUploadCrossSigningKeys<br/>WithPassword()"]
|
||||
GENKEYS --> RECOVERY["Imprime SSSS recovery key"]
|
||||
GENKEYS -->|"keys exist"| SIGNOWN["signOwnDevice()<br/>→ mach.SignOwnDevice()"]
|
||||
end
|
||||
```
|
||||
|
||||
## 9. Flujo completo end-to-end
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
USER["Usuario Matrix"] -->|"mensaje"| HOMESERVER["Matrix Homeserver"]
|
||||
HOMESERVER -->|"sync"| LISTENER["Listener.Run()<br/>shouldHandle()<br/>checkIsDM()"]
|
||||
LISTENER -->|"Parse()"| MSGCTX["MessageContext<br/>(puro)"]
|
||||
MSGCTX -->|"handleEvent()"| DECIDE["Evaluate(rules)<br/>(puro)"]
|
||||
|
||||
DECIDE -->|"[]Action"| BRANCH{"¿tipo?"}
|
||||
|
||||
BRANCH -->|"LLM"| LLM["runLLM()<br/>→ tool-use loop"]
|
||||
LLM -->|"CompleteFunc"| API["Anthropic / OpenAI API"]
|
||||
API -->|"ToolCalls"| TOOLS["Registry.Execute()<br/>http / ssh / file / time"]
|
||||
TOOLS -->|"results"| LLM
|
||||
LLM -->|"texto final"| REPLY
|
||||
|
||||
BRANCH -->|"Reply"| REPLY["SendText() / SendMarkdown()"]
|
||||
BRANCH -->|"SSH"| SSH["Executor.Execute()"]
|
||||
SSH -->|"resultado"| REPLY
|
||||
|
||||
REPLY -->|"respuesta"| HOMESERVER
|
||||
HOMESERVER -->|"mensaje"| USER
|
||||
```
|
||||
Reference in New Issue
Block a user