Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -326,8 +326,8 @@ jobs:
--cache-to type=gha,mode=max,scope=ssh-fixture
run: vcs/scripts/ssh-fixture/run.sh "vcs/target/$TARGET/release/wr-agent"

# The `workroom-host` image (`make remote-host-image`, FIXTURE=0) a Nightly app's remote
# workrooms run on (#253). The step above only builds FIXTURE=1, so this is the only check on
# The `workroom-host` image (`make remote-host-image`, FIXTURE=0) an app's remote workrooms
# run on (#253). The step above only builds FIXTURE=1, so this is the only check on
# the variant's own paths (#288). It builds twice: a FIXTURE=1 control, whose apt layer is the
# one the step above caches under `ssh-fixture`, so it only reads that scope; then the host,
# under its own scope. Never one scope for both: a second export replaces the first.
Expand Down Expand Up @@ -398,6 +398,22 @@ jobs:
archive: aarch64-macos
sha256: "b23d70deaa879b5c2d486ed3316f7eaa53e84acf6fc9cc747de152450d401489"

# The Debug build bundles the Linux agents too (#227), cross-built by build-agent.sh with
# cargo-zigbuild and the Zig above.
- name: Add rustup targets (Linux agents)
run: rustup target add --toolchain stable aarch64-unknown-linux-musl x86_64-unknown-linux-musl

- name: Cache cargo-zigbuild
id: zigbuild
uses: actions/cache@v4
with:
path: ~/.cargo/bin/cargo-zigbuild
key: cargo-zigbuild-${{ runner.os }}-${{ runner.arch }}-0.23.4

- name: Install cargo-zigbuild
if: steps.zigbuild.outputs.cache-hit != 'true'
run: cargo install cargo-zigbuild --locked --version 0.23.4

# Keyed on the pinned engine sha, as in `agent-terminal-state`: a bump refetches and
# rebuilds, nothing else does.
- name: Cache libghostty-vt
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/host-image.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Host image

# Publishes the `workroom-host` image a local container workroom runs (#309), for the Nightly
# workflow, which pins the app it builds to the digest this outputs. Release joins it with #260.
# Publishes the `workroom-host` image a local container workroom runs (#309), for the Nightly and
# Release workflows, which pin the app they build to the digest this outputs.
#
# Each architecture is built once, smoke-tested as that exact image (host-image-test.sh with
# WR_HOST_IMAGE), and only then pushed by digest: a second build for the push could resolve other
Expand Down
18 changes: 15 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ concurrency:
cancel-in-progress: false

jobs:
# The `workroom-host` image this release's local container workrooms run (#309), built, tested
# and published per architecture under the release's tag; its digest is pinned into the app.
# Remote workrooms are in every build, so a stable release needs one as a Nightly does.
host-image:
uses: ./.github/workflows/host-image.yml
permissions:
contents: read
packages: write
with:
tag: ${{ github.ref_name }}

release:
runs-on: ubuntu-latest
steps:
Expand All @@ -41,10 +52,9 @@ jobs:
# NOTARY_KEY / NOTARY_KEY_ID / NOTARY_ISSUER_ID.
# Optional: SENTRY_AUTH_TOKEN / SENTRY_ORG / SENTRY_PROJECT enable Sentry dSYM upload so release
# crashes symbolicate; release.sh skips that step when any of them (or sentry-cli) is absent.
# No `workroom-host` image (#309) yet: container workrooms are Nightly-only until #260, so a
# release has nothing to pin, and an image build that failed on tag day would hold the DMG back
# after the CLI has shipped. Add the `host-image` job (as nightly.yml has it) with #260.
# The release's DMG pins the `workroom-host` image its container workrooms run (#309), by digest.
macapp:
needs: host-image
runs-on: macos-15
steps:
- uses: actions/checkout@v5
Expand Down Expand Up @@ -132,6 +142,8 @@ jobs:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
# The host image this app runs, by digest (release.sh → WorkroomHostImage).
WORKROOM_HOST_IMAGE: ${{ needs.host-image.outputs.image }}
run: |
echo "$NOTARY_KEY" | openssl base64 -d -A > "$RUNNER_TEMP/notary.p8"
NOTARY_KEY_PATH="$RUNNER_TEMP/notary.p8" make app-test APP_SIGN_FLAGS="CODE_SIGN_IDENTITY=- CODE_SIGN_STYLE=Manual CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=YES DEVELOPMENT_TEAM="
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ app-uitest: ## Run the app's UI tests (XCUITest — needs the GUI session; queue
app-identity: ## Print the bundle id this checkout's Debug build gets (one per workroom)
@echo "com.developwithstyle.workroom.dev$(APP_DEV_ID_SUFFIX)"

