- Python 50.8%
- JavaScript 29%
- TypeScript 7.9%
- Rust 7.4%
- Nix 2.3%
- Other 2.6%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| agents | ||
| benchmarks | ||
| config | ||
| docs | ||
| nix | ||
| plugin-support | ||
| plugins/v2 | ||
| prompts | ||
| scripts | ||
| skills | ||
| tests | ||
| ui/hub | ||
| .envrc | ||
| .gitignore | ||
| .muxrun.toml | ||
| AGENTS.md | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CONTEXT.md | ||
| flake.lock | ||
| flake.nix | ||
| justfile | ||
| README.md | ||
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 --serviceargv 和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-commitrecipe 保持兼容。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 按以下顺序执行,列表在开始执行前就已确定:
- 运行环境变量
GJZ010_HARNESS_SANDBOX_HOOK(也可写进上面的 env 文件)。非空时解析为 可读普通文件(realpath -e,相对路径相对启动 cwd 而非--workspace),source 一次。未设置或为空表示不使用它。 - 所选 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,保留 context131072/ output16384。 - 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.devgoogle/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-sandboxinstance 运行任务。详见 docs/project-daemon.md 与kuruwaskill。 - 默认数据库是
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。宿主的
ghCLI 状态目录$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.