ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
detections / detections (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
233 lines
8.0 KiB
Go
233 lines
8.0 KiB
Go
package db
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
)
|
|
|
|
// Tool is one row of the built-in tool catalog. key + handler live in code; this
|
|
// row carries only the page-editable surface: description, parameter schema
|
|
// (structure read-only, per-param description/default editable), agent binding,
|
|
// and the on/off switch. See schema.sql §H and agent/toolcatalog.go.
|
|
type Tool struct {
|
|
Key string `json:"key"`
|
|
System bool `json:"system"`
|
|
Description string `json:"description"`
|
|
Schema json.RawMessage `json:"schema"`
|
|
Agents []string `json:"agents"`
|
|
Enabled bool `json:"enabled"`
|
|
Kind string `json:"kind"` // builtin | command | script | http
|
|
Exec json.RawMessage `json:"exec"` // 自定义工具执行规格(kind!=builtin)
|
|
Deferred bool `json:"deferred"` // schema 延迟(走 SearchExtraTools/ExecuteExtraTool)
|
|
Calls int `json:"calls"` // runtime ledger aggregate; not stored in tools
|
|
}
|
|
|
|
const toolCols = `key, system, description, schema, agents, enabled, kind, exec, deferred`
|
|
|
|
// SeedTool inserts a built-in tool's code-defined defaults ONCE. ON CONFLICT DO
|
|
// NOTHING: an existing row (possibly edited in the UI) is never overwritten on
|
|
// startup — that's what keeps page edits from being wiped every restart. Use
|
|
// UpsertToolForce for an explicit "reset to code default".
|
|
func (d *DB) SeedTool(key, desc string, schema, agents json.RawMessage) error {
|
|
if len(schema) == 0 {
|
|
schema = json.RawMessage("{}")
|
|
}
|
|
if len(agents) == 0 {
|
|
agents = json.RawMessage("[]")
|
|
}
|
|
_, err := d.Exec(`
|
|
INSERT INTO tools(key, system, description, schema, agents, enabled)
|
|
VALUES ($1, true, $2, $3, $4, true)
|
|
ON CONFLICT (key) DO NOTHING`, key, desc, schema, agents)
|
|
return err
|
|
}
|
|
|
|
// AddAgentToToolBinding adds an agent key to the given tools' `agents` arrays if
|
|
// not already present (idempotent). Used to give the built-in Auto agent its
|
|
// default toolset on existing DBs.
|
|
func (d *DB) AddAgentToToolBinding(agentKey string, keys []string) error {
|
|
for _, k := range keys {
|
|
if _, err := d.Exec(`UPDATE tools SET agents = agents || to_jsonb($1::text) WHERE key=$2 AND NOT (agents ? $1)`, agentKey, k); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RemoveAgentFromToolBindings strips an agent key from every tool's `agents`
|
|
// JSONB array — called when a custom agent is deleted so no tool keeps a dangling
|
|
// binding. Uses jsonb `-` (remove array element) guarded by `?` (membership).
|
|
func (d *DB) RemoveAgentFromToolBindings(agentKey string) error {
|
|
_, err := d.Exec(`UPDATE tools SET agents = agents - $1 WHERE agents ? $1`, agentKey)
|
|
return err
|
|
}
|
|
|
|
// RemoveAgentFromTool strips one agent key from a SINGLE tool's `agents` array —
|
|
// used by one-time migrations that change a tool's default binding on existing DBs
|
|
// (SeedTool is first-insert-only, so a changed default never reaches a seeded row).
|
|
func (d *DB) RemoveAgentFromTool(agentKey, toolKey string) error {
|
|
_, err := d.Exec(`UPDATE tools SET agents = agents - $1 WHERE key=$2 AND agents ? $1`, agentKey, toolKey)
|
|
return err
|
|
}
|
|
|
|
// UpsertToolForce overwrites a tool row with the given code-default values (used by
|
|
// the per-tool "reset" action). It resets description/schema/agents and re-enables
|
|
// the tool, but keeps system=true.
|
|
func (d *DB) UpsertToolForce(key, desc string, schema, agents json.RawMessage) error {
|
|
if len(schema) == 0 {
|
|
schema = json.RawMessage("{}")
|
|
}
|
|
if len(agents) == 0 {
|
|
agents = json.RawMessage("[]")
|
|
}
|
|
_, err := d.Exec(`
|
|
INSERT INTO tools(key, system, description, schema, agents, enabled)
|
|
VALUES ($1, true, $2, $3, $4, true)
|
|
ON CONFLICT (key) DO UPDATE
|
|
SET description = EXCLUDED.description,
|
|
schema = EXCLUDED.schema,
|
|
agents = EXCLUDED.agents,
|
|
enabled = true`, key, desc, schema, agents)
|
|
return err
|
|
}
|
|
|
|
// RefreshToolDefaults updates a system tool's model-facing description + schema to the
|
|
// code defaults, PRESERVING the user's agent binding + enabled flag. Used by one-time
|
|
// migrations to propagate a code schema change (SeedTool is first-insert-only, so a new
|
|
// parameter added in code otherwise never reaches an already-seeded row). No-op for
|
|
// custom tools or unknown keys.
|
|
func (d *DB) RefreshToolDefaults(key, desc string, schema json.RawMessage) error {
|
|
if len(schema) == 0 {
|
|
schema = json.RawMessage("{}")
|
|
}
|
|
_, err := d.Exec(`UPDATE tools SET description=$2, schema=$3, updated_at=now() WHERE key=$1 AND system`, key, desc, schema)
|
|
return err
|
|
}
|
|
|
|
func scanTool(rows interface{ Scan(...any) error }) (*Tool, error) {
|
|
var t Tool
|
|
var agents []byte
|
|
if err := rows.Scan(&t.Key, &t.System, &t.Description, &t.Schema, &agents, &t.Enabled, &t.Kind, &t.Exec, &t.Deferred); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(agents) > 0 {
|
|
_ = json.Unmarshal(agents, &t.Agents)
|
|
}
|
|
if t.Agents == nil {
|
|
t.Agents = []string{}
|
|
}
|
|
return &t, nil
|
|
}
|
|
|
|
// ListTools returns the whole tool catalog, ordered by key.
|
|
func (d *DB) ListTools() ([]*Tool, error) {
|
|
rows, err := d.Query(`SELECT ` + toolCols + ` FROM tools ORDER BY key`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []*Tool
|
|
for rows.Next() {
|
|
t, err := scanTool(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, t)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// GetTool fetches one tool row (nil if absent).
|
|
func (d *DB) GetTool(key string) (*Tool, error) {
|
|
row := d.QueryRow(`SELECT `+toolCols+` FROM tools WHERE key=$1`, key)
|
|
t, err := scanTool(row)
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
return t, err
|
|
}
|
|
|
|
// CreateCustomTool inserts a user-defined tool (system=false) with an execution
|
|
// spec. Fails if the key already exists.
|
|
func (d *DB) CreateCustomTool(t *Tool) error {
|
|
schema := t.Schema
|
|
if len(schema) == 0 {
|
|
schema = json.RawMessage("{}")
|
|
}
|
|
exec := t.Exec
|
|
if len(exec) == 0 {
|
|
exec = json.RawMessage("{}")
|
|
}
|
|
agents, _ := json.Marshal(t.Agents)
|
|
if len(agents) == 0 {
|
|
agents = json.RawMessage("[]")
|
|
}
|
|
_, err := d.Exec(`
|
|
INSERT INTO tools(key, system, description, schema, agents, enabled, kind, exec, deferred)
|
|
VALUES ($1, false, $2, $3, $4, $5, $6, $7, $8)`,
|
|
t.Key, t.Description, schema, agents, t.Enabled, t.Kind, exec, t.Deferred)
|
|
return err
|
|
}
|
|
|
|
// UpdateCustomTool updates a custom tool's editable fields (kind/exec/deferred +
|
|
// desc/schema/agents/enabled). Only touches system=false rows.
|
|
func (d *DB) UpdateCustomTool(t *Tool) error {
|
|
schema := t.Schema
|
|
if len(schema) == 0 {
|
|
schema = json.RawMessage("{}")
|
|
}
|
|
exec := t.Exec
|
|
if len(exec) == 0 {
|
|
exec = json.RawMessage("{}")
|
|
}
|
|
agents, _ := json.Marshal(t.Agents)
|
|
if len(agents) == 0 {
|
|
agents = json.RawMessage("[]")
|
|
}
|
|
_, err := d.Exec(`
|
|
UPDATE tools SET description=$2, schema=$3, agents=$4, enabled=$5, kind=$6, exec=$7, deferred=$8
|
|
WHERE key=$1 AND system=false`,
|
|
t.Key, t.Description, schema, agents, t.Enabled, t.Kind, exec, t.Deferred)
|
|
return err
|
|
}
|
|
|
|
// DeleteCustomTool removes a custom tool (system=false only; built-ins protected).
|
|
func (d *DB) DeleteCustomTool(key string) error {
|
|
_, err := d.Exec(`DELETE FROM tools WHERE key=$1 AND system=false`, key)
|
|
return err
|
|
}
|
|
|
|
// ListCustomTools returns only the user-defined (system=false) tools.
|
|
func (d *DB) ListCustomTools() ([]*Tool, error) {
|
|
rows, err := d.Query(`SELECT ` + toolCols + ` FROM tools WHERE system=false ORDER BY key`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []*Tool
|
|
for rows.Next() {
|
|
t, err := scanTool(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, t)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// UpdateTool saves the page-editable fields. key is never changed (it is welded to
|
|
// the Go handler). system tools: the caller must keep the schema structure — only
|
|
// per-param description/default and the agent binding are meant to move.
|
|
func (d *DB) UpdateTool(key, desc string, schema, agents json.RawMessage, enabled bool) error {
|
|
if len(schema) == 0 {
|
|
schema = json.RawMessage("{}")
|
|
}
|
|
if len(agents) == 0 {
|
|
agents = json.RawMessage("[]")
|
|
}
|
|
_, err := d.Exec(`
|
|
UPDATE tools SET description=$2, schema=$3, agents=$4, enabled=$5 WHERE key=$1`,
|
|
key, desc, schema, agents, enabled)
|
|
return err
|
|
}
|