# Desktop updates Ace ships one application bundle containing the UI, Ace Helper, and the channel worker runtime. [Sparkle 2](https://sparkle-project.org/documentation/) verifies and replaces that whole bundle. Its installers are temporary processes; Ace Helper remains the persistent background service. The current release target is Apple silicon on macOS 15 or later. ## Experience Ace checks automatically every six hours while open and shows an update notification. Settings → Updates and Ace → Check for Updates share version, progress, and error reporting. Users can turn automatic checks off. **Install and Restart** is an explicit action. Automatic downloads and installation on ordinary quit are disabled. Quitting normally leaves Ace Helper running. After choosing to install, this machine's channels and workspace tools pause while the update downloads and installs. Unfinished local channel work resumes automatically after restart. Terminal shells close and do not resume. Hosted channels stay reachable, but their workspace disconnects during the update. Updates require a running, enabled Ace Helper. Command-line hosts and helpers disabled in macOS are left alone. Development builds have no feed. The first Sparkle-enabled build requires a manual installation; previous builds do not understand this feed. Stop Ace Helper before manually replacing an existing installation, then reopen Ace. Builds predating resume receipts need their channels reopened once to resume any unfinished runs. ## Handoff and recovery The desktop's authenticated native bridge owns the updater. Two owner-only loopback requests coordinate the host: 1. `update-prepare` blocks new work, drains admitted requests, closes workspace tools, and suspends local workers. It acknowledges only after their identified processes exit. Stale PID files are never used to choose a process to signal. 2. `update-cancel` resumes workers and workspace links if the update ends before the helper exits. Before stopping workers, the host atomically writes their IDs to `$ACE_HOME/resume.json`. On startup it wakes those workers and removes successfully resumed IDs. Failed starts remain in the receipt and are logged. Ordinary helper restarts use the same mechanism. There is no second store for tasks or messages: all resumable work remains in pi-durable, and shutdown never writes abort marks. The desktop writes `$ACE_HOME/desktop-update.json` before preparing the host. It unregisters Ace Helper and waits for its process to exit before allowing Sparkle to start a download or installer. This prevents an old helper from spawning workers from a replaced bundle. The relaunched app restores the helper and removes the desktop receipt after recovery. Failures and download cancellation resume the helper while Ace is open. If the desktop is forcibly terminated or power is lost, opening Ace recovers the receipt. A recovery error remains visible with a **Resume Ace Helper** action. Keeping workers running during a download is deferred until there is an equally safe installer handoff; a slow download currently means a longer pause. Projects, databases, preferences, logs, and Keychain credentials are outside the app bundle. Installed apps need neither `.env` nor a checkout. Stable and canary have separate identifiers, helper registrations, ports, data directories, preferences, and Keychain services. Stable and development builds use the original green Ace icon; canary uses the same artwork in yellow. Both icon sets include all macOS sizes and are compiled into the signed application bundle. ## Build and verification Electrobun 1.18.1's updater deletes the current app before moving its replacement, does not authenticate its metadata or archive, and has no helper handoff. Ace instead extracts the actual application from Electrobun's build archive; its self-extracting wrapper and `Updater.applyUpdate()` are unused. The Sparkle SDK and Electrobun CLI downloads are pinned by version and SHA-256. Native desktop inspection builds the Swift package in `apps/desktop/native` with Swift 6.2 or newer; CI selects Xcode 26.2. `Package.resolved` pins Peekaboo 4.8.0 and its dependencies, and ordinary builds require those resolved versions. To intentionally update the lockfile, run `xcrun swift package --package-path apps/desktop/native resolve` and commit the resulting file. The bundle contains `libAceDesktop.dylib` and `ace-desktop-client`, with needed Swift runtime libraries discovered from their compiled dependencies. It does not ship Peekaboo's standalone app or CLI. Sparkle's license and the native dependencies' licenses and notices are included in Resources. Code is signed inside out, including the Swift runtimes, Ace Helper, and Sparkle's nested installers. The desktop client uses the app's actual bundle identifier followed by `.desktop-client`; the embedded bridge accepts only a client with that identifier and the app's Apple Team ID. Paths containing spaces are passed as individual command arguments. A release requires Developer ID signing, hardened runtime, notarization, and stapled tickets for both the app and disk image. The image contains the app and an Applications shortcut. Sparkle signs its archive and feed with Ed25519; the app embeds only the public key and enables `SURequireSignedFeed` and `SUVerifyUpdateBeforeExtraction`. Feed URLs must use HTTPS. Sparkle's 20-day feed-signing recovery window remains enabled for lost-key recovery; archive verification and Apple signatures still apply during that recovery path. Increment `apps/desktop/package.json` for each release. Sparkle compares bundle versions and does not offer ordinary downgrades. Revert a bad release by shipping a fixed build under a higher version. The initial pipeline uses complete disk images; delta generation is deferred. ## Hosting and publishing Keep the source repository private. Only signed release artifacts go to a public Cloudflare R2 bucket over HTTPS. The app contains no GitHub or Cloudflare credential. Publishing uses R2's bucket-scoped S3 credentials through Bun; it does not need an account-wide Cloudflare API token. The initial canary uses the existing `luau-updates` bucket under its own `/ace` prefix, at `https://pub-5bdeb2efd19f42f09df9efad8b20c91e.r2.dev/ace`. Luau's feed and signing key are separate. Move to a custom download domain before wider stable distribution: Cloudflare's [public development URL](https://developers.cloudflare.com/r2/buckets/public-buckets/#public-development-url) is rate-limited and intended for development traffic. ```text stable/macos-arm64/appcast.xml stable/macos-arm64/ace-stable-0.0.3-macos-arm64.dmg canary/macos-arm64/appcast.xml canary/macos-arm64/ace-canary-0.0.3-macos-arm64.dmg ``` Images have immutable URLs and one-year cache headers. Retain old images for clients that already selected them. Feeds contain the latest release and use `Cache-Control: no-store`; configure the domain's cache rules to respect that header. The publisher verifies an existing feed's signature and refuses a version regression or changed bytes at an existing archive URL. It uploads and downloads the archive to verify its public bytes, then uploads and reads back the feed. The feed is the final commit point. Channel releases are serialized by the workflow. ## Building from Ace An agent in an Ace channel can use its shell tool to run `bun desktop ci canary` from its lane. The machine needs Bun, Git, and an authenticated GitHub CLI (`gh auth login`) with permission to run this repository's workflows. Commit and push the lane first. The command refuses uncommitted changes or a remote branch that differs from the lane, and pins the build to that exact commit. Apple, Sparkle, and R2 credentials stay in the GitHub release environment. The command returns immediately with the workflow URL and run ID. Use `bun desktop ci status ` to follow progress, then `bun desktop ci download ` to retrieve the successful build. Downloads go under `apps/desktop/artifacts/ci//`; the command checks the DMG's SHA-256 against its release manifest and reports the source revision. Use `stable` instead of `canary` for the main app. Building does not publish; add `--publish` explicitly to build and publish a release. Manual builds may run at any time. A channel can use [native desktop inspection](desktop-tools.md) to examine the installed app on its execution host. Native desktop inspection in development requires an Apple Development signing identity selected with `ace.codesignIdentity` or `ACE_CODESIGN_IDENTITY` (see [UI testing](desktop.md#ui-testing)). Development builds refuse missing or ad-hoc signing, and use one fixed app identity across checkouts so permissions survive rebuilds. Release builds use the Developer ID identity below. Use source dev builds alongside installed Canary. The optional installed development helper mode must not coexist with source dev copies (see [installed Ace-dev](desktop.md#installed-ace-dev)). Keep the same signing identity for installed development builds. Changing from Apple Development to Developer ID, or to an ad-hoc signature, can invalidate Ace Helper's macOS launch constraint even when the Team ID stays the same. ## Credentials and first release Release credentials are backed up in **1Password → Dev → Ace desktop releases**. Both GitHub environments, `desktop-canary` and `desktop-stable`, are configured from that item. They share Nate's existing Developer ID identity and notarization key, and a dedicated Ace Sparkle key. The item contains the following values for rebuilding either environment: | Kind | Name | Value | | -------- | -------------------------- | ------------------------------------------------------------ | | Variable | `ACE_CODESIGN_IDENTITY` | Developer ID Application name or SHA-1 | | Secret | `ACE_CERTIFICATE_P12` | Base64 P12 export containing the certificate and private key | | Secret | `ACE_CERTIFICATE_PASSWORD` | P12 password | | Secret | `ACE_NOTARY_PRIVATE_KEY` | App Store Connect API `.p8` contents | | Variable | `ACE_NOTARY_KEY_ID` | API key ID | | Variable | `ACE_NOTARY_ISSUER` | API issuer UUID | | Secret | `ACE_SPARKLE_PRIVATE_KEY` | Exported Sparkle Ed25519 seed, base64 | | Variable | `ACE_UPDATE_PUBLIC_KEY` | Matching Sparkle public key | | Variable | `ACE_UPDATE_URL` | Public HTTPS base URL, without a channel suffix | | Variable | `ACE_UPDATE_BUCKET` | R2 bucket name | | Variable | `CLOUDFLARE_ACCOUNT_ID` | Account containing the bucket | | Secret | `R2_ACCESS_KEY_ID` | R2 S3 access key scoped to the download bucket | | Secret | `R2_SECRET_ACCESS_KEY` | Matching R2 S3 secret key | Use the backed-up signing keys for subsequent releases. Never commit exports. Follow Sparkle's [key rotation guidance](https://sparkle-project.org/documentation/#rotating-signing-keys): change the Apple identity or Ed25519 key in one release, never both together. Signed disk images provide a key recovery route when archive validation is required before extraction. Manually run **Desktop release** on the intended commit. It defaults to canary and publishing off, so the notarized artifact is reviewable before the first public release. With publishing enabled, it uploads and verifies the release. There is no recurring schedule. CI uses a temporary signing keychain and removes credentials even when the build fails. For a local release with the certificate installed, export the corresponding variables and set `ACE_NOTARY_KEY` to the `.p8` file path: ```sh bun desktop release canary bun desktop publish ``` `release` builds and verifies without publishing. `publish` uploads the prepared artifact. ## Validation The October 2, 2026 local check used a separate app identifier, helper, and data directory. Native Sparkle upgrades from 0.0.3 to 0.0.4 (ZIP) and 0.0.4 to 0.0.5 (signed DMG with hardened runtime) replaced the app, re-registered Ace Helper, and resumed interrupted real Anthropic runs without opening their channels. Old workers exited, channel data remained, and recovery receipts cleared. Corrupted feeds and archives were rejected. Download cancellation restored hosting. A canceled preparation could not stop workers after a delayed request drained, and preparation waited for a real terminal shell to exit. Canary packaging and workflow syntax checks passed, as did repository type, formatting, and lint checks. A real host accepted update coordination over authenticated loopback and rejected both operations from tailnet peers and the owner's tailnet browser. The first Developer ID canary, 0.0.3, passed Apple's notarization for both the app and disk image; both tickets were stapled and verified. The app extracted from the image passed Gatekeeper as `Notarized Developer ID`. Real R2 uploads, public downloads, cache headers, and cleanup passed. Both GitHub release environments contain the verified configuration. An Ace channel completed the first signed CI build on October 3, 2026: [canary 0.0.4, run 37088236918](https://github.com/githubnext/ace2/actions/runs/37088236918). Its agent invoked `bun desktop ci canary`, checked status, and downloaded the artifact through `bun desktop ci download`. The workflow built, signed, notarized, and verified the release; the download matched the requested source commit and DMG checksum. Publishing was off. The next [CI build, canary 0.0.5](https://github.com/githubnext/ace2/actions/runs/37088762958), includes the yellow icon and chat usage display. Its agent downloaded and verified the artifact; both the app and disk image passed local Gatekeeper and stapled-ticket checks. It replaced 0.0.3 at `/Applications/Ace Canary.app` after stopping Ace Helper. The new helper started from that bundle, the existing project list survived, and the installed icon matched the yellow source. Native checks covered Ace Helper registration, opening a folder with Cmd+O before provider setup, the project dashboard, and Updates settings showing 0.0.5. Opening the folder created no channel. The feed was not yet published at that stage, so update checks reported a feed retrieval error. Verify a published notarized upgrade on another Mac before wider distribution. [Canary 0.0.6](https://github.com/githubnext/ace2/actions/runs/37135810035), from `76435b8`, defers provider setup until agent invocation and includes the context-usage ring. An Ace channel dispatched and downloaded the signed CI build. Both the app and DMG passed local Gatekeeper and stapled-ticket checks before replacing 0.0.5 in `/Applications`. The packaged helper and workers created a channel and accepted human messages with zero available providers; rejected agent requests added no input or run. They also reopened a backed-up legacy channel without changing its history or model. The installed app preserved all 55 backed-up entries in its existing channel, created a new channel in Chat mode, and sent a human message with no model selected or agent tokens used. The temporary channel was archived with its history retained. Settings showed 0.0.6 and the existing provider keys; the authenticated helper used protocol 5 and ran from the installed bundle. Source UI checks also retained a failed dashboard agent draft through reload and successfully retried it after adding a real key, without recreating the channel or worker. [Canary 0.0.7](https://github.com/githubnext/ace2/actions/runs/37203388007) published the first signed feed and public DMG. The installed 0.0.6 app found the update through Check for Updates. Packaged helper checks must also start with launchd's system-only PATH: Bun 1.4's default `Bun.which` lookup retains that startup PATH after the helper imports the login shell environment. Pass the imported PATH explicitly when resolving developer tools. The 0.0.7 helper reproduced the false missing-GitHub-CLI error; a compiled helper with the fix read real issues and PRs using the same restricted startup environment and existing GitHub login. [Canary 0.0.8](https://github.com/githubnext/ace2/actions/runs/37204148484), from `607d91e`, published the helper tool lookup fix. [Canary 0.0.9](https://github.com/githubnext/ace2/actions/runs/37238501439), from `5b7e614`, adds persistent GitHub page caching and the shared project sidebar. The 0.0.9 checks, release dispatch, and artifact download ran through an Ace terminal. CI passed type, formatting, and lint checks; signed and notarized the app and DMG; and verified the public archive before publishing the signed feed. The download matched the requested source revision and SHA-256, and the DMG passed local Gatekeeper and stapled-ticket checks as `Notarized Developer ID`. The public feed serves 0.0.9 with `Cache-Control: no-store`.