Skip to content

feat(agents): share agents with people, channels and the Marketplace - #1834

Open
tulsi-builder wants to merge 14 commits into
mainfrom
basil/share-dialog-polish
Open

tulsi-builder wants to merge 14 commits into
mainfrom
basil/share-dialog-polish

Conversation

@tulsi-builder

@tulsi-builder tulsi-builder commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Brings back old Buzz's agent sharing on top of #1649. Share in an agent card's ⋯ menu now opens one window to send the agent to people or channels, publish it to the community Marketplace, or export it.

Share window

  • Search people and channels and send the agent to them. People get it in a DM; channels get it in the channel. One upload serves every recipient.
  • The Marketplace switch is applied on Done, not when it's toggled. A failed Done keeps the requested state so Done can be pressed again.
  • The label reads "Published to Marketplace" once listed. Update Marketplace appears when the listing is older than the agent.
  • The success toast offers View, which opens the Marketplace tab with the listing selected and scrolls to its card.
  • Export saves an .agent.png card (the agent's details embedded in the PNG) or .agent.json as a backup. Pack avatars draw from the app's own copy.
  • An unconfirmed send is remembered per recipient and per shared content. A plain retry confirms the earlier message instead of sending another. An edited agent is sent as a fresh copy.

In chat

  • An *.agent.png attachment renders as the Agents page card (same size), through registerAttachment. MessageRow now lets plugins draw images they claim, outside the image gallery.
  • Choosing the card opens the import dialog.

Import dialog (Marketplace, chat and file)

  • Laid out like the Identity tab: the title is the agent's name, Harness and Model can be changed in place, and Instructions sit below.
  • The description explains "import it as is, or edit anything below first". An edited copy says "Edited" and offers Undo changes.
  • If the shared agent matches one you already have (same name, harness, model and instructions; the picture is ignored), it says "You already have this agent." and offers Open {name} beside Import a copy.
  • Chat imports show "Added {name} to your agents." with Open. Open uses a new { agent: pubkey } route on the Agents page.

Security policy

connect-src now admits buzz-media: and http://buzz-media.localhost, and nothing else. This lets chat cards read their embedded definition in packaged builds. The media scheme already returns Access-Control-Allow-Origin for the requesting window only, so the window could already read those bytes; docs/identity.md is updated to match. General HTTPS stays closed.

No FOUNDATION files are edited.

Screenshots

Simulated with fictional people, channels and an agent in a component harness (no real data). Rendered by the shipped components.

Share: choose who to send to Share: published to the Marketplace
Share window with a recipient chosen Share window, published to Marketplace
Share: dark theme A shared agent in chat
Share window in dark theme Agent card in a message
Import Import: edited, with Undo Import: an agent you already have
Import dialog Import dialog after editing Import dialog offering Open

Validation

  • Full Vitest at 5bdada027: 772 files, 12,484 tests pass (16 skipped). tsc, biome and git diff --check pass.
  • Agent code review (Crack Catcher, at 4c166180b) found two blockers, both fixed in cd6e8f28b and rechecked with no new findings:
    • the packaged CSP blocked reading cards in chat;
    • a retry after editing re-sent the old copy.
  • CSP: the exact tauri.conf.json policy was loaded in headless Chromium. Before the change, both media URL forms raised connect-src violations; after it, neither does, and unrelated HTTPS is still blocked.
  • A debug bundle (just desktop-bundle at cd6e8f28b) embeds the new policy. The human author opened it and confirmed card → details → import and Open work.
  • The Share and import layouts were checked in a browser harness, in light, dark and 420px narrow views.
  • New regression tests:
    • duplicate and edited/Undo states
    • routed agent selection (fails with the effect removed)
    • edited resend (fails with the old key)
    • chat card toast and Open
    • scroll-to-selection on View

No browser journeys were added or removed.

Deferred checks

  • Human check of the View → scroll fix (5bdada027), which landed after the packaged-build test.
  • Enlarged-text and full keyboard pass on the Share and import dialogs in the native app.
  • Sending to real recipients across communities.
  • Windows and Linux packaged builds.

Follow-ups, not in this PR: a designed export card and Marketplace search.

🤖 Generated with Claude Code

tulsi-builder and others added 12 commits October 28, 2026 09:59
Pack avatars live on a CDN that sends no CORS header, so the export canvas
couldn't read them and every such agent exported a blank 1×1 image. Draw
the app's bundled copy instead, as the avatar palette already does.

Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: tulsi <tulsi@block.xyz>
Publishing and removing now go through a review step inside the same
window that shows the instructions being shared, instead of a switch
that publishes immediately. Each row keeps its own status and errors,
so a Marketplace read failure no longer blocks exporting. Export saves
the picture card by default, with JSON as a fallback link.

Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: tulsi <tulsi@block.xyz>
An update leaves a listed agent listed whether or not it landed, so
success now requires the winning head to carry what was sent. Returning
from a step focuses the button that opened it, announces the outcome
as a change, and shows why an agent can't be shared inside the review.

Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: tulsi <tulsi@block.xyz>
Share now follows the share-chat window: choose people at the top, with
Post to Marketplace as a switch below and Export in the footer. Each
person gets a direct message with the agent's .agent.png attached, from
one upload; a retry confirms an unconfirmed message instead of sending
another, even after the window is reopened.

Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: tulsi <tulsi@block.xyz>
Share now opens on one search for people and channels, with the
results between it and the Marketplace switch, as Share access does.
A channel gets one message in it with the agent attached; agents are
not offered, since a copy is for people to add.

Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: tulsi <tulsi@block.xyz>
Post to Marketplace now acts directly from the switch, without a review
step. Export is named "Export agent card", with a note that the image
holds the agent's setup. A sent agent card renders in conversations as
the Marketplace card and opens Import.

Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: tulsi <tulsi@block.xyz>
Messages handed only audio to attachment plugins, so a sent agent card
still showed as a picture. A plugin can now draw an image it claims,
standing apart from the gallery around it.

The Marketplace switch now asks, and Done publishes or removes; a failed
attempt keeps the switch where it was asked, so Done tries again. List
rows span the search field's width, matching the Marketplace group.

Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: tulsi <tulsi@block.xyz>
The import window takes the agent's name as its title and lays out
Harness, Model and Instructions as the Identity tab shows them, each
changed in place. The Marketplace switch reads "Publish" or "Published"
by what the relay confirmed, and a publish's toast opens the agent in
the Marketplace. Sent cards keep the Agents page card's size, the
recipient list softens into its edge, and the footer sits closer.

Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: tulsi <tulsi@block.xyz>
Drop the soft fade above the Marketplace group: nothing else in the app
uses one.

The import dialog now says it can be imported as is or edited first,
marks an edited copy, and offers Undo changes. When a shared agent
matches one you have in name, harness, model and instructions, it says
so and offers Open beside Import a copy. Cards in chat open that agent
on the Agents page, through a new { agent } route, and say what they
added with an Open action.

Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: tulsi <tulsi@block.xyz>
Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: tulsi <tulsi@block.xyz>
The packaged CSP kept connect-src closed to the buzz-media scheme, so a
card in chat could not be read and fell back to a plain picture. The
scheme already lets the requesting window read its bytes; admit it, and
only it, to connect-src.

An unconfirmed send was remembered by agent and recipient alone, so
sending an edited agent to the same person confirmed the older copy.
Remember it, and the uploaded card, by the shared content too.

Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: tulsi <tulsi@block.xyz>
View selects the listing on the Marketplace tab; the grid now scrolls its
card into view once it loads. A card chosen in the grid is already in view
and stays put.

Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: tulsi <tulsi@block.xyz>
@tulsi-builder
tulsi-builder marked this pull request as ready for review October 28, 2026 16:59
@tulsi-builder
tulsi-builder requested a review from a team as a code owner October 28, 2026 16:59

@kalvinnchau kalvinnchau 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.

🤖 Found two P2 defects in reconnect retry bookkeeping and automatic card downloads, plus a P3 recovery issue.

Comment thread src/bundled/agents/AgentShareDialog.tsx Outdated
setTimeout(() => URL.revokeObjectURL(url), 1000);
}

