chore: sync upstream v0.3.6 changes

This commit is contained in:
pphua committed 2026-08-28 14:03:07 +08:00
1 parent 9da521ee4e
commit 22132a4d21
34 files changed
+897 -127

No files matched your search

+1 -1
View File
@@ -326,7 +326,7 @@ func TestBlackboardToolsReadDirectSources(t *testing.T) {
t.Fatalf("inherited host context missing: %#v", coverage)
}
facts := callReadJSON(t, tools.listFacts(), `{}`).([]any)
facts := callReadJSON(t, tools.listFacts(), `{}`).(map[string]any)["facts"].([]any)
seenSource, seenGrand := false, false
for _, raw := range facts {
item := raw.(map[string]any)
+11
View File
@@ -19,6 +19,17 @@ func (p captureUsageProvider) Stream(ctx context.Context, _ llm.CompletionReques
return func(yield func(llm.StreamEvent, error) bool) { p.stream(ctx, yield) }
}
func (p captureUsageProvider) Complete(ctx context.Context, req llm.CompletionRequest) (llm.Message, string, llm.Usage, error) {
acc := llm.NewAccumulator()
for ev, err := range p.Stream(ctx, req) {
if err != nil {
return llm.Message{}, "", llm.Usage{}, err
}
acc.Add(ev)
}
return acc.Message(), acc.StopReason, acc.Usage, nil
}
func TestCaptureRunPersistsUsageOnProviderFailure(t *testing.T) {
wantErr := errors.New("provider failed after reporting usage")
provider := captureUsageProvider{stream: func(_ context.Context, yield func(llm.StreamEvent, error) bool) {
+26 -18
View File
@@ -6,13 +6,13 @@ import (
"path/filepath"
"time"
"github.com/Autumn-27/artex/db"
"github.com/Autumn-27/artex/guard"
"github.com/Autumn-27/norma/agentcore"
"github.com/Autumn-27/norma/llm"
"github.com/Autumn-27/norma/permission"
actool "github.com/Autumn-27/norma/tool"
"github.com/Autumn-27/norma/transcript"
"github.com/Autumn-27/artex/db"
"github.com/Autumn-27/artex/guard"
)
// ChatAgent is the generic, task-independent conversational runner behind the chat
@@ -22,21 +22,28 @@ import (
// LS/Glob/Grep) plus whatever skills/MCP the key is made visible; NO pentest
// graph/task context is injected (that stays exclusive to MainAgent).
type ChatAgent struct {
prov llm.Provider
model string
workDir string
tx *transcript.Store
window int
proxyAddr string
proxyCACert string
webSearch WebSearchOpts
guard *guard.Guard // optional; nil disables intercept hooks for chat
prov llm.Provider
model string
workDir string
tx *transcript.Store
window int
proxyAddr string
proxyCACert string
webSearch WebSearchOpts
guard *guard.Guard // optional; nil disables intercept hooks for chat
nonStreamingFn func() bool // resolver: use non-streaming (Complete) path? (nil = streaming)
}
func NewChatAgent(prov llm.Provider, model, workDir string, tx *transcript.Store, window int) *ChatAgent {
return &ChatAgent{prov: prov, model: model, workDir: workDir, tx: tx, window: window}
}
// SetNonStreaming wires a resolver deciding whether chat runs use the
// non-streaming model path (true = non-streaming). nil/unset = streaming.
func (c *ChatAgent) SetNonStreaming(fn func() bool) { c.nonStreamingFn = fn }
func (c *ChatAgent) nonStreaming() bool { return c.nonStreamingFn != nil && c.nonStreamingFn() }
// SetProxy points the chat agent's WebFetch/Bash at the recording proxy plus the
// CA cert it trusts (empty addr = direct). Kept for parity with the other agents.
func (c *ChatAgent) SetProxy(addr, caCert string) { c.proxyAddr, c.proxyCACert = addr, caCert }
@@ -115,18 +122,19 @@ func (c *ChatAgent) Chat(ctx context.Context, agentKey, sessionID, message strin
BraveSearchAPIKey: ws.BraveKey,
TavilySearchAPIKey: ws.TavilyKey,
WebSearchProxy: ws.Proxy,
BashEnv: proxyEnv(c.proxyAddr, c.proxyCACert), // Bash 子命令默认走代理+信任 CA
WorkingDir: sessionWorkDir,
MaxTurns: maxTurns,
MaxDuration: maxDuration,
Compaction: compactionConfig(c.window),
Todos: actool.NewTodoStore(),
BashEnv: proxyEnv(c.proxyAddr, c.proxyCACert), // Bash 子命令默认走代理+信任 CA
WorkingDir: sessionWorkDir,
MaxTurns: maxTurns,
MaxDuration: maxDuration,
Compaction: compactionConfig(c.window),
Todos: actool.NewTodoStore(),
// large tool output spills to cmd-output/ under the session dir.
// 截断上限用 SDK 默认(tool.Capture 的 30000 字符)。
ToolOutputDir: filepath.Join(sessionWorkDir, "cmd-output"),
// 命中预算(步数)→ SDK 跑收尾:输出一句总结。Prompt 与收尾轮数按本 agent key 后台可编辑
// (自定义 agent 各自一份;留空/0 用通用默认:10 轮)。
Settlement: wrapupSettlement(agentKey, nil),
Settlement: wrapupSettlement(agentKey, nil),
NonStreaming: c.nonStreaming(), // 该 profile 选非流式时走 Provider.Complete
}
if c.guard != nil {
opts.Hooks = c.guard.Hooks()
+4 -3
View File
@@ -98,13 +98,13 @@ func DecomposeGoals(ctx context.Context, prov llm.Provider, dataDir, goalText, d
if prov == nil {
return nil
}
return DecomposeGoalsWithProvider(ctx, prov, dataDir, goalText, desc, as, ts, taskID, emit)
return DecomposeGoalsWithProvider(ctx, prov, dataDir, goalText, desc, as, ts, taskID, false, emit)
}
// DecomposeGoalsWithProvider is the task-runtime variant used when a task has an
// ordered provider chain. It preserves the same tools and write behavior while
// letting the caller own provider selection/failover.
func DecomposeGoalsWithProvider(ctx context.Context, prov llm.Provider, dataDir, goalText, desc string, as *db.AssetStore, ts *db.ExplorationStore, taskID int64, emit func(db.Activity)) []GoalSpec {
func DecomposeGoalsWithProvider(ctx context.Context, prov llm.Provider, dataDir, goalText, desc string, as *db.AssetStore, ts *db.ExplorationStore, taskID int64, nonStreaming bool, emit func(db.Activity)) []GoalSpec {
if prov == nil {
return nil
}
@@ -145,7 +145,8 @@ func DecomposeGoalsWithProvider(ctx context.Context, prov llm.Provider, dataDir,
PermissionMode: acperm.ModeBypass,
DisableBackgroundTasks: true,
// 3 步(抽约束 → 登记范围 → 拆目标)各需一次工具调用,给足回合避免收尾前漏调 set_goals。
MaxTurns: 8,
MaxTurns: 8,
NonStreaming: nonStreaming, // 该 profile 选非流式时走 Provider.Complete
}, userMsg, captureEmit)
// set_goals persisted the goals directly; read them back so the caller sees what
// was written (empty slice ⇒ the LLM produced nothing ⇒ caller falls back).
+20 -12
View File
@@ -17,19 +17,26 @@ 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)
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)
}
// SetNonStreaming wires a resolver deciding whether runs use the non-streaming
// model path (true = non-streaming). nil/unset = streaming (default).
func (m *MainAgent) SetNonStreaming(fn func() bool) { m.nonStreamingFn = fn }
func (m *MainAgent) nonStreaming() bool { return m.nonStreamingFn != nil && m.nonStreamingFn() }
func NewMainAgent(prov llm.Provider, model, workDir string, tx *transcript.Store, window, maxTurns int) *MainAgent {
return &MainAgent{prov: prov, model: model, workDir: workDir, tx: tx, window: window, maxTurns: maxTurns}
}
@@ -128,7 +135,8 @@ func (m *MainAgent) Chat(ctx context.Context, taskID int64, as *db.AssetStore, t
Compaction: compactionConfig(m.compactionWindow()), // long chats stay within the window
Todos: actool.NewTodoStore(), // 会话级临时待办(TodoWrite),纯规划用,退出即丢
// 命中预算(步数)→ SDK 跑收尾:向用户输出一句进展总结。Prompt 与收尾轮数可后台编辑(默认 10 轮)。
Settlement: wrapupSettlement("mainagent", nil),
Settlement: wrapupSettlement("mainagent", nil),
NonStreaming: m.nonStreaming(), // 该 profile 选非流式时走 Provider.Complete
}
if m.tx != nil { // persist raw human↔AI conversation; one accumulating file per task
opts.Transcript = m.tx
+23 -15
View File
@@ -20,19 +20,20 @@ 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 {
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)
killWork func(intentID int64) error // engine callback to terminate a running work (nil = off)
steerWork func(intentID int64, msg string) error // engine callback to steer a running work mid-run (nil = off)
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)
injectConstraints func() bool // resolver: inject task operation constraints into system prompt? (nil = yes)
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)
killWork func(intentID int64) error // engine callback to terminate a running work (nil = off)
steerWork func(intentID int64, msg string) error // engine callback to steer a running work mid-run (nil = off)
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)
injectConstraints func() bool // resolver: inject task operation constraints into system prompt? (nil = yes)
nonStreamingFn func() bool // resolver: use non-streaming (Complete) path? (nil = streaming)
// todos keeps ONE plan-scratchpad per task (keyed by exploration id) so the
// planner's multi-step plan survives across wake-ups — each Plan() is a fresh
@@ -48,6 +49,12 @@ func NewPlanner(prov llm.Provider, model, workDir string, tx *transcript.Store,
func (p *Planner) SetCompactionWindowResolver(fn func() int) { p.windowFn = fn }
// SetNonStreaming wires a resolver deciding whether runs use the non-streaming
// model path (true = non-streaming). nil/unset = streaming (default).
func (p *Planner) SetNonStreaming(fn func() bool) { p.nonStreamingFn = fn }
func (p *Planner) nonStreaming() bool { return p.nonStreamingFn != nil && p.nonStreamingFn() }
func (p *Planner) compactionWindow() int {
if p.windowFn != nil {
return p.windowFn()
@@ -238,7 +245,7 @@ const plannerDefaultTmpl = `你是一个授权渗透测试系统的"规划者"
⚠️ 最重要的原则:**这一轮生成 0 个意图是完全正常、而且是最常见的结果。** 你不是"每次都要产出意图"的机器。绝大多数唤醒,frontier 里已有的意图已经覆盖了所有已知方向,你应当什么都不加、直接结束。重复/换措辞地生成已经存在的意图是严重错误。
决策流程(每次唤醒):
1. **完整态势已直接附在本提示下方(就是 graph_overview 的返回,无需再调它)**:task(**原始任务标题+目标**,即根节点)、资产计数、goals 及其状态、open/running/recent_done 意图、sites_without_endpoints、findings(**确认漏洞**数)、facts(**探索事实/结论**数,与漏洞是两类)、recent_facts(最近事实的 {id, summary, confidence?},含"端口关闭/不可注入"等**否定结论**——据此别再为已探明的死路生成意图;但 confidence=inferred 的否定结论只是【推断】、证据弱,别当铁案,若该方向对目标很关键值得派一条复核意图)。**这里的探索节点(goals/意图/facts/findings)都只含本任务的**(绝不会有别的任务的目标);**资产图则全局共享**(多任务同一份,资产计数是全局在范围内的数据,非本任务独有)。需要更深的细节时才按需调:list_facts 列全部事实、list_findings 列全部漏洞、**node_detail(id)** 取某条的完整证据/详情(列表/recent_facts 只给摘要)。
1. **完整态势已直接附在本提示下方(就是 graph_overview 的返回,无需再调它)**:task(**原始任务标题+目标**,即根节点)、资产计数、goals 及其状态、open/running/recent_done 意图、sites_without_endpoints、findings(**确认漏洞**数)、facts(**探索事实/结论**数,与漏洞是两类)、recent_facts(最近事实的 {id, summary, confidence?},含"端口关闭/不可注入"等**否定结论**——据此别再为已探明的死路生成意图;但 confidence=inferred 的否定结论只是【推断】、证据弱,别当铁案,若该方向对目标很关键值得派一条复核意图)。**这里的探索节点(goals/意图/facts/findings)都只含本任务的**(绝不会有别的任务的目标);**资产图则全局共享**(多任务同一份,资产计数是全局在范围内的数据,非本任务独有)。需要更深的细节时才按需调:list_facts 分页列事实(最新在前,默认 20 条,可传 q 关键词过滤、before 翻页,返回带 total/has_more)、list_findings 列全部漏洞、**node_detail(id)** 取某条的完整证据/详情(列表/recent_facts 只给摘要)。
- open_intents / running_intents / recent_done_intents——"哪些方向已经有意图在覆盖/已尝试"。每个意图还带 **parents(上游:它派生自哪些事实/意图)和 yields(下游:它产生了哪些事实/发现)**——这就是探索图的**血缘关系**,据此理解"哪些事实来自哪个方向、能否综合成新方向"。recent_facts 里每个事实带 **from_intent**(由哪个意图产生)。
- sites_without_endpoints / findings——"哪些方向【可能】需要探索"。
- 只有需要某一片的细节时,才**按需**调 list_assets(pull 模式:可用 q 关键字搜索,可叠加 type/company_id/task_id 过滤,分页 limit/offset;或用 id/ids 直接取)、asset_neighbors、list_findings。资产图全局共享,别默认拉全量。资产中可能包含非本次任务涉及到的资产,所以需要主要出现非本次任务相关的资产时忽略这些资产。
@@ -353,7 +360,8 @@ func (p *Planner) Plan(ctx context.Context, taskID int64, as *db.AssetStore, ts
// 命中【本轮】步数预算→ SDK 跑收尾:把本轮已想清楚的结论落地(该派的 add_intent、
// 能证的 prove_goal、串行链记 TodoWrite),而非停止规划——planner 之后仍会被反复唤醒。
// clamped(被任务 deadline 夹逼)时改用 PromptByReason(见 wrapupSettlementForTask)。
Settlement: settle,
Settlement: settle,
NonStreaming: p.nonStreaming(), // 该 profile 选非流式时走 Provider.Complete
}
if p.tx != nil { // persist raw LLM conversation; one accumulating file per task's planner
opts.Transcript = p.tx
+36 -5
View File
@@ -51,6 +51,11 @@ type Config struct {
// "" = 不发送(默认); "low"/"medium"/"high"/"xhigh"/"max" = 对应强度.
// OpenAI 映射为顶层 reasoning_effort;Anthropic 映射为 output_config.effort.
ReasoningEffort string
// Stream 控制该 profile 是否使用流式(SSE)接口。true(默认)= 流式;false = 真·
// 非流式(发 stream:false,一次性拿完整 JSON,走 Provider.Complete)。非流式可绕开
// 某些网关糟糕的 SSE 实现(空帧、思考字段丢帧),代价是失去运行中的实时进度/实时
// token 计数。映射为 agentcore.Options.NonStreaming = !Stream。
Stream bool
}
// compaction window resolution bounds (in K tokens). Below the floor the
@@ -116,6 +121,8 @@ func FromEnv() (Config, bool) {
BaseURL: os.Getenv("ARTEX_LLM_BASE_URL"),
Model: os.Getenv("ARTEX_LLM_MODEL"),
Proxy: strings.TrimSpace(os.Getenv("ARTEX_LLM_PROXY")),
// 默认流式;ARTEX_LLM_STREAM=false/0/off 显式关闭走非流式。
Stream: !isFalsy(os.Getenv("ARTEX_LLM_STREAM")),
}
switch prov {
case "openai":
@@ -147,6 +154,7 @@ func ConfigFrom(provider, model, baseURL, apiKey, proxy string) Config {
BaseURL: strings.TrimRight(strings.TrimSpace(baseURL), "/"),
APIKey: strings.TrimSpace(apiKey),
Proxy: strings.TrimSpace(proxy),
Stream: true, // 默认流式;调用方按 profile 覆盖
}
switch strings.TrimSpace(provider) {
case "openai":
@@ -167,6 +175,16 @@ func ConfigFrom(provider, model, baseURL, apiKey, proxy string) Config {
return c
}
// isFalsy reports whether an env-var string explicitly requests "off". Empty or
// unrecognized → false (so an unset var keeps the streaming default).
func isFalsy(s string) bool {
switch strings.ToLower(strings.TrimSpace(s)) {
case "0", "false", "off", "no":
return true
}
return false
}
// Provider returns the short provider name ("anthropic"/"openai").
func (c Config) Provider() string {
if c.Format == llm.FormatOpenAI {
@@ -288,11 +306,12 @@ func quotaAwareHTTPClient(proxy string) (*http.Client, error) {
}
// TestConnection makes a minimal real completion to verify the provider/model/
// endpoint/key actually work. Returns the round-trip latency.
func TestConnection(ctx context.Context, c Config) (time.Duration, error) {
// endpoint/key actually work. Returns the round-trip latency and the model's
// reply text.
func TestConnection(ctx context.Context, c Config) (time.Duration, string, error) {
prov, err := c.NewProvider()
if err != nil {
return 0, err
return 0, "", err
}
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
@@ -302,12 +321,24 @@ func TestConnection(ctx context.Context, c Config) (time.Duration, error) {
// 就撞到输出上限(finish=length)、被截断,连接测试虽仍算通(err=nil)但显示成
// "已中断/length/resume" 一团糟。给足预算让它把 OK 干净吐完(finish=stop)。
// EscalateMaxTokens 保持 false:不因截断而抬额重试,避免 resume 循环空烧。
_, err = agentcore.Run(ctx, agentcore.Options{
reply, err := agentcore.Run(ctx, agentcore.Options{
Provider: prov,
SystemPrompt: []string{"你是连接测试。直接输出两个字符 OK 即可,不要思考、不要解释、不要别的。"},
PermissionMode: acperm.ModeBypass,
MaxTurns: 1,
MaxTokens: 8192,
NonStreaming: !c.Stream, // 用该 profile 的真实收发模式做连接测试
}, "ping")
return time.Since(start), err
lat := time.Since(start)
if err != nil {
return lat, "", err
}
// err==nil 还不够:请求通了但模型一个字都不吐的情况真实存在(思考把预算烧光、
// 正文被安全策略吞掉、兼容层把 content 丢了)。这种配置在会话里就是"不回话",
// 测试却报成功——正是本项要消除的落差。没有可见正文一律判失败。
reply = strings.TrimSpace(reply)
if reply == "" {
return lat, "", fmt.Errorf("模型无回复内容(请求已通,但未返回任何文本)")
}
return lat, reply, nil
}
+51 -6
View File
@@ -898,19 +898,64 @@ func (t *ToolSet) listFindings() actool.CoreTool {
})
}
// factsPageSize is the default page size for list_facts. Facts pile up on long
// tasks; returning all of them at once (the old behaviour) could blow up the
// context, so default to the newest page and let the agent page/filter for more.
const factsPageSize = 20
func (t *ToolSet) listFacts() actool.CoreTool {
return readTool("list_facts", "列本任务及直接关联任务的【探索事实/结论】(紧凑:id+摘要+状态)。关联任务条目带 source_task_id/inherited=true 且只读。详情用 node_detail(id),漏洞看 list_findings。",
obj(map[string]any{}),
func(context.Context, json.RawMessage) (actool.Result, error) {
f, _ := t.ts.ListByKindWithSources(db.KindFact, 500)
return readTool("list_facts", "分页列本任务及直接关联任务的【探索事实/结论】,最新在前(紧凑:id+摘要+状态,摘要过长会截断,全文用 node_detail(id))。参数均可选:limit(默认 20,上限 100)、before(游标,传上一页返回的 next_before 取更旧的一页;省略/0=最新一页)、q(按摘要关键词过滤)。返回 {facts, total, has_more, next_before}:total 是过滤后的总数,has_more=true 时用 next_before 继续翻页。关联任务条目带 source_task_id/inherited=true 且只读。漏洞看 list_findings。",
obj(map[string]any{
"limit": intp("返回条数,默认 20,上限 100"),
"before": intp("分页游标:只返回 id 小于该值的更旧事实;省略或 0 = 最新一页"),
"q": str("按事实摘要关键词过滤(不区分大小写);省略 = 不过滤"),
}),
func(_ context.Context, in json.RawMessage) (actool.Result, error) {
var a struct {
Limit int `json:"limit"`
Before int64 `json:"before"`
Q string `json:"q"`
}
_ = json.Unmarshal(in, &a)
limit := a.Limit
if limit <= 0 {
limit = factsPageSize
}
if limit > 100 {
limit = 100
}
f, hasMore, total, err := t.ts.ListByKindPageWithSources(db.KindFact, a.Before, limit, strings.TrimSpace(a.Q))
if err != nil {
return actool.Result{}, err
}
out := make([]map[string]any, 0, len(f))
for _, n := range f {
out = append(out, compactNode(n))
out = append(out, compactFact(n))
}
return jsonResult(out)
res := map[string]any{"facts": out, "total": total, "has_more": hasMore}
if hasMore && len(f) > 0 {
res["next_before"] = f[len(f)-1].ID // 传回它取下一页(更旧的)
}
return jsonResult(res)
})
}
// factSummaryMax caps a fact summary in list_facts output. Facts carry one-line
// conclusions, but nothing enforces brevity; a runaway summary must not bloat a
// whole page. Full text stays available via node_detail(id).
const factSummaryMax = 160
// compactFact is compactNode with the summary rune-capped for list_facts, so a
// page of facts stays bounded regardless of how long any single summary grew.
func compactFact(n *db.Node) map[string]any {
m := compactNode(n)
if s, ok := m["summary"].(string); ok && len([]rune(s)) > factSummaryMax {
m["summary"] = string([]rune(s)[:factSummaryMax]) + "…"
m["summary_truncated"] = true
}
return m
}
func (t *ToolSet) nodeDetail() actool.CoreTool {
return readTool("node_detail", "按 id 取本任务或直接关联任务的【探索图节点】完整内容。继承节点带 source_task_id/inherited=true 且只读。仅限 list_facts/list_findings/graph_overview 返回的探索节点 id;资产请用 list_assets/asset_neighbors。",
obj(map[string]any{"id": idp("探索图节点 id(非资产 id)")}, "id"),
+13 -1
View File
@@ -62,8 +62,19 @@ type Worker struct {
// injected into the worker system prompt. Read per run so the settings toggle
// takes effect without rebuilding the agent. nil = inject (default).
injectConstraints func() bool
// nonStreamingFn resolves whether this run uses the non-streaming (Complete)
// path. Read per run so a profile/task toggle takes effect without rebuilding
// the agent. nil = streaming (default).
nonStreamingFn func() bool
}
// SetNonStreaming wires a resolver deciding whether runs use the non-streaming
// model path (true = non-streaming). nil/unset = streaming (default). Read per
// run so a profile or task-chain toggle takes effect without rebuilding.
func (w *Worker) SetNonStreaming(fn func() bool) { w.nonStreamingFn = fn }
func (w *Worker) nonStreaming() bool { return w.nonStreamingFn != nil && w.nonStreamingFn() }
// SetConstraintInject wires a resolver deciding whether this task's operation
// constraints get injected into the worker system prompt. nil = inject (default).
func (w *Worker) SetConstraintInject(fn func() bool) { w.injectConstraints = fn }
@@ -160,7 +171,7 @@ const workerDefaultTmpl = `你是一个授权渗透测试系统的"执行者"(wo
- insert_assets:登记新资产(资产图)。**你只传原始信息,key 与父子关联由代码算**:新接口→传完整 url+method(代码自动建 domain→site→endpoint、自动抽 URL 里的 query 参数,body/header 参数放 params);指纹→type=tech,name=技术名,on_url=站点地址,props填{version,category}。多个资产放 assets 数组一次批量登记。属性写在资产自己的 props 上,探索结论不要写这里。
- record_fact:把探索【事实/结论】写入探索图并连到意图(传 intent_id)。正向/否定结论、观察、判断用它;**一次探索的多个观察汇总成一条事实**(summary 总结一句话 + detail 放细节),不要一个属性一条。真有多条不同结论才用 facts 数组。**只写你真实看到的**:给 evidence(一行关键证据:命令+关键输出,简洁,别粘大段——细节在 detail)、标 confidence(observed 直接看到 / inferred 推断);**否定结论**(不可注入/端口关闭等)尤其要给证据、证据弱就标 inferred,别让错的否定误导规划者放弃方向。
- report_finding:确认漏洞 → 记录(含 PoC,传 intent_id=你领到的意图id)。**只有你在本次运行里真实触发过该漏洞、拿到了可复现的证据(请求/响应或命令输出)才用它。** 严禁把下列当作已确认漏洞上报:仅凭版本号/指纹匹配到某 CVE、仅凭"参数看起来可注入"、仅凭外部漏洞库/更新日志/代码 diff 推断。**不要用查 CVE 库或"对比补丁版本"替代实际触发。** 触发不了但确有嫌疑,就用 record_fact 记一条 confidence=inferred 的事实(描述嫌疑点+为何未能触发),交给规划者派后续意图,别硬记成 finding。
- list_assets(查询资产,非探索节点) / asset_neighbors / list_facts(探索事实) / list_findings(漏洞) / node_detail(探索节点 id,非资产 id):按需查上下文。
- list_assets(查询资产,非探索节点) / asset_neighbors / list_facts(探索事实,分页最新在前;默认 20 条,可传 q 关键词过滤、before 翻页) / list_findings(漏洞) / node_detail(探索节点 id,非资产 id):按需查上下文。
只在授权范围内操作;若系统提示顶部附有【操作约束】,那是最高优先级红线——任何命令/探测在执行前先自检是否违反,违反即不做(哪怕它落在你领到的意图里)。完成本意图后用一句话总结你做了什么、写回了哪些事实。务实、克制、聚焦这一条意图。`
@@ -337,6 +348,7 @@ func (w *Worker) Execute(ctx context.Context, name string, taskID int64, as *db.
ToolOutputDir: cmdOutDir(runDir),
Compaction: compactionConfig(w.compactionWindow()), // long tool-heavy runs stay within the window
Todos: actool.NewTodoStore(), // 会话级临时待办(TodoWrite),纯规划用,退出即丢
NonStreaming: w.nonStreaming(), // 该 profile 选非流式时走 Provider.Complete
}
if hooks != nil { // typed-nil guard: only set when concrete (avoids harness panic)
opts.Hooks = hooks