Files
HomeAgent/docs/zh/commondev-design.md
JianFeeeee a68d6a428c 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>
2026-10-17 19:18:24 +08:00

55 KiB
Raw Blame History

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。

插件应提供的能力面(用户列举):

  1. 远程 git 仓库全套操作(clone / fetch / branch / commit / push / PR / merge)
  2. 多平台凭据(GitHub / GitCode / Gitea …)[已按用户澄清修正:多凭据,非多 agent 账号]
  3. 账号/仓库搜索
  4. 项目列表与分支状态台账
  5. 判据定义与复核(主管定义单元测试 → 插件在 agent 改完后执行)
  6. 测试模板:压力 / 接口 / 编译

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 —— 无 cwd
  • acp_query 参数:server_url / prompt / session_id / timeout —— 无 cwd
  • pi-acp 扩展的 session/new payload:{"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):

  1. 它是 Type: "string" 的全局单值,不是 per-agent 配置 ⇒ 配了影响所有 agent
  2. 它只是 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 盘点并行推进的分支线(按项目分组),派活前确认边界

☆ 实测发现

冲突解析有两条独立路径,缺一不可:

  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 复刻过最小场景:

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 用户所有仓库的凭据,性质完全不同。

建议:

  1. 不放 config.db。放独立文件(如 <插件数据目录>/credentials.json),权限 0600, 启动时校验权限,不对就拒绝启动(本仓纪律:响亮失败,不静默降级)
  2. 优先 SSH key 而非 PAT:key 可按平台单独生成/吊销,不随账号密码轮换失效
  3. 每平台用独立、最小授权的凭据(GitHub fine-grained PAT 支持单仓授权), 不要一个 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 是常见做法, 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 为什么平台部分不该在插件里

  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 猜默认值 ❌ 报红

复现命令:

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 的变异验证不是可选项, 它是这套机制唯一能防住假绿的手段。本仓当天因此浪费过一整轮排查。

  2. 不要用提示词层面的软约定替代机制。前序 worktree 讨论里 「在任务描述里写明工作目录」就是这一类——它靠模型自觉,没有任何保证。§3.8 已记录该教训。

  3. 前提不成立时,不要继续设计上层。§3.6 的「拉起 agent 不工作」如果没解决, 后续所有设计都是纸面的。先让它能跑,再让它跑得好。