Files
artex/agent/toolcatalog.go
dela 0335d572de
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
First Commit
2026-10-09 08:38:16 +08:00

202 lines
8.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package agent
import (
"context"
"encoding/json"
actool "github.com/Autumn-27/norma/tool"
)
// 本文件把「内置工具」从纯代码变成可枚举、可被 DB 覆盖的目录:
// - BuiltinToolSeeds():把三个执行 agent 的内置工具集展开成 seed 记录(key +
// 描述 + 参数 schema + 默认绑定的 agent),供服务端开机幂等播种进 tools 表。
// - ToolResolve 钩子:运行时按 DB 里的 tools 行对已装配的工具做「按 agent 过滤 +
// 覆盖描述/schema + 注入参数默认值」。key/handler 仍在代码层,DB 只改「散文与默认值」。
// handler(Call 行为)永远来自代码——DB 改不了它,只能改模型看到的说明与缺省入参。
// ToolSeed 是一个内置工具的可播种快照:key 即 CoreTool.Name()(与 handler 死绑,
// UI 只读),Desc/Schema 取自代码里的工具定义,Agents 是代码默认把它给了哪些 agent。
type ToolSeed struct {
Key string // = CoreTool.Name(),主键,不可改
Desc string // 顶层描述(可在 UI 覆盖)
Schema map[string]any // 参数 JSON-Schema(结构只读,description/default 可在 UI 改)
Agents []string // 默认绑定的 agent key(worker/planner/mainagent)
}
// builtinToolsByAgent 用一个「只读空壳」ToolSet(nil stores)构造每个执行 agent 的
// 领域工具集。工具构造函数只把闭包塞进 Spec、构造期不解引用 store,所以 nil 安全——
// 这些工具在这里只用来读 Name()/Description()/InputSchema(),绝不 Call。
//
// 刻意【不含】SDK 通用工具 actool.DefaultTools()(Read/Write/Edit/MultiEdit/LS/Glob/
// Grep/Bash):它们每个 agent 都固定拥有、没有「绑定到谁」的取舍,且说明大多在 Prompt()
// 里(本表只覆盖 Description(),会造成半覆盖误导)。不 seed → 无 DB 行 → ToolResolve
// 原样放行、不覆盖,行为与从前一致。只有 artex 自己的领域工具入表可管。
func builtinToolsByAgent() map[string][]actool.CoreTool {
ts := NewToolSet(nil, "")
return map[string][]actool.CoreTool{
"mainagent": ts.MainAgentTools(),
"planner": ts.PlannerTools(),
"worker": ts.WorkerTools(),
// goals(目标拆解器)默认绑 set_goals + set_constraints:靠它们把拆出的目标、
// 抽出的操作约束写进库。与 mainagent 共用同一受管工具,web 端可改描述/schema、按 agent 勾选。
"goals": {ts.setGoals(), ts.setConstraints()},
// auto 默认绑漏洞上报 + 资产管理工具,其他域工具可在 UI 按需勾选。
// 新库由此 seed 写入;老库由 seedAutoDefaultBindings 迁移。
"auto": {ts.addFinding(), ts.insertAssets(), ts.addCompanyScope(), ts.listAssets(), ts.listCompanies()},
// pentest(独立渗透 agent)默认绑:查资产 / 插资产 / 报漏洞 / 查漏洞 / 查企业。
// 新库由此 seed 写入;老库由 seedPentestDefaultBindings 迁移。
"pentest": {ts.listAssets(), ts.insertAssets(), ts.addFinding(), ts.listFindings(), ts.listCompanies()},
}
}
// defaultUnbound:这些 system 工具会照常入目录(web 端可见、可手动按 agent 勾选),但
// 默认【不绑任何 agent】——ToolResolve 对空绑定的工具对所有 agent 一律丢弃,须显式 opt-in。
// 之所以仍留在某个 agent 的 base 工具集里(如 goal_met 在 PlannerTools):一是让 seed 能
// 构造它拿到 desc/schema,二是用户手动绑回后运行时 base 里有它、ToolResolve 才留得住。
//
// goal_met:绕过逐个 prove_goal、直接从全局宣布【整个任务完成】,权重大且有误判风险,又与
// 「prove_goal 标记最后一个目标 → 自动收官」重复,故默认不给任何 agent,需要时再手动绑。
var defaultUnbound = map[string]bool{"goal_met": true}
// BuiltinToolSeeds 把各 agent 的内置工具集去重合并成 seed 列表:同名工具(如 list_assets
// 多个 agent 都有)合成一条,Agents 取并集;defaultUnbound 里的工具则强制绑定为空。
func BuiltinToolSeeds() []ToolSeed {
byAgent := builtinToolsByAgent()
order := []string{"mainagent", "goals", "planner", "worker", "auto", "pentest"}
type acc struct {
tool actool.CoreTool
agents []string
}
m := map[string]*acc{}
var keys []string
for _, ak := range order {
for _, t := range byAgent[ak] {
a, ok := m[t.Name()]
if !ok {
a = &acc{tool: t}
m[t.Name()] = a
keys = append(keys, t.Name())
}
a.agents = append(a.agents, ak)
}
}
out := make([]ToolSeed, 0, len(keys))
for _, k := range keys {
a := m[k]
agents := a.agents
if defaultUnbound[k] {
agents = []string{} // 入目录、可手动绑,但默认不给任何 agent(存 [] 而非 null,与其它工具一致)
}
out = append(out, ToolSeed{
Key: k,
Desc: a.tool.Description(),
Schema: a.tool.InputSchema(),
Agents: agents,
})
}
return out
}
// ToolResolve, if set, post-processes an agent's fully-assembled tool list against
// the DB tools table: it drops tools not bound to this agent (or globally disabled)
// and wraps the rest so the model sees the DB-overridden description/schema and
// 缺省入参 get injected. Tools with no matching DB row (MCP/skill/host tools like
// traffic) pass through untouched. nil = tools unchanged. Wired in server/assembly.go.
var ToolResolve func(ctx context.Context, agentKey string, tools []actool.CoreTool) []actool.CoreTool
// DecorateTool wraps t so Description()/InputSchema() report the DB overrides and
// Call() injects scalar parameter defaults (from schema's "default" props) whenever
// the model omitted them. Name/Prompt/permission/scheduler flags delegate to t, so
// the tool's identity and handler are unchanged. Empty desc/schema fall back to t's.
func DecorateTool(t actool.CoreTool, desc string, schema map[string]any) actool.CoreTool {
if desc == "" {
desc = t.Description()
}
if len(schema) == 0 {
schema = t.InputSchema()
}
return &overriddenTool{CoreTool: t, desc: desc, schema: schema}
}
// overriddenTool is a CoreTool decorator: it embeds the original (so all behavioral
// methods — Prompt/IsReadOnly/IsConcurrencySafe/CheckPermissions/Name — delegate)
// and overrides only the model-facing description/schema plus default injection.
type overriddenTool struct {
actool.CoreTool
desc string
schema map[string]any
}
func (o *overriddenTool) Description() string { return o.desc }
func (o *overriddenTool) InputSchema() map[string]any { return o.schema }
func (o *overriddenTool) Call(ctx context.Context, in json.RawMessage, tc *actool.ToolContext) (actool.Result, error) {
return o.CoreTool.Call(ctx, injectDefaults(in, o.schema), tc)
}
// injectDefaults fills scalar parameter defaults declared in the (possibly edited)
// schema into the input JSON whenever the model omitted the field or left it empty/
// null. Structure (names/types/required) is untouched — only缺省值 are merged in.
func injectDefaults(in json.RawMessage, schema map[string]any) json.RawMessage {
defs := scalarDefaults(schema)
if len(defs) == 0 {
return in
}
m := map[string]json.RawMessage{}
if len(in) > 0 {
if err := json.Unmarshal(in, &m); err != nil {
return in // non-object input: don't touch it
}
}
changed := false
for k, dv := range defs {
if cur, ok := m[k]; !ok || isEmptyJSON(cur) {
m[k] = dv
changed = true
}
}
if !changed {
return in
}
b, err := json.Marshal(m)
if err != nil {
return in
}
return b
}
// scalarDefaults extracts properties[k]["default"] for scalar params (string/
// integer/number/boolean). Array/object defaults are skipped: merging them is
// ambiguous and not worth the surprise.
func scalarDefaults(schema map[string]any) map[string]json.RawMessage {
props, _ := schema["properties"].(map[string]any)
if len(props) == 0 {
return nil
}
out := map[string]json.RawMessage{}
for name, raw := range props {
p, ok := raw.(map[string]any)
if !ok {
continue
}
dv, ok := p["default"]
if !ok || dv == nil {
continue
}
switch p["type"] {
case "string", "integer", "number", "boolean":
if b, err := json.Marshal(dv); err == nil {
out[name] = b
}
}
}
return out
}
func isEmptyJSON(raw json.RawMessage) bool {
s := string(raw)
return s == "null" || s == `""`
}