Skip to content

feat: disposable macOS guests with pre-baked pulled base images#359

Open
PeronGH wants to merge 51 commits into
masterfrom
feat/macos-guest-vz
Open

feat: disposable macOS guests with pre-baked pulled base images#359
PeronGH wants to merge 51 commits into
masterfrom
feat/macos-guest-vz

Conversation

@PeronGH

@PeronGH PeronGH commented Jul 2, 2026

Copy link
Copy Markdown
Member

What

macOS guest VMs on Apple Silicon as a first-class ArcBox noun: pull a pre-baked base image from ArcBox's distribution bucket, copy-on-write clone it, and boot clean throwaway guests — the Linux "clone, use, discard" model extended to macOS. Foundation for the BYOC macOS Actions runner fleet (Linear RUN-36 tree).

Surface

arcbox macos image pull tahoe-base[@2026.07.02]   # streamed progress; --manifest <url|path> for dev
arcbox macos image ls | rm
arcbox macos create ci-1 --image tahoe-base --cpus 4 --memory 8192
arcbox macos start | stop | rm | ls

Served by a dedicated MacosService (own proto, no MachineService coupling), registered only on Apple Silicon hosts. ImagePull is server-streaming (stage + fraction events).

Design notes

  • Distribution format is ours (JSON manifest + zstd, on the B2 darwin bucket behind darwin.arcboxcdn.com): no OCI client, no registry auth, no Tart traces in this codebase. Format spec + bake pipeline live in macos-runner-image-builder (the producer; this repo consumes).
  • Pull path (macos/remote.rs + macos/pull.rs): index/manifest resolution → hardware-model validation before the multi-GB download → socket → zstd → zero-skipping sparse writes (compressed bytes never touch disk; SHA-256 hashed in flight) → staging dir renamed live only after every check passes.
  • Resume: transient HTTP failures retry with Range: bytes=N- against the live decoder state (no temp file). A daemon restart restarts the pull; resume covers network blips, the failure that matters at 20 GB.
  • Cancellation: client disconnect cancels the pull (tx.closed() + future drop); a Drop-based staging guard cleans partials at any await point.
  • Zero-skip writes are load-bearing: zstd --sparse under-recovers holes on macOS (verified: 45 GB actual from a 31 GB original); our writer restores bit-identical sparseness.
  • IPSW installer retained, unshipped: install_from_ipsw + Apple IPSW download compile under the off-by-default macos-ipsw-install feature (clippy-clean both configs) with zero product surface.
  • Clones share the base machine identifier (same practice as Tart, proven in production CI fleets); docs/macos-guest.md corrected accordingly.

Verification

  • 12 unit tests incl. full pull round-trip against a synthetic artifact, corrupted-manifest rejection, sparse-writer allocation check (at 256 MiB — APFS materializes small files), and a flaky-HTTP-server resume test.
  • On hardware (M-series, macOS 27): pulled a real 21 GiB baked artifact in 81 s with streamed CLI progress, disk landed 31 GB actual / 50 GB logical (bit-identical to bake source), create+start booted macOS 26.5 to SSH; stop/rm clean. Kill-mid-pull verified: staging removed within seconds.
  • cargo fmt --check, clippy --workspace --all-targets and clippy --features macos-ipsw-install at zero warnings, full workspace test suite green.

Known gaps (tracked)

  • Reference-based pull against the live bucket awaits the first publish (RUN-38 In Review; --manifest path is fully E2E-verified).
  • Runner bootstrap contract in the baked image is RUN-6.

Copilot AI review requested due to automatic review settings July 2, 2026 13:46
@pullfrog

pullfrog Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Run failed. View the logs →

Pullfrog  | Rerun failed job ➔View workflow run | via Pullfrog | Using Claude Opus𝕏

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown

Greptile Summary