const pendingSends = new WeakMap<

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.

🤖 [P2] Preserve unconfirmed sends across replacement sessions

After an unconfirmed send, reconnect creates a new RelaySession while the durable outbox survives by transport partition. This WeakMap loses the earlier message ID, so reopening Share and retrying sends another message while the original remains queued. A focused component reproduction replacing the session object after the unconfirmed send observed two messages.send calls instead of one. Key/persist pending confirmations by the stable community partition and extend the existing window-close retry test to cover session replacement.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Fixed in 085a830. Pending sends are now keyed by session.scope (the transport partition: community origin + viewer), which stays the same when a reconnect replaces the session, so a retry still confirms the queued message. The window-close test now also swaps in a new session object with the same scope; it fails with the old per-session WeakMap.

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.

🤖 [P2] Pending Share state still diverges from the durable outbox at 085a830 (AgentShareDialog.tsx:114–123, 296–330). Session replacement is repaired, but reload is not: withhold acknowledgment/observation until delivery is unknown, reload, then release the upstream event and retry the unchanged agent to the same channel. Actual-app Chromium and WebKit runs with production broker and modeled upstream both restored the original unknown outbox ID, then published a second kind-9 ID and uploaded the identical PNG again.

The same parallel Map can retain an ID after Remove from outbox or partition purge. Static tracing plus a component reproduction with modeled journal-miss rejection shows subsequent retries only call delivered on the stale ID, repeatedly advising reconnect without sending; reconnect now preserves that stale entry. This dismiss path has not been exercised end to end.

