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:
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Executable
+167
@@ -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"
|
||||
@@ -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
|
||||
```
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user