feat: update access token environment variables and enhance device signing process for E2EE agents; add verification script and system flow documentation

This commit is contained in:
2026-03-05 23:46:07 +00:00
parent 0f900d1560
commit a92fbff801
7 changed files with 589 additions and 14 deletions
+1 -1
View File
@@ -115,7 +115,7 @@ tools:
matrix:
homeserver: "https://matrix-af2f3d.organic-machine.com"
user_id: "@asistente-2:matrix-af2f3d.organic-machine.com"
access_token_env: MATRIX_TOKEN_ASISTENTE2
access_token_env: MATRIX_TOKEN_ASISTENTE_2
device_id: "XUGTSZJYFQ"
encryption:
+1 -1
View File
@@ -116,7 +116,7 @@ tools:
matrix:
homeserver: "https://matrix-af2f3d.organic-machine.com"
user_id: "@assistant-bot:matrix-af2f3d.organic-machine.com"
access_token_env: MATRIX_TOKEN_ASSISTANT
access_token_env: MATRIX_TOKEN_ASSISTANT_BOT
device_id: "SMWMRKMHDH"
encryption:
+7
View File
@@ -68,6 +68,13 @@ func New(cfg *config.AgentConfig, rules []decision.Rule, logger *slog.Logger) (*
}
}
// Sign own device with the self-signing key so Element shows it as verified.
if err := matrixClient.SignOwnDevice(context.Background()); err != nil {
logger.Warn("failed to sign own device (non-fatal)", "err", err)
} else {
logger.Info("own device signed with cross-signing key")
}
logger.Info("e2ee ready")
}
+104 -12
View File
@@ -4,6 +4,7 @@
// Usage:
//
// go run -tags goolm ./cmd/verify --homeserver https://... --username asistente-2 --password <pass> --token <access_token>
// go run -tags goolm ./cmd/verify --homeserver https://... --username asistente-2 --token <access_token> # tries dummy/admin UIA
package main
import (
@@ -38,7 +39,8 @@ func main() {
Long: `Generates and uploads cross-signing keys so the bot's device is verified.
This removes the "Encrypted by a device not verified by its owner" warning.
Requires the bot's access token and password (for UIA during key upload).`,
Requires the bot's access token. Password is optional — if omitted, tries
dummy auth (MSC3967, Synapse 1.79+) then falls back to password if needed.`,
RunE: func(cmd *cobra.Command, args []string) error {
homeserver = strings.TrimRight(homeserver, "/")
serverName := homeserver
@@ -102,7 +104,9 @@ Requires the bot's access token and password (for UIA during key upload).`,
}
fmt.Println("→ Generating and uploading cross-signing keys...")
recoveryKey, _, err := olmMachine.GenerateAndUploadCrossSigningKeysWithPassword(ctx, password, "")
// Try multiple UIA strategies in order of preference.
recoveryKey, err := uploadCrossSigningKeys(ctx, olmMachine, password)
if err != nil {
// If keys already exist, try to just sign our device
fmt.Printf(" Note: %v\n", err)
@@ -111,41 +115,129 @@ Requires the bot's access token and password (for UIA during key upload).`,
}
fmt.Println("✓ Cross-signing keys uploaded successfully")
fmt.Printf("✓ Device %s is now verified by %s\n", client.DeviceID, userID)
// Sign own device immediately after uploading keys
if signErr := signOwnDevice(ctx, olmMachine, client); signErr != nil {
fmt.Printf(" Warning: could not auto-sign device: %v\n", signErr)
}
fmt.Println()
fmt.Println("─── IMPORTANT: Save the recovery key ───")
fmt.Printf("SSSS_RECOVERY_KEY_%s=%s\n", strings.ToUpper(strings.ReplaceAll(username, "-", "_")), recoveryKey)
envKey := strings.ToUpper(strings.ReplaceAll(username, "-", "_"))
fmt.Printf("SSSS_RECOVERY_KEY_%s=%s\n", envKey, recoveryKey)
fmt.Println()
fmt.Println("Add this to your .env file and set recovery_key_env in the agent's config.yaml:")
fmt.Println(" encryption:")
fmt.Printf(" recovery_key_env: SSSS_RECOVERY_KEY_%s\n", strings.ToUpper(strings.ReplaceAll(username, "-", "_")))
fmt.Printf(" recovery_key_env: SSSS_RECOVERY_KEY_%s\n", envKey)
return nil
},
}
root.Flags().StringVar(&homeserver, "homeserver", "", "Matrix homeserver URL")
root.Flags().StringVar(&username, "username", "", "Bot username (without @ or server)")
root.Flags().StringVar(&password, "password", "", "Bot password (for UIA auth)")
root.Flags().StringVar(&password, "password", "", "Bot password (for UIA auth, optional)")
root.Flags().StringVar(&token, "token", "", "Bot access token")
root.Flags().StringVar(&storePath, "store", "./data/verify-crypto/", "Crypto store path")
root.Flags().StringVar(&pickleKeyHex, "pickle-key", "", "Hex-encoded pickle key (must match agent's pickle key if sharing crypto store)")
_ = root.MarkFlagRequired("homeserver")
_ = root.MarkFlagRequired("username")
_ = root.MarkFlagRequired("password")
_ = root.MarkFlagRequired("token")
// password is no longer required
if err := root.Execute(); err != nil {
os.Exit(1)
}
}
func signOwnDevice(ctx context.Context, mach *crypto.OlmMachine, client *mautrix.Client) error {
device := &id.Device{
UserID: client.UserID,
DeviceID: client.DeviceID,
// uploadCrossSigningKeys tries multiple UIA strategies to upload cross-signing keys.
// Order: password (if provided) → dummy (MSC3967) → password with empty string.
func uploadCrossSigningKeys(ctx context.Context, mach *crypto.OlmMachine, password string) (string, error) {
type strategy struct {
name string
fn func(*mautrix.RespUserInteractive) interface{}
}
err := mach.SignOwnDevice(ctx, device)
var strategies []strategy
// If password provided, try it first
if password != "" {
strategies = append(strategies, strategy{
name: "password auth",
fn: func(uiResp *mautrix.RespUserInteractive) interface{} {
return &mautrix.ReqUIAuthLogin{
BaseAuthData: mautrix.BaseAuthData{
Type: mautrix.AuthTypePassword,
Session: uiResp.Session,
},
User: mach.Client.UserID.String(),
Password: password,
}
},
})
}
// Try dummy auth (MSC3967 — works on first upload with Synapse 1.79+)
strategies = append(strategies, strategy{
name: "dummy auth (MSC3967)",
fn: func(uiResp *mautrix.RespUserInteractive) interface{} {
return &mautrix.BaseAuthData{
Type: mautrix.AuthTypeDummy,
Session: uiResp.Session,
}
},
})
// If no password was given, also try password auth with empty as last resort
if password == "" {
strategies = append(strategies, strategy{
name: "empty password auth",
fn: func(uiResp *mautrix.RespUserInteractive) interface{} {
return &mautrix.ReqUIAuthLogin{
BaseAuthData: mautrix.BaseAuthData{
Type: mautrix.AuthTypePassword,
Session: uiResp.Session,
},
User: mach.Client.UserID.String(),
Password: " ", // non-empty to avoid omitempty dropping the field
}
},
})
}
var lastErr error
for _, s := range strategies {
fmt.Printf(" Trying %s...\n", s.name)
recoveryKey, _, err := mach.GenerateAndUploadCrossSigningKeys(ctx, s.fn, "")
if err == nil {
fmt.Printf(" ✓ Succeeded with %s\n", s.name)
return recoveryKey, nil
}
fmt.Printf(" ✗ %s failed: %v\n", s.name, err)
lastErr = err
}
return "", lastErr
}
func signOwnDevice(ctx context.Context, mach *crypto.OlmMachine, client *mautrix.Client) error {
// Force-fetch own device keys from the server so the local store has
// the correct signing key. Without this, SignOwnDevice fails with
// "received update for device with different signing key (expected , got X)".
devices, err := mach.FetchKeys(ctx, []id.UserID{client.UserID}, true)
if err != nil {
return fmt.Errorf("fetch own device keys: %w", err)
}
userDevices, ok := devices[client.UserID]
if !ok {
return fmt.Errorf("own user %s not found in fetched keys", client.UserID)
}
device, ok := userDevices[client.DeviceID]
if !ok {
return fmt.Errorf("own device %s not found in fetched keys", client.DeviceID)
}
if err := mach.SignOwnDevice(ctx, device); err != nil {
return fmt.Errorf("sign own device: %w", err)
}
fmt.Printf("✓ Device %s signed with cross-signing key\n", client.DeviceID)
+167
View File
@@ -0,0 +1,167 @@
#!/usr/bin/env bash
# verify.sh — (re)verifica dispositivos E2EE de agentes Matrix
#
# Genera/sube cross-signing keys y firma el device de cada agente.
# Usa el MISMO crypto store que el agente para que las keys queden disponibles.
#
# Uso:
# ./dev-scripts/verify.sh # verifica todos los habilitados con E2EE
# ./dev-scripts/verify.sh assistant-bot # verifica uno específico
source "$(dirname "$0")/_common.sh"
load_env
TARGET="${1:-}"
# ── YAML helpers (simple grep-based, no deps) ────────────────────────────
yaml_val() {
# Extract a simple YAML value: yaml_val file "key"
# Handles both quoted and unquoted values.
local file="$1" key="$2"
grep -m1 "^\s*${key}:" "$file" 2>/dev/null \
| sed 's/^[^:]*:\s*//' \
| tr -d '"' \
| tr -d "'" \
| xargs
}
# ── Verify a single agent ────────────────────────────────────────────────
verify_agent() {
local cfg="$1"
local agent_id; agent_id="$(yaml_val "$cfg" "id")"
local agent_dir; agent_dir="$(dirname "$cfg")"
# Check E2EE is enabled
local enc_enabled; enc_enabled="$(yaml_val "$cfg" "enabled")"
# The first "enabled" is agent.enabled; we need encryption.enabled specifically
enc_enabled="$(grep -A5 'encryption:' "$cfg" | grep -m1 'enabled:' | awk '{print $2}')"
if [[ "$enc_enabled" != "true" ]]; then
dim " $agent_id — E2EE deshabilitado, saltando"
return 0
fi
# Extract config values
local user_id; user_id="$(yaml_val "$cfg" "user_id")"
local username; username="$(echo "$user_id" | sed 's/@\([^:]*\):.*/\1/')"
local token_env; token_env="$(yaml_val "$cfg" "access_token_env")"
local pickle_env; pickle_env="$(yaml_val "$cfg" "pickle_key_env")"
local recovery_env; recovery_env="$(yaml_val "$cfg" "recovery_key_env")"
local store_path; store_path="$(grep -A5 'encryption:' "$cfg" | grep -m1 'store_path:' | sed 's/^[^:]*:\s*//' | tr -d '"' | xargs)"
local token="${!token_env:-}"
local pickle_key="${!pickle_env:-}"
# Find password — convention: MATRIX_PASSWORD_<NORMALIZED>
local norm; norm="$(echo "$username" | tr '-' '_' | tr '[:lower:]' '[:upper:]')"
local pass_env="MATRIX_PASSWORD_${norm}"
local password="${!pass_env:-}"
# Validate required values
if [[ -z "$token" ]]; then
fail " $agent_id$token_env no está en .env"
return 1
fi
if [[ -z "$password" ]]; then
warn " $agent_id$pass_env no está en .env, intentando sin password..."
fi
info "$agent_id — verificando device..."
dim " user: $username"
dim " store: $store_path"
dim " pickle_env: $pickle_env"
dim " token_env: $token_env"
# Stop agent if running (crypto store can't be shared)
local was_running=false
if is_running "$agent_id"; then
was_running=true
info " Deteniendo $agent_id antes de verificar..."
"$REPO_ROOT/dev-scripts/stop.sh" "$agent_id"
sleep 1
fi
# Build verify command
local verify_bin="$REPO_ROOT/bin/verify"
if [[ ! -x "$verify_bin" ]] || [[ "$(find ./cmd/verify -newer "$verify_bin" 2>/dev/null | head -1)" ]]; then
info " Compilando cmd/verify..."
mkdir -p "$(dirname "$verify_bin")"
"$GO" build -tags goolm -o "$verify_bin" ./cmd/verify || {
fail " No se pudo compilar cmd/verify"
return 1
}
fi
# Run verification
local verify_args=(
--homeserver "$MATRIX_HOMESERVER"
--username "$username"
--token "$token"
--store "$store_path"
)
if [[ -n "$password" ]]; then
verify_args+=(--password "$password")
fi
if [[ -n "$pickle_key" ]]; then
verify_args+=(--pickle-key "$pickle_key")
fi
local output
if output=$("$verify_bin" "${verify_args[@]}" 2>&1); then
ok "$agent_id — verificación exitosa"
# Extract recovery key from output if present
local new_rk
new_rk="$(echo "$output" | grep "^SSSS_RECOVERY_KEY_" | cut -d= -f2-)"
if [[ -n "$new_rk" && -n "$recovery_env" ]]; then
# Update .env with new recovery key (quoted — keys contain spaces)
local quoted_rk="\"${new_rk}\""
if grep -q "^${recovery_env}=" "$REPO_ROOT/.env"; then
sed -i "s|^${recovery_env}=.*|${recovery_env}=${quoted_rk}|" "$REPO_ROOT/.env"
ok " Recovery key actualizada en .env ($recovery_env)"
else
echo "${recovery_env}=${quoted_rk}" >> "$REPO_ROOT/.env"
ok " Recovery key añadida a .env ($recovery_env)"
fi
fi
else
warn "$agent_id — verify output:"
echo "$output"
# If it says keys already exist, that's usually fine
if echo "$output" | grep -q "signed with cross-signing key"; then
ok "$agent_id — device firmado con keys existentes"
else
warn "$agent_id — puede necesitar atención manual"
fi
fi
echo "$output" | sed 's/^/ /'
# Restart agent if it was running
if [[ "$was_running" == "true" ]]; then
info " Reiniciando $agent_id..."
"$REPO_ROOT/dev-scripts/start.sh" "$agent_id"
fi
echo
}
# ── Main ──────────────────────────────────────────────────────────────────
echo
info "Verificación E2EE de agentes Matrix"
echo
if [[ -n "$TARGET" ]]; then
cfg="$(config_path_for "$TARGET")"
[[ -n "$cfg" ]] || fail "Agente '$TARGET' no encontrado"
verify_agent "$cfg"
else
while IFS='|' read -r id version enabled desc cfg; do
[[ "$enabled" == "true" ]] || continue
verify_agent "$cfg"
done < <(list_agents_raw)
fi
ok "Verificación completada"
+274
View File
@@ -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
```
+35
View File
@@ -150,6 +150,41 @@ func (c *Client) FetchCrossSigningKeys(ctx context.Context, recoveryKey string)
return fetchCrossSigningKeysCore(ctx, &olmSSSSFetcher{machine}, recoveryKey)
}
// SignOwnDevice signs the bot's current device with the self-signing key.
// This is the step that makes Element show the device as "verified".
// Must be called after cross-signing private keys are available (via
// FetchCrossSigningKeys or GenerateAndUploadCrossSigningKeys).
// It force-fetches device keys from the server first to ensure the local
// store has the correct signing key.
func (c *Client) SignOwnDevice(ctx context.Context) error {
wrapper, ok := c.raw.Crypto.(*mautrixCryptoWrapper)
if !ok || wrapper == nil {
return fmt.Errorf("crypto not initialized")
}
machine := wrapper.Machine()
if machine == nil {
return fmt.Errorf("olm machine not available")
}
// Force-fetch own device keys so the local store has the correct signing key.
// Without this, SignOwnDevice fails with "different signing key" when the
// store has a stale or empty entry.
devices, err := machine.FetchKeys(ctx, []id.UserID{c.raw.UserID}, true)
if err != nil {
return fmt.Errorf("fetch own device keys: %w", err)
}
userDevices, ok := devices[c.raw.UserID]
if !ok {
return fmt.Errorf("own user not found in fetched keys")
}
device, ok := userDevices[c.raw.DeviceID]
if !ok {
return fmt.Errorf("own device %s not found in fetched keys", c.raw.DeviceID)
}
return machine.SignOwnDevice(ctx, device)
}
// fetchCrossSigningKeysCore contains the testable logic for SSSS key retrieval.
func fetchCrossSigningKeysCore(ctx context.Context, fetcher ssssKeyFetcher, recoveryKey string) error {
keyID, keyData, err := fetcher.GetDefaultKeyData(ctx)