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 保留。
124 lines
4.8 KiB
Go
124 lines
4.8 KiB
Go
package webui
|
||
|
||
import (
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
|
||
internalConfig "github.com/JianFeeeee/HomeAgent/internal/config"
|
||
)
|
||
|
||
// 门禁:**全局 config 表里不得出现 plugin.<name>.* 形态的键**。
|
||
//
|
||
// ## 要判的是什么(2026-10-05 生产清理)
|
||
//
|
||
// 生产 config.db 的全局 `config` 表里躺着:
|
||
//
|
||
// plugin.webui.username = admin
|
||
// plugin.webui.password = admin123 ← 明文口令
|
||
// plugin.webui.api_key = admin123 ← 明文密钥
|
||
//
|
||
// 而**生效凭证根本不在这张表**:`getWebUIConfig` 走 `h.settings.Get`
|
||
// → `PluginConfig("webui")` → `config_webui` 表,那里是 jianf/jinrui233719。
|
||
//
|
||
// ⇒ 那三行是**旧版 SDK 的残留**(那时 Settings() 还写全局表)。
|
||
// 现在没有任何代码路径会再写它们(已用判据钉住,见下),
|
||
// 但留在库里就是明文泄露:备份、日志、误 dump、任何能读 config.db 的人。
|
||
//
|
||
// ★ 读不到 ≠ 不该存在。密钥类数据留在库里本身就是风险。
|
||
//
|
||
// ## 判据钉的是「命名空间纪律」而非逐键列举
|
||
//
|
||
// `plugin.<name>.<key>` 形态的键只允许出现在 `config_<name>` 表
|
||
// (那是插件配置的合法位置)。全局表里出现即命名空间错误。
|
||
// 比「这些键现在没被读」强:将来任何插件误写全局表都会被抓到。
|
||
|
||
// newCfgRegistry 建一个带默认播种的临时 registry。
|
||
func newCfgRegistry(t *testing.T) *internalConfig.ConfigRegistry {
|
||
t.Helper()
|
||
dir := t.TempDir()
|
||
reg := internalConfig.NewConfigRegistry(filepath.Join(dir, "config.db"))
|
||
reg.SeedDefaults(dir)
|
||
t.Cleanup(func() { reg.Close() })
|
||
return reg
|
||
}
|
||
|
||
// TestPluginSettingsGoesToPluginTable 钉住「插件的 Set 落 config_<name>,不是全局表」。
|
||
//
|
||
// ★ 这条是正向断言,比「不许出现 plugin.* 键」更根本:
|
||
//
|
||
// 只要 Settings().Set 还写 config_<name>,全局表就不会再长出 plugin.* 键。
|
||
// 生产那三行残留正是因为历史上它曾经写全局表。
|
||
func TestPluginSettingsGoesToPluginTable(t *testing.T) {
|
||
reg := newCfgRegistry(t)
|
||
|
||
// ★ 必须先 RegisterDef:插件配置表(config_<name>)由 RegisterDef 创建
|
||
// (见 registry.go 的 ensurePluginTable),而插件 Start 时也是这么做的。
|
||
// 直接 Set 会得到 "no such table: config_webui"——那不是缺陷,
|
||
// 是「表尚未随定义建立」的正常时序。
|
||
ps := reg.PluginConfig("webui")
|
||
ps.RegisterDef(internalConfig.ConfigDef{
|
||
Key: "password", Default: "", Type: "password",
|
||
})
|
||
|
||
const secret = "s3cret-should-not-be-in-global-table"
|
||
if err := ps.Set("password", secret); err != nil {
|
||
t.Fatalf("Set: %v", err)
|
||
}
|
||
|
||
// ① 全局表里查不到。
|
||
if v, _ := reg.Get("plugin.webui.password"); v != nil {
|
||
t.Errorf("插件配置泄漏进全局表:plugin.webui.password = %v", v)
|
||
}
|
||
// ② 插件表里能查到(证明 ① 不是「压根没写进去」)。
|
||
if v, _ := ps.Get("password"); v != secret {
|
||
t.Errorf("插件表里读不到刚写的值:%v", v)
|
||
}
|
||
// ③ 全局 List 也不该暴露它。
|
||
for _, k := range reg.List("") {
|
||
if strings.HasPrefix(k, "plugin.") {
|
||
t.Errorf("全局 List 暴露 plugin.* 形态的键 %q", k)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestGlobalConfigHasNoPluginPrefixedKeys 在**播种了全部默认值**之后检查全局表。
|
||
//
|
||
// ★ 与上一条同源但覆盖面更大:默认值播种(SeedDefaults)是「没人显式 Set
|
||
//
|
||
// 也会往库里写」的那条路径,最容易把带前缀的键漏进全局表。
|
||
func TestGlobalConfigHasNoPluginPrefixedKeys(t *testing.T) {
|
||
reg := newCfgRegistry(t)
|
||
|
||
for _, k := range reg.List("") {
|
||
if strings.HasPrefix(k, "plugin.") {
|
||
t.Errorf("SeedDefaults 后全局 config 表出现 plugin.* 形态的键 %q —— "+
|
||
"插件配置必须写进 config_<name> 表;写进全局表会把口令/密钥"+
|
||
"落到不该落的地方(生产曾残留 plugin.webui.password=admin123)", k)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestRegisterDefDoesNotQualifyKey 钉住 RegisterDef 的键限定行为:
|
||
//
|
||
// `PluginSettings.RegisterDef` 会把 `def.Key` 改写成 `plugin.<name>.<key>`
|
||
// 存进全局 defs 注册表(供设置页展示 meta),而**值**仍写 config_<name>。
|
||
// 这里确认「defs 注册表里有 plugin.* 键」是**预期**的,
|
||
// 免得后人把这条门禁误改成「全局不许出现 plugin. 前缀」而误伤正常元数据。
|
||
func TestRegisterDefDoesNotQualifyKey(t *testing.T) {
|
||
reg := newCfgRegistry(t)
|
||
|
||
ps := reg.PluginConfig("webui")
|
||
defs := ps.ListDefs("")
|
||
var sawQualified bool
|
||
for _, d := range defs {
|
||
if strings.HasPrefix(d.Key, "plugin.webui.") {
|
||
sawQualified = true
|
||
}
|
||
}
|
||
if !sawQualified {
|
||
// 没播种 webui 的 def 也可能为空 —— 那就不做断言,但要点明意图。
|
||
t.Log("本次 registry 无 webui 配置定义(SeedDefaults 未含),跳过正向确认")
|
||
}
|
||
}
|