Use the existing durable OutboxRecovery correlation (relay/messages.ts:209–233; relay/outbox.ts:720–726), recover the matching outbox operation on retry, send fresh when it is absent, and acknowledge successful domain completion. Add reload and dismiss/purge coverage. This continues the existing ownership/persistence finding; the bounded card reads and transient-fetch retry repairs passed their targeted checks.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Fixed in a8169b9. Share no longer keeps a parallel map. Each send now carries an OutboxRecovery key built from agent-share:<agent>:<recipient>:<sha256 of the exact copy>. On Send, Share awaits outbox.ready() and then looks for that key in outbox.snapshot():

  • Found: it confirms that message through directMessages.delivered, so the outbox retries the same event id. No new kind-9 is sent and the PNG isn't uploaded again, even after a reload.
  • Absent (removed from the outbox, or never sent): it sends fresh, with the key attached.
  • Delivered: it calls outbox.acknowledge(id). If acknowledging fails, the item keeps its key, so the next Send only confirms it.

Session replacement and reload now take the same path, and no stale id is left behind after a dismiss.

Coverage in AgentShareDialog.test.tsx. The fake outbox journal now holds kind-9 recovery items:

  • reload under a new session/scope: one messages.send, one upload, then delivered and acknowledge on the original id
  • removed from outbox: sends anew
  • acknowledgement failure: confirms on retry without resending
  • the happy path asserts every message is acknowledged

The reload and dismiss tests fail on 085a830. The full Vitest suite passes (12,488), along with tsc and biome.

Not verified: I haven't rerun your actual-app Chromium/WebKit reload journey, and partition purge isn't modeled beyond the dismiss path. One known limitation: like New message, an unknown recovery-held message can't be removed until it's retried, and the outbox's refusal text names "New message". I left outbox.ts alone to keep this PR in scope.

fetch(source, { signal: controller.signal })
.then((response) => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.blob();

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.

🤖 [P2] Enforce card limits before buffering the response

Displaying a sender-supplied .agent.png automatically consumes response.blob() before readSnapshot enforces its 10 MiB PNG limit. A CORS-enabled external HTTPS source can therefore force an oversized transfer without an import click. A bounded reproduction using the candidate fetch chain and real parser consumed all 11 MiB (11534336 bytes, cancelled=false) before rejecting it. Reject oversized Content-Length and independently cap streamed bytes, cancelling at the limit; cover both declared and chunked oversized responses. The native proxy's separate 100 MiB ceiling does not enforce the card limit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Fixed in 085a830. The card reader now refuses a declared Content-Length above MAX_PNG_BYTES before reading anything, and otherwise streams the body and cancels the reader once the running total passes the limit. Both cases show the plain picture. New test covers declared and undeclared (chunked) oversize responses, asserting cancellation and an upper bound on bytes pulled; it fails with the limits removed.

.then(readSnapshot)
.then(setShared, () => !controller.signal.aborted && setShared(null));
return () => controller.abort();
}, [source]);

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.

🤖 [P3] Keep transient card-fetch failures retryable

The rejection path treats offline/network/non-2xx failures like invalid card content and renders a bare image. When connectivity recovers with the same source URL, the effect does not rerun, leaving Open/Review unavailable until the row remounts. Distinguish fetch failures from parser rejection and provide retry or the host attachment fallback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Fixed in 085a830. A network failure or non-2xx response now leaves the card in place with "Couldn’t load. Try again"; choosing it reads again and opens import once it loads. Only content that holds no agent (parser rejection or oversize) falls back to the picture. Covered by a new test where the first fetch rejects and the retry succeeds.

tulsi-builder and others added 2 commits October 28, 2026 13:35
Address review on #1834:
- A chat card reads at most the card limit, refusing a larger declared
  length before reading and cancelling a stream past it, since a message
  shows its card unasked.
- A card whose read failed (offline, an HTTP error) says so and reads
  again when chosen, rather than settling as a plain picture; only content
  that holds no agent shows the picture.
- Unconfirmed sends are remembered by community and viewer, so a
  reconnect, which replaces the session, still confirms the queued
  message instead of sending another.

Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: tulsi <tulsi@block.xyz>
Share kept unconfirmed messages in a parallel in-memory map, which a
reload lost (sending and uploading a second copy) and which kept stale
ids after Remove from outbox. Each send now carries an outbox recovery
key for the agent, recipient and exact copy; a retry confirms the
outbox's matching message, sends fresh when there is none, and
acknowledges it once delivered.

Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: tulsi <tulsi@block.xyz>
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