mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-10-27 17:03:49 +08:00
fix(agent): 探索图工具在无任务上下文时拒绝而非 nil 崩溃
buildDomainReg 构造的 server 级 ToolSet 恒持 nil ExplorationStore(taskID=0), tools 表可把其中任一工具绑给不在任务内运行的 agent(auto/pentest/reporter/ 自定义/旁路提问)。node_detail 等探索图工具直接解引用 t.ts,被这类 agent 调用即 nil deref;panic 发生在 harness 自己的 goroutine 上,server 侧任何 recover 都 够不到,整进程退出,重启后调度器又拉起同一 agent 再崩,形成崩溃循环。 - 新增 readExpTool/writeExpTool + needExploration:t.ts==nil 时返回可读的工具 错误(提示改用带 task_id 的跨任务读取工具),把 17 个探索图工具切过去 - anchorOwner 补判空 - AugmentTools 出口给每个工具套 guardPanic:handler panic 降级成工具错误并打 stack,覆盖域工具/SDK/MCP/skill,进程不再陪葬 - 注明 buildDomainReg 的 nil-store 契约
This commit is contained in:
1 parent
1a388e3e9e
commit
5963281582
5 files changed
+147
-18
No files matched your search
@@ -2,6 +2,10 @@ package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"runtime/debug"
|
||||
|
||||
actool "github.com/Autumn-27/norma/tool"
|
||||
)
|
||||
@@ -57,5 +61,27 @@ func AugmentTools(ctx context.Context, agentKey string, base []actool.CoreTool)
|
||||
out = ToolResolve(ctx, agentKey, out)
|
||||
}
|
||||
out, def.FindingGuidance = findingWorkflowTools(agentKey, out)
|
||||
for i, t := range out {
|
||||
out[i] = guardPanic(t)
|
||||
}
|
||||
return out, def, cleanup
|
||||
}
|
||||
|
||||
// guardPanic turns a panicking tool handler into an ordinary tool error. The
|
||||
// harness runs each tool on its own goroutine, so a panic inside a handler can't
|
||||
// be recovered by the caller that started the run — it takes the whole process
|
||||
// down, and on restart the agent replays the same call and crashes again. Applied
|
||||
// last, so it covers every tool the agent can reach: domain, SDK, MCP and skill.
|
||||
func guardPanic(t actool.CoreTool) actool.CoreTool { return &guardedTool{CoreTool: t} }
|
||||
|
||||
type guardedTool struct{ actool.CoreTool }
|
||||
|
||||
func (g *guardedTool) Call(ctx context.Context, in json.RawMessage, tc *actool.ToolContext) (res actool.Result, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("[tools] %s panic: %v\n%s", g.Name(), r, debug.Stack())
|
||||
res, err = actool.Errorf(fmt.Sprintf("工具 %s 内部错误:%v(本次调用已失败,可换个参数或改用别的工具)", g.Name(), r)), nil
|
||||
}
|
||||
}()
|
||||
return g.CoreTool.Call(ctx, in, tc)
|
||||
}
|
||||
+45
-16
@@ -251,7 +251,7 @@ func (t *ToolSet) SetOwnerNode(id int64) { t.ownerNode = id }
|
||||
// (no-op if unset). Provenance only — the asset graph is global and shared, so
|
||||
// this no longer affects which assets a task can read.
|
||||
func (t *ToolSet) anchorOwner(assetID int64) {
|
||||
if t.ownerNode > 0 && assetID > 0 {
|
||||
if t.ts != nil && t.ownerNode > 0 && assetID > 0 {
|
||||
_ = t.ts.Anchor(t.ownerNode, assetID)
|
||||
}
|
||||
}
|
||||
@@ -321,6 +321,35 @@ func writeTool(name, desc string, schema map[string]any, run func(context.Contex
|
||||
})
|
||||
}
|
||||
|
||||
// readExpTool / writeExpTool build a domain tool whose handler dereferences the
|
||||
// task-bound ExplorationStore. Two ToolSets carry a nil store: the catalog's
|
||||
// seed-only shell (never called) and the server-level one behind buildDomainReg,
|
||||
// which the tools table can bind to ANY agent — including ones that never run
|
||||
// inside a task (auto/pentest/reporter/自定义 agent/旁路提问). Refusing there
|
||||
// keeps a mis-bound tool a bad tool call; without the guard it was a nil deref,
|
||||
// and tool handlers run on the harness's own goroutine, so the panic is out of
|
||||
// reach of every recover() in the server and kills the whole process.
|
||||
func (t *ToolSet) readExpTool(name, desc string, schema map[string]any, run func(context.Context, json.RawMessage) (actool.Result, error)) actool.CoreTool {
|
||||
return readTool(name, desc, schema, t.needExploration(name, run))
|
||||
}
|
||||
|
||||
func (t *ToolSet) writeExpTool(name, desc string, schema map[string]any, run func(context.Context, json.RawMessage) (actool.Result, error)) actool.CoreTool {
|
||||
return writeTool(name, desc, schema, t.needExploration(name, run))
|
||||
}
|
||||
|
||||
// needExploration wraps a handler so it only runs with an exploration store.
|
||||
// Tools that degrade more usefully than "unavailable" (report_finding points at
|
||||
// add_task_hint, set_goals/set_constraints at the task itself) keep their own
|
||||
// bespoke guard instead.
|
||||
func (t *ToolSet) needExploration(name string, run func(context.Context, json.RawMessage) (actool.Result, error)) func(context.Context, json.RawMessage) (actool.Result, error) {
|
||||
return func(ctx context.Context, in json.RawMessage) (actool.Result, error) {
|
||||
if t.ts == nil {
|
||||
return actool.Errorf(name + " 需要任务上下文(探索图):当前 agent 不在某个任务内运行,取不到任务的探索图,该工具不可用。请在任务内使用它,或改用带 task_id 的跨任务读取工具(get_task_node_detail / list_task_findings / get_task_graph 等)。"), nil
|
||||
}
|
||||
return run(ctx, in)
|
||||
}
|
||||
}
|
||||
|
||||
func jsonResult(v any) (actool.Result, error) {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
@@ -332,7 +361,7 @@ func jsonResult(v any) (actool.Result, error) {
|
||||
// --- read tools (planner + worker) ---
|
||||
|
||||
func (t *ToolSet) graphOverview() actool.CoreTool {
|
||||
return readTool("graph_overview",
|
||||
return t.readExpTool("graph_overview",
|
||||
"(探索链路图)探索态势蒸馏摘要:资产计数、无接口的站点、frontier、发现、hints(人类/主 agent 的战略提示,生成意图时须纳入)。规划时先调它。",
|
||||
obj(map[string]any{}),
|
||||
func(context.Context, json.RawMessage) (actool.Result, error) {
|
||||
@@ -1032,7 +1061,7 @@ func compactFinding(n *db.Node) map[string]any {
|
||||
}
|
||||
|
||||
func (t *ToolSet) listFindings() actool.CoreTool {
|
||||
return readTool("list_findings", "列本任务及直接关联任务的【确认漏洞】(紧凑:id+task_id+intent_id+vulnclass+severity+摘要+状态)。关联任务条目带 source_task_id/inherited=true 且只读。这里只含漏洞;普通探索事实用 list_facts,详情用 node_detail(id)。",
|
||||
return t.readExpTool("list_findings", "列本任务及直接关联任务的【确认漏洞】(紧凑:id+task_id+intent_id+vulnclass+severity+摘要+状态)。关联任务条目带 source_task_id/inherited=true 且只读。这里只含漏洞;普通探索事实用 list_facts,详情用 node_detail(id)。",
|
||||
obj(map[string]any{}),
|
||||
func(context.Context, json.RawMessage) (actool.Result, error) {
|
||||
f, _ := t.ts.ListByKindWithSources(db.KindFinding, 500)
|
||||
@@ -1070,7 +1099,7 @@ func (t *ToolSet) listFindings() actool.CoreTool {
|
||||
const factsPageSize = 20
|
||||
|
||||
func (t *ToolSet) listFacts() actool.CoreTool {
|
||||
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。",
|
||||
return t.readExpTool("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 = 最新一页"),
|
||||
@@ -1123,7 +1152,7 @@ func compactFact(n *db.Node) map[string]any {
|
||||
}
|
||||
|
||||
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。",
|
||||
return t.readExpTool("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"),
|
||||
func(_ context.Context, in json.RawMessage) (actool.Result, error) {
|
||||
var a struct {
|
||||
@@ -1207,7 +1236,7 @@ func (t *ToolSet) addOneIntent(it intentItem) (int64, error) {
|
||||
}
|
||||
|
||||
func (t *ToolSet) addIntent() actool.CoreTool {
|
||||
return writeTool("add_intent", "生成【探索方向】写入 frontier,并连入探索链路。意图是开放的探索方向,不是固定类型——用 summary 一句话自由描述要探索/验证/利用什么。\n"+
|
||||
return t.writeExpTool("add_intent", "生成【探索方向】写入 frontier,并连入探索链路。意图是开放的探索方向,不是固定类型——用 summary 一句话自由描述要探索/验证/利用什么。\n"+
|
||||
"★优先批量:一轮筛出的多个新方向放进 intents 数组一次提交(比逐条调用省往返)。返回 ids 数组,与 intents 等长同序(失败项 id=0,详情见 errors)。单条则省略 intents 直接给顶层 summary。",
|
||||
obj(map[string]any{
|
||||
"intents": map[string]any{"type": "array", "description": "【优先用这个】要新增的探索方向数组,按顺序处理。每个元素字段同下方顶层字段(summary/asset_ids/parent_ids/priority)。返回 ids 与本数组等长、同序。", "items": map[string]any{"type": "object"}},
|
||||
@@ -1264,7 +1293,7 @@ func (t *ToolSet) addIntent() actool.CoreTool {
|
||||
}
|
||||
|
||||
func (t *ToolSet) listGoals() actool.CoreTool {
|
||||
return readTool("list_goals", "列出本任务的目标节点及其状态(open/met),用于判断是否达成。",
|
||||
return t.readExpTool("list_goals", "列出本任务的目标节点及其状态(open/met),用于判断是否达成。",
|
||||
obj(map[string]any{}),
|
||||
func(context.Context, json.RawMessage) (actool.Result, error) {
|
||||
g, _ := t.ts.ListByKind(db.KindGoal, 100)
|
||||
@@ -1273,7 +1302,7 @@ func (t *ToolSet) listGoals() actool.CoreTool {
|
||||
}
|
||||
|
||||
func (t *ToolSet) proveGoal() actool.CoreTool {
|
||||
return writeTool("prove_goal", "当你判断某个发现/事实证明了某个目标达成时调用:把证据节点连到目标节点,并标记目标 met。",
|
||||
return t.writeExpTool("prove_goal", "当你判断某个发现/事实证明了某个目标达成时调用:把证据节点连到目标节点,并标记目标 met。",
|
||||
obj(map[string]any{
|
||||
"goal_id": idp("目标节点 id"),
|
||||
"evidence_id": idp("证明它的发现/事实节点 id"),
|
||||
@@ -1469,7 +1498,7 @@ func (t *ToolSet) recordOneFact(it factItem, defaultIntent int64) (int64, error)
|
||||
}
|
||||
|
||||
func (t *ToolSet) recordFact() actool.CoreTool {
|
||||
return writeTool("record_fact", "把探索【事实/结论】写入探索图,连到产生它的意图(intent_id)。用于记录探索结果——包括指纹/枚举等【正向结论】,和'端口关闭'/'参数不可注入'/'未发现登录入口'等【否定结论】。\n"+
|
||||
return t.writeExpTool("record_fact", "把探索【事实/结论】写入探索图,连到产生它的意图(intent_id)。用于记录探索结果——包括指纹/枚举等【正向结论】,和'端口关闭'/'参数不可注入'/'未发现登录入口'等【否定结论】。\n"+
|
||||
"⚠️一次探索的多个观察要【汇总成一条事实】,不要拆成多条,可以合并成一条事实的就尽量用一条事实表示:summary=对本次结论的总结性一句话,detail=相关细节(可含多个具体项)。例:指纹意图→一条事实 {summary:'识别了 X 站点的技术栈与响应特征', detail:'nginx 1.25 / Vue3 / 200 / title=.. / body_len=..'},而不是状态码、指纹、标题各记一条。一条意图通常只产出一条事实,拆太碎会让图谱无限膨胀。\n"+
|
||||
"★facts 数组用于一次写多条【彼此不同】的结论(每条可省略 intent_id,默认用顶层 intent_id)。返回 ids 数组,与 facts 等长同序。\n"+
|
||||
"⚠️只写你在工具输出里【真实看到】的结论,不要脑补。evidence 与 confidence 用来防止不准确的结论污染图谱:\n"+
|
||||
@@ -1732,7 +1761,7 @@ func (t *ToolSet) setConstraints() actool.CoreTool {
|
||||
}
|
||||
|
||||
func (t *ToolSet) addHint() actool.CoreTool {
|
||||
return writeTool("add_hint", "把人类/主 agent 的战略提示挂到探索图,规划者下次生成意图时会读到它。\n"+
|
||||
return t.writeExpTool("add_hint", "把人类/主 agent 的战略提示挂到探索图,规划者下次生成意图时会读到它。\n"+
|
||||
"★优先批量:多条提示放进 hints 数组一次提交(比逐条调用省往返)。返回 ids 数组,与 hints 等长同序(失败项 id=0,详情见 errors)。单条则省略 hints 直接给顶层 text。",
|
||||
obj(map[string]any{
|
||||
"hints": map[string]any{"type": "array", "description": "【优先用这个】要新增的提示数组,按顺序处理。每个元素字段同下方顶层字段(text/asset_ids/traffic_refs)。返回 ids 与本数组等长、同序。", "items": obj(map[string]any{"text": str("提示内容"), "asset_ids": map[string]any{"type": "array", "items": map[string]any{"type": "integer"}}, "traffic_refs": HintTrafficSchema()})},
|
||||
@@ -1792,7 +1821,7 @@ func (t *ToolSet) addHint() actool.CoreTool {
|
||||
|
||||
// killWorkTool lets the planner terminate a single running work (by intent id).
|
||||
func (t *ToolSet) killWorkTool() actool.CoreTool {
|
||||
return writeTool("kill_work", "终止一条正在运行的意图(work)。用于叫停跑偏/无意义的探索;被终止的意图标记为 stopped,不再自动重领。先用 get_worker_output 看看它在干嘛再决定。",
|
||||
return t.writeExpTool("kill_work", "终止一条正在运行的意图(work)。用于叫停跑偏/无意义的探索;被终止的意图标记为 stopped,不再自动重领。先用 get_worker_output 看看它在干嘛再决定。",
|
||||
obj(map[string]any{"intent_id": idp("要终止的意图 id(= work 句柄)")}, "intent_id"),
|
||||
func(_ context.Context, in json.RawMessage) (actool.Result, error) {
|
||||
if t.killWork == nil {
|
||||
@@ -1822,7 +1851,7 @@ func (t *ToolSet) killWorkTool() actool.CoreTool {
|
||||
// which re-plans its next step (already-gathered context is kept). For in-intent
|
||||
// nudges ("停做 X、聚焦 Y"); if the whole direction is wrong use kill_work + a new intent.
|
||||
func (t *ToolSet) steerWorkTool() actool.CoreTool {
|
||||
return writeTool("steer_work", "给一条正在运行的意图(work)实时注入纠偏指令,不打断它、不丢已有进展:worker 会在下一步动作前收到你的指令并据此调整。用于'别再走 X、聚焦 Y'这类【意图内】纠偏;若方向整个错了应改用 kill_work 再下新意图。建议先用 get_worker_output 看它在干嘛。",
|
||||
return t.writeExpTool("steer_work", "给一条正在运行的意图(work)实时注入纠偏指令,不打断它、不丢已有进展:worker 会在下一步动作前收到你的指令并据此调整。用于'别再走 X、聚焦 Y'这类【意图内】纠偏;若方向整个错了应改用 kill_work 再下新意图。建议先用 get_worker_output 看它在干嘛。",
|
||||
obj(map[string]any{
|
||||
"intent_id": idp("要纠偏的意图 id(= work 句柄)"),
|
||||
"message": str("给 worker 的纠偏指令,明确让它停止什么、转向什么"),
|
||||
@@ -1856,7 +1885,7 @@ func (t *ToolSet) steerWorkTool() actool.CoreTool {
|
||||
|
||||
// getWorkerOutput returns a work's final (or截至中止时的) conclusion text by intent id.
|
||||
func (t *ToolSet) getWorkerOutput() actool.CoreTool {
|
||||
return readTool("get_worker_output", "取本任务或直接关联任务某条意图(work)的最终输出结论。关联任务结果带 source_task_id/inherited=true 且只读。正常结束返回其总结;被终止(stopped)/异常的 work 返回其截至中止时的最后输出。",
|
||||
return t.readExpTool("get_worker_output", "取本任务或直接关联任务某条意图(work)的最终输出结论。关联任务结果带 source_task_id/inherited=true 且只读。正常结束返回其总结;被终止(stopped)/异常的 work 返回其截至中止时的最后输出。",
|
||||
obj(map[string]any{"intent_id": idp("意图 id(= work 句柄)")}, "intent_id"),
|
||||
func(_ context.Context, in json.RawMessage) (actool.Result, error) {
|
||||
var a struct {
|
||||
@@ -1937,7 +1966,7 @@ func traceSteps(acts []db.Activity) []map[string]any {
|
||||
// list step summaries, keyword-search within one work, or pull full detail of a
|
||||
// few specific steps. Thinking steps are excluded everywhere.
|
||||
func (t *ToolSet) getWorkerTrace() actool.CoreTool {
|
||||
return readTool("get_worker_trace",
|
||||
return t.readExpTool("get_worker_trace",
|
||||
"查看某条意图(work)的【执行过程】(区别于 get_worker_output 只给最终结论)。三种用法:\n"+
|
||||
"① 只传 intent_id → 返回该 work 每一步的摘要流(summary≤100字,含 step_id;只是动作轮廓,不含完整输出);\n"+
|
||||
"② intent_id + q → 只返回命中关键字的步骤摘要(在摘要和完整输出里都搜;仍只给 summary,要看内容用③);\n"+
|
||||
@@ -2046,7 +2075,7 @@ func (t *ToolSet) getWorkerTrace() actool.CoreTool {
|
||||
// finding what a worker saw but never wrote back as a fact. Returns only matching
|
||||
// summaries (≤100 chars), each tagged with its intent_id for follow-up drill-down.
|
||||
func (t *ToolSet) searchAllWorkerTraces() actool.CoreTool {
|
||||
return readTool("search_all_worker_traces",
|
||||
return t.readExpTool("search_all_worker_traces",
|
||||
"【通常不推荐使用,因为系统中已经给了大部分信息了】在【本任务其他 work 的执行过程】里按关键字(q)检索——用于找回某个 worker 见过、却没写进 fact 的东西(某路径/token/报错等)。"+
|
||||
"已自动排除你自己这条意图的步骤(那些本就在你上下文里)。"+
|
||||
"只返回命中步骤的摘要(summary≤100字),每条带 intent_id;据此再用 get_worker_trace(intent_id, step_ids=[...]) 取完整内容。",
|
||||
@@ -2095,7 +2124,7 @@ func (t *ToolSet) searchAllWorkerTraces() actool.CoreTool {
|
||||
// from search_all_worker_traces hits. Excludes still-open intents (not yet run →
|
||||
// no process to inspect).
|
||||
func (t *ToolSet) listWorkerTraces() actool.CoreTool {
|
||||
return readTool("list_worker_traces",
|
||||
return t.readExpTool("list_worker_traces",
|
||||
"【通常不推荐使用,因为系统中已经给了大部分信息了】列出本任务里【已跑过的 work(意图)】索引:intent_id + 一句话方向(summary) + 状态。"+
|
||||
"你(worker)看不到探索图,用它来发现有哪些 work 值得翻看——再用 get_worker_trace(intent_id) 看其步骤、get_worker_trace(intent_id, step_ids=[...]) 取详情。"+
|
||||
"只列已执行的(running/done/exhausted/blocked/stopped),不含还没跑的 open。注意:你的任务边界仍是你领到的那条意图,看别的 work 只为复用观察/避免重复劳动。",
|
||||
|
||||
@@ -195,7 +195,7 @@ func (t *ToolSet) resolveDigest(id int64) (*db.Node, *db.ExplorationStore, int64
|
||||
// a distinct tool from node_detail because it returns a LIST of members, not one
|
||||
// node's full detail.
|
||||
func (t *ToolSet) expandDigest() actool.CoreTool {
|
||||
return writeTool("expand_digest",
|
||||
return t.writeExpTool("expand_digest",
|
||||
"展开一个 cold digest:返回它折叠的成员紧凑列表(id/summary/state/confidence),与概览 recent_facts/recent_done_intents 同形状。要某条完整细节/证据用 node_detail(member_id)。",
|
||||
map[string]any{
|
||||
"type": "object",
|
||||
@@ -245,7 +245,7 @@ func (t *ToolSet) expandDigest() actool.CoreTool {
|
||||
// with its body + member count — so the planner can drill an asset directory down
|
||||
// to its directions without reading every digest globally.
|
||||
func (t *ToolSet) expandIndex() actool.CoreTool {
|
||||
return writeTool("expand_index",
|
||||
return t.writeExpTool("expand_index",
|
||||
"展开冷区某个资产条目(来自概览 cold_index):返回该资产名下的 cold digest 列表(id/body/member_count)。再往下看某个 digest 的成员用 expand_digest(id)。",
|
||||
map[string]any{
|
||||
"type": "object",
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
actool "github.com/Autumn-27/norma/tool"
|
||||
)
|
||||
|
||||
// The server-level ToolSet behind buildDomainReg carries a nil ExplorationStore,
|
||||
// and the tools table can bind any of its tools to any agent — including agents
|
||||
// that never run inside a task. Every domain tool must therefore survive being
|
||||
// called with nil stores: a nil deref here runs on the harness's own goroutine,
|
||||
// out of reach of every recover() in the server, and kills the whole process.
|
||||
func TestDomainToolsSurviveNilStores(t *testing.T) {
|
||||
inputs := []string{
|
||||
`{}`,
|
||||
`{"id":379,"asset_id":1,"goal_id":1,"evidence_id":1,"intent_id":1,"node_id":1,"work_id":1,` +
|
||||
`"summary":"x","reason":"x","name":"x","severity":"low","vulnclass":"x","text":"x","q":"x"}`,
|
||||
}
|
||||
for _, tool := range NewToolSet(nil, "").AllDomainTools() {
|
||||
for _, in := range inputs {
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("%s panicked with nil stores on %s: %v", tool.Name(), in, r)
|
||||
}
|
||||
}()
|
||||
if _, err := tool.Call(context.Background(), json.RawMessage(in), nil); err != nil {
|
||||
t.Fatalf("%s returned a transport error: %v", tool.Name(), err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// node_detail is the one that took the process down; assert it now answers with a
|
||||
// usable refusal rather than dying.
|
||||
func TestExplorationToolRefusesWithoutTask(t *testing.T) {
|
||||
ts := NewToolSet(nil, "")
|
||||
res, err := ts.NodeDetailTool().Call(context.Background(), json.RawMessage(`{"id":379}`), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !res.IsError || !strings.Contains(res.Flatten(), "任务上下文") {
|
||||
t.Fatalf("want an explanatory tool error, got IsError=%v %q", res.IsError, res.Flatten())
|
||||
}
|
||||
}
|
||||
|
||||
// A panicking tool must degrade to a tool error: the run continues and the process
|
||||
// survives, instead of the supervisor restarting into the same crash on replay.
|
||||
func TestGuardPanicConvertsPanicToToolError(t *testing.T) {
|
||||
boom := actool.Build(actool.Spec{
|
||||
Name: "boom",
|
||||
Run: func(context.Context, json.RawMessage, *actool.ToolContext) (actool.Result, error) {
|
||||
panic("nil map write")
|
||||
},
|
||||
})
|
||||
res, err := guardPanic(boom).Call(context.Background(), json.RawMessage(`{}`), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !res.IsError || !strings.Contains(res.Flatten(), "nil map write") {
|
||||
t.Fatalf("want the panic reported as a tool error, got IsError=%v %q", res.IsError, res.Flatten())
|
||||
}
|
||||
}
|
||||
@@ -337,6 +337,12 @@ func contains(ss []string, v string) bool {
|
||||
// (real AssetStore, nil ExplorationStore, taskID=0). Used by ToolResolve to inject
|
||||
// domain tools into agents (Auto, custom) that don't own a per-task ToolSet.
|
||||
// nil as → returns nil (no injection, graceful degradation).
|
||||
//
|
||||
// The nil ExplorationStore is deliberate — these instances are task-less by
|
||||
// construction — so every tool here must tolerate it. Asset/company tools do
|
||||
// (they only need the AssetStore); the exploration-graph tools refuse with a
|
||||
// clear message via ToolSet.needExploration. Binding one of them to a task-less
|
||||
// agent in the tools table is therefore a useless tool, not a crash.
|
||||
func buildDomainReg(as *db.AssetStore) map[string]actool.CoreTool {
|
||||
if as == nil {
|
||||
return nil
|
||||
|
||||
Reference in new issue
Block a user