Skip to content

[upstream pr #427] feat: macOS 原生适配——daemon 内置 loopback NFSv3 导出,普通用户 mount_nfs 挂载 Workspace(无 Docker / 无 macFUSE / 无 root) #57

Open

Description

@mengmengmengqiang

[upstream PR Ephemeral-AI-Lab#427] feat: macOS 原生适配——daemon 内置 loopback NFSv3 导出,普通用户 mount_nfs 挂载 Workspace(无 Docker / 无 macFUSE / 无 root)

Original body (excerpt)

jiangkoumo:feat/macos-native-on-main → main。本 PR 只做 macOS 原生适配,自成一体:它用到的本地 daemon 运行时(最早在已关闭的 Ephemeral-AI-Lab#402 里提出)已经作为本 PR 的一部分收进来,不依赖其他 PR。
分支头 d55acc15f,已 merge 到 main 582bc786f。历史只用 merge,不 rebase、不 force-push。

这个 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)。
  • Commit 前 daemon 先把客户端缓冲的写回;Commit/merge 换根后让客户端立即丢掉被替换文件的属性缓存。
  • 记录、空闲 2 分钟关闭、回收、创建意图、sandbox.close 等语义和 Docker Sandbox 一致;daemon 被杀后,下一次 sandbox.create / runtime.sandbox.reclaim 会强制卸掉它留下的挂载再删目录。
  • 文档:api/docs/agent.md 的 "Native Sandboxes (macOS)";生命周期设计:core/docs/architecture/nfs-native-lifecycle.md。

平台范围

  • macOS:唯一的新部署方式 native。
  • Linux:保持 main 的 Docker 路线不变。LAYERFS_SANDBOX 只接受不设、docker、native;native 在非 macOS 上由 sandbox.create 在做任何事之前拒绝为 Unsupported;其他值(包括早先草案里的 local)拒绝为 InvalidInput,不会被悄悄当成 Docker。没有任何部署会在 Linux 上启动本地 daemon 或启用 daemon 的 NFS 路线,Linux daemon 仍走 FUSE。
  • 早先草案(feat(sandbox): 本地进程 Sandbox 后端 LAYERFS_SANDBOX=local——在没有 Docker 的 Linux VM / CI 里使用 workspace.* Ephemeral-AI-Lab/layerfs#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 的用户。
  • SDK 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),同一台机器上对照 main 582bc786f:

检查 本分支 main
cargo fmt --all --check ✅ ✅
tools/check_product_boundary.py(完整 / --skip-sdk) ✅ / ✅ ✅ / ✅
check_fuser_integrity.py、tools 单测 ✅ ✅
CI clippy(--workspace --exclude layerfs-node --all-targets -D warnings)、node clippy ✅ ✅
Darwin 交叉 clippy(--target aarch64-apple-darwin,nfs/sandbox/node/daemon/store-service,-D warnings) ✅ —
fast lane(tools/ci_tests.py targets fast,测试二进制 / 通过 / 失败 / 忽略) 256 / 1160 / 2* / 12 247 / 1136 / 1* / 12
node 测试 native_local(native 记录、旧 local 记录拒绝) 7/7 不适用
JS(假 binding):native_deployment、calls、exec_output、trace×9、runtime×11、cli、doc-tables、backend×4 全过 全过

* 失败都与本 PR 无关:layerfs-api::sdk_method_golden(两边都失败:测试机 umask 002,文件 mode 436≠420)、layerfs-node 的 native_admission 2 例和 native_discovery 3 例(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):

  • release 构建 daemon + node addon + daemon_nfs 示例:✅,addon 可加载(5 个导出)。
  • 单测:layerfs-nfs 17 / layerfs-sandbox 37(1 个 Linux 限定忽略)/ layerfs-store-service 7,全过;layerfs-node 测试按上游设计只在 Linux 运行。
  • Mac clippy -D warnings(nfs/sandbox/node/daemon/store-service/persistence/overlay,--all-targets):✅ 0 警告。
  • SDK smoke LAYERFS_SANDBOX=native:48/48(连跑两次),无挂载、无 daemon 残留。
  • PATH 检查 11/11(沿用 PATH 7/7,无 PATH 回落 4/4);卸载租约真机回归 api/agent/test/verify/native_unmount_real.mjs PASS。
  • 第一次 smoke 是在 release 构建刚结束、负载很高时跑的,workspace.unmount 遇到一次 macOS 客户端返回 Resource busy(之后的连带失败都源于这个挂载没卸掉);之后两次完整重跑均 48/48。这个偶发情况我会继续观察。

复现命令:

export CARGO_PROFILE_RELEASE_STRIP=none   # macOS 27 + rustc 1.85.1 必须关 strip,否则 dyld 拒绝加载 layerfs.node
cargo +1.85.1 build --release --locked -p layerfs-daemon -p layerfs-node
cargo +1.85.1 test --locked -p layerfs-nfs -p layerfs-sandbox -p layerfs-store-service
mkdir -p ~/lfs-try/local && chmod 755 ~/lfs-try/local
export LAYERFS_SANDBOX=native LAYERFS_LOCAL_DIR=~/lfs-try/local LAYERFS_HOME=~/lfs-try/home \
       LAYERFS_STORE_PATH=~/lfs-try/vol/global/store.sqlite \
       LAYERFS_DAEMON=$PWD/target/release/layerfs-daemon LAYERFS_NATIVE=$PWD/layerfs.node
cp target/release/liblayerfs_node.dylib layerfs.node   # Node 只加载 .node 后缀
# 然后用 api/agent/src 的 LayerFS:init → branch.fork → sandbox.create → workspace.mount(sandbox, branch) → exec → commit → unmount → sandbox.close

问题反馈请附: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 末尾。

已知限制

  • macOS 上后台进程(exec 的 background)仍依赖 /proc,暂不可用。
  • 大量小文件写入比直接写 APFS 慢约 6 倍。
  • @layerfs/runtime 的 Docker 侧 runtime.sandbox.list/sweep 看不到 native Sandbox。
  • 只在 Apple silicon + macOS 27.2 上验证过。

Sync note

这是只读镜像,不是上游仓库的替代。上游 issue/PR 仍是唯一真源;本镜像不复制评论、不复制代码分支。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    upstream-prMirrored upstream pull request as an issueupstream-syncMirror of Ephemeral-AI-Lab/layerfs upstream

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions