* 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>
55 KiB
CommonDev:主管式多仓库开发流水线(设计 · 未实现)
状态:架构已变更(见 §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。
标记约定(沿用本仓惯例):
- [已核] = 本文写作时已在本机实测/源码核对确认,附证据
- [待定] = 需要决策,本文给出建议但未拍板
- [风险] = 已知会让整套机制失效的点
撰写时间:2026-10-13。事实基线:TrueAgent 工作区
/home/program/TrueAgent, 生产实例/home/newqqagent(homed PID 2966649 guard / 2966664 agent)。
1. 背景:这套机制要解决什么
1.1 用户的原话(原样引用,避免转述走样)
「其实我想的是以后你就是开发主管,我以后只跟你对接,然后你有一个 commondev 插件, 包含在远程 git 仓库全套操作支持添加多个账号搜索账号仓库等一系列能力)然后有项目列表 每个项目有独立的一系列(分支,对应分支分配给的 agent,分支进行的工作与验收与否) 然后具体实现是通过临时目录(插件数据目录下)有 program/hash-name 目录,每个目录下是 独立仓库的独立分支,每次修改包含(更新,bugfix 等)都是拉取仓库,拉出分支,修改, agent 自审,你复审,你调用插件提 pr 合入(注意审核这部分是插件提供一组让你定义单元 测试的工具,你定义过单元测试后,对应 agent 完成修改后你直接让插件进行复核即可) 插件中包含压力测试,接口测试,编译测试等的多个测试模板。」
关于多账号,用户后续澄清:
「我说的多账号是指插件持有多个 git 平台的访问凭据」
关于 agent 如何进目录,用户后续澄清:
「是 homeagent 负责把仓库拉下来,开出分支,然后在目录中拉起 agent 注入提示词, 这好像需要进一步完善优化 a2a/acp 或者直接调 cli」
1.2 角色模型 [已定]
| 角色 | 是谁 | 职责 |
|---|---|---|
| 人(用户) | 老板 | 只与「开发主管」对接,不直接指挥执行 agent |
| 开发主管 | 长期驻留的 HomeAgent 主 agent | 拆任务、定义判据、复审、决定合入 |
| 执行 agent | 按任务拉起的编码 agent(pi / dsh) | 在某棵工作树里干活 |
| commondev 插件 | 新增 Go 插件 | 仓库/凭据/分支/PR/测试执行,全部机械化操作 |
1.3 为什么这件事值得做
现状是「在项目文件夹里开 agent」,导致同项目的多个 agent 无法各占分支并行。 用户要的是:一个项目可同时有多条分支在被不同 agent 推进,而合入点由主管把控。
2. 目标机制(用户描述 + 本文补全)
项目列表(每项目一条)
└─ 分支列表(每分支:名字 / 分配给哪个 agent / 工作内容 / 验收状态 / base_sha)
└─ 临时工作目录(插件数据目录下 program/<hash>-<name>/)
└─ 独立仓库的独立分支(本地 clone)
└─ ★ 拉起 pi 并拿到 session_id
└─ ★ 才有通道:插件注册 inputch commondev/<id>
+ outputch commondev_<id>
└─ 主 agent 建驻留子并划入这对通道
└─ 子调 output_send__commondev_<id> 下任务
└─ agent 自审 → 主管复审 → 插件复核 → 提 PR → 合入
顺序要点(容易搞反):先有 session,才有通道。通道是“与某个具体 pi 会话 对话的入口”,没 session 注册出来就是空壳。所以: 建工程 → 拉 pi(拿 session)→ 注册通道 → 建驻留子并划入 → 投递。 详见续篇 §2.2。
插件应提供的能力面(用户列举):
- 远程 git 仓库全套操作(clone / fetch / branch / commit / push / PR / merge)
- 多平台凭据(GitHub / GitCode / Gitea …)[已按用户澄清修正:多凭据,非多 agent 账号]
- 账号/仓库搜索
- 项目列表与分支状态台账
- 判据定义与复核(主管定义单元测试 → 插件在 agent 改完后执行)
- 测试模板:压力 / 接口 / 编译
3. 已核实的事实(接手者可直接依赖)
3.1 仓库与平台
| 事实 | 证据 |
|---|---|
TrueAgent 自身 remote 为 gitcode + origin(github) |
git remote -v |
本机有 Gitea 1.24.2 在 192.168.2.106:3000 |
curl /api/v1/version → {"version":"1.24.2"} |
| 本机无 git 库依赖(go.mod 无 go-git/git2go) | 插件现只能用 os/exec 调 git |
~/.git-credentials 存在 |
ls ~/.git-credentials |
3.2 凭据现状 [风险]
| 事实 | 证据 |
|---|---|
插件配置存于 config_<plugin> 表,key→TEXT,可存 JSON |
config_qq.group_tool_allowlists 即 JSON |
| 凭据明文存储 | config_vikunja.token = tk_2b7ecac1... |
config.db 权限 644(全局可读) |
ls -l /home/newqqagent/config.db |
[风险] commondev 的凭据是能 push 用户仓库的凭据,与现有 token 不同量级。 按现有 644 明文惯例存,等于把仓库写权限暴露给同机任何可读文件的东西 (本容器跑着数十个子进程插件、浏览器、MCP 服务)。详见 §7.1。
3.3 插件开发基建(可复用的现成东西)
| 事实 | 证据/位置 |
|---|---|
已有 plugindev 示例插件,含 plugindev_status/init/build/sdk/projects |
third_party/homeagent-sdk/example/plugindev/ |
| 插件数据目录 API | sdk.Settings().DataDir()(internal/sdk/settings_impl.go:24) |
hmapdev 工具链已装 |
/usr/local/bin/hmapdev |
SDK 无 HTTP/exec 能力面,但插件是编译型 Go,可自行 os/exec |
internal/sdk/plugin.go 方法清单 |
commondev 可从
plugindev长出来,不必从零起。
3.4 「把 agent 拉进目录」——三条路的真实能力 [已核,关键]
| 路径 | 能否指定 cwd | 结论 |
|---|---|---|
pi -p(无头) |
✅ cwd = 进程 cwd | 实测:在 proj-b 下运行,正确读出 marker.txt = BBB |
dsh headless |
✅ 逻辑同 CLI(cwd 继承) | 实测被凭据挡下(非 cwd 问题) |
a2a_query / acp_query |
❌ 参数里没有 cwd | 见下 |
a2a_query / acp_query 的确切形状(已读源码):
a2a_query参数:agent_url/query/session_id/timeout—— 无 cwdacp_query参数:server_url/prompt/session_id/timeout—— 无 cwd- pi-acp 扩展的
session/newpayload:{"request": {"text": text}}—— 无 cwd (/root/.pi/agent/extensions/pi-acp/index.ts:127)
它们只能把文本发给「已在运行的」 agent,而该 agent 的工作目录是启动时固定的。
这是用户所说「需要进一步完善优化」的准确位置。
A2A/ACP 是长期驻留 agent 的模型(peer-to-peer,一个 agent 一个进程一个目录); 而本需求是按任务拉起(一次性、每任务一个目录)。两者模型不匹配。
- 若走 A2A/ACP:必须扩建协议增加
cwd(改动扩展 + 可能改动 ACP 实现)- 若走 CLI:
pi -p天然贴合(进程 cwd 即工作目录),零协议改动本文倾向 CLI 路线 [待定],理由见 §6.1。
3.5 端口占用:A2A/ACP 不是通用能力 [已核]
127.0.0.1:14010(A2A)与 14011(ACP)由 PID 819 pi-web-sessiond 监听,
不是 general pi 的能力。普通 pi -p 启动时只打印:
[pi-a2a] A2A server not listening on 127.0.0.1:14010 (EADDRINUSE)
pi --help 里没有 --serve 之类参数——服务由 ~/.pi/agent/extensions/pi-a2a|pi-acp
两个扩展提供,且扩展内有「子进程不继承监听端口」的守卫。
3.6 拉起 agent 当前不工作 [风险 · 最高优先级]
实测三条拉起尝试全部失败:
| 尝试 | 错误 |
|---|---|
pi -p --provider llmsproxy --model AUTO |
成功(同目录测试通过),但另一轮撞 model "claude-opus-5" is not allowed for this key(403) |
dsh headless |
AUTH: Authentication Fails, Your api key: ****d63d is invalid |
且与 QQ 会话中 agent 自身遭遇的失败同源(/home/newqqagent/memory/context.json
第 23–28 条):
[23] 403: model "claude-opus-5" is not allowed for this key
[24] 503: all 6 auto providers failed
[26][27][28] 503: all 6 auto providers failed(codebuddy_kimi / vela_deepseek …)
[风险]这是整套设计的致命前提。 主管模式、worktree 隔离、PR 流水线、变异复核—— 每一环都要求「能成功拉起一个 agent 并让它在指定目录干活」,而这一步当前成功率 无法保证。在解决它之前,不应继续构建上层机制。
注意:失败集中在
llmsproxy/AUTO与justworker/claude-opus-5。而本机大量其他 agent 调用(子代理、go test、端到端验证)当日是成功的 ⇒ 可用 provider/model 组合 需要被枚举出来,而不是继续用AUTO去赌。
3.7 内核侧 spawn_child 与本题无关 [已核]
spawn_child 是内核自有的工作式轻量子(goroutine),参数只有 task 与 max_turns
(internal/agent/core/spawn.go:49),不是拉起外部编码 agent。
用户已明确:本需求是「homeagent 拉起本机的编码 agent(pi 和 dsh)」,与此无关。
3.8 值得保留的历史结论(前序 git worktree 讨论)
在 QQ 会话(message_id 1685590448)中,agent 给出的 git worktree 方案,本文复核:
| agent 的说法 | 复核结果 |
|---|---|
| worktree 隔离有效、互不污染主仓 | ✅ 实测通过 |
| 「同一分支不能被两棵 worktree 同时挂,git 会直接拒绝」 | ✅ 实测 fatal: 'feat/a' is already used by worktree at '...' |
| worktree 共用对象库、很轻 | ✅ |
「给专属 agent 配 core.agent.workdir」 |
❌ 错。见下 |
core.agent.workdir 的两处错误理解(internal/config/registry.go:863):
- 它是
Type: "string"的全局单值,不是 per-agent 配置 ⇒ 配了影响所有 agent - 它只是
cmd_run的 fallback,agent 显式传workdir时被覆盖(internal/plugins/cmd/plugin.go:149)
教训:worktree 方案本身没错,但「怎么把某个 agent 绑到某棵树」当时没有机制, 只有「在任务描述里写明」这种提示词层面的软约定。这正是 §3.4 的问题。
4. 设计:目标架构
4.1 commondev 插件的工具面(建议)
# 凭据与平台
commondev_account_add 添加一个平台凭据(kind: github|gitea|gitcode)
commondev_account_list 列出已配凭据(**不返回 secret 本身**)
commondev_account_test 验证某凭据可用(调平台 API 而非只看字符串非空)
commondev_repo_search 按关键字搜账号下/平台上的仓库
# 项目台账
commondev_project_add 登记一个项目(本地名 → 远程 URL + 平台 + 默认分支)
commondev_project_list 项目与分支总览(含每分支的 agent / 工作内容 / 验收状态 / base_sha)
commondev_branch_open 开一条分支工作线(分配 agent、写工作内容、记录 base_sha)
commondev_branch_status 单条分支的详细状态(含临时目录路径、最近提交、测试结果)
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 执行判据集合(正向)
commondev_test_mutate 执行**变异验证**(反向,见 §4.5)★ 必须有
commondev_review_submit agent 完成后提交复核(正向 + 变异一起跑)
# 提 PR
commondev_pr_open 提 PR(含 conflict 预检,见 §4.4)
commondev_pr_status 查 PR 状态
commondev_pr_merge 合入(**仅主管授权的凭据可调**)
4.2 项目/分支台账(建议 schema)
projects
id, name, remote_url, platform, default_branch, mirror_path, created_at
branches
id, project_id, name, agent_name, work_desc,
base_sha, # ★ 用户原描述中缺失,必须补(理由见下)
head_sha, status, # status: open|review|merged|abandoned
worktree_path, created_at, closed_at
criteria # 主管定义的判据
id, project_id, branch_id, name, kind, # kind: unit|interface|stress|build
script, expect, definition_hash, created_at
review_runs
id, branch_id, criteria_id, phase, # phase: forward|mutate
result, evidence, started_at, finished_at
为什么必须加 base_sha(用户原描述没有):
没有它,插件无法判断「两个 PR 改了同一文件时第二个该不该 rebase、能不能自动合」,
出问题也无法重放。建议在 commondev_pr_open 前做 conflict 预检。
⇒ 刀 3 已落地(见 §4.6)。
4.3 临时工作目录:mirror + 本地 clone(硬链接) [已定 2026-10-13]
用户拍板:每个工程对话一个独立目录 program/<hash>-<name>(name = 仓库名),
该分支修改提交合入后直接删除该目录。因此需要「删掉即彻底」,这直接排除了两个
候选方案(均实测)。
<插件数据目录>/program/
mirrors/<platform>-<repo>.git/ # git clone --mirror,每仓库一份,定期 fetch
<hash>-<name>/ # 每个工程对话一份;合入后整目录删除
<hash> 用「项目名 + 分支名」的短哈希,保证可重算(便于清理孤儿目录),
并且 hash 是目录唯一性的来源。
目录映射:★ 同一仓库的不同分支必然是不同目录 [已定 2026-10-14]
用户明确要求:
「即使是同一个仓库,只要是不同的分支都拉到 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 完全相同);删掉即彻底 |
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 ★ 复核必须是两步:正向 + 变异(本文最重要的设计主张)
用户原话:
「我定义过单元测试后,对应 agent 完成修改后你直接让插件进行复核即可」
这里的隐含假设是「插件 GREEN = 改对了」。该假设不成立。
插件只能跑主管预先写下的判据。若判据没覆盖那条 bug,插件照样打勾。 自动化不会发现假绿,只会让假绿跑得更快、更可信。
本仓当日的实证:2026-10-13 下午新写的协议一致性判据组,变异测试中
M5 未报红——把 json:"phase" 改成 json:"phaseX",判据仍全绿,
因为它只查了前缀。是变异测试把它咬出来的(该轮修复的最终判据已改为匹配完整 tag)。
因此插件的复核必须包含两个阶段:
| 阶段 | 做什么 | 不通过的后果 |
|---|---|---|
| 正向(forward) | 跑主管定义的判据 → 期望通过 | 实现有 bug,驳回 |
| 反向(mutate)★ | 自动改坏被测代码(按主管给的变异规则或自动生成)→ 期望判据报红 | 判据太松,驳回 |
没有反向阶段,就是在给「测试通过」这个信号不断充值信用,直到它骗一次大的。
变异规则由主管随判据一起定义(commondev_test_define 可迭代幂等),存入 criteria 表。
注意本仓纪律:判据类写「某标识符是否出现在函数体」时必须先剔除注释再匹配(曾因注释含
offsetParent 导致假绿)。
4.6 三层审查的分工(建议明确,避免主管被 diff 淹没)
用户描述三层:agent 自审 → 主管复审 → 插件复核。
建议定义清楚「主管复审」审什么,否则主管会变成更贵的高级码农:
| 层 | 审什么 | 不审什么 |
|---|---|---|
| agent 自审 | 代码正确性、风格、实现细节 | — |
| 主管复审 | 这次的改动,判据够不够:引入了哪些新失败方式?判据覆盖了吗?变异咬得住吗? | 逐行读 diff |
| 插件复核 | 机械执行正向 + 变异 | 语义判断 |
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 |
盘点并行推进的分支线(按项目分组),派活前确认边界 |
☆ 实测发现
冲突解析有两条独立路径,缺一不可:
CONFLICT (content): Merge conflict in <path>行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 复刻过最小场景:
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. 与现有系统的关系
| 现有物 | 关系 |
|---|---|
plugindev 示例插件 |
commondev 的起点(可复用其 hmapdev 调用、workspace 管理) |
plugindev 的 projects 工具 |
可参考其「列工程 + 构建产物」的形态 |
core.agent.workdir |
全局单值 fallback,不要把它当 per-agent 机制用(§3.8) |
spawn_child |
内核工作式轻量子,与本需求无关(§3.7) |
a2a / acp 插件 |
现无 cwd,需扩建或绕过(§3.4) |
| Gitea 1.24.2(本机) | 建议作为第一刀的唯一平台(见 §6.2) |
tracker(changeset/回滚) |
已有的文件系统变更追踪,与 git 分支台账无关,勿混 |
6. 关键决策建议(未拍板,供接手者与用户确认)
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 --mode rpc(常驻) |
同 cwd;同会话续投;steer/follow_up 可中途纠偏;get_state 可断言 session 未变 |
要实现/复用 RPC 客户端;stdout 必须持续读 |
| Node SDK(进程内) | 最贴身;createAgentSession({cwd}) 可控 cwd 与会话 |
需 Node 宿主 |
dsh headless |
同上 | 本机凭据失效,需先修 |
| 扩建 A2A | 复用现有插件 | 要改扩展 + payload;参数里没有 cwd(§3.4) |
| 扩建 ACP | 同上 | 同上;且 ACP 的 session 模型本就假设长驻,加 cwd 是违背其设计意图 |
修正后的建议:内嵌 sidecar 为默认形态,远程 SSH 由配置切换
(两形态共用同一 sidecar,差异只在传输层)。pi -p 仅作侧车不可用时的降级,
不是正式形态。A2A/ACP 依然排除(「无 cwd」这条事实没变),
排除理由与本需求无关:它们是常驻 peer 的模型,而本需求是按工程对话拉起。
选型已定 [2026-10-13 用户拍板]:默认内嵌(进程内 pi 实例), 配置可切到远程 SSH;远端同样创建目录并在该目录拉起 pi 实例。 远端环境缺失时询问用户、不得擅自安装(见续篇 §3.4)。
6.2 第一刀只做一个平台、一个项目、一个任务
理由:这套机制有至少四个独立的失败面(凭据 / 拉起 / 判据 / 合入), 一次全上会无法归因。先用 Gitea(本机就有,API 可控)跑通一条真实链路:
登记项目 → mirror → 开分支 → 拉起 pi -p(cwd=工作目录)
→ agent 改 → 主管定义判据 → 插件跑「正向 + 变异」
→ 提 PR → 主管合入
这一刀的目的不是功能完整,是证明「假绿检测」这一环真的能工作。
6.3 权限模型:让「必须有主管复审」成为机制而非礼貌 [✅ 已完成 · 真实 Gitea 实测]
若执行 agent 所用的凭据能 push 主干,则「主管复审」只是流程约定——想绕过随时能绕过。
机制(已在真实 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 落地顺序
| 刀次 | 内容 | 证明什么 |
|---|---|---|
| 0 | 修「能拉起 agent」(§3.6) | 前提是否成立 |
| 1 | 单平台单项目:mirror → 分支 → 拉起 → 判据+变异 → PR | 机制成立,假绿可检测 |
| 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 刀为什么放最后:它最容易写,也最容易变成摆设。先让「变异能咬住」 这一环成立,模板才有意义。
7. 安全设计(必读)
7.1 凭据存放 [风险]
现状(§3.2):config_<plugin> 明文、config.db 644。
commondev 的凭据是能 push 用户所有仓库的凭据,性质完全不同。
建议:
- 不放
config.db。放独立文件(如<插件数据目录>/credentials.json),权限0600, 启动时校验权限,不对就拒绝启动(本仓纪律:响亮失败,不静默降级) - 优先 SSH key 而非 PAT:key 可按平台单独生成/吊销,不随账号密码轮换失效
- 每平台用独立、最小授权的凭据(GitHub fine-grained PAT 支持单仓授权), 不要一个 token 通吃
- 接
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 换环境变量即可 |
| 轮换不需改数据 | 改环境变量 + 重启即可 |
两条不可逆的纪律:
- 变量解析不到值 ⇒ 响亮报错,绝不静默当空串。
后者会退化成匿名访问,表现成 401 而非“凭据没配” —— 极难排查。
实测:
${NONEXISTENT_VAR_ZZZ}→ 报“凭据引用的环境变量未设置: …”。 - 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 是常见做法,
git remote -v 就能读出来)。
规矩:clone 出来的 remote URL 不带凭据;push 时临时注入
(Gitea/GitHub 均支持 http.extraheader 或 GIT_ASKPASS 一次性注入)。
这样临时目录删掉就是真删掉。
建议 commondev_workdir_clean 内置一道残留检查(扫 remote URL / config / 已知凭据指纹)。
7.3 插件自身不是沙箱
本仓已知:插件是编译型 Go 子进程,能做 os/exec、读写文件。commondev 持有高价值凭据,
它自己的权限边界需要单独评估(用户在主机的进程隔离、文件权限是否足够)。
本文不展开,但接手者应至少确认:commondev 的数据目录权限、以及宿主对插件进程的隔离手段。
8. 未决问题(需用户拍板)
8.1 「独立仓库」还是「worktree」?[已解决 2026-10-13]
用户拍板:独立仓库(本地 clone)。 目录 program/<hash>-<name>(name = 仓库名),
合入后直接删除目录。
理由:worktree 的 rm -rf 不彻底(.git/worktrees/ 残留,需 prune),
而 --reference 有外部依赖(mirror 被清理即损坏)——两者都不满足「删掉即彻底」。
实测与取舍见 §4.3。
代价与补偿:失去 worktree 的「同分支不可双挂」保护 ⇒ 台账层补唯一性闸(§4.3)。
8.2 主管(主 agent)自己的模型可用性
主管也要调 LLM(拆任务、定义判据、复审)。若主管所用模型正是 §3.6 里失效的那些, 整套机制同样停摆。需要为「主管」单独确认一个稳定可用的 provider/model。
8.3 判据由谁写、写在哪
用户说「你(主管)定义单元测试」。但判据的落地形态未定:
- 写进被测仓库(成为仓库的一部分,随 PR 一起走)?
- 还是留在 commondev 台账里(
criteria表)?
两种各有取舍:前者可被仓库自身复用,后者与实现解耦但要维护两处。 建议:留在台账,但把生成的脚本落盘到工作目录内执行,并在 PR 描述里附判据摘要。
8.4 冲突与依赖
若并行任务之间有依赖(A 改接口、B 用接口),worktree/独立 clone 会放大问题: 两棵树互不可见,A 的改动 B 编译不过。
用户需要明确:并行的是「互不相干的模块」还是「一个功能的几个部分」?
- 前者 → 本设计正合适
- 后者 → 会集中在 merge 时爆发,建议先拆分边界(谁拥有哪些文件)再谈并行
8.5 责任边界
用户说「以后只跟主管对接」。那么当判据打勾、PR 合了、线上仍出问题时,责任如何划分? 这决定判据里必须包含什么:若主管负责,则光有单元测试不够, 需要纳入可观测性/回归判据(真跑一遍看真事件流,而非只看测试绿)。
9. 接手者请从这里开始
第一步(必做):验证前提是否成立
写一个最小探针,回答一个问题:「能否稳定拉起一个 agent 并让它读到指定目录?」
要求:
- 在临时目录放 marker 文件,拉起
pi -p,要求输出 marker 内容 - 跑 20 次,统计成功率(不要只跑一次成功就下结论)
- 记录每次使用的 provider/model,据此枚举可用组合
- 若成功率低,先解决 §3.6,不要继续上层设计
第二步:只做 §6.2 的第一刀
用 Gitea。链路:mirror → 分支 → 拉起 → 判据 + 变异 → PR。 重点验证「变异能咬住判据」——故意写一个太松的判据,确认变异阶段能把它驳回。
第三步:把本篇文档的每一条事实重新核对
本文的 [已核] 项都有命令可复现,见 §10。若发现与本文不符,先更新本文再动手。
10. 自检清单(逐条可复现)
接手者应能执行以下命令并得到与本文一致的结论:
# §3.1 平台
git -C /home/program/TrueAgent remote -v
curl -s http://192.168.2.106:3000/api/v1/version # 期望 {"version":"1.24.2"}
# §3.2 凭据现状(注意:不要打印完整 token)
ls -l /home/newqqagent/config.db # 期望 644
sqlite3 /home/newqqagent/config.db "select key from config_vikunja;" # 含 token
# §3.4 拉起 agent 的 cwd 行为
mkdir -p /tmp/probe-a && echo AAA > /tmp/probe-a/marker.txt
cd /tmp/probe-a && pi -p --provider <可用provider> --model <可用model> \
"执行 pwd 和 cat marker.txt,只输出这两行原始内容"
# 期望输出 /tmp/probe-a 与 AAA
# §3.4 a2a/acp 无 cwd
grep -n "agent_url\|query\|session_id" \
/home/program/TrueAgent/third_party/homeagent-sdk/example/a2a/plugin.go
grep -n "cwd" /root/.pi/agent/extensions/pi-acp/index.ts # 期望无结果
# §3.5 端口归属
ss -tlnp | grep -E "14010|14011" # 期望 pid=819 pi-web-sessiond
# §3.8 core.agent.workdir 是全局单值
grep -n "core.agent.workdir" /home/program/TrueAgent/internal/config/registry.go
# 期望 Type: "string"
# §3.7 spawn_child 参数
grep -n "max_turns\|task" /home/program/TrueAgent/internal/agent/core/spawn.go | head
# §4.5 变异能咬住判据的现成范例
cd /home/program/TrueAgent
go test -count=1 -run TestStageFrameFieldsMatchServer ./internal/plugins/webui/
grep -n "phase,omitempty" cmd/waiter/state.go # 判据匹配的是完整 tag
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 为什么平台部分不该在插件里
- 每接一个托管平台都要改 Go 代码 → 重编译 → 重打包 → 重启服务。 而平台知识(哪个端点、什么认证头)本该和文档一起演进。
- 平台形状被硬编码:
platformSpec的 switch 每加一家就长一截, 未列出的平台直接报错——实测 GitLab / Gitee / Bitbucket / Azure DevOps 全不支持。 而 agent 用curl本来什么平台都能打。 - 工具面膨胀: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 猜默认值 | ❌ 报红 |
复现命令:
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,无法解析时显式警告,不编造版本。
审计结论:原始设计的核心机制已实现,但不能宣称所有后续项全部完成。 本节列出的项目是剩余真实边界。
-
不要把「测试通过」当作「改对了」。本文 §4.5 的变异验证不是可选项, 它是这套机制唯一能防住假绿的手段。本仓当天因此浪费过一整轮排查。
-
不要用提示词层面的软约定替代机制。前序 worktree 讨论里 「在任务描述里写明工作目录」就是这一类——它靠模型自觉,没有任何保证。§3.8 已记录该教训。
-
前提不成立时,不要继续设计上层。§3.6 的「拉起 agent 不工作」如果没解决, 后续所有设计都是纸面的。先让它能跑,再让它跑得好。