feat: add asistente-2 agent with tool-use and E2EE verification

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 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 21:38:55 +00:00
parent 0f8d2f9ca0
commit dd4a101139
8 changed files with 514 additions and 0 deletions
+39
View File
@@ -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{},
}},
},
}
}
+268
View File
@@ -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
+24
View File
@@ -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.
+4
View File
@@ -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")
+2
View File
@@ -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,
}
+134
View File
@@ -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 <pass> --token <access_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
}
+12
View File
@@ -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{})
}
+31
View File
@@ -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}
},
}
}