本页目录
  1. 1. 固定输入
  2. 2. 构建与运行
  3. 3. 包内布局
  4. 4. 补丁版 RMUX
  5. 5. 安装配置与示例
  6. bwrap 内的命令路径
  7. 6. 包外真实烟测
  8. 7. 限制
  9. 8. 验收记录
  10. 8.1 flake 与构建
  11. 8.2 Nix 测试
  12. 8.3 nix run
  13. 8.4 包外烟测
  14. 8.5 未解决项与边界

Nix 打包与可复现安装

本文说明 Kuruwa 的 Nix flake、包内布局、安装配置和真实打包烟测。范围只有一个 x86_64 Linux 输出包:产品 kuruwa、补丁版 RMUX 0.8.0、示例定义和安装配置。 不含 overlay、NixOS module、开发 shell 或 apps 输出;nix run 依赖包的 meta.mainProgram。

1. 固定输入

flake.nix 的两个输入与 devenv.lock 完全一致:

flake input devenv.lock 节点 revision
devenv-nixpkgs nixpkgs cachix/devenv-nixpkgs 760b85220489f69855e6df78a6d1dad5873f8bf6
nixpkgs-src nixpkgs-src NixOS/nixpkgs 8edc0c72e3a38faf5434e40d1f19431125fe4b30

两者都是 flake = false,pkgs 通过 import devenv-nixpkgs { inherit system; } 得到,也就是 devenv 正在使用的滚动包集 及其 overlay。flake.lock 是在本地生成的,没有独立更新任何依赖;它的 narHash 与 devenv.lock 中两个节点的值相同。

Rust 依赖来自仓库 Cargo.lock;项目没有新增 Nix 专属依赖或环境 pin。

2. 构建与运行

在仓库根目录:

nix build .#kuruwa
nix run .#kuruwa -- --help

如果新增的 flake 文件还没有被 Git 跟踪,使用路径 flake,避免为了构建而 git add:

nix build path:$PWD#kuruwa
nix run path:$PWD -- --help

packages.x86_64-linux.kuruwa 与 default 是同一个派生包。构建完成后:

export PATH="$(readlink -f result)/bin:$PATH"   # 仅当前 shell
kuruwa --help

flake 检查:

nix flake show path:$PWD
nix flake check path:$PWD

3. 包内布局

bin/kuruwa                                  # makeWrapper 包装的产品 CLI
share/kuruwa/kuruwa.toml                    # 生成的安装配置(绝对 store 路径)
share/kuruwa/examples/kuruwa.toml           # 原仓库示例,逐字节安装
share/kuruwa/examples/init-host.sh          # host 初始化示例
share/kuruwa/examples/init-bwrap.sh         # bubblewrap 初始化示例
share/kuruwa/examples/kuruwa-sandbox.toml   # 声明式 sandbox 示例配置
share/kuruwa/examples/sandbox-hooks/instance-work-dir.sh  # 上面配置引用的 hook
share/kuruwa/logo.svg                       # 纯图标 logo

bin/kuruwa 由 makeWrapper 包装产品二进制,做四件事:

  • --set-default KURUWA_RMUX_BIN=<patched-rmux>/bin/rmux。用户显式设置的 KURUWA_RMUX_BIN 优先;无效路径会在启动时明确报错,不会回退到包内后端。
  • --set-default KURUWA_BWRAP_BIN=${bubblewrap}/bin/bwrap 与 --set-default KURUWA_BASH_BIN=${bash}/bin/bash:声明式 sandbox 定义用同一个 解析规则固定 bwrap 与 Bash,显式环境变量仍然优先,无效路径直接失败。
  • --prefix PATH 加入包内 bash、coreutils、bubblewrap 和补丁版 rmux, 因此已有配置里的裸 bash、bwrap 等 init 程序名可以直接解析。
  • 不设置其他命令环境、网络或 namespace 变量;实例里运行的命令环境仍由定义和 宿主决定。

rmux 与 rmux-daemon 安装在同一 bin 目录,init-bwrap.sh 会把两者一起暂存 进实例私有目录。

4. 补丁版 RMUX

Kuruwa 的实例启动契约依赖 listener FD 导入,而上游 0.8.0 没有这个入口。nixpkgs 的 rmux 是 0.10.0,且其 long-socket 改动与本项目补丁不兼容,所以包内不引用 pkgs.rmux,而是固定构建:

  • 来源:Helvesec/rmux,commit c4ced0ae6c49781bc7095933e593e8084a62c918 (0.8.0),fetchFromGitHub。
  • 源归档固定输出 hash: sha256-73pSH4wowEWYyKQf1htbB0RnCw3qHe0rENr66eyFnM4=。这是 Nix 对固定源 归档的完整性校验值,不是项目自定义摘要。
  • 补丁:patches/rmux-listener-fd.patch(8 个文件)与 patches/rmux-long-socket-path.patch(长 sun_path 通过 /proc/self/fd 间接寻址,真实 socket 路径不变)。两者不重叠,双向顺序都能应用。
  • 构建:rustPlatform.buildRustPackage,cargoLock.lockFile 指向上游 Cargo.lock,不执行 scripts/build-rmux.sh,也不在构建中联网运行测试脚本。
  • 产品依赖的 crates.io rmux-ipc/rmux-sdk 也必须带同一 long-socket 补丁, 否则产品自己连不上深路径实例。nix/package.nix 用 importCargoLock 得到 baseCargoDeps,在 cargo-vendor-dir 里复制可写 vendor tree、从 rmuxSrc 取 crates/rmux-server 与根 src/(让整份补丁可应用),应用补丁后 写回两个 crate;crate 的 .cargo-checksum.json 原样保留,不手改 Cargo.lock。产品构建因此使用 cargoDeps(离线可用),不再用 cargoLock。
  • 测试:构建阶段显式选择 cargo test --locked -p rmux-ipc --lib --test long_socket_path,覆盖补丁新增的 inherited-listener 单元测试与长路径集成测试。这里选择的是 IPC 库范围,不是关闭检查;上游完整 workspace 套件属于开发验收范围,本次没有把它 当作 Nix 构建检查,也没有据此声称整套测试与 sandbox 不兼容。包外真实行为由 第 6 节烟测和 scripts/test-long-socket.py 验证。

5. 安装配置与示例

share/kuruwa/kuruwa.toml 是包生成的配置,语义与 examples/kuruwa.toml 一致:

  • host:host 初始化;
  • bwrap:bubblewrap 初始化;
  • 租户 alice 获准 bwrap;
  • 租户 bob 没有获准定义。

区别只在于所有 init 程序和脚本都是绝对 store 路径。配置可以复制到任意目录、 任意项目根目录后直接使用,不依赖当前工作目录或仓库 checkout:

pkg="$(nix build .#kuruwa --no-link --print-out-paths)"
mkdir -p ./kuruwa-run
cp "$pkg/share/kuruwa/kuruwa.toml" ./kuruwa.toml
"$pkg/bin/kuruwa" serve --config ./kuruwa.toml --runtime ./kuruwa-run

包内二进制同时嵌入原 examples/init-host.sh 与 examples/init-bwrap.sh 的 字节内容。首次在项目目录执行不带 --config 的 "$pkg/bin/kuruwa" serve 时, 它会在 $PWD/.kuruwa 生成默认 config.toml、init/ 脚本和后台日志; wrapper 提供 bash、bwrap、coreutils 与补丁版 rmux,因此这些相对 init 路径可直接工作。已有配置/脚本不会被覆盖。该默认生成行为与 share/kuruwa 下两个安装配置是两条兼容路径:默认生成适合新项目,显式安装配置适合固定部署。

share/kuruwa/examples/kuruwa.toml 保留原示例的相对路径说明;它不是自动使用的 安装配置。原 examples/init-*.sh 没有被修改,包只是逐字节复制它们;安装阶段 关闭了 shebang 重写,避免 Nix fixup 改写原 #!/bin/bash。

bwrap 内的命令路径

init-bwrap.sh 进入 namespace 后会 --clearenv 并固定 PATH=/opt:/run/current-system/sw/bin:/usr/bin:/bin。包 wrapper 在外层加的 PATH 不会自动带进这个沙盒。因此:

  • 宿主编排程序仍可使用包 wrapper 提供的裸 bash、bwrap、coreutils;
  • bwrap 实例内要运行的命令必须由定义或宿主明确提供,或使用 store 绝对 argv;
  • 纯 Nix 场景下不要假设 bwrap 内能按名字找到 bash、printf、sh。

例如用 store 绝对路径启动一个 bwrap 终端命令:

BASH=/nix/store/...-bash-interactive-5.3p15/bin/bash
COREUTILS=/nix/store/...-coreutils-9.11/bin

kuruwa --socket "$RUNTIME/tenants/alice.sock" run \
  --instance alice-lab --name probe --interval 10 -- \
  "$BASH" -c "$COREUTILS/printf 'INSIDE=%s\\n' \"\${KURUWA_BWRAP:-none}\"; exit 17"

