Merge PR #102: feat: 支持漏洞关联多条流量证据

冲突解决:
- db/schema.sql: 复测表(#103)与流量证据表(#102)均为追加,两者保留
- server/server.go: 启动序列同时保留 seedFindingRetester 与 evidence GC
- web/src/lib/types.ts: InterceptAudit/InterceptDetail(#101) 与流量证据类型(#102)并存
- findings/detail/page.tsx: 去掉 #103 带入的重复 import,仅保留 FindingRetestPanel
- chat/page.tsx: 保留 #103 的 Spinner import
This commit is contained in:
Autumn-27 committed 2026-09-10 10:04:53 -04:00
commit e8efce605c
54 files changed
+5328 -552

No files matched your search

+6 -4
View File
@@ -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(agentKey, out)
return out, def, cleanup
}
+1
View File
@@ -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
+22
View File
@@ -0,0 +1,22 @@
package agent
import (
"context"
"github.com/Autumn-27/artex/db"
)
// FindingRecorder is injected by the host; agents never synthesize or copy
// evidence bodies themselves. Its implementation owns the atomic write.
type FindingRecorder interface {
Record(context.Context, db.RecordFindingInput, []db.TrafficRef) (*db.RecordedFinding, error)
}
// Tool-use guidance is appended without replacing the user's editable prompt.
// It does not require capture or claim that unavailable traffic tools exist.
const findingTrafficGuidance = "\n\n**漏洞流量证据(可选)**:调用 report_finding 上报漏洞时,如有已查看并确认支持漏洞结论的 HTTP 请求/响应,可用 traffic_refs 按复现顺序绑定真实 ID;域名和时间只作候选筛选,不推定关联。TCP 等非 HTTP 漏洞、未采集或无确切匹配时省略或传 [],在 evidence 保留命令输出、日志等其他可验证证据,建议说明未绑定原因。不要猜测 ID,也不要仅为补包重复探测。"
func (t *ToolSet) SetFindingRecorder(r FindingRecorder) { t.findingRecorder = r }
func (w *Worker) SetFindingRecorder(r FindingRecorder) { w.findingRecorder = r }
func (p *Planner) SetFindingRecorder(r FindingRecorder) { p.findingRecorder = r }
func (m *MainAgent) SetFindingRecorder(r FindingRecorder) { m.findingRecorder = r }
+108
View File
@@ -0,0 +1,108 @@
package agent
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"testing"
"github.com/Autumn-27/artex/db"
"github.com/Autumn-27/artex/evidence"
)
type failingFindingRecorder struct{}
func (failingFindingRecorder) Record(context.Context, db.RecordFindingInput, []db.TrafficRef) (*db.RecordedFinding, error) {
return nil, errors.New("fixture persistence failed")
}
func TestReportFindingOptionalTrafficWithoutCapture(t *testing.T) {
d := testDB(t)
defer d.Close()
task, err := d.CreateTask("TCP evidence", "fixture", nil, 0, 0)
if err != nil {
t.Fatal(err)
}
defer d.DeleteTask(task.ID)
ts := NewToolSet(nil, "")
ts.ts, ts.taskID = d.Exploration(task.ExplorationID), task.ID
ts.SetFindingRecorder(evidence.New(d, nil, t.TempDir()))
notices := 0
ts.notifyFinding = func(int64, string) { notices++ }
for _, refs := range []string{"", `,"traffic_refs":[]`, `,"traffic_refs":null`} {
res, err := ts.addFinding().Call(t.Context(), json.RawMessage(`{"vulnclass":"TCP","severity":"low","summary":"verified TCP fixture","evidence":"command output proves the finding"`+refs+`}`), nil)
if err != nil || !strings.HasPrefix(res.Flatten(), "finding recorded: ") {
t.Fatalf("optional traffic rejected: %v %s", err, res.Flatten())
}
var out db.RecordedFinding
if err := json.Unmarshal([]byte(strings.SplitN(res.Flatten(), "\n", 2)[1]), &out); err != nil {
t.Fatal(err)
}
f, err := d.GetFinding(out.FindingID)
if err != nil || f == nil || len(out.Traffic.Bindings) != 0 || f.Evidence != "command output proves the finding" {
t.Fatalf("lost non-HTTP evidence: %+v %v", f, err)
}
}
if notices != 3 {
t.Fatalf("successful reports notified %d times", notices)
}
}
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)
if err != nil {
t.Fatal(err)
}
defer d.DeleteTask(task.ID)
ts := NewToolSet(nil, "")
ts.ts = d.Exploration(task.ExplorationID)
ts.taskID = task.ID
ts.worker = "fixture"
notices := 0
ts.notifyFinding = func(int64, string) { notices++ }
call := func(body string) string {
t.Helper()
res, err := ts.addFinding().Call(t.Context(), json.RawMessage(body), nil)
if err != nil {
t.Fatal(err)
}
return res.Flatten()
}
ts.SetFindingRecorder(failingFindingRecorder{})
if got := call(`{"vulnclass":"TEST","summary":"fail","severity":"low","traffic_refs":[{"traffic_id":"x"}]}`); !strings.Contains(got, "persistence failed") {
t.Fatal(got)
}
if notices != 0 || ts.writes.Findings != 0 {
t.Fatal("notified before commit")
}
ts.SetFindingRecorder(nil)
got := call(`{"vulnclass":"TEST","summary":"legacy report","severity":"low"}`)
lines := strings.SplitN(got, "\n", 2)
if len(lines) != 2 {
t.Fatal(got)
}
var out db.RecordedFinding
if err = json.Unmarshal([]byte(lines[1]), &out); err != nil {
t.Fatal(err)
}
if lines[0] != fmt.Sprintf("finding recorded: %d", out.NodeID) || out.FindingID <= 0 || notices != 1 {
t.Fatal(got)
}
f, err := d.GetFinding(out.FindingID)
if err != nil || f == nil || f.NodeID == nil || *f.NodeID != out.NodeID {
t.Fatalf("wrong finding/node mapping: %+v %v", f, err)
}
if got = call(`{"vulnclass":"TEST","summary":"no storage","severity":"low","traffic_refs":[{"traffic_id":"x"}]}`); !strings.Contains(got, "未登记") {
t.Fatal(got)
}
if notices != 1 {
t.Fatal("failed tool triggered reporter")
}
}
+130
View File
@@ -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(agentKey string, 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
}
if agentKey == "reporter" && (tool.Name() == "traffic_search" || tool.Name() == "traffic_get" || tool.Name() == "traffic_blob") {
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 = "\n默认由报告 Agent 在编写报告前核对并绑定流量。上报者在 evidence 中保留验证命令、关键输出、已有的真实流量 ID 及其用途,供报告 Agent 对照执行记录核实;无需为绑定额外查包。兼容显式即时绑定:traffic_refs 或 evidence_hint_id 可提交已核实的引用,后者读取本任务指定 hint 的结构化引用;任一无效则本次上报全部失败。TCP/无包不需要这些可选参数。返回 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 = "\n\n**流量证据交接(可选)**:自动绑定默认由报告 Agent 在漏洞入库后、编写报告前完成。上报者应在 evidence 保留验证命令、关键输出、已有真实流量 ID 及其用途,任务中带 intent_id,便于报告 Agent 追溯;不必为了绑定额外查包。Auto / Planner 代为上报时不要丢弃执行者已有的引用。add_hint / add_task_hint 可用 traffic_refs 交接;显式即时绑定仍兼容 report_finding 的 traffic_refs / evidence_hint_id。TCP 或无包时正常登记,不能猜测 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["update_finding_report"] && has["bind_finding_traffic"] && has["get_finding_traffic"] {
guidance += "\n\n**报告前自动关联流量(已开启)**:你负责为本次触发的漏洞核对并绑定流量,再撰写报告。先从 report_finding 返回 JSON 或 get_task_node_detail / list_task_findings 取得明确的 finding_id 与 finding_node_id。读取漏洞详情、对应意图的执行记录及已有证据清单,优先使用上报者交接的真实 ID。若本次验证为 HTTP 且流量工具可用,用 traffic_search 筛选候选,再用 traffic_get 逐条核实请求/响应确实支持该漏洞;域名和时间只用于筛选,不证明归属。将确认的证据按复现顺序用 bind_finding_traffic(finding_id, traffic_refs) 关联,选择 baseline / proof / verification / supporting 并说明用途。只能操作本次漏洞,不重复创建漏洞或重新探测目标。绑定成功后重新调用 get_finding_traffic 获取最新 version,读取所需正文,再将实际读取的 version 作为 evidence_version 传给 update_finding_report(其 finding_id 参数仍用 finding_node_id)。已有绑定不必重复追加。TCP、未采集、工具不可用或没有确切匹配时,跳过自动绑定,依据文字/命令证据正常写报告并说明原因,不得为凑齐流量而猜测。绑定失败不宣称成功;保留已有证据并在报告说明未绑定原因。"
}
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...))
}
+68
View File
@@ -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")
}
}
+16 -14
View File
@@ -17,19 +17,20 @@ import (
// (→planner) or direct high-priority intents (→frontier). It does NOT run the
// autonomous intent-generation loop (that is the planner's job).
type MainAgent struct {
prov llm.Provider
model string
tx *transcript.Store // raw LLM conversation persistence (nil = off)
window int // context window in tokens (for compaction)
windowFn func() int // optional dynamic task-chain minimum
maxTurns int // max agent turns per run (0 = unlimited)
proxyAddr string // recording proxy for WebFetch (empty = direct)
proxyCACert string // recording proxy's CA cert path (HTTPS verify)
webSearch WebSearchOpts // web_search tool backend selection (off by default)
workDir string // shared work dir (surfaced in prompt as artifact-output target)
steerWork func(intentID int64, msg string) error // engine callback: steer a running work (nil = off)
nonStreamingFn func() bool // resolver: use non-streaming (Complete) path? (nil = streaming)
maxTokensFn func() int // resolver: per-reply output cap (nil/0 = send no cap)
findingRecorder FindingRecorder
prov llm.Provider
model string
tx *transcript.Store // raw LLM conversation persistence (nil = off)
window int // context window in tokens (for compaction)
windowFn func() int // optional dynamic task-chain minimum
maxTurns int // max agent turns per run (0 = unlimited)
proxyAddr string // recording proxy for WebFetch (empty = direct)
proxyCACert string // recording proxy's CA cert path (HTTPS verify)
webSearch WebSearchOpts // web_search tool backend selection (off by default)
workDir string // shared work dir (surfaced in prompt as artifact-output target)
steerWork func(intentID int64, msg string) error // engine callback: steer a running work (nil = off)
nonStreamingFn func() bool // resolver: use non-streaming (Complete) path? (nil = streaming)
maxTokensFn func() int // resolver: per-reply output cap (nil/0 = send no cap)
}
// SetNonStreaming wires a resolver deciding whether runs use the non-streaming
@@ -103,6 +104,7 @@ func mainAgentSystem(goal, dataDir, workDir string) string {
// worker/planner sessions — not just the final answer.
func (m *MainAgent) Chat(ctx context.Context, taskID int64, as *db.AssetStore, ts *db.ExplorationStore, goal, message string, emit func(db.Activity), notify, resume func(), notifyGoal func([]string)) (string, error) {
tsx := NewToolSet(ts, "human")
tsx.SetFindingRecorder(m.findingRecorder)
if as != nil {
tsx.SetAssetStore(as, as.Companies())
}
@@ -149,7 +151,7 @@ func (m *MainAgent) Chat(ctx context.Context, taskID int64, as *db.AssetStore, t
// 命中预算(步数)→ SDK 跑收尾:向用户输出一句进展总结。Prompt 与收尾轮数可后台编辑(默认 10 轮)。
Settlement: wrapupSettlement("mainagent", nil),
NonStreaming: m.nonStreaming(), // 该 profile 选非流式时走 Provider.Complete
MaxTokens: m.maxTokens(), // 0 = 不发上限,由服务端默认值决定
MaxTokens: m.maxTokens(), // 0 = 不发上限,由服务端默认值决定
}
if m.tx != nil { // persist raw human↔AI conversation; one accumulating file per task
opts.Transcript = m.tx
+2
View File
@@ -20,6 +20,7 @@ import (
// assets, judges whether the task goal is met, and emits 0..N exploration intents
// into the frontier. It is the sole intent generator.
type Planner struct {
findingRecorder FindingRecorder
prov llm.Provider
model string
tx *transcript.Store // raw LLM conversation persistence (nil = off)
@@ -338,6 +339,7 @@ func (p *Planner) Plan(ctx context.Context, taskID int64, as *db.AssetStore, ts
// runs in a detached goroutine so it never adds latency to this round.
p.compactor.OnPlannerRound(ctx, ts)
tsx := NewToolSet(ts, "planner")
tsx.SetFindingRecorder(p.findingRecorder)
if as != nil {
tsx.SetAssetStore(as, as.Companies())
}
+3
View File
@@ -57,6 +57,9 @@ 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") {
t.Fatalf("worker bypassed shared optional evidence policy: %q", recording)
}
if !strings.Contains(recording, "中间产物输出规约") {
t.Fatalf("worker missing artifact tail: %q", recording)
}
+10 -7
View File
@@ -52,7 +52,9 @@ const pentestDefaultTmpl = `你是一个授权渗透测试系统的"独立渗透
- 每得出一个结果**立刻**落地,别攒到最后(会话步数耗尽就全丢;记下来的才算数,活在脑子里的不算)。这些记录也是你抗 compaction 的长期记忆。
- **只写增量**:写之前扫一眼已登记的资产/已记的路线,只记你**新得到**的东西,别把已有内容换措辞重记(重复只会膨胀、也误导你自己以为有新进展)。只是印证已有结论而无新增,就不必再记。
- **发现新资产/入口** → insert_assets(资产本身:endpoint/parameter/tech 指纹/service/凭据/子域等,结构化属性写在资产 props 上)。回看已登记资产用 list_assets,避免重复登记。
- **确认漏洞** → report_finding(含可复现 PoC)。**只有你在本次运行里真实触发过、拿到可复现证据(请求/响应或命令输出)才用它**;回看已报漏洞用 list_findings。严禁把仅凭版本/CVE 匹配、"看起来可注入"、外部漏洞库/更新日志/代码 diff 推断的东西当已确认漏洞上报。**不要用查 CVE 库或"对比补丁版本"替代实际触发**;触发不了但有嫌疑,就在 TodoWrite 里标为"存疑/待验证",别硬记成 finding。
- **确认漏洞** → report_finding(含可复现 PoC)。**只有你在本次运行里真实触发过、拿到可复现证据(请求/响应或命令输出)才用它**;回看已报漏洞用 list_findings。有对应录制流量时,先 traffic_search / traffic_get 核对真实记录,再用 traffic_refs 按复现顺序绑定;域名和时间只用于候选筛选,不代表任务归属。严禁把仅凭版本/CVE 匹配、"看起来可注入"、外部漏洞库/更新日志/代码 diff 推断的东西当已确认漏洞上报。**不要用查 CVE 库或"对比补丁版本"替代实际触发**;触发不了但有嫌疑,就在 TodoWrite 里标为"存疑/待验证",别硬记成 finding。
流量绑定可选:TCP 等非 HTTP 漏洞、未采集或无确切匹配记录时,省略 traffic_refs 或传 [],在 evidence 保留命令输出、日志等其他可验证证据,建议说明未绑定原因。不要猜测 ID,也不要仅为补包重复探测。
━━ 判定与收尾 ━━
- 随时对照任务目标:已被你**验证过**的成果满足了目标,就据此判定达成并说明依据。判"达成"的前提是心法 4 的自检已通过——没独立复现过的战果不算达成依据。
@@ -77,15 +79,16 @@ const ReporterDefaultPrompt = `你是一个授权渗透测试系统里的**漏
每当有 worker 调用 report_finding 登记了一个漏洞,系统就会用一段【由工具调用触发】的上下文唤起你,其中包含:
- **任务 id**(task_id,见上下文"任务: #<id>")
- report_finding 的**入参**(vulnclass / severity / summary / evidence 等)
- report_finding 的**返回**:形如 "finding recorded: <id>" —— 这个 **<id> 就是本漏洞的 finding_id**,是你后面保存报告的句柄。
- report_finding 的**返回**:形如 "finding recorded: <id>" —— 这个 **<id> 是探索节点 ID**,是 get_task_node_detail 和 update_finding_report 使用的旧句柄。返回 JSON 中的 finding_id 则是独立漏洞记录 ID,get_finding_traffic 使用它。
先从上下文里**准确抽取 finding_id 和 task_id**。抽取不到 finding_id 就不要瞎写,说明情况即可。
先从上下文里**准确抽取 task_id、探索节点 node_id,以及 JSON 中的独立漏洞 finding_id(如有)**,不得混用两种 ID。抽取不到 node_id 就不要瞎写,说明情况即可。
━━ 工作步骤 ━━
1. **取全证据**:用 get_task_node_detail(task_id, id=<finding_id>) 读该漏洞节点的**完整证据/PoC**(触发上下文里的 evidence 可能被截断)。
2. **还原过程**:用 list_task_worker_traces(task_id) 找到相关的 work,再用 get_task_worker_trace(task_id, intent_id[, step_ids]) 或 search_task_worker_traces(task_id, q) 看这个漏洞**是怎么被发现和验证的**(用了什么请求/命令、目标怎么响应)。必要时 get_task_graph(task_id) 看整体态势、list_task_findings(task_id) 看是否有关联漏洞。
3. **写报告**:综合以上,写一份结构化 Markdown 报告(见下方模板)。
4. **保存**:调用 **update_finding_report(finding_id=<那个 id>, report=<Markdown 全文>)** 保存。这是你的最终产物——不写进去等于没做。
1. **取全证据**:用 get_task_node_detail(task_id, id=<node_id>) 读该漏洞节点的**完整证据/PoC**(触发上下文里的 evidence 可能被截断)。
2. **流量证据**:如返回 JSON 包含独立 finding_id,用 get_finding_traffic 先读有序清单及 version,有绑定时再按 binding_id 分段读取请求/响应。绑定可选,空清单不阻止撰写报告:TCP 等非 HTTP 漏洞或未采集的情况,依据节点证据、命令输出和日志说明复现与影响,建议如实说明未绑定原因,不虚构请求/响应,不仅为补包重新探测。报告引用稳定证据编号及用途;仅按真实内容描述。保存报告时传入所读 version 作为 evidence_version;如版本冲突,重新读取并生成,不得直接换版本重试。
3. **还原过程**:用 list_task_worker_traces(task_id) 找到相关的 work,再用 get_task_worker_trace(task_id, intent_id[, step_ids]) 或 search_task_worker_traces(task_id, q) 看这个漏洞**是怎么被发现和验证的**(用了什么请求/命令、目标怎么响应)。必要时 get_task_graph(task_id) 看整体态势、list_task_findings(task_id) 看是否有关联漏洞。
4. **写报告**:综合以上,写一份结构化 Markdown 报告(见下方模板)。
5. **保存**:调用 **update_finding_report(finding_id=<node_id>, report=<Markdown 全文>, evidence_version=<实际读取的 version>)** 保存;未读取版本时省略 evidence_version,不得猜测。这是你的最终产物——不写进去等于没做。
━━ 报告结构(Markdown,按需裁剪,但证据/复现/修复必须有)━━
- ` + "`## 概述`" + `:一句话说清是什么漏洞、在哪、能造成什么。
+111 -72
View File
@@ -73,11 +73,12 @@ func compactIntents(ns []*db.Node, parentsOf, yieldsOf map[int64][]int64) []map[
// ToolSet exposes the PG-backed dual graph (asset + exploration) to an LLM agent.
// One ToolSet is created per planner/worker run; per-run signals live here.
type ToolSet struct {
as *db.AssetStore // asset store (optional; nil = asset tools not available)
cs *db.CompanyStore // company store (optional)
ts *db.ExplorationStore
worker string
taskID int64 // PG tasks.id; 0 when unknown (tests / orchestrator cross-task reads)
findingRecorder FindingRecorder
as *db.AssetStore // asset store (optional; nil = asset tools not available)
cs *db.CompanyStore // company store (optional)
ts *db.ExplorationStore
worker string
taskID int64 // PG tasks.id; 0 when unknown (tests / orchestrator cross-task reads)
// coverageDisabled mirrors tasks.coverage_enabled=false. Stored inverted so the
// zero value (all existing ToolSet constructions) means ENABLED — matching the
// DB default (true). When true: graphOverviewData drops the coverage block, the
@@ -398,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
@@ -1068,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 {
@@ -1076,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 {
@@ -1167,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
})
}
@@ -1358,67 +1372,79 @@ func (t *ToolSet) goalMet() actool.CoreTool {
// --- worker write tools ---
func (t *ToolSet) addFinding() actool.CoreTool {
return writeTool("report_finding", "[重要]发现漏洞时必须调用该工具进行记录!记录一个确认的漏洞发现。在任务上下文中 intent_id 必填(当前正在执行的意图 id);在会话上下文中 intent_id 可不填。",
obj(map[string]any{
"vulnclass": str("漏洞类(分类,如 SQL Injection / IDOR / XSS)"),
"name": str("漏洞名称(具体可读的标题,如『用户中心订单接口存在越权访问』;建议填写,留空时前端回退展示 vulnclass)"),
"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(可选,0/1/多个):参数/端点/站点等。一个漏洞影响多处可全填,纯观察可不填。"},
"evidence": str("证据/PoC 文本"),
}, "vulnclass", "severity", "summary"),
func(_ context.Context, in json.RawMessage) (actool.Result, error) {
var a struct {
VulnClass, Name, Severity, Summary, Evidence string
IntentID json.RawMessage `json:"intent_id"`
AssetIDs []json.RawMessage `json:"asset_ids"`
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_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) {
var a struct {
VulnClass, Name, Severity, Summary, Evidence string
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 需要任务上下文;平台对话请通过 add_task_hint 向对应任务交接漏洞,并在提示中携带已有的 traffic_refs,由任务 Agent 登记。已登记漏洞可用 bind_finding_traffic 补绑。"), nil
}
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
}
input := db.RecordFindingInput{TaskID: t.taskID, ExplorationID: t.ts.ID(), IntentID: pid(a.IntentID), VulnClass: a.VulnClass, Name: a.Name, Severity: a.Severity, Summary: a.Summary, Evidence: a.Evidence, Worker: t.worker, AssetIDs: pidList(a.AssetIDs)}
var recorded *db.RecordedFinding
if t.findingRecorder != nil {
recorded, err = t.findingRecorder.Record(ctx, input, refs)
} else if len(refs) > 0 {
return actool.Errorf("流量证据存储不可用;未登记漏洞"), nil
} else {
recorded, err = t.ts.RecordFinding(ctx, input)
}
if err != nil {
return actool.Errorf(err.Error()), nil
}
if t.notifyFinding != nil {
iid := input.IntentID
if iid <= 0 {
iid = t.ownerNode
}
_ = json.Unmarshal(in, &a)
payload := map[string]any{"vulnclass": a.VulnClass, "name": a.Name, "severity": a.Severity, "summary": a.Summary,
"evidence": map[string]any{"by": t.worker, "poc": a.Evidence}}
var anchors []int64
for _, raw := range a.AssetIDs {
if p := pid(raw); p > 0 {
anchors = append(anchors, p)
}
t.notifyFinding(iid, a.Summary)
} else if t.notify != nil {
t.notify()
}
t.writes.Findings++
// Keep the first line's node-ID contract for existing reporter triggers.
for i := range recorded.Traffic.Bindings {
recorded.Traffic.Bindings[i].Snapshot.ReqHead = ""
recorded.Traffic.Bindings[i].Snapshot.RespHead = ""
}
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 自动绑定流量已关闭,可在页面人工关联流量。"
}
var id int64
if t.ts != nil {
intent := pid(a.IntentID)
if intent > 0 {
node, err := t.ts.GetNode(intent)
if err != nil || node == nil || node.Kind != db.KindIntent {
return actool.Errorf("intent_id 必须是本任务的意图(关联任务意图只读)"), nil
}
}
var err error
id, err = t.ts.AddNode(db.KindFinding, payload, 9, "confirmed", t.worker, anchors)
if err != nil {
return actool.Errorf(err.Error()), nil
}
if intent > 0 {
_ = t.ts.Link(intent, db.RelYields, id) // chain: intent -> finding
}
_, _ = t.ts.AddStandaloneFinding(t.taskID, id, a.VulnClass, a.Name, a.Severity, a.Summary, a.Evidence, t.worker, anchors)
// 确证漏洞落库 → 当场唤醒本任务 planner(不等 worker 收工,debounce 合并)。
// 优先带上下文(哪个意图+finding摘要);intent 用工具参数,缺省回退到 owner 意图。
if t.notifyFinding != nil {
iid := pid(a.IntentID)
if iid <= 0 {
iid = t.ownerNode
}
t.notifyFinding(iid, a.Summary)
} else if t.notify != nil {
t.notify()
}
} else {
// conversation context: no exploration store available, cannot record finding
return actool.Errorf("report_finding 需要任务上下文(exploration store 未初始化)"), nil
}
t.writes.Findings++
return actool.Text(fmt.Sprintf("finding recorded: %d", id)), nil
})
}
raw, _ := json.Marshal(result)
return actool.Text(fmt.Sprintf("finding recorded: %d\n%s", recorded.NodeID, raw)), nil
})
}
// recordFact writes a general exploration RESULT/conclusion (not a vuln, not a
@@ -1530,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 不能为空")
}
@@ -1545,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)
}
@@ -1731,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 {
+17 -12
View File
@@ -41,17 +41,18 @@ type WebSearchOpts struct {
}
type Worker struct {
prov llm.Provider
model string
workDir string
proxyAddr string
proxyCACert string // recording proxy's CA cert path (for WebFetch HTTPS verify)
webSearch WebSearchOpts // web_search tool backend selection (off by default)
mem *memory.Store // cross-engagement tradecraft memory (G4)
tx *transcript.Store // raw LLM conversation persistence (nil = off)
window int // context window in tokens (for compaction)
windowFn func() int // optional dynamic task-chain minimum
maxTurns int // max agent turns per run (0 = unlimited)
findingRecorder FindingRecorder
prov llm.Provider
model string
workDir string
proxyAddr string
proxyCACert string // recording proxy's CA cert path (for WebFetch HTTPS verify)
webSearch WebSearchOpts // web_search tool backend selection (off by default)
mem *memory.Store // cross-engagement tradecraft memory (G4)
tx *transcript.Store // raw LLM conversation persistence (nil = off)
window int // context window in tokens (for compaction)
windowFn func() int // optional dynamic task-chain minimum
maxTurns int // max agent turns per run (0 = unlimited)
// runTimeout is the wall-clock budget for the main exploration of one intent
// (0 = unlimited). When it fires, the run is cut and a settlement round is
// forced so already-identified facts get written back instead of being lost.
@@ -204,7 +205,9 @@ const workerDefaultTmpl = `你是一个 ARTEX 平台授权渗透测试系统的"
**边发现边写回**(写进图才算数,脑子/文字里的不算;每得一个结果立刻写,别攒到最后被步数耗尽丢掉)。三种写回,别串图:
- **新资产/资源 → insert_assets(资产图)**:子域 / service / endpoint / 指纹 / 凭据 等一切资产【本身】。**这里只登记资产;探索结论/判断不写这里,用 record_fact。**
- **探索结论/事实 → record_fact(探索图,传 intent_id)**:都用它。**多个观察汇总成【一条】事实**(summary 一句总结 + detail写对总结的拓展,依靠真实的执行过程),不要一个属性一条、一意图通常只一条,拆碎会让图谱无限膨胀——**默认就写一条,能并进 detail 的都并进去**;仅当确有【彼此完全独立、无法归并】的结论时才用 facts 数组分条,这是极少数例外,不是常规。**只写增量**:只记这次【新得到】的,别把已有事实换措辞重记(只印证已有、无新增就不必记)。**只写真实看到的**:给 evidence(一行:命令+最能证明的一两行输出,简洁,细节在 detail)、标 confidence(observed=直接看到 / inferred=据现象推断)。
- **确认漏洞 → report_finding(探索图,含 PoC,传 intent_id)**:**只有你本次真实触发过、拿到可复现证据(请求/响应或命令输出)才用**。严禁把"版本/指纹匹配到 CVE""参数看起来可注入""外部漏洞库/更新日志/代码 diff 推断"当已确认,也不要用查 CVE 库或对比补丁版本替代实际触发。触发不了但有嫌疑 → 用 record_fact 记一条 inferred 事实(嫌疑点+为何未触发)交规划者,别硬记成 finding。
- **确认漏洞 → report_finding(探索图,含 PoC,传 intent_id)**:**只有你本次真实触发过、拿到可复现证据(请求/响应或命令输出)才用**。严禁把"版本/指纹匹配到 CVE""参数看起来可注入""外部漏洞库/更新日志/代码 diff 推断"当已确认,也不要用查 CVE 库或对比补丁版本替代实际触发。触发不了但有嫌疑 → 用 record_fact 记一条 inferred 事实(嫌疑点+为何未触发)交规划者,别硬记成 finding。有对应录制流量时,先用 traffic_search / traffic_get 核对真实 ID,再用 traffic_refs 按复现顺序绑定多条请求/响应;域名和时间只用于候选筛选,不推定任务归属。
流量绑定可选:TCP 等非 HTTP 漏洞、未采集或无确切匹配记录时,省略 traffic_refs 或传 [],在 evidence 保留命令输出、日志等其他可验证证据,建议说明未绑定原因。不要猜测 ID,也不要仅为补包重复探测。
完成本意图后用一句话总结你做了什么、写回了哪些事实。务实、克制、聚焦这一条意图。`
@@ -255,6 +258,7 @@ 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.
// Optional finding guidance is added for every role after tool resolution.
return body + workerTrafficBlock(caCert != "") + workerArtifactSpec(runDir)
}
@@ -325,6 +329,7 @@ func (w *Worker) ExecuteWithMessage(ctx context.Context, name string, taskID int
func (w *Worker) execute(ctx context.Context, name string, taskID int64, as *db.AssetStore, ts *db.ExplorationStore, intent *db.Node, hooks harness.HookRunner, emit func(db.Activity), enr EnrichTrigger, notifyFinding func(int64, string), requestID, message string) (harness.TerminalReason, WriteCounts, error) {
tsx := NewToolSet(ts, name)
tsx.SetFindingRecorder(w.findingRecorder)
tsx.SetTaskID(taskID)
coverageEnabled := as == nil || as.CoverageEnabled(taskID)
tsx.SetCoverageEnabled(coverageEnabled)