Repository navigation
feat(sandbox): create a sandbox from a custom image, and check the name - #243
Merged
Merged
Conversation
Creating a sandbox always downloaded its guest image — over a gigabyte, and impossible on a machine that cannot reach the storage host. The Add sandbox dialog now takes an optional image file. Empty means download as before; a path means use that file, read where it sits rather than copied, and persisted so reset re-provisions from the same file. An image the user picked has three ways to be wrong that a create would otherwise discover minutes in, or worse, only when the guest fails to boot: it is not a sandbox image, it is for another OS, or it is for another architecture. inspect.ts streams the tarball to answer that, reading only the first bytes of each member so a 1.3 GB image is judged in milliseconds (measured: 1-127ms across the four published images). Format comes from the members: a kernel plus a disk is the macOS image, a Linux root filesystem is the one Windows and Linux share — which is why those two cannot be distinguished, and accepting one image for both is correct rather than a gap. Architecture comes from the kernel's own magic (arm64 stamps "ARM\x64" at offset 56, x86 stamps "HdrS" at 514 — both start "MZ", so the offsets are what tell them apart), from the build's arch marker in a rootfs, or failing that from the first ELF header in the tree. An image whose architecture cannot be determined is allowed through: refusing a valid image because the probe could not prove its arch is worse than the rare mismatch, which the guest reports loudly on first boot anyway. The main process is the authority — the same check runs on add, so a stale or hand-crafted call cannot get an unusable image past the dialog. The name field is checked while typing too. It becomes the sandbox id, its data directory, and the WSL distribution name, so a duplicate (compared case-insensitively, since macOS and Windows would give both sandboxes one directory), an over-long name, or an unsafe character used to surface only after a create had started and downloaded its image. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Choosing an image left the field empty. open-file-picker is the attachment picker: it stats every file, enforces MAX_ATTACHMENT_BYTES (20 MB) and mints a read token for the renderer. A guest image is over a gigabyte, so the handler threw, the invoke rejected, and the dialog's un-caught .then never ran — the picker closed and nothing appeared, with no error to explain it. Picking a path is not attaching a file: nothing is read, so it gets its own channel with no budget and no read authorization. Its filters also carry an "All files" group, so an image with an unusual name stays selectable. The dialog now reports a failed pick instead of dropping it. That silence is what made a 20 MB limit look like the app ignoring the click. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The status line under the Add sandbox form read the create job's progress and treated any value as a download in flight. That progress covers fetching the image and nothing else, so it was wrong in three situations, and wrong for the whole slow part of each: - a custom image reports its full size in one go, so the dialog claimed to be downloading a file the user had picked off their own disk; - a finished download leaves progress pinned at 100% for the entire unpack, so the bar sat full while still saying "Downloading"; - an image already in the cache reports nothing, so the idle hint about first downloads stayed up for the whole create. The phase now comes from what the progress is not saying — complete, or absent, means the fetch is over and the sandbox is being built — and the dialog says "Creating the sandbox…" for that stretch. The download hint is also dropped once a custom image is chosen, since nothing will be downloaded. The rule is in create-phase.ts with tests rather than inline in the JSX; it is small, pure, and had already been got wrong three ways by eye. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Creating a sandbox always downloaded its guest image — well over a gigabyte, and impossible on a machine that cannot reach the storage host. Add sandbox now takes an optional image file, and both it and the name field are validated in the dialog instead of by a create that fails minutes in.
Custom image
Empty means download as before. A path means use that file: read where it sits rather than copied into the cache (duplicating gigabytes helps nobody), and persisted on the sandbox so Reset re-provisions from the same file.
The plumbing reuses the existing asset path rather than growing a parallel one —
localImageAsset()describes the file as the same{url, sha256, bytes}shape a published asset has, andstageAsset()short-circuitsfile:URLs. Each provider gains one branch; the marker logic, unpack and reset behaviour are untouched. The digest means a reset still re-provisions when the file behind it has changed.Validating the image
Three ways a picked file can be wrong, all caught before a create starts:
inspect.tsstreams the tarball and reads only the first bytes of each member, so a 1.3 GB image is judged in milliseconds. Measured against the four published images:Format comes from the members: a kernel plus a disk is the macOS image; a Linux root filesystem is the tarball Windows and Linux share. Those two are therefore indistinguishable — accepting one image for both is correct, not a gap, and there's a test asserting it.
Architecture comes from the kernel's own magic, the build's arch marker in a rootfs, or failing those the first ELF header in the tree. The kernel magic was verified against both built kernels: arm64 stamps
ARM\x64at offset 56, x86 stampsHdrSat 514 — both startMZ, so those offsets are what actually distinguish them.One deliberate call: an image whose architecture cannot be determined is allowed through. Refusing a valid image because the probe could not prove its arch is worse than the rare mismatch that slips past, and the guest fails loudly on first boot either way.
The dialog only renders a verdict — the main process decides, and the same check runs on
sandbox-add, so a stale or hand-crafted IPC call cannot get an unusable image past it. A path typed by hand rather than picked is tracked as unchecked until it is committed, so it can never show as approved without having been looked at.Name validation
The name is not a label: it becomes the sandbox id (
sandbox:<name>), its data directory, and on Windows the WSL distribution name. Previously only the character set was checked, and only in the dialog. Now also:Workandworkthe same data directory.All of it up front, instead of an
already addederror thrown after the image had downloaded.Testing
packages/codev-sandbox: 141 → 156 pass. 12 new inspector tests over synthetic archives (both kernel arches, marker file, ELF fallback, non-archive, non-sandbox archive, missing/empty, and the full validate matrix), plus 4 covering local-image staging.packages/app: 670 → 677 pass, 7 of them the new name-model tests.packages/desktop: 96 pass.bun typecheckclean in all three.DIVERGENCES rows updated for
platform.tsx/renderer/index.tsx(a new path-only file picker — the existing attachment picker reads the file's bytes, unusable for a multi-gigabyte image) and for the i18n families.🤖 Generated with Claude Code