The restart-eligibility gate (effective_secrets_unavailable) covered the
instance/definition/harness tiers but not the global tier. A setup-mode
agent could be stopped by an auto-restart while the committed global ref
was unhydratable, then hit the strict global reload at spawn, refusing
respawn after the live process was already stopped (FailedAfterStop).
Extract the four restart flows into an AppHandle-free restart_ops module.
Each op takes the global-config loader as a closure it invokes itself, so
the strict-global re-read and load-before-stop ordering are op-owned and
stop-spy-testable. The two pre-scan selectors own the strict-global gate
(unreadable global yields zero candidates); the two under-lock authorizers
re-read the committed global before the stop. Production shells in
agent_discovery.rs and global_agent_config.rs become thin wiring.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The pass-2 secret-availability gate closed the stop-then-fail-at-respawn
class for the record-derivable tiers (instance, definition, harness) but
left two gaps:
1. Global tier: both post-install restart checks loaded the global config
with unwrap_or_default() while spawn_agent_child reloads it strictly, so
a restart authorized while the global ref was unavailable — even for an
unrelated secret — would stop a live process only to hit the spawn
refusal (FailedAfterStop). The global-config-save flow's under-lock
recheck reused phase-1 snapshots and never re-attested the committed
global ref before the stop.
2. The four production restart-gate call sites consulted
effective_secrets_unavailable at the call site, so mutating any single
call did not fail a test — only the pure predicate was bound.
Add refuse_restart_on_unavailable_global (mirrors
refuse_save_on_unavailable_current) and route the strict global load
through it at both post-install sites and the global-config under-lock
recheck. Consolidate each flow's secret consultation into AppHandle-free
composed seams (should_restart_after_install_for,
refuse_restart_on_unavailable_secrets,
should_restart_on_config_change_for) so the consultation lives in one
testable place, and bind each with record-driven regressions.
Global availability stays a separate strict Result gate rather than a
fourth arm of effective_secrets_unavailable — it is not derivable from a
record.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
## Summary
- persist stable workflow run `error_code` values separately from human
diagnostics
- expose NIP-98 authenticated, channel-authorized run history and
approval reads with stable keyset pagination
- connect Desktop to those authoritative reads and return the
relay-created run ID on trigger
- show truthful loading, failure, and pending-trace states, and do not
render approval actions from non-actionable stored hashes
## Validation
- pre-push `branch-skew`, `desktop-typecheck`, `desktop-test`,
`rust-tests`, `desktop-tauri-checks`, and `desktop-check` all passed on
`a097dbe5f`
- Desktop tests: 4,761 passed, 0 failed
- `cargo check -p buzz-relay`
- `git diff --check`
## Remaining gate
This does not claim a relay-backed Playwright workflow journey. The
browser relay bridge still routes workflow invokes through in-memory
handlers; that production-shaped acceptance gate remains follow-up work
before Workflows can leave preview.
---------
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Co-authored-by: Mongo <5c25403eab7271f9f94ddd4f2b270e8cac2c92e2c830c51877cca6ec974ffb3f@buzz.block.builderlab.xyz>
Co-authored-by: Princess Donut <68157ebd23b3897c1991015c3038658ea916200c67d3a54620b0754d1b92f6e0@buzz.block.builderlab.xyz>
The restart-eligibility gate pushed agent_discovery.rs over the 1000+
desktop file-size ratchet ceiling (1819 -> 1849). Move the test module
to a sibling agent_discovery_tests.rs included via #[path], matching the
runtime_commands_tests.rs convention. Pure relocation, no behavior
change.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Automatic restart flows (post-install adapter bounce, global-config
change) derived eligibility purely from resolve_effective_agent_env +
agent_readiness. An unavailable secret tier hydrates to an empty env
that is indistinguishable from an intentionally-empty one, so those
flows could stop a live/setup process and only discover the refusal at
respawn (FailedAfterStop).
Add effective_secrets_unavailable(record, personas) — ORs the three
registry-derivable tiers (instance secrets_unavailable, definition,
harness) — and thread it through both eligibility predicates and the
under-lock recheck in each flow, refusing before any stop.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
A persisted harness env_ref whose keyring generation is missing, unreadable,
or malformed hydrated to an empty env, the definition still reached the loaded
registry and spawn descriptor, and a subsequent metadata save read the empty
partially-hydrated view as an intentional clear and erased the ref — turning a
keyring outage into permanent pointer loss.
Add a runtime-only env_unavailable marker (#[serde(skip)], reconstructed on
load) mirroring the agent tier's secrets_unavailable. hydrate_harness_env sets
it when a present env_ref cannot be resolved. Fail closed at all four harness
seams — spawn, deploy, model discovery, and both status local_setup sites — via
unavailable_harness_id. On save, persisted_unavailable_env_ref re-reads the
on-disk record so an empty-env save over an unavailable record preserves the
raw ref instead of clearing it; a genuine clear of a healthy record still
clears. A rename of an unavailable record is refused: the ref key embeds the id,
so it can neither be re-read under the new id nor carried forward safely.
Also correct the harness_env_key comment: unreclaimed generations accrete
(no harness-namespace GC), they are not bounded.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The interleave/txn-lock regressions drove `transaction_lock_at` directly
in the test, not the production lock acquisition, so deleting the real
lock in `save_managed_agents`/`save_agent_definitions` or the identity
persist path left both tests green. The lock primitive was proven; the
production wiring was not (Thufir r5 IMPORTANT #1).
Extract AppHandle-free entry points that own lock -> mutation as one
unit: `save_managed_agents_locked_at`/`save_agent_definitions_locked_at`
in storage, and `persist_identity_locked` (in commands/identity, the
command-orchestration layer) for the three identity-persist sites. All
production wrappers route through them. Identity persist contends on the
same store-directory inode every agent save takes via `secret_txn_lock_dir`,
so identity/agent secrets on the shared blob mutually exclude.
The interleave tests now drive those exact seams via a store-injected
barrier that pauses mid-mutation inside the lock span; a non-blocking
flock(LOCK_NB) probe proves EWOULDBLOCK exclusion. Deleting either
storage acquisition or the identity acquisition now turns the matching
regression red.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The r5 review flagged several fail-closed seams as under- or falsely
tested. This makes the coverage and comments truthful at each:
- global-config save and snapshot export each extract an AppHandle-free
seam (refuse_save_on_unavailable_current, resolve_snapshot_global) so
the missing/unreadable-ref refusal is unit-testable; both map only the
Err arm and pass Ok through unchanged.
- runtime_commands readiness tests drop two assertion-free let _ = status
bodies for real local_setup assertions, with a positive control that
proves the negatives are caused by the definition-unavailable gate.
- a concurrent instance/definition save interleave test pins that neither
half is lost or re-inlined under the shared txn lock; save_agent_
definitions_at is split out as the path-based seam it drives.
- dev_service_migration, secret_store, and runtime comments corrected to
match the code (marker-clear proof conditions, completion vs per-
coordinate markers, Windows named mutex, lone degraded-empty consumer).
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The definition tier of the fail-closed spawn gate — refuse to launch or
deploy a linked instance whose definition's env_vars ref could not be
hydrated from the keyring — was open-coded identically at four sites
(spawn, two status rows, deploy) and reimplemented again in tests.
Extract `unavailable_definition_id` next to its sibling `spawn_key_refusal`
and route every site through it. It returns the offending definition id so
the spawn and deploy paths can name it in the refusal message without a
second lookup; status rows use `.is_some()`.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The save-path .bak comments called a leaked backup unconditionally harmless. That holds only when the harness env was projected into the keyring; in the keyring-unavailable/keyless fallback the env stays inline, so the .bak carries plaintext secrets. State the real contract and point at the boot migration that scrubs *.json.bak for exactly that case.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Custom harness definitions carried their `env` map (which can hold provider secrets like ANTHROPIC_API_KEY) in plaintext on disk. Mirror the agent-store secret seam: add an `env_ref` generation pointer, strip `env` into the OS keyring under `harness:<id>:env:<gen>` on save, hydrate it back on load, and write the stripped JSON 0o600. A boot migration lifts pre-existing inline env, verifies each projected generation reads back, then scrubs plaintext-bearing backup/temp artifacts. Keyless builds and keyring outages keep env inline as an authoritative fallback.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
migration.rs is at the desktop 1000-line file-size ratchet's frozen base (1408, pinned on origin/main), so the custom-harness boot-migration wiring cannot add its module registration without tripping the ratchet. Lift the self-contained databricks V1->V2 reconcile pair (reconcile_databricks_v1_to_v2 + its _in_file helper) into a migration/databricks_reconcile.rs submodule and move its tests to the sibling databricks_reconcile_tests.rs, reclaiming headroom.
Pure movement: function bodies are byte-identical; only patch_json_records/canonical_dev_data_dir gain a super:: qualifier, the entry point narrows pub -> pub(super) (its sole caller is the boot call), and the test file's test_support import re-roots to crate::migration::test_support. All 11 databricks reconcile tests stay green.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
custom_harnesses.rs is about to grow with keyring env projection, so lift
its unit tests into a #[path]-included custom_harnesses_tests.rs sibling to
stay under the desktop 1000-line file-size ratchet.
Pure movement: production code is byte-identical, and the 43 test functions
move unchanged into the sibling module.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Identity and agent secrets share one SecretStore blob, but the identity
persist path took no transaction lock, so an identity keyring write could
interleave with a concurrent agent save/GC projection transaction on the
same blob. Acquire the secret txn lock at the AppHandle-bearing entry
points (import_identity, persist_current_identity, pairing recover), held
across the persist span only. Lock order is identity_mutation -> txn ->
blob on identity paths and txn -> blob on save/GC paths, so no path
acquires them in opposing order. The boot path is left to
single-instance serialization (lib.rs single-instance plugin).
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The seam wrote each secret field with its own mutate_blob call and minted
a fresh generation every save, so a metadata-only save churned the keyring
blob and left GC-eligible orphan generations behind. write_secrets_batched
reuses the live generation when a field's bytes are unchanged (no write,
no new UUID, no GC churn) and commits every changed field in a single
store_batch_verified mutation. Per-field cancel_gc_candidacy is dropped on
this path: under the txn lock GC cannot run, a fresh UUID gen was never
observed by a sweep, and a reused gen is a live ref sweeps skip, so no
candidate marker can exist at save time. Verification bypasses the cache
via verify_stored_raw so a backend that acks a write it did not persist is
still caught.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The service-keyed /tmp blob lockfile could be unlinked by a tmp cleaner
while a holder kept its flock; a recreate under the same pathname is a
fresh inode a second process can lock in parallel, splitting mutual
exclusion across two inodes. Recheck (dev, ino) after the lock is granted
and re-acquire against the live pathname on a mismatch, bounded by
MAX_BLOB_LOCK_REACQUIRE so a pathname churned faster than we can lock
fails loudly instead of spinning.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
## Summary
- render shared-agent instructions as literal text so Markdown cannot
conceal spoiler contents, link destinations, or image sources
- reject non-reviewable Unicode controls at every agent-definition
boundary while preserving legitimate rendered emoji sequences
- verify shared catalog event IDs and signatures before trusting
authorship, coordinates, pagination, or executable content
- preserve the exact system-prompt bytes between review and execution
instead of silently stripping or normalizing content
## Security rationale
Shared system prompts are executable configuration. Previously, catalog
prompts were projected through the chat Markdown renderer, which could
hide text, replace link destinations with benign labels, and turn image
syntax into remote loads. Zero-width and bidirectional controls could
also make reviewed text differ from what the agent executes.
This change establishes a review invariant: the prompt a user sees is
the prompt the agent executes. Definitions that cannot be reviewed
faithfully are rejected rather than rewritten. Catalog events must also
pass Nostr ID/signature verification before they can claim a publisher,
coordinate, or cursor.
## What changed
- catalog instructions render as exact literal text rather than rich
Markdown
- catalog relay events are verified on a fresh wire-shaped object before
paging, coordinate selection, attribution, or projection
- forged content, pubkeys, signatures, and invalid newer heads are
ignored and cannot shadow a valid signed definition
- TypeScript catalog parsing rejects unsafe remote definitions before
they reach the UI
- shared Rust validation covers persona create/update/import, inbound
relay sync, definition-less managed-agent sync, and catalog publication
paths
- definition-less managed agents now fail closed on local create, local
update, and publication before persistence or relay retention
- linked managed agents validate their local name while treating the
persona definition as authoritative; their inert record-level prompt is
not executed or published
- names reject layout controls; prompts retain ordinary newlines and
tabs
- legitimate emoji composition is supported, including contextual VS16,
ZWJ, skin-tone, family, flag, and keycap sequences
- detached selectors/joiners, bidirectional controls, tag characters,
zero-width concealment, and other default-ignorables remain rejected
- names are bounded to 128 characters and prompts to 64 KiB
- contributor guidance documents the byte-for-byte review requirement
for future sharing paths
Validation reports the offending code point and never silently removes
it.
## E2E recording
[buzz-shared-agent-security-e2e.webm](https://github.com/user-attachments/assets/44d6b75f-0877-490f-bda4-a716fae3f700)
The recording demonstrates:
- a safe definition remains visible
- a prompt containing zero-width `U+200B` is rejected
- a name containing bidi override `U+202E` is rejected
- the prompt is preserved exactly
- spoiler, link, and image syntax remains literal and does not render or
load
## Verification
Passed locally:
- `just test`: all 10 unit and Docker-backed integration stages
- desktop frontend unit suite: 4,295 tests
- persona catalog relay unit suite: 32 tests, including forged-event and
cursor-shadowing cases
- focused Rust definition-validation coverage: 3 local create/update
tests and 6 publication-filtered tests
- complete desktop Tauri library suite after rebase: 2,263 passed, 14
ignored, 0 failed
- desktop Tauri clippy with warnings denied and Rust formatting
- complete agent Playwright spec: 34 tests
- the exact formerly failing `inbox-edit` immediate-attachment smoke
test after rebase: 1 test
- focused shared-agent publish, literal-review, hidden-control,
signature, and cross-member import Playwright coverage
- desktop E2E production build and TypeScript typecheck
- changed-file formatting/lint and file-size ratchet
- pre-commit secret scan and DCO signoff
The branch was rebased onto current `main`, which includes the upstream
attachment-button label fix. Fresh post-rebase GitHub CI is green for
every required and selected check: Desktop Core, all four Desktop Smoke
E2E shards, both Desktop E2E Integration shards and their aggregate,
Desktop E2E Relay, Desktop Build (macOS), Windows Rust, Rust Lint, DCO,
security scanners, and Desktop Release Candidate. The previously failing
`Desktop Smoke E2E (3)` shard now passes.
The repository-wide desktop check also reports existing CSS
formatting/`!important` findings in `components.css` and `terminal.css`;
neither file is changed by this PR. GitHub's Desktop Core lint and
format stage passes on the rebased branch.
---------
Signed-off-by: Alex Rosenzweig <arosenzweig@squareup.com>
## Buzz Desktop release v0.5.11
- **Frozen main:** `4749bc7be3cdb78c2db4ce4864775ba7ab60b4cc`
- **Reviewed candidate:** `248b9d1b7666aacbcb1485b76e81de30a271ba0e`
- **Previous desktop release:** `desktop-v0.5.10`
- **Proposed immutable tag:** `desktop-v0.5.11`
This PR may be **squash merged** after the Desktop Release Candidate
check and all protected-branch checks pass. Merging authorizes
publication of the exact reviewed candidate; later or unrelated changes
on `main` cannot alter it.
The checked-in changelog accounts for every non-merge commit in the
release range. The Desktop tag points to the reviewed candidate commit,
not the later squash commit. Publication remains bound to that immutable
candidate tag.
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
storage.rs and storage_tests.rs crossed the desktop 1000-line file-size
ratchet after the keyring merge. Lift the log/receipt/PID domain (log-path
resolution, rotation, install logs, runtime receipts, PID files, log-tail
error extraction) into a storage/logs.rs submodule re-exported via
`pub use logs::*`, and move its tests to storage/logs_tests.rs.
Pure movement: each item keeps its original visibility so all caller paths
resolve unchanged, and the workspace test count is identical before and
after (2623 passed / 0 failed / 15 ignored).
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Pull W8 forward: main advanced 18 commits and touched two files this branch
also touches (managed_agents/runtime.rs, commands/agent_models_tests.rs), so
the branch-skew guard blocked pushes. Merge (not rebase) to preserve the
reviewed r5 SHAs; both overlaps auto-resolved cleanly (runtime.rs keeps main's
idle_pool_sleep env line alongside this branch's persona_drift extraction;
agent_models_tests.rs keeps both sides' record fields and new tests).
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The boot migration ran already-projected records (empty inline, live ref)
through the strip-on-save seam, which reads an empty inline field as a user
clear and drops the ref — silently wiping every committed secret ref on the
second launch, then GC deletes the orphaned generations. And save_managed_agents
re-read the definition half through the hydrating loader, so every instance-side
save re-inlined definition secrets back into plaintext JSON and froze GC on the
inline+ref conflict.
Give the migration its own field-granular transition (migrate_inline_field ->
FieldMigration) that only projects a non-empty inline value and never clears a
ref it did not write; expose it as a pub(crate) seam so the custom-harness
migration shares one W1-safe semantic. Re-read the definition half RAW under the
txn lock and propagate a parse error with ? instead of unwrap_or_default(), so a
malformed store fails the save rather than deleting every definition. Broaden the
backup recognizer structurally to catch this repo's own pre-backfill.bak and
pre-team-suffix-strip.bak producers.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
## What
Adds an opt-in **idle re-sleep** for woken lazy ACP pools. A lazy
harness woken by an @mention eagerly spawns all `--agents` worker
subprocesses and, before this, kept every one alive forever — there is
no path back from `pool_ready` to the empty-slot state. Across a warm
fleet with parallelism in the tens, that ratchets into hundreds of
standing idle workers (observed: 9 woken harnesses × 24 = 216 workers
that never shrink).
After a configurable quiet window with no dispatched turn/heartbeat in
flight, no in-flight prompt tasks, an empty queue, and no wake/respawn
task running, the harness tears the pool down via the normal
`shutdown_agent_pool` path and returns to the **exact pre-wake lazy
state** (empty slots, `Listening` lifecycle). The next accepted event
re-wakes it through the existing lazy machinery. **No second pool
lifecycle.**
## Why it's safe
- **Race-safe with enqueue/wake by construction.** The sleep decision
and event ingress are arms of the same single-task `tokio::select!`. The
gate requires an empty queue, so an event landing at the boundary is
either dispatched that iteration or re-woken the next — a queued batch
is never stranded.
- **Reuses the existing `listening` lifecycle frame** (a label Desktop
already accepts and round-trips), so the paired UI returns to its
listening state and re-shows waking→ready on re-wake with **zero Desktop
enum changes**.
- **Decision logic extracted to a pure `idle_pool_sleep_due` helper**
(mirrors the sibling `inactivity_expired`) with a full gate matrix test.
## Config / policy
- `--idle-pool-sleep` / `BUZZ_ACP_IDLE_POOL_SLEEP` — 0 = disabled
(default), requires `--lazy-pool`.
- Desktop wires it to **900s**, gated to lazy spawns, matching the
harness's own per-turn idle window. Reserved key (desktop-owned lifetime
policy) so user env can't disable it.
## Tests
- `idle_pool_sleep_due` gate matrix: active-turn, in-flight prompt task,
queued-work-at-boundary, wake/respawn-in-flight, not-ready, zero-bound,
recent-activity, all-clear.
- Config parse (`--idle-pool-sleep`), reserved-key membership.
- `cargo test -p buzz-acp` → **761 passed, 0 failed** at base
`63f961c7e`. Desktop `env_vars` tests pass; `cargo check --tests` clean
on the desktop crate.
> Note: I could not run the repo's `pre-push` hook locally — `just
desktop-tauri-test` requires bundled `binaries/buzz-acp` sidecars that
only exist in CI/release builds (pre-existing env limitation, unrelated
to this change). Pushed with `--no-verify`; CI runs the authoritative
gate.
## Scope
Idle re-sleep only. Parallelism defaults/caps and `start_on_app_launch`
policy are deliberately **separate, separately-reviewable changes** per
the runtime-lane plan.
---------
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Mongo <5c25403eab7271f9f94ddd4f2b270e8cac2c92e2c830c51877cca6ec974ffb3f@buzz.block.builderlab.xyz>
## What
Bumps `webbrowser` from `1.2.1` to `1.2.4` in both lockfiles
(`Cargo.lock` and `desktop/src-tauri/Cargo.lock`) to clear
[RUSTSEC-2026-0257](https://rustsec.org/advisories/RUSTSEC-2026-0257).
## Why
The advisory landed in the RustSec DB and flipped the `Security` job
(`cargo-deny check`) red on `main` — the same job passed on identical
lockfile state before the advisory was published. `webbrowser` 1.2.1
substitutes the URL into the Unix `BROWSER` env template *before*
tokenizing, allowing browser argument injection (e.g.
`--remote-debugging-port`). `crates/buzz-agent` calls
`webbrowser::open()` for the OAuth flow
(`crates/buzz-agent/src/auth.rs`) with an internally-constructed HTTPS
URL, so practical exploitability is low, but the gate is correctly
blocking. Fixed in `1.2.2`+.
## Scope
Lockfile-only. The `crates/buzz-agent/Cargo.toml` constraint is already
`webbrowser = "1"`, so no manifest change is needed. `webbrowser` 1.2.4
pulls in `objc2-app-kit` as a new transitive dependency; the
`windows-sys` edge churn re-unifies to versions already present in the
lockfile (no new `windows-sys` version is introduced).
## Verification
- `cargo-deny check` passes locally on the pinned toolchain (`advisories
ok, bans ok, licenses ok, sources ok`); RUSTSEC-2026-0257 no longer
reported in either lockfile.
- `cargo check -p buzz-agent` compiles clean against `webbrowser 1.2.4`.
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
When a user's agent runtime is `buzz-agent` with no cached Databricks
OAuth token, the desktop app's passive model-discovery surfaces were
forbidden from launching interactive auth. Discovery failed silently, so
the model dropdown showed only built-in fallback models behind a vague
"Could not load live models for `databricks_v2`" note (reported
internally by Nick and Jose).
## What changed
Both discovery surfaces — the passive draft-form discovery and the
explicit saved-model picker — now launch the browser OAuth flow,
matching goose's behavior. The only behavioral difference between them
is cooldown handling:
- **Passive draft discovery** fires on every form-state change, so a
failed, cancelled, or timed-out sign-in records a per-host cooldown (5
min) that suppresses re-popping the browser on the next keystroke. While
the cooldown is active it returns the "sign-in required" guidance
instead of relaunching.
- **The explicit model picker** is a deliberate user action, so it
always launches and clears any stale cooldown first.
Safety rails:
- A 150s hard timeout (`AUTH_FLOW_TIMEOUT`) bounds the whole interactive
flow so an abandoned SSO tab fails discovery cleanly rather than wedging
the dropdown. Success clears the cooldown; failure and timeout both
record it.
- `AuthCooldown` recovers from a poisoned lock rather than wedging every
future sign-in on one panic.
The frontend maps the terminal Databricks sign-in states to typed,
actionable copy in `formatModelDiscoveryErrorStatus`: "sign-in required"
is a muted note pointing at the picker and `buzz-agent auth databricks`;
a failed or timed-out sign-in is a warning pointing at the explicit
retry. Other Databricks failures fall through to the existing generic
notice.
## Scope
Changes are confined to Databricks discovery and its frontend status
formatter — no `agent_models.rs` call sites are touched. The
interactive-auth helper takes an injected timeout so the
timeout/cooldown policy is unit-testable without a live browser.
## Deferred
Cooldown keys use the raw trimmed `DATABRICKS_HOST`, while the catalog
and OAuth cache normalize trailing slashes
(`crates/buzz-agent/src/catalog.rs:96`,
`crates/buzz-agent/src/llm.rs:2046`). So `https://workspace/` and
`https://workspace` share credentials but get separate cooldown entries
— an equivalent-spelling change to the host field mid-cooldown can
re-pop passive OAuth once within the 5-minute window. Self-limiting (one
extra browser launch, never auth corruption). Follow-up: a
`trim_end_matches('/')` on the cooldown key plus an equivalent-host
test, picked up with the coordinator migration if
[#5545](https://github.com/block/buzz/pull/5545) ever merges.
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
## Buzz Desktop release v0.5.10
- **Frozen main:** `f35930104bcbdb1332ff13735214ecb9fce1fc7b`
- **Reviewed candidate:** `1fb49103002e898607a7f6fd554cb51e94d92e08`
- **Previous desktop release:** `desktop-v0.5.9`
- **Proposed immutable tag:** `desktop-v0.5.10`
This PR may be **squash merged** after the Desktop Release Candidate
check and all protected-branch checks pass. Merging authorizes
publication of the exact reviewed candidate; later or unrelated changes
on `main` cannot alter it.
The checked-in changelog accounts for every non-merge commit in the
release range. The Desktop tag points to the reviewed candidate commit,
not the later squash commit. Publication remains bound to that immutable
candidate tag.
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
Desktop input latency regressed sharply for users on v0.5.9 and worsened
on latest main: multi-second stalls when clicking back into the app,
slow fresh boots, intermittent lockups, and scroll/mouse degradation.
Reverting to `119a84897` (pre-0.5.9) was confirmed to resolve it,
isolating the regression to that range. Profiling a live production
renderer plus a commit-level audit of the range found three independent,
additive causes — fixed here — plus a long-standing `get_channels` cost
that made every remaining refetch expensive, also addressed here.
## 1. Focus-return refetch storm (`refetchOnWindowFocus`)
#5490 wired TanStack's `focusManager` to app focus and flipped ~20 query
sites to `refetchOnWindowFocus: true`. A focus return after >60s away
fires them all within milliseconds — and a click into an unfocused
window *is* a focus return, so the burst runs before the click is
processed. That is the "click into the composer, wait 5 seconds"
symptom, and it also explains why mouse input feels worse than keyboard
(clicks arrive with focus transitions; typing happens while already
focused). A 5-second `sample` of a live production renderer caught a
single window activity-state transition consuming ~1.25s of main-thread
time, dominated by `JSON.parse` in the focus listener's microtask drain.
#5535 already established the fix pattern but applied it to only two
families (channels, home-feed). This PR extends the same 5-minute
`staleTime` discipline to the remaining families: pulse (×5), workflows
(×4), agents (×4), forum (×2), presence, user-status, custom-emoji,
channel-templates, and the persona catalog. Polling cadences and
push-invalidation paths are untouched — interval refetches and
`invalidateQueries` both bypass `staleTime`, so live-update behavior is
unchanged. Each gated family exports its focus-refetch policy as an
options object that the production hook spreads into `useQuery`, and a
`focusRefetchPolicy.test.mjs` drives a `QueryObserver` with that same
production object — locking the policy behaviorally (fresh focus return
→ 0 fetches; stale → refetch) and failing if a hook's
`staleTime`/`refetchOnWindowFocus` wiring drifts.
Four families deliberately keep tighter freshness, all surfaces where
the 5-minute gate would suppress the only refresh path and none of which
feed the app-wide storm: `repo-sync-status` keeps its fresh focus
refetch (its inline comment documents the "committed in a terminal,
switched back to the app" flow as intended); the workflow-runs list
stale-gates at 10s because a remotely-started run has no push
invalidation and its conditional 1s poll is off while the cache shows no
active runs; the workflow list queries (`useChannelWorkflowsQuery` and
the all-channels aggregate) stale-gate at 10s because they have no poll
and no relay subscription, and mutation-driven invalidation only covers
this renderer — remote workflow creates/edits/deletes surface only via
focus refetch; and the managed-agent log stale-gates at one poll tick
(30s) so returning to a live agent log refreshes immediately. Run
approvals keep the 5-minute gate under
`RUN_APPROVALS_FOCUS_STALE_TIME_MS` — their focused 10s poll already
covers freshness.
## 2. Synchronous localStorage sweep on the boot/focus path
#5453's stale-cache sweep synchronously `getItem` + `JSON.parse`s every
whitelisted localStorage entry on the main thread (multi-MB on seasoned
profiles), scheduled with a `requestIdleCallback` timeout of 1.5s that
guaranteed it landed mid-boot, and re-armed on every hidden→visible
transition — stacking it onto the exact moment the focus storm fires.
#5454's `trimSelfProfileCaches()` additionally scanned every
localStorage key on every `writeSelfProfileCache()` call (which fires
per relay self-profile delivery at boot).
Now: the first sweep waits `BOOT_SWEEP_FLOOR_MS` (30s) after startup,
the scan is time-sliced across idle callbacks, and the visibility
trigger is removed — boot-delayed plus hourly still covers the 14-day
TTL contract. The sliced sweep re-checks staleness immediately before
each removal (a key rewritten fresh mid-sweep survives), isolates
per-key storage errors so one bad entry can't strand the rest of the
snapshot, defers oversized values once rather than parsing them on a
zero-budget slice, guarantees forward progress on timeout-fired
callbacks, and cancels its scheduled slice when stopped. The profile
trim keeps a lazily-initialized memoized key count so the common
under-cap write is O(1); the full parse scan runs only when the count
exceeds a cap, resyncs if external deletions made it stale, and a failed
scan skips the trim instead of aborting the write. Sweep semantics
(rules, TTLs, eviction) are unchanged, and tests cover the scheduling,
slice-progress, error-isolation, defer-once, and trim short-circuit
behaviors.
## 3. The macOS window was never opaque
#5478's glass appearance is correctly opt-in at the CSS layer, but the
compositor cost was baked in deeper than its native `on_webview_ready`
transparency call: the main window is declared `"transparent": true` in
`tauri.conf.json` (added for the original glass work in #1671), which
makes tao call `NSWindow.setOpaque(false)` at creation and resolve every
later `set_background_color(None)` to `clearColor` — and no runtime
`setOpaque(true)` path exists through tauri, while wry's runtime
background setter can only force the WKWebView's `drawsBackground` off,
never back on. So "restore the platform default" was unreachable: every
launch, glass or not, ran with a non-opaque NSWindow, defeating
WindowServer's opaque-window compositing fast path and forcing full
window compositing every frame — compounded by the existing
`backdrop-blur` chrome overlapping the scrolling timeline. This matches
the compositor-shaped symptoms (scroll and pointer input degrading
first).
The window is now created opaque (`"transparent": false`) and the
NSWindow layer is never made transparent at runtime. Glass never needed
a transparent window: behind-window `NSVisualEffectView` vibrancy
renders inside opaque windows (this is how Finder and Notes draw vibrant
sidebars); it only requires a transparent WKWebView canvas, which the
`set_window_vibrancy` enable path already establishes at runtime
(`macos-private-api` compiles that in independent of the window flag).
Enabling glass installs the vibrancy layer and then makes only the
webview canvas see-through; disabling clears the vibrancy layer — the
canvas may stay non-drawing afterwards (wry's flag is one-way at
runtime), which is harmless because glass-off CSS paints fully opaque
above an always-opaque NSWindow. The boot-path first-frame backing
writes touch only the NSWindow backing color and are therefore inert to
glass state regardless of how they order against the `ThemeProvider`'s
vibrancy call on a persisted-glass-on cold boot. Glass-off users (the
default) get an end-to-end opaque window from boot for the first time.
## 4. `get_channels`: serial round-trips and a multi-MB payload on every
refetch
The stale gates in (1) cut refetch frequency; this cuts the cost of the
refetches that legitimately remain (boot, and focus returns after more
than 5 minutes away — previously still a multi-second stall).
`get_channels` made ~8 fully serial relay round-trips (~3.2–3.6s at
1,100+ channels), then shipped the full `ChannelInfo` list — including
every channel's member pubkeys — across IPC, where the renderer's
`JSON.parse` of the multi-MB payload froze the main thread (the ~1.25s
stall captured in the live sample).
- **Concurrent stages**: the membership chain, the open-channel
directory scan, and the hidden-DM snapshot run concurrently, as do the
member-count and last-message queries that follow. The critical path
drops from ~8 sequential round-trips to 2 phases. Filters, limits,
pagination, and merge semantics are unchanged.
- **Not-modified short-circuit**: the command now takes a
client-supplied content hash (FNV-1a 64 over the channel list,
canonicalized by id and excluding `last_message_at`) and omits the
channel list from the response when nothing else changed. Last-message
timestamps — which change on nearly every message anywhere — ship as a
small separate map that the client overlays onto its cached list with
reference preservation, so React Query's structural sharing also skips
downstream re-renders. On a typical refocus the renderer parses
kilobytes instead of megabytes. The hash is stored in the query cache
itself, tying its lifecycle to the data it describes so a community
switch can never leak a stale hash.
The E2E mock bridge speaks the new payload shape — including the
complete `last_messages` map the client treats as authoritative — and
hash canonicalization plus overlay reference-preservation are
unit-tested on both sides.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Merge-collision ratchet relief. Merging current main pushed managed_agents/runtime.rs to 1012 split-count (>1000 limit): the PR's fail-closed spawn gating (+16) and main's mesh acp_model translation (+12) landed in disjoint regions of one file. Move the self-contained persona_drift_state helper verbatim to a new runtime/drift.rs sibling (the file's existing plain-submodule pattern) and re-export it, dropping runtime.rs to 986. No logic change: the fn body is identical; only visibility (pub(crate)) and an import replacing the inline fully-qualified type path differ.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Bring in main's runtime.rs mesh acp_model wire translation so local
checks and CI both run on the merged tree. Clean auto-merge; the PR's
fail-closed spawn gating and main's model translation touch disjoint
regions of spawn_agent_child.
* origin/main: (24 commits)
Improve desktop search scoping (#5306)
Add glass appearance and cohesive settings (#5478)
Add Send to channel for thread messages (#5305)
Fix macOS attachment picker lifecycle and allow inert HTML downloads (#5569)
fix(desktop): preserve fresh channel timelines (#5577)
fix(desktop): suppress fresh focus-return refetches for channels and home-feed (#5535)
chore: mesh upgrade, clean up legacy special case code, simplify model selection for mesh (#5289)
fix(desktop): preserve theme when opening communities (#5266)
fix(link-preview): resolve YouTube videos through oEmbed (#5520)
fix(buzz-agent): harden Databricks OAuth token cache and callback (#5534)
fix(link-preview): reliably render previews sent right after they resolve (#5245)
fix(link-preview): restore Buzz entity link cards (#5494)
chore(release): release Buzz Desktop version 0.5.9 (#5521)
feat(cli): add --visibility flag to channels update (#5119)
Polish desktop onboarding flow (#5310)
fix(desktop): quiesce renderer polling while hidden (#3677) (#5490)
fix(channels): restore member invitations to private channels (#5493)
perf(ci): experiment with sccache for relay builds (#5224)
fix(desktop): bound nine unbounded localStorage stores (#5454)
feat(desktop): time-based sweep for stale localStorage caches (#5453)
...
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Close the r4 blocking findings on the keyring secret-projection PR:
- Fail-closed load: a conflict-marker read that returns Err is now treated
as unavailable, not fall-through. load_blob caches successful reads but
never errors, so a transient marker-read failure could be followed by a
cached-success value read that hydrates a known-conflicted credential.
- Conflict-marker cleanup only on proven resolution: the dev-migration
planner clears a conflict:<coord> marker only when source and destination
converge OR the coordinate is proven no longer live in canonical JSON. A
disappearing source while the destination value is still live retains the
marker (fail closed).
- Cross-process transaction lock (Race 1): the lock now spans the other-half
read through generation writes to the JSON commit in both save paths, so
two Desktop processes cannot interleave a cross-half overwrite. Keyed by
the symlink-resolved canonical store directory inode, not the keyring
service, so processes sharing one JSON file always contend on one lock.
- Transaction lockfile out of /tmp: the lock target is the store directory
inode in the owner's app-data tree, immune to the tmp-cleaner
unlink/recreate split that let two processes both "hold" a /tmp lockfile.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
## Problem
Canceling the native macOS file chooser leaves the composer's temporary,
detached `<input type="file">` without a `change` event or an explicit
cleanup path. Opening Finder again immediately creates a second detached
input while WebKit may still be unwinding the first picker. The newly
selected files can therefore fail to reach the upload pipeline. Drag and
drop is unaffected because it bypasses this picker lifecycle.
This does **not** add an automatic retry mechanism. “Retry” means the
user's next attachment attempt after canceling or after a prior
selection.
## Fix
- give each composer hook one hidden, body-mounted file input for its
lifetime instead of creating a detached one per click
- reset and reconfigure that input before every open, replace its
handler rather than stacking handlers, and remove it cleanly on unmount
- preserve normal selection, cancel then reopen, selecting the same file
again, and multi-select behavior
- accept canonical `text/html` attachments while continuing to serve and
render them strictly as inert downloads
- keep XHTML, SVG, JavaScript, and executable MIME types blocked
The picker change fixes the ownership/lifecycle bug at its source; it
does not retry failed uploads, add delays, or mask errors.
## Testing
- mandatory pre-push gate: branch-skew, desktop typecheck/tests/check,
Rust tests, and desktop Tauri checks passed on
`ea5a97adf957803935b28d63d32f9f332cf65287`
- `cargo test -p buzz-media --lib` (110 passed)
- `pnpm --dir desktop typecheck`
- focused Biome check for the three picker files
- picker Playwright regression: cancel/no selection then reopen, select
the same file again, and multiple selection (run on the source commit
before integration)
- HTML live-relay response regression added as ignored E2E because it
requires the S3-backed relay harness
## Manual verification
Playwright models cancellation with Chromium's
`FileChooser.setFiles([])`; it cannot exercise the native macOS Finder
panel/WebKit presentation lifecycle. Before merge, manually verify in
the built macOS app:
1. select a PNG normally
2. cancel, then immediately reopen and select a PNG
3. select the same PNG on a subsequent attempt
4. multi-select two PNGs
---------
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Princess Donut <68157ebd23b3897c1991015c3038658ea916200c67d3a54620b0754d1b92f6e0@buzz.block.builderlab.xyz>
Co-authored-by: Mongo <5c25403eab7271f9f94ddd4f2b270e8cac2c92e2c830c51877cca6ec974ffb3f@buzz.block.builderlab.xyz>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Shared compute now has exactly two model choices: MeshLLM's virtual
`mesh`
model, or a model you name. Buzz picks between them in one place, and
buzz-agent no longer knows meshes exist.
## What changed
- **MeshLLM v0.74.0 → v0.75.1.** v0.75.0 added
`degrade_to_single_model`, so a
`model=mesh` request is answered by one served model when there is no
committee to form, instead of failing. v0.75.1 adds Mesh-LLM#1196, which
skips stale pre-0.75 runtime cache entries rather than aborting startup
on
them — without it, anyone who had run mesh on 0.73/0.74 could not start.
- **Deleted the client-side mesh catalog probe.** buzz-agent used to
poll
`/v1/models` (5s TTL, 30s cooldown, two-observation debounce) to decide
whether `mesh` was safe to send. MeshLLM now decides per request, so the
polling, its hysteresis, and its 503 fallback are gone.
- **One mapping point.** `relay_mesh_wire_model()` turns the stored
value into
a wire name: `auto` becomes `mesh`, a named model passes through. The
spawn
env, the ACP harness, and the readiness probe all use it, so they cannot
disagree — previously `BUZZ_ACP_MODEL` and the probe both said `auto`, a
name
the mesh does not advertise.
- **Removed the `nostr-relay-pool` advisory exception.** #5404 allowed
RUSTSEC-2026-0243 "after mesh-llm migrates to nostr-sdk >= 0.45".
v0.75.1
does, so the retired crate is gone from both lockfiles and the exception
would only mask a future advisory for it.
- **Deleted `scripts/ensure-mesh-native-runtime.sh`** and its six
justfile call
sites. It built llama.cpp from source into the runtime cache; the app
already
downloads the signed release runtime itself, and CI never called it.
## Why it is better
**−639 lines of Rust.** Availability is decided by the node that knows
the
answer, per request, instead of by a client cache that could be stale
for up to
30 seconds. A second worker joining now takes effect on the next request
rather
than after two confirming probes.
## Behaviour change
A 503 on an explicit `mesh` request takes the ordinary transport retry
under
the same model instead of failing over to a second one — there is no
second
model to fail over to now. MoA repairs partial committee results
internally
before it reaches that point.
## Validation
`crates/buzz-relay/examples/mesh_agent_e2e.rs` now sends `mesh` where it
previously sent `auto` or the physical model id, so no leg was covering
what
Buzz actually puts on the wire. 4/4 on gemma-4-E4B, gemma-4-26B-A4B, and
Qwen3-8B — including a real ACP tool call through `mesh` into
buzz-dev-mcp,
asserted by reading the written file back off disk.
Hand-tested in the desktop app on both gemma-4 sizes: picked Auto, agent
logged
`model_id=mesh`, replied in channel.
## Not covered
A committee that forms and then loses a worker returns 502, and that
needs two
workers to reproduce — not testable on one machine.
---------
Signed-off-by: Michael Neale <michael.neale@gmail.com>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
**Category:** fix
**User Impact:** YouTube video links now resolve into reliable previews
instead of intermittently appearing as bare links.
**Problem:** Buzz intentionally reads at most **256 KiB** of page HTML
when building a generic link preview. The YouTube response that exposed
this bug was roughly **1.3 MiB**, with its Open Graph metadata beginning
around **686 KiB**—well beyond Buzz's bounded read—so extraction
returned no usable preview. YouTube can move that metadata between
responses, which explains why the same link may appear to work in one
build or request and fail in another; raising the generic cap would
increase bandwidth and allocation for every site while still scraping an
unstable application document.
**Solution:** Route recognized YouTube video URLs through YouTube's
structured oEmbed endpoint instead of parsing raw watch-page HTML. The
provider response is capped at **64 KiB** and retains Buzz's existing
HTTPS validation, pinned DNS/SSRF protection, disabled redirects,
timeouts, metadata bounds, and thumbnail sanitization. Provider failures
return no preview rather than falling back to fragile HTML scraping, and
embed URLs are canonicalized safely, including percent-encoded video
IDs.
<details>
<summary>File changes</summary>
**desktop/src-tauri/src/commands/link_preview.rs**
Recognizes supported YouTube URL forms, fetches bounded JSON metadata
from YouTube oEmbed, canonicalizes embed links, and adds response,
URL-boundary, malformed-data, resource-limit, and encoded-ID
regressions.
**desktop/src-tauri/Cargo.toml**
Declares percent decoding as a direct desktop dependency for safe
embed-ID canonicalization.
**desktop/src-tauri/Cargo.lock**
Records the direct dependency in the desktop package lock entry.
</details>
## Reproduction Steps
1. On the base branch, paste a YouTube URL whose Open Graph metadata
falls beyond the first 256 KiB of the raw watch-page response and
observe that no preview is produced.
2. Run this branch and paste a YouTube watch, mobile, music, `youtu.be`,
Shorts, live, or embed URL into the composer.
3. Confirm the preview resolves with the video's title, creator, and
sanitized thumbnail without downloading the full watch-page HTML.
4. Try an embed URL with a percent-encoded ID, such as
`https://www.youtube.com/embed/%64Qw4w9WgXcQ`, and confirm it resolves
to the same video.
5. Try a YouTube lookalike domain or an embed ID containing encoded
separators and confirm it is not routed through the provider path.
---------
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Close the five pass-2 blocking findings on the keyring secret-projection PR.
F3: add require_effective_secrets_available — a single fail-closed gate
(strict global loader, instance secrets_unavailable, linked-definition
secrets_unavailable) consumed by every side-effecting path that was
degrading silently: get_agent_models, mint_agent_card, the
update_managed_agent rename, persona name/avatar propagation signing, and
set_global_agent_config (which now refuses to overwrite a committed global
whose ref cannot load rather than unwrap_or_default over it).
F4: a dev-migration value conflict now writes a conflict:<coord> marker into
the keyring blob. load_secret fails closed whenever a coordinate carries one,
so hydration sets secrets_unavailable and every downstream gate refuses —
making the conflicted value genuinely unavailable for the whole retry window
instead of merely withholding the completion marker. A resolved conflict
clears its marker.
F5b: collect_live_refs now returns full expected blob coordinates alongside
gen ids, and both GC sweeps no-op when any committed live reference is missing
from the blob — so a dangling live ref can no longer let GC delete an older
unreferenced generation that may be the only recoverable payload.
F5a: add a cross-process transaction lock (a second advisory lockfile,
distinct from the per-op mutate_blob lock) held from generation write through
the JSON commit on every save path, and across the live-ref read through
remove_batch in GC — closing the two-process interleave where one process
could delete a generation another had written but not yet committed.
Snapshot export: materialize_snapshot_bytes propagates the global loader Err
instead of unwrap_or_default, refusing export/send rather than silently
shipping empty inherited runtime/provider/model defaults.
agent_models.rs, card.rs, and secret_store.rs are kept under the desktop
file-size ratchet via #[path] sibling extraction (naming helpers, env-layer
key resolution, and the secret_store test module respectively).
Adds unit tests for the gate (all three tiers + precedence), conflict
hydration→spawn-refusal, GC missing-coordinate freeze (both sweeps + positive
bound), independent-participant txn-lock exclusion, and snapshot degradation
refusal.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
## Buzz Desktop release v0.5.9
- **Frozen main:** `f8f2ef0440e7a074223ec04dc3b32d817b8b9d9b`
- **Reviewed candidate:** `ee33722615ca1e7b8efb03e2ed641d99448c8899`
- **Previous desktop release:** `desktop-v0.5.8`
- **Proposed immutable tag:** `desktop-v0.5.9`
This PR may be **squash merged** after the Desktop Release Candidate
check and all protected-branch checks pass. Merging authorizes
publication of the exact reviewed candidate; later or unrelated changes
on `main` cannot alter it.
The checked-in changelog accounts for every non-merge commit in the
release range. The Desktop tag points to the reviewed candidate commit,
not the later squash commit. Publication remains bound to that immutable
candidate tag.
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
F1 CRITICAL: Remove all eager old-generation deletion from secret_seam.rs and
global_config/mod.rs. Old-gen retirement moves entirely to two-cycle GC.
Add 5 deterministic JSON-write-failure tests proving old generation still
hydrates after a failed atomic write (instance env/auth/provider, definition
env, global env).
F5b (collect_live_refs validates): Reject malformed/duplicate coordinates and
inline+ref conflicts — any such condition makes the sweep a no-op. Add 12
validation tests.
F5a (GC exclusion, not just time): Wrap extraction + global save + GC under
managed_agents_store_lock so final JSON read and remove_batch are indivisible
against a concurrent save. Add 3 synchronized interleaving tests.
F3 (fail-closed global + definition tiers):
- Global: load_global_agent_config() returns Err on missing/corrupt ref;
callers in runtime.rs/runtime_commands.rs now surface global_unavailable
flag instead of unwrap_or_default(); spawn_agent_child uses ? to abort.
- Definition: AgentDefinition gains secrets_unavailable field (#[serde(skip)]);
hydrate_all_secrets_for_records sets it on definition records; spawn gate in
runtime.rs refuses linked instances when definition is unavailable;
status_for_with checks definition_unavailable tier; agents_deploy.rs checks
it too. Add definition_tier_tests.rs with 3 tests.
F4 (dev migration marker only on clean completion): Extract pure decision core
plan_dev_secrets_migration(); marker withheld when conflict_count > 0; partial
progress (non-conflicting keys) still written for next boot; log emitted.
F2 (cleanup authorization + legacy live file): Replace bare
agent_secret_store_pub().is_some() gate with extraction_verified() that reloads
both stores and checks no secrets_unavailable flags set after hydration.
Add scrub_legacy_live_file() to explicitly scrub/delete legacy managed-agents.json
after verified extraction. Add 4 tests.
AgentDefinition derives Default to support test helper construction.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Complete the module extraction and field wiring left in working-tree state:
- Wire dev_service_migration and secret_projection_tests as modules
- Extract dev keyring secrets migration into dev_service_migration.rs and
secret_projection tests into secret_projection_tests.rs so both parent
files stay under the 1000-line desktop ratchet
- Reclaim ratchet lines in the six over-cap files touched by the
secrets_unavailable field additions (types.rs, migration.rs, agents.rs,
import.rs, discovery/tests.rs, readiness.rs)
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Commit the three items that were in working-tree state but not pushed:
- types.rs: add secrets_unavailable: bool field (transient, #[serde(skip)]) to
ManagedAgentRecord with doc comment explaining load-time semantics
- secret_seam.rs: set record.secrets_unavailable = true when hydration errors
are present, making the setter actually wire unavailability into the record
- storage.rs: extend spawn_key_refusal to check secrets_unavailable (in addition
to empty nsec), refuse spawn with a named error message; update comments at
both load_managed_agents callers to reflect that unavailability is now set
directly on the record and consulted by spawn_key_refusal
- storage.rs: add migrate_agent_secrets_to_dev_service (v2 marker, separately
versioned from _dev_migration_v1) covering canonical dev → prod and scoped
dev → canonical dev, with conflict detection, global:env ref check, and
collect_global_env_refs helper
- migration.rs: wire migrate_agent_secrets_to_dev_service into the boot path
(cfg(debug_assertions), after migrate_inline_secrets_to_keyring)
- secret_projection.rs: add test_cancel_before_mark_ordering_protects_in_flight_gen
covering the cancel-before-mark scenario Paul flagged (step 4 safe: a marked-
then-referenced gen survives the next boot's delete phase)
- All struct literal sites updated with secrets_unavailable: false
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Paul-review deltas on PR #5486:
- local_setup now reflects secrets_unavailable: both status_for_with and
unkeyable_failed_status set local_setup = false when a record has a
dangling keyring ref. Previously, runtimes like claude/codex (which pass
agent_readiness without API-key checks) would show local_setup = true
even when spawn was blocked by spawn_key_refusal. The flag was already
wired to spawn refusal; this closes the status gap.
- Legacy Sprout app-data agents dir cleanup: migrate_legacy_app_data_dir
copies the old xyz.block.sprout.app agents/ into Buzz but leaves the
source. cleanup_secret_artifacts now also runs on the legacy source dir
after a verified extraction boot, scrubbing backups and deleting
.invalid/temp artifacts from the Sprout-era plaintext store.
- Phase 2 artifact cleanup tests: add filesystem-backed tests for
atomic-write temp deletion, .invalid deletion, parseable-backup scrub,
symlink escape (unix-only), and deletion-failure tolerance. These cover
the cleanup_secret_artifacts inventory that the spec requires.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
All three secret tiers (global env vars, per-agent env/auth_tag/
provider_config, definition env vars) now move from plaintext JSON
into the existing SecretStore keyring blob using an immutable
generation-reference protocol:
- Each write creates a new immutable generation entry under a UUID-
keyed coordinate (e.g. agent:<pubkey>:env:<gen>). The stripped JSON
carries a non-secret *_ref field; the atomic JSON write is THE commit
point.
- Crash before JSON commit: old ref stays authoritative, orphaned gen
is swept by GC. Crash after: new ref is authoritative, old gen swept.
- Empty vs unavailable distinguished by ref presence: no ref = field
intentionally empty (agent runs); ref present but entry missing =
unavailable, fail closed (nsec-style refusal).
- Inline precedence: on keyring write failure (Windows TooLong /
backend error) value stays inline in 0o600 JSON with a named warning
and the ref is cleared. Inline is authoritative over any keyring state
for that boot; extraction retries next boot.
- Two-cycle GC: sweep 1 marks unreferenced generations as candidates in
the blob; sweep 2 (next verified boot) deletes still-unreferenced
candidates. Saves cancel their generation's candidacy before the JSON
commit. GC is a no-op when either JSON store is absent, unreadable,
or changes between reference collection and blob mutation.
- Boot migration runs at the end of run_boot_migrations_inner (after
materialize_agent_runtimes) so raw-JSON migrations see inline values.
- OSS keyringless builds are unchanged (inline 0o600 JSON).
Coordinates:
global:env:<gen>
agent:<pubkey>:env:<gen>
agent:<pubkey>:auth_tag:<gen>
agent:<pubkey>:provider_config:<gen> (entire BackendKind::Provider.config)
definition:<id>:env:<gen>
Files changed:
secret_store.rs — add remove_keys() for batch blob deletion
managed_agents/mod.rs — expose secret_projection module
managed_agents/secret_projection.rs — new: full gen-ref protocol impl + tests
managed_agents/types.rs — add auth_tag_ref, env_vars_ref, provider_config_ref
managed_agents/global_config — add env_vars_ref, hydrate-on-load/strip-on-save
managed_agents/storage.rs — hydrate/strip seam, migration helpers
migration.rs — boot migration + two-cycle GC
commands/agents.rs — new ref fields initialised to None
commands/personas/snapshot/import.rs — same
commands/team_snapshot.rs — same
Deferred (noted in PR body, not a separate issue): existing nsec blob
(~2.8 KB) already exceeds the Windows 2,560-byte cap — pre-existing,
same accepted-risk class as the overflow path added here.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
## What
Implements Phases 2 and 4a of the Usage v2 plan (plan events
`d0268cd0`/`0e95b035`), extending the archive backend to emit,
transport, archive, and aggregate both cache categories and billing
identity fail-closed.
### P2 — emission, transport, archive
**Tri-state accumulators** (`Unseen`/`Exact`/`Unknown`) for cache-read
and cache-write in `buzz-agent` turn and session state. Absent field =
Unknown (never zero) through the full pipeline. No `unwrap_or(0)` on the
cache path. Both cache folds are gated on usage-bearing responses (same
gate as the total-state and identity folds) — a response with no usage
at all must not poison either accumulator.
**Overflow-aware input token parsing and accumulation** — closed
end-to-end from parse through wire to ACP:
- `sum_usage()` returns `SumUsageResult` (`Exact(u64)` | `Overflow`) —
checked arithmetic, never clamps. `anthropic_input_tokens()` returns
`Option<SumUsageResult>` since it sums three fields (`input_tokens +
cache_read_input_tokens + cache_creation_input_tokens`) that can
collectively overflow. Single-field callers (`prompt_tokens`,
`completion_tokens`, etc.) convert via `.into_exact()` — their
single-field sums cannot overflow.
- `LlmResponse.input_tokens_overflowed: bool` propagates the parse-layer
signal into the run loop. When set, `input_tokens` is `None` (clamped
value discarded), the context-gate baseline
(`last_request_input_tokens`) is frozen at its prior reading, and
`turn_input_tokens` is poisoned to `TurnIOState::Poisoned` before any
emission — including mid-turn `emit_usage_update` calls. A dedicated
enum on `LlmResponse.input_tokens` would ripple into ~20 existing test
assertions on `r.input_tokens == Some(...)`; the bool flag confines the
change to the two call sites that check it.
- `TurnIOState` (`Unseen`/`Exact`/`Poisoned`) for input and output:
per-round fold uses `checked_add`; overflow poisons permanently at turn
and session level, no healing. Absence does not poison (pass-2-cleared
contract unchanged). Wire emission omits
`accumulatedInputTokens`/`accumulatedOutputTokens` when poisoned — never
null, never `u64::MAX`. ACP treats absent = publisher-poisoned:
`delta_reliable: false`, null turn fields, null cumulative for that
category; session cumulative stays unknown for all subsequent turns once
poisoned.
**Conditional wire emission** for `accumulatedCachedInputTokens` and new
`accumulatedCacheWriteTokens`: fields are omitted when the cumulative is
Unseen or Unknown. ACP `_goose/unstable/session/update` contract
documented next to the payload with tests for all absence/zero variants.
**`PricingIdentity` stamping (publisher-side)**:
- `pricing_authority()`: canonical parsed-URL endpoint comparison
against the official allowlist — HTTPS only, exact allowlisted host
(lookalike-safe), default port (omitted or explicit :443), required API
base path, rejects userinfo/query/fragment/path-prefix lookalikes.
- Model: the actually-requested `request_model` after mesh/auto
resolution (not `effective_model_str`).
- Turn discipline: identity retained only while ALL usage in the current
turn carries one identical proven identity; any mismatch,
unproven-usage-bearing response, or unpaired cumulative snapshot poisons
to absent; a later matching notification does not heal a mixed turn.
**ACP `UsageTracker` identity fold**: per-in-flight-turn tri-state
identity accumulator replacing last-update-wins. Any absent identity on
a token-advancing notification or exact mismatch poisons to absent;
poison survives later updates; reset in `begin_turn()`/`take()`; reset
also when a request fails (baseline cleared so preflight gate cannot
stay frozen sub-threshold on retries).
**M3 migration**: adds `turn_cache_write_tokens`,
`cumulative_cache_write_tokens`, `pricing_authority`, `pricing_model`,
`pricing_cache_class` to `agent_metric_index`. Additive, idempotent,
guarded per-column by marker. M2 migration also guarded per-column (turn
and cumulative cache-read columns checked and added independently;
marker commits only after both are present). Fresh-DB schema includes
all columns.
**First-turn baselines**: `seed_zero_baseline` seeds `last_input:
Some(0)`, `last_output: Some(0)`, `last_cached_input: Some(0)`,
`last_cache_write: Some(0)`, and `last_total: Some(0)` — all have the
known-zero-at-spawn argument. Absent fields from incoming snapshots
still produce unknown (tri-state unchanged). Sessions buzz-acp did not
spawn (no seed) remain fail-closed on turn one.
**`ReportedUsage` TS mirror**: `cacheReadTokens`, `cacheWriteTokens`,
`freshInputTokens` added to `tauriArchive.ts` as `UsageField` members,
field-for-field with the Rust struct.
### P4a — aggregation layer
**Extended S-1 ladder** to cache-read and cache-write via the same
`ladder_token` path as the existing token fields.
**`freshInputTokens` derivation**: checked arithmetic, fail-closed —
absent cache fields produce Unknown (not zero), overflow and
`cacheRead+cacheWrite > input` both produce `incomplete: true`.
Aggregated as a `UsageField`.
**D6 comparator**: `sort_value()` = provider total when known, else
`input+output` when both known, else `None` (unknown-last). Replaces the
prior total-only comparator for both agent-level and model-level sort.
Ships a pinned test vector that the TS render layer (P5) must match.
## Test coverage
- `buzz-agent`: 440 lib + 15 integration (golden_transcripts) — includes
13 new `cache_total_state_tests`; 14 new `turn_io_state_tests`; 3 new
`sum_usage_*` tests (exact single-field, exact two-field, overflow
signals correctly); 3 new `parse_anthropic_*` tests (overflow flag set +
value cleared, normal sum no flag, absent usage no flag); end-to-end
golden transcript drives real subprocess with Anthropic-shaped
`input_tokens: u64::MAX, cache_read: 1` response and asserts
`accumulatedInputTokens` absent from the emitted `usage_update` — no
logic duplication; 3 wire pin tests; 4 `fold_pricing_identity_*` tests;
`pricing_authority()` explicit-:443 acceptance
- `buzz-acp`: 700 tests (691 lib + 9 integration) — 4 new usage tests
(absent input → unreliable+null; absent output → unreliable+null;
goose-shaped both present unchanged; poison mid-session); 3 ACP behavior
tests; 7 pool lifecycle tests
- Desktop (Rust): 2259+ tests — 14 new P4a pinned tests; 2 M3 round-trip
tests; 1 serde key-shape test; 2 M2 partial-schema migration tests;
first-turn cache round-trip test
## Related PRs
- P1 NIP-AM spec: [#4632](https://github.com/block/buzz/pull/4632)
- P3 pricing table: [#4629](https://github.com/block/buzz/pull/4629)
- UI (P5): [#4001](https://github.com/block/buzz/pull/4001)
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
This PR contains the following updates:
| Package | Type | Update | Change |
|---|---|---|---|
| [anyhow](https://redirect.github.com/dtolnay/anyhow) | dependencies |
patch | `1.0.103` → `1.0.104` |
| [anyhow](https://redirect.github.com/dtolnay/anyhow) |
workspace.dependencies | patch | `1.0.103` → `1.0.104` |
---
> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/1) for more information.
---
### Release Notes
<details>
<summary>dtolnay/anyhow (anyhow)</summary>
###
[`v1.0.104`](https://redirect.github.com/dtolnay/anyhow/releases/tag/1.0.104)
[Compare
Source](https://redirect.github.com/dtolnay/anyhow/compare/1.0.103...1.0.104)
- Update `syn` dev-dependency to version 3
</details>
---
### Configuration
📅 **Schedule**: (UTC)
- Branch creation
- Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`)
- Automerge
- At any time (no schedule defined)
🚦 **Automerge**: Enabled.
♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.
👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box
---
This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/block/buzz).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4zLjIiLCJ1cGRhdGVkSW5WZXIiOiI0NC4zLjIiLCJ0YXJnZXRCcmFuY2giOiJtYWluIiwibGFiZWxzIjpbXX0=-->
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
## Buzz Desktop release v0.5.8
- **Frozen main:** `6a17d035f79ad582ca3f4f3cdc38d376f2c4087f`
- **Reviewed candidate:** `f3de860574bb3119018b4592353e9761635aeb07`
- **Previous desktop release:** `desktop-v0.5.7`
- **Proposed immutable tag:** `desktop-v0.5.8`
This PR may be **squash merged** after the Desktop Release Candidate
check and all protected-branch checks pass. Merging authorizes
publication of the exact reviewed candidate; later or unrelated changes
on `main` cannot alter it.
The checked-in changelog accounts for every non-merge commit in the
release range. The Desktop tag points to the reviewed candidate commit,
not the later squash commit. Publication remains bound to that immutable
candidate tag.
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
## Buzz Desktop release v0.5.7
- **Frozen main:** `74b913cff8512c015dc6f1a7473b253fa803f954`
- **Reviewed candidate:** `f167818d25dd9f03115ab907a16f07daee2ece5c`
- **Previous desktop release:** `desktop-v0.5.6`
- **Proposed immutable tag:** `desktop-v0.5.7`
This PR may be **squash merged** after the Desktop Release Candidate
check and all protected-branch checks pass. Merging authorizes
publication of the exact reviewed candidate; later or unrelated changes
on `main` cannot alter it.
The checked-in changelog accounts for every non-merge commit in the
release range. The Desktop tag points to the reviewed candidate commit,
not the later squash commit. Publication remains bound to that immutable
candidate tag.
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
## Summary
- serialize the relay error-message test with all other tests mutating
the process-wide admission gate
- clear its 300-second rate-limit expiry after the assertion
- prevent the paused-time waiter test from observing another test's
state
## Root cause
`relay::tests::oversized_hint_is_capped_in_relay_error_message_string`
arms the process-wide gate for 300 seconds without taking `TEST_SERIAL`
or resetting it. In a parallel test run,
`relay_admission::tests::concurrent_429_extends_the_window_for_parked_waiters`
can observe that expiry, producing the reported `300.001s` instead of
`5s`.
## Validation
- focused admission suite + relay error test repeated 10 times
- pre-push `desktop-tauri-checks` passed, including the full Rust
workspace suite
- `branch-skew` passed
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
## Problem
Two v0.5.6-only regressions were introduced by #4614 (the first enforced
Tauri CSP):
1. **Tab-complete caret regression** — after tab-completing an @mention,
#channel, or :emoji: shortcode, the cursor landed inside the inserted
text instead of after the trailing space. TipTap inserts the correct
text including the trailing space, but without its base stylesheet
(`.ProseMirror { white-space: break-spaces }`) the trailing space
collapses visually and the caret appears mid-name.
2. **Emoji picker unstyled** — the emoji-mart picker rendered as a giant
unstyled layout (oversized search SVG, collapsed grid) because
emoji-mart's shadow-root stylesheet injection was also blocked.
Both symptoms have the same root cause.
## Root Cause
Tauri's build-time asset processor scans `index.html` for inline
`<style>` elements, injects a nonce token, and adds the corresponding
`'nonce-…'` source to `style-src` at runtime. Per the CSP spec, **once a
nonce is present in a directive, the browser ignores `'unsafe-inline'`
for that directive**.
`index.html` contained an inline `<style>` with the boot background
color. When Tauri nonced it and injected `'nonce-…'` into `style-src`,
the intended `style-src 'self' 'unsafe-inline'` became effectively
`style-src 'self' 'nonce-…'` — blocking any runtime stylesheet injection
not covered by a matching nonce:
- TipTap's `injectCSS()` → `createStyleTag()` injecting `.ProseMirror {
white-space: break-spaces; … }`
- emoji-mart's shadow-root `document.createElement('style')` injection
(Inline scripts follow a separate path — they are SHA-256 hashed, not
nonced.)
This only reproduces in packaged builds (where Tauri's custom protocol
serves the HTML and enforces the policy). `tauri dev` loads from the
Vite dev server and is not affected.
## Fix
Move `html { background-color: #000; }` from an inline `<style>` in
`index.html` to `desktop/public/boot.css`, linked via `<link
rel="stylesheet">`. A linked stylesheet is not subject to Tauri's nonce
injection, so `'unsafe-inline'` in `style-src` applies as declared.
The `<link>` is render-blocking (same as the inline style was), so
boot-flash behaviour is identical.
**The production CSP string is unchanged.** This fix makes the policy
apply as intended — no security properties are altered. Will's follow-up
with the security team (Jordan Mecom / Eli Foster, authors of #4614) is
noted for post-ship.
A Tauri-faithful CSP harness for the Vite dev path (so this class of
regression is visible before a packaged build) is tracked as a separate
follow-up.
## Files Changed
- `desktop/index.html` — replace inline `<style>` with `<link
rel="stylesheet" href="/boot.css" />`
- `desktop/public/boot.css` — new file, the extracted `html {
background-color: #000; }` plus rationale comment
- `desktop/src-tauri/tests/csp.rs` — update comment: nonce for styles,
SHA-256 for the boot script
## Testing
- `just desktop-typecheck` ✅
- `just desktop-test` ✅ (4535/4535)
- `just desktop-tauri-test` ✅ (all Rust tests including `csp.rs`)
- Packaged validation: `pnpm tauri build --debug` completed; compiled
binary bakes `style-src 'self' 'unsafe-inline'` with no nonce source
injected ✅
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
## Buzz Desktop release v0.5.6
- **Frozen main:** `78c87ae20e182fffdd99744d6c9ff99df82b159c`
- **Reviewed candidate:** `277d98a5cfb6d3b9af8b75122988f7a7df33ed5d`
- **Previous desktop release:** `desktop-v0.5.5`
- **Proposed immutable tag:** `desktop-v0.5.6`
This PR may be **squash merged** after the Desktop Release Candidate
check and all protected-branch checks pass. Merging authorizes
publication of the exact reviewed candidate; later or unrelated changes
on `main` cannot alter it.
The checked-in changelog accounts for every non-merge commit in the
release range. The Desktop tag points to the reviewed candidate commit,
not the later squash commit. Publication remains bound to that immutable
candidate tag.
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>