mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-15 12:15:16 +00:00
## 背景 仓库早已迁移到 GitHub(`github.com/JianFeeeee/HomeAgent`,CI/CD 都在那边跑), gitcode 保留为**国内只读镜像**(`ci.yml` 的 sync-gitcode-main 快进推送)。 但 go.mod 的 module 路径一直是 `gitcode.com/JianFeeeee/...` —— 既与权威源 不符,`meta.SourceURL`(AGPL §13 的源码提供入口)也指向镜像而非实际运行的代码。 ## 关键区分(这是本次改动安全的前提) 两个 module 是**不同**的,影响面差一个数量级: | module | 谁依赖 | 改它的后果 | |---|---|---| | `.../HomeAgent`(主仓) | **只有主仓自己** | 内部重构,零外部影响 | | `.../homeagent-sdk`(SDK 仓) | 主仓 49 文件 + 所有外部插件 | 需重编插件 | ★ 实测确认:**插件不 import 主仓 module**(`strings qq/plugin.bin | grep HomeAgent` = 0), 只依赖 SDK 仓。而插件与内核的边界是 **RPC 方法名字符串** (`plugin.init` / `tool.invoke`…,见 proc/protocol.go),**与 Go module 名无关**。 ⇒ module 改名**不改变任何 wire 协议**,现网已装的插件照常运行。 SDK 的 GitHub 仓名是 `homeagentsdk`(无连字符),module 路径必须与之精确匹配, 否则 `go get` 会因路径不符失败,故取 `github.com/JianFeeeee/homeagentsdk`。 ## 改动 - `go.mod` / `go.sum`:两个 module 路径 + require/replace - 277 个 Go 文件 + Makefile + 打包脚本的 import 与 ldflags 路径 - SDK 仓 `meta.CoreModule`、`tools/hmapdev` 的 module 声明与工程模板 - 对外文档/链接的仓库地址(README、PLUGIN_DEV、OVERVIEW 等) ## 明确**保留** gitcode 的地方(镜像机制,不能动) - `GITCODE_TOKEN`、`GC_URL="…@gitcode.com"`(CI 同步 job) - `gitcode.com/api/v5/...`(release 附件上传) - git remote `gitcode`(镜像拉取/推送) - `docs/zh/ci-cd-runbook.md`(整篇讲镜像运维) ## 验证 - `go build ./...` / `go vet ./...` 绿 - `go test ./... -count=1` → 48 包 ok / 0 FAIL - hmapdev 工具链编译通过(`third_party/homeagent-sdk/tools/hmapdev`) - 全仓残留检查:module 形态的 `gitcode.com/JianFeeeee` = 0 - 镜像机制抽验:GC_URL / @gitcode / api/v5 / remote 均完好 ## 顺带修正 `hmapdev/templates.go` 的注释此前写「gitcode 的模块不在 proxy.golang.org 上」—— module 已迁 GitHub,该论断不再成立。改为如实说明**为何仍默认 replace 到本机 SDK**: 离线可构建、避免版本漂移、内网可用。理由与托管地无关,故 replace 保留。
181 lines
5.9 KiB
Go
181 lines
5.9 KiB
Go
package sdk
|
||
|
||
import (
|
||
agentIO "github.com/JianFeeeee/HomeAgent/internal/agent/io"
|
||
)
|
||
|
||
// toolImpl 桥接 StageHost(插件工具)与 IOManager(设备/通道工具)。
|
||
type toolImpl struct {
|
||
stageHost ToolSource
|
||
iom *agentIO.IOManager
|
||
}
|
||
|
||
func NewTool(sh ToolSource, iom *agentIO.IOManager) ToolAPI {
|
||
return &toolImpl{stageHost: sh, iom: iom}
|
||
}
|
||
|
||
func (t *toolImpl) GetToolDefs() []ToolDef {
|
||
if t.stageHost == nil {
|
||
return nil
|
||
}
|
||
return t.stageHost.GetToolDefs()
|
||
}
|
||
|
||
func (t *toolImpl) GetAllTools() []ToolDef {
|
||
if t.iom == nil {
|
||
return nil
|
||
}
|
||
defs := t.iom.GetAllTools()
|
||
out := make([]ToolDef, 0, len(defs))
|
||
for _, d := range defs {
|
||
// ⚠️ ParallelSafe 必须一并带出:它决定该工具能否被并发执行。
|
||
// 此前这里漏了它 ⇒ 插件看到的设备工具一律"不可并发",
|
||
// 设备工具的并发声明等于对插件不可见。
|
||
out = append(out, ToolDef{
|
||
Name: d.Name,
|
||
Description: d.Description,
|
||
Parameters: d.Parameters,
|
||
ParallelSafe: d.ParallelSafe,
|
||
})
|
||
}
|
||
return out
|
||
}
|
||
|
||
func (t *toolImpl) ExecuteTool(name string, args map[string]interface{}) (interface{}, error) {
|
||
if t.stageHost != nil {
|
||
if def := t.stageHost.ToolDef(name); def != nil {
|
||
return t.stageHost.ExecuteTool(name, args)
|
||
}
|
||
}
|
||
if t.iom != nil {
|
||
ret, err := t.iom.ExecuteTool(name, args)
|
||
if err == nil {
|
||
return ret, nil
|
||
}
|
||
// io 没有该工具 ⇒ 试内置工具(方案 B)。
|
||
// ⚠️ 只在「确实不存在」时才继续;io 的**执行失败**必须如实上抛,
|
||
// 否则会把「设备离线」误报成「工具不存在」,让调用方按 missing
|
||
// 策略跳过——这与 P3 修过的父 io 吞错误是同一族陷阱。
|
||
if !agentIO.IsToolNotFound(err) {
|
||
return nil, err
|
||
}
|
||
}
|
||
// ② 内核内置工具
|
||
if hasBuiltin(name) {
|
||
return builtinExec(name, args)
|
||
}
|
||
return nil, agentIO.ToolNotFound(name)
|
||
}
|
||
|
||
var _ ToolAPI = (*toolImpl)(nil)
|
||
|
||
// ToolDefByName 按名字查任一来源的工具声明(StageHost 优先,再查 IO 设备)。
|
||
//
|
||
// 用途:插件在**运行前**判断目标工具是否存在、是否并发安全。工具是动态
|
||
// 注册的,"不存在"是常态(插件未加载/已卸载/崩溃),因此查不到一律返回
|
||
// nil 交由调用方按"不存在"处理——不得 panic。
|
||
func (t *toolImpl) ToolDefByName(name string) *ToolDef {
|
||
if t == nil || name == "" {
|
||
return nil
|
||
}
|
||
if t.stageHost != nil {
|
||
if def := t.stageHost.ToolDef(name); def != nil {
|
||
return def
|
||
}
|
||
}
|
||
if t.iom != nil {
|
||
if def, ok := t.iom.ToolDefOf(name); ok {
|
||
return &ToolDef{
|
||
Name: def.Name,
|
||
Description: def.Description,
|
||
Parameters: def.Parameters,
|
||
ParallelSafe: def.ParallelSafe,
|
||
}
|
||
}
|
||
}
|
||
// ③ 内核内置工具(方案 B):此前这里直接返回 nil ⇒ 插件既查不到也调不了
|
||
// memory_*/knowledge_*/doc_*/person_*。
|
||
for _, d := range builtinDefs() {
|
||
if d.Name == name {
|
||
return &ToolDef{
|
||
Name: d.Name,
|
||
Description: d.Description,
|
||
Parameters: d.Parameters,
|
||
// ⚠️ 内置工具**默认不声明并发安全**:它们含 SQLite 写与召回,
|
||
// 且部分依赖 per-agent 状态(驻留子是轻量内核,memory 为 nil)。
|
||
// 保守默认 = 不并发,与它们今天的行为一致。
|
||
ParallelSafe: false,
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// CanUse 报告「执行该工具是否被授权」(D4)。
|
||
//
|
||
// 实现要点:**必须与 core.executeToolCallInner 里的那道闸同源同语义**,
|
||
// 否则两条路径判定不同,本身就是漏洞。判据 core.TestCanUseAgreesWithInnerPath
|
||
// 逐例比对两条路径的结论。
|
||
//
|
||
// 判据只有一条:设备类工具按 `device/<id>` 查当前 agent 的 allowedOutputs。
|
||
//
|
||
// ⚠️ device_id 缺失时**放行**(fail-open)——这是内核现状
|
||
// (core 的 TestDeviceToolAuth_* 依赖它)。两处行为已由
|
||
// TestCanUseMatchesInnerFailOpenOnMissingDeviceID 钉住;若将来要改成
|
||
// fail-closed,**必须两处同时改**,否则两条路径不一致。
|
||
func (t *toolImpl) CanUse(toolName string, args map[string]interface{}) bool {
|
||
if t == nil || t.iom == nil {
|
||
return true // 拿不到设备视图 ⇒ 不拦(与内核无 io 时的行为一致)
|
||
}
|
||
// 非设备工具不受此闸影响:闸的作用域必须窄,否则会把所有工具锁死。
|
||
if _, isDeviceTool := t.iom.DeviceOfTool(toolName); !isDeviceTool {
|
||
return true
|
||
}
|
||
id, _ := args["device_id"].(string)
|
||
if id == "" {
|
||
return true // fail-open,与内核一致
|
||
}
|
||
return t.canUseDevice(id)
|
||
}
|
||
|
||
// canUseDevice 是**可注入**的授权查询。
|
||
//
|
||
// 为何不直接在 toolImpl 里调 Agent:toolImpl 在 internal/sdk 包,
|
||
// 而 IsOutputAllowed 是 core.*Agent 的方法(core 反向依赖 sdk,
|
||
// sdk 不能依赖 core)。故内核在装配时把查询函数注入进来。
|
||
var deviceAuthQuery func(deviceID string) bool
|
||
|
||
// SetDeviceAuthQuery 注入"设备是否已授权"的查询(由内核在装配时调用)。
|
||
//
|
||
// 传 nil 表示尚未注入 ⇒ CanUse 对设备工具**放行**(保持存量行为不变)。
|
||
func SetDeviceAuthQuery(fn func(deviceID string) bool) { deviceAuthQuery = fn }
|
||
|
||
func (t *toolImpl) canUseDevice(deviceID string) bool {
|
||
if deviceAuthQuery == nil {
|
||
return true
|
||
}
|
||
return deviceAuthQuery(deviceID)
|
||
}
|
||
|
||
// builtinExec 执行内核内置工具。
|
||
//
|
||
// 放在本文件是因为要用 agentIO.ErrToolNotFound(sdk/tool.go 未导入 agentIO)。
|
||
// 未注入 provider 时返回**类型化**的 not-found,使调用方(如 seq 的
|
||
// missing 策略)能把它与「插件执行失败」区分开 —— 二者的处置完全不同。
|
||
func builtinExec(name string, args map[string]interface{}) (string, error) {
|
||
if builtinProv == nil {
|
||
return "", agentIO.ToolNotFound(name)
|
||
}
|
||
return builtinProv.Exec(name, args)
|
||
}
|
||
|
||
// hasBuiltin 报告该名字是否是可见的内置工具。
|
||
func hasBuiltin(name string) bool {
|
||
for _, d := range builtinDefs() {
|
||
if d.Name == name {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|