Repository navigation
feat(agents): share agents with people, channels and the Marketplace - #1834
tulsi-builder wants to merge 14 commits into
Conversation
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>
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 Found two P2 defects in reconnect retry bookkeeping and automatic card downloads, plus a P3 recovery issue.
| setTimeout(() => URL.revokeObjectURL(url), 1000); | ||
| } | ||
|
|
||
| const pendingSends = new WeakMap< |
There was a problem hiding this comment.
🤖 [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.
There was a problem hiding this comment.
🤖 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.
There was a problem hiding this comment.
🤖 [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.
There was a problem hiding this comment.
🤖 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, thendeliveredand 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(); |
There was a problem hiding this comment.
🤖 [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.
There was a problem hiding this comment.
🤖 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]); |
There was a problem hiding this comment.
🤖 [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.
There was a problem hiding this comment.
🤖 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.
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>
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
.agent.pngcard (the agent's details embedded in the PNG) or.agent.jsonas a backup. Pack avatars draw from the app's own copy.In chat
*.agent.pngattachment renders as the Agents page card (same size), throughregisterAttachment.MessageRownow lets plugins draw images they claim, outside the image gallery.Import dialog (Marketplace, chat and file)
{ agent: pubkey }route on the Agents page.Security policy
connect-srcnow admitsbuzz-media:andhttp://buzz-media.localhost, and nothing else. This lets chat cards read their embedded definition in packaged builds. The media scheme already returnsAccess-Control-Allow-Originfor the requesting window only, so the window could already read those bytes;docs/identity.mdis updated to match. General HTTPS stays closed.No
FOUNDATIONfiles are edited.Screenshots
Simulated with fictional people, channels and an agent in a component harness (no real data). Rendered by the shipped components.
Validation
5bdada027: 772 files, 12,484 tests pass (16 skipped).tsc, biome andgit diff --checkpass.4c166180b) found two blockers, both fixed incd6e8f28band rechecked with no new findings:tauri.conf.jsonpolicy was loaded in headless Chromium. Before the change, both media URL forms raisedconnect-srcviolations; after it, neither does, and unrelated HTTPS is still blocked.just desktop-bundleatcd6e8f28b) embeds the new policy. The human author opened it and confirmed card → details → import and Open work.No browser journeys were added or removed.
Deferred checks
5bdada027), which landed after the packaged-build test.Follow-ups, not in this PR: a designed export card and Marketplace search.
🤖 Generated with Claude Code