- prompt: 新增全局运行时变量 {{.DataDir}}(服务端数据根目录 s.m.dir),
与 {{.Now}} 一样对所有 agent(planner/worker/mainagent/goals/chat 及自定义)可用;
各 Vars 结构体加 DataDir 字段并在渲染时填入根 workDir,globalPromptVars 登记以便
校验/UI 变量列表/预览识别(纯运行时变量,不入库,无需迁移)。
- trigger_merge_mode 默认 by_task→all(schema/config/后端兜底/前端初值同步)。
- seed_first_intent 默认 true→false(走标准先规划再执行);spawn_task 不再暴露该参数,
工具建任务一律 false。
139 lines
6.8 KiB
Go
139 lines
6.8 KiB
Go
package agent
|
||
|
||
import (
|
||
"context"
|
||
"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"
|
||
)
|
||
|
||
// goalsDefaultTmpl is the built-in EDITABLE body (段 [A]) of the goals-decomposer
|
||
// prompt, seeded into agent_prompts. No template vars are used today.
|
||
const goalsDefaultTmpl = `你是渗透测试目标分解器。你的职责是从用户输入中识别出**最终要达成的结果**,而不是规划攻击步骤。
|
||
|
||
**目标 = 最终可交付/可核验的结果**
|
||
|
||
**不是目标的内容(禁止列为子目标)**:
|
||
- 信息收集、侦察、端点扫描
|
||
- 漏洞分析与验证过程
|
||
- 攻击步骤、利用手段
|
||
- 结果验证步骤
|
||
|
||
**拆分原则**:
|
||
- 用户描述的最终目标只有一个 → 输出一个
|
||
- 存在多个**相互独立**的最终交付物 → 分别列出
|
||
- 能对应明确漏洞类的标注 vulnclass;信息收集/业务逻辑类目标留空
|
||
- 严禁臆造用户未提及的目标
|
||
|
||
调用 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 登记(资产测试覆盖度的分母,也是授权边界)。**最小范围原则:只登记用户明确点到的那一个目标,绝不擅自放大。**
|
||
- 目标是 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 会把范围扩到用户目标之外,违背最小范围原则。
|
||
- 仅当用户给的就是**裸根域名、且不含任何子域**(如直接写 example.com),或明确说“整个站点 / 所有子域 / 全域名” → 才用 kind=root_domain,value=example.com。
|
||
- 纯 IP 或网段 → kind=ip / cidr,value=IP 或 CIDR。
|
||
- **不要**登记公司范围(company)——任务刚建立、资产系统里通常还没有这家公司,登记不上,公司级范围交由后续 plan 阶段处理。
|
||
其它规则:
|
||
- 只登记**目标/描述里明确写出**的范围;严禁臆造或推断未提及的域名/IP。
|
||
- reason 简述依据来自哪句话,便于审计。
|
||
- 若目标/描述中没有任何明确资产范围,则**不要**调用 add_task_scope。
|
||
先用 add_task_scope 登记范围(如有),再调用 set_goals 提交目标。`
|
||
|
||
// GoalSpec is one decomposed objective.
|
||
type GoalSpec struct {
|
||
Text string `json:"text"`
|
||
VulnClass string `json:"vulnclass,omitempty"`
|
||
}
|
||
|
||
// DecomposeGoals asks the LLM to break a pentest task goal into discrete,
|
||
// independently-verifiable objectives (each becomes a goal node). Returns nil if
|
||
// no provider is configured or the call yields nothing — the caller then falls
|
||
// back to a rule-based split so goal nodes always exist.
|
||
//
|
||
// desc is the task's free-text description (背景:靶标范围/flag 数量/交战说明等).
|
||
// It is fed alongside the goal so the decomposer no longer splits blind — the
|
||
// prompt still forbids inventing anything the two texts don't state.
|
||
//
|
||
// 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.
|
||
//
|
||
// 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, dataDir, goalText, desc string, as *db.AssetStore, taskID int64, emit func(db.Activity)) []GoalSpec {
|
||
prov, err := c.NewProvider()
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
var out struct {
|
||
Goals []GoalSpec `json:"goals"`
|
||
}
|
||
captured := false
|
||
submitTool := actool.Build(actool.Spec{
|
||
Name: "set_goals",
|
||
Description: "提交拆分后的目标列表。",
|
||
Schema: map[string]any{"type": "object", "properties": map[string]any{
|
||
"goals": map[string]any{"type": "array", "items": map[string]any{
|
||
"type": "object", "properties": map[string]any{
|
||
"text": map[string]any{"type": "string", "description": "一个独立可验证的目标"},
|
||
"vulnclass": map[string]any{"type": "string", "description": "对应漏洞类(若明确),如 SQLi/IDOR;业务逻辑目标可留空"},
|
||
}, "required": []string{"text"},
|
||
}},
|
||
}, "required": []string{"goals"}},
|
||
Permissions: func(context.Context, json.RawMessage, acperm.Context) acperm.Decision { return acperm.Allowed() },
|
||
Run: func(_ context.Context, in json.RawMessage, _ *actool.ToolContext) (actool.Result, error) {
|
||
_ = json.Unmarshal(in, &out)
|
||
captured = true
|
||
return actool.Text("ok"), nil
|
||
},
|
||
})
|
||
// Description rides in the user message (same channel as the goal), NOT via the
|
||
// {{.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{DataDir: dataDir, Now: nowStr()})
|
||
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
|
||
}
|
||
// Use captureRun so every LLM step is emitted as an activity record (visible in
|
||
// the plan tab under the round-0 marker). Falls back gracefully when emit is nil.
|
||
captureEmit := func(r db.Activity) {
|
||
if emit != nil {
|
||
r.Worker = "planner"
|
||
emit(r)
|
||
}
|
||
}
|
||
captureRun(ctx, agentcore.Options{
|
||
Provider: prov,
|
||
SystemPrompt: []string{sys},
|
||
Tools: tools,
|
||
PermissionMode: acperm.ModeBypass,
|
||
DisableBackgroundTasks: true,
|
||
MaxTurns: 6,
|
||
}, userMsg, captureEmit)
|
||
if !captured {
|
||
return nil
|
||
}
|
||
return out.Goals
|
||
}
|