chore: sync upstream v0.3.6 changes
This commit is contained in:
1 parent
9da521ee4e
commit
22132a4d21
34 files changed
+897
-127
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user