This PR introduces disposable macOS guests as a first-class ArcBox surface: pull a pre-baked base image from ArcBox's CDN, copy-on-write clone it, and boot throwaway VMs. It adds MacosService (own gRPC proto), the CLI macos noun, and the complete image-pull and machine-lifecycle stack in arcbox-core.

  • Image pull path: index/manifest resolution \u2192 hardware-model validation before any large download \u2192 chunked parallel download with in-memory SHA-256 verification before decode \u2192 zero-skipping sparse writes \u2192 staging-dir-rename landing. Concurrent pulls of the same name are serialized by a per-name async mutex.
  • Machine lifecycle: RunningSlot placeholder state machine (Starting, Running, Stopping, Removing) enforces the 2-guest cap and prevents start/stop/remove races; create stages into a UUID-suffixed directory before renaming under the write lock.
  • Supporting modules: lease.rs discovers guest IPs from the host DHCP table; image.rs manages the base-image registry with path-traversal-safe name validation; disk.rs implements the concurrent sparse writer with verify-before-decode retry semantics.

Confidence Score: 5/5

Safe to merge. All previously identified races and integrity issues have been addressed; the new code is well-tested and hardware-verified.

The RunningSlot placeholder state machine correctly serializes all concurrent operations without per-machine mutexes. The verify-before-decode approach eliminates stale-data-on-retry bugs. Staging-then-rename landing is used consistently. Name validation blocks path traversal at every entry point. The two observations (noisy log warnings from staging dirs, asymmetric not-found behavior on machine remove) are minor style issues that do not affect correctness or safety.

No files require special attention. machine.rs and pull.rs contain the most complex concurrent logic and both look correct.

Important Files Changed

