My LLM harness. Currenly OpenCode.
  • Python 50.8%
  • JavaScript 29%
  • TypeScript 7.9%
  • Rust 7.4%
  • Nix 2.3%
  • Other 2.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-11-03 16:02:29 +08:00
agents feat: upgrade Zotero MCP and expose local libraries read-only 2026-11-02 15:44:59 +08:00
benchmarks feat: add session affinity, metrics, and benchmark tooling 2026-10-26 02:37:30 +08:00
config feat: upgrade Zotero MCP and expose local libraries read-only 2026-11-02 15:44:59 +08:00
docs feat: include no-negative-echo in default agent prompts 2026-11-03 16:02:29 +08:00
nix feat: include no-negative-echo in default agent prompts 2026-11-03 16:02:29 +08:00
plugin-support feat: add independent computer-use model selection 2026-10-29 12:34:15 +08:00
plugins/v2 feat: add independent computer-use model selection 2026-10-29 12:34:15 +08:00
prompts feat: include no-negative-echo in default agent prompts 2026-11-03 16:02:29 +08:00
scripts feat: include no-negative-echo in default agent prompts 2026-11-03 16:02:29 +08:00
skills feat: unify project Kuruwa manager and add harness exec 2026-10-28 23:12:34 +08:00
tests feat: include no-negative-echo in default agent prompts 2026-11-03 16:02:29 +08:00
ui/hub feat(hub): add Ghostty workbench and RMUX terminal recovery 2026-10-14 15:44:41 +08:00
.envrc Package harness as a standalone flake-parts agent environment 2026-09-17 21:27:45 +08:00
.gitignore feat: add session affinity, metrics, and benchmark tooling 2026-10-26 02:37:30 +08:00
.muxrun.toml Initial commit 2026-09-17 11:40:19 +00:00
AGENTS.md feat: include no-negative-echo in default agent prompts 2026-11-03 16:02:29 +08:00
Cargo.lock feat(hub): add Ghostty workbench and RMUX terminal recovery 2026-10-14 15:44:41 +08:00
Cargo.toml feat(hub): add Ghostty workbench and RMUX terminal recovery 2026-10-14 15:44:41 +08:00
CONTEXT.md feat: guard legacy daemon cutovers and document session collaboration 2026-10-26 15:45:10 +08:00
flake.lock chore: update Kuruwa with lifecycle recovery and tracing fixes 2026-11-02 12:44:51 +08:00
flake.nix chore: refresh Hakoniwa and track flake input branches 2026-10-30 14:13:01 +08:00
justfile feat: make sandbox tmp writable and update Hakoniwa 2026-11-01 04:24:55 +08:00
README.md feat: upgrade Zotero MCP and expose local libraries read-only 2026-11-02 15:44:59 +08:00

gjz010/harness

gjz010 的 opinionated OpenCode 个人工具:通过 Nix 打包启动器、签名提交工具、 Kuruwa、skills、direnv 和 goal plugins,并保留 bubblewrap 沙箱。它不是需要复制到每个 项目的 starter,也不再根据 Git 身份选择 developer profile。

本仓库使用 flake-parts,支持 x86_64-linux。开发本仓库不需要 devenv CLI; 使用方既可以直接 nix run,也可以把包加入已有 flake 或 devenv 项目的 packages。

在已有项目中安装(agent 入口)

把一个已有项目接入 gjz010/harness,请让该项目的 agent 读取 skills/install-gjz010-harness/SKILL.md。该 skill 是 canonical 安装/迁移流程:先读用户 Git URL 指向的 revision 的 README 与 skill, 盘点目标项目,再按目标现有的 flake、devenv 或无 Nix 环境加包,最后按证据去重 (goal/TPS、Matt skills、Kuruwa、签名、agent/routing 兼容、启动沙箱)。

