Repository navigation
feat: macOS 原生适配——daemon 内置 loopback NFSv3 导出,普通用户 mount_nfs 挂载 Workspace(无 Docker / 无 macFUSE / 无 root) - #427
jiangkoumo wants to merge 74 commits into
Conversation
…=local, for a VM or CI box without Docker Part of one change pushed file by file through the GitHub contents API; the reviewable unit is the whole branch (or the single-commit patch). This commit: core/crates/layerfs-sandbox/Cargo.toml (not production (test, manifest or docs)). No tests changed or deleted. Production LOC: 108,451 -> 108,451 (delta 0) Scope: combined core+sdk+dsh, tools/production_loc.py, base 5fb68dd.
…=local, for a VM or CI box without Docker Part of one change pushed file by file through the GitHub contents API; the reviewable unit is the whole branch (or the single-commit patch). This commit: core/crates/layerfs-sandbox/src/backend/local/mod.rs (production). No tests changed or deleted. Production LOC: 108,451 -> 108,460 (delta +9) Scope: combined core+sdk+dsh, tools/production_loc.py, base 5fb68dd.
…=local, for a VM or CI box without Docker Part of one change pushed file by file through the GitHub contents API; the reviewable unit is the whole branch (or the single-commit patch). This commit: core/crates/layerfs-sandbox/src/backend/local/process.rs (production). No tests changed or deleted. Production LOC: 108,460 -> 108,505 (delta +45) Scope: combined core+sdk+dsh, tools/production_loc.py, base 5fb68dd.
…=local, for a VM or CI box without Docker Part of one change pushed file by file through the GitHub contents API; the reviewable unit is the whole branch (or the single-commit patch). This commit: core/crates/layerfs-sandbox/src/backend/local/mounts.rs (production). No tests changed or deleted. Production LOC: 108,505 -> 108,576 (delta +71) Scope: combined core+sdk+dsh, tools/production_loc.py, base 5fb68dd.
…=local, for a VM or CI box without Docker Part of one change pushed file by file through the GitHub contents API; the reviewable unit is the whole branch (or the single-commit patch). This commit: core/crates/layerfs-sandbox/src/backend/local/create.rs (production). No tests changed or deleted. Production LOC: 108,576 -> 108,834 (delta +258) Scope: combined core+sdk+dsh, tools/production_loc.py, base 5fb68dd.
…=local, for a VM or CI box without Docker Part of one change pushed file by file through the GitHub contents API; the reviewable unit is the whole branch (or the single-commit patch). This commit: core/crates/layerfs-sandbox/src/backend/local/sandbox.rs (production). No tests changed or deleted. Production LOC: 108,834 -> 109,057 (delta +223) Scope: combined core+sdk+dsh, tools/production_loc.py, base 5fb68dd.
…=local, for a VM or CI box without Docker Part of one change pushed file by file through the GitHub contents API; the reviewable unit is the whole branch (or the single-commit patch). This commit: core/crates/layerfs-sandbox/src/backend/local/exec.rs (production). No tests changed or deleted. Production LOC: 109,057 -> 109,166 (delta +109) Scope: combined core+sdk+dsh, tools/production_loc.py, base 5fb68dd.
…=local, for a VM or CI box without Docker Part of one change pushed file by file through the GitHub contents API; the reviewable unit is the whole branch (or the single-commit patch). This commit: core/crates/layerfs-sandbox/src/backend/mod.rs (production). No tests changed or deleted. Production LOC: 109,166 -> 109,167 (delta +1) Scope: combined core+sdk+dsh, tools/production_loc.py, base 5fb68dd.
…=local, for a VM or CI box without Docker Part of one change pushed file by file through the GitHub contents API; the reviewable unit is the whole branch (or the single-commit patch). This commit: core/crates/layerfs-sandbox/tests/local_runtime.rs (not production (test, manifest or docs)). No tests changed or deleted. Production LOC: 109,167 -> 109,167 (delta 0) Scope: combined core+sdk+dsh, tools/production_loc.py, base 5fb68dd.
…=local, for a VM or CI box without Docker Part of one change pushed file by file through the GitHub contents API; the reviewable unit is the whole branch (or the single-commit patch). This commit: sdk/js/native/src/runtime.rs (production). No tests changed or deleted. Production LOC: 109,167 -> 109,274 (delta +107) Scope: combined core+sdk+dsh, tools/production_loc.py, base 5fb68dd.
…=local, for a VM or CI box without Docker Part of one change pushed file by file through the GitHub contents API; the reviewable unit is the whole branch (or the single-commit patch). This commit: sdk/js/native/src/local.rs (production). No tests changed or deleted. Production LOC: 109,274 -> 109,527 (delta +253) Scope: combined core+sdk+dsh, tools/production_loc.py, base 5fb68dd.
…=local, for a VM or CI box without Docker Part of one change pushed file by file through the GitHub contents API; the reviewable unit is the whole branch (or the single-commit patch). This commit: sdk/js/native/src/registry.rs (production). No tests changed or deleted. Production LOC: 109,527 -> 109,527 (delta 0) Scope: combined core+sdk+dsh, tools/production_loc.py, base 5fb68dd.
…=local, for a VM or CI box without Docker Part of one change pushed file by file through the GitHub contents API; the reviewable unit is the whole branch (or the single-commit patch). This commit: sdk/js/native/src/discovery.rs (production). No tests changed or deleted. Production LOC: 109,527 -> 109,644 (delta +117) Scope: combined core+sdk+dsh, tools/production_loc.py, base 5fb68dd.
…=local, for a VM or CI box without Docker Part of one change pushed file by file through the GitHub contents API; the reviewable unit is the whole branch (or the single-commit patch). This commit: sdk/js/native/src/exec.rs (production). No tests changed or deleted. Production LOC: 109,644 -> 109,633 (delta -11) Scope: combined core+sdk+dsh, tools/production_loc.py, base 5fb68dd.
…=local, for a VM or CI box without Docker Part of one change pushed file by file through the GitHub contents API; the reviewable unit is the whole branch (or the single-commit patch). This commit: sdk/js/native/src/sandbox.rs (production). No tests changed or deleted. Production LOC: 109,633 -> 109,641 (delta +8) Scope: combined core+sdk+dsh, tools/production_loc.py, base 5fb68dd.
…=local, for a VM or CI box without Docker Part of one change pushed file by file through the GitHub contents API; the reviewable unit is the whole branch (or the single-commit patch). This commit: sdk/js/native/src/workspace.rs (production). No tests changed or deleted. Production LOC: 109,641 -> 109,636 (delta -5) Scope: combined core+sdk+dsh, tools/production_loc.py, base 5fb68dd.
…=local, for a VM or CI box without Docker Part of one change pushed file by file through the GitHub contents API; the reviewable unit is the whole branch (or the single-commit patch). This commit: sdk/js/native/src/lib.rs (production). No tests changed or deleted. Production LOC: 109,636 -> 109,638 (delta +2) Scope: combined core+sdk+dsh, tools/production_loc.py, base 5fb68dd.
…=local, for a VM or CI box without Docker Part of one change pushed file by file through the GitHub contents API; the reviewable unit is the whole branch (or the single-commit patch). This commit: sdk/js/native/tests/unit/runtime.rs (not production (test, manifest or docs)). No tests changed or deleted. Production LOC: 109,638 -> 109,638 (delta 0) Scope: combined core+sdk+dsh, tools/production_loc.py, base 5fb68dd.
…=local, for a VM or CI box without Docker Part of one change pushed file by file through the GitHub contents API; the reviewable unit is the whole branch (or the single-commit patch). This commit: sdk/js/src/client.js (production). No tests changed or deleted. Production LOC: 109,638 -> 109,656 (delta +18) Scope: combined core+sdk+dsh, tools/production_loc.py, base 5fb68dd.
…=local, for a VM or CI box without Docker Part of one change pushed file by file through the GitHub contents API; the reviewable unit is the whole branch (or the single-commit patch). This commit: sdk/js/src/index.js (production). No tests changed or deleted. Production LOC: 109,656 -> 109,658 (delta +2) Scope: combined core+sdk+dsh, tools/production_loc.py, base 5fb68dd.
…=local, for a VM or CI box without Docker Part of one change pushed file by file through the GitHub contents API; the reviewable unit is the whole branch (or the single-commit patch). This commit: sdk/js/test/local_deployment.mjs (not production (test, manifest or docs)). No tests changed or deleted. Production LOC: 109,658 -> 109,658 (delta 0) Scope: combined core+sdk+dsh, tools/production_loc.py, base 5fb68dd.
…=local, for a VM or CI box without Docker Part of one change pushed file by file through the GitHub contents API; the reviewable unit is the whole branch (or the single-commit patch). This commit: sdk/js/README.md (not production (test, manifest or docs)). No tests changed or deleted. Production LOC: 109,658 -> 109,658 (delta 0) Scope: combined core+sdk+dsh, tools/production_loc.py, base 5fb68dd.
…l-AI-Lab#402) Part of one change pushed file by file through the GitHub contents API; the reviewable unit is the whole set (local commit 9334dbd22, one patch). This commit: core/crates/layerfs-sandbox/src/backend/local/process.rs (production). No tests changed or deleted except additions. Production LOC: 109,658 -> 109,696 (delta +38) Scope: combined core+sdk+dsh, tools/production_loc.py, parent 85255766e.
…l-AI-Lab#402) Part of one change pushed file by file through the GitHub contents API; the reviewable unit is the whole set (local commit 9334dbd22, one patch). This commit: core/crates/layerfs-sandbox/src/backend/local/groups.rs (production). No tests changed or deleted except additions. Production LOC: 109,696 -> 109,818 (delta +122) Scope: combined core+sdk+dsh, tools/production_loc.py, parent 85255766e.
…l-AI-Lab#402) Part of one change pushed file by file through the GitHub contents API; the reviewable unit is the whole set (local commit 9334dbd22, one patch). This commit: core/crates/layerfs-sandbox/src/backend/local/exec.rs (production). No tests changed or deleted except additions. Production LOC: 109,818 -> 109,835 (delta +17) Scope: combined core+sdk+dsh, tools/production_loc.py, parent 85255766e.
…l-AI-Lab#402) Part of one change pushed file by file through the GitHub contents API; the reviewable unit is the whole set (local commit 9334dbd22, one patch). This commit: core/crates/layerfs-sandbox/src/backend/local/sandbox.rs (production). No tests changed or deleted except additions. Production LOC: 109,835 -> 109,868 (delta +33) Scope: combined core+sdk+dsh, tools/production_loc.py, parent 85255766e.
…l-AI-Lab#402) Part of one change pushed file by file through the GitHub contents API; the reviewable unit is the whole set (local commit 9334dbd22, one patch). This commit: core/crates/layerfs-sandbox/src/backend/local/create.rs (production). No tests changed or deleted except additions. Production LOC: 109,868 -> 109,882 (delta +14) Scope: combined core+sdk+dsh, tools/production_loc.py, parent 85255766e.
…l-AI-Lab#402) Part of one change pushed file by file through the GitHub contents API; the reviewable unit is the whole set (local commit 9334dbd22, one patch). This commit: core/crates/layerfs-sandbox/src/backend/local/mounts.rs (production). No tests changed or deleted except additions. Production LOC: 109,882 -> 109,870 (delta -12) Scope: combined core+sdk+dsh, tools/production_loc.py, parent 85255766e.
…l-AI-Lab#402) Part of one change pushed file by file through the GitHub contents API; the reviewable unit is the whole set (local commit 9334dbd22, one patch). This commit: core/crates/layerfs-sandbox/src/backend/local/mod.rs (production). No tests changed or deleted except additions. Production LOC: 109,870 -> 109,871 (delta +1) Scope: combined core+sdk+dsh, tools/production_loc.py, parent 85255766e.
…l-AI-Lab#402) Part of one change pushed file by file through the GitHub contents API; the reviewable unit is the whole set (local commit 9334dbd22, one patch). This commit: core/crates/layerfs-sandbox/src/backend/mod.rs (production). No tests changed or deleted except additions. Production LOC: 109,871 -> 109,872 (delta +1) Scope: combined core+sdk+dsh, tools/production_loc.py, parent 85255766e.
The Workspace unmount lease opened its record with a hand-written O_NOFOLLOW: 0x100 on macOS and 0x20000 on Linux. 0x20000 is O_NOFOLLOW only on x86_64 Linux; on aarch64 Linux O_NOFOLLOW is 0x8000 and 0x20000 is O_LARGEFILE, so the lease would silently follow a symlink there. Take the constant from libc (already in the lock at =0.2.189 through other crates) so every target gets its own value. No behavior change on macOS or x86_64 Linux. The binding's existing discovery::no_follow() carries the same hand-written values and is left as it is (it predates this branch).
Sandbox::stop() ended the daemon first and left the Sandbox's mounts in place until Delete detached them. Detach them first, while the daemon can still answer, then stop the daemon as before. A failed detach is reported and does not block the crash teardown; Delete keeps its own checked detach before removing anything through a mount. On Linux FUSE this is ordering only: Stop now returns with the mounts already gone (measured 21.6 ms with a live holder and a dirty writer; before the change the mounts stayed until Delete), and reclaim is unchanged (0.09-0.10 s). MNT_DETACH does not flush dirty pages, so it adds no data guarantee; Stop stays the crash teardown it was. Same hunk as the local-backend part of the macOS native branch's NFS unmount-hang fix, so merging this branch there stays clean.
|
新提交(3 个,叠在 a700af8 上):修复 macOS NFS 卸载卡住时 daemon 跟着挂死、并拖住整个 Sandbox 的问题 • 4c1648e fix(native): bound NFS helper reaping and detach before daemon stop 问题
改法 需要你确认的三处对外行为变化(没有新方法、新错误码,daemon 协议不变)
复现 验证(头 d3a2a8c) 没覆盖的 #402 那边只放了本地后端 "Stop 先 detach" 这一段(和这里的 hunk 完全相同),NFS helper 和 binding 放锁的部分只在本 PR。 |
Lists herdr-layerfs (jiangkoumo) at plugins/herdr-layerfs/: versions 0.2.1 through 0.3.3 (latest d2bf1a854b), all on LayerFS ad78914 (v1.1.0 plus the #436 binding fix). Linux docker mode only, status preview; the macOS native mode waits for #427. plugin_check.py --online prints ok. The listing matches layerfs.ai. Production LOC: 102162 -> 102162 (delta 0) First-parent comparison (main 0f7054e -> this merge). core 38241, engine 61288, api 2633, unchanged; reference absent (retired R9c; last counted 65,417 at tree 498dd19). Counted with tools/production_loc.py; listing files only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…) into PR Ephemeral-AI-Lab#402 (local port)
…) into PR Ephemeral-AI-Lab#427 (local port)
…meral-AI-Lab#402) Upstream now keeps the Node binding's tests outside the crate source: they load the ordinary built addon and call its exported methods, never including private binding source (engine/AGENTS.md, engine/crates/layerfs-node/tests). The Ephemeral-AI-Lab#402 unit tests that were included by runtime.rs and sandbox.rs (tests/unit/{runtime,reclaim}.rs before D8) are rewritten as engine/crates/layerfs-node/tests/native_local.rs, which writes a Sandbox record as an earlier call would and calls the public `runtime.sandbox.reclaim` with a local deployment: - a local record naming any other directory (a data directory, another LAYERFS_LOCAL_DIR, another Sandbox or name, a relative or `..` path, `/`) is refused InvalidInput, "neither used nor deleted", and nothing is removed; - a local record is used only with an absolute, plain deploy.localDir; - a stopped local Sandbox where this deployment puts it is deleted and its number freed; a record from before boot ids is read without one; a damaged boot or port is refused and removes nothing; - a Docker record is not read as a local Sandbox. The Docker reclaim cases of the old reclaim.rs are covered by upstream's native_discovery.rs. Selection of a misspelled deploy.sandbox is refused only by sandbox.create after the project's Store is installed, so it is not reachable without a Store and is left to the SDK test (local_deployment.mjs). Production LOC: 103762 -> 103762 (delta 0) (tools/production_loc.py --root ., core+engine+api; test-only commit)
…e built addon) into PR Ephemeral-AI-Lab#427 (local port) Brings Ephemeral-AI-Lab#402's engine/crates/layerfs-node/tests/native_local.rs. One conflict: api/binding/tests/unit/runtime.rs, deleted on Ephemeral-AI-Lab#402 and kept by Ephemeral-AI-Lab#427's D8 merge, is deleted here too. api/binding/ no longer exists after D8, the file was included by nothing, and its cases move to native_local.rs in the next commit. Production LOC: first-parent comparison 1931032 -> this merge, see tools/production_loc.py --root .; tests only, delta 0.
Production LOC: 103762 -> 103762 (delta 0) (tools/production_loc.py --root ., core+engine+api; formatting of a test file only)
…fter D8 (Ephemeral-AI-Lab#427) - engine/crates/layerfs-node/tests/native_local.rs gains local_and_native_sandboxes_are_never_mixed: through the built addon's public runtime.sandbox.reclaim, a native record is refused (InvalidInput, "never mixed") by a local deployment and a local record by a native one, nothing is removed, and a native deployment reclaims its own stopped record. It replaces the native cases of the old included api/binding/tests/unit/runtime.rs (removed in the previous merge). - The real native SDK regression of the unmount lease moves from api/binding/tests/unmount-concurrency.mjs (api/binding/ is gone after D8) to api/agent/test/verify/native_unmount_real.mjs, next to upstream's other *_real.mjs verifications, with its imports updated to D8's layout (api/agent/src, api/backend/src). It stays macOS-only and explicit. - discovery.rs: the "never mixed" refusal lost its line continuation in the v1.1 port and carried a run of spaces inside the message; restored. Production LOC: 108595 -> 108596 (delta +1) (tools/production_loc.py --root ., core+engine+api; the restored message continuation splits one source line in two)
…Ephemeral-AI-Lab#427) D8 (Ephemeral-AI-Lab#465) made tools/check_product_boundary.py fail closed on any LayerFS crate missing from its core/engine/api layer map. Ephemeral-AI-Lab#427's layerfs-nfs (the loopback NFSv3 export, beside layerfs-fuse under engine/crates) was not in it, so the full and the CI (--skip-sdk) runs reported "crate missing from layer map: layerfs-nfs" for layerfs-nfs and for layerfs-daemon, which depends on it. It is an engine crate like layerfs-fuse; its dependencies (content, overlay, workspace, fuse) are at or below that layer. Production LOC: 108596 -> 108596 (delta 0) (tools/production_loc.py --root ., core+engine+api; development tool only)
…-Lab#402) Production LOC: 103762 -> 103762 (delta 0) (tools/production_loc.py --root ., core+engine+api; documentation only)
…nto PR Ephemeral-AI-Lab#427 (local port) Production LOC: 108596 -> 108596 (delta 0), first-parent comparison (tools/production_loc.py --root ., core+engine+api; documentation only)
…b#427) Production LOC: 108596 -> 108596 (delta 0) (tools/production_loc.py --root ., core+engine+api; documentation only)
|
同步一下:本分支已跟上 D8 三层结构(main caebd26,#465),#402 也同步更新了。 主要变化 验证 有一处想请教:deploy.sandbox 拼错(例如 Local)时的拒绝,在 binding 里只有带 Store 的 sandbox.create 才会检查,addon 测试不带 Store 走不到,目前只由 SDK 的 local_deployment.mjs 覆盖。这样是否可以,还是希望在 binding 这层补一个测试? |
…-AI-Lab#427) Brings main up to 582bc78 (Ephemeral-AI-Lab#488, Ephemeral-AI-Lab#481 forced unmount after a stalled Join, Ephemeral-AI-Lab#479, Ephemeral-AI-Lab#471, Ephemeral-AI-Lab#473, Ephemeral-AI-Lab#445 fs modes, Ephemeral-AI-Lab#475, Ephemeral-AI-Lab#478, Ephemeral-AI-Lab#476 Docker API negotiation, Ephemeral-AI-Lab#396, Ephemeral-AI-Lab#419, Ephemeral-AI-Lab#418, Ephemeral-AI-Lab#415, Ephemeral-AI-Lab#477, Ephemeral-AI-Lab#468). Two conflicts, both from Ephemeral-AI-Lab#481 (forced unmount of a normal unmount stalled at Join): - engine/crates/layerfs-daemon/src/control/force.rs: guard() keeps this branch's ExportKept check (an NFS export kept after a failed teardown answers with its stored custody first) and takes main's comment and its forceable() exception for a retained FUSE session. The non-Linux `let _ = token;` stays removed: ExportKept uses the token on every platform. - engine/crates/layerfs-node/src/workspace.rs: unmount() keeps this branch's per-Workspace unmount lease (repeat a published outcome, drop the admission guard before the daemon call, publish the outcome before cleanup) and takes main's `force` option inside it: forced, one api.unmount(token, true) whose None answer is Unmounted::Closed; otherwise unmount_checked with `discard` as before. Production LOC: first-parent 9478190 108596 -> 109158 (tools/production_loc.py --root .; the delta is main's own changes)
…er selectable (Ephemeral-AI-Lab#427) Ephemeral-AI-Lab#427 is the macOS native adaptation. The local-daemon runtime it builds on (first proposed in the closed Ephemeral-AI-Lab#402) is now part of this PR, and its only selectable form is `native`: on macOS a daemon of this user serves its Workspaces over loopback NFS to the host's own client. Linux keeps the Docker route exactly as on main. - deploy.sandbox (LAYERFS_SANDBOX) accepts unset, `docker` or `native`; `local` is refused InvalidInput like any other value, in the SDK (api/agent/src/client.js) and in the binding (runtime::local). - local::start refuses `native` off macOS as Unsupported before any effect, so no deployment starts a local daemon on Linux. The root check and the Local::new (Linux FUSE) branch go: native always takes Local::native, LAYERFS_KERNEL=nfs and this user's identity. discovery::root() had no other caller and goes too. - Records: a new record's runtime is always `native`. A native record is used only by a native deployment; a record of the former `local` runtime (an earlier, unmerged build) is used by no deployment, refused InvalidInput ("never mixed", "neither used nor deleted"), and nothing it names is deleted. - Tests: engine/crates/layerfs-node/tests/native_local.rs writes native records (record handling is the same code on Linux) and gains a_record_of_the_former_local_runtime_is_never_used_nor_deleted; api/agent/test/local_deployment.mjs becomes native_deployment.mjs with `local` among the refused values. - api/docs/agent.md drops the "Local Sandboxes" section and keeps its layout, record and reclaim facts under "Native Sandboxes (macOS)". The local backend in layerfs-sandbox (engine/crates/layerfs-sandbox/src/ backend/local) is kept as it is: native uses its process, exec, group, mount and record code; Local::new and its Linux tests stay as tests of that shared code. Production LOC: 109158 -> 109142 (delta -16) (tools/production_loc.py --root .)
|
新提交 d55acc1:改成独立的 macOS 原生适配,并 merge 到 main 582bc78 • 不再依赖 #402:它用到的本地 daemon 运行时已经作为本 PR 的一部分收进来。 验证:上游 CI 全过;Linux 上 fmt / 边界检查 / clippy / fast lane / node 与 JS 测试和 main 对照,没有新增失败;Mac(Apple silicon,macOS 27.2)上 release 构建、单测、clippy、native smoke 48/48、PATH 检查 11/11、卸载租约回归都通过。详细结果已更新到 PR 描述。 |
Poll endpoint through the Sandbox that owns the Child before asserting that no zombie remains. Bound the observation by WAIT and preserve the stopped and not-gone assertions without changing product behavior. Validation: cargo +1.85.1 fmt --all -- --check; cargo +1.85.1 clippy --locked -p layerfs-sandbox --all-targets -- -D warnings. Both passed on macOS with this worktree private target. Linux-only runtime tests were not executed; Hark owns Linux verification. Production LOC: 109210 -> 109210 (delta +0). Scope: core 41400, engine 65131, API 2679; reference absent (retired R9c; last counted 65,417 at tree 498dd19). Tests, tools, docs, dependencies and generated artifacts excluded; inline test code excluded by the counter. Method: tools/production_loc.py at Git blob 3341e4d; export parent 3147916 and final staged tree d97b40d using git archive, then run python3 -B tools/production_loc.py --root <each-export> --json. Both invocations exited 0.
… test fix) into B1
这个 PR 做什么
LayerFS 在 Linux 上用 FUSE 把 Workspace 呈现为普通目录。macOS 没有内置 FUSE,现在的选择是 macFUSE(要内核扩展,Apple Silicon 上要降低安全级别)、FUSE-T(挂载不稳定)或 Docker Desktop 里的 Linux(要装 Docker,I/O 全过虚拟机文件共享)。
本 PR 走另一条路:Sandbox 的 daemon 作为当前用户的普通进程运行,自己提供一个只监听
127.0.0.1的 NFSv3 服务,macOS 自带的/sbin/mount_nfs以普通用户身份把 Workspace 挂到 Sandbox 目录下。 和 Meta EdenFS 在 macOS 上的做法一致。不需要 root、内核扩展、虚拟机或 Docker。LAYERFS_SANDBOX=native(仅 macOS),另需LAYERFS_DAEMON、LAYERFS_LOCAL_DIR(0755、属于当前用户)和放在本地 apfs/hfs 上的LAYERFS_STORE_PATH;不读任何 Docker 设置。PATH(Homebrew、~/.local/bin、nvm 的node都能找到)。它不做隔离,隔离交给调用方自己的沙箱(例如 Seatbelt)。sandbox.close等语义和 Docker Sandbox 一致;daemon 被杀后,下一次sandbox.create/runtime.sandbox.reclaim会强制卸掉它留下的挂载再删目录。api/docs/agent.md的 "Native Sandboxes (macOS)";生命周期设计:core/docs/architecture/nfs-native-lifecycle.md。平台范围
native。LAYERFS_SANDBOX只接受不设、docker、native;native在非 macOS 上由sandbox.create在做任何事之前拒绝为Unsupported;其他值(包括早先草案里的local)拒绝为InvalidInput,不会被悄悄当成 Docker。没有任何部署会在 Linux 上启动本地 daemon 或启用 daemon 的 NFS 路线,Linux daemon 仍走 FUSE。LAYERFS_SANDBOX=local——在没有 Docker 的 Linux VM / CI 里使用 workspace.* #402)里的 Linux 对外部署模式local已经去掉。为了不重写已验证的代码,engine/crates/layerfs-sandbox/src/backend/local/这个本地进程后端整体保留(native 用的就是它的进程、exec、进程组、挂载和记录代码),里面Local::new和两个 Linux 测试(local_runtime、local_stop_mount)只作为这份共享代码的测试留着,不再有部署入口。runtime: "local"的 Sandbox 记录(只会来自早先未合并的构建)任何部署都不使用,返回InvalidInput("never mixed … neither used nor deleted"),不删除它指向的任何东西,需要手动清理。和 2.0 统一 provider 设计的关系
作者提到 2.0 要把 Docker、原生 Windows/macOS/Linux 和其他沙箱 provider 一起设计。本 PR 可以当作其中 macOS 原生 provider 的参考实现:它把"provider 怎么启动 daemon、怎么挂载、怎么跑命令、怎么记录和回收"在 macOS 上完整走通了一遍,NFS 导出(
engine/crates/layerfs-nfs、daemon 的control/export.rs)和 provider 外壳是分开的。2.0 的 provider 接口定下来以后,我愿意按新接口调整或拆分本 PR。主要改动
engine/crates/layerfs-nfs(新):NFSv3/MOUNT 协议服务、AppleDouble 处理、写回与属性刷新,只听 loopback。engine/crates/layerfs-daemon:control/export.rs、export_helper.rs把 Workspace 以 NFS 导出并调用挂载/卸载助手;Commit 前 flush、换根后刷新;macOS 上 daemon 的进程保护(umask 077、禁 core dump)。engine/crates/layerfs-node:runtime.rs(Docker / 本地 daemon 两种运行时)、local.rs(native Sandbox 的启动、失败清理)、discovery.rs(native 记录的读写与安全校验)、unmount.rs(Workspace 级卸载租约:daemon 等待卸载助手时不占住整个 Sandbox 的锁)。engine/crates/layerfs-sandbox/src/backend/local/:本地进程后端(macOS 用proc_pidinfo/sysctl/mount读进程和挂载,umount -f强制卸载)。engine/crates/layerfs-store-service:macOS 上 Store 只允许放在本地 apfs/hfs;native 的安装者就是 daemon 的用户。api/agent/src:LAYERFS_SANDBOX/LAYERFS_LOCAL_DIR。tools/production_loc.py --root .):本分支 109,142(去掉local部署的提交 −16)。验证
上游 CI(本 PR 头
d55acc15f):fmt and guards、ruff、clippy、test 全部通过。Linux(x86_64,cargo 1.85.1
--offline --locked,Node 22.23.3),同一台机器上对照 main582bc786f:cargo fmt --all --checktools/check_product_boundary.py(完整 /--skip-sdk)check_fuser_integrity.py、tools 单测--workspace --exclude layerfs-node --all-targets -D warnings)、node clippy--target aarch64-apple-darwin,nfs/sandbox/node/daemon/store-service,-D warnings)tools/ci_tests.py targets fast,测试二进制 / 通过 / 失败 / 忽略)native_local(native 记录、旧 local 记录拒绝)* 失败都与本 PR 无关:
layerfs-api::sdk_method_golden(两边都失败:测试机 umask 002,文件 mode 436≠420)、layerfs-node的native_admission2 例和native_discovery3 例(mock Engine 连接被拒,main 同样)、packaging.mjs(两边都失败,需要 Node 24)。本分支多出的 1 个失败是layerfs-persistence::concurrent_first_installs,已有的并发抖动:各跑 30 次,本分支 3/30、main 5/30 失败。macOS(Apple silicon,macOS 27.2,
CARGO_PROFILE_RELEASE_STRIP=none):daemon_nfs示例:✅,addon 可加载(5 个导出)。-D warnings(nfs/sandbox/node/daemon/store-service/persistence/overlay,--all-targets):✅ 0 警告。LAYERFS_SANDBOX=native:48/48(连跑两次),无挂载、无 daemon 残留。api/agent/test/verify/native_unmount_real.mjsPASS。workspace.unmount遇到一次 macOS 客户端返回Resource busy(之后的连带失败都源于这个挂载没卸掉);之后两次完整重跑均 48/48。这个偶发情况我会继续观察。复现命令:
问题反馈请附:
sw_vers、uname -m、rustc +1.85.1 -V、node -v、失败输出、mount | grep 127.0.0.1、pgrep -lf layerfs-daemon、LAYERFS_LOCAL_DIR/<sandbox>/daemon.log末尾。已知限制
exec的 background)仍依赖/proc,暂不可用。@layerfs/runtime的 Docker 侧runtime.sandbox.list/sweep看不到 native Sandbox。