mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
kennylopez-mobile-solo-create-speed
3031 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1a2ddf9eaa |
feat(relay): private read-state accessory API (#7906)
Buzz BFF: Private read-state accessory API (`/buzz/v1`) |
||
|
|
46dcc01873 |
fix(acp): keep agent-to-agent thread replies in the thread (#8124)
## Summary When an agent @mentions another agent inside a thread, `resolve_reply_anchor` returned `None`, so the receiving agent's `<context>` had no `--reply-to` instruction. A harness that follows the base prompt literally then posted its reply at the channel top level. Agent↔agent turns in a thread now anchor to the triggering event. The reply stays in the thread and can still nest (the reason #1281 left these turns unanchored). Top-level agent↔agent turns and all human-facing turns are unchanged. Native steering keeps the running turn's `<context>`, so a message steered into a turn anchored to its trigger would be answered under that trigger too. The prompt task now records whether its anchor is the trigger, next to its DM classification (`PromptDmClassification` → `PromptRouting`). The steer guard refuses native steering into a channel thread turn whose anchor is the trigger, or not yet recorded. That message takes the cancel+merge path, whose re-prompt carries its own `<context>`. Behavior change: a same-thread follow-up that arrives before the running turn's prompt is formatted (for example during `session/new`) now cancels and merges instead of steering natively, because the anchor is not known yet. Once the prompt is formatted, follow-ups to human-facing thread turns steer natively as before. ### Related issue Fixes #8122 ### Testing - `cargo test -p buzz-acp -- --test-threads=1` (BUZZ_* env cleared): 1046 passed. `cargo clippy -p buzz-acp --all-targets -D warnings` clean. Pre-push workspace `rust-tests` hook passed. - New regressions, each confirmed to fail when its guard is removed: - `human_follow_up_to_agent_thread_turn_cancels_and_merges`: a real dispatched turn with agent profiles served by the relay fixture. A human message in the same thread is not steered natively. - `test_human_merged_into_agent_thread_turn_anchors_to_root`: the merged re-prompt anchors to the thread root. - `channel_same_thread_follow_up_during_prompt_steers_natively` / `..._during_setup_cancels_and_merges`. - Live local (`buzz-relay` + `buzz-acp` at this head + `buzz-pi-acp`; agent B is a CLI identity with a NIP-OA auth tag): - B @mentions A in a human-rooted thread → A's `<context>` says `--reply-to <B's event>`, and the reply nests under B. On `main` the `<context>` had no reply instruction. - B asks A to `sleep 30` then reply; while A works, the human @mentions A in the same thread → `control signal sent to in-flight task` (cancel+merge), the re-prompt says `--reply-to <root>`, and both replies land at the thread root. --------- Signed-off-by: Leo <a3812943bac59c128e311ca05f83bf91113f179bb6fa9ca96c3d8efe15c330e6@buzz.block.builderlab.xyz> Co-authored-by: Leo <a3812943bac59c128e311ca05f83bf91113f179bb6fa9ca96c3d8efe15c330e6@buzz.block.builderlab.xyz> |
||
|
|
c6dd587e2a |
release: push gateway chart 0.3.4 (#8126)
Publish push gateway chart 0.3.4 through the normal chart release workflow. The chart version and implementation were merged in [#8113](https://github.com/block/buzz/pull/8113); this release PR records the release notes and triggers the immutable chart tag and OCI publisher. Publication is authorized. The pending security review of #8113 is a blocker for pinning the chart in the later infrastructure PR, not for this publication. No gateways or relays are activated. ### Validation The merged implementation passed the complete chart render suite, chart CI, and all 14 local wrapper tests. This release changes only the changelog; chart contents are unchanged from the validated implementation. Published OCI contents and digest will be verified after publication. --------- Signed-off-by: Tom Brow <tomb@block.xyz>push-chart-v0.3.4 |
||
|
|
73a2a05c97 |
feat(push): support platform-managed gateway runtime pods (#8113)
Platform-owned service meshes need runtime pod identity and a replacement network policy without patching rendered gateway manifests. Add runtime-only pod labels, service account and shutdown-grace settings, plus an explicit named replacement-policy mode. Migration isolation remains independent; the default render is unchanged. Chart 0.3.4 publication and live deployment verification follow in the separate release and deployment work. ### Validation Added tests cover runtime-only settings, reserved selector labels, minimum shutdown grace, replacement-policy acknowledgement and conflicting settings. The complete chart render suite and release-contract tests pass. Default rendering matches the current main chart byte-for-byte. Repository-wide `just ci` reached unit tests but failed in existing buzz-acp tests outside this chart change. Live mesh behavior has not been exercised. --------- Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
001290a34d |
chore(release): release Buzz Desktop version 0.5.27 (#8125)
## Buzz Desktop release v0.5.27 - **Frozen main:** `af5bb0af488784c3707e99c87e0675a729711a6e` - **Reviewed candidate:** `7a2fb3cc2d401e91e67d9819741d9025278cbec0` - **Previous desktop release:** `desktop-v0.5.26` - **Proposed immutable tag:** `desktop-v0.5.27` 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: Will Pfleger <wpfleger@block.xyz> Co-authored-by: Release Automation <release-automation@users.noreply.github.com> |
||
|
|
403c19f673 |
fix(desktop): show OpenAI-compatible endpoint settings in the baked-env UI (#8117)
## Summary `is_safe_to_reveal` (`desktop/src-tauri/src/managed_agents/env_vars.rs`) is the single default-deny allowlist for showing baked env values. It lists `DATABRICKS_HOST`/`DATABRICKS_MODEL`/`DATABRICKS_MODEL_FILTER` but not their OpenAI-compatible counterparts, so a build that bakes `OPENAI_COMPAT_BASE_URL`, `OPENAI_COMPAT_MODEL`, `OPENAI_COMPAT_API` shows them as `••••••`. Adds those three non-secret settings to the allowlist: an endpoint URL, a model id, and an `auto|chat|responses` enum. **No secret is revealed: `OPENAI_COMPAT_API_KEY` is deliberately not added and remains masked.** ### Related issue None found. ### Testing - New test `baked_env_openai_compat_endpoint_is_unmasked_but_key_stays_masked` covers the three revealed settings and asserts the API key stays masked; `baked_env_allowlist_is_case_insensitive` extended. - `cargo test --lib baked_env` in `desktop/src-tauri`: 11 passed. `cargo fmt` clean. - Not exercised in the running desktop UI. Signed-off-by: Tom Bedor <tbedor@squareup.com> Co-authored-by: Tom Bedor <tbedor@squareup.com> |
||
|
|
1e2281fd45 |
feat(buzz-agent): send User-Agent on LLM requests (#8116)
## Summary `buzz-agent` builds its LLM `reqwest::Client` without a User-Agent, and reqwest sends none by default, so OpenAI-compatible providers that identify clients by product token see buzz-agent as an anonymous caller. This sets `User-Agent: buzz-agent/<crate version>` on the shared LLM client in `crates/buzz-agent/src/llm.rs`. The version is `CARGO_PKG_VERSION`, the same value `agentInfo` reports over ACP. No request body or endpoint changes. Open point: the crate version is the workspace version (`0.1.0`), so every build reports the same version. A per-build version would need the desktop to pass one through at spawn time; this PR does not do that. ### Related issue None found. ### Testing - The test stub now captures the `User-Agent` request header; new `llm_requests_carry_buzz_agent_user_agent` asserts `buzz-agent/<version>` on a Chat Completions POST. - Verified red without the `.user_agent(...)` line (`left: None`), green with it. - `cargo test -p buzz-agent --lib llm::tests`: 194 passed. `cargo fmt` clean; the one clippy warning is pre-existing (`hints.rs:33`). - Not exercised in a built desktop app. Signed-off-by: Tom Bedor <tbedor@squareup.com> Co-authored-by: Tom Bedor <tbedor@squareup.com> |
||
|
|
af5bb0af48 |
docs(changelog): remove hand-written Unreleased section (#8123)
`CHANGELOG.md` is written entirely by `scripts/desktop_release.py`, but #7692 hand-added a `## [Unreleased]` section at the top. `generate` puts each new release block directly under `# Changelog`, which leaves `## [Unreleased]` sandwiched between the new block and the previous release. `validate` reads a release block up to the next `## v` heading, so it pulls the Unreleased lines into the new block and fails with `changelog block is not deterministic for recorded candidate base`. Every `just release-desktop` run is blocked until the section is gone. This removes the section. Its only entry (#7692, `036215636`) is already in the generated v0.5.27 changelog block, so nothing is lost. Signed-off-by: Will Pfleger <wpfleger@block.xyz> |
||
|
|
9c6fa14aa0 |
feat(desktop): offer Claude effort levels in agent edit and create (#8059)
Claude Code agents now get a thinking-effort dropdown in the agent Edit and Create dialogs, matching the one buzz-agent and Codex agents already have. ## Behavior - **Where levels come from:** Claude effort levels come from the Claude model data for the model that will actually run after Save, resolved in order: explicit model, then inherited default (linked definition or global), then adapter default. Stored Claude session levels are not used, because the stored session data does not record which model produced them. - **Edit:** for an agent linked to a definition, effort follows the definition's model, which stays authoritative. Save never sends a model for linked agents. - **Create:** effort is offered for local agents only and is stored on the instance. Allowed levels are recomputed on every render, so collapsing Advanced and then switching to a model without effort (e.g. Haiku) drops effort from the request. Non-local effort is rejected before any create side effects. - **Persistence:** `create_managed_agent` sets the picked effort when it builds the record and drops stale effort env aliases. Its body runs over any Tauri runtime, so a mock-app test drives the real create path and reads the record back from disk. That test runs in a child process with temp app-data and nest folders, and a test-only switch turns keychain use off so agent keys stay inline in the temp agent file and the OS keychain is never touched. - **Observer store:** live session-config captures are written to the store before the dialog refreshes; archived captures are read-only. - **Codex:** its existing session-native effort picker is unchanged. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz> |
||
|
|
d0fd96fca6 |
BUZZ-175: Route serving event writes through the tenant lock (#7828)
## Summary
Route every serving event write through one community-admitted
transaction entry point. `Db::begin_event_write_transaction(community)`
is now the only `Db` constructor for event-write transactions: it takes
the shared community admission lock before returning, so callers that
use it cannot take domain or row locks ahead of tenant admission, and a
quiescing community rejects the write at entry. Database triggers and
commit-time fences remain the authoritative backstop.
This migrates normal, replaceable, roster, relay membership, reaction,
reminder, admin-delete, push-owned, artifact-accept, huddle-join
(48101), workflow-deletion, and command-executor event writes. Coupled
mention writes stay in the same transaction; pool-level
`insert_mentions` now opens its own admitted transaction.
**Review fixes (since `950d5f6`):**
- The unguarded event-write constructor is gone. Removed with it: the
deprecated `begin_transaction` alias, the test-only replica-floor
constructor, and `begin_community_write_transaction` (now identical to
the single constructor). Artifact accept, the huddle join, and both
workflow-deletion paths now admit before any domain or row lock;
`persist_command_event` no longer hand-rolls `guard_transaction`;
read-only `query_artifacts` no longer opens an event-write transaction.
- `insert_channel_head_checked` is now `insert_canvas_head_checked`: it
rejects non-canvas kinds before opening a transaction and always takes
the coordinate lock.
- Source policy now also scans `buzz-relay`: any function that hands a
transaction to an event-write helper must have opened it through the
admitted constructor. The scan drops each `#[cfg(test)]` item instead of
stopping at the first one: it tracks brace depth and ends the item on a
line ending in `;` or `}` (ignoring a trailing `//` comment, even after
a string that contains `//`), with a column-0 `}` as fallback. Fixtures
pin a unit struct, a multi-line static, a `const { … }` block, a struct
with fields, a module, and one-line items with trailing comments,
including after `"https://…"` and `"ws://…"` strings; a raw opener after
each must still be flagged. Known limit: braces inside string or char
literals or comments are still counted, and only `//` trailing comments
are recognized, so a test-only item with unbalanced braces in a literal
or comment, or with a trailing `/* */`, would hide what follows; none
exists today. The scan checks that admission is present; the per-path
PostgreSQL tests pin that it comes before domain locks.
- `just test-unit` (and the non-nextest fallback in
`scripts/run-tests.sh`) now runs `buzz-db --test observability_source`.
Before this, no CI job ran these source-policy tests: the unit lane ran
`buzz-db --lib` only and the PostgreSQL lane runs only ignored tests.
- Renamed or removed since `950d5f6`: `begin_transaction`,
`begin_community_write_transaction`, and the replica-floor test
constructor are gone; `insert_channel_head_checked` →
`insert_canvas_head_checked`; test helper `quiesce_community_for_tests`
now delegates to `main`'s `set_deletion_state`, which reads the real
`deletion_fence_generation` instead of hard-coding `'0'`;
`runtime::insert_mentions` is pinned to the admitted constructor by
`pool_level_insert_mentions_opens_the_tenant_local_chokepoint`.
**What got simpler:** four transaction constructors collapse to one, and
the hand-rolled admission guard is deleted. The compiler rejects callers
of the removed constructors, but a transaction opened from `Db::pool()`
can still reach the public `*_in_transaction` helpers; only the
source-policy scan and the database fences catch that until BUZZ-254.
**Behavior note:** #7932 made `insert_event` skip the transaction for
kinds that are not listener mentions. Every serving event write now has
to run in a transaction that holds the community admission lock, so
`insert_event` always opens the admitted transaction again. `main`'s
`soft_delete_event_and_update_thread` / `_in_tx` split and its
removal-marker guard are kept; the outer function now opens the admitted
transaction.
### Related issue
[BUZZ-175](https://linear.app/squareup/issue/BUZZ-175). Caller-migration
stage following #7706 (merged); rebased onto `main` at `5173fad66`.
Follow-up: [BUZZ-254](https://linear.app/squareup/issue/BUZZ-254) makes
event-write helpers require a type-level admitted transaction.
**Known debt, out of scope:** `create_channel`, `update_channel`, and
`insert_thread_metadata` write channel and thread rows (not events)
outside the admitted constructor.
### Testing
At `efe4a94623dd69973780a3c39f16dd972cf7cb76` on a cloud workstation:
fmt; `buzz-db` clippy (all targets, `-D warnings`);
`observability_source` 14/14; `just --dry-run test-unit` includes the
new step. Reverting just the end-of-item check to an earlier rule makes
a trailing-comment fixture fail.
At `eaf190ae59d1595741c3d19dc4c23ce492b5f4d4` (the commit after it only
changes this test file, `Justfile`, and `scripts/run-tests.sh`): full
GitHub CI green; on the workstation, clippy for `buzz-db` and
`buzz-relay`, ordinary `buzz-db` 154/154, `just test-unit` green, and
the PostgreSQL lane 805 of 806 (exception below). An end-to-end run
against a local relay and the built desktop app covered artifact accept,
huddle join, and workflow deletion for both a healthy and a quiescing
community, plus p-tag reaction rollback and replay of the same signed
event.
New tests: one fenced-community test per migrated path (artifact accept,
huddle join, both workflow deletions) puts the community into
`quiescing`, holds the lock the path takes next, and asserts rejection
at entry with nothing stored; a DB-free test pins the non-canvas
rejection.
Exceptions:
-
`runtime::postgres_tests::writer_pool_rejects_non_read_committed_database_default`
hangs on that workstation's local Postgres 16 at both this head and an
earlier `main` base, `f0eb5575f` (2/2 runs each). The test, `Db::new`,
and the pool setup are unchanged by this PR, and the test passes in
GitHub CI. Live traces put the hang in the client, not the database: the
1 s acquire timeout stops firing after sqlx's `after_connect` rejection
path under tokio's `current_thread` runtime. No database lock waits were
observed.
- Two `observability_source` assertions were already failing on `main`,
unnoticed because no CI job ran that test binary: a stale
`fetch_optional` pin after #8005, and lease-seam attribution that now
lives in the `bounded_serving_lease_sql` helper. Both are corrected
here, and the binary now runs in `just test-unit`.
Generated with Codex
---------
Signed-off-by: tornquist <tornquist@squareup.com>
Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
|
||
|
|
9322289364 |
feat(mobile): follow the portable mention rules, including DMs (#8102)
🤖 ## Summary The mobile app's @mention chooser now follows the same rules as the desktop app (buzz-app). The same typed text finds the same people in the same order on both apps, and sending works the same way. - **Same matching and order.** Mobile uses the shared mention rules and runs their shared test cases. A multi-word search such as `@Mary J` still searches the community directory, so it can find "Mary Jane" even when she is not in the channel. - **DMs can mention people outside the DM.** The chooser marks them "not in DM". Nobody can be added to a DM, so the message sends without a prompt. Outside people go out as references, so they are not notified. This matches buzz-app #510. - **Channels still ask.** In a stream or forum, mentioning someone outside the channel asks "Invite" or "Do nothing" before the message sends. A channel with no type tag counts as a stream. - **Sessions do not search the directory.** Only session members appear. - **A failed directory search stays visible.** The chooser shows the error and a Retry button. Before, it closed. - **Search results stay in their community.** If you switch communities while a search runs, its people and its error do not show in the new community. The search starts again there. - **A failed send check keeps the draft.** Before a message with a mention sends, the app checks who is in the channel. If that check fails, the message stays in the compose bar and a notice offers Retry. Before, nothing was sent and nothing was shown. - **Invite stays in its community.** If you switch communities while the "Invite" question is open, Invite adds nobody. ### Screenshots The real compose bar widget, rendered by Flutter's test renderer with the app's fonts and theme. The data is sample data. These are not device captures. | DM: outside person is "not in DM" | Stream: outside person is "not in channel" | |---|---| | <img width="320" alt="DM chooser" src="https://github.com/user-attachments/assets/0e884518-2386-4cd1-9f19-16661aaa194e" /> | <img width="320" alt="Stream chooser" src="https://github.com/user-attachments/assets/5395e4e1-032e-459b-98c6-0126c060c4d9" /> | | Stream: send asks first | Session: members only | Search failed: Retry | |---|---|---| | <img width="260" alt="Stream ask on send" src="https://github.com/user-attachments/assets/2b988257-3c6a-445d-acfa-fca47b9752fe" /> | <img width="260" alt="Session chooser" src="https://github.com/user-attachments/assets/e193551f-f5bf-41d9-aa29-bc8665c643f9" /> | <img width="260" alt="Search failed with Retry" src="https://github.com/user-attachments/assets/e8c4b9bb-067e-4fe9-9fdd-4e5691bc66be" /> | ### Details - `mobile/lib/shared/mentions/mention_rules.dart` holds the rules: query syntax, match tiers, sort order, Space selection of an exact unique name, stable rows while results arrive, and `p` and `mention` tags. A `p` tag notifies the person. A `mention` tag is a reference and does not notify. - `mobile/test/shared/mentions/mention-rules.fixtures.json` is an exact copy of the buzz-app fixture file. `mention_rules_test.dart` runs every case. - Known difference: desktop gives the directory to a channel with an unknown explicit type (not stream, forum, DM or session). Mobile does not. No such type exists today. ### Related issue None found. Desktop counterpart: block/buzz-app#510. #7513 also edits `send_message_provider.dart` and may conflict. ### Testing `just mobile-install mobile-check mobile-test` passes: analyze clean, 2998 tests pass on `49c44653` (before rebase). The pre-push hook ran `just mobile-check && just mobile-test` again on the rebased head `de1ca080` and passed. Each review fix was mutation-checked: removing the fix makes its new test fail. --------- Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
166d383ca7 |
feat(mobile): read web app catch-up marks for unread state (#8103)
🤖 ## Summary The web and desktop app (`buzz-app`) saves a "caught up" mark when you scroll to the bottom of a channel and stay there, and another one for a thread. Mobile did not understand these marks. It relied on the separate per-message read marks that `buzz-app` also kept. `buzz-app` is about to stop keeping those per-message marks under the "caught up" mark (block/buzz-app#590), to save space in the synced read state. With this change, mobile reads the "caught up" marks with the same rules as `buzz-app`: - `activity:<channel>` reads ordinary top-level messages up to its time. It does not read thread replies, mentions, broadcasts or DM messages, because reaching the bottom of the channel does not prove you saw those. - `thread-activity:<root>` reads the replies in that thread up to its time. So a message you read in `buzz-app` stays read on mobile: in the app badge, the channel list unread state, the "new messages" divider when you open a channel, and the Mark read / Mark unread action. Mobile does not write these marks. It only reads them. ### Related issue None found. Companion to block/buzz-app#590. The old desktop app in this repo is intentionally not changed. ### Testing - New badge tests: `activity:` clears an ordinary top-level message, and does not clear a mention, a thread reply or a DM. `thread-activity:` clears replies in its own thread only. The two "clears" tests fail without this change. - New tests for the per-message read check and the "ordinary message" rule. - `flutter analyze`, `dart format` check and the full mobile test suite pass locally. - Not yet exercised on a device. --------- Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
1e67395fd1 |
docs(identity): tighten enterprise adapter contract (#8069)
Tightens `docs/enterprise-identity-adapter.md` to define the enterprise adapter contract implemented by kgoose in [cash-server#132770](https://github.com/squareup/cash-server/pull/132770) and the Buzz Desktop client in [buzz-app#562](https://github.com/block/buzz-app/pull/562). - Requires canonical `wss://host[:port]` relay URLs with a lowercase scheme, the lowercase ASCII (A-label) host form with no trailing dot, the default port omitted, and no trailing slash, path, query, or fragment. Adapters MUST configure relays in canonical form, compare `relay_url` against that exact form, and reject unknown relays with 403 `authorization_denied`. - Requires NIP-98 proof freshness of at most 60 seconds old and at most 5 seconds in the future, with the proof bound to the endpoint, method, and exact request body. - Rejects content-encoded request bodies with 400 `invalid_request`. - Caps assertions at `exp - iat <= 300` seconds and the adapter session expiry, requires `typ` `nip-fi+jwt`, and validates the echoed `nostr_pubkey`. - Defines refusal and retry handling: Clients MUST retry 429 and 503 with bounded backoff whatever the `error` code. Clients MUST treat any other status, a 400/401/403/413 response with a contract-undefined code, or a rejected `200` response including one it cannot parse, as a refusal: keep the session, show it as refused, and not retry automatically. Network failures retry with bounded backoff. This tightens the enterprise adapter contract restored in [#8064](https://github.com/block/buzz/pull/8064). --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Alia <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz>mobile-v0.20.0-rc.1 |
||
|
|
c9143ed8f7 |
chore(push): release gateway chart 0.3.3 (#8107)
Prepares publication of the Buzz push gateway chart 0.3.3, including the optional migration Pod annotations already merged in #8105. This PR adds release notes only; chart templates, values, and schema are unchanged from the reviewed implementation. Merging the `push-chart-release/0.3.3` branch triggers the normal release workflow: create `push-chart-v0.3.3`, validate the chart, then publish `oci://ghcr.io/block/buzz/charts/buzz-push-gateway:0.3.3`. Keep this PR in draft until publication is explicitly authorized. It does not deploy a gateway. ### Validation Focused migration annotation renders, release contract checks, and local chart packaging pass. The merged implementation passed chart CI and Jude's automated review in #8105. The earlier repository-wide local `just ci` run failed in unchanged ACP tests; it was not rerun for release notes. The registry manifest digest can only be verified after publication. --------- Signed-off-by: Tom Brow <tomb@block.xyz>push-chart-v0.3.3 |
||
|
|
fcf5f044b3 |
fix(desktop): use object-scale-down for inline markdown images (#4686)
Fixes #4384. A dim-less markdown image with no cached decode gets `DEFAULT_IMAGE_RESERVE = { height: 256, width: 384 }` and `useFixedReserveBox: true` in `resolveImageReserveBox`. The frame is that big on first view, and the inline `<img>` uses `object-contain` which scales UP to fill the frame — so a 46×20 shields badge renders as a giant blurry block. The same message renders at the natural size on the second view because `rememberDecodedImageDimensions` populates the cache, but the inconsistency between first and subsequent view is the bug. Switches `object-contain` → `object-scale-down` on `ProgressiveImage.tsx`'s shared `IMAGE_CLASS`. Both `object-contain` and `object-scale-down` scale down to fit, but only `object-scale-down` also refuses to scale up — a small image now sits centered in the reserve box on first view, never upscaled. The layout-shift guarantee for large images is preserved (they still scale down to fit the reserve), matching the issue's stated constraint. Chose option 2 ("Alternatively/minimally") from the issue over option 1 because: - It's a one-class-name change with zero new state, zero new effects, and zero risk to the existing layout-shift guarantee that the surrounding `useFrozenImageReserve` is explicitly designed to preserve. - Option 1 (collapse the frame on decode) is the issue author's preferred direction but requires deciding whether the layout shift is acceptable *shrink*-only — a direction question that should be confirmed with the maintainer first. The issue explicitly says "Happy to open a PR for option 1 if the direction is acceptable" — that's the sign-off step, not this PR. If maintainers want the full first-view-equals-second-view behavior, option 1 is a follow-up that builds on this base. Not added: a regression test. The current desktop test infra (`desktop/src/shared/ui/markdown/markdown.test.mjs` and siblings) doesn't cover `ProgressiveImage` directly; a DOM-style snapshot would need a new test harness. The change is one CSS class name; the visual regression is plain to eyeball in the screenshots CI already captures. Verified: - `pnpm check` (biome + file-sizes + px-text + pubkey-truncation) — clean - `npx tsc --noEmit` — clean Signed-off-by: kushaim <carlossilvajimenez@gmail.com> Co-authored-by: kushaim <carlossilvajimenez@gmail.com> |
||
|
|
964b556a42 |
fix(desktop): detect Kimi Code in its default install path (#3700)
## Summary By default on linux kimi is installed at `~/.kimi-code/bin/kimi` and this was not detected automatically. This now fixes it by adding this path to the detection ### Related issue N/A ### Testing Before it did not appear / did not detect. Now it does :) Signed-off-by: benthecarman <benthecarman@live.com> |
||
|
|
0a070dfd3f |
fix(acp): steer mid-turn DM follow-ups natively again (#8058)
🤖 ## Summary Some agents can take a new message into a turn that is already running. This is called "native steering": the agent keeps working and folds the new message in, instead of stopping and starting over. #6132 made this safer in channels: a message is steered in only when it belongs where the running turn will reply. In a channel, each top-level message gets its own reply thread, so a second top-level message must not be steered into a turn that replies in the first one's thread. That rule also ran in DMs, where it does not fit. In a DM, a top-level message is answered at the top of the conversation, not in a new thread. So every top-level DM message replies in the same place. But the rule treated each one as its own thread. As a result, **every DM follow-up sent while the agent was working cancelled and restarted the turn**, instead of being steered in as it was before #6132. Now: - **DMs:** a follow-up is steered natively when it replies in the same place as the running turn: both top-level, or both in the same thread. A thread reply during a top-level DM turn (or the reverse) still takes the cancel-and-restart path, because the running turn would reply in the wrong place. - **Channels:** unchanged from #6132. ## Details - The queue now records each running turn's reply route: its thread root (or none) and its reply thread. One function, `ReplyRoute::accepts_steer`, decides whether a new message may be steered in. It compares thread roots in DMs and reply threads in channels. - The DM rule mirrors how the prompt is built: a top-level DM's `<context>` has no `--reply-to` target, and a threaded DM's target is in its thread. - The steer decision must agree with how the running turn's prompt was written. The prompt task records whether it writes the prompt as a DM, and the steer decision reads that recorded value. So if channel metadata was missing when the turn started (the prompt then uses the normal channel form) and loads later, the DM rule is not applied to that turn. - The prompt task records this value as soon as channel metadata resolves, before session setup. A follow-up sent during a slow `session/new` is steered in, as on main. When an `initial_message` setup prompt runs first, the value is recorded after that setup turn, because the setup prompt reads the same steer mailbox. - Before the value is recorded, the steer decision uses the listener's own channel classification. A channel the listener knows is not a DM is always written in channel form, so the channel rule applies at once, exactly as on main. A DM, or a channel whose type is not resolved, takes the cancel-and-restart path until the value is recorded. ### Related issue Follow-up to #6132 (merged). Found in review after merge. No separate issue. ### Testing - New tests drive DM messages through the listener's real steer decision with a running turn. A second top-level DM message is steered natively with no cancel. A DM thread reply during a top-level DM turn is not steered and takes cancel-and-restart. - Direct tests of the steer rule cover channel and DM cases: same thread, different thread, top-level after top-level, and top-level mixed with thread replies. - With the DM rule disabled, the DM rule test and the top-level DM listener test fail. - Listener tests also cover a DM turn whose prompt was written as a channel (top-level follow-up is not steered) and a DM follow-up that arrives before the value is recorded (not steered). - Production-path tests dispatch a real turn through `dispatch_pending` and `run_prompt_task` to an agent that holds at `session/new` or `session/prompt`, then send a follow-up through the real listener. A top-level DM follow-up during the prompt or during `session/new` is steered with no cancel. A DM follow-up before an `initial_message` setup turn is not steered. A same-thread channel follow-up during setup is steered with no cancel. - Mutation checks: each fails its test. They are: not recording the value in `run_prompt_task`, passing a detached value from dispatch, not recording it before session setup, recording it before an `initial_message` turn, and removing the known-channel rule for the unrecorded case. - `cargo test -p buzz-acp` passes on the rebased head, with the timing-sensitive `acp::tests` run alone (`--test-threads=1`; this PR does not change them, and they fail only under full parallel load on this laptop). `cargo clippy -p buzz-acp --tests -D warnings` and `just desktop-tauri-clippy` are clean. --------- Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
fd885b5530 |
feat(push): support migration pod annotations (#8105)
Allow operators to configure migration Pod annotations independently of runtime Pod annotations. This lets clusters with automatic sidecar injection opt the migration Job out without changing the gateway Deployment or Helm hook metadata. The new string-map setting defaults to empty. Chart version 0.3.3 is prepared here; publication requires a separate approved chart release PR. ### Validation The full chart render suite and focused migration annotation tests pass. Tests verify unchanged default resources and hooks, independent runtime annotations, and rejection of non-string annotation values. Independent agent review found no blockers. Repository-wide `just ci` exited 1 in unchanged `buzz-acp` tests: idle/stdout keepalive timing and steering assertions (one failure in the first group, four in the second). Subsequent recipes were not reached. No rerun or unrelated code fix was attempted. Human testing remains pending. Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
5173fad660 |
feat(pairing): enter desktop codes on mobile (#8085)
## Summary Enter a desktop-only, random six-digit code on mobile to pair without a desktop confirmation click. Verification uses the encrypted session, limits guesses to five, and never treats the QR-derived transcript as approval. Older clients retain desktop confirmation. Adds digit-only error shakes and native haptics, a separate biometrics choice, and a direct community-icon arrival animation. Fixes dropped early offers and resets completed pairing so reopening or signing out cannot get stuck. ### Related issue Related: #8060; no duplicate code-entry PR found. <img width="780" height="1688" alt="01-enter-code" src="https://github.com/user-attachments/assets/a99ced3e-4305-4ac6-a6cc-d598ced423e7" /> <img width="780" height="1688" alt="02-incorrect-code" src="https://github.com/user-attachments/assets/19cdb109-1f10-40a9-a661-1d7ba89c81c1" /> <img width="780" height="1688" alt="03-biometrics" src="https://github.com/user-attachments/assets/af1b2924-9ab6-4d1a-b4b1-fccfdf446404" /> <img width="780" height="1688" alt="04-community-loading" src="https://github.com/user-attachments/assets/84145d82-843a-4c25-aef7-6492fc241d4b" /> <img width="897" height="388" alt="05-desktop-code" src="https://github.com/user-attachments/assets/d8a2e469-7e42-466a-988f-bab6b1df85fc" /> ### Release order Deploy the updated pairing relay first, then desktop, then mobile. The relay now allows eight events per connection (seven for a successful fifth guess plus room for cancellation), with a corresponding bounded delivery budget. Existing clients remain supported; older client combinations keep desktop confirmation. ### Testing - 103 mobile pairing tests, 270 core tests, 51 relay integration tests, and the new real-relay fifth-guess flow passed. - The real-relay regression verifies proof, decrypted identity import, and completion after four wrong guesses. Restoring the old six-event cap makes it fail. - A paused-clock desktop regression confirms slow readiness cannot extend the visible QR beyond protocol expiry. Mobile regression covers typing before negotiation completes. - Core/pair-relay Clippy and full native desktop Clippy/workspace tests passed. Mobile analysis passed; broad mobile runs each hit an unrelated failure (a gateway-script timeout and a native-menu missing-plugin error), and both affected test files passed separately. - Repository-wide Rust testing exhausted local disk space while compiling unrelated packages. The final push omitted that resource-blocked hook and avoided repeating the desktop/mobile runs above; hosted CI remains required. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
7f6ffd5ee5 |
fix(db): enumerate only active communities for maintenance (#8061)
Fixes #7558. Supersedes #7630. The SQL predicate here comes from #7630 by @olympusbuildz, who is credited as co-author on the commit. This PR keeps that fix unchanged and adds the lifecycle fixtures and worker-path coverage the fix needed. ## Problem `community_hosts_with_operation` ran `SELECT id, host FROM communities` with no filter. After a whole-community deletion, the `communities` row stays behind as a `tombstone` until retention expires, which is by design. But the NIP-43 maintenance sweep kept handing that community to `publish_nip43_membership_list`. The community write fence rejected the write, so the relay logged `NIP-43 membership reconciliation failed` about every 60s, indefinitely, for every deleted community. ## Change The helper is split by caller purpose: - `active_community_hosts` (maintenance) and `bootstrap_community_hosts` filter to `archived_at IS NULL AND deleted_at IS NULL AND deletion_state = 'active'`. That's the same active-community predicate already used in `community.rs` and `deletion.rs`. - `usage_community_hosts` still enumerates every row, exactly as on main. The startup large-roster reconciliation query (`list_large_channel_rosters_needing_reconciliation`) joined `communities` without any lifecycle filter, so it gets the same active predicate on its `JOIN communities`. Tombstone rows aren't modified. ### Caller impact | Caller | Enumeration | Effect | |---|---|---| | NIP-43 maintenance sweep (`side_effects.rs`) | active | Stops trying fenced writes and logging warnings forever. This is the bug fix. | | NIP-43 bootstrap reconciliation | active | Same as maintenance, at startup. | | Push-wake worker (`push_runtime.rs`) | active | Stops claiming wakes for these communities. Archived communities are unreachable through host resolution; quiescing, fenced and tombstone writes are DB-fenced. Wakes already queued for an archived community aren't delivered late after unarchive, because claim and send both re-check `expires_at`. | | Startup large-roster reconciliation (`side_effects.rs`, kind 39002) | active | Stops rewriting roster snapshots for archived communities and attempting fenced writes for deleting or tombstoned ones. | | Usage-metrics host map (`main.rs`) | all rows | Unchanged. Per-community labels, fleet totals and storage attribution keep covering the same rows as the unfiltered usage count queries. | ## Tests #7630's test tombstoned a row with a plain `UPDATE`. On current main, the `enforce_community_tombstone` trigger from migration 0029 rejects that unless the transaction carries the deletion-executor GUCs. The fixtures here set those GUCs the way the executor does. - `buzz-db` `store::usage::postgres_tests::test_community_hosts_excludes_archived_and_deleted_communities` sets up active, archived, quiescing, fenced and tombstoned fixtures. Across two sweeps it checks that `active_community_hosts` (maintenance) and `bootstrap_community_hosts` both return only the active community, and that `usage_community_hosts` still returns all five. It also checks that the tombstone row still exists with `deleted_at` set. - `buzz-relay` `handlers::side_effects::tests::postgres_tests::maintenance_sweep_skips_archived_and_deleted_communities` is the worker-path regression. It runs `reconcile_nip43_membership_snapshots_with_purpose(Maintenance)` three times against the same five fixtures. It checks that `buzz_nip43_membership_reconciliation_failures_total` stays at 0, the active community gets exactly one membership snapshot, the four non-active communities get no maintenance write, and the tombstone row is still there. - `buzz-db` `store::channel_members::postgres_tests::large_roster_reconciliation_skips_archived_and_deleted_communities` seeds a channel whose roster has diverged from its live relay-authored kind-39002 snapshot in each of the five fixture communities. Only the active community's channel may be returned. All three tests are `#[ignore = "requires Postgres"]` inside `postgres_tests::`, so the PostgreSQL CI lane picks them up through its existing structural filter. ## What got simpler The background writers share one definition of "community eligible for maintenance": the same active predicate the rest of the store uses. Usage metrics keep their existing semantics, so this PR changes no metric. One enumeration helper is added (`active_community_hosts`), and the old `community_hosts_with_operation` indirection is gone. The executor-GUC lifecycle fixture lives once in `test_support` and is shared by both DB tests instead of being duplicated. --------- Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Olympusbuildz <Olympus.roots@outlook.com> |
||
|
|
8746bfebff |
Polish iOS header glass and scroll-edge blur (#8097)
Wrap channel and DM titles in native glass and remove the temporary-channel clock. Apply a soft status-area fade across iPhone navigation, and fade the large title with scroll progress and fade in the centered title, with Reduce Motion support. Validation: mobile formatting/analysis and the full mobile suite passed. Native iOS 26/27 simulator checks covered actual text opacity, header resizing, quick reversal, and restored heading position; signed iPhone build, installation, and launch succeeded. `just ci` was blocked by disk exhaustion during Rust compilation. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
46e60206a4 |
fix(mobile): fill and stabilize content-aware loading placeholders (#8083)
Fill mobile loading views across the screen and use known image, video, voice-note, and file shapes in message placeholders. Freeze each loading cycle’s layout so arriving metadata cannot reshuffle the shimmer just before content appears; preserve cached Home and forum content during reconnects. Validation: mobile analysis and full test suite passed (2,752 passed, 4 skipped), plus 3 unconfigured-push tests and file-size checks. Installed and launched the loading changes on Pixel 10 before the latest main merge. Full `just ci` was stopped during desktop checks due to low disk space. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
f0eb5575ff |
fix(mobile): polish photo carousels and video viewer controls (#8079)
## Summary - Keep neighboring carousel photos visible, add page-change haptics, and reuse appearance-style dots with native iOS glass and grouped actions. - Refine full-screen video controls with fading transport, cycling speeds, pinch-to-zoom, and double-tap reset before swipe-to-dismiss. ### Related issue N/A. Checked recent open PRs; repository issue search was unavailable. ### Testing - Mobile analysis/format checks, file-size guards, and full Flutter suite passed (2,680 tests; 4 skipped). - Built, installed, and manually verified on the connected iPhone. - `just ci` was attempted but blocked fetching a Rust dependency because this Mac could not connect to GitHub. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: klopez4212 <klopez4212@gmail.com> |
||
|
|
5f2d801b8f |
fix(mobile): align Android conversation headers and reactions (#8082)
## Summary Align Android channel and DM headers with iOS: centered, tappable titles with presence or member counts and no avatars. Keep conversation navigation frosted through keyboard and scroll changes, and run the typing shimmer left to right. Add an Android reaction details sheet with All/emoji tabs, swipe navigation, and compact rows aligned to the tab gutters. Native iOS reaction presentation is unchanged. ### Related issue None found. ### Testing - Mobile analysis, formatting, file-size checks, and full mobile tests passed. - Android debug build installed and launched on Pixel 10. - Full `just ci` could not complete: disk space exhausted while fetching an unrelated Rust dependency. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
75a4efd0ca |
fix(mobile): unify sheet sections and bottom spacing (#8081)
## Summary Use rounded, grouped rows consistently across mobile sheets and menus, including Members and list pickers. Keep sheet content above the system gesture area with 4dp of extra bottom padding on Android and iOS. ### Related issue No duplicate found; related member-picker work: #4051. ### Testing - Mobile analysis, formatting, file-size checks, and full mobile test suite pass. - Android build installed and launched on Pixel 10. - Before/after Members previews use sample data (attached below). - Full `just ci` stopped during Rust compilation because local disk space ran out. Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
03f24a69bf |
feat(mobile): add iOS Liquid Glass tabs and quick actions (#8080)
## Summary Use native Liquid Glass for the iOS tab bar and expanding + menu, preserving the existing Lucide icons, icon-only layout, badges, accessibility labels, and tab interactions. The + uses black-tinted glass with white controls; the selected tab has a subtle theme accent (6% blended into the existing selection color). Android retains its current styling. ### Related issue No duplicate issue found. Related prior navigation work: #7281 (closed). This change covers the tab bar and quick actions; it does not add page-to-page navigation-bar morphing. ### Testing - `just mobile-check` passed (formatting, analysis, and file-size checks). - `just mobile-test` passed: 2,678 tests passed, 4 skipped; the separate unconfigured-push suite also passed all 3 tests. - Widget coverage verifies native glass parameters, black + tint, original icons, icon-only layout, tab selection, badges, and quick-menu open/close behavior. - Signed Release iOS build installed and launched on a physical iPhone; the final glass and tint styling was reviewed on device. No final-app screenshot was captured. - Full `just ci` was attempted but stopped during desktop compilation as available disk space dropped to 3.5 GiB. The complete repository-wide gate is not locally verified; hosted CI remains pending. Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
d2da104d94 |
feat(mobile): add animated community switcher (#8076)
## Summary Replace the community switch sheet with a two-column picker. Selection enlarges the avatar, waits with the voice-note loader for destination channel content and layout to settle, then follows a curved path into the header. Cache raster icons at a maximum 512-pixel size and accept only resource-free emoji SVGs. Cancel timed-out or disposed downloads without delaying content readiness. Animate Edit badges in place, and open Add Community on the pairing welcome page. Community removal lives only in the switcher’s Edit mode. Keep the flow shared across Android/iOS, with native iOS controls and removal confirmation, and clear navigation backgrounds at rest. ### Related issue Related to #8070 (community icon refresh/caching). ### Testing - 122 focused Flutter regression tests and seven native iOS tests passed after merging the latest navigation changes from `main`. Earlier feature build installed and launched on a physical iPhone. - Full mobile pre-push checks passed; regression coverage includes gradient rendering, rejected/oversized icons, bounded persisted raster decoding, SVG resource rejection and cache migration, timeout/disposal cancellation, disconnected destination channels, degraded unread-history settlement, and switching while icons refresh. Full `just ci` stopped during Rust checks for low disk space. Hosted CI remains required. - [Grid and Edit previews](https://github.com/block/buzz/pull/8076#issuecomment-5977838957). --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
05d1714912 |
fix(mobile): manage channel members from profile sheets (#8078)
Keep member administration reachable when small channels omit “See all.” Tapping a member now opens profile-sheet rows to make a channel admin, change an admin to member, or remove the person with confirmation, using the existing grouped list style. Actions follow the live roster, respect channel permissions, prevent duplicate submissions, and surface failures. Existing guests can become members or admins; owners, self, archived channels, and DMs do not expose these controls. Validation: mobile analysis, formatting, file-size checks, and the full mobile suite passed. Release iOS build installed and launched on a physical iPhone. Full repository `just ci` was previously stopped for low disk space during desktop compilation. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
a14107ab2e |
fix(mobile): refine iOS navigation and conversation details (#8075)
Fix iOS navigation title taps and truncation, theme-aware blur (including chats opened at the newest message), and the search focus animation with a plain, correctly spaced Cancel button. Simplify DM details and header actions, move profile descriptions below names, and remove redundant Settings/Add Community titles and unnecessary “See all” links. Validation: mobile analysis, formatting, file-size checks, and the full mobile test suite passed in pre-push hooks; Release iOS build installed and launched on a physical iPhone. Full `just ci` was stopped during unrelated desktop compilation due to low disk space. Native XCTest additions have not been run locally. Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
e982f70fba |
docs: restore enterprise identity adapter contract with relay assertion (#8064)
🤖 Restores `docs/enterprise-identity-adapter.md`, the vendor-neutral HTTP contract between Buzz Desktop and an enterprise identity adapter, and adds the request that turns an adapter session into a NIP-FI relay assertion. The login, code exchange, session check, transport, and privacy sections are byte-for-byte the text from the head of #7715 (`4ef08e23`). Buzz Desktop's enterprise login client ([buzz-app#525](https://github.com/block/buzz-app/pull/525)) already calls those routes. The new **Relay assertion** section defines `POST /v1/identity/assertions`: - **Request:** a JSON body with `relay_url` and `nostr_pubkey`. The adapter session goes in `Authorization: Bearer`, and a NIP-98 proof signed by that key, bound to the endpoint URL, method, and exact body hash, goes in `Nostr-Authorization`. Clients and adapters must not log either header's value. - **Response:** `assertion`, `nostr_pubkey`, and `expires_at`. Under this contract an assertion lives at most 5 minutes and never outlives the adapter session; that cap is the adapter contract's rule, not a relay constant. The assertion's `typ` and `aud` rules point to NIP-FI. The assertion key must be the key that signs the NIP-42 relay login. - **Denials:** a table of status, error code, meaning, and the client action for each. A missing session is 401 `session_required`; missing or repeated proof headers, a repeated `Authorization` header, multiple session credential kinds, malformed bodies, and unknown fields are 400 `invalid_request`. 401s clear the session and return to login; 403 `authorization_denied` keeps the session and disconnects from that relay without automatic retry; other 403s, 400, and 413 keep the session and show the error without retry; 429, 503, and network failures retry with bounded backoff. Adapters may return `session_required` for any invalid, expired, or revoked credential, and clients handle both 401 codes identically. Agent keys are out of scope and need a separate design. Related: [cash-server#132756](https://github.com/squareup/cash-server/pull/132756) implements this contract in kgoose. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
6cf4582002 |
Use native iOS navigation bars with large titles and avatars (#8052)
## Summary Use UIKit navigation bars across iOS pages, with large titles that collapse into the toolbar on scroll and a light native blur. Home uses the active community name as its title and shows community and profile avatars with centered emoji and stable loading, without flashing back to icons. Channel headers retain the member count, tap-to-settings title, and Huddle action. Their ultra-thin native material has a soft lower edge and stays visible at the timeline bottom. The hidden title-layout scroll view has its automatic edge effects disabled so it cannot add a second backdrop. | Expanded Home | Scrolled Home | Settings | | --- | --- | --- | | <img src="https://raw.githubusercontent.com/block/buzz/47a77d3ff1d30fc74f7488c1a4427e41bbb85c4e/pr-8052--home.png" alt="home" width="240"> | <img src="https://raw.githubusercontent.com/block/buzz/47a77d3ff1d30fc74f7488c1a4427e41bbb85c4e/pr-8052--scrolled.png" alt="scrolled" width="240"> | <img src="https://raw.githubusercontent.com/block/buzz/47a77d3ff1d30fc74f7488c1a4427e41bbb85c4e/pr-8052--settings.png" alt="settings" width="240"> | Channel title, member count, and Huddle control: <img src="https://raw.githubusercontent.com/block/buzz/47a77d3ff1d30fc74f7488c1a4427e41bbb85c4e/pr-8052--channel.png" alt="Channel navigation" width="280"> ### Related issue No matching open issue found. ### Testing - Added causal regressions for Profile/Edit Photo native titles, metrics-only collapse changes, and native-header suppression throughout channel/thread reaction and action backdrops. Each regression fails when its corresponding fix is removed. - Mobile formatting and analysis passed. Full mobile test rerun passed: 2,622 tests, 4 skipped, plus 3 push-disabled build tests. - Native iPhone Air regression passed: repeated title expansion/collapse and scroll material behavior. - Navigation and blur were accepted on the connected iPhone using the existing profile. Simulator snapshots use real community data. The subsequent review fixes have widget coverage; fresh physical-device verification of Profile states, Dynamic Type changes, and message backdrops is pending. - Full repository `just ci` was blocked by local disk exhaustion while compiling Rust dependencies. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
33f54de2dd |
feat(mobile): organize community and profile settings (#8056)
## Summary Move Invite, Appearance, and Remove community into a **Community settings** section in the community-avatar sheet, above the existing switcher. Keep the sheet untitled and use circle-minus for removal. Personal Settings groups **Set status** and **Copy public key (npub)** above an untitled card containing **Display name**, **Profile description**, and **Edit photo**. Remove the leading icons from status, public key, and Send identity to desktop; keep the latter in its own untitled card. Remove the avatar status badge and top Edit action, and rename the Theme page to **Appearance**. Opening the presence chip triggers selection haptics; copying plays native iOS success feedback and confirms with “Public key (npub) copied.” ### Testing - Mobile formatting, analysis, and file-size checks passed. - 111 affected widget tests passed; all 48 settings tests passed again after the final labels and icon changes. - Final release build installed and launched on a physical iPhone in the separate Buzz Community preview, retaining its existing test account. - Full repository `just ci` was blocked by local disk exhaustion during Rust compilation. The full mobile suite passed (2,609 passed, four skipped), plus three push-unconfigured tests. The presence and copy feedback passed focused widget tests and analysis, including native iOS success-channel coverage. ### Snapshots Light and dark Flutter widget renders with fixture data. Images are GitHub attachments, not committed to the repository. The personal-settings snapshots precede the final “(npub)” label clarification. | Surface | Light | Dark | | --- | --- | --- | | Community sheet | <img src="https://github.com/user-attachments/assets/d52a192e-bd40-4263-ac2d-05d589c7ebf2" width="280" alt="Community sheet, light"> | <img src="https://github.com/user-attachments/assets/8756ebe9-b6d4-4920-8fb7-1cb7ee8ef778" width="280" alt="Community sheet, dark"> | | Personal settings | <img src="https://github.com/user-attachments/assets/962b98bd-e4cf-4cd1-8948-69b349390ceb" width="280" alt="Personal settings, light"> | <img src="https://github.com/user-attachments/assets/223bb0a8-e10e-43b1-83ea-e0955af4627a" width="280" alt="Personal settings, dark"> | | Appearance | <img src="https://github.com/user-attachments/assets/334ba797-55cb-47ce-b677-0befafb7659b" width="280" alt="Appearance, light"> | <img src="https://github.com/user-attachments/assets/79b62503-d476-4dbc-8c75-8cee0ee37e98" width="280" alt="Appearance, dark"> | --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
8af2d91f37 |
refactor(relay): bind HTTP tenants through one shadow-aware helper (#8062)
Adds `bind_tenant()`, a single helper that resolves the community from the request `Host` and records the NIP-FI shadow verdict when binding fails. Ten protected HTTP handlers previously duplicated the Host lookup and called `observe_unbound` in their own failure branches, so each one had to remember the observer call. Moving those ten handlers onto the helper makes recording the verdict part of binding itself, and `observe_unbound` becomes private to the shadow module. Each caller keeps its existing rejection response, so Off/Enforce behavior is unchanged. `bind_community()` stays directly callable. Routes exempt from NIP-FI (invite claim, webhooks, NIP-05 and NIP-11 metadata) keep using it, and the WebSocket and audio upgrades record their own verdicts before they bind. A new protected HTTP handler should bind through `bind_tenant()`. 🤖 Authored by Duncan (agent) on behalf of Will. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
df3f28a2c4 |
feat(relay): add NIP-FI shadow mode for HTTP, WebSocket and admin (#8034)
Stack: this PR → #8062 🤖 Follows #8028 (merged). Adds `BUZZ_NIP_FI_MODE=shadow`. The relay loads and validates the full enforce configuration (issuers, communities, lifetimes, command issuers), evaluates NIP-FI evidence wherever enforce would decide, and records what enforce would have done, while every request, upgrade and session behaves exactly as in `off`. The one approved exception is admin disconnect: shadow verifies the command and returns `200`, where Off has no verifier and returns `503`. It lets an operator measure the would-deny rate, including Hosts missing from `BUZZ_NIP_FI_COMMUNITIES`, before switching to `enforce`. - **Mode predicates.** `NipFiMode` gains `is_off`, `restricts`, `evaluates`, `enforces`, `denies_unconditionally` and `observes_only`, built on two exhaustive `match` bases (`restricts`, `evaluates`); `is_off` is a `matches!` and the rest derive from the bases, so a new mode cannot silently fall into one side. Every call site uses them, including the Git membership-lookup failure from #8005, which uses `restricts()` so shadow keeps Off's body. A test scans each crate's `src/` and fails on any `NipFiMode::<variant>` in production code outside the predicate definitions and the env parser, which catches `==`, `matches!` on any number of lines, `if let` and or-patterns. - **Startup.** Shadow goes through enforce's path, so every enforce startup refusal applies, including the required connection lifetime. Missing-setting errors name the configured mode through one formatter (`mode_requires`); the enforce text is byte-identical. Shadow builds the assertion verifier and claims command replays under its own Redis prefix, both through the production `AppState::new`. - **HTTP and Blossom.** `admit_nip_fi_http` keeps Off's result in shadow and additionally replays enforce in enforce's order: first the router guard's steps (community, then assertion), then the handler's steps (NIP-98, pairing, deny set) on the same NIP-98 proof, through `evaluate_enforce_steps`, which enforce shares. A Host mapped in config but missing from the communities table, with a valid assertion, records an admit, because enforce's guard passes and its 404 is not a NIP-FI denial. A dev-mode `X-Pubkey` identity is marked unsigned and is never an enforce proof, so shadow records it as a `nip98` would-deny. On bridge and Blossom routes, the strict proof check (payload tag, Blossom `Strict`) runs as a separate pure check that consumes no replay entry and records on its own series, so it never counts as an admission verdict. Shadow records exactly one verdict for every identity, community or credential refusal enforce would make, with Off's status, body and challenge unchanged: bridge, workflows, Blossom, Git transport and settings, GIF search and share, and invite minting record the `community` would-deny when the Host is unmapped. Git's missing or malformed credentials still reject before any database work, and shadow records the enforce verdict for that failed proof. The router guard stays transparent in shadow. - **WebSocket, root and audio.** The upgrade check evaluates the attached assertion once. An upgrade enforce would refuse records that would-deny; otherwise the assertion goes into a shadow-only session object and the upgrade returns as in Off. The assertion never reaches the connection, the identity registries, the admission gate, terminal frames or cancellation. Each session records at most one admission verdict. **Pairing** is recorded on both ingresses right after a valid NIP-42 proof, before ordinary relay policy, where enforce pairs: a key mismatch or claimless assertion is a `pairing` denial. **The deny-set check** runs where enforce runs it, after registration: a hit is a `deny_set` denial, and a clean read keeps the session pending and registered. **The admit** is recorded only when the connection is actually admitted: on root at the AUTH commit, after ordinary policy; on audio when the participant join transaction commits, after relay membership, channel membership, the final ban and membership decision, mesh routing and the join's own checks, and before publication. A NIP-42 failure, which comes before pairing, and any refusal between pairing and the admit (ordinary policy, mesh, or a join refusal inside the join transaction) retires the observer when the refusal is decided, before any refusal frame, rollback or cleanup is awaited, so it leaves no record even if the deadline passes during that cleanup. A record-only deadline task, armed at upgrade with enforce's `compute_session_deadline`, records `expired` when enforce would have closed an admitted session. If the deadline passes while the session is still pending, that is its single admission verdict, a denial with `stage=deadline`, and a later AUTH records nothing. A deny for the key (admin or cross-pod) records `revoked` on an admitted session; if it arrives after the deny-set check but before the admit, including a deny refused for capacity, the session keeps a pending-revocation mark and records the admit followed by exactly one `revoked` end, matching enforce's phase-independent close, and the deadline cannot take that end. The phase, the mark and retirement change together under one session lock. Each session records at most one end. The session holds the socket's cancellation token for recording only: once the socket is cancelled, no transition records anything, even while connection references remain (a cancel fence; the token never feeds a close action). Upgrade would-denies carry the ingress route, `ws` or `audio`. - **Admin disconnect.** Shadow verifies the command and records the deny entry, so later admissions record would-denies, but closes no session; the response stays `{"disconnected": true}`. Shadow publishes cross-pod on its own channel, `buzz:nip-fi:shadow-disconnect`, which only shadow pods subscribe to, so no enforce pod (including older builds that know only `buzz:nip-fi:disconnect`) acts on a shadow command. Shadow pods also keep listening on `buzz:nip-fi:disconnect`, so their deny record includes enforce's real disconnects. Shadow claims command replays under `buzz:nip-fi:shadow-command:{hash}`: replays are still rejected within shadow, and a shadow accept never uses up the enforce claim. Enforce and Off keep the same channel, keys and close behavior. - **NIP-11.** Shadow serves the same document as enforce, including `limitation.federated_identity`, because clients only attach evidence to a relay that advertises it. ## Recording | Series | Labels | One increment per | |---|---|---| | `buzz_nip_fi_shadow_total` | `route` (`http`, `ws`, `audio`), `stage`, `outcome`, `community` | decision enforce would make: an HTTP admission, a refused upgrade, a session's NIP-FI AUTH decision, or its deadline passing before AUTH (`stage=deadline`) | | `buzz_nip_fi_shadow_strict_proof_total` | `route` (`bridge`, `blossom`), `outcome` (`pass`, `rejected`), `community` | strict NIP-98 side check | | `buzz_nip_fi_shadow_session_end_total` | `route` (`ws`, `audio`), `reason` (`expired`, `revoked`), `community` | admitted session enforce would have ended (admitted sessions only) | | `buzz_nip_fi_shadow_disconnect_total` | `route` (`admin`, `cross_pod`), `outcome` | disconnect-path event, in place of the real disconnect, lag, capacity and poison counters | `community` is the configured canonical URI or `unmapped`, never the raw `Host`; an unmapped or empty Host records `stage=community, outcome=unavailable`. A shadow pod never moves an enforce disconnect or failure counter. Strict-proof rates use their own `pass + rejected` total, and session-end rates divide by the `admit` count for the same route. There is no per-verdict `info` log; a would-deny writes a `debug` line with no token, claim or issuer. The pre-existing enforce `debug` line for the proven NIP-98 key is shared and also fires in shadow. Admin commands rejected for a bad header, body or proof, a replay, or a dependency error record no shadow verdict; only capacity rejection is counted. ## Not observed - Handler-side path-validation exits: an invalid Git owner or repo name on default-branch, a media path that is not a valid hash, and a Git pack URL with a query string record nothing from the handler. The router guard still records what enforce would refuse at the guard (for example a missing assertion), because enforce refuses those requests before it validates the path. - A refusal that is not a NIP-FI decision, such as a tenant-binding 404 behind a passing guard, or an ordinary WebSocket refusal after pairing (a ban, membership or join refusal before admission). ## Follow-ups - A bounded `route` label per HTTP surface (bridge, Blossom, Git, …) on `buzz_nip_fi_shadow_total`; today every HTTP verdict carries `route=http`. - One shared helper for the ten copied `observe_unbound` recording hooks, stacked on this PR. The shadow recorder and session suites and the root pairing witness run in `just test-unit` and the `scripts/run-tests.sh` fallback; the `AppState::new` shadow witness, which exercises the command verifier against real Redis, runs in the `external_infra_redis` lane. NIP-FI env tests recover a poisoned env lock so one panic cannot cascade. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <wpfleger@block.xyz> Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Signed-off-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz> |
||
|
|
fe9e2409a0 |
BUZZ-175: Add application-owned writer lock foundations (#7706)
## Summary Buzz is moving database invariants from triggers and foreign keys into application-owned transaction protocols. This PR adds the first transition foundations while preserving all current database backstops. - Add a shared community-write lock for single-community writer transactions, and make deletion lifecycle transitions take the matching exclusive lock. A supported serving transaction never spans communities. - Add shared event-writer and exclusive replica-probe lock ordering around the existing heartbeat publication model. - Document the supported writer contract, reviewed backfill path, and transition gates. - Keep one replica-floor publication concept and remove an unrelated reconciliation-script edit found during review. This narrows the change rather than adding parallel protocols. The blast radius is limited to new opt-in writer helpers plus lock acquisition in deletion and replica probing. Existing trigger and GUC enforcement remains authoritative, so callers can migrate in later pull requests. ### Related issue [BUZZ-175](https://linear.app/squareup/issue/BUZZ-175). This is the first implementation slice from the OSC migration plan. ### Testing No manual testing. CI covers the Rust checks and database tests added here. Generated with [Codex](https://openai.com/codex) --------- Signed-off-by: tornquist <tornquist@squareup.com> Co-authored-by: Codex <noreply@openai.com> |
||
|
|
ea143ea64b | feat(mobile): native iOS message menus and reaction sheets (#8055) | ||
|
|
5a4b2d3dcc |
fix(desktop): show readable Claude Code model names and descriptions (#8053)
Claude Code agents' model dropdown showed raw IDs (`opus[1m]`,
`claude-fable-5-1[1m]`, `haiku`) and its effort choices showed
`low`/`xhigh`. Claude Code's ACP adapter labels config options and
choices with `name`, but Buzz read `displayName`, which no adapter
sends.
**Adapter labels and descriptions**
- `normalize_agent_models` reads `name`, keeps each model's
`description`, and uses the model option's `currentValue` as the agent's
default model (ahead of the unstable `currentModelId`).
- `parse_config_options` / `parse_option_values` read `name` for option
and choice labels, so effort shows "Low"/"Xhigh".
- The create (`PersonaModelCombobox`) and edit (`PersonaDropdownField`)
model pickers show the description as a muted second line, through both
option builders (`modelDropdownOptions` and
`buildTemplateModelDropdownOptions`).
**Generated names for full Claude model IDs**
- Claude Code's own names drop versions ("Sonnet" for
`claude-sonnet-5`). The desktop's Databricks label grammar already
parses a bare Claude ID once its wrapper is removed, so that body parse
is exposed as `generateBareModelLabel`; Databricks entry points and
outputs are unchanged.
- For the `claude` runtime, `claudeCodeModelLabel` formats full IDs and
keeps a `[1m]` suffix as "(1M context)". Short names (`opus`, `haiku`,
`opus[1m]`), unknown bracket suffixes, and IDs containing any non-ASCII
character keep the adapter's name.
- This applies to the model dropdowns in the create and edit dialogs and
the Agent defaults editor. The "Default model (…)" row appears in the
edit dialog and the Agent defaults editor; the create dialog still
requires an explicit model and shows no Default row. Model labels on
agent cards are unchanged.
- In the edit dialog, the Default row keeps the discovered label (e.g.
"Default model (Opus)") unless a global default model is set, in which
case it shows that model. When the adapter's default is its own
`default` entry (e.g. "Default (recommended)"), the row reads plain
"Default model" and keeps that entry's description.
**`models` debug command**
- The harness's readable `models` output reads option and choice labels
from `name` and the option id from `id` (falling back to `configId`), so
it no longer prints raw IDs for Claude Code. JSON output is unchanged.
| Model ID | Before | After |
|---|---|---|
| `opus[1m]` | `opus[1m]` | Opus · Opus with 1M context · … |
| `claude-fable-5-1[1m]` | `claude-fable-5-1[1m]` | Claude Fable 5.1 (1M
context) · Fable 5.1 · … |
| `claude-sonnet-5` | `claude-sonnet-5` | Claude Sonnet 5 · Sonnet 5 · …
|
| `claude-sonnet-4-6` | `claude-sonnet-4-6` | Claude Sonnet 4.6 ·
Previous Sonnet version |
| `claude-sonnet-4-6[1m]` | `claude-sonnet-4-6[1m]` | Claude Sonnet 4.6
(1M context) · Sonnet 4.6 for long sessions · … |
| `haiku` | `haiku` | Haiku · Haiku 4.5 · … |
🤖
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
|
||
|
|
cc528dbe05 |
feat(agents): request summarized Claude thinking and label empty rows (#8051)
Claude Code agents on Opus showed Thinking rows that opened to nothing.
Opus returns no readable thinking unless Claude Code is asked for
summaries, and Buzz never asked. Sonnet returns readable thinking either
way.
**Request summaries from Claude Code.** `session_new_full` now adds
`_meta.claudeCode.options.extraArgs: {"thinking-display": "summarized"}`
for the Claude adapter. claude-agent-acp spreads `extraArgs` into the
CLI's arguments; 0.36.1, 0.60.0 and 0.66.0 all read the same shape. The
value is merged into the existing `_meta`, so `systemPrompt` and
`sessionTitle` are kept. `session_new_full` is the only place Buzz sends
`session/new`, so agent sessions and model discovery both get it.
**Only for a CLI that accepts the flag.** `--thinking-display` first
shipped in Claude Code 2.1.94, and older CLIs exit on unknown flags,
which would fail every session start. When the adapter is spawned, Buzz
runs `$CLAUDE_CODE_EXECUTABLE --version` once with a 5-second limit,
reading at most 256 bytes of output and killing its whole process group
on timeout, and sends the flag only on 2.1.94 or newer. The flag is left
off, and sessions start exactly as before, when:
- the CLI is older than 2.1.94;
- `CLAUDE_CODE_EXECUTABLE` is unset (the adapter then runs its own
bundled CLI, which Buzz can't inspect) or empty;
- the binary hangs, fails, or prints anything other than a clean
`major.minor.patch` version;
- `BUZZ_ACP_LAUNCH_PREFIX` is set, since the wrapper can change which
CLI the adapter actually runs.
The result is logged once at debug level.
**Label rows that still have no text.** A Thinking row whose text is
empty or only whitespace keeps its header and shows a muted "No readable
reasoning tokens", with nothing to expand. This covers models or CLIs
that still return no readable thinking. If text arrives later in the
same row, it renders as an expandable section as before.
`ThoughtActivity` is the only component that draws Thinking rows.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz>
|
||
|
|
133fb98692 |
fix(acp): steer running turns with edited mentions (#6132)
🤖 ## Summary Some agents can take a new message into a turn that is already running. This is called "native steering." Before this PR, an edit that arrived during a turn always cancelled the turn and started it again. Now the edit is steered into the running turn, like any other new message. The steered edit still points at the original message and its thread, so the agent replies in the right place. - The steer text carries the edit's route: `Edit of: …`, plus the original's thread root. - **Steer only within the same reply thread.** A native steer keeps the running turn's `<context>`, so the reply goes where that `<context>` points. The queue now records each running turn's reply thread. A message is steered natively only when it replies in that same thread. Under channel sessions one session spans threads, so an edit for another thread (or another top-level message) takes the cancel-and-restart path. Its re-prompt carries the message's own full `<context>`, with channel, project, and profile data. The same rule applies to ordinary steered messages, which had the same gap before this PR. - **Per-thread sessions:** the edit steers only a turn running in the same session as its original message. An edit in thread A is never added to a turn running in thread B. If no turn is running in that session, the edit waits in the queue as its own item. - **Channel sessions** (the default): the channel has one session. A running turn receives the edit natively only when the edit replies in that turn's thread. - If native steering is not available, or the transport fails or the agent rejects the steer, the existing cancel-and-restart fallback runs. If the agent returns another error, or the turn has already finished, the edit goes back to the queue for normal delivery. In every case the queued edit keeps its route. ## Details - #4741 looks up the original message once, before the edit is queued. This PR passes that stored route to the existing steer path, and uses the same session the edit was queued under. No new lookup or preparation step is needed. - Removal, re-add, and fallback all use the existing lifecycle for withheld steers. ### Stack **3 of 3**. #6131 is merged; this PR is now based on `main`. ### Related issue Part of #2540 (fixed by #6131). This PR adds the running-turn behavior. ### Testing - `cargo test -p buzz-acp --all-targets` passes. The one test that fails when the git setting `nostr.keyfile` is set is described in #4741. - Tests check that the steer text carries the original's route, and that the listener gives the steer path the same route and session it queues. - End-to-end tests send a routed edit through the listener's steer decision under channel sessions. An edit in the running turn's thread, and an edit of the running top-level message, steer natively with no cancel. An edit for thread B during a thread-A turn, and an edit of another top-level message, send no native steer and take cancel+merge. Removing the same-thread check makes both cross-thread tests fail. --------- Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
4f51b9e101 |
fix(mcp): support Goose discovery handshake (#8037)
## Summary Goose 1.52 starts MCP connections with `server/discover`. `buzz-dev-mcp` rejects that request with `ExpectedInitializeRequest`, preventing Goose from loading its Buzz tools. Upgrade the tool server to rmcp 3.4.1 and adopt the renamed SDK types. Subprocess tests exercise discovery and legacy initialization, tool listing, file reads/edits, invalid arguments, connection survival, and EOF shutdown. Include the server in both Rust unit-test runners and make the cancellation regression approve its shell call before testing cancellation. The native agent retains its independent rmcp 1.x client. Cargo also unifies the existing 3.x dependency used by the mesh smoke harness. ## Validation - Normal `git push` passed all hooks, including Rust tests, Tauri checks, and repository policy checks. - `just ci` passed on the final stack: repository checks, Rust and Tauri tests/lint, desktop tests/build, web build, and mobile checks/tests. - MCP: 78 existing tests and two subprocess lifecycle tests passed; the discovery regression fails with the old SDK. - All 54 native-agent regressions passed on the final stack, including cancellation of a real MCP shell process. - Previously exercised unmodified Goose 1.52 and 1.47 with Sandpit and the real tool server. Allowed workspace operations and permitted network traffic succeeded; disallowed file and network operations were denied. - Live staging desktop check with Goose 1.52: the reply appeared in Buzz, an allowed request returned HTTP 200, and a disallowed request returned CONNECT 403. ## Stack 1. **#8042 → `main`**: deterministic handoff regression. 2. **This PR → #8042**: Goose MCP discovery compatibility. Merge the lower PR first, then retarget this PR to `main`. This diff contains only the compatibility change and its tests; the relay build repair is already on `main` in #8036. --------- Signed-off-by: Alex Rosenzweig <arosenzweig@squareup.com> |
||
|
|
2c270cf9d7 |
test(agent): synchronize handoff steering with tool approval (#8042)
## Summary `handoff_cap_binds_within_a_single_turn` can finish the turn before its steer is accepted. It also requests `test_tool`, which the fake MCP server does not expose, so the supposed tool round fails immediately. Call the real `cap_test__tool_0` fixture and hold its permission response until the steer is acknowledged. Assert that the next model request contains the steer and the handoff cap still prevents a second summary. This changes one regression test; production code is unchanged. ## Validation - Reproduced the exact CI failure on unchanged `main` with a temporary 250 ms reader delay; the repaired test passed with the same delay. The delay is not committed. - Temporarily reset the handoff counter when a steer is queued: the repaired test failed. Restored production code afterward. - All six handoff regressions passed: `cargo test -p buzz-agent --locked --test regressions handoff_`. - `just ci` passed on the combined stack: repository checks, Rust and Tauri tests/lint, desktop tests/build, web build, and mobile checks/tests. - All 54 native-agent regressions passed on the combined stack, including this test. ## Stack 1. **This PR → `main`**: deterministic handoff regression. 2. **#8037 → this branch**: Goose MCP discovery compatibility. Merge this PR first, then retarget #8037 to `main`. Signed-off-by: Alex Rosenzweig <arosenzweig@squareup.com> |
||
|
|
448407a972 |
test(relay): configure a community in the Git ban-lookup fixture (#8039)
🤖 Follows #8036 (merged). The Postgres test `failed_restriction_lookup_is_503_not_403` in `crates/buzz-relay/src/api/git/transport.rs` sets `nip_fi.mode = Enforce` but configured no NIP-FI communities. Since #8028, enforce mode refuses that request at Host-to-community lookup with the same `503 authorization unavailable` the test expects, so the enforce half never reached the ban lookup it is meant to guard. Deleting the `unavailable` mapping on the ban-lookup error in `GitAuth::from_request_parts` left the test passing. The fixture now binds a community with `test_support::any_host`, the same way #8028 fixed the other enforce fixtures in this file. With the mapping deleted, the enforce half now fails. Found by @loganj's review comment on #8036. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
47654215eb |
fix(acp): wake agents for mentions added by edits (#6131)
🤖 ## Summary Today an agent does not wake up when someone edits an existing message to @mention it. With this PR, adding the mention in an edit is enough to reach the agent. The agent then works on the original message, as #4741 set up. - Agents now receive message edits by default. Before, they subscribed only to new messages (and a few other event types). - The desktop app puts a mention tag on an edit only for people whom that edit newly mentions. So an agent wakes once, for the new mention. It does not wake again when the same message is edited later. This PR also fixes one case in the desktop app: when the original also mentioned someone whose profile was not loaded, every mention counted as new, so a typo fix woke the agent again. Now the recipients that the app knows from the original always count as already notified. - This default applies in normal mode, to channels the agent joins while it is running, and in setup mode. - If you give an explicit `--kinds` list, it is used without change. - Sessions do not change in this PR. An edit uses the session that #4741 chooses: with per-thread sessions, the original message's thread; with channel sessions, the channel. ## Details - The change adds the edit event kind (40003) to one shared default list. The startup rules, the relay subscription filters, and the setup rules all use that list. - An agent accepts an event only if both the relay subscription and its local rules allow it. The normal listener's startup rules now come from one function, `startup_subscription_rules`. The harness and the tests both call it, so the tests check the rules that actually run. ### Stack **2 of 3**, based on #4741. Next: #6132. ### Related issue Fixes #2540. ### Testing - `cargo test -p buzz-acp --all-targets` passes. The one test that fails when the git setting `nostr.keyfile` is set is described in #4741. - Admission tests run on the rules the harness installs, for the normal listener and for setup mode. An edit that mentions the agent is accepted. An edit that does not mention it is rejected. - I removed kind 40003 from each of the two defaults, one at a time. Each time, these tests failed. - Config tests check that edits are in the default kinds for startup channels and for channels joined later. - Desktop regression: `@Agent @Missing User please chek` edited to `check`, with only the agent's profile loaded, sends no new notification. It fails on the old code. --------- Signed-off-by: npub1em3jmyn4vu57urqf03txrwreccvejvwdy5c4er8nnrwt7rc4tncscs3ssu <cee32d92756729ee0c097c5661b879c6199931cd25315c8cf398dcbf0f155cf1@buzz.block.builderlab.xyz> Signed-off-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz> Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-authored-by: npub1em3jmyn4vu57urqf03txrwreccvejvwdy5c4er8nnrwt7rc4tncscs3ssu <cee32d92756729ee0c097c5661b879c6199931cd25315c8cf398dcbf0f155cf1@buzz.block.builderlab.xyz> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
e4681e44a7 |
fix(acp): wake quiet hosts for respawns and queued retries (#7459)
## Problem With `--no-presence --no-typing --heartbeat-interval 0`, a crashed ACP transport can leave work queued indefinitely. Background respawn results were only drained before `select!`; neither their arrival nor queue retry eligibility woke the host. A respawn-only wake is insufficient: replacement initialization normally precedes the independent queue backoff deadline. ## Change - Select on respawn completion, reusing the existing return-to-pool and dispatch paths. - Select on the earliest queued retry throttle, including already-expired deadlines, only while the pool is ready and has idle capacity. - Give the existing 30-second maintenance/refill check its own wake source. Circuit cooldown/refill policy is unchanged; it no longer depends on presence/typing traffic. - Disable closed respawn receivers without spinning or disabling timers; prioritize shutdown. - Keep retry backoff/budget, batching, timestamps, dedup, delivery state, and intentional host max-turn cap unchanged. Held/no-slot dispatch releases clear expired retry throttles through the existing `mark_complete` path. No reconnect/gateway changes or adapter retry engine. Based directly on main, independent of other ACP plumbing work. Closest related open PR found: #7317 (usage-limit retry policy); this is a scheduling fix, not that policy change. ## Validation - `cargo test -p buzz-acp`: 920 library tests + 9 integration tests pass. - Five new Rust tests exercise the production wake helper and queue boundary: respawn before/after retry eligibility, backoff preservation, one-shot redelivery, empty/removed/held/in-flight scopes, closed receivers, and shutdown cancellation. - `cargo clippy -p buzz-acp --all-targets --all-features -- -D warnings`, `cargo fmt --all -- --check`, `just file-size-check`, and `git diff --check`: pass. - Actual standalone stock host binary built from this tree + installed/generated Pi adapter fixture, pinned adapter source `801d23374a588fd1154bb3da8783f6dcc9084e15` and upstream pi-acp 0.0.33. Loopback-only synthetic relay/provider; no live agent/config changes. - All optional presence/typing/heartbeat wakes disabled; 2.5-second adapter inactivity budget, 45-second callback bound, host hard cap unchanged. - Fast replacement: initialization 1.347s after requeue, retry eligible at 4.447s. Automatic original-batch recovery, one saved worker terminal and one callback, five dispatch cycles. - Delayed replacement: initialization 9.118s after requeue, retry eligible at 4.157s. Same recovery invariants, four dispatch cycles. - Both use exactly one relay input; both settle within 2.84s of the later of replacement/eligibility (including a 1.5s duplicate-observation window). This also rejects waiting for the 30s maintenance tick. - Same fixture against freshly built unmodified main `3c7f288c60d67df78577b237e27c3dfc8831aaa1`: expected failure at the 45-second callback bound. Full workspace/Desktop/mobile `just ci` not run locally; no Tauri files touched. Relevant ACP package checks above are complete. ## Review / integration gate Draft for independent review. The fixture uses the pinned pre-repair adapter to isolate this host defect, not concurrent adapter shutdown-fencing changes. Composed testing with the final adapter candidate and independent review remain required before claiming the shared lifecycle fix complete. No activation, merge, direct main push, or incident replay performed. Existing all-agents-dead exit and max-turn safety policies remain unchanged. Signed-off-by: Logan Johnson <loganj@squareup.com> |
||
|
|
639593bba9 |
fix(relay): restore buzz-relay build after #8005 and #8028 (#8036)
🤖 `main` at `2ebfde914` doesn't compile `buzz-relay`. #8005 and #8028 each passed CI alone and git merged them cleanly, but they conflict in meaning: - #8005 added a call to `crate::nip_fi_http::http_denial` in `api/git/transport.rs`. #8028 turned that path into a private re-import of `nip_fi_core::http_denial`, so the call fails with E0603. It now calls `crate::nip_fi_core::http_denial` directly. Behavior is unchanged. - #8005's test `failed_restriction_lookup_is_503_not_403` implements `VerifyAssertion::verify_assertion` with one argument. #8028 added a `community: &CommunityBinding` parameter to the trait, so the test fails with E0050. The test stub now accepts and ignores `_community`. Each PR's CI ran against a base without the other's change, so neither CI run could catch this. 🤖 The local `rust-tests` pre-push hook was skipped for this push. It kept failing on an unrelated `buzz-acp` timing test, `keepalive_resets_idle_past_deadline`, which passes when run alone. A serial `just test-unit` run was fully green. CI's Unit Tests job is the gate for this PR. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
2782b9cd5e |
fix(mobile): preserve exact mention recipients in saved drafts (#7387)
<!-- Draft PR body for #7387 · fix(mobile): preserve exact mention recipients in saved drafts · https://github.com/block/buzz/pull/7387 --> 🤖 ## Summary Saved drafts remembered their text but not exactly who each mention referred to: reopening a draft (or restarting the app) could re-bind a mention to the wrong same-name teammate, and a draft restored in one thread could capture text arriving for another. This PR saves the exact selected identities alongside the draft text and restores them together, so a reopened draft names the same recipients — with their agent markings — that you picked, including after a failed send. - Persists selected keys and agent presentation in the app's existing per-account/channel/thread draft storage (device-local; no cross-device portability claim). - Restores recipient bindings before text listeners run, including recovery from a rejected send; malformed legacy entries are ignored rather than guessed. - Retires the previous draft's listeners before restoring another, so switching threads can no longer write incoming text into your outgoing draft. ### Related issue - Fixes: N/A. No separate issue; the related work is the stack below. - Depends on #7385 (declared base) — same-name selections and qualified labels come from that PR. - Landing note: branches in this series overlap in the composer — when rebasing, keep exact/durable mention bindings, the explicit invite/reference-only choice, the account/visit/revision fences, and authorization before membership preparation and publication; don't resolve conflicts by taking either side wholesale. - Draft — not requesting merge yet; the security advisory doesn't run on this stacked base (it reviews main-based ranges). ### Testing - Regressions cover two namesakes, restart, rename, rejected send, a second restart, and changing draft scope. - At `e544b247c32bdccf1b316d3329be247faf412f5f`: `just mobile-check` and full `just mobile-test` pass (2,085 tests). The earlier full `just ci` receipt linked below is reused only for unchanged non-mobile code/tooling, not claimed as a rerun at this head. - Previous-head evidence: `just mobile-check`, the full mobile test suite, and full local `just ci` all pass — receipts in the [exact-head evidence comment](https://github.com/block/buzz/pull/7387#issuecomment-5574635970). - Verification is widget-test level; no native device or simulator run is claimed. To see it: mention an agent or a namesake, leave the channel, restart the app, and return — the draft still names the exact recipients you picked. ### Screenshots Flutter production-widget test renders — not native-device screenshots or acceptance captures. | Scenario | Before | After | |---|---|---| | Draft reopened after leaving the channel and restarting the app |  |  | <details> <summary>Capture provenance</summary> Rendered by the Flutter widget engine in a `flutter test` run (production widgets, production theme; no device or simulator). Before: this PR's declared base `39afd73b0adfde14164f4b10dbd089cb498312b6`. After: its head `8a781108507dc4501192b0101185decc1ed6f118`. </details> Signed-off-by: Logan Johnson <loganj@squareup.com> |
||
|
|
2ebfde9147 |
feat(nip-fi): bind assertions to the request Host's community (#8028)
🤖 Follows #7990 (merged). Binds every enforce-mode NIP-FI assertion to the community served at the request's `Host`. Before this, an assertion was accepted if its `aud` matched any value in a deployment-wide set. That meant an issuer trusted by one community could mint an assertion another community accepted. **Rollout consequence: an enforce relay will not start without `BUZZ_NIP_FI_COMMUNITIES`.** `off` and `deny_protected` do not read it, so repair mode still boots. ## What changes - **Host resolution runs first.** In enforce mode, right after the `off` and `deny_protected` checks, the `Host` must map to a configured community. An unmapped or missing `Host` returns 503 `authorization unavailable`. This happens at all three call sites: the router guard, the upgrade (WS root h1/h2 and audio), and HTTP admission. Off mode behaves exactly as before. - **The allowlist is checked before JWKS.** `VerifyAssertion::verify_assertion(token, &CommunityBinding)` rejects an issuer the community does not authorize before any key-source lookup. Clients can't tell this rejection apart from an unknown `iss`. `aud` must exactly equal the community's canonical URI. - **New config: `BUZZ_NIP_FI_COMMUNITIES`.** It is a JSON array of `{canonical_uri, authorized_issuers}`. Startup fails if: - a `canonical_uri` is anything other than a canonical `https://<host>[:port]`: the URL parser must reserialize the configured authority byte-for-byte, so whitespace, control characters, backslashes, uppercase, a redundant `:443`, and non-canonical IPv6 are rejected rather than repaired. The authority must also already be Host-normalized, so a trailing dot or `:80` is rejected and the Host map key always equals the `aud` authority exactly (never empty); - two communities share a `Host`; - an allowlisted issuer is missing from `BUZZ_NIP_FI_ISSUERS`; - an allowlist is empty; - a configured issuer belongs to no community. Errors name entries by index only. - **`IssuerPolicy.audiences` is removed.** Command JWTs use the new per-issuer `command_audiences`, so #7977's admin-disconnect command behavior is unchanged. A config that still sets `audiences`, even to `null`, fails with a migration message instead of being silently ignored. - **`AssertionPolicyId` now covers the community allowlist.** It hashes the `aud` of each community that authorizes the issuer, so changing the allowlist changes the ID. This is code only; no spec file changes here. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <wpfleger@block.xyz> Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz> |
||
|
|
a38707d35c |
fix(relay): close community ban gaps across audio, agents and HTTP (#8005)
🤖 Community bans had gaps. Audio sockets never checked them: the audio join path didn't look up ban state, and a ban closed only the member's root sockets, so a banned member could join a huddle or stay in one. An agent's access comes from its owner, but the ban and timeout checks only looked at the pubkey that signed the event. Several write paths returned before those checks ran, admission could miss a ban that landed mid-AUTH, and removing or losing a member left live sockets open. This PR closes those gaps. ## Audio - `handlers/auth.rs`: the root socket's ban lookup, including the NIP-OA owner cascade and fail-closed DB handling, is `community_ban_outcome`. Root auth and audio join both call it, so the two checks can't drift apart. - `audio/handler.rs`: audio join runs the ban check after the write-free channel membership check and before any huddle lease or durable write. A ban is refused with `AuthorizationDenied` and a DB error with `AuthorizationUnavailable`. With no FI assertion, the refusal is the root route's text (`blocked: you are banned from this community` / `error: internal error checking restriction state`). - `state.rs`: `CommunityConnectionControl` holds the authenticated pubkey. `CommunityConnectionRegistry::disconnect_pubkey` closes bound audio sockets for one pubkey in one community with a 1008 `access revoked` close. `AppState::disconnect_pubkey_local` closes everything a pubkey has open in a community on this pod (root and audio); the clusterwide disconnect and the conn-control Redis subscriber in `main.rs` both use it, so remote pods close audio sockets too. Root sockets are still closed through `ConnectionManager`, so nothing is closed twice. A socket that proved a NIP-FI identity instead gets the standard NIP-FI denial (the restricted frame, then 1008 `authorization denied`), and a ban never overwrites a close reason already chosen. ## Behavior - **Owner restrictions carry to agents.** `restriction_state` folds in the restriction row of the agent's owner (`users.agent_owner_pubkey`) at read time. Nothing is copied onto the agent, so unbanning or un-timing-out the owner restores the agent immediately. Every hot-path caller (AUTH, ingest, audio, HTTP) picks this up. - **Admission is fenced against a concurrent ban.** Root AUTH and audio admission bind the proven pubkey to the socket first, then make the final ban and relay-membership decision (`handlers::auth::final_admission_denial`), then refuse if a disconnect already cancelled the socket. A ban or removal whose disconnect ran before the bind is visible to those fresh reads; one that runs after it finds and closes the bound socket. Both reads come from the writer, never the bounded-stale replica, so a removal that committed before the bind cannot be undone by a lagging replica. Both fail closed, and the check covers a delegated agent's owner. Each admitted socket records the agent's owner, from the NIP-OA tag or the stored link. - **Agents are always linked to their owner before admission.** Root AUTH and audio both record `users.agent_owner_pubkey` for a NIP-OA agent before admitting it, and refuse admission (`error: internal error recording agent owner`) if that write fails, because revoking the owner finds the agent's sockets through that link. - **Revoking live access covers agents without a database read.** `AppState::revoke_live_access` sends one clusterwide disconnect that closes, on every pod, each root and audio socket in the community whose principal **or recorded owner** is the target. An agent socket admitted before its owner link existed carries no recorded owner, so recording an owner for the first time (`materialize_nip_oa_owner`, the single writer used by root AUTH, audio admission and HTTP NIP-OA) closes that agent's ownerless root and audio sockets clusterwide; they reconnect with the owner attached. That disconnect skips owner-bound sockets. Admission with a NIP-OA owner records it on the socket before binding the pubkey, so the socket survives its own link; admission without one binds the pubkey first and then reads the stored owner, so a concurrent link either closes the socket or is seen by the read. A `users.agent_owner_pubkey` sweep still runs as a secondary path; if its lookup fails, the error is returned and there is no retry task. The kind-9040 ban, the report-action ban (left non-terminal so recovery re-runs it), relay-admin removal (9031), and NIP-43 self-leave all use it and report an incomplete revoke instead of claiming success. Each revokes right after its change commits; the ban no longer waits on its audit insert, whose error is still returned. - **NIP-43 self-leave works over WebSocket.** Kind 28936 is in the ephemeral range, so `handle_event` used to fan it out and answer `OK true` without removing membership. It is now routed to ingest; no other ephemeral kind changes. - **The write gate runs before every kind-specific branch.** `enforce_write_restriction` runs ahead of command routing (DM open, DM add member, DM hide, workflow definitions and triggers, approvals), product feedback, reports, and moderation or relay-admin commands. Ephemeral events and agent observer frames return before ingest in `handle_event`, so they go through the same gate there. - A ban exempts nothing. - A timeout admits only reports (kind 1984), unban (9041) and untimeout (9043). A timed-out admin cannot send relay-admin commands (9030–9033) until the timeout ends. - **HTTP refuses banned principals.** `enforce_relay_membership` runs a ban check after the membership decision, including on open relays: 403 `blocked: you are banned from this community` for a ban, 503 for a failed lookup. Media upload, media GET/HEAD and Git transport keep that 503 instead of reporting it as "not a relay member": in NIP-FI Enforce mode they send the canonical `AuthorizationUnavailable` response (`text/plain; charset=utf-8`, `authorization unavailable\n`), and in Off mode each keeps its legacy unavailable response. Media storage failures are not reclassified. Git transport checks membership and then runs its own ban check once per request. The NIP-OA owner is resolved from `x-auth-tag` when present. HTTP checks **bans only**: a timeout blocks writes, HTTP writes already reach the ingest gate, and reading stays allowed under a timeout. Unchanged by design: a lost Redis fan-out can leave another pod's socket open until it reconnects, and there is no recurring ban recheck on open sockets. ## Path table | Path | Category | Ban check | |---|---|---| | Root WebSocket AUTH and audio admission, including NIP-OA owner binding | community admission | final fenced check after bind; owner link required | | `POST /events`, `/query`, `/count` (`bridge.rs`) | community HTTP | existing `enforce_relay_membership` caller | | GIF search and `POST /gifs/share` (`gifs.rs`), media upload and fetch (`media.rs`), workflows (`workflows.rs`) | community HTTP | existing caller; media keeps 503 on lookup failure | | Git transport (`git/transport.rs`), git settings (`git/settings.rs`) | community HTTP | existing caller; transport keeps 503 on lookup failure | | `GET /moderation/reports`, `/moderation/audit`, `/moderation/restricted` | community HTTP | **newly covered** | | `POST /api/invites` (invite mint) | community HTTP | **newly covered** | | NIP-43 self-leave (WebSocket and `POST /events`, both via ingest) | roster change | revokes live access like admin removal | | Invite claim, accept-policy, `/api/join-policy*` | pre-membership invite and policy flows | none; ban state is checked when the caller authenticates | | `/`, `/info`, `/.well-known/nostr.json`, `/health`, `/_liveness`, `/_readiness` | public metadata and health | none | | `/hooks/{id}` | secret-authenticated webhooks | none; no community pubkey | | `/operator/*`, `/api/admin/v1`, `/internal/git/policy` | operator, admin and internal routes | none; separate auth plane | | `/_mesh/demo/echo` | demo only (mesh plus `mesh_demo_echo`) | none | ## Known limits - Cross-pod closure depends on the Redis fan-out; the tests prove the pod-local close by principal and recorded owner. - Live delivery to existing subscriptions depends on the disconnect. Events are not rechecked one by one. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz> |
||
|
|
9b083957f6 |
refactor(buzz-relay): share NIP-FI assertion evaluation across adapters (#7990)
🤖 Stack: #7990 → #8028 The three production NIP-FI assertion-verification sites (the HTTP assertion guard in `router.rs`, `check_nip_fi_at_upgrade` in `nip_fi_upgrade.rs`, and `admit_nip_fi_http` in `nip_fi_http.rs`) each carried their own extract → verify → map-to-denial sequence, and HTTP admission and `enforce_nip_fi_key_pairing` each carried their own key-equality check. The admin disconnect route (`POST /api/nip-fi/disconnect`) kept a second copy of the header parser and hand-typed its denial bodies. Community binding and shadow mode both need to change exactly those decisions, so they now live in one place. **No behavior change.** Every denial class, status code, body, header and log line is preserved, including `deny_protected` (503), the startup-race 503, transport-before-verifier precedence, NIP-98-before-assertion ordering, and guard + handler double verification. - New relay-private `nip_fi_core` owns `extract_bearer_token` and `http_denial` (moved from `nip_fi_http`), `evaluate_attached_assertion` (the single `verify_assertion` call for all three adapters, returning a typed `AssertionRejection`), and `asserted_key_matches` (the pairing predicate; a claimless assertion never matches). `http_denial` and the disconnect route's plain-text renderer fall back to the bare denial status instead of panicking or answering an empty 200; both fallbacks are unreachable because every status and header value is a constant. - Adapters keep their own mode short-circuits, exemptions, ordering, metrics and `code()` debug logs; the guard still logs nothing. - The disconnect route parses its header with `extract_bearer_token`, renders header failures with `http_denial`, and renders every other rejection (malformed body, bad pubkey, no command verifier, every `CommandError`) through `CommandError::http_status()`/`response_body()`. Its check order (header → body → pubkey → verifier present → verify), `DenySetFull` warning and metric, success body and cross-pod publish are unchanged. The old Unicode whitespace check and the shared ASCII one agree on every reachable input, because `HeaderValue::to_str` only admits visible ASCII and tab. - The 12 HTTP admission callers, 3 WS upgrade placements, deny-map checks, expected-URL constructors and the command/admin proof path are untouched. - Characterization tests land before each refactor, green against the unmodified code, and pass unchanged after it. A routed test counts both verifications on one request (guard, then handler admission) and shows admission's own failure decides the response. Disconnect-route tests pin exact status, content type, challenge and body for every rejection, the observable check order, and that only `DenySetFull` increments `buzz_nip_fi_disconnect_capacity_rejections_total`. The test modules share one counting `ScriptedVerifier` from `nip_fi_core::tests`, and the session tests build connections through `connection::tests::test_conn`. `nip_fi_core::tests::` is added to the unit selectors in `Justfile` and `scripts/run-tests.sh`. 🤖 Implemented by Hayt. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <wpfleger@block.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz> |
||
|
|
2e8b8d9a92 |
Include thread roots in agent activity events (#8029)
Include the canonical thread root in turn-start and liveness payloads so clients can distinguish concurrent threads under thread conversation context. Exercise liveness propagation through production prompt dispatch. Co-authored-by: Codex <codex@openai.com> Signed-off-by: Codex <codex@openai.com> Signed-off-by: Salman Mohammed <smohammed@squareup.com> |