Skip to content

fix(desktop): enforce agent mention authorization at send boundaries - #5681

Merged
wesbillman merged 12 commits into
mainfrom
fix/remote-agent-member-mentions
Aug 13, 2026
Merged

fix(desktop): enforce agent mention authorization at send boundaries#5681
wesbillman merged 12 commits into
mainfrom
fix/remote-agent-member-mentions

Conversation

@wesbillman

Copy link
Copy Markdown
Collaborator

Summary

  • allow channel-member remote/headless agents only with current kind 10100 directory evidence, while stale member identities remain hidden
  • fail closed while managed/relay directories load, error, or background-refetch across channel, forum, and cached autocomplete surfaces
  • revalidate agent mention authorization immediately before normal sends and message-edit saves, including after deferred uploads
  • in owner-only builds, fetch fresh authoritative profile ownership at send time and deny missing, changed-owner, or unavailable proofs
  • preserve human mention tags when agent authorization is revoked or unknown

Supersedes #5536 because its contributor-fork head cannot be updated by maintainers.

Validation

Exact head: 7278cdd5fbcee676c7b858ea098503c62eeeff0d

  • mandatory pre-push suites passed: desktop check/typecheck/tests, Rust tests, mobile tests, desktop Tauri checks, branch-skew
  • desktop unit tests: 4,732 passed
  • focused edit/ownership regressions: 8 passed
  • focused mention E2E: 5 passed (remote positive, stale-member negative, directory error, pre-send revocation, mid-send revocation)
  • file-size ratchet passed

One first focused E2E batch had a timing-only miss where the send click did not emit; the isolated rerun passed. One separate pre-push attempt hit the existing randomized passphrase separator test; the successful exact-head push reran and passed the mandatory suite.

JDiz00 and others added 5 commits August 12, 2026 08:41
Signed-off-by: JDiz00 <174381550+JDiz00@users.noreply.github.com>
Require affirmative relay-directory admission before exposing remote agents,
apply the internal same-owner boundary, and filter selected agent identities
again when emitting mention tags. Cover directory errors, recovery, outgoing
tags, and revocation before send.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Hide remote agents while directory policy is refetching and refresh both
agent directories immediately before sending. Drop agent p tags and audience
promotion when the fresh authorization is absent, revoked, or errors.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Revalidate edited-message agent tags immediately before save, including after
deferred uploads. In owner-only builds, fetch fresh authoritative profile
ownership for outgoing agent tags and fail closed on missing, changed, or
unavailable ownership proof.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>

@wpfleger96 wpfleger96 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 Combined review — two independent agent passes (Paul + Thufir), synthesized. Not approving yet: one authorization gap that both the verification trace and a full-suite run confirm should close before merge. Smaller items are inline.

Forum sends bypass the new revalidation boundary

desktop/src/features/forum/ui/ForumComposer.tsx:230-253 (file not in this diff, so noting here instead of inline): submitMessage calls the changed mentions.extractMentionPubkeys(trimmed) and passes that snapshot directly to submitterForumView.onSubmitcreatePostMutation, which emits the p tags. It never calls the new mentions.revalidateMentionPubkeys, so forum posts/replies get only render-time filtering — a directory/allowlist/owner change between selecting an agent and submitting is not freshly checked, and the stale pubkey can still wake an agent current policy no longer authorizes. This is the same boundary this PR closes for chat sends and edit saves; the forum composer is a production consumer of the same mention API that misses it.

Suggested fix: await mentions.revalidateMentionPubkeys(mentions.extractMentionPubkeys(trimmed)) in ForumComposer.submitMessage before clearing/submitting, plus a regression that selects an agent, revokes authorization, submits, and asserts the signed forum event carries no agent p tag (the current forum e2e proves autocomplete visibility only, not emitted tags).

CI attribution (from the logs, not assumed)

The red checks are infra + pre-existing flake, not this diff: Desktop E2E Integration (2/2) died on docker compose (digest-mismatch, exit 101) before any test ran; Desktop Smoke E2E (4)'s failure is video-attachment.spec.ts:1242 with a virtualization.spec.ts flake — neither file is touched here; main's latest completed CI run failed the same Integration shards. The PR's own 12 new/changed mentions.spec.ts tests ran green in Smoke shard 3, and the full desktop unit suite (4,732) passes at this head.

What's solid

The tri-state admission model is the right shape. Chat sends and edit saves revalidate after deferred uploads and before signing/saving, owner-only mode fetches fresh verified ownership at send time, and error/missing/changed-owner all fail closed with unit coverage for each branch. Human mention tags survive revocation exactly as described. Most importantly the fix is tested at its seam: the e2e bridge now reads the signed event's actual p tags (pre-send and mid-send revocation), and deleting the production wiring turns the suite red — verified by mutation, not assumed from green.

mentionableAgentPubkeys,
directoryAgentPubkeys,
directoryReady = true,
ownerOnly = false,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 ownerOnly = false here swallows the undefined that useMentions passes while the owner-policy query is still loading (ownerOnly: agentAccessOwnerOnlyQuery.data), so the "unknown" branch getAgentMentionAdmission has for exactly that state is unreachable through this wrapper. During the window where the agent directories are ready but the owner policy isn't, an other-owned relay agent is admitted into autocomplete in an owner-only build. Send-time revalidation strips it on the chat/edit paths (it checks ownerOnly === undefined explicitly), which keeps this non-blocking — but it contradicts the fail-closed intent, and both reviews flagged it independently. Fix: drop the destructuring default and pass undefined through, or fold ownerPolicyReady into the directoryReady value this call site receives.