Filename Overview
app/arcbox-core/src/macos/machine.rs Full machine lifecycle with RunningSlot placeholder state machine; all previously identified TOCTOU races addressed via atomic slot transitions and UUID staging directories.
app/arcbox-core/src/macos/pull.rs Image pull orchestration: per-name async pull lock serializes concurrent pulls, StagingGuard cleans partials on cancellation, version no-op check is inside the lock.
app/arcbox-core/src/macos/disk.rs Chunked download with verify-before-decode; SparseWriter correctly handles APFS holes; comprehensive tests including corrupt-attempt and flaky-HTTP retry scenarios.
app/arcbox-core/src/macos/image.rs Base image registry with path-traversal-safe name validation; list() warns on unreadable entries including in-progress staging dirs.
app/arcbox-core/src/macos/remote.rs Index/manifest deserialization, ImageReference parsing, schema version checks, chunk-count validation, and stream/manifest name-match all present.
app/arcbox-core/src/macos/mod.rs validate_name guards against path traversal; StagingGuard provides Drop-based cleanup for both image pull and machine create staging flows.
app/arcbox-core/src/macos/vm.rs MacVm wraps VirtualMachine; boot polls for Running state; !Send ObjC handles correctly documented.
app/arcbox-core/src/macos/lease.rs Parses /var/db/dhcpd_leases for guest IP discovery; handles bootpd's stripped leading-zero MAC format; well-tested.
app/arcbox-api/src/grpc/macos.rs gRPC service implementation; !Send lifecycle ops dispatched via run_macos_blocking; image pull streamed via unbounded channel.
app/arcbox-daemon/src/services.rs MacosService gated on cfg(target_os = "macos") — not registered on non-macOS hosts.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant CLI as arcbox CLI
    participant GRPC as MacosService
    participant MGR as MacImageManager
    participant DSK as disk.rs
    participant FS as Filesystem

    CLI->>GRPC: ImagePull(reference)
    GRPC->>MGR: pull_remote()
    MGR->>MGR: resolve_source() fetch index + manifest
    MGR->>MGR: pull_lock(name).lock()
    MGR->>MGR: validate_hardware_model()
    MGR->>MGR: version check (no-op if same)
    MGR->>FS: create staging dir
    MGR->>DSK: fetch_disk()
    loop per chunk bounded parallel
        DSK->>DSK: download into memory
        DSK->>DSK: verify SHA-256 before decode
        DSK->>FS: sparse write at chunk offset
    end
    MGR->>FS: fetch+verify aux, write meta
    MGR->>FS: rename staging to images/name
    MGR-->>GRPC: MacImage
    GRPC-->>CLI: stream PullEvent

    CLI->>GRPC: CreateMachine(name, image)
    GRPC->>MGR: MacMachineManager.create()
    MGR->>MGR: validate name + image minimums
    MGR->>FS: stage in .create-name-uuid
    MGR->>FS: clonefile disk
    MGR->>FS: rename under write lock
    GRPC-->>CLI: Empty

    CLI->>GRPC: StartMachine(name)
    GRPC->>MGR: start()
    MGR->>MGR: insert Starting slot check cap
    MGR->>MGR: boot_reserved MacVm::boot
    MGR->>MGR: slot Starting to Running
    GRPC-->>CLI: Empty
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant CLI as arcbox CLI
    participant GRPC as MacosService
    participant MGR as MacImageManager
    participant DSK as disk.rs
    participant FS as Filesystem

    CLI->>GRPC: ImagePull(reference)
    GRPC->>MGR: pull_remote()
    MGR->>MGR: resolve_source() fetch index + manifest
    MGR->>MGR: pull_lock(name).lock()
    MGR->>MGR: validate_hardware_model()
    MGR->>MGR: version check (no-op if same)
    MGR->>FS: create staging dir
    MGR->>DSK: fetch_disk()
    loop per chunk bounded parallel
        DSK->>DSK: download into memory
        DSK->>DSK: verify SHA-256 before decode
        DSK->>FS: sparse write at chunk offset
    end
    MGR->>FS: fetch+verify aux, write meta
    MGR->>FS: rename staging to images/name
    MGR-->>GRPC: MacImage
    GRPC-->>CLI: stream PullEvent

    CLI->>GRPC: CreateMachine(name, image)
    GRPC->>MGR: MacMachineManager.create()
    MGR->>MGR: validate name + image minimums
    MGR->>FS: stage in .create-name-uuid
    MGR->>FS: clonefile disk
    MGR->>FS: rename under write lock
    GRPC-->>CLI: Empty

    CLI->>GRPC: StartMachine(name)
    GRPC->>MGR: start()
    MGR->>MGR: insert Starting slot check cap
    MGR->>MGR: boot_reserved MacVm::boot
    MGR->>MGR: slot Starting to Running
    GRPC-->>CLI: Empty
Loading

Reviews (10): Last reviewed commit: "fix(macos): serialize machine lifecycle ..." | Re-trigger Greptile

Comment thread app/arcbox-core/src/macos/image.rs Outdated
Comment thread app/arcbox-core/src/macos/machine.rs Outdated
Comment thread app/arcbox-core/src/macos/machine.rs Outdated
Copilot AI review requested due to automatic review settings July 3, 2026 07:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread app/arcbox-daemon/src/services.rs
@PeronGH PeronGH force-pushed the feat/macos-guest-vz branch from 46fb489 to 2c868c0 Compare July 3, 2026 08:38
Comment thread app/arcbox-core/src/macos/pull.rs
Copilot AI review requested due to automatic review settings July 3, 2026 09:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread app/arcbox-core/src/macos/machine.rs
Comment thread app/arcbox-core/src/macos/machine.rs
Copilot AI review requested due to automatic review settings July 6, 2026 10:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

