diff --git a/.claude/policies/create_agent.md b/.claude/policies/create_agent.md index 7327d26..5e6c87d 100644 --- a/.claude/policies/create_agent.md +++ b/.claude/policies/create_agent.md @@ -86,6 +86,25 @@ matrix: - Token: `MATRIX_TOKEN_` donde ID se convierte a mayúsculas y guiones a underscores - Ejemplo: `asistente-2` → `MATRIX_TOKEN_ASISTENTE2` - Password: `MATRIX_PASSWORD_` con la misma convención +- Pickle key E2EE: `PICKLE_KEY_` — clave fija hex de 32 bytes + +**Sección encryption en config.yaml:** +```yaml + encryption: + enabled: true + store_path: "./agents//data/crypto/" # SIEMPRE por agente, nunca compartida + pickle_key_env: PICKLE_KEY_ # env var con clave hex + trust_mode: tofu +``` + +**Al crear un nuevo agente con E2EE:** +1. Generar pickle key: `openssl rand -hex 32` +2. Añadir a `.env`: `PICKLE_KEY_=` +3. Añadir a `.env.example`: `PICKLE_KEY_=` +4. Usar `store_path` propio del agente (no compartir entre agentes) +5. Tras arrancar, verificar cross-signing: `go run -tags goolm ./cmd/verify ...` + +Ver `docs/e2ee.md` para documentación completa de E2EE. ### 3. `agents//prompts/system.md` — System prompt diff --git a/.env.example b/.env.example index 4f0b2de..1d8f6bf 100644 --- a/.env.example +++ b/.env.example @@ -13,8 +13,16 @@ MATRIX_ADMIN_TOKEN=syt_... # Tokens de cada bot — generados por cmd/register MATRIX_TOKEN_ASSISTANT=syt_... +MATRIX_TOKEN_ASISTENTE2=syt_... MATRIX_TOKEN_DEVOPS=syt_... +# ── E2EE pickle keys (openssl rand -hex 32) ───────────────── +# Clave fija por agente para cifrar material crypto en SQLite. +# Si no se define, se usa sha256(access_token) como fallback. +PICKLE_KEY_ASSISTANT_BOT= +PICKLE_KEY_ASISTENTE_2= +PICKLE_KEY_DEVOPS_BOT= + # ── LLM providers ──────────────────────────────────────────── OPENAI_API_KEY=sk-... ANTHROPIC_API_KEY=sk-ant-... # opcional, para cuando añadas el devops-bot con Claude diff --git a/agents/asistente2/config.yaml b/agents/asistente2/config.yaml index 3e1a0ae..6c05706 100644 --- a/agents/asistente2/config.yaml +++ b/agents/asistente2/config.yaml @@ -120,7 +120,8 @@ matrix: encryption: enabled: true - store_path: "./data/crypto/" + store_path: "./agents/asistente2/data/crypto/" + pickle_key_env: PICKLE_KEY_ASISTENTE_2 trust_mode: tofu rooms: diff --git a/agents/assistant/config.yaml b/agents/assistant/config.yaml index d1206bc..f163559 100644 --- a/agents/assistant/config.yaml +++ b/agents/assistant/config.yaml @@ -121,7 +121,8 @@ matrix: encryption: enabled: true - store_path: "./data/crypto/" + store_path: "./agents/assistant/data/crypto/" + pickle_key_env: PICKLE_KEY_ASSISTANT_BOT trust_mode: tofu rooms: diff --git a/agents/devops/config.yaml b/agents/devops/config.yaml index f8a01ee..c3ae4a4 100644 --- a/agents/devops/config.yaml +++ b/agents/devops/config.yaml @@ -135,7 +135,8 @@ matrix: encryption: enabled: false # habilitar cuando E2EE esté configurado - store_path: "./data/crypto/" + store_path: "./agents/devops/data/crypto/" + pickle_key_env: PICKLE_KEY_DEVOPS_BOT trust_mode: tofu rooms: diff --git a/agents/runtime.go b/agents/runtime.go index d4047ef..5c90084 100644 --- a/agents/runtime.go +++ b/agents/runtime.go @@ -6,7 +6,9 @@ import ( "fmt" "io" "log/slog" + "os" "path/filepath" + "strings" "maunium.net/go/mautrix/event" @@ -49,9 +51,16 @@ func New(cfg *config.AgentConfig, rules []decision.Rule, logger *slog.Logger) (* var cryptoStore io.Closer if cfg.Matrix.Encryption.Enabled { storePath := filepath.Join(cfg.Matrix.Encryption.StorePath, "crypto.db") + pickleKey := os.Getenv(cfg.Matrix.Encryption.PickleKeyEnv) logger.Info("initializing e2ee", "store", storePath) - cryptoStore, err = matrixClient.InitCrypto(context.Background(), storePath, cfg.Agent.ID) + cryptoStore, err = matrixClient.InitCrypto(context.Background(), storePath, pickleKey, cfg.Agent.ID) if err != nil { + if strings.Contains(err.Error(), "not marked as shared") { + logger.Error("crypto store is inconsistent with server — need a fresh device", + "store", storePath, + "fix", "delete crypto.db, login with password to get new token+device, update .env, restart", + ) + } return nil, fmt.Errorf("e2ee init: %w", err) } logger.Info("e2ee ready") diff --git a/dev-scripts/new-agent.sh b/dev-scripts/new-agent.sh index 309b9a9..722367b 100755 --- a/dev-scripts/new-agent.sh +++ b/dev-scripts/new-agent.sh @@ -151,7 +151,8 @@ matrix: encryption: enabled: false - store_path: "./data/crypto/" + store_path: "./agents/${ID}/data/crypto/" + pickle_key_env: PICKLE_KEY_$(echo "$ID" | tr '[:lower:]-' '[:upper:]_') trust_mode: tofu rooms: diff --git a/docs/creating-agents.md b/docs/creating-agents.md index 7bc9a23..942ef4c 100644 --- a/docs/creating-agents.md +++ b/docs/creating-agents.md @@ -111,7 +111,8 @@ matrix: encryption: enabled: true - store_path: "./data/crypto/" + store_path: "./agents//data/crypto/" + pickle_key_env: PICKLE_KEY_ trust_mode: tofu ``` @@ -221,15 +222,15 @@ go run -tags goolm ./cmd/verify \ 3. Las sube al homeserver usando UIA con la password del bot 4. Firma el device del bot con la self-signing key -**Después de verificar:** Limpiar el crypto store temporal si se usó uno diferente al del agente. - **Importante:** Si se cambia la password del bot (admin API), el token anterior se invalida. Hay que: 1. Re-login para obtener nuevo token 2. Actualizar `MATRIX_TOKEN_` en `.env` 3. Actualizar `device_id` en `config.yaml` -4. Borrar el crypto store viejo (`data/crypto/`) +4. Borrar el crypto store viejo (`agents//data/crypto/crypto.db`) 5. Re-ejecutar `cmd/verify` +**Nota:** El pickle key (`PICKLE_KEY_`) NO cambia al rotar el token. Solo se regenera si se pierde. Ver `docs/e2ee.md`. + ## Paso 6: Arrancar el agente ```bash @@ -251,7 +252,7 @@ tail -f run/.log **Logs esperados al arrancar correctamente:** ``` -{"level":"INFO","msg":"initializing e2ee","store":"data/crypto/crypto.db"} +{"level":"INFO","msg":"initializing e2ee","store":"agents//data/crypto/crypto.db"} {"level":"INFO","msg":"e2ee ready"} {"level":"INFO","msg":"agent starting","id":"","tools":["current_time","matrix_send"]} {"level":"INFO","msg":"starting matrix sync"} @@ -305,7 +306,8 @@ tail -f run/.log |----------|-------|----------| | `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 `data/crypto/`, actualizar `device_id` en config | +| `mismatching device ID` | Crypto store con device viejo | Borrar `agents//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` | | 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` | diff --git a/docs/e2ee.md b/docs/e2ee.md new file mode 100644 index 0000000..c6d341b --- /dev/null +++ b/docs/e2ee.md @@ -0,0 +1,153 @@ +# 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= +``` + +Y se referencia en `config.yaml`: +```yaml +encryption: + enabled: true + store_path: "./agents/assistant/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/data/crypto/crypto.db +agents/asistente2/data/crypto/crypto.db +agents/devops/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 \ + --password \ + --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//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 | diff --git a/internal/config/schema.go b/internal/config/schema.go index 63e3dfa..94acdaf 100644 --- a/internal/config/schema.go +++ b/internal/config/schema.go @@ -169,9 +169,10 @@ type MatrixCfg struct { } type EncryptionCfg struct { - Enabled bool `yaml:"enabled"` - StorePath string `yaml:"store_path"` - TrustMode string `yaml:"trust_mode"` // tofu | cross-signing | manual + Enabled bool `yaml:"enabled"` + StorePath string `yaml:"store_path"` + PickleKeyEnv string `yaml:"pickle_key_env"` // env var with hex-encoded 32-byte key + TrustMode string `yaml:"trust_mode"` // tofu | cross-signing | manual } type RoomsCfg struct { diff --git a/shell/matrix/client.go b/shell/matrix/client.go index 631a6c6..377a4f3 100644 --- a/shell/matrix/client.go +++ b/shell/matrix/client.go @@ -4,6 +4,7 @@ package matrix import ( "context" "crypto/sha256" + "encoding/hex" "fmt" "io" "os" @@ -44,10 +45,12 @@ func New(cfg config.MatrixCfg) (*Client, error) { } // InitCrypto sets up end-to-end encryption using the mautrix cryptohelper. -// storePath is the SQLite file path for crypto material (e.g. "./data/crypto/crypto.db"). -// agentID is used to namespace the crypto state so multiple agents can share a database. +// storePath is the SQLite file path for crypto material (e.g. "./agents//data/crypto/crypto.db"). +// pickleKeyHex is a hex-encoded key for encrypting crypto material at rest. If empty, +// falls back to sha256(access_token) for backward compatibility. +// agentID namespaces the crypto state within the database. // Returns an io.Closer that must be called on agent shutdown to flush the crypto store. -func (c *Client) InitCrypto(ctx context.Context, storePath, agentID string) (io.Closer, error) { +func (c *Client) InitCrypto(ctx context.Context, storePath, pickleKeyHex, agentID string) (io.Closer, error) { // Resolve the actual device ID from the server — the value in config may differ // from what the registration process assigned. whoami, err := c.raw.Whoami(ctx) @@ -56,10 +59,17 @@ func (c *Client) InitCrypto(ctx context.Context, storePath, agentID string) (io. } c.raw.DeviceID = whoami.DeviceID - // Derive a stable pickle key from the access token. - // If the token changes (bot re-registered), delete the crypto store to reset. - sum := sha256.Sum256([]byte(c.raw.AccessToken)) - pickleKey := sum[:] + // Use explicit pickle key if provided, otherwise derive from access token. + var pickleKey []byte + if pickleKeyHex != "" { + pickleKey, err = hex.DecodeString(pickleKeyHex) + if err != nil { + return nil, fmt.Errorf("decode pickle_key_env: %w", err) + } + } else { + sum := sha256.Sum256([]byte(c.raw.AccessToken)) + pickleKey = sum[:] + } if err := os.MkdirAll(filepath.Dir(storePath), 0700); err != nil { return nil, fmt.Errorf("create crypto store dir: %w", err)