
这是什么
HarnessRouter Community Edition 是一件两半产品里的开源那一半:一个把现成的 agent harness 放到同一套接口后面的自托管服务器,以及它实现的那份 Unified Harness Protocol。一个 Docker 容器里跑三个进程——只对外发布一个端口的 Next.js console、说 Responses 兼容 API 的 gateway、以及真正拉起 CLI 的 runner;gateway 与 runner 只听回环地址,数据库、文件、密钥与工作目录都落在同一个 /data 卷里。会话之间靠操作系统用户与工作目录隔开,不靠容器,于是每种 harness 都留着真实的 shell、文件系统与 git。它支持十六种 harness,每一种都钉住一个精确的上游版本、并在安装时按校验和验证:Codex、Claude Code、Hermes、Pi、DeepSeek Harness、OpenCode、Qwen Code、Gemini CLI、Cline、Oh My Pi、goose、Kimi Code CLI、Aider、OpenHands、CheetahClaws 与 System One。产品只调一个端点,用 metadata.harness_id 指名 harness,就能拿到持久会话、流式进度、文件与产物、取消以及结构化失败;自定义 harness 可以带上自己的指令、工具、MCP 服务器与 skill,不必重新部署。environment——一个项目的文件与依赖,构建一次、只读挂载——在 2026-09-28 成了协议的第七种对象。全部 Apache-2.0,provider 密钥留在自己手里。
谁做的那位在 pull request 里回话的维护者。仓库 603 次提交里有 331 次出自他名下的两个身份——richard-epsilla 310 次、Richard Song 21 次——也出自两个邮箱:richard@epsilla.com 271 次,richard@readily.ai 21 次。一个 GitHub Actions 机器人另外写了 209 次提交,贡献者名单里有十八个人,而 603 次提交中有 477 次带着共同作者尾注。
它是怎么搭起来的
组成 · 6一个把现成的 agent 运行时变成同一套契约背后后端的服务器,而契约、实现以及度量契约的套件都放在同一个仓库里。三个进程共用同一个容器:只对外发布一个端口的 console;说出 Responses 兼容接口、并且管着 harness 生命周期、模型目录、控制存储与几个 plug 平面的 gateway;以及在会话工作目录里逐轮拉起某个 CLI 的 runner。会话之间靠操作系统用户与目录隔开,不靠容器,于是每种 harness 都留着真实的 shell、文件系统与 git,而隔离的功夫花在文件权限、属组和每个环境自己的身份上,不花在镜像上。度量装置是架构的一部分,不是事后补的:support matrix、自定义 harness 那一维、family tour、plugin/browser/environments 三列以及 benchmark,判定全都从轮次记录里读。又因为新增一种 harness 要在十七个地方登记、漏掉任何一处都只会静静失效,对这个设计最老实的读法是:它最难的不是把一轮任务路由出去,而是不让某一项能力无声无息地消失。
- protocol/
- 标准本体:三个带日期的版本共 36 个文件,一个 schema 构建器与 OpenAPI/JSON Schema(27,956 字节长到 52,992),CONNECTING、SERVING、GOVERNANCE、VERSIONING、naming 几份文档,一份 16 KB 的 changelog,以及 conformance 套件——37 个文件,其中光
uhp_conformance/checks.py就有 108,299 字节,旁边还有 CLI、报告生成器、几个桩和一份 fixture 包。reports/里已经落着 harnessrouter-ce、托管版 HarnessRouter 以及第三方服务器 superqode 的实测结果。protocol/site/build.py(69,851 字节)负责生成发布站点,而站点构建会逐页断言必须有 description。 - gateway/ 与 runner/
- 两个服务端进程。
gateway/app.py一个文件 1,025,053 字节,装着 Responses 接口、harness 生命周期与各种目录,旁边是media_plane.py(106,461)、plugs_plane.py(47,280)、browser_plane.py(39,196)以及控制与 SQL 存储,其下 74 个测试文件。runner/server.py465,197 字节,按 CLI 分家的驱动就摆在旁边——aider 36,780、openhands 36,763、dsh 28,339、cheetahclaws 20,982、systemone 14,611 字节——另有 60 个测试文件,其中好几个钉住的是中继与归一化本身,而不是它们承载的行为。 - ui/
- 一个 Next.js console,121 个文件、1.4 MB:harnesses、environments、integrations、kits、keys、plugins、tasks 各一条路由,
HarnessSettings.tsx51,445 字节、TaskChat.tsx46,893 字节;lib/harness.ts(47,865 字节)里放着 backend 联合类型,以及带每种 base 上游版本号的内置目录。界面的大半落在三张样式表上——revamp.css127,436、hr.css100,961、v2.css79,554 字节——另有一个 studio 目录装着 traces 与工作流视图。 - docker/ 与 .github/workflows/
entrypoint.sh59,305 字节,在首次启动时按精确版本安装每一个 harness CLI 并当场验证:上游发布校验和的就去取来比对,没发布的就自己算摘要、钉在版本号旁边。另有装 starter kit 与 skill 的两个脚本,以及镜像与参考 runner 两个 Dockerfile。旁边是八个工作流:release.yml(13,554 字节)、tests.yml、conformance-measure.yml、conformance-remeasure.yml、open-pr.yml、readme-stars.yml 与一个清理任务。- docs/ 与 scripts/
- 那些说法所依赖的家当:
support-matrix.md345,599 字节,配套结果文件 3,033,683 字节、笔记 128,496 字节;self-hosting-guide.md59,286、harness-verification.md15,937、environments.md11,866 字节;benchmark 页面与它那份 213,287 字节的结果文件。产出这些表的脚本也在树里——playwright 驱动的矩阵、按 python 写的 environments/plugins/browser 各列、custom-harness 与 family-tour 两个驱动、SpreadsheetBench 的 benchmark 包,以及一个把 console 每个界面在每个宽度上扫一遍的响应式审计。 - 仓库根
- README.md 21,440 字节;CONTRIBUTING.md 3,714 字节,写着改动如何落到 main;一份 2,541 字节的行为准则、一份 1,328 字节的安全政策、2,336 字节的第三方声明与 Apache-2.0 许可。而
docs/images/是这个仓库里最大的一块:25 个文件、34.7 MB 里占 20.9 MB,多是 README 与 support-matrix 表格用到的 GIF 和 console 截图。
取舍,以及它替代了什么
任务用
metadata.environment指认环境 替代 在请求顶层加一个environment字段changelog 把这次反转写明了:任务这一面就是一份 Responses 请求,而
metadata是它的扩展点,harness_id本来就走在里面,所以顶层字段会是同一件事的第二套约定,而且是没有任何 Responses SDK 会原样发出来的那套。已发布的章节用顶层字段只挂了一天,随后规范、schema、套件与参考服务器一起改。「配置好的 harness」才是可寻址的单位 替代 让客户端只选一个 base
同一个 base 换个系统提示、换个模型、换套工具就完全不一样。把配置做成可寻址的一等对象,意味着产品不必重新部署后端就能改变 agent 的行为,也意味着两个产品可以共用一台服务器而不共用行为。
会话是隐式的,由第一次任务创建 替代 动手之前必须先建一个会话
规范把账算在自己的正文里:先要求
POST /sessions会多一次往返、多一种失败、多一个要清理的对象,而这个概念多数第一次任务根本用不到。只发一次性任务的客户端永远不必知道「会话」这个词;需要连续性的客户端引用手上已有的 id 就够了。environment 只读挂载,与工作目录并排 替代 把构建好的环境当作产物命名空间
它永远不属于某个 container:产物来自工作目录,而环境是工作目录去读的东西。隔离用操作系统属组与身份来划,而不是用容器——每个环境按自己的 id 派生一个组,源目录 0700、构建版本 0750、存储目录可穿行但不可列目录——因为安装过程跑的是包自己的代码,而它绝不能以 root 跑。
pull request 由机器人身份开出来 替代 由操作者自己开,或者用个人访问令牌开
main要求一个批准、谁都没有旁路,管理员也不例外;而 pull request 的作者又不能自己批准——于是一个人单干、或者借自己账号让 agent 干活,就永远合不进去。个人访问令牌出于同样的理由也不行。工作流的文件头还记着:机器人令牌开出来的 pull request 根本不触发检查,所以仓库改用 GitHub App 的安装令牌。
依据protocol/versions/2026-09-28/architecture.md、protocol/CHANGELOG.md、protocol/IMPLEMENTATIONS.md、protocol/versions/2026-09-28/harnesses.md、CONTRIBUTING.md、.github/workflows/open-pr.yml、docs/harness-verification.md、scripts/support-matrix/README.md、ui/src/lib/harness.ts、docs/benchmark.md、README.md,以及完整的 508 个文件树及其按文件、按目录的体积。
制作过程
6 个阶段- 01
五十二天,每隔几小时就发一个版本
仓库建于 2026-08-09 20:19 UTC,半小时后第一笔提交就到了——「Self-contained core: gateway + runner running with no cloud」。到 2026-09-30 最新那笔为止,它攒下 603 次提交:八月剩下的二十二天里 303 次,九月 300 次。侦察报告取到的最近 20 个 release 从 2026-09-28 17:19 的
v0.25.17排到 2026-09-30 10:12 的v0.27.0-rc.2,其中仅 2026-09-29 一天就发了十四个正式版:00:19 的v0.26.0,接着 0.26.1、.2、.3、.5、.6、.7、.8、.9、.10、.11、.12、.13 一路到 09:46,17:27 又是 0.26.14,当晚再加一个v0.27.0-rc.1。2026-09-30 则在v0.27.0-rc.2前后补上 0.26.15、0.26.16 与 0.26.17。版本号一次修复走一格,而这份样本里找不到v0.26.4。这些数字旁边是 2,793 个星、285 个 fork、21 个 watcher、12 个开着的 issue、十八位贡献者、508 个文件与 34.7 MB 的仓库,语言 Python,许可 Apache-2.0。 - 02
477 条共同作者尾注,和一个专开 pull request 的机器人
每一笔提交的尾注都在说它是谁写的。603 笔里有 477 笔带着共同作者尾注,其中 433 条点名的是某个 Claude 模型:Claude Fable 5.1 出现在 195 笔上,Claude Opus 5(1M context)128 笔,Claude Fable 5 81 笔,Claude Opus 4.8 21 笔,Opus 5 四笔,Opus 5.5(1M context)两笔,Opus 4.8(1M context)与 Sonnet 5 各一笔。人也出现在共同作者里——AIinWEB3 十四笔,维护者本人十笔,sakurahello1、ZixiaoL 与 Hao Lu 各三笔。另有 209 笔提交的作者是
github-actions[bot],而这个身份是刻意的。main只收 pull request:一个批准、所有检查全绿、谁都没有旁路,管理员也不例外;而 pull request 的作者不能自己批准,于是一个人单干、或者借自己的账号让 agent 干活,就永远合不进去。open-pr.yml改用github-actions[bot]的身份把 pull request 开出来,这样维护者变成审阅者而不是作者。它的文件头还记着促成它的那次故障:用GITHUB_TOKEN开出来的 pull request 不会触发检查工作流,因为 GitHub 要防递归运行,2026-09-06 有一个文档 pull request 就正好卡在这里。现在仓库改用 GitHub App 的安装令牌来开;注释里说个人访问令牌不行,因为那样操作者本人就成了作者。 - 03
一个社区修复,被拿去对着二进制复核,最后换一条路做了
pull request 332 来自 lab1207,是 issue 202 的第一片:Codex 不认识的模型拿不到
apply_patch工具,模型于是在不支持的调用上打转。那条回复是这个仓库里关于「一次改动到底怎么定下来」最好的记录。维护者先谢过作者细读了spec_plan.rs,同意诊断是对的,然后拿他们出货的那份 Codex(0.154.0)去复核,发现杠杆在别处:apply_patch_tool_type是 Codex 模型目录里的字段,不是config.toml的键;出货的二进制接受它写在顶层然后无视它,也没有任何配置读取器消费它,而它的枚举只有一个值freeform。目录可以用model_catalog_json换掉,但这个工具只在 Codex 自己的多环境执行器里注册。一个记录请求的桩 Responses 端点显示,gpt-5-codex与一个未知模型发回的工具有列表一模一样,两种情况里都没有apply_patch。332 被关掉,改由 346 落地,而提交上带着原作者的共同署名。另一处社区改动是 326(kuishou68),更小也一样说明问题:两个 npm 脚本指向一个根本不在树里的ui/doc-editor/scripts目录,于是npm run不再列出会立刻报错的命令。 - 04
要让一种 harness 算「真的能用」,得付什么
这个项目用一句话立了自己的标准:能回答的 harness 不等于能用的 harness。对每一种 harness、每一个它菜单上提供的模型,一个会话要跑五个场景——第一轮、追问一轮、中途换到另一个模型再换回来、一个必须产出的文件、以及一次回收:故意把沙箱放掉,再追问时仍要记得第一句话。四条规则把一次运行判成结论,每一条都由代码执行而不是靠眼睛。一条拿每份轮次记录自带的连接戳去比对被测的那条连接。一条比对「要的模型」与「实际服务的模型」,而这条规则曾经在三种 harness 上根本没法执行——goose、cline 与 qwen 都不报告实际服务的模型——直到这些后端上的每一轮都改走一个回环中继,由中继从 provider 自己的字节里读出
model。还有一条要求渲染出来的文件卡片就是这一轮存下来的那些文件,因为只问「有没有一张卡片带着预期文件名」曾让一个被渲染两次的文件冒充产出物好几个月。family tour 起源于同一类失败:2026-09-27 有人在五个轮次里发现,一次完整作答的 CheetahClaws 轮次被记成了失败,原因是模型上下文较小、让 CLI 在轮次中途压缩了历史,而驱动按那段历史里的某个位置来判断这一轮。新增一种 harness 要在十七个地方登记,而除了 console 的 backend 联合类型之外,漏掉任何一处都只会静静失效。 - 05
标准改动当天就发布
协议就住在这个仓库里,七周内出了三个带日期的版本——2026-08-11、2026-09-12、2026-09-28——JSON Schema 从 27,956 字节长到 52,992,OpenAPI 文档从 40,505 长到 76,548,conformance 套件则一路是 2026.9.12.post3、post4、2026.9.28,然后 post1、post2、post3 全挤在同一天里。2026-09-28 那一版最初把任务所指的环境放在请求顶层;复核把它改回
metadata.environment,changelog 也把话说白了——那份已发布的章节把这个字段挂在顶层挂了整整一天——而规范、schema、套件与参考服务器 0.26.7 是一起改的。同一天,EN-02 靠着去读客户端结果对象里从来没有过的字段而在每台服务器上出错(84 项里 83 项),改完之后整个 class 84/84 通过。另一次安全复核发现PUT /v1/admin/integrations的 422 会把请求体连 provider 密钥一起原样回显,因为 FastAPI 默认的校验响应会把每个出错的值挂在input下面;现在的回答是一个只讲字段的信封,并且有一个测试专门提交假密钥、断言它不出现在响应里。还有一次,站点构建要求每个页面都有 description,于是新章节缺这一项时,协议站就一直停在上一版。 - 06
两种取舍,都被写下来了
规范把理由写在正文里,而不是另开一份设计文档。为什么要「配置好的 harness」而不只是一个 base:同一个 base 换个系统提示、换个模型、换套工具就完全不一样,把配置做成可寻址的一等对象,产品就能不重新部署后端而改变 agent 的行为,两个产品也能共用一台服务器而不共用行为。为什么会话是隐式的:先要求
POST /sessions会多一次往返、多一种失败、多一个要清理的对象,而这个概念多数第一次任务根本用不到。「不知道」不算「空」,「空」也不算「零」。而服务器不得宣称自己没实现的能力,因为宣称就是承诺。度量规则是同一路写法:一次只接一个 provider,而且一次运行独占这台实例,因为部署恰好落在两个轮次之间的空隙里,就会杀掉一个 worker 的会话、毁掉整列结果。代码记下的是另一种交换。gateway/app.py是一个 1,025,053 字节的 Python 文件,runner/server.py465,197 字节,gateway/tests/test_media_mcp.py182,516 字节、test_media_attack.py93,274 字节——看起来是用体积换来了「harness 注册表只在一处」这件事。连自动化都有一个带日期的来历:插件矩阵自 2026-09-18 起就留在仓库里,因为放在临时目录里的那一份在评审途中被清空了。
相关档案
全部档案 →第 070 号
OpenChatCut
一个本地优先的视频剪辑器,剪辑方式是跟它说话:内置 agent 与外部 Codex、Claude Code 会话调用的是界面自己在用的同一套剪辑工具,于是每一处改动都落在一条真实的多轨时间线上——是片段、转场、字幕、特效或音频,仍然能拖、能撤销、能导出。工程与素材留在本机,预览与最终渲染都出自 Remotion。
第 064 号
delegate-skills
一个技能包,给每一种编码 agent CLI 各配一份委派技能:编排方写好自足的任务书,另一条 CLI 去改真实工作树,而审查与提交留给人。
第 061 号
Reticle
一个 MCP 服务器加一个只在开发期生效的 SDK:让编码 agent 从应用内部去读、去操作一个正在运行的 web 或桌面应用,然后给出判词和该改的文件与行号,而不是一张截图。