// Package tools is the tool layer: everything an agent can do that is not // talking. // // A tool is a named, schema'd function the model may call. The contract below // is §4's, and three parts of it are load-bearing rather than stylistic: // // - **Every handler authorizes on ctx.Principal, first line.** A handler that // reads ctx for anything except authorization is wrong. This is where I1 // lives: an agent may read exactly what its caller could read directly, and // the only way to guarantee that is for the tool — not the model, not the // prompt — to apply the caller's own permissions. // - **Filters are pre-filters.** A handler narrows in SQL, never in Go over a // fetched result set. I2: post-filtering leaks through counts and ranking // positions even when no forbidden row is ever printed. // - **Errors are returned, not raised.** A tool that panics or returns a bare // error takes the whole run with it. The runtime decides whether the model // sees a failure and retries, and it can only decide that if the failure // arrives as data. package tools import ( "context" "encoding/json" "fmt" "github.com/krow/krow-backend/go-api/internal/authctx" ) // Effect is whether a tool changes anything. type Effect string const ( // EffectRead observes. Safe to call without asking anyone. EffectRead Effect = "read" // EffectWrite writes, sends, deletes, charges or notifies. Never runs // without a resolved confirmation — see Tool.RequiresConfirmation. EffectWrite Effect = "write" ) // DefaultMaxResultBytes caps a tool result. // // Not a performance guard. An unbounded result is an unbounded prompt on the // next turn, which is an unbounded bill and eventually a context overflow that // presents as the model ignoring the middle of its own evidence. const DefaultMaxResultBytes = 262_144 // Context is what a handler is given about its caller. // // Carries the principal, the tenant, the run and what budget is left, per §4. // Deliberately a struct and not a context.Context value: a handler must not be // able to *forget* to read it, and a compile error is a better reminder than a // convention. type Context struct { // Principal is the caller the agent is acting for. Never the agent. Principal authctx.Identity // RunID addresses the trajectory this call is recorded in. RunID string // RemainingTokens is what the run has left to spend. A handler may use it // to decide how much to return; it must not use it to decide whether the // caller is allowed something. RemainingTokens int64 // Confirmation is the resolved token for a write. Empty on a read, and // empty on a write that has not been confirmed yet — which the dispatcher // refuses before a handler is ever reached. Confirmation string // AgentID is the running agent, for the record. Never for authorization — // what a caller may do is decided by their principal, and an agent that // could widen that by being named would be an agent that expands access. AgentID string // KnowledgeSources are the corpora the running agent's SPEC declares. // // Here rather than in a tool argument, and the difference is the whole // security property: an argument is something a model can choose, and which // documents an agent may read is not the model's to choose. The loop sets // this from the agent record; nothing in the conversation can reach it. // // Empty means this agent has no knowledge. It does not mean "all of it" — // retrieval refuses an empty source list for exactly that reason. KnowledgeSources []string } // OrgID is the tenant this call runs inside. I5 — every handler's query // narrows by it, and there is no path that produces a call without one. func (c Context) OrgID() string { return c.Principal.OrgID } // Handler runs one tool. // // The signature is `(inputs, ctx)` in §4's terms, with the Go context first by // convention so cancellation and the deadline reach the query. A handler // returns a Result and never an error: a failure is a value the runtime routes, // not a panic that ends a run. type Handler func(ctx context.Context, tc Context, inputs json.RawMessage) Result // Tool is one callable capability. type Tool struct { Name string Description string // InputSchema is JSON Schema. Every field described, because the // description is what the model reads instead of documentation — and a // tool whose schema demands an id the model was never given is a design // bug, not a prompt problem. Add a lookup tool instead. InputSchema map[string]any Effect Effect // RequiresConfirmation is forced true for a write by Register. It is a // field rather than a method so a read tool may opt in — some reads are // expensive enough to be worth asking about — but a write can never opt // out. The model does not get a say either way. RequiresConfirmation bool // Confirm renders, in plain language, what this tool will do if approved. // Mandatory when RequiresConfirmation is set: Register refuses a write // without one, because a confirmation a person cannot read is not a // confirmation, it is a click. See confirm.go. Confirm Confirmer MaxResultBytes int Handler Handler } // Result is what a tool returns. // // Structured data, never prose: formatting is the model's job, and a handler // that returns a sentence has decided how the answer reads before the model has // seen the question. type Result struct { Data any `json:"data,omitempty"` Error *ToolError `json:"error,omitempty"` // Truncated says the result was cut at MaxResultBytes. Set alongside the // data that survived — never as a silent drop, because a model given a // truncated list with no marker will reason about it as if it were whole. Truncated bool `json:"truncated,omitempty"` // Confirmation is set when a write was described but not performed. It is // neither success nor error: nothing happened, and something must now be // approved by a person before anything can. The loop reads this and ends // the run at ConfirmationPending rather than handing it to the model — // see I4, and the note on Registry.Dispatch. Confirmation *Confirmation `json:"confirmation,omitempty"` } // ToolError is a failure a handler chose to report. type ToolError struct { Code string `json:"code"` Message string `json:"message"` } // Failf builds an error result. func Failf(code, format string, args ...any) Result { return Result{Error: &ToolError{Code: code, Message: fmt.Sprintf(format, args...)}} } // OK builds a success result. func OK(data any) Result { return Result{Data: data} } // Standard tool error codes. A denial is deliberately one code with one // wording — see Denied. const ( CodeDenied = "tool.denied" CodeInvalidInput = "tool.invalid_input" CodeUnavailable = "tool.unavailable" CodeFailed = "tool.failed" ) // Denied is the single refusal every handler returns when a caller may not do // something. // // One code, one message, no detail. §8: a denial must not reveal that the // resource exists. Two different refusals — "no such venue" and "not your // venue" — are an oracle: a caller who can tell them apart can enumerate what // they cannot see, and the agent will happily run that enumeration for them one // question at a time. func Denied() Result { return Result{Error: &ToolError{ Code: CodeDenied, Message: "the caller does not have access to this", }} }