Skip to content

feat(desktop): add dismissible banner pointing to the new buzz app - #9379

Merged
matt2e merged 9 commits into
mainfrom
peon/desktop-deprecation-banner
Nov 13, 2026
Merged

matt2e merged 9 commits into
mainfrom
peon/desktop-deprecation-banner

Conversation

@kalvinnchau

@kalvinnchau kalvinnchau commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The Buzz 1.0 desktop app now lives in block/buzz-app. This adds a banner to the 0.X desktop app that tells users, and lets them download the new app's installer without leaving the app.

  • A full-width banner sits under the top chrome: "A new Buzz app is here! This version is being retired, so grab the new one whenever you get a chance."
  • Upgrade downloads the installer for this machine inside the app. A dialog shows the progress, then the saved file's name and folder with Show in Finder (worded per platform) and Done. The file goes to the Downloads folder and is revealed in the file manager. It is never opened, mounted or run.
  • Cancel, Escape or the dialog's close button stops a running download and removes the partial file.
  • When no installer can be resolved for this machine (unsupported platform, or the manifest fetch fails or is slow), Upgrade opens https://buzz.xyz in the browser instead.
  • More info opens the upgrade guide in the browser.
  • Dismissing (×, labelled "Remind me later") stores a timestamp in localStorage. The banner is read once per mount, so it comes back on the next launch (or community switch) once 24h have passed.
  • It is hidden in the huddle room window.

Demo

buzz-banner-upgrade-in-app-download.mp4

Recorded on macOS (Apple Silicon) from a native debug build with the real Rust backend: a full download, Done, then a second download cancelled part-way.

  • The network is throttled through a local proxy so the progress stays visible. Unthrottled, the installer arrives in about two seconds.
  • The app was launched with a scratch home folder, so the path in the dialog is a scratch path, and the Finder window that the reveal opens is left out of the recording.
  • Presses are accessibility presses, marked with an orange ring. The OS cursor is not shown.

How the download works

  • Which installer: a Tauri command reports this install's key (macos-arm64, macos-x64, windows-x64, linux-appimage or linux-deb). The webview reads the matching entry from the buzz-app download manifest once per banner mount.
  • Validation: Rust re-validates the URL on every call. It must be https://github.com/block/buzz-app/releases/download/<tag>/<file>, with a restricted tag and file-name character set and the extension this machine installs. The webview names a URL and nothing else.
  • Redirects: a dedicated HTTPS-only client follows at most 5 redirects, and only to github.com, objects.githubusercontent.com or release-assets.githubusercontent.com.
  • Limits: 512 MiB, 15 s to connect, 30 s for headers, 30 s without a byte, 60 min in total. One download at a time.
  • Saving: the body is written to <name>.<random>.part in the Downloads folder, then renamed to <name> (or <name> (1) and so on) without ever replacing an existing file. The partial file is removed on failure, on cancel and at app exit. A partial left by a crash or force-quit is removed on the next launch.
  • Downloaded-from-the-internet marking: macOS gets the com.apple.quarantine attribute a browser would write, and Windows gets the Mark of the Web (Zone.Identifier). If the mark cannot be written, the download fails and the file is not kept.
  • Failures: the dialog says what went wrong in plain words. Transient failures lead with Try again. Failures a retry cannot fix lead with Download in browser, which opens the validated installer URL.

Testing

  • Unit: deprecationBannerVisibility.test.mjs, newAppInstaller.test.mjs (manifest and URL rules), newAppInstallerDownload.test.mjs (dialog state), and a jsdom test of the download hook.
  • Rust: new_app_installer_tests.rs covers URL validation, the redirect policy, limits, naming without overwrite, cancel and shutdown cleanup, launch cleanup, and the quarantine and Mark of the Web values. CI runs this crate's tests on Linux and on windows-latest, so the Mark of the Web test writes a real alternate data stream there. The quarantine test runs on macOS only, which CI builds but does not test.
  • Smoke: tests/e2e/deprecation-banner.spec.ts drives the banner, the download dialog, each failure's actions, cancel and the keyboard paths against the mock bridge.
  • Native, macOS arm64 debug build: the download was saved with com.apple.quarantine set and revealed in Finder; cancel removed the partial; the download kept running while the window was hidden; SIGTERM and a menu quit mid-download removed the partial; after a force kill the next launch removed it.

Not verified by hand:

  • The Windows and Linux flows in a running app, including SmartScreen on Windows.
  • The Gatekeeper prompt when opening the downloaded DMG, and the macOS Downloads-folder consent prompt, which need a signed build.

🤖 Generated with Claude Code

@kalvinnchau

Copy link
Copy Markdown
Contributor Author

🤖 Mock-bridge screenshots (just desktop-screenshot).

