Skip to content

[B] Sandbox compute plane: machines, backends, observability and lifecycle #545

Closed as not planned
6 of 6 issues completed
Closed as not planned

Description

@mengmengmengqiang

Goal

Take over B · Sandbox compute plane and implement it per the Machine + Backend model of #533:

Machine (compute ownership)
  └── Sandbox (bound to one project)
        └── Backend (docker / native / namespaces / VM / ssh)

Do observable, bounded, recoverable, cleanable first; unlock Windows/cloud only after their Decisions.

Status (2026-11-06)

Index only; delivery is tracked in the native sub-issues below. Current progress is 0 / 5 closed:

Sub-issue Area PR CI / merge State
#548 B5 lifecycle — generation CAS, exec lease, Draining, Lost/Unresolved #566 CLEAN / MERGEABLE, not merged open
#549 B6 scratch — owned path, generation check, bounded cleanup, operator dry-run #558 CLEAN / MERGEABLE, not merged open
#550 B9 worker liveness/progress — no permanently hung call #556 CLEAN / MERGEABLE, not merged open
#551 B0 contract + B8 inspect + B10 doctor #567 CLEAN / MERGEABLE, not merged open
#552 StoragePlacement — live SQLite only on its owning machine; Family 2 admits only PostgreSQL/snapshot #557 CLEAN / MERGEABLE, not merged open

All five sub-issue PRs are green and CLEAN / MERGEABLE; 0 are merged. Merge permission is with maintainers.

Acceptance extra: #587 (agent-d1-p3) carries the D1 network-namespace fix and the 0701/0700 data-root fix, stacked on #556. It is also CLEAN / MERGEABLE, not merged.

Acceptance evidence is complete for the local 2.0 scope: native and Docker close/exec E2E, per-command CLONE_NEWNET (guest has only lo, cannot reach 127.0.0.1:7070), protocol-3 handshake, 0701/0700 permissions, and the five invariants. #551's live-Sandbox-across-restart is explicitly out of 2.0 (Option A, core/docs/issues/545/551-LIVE-RESTART.md).

Architecture + cs-infra review: 0 P0, 0 P1 after fixes; the remaining P2s are non-blocking. Suggested merge order: #556 -> #587, #566 -> #558, #567 -> #557.

Diagrams

Rendered from the editable Excalidraw sources in Comic Shanns (fontFamily: 8); release assets are in issue-assets-2026-11-06. All nine PNGs were checked for dimensions and re-uploaded after the font/render fix.

Compute plane

Sandbox compute plane architecture

Sandbox compute plane lifecycle

Sandbox compute plane observability

Sandbox compute plane provider storage

Worker liveness (#550 / #556)

Worker liveness architecture

Worker liveness sequence

Scratch (#549 / #558)

Scratch lifecycle

Storage placement (#552 / #557)

Storage placement architecture

Storage placement sequence

Boundary

Main / Compute Plane — this issue:

Staging — blocked on Decisions, tracked separately:

Agent Runtime — outside this plane, tracked separately:

#553, #554 and #555 are not native sub-issues of #545 and do not affect its 0/5 progress.

Model (per #533)

  • Compute is machines.
  • native / docker are built in; VM kinds may be many (apple-container, microsandbox, lima, firecracker); an ssh host is a machine.
  • A Sandbox has a name and is bound to one project and one machine.
  • A backend is the concrete runtime inside a Sandbox.
  • Records live under machine/<machine>/sandbox/<name>/.
  • The local home record is the single authority for control-plane state; PostgreSQL carries Store records and receipts only.
  • There is no platform/ directory, and LAYERFS_SANDBOX no longer selects a backend.
  • Calls are routed by records.

Four axes

Axis What it is Layer
Abstraction what the caller talks to Machine → Sandbox → Backend
Isolation how strong the boundary is process / container / VM / remote
Scheduling who runs work and for how long Worker + long-lived Sandbox
Semantic boundary what the state contract is Store / Commit / Branch + Workspace

Isolation capability

Backend Isolation Kernel Notes
native / namespaces process shared namespaces + cgroup; not a security boundary against root
docker container shared default Linux
lima / firecracker / microsandbox / apple-container VM separate higher cost
ssh remote separate host network boundary, latency

The backend declares its isolation capability. Callers ask for a capability; they do not infer one from the backend name.

Cross-cutting invariants

Scope for this version

In:

  • Machine / Sandbox / Backend records, routing and inspect.
  • B0/B5/B6/B8/B9/B10 for the four backend families.
  • Isolation capability and scheduling class in records and read models.
  • StoragePlacement refusal and the no-live-remote-SQLite guard as 2.0.0 invariants.
  • Default isolation for sandbox.create and refusal when the requested class is unavailable.
  • requested and effective isolation in the Sandbox record.

Out:

  • No serverless execution backend.
  • No core queue or actor.
  • No agent loop or LLM.
  • No Lambda / SQS / Aurora coupling.
  • Step/serverless execution belongs to the agent runtime through the SDK, outside this plane.

Dependencies

Dependency Impact Handling
#533 G0/G1/G2 machine model, home layout, routes align first; do not write against the old platform
D2 B4 / B9 restart/transport do B8/B10/B5/B6 first; do not wait
D6/D12 B7 cloud wait for Decision
#529/#530 backend semantics align with @yifanxuaaa; do not rewrite
#508 unbounded control calls separate issue; bounded principle adopted here
#422/#423 B5/B6 covered directly here
#543/#544 controllability work basis for B8/B10

Done when

Non-goals

Design diagrams

History

This body was edited heavily on 2026-11-04 (20 revisions, 3 KB → 33 KB) while the scope moved from a control-plane subset to a compute-plane index. The last full 66-checkbox audit is historical: 6 done / 16 partial / 41 not done / 3 N/A against layerfs_v2_0_0 (d2c3f2c97). Current delivery progress is the 5 sub-issues above, 0/5 closed.

Activity

  1. mengmengmengqiang commented on Nov 4, 2026

    @mengmengmengqiang
    ContributorAuthor

    I'll take this issue. I'll start with B8, B10, B5, and B6; B9, B4, and B7 will follow once D2, D6, and D12 land.

    The assignee is @mengmengmengqiang, with the v2.0.0 and enhancement labels.

  2. added this to the LayerFS 2.0 milestone on Nov 4, 2026
  3. added
    v2.0.0LayerFS 2.0 scope, roadmap and delivery
    on Nov 4, 2026
  4. added theissue type on Nov 4, 2026
  5. changed the title [-][B] Sandbox control plane: observability, lifecycle and bounded recovery[/-] [+][B] Sandbox compute plane: machines, backends, observability and lifecycle[/+] on Nov 4, 2026
  6. mengmengmengqiang commented on Nov 4, 2026

    @mengmengmengqiang
    ContributorAuthor

    #545 is now an index

    The body has been reduced to an index: a Status block, five cross-cutting invariant checkboxes, and a per-family table. The 66-box audit is 6 done / 16 partial / 41 not done / 3 N/A (60 unchecked). All 6 checked boxes are factually correct but 0 are #545's own delivery: 4 are satisfied by merged #535/#536 work, 2 are permanent invariants, and B9-210 is the third invariant (N/A). The old 66-checkbox list is gone; status is now the open/closed state of the sub-issues.

    Delivery split (8 sub-issues):

    The status scene jhh8rntcFu was rebuilt as Status index: corrected counts (6 / 16 / 41 + 3 N/A), 60 unchecked, no "51 unchecked" footer, and an INDEX ONLY badge. It is exported to the docs branch at commit eea0e59:
    sandbox-compute-plane-status-3715063e.png / .excalidraw (Comic Shanns throughout), README bumped to v6. The body references the new image URL.

    #545 stays OPEN as the index and is not closed by this split. Close it only when #548–#554 are closed and #555 is resolved or moved to its owning area.

    Label note: the available token lacks triage, so the new issues were created unlabeled; each has a comment requesting v2.0.0 / bug / enhancement (plus future / deferred for the blocked ones).

  7. removed
    v2.0.0LayerFS 2.0 scope, roadmap and delivery
    on Nov 5, 2026
  8. 10 remaining items

  9. yifanxuaaa commented on Nov 6, 2026

    @yifanxuaaa
    Contributor

    Decision: remove "snapshot" from the compute plane (owner, 2026-11-06)

    @mengmengmengqiang, to keep the design small, "snapshot" comes out of #551 and #552. Please change your two PRs before they merge.

    1. #567 (#551 B0): drop SnapshotCapability

    All three backends set it to None, so nothing reads it. Remove:

    • the SnapshotCapability type and the snapshot field in connect/crates/layerfs-sandbox/src/types/capabilities.rs, plus their re-exports in types/mod.rs and lib.rs
    • snapshot: None in the docker, host_process and namespaces adapters
    • the "snapshot" key in the capability read model (api/crates/layerfs-control/src/machine/capability.rs) and in .agents/skills/layerfs-js-sdk/references/methods/machine.md
    • the B0-145 snapshot item in core/docs/issues/551/README.md

    None of this has merged, so keep capability schema_version at 1.

    2. #557 (#552): Family 2 admits PostgreSQL only

    The placement rule becomes:

    Store Owning machine Family 2 machine
    live SQLite admitted refused, StoragePlacement
    PostgreSQL admitted admitted

    Not changing

    The snapshot step inside the merged store push, store pull and backup create (#536) stays. It's only the name of the step that freezes a Store's bytes before the copy. backup create / backup restore and "object storage is never a live Store" are unchanged.

    The suggested merge order holds: #567 → #557.

  10. yifanxuaaa commented on Nov 6, 2026

    @yifanxuaaa
    Contributor

    Owner spec for the 2.0 compute plane (2026-11-06)

    @mengmengmengqiang, this is the spec we'll build the compute plane to. Where it differs from this issue's body, the spec wins.

    Decisions (owner, 2026-11-06):

    Proposals for review (not decided yet): trims P-1 to P-6 for #566 and #567 (§3), and the internal layout P-7 (§6.2). Please hold the trims until they're confirmed. The snapshot removal (D-1) can go ahead now.

    One correction to the body: Sandbox records live at sandbox/<name>/ (#536 C2, owner 2026-11-03), not machine/<machine>/sandbox/<name>/.

    The full spec is below. It's written against layerfs_v2_0_0 at 8b701f42f0, and it will be committed as core/docs/proposals/compute-plane-2.0.md.

    Full spec: Compute plane 2.0

    Compute plane 2.0: machines, Sandboxes, exec and the control layer

    Status: Proposal; target LayerFS 2.0.0; not a released contract.
    Written against layerfs_v2_0_0 at 8b701f42f0 (2026-11-06); repository
    links resolve on that branch. Production LOC is from that commit's
    tools/production_loc.py --files.

    This is our own spec for the 2.0 compute plane. It takes over from the
    #545 index where the two differ. Owner decisions are marked decided with
    their date. Everything else is a proposal until the owner accepts it.

    1. Decisions

    Id Decision Owner, date
    D-1 No snapshot. The capability's snapshot: Option<SnapshotCapability> field is removed from the capability contract (#567). A Family 2 machine admits only PostgreSQL Stores; the "SQLite snapshot → admitted" placement row, its test and the B12 snapshot Store kind are gone, not deferred (#557). The snapshot step inside store push, store pull and backup create stays. owner, 2026-11-06
    D-2 layerfs-control moves to connect/crates/layerfs-control, after the trimmed #545 PRs merge, as one move-only commit. Supersedes #536 C1 ("in the api layer"). owner, 2026-11-06
    D-3 Exec and cancel are one model on workspace.exec (§5). PR #569 (#555, the separate step.* family) is rejected. owner, 2026-11-06
    D-4 Per-exec network isolation is optional for the exec SDK (§4). owner, 2026-11-06 (requested)

    2. Model

     home records (the only control-plane authority)
       machine/<machine>/record.json       a machine: native, docker, lima, apple-container,
                                           microsandbox (firecracker, ssh: existing only)
       sandbox/<name>/record.json          a Sandbox: names its machine and its Store (#536 C2)
       store/<store>/record.json           a Store: sqlite on one machine, or PostgreSQL
    
     machine ──runs──▶ Sandbox ──through──▶ backend (docker | native | namespaces)
                         │
                         └──mounts──▶ Workspace ──exec──▶ command (bash -c)
    

    3. Scope: what we keep from the #545 PRs

    Production LOC is each PR's own increment over the PR it is stacked on,
    measured at its head on 2026-11-06. Every PR is stacked on #556.

    PR Area Prod LOC Proposal
    #556 worker liveness, protocol 3 +570 Keep. A dead worker is Unavailable/Unknown in bounded time; a live command is not cut off.
    #587 per-exec netns, data-root modes +6 Keep, then make the netns optional (§4).
    #558 Sandbox-owned scratch +486 Keep. Sweep Closed/Lost only, re-check ownership before delete.
    #557 placement guard +158 Keep without snapshot (D-1).
    #566 lifecycle +1,087 Trim (P-1 to P-3).
    #567 contract and read models +1,406 Trim (P-4 to P-6).
    #569 step runtime — Rejected (D-3).

    Proposals:

    • P-1 States. Keep the derived states the branch has (creating,
      running, closing, unresolved, gone) and add one: lost. A Sandbox
      is lost when its record has a runtime id and no close intent, and its
      machine answers that the runtime is not there. sandbox.close clears it.
      Drop Provisioning, Starting, Ready, Busy, Draining and Failed.
    • P-2 Close. No generation compare-and-swap. The Sandbox lock and the
      op intent already serialize close; concurrent closes are rare under the
      one-writer model. Close is refused Busy while a live exec record exists
      (§5.2); sandbox.close(name, { force: true }) cancels those commands first.
    • P-3 No lease heartbeat and no reclaimer. The exec record (§5.2) replaces
      ExecLease and its heartbeat; liveness of a remote call is fix(545): bound worker liveness waits, never an operation #556's beat.
      There is no automatic reclaim in 2.0, so reclaim.enabled, --dry-run
      and --freeze go.
    • P-4 No caller isolation request. Drop sandbox.create({ isolation }),
      requested_isolation/effective_isolation and the strength order.
    • P-5 Typed capability, nothing more. Keep the typed, #[non_exhaustive]
      Capability that replaces the flat HashMap<String, bool>: isolation,
      kernel, mount driver, idle stop and direct_store_access (which fix(545): refuse to place a sqlite Store on a machine that keeps no disk (B0-150 / B7-223) #557
      reads). machine get and sandbox get show it.
    • P-6 No capacity contract or extra read models. Drop the per-Store quota,
      in-flight limit, queue bound and Capacity refusal, the Sandbox activity
      read model and the scheduling facts. Revisit after 2.0 with a measured need.

    4. Exec network option

    workspace.exec(ws, command, { spool?, isolateNetwork? })

    Backend omitted true false
    namespaces (VM machines) isolated (as #587) a fresh network namespace for this command, with lo up the machine's network, as before #587
    docker the container's network refused Unavailable the container's network
    native the host's network refused Unavailable the host's network
    • A request the backend cannot meet is refused, never silently ignored.
    • The result gains network: "none" (isolated) or "sandbox" (the network
      the Sandbox has), so a caller sees what took effect.
    • Plumbing: an isolate_network: bool on ExecRequest
      (layerfs-sandbox/src/types/request.rs), passed to the helper as
      --isolate-network; the helper's exec stage
      (layerfs-sandbox-helper/exec.rs) runs unshare(CLONE_NEWNET) only with
      the flag, then brings lo up. The worker request carries the field.
    • What isolation protects: the command cannot reach the worker's control
      port or other services on the machine's loopback. With false the port is
      reachable, but the worker's secret stays in /layerfs/state/secret
      (directory 0700, root-owned), out of reach of the command's uid.

    5. Exec and cancel

    5.1 Calls

    workspace.exec(ws, command, { spool?, isolateNetwork?, execId? })
        execId: the caller's name for this command, unique among the Sandbox's
                running commands; when omitted, control makes one and sends it
                in the first progress event: { exec: { id } }
        result: as today, plus execId and network; a cancelled command has
                kind "cancelled" and exitCode 128 + signal (143 TERM, 137 KILL)
    
    workspace.exec_cancel(ws, execId, { graceMs? })   graceMs defaults to 5000
        → { outcome: "cancelled" | "ended" | "unknown", signal? }
        sends SIGTERM to everything the command started, waits up to graceMs,
        then SIGKILL; "ended" means it had already finished
    

    One new call in the call table, with its CLI, Node and Python forms
    (layerfs workspace exec-cancel <ws> <exec-id> [--grace-ms N]).

    5.2 The exec record

    A running command has sandbox/<name>/exec/<execId>.json: its owner
    process (pid, kernel start time, boot, as Store leases name theirs), where it
    runs (local or a machine's worker) and how to stop it (cgroup path or process
    group). It is written under the Sandbox lock when the command starts and
    removed when it ends. The record is how a different process cancels a command,
    and how sandbox.close knows commands are running. A record whose owner is
    dead is stale; close and cancel remove it under the Sandbox lock and log it.

    5.3 How each backend stops one command

    Backend Group Cancel
    namespaces a child cgroup per command, <sandbox cgroup>/exec-<id>; the helper joins it instead of the Sandbox's cgroup SIGTERM to the cgroup's processes, then cgroup.kill; this also catches children that left the process group
    native a new session per command (setsid at spawn) killpg SIGTERM, then SIGKILL. On macOS there is no cgroup, so a child that calls setsid itself escapes; documented
    docker the command runs under a small wrapper that starts a new session and writes its id to the Sandbox's scratch a second Engine exec in the container runs kill -TERM -<pgid>, then -KILL. The Engine has no per-exec kill (docker/streams.rs refuses one today)
    worker route the worker holds the command and its group a new worker op, sandbox.exec_cancel, in the next worker protocol version

    5.4 Rules

    • Cancel is explicit only. No automatic timeout, no retry, no replay. A
      cancel whose result cannot be confirmed (a worker that does not answer) is
      unknown, and the exec record stays until its owner ends or close clears it.
    • The daemon supervises no commands; cancel lives in control and the Sandbox
      backend, like exec.
    • Exiting bash is not proof every descendant is gone; only the cgroup form
      (namespaces) can promise that, and the result says which form ran.

    6. Code layout

    6.1 Crates

    Scope: crate level, production LOC at 8b701f42f0. Tests stay with their
    crate under tests/ and are not counted. The production total (139,405) does
    not change; the counter's api scope drops by 18,202 and connect grows by
    the same.

      connect/crates/                           [34270]
        layerfs-control/                        [18202]   (moved from api/crates/layerfs-control; D-2)
        layerfs-machine/                         [4157]   (where: one folder per platform)
        layerfs-sandbox/                         [8610]   (how: one folder per backend)
        layerfs-store-backup/                    [3301]   (bucket I/O for backups)
      api/crates/                                [2214]   (public surface only)
        layerfs-api/                             [1427]   (the call table; depends on layerfs-control)
        layerfs-cli/                              [644]
        layerfs-node/                              [93]
        layerfs-python/                            [50]

    Layers stay core < engine < connect < api. Inside connect:
    layerfs-control → layerfs-machine → layerfs-sandbox, and
    layerfs-control → layerfs-store-backup. The drivers (layerfs-machine,
    layerfs-sandbox, layerfs-store-backup) keep their rule: every input as
    arguments, no home, no records. layerfs-control is the one connect crate
    that owns the home.

    Non-production changes in the same commit:

    • tools/production_loc.py: layerfs-control leaves API_CRATES and
      counts in connect.
    • tools/check_product_boundary.py: layerfs-control's layer becomes
      connect; ALLOWED_DEPENDENCIES is unchanged.
    • Cargo.toml paths in layerfs-api and layerfs-control; Cargo.lock;
      .github/workflows/{ci,nightly}.yml and tools/ci_tests.py paths.
    • connect/AGENTS.md and connect/README.md (connect holds the drivers and
      the control library), api/AGENTS.md, root AGENTS.md, and
      docs/architecture/api/{control,routing}/ → docs/architecture/connect/.
    • core/docs/issues/536/DECISIONS.md: C1 marked superseded by D-2.

    6.2 Inside layerfs-control: machine → Sandbox → Workspace, sqlite vs PostgreSQL

    Proposal P-7, done as moves only (no body changes), in a commit after
    the crate move. Scope: layerfs-control/src at 8b701f42f0. A directory
    marked collapsed is unchanged and its bracket counts all its files; every
    other directory lists every file.

      layerfs-control/src/                [18202]   (moved from api/crates/layerfs-control)
        inventory.rs                          202
        lib.rs                                 33
        calls/                              [710]   (8 files, collapsed)
        common/                            [2108]   (19 files, collapsed; gains random/text/lock from sandbox/registry.rs)
        doctor/                             [618]   (6 files, collapsed)
        machine/                           [1817]   (20 files, collapsed; bundle.rs gains bundle_dir/IMAGE_ARCHIVE from sandbox/deploy.rs)
    +   migrate/                            [933]   (moved from common/home/migrate/: it reads every entity's records)
          mod.rs                                6   (moved from common/home/migrate/mod.rs)
          open.rs                              52   (moved from common/home/migrate/open.rs)
          v110.rs                             364   (moved from common/home/migrate/v110.rs)
          v2.rs                               418   (moved from common/home/migrate/v2.rs)
          volume.rs                            93   (moved from common/home/migrate/volume.rs)
        project/                           [1216]
          family.rs                           310
          init.rs                              47
          mod.rs                               13
          record.rs                            80
    +     postgresql/                       [115]   (Init into a PostgreSQL Store)
            init.rs                           115   (moved from project/postgres_init.rs)
    +       mod.rs                              —   new
    +     sqlite/                           [651]   (folder copy to the Store's machine)
    +       mod.rs                              —   new
            send.rs                           345   (moved from project/send.rs)
            staged.rs                         175   (moved from project/staged.rs)
            tree.rs                           131   (moved from project/tree.rs)
        sandbox/                           [5400]
          close.rs                            116
          create.rs                           125
          deploy.rs                           237   (bundle_dir/IMAGE_ARCHIVE move to machine/bundle.rs)
          direct.rs                           343
          dispatch.rs                         104
          exec.rs                              53
          family.rs                            40
          files.rs                            608
          get.rs                               48
          list.rs                              18
          mod.rs                               28
          output.rs                           163
          record.rs                           136
          recorded.rs                         298
          registry.rs                         115   (random/text/lock move to common/)
          route.rs                            446
          runtime.rs                          193
          serve.rs                            491
    +     daemon/                           [884]   (moved from daemon/: the channel to a Sandbox's daemon)
            call.rs                            59   (moved from daemon/call.rs)
            channel.rs                        283   (moved from daemon/channel.rs)
            failure.rs                         61   (moved from daemon/failure.rs)
            internal.rs                        79   (moved from daemon/internal.rs)
            mod.rs                             10   (moved from daemon/mod.rs)
            operator.rs                        12   (moved from daemon/operator.rs)
            pool.rs                           198   (moved from daemon/pool.rs)
            values.rs                         182   (moved from daemon/values.rs)
          launch/                           [954]
            connection.rs                     105
            create.rs                          62
            local.rs                          397
            mod.rs                             14
            provider.rs                       376
        store/                             [4302]   (kind-neutral: records, routing, Store calls)
          call.rs                              73
          calls.rs                            363
          identity.rs                         479
          list.rs                              39
          mod.rs                               23
          open.rs                             180
          record.rs                           325
          resolve.rs                           30
          service.rs                          188
          status.rs                            45
    +     backup/                          [1032]   (moved from backup/: backups of sqlite Stores)
            create.rs                         226   (moved from backup/create.rs)
            family.rs                          77   (moved from backup/family.rs)
            list.rs                           125   (moved from backup/list.rs)
            mod.rs                              9   (moved from backup/mod.rs)
            progress.rs                       142   (moved from backup/progress.rs)
            record.rs                         195   (moved from backup/record.rs)
            restore.rs                        258   (moved from backup/restore.rs)
    +     postgresql/                       [688]   (PostgreSQL-only Store code)
            connect.rs                        248   (moved from store/connect.rs)
    +       mod.rs                              —   new
            open.rs                           219   (moved from store/postgres.rs)
            reader.rs                         221   (moved from store/reader.rs)
    +     sqlite/                           [837]   (sqlite-only Store code)
            create.rs                          35   (moved from store/create.rs)
            family.rs                         134   (moved from store/family.rs)
    +       mod.rs                              —   new
            remove.rs                          67   (moved from store/remove.rs)
    +       transfer/                       [601]
              mod.rs                            3   (moved from store/transfer/mod.rs)
              phases.rs                       263   (moved from store/transfer/phases.rs)
              resolve.rs                      102   (moved from store/transfer/resolve.rs)
              steps.rs                        233   (moved from store/transfer/steps.rs)
        workspace/                          [863]
          calls.rs                             97
          mod.rs                                4
          serve.rs                            249
          unmount_guard.rs                    274
    +     daemon/                           [239]   (moved from daemon/workspace/)
            commit.rs                          41   (moved from daemon/workspace/commit.rs)
            mod.rs                             10   (moved from daemon/workspace/mod.rs)
            mount.rs                           70   (moved from daemon/workspace/mount.rs)
            recovery.rs                        52   (moved from daemon/workspace/recovery.rs)
            status.rs                          15   (moved from daemon/workspace/status.rs)
            unmount.rs                         51   (moved from daemon/workspace/unmount.rs)

    The top-level backup/ and daemon/ folders and common/home/migrate/ are
    gone after these moves. Bucket I/O stays in the layerfs-store-backup driver;
    control's store/backup/ holds bucket records and the backup.* calls.

    6.3 Dependency direction inside layerfs-control

    The folders say machine → Sandbox → Workspace; the imports must say it too.
    Today they point both ways, mostly through misplaced helpers:

    Edge today Cause Fix in P-7
    store, common, machine → sandbox::registry random, text, lock are general helpers kept in sandbox/registry.rs move them to common/
    machine → sandbox::deploy bundle_dir, IMAGE_ARCHIVE describe what a machine installs move them to machine/bundle.rs
    common::home::migrate → every entity migration reads every entity's old layout migrate/ becomes its own top-level module
    store::identity → sandbox::files project-record discovery lives in Sandbox code move the discovery to project/
    machine::worker::server → store, sandbox the worker's dispatcher runs Store and Sandbox ops allowed: the one dispatcher edge

    Target rule, checked by a test that reads use crate::… lines:

    common      imports no entity
    machine     common                        (+ worker/server.rs, the dispatcher)
    store       common, machine
    project     common, store, machine
    sandbox     common, machine, store, project
    workspace   common, sandbox, store, machine
    migrate, doctor, inventory, calls   any
    

    Any edge outside the rule that cannot move in P-7 goes on a named allowlist
    in that test, each with the issue that removes it.

    7. Order of work

    1. Snapshot removal in feat(machine): typed isolation/capability contract and bounded read models (#551) #567 and fix(545): refuse to place a sqlite Store on a machine that keeps no disk (B0-150 / B7-223) #557 (D-1); the assignee is asked ([B] Sandbox compute plane: machines, backends, observability and lifecycle #545).
    2. Trims P-1 to P-6 in feat(sandbox): explicit B5 lifecycle — generation CAS, exec lease, Draining and Lost (#548) #566 and feat(machine): typed isolation/capability contract and bounded read models (#551) #567, if accepted.
    3. Merge: #556 → #587, #566 → #558, #567 → #557. The two stacks
      conflict in sandbox/{record,get,mod}.rs; the second stack rebases.
    4. Exec network option (§4) and exec cancel (§5) on the merged branch.
    5. Crate move (D-2), one commit, production delta 0.
    6. Internal moves P-7 and the dependency test, one commit, delta 0.

    Steps 5 and 6 are moves only: each commit builds, passes the same tests and
    reports a production LOC delta of 0 against its parent.

    8. Acceptance

    • isolateNetwork: on namespaces, true (and omitted) gives a command
      with only lo (up) that cannot reach the worker port; false reaches the
      machine's network; true on docker and native is refused
      Unavailable. The result's network names what ran.
    • Cancel: a sleep 600 started by one process is cancelled by another
      through workspace.exec_cancel; the exec call returns kind: "cancelled"; the command's background children are gone (cgroup empty on
      namespaces); cancelling an ended command answers ended; a cancel
      against an unreachable worker answers unknown and changes nothing.
    • Close: refused Busy while an exec record is live; force: true cancels
      and then closes; a stale record (dead owner) is removed and logged.
    • lost is reachable: remove a Sandbox's container behind control's back;
      sandbox.get answers lost; sandbox.close clears it.
    • The moves (§7 steps 5–6) change no behavior: the same test set passes
      before and after, and the dependency test (§6.3) passes.

    9. Open questions

    Id Question Recommendation
    O-1 Default for isolateNetwork on namespaces when omitted isolated (D-4 asks for an opt-out, which implies isolated by default)
    O-2 Bring lo up in isolated mode yes; local test servers need it
    O-3 Accept trims P-1 to P-6 yes
    O-4 Accept internal layout P-7 and the dependency rule (§6.3) yes, after the crate move
    O-5 execId chosen by the caller, by control, or both both (caller may name it; else control)
  11. yifanxuaaa commented on Nov 7, 2026

    @yifanxuaaa
    Contributor

    Closed by the owner (2026-11-07). Its five sub-issues (#548–#552) are closed as not planned: the PRs that were to deliver them (#556, #557, #558, #566, #567, #587) are recorded in layerfs_v2_0_0 as history only (git merge -s ours in #601), with mengmengmengqiang's commits and credit kept but no code applied. The control layer split (layerfs-project#101) replaced this area. Exec records and cancel continue in Ephemeral-AI-Lab/layerfs-project#113, which stays open.

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

Metadata

Metadata

Labels

enhancementNew feature or request

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions