Repository navigation
[Draft] feat: macOS 原生支持(长期适配分支)——内置 loopback NFSv3 导出,普通用户 mount_nfs 挂载 Workspace(无 Docker / 无 macFUSE / 无 root) - #427
jiangkoumo wants to merge 70 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.
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.
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.
…irectory attributes The macOS mount adds dumbtimer,timeo=300 and mutejukebox. Against a loopback server the round-trip estimator settles near its floor, so a soft call failed with ETIMEDOUT after a few seconds of a slow answer: a daemon paused for 20 s made a stat fail after 8.3 s, and with these options it succeeded after the pause. A dead export still fails calls within seconds (connection refused). LOOKUP answers, found or not, carry the directory post-op attributes as RFC 1813 allows, so the client need not ask for them.
…ral-AI-Lab#440) into feat/macos-native-on-main Conflicts in core/tools/check_product_boundary.py and its test: kept this branch's changes and reformatted with ruff 0.17.2 (upstream side was format-only; AST of the result equals this branch's version).
…name) into PR Ephemeral-AI-Lab#402 (local port)
…name) into feat/macos-native-on-main
… refusing them On NFSv3 the macOS client writes extended attributes into a `._<name>` companion file. The export refused creating one with NFS3ERR_ACCES, which fails every copy whose source carries an attribute. On macOS 27 that is almost every file: anything a downloader wrote carries com.apple.provenance, including the whole cargo registry, so a `cargo build` with its target in a Workspace failed in the build scripts of libsqlite3-sys and zstd-sys (std::fs::copy: Operation not permitted). A CREATE of such a regular file now makes a companion that lives only in the export's memory, under its own serial range (apple_double.rs). LOOKUP, GETATTR, ACCESS, READ, WRITE, SETATTR, COMMIT, REMOVE and a RENAME to another AppleDouble name work on it; READDIR does not list it; no mutation of it reaches the engine, so the Workspace stays clean (unmount is not refused as Dirty) and no Commit captures it. Companions are dropped when the export is released. Memory is bounded: 4096 companions of at most 1 MiB, 64 MiB in all; beyond the count a creation is refused as before. A directory, symlink, link or rename target under such a name is still refused.
|
新提交 a700af8:macOS 上 ._*(AppleDouble)文件改为只放在导出进程内存里,不再拒绝 问题 改法(新模块 core/crates/layerfs-nfs/src/apple_double.rs) 验证
|
Replace the unconditional wait after helper KILL with a five-second try_wait observation; retain unreaped children and report their PID. Read helper pipes nonblockingly with capped diagnostics, and keep the export and engine custody when a mount helper times out after spawn. Detach local Sandbox mounts while the daemon still serves NFS. Continue stopping after detach failure; keep Delete's checked detach as its fence. Bound macOS force-unmount helpers too, including stderr capture. Keep mount-table process imports macOS-only so Linux has no unused imports. Give discovery ownership acquisition one 70-second deadline, covering a single 60+5-second helper and return headroom. Preserve lock scope, ordering and Busy refusal; do not release ownership around an in-flight mutation. A real SDK lock test refuses at 60s before and succeeds after release at 62s with the new budget. Multiple queued holders can still yield Busy. Add process-helper regression tests and an opt-in real macOS mount proof. Mark the actual 60-second deadline test slow. No mount-option changes. Original kernel U-state hang remains unreproduced on this Mac. Validation: daemon/NFS/sandbox all-target tests 102 passed, 0 failed, 4 ignored; native smoke 48/48; macOS all-target Clippy including binding and Rust formatting pass. Prior real Stop fixture failed before and passed after. Existing product guard reports five binding inline-test violations. Linux cross-Clippy could not build SQLite/zstd without x86_64-linux-gnu-gcc; the cfg correction still needs native Linux confirmation. Fresh-context read-only review of the teardown fix found no P0/P1. Production LOC: 108058 -> 108240 (delta +182) Scope: first-party core Rust/runtime SQL plus api Rust/JS; core 101993 -> 102175, api 6065 unchanged. Daemon 7058 -> 7157 (+99), sandbox 4476 -> 4559 (+83). Other packages unchanged; helper relocation included. Method: tools/production_loc.py at a700af8, same counter over git archive parent a700af8 and exact staged tree 0edfb78. Excludes tests/docs/tooling and test-only source. Reference absent (retired R9c; last counted 65,417 at tree 498dd19).
Register a Workspace-scoped unmount intent and exclusive flock under the Sandbox guard, then release Sandbox ownership during the daemon exchange. Publish the known result under the Workspace lease before shared-record cleanup. Repeated requests read progress or the saved outcome, never resend. Use existing Busy/Unknown refusals with Unmounting/Retained state and elapsed milliseconds. Gate token/directory and backend reads for that Workspace while other Workspaces and discovery reads proceed. Keep the 60+5-second helper budget and restore the discovery readiness wait to 60 seconds. Explicit Sandbox close may take over orphaned or retained unmounts but still refuses unrelated active Workspaces. Preserve Stop's detach-before-termination and Delete's checked detach. Keep known daemon success on cleanup failure and report cleanupError separately. Nonblocking shared lease probes cannot block one another; known-token lookup avoids scanning historical result records. Add a real native SDK regression with paused helpers, cross-process progress, Busy/Dirty reversibility, publication during a held discovery lock, caller crash, saved failure, and explicit close takeover. The old binding fails its 900ms discovery-read assertion; the new binding completes unrelated calls in milliseconds. Update lifecycle documentation and describe interface details. Validation: daemon/NFS/sandbox all-target tests 102 passed/0 failed/4 ignored; binding 9/9; native smoke 48/48; real concurrency fixture PASS; macOS Clippy (all targets, including binding), fmt, JS syntax and diff checks pass. Linux cross-Clippy blocked by missing x86_64-linux-gnu-gcc in SQLite/zstd; existing five binding product-boundary violations remain unchanged in kind. Fresh-context review identified two recovery issues, both fixed and verified. No public method or daemon wire change; new detail shapes and close takeover semantics require maintainer alignment before upstream publication. Production LOC: 108240 -> 108557 (delta +317) Scope: first-party core Rust/runtime SQL plus api Rust/JS; core 102175 unchanged, api 6065 -> 6382, binding 3412 -> 3729; other packages unchanged. Method: tools/production_loc.py over git archive first parent 4c1648e and exact staged tree 5394d16, same counter, excluding tests/docs/tooling and test-only source. Reference absent (retired R9c; last counted 65,417 at tree 498dd19).
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 这层补一个测试? |
这是长期适配分支,会持续跟进 main 和 #402
LAYERFS_SANDBOX=local——在没有 Docker 的 Linux VM / CI 里使用 workspace.* #402 有新提交时,把 feat(sandbox): 本地进程 Sandbox 后端LAYERFS_SANDBOX=local——在没有 Docker 的 Linux VM / CI 里使用 workspace.* #402 的头 merge 进本分支。已经拉过这个分支的人直接git pull即可,历史不会被改写。-D warnings、Darwin 交叉编译、nfs/sandbox/store-service/node 单测、DSH 插件测试、Linux 上的 native Store 一侧端到端),结果写在 PR 评论里;冲突文件和解决方式也写在评论里。LAYERFS_SANDBOX=local——在没有 Docker 的 Linux VM / CI 里使用 workspace.* #402 合并进 main 之后,下一次同步 merge main 时,feat(sandbox): 本地进程 Sandbox 后端LAYERFS_SANDBOX=local——在没有 Docker 的 Linux VM / CI 里使用 workspace.* #402 那部分自然变成"已在 main",PR 的 diff 只剩 macOS 部分。dsh/下的改动(第 5 个提交的 11 个文件)改到 DSH 的新仓库提;本 PR 届时用一个普通提交去掉dsh/的改动(同样不改写历史),只保留 core + SDK。SDK 里给 dsh 用的DSH_LAYERFS_NFS_*兜底(sdk/js/src/client.js)属于 SDK,留在本 PR。怎么参与测试(Mac 最小步骤)
需要:Apple Silicon 或 Intel Mac、普通用户、Node ≥ 22.19、Xcode Command Line Tools。不需要 root、Docker、macFUSE。
遇到问题请直接在本 PR 下评论,附:
sw_vers、uname -m、rustc +1.85.1 -V、node -v、失败命令的完整输出、mount | grep 127.0.0.1、pgrep -lf layerfs-daemon,以及 Sandbox 目录下的daemon.log末尾几十行(在LAYERFS_LOCAL_DIR/<sandbox>/daemon.log)。背景
LayerFS 在 Linux 上用 FUSE 把 Workspace 呈现为普通目录。macOS 没有内置 FUSE,目前在 Mac 上只能:
本 PR 走第四条路:daemon 自己提供一个只监听
127.0.0.1的 NFSv3 服务,macOS 自带的/sbin/mount_nfs以普通用户身份挂载。 这与 Meta 的 EdenFS 在 macOS 上的做法相同(EdenFS 同样提供 NFSv3 loopback,并用"保持 mode 的 chmod"让客户端丢缓存)。不需要 root、内核扩展、虚拟机或 Docker。clonefile/拷贝方案也评估过,但会丢掉 LayerFS 的核心语义(实时共享视图、增量 Commit),没有采用。开关:SDK
LAYERFS_SANDBOX=native;DSH 插件sandbox: native。Linux 现有的 FUSE / Docker / local 路线默认行为不变(见"Linux 默认行为"一节)。架构
KernelMutation::perform,先记录回执再回复:丢失的回复不会导致变更重放。.nfs*(silly rename)与._*(AppleDouble)在 NFS 层处理,永远不进 Workspace / Commit。sync(2)),语义与 FUSE "write 返回即到 Workspace" 对齐。LAYERFS_NFS_REFRESH=0可关(关掉后要等actimeo=60才可见,Mac 实测约 77 s)。abi_major = 0、mount_id = 导出端口表示,Ready/Status/Dirty/Committed/Conflict/Unmounted/ForceUnmounted 与 FUSE 同形同义。改动(按层,5 个提交)
e63af4c10feat(nfs): a loopback NFSv3 export of one mounted Workspace (layerfs-nfs)core/crates/layerfs-nfs:自写 XDR/RPC/MOUNT/NFSv3,零第三方依赖,#![forbid(unsafe_code)];有界内核引用(默认 65 536,LRU 叶子回收、按名字重新解析);刷新标记;边界脚本登记(只能用 content/overlay/workspace/fuse,不能用 daemon)757e4480cfeat(daemon): NFS as a kernel mount entry beside FUSE (LAYERFS_KERNEL=nfs)control/export.rs(Attach/flush/Unmount 可逆 busy/ForceUnmount/保留 custody/状态/诊断);LAYERFS_NFS_{MOUNT,UNMOUNT,FORCE,FLUSH}助手;macOS 默认挂载参数;merge 后刷新;macOS 的secure_process(umask 077 +RLIMIT_CORE=0);Kernel::Attaching去掉 Linux 限定、settle挪到operations.rs;nix 打开已有 crate 的resourcefeature6c8ca0e96feat(sandbox): local Sandboxes on macOS and the SDK's native route (LAYERFS_SANDBOX=native)backend/local/darwin.rs:proc_pidinfo/proc_listallpids取进程事实,sysctl kern.bootsessionuuid取启动身份,mount/umount -f处理挂载;macOS 上 daemon 和命令沿用调用方进程的PATH(backend/local/search.rs,见下"需要作者重点看"第 6 条);SDKnative:本机 daemon + loopback NFS,命令以当前用户运行,LAYERFS_NFS_*透传;sdk/js/README.md新增 "Native Sandboxes (macOS)"1deb780d1feat(store-service): open the Store on macOS for the native route; bundled SQLite on macOSinstall.rs/kernel_guard.rs在 macOS 放行(见下);overlay/persistence 的 rusqlitebundled扩到 macOS;Cargo.lock 不变5169c307bfeat(dsh): the LayerFS Host on the native macOS route (sandbox: native, no Docker)dsh/packages/host:native 配置与默认目录(都在 dsh Seatbelt 可写根内)、Store 目录代替 Docker volume、removeNative(daemon 在跑或有活挂载就 busy,检查本身失败也拒绝,绝不在活 NFS 挂载上 rm)、overview 不问 Docker、status 多sandbox/localDir/stores;sdk/js/src/client.js的DSH_LAYERFS_NFS_*兜底;dsh/README.md新增一段生产 LOC 用
tools/production_loc.py(combined core+sdk+dsh),每个提交信息里都有 before/after/delta。没有删除或放宽任何既有测试断言;tests/kernel_guard.rs的 macOS 用例从"拒绝"改为"apfs/hfs 放行"(这是行为本身的变化,见下第 2 条)。新增第三方 crate:无(
layerfs-nfs是一方 crate;daemon 只多开nix已有的resourcefeature;sandbox 的libc原本就在)。没有 vendor/fork/patch 第三方代码。需要作者重点看的 6 处
layerfs-store-service/src/install.rs:installer_user()在 macOS 直接放行(#[cfg(target_os = "macos")]),Linux 版本与DAEMON_UID原样保留、只在非 macOS 编译。理由:Linux 上 daemon 在容器里以 root 运行,所以要求安装者同为 root(靠/proc/self属主判断);macOS 只有 native 路线,daemon 就是安装 Store 的那个用户本人在跑,没有 root daemon 可对齐,也没有/proc。daemon 自己"只打开自己拥有的 Store 文件/目录"的检查仍然生效。layerfs-store-service/src/kernel_guard.rs——与 dsh history replay: LayerFS load test over deepseek-harness's git history #352 F-020 冲突,最需要拍板:F-020 有意规定"Store 调用只在 Linux、其他平台一律Unsupported"(AGENTS.md 也这样写)。本 PR 在 macOS 上对 apfs/hfs 放行(statfs判定),其他文件系统仍拒绝;Linux 分支与其他平台不变。理由:native 路线下 daemon、SDK 与所有命令都在同一个 macOS 内核上,SQLite 的锁与 Linux 同语义(F-020 要防的是"锁跨不过内核/虚拟机边界")。若接受,AGENTS.md 的 Store 平台说明和core/docs/issues/352/DIRECT_STORE.md需要作者按 owner 口径补一条 macOS native 例外——本 PR 没有改这些 owner 文本。若更希望显式开关(例如只有LAYERFS_SANDBOX=native时才放行),可以改。layerfs-sandbox的 unsafe 策略:#![forbid(unsafe_code)]→#![deny(unsafe_code)],只有backend/local/darwin.rs的两个函数#[allow(unsafe_code)](调用libc::proc_pidinfo(PROC_PIDTBSDINFO)与libc::proc_listallpids,带SAFETY注释、长度/pid 校验);边界脚本把它登记为该 crate 唯一的审计模块(与 persistence 的file_control.rs同一形式),自测覆盖"兄弟模块仍禁止 unsafe"。为什么需要:最初用/bin/ps,但它是 setuid root,dsh 给 agent bash 的 Seatbelt 配置拒绝执行它,导致 Sandbox 起不来(Mac 实测)。按 core/AGENTS.md,新增 unsafe 例外需要 owner 授权。替代方案:Cargo.lock 里已有的libproc 0.14.11(目前是 telemetry 的可选依赖),可保持forbid,但它构建时要 bindgen + libclang。bundled扩到 macOS(layerfs-overlay/Cargo.toml、layerfs-persistence/Cargo.toml):macOS 27.2 系统 SQLite 3.54.0 不在 overlay 合格的增长配置里(unqualified SQLite physical growth profile,与 feat(macos): add native macOS support across Store, FUSE-T, Sandbox, SDK, and DSH #403 评审根因 1 相同)。Cargo.lock 不变。代价:macOS 构建多编译一份 sqlite3.c。另外 persistence 的 macOS seal 模块(file_control.rs,SQLITE_FCNTL_PERSIST_WAL)原本针对系统 SQLite,bundled 下行为是否仍符合该授权的前提,请作者确认(Mac 上相关测试已通过)。layerfs.node的LC_SYMTAB字符串池不 8 字节对齐,dyld 拒绝加载("mis-aligned LINKEDIT string pool"),可执行文件不受影响。规避:CARGO_PROFILE_RELEASE_STRIP=none。sdk/js/scripts/build-native.mjs在 macOS 27 上应该同样会遇到,本 PR 没有改它(是否改、改成什么由作者定)。PATH(backend/local/search.rs,exec.rs/create.rs改为调用search_path())。LAYERFS_SANDBOX=local——在没有 Docker 的 Linux VM / CI 里使用 workspace.* #402 的 local 后端模仿容器,daemon 和每条命令都env_clear()后只给一个写死的PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin。本 PR 在cfg(target_os = "macos")(即 native 路线)上改为std::env::var_os("PATH")——也就是 SDK 所在进程(Node)自己的 PATH,daemon 启动时同样拿到它;PATH 不存在或为空时回落到原来那串再加:/opt/homebrew/bin。仍然只传 PATH,调用方的其他环境变量一个都不传;命令自己的environment里若给了 PATH,照旧以它为准。/usr/local/bin/node。Mac 实测:DSH 会话里lfs run … "node main.mjs"在 native Workspace 里报 127——node 在~/.local/bin(指向版本管理器目录),Homebrew 在/opt/homebrew/bin,都不在写死的列表里。容器路线有镜像保证工具位置,native 路线只能用用户自己 shell 里的 PATH。dsh web(继承终端环境)→ agent 的 bash(deepseek-harness 0.2.1-rc.2 的scrubbedParentEnv()只去掉名字像 KEY/PASSWORD/SECRET/TOKEN 的变量和DSH_*,PATH 保留,再叠加DSH_*)→node "$DSH_LAYERFS_CLI" run …(SDK 和 addon 在这个 Node 进程里)→ addon 起 daemon / 执行命令。中间没有env -i;唯一清掉 PATH 的地方就是 local 后端自己的env_clear(),所以只需改这两处,不需要在 SDK/DSH 补传。cfg(not(target_os = "macos"))仍返回同一串,tests/local_runtime.rs新增用例锁住"daemon 和命令的 PATH 恰为这串,与测试进程自己的 PATH 无关";macOS 用例在tests/local_search_path.rs(#![cfg(target_os = "macos")],Darwin 交叉 clippy 覆盖编译,Mac 上运行)。LAYERFS_NATIVE_PATH),可以改成"有它用它、没有才继承"。Linux 默认行为(不变)
LAYERFS_KERNEL=nfs时 daemon 不构建导出,installed_export返回 false,FUSE 失效路径照旧;LAYERFS_KERNEL在 macOS 上恒为 NFS。config.rs的"命令身份与 daemon 身份必须不同"只在 NFS 路线放开(native 命令就是本用户);Linux 默认仍拒绝。deploy.sandbox为native时才走新分支;local/docker不变(错误信息文本多了"(or native)";local 记录多存uid/gid两个字段,读取兼容)。sandbox且 dsh 进程环境没有LAYERFS_SANDBOX=native时,所有代码路径与原来相同(原有 host/bindings-shared/sim/runtime 测试不改、全过)。Linux 验证(测试机 x86_64,cargo 1.85.1
--offline --locked,每个测试二进制 120 s 墙钟上限)对照组
mainbase= mainb5814bf3a+ #402 头8c22d1754(5f83fabe1),不含本 PR。下表是 PATH 修复折入后的结果(2026-10-23 11:05 CST),与修复前(12992df51)逐项相同,只有 sandbox 多 1 个 Linux 用例。cargo fmt --all --check(core、sdk/js/native)-D warnings,Linux,nfs/daemon/sandbox/store-service/overlay/persistence/node--all-targetscheck/clippy -D warnings(aarch64-apple-darwin,同 7 个包--all-targets,含 macOS 专用测试)check_product_boundary.py/data/tmpmktemp 权限、时序窗口等环境原因);多出的 1 个是mounted_install既有抖动(重跑 10 次:本分支 0/10、mainbase 3/10)。PATH 修复不涉及 daemon crate(daemon 不用 layerfs-sandbox)LAYERFS_SANDBOX=local(Linux FUSE,root,本分支 release 构建;走改过的 exec/create)mounts=0 daemons=0core/crates、core/tools、dsh/packages/host、sdk/js与两个 README;不含任何测试机 / VM 脚本完整命令与输出见附带的 VERIFY.md。上游没有 CI,以上均为本地运行结果。
Mac 真机结果(MacBook Air M2 16 GB,macOS 27.2,普通用户;无 root、无 Docker、无虚拟机、无 macFUSE/FUSE-T)
本 PR 的 PATH 修复之前的版本(
12992df51,与现在只差第 6 条)在 Mac 上的结果:mount_nfse2esoft,nobrowse)被接受;git/cp/硬链/符号链/Unicode 正常;.nfs*/._*不可见;Commit 约 0.2–0.5 s;Commit 视图与实时一致;busy → 释放后正常卸载;无残留LAYERFS_NFS_REFRESH=0对照 48/49,只有该项要约 77 s 才可见mount_nfs、exec/write/commit、merge/publish 全过,leftovers=0;removeNative真机检查 13/13(pgrep缺失、被 Seatbelt 拒绝、mount缺失都拒绝删除);经真实 dsh web 路由删除:检查失败 409、正常 200lfs run里不写绝对路径执行node报 127——这就是第 6 条修复的问题PATH 修复后(本 PR 头
5169c307b)的 Mac 增量复验,2026-10-23:layerfs.node可加载;sandbox 29(含新的local_search_path,在 Mac 上真实运行)、node 10,全过$PATH与调用方一致,command -v node→~/.local/bin/node,node -v→ v22.22.2(rc=0,修复前 127),brew 可用(7/7);env -i无 PATH 时回落/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/homebrew/bin(4/4)lfs run里直接node main.mjs成功,提交并发布到 main;daemon 确认为新二进制;三个已有项目均可读;回滚正常,无残留已知限制
operator.process.*(后台进程)在 macOS 仍读/proc,agent 用&起后台进程会失败;workspace.exec/lfs run不受影响。后续处理。abort=Written表示"强制卸载助手已执行",fenced=0。想请作者决定
kernel_guard/installer_user在 macOS 的放行方式是否可接受,还是希望换成显式开关?layerfs-sandbox用deny(unsafe_code)+ 一个审计模块,还是改用libproccrate?operator.process)的 macOS 支持放到后续?