Banner shown

Shown on launch when it has never been dismissed, or was dismissed 24h+ ago.

01-banner

After dismiss

Hidden until the next launch after 24h.

02-dismissed

@kalvinnchau
kalvinnchau force-pushed the peon/desktop-deprecation-banner branch from 137f59d to 21a0548 Compare November 11, 2026 23:31
kalvinnchau and others added 5 commits November 13, 2026 10:23
show a banner under the top chrome telling users this desktop app is
being retired, with a link to the latest block/buzz-app release.
dismissing stores a timestamp in localstorage; the banner returns on the
next launch once 24h have passed. hidden in the huddle room window.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Kalvin Chau <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
the banner shifts the app layout by its height, breaking existing
geometry assertions and visual snapshots in the smoke suite.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Kalvin Chau <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
add a "How to upgrade" button next to "Get the new app". it opens
https://github.com/block/buzz-app/blob/main/docs/upgrading.md in the
external browser through the same opener plugin call as the releases
link; opener:default already permits https urls, so the capability
config is unchanged. the url is a named constant beside the releases
url, and the new button is the ghost variant so the existing outline
button stays the primary action.

the message and the two actions now sit in a wrapping row, so the
buttons drop under the message instead of squeezing it when the row is
narrow (the 900px minimum window at 1.5x zoom).

add a smoke spec that shows the banner, checks each button reaches the
opener with its own url by pointer and keyboard, and checks the layout
at 900px and 600px. the e2e bridge still seeds the banner as dismissed
by default; specs that exercise the banner opt out with
seedDeprecationBannerDismissed: false.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
rename the deprecation banner's "How to upgrade" button to "More info",
drop its external-link icon so it is text only, and switch it from the
ghost variant to the shared outline variant. the url it opens is
unchanged, and so is the "Get the new app" button.

the outline variant's own border (border-input/40) measures 1.03 to
1.55 against the banner tint across five themes, so on the banner it
reads as a borderless pill. this button overrides only the border
color with border-foreground/70, which measures 3.12 to 7.21 against
the banner in the same themes.

"More info" says little on its own and the icon that marked the link
as external is gone, so the button's accessible name is "More info
about upgrading, opens in your browser". it starts with the visible
label.

the smoke spec finds the button by that exact accessible name and
checks its visible text is "More info".

Co-Authored-By: Claude Fable 5.2 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
the deprecation banner's "Get the new app" button now opens
https://buzz.xyz instead of the block/buzz-app latest-release page. the
constant is renamed from BUZZ_APP_RELEASES_URL to BUZZ_WEBSITE_URL. the
label, icon and outline style are unchanged, and the link still goes
through the opener plugin's openUrl. opener:default includes
allow-default-urls, whose scope allows https://*, so the capability
config needs no change for the new host.

"More info" loses the border-foreground/70 override that made it look
heavier than "Get the new app". both buttons now take the outline
variant, the sm size and one shared class constant, so they have the
same border, fill and hover treatment in every theme. "More info" stays
text only, and its label, url and accessible name are unchanged.

the smoke spec expects the new url, and checks the two buttons render
the same class list and that "More info" has no icon. run against the
previous component, the spec fails on the class comparison.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
@matt2e
matt2e force-pushed the peon/desktop-deprecation-banner branch from 21a0548 to 5498a00 Compare November 13, 2026 00:57
matt2e and others added 4 commits November 13, 2026 12:55
…ad button

the deprecation banner's "Get the new app" button now opens the new
app's installer for the running machine when it can, and
https://buzz.xyz otherwise. the label, icon, outline style and
accessible name are unchanged.

how it resolves the installer:

- a new tauri command, new_app_installer_target, reports the install's
  key in the block/buzz-app installer manifest: macos-arm64, macos-x64,
  windows-x64, linux-appimage or linux-deb. linux reports the appimage
  key when APPIMAGE is set, the deb key only when it is unset and
  /usr/bin/dpkg exists, and no key otherwise. no other os or
  architecture gets a key. there is no rosetta detection, so an intel
  build on apple silicon gets the intel installer.
- when the banner mounts visible and the command returned a key, the
  webview fetches
  https://raw.githubusercontent.com/block/buzz-app/download-feed/downloads.json
  once, with a 5 second timeout. dismissing the banner aborts it.
- resolveInstallerUrl in the new src/app/newAppInstaller.ts accepts an
  entry only when schemaVersion is 1, channel is "stable", the entry is
  a string, and its parsed url is https://github.com with no
  credentials, a path under /block/buzz-app/releases/download/ and the
  file extension that key installs.
- the click handler stays synchronous. it opens the resolved url if
  there is one and https://buzz.xyz if not, so it never waits on the
  fetch. a missing key, a failed or slow fetch and a rejected manifest
  all leave the fallback in place; failures log one console warning and
  are not retried until the next launch.

