Skip to content

feat(desktop): Agent Trading Cards — mintable agent-snapshot card PNGs with optional NIP-44 lock - #3278

Open
tlongwell-block wants to merge 8 commits into
mainfrom
eva/agent-trading-cards
Open

feat(desktop): Agent Trading Cards — mintable agent-snapshot card PNGs with optional NIP-44 lock#3278
tlongwell-block wants to merge 8 commits into
mainfrom
eva/agent-trading-cards

Conversation

@tlongwell-block

Copy link
Copy Markdown
Collaborator

Agent Trading Cards

"Create Agent Card" action in the agent panel that mints an AI-generated MTG-style trading card PNG which is the agent: the card carries the buzz_agent_snapshot tEXt chunk and is drag-in importable like any snapshot PNG.

What's in here

  • Mint pipeline (Rust): one OpenAI Responses call — gpt-5.6-sol as card designer with gpt-image-2 via the image_generation tool (~2–3 min). New mint_agent_card / save_agent_card commands; preview with reroll; save or send as .agent.png with round-trip verification before any bytes leave the app.
  • Snapshot/chunk work stays in Rust, reusing the existing encoder/decoder seams (byte-compat golden vector proves the plain path is identical to the pre-envelope encoder for placeholder, PNG-injection, and JPEG-transcode paths).
  • Locked cards (NIP-44): optional buzz-agent-snapshot-encrypted envelope encrypted to the (owner, agent) pair. parse_canonical_pubkey performs lift-x curve validation before any API spend; wrong-key decrypt returns a fixed refusal; the plain decoder refuses locked cards.
  • Guardrails: 10 MiB ceiling on final bytes, memory structurally none in the snapshot, full-manifest import disclosure, API-key hygiene via env layering (record > persona > global > process), fail-early validation ordering (all key/lock/NIP-44-cap checks before Responses spend).
  • Import side: full-manifest disclosure dialog, locked-card import disclosure, bounded avatar fetch.

Review

Code reviewed by Wren across the full arc; final locked-card cross-review APPROVED 9/9/9 at exactly this head (64f819dc8), with independent same-SHA verification: Rust lib 1,843/1,843, clippy --all-targets -D warnings, desktop file-size gate.

Live-mint evidence (real API, shipping seams, this SHA)

  • Plain (Honey): 188s, 1500x2250, 5,101,503 bytes (< 10 MiB); decoded manifest == built manifest; memory=none.
  • Locked (Fizz): 176s, 4,670,184 bytes; owner-key and agent-key decrypt both verified via logical manifest compare; wrong-key refusal exact; plain decoder refuses.
  • Live finding: built-in agents' ~171 KB inline avatars exceed the NIP-44 65,535-byte plaintext cap and the fail-early guard fires before API spend — clean error path, noted as a UX follow-up for large-avatar agents choosing lock.

Full evidence (cards + dialog screenshots) posted in the originating thread.

npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d and others added 8 commits July 27, 2026 22:15
…rading Cards

One OpenAI Responses call (designer model + native image_generation tool)
mints a collectible card PNG; the existing agent_snapshot encoder injects
the buzz_agent_snapshot chunk so the card IS an importable .agent.png.

Boundary rules (agreed with Wren):
- Snapshot construction/injection reuses agent_snapshot.rs; no card-only
  wire format.
- Memory exclusion is structural (MemoryLevel::None at build).
- 10 MiB .agent.png ceiling enforced on FINAL bytes post-injection.
- Round-trip verification decodes final bytes and compares the manifest.
- OPENAI_API_KEY resolved via existing env layering (record > persona >
  global > process), never leaves Rust, never logged.
- Card frame template embedded via include_bytes! (generation input only);
  test gate asserts it decodes with the expected shape.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Review finding (Wren): fetch_avatar buffered the full response before
checking MAX_AVATAR_FETCH_BYTES, so a missing or dishonest Content-Length
could consume unbounded network/memory. Now preflights content_length()
and streams with a running cap check that rejects before appending any
chunk that would cross 10 MiB — same contract as media_download.rs.
Accumulator boundary extracted for unit testing.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
UI lane for Agent Trading Cards: a Sparkles 'Create card' quick action
in the agent panel's primary-action row (gated canManagePersona), opening
AgentCardMintDialog — optional style notes, one long Rust-side mint call
(~2-3 min) with progress copy, preview with reroll, save as .agent.png.
NO_OPENAI_KEY errors point the user at provider settings. tauriPersonas
gains mintAgentCard/saveAgentCard wrappers; no new backend state — reroll
is simply another mint call.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
…are path

