feat: 增加 Agent 流量绑定开关并补齐证据交接
This commit is contained in:
1 parent
ec4ef35fff
commit
a3bb2c7959
21 files changed
+810
-62
No files matched your search
+6
-4
@@ -12,10 +12,11 @@ import (
|
||||
// UnlockSkill unlocks a named skill's MCPs — hosts call it to rebuild the unlock set
|
||||
// from history on a resumed session (design doc C2).
|
||||
type DeferredInfo struct {
|
||||
Deferred []string // all MCP tool names (schema withheld)
|
||||
GlobalNames []string // MCP names to list in the system-prompt block
|
||||
Unlock *actool.UnlockSet // shared call-gate; nil when no MCP tools
|
||||
UnlockSkill func(skillName string)
|
||||
FindingGuidance string // derived from the final permitted tools, including DB overrides
|
||||
Deferred []string // all MCP tool names (schema withheld)
|
||||
GlobalNames []string // MCP names to list in the system-prompt block
|
||||
Unlock *actool.UnlockSet // shared call-gate; nil when no MCP tools
|
||||
UnlockSkill func(skillName string)
|
||||
}
|
||||
|
||||
// ToolAugment, if set, returns the EXTRA tools an agent should see beyond its
|
||||
@@ -55,5 +56,6 @@ func AugmentTools(ctx context.Context, agentKey string, base []actool.CoreTool)
|
||||
if ToolResolve != nil {
|
||||
out = ToolResolve(ctx, agentKey, out)
|
||||
}
|
||||
out, def.FindingGuidance = findingWorkflowTools(out)
|
||||
return out, def, cleanup
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
// skill loads. Returns a single plain segment + boundary 0 when there is no global
|
||||
// block to add.
|
||||
func deferredSystem(sysText string, def DeferredInfo) (system []string, boundary int) {
|
||||
sysText += def.FindingGuidance
|
||||
block := actool.RenderDeferredToolsBlock(def.GlobalNames)
|
||||
if block == "" {
|
||||
return []string{sysText}, 0
|
||||
|
||||
@@ -51,6 +51,9 @@ func TestReportFindingOptionalTrafficWithoutCapture(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestReportFindingAtomicContract(t *testing.T) {
|
||||
old := FindingTrafficBindingEnabled
|
||||
FindingTrafficBindingEnabled = func() bool { return true }
|
||||
t.Cleanup(func() { FindingTrafficBindingEnabled = old })
|
||||
d := testDB(t)
|
||||
defer d.Close()
|
||||
task, err := d.CreateTask("report contract", "fixture", nil, 0, 0)
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/Autumn-27/artex/db"
|
||||
actool "github.com/Autumn-27/norma/tool"
|
||||
)
|
||||
|
||||
const findingIDGuidance = "\n\n**漏洞编号约定**:finding_id 是独立漏洞记录 ID;finding_node_id 是探索节点 ID。list_findings / list_task_findings / node_detail / get_task_node_detail 的 id 保留为探索节点 ID,应从同一返回的 finding_id 读取独立编号。get_finding_traffic / bind_finding_traffic 用独立 finding_id。旧 update_finding_report 的 finding_id 参数仍传 finding_node_id。不要把 report_finding 第一行的数字用于证据工具,也不要遇到编号错误后猜测其他数字。"
|
||||
|
||||
// The server supplies the persisted setting. A missing setting/host is off.
|
||||
// Consulted at assembly and again on writes so an already-running session
|
||||
// cannot keep binding after the user switches the feature off.
|
||||
var FindingTrafficBindingEnabled func() bool
|
||||
|
||||
func findingTrafficBindingEnabled() bool {
|
||||
return FindingTrafficBindingEnabled != nil && FindingTrafficBindingEnabled()
|
||||
}
|
||||
|
||||
const findingTrafficDisabled = "Agent 自动绑定流量已关闭;请在系统设置开启后重试,或使用页面人工绑定。未登记本次携带流量的漏洞。无流量上报可省略 traffic_refs / evidence_hint_id。"
|
||||
|
||||
// Applied after ToolResolve: user descriptions and prompts remain intact, while
|
||||
// all actual reporters (including Planner and custom chat agents) see the same
|
||||
// API contract. Disabled/unbound tools are never reintroduced here.
|
||||
func findingWorkflowTools(tools []actool.CoreTool) ([]actool.CoreTool, string) {
|
||||
if !findingTrafficBindingEnabled() {
|
||||
out := make([]actool.CoreTool, 0, len(tools))
|
||||
for _, tool := range tools {
|
||||
if tool.Name() == "bind_finding_traffic" {
|
||||
continue
|
||||
}
|
||||
switch tool.Name() {
|
||||
case "report_finding", "add_hint", "add_task_hint":
|
||||
// Work on a copy: toggling back on must restore the original schema.
|
||||
raw, _ := json.Marshal(tool.InputSchema())
|
||||
var schema map[string]any
|
||||
if json.Unmarshal(raw, &schema) == nil {
|
||||
stripTrafficParameters(schema)
|
||||
tool = DecorateTool(tool, tool.Description(), schema)
|
||||
}
|
||||
}
|
||||
out = append(out, tool)
|
||||
}
|
||||
return out, ""
|
||||
}
|
||||
out := append([]actool.CoreTool(nil), tools...)
|
||||
has := map[string]bool{}
|
||||
for i, tool := range out {
|
||||
has[tool.Name()] = true
|
||||
note := ""
|
||||
switch tool.Name() {
|
||||
case "report_finding":
|
||||
note = findingTrafficGuidance + "\n已有对应的、经核实的 HTTP 流量时,随本次上报提交 traffic_refs。代为上报时保留交接中的引用,或传 evidence_hint_id 读取本任务指定 hint 的结构化 traffic_refs;不要只复制文字结论。无数据包仍可上报。返回 finding_id 与 finding_node_id 分别表示独立记录和探索节点。"
|
||||
case "add_hint", "add_task_hint":
|
||||
note = "\n交接已确认漏洞时,在对应提示的 traffic_refs 中保留已核实流量的 ID、用途、说明和顺序(单条放顶层,批量放对应 hints 元素),并在 text 中说明它证明的具体漏洞。调用方不能只交接文字而丢弃已有流量引用。未核实的候选不能作为证据传递。"
|
||||
case "get_finding_traffic", "bind_finding_traffic", "list_findings", "list_task_findings", "node_detail", "get_task_node_detail", "update_finding_report":
|
||||
note = findingIDGuidance
|
||||
}
|
||||
if note != "" {
|
||||
out[i] = DecorateTool(tool, tool.Description()+note, tool.InputSchema())
|
||||
}
|
||||
}
|
||||
guidance := ""
|
||||
if has["report_finding"] || has["add_task_hint"] || has["add_hint"] {
|
||||
guidance = findingTrafficGuidance + "\n**代上报与交接**:有已核实 HTTP 证据时,报告保存应同时携带 traffic_refs;用 add_hint / add_task_hint 交接时保留结构化引用。收到交接提示后用 evidence_hint_id 明确选择本任务对应 hint,不能混用其他漏洞的证据。若只收到文字,先查已有执行记录或请原执行者交接证据,不要仅为补包重新探测。只有拿到真实 ID 才绑定,TCP 或无包时正常上报。"
|
||||
if has["traffic_search"] && has["traffic_get"] {
|
||||
guidance += "\n可用 traffic_search 筛选,再用 traffic_get 逐条核实请求/响应及真实 ID。"
|
||||
}
|
||||
if has["add_task_hint"] && !has["add_hint"] {
|
||||
guidance += "\n平台对话没有任务上下文时,不直接调用 report_finding;通过 add_task_hint 向已有对应任务交接,由任务 Agent 登记,并用 list_task_findings 核对结果。"
|
||||
}
|
||||
if has["prove_goal"] || has["goal_met"] {
|
||||
guidance += "\n判定目标完成前,先完成本次已有证据的上报/交接。不要在证据交接尚未完成时仅因文字漏洞已登记就结束任务、取消 Worker;无包不要求等待或强行抓包。"
|
||||
}
|
||||
if has["bind_finding_traffic"] {
|
||||
guidance += "\n若漏洞已登记而遗漏了已有的真实流量,用 bind_finding_traffic 补绑,不重复创建漏洞。"
|
||||
}
|
||||
}
|
||||
if guidance != "" || has["get_finding_traffic"] || has["update_finding_report"] {
|
||||
guidance += findingIDGuidance
|
||||
}
|
||||
return out, guidance
|
||||
}
|
||||
|
||||
func stripTrafficParameters(schema map[string]any) {
|
||||
props, _ := schema["properties"].(map[string]any)
|
||||
delete(props, "traffic_refs")
|
||||
delete(props, "evidence_hint_id")
|
||||
if required, ok := schema["required"].([]any); ok {
|
||||
kept := required[:0]
|
||||
for _, key := range required {
|
||||
if key != "traffic_refs" && key != "evidence_hint_id" {
|
||||
kept = append(kept, key)
|
||||
}
|
||||
}
|
||||
schema["required"] = kept
|
||||
}
|
||||
if hints, ok := props["hints"].(map[string]any); ok {
|
||||
if items, ok := hints["items"].(map[string]any); ok {
|
||||
stripTrafficParameters(items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HintTrafficSchema is shared by the task-local and cross-task hint tools.
|
||||
func HintTrafficSchema() map[string]any {
|
||||
return map[string]any{"type": "array", "description": "可选:已核实且对应本提示中具体漏洞的流量引用,保留顺序;交接后 report_finding 可传 evidence_hint_id 携带这些引用。", "items": obj(map[string]any{"traffic_id": str("真实流量 ID"), "role": str("baseline / proof / verification / supporting"), "note": str("该流量支持什么结论")}, "traffic_id")}
|
||||
}
|
||||
|
||||
func (t *ToolSet) findingRefsFromHint(hintID int64, explicit []db.TrafficRef) ([]db.TrafficRef, error) {
|
||||
if hintID <= 0 {
|
||||
return db.NormalizeTrafficRefs(explicit)
|
||||
}
|
||||
n, err := t.ts.GetNode(hintID) // local store only: inherited hints cannot supply evidence
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if n == nil || n.Kind != db.KindHint {
|
||||
return nil, fmt.Errorf("evidence_hint_id=%d 必须是本任务的提示节点(继承提示不可直接用于绑定)", hintID)
|
||||
}
|
||||
var payload struct {
|
||||
Refs []db.TrafficRef `json:"traffic_refs"`
|
||||
}
|
||||
if err := json.Unmarshal(n.Payload, &payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return db.NormalizeTrafficRefs(append(append([]db.TrafficRef{}, explicit...), payload.Refs...))
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
actool "github.com/Autumn-27/norma/tool"
|
||||
)
|
||||
|
||||
func TestFindingWorkflowSharedAssemblyAndSwitch(t *testing.T) {
|
||||
oldPolicy, oldResolve, oldAugment := FindingTrafficBindingEnabled, ToolResolve, ToolAugment
|
||||
t.Cleanup(func() { FindingTrafficBindingEnabled, ToolResolve, ToolAugment = oldPolicy, oldResolve, oldAugment })
|
||||
ToolAugment = nil
|
||||
on := false
|
||||
FindingTrafficBindingEnabled = func() bool { return on }
|
||||
ts := NewToolSet(nil, "fixture")
|
||||
ToolResolve = func(_ context.Context, _ string, base []actool.CoreTool) []actool.CoreTool {
|
||||
out := make([]actool.CoreTool, len(base))
|
||||
for i, tool := range base {
|
||||
out[i] = DecorateTool(tool, "CUSTOM DESCRIPTION", tool.InputSchema())
|
||||
}
|
||||
return out
|
||||
}
|
||||
for _, role := range []string{"worker", "planner", "mainagent", "auto", "pentest", "custom-agent"} {
|
||||
for _, enabled := range []bool{false, true, false} {
|
||||
on = enabled
|
||||
out, def, cleanup := AugmentTools(t.Context(), role, []actool.CoreTool{ts.addFinding(), ts.addHint()})
|
||||
cleanup()
|
||||
system, _ := deferredSystem("USER CUSTOM PROMPT", def)
|
||||
if !strings.HasPrefix(system[0], "USER CUSTOM PROMPT") {
|
||||
t.Fatal("custom prompt replaced")
|
||||
}
|
||||
if strings.Contains(system[0], "traffic_refs") != on {
|
||||
t.Fatalf("%s: guidance ignored switch: %v", role, on)
|
||||
}
|
||||
if on && (!strings.Contains(system[0], "TCP") || !strings.Contains(system[0], "evidence_hint_id")) {
|
||||
t.Fatal("missing optional/handoff contract")
|
||||
}
|
||||
for _, tool := range out {
|
||||
if !strings.HasPrefix(tool.Description(), "CUSTOM DESCRIPTION") {
|
||||
t.Fatal("custom description replaced")
|
||||
}
|
||||
raw, _ := json.Marshal(tool.InputSchema())
|
||||
var schema map[string]any
|
||||
json.Unmarshal(raw, &schema)
|
||||
props := schema["properties"].(map[string]any)
|
||||
if (props["traffic_refs"] != nil) != on {
|
||||
t.Fatalf("%s: binding schema ignored switch", role)
|
||||
}
|
||||
if tool.Name() == "add_hint" {
|
||||
nested := props["hints"].(map[string]any)["items"].(map[string]any)["properties"].(map[string]any)
|
||||
if (nested["traffic_refs"] != nil) != on {
|
||||
t.Fatal("nested hint schema ignored switch")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
on = true
|
||||
ToolResolve = func(context.Context, string, []actool.CoreTool) []actool.CoreTool { return nil }
|
||||
out, def, cleanup := AugmentTools(t.Context(), "planner", []actool.CoreTool{ts.addFinding()})
|
||||
defer cleanup()
|
||||
if len(out) != 0 || def.FindingGuidance != "" {
|
||||
t.Fatal("reintroduced disabled tool or its guidance")
|
||||
}
|
||||
}
|
||||
@@ -57,8 +57,8 @@ func TestRenderSystemOverrideAndFallback(t *testing.T) {
|
||||
if !strings.Contains(recording, "traffic_search") {
|
||||
t.Fatalf("worker while recording should inject trafficTool: %q", recording)
|
||||
}
|
||||
if !strings.Contains(recording, "traffic_refs") || !strings.Contains(recording, "TCP") {
|
||||
t.Fatalf("custom worker prompt lost optional finding evidence guidance: %q", recording)
|
||||
if strings.Contains(recording, "traffic_refs") {
|
||||
t.Fatalf("worker bypassed shared optional evidence policy: %q", recording)
|
||||
}
|
||||
if !strings.Contains(recording, "中间产物输出规约") {
|
||||
t.Fatalf("worker missing artifact tail: %q", recording)
|
||||
|
||||
+59
-13
@@ -399,7 +399,11 @@ func (t *ToolSet) graphOverviewData() map[string]any {
|
||||
for _, h := range hints {
|
||||
var p map[string]any
|
||||
_ = json.Unmarshal(h.Payload, &p)
|
||||
hsum = append(hsum, map[string]any{"id": h.ID, "state": h.State, "text": p["text"]})
|
||||
hint := map[string]any{"id": h.ID, "state": h.State, "text": p["text"]}
|
||||
if findingTrafficBindingEnabled() && p["traffic_refs"] != nil {
|
||||
hint["traffic_refs"] = p["traffic_refs"]
|
||||
}
|
||||
hsum = append(hsum, hint)
|
||||
}
|
||||
out["hints"] = hsum
|
||||
// lineage from the exploration edges: an intent's parents (what it
|
||||
@@ -1069,6 +1073,9 @@ func (t *ToolSet) listFindings() actool.CoreTool {
|
||||
obj(map[string]any{}),
|
||||
func(context.Context, json.RawMessage) (actool.Result, error) {
|
||||
f, _ := t.ts.ListByKindWithSources(db.KindFinding, 500)
|
||||
if err := t.ts.PopulateFindingTrafficIDs(f); err != nil {
|
||||
return actool.Errorf(err.Error()), nil
|
||||
}
|
||||
intentOf, _ := t.ts.FindingIntentsWithSources() // finding id -> 产生它的 intent id
|
||||
taskID := t.taskID
|
||||
if taskID <= 0 {
|
||||
@@ -1077,6 +1084,9 @@ func (t *ToolSet) listFindings() actool.CoreTool {
|
||||
out := make([]map[string]any, 0, len(f))
|
||||
for _, n := range f {
|
||||
m := compactFinding(n)
|
||||
if n.FindingID > 0 {
|
||||
m["finding_id"], m["finding_node_id"], m["traffic_count"] = n.FindingID, n.ID, n.TrafficCount
|
||||
}
|
||||
if n.Inherited {
|
||||
m["task_id"] = n.SourceTaskID
|
||||
} else {
|
||||
@@ -1168,7 +1178,10 @@ func (t *ToolSet) nodeDetail() actool.CoreTool {
|
||||
if n == nil {
|
||||
return actool.Errorf(fmt.Sprintf("未找到探索节点 %d。若你想查的是资产,请用 list_assets / asset_neighbors(资产与探索节点是不同的 id 空间,资产 id 不能传给 node_detail)。", id)), nil
|
||||
}
|
||||
return jsonResult(n) // full payload incl. detail / evidence
|
||||
if err := t.ts.PopulateFindingTrafficIDs([]*db.Node{n}); err != nil {
|
||||
return actool.Errorf(err.Error()), nil
|
||||
}
|
||||
return jsonResult(n) // full payload incl. detail / evidence, plus explicit finding IDs
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1359,10 +1372,11 @@ func (t *ToolSet) goalMet() actool.CoreTool {
|
||||
// --- worker write tools ---
|
||||
|
||||
func (t *ToolSet) addFinding() actool.CoreTool {
|
||||
return writeTool("report_finding", "记录确认的漏洞。可选 traffic_refs 绑定多条真实 HTTP 请求/响应:先用 traffic_search / traffic_get 核对 ID;提交的流量必须全部绑定成功。TCP 等非 HTTP 漏洞、未采集或无确切匹配记录时可省略 traffic_refs 或传空数组,仍用 evidence 提供命令输出、日志等可验证证据,建议说明未绑定原因。任务上下文传当前 intent_id。", obj(map[string]any{
|
||||
return writeTool("report_finding", "记录确认的漏洞,用 evidence 提供命令输出、日志等可验证证据。任务上下文传当前 intent_id。返回的 finding_id 是独立漏洞记录 ID,finding_node_id 是探索节点 ID(第一行保留该节点编号)。", obj(map[string]any{
|
||||
"vulnclass": str("漏洞类别"), "name": str("漏洞名称"), "severity": str("critical|high|medium|low"), "summary": str("发现摘要"),
|
||||
"intent_id": idp("当前任务的意图 id"), "asset_ids": map[string]any{"type": "array", "items": map[string]any{"type": "integer"}, "description": "受影响资产 id"},
|
||||
"evidence": str("证据/PoC 文本"),
|
||||
"evidence": str("证据/PoC 文本"),
|
||||
"evidence_hint_id": idp("可选:本任务中对应此漏洞的提示节点 ID,自动携带其结构化 traffic_refs;不能引用继承提示或其他漏洞的提示"),
|
||||
"traffic_refs": map[string]any{"type": "array", "description": "可选;HTTP/HTTPS 漏洞先检索并逐条核实请求/响应确实支持漏洞结论,再按复现顺序填写真实 ID。TCP 等非 HTTP 漏洞、未采集或找不到确切记录时省略或传 [],不阻止上报;可在 evidence 说明原因并提供其他可验证证据。不要猜测 ID、按域名/时间推定关联或仅为补包重复探测。用途 baseline 正常对照 / proof 漏洞证明 / verification 补充验证 / supporting 辅助证据。",
|
||||
"items": obj(map[string]any{"traffic_id": str("traffic_search 返回的真实流量 ID"), "role": map[string]any{"type": "string", "enum": []string{"baseline", "proof", "verification", "supporting"}}, "note": str("该流量如何支持漏洞结论")}, "traffic_id")},
|
||||
}, "vulnclass", "severity", "summary"), func(ctx context.Context, in json.RawMessage) (actool.Result, error) {
|
||||
@@ -1371,14 +1385,21 @@ func (t *ToolSet) addFinding() actool.CoreTool {
|
||||
IntentID json.RawMessage `json:"intent_id"`
|
||||
AssetIDs []json.RawMessage `json:"asset_ids"`
|
||||
TrafficRefs []db.TrafficRef `json:"traffic_refs"`
|
||||
EvidenceHintID json.RawMessage `json:"evidence_hint_id"`
|
||||
}
|
||||
if err := json.Unmarshal(in, &a); err != nil {
|
||||
return actool.Errorf(err.Error()), nil
|
||||
}
|
||||
if t.ts == nil {
|
||||
return actool.Errorf("report_finding 需要任务上下文(exploration store 未初始化)"), nil
|
||||
return actool.Errorf("report_finding 需要任务上下文;平台对话请通过 add_task_hint 向对应任务交接漏洞,并在提示中携带已有的 traffic_refs,由任务 Agent 登记。已登记漏洞可用 bind_finding_traffic 补绑。"), nil
|
||||
}
|
||||
refs, err := db.NormalizeTrafficRefs(a.TrafficRefs)
|
||||
if !findingTrafficBindingEnabled() && (len(a.TrafficRefs) > 0 || pid(a.EvidenceHintID) > 0) {
|
||||
return actool.Errorf(findingTrafficDisabled), nil
|
||||
}
|
||||
if len(a.EvidenceHintID) > 0 && pid(a.EvidenceHintID) <= 0 {
|
||||
return actool.Errorf("evidence_hint_id 必须为有效的提示节点 ID;无交接提示时省略"), nil
|
||||
}
|
||||
refs, err := t.findingRefsFromHint(pid(a.EvidenceHintID), a.TrafficRefs)
|
||||
if err != nil {
|
||||
return actool.Errorf(err.Error()), nil
|
||||
}
|
||||
@@ -1409,7 +1430,19 @@ func (t *ToolSet) addFinding() actool.CoreTool {
|
||||
recorded.Traffic.Bindings[i].Snapshot.ReqHead = ""
|
||||
recorded.Traffic.Bindings[i].Snapshot.RespHead = ""
|
||||
}
|
||||
raw, _ := json.Marshal(recorded)
|
||||
result := struct {
|
||||
*db.RecordedFinding
|
||||
EvidenceStatus string `json:"evidence_status"`
|
||||
EvidenceNote string `json:"evidence_note,omitempty"`
|
||||
}{RecordedFinding: recorded, EvidenceStatus: "bound"}
|
||||
if len(recorded.Traffic.Bindings) == 0 {
|
||||
result.EvidenceStatus = "not_bound"
|
||||
result.EvidenceNote = "漏洞已保存,未绑定流量。TCP/无包情形可正常继续;若已有核实的 HTTP 流量,请用可用的 bind_finding_traffic 或漏洞页面补绑,再完成证据交接。不要重复创建漏洞。"
|
||||
if !findingTrafficBindingEnabled() {
|
||||
result.EvidenceNote = "漏洞已保存。Agent 自动绑定流量已关闭,可在页面人工关联流量。"
|
||||
}
|
||||
}
|
||||
raw, _ := json.Marshal(result)
|
||||
return actool.Text(fmt.Sprintf("finding recorded: %d\n%s", recorded.NodeID, raw)), nil
|
||||
})
|
||||
}
|
||||
@@ -1523,12 +1556,16 @@ func (t *ToolSet) recordFact() actool.CoreTool {
|
||||
}
|
||||
|
||||
type hintItem struct {
|
||||
Text string `json:"text"`
|
||||
AssetIDs []json.RawMessage `json:"asset_ids"`
|
||||
Text string `json:"text"`
|
||||
AssetIDs []json.RawMessage `json:"asset_ids"`
|
||||
TrafficRefs []db.TrafficRef `json:"traffic_refs"`
|
||||
}
|
||||
|
||||
// addOneHint 挂一条 hint 节点(active/human)到探索图,可锚定资产,返回 id。
|
||||
func (t *ToolSet) addOneHint(it hintItem) (int64, error) {
|
||||
if len(it.TrafficRefs) > 0 && !findingTrafficBindingEnabled() {
|
||||
return 0, fmt.Errorf("Agent 自动绑定流量已关闭,未保存携带 traffic_refs 的提示;可在系统设置开启,或仅交接文字")
|
||||
}
|
||||
if strings.TrimSpace(it.Text) == "" {
|
||||
return 0, fmt.Errorf("text 不能为空")
|
||||
}
|
||||
@@ -1538,7 +1575,15 @@ func (t *ToolSet) addOneHint(it hintItem) (int64, error) {
|
||||
anchors = append(anchors, tid)
|
||||
}
|
||||
}
|
||||
id, err := t.ts.AddNode(db.KindHint, map[string]any{"text": it.Text}, 0, "active", "human", anchors)
|
||||
refs, err := db.NormalizeTrafficRefs(it.TrafficRefs)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
payload := map[string]any{"text": it.Text}
|
||||
if len(refs) > 0 {
|
||||
payload["traffic_refs"] = refs
|
||||
}
|
||||
id, err := t.ts.AddNode(db.KindHint, payload, 0, "active", "human", anchors)
|
||||
if err == nil && t.notify != nil {
|
||||
t.notify() // wake the planner so the new hint is read promptly (debounced)
|
||||
}
|
||||
@@ -1724,9 +1769,10 @@ func (t *ToolSet) addHint() actool.CoreTool {
|
||||
return writeTool("add_hint", "把人类/主 agent 的战略提示挂到探索图,规划者下次生成意图时会读到它。\n"+
|
||||
"★优先批量:多条提示放进 hints 数组一次提交(比逐条调用省往返)。返回 ids 数组,与 hints 等长同序(失败项 id=0,详情见 errors)。单条则省略 hints 直接给顶层 text。",
|
||||
obj(map[string]any{
|
||||
"hints": map[string]any{"type": "array", "description": "【优先用这个】要新增的提示数组,按顺序处理。每个元素字段同下方顶层字段(text/asset_ids)。返回 ids 与本数组等长、同序。", "items": map[string]any{"type": "object"}},
|
||||
"text": str("[单条] 提示内容,如'重点挖认证后接口'"),
|
||||
"asset_ids": map[string]any{"type": "array", "items": map[string]any{"type": "integer"}, "description": "锚定的资产 id(可选,0/1/多个)"},
|
||||
"hints": map[string]any{"type": "array", "description": "【优先用这个】要新增的提示数组,按顺序处理。每个元素字段同下方顶层字段(text/asset_ids/traffic_refs)。返回 ids 与本数组等长、同序。", "items": obj(map[string]any{"text": str("提示内容"), "asset_ids": map[string]any{"type": "array", "items": map[string]any{"type": "integer"}}, "traffic_refs": HintTrafficSchema()})},
|
||||
"text": str("[单条] 提示内容,如'重点挖认证后接口'"),
|
||||
"traffic_refs": HintTrafficSchema(),
|
||||
"asset_ids": map[string]any{"type": "array", "items": map[string]any{"type": "integer"}, "description": "锚定的资产 id(可选,0/1/多个)"},
|
||||
}),
|
||||
func(_ context.Context, in json.RawMessage) (actool.Result, error) {
|
||||
var a struct {
|
||||
|
||||
+2
-1
@@ -258,7 +258,8 @@ func workerSystem(proxyAddr, caCert, dataDir, runDir string) string {
|
||||
body := renderSystem("worker", workerDefaultTmpl, WorkerVars{ProxyAddr: proxyAddr, DataDir: dataDir, Now: nowStr()})
|
||||
// caCert is present only when the recording MITM is on, which is exactly when
|
||||
// the traffic_* tools are registered — so it gates the traffic-tool note.
|
||||
return body + workerTrafficBlock(caCert != "") + findingTrafficGuidance + workerArtifactSpec(runDir)
|
||||
// Optional finding guidance is added for every role after tool resolution.
|
||||
return body + workerTrafficBlock(caCert != "") + workerArtifactSpec(runDir)
|
||||
}
|
||||
|
||||
// renderIntentTask formats the claimed intent for the worker's launch USER message:
|
||||
|
||||
Reference in new issue
Block a user