post inicial
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
// Package tools implementa las herramientas que el modelo puede invocar:
|
||||
// ejecución de procesos, shell, sistema de archivos e información del sistema.
|
||||
//
|
||||
// Toda herramienta sigue el mismo camino (Registry.Execute):
|
||||
//
|
||||
// 1. validar estrictamente los argumentos JSON (campos desconocidos = error);
|
||||
// 2. consultar al policy.Guard;
|
||||
// 3. en --dry-run, describir la acción en lugar de ejecutarla;
|
||||
// 4. ejecutar, auditar en agent.log y redactar secretos de la salida.
|
||||
//
|
||||
// Nunca se ejecuta texto del modelo que no haya pasado por este camino.
|
||||
package tools
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"sysagent/internal/audit"
|
||||
"sysagent/internal/llm"
|
||||
"sysagent/internal/memory"
|
||||
"sysagent/internal/policy"
|
||||
)
|
||||
|
||||
// Env es el entorno compartido por todas las herramientas.
|
||||
type Env struct {
|
||||
Guard *policy.Guard
|
||||
Policy *policy.Policy
|
||||
Log *audit.Logger
|
||||
Root string // PROJECT_ROOT
|
||||
DryRun bool
|
||||
// Memory es el almacén de memoria.md (herramientas remember/forget).
|
||||
Memory *memory.Store
|
||||
// HTTP es el cliente compartido de las herramientas de red (§67).
|
||||
HTTP *http.Client
|
||||
UserAgent string
|
||||
// SearchURL es la plantilla del buscador ({query}); vacío = DuckDuckGo.
|
||||
SearchURL string
|
||||
// Secrets guarda credenciales de la sesión; Prompt las pide al operador
|
||||
// (nil en modo tareas).
|
||||
Secrets *SecretStore
|
||||
Prompt SecretPrompter
|
||||
// NVDBase y DebianBase permiten apuntar cve_lookup a espejos (y a
|
||||
// servidores de prueba); vacío = servicios oficiales.
|
||||
NVDBase, DebianBase string
|
||||
// Now permite fijar la hora en tests (nombres de backup).
|
||||
Now func() time.Time
|
||||
}
|
||||
|
||||
func (e *Env) now() time.Time {
|
||||
if e.Now != nil {
|
||||
return e.Now()
|
||||
}
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
// Result es el resultado de una herramienta.
|
||||
type Result struct {
|
||||
// Output es el texto que recibe el modelo.
|
||||
Output string
|
||||
// OK indica éxito (para comandos: exit code 0).
|
||||
OK bool
|
||||
// Denied indica que la política impidió la acción.
|
||||
Denied bool
|
||||
// Summary es una línea corta para consola y auditoría.
|
||||
Summary string
|
||||
// FilesChanged lista archivos creados/modificados/eliminados.
|
||||
FilesChanged []string
|
||||
// Backups lista copias de seguridad creadas.
|
||||
Backups []string
|
||||
// ExitCode de comandos (-1 si no aplica o no arrancó).
|
||||
ExitCode int
|
||||
// Audit son pares clave/valor adicionales para agent.log.
|
||||
Audit []string
|
||||
// Diff es el cambio exacto aplicado a un archivo existente (§88).
|
||||
Diff string
|
||||
}
|
||||
|
||||
// Tool es una herramienta invocable por el modelo.
|
||||
type Tool interface {
|
||||
Name() string
|
||||
Description() string
|
||||
// Schema devuelve el JSON Schema de los argumentos.
|
||||
Schema() map[string]any
|
||||
// Run ejecuta la herramienta con argumentos ya decodificados por decode.
|
||||
Run(ctx context.Context, env *Env, raw json.RawMessage) Result
|
||||
}
|
||||
|
||||
// Registry agrupa las herramientas disponibles.
|
||||
type Registry struct {
|
||||
env *Env
|
||||
tools map[string]Tool
|
||||
}
|
||||
|
||||
// NewRegistry crea el registro con todas las herramientas estándar.
|
||||
func NewRegistry(env *Env) *Registry {
|
||||
r := &Registry{env: env, tools: map[string]Tool{}}
|
||||
for _, t := range []Tool{
|
||||
execTool{}, shellTool{},
|
||||
readFileTool{}, writeFileTool{}, editFileTool{}, appendFileTool{},
|
||||
copyFileTool{}, moveFileTool{}, removeFileTool{}, mkdirTool{},
|
||||
listDirTool{}, statTool{}, chmodTool{}, chownTool{},
|
||||
systemInfoTool{}, rememberTool{}, forgetTool{},
|
||||
webSearchTool{}, webFetchTool{}, cveLookupTool{}, downloadTool{},
|
||||
applyPatchTool{}, diffFilesTool{}, registerToolTool{}, findProgramTool{},
|
||||
askSecretTool{},
|
||||
} {
|
||||
r.tools[t.Name()] = t
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Names devuelve los nombres ordenados.
|
||||
func (r *Registry) Names() []string {
|
||||
names := make([]string, 0, len(r.tools))
|
||||
for n := range r.tools {
|
||||
names = append(names, n)
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
// Specs devuelve las definiciones para tool calling nativo.
|
||||
func (r *Registry) Specs() []llm.Tool {
|
||||
var out []llm.Tool
|
||||
for _, n := range r.Names() {
|
||||
t := r.tools[n]
|
||||
out = append(out, llm.Tool{Type: "function", Function: llm.ToolFunction{
|
||||
Name: t.Name(), Description: t.Description(), Parameters: t.Schema(),
|
||||
}})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Describe genera la documentación de herramientas para el protocolo JSON
|
||||
// (cuando el servidor no soporta tool calling nativo).
|
||||
func (r *Registry) Describe() string {
|
||||
var b strings.Builder
|
||||
for _, n := range r.Names() {
|
||||
t := r.tools[n]
|
||||
schema, _ := json.Marshal(t.Schema())
|
||||
fmt.Fprintf(&b, "- %s: %s\n arguments schema: %s\n", t.Name(), t.Description(), schema)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// Execute valida y ejecuta una llamada. label identifica la tarea para auditoría.
|
||||
func (r *Registry) Execute(ctx context.Context, label, name, args string) Result {
|
||||
t, ok := r.tools[name]
|
||||
if !ok {
|
||||
return Result{Output: fmt.Sprintf("error: unknown tool %q; available tools: %s", name, strings.Join(r.Names(), ", ")),
|
||||
Summary: "unknown tool " + name, ExitCode: -1}
|
||||
}
|
||||
args = strings.TrimSpace(args)
|
||||
if args == "" {
|
||||
args = "{}"
|
||||
}
|
||||
if !json.Valid([]byte(args)) || !strings.HasPrefix(args, "{") {
|
||||
return Result{Output: fmt.Sprintf("error: arguments for %s must be a JSON object, got: %s", name, clip(args, 200)),
|
||||
Summary: "invalid arguments", ExitCode: -1}
|
||||
}
|
||||
|
||||
if len(args) > maxArgsBytes {
|
||||
return Result{Output: fmt.Sprintf("error: arguments for %s are too large (%d bytes, max %d)", name, len(args), maxArgsBytes),
|
||||
Summary: "arguments too large", ExitCode: -1}
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
res := r.run(ctx, t, args)
|
||||
elapsed := time.Since(start).Round(time.Millisecond)
|
||||
r.env.Log.Debugf("tool %s args=%s -> ok=%t denied=%t exit=%d (%s)", name, clip(args, 500), res.OK, res.Denied, res.ExitCode, elapsed)
|
||||
|
||||
res.Output = r.env.Log.Redact(res.Output)
|
||||
res.Summary = r.env.Log.Redact(res.Summary)
|
||||
|
||||
result := "ok"
|
||||
switch {
|
||||
case res.Denied:
|
||||
result = "denied"
|
||||
case !res.OK:
|
||||
result = "error"
|
||||
}
|
||||
fields := []string{"task", label, "action", "tool:" + name, "detail", clip(res.Summary, 500),
|
||||
"result", result, "exit_code", fmt.Sprint(res.ExitCode), "elapsed", elapsed.String()}
|
||||
if len(res.FilesChanged) > 0 {
|
||||
fields = append(fields, "files_changed", strings.Join(res.FilesChanged, ","))
|
||||
}
|
||||
if len(res.Backups) > 0 {
|
||||
fields = append(fields, "backups", strings.Join(res.Backups, ","))
|
||||
}
|
||||
if r.env.DryRun {
|
||||
fields = append(fields, "dry_run", "true")
|
||||
}
|
||||
fields = append(fields, res.Audit...)
|
||||
r.env.Log.Action(fields...)
|
||||
return res
|
||||
}
|
||||
|
||||
// maxArgsBytes limita el tamaño de los argumentos de una llamada.
|
||||
const maxArgsBytes = 40 << 20
|
||||
|
||||
// run ejecuta la herramienta recuperando pánicos: un fallo de programación
|
||||
// en una herramienta se informa como error al modelo en lugar de tumbar al
|
||||
// agente a mitad de una tarea (sin guardar contexto.mem).
|
||||
func (r *Registry) run(ctx context.Context, t Tool, args string) (res Result) {
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
r.env.Log.Errorf("internal error in tool %s: %v\n%s", t.Name(), p, debug.Stack())
|
||||
res = Result{Output: fmt.Sprintf("internal error in tool %s: %v (reported in agent.log)", t.Name(), p),
|
||||
Summary: fmt.Sprintf("internal error: %v", p), ExitCode: -1}
|
||||
}
|
||||
}()
|
||||
return t.Run(ctx, r.env, json.RawMessage(args))
|
||||
}
|
||||
|
||||
// decode decodifica estrictamente los argumentos en v.
|
||||
func decode(raw json.RawMessage, v any) error {
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(v); err != nil {
|
||||
return fmt.Errorf("invalid arguments: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fail construye un Result de error.
|
||||
func fail(summary string, err error) Result {
|
||||
res := Result{Output: "error: " + err.Error(), Summary: summary + ": " + err.Error(), ExitCode: -1}
|
||||
if policy.IsDenied(err) {
|
||||
res.Denied = true
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// dryRun construye el Result de una acción simulada.
|
||||
func dryRun(what string) Result {
|
||||
return Result{OK: true, Output: "DRY-RUN: not executed. Would " + what, Summary: "dry-run: " + what, ExitCode: -1}
|
||||
}
|
||||
|
||||
func clip(s string, n int) string {
|
||||
if len(s) <= n {
|
||||
return s
|
||||
}
|
||||
return s[:n] + "..."
|
||||
}
|
||||
|
||||
// Helpers para esquemas JSON compactos.
|
||||
func obj(required []string, props map[string]any) map[string]any {
|
||||
s := map[string]any{"type": "object", "properties": props, "additionalProperties": false}
|
||||
if len(required) > 0 {
|
||||
s["required"] = required
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func str(desc string) map[string]any { return map[string]any{"type": "string", "description": desc} }
|
||||
func num(desc string) map[string]any { return map[string]any{"type": "integer", "description": desc} }
|
||||
func boolean(desc string) map[string]any {
|
||||
return map[string]any{"type": "boolean", "description": desc}
|
||||
}
|
||||
func strList(desc string) map[string]any {
|
||||
return map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": desc}
|
||||
}
|
||||
Reference in New Issue
Block a user