请安装 gjz010/harness(git+https://git.gjz010.com/gjz010-harness/harness.git)

安装不改动目标项目的工具体系:已有 flake/devenv 只合并 input 和 package;无 Nix 项目保留自己的 npm/uv/cmake 等,宿主具备 Nix 与 x86_64-linux 时可用 nix run 'git+https://git.gjz010.com/gjz010-harness/harness.git' -- --workspace 项目路径 直接运行,不为项目新建 flake、devenv 或 Justfile。默认不安装到全局 profile。

安装/更新快捷入口

公开子命令是 harness install [--workspace DIR](与 web 同层的启动器模式), gjz010-harness-install 和 #install app 只是执行 harness install "$@" 的兼容 shim。它在当前终端开启一个交互式 harness 会话,并让其中的 agent 盘点所选项目: 没有持久 harness 依赖时走 install-gjz010-harness,已有依赖时走 update-gjz010-harness 把该项目的 harness pin 更新到上游。默认 workspace 是 cwd,用 --workspace DIR 切换。

# 已发布版本;包含新接口的 revision push 前请改用本地 checkout
nix run 'git+https://git.gjz010.com/gjz010-harness/harness.git#install'
nix run 'git+https://git.gjz010.com/gjz010-harness/harness.git#install' -- --workspace /path/to/project
nix run .#install
nix run .#install -- --workspace /path/to/project

这是 agent 驱动的入口,不是确定性的系统安装器:真正的安装/更新由会话内的 skill 按目标项目现有工具执行。它需要宿主启用 flakes 的 Nix、x86_64-linux、 可用的 bubblewrap namespace,以及目标 provider 的认证;默认不写入全局 profile。 命令在当前终端前台运行 TUI;退出会话即返回 shell。更新 skill 的独立性、目标输入 边界和报告要求见 skills/update-gjz010-harness/SKILL.md。

安装模式按次选择 fast primary,不改动持久配置:入口先确保本项目 daemon 就绪, 再通过原生 POST /api/session(agent=fast)新建会话,最后用完整 TUI 显式连接该会话 并带上 prompt。它不写任何配置文件、不改 daemon 的 default_agent、不重启 daemon; OPENCODE_CONFIG_CONTENT 不是单个 JSON 对象时明确失败且不回显内容。 安装模式沿用正常入口选择(显式 oc2 > env > workspace .harness/config.toml > oc2), 只接受 --workspace DIR 和 --help,不转发任意 OpenCode 参数,prompt 由包内只读 资源 harness-install-prompt.txt 提供单一来源。等价入口:

harness install --workspace /path/to/project
harness oc2 install --workspace /path/to/project
nix run . -- install --workspace /path/to/project

改动包内资源(含安装 prompt 与 overlay 逻辑)后必须退出并重启 OpenCode。

本地配置说明:gjz010/harness 不假设目标存在 OpenCode,也不要求固定的配置布局。 目标已有的 opencode.json/opencode.jsonc、tui.json/tui.jsonc 会被原生合并, project MCP、instructions、普通 plugins 和标量保留。agent 也按原生 deep merge 处理:同名 agent、default_agent、disable 与 permission 只覆盖显式字段,未覆盖 字段继承打包定义;没有强制还原 agent 拓扑的运行时插件。 机器相关的 token、代理和本机参数放在启动时 source 的私有 env 文件中(见下文), 不进入 Git、Nix 表达式或 store。完整的调研证据见 docs/harness-installation-research.md。

直接运行

宿主需要启用 flakes 的 Nix,以及允许 bubblewrap 使用的 Linux namespace。 在本地 checkout 中运行(新接口尚未 push 时请使用本地版本):

nix run .
nix run . -- --workspace /path/to/project
nix run . -- oc2 web --workspace /path/to/project
nix run .#install
nix run .#install -- --workspace /path/to/project

新接口发布后,可在目标项目目录运行:

nix run 'git+https://git.gjz010.com/gjz010-harness/harness.git'

安装或将包加入开发环境后,命令接口为:

harness [oc2] [选项] -- <OpenCode args...>
harness [oc2] install [--workspace DIR]
harness [oc2] config [--workspace DIR]
harness [oc2] web
harness [oc2] service start|status|stop|restart
harness hub start|status|stop|restart
harness sponsor list|add <profile>|key <profile>|remove <profile>|sync [--workspace DIR]
harness exec [--workspace DIR] [--] COMMAND [ARGS...]
harness identity exec -- COMMAND [ARGS...]

公开语法由包装器(scripts/harness-wrapper.rs,clap 4)解析并生成帮助,Bash 启动器 只消费包装器输出的固定执行请求,不再重复解析公共命令。harness --help、 harness oc2 --help、harness service --help 只打印帮助,不启动 launcher、daemon 或 OpenCode。没有 selector 时,命令默认直接打开项目(不自动 --continue);后台 daemon 的存活与 --continue 无关,--continue 只是可选地继续上一个会话。

当前只支持 OpenCode 2:harness oc2 显式选择 v2 入口(opencode2),默认入口也是 oc2。argv 最前端的字面 token 为 oc 时以退出码 2 明确提示 V1 入口已移除,不再 启动任何 OpenCode;只有 argv 最前端的字面 oc2 才是 selector,全局 flag 之后的 oc/oc2 以及透传段里的同名 token 都按原生参数透传,不会误选。 没有 selector 时入口解析优先级为:显式 oc2 > GJZ010_HARNESS_ENTRYPOINT(只接受 opencode2)> 所选 workspace 的 .harness/config.toml 中的 entrypoint(只接受 oc2)> 默认 oc2。

.harness/config.toml 是标准 TOML,从不作为 shell 执行。entrypoint 是可选的字符串 oc2;文件缺失或字段缺失时用默认 oc2。hooks 是可选字符串数组,列出启动前 按顺序 source 的宿主 Bash hook 路径(见下文“沙箱 hook”)。TOML 语法错误、hooks 类型 错误、[oc2] 字段类型/取值错误或文件存在但不可读时明确以退出码 2 失败;只有在没有 显式 selector/env、实际使用配置里的 entrypoint 时才校验它的取值,显式指定入口时会忽略 该字段但仍读取 hooks。[oc2] 表可选:runtime = "bun" | "node"(默认 bun)选择项目 的 OC2 运行时,node_inspector = true | false(默认 false)为内部 daemon 请求 Node Inspector;未知的 [oc2] 键保留不动,类型或取值错误则启动失败。读取本身不会创建或重写 该文件,也没有 harness init。clap 在任何真实执行前处理根与子命令的 --help/--version,因此它们不读该文件也不执行 hook。所选 workspace 决定读取位置: 默认 cwd;--workspace DIR 是全局选项(可在根、selector 或 harness 子命令旁使用, 可重复,最后一个生效),等号与空格形式都接受,路径中的空格原样保留。 selector 之后的第一个非 harness 命令 token 起即为 OpenCode argv,透传段里的 --workspace 不会被 harness 攫取。文件只从所选 workspace 读取,不向父目录查找。

harness --version / harness --help 由包装器直接回答,不启动 OpenCode 或 bubblewrap;harness oc2 --version 之类的 selector 后参数会原样转发给该入口(selector 把未知参数一律当作透传)。harness --version 显示 gjz010/harness,若构建来自 Git checkout 还会附加已编译进去的 revision(plain path 构建无 revision 时只显示名称)。

workspace 默认是 cwd,不是位置参数。切换项目使用 --workspace,OpenCode 参数放在 -- 后(也可直接前导,例如 harness --continue、harness --session ID、 harness run ...、harness api ...;harness 自有子命令名之后的参数才被当命令)。 -- 之后的所有字节与顺序原样传给 OpenCode,消息内容不会被解析成 harness 命令。

harness exec [--workspace DIR] [--] COMMAND [ARGS...] 在调用方环境和当前目录运行配套 工具:优先使用包内 Kuruwa/Hakoniwa,设置项目唯一 manager 的 KURUWA_ROOT,清除继承的 KURUWA_SOCKET,并将 HAKONIWA_ROOT 指向项目真实桌面状态目录。它准备缺失配置,但不 自动启动服务、进入沙箱、加载私密 harness env 文件或注入 agent Git 身份。COMMAND 后 的参数原样传递,退出码保留;在沙箱内调用仍留在原沙箱。

harness exec hakoniwa ps
harness exec kuruwa instance list
# 正常 harness/service start 已建立 host 和 harness-sandbox 后:
harness exec kuruwa start --instance host --name ops -- bash
harness exec kuruwa grant --tenant harness --terminal ops

每个项目只有一个宿主 Kuruwa manager。管理员拥有 host instance,租户 harness 拥有 bwrap 内的 harness-sandbox instance;OpenCode daemon、agent 任务和 Hub TUI 共用后者。租户可以 native attach 自己实例的终端;用户 grant 的单个宿主终端只开放 输入、输出、resize 和等待。service stop/restart 会结束该项目的全部终端并清除 grant。 拓扑、交付和升级步骤见 项目 daemon 文档。

harness identity exec -- COMMAND [ARGS...] 在当前目录与当前环境运行命令,并通过与 沙箱启动相同的 per-process 身份层注入打包的 agent Git 身份(user.name/user.email、 签名密钥、commit.gpgsign、gpg.format=openpgp 与 gpg 程序),保留继承的 GIT_CONFIG_* 条目并追加身份覆盖。它不启动 sandbox、项目 daemon 或 OpenCode,不读取所选 workspace 的 .harness/config.toml,因此无效的 GJZ010_HARNESS_ENTRYPOINT 不影响它; 命令参数不经过 shell,按原样 exec 并保留退出码。该子命令只作用于当前目录,显式 --workspace 以退出码 2 拒绝。

正常启动(OC2 standalone 与内部 daemon)自动应用同一身份层,所以沙箱内 agent 后续的 原生 git 提交自动 GPG 签名;rebase/cherry-pick/amend 等重放保留原作者(身份层只设置 committer 并清除继承的作者变量,不修改 Git 日期)。这是默认身份层,不是反绕过边界:原生 git 覆盖参数仍然生效。gjz010-harness-commit 二进制与 just gjz010-harness-commit recipe 保留,向后兼容地复用同一身份实现。已有项目 daemon 需用户显式 service restart 才会带上 新的身份环境,命令重放不会自动更新 live daemon。 web 打开 oc2 的 web 界面:它先启动或复用本项目 daemon(冷启动一次,之后复用), 只打印不含 secret 的基础 URL,并在宿主用 xdg-open 打开同 origin 的一次性 /auth/connect/<code> 配对链接;浏览器关闭后 daemon 继续运行,因此额外监听参数 (--port/--hostname 等)以退出码 2 拒绝,daemon 端口由原生 managed server 决定。 缺 xdg-open 或打开失败时,改为打印清楚的手动打开提示与同一短期配对 URL,不把长期 password 放进 URL、argv、错误或 stdout。web 只做 daemon 配对,不提供固定端口,也没有 端口环境变量;无需 bootstrap,已有 provider/auth 可继续使用。

install 是公开子命令,只接受 --workspace DIR 和 --help(重复 --workspace 时最后一个生效),拒绝 -- 和任意 OpenCode 参数;未知参数由 clap 以 退出码 2 拒绝,且不启动 launcher。 它按上文“安装/更新快捷入口”所述按次选择 fast 并打开完整 TUI 会话。harness install --help 只打印用法,不启动 OpenCode、bubblewrap 或安装流程;harness --help 仍由包装器 短路回答。

service start|status|stop|restart 管理本项目的 v2 daemon;action 由 clap 校验,未知 action 或多余位置参数以退出码 2 拒绝,harness service 无 action 只显示 usage,不启动 daemon。详情见 docs/project-daemon.md。

harness hub start|status|stop|restart 管理每用户一份的全局 Harness Hub:它 把按 workspace 运行的受管实例纳入自己的实例列表,并透明代理所选实例的原生 OpenCode API,附一个 SolidJS WebUI。hub 是全局命令,不接受 --workspace 或 OpenCode 参数;harness hub stop 只停 Hub 自身,不停任何项目实例。默认状态目录为 ${XDG_STATE_HOME:-$HOME/.local/state}/gjz010/harness(可用 GJZ010_HARNESS_HUB_DIR 覆盖),默认不授权任何目录,需在主机编辑 hub.toml 配置目录范围;Hub 凭据与范围 规则不挂入项目 sandbox,用户在本机直接使用 harness 不受该范围限制。用法、认证、 WebUI、终端与维护检查见 docs/hub.md。harness web 仍是原项目 daemon 的配对入口,未改为 Hub WebUI。

v2 入口(harness oc2)

v2 默认运行时是 fork 自身的 Bun 构建产物。项目可在 .harness/config.toml 写 [oc2] runtime = "node" 选择同一 pin 的 Node 26.4.0 SEA 产物(nix/opencode-v2-node.nix, Bun 仅作为其构建驱动,实际进程是内嵌 Node);有效运行时优先级为 GJZ010_HARNESS_OC2_RUNTIME > 项目 [oc2] runtime > 默认 bun,两种产物都在 harness closure 内,切换不需要重新构建、改 launcher/daemon 或额外安装 Bun/Node,其它值在启动前于 stderr 报错并 exit 2,argv 原样转发。宿主不需要安装 Bun 或 Node。项目运行时同时用于 OC2 client 和本项目 daemon;已运行的 daemon 沿用启动时的运行时,改选项目配置或环境变量后需显式重启才会换用(可用 harness oc2 config 或 harness oc2 service restart)。harness oc2 --version 默认打印 opencode v2.0.24+978c01f,Node 运行时打印 opencode2-node v2.0.24+978c01f。 该 pin 的 TUI 右栏新增 Location idle 状态:它读取上游真实的活动/idle 截止计时并展示, 只反映实际计时,不发送保活请求,也不改变 Goal 的暂停/自动继续策略或任何沙箱边界。

v2 入口加载只读 $GJZ010_HARNESS_RESOURCES/v2/opencode.json,其中原生 plugins 显式声明九个预装 plugin 目录;它不替换 OPENCODE_CONFIG_DIR,因此宿主 provider/auth 配置仍按原生优先级合并进来。每个 canonical workspace 由独立的宿主侧 daemon 托管沙箱内的 serve --service:交互式 TUI(含 --session/--continue)、 run、api 都显式连接本项目 daemon;未接线的子命令明确报错,外部 --server 被 拒绝。harness oc2 --standalone 仍可选择沙箱内私有 server 而不使用 daemon。布局、 service start|status|stop|restart 与生命周期见 docs/project-daemon.md。该 config 仍保留 instructions 字段, 但 pinned v2 只保留字段而不加载它,因此 config/instructions.md 不会注入 v2 会话;共享的 简短 harness 环境提示改由下文 Astra 一节的 v2 context hook 追加。

项目数据库与 state 保存在 <workspace>/.harness/opencode/ 的原生 opencode/ 子目录下,默认 DB 为 data/opencode/opencode-v2.db,不会触碰历史的 opencode-stable.db;显式 OPENCODE_DB(含运行时 env 文件)仍然优先,但必须落在项目 state root 内。项目不再使用独立的 OpenCode cache 目录:宿主解析后的 XDG_CACHE_HOME 在同一绝对路径读写挂入,详见下文沙箱边界。TPS 与 harness-version 客户端 UI 通过 v2 原生 OPENCODE_CLI_CONFIG_CONTENT 注入:以包内 v2/cli.json 为基底,与用户已有 的 OPENCODE_CLI_CONFIG_CONTENT 合并(用户键优先,plugins 按用户列表在前、首次 出现顺序去重),launcher 不读写宿主磁盘 cli.json。原生 v2 仍加载该文件,但 env overlay 会替换其 plugins 数组;需要保留的磁盘插件也应显式列入 OPENCODE_CLI_CONFIG_CONTENT。harness install 走 v2 时按上文连接 daemon 新建 fast 会话,不写磁盘配置,也不改 daemon 的默认 agent。V2 入口仍需要 provider/auth 与 沙箱,并未放宽任何边界;包升级、运行时环境、agent 身份或 hooks 变化需显式 service restart 本 workspace 的 daemon,配置/插件改动则需退出并重启 OpenCode。

客户端页脚(home.footer.status)与侧栏底部(sidebar.footer)由只读 gjz010-harness.version 显示两端构建 revision:client 读自身进程 GJZ010_HARNESS_RESOURCES/harness-build.json,server 在 setup 时捕获自身构建并经原生 RPC 上报,因此各自进程的环境不会混用,也不读取当前 workspace 的 git。两者已知且不同 时整行显示 warning 黄色,缺失、格式错误或旧 server 无该 RPC 时显示 unknown;dirty 构建的短 revision 保留 -dirty 后缀。/harness-version 命令与命令面板条目用原生 dialog.alert 展示完整两端 revision。每次挂载或调用命令自然刷新,无轮询;该插件不含 写入、模型工具、listener 或 timer,失败也不会导致 UI 插件加载失败。

harness config(项目 OC2 配置菜单)

harness config [--workspace DIR] 打开一个简单的终端编号菜单,用于查看和保存本项目 .harness/config.toml 的 [oc2] 选项;它只接受 --workspace DIR/--help,不转发任意 OpenCode 参数,且仅对 oc2 入口可用(当前唯一入口;harness oc config 在 selector 层即以退出码 2 提示移除)。菜单在启动 daemon、bwrap 或 OpenCode 之前 分流,所以查看或取消不会写 config.toml、也不会启动或重启 OpenCode daemon;launcher 仍可能按需创建默认的 .harness 忽略目录(.gitignore)。它分别显示:

  • 当前 daemon:运行/停止/未知,以及从活进程只读观察到的当前运行时(不是从正在编辑的 TOML 推断)。观察基于已有 RMUX session 和原生注册 PID:把注册 PID 当作 namespace PID, 用 /proc/<hostpid>/status 的 NSpid 末字段匹配,再核对 canonical cwd、serve --service argv 和 opencode 可执行名,只从该进程的环境内容中提取 GJZ010_HARNESS_OC2_RUNTIME 一个变量、不输出其他环境内容;不能确定、匹配到多个或权限受限时诚实显示 unknown, 不发信号、不输出任意 cmdline/env/凭据。
  • 保存值与下次配置:项目里的 runtime/node_inspector,以及待切换的目标值。
  • 有效运行时:GJZ010_HARNESS_OC2_RUNTIME > 项目 [oc2] runtime > bun,环境覆盖会 与保存值分开展示并提示它优先。

选项为 1) 切换 runtime、2) 切换 node inspector、3) 保存并退出、4) 保存并确认重启、5) 取消, 以及 6) 立即开启 Node Inspector、7) 立即关闭、8) 刷新当前状态。1–5 的保存/重启语义不变;6/7/8 是对正在运行 daemon 的运行时控制,不写 config.toml、不丢失待保存编辑、不重启、不退出菜单, 成功后重新观察当前状态,保存值与待保存值仍分开展示。7 会再次确认(会断开已接入的调试器),取消 则不调用;即使当前 runtime 未知也允许请求,Bun 由 server 以固定错误拒绝。控制走已认证的原生 RPC (gjz010-harness.inspector 的 status/open/close,固定空输入),但 Inspector 本身仍无认证、 只监听本机回环。已有 daemon 未加载该插件时,菜单说明需安装包含该插件的新包并重启一次 daemon 来加载,而不是每次开关都重启。RPC 超时表示结果未知(可能已执行),不会自动重试或重启;主线程被 调试器暂停或卡死时 RPC 可能不可达,外部开启的限制见下文。EOF 或取消不写文件;保存通过打包的 @decimalturn/toml-patch 就地 patch,保留注释、未知键与格式,保存后重新解析校验;保存失败不会 重启。重启必须再明确确认一次,且以真正用户环境覆盖重建子进程环境后重新进入 launcher,让它重新 读取刚保存的配置,因此不会继承 launcher 从旧配置导出的伪覆盖。

