Repository navigation
#527: Sandbox platform consolidation and twelve-cell platform pass - #528
Open
yifanxuaaa wants to merge 37 commits into
Open
yifanxuaaa wants to merge 37 commits into
yifanxuaaa wants to merge 37 commits into
Conversation
The Docker create path in sandbox.rs and the native/namespaces path in local.rs each built `<prefix>[<store>.]<project>-<id>` themselves. Both now call one `sandbox::sandbox_name`, so a native or namespaces Sandbox's directory and a container cannot drift apart. The name is unchanged. Checks: cargo +1.85.1 clippy -p layerfs-node --all-targets -D warnings on macOS and Linux (pass); Linux native_local 7/7 and native_discovery 6/6 (test binaries run in the runtime image, which has Node). Production LOC: 123141 -> 123133 (delta -8) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent 81f8758 and the staged tree daf6518; after: core 41405; engine 77504; api 4224; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The kernel route was named in three places: layerfs-node's native deployment set LAYERFS_KERNEL, the native provider checked and re-set it, and the namespaces backend only checked that the caller had set it to fuse. Now only the provider names it: the native provider sets fuse on Linux and nfs on macOS, the namespaces backend sets fuse when it starts the helper, and a caller that names LAYERFS_KERNEL is refused at Validate (Protocol) before anything is made. layerfs-node passes only the NFS helper overrides. The daemon keeps its check as an assertion: at startup it refuses any LAYERFS_KERNEL other than fuse or nfs, and anything but nfs on macOS, instead of silently serving another route. Tests: native_provider gains the refusal and the route the daemon receives; namespaces_lifecycle no longer passes LAYERFS_KERNEL (it would now be refused). Checks: cargo +1.85.1 clippy -p layerfs-sandbox -p layerfs-daemon -p layerfs-node --all-targets -D warnings on macOS and Linux (pass); native_provider on macOS and on Linux as root (pass); Linux native_local 7/7. namespaces_lifecycle needs root with cgroup v2 and user.max_user_namespaces=0: run in the platform pass's Linux VM. Production LOC: 123133 -> 123145 (delta +12) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent 133145f and the staged tree 8c97c6f; after: core 41405; engine 77516; api 4224; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…step 9a) A move, with no behaviour change (F8: provider/ was not a leaf). - provider/capabilities.rs and provider/options.rs move to types/; LaunchPhase joins LaunchRequest in types/options.rs; SandboxId moves to types/mod.rs. - LaunchFailure, which holds a backend Sandbox, moves to backend/runtime.rs. - The From conversions between DockerRecord/SandboxRecord and Docker's SandboxRequest move to backend/docker/container_types.rs, so types/ depends on no backend except through RuntimeError::Docker. - The provider module is gone; its items are re-exported at the crate root (layerfs_sandbox::provider::X becomes layerfs_sandbox::X) and every caller in layerfs-sandbox, layerfs-node and layerfs-api (sources, examples, tests) is updated. Root names do not collide. - Step 6's KERNEL_ROUTE constant moves from native/mod.rs into native/provider.rs (the product boundary guard keeps mod.rs files to declarations); PROVIDERS.md (both copies) describe steps 6 and 9a. No tests deleted. Checks: cargo +1.85.1 clippy -p layerfs-sandbox -p layerfs-node -p layerfs-api -p layerfs-daemon --all-targets -D warnings on macOS and Linux (pass); tools/check_product_boundary.py (PASS); Linux tests (run as root in the runtime image): native_provider 1, native_platform 3, namespaces_protocol 5, namespaces_rootfs 12, docker_provider 7, engine_lifecycle 12, runtime_enum 1, container_labels 3, docker_naming 1, native_local 7, native_discovery 6, layerfs-api sandbox_operations 7 (all pass). tools/test_check_product_boundary.py has one pre-existing error (test_path_violations), the same on the base commit. Production LOC: 123145 -> 123137 (delta -8) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent 527c32f and the staged tree 635fe05; after: core 41405; engine 77508; api 4224; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tep 2)
F3: on a `vm:<provider>` home each project's Store is in that project's
own VM, and the home keeps no Store of its own. Calls that name a Store,
a remote or the home rather than one project crashed or ran in the wrong
process: `store.create`, `store.start_load` and `start_init({ store })`
called `platform.init(settings)` with no project and the VM's `init`
threw a plain TypeError on `project.name`; `vm.storePath()` ignored the
volume, so every Store of the home mapped to one guest file; calls with
no project fell back to this host's in-process addon with the guest
home; a PostgreSQL project's client opened the VM's SQLite Store.
Now the VM platform declares `homeStores: false`, and:
- every `runtime.store.*` call except `list`, every `runtime.remote.*`
call and `runtime.postgresql.status` refuse `Unsupported` (naming the
call and the reason) before any effect, through one guard in index.js;
- `project.start_init` with `{ store }` refuses the same way in prepare;
- `vm.init` checks its project and refuses a call without one;
- `vm.storePath` refuses instead of mapping every Store to one file;
- the VM `deploymentEnv` refuses a project of a PostgreSQL Store before
its client is made;
- `store.list` still lists the home's records, with null stats and no
call (storeFields moved inside locals' try).
api/docs/runtime.md documents the refusals.
Tests: new `vm_placement.mjs stores` (every refused call, the hooks, the
PostgreSQL client, no provider call made, `store.list` answers); it
fails on the parent with F3's TypeError. No tests deleted.
Checks (each under the 120 s `to` wrapper, after `npm install` in api/,
which only linked the three workspaces offline):
- node api/runtime/test/runtime.mjs: all 15 cases PASS
- node api/runtime/test/vm_placement.mjs settings flow two bundle delete
hosts stores apple-container ssh cleanup: all 10 PASS
Production LOC: 123137 -> 123155 (delta +18)
Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19).
Method: tools/production_loc.py --root on the first parent c6f5dd2 and the staged tree 1735769; after: core 41405; engine 77508; api 4242;
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F4: the local deployment (clients.js) never set LAYERFS_SANDBOX, _SANDBOX_BACKEND or _LOCAL_DIR, so whatever the runtime's own process had in its environment reached every project client it made (a host LAYERFS_SANDBOX=native turned a local home's Sandboxes native). The `vm:` deployment read `process.env.LAYERFS_SANDBOX_BACKEND`, so a host value of `native` replaced the VM's `namespaces` backend. Now the local deployment, for SQLite and PostgreSQL projects, sets LAYERFS_SANDBOX=docker and unsets LAYERFS_SANDBOX_BACKEND, _LOCAL_DIR and _IMAGE_ARCHIVE (the native-only settings) for the client it makes; the VM deployment sets SANDBOX_BACKEND=namespaces always. Setting names are unchanged (D3's one-setting rename is step 11). api/docs/runtime.md says so and drops "an explicit LAYERFS_SANDBOX_BACKEND wins". Tests: new `runtime.mjs backend` (ambient native/namespaces settings, the client's deploy says docker and carries no native-only field, the ambient values are restored); `vm_placement.mjs flow` now sets a host LAYERFS_SANDBOX_BACKEND=native and still expects namespaces. Both fail on the parent. No tests deleted. Checks (each under the 120 s `to` wrapper): - node api/runtime/test/runtime.mjs: all 16 cases PASS (with backend) - node api/runtime/test/vm_placement.mjs: all 10 cases PASS Production LOC: 123155 -> 123158 (delta +3) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent 27d1784 and the staged tree ed50744; after: core 41405; engine 77508; api 4245; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… do not run on (#527 step 8) Placement settings accepted any implemented VM provider on any host, and the agent sent LAYERFS_SANDBOX=native (or the namespaces backend) from a Windows process to its WSL2 worker's Linux addon, untested (§3.1 matrix). - settings.js: each VM provider lists the host systems it runs on, and `resolveSettings` refuses another host `Unsupported` before anything is made: apple-container on macOS only; microsandbox (libkrun microVMs) on Linux (KVM) and macOS (Apple silicon); ssh on Linux and macOS. No provider runs on Windows: the VM placement maps host folders into the guest as POSIX paths (vm/index.js `path`). - agent client.js: on Windows `deployment()` refuses LAYERFS_SANDBOX=native, naming LAYERFS_SANDBOX_BACKEND=namespaces when that is set, `Unsupported`; a Windows Sandbox is Docker through the WSL2 worker. Docker deployments are unchanged. api/docs/runtime.md and agent.md say so. Tests: `vm_placement.mjs settings` covers each provider on each host; `native_deployment.mjs` fakes process.platform=win32 and checks both refusals, an unchanged Docker deployment, and that sandbox.create makes no binding call. Both fail on the parent. No tests deleted. Checks (each under the 120 s `to` wrapper): - node api/runtime/test/runtime.mjs: all 16 cases PASS - node api/runtime/test/vm_placement.mjs: all 10 cases PASS - node api/agent/test/native_deployment.mjs, calls.mjs, exec_output.mjs, packaging.mjs (surface lifecycle copy sandbox worker-route worker-per-project transport-order), trace.mjs (all 9 cases): PASS - NOT_RUN (need the real addon or the E2E launcher's Store, not built here; they fail the same way on the parent): binary_write.mjs, init_refusals.mjs, reinit.mjs, packaging.mjs dispatch Production LOC: 123158 -> 123165 (delta +7) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent 18b0810 and the staged tree 485ce56; after: core 41405; engine 77508; api 4252; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#527 step 10d) A move, with no behaviour change: project/init.js chose how to run and judge an Init by comparing `platform.name` with "macos" and "windows" (runInit and runInto, two places each). The platforms now declare it: - macos `initCounts: false`: Init runs in its own container, whose counts this process cannot read, so Init is not watched; - windows `uncertainInit: true`: any failed Init is kept and recorded in init-failures.jsonl, never cleaned up on a guess. Absent, as on linux and vm:, Init is watched and only `Unknown` is uncertain, as before. `platform.name` stays in init-failures.jsonl and in messages. api/docs/runtime.md lists the two capabilities. No tests deleted or changed. Checks (each under the 120 s `to` wrapper): - node api/runtime/test/runtime.mjs: all 16 cases PASS - node api/runtime/test/vm_placement.mjs: all 10 cases PASS Production LOC: 123165 -> 123167 (delta +2) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent 4083e75 and the staged tree b7021c5; after: core 41405; engine 77508; api 4254; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rows of #527 §4 F7 that no later step changes. Not here: the reopen text (agent.md, step 4), the CLI settings list (help.js, step 11) and the macOS-native Store sentence in AGENTS.md (owner decision D5). - api/docs/agent.md: `LAYERFS_SANDBOX=native` is Linux or macOS, not macOS only; the section, now "Native Sandboxes (Linux and macOS)", says Linux runs a root FUSE daemon with commands as 501:20 and no longer says Linux keeps Docker only. Namespace Sandboxes: the root is an overlay of the Sandbox image (LAYERFS_IMAGE, unpacked from LAYERFS_IMAGE_ARCHIVE into LAYERFS_LOCAL_DIR/images), not the VM's userland; `image` is reported as the digest, not `local`; the stale limits (VM userland, read-only binds) give way to the real one (compressed image layers are refused). - AGENTS.md: the VM providers are microsandbox, apple-container and ssh, verified with stand-in providers (real runs: the #527 platform pass); the calls are `project.start_init` and `project.delete`, not `project.init`/`project.remove`. - api/docs/runtime.md: the same "verified" and `project.remove` corrections, and the anchor `#placement-one-linux-vm-per-store`. - layerfs-api/PROVIDERS.md and the layerfs-sandbox Windows README list `Runtime::Namespaces` among the variants. - core/docs/architecture/76-native-mount-session.md and nfs-native-lifecycle.md link into engine/crates; three targets had also moved: bridge control_native.rs is control_mount.rs, overlay lifetime/native.rs is lifetime/kernel.rs (with `revoke_kernel_mount`), and maintenance/native.rs was removed by 1ad1e90, which the text now says. Checks: every relinked target exists (scripted check); node --test api/cli/test/cli.test.mjs (reads agent.md's tables): 75/75 PASS under the 120 s `to` wrapper. No tests changed. Production LOC: 123167 -> 123167 (delta 0) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent 7080ac9 and the staged tree 11f4b54; after: core 41405; engine 77508; api 4254; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found by the #527 platform pass: the namespaces backend refused the Sandbox image the twelve cells run in (layerfs-352-runtime, Debian based), so linux-namespaces and every vm: placement could not start a Sandbox on it. - Its base layer has an entry for `./`, the root itself; the reader refused it as an empty path (tar.rs). A directory entry whose name is the root is now skipped: the root already exists. - A symbolic link whose target does not fit the header (an absolute, non-ASCII path under /etc/ssl/certs) carries it in a pax `linkpath` (or GNU `K`) record, and the reader checked that as a relative path, unlike the same target in the header's own link field. Extended-header link targets are now taken as the header's are: a symbolic link's may be absolute (it is never followed while unpacking, and a later write through it is still refused), a hard link's is still checked when its kind is decided. A pax `path` is still checked like every name. Test: namespaces_rootfs gains a_debian_layers_root_entry_and_long_absolute_link_unpack (root entry, pax linkpath with an absolute non-ASCII target, and a pax path still refused). Checks: cargo +1.85.1 clippy -p layerfs-sandbox --all-targets -D warnings (Linux, pass); namespaces_rootfs 13/13 (Linux, runtime image). Production LOC: 123167 -> 123188 (delta +21) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent 9952d16 and the staged tree 20eef63; after: core 41405; engine 77529; api 4254; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found by the #527 platform pass: on vm:ssh every project made from a Mac held a file `._.`, so each cell's exact-names check failed. The SSH provider copies a folder (an Init source, the bundle) as one `tar` stream (ssh.js copy), and the Mac's tar writes an AppleDouble `._<name>` entry for every entry with macOS metadata (com.apple.provenance is on every file a sandboxed process makes). A Linux host's tar creates those as ordinary files. The pack side now runs with COPYFILE_DISABLE=1, which stops them; it does nothing on Linux. Test: vm_placement ssh copies a folder whose entries carry com.apple.provenance and checks the stream's entry names. The fake SSH host now logs each copied stream's entry names (copies.jsonl), because the Mac tar that unpacks there folds `._` entries back into attributes and hides them. The test fails without the fix (`._.`, `./._a.txt`) and passes with it. Checks: api/runtime/test/vm_placement.mjs, all 10 modes, and api/runtime/test/runtime.mjs, all 16 modes (macOS, pass). Production LOC: 123188 -> 123188 (delta 0) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent 24c3c5f and the staged tree 4a074e2; after: core 41405; engine 77529; api 4254; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F5: reopen was asymmetric. A namespaces record under a native deployment was refused, but a native record under a namespaces deployment (LAYERFS_SANDBOX=native, LAYERFS_SANDBOX_BACKEND=namespaces) was reopened as a native Sandbox, since local_record checked only LAYERFS_SANDBOX. local_record now also requires the native backend, so each backend reopens only its own records and refuses the other's with InvalidInput, deleting nothing. Compatibility break: a native Sandbox record left in a home whose deployment is now namespaces, including every vm: home (whose deployment names namespaces since step 3), is no longer reopened or reclaimed by that deployment. Its message says to remove what it left by hand and delete the record, as for any other runtime's record. Test: native_local gains a_native_record_is_not_reopened_by_a_namespaces_deployment (refused, record and directory kept; then reclaimed by a native deployment). agent.md's native and namespaces record paragraphs say so. Checks: cargo +1.85.1 clippy -p layerfs-node --all-targets -D warnings on Linux and macOS (pass); Linux native_local 8/8 (runtime image). Production LOC: 123188 -> 123194 (delta +6) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent 781e1c0 and the staged tree c8cdfd3; after: core 41405; engine 77535; api 4254; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
api/runtime/test/verify/cells/ runs #516's twelve R7 cells (C01-C12, bodies byte-exact from benchmark/cluster-two/cells/workloads.py at 196c714^, each body's sha256 recorded) through the current SDK on one platform row per invocation: macos-docker, macos-native, linux-docker, linux-native, linux-namespaces (run as root in a Linux VM), vm-apple-container, vm-microsandbox, vm-ssh, plus the documented refusals. Per cell (#516 class B): a fresh project and Store (Init from an empty folder; a lane forked from main, since Init makes no Branch), an untimed setup (the committed 64 MiB base for C09-C11), one untimed identical run on an earlier mount unmounted with discard, then under a host-wide lock a fresh mount, workspace.exec of the body, Commit and unmount, each timed; then a verifier on a further fresh mount checks exact names, sizes and contents. One receipt JSON per cell with every timing, the 1-minute load, and the bundle, image, kernel, vCPU and memory identities; the project and everything it made are removed, also on failure. Each invocation is bounded at 115 s. Test-only; no product change. Production LOC: 123194 -> 123194 (delta 0) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent 38ad8ab and the staged tree a0901d2; after: core 41405; engine 77535; api 4254; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found by the #527 platform pass's refusal check: on macOS, LAYERFS_SANDBOX=native with LAYERFS_SANDBOX_BACKEND=namespaces is documented as refused Unsupported (#527 §3.1), and the binding does refuse it so, but only when LAYERFS_IMAGE and LAYERFS_IMAGE_ARCHIVE are set: without them the client refused InvalidInput "deploy.image is not configured" first, hiding the reason. The client now refuses namespaces off Linux with the binding's own Unsupported message before it reads the image settings, as step 8 does for Windows. Test: native_deployment.mjs checks the macOS refusal with no image settings (fails before this change), and fakes Linux for its existing namespaces pass-through cases, which now run on any host. Checks (macOS): api/agent/test/native_deployment.mjs, calls.mjs (39 cases) pass. Production LOC: 123194 -> 123197 (delta +3) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent 50e2c2b and the staged tree c57a7dc; after: core 41405; engine 77535; api 4257; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#527 §10 listed, from code only: macOS releaseStore was a no-op although the macOS worker caches open Stores, so removing a Store's volume (project.delete, store removal) left the worker holding the deleted Store file open until its quiet minute ended; a call in that window would act on the unlinked file. Windows and vm: already ask their worker to close it first. macOS now does the same: it sends the worker `releaseStore` for the volume's Store path, refuses an invalid volume name, and does nothing when no worker endpoint exists or the worker does not answer (nothing holds the Store then). Test: runtime.mjs macos-worker checks the call reaches the worker with the Store path, and the refusal. Checks (macOS): api/runtime/test/runtime.mjs, all 16 modes, pass: surface home-mode projects init state events leftovers sandboxes volumes transport-order macos-worker worker-frames docker-default worker-docker stores backend Production LOC: 123197 -> 123206 (delta +9) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent cb7dc81 and the staged tree 7ad794d; after: core 41405; engine 77535; api 4266; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found by the #527 platform pass (linux-namespaces, all twelve cells): every sandbox.close failed before anything was sent, with "the namespaces Sandbox runtime needs the layerfs-sandbox-helper executable beside the daemon (or LAYERFS_SANDBOX_HELPER set)", and each Sandbox's init, init-inner and daemon were left until their idle close. create found the helper beside the deployment's daemon executable, but reopen and reopen_stopped (which close goes through) looked only at LAYERFS_SANDBOX_HELPER and beside the current executable; for the SDK in process that is `node`. Only the vm: worker sets the variable, so only SDK-only Linux deployments hit it. create now copies the helper into the Sandbox's own bin/ beside its daemon copy (both 0755, deleted with the directory), and reopen runs that copy, the helper the Sandbox was started with. A Sandbox made before this change falls back to the previous lookup. Test: namespaces_lifecycle's reopen case checks the copy. Checks: cargo +1.85.1 clippy -p layerfs-sandbox -p layerfs-node --all-targets -D warnings (Linux, pass); as root in the Lima VM lfs527-linux (Ubuntu 24.04, 6.8.0-147, cgroup v2, user.max_user_namespaces=0): namespaces_lifecycle 5/5 and namespaces_userns 1/1 (the limit is 0 again afterwards) — also #527 §10's root check at this commit. Production LOC: 123206 -> 123217 (delta +11) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent c8f5d83 and the staged tree 25f2ce2; after: core 41405; engine 77546; api 4266; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… step 7) Owner decisions D2 and D6 (2026-11-01). A behaviour change only in that a variant nobody could construct on Windows is gone. - F2: Runtime::Windows, its WINDOWS refusal and its arms in every Runtime method go, with backend/windows/README.md. Runtime::capabilities keeps its Result signature. Both PROVIDERS.md files now say Windows has no backend and runs the Linux build through the WSL2 worker or a VM. - F2: layerfs-node's cfg(not(any(linux, macos))) native_runtime in local.rs is removed: mod local is itself built on Linux and macOS only, so it was dead on every target. The similar branches in layerfs-node's sandbox.rs and discovery.rs stay: they are live on any other target that builds the binding, which is what they refuse. - F1: layerfs-sandbox now builds for x86_64-pc-windows-msvc without warnings. After step 9a only the shared types are built there (backend stays cfg(unix)); CommandIdentity::check, ExecRequest::check and the STOPPED re-export, read by the backends only, are cfg(unix). - A build-only windows-build job in .github/workflows/ci.yml runs `cargo clippy -p layerfs-sandbox -D warnings` on windows-2025. It reports only: continue-on-error, no test, not a required check. layerfs-node and layerfs-api are not built for Windows: their dependency layerfs-store-service does not compile there (14 errors: unix permission modes, MetadataExt::uid, nix::sys::statvfs and layerfs_persistence's STORE_HEADROOM_BYTES, which is not built for Windows), and layerfs-api's Sandbox control uses the Unix-only Runtime. On this Mac their C build scripts (blake3's ml64.exe, zstd-sys, libsqlite3-sys) also need a Windows C toolchain; a check-only stand-in compiler was used to reach the Rust errors. Deleted test: native_platform::windows_is_refused_before_any_effect (it drove the removed variant; nothing it guarded survives). Checks: cargo +1.85.1 fmt -p layerfs-sandbox -p layerfs-node --check (pass); clippy -p layerfs-sandbox -p layerfs-node --all-targets -D warnings on macOS and Linux (pass); cargo +1.85.1 clippy -p layerfs-sandbox --target x86_64-pc-windows-msvc -D warnings (pass; rust-std for that target installed with rustup); tools/check_product_boundary.py (PASS); tools/test_check_product_boundary.py 34 pass, the pre-existing test_path_violations error unchanged; Linux tests as root in the runtime image: native_platform 2, runtime_enum 1, native_provider 1 (pass); macOS native_platform 2, runtime_enum 1 (pass). Production LOC: 123217 -> 123204 (delta -13) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent 44d41ef and the staged tree 9435d6c; after: core 41405; engine 77533; api 4266; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A move, with no behaviour change (D1 option C, D7).
- backend/docker, backend/native and backend/namespaces move to
platform/{docker,native,namespaces}; backend/linux and backend/macos move
under platform/native/, the only code that reads them, and so do the two
local helpers backend/listener.rs and backend/streams.rs.
platform/namespaces keeps importing native's lifecycle as a sibling; its
mod.rs doc comment now says so (F9).
- backend/runtime.rs and backend/helper_spec.rs move to the crate root;
backend/json.rs and backend/strings.rs become json.rs and
json/strings.rs. The design listed json/mod.rs, but the product boundary
guard keeps mod.rs files to declarations, so the parser keeps its own
file beside the folder (as layerfs-content's filesystem/validate.rs does).
- backend/identity.rs becomes ContainerId::random in types/identity.rs
(Linux and macOS, as before); backend/mod.rs's declarations move to
lib.rs and the new platform/mod.rs.
- Modules are private. The public paths are the crate root and one module
per backend: layerfs_sandbox::docker, ::native (Linux, macOS) and
::namespaces (Linux), re-exported from platform/ so no external path
names it. The root also re-exports Docker, DockerProvider,
NativeMacosProvider, NativeLinuxProvider, NamespaceProvider and HelperSpec.
layerfs_sandbox::backend::X becomes layerfs_sandbox::X for each.
- Every caller is updated: layerfs-node (discovery, local, sandbox),
layerfs-api (two examples, sandbox_operations, provider_ownership), the
helper binary, layerfs-sandbox's example and tests.
- tools/check_product_boundary.py audits src/platform/native/macos/darwin.rs
for unsafe, and its test names the new paths (Ruff-formatted).
- lib.rs's comment, both PROVIDERS.md files and the current architecture
documents' source links (71, 76, nfs-native-lifecycle, the
sandbox-command-launcher proposal) name the new paths. Historical
core/docs/issues/** records keep theirs.
No tests deleted; no test files renamed.
Checks: cargo +1.85.1 fmt (pass); clippy -p layerfs-sandbox -p layerfs-node
-p layerfs-api -p layerfs-daemon --all-targets -D warnings on macOS and
Linux (pass); clippy -p layerfs-sandbox --target x86_64-pc-windows-msvc
-D warnings (pass); tools/check_product_boundary.py (PASS);
tools/test_check_product_boundary.py 34 pass, the pre-existing
test_path_violations error unchanged. Linux tests as root in the runtime
image: native_provider 1, native_platform 2, namespaces_protocol 5,
namespaces_rootfs 13, docker_provider 7, engine_lifecycle 12,
runtime_enum 1, container_labels 3, docker_naming 1, engine_protocol 18,
engine_keepalive 4, local_runtime 11, native_local 8, native_discovery 6,
layerfs-api sandbox_operations 7 (all pass). Root-only, in the Lima VM
lfs527-linux: namespaces_lifecycle 5, namespaces_userns 1 (pass). macOS:
native_provider 1, native_platform 2, local_search_path 1, runtime_enum 1,
docker_naming 1, namespaces_protocol 5, layerfs-api provider_ownership 1
(pass).
Production LOC: 123204 -> 123207 (delta +3)
Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19).
Method: tools/production_loc.py --root on the first parent ab00731 and the staged tree 42efec4; after: core 41405; engine 77536; api 4266;
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tep 9c) A move, with no behaviour change: the same file names for the same roles in every backend (provider, create, sandbox, exec). - docker/container.rs becomes create.rs (create and start); its stop, delete and their one-response helper move to sandbox.rs, the former container_types.rs, beside the Sandbox they act on. - request.rs (Exec creation), inspect.rs (Exec inspection) and provider_exec.rs (the provider's noninteractive run) join in exec.rs; provider_failure.rs becomes failure.rs and archive.rs upload.rs. - docker/client/ is the Engine HTTP client: http.rs, socket.rs, body.rs, mux.rs and version.rs; headers.rs's one header grammar joins body.rs. Items the rest of Docker reads become pub(in crate::platform::docker). - native's `use linux|macos as platform` alias becomes `os`, so it no longer shadows crate::platform. - native::LocalExit goes: native's Sandbox::run returns the shared ExecExit (pid Some, stdout_bytes, stderr_bytes), the shape Docker and namespaces already return, and Runtime's native arm no longer converts. - The unused DockerInstance, NativeLinuxInstance and NativeMacosInstance aliases are removed. - PROVIDERS.md describes the roles; the architecture document 76 and the sandbox-command-launcher proposal link the new Docker paths; the product boundary test names client/http.rs. No tests deleted. Adapted: local_runtime reads exit.stdout_bytes and exit.stderr_bytes (LocalExit's stdout/stderr). Checks: cargo +1.85.1 fmt (pass); clippy -p layerfs-sandbox -p layerfs-node -p layerfs-api -p layerfs-daemon --all-targets -D warnings on macOS and Linux (pass); clippy -p layerfs-sandbox --target x86_64-pc-windows-msvc -D warnings (pass); tools/check_product_boundary.py (PASS); tools/test_check_product_boundary.py 34 pass, the pre-existing test_path_violations error unchanged. Linux tests as root in the runtime image: native_provider 1, native_platform 2, namespaces_protocol 5, namespaces_rootfs 13, docker_provider 7, engine_lifecycle 12, runtime_enum 1, container_labels 3, docker_naming 1, engine_protocol 18, engine_keepalive 4, local_runtime 11, native_local 8, native_discovery 6, layerfs-api sandbox_operations 7 (all pass). Root-only, in the Lima VM lfs527-linux: namespaces_lifecycle 5, namespaces_userns 1 (pass). macOS: native_provider 1, native_platform 2, local_search_path 1, runtime_enum 1, docker_naming 1, namespaces_protocol 5, engine_protocol 18, layerfs-api provider_ownership 1 (pass). Production LOC: 123207 -> 123182 (delta -25) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent ad7cb42 and the staged tree 34c14ee; after: core 41405; engine 77511; api 4266; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cb7dc81 refused LAYERFS_SANDBOX_BACKEND=namespaces whenever this process was not on Linux, and step 8 refused native and namespaces whenever it was on Windows. A client that sends its calls to a worker (a vm: placement's VM, or the macOS route) has its Sandboxes made on the worker's Linux host, so every vm:* row run from macOS was refused at sandbox.create ("namespaces is Linux only"). Found by the vm:lima real proof (vm_real.mjs, wt-vm), which passed VM create, bundle, worker and Init of both projects and then stopped there. sandbox.create now passes the client's Sandbox host to deployment(): Linux when its transport is a worker (the worker client says so), this process's platform otherwise. Both refusals read that host. Deleted tests: none. native_deployment.mjs adds the worker-host case. Checks (each bounded at 110 s): native_deployment, calls (39), exec_output ok; vm_placement settings, flow, two, bundle, delete, hosts, stores, apple-container, ssh, cleanup all PASS; cli.test.mjs 75/75. Production LOC: 123182 -> 123184 (delta +2) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent 2d6770f and the staged tree 39a98cb; after: core 41405; engine 77511; api 4268; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
step 10a) F10, D4 and D7: Docker in @layerfs/runtime had no home. Its argv was built in platform/shared.js and in six files outside platform/, and each caller first checked a flag (hostDocker, hostSandboxes) that a vm: home set, while vm supplied a docker() that threw. Move only, no behaviour change: - platform/linux/index.js, platform/macos/index.js and platform/windows/ move to platform/docker/linux.js, macos.js and windows/ (D4: the three Docker placements nest under the local placement kind; platform/ keeps meaning placement, D7). - platform/docker/index.js builds platform.docker, one Docker group per Docker platform from its own CLI call: run (the host socket on Linux and macOS, wsl.exe on Windows), version, sandboxes, stores and disk. - The Docker argv moves into the group: sandboxes.js (the containers of sandbox/leftovers.js, the ps/inspect of sandbox/list.js, the rm of leftovers and project/delete.js, STOPPED), stores.js (the four Store hooks from shared.js, makeVolume from store/calls.js) and disk.js (the volume listing and removal of volume.js). - hostDocker, hostSandboxes and vm's throwing docker() go: a vm: platform has no group, and callers that find none refuse with the same Unsupported messages as before (shared.js dockerOf), or, for sweep and status, answer as before ({ host: false }, docker "not-used"). - Every helper reaches Docker through runtime.platform.docker.run, so the tests replace that one call (runtime.mjs, verify/runtime_real.mjs). api/docs/runtime.md describes the layout. No other path in the repository named the moved files (worker/server.js keeps its path; worktree_build.py copies api/runtime whole). Deleted tests: none. Checks (each bounded at 120 s): node api/runtime/test/runtime.mjs surface, home-mode, projects, init, state, events, leftovers, sandboxes, volumes, transport-order, macos-worker, worker-frames, docker-default, worker-docker, stores, backend: all pass. node api/runtime/test/vm_placement.mjs settings, flow, two, bundle, delete, hosts, stores, apple-container, ssh, cleanup: all pass. node api/agent/test/{calls,exec_output,native_deployment}.mjs: pass; packaging.mjs surface, lifecycle, copy, sandbox, worker-route, transport-order: pass; packaging dispatch: FAIL before and after this change (needs a built layerfs.node, absent in this worktree). node --test api/cli/test/cli.test.mjs: pass. python3 tools/check_product_boundary.py: PASS. verify/runtime_real.mjs: syntax only (needs the E2E launcher). Production LOC: 123184 -> 123205 (delta +21) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent 34d49eb and the staged tree 84c1e87; after: core 41405; engine 77511; api 4289; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lume (#527 step 10b) F3 (rest): the Store hooks took a project ({ name, volume }), so a Store-level call handed them a Store as if it were a project, and a vm: home's hooks read store.name as the name of a project's VM. A Store's volume was also made in two places with two behaviours: Init's makeStore (shared.js, no existence check) and store.create/start_load's makeVolume (store/calls.js, refusing a volume that exists). Now: - Every hook (createStore, storeHolders, releaseStoreHolders, releaseStore, removeStore) takes a Store { name, volume, legacy }; legacy marks the Store a project's own Init makes, named after it. - createStore replaces makeStore and makeVolume: on a Docker placement it makes the labelled volume (the project's own, labelled layerfs.project, or <prefix>store-<name>, labelled layerfs.store) and answers its name. Init, store.create and store.start_load all use it. - Behaviour change: Init now refuses Exists when its volume already exists, instead of creating over it (docker volume create reuses an existing volume); the refused Init makes and removes nothing. - The vm: hooks hold only a project's own Store (holderOf) and refuse any other Unsupported, with step 2's noHomeStores refusal, before any provider call. Step 2's homeStores guard in index.js still refuses the Store-level calls first; the hook refusal is the same refusal one layer down, not a second guard. api/docs/runtime.md describes the hooks. Deleted tests: none. Tests: runtime.mjs init expects the volume lookup before each create and adds an Init refused Exists on an existing volume; vm_placement.mjs stores adds the refusal of createStore, storeHolders, removeStore and releaseStore for a Store that is not a project's own, with no provider call made. Checks (each bounded at 120 s): node api/runtime/test/runtime.mjs surface, home-mode, projects, init, state, events, leftovers, sandboxes, volumes, transport-order, macos-worker, worker-frames, docker-default, worker-docker, stores, backend: all pass. node api/runtime/test/vm_placement.mjs settings, flow, two, bundle, delete, hosts, stores, apple-container, ssh, cleanup: all pass. node api/agent/test/{calls,exec_output,native_deployment}.mjs: pass; packaging.mjs surface, lifecycle, copy, sandbox, worker-route, transport-order: pass; packaging dispatch: FAIL before and after (needs a built layerfs.node, absent in this worktree). node --test api/cli/test/cli.test.mjs: pass. python3 tools/check_product_boundary.py: PASS. Production LOC: 123205 -> 123209 (delta +4) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent 9b307f3 and the staged tree a478c17; after: core 41405; engine 77511; api 4293; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move only, no behaviour change. Each duplicate of F11 now has one home:
- STOPPED: platform/docker/sandboxes.js; project/delete.js's copy goes.
- Docker's volumes directory and the Store file inside a volume:
VOLUMES and volumeStore in platform/docker/index.js, used by linux.js
(settings.volumes), macos.js, windows/ (storePath and releaseStore) and
the settings.volumes default.
- df -kP parsing: dfFree in platform/shared.js, used by macos.js and
windows/. The three VM providers (microsandbox.js, apple-container.js,
ssh.js) keep their own copy for now: platform/vm/ is being extended in
parallel (vm:lima, vm:firecracker), and they can adopt dfFree after that
merge.
- The worker endpoint read (macOS status and releaseStore, Windows status,
vm endpoint) and the releaseStore call (macOS, Windows, vm): readEndpoint
and releaseStore in worker/call.js. Windows' releaseStore keeps its own
read, which throws when no endpoint exists, as before.
- homeNames and removeHome of linux, macos and vm: localHome in
platform/shared.js.
Deleted tests: none.
Checks (each bounded at 120 s): node api/runtime/test/runtime.mjs
surface, home-mode, projects, init, state, events, leftovers, sandboxes,
volumes, transport-order, macos-worker, worker-frames, docker-default,
worker-docker, stores, backend: all pass. node
api/runtime/test/vm_placement.mjs settings, flow, two, bundle, delete,
hosts, stores, apple-container, ssh, cleanup: all pass. node
api/agent/test/{calls,exec_output,native_deployment}.mjs: pass;
packaging.mjs surface, lifecycle, copy, sandbox, worker-route,
transport-order: pass; packaging dispatch: FAIL before and after (needs a
built layerfs.node, absent in this worktree). node --test
api/cli/test/cli.test.mjs: pass. python3 tools/check_product_boundary.py:
PASS. The Windows platform module only loads here (no WSL on this host).
Production LOC: 123209 -> 123216 (delta +7)
Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19).
Method: tools/production_loc.py --root on the first parent 3f28127 and the staged tree 8b28c4d; after: core 41405; engine 77511; api 4300;
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#527 step 12, sweep only) Step 12's JavaScript-only part. runtime.sandbox.sweep chose the projects to reclaim by an ended container's labels and also by rebuilding its name in JavaScript (<prefix>[<store>.]<project>-), a third copy of the Sandbox name format that the binding owns since step 5 (sandbox_name). It now matches the labels alone (layerfs.project, layerfs.store, which give the project key). Behaviour change: an ended container whose labels name a project of this home but whose name does not follow that format now starts that project's reclaim; the project's own records still decide what goes, so nothing is deleted by name (the only effect is one more reclaim call). The home's containerPrefix filter on the listing stays: a Sandbox carries no home label. The rest of step 12 (local discovery of native and namespaces Sandboxes through project.list_sandboxes) is NOT in this commit: it needs a binding change. The local placement's clients run a Docker deployment (step 3), and under it project.list_sandboxes refuses InvalidInput on a native or namespaces record (discovery.rs local_record / namespaces_record, step 4), so the call cannot list those Sandboxes, and the whole listing fails for a project that has one. Test: runtime.mjs sandboxes adds a sweep that reclaims a project from a labelled container named outside the format, and one that starts no reclaim for a running container or one of no project of this home. Deleted tests: none. Checks (each bounded at 120 s): node api/runtime/test/runtime.mjs surface, home-mode, projects, init, state, events, leftovers, sandboxes, volumes, transport-order, macos-worker, worker-frames, docker-default, worker-docker, stores, backend: all pass. node api/runtime/test/vm_placement.mjs settings, flow, two, bundle, delete, hosts, stores, apple-container, ssh, cleanup: all pass. node api/agent/test/{calls,exec_output,native_deployment}.mjs: pass; packaging.mjs surface, lifecycle, copy, sandbox, worker-route, transport-order: pass; packaging dispatch: FAIL before and after (needs a built layerfs.node, absent in this worktree). node --test api/cli/test/cli.test.mjs: pass. Production LOC: 123216 -> 123214 (delta -2) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent 9a241c0 and the staged tree 16946f5; after: core 41405; engine 77511; api 4298; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…527 platform pass) What: apple-container.js copy() no longer runs `container copy`. It clones the folder into a private <home>/.vm-copy-* staging folder (APFS clone where the volume allows, else a plain copy), the guest copies it from its home bind mount (virtiofs) onto its own disk with `cp -Rp`, and the staging folder is removed whatever the outcome. A folder already inside the home is read in place. index.js and the step order are unchanged. Why: in the #527 pass every vm-apple-container cell spent 67-78 s in setup (6 of 12 FAILed for budget), against 9 s on Microsandbox. Measured, one diagnostic sample each (container 1.6.1, 4 cpus/4G VM, bundle cells-4bedac61 with the 2.7 GB uncompressed runtime image): worker.start + Init, old route: 92.6 s, of which `container copy` of the bundle 89.5 s (a direct `container copy`: 77.2 s, ~35 MB/s); VM create 0.8 s, Store volume 0.06 s, every other step < 0.1 s, Init after start 0.5 s same, this route: 8.3 s, bundle copy 4.8 s 6 MB-image bundle (h39): old 4.2 s (copy 1.4 s), new 3.4 s (copy 0.3 s) a tar stream through `exec -i` (not chosen): 23.5 s The image unpack is not in setup (it is sandbox.create, ~3 s in the cell). The contract stays: the bundle identity check and .sealed marker are index.js's and unchanged, the VM owns its copy on its own disk, no secret is on a command line (the worker env still goes by `exec -i` stdin), one attempt per step and no retry. A create-time bind mount of the bundle was not used: it would tie a VM's restart to that bundle path for the VM's life. Tests: vm_placement apple-container asserts no `container copy`, the guest `cp -Rp` from /layerfs/home/.vm-copy-*/c, the VM-owned copy, no staging folder left (also after a failed copy), and an in-home folder read in place; the stand-in maps /layerfs/home to the host home. Docs: api/docs/runtime.md step 2. Checks (each <= 120 s wall): vm_placement settings: PASS vm_placement/settings (0 s) vm_placement flow: PASS vm_placement/flow (2 s) vm_placement two: PASS vm_placement/two (2 s) vm_placement bundle: PASS vm_placement/bundle (3 s) vm_placement delete: PASS vm_placement/delete (1 s) vm_placement hosts: PASS vm_placement/hosts (2 s) vm_placement stores: PASS vm_placement/stores (0 s) vm_placement apple-container: PASS vm_placement/apple-container (2 s) vm_placement ssh: PASS vm_placement/ssh (3 s) vm_placement cleanup: PASS vm_placement/cleanup (1 s) runtime surface: PASS runtime/surface (0 s) runtime home-mode: PASS runtime/home-mode (0 s) runtime projects: PASS runtime/projects (0 s) runtime init: PASS runtime/init (2 s) runtime state: PASS runtime/state (0 s) runtime events: PASS runtime/events (0 s) runtime leftovers: PASS runtime/leftovers (0 s) runtime sandboxes: PASS runtime/sandboxes (0 s) runtime volumes: PASS runtime/volumes (1 s) runtime transport-order: PASS runtime/transport-order (0 s) runtime macos-worker: PASS runtime/macos-worker (0 s) runtime worker-frames: PASS runtime/worker-frames (5 s) runtime docker-default: PASS runtime/docker-default (0 s) runtime worker-docker: PASS runtime/worker-docker (1 s) runtime stores: PASS runtime/stores (0 s) runtime backend: PASS runtime/backend (0 s) Production LOC: 123214 -> 123233 (delta +19) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent 72bd04c and the staged tree 7ce514c; after: core 41405; engine 77511; api 4317; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ive|namespaces (#527 step 11) Owner decision D3. LAYERFS_SANDBOX names the backend; namespaces is no longer native with LAYERFS_SANDBOX_BACKEND=namespaces. - layerfs-node runtime.rs is the one validator: `backend(deploy)` answers Docker (None), Native or Namespaces from deploy.sandbox and maps the former pair for one release (native + sandboxBackend namespaces is namespaces; native + unset or native is native). Any other combination with sandboxBackend set (namespaces + native, docker + either, unset + either) is refused InvalidInput naming both settings; an unknown value of either setting is refused naming the values it takes; namespaces off Linux is Unsupported. All before any effect. `local()` and `native()` go: sandbox.rs passes the selected backend to local::start, and discovery's native and namespaces record checks read the same function (step 4's rule unchanged). - @layerfs/agent `deployment` only checks the fields each value needs: native needs daemon and localDir, namespaces (or the former pair) also image and imageArchive, Docker its Docker settings. It passes sandboxBackend on (beside Docker too) and passes an unknown LAYERFS_SANDBOX on alone, so the binding refuses them. 34d49eb's host rule is kept: the Windows and off-Linux refusals key on sandboxHost. - @layerfs/runtime names the backend: local still sets LAYERFS_SANDBOX=docker with the former setting unset; vm: now sets LAYERFS_SANDBOX=namespaces and unsets LAYERFS_SANDBOX_BACKEND, from its own settings, so neither an ambient LAYERFS_SANDBOX nor the former one reaches its clients (F4). - The CLI help lists every LAYERFS_* setting the SDK reads, grouped (Store, Sandbox per backend, the former setting, binding, trace): seven were missing. - Docs: agent.md's settings table and the native and namespaces sections, runtime.md's local and vm: deployments, layerfs-api PROVIDERS.md and AGENTS.md's VM paragraph name LAYERFS_SANDBOX=namespaces. §7.1's names (record runtime values, capabilities, helper, on-disk names, variants) do not change. No tests deleted. Adapted: native_deployment's typo loop (an unknown value is now passed on for the binding to refuse, not refused in JS); vm_placement's settings case expects SANDBOX namespaces with the former setting unset, under an ambient LAYERFS_SANDBOX=docker too. Added: native_local::one_sandbox_setting_maps_the_former_pair_and_refuses_conflicts (five refusals through a namespaces record, then the former native pair reclaims a native record); native_deployment cases for namespaces on Linux, its fields, the passed-on conflicts, a client's sandbox.create, and the darwin and win32 refusals by the new name. Checks: cargo +1.85.1 fmt -p layerfs-node --check (pass); clippy -p layerfs-node --all-targets -D warnings on macOS and Linux (pass); tools/check_product_boundary.py (PASS). Linux, in the runtime image: native_local 9, native_discovery 6, layerfs-api sandbox_operations 7 (all pass). JS: runtime.mjs all 16 modes, vm_placement.mjs all 10 modes, agent native_deployment, calls (39), exec_output, node --test cli.test.mjs (all pass). Production LOC: 123233 -> 123262 (delta +29) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent 915522d and the staged tree 49daba8; after: core 41405; engine 77533; api 4324; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…overy reads them (#527 step 12, discovery) The discovery half of step 12 (F6); the sweep half is 72bd04c. Since step 3 a local client always runs a Docker deployment, and under it project.list_sandboxes, runtime.sandbox.reclaim (and so sweep) and sandbox.create's reclaim refused InvalidInput on any native or namespaces record, so one such record failed them for the whole project. - layerfs-node: a new backend call runtime.sandbox.records lists every record of the project with its runtime (docker, native, namespaces, or local for the former runtime) and state (running, stopped, gone, unknown with a message) without consulting the deployment: a Docker record's container is inspected once at its own Engine as before, a native or namespaces record's process is read from this kernel by id, start time and boot (LocalProcess::alive), a local record is never read. Nothing is reopened, dialed or changed. discovery's record-process parsing is one function (recorded_process) shared with local_record_of; runtime::recorded names a record's backend. - project.list_sandboxes, runtime.sandbox.reclaim and sandbox.create's reclaim leave out a record the deployment's backend may not reopen (a native or namespaces record of another backend, a local record) instead of failing; Docker records are every deployment's, as before. Step 4's rule in local_record and namespaces_record is unchanged: reopening such a record by its number (mount, close) is still refused, never mixed. - @layerfs/runtime: sandbox/records.js reads runtime.sandbox.records with one backend call on the project's binding (no client, no Store open). On Linux (new platform capability localSandboxes: an SDK run as root can start native or namespaces Sandboxes on this home's Stores), runtime.sandbox.list adds each SQLite project's native and namespaces Sandboxes beside the containers (every row now carries runtime), and the Docker Store hook storeHolders adds the running ones (and unknown) of the Store's projects; releaseStoreHolders refuses Busy, naming them, before any docker rm, since only their own backend closes them. The vm: placement's storeHolders uses the same read instead of a client's list_sandboxes, so it sees every backend's record too. removeVolume's Busy says Sandbox(es), not container(s). - Docs: agent.md (list_sandboxes and reclaim leave other backends' records out; reopen still refuses), runtime.md (sandbox.list rows and runtime, Store removal holders, sweep, localSandboxes), tests/README.md. No tests deleted. Adapted (native_local): a_record_of_the_former_local_ runtime_is_never_used_nor_deleted and a_native_record_is_not_reopened_by_ a_namespaces_deployment now expect reclaim to leave the record out (reclaimed []) and check the never-mixed refusal through sandbox.close. Added: native_local::every_backend_s_record_is_listed_with_its_runtime_ and_state (stopped native, running namespaces with this test's own process, local unknown; list_sandboxes and reclaim of a Docker deployment leave them out and remove nothing); runtime.mjs sandboxes mode: list with records of three backends, one records call per project with no deployment, Store removal Busy with and without stopSandboxes naming the native Sandbox and removing nothing, a stopped one not a holder. The stand-in binding answers runtime.sandbox.records from recordList. Checks: cargo +1.85.1 fmt -p layerfs-node --check (pass); clippy -p layerfs-node --all-targets -D warnings on macOS and Linux (pass); tools/check_product_boundary.py (PASS; discovery.rs 997 lines, under the 999 ceiling). Linux, in the runtime image: native_local 10, native_discovery 6, layerfs-api sandbox_operations 7 (all pass). JS: runtime.mjs all 16 modes, vm_placement.mjs all 10 modes, agent native_deployment, calls, exec_output, node --test cli.test.mjs (all pass). Not run: a real Linux local runtime with a native Sandbox beside Docker ones; the vm: storeHolders change ran only against vm_placement's stand-ins. Production LOC: 123262 -> 123405 (delta +143) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent 89226d7 and the staged tree 2b99241; after: core 41405; engine 77639; api 4361; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Windows platform's releaseStore read the worker endpoint file with readFileSync and threw when none existed, so removing a Store (project.delete, store.remove, a failed Init's cleanup) failed on a home whose WSL worker had never started. It now does what macOS's does (and both share worker/call.js since step 10c): readEndpoint, and return without a call when there is no endpoint or the worker does not answer a ping, since then no worker has the Store cached. A running worker is still asked once to release it; an invalid volume name is still refused InvalidInput first. No tests deleted. Added: runtime.mjs mode windows-worker (in the usage line): no endpoint resolves with no call; a stand-in worker gets one releaseStore with the WSL home and the volume's Store path; an invalid volume is refused; the endpoint of a worker that has ended sends nothing. Checks: node api/runtime/test/runtime.mjs, all 17 modes (pass); vm_placement.mjs all 10 modes (pass); agent native_deployment, calls, exec_output, node --test cli.test.mjs (pass); tools/check_product_boundary.py (PASS). No Rust changed. Not run on a Windows host. Production LOC: 123405 -> 123406 (delta +1) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent d82d6a3 and the staged tree 0c157d7; after: core 41405; engine 77639; api 4362; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In the 50e2c2b pass, vm-apple-container C01 and C02 spent more than 115 s in setup (VM create plus a 2.6 GB bundle copy); run.mjs checked its deadline only after setup returned, so the outer 120 s bound killed it with no receipt and no teardown. Setup now races a timer 15 s before the deadline; when it loses, the receipt is written as FAIL (setup) and names what was left for cleanup: the run's work folder (recorded as soon as it is made) and anything named with the run's prefix. Checks: node --check; macos-native C01 with --budget-ms 15050 wrote a FAIL (setup) receipt both when the timer won and when setup finished first (then the existing "only N ms left" refusal); the one leftover work folder of the first check was removed by hand. Production LOC: 123406 -> 123406 (delta 0) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent a1f91f0 and the staged tree e2780c5; after: core 41405; engine 77639; api 4362; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The owner chose D5's recommended option (2026-11-01): kernel_guard keeps admitting a local apfs or hfs Store directory on macOS (#427), and AGENTS.md, which said direct Store access is Linux only, gains the exception: with LAYERFS_SANDBOX=native the SDK opens the Store in its own process on this one Mac, so the one-kernel, local-filesystem rule holds. Production LOC: 123406 -> 123406 (delta 0) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent a1bf3fd and the staged tree b916aa0; after: core 41405; engine 77639; api 4362; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two rows for the new providers (owner 2026-11-01: all four #516 VMs as real vm: placements). vm-lima is vmRow("lima"). vm-firecracker sets the provider's settings on this process's environment, where the runtime reads them: LAYERFS_FIRECRACKER_LIMA (default lfs527-vzkvm, the Lima vz host with nested virtualization), LAYERFS_FIRECRACKER_DIR (default /var/lib/lfs527-firecracker) and a per-run LAYERFS_FIRECRACKER_STATE_DIR, removed at teardown. Its microVM gets 2G unless LFS527_VM_MEMORY says otherwise, since the host has 4 GiB. Firecracker cells are pass/fail only (#516): their timings describe the nesting. Checks: node --check; setting names match the provider's source. Production LOC: 123406 -> 123406 (delta 0) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent 09bf9d1 and the staged tree cba36ad; after: core 41405; engine 77639; api 4362; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What: a fourth real VM provider for @layerfs/runtime's VM placement (owner requirement 2026-11-01: all four VMs of #516 as real vm:<provider> adapters). platform/vm/lima.js makes one Lima vmType: vz instance per project with limactl and implements the five adapter steps: - Store disk: a raw Lima disk (limactl disk create --format raw, which vz needs) of storeSize, made before the instance. Lima formats it ext4 on first attach. The adapter bind-mounts /mnt/lima-<disk> at /layerfs/store after every boot and checks that it is ext4. - Instance: a template written to "limactl create --name <vm> -" on stdin: Ubuntu 24.04 cloud image (or vm.image when it is an https URL or an absolute path), cpus/memory from settings.vm, the home mounted writable at /layerfs/home (virtiofs), and Node.js v22.23.4 installed by a system provision script, checked against nodejs.org's SHASUMS256.txt. - Bundle and Init sources: staged in a private <home>/.vm-copy-* folder and copied by the guest with cp -Rp onto its own disk, the same route Apple Container takes since 24681576cb. The staging folder is removed whatever the outcome. - Guest commands: root (sudo -n) over the instance's own ssh.config, with each argument quoted by ssh.js's shellEscape. The worker's secret goes in a 0600 file from stdin and never in argv. - Private stream: the instance's portForwards rule 127.0.0.1:<host> -> 7070; a last rule ignores every other guest port. - Stop/delete: limactl stop keeps everything. limactl delete --force keeps the disk, which only discardStore (limactl disk delete) removes. settings.js lists lima as macOS only (#527 step 8; vz is Virtualization.framework). index.js registers the provider. vm_real.mjs gains lima in its provider tables and VM_REAL_PREFIX (default lfs-) for its names. Docs: api/docs/runtime.md "The Lima provider (vz)". Why the disk name is short: the first real run showed a VM restart emptying the Store. Lima's boot script finds a formatted disk by the ext4 label lima-<disk>, mkfs.ext4 cuts labels at 16 bytes, so a long disk name (lfs527-lima-<id>-shop-vm-store) is never found and the disk is formatted again at every boot. The Lima disk is therefore lfs + 8 hex of the Store volume's name. The volume's name and the home are kept as the instance's LAYERFS_STORE/LAYERFS_HOME params, which inspect answers from. Lima 1.0.4 cannot start vz on this Mac at all (gvisor-tap-vsock's resolv.conf watcher opens Docker Desktop's /var/run/docker.sock symlink: "adding watcher failed ... operation not supported on socket"). brew's Lima is now 2.2.1, upgraded by the lead's vz helper. Checks (macOS 26.4.1, M3 Max, Lima 2.2.1; each <= 120 s wall): vm_placement settings|flow|two|bundle|delete|hosts|stores|apple-container| ssh|lima|cleanup: all PASS (0-4 s each) runtime surface|home-mode|projects|init|state|events|leftovers|sandboxes| volumes|transport-order|macos-worker|worker-frames|docker-default| worker-docker|stores|backend: all PASS (0-5 s each) Real proof, one sample each, VM_REAL_PREFIX=lfs527- node api/runtime/test/verify/vm_real.mjs <h39-6e1774bc bundle copy> lima: - run 1 (base tree): FAIL at the first agent sandbox.create, "namespaces is Linux only" from cb7dc81; reported, fixed by the lead in 34d49eb (cherry-picked here as 065824f44b). - run 2 (cb7dc81's check reverted locally, uncommitted): FAIL after the VM restart: Lima reformatted the Store disk (the label finding above). - run 3 (short disk names, check reverted locally): PASS, 65.1 s. - run 4 (this tree, on 065824f44b): PASS, "vm real: ok in 65.2 s", 1-min load 3.6. That run covered two projects in two vz VMs, Init through each worker, Sandboxes on the h39 image unpacked on the ext4 Store disk, user namespaces blocked, Commit, stop/start on the same port, worker restart, the leftover Sandbox reclaimed, V8 refusal, and one project removed while the other runs. Afterwards limactl list and limactl disk list held none of its VMs or disks. Production LOC: 123406 -> 123560 (delta +154) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent d610a82 and the staged tree f3af0ef; after: core 41405; engine 77639; api 4516; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What: the fifth real VM provider of the VM placement (owner requirement 2026-11-01, #516's Firecracker). platform/vm/firecracker.js runs one Firecracker microVM per project inside a long-lived Lima vz instance with nested virtualization (macOS has no KVM). The Lima instance belongs to the user, like an SSH host: - LAYERFS_FIRECRACKER_LIMA names it (default layerfs-firecracker). - LAYERFS_FIRECRACKER_DIR (default /var/lib/layerfs-firecracker) holds bin/firecracker, vmlinux, rootfs.ext4 (node and an sshd) and the guest key id_ed25519. - Before any disk or VM, the adapter checks that the instance runs, that it has /dev/kvm, those files, mkfs.ext4, ip and flock. Anything missing is named. The five steps: - Store disk: <dir>/disks/<store>.ext4, sparse and made ext4 by mkfs.ext4. It is the second drive and is mounted at /layerfs/store after boot. - VM: <dir>/vms/<vm> holds its own rootfs copy and a config for "firecracker --no-api --config-file" (no PMU: nested KVM here has none). The guest gets 2 MiB huge pages when the host has enough free; #516 found 4 KiB first-touch faults through both hypervisors made boot take 28-55 s. Each VM has a tap lfsfc<n> on 172.30.<n>.0/30, with the number taken under flock. The guest address comes from the kernel's ip= argument and the CI rootfs's MAC rule. The boot waits up to 60 s for the guest sshd (a readiness wait). - Guest commands: root over two SSH hops (Lima's, then the host's into the microVM). The bundle goes in as one tar stream (no shared folder). The worker's secret goes from stdin into a 0600 file. - Private stream: ssh -N -L 127.0.0.1:<host>:172.30.<n>.2:7070 from this machine, with ExitOnForwardFailure and ControlPath=none. A VM runs when its Firecracker process and the forward both run; when only the forward ended, start reopens it without a reboot. - Stop and delete: stop syncs and resets the guest (Firecracker exits), SIGKILLs after 5 s and removes the tap. Delete removes the VM folder and keeps the disk file unless discardStore. lima.js exports its ssh target, instance folder and tar stream for this adapter. settings.js lists firecracker as macOS only and index.js registers it. vm_real.mjs adds firecracker; there it reads the leftover Sandbox record in the guest, because Firecracker shares no folder with this machine. Docs: api/docs/runtime.md "The Firecracker provider (nested in Lima)". Found on the real host: Lima's ssh.config keeps a persistent control master. A multiplexed "ssh -N -L" handed its forward to that master and exited 0 at once, so the adapter could neither see the forward nor end it (smoke run 1; the stray forward was cancelled with ssh -O cancel). The forward now opens its own connection, and the stand-in test checks that. Host used (Lima 2.2.1 instance lfs527-vzkvm: vz, nestedVirtualization, 4 CPUs, 4 GiB, Ubuntu 24.04.5, vm.nr_hugepages=1100 set at runtime), with LAYERFS_FIRECRACKER_DIR=/var/lib/lfs527-firecracker. All downloads were checksum-verified: - Firecracker v1.17.0 aarch64 tgz, sha256 e351ebe4...2256; the binary is OK against the release SHA256SUMS. - CI kernel vmlinux-6.18.56 (ba2081f7...883f) and ubuntu-24.04.squashfs (7aeec171...fde5), from s3.amazonaws.com/spec.ccfc.min/firecracker-ci/20261028-84df49062576-0/aarch64/. - Node.js v22.23.4 linux-arm64, OK against SHASUMS256. The root filesystem is the squashfs plus Node.js plus the key, as a 6 GiB sparse ext4 image. Checks (macOS 26.4.1, M3 Max; each <= 120 s wall): vm_placement settings|flow|two|bundle|delete|hosts|stores|apple-container| ssh|lima|firecracker|cleanup: all PASS (0-8 s each) runtime surface|home-mode|projects|init|state|events|leftovers|sandboxes| volumes|transport-order|macos-worker|worker-frames|docker-default| worker-docker|stores|backend: all PASS (0-5 s each) Real, one sample each (pass/fail only per #516: these timings describe the nesting): - adapter smoke (one VM, then create/exec/remove): boot to SSH 6.6 s; the guest showed kernel 6.18.56, /dev/vdb ext4 at /layerfs/store, /dev/fuse, node v22.23.4 and eth0 172.30.1.2/30. - VM_REAL_PREFIX=lfs527- LAYERFS_VM_MEMORY=1G vm_real.mjs <h39-6e1774bc bundle copy> firecracker: - run 1: FAIL at the leftover-record check. It read this machine's home, which a Firecracker guest does not share; the test was fixed. - run 2: PASS, "vm real: ok in 87.3 s", 1-min load 3.5. Both runs tore down cleanly: no VM folders, disks, taps, firecracker processes or forwards left, and HugePages_Free back to 1100. Production LOC: 123560 -> 123808 (delta +248) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent b39550b and the staged tree da35d7f; after: core 41405; engine 77639; api 4764; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Step 11 made LAYERFS_SANDBOX=docker|native|namespaces the one setting, with the former pair (native + LAYERFS_SANDBOX_BACKEND=namespaces) kept for one release. The linux-namespaces row now sets LAYERFS_SANDBOX=namespaces, and the macos-namespaces refusal expects Unsupported three times: the new name without and with the image settings, and the former pair. Checks: node --check. Production LOC: 123808 -> 123808 (delta 0) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent a8cb674 and the staged tree 577c7f7; after: core 41405; engine 77639; api 4764; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The final pass's refusals vm-docker cell (5431c7e) sent LAYERFS_SANDBOX=docker to a vm:apple-container home: sandbox.create reached the VM's worker, whose Linux has no Docker Engine, and ended Failed at Create/Volume with Io NotFound (attempted: false, nothing made). Design §3.1 lists Docker on vm:* as impossible; it now has a clean, documented answer. The worker client reads the endpoint's vm field (written by the vm placement at launch) and refuses any call whose deployment is docker, or unset, with Unsupported naming the provider and VM, before anything is sent. The macOS and Windows worker routes (no vm field) still send Docker deployments. Deleted tests: none. native_deployment.mjs adds the vm endpoint cases. Checks (each ≤100 s): native_deployment, calls (39), exec_output ok; vm_placement flow, two, stores, lima, firecracker, apple-container, ssh PASS; runtime macos-worker, windows-worker, worker-frames, worker-docker PASS. Production LOC: 123808 -> 123811 (delta +3) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent 5431c7e and the staged tree 4907337; after: core 41405; engine 77639; api 4767; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With e9b9646, Docker on a vm: home has a documented answer, so the refusals vm-docker cell gives PASS or FAIL instead of OBSERVED: placement vm:docker Unsupported, LAYERFS_SANDBOX=docker on the VM home Unsupported, and LAYERFS_SANDBOX_BACKEND=docker InvalidInput (step 11). Checks: node --check. Production LOC: 123811 -> 123811 (delta 0) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent e9b9646 and the staged tree c5b85a5; after: core 41405; engine 77639; api 4767; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PLATFORM-PASS.md (status: dated planning checkpoint) and its receipts. At the final product commit 97d408c, all 120 cells PASS on ten rows: macOS docker and native; Linux docker, native and namespaces; vm:apple-container, vm:microsandbox, vm:lima (vz), vm:firecracker (nested in Lima vz, pass/fail only) and vm:ssh. Both refusals (macOS namespaces, Docker on a vm: home) PASS. Windows rows are NOT_RUN (no Windows host). The document lists the receipt identities and why the 5431c7e receipts count at 97d408c, which rows overlapped, the §10 checks, the decisions taken during the pass, the open findings, every commit's LOC line and the cleanup. receipts/ holds the first pass (50e2c2b, kept with its failures), the final pass, the vm-docker rerun and the Apple Container fix's proof cell. Production LOC: 123811 -> 123811 (delta 0) Scope: core, engine, api (dsh 0; reference absent, retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root on the first parent 97d408c and the staged tree a18da8a; after: core 41405; engine 77639; api 4767; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The design that the #527 platform pass implemented, written against layerfs_v2_0_0 at 81f8758: support matrix, findings F1-F11, the target Rust and JS layouts, one LAYERFS_SANDBOX setting, the step plan and decisions D1-D7. The status banner records the owner's answers (all recommended, D1 platform/namespaces/) and points to PLATFORM-PASS.md for results. Production LOC: 123811 -> 123811 (delta 0). Docs only. Scope: core, engine and api production source; counted with tools/production_loc.py (blob 686d269) at the parent and at this tree. Reference absent (retired R9c; last counted 65,417 at tree 498dd19). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 31, 2026
This was referenced Nov 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements #527 (Sandbox platform integration and consolidation) on
layerfs_v2_0_0, and adds its terminal pass: the twelve #516 cells (C01–C12) on every available platform.Result
All 120 cells PASS on ten platform rows, and both expected refusals PASS.
vm:apple-container,vm:microsandbox,vm:lima(vz),vm:firecracker(nested in Lima, pass/fail only) andvm:ssh.vm:home.Full table, receipts, identities and decisions:
core/docs/issues/527/PLATFORM-PASS.md. The design:core/docs/issues/527/README.md.What changes
layerfs-sandbox: reorganised underplatform/{docker,native,namespaces}/.Runtime::Windowsis removed, and the crate builds for Windows.@layerfs/runtime:platform/docker/;createStore;platform.name.LAYERFS_SANDBOX=docker|native|namespaces. The old pair still works for one release, and contradicting combinations are refused.Busywhile a Sandbox of another backend is running.vm:homes refuse Store-level calls;vm:workers are refused.vm:limaandvm:firecracker.Existswhen its Store volume already exists, instead of reusing it.Production LOC: 123141 -> 123811 (delta +670), counted with
tools/production_loc.py(blob686d269d). Each commit's message records its own comparison.Not covered
layerfs-store-servicedoesn't compile for Windows (14 errors).🤖 Generated with Claude Code