the link still goes through the opener plugin's openUrl, and the
default browser performs the download. the csp already allows https
connect-src and opener:default already allows https urls, so neither
config changes.

the button carries data-download-source="installer" or "fallback" so
specs can wait on the resolution. the e2e mock bridge answers the new
command from mock.newAppInstallerTarget and defaults to no key, so no
spec fetches the manifest unless it opts in.

tests added:

- rust: a table test for the key mapping.
- unit: newAppInstaller.test.mjs covers the five valid keys and each
  rejection rule.
- smoke: the existing banner journey asserts the fallback url on the
  default mock. one new test fulfils the manifest route and checks the
  click opens the arm64 dmg url. another holds the route pending,
  checks the click opens buzz.xyz at once, then releases it and checks
  the button switches to the installer.

checks that ran and passed:

- desktop `pnpm check` (biome, px-text, pubkey-truncation),
  `pnpm typecheck` and `pnpm check:file-sizes`.
- desktop `pnpm test` under the repo's pinned node 24.15.0: 6956 and
  185 tests, 0 failures, including the new resolver tests.
- `cargo test --manifest-path desktop/src-tauri/Cargo.toml --lib
  installer_target_key` on rust 1.98.1: 1 passed. this is the new
  helper's test only, not the whole tauri crate suite.
- `cargo clippy --manifest-path desktop/src-tauri/Cargo.toml
  --all-targets -- -D warnings` and `cargo fmt --check` for the same
  crate.
- tests/e2e/deprecation-banner.spec.ts on an e2e build: 3 passed.
- falsification: with the path-prefix check removed, the unit
  rejection table fails on "another repository". with the banner
  effect disabled, both new smoke tests fail with the button still on
  "fallback". both edits were reverted.

not run or not finished:

- the full `pnpm test:e2e:smoke` run was still going at commit time,
  at test 1208 of 1496 on one worker. so far 1207 passed and 1 failed.
  the three banner tests passed in that run.
- the one failure is test 814, tests/e2e/mentions.spec.ts:4595
  "mention text is highlighted in sent messages". the sent row reads
  "Hey @bobcheck this out ...": the typed suffix landed against the
  mention without its separator space, so no mention chip rendered.
  the banner is seeded dismissed in that spec and is absent from the
  failure snapshot, and with no installer key the new effect does
  nothing, so this change does not look like the cause. it reads as
  the composer's caret timing after autocomplete. it was not rerun, so
  that judgement is from the error text alone.
- `just ci` was not run, because it rebuilds the desktop output the
  smoke run is serving. the whole tauri crate test suite, the root
  workspace tests, the web build and the mobile tests were therefore
  not run either.
- the fetch was not exercised in the native app window. playwright
  runs in plain chromium without the tauri csp, so nothing here proves
  the webview can reach the manifest or that the browser downloads the
  right installer. that needs a person: launch a build on macos, click
  "Get the new app", and confirm the matching dmg downloads.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
the deprecation banner's download button now downloads the new app's
installer itself when the installer is known. a modal dialog shows the
progress, and the finished file is revealed in the file manager.
nothing is sent to the browser on that path. when no installer is
resolved the button still opens https://buzz.xyz in the browser, as
before. the click is synchronous in both states and never waits on the
manifest fetch.

the button's visible text is now "Upgrade" and it has no icon, in both
states. its accessible name is "Upgrade, downloads the installer" with
aria-haspopup="dialog" when it downloads in the app, and "Upgrade,
opens in your browser" when it does not.

design:

- where the download runs: in rust, in the new
  src-tauri/src/commands/new_app_installer.rs. three commands:
  download_new_app_installer(url, onEvent),
  cancel_new_app_installer_download() and reveal_new_app_installer().
  the webview names a url and nothing else.
- what is validated, in rust, on every call: the url must parse to
  https://github.com with no port, credentials, query or fragment, a
  path under /block/buzz-app/releases/download/<tag>/, and a last
  segment that is a plain file name of at most 128 characters from
  [A-Za-z0-9._+-], starting with a letter or digit, not a windows
  device name, and ending in the extension this machine's manifest
  key installs. resolveInstallerUrl in the webview gained the same
  query, fragment, tag and file-name rules so the two agree.
- redirects: a dedicated reqwest client with https_only and a custom
  policy. it follows at most 5 redirects and only to https, default
  port, no credentials, on github.com,
  objects.githubusercontent.com or
  release-assets.githubusercontent.com. the app-wide client is not
  used because it follows redirects anywhere.
