package tools import ( "context" "encoding/json" "fmt" "io" "strings" "time" ) // Result is the tool payload returned to the model. type Result struct { Content string IsError bool } // Tool is one executable function with a JSON schema. type Tool interface { Name() string Description() string Parameters() json.RawMessage Execute(ctx context.Context, args json.RawMessage) (Result, error) } // BeforeHook can block a call after schema validation. type BeforeHook func(ctx context.Context, name string, args json.RawMessage) (block bool, reason string) // AfterHook can rewrite a tool result before it is returned. type AfterHook func(ctx context.Context, name string, args json.RawMessage, result Result) Result // Registry executes jailed tools and appends an audit line per call. type Registry struct { root string tools map[string]Tool order []string Before BeforeHook After AfterHook Audit io.Writer } func NewRegistry(root string) *Registry { return &Registry{root: root, tools: map[string]Tool{}} } func (r *Registry) Root() string { return r.root } func (r *Registry) Register(t Tool) { if _, ok := r.tools[t.Name()]; !ok { r.order = append(r.order, t.Name()) } r.tools[t.Name()] = t } func (r *Registry) Decls() []Decl { out := make([]Decl, 0, len(r.order)) for _, name := range r.order { t := r.tools[name] out = append(out, Decl{Name: t.Name(), Description: t.Description(), Parameters: t.Parameters()}) } return out } // Decl is the schema view of a tool. type Decl struct { Name string Description string Parameters json.RawMessage } // Execute validates arguments, runs hooks, and audits the outcome. func (r *Registry) Execute(ctx context.Context, name string, args json.RawMessage) Result { t, ok := r.tools[name] if !ok { res := Result{Content: "unknown tool " + name, IsError: true} r.audit(name, false, true, "unknown") return res } if len(args) == 0 { args = []byte(`{}`) } if err := validateArgs(t.Parameters(), args); err != nil { res := Result{Content: err.Error(), IsError: true} r.audit(name, false, true, "schema") return res } if r.Before != nil { if block, reason := r.Before(ctx, name, args); block { if reason == "" { reason = "blocked" } res := Result{Content: reason, IsError: true} r.audit(name, true, true, reason) return res } } res, err := t.Execute(ctx, args) if err != nil { res = Result{Content: err.Error(), IsError: true} } if r.After != nil { res = r.After(ctx, name, args, res) } r.audit(name, false, res.IsError, "") return res } func (r *Registry) audit(name string, blocked, isError bool, note string) { if r.Audit == nil { return } fmt.Fprintf(r.Audit, "%s\t%s\tblocked=%t\terror=%t\t%s\n", time.Now().UTC().Format(time.RFC3339Nano), name, blocked, isError, note) } func validateArgs(schema, args json.RawMessage) error { var spec struct { Type string `json:"type"` Required []string `json:"required"` Properties map[string]json.RawMessage `json:"properties"` } if len(schema) > 0 { if err := json.Unmarshal(schema, &spec); err != nil { return fmt.Errorf("invalid tool schema") } } var obj map[string]any if err := json.Unmarshal(args, &obj); err != nil { return fmt.Errorf("arguments must be a JSON object") } if spec.Type != "" && spec.Type != "object" { return fmt.Errorf("unsupported schema type %s", spec.Type) } for _, req := range spec.Required { v, ok := obj[req] if !ok || v == nil { return fmt.Errorf("missing required argument %s", req) } if s, ok := v.(string); ok && strings.TrimSpace(s) == "" { return fmt.Errorf("missing required argument %s", req) } } for key, raw := range spec.Properties { v, ok := obj[key] if !ok { continue } var prop struct { Type string `json:"type"` } if err := json.Unmarshal(raw, &prop); err != nil || prop.Type == "" { continue } if !matchesType(prop.Type, v) { return fmt.Errorf("argument %s must be %s", key, prop.Type) } } return nil } func matchesType(kind string, v any) bool { switch kind { case "string": _, ok := v.(string) return ok case "number": _, ok := v.(float64) return ok case "integer": f, ok := v.(float64) return ok && f == float64(int64(f)) case "boolean": _, ok := v.(bool) return ok default: return true } } // WorkspaceTools registers the document workspace tool set. func WorkspaceTools(root string) *Registry { r := NewRegistry(root) r.Register(&readFile{root: root}) r.Register(&writeFile{root: root}) r.Register(&editFile{root: root}) r.Register(&listDir{root: root}) r.Register(&grepTool{root: root}) return r } func mustPath(root, rel string) (string, error) { return Resolve(root, rel) } func readArg(args json.RawMessage, key string) (string, error) { var obj map[string]string if err := json.Unmarshal(args, &obj); err != nil { return "", err } return obj[key], nil } const maxReadBytes = 200 << 10