pullfrog[bot]
pullfrog Bot previously approved these changes Jul 6, 2026

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes — the schema-2 chunked-CDN refactor of the macOS image pull path (commit 892f5c6), which replaces the single-stream zstd download with parallel per-chunk fetch, decode, and sparse-write.

  • New disk.rs chunked restorefetch_disk downloads chunks with bounded parallelism (JoinSet + Semaphore, concurrency 4), streams each through zstd into a shared Arc<File> via a zero-skipping SparseWriter writing at base = index * chunk_size, and verifies each chunk's compressed size, compressed SHA-256, and exact decompressed length.
  • Manifest schema v2disk becomes a DiskManifest (uncompressed_size, chunk_size, ordered chunks) in remote.rs; resolve_source now validates that the chunk list exactly tiles the declared disk size (chunks.len() == div_ceil(uncompressed_size, chunk_size), non-empty, non-zero chunk_size).
  • Removed single-stream pathfetch_zstd_to_sparse and its byte-range resume are gone; SparseWriter / verify_sha256 / hex moved from pull.rs into disk.rs. Per-chunk retry re-downloads the whole chunk (no resume).
  • CDN base movedDEFAULT_IMAGE_BASE is now https://image.arcboxcdn.com/darwin.
  • Tests — local assembly with a zero gap and partial last chunk, wrong-hash rejection, HTTP chunk retry after a mid-body connection drop, sparse-writer hole allocation at 256 MiB, and inconsistent-chunk-count rejection.

The refactor is coherent and well-covered. Retry is idempotent (a failed attempt flushes only full 64 KiB blocks, and truncated zstd output is a strict prefix of the full decode, so re-download rewrites identical bytes), and the parallel positioned writes target disjoint byte ranges so they never race. No stale references to the removed types remain.

ℹ️ Published whole-disk hash is never verified; interior-chunk ordering is unchecked

The manifest carries a whole-disk uncompressed_sha256, but the client never reads it — integrity rests entirely on per-chunk hashing plus placement by array index. Because every interior chunk shares the same expected_len (chunk_size), two interior chunks swapped in the manifest's chunks array would pass all per-chunk checks (compressed hash, decompressed length, zstd frame checksum) yet land a corrupt disk with no error. The producer is trusted and JSON array order is deterministic, so this is low-probability — worth surfacing so the intent behind the published-but-unused field is explicit.

Technical details
# Published whole-disk hash is never verified; interior-chunk ordering is unchecked

## Affected sites
- `app/arcbox-core/src/macos/remote.rs:187-205``DiskManifest` doc notes `uncompressed_sha256` "is not consumed here".
- `app/arcbox-core/src/macos/disk.rs:190-232``fetch_disk` places chunk `i` at `base = i * chunk_size` purely by iteration index; no chunk-level verification ties the assembled whole to `uncompressed_sha256`.
- `app/arcbox-core/src/macos/pull.rs:141-157``resolve_source` validates chunk *count* but not chunk identity/order.

## Required outcome
- Make the trust model explicit and closed: either drop the unused `uncompressed_sha256` from the consumed schema, or wire in a cheap ordering/identity guard so a mis-ordered manifest cannot land silently.

## Suggested approach (optional)
- A whole-disk re-read defeats the perf goal, so avoid it. A cheap alternative: assert each `chunk.path` matches the expected `disk.{i:03}.zst` for its index during `resolve_source`, coupling array position to filename. This catches reordering without a multi-GB read.
- Alternatively, document in `remote.rs` why the field is retained despite being unverified (e.g. for out-of-band/publisher-side auditing), so a future reader doesn't assume it's a live client check.

## Open questions for the human
- Is chunk placement contractually keyed to array order alone, or should the client enforce the `disk.NNN.zst` naming convention as the ordering source of truth?

Pullfrog  | View workflow run | Using Claude Opus𝕏

Comment thread app/arcbox-core/src/macos/disk.rs Outdated
@pullfrog

pullfrog Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

The check suite failed on Step 6: Check formatting (cargo fmt --check) — the --no-linux-vm notice println! in app/arcbox-daemon/src/startup/pipeline.rs:180 exceeded the line-width limit and needed wrapping. Applied the rustfmt-mandated multi-line formatting, verified cargo fmt --check is clean workspace-wide, and pushed to feat/macos-guest-vz.