remote-host-image: ## Build the `workroom-host` image a Nightly or Dev app's remote workrooms run on (Docker, #253)
remote-host-image: ## Build the `workroom-host` image an app's remote workrooms run on (Docker, #253)
docker build --tag workroom-host vcs/scripts/ssh-fixture

remote-host-image-test: ## Build the `workroom-host` image, without its tag, and smoke-test it: sshd serves and is hardened, no fixture pieces, a pushed agent starts (#288)
Expand Down
7 changes: 4 additions & 3 deletions docs/designs/oq8-cross-machine-reattach.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,8 @@ Codex (gpt-6.1-sol, cold read of the repo):
- **Capability detection:** the layout requests arrive with a protocol version bump; the app reads
the agent's version at the handshake it already performs. An older agent means today's per-Mac
behaviour for that workroom: no host layout, no second-Mac restore, nothing shown to the user
(the remote UI is Nightly-only, and the bootstrap pushes the new agent on the next connect,
#231).
(the bootstrap pushes the new agent on the next connect and hands off to it, #231, in every
build since 2026-10-07).
- **Lifecycle:** a remote workroom's layout lives on that workroom's own host, so deleting the
workroom destroys the host and its layout with it. No delete request is needed in #255.
- **Session metadata:** the workroom ID, initial title and creation order travel in the attach
Expand Down Expand Up @@ -277,7 +277,8 @@ Codex (gpt-6.1-sol, cold read of the repo):

No new artifact. The agent ships inside the app (Mac and static Linux builds) and is pushed to a
host on a version mismatch (#231), so a new protocol version reaches hosts through the existing
bootstrap. The remote UI stays behind `remoteWorkroomsPreview` in Nightly and Dev until #260.
bootstrap. The remote UI stayed behind `remoteWorkroomsPreview` in Nightly and Dev until 2026-10-07, when
the owner removed that gate.

## Next Steps

Expand Down
30 changes: 18 additions & 12 deletions docs/designs/remote-workrooms.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ Mode: Builder

## Current Status — 2026-10-06

**Phase 4 is under way, and the app can now create, open and delete remote workrooms.** That works
only in a Nightly or Dev build with the hidden `remoteWorkroomsPreview` setting, and only on the
container driver (the Mac's own Docker, from the `workroom-host` image). Merged so far: host
**Phase 4 is under way, and the app can now create, open and delete remote workrooms.** Since
2026-10-07 that works in every build, stable included: the owner removed the Nightly-only gate and
the hidden `remoteWorkroomsPreview` setting before #260's success criteria were run. At #253 it
worked only on the container driver (the Mac's own Docker, from the `workroom-host` image). Merged
so far: host
descriptors in config (#249, PR #275, 2026-10-01); the credential broker service (#250, in
joelmoss/codaset#43, 2026-10-01) and its clients (#251, PR #263, 2026-09-30); portable derivation on
the container driver (#252, PR #280, 2026-10-01); the boxd driver with portable derivation (#256, PR
Expand All @@ -24,7 +26,7 @@ the agent, not a shim. Follow-ups from #253's reviews are #283 to #288; #283 (cl
ends its session on the host) is merged (PR #306, 2026-10-03), with its own follow-ups in #293, #297
and #304. The rest of Phase 4 is open: the second real provider, exe.dev (#259), and boxd live fork
(#258), which is deferred to a later release and does not gate #260. #260 is the gate: it runs the
Success Criteria on two real providers, and the remote UI leaves Nightly only after they pass. The
Success Criteria on two real providers; the remote UI already left Nightly, ahead of it (2026-10-07). The
first Nightly DMG with the Linux agent inside (#227) still has to be checked, and that check is part
of #260. Each merged item has its "As built" entry under Phase 4, except #255, which is recorded as
the answer to open question 8.
Expand Down Expand Up @@ -1476,8 +1478,7 @@ these are the subsystems that actually gate "a remote workroom is a real workroo
snapshot, a 0.5 s restore, then the reboot), not timed end to end.
- **Tests.** `BoxdHostDriverTests` (CI) pin the ssh-block parsing, the not-found matching, the
org check and the rollback with the CLI stubbed. `BoxdIntegrationTests` run on real boxd
machines, only with `TEST_RUNNER_WR_BOXD_TESTS=1`, the sandbox off and a build with
`WR_AGENT_LINUX=1`. They
machines, only with `TEST_RUNNER_WR_BOXD_TESTS=1` and the sandbox off. They
cover a derive returning only once its agent answers, even with the base's supervisor made
slow (red without the wait), and distinct ssh host keys, machine-ids and boot_ids across two
instances. Every fresh boxd
Expand Down Expand Up @@ -1580,8 +1581,8 @@ these are the subsystems that actually gate "a remote workroom is a real workroo
`RemoteHosts` makes the one `ContainerHostDriver`: `docker` from the usual install paths (a GUI
app's PATH has none), the image from the hidden `remoteHostImage` setting, an ed25519 key it
makes under `Application Support/Workroom/<bundle id>/remote`, and the label
`workroom.provisioner=<bundle id>`, so one build's sweep never takes another's hosts. A Debug
build carries no Linux agent unless built with `WR_AGENT_LINUX=1`.
`workroom.provisioner=<bundle id>`, so one build's sweep never takes another's hosts. Every
build, Debug included, carries the Linux agents.
- **The sequence** (`RemoteWorkrooms.create`): the project's GitHub repository from `gh`
(github.com only, since the broker mints for it alone); its base, built and recorded on the
project if it has none, cloned at `/home/workroom/<repo>`; then a name, taken before the
Expand Down Expand Up @@ -3346,7 +3347,10 @@ disagreement passes every test on either side alone while presenting as an empty
reattach in `wr-agent attach` (the same ssh and pty, ~100 ms, local too) is the upgrade,
and needs the stdin and resize relays re-plumbed onto a swappable stream.
- *Gated as the local hand-off is* (`AgentHandOff.isEnabled`, Nightly and Dev): one
older-agent policy on both hosts. Off, the scripts skip the hand-off and the install still
older-agent policy on both hosts. **Changed 2026-10-07:** remote hand-off is on in every
build, stable included, when remote workrooms left Nightly; the local one stays gated. A
remote agent runs under a supervisor with no idle exit, so without it a stable host would
keep its first agent until the box restarts. Off, the scripts skip the hand-off and the install still
renames, so the next probe sees a match, nothing is pushed again, and the supervisor picks
the file up on its next start.
- *The crashed restore, remotely: measured.* A binary that passes `protocol` and the restore
Expand All @@ -3359,7 +3363,7 @@ disagreement passes every test on either side alone while presenting as an empty
(`over_ssh_the_bootstrap_installs_hands_off_and_survives_a_crashed_restore`). What is
unrecoverable is the same as locally; it now costs one connect's sessions rather than a
host, and until the app is fixed every connect pushes that binary again.
- *This build has no agent for the host* (a Debug build without `WR_AGENT_LINUX=1`): a host
- *This build has no agent for the host* (a bundle missing its `wr-agent-linux-*`): a host
already holding one is connected to as it is, and a host with none is an error naming the
architecture. A host that is not a Linux this app builds for is refused before anything is
pushed.
Expand Down Expand Up @@ -3904,8 +3908,10 @@ service milestones below so each layer can be reviewed and landed independently.
first, including identity/key isolation and cleanup of instances and credentials after partial
failure. Apply the measured wakefulness policy through the far-side shim (#257: through the
agent's own heartbeat instead). Add boxd's fast
derivation only after the portable path passes. Remote UI remains Nightly-only until the success
criteria pass; the container fixture alone does not establish parity across two real providers.
derivation only after the portable path passes. ~~Remote UI remains Nightly-only until the success
criteria pass~~ — the owner lifted that gate on 2026-10-07, ahead of #260, which now verifies
the criteria after the fact; the container fixture alone does not establish parity across two
real providers.
**Filed 2026-09-27** as #249 (host descriptors; built 2026-10-01, PR #275), #250 (broker service;
built 2026-10-01, joelmoss/codaset#43) → #251 (broker clients; built 2026-09-30, PR #263) → #252
(portable derivation on the container driver; built 2026-10-01, PR #280) → #253 (remote workrooms
Expand Down
12 changes: 6 additions & 6 deletions macapp/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,13 @@ the app**: without it a reattaching pane repaints blank, which only shows up aft
quit-and-relaunch. `wr-agent protocol` reports `terminal-state yes|no`, and the build test asserts it
against the shipped binary.

**Linux agents (issue #227).** Release and Nightly builds also put a static musl `wr-agent` per
**Linux agents (issue #227).** Every build also puts a static musl `wr-agent` per
Linux arch in `Contents/Resources/wr-agent-linux-{aarch64,x86_64}`, for pushing to remote hosts.
Both arches ship always, whatever `ARCHS` says, because a remote box's arch has nothing to do with
the Mac's. They are not codesigned; the app's signature seals them as resources. Debug skips them
(and removes stale ones) unless `WR_AGENT_LINUX=1`. Building them needs `cargo install
the Mac's. They are not codesigned; the app's signature seals them as resources. Debug builds them
too, since every build offers remote workrooms. Building them needs `cargo install
cargo-zigbuild --locked` and `rustup target add aarch64-unknown-linux-musl
x86_64-unknown-linux-musl`, which the release workflows install. `release.sh` asserts both ELFs are
x86_64-unknown-linux-musl`, which the CI and release workflows install. `release.sh` asserts both ELFs are
present and static, and the `agent-linux` CI job runs `protocol` on each under Linux.
`AgentBootstrap.connect` (#231) pushes the matching one to a remote host on first connect, beside
the agent's socket, and hands the running agent off to it; the far side is
Expand Down Expand Up @@ -203,7 +203,7 @@ side is `Core/Broker/`:
- `AgentEnrolment.swift`: creates a grant and runs `wr-agent enrol` over `HostDriver.exec`, code on
stdin. The agent side is `wr-agent enrol` / `wr-agent credential` (`vcs/crates/wr-agent/src/
broker.rs`), git's credential helper on the remote host.
- `BrokerSession.swift` backs a Settings ▸ General row hidden behind `remoteWorkroomsPreview`.
- `BrokerSession.swift` backs the Settings ▸ General ▸ Remote workrooms row.
`BrokerEndpoint` decides which Codaset a build talks to. Release and Nightly use codaset.dev,
or a `brokerURL` override that is https or `http://127.0.0.1`, as the agent accepts. **A Debug
build (the Dev app and every test host) never reaches another machine**: it defaults to the
Expand All @@ -225,7 +225,7 @@ is told its org and account (`Configuration.org`, `Configuration.account`; every
account's org is nil, so the org alone cannot tell two accounts apart) and refuses to act while
another is active (#356). Machines and snapshots are named
`<prefix>-<host id>`, so cleanup and a relaunched app find them by ID. `BoxdIntegrationTests`
make real machines: they run only with `TEST_RUNNER_WR_BOXD_TESTS=1`, a `WR_AGENT_LINUX=1` build,
make real machines: they run only with `TEST_RUNNER_WR_BOXD_TESTS=1`
and the sandbox off. The app reaches it from New Workroom › Remote › boxd (#356): `RemoteHosts`
holds any driver by `DriverKey` (a container runtime or a boxd org and account), a background read
of a boxd host asks boxd first and leaves an asleep box asleep, and an idle host's connection is
Expand Down
13 changes: 4 additions & 9 deletions macapp/Scripts/build-agent.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
# not collide with the app executable "Workroom" on a case-insensitive filesystem. It goes there
# because that is where `PersistentSessionPaths.binaryURL(for:)` looks, beside workroom-session.
#
# Release and Nightly builds also put a static Linux agent per arch in Contents/Resources, for remote
# hosts. See the end of this file.
# Every build also puts a static Linux agent per arch in Contents/Resources, for remote hosts. See
# the end of this file.
#
# Env vars provided by Xcode: SRCROOT, TARGET_BUILD_DIR, EXECUTABLE_FOLDER_PATH,
# UNLOCALIZED_RESOURCES_FOLDER_PATH, EXPANDED_CODE_SIGN_IDENTITY, ARCHS, DERIVED_FILE_DIR,
Expand Down Expand Up @@ -181,14 +181,9 @@ fi
# Resources, not MacOS, and not codesigned. codesign sees an ELF as data, and the app's own
# signature seals it like any other resource.
#
# Release and Nightly only, unless WR_AGENT_LINUX=1. They are two more release builds of the agent,
# and a Debug app has no remote host to push them to yet. A build that skips them also removes any
# that an earlier opt-in build left, so a bundle never carries a stale agent.
# Every configuration, Debug included: every build offers remote workrooms, and a host with no agent
# needs one pushed from this bundle. Cargo caches both builds, so only the first costs a compile.
RES_DIR="${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
if [ "${CONFIGURATION:-Debug}" = "Debug" ] && [ "${WR_AGENT_LINUX:-}" != "1" ]; then
rm -f "$RES_DIR/${HELPER_NAME}-linux-"*
exit 0
fi

if ! command -v cargo-zigbuild >/dev/null 2>&1; then
echo "error: the Linux agents need cargo-zigbuild. Run 'cargo install cargo-zigbuild --locked'." >&2
Expand Down
Loading
Loading