Skip to content

fix(desktop): authorize remote mentions at publication - #7124

Merged
loganj merged 13 commits into
mainfrom
split/remote-mention-routing
Sep 4, 2026
Merged

fix(desktop): authorize remote mentions at publication#7124
loganj merged 13 commits into
mainfrom
split/remote-mention-routing

Conversation

@loganj

@loganj loganj commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Summary

Buzz Desktop could hide an agent you own on another device before you could invite it, or silently drop a selected agent when its permissions changed. This lets eligible owned agents reach the existing @ menu → Send → Invite flow and checks the intended recipients again before publishing. Lost access shows an error and preserves a recoverable draft instead of sending without the agent.

  • Separate selection/invitation eligibility from publication authorization. Invite adds the agent to the channel, then refreshes membership and response permission; new DMs are checked against their actual destination.
  • Preserve selected identities through clipboard verification, attachment uploads and edits. Pending clipboard verification settles before recipient capture; edits or navigation during that wait cancel the stale attempt.
  • Tie pending invitations and draft recovery to the original composer visit and draft revision. Leaving cancels the invitation, and late completion cannot overwrite newer text, deliberately cleared drafts or a newer send. Ordinary sends remain bound to their original destination.
  • Keep Jump to latest available when the timeline has buffered messages, including after sharing a reply to the channel.

Invite is not the only choice: chat's Do nothing / Send anyway sends without inviting or notifying the nonmembers; their names remain references. Escape cancels. Invitation adds channel-wide membership, not a one-thread guest, and does not start a remote agent or guarantee a reply.

Related issue

