## Why
Installing the Codex, Claude, or Goose desktop app does not install the
command-line harness Buzz needs. The current UI makes that distinction
unclear, links some missing-CLI states to adapter documentation, and can
report a successful install from the installer exit code even when
runtime discovery still fails. On Windows, Buzz also invokes Goose's
Bash installer, which writes the executable somewhere Buzz does not
discover.
## What
- distinguish missing vendor CLIs from missing or outdated ACP adapters
in runtime metadata and UI guidance
- link Codex, Claude Code, and Goose missing-CLI states to their
official CLI installation documentation
- explain in Settings, onboarding, and agent configuration that the
desktop app alone is not sufficient
- use Goose's official PowerShell installer on Windows
- refresh PATH and rediscover the requested runtime after installation,
keeping the control retryable if the runtime is still unavailable
- add Rust and Playwright regression coverage for Windows installer
selection, CLI/adapter guidance, false-success prevention, verified
installs, and onboarding copy
## Risk Assessment
Medium. This changes desktop onboarding and runtime installation
behavior. Successful installs now require the runtime catalog to verify
availability; previously hidden discovery failures will surface as
actionable errors instead of a false success state.
## References
- [Codex CLI installation](https://developers.openai.com/codex/cli/)
- [Claude Code
installation](https://code.claude.com/docs/en/getting-started)
- [Goose
installation](https://goose-docs.ai/docs/getting-started/installation/)
- Follow-up to #2563 and #2587
## Validation
- `just desktop-typecheck`
- `just desktop-test` — 3,455 passed
- focused Rust post-install verification tests
- focused Playwright Doctor/onboarding coverage (in progress; CI and
local sequential rerun will provide final results)
Generated with Codex
---------
Signed-off-by: Atish Patel <atish@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Goose <opensource@block.xyz>
Fixes#2637 — full analysis and reproduction there.
## Problem
Audit entries are stamped and hashed with `Utc::now()` (nanoseconds),
then stored in a `TIMESTAMPTZ` column (microseconds). `compute_hash`
covers `created_at.to_rfc3339()`, and chrono emits 0/3/6/**9**
fractional digits depending on the value — so the digest written at
`service.rs:103` is computed over `…T12:00:00.123456789+00:00` while
`verify_chain` recomputes over the `…T12:00:00.123456+00:00` that
Postgres hands back.
Every hash chain backed by a real database therefore fails verification
at its first entry, on untampered data. That is not just a broken
feature — it means a genuinely forged row is indistinguishable from the
permanent baseline failure, so `HashMismatch` carries no signal.
It is invisible in CI because all six chain tests are `#[ignore =
"requires Postgres"]`, and the in-process `hash.rs` tests use a fixture
timestamp of `2026-01-01T00:00:00Z` — zero sub-seconds, the one value
where the bug cannot appear.
## Solution
Reduce `created_at` to the stored precision *before* hashing, so the
in-memory entry and the row are byte-identical:
```rust
pub fn to_storage_precision(created_at: DateTime<Utc>) -> DateTime<Utc> {
created_at.trunc_subsecs(6)
}
```
`log_inner` is the only place that assigns `created_at` — every caller
goes through `NewAuditEntry`, which carries no timestamp — so this is a
single choke point. It is wrapped in a `log_timestamp()` helper purely
so the invariant is assertable without a database.
I chose truncation at the write path over the alternative (hashing a
precision-independent encoding such as
`timestamp_micros().to_be_bytes()`). Both fix the mismatch, but
truncating keeps the existing hash preimage format and gives the
stronger invariant: the `AuditEntry` returned from `log()` is now
exactly what a later read returns.
Truncation matches what actually happens on the wire — sqlx encodes
`DateTime<Utc>` as microseconds since the Postgres epoch, truncating —
so the value hashed is the value stored.
## Validation
Toolchain note: built on Windows with the `x86_64-pc-windows-gnu`
toolchain (no MSVC linker locally).
**Before**, against Postgres 17 with `migrations/*` applied:
```
$ cargo test -p buzz-audit --lib -- --ignored --test-threads=1
test service::tests::chain_links_within_one_community ... FAILED
test service::tests::chains_are_independent_per_community ... FAILED
test service::tests::community_chain_starts_at_seq_1_with_null_prev ... ok
test service::tests::cross_community_row_does_not_verify ... ok
test service::tests::verify_detects_tampering_within_a_community ... FAILED
test service::tests::verify_empty_range_is_false ... ok
test result: FAILED. 3 passed; 3 failed
```
with `HashMismatch { seq: 2 }` / `HashMismatch { seq: 1 }` on untampered
chains.
**After**, same database:
```
test result: ok. 6 passed; 0 failed
```
`verify_detects_tampering_within_a_community` is the one to look at: it
asserts `HashMismatch` lands on the *tampered* entry's `seq`. It was
failing because verification already blew up on an earlier untampered
row — so the assertion proving tamper detection works had never actually
been exercised. It passes now.
Also:
- `cargo test -p buzz-audit --lib` (no Postgres) — 12 passed, 0 failed.
- `cargo clippy -p buzz-audit --all-targets -- -D warnings` — clean.
- `cargo fmt -p buzz-audit -- --check` — clean.
## New tests
Three in `hash.rs`, none needing Postgres:
- `storage_precision_drops_sub_microsecond_digits` — the helper's
contract, and that it is idempotent so a re-read value is unchanged.
- `nanosecond_timestamps_cannot_survive_a_database_round_trip` — asserts
the digests **differ**. This is the trap itself, written down so the
next person changing the hash preimage sees why the precision reduction
is load-bearing.
- `storage_precision_timestamps_survive_a_database_round_trip` — the
invariant the write path must hold.
Plus `log_timestamp_carries_no_sub_microsecond_digits` in `service.rs`,
deliberately **not** `#[ignore]`d, so a regression on the write path is
caught by `just test-unit` instead of only by Postgres-gated tests that
normally never run.
## Compatibility
Rows written before this stay unverifiable — they always were — so there
is no migration. An operator relying on an existing chain has to
re-anchor.
## Relationship to #2620#2620 proposes a shared `verify_entries` walk (anchoring, seq
contiguity, tail-truncation detection) plus a `buzz-admin audit verify`
command. Its Postgres-free unit tests build entries in memory and would
pass regardless, but its `#[ignore]` Postgres tests and the operator
command itself would fail on every real chain until this lands. Worth
taking this first so that work has a verifiable baseline — the two
changes don't overlap in code.
---------
Signed-off-by: Shani Singh <teamdeveloperworld@gmail.com>
## Summary
- Expand the pairing scanner from the Dynamic Island on supported
iPhones
- Reveal the camera behind the pairing UI on Android and standard
iPhones
- Preserve tap-to-dismiss and reduced-motion behavior
## Testing
- `just mobile-check`
- `just mobile-test`
- iOS `RunnerTests`
## Problem
Auth-class errors (expired OAuth token, HTTP 401) are non-retryable: the
token won't self-repair between attempts. Today, `PromptOutcome::Error`
for an application-class error falls into the generic `queue.requeue()`
path, burning up to 10 retry slots over a long backoff window before
dead-lettering. Will's canary run observed the 401 message being retried
repeatedly.
## Solution
Add `is_auth_error()` that classifies `AcpError::AgentError` messages
matching two narrow patterns observed in the field:
- `"Re-authenticate"` — emitted by the Claude CLI for expired OAuth
tokens
- `"API Error: 401"` — present in Claude/Codex HTTP-401 responses
Conservative matching is intentional: a false positive (misclassifying a
transient error as non-retryable) silently drops a user message, which
is worse than a false negative (extra retries on an auth error).
In `handle_prompt_result`, a new branch intercepts the failing batch
before `queue.requeue()` for auth-class errors and dead-letters
immediately, posting a user-visible notice to re-authenticate the CLI
(e.g. `claude /login` or `codex login`).
The transport/application split in `PromptOutcome::Error` is untouched —
this only changes batch fate after an application-class auth error.
## Tests
6 new tests in `error_outcome_emission_tests`:
1. `is_auth_error` matches `Re-authenticate` message
2. `is_auth_error` matches `API Error: 401` message
3. `is_auth_error` rejects other `AgentError` messages (usage credits,
etc.)
4. `is_auth_error` rejects transport errors (I/O, WriteTimeout)
5. Auth error dead-letters immediately — 0 pending channels after
`handle_prompt_result`
6. Non-auth application error still requeued — 1 pending channel after
`handle_prompt_result`
Full `cargo test -p buzz-acp`: 598/598 passing.
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
## Problem
On Windows, `install_shell_command` wraps every install command in Git
Bash `-l -c`, including the Windows-specific `powershell.exe … irm
https://chatgpt.com/codex/install.ps1 | iex` command. A Git Bash login
shell prepends its POSIX dirs (`C:\Program Files\Git\usr\bin`) to PATH,
so when Codex's `install.ps1` shells out to bare `tar -xzf C:\…`, it
resolves Git's GNU tar (`/usr/bin/tar`) instead of Windows bundled
bsdtar. GNU tar parses `C:` as a remote host:
```
tar (child): Cannot connect to C: resolve failed
gzip: stdin: unexpected end of file
/usr/bin/tar: Child returned status 128
/usr/bin/tar: Error is not recoverable: exiting now
Downloaded Codex package archive did not contain the expected package layout.
```
Claude's installer doesn't hit the same failure because it doesn't shell
out to `tar`.
## Solution
On Windows, detect `powershell.exe` install commands and spawn them
natively (`Command::new("powershell.exe")`) instead of routing them
through Git Bash. The discriminator is a case-insensitive prefix check
on the first whitespace-delimited token — minimal and precise.
The native spawn preserves everything `install_shell_command` provides
that applies:
- `NPM_CONFIG_*` / `COREPACK` env strip + managed npm prefix env
- PATH composed from managed Buzz dirs + inherited process PATH (no
POSIX login-shell dirs)
- `CREATE_NO_WINDOW` so no console flash
- stdin null, piped-drain in `run_install_command`
- The retry/backoff/annotate logic is fully shared
The `-Command` body is split correctly at the boundary
(case-insensitive) and passed as a single argument to preserve pipes and
spaces inside the installer script call.
Non-PowerShell commands (e.g. `npm install -g …` adapter steps) continue
through the existing Git Bash path unchanged.
## Tests
6 unit tests:
1. `is_powershell_command` detection (positive + negative)
2. Routing: PowerShell → native spawn on Windows, non-PowerShell → Git
Bash
3. Unix: non-Windows path returns the shell command unchanged
(compile-time cfg)
4. `-Command` body preservation (no bash args in native spawn; body is
single arg)
Full `just desktop-tauri-test` suite: 1627/1627 passing. Windows CI will
validate end-to-end.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
On Windows, the model dropdown never populates for CLI harnesses (Claude
Code, Codex) while the same flow works on macOS.
Model discovery spawns `buzz-acp models` with `PATH` taken from
`login_shell_path()` — which by design always returns `None` on Windows
(Git Bash's POSIX-shaped PATH would poison native children). The
discovery child therefore ran with only the raw inherited process PATH,
missing the Buzz-managed Node/npm directories and exe-parent sidecar
dir, so the ACP adapter's `.cmd` shims failed to resolve `node` and
discovery returned nothing. macOS worked only because a login-shell PATH
exists there.
The fix reuses the existing `augmented_path()` helper (already used by
CLI login probes and auth commands, built on the same
`build_augmented_path` kernel as the real agent spawn), so model
discovery resolves the identical toolchain the agent will actually run
with. `login_shell_path()` remains the login-shell component inside that
composition — on macOS the composed PATH is a superset of the previous
value.
Related: #2661 (managed Node fallback these entries point at), reported
in the Windows install-issues follow-up.
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1fgdl5qqnh3k3f2xkqrvt7cujalhm623x4s7fdjdj5yrtp5fzjl9qrjpucw <4a1bfa0013bc6d14a8d600d8bf6392efefbd2a26ac3c96c9b2a106b0d12297ca@buzz.block.builderlab.xyz>
## Summary
- Preserve plain-string errors returned by Tauri and show an actionable
message when huddle audio is unavailable in the relay deployment.
- Use one error formatter across channel, timeline, wave, and profile
huddle actions while preserving other relay and device errors.
- Complete the huddle lifecycle when audio setup fails after publishing
a start event, preventing peers and reloaded clients from reconstructing
a phantom active huddle.
- Cover unavailable-audio formatting and START → rollback-END
reconstruction with regression tests.
## Behavior
The Tauri huddle commands reject with a plain string. Several desktop
toast call sites only preserved JavaScript `Error` objects, so the relay
message was discarded and replaced with “Failed to join huddle.”
The desktop now recognizes `huddle_audio_unavailable` and the current
relay message, then shows:
> Huddle audio isn’t available on this server. Ask an administrator to
turn it on.
Other relay and device messages remain intact, including microphone
errors.
`start_huddle` also publishes `KIND_HUDDLE_STARTED` before audio setup.
If setup fails, rollback now publishes `KIND_HUDDLE_ENDED` through the
normal end-and-archive path before resetting local state. This makes the
failed start observable to lifecycle reconstruction and prevents stale
join affordances.
## Checks
- `cd desktop && pnpm test` — 3,405 passed
- `cd desktop/src-tauri && cargo test` — 1,560 passed, 13 ignored; 3
diagnostic tests passed
- `cd desktop && pnpm exec playwright test tests/e2e/channels.spec.ts
--project=smoke --grep 'huddle rollback end event'` — passed
- `just desktop-tauri-check`
- `cd desktop && pnpm typecheck`
- `cd desktop && pnpm check`
- Pre-commit and pre-push hooks