- limits: 15 s to connect, 30 s for response headers, 30 s without a
  byte before the transfer counts as stalled, 60 min in total, and a
  512 MiB cap checked against the announced length and against the
  bytes actually received. a body that ends short of its announced
  length, or is empty, is a failure.
- where the file goes: the user's Downloads folder, from tauri's
  download_dir. the file name comes only from the validated url. the
  body is written to "<name>.<random>.part" in that folder, then moved
  to "<name>", or "<stem> (1)<ext>" up to "(99)", with a rename that
  refuses to replace an existing file. nothing is ever overwritten.
  the partial file is removed on failure, on cancel, when the command
  future is dropped, and synchronously at app shutdown.
- how progress reaches the webview: a tauri ipc channel passed to the
  download command, the convention the terminal and websocket
  commands already use. it carries one "started" event with the file
  name and "progress" events with received and total bytes, at most
  one per 100 ms. the command's return value is the outcome: saved,
  cancelled, or failed with a reason.
- one download at a time: rust holds a single slot and answers "busy"
  to a second call. the webview hook also ignores a second start while
  one is running.
- cancel: the dialog's Cancel button, Escape and the dialog's close
  button all cancel a running download; the dialog closes once rust
  reports it stopped. a click beside the dialog does not cancel.
  unmounting the banner cancels. rust also cancels when the main
  window is hidden by its close button on macos, when the main window
  is destroyed, and at app shutdown. mounting the banner cancels any
  download left over from an earlier page load.