Pullfrog  | View workflow run | via Pullfrog | Using Claude Opus𝕏

pullfrog[bot]
pullfrog Bot previously approved these changes Jul 6, 2026

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes — the --no-linux-vm VM-host-only mode added in commit 3172451 (the only new commit since the prior pullfrog review at 892f5c6).

  • New --no-linux-vm daemon flag + [vm] autostart config — flag defaults autostart to false, overriding the config file in init_runtime; documented on both the CLI (app/arcbox-cli/src/commands/daemon.rs) and daemon (app/arcbox-daemon/src/main.rs) argument surfaces.
  • Core bootstrap gateRuntime::init early-returns before the Linux/Docker system-VM bootstrap when autostart is false, so no guest binaries download and no VM lifecycle actor starts; the macOS guest manager is untouched.
  • Daemon service gating — the Docker API server handle becomes Option (spawned only when the Linux VM runs), and Docker CLI integration, the Kubernetes proxy, and post-startup recovery are all skipped in host-only mode.
  • Consistent wiringServiceHandles.docker and the shutdown drain handle the optional Docker task; pipeline.rs threads a linux_vm bool so the ready banner omits the Docker/K8s lines and prints the host-only notice.

Every gate reads the same post-override runtime.config().vm.autostart, the changes are coherent across startup, services, recovery, and shutdown, and the optional Docker handle is handled correctly on both the drain and abort paths.

Pullfrog  | View workflow run | Using Claude Opus𝕏

Copilot AI review requested due to automatic review settings July 6, 2026 11:30
@PeronGH PeronGH removed the request for review from Copilot July 6, 2026 11:30
Comment thread app/arcbox-core/src/macos/machine.rs Outdated
@PeronGH

PeronGH commented Jul 6, 2026

Copy link
Copy Markdown
Member Author

Re: the pullfrog note on the unused whole-disk uncompressed_sha256 and unchecked interior-chunk ordering — addressed in 517f9c17. The integrity/trust model is now documented explicitly on DiskManifest: each chunk's compressed hash is verified before decode, chunks are placed by array index trusting the producer's offset order (disk.000.zst, disk.001.zst, …), and uncompressed_sha256 is retained for out-of-band/publisher-side auditing rather than a client re-read (which would defeat the streaming design). The same commit closes the verify-before-write gap from the greptile P1.