这里没有把 store 路径写进原 examples/init-bwrap.sh,也没有扩大 sandbox 策略; 它只是调用者对自己要执行的命令负责。

6. 包外真实烟测

脚本 scripts/accept-package.py 只消费安装包,不读取 target/debug/kuruwa、 target/rmux 或仓库构建产物:

pkg="$(nix build path:$PWD#kuruwa --no-link --print-out-paths)"
python3 scripts/accept-package.py "$pkg"

脚本在 /tmp/opencode 下创建独立临时目录(可用 KURUWA_SMOKE_BASE 覆盖),从 安装包的 share/kuruwa/kuruwa.toml 复制配置,以显式 --foreground 启动测试 daemon(后台默认行为由 scripts/test-root.py 覆盖);serve 和所有客户端构造 等价于 env -i 的环境:只有 HOME、TMPDIR、PATH=/usr/bin:/bin、 LANG=C.UTF-8,不带 KURUWA_RMUX_BIN。它断言:

  • wrapper 在子进程中实际导出了补丁版 rmux 0.8.0 的绝对路径;
  • host 实例创建、run 字面输出和真实退出码保留;
  • bwrap 实例创建,使用 store bash 与 store coreutils 绝对 argv,得到 KURUWA_BWRAP=1、PWD=/mnt 等真实输出,并确认管理 socket 被 namespace 隐藏;
  • 显式设置无效 KURUWA_RMUX_BIN 时 serve 明确失败,日志包含该路径,且不创建 management socket,证明 wrapper 没有吞掉 override 或回退;
  • shutdown 后 serve exit 0,admin/tenant socket 和实例目录被清理。

失败时保留临时目录并打印位置;成功默认删除自己的临时 runtime 和目录。不会 停止或复用任何其他 Kuruwa/RMUX 服务。

深路径(runtime 内任意 socket 路径超过 108 字节 sun_path)由 scripts/test-long-socket.py 覆盖;它对安装包运行 serve、租户入口、实例创建、 start/status/tail 和 shutdown,并断言 socket 仍位于真实深路径:

pkg="$(nix build .#kuruwa --no-link --print-out-paths)"
KURUWA_BIN="$pkg/bin/kuruwa" python3 scripts/test-long-socket.py

该脚本默认使用 checkout 的 target/debug/kuruwa 时也能验证管理与租户入口,但 实例后端需要自带 long-socket 补丁的 rmux-sdk(发布包满足),普通 cargo build 链接的是 crates.io 未补丁版本,脚本会明确报告这一点。

7. 限制

  • 仅提供 x86_64-linux,与其他已验证平台范围一致;没有 macOS/Windows 输出。
  • 构建需要 Nix flakes 和网络(固定输入和 crates.io 依赖);首次构建会编译 Rust 产品与 RMUX。
  • 构建阶段只运行不需要 namespace 的单元测试。真实 host/bwrap 行为、bwrap namespace 权限和监听 FD 交接由包外烟测覆盖。
  • path:$PWD 在本地包含活动 Unix socket 时会被 Nix 路径拷贝拒绝。本次仓库中的 .harness/opencode/runtime/rmux.sock 就触发了 error: file ... has an unsupported type;这是 Nix 源路径机制的限制,不是包 构建失败。验收因此从排除 .harness/、target/、.devenv/ 等工作树副本的 path: 源构建;不要把正在运行的服务 socket 移走。
  • 包没有新增 LICENSE;许可证仍待项目所有者决定。包内只复制仓库现有品牌资产和 示例,不改变产品源码、环境 pin、RMUX 补丁或构建脚本。
  • bwrap 示例是 mount namespace 弱隔离,不是安全边界;同 UID 进程仍可能通过 /proc、ptrace 或开放 FD 观察。详见 部署与 M5 演练。

8. 验收记录

本节记录本次真实执行结果。构建源是从工作树排除 .git/、.harness/、target/、 .devenv/、.direnv/ 后得到的路径副本;flake 文件尚未被 Git 跟踪,没有执行 git add 或任何提交。

8.1 flake 与构建

nix flake show  path:/tmp/opencode/kuruwa-nix-src   # exit 0
nix flake check path:/tmp/opencode/kuruwa-nix-src   # exit 0
nix build path:/tmp/opencode/kuruwa-nix-src#kuruwa \
  --no-link --print-out-paths                       # exit 0

关键输出:

packages.x86_64-linux.default: package 'kuruwa-0.1.0'
packages.x86_64-linux.kuruwa: package 'kuruwa-0.1.0'
all checks passed!
/nix/store/4rik66n7syyjhrsd3f4j9bsqcv0hk67x-kuruwa-0.1.0
NIX_BUILD_EXIT=0

