Files
homeagent-sdk/example/commondev/skill/commondev-discipline/SKILL.md
JianFeeeee 81a3efa4df feat(commondev): 主管式多仓库开发流水线 + sidecar 后端抽象(pi/zcode) (#3)
* feat(commondev): 主管式多仓库开发流水线 + sidecar 后端抽象(pi/zcode)

## 是什么

agent 统筹开发流程的插件:登记项目 → 开工程线(每条线一个隔离工作目录 +
一个 sidecar 子进程 + 一对通道)→ 投递任务 → 同会话续投 → 评审/合入。

38 个工具:project_*/branch_*/workdir_*/job_*/review_*/test_*/
endpoint_*/agent_*/pi_*/conflict_check。

## sidecar 后端抽象(本轮核心)

传输层(local/ssh)与后端(pi/zcode/自定义)**正交**,四个自由组合:

  driver.json 描述一个后端:script / cliPath / env / args / llmConfig
  三级查找:插件数据目录 → **随包 rawfile** → 内置 pi 描述
  占位符:{workdir} {dataDir} {pluginDir} {rawfile} {sessionId}

★ 内置 pi 回退**与默认后端解耦**(2026-10-17 修):原实现写
`if cli == defaultAgentCLI { return builtinPiDriver() }`,把"给谁兜底"
和"默认选谁"耦合到同一常量 —— 一旦有人改默认,pi 当场失去兜底,
显式 cli="pi" 开始报「驱动不存在」。现在判断字面 "pi"。

默认后端保持 **pi**(我们本地主用);交付方用 zcode 走两种局部开关:
 · 单线:commondev_branch_open(cli="zcode")
 · 按机器:commondev_endpoint_set(host=..., cli="zcode")

## zcode 接入(三条真机判据,全部要求真改工程文件)

 · TestZcodeFullPipeline(29-34s):随包 driver + 真实 ZCode CLI
 · TestZcodeOverSSH(39s):真 ssh + scp,远端 node 跑 zcode
 · TestZcodeOverSSHWithPerHostEndpoints(68s):两个 host 别名配不同
   cli_path,走端点表而不是全局 settings

★ 会话库必须隔离到工作区(sidecar.mjs 的 ensureWorkspaceSessionStore):
实测 ZCode 的会话库路径**硬编码 homedir()**,`ZCODE_DATA_BASE_DIR` 对它
完全无效(26 个会话全落全局库)。但 `<workdir>/.zcode/config.json` 的
`storage.sessionDbPath` **确实生效**。所以启动前写一份工作区配置,
一举解决:多线不共用历史 / 不污染全局 ~/.zcode / 删线不留孤儿会话。
只在不存在时写 —— 用户手工放的要尊重。

## 其他

 · rawfile:项目根 rawfile/ 默认全收,解到 <插件目录>/rawfile/,绝对路径拒绝
 · 凭证:命名引用 + ${ENV_VAR},独立 0600 文件,绝不进 config.db(0644)
 · secretshell:给不受信 agent 用的安全 shell(ackey + safeshell)
 · 冲突检查 / 评审判据 / 镜像 / 每条线独立 workdir(hash 命名,合并后删除)

判据 30+ 条,含 4 条「判据本身的判据」(防止判据假绿)。

* fix(sdk): 协议通道纪律 + hmapdev 模板升级 + 示例插件 stdout 污染

## 核心缺陷:插件往 stdout 打日志 = 污染 JSON-RPC 协议通道

子进程插件与内核用 stdout 传 NDJSON 帧,stderr 才是日志通道。
脚手架模板(及 4 个示例插件)用 `fmt.Printf` 打日志 ⇒ 内核收到非 JSON 文本:

    [proc] weather 收到非法 JSON 帧(21 字节): invalid character 'i'

**生产实测 1038 条**(huawei_smarthome 720 / weather 113 / rss 103 / calendar 102)。
后果不只是日志噪音:那一行日志被**丢弃**,插件看起来"没启动"。

修法:
- hmapdev 模板 `fmt.Printf` → `log.Printf`(9 处)
- 示例插件 4 个 14 处:ai_image(2) calendar(5) rss(4) weather(3)
  + 补 "log" import
- 内核侧(主仓)同步让报错可定位:内容摘要 + 可能原因 + 修法

判别法:`package main` = 子进程(协议通道,禁用 stdout 打日志);
普通库包 = 进程内插件(编译进内核,写真实终端,**不是**缺陷)——
`internal/plugins/cli` 的 `fmt.Println` 属后者。

## hmapdev

- `cmd_build.go` +136:平台产物精确匹配(`--target` 是**append** 语义,
  原脚本 `ls dist/*.hmap | head -1` 会按字典序抓到 amd64,
  **导致 arm64 包里装的是 x86-64 二进制**,且退出码 0、校验和通过)
- `cmd_init.go` +46:脚手架 stdio 纪律
- `templates/proc_main.go.tmpl`:补齐 mgrStop/mgrStart/mgrIsRuntimeStopped
  三态能力(插件管理器运行态停启)
- `templates/plg.json.tmpl` 删除(合并进 cmd_init)

## 判据

- `scaffold_stdout_test.go`(3 条):扫模板生成的代码不得写 stdout。
  ★ 判据自身先假红过一次:**模板的注释里就写了 `fmt.Printf`** ⇒
  必须 `stripTemplateComments()` 之后才判
- `example_stdout_test.go`(2 条):扫全部 20 个子进程示例插件,
  报错给**文件:行号**;变异咬住并给出准确行号
- `package_examples_test.go`(2 条):**执行真实打包脚本**
  (第一版测的是 hmapdev 而不是脚本 ⇒ 变异不咬)
- `cmd_build` / `cmd_init` 新判据

## 其他

- sdk/settings.go +15(三态能力透传)
- `docs/versions.md` +§示例插件协议通道纪律(含生产 1038 条实测表格)
- `skills/homeagent-plugin-dev/SKILL.md` +§协议通道纪律(正误代码对比)

* fix(examples): 恢复 go.mod 相对路径 + 移除 plg.json 的 sdk 固定(修 CI 红)

CI 上 `Examples (linux/amd64)` 与 `(darwin/arm64)` 双红,只有两个失败:

  ✗ commondev   构建失败
  ✗ editdoc     构建失败
  error: 项目声明 sdk=1.4.0,但本地 SDK 存储里没有这个版本

其余 20 个示例全部成功 —— 说明不是通用问题,而是这两个被改坏了。

## 根因(两个叠加)

**① go.mod 被本机绝对路径污染**

我在上一个提交里用 `git add -A`,把本机 `go build` **自动改写**过的
go.mod 一并提交了:

  require github.com/JianFeeeee/homeagentsdk v1.4.0
  replace github.com/JianFeeeee/homeagentsdk => /root/.homeagent/hmapdev/sdk/v1.4.0

基线(origin/main)是:

  require github.com/JianFeeeee/homeagentsdk v0.0.0
  replace github.com/JianFeeeee/homeagentsdk => ../../

本机那个目录真的存在 ⇒ 本地全绿;CI 没有 ⇒ 必红。

**② plg.json 声明了 `"sdk": "1.4.0"`**

一声明版本,hmapdev 就去**本地 SDK store** 找它,找不到直接拒绝构建。
全仓 20+ 示例**都不声明**,只有我改过的这两个声明了。
示例的正确依赖表达是 go.mod 的 `v0.0.0` + `replace ../../`。

## 修法

两个文件都恢复到与其余示例一致的写法。

## 判据(3 条,都是新加的)

 · `TestExampleGoModHasNoAbsoluteReplace`:任何示例 go.mod 不得含
   `/root/`、`/home/`、`/Users/`、`C:\`
 · `TestExampleGoModUsesRelativeSDKReplace`:replace 必须是相对路径 `../../`
 · `TestExamplePlgJsonDoesNotPinSDKVersion`:plg.json 不得声明 sdk

变异 2/2 咬住(重新引入绝对路径 / 重新声明 sdk,都准确报出文件路径)。

## 本地验证(模拟 CI 干净环境)

用干净 HOME 跑 `package/build-examples.sh`:
 · 修复前:成功 18 / 失败 4(commondev, editdoc, browser, rss)
 · 修复后:成功 20 / 失败 2(browser, rss —— **本机代理不通**,
   `proxy.golang.org` 超时;用本地 modcache 验证这两个可正常构建)

即 22/22 源码都能构建,CI 有网络不受影响。

## 教训

`git add -A` 会把「构建工具自动改写的文件」一起提交。
提交前必须 `git status` 逐个看,尤其 go.mod / go.sum。

---------

Co-authored-by: JianFeeeee <JianFeeeee@users.noreply.gitcode.com>
2026-10-17 19:44:15 +08:00

45 KiB
Raw Blame History

name, description, version, author
name description version author
commondev-discipline commondev 主管式多仓库开发流水线的使用与开发纪律。当你要为某个工程开一条分支、把工程派给一个下属 agent、或用 pi 拉起执行 agent 干活时必读。核心动作:为每个工程对话新建驻留式子 agent 并把该工程的一对通道划给它(否则所有工程都挤在根 agent 上)。**git 托管平台的操作(Gitea/GitHub/GitLab)不在插件里,而在本文** —— 用 enc_shell + 保存的凭据调 curl/CLI 完成。也含 hmap 插件开发与部署纪律。 2.0.0 HomeAgent

commondev 纪律(主管模式 · 工程对话 · 平台操作)

本技能三部分,都可能与你的任务直接相关:

  • 第一部分:使用者纪律 —— 怎么开工程、怎么派活。
  • 第二部分:★ git 平台操作 —— 本文是权威。开 PR、审批、分支保护怎么做。
  • 第三部分:开发与部署纪律 —— 怎么不踩坑。

★ 架构边界(用户拍板 2026-10-15)

层 负责
插件 commondev 项目台账、工作目录、pi 会话与通道、判据与变异复核、冲突预检、凭据保存与安全执行
本文(skill) 各 git 托管平台的认证头、API 端点、PR 与分支保护流程

插件里没有平台逻辑。 操作 Gitea/GitHub/GitLab 等平台一律: 读本文 → 用 enc_shell + @{凭据名} 执行 curl/gh/tea。

⇒ 新增一个托管平台只需改本文,不需要改插件、不需要重编译、不需要重启服务。

原本这些在插件里(platformSpec / commondev_account_* / commondev_pr_* / commondev_protect_branch / commondev_repo_search),代价是: 每接一个平台都要改 Go 代码 → 重编译 → 重打包 → 重启, 且未列出的平台直接报错(GitLab/Gitee/Bitbucket 全不支持)—— 而 agent 用 curl 本来什么平台都能打。这 10 个工具已移除。

看不懂某个词时的权威文档(按顺序读):

  1. docs/zh/commondev-design.md —— 目标机制、台账 schema、判据与变异、安全(§12 是架构变更)。
  2. docs/zh/commondev-agent-runtime-design.md —— 运行时与内核接入(续篇)。
  3. docs/zh/kernel-secret-firewall-design.md —— 凭据模块(enc_* 的底座)。
  4. docs/zh/resident-subagent-design.md —— 驻留子设计的原始文档。

本文与文档冲突时,以文档为准,并回来修正本文。


Setup: 第二部分 git 平台操作(本文是权威,不要凭记忆写 API)

11. 所有平台都是同一个形状

「带认证头发一个 REST 请求」。用 enc_shell + curl 即可:

enc_shell(command="curl -s -H \"<认证头>\" <API前缀><路径>")

差异只有两个维度:API 前缀 与 认证头。 其余是 REST 常规。

12. 认证头与 API 前缀(逐平台,★ 四种形状互不相同)

平台 API 前缀 认证头
Gitea <base>/api/v1 Authorization: token <T> ← 不是 Bearer
GitHub https://api.github.com Authorization: Bearer <T>
GitLab <base>/api/v4 PRIVATE-TOKEN: <T> ← 不是 Authorization
GitCode https://api.gitcode.com/api/v5 Authorization: Bearer <T>
Gitee https://gitee.com/api/v5 access_token=<T>(查询参数,不是头)

⚠ 头名、前缀、甚至「用头还是用查询参数」都不同。未知平台先查官方文档,不要猜形状。

13. 先保存凭据,再用它

enc_store(alias="gitea-token", value="${GITEA_TOKEN}")     # 值优先走 ${ENV} 或 @file:
enc_shell(command="curl -s -H \"Authorization: token @{gitea-token}\" http://192.168.2.106:3000/api/v1/user")

⚠ 优先用 enc_store,不要用 commondev_account_add —— 后者已不存在 (它内建了平台语义:platform/role/base_url,那正是被移出的东西)。

先验凭据再干活(早失败,别推了一半才发现 token 错):

enc_shell(command="curl -s -o /dev/null -w '%{http_code}' -H \"Authorization: token @{gitea-token}\" http://192.168.2.106:3000/api/v1/user")
# 期望 200;401/403 = 凭据无效或权限不足

14. 常见操作(以 Gitea 为例,其他平台换前缀与头)

# 搜仓库
curl -s -H "Authorization: token @{t}" "http://host/api/v1/repos/search?q=myname&limit=20"

# 看仓库 / 分支
curl -s -H "Authorization: token @{t}" "http://host/api/v1/repos/jianf/MyProj"
curl -s -H "Authorization: token @{t}" "http://host/api/v1/repos/jianf/MyProj/branches"

# 开 PR(head=你的分支,base=目标分支)
curl -s -X POST -H "Authorization: token @{t}" -H 'Content-Type: application/json'   -d '{"title":"...","head":"feat/x","base":"main"}'   "http://host/api/v1/repos/jianf/MyProj/pulls"

# 查 PR 状态
curl -s -H "Authorization: token @{t}" "http://host/api/v1/repos/jianf/MyProj/pulls/7"

# 合 PR
curl -s -X POST -H "Authorization: token @{t}" -H 'Content-Type: application/json'   -d '{"Do":"merge"}' "http://host/api/v1/repos/jianf/MyProj/pulls/7/merge"

push 时凭据不进 remote URL:

# ✅ 一次性注入(仅本次命令);clone 出来的 remote 保持干净
enc_shell(command="cd <repo> && git -c http.extraheader=\"Authorization: token @{t}\" push origin feat/x")

⚠ 若改用 https://token@host/... 形式,token 会持久化进 .git/config —— 删掉临时目录也删不掉凭据(git remote -v 就能读出来)。设计 §7.2 明令禁止。

15. Gitea 分支保护:让「必须复审」成为机制而非礼貌

光有流程约定不算机制 —— 执行 agent 可以直接推主干。必须让平台拦。

curl -s -X POST -H "Authorization: token @{t}" -H 'Content-Type: application/json'   -d '{"enable_push":false,"required_approvals":1,"block_admin_merge_override":true,"dismiss_stale_approvals":true}'   "http://host/api/v1/repos/jianf/MyProj/branch_protections?name=main"

四个开关的意义(缺一个机制就有洞):

开关 作用 缺了会怎样
enable_push: false 禁止直接推送 执行 agent 直推主干,绕过评审
required_approvals: ≥1 至少一个审批 自己就能合
block_admin_merge_override: true admin 也不能绕过 用 admin token 的人绕过一切
dismiss_stale_approvals: true 新提交作废旧审批 先批准再改代码,批准白拿

真机实测(Gitea 1.24.2):

动作 普通用户 token admin token
push main ❌ pre-receive hook declined ⚠ 可能成功(见下)
push feat/* ✅ ✅
自合 PR ❌ Does not have enough approvals ⚠ 取决于开关
审批 → 合 PR — ✅

⇒ 推论:执行侧必须用普通用户 token。 用 admin token 会静默绕过分支保护 (看起来正常,实际机制失效且无报错)—— 这是最容易搞错的一点。

⚠ 分支保护已存在时用 PATCH(POST 会 403 already exist)。

16. 审批:三个必踩的 API 坑(实测)

  1. event 必须是 APPROVED,不是 APPROVE。 传 APPROVE 时请求返回 200 且不报错,但 review 状态落成 PENDING —— 于是 required_approvals 永远不满足,合并时报 Does not have enough approvals。 表象(权限不足)与真因(枚举值少个 D)毫无关联,极难自查。
  2. 必须带 commit_id —— 不带时同样落成 PENDING(与 event 是两个独立条件)。
  3. required_approvals 也约束 admin —— 别指望用 admin 跳过审批。
# ① 先取 head sha(审批必须绑定**具体提交**,不是"移动的目标")
enc_shell(command="curl -s -H \"Authorization: token @{t}\" http://host/api/v1/repos/jianf/MyProj/pulls/7 | grep -o '\"sha\":\"[a-f0-9]*\"' | head -1")
# ② 审批(event 必须 APPROVED,且带 commit_id)
enc_shell(command="curl -s -X POST -H \"Authorization: token @{t}\" -H 'Content-Type: application/json' -d '{\"body\":\"通过\",\"event\":\"APPROVED\",\"commit_id\":\"<sha>\"}' http://host/api/v1/repos/jianf/MyProj/pulls/7/reviews")

失败时保留平台原话:Does not have enough approvals 是区分「机制生效」 与「配置错了」的唯一线索,别把它翻译掉或吞掉。


Usage: 第一部分 使用者纪律

★ 设计总纲 [已定]:插件对 pi 做的事,本质就是 「把给 pi 发消息包装一下、把 pi 返回包装一下」,深度融合内核的 IO 抽象与中断系统 —— 即 pi = 一个 Device。不要自己造调度器 / 会话路由 / 中断栈, 全部接到内核已有的那套上(InjectInputTo / RegisterOutputChannel / InjectInterrupt*Opts)。

21. 角色与单位

角色 是谁 干什么
人 老板 只与主管对接
主管 你(长期驻留的 HomeAgent 主 agent) 拆任务、定义判据、复审、决定合入
执行 agent 按工程拉起的一个 pi / dsh 实例 在某棵工作树里干活
commondev 插件 外部独立插件 仓库/凭据/分支/PR/测试,全部机械化操作

为何能用内核机制(而不是自己造):

内核概念 pi 侧对应 落点(内核已有)
给 pi 发消息 pi.prompt / steer / follow_up InjectInputTo(source, "commondev/<id>", …)
pi 返回 sidecar 回调 / tool_execution_* / agent_settled InjectTextTo(source, "commondev/<id>", …)(按 inputch Owner 路由)
中断 pi steer(插到当前轮之后) InjectInterruptTextOpts(..., Priority: L3)
音声/图像产物 pi 可产出的媒体 OutputCapabilities() 声明

⇒ 插件只做两件额外事:① 把 pi 接成 Device;② 台账记 session_id 并断言未变。 调度/中断/路由/记忆策略全是内核已经在做的事。

22. 「工程对话」是一等对象

它是四元组,缺一不可:

工程对话 (id = br-<project>-<slug>)
├─ workdir      <插件数据目录>/program/<hash>-<name>
│                ★ hash = H(项目+分支) 是**唯一性来源**
│                  ⇒ **同一仓库的不同分支必然是不同目录**(否则两 agent 共用工作树、改动互相污染)
│                ★ name = 分支名派生,**仅做可读性**,不承担唯一性
│                  (派生后可能撞:`feat/a` 与 `feat.a` 都成 `feat_a`,所以必须带 hash)
│                独立 clone(非 worktree、非 --reference);合入后**整目录删除**
├─ session_id   pi 会话(★ 续投的唯一依据)
├─ runtime      local(默认,内嵌) | ssh(远端同样建目录拉 pi)
├─ host         runtime=ssh 时的目标主机(**branch_open 时给**,见 §24.1)
├─ agent_cli    执行后端(默认 pi;其他 CLI 需自备 sidecar,见 §24.2)
└─ channels     插件为该工程注册的一对通道
                ├─ inputch  commondev/<id>
                └─ outputch commondev_<id>

一个仓库可并行开发:每分支一份独立 clone,互不污染、可同时推进。 但并行有前提——必须是互不相干的模块;有依赖的任务请先拆边界。

为何不能用 worktree / --reference(实测):worktree 删后有 .git/worktrees/ 残留(需 prune);--reference 有 alternates 外部依赖, mirror 被清即损坏。本地 clone 对象为硬链接(实测 inode 相同)⇒ 省磁盘、 无外部依赖、刪掉即彻底。代价:失去 worktree「同分支不可双挂」的保护 ⇒ 台账层必须补唯一性闸(同名未结分支响亮拒绝)。

23. ★ 必做:为每个工程对话新建驻留子,并把通道划给它

★ 顺序很关键:通道是拉起 pi 拿到 session 之后才注册的,所以你的动作是:

建工程 ──► commondev_agent_up(拉起 pi,拿到 session_id)
            └─► 插件**此刻才**注册通道 inputch commondev/<id> + outputch commondev_<id>
                  └─► ★ 你建驻留子并把这对通道划给它

为何必须先有 session:通道是“与某个具体 pi 会话对话的入口”,没 session 就是空壳。 顺序错了(先建子)会拿到一个指向不存在通道的 output_send__…。

插件只能把通道登记出来;「划入(assign)」只有内核能做。 所以这一步必须由你(agent) 自己调内核工具完成,插件无法代劳。

resident_agents create
    id              = dev-<br-id>
    input_chs       = commondev/<br-id>       ← 划入该 inputch(可用斜杠)
    allowed_outputs = commondev_<br-id>       ← 授权该 outputch(★ 下划线,见下)
    capacity        = 64
    task_prompt     = "你在 <workdir> 推进 <work>。
                       下任务给自己用 output_send__commondev_<br-id>(payload=…),
                       pi 的结果会回投到你的 commondev/<br-id>;
                       有结论用 notify_parent 汇报。"

★ 下任务不需要新工具:给 pi 发消息就是子调内核原生的输出门 output_send__commondev_<br-id>(payload="…") ⇒ 打到插件的通道 handler ⇒ 插件 查台账拿到 session_id 后 sidecar.prompt()。插件不提供 agent_send 之类的工具。

⚠️ 通道命名(很容易踩):

能用 / 吗 为什么
inputch ✅ 内核自己就用 child/<id>
outputch ❌ 绝对不行 它会拼成 output_send__<name>,上游要求函数名 ^[a-zA-Z0-9_-]{1,64}$;违反的后果不是“这个工具不可用”,而是整条请求 400、agent 整体失语

⇒ 所以 inputch 叫 commondev/<br-id>、outputch 叫 commondev_<br-id>。 <br-id> 本身也只能用 [A-Za-z0-9_-]。

为什么必须这么做(不是可选的优化):

  1. inputch 是最基本的输入路由单位,路由按 Owner 在进内核之前完成。 划给子之后,该工程的输入只流向那个子,根 agent 看不到它 —— 这正是 多工程并行能做到"互不串台"的原因。
  2. 不划会的代价(如果你偷懒):所有工程对话的输入都涌向根 agent, 根成了瓶颈、上下文被 N 条工程的细节淹没,多工程并行等于没做。 现场教训(resident-subagent-design.md):子挂着 inputch=[timer] 而 timer 的输入打在父身上,子的轮次永远是 0 —— 登记了不等于划了。

落地判据(怎么知道做对了):

  • 投一条输入到 commondev/<br-id>,应只被 dev-<br-id> 处理;
  • 根 agent 的轮次不因这条输入增加;
  • input_channels 工具(内核自带,单工具多视图)用 view=by_agent 能看到划分总览。

24. 形态选择、自包含运行时与远端环境

默认本地内嵌(进程内 pi 实例);通过配置 runtime=ssh 切到远程, 远端同样创建目录并在该目录拉起 pi。

★ 自包含运行时:插件自带独立的 node 与独立的 pi,都装在插件工作目录, pi 的插件也装到这个目录 —— 不靠宿主机的 node/pi,也不污染用户的 ~/.pi。 独立 node/pi 随包分发(不首次下载、不依赖用户装)。

<插件数据目录>/runtime/
  node/  pi/
  agentdir/          ← 每个内层 pi 必须传 PI_CODING_AGENT_DIR 指这里
    settings.json    ← pi 自己的配置(偏好 / 默认 model / 全局包声明)
    models.json      ← pi 自己的配置(★ LLM 源写这里)
    auth.json        ← pi 自己的配置(key / OAuth,0600)
    mcp.json         ← pi 自己的配置(MCP 服务器)
    skills/ prompts/ themes/ extensions/

24.1 远端(ssh)怎么开

两个轴正交,开线时一起给:

commondev_branch_open(project="p", branch="feat/x",
                      runtime="ssh", ssh_host="build@10.0.0.9")
  • runtime=ssh 而没给 ssh_host → 直接拒绝(不会拖到拉起时才报)
  • 未知 runtime(比如 k8s)→ 响亮拒绝,不静默降级为 local (静默降级会让"我以为跑在远端"一路错下去)
  • 远端前置条件(先配好,不要指望交互输密码):
    • 从本机到目标机的免密(BatchMode=yes,卡在密码提示会超时)
    • 远端有 node(不在 PATH 里就配 ssh_node_path,例如 /opt/node/bin/node)
    • 必配两个根目录(缺任一,远端线开不起来):
      • ssh_workdir_root — 远端放工程工作目录的根
      • ssh_runtime_root — 远端放 sidecar 脚本的目录
  • ★ 工作目录在远端准备:本地 clone --mirror 出来的目录远端看不到, 所以远端线走的是远端建目录 → 远端共享 mirror(<root>/.mirrors/)→ 远端 clone → 远端切分支。同一远端上的多条分支线共享一份 mirror。 台账里的 workdir 会被改写成远端路径(不是本地路径)。 远端拉取要求远端自己能认证该仓库 —— 本插件不把凭据推到远端 (凭据不进工作目录、不进 remote URL)。拉不动就响亮报错。
  • 每次拉起会 scp 把 sidecar 推过去(幂等)——不推就会跑上一次的旧脚本, 症状是"改了没生效",极难归因。推送失败响亮报错,不静默用旧脚本。
  • 用 commondev_agent_state 看 ssh_host 确认打到哪台机。

24.2 接其他 CLI(zcode 等)——不改插件核心

执行后端是声明式的:放一个目录就多一个后端。

<插件数据目录>/runtime/<cli>/
    driver.json     ← 描述:脚本名 / 参数 / 环境变量 / LLM 源策略
    sidecar.mjs     ← 实现 sidecar 契约(ping / prompt / state / ...)

driver.json 关键字段:

字段 含义
script 脚本文件名(默认 sidecar.mjs)
node auto = 用插件自带的 node
args 参数模板,占位符 {workdir} {sessionId} {sessionName} {dataDir} {pluginDir}
env 环境变量模板(同样支持占位符)
llmConfig pi = 插件替它写 pi 的配置;none = 它自管凭据,插件不碰
sessionDirSuffix 会话目录后缀

纪律:

  • 未知 agent_cli 响亮报错,绝不回退到 pi —— 回退会用错后端跑任务而无提示。
  • driver.json 损坏也报错(不猜默认),否则是"改了没生效"的经典来源。
  • 内置 pi 不需要 driver.json(有内建描述),老部署不受影响。
  • 现成例子看 examples/custom-cli/zcode/(源码核实过的 ZCode 契约)。

25. ★ 两种插件安装粒度(临时 vs 全局)

粒度 作用域 落到实处 用什么
临时 一个工程对话 <workdir>/.pi/settings.json pi install -l
全局 插件自带 pi 的所有工程 <agentdir>/settings.json pi install + PI_CODING_AGENT_DIR

支持的源(实测):npm:@scope/pkg@1.0.0(带版本即钉住)/ git:github.com/u/r@v1 / https://github.com/u/r(当 git 源)/ ssh://… / 本地路径(不复制)。

三条纪律:

  1. 必须传 PI_CODING_AGENT_DIR,否则「全局」安装会写进用户的 ~/.pi/agent/settings.json —— 那是对宿主的越界。
  2. ⚠ 安装是复制,不是引用:pi install ./pkg 会把包复制到目标;改源目录 不影响已装的那份(我曾因此得出「插件不生效」的错误结论)。要更新必须重装。
  3. 安装是显式一步,不藏在 agent_up 里;来源(npm 包名 / 仓库 URL / 版本钉点)要可见。

26. ★ LLM 源与 pi 配置:直接写 pi 的配置文件

⛔ 不要自建第二套配置存储(本稿前版曾提 config_commondev,已废弃)。 pi 本身就有完整的配置系统,插件只是它的受控写入入口:

<agentdir>/settings.json   ← 偏好 / 默认 model / 「全局」包声明
<agentdir>/models.json     ← 兼容端点与模型(★ LLM 源写这里)
<agentdir>/auth.json       ← API key / OAuth(0600)
<agentdir>/mcp.json        ← MCP 服务器

内核把 LLM 源存在 config 表(不是 config_<plugin>),键前缀 core.llm.:

core.llm.provider                 = llmsproxy     ← ★ 默认源就是这一项
core.llm.sources.<n>.base_url / .api_key / .model / .adapter / .context_window …

导入时字段一对一对上:base_url→baseUrl、api_key→apiKey、 adapter=openai→api="openai-completions"、model→models[].id、 context_window→contextWindow。

工具面(每个工具对应一处 pi 配置):

commondev_pi_source_import   把内核的源批量导入内置 pi
commondev_pi_source_add      添加源(写 models.json / auth.json)
commondev_pi_source_list     列出(默认**不返回** api_key)
commondev_pi_source_default  指定默认源(写 settings.json)
commondev_pi_config_get/set  读写其他 pi 配置
commondev_pi_mcp_add/remove/list

四条实现纪律:

  1. 写的就是 pi 的配置文件,不造中间层;格式与 pi 自己写出的保持一致 (否则 pi 重写文件时会把自定义字段抹掉)。
  2. 改前备份、改后回读校验:json 写坏会让内层 pi 直接起不来;失败则回滚 + 响亮报错。
  3. key 不进日志/不进返回值。
  4. 不改用户全局:所有写入带 PI_CODING_AGENT_DIR=<agentdir> 或显式 scope, 绡不碰 ~/.pi/agent/。

两条红线:

  1. key 不能明文落盘到无保护位置:models.json/auth.json 含明文 key ⇒ 目录 0700、 文件 0600。内核那套是明文存 config.db(644 全局可读)—— 别把这套惯例带进来。
  2. 不得静默回退到宿主凭据:导入失败/源不可用就报错,不要偷偷用 ~/.pi/agent/auth.json 跑起来(那是宿主的凭据,用了就是越界)。

27. 远端环境缺失:问用户,不要自己装(硬纪律)

ssh 不通          → 报错(配置问题,不是缺环境)
无 pi / 无 node   → ★ 注入 L3「远端 <host> 未装 X,是否安装?」→ **你(agent)去问用户**
目录不可写        → 报错(权限问题,需人处理)
  • SDK 没有“向用户提问”的 API(插件是子进程,不能直接弹 UI)。现有机制是插件把 决策消息注入给 agent(L1–L3),由 agent 问用户(先例:cli 的 /stop 用 L3)。
  • 不得擅自安装:不 curl | sh 到别人的机器上。要先说清「装什么、装哪里、要什么权限」。
  • 不冒充成功:环境缺失时 agent_up 响亮失败 + 留一条待决策项, 不静默降级到本地跑(那会让人以为活派到了远端)。
  • 问完要能续:安装动作本身是可见的一步(可查、可重跑),不是 agent_up 的隐式副作用。

28. 投递纪律(这是最容易犯的错)

每个工程的投递必须落到同一个 session_id。 实测反例(别重犯):

错法 后果
漏传 --session-id pi 静默开新会话 —— 你以为在催上一次,其实是把活派给了陌生人
两个进程并发同 --session-id 会话文件被写坏:user A/user B 相邻,assistant B 在前、assistant A 落在最后 ⇒ B 那轮的模型上下文里根本没有 A 的用户消息,树链交错

⇒ 投递前先拿台账里的 session_id,投完断言它没变;一个 session 绝不并发投递。 这两条插件侧已做成结构性保证(单一投递队列 + 断言),你做主管时不要绕过插件直接 去敲 pi -p。

★ 另外两条实测硬经验(别再踩):

  1. SDK 路径必须显式指定会话 id。用 createAgentSession({cwd}) 而不传 sessionManager 时,pi 自己生成 UUID 作会话 id ⇒ 与台账名字不一致 ⇒ 断言永远失败(实测:期望 br-cdtest-feat_alpha,实际 01a13959-…)。 正确:SessionManager.create(cwd, sessionDir, {id}) 再传给 createAgentSession。
  2. 工具返 ok ≠ 消息到了。内核对成功回执刻意极简(防 output loop), 所以 ok 只说 handler 成功了。必须去会话文件里看到 user: <payload> 才算数。 (这条能防住“以为投了、其实没投”这类假成功。)

29. 三层审查的分工(别把自己变成贵的高级码农)

层 审什么 不审什么
agent 自审 代码正确性、风格、实现细节 —
你(主管)复审 这次的改动,判据够不够:引入了哪些新失败方式?判据覆盖了吗?变异咬得住吗? 逐行读 diff
插件复核 机械执行「正向 + 变异」 语义判断

30. ★ 不要把「测试通过」当作「改对了」

插件只能跑你预先写下的判据。判据没覆盖那条 bug,插件照样打勾 —— 自动化不会发现假绿,只会让假绿跑得更快、更可信。

因此复核必须两步:

阶段 做什么 不通过的后果
正向 跑判据 → 期望通过 实现有 bug,驳回
变异 ★ 自动改坏被测代码 → 期望判据报红 判据太松,驳回

没有变异阶段,就是在给「测试通过」这个信号不断充值信用,直到它骗一次大的。 本仓实证:一条只查前缀的判据在变异测试下仍全绿,是变异把它咬出来的。

配套纪律:判据写「某标识符是否出现在函数体」时必须先剔除注释再匹配 (曾因注释里含 outputParent 之类的词导致假绿)。

31. 工程的目录与生命周期(一个仓库并行开发)

branch_open
  └─ <hash>-<name>/ (由 mirror **本地 clone** 派生)
       ★ hash = H(项目+分支):**唯一性来源**(同仓库不同分支必不同目录)
       ★ name = 分支名派生:仅可读性
       └─ 干活 → 提交 → push → PR
            └─ 合入(或废弃)
                 └─ ★ **整个目录删除**(不保留、不归档)

要点:

  • 合入后直接删目录 —— 用户明确要求;不删则下次同名分支会与残留目录混淆。
  • 一个仓库可并行:每分支一份独立 clone。
  • ★ 同仓库不同分支必然不同目录:<hash> 含分支(H(项目+分支)),hash 是唯一性来源; name 只做可读性。实测:feat/alpha vs feat/beta → b3426c01-feat_alpha vs 0566c1f5-feat_beta。
  • ⚠ 慢操作必须异步:内核把每次工具调用硬限制在 60 秒(toolcall.go:62, 硬编码无配置项)。git clone --mirror 同步做 ⇒ 工具报超时、但 mirror 其实已建好 —— 调用方只看到失败、状态却是“做了一半”。正确做法:工具只登记 + 发起后台作业, 用状态工具追进度(本插件:commondev_job_status)。
  • 为什么不是 worktree / --reference(实测):worktree 删后有 .git/worktrees/ 残留(需 prune);--reference 有 alternates 外部依赖,mirror 被清即损坏。 本地 clone 对象为硬链接 ⇒ 省磁盘、无外部依赖、删掉即彻底。
  • ⚠️ 代价:唯一性闸必须补上。worktree 天然禁止「同分支双挂」,独立 clone 没有这条 保护(两份 clone 都能在 feat/x 上各自 push,直到被拒才知道)。 台账必须对「同名且未结(open/review)的分支」建唯一索引,重复 branch_open 响亮拒绝,不静默复用。
  • 并行有前提:并行的必须是互不相干的模块。有依赖的任务(A 改接口、B 用接口) 两棵树互不可见,问题会集中到 merge 时爆发 ⇒ 先拆边界再谈并行。
  • 与内核「子不得比父活得久、无残留」同一条纪律(见第 20 节)。

Notes: 第三部分 hmap 插件开发与部署纪律

面向「要改 commondev」或「要写另一个插件」的场景。所有条目都在本机实测过, 不是从文档抄的。

32. 先搞清三件事,别猜

hmapdev version        # 工具链版本 + SDK 模块 + 构建用 Go
hmapdev sdk current    # 当前活跃 SDK 版本(★ 动手前必看)
hmapdev sdk list       # 本机已装的所有 SDK 版本
  • 核心仓 /home/program/TrueAgent(Go module gitcode.com/JianFeeeee/HomeAgent)
  • SDK 仓 third_party/homeagent-sdk(独立 git 仓、独立版本号)

33. 开工流程

hmapdev init <name>              # Go 脚手架(--lua / --type remotedevice 可选)
cd <name> && vim plugin.go       # 注册工具
hmapdev debug .                  # 不想装就先跑一遍
hmapdev build                    # 产出 dist/<name>_bundle.hmap

34. ★ 五个必踩的坑(按踩中概率排序)

# 坑 事实
1 SDK 版本 插件编译时校验 SDK 能力,用旧接口的工程直接构建失败,报错形如「当前 SDK 缺少 sdk.InjectOptions.Priority」,并给出两条出路:hmapdev sdk install <version> 或 hmapdev sdk install --from <本地源码>。核心仓有新能力但 SDK 未跟上时用 --from
2 hmapdev build/debug 不支持 --help 不是打印帮助,而是去读当前目录的 plg.json ⇒ 你会看到 error: read plg.json: no such file or directory。这不是故障。要看 flag 用 hmapdev --help
3 产物是 ZIP,不是二进制 dist/<name>_bundle.hmap 是 ZIP(魔数 PK),用 unzip 解,内含 plugin.bin.linux.amd64 等多平台二进制。生产要的是解包后的单个二进制 /home/newqqagent/plugins/<name>/plugin.bin(不是 zip)
4 plugin.json 是安装标志 生产目录里没有它,插件不会被识别。plg.json 是构建标志,两者不同
5 plg.json 的 sdk 字段必须完整 x.y.z 写 "1.4" 会被直接拒绝(SDK 版本纪律:跟随内核中版本、patch 位恒为 .0,一条内核线只有一个 SDK 版本)

35. 注册工具的正确形状

s.RegisterTool(tp+"mytool", sdk.ToolDef{
    Name:        tp + "mytool",     // ← Name 必填,放结构体**首位**
    Description: "……",
    Parameters:  map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
    Handler:     h.mytool,
    NoMemory:    true,               // 声明字段一律放**末尾**
}, s)

并发声明(同一批多个 tool_call 时):

  • ParallelSafe: true —— 可并发。三个条件都满足才可声明:handler 线程安全 / 不与同批工具争抢同一资源(SQLite 写、同一设备、同一输出通道)/ 执行顺序无关。
  • Serial: true —— 必须串行,优先级高于 ParallelSafe。
  • 默认(都不写)= 整批串行,是保守设计不是遗漏。

⇒ commondev 的写操作(凭据增删、台账写、workdir_prepare、pr_open/merge、 agent_send)一律 Serial: true;只读(list/status)才 ParallelSafe。

36. ★ 外部插件的能力边界(选外部形态就必须接受)

公开 SDK 里声明的符号,不等于外部插件拿得到。 外部插件跑在独立进程里,能力由桥接注入:

API 外部插件的实际 该用什么
Events() 恒为 nil(桥不注入 subscriber) 桥接运行时按你的声明完成订阅
UnregisterOutputChannel 静默无效(只注入 registrar,不注入 unregistrar) 注销需重载插件
PriorityL4 被夹到 L3 用 L1–L3
SocialAPI 写操作 只有 6 个只读方法 读用 GetPerson 等
PluginMgr() 只有 3 个方法(ReloadOne/ListLoadedPlugins/IsPluginDisabled) 就这 3 个
resident_agents 等内核工具 调不到(proc 桥无对应方法) 由 agent 侧调(见第 3 节)

外部形态的 L4 天花板:commondev 作为外部插件永远拿不到 L4。

⚠️ 不要用 ToolAPI 去调内核内置工具(如 resident_agents):ToolAPI 是 单例,per-agent 数据靠「最后注入者」,而创建驻留子也会注入 ⇒ 你可能打到 某个驻留子的表上。这是内核已自陈的局限,别踩。

L4 归属表(这块最容易搞错):

agent 它的"内核" 谁能产生 L4
根 agent 内核自身 + 内置插件 raiseKernelInterrupt、WebUI 终止按钮等
驻留子 父 agent 父(父→子消息固定 L4)
外部插件(如 commondev) — 永远不能(被夹到 L3)

⇒ 外部形态下,「立即取消整条分支」只能靠 L3 打断主管 + 主管下令。

37. 安装与验证

# ① 从 bundle 取出本机平台二进制
unzip -o dist/<name>_bundle.hmap 'plugin.bin.linux.amd64'
# ② 放进插件目录(<name> 必须与 plg.json 的 name 一致)
mkdir -p /home/newqqagent/plugins/<name>
cp plugin.json /home/newqqagent/plugins/<name>/
cp plugin.bin.linux.amd64 /home/newqqagent/plugins/<name>/plugin.bin
chmod +x /home/newqqagent/plugins/<name>/plugin.bin
# ③ 重启并确认工具**真的注册了**
systemctl restart homeagent.service
journalctl -u homeagent.service --since "-2 min" | grep "registering tool: <你的工具名>"

内置 vs 独立(目录存在 ≠ 有 plugin.bin):

for p in webui cmd seq qq; do
  printf "%-8s " $p
  ls /home/newqqagent/plugins/$p/plugin.bin >/dev/null 2>&1 \
    && echo "独立(要单独构建部署)" || echo "内置(随 homed 部署)"
done

★ 改了内置插件的源码 ⇒ 必须重新构建并部署 homed,重启服务不生效(//go:embed 编进二进制)。 ★ 判断线上跑的是哪次构建,看 commit 字段(不要用 strings):

K=$(sqlite3 /home/newqqagent/config.db "select value from config_webui where key='api_key';")
curl -s -H "X-API-Key: $K" http://127.0.0.1:8080/api/v1/status | grep -oE '"commit":"[^"]*"'

⚠ 必须带 X-API-Key:无认证时返回 200 + 登录页 HTML,只看状态码会误判。

38. 排查「插件装了却不生效」

按顺序查,别跳步:

ls /home/newqqagent/plugins/<name>/plugin.bin          # 1) 内置还是独立
journalctl -u homeagent.service --since "-5 min" | grep "registering tool:" | grep -i <name>   # 2) 注册了吗
journalctl -u homeagent.service --since "-5 min" | grep -E "loaded: <name>"                    # 3) 加载了吗
journalctl -u homeagent.service --since "-5 min" | grep -iE "<name>.*(error|panic|failed)"     # 4) 启动报错

39. ★ 安全纪律(commondev 拿的是能 push 用户所有仓库的权限)

★ 架构决定(用户拍板 2026-10-14):敏感信息拦截下沉为内核安全模块。

理由:LLM 调用本身不可信,而插件在信任边界之外 —— 插件注册的 stage 拦截是“自愿的”,不是强制的,也算不上边界。

★ 它不是单纯拦截,而是一个闭环(用户纠正:“只是简单的拦截的话, 绝大部分插件会失去原本的功能”):

① 拦截      LLM 出向前扫到明文 → 换成 @{别名}
② 加密暫存  明文 AES-GCM **落盘**存内核(不落明文)
③ 别名      ★ **哈希键**:sec_ + HMAC-SHA256(masterKey, plain)[:8]
              ⇒ **同一明文自动得同一个别名**(同变量合并,天然性质)
              ⇒ 必须 HMAC 而非裸哈希(裸哈希可字典枚举);**不加 salt**(否则失去合并)
④ 回填      插件调用前把 @{别名} 换回明文 ← 插件无感知

★ 变量不自动淘汰(跨轮/跨子 agent 随时要用);agent 可主动调用 安全模块提供的提交/删除工具。泄漏风险靠主动删控制,不靠超时。

★ 因为内核零 IO(一切外部交互都从插件走),只需两根咽喉: · 入向 toolcall:StageHost.ExecuteTool(name, args)(core/stages.go:140) · 入向 outputch:设备 Execute(core/output.go:76) · 出向 LLM:provider 层的 Chat(req)(已核:构造 CompletionRequest 有 7 处, task 层拦不全)

★ 变量系统是全局唯一的(用户拍板):主 agent / 子 agent / 驻留子 agent 同一套别名表。已核依据:驻留子共享父的 ProviderManager(resident.go:185), 而它全进程只建一份(bootstrap.go:249)⇒ 安全模块天然是进程级单例, 不需要跨 agent 同步。 ⚠ 变量共享 ≠ 授权共享:能不能调某工具/通道仍看该 agent 自己的 AllowedOutputs (与 KernelSource 一道是 per-agent 的)。两者正交。

★ “插件无感知”是硬要求:替换发生在路由到插件的之前; 插件代码一行不改、不可分辨、无法绕过 ⇒ 存量插件自动获得该能力, 且无需给 SDK 加参数(不破接口兼容)。

对本插件的直接影响(大幅简化):插件可以把密钥明文直接投给 LLM, 由内核安全模块全权负责;enc_* 工具族与加密 shell 应删减, 不再自带加密库/别名表(前稿那套作废)。

详见 docs/zh/kernel-secret-firewall-design.md(含硬约束 S1–S9)。

两条不可让步的约束: · 只能替代引用,永远不可取回明文(用户明确)—— 读接口与用接口是两件事; · 模式库宁漏勿误伤 —— 误报会搞坏正常任务。

凭据:按名字引用 + 变量填充(用户明确要求的能力)

enc_store(alias="gitea-token", value="${GITEA_TOKEN}")   ← ★ 引用,不是明文
enc_list                                                  ← 只列名字,**绝不返回值**

之后一切操作只带别名:@{gitea-token} —— 参数里永远不出现 secret。 详见 第二部分 §13(那里是权威)。

为何要 ${ENV_VAR} 变量填充:(①)明文不落盘;(②)dev/prod 换环境变量即可; (③)轮换凭据不需改插件数据。

★★ 判据与复核:插件 GREEN ≠ 改对了

这是本流水线最重要的一条纪律。 插件只能跑你预先写下的判据; 判据没覆盖那条 bug,插件照样打勾。自动化不会发现假绿, 只会让假绿跑得更快、更可信。

真实数据(我用最小例子复刻过):

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 ⇒ 同一变异立刻报红

⇒ 所以复核必须两步,你定义判据时必须同时给变异规则:

commondev_test_define(
  project="MyProj", name="clamp 上下界", kind="unit",
  script="go test -count=1 ./...",
  expect="上下界都被限制",
  mutations=[{ id="M1", file="calc.go",
               old="	if v > hi {\n		return hi\n	}\n", new="",
               note="删掉上界分支(真 bug)——松判据看不见它" }]
)
commondev_review_submit(branch_id="br-...")
commondev_review_result()          ← 取结论

结论三档,注意第二档不是"通过":

verdict 含义 你该做什么
pass 正向全过 且全部变异被咬住 可以进下一步
pass_with_gaps 正向过,但有变异没被咬住或判据没变异规则 不能当通过 —— 去补判据/变异,再复核
fail 有判据没过 打回执行 agent

★ pass_with_gaps 就是"你的判据有盲区"。把它当绿色勾, 就是在给自己攒一次大的意外。

定义变异规则的三个要点:

  1. old 必须在文件里恰好出现一次 —— 出现 0 次 ⇒ 变异无效(却会被误报成"判据太松"); 出现多次 ⇒ 可能改错地方(却会被误报成"咬住了")。 多出现时给出更长的上下文。
  2. 变异要"真坏" —— 改的是逻辑,不是格式。改不坏就验不出判据的松紧。
  3. script 里带 -count=1 —— Go 默认缓存测试结果; 命中缓存时测试根本没跑,"变异后报红"就无从谈起。

双凭据与分支保护:已移到第二部分(技能纪律 §15–§16)

原来这里用 commondev_account_add(role=...) + commondev_protect_branch 两个 插件工具实现。这些工具已移除(平台知识不再进插件)。

现在这样做(命令见 第二部分 §15):

  1. 在平台上建两个用户:一个普通用户(执行侧,只能 push feat/*)、 一个有管理权限的(主管侧,能审批与合并)。各自签发 token。
  2. 两个 token 都存进凭据库:enc_store(alias="gitea-exec", ...) / enc_store(alias="gitea-sup", ...)。
  3. 给主干上锁(第二部分 §15 的 curl 命令,四个开关都要)。
  4. 审批与合并用主管侧别名(§16)。

★ 执行侧必须用普通用户 token —— 用 admin token 会直接绕过分支保护, 机制失效且无任何报错。这是最容易搞错的一点。

★ 拦你的是平台,不是插件 —— 所以改插件代码也绕不过去。 这才是「机制」与「流程约定」的区别。

两条不可逆的红线:

  1. 变量解析不到值 ⇒ 响亮报错,绝不静默当空串 —— 后者会退化成匿名访问, 表现成 401 而非“凭据没配”,极难排查。
  2. secret 绝不进返回值/日志。

平台形状差异(不能一个认证头走天下):

平台 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>

未知平台必须报错(不猜形状)。

其他:

  1. 凭据不入 config.db:现有惯例是明文存 config_<plugin> 表,而 config.db 是 644 全局可读。commondev 的凭据性质完全不同 ⇒ 放独立文件 <DataDir>/credentials.json,0600,启动时校验权限,不对就拒绝启动 (本仓纪律:响亮失败,不静默降级)。
  2. 优先 SSH key 而非 PAT;每平台独立、最小授权,不要一个 token 通吃。
  3. token 不进 remote URL:拼接仅用于单次命令(pushURLWithToken), clone 出来的 remote 不带凭据 —— 否则删掉临时目录也删不掉凭据。
  4. 不要硬编码 token:走 Settings() 或宿主注入。
  5. 拉起 pi 时隔离端口:内层 pi 会尝试监听 A2A/ACP 端口并打 EADDRINUSE, 显式设 PI_CODING_AGENT_SESSION_DIR / PI_A2A_PORT / PI_ACP_PORT。

40. ★ 孤儿清理(插件有自己的硬约束)

内核对自己的驻留子有硬约束:子不得比父活得久 / 无孤儿 goroutine / 无残留 temp (resident-subagent-design.md §10,写成判据)。

插件对自己拉起的进程必须同样硬:Stop() 必须是幂等的全清 —— pi 进程、sidecar、ssh 会话、workdir(含凭据残留检查)一个不留。 写成判据:TestStopKillsAllRuntimes(变异:删掉某个清理 → 必须报红)。

41. 参考资料(别凭记忆写 API)

内容 路径
SDK 源码 third_party/homeagent-sdk/sdk/
可运行的示例插件 third_party/homeagent-sdk/example/(a2a / acp / qq / plugindev…)
能力边界 third_party/homeagent-sdk/docs/guide/capability-boundary.md
并发声明 third_party/homeagent-sdk/docs/guide/parallel-tool-declaration.md
部署手册 docs/zh/deploy-runbook.md
sidecar 参考实现 internal/plugins/clawhubadapter/sidecar.go

★ 写插件前先翻 example/:那里的代码是编译通过的,比文档可靠。 文档与示例冲突时以示例为准。 注意 example/ 是外部插件, 能被它们编译通过的写法,外部就一定可用。


Notes: 红牌(做了就是错)

  1. 不划通道就当派了活 —— 登记 ≠ 划入;不划则所有输入涌向根 agent。
  2. 绕过插件直接敲 pi -p —— 漏 --session-id 会静默开新会话。
  3. 并发投同一个 session —— 会写坏会话(实测)。
  4. 没有变异阶段的复核 —— 等于给假绿充值。
  5. 凭据写进 config.db 或 remote URL。
  6. 改了内置插件源码只重启服务 —— 必须重编 homed。
  7. 手改生成的文档(docs/api/*.md 首行写着「请勿手改」)—— 改 sdk/*.go 注释再重新生成。
  8. 为了让插件生效去改 homed 的源码 —— 先确认它是不是内置插件。