- reveal: on success the webview calls reveal_new_app_installer, which
  takes no path and reveals the last file rust saved through the
  opener plugin's reveal_item_in_dir. the dialog then shows the file
  name, the folder, a reveal button worded per platform ("Show in
  Finder", "Show in File Explorer", "Show in folder") and Done. the
  installer is never opened, mounted or run.
- failure: the dialog says what went wrong in plain words and offers
  "Try again" and "Open buzz.xyz".
- accessibility: the dialog is the shared radix dialog, so focus is
  trapped. focus starts on Cancel, moves to Done or Try again when the
  state changes, and returns to the banner button on close. the bar is
  the shared Progress with role progressbar, a label, a value when the
  total is known and a value text. one polite live region announces
  the start, each 25% step, and the end.

downloaded-from-the-internet marking:

- macos: before the move into place the file gets
  com.apple.quarantine = "0081;<hex seconds>;Buzz;", the shape a
  non-sandboxed browser writes, and it is read back. if that fails the
  download fails and the file is removed. the origin url is also
  written to kMDItemWhereFroms, best effort.
- windows: the Mark of the Web is written as the
  "<file>:Zone.Identifier" stream with ZoneId=3 and the origin url,
  using std::fs, and read back. a failure fails the download.
  THIS PATH HAS NEVER RUN ON WINDOWS. it compiles on macos and its
  contents are unit tested there, nothing more.
- linux: there is no system mechanism that checks a downloaded file,
  so nothing is written.

the installer manifest publishes only urls. it has no size and no
checksum, so the download is not verified against either. what is
checked is the announced content-length against the bytes received.

config changes:

- src-tauri/Cargo.toml: xattr = "1" added to the macos target
  dependencies, for the quarantine attribute. it has a safe api and
  was already in Cargo.lock as a transitive dependency, so the
  lockfile gains one line.
- no capability or permission change. opener:default already allows
  reveal-item-in-dir, and the reveal is called from rust anyway.
- no csp change.
- src-tauri/src/lib.rs registers the three commands, manages the
  download state and adds the window hooks. src-tauri/src/shutdown.rs
  removes a partial file at shutdown.

the e2e mock bridge answers the three commands from the new
src/testing/e2eBridgeInstallerDownload.ts and exposes
window.__BUZZ_E2E_INSTALLER_DOWNLOAD__ so specs can drive progress,
success and failure.

checks that passed:

- desktop: pnpm check, pnpm typecheck, pnpm check:file-sizes and the
  file-size policy tests, and pnpm test on node 24.15.0 (6969 and 185
  tests, no failures). that includes 12 new tests for the dialog's
  state logic and new rejection rows for resolveInstallerUrl.
- tauri crate on rust 1.98.1: cargo fmt --check, cargo clippy
  --all-targets -- -D warnings, and the whole cargo test suite, 3465
  passed. 19 of those are new: url acceptance and rejection tables,
  the redirect table and the client's redirect policy, file naming
  and collision, and transfers against a local axum server covering
  success, a missing length, every failure leaving nothing behind,
  cancel mid-transfer, cancel just before the move, the single slot,
  shutdown cleanup, the quarantine attribute and the Mark of the Web.
- tests/e2e/deprecation-banner.spec.ts on an e2e build: 5 passed. the
  fallback journey still passes, and four journeys are new: progress
  and success with reveal, failure with retry and the website link,
  cancel by button and Escape, and a click before the manifest
  resolves.
- falsification, each reverted after: with persist_noclobber swapped
  for an overwriting persist, never_overwrites_an_existing_file
  fails. with the https check removed from the redirect rule,
  redirects_stay_on_https_release_asset_hosts fails. with the hook's
  second-start guard removed, the success journey fails.
- native run on this mac (macos 27.2, apple silicon), unsigned debug
  build with the production frontend embedded and the repo's security
  config unchanged, throwaway identity, no relay. the button read
  "Upgrade, downloads the installer", so the manifest resolved in the
  real webview. one press opened the dialog, the bar and the byte
  count advanced, and Buzz_1.0.1_aarch64.dmg landed in ~/Downloads
  about 10 s later, mode 0644. xattr -l showed
  com.apple.quarantine: 0081;6af685f0;Buzz; and the origin url. its
  size and sha256 matched the asset's digest from the github api.
  Finder came to the front with the file selected. "Show in Finder"
  selected it again and Done returned focus to the button. a second
  download saved "Buzz_1.0.1_aarch64 (1).dmg" and left the first file
  untouched. Cancel mid-download and quitting the app mid-download
  each removed the partial file at once. the downloaded files were
  deleted afterwards and never opened.

not run or not proven:

- windows and linux were not run at all. on windows the Mark of the
  Web, the no-overwrite rename, removing the partial file while it is
  open, and the reveal are untested. on linux the reveal is untested.
  if the mark cannot be written on windows the download fails and the
  dialog offers the website, so the protection is not dropped
  silently, but a person needs to try a windows build before this
  ships there.
- Gatekeeper's prompt was not observed, because that needs the
  installer to be opened. only the attribute was confirmed.
- the native presses were accessibility presses, not pointer clicks or
  key presses, and no screen reader was run. while the byte count was
  updating it did not show up as its own text element in my
  accessibility dumps; the progress bar's value text carries the same
  words. i did not find out why.
- closing or hiding the window mid-download was not tried natively.
  the cancel it calls is unit tested; the window hooks themselves are
  not.
- a signed release build was not tried.
- just ci, the full desktop smoke suite, the root workspace tests,
  the web build and the mobile tests were not run. only the banner
  spec ran from the smoke suite.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
Applies the review findings on d5bd8fe, which made the deprecation
banner download the new app's installer inside the app.

Leftover partial files
- The signal-handler exit path now aborts the download and removes its
  partial file. That path ends in exit(), so the download never cleaned
  up after itself.
- The running download's partial path is recorded in the app data
  directory. The next launch removes exactly that path, and only while
  it is still a regular file named like one of this module's partials.
  No folder is searched. A normal finish, cancel or exit clears the
  record.

Failure reasons and actions
- A redirect the policy refuses is "redirect-blocked". It no longer
  asks the user to check their internet connection.
- A write the system refuses is "permission-denied". It no longer shows
  the free-space message. On macOS the wording says where to allow
  access to the Downloads folder, for a user who declined the system
  prompt.
- Linux falls back to ~/Downloads when no XDG download directory is
  configured.
- Failures a retry cannot fix (redirect blocked, protection mark not
  written, permission denied, too large) lead with "Download in
  browser", which opens the validated installer URL as 78d9bcd did.
  A rejected URL leads with buzz.xyz and is never opened. "Try again"
  leads only for transient failures.
- The release tag is restricted to letters, digits and ._+-/ in both
  the Rust validator and resolveInstallerUrl, so no percent escape
  reaches GitHub.

Window and remount
- Hiding the main window no longer cancels the download. It is
  cancelled when the window is destroyed or the app exits.
- The banner's owner remounts on a community switch, an identity change
  and a signer change. The download state moved out of the component
  into an app-level store, so the download and its dialog survive that.
  This was smaller than a visible "cancelled" state, which needs the
  same store to outlive the remount and new wording on top.

Dialog
- Focus moves to Cancel when a download starts or restarts. A repeated
  Enter keydown inside the dialog is ignored, so holding Enter on the
  banner button or on "Try again" cannot cancel the download it started.
- A failed reveal says the folder could not be opened and points at the
  path shown. It no longer claims the file was moved or deleted.
- An AppImage's finished message gives the make-it-executable step.
- The saved file's mode follows the user's umask instead of a fixed
  0644: tempfile creates the partial with mode 0666 under the umask.

Tests
- The mock bridge gained "finish on next cancel" and "fail next
  reveal". The smoke journeys assert the rendered result of each, plus
  held Enter, focus after retry, each failure's actions, and a community
  switch mid-download.
- A jsdom test drives the real hook: the attempt fence, the mount-time
  cancel, and survival across unmount and remount.

Where this differs from the review decisions
- The hook's unmount cancel is removed, not tested. Surviving a remount
  requires that unmounting does not cancel, so the test asserts the
  opposite: unmount sends no cancel and the next mount shows the same
  download. The mount-time cancel now runs only when this page has no
  download of its own, and the hook's doc comment states the
  single-window assumption.
- permission-denied keeps "Try again" as its secondary action. The
  message tells the user how to grant access, after which the same
  download works.

Windows: no behaviour changed. CI does run this crate's tests on
Windows. The required "Windows Rust (x86_64-pc-windows-msvc)" job in
_ci-rust.yml runs cargo test for desktop/src-tauri on windows-latest
when desktop/src-tauri changes, so
mark_of_the_web_write_is_verified_by_reading_it_back writes and reads a
real alternate data stream there. Not covered anywhere: the stream
surviving the final rename inside download_installer, and SmartScreen.
macOS CI only builds this crate, so the quarantine test runs locally
only. d5bd8fe is not pushed yet, so no Windows or Linux CI result
exists for this code.

Verified on macOS arm64
- Desktop: biome check, tsc, file-size check, unit tests (6970 node,
  187 jsdom), production build.
- Tauri crate: fmt, clippy with and without mesh-llm, cargo test
  --workspace (3470 lib tests), cargo test --features mesh-llm --lib
  (3576 tests).
- Smoke: deprecation-banner.spec.ts, 6 journeys, on an e2e build.
- Falsified: 13 Rust, 15 unit and hook, and 7 smoke mutations. Each
  fails the test that guards it. One gap found this way is closed: the
  recovery test did not notice a followed symlink.
- Native debug build: the download was saved with the umask's mode and
  com.apple.quarantine set, and Finder revealed it. Cancel removed the
  partial. With the window hidden the partial kept growing, and the
  dialog reopened from the tray read 29% then 35%. SIGTERM and a menu
  quit mid-download removed the partial. After SIGKILL the next launch
  removed the recorded partial and left a lookalike .part file beside
  it. Mid-download steps ran through a local throttling proxy.

Not verified
- The macOS consent prompt and the permission-denied path in a real
  app: a binary started from a terminal inherits the terminal's access.
- The new Rust tests on Windows and Linux. They run in CI after a push.
- The full desktop smoke suite. Only the banner spec ran.

The rebase of this branch onto origin/main was aborted before this
commit and still needs doing.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
…d review

Fixes the two CI failures on e3b456c and applies the six nice-to-have
findings of the second review of the in-app installer download.

CI failures fixed

- "Desktop Tauri (core)", clippy on Linux: QUARANTINE_AGENT and
  quarantine_value were dead code in the lib target, because only macOS
  code and a test use them. Both are now behind
  cfg(any(target_os = "macos", test)). There is no allow(dead_code).
- "Flake check (Desktop Tauri)": macos_download_is_quarantined was
  compiled only on macOS, so the Linux runner selected it from the file
  and nextest never listed it, which fails the lane by design. The test
  is now compiled on every platform, its body only on macOS, and it
  carries cfg_attr(not(target_os = "macos"), ignore = "...").
  scripts/flake-check.mjs is unchanged.

How that form fits the script: its scanner reads attribute paths and
treats only a plain #[ignore] as ignored, so it still selects this
test on every runner. On Linux nextest lists the test with ignored
set, so chooseRust finds it and it is not "not listed"; it is then
left out as not runnable. A plain #[ignore] would have dropped the
test on macOS too.

Platform audit, by reading: every item in the installer module, and
this branch's changes to shutdown.rs, lib.rs and updater.rs, for
Linux, Windows and macOS, in the lib and the test target. The
quarantine pair was the only thing used on one platform only and not
gated. write_quarantine and QUARANTINE_XATTR were already gated to
macOS. The Mark of the Web helpers are reached through
`if cfg!(windows)`, which compiles everywhere. The new test helpers
for the mode check are cfg(unix), as is their only caller.

Review findings applied

- Launch cleanup is confined to the Downloads folder. The record now
  holds the partial file's name, not its path, and the next launch
  joins it to the Downloads folder it resolves itself. A record that
  holds a path, or a name that is not this module's shape, is dropped
  and removes nothing. With no Downloads folder to look in, the record
  is kept.
- The record survives a failed removal. Freeing the download slot
  clears it only when nothing is left at the partial path, and the
  exit path no longer forgets the path it failed to remove.
- The partial file is requested with mode 0644, not 0666, so no umask
  can make the installer writable by group or others. A stricter umask
  still applies.
- A download can no longer run without its dialog. The dialog and the
  hook moved into NewAppInstallerDownloadHost, mounted once in main.tsx
  beside App and outside everything that unmounts with the community,
  the identity or onboarding. The banner button only starts the
  download. I chose this over cancelling when the banner stays
  unmounted: that needs a grace period for the community-switch gate,
  and the download would still run unseen during it. The huddle window
  does not mount the host, because a second host would cancel the main
  window's download on mount.
- A repeated Enter keydown on the banner button is ignored, with the
  same handler the dialog uses. Held Enter on Done, "Download in
  browser" or "Open buzz.xyz" no longer presses the button that focus
  returns to.
- The community-switch journey asserts the manifest was fetched a
  second time. The banner fetches once per mount, so that is the new
  banner.

Tests added or changed

- Rust: a new test for the record surviving a failed removal, at slot
  release and at exit, then the next launch removing the file. A folder
  stands in for a file that cannot be removed, so it runs on all three
  platforms. The launch-cleanup test gained the confinement cases. The
  mode test reruns itself in a child process under umask 000, because
  the default umask hides a 0666 request and a umask is process-wide.
- Smoke: a new journey fails a community switch mid-download, checks
  the banner is gone and the dialog still follows progress, cancels by
  keyboard, then retries the connection and downloads again. The mock
  bridge gained __BUZZ_E2E_APPLY_COMMUNITY_ERROR__ for it. Three
  journeys gained held-Enter steps.
- The hook's jsdom test is unchanged and still passes.

Verified locally on macOS arm64, rust 1.98.1, node 24.15.0

- Tauri crate: cargo fmt --check; clippy --workspace --all-targets
  -D warnings, with and without mesh-llm; cargo test --workspace
  (3471 lib tests, 0 failed).
- Desktop: pnpm check, pnpm typecheck, the file-size gate, pnpm test
  (6970 node and 187 jsdom tests, 0 failed).
- Smoke: deprecation-banner.spec.ts on an e2e build, 7 journeys.
- Flake check, with the selection CI computes for this commit:
  `flake-check.mjs select HEAD^1 HEAD` picks the 25 tests of
  new_app_installer_tests.rs and the 7 banner journeys, and no node or
  jsdom test. `flake-check.mjs tauri --iterations=10 <selected>` exits
  0 with 25 tests passing in both feature graphs. The 7 journeys pass
  playwright --repeat-each=10 --retries=0, 70 of 70.
- The Linux runner, simulated: with the cfg_attr condition inverted so
  the quarantine test is ignored on this Mac, the same lane repeats the
  other 24 tests and exits 0, and with that test alone it reports
  "nothing to repeat" and exits 0. Reverted.
- Falsified, each reverted: recording a path again fails the
  launch-cleanup test; clearing the record on every slot release fails
  the new record test; mode 0666 fails the mode test in its child run;
  the dialog back inside the banner fails the new journey; no key guard
  on the banner button fails both held-Enter journeys; no second
  manifest fetch fails the community-switch journey.

Not verified

- Linux and Windows. Clippy and the tests for those platforms cannot
  run on this Mac, so both CI fixes rest on the reading and the
  simulation above until CI runs after a push. The Windows Rust job
  passed on e3b456c; the new and changed tests have not run there.
- The full desktop smoke suite, the production build and the native
  app. Only the banner spec ran.
- A render error caught by the root error boundary replaces the whole
  tree, the dialog included. A download running at that moment still
  ends only at its limits, at window destroy or at exit.

Not pushed and not rebased.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
@matt2e
matt2e marked this pull request as ready for review November 13, 2026 06:20
@matt2e
matt2e requested a review from a team as a code owner November 13, 2026 06:20
@matt2e
matt2e deployed to codex-review November 13, 2026 06:21 — with GitHub Actions Active
@github-actions github-actions Bot added the codex-security-review-current The posted Codex security review matches its recorded range. label Nov 13, 2026
@github-actions

Copy link
Copy Markdown

🔐 Codex Security Review

Note: This is an automated, security-focused review generated by Codex.
Use it as a supplement to human review; false positives are possible.

Scope

  • Exact PR diff: 3c0adc188206bab8551a7af53dcf6bd54c9d0d09...24f54fe63ac1314fb5dcf09ccb13933d00f13d20
  • Model: gpt-6.1-sol

💡 Click "edited" above to see earlier reviews for this PR.


Review Summary

Overall Risk: NONE

No concrete security, correctness, or reliability findings identified in the authorized PR range.

Findings

No concrete security, correctness, or reliability findings were identified.

Notes

  • Review used read-only source and history inspection. Builds, tests, network requests, and native platform behavior were not executed.

Generated by Codex Security Review |
Requested by: @matt2e |
Workflow run

@kazemek21 kazemek21 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Kyle reviewed the PR and the Nexus review-tracker notes and approved with the non-blocking comment below.

Posted by Codex (gpt-6.1-sol) on behalf of kylez.

.active
.as_ref()
.and_then(|active| active.partial.clone());
if partial.as_deref().is_none_or(partial_is_gone) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] Preserve unresolved partial records across later attempts