Targets main; owned-agent discovery (#7122) is already merged. Split from #7114. #7125 depends on this PR and adds standalone forum invitations and failed-send recipient recovery. Duplicate-name binding from #7133 is already merged and retained by this stack. No new profile, presence or remote-management UI.

Testing

Focused production-hook lifecycle tests and mock-IPC browser checks passed for the published refresh, covering clipboard settlement, permission revocation and recipient/wake behavior; the frontend build/typecheck passed. Broader desktop results predate the final settlement repair and are not claimed as a new-head full-suite run. See live CI for current-head results; no full local just ci pass or native/live-relay validation is claimed.

To try it: select an owned nonmember agent in a channel or thread, send, then Invite or Escape/retry. Deny the add or revoke response permission: expect an error, no publication and a recoverable draft. During a pending send, edit or clear the source draft and navigate away/back; late completion must not overwrite that change.

Existing channel invitation dialog

Earlier mock-browser capture, not current-head runtime proof. Success/denial captures; no before-state screenshot available.

Limits: authorization reads and publication are not atomic; cancellation cannot undo accepted membership changes or dispatched messages. Draft protection is same-window. Legacy already-member agent compatibility remains unchanged: self-declared identity does not prove ownership or enable owned-nonmember invitations, and invalid verified-owner policy remains rejected.

@loganj

This comment has been minimized.

@loganj

This comment has been minimized.

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verdict: APPROVE

Reviewed f3ad8ad2d202047da1170516ba8d072ecec777e0..7ffead0f8ae128782991153054abd388e4ff04d9 (exact head 7ffead0f8ae128782991153054abd388e4ff04d9).

Risk: high — this changes mention authorization, membership mutation, publication recipients, async cancellation, and draft recovery across renderer→Tauri→relay boundaries.

Behavior/contracts traced: owned relay-agent discovery vs local custody; preparation-time invitation eligibility vs publication-time destination membership; channel/thread/new-DM destination binding; member/bot adds; notifying p recipients vs reference-only mention tags; edits and media continuation; Escape/navigation/unmount/replacement cancellation; cross-visit draft ownership and buffered timeline catch-up.

Findings: no blocking or non-blocking code/product defects found. Fresh relay evidence and fresh local-runtime evidence authorize only their own identities; unavailable directories fail closed. Selected recipients are not silently discarded. Final validation binds the eventual destination, while reference-only send strips notification recipients but retains textual references. Cancellation and late failure cannot revive abandoned intent or overwrite a newer/explicitly cleared draft.

Author action: none.

Verification owner: CI/release gate for the remaining broad/package-native confidence gaps; no author rework.

Validation at the reviewed exact head:

  • Independent systems lane: desktop typecheck passed; 87 focused revalidation/draft/edit/cancellation tests passed.
  • Independent product/UI lane: 124 focused unit tests and 18 isolated production-seam Playwright tests passed, including invite/pending/failure, policy and membership revocation, add/publish failures, both DM variants, navigation/unmount, duplicate action, reference-only/media, and cross-thread/cross-visit recovery.
  • Additional reviewer run: the full Desktop JS test command completed with 5,882/5,882 passing.
  • GitHub exact-head checks are successful: Desktop Core, all four Desktop Smoke E2E shards, Desktop E2E Relay, both Desktop E2E Integration shards and aggregate, macOS build, Semgrep, zizmor, and DCO. PR remains mergeable at the pinned base/head.

Manual/native evidence: reviewed the supplied invite, pending, and authorization-error captures; the distinct actions and failure state are clear. No independent packaged Tauri/hardware journey was completed.

Residual risk: one independent broad Desktop run timed out/interrupted without an observed assertion failure, and the first Playwright attempt collided with another reviewer server before the isolated rerun passed. Packaged-native behavior was not independently witnessed. Exact-head CI plus the isolated production-seam coverage owns those confidence gaps; neither establishes an author-actionable defect.

— :bot: Jude’s code review agent

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review verdict: approve

Reviewed the exact live head 7ffead0f8ae128782991153054abd388e4ff04d9 and integrated both assigned review lanes. No author-actionable defect remains.

The change closes the publication boundary correctly: remotely supplied mentions are re-authorized before publication rather than trusting stale draft state, while the existing local/exact-recipient behavior remains intact. Both independent lanes reported clear.

Verification

  • Full Desktop package suite at this exact head: 5,882/5,882 passed.
  • Focused changed-path evidence: 124/124 passed.
  • Browser E2E evidence: 18/18 passed.
  • Mutation/regression proof exercised the authorization fence and failed when the protective behavior was removed.

Residual native/Tauri and live-relay observation is a reviewer/release confidence gap, not author rework.

— :bot: Jude’s code review agent

@loganj

loganj commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

@jedwards27 — requesting renewed formal review of exact published head 1144465d00273cf74b7c22544ae5a3299bd98560 against split/owned-agent-discovery at 3a56d17824522580fe04cae463b54f4c7ba66021. The two earlier approvals bind 7ffead0f, not this head; they remain preserved, not dismissed.

This is the independently scoped-approved routing integration: seven original patches replay unchanged; two reconciliations preserve root's generic publication-error toast alongside authorization errors and preserve programmatic editability updates. Two added production-hook tests require the generic error, recovered draft and released pending state for normal and queued-media publication. No other source change was made during publication.

Validation/provenance is detailed in the updated PR body: 5,995 Desktop / 42 focused / 22 isolated mock-IPC browser + 1 voice-note tests passed on the integration candidate before its semantics-preserving formatting amendment; independent AST comparison plus 4 fresh final-head assertions cover that amendment. Final publication typecheck, amended-test formatting and diff checks pass. These are not live-native/relay certification, and no new full just ci execution is claimed. Exact-head hosted checks are now running. Root #7122's CI/security gates remain separate prerequisites; this is not a request to merge ahead of them.

No inline review threads or author-actionable comments existed at publication. The two older informational screenshot comments were minimized as outdated while keeping the evidence linked in the body and retaining their audit history. Formal reviews were not modified.

@loganj
loganj requested a review from jedwards27 September 2, 2026 16:56
jedwards27
jedwards27 previously approved these changes Sep 2, 2026

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verdict: APPROVE

Reviewed 3a56d17824522580fe04cae463b54f4c7ba66021..1144465d00273cf74b7c22544ae5a3299bd98560 (exact head 1144465d00273cf74b7c22544ae5a3299bd98560). This supersedes my prior approval at 7ffead0f8ae128782991153054abd388e4ff04d9.

Risk: high — mention authorization, membership mutation, publication recipients, async cancellation, and draft recovery cross renderer→Tauri→relay boundaries.

Behavior/contracts traced: reconciliation onto the newly approved discovery root; preparation eligibility vs publication-time membership; channel/thread/new-DM destination binding; member/bot adds; notifying recipients vs reference-only tags; generic vs authorization errors; edit/media continuations; Escape/navigation/unmount/replacement cancellation; cross-visit draft authority; buffered timeline catch-up.

Findings: no blocking or non-blocking code/product defects found. Range-diff preserves the prior routing/recovery series; the meaningful reconciliation delta keeps the base branch's generic formatMessageSendError behavior while retaining the specific authorization error. Publication re-fetches selected identities for the eventual destination, unavailable evidence fails closed, and selected recipients are not silently discarded. Reference-only send remains distinct from Invite and Cancel. Late cancellation/failure cannot revive abandoned intent or overwrite a newer or explicitly cleared draft.

Author action: none.

Verification owner: CI/release gate for completion of any still-running required jobs and any desired packaged-native/live-relay observation; no author rework.

Validation at the reviewed exact head:

  • Two independent full Desktop JS runs passed 5,995/5,995; exact-head E2E app build passed.
  • Isolated remote-owned-mentions.spec.ts passed 18/18, covering revocation/no-send, invite/reference-only, delayed add/publish cancellation, A→B→A draft authority, media, and catch-up routing.
  • Additional focused rerun passed 42/42 cancellation/recovery/publication-error tests.
  • git diff --check and review preflight passed; base/head remained exact, clean, mergeable, with no unresolved review threads.
  • At submission, relay/integration E2E, both integration shards, macOS and Windows builds, Semgrep, zizmor, and DCO were green; Desktop Core and four smoke shards were still running, with no failed required gate.

Manual/native evidence: source and renderer/browser evidence cover the visible states and production renderer flow. No independent packaged Tauri/live-relay native authorization journey was completed for this changed head.

Residual risk: browser tests use mock IPC and therefore do not independently certify the live Tauri relay-directory query. Initial focused Playwright attempts collided with an unrelated process on port 4173; the isolated port 4184 rerun passed. These are confidence/tooling gaps, not author-actionable defects.

— :bot: Jude’s code review agent

loganj added a commit that referenced this pull request Sep 2, 2026
🤖

## Summary

An agent you own could be missing from **New message → To:** and
**Channel members → Add people and agents** on a machine that has never
managed it. This PR lets those existing lists find your agent without
requiring a shared channel first. Desktop now checks records proving you
own it, rather than looking only at agents in channels you've already
joined.

**No new screen or control is added.** For example, an agent with
verified ownership and **Who can send instructions → Only me (default)**
can now appear even with no shared channels. Each screen still applies
its existing access rules; this does not make every discovered agent
selectable everywhere.

| Screen / control | Before | After this PR alone |
| --- | --- | --- |
| **New message → To:** recipient picker | An owned agent absent from
this machine and shared-channel bot lists could be missing. | Its named
**agent** row can appear; selecting it adds a recipient chip. This is
recipient selection, not a guarantee that a later message will reach or
wake the agent. |
| **Channel members → Add people and agents** | The same agent could be
missing from **Not in this channel** search results. | Its row can
appear with the existing **Add** button. If you can add members, that
button submits the existing channel-membership request; finding the row
alone changes no membership. |
| **Stream / forum composer → @ suggestions** | An owned agent already
in the channel under an ordinary member role could be missing from agent
suggestions. | Its actual membership is recognized without requiring the
bot role. Agents not managed on this device still need membership in
that channel. |
| **Pulse → Agents** | An agent absent from both local management and
the server's agent list was omitted from the count and author lookup. |
The count and feed's author lookup can include it; notes appear only if
it has published them. |

Being listed does **not** mean the agent is online, add it to a channel,
or grant local Start/Edit controls. For agents not managed on this
device, global **Search** still excludes those configured for “Only me”,
and DM @ selection is not added here. DM @ selection and message-driven
nonmember invitation are addressed in
[#7124](#7124); the standalone forum
**Invite / Cancel** flow is in
[#7125](#7125).

<details>
<summary>Ownership and membership checks</summary>

A discovery lead is not proof: the latest agent profile must have a
valid signature and exactly one valid ownership attestation—the owner's
signed link to that agent. Its response policy must be signed by that
verified owner; an invalid latest policy cannot restore an older
permission. Membership comes separately from the latest server-signed
roster, including removals.

Existing profile cards, owner labels and agent-avatar shapes also use
this stricter verification: malformed or forged evidence must not supply
ownership/agent classification on its own. Valid ownership was already
recognized; no profile-picture or badge design changes.

Attestation time conditions apply to the signed event's timestamp, not a
live expiry timer. Existing legacy compatibility and builds requiring
verified owner policy retain their respective rules. Discovery and
sending remain separate operations, not an atomic permission check.

</details>

### Review corrections

- When runtime and owner policy overlap, **explicit online/away/offline
from the verified latest runtime is retained**. Policy still supplies
ownership/permissions; claimed runtime membership is not restored.
Missing/unrecognized status stays unknown, and invalid latest policy
cannot revive runtime permissions.
- Discovery without runtime evidence is now **unknown**, not offline:
native conversion, both IPC adapters, Pulse, Projects and
profile/session consumers preserve that distinction. Unknown has no
status dot and is not promoted to a deployed/running agent.
- Both relay-only picker paths retain the authenticated owner, including
the existing **managed by you** label. The analogous global Search
projection is fixed without changing its existing “anyone” filter.
- Authorized stored profile activity remains visible when liveness
becomes unknown/absent or the active turn ends. History reads do not
start a live subscription, grant access, or imply current availability.

### Related issue

Independent base: `main`. Child:
[#7124](#7124), then
[#7125](#7125). Extracted from
[#7114](#7114), retained as historical
source (`98fe33ec`).

[Behavior
contract](https://github.com/block/buzz/blob/3a56d17824522580fe04cae463b54f4c7ba66021/docs/owned-agent-discovery.md).
Originating [Buzz
discussion](buzz://message?channel=f7a9536a-1738-4bad-a888-b3ea25010ef1&id=7aa1f0ab23dce514bd8a0221441cf005bf428914621171472b79747c50820848)
· channel `f7a9536a-1738-4bad-a888-b3ea25010ef1`.

### Testing

Current candidate: `3a56d17824522580fe04cae463b54f4c7ba66021`, a
four-file native/test/doc runtime-status repair atop published
`ae23c1c9680a881cee7eed94e259bf15bf8ce3f7`. Branch ancestry is main
`1c8321cd08feb597f8bcff5195c21148fb3e98ed`; refreshed main
`0e878664b08cdf7fb2d89d940bc2aa92cdc485f7` adds only the independent
CI-workflow split. Read-only mergeability succeeds; this is not a tested
merged-tree claim.

**Local CI attempt and continuation (not an uninterrupted green run):**
the new exact-head `just ci` passed formatting/static checks, workspace
and Tauri clippy, workspace Rust tests, **5,910 desktop tests**, desktop
production build and Tauri check. Its native main target finished
**3,073 passed / 1 failed / 19 ignored** (exit 101):
`cheap_discovery_reports_absent_before_any_forced_probe` saw a
process-global login-shell counter of 2 instead of 0. The counter
includes unrelated version/adapter probes whose tests do not hold the
failed test's PATH mutex; no managed-agent discovery implementation
changed in the runtime repair. The unchanged failing test then passed
**three isolated invocations**. Only the failed native workspace lane
was retried with `RUST_TEST_THREADS=1 just desktop-tauri-test`: **3,074
main-target tests passed / 19 ignored**, all additional workspace
targets passed (exit 0). The previously unrun `just web-build
mobile-test` tail then passed (exit 0; **2,019 mobile tests**). Earlier
successful lanes were reused; no source/guard changes or blanket CI
rerun. The original failure and all diagnostic/retry logs are retained.

- **71 native `nostr_convert` tests pass**, including seven new
production merge regressions: online/away/offline, missing/invalid
status, policy-only, status-less latest replacement and forged latest
replacement. Before production repair, those seven yielded **4 failures
/ 3 passing controls**.
- Reused frontend evidence from `ae23c1c9` (frontend is unchanged):
Desktop TypeScript and isolated E2E build pass; **9 browser tests / 0
retries**, covering both relay-only picker journeys and seven adjacent
stop-control regressions. Real UI with mock Tauri IPC, not live
relay/native webview.
- Earlier `ae23c1c9` local `just ci` passed without failures, including
3,067 native main-target tests / 19 ignored and 2,019 mobile tests; not
substituted for the new source gate above.
- Reused unchanged repair evidence: **17 real-store/hook history
regressions**, **161 focused tests**, and independent **9 mounted
owner/bot/identity revocation/regrant transitions** with zero hook-phase
native calls. The regression was falsified before repair (14 failures, 3
controls).
- Signed local-server fixtures cover discovery with no local/shared
record, ordinary-role membership, forged ownership, invalid signatures,
duplicate authentication, wrong-owner/latest-invalid policy, revoked
membership and wrong destinations. These establish native data checks,
not a live agent response.

GitHub checks and renewed technical/security review must apply to the
current published head; earlier-head green checks are not
replacement-head proof. Local source review is not formal
code-owner/latest-push approval or exact-range security authorization. A
green security workflow with substantive review skipped is not security
clearance.

### Screenshots

#### Relay-only picker evidence —
`ae23c1c9680a881cee7eed94e259bf15bf8ce3f7`

These cropped rows come from the two real production picker journeys in
[`owned-agent-discovery.spec.ts`](https://github.com/block/buzz/blob/ae23c1c9680a881cee7eed94e259bf15bf8ce3f7/desktop/tests/e2e/owned-agent-discovery.spec.ts),
using mock Tauri IPC with **no local agents and no user-search
duplicate**. The fixture supplies verified-owner data and unknown
availability; the browser test checks its presentation, not native
signature verification. Both exact-tip journeys pass without retries. No
live relay, native webview, invitation, delivery or wakeup is claimed.

Before the repair, both relay-only candidate constructors discarded the
owner, so the existing “managed by you” label was absent. These are
after-repair captures; no before image was captured.

#### New Message → To
The relay-only agent retains its authenticated owner label.


![new-message-owner](https://raw.githubusercontent.com/block/buzz/536c6d3c90776cf85b1d5ce58666f2c8ad518829/pr-7122--new-message-owner.png)

#### Channel members → Add people and agents
The matching result retains “managed by you” beside the existing Add
action; the test does not click Add or claim membership changed.


![member-add-owner](https://raw.githubusercontent.com/block/buzz/536c6d3c90776cf85b1d5ce58666f2c8ad518829/pr-7122--member-add-owner.png)

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Base automatically changed from split/owned-agent-discovery to main September 2, 2026 20:18
@loganj
loganj dismissed jedwards27’s stale review September 2, 2026 20:18

The base branch was changed.

@loganj
loganj requested a review from jedwards27 September 2, 2026 20:19
@loganj
loganj enabled auto-merge (squash) September 2, 2026 20:20
@loganj

loganj commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator Author

Pending base-change re-review: unchanged routing head

The remaining gate is the already requested automated review after #7122 merged and this PR was retargeted to main. Head remains 1144465d00273cf74b7c22544ae5a3299bd98560; no new routing code or rebase is being proposed.

The timeline records that exact-head approval 5092775370 was dismissed at 2026-09-02 20:18:58 UTC with “The base branch was changed.” The replacement request at 20:19:26 remains pending. All current checks are successful or legitimate path skips, and GitHub reports MERGEABLE, but REVIEW_REQUIRED/BLOCKED. This does not restore or bypass the dismissed approval.

@jedwards27 — please complete the existing automated request against the current main comparison, reusing your valid unchanged technical evidence and checking the base-change delta rather than repeating the whole feature audit. Your prior exact-head verdict reported no author action; no new finding has appeared in the PR. This is not a duplicate requested_reviewers action or a request to merge.

@loganj
loganj force-pushed the split/remote-mention-routing branch from 1144465 to e66b949 Compare September 3, 2026 02:54

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verdict: APPROVE

Reviewed ee883d73fb84752d26f52c6606ad3cadc9f12cc7..0b303f805e4420eae682087555075c5a7472e40e at exact head 0b303f805e4420eae682087555075c5a7472e40e.

Risk: high — publication-time authorization crosses renderer state, Tauri IPC, destination-scoped relay policy, cancellation, edit semantics, and draft recovery.

Changed-head reconciliation: range-diff against the prior approved stack found commits 2–12 patch-equivalent, patch 1 reconciled onto the new base, and one new patch adding a production-seam regression test for edit authority. The broad prior-head comparison is rebase noise rather than thousands of lines of new routing behavior. No production behavior changed after the stale approval.

Behavior/contracts traced: fresh exact-recipient revalidation immediately before publication; captured destination/thread binding; source-visit and authored-revision fencing; cancellation after relevant awaits; renderer → Tauri → relay selected-key propagation; relay-derived owner/policy and channel-membership filtering; edit snapshots and revalidation of exact selected agents plus typed recipients; notifying-versus-reference tag behavior; invite/reference-only/Cancel/error/retry; media/edit continuation; navigation, deletion, unmount, A→B→A recovery; keyboard/focus, wrapping, and catch-up.

Findings: no blocking or non-blocking code defect found. The new test observes intendedAgentPubkeys at the production edit seam and exact saved reference/notifying recipients. Removing that production argument made the test fail, establishing causal coverage.

Author action: none.

Validation at exact clean head:

  • Full Desktop JS suite: 6,409/6,409 pass
  • pnpm typecheck: pass
  • pnpm build:e2e: pass
  • Focused authorization/edit/cancellation/recovery suite: 101/101 pass
  • Production-renderer remote-owned mentions: 18/18 pass
  • Focused production-built mention/remote-owned Playwright: 39/39 pass
  • Causal mutation of edit intendedAgentPubkeys: 0/1 pass as expected; restored tree passed and remained clean
  • git diff --check: pass; all 13/13 commits carry sign-off
  • Live API head/base matched and GitHub reported the PR mergeable before submission

One initial broad three-spec Playwright attempt overcommitted the local preview server and ended in ERR_CONNECTION_REFUSED after 45 passes; the focused serial production-built rerun passed 39/39. This is local harness/resource noise, not a product failure.

CI/release evidence: macOS and Windows builds, relay/integration jobs, all four smoke shards, Semgrep, zizmor, and DCO are green. Desktop Core remained in progress at submission.

Manual/native evidence: packaged Tauri/live-relay interaction and direct assistive-technology observation were not independently rerun.

Residual risk / verification owner: CI owns terminal Desktop Core. Release/manual QA owns any packaged-native, live-relay rollout, and direct assistive-technology observation still required. These are confidence gaps, not author-actionable defects. Any head movement invalidates this approval.

@loganj
loganj merged commit d595806 into main Sep 4, 2026
57 checks passed
@loganj
loganj deleted the split/remote-mention-routing branch September 4, 2026 16:35
loganj added a commit that referenced this pull request Sep 4, 2026
## Summary

In Desktop's standalone Forums, selecting an owned agent from another
device could leave a post or reply unsendable if the agent had not
joined the forum. This adds **Invite / Cancel** to the send flow so you
can resolve membership without leaving your draft.

- **Invite** checks response policy and your permission to add members,
adds the agent to the forum, waits for refreshed membership, then
rechecks authorization before posting to the original destination.
Membership is forum-wide, not limited to one post.
- **Cancel / Escape** keeps the text, attachments and selected
recipients for retry. Unlike chat's **Do nothing / Send anyway**, this
dialog has no reference-only send choice. Invite is disabled while
pending; Cancel remains available.
- Leaving the source post/reply cancels its pending invitation, even if
you return. Errors remain visible, focus returns to the initiating
editor when appropriate, and late completion cannot resume a cancelled
post or interfere with a newer attempt.
- A rejected send restores text, uploaded media and exact selected
recipients to the source draft only if no newer edit, deletion, upload
intent or send supersedes it. Clipboard verification settles before
recipient capture, with stale edits/navigation fenced out.

### Related issue

Targets `main` after #7124 merged. This PR reuses its publication checks
and draft protection; the five forum commits have been replayed
unchanged onto the merged parent. Owned-agent discovery (#7122) is
already merged. Split from #7114.

Forum creation/templates, channel-less Notes and local-agent management
are unchanged. Duplicate-name binding from #7133 is already merged and
retained by this stack. Inviting does not start a remote agent or
promise that it is online or will reply.

### Testing

Forum composer lifecycle tests, including clipboard-settlement cases,
and TypeScript/changed-file formatting checks passed after the restack.
Earlier invitation, focus and transport-recovery browser coverage is
retained, not claimed as a fresh full browser run on this head. See
[live CI](https://github.com/block/buzz/pull/7125/checks) for
current-head results. Browser evidence uses mock IPC; no full local
`just ci` pass or native/live-relay validation is claimed.

To try it: open a forum post or reply, select an owned nonmember agent
and send. Cancel, then retry without reselecting; Invite should add that
agent before posting. Deny the add to check the visible error and
retained draft. Navigate away/back during a pending invitation or
rejected send; no stale publication or overwrite of a newer draft should
occur.

![Forum invitation with Invite and
Cancel](https://raw.githubusercontent.com/block/buzz/6481bd088d661975672addcc834231df6ac88705/pr-7125--forum-invite.png)

*Earlier mock-browser capture, not current-head runtime proof. [Error
and successful-post
captures](#7125 (comment));
no before-state/native capture available.*

**Limits:** cancellation cannot undo accepted membership changes or
dispatched posts; authorization and publication are not atomic. Recovery
is same-window, subject to browser storage limits, and is not a durable
in-flight send journal: reload/crash can lose a pending snapshot.
Cross-window coordination and in-flight upload custody are unchanged.
The parent's legacy member-agent compatibility does not establish
ownership for nonmember invitations.

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
nambse pushed a commit to nambse/buzz that referenced this pull request Sep 4, 2026
…lock#7133)

## Summary

Selecting two people or agents named Scout could replace the first
recipient with the second even though the message still looked right.
This binds each selection to its exact identity: the first keeps
`@Scout`, and a conflicting selection gets `@Scout (<full public key>)`.
Removing one no longer removes or redirects the other.

- Reuse the existing **@ suggestion list**; team selection and automatic
agent addressing reserve and reuse distinct labels too. Typing an
ambiguous name manually shows an instruction to use the picker and
preserves the draft without publishing—in chat, edits and standalone
forums.
- Share literal mention matching across recipient extraction, removal,
display and editing so a shorter name cannot claim another recipient's
longer or qualified label.
- Rebuild authored references when a message is edited, and use the
latest authorized snapshot when reopening or forwarding.
Rendering/editing can recover qualified identities only from the
message's recorded references, not from a key typed into its body.
Unresolvable historical names remain literal rather than guessed
recipients.
- Wrap full-key labels within narrow/zoomed layouts while keeping their
complete accessible label and ordinary mention icons. Edit activation
waits for the action menu's focus cleanup before focusing the editor.

### Related issue

Targets `main`; mention spacing (block#7128) is already merged. Split from
block#7114. This is independent of the block#7124block#7125 remote-invitation stack
and does not expand agent eligibility or invitation permissions. The
separate Enter-selection suffix issue remains tracked in block#7253.

### Testing

The desktop unit suite and focused mock-Chromium checks passed on the
published integration candidate, including pending-paste selection,
edit/forwarding, copy and narrow-layout cases; formatting, types and
frontend builds passed. On `0b3b18c0`, 64 focused trust/paste/selection
unit tests and six mock-Chromium tests passed with zero browser retries,
including actual timeline chip copy → fresh channel paste → send and
mismatched-key rejection; TypeScript, changed-file Biome and an isolated
E2E build passed. The broader browser run had copy failures before the
focused repairs and is not claimed as wholly green. See [live
CI](https://github.com/block/buzz/pull/7133/checks) for current-head
results. No full local `just ci` pass, native/live-relay or
cross-browser validation is claimed.

To try it: select two same-name recipients, remove one, send, then
edit/reopen and forward; only the intended identities should remain.
Type an ambiguous name without choosing a suggestion and check that
sending retains the draft with an error. Inspect full-key labels in a
narrow window at 150% text size, and open Edit and type immediately.

![Ambiguous mention keeps the chat
draft](https://raw.githubusercontent.com/block/buzz/c48053209b36b3b1bf7197cf2d606d27b48a27c0/pr-7133--ambiguous-general.png)

![Full-key label in a narrow, zoomed
composer](https://raw.githubusercontent.com/block/buzz/ac262f00de6fe3860bff2f87dc0e98165ed6b651/pr-7133--layout-1.5-composer.png)

*Earlier mock-browser captures, not current-head runtime proof. No new
before-state capture; screenshots alone do not prove recipient
delivery.*

**Clipboard trust:** generated full-key-qualified mentions now retain
their exact recipient after copy/paste when the full key matches the
clipboard record and community directory/profile state independently
vouches for the base alias, including numeric collision suffixes. A
qualifier alone does not establish trust. Arbitrary historical labels
still cannot always be reconstructed.

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-authored-by: Bad Janet <150b20bdf6130418df9239dd1bd082c71612c8d653b47c277200365b9be215dc@buzz>
(cherry picked from commit ee883d7)
Signed-off-by: nambse <sefa.esendemir@gmail.com>
nambse pushed a commit to nambse/buzz that referenced this pull request Sep 4, 2026
🤖

## Summary

In Buzz Desktop, you could own an agent running on another device but be
unable to mention it in a channel where it had not yet joined: it was
filtered out before you could invite it. A selected agent could also
disappear from the message's recipients when permissions changed. This
lets you select an eligible agent in the existing **@ menu**, invite it
from the message composer, and send to that agent—or see an error and
keep your draft rather than silently sending without it.

#### Where the experience changes

| Screen / control | Before → after |
| --- | --- |
| A channel's **Message #…** composer, or a message's **Reply in thread
to …** composer | Type `@` (or use the existing @ button), choose your
agent, write the message and press **Send message**. An owned agent not
yet in the channel can now reach the existing **“Mention people outside
this channel?”** dialog when its response settings allow you to address
it. |
| That dialog's **Invite** button | Previously the membership
requirement could block the agent before the invitation. Now Invite
checks permission to add it, adds it as an agent member of the
**channel** (not just the thread), then rechecks membership and response
permission before sending the waiting message. An agent already in the
channel needs no invitation. |
| Existing direct message, or the new-message screen with the **To:**
field | A mention is checked against the conversation the message will
actually enter, including a newly created direct message, rather than
the old or not-yet-created destination. This does not add an Invite
control to direct messages. |
| Editing a message / sending attachments | The selected agent remains
part of the send or edit attempt through attachment upload and the final
permission check. Lost permission produces a visible error instead of
dropping that recipient. |

**Invite is not the only chat choice.** The existing **Do nothing**
button sends the message *without inviting or notifying the nonmembers*;
their names remain references in the text. Where you cannot invite, that
choice is labelled **Send anyway**. To abandon the send instead, dismiss
the dialog with Escape. Invitation actions are disabled while
preparation is pending, preventing duplicate clicks.

**Leaving and returning must not resurrect a cancelled send.** Switching
threads or leaving the composer cancels its pending invitation, even if
you return to the same thread. Cancellation before dispatch sends no
message; an accepted membership change cannot be automatically undone.
An ordinary send without a pending invitation remains bound to its
original destination rather than following you into another
conversation.

**Failed sends must not overwrite your next draft.** If you leave a
thread, return and replace or deliberately clear its draft while an
older send is pending, the older failure cannot restore deleted text,
recipients or files; success cannot erase the newer draft—even if its
text is identical. An untouched draft cleared automatically for sending
remains recoverable on failure. This protection also covers reopening
the composer and starting a newer send.

The channel timeline also keeps its existing **new-messages / Jump to
latest** button available when newer messages are waiting to be
displayed. For example, after sharing a reply to the channel and closing
the thread panel, you can click the catch-up button to reveal buffered
messages. Closing the thread does **not** guarantee the shared row
appears automatically or force you away from reading history.

### Related issue

Built on [block#7122](block#7122), base branch
`split/owned-agent-discovery`, which lets Desktop find and verify owned
agents independently of this device. Current integration head:
`1144465d00273cf74b7c22544ae5a3299bd98560`, built on exact published
root `3a56d17824522580fe04cae463b54f4c7ba66021`. Root block#7122 has its own
CI and security gates; this PR must not land ahead of that dependency.
Finding an agent is not channel membership, online status or a promise
of a reply. This PR changes what the existing message controls can do
with those agents; it adds no profile, presence, cloud marker or remote
start/stop UI.

Standalone forum post/reply **Invite / Cancel** is added separately in
[block#7125](block#7125); here those composers
only gain visible authorization errors. Same-name selection/binding
fixes ([block#7133](block#7133)) and mention
spacing ([block#7128](block#7128)) are not
included.

Extracted from [block#7114](block#7114)
(historical source `98fe33ec`). [Behavior and draft-recovery
contract](https://github.com/block/buzz/blob/1144465d00273cf74b7c22544ae5a3299bd98560/docs/remote-mention-routing.md)
· [Originating
discussion](buzz://message?channel=f7a9536a-1738-4bad-a888-b3ea25010ef1&id=7aa1f0ab23dce514bd8a0221441cf005bf428914621171472b79747c50820848).

### Testing

![Channel composer after Send: RemoteScout is not a channel member;
Invite adds it before sending, while Do nothing sends without inviting
or notifying
it](https://raw.githubusercontent.com/block/buzz/b20345da0bd648ecbc78988637c89893a33ca2f3/pr-7124--remote-invite.png)

*Earlier candidate, mock desktop browser: the existing channel dialog
now reachable for an eligible owned agent on another device. The two
buttons have different send outcomes; Do nothing is not Cancel.*
[Success and denial
captures](block#7124 (comment))
· [Pending-state
capture](block#7124 (comment)).
These show the relevant UI, not live agent availability, native
authorization or the later draft-storage/catch-up repairs. No
before-state screenshot is available.

Existing coverage exercises exact recipients, invitation
rejection/cancellation, new direct-message destinations, uploads, edits,
thread re-entry and stored-draft deletion. The timeline regression
checks the shared reply becomes visible using the available catch-up
action.

**Integration validation (2026-09-02):** independently reviewed the
routing delta onto root `3a56d178`: seven original patches unchanged;
two reconciliations retain generic publication-error toasts alongside
authorization errors and retain non-authored editability updates. Added
two production-hook regression tests (normal and queued-media
publication) requiring visible generic error, recovered draft and
released pending state.

- Writer validation: **5,995 Desktop tests**, **42 focused tests**, **22
mock-IPC browser journeys** (18 routing, 2 root provenance, 2
destination binding), and **1 voice-note failure journey** passed; lint,
types, size guards and E2E build also passed.
- The broad suite ran before the final formatting-only test amendment,
not as an exact-final-head rerun. Independent AST comparison confirmed
that amendment is semantics-preserving; **4 fresh assertions at final
`1144465d`** passed. Publication rechecked final-head TypeScript,
amended-test formatting and `git diff --check` successfully. No new full
repository `just ci` run is claimed.
- Browser tests use an isolated E2E build and **mock IPC**, not live
relay/native authorization. Historical screenshots above are explicitly
earlier UI evidence, not exact-head runtime certification. Packaged
Tauri/live-relay behavior was not independently witnessed.
- **Published-head gates:** [current CI
run](https://github.com/block/buzz/actions/runs/33657948560) and
[renewed exact-head formal review
request](block#7124 (comment))
must clear before landing. [Earlier CI
run](https://github.com/block/buzz/actions/runs/33438436438) and the two
earlier approvals cover `7ffead0f`, not this new head. Root CI/security
clearance remains separate; the independent scoped integration approval
is not merge authorization.

To try it: in a channel or thread, select an owned nonmember agent,
Send, then Invite or Escape and retry. Deny the add or revoke its
response permission before sending: expect a visible error and
recoverable draft, not a message missing the agent. During a pending
send, return to the source thread, edit or clear the draft, then leave
again: late completion must not overwrite that choice.

**Limits:** permission checks and sending are separate operations;
cancellation cannot retract a dispatched message. Draft protection is
same-window, not new cross-window deletion synchronization. Standalone
forum transport failure can still restore text/media without the exact
selected recipients. Native compatibility is inherited: open-source
builds may still recognize a valid legacy, self-declared agent already
in the channel when verified ownership is absent or rejected; that does
not establish ownership or unlock this owned-nonmember invitation path.
Invalid policy from a verified owner is still rejected. No agent
response is guaranteed.

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
(cherry picked from commit d595806)
Signed-off-by: nambse <sefa.esendemir@gmail.com>
This was referenced Sep 5, 2026
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.

2 participants