Skip to content

LOCAL: update SwapLabs macOS builds in place with a verified self-signed installer - #16

Merged
kiwidream merged 11 commits into
swaplabs/mainfrom
fork-update-sources/mac-self-update-app
Sep 28, 2026
Merged

kiwidream merged 11 commits into
swaplabs/mainfrom
fork-update-sources/mac-self-update-app

Conversation

@kiwidream

@kiwidream kiwidream commented Sep 25, 2026 •

Copy link
Copy Markdown
Files Added Deleted Net
Test 22 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​3158 $\color{#cf222e}{\Huge{\mathbf{−}}}$​42 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​3116
Prod 37 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2863 $\color{#cf222e}{\Huge{\mathbf{−}}}$​54 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2809

ELI5

Orca on a Mac normally updates itself through Apple's Squirrel mechanism, which only accepts a new copy of the app that carries the same Apple Developer ID signature as the running one. SwapLabs builds are signed with our own certificate, so Squirrel would refuse every SwapLabs update and the "Download" button could only send people to a page to install a DMG by hand.

This change gives SwapLabs macOS builds their own installer inside Orca. When a new SwapLabs release appears, Orca downloads it, checks it very carefully (a signed description of the file, the file's size and checksum, and the new app's code signature and identity), and then, when you press "Restart to update", a tiny helper swaps the old app for the new one, relaunches it, and puts the old one back if the new one does not show a working window within 90 seconds.

What Changed

Before: on a SwapLabs macOS build, Settings → Updates showed "Open download page (SwapLabs)" for the fork's own next build. Background checks found nothing installable (the fork publishes no latest-mac.yml).

After (only when every condition below holds): routine checks, wake/focus checks, the menu check, the nudge campaign and the Updates button all work as on Linux: "Download 1.4.197-swaplabs.… (SwapLabs)" → progress → "Restart to update" (or install on next quit) → the app quits, is replaced, and relaunches into the new build. A refused or failed update shows an error in the update card with a link to the release page; a rollback is reported by the build that came back.

Conditions: macOS, a packaged SwapLabs build (…-swaplabs.<stamp>), compiled with the new ORCA_SWAPLABS_UPDATE_PUBLIC_KEY define (the raw 32-byte Ed25519 public key; validated at build like ORCA_RELEASE_SOURCES), a running bundle whose designated requirement is identity-bound (a self-signed certificate, not an ad-hoc cdhash), and a process that is not a supervised orca serve. Otherwise nothing changes, except that requiresManualInstall now correctly says "manual" for a fork build's own next build on macOS (decision D4), where it used to promise an in-app install Squirrel would have refused.

Supervised orca serve is out of scope (user decision, review round 7): a process the serve supervisor started keeps the manual update path a fork build had before (the DMG is listed, the buttons say "Open download page"), and records one lifecycle diagnostic (updater_mac_self_update_inactive, reason supervised-serve) saying why. The helper's swap, relaunch and health watch are written for the desktop app, where a window is the health signal, while the supervisor restarts a serve on its own terms; teaching the two to cooperate is a possible follow-up PR. The supervisor, the serve handoff record and printServeReady are untouched by this PR. An unsupervised headless serve defers downloads and installs as it always has.

Mechanism: src/main/updater/mac-self-update/ adds an update engine with the same surface and events as electron-updater's AppUpdater (UpdateEngine), and getAutoUpdater() picks it over electron-updater when the conditions hold. Every existing handler, guard, the pinned/cross-source flow, requiresManualInstall, the mac installer-ready gate and the exit watchdog run unchanged; the engine's own update-downloaded is the installer-ready signal that Squirrel's native event used to be.

  • Check: the existing atom-feed preflight pins the newest fork tag (readiness and listing read swaplabs-update-mac-<arch>.json + .sig in place of latest-mac.yml). The engine fetches manifest and signature (64 KiB / 1 KiB caps) and refuses unless, in order: Ed25519 signature over the exact bytes; strict parse; source, arch, bundleId match; version belongs to the source and is newer (routine) or exactly the pinned target; designatedRequirementSha256 equals the running bundle's.
  • Download: streams the zip under the size and sha512 the signed manifest fixed (a transfer past the size is abandoned), ditto -x -k into .<App>-update-staging/ beside the bundle (same volume, so the swap is a rename), codesign --verify --deep --strict, CFBundleIdentifier, CFBundleShortVersionString, designated requirement equal to the running app's, then strips com.apple.quarantine. Any failure removes staging and the zip and carries the tag's release page as manualInstallUrl. An unwritable app location is refused before the download. The staging directory belongs to the download for as long as it runs: a check that supersedes the offer mid-extract leaves it alone, and the download discards it itself as superseded.
  • Restart: after the existing quit-and-install sequence, the engine records install-state.json and starts a detached /bin/sh -c helper. The script is a fixed string; every path and number is an argv entry. It waits for the pid to exit, moves the bundle to .<App>-update-rollback/, moves the staged bundle in, relaunches with /usr/bin/open, and waits up to 90 s for the health marker the new app writes once its first window can paint (ready-to-show), and on no other signal: no fallback timer, so a renderer that hangs or crashes before it is never certified. Without the marker it stops the new build, restores the rollback and relaunches the old build.
  • Next launch: writes the marker, records updater_mac_self_update_completed/_failed with the helper's one-word outcome, shows a rollback in the update card, discards a never-installed bundle, prunes the rollback a few minutes after a healthy launch.

Spawns go through src/shared/child-process only. Docs: docs/reference/swaplabs-fork-builds.md gains a "macOS self-updates" section with the asset contract, activation rules (including the supervised-serve exclusion) and the unverified list.

Also in this PR: config/scripts/swaplabs-mac-update-manifest.test.mjs (from #15) tampered with a signature by replacing its first character with a fixed letter, which about one fresh key pair in 64 already starts with; that run then saw no mismatch and failed. The tamper now always changes the character.

Why

Plan §13 (user decisions 2026-09-25): self-signed certificate, Ed25519-signed manifest, same UX as upstream. Alternatives considered:

  • Keep Squirrel and sign with Developer ID: no Apple credentials.
  • Sparkle or a second updater library: a new dependency and a second state machine beside electron-updater's, with the Updates buttons, nudges and remote RPC all reimplemented; the engine adapter reuses everything instead.
  • Verify only the zip hash: the manifest naming the hash must itself be trusted, so it is signed, and the bundle is verified after extraction because an archive cannot prove a signature.
  • A JavaScript helper: it would have to run from the bundle being replaced; a fixed /bin/sh program with argv-only inputs has no interpolation surface and no interpreter beyond mv, ps, kill and open.
  • Supervised serve in this PR: round 6 tried it (the helper watching the serve handoff, the supervisor restarting a restored build) and review found three more ways the supervisor and the helper could disagree about a graceful stop or a late rollback. Descoping it keeps the PR to one health signal (a window) and one relaunch owner (the helper).

Security model: the compiled-in public key is the root of trust (a shell export cannot re-key it); nothing from a release is parsed before its signature verifies; nothing downloaded is executed before codesign --deep --strict passes; the only programs run are Apple's own under /usr; the helper cannot be steered by content; signing is never weakened.

Linked Issue

Fixes # — fork-only work under plan §13 (Phase 5B); the pipeline side is #15.

Visual Proof

N/A — the Updates section, card and copy are unchanged; only which builds are offered in-app changes, and only on a real signed Mac build.

Testing

Linux (minidev), all passing:

  • pnpm tc
  • pnpm test src/main/updater src/main/updater.mac-self-update.test.ts src/main/updater.headless-serve-install.test.ts src/cli/runtime/launch.test.ts src/main/serve-update-handoff.test.ts src/main/serve-update-handoff.app-environment.test.ts src/main/startup/desktop-startup-ordering.test.ts src/main/startup/first-window-deferral.test.ts src/main/update-install-exit-watchdog.test.ts and the config/scripts/swaplabs-* / verify-swaplabs-packaging suites, the child-process import boundary and both fetch call-site audits
  • ORCA_CODE_QUALITY_BASE=refs/remotes/swaplabs/swaplabs/main pnpm run check:code-quality:changed — passed; oxlint, oxfmt --check, pnpm run audit:anti-slop, pnpm run check:max-lines-ratchet — passed
  • No renderer strings changed, so the localization verifiers were not run.

New suites: manifest verification (good signature, tampered bytes, foreign key, wrong source/arch/bundle id/identity, downgrade, pinned mismatch, malformed fields), verified download (size/hash mismatch removal, progress), bundle verification and quarantine (mocked runProcess), helper argv and a real /bin/sh run of the script over a temp tree (swap + healthy, rollback on missing marker, missing staged bundle, blocked or failed restores, a new build that ignores SIGTERM or survives SIGKILL), install-state and launch-outcome, the first-window deferral (a window-only task never runs on a timer), activation (inactive under a supervised serve, with one diagnostic), the engine end to end with fakes (including a check that supersedes the offer mid-extract), and the updater integration (routine check → available → download → downloaded → Restart spawns the helper and arms the exit watchdog; refusal surfaces manualInstallUrl/retryable: false; listSources in-app once active; electron-updater path unchanged when unsupported).

UNVERIFIED on a real Mac (no macOS host in this session): TCC grants surviving the swap with the self-signed identity; Gatekeeper on relaunch after the quarantine strip for a non-notarized bundle; renaming the bundle under a still-running terminal daemon; open relaunching promptly after the previous instance's exit; codesign/PlistBuddy output shapes on current macOS.

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

AI Disclosure

Claude Fable 5.1 (Claude Code) wrote this change under the coordinator's plan.

Review

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Security: see the model above. Cross-platform: the engine is created only on darwin; Linux/Windows and upstream builds keep electron-updater. Remote SSH: the status shape is unchanged (manualInstallUrl/retryable were already optional); a paired SwapLabs macOS host updates through the same path via updater.* RPC, unless it is a supervised serve, which stays manual. Mobile: untouched. Backwards compatibility: a build compiled without the key behaves as before, minus the incorrect in-app promise.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

🤖 Generated with Claude Code

@tenki-reviewer

tenki-reviewer Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Review complete. 🟠 1 high

💬 Inline comments (1)

Reviewed commit: b5e0e67

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-28T20:53:03.039747Z a2d83d8 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@tenki-reviewer tenki-reviewer Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Expand to see previous review

Adds a custom macOS self-update engine (Ed25519-signed manifest download, verify, shell-swap install with rollback) alongside release build/channel selection updates.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b5e0e67969

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/main/updater/updater-status.ts Resolved
@tenki-reviewer

tenki-reviewer Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Review complete. 🟡 2 medium

💬 Inline comments (2)

Reviewed commit: 2b75969

@tenki-reviewer tenki-reviewer Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Expand to see previous review

Adds a signed-manifest macOS self-update path for SwapLabs fork builds behind a new UpdateEngine abstraction, with release-channel/feed probing and test coverage; two medium-severity race/timeout issues remain.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2b759695e8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@tenki-reviewer

tenki-reviewer Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Review complete. 🟡 1 medium

💬 Inline comments (1)

Reviewed commit: 1f63026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1f630262c7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@tenki-reviewer tenki-reviewer Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Expand to see previous review

Replaces electron-updater with a signed-manifest staged-bundle macOS self-update engine plus release-source/channel plumbing; one medium finding on a fail-open version check.

kiwidream and others added 3 commits September 28, 2026 11:21
…ned installer

Squirrel.Mac refuses a bundle without upstream's Developer ID requirement, so a
SwapLabs build on macOS now updates through Orca's own installer. The pinned
tag's Ed25519-signed manifest is verified against the compiled-in
ORCA_SWAPLABS_UPDATE_PUBLIC_KEY, the zip against its size and sha512, and the
unpacked bundle with codesign --deep --strict, its Info.plist identity and
version, and the running app's designated requirement, before a static /bin/sh
helper swaps the bundles after quit, relaunches, and rolls back when no health
marker appears within 90 s. The next launch reports the outcome.

The engine speaks electron-updater's events and surface, so the updater state
machine, cards, buttons, nudges and the quit-and-install sequence are unchanged.
Without the key, or on any other platform or source, nothing changes except that
a fork build's own next macOS build is now a manual download (D4) rather than a
Squirrel install that would fail.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…failures

PR #16 review round 1.

- Run the update download through a stream pipeline so a full or unwritable
  disk rejects the download (retryable, partial file removed) instead of an
  unhandled write-stream error crashing the main process.
- Cut off manifest and signature reads at their byte caps while the body
  streams, instead of buffering the whole response before checking.
- Relaunch the previous bundle when the helper fails before or during the
  swap (staged bundle missing, no rollback folder, either rename refused),
  so a failed update never leaves Orca closed; the supervisor case still only
  reports.
- Refuse "Check for local build" before its file dialog while the
  self-updater is active: the loopback latest-mac.yml feed cannot pass the
  signed-manifest check, and a local build never carries the release identity.
- Name the injected Electron net.fetch dependency `fetchAsset` so the
  global-fetch audit no longer sees a bare fetch call; production wiring is
  unchanged (net.fetch in mac-self-update-activation.ts).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… rollback from breaking the mac self-updater

PR #16 review round 2.

- Revalidate the offer when a download finishes staging: a check that
  completed meanwhile and found another build now fails the download as
  `offer-superseded` (retryable, pointing at the newer release page) and
  discards the staging directory, instead of leaving the replaced build
  staged for Restart. A failed check or one that finds the same build
  discards nothing.
- Compare builds by version and sha512, so a release cut again under the
  same version also drops the bundle staged from the earlier cut.
- Listen for the helper child's `error` event. Node reports a refused spawn
  (EAGAIN, EMFILE, EACCES, ENOENT) as a pid-less child whose error fires a
  tick later; unheard it was an uncaught exception in the main process. The
  cause is recorded as `mac_self_update_helper_spawn_error`.
- Stop the helper with `rollback-dir-failed` and relaunch the untouched app
  when the previous rollback cannot be removed: `mv` onto the surviving
  directory nested the installed bundle inside it, so a later rollback
  restored a directory with no executable at the bundle path.
- Document that the small-asset deadline covers the body as well as the
  headers, and pin how a body cut off by it is reported. Electron's
  net.fetch errors the body when its signal aborts, so no timer was added.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@kiwidream
kiwidream force-pushed the fork-update-sources/mac-self-update-app branch from 1f63026 to e32e345 Compare September 28, 2026 15:23
@tenki-reviewer

tenki-reviewer Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Review complete. 🟠 1 high

💬 Inline comments (1)

🧹 Nitpicks (2) — 🟢 2 low
  • 🟢 Picker lists fork release before its signature uploads (updater-release-builds.ts:153) — hasMacSelfUpdateManifest marks a fork macOS release installable as soon as the per-slice manifest JSON asset exists (updater-release-builds.ts:158-161), but the readiness probe additionally requires the .sig asset because it is uploaded last (updater-release-asset-readiness.ts:198-201).
  • 🟢 Error settle drops a retryable-only verdict (updater-check-state.ts:325) — sendSettledCheckStatus routes settled errors without manualInstallUrl through sendErrorStatus without passing the source status's presentation (updater-check-state.ts:326), which recomputes an empty presentation and silently drops a retryable-only verdict the caller attached.

Reviewed commit: e32e345

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e32e345f9a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/main/updater/updater-setup.ts Resolved

@tenki-reviewer tenki-reviewer Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Expand to see previous review

Adds a verified macOS self-updater (Ed25519-signed manifests, streamed downloads, bundle swap via a fixed-string shell helper) alongside release-channel and release-source plumbing.

…and its refusals honest

Helper (real /bin/sh tests, each failing on the old script):
- every restore of the previous bundle runs only into an empty app slot; a
  move-aside that leaves the slot occupied reports rollback-blocked and
  relaunches the occupant instead of nesting the good bundle inside it, and a
  restore that fails into an empty slot reports rollback-failed
- the staged move is skipped, and the previous bundle restored, when something
  reoccupied the slot between the two renames
- the unhealthy build is stopped with TERM then KILL and the helper waits for
  it to be gone before it swaps and relaunches, so the relaunch cannot hand off
  to a process that still owns the single-instance lock
- outcomes are reported before the relaunch, so the relaunched app finds them

Updater state machine:
- a download the engine refused as superseded no longer ends in an error card
  whose Retry could be dead; the engine rejects without an error event and the
  download handler answers with a fresh user-initiated check that re-offers the
  newer build, in both event orderings
- a pinned jump the engine refuses keeps its manual-install page, retry verdict
  and release source when the check's rejection repeats the error event's status
- reading the previous launch's failure for the updater card no longer writes
  the health marker, so a 15 s crash-loop setup fallback cannot certify a build
  whose window never showed
- the staged bundle's version check fails closed on an unparseable or empty
  CFBundleShortVersionString

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@tenki-reviewer

tenki-reviewer Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Review complete. 🟡 2 medium

💬 Inline comments (2)

Reviewed commit: 501a015

@kiwidream

Copy link
Copy Markdown
Author

Rebased onto swaplabs/main (#15 merged; one conflict in docs/reference/swaplabs-fork-builds.md, resolved to main's table with this PR's self-update section kept) and pushed 501a015 with the review round's fixes. Every fix carries a test that fails on the previous code.

Helper rollback can no longer nest a bundle inside another (mac-self-update-helper.ts)

  • Every restore of the previous bundle now runs only into an empty app slot. When the move-aside of the unhealthy build fails and the slot stays occupied, the helper reports rollback-blocked, leaves the previous build in the rollback folder and relaunches whatever occupies the slot; a restore that fails into an empty slot reports rollback-failed. The staged move is skipped, and the previous bundle restored, when something reoccupied the slot between the two renames.
  • Audit of every mv in the script: mv "$app" "$rollback" (destination proven absent by the rm -rf + -e check), mv "$staged" "$app" (slot vacated by the previous rename, now also checked), the three mv "$rollback" "$app" restores (now guarded), the two mv "$app" "$staged" move-asides (a foreign directory at $staged would only nest the discarded build inside the disposable staging folder, and the slot check after it catches a failed move), and mv -f "$outcome.tmp" "$outcome" (a file rename under the app's own state folder).
  • The unhealthy build is stopped with TERM, then KILL, and the helper waits (bounded) for the matched process to be gone before it swaps and relaunches, so the relaunch cannot hand off to a process that still owns the single-instance lock. Outcomes are written before the relaunch so the relaunched app finds them.
  • Real /bin/sh tests over a temp tree for each branch: move-aside blocked after a health timeout, move-aside blocked after a failed launch, a slot reoccupied between the renames, a restore that fails into an empty slot, and a new build that ignores SIGTERM.

A superseded download re-offers the newer build instead of a dead Retry (mac-self-update-engine.ts, updater-download-install.ts)

  • The engine rejects an offer-superseded download without emitting error, and the state machine's download handler answers that one rejection with a fresh user-initiated check, so the card goes checking → available <newer> and Download fetches the newer build. This is the one shared-state-machine touch: the overtaking check clears availableVersion on checking-for-update, and when download progress lands before that check's update-available settles, the newer version never reaches the card, so Retry Download had nothing to download. Only the state machine can re-offer; the engine's events are dropped without an active check attempt.
  • Tested through the updater state machine in both orderings (check settles before the download; progress lands before the check settles).

Pinned refusals keep their presentation (updater-build-selection.ts): the check's rejection now repeats the error event's status (manual-install page, retry verdict, release source) instead of overwriting it with a plain error.

Launch failure lookup is read-only (mac-self-update-launch-outcome.ts): reading the previous launch's failure for the updater card no longer writes the health marker, so the 15 s crash-loop setup fallback cannot certify a build whose window never showed.

Bundle version check fails closed (mac-self-update-bundle.ts): an unparseable or empty CFBundleShortVersionString is refused as bundle-version-mismatch; the manifest side was already validated by verifyMacSelfUpdateManifest.

Gates: pnpm tc, the updater and mac-self-update suites (100 files, 1041 tests), global-fetch and proxy-guarded-fetch audits, child-process import boundary, check:code-quality:changed against swaplabs/main, oxlint, oxfmt, audit:anti-slop, max-lines ratchet: all green on Linux. UNVERIFIED on a real Mac, as before.

🤖 Generated with Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 501a015c50

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@tenki-reviewer tenki-reviewer Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Expand to see previous review

Adds a verified-signature macOS self-update engine with staged bundle swap, plus release-source readiness probing; two medium issues found in the installability predicate and error-state handling.

Comment thread src/main/updater-release-builds.ts Resolved
Comment thread src/main/updater/updater-check-state.ts Resolved
…ses and keep a settled check's retry verdict

The readiness preflight read a SwapLabs self-update manifest whole before
anything about it was checked, so an oversized asset could exhaust the main
process before the installer's own 64 KiB cap ever ran. It now streams the
manifest through the shared bounded reader under that same cap and answers
'not-ready' the moment the limit is passed, cancelling the body.

The dev picker listed a fork release as installable on the strength of its
manifest alone. The installer fetches the zip, the manifest and its signature,
so a release caught between uploads (or with one asset removed by hand)
became a row that 404s on selection. The picker now requires all three, from
one shared asset list, and the preflight's stale upload-order comment is gone.

sendSettledCheckStatus rebuilt an error status from its message, dropping a
retry verdict that had no manual-install page beside it. sendErrorStatus now
takes the presentation itself, every settled error hands its own through, and
the repeat guard compares the verdict too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@tenki-reviewer

tenki-reviewer Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Review complete. No issues found — approved ✅.


Adds a SwapLabs-fork macOS self-update pipeline (signed-manifest Ed25519 verification, size-capped verified download, staged bundle validation, launch-outcome reporting) wired into the existing updater state machine, plus cross-source install-mode rules and tests.

Reviewed commit: 046d645

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 046d645fb7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/main/updater-release-builds.ts Resolved
tenki-reviewer[bot]
tenki-reviewer Bot previously approved these changes Sep 28, 2026
…k while the new build lives and probe beside the app before staging

Review round 5 on the macOS self-updater.

- Release list: on a build Orca's own installer updates, a release is
  listed only when the slice's zip, manifest and signature are attached.
  The platform rule (`latest-mac.yml`) used to be accepted as an
  alternative, so a release carrying it with an incomplete slice was
  offered and then failed the manifest preflight on selection.
- Helper: the rollback now checks that the new build is off the process
  table after SIGTERM and SIGKILL. One that survives still holds the
  single-instance lock, so the helper leaves both bundles in place,
  relaunches nothing and records `new-app-still-running`. The check runs
  for a build that could not be launched as well as one that never
  reported healthy.
- Engine: the writability probe runs before the staging folder is
  touched and is made in the folder holding the app, where the swap
  renames. A parent a non-admin user cannot write is refused as
  `app-location-unwritable` (not retryable, release page attached)
  instead of a raw EACCES from `mkdir`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@tenki-reviewer

tenki-reviewer Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Review complete. 🟡 2 medium

💬 Inline comments (2)

🧹 Nitpicks (1) — 🟢 1 low
  • 🟢 Rollback-restore failure leaves no bundle and no relaunch (mac-self-update-helper.ts:104) — In the helper script, restore_previous reports rollback-failed and exits 1 when mv "$rollback" "$app" fails (src/main/updater/mac-self-update/mac-self-update-helper.ts:104-106).

Reviewed commit: 348d5a3

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 348d5a301c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@tenki-reviewer tenki-reviewer Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Expand to see previous review

Adds a signed staged-bundle Mac self-update path with a shell helper for bundle swap/rollback, plus updater state and release-feed changes; three medium/low robustness findings flagged.

@tenki-reviewer
tenki-reviewer Bot dismissed their stale review September 28, 2026 17:13

A newer Tenki review found issues on this pull request.

… list by the installer in use and retry the health marker

Review round 6 on the macOS self-updater.

- Supervised `orca serve`: the helper used to exit right after the swap
  when the supervisor owned the relaunch, so a replacement that crashed
  or never sent `orca:serve-ready` stayed installed with the previous
  build in the rollback folder and the supervisor exited. The helper now
  reports `swapped` and keeps its health watch: the serving runtime
  writes the marker from `printServeReady` (the window path's 20 s
  fallback no longer certifies a headless launch), a `failed` phase in
  the serve handoff ends the wait at once (`supervisor-rejected`), the
  serve command line is matched when stopping the new build, and the
  rollback is restored without relaunching. The handoff records that
  Orca's own installer performed the swap, and on that record the
  supervisor waits for the previous build to come back and starts it
  instead of exiting.
- Release list: `getMacSelfUpdateSourceFor` applies the manifest contract
  to the running source only, and the picker's default listing keys on
  the installer that is in use (`getMacSelfUpdateInstallerSourceFor`):
  the slice's zip, manifest and signature when Orca's own installer can
  act for this build, otherwise the DMG a hand install needs. An ad-hoc
  build and any other source's releases used to be filtered by assets the
  installer would never fetch for them.
- Launch outcome: a refused health-marker write is retried every 5 s
  within the helper's window instead of being swallowed once and cached.
- Local builds: the staged-bundle refusal now runs after the in-flight
  guards, so a click mid-check or mid-download is ignored as before
  rather than replacing the card and restoring the release feed under
  the running attempt.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@tenki-reviewer

tenki-reviewer Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Review complete. 🟡 3 medium

💬 Inline comments (2)

📍 Outside the diff (1)

  • 🟡 Supervisor waits full timeout for helper-abandoned installs (serve-update-supervisor.ts:108–115) — When the helper gives up without swapping (staged-missing, app-still-running, rename-current-failed, rollback-dir-failed), the bundle stays at fromVersion but the handoff stays install-requested, so the supervisor blocks in waitForMacBundleVersion for the full 120 s budget before recording failure — the server stays down two minutes beside a runnable old build it could restart immediately (src/cli/runtime/serve-update-supervisor.ts:108).
🧹 Nitpicks (2) — 🟢 2 low
  • 🟢 Engine fixture leaks its tmp dir when construction throws (mac-self-update-engine.fixture.ts:112) — createMacSelfUpdateEngineFixture calls mkdtempSync first, then performs fallible steps (mkdirSync, writeFileSync, resolveMacSelfUpdatePaths, key generation) with no try/finally (src/main/updater/mac-self-update/mac-self-update-engine.fixture.ts:112).
  • 🟢 Helper script runs PATH-resolved mv/ps/rm (mac-self-update-helper.ts:79) — The helper's swap program invokes mv, ps, rm, mkdir, dirname and sleep by bare name, so they resolve through the PATH the app inherited from its launcher (src/main/updater/mac-self-update/mac-self-update-helper.ts:79).

Reviewed commit: 4dbfe35

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4dbfe35776

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@tenki-reviewer tenki-reviewer Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Expand to see previous review

Adds a signed, self-hosted macOS self-update flow (Ed25519-verified manifest, streamed download, bundle swap helper) plus a serve-update handoff supervisor, with mostly solid security foundations but a few supervisor and staging race gaps.

…lth from the window alone and pin the staging race

Review round 7 on the macOS self-updater.

- Supervised `orca serve` (user decision): the installer no longer activates
  in a process the serve supervisor started (`getMacSelfUpdateSupport`
  reports `supervised-serve`, once, as a lifecycle diagnostic), so such a
  host keeps the manual update path a fork build had before. Round 6's
  supervisor integration goes with it: the supervisor, the shared handoff
  record (`installer` field), `requestServeUpdateHandoff`, `printServeReady`
  and their tests are back at swaplabs/main, and the helper has no serve
  branches (no handoff argument, no `swapped`/`supervisor-rejected`
  outcomes, no `--serve` process match, no relaunch-less mode); it always
  relaunches and health-checks. Serve support is a possible follow-up.
- Health signal: the first window's `ready-to-show` is the only thing that
  writes the health marker (`runOnlyAfterFirstWindowShown`); the 20 s
  fallback timer used to certify a renderer that hung or crashed before it,
  so the helper kept an unusable build and pruned its rollback.
- Staging race: a check that supersedes the offer while `ditto` is still
  extracting leaves the staging directory to that download, which discards
  it as superseded; a test holds the extract to pin that.
- CI: #15's manifest verifier test tampered with a signature by replacing
  its first character with a fixed letter, which one fresh key pair in 64
  already starts with; the tamper now always changes it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@tenki-reviewer

tenki-reviewer Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Review complete. 🟡 1 medium

💬 Inline comments (1)

Reviewed commit: 5690d15

@tenki-reviewer tenki-reviewer Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Expand to see previous review

The mac self-update quarantine handling strips the extended attribute only from files directly inside the staged bundle, leaving nested resources unquarantined and Gatekeeper-unverified.

kiwidream and others added 2 commits September 28, 2026 16:24
…elf-update-app

Brings in the 2026-09-28 upstream sync (#19) and the devvms pin wake-up (#20).
No textual conflicts. The base moved the unit-test shards to arm64 hosts, which
the feed and readiness suites did not survive; the following commit pins the
slice they model.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… quarantine strip across the bundle

The unit-test shards now run on arm64 Linux, where `getUpdateManifestName`
names `latest-linux-arm64.yml`; the feed, readiness, asset-readiness and
release-builds suites mock the x64 manifest and installer names, so 36 cases
failed on the ARM runners while passing on x64 hosts. A `setHostSliceForTest`
fixture pins `process.arch` (and platform, where a case sets one) for the slice
each file models and restores the host's own values after every test.

`stripQuarantine` read the quarantine attribute back from the bundle directory
alone, so a partial `xattr -r -d` that cleared the root and left
`Contents/MacOS/<exe>` flagged passed the check and Gatekeeper blocked the
relaunch. The verdict is now the recursive `xattr -r` listing: a non-zero exit
refuses the bundle as unconfirmed, and any file still naming the attribute
refuses it with that path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@tenki-reviewer

tenki-reviewer Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Review complete. 🟡 2 medium

💬 Inline comments (2)

Reviewed commit: 9c189a1

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9c189a1abb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@tenki-reviewer tenki-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This PR adds a signed-manifest macOS self-update path for the SwapLabs fork (Ed25519 verification, staged-bundle install via a detached helper, health-marker deferral of first window) alongside release-feed probing and build-config plumbing.

…he update helper gave up on

`probeReleaseManifest` took every asset a fork's self-update manifest named,
so a manifest whose `file` was an absolute URL had background checks HEAD that
address before any signature was verified. The probe now requests only the
slice's fixed zip and signature under the release, and a manifest naming any
other file is not ready, as the installer would refuse it.

When the helper cannot restore the previous build it relaunches the new one,
so the build reporting a completed launch can be one that missed the helper's
health deadline. That launch is still a completed update, but its record now
carries the helper's verdict at warning level instead of reading like a healthy
one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@tenki-reviewer

tenki-reviewer Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Review complete. 🟡 1 medium

📍 Outside the diff (1)

  • 🟡 New await in checkForPinnedBuild bypasses in-flight guards (updater-build-selection.ts:220–246) — The new await isMacSelfUpdateActive() at src/main/updater/updater-build-selection.ts:220 suspends before the in-flight guards at lines 240-246, and the call is dispatched fire-and-forget.
🧹 Nitpicks (1) — 🟢 1 low
  • 🟢 Version read failure reported as bundle-identity-mismatch (mac-self-update-bundle.ts:88) — readInfoPlistString throws bundle-identity-mismatch for any PlistBuddy failure, but it also serves the CFBundleShortVersionString read in verifyStagedBundle (src/main/updater/mac-self-update/mac-self-update-bundle.ts:135).

Implements a SwapLabs-forked macOS self-update flow (signature-verified staged bundle swap with rollback) plus updater feed/build-selection changes; review found one medium race in pinned-build checks and one low-severity error-classification mislabel.

Reviewed commit: a2d83d8

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.

1 participant