mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-01 06:47:29 +00:00
feat: 模块化内核(编译期依赖图定序)+ 密钥防火墙 + 插件三态生命周期 (#10)
* feat(kernel): 模块化内核 + 编译期依赖图定序
把单体 init 序列拆成「模块自声明依赖 + 编译期生成依赖图 + 运行期只读图」。
## 为什么
原 cmd/homed/main.go 里 17 步 init 是**手写顺序**,依赖关系只存在于
注释和人脑里。实测踩到的坑(见 docs/zh/kernel-modularization-design.md):
· nlp.defaultParser 被 initONNXParser 写、被 distiller 读,两者相距 60 行,
只靠 ticker 10min 才没炸(隐式依赖,无声明)
· 加一个模块要人肉找"该插在哪",插错的表现是运行期 nil 解引用
· 关停是 defer ks.CloseAll(),而热更新路径 os.Exit(42) **跳过 defer**
⇒ docStore/distiller/textMem 的 flush 全部丢失(每次热更新都发生)
## 怎么改
- internal/kernelboot/:KernelState(一个结构体收全部包级共享变量,字段
nil 安全)、Module 接口、graphfile(依赖图编解码 + 与二进制 hash 对账)、
scheduler(按 BFS 层并行 Init)、syncbus(层边界写通知)、
lifecycle(Quiesce/Resume 可逆伪关机,区别于一次性 CloseAll)
- internal/kernelmod/<name>/:每个模块一个文件夹,Requires/Provides 自声明;
all/all.go 是唯一的 import 清单(Go 循环导入约束)
- cmd/homed-depgraph:编译期生成依赖图,Makefile 注入 digest
- 运行期只**读**图,不计算顺序(顺序是编译期决定的,源码里没有中央清单)
## 判据
kernelboot 8 条 + 图失败模式 7 条(全部拒绝并给准确错误)+ 模块目录数
与注册表一致性。变异测试 15/15 咬住,含:
· 删掉 AddCloser 的锁 → -race 报数据竞争
· Scheduler 同批跑去重 → 203 vs 200
· 图 hash 不匹配 → 拒绝启动
## 顺带修的缺陷
热更新路径漏调模块关闭 ⇒ 显式 stopKernelModules(),并用 AST 判据钉住
"必须在 os.Exit 之前"(字符串检查在本仓已骗过我三次)。
实测:冷启动 1630ms(5 层/17 模块,原 17 步串行);生产 22 插件全部建链。
* feat(security): 密钥防火墙 + 配置加密 + close 纪律
## 密钥防火墙
模型只见别名、不见明文;插件工具调用入口(StageHost.ExecuteTool)做入向
回填。三维能力:
· 出向:工具输出里的密钥被替换成 [[secret:name]] 别名后才进上下文
· 入向:模型传别名,入口处填回真值(插件完全无感知)
· 存储:config.db 里不存明文(0644 的库可读),敏感值独立 0600 文件
模式库「宁漏勿误伤」:宁可漏掉一次替换,也不把正常文本误当密钥。
## 配置加密
- internal/security/{codec,key,keyring,store,global}.go:AES-256-GCM,
enc:v1: 前缀,密钥轮换(OldKeys 解码 + 新钥加密)
- harden.go:数据目录权限收紧
- embedded.go:支持内嵌主密钥(可选,仍保留外部 key 文件路径)
- EncryptExistingSecrets / AuditSecrets:启动时幂等迁移 + 审计
## close 纪律(用户明确要求:只要调 close 就要有前置检查)
internal/safeclose:Safe / SafeFunc。全仓 37 处裸 close(chan) → 0。
关键教训(都写进注释和判据):
· `select { case <-ch: default: close(ch) }` **不是**并发安全的
(两个 goroutine 可同时命中 default)—— healthcheck 原先是这种写法
· `sync.Once` **不可重入**:X.Do(func(){ Safe(&X, ...) }) 会自锁,
症状是 go test 直接挂住(3 个包 600s 超时),栈里只有嵌套的 Once.Do
· map 里按请求持有的 channel(mcp/stdio.go、clawhubadapter/sidecar.go)
改成 *pendingRPC 包装,每通道一个 once
## 判据
security 全量 + TestNoBareCloseOutsideSafeclose(AST 级扫描,含注释剥离)
+ TestNoNestedSelfOnceDeadlock(正则修正:\w+ 不匹配点号,捕获组要含 '.')。
变异 2/2 咬住。全量 52 包 ok。
* feat(plugin+deploy): 插件三态生命周期、数据目录迁移、发行包修复
## 插件三态生命周期(用户要求:自动启动 / 启用但不自动启动 / 禁用)
- Manifest 新增 requires_facilities / auto_start(*bool,用于区分
"未声明"与"显式 false")
- plugin-host 模块(internal/kernelmod/pluginhost):用 kernelboot.Scheduler
按依赖时机拉起,**不再手写两阶段**
- 运行态停启 StopPlugin/StartPlugin **可逆**:不写 disabled_plugins
(不改用户决定)、不置 shuttingDown(之后还能加载)、会摘掉注册面
- 与 StopAll 的区别写进注释 —— StopAll 置 shuttingDown 后**永久无法加载**,
那是进程退出用的,误用于"暂停"就是关了就死
## 数据目录迁移(伪关机 → 迁移 → 唤醒)
- kernelboot/lifecycle.go:Quiesce/Resume(可逆,区别于一次性 CloseAll),
逆序暂停、逆序恢复、Pause 失败回滚
- kernelboot/migrate.go:BeginMigration/End 编排(先停插件 → 再停内核写者
→ 等在途归零)
- distiller/textmem 增 Pause/Resume 门(不是 cancel —— cancel 不可逆,
恢复要重建整个对象)
- 模块 migration:体检 + 生成可执行迁移脚本
★ 迁移脚本的三个实测教训(都写成了判据):
1. 无条件改 /etc/systemd/system ⇒ 演练把**生产** -data 改成了演练目录。
加 --dry-run(零改动)+ 强制校验 systemd 当前 -data == OLD
2. 本机没装 rsync,脚本在"服务已停、数据未搬"处硬失败 ⇒ cp -a 回退,
并明确警告不保硬链接/ACL(生产 plugindev 有大量 git 硬链接)
## 发行包修复(全部经实测确认)
1. package-linux.sh **完全漏打 depgraph.bin** ⇒ 内核 exit 1
("生产构建(内嵌 hash 非空)但读不到依赖图")
2. build_homed 不走 Makefile ⇒ 单独修 Makefile 无效,必须同时生成图+注入
3. build.sh 的 LDFLAGS 用**历史模块路径 gitcode.com** ⇒ -X 静默 no-op
⇒ --version 显示 unknown
4. Makefile -ldflags 用**单引号** ⇒ 注入了字面 $GRAPH_HASH
## ★ build.sh 的版本号必须校验(真实事故)
原 `VERSION="$(git describe --tags --dirty)"`。无 tag 分支上它返回**分支名**
`backup/pre-identity-rewrite-20261005-51-g03d87a4e-dirty`,被注入 meta.Version
⇒ 插件握手判据 MajorVersion() 把整串当主版本 ⇒ **22 个子进程插件全部
握手失败**("主版本不匹配")。内核活着、systemd active、日志每条都像
插件的问题 —— 实际是发行包带着非法版本号。
修法:git describe 只在形如 vX.Y.Z 时采用,否则回退 meta.Version(权威源),
并对最终值做形态守卫。deploy/packaging/version_guard_test.go 3 条判据,
变异 2/2 咬住。
## deb 的 systemd unit 改为 guard 模式
ExecStart=/usr/bin/homed --role=guard --data /var/lib/homeagent
(worker exit 42 → guard 重拉,不经过 systemd、不计失败)
含 RestartSteps/RestartMaxDelaySec/TimeoutStopSec + 硬化
(NoNewPrivileges/ProtectSystem/ProtectHome/DevicePolicy/ReadWritePaths)。
## 其他
- internal/plugin/proc:非法 JSON 帧的报错**可定位**(内容摘要 + 可能原因 +
修法)。生产实测 1038 条非法帧(插件往 stdout 打日志 = 污染协议通道)
- 示例插件 4 个 14 处 fmt.Printf → log.Printf
- hmapdev 模板同步修复;scaffold 判据剥离注释(模板注释里就含 fmt.Printf)
* chore: 收尾(主密钥轮换命令、IPC/知识库加固、SDK 对齐)
- cmd/homed-key-rotate:把 security 里的 RotateKey/RotateKeyFile/RekeySecrets
串成一条**可执行**的运维路径。没有它,E-1 就只是库里的能力而不是
"出事时能做的事",而轮换的价值恰恰在于能执行。
- cmd/homed:kmod_runtime(模块装配入口)、plugins_scheduled(failback
白名单)、memcandidate、bootstrap_order_test / shutdown_flush_test
- internal/ipc、internal/devicebridge、internal/knowledge:配套加固
- third_party/homeagent-sdk/sdk:PluginMgrAPI 增三态能力(mgrStop/mgrStart/
mgrIsRuntimeStopped)+ settings 对齐
判据:bootstrap_order_test(启动顺序不依赖手写序列)、
shutdown_flush_test(停机的两条路径必须对称 —— exit 42 会跳过 defer,
热更新路径下模块关闭/flush 曾全部丢失)。
* test(security): 测试夹具改用不可误认的占位值(修 push protection 拦截)
GitHub secret scanning 拦住了推送:三处测试夹具用了"看起来像真密钥"的
字面量:
internal/security/prefilter_test.go:39
internal/security/store_test.go:177
internal/security/survey_test.go:20
它们全是**假密钥**(`AKIAIOSFODNN7EXAMPLE` 是 AWS 官方文档的标准示例,
其余是 `abcdefghij...` 这类可读占位符),但扫描器无法区分 —— 它只看形态。
★ 不绕过(不用 unblock URL、不加 allowlist):直接改夹具。
原因:一旦允许"假密钥可以通过",真密钥混进来时就没有最后一道门了。
改法:保留**模式库匹配所需的结构**,把可识别内容换成 FAKE:
glpat-abcdefghijklmnopqrst → glpat-FAKE-TEST-TOKEN-NOT-REAL
sk-abcdefghij...012345 → sk-FAKE-TEST-KEY-NOT-REAL-0000000
AKIAIOSFODNN7EXAMPLE → AKIA0000FAKE0000TEST (保持 20 字符)
ASIAIOSFODNN7EXAMPLE → ASIA0000FAKE0000TEST
xoxb-123456789012-abcdefghijklmnop → xoxb-FAKE-TEST-TOKEN-NOT-REAL
★ 中途踩到一次:AWS 键的模式是 `\b(AKIA|ASIA)[0-9A-Z]{16}\b`(共 20 字符),
我第一次替换成 19 字符的 `AKIAFAKETESTKEY0000` ⇒ 模式匹配不上 ⇒
TestSurvey_WhatIsCaught / TestRecognizesCommonShapes /
TestPrefilterNeverBlocksRealMatches 三条当场变红。
判据正确地拒绝了"为了过扫描器而削弱覆盖"。
顺带核实:全仓 grep 无任何真实密钥泄漏。
* fix(packaging): package-linux.sh 的版本解析与 build.sh 统一
上一版只修了 build.sh,漏了 package-linux.sh —— 它仍只看 git describe:
VERSION="${VERSION:-$(git describe --tags --dirty)}"
无 tag 分支上 git describe 返回**分支名**
(backup/pre-identity-rewrite-20261005-56-g98850e1),守卫会正确拒绝,
但要求人**手动传 VERSION** 才能打包。而 build.sh 早已能自动回退 meta.Version
⇒ 同一个版本,走 build.sh 打得出来、走 package-linux.sh 打不出来。
危害不只是"要手动传参":运维为了跑通会去掉守卫,而分支名一旦进
meta.Version 就是**插件协议主版本**的来源 ⇒ 22 个子进程插件全部握手失败
(这是真实发生过的事故,见 3efa4e5)。
修法:两个脚本用同一套规则(git describe 仅在 vX.Y.Z 形态时采用,
否则回退 meta.Version),并对最终值做形态守卫。
判据:TestBothPackagingScriptsShareVersionRules —— 逐个读两个脚本,
要求都能从 meta/meta.go 取权威版本、都校验 git describe 形态。
变异(把 package-linux.sh 改回旧规则)咬住并准确指出是哪个脚本。
实测:不传 VERSION 跑 package-linux.sh,输出
[PKG] warn: git describe 给出非版本串(...),改用 meta.Version=1.4.0
Version: 1.4.0
* fix(test): 跨仓判据在 CI 上应 skip 而非 fail(修 PR 双红)
CI 上 "Go build / vet / test" 与 "Build linux/amd64" 双红:
--- FAIL: TestHmapdevTemplateImplementsAllPluginMgrMethods
bridge_sync_test.go:170: 读模板: open
../../../third_party/homeagent-sdk/tools/hmapdev/templates/proc_main.go.tmpl:
no such file or directory
## 根因:判据依赖了一个**被 gitignore 的路径**
`.gitignore:33` 有 `third_party/homeagent-sdk/tools/`(SDK 仓自治,本仓不跟踪)。
于是:
· 本机(完整 SDK 检出)有该文件 ⇒ 判据通过
· CI(只 checkout 本仓)没有该文件 ⇒ Fatalf ⇒ **必红**
我提交时只在本机跑测试、没看 CI,所以这个问题拖到 PR 才暴露。
## 修法
文件缺失时 `t.Skipf` 而不是 `t.Fatalf`。
★ 这不是"放弃检查":
· 跨仓一致性由 **SDK 仓自己的判据**守着
(tools/hmapdev/scaffold_stdout_test.go 等),那里才是模板的真实归属地
· 本判据在本机开发时**仍然生效**(已验证:有文件时 PASS,缺文件时 SKIP)
## 验证
· 本机(有文件) → PASS(判据真跑)
· 模拟 CI(移走 tools/)→ SKIP(不再 fail)
· 全量 53 包 ok / 0 fail
## 顺带
同目录还有 e2e_template_test.go 也读 tools/,但它是 `//go:build` 门控的
e2e 判据,CI 默认不跑,无需处理。
---------
Co-authored-by: JianFeeeee <JianFeeeee@users.noreply.gitcode.com>
This commit is contained in:
@ -1,7 +1,22 @@
|
||||
# CommonDev:主管式多仓库开发流水线(设计 · 未实现)
|
||||
|
||||
> **状态:设计稿,尚未实现。** 本文只描述目标机制、已核实的事实、未决问题与建议的
|
||||
> 落地顺序。接手者请从 §9 开始,并按 §10 的自检清单逐条验证本文的每一项事实陈述。
|
||||
> **状态:架构已变更(见 §12)。** **git 托管平台专有的工具**已移出插件、进 skill;
|
||||
> 其余机制(台账、工作目录、pi 会话与通道、判据与变异复核、冲突预检、凭据)保留。
|
||||
> §4.1 里的平台工具清单**不再是实现目标**;§13 保留历史实现审计供追溯。
|
||||
> 接手者请从 §12 开始读。
|
||||
>
|
||||
> **续篇(必读)**:`docs/zh/commondev-agent-runtime-design.md` —— 运行时与内核接入
|
||||
> (工程对话四元组、内嵌/SSH 两形态、inputch 路由、L4 归属、语义缺口)。
|
||||
> **本文 §6.1 的结论已被续篇修正**(只论证了 cwd 一维)。
|
||||
>
|
||||
> **★ 安全模块(计划追加)**:`docs/zh/kernel-secret-firewall-design.md` ——
|
||||
> 敏感信息拦截**下沉为内核安全模块**(用户纠正:LLM 调用本身不可信,插件拦截不算边界)。
|
||||
> 该文成立后,**本插件侧的加密 shell 会大幅变薄**(不再自带加密库/别名表)。
|
||||
>
|
||||
> **★ 设计总纲 [已定|2026-10-13]**:插件对 pi 做的事,本质就是
|
||||
> **「把给 pi 发消息包装一下、把 pi 返回包装一下」,并深度融合内核的 IO 抽象与中断系统**
|
||||
> —— 即 **pi = 一个 Device**。**不自己造调度器/会话路由/中断**,全部接到内核已有的那套上。
|
||||
> 详见续篇 §2.5 / §3.1。
|
||||
>
|
||||
> **标记约定**(沿用本仓惯例):
|
||||
> - **[已核]** = 本文写作时已在本机实测/源码核对确认,附证据
|
||||
@ -57,11 +72,20 @@
|
||||
项目列表(每项目一条)
|
||||
└─ 分支列表(每分支:名字 / 分配给哪个 agent / 工作内容 / 验收状态 / base_sha)
|
||||
└─ 临时工作目录(插件数据目录下 program/<hash>-<name>/)
|
||||
└─ 独立仓库的独立分支(clone 或 worktree)
|
||||
└─ 拉起 agent(注入提示词,cwd = 该目录)
|
||||
└─ agent 自审 → 主管复审 → 插件复核 → 提 PR → 合入
|
||||
└─ 独立仓库的独立分支(本地 clone)
|
||||
└─ ★ 拉起 pi 并拿到 session_id
|
||||
└─ ★ 才有通道:插件注册 inputch commondev/<id>
|
||||
+ outputch commondev_<id>
|
||||
└─ 主 agent 建驻留子并划入这对通道
|
||||
└─ 子调 output_send__commondev_<id> 下任务
|
||||
└─ agent 自审 → 主管复审 → 插件复核 → 提 PR → 合入
|
||||
```
|
||||
|
||||
> **顺序要点(容易搞反)**:**先有 session,才有通道**。通道是“与某个具体 pi 会话
|
||||
> 对话的入口”,没 session 注册出来就是空壳。所以:
|
||||
> **建工程 → 拉 pi(拿 session)→ 注册通道 → 建驻留子并划入 → 投递**。
|
||||
> 详见续篇 §2.2。
|
||||
|
||||
**插件应提供的能力面**(用户列举):
|
||||
|
||||
1. 远程 git 仓库全套操作(clone / fetch / branch / commit / push / PR / merge)
|
||||
@ -221,6 +245,22 @@ commondev_branch_close 收尾清理(merge 后或废弃时)
|
||||
commondev_workdir_prepare 准备/复用临时工作目录(mirror 派生,见 §4.3)
|
||||
commondev_workdir_clean 清理临时目录(含凭据残留检查,见 §7.2)
|
||||
|
||||
# 内置 pi 的运行时(★ 本质是“包一下”:发消息 / 收返回,见续篇 §2.5 §3.1)
|
||||
commondev_agent_up 拉起 sidecar(内嵌 pi;remote 时 ssh)并登记台账
|
||||
commondev_agent_state 查 session / runtime / 健康
|
||||
commondev_agent_down 收尾(幂等全清,不留孤儿)
|
||||
# ★ 下任务**不走插件工具**:子调内核原生的 output_send__commondev_<id>(payload=…),
|
||||
# 插件写的通道 handler 负责“包一下”→ sidecar.prompt(见续篇 §2.5)。
|
||||
# “中断/纠偏 pi”走**内核已有的** InjectInterrupt(L3),也不另造工具。
|
||||
commondev_pi_plugin_install ★ 为内置 pi 装插件(scope=project 临时 / plugin 全局)
|
||||
commondev_pi_plugin_remove 卸载(同 scope 语义)
|
||||
commondev_pi_plugin_list 列出已装(分 scope)
|
||||
commondev_pi_source_import ★ 把 HomeAgent 内部配置的 LLM 源导入内置 pi
|
||||
commondev_pi_source_add 添加一个源(直接写 pi 的 models.json/auth.json)
|
||||
commondev_pi_source_list 列出可用源(默认**不返回** api_key)
|
||||
commondev_pi_source_default ★ 指定内置 pi 默认用哪个源(写 settings.json)
|
||||
commondev_pi_config_get/set 读写 pi 配置(scope=plugin|project)
|
||||
|
||||
# 判据与复核
|
||||
commondev_test_define **主管**定义一条判据(落地成可执行脚本 + 期望)
|
||||
commondev_test_run 执行判据集合(正向)
|
||||
@ -258,33 +298,124 @@ review_runs
|
||||
没有它,插件无法判断「两个 PR 改了同一文件时第二个该不该 rebase、能不能自动合」,
|
||||
出问题也无法重放。建议在 `commondev_pr_open` 前做 conflict 预检。
|
||||
|
||||
### 4.3 临时工作目录:用 mirror 派生,不要每个任务全量 clone
|
||||
⇒ **刀 3 已落地**(见 §4.6)。
|
||||
|
||||
用户描述「每次修改都是拉取仓库」。直接 clone 的问题:Go 这类仓库一份全量历史可达
|
||||
数百 MB,并行十个就不合适。
|
||||
### 4.3 临时工作目录:mirror + **本地 clone(硬链接)** [已定 2026-10-13]
|
||||
|
||||
**建议**(**[待定]**,需接手者评估):
|
||||
**用户拍板**:每个工程对话一个独立目录 `program/<hash>-<name>`(`name` = **仓库名**),
|
||||
**该分支修改提交合入后直接删除该目录**。因此需要「删掉即彻底」,这直接排除了两个
|
||||
候选方案(均实测)。
|
||||
|
||||
```
|
||||
<插件数据目录>/program/
|
||||
mirrors/<platform>-<repo>.git/ # git clone --mirror,每个项目一份,定期 fetch
|
||||
work/<hash>-<name>/ # 每个任务一份,由 mirror 派生
|
||||
mirrors/<platform>-<repo>.git/ # git clone --mirror,每仓库一份,定期 fetch
|
||||
<hash>-<name>/ # 每个工程对话一份;合入后整目录删除
|
||||
```
|
||||
|
||||
派生方式二选一(都要实测磁盘与耗时):
|
||||
- `git clone --reference <mirror> <mirror> <workdir>`(`--dissociate` 可选)
|
||||
- `git worktree add`(注意 §3.8:同分支不可双挂——这反而是好事,能防两个 agent 抢同一分支)
|
||||
`<hash>` 用「项目名 + 分支名」的短哈希,**保证可重算**(便于清理孤儿目录),
|
||||
并且 **hash 是目录唯一性的来源**。
|
||||
|
||||
> **注意**:用户原话是「每个目录下是**独立仓库**的独立分支」。若严格按「独立仓库」,
|
||||
> worktree 就不适用(它是同仓库多工作区)。**这是需要与用户确认的选择**(§8.1)。
|
||||
#### 目录映射:★ 同一仓库的不同分支必然是不同目录 [已定 2026-10-14]
|
||||
|
||||
### 4.4 目录命名
|
||||
用户明确要求:
|
||||
|
||||
用户描述 `program/hash-name`。建议:
|
||||
- `hash` 用 `项目名 + 分支名` 的短哈希,**保证可重算**(便于清理孤儿目录)
|
||||
- 目录内记录一份 `_commondev.json`(项目/分支/agent/base_sha/凭据指纹),便于事后审计
|
||||
> 「即使是同一个仓库,只要是不同的分支都拉到 hash-name 下使用 hash 区分,
|
||||
> 插件通过哈希表映射工程分支与文件夹」
|
||||
|
||||
```
|
||||
branchKey(project, branch) = project + "\x00" + branch
|
||||
└─ shortHash() → <hash>(8 位 16 进制,可重算)
|
||||
└─ program/<hash>-<name> 其中 name = 分支名派生(可读性)
|
||||
```
|
||||
|
||||
**两条职责必须分清**(这是本节能防住的事故):
|
||||
|
||||
| 要素 | 承担什么 | 为什么 |
|
||||
|---|---|---|
|
||||
| **hash** | ★ **唯一性** | 一个仓库可并行多条分支;唯一性只能由 hash 承担,**不依赖 name** |
|
||||
| **name** | 可读性 | 清理孤儿/审计时纯 hash 无信息量 |
|
||||
|
||||
若只按仓库名建目录,两条分支会**共用同一个工作树** —— 那是灾难性的:
|
||||
agent A 的未提交改动会出现在 agent B 的 `git status` 里,且「独立 clone」的前提(§4.3)不成立。
|
||||
|
||||
**验证**(已实施):`branchDir("TrueAgent","feat/a") != branchDir("TrueAgent","feat/b")`,
|
||||
且同输入可重算。变异(hash 只用项目名)→ 报红。
|
||||
|
||||
**⚠️ name 不能承担唯一性**:分支名经派生后可能撞(如 `feat/a` 与 `feat.a` 都派生为 `feat_a`),
|
||||
所以目录名必须带 hash。
|
||||
|
||||
**映射是双向的、且只有一处真相**:目录名可由 `(project, branch)` 重算;
|
||||
反查靠台账的 `branch.Workdir`(`resolveBranchDir`)。
|
||||
**绝不要在映射函数之外另算目录名**。
|
||||
|
||||
**孤儿检测**:`program/` 下符合 `<hash>-<name>` 形态、但台账里无记录的目录 = 孤儿
|
||||
(`listOrphanDirs`)。非本插件形态的目录不碰。
|
||||
|
||||
#### 为什么是本地 clone,而不是 worktree 或 `--reference`(均实测)
|
||||
|
||||
| 方案 | `rm -rf` 之后 | 外部依赖 | 结论 |
|
||||
|---|---|---|---|
|
||||
| **worktree** | **`.git/worktrees/<name>` 残留**,状态变 `prunable`;必须再跑 `git worktree prune` 才清 | 无 | ❌ 删除不彻底;中央仓需持续维护 |
|
||||
| **`--reference`**(本稿初版建议) | 干净 | **★ `.git/objects/info/alternates` 指向 mirror** | ❌❌ **mirror 一旦被清理,work tree 彻底损坏**:实测 `error: unable to normalize alternate object path`,之后**每次** git 操作都报错 |
|
||||
| **本地 clone** | **干净**(只剩源仓) | **无** | ✅ **对象是硬链接** ⇒ 省磁盘(实测两侧对象 inode 完全相同);删掉即彻底 |
|
||||
|
||||
```bash
|
||||
git clone <mirror> <workdir> # 本地 clone:硬链接共享对象、无 alternates
|
||||
```
|
||||
|
||||
> **为什么硬链接这件事重要**:本地 clone 复制对象时用硬链接(实测 inode 相同,如
|
||||
> `5836713`),因此并行 N 份工作树**几乎不额外占磁盘**,同时**每个目录都是完整自洽的仓**
|
||||
> —— 既无 worktree 的中央元数据,也无 `--reference` 的外部依赖。
|
||||
> 两个要求(「删除彻底」与「不是每任务全量 clone」)同时满足。
|
||||
|
||||
#### 已考虑并否决的替代方案
|
||||
|
||||
> **用 worktree + 只删分支(不删目录)**:即「删掉分支名但留下 worktree 目录」。
|
||||
> **不采纳**:残留的 checkout 会持有一个已删除分支的引用,下一次同名分支的
|
||||
> worktree 创建可能冲突(实测 `fatal: 'feat/a' is already used by worktree`);
|
||||
> 且目录永远删不干净,与用户要求的「合入后直接删除目录」相悖。
|
||||
|
||||
#### ⚠️ 失去的一条保护,必须用台账补回
|
||||
|
||||
worktree 天然禁止「同一分支被两棵工作树同时挂」—— 实测
|
||||
`fatal: 'feat/a' is already used by worktree at '...'`。**改用独立 clone 后这条保护消失了**:
|
||||
两份 clone 可以在同名分支上各自 push,后者会被拒(非 fast-forward)而暴露问题,
|
||||
但**在 push 之前**两边都以为自己是唯一。
|
||||
|
||||
⇒ **必须在台账层补一道唯一性闸**:
|
||||
|
||||
- `branches` 表对 `(project_id, name, status IN ('open','review'))` 建**唯一索引**;
|
||||
- `commondev_branch_open` 遇到同名且仍开着(或已进 review)的分支必须**响亮拒绝**,
|
||||
不是静默复用。
|
||||
|
||||
(用户为「一个仓库并行开发」选择独立 clone 是**正确的** —— 代价只是这道补偿闸。)
|
||||
|
||||
#### 目录生命周期
|
||||
|
||||
```
|
||||
branch_open
|
||||
└─ 建 <hash>-<name>/(由 mirror 本地 clone)
|
||||
└─ 干活 → 提交 → push → PR
|
||||
└─ 合入(或废弃)
|
||||
└─ ★ 整目录删除(不保留、不归档)
|
||||
```
|
||||
|
||||
> **删除是收尾动作的一部分**:合入后不删,下次同名分支会与残留目录混淆;
|
||||
> 并行开发的意义就是「用完即清」。与内核「子不得比父活得久、无残留」同一条纪律
|
||||
> (见续篇 §4 孤儿清理)。
|
||||
|
||||
### 4.4 目录命名与并行开发
|
||||
|
||||
- 目录:`program/<hash>-<name>`,`<hash>` = 项目名+分支名短哈希(★ **唯一性来源**),
|
||||
`name` = 分支名派生(**只做可读性**,不承担唯一性,见 §4.3)
|
||||
- **一个仓库可并行开发**:每分支一份独立 clone ⇒ 互不污染、可同时推进
|
||||
- 目录内记录 `_commondev.json`(项目/分支/agent/base_sha/凭据指纹),便于事后审计
|
||||
- **不要**把凭据写进目录(见 §7.2)
|
||||
|
||||
> **并行是有前提的**(见 §8.4):并行的必须是**互不相干的模块**。
|
||||
> 若任务间有依赖(A 改接口、B 用接口),两棵独立 clone 互不可见,
|
||||
> 问题会集中到 merge 时爆发 —— 那时应先拆边界,再谈并行。
|
||||
|
||||
### 4.5 ★ 复核必须是两步:正向 + 变异(本文最重要的设计主张)
|
||||
|
||||
用户原话:
|
||||
@ -327,6 +458,143 @@ review_runs
|
||||
|
||||
---
|
||||
|
||||
### 4.7 ★ 刀 3:conflict 预检与 base_sha [✅ 已完成 · 真实 git 实测]
|
||||
|
||||
#### 为什么这一刀必须做
|
||||
|
||||
并行开发(§4.4)让冲突从"可能"变成"必然":每分支一份独立 clone,
|
||||
两棵树**互不可见**。冲突不会在开发时暴露,而是**全部堆到 merge 时爆发** ——
|
||||
那时判据已经变绿、复审已经通过,却发现合不进去。
|
||||
|
||||
⇒ 必须在**提 PR 之前**就发现。
|
||||
|
||||
#### 做法:`git merge-tree --write-tree`(**完全不碰工作区**)
|
||||
|
||||
实测 git 2.53 的退出码语义:
|
||||
|
||||
| 退出码 | 含义 |
|
||||
|---|---|
|
||||
| `0` | 可干净合并 |
|
||||
| `1` | 有冲突 |
|
||||
| 其它 | 命令本身失败(**不能当成"无冲突"**)|
|
||||
|
||||
★ **它不写索引、不动 HEAD、不留 MERGE_HEAD** ⇒ 可以在任何时候安全地跑,
|
||||
不需要 stash/checkout,也不会破坏正在进行的开发。这一条有判据钉住
|
||||
(跑完预检后断言 HEAD 未变、无 MERGE_HEAD、工作区零改动)。
|
||||
|
||||
#### 三类结果必须分开(防静默降级)
|
||||
|
||||
| 结果 | 语义 |
|
||||
|---|---|
|
||||
| `Mergeable: true` | 检查过,可合并 |
|
||||
| `Mergeable: false` + `Conflicts` | 检查过,有冲突(列出文件)|
|
||||
| **`Skipped: true`** | **没检查**(目标分支本地不存在 / 非 git 仓库 / merge-tree 不可用)|
|
||||
|
||||
★ **`Skipped` 时绝不声称可合并** —— 把"没检查"当成"检查通过"是最危险的
|
||||
静默降级,调用方会以为安全。有判据**逐条覆盖全部 Skipped 出口**
|
||||
(初版只测了其中一条,于是另一条的变异不报红 —— 判据覆盖不全等于留洞)。
|
||||
|
||||
#### `base_sha` 的捕获时机
|
||||
|
||||
在 `runPrepareWorkdir` 里、**目录就绪后立刻**记 —— 必须在 agent 开始改之前。
|
||||
agent 一旦提交,HEAD 就不再是"这条线从哪出发"。
|
||||
|
||||
★ **只记第一次**:重开同一分支时若覆盖基点,"这条线从哪出发"就永久丢失,
|
||||
冲突判定与事后重放都失去依据。
|
||||
|
||||
#### 新增工具
|
||||
|
||||
| 工具 | 作用 |
|
||||
|---|---|
|
||||
| `commondev_conflict_check` | 提 PR 前预检可合并性(只读,`ParallelSafe`)|
|
||||
| `commondev_parallel_status` | 盘点并行推进的分支线(按项目分组),派活前确认边界 |
|
||||
|
||||
#### ☆ 实测发现
|
||||
|
||||
**冲突解析有两条独立路径**,缺一不可:
|
||||
|
||||
1. `CONFLICT (content): Merge conflict in <path>` 行
|
||||
2. `100644 <oid> <stage>\t<path>` 的 ls-tree 阶段行
|
||||
|
||||
前者覆盖常见情形,后者覆盖**不打印 CONFLICT 行**的冲突类型(如 rename/rename)。
|
||||
实测:只保留路径 ①、把 ② 的判定前缀改坏,**"仅 ls-tree 形态"的判据会报红** ——
|
||||
说明 ② 是真·载重路径,不是冗余。
|
||||
|
||||
#### 判据与变异
|
||||
|
||||
`conflict_parse_test.go` 8 条(含**真实 git 仓库**的端到端:可合并 → 合入后冲突
|
||||
→ 工作区零影响);变异 6 条全部咬住(含 1 次暴露判据覆盖不全、1 次暴露
|
||||
"冗余路径其实是载重路径")。
|
||||
|
||||
---
|
||||
|
||||
### 4.8 ★ 刀 4:判据与复核(正向 + 变异) [✅ 已完成 · 真实 go test 端到端]
|
||||
|
||||
#### 核心命题:插件 GREEN ≠ 改对了
|
||||
|
||||
设计 §4.5 的主张在本刀**落地并用真实数据验证**。我在 `/tmp` 复刻过最小场景:
|
||||
|
||||
```go
|
||||
func Max(a, b int) int { if a > b { return a }; return b }
|
||||
```
|
||||
- 松判据只测 `Max(5,3) == 5`(漏了 `b>a`)
|
||||
- 变异 `return b` → `return a`(**真 bug**)⇒ **判据仍全绿**
|
||||
- 补一条 `Max(3,5) == 5` ⇒ 同一变异**立刻报红**
|
||||
|
||||
⇒ **没有反向阶段,就是在给「测试通过」这个信号不断充值信用,
|
||||
直到它骗一次大的。**
|
||||
|
||||
#### 两阶段
|
||||
|
||||
| 阶段 | 做什么 | 不通过意味着 |
|
||||
|---|---|---|
|
||||
| **正向** forward | 跑判据 | 实现有 bug |
|
||||
| **反向** mutate ★ | 把文件里 `old` 换成 `new`,期望判据**报红** | **判据太松**(该变异没被看见)|
|
||||
|
||||
#### 结论三档(**不合并成一个绿勾**)
|
||||
|
||||
| verdict | 含义 |
|
||||
|---|---|
|
||||
| `pass` | 正向全过 **且**全部变异被咬住 |
|
||||
| `pass_with_gaps` | 正向过,但**有变异未被咬住** 或**有判据没定义变异规则** |
|
||||
| `fail` | 有判据未通过 |
|
||||
|
||||
★ `pass_with_gaps` **不是"通过"** —— 它是「判据没覆盖到」。
|
||||
把它说成通过,就是本刀要消除的那种"信号充值"。
|
||||
|
||||
#### 新增工具
|
||||
|
||||
| 工具 | 作用 |
|
||||
|---|---|
|
||||
| `commondev_test_define` | 主管定义判据(**幂等**:同定义重复登记不改动);必须同时给 `mutations` |
|
||||
| `commondev_test_list` | 列出判据(含变异条数 —— **为 0 的是复核缺口**)|
|
||||
| `commondev_review_submit` | 跑复核(后台作业:判据可能含编译/压测,超 60s 工具上限)|
|
||||
| `commondev_review_result` | 取结论与历史(含逐条证据)|
|
||||
|
||||
#### ☆ 实现要点(每条都对应一个可致错的点)
|
||||
|
||||
| 点 | 为什么 |
|
||||
|---|---|
|
||||
| **必须 `-count=1`** | Go 默认缓存测试结果。实测:变异后若命中缓存,测试**根本没跑**,"报红"无从谈起 |
|
||||
| **变异必须恢复文件** | `defer` 保证 panic 也恢复;且恢复后**校验内容一致**——恢复不回去比变异本身危险(agent 会基于坏代码继续改)|
|
||||
| **`old` 必须恰好出现一次** | 出现 0 次 ⇒ 变异无效,却被误报成"判据太松"(**假警报**);出现多次 ⇒ 可能改错地方,却报"咬住"(**假通过**)|
|
||||
| **通过由退出码判定** | 不是看输出里有没有 `PASS`(输出可被伪造/污染)|
|
||||
| **固定判据顺序** | map 遍历随机 ⇒ 报告不可对照 |
|
||||
|
||||
#### 判据与变异
|
||||
|
||||
`criteria_test.go` 10 条 + `e2e_criteria_test.go` 2 条(**真实项目 + 真实 go test**);
|
||||
变异 5 条对**复核机制本身**做变异(元层级),全部咬住 ——
|
||||
包括「变异后不恢复文件」「变异未咬住却报 ok」「无变异规则却给 pass」。
|
||||
|
||||
#### ⚠️ 尚未做(原计划的"模板库"部分)
|
||||
|
||||
设计 §6.4 原写「测试模板库(压力/接口/编译)」,本次落地的是**机制**
|
||||
(判据定义 + 复核执行),**预置模板库**仍未做。理由:
|
||||
机制不成立时模板是摆设;现在机制已用真实数据证明,模板可以后续按需补。
|
||||
|
||||
---
|
||||
|
||||
## 5. 与现有系统的关系
|
||||
|
||||
| 现有物 | 关系 |
|
||||
@ -343,17 +611,34 @@ review_runs
|
||||
|
||||
## 6. 关键决策建议(未拍板,供接手者与用户确认)
|
||||
|
||||
### 6.1 拉起 agent:走 CLI,不走 A2A/ACP [本文建议]
|
||||
### 6.1 拉起 agent:走 CLI 家族,不走 A2A/ACP [本节已由续篇修正]
|
||||
|
||||
> **⚠️ 修正(2026-10-13):** 本节初稿只论证了 **cwd** 一个维度就下了结论,
|
||||
> **论据不全**。用户明确要求「投递必须落到**同一个会话**,不能前一个没做好就投给新会话」,
|
||||
> 而 `pi -p` 每轮一个进程,**无法保证**这一点(实测:漏传 `--session-id` 会静默开新会话;
|
||||
> 并发传同一个 id 会写坏会话)。
|
||||
>
|
||||
> 正确的两轴(会话粒度 × cwd 由谁承担)与形态取舍,见续篇
|
||||
> **`docs/zh/commondev-agent-runtime-design.md` §1.2 / §3**。
|
||||
> 本节保留,只作为「A2A/ACP 为何被排除」的论据(该结论本身没变)。
|
||||
|
||||
| 方案 | 优点 | 缺点 |
|
||||
|---|---|---|
|
||||
| **`pi -p`(CLI)** | 进程 cwd 即工作目录(**已实测**);一次性进程,天然按任务隔离;零协议改动 | 每次冷启动(可接受);需要自己管进程与超时 |
|
||||
| **`pi -p`(CLI)** | 进程 cwd 即工作目录(**已实测**);一次性进程,天然按任务隔离;零协议改动 | **无法保证续投同一会话**(见上);每次冷启动;无中途纠偏 |
|
||||
| **`pi --mode rpc`(常驻)** | 同 cwd;同会话续投;`steer`/`follow_up` 可中途纠偏;`get_state` 可断言 session 未变 | 要实现/复用 RPC 客户端;stdout 必须持续读 |
|
||||
| **Node SDK(进程内)** | 最贴身;`createAgentSession({cwd})` 可控 cwd 与会话 | 需 Node 宿主 |
|
||||
| `dsh headless` | 同上 | 本机凭据失效,需先修 |
|
||||
| 扩建 A2A | 复用现有插件 | 要改扩展 + payload;ACP 的 session 模型本就假设长驻,加 cwd 是**违背其设计意图** |
|
||||
| 扩建 ACP | 同上 | 同上 |
|
||||
| 扩建 A2A | 复用现有插件 | 要改扩展 + payload;**参数里没有 cwd**(§3.4) |
|
||||
| 扩建 ACP | 同上 | 同上;且 ACP 的 session 模型本就假设长驻,加 cwd 是**违背其设计意图** |
|
||||
|
||||
**建议**:第一刀用 `pi -p`。它同时满足「指定目录」与「一次性拉起」两个硬需求,
|
||||
且不需要动任何协议。A2A/ACP 留给「需要与**常驻** agent 对话」的场景。
|
||||
**修正后的建议**:**内嵌 sidecar 为默认形态**,远程 SSH 由配置切换
|
||||
(两形态共用同一 sidecar,差异只在传输层)。`pi -p` 仅作侧车不可用时的降级,
|
||||
**不是正式形态**。A2A/ACP 依然排除(「无 cwd」这条事实没变),
|
||||
排除理由与本需求无关:它们是**常驻 peer** 的模型,而本需求是**按工程对话拉起**。
|
||||
|
||||
**选型已定 [2026-10-13 用户拍板]**:**默认内嵌(进程内 pi 实例)**,
|
||||
配置可切到**远程 SSH**;远端同样**创建目录并在该目录拉起 pi 实例**。
|
||||
远端环境缺失时**询问用户、不得擅自安装**(见续篇 §3.4)。
|
||||
|
||||
### 6.2 第一刀只做一个平台、一个项目、一个任务
|
||||
|
||||
@ -368,14 +653,57 @@ review_runs
|
||||
|
||||
**这一刀的目的不是功能完整,是证明「假绿检测」这一环真的能工作。**
|
||||
|
||||
### 6.3 权限模型:让「必须有主管复审」成为机制而非礼貌 [建议]
|
||||
### 6.3 权限模型:让「必须有主管复审」成为机制而非礼貌 [✅ 已完成 · 真实 Gitea 实测]
|
||||
|
||||
若执行 agent 所用的凭据能 push 主干,则「主管复审」只是**流程约定**——想绕过随时能绕过。
|
||||
|
||||
建议(依赖 Gitea 分支保护):
|
||||
- 执行侧凭据:**只能 push `feat/*`,不能 push `main`,不能合 PR**
|
||||
- 主管侧凭据:**能合 PR**
|
||||
- 这样「用户只与主管对接」在用户不在场时**也成立**
|
||||
**机制**(已在真实 Gitea 1.24.2 上逐条实测):
|
||||
|
||||
| 动作 | 执行侧凭据 | 主管侧凭据 |
|
||||
|---|---|---|
|
||||
| push `main` | ❌ `pre-receive hook declined` | (不需要)|
|
||||
| push `feat/*` | ✅ | ✅ |
|
||||
| 开 PR | ✅ | ✅ |
|
||||
| **自合 PR(无审批)** | ❌ `Does not have enough approvals` | ❌ |
|
||||
| 审批(带 commit_id)| ❌(角色不符) | ✅ |
|
||||
| 合 PR(有审批)| — | ✅ |
|
||||
|
||||
★ **限制的真身是平台的分支保护**(`enable_push:false` + `required_approvals≥1`),
|
||||
不是插件里的判断 —— 插件在信任边界外,改一行代码就能绕过;平台的
|
||||
pre-receive hook 不能。这是刀 2 与「流程约定」的本质区别。
|
||||
|
||||
**实现落点**:
|
||||
|
||||
| 件 | 作用 |
|
||||
|---|---|
|
||||
| `Account.Role`(`executor`/`supervisor`) | 标注凭据能做什么;空值按 supervisor(兼容老数据)|
|
||||
| `commondev_protect_branch` | 幂等建立分支保护(只改必要项,不反复改用户仓库)|
|
||||
| `commondev_pr_approve` / `commondev_pr_merge` | 主管侧动作,按角色选凭据 |
|
||||
| `accountForRole` | 角色不符时**明确报错**(而非让平台报一个看不懂的错)|
|
||||
|
||||
**`defaultProtection()` 的四条**(每条堵一条绕过路径,均有判据钉住):
|
||||
|
||||
| 字段 | 值 | 堵住的绕过 |
|
||||
|---|---|---|
|
||||
| `enable_push` | `false` | 执行侧直接推 main |
|
||||
| `required_approvals` | `≥1` | 无人审批也能合 |
|
||||
| `block_admin_merge_override` | `true` | **admin 绕过**(主管账号的后门)|
|
||||
| `dismiss_stale_approvals` | `true` | 审批后追加提交偷换内容 |
|
||||
|
||||
#### ☆ 实测踩到的三个 API 坑(都不是文档里写着的)
|
||||
|
||||
| 坑 | 现象 | 真因 |
|
||||
|---|---|---|
|
||||
| **`event` 必须是 `APPROVED`** | 传 `APPROVE` **返回 200 不报错**,但 review 落成 `PENDING` ⇒ 合并报 "Does not have enough approvals"(看起来像权限问题)| 枚举少了个 D |
|
||||
| **审批必须带 `commit_id`** | 不带时同样落成 `PENDING`(与 event 是两个独立条件)| 审批要绑定具体提交 |
|
||||
| **`required_approvals` 也管 admin** | admin 没有绕过审批的特权 | 这是**正确的** —— 否则"复审"对 admin 形同虚设 |
|
||||
|
||||
附带(正确行为但要知):`REQUEST_CHANGES` 会**阻断**合并,即使同时有 `APPROVED`。
|
||||
|
||||
#### 判据与变异
|
||||
|
||||
`protection_test.go` 11 条;变异 9 条全部咬住(含 1 次暴露判据测错层次 ——
|
||||
只测 `platformMessage()` 没测 `MergePR` 是否调用它)。
|
||||
|
||||
### 6.4 落地顺序
|
||||
|
||||
@ -383,9 +711,18 @@ review_runs
|
||||
|---|---|---|
|
||||
| **0** | **修「能拉起 agent」**(§3.6) | 前提是否成立 |
|
||||
| **1** | 单平台单项目:mirror → 分支 → 拉起 → 判据+变异 → PR | 机制成立,假绿可检测 |
|
||||
| 2 | 接 Gitea 分支保护 + 执行/主管双凭据 | 复审是机制不是礼貌 |
|
||||
| 3 | 多项目 + 并行 + conflict 预检 | 可扩展 |
|
||||
| 4 | 测试模板库(压力/接口/编译) | ⚠️ **放最后** |
|
||||
| 2 | 接 Gitea 分支保护 + 执行/主管双凭据 | ✅ **已完成**(真实 Gitea 1.24.2 实测,见 §6.3)|
|
||||
| 3 | 多项目 + 并行 + conflict 预检 | ✅ **已完成**(真实 git 实测,见 §4.6)|
|
||||
| 4 | 判据与复核(正向 + 变异) | ✅ **已完成**(真实 go test 端到端,见 §4.8)|
|
||||
| **S** | **★ 内核安全模块(Secret Firewall)** —— 见
|
||||
`docs/zh/kernel-secret-firewall-design.md`。
|
||||
插在 **provider 层**(已核:构造 `CompletionRequest` 有 7 处,task 层拦不全)。
|
||||
**它先行的理由**:插件把密钥明文直接投给 LLM(用户拍板)—— **没有这道闸,
|
||||
密钥就真的发出去了**。而 2/3/4 刀都要用凭据(push / PR / 测试),
|
||||
它们建立在“提交密钥是安全的”这个前提上 ⇒ **安全模块是它们的前提**。 | 凭据不泄露 |
|
||||
|
||||
> **第 S 刀的位置说明**:它不排在 2/3/4 之后,而是**与刀 2 并行且先行**。
|
||||
> 这与刀 0 同理:**前提不成立时,不要继续构建上层**。
|
||||
|
||||
> **第 4 刀为什么放最后**:它最容易写,也最容易变成摆设。先让「变异能咬住」
|
||||
> 这一环成立,模板才有意义。
|
||||
@ -408,6 +745,49 @@ commondev 的凭据是**能 push 用户所有仓库**的凭据,性质完全不
|
||||
**不要一个 token 通吃**
|
||||
4. 接 `git push` 时走 **credential helper / 环境变量注入**,**不要**写进 git config 文件
|
||||
|
||||
#### ★ 变量填充引用 [已定 2026-10-14 · 用户要求]
|
||||
|
||||
用户明确要求:「gitea / gitcode / github 等主流系统的 ackey 保存与**通过变量填充引用**」。
|
||||
|
||||
**核心设计:按名字引用 + 值可写 `${ENV_VAR}`**
|
||||
|
||||
```
|
||||
commondev_account_add(name="gitea-local", platform="gitea",
|
||||
token="${GITEA_TOKEN}", ← ★ 引用,不是明文
|
||||
base_url="http://192.168.2.106:3000")
|
||||
|
||||
之后一切操作只带**凭据名**:
|
||||
commondev_branch_open(..., account="gitea-local") ← 参数里永远不出现 secret
|
||||
```
|
||||
|
||||
**为何要变量填充**(三个真实好处):
|
||||
|
||||
| 好处 | 说明 |
|
||||
|---|---|
|
||||
| 明文不落盘 | 文件里只有 `${GITEA_TOKEN}` |
|
||||
| 环境可切换 | 同一份配置,dev/prod 换环境变量即可 |
|
||||
| 轮换不需改数据 | 改环境变量 + 重启即可 |
|
||||
|
||||
**两条不可逆的纪律**:
|
||||
|
||||
1. **变量解析不到值 ⇒ 响亮报错**,绝不静默当空串。
|
||||
后者会退化成匿名访问,表现成 **401** 而非“凭据没配” —— 极难排查。
|
||||
实测:`${NONEXISTENT_VAR_ZZZ}` → 报“凭据引用的环境变量未设置: …”。
|
||||
2. **secret 绝不进任何返回值/日志**:`account_list` 只报 `token_ref`(引用本身不是秘密,
|
||||
对排查有用)与 `token_set` 布尔。实测:明文 token 在日志中出现 **0 次**。
|
||||
|
||||
**平台形状差异**(必须抽象,不能一个头部走天下):
|
||||
|
||||
| 平台 | API 前缀 | 认证头 |
|
||||
|---|---|---|
|
||||
| Gitea | `<base>/api/v1` | `Authorization: token <T>` ← **不是 Bearer** |
|
||||
| GitHub | `https://api.github.com` | `Authorization: Bearer <T>` |
|
||||
| GitCode | `https://api.gitcode.com/api/v5` | `Authorization: Bearer <T>` |
|
||||
|
||||
未知平台**必须报错**(不猜默认形状)。
|
||||
|
||||
**凭据可用性要真验**:`account_test` 调平台的 `/user` 端点,不是只看字符串非空。
|
||||
|
||||
### 7.2 临时目录的凭据残留 [风险]
|
||||
|
||||
任务跑完删目录后,**clone 里可能仍留着 token**(remote URL 带 token 是常见做法,
|
||||
@ -429,14 +809,16 @@ commondev 的凭据是**能 push 用户所有仓库**的凭据,性质完全不
|
||||
|
||||
## 8. 未决问题(需用户拍板)
|
||||
|
||||
### 8.1 「独立仓库」还是「worktree」?
|
||||
### 8.1 「独立仓库」还是「worktree」?[已解决 2026-10-13]
|
||||
|
||||
用户原话是「每个目录下是**独立仓库**的独立分支」。
|
||||
- 独立 clone:隔离彻底、删除干净、无「同分支双挂」冲突 —— 更贴合用户描述
|
||||
- worktree:省磁盘与网络,但不能叫「独立仓库」
|
||||
**用户拍板:独立仓库(本地 clone)。** 目录 `program/<hash>-<name>`(`name` = 仓库名),
|
||||
**合入后直接删除目录**。
|
||||
|
||||
本文建议:**mirror + 派生**(§4.3),本质仍满足「独立仓库」语义(每目录一份 checkout),
|
||||
但需要用户确认这符合预期。
|
||||
理由:worktree 的 `rm -rf` 不彻底(`.git/worktrees/` 残留,需 `prune`),
|
||||
而 `--reference` 有外部依赖(mirror 被清理即损坏)——**两者都不满足「删掉即彻底」**。
|
||||
实测与取舍见 §4.3。
|
||||
|
||||
**代价与补偿**:失去 worktree 的「同分支不可双挂」保护 ⇒ 台账层补**唯一性闸**(§4.3)。
|
||||
|
||||
### 8.2 主管(主 agent)自己的模型可用性
|
||||
|
||||
@ -534,7 +916,182 @@ grep -n "phase,omitempty" cmd/waiter/state.go # 判据匹配的是
|
||||
|
||||
---
|
||||
|
||||
## 11. 给接手者的三条提醒
|
||||
## 12. ★ 架构变更:平台知识进 skill(2026-10-15)
|
||||
|
||||
> **本节修正本文 §4.1。** 原清单里的**平台专有工具**不再实现在插件里;
|
||||
> 其余机制(台账、工作目录、pi 会话与通道、判据与变异复核、冲突预检、
|
||||
> 凭据保存与安全执行)**保留**。
|
||||
|
||||
### 12.1 用户拍板的边界
|
||||
|
||||
> 「我说的是 **git 部分 skill 化**啊,其他部分还是需要插件的」
|
||||
|
||||
| 层 | 负责 |
|
||||
|---|---|
|
||||
| **插件 commondev** | 项目台账、工作目录(mirror + 独立 clone)、pi 会话与通道、判据与变异复核、冲突预检、`enc_*` 凭据保存与安全执行 —— **40 个工具** |
|
||||
| **skill** | **各 git 托管平台的认证头、API 端点、PR 与分支保护流程** |
|
||||
|
||||
### 12.2 移出插件的部分(10 个工具 → skill)
|
||||
|
||||
| 原工具 | 现在怎么做 |
|
||||
|---|---|
|
||||
| `commondev_account_add/list/test/remove` | `enc_store` / `enc_list` + skill 里的认证头对照表 |
|
||||
| `commondev_repo_search` | `enc_shell` + `curl <API>/repos/search?q=` |
|
||||
| `commondev_pr_open` / `pr_status` | `enc_shell` + `curl -X POST .../pulls` |
|
||||
| `commondev_pr_approve` / `pr_merge` | `enc_shell` + `curl .../pulls/{n}/reviews`、`/merge` |
|
||||
| `commondev_protect_branch` | `enc_shell` + `curl -X POST .../branch_protections` |
|
||||
|
||||
删除的文件:`platform.go`、`protection.go`、`pr_workflow.go`(约 1270 行)。
|
||||
`credentials.go` 保留但**去掉了平台语义**(`platformSpec` / `Role` / `BaseURL`)。
|
||||
|
||||
### 12.3 为什么平台部分不该在插件里
|
||||
|
||||
1. **每接一个托管平台都要改 Go 代码** → 重编译 → 重打包 → 重启服务。
|
||||
而平台知识(哪个端点、什么认证头)本该和文档一起演进。
|
||||
2. **平台形状被硬编码**:`platformSpec` 的 switch 每加一家就长一截,
|
||||
**未列出的平台直接报错**——实测 GitLab / Gitee / Bitbucket / Azure DevOps 全不支持。
|
||||
而 agent 用 `curl` 本来什么平台都能打。
|
||||
3. **工具面膨胀**:50 个工具让模型选择成本上升。
|
||||
|
||||
⇒ 平台知识进 skill(改文档**立即生效**);插件只保留机器必须保证的机制。
|
||||
|
||||
### 12.4 保留的纪律(没变)
|
||||
|
||||
- 凭据不进 `config.db`(0644 全局可读)、不进 remote URL、不进命令行参数;
|
||||
- **没有「取回明文」的接口**:只能被 `@{别名}` 引用(S1);
|
||||
- 变量库不自动淘汰,泄漏控制靠主动删除;
|
||||
- `enc_shell` 三条保障:明文不进 argv(stdin)、输出脱敏、超时;
|
||||
- 「不要把测试通过当作改对了」——复核必须含**变异**阶段(§4.5 主张仍成立)。
|
||||
|
||||
### 12.5 判据(钉住不回退)
|
||||
|
||||
`alignment_test.go` 新增两条:
|
||||
|
||||
- `TestPlatformToolsAreNotInPlugin` —— 那 10 个平台工具不得再注册;
|
||||
- `TestNoPlatformSpecificCodeInPlugin` —— 源码里不得再出现 `platformSpec` /
|
||||
`AuthHeader:` / `DefaultAPI` / `RoleExecutor` 等平台专有符号
|
||||
(**排除注释**:说明"已移出"是允许的)。
|
||||
|
||||
**为何要判据而不只是文档**:文档说了还会有人(包括将来的我)图省事加回去,
|
||||
而那种回退**功能上看起来完全正常** —— 只是把可移植性换成了硬编码。
|
||||
|
||||
---
|
||||
|
||||
## 13. 功能完备性核对(2026-10-15)
|
||||
|
||||
逐章节对照设计文档核对实现,**19/19 通过**(脚本化核对,非人工目测):
|
||||
|
||||
| 设计条目 | 实现 |
|
||||
|---|---|
|
||||
| §4.1 工具面(扣除已移出的 10 个平台工具) | ✅ 40 个工具 |
|
||||
| §4.2 台账 schema(Project/Branch) | ✅ |
|
||||
| §4.3 mirror + 本地 clone(硬链接) | ✅ |
|
||||
| §4.3 ⚠ 唯一性闸(同名未结分支拒绝) | ✅ |
|
||||
| §4.3 目录名 `<hash>-<name>` | ✅ |
|
||||
| §4.4 可并行(每分支独立 clone) | ✅ |
|
||||
| §4.5 复核两步(正向 + 变异) | ✅ |
|
||||
| §4.7 `base_sha` 首次捕获 | ✅ |
|
||||
| §4.7 `merge-tree` 预检(不碰工作区) | ✅ |
|
||||
| §4.7 `Skipped` 与 `Mergeable` 分开 | ✅ |
|
||||
| §4.8 三档 verdict | ✅ |
|
||||
| §4.8 变异必须唯一匹配 | ✅ |
|
||||
| §7.2 凭据残留检查 | ✅ |
|
||||
| §7.2 remote 不带凭据 | ✅ |
|
||||
| 续篇 §2.2 先 session 后通道 | ✅ |
|
||||
| 续篇 §2.5 投递走内核 output 门 | ✅ |
|
||||
| 续篇 §3.6.3 直接写 pi 配置文件 | ✅ |
|
||||
| 续篇 §3.6.5 不静默回退宿主凭据 | ✅ |
|
||||
| S1 无「取回明文」接口 | ✅ |
|
||||
|
||||
### 13.1 LLM 源端到端(本轮新增)
|
||||
|
||||
新增 `llmsource_e2e_test.go`(5 条判据),把「内核源 → 插件导入 → pi 配置 →
|
||||
**真实调用成功**」整链收口:
|
||||
|
||||
| 判据 | 层次 | 验证 |
|
||||
|---|---|---|
|
||||
| `TestLLMSourceImportProducesValidPiConfig` | 纯逻辑 | models.json 形状、`openai`→`openai-completions` 映射、contextWindow 透传、0600 权限、defaultProvider |
|
||||
| `TestLLMSourceImportRejectsUnknownAdapter` | 纯逻辑 | 未知 adapter **必须报错**(猜默认会让 pi 用错协议,现象是「模型不回复」)|
|
||||
| `TestLLMSourceImportFailsWithoutKernelSources` | 纯逻辑 | 无源时报错,不生成空配置 |
|
||||
| `TestLLMSourceImportFromRealKernelConfig` | **真机** | 读生产 `config.db` 的真实字段形态,3 个源全部导入成功 |
|
||||
| `TestLLMSourceRoundTripAgainstRealEndpoint` | **真机** | 从**导入产物**(而非测试变量)读回地址与 key,真实 HTTP 调用成功 |
|
||||
|
||||
**真机结果**:
|
||||
|
||||
```
|
||||
✅ 真实内核配置导入成功:3 个源,默认=llmsproxy
|
||||
✅ 端到端通过:内核源 → 插件导入 → 用磁盘配置真实调用成功
|
||||
```
|
||||
|
||||
**为何必须端到端**:导入逻辑正确 ≠ pi 能用。中间三层各自可能出错
|
||||
(字段映射 / adapter→api 名 / 默认源写入),任何一层错了现象都是
|
||||
「pi 起不来 / No API key found」,而单元判据全绿。
|
||||
|
||||
**变异验证(4 条全咬住)**:
|
||||
|
||||
| 变异 | 结果 |
|
||||
|---|---|
|
||||
| adapter 映射成错的 api 名 | ❌ 报红(纯逻辑判据)|
|
||||
| base_url 不写入 | ❌ 报红 |
|
||||
| **api_key 不写 auth.json** | ❌ **真机 e2e 报红**(真实调用失败)|
|
||||
| 未知 adapter 猜默认值 | ❌ 报红 |
|
||||
|
||||
复现命令:
|
||||
|
||||
```bash
|
||||
cd third_party/homeagent-sdk/example/commondev
|
||||
K=$(sqlite3 /home/newqqagent/config.db "select value from config where key='core.llm.api_key';")
|
||||
HA_E2E_LLM=1 HA_E2E_API_KEY="$K" HA_E2E_REAL_DB=/home/newqqagent/config.db \
|
||||
go test -count=1 ./...
|
||||
```
|
||||
|
||||
### 13.2 判据与变异总览
|
||||
|
||||
| | 数量 |
|
||||
|---|---|
|
||||
| 判据 | **62** |
|
||||
| 变异(累计验证咬住) | 30+ |
|
||||
| 工具 | 40 |
|
||||
| 非测试代码行数 | 5437 |
|
||||
|
||||
---
|
||||
|
||||
## 14. 历史实现审计(2026-10-15 早)
|
||||
|
||||
本节是对原始设计清单的逐条回读结果;不要仅依据文档顶部的历史状态判断实现状态。
|
||||
|
||||
### 14.1 已实现并有测试/实测
|
||||
|
||||
| 设计项 | 实现/证据 |
|
||||
|---|---|
|
||||
| 刀 0/1:项目、分支、独立 clone、sidecar、通道、续投 | `plugin.go` / `sidecar.go`;branch/session/channel 端到端测试 |
|
||||
| 刀 2:双角色凭据、分支保护、审批、合并 | `protection.go`;真实 Gitea 1.24.2 矩阵 + 变异 |
|
||||
| 刀 3:`base_sha`、`merge-tree --write-tree`、Skipped 分离 | `conflict.go`;真实 git 仓库 + 变异 |
|
||||
| 刀 4:判据、正向、变异、三档 verdict、后台复核 | `criteria.go`;真实 `go test` 端到端 + 机制元变异 |
|
||||
| 原 §4.1 工具面 | 项目/分支/工作目录/agent/PR/判据/pi 配置/MCP/仓库搜索工具均已注册 |
|
||||
| Secret Firewall 与配置加密 | `internal/security`、`internal/config`;race 与三层回归 |
|
||||
| E-4 生产密钥注入 | `deploy/homeagent.service` 的 `LoadCredential`;真实 systemd 262 实测 tmpfs/0400/不进 environ |
|
||||
|
||||
### 14.2 设计文字与实现的取舍
|
||||
|
||||
- `commondev_agent_send`:续篇 §2.5 已明确废除;发送继续走内核原生 `output_send__...`,不重复造工具。
|
||||
- `commondev_test_run` / `commondev_test_mutate`:早期清单的两个单阶段入口已保留;`commondev_review_submit` 是两阶段一键入口。
|
||||
- `commondev_pi_plugin_*` 与细化稿的 `commondev_pi_package_*`:两套命名均注册为兼容别名;底层调用同一套 `pi install/remove/list/update`。
|
||||
- `commondev_project_open`:续篇早期别名已补,底层复用 `project_add`。
|
||||
- skill 双投不是运行时插件行为:主管侧 skill 由 HomeAgent 发布,执行侧 skill 由工作目录/发布包注入;插件不擅自把代码写进用户全局目录。
|
||||
|
||||
### 14.3 仍明确未实现的设计项
|
||||
|
||||
这些不是本次审计漏掉的隐藏功能,而是设计文档已标出的未决/后续项:
|
||||
|
||||
- 远端 SSH runtime(当前仅 local);
|
||||
- 预置测试模板库(当前完成的是判据/变异机制,不是 unit/interface/stress/build 模板集合);
|
||||
- U4 `steer` 语义降级:当前接受 pi 的插话语义,不伪称为内核式抢占恢复;
|
||||
- U8 的“随包锁定独立 pi 二进制”仍取决于发布包;当前 agent_up 会回显 sidecar 可解析到的 `pi_version`,无法解析时显式警告,不编造版本。
|
||||
|
||||
审计结论:**原始设计的核心机制已实现,但不能宣称所有后续项全部完成。** 本节列出的项目是剩余真实边界。
|
||||
|
||||
|
||||
|
||||
1. **不要把「测试通过」当作「改对了」**。本文 §4.5 的变异验证不是可选项,
|
||||
它是这套机制唯一能防住假绿的手段。本仓当天因此浪费过一整轮排查。
|
||||
|
||||
Reference in New Issue
Block a user