PeronGH added a commit that referenced this pull request Jul 6, 2026
The chunked pull decoded compressed bytes straight into disk.img while
downloading, so a failed/corrupt attempt mutated the destination before its
hash was ever checked. On retry, SparseWriter skips zero blocks, so a prior
attempt's divergent non-zero bytes in a now-zero region would survive — silent
base-image corruption, with no whole-disk check to catch it (greptile P1 on
PR #359).

Reorder the flow to verify-then-decode: download each chunk into memory, check
exact size + compressed SHA-256, and only then decompress the proven-correct
bytes into the sparse file. A rejected attempt never touches disk.img, so
retries are safe by construction, and every byte written comes from correct
data — sparse-skipping a zero block can never leave stale bytes. No per-attempt
special-casing and no multi-GB re-read.

- Buffered downloads are bounded by a byte-budget semaphore (1 GiB) that also
  caps concurrency (≥ MIN_SLOT per chunk → ≤ 8 connections); a runaway response
  is capped at the manifest size.
- Decode runs on spawn_blocking, parallelizing decompression across cores.
- New test corrupt_attempt_never_reaches_disk: a complete-but-wrong first
  response (non-zero where the correct chunk is zero) is rejected in memory and
  the retry lands the correct disk — fails under the old streaming path.
- Document the integrity/trust model on DiskManifest (per-chunk hash + producer
  chunk ordering; whole-disk uncompressed_sha256 is for out-of-band auditing).
Copilot AI review requested due to automatic review settings July 6, 2026 12:39
PeronGH and others added 20 commits July 10, 2026 10:54
The positional <name> read like 'what you're pulling' (a version), when
it's actually a local label for the base image. Move it to --name and free
the positional slot for the eventual version|latest selector.
Replaces the IPSW-install ImagePull RPC with pull-by-reference from the
published distribution bucket: MacosService.ImagePull is now server-
streaming, the CLI takes a positional stream[@Version] reference (or
--manifest for development), and image ls shows version/OS provenance.
The product acquires base images via pull only. install_from_ipsw and the
Apple IPSW downloader stay compiled under an off-by-default feature so
they remain compiler-verified without being product surface. Docs updated
to the pull flow, including the corrected shared-machine-identifier note.
HTTP downloads retry with Range: bytes=N- on transient failures — the
zstd decoder state lives in memory, so decoding continues where it left
off. A staging Drop guard makes the pull future cancellation-safe:
dropping it at any await point removes the partial image.
The file-source branch read with std::fs in a loop with no await points,
so the pull future ran to completion inside a single poll — immune to
select!-based cancellation and pinning a runtime worker throughout.
tokio::fs reads yield, verified by killing a pull mid-flight: staging
stops growing and the guard removes it within seconds.
Extracts pull_remote's resolution prefix (index resolve, schema/format/
name checks) into a shared helper and adds MacImageManager::resolve_remote
on top of it: the concrete version a pull would land, its requirements,
host-support validation, and the locally installed version — so a caller
(the fleet agent's settings/prepare path, RUN-35 follow-up) can answer
"does this fit here" and "is an update pending" without a download.
ImageResolve exposes core's resolve_remote over MacosService, and
ImagePull's stream now ends with a terminal "done" event carrying the
registered image's summary, so a caller learns the concrete version a
floating reference landed. CLI: new `arcbox macos image resolve`, and
`image pull` reports the landed name@version.
…races

Address the five open code-review findings on the disposable-macOS-guest work:

- Validate image/machine names to a single path component at the directory
  constructors (image_dir/machine_dir are now fallible), so a crafted
  --manifest name or a create/remove name with separators cannot escape the
  managed macos/images and macos/machines roots.
- Enforce the image's published CPU/memory minimums in create (fail fast).
- Assemble a machine in a unique staging dir and rename it into place under
  the records lock, so concurrent same-name creates can no longer delete each
  other's cloned disks. Move cloning to MacImage::clone_into and share the
  StagingGuard RAII helper via the macos module.
- Serialize pulls of the same image with a per-name async lock, so concurrent
  pulls can no longer corrupt the shared staging directory.
- Re-insert the running slot when a stop fails, keeping the guest cap and
  list() accurate while the VZ guest may still hold a framework slot.
The pull consumer was written against a schema-1 monolithic-disk.img.zst
manifest, but the published layout is schema-2: the disk is split into
fixed-size, independently decompressible zstd chunks (disk.NNN.zst) served
under the darwin/ namespace at image.arcboxcdn.com. The old consumer could
not even deserialize a live manifest.

- Point DEFAULT_IMAGE_BASE at the real namespace root
  (https://image.arcboxcdn.com/darwin); the darwin.arcboxcdn.com subdomain
  never existed.
- Accept manifest schema_version 2; replace DiskFile with DiskManifest +
  DiskChunk (chunk_size + per-chunk path/size/sha256).
- Validate that the chunk list exactly tiles the declared disk size before
  downloading anything.
- New macos/disk module: bounded-parallel chunk download (JoinSet +
  Semaphore), streaming decompression into a positioned sparse writer at
  index*chunk_size, per-chunk compressed-hash + exact-length verification,
  and whole-chunk retry on transient failure. Deterministic placement plus
  per-chunk integrity replaces an end-to-end re-read of the assembled disk.

Public API (PullStage, RemoteSource, ResolvedImage, pull_remote,
resolve_remote) is unchanged; aux restore is unchanged.
Adds a first-class mode that runs the daemon without the default Linux/Docker
system VM. Useful for macOS-guest-only workflows and for hosts that don't need
Docker/Kubernetes, and it avoids booting a VM that would otherwise sit idle.

- config: `[vm] autostart` (default true). When false, Runtime::init skips the
  entire Linux bootstrap — guest-binary download, validation, and VM boot — so
  no VM lifecycle actor is ever created.
- daemon: `--no-linux-vm` flag (forwarded by the abctl `daemon start` wrapper)
  overrides `[vm] autostart` to false.
- When disabled, the daemon layer skips the Docker API server, Docker CLI
  integration, the Kubernetes proxy, and container-networking recovery; the
  ready banner reflects VM-host-only mode. gRPC (incl. macOS guests), DNS, and
  machine management are unaffected.

ServiceHandles.docker becomes Option to reflect the skipped Docker server.
The chunked pull decoded compressed bytes straight into disk.img while
downloading, so a failed/corrupt attempt mutated the destination before its
hash was ever checked. On retry, SparseWriter skips zero blocks, so a prior
attempt's divergent non-zero bytes in a now-zero region would survive — silent
base-image corruption, with no whole-disk check to catch it (greptile P1 on
PR #359).

Reorder the flow to verify-then-decode: download each chunk into memory, check
exact size + compressed SHA-256, and only then decompress the proven-correct
bytes into the sparse file. A rejected attempt never touches disk.img, so
retries are safe by construction, and every byte written comes from correct
data — sparse-skipping a zero block can never leave stale bytes. No per-attempt
special-casing and no multi-GB re-read.

- Buffered downloads are bounded by a byte-budget semaphore (1 GiB) that also
  caps concurrency (≥ MIN_SLOT per chunk → ≤ 8 connections); a runaway response
  is capped at the manifest size.
- Decode runs on spawn_blocking, parallelizing decompression across cores.
- New test corrupt_attempt_never_reaches_disk: a complete-but-wrong first
  response (non-zero where the correct chunk is zero) is rejected in memory and
  the retry lands the correct disk — fails under the old streaming path.
- Document the integrity/trust model on DiskManifest (per-chunk hash + producer
  chunk ordering; whole-disk uncompressed_sha256 is for out-of-band auditing).
Copilot AI review requested due to automatic review settings July 10, 2026 03:00
@PeronGH PeronGH force-pushed the feat/macos-guest-vz branch from 517f9c1 to cbffb05 Compare July 10, 2026 03:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

pullfrog[bot]
pullfrog Bot previously approved these changes Jul 10, 2026

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes — the verify-then-decode chunk-integrity fix (cbffb055), the only new commit since the prior pullfrog review at 3172451. It reorders the chunked pull so compressed bytes are proven correct in memory before any byte reaches disk.img.

  • Verify-then-decode flowfetch_verified buffers each chunk fully in memory and checks exact size + compressed SHA-256 before decode_into decompresses the proven-correct bytes into the sparse file. A rejected attempt never touches disk.img, so SparseWriter's zero-block skip can no longer leave a prior attempt's stale non-zero bytes behind (the greptile P1). The single-pass streaming ChunkSink is removed.
  • Byte-budget concurrency — replaces the fixed DOWNLOAD_CONCURRENCY = 4 count with a 1 GiB BUFFER_BUDGET semaphore where each chunk reserves clamp(chunk.size, MIN_SLOT=128 MiB, BUFFER_BUDGET), capping peak buffered memory at ~1 GiB and concurrent connections at 8. The HTTP loop rejects any response exceeding the manifest chunk.size.
  • Decode off the async runtime — decompression runs on spawn_blocking; local-file chunks now read via tokio::fs::read then verify through the same verify_chunk path.
  • Trust model documentedDiskManifest doc in remote.rs now states integrity rests on per-chunk hashing plus producer offset ordering (disk.NNN.zst), and that the whole-disk uncompressed_sha256 is intentionally unconsumed (out-of-band auditing). This closes the prior pullfrog informational finding via its documentation option.
  • New testcorrupt_attempt_never_reaches_disk drives the retry path with a complete-but-wrong first response whose decoded bytes are non-zero exactly where the correct chunk is zero; it passes only because verification precedes decode.

The reorder is correct: cost is clamped to BUFFER_BUDGET, so acquire_many_owned can never deadlock; runaway and short responses both fail the size check and retry; and detached spawn_blocking decodes only ever write verified-correct data into the staging file the StagingGuard removes on cancel, so there is no corruption of any surviving file. All prior pullfrog reviews were body-only, leaving no inline threads to retire.

Pullfrog  | View workflow run | Using Claude Opus𝕏

…ders

stop() vacated the machine's runtime slot before awaiting vm.stop(), so a
concurrent start could boot a second VM on the same disks while the VZ
guest was still live, and stop's failure re-insert could clobber that
start's reservation, orphaning a running guest. remove() never reserved
the slot at all, so a racing start could boot from files mid-deletion
(or resurrect a half-deleted directory via the MAC backfill) and end up
as a running ghost with no record.

Make the running-slot map the single serialization point for all
lifecycle transitions: stop holds a Stopping placeholder for the whole
in-flight stop, remove holds a Removing placeholder while it deletes,
and start re-validates the record (and does the MAC backfill) only
after reserving its Starting slot. In-flight owners re-check their slot
before finalizing so stop_all's shutdown sweep is never clobbered.

Found by Greptile on PR #359 (both threads); the placeholder design
replaces its proposed per-machine mutex so racing callers fail fast
with precise states instead of queueing behind a multi-second boot.
Copilot AI review requested due to automatic review settings July 10, 2026 03:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes — the machine-lifecycle serialization refactor in app/arcbox-core/src/macos/machine.rs (443dac5f), the only new commit since the prior pullfrog review at cbffb055. It makes the running slot map the single serialization point for start/stop/remove so the per-host guest cap and mutual exclusion are both enforced through slot reservations held across the multi-second boot/stop windows.

  • New placeholder slot statesRunningSlot gains Stopping and Removing variants (alongside Starting/Running) plus a describe() helper for uniform "cannot do that now" errors; is_running/state_of/stop_all updated to account for them.
  • start split into reserve + bootstart does a fast not-found check, then atomically checks the cap and reserves Starting, then delegates to the new boot_reserved (record re-read, MAC mint, MacVm::boot) which runs entirely under the reservation so a concurrent remove cannot delete files mid-boot; finalization promotes StartingRunning or releases the slot on error/shutdown-sweep.
  • stop leaves a live-guest placeholder — a running machine transitions RunningStopping (keeping the cap accurate while the VZ guest may still be live), then clears the placeholder on success or restores Running on failure; refuses when another op holds the slot.
  • remove reserves before touching disk — non-force refuses any held slot, force stops first then claims the freed slot (aborting cleanly if a concurrent start won it); disk+record deletion extracted into delete_machine, run under a Removing reservation and released on success and failure alike.
  • Tests — four new cases cover held-slot refusal across start/stop/remove, reservation release after remove and after a failed start, and placeholder slots counting against the guest cap.

The serialization is coherent: the RwLock write is only ever held briefly for reservation and finalization (never across an await), the slot itself provides mutual exclusion during async work, and every finalize path re-checks its own placeholder with a matches! guard so a stop_all shutdown sweep is never clobbered. Force-remove ordering is airtight — whoever grabs the slot first wins, and the slot stays held across the dangerous delete/boot window, so a concurrent start can never boot from mid-deletion files. No slot leaks on any success or error path, and the new Stopping/Removing states surface safely through the gRPC layer's {:?}-to-string state mapping.

Pullfrog  | View workflow run | Using Claude Opus𝕏

@PeronGH PeronGH requested a review from AprilNEA July 10, 2026 03:44
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.

3 participants