node_inspector = true 时,仅当有效运行时为 Node 才向内部 daemon argv 注入 --node-options=--inspect=127.0.0.1:0,绝不注入 TUI/client;端口 0 让 OS 为每个 daemon 分配空闲端口,因此多个项目同时调试不会争用同一端口。Bun 下配置为 true 只显示“不适用” 且不注入,不造成启动失败。菜单优先通过已认证的 status RPC 显示实时地址;RPC 不可用时 才从本项目固定 daemon session 的启动输出中解析严格的本机 回环 Node 调试地址,并用该端口的原生 /json/list 做只读校验:只有 type = "node" 且 webSocketDebuggerUrl 与解析值完全一致时才显示 listening at <host:port> 与 WebSocket URL; 日志缺失/被逐出、端点已关闭或 UUID 不符时地址显示为未知,不显示陈旧地址,也不扫描系统 端口。在 chrome://inspect 添加菜单显示的 host:port(或直接使用 WebSocket URL)即可调试; 地址由 OS 动态分配,打开/关闭/重启后可能变化。立即开启/关闭用上面的 6/7,不需要重启;下次启动 是否注入仍由保存的 node_inspector 决定。Inspector 无认证、只监听本机回环,仍不应视作隔离 边界。Chrome 连接与 source map 步骤见 docs/project-daemon.md 和 docs/opencode-v2-experiment.md。

若预计需要排查硬卡死,请提前通过启动配置或运行时菜单开启 Inspector,再在卡死时连接。 普通 Node 26.4 进程可通过 SIGUSR1 外部开启 Inspector,但当前打包的 OpenCode Node SEA 2.0.24+978c01f 将该信号用于 heap snapshot:实测写出约 168 MB 快照而不开 Inspector, --inspect-port 也不改变这一行为。不要向活动 OpenCode 发送 SIGUSR1 来尝试开启调试; harness 不做自动 signal fallback。已卡死且 Inspector 未开启时,本版本没有验证可用的外部 开启办法;这不影响正常响应时通过 RPC 动态开启 Inspector。

v2 内置项目级赞助(sponsor)插件:项目在 <Location>/.harness/config.toml 写非秘密 [sponsor] profile = "...",provider 按 Location 绑定独立 integration;未配置即保持个人 默认行为,配置错误或缺凭据时 fail closed。宿主原生数据库 ($XDG_DATA_HOME/opencode/opencode-v2.db,默认 ~/.local/share/opencode/opencode-v2.db) 是 sponsor 清单与全部 API key 的唯一权威来源:用 harness sponsor list|add <profile>|key <profile>|remove <profile>|sync 在宿主管理(key 为隐藏交互输入、无 key 参数,list 只读且不显示 key 值),harness oc2 的正常受管路径与 service start|restart 每次 ensure 后、交付客户端前把宿主清单与 key 单向同步到项目(包括删除),复用运行中 daemon 也同步。 显式 --standalone、help/version、service status|stop 与 harness config 不自动 同步;没有定时/后台同步,已打开的项目需重新打开或显式 harness sponsor sync 才更新。受管 项目的 /sponsor 面板只保留选择/恢复个人,添加/连接隐藏并提示改用宿主命令;原生 /connect 的本地改动不是权威、会在下次同步被覆盖。非受管项目(旧版行为)仍可用 /sponsor 创建本地全局 slug、经原生 /connect 录入 key 并保存项目选择,创建身份不写 项目文件。升级时先把已有 sponsor 在宿主录入,再同步,否则宿主权威会删除项目独有的 sponsor(不要删除数据库规避)。它依赖 fork opencode-v2 的 Provider.Info.credentialRequired 原语(当前 pin 为 branch v2 的 978c01f)与原生 prompt.metadata.provider TUI slot。两种运行时都来自同一 pin:默认 Bun 运行时打印 opencode v2.0.24+978c01f,Node 运行时打印 opencode2-node v2.0.24+978c01f。完整配置、边界与 UI 状态见 docs/sponsor.md。JS TOML writer 保留其它配置与注释,无法无损删除时 保守拒绝。面板区分已保存/生效选择,切换及恢复个人账户立即用于后续模型调用,已发出的请求 不追溯切换;即时切换失败时保留原生效选择并提示重启。 首页和会话输入区的 NewAPI provider 右侧显示紧凑 Sponsor 状态;个人账户或其它 provider 不显示。

v2 还提供 Location 级模型档位(/tier):项目可在 <Location>/.harness/tiers.json 选择,或整体重定义 config/model-tiers.json 中的档位(id、标签、样式与源 provider/model → 目标本地 provider/model 的精确一跳映射)。/tier 只把源引用换成目标 本地完整 model 配置:不猜 suffix、不改 HTTP body,主会话与带固定模型的 subagent 都适用; 未命中的引用与选中的 variant(如 high)原样保留,实际上游 API ID 与请求 settings 由目标 完整定义生效。它只影响该 Location 后续的模型解析,不改进行中请求,也不是所有机器/项目 共享的 global 选择;目标必须可用,失败明确拒绝。sponsor 与 tier 正交,tier 没有自动回退到 个人 key 的路径。构建期 scripts/build-tier-models.py 依 config/tier-models.json 的 key (本地引用)、extends(源 metadata 继承)、id(显式上游 API ID,省略时为本地裸 key)、 name 与 options 展开模型定义:4 个打包 fast/ultrafast 的本地引用带后缀、id 指回 base、options.serviceTier 为对应档位,因此只有本地引用带后缀,网关只收到 base 上游 ID 加 service_tier;生成的 id/options 经原生迁移成为 modelID/settings,结果只合入 v2/opencode.json,共享基础配置与 16 模型 manifest 保持不含别名。该功能依赖 fork 的原生 ctx.model.hook("resolve") 与别名自动 variants 的顺序无关 fold 修复,两者都由固定的 gjz010-harness/opencode-v2 fork 源码提供,随包构建进 Bun 与 Node 运行时;旧 host 无 该 hook 时 /tier 显示 unsupported,不宣称切换成功。配置 热生效,包/插件变更需用新包并显式 harness service restart 重启项目 daemon、再退出重启 OpenCode。详见 docs/tiers.md。

