From dd4a1011390ba01373129ec22f8ddf6b9bc05011 Mon Sep 17 00:00:00 2001 From: Enmanuel Date: Wed, 4 Mar 2026 21:38:55 +0000 Subject: [PATCH] feat: add asistente-2 agent with tool-use and E2EE verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nuevo agente asistente-2 con herramienta current_time habilitada para demostrar el flujo completo de tool-use (LLM → tool call → resultado → respuesta). Incluye: - agents/asistente2/: reglas puras, config con tool_use.enabled, system prompt - tools/time.go: herramienta current_time (siempre disponible para todos los agentes) - cmd/verify/: comando para subir cross-signing keys y eliminar el warning "Encrypted by a device not verified by its owner" - Registro en runtime.go (current_time) y launcher/main.go (rulesRegistry) El cmd/verify usa mautrix GenerateAndUploadCrossSigningKeysWithPassword para configurar cross-signing via UIA con la password del bot. Co-Authored-By: Claude Opus 4.6 --- agents/asistente2/agent.go | 39 ++++ agents/asistente2/config.yaml | 268 ++++++++++++++++++++++++++++ agents/asistente2/prompts/system.md | 24 +++ agents/runtime.go | 4 + cmd/launcher/main.go | 2 + cmd/verify/main.go | 134 ++++++++++++++ cmd/verify/sqlite.go | 12 ++ tools/time.go | 31 ++++ 8 files changed, 514 insertions(+) create mode 100644 agents/asistente2/agent.go create mode 100644 agents/asistente2/config.yaml create mode 100644 agents/asistente2/prompts/system.md create mode 100644 cmd/verify/main.go create mode 100644 cmd/verify/sqlite.go create mode 100644 tools/time.go diff --git a/agents/asistente2/agent.go b/agents/asistente2/agent.go new file mode 100644 index 0000000..1b4ac1f --- /dev/null +++ b/agents/asistente2/agent.go @@ -0,0 +1,39 @@ +// Package asistente2 defines the pure rules for the asistente-2 bot. +// This agent uses tool_use (current_time) to demonstrate the tool-use loop. +package asistente2 + +import ( + "github.com/enmanuel/agents/pkg/decision" +) + +// Rules returns the decision rules for the asistente-2 bot. +func Rules() []decision.Rule { + return []decision.Rule{ + // !help — explicit help command + { + Name: "help", + Match: decision.MatchCommand("help"), + Actions: []decision.Action{{ + Kind: decision.ActionKindReply, + Reply: &decision.ReplyAction{ + Content: "Soy asistente-2. Puedo responder preguntas y además consultar la hora actual.\n" + + "- Pregúntame cualquier cosa\n" + + "- Puedo decirte la fecha y hora actual\n\n" + + "Escríbeme directamente lo que necesitas.", + }, + }}, + }, + + // Any DM or mention → LLM (with tool-use enabled) + { + Name: "llm-all", + Match: func(ctx decision.MessageContext) bool { + return ctx.IsDirectMsg || ctx.IsMention + }, + Actions: []decision.Action{{ + Kind: decision.ActionKindLLM, + LLM: &decision.LLMAction{}, + }}, + }, + } +} diff --git a/agents/asistente2/config.yaml b/agents/asistente2/config.yaml new file mode 100644 index 0000000..3e1a0ae --- /dev/null +++ b/agents/asistente2/config.yaml @@ -0,0 +1,268 @@ +# ============================================ +# IDENTIDAD +# ============================================ +agent: + id: asistente-2 + name: "Asistente 2" + version: "1.0.0" + enabled: true + description: "Asistente con herramientas. Puede responder preguntas y consultar la hora actual." + tags: [assistant, llm, tools] + +# ============================================ +# PERSONALIDAD Y COMPORTAMIENTO +# ============================================ +personality: + tone: friendly + verbosity: concise + language: es + languages_supported: [es, en] + emoji_style: minimal + prefix: "🛠️" + error_style: helpful + + templates: + greeting: "Hola, soy asistente-2. ¿En qué puedo ayudarte?" + unknown_command: "No entiendo ese comando. Escríbeme directamente lo que necesitas." + permission_denied: "No tengo permiso para hacer eso." + error: "Algo salió mal: {{.Error}}" + success: "{{.Summary}}" + busy: "Procesando tu solicitud anterior, dame un momento..." + + behavior: + proactive: false + ask_confirmation: false + show_reasoning: false + thread_replies: true + typing_indicator: true + acknowledge_receipt: false + +# ============================================ +# LLM — CONEXIÓN Y RAZONAMIENTO +# ============================================ +llm: + primary: + provider: openai + model: gpt-4o + api_key_env: OPENAI_API_KEY + base_url: "" + max_tokens: 4096 + temperature: 0.7 + + fallback: + provider: "" + model: "" + api_key_env: "" + base_url: "" + max_tokens: 0 + temperature: 0 + + reasoning: + system_prompt_file: "prompts/system.md" + context_window: 16384 + memory_messages: 30 + + tool_use: + enabled: true # herramientas HABILITADAS + max_iterations: 5 + parallel_calls: false + + rate_limit: + requests_per_minute: 60 + tokens_per_minute: 200000 + concurrent_requests: 5 + +# ============================================ +# TOOLS — current_time habilitada +# ============================================ +tools: + ssh: + enabled: false + allowed_targets: [] + forbidden_commands: [] + timeout: 0s + max_concurrent: 0 + require_confirmation: [] + + http: + enabled: false + allowed_domains: [] + timeout: 0s + max_retries: 0 + + scripts: + enabled: false + scripts_dir: "" + allowed: [] + timeout: 0s + sandbox: false + + file_ops: + enabled: false + allowed_paths: [] + read_only: true + + mcp: + enabled: false + servers: [] + expose: + port: 0 + tools: [] + +# ============================================ +# MATRIX — CONEXIÓN Y ROOMS +# ============================================ +matrix: + homeserver: "https://matrix-af2f3d.organic-machine.com" + user_id: "@asistente-2:matrix-af2f3d.organic-machine.com" + access_token_env: MATRIX_TOKEN_ASISTENTE2 + device_id: "YBFNMNMJIC" + + encryption: + enabled: true + store_path: "./data/crypto/" + trust_mode: tofu + + rooms: + listen: [] + respond: [] + admin: [] + + filters: + command_prefix: "!" + mention_respond: true + dm_respond: true + ignore_bots: true + ignore_users: [] + min_power_level: 0 + +# ============================================ +# COMUNICACIÓN INTER-AGENTES +# ============================================ +agents: + peers: + - id: assistant-bot + capabilities: [general, llm] + room: "" + + delegation: + enabled: false + can_delegate_to: [] + can_receive_from: [assistant-bot] + max_delegation_depth: 1 + timeout: 30s + + protocol: + format: json + channel: matrix + heartbeat_interval: 60s + +# ============================================ +# SSH — no aplica para este bot +# ============================================ +ssh: + defaults: + user: "" + port: 22 + key_file_env: "" + known_hosts: "" + keepalive_interval: 0s + timeout: 0s + targets: {} + +# ============================================ +# PERMISOS Y SEGURIDAD +# ============================================ +security: + roles: + admin: + users: ["@admin:matrix-af2f3d.organic-machine.com"] + actions: ["*"] + user: + users: ["*"] + actions: ["ask", "help", "summarize"] + + audit: + enabled: false + log_file: "./data/audit.log" + log_to_room: "" + include: [] + + secrets: + provider: env + +# ============================================ +# SCHEDULING — sin tareas automáticas +# ============================================ +schedules: [] + +# ============================================ +# OBSERVABILIDAD +# ============================================ +observability: + logging: + level: info + format: json + output: stdout + file: "./data/asistente2.log" + + metrics: + enabled: false + port: 9092 + path: /metrics + export: prometheus + + health: + enabled: true + port: 8082 + path: /healthz + + tracing: + enabled: false + provider: "" + endpoint: "" + +# ============================================ +# RESILIENCIA +# ============================================ +resilience: + circuit_breaker: + failure_threshold: 5 + timeout: 30s + half_open_max: 2 + + retry: + max_attempts: 2 + backoff: exponential + initial_delay: 1s + max_delay: 10s + + shutdown: + timeout: 10s + drain_messages: true + save_state: false + state_file: "" + + queue: + enabled: true + max_size: 100 + priority_users: ["@admin:matrix-af2f3d.organic-machine.com"] + +# ============================================ +# ALMACENAMIENTO Y ESTADO +# ============================================ +storage: + state: + backend: sqlite + path: "./data/asistente2.db" + + cache: + enabled: true + backend: memory + ttl: 5m + max_entries: 200 + + history: + backend: sqlite + path: "./data/history.db" + retention: 168h # 7 días diff --git a/agents/asistente2/prompts/system.md b/agents/asistente2/prompts/system.md new file mode 100644 index 0000000..a3b748a --- /dev/null +++ b/agents/asistente2/prompts/system.md @@ -0,0 +1,24 @@ +# Asistente 2 — System Prompt + +Eres un asistente conversacional amigable y directo. Operas en Matrix, respondiendo mensajes directos (DMs) y menciones en rooms. + +## Capacidades +- Responder preguntas generales +- Resumir texto o documentos pegados en el chat +- Redactar textos, emails, documentación +- Explicar conceptos técnicos y no técnicos +- Ayudar con código: revisar, corregir, explicar +- **Consultar la hora y fecha actual** usando la herramienta `current_time` + +## Herramientas disponibles +- `current_time`: Devuelve la fecha y hora actual del servidor. Úsala cuando alguien pregunte por la hora, fecha, o necesites contexto temporal. + +## Estilo +- Respuestas concisas por defecto. Si necesitas extensión, pregunta primero. +- Usa markdown cuando ayude a la legibilidad (listas, código, headers) +- Idioma principal: español. Cambia al idioma del usuario si escribe en otro. +- Sin emojis excesivos. Uno o dos si aportan contexto. + +## Uso de herramientas +- Cuando alguien pregunte por la hora o fecha, usa `current_time` antes de responder. +- No inventes datos temporales; siempre consulta la herramienta. diff --git a/agents/runtime.go b/agents/runtime.go index d50bea0..d4047ef 100644 --- a/agents/runtime.go +++ b/agents/runtime.go @@ -287,6 +287,10 @@ func buildToolRegistry(cfg *config.AgentConfig, sshExec *ssh.Executor, matrixCli logger.Debug("registered file tool") } + // current_time is always available + reg.Register(tools.NewCurrentTime()) + logger.Debug("registered current_time tool") + // matrix_send is always available reg.Register(tools.NewMatrixSend(matrixClient)) logger.Debug("registered matrix tool") diff --git a/cmd/launcher/main.go b/cmd/launcher/main.go index 3596f96..2dc8119 100644 --- a/cmd/launcher/main.go +++ b/cmd/launcher/main.go @@ -19,6 +19,7 @@ import ( "github.com/enmanuel/agents/agents" assistantagent "github.com/enmanuel/agents/agents/assistant" + asistente2agent "github.com/enmanuel/agents/agents/asistente2" devopsagent "github.com/enmanuel/agents/agents/devops" "github.com/enmanuel/agents/internal/config" "github.com/enmanuel/agents/pkg/decision" @@ -28,6 +29,7 @@ import ( // Add a new entry here when you create a new agent package. var rulesRegistry = map[string]func() []decision.Rule{ "assistant-bot": assistantagent.Rules, + "asistente-2": asistente2agent.Rules, "devops-bot": devopsagent.Rules, } diff --git a/cmd/verify/main.go b/cmd/verify/main.go new file mode 100644 index 0000000..bff5d12 --- /dev/null +++ b/cmd/verify/main.go @@ -0,0 +1,134 @@ +// Command verify sets up cross-signing keys for a Matrix bot user. +// This eliminates the "Encrypted by a device not verified by its owner" warning. +// +// Usage: +// +// go run -tags goolm ./cmd/verify --homeserver https://... --username asistente-2 --password --token +package main + +import ( + "context" + "crypto/sha256" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/spf13/cobra" + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/crypto" + "maunium.net/go/mautrix/crypto/cryptohelper" + "maunium.net/go/mautrix/id" +) + +func main() { + var ( + homeserver string + username string + password string + token string + storePath string + ) + + root := &cobra.Command{ + Use: "verify", + Short: "Set up cross-signing keys for a Matrix bot", + 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).`, + RunE: func(cmd *cobra.Command, args []string) error { + homeserver = strings.TrimRight(homeserver, "/") + serverName := homeserver + serverName = strings.TrimPrefix(serverName, "https://") + serverName = strings.TrimPrefix(serverName, "http://") + + userID := id.UserID(fmt.Sprintf("@%s:%s", username, serverName)) + fmt.Printf("→ Setting up cross-signing for %s\n", userID) + + // Create mautrix client + client, err := mautrix.NewClient(homeserver, userID, token) + if err != nil { + return fmt.Errorf("create client: %w", err) + } + + ctx := context.Background() + + // Resolve device ID + whoami, err := client.Whoami(ctx) + if err != nil { + return fmt.Errorf("whoami: %w", err) + } + client.DeviceID = whoami.DeviceID + fmt.Printf("→ Device ID: %s\n", client.DeviceID) + + // Initialize crypto + sum := sha256.Sum256([]byte(token)) + pickleKey := sum[:] + + dbPath := filepath.Join(storePath, "crypto.db") + if err := os.MkdirAll(filepath.Dir(dbPath), 0700); err != nil { + return fmt.Errorf("create store dir: %w", err) + } + + helper, err := cryptohelper.NewCryptoHelper(client, pickleKey, dbPath) + if err != nil { + return fmt.Errorf("create crypto helper: %w", err) + } + helper.DBAccountID = username + + if err := helper.Init(ctx); err != nil { + return fmt.Errorf("init crypto: %w", err) + } + defer helper.Close() + + client.Crypto = helper + + // Get the OlmMachine to generate cross-signing keys + olmMachine := helper.Machine() + if olmMachine == nil { + return fmt.Errorf("olm machine not available") + } + + fmt.Println("→ Generating and uploading cross-signing keys...") + _, _, err = olmMachine.GenerateAndUploadCrossSigningKeysWithPassword(ctx, password, "") + if err != nil { + // If keys already exist, try to just sign our device + fmt.Printf(" Note: %v\n", err) + fmt.Println("→ Attempting to sign own device with existing keys...") + return signOwnDevice(ctx, olmMachine, client) + } + + fmt.Println("✓ Cross-signing keys uploaded successfully") + fmt.Printf("✓ Device %s is now verified by %s\n", client.DeviceID, userID) + 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(&token, "token", "", "Bot access token") + root.Flags().StringVar(&storePath, "store", "./data/verify-crypto/", "Crypto store path") + _ = root.MarkFlagRequired("homeserver") + _ = root.MarkFlagRequired("username") + _ = root.MarkFlagRequired("password") + _ = root.MarkFlagRequired("token") + + 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, + } + err := mach.SignOwnDevice(ctx, device) + if err != nil { + return fmt.Errorf("sign own device: %w", err) + } + fmt.Printf("✓ Device %s signed with cross-signing key\n", client.DeviceID) + return nil +} diff --git a/cmd/verify/sqlite.go b/cmd/verify/sqlite.go new file mode 100644 index 0000000..20be13d --- /dev/null +++ b/cmd/verify/sqlite.go @@ -0,0 +1,12 @@ +package main + +import ( + "database/sql" + + moderncsqlite "modernc.org/sqlite" +) + +func init() { + // mautrix dbutil opens sqlite as "sqlite3"; register the pure-Go driver under that name. + sql.Register("sqlite3", &moderncsqlite.Driver{}) +} diff --git a/tools/time.go b/tools/time.go new file mode 100644 index 0000000..9994622 --- /dev/null +++ b/tools/time.go @@ -0,0 +1,31 @@ +package tools + +import ( + "context" + "fmt" + "time" +) + +// NewCurrentTime creates a current_time tool that returns the current date and time. +// Useful for agents that need temporal awareness. +func NewCurrentTime() Tool { + return Tool{ + Def: Def{ + Name: "current_time", + Description: "Returns the current date and time in the server's timezone. Use this when you need to know the current time or date.", + Parameters: []Param{ + {Name: "format", Type: "string", Description: "Optional Go time format string. Defaults to RFC3339 if empty.", Required: false}, + }, + }, + Exec: func(ctx context.Context, args map[string]any) Result { + layout := getString(args, "format") + if layout == "" { + layout = time.RFC3339 + } + + now := time.Now() + output := fmt.Sprintf("Current time: %s\nTimezone: %s", now.Format(layout), now.Location().String()) + return Result{Output: output} + }, + } +}