diff --git a/.claude/policies/create_agent.md b/.claude/policies/create_agent.md index e7bd4fe..063ce92 100644 --- a/.claude/policies/create_agent.md +++ b/.claude/policies/create_agent.md @@ -2,6 +2,43 @@ Guía para LLMs que asisten en la creación de agentes en este proyecto. +## Flujo completo automatizado + +```bash +# 1. Scaffold — crea config.yaml (E2EE habilitado), agent.go, prompts/, data/ +./dev-scripts/new-agent.sh "Display Name" + +# 2. Registrar en Matrix — genera y guarda en .env: +# MATRIX_TOKEN_, MATRIX_PASSWORD_, PICKLE_KEY_ +./dev-scripts/register.sh "Display Name" + +# 3. Verificar E2EE — genera cross-signing keys, firma el device, +# guarda SSSS_RECOVERY_KEY_ en .env +./dev-scripts/verify.sh + +# 4. Arrancar — ya verificado desde el primer arranque +./dev-scripts/start.sh +``` + +Los scripts automatizan todo. Solo intervenir manualmente si un paso falla. + +## Convención de nombres de env vars — REGLA CRÍTICA + +**Normalización**: agent ID → mayúsculas, guiones → underscores. **Sin eliminar sufijos.** + +Función canónica en `dev-scripts/_common.sh`: +```bash +normalize_id() { echo "$1" | tr '[:lower:]-' '[:upper:]_'; } +``` + +| Agent ID | Sufijo normalizado | Env vars | +|---|---|---| +| `assistant-bot` | `ASSISTANT_BOT` | `MATRIX_TOKEN_ASSISTANT_BOT`, `MATRIX_PASSWORD_ASSISTANT_BOT`, `PICKLE_KEY_ASSISTANT_BOT`, `SSSS_RECOVERY_KEY_ASSISTANT_BOT` | +| `asistente-2` | `ASISTENTE_2` | `MATRIX_TOKEN_ASISTENTE_2`, `MATRIX_PASSWORD_ASISTENTE_2`, `PICKLE_KEY_ASISTENTE_2`, `SSSS_RECOVERY_KEY_ASISTENTE_2` | +| `monitor-bot` | `MONITOR_BOT` | `MATRIX_TOKEN_MONITOR_BOT`, ... | + +**NUNCA** usar `sed 's/_BOT$//'` ni transformaciones que eliminen partes del ID. + ## Estructura requerida Cada agente vive en `agents//` con esta estructura: @@ -10,6 +47,8 @@ Cada agente vive en `agents//` con esta estructura: agents// ├── agent.go # Package propio, exporta Rules() []decision.Rule ├── config.yaml # Configuración completa (ver schema en internal/config/schema.go) +├── data/ # Runtime data (crypto, logs) — en .gitignore +│ └── crypto/ # Crypto store E2EE — NUNCA compartir entre agentes └── prompts/ └── system.md # System prompt del LLM ``` @@ -58,9 +97,7 @@ func Rules() []decision.Rule { ### 2. `agents//config.yaml` — Configuración -Usar como plantilla `agents/assistant/config.yaml` o `agents/asistente2/config.yaml`. - -**Campos que SIEMPRE hay que personalizar:** +`new-agent.sh` genera esto automáticamente. Campos que hay que personalizar: ```yaml agent: @@ -78,49 +115,21 @@ llm: matrix: user_id: "@:matrix-af2f3d.organic-machine.com" - access_token_env: MATRIX_TOKEN_ - device_id: "" + access_token_env: MATRIX_TOKEN_ + device_id: "" # se resuelve automáticamente via whoami ``` -**Convención de nombres de env vars:** -- 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 E2EE en config.yaml (generada por new-agent.sh) -**Sección encryption en config.yaml:** ```yaml encryption: - enabled: true + enabled: true # SIEMPRE true para agentes nuevos store_path: "./agents//data/crypto/" # SIEMPRE por agente, nunca compartida - pickle_key_env: PICKLE_KEY_ # env var con clave hex + pickle_key_env: PICKLE_KEY_ # generada por register.sh trust_mode: tofu - recovery_key_env: SSSS_RECOVERY_KEY_ # env var con base58 recovery key + recovery_key_env: SSSS_RECOVERY_KEY_ # generada por verify.sh ``` -**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. Ejecutar `cmd/verify` con `--store` y `--pickle-key` del agente: - ```bash - ./bin/verify --homeserver "$MATRIX_HOMESERVER" --username "" \ - --password "$MATRIX_PASSWORD_" --token "$MATRIX_TOKEN_" \ - --store "./agents//data/crypto/" --pickle-key "$PICKLE_KEY_" - ``` -6. Guardar el recovery key en `.env` (con comillas por los espacios): - ```bash - SSSS_RECOVERY_KEY_="EsXX YYYY ZZZZ ..." - ``` -7. Añadir `recovery_key_env` al config.yaml: - ```yaml - encryption: - recovery_key_env: SSSS_RECOVERY_KEY_ - ``` - -**Sin el recovery key**, el agente arranca pero los mensajes muestran "Encrypted by a device not verified by its owner". - ### 3. `agents//prompts/system.md` — System prompt Debe incluir: @@ -134,7 +143,7 @@ Debe incluir: ### 4. `cmd/launcher/main.go` — Registro en el launcher -Dos cambios: +`new-agent.sh` hace esto automáticamente. Dos cambios: **Import:** ```go @@ -157,6 +166,49 @@ Si el agente necesita una herramienta nueva (no existente), ver la policy `creat Las herramientas "siempre disponibles" (`current_time`, `matrix_send`) ya están registradas para todos los agentes. +## E2EE — Cómo funciona la verificación + +### Flujo al arrancar (agents/runtime.go) + +``` +InitCrypto → crea/carga el device y Olm account del crypto store +FetchCrossSigningKeys → obtiene private keys de SSSS usando recovery key +SignOwnDevice → fetch device keys del servidor + firma con self-signing key +``` + +### Qué hace cada script + +| Script | Qué genera | Dónde se guarda | +|---|---|---| +| `register.sh` | Token, password, pickle key (32 bytes hex) | `.env` | +| `verify.sh` | Cross-signing keys (master, self-signing, user-signing) + recovery key | Server (keys) + `.env` (recovery key) | + +### Por qué cada credential importa + +| Credential | Para qué | Si falta | +|---|---|---| +| `MATRIX_TOKEN_*` | Autenticación del bot con el homeserver | Bot no arranca | +| `MATRIX_PASSWORD_*` | UIA al subir cross-signing keys (verify.sh) | verify.sh intenta dummy auth (MSC3967) | +| `PICKLE_KEY_*` | Cifrar el crypto store en disco | Usa sha256(token) como fallback — inseguro | +| `SSSS_RECOVERY_KEY_*` | Recuperar private keys de cross-signing al arrancar | Device no se firma → "not verified by its owner" | + +### Problemas comunes y soluciones + +**"Encrypted by a device not verified by its owner"** +→ Ejecutar `./dev-scripts/verify.sh ` y reiniciar + +**"self-signing private key not in cache"** +→ La recovery key en `.env` no corresponde a las cross-signing keys actuales. Re-ejecutar verify.sh. + +**"received update for device with different signing key"** +→ Bug resuelto: `SignOwnDevice` ahora hace `FetchKeys` antes de firmar. Si reaparece, recompilar el launcher: `go build -tags goolm -o bin/launcher ./cmd/launcher` + +**"data is not encrypted for given key ID"** +→ Las cross-signing keys fueron regeneradas pero la recovery key en `.env` es la vieja. Re-ejecutar verify.sh (actualiza .env automáticamente). + +**Recovery key sin comillas en .env** +→ Causan `command not found` al hacer `source .env`. Las recovery keys tienen espacios y DEBEN ir entre comillas: `SSSS_RECOVERY_KEY_*="EsXX YYYY ZZZZ ..."` + ## Después de crear los archivos Verificar compilación: @@ -164,15 +216,17 @@ Verificar compilación: go build -tags goolm ./... ``` -Luego seguir con registro, avatar, verificación y arranque (ver `docs/creating-agents.md`). +Luego seguir con registro, verificación y arranque usando los dev-scripts. ## Reglas generales - **Nunca** poner side effects en `agent.go` — es código puro - **Siempre** verificar que `agent.id` coincide entre config.yaml, rulesRegistry y el directorio - **Siempre** compilar con `-tags goolm` para soporte E2EE +- **Siempre** usar `normalize_id()` de `_common.sh` para nombres de env vars - **Idioma**: español en configs, prompts y descripciones de dominio; inglés en código Go - **No** crear archivos `data/` — se generan automáticamente al arrancar - **No** commitear tokens ni passwords — solo van en `.env` +- **No** compartir crypto stores entre agentes — cada uno tiene su `store_path` - Si el agente usa tool_use, asegurarse de que `llm.tool_use.enabled: true` en el config - Usar `agents/asistente2/` como referencia completa de un agente con tools habilitadas diff --git a/cmd/register/main.go b/cmd/register/main.go index 9401782..8d5fd92 100644 --- a/cmd/register/main.go +++ b/cmd/register/main.go @@ -12,6 +12,7 @@ package main import ( "bytes" + "encoding/hex" "encoding/json" "fmt" "io" @@ -80,11 +81,17 @@ Example: } fmt.Printf("✓ Logged in, device ID: %s\n", deviceID) - // Step 3: Print results + // Step 3: Generate pickle key for E2EE crypto store + pickleKey := generatePickleKey() + + // Derive env var prefix from envVar (e.g. MATRIX_TOKEN_FOO → FOO) + norm := strings.TrimPrefix(envVar, "MATRIX_TOKEN_") + + // Step 4: Print results — parseable lines for register.sh fmt.Println("\n─── Add to your .env ───────────────────────────────") fmt.Printf("%s=%s\n", envVar, token) - fmt.Printf("MATRIX_HOMESERVER=%s\n", homeserver) - fmt.Printf("MATRIX_SERVER_NAME=%s\n", serverName) + fmt.Printf("MATRIX_PASSWORD_%s=%s\n", norm, password) + fmt.Printf("PICKLE_KEY_%s=%s\n", norm, pickleKey) fmt.Println("────────────────────────────────────────────────────") fmt.Printf("\nUser ID: %s\n", userID) fmt.Printf("Device ID: %s\n", deviceID) @@ -176,7 +183,6 @@ func loginAs(homeserver, username, password string) (token, deviceID string, err // generatePassword creates a random-enough password for the bot account. func generatePassword() string { - // Simple: use os.ReadFile on /dev/urandom, encode hex f, err := os.Open("/dev/urandom") if err != nil { return "agent-bot-default-please-change" @@ -186,3 +192,15 @@ func generatePassword() string { _, _ = io.ReadFull(f, buf) return fmt.Sprintf("%x", buf) } + +// generatePickleKey creates a 32-byte hex-encoded key for E2EE crypto store encryption. +func generatePickleKey() string { + f, err := os.Open("/dev/urandom") + if err != nil { + return "" + } + defer f.Close() + buf := make([]byte, 32) + _, _ = io.ReadFull(f, buf) + return hex.EncodeToString(buf) +} diff --git a/dev-scripts/_common.sh b/dev-scripts/_common.sh index e104fd6..c94621b 100755 --- a/dev-scripts/_common.sh +++ b/dev-scripts/_common.sh @@ -135,6 +135,16 @@ list_agents_raw() { done } +# ── Naming convention ───────────────────────────────────────────────────── +# Normalizes an agent ID to an env-var suffix. +# Convention: uppercase, hyphens → underscores. No stripping of suffixes. +# "assistant-bot" → "ASSISTANT_BOT" +# "asistente-2" → "ASISTENTE_2" +# "devops-bot" → "DEVOPS_BOT" +normalize_id() { + echo "$1" | tr '[:lower:]-' '[:upper:]_' +} + # ── Usage helper ────────────────────────────────────────────────────────── need_arg() { [[ -n "${1:-}" ]] || { echo "Usage: $0 "; exit 1; } diff --git a/dev-scripts/new-agent.sh b/dev-scripts/new-agent.sh index 722367b..91e8b1e 100755 --- a/dev-scripts/new-agent.sh +++ b/dev-scripts/new-agent.sh @@ -23,6 +23,7 @@ need_arg "${1:-}" ID="$1" DISPLAYNAME="${2:-$ID}" PACKAGE="$(echo "$ID" | tr '-' '_' | sed 's/_bot//')" # "monitor-bot" → "monitor" +NORM="$(normalize_id "$ID")" # "monitor-bot" → "MONITOR_BOT" DIR="agents/$ID" [[ -d "$DIR" ]] && fail "Ya existe agents/$ID — ¿ya fue creado?" @@ -146,14 +147,15 @@ tools: matrix: homeserver: "${MATRIX_HOMESERVER}" user_id: "@$ID:${MATRIX_SERVER_NAME}" - access_token_env: MATRIX_TOKEN_$(echo "$ID" | tr '[:lower:]-' '[:upper:]_' | sed 's/_BOT$//') - device_id: "$(echo "$ID" | tr '[:lower:]-' '[:upper:]_')01" + access_token_env: MATRIX_TOKEN_${NORM} + device_id: "" encryption: - enabled: false + enabled: true store_path: "./agents/${ID}/data/crypto/" - pickle_key_env: PICKLE_KEY_$(echo "$ID" | tr '[:lower:]-' '[:upper:]_') + pickle_key_env: PICKLE_KEY_${NORM} trust_mode: tofu + recovery_key_env: SSSS_RECOVERY_KEY_${NORM} rooms: listen: [] @@ -386,7 +388,9 @@ else fi echo "" -echo -e "${YLW}Queda 1 paso:${RST} registrar el bot en Matrix y añadir su token a .env:" +echo -e "${YLW}Quedan 3 pasos:${RST}" echo "" -echo -e " ${DIM}./dev-scripts/register.sh $ID \"$DISPLAYNAME\"${RST}" +echo -e " ${DIM}1. ./dev-scripts/register.sh $ID \"$DISPLAYNAME\"${RST} # registra en Matrix + genera token, password, pickle key" +echo -e " ${DIM}2. ./dev-scripts/verify.sh $ID${RST} # genera cross-signing keys + verifica device" +echo -e " ${DIM}3. ./dev-scripts/start.sh $ID${RST} # arranca el agente" echo "" diff --git a/dev-scripts/register.sh b/dev-scripts/register.sh index 2a35f80..d7e354a 100755 --- a/dev-scripts/register.sh +++ b/dev-scripts/register.sh @@ -2,11 +2,16 @@ # register.sh — registra un nuevo bot en el servidor Matrix via Synapse admin API # # Uso: -# ./dev-scripts/register.sh [displayname] [env-var-name] +# ./dev-scripts/register.sh [displayname] # # Ejemplos: -# ./dev-scripts/register.sh assistant-bot "Assistant" MATRIX_TOKEN_ASSISTANT -# ./dev-scripts/register.sh devops-bot "DevOps Agent" MATRIX_TOKEN_DEVOPS +# ./dev-scripts/register.sh assistant-bot "Assistant" +# ./dev-scripts/register.sh devops-bot "DevOps Agent" +# +# Genera y guarda en .env: +# MATRIX_TOKEN_=... (access token) +# MATRIX_PASSWORD_=... (password para UIA) +# PICKLE_KEY_=... (E2EE crypto store key) # # Requiere en .env: # MATRIX_ADMIN_TOKEN=syt_... @@ -19,12 +24,14 @@ need_arg "${1:-}" USERNAME="$1" DISPLAYNAME="${2:-$USERNAME}" -ENV_VAR="${3:-MATRIX_TOKEN_$(echo "$USERNAME" | tr '[:lower:]-' '[:upper:]_' | sed 's/_BOT$//')}" +NORM="$(normalize_id "$USERNAME")" +ENV_VAR="MATRIX_TOKEN_${NORM}" [[ -n "${MATRIX_ADMIN_TOKEN:-}" ]] || fail "MATRIX_ADMIN_TOKEN no está en .env" [[ -n "${MATRIX_HOMESERVER:-}" ]] || fail "MATRIX_HOMESERVER no está en .env" info "Registrando @${USERNAME}:${MATRIX_SERVER_NAME:-$MATRIX_HOMESERVER}..." +dim " Env var prefix: ${NORM}" echo "" # Ejecutar cmd/register y capturar su output completo @@ -37,23 +44,42 @@ OUTPUT=$("$GO" run ./cmd/register \ echo "$OUTPUT" echo "" -# Extraer la línea ENV_VAR=token del output -TOKEN_LINE=$(echo "$OUTPUT" | grep "^${ENV_VAR}=") -[[ -n "$TOKEN_LINE" ]] || fail "No se encontró '${ENV_VAR}=' en el output de cmd/register" +# ── Parsear y guardar cada variable en .env ────────────────────────────── -TOKEN=$(echo "$TOKEN_LINE" | cut -d= -f2-) -[[ -n "$TOKEN" ]] || fail "Token vacío para $ENV_VAR" +save_env_var() { + local key="$1" value="$2" + [[ -n "$value" ]] || return -# Actualizar .env — reemplazar si ya existe, añadir si no -if grep -q "^${ENV_VAR}=" .env; then - awk -v key="$ENV_VAR" -v val="$TOKEN" \ - 'index($0, key "=") == 1 { print key "=" val; next } { print }' \ - .env > /tmp/_env_tmp && mv /tmp/_env_tmp .env - ok "$ENV_VAR actualizado en .env" -else - printf '\n%s=%s\n' "$ENV_VAR" "$TOKEN" >> .env - ok "$ENV_VAR añadido a .env" -fi + # Quote values with spaces + if [[ "$value" == *" "* ]]; then + value="\"${value}\"" + fi + + if grep -q "^${key}=" .env; then + awk -v key="$key" -v val="$value" \ + 'index($0, key "=") == 1 { print key "=" val; next } { print }' \ + .env > /tmp/_env_tmp && mv /tmp/_env_tmp .env + ok "$key actualizado en .env" + else + printf '%s=%s\n' "$key" "$value" >> .env + ok "$key añadido a .env" + fi +} + +# Extract parseable lines from output +TOKEN=$(echo "$OUTPUT" | grep "^${ENV_VAR}=" | cut -d= -f2-) +PASSWORD=$(echo "$OUTPUT" | grep "^MATRIX_PASSWORD_${NORM}=" | cut -d= -f2-) +PICKLE_KEY=$(echo "$OUTPUT" | grep "^PICKLE_KEY_${NORM}=" | cut -d= -f2-) + +[[ -n "$TOKEN" ]] || fail "No se encontró '${ENV_VAR}=' en el output" + +save_env_var "$ENV_VAR" "$TOKEN" +save_env_var "MATRIX_PASSWORD_${NORM}" "$PASSWORD" +save_env_var "PICKLE_KEY_${NORM}" "$PICKLE_KEY" echo "" -dim " Arranca el bot con: ./dev-scripts/start.sh $USERNAME" +echo -e "${YLW}Siguientes pasos:${RST}" +echo "" +echo -e " ${DIM}1. ./dev-scripts/verify.sh $USERNAME${RST} # genera cross-signing keys E2EE" +echo -e " ${DIM}2. ./dev-scripts/start.sh $USERNAME${RST} # arranca el agente" +echo "" diff --git a/register b/register new file mode 100755 index 0000000..9deb78b Binary files /dev/null and b/register differ