新增 seed_first_intent(默认开启):创建即把"描述+目标"作为一条意图 写入 frontier,worker 轮询领取立即执行,省掉开跑前的首轮 planner LLM; 跑完由 NotifyDone 正常唤醒 planner 判定/补充。CTF 场景常一 work 解决。 - Engine.Run 首轮 kick 改为"frontier 为空才触发",一处覆盖普通/种子/恢复三种情况 - HTTP createTask 与 spawn_task 工具同步支持;bump 工具 schema 刷新 flag 使旧库生效 Co-Authored-By: Claude Opus 4.8 <[email protected]>
490 lines
21 KiB
Go
490 lines
21 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Autumn-27/artex/agent"
|
|
"github.com/Autumn-27/norma/permission"
|
|
actool "github.com/Autumn-27/norma/tool"
|
|
)
|
|
|
|
// jsonResult marshals v to a JSON tool result.
|
|
func jsonResult(v any) (actool.Result, error) {
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
return actool.Errorf(err.Error()), nil
|
|
}
|
|
return actool.Text(string(b)), nil
|
|
}
|
|
|
|
// 本文件实现 P2「跨任务编排工具集」(docs/跑分编排 §2 P2)。这些是 host 工具——需要
|
|
// 访问 Manager(任意任务的 Store)、Engine(暂停)、以及建任务流程,所以住在 server 层。
|
|
// 读类工具把「现有 per-task 工具」重定向到目标任务的 store 上跑(建一个临时 ToolSet
|
|
// 并 Call 其对应工具),从而复用完全相同的逻辑;控制类(spawn/pause)直接调 Manager/Engine。
|
|
// 它们像流量工具一样 seed 进 tools 表、按 agent 绑定(只绑给编排 agent 才可见)。
|
|
|
|
// hostTools is the runtime host-tool provider fed to ToolAugment: traffic tools
|
|
// (gated by capture) + cross-task orchestration tools + user-defined custom tools.
|
|
// The second return is the names of custom tools flagged `deferred` (schema
|
|
// withheld, routed via SearchExtraTools/ExecuteExtraTool). Per-agent binding still
|
|
// decides who actually sees any of them.
|
|
//
|
|
//nolint:unused // used as the hostTools provider in wireAgentAugment
|
|
func (s *Server) hostTools() ([]actool.CoreTool, map[string][]string) {
|
|
tools := append(s.m.HostTools(), s.orchestrationTools()...)
|
|
tools = append(tools, s.platformTools()...) // 平台操作工具(建改 skill/工具/MCP,给 Auto 用)
|
|
custom, err := s.customTools()
|
|
if err != nil {
|
|
log.Printf("[custom-tool] 加载失败: %v", err)
|
|
return tools, nil
|
|
}
|
|
tools = append(tools, custom...)
|
|
// deferred custom tools → name -> its bound agent keys. ToolAugment turns a
|
|
// name into a deferred entry only for agents it's actually bound to (so we don't
|
|
// advertise a tool the per-agent binding will drop from the callable set).
|
|
deferred := map[string][]string{}
|
|
rows, _ := s.m.pg.ListCustomTools()
|
|
for _, t := range rows {
|
|
if t.Deferred && t.Enabled {
|
|
deferred[t.Key] = t.Agents
|
|
}
|
|
}
|
|
return tools, deferred
|
|
}
|
|
|
|
// orchestrationTools returns the cross-task tool set. Bound per-agent via the
|
|
// tools table (default: no binding — opt-in for orchestration agents).
|
|
func (s *Server) orchestrationTools() []actool.CoreTool {
|
|
return []actool.CoreTool{
|
|
s.toolListTasks(),
|
|
s.toolListLLMProfiles(),
|
|
s.toolSpawnTask(),
|
|
s.toolPauseTask(),
|
|
s.toolGetTaskGraph(),
|
|
s.toolListTaskFindings(),
|
|
s.toolAddHint(),
|
|
s.toolGetWorkerTrace(),
|
|
s.toolListWorkerTraces(),
|
|
s.toolSearchWorkerTraces(),
|
|
}
|
|
}
|
|
|
|
// --- schema helpers ---
|
|
|
|
func strParam(desc string) map[string]any {
|
|
return map[string]any{"type": "string", "description": desc}
|
|
}
|
|
|
|
// parseProfileID reads an LLM profile id from a tool arg that may arrive as a JSON
|
|
// number (5) or a numeric string ("5"); returns 0 when absent/unparseable.
|
|
func parseProfileID(raw json.RawMessage) int64 {
|
|
if len(raw) == 0 {
|
|
return 0
|
|
}
|
|
var n int64
|
|
if json.Unmarshal(raw, &n) == nil {
|
|
return n
|
|
}
|
|
var str string
|
|
if json.Unmarshal(raw, &str) == nil {
|
|
v, _ := strconv.ParseInt(strings.TrimSpace(str), 10, 64)
|
|
return v
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func objSchema(props map[string]any, required ...string) map[string]any {
|
|
m := map[string]any{"type": "object", "properties": props}
|
|
if len(required) > 0 {
|
|
req := make([]any, len(required))
|
|
for i, r := range required {
|
|
req[i] = r
|
|
}
|
|
m["required"] = req
|
|
}
|
|
return m
|
|
}
|
|
|
|
func roTool(name, desc string, schema map[string]any, run func(context.Context, json.RawMessage) (actool.Result, error)) actool.CoreTool {
|
|
return actool.Build(actool.Spec{
|
|
Name: name, Description: desc, Schema: schema,
|
|
ReadOnly: func(json.RawMessage) bool { return true },
|
|
Concurrent: func(json.RawMessage) bool { return true },
|
|
Permissions: func(context.Context, json.RawMessage, permission.Context) permission.Decision {
|
|
return permission.Allowed()
|
|
},
|
|
Run: func(ctx context.Context, in json.RawMessage, _ *actool.ToolContext) (actool.Result, error) {
|
|
return run(ctx, in)
|
|
},
|
|
})
|
|
}
|
|
|
|
func wrTool(name, desc string, schema map[string]any, run func(context.Context, json.RawMessage) (actool.Result, error)) actool.CoreTool {
|
|
return actool.Build(actool.Spec{
|
|
Name: name, Description: desc, Schema: schema,
|
|
Permissions: func(context.Context, json.RawMessage, permission.Context) permission.Decision {
|
|
return permission.Allowed()
|
|
},
|
|
Run: func(ctx context.Context, in json.RawMessage, _ *actool.ToolContext) (actool.Result, error) {
|
|
return run(ctx, in)
|
|
},
|
|
})
|
|
}
|
|
|
|
// delegateToTask resolves the `task_id` in the input, builds a ToolSet bound to
|
|
// that task's store, strips task_id, and calls the chosen per-task tool — so the
|
|
// cross-task read reuses the exact in-task logic against another task.
|
|
func (s *Server) delegateToTask(ctx context.Context, in json.RawMessage, pick func(*agent.ToolSet) actool.CoreTool) (actool.Result, error) {
|
|
var head struct {
|
|
TaskID string `json:"task_id"`
|
|
}
|
|
_ = json.Unmarshal(in, &head)
|
|
if strings.TrimSpace(head.TaskID) == "" {
|
|
return actool.Errorf("task_id 为必填"), nil
|
|
}
|
|
t, ok := s.m.Task(head.TaskID)
|
|
if !ok {
|
|
return actool.Errorf("task 不存在: " + head.TaskID), nil
|
|
}
|
|
var m map[string]json.RawMessage
|
|
_ = json.Unmarshal(in, &m)
|
|
delete(m, "task_id")
|
|
inner, _ := json.Marshal(m)
|
|
tsx := agent.NewToolSet(t.Store, "orchestrator")
|
|
if s.m.Assets() != nil {
|
|
tsx.SetAssetStore(s.m.Assets(), s.m.Assets().Companies())
|
|
}
|
|
tsx.SetNotify(t.Notify) // hint writes wake this task's planner (no-op for read tools)
|
|
return pick(tsx).Call(ctx, inner, nil)
|
|
}
|
|
|
|
// --- tools ---
|
|
|
|
func (s *Server) toolListTasks() actool.CoreTool {
|
|
return roTool("list_tasks",
|
|
"列出所有任务(id/描述/目标/状态/运行时长/父任务/LLM 配置),编排 agent 用它掌握全局、看哪些任务卡太久、各自用哪个 LLM。运行时长:运行中=创建→现在,终态=创建→最后活动(秒)。llm_profile:任务 planner/worker 用的配置名,(激活配置)=跟随全局激活。",
|
|
objSchema(map[string]any{}),
|
|
func(context.Context, json.RawMessage) (actool.Result, error) {
|
|
lastAct, _ := s.m.PG().LastActivityAll()
|
|
// id -> name to resolve each task's pinned LLM profile.
|
|
profName := map[int64]string{}
|
|
if profs, err := s.m.pg.ListProfiles(); err == nil {
|
|
for _, p := range profs {
|
|
profName[p.ID] = p.Name
|
|
}
|
|
}
|
|
out := make([]map[string]any, 0)
|
|
for _, t := range s.m.List() {
|
|
status := s.deriveTaskStatus(t)
|
|
end := lastAct[t.ExpID]
|
|
if live := s.engine.LastActivity(t.ID); live > end {
|
|
end = live
|
|
}
|
|
dur := int64(0)
|
|
if status == "running" {
|
|
dur = time.Now().Unix() - t.CreatedAt
|
|
} else if end > t.CreatedAt {
|
|
dur = end - t.CreatedAt
|
|
}
|
|
row := map[string]any{"id": t.ID, "description": t.Description, "goal": t.Goal, "status": status, "run_seconds": dur}
|
|
if t.ParentRef != "" {
|
|
row["parent_ref"] = t.ParentRef
|
|
}
|
|
if t.LLMProfileID == nil {
|
|
row["llm_profile"] = "(激活配置)"
|
|
} else if n, ok := profName[*t.LLMProfileID]; ok {
|
|
row["llm_profile"] = n
|
|
} else {
|
|
row["llm_profile"] = fmt.Sprintf("#%d(已删除)", *t.LLMProfileID)
|
|
}
|
|
out = append(out, row)
|
|
}
|
|
return jsonResult(out)
|
|
})
|
|
}
|
|
|
|
// toolListLLMProfiles lists the available LLM profiles (name/model/active) so an
|
|
// orchestration agent can pick one for spawn_task's llm_profile. Never leaks keys.
|
|
func (s *Server) toolListLLMProfiles() actool.CoreTool {
|
|
return roTool("list_llm_profiles",
|
|
"列出可用的 LLM 配置(profile):id、名称、模型、格式、是否为当前激活配置。用 id 给 spawn_task 的 llm_profile_id 参数指定子任务专属 LLM(如侦察用便宜模型、利用用强模型)。不含 API Key。",
|
|
objSchema(map[string]any{}),
|
|
func(context.Context, json.RawMessage) (actool.Result, error) {
|
|
profs, err := s.m.pg.ListProfiles()
|
|
if err != nil {
|
|
return actool.Errorf(err.Error()), nil
|
|
}
|
|
out := make([]map[string]any, 0, len(profs))
|
|
for _, p := range profs {
|
|
out = append(out, map[string]any{
|
|
"id": p.ID, "name": p.Name, "model": p.Model, "format": p.Format, "is_active": p.IsDefault,
|
|
})
|
|
}
|
|
return jsonResult(map[string]any{"profiles": out})
|
|
})
|
|
}
|
|
|
|
func (s *Server) toolSpawnTask() actool.CoreTool {
|
|
return wrTool("spawn_task",
|
|
"新建一个子任务并启动探索引擎,返回 task_id。用于把一件事(如一道题/一个目标)派成独立任务。parent_ref 可选:填当前编排关联的父任务 id 做父子关联。",
|
|
objSchema(map[string]any{
|
|
"description": strParam("任务描述(简短标题)"),
|
|
"goal": strParam("任务目标(要达成什么)"),
|
|
"parent_ref": strParam("可选:父任务 id(做父子关联)"),
|
|
"llm_profile_id": map[string]any{"type": "integer", "description": "可选:指定本子任务 planner/worker 用的 LLM 配置 id(见 list_llm_profiles);留空则继承父任务、再回退全局激活配置"},
|
|
"timeout_seconds": map[string]any{"type": "integer", "description": "可选:任务级超时(秒)。到点后触发优雅收尾并进入 timeout 终态;留空或 0 = 不限时"},
|
|
"seed_first_intent": map[string]any{"type": "boolean", "description": "可选:默认 true。true=创建即把「描述+目标」作为一条种子意图下发,worker 免等首轮 planner 直接开跑(CTF 等常一个 work 解决的场景推荐);false=走标准的先规划再执行"},
|
|
}, "description", "goal"),
|
|
func(_ context.Context, in json.RawMessage) (actool.Result, error) {
|
|
var a struct {
|
|
Description, Goal, ParentRef string
|
|
LLMProfileID json.RawMessage `json:"llm_profile_id"`
|
|
TimeoutSeconds int `json:"timeout_seconds"`
|
|
SeedFirstIntent *bool `json:"seed_first_intent"`
|
|
}
|
|
_ = json.Unmarshal(in, &a)
|
|
if strings.TrimSpace(a.Description) == "" {
|
|
a.Description = "未命名任务"
|
|
}
|
|
if strings.TrimSpace(a.Goal) == "" {
|
|
return actool.Errorf("goal 为必填"), nil
|
|
}
|
|
if a.TimeoutSeconds < 0 {
|
|
a.TimeoutSeconds = 0
|
|
}
|
|
// LLM profile resolution: explicit id > inherit parent's pin > active(nil).
|
|
var pin *int64
|
|
if id := parseProfileID(a.LLMProfileID); id > 0 {
|
|
if _, ok := s.loadProfileConfig(id); !ok {
|
|
return actool.Errorf(fmt.Sprintf("LLM 配置 #%d 不存在或未设置 API Key", id)), nil
|
|
}
|
|
pin = &id
|
|
} else if a.ParentRef != "" {
|
|
if pt, ok := s.m.Task(a.ParentRef); ok {
|
|
pin = pt.LLMProfileID
|
|
}
|
|
}
|
|
t, err := s.m.CreateTask(a.Description, a.Goal, pin, a.TimeoutSeconds)
|
|
if err != nil {
|
|
return actool.Errorf(err.Error()), nil
|
|
}
|
|
if a.ParentRef != "" {
|
|
t.ParentRef = a.ParentRef
|
|
if id, e := strconv.ParseInt(t.ID, 10, 64); e == nil {
|
|
_ = s.m.PG().SetParentRef(id, a.ParentRef)
|
|
}
|
|
}
|
|
s.seed(t, a.Description+" "+a.Goal) // seed 初始资产,喂给事件驱动 loop
|
|
// 种子意图(默认开启,仅显式 false 关闭):worker 免等首轮 planner 直接开跑。
|
|
if a.SeedFirstIntent == nil || *a.SeedFirstIntent {
|
|
s.seedFirstIntent(t)
|
|
}
|
|
s.createGoals(s.ctx, t, nil) // 目标分解(LLM;规则兜底)
|
|
s.engine.Run(s.ctx, t) // 启动该任务的探索引擎
|
|
return actool.Text(fmt.Sprintf("task created: %s", t.ID)), nil
|
|
})
|
|
}
|
|
|
|
func (s *Server) toolPauseTask() actool.CoreTool {
|
|
return wrTool("pause_task", "暂停指定任务(停止其 planner/worker 循环)。",
|
|
objSchema(map[string]any{"task_id": strParam("要暂停的任务 id")}, "task_id"),
|
|
func(_ context.Context, in json.RawMessage) (actool.Result, error) {
|
|
var a struct {
|
|
TaskID string `json:"task_id"`
|
|
}
|
|
_ = json.Unmarshal(in, &a)
|
|
if _, ok := s.m.Task(a.TaskID); !ok {
|
|
return actool.Errorf("task 不存在: " + a.TaskID), nil
|
|
}
|
|
s.engine.Pause(a.TaskID)
|
|
return actool.Text("task paused: " + a.TaskID), nil
|
|
})
|
|
}
|
|
|
|
func (s *Server) toolGetTaskGraph() actool.CoreTool {
|
|
return roTool("get_task_graph", "读指定任务的探索图总览(同 graph_overview:资产计数/frontier/发现/覆盖等),用 task_id 指定任务。",
|
|
objSchema(map[string]any{"task_id": strParam("任务 id")}, "task_id"),
|
|
func(ctx context.Context, in json.RawMessage) (actool.Result, error) {
|
|
return s.delegateToTask(ctx, in, (*agent.ToolSet).GraphOverviewTool)
|
|
})
|
|
}
|
|
|
|
func (s *Server) toolListTaskFindings() actool.CoreTool {
|
|
return roTool("list_task_findings", "读指定任务的确认漏洞(含 flag/PoC;每条带 id/task_id/intent_id/vulnclass/severity/摘要/状态),用 task_id 指定任务。",
|
|
objSchema(map[string]any{"task_id": strParam("任务 id")}, "task_id"),
|
|
func(ctx context.Context, in json.RawMessage) (actool.Result, error) {
|
|
return s.delegateToTask(ctx, in, (*agent.ToolSet).ListFindingsTool)
|
|
})
|
|
}
|
|
|
|
func (s *Server) toolAddHint() actool.CoreTool {
|
|
return wrTool("add_task_hint", "给指定任务注入战略提示(该任务的 planner 下轮生成意图时会读到)。\n"+
|
|
"★优先批量:多条提示放进 hints 数组一次提交(返回 ids 数组,与 hints 等长同序,失败项 id=0);单条则省略 hints 直接给顶层 text。",
|
|
objSchema(map[string]any{
|
|
"task_id": strParam("任务 id"),
|
|
"hints": map[string]any{"type": "array", "description": "【优先用这个】提示数组,每个元素字段同顶层(text/asset_ids)。", "items": map[string]any{"type": "object"}},
|
|
"text": strParam("[单条] 提示内容"),
|
|
"asset_ids": map[string]any{"type": "array", "items": map[string]any{"type": "integer"}, "description": "锚定的资产 id(可选,0/1/多个;该任务内的资产 id)"},
|
|
}, "task_id"),
|
|
func(ctx context.Context, in json.RawMessage) (actool.Result, error) {
|
|
return s.delegateToTask(ctx, in, (*agent.ToolSet).AddHintTool)
|
|
})
|
|
}
|
|
|
|
func (s *Server) toolGetWorkerTrace() actool.CoreTool {
|
|
return roTool("get_task_worker_trace",
|
|
"看指定任务里某个 work(意图)的执行过程:get_task_worker_trace(task_id, intent_id) 看步骤摘要;再带 step_ids=[...] 取那几步完整内容(一次≤5)。",
|
|
objSchema(map[string]any{
|
|
"task_id": strParam("任务 id"),
|
|
"intent_id": map[string]any{"type": "integer", "description": "意图 id(该任务里的 work)"},
|
|
"step_ids": map[string]any{"type": "array", "items": map[string]any{"type": "integer"}, "description": "可选:要取完整内容的步骤 id(≤5)"},
|
|
}, "task_id", "intent_id"),
|
|
func(ctx context.Context, in json.RawMessage) (actool.Result, error) {
|
|
return s.delegateToTask(ctx, in, (*agent.ToolSet).GetWorkerTraceTool)
|
|
})
|
|
}
|
|
|
|
func (s *Server) toolListWorkerTraces() actool.CoreTool {
|
|
return roTool("list_task_worker_traces", "列出指定任务里跑过哪些 work(意图) + 各自步数,用于发现哪些 work 值得翻看(再用 get_task_worker_trace)。",
|
|
objSchema(map[string]any{"task_id": strParam("任务 id")}, "task_id"),
|
|
func(ctx context.Context, in json.RawMessage) (actool.Result, error) {
|
|
return s.delegateToTask(ctx, in, (*agent.ToolSet).ListWorkerTracesTool)
|
|
})
|
|
}
|
|
|
|
func (s *Server) toolSearchWorkerTraces() actool.CoreTool {
|
|
return roTool("search_task_worker_traces", "在指定任务里按关键字搜索所有 work 的执行过程(返回命中步骤摘要 + intent_id)。",
|
|
objSchema(map[string]any{"task_id": strParam("任务 id"), "q": strParam("搜索关键字")}, "task_id", "q"),
|
|
func(ctx context.Context, in json.RawMessage) (actool.Result, error) {
|
|
return s.delegateToTask(ctx, in, (*agent.ToolSet).SearchWorkerTracesTool)
|
|
})
|
|
}
|
|
|
|
// deriveTaskStatus mirrors listTasks' status derivation for the list_tasks tool.
|
|
func (s *Server) deriveTaskStatus(t *Task) string {
|
|
switch {
|
|
case isTerminalStatus(t.Status):
|
|
return t.Status
|
|
case t.Paused || s.engine.IsPaused(t.ID):
|
|
return "paused"
|
|
case s.engine.Ready() && s.engine.Started(t.ID):
|
|
return "running"
|
|
}
|
|
return "created"
|
|
}
|
|
|
|
// orchestrationToolSeeds seeds the cross-task tools into the tools table so they
|
|
// are bindable per-agent (default: bound to nobody — opt-in for orchestration
|
|
// agents). First-insert only, like the traffic seeds.
|
|
func (s *Server) seedOrchestrationTools() {
|
|
// task-op + platform tools default-bind to the built-in Auto agent (它天生用来
|
|
// 操作平台)。SeedTool 首插入生效;老库已 seed 的行由 seedAutoDefaultBindings 补绑。
|
|
autoAgents, _ := json.Marshal([]string{"auto"})
|
|
for _, t := range s.orchestrationTools() {
|
|
schema, _ := json.Marshal(t.InputSchema())
|
|
_ = s.m.PG().SeedTool(t.Name(), t.Description(), schema, autoAgents)
|
|
}
|
|
for _, t := range s.platformTools() {
|
|
schema, _ := json.Marshal(t.InputSchema())
|
|
_ = s.m.PG().SeedTool(t.Name(), t.Description(), schema, autoAgents)
|
|
}
|
|
s.refreshBuiltinToolSchemas()
|
|
s.seedAutoDefaultBindings()
|
|
s.seedPlannerDefaultBindings()
|
|
s.seedAutoReportFindingBinding()
|
|
// 注:pentest 的默认工具绑定无需迁移——BuiltinToolSeeds 在全新初始化时就把
|
|
// list_assets/insert_assets/report_finding/list_findings/list_companies 连同
|
|
// pentest 一起 seed 好了(项目尚无旧库,不做迁移)。
|
|
}
|
|
|
|
// refreshBuiltinToolSchemas propagates code schema/description changes on the
|
|
// orchestration + platform tools into already-seeded rows ONCE per version flag —
|
|
// SeedTool is first-insert-only, so a new param (e.g. spawn_task 的 llm_profile) never
|
|
// reaches an old DB otherwise. Preserves each tool's agent binding + enabled flag.
|
|
// Bump the flag whenever these tools' schemas/descriptions change in code.
|
|
func (s *Server) refreshBuiltinToolSchemas() {
|
|
const flag = "tool_schema_refresh_v5_spawn_seed_intent"
|
|
if v, _, _ := s.m.pg.GetSetting(flag); v == "true" {
|
|
return
|
|
}
|
|
tools := append(s.orchestrationTools(), s.platformTools()...)
|
|
for _, t := range tools {
|
|
schema, _ := json.Marshal(t.InputSchema())
|
|
if err := s.m.pg.RefreshToolDefaults(t.Name(), t.Description(), schema); err != nil {
|
|
log.Printf("[tools] refresh %s schema failed: %v", t.Name(), err)
|
|
}
|
|
}
|
|
// 同时把 planner 的 goal_met 描述刷成代码默认:旧库 seed 的描述带“结束本轮规划”的
|
|
// 误导,会让 planner 把 goal_met 当成“结束空轮”的手段、刚开跑就误判整个任务完成。
|
|
for _, sd := range agent.BuiltinToolSeeds() {
|
|
if sd.Key != "goal_met" {
|
|
continue
|
|
}
|
|
schema, _ := json.Marshal(sd.Schema)
|
|
if err := s.m.pg.RefreshToolDefaults(sd.Key, sd.Desc, schema); err != nil {
|
|
log.Printf("[tools] refresh %s desc failed: %v", sd.Key, err)
|
|
}
|
|
}
|
|
_ = s.m.pg.SetSetting(flag, "true")
|
|
log.Printf("[tools] 已刷新 orchestration/platform 工具 schema 到代码默认(一次性)")
|
|
}
|
|
|
|
// seedAutoReportFindingBinding adds "auto" to report_finding's binding ONCE so
|
|
// conversation-context agents can call it without requiring an intent_id.
|
|
func (s *Server) seedAutoReportFindingBinding() {
|
|
const flag = "auto_report_finding_v1"
|
|
if v, _, _ := s.m.pg.GetSetting(flag); v == "true" {
|
|
return
|
|
}
|
|
if err := s.m.pg.AddAgentToToolBinding("auto", []string{"report_finding"}); err != nil {
|
|
log.Printf("[auto] report_finding 默认绑定失败: %v", err)
|
|
return
|
|
}
|
|
_ = s.m.pg.SetSetting(flag, "true")
|
|
}
|
|
|
|
// seedPlannerDefaultBindings adds "planner" to report_finding's binding ONCE
|
|
// (guarded by a settings flag), so existing DBs — whose report_finding row was
|
|
// seeded as worker-only — also let the planner record findings. Fresh DBs already
|
|
// get it via PlannerTools(); this only backfills without overriding a user unbind.
|
|
func (s *Server) seedPlannerDefaultBindings() {
|
|
const flag = "planner_report_finding_v1"
|
|
if v, _, _ := s.m.pg.GetSetting(flag); v == "true" {
|
|
return
|
|
}
|
|
if err := s.m.pg.AddAgentToToolBinding("planner", []string{"report_finding"}); err != nil {
|
|
log.Printf("[planner] report_finding 默认绑定失败: %v", err)
|
|
return
|
|
}
|
|
_ = s.m.pg.SetSetting(flag, "true")
|
|
}
|
|
|
|
// seedAutoDefaultBindings adds "auto" to the task-op + platform tools' bindings
|
|
// ONCE (guarded by a settings flag), so existing DBs whose tool rows were seeded
|
|
// before Auto existed still give Auto its default toolset — without re-adding it
|
|
// after a user deliberately unbinds.
|
|
func (s *Server) seedAutoDefaultBindings() {
|
|
const flag = "auto_default_bindings_v3" // v3: 替换旧资产工具名,加入 insert_assets/add_company_scope
|
|
if v, _, _ := s.m.pg.GetSetting(flag); v == "true" {
|
|
return
|
|
}
|
|
keys := make([]string, 0, len(platformToolKeys)+12)
|
|
for _, t := range s.orchestrationTools() {
|
|
keys = append(keys, t.Name())
|
|
}
|
|
keys = append(keys, platformToolKeys...)
|
|
// 资产工具:Auto 操作平台常要看/登记资产、管理公司范围。
|
|
keys = append(keys, "insert_assets", "add_company_scope", "list_assets")
|
|
if err := s.m.pg.AddAgentToToolBinding("auto", keys); err != nil {
|
|
log.Printf("[auto] 默认绑定失败: %v", err)
|
|
return
|
|
}
|
|
_ = s.m.pg.SetSetting(flag, "true")
|
|
}
|