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
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
detections / detections (push) Canceled after 0s
202 lines
8.2 KiB
Go
202 lines
8.2 KiB
Go
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 == `""`
|
||
}
|