Repository navigation
#515:VM Sandbox 后端实现(Namespaces、镜像根、Microsandbox / Apple Container / SSH 适配器) - #526
jiangkoumo wants to merge 3 commits into
Conversation
…der (Area B1) Extract the complete Sandbox runtime interface and shared native/node plumbing from 9aa10e6 onto upstream/main (35d3657), implementing the Docker backend and leaving same-signature Unsupported slots for backend/{linux,macos,namespaces}/ so B2, B3 and Ephemeral-AI-Lab#515 only replace their own backend directories. In layerfs-sandbox: - Define concrete Runtime (Docker, MacosNative, LinuxNative, Namespaces, Windows) and Sandbox (Docker, Native, Namespaces) enums in backend/runtime.rs. - Add provider capabilities (including LoopbackNfs, MicroVm, RemoteCloud), LaunchPhase/LaunchFailure, ProviderLaunchOptions (Local, Namespaces, Docker) and SandboxRecord (Local, Namespaces, Docker). - Add types::Image and cross-backend ExecRequest; move shared json and strings helpers to backend/. - Implement DockerProvider and DockerInstance over existing Docker Engine lifecycle and streaming execution. - Include shared backend/native/* lifecycle, identity, listener and streams modules under cfg(any(linux, macos)), with same-signature Unsupported stubs in backend/{linux,macos,namespaces}/. In layerfs-api: - Route SandboxApi through Arc<Runtime> via with_runtime and create_provider_file, retaining local resource ownership and command identity validation. - Share TCP/Noise/Hello and Store install pipeline in sandbox/connection.rs. In layerfs-node and @layerfs/agent: - Take verbatim 9aa10e6 shared binding modules (local, discovery, runtime, sandbox, workspace, exec, registry, lib) with a no-op unmount slot for B3. - Expose deploy.sandbox, deploy.localDir, deploy.sandboxBackend and deploy.imageArchive settings in @layerfs/agent. Production LOC: 102831 -> 106022 (delta +3191) First-parent comparison: upstream/main 35d3657 (tree ddea2b5) -> committed tree ae48e29. Scopes: - core: 38395 -> 38395 (delta 0) - engine: 61777 -> 64938 (delta +3161) * layerfs-sandbox: 3417 -> 5526 (delta +2109) * layerfs-api: 1940 -> 2081 (delta +141) * layerfs-node: 2744 -> 3655 (delta +911) - api: 2659 -> 2689 (delta +30) * @layerfs/agent: 849 -> 879 (delta +30) - dsh: 0 -> 0 (delta 0) - reference: absent (retired R9c; last counted 65,417 at tree 498dd19) Method: tools/production_loc.py (blob 3341e4d), --root <exact exported tree> --json for both snapshots; first-party production Rust, runtime SQL and JavaScript; tests, docs, tooling, generated and third-party files excluded.
Production LOC: 106022 -> 109342 (delta +3320) First-parent comparison: b1-interface a32da57 (tree ae48e29) -> rehearsal-515 tree. Scopes: - core: 38395 -> 38395 (delta 0) - engine: 64938 -> 67328 (delta +2390; layerfs-sandbox 5526 -> 7916) - api: 2689 -> 3619 (delta +930; @layerfs/agent +5, @layerfs/runtime +925) - dsh: 0 -> 0 (delta 0) - reference: absent (retired R9c; last counted 65,417 at tree 498dd19) Method: tools/production_loc.py (blob 3341e4d), --root <exact tree> --json.
… privilege
`namespaces_rootfs::a_second_sandbox_waits_for_the_unpack_that_is_running`
published an image and then removed it only to get back to "nothing
published yet". A published image root is 0555, and unlinking inside a
read-only directory is root's DAC privilege, so the CI fast lane (Ubuntu,
nonroot) failed with `PermissionDenied` at namespaces_rootfs.rs:153:36 while
every local root run passed.
The test now takes the digest's own lock, which is the state it is about: an
unpack in progress. Nothing is published while the lock is held, the release
publishes, and the assertion that the archive's content lands is unchanged.
Product stays as it is: the 0600 lock file and the 0555 image root are
deliberate — the backend runs as root in the project VM, and 0555 is the
contract that a command cannot write the shared image.
Swept the other namespaces tests for the same assumption:
- namespaces_lifecycle (5 tests) and namespaces_userns (1) already report
"not root" and return; confirmed in a Debian 13 VM as a nonroot user.
- namespaces_protocol (5) and the other 11 rootfs tests touch no privileged
path.
Verified in a Debian 13 (aarch64) VM with cargo 1.85.1 --locked:
- nonroot user: the previous test fails exactly as CI did (os error 13 at
153:36); this one passes, all 12 rootfs tests pass, lifecycle and userns
report "SKIPPED ... not root", every binary exits 0.
- root: all 8 layerfs-sandbox test binaries pass, 56 tests.
Deleted tests: none.
Production LOC: 109342 -> 109342 (delta 0)
Scope: core, engine, api, dsh; the retired reference is absent (R9c).
Method: tools/production_loc.py --root . (combined 109342; core 38395,
engine 67328, api 3619, dsh 0). This commit changes one integration test
outside the production scope, which `--files` does not list.
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
B1 (layerfs-connect#45): one Sandbox interface, with Docker moved behind it. Merged at PR head a32da57. B1 was cut from main 35d3657, before the #506 Store-backend work on this branch, so the merge ports #506 onto B1's provider interface: - PostgreSQL Stores: ProviderLaunchOptions::Docker and DockerRecord carry `store_volume: Option<String>` like SandboxRequest; v2's create_postgres becomes SandboxApi::create_provider_postgres (Docker launch with no volume, no install, starts ready); the provider's startup Hello waits on DaemonSetup::opens_existing() as v2's create did. B1's create.rs is taken whole, since both v2 edits there now live in provider.rs. - Node binding: v2's DaemonStore start path runs through create_provider_file / create_provider_postgres. A native Sandbox of a PostgreSQL Store is refused Unsupported (B1's native daemon opens a Store file). Native Sandboxes take v2's shared-Store naming (<prefix><store>.<project>-<n>) and the layerfs.store label. - Two modules named local.rs: B1's native Sandbox module keeps `local` (B2, B3 and #526 call crate::local); v2's Store size/heads module (#506 E1, E2) is renamed `local_stores`, unchanged otherwise. - Records save and read an optional `volume`; the creation-intent hint names the volume or, without one, the project label. - client.js keeps v2's `postgresql` deployment flag and B1's native deployment; the boundary guard keeps both sides' entries. - Tests: the node mock Engine keeps v2's version (same behaviour as B1's); B1's new tests get `Some(...)` volumes and `postgresql: None`. Verified on macOS (Rust 1.85.1): workspace check and warning-denying Clippy, all targets; layerfs-sandbox, layerfs-node and layerfs-api tests 220 passed, 0 failed, 15 ignored; boundary guard PASS and its 34 self-tests; agent calls/exec_output and packaging surface/sandbox/ lifecycle PASS. Not run: Linux build and tests (no Linux C toolchain on the host, Docker not running), Docker and FUSE E2E, packaging dispatch (needs the built addon). Disposable only; Durable NOT_RUN, disabled by owner. Production LOC: 111893 -> 115107 (delta +3214) First-parent comparison: layerfs_v2_0_0 da70f98 (tree a949ae7) to prepared merge tree cae5bb4. Scopes: core 38400 -> 38400 (+0), engine 70228 -> 73412 (+3184), api 3265 -> 3295 (+30). By crate: layerfs-sandbox +2109, layerfs-node +927, layerfs-api +148, @layerfs/agent +30. Method: tools/production_loc.py (blob 686d269) --root <exported tree> --json for both snapshots; first-party production Rust, runtime SQL and JavaScript; tests, docs, tooling, generated and third-party files excluded. Reference absent (retired R9c; last counted 65,417 at tree 498dd19). Deleted tests: none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VM Sandbox backend (#512 proposal, #515): one Linux VM per project whose Sandboxes are Linux namespaces (namespace backend, image root, the layerfs-sandbox-helper binary), with Microsandbox, Apple Container and SSH providers in @layerfs/runtime. Merged at PR head 8724090 onto the B3 merge a1cd85c. The PR predates #506 and #511 on this branch; the merge ports it: - project/remove.js: #511 replaced it with project/delete.js and #506 moved its volume steps into store/volume.js removeVolume. The VM's four platform Store hooks (storeHolders, releaseStoreHolders, releaseStore, removeStore) now run inside removeVolume, which takes `{ name, volume }`; delete.js, store calls.js and sync.js pass it. On local placements the shared hooks issue the same Docker commands as before; Windows' releaseStore reads `{ volume }` as the PR made it. remove.js stays deleted. - clients.js: a platform's own deploymentEnv (a `vm:` home) comes first, then v2's PostgreSQL environment, then the Docker default. - sandbox leftovers/list/sweep and volume.js: the VM's no-host-Docker guards come first, then v2's shared-Store aware code; both imports kept. - Tests: verify/vm_real.mjs calls project.delete (kind "Deleted"); the namespaces Linux tests give their Docker records `Some(...)` volumes. Gaps: a `vm:` home's deploymentEnv does not cover PostgreSQL Stores, and v2's shared-Store calls (store.create, load) still make host Docker volumes, so neither is wired to VM placements. Verified on macOS (Rust 1.85.1): workspace warning-denying Clippy, all targets; format check (only the existing minio.rs drift); layerfs-sandbox tests 56 passed, 0 failed, 1 ignored (namespaces tests are Linux-only). @layerfs/runtime with the worktree's own agent/backend linked in a temporary node_modules (removed after): vm_placement all 9 selections PASS (settings, flow, two, bundle, delete, hosts, apple-container, ssh, cleanup); runtime.mjs all 15 selections PASS. Boundary guard PASS with self-tests; tools/test_worktree.py OK; Ruff 0.17.2 format check clean. Not run: Linux build and tests (no Linux C toolchain on the host, Docker not running), verify/vm_real.mjs and ssh_faults.mjs against real VMs. Disposable only; Durable NOT_RUN, disabled by owner. Production LOC: 119822 -> 123141 (delta +3319) First-parent comparison: layerfs_v2_0_0 a1cd85c (tree 63d9a3c) to prepared merge tree 01a2878. Scopes: core 41405 -> 41405 (+0), engine 75122 -> 77512 (+2390), api 3295 -> 4224 (+929). By crate: layerfs-sandbox +2390 (namespace backend and the layerfs-sandbox-helper binary), @layerfs/runtime +924, @layerfs/agent +5. Method: tools/production_loc.py (blob 686d269) --root <exported tree> --json for both snapshots; first-party production Rust, runtime SQL and JavaScript; tests, docs, tooling, generated and third-party files excluded. Reference absent (retired R9c; last counted 65,417 at tree 498dd19). Deleted tests: none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@jiangkoumo Merged into 已合入 |
一句话
这是作者 #512 方案稿 的实现:一个 Store 一个 Linux VM,VM 里的 Sandbox 用 Linux namespaces 代替 Docker 容器,宿主机只连每个 VM 里那一个 worker。Docker 仍然是默认后端,不选
vm:时行为完全不变。它不替代 #512。 #512 是方案稿(V1–V10 是待定的选择题),本 PR 是按那张表逐条落地的实现记录,§10 的每条选择在下面第 4 节写明。方案稿里的结论、边界和不做的事都不改;本 PR 只把「选了什么、为什么、验证到什么程度」写实。
1. 这个 PR 依赖 B1,平台无关的公共层在 B1
四个 PR 是一组,都基于同一个基点:
b1-interface,提交a32da5732,父提交upstream/main35d36575cvm515-v2(942cf923e切出 #515 的文件 +872409049修 CI 失败)9aa10e65c),四个 PR 建好后关闭统一口径(用户 2026-10-31 定):Linux/macOS 共用的本地进程实现放在 B1——即
engine/crates/layerfs-sandbox/src/backend/native/*(约 1230 行)和layerfs-node/src的local.rs、runtime.rs、discovery.rs等本地部署接入。这样 B2、B3、#515 三个 PR 只往各自的后端目录里填实现,彼此不叠、两两合并 0 冲突、可以按任意顺序合入(已预演:三者全合后的树与原 #498 头9aa10e65c逐字节相同)。B2、B3 的正文同样会写「平台无关部分已在 B1」,它们只剩平台相关代码。所以本 PR 的体量集中在两块:
engine/crates/layerfs-sandbox/src/backend/namespaces/**(新后端,只在 Linux 上编译)与api/runtime/src/platform/vm/**(宿主侧的 VM 适配器,纯 JavaScript)。一个运行时的依赖要说清:这个拆分让
backend/linux/{mounts,process}.rs(/proc与挂载表读取)落在 B2。合并不受影响(两边文件不重叠,零冲突),但单独这个 PR 上跑 root 的 namespaces 生命周期测试会 4 条失败(Unsupported "Linux native process table"),加上 B2 就到 5/5。CI 看不到这个差别(CI 非 root,这些用例提前 return),但想在本机验证 namespaces 真实行为时必须知道。细节见 §5.2。2. 为什么 Namespaces 放在这个 PR,而不是 B2(Linux 本机)
B2 是「Linux 本机、Docker 里的进程」;Namespaces 后端确实也不碰 Docker,但它有一个 B2 给不了的前提:一个进程独占整台机器的内核。
它要做的事都是整机级的:
pivot_root、挂/dev/fuse;user.max_user_namespaces写成0。这是整机 sysctl,不能按进程设。不写,命令就能自己unshare -U造一个用户 namespace,把 Workspace 的挂载复制进去,从而躲开 daemon 的正常和强制卸载;这三条只在「每 Store 一个 VM」里成立。放到 B2,就等于让开发机、CI runner 那种共享内核的 Linux 也走上一条它是错的路径:改整机 sysctl 会波及别人的进程,而共享内核下防不住用户 namespace。
所以在类型上它们是并列的三兄弟,不是父子:
SandboxRecord::{Docker, Local, Namespaces},各自只认自己的记录,读到别的变体就拒绝(namespaces_protocol.rs有这条断言),不会把 Docker 记录解释成 namespace Sandbox。3. 改了什么(按目录)
相对 B1
a32da5732共 56 个文件、+8033 / −138(其中测试 13 个、文档 2 个、仓库工具 1 个,其余 40 个是实现文件)。下面是实现文件的生产 LOC 快照,范围是提交
872409049的工作树,计数器tools/production_loc.py --files,只列本 PR 新增或改动的文件;测试在tests/下、不在src/里,不计入。engine/crates/layerfs-sandbox/src/ [2519] (只列本 PR 新增/改动的文件;crate 合计 7916) backend/ helper_spec.rs 110 (新的 init 阶段规约:一行一字段) mod.rs 21 (导出 helper spec 与 namespaces) namespaces/ [1954] mod.rs 11 (新) paths.rs 35 (新:Sandbox 目录布局) cgroup.rs 62 (新:cgroup v2 与 cgroup.kill) create.rs 250 (新:intent、overlay 上层、记录) host.rs 79 (新:root、cgroup v2、/dev/fuse 前置条件) provider.rs 194 (新:启动、重开、拒绝请求) exec.rs 103 (新:exec 段入口) sandbox.rs 284 (新:启动、日志、run、stop、delete、reopen) rootfs/ [936] mod.rs 8 (新) types.rs 22 (新) tar.rs 304 (新:tar 读取、白化与拒绝) archive.rs 289 (新:docker-archive 解析与 digest 校验) layers.rs 192 (新:逐层落盘、符号链接与 device node 拒绝) cache.rs 121 (新:每个 digest 只解一次,flock + rename) + bin/ [434] (新二进制 layerfs-sandbox-helper) layerfs-sandbox-helper/main.rs 45 (新:init / exec 两段入口) layerfs-sandbox-helper/init.rs 88 (新:unshare(mount,pid,ipc) + cgroup) layerfs-sandbox-helper/mounts.rs 160 (新:root、Store、/dev、pivot_root) layerfs-sandbox-helper/exec.rs 141 (新:setns、降权、no-new-privs、退出码)api/runtime/src/ [1691] (只列本 PR 新增/改动的文件;crate 合计 2060) index.js 115 (按 placement 选平台) settings.js 117 (placement、VM 尺寸、SSH 设置) clients.js 63 (deploymentEnv 交给平台) status.js 26 volume.js 51 platform/ (只列改动的文件) shared.js 62 linux/index.js 34 macos/index.js 109 windows/index.js 135 vm/ [789] (新) index.js 312 (新:五步适配器) microsandbox.js 72 (新:msb) apple-container.js 91 (新:container 1.6.1) ssh.js 284 (新:任意 Linux 主机) cli.js 30 (新:外部 CLI 的最小封装) project/ (只列改动的两个文件) init.js 80 remove.js 23 sandbox/ (只列改动的三个文件) leftovers.js 32 list.js 37 sweep.js 18api/agent/src/ transport/index.js 24 (useWorker 接受 resolver:一个进程一条路由)其余非 prod 改动:
AGENTS.md(+20 行,记录 VM 放置这条平台事实)、api/docs/runtime.md(+244 行,新增「Placement: one Linux VM per Store」与「SSH provider」两节)、tools/worktree_build.py(隔离构建多产出一个layerfs-sandbox-helper,VM 里要它),以及测试 6 个文件(namespaces_{lifecycle,protocol,rootfs,userns}.rs、tests/support/image.rs手工造 docker-archive、api/runtime/test/{vm_placement.mjs,fixture/fake-*.cjs,verify/*})。Production LOC(本 PR 两个提交合计):
106022 → 109342(delta +3320)。按层:
core 38395 → 38395 (0)、engine 64938 → 67328 (+2390,其中 layerfs-sandbox 5526 → 7916)、api 2689 → 3619 (+930,其中 @layerfs/runtime +925、@layerfs/agent +5)、dsh 0 → 0;reference 已按 R9c 退役,记为absent (retired R9c; last counted 65,417 at tree 498dd1917812)。方法:
tools/production_loc.py --root <tree>(blob3341e4d0),同一个计数器比较两个快照。4. #515 §10 的 V1–V10:我们选了什么、为什么
LAYERFS_PLACEMENT、LAYERFS_SANDBOX_BACKEND(另LAYERFS_VM_*、LAYERFS_SSH_*)tools/?逐条理由:
V1 —— 接受。 #512 的四条结论里第一条是硬事实:VM 之间不共享内核,也不共享磁盘;SQLite 的 WAL 和目录锁只能在一个内核内协调。所以「一个 Sandbox 一个 VM」不可能成立,只能「一个 Store 一个 VM」。Docker 仍是默认后端:
placement不写就是local,Linux 本机、macOS 的 worker 容器、Windows 的 WSL2 worker 都照旧。V2 —— 接受,且规则一个字都没改。 以前只有 macOS 和 Windows 需要把 Store 调用发给另一个进程;
vm:放置下 Linux 也一样,因为 Store 在 VM 的盘上。规则依旧是:通道只在本机 loopback(或 provider 的私有通道)、必须带 secret、一次尝试、丢了回复绝不重放。新增的只有「一条进程级路由」,因为现在每个项目有各自的 endpoint:useWorker(resolver)或该项目的LAYERFS_WORKER_ENDPOINT,同一个进程里不同项目的调用会各自落到自己的 VM。这是本 PR 在@layerfs/agent里唯一的改动(+5 行 prod),也有对应测试。V3 —— 选了三个适配器。
microsandbox(msb)和ssh(任意 Linux 主机,含 EC2 / 自建 VM)是 #512 的建议首选,apple-container(container1.6.1)随后补上;三个都实现了。Daytona、E2B、Vercel Sandbox、Modal 没做:需要账号和账单,而且它们的 runner 是共享内核,需要 V4 的第二条路(seccomp)才能防住用户 namespace,本版明确不支持。V4 —— 只做整机 sysctl。 worker 启动时把
user.max_user_namespaces写成0,然后读回来校验,不是假设成功(prepareGuest)。理由:每个 VM 只服务一个 Store,整机封掉在语义上是正确的,不需要新依赖或 unsafe 例外(seccomp 过滤器需要 BPF,仓库现有 crate 在安全 Rust 里给不了)。代价写清楚了:共享内核的宿主(Daytona 那类)本版不支持,而不是「看起来支持」。V5 —— 共享 VM 网络。 helper 的 init 阶段只
unshare(CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWIPC),没有CLONE_NEWNET。理由:最简单,且不损失安全边界——命令最多能到别的 Sandbox 的 daemon 端口,但那条连接要 secret;命令本身没有 capability、不能 mount、不能造用户 namespace。per-Sandbox netns 要用 netlink 起 loopback,属于能后加的独立改动。V6 —— 接受。
layerfs-sandbox的nix打开sched/mount/user/process,并加一个layerfs-sandbox-helper二进制。原因:setns/unshare只作用于调用者进程,没法在安全 Rust 里表达成库函数;helper 是单线程两段式(init 与 exec),unsafe依然为零。不用nsenter/setpriv,VM 里不需要 util-linux。V7 —— 接受。 bundle 里带
<bundle>/images/<64 hex>.tar(未压缩的docker save归档),后端校验它的 config digest 等于deploy.image,每个 digest 只解一次到 0555 只读目录,每个 Sandbox 在它上面挂自己的一层可写 overlay。gzip 层会被拒绝并给出理由;bundle 里多于一个镜像直接拒绝(一次部署只有一个镜像)。这样 VM 里不需要任何 Engine。V8 —— 要求持久盘。 三个 provider 的磁盘在 stop/start 之间都保留,够 Disposable 用。远程 Store push(#506)还不存在,所以没有「先推送再删」的路径:项目还在时删它的 VM 会被拒绝,并说明「这个 Store 只在那块盘上」;删项目才删 VM 和它的 Store 盘。这是事实约束,不是保守。
V9 —— 设置名。
LAYERFS_PLACEMENT=local|vm:<provider>,VM 内的后端LAYERFS_SANDBOX_BACKEND=namespaces;另有LAYERFS_VM_{IMAGE,CPUS,MEMORY,STORE_SIZE}与LAYERFS_SSH_*(host/user/port/identityFile/storePath 等)。只读LAYERFS_*;插件自己的前缀(如 dsh 的DSH_*)由插件自己映射,LayerFS 不认任何插件名。写错的 provider 名会立刻Unsupported,不会被当成local悄悄跑。V10 —— 不放进
tools/。 #512 那个「用替身回答 Engine API、每个容器就是一组 namespace」的 stand-in 没有进tools/。理由:它是某个包里「没有 Docker 也能跑」的证明工具,不是仓库级工具;放进tools/会看起来像一个受支持的运行方式,而 CI 根本不跑 JS 测试。取代做法是各 provider 一个窄替身,跟测试放一起:api/runtime/test/fixture/fake-{msb,container,ssh}.cjs(外加既有的fake-native.cjs)。5. 怎么验证
5.1 macOS 本机(全部退出码 0)
cargo +1.85.1 fmt --all -- --checkcargo +1.85.1 check --locked --workspace --all-targetscargo +1.85.1 clippy --locked --workspace --exclude layerfs-node --all-targets -- -D warningscargo +1.85.1 clippy --locked -p layerfs-sandbox --all-targets --target x86_64-unknown-linux-gnu -- -D warningsnamespaces_*测试代码的路径)python3 -B tools/check_product_boundary.pypython3 -B tools/check_fuser_integrity.pyerrors: [],授权补丁校验通过python3 -B -m unittest discover -s tools -p 'test_*.py'for c in settings flow two bundle delete hosts apple-container ssh cleanup; do node api/runtime/test/vm_placement.mjs $c; donenode api/runtime/test/runtime.mjs <14 个 case>node api/backend/test/backend.mjs <4 个 case>node --test api/cli/test/cli.test.mjsnode api/runtime/test/verify/ssh_faults.mjsnode api/agent/test/{calls,native_deployment,exec_output}.mjspackaging.mjs的worker-per-projectcase(见风险 7)5.2 Linux 真机(Debian 13 / aarch64 VM,cargo 1.85.1
--locked)这一条是本 PR 修的 CI 失败,所以两侧都跑了。
非 root(CI 的身份):
cargo test -p layerfs-sandbox --tests --no-fail-fastnamespaces_lifecycle(5) 与namespaces_userns(1) 按惯例打印SKIPPED … not rootnamespaces_rootfs(修复前的文件)PermissionDeniedatnamespaces_rootfs.rs:153:36,与 CI 逐字一致namespaces_rootfs(修复后)12 passedCI 用的是
python3 -B tools/ci_tests.py targets fast+nextest --profile ci(非 root)。已确认这四个namespaces_*目标都在 fast lane、都不在 nightly 列表里,所以它们必须能在非 root 下通过——现在可以。root(真实的 namespaces 生命周期):这两条限制是事实,不是缺陷,但必须写明:
/proc实现。 本 PR 单独在树里时,backend/linux/process.rs是 B1 的占位实现(真正的读取在 B2),所以 lifecycle 会是1 passed / 4 failed,原因为LaunchFailure { phase: Start, cause: Io(Unsupported("Linux native process table")) }。用git merge-tree --write-tree rehearsal-b2 HEAD(→5ec79f35,0 冲突)得到的合并树就全过。require_overlay_upper守卫(overlay / nfs / cifs / 9p / virtiofs 均拒绝),而这个 VM 的/本身就是 overlay,所以 fixture 的TMPDIR要指到 tmpfs(真实部署里是 Store 那块 ext4)。user.max_user_namespaces=0、tmpfscgroup.kill拆除)Unsupported(…overlayfs…)源码里
#[test]共 71 个(12 个二进制);非 root 一次完整运行里... ok行数也是 71、退出码 0。5.3 未跑(不当作通过)
vm_real.mjs,需要msb/container真机):本轮只跑了替身套件。x86_64-linux-gnu的 C 工具链,libsqlite3-sys/zstd-sys/napi 交叉编译失败),这是 B1 就有的限制,留给 Linux 机。/proc),见上表;不把它当作通过。6. 风险与已知限制
Unsupported而不是降级。PROVIDERS只有三个,写别的名字立刻Unsupported。-p端口转发曾出现一次失败(Sandbox backends: which agent sandboxes can replace LayerFS's Docker container? Local stand-ins first #512 记录,原因未知);失败会报出来,不会重试也不会假装成功。docker save归档;gzip 层被拒绝(附理由),bundle 里多于一个镜像被拒绝。packaging.mjs在 Node 22 上无法整体解析(文件里有await using,这是 B1 就有的写法,与本 PR 无关)。本 PR 新增的worker-per-projectcase 已单独以等价方式跑过并通过,但在 Node ≥ 24 上才算真正跑过整个文件。backend/linux/process.rs是 B1 的占位实现,Start会报Unsupported "Linux native process table"(4/5 失败);B2 合进来后 5/5 通过(§5.2 有实测)。合并不受影响,但“本 PR 独自就能跑完全部测试”不成立。require_overlay_upper会拒绝 overlay / nfs / cifs / 9p / virtiofs 上的 Sandbox 可写层;这是有意的守卫(真实部署放 Store 那块 ext4/xfs/btrfs),但搭测试环境时容易踩到(VM 的/往往是 overlay)。7. 附:本 PR 的 CI 修复(第二个提交)
原 #498 的 fast lane 有一条失败,只在非 root 下出现:
原因:那条测试先解一次镜像(发布目录是 0555,这是契约:命令不能写共享镜像),再把它
remove_dir_all掉,只为了回到「还没发布」的状态。而在一个只读目录里 unlink 是 root 的 DAC 特权,非 root 必然EACCES——本地一直用 root 跑,所以没暴露。修的是测试,不是产品:产品那 0600 的 lock 文件和 0555 的镜像根都是刻意的(后端在 VM 里以 root 跑;0555 是「命令不能写共享镜像」的防线)。测试现在直接拿该 digest 的锁,也就是它本来要表达的状态「有一个解包正在进行」:锁被持有时不发布、释放后发布、内容断言不变。同一条测试在 Debian 13 的非 root 用户下,修复前失败、修复后通过(§5.2)。
生产代码 LOC:
109342 → 109342(delta 0,只改了一个集成测试)。