feat(task): 资产覆盖度功能可按任务开关
创建任务时新增「资产覆盖度功能」开关(默认开)。关闭后: - 不计算/展示测试覆盖度,总览卡片隐藏、覆盖度 API 返回 enabled:false - 态势图仅展示资产不显示测试进度(B1:抹平 tested、去已测统计与图例) - 不再自动累积 task_scope(source=auto) - planner/mainagent 的 add_task_scope、list_untested_assets 从工具列表剔除 - graph_overview 丢弃 coverage 度量块、保留 scope 字段(防上下文污染) company 关联(task_scope kind=company)与此开关无关,永不受影响: insertTaskCompanies 不 gate,TaskCompanyIDs 照常读回,scope 字段照常喂给 agent。 DB:tasks 加 coverage_enabled 列(schema.sql + ALTER ADD COLUMN IF NOT EXISTS 补旧库,默认 true 保持存量任务原行为)。 Claude-Session: https://claude.ai/code/session_01Jm87Gr5fq5aso6SfLyguhx
This commit is contained in:
1 parent
5126ac4592
commit
eb3f278711
18 files changed
+179
-43
No files matched your search
+3
-1
@@ -80,11 +80,13 @@ func (m *MainAgent) Chat(ctx context.Context, taskID int64, as *db.AssetStore, t
|
||||
tsx.SetAssetStore(as, as.Companies())
|
||||
}
|
||||
tsx.SetTaskID(taskID)
|
||||
tsx.SetCoverageEnabled(as == nil || as.CoverageEnabled(taskID))
|
||||
tsx.SetNotify(notify) // add_hint wakes this task's planner (debounced)
|
||||
tsx.SetResumeTask(resume) // set_goals 新增目标 → 把已完成/暂停的任务拉回 running
|
||||
tsx.SetNotifyGoal(notifyGoal) // set_goals 新增目标 → 给 planner 记一条「人新增了目标:…」触发
|
||||
// 领域工具 + 基础默认工具集(Read/Write/Edit/MultiEdit/LS/Glob/Grep/Bash)
|
||||
base := append(tsx.MainAgentTools(), actool.DefaultTools()...)
|
||||
// 资产覆盖度功能关闭时剔除 add_task_scope/list_untested_assets(不入 prompt)。
|
||||
base := append(tsx.DropCoverageTools(tsx.MainAgentTools()), actool.DefaultTools()...)
|
||||
ctx = WithRunInfo(ctx, RunInfo{TaskID: taskID, ExplorationID: explorationID(ts)})
|
||||
tools, def, cleanup := AugmentTools(ctx, "mainagent", base)
|
||||
defer cleanup()
|
||||
|
||||
+3
-1
@@ -266,13 +266,15 @@ func (p *Planner) Plan(ctx context.Context, taskID int64, as *db.AssetStore, ts
|
||||
tsx.SetAssetStore(as, as.Companies())
|
||||
}
|
||||
tsx.SetTaskID(taskID)
|
||||
tsx.SetCoverageEnabled(as == nil || as.CoverageEnabled(taskID))
|
||||
tsx.killWork = p.killWork // enable kill_work tool (nil = unavailable)
|
||||
tsx.steerWork = p.steerWork // enable steer_work tool (nil = unavailable)
|
||||
if origin, _ := ts.OriginFactID(); origin > 0 {
|
||||
tsx.SetOwnerNode(origin) // planner-side anchors default to the task root (origin fact)
|
||||
}
|
||||
// 领域工具 + 基础默认工具集(Read/Write/Edit/MultiEdit/LS/Glob/Grep/Bash)
|
||||
base := append(tsx.PlannerTools(), actool.DefaultTools()...)
|
||||
// 资产覆盖度功能关闭时剔除 add_task_scope/list_untested_assets(不入 prompt)。
|
||||
base := append(tsx.DropCoverageTools(tsx.PlannerTools()), actool.DefaultTools()...)
|
||||
ctx = WithRunInfo(ctx, RunInfo{TaskID: taskID, ExplorationID: explorationID(ts)})
|
||||
tools, def, cleanup := AugmentTools(ctx, "planner", base)
|
||||
defer cleanup()
|
||||
|
||||
+58
-14
@@ -56,6 +56,12 @@ type ToolSet struct {
|
||||
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
|
||||
// auto-scope hook (insertAssets) is skipped, and add_task_scope/list_untested_assets
|
||||
// are filtered out of the agent's tool list. The scope field stays regardless.
|
||||
coverageDisabled bool
|
||||
// ownerNode is the exploration node that writes attach to: assets this run
|
||||
// touches get anchored to it as lineage/provenance (NOT visibility — the asset
|
||||
// graph is global and shared). Worker = its claimed intent; planner = begin root.
|
||||
@@ -150,6 +156,36 @@ func NewToolSet(ts *db.ExplorationStore, worker string) *ToolSet {
|
||||
// dual-write to the standalone findings table (which survives task deletion).
|
||||
func (t *ToolSet) SetTaskID(id int64) { t.taskID = id }
|
||||
|
||||
// SetCoverageEnabled records whether this task has the asset-coverage feature on
|
||||
// (default enabled). Passing false makes graphOverviewData omit the coverage block,
|
||||
// the insertAssets auto-scope hook a no-op, and CoverageTools reports the two
|
||||
// coverage-only tools so callers can drop them from the agent's tool list.
|
||||
func (t *ToolSet) SetCoverageEnabled(enabled bool) { t.coverageDisabled = !enabled }
|
||||
|
||||
// CoverageDisabled reports whether the coverage feature is off for this task.
|
||||
func (t *ToolSet) CoverageDisabled() bool { return t.coverageDisabled }
|
||||
|
||||
// coverageOnlyTools are the LLM tools that only make sense when asset coverage is
|
||||
// on. When the feature is off they are filtered out of the agent's tool list so
|
||||
// they neither pollute the prompt nor let the model build a disabled denominator.
|
||||
var coverageOnlyTools = map[string]bool{"add_task_scope": true, "list_untested_assets": true}
|
||||
|
||||
// DropCoverageTools returns tools with the coverage-only ones removed when this
|
||||
// task has the feature disabled; otherwise it returns tools unchanged.
|
||||
func (t *ToolSet) DropCoverageTools(tools []actool.CoreTool) []actool.CoreTool {
|
||||
if !t.coverageDisabled {
|
||||
return tools
|
||||
}
|
||||
out := tools[:0:0]
|
||||
for _, tool := range tools {
|
||||
if coverageOnlyTools[tool.Name()] {
|
||||
continue
|
||||
}
|
||||
out = append(out, tool)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Cross-task reuse: exported accessors returning the per-task tool logic bound to
|
||||
// THIS ToolSet's store. Host-side orchestration tools build a ToolSet for an
|
||||
// arbitrary task, then Call these — so cross-task reads/hint reuse the exact
|
||||
@@ -362,22 +398,28 @@ func (t *ToolSet) graphOverviewData() map[string]any {
|
||||
out["related_tasks"] = t.relatedTaskOverviews()
|
||||
// coverage:粗略的资产测试覆盖度参考——范围(task_scope)内的资产里,被 fact 碰过的
|
||||
// 占比 + by_type(按类型的 总数/已测)。要看未测的具体资产由 agent 按需调 list_untested_assets 自行判断。仅任务上下文有。
|
||||
// 资产覆盖度功能关闭时(coverageDisabled):只保留 scope/hosts(范围边界与目标主机的
|
||||
// 感知信息,company 关联经由 scope 在此浮现),丢弃 denominator/tested/pct/by_type/note
|
||||
// 等覆盖度度量,避免污染上下文、也不诱导已隐藏的 add_task_scope/list_untested_assets。
|
||||
if t.as != nil && t.ts != nil && t.taskID > 0 {
|
||||
if cov, err := t.as.TaskCoverageWithSources(t.taskID); err == nil {
|
||||
m := map[string]any{
|
||||
"denominator": cov.Denominator,
|
||||
"tested": cov.Tested,
|
||||
"by_type": cov.ByType,
|
||||
"note": "coverage资产测试覆盖度(包括接口等各种相关资产),粗略估计、仅供参考:包含当前任务与直接关联任务的 scope、事实锚点;关联 scope 只读。容器型资产/大量枚举会让它偏低,勿据此认为已测完;可用 add_task_scope 增补本任务范围、list_untested_assets 看未测资产【通常不调用list_untested_assets,按照任务推进即可】;",
|
||||
}
|
||||
if cov.Denominator == 0 {
|
||||
m["pct"] = nil
|
||||
m["status"] = "范围未锚定"
|
||||
} else {
|
||||
m["pct"] = cov.Pct
|
||||
{
|
||||
m := map[string]any{}
|
||||
if !t.coverageDisabled {
|
||||
if cov, err := t.as.TaskCoverageWithSources(t.taskID); err == nil {
|
||||
m["denominator"] = cov.Denominator
|
||||
m["tested"] = cov.Tested
|
||||
m["by_type"] = cov.ByType
|
||||
m["note"] = "coverage资产测试覆盖度(包括接口等各种相关资产),粗略估计、仅供参考:包含当前任务与直接关联任务的 scope、事实锚点;关联 scope 只读。容器型资产/大量枚举会让它偏低,勿据此认为已测完;可用 add_task_scope 增补本任务范围、list_untested_assets 看未测资产【通常不调用list_untested_assets,按照任务推进即可】;"
|
||||
if cov.Denominator == 0 {
|
||||
m["pct"] = nil
|
||||
m["status"] = "范围未锚定"
|
||||
} else {
|
||||
m["pct"] = cov.Pct
|
||||
}
|
||||
}
|
||||
}
|
||||
// scope:当前测试范围的根资产(task_scope 原始行),让 agent 知道这个任务到底
|
||||
// 圈定了哪些目标(不是全部测试资产,而是范围边界本身)。
|
||||
// 圈定了哪些目标(不是全部测试资产,而是范围边界本身)。覆盖度开关无关,始终提供。
|
||||
if rows, err := t.as.ListTaskScopeWithSources(t.taskID); err == nil && len(rows) > 0 {
|
||||
scope := make([]map[string]any, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
@@ -445,7 +487,9 @@ func (t *ToolSet) graphOverviewData() map[string]any {
|
||||
m["hosts_truncated"] = true
|
||||
}
|
||||
}
|
||||
out["coverage"] = m
|
||||
if len(m) > 0 {
|
||||
out["coverage"] = m
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
|
||||
@@ -274,11 +274,14 @@ func (t *ToolSet) insertAssets() actool.CoreTool {
|
||||
t.anchorOwner(id)
|
||||
// 自动入测试范围(source='auto'):只对 worker 顶层显式插入的这一项,按其
|
||||
// 类型加保守范围;side-effect 派生的资产不经此处,故范围不盲目扩大。taskID=0 时无操作。
|
||||
svcIP := item.ServiceIP
|
||||
if svcIP == "" {
|
||||
svcIP = item.IP
|
||||
// 资产覆盖度功能关闭时不再累积测试范围(分母)。
|
||||
if !t.coverageDisabled {
|
||||
svcIP := item.ServiceIP
|
||||
if svcIP == "" {
|
||||
svcIP = item.IP
|
||||
}
|
||||
_ = t.as.AddAutoScope(taskID, typ, item.Domain, item.URL, svcIP)
|
||||
}
|
||||
_ = t.as.AddAutoScope(taskID, typ, item.Domain, item.URL, svcIP)
|
||||
}
|
||||
|
||||
return jsonResult(map[string]any{
|
||||
|
||||
+7
-2
@@ -254,6 +254,8 @@ func renderWorkerGraphOverview(data map[string]any) string {
|
||||
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)) (harness.TerminalReason, WriteCounts, error) {
|
||||
tsx := NewToolSet(ts, name)
|
||||
tsx.SetTaskID(taskID)
|
||||
coverageEnabled := as == nil || as.CoverageEnabled(taskID)
|
||||
tsx.SetCoverageEnabled(coverageEnabled)
|
||||
if as != nil {
|
||||
tsx.SetAssetStore(as, as.Companies())
|
||||
}
|
||||
@@ -351,8 +353,11 @@ func (w *Worker) Execute(ctx context.Context, name string, taskID int64, as *db.
|
||||
// 意图明确针对的这些资产 → 自动纳入任务测试范围(与 insertAssets 同一套
|
||||
// 保守粒度)。upsertTaskScope 的 ON CONFLICT DO NOTHING + uq_task_scope
|
||||
// 唯一索引保证不会重复添加;重跑/重试同样是幂等 no-op。
|
||||
for _, a := range assets {
|
||||
_ = as.AddAutoScope(taskID, a.Type, a.Domain, a.URL, a.IP)
|
||||
// 资产覆盖度功能关闭时不再累积测试范围(分母)。
|
||||
if coverageEnabled {
|
||||
for _, a := range assets {
|
||||
_ = as.AddAutoScope(taskID, a.Type, a.Domain, a.URL, a.IP)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user