feat(goals): 目标设定 agent 提取并登记测试资产范围

- DecomposeGoals 新增 as/taskID 参数,有效时复用 add_task_scope 工具挂入本次调用
- 追加 code-owned tail(仅工具挂上时):从目标/描述提取明确的
  root_domain/subdomain/ip/cidr 并登记为 task_scope;只登记明确写出的、
  拿不准选更精确的;不处理 company(任务刚建公司通常未入库、名字解析不上,
  留给 plan 阶段)
- MaxTurns 4→6;server createGoals 传入 AssetStore + 数字 taskID

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Autumn-27andClaude Opus 4.8 committed 2026-08-08 12:30:51 -04:00
1 parent c396674d2b
commit e841bc9332
2 files changed
+42 -5

No files matched your search

+35 -4
View File
@@ -5,10 +5,10 @@ import (
"encoding/json"
"strings"
"github.com/Autumn-27/artex/db"
"github.com/Autumn-27/norma/agentcore"
acperm "github.com/Autumn-27/norma/permission"
actool "github.com/Autumn-27/norma/tool"
"github.com/Autumn-27/artex/db"
)
// goalsDefaultTmpl is the built-in EDITABLE body (段 [A]) of the goals-decomposer
@@ -31,6 +31,26 @@ const goalsDefaultTmpl = `你是渗透测试目标分解器。你的职责是从
调用 set_goals 提交结果。`
// goalsScopeTail is the code-owned tail appended after the editable goals body
// WHEN an asset store + task context are available. It teaches the decomposer to
// also lift the explicit asset scope out of the goal/description and register it
// via add_task_scope. Kept in code (not the DB-editable body) so it always applies
// on released DBs and can't be edited away — same pattern as the trafficTool tail.
const goalsScopeTail = `
**额外职责:登记测试资产范围**
除拆分目标外,你还要从上面的「任务目标 / 任务描述」里识别出**明确给出的测试资产范围**,并调用 add_task_scope 登记为本任务范围(资产测试覆盖度的分母,也是授权边界):
- 完整根域名(如 example.com,其所有子域都在测试范围)→ kind=root_domain,value=example.com
- 单个精确子域名(如 app.example.com,只测这一个)→ kind=subdomain,value=app.example.com
- IP 或网段 → kind=ip / cidr,value=IP 或 CIDR
规则(务必遵守):
- 只登记**目标/描述里明确写出**的范围;严禁臆造或推断未提及的域名/IP。
- 粒度拿不准时选更**精确**的那种(宁可 subdomain,不要轻易升级为 root_domain)。
- 只处理域名 / IP / 网段;**不要**登记公司范围(company)——任务刚建立、资产系统里通常还没有这家公司,登记不上,公司级范围交由后续 plan 阶段处理。
- reason 简述依据来自哪句话,便于审计。
- 若目标/描述中没有任何明确资产范围,则**不要**调用 add_task_scope。
先用 add_task_scope 登记范围(如有),再调用 set_goals 提交目标。`
// GoalSpec is one decomposed objective.
type GoalSpec struct {
Text string `json:"text"`
@@ -48,7 +68,10 @@ type GoalSpec struct {
//
// emit, when non-nil, receives every LLM step (thinking/tool_use/result) with
// Worker="planner" so the round-0 goal-decomposition activity is visible in the UI.
func DecomposeGoals(ctx context.Context, c Config, goalText, desc string, emit func(db.Activity)) []GoalSpec {
//
// as + taskID, when non-nil/positive, wire the add_task_scope tool so the
// decomposer can register the explicit asset scope it extracts from the goal.
func DecomposeGoals(ctx context.Context, c Config, goalText, desc string, as *db.AssetStore, taskID int64, emit func(db.Activity)) []GoalSpec {
prov, err := c.NewProvider()
if err != nil {
return nil
@@ -79,6 +102,14 @@ func DecomposeGoals(ctx context.Context, c Config, goalText, desc string, emit f
// {{.EngagementDescription}} template var — else a prompt that references the var
// would inject the description twice. System prompt stays pure static instructions.
sys := renderSystem("goals", goalsDefaultTmpl, GoalsVars{})
tools := []actool.CoreTool{submitTool}
// Wire add_task_scope only when we have a real asset store + task to write to.
// The scope-extraction tail is appended in lockstep so the prompt never asks for
// a tool that isn't present.
if as != nil && taskID > 0 {
tools = append(tools, (&ToolSet{as: as, taskID: taskID}).addTaskScope())
sys += goalsScopeTail
}
userMsg := "任务目标:\n" + goalText
if d := strings.TrimSpace(desc); d != "" {
userMsg += "\n\n任务描述(背景信息,可能含靶标范围/flag 数量/交战说明;仅供参考,不要臆造其中未提及的内容):\n" + d
@@ -94,10 +125,10 @@ func DecomposeGoals(ctx context.Context, c Config, goalText, desc string, emit f
captureRun(ctx, agentcore.Options{
Provider: prov,
SystemPrompt: []string{sys},
Tools: []actool.CoreTool{submitTool},
Tools: tools,
PermissionMode: acperm.ModeBypass,
DisableBackgroundTasks: true,
MaxTurns: 4,
MaxTurns: 6,
}, userMsg, captureEmit)
if !captured {
return nil
+7 -1
View File
@@ -3,6 +3,7 @@ package server
import (
"context"
"log"
"strconv"
"strings"
"github.com/Autumn-27/artex/agent"
@@ -28,7 +29,12 @@ func (s *Server) createGoals(ctx context.Context, t *Task, emit func(db.Activity
// splitter (which shredded URLs / made meaningless 2-way splits).
var specs []goalSpec
if cfg, ok := s.goalsLLMConfig(t); ok {
for _, g := range agent.DecomposeGoals(ctx, cfg, t.Goal, t.Description, emit) {
var as *db.AssetStore
if s.m != nil {
as = s.m.Assets()
}
taskID, _ := strconv.ParseInt(t.ID, 10, 64)
for _, g := range agent.DecomposeGoals(ctx, cfg, t.Goal, t.Description, as, taskID, emit) {
if strings.TrimSpace(g.Text) != "" {
specs = append(specs, goalSpec{Text: g.Text, VulnClass: g.VulnClass})
}