Skip to content

Sandbox platform: sandbox.create({ platform }) on every platform (#529) - #530

Open
yifanxuaaa wants to merge 13 commits into
claude/issue527-platform-pass-e05aa3from
claude/issue529-sandbox-platform-559e87
Open

yifanxuaaa wants to merge 13 commits into
claude/issue527-platform-pass-e05aa3from
claude/issue529-sandbox-platform-559e87

Conversation

@yifanxuaaa

Copy link
Copy Markdown
Contributor

Closes #529. Stacked on #528: the base is #528's branch, so this diff shows only #529. Merge #528 first, then retarget this PR to layerfs_v2_0_0.

What changes

A Sandbox now names the platform it runs on. LayerFS.init and where a home keeps its Stores (LAYERFS_PLACEMENT) don't change.

- sandbox.create({ image }?)             -> { sandbox, image }
+ sandbox.create({ image?, platform? }?) -> { sandbox, image, platform }
  project.list_sandboxes(...)            -> [{ ..., platform, ... }]   (every platform's Sandboxes)
  workspace.mount(sandbox, branch)       -> { ..., platform }
+ runtime.status().platforms             -> [{ id, allowed, ready, missing }]
+ layerfs sandbox create --platform <name>
  • Platforms: docker, mac, linux, apple-container, microsandbox, lima, firecracker, ssh. There is no windows platform: Windows uses docker through the WSL2 worker.
  • What a home allows:
    • macOS Docker and Windows: docker.
    • macOS native: mac.
    • Linux: docker and linux, mixed in one project on one Store file.
    • vm:X: X.
  • Refusals happen before anything is made. A known platform the home doesn't allow is Unsupported. Anything else, including windows, native and namespaces, is InvalidInput.
  • The record decides. Every later call on a Sandbox follows its record, not the client's LAYERFS_SANDBOX. That setting is now only the home's default. Old records with no platform are read with a default; nothing is rewritten.
  • Default image: a linux or VM Sandbox defaults to its archive's image. An image the archive doesn't have is InvalidInput before any effect.
  • Native records: with no LAYERFS_LOCAL_DIR, a native record must own its directory, through a mode-0600 owner marker.
  • Finding 6, fixed: a record that can't be read is left out of list_sandboxes and cleanup. It is reported as refused in runtime.sandbox.records and kept on disk.

Spec: core/docs/issues/529/README.md. Pass record: core/docs/issues/529/PLATFORM-PASS.md.

Terminal pass

  • All 108 cells pass (C01–C12 on 9 platforms), with every Sandbox made by sandbox.create({ platform }).
  • Other checks pass:
    • the mixed Linux project: one Store file (dev 64769, inode 797004) shared by the agent, the docker daemon and the linux daemon;
    • the default on ten homes;
    • 107 refusal answers;
    • old records, rerun after finding 6.
  • Not run:
    • Windows: there is no Windows host;
    • live Connect: Connect's UI change is in the layerfs-connect repository, uncommitted, and passes in its harness only.

Open findings

  • One LAYERFS_IMAGE alone can't serve both Linux platforms. The per-platform default image covers it.
  • One test needs cc.
  • discovery.rs is 985 lines, close to the 999-line limit.
  • Sandbox platform integration and consolidation #527's findings are still open.
  • Finding 7: a stopped native Sandbox whose directory is gone is not reclaimed when LAYERFS_LOCAL_DIR isn't set. It has to be removed by hand.

Production LOC

tools/production_loc.py, first-party product code. Reference absent (retired R9c; last counted 65,417 at tree 498dd1917812).

Commit Production LOC
b3efa9b move to place/ 123811 -> 123811 (delta 0)
db4b8e4 a Sandbox names its platform 123811 -> 123966 (delta +155)
6f242b7 cell runner 123966 -> 123966 (delta 0)
33733d0 back to platform/, move only 123966 -> 123966 (delta 0)
91f3161 default image per platform 123966 -> 123993 (delta +27)
4e89998 owner marker for native records 123993 -> 124051 (delta +58)
78b63e8 image the archive lacks 124051 -> 124059 (delta +8)
a3e956d finding 6 124059 -> 124168 (delta +109)
Docs and receipts commits delta 0
Total 123811 -> 124168 (delta +357)

🤖 Generated with Claude Code

yifanxuaaa and others added 13 commits November 1, 2026 06:56
The spec for #529: a Sandbox names its platform (docker, mac, linux,
apple-container, microsandbox, lima, firecracker, ssh); sandbox.create
takes it, records it, and later calls follow the record. LayerFS.init and
LAYERFS_PLACEMENT do not change. Owner decisions P1-P6, allowed platforms
per home, API and implementation changes, the Connect UI follow-on and the
terminal pass.

Production LOC: 123811 -> 123811 (delta 0). Docs only. Scope: core,
engine and api production source; tools/production_loc.py (blob
686d269). Reference absent (retired R9c; last counted 65,417 at tree
498dd19).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In @layerfs/runtime, `src/platform/` named where a home lives (docker/,
vm/), which clashes with a Sandbox's new `platform`. Moved whole to
`src/place/`; imports, test paths, fixture comments and runtime.md updated.
No behaviour changes. The Rust platform/{docker,native,namespaces} keeps
its name.

Checks: runtime.mjs (17 modes) and vm_placement.mjs (12 modes) pass.

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 71f825a and the staged tree; after: core 41405; engine 77639; api 4767.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sandbox.create({ image?, platform? }) -> { sandbox, image, platform }. The
platforms are docker, mac, linux, apple-container, microsandbox, lima,
firecracker, ssh (P4); there is no windows (P5) and linux is the
namespaces runtime (P6). The home decides which are allowed and the
default (spec §4): Linux docker + linux, macOS docker (mac with
LAYERFS_SANDBOX=native), Windows docker, vm:<provider> the provider. A
known platform the home does not allow is Unsupported, any other value
(windows, native, namespaces, unknown) InvalidInput, both before any call.
LayerFS.init and LAYERFS_PLACEMENT are unchanged.

- agent: platform.js (home, checks, each platform's deployment, the
  read-time default of an old record); create/list_sandboxes/mount return
  platform, and image null for a native Sandbox; the worker client says
  which VM its endpoint names. sandboxHost is gone (platform.js decides).
- node: create checks platform against the deployment's backend before any
  effect and records it beside runtime; reopen, close, reclaim, idle close
  and listing follow the record for every runtime, with no deployment check
  (one reopen path, #527 F5). A local record with no LAYERFS_LOCAL_DIR in
  the call is checked against its own directory's name. list_sandboxes,
  mount and runtime.sandbox.records report platform (null in an old record)
  and runtime.
- runtime: status().platforms [{ id, allowed, ready, missing }]; Linux
  settings localDir and imageArchive give its clients the linux platform;
  sandbox.list reports platform; sweep also reclaims stopped native and
  namespaces Sandboxes on Linux.
- CLI: sandbox create --platform; docs (agent.md Platforms section, the
  "There is no placement" paragraph rewritten, runtime.md Status), skill.

Behaviour change: #527 step 4 (a record is reopened only by a deployment
of its own backend) is replaced; any client of the home reopens a Sandbox
by its record.

Tests changed or deleted (native_local.rs):
- a_native_record_is_not_reopened_by_a_namespaces_deployment: replaced by
  a_native_record_is_reopened_by_its_runtime_under_any_deployment;
- a_native_record_is_used_only_with_its_deployment_s_absolute_local_dir:
  rewritten as a_native_record_s_directory_is_checked_with_or_without_a_local_dir;
- close_refused_as_mixed: replaced by close_refused_as_unused; the
  native-under-docker half of the former-local test now expects a reclaim.
native_deployment.mjs: the two sandboxHost assertions are deleted with
sandboxHost (covered by the new platform.mjs).

Checks: Clippy -D warnings on macOS and Linux (layerfs-node); cargo fmt;
check_product_boundary PASS; native_local 10/10, native_admission 2/2,
native_connection_wait 1/1 as root in Lima lfs529-linux; native_discovery
5/6 (the sixth needs cc, absent in that VM; untouched code). JS:
platform.mjs (new), calls (42 cases), native_deployment, exec_output,
packaging (7 modes), trace (9 modes), runtime.mjs (17 modes),
vm_placement.mjs (12 modes), cli.test.mjs 78/78.

Production LOC: 123811 -> 123966 (delta +155)
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 b3efa9b and the staged tree; after: core 41405; engine 77696; api 4865.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cell.mjs makes its Sandbox with sandbox.create({ platform }) and requires
the create and mount answers to name it; with LFS529_EXTRAS (C01) it then
checks the home's default and every refusal pair (spec §4), with the
project's Sandboxes unchanged. run.mjs maps each row to its platform,
allowed set and default, and adds two Linux rows that take the cell
`check`: linux-mixed (mixed.mjs: a docker and a linux Sandbox of one
project, one Store file by (dev, inode) through /proc, Commits seen both
ways, close one) and linux-oldrecords (make.mjs with the pre-#529 bundle,
then oldrec.mjs). Test files only.

Production LOC: 123966 -> 123966 (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 db4b8e4 and the staged tree; test files only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The owner kept `@layerfs/runtime`'s folder name: `src/place/` (b3efa9b,
#529 §6.6) moves back to `src/platform/`, with its imports, comments, test
paths and api/docs/runtime.md. No behaviour changes. The #529 spec records
the reversal in §6.6. The §8 cells run on bundles sealed at 6f242b7,
which differs from this tree only by this move.

No tests deleted.

Production LOC: 123966 -> 123966 (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 6f242b7 and the staged tree; after: core 41405; engine 77696; api 4865.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PLATFORM-PASS.md and its 111 receipts (receipts/6f242b72bf/): C01-C12 PASS
on nine platforms (docker on macOS and Linux, mac, linux, apple-container,
microsandbox, lima, firecracker nested in Lima vz, ssh), 108 cells; the
mixed project (one Store file, held by both Sandboxes' daemons), the
default on ten homes, 107 refusals and the old records PASS. Windows
NOT_RUN. Connect and cleanup sections follow in a later commit.

Production LOC: 123966 -> 123966 (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 33733d0 and the staged tree; docs only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PLATFORM-PASS.md gains its Connect section (harness PASS on ten cases,
narrow dark and wide light; live NOT_RUN; the changes are uncommitted in
layerfs-connect on feat/connect-ui-polish) and its cleanup record (the
three Lima VMs, pulled images, caches and the Apple Container service
removed or stopped; host free 219.9 GB).

Production LOC: 123966 -> 123966 (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 71ecd9f and the staged tree; docs only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#529)

Parent decision 5 (2026-11-01): `image` defaults per platform. `docker`
keeps LAYERFS_IMAGE; `linux` and the VM platforms use the image that
LAYERFS_IMAGE_ARCHIVE holds (its config digest) when that is set. An
explicit `image` still wins. The archive must name exactly one image when
no image is given; otherwise the start is refused before any effect.

- layerfs-sandbox: rootfs::image_of(archive) reads the archive's one
  config digest; manifest() takes an optional image.
- layerfs-node: namespaces_image() derives the image from the archive
  when deploy.image is absent.
- agent: platformDeployment() drops the home's image for linux and VM
  platforms unless the call gave one; localDeployment() leaves
  deploy.image unset when an archive is set.
- runtime: the linux platform no longer needs LAYERFS_IMAGE.
- mixed check (parent decision 3): records the Store's (dev, ino) and
  each daemon's pid and (dev, ino); creates linux with no image, then
  with an explicit image, and refuses the Docker digest for linux.
- docs: agent.md (Images bullet, settings, LAYERFS_IMAGE row), the SDK
  skill's sandbox.create default.

Tests: new namespaces_rootfs::an_archive_names_its_one_image_by_its_config_digest;
platform.mjs adds vm, linux and native-on-vm cases; native_deployment.mjs
expectations changed (the namespaces deployment has no image unless given).
No tests deleted.

Checks: Clippy -D warnings on Linux (layerfs-sandbox, layerfs-node) and
macOS (layerfs-node); fmt; namespaces_rootfs 14/14 on Linux; agent
platform, native_deployment, calls 42; runtime surface, sandboxes,
backend, docker-default; vm_placement settings, flow, bundle; CLI 78/78.

Production LOC: 123966 -> 123993 (delta +27)
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 2fe92d2 and the staged tree 0c935fd; after: core 41405; engine 77721; api 4867.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#529)

Parent decision (2026-11-01). A native or namespaces record reopened by a
client that sets no LAYERFS_LOCAL_DIR is used only when:
(a) its directory is its own real path (no symlink in it);
(b) its last component is the name this home gives that Sandbox;
(c) the directory's `owner` marker, written once at create (mode 0600,
    create-new, not followed if a symlink), equals the real path of the
    record's own directory LAYERFS_HOME/<project>/sandboxes/<n>.
Anything else is refused InvalidInput before any effect, naming
LAYERFS_LOCAL_DIR as the fix, and left in place. A record made before
#529 has no marker, so a client with no LAYERFS_LOCAL_DIR is refused on
it. With LAYERFS_LOCAL_DIR set nothing changes.

- local.rs: mark_owner() at create, before the record is saved; owned()
  at reopen.
- discovery.rs: local_record_of() calls owned() when localDir is unset;
  the namespaces_record doc no longer says only its backend uses it.
- agent.md: the native and namespaces record rules, and that a refused
  record fails the call that reads it (listing included) rather than
  being left out.
- oldrec.mjs: a reclaim with no LAYERFS_LOCAL_DIR must be refused on the
  old records and keep them; the rest keeps the home's LAYERFS_LOCAL_DIR,
  as a pre-#529 home had.

Tests: new native_local::with_no_local_dir_a_record_s_directory_must_be_real_and_owned_by_it
(a symlink above the directory, the directory itself a symlink, a wrong
last component, no marker, a marker naming Sandbox 2, a symlinked marker,
then an owned record reclaimed). Changed: two native_local tests write
the owner marker their records stand for
(a_native_record_s_directory_is_checked_with_or_without_a_local_dir,
every_backend_s_record_is_listed_with_its_runtime_and_state). No tests
deleted.

Checks: native_local 11/11 (Linux, in the Node runtime image); Clippy
-D warnings on Linux and macOS (layerfs-node); fmt.

Production LOC: 123993 -> 124051 (delta +58)
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 91f3161 and the staged tree 507fb35; after: core 41405; engine 77779; api 4867.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ct (#529)

Found by the rerun's mixed check: a `linux` Sandbox given Docker's index
digest (sha256:afa1ef9c...) was refused only at the provider's Validate
step, as Failed, after a Sandbox number was claimed (and released by the
failed start's cleanup). namespaces_image() now checks that the archive
names the given image (rootfs::holds, the same manifest read the no-image
default does), so it is InvalidInput while the deployment is built.

Tests: namespaces_rootfs::an_archive_names_its_one_image_by_its_config_digest
also checks holds() for a held and a missing digest. No tests deleted.

Checks: namespaces_rootfs 14/14 on Linux; Clippy -D warnings on Linux
(layerfs-sandbox, layerfs-node) and macOS (layerfs-node); fmt.

Production LOC: 124051 -> 124059 (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 4e89998 and the staged tree 40e814b; after: core 41405; engine 77787; api 4867.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PLATFORM-PASS.md:
- Decisions 5 and 6 rewritten (the owner marker; image defaults per
  platform), plus 13 (native on a vm: home maps to the provider), 14 (the
  Store identity receipt) and 15 (a given image the archive lacks is
  InvalidInput).
- Finding 1 is now fixed by default; new findings 6 (a refused record
  fails the call that reads it) and 7 (with no LAYERFS_LOCAL_DIR a
  stopped record whose directory is gone is not reclaimed).
- A rerun section, the receipt identities, commits and cleanup.

Receipts (7, lfs527-cell/1), all kept:
- receipts/4e8999872b/: macos-native C01 PASS; linux-mixed FAIL (Docker's
  digest refused Failed after a number was claimed; fixed by 78b63e8).
- receipts/78b63e86eb/: linux-mixed PASS, linux-oldrecords PASS twice
  (the first one's no-LOCAL_DIR step reached the name check, not the
  marker), linux-namespaces C01 PASS, vm-lima C01 PASS.

oldrec.mjs: the no-LAYERFS_LOCAL_DIR step keeps the home's container
prefix and requires the owner-marker refusal.

Production LOC: 124059 -> 124059 (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 78b63e8 and the staged tree be6fdef; after: core 41405; engine 77787; api 4867.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…call (#529 finding 6)

Parent, with the owner's go-ahead. A Sandbox record that cannot be read for
a call (a damaged file, or a local record failing its directory or owner
marker checks) is now left out instead of failing the call for the
project:
- project.list_sandboxes leaves it out; the agent's listing shape is
  unchanged (the binding's answer adds `skipped`).
- runtime.sandbox.records keeps it with state "refused" and
  refusal { code, message }. A host that cannot run a namespaces record
  (Unsupported) is no fault of the record: its process is read as before.
- runtime.sandbox.reclaim, sandbox.create's reclaim and the sweep skip it
  and leave it in place (nothing deleted, opened or stopped); reclaim and
  create answer `skipped`.
- the agent's trace writes one `phase: "skipped"` line per skipped record.
- a call naming that Sandbox (mount, exec, close, list_workspaces) is still
  refused with its own code.
- a malformed LAYERFS_LOCAL_DIR or LAYERFS_SANDBOX is the caller's setting
  and still refuses the call; a failure to inspect what a readable record
  names (Docker not answering) still fails it.
- runtime: records are read with the home's containerPrefix and, on a Linux
  local home, its localDir (no backend), so local records are checked as
  the home's clients check them; a refused record counts among a Store's
  holders.
Finding 7 stays a finding: removing a record whose ownership cannot be
checked is a manual step (agent.md, runtime.md).

Tests: new native_local::a_record_refused_when_read_is_left_out_reported_and_kept
(a good running record, a pre-#529 one with no marker read with no
LAYERFS_LOCAL_DIR, a damaged one; listing, records, reclaim, close), trace
mode `skipped`, and a runtime sandboxes check (the sweep starts no reclaim
for a refused record; holding counts it). Changed to the new contract:
native_local's reclaim assertions on refused records (SKIPPED instead of a
refusal; RECLAIMED, LEFT_OUT and the listing gain `skipped: []`),
native_discovery's three reclaim answers (`skipped: []`), runtime.mjs's
records-call assertion (a deploy of only the home's name prefix), and
oldrec.mjs's no-LAYERFS_LOCAL_DIR step. None deleted.

Checks: Clippy -D warnings on Linux and macOS (layerfs-node); fmt;
check_product_boundary; native_local 12/12, native_admission 2/2,
native_connection_wait 1/1, native_discovery 5/6 (the fork fixture's C
source is not mounted in the runtime image: finding 2) on Linux in the
Node runtime image; agent trace (10 modes), platform, native_deployment,
calls 42, exec_output, packaging (6 modes; dispatch needs the built addon);
runtime.mjs (16 modes), vm_placement (12 modes); CLI 78/78.

Production LOC: 124059 -> 124168 (delta +109)
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 7bfb4d6 and the staged tree c0bba4f; after: core 41405; engine 77884; api 4879.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PLATFORM-PASS.md: finding 6 marked fixed by a3e956d, with what now
happens to a record refused when read; finding 7 notes that such a record
is skipped and kept, and that removing it stays manual. Added a3e956d
to the commits (over #529: 123811 -> 124168, delta +357), its tests and
checks, the rerun's receipt identity and cleanup.

Receipt: receipts/a3e956dd40/linux-oldrecords-check-...-vx635i336i.json
PASS (Linux bundle build-linux-ab4a0253..., manifest 27923eca...,
oldrec.mjs 04b028c6). With no LAYERFS_LOCAL_DIR the old namespaces record
is `refused` in runtime.sandbox.records, left out of listing and skipped
by reclaim, and stays running; with the home's LAYERFS_LOCAL_DIR both old
Sandboxes list, mount, run, unmount and close.

Production LOC: 124168 -> 124168 (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 a3e956d and the staged tree c0b6e31; after: core 41405; engine 77884; api 4879.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant