Repository navigation
Description
🤖 Left by Claude Code.
Summary
Let users create workrooms in a local container, on Docker or Apple's container: a Linux sandbox per workroom, on the user's own Mac.
These are not remote workrooms. The container runs on the Mac, so it stops when the Mac sleeps. The remote headline, "the work keeps running when the laptop closes" (docs/designs/remote-workrooms.md:90), stays with the cloud providers (boxd #256/#258, exe.dev #259). The release gate #260 is unchanged.
Out of scope:
- Docker on another machine (Remote workrooms on Docker running on another machine #307).
- A development environment inside the container, meaning toolchains and agent CLIs (A development environment inside container workrooms #308). Until A development environment inside container workrooms #308 lands, a local container gives git and a terminal; tooling setup comes later.
Why
- Isolation. An agent works in its own Linux box, so it can run with permissions skipped without touching
~or other projects. - A clean environment per workroom. No port clashes between parallel workrooms.
- No cloud machine to pay for. It works with either a Codaset sign-in or just
gh(see §6). - Mostly built already. The container driver, portable derivation (Portable derivation on the container driver: the base machine and deriveFromBase #252), the agent push (Remote bootstrap: push the Linux agent on first connect, hand off on version mismatch #231), the sweep and the image smoke tests on x86_64 and arm64 (Build and smoke-test the workroom-host image (FIXTURE=0) in CI #288, workroom-host smoke test: check sshd hardening, and cover arm64 in CI #299) are all done.
Scope
1. Ship the image
- Build once, test, then publish the same image. CI builds the
FIXTURE=0image once per architecture (amd64 + arm64) and runshost-image-test.shagainst those exact images; the script must take an image reference instead of building its own. Only then does CI push them to ghcr as one multi-arch manifest and record its digest. - Each app build pins that digest, so an app version always runs the image it was tested with.
remoteHostImageremains as the override for development. - Pull explicitly, then run with
--pull=never. Todayrunhas no--pullflag (ContainerHostDriver.swift:315), so a missing local image falls back to a Docker Hub pull oflibrary/workroom-host. - Show pull progress and failures in the UI, not as a raw CLI error.
2. One driver per runtime, routed by each workroom's record
- Today
RemoteHostsbuilds oneContainerHostDriver(RemoteWorkrooms.swift:340-349).RemoteWorkrooms.createtakes that concrete type (:136) and hard-codesdriver: "container"(:165,176,212,239). - Keep one driver per kind instead.
"container"keeps meaning Docker, so existing records work; add"apple-container". Adopt, sweep and reconnect route by each descriptor's kind. - Pin the Docker context. At create, store
docker context showin the Docker record, and pass--contexton every command for that workroom. A missing context gives a clear error, and the sweep covers each recorded context. Without this, switching between OrbStack and Docker Desktop makes workrooms seem to vanish. - A base per runtime. The project's host keeps one base per (runtime, Docker context), and create uses or makes the base matching the chosen target. Today a project has one base, and its runtime is never checked (
RemoteWorkrooms.swift:139-150). - Run the runtime CLIs through
StatusCommandRunning, asBoxdHostDriverdoes, so tests can drive them with a stub CLI.
3. Support Apple container
These gaps come from Apple's command reference, not yet tried on a real install. The runtime differences go in a small dialect inside ContainerHostDriver (argument builders plus output parsing), not a second driver.
| Driver uses (Docker) | Apple container |
|---|---|
run --restart unless-stopped (:315) |
no --restart; see §4 |
ps -a --filter … --format '{{…}}', images -aq --filter (sweep, :238, :254) |
no --filter; --format takes only json/table/yaml/toml. Parse JSON in Swift |
image inspect --format '{{index .Config.Labels …}}' (:259) |
read labels from JSON |
rmi |
image delete |
commit --change "LABEL …" (derive, :136) |
commit has no label option: container commit <id> <reference>. Name each derived image workroom-<bundle id>-<epoch>-<uuid>. The rollback and the sweep find images by that prefix, and the sweep reads age from the epoch. A missing age must never count as old (today's old() treats missing as old, :231) |
shortImageID (12-hex / sha256:) |
Apple's output format is unverified; it is part of the dialect |
| daemon always running | container system start asks whether to install a kernel, so the app never runs it. A stopped service shows as a disabled reason, with the command for the user to run |
4. Start a stopped container only when it is opened
- Apple has no restart policy, and a container stopped by hand needs the same handling. When the user opens a workroom (terminal, Changes or Files), the app starts its container if it is stopped, then connects. This applies to both runtimes.
- Background status probes must not start containers. They reach every healthy remote workroom (
Models.swift:294-299,AppStore+WorkroomStatus.swift:67-68) throughconnectRemote, so a probe of a stopped container reports "stopped" instead.
5. Choose where to create a workroom
- The sidebar's New Workroom becomes a submenu (This Mac / Docker / Apple container) once more than one place is enabled. With only This Mac enabled, as in every stable build until Phase 4 gate: the success criteria on two real providers #260, it stays a plain item.
- The user picks the runtime; the app never picks one for them.
- Docker covers Docker Desktop, OrbStack and Colima, found in the usual install paths, because a GUI app's PATH contains none of them.
- An entry that can't be used is shown disabled, with its reason in its title. Reasons include: not installed, service stopped (
container system start), macOS too old, not Apple silicon, or no credentials (§6). - Every workroom records its target, and the UI shows it.
- The Remote entries (boxd, exe.dev) are not part of this issue. Each one lands with its driver (boxd live-fork derivation, as an accelerator behind deriveFromBase #258, exe.dev driver: the second real provider #259) on top of the driver-per-kind routing in §2.
6. GitHub credentials for local containers
- When signed in to Codaset and the Codaset App is installed for the repository's owner, use the broker as remote workrooms do: enrolment and App-scoped tokens.
- Otherwise, use a Mac relay:
wr-agent credential getforwards to the app over a reverse forward on the app's ssh connection, theBrokerReverseForwardsmechanism.- The app answers only for
github.com, fromgh auth tokenor the keychain. - Nothing is persisted in the container.
- This gives a container agent the same GitHub access a This Mac workroom already has.
- Remote workrooms stay broker-only.
7. Resource limits
Set CPU and memory caps per container, with a default and a Settings option. Each Apple container is a lightweight VM, so measure before picking the defaults.
8. Release
- Ships together with the Remote targets: until Phase 4 gate: the success criteria on two real providers #260 passes, Docker and Apple container stay behind
remoteWorkroomsPreviewand the Nightly check (RemoteWorkrooms.swift:15-19). - This Mac is always available.
- The local entries must not promise that work survives a closed lid.
Tests
- Stub-CLI unit tests in CI for both dialects:
- argument building
- JSON parsing
- name-prefix sweep with age from the name
- a derive racing a sweep
- error mapping
- Regression contract for existing Docker workrooms:
- An old descriptor (
"container", no context, single base) still adopts and reconnects as Docker on the current context. - The Docker sweep's rules are unchanged.
- Proof: a decoding and routing test in
RemoteHostsTests, a stub test of the Docker sweep's commands, and therun.shfixture suites passing unchanged.
- An old descriptor (
- A unit test that a sidebar status refresh leaves an unopened, stopped container stopped.
- An XCUITest of the submenu: it shows only when more than one place is enabled, and a disabled entry shows its reason.
AppleContainerIntegrationTests, opt-in withWR_APPLE_CONTAINER_TESTS=1:- covers create, derive, stop and reconnect, delete and sweep on a real Apple
container - added to the pre-release checklist, since CI's macOS runners can't run Apple containers.
- covers create, derive, stop and reconnect, delete and sweep on a real Apple
Implementation tasks
From the eng review, in dependency order: T1 first, then T2-T8 as the parallelization allows.
- T1 (P1, human: ~1d / CC: ~30min) — Core — One driver per kind, route by descriptor, keep
"container"= Docker- Files: Core/RemoteWorkrooms.swift, Core/ContainerHostDriver.swift, Core/Models.swift
- Verify:
make app-test(RemoteHostsTests),run.shsuites
- T2 (P1, human: ~4h / CC: ~15min) — Core — Pin the Docker context per workroom
- Files: Core/ContainerHostDriver.swift, Core/Models.swift
- Verify: stub-CLI test that
--contextappears on every command
- T3 (P1, human: ~1d / CC: ~30min) — Core — Base per (runtime, context)
- Files: Core/RemoteWorkrooms.swift, Core/Models.swift
- Verify: old-descriptor test, plus a test that mixed runtimes in one project derive from the right base
- T4 (P1, human: ~1.5d / CC: ~45min) — Core — Apple dialect: run args, JSON parsing, named commits with age, name-prefix sweep,
image delete, neversystem start- Files: Core/ContainerHostDriver.swift
- Verify: ContainerDialectTests (stub CLI), including the derive-vs-sweep race
- T5 (P1, human: ~3h / CC: ~15min) — Core — Start on explicit open only
- Files: Core/RemoteWorkrooms.swift, Core/AppStore.swift
- Verify: unit test that a status refresh leaves a stopped container stopped
- T6 (P1, human: ~3d / CC: ~1h) — wr-agent + Broker — Broker with a Mac-relay fallback for local containers
- Files: vcs/crates/wr-agent/src/broker.rs, Core/Broker/BrokerReverseForwards.swift, Core/RemoteProvisioning.swift
- Verify: wr-agent unit tests for the helper source, plus a relay test in RemoteProvisioningTests
- T7 (P1, human: ~1d / CC: ~30min) — CI — Build once, test those images, publish the multi-arch manifest, embed the digest
- Files: .github/workflows/*.yml, vcs/scripts/ssh-fixture/host-image-test.sh, macapp build settings
- Verify: CI run on a tag; the digest in the app matches the pushed manifest
- T8 (P2, human: ~4h / CC: ~15min) — Views — New Workroom submenu with disabled reasons
- Files: Views/ProjectSidebar.swift, Core/AppStore.swift
- Verify: XCUITest (
-only-testing) of the submenu states
- T9 (P2, human: ~4h / CC: ~15min) — Tests — Regression contract tests
- Files: WorkroomAppTests/RemoteHostsTests.swift, WorkroomAppTests/ContainerDialectTests.swift
- Verify: break the old-descriptor routing and confirm the tests go red
- T10 (P2, human: ~1d / CC: ~30min) — Tests —
AppleContainerIntegrationTests(opt-in), added to the pre-release checklist- Files: WorkroomAppTests/AppleContainerIntegrationTests.swift, docs
- Verify:
WR_APPLE_CONTAINER_TESTS=1run on an Apple-silicon Mac; it answers open questions 1-3
- T11 (P3, human: ~2h / CC: ~10min) — Core — Measure Apple VM memory, then set the default caps
- Files: Core/ContainerHostDriver.swift, Core/DefaultsKeys.swift
- Verify: a documented measurement
Acceptance
- A fresh Mac with only Docker Desktop, signed in only to
gh, with no repo checkout, creates, opens, derives and deletes a local container workroom. - The same on a Mac with only Apple
container. - A Mac with both runtimes can hold workrooms of one project on each.
- After a Mac restart, opening a container workroom brings it back on both runtimes. A status refresh starts nothing.
- Switching Docker contexts doesn't break existing Docker workrooms.
- Existing Nightly Docker workrooms keep working after the update.
- The sweep removes only this build's leftovers on both runtimes, and never a fresh derive.
- The published image digest is the one the smoke test ran against, and the app pins it.
- An entry that can't be used is shown disabled with its reason. The pull-failure case shows a message that says what to do.
Open questions
These need a probe on a real install, through the opt-in suite:
- Does Apple
container's published port survive a container restart? - What does
container commitprint, and what doescontainer list --format jsonlook like, including labels? - Can Apple
containerpull from ghcr?
Related: #228, #231, #252, #253, #258, #259, #260, #288, #299, #307, #308.
Activity
fix(macapp): build a container's run arguments in steps CI's Swift ca…
test(macapp): compare a stub runtime's calls as Strings, which CI's S…
fix(macapp): address PR #320's review comments (#309)
feat(macapp): pin each Docker workroom to its Docker context (#309)
feat(ci,macapp): publish the workroom-host image and pin it into the …
fix(macapp): address the review of Docker context pinning (#309)
feat(macapp): start a stopped container when its workroom is opened (#…
feat(macapp): run container workrooms on Apple's container runtime (#309
feat(macapp): New Workroom asks where: this Mac, Docker or Apple Cont…
feat(macapp): keep a base per container runtime and Docker context (#309
fix(macapp): address the review of the Apple container dialect (#309)
feat(wr-agent): relay git credentials from the Mac for a workroom tha…
feat(macapp): relay git credentials from the Mac's gh for local conta…
docs(remote-workrooms): describe local container workrooms as built (#…
fix(macapp): harden the credential relay's listener (#309)
28 remaining items
test(macapp): XCUITest the New Workroom places submenu (#309)
feat(macapp): show how far a container workroom's image download has …
fix(macapp): keep New Workroom's tooltip off its submenu, and UI-test…
feat(macapp): say why New Workroom's entries are off while a create r…
fix(macapp): say once why New Workroom's entries are off (#309)
feat(macapp): the row's + asks where, and a workroom's icon says what…
feat(macapp): mark a workroom on a host with the network glyph on its…
feat(macapp): a remote workroom's sidebar icon is its host glyph, not…
feat(macapp): New Workroom's picker asks where too (#309)
fix(macapp): address the pre-landing review of local container workro…
ci: keep Release off the host image, and pin a public one into each N…
test(wr-agent): pin the credential relay's error, cap and port paths (#…
fix(macapp): build a container's run arguments in steps CI's Swift ca…
test(macapp): compare a stub runtime's calls as Strings, which CI's S…
fix(macapp): address PR #320's review comments (#309)