feat: AutoZero 通用 Agent 框架(从 AutoGo 五阶段拆分完成)

llm(三协议 Provider)/ session(JSONL 持久化)/ tools(Registry、围栏、
builtin、computer)/ cdp(browser)/ skills / mcp / agent(循环、两级压缩、
编排、审批门控)+ 底座 jsonl/textx/execx/diag/scratch/spill/imagecache。

产品装配代码(config 桥接、schedule 工具、memory/plan/retro/prompt)
留在 AutoGo,宿主接入方式见 MIGRATION.md。
This commit is contained in:
Sakurasan committed 2026-10-26 00:54:46 +08:00
commit d5dac5a6fc
140 files changed
+26497

No files matched your search

+178
View File
@@ -0,0 +1,178 @@
package agent
import (
"bytes"
"context"
"fmt"
"strings"
"testing"
"git.gopher.ink/mirrors2/AutoZero/llm"
"git.gopher.ink/mirrors2/AutoZero/session"
"git.gopher.ink/mirrors2/AutoZero/tools"
)
// 子 agent 的审批通道必须通向**同一个宿主**。修之前只继承了 Stdin:WebUI 那条路
// stdin 传的就是 nil,于是子 agent 撞上危险门时既没回调也没 stdin,询问直接按
// "无人可问"拒绝,弹框根本不会出现——模型只会说"没有审批人,你手动来吧"。
// subApproverAgent 造一个只有假 bash 的父 agent 并挂上 agent 工具(runs 记录 bash
// 真的跑了几次,绝不让真命令落地)。
func subApproverAgent(t *testing.T, runs *int) (*Agent, *tools.Tool) {
t.Helper()
t.Setenv("HOME", t.TempDir())
reg := tools.NewRegistry()
if err := reg.Register(&tools.Tool{
Name: "bash", SideEffect: true, Kind: tools.KindExec,
Run: func(context.Context, string) (tools.Result, error) {
*runs++
return tools.TextResult("ran"), nil
},
}); err != nil {
t.Fatal(err)
}
sess, err := session.New()
if err != nil {
t.Fatal(err)
}
var out bytes.Buffer
a := &Agent{
Tools: reg,
Session: sess,
System: "test",
MaxDepth: 1,
Opt: Options{Stdout: &out, Stderr: &out},
trusted: map[string]bool{},
}
if err := a.Tools.Register(a.SubAgentTool()); err != nil {
t.Fatal(err)
}
tl, ok := a.Tools.Get("agent")
if !ok {
t.Fatal("agent tool not registered")
}
return a, tl
}
// childToolProvider 驱动子 agent:第一步调 bash 跑给定命令,第二步收尾。
// 用例直接调用 agent 工具,所以父级不会消耗 provider 的 call 1。
func childToolProvider(cmd string) *scriptedProvider {
return &scriptedProvider{fn: func(call int, _ string, _ []llm.Message, _ []llm.ToolDef) (llm.Message, error) {
if call == 1 {
return llm.Message{Role: llm.RoleAssistant, ToolCalls: []llm.ToolCall{
{ID: "b1", Name: "bash", Input: fmt.Sprintf(`{"command":%q}`, cmd)},
}}, nil
}
return llm.Message{Role: llm.RoleAssistant, Content: "SUB-DONE"}, nil
}}
}
const (
sshKeyRead = `cat ~/.ssh/id_rsa`
rmRFCmd = `rm -rf /tmp/x` // 注意:danger_gate_test.go 里的 rmRF 是整段 JSON 入参,不是命令
)
// TestSubAgentDangerReachesParentApprover:子 agent 的不可逆动作要走到父级的审批
// 回调,人点了放行就真的执行。
func TestSubAgentDangerReachesParentApprover(t *testing.T) {
runs := 0
a, tl := subApproverAgent(t, &runs)
a.Provider = childToolProvider(rmRFCmd)
a.Mode = ModeAsk
var asked []ApprovalRequest
a.Opt.Approval = func(req ApprovalRequest) ApprovalVerdict {
asked = append(asked, req)
return ApprovalOnce
}
res, err := tl.Run(context.Background(), `{"prompt":"child"}`)
if err != nil {
t.Fatal(err)
}
if len(asked) != 1 {
t.Fatalf("子 agent 的危险动作应当问一次,实际 %d 次", len(asked))
}
if asked[0].Tool != "bash" || !asked[0].Danger {
t.Errorf("询问内容不对:tool=%q danger=%v", asked[0].Tool, asked[0].Danger)
}
if !strings.Contains(asked[0].Preview, "rm-recursive") && !strings.Contains(asked[0].Preview, "递归删除") {
t.Errorf("审批预览里要点名危险原因:%q", asked[0].Preview)
}
if runs != 1 {
t.Errorf("人已放行,bash 却没跑(runs=%d)", runs)
}
if !strings.Contains(res.Text, "SUB-DONE") {
t.Errorf("子 agent 收尾回复没回来:%q", res.Text)
}
}
// TestSubAgentDangerDenyStaysFailClosed:接上通道不等于放松门——父级拒绝时子
// agent 依然不执行,只是终于知道"是人不批"而不是"没人可问"。
func TestSubAgentDangerDenyStaysFailClosed(t *testing.T) {
runs := 0
a, tl := subApproverAgent(t, &runs)
a.Provider = childToolProvider(rmRFCmd)
a.Mode = ModeAsk
asked := 0
a.Opt.Approval = func(ApprovalRequest) ApprovalVerdict {
asked++
return ApprovalDeny
}
if _, err := tl.Run(context.Background(), `{"prompt":"child"}`); err != nil {
t.Fatal(err)
}
if asked != 1 {
t.Fatalf("应当问一次,实际 %d 次", asked)
}
if runs != 0 {
t.Errorf("人被拒了还执行(runs=%d)", runs)
}
}
// TestSubAgentSecretReadPiercesBypass:bypass 收回的是"别问我",不是"凭据可以念"。
// 子 agent 里读私钥仍要过审批——这条以前根本问不到人,所以从来没被验证过。
func TestSubAgentSecretReadPiercesBypass(t *testing.T) {
runs := 0
a, tl := subApproverAgent(t, &runs)
a.Provider = childToolProvider(sshKeyRead)
a.Mode = ModeBypass
a.Opt.ApproveAll = true // 连 --yes 一起叠上:这两样都不该让凭据读取静默通过
asked := 0
var deny bool
a.Opt.Approval = func(ApprovalRequest) ApprovalVerdict {
asked++
deny = true
return ApprovalDeny
}
if _, err := tl.Run(context.Background(), `{"prompt":"child"}`); err != nil {
t.Fatal(err)
}
if !deny {
t.Fatal("子 agent 没有把凭据读取交给父级审批回调")
}
if asked != 1 {
t.Fatalf("应当只问一次,实际 %d 次", asked)
}
if runs != 0 {
t.Errorf("拒绝后仍执行了(runs=%d)", runs)
}
}
// TestSubAgentNoApproverStillFailsClosed:反向对照——父级确实没人可问(-p / cron /
// 渠道:回调与 stdin 都是 nil)时,子 agent 保持原有语义,直接拒,不静默放行。
func TestSubAgentNoApproverStillFailsClosed(t *testing.T) {
runs := 0
a, tl := subApproverAgent(t, &runs)
a.Provider = childToolProvider(rmRFCmd)
a.Mode = ModeAsk
a.Opt.Approval = nil
a.Opt.Stdin = nil
if _, err := tl.Run(context.Background(), `{"prompt":"child"}`); err != nil {
t.Fatal(err)
}
if runs != 0 {
t.Errorf("无人可问时必须拒绝,却执行了(runs=%d)", runs)
}
}