v2 还默认启用 生成图片导出 server plugin(gjz010-harness.generated-images, 配套无操作 TUI shim gjz010-harness.generated-images.tui):当上游 hosted 工具 (默认 image_generation)返回内嵌 image/* base64 data URI 的结果时,插件在该会话 工作区的 .harness/generated-images/ 目录下延迟导出原图,并把文件路径文字追加到发给 模型的该次 tool result。原图仍作为多模态内容喂给模型;路径文字只存在于该次请求内, 不写入会话数据库,也不改动历史消息。导出发生在下一次模型请求前(原生 session context hook),因此没有发生续轮的图片暂不落盘。默认启用;可用原生 plugin 条目的 options.tools 以精确工具名字符串数组替换或置空([])禁用导出,且只处理 providerExecuted 的 hosted 工具结果,普通同名 function/MCP 工具不处理。导出文件会 自动占用磁盘空间。该插件由 oc2 入口加载。启用、路径与复用/冲突 规则、重载边界见 docs/generated-images.md。

v2 还提供会话亲和性(affinity) TUI:会话输入区显示有效 affinity ID 与 Original/Override 状态,旁边的 ↻ 一键生成随机 override;/affinity 面板可查看原始 ID、当前 override 和有效 ID,并手动设置、随机刷新或恢复原始,/affinity-refresh 是 一步随机刷新命令。override 只对当前 session 的后续模型请求生效,子会话/fork 不继承, 写入后会读回校验。需要包含 affinityOverride 的新核心;详见 docs/affinity.md。

v2 的 Session 协作插件在同一个 server 内提供私聊、聊天群和全组广播。 人通过 /collaboration 管理成员并查看 IM 历史,agent 通过 collaboration 工具收发; 普通消息默认唤醒接收 session,silent: true 仅保存消息。goal 可以用 wait_goal 的 mailbox 条件等待非静默消息,但 IM 不受 goal 状态控制。 使用方式、重启边界和跨 workspace 限制见 docs/session-collaboration.md。

v2 的 Node SEA 运行时(GJZ010_HARNESS_OC2_RUNTIME=node)自带外置 source map:该 运行时的 out 在 SEA 主脚本旁提供 bin/opencode.mjs.map 软链接,指向独立的 debug 输出,Inspector 会自动解析并以 原始 TypeScript 调试;Inspector 仍需显式 --inspect 打开,默认不启用。out 与 harness closure 因此包含约 44 MB 的独立 map(不 inline 进可执行文件);默认 Bun 运行时 没有该输出。packages.x86_64-linux.opencode-v2-debug 仍作为 Node 产物单独、可直接 链接的 map 输出保留。 构建命令、Chrome 操作步骤与已知限制见 docs/opencode-v2-experiment.md 的 “External source map debugging (Node SEA)” 一节。

Flake 接口

inputs 为 nixpkgs、flake-parts、muxrun(兼容 pin)、kuruwa (ssh://[email protected]/gjz010-harness/kuruwa.git,固定完整 commit)、 mcp-harbor(https://git.gjz010.com/gjz010-harness/mcp-harbor.git)、hakoniwa (https://git.gjz010.com/gjz010-harness/hakoniwa.git,固定完整 commit)、 opencode-v2(fork ssh://[email protected]/gjz010-harness/opencode-v2.git 的 branch v2,978c01f53c4ea51383e97892ef8b0f1e18438ed9),以及 non-flake inputs matt-skills(GitHub mattpocock/skills)、no-negative-echo(GitHub LB623/no-negative-echo)、tps-meter (https://git.gjz010.com/gjz010-harness/opencode-tps-meter.git fork)与 goal-plugin-v2(https://git.gjz010.com/gjz010-harness/opencode-goal-plugin.git fork,锁定已发布的 v2 分支),由 flake.lock 固定。opencode-v2 保留上游自己的 nixpkgs(不设置 follows),是唯一 OpenCode 产品来源:由 nix/opencode-v2-node.nix 复用 fork 依赖树和 configure 阶段,改为 fork 的 Node 入口构建 Node 26.4.0 SEA 可执行文件(Bun 只做构建驱动),并复用离线 models.dev 快照与构建环境。mcp-harbor 同样保留上游自己的 nixpkgs pin(不设置 follows)。旧的 V1 opencode-nixpkgs 与 V1 goal-plugin input 已移除。hakoniwa 同样保留上游自己的 nixpkgs/devenv pin(不设置 follows), harness 直接消费其原生产品包。

输出 用途
packages.x86_64-linux.default / harness 同一个默认包,包含 Rust harness 包装器、gjz010-harness-install、gjz010-harness-commit、kuruwa、muxrun(兼容下载)、gjz010-harness-models 二进制;内部 Bash launcher 不作为公开 bin
packages.x86_64-linux.resources 只读 OpenCode config、instructions、plugin 和合并后的 skills;含 v2/ 树
packages.x86_64-linux.agent-tools 签名提交等 agent 工具,目前提供 gjz010-harness-commit
packages.x86_64-linux.kuruwa 固定的原生沙盒运行器与项目/任务控制面;启动器用它执行配置化 exec 和管理项目 daemon
packages.x86_64-linux.muxrun 固定版本的 muxrun(仅兼容下载;不在 launcher 工具 PATH,运行时不再读取其配置)
packages.x86_64-linux.mcp-harbor 固定版本的 mcp-harbor stdio MCP proxy,同时随默认包进入沙箱兜底 PATH
packages.x86_64-linux.hakoniwa 固定的上游 Hakoniwa 产品包(CLI、查看器、SDK、桌面运行时);不加入沙箱工具 PATH,MCP 使用其绝对 store 路径
packages.x86_64-linux.goal-plugin 离线打包的 goal plugin(V2 server.mjs/tui.mjs/rpc.mjs 三文件 bundle)
packages.x86_64-linux.model-snippet 独立的 gjz010-harness-models 模型 snippet 生成工具
packages.x86_64-linux.opencode-v2-debug opencode2 的 debug 输出,含 share/opencode/opencode.mjs.map;默认 out 通过 bin/opencode.mjs.map 软链接引用它,因此默认 runtime closure 已包含该 map
apps.x86_64-linux.default 运行 gjz010/harness
apps.x86_64-linux.install gjz010-harness-install 兼容 shim(exec harness install "$@"),按次以 fast 打开安装/更新 gjz010/harness 的交互式会话
devShells.x86_64-linux.default 开发本仓库所需的工具,不依赖 devenv CLI
devenvModules.default 可选薄模块,仅提供 gjz010.harness.enable、gjz010.harness.package;只加包,不启动服务

加入外部 flake

以下为外部项目的最小 flake.nix 示例;已有项目只需合并 input 和 package:

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    harness.url = "git+https://git.gjz010.com/gjz010-harness/harness.git";
  };

  outputs = { nixpkgs, harness, ... }:
    let
      system = "x86_64-linux";
      pkgs = import nixpkgs { inherit system; };
    in {
      devShells.${system}.default = pkgs.mkShell {
        packages = [ harness.packages.${system}.default ];
      };
    };
}

加入外部 devenv

在目标项目已有的 devenv.yaml 中添加 input(保留原有 inputs):

inputs:
  nixpkgs:
    url: github:cachix/devenv-nixpkgs/rolling
  harness:
    url: git+https://git.gjz010.com/gjz010-harness/harness.git

然后在该项目的 devenv.nix 中加包:

{ inputs, pkgs, ... }: {
  packages = [ inputs.harness.packages.${pkgs.stdenv.hostPlatform.system}.default ];
}

也可用可选模块替代上述直接加包方式:

{ inputs, ... }: {
  imports = [ inputs.harness.devenvModules.default ];
  gjz010.harness.enable = true;
  # gjz010.harness.package = ...; # 可选:覆盖默认包
}

这些远端示例需要包含新接口的版本已经发布;未 push 的本地改动不会被远端 URL 使用。

配置与重载

包内配置位于只读 Nix store,由启动器设置的 GJZ010_HARNESS_RESOURCES 指向。 resources 按 config/matt-skills.json 从 Nix input 复制 25 个完整 Matt Pocock skill 目录,并复制本仓库维护的 skills/kuruwa(说明项目任务根与原生 serve/instance/terminal 流程)。 选择的 skill 名称与原 vendored 集合相同,但上游 revision 可能包含更新内容, 不保证与旧副本逐字节一致。skills/configure-environment-in-gjz010-harness 仍由本仓库维护。 第三方 Skill no-negative-echo 也作为 non-flake Nix input 固定:flake.nix 以 github:LB623/no-negative-echo/<rev> 锁定上游整仓库,构建时只把其 no-negative-echo/ 子目录复制到只读 resources 的 skills/no-negative-echo/, 并对 7 个关键文件做 sha256 断言,源树或副本被替换都会使构建失败。 resources 还包含 plugins、instructions 和 OpenCode config。 修改源码后需要重新构建/运行新包,不能直接修改 store。

公共配置常量

以下两个文件是受信任的仓库构建输入,随包编译或复制进只读 store,不是本机设置, 也不应包含 token、私钥或宿主环境值:

  • config/commit-identity.sh:gjz010-harness-commit 的公开 agent Git 姓名、邮箱和签名密钥指纹 (只有公钥指纹,没有私钥),以 readonly 方式拼接进脚本,不能被子进程环境覆盖。 config/commit-identity-apply.sh 提供共享的 apply_commit_identity 函数;两者在构建期 一起拼接进启动器和旧 gjz010-harness-commit。启动器自动把该身份注入每次沙箱启动和 harness identity exec,旧工具与 gjz010-harness-commit recipe 保持兼容。
  • config/kuruwa-sandbox.toml:固定沙盒定义模板。首次启动复制到 workspace/.harness/kuruwa/config.toml;启动时定向迁移可识别的旧受管 tmp 布局,其他 自定义配置保留,不用模板整份覆盖。静态策略只使用 ${变量} 引用,不包含宿主 secret、过滤后的宿主配置内容或口令。
  • config/hakoniwa.toml:Hakoniwa root 配置模板。首次启动复制到 workspace/.harness/hakoniwa/config.toml,声明私有 /tmp 和默认 tmp-policy 插件的共享目录 /tmp/persist;与旧受管 tmp 布局配对的配置自动迁移,其他字段保留,省略字段由 Hakoniwa 在内存 中补全(不写回文件),修改只影响新桌面会话。
  • scripts/harness-sandbox-hook.sh:由沙盒定义引用的可信 Bash hook,负责条件性挂载 (SSH Include、公开配置、宿主 XDG cache、direnv、GPG/SSH/Wayland socket、model 过滤 FD) 并最后按原顺序 source 旧 env hook 与项目 hooks。
  • scripts/harness-tmp-finalize-hook.sh:新默认沙盒定义保留在最后的兼容 hook 入口; 它不再修改 /tmp 根挂载,因此每次启动私有的 /tmp 在本次启动内可写,退出时随 tmpfs 丢弃。先前建立的 persist、runtime 和用户子挂载保持各自权限。自动迁移仍将此 hook 放到旧受管配置的最后,并提示已有服务需要正常重启才能使用新挂载。

修改这些文件后需要重新构建/运行新包(Nix store 不会原地更新);本机差异继续通过 运行时 env 文件等既有覆盖方式提供,不能写进 Nix 表达式或 store。

宿主 OpenCode 的 config 目录仍被绑定以保留 provider/auth 与插件选项。入口使用 workspace 内的项目 state root(.harness/opencode/)保存 data/state;宿主 OpenCode 数据根 (data/state)不再挂入。宿主解析后的 XDG cache(XDG_CACHE_HOME,未设置或为空时 $HOME/.cache)在同一绝对路径以读写方式整体挂入,包括 OpenCode 与其它工具自身的缓存。 作为独立映射,宿主 direnv 授权目录以只读方式挂到项目 XDG_DATA_HOME/direnv,使宿主 授权/撤销仍然有效(其中的下载缓存随宿主 cache 共享,见下文 direnv 说明)。宿主 config 仍加载,所以这不是完全 hermetic 的 OpenCode 配置环境。用户已有的 OPENCODE_CONFIG_CONTENT 等配置来源仍需在排查时检查;详见 docs/project-daemon.md。

启动时可选 source 以下 shell 文件:

${GJZ010_HARNESS_ENV_FILE:-${XDG_CONFIG_HOME:-$HOME/.config}/gjz010/harness/env}

默认文件不存在不影响启动。config/env.example 只提供公开占位示例;真实 token、 代理和本机参数放在权限为 600 的私有 env 文件中,不能进入 Git、Nix 表达式或 store。该文件是会执行的 shell 代码,只使用可信内容。显式设置 GJZ010_HARNESS_ENV_FILE 时应指向已存在的文件。旧默认路径 $XDG_CONFIG_HOME/harness/env 不再隐式读取; 旧 ignored profile .env 不自动搬迁或覆盖,手工迁移见 迁移说明。V2 内置代理控制与 /proxy 命令用法见 docs/proxy.md。

GitHub MCP 与 gh 命令

打包的 resources 在沙箱 PATH 上提供 gh CLI,并在 config/opencode.json 中默认注册 官方 GitHub MCP server 为 mcp.github:类型为 local,命令为 github-mcp-server stdio, 可执行文件的绝对 Nix store 路径在构建时写入 command[0],不在 PATH 中查找。打包的是 GitHub 官方 release 二进制,内置 github.com OAuth 公共客户端凭据,因此首次使用无需 token 即可通过浏览器 OAuth 登录。GITHUB_PERSONAL_ACCESS_TOKEN(或 GitHub App 环境变量) 仍然可用且优先级更高,可写进上面的私有 env 文件;config/env.example 中有一行注释示例。 OAuth token 只保存在内存中,每次重启 OpenCode 后都需要重新登录。gh 使用自己的认证, 可独立使用:宿主 $XDG_CONFIG_HOME/gh(默认 ~/.config/gh)以只读方式回绑,宿主上已 gh auth login 的账号在沙箱内直接可用;沙箱内无法改写该目录,需要重新登录请在宿主完成。 修改 config 或 MCP 后需要重新构建/运行新包并重启 OpenCode;新建 chat session 不等于重启。

Forgejo MCP

打包的 config/opencode.json 默认注册 Forgejo MCP server 为 mcp.forgejo:类型为 local,命令为 forgejo-mcp --transport stdio --url https://git.gjz010.com。可执行 文件的绝对 Nix store 路径在构建时写入 command[0],不在 PATH 中查找;实例 URL 固定为 本站 https://git.gjz010.com,不使用运行时 FORGEJO_URL。打包的是 agentic-forges/forgejo-mcp 官方 goreleaser release(Linux x86_64, GPL-3.0-or-later)的固定版本 tarball,按既有的 fetchurl 固定输出校验,构建时无需任何 token。

认证只来自运行时环境变量 FORGEJO_ACCESS_TOKEN,没有 token 时 server 仍会启动,但 Forgejo 会拒绝未认证的 API 调用。把它写进上文权限为 600 的私有 env 文件即可,默认 路径为 ${XDG_CONFIG_HOME:-$HOME/.config}/gjz010/harness/env,可用 GJZ010_HARNESS_ENV_FILE 覆盖;文件保留已有内容,真实 token 不进仓库或 Nix store, config/env.example 只有注释示例。token 只授予实际需要的仓库范围和最小权限:server 暴露的 Forgejo 工具范围完全受该 token 的权限约束,harness 不额外放宽。

与 GitHub MCP 相同,修改 config、env 或换用新包后需要重新构建/运行新包并重启 OpenCode 项目 daemon;新建 chat session 不等于重启,新包或 env 变更 不会自动重启正在运行的项目 daemon。

Zotero MCP

打包的 config/opencode.json 默认注册 Zotero MCP server 为 mcp.zotero:类型为 local,enabled 为 true,命令为 zotero-mcp。可执行文件的绝对 Nix store 路径在 构建时写入 command[0],不在 PATH 中查找。Zotero 没有现成的 nixpkgs 包,因此在 nix/zotero-mcp/ 下用原生 Nix 打包固定的 zotero-mcp-server 0.15.2 及其 pdf extra (pymupdf 1.28.2),并把所需共享库一并纳入 Nix 闭包;运行时不会调用 uvx,也不 按需下载任何 Python 包。认证与本地/远端 API 配置按 server 自身约定在运行时提供,不写入 仓库或 store。

本地库默认只读可见:当宿主 $HOME/Zotero/zotero.sqlite 是普通文件时,维护的 sandbox hook 在 HOME 被掩蔽后把整个 $HOME/Zotero 以只读方式 bind 到同一路径;没有该数据库 的机器不会增加挂载,也不受影响。必须绑定整个目录而不是单个 SQLite 文件,是因为 0.15.2 的 local_db 后端还依赖同级的 zotero.sqlite-wal 文件和 storage/ 附件树。仅当调用方未设置 ZOTERO_DB_PATH(或值为空)时,hook 才用 --setenv 把它指向默认的 $HOME/Zotero/zotero.sqlite;显式值原样保留,且不会依据它扩大或改写挂载范围。若数据库 位于默认可见范围之外,需要在可信 hook 中自行添加只读挂载并设置路径。

该只读目录只覆盖读取。Zotero 的写入仍应通过 HTTP API 完成,不直接写本地 SQLite;本策略 不挂载 ~/.config/zotero-mcp,不提供任何可写目录,也不自动设置 ZOTERO_LOCAL 或写入 API 授权。使用新包或修改 hook 后,宿主需要显式重启本项目的 service 才会生效;运行中的 项目 daemon 不会热加载新包或新挂载,新建 chat session 也不等于重启。

与其它 MCP 相同,修改 config 或换用新包后需要重新构建/运行新包并重启 OpenCode 项目 daemon;新建 chat session 不等于重启。

Hakoniwa MCP(隔离桌面)

打包的 config/opencode.json 默认注册 mcp.hakoniwa:类型为 local,enabled 为 true,command 为包内 hakoniwa 的绝对 store 路径加 mcp。CLI、查看器、SDK 与 桌面运行时全部来自固定的上游 flake input 原生包,本仓不复制源码或打补丁。桌面状态存储 在选定 workspace 的 .harness/hakoniwa,但沙箱内 HAKONIWA_ROOT 固定为短路径 /tmp/harness-runtime/hakoniwa:维护的 sandbox hook 在已有的每启动私有 0700 /tmp/harness-runtime tmpfs 内创建该目录,并把 workspace 存储目录 bind 到下面,因此深 workspace 下会话 socket 仍保持短路径。宿主 /run//tmp 不会被写入;在宿主直接使用 CLI 或查看器时 HAKONIWA_ROOT 需指向真实 workspace 存储 root。该 pin 的 CLI、SDK 与 MCP 共用 config.toml:harness 首次生成 tmp = "private",并给默认 tmp-policy 插件设置 shared_dir = "/tmp/persist";每个桌面独占 /tmp,只共享应用 scratch,插件同时设置 桌面 TMPDIR。省略字段在内存中补全,修改只影响新会话;宿主可用 HAKONIWA_ROOT=<workspace>/.harness/hakoniwa hakoniwa config show --human 和 config check 查看。status(以及 start)的 session/control/rfb 是 root-relative 的 sessions/<name>...,ps.session 仍是客户端视图的绝对候选路径; home 与截图/录像输出字段保持原语义。宿主查看器用自身 HAKONIWA_ROOT 解析会话, 可直接打开沙箱 daemon 创建的桌面。root-relative 是破坏性协议变化:新版查看器和 discovery 拒绝 旧 daemon 的绝对身份,已运行桌面不会热升级;保存工作后正常停止旧桌面,再用新版新建 桌面。新版 CLI 仍可用 --session-path 对旧 daemon 执行 status/stop。宿主控制 connect 的长路径修复仍只覆盖 CLI ps/status/exec;查看器的 Unix socket 仍受 107 字节限制,root-relative 不改变这一点。该 pin 还默认让新桌面继承调用方的 XDG_CACHE_HOME(未设置或为空时回退调用方 $HOME/.cache,start --home 不改变 它);harness 把宿主 cache 以同一绝对路径挂入,因此桌面、沙箱工具与 MCP 端到端共享 一份宿主缓存。这个共享是 harness 的启动策略,不是 Hakoniwa 的产品默认;已有会话保留 创建时记录的缓存位置。桌面会话与宿主桌面、剪贴板、音频隔离, 但网络未隔离,不能当作恶意代码的安全边界。打包内容或换包后需重新构建并显式重启 项目 daemon;root 配置修改不需要重启 daemon。任何情况下新建 chat 都不等于重启。 完整边界、工具范围与验证命令见 docs/hakoniwa.md。

mcp-harbor

包内提供固定的 mcp-harbor stdio MCP proxy 二进制,随默认包和沙箱兜底 PATH 可用; 它把本地 stdio client 转发到远端 Streamable HTTP MCP backend,并可把 catalog 快照缓存到 项目内。默认包不注册任何 mcp-harbor MCP 服务,也不改动现有 MCP 条目:需要时由目标项目 通过 configure-mcp-harbor skill 配置。构建时从 mcp-harbor 上游复制完整的 skills/configure-mcp-harbor/ 目录到只读 resources ($GJZ010_HARNESS_RESOURCES/skills/configure-mcp-harbor/),直接采用上游原文, 本仓不维护副本或补丁。二进制与 skill 共用同一 flake pin,通过 just update-mcp-harbor 一起更新;更新后需要重新构建/运行新包并重启 OpenCode 项目服务。

Antigravity CLI(agy-gjz010-harness)

包内提供官方(unfree)Antigravity CLI(agy)的固定包装器,公开命令与 flake output 都是 agy-gjz010-harness:它随默认 harness 包进入工具 PATH,加入默认包的项目(含 devenv 原有模块)自然获得,不新增插件或原生 subagent。每次启动它都从 /proxy 保存的 ${XDG_CONFIG_HOME:-$HOME/.config}/opencode/harness-proxy.json 读取代理地址,把 agy 固定到受保护的 0700 状态目录 ${XDG_CONFIG_HOME:-$HOME/.config}/gjz010/harness/antigravity。构建用 nix build . 或 nix build .#agy-gjz010-harness;部署新包后需显式 harness service restart 并重开客户端。 调用顺序、登录步骤、代理与边界说明见 docs/antigravity-cli.md。

沙箱 hook

启动器在追加完全部默认参数和 web 参数后、exec bwrap 之前 source hook。set -euo pipefail 仍然生效:任一 hook 中失败的普通命令或 return 都会中止启动,后续 hook 与 exec bwrap 都不会执行;文件缺失、目录或不可读会以明确的 harness 错误退出。

hook 按以下顺序执行,列表在开始执行前就已确定:

  1. 运行环境变量 GJZ010_HARNESS_SANDBOX_HOOK(也可写进上面的 env 文件)。非空时解析为 可读普通文件(realpath -e,相对路径相对启动 cwd 而非 --workspace),source 一次。未设置或为空表示不使用它。
  2. 所选 workspace .harness/config.toml 的 hooks 数组,按配置顺序逐个 source;相对 路径相对规范化的所选 workspace 解析,绝对路径原样使用;空字符串条目按无效路径 处理而不是禁用。列表不做合并或去重,也不自动搜索项目。

这两类 hook 都以启动用户的权限执行受信任的宿主 Bash,可以读取规范化的 workspace 变量并向 Bash 数组 args 追加 bwrap 参数,因此能改变甚至覆盖默认沙箱限制;它们不是隔离 边界。环境变量 hook 仍是 opt-in;普通启动会读取所选项目的 .harness/config.toml 并执行 其中显式列出的 hook,因此该配置和脚本都必须可信。路径不会隐式发现或自动搜索;除这两类 来源外的启动器内部结构不是稳定接口。

例如把一个 workspace 内的目录再追加为绑定:

args+=(--bind "$workspace/data" "$workspace/data")

这里只展示未经沙箱验证的 bwrap 参数;是否可行取决于具体参数,不要据此假设 OverlayFS 等更复杂的能力。hook 每次启动只执行一次,修改 hook 后需要重新启动 harness。

项目依赖属于目标项目:继续使用它已有的 flake、devenv 或其他工具,而不是 把依赖写进 gjz010/harness 仓库。需要更改依赖、代理或凭据时使用 configure-environment-in-gjz010-harness skill,先区分项目依赖与本机环境。

direnv plugin 在每次 OpenCode shell/PTY 调用前通过 shell.env 刷新目标项目的 direnv diff,并处理变量删除。目标项目使用 direnv 时,修改环境后执行:

direnv reload
direnv export json >/dev/null

launcher 保留完整 DIRENV_* 元数据。plugin 首次刷新先正常执行 direnv export json, 使切换项目或进入无 .envrc 目录时仍能撤销旧环境;若没有导出变化且仍有活动的 DIRENV_FILE,仅在额外一次 export 子进程中移除 DIRENV_WATCHES,重新应用项目环境。 之后仍按正常 direnv 增量机制刷新,不每次重新加载,也不清除 DIRENV_DIFF、 DIRENV_FILE 或 DIRENV_DIR。若 .envrc 被阻止,先审查可信内容, 再在宿主终端执行 direnv allow .。沙箱只读使用宿主 direnv 授权记录,不允许 agent 修改宿主的授权库。若授权目录在 gjz010/harness 启动后才首次创建,需要重启 gjz010/harness 才会挂载它。授权记录与下载缓存是两回事:启动器只回绑授权记录;沙箱的 XDG_CACHE_HOME 就是宿主解析后的缓存目录(默认 ~/.cache),因此其中的 direnv CAS 下载缓存与宿主共享并整体可写,但授权记录仍只读,agent 不能修改宿主授权库。本仓库 .envrc 使用 use flake;外部项目保留自己的激活方式。plugin 不替项目选择或迁移环境工具。

PATH 优先级:打包的 harness 工具列表通过 GJZ010_HARNESS_TOOL_PATH 暴露,启动器 把它从 wrapper 生成的前置段移出并追加到 PATH 末尾,因此项目/宿主 PATH 优先,打包工具 兜底。launcher 自身从打包工具列表固定 bwrap/OpenCode 的绝对路径,不受项目同名程序 影响。direnv plugin 初始化只移除 PATH 末尾完整匹配的一个 launcher 兜底块,保留项目/宿主 原有的同目录项;若末尾已被其他环境刷新改变,则不猜测删除。direnv 自身以 process.env.PATH 为刷新基线;每次 shell.env 交给 OpenCode 执行环境的 output.env.PATH = 刷新后的项目/宿主 PATH + 唯一追加的打包兜底,不重复累积,也不回流到下一次 direnv export 的 env。即使没有 PATH delta、export 为空、目标目录没有 .envrc 或刷新报错, 执行环境仍然保留兜底。plugin 在初始化时从该工具列表解析打包 direnv 的绝对路径, 所以项目把 PATH 完全替换掉也能刷新。修改本段涉及的 launcher/plugin 后需要重新 构建/运行新包并重启 OpenCode;新建 chat session 不等于重启。

V2 入口(harness oc2)为 direnv 刷新增加了进度提示:刷新超过 1 秒时通过 native RPC 事件通道向 TUI 显示 toast,开始时为“direnv 正在加载环境…”,结束后提示 “direnv 环境加载完成”或“direnv 环境刷新失败,请检查项目环境配置”;1 秒内完成的 快速刷新保持安静。提示按当前 location(目录,必要时比较 workspace id)过滤,其它 项目或 workspace 的刷新不会串台。它只是旁路通知:发送失败、被拒绝或排队都不会改变 环境刷新/缓存/超时/取消语义,也不显示 env 或原始 stderr。刷新失败仍沿用既有 fallback 行为,因此该提示不承诺中止 shell。修改 plugin/resources 后需要重新构建并重启 OpenCode;新建 chat session 不等于重启。

goal plugin 由 fork 已发布的 v2 分支提供(flake input goal-plugin-v2, URL 带 ?ref=v2):nix/goal-plugin-v2.nix 复用 nix/goal-plugin-package-lock.json 里的 server 依赖锁闭包(effect/zod),离线构建 server.mjs、tui.mjs、rpc.mjs 三个目录入口。 server.mjs 是自包含 bundle(仅 node 内建模块 external);tui.mjs 打包本地 模块但保留 solid-js、@opentui/solid 为 external,由宿主提供唯一的 reactive runtime;rpc.mjs 是 server 与 TUI 共享的 native RPC 定义。全部 TUI 引用在构建期 解析,运行时仍不允许 npm 安装或联网。V2 把 goal 创建和 /goal 入口交给 TUI 控制 面板,server 只注册收窄后的 goal tools(无 create_goal/clear_goal)和 native RPC,不再注册 goal command。已发布的 v2 分支还带有美化后的金色 Goal 面板(状态按结果着色)与 extend control:面板的 Extend only/Extend & resume 增减自动继续额度或随后重查预算,同样只走 native RPC,不经过模型。 面板只对 unmet 增加 Resume goal 行:显式恢复会重新检查全部预算,保留记录 id、objective、history、已用 tokens/time/auto-continue,清除 closedAt 与 blocker, 并清理关闭遗留的执行状态;complete/cancelled 仍是只读交付卡片,通用 closed 状态转换(例如 pause/edit)也不会因此开放恢复。 goal 目录只列在 v2/opencode.json 的 server plugins 中,TUI 进程通过 server inventory 发现其 tui 入口,因此不再 重复列到 v2/cli.json。 启动器在运行时设置 OPENCODE_GOAL_STATE_PATH,默认值为所选 workspace 下的 .harness/goals.json:cwd 启动时是 $PWD/.harness/goals.json, --workspace DIR 时是 DIR/.harness/goals.json。workspace 已作为可写目录挂载, 因此该状态文件可写;启动器不做 Git 根发现,路径只取决于 workspace 选择,外部项目 无需是 Git 仓库。使用默认路径时,启动器按需创建 .harness 并写入内部 .gitignore (内容为 *,已存在则保留),goal 插件再在写入时创建缺失的父目录。 可通过运行环境显式设置 OPENCODE_GOAL_STATE_PATH 覆盖,非空值原样保留,空值回退到 上述默认路径;非空覆盖时 goal 不再创建 .harness(Kuruwa 沙盒策略/任务配置仍可能按需创建)。 覆盖不会增加沙箱挂载:路径必须位于已有可写挂载内。旧版全局状态 ${XDG_DATA_HOME:-$HOME/.local/share}/opencode/goal/goals.json 和旧默认 workspace/.opencode/goals.json 保留在原处,不自动迁移或删除;迁移说明见 docs/harness-migration.md。当前固定的上游版本仅在进程内串行化 状态写入;多个 gjz010/harness 进程同时修改同一 goals.json 可能丢失更新。并行使用时 请为各进程设置不同的状态文件,或避免同时写入;原子文件替换并不能代替跨进程锁。 状态文件路径在启动时确定,修改后需要重启 OpenCode。 默认状态目录自带 .harness/.gitignore(*),把其中的 Kuruwa/goal 状态排除在 Git 之外;已存在的 .gitignore 不会被重写。.harness/config.toml 是否纳入版本控制由项目 自行决定,启动器不管理该文件。不要忽略整个 .opencode 目录,以免掩盖其他需要跟踪的配置。

V2 入口还提供一个可选的 ntfy 通知插件(server plugin gjz010-harness.ntfy,未设置 GJZ010_HARNESS_NTFY_URL 时完全关闭):启用后自动通知带有会话 title、最后回复、表单/ 权限/错误/goal 详情,并注册一个 agent 工具 send_ntfy 供主动进度或人工接手使用。 启用方式、私有 env、脱敏与出本机边界见 docs/notifications.md。

TUI 配置由只读 resources 的 v2/cli.json 提供,启动器经 OPENCODE_CLI_CONFIG_CONTENT 注入;它列出 TPS 与 harness-version 两个 TUI 目录,goal TUI 由 V2 server inventory 关联发现,因此不重复注册。仓库不再打包单独的 legacy config/tui.json。

TPS Meter 与 goal-plugin 一样由固定的 Git 源码在 Nix 内构建,不依赖运行时 npm 下载:flake.nix 中的 non-flake input tps-meter 指向 https://git.gjz010.com/gjz010-harness/opencode-tps-meter.git(上游 v0.4.0 的 fork,包含修复 TUI 仪表盘按 Escape 返回的 commit 05415ddb6b58bab1f437d311bb83588972d959b8),flake.lock 以 narHash 锁定该 revision。nix/tps-meter.nix 先通过固定输出派生按上游 bun.lock 执行一次 bun install --frozen-lockfile (依赖树的 sha256 写在文件中),再在无网络阶段用 Bun 运行上游 build.ts 生成 dist/。Nix build 时把 dist/tui.mjs 复制到只读 resources 的 plugins/opencode-tps-meter/,把 dist/tui.mjs 与 dist/v2/server.mjs 复制到 v2/tui-plugins/opencode-tps-meter/;OpenCode 启动时直接本地加载,不需要网络。 上游构建把 solid-js、@opentui/solid、@opencode-ai/plugin 等保留为 external import,因此宿主提供唯一的 reactive runtime,不打包第二套副本。

input URL 不写死 rev,just update-tps-meter 会把 fork 默认分支(main)的最新 revision 写入 flake.lock,因此升级无需手改 URL。若新 revision 改变了上游 bun.lock,固定输出的依赖 hash 也会变化,需按 Nix 报出的新 hash 更新 nix/tps-meter.nix。修改后需重新构建/运行新包,并重启 OpenCode 才会生效。

update-gjz010-harness skill

V1 的 /update-gjz010-harness slash command 随 V1 入口一并移除。需要更新本项目锁定的 gjz010/harness 时,直接使用 update-gjz010-harness skill。

OpenCode 配置变化后重启 OpenCode:包括 config、skills、instructions 路径、 MCP、agents、commands、plugin 和包版本。启动时 source 的 gjz010/harness env,以及 provider/config substitution/MCP 使用的进程环境变化,也需要重启。仅用于 shell 的目标项目环境在成功 reload 后可由 plugin 注入下一次调用;新建 chat session 不等于重启。历史源码调研保留在 docs/research/,不作为跨版本重载保证。

模型元数据

打包的 resources/opencode.json 自动包含 provider.newapi-selfhost.models 的完整 16 个模型,含 mimo-v2.6-pro / mimo-v2.6-flash。宿主只需在项目或 global OpenCode 配置里提供 provider.newapi-selfhost 的连接设置与凭据; OpenCode 原生 deep merge 按字段合并,会保留宿主的 options.baseURL、 apiKey、provider 级 npm 和自定义模型,再把它们与打包的模型列表并集。 同名字段按 global($XDG_CONFIG_HOME/opencode/opencode.json)→ 打包 OPENCODE_CONFIG → 项目 opencode.json 的优先级递增,项目只覆盖它显式写出的 字段,其余字段保留低优先层的值。打包产物只增加 provider.newapi-selfhost.models, 不含连接地址、认证或 provider 级 npm。模型来源和 overrides 以 config/newapi-models.json 为准。

构建使用提交在仓库里的离线快照 config/models-dev-snapshot.json:它取自 https://models.dev/api.json(旧条目获取日期 2026-09-22;gpt-6.1-sol 于 2026-10-08 单独获取并插入,gpt-6.1-terra 于 2026-10-28 单独获取并插入,gemini-4-argon 于 2026-10-14 单独获取并插入,废止的 gpt-6-luna 与 gpt-6-sol 条目随旧模型一并删除,均未整份刷新),只保存清单引用的来源, 且每个模型都经过 project() 白名单,不含 id、api、baseURL、headers、 options、env、npm 等传输字段。构建期 nix/package.nix 运行 scripts/model-snippet.py generate --manifest config/newapi-models.json --catalog config/models-dev-snapshot.json,再用 jq deep merge 进编译后的 opencode.json。构建和启动都不访问 models.dev。

更新快照(先确认 models.dev 仍有清单中的全部来源,审查 diff 后提交):

curl -fsSL https://models.dev/api.json -o /tmp/models-dev.json
python3 - <<'PY' > config/models-dev-snapshot.json
import importlib.util, json
from pathlib import Path
spec = importlib.util.spec_from_file_location("models", "scripts/model-snippet.py")
models = importlib.util.module_from_spec(spec)
spec.loader.exec_module(models)
catalog = json.loads(Path("/tmp/models-dev.json").read_text())
manifest = json.loads(Path("config/newapi-models.json").read_text())
snapshot = {}
for entry in manifest["models"].values():
    provider, model_id = entry["source"].split("/", 1)
    snapshot.setdefault(provider, {}).setdefault("models", {})[model_id] = models.project(
        catalog[provider]["models"][model_id])
print(json.dumps(
    {p: {"models": {m: snapshot[p]["models"][m] for m in sorted(snapshot[p]["models"])}}
     for p in sorted(snapshot)}, indent=2, ensure_ascii=False))
PY

just check 校验该 JSON;just test 实际从快照生成并核对 16 个模型且快照无传输 字段。打包 agent 的 newapi-selfhost/... 模型必须全部存在于生成结果中,没有例外; 错拼或其他未收录 ID 仍会失败。修改快照、清单或 nix/package.nix 后必须重新构建并 退出、完全重启 OpenCode;新建 chat session 不等于重启。

gjz010-harness-models 仍可从 models.dev 读取元数据,或用显式快照离线生成 snippet:

# 使用包内的清单;也可用 nix run .#model-snippet -- generate
gjz010-harness-models generate > /tmp/newapi-models.json

# 仓库开发:使用当前源码清单;--catalog 可指定 models.dev API JSON 快照以离线生成
just model-snippet --catalog /path/to/models-dev.json

# 使用自己的来源映射,从 OpenCode JSON/JSONC 中提取模型列表和当前差异
gjz010-harness-models extract --config ~/.config/opencode/opencode.jsonc \
  --sources /path/to/sources.json > /tmp/model-manifest.json

sources.json 是网关模型名到 models.dev provider/model-id 的映射,例如 {"qwen-3.8-27b":"cerebras/qwen-3.8-27b"}。它必须覆盖输入配置中的所有模型。 生成时可用 --manifest 或 GJZ010_HARNESS_MODEL_MANIFEST 指定其他清单。网络只在 工具运行时使用,Nix 构建不读取宿主配置,也不抓取实时模型目录。

当前清单包含 16 个模型:11 个从本机 newapi-selfhost.models 提取,另有 2 个 MiMo 模型、1 个 GPT 6.1 Sol 模型、1 个 GPT 6.1 Terra 模型和 1 个 Gemini 4 Argon 模型 映射到 models.dev。提取的条目仅记录本地已有字段相对 models.dev 的差异。相同字段跟随目录更新;差异无法自动判断是有意修正还是 旧数据,因此全部保留供人工审核。覆盖对象递归合并,数组整体替换,null 删除字段。 支持能力、价格、限制、模态、状态及显式 provider.npm;不复制 catalog 的 id 或连接配置。未知模型来源、无映射或不支持的 override 字段会报错,不猜测替代模型。

  • Qwen 使用 Cerebras qwen-3.8-27b,保留 context 131072 / output 16384。
  • GPT 的网关 -codex 名称映射到对应 OpenAI 元数据,保留本机名称、限制、价格和 SDK 修正。
  • GPT 5.6 的 Sol/Luna/Terra 额外用 limit.input: null 去除上游 922000 输入限制,避免超过本地 262144 上下文;重新 extract 后应复核这一项。
  • GPT 6.1 Sol 的 gpt-6.1-sol-codex 覆盖显示名、provider.npm 和与 Astra Codex 相同的截断限制 context 272000 / input 258400 / output 32767;family、能力和价格采用上游 openai/gpt-6.1-sol,不复制 GPT 5.6 的本地价格覆盖。
  • GPT 6.1 Terra 的 gpt-6.1-terra-codex 采用同样的处理:只覆盖显示名、provider.npm 和与 Astra Codex 相同的截断限制 context 272000 / input 258400 / output 32767;family(gpt-terra)、能力、价格(含 context_over_200k 嵌套)和 2026-10-26 发布日期都来自上游 openai/gpt-6.1-terra,不复制 GPT 5.6 Terra 的 limit.input: null 处理。
  • DeepSeek V4.1 Flash 在当前 catalog 的 DeepSeek provider 中没有精确条目,显式选用 openrouter/deepseek/deepseek-v4.1-flash;可在清单中更换来源。
  • MiMo 直接采用 models.dev opencode-go 的 mimo-v2.6-flash 与 mimo-v2.6-pro,无本地覆盖;生成器会去除上游 transport 字段。
  • Gemini 系列(gemini-3.6-flash、gemini-3.8-flash、gemini-3.1-pro-preview)显式覆盖 provider.npm: @ai-sdk/google,走 Gemini 协议;便宜的 Argon 模型使用上游调用名 gemini-4-argon-ag,采用 models.dev google/gemini-4-argon 元数据(2026-10-13 发布),并覆盖 provider.npm: @ai-sdk/openai-compatible,走 OpenAI 兼容协议。

Runtime modes

默认模式为 normal(普通);Tab 在 expert(专家)、pro(专业)、 tokenmaxxing(TOKENMAXXING)、normal(普通)、fast(极速)、simple(简洁) 六种 primary 间切换。默认的 OC2 交互入口不接受 --agent,启动时使用生效的 default_agent(打包默认 normal,项目配置可覆盖)。 harness install 是例外:它按次通过项目 daemon 原生新建 fast 会话,不改持久配置、 不写配置文件。Build/Plan 和旧的通用 agents 已禁用; 模式不是自动执行的 worker 流水线。

模式 Primary 模型 可用 workers
专家 expert Astra explore-sol, explore-expert, code-sol, code-astra, review-fix-astra, computer-use-astra, computer-use-terra
专业 pro Astra explore-deepseek, explore-expert, code-deepseek, code-astra, review-deepseek, review-astra, computer-use-astra, computer-use-terra
TOKENMAXXING tokenmaxxing Astra explore-expert, code-astra, review-astra, computer-use-astra, computer-use-terra
普通 normal(默认) Astra explore-deepseek, code-deepseek, review-fix-astra, computer-use-astra, computer-use-terra
极速 fast DeepSeek explore-deepseek, code-deepseek, review-deepseek
简洁 simple 未指定(继承运行时默认) 无(task: deny 且 external_worker: deny,禁止全部委派)

agents/ 维护 6 个 primary 和可发现的 workers。Nix 将 Markdown frontmatter 与正文编译进 OpenCode 配置;primary 只组合公共编排职责和本模式路由规则。 统一 subagent-routing skill 已移除。原生 task 权限同时过滤工具描述中的 worker 列表并拒绝不允许的目标;所有 worker 禁止递归 task。

pro 由 Astra 掌握需求、架构与最终验收:常规探索和边界清晰的实现交给 explore-deepseek/code-deepseek;需要综合交互不变量的探索交给 explore-expert; 架构、复杂调试或交互不变量主导的实现交给 code-astra。普通低风险 review 交给 review-deepseek,复杂或高风险 review 交给 review-astra,两者按实际风险独立选择。 worker 的概念性阻塞返回主控,由主控重新评估契约并选择合适的 worker,而不是重复同一 条失败路径。

tokenmaxxing 的主模型以及默认 explore、coding、review、computer-use 四类 worker 都使用 Astra:常规与复杂探索都交给 explore-expert,所有代码实现交给 code-astra,需要独立 审查时按实际风险按需调用 review-astra,不把它当作每次改动的固定流水线。显式 Terra worker computer-use-terra 是唯一例外,只在用户明确要求 Terra 时使用。这些 worker 的说明把自己限定在复杂任务并建议常规任务交给更便宜的模型,本模式不采用该成本指导: 常规与复杂任务都路由到同一批默认 Astra worker,没有按难度切换到其他档位的路径。

expert 同样由 Astra 掌握需求、架构与最终验收:常规探索和边界清晰的实现交给 explore-sol/code-sol;困难探索和涉及交互不变量、模糊架构的实现交给 explore-expert/code-astra。按推理难度而非代码量选择档位;Sol 的概念性阻塞返回 主控,由主控决定是否升级到 Astra,而不是重复盲试。该模式按需使用 review-fix-astra 进行独立审查与修复,它不是每次 patch 的必经流程。

simple 是单代理直接执行模式:不预设 model,使用运行时生效的默认模型; permission.task 为标量 deny,禁用全部子代理(含项目新增的子代理),因此不委派, 直接探索、实现和检查。它与其他 primary 的差异在于:其他 primary 编译出精确 deny/allow 字典,未列入字典的项目新增任务名不会被这段配置拦截;simple 的标量 deny 则拒绝所有 task 目标。两者都仍受项目/global 原生权限合并约束,不是不可覆盖 的隔离边界。

computer-use-astra 是交互式应用闭环的专职 worker,只在 normal/expert/pro/tokenmaxxing 可用, fast 对其精确 deny,simple 则禁止全部子代理。computer-use-terra 是同一角色在 newapi-selfhost/gpt-6.1-terra-codex 上的副本,正文、权限与可见性和 computer-use-astra 完全相同, 并在同样的四个 primary 中 allowlist;默认仍用 Astra,只有用户明确要求 Terra 时才用它, 它不是 Astra 失败后的替代档位,调用时的契约、证据标准与边界完全一致。仅当任务契约明确目标应用、 需要现场观察的状态或交互反馈, 以及无法靠源码、编译或确定性测试确认的验收项时才调用;任务难、其他 worker 失败、 想要更强推理或仅存在 MCP 都不构成理由,也不能把它当作代码执行的兜底或绕过当前模式 的代码执行层级。

写作路由由 /writing 开关选择,五个选项:antigravity(默认,官方 Antigravity ACP 执行端默认模型)、writing-deepseek(原生 writer newapi-selfhost/deepseek-v4.1-flash)、writing-gemini-3.8-flash(原生 writer newapi-selfhost/gemini-3.8-flash)、writing-gemini-4-argon(原生 writer newapi-selfhost/gemini-4-argon-ag)、disabled(不委派,primary 直接写作)。该选择是宿主范围的全局值,由同一用户、同一宿主配置根下的所有 harness 项目共享,不是按 Location、也不是按项目;作用域、生效时机与切换成本见 写作路由。

主模型只看到同一个 writing 工具,写作后端由 /writing 开关决定;模型无需选择 executor/model/agent,也无需了解后端。五个选项(含 disabled)都保持固定的 system 写作指令与工具 schema/description,从而减少切换对提供方提示词前缀缓存的扰动(只减少扰动,不保证真实命中率)。taskID 对模型不透明:省略即新建任务,原样传回即续接同一任务;其内部记录 route 与 native ID,无新增数据库。writing 每次执行时读取共享选择,切换在下一次 writing 调用生效(含同一用户请求内后续新建任务),已有续接固定其原后端。disabled 拒绝新建与续接并让主模型直接写,不取消正在运行的任务。background: true 沿用宿主自带的后台通知;仅在已有 active goal 且无独立工作时才用 wait_goal 的 kind="writing" 与 taskID 等待(由插件翻译后交给原 goal 校验),没有 active goal 时仅依赖自动通知、不调用等待工具。

writing 是打包的自然语言写作角色:按给定的受众、意图、论点、口吻、格式和来源撰写、 改写、翻译、总结或转写人类或模型要阅读的文本,涵盖用户文案、文档以及 LLM prompt/instruction;保留事实与代码标识符,缺失来源时明确反馈而不是编造,并在契约要求 时直接在指定文档内改写。写作与用户要求的审阅分开,不擅自弱化、拓宽或加限定,也不扩大 为代码实现。五个可委派的 primary(normal/fast/expert/pro/tokenmaxxing)携带完整 writing 标记段,经该 writing 工具进入统一入口;simple 与其它非受管代理不携带标记段, 不会因此获得写作委派权限。native writing 仍不被这些 primary allowlist,编译器对其生成 exact task: deny,因此原生工具列表不列它;具体 writer 后端名 (writing-deepseek/writing-gemini-3.8-flash/writing-gemini-4-argon)也不必进入模型可用代理列表。通用 subagent 与 external_worker 工具仍服务其它用途,不因写作入口统一而改变。角色保留 edit 与 bash 允许、native subagent 委派(task)拒绝。simple 以 task: deny 与 external_worker: deny 禁止全部委派。打包 model: newapi-selfhost/deepseek-v4.1-flash 仅在项目显式重新启用 native writing 时使用,统一写作入口不读取它。fast 的既有 ceiling 只约束代码、探索与审阅,写作路由是该模式的唯一例外,其代码档位边界不变。

Primary 在模式允许委派时默认委派 worker,只有判断某次分派明显不必要(例如无设计 歧义的一行或单文件改动,委派成本明显高于收益)时才直接编辑;非平凡、有风险、 多文件、涉及架构或需要独立验证的工作仍交给 worker。simple 禁用全部子代理, 不委派,直接完成探索、实现与验证。bash: allow 不是文件系统只读保证,也没有新增 沙箱。打包 agents 只带 agent-from-gjz010-harness 来源标记,它不是所有权或不可覆盖 保证;agent 配置按原生 deep merge,同名项目 agent、default_agent、disable 与 permission 可覆盖打包默认,未知项目 agent 也不会被这段配置拦截,仍受项目/global/ native 权限约束。用户控制的其他插件、指令、显式 API/CLI 调用不是此路由策略的安全 边界。

若项目复用某个受限 worker 的名字,只改该 agent 定义不会自动改掉调用方 primary 上 编译出的 task: deny;需要在调用方项目的 permission.task 里显式 allow,例如 {"agent":{"normal":{"permission":{"task":{"my-worker":"allow"}}}}}。请使用精确规则, 不要用全局 "*": "allow" 取代它。

模型使用 newapi-selfhost/...;包的离线快照自动加载全部 16 个模型元数据, provider 连接和凭据仍由本机 OpenCode 配置提供。包加载成功不代表远端模型可用。 expert 的 Sol worker 引用 newapi-selfhost/gpt-6.1-sol-codex;computer-use-terra 引用 newapi-selfhost/gpt-6.1-terra-codex。两者都收录在离线清单与快照中,元数据分别来自 models.dev 的 openai/gpt-6.1-sol 与 openai/gpt-6.1-terra,各自覆盖显示名、SDK 和与 Astra Codex 相同的截断限制。 tests/models.py 要求每个被引用的打包模型都有离线元数据,没有例外;错拼或其他 未收录 ID 仍会失败。定义或插件更改后重新构建并退出、重启 OpenCode;新建聊天 不会重载配置。实现证据、输入修正和限制见 runtime modes。

沙箱边界

Astra 模型提示词与 harness 环境提示

oc2 入口的 plugins/v2/astra-scope.ts 使用 ctx.session.hook("context", ...),模型范围文本 改由配置的 ctx.options.rules 提供:它先在模型过滤之前把共享的 prompts/harness-environment.md 追加给所有模型(与规则无关),再对每条规则用本地 event.model.id 做忽略大小写的子串匹配,命中且 system 中尚无同文本时按规则声明顺序追加 text.trim()。规则不筛 provider/agent/variant。打包默认的标记是 gpt-6-astra、 gpt-6.1-sol 与 gpt-6.1-terra,凡是本地 id 子串命中这些标记就注入: gpt-6-astra-codex 与 gpt-6.1-sol-codex 的 default/-fast/-ultrafast 别名命中, gpt-6.1-terra-codex 当前只有 default 本地 ID(未定义 tier 别名)也命中;其他模型 (含旧 gpt-6-sol)不注入。范围 只匹配 event.model.id,不像旧入口那样同时匹配 API ID 或显示名。这一步是必须的,因为 pinned v2 只保留 config.instructions 字段而不加载它, 所以 config/instructions.md 不会注入 v2 会话;v2 仅通过该 hook 携带这条简短 harness 环境 提示,并没有新增插件 id 或注册目录。插件规则用原生 {package, options} 条目声明,正文的 {file:...} 由 v2 原生配置层在插件 初始化前展开,路径相对该配置文件所在目录解析,指向的文件必须存在。项目 opencode.json 的 plugins 数组示例:

{
  "plugins": [
    {
      "package": "{env:GJZ010_HARNESS_RESOURCES}/v2/plugins/astra-scope",
      "options": {
        "rules": [
          { "modelIncludes": ["gpt-6-astra", "gpt-6.1-sol"], "text": "{file:./prompts/astra-scope.md}" }
        ]
      }
    }
  ]
}

打包构建(nix/package.nix)用同一机制,只是 {file:...} 指向 Nix store 中的 prompts/astra-scope.md,构建期不把正文内联进 JSON。context hook 收到的 event.model.id 是 tier model.hook("resolve") 解析后的目标本地 ID(default/-fast/-ultrafast 别名解析后 仍是含标记的本地名),匹配只针对它,不筛 provider/agent/variant。

options.rules 缺失或为 [] 表示不追加任何模型范围文本,但 harness-environment 仍会 注入;显式无效的 rules、条目、空匹配串或空文本会让插件初始化直接报错。环境提示与 scope 提示都在插件初始化时读取/展开,资源更新后必须显式重启项目 daemon,新建聊天不会重载。 这是提示词约束而不是权限隔离;该 context hook 升级 OpenCode 后应复验。

gjz010/harness 公共 OpenCode 配置使用 {"permission":{"*":"allow","external_directory":"allow"}}, 默认不弹出工具权限确认。subagent 显式的 task: deny / edit: deny 仍保留; 这不改变下述 bubblewrap 挂载限制,也不改写宿主 OpenCode 配置。

  • 保留网络,根文件系统只读,HOME 被遮蔽,显式 workspace 可读写。
  • 新默认使用每次启动独立的 tmpfs /tmp,根目录在本次启动内可写,退出即随 tmpfs 丢弃,不继承宿主 /tmp 或上次启动。workspace/.harness/tmp/ 以可写方式绑定到 /tmp/persist,并显式设置 TMPDIR=/tmp/persist;只有这个共享 scratch 子树跨启动 保留,不自动清理。宿主 /tmp 保持隐藏,位于 /tmp 下的 workspace 由后续绑定重新 暴露。 /tmp/harness-runtime 是独立的可写 per-launch tmpfs(0700),不随 scratch 持久化。 <workspace>/.harness/tmp/opencode/ 另以可写方式绑定为 /tmp/opencode,与 /tmp/persist/opencode 同源,供硬编码该路径的工具共用同一份持久 scratch; TMPDIR 仍为 /tmp/persist,/tmp 根可写但每次启动独立。该绑定由动态打包 hook 追加,因此不改写项目 TOML,但换包后需要显式重启项目服务(桌面按正常流程重开)才 生效。 可识别的旧受管策略在启动时自动迁移,无需手改 TOML;生效边界见 Hakoniwa 临时目录迁移。
  • XDG_RUNTIME_DIR 指向沙箱内私有临时目录,供 devenv 等工具创建运行时文件, 不把宿主 /run/user 改为可写。
  • 当调用者进程可见的 XDG_RUNTIME_DIR 与 WAYLAND_DISPLAY 能解析出宿主上真实的 Unix socket 时,启动器只把这个单个 socket 绑定到沙箱私有 /tmp/harness-runtime/<basename>,并把沙箱内 WAYLAND_DISPLAY 指向该 basename, 以便 TUI 的剪贴板工具(wl-paste/wl-copy)连接;XDG_RUNTIME_DIR 仍为沙箱私有, 不绑定整个 /run/user,也不暴露宿主 runtime 目录下的 lock、xauth 等其它文件。 仅当该宿主 socket 可解析时才会追加这些参数,否则保持旧行为。检测依赖调用者进程 可见的环境变量,因此在 harness 沙箱内部再次启动 harness(嵌套启动)时,调用者看到的是 已被改写的 /tmp/harness-runtime,转发不会生效;这是不报错也不接入的安全 fail-safe, 嵌套场景下图片粘贴仍不可用。
  • 宿主 OpenCode config 可写;入口使用 workspace 内的项目 state root (.harness/opencode/)保存 data/state,宿主 OpenCode 数据根(data/state)不再挂入。 宿主缓存的共享是 Harness 的启动策略:宿主 → 沙箱默认使用宿主解析后的 XDG_CACHE_HOME(未设置或为空时 $HOME/.cache),以相同绝对路径读写挂载,包括其中 工具自身的缓存;HOME 仍被遮蔽,cache 不得等于或包含 HOME,也不得与 config、data、 state、GPG/SSH、私有 runtime 等受保护目录重叠,启动器在进入 bwrap 前拒绝并退出。旧项目 .harness/opencode/cache 不迁移、不删除;换包后需显式重启项目 daemon 才生效。 作为独立映射,宿主 direnv 授权目录以只读方式挂到项目 XDG_DATA_HOME/direnv(见上文 direnv 说明)。项目唯一 manager 位于 workspace/.harness/kuruwa/daemon/,沙盒策略位于 .harness/kuruwa/config.toml。 agent 通过租户入口在现有 harness-sandbox instance 运行任务。详见 docs/project-daemon.md 与 kuruwa skill。
  • 默认数据库是 opencode-v2.db,显式 OPENCODE_DB 同样优先,但必须落在项目 state root 内。历史的 opencode-stable.db(旧 V1 数据库)保留原处,不删除、不迁移, 当前入口不会触碰它。
  • 运行时不再生成、发现或校验 muxrun 配置;显式 MUXRUN_CONFIG 与祖先 .muxrun.toml 被忽略并在沙箱内 unset。旧文件保留原处,不自动迁移或删除。
  • Kuruwa 的沙盒策略根固定为 workspace/.harness/kuruwa/,manager 根为 .harness/kuruwa/daemon/。launcher 拒绝这些路径上的 symlink 逃逸;socket 长路径由 上游原生 /proc/self/fd 机制处理,harness 不添加 hash、短路径 symlink 或人为长度拒绝。
  • GPG 使用 extra socket 和公钥材料;SSH 使用 agent socket 以及只读 config、 known_hosts。宿主的 gh CLI 状态目录 $XDG_CONFIG_HOME/gh(默认 ~/.config/gh, 其中的 hosts.yml 可能含 OAuth token)以只读方式回绑,因此沙箱内 gh 可直接复用 宿主已登录的 GitHub 账号,但无法改写或覆盖该凭据。启动器不绑定 GPG/SSH 私钥文件。
  • 系统 SSH 配置保持启用(不禁用 /etc/ssh,也不把 GIT_SSH_COMMAND 改成 -F /dev/null)。根只读绑定会让 Include 指向的 Nix store 文件在沙箱内保持 商店的合成属主(nobody:nogroup),OpenSSH 会因此报 Bad owner or permissions 并直接退出。启动器读取 /etc/ssh/ssh_config,枚举其递归 Include 引用的现有文件(含嵌套、glob、相对 /etc/ssh 路径、条件块下的 Include, 不执行任何 Match exec),把每个 Include 的真实目标重新绑定为调用用户属主的只读副本 (--perms 0444 --ro-bind-data)。路径和内容不变,不写宿主、不改变 UID 映射, 遇到 group/world-writable Include 或读取失败则终止启动。系统入口本身不复制: OpenSSH 只对 Include 目标启用该权限检查。默认 Git 与普通 SSH 都按同一套 正常配置加载;显式传入的 GIT_SSH_COMMAND 原样保留;~/.ssh/config 与 known_hosts 的只读挂载不变。
  • 该 SSH 属主副本只在启动器构建 bwrap 参数时生成,需重启启动器(新会话)才生效。 真实沙箱验证入口:just smoke-ssh-config [PACKAGE](离线、无网络、不发起 OpenCode 模型请求)。
  • 宿主 cache 共享的真实沙箱验证入口:just smoke-cache [PACKAGE](离线、无网络, 使用一次性 HOME 和一次性缓存目录,不读写真实用户缓存)。
  • 拒绝 /、HOME、会暴露 HOME 的上层目录以及敏感/运行时目录作为 workspace。 选择项目目录,而不是扩大到整个 HOME 来绕过限制。
  • Git worktree 的 common dir 若位于 workspace 外会明确失败。使用同时包含 worktree 与 common dir 的安全共同上层目录作为 --workspace;启动器不会默默 增加额外挂载。若共同上层是 HOME 等被拒绝目录,请先调整项目布局。

这不是针对恶意代码的密闭沙箱。网络、认证 sockets、Nix daemon、可读的宿主根目录 和持久 OpenCode 配置仍提供宿主能力;socket 可代为认证/签名,即使没有暴露私钥。 只读根也不等于隐藏所有宿主数据。只对可信项目和工具使用这些权限。

开发与维护

nix develop
# 或在审查 .envrc 后执行 direnv allow .
just check
Recipe 用途
just check 静态检查和 tests
just test 少量离线合同测试,不启动 OpenCode、bubblewrap 或认证服务
just build nix build .
just check-flake nix flake check
just update-matt-skills nix flake update matt-skills,仅更新指定 input
just update-no-negative-echo nix flake update no-negative-echo,仅更新指定 input
just update-kuruwa nix flake update kuruwa,仅更新指定 input
just update-muxrun nix flake update muxrun,仅更新指定 input(兼容 pin)
just update-mcp-harbor nix flake update mcp-harbor,仅更新指定 input
just update-goal-plugin nix flake update goal-plugin-v2(兼容名;现在更新唯一的 V2 goal input)
just update-goal-plugin-v2 nix flake update goal-plugin-v2,仅更新指定 input
just gjz010-harness-commit "message" 显式请求提交时使用的兼容签名提交 wrapper(新会话已自动签名)
just opencode 本仓库启动入口

Matt skills 初始迁移 pin 为 74ca5fe077456a0b3b2f5310cf9430999fd0b5fd, 当前 goal fork(goal-plugin-v2)为 a70c6fdeb7810dec7ddec44929a4cf13baa38fdc;no-negative-echo 固定为 eba9f1d2b4c19e699786a49427189988ad6d8d65;mcp-harbor 固定为 9a251497488d5411a4b788fd7074545468a461f3;muxrun pin 保持不变。 后续以 flake.lock 为准。旧 sync/restore recipes 和 skills-lock.json 已移除; 普通构建直接使用 Nix inputs,无需恢复 checkout 内副本。 更新后审查 lock 和上游内容,运行 just check、just build、just check-flake。 goal 上游变更可能需要手工更新 nix/goal-plugin-package-lock.json 和 nix/goal-plugin-v2.nix 中的 npmDepsHash;两个 update recipe(update-goal-plugin、update-goal-plugin-v2)都不会自动更新它们。 重新构建并重启 OpenCode 才会加载新资源。

架构与 agent 检查规则见 AGENTS.md;skills 维护见 skills README。任务 scratch 使用 workspace/.harness/tmp/<task>/, 位于 workspace 内、被内部忽略文件覆盖;启动器把 scratch 根 workspace/.harness/tmp/ 以可写方式绑定为沙箱 /tmp/persist,任务目录因此表现为 /tmp/persist/<task>/; 硬编码的 /tmp/opencode 也指向同一 scratch 下的 opencode/。优先使用 $TMPDIR, 不要假定 /tmp 根可写。scratch 跨启动保留、不自动清理,宿主 /tmp 仍隐藏;已运行的沙箱仍使用其原有布局,自动迁移配置后需正常重启服务。

自动化检查只覆盖关键逻辑,不代表真实沙箱、认证或 OpenCode 集成已经通过。 修改启动器或资源时,应按改动范围在临时 workspace/HOME 中手工做 smoke test。

English

An opinionated, sandboxed OpenCode tool for gjz010, packaged with flake-parts for x86_64-linux. Run locally with nix run ., or add its package to an existing flake/devenv environment. Host authentication and persistent OpenCode state are retained; this is not a hermetic security boundary against malicious code.