feat: implement end-to-end encryption (E2EE) support for agents with configuration and documentation updates
This commit is contained in:
@@ -86,6 +86,25 @@ matrix:
|
||||
- Token: `MATRIX_TOKEN_<ID_UPPER>` donde ID se convierte a mayúsculas y guiones a underscores
|
||||
- Ejemplo: `asistente-2` → `MATRIX_TOKEN_ASISTENTE2`
|
||||
- Password: `MATRIX_PASSWORD_<ID_UPPER>` con la misma convención
|
||||
- Pickle key E2EE: `PICKLE_KEY_<ID_UPPER>` — clave fija hex de 32 bytes
|
||||
|
||||
**Sección encryption en config.yaml:**
|
||||
```yaml
|
||||
encryption:
|
||||
enabled: true
|
||||
store_path: "./agents/<agent-id>/data/crypto/" # SIEMPRE por agente, nunca compartida
|
||||
pickle_key_env: PICKLE_KEY_<ID_UPPER> # 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_<ID_UPPER>=<hex>`
|
||||
3. Añadir a `.env.example`: `PICKLE_KEY_<ID_UPPER>=`
|
||||
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/<agent-id>/prompts/system.md` — System prompt
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
+10
-1
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -111,7 +111,8 @@ matrix:
|
||||
|
||||
encryption:
|
||||
enabled: true
|
||||
store_path: "./data/crypto/"
|
||||
store_path: "./agents/<agent-id>/data/crypto/"
|
||||
pickle_key_env: PICKLE_KEY_<AGENT_UPPER>
|
||||
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_<AGENT>` en `.env`
|
||||
3. Actualizar `device_id` en `config.yaml`
|
||||
4. Borrar el crypto store viejo (`data/crypto/`)
|
||||
4. Borrar el crypto store viejo (`agents/<id>/data/crypto/crypto.db`)
|
||||
5. Re-ejecutar `cmd/verify`
|
||||
|
||||
**Nota:** El pickle key (`PICKLE_KEY_<AGENT>`) 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/<agent-id>.log
|
||||
|
||||
**Logs esperados al arrancar correctamente:**
|
||||
```
|
||||
{"level":"INFO","msg":"initializing e2ee","store":"data/crypto/crypto.db"}
|
||||
{"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"}
|
||||
@@ -305,7 +306,8 @@ tail -f run/<id>.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/<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` |
|
||||
| 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` |
|
||||
|
||||
+153
@@ -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=<hex random 32 bytes>
|
||||
```
|
||||
|
||||
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 <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 |
|
||||
@@ -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 {
|
||||
|
||||
+17
-7
@@ -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/<id>/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)
|
||||
|
||||
Reference in New Issue
Block a user