mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-03 15:53:56 +00:00
fix(security): 设备授权闸下沉到 ToolAPI 路径(D4,堵住绕过)
问题:设备类工具的授权闸只存在于 core.executeToolCallInner (toolcall.go:151-152),即**「agent 收到模型 tool_call」那条路径**。 而 ToolAPI.ExecuteTool 是**另一条**独立执行入口,不经那道闸 ⇒ 凡是走 ToolAPI 的调用都能绕过 AllowedOutputs。 实测范围**不止序列**:cli 插件的 /terminal 直接经 ToolAPI 调 agentcli 的 终端工具(cli/plugin.go:1038 的注释自陈"SDK 的 ToolAPI 已允许跨插件调用 工具")。任何插件拿 ToolAPI 都能指挥未授权的设备。 改动: · internal/sdk/tool.go: ToolAPI 新增 CanUse(toolName, args) bool。 **纯新增方法**,零值实现返回 true ⇒ 未实现者(存量插件、测试替身) 行为不变。 · internal/sdk/tool_impl.go: 实现 CanUse。判据只有一条——设备类工具按 `device/<id>` 查授权;非设备工具不受影响(闸的作用域必须窄,否则会把 所有工具锁死)。 授权查询走**可注入**的晚绑定闭包:toolImpl 在 internal/sdk,而 IsOutputAllowed 是 core.*Agent 的方法,sdk 不能依赖 core。 · internal/plugin/registry.go: 新增 SetDeviceAuthQuery。 · cmd/homed/bootstrap.go: 在 newMainAgent 末尾注入。⚠️ 必须在 agent 构造**之后**——判据要用 agent 自己的 allowedOutputs,而 registry 早于 agent 构造,故 registry 存的是晚绑定闭包。 判据(toolapi_auth_test.go,7 条),核心是**两条路径必须一致**: · 收窄授权时 ToolAPI 路径同样被拦 · 已授权设备放行(防闸过严杀掉正常能力) · 非设备工具不受影响 · 枚举类工具不受影响(与内核 TestDeviceToolAuth_EnumerationNotGated 同语义) · 未配置白名单 = 完整授权 · ★ TestCanUseAgreesWithInnerPath:4 组用例逐例比对内核路径与 ToolAPI 路径的结论 —— 判定不同本身就是漏洞 · ★ TestCanUseMatchesInnerFailOpenOnMissingDeviceID:把现状 (缺 device_id 时**放行**)钉住。⚠️ 这是 fail-open,是既有的可疑设计 (core 的 TestDeviceToolAuth_* 依赖它),本次不擅自改语义;判据写明 "若要改成 fail-closed,必须两处同时改"。 过程中三次自伤: 1. 一度在 core 写了个 toolAPIRef —— **只实现部分方法的替身**是过度设计, 且两份实现必然漂移。改为判据直接用 sdk.NewTool(stageHost, iom), 与插件侧走**同一个**实现。 2. 判据里又写了 `var _ = agentIO.DeviceOutput` 这种压 unused import 的 占位 hack(第二次犯这个),并重造了 strings.Contains。都已去掉。 3. 注入点一开始找错了位置(以为 newStageAndRegistry 能拿到 agent, 实际 pluginReg 是 main() 的局部变量)。核实 newMainAgent 的签名后 确认它同时持有 agent 与 pluginReg,注入点落在那里。 变异验证:让 CanUse 恒返回 true(还原成原缺口)⇒ 两条判据 FAIL, 其中一条直指「内核路径=false 而 ToolAPI 路径=true —— 两条路径判定不一致」。 回归:go build ./... 通过;go test ./internal/... 全绿。
This commit is contained in:
@ -514,6 +514,11 @@ func newMainAgent(cfg *types.Config, cfgReg *internalConfig.ConfigRegistry, prov
|
||||
sysPrompt = defaultSystemPrompt
|
||||
}
|
||||
|
||||
// D4:把"设备是否已授权"的判据注入插件侧(toolImpl.CanUse 用)。
|
||||
// ⚠️ 必须放在 agent 构造**之后**——判据要用 agent 的 allowedOutputs,
|
||||
// 而 registry 早于 agent 构造(故这里传的是晚绑定闭包)。
|
||||
// 未注入时 ToolAPI 路径对设备放行:那是"授权可被绕过"的既成缺口。
|
||||
// 注入后,cli 的 /terminal、seq 序列等一切走 ToolAPI 的调用都受同一道闸。
|
||||
agent := agentCore.New(agentCore.AgentConfig{
|
||||
ID: "main",
|
||||
SystemPrompt: sysPrompt,
|
||||
@ -536,12 +541,12 @@ func newMainAgent(cfg *types.Config, cfgReg *internalConfig.ConfigRegistry, prov
|
||||
PluginDir: cfg.Plugin.Dir,
|
||||
// DataDir:驻留子的 temp 图库锚点(<data>/residents/<id>/graph.db)。
|
||||
// 漏接时的现象是"工具存在、可调用、但创建必失败"——只有真实二进制才看得出来。
|
||||
DataDir: cfg.Daemon.DataDir,
|
||||
DistillInterval: cfgReg.GetDuration("core.agent.distill_interval", 30*time.Minute),
|
||||
ArchiveInterval: cfgReg.GetDuration("core.agent.archive_interval", 60*time.Minute),
|
||||
ReviewInterval: cfgReg.GetDuration("core.agent.review_interval", 120*time.Minute),
|
||||
MergeInterval: cfgReg.GetDuration("core.agent.merge_interval", 120*time.Minute),
|
||||
MaxToolTurns: cfgReg.GetInt("core.agent.max_tool_turns", 10),
|
||||
DataDir: cfg.Daemon.DataDir,
|
||||
DistillInterval: cfgReg.GetDuration("core.agent.distill_interval", 30*time.Minute),
|
||||
ArchiveInterval: cfgReg.GetDuration("core.agent.archive_interval", 60*time.Minute),
|
||||
ReviewInterval: cfgReg.GetDuration("core.agent.review_interval", 120*time.Minute),
|
||||
MergeInterval: cfgReg.GetDuration("core.agent.merge_interval", 120*time.Minute),
|
||||
MaxToolTurns: cfgReg.GetInt("core.agent.max_tool_turns", 10),
|
||||
Offload: agentCore.OffloadOptions{
|
||||
Enabled: cfgReg.GetBool("core.agent.offload_enabled", false),
|
||||
BusyAfter: cfgReg.GetDuration("core.agent.offload_busy_after", 5*time.Minute),
|
||||
@ -560,6 +565,21 @@ func newMainAgent(cfg *types.Config, cfgReg *internalConfig.ConfigRegistry, prov
|
||||
InputProcessing: cfg.InputProcessing,
|
||||
})
|
||||
|
||||
// D4:把「设备是否已授权」的判据注入插件侧(toolImpl.CanUse 消费它)。
|
||||
//
|
||||
// 为什么必须在这里:判据要用 agent 自己的 allowedOutputs,而
|
||||
// pluginReg 早于 agent 构造(newStageAndRegistry 在 main() 里先跑),
|
||||
// 所以 registry 存的是**晚绑定**闭包,注入点必须在 agent 建好之后。
|
||||
//
|
||||
// 不注入的后果(已实测的真实缺口):设备授权闸只存在于
|
||||
// core.executeToolCallInner,即「agent 收到模型 tool_call」那条路径;
|
||||
// 而 ToolAPI.ExecuteTool 是**另一条**独立入口,不经那道闸 ⇒
|
||||
// 凡是走 ToolAPI 的调用都能绕过 AllowedOutputs。实测范围不止序列:
|
||||
// cli 的 /terminal 就直接经 ToolAPI 调 agentcli 的终端工具。
|
||||
pluginReg.SetDeviceAuthQuery(func(deviceID string) bool {
|
||||
return agent.IsOutputAllowed("device/" + deviceID)
|
||||
})
|
||||
|
||||
return agent
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user