return lookup;
}, [managedAgentsQuery.data, personasQuery.data]);
const knownAgentPubkeys = mentionableAgentPubkeys;
const knownAgentPubkeys = new Set([

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 This Set is rebuilt every render (on main, knownAgentPubkeys aliased the memoized mentionableAgentPubkeys) and sits in the dep arrays of handleMentionSelect and isAgentPubkey, so both recreate every render and the churn propagates into useMentionSendFlow's callbacks. Same pattern in useAgentMentionRevalidation: getSelectedAgentPubkeys: () => selectedAgentMentionPubkeysRef.current is a fresh arrow per render listed in its useCallback deps, so revalidateMentionPubkeys never holds identity and MessageComposer's submitMessage churns with it. Correctness is unaffected — this is render hygiene on the hottest composer path. Wrap the Set in useMemo; pass the ref (or a stable callback) for the getter.

const [selectedAgentMentionNames, setSelectedAgentMentionNames] =
React.useState<string[]>([]);
const selectedAgentMentionNamesRef = React.useRef<string[]>([]);
const selectedAgentMentionPubkeysRef = React.useRef<Set<string>>(new Set());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 clearMentions() clears both name states and both maps but never resets this set, so it accumulates for the hook lifetime. The effect is conservative (extra pubkeys get revalidated, never skipped), so no correctness issue — but reset it in clearMentions for symmetry. Both reviews flagged this independently.

wesbillman and others added 2 commits August 12, 2026 12:03
Apply the same final authorization refresh used by chat sends and edits to
forum posts and replies. Preserve the draft when refresh or submission fails,
and guard against duplicate submits while revalidation is in flight.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
@wesbillman

Copy link
Copy Markdown
Collaborator Author

Reviewing on Wes's behalf at exact head 1709de5aec3fb77209a3b3895aa962c2edbf4635.

The forum authorization bypass is closed, but the repair introduces a blocking draft-loss race.

[P1] Forum edits made during final revalidation are silently discarded. ForumComposer.submitMessage snapshots trimmed and currentPendingImeta before awaiting mentions.revalidateMentionPubkeys (desktop/src/features/forum/ui/ForumComposer.tsx:218-236). That refresh performs relay directory/profile I/O. During the await, the editor and toolbar remain enabled: isSubmissionPendingRef guards only another submit, while richText is still editable and sendDisabled/toolbar state do not include the pending flag (:113-131, :380-386, :514-583). If the user types or adds content while the refresh is in flight, success builds/sends the old snapshot and then clears the current editor at :238-257, deleting the newer input without sending or restoring it.

Please either lock the forum editor/attachment controls for the full pending interval using rendered state, or re-read and atomically snapshot the current draft after revalidation before clearing. Add a deferred-revalidation regression: start submit, edit the draft while refresh is pending, release refresh, and prove the later input is neither silently cleared nor omitted.

The final forum gate itself is correctly placed before signing, preserves the draft on refresh/submission failure, and has signed-event coverage for revoked-agent tag removal. Chat and edit boundaries remain sound. The earlier owner-policy default and callback/set churn comments are worthwhile follow-ups, but send-time admission remains fail-closed, so they are not additional merge blockers.

@wesbillman

Copy link
Copy Markdown
Collaborator Author

Blocker: the new forum preflight can discard edits made while authorization refresh is in flight.

submitMessage snapshots trimmed and currentPendingImeta before awaiting revalidateMentionPubkeys, but it does not disable the editor/media controls during that await. After the await, it snapshots savedContent from the current editor and then unconditionally clears the composer, while finalContent is still built from the old pre-await trimmed value. If the user types or attaches media during a slow directory/profile refresh, a successful submission sends the old body and clears the newer body/attachment. The newer draft is restored only when submitter rejects.

This is especially plausible here because the preflight does two directory refetches and, in owner-only mode, a fresh profile fetch. The ref prevents duplicate submits, but it does not prevent editing, and it is not represented in the toolbar/editor disabled props.

Please make the snapshot/clear boundary coherent: either capture and clear the entire draft before the await and restore it on refresh/submission failure (while preserving any subsequently started draft), or explicitly disable editing/media for the whole pending interval and snapshot all fields consistently. Add a regression with a deferred revalidation promise: submit body A, edit to body B while deferred, release it, and prove B is neither cleared nor silently replaced by A.

The authorization direction is otherwise strong: current-head forum sends now share the final revalidation boundary, chat and edit paths revalidate after deferred uploads, human mention tags survive agent revocation, and owner-only proofs fail closed.

wesbillman and others added 2 commits August 12, 2026 13:01
Keep the forum composer mutation surface disabled while final agent mention
authorization runs so the submitted snapshot cannot race newer edits. Preserve
the synchronous duplicate-submit guard, fail closed while owner policy loads,
and clean up mention selection callback and cache stability.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Make the forum form inert while final mention authorization is pending, guard
attachment callbacks against programmatic activation, and close already-open
autocomplete, emoji, and link overlays. Extend the delayed preflight regression
to prove an existing attachment cannot be removed and is included in the signed
post.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
@wesbillman

Copy link
Copy Markdown
Collaborator Author

Release-safety follow-up reviewed by Carl on Wes’s behalf at exact head ae71c72c00af66e582f3c22747a77de7c3021c27: APPROVED.

I re-traced the complete authorization boundary across autocomplete, chat send (including deferred upload), edit save, and forum post/reply. The two prior forum blockers are closed: forum sends now revalidate before signing, and the composer plus media/overlay mutation surfaces are locked for the full asynchronous preflight so the submitted snapshot cannot discard concurrent edits. The regression verifies both editor and attachment immutability and asserts the signed outgoing event omits the revoked agent while retaining the attachment.

The policy remains fail-closed on directory loading/error/refetch, unknown owner policy, stale directory membership, and missing/changed/unavailable owner proof. Human mention tags are preserved. I found no remaining release blocker or compatibility/migration risk. GitHub CI is fully green on this exact head, including Desktop Core, all smoke shards, integration shards, builds, security, DCO, and cross-platform checks.

@wesbillman

Copy link
Copy Markdown
Collaborator Author

Correction to my release verdict: do not merge this head yet. Princess Donut identified a real P1 gap I missed.

At useMentionSendFlow.ts:401-475, completeSend classifies mentions from stale render-time state and can prepare a DM/channel, start or attach managed agents, and enroll agents into the active Huddle. Fresh authorization does not occur until finishSend at :556-568, after those side effects. A revoked/unknown agent can therefore be awakened or enrolled even though its final signed p tag is correctly stripped.

Required fix: revalidate before deriving any agent-preparation list and use only admitted pubkeys for channel preparation, start/attach, and Huddle enrollment; retain the existing final post-upload revalidation before signing. Add a regression asserting revocation causes zero preparation/enrollment side effects as well as no outgoing agent tag.

I previously focused the release pass on signed-event enforcement and the repaired forum mutation race, and failed to trace authorization backward through all pre-sign side effects. That was my miss, not an ambiguity in the code.

Refresh mention authorization before preparing channels, starting or attaching
managed agents, or enrolling agents into Huddles. Keep the existing post-upload
refresh before signing, and verify revoked relay agents cause no preparation
side effects.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
@wesbillman
wesbillman requested a review from wpfleger96 August 13, 2026 18:00
Refresh pending non-member mention authorization when Invite is clicked so a
revoked relay agent cannot be added to channel membership before send-time
preparation. Cover revocation after the dialog opens and assert no membership,
agent lifecycle, Huddle, or outgoing mention side effects.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>

@wpfleger96 wpfleger96 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 Re-reviewed at head 939bf536 (two independent passes, Paul + Thufir, synthesized).

All prior feedback is addressed:

  • Forum send bypass (blocking)ForumComposer.submitMessage now awaits revalidateMentionPubkeys before building/submitting, keeps the draft on refresh failure, guards duplicate submits, and locks the composer surface (inert, disabled toolbar/attachments, guarded insert callbacks) while revalidation is in flight so the submitted snapshot can't race edits. Signed-event regression covers revocation mid-preflight, including proving a queued attachment can't be removed.
  • ownerOnly = false fail-open window — the destructuring default is gone; ownerOnly is now required boolean | undefined, so an unresolved owner-policy query reaches the tri-state "unknown" branch and fails closed, with a unit test for exactly that state.
  • clearMentions asymmetryselectedAgentMentionPubkeysRef is now cleared.
  • Memoization churnknownAgentPubkeys is useMemo'd and the revalidation getter is a stable ref, restoring the identity chain.

Beyond the asked fixes, the new commits also close the invite/preparation side doors: revalidation now runs before channel prep, managed-agent start/attach, and huddle sync in completeSend, and again on Invite click in handleInviteNonMembers — with e2e coverage asserting a revoked agent produces neither a p tag nor any membership/lifecycle/huddle commands. Traced all revalidation call sites at this head; no remaining consumer of the mention API skips the boundary.

CI: the only red (Smoke E2E shard 3, messaging.spec.ts:1311 compact link preview geometry) fails identically on main's latest run (45f4b91a, the commit that introduced that test) and neither the test nor link-preview-attachment.tsx is touched by this branch — pre-existing main breakage, not this PR.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
@wesbillman
wesbillman enabled auto-merge (squash) August 13, 2026 21:26
@wesbillman
wesbillman merged commit bcf353c into main Aug 13, 2026
26 checks passed
@wesbillman
wesbillman deleted the fix/remote-agent-member-mentions branch August 13, 2026 21:40
wpfleger96 pushed a commit that referenced this pull request Aug 13, 2026
…-projection

* origin/main:
  test: add deterministic desktop release smoke (#5699)
  fix(channels): return complete member rosters (#5765)
  feat(desktop): add Inbox message delete action (#5779)
  fix(desktop): enforce agent mention authorization at send boundaries (#5681)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
tellaho added a commit that referenced this pull request Aug 13, 2026
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>

* origin/main:
  Polish glass Huddle tray behavior (#5590)
  test: add deterministic desktop release smoke (#5699)
  fix(channels): return complete member rosters (#5765)
  feat(desktop): add Inbox message delete action (#5779)
  fix(desktop): enforce agent mention authorization at send boundaries (#5681)
  fix(desktop): route compact preview geometry fixture through media proxy (#5799)
  Make workflow run history authoritative in Desktop (#5780)
  fix(desktop): more compact "compact" link previews (#5629)
  Fix mobile composer input regressions (#5594)
  Add mobile community invites (#5641)
  Harden shared agent instruction review (#4220)
  chore(release): release Buzz Desktop version 0.5.11 (#5714)
  feat(acp): report standard adapter usage (#4950)
  fix(mobile): settle hydrated threads on latest reply (#4702)
  perf(desktop): persist channel snapshot hash (#5684)

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
yjc801 added a commit to yjc801/buzz that referenced this pull request Aug 14, 2026
* Make workflow run history authoritative in Desktop (block#5780)

## Summary

- persist stable workflow run `error_code` values separately from human
diagnostics
- expose NIP-98 authenticated, channel-authorized run history and
approval reads with stable keyset pagination
- connect Desktop to those authoritative reads and return the
relay-created run ID on trigger
- show truthful loading, failure, and pending-trace states, and do not
render approval actions from non-actionable stored hashes

## Validation

- pre-push `branch-skew`, `desktop-typecheck`, `desktop-test`,
`rust-tests`, `desktop-tauri-checks`, and `desktop-check` all passed on
`a097dbe5f`
- Desktop tests: 4,761 passed, 0 failed
- `cargo check -p buzz-relay`
- `git diff --check`

## Remaining gate

This does not claim a relay-backed Playwright workflow journey. The
browser relay bridge still routes workflow invokes through in-memory
handlers; that production-shaped acceptance gate remains follow-up work
before Workflows can leave preview.

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Co-authored-by: Mongo <5c25403eab7271f9f94ddd4f2b270e8cac2c92e2c830c51877cca6ec974ffb3f@buzz.block.builderlab.xyz>
Co-authored-by: Princess Donut <68157ebd23b3897c1991015c3038658ea916200c67d3a54620b0754d1b92f6e0@buzz.block.builderlab.xyz>

* fix(desktop): route compact preview geometry fixture through media proxy (block#5799)

**Category:** fix (CI)
**User Impact:** None — test-only change that unblocks `main` and every
open PR.

**Problem:** `main` has been red since block#5629 landed on `45f4b91a3`:
`Desktop Smoke E2E (3)` fails `compact link preview image geometry
truncates long titles to one line` on every build (main run 31727837133,
and e.g. block#5792, block#5790). Two independently-green PRs raced: block#5629 added
the test stubbing its preview image at the raw relay origin
(`http://localhost:3000/media/*.png`), while block#5627 rewrites sent
snapshot media through the authenticated local media proxy
(`http://127.0.0.1:54321` in the E2E mock bridge). Merged together, the
image request goes to the proxy origin, the stub never matches, and
`naturalWidth` stays `0`.

**Solution:** Point the route stub at the mock proxy origin, matching
the existing `sent link preview media uses the authenticated proxy in
compact and rich cards` test in the same spec.

**Testing:** Reproduced the failure locally on `45f4b91a3`, then with
this fix: targeted test passes, and the full `messaging.spec.ts` smoke
suite passes 58/58.

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Co-authored-by: Wintermute <3f1797424fd9ad6653a83665c660517777cd7f8c228c0d5907f49e01537f3ca5@buzz.block.builderlab.xyz>

* fix(desktop): enforce agent mention authorization at send boundaries (block#5681)

## Summary
- allow channel-member remote/headless agents only with current kind
`10100` directory evidence, while stale member identities remain hidden
- fail closed while managed/relay directories load, error, or
background-refetch across channel, forum, and cached autocomplete
surfaces
- revalidate agent mention authorization immediately before normal sends
and message-edit saves, including after deferred uploads
- in owner-only builds, fetch fresh authoritative profile ownership at
send time and deny missing, changed-owner, or unavailable proofs
- preserve human mention tags when agent authorization is revoked or
unknown

Supersedes block#5536 because its contributor-fork head cannot be updated by
maintainers.

## Validation
Exact head: `7278cdd5fbcee676c7b858ea098503c62eeeff0d`

- mandatory pre-push suites passed: desktop check/typecheck/tests, Rust
tests, mobile tests, desktop Tauri checks, branch-skew
- desktop unit tests: 4,732 passed
- focused edit/ownership regressions: 8 passed
- focused mention E2E: 5 passed (remote positive, stale-member negative,
directory error, pre-send revocation, mid-send revocation)
- file-size ratchet passed

One first focused E2E batch had a timing-only miss where the send click
did not emit; the isolated rerun passed. One separate pre-push attempt
hit the existing randomized passphrase separator test; the successful
exact-head push reran and passed the mandatory suite.

---------

Signed-off-by: JDiz00 <174381550+JDiz00@users.noreply.github.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: JDiz00 <174381550+JDiz00@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>

* feat(desktop): add Inbox message delete action (block#5779)

### What changed?

Inbox message action menus now show a standalone Delete action beside
Edit for manageable messages. Delete reuses the existing confirmation
and targets the message whose menu was opened, while the existing
empty-edit deletion path remains unchanged.

### Why?

Inbox users can delete a message directly without first entering edit
mode. Thread context can contain multiple messages, so the action must
preserve the active Inbox selection and delete only the chosen row.

### How is it tested?

Desktop checks, typechecking, builds, and test suites pass.

Added tests:

- [Inbox edit and delete E2E
coverage](https://github.com/block/buzz/tree/main/desktop/tests/e2e/inbox-edit.spec.ts)

Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Codex <noreply@openai.com>

* fix(channels): return complete member rosters (block#5765)

## Summary

- return complete channel rosters instead of truncating at 1,000 members
- chunk `event_mentions` inserts inside one transaction so large kind
`39002` snapshots remain discoverable by every `p` tag
- add a targeted `buzz-admin reconcile-channels --channel <uuid>`
force-republish path for stale discovery snapshots
- cover a 1,501-member roster, 11,000-tag mention index, and kind
`39002` tag construction past member 1,000

## Why

The relay builds NIP-29 discovery and several authorization decisions
from `get_members()`, but that helper silently returned only the first
1,000 active members. Desktop then counted the truncated kind `39002`
event, while late members could be rejected by roster-scanning member
actions.

Removing the roster cap exposes PostgreSQL's 65,535 bind-parameter
ceiling in mention indexing, so the insert is chunked transactionally to
preserve all-or-nothing indexing.

The existing reconcilers only fill missing discovery events. The
targeted admin option bypasses the separately known 1,000-channel
reconciliation-list ceiling and replaces an existing channel snapshot
using the configured production relay key.

## Attribution

This supersedes and builds on block#3166 by @LordMelkor. Thank you for
identifying the roster boundary and contributing the original
complete-roster and mention-index patch. The production roster/query
changes and the two PostgreSQL regressions retain that work's shape;
this PR rebases it onto current `main`, adds relay coverage, and adds
the targeted repair operation requested for rollout.

## Validation

Exact pushed head: `24d02e4f3824150ed84913c9d230e675502e5b12`

- `cargo check -p buzz-db -p buzz-admin`
- `cargo test -p buzz-db
channel::tests::get_members_returns_full_roster_beyond_1000 -- --ignored
--exact --nocapture`
- `cargo test -p buzz-db
feed::tests::insert_mentions_indexes_rosters_past_bind_parameter_cap --
--ignored --exact --nocapture`
- `cargo test -p buzz-relay --lib
handlers::side_effects::tests::group_members_snapshot_keeps_members_past_one_thousand
-- --exact`
- `cargo run -q -p buzz-admin -- reconcile-channels --help`
- mandatory pre-push hook: branch-skew, desktop checks/typecheck/tests,
mobile tests, Rust tests, and desktop Tauri checks all passed on the
pushed head

## Rollout

1. Deploy the relay/backend build.
2. Run `buzz-admin reconcile-channels --channel <general-channel-uuid>`
with `BUZZ_RELAY_PRIVATE_KEY` configured.
3. Verify the replacement kind `39002` roster count matches the active
database membership count.

No schema migration or desktop release is required.

Fixes block#3156
Supersedes block#3166

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>

* test: add deterministic desktop release smoke (block#5699)

## Summary

- add `just desktop-release-smoke`, a deterministic desktop
correctness/reachability smoke against an ephemeral real local relay
- preserve existing DM history when the first live DM enters a pageless
query window, the desktop-v0.5.10 disappearing-DM regression
- enforce foreground JS ordering: a frame and actionable sidebar input
must dispatch before mounted stale queries begin resume refetches, while
separately requiring the navigation to commit promptly
- seed a 10,000-event dense-second fixture and verify exact event-ID
reachability, SHA-256 identity, ordering, duplicate absence, bounded
mounted rows, and drained render work
- isolate Postgres per run, serialize the shared Redis DB, retain
phase/relay/Playwright diagnostics, and gate desktop release manifest
assembly on the smoke

This is deliberately **not a performance-regression gate**. CDP and
action timing fields are informational only. There is no
candidate/baseline comparison or threshold. A future performance lane
needs repeated equivalent fixtures, discrete interaction samples, and an
explicit comparator/noise policy.

The diagnostics record the fixture version, row count, wall-clock base
timestamp (`fixtureSecond`), expected event-ID hash, observed state, and
measurements. Because the created-at floor requires a current timestamp,
paired comparison remains disabled.

The release job runs on an isolated GitHub-hosted runner. The script
also guards automatic local runs with a Redis allocation lock. Its
remaining direct-PID cleanup and free-port selection race mean it should
not be repurposed onto a persistent concurrent shared runner without
first hardening process-group cleanup and port reservation.

### Related issue

N/A

### Testing

- `pnpm --dir desktop typecheck`
- focused real-local-relay release smoke passed after adversarial review
fixes
- identical DM witness passed current and failed `desktop-v0.5.10` with
the history-loss signature
- identical foreground witness bytes
(`2c1e97df04c9b8ca0304b66bbbe9bdb4d08924ad8ce0f68a9c490458fcc3aca8`)
failed `desktop-v0.5.10` structurally: the first resume fetch was marker
1, before first frame/sidebar dispatch at marker 8
- with PR block#5696 (`59f613c40`) merged, the witness showed focus at 951.3
ms, first frame at 951.6 ms, click dispatch at 952.1 ms, first resume
fetch at 968.9 ms, and route commit at 992.4 ms
- the gate therefore protects first paint and actionable input dispatch;
route commit is a bounded responsiveness witness, not a prerequisite for
resume work
- the corrected focused foreground scenario passed at
`6d9b5be40da58bbee92a856b04c3558946d0a950`; the prior merged-tree full
run passed DM retention and 10k reachability before exposing this
contract mismatch
- pre-push passed on exact pushed head
`6d9b5be40da58bbee92a856b04c3558946d0a950`, including desktop checks,
typecheck, desktop tests, Rust tests, mobile tests, and Tauri checks
- full 10,000-event scenario reached 10,000/10,000 exact IDs with
matching SHA-256, 199 continuation requests, and 95 mounted rows in
about 4.4 minutes
- reduced-row review run passed in 18.4 seconds

### Foreground witness boundary

The Chromium test is a deterministic JS policy gate. Headless Chromium
does not expose an honest blur/focus transition in this fixture, so the
test drives the production focus listener and `document.hasFocus()`
predicate together and records that simulation explicitly. It proves
refetch fan-out ordering, not AppKit activation, WKWebView paint, or an
activating physical click. A packaged macOS native lane is still
required before claiming the actual desktop activation experience is
certified.

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>

* Polish glass Huddle tray behavior (block#5590)

## Summary

- inset the in-app Huddle tray with four rounded corners and even 8px
spacing when Glass background is enabled
- keep the popped-out Huddle dock full-width
- hide and suppress Glass background on Linux

## Why

The in-app tray reused the opaque backing needed by non-glass windows,
which covered the native vibrancy around it. Linux does not support this
window treatment.

## Testing

- `pnpm -C desktop build:e2e`
- focused Appearance and Huddle Playwright smoke tests
- pre-push desktop checks, typecheck, and 4,666 unit tests

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>

* Speed up initial direct messages (block#5658)

## Summary

- avoid blocking first-DM navigation on a full channel-list refresh
- publish the initial message through the acknowledged HTTP path instead
of waiting on a missing WebSocket acknowledgement

## Validation

- 4,715 desktop unit tests
- desktop typecheck and checks
- focused new-DM Playwright coverage

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Mongo <9cfd347903944d5b85aa6c93d2ab67381b978a92a31914bca69998968752a1d7@buzz.block.builderlab.xyz>

* Preserve member admission during send-time mention revalidation

getAgentMentionAdmission's lenient-member rule requires isMember and
compares against directoryAgentPubkeys, but revalidateAgentMentionPubkeys
called it without either — a channel-member agent with no kind:10100
directory record that the picker correctly admits was then denied by the
mandatory pre-send revalidation pass, silently dropping its mention tag
(no wake, no audience promotion, no Huddle enrollment) while the visible
@name stayed in the message text.

Thread channel-membership pubkeys through revalidateAgentMentionPubkeys
and useAgentMentionRevalidation, and derive directoryAgentPubkeys from the
freshly refetched relay directory so revalidation applies the same
admission rule the picker uses.

Reported-by: Alex <alex@buzz>
Signed-off-by: Junchao Yan <yjc801@gmail.com>

* Refetch channel roster during send-time mention revalidation

The prior fix passed the picker's cached membership set into revalidation
while the managed-agent and relay directories were freshly refetched. That
left a stale-membership window: if another admin removed a directory-less
agent after the picker/draft loaded, the cached member set still marked it
a member, so isLenientMember kept admitting it and the send emitted its
mention/wake tag for an agent no longer in the channel. The membership
query's 30s staleTime and user-scoped invalidation subscription don't fence
against another member's removal.

Refetch the channel roster in the same Promise.all as the managed/relay
directory refetches and derive membership from that fresh result. Fail
closed (deny) when the roster refetch errors or returns no data, matching
the existing fail-closed behavior for the other directory fetches.

Reported-by: Alex <alex@buzz>
Signed-off-by: Junchao Yan <yjc801@gmail.com>

* Only require fresh channel roster for channel-scoped mention admission

Round-2's fail-closed roster refetch ran unconditionally, so a new-DM
composer (MessageComposer with channelId=null, eligibilityScope
"managed-only", before onPrepareSendChannel creates the channel) had no
roster to fetch, failed closed, and stripped a valid managed-agent mention
before the DM was ever created.

Roster proof is only relevant to the lenient channel-member admission
branch. Fetch it only when eligibilityScope.type === "channel"; other
scopes use an empty member set and are admitted on the managed/relay
directory checks alone, same as before the roster refetch existed.

Reported-by: Alex <alex@buzz>
Signed-off-by: Junchao Yan <yjc801@gmail.com>

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Signed-off-by: JDiz00 <174381550+JDiz00@users.noreply.github.com>
Signed-off-by: Tom Brow <tomb@block.xyz>
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Signed-off-by: Junchao Yan <yjc801@gmail.com>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Co-authored-by: Mongo <5c25403eab7271f9f94ddd4f2b270e8cac2c92e2c830c51877cca6ec974ffb3f@buzz.block.builderlab.xyz>
Co-authored-by: Princess Donut <68157ebd23b3897c1991015c3038658ea916200c67d3a54620b0754d1b92f6e0@buzz.block.builderlab.xyz>
Co-authored-by: thomaspblock <thomasp@squareup.com>
Co-authored-by: Wintermute <3f1797424fd9ad6653a83665c660517777cd7f8c228c0d5907f49e01537f3ca5@buzz.block.builderlab.xyz>
Co-authored-by: JDiz00 <174381550+JDiz00@users.noreply.github.com>
Co-authored-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: klopez4212 <klopez4212@gmail.com>
Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Co-authored-by: Mongo <9cfd347903944d5b85aa6c93d2ab67381b978a92a31914bca69998968752a1d7@buzz.block.builderlab.xyz>
shelman09 added a commit to Namleh-Studios/buzz that referenced this pull request Aug 17, 2026
* fix(desktop): enforce agent mention authorization at send boundaries (block#5681)

- allow channel-member remote/headless agents only with current kind
`10100` directory evidence, while stale member identities remain hidden
- fail closed while managed/relay directories load, error, or
background-refetch across channel, forum, and cached autocomplete
surfaces
- revalidate agent mention authorization immediately before normal sends
and message-edit saves, including after deferred uploads
- in owner-only builds, fetch fresh authoritative profile ownership at
send time and deny missing, changed-owner, or unavailable proofs
- preserve human mention tags when agent authorization is revoked or
unknown

Supersedes block#5536 because its contributor-fork head cannot be updated by
maintainers.

Exact head: `7278cdd5fbcee676c7b858ea098503c62eeeff0d`

- mandatory pre-push suites passed: desktop check/typecheck/tests, Rust
tests, mobile tests, desktop Tauri checks, branch-skew
- desktop unit tests: 4,732 passed
- focused edit/ownership regressions: 8 passed
- focused mention E2E: 5 passed (remote positive, stale-member negative,
directory error, pre-send revocation, mid-send revocation)
- file-size ratchet passed

One first focused E2E batch had a timing-only miss where the send click
did not emit; the isolated rerun passed. One separate pre-push attempt
hit the existing randomized passphrase separator test; the successful
exact-head push reran and passed the mandatory suite.

---------

Signed-off-by: JDiz00 <174381550+JDiz00@users.noreply.github.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: JDiz00 <174381550+JDiz00@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
(cherry picked from commit bcf353c)

* feat(mobile): require device authentication for identity export (block#5116)

**Category:** new-feature
**User Impact:** Mobile users must confirm with Face ID, biometrics, or
their device passcode before sending their Buzz identity to Desktop.

**Problem:** A signed-in phone could send its full identity, including
the `nsec`, to a desktop without fresh local verification.

**Solution:** Require OS device authentication before opening the
identity-recovery scanner, retain that authorization only for the active
pairing session and short pairing window, and require fresh
authentication again if it expires before the identity payload is sent.
Normal app opening, identity import, and community removal remain
unchanged.

## Screencasts

| Enable Face ID | Use Face ID |
| --- | --- |
| ![Enabling Face ID during identity
import](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/5116/enable-face-id.gif)
| ![Using Face ID for identity
export](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/5116/use-face-id.gif)
|

<details>
<summary>File changes</summary>

**Android and iOS integration**
- `mobile/android/app/build.gradle.kts` declares the AppCompat
dependency required by the biometric activity theme.
-
`mobile/android/app/src/main/kotlin/xyz/block/buzz/mobile/MainActivity.kt`
uses the activity type required by the system authentication prompt.
- `mobile/android/app/src/main/res/values/styles.xml` and
`mobile/android/app/src/main/res/values-night/styles.xml` use the
compatible launch theme.
- `mobile/ios/Podfile.lock` records the native local-authentication
dependency.
- `mobile/ios/Runner/Info.plist` explains why Buzz requests Face ID
access.

**Identity policy and pairing flow**
- `mobile/lib/shared/security/sensitive_action_authorizer.dart` wraps OS
authentication and maps platform errors to stable app-level outcomes.
- `mobile/lib/shared/community/community.dart` and
`mobile/lib/shared/community/community_storage.dart` persist the
sensitive-action policy.
- `mobile/lib/features/invites/invite_join_provider.dart` assigns the
explicit policy for invite-created communities.
- `mobile/lib/features/pairing/pairing_provider.dart` gates export,
binds grants to the active community/session, reauthenticates expired
grants, and clears grants on every terminal path.
- `mobile/lib/features/pairing/pairing_page.dart` lets users choose
biometric protection while importing an identity.
- `mobile/lib/features/settings/settings_page.dart` wires pairing into
settings.
- `mobile/lib/features/settings/settings_page/connection_section.dart`
authenticates before opening export recovery and bounds the
foreground-resume wait.
- `mobile/pubspec.yaml` and `mobile/pubspec.lock` add and lock
`local_auth`.

**Coverage**
- `mobile/test/shared/security/sensitive_action_authorizer_test.dart`
covers native result mapping, unsupported devices, and single-flight
behavior.
- `mobile/test/shared/community/community_test.dart` and
`mobile/test/shared/community/community_storage_test.dart` cover policy
defaults and persistence.
- `mobile/test/features/invites/invite_join_provider_test.dart` covers
the invite policy.
- `mobile/test/features/pairing/pairing_page_test.dart` covers import
protection controls.
- `mobile/test/features/pairing/pairing_provider_test.dart` covers
export/import authorization, stale/reset/concurrent guards, malformed
payload cleanup, and no-export failure paths.
- `mobile/test/features/settings/connection_section_test.dart` covers
the tap gate, lifecycle resume, and timeout behavior.

</details>

## Reproduction steps

1. Pair an identity into the mobile app.
2. Open Settings and choose “Send identity to desktop.”
3. Verify Face ID, biometrics, or the device passcode is required before
the recovery scanner opens.
4. Cancel device authentication and verify the scanner does not open and
no identity transfer begins.
5. Authenticate, scan a Desktop recovery code, confirm the SAS, and
verify the identity transfer completes.

## Validation

At `be5620f5f10aa6cc16e86a4f01f102f3d9aeef9b`:
- `cd mobile && ../bin/flutter analyze` — no issues
- `cd mobile && ../bin/flutter test` — 1,368 tests passed
- `cd mobile/android && JAVA_HOME=$(/usr/libexec/java_home -v 21)
./gradlew app:assembleDebug` — debug APK assembled successfully

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
(cherry picked from commit d8281b9)

* Adapt security ports to Namleh baseline

Signed-off-by: shelman09 <shelman09@outlook.com>

---------

Signed-off-by: JDiz00 <174381550+JDiz00@users.noreply.github.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: shelman09 <shelman09@outlook.com>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: JDiz00 <174381550+JDiz00@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
bhargavms pushed a commit to EWA-Services/buzz that referenced this pull request Aug 18, 2026
…lock#5681)

## Summary
- allow channel-member remote/headless agents only with current kind
`10100` directory evidence, while stale member identities remain hidden
- fail closed while managed/relay directories load, error, or
background-refetch across channel, forum, and cached autocomplete
surfaces
- revalidate agent mention authorization immediately before normal sends
and message-edit saves, including after deferred uploads
- in owner-only builds, fetch fresh authoritative profile ownership at
send time and deny missing, changed-owner, or unavailable proofs
- preserve human mention tags when agent authorization is revoked or
unknown

Supersedes block#5536 because its contributor-fork head cannot be updated by
maintainers.

## Validation
Exact head: `7278cdd5fbcee676c7b858ea098503c62eeeff0d`

- mandatory pre-push suites passed: desktop check/typecheck/tests, Rust
tests, mobile tests, desktop Tauri checks, branch-skew
- desktop unit tests: 4,732 passed
- focused edit/ownership regressions: 8 passed
- focused mention E2E: 5 passed (remote positive, stale-member negative,
directory error, pre-send revocation, mid-send revocation)
- file-size ratchet passed

One first focused E2E batch had a timing-only miss where the send click
did not emit; the isolated rerun passed. One separate pre-push attempt
hit the existing randomized passphrase separator test; the successful
exact-head push reran and passed the mandatory suite.

---------

Signed-off-by: JDiz00 <174381550+JDiz00@users.noreply.github.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: JDiz00 <174381550+JDiz00@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: bhargavms <bhargav.m@ewa-services.com>
kaalph pushed a commit to kaalph/buzz that referenced this pull request Aug 21, 2026
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>

* origin/main:
  Polish glass Huddle tray behavior (block#5590)
  test: add deterministic desktop release smoke (block#5699)
  fix(channels): return complete member rosters (block#5765)
  feat(desktop): add Inbox message delete action (block#5779)
  fix(desktop): enforce agent mention authorization at send boundaries (block#5681)
  fix(desktop): route compact preview geometry fixture through media proxy (block#5799)
  Make workflow run history authoritative in Desktop (block#5780)
  fix(desktop): more compact "compact" link previews (block#5629)
  Fix mobile composer input regressions (block#5594)
  Add mobile community invites (block#5641)
  Harden shared agent instruction review (block#4220)
  chore(release): release Buzz Desktop version 0.5.11 (block#5714)
  feat(acp): report standard adapter usage (block#4950)
  fix(mobile): settle hydrated threads on latest reply (block#4702)
  perf(desktop): persist channel snapshot hash (block#5684)

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
kaalph pushed a commit to kaalph/buzz that referenced this pull request Aug 21, 2026
…-projection

* origin/main:
  test: add deterministic desktop release smoke (block#5699)
  fix(channels): return complete member rosters (block#5765)
  feat(desktop): add Inbox message delete action (block#5779)
  fix(desktop): enforce agent mention authorization at send boundaries (block#5681)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
cursor Bot pushed a commit to Nuncio-hq/crew that referenced this pull request Aug 23, 2026
… (upstream block#5681)

Ported from block/buzz bcf353c. Crew keeps its extracted
useMentionSendComplete hook, explicit-empty imeta edit save, removed-mention
diff and Project workspace resolution; upstream revalidation is layered on
top.

Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Oscar Le <oscar.lehuu@gmail.com>
oscarlehuu added a commit to Nuncio-hq/crew that referenced this pull request Aug 23, 2026
… RUSTSEC-2026-0258 (#306)

* port(desktop): enforce agent mention authorization at send boundaries (upstream block#5681)

Ported from block/buzz bcf353c. Crew keeps its extracted
useMentionSendComplete hook, explicit-empty imeta edit save, removed-mention
diff and Project workspace resolution; upstream revalidation is layered on
top.

Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Oscar Le <oscar.lehuu@gmail.com>

* port(desktop): bound send-time relay agent mention authorization (upstream block#6224, block#6338)

Ported from block/buzz 3fdf289 and the block#6338 follow-up. Crew keeps its inline
list_relay_agents directory command and invoke.rs handler macro; the bounded
send-time check lands as a new commands::mention_authorization module instead of
upstream's relay_directory.rs rewrite. Cross-owner relay agents stay mentionable
in owner-only builds when relay policy plus bot-role membership authorize them;
Crew's RelayAgent has no ownerPubkey field, so admission is derived from
respondTo/allowlist and channel membership rather than owner identity.

Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Oscar Le <oscar.lehuu@gmail.com>

* port(acp): guard against unrequested public relay skills (upstream block#6394)

Ported from block/buzz d274a6e. Adds the base-prompt restriction plus a
regression test asserting the guard stays in the shared prompt.

Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Oscar Le <oscar.lehuu@gmail.com>

* port(deps): bump h2 to 0.4.16 for RUSTSEC-2026-0258 (upstream block#6222)

Ported from block/buzz cc8a8b0. Only the h2 entry is bumped; upstream's
incidental windows-sys re-resolution churn is left out to keep the lockfile
diff scoped.

Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Oscar Le <oscar.lehuu@gmail.com>

* test(e2e): accept the invite prompt in the cross-owner relay mention test

The owner-only cross-owner case still surfaces the not-in-channel invite
prompt before publication, so the test must accept it to observe the
outgoing p tag.

Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Oscar Le <oscar.lehuu@gmail.com>

* fix(mentions): drop reference mention tags for denied edit mentions

Edit-save revalidation stripped denied agents from mentionPubkeys but the
non-notifying reference tags were built before revalidation, so a revoked
agent's pubkey was still published and rendered as an agent chip.

Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Oscar Le <oscar.lehuu@gmail.com>

* fix(mentions): map RelayAgent.ownerPubkey in revalidate wrapper

Main now requires ownerPubkey on RelayAgent. Restore the mapping that
lived in the previous merge resolution so send-time revalidation stays
type-correct after the rebase.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: oscarlehuu <oscarlehuu@users.noreply.github.com>

---------

Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Oscar Le <oscar.lehuu@gmail.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: oscarlehuu <oscarlehuu@users.noreply.github.com>
BradGroux pushed a commit to BradGroux/buzz that referenced this pull request Aug 23, 2026
…lock#5681)

## Summary
- allow channel-member remote/headless agents only with current kind
`10100` directory evidence, while stale member identities remain hidden
- fail closed while managed/relay directories load, error, or
background-refetch across channel, forum, and cached autocomplete
surfaces
- revalidate agent mention authorization immediately before normal sends
and message-edit saves, including after deferred uploads
- in owner-only builds, fetch fresh authoritative profile ownership at
send time and deny missing, changed-owner, or unavailable proofs
- preserve human mention tags when agent authorization is revoked or
unknown

Supersedes block#5536 because its contributor-fork head cannot be updated by
maintainers.

## Validation
Exact head: `2014b0c6a3a156300791431a82039a6a93a3a6ab`

- mandatory pre-push suites passed: desktop check/typecheck/tests, Rust
tests, mobile tests, desktop Tauri checks, branch-skew
- desktop unit tests: 4,732 passed
- focused edit/ownership regressions: 8 passed
- focused mention E2E: 5 passed (remote positive, stale-member negative,
directory error, pre-send revocation, mid-send revocation)
- file-size ratchet passed

One first focused E2E batch had a timing-only miss where the send click
did not emit; the isolated rerun passed. One separate pre-push attempt
hit the existing randomized passphrase separator test; the successful
exact-head push reran and passed the mandatory suite.

---------

Signed-off-by: JDiz00 <174381550+JDiz00@users.noreply.github.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: JDiz00 <174381550+JDiz00@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
tellaho added a commit that referenced this pull request Aug 28, 2026
Keep successfully cached managed and relay agent directories usable while background refreshes run, including the matching members-sidebar classification. This intentionally changes autocomplete from the fail-closed-on-refetch policy introduced by #5681: autocomplete is only a hint, while send-time revalidation still fetches authoritative evidence and fails closed before any send.

Add coverage for refetch stability and the existing relay-agent invite-as-bot send flow.

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
tellaho added a commit that referenced this pull request Aug 28, 2026
Keep successfully cached managed and relay agent directories usable while background refreshes run, including the matching members-sidebar classification. This intentionally changes autocomplete from the fail-closed-on-refetch policy introduced by #5681: autocomplete is only a hint, while send-time revalidation still fetches authoritative evidence and fails closed before any send.

Add coverage for refetch stability and the existing relay-agent invite-as-bot send flow.

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
morgmart pushed a commit that referenced this pull request Aug 28, 2026
Keep successfully cached managed and relay agent directories usable while background refreshes run, including the matching members-sidebar classification. This intentionally changes autocomplete from the fail-closed-on-refetch policy introduced by #5681: autocomplete is only a hint, while send-time revalidation still fetches authoritative evidence and fails closed before any send.

Add coverage for refetch stability and the existing relay-agent invite-as-bot send flow.

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
matt2e added a commit that referenced this pull request Sep 2, 2026
… self

For the common already-running-agent send, the ~1s of latency was not the
agent wake but two identical revalidateMentionPubkeys passes (~8 sequential
relay round-trips), each fronted by an uncached NIP-11 HTTP GET. This lands
the independent quick win from the faster-agent-sends analysis on top of
the publish-first change.

- useMentionSendFlow: the publish-boundary pass now reuses the
  pre-side-effect pass's admitted result on the immediate path, and only
  re-validates when a deferred wait (background media upload, link-preview
  settlement) separated the two — preserving the #5681 authorization
  boundary where revocation can actually race the publish. Inputs are
  already normalized/deduped, so the substitution is behaviorally
  identical on the fast path.
- fetch_relay_self_at: NIP-11 `self` lookups are now cached per relay URL
  in AppState for 5 minutes. Only verified Some values are cached; non-2xx
  responses and missing/malformed `self` stay retryable so an outage is
  never pinned for the TTL. Keying by URL keeps community switches from
  ever serving another relay's identity.
- E2E: the two specs pinning revalidate_relay_agents at +2 per send now
  pin +1; a new spec pins the deferred-upload path still revalidating at
  the publish boundary (revocation injected mid-upload strips the p tag,
  +2 calls). Three new Rust tests pin cache hit, non-success no-cache,
  and TTL-expiry refetch.

Verified: cargo test -p buzz-lib --lib (3008 passed), clippy + fmt on
desktop/src-tauri, desktop tsc --noEmit, biome check, desktop unit tests
(5799 passed), and the full mentions Playwright smoke suite (80/81; the
one failure is the pre-existing under-load flake in the publish-first
provider-deploy spec, green in isolation) plus a 3x stress rerun of the
dedupe-affected specs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
matt2e added a commit that referenced this pull request Sep 2, 2026
`applyReusableAgentAccessPolicy` signalled "this hit the relay" by
returning a different object than it was handed: a matching policy
returned the caller's `agent`, a diverging one returned the fresh record
from `updateManagedAgent`. `useMentionSendFlow` read that with
`readyAgent !== agent` to decide whether an awaited relay round-trip
separated its pre-side-effect mention-authorization pass from the
publish, and therefore whether to revalidate at the publish boundary
(#5681).

The contract held today, and the safe failure direction (a gratuitous new
object) only costs a redundant pass — but the unsafe direction is silent:
a future in-place cache update that writes to the relay and returns the
caller's object would skip the publish-boundary revalidation, and nothing
pinned the convention. Make the signal load-bearing by construction.

- `applyReusableAgentAccessPolicy` now returns
  `{ agent, wrote }` (`ApplyReusableAgentAccessPolicyResult`), with
  `wrote` set from whether `updateManagedAgent` actually ran rather than
  from anything about the returned record. Its doc comment names the
  send-path consumer so the flag is not mistaken for incidental.
- The send path destructures `{ agent: readyAgent, wrote }` and sets
  `wroteRelayState` from `wrote`; the existing-member branch supplies
  `{ agent, wrote: false }`. Behaviour is identical — for the current
  implementation the two signals agree case for case.
- `provisionChannelManagedAgent`'s two reuse branches destructure the
  agent out; they never consulted the identity signal.

Three unit tests pin the contract in a new
`channelAgents.accessPolicy.test.mjs`: a matching policy reports
`wrote: false`, returns the input agent, and issues no command; a
diverging policy reports `wrote: true` and invokes
`update_managed_agent` with the resolved policy; and a write whose
response is content-identical to the input still reports `wrote: true`,
which is the case an identity or content comparison would miss.

The repository file-size ratchet stays red on this branch for
app_state.rs, agents.rs, managed_agents/runtime.rs and
useMentionSendFlow.ts. All four were already over before this change;
useMentionSendFlow.ts holds at its inherited 1077 lines here, so this
commit adds nothing to the ratchet and does not split the pre-existing
four.

Verified: desktop tsc --noEmit, biome check over src + tests (exit 0),
desktop unit tests (5810 tests, 5804 passed; the 5 failures are the
pre-existing inboxReopenNavigation and useRetainedProjectGitViews
loader failures, present on origin/main), and the full mentions (84) and
channels + agent-access-warning (93) Playwright smoke suites against a
pnpm build:e2e bundle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
matt2e added a commit that referenced this pull request Sep 2, 2026
…can be

PR #7154 review round 2, point 1 (P2): the publish-boundary revalidation
skips when `preparedUpload`, `draft.preparedLinkPreviews`, and
`relaySideEffectsRan` are all false, so the pre-side-effect authorization
pass stands as the publish answer across whatever separated the two. Those
three are an *enumeration* of the awaited steps known to reach the relay,
and enumerations rot: a contributor adding an awaited step later must
remember to set the boolean, and forgetting fails silent and unsafe — a
stale admission publishes and nothing goes red. This branch already
hardened one instance of this exact fragility class in cbd43fa, making the
access-policy `wrote` signal load-bearing by construction rather than by
convention.

Measure the gap instead of enumerating its causes:

- New `shouldRevalidateMentionsAtPublish` in `useMentionSendFlow.helpers`
  takes the three named triggers plus `msSinceAdmission` and adds a fourth
  clause: revalidate when at least `MENTION_ADMISSION_MAX_AGE_MS` (200)
  elapsed since the admission pass resolved. `completeSend` stamps
  `performance.now()` at admission and passes the delta at the boundary —
  monotonic, so a wall-clock step backwards cannot suppress the check.
- The named triggers stay: they catch sub-threshold relay writes, which
  the clock alone would miss.
- Cost on the fast path is nil. The measured immediate send is ~5 ms of
  local IPC between admission and publish (managed-agents cache read, the
  member short-circuit in readiness, and the no-active-huddle sync, which
  returns before touching the relay), so the bound never fires and the
  send keeps its single pass. What it converts is the unenumerated case:
  a future awaited step, a cold cache, or contention degrades to one
  extra ~570 ms pass instead of a silently stale publish.

Conceding one tail the review write-up did not name and we had not
either: `sync_agents_to_active_huddle` takes `AGENT_SYNC_LOCK` *before*
its phase check, so a concurrent huddle-enrolling send can hold the
"no active huddle" IPC — the leg that reports no relay work — open for
hundreds of ms. Rare, but it is a real case where that step is not
instant, and it is precisely what a clock catches and an enumeration
cannot.

Two corrections to the review's narrative, neither changing the fix.
The "matching access policy leaves the flag false" combination is
unreachable: `applyReusableAgentAccessPolicy` only runs for a non-member
(`useEnsureAgentMentionsReady.ts:105-111`), and a non-member that skips
the `wrote: true` attach must be in `participants` via
`preparedParticipantPubkeys`, which is non-empty only under DM expansion
— and that branch already set `relaySideEffectsRan` before readiness
ran. And unconditional revalidation was not taken because it re-adds
~570 ms to every send to shrink an uncaught window that is ~600 ms on
this branch *and* on `origin/main` (the admission pass's own three
sequential relay queries leave its answer ~380 ms stale at resolution;
the publish flight adds ~215 ms). Moving admission to the publish
boundary was not taken because `admittedMentionPubkeys` gates every
relay side effect that must precede the publish — DM participant set,
access-policy/attach, the wake queue, huddle enrollment — so a late-only
pass would write membership and queue agent wakes for unadmitted agents,
the #5681 violation this ordering exists to prevent.

Tests:
- The requested causal regression, red-on-revert verified: a member relay
  agent on `general` (readiness short-circuits — no policy read, no
  attach, no wake), no huddle, no attachments. A new releasable bridge
  knob `syncAgentsToActiveHuddleDelayMs` holds the huddle-sync IPC open;
  the spec waits for +1 revalidation, injects the revocation, then
  releases via `__BUZZ_E2E_RELEASE_HUDDLE_AGENT_SYNCS__()` — injection
  before release makes the ordering deterministic by construction rather
  than by timing. It asserts the message publishes, quinn's p tag is
  stripped, revalidation is +2, and `add_channel_members`,
  `attach_managed_agent`, `update_managed_agent` and `start_managed_agent`
  are all unchanged, which is what proves the leg wrote nothing and so
  fired no named trigger. Neutering the elapsed clause makes it red (p tag
  present).
- The untouched fast-path spec at `mentions.spec.ts:1793` stays at +1 as
  the over-trigger control: the bound must not fire on unheld sends, or
  the latency win this branch exists for is gone.
- Three unit tests pin the predicate directly: the fast path reuses the
  admitted set, the bound fires at exactly `MENTION_ADMISSION_MAX_AGE_MS`
  and not one ms below, and each named trigger still fires on its own
  inside the bound.

The repository file-size gate stays green.

Verified: desktop tsc --noEmit, biome check over the touched files (exit
0), desktop unit tests (5909 passed, 0 failed), and the full mentions
(89) and channels (88) Playwright smoke suites against a fresh
pnpm build:e2e bundle, plus a 3x stress rerun of the new spec.

Unrelated, recorded so it is not mistaken for fallout: the
`huddle-transcription.spec.ts` "assigns distinct agent voices" spec fails
on this branch — it also fails 3/3 with this change stashed and on a
clean tree, so it predates this commit. Diagnosing it needs a stable
preview server: a stale or dying server on port 4173 produces both
phantom failures and phantom passes here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
matt2e added a commit that referenced this pull request Sep 2, 2026
…boundary

PR #7154 review round 3 (P2): the elapsed-time staleness bound shipped in
c2c3aac deliberately admitted a <=200 ms window in which the publish
reused the pre-side-effect mention admission, and the reviewer's
shipped-bundle probe (the checked-in huddle-hold spec with its 400 ms
post-revocation wait turned down to zero) proved a revocation landing
inside that window published the stale p tag. That was the bound working
as designed — "<=200 ms of admitted staleness is acceptable" is a policy
call, and no explicit product/security sign-off exists for it. Third
round on the same point; concede in full and remove the policy question
rather than re-arguing it.

The publish boundary now re-runs mention revalidation unconditionally,
and the entire conditional apparatus is deleted: the
shouldRevalidateMentionsAtPublish predicate, the
MENTION_ADMISSION_MAX_AGE_MS threshold, the monotonic admittedAtMs
stamp, the relaySideEffectsRan trigger tracking (its DM-expansion and
readiness assignments and the matched_active_huddle branch — the huddle
sync invoke and its error handling stay), and the three predicate unit
tests. The resulting contract fits in one sentence — the pass
immediately before signing/publish is always fresh — and cannot rot the
way a trigger enumeration or a clock policy can. The early admission
pass stays: it gates every relay side effect that must precede the
publish (DM expansion, access-policy/attach, wake queueing, huddle
enrollment), the #5681 ordering a late-only pass would violate.

Sends with no agent mentions are unaffected: revalidateAgentMentionPubkeys
returns before any relay traffic when the requested set contains no
agents. Agent-mention sends pay one extra targeted pass (~570 ms today);
the follow-up commit joins the membership read into the backend's batch
fan-out to claw most of that back. A second pass shifts rather than
shrinks the ~600 ms revocation blind window (membership-read staleness
plus publish flight) — relay-side NIP-29 scoping remains the enforcement
boundary — but removing the conditional removes the accepted-staleness
policy and the enumeration-rot surface with it.

useEnsureAgentMentionsReady keeps its wroteRelayState signal and
applyReusableAgentAccessPolicy keeps the {agent, wrote} contract — both
stay truthful and unit-pinned; the doc comment now records that nothing
consumes the flag to decide anything.

E2E, per the review:
- The huddle-hold staleness spec becomes exactly the reviewer's probe:
  the 400 ms post-revocation wait is deleted, so the revocation is
  released with zero further hold (revoke-before-release keeps the
  ordering deterministic by construction). Red-on-revert verified: with
  the pre-fix useMentionSendFlow restored against this spec, quinn's
  pubkey stays in the outgoing mentions and the revalidation count stays
  at +1.
- The two fast-path specs pinning revalidate_relay_agents at +1 now pin
  +2: the publish-boundary pass is unconditional, including on the
  fastest member-agent path.
- The deferred-upload (+2), attach-path (mid-hold +1, final +2),
  active-huddle (+2), and DM-expansion (channels.spec.ts, 2) pins are
  unchanged and still meaningful; their comments no longer describe a
  conditional reuse.

Verified: desktop tsc --noEmit, biome check over the touched files
(exit 0), desktop unit tests (5906 passed, 0 failed — down exactly the
three deleted predicate tests), the full mentions (88) and channels (89)
Playwright smoke suites against a fresh pnpm build:e2e bundle, a 3x
stress rerun of the zero-wait staleness spec, and the red-on-revert
check above. Two infra notes from the runs: a self-hosted or
Playwright-managed preview server on 4173 is killed mid-run by the
sandboxed shell — both mass-failure runs (79 and 86 connection-refused
failures) reran green unsandboxed with zero test changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
matt2e added a commit that referenced this pull request Sep 3, 2026
… self

For the common already-running-agent send, the ~1s of latency was not the
agent wake but two identical revalidateMentionPubkeys passes (~8 sequential
relay round-trips), each fronted by an uncached NIP-11 HTTP GET. This lands
the independent quick win from the faster-agent-sends analysis on top of
the publish-first change.

- useMentionSendFlow: the publish-boundary pass now reuses the
  pre-side-effect pass's admitted result on the immediate path, and only
  re-validates when a deferred wait (background media upload, link-preview
  settlement) separated the two — preserving the #5681 authorization
  boundary where revocation can actually race the publish. Inputs are
  already normalized/deduped, so the substitution is behaviorally
  identical on the fast path.
- fetch_relay_self_at: NIP-11 `self` lookups are now cached per relay URL
  in AppState for 5 minutes. Only verified Some values are cached; non-2xx
  responses and missing/malformed `self` stay retryable so an outage is
  never pinned for the TTL. Keying by URL keeps community switches from
  ever serving another relay's identity.
- E2E: the two specs pinning revalidate_relay_agents at +2 per send now
  pin +1; a new spec pins the deferred-upload path still revalidating at
  the publish boundary (revocation injected mid-upload strips the p tag,
  +2 calls). Three new Rust tests pin cache hit, non-success no-cache,
  and TTL-expiry refetch.

Verified: cargo test -p buzz-lib --lib (3008 passed), clippy + fmt on
desktop/src-tauri, desktop tsc --noEmit, biome check, desktop unit tests
(5799 passed), and the full mentions Playwright smoke suite (80/81; the
one failure is the pre-existing under-load flake in the publish-first
provider-deploy spec, green in isolation) plus a 3x stress rerun of the
dedupe-affected specs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
matt2e added a commit that referenced this pull request Sep 3, 2026
`applyReusableAgentAccessPolicy` signalled "this hit the relay" by
returning a different object than it was handed: a matching policy
returned the caller's `agent`, a diverging one returned the fresh record
from `updateManagedAgent`. `useMentionSendFlow` read that with
`readyAgent !== agent` to decide whether an awaited relay round-trip
separated its pre-side-effect mention-authorization pass from the
publish, and therefore whether to revalidate at the publish boundary
(#5681).

The contract held today, and the safe failure direction (a gratuitous new
object) only costs a redundant pass — but the unsafe direction is silent:
a future in-place cache update that writes to the relay and returns the
caller's object would skip the publish-boundary revalidation, and nothing
pinned the convention. Make the signal load-bearing by construction.

- `applyReusableAgentAccessPolicy` now returns
  `{ agent, wrote }` (`ApplyReusableAgentAccessPolicyResult`), with
  `wrote` set from whether `updateManagedAgent` actually ran rather than
  from anything about the returned record. Its doc comment names the
  send-path consumer so the flag is not mistaken for incidental.
- The send path destructures `{ agent: readyAgent, wrote }` and sets
  `wroteRelayState` from `wrote`; the existing-member branch supplies
  `{ agent, wrote: false }`. Behaviour is identical — for the current
  implementation the two signals agree case for case.
- `provisionChannelManagedAgent`'s two reuse branches destructure the
  agent out; they never consulted the identity signal.

Three unit tests pin the contract in a new
`channelAgents.accessPolicy.test.mjs`: a matching policy reports
`wrote: false`, returns the input agent, and issues no command; a
diverging policy reports `wrote: true` and invokes
`update_managed_agent` with the resolved policy; and a write whose
response is content-identical to the input still reports `wrote: true`,
which is the case an identity or content comparison would miss.

The repository file-size ratchet stays red on this branch for
app_state.rs, agents.rs, managed_agents/runtime.rs and
useMentionSendFlow.ts. All four were already over before this change;
useMentionSendFlow.ts holds at its inherited 1077 lines here, so this
commit adds nothing to the ratchet and does not split the pre-existing
four.

Verified: desktop tsc --noEmit, biome check over src + tests (exit 0),
desktop unit tests (5810 tests, 5804 passed; the 5 failures are the
pre-existing inboxReopenNavigation and useRetainedProjectGitViews
loader failures, present on origin/main), and the full mentions (84) and
channels + agent-access-warning (93) Playwright smoke suites against a
pnpm build:e2e bundle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
matt2e added a commit that referenced this pull request Sep 3, 2026
…can be

PR #7154 review round 2, point 1 (P2): the publish-boundary revalidation
skips when `preparedUpload`, `draft.preparedLinkPreviews`, and
`relaySideEffectsRan` are all false, so the pre-side-effect authorization
pass stands as the publish answer across whatever separated the two. Those
three are an *enumeration* of the awaited steps known to reach the relay,
and enumerations rot: a contributor adding an awaited step later must
remember to set the boolean, and forgetting fails silent and unsafe — a
stale admission publishes and nothing goes red. This branch already
hardened one instance of this exact fragility class in cbd43fa, making the
access-policy `wrote` signal load-bearing by construction rather than by
convention.

Measure the gap instead of enumerating its causes:

- New `shouldRevalidateMentionsAtPublish` in `useMentionSendFlow.helpers`
  takes the three named triggers plus `msSinceAdmission` and adds a fourth
  clause: revalidate when at least `MENTION_ADMISSION_MAX_AGE_MS` (200)
  elapsed since the admission pass resolved. `completeSend` stamps
  `performance.now()` at admission and passes the delta at the boundary —
  monotonic, so a wall-clock step backwards cannot suppress the check.
- The named triggers stay: they catch sub-threshold relay writes, which
  the clock alone would miss.
- Cost on the fast path is nil. The measured immediate send is ~5 ms of
  local IPC between admission and publish (managed-agents cache read, the
  member short-circuit in readiness, and the no-active-huddle sync, which
  returns before touching the relay), so the bound never fires and the
  send keeps its single pass. What it converts is the unenumerated case:
  a future awaited step, a cold cache, or contention degrades to one
  extra ~570 ms pass instead of a silently stale publish.

Conceding one tail the review write-up did not name and we had not
either: `sync_agents_to_active_huddle` takes `AGENT_SYNC_LOCK` *before*
its phase check, so a concurrent huddle-enrolling send can hold the
"no active huddle" IPC — the leg that reports no relay work — open for
hundreds of ms. Rare, but it is a real case where that step is not
instant, and it is precisely what a clock catches and an enumeration
cannot.

Two corrections to the review's narrative, neither changing the fix.
The "matching access policy leaves the flag false" combination is
unreachable: `applyReusableAgentAccessPolicy` only runs for a non-member
(`useEnsureAgentMentionsReady.ts:105-111`), and a non-member that skips
the `wrote: true` attach must be in `participants` via
`preparedParticipantPubkeys`, which is non-empty only under DM expansion
— and that branch already set `relaySideEffectsRan` before readiness
ran. And unconditional revalidation was not taken because it re-adds
~570 ms to every send to shrink an uncaught window that is ~600 ms on
this branch *and* on `origin/main` (the admission pass's own three
sequential relay queries leave its answer ~380 ms stale at resolution;
the publish flight adds ~215 ms). Moving admission to the publish
boundary was not taken because `admittedMentionPubkeys` gates every
relay side effect that must precede the publish — DM participant set,
access-policy/attach, the wake queue, huddle enrollment — so a late-only
pass would write membership and queue agent wakes for unadmitted agents,
the #5681 violation this ordering exists to prevent.

Tests:
- The requested causal regression, red-on-revert verified: a member relay
  agent on `general` (readiness short-circuits — no policy read, no
  attach, no wake), no huddle, no attachments. A new releasable bridge
  knob `syncAgentsToActiveHuddleDelayMs` holds the huddle-sync IPC open;
  the spec waits for +1 revalidation, injects the revocation, then
  releases via `__BUZZ_E2E_RELEASE_HUDDLE_AGENT_SYNCS__()` — injection
  before release makes the ordering deterministic by construction rather
  than by timing. It asserts the message publishes, quinn's p tag is
  stripped, revalidation is +2, and `add_channel_members`,
  `attach_managed_agent`, `update_managed_agent` and `start_managed_agent`
  are all unchanged, which is what proves the leg wrote nothing and so
  fired no named trigger. Neutering the elapsed clause makes it red (p tag
  present).
- The untouched fast-path spec at `mentions.spec.ts:1793` stays at +1 as
  the over-trigger control: the bound must not fire on unheld sends, or
  the latency win this branch exists for is gone.
- Three unit tests pin the predicate directly: the fast path reuses the
  admitted set, the bound fires at exactly `MENTION_ADMISSION_MAX_AGE_MS`
  and not one ms below, and each named trigger still fires on its own
  inside the bound.

The repository file-size gate stays green.

Verified: desktop tsc --noEmit, biome check over the touched files (exit
0), desktop unit tests (5909 passed, 0 failed), and the full mentions
(89) and channels (88) Playwright smoke suites against a fresh
pnpm build:e2e bundle, plus a 3x stress rerun of the new spec.

Unrelated, recorded so it is not mistaken for fallout: the
`huddle-transcription.spec.ts` "assigns distinct agent voices" spec fails
on this branch — it also fails 3/3 with this change stashed and on a
clean tree, so it predates this commit. Diagnosing it needs a stable
preview server: a stale or dying server on port 4173 produces both
phantom failures and phantom passes here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
matt2e added a commit that referenced this pull request Sep 3, 2026
…boundary

PR #7154 review round 3 (P2): the elapsed-time staleness bound shipped in
c2c3aac deliberately admitted a <=200 ms window in which the publish
reused the pre-side-effect mention admission, and the reviewer's
shipped-bundle probe (the checked-in huddle-hold spec with its 400 ms
post-revocation wait turned down to zero) proved a revocation landing
inside that window published the stale p tag. That was the bound working
as designed — "<=200 ms of admitted staleness is acceptable" is a policy
call, and no explicit product/security sign-off exists for it. Third
round on the same point; concede in full and remove the policy question
rather than re-arguing it.

The publish boundary now re-runs mention revalidation unconditionally,
and the entire conditional apparatus is deleted: the
shouldRevalidateMentionsAtPublish predicate, the
MENTION_ADMISSION_MAX_AGE_MS threshold, the monotonic admittedAtMs
stamp, the relaySideEffectsRan trigger tracking (its DM-expansion and
readiness assignments and the matched_active_huddle branch — the huddle
sync invoke and its error handling stay), and the three predicate unit
tests. The resulting contract fits in one sentence — the pass
immediately before signing/publish is always fresh — and cannot rot the
way a trigger enumeration or a clock policy can. The early admission
pass stays: it gates every relay side effect that must precede the
publish (DM expansion, access-policy/attach, wake queueing, huddle
enrollment), the #5681 ordering a late-only pass would violate.

Sends with no agent mentions are unaffected: revalidateAgentMentionPubkeys
returns before any relay traffic when the requested set contains no
agents. Agent-mention sends pay one extra targeted pass (~570 ms today);
the follow-up commit joins the membership read into the backend's batch
fan-out to claw most of that back. A second pass shifts rather than
shrinks the ~600 ms revocation blind window (membership-read staleness
plus publish flight) — relay-side NIP-29 scoping remains the enforcement
boundary — but removing the conditional removes the accepted-staleness
policy and the enumeration-rot surface with it.

useEnsureAgentMentionsReady keeps its wroteRelayState signal and
applyReusableAgentAccessPolicy keeps the {agent, wrote} contract — both
stay truthful and unit-pinned; the doc comment now records that nothing
consumes the flag to decide anything.

E2E, per the review:
- The huddle-hold staleness spec becomes exactly the reviewer's probe:
  the 400 ms post-revocation wait is deleted, so the revocation is
  released with zero further hold (revoke-before-release keeps the
  ordering deterministic by construction). Red-on-revert verified: with
  the pre-fix useMentionSendFlow restored against this spec, quinn's
  pubkey stays in the outgoing mentions and the revalidation count stays
  at +1.
- The two fast-path specs pinning revalidate_relay_agents at +1 now pin
  +2: the publish-boundary pass is unconditional, including on the
  fastest member-agent path.
- The deferred-upload (+2), attach-path (mid-hold +1, final +2),
  active-huddle (+2), and DM-expansion (channels.spec.ts, 2) pins are
  unchanged and still meaningful; their comments no longer describe a
  conditional reuse.

Verified: desktop tsc --noEmit, biome check over the touched files
(exit 0), desktop unit tests (5906 passed, 0 failed — down exactly the
three deleted predicate tests), the full mentions (88) and channels (89)
Playwright smoke suites against a fresh pnpm build:e2e bundle, a 3x
stress rerun of the zero-wait staleness spec, and the red-on-revert
check above. Two infra notes from the runs: a self-hosted or
Playwright-managed preview server on 4173 is killed mid-run by the
sandboxed shell — both mass-failure runs (79 and 86 connection-refused
failures) reran green unsandboxed with zero test changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
matt2e added a commit that referenced this pull request Sep 3, 2026
## Summary

Cuts perceived agent-mention send latency by publishing the message
first and waking the agent afterwards, instead of blocking the send on a
synchronous agent start/deploy round-trip. A send that mentions a
stopped or undeployed managed agent now shows the message immediately;
the wake runs fire-and-forget after the relay accepts the publish. The
already-running-agent send also gets faster via revalidation dedupe and
NIP-11 caching.

## Changes

### Publish-first agent wake

- Wakes for mentioned managed agents are collected during send
preparation and flushed fire-and-forget only after `await send(...)`
resolves. No start can fire — and no "your message was sent" toast can
appear — for a message the relay never accepted; every abort path
(cancel, readiness error, publish rejection, dismissed non-member
prompt) simply drops the queue. Persona-create wakes ride the pending
draft behind the non-member prompt for the same reason.
- Each wake is bound to the tenant scope captured at send time: the new
`useDetachedAgentStart` hook passes `expectedRelayUrl` +
`expectedSignerPubkey` with every start, so a wake that outlives a
community switch fails closed at the backend instead of spawning against
the new tenant. A wake whose scope has not resolved yet (identity query
still loading, blank stored relay URL) is refused with a recoverable
toast rather than fired unscoped.
- In-flight wakes are deduped through a module-level map keyed by
`(relay URL, pubkey)` — the same tenant pair the backend keys on — so
two quick sends or two composers cannot double-spawn a cold agent during
the seconds-long start window. Entries are deliberately retained across
community switches (the key *is* the tenant scope, so a retained entry
can never affect another community, and clearing it let an A→B→A round
trip deploy a provider agent twice) and self-clean when the start
settles.
- Wake-failure toasts are fenced to the community they fired in via a
module-level scope mirror: a start that settles after a community switch
logs instead of rendering community A's failure over community B's UI,
and an A→B→A return re-delivers the warning where it is actionable.
- Membership attach and access-policy writes stay synchronous, so the
harness's first kind-39002 read still sees the channel.

### Replay floor

- The send timestamp travels with the wake as `BUZZ_ACP_REPLAY_FLOOR`,
threaded through both local spawns (`spawn_agent_child`) and provider
deploys (`deploy_to_provider` injects it into `launch.policy_env`), so
the harness's startup watermark replays back past the just-published
triggering message no matter how long the spawn takes. `buzz-acp` clamps
the floor to `[now − 15min, now]`.
- The floor is captured at enqueue time, not flush time — the flush runs
post-publish, so a flush-time stamp could exceed the message's
`created_at` and skip the very message the floor exists to cover.
- On local spawns the caller's floor is asserted *after* the user env
layering (and the ambient parent-process value is stripped
unconditionally), so a saved persona/global/agent env entry cannot
shadow this send's floor — mirroring the shadow-strip the provider path
applies to `launch.env`. Both halves share one `REPLAY_FLOOR_ENV_VAR`
const.

### Send-path latency reductions (already-running agents)

- Mention revalidation is deduped: the publish-boundary pass reuses the
pre-side-effect authorization pass unless an awaited round-trip actually
separated the two (background upload, link-preview settlement, DM
expansion, a real access-policy/membership write, or active-huddle
enrollment). This preserves the #5681 authorization boundary while
making the common send single-pass.
- NIP-11 `self` lookups are cached per relay URL for 5 minutes. Only
verified values are cached — non-2xx and malformed responses stay
retryable — and URL keying keeps community switches from serving another
relay's identity.
- `applyReusableAgentAccessPolicy` now reports its relay write
explicitly (`{ agent, wrote }`) instead of signalling through object
identity, so the revalidation trigger above is load-bearing by
construction.

### File splits

Four files crossed the repository file-size ratchet during this work;
one cohesive unit was extracted from each rather than raising a ceiling
— `runtime/setup_payload.rs`, `commands/agents_create_fields.rs`,
`app_state_accessors.rs`, and `useEnsureAgentMentionsReady.ts`. The
ratchet is green at the tip.

### Review follow-ups

The three concrete findings from the first review round are fixed at the
tip: the pre-publish wake and its false "your message was sent" toast
(fixed by queueing wakes behind the publish), the stale cross-community
failure toast (fixed by the scope-mirror fence), and the A→B→A duplicate
provider deploy (fixed by retaining the tenant-keyed in-flight entries
across switches). The fast-path admission-staleness point is answered in
the review thread: deferred paths already re-validate at the publish
boundary, and the remaining fast-path window is milliseconds against an
irreducible network-transit race.

Mid-branch send-perf instrumentation was added to attribute the residual
spinner latency and reverted once that analysis concluded — it is
net-zero in this diff.

### Deferred follow-ups

Durable mention catch-up via `event_mentions` (option 2 step 3) and
backend deploy-epoch coalescing for the wake paths that do not funnel
through `useDetachedAgentStart` (Agents-panel Start, restore,
inbound-persona deploys) are intentionally left for separate changes.

## Testing

- `cargo test --lib` on desktop/src-tauri: 3054 passed; clippy `-D
warnings` + fmt clean
- Desktop unit tests: 5856 passed (the 5 failures are the pre-existing
`inboxReopenNavigation` / `useRetainedProjectGitViews` baseline, present
on origin/main); `tsc --noEmit` and biome clean
- Full mentions (87), channels (89), and community-rail (25) Playwright
smoke suites against `pnpm build:e2e` bundles, with 3× stress reruns of
each new spec
- The load-bearing regression specs were confirmed red on the pre-fix
code: publish-failure → zero starts and no false toast, the dedupe hold
(1 call vs 2), the fail-closed scope refusal, the rail-switch toast
fence, and the A→B→A retention spec (1 deploy vs 2)
- New unit coverage pins the queue contract (enqueue-time floors,
attach-seam queueing), the scope capture and verbatim relay-URL handoff,
the dedupe map's keying and settle-then-repermit behavior, the unscoped
refusal, the toast-scope mirror, the `{ agent, wrote }` contract, and
the replay-floor env layering on both spawn paths

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants