mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-31 22:37:28 +00:00
批 1,不含 SDK 仓与目录改名。 - go.mod 第 1 行 module 路径 - 366 个 .go 文件的完整前缀 github.com/JianFeeeee/HomeAgent 机械替换 (857 处,仅完整前缀;展示名/注释里的 HomeAgent 字样留给批 5) - 未改:third_party 目录名、SDK 自身 go.mod module、go.mod replace 目标、 homed/waiter 可执行名、.hmap 后缀
166 lines
7.1 KiB
Go
166 lines
7.1 KiB
Go
package core
|
||
|
||
// 工具调用帧的「来源标识」判据(2026-10-16)。
|
||
//
|
||
// 要证明的核心是:**驻留子的工具调用能被识别为"内部委派",
|
||
// 而不是被当成"那位用户发起的请求"** —— 后者会让通道插件的
|
||
// 用户权限模型去拦一份已经准入的工作(现场实证:驻留子零产出)。
|
||
|
||
import (
|
||
"testing"
|
||
|
||
agentAPI "github.com/JianFeeeee/HologramAgent/internal/agent/api"
|
||
agentIO "github.com/JianFeeeee/HologramAgent/internal/agent/io"
|
||
sdk "github.com/JianFeeeee/HologramAgent/internal/sdk"
|
||
"github.com/JianFeeeee/HologramAgent/pkg/types"
|
||
)
|
||
|
||
// ★★★ 判据:来源标识**必须在帧的起点就写入**(不能只在工具帧上)。
|
||
//
|
||
// 为什么:插件可能在 on_input 阶段就据此决定是否介入,
|
||
// 而那个阶段**早于**工具帧构造。只在工具帧写会漏掉这条路径。
|
||
func TestComeFromIsStampedAtFrameOrigin(t *testing.T) {
|
||
root := newBareAgent("main", "")
|
||
ctx := root.stageCtxFromInput("hi", "", "")
|
||
got, ok := ctx.Extra[extraKeyComeFrom]
|
||
if !ok {
|
||
t.Fatal("★ 帧起点未写 comefrom(插件在 on_input 阶段拿不到来源标识)")
|
||
}
|
||
if got != comeFromUser {
|
||
t.Errorf("根 agent 的帧应为 %q,实际 %q", comeFromUser, got)
|
||
}
|
||
|
||
// 驻留子:有 parentID ⇒ internal
|
||
child := newBareAgent("worker-1", "main")
|
||
cctx := child.stageCtxFromInput("hi", "", "")
|
||
if got := cctx.Extra[extraKeyComeFrom]; got != comeFromInternal {
|
||
t.Errorf("★ 驻留子的帧应为 %q(它是父的委派体,不是用户请求的边界),实际 %q",
|
||
comeFromInternal, got)
|
||
}
|
||
}
|
||
|
||
// ★★★ **最关键的一条**:工具调用帧必须带标识,且驻留子的是 internal。
|
||
//
|
||
// 这是本次缺陷的直接回归判据:缺了它,通道插件的 before_toolcall
|
||
// 会把驻留子的调用按"非所有者用户"判定 ⇒ 开发工具全被拒。
|
||
func TestToolFrameCarriesComeFrom(t *testing.T) {
|
||
// 驻留子(有父)⇒ 工具帧必须是 internal
|
||
child := newBareAgent("worker-1", "main")
|
||
base := child.stageCtxFromInput("do work", "", "")
|
||
f := &TaskFrame{StageCtx: base, PendingTools: []agentAPI.ToolCall{
|
||
{Name: "cmd_run"}, {Name: "files_ls"},
|
||
}}
|
||
f.buildToolContexts()
|
||
if len(f.toolCtxs) != 2 {
|
||
t.Fatalf("工具帧数量不对: %d", len(f.toolCtxs))
|
||
}
|
||
for i, name := range []string{"cmd_run", "files_ls"} {
|
||
got, ok := f.toolCtxs[i].Extra[extraKeyComeFrom]
|
||
if !ok {
|
||
t.Fatalf("★ 工具 %s 的帧没有 comefrom(缺陷回归)", name)
|
||
}
|
||
if got != comeFromInternal {
|
||
t.Errorf("★ 驻留子的工具 %s 应为 %q,实际 %q —— "+
|
||
"会被通道插件按用户权限模型拦住", name, comeFromInternal, got)
|
||
}
|
||
}
|
||
|
||
// 根 agent ⇒ 工具帧必须是 user(不能因为加了这个就放宽用户权限)
|
||
root := newBareAgent("main", "")
|
||
rbase := root.stageCtxFromInput("hi", "", "")
|
||
rf := &TaskFrame{StageCtx: rbase, PendingTools: []agentAPI.ToolCall{{Name: "cmd_run"}}}
|
||
rf.buildToolContexts()
|
||
if got := rf.toolCtxs[0].Extra[extraKeyComeFrom]; got != comeFromUser {
|
||
t.Errorf("★ 根 agent 的工具帧应为 %q(否则会放宽用户权限门),实际 %q",
|
||
comeFromUser, got)
|
||
}
|
||
}
|
||
|
||
// ★★★ 判据:即使 base 帧**没有**标识,工具帧也必须带上(不变量兜底)。
|
||
//
|
||
// 为什么这条必须独立存在(本会话第 9 次同类教训):
|
||
// 初版判据只验证"走 stageCtxFromInput 的正常路径",而 copyExtraMap
|
||
// 已经把 comefrom 复制进工具帧 —— 于是**删掉工具帧里那行 stampComeFrom
|
||
// 判据依然全绿**(变异不咬)。那说明判据测的是"继承",没测"兜底"。
|
||
//
|
||
// 而兜底是有意义的:TaskFrame 可以被手搓(新内部路径、测试替身),
|
||
// 那种帧的 StageCtx 不经 stageCtxFromInput。没有兜底就会出现
|
||
// "某些路径的工具帧没有来源标识" —— 而缺标识时插件按旧语义(用户)
|
||
// 判定 ⇒ 驻留子又被拦。不变量必须由**工具帧自己**保证。
|
||
func TestToolFrameStampsComeFromEvenWhenBaseLacksIt(t *testing.T) {
|
||
// 手搓一个**没有** comefrom 的 base(模拟不经 stageCtxFromInput 的路径)
|
||
base := &sdk.StageContext{Extra: map[string]interface{}{}, Phase: sdk.StageOnInput}
|
||
if _, ok := base.Extra[extraKeyComeFrom]; ok {
|
||
t.Fatal("前置条件错:这个 base 不该有标识")
|
||
}
|
||
f := &TaskFrame{StageCtx: base, PendingTools: []agentAPI.ToolCall{{Name: "cmd_run"}}}
|
||
f.buildToolContexts()
|
||
got, ok := f.toolCtxs[0].Extra[extraKeyComeFrom]
|
||
if !ok {
|
||
t.Fatal("★ 工具帧未兜底写入 comefrom —— base 没标时就完全没标识了")
|
||
}
|
||
// 兜底值必须是**保守**的 user(宁可多查一次,不可静默放宽权限)
|
||
if got != comeFromUser {
|
||
t.Errorf("兜底值应为保守的 %q,实际 %q", comeFromUser, got)
|
||
}
|
||
}
|
||
|
||
// ★★★ 判据:工具帧的 Extra 仍然**逐份独立**(不能因为加键而退化成共享 map)。
|
||
//
|
||
// 既有纪律(task.go 注释):Extra 被 stage handler 写入,若多份共享同一
|
||
// map,一个工具的身份绑定会串到同批其他工具上 —— 那是越权。
|
||
func TestToolFramesHaveIndependentExtra(t *testing.T) {
|
||
a := newBareAgent("worker", "main")
|
||
base := a.stageCtxFromInput("x", "", "")
|
||
f := &TaskFrame{StageCtx: base, PendingTools: []agentAPI.ToolCall{{Name: "t1"}, {Name: "t2"}}}
|
||
f.buildToolContexts()
|
||
if len(f.toolCtxs) != 2 {
|
||
t.Fatal("工具帧数量不对")
|
||
}
|
||
f.toolCtxs[0].Extra["probe"] = "only-first"
|
||
if _, leaked := f.toolCtxs[1].Extra["probe"]; leaked {
|
||
t.Fatal("★ 工具帧共享了 Extra(一个工具的改写会串到别的工具上 = 越权风险)")
|
||
}
|
||
}
|
||
|
||
// ★★★ 判据:**不覆盖**已有标识(更内层知道更准)。
|
||
func TestStampComeFromDoesNotOverwrite(t *testing.T) {
|
||
m := map[string]interface{}{extraKeyComeFrom: comeFromInternal}
|
||
stampComeFrom(m, comeFromUser)
|
||
if m[extraKeyComeFrom] != comeFromInternal {
|
||
t.Errorf("★ 不该覆盖已有值(内层更准),实际被改成 %v", m[extraKeyComeFrom])
|
||
}
|
||
// nil map 不得 panic
|
||
stampComeFrom(nil, comeFromUser)
|
||
}
|
||
|
||
// ★★★ 判据:缺标识时的语义 = 旧行为(user)。
|
||
//
|
||
// 已发布插件不认识 comefrom。内核必须保证:**缺失时插件看到的行为
|
||
// 与改动前完全一致**,否则这个改动对存量插件是破坏性的。
|
||
func TestMissingComeFromKeepsLegacySemantics(t *testing.T) {
|
||
// 一个没有任何标识的帧(模拟旧路径)
|
||
ctx := &sdk.StageContext{Extra: map[string]interface{}{}}
|
||
if _, ok := ctx.Extra[extraKeyComeFrom]; ok {
|
||
t.Fatal("前置条件错:这个帧不该有标识")
|
||
}
|
||
// 内核侧的保守默认就是 comeFromUser —— 插件缺失时按旧语义(用户)处理
|
||
if comeFromUser != "user" {
|
||
t.Errorf("默认值必须是与旧行为等价的 %q,实际 %q", "user", comeFromUser)
|
||
}
|
||
}
|
||
|
||
// ---- 测试脚手架 ----
|
||
|
||
// newBareAgent 造一个只有身份(id + parentID)的最小 Agent。
|
||
//
|
||
// 不装配 io/provider:判据只验证"来源标识怎么算、怎么落到帧上",
|
||
// 装重型依赖会让判据变慢且掩盖真正的断言点。
|
||
func newBareAgent(id, parent string) *Agent {
|
||
return New(AgentConfig{
|
||
ID: types.AgentID(id),
|
||
ParentID: parent,
|
||
IO: agentIO.NewIOManager(),
|
||
})
|
||
}
|