If launch recovery cannot remove an old partial file, it correctly leaves its record for another launch. But if the next Upgrade attempt fails before creating a partial, active.partial is None here and this deletes that old record. The original partial then remains in Downloads even after the temporary permission/sharing problem clears. Could we preserve unresolved recovery records independently of the current attempt, rather than clearing or replacing them?

Posted by Codex (gpt-6.1-sol) on behalf of kylez.

@matt2e
matt2e merged commit 1fc6a71 into main Nov 13, 2026
87 checks passed
@matt2e
matt2e deleted the peon/desktop-deprecation-banner branch November 13, 2026 08:08
bostonaholic added a commit that referenced this pull request Nov 13, 2026
Co-authored-by: mboston.Engineer <932d3b6ebea6cc83e091551ae46d1c7e7f8f7d0fa8e9cef5e6facc8544af413b@buzz.block.builderlab.xyz>

* origin/main:
  fix(mobile): require Builderlab consent for existing identities (#8823)
  feat(mobile): use HTTP/2 on iOS through app-wide URLSessions (#8897)
  fix(relay): publish a git push only when refs changed; check the protocol by refinement (#9425)
  fix(mobile): show shared chats under Solo (#9348)
  chore(release): release Buzz Desktop version 0.5.29 (#9456)
  feat(mobile): render messages with gpt_markdown 1.3.4 plusparse (#8945)
  fix(acp): bound observer retention by bytes (#7987) (#9436)
  Tune release codegen for faster/smaller binaries (#9434)
  feat(desktop): add dismissible banner pointing to the new buzz app (#9379)
  fix(mobile): keep publishing presence while the app is open (#9441)
  feat(mobile): share into Buzz from the iOS share sheet (#9433)
  fix(mobile): qualify every namesake mention and keep authored mention casing (#9325)
  fix(mobile): match browser GIF timing in the iOS reaction sheet (#9437)
  fix(mobile): play short-delay animation frames at browser speed (#9432)
  buzz-search: read profile names without the profile_name_fields function (#9397)
  buzz-db: build the community deletion lock key in Rust (#9396)
  fix(cli): pair for the active community, skip stopped desktops, accept a bare relay host (#9417)
  test(mobile): run push tests without a gateway dart-define (#9423)
  fix(mobile): never pop Home when archiving from channel settings (#9387)
  perf(acp): reduce observer CPU and lock overhead (#9381)

Signed-off-by: Matthew Boston <[email protected]>
bostonaholic added a commit that referenced this pull request Nov 13, 2026
…count-backfill

Co-authored-by: mboston.Engineer <932d3b6ebea6cc83e091551ae46d1c7e7f8f7d0fa8e9cef5e6facc8544af413b@buzz.block.builderlab.xyz>

* bostonaholic--agent-reply-count:
  fix(mobile): require Builderlab consent for existing identities (#8823)
  feat(mobile): use HTTP/2 on iOS through app-wide URLSessions (#8897)
  fix(relay): publish a git push only when refs changed; check the protocol by refinement (#9425)
  fix(mobile): show shared chats under Solo (#9348)
  chore(release): release Buzz Desktop version 0.5.29 (#9456)
  feat(mobile): render messages with gpt_markdown 1.3.4 plusparse (#8945)
  fix(acp): bound observer retention by bytes (#7987) (#9436)
  Tune release codegen for faster/smaller binaries (#9434)
  feat(desktop): add dismissible banner pointing to the new buzz app (#9379)
  fix(mobile): keep publishing presence while the app is open (#9441)
  feat(mobile): share into Buzz from the iOS share sheet (#9433)
  fix(mobile): qualify every namesake mention and keep authored mention casing (#9325)
  fix(mobile): match browser GIF timing in the iOS reaction sheet (#9437)
  fix(mobile): play short-delay animation frames at browser speed (#9432)
  buzz-search: read profile names without the profile_name_fields function (#9397)
  buzz-db: build the community deletion lock key in Rust (#9396)
  fix(cli): pair for the active community, skip stopped desktops, accept a bare relay host (#9417)
  test(mobile): run push tests without a gateway dart-define (#9423)
  fix(mobile): never pop Home when archiving from channel settings (#9387)
  perf(acp): reduce observer CPU and lock overhead (#9381)

Signed-off-by: Matthew Boston <[email protected]>

This branch was successfully deployed

1 active deployment
codex-review — 24f54fe6 Deployed Nov 13, 2026 by matt2e via Run Codex Security Review #14868
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

codex-security-review-current The posted Codex security review matches its recorded range.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants