refactor(agent): 测试范围与资产覆盖度开关解耦,移除 insert_assets 的 related 入参
task_scope 是任务的授权边界、资产查询的过滤基准,覆盖度开关只该决定要不要 拿它当分母算指标,不该决定要不要累积范围本身。此前关闭覆盖度会让 auto 与 agent 两条写入路径同时失效,task_scope 只剩界面手工添加的行。 - insert_assets 的自动入范围(source='auto')无条件执行,不再判 coverageDisabled - add_task_scope 移出 coverageOnlyTools,planner / 任务内主 Agent / 目标拆解 Agent 在覆盖度关闭时照样持有;list_untested_assets 仍随开关隐藏(纯覆盖度视角) - 移除 insert_assets 每条资产的 related 入参:唯一作用是决定要不要入范围,值又 不落库(同一资产再次登记即作废、界面上也看不出谁被判为无关),连带删除 StripCoverageParams 及其专用的 deepCopyJSONMap / nestedMap - add_task_scope 与 goalsScopeTail 的描述改为「授权边界」优先、「覆盖度分母」其次
This commit is contained in:
1 parent
33d3d00c4b
commit
42f0ff97d3
7 files changed
+22
-124
No files matched your search
@@ -4,6 +4,13 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Agent
|
||||
|
||||
#### 修改的功能
|
||||
|
||||
- `insert_assets` 移除每条资产的 `related` 入参:它唯一的作用是决定要不要把这条资产加进本任务范围,值又不落库(同一资产被再次登记就作废、界面上也看不出谁被判为无关),实际只是让模型多做一次留不住的判断。
|
||||
- **测试范围(`task_scope`)不再受资产覆盖度开关影响**:`insert_assets` 的自动入范围(`source='auto'`)无论覆盖度开启与否都执行,`add_task_scope` 也始终提供给 planner / 任务内主 Agent / 目标拆解 Agent。范围是任务的授权边界、资产查询的过滤基准,覆盖度开关只决定要不要拿它当分母算指标,不该决定要不要累积范围本身。此前关闭覆盖度会让 `auto`/`agent` 两条写入路径同时失效,`task_scope` 只剩界面手工添加的行。`list_untested_assets` 仍随覆盖度关闭而隐藏(它本身就是纯覆盖度视角)。
|
||||
|
||||
## [0.3.9] - 2026-09-11
|
||||
|
||||
### Agent
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
actool "github.com/Autumn-27/norma/tool"
|
||||
)
|
||||
|
||||
// hasRelatedParam reports whether insert_assets' schema still advertises the
|
||||
// per-item `related` parameter (properties.assets.items.properties.related).
|
||||
func hasRelatedParam(tool actool.CoreTool) bool {
|
||||
props, ok := nestedMap(tool.InputSchema(), "properties", "assets", "items", "properties")
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
_, ok = props["related"]
|
||||
return ok
|
||||
}
|
||||
|
||||
// TestStripCoverageParams verifies the insert_assets `related` param is visible
|
||||
// while coverage is on and hidden once it's off, and that stripping never mutates
|
||||
// the original tool's schema (deep copy).
|
||||
func TestStripCoverageParams(t *testing.T) {
|
||||
ts := NewToolSet(nil, "")
|
||||
insert := ts.insertAssets()
|
||||
if !hasRelatedParam(insert) {
|
||||
t.Fatal("insert_assets 应默认带 related 参数")
|
||||
}
|
||||
|
||||
// Enabled (default): schema unchanged, related still present.
|
||||
ts.SetCoverageEnabled(true)
|
||||
if got := ts.StripCoverageParams([]actool.CoreTool{insert}); !hasRelatedParam(got[0]) {
|
||||
t.Error("覆盖度开启时不应剔除 related 参数")
|
||||
}
|
||||
|
||||
// Disabled: related stripped from the model-facing schema.
|
||||
ts.SetCoverageEnabled(false)
|
||||
out := ts.StripCoverageParams([]actool.CoreTool{insert})
|
||||
if hasRelatedParam(out[0]) {
|
||||
t.Error("覆盖度关闭时应隐藏 related 参数")
|
||||
}
|
||||
// The original tool must be untouched (deep copy, no shared-map mutation).
|
||||
if !hasRelatedParam(insert) {
|
||||
t.Error("StripCoverageParams 不应修改原始 tool 的 schema")
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -51,7 +51,7 @@ const goalsDefaultTmpl = `你是渗透测试目标分解器。你的职责是从
|
||||
const goalsScopeTail = `
|
||||
|
||||
**额外职责:登记测试资产范围**
|
||||
除拆分目标外,你还要从「任务目标 / 任务描述」里识别出**明确给出的测试资产范围**,调用 add_task_scope 登记(资产测试覆盖度的分母,也是授权边界)。**最小范围原则:只登记用户明确点到的那一个目标,绝不擅自放大。**
|
||||
除拆分目标外,你还要从「任务目标 / 任务描述」里识别出**明确给出的测试资产范围**,调用 add_task_scope 登记(本任务的授权边界,也是资产测试覆盖度的分母)。**最小范围原则:只登记用户明确点到的那一个目标,绝不擅自放大。**
|
||||
- 目标是 URL 或带主机名的地址(如 https://xxx.example.com/path、app.example.com)→ 取其**完整主机名**,kind=subdomain,value=完整主机名。
|
||||
例:目标 https://a1b2c3.lab.example.net/path → kind=subdomain,value=a1b2c3.lab.example.net(**不是** example.net)。
|
||||
**严禁**把带子域的主机名缩成根域名——看到 xxx.example.com 就登记整个 example.com 会把范围扩到用户目标之外,违背最小范围原则。
|
||||
|
||||
@@ -119,7 +119,6 @@ func (m *MainAgent) Chat(ctx context.Context, taskID int64, mainSeg int, as *db.
|
||||
base := append(tsx.DropCoverageTools(tsx.MainAgentTools()), actool.DefaultTools()...)
|
||||
ctx = WithRunInfo(ctx, RunInfo{TaskID: taskID, ExplorationID: explorationID(ts)})
|
||||
tools, def, cleanup := AugmentTools(ctx, "mainagent", base)
|
||||
tools = tsx.StripCoverageParams(tools) // 覆盖度关闭时隐藏 insert_assets 的 related 入参
|
||||
defer cleanup()
|
||||
// 本任务的工作目录 <workDir>/tasks/<taskID>,先建好。
|
||||
mainDir := ensureRunDir(m.workDir, taskID, 0)
|
||||
|
||||
+10
-58
@@ -180,9 +180,11 @@ func NewToolSet(ts *db.ExplorationStore, worker string) *ToolSet {
|
||||
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.
|
||||
// (default enabled). Passing false makes graphOverviewData omit the coverage block
|
||||
// and DropCoverageTools filter the two coverage-only tools out of the agent's tool
|
||||
// list. It does NOT stop scope accumulation: insertAssets' auto-scope hook runs
|
||||
// either way, because task_scope is the task's range boundary (the filter basis for
|
||||
// asset queries), not merely a coverage denominator.
|
||||
func (t *ToolSet) SetCoverageEnabled(enabled bool) { t.coverageDisabled = !enabled }
|
||||
|
||||
// CoverageDisabled reports whether the coverage feature is off for this task.
|
||||
@@ -191,7 +193,11 @@ 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}
|
||||
// add_task_scope is deliberately NOT here: task_scope is the task's range boundary
|
||||
// (the filter basis for asset queries), not merely a coverage denominator, so the
|
||||
// agents that own范围定义 keep it either way — in lockstep with insertAssets'
|
||||
// auto-scope hook, which also runs regardless of the switch.
|
||||
var coverageOnlyTools = map[string]bool{"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.
|
||||
@@ -209,60 +215,6 @@ func (t *ToolSet) DropCoverageTools(tools []actool.CoreTool) []actool.CoreTool {
|
||||
return out
|
||||
}
|
||||
|
||||
// StripCoverageParams hides coverage-only parameters (currently insert_assets'
|
||||
// per-item `related`, which only decides whether an asset enters the coverage
|
||||
// denominator) from the model-facing schema when coverage is disabled — the param
|
||||
// has no effect then, so showing it just pollutes the prompt. MUST run on the FINAL
|
||||
// tool list (after AugmentTools/ToolResolve): the DB tools table is authoritative on
|
||||
// schema, so stripping the code schema earlier would be overwritten. No-op when
|
||||
// enabled or when the list has no insert_assets. The schema is deep-copied before
|
||||
// editing so the shared/DB schema map is never mutated.
|
||||
func (t *ToolSet) StripCoverageParams(tools []actool.CoreTool) []actool.CoreTool {
|
||||
if !t.coverageDisabled {
|
||||
return tools
|
||||
}
|
||||
for i, tool := range tools {
|
||||
if tool.Name() != "insert_assets" {
|
||||
continue
|
||||
}
|
||||
schema := deepCopyJSONMap(tool.InputSchema())
|
||||
if props, ok := nestedMap(schema, "properties", "assets", "items", "properties"); ok {
|
||||
delete(props, "related")
|
||||
}
|
||||
tools[i] = DecorateTool(tool, tool.Description(), schema)
|
||||
}
|
||||
return tools
|
||||
}
|
||||
|
||||
// deepCopyJSONMap returns a JSON round-trip deep copy of a schema map so callers can
|
||||
// edit it without touching the original (which may be shared/cached). Falls back to
|
||||
// the input on any marshal error (edits then become best-effort no-ops upstream).
|
||||
func deepCopyJSONMap(m map[string]any) map[string]any {
|
||||
b, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return m
|
||||
}
|
||||
var out map[string]any
|
||||
if json.Unmarshal(b, &out) != nil {
|
||||
return m
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// nestedMap walks a chain of string keys through nested map[string]any values,
|
||||
// returning the final map and whether the whole path resolved to one.
|
||||
func nestedMap(m map[string]any, keys ...string) (map[string]any, bool) {
|
||||
cur := m
|
||||
for _, k := range keys {
|
||||
next, ok := cur[k].(map[string]any)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
cur = next
|
||||
}
|
||||
return cur, true
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
+4
-17
@@ -63,13 +63,6 @@ type assetInputItem struct {
|
||||
// ---- endpoint ----
|
||||
Method string `json:"method"`
|
||||
Params []map[string]any `json:"params"`
|
||||
|
||||
// Related marks whether this asset is relevant to the CURRENT test task and
|
||||
// should therefore auto-enter the coverage system (task_scope denominator).
|
||||
// nil = default true (relevant → auto-scoped). false = insert into the shared
|
||||
// asset库 but keep it OUT of this task's coverage. Only consulted when the task
|
||||
// has the coverage feature enabled; ignored (no auto-scope either way) when off.
|
||||
Related *bool `json:"related"`
|
||||
}
|
||||
|
||||
// insertAssets is the unified insert_assets agent tool.
|
||||
@@ -151,11 +144,6 @@ func (t *ToolSet) insertAssets() actool.CoreTool {
|
||||
"description": "请求参数列表,每条含 location(query/body/header/path)/name/value/type(可选,追加不覆盖)",
|
||||
"items": map[string]any{"type": "object"},
|
||||
},
|
||||
// 覆盖度相关:该资产是否与当前测试任务有关。
|
||||
"related": map[string]any{
|
||||
"type": "boolean",
|
||||
"description": "该资产是否与【当前测试任务】相关:true(默认)才自动纳入资产覆盖度(测试范围分母),该资产会进入待测资产中,如果是和任务无关的,例如CDN仅存储静态资源类,必须设置为false或不进行资产插入;false 则只入库、不计入本任务覆盖度(如顺带发现的旁站/无关资产)。仅在任务开启资产覆盖度功能时生效。",
|
||||
},
|
||||
}, "type"),
|
||||
},
|
||||
}, "assets"),
|
||||
@@ -292,10 +280,9 @@ func (t *ToolSet) insertAssets() actool.CoreTool {
|
||||
}
|
||||
// 自动入测试范围(source='auto'):只对 worker 顶层显式插入的这一项,按其
|
||||
// 类型加保守范围;side-effect 派生的资产不经此处,故范围不盲目扩大。taskID=0 时无操作。
|
||||
// 资产覆盖度功能关闭时不再累积测试范围(分母);related=false(与当前任务无关)
|
||||
// 的资产也只入库、不计入覆盖度。related 省略/null 视为 true(默认纳入)。
|
||||
related := item.Related == nil || *item.Related
|
||||
if !t.coverageDisabled && related {
|
||||
// 与覆盖度开关无关:task_scope 是任务的范围边界(list/查询的过滤基准),
|
||||
// 覆盖度开关只决定要不要把它当分母去算指标,不决定要不要累积范围本身。
|
||||
{
|
||||
svcIP := item.ServiceIP
|
||||
if svcIP == "" {
|
||||
svcIP = item.IP
|
||||
@@ -370,7 +357,7 @@ func (t *ToolSet) addCompanyScope() actool.CoreTool {
|
||||
func (t *ToolSet) addTaskScope() actool.CoreTool {
|
||||
return writeTool(
|
||||
"add_task_scope",
|
||||
"把测试范围加入【本任务】——这是资产测试覆盖度的分母,也是本任务的授权边界。\n"+
|
||||
"把测试范围加入【本任务】——这是本任务的授权边界,也是资产测试覆盖度的分母。\n"+
|
||||
"kind 支持:company(整个公司名下资产) / root_domain(整个根域,含所有子域) / subdomain(单个精确子域) / ip / cidr / icp / keyword。\n"+
|
||||
"说明:worker 逐个碰到的主机会被系统【自动】加进范围(精确子域);本工具用于【主动扩大】——把整个根域/整个公司纳入,或补充指定某子域/IP。\n"+
|
||||
"value:company 传公司名或 id(公司须已存在);root_domain/subdomain 传域名;ip/cidr 传 IP 或网段;icp/keyword 传备案号或企业关键词。\n"+
|
||||
|
||||
@@ -358,7 +358,6 @@ func (w *Worker) execute(ctx context.Context, name string, taskID int64, as *db.
|
||||
base = append(base, defaultToolsExcept("MultiEdit", "Glob", "Grep")...)
|
||||
ctx = WithRunInfo(ctx, RunInfo{TaskID: taskID, ExplorationID: explorationID(ts), IntentID: intent.ID})
|
||||
tools, def, cleanup := AugmentTools(ctx, "worker", base)
|
||||
tools = tsx.StripCoverageParams(tools) // 覆盖度关闭时隐藏 insert_assets 的 related 入参
|
||||
defer cleanup()
|
||||
|
||||
// 意图是 worker 的【唯一职责、贯穿整个 run 的不变量】→ 连同启动指令、意图锚定的目标资产
|
||||
|
||||
Reference in new issue
Block a user