Closes the v1 scope gap Wren flagged: the mint dialog only offered Save.
Card preview now includes PersonaShareRecipients + a Send action that
routes through useSnapshotSendController — the exact guarded
encode → upload (uploadMediaBytes) → send pipeline agent snapshots use,
with the same eligibility checkpoints and DM-safety gating. No
card-special relay path, per the kickoff contract.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
…port)

Tyler's encryption requirement, per Wren's wire-contract spec:

- New agent_snapshot_envelope.rs: typed outer envelope
  (format "buzz-agent-snapshot-encrypted", version 1, scheme nip44-v2)
  riding the same allowlisted buzz_agent_snapshot chunk. Plain cards
  keep today's encoder byte-for-byte. NIP-44 v2 conversation key over
  the (owner, agent) pair — both nsecs decrypt, nobody else's.
- Inner caps enforced per NIP-AE's local contract: 65,535-byte
  plaintext cap on the serialized manifest before encryption, envelope
  JSON/ciphertext caps before base64/decrypt work, decrypted UTF-8 cap
  before snapshot parsing.
- Exact-endpoint key resolution only (owner identity key match, or a
  hydrated managed-agent record whose pubkey + derived-secret pubkey
  both equal the embedded agentPubkey) — no trial decryption. All
  unlock failures surface only the constant LOCKED_CARD_REFUSAL.
- mint path: lock flag on mint_agent_card; encrypted round-trip
  verifies by extracting the chunk, decrypting with the same endpoint
  key, and comparing logical manifests (not ciphertext).
- preview/confirm import wired through decode_snapshot_for_import with
  owner keys + loaded records; AgentSnapshotImportPreview gains
  locked: bool (true = unlocked by local keys, full payload disclosed).
- fetch_snapshot_bytes validates locked envelopes structurally in
  transit without decrypting.
- Test vectors per Wren's list: owner/agent unlock, unrelated-key
  refusal, tampered ciphertext, swapped/malformed pubkeys, unknown
  format/version/scheme, plaintext + ciphertext caps, encrypted
  final-PNG round trip, plain-card bytes unchanged.
- File-size gate: inline test modules split to sibling #[path] files
  (agent_snapshot_tests.rs, snapshot/tests_locked.rs) following the
  storage_tests.rs precedent; no new size-gate exceptions.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
…sure

UI half of the optional locked-card feature:

- AgentCardMintDialog: Lock switch (canLock-gated — enabled only when
  the persona has a linked agent instance/pubkey; disabled with a
  'start this agent once' explanation for bare definitions, per Wren's
  discoverability preference), locked-specific post-mint copy.
- UserProfilePanel: canLock = Boolean(managedAgent?.pubkey) threaded
  into the mint target.
- tauriPersonas: mintAgentCard(id, styleNotes?, lock?); MintedAgentCard
  and AgentSnapshotImportPreview gain locked.
- AgentSnapshotImportDialog: locked-card provenance notice rendered
  only when preview.locked (unlocked by local keys, full decrypted
  payload disclosed as usual) + rendering test.
- e2eBridge preview mock updated with the new preview fields.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
…-byte compat vector

Closes both findings from Wren's cross-review at 4434d39:

1. parse_canonical_pubkey now requires PublicKey::xonly() to succeed —
   nostr 0.44's from_hex only decodes 32 bytes and defers lift-x
   validation, so a non-point like "f"*64 previously passed structural
   transit/save validation and failed only at unlock. Non-points are
   now rejected structurally, per the agreed wire contract; the
   malformed-pubkeys vector asserts structural rejection instead of
   pinning the deferred-failure behavior. mint_agent_card uses the same
   canonical check on record.pubkey so a non-point fails BEFORE the
   API spend.

2. Added the plain-byte compatibility vector that the review claim
   referenced: plain_encoder_bytes_identical_to_pre_envelope_encoder
   reimplements the pre-refactor encode_snapshot_png body verbatim and
   asserts byte-identical output across all three composition paths —
   placeholder, PNG-avatar (chunk injection ordering), and JPEG
   transcode.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@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.

1 participant