default、kuruwa 和显式 #default 三次构建得到同一输出路径。安装包内的 Rust 产品二进制为:

/nix/store/ak94r4w5y4hqif7q2vyzy1svrxiikgkw-kuruwa-0.1.0/bin/kuruwa

补丁版 RMUX 为:

/nix/store/ybrqvq724hs9iaxxcwlhckhpdil0smai-rmux-0.8.0
  bin/rmux          rmux 0.8.0
  bin/rmux-daemon

rmux 与 rmux-daemon 同目录,源码 hash、补丁和 cargoLock 在构建中被 Nix 校验。产品闭包中没有 /mnt/...、target/debug 或 target/rmux 引用。

8.2 Nix 测试

RMUX 的 checkPhase 实际命令与结果:

cargoCheckHook flags: -j 32 --profile release --target x86_64-unknown-linux-gnu \
  --offline --locked -p rmux-ipc --lib --
test listener::linux_tests::imported_listener_rejects_and_closes_a_connected_socket ... ok
test listener::linux_tests::imported_listener_accepts_a_client_and_sets_close_on_exec ... ok
test result: ok. 16 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

这确认 cargoTestFlags 的字符串值被 buildRustPackage 接受并作为 cargo 参数执行, 不是只写在 Nix 表达式里。Kuruwa 产品包的 checkPhase:

test result: ok. 13 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

8.3 nix run

nix run path:/tmp/opencode/kuruwa-nix-src -- --help
# exit 0
Project-level execution environments and terminal access

Usage: kuruwa [OPTIONS] <COMMAND>

Commands:
  serve       Start the foreground management daemon
  shutdown    Stop the management daemon and clean up managed instances
  instance    Manage sandbox instances

8.4 包外烟测

PKG=/nix/store/4rik66n7syyjhrsd3f4j9bsqcv0hk67x-kuruwa-0.1.0
python3 scripts/accept-package.py "$PKG"     # exit 0

关键输出:

ASSERT_OK installed configuration points at the packaged host init
ASSERT_OK installed configuration points at the packaged bwrap init
ASSERT_OK wrapper supplies the pinned RMUX 0.8.0 backend (/nix/store/...-rmux-0.8.0/bin/rmux)
ASSERT_OK host instance is created from the packaged definition
ASSERT_OK host run waits for the real exit
ASSERT_OK host terminal is exited
ASSERT_OK host run reports literal exit code 23
ASSERT_OK host transcript contains the literal output line
ASSERT_OK bwrap instance is created from the packaged definition
ASSERT_OK bwrap run reports literal exit code 29
ASSERT_OK bwrap terminal reports KURUWA_BWRAP=1
ASSERT_OK bwrap terminal starts in the private work directory
ASSERT_OK bwrap namespace hides the runtime control socket
ASSERT_OK invalid explicit KURUWA_RMUX_BIN fails startup
ASSERT_OK invalid override is reported by path instead of falling back
ASSERT_OK invalid override creates no management socket
ASSERT_OK shutdown returns stopped=true
ASSERT_OK serve exits 0 after normal shutdown
ASSERT_OK shutdown removes the admin socket
ASSERT_OK shutdown removes tenant entry sockets
ASSERT_OK shutdown removes managed instance directories
SMOKE_PASS assertions=27

烟测的 serve、客户端和 bwrap 实例都在 env -i 等价环境中启动,只有 HOME、 TMPDIR、PATH=/usr/bin:/bin、LANG;没有 KURUWA_RMUX_BIN。bwrap 终端 argv 使用 store bash 绝对路径,输出使用 store coreutils printf。显式 override 使用 不存在的路径,serve 以非 0 退出并在日志中报告该路径,没有创建 management socket。烟测临时目录和实例进程已清理,未触碰其他服务。

8.5 未解决项与边界

  • 直接对包含活动 harness socket 的仓库根执行 nix flake show path:$PWD 会失败: error: file '/.../.harness/opencode/runtime/rmux.sock' has an unsupported type。 本次验收使用排除运行时目录的路径副本完成;包本身没有构建失败。
  • 未把上游完整 RMUX workspace 测试纳入 Nix 构建检查;本次只声明并验证 IPC 库 范围。没有据此声称全套测试与 sandbox 不兼容。
  • 未新增 LICENSE,未修改产品源码、原 examples、环境 pin、RMUX 补丁、Cargo 或 现有构建脚本;未提交 Git。