Skip to content

feat(desktop): inline key setup, owner-direction primacy, and OPENAI_BASE_URL for card mints - #3315

Closed
tlongwell-block wants to merge 11 commits into
mainfrom
eva/card-mint-key-ux
Closed

feat(desktop): inline key setup, owner-direction primacy, and OPENAI_BASE_URL for card mints#3315
tlongwell-block wants to merge 11 commits into
mainfrom
eva/card-mint-key-ux

Conversation

@tlongwell-block

@tlongwell-block tlongwell-block commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to #3278 (Agent Trading Cards), per the design conversation in buzz-agent-trading-cards. Three knobs, one small PR — #3278 stays frozen at its approved SHA.

1. Inline OPENAI_API_KEY setup in the mint dialog

New Tauri command card_mint_key_status(id) reports whether a key resolves through the exact same env layering the mint uses (agent record > persona > global Agent Defaults > process env). It never returns the key.

When no key resolves, the dialog renders a key-setup panel instead of the mint form:

  • Cost disclosure: "Minting a card costs money — it generates the art and card text through the OpenAI API with your key…"
  • Pointer to platform.openai.com for key creation.
  • Free-path pointer: "Don't want to spend money? The Export action on the agent shares the same importable agent as a plain file — free, just without the collectible card art."
  • Password input + "Save key & continue" writes the key into the global Agent Defaults env — the same single source of truth every agent inherits. No second key store, nothing new to snapshot-scrub, no card-specific key that can drift from the agent key.

A NO_OPENAI_KEY mint error (key removed after the dialog opened) now flips into key-setup mode instead of a dead-end toast.

2. Owner-direction primacy (art AND card text)

The direction box now advertises both uses: "Optional directions — art ("stormy night, lightning motif") and/or card text ("ability: Verify — scry 2"). Your directions take priority; anything you leave open is designed for you."

The Responses prompt is restructured so the designer model treats owner input as authoritative:

  • Owner directions are framed as OWNER'S DIRECTIONS — these override the default art-style and copy guidance below wherever they conflict, explicitly covering type line / ability / flavor text, with "use their wording (edited only for spelling)".
  • The style-match-the-avatar anchor is demoted to a default that owner directions override.
  • The fixed contract survives regardless: frame identity, layout, and text-fidelity requirements cannot be overridden.

Pinned by the rewritten instructions_pin_style_match_default_and_owner_primacy test.

3. OPENAI_BASE_URL override

Resolved through the same env layering; the mint posts to {base}/responses instead of the hardcoded api.openai.com. Supports endpoints and proxies that speak the OpenAI Responses shape with Bearer auth. (Azure OpenAI is NOT covered by this alone — it uses its own URL scheme and api-key header, so it would need a real driver.) Trailing slashes normalized (responses_url_default_and_override test).

Verification (at 3f3f0fc, same shell)

  • cargo test --lib: 1844 passed / 0 failed
  • cargo clippy --all-targets -- -D warnings: clean; cargo fmt --check: clean
  • tsc --noEmit: clean; desktop tests: 3723/3723; pnpm check (biome + file-size gates): clean

Round 2 (Wren's UX/code review — all findings addressed)

  1. Persistent pre-spend disclosure: the normal mint form (key present) now carries a compact cost note + the free-path row — not just the no-key onboarding panel.
  2. Actionable directions: "Get a key at platform.openai.com" is a real external-link button (openUrl), and the free path is a real Share without card art action that closes the mint dialog and opens the existing snapshot export flow (wired at the UserProfilePanel callsite; the prop is optional so the dialog stays testable standalone).
  3. No silent agent restarts: key save no longer calls the general set_global_agent_config (which stops/restarts every running local agent whose effective env changes and whose restart counts the dialog discarded). New narrow card_mint_save_openai_key command: validated single-key read-modify-write of the latest on-disk config under the managed-agents store lock, never restarts anything — the mint re-reads config per call, so no restart is needed. Toast tells the user running agents pick the key up on next restart. Settings cache invalidated so the Agent Defaults editor stays fresh.
  4. Azure claim narrowed in code comment + this body (Bearer-auth Responses endpoints/proxies only).
  5. Owner-copy rule vs 220-char limit: prompt now says "use their wording within the 220-character text-box limit … if their text exceeds the limit, condense it minimally while keeping their words and intent" — no mutually impossible instructions. Prompt-contract test updated.

Review plan

Wren co-reviews the UX (Tyler's directive: simple and intuitive) and produces fresh screenshots of the key-setup panel + updated direction box.

npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d and others added 9 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>
…BASE_URL for card mints

Three follow-ups to the Agent Trading Cards mint flow (#3278):

1. Inline OPENAI_API_KEY capture: new card_mint_key_status command
   checks (never returns) whether a key resolves through the existing
   env layering. When none does, the mint dialog swaps to a key-setup
   panel with a cost disclosure, a pointer to platform.openai.com, and
   a pointer to the free Export sharing path. Saving writes the key to
   the global Agent Defaults env — the same single source of truth the
   mint already reads; no second key store. A NO_OPENAI_KEY mint error
   now flips into key-setup mode instead of a dead-end toast.

2. Owner-direction primacy: the direction box explicitly accepts card
   TEXT direction as well as art, and the Responses prompt is
   restructured so the owner's directions override the default
   art-style and copy guidance (but never the fixed frame/layout/
   text-fidelity contract). Dialog placeholder updated to match.

3. OPENAI_BASE_URL override resolved through the same env layering,
   unlocking Azure OpenAI and Responses-speaking proxies; trailing
   slashes normalized.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
@tlongwell-block
tlongwell-block requested a review from a team as a code owner July 28, 2026 13:50
npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d and others added 2 commits July 28, 2026 10:10
… paths, no silent restarts

Addresses Wren's review of the card-mint key UX (buzz-agent-trading-cards):

1. Cost disclosure + free alternative now persist in the normal mint form
   (pre-spend), not only the no-key setup panel: a compact cost note above
   the Mint action plus a shared FreeSharePathRow.

2. Both directions are actionable: 'Get a key at platform.openai.com' is a
   real external-link button (openUrl), and the free path is a 'Share
   without card art' action that closes the mint dialog and opens the
   existing snapshot export flow (wired at the UserProfilePanel callsite;
   optional prop so the dialog degrades gracefully elsewhere).

3. Key save no longer restarts running agents: replaced the general
   set_global_agent_config call (which stops/restarts every running local
   agent whose effective env changes, and whose restart counts the dialog
   discarded) with a narrow card_mint_save_openai_key command — validated
   single-key read-modify-write of the latest on-disk config under the
   managed-agents store lock, no restarts. The mint re-reads config per
   call, so no restart is needed; the toast says running agents pick the
   key up on next restart, and the Agent Defaults query cache is
   invalidated so the settings editor stays fresh.

4. Azure claim narrowed in the responses_url doc comment: OPENAI_BASE_URL
   covers Bearer-auth Responses-speaking endpoints/proxies; Azure's URL
   scheme + api-key header would need a real driver.

5. Owner-copy rule acknowledges the 220-character text-box contract:
   over-limit owner text is condensed minimally, keeping words and intent —
   no mutually impossible verbatim-vs-limit instructions. Prompt-contract
   test updated.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
@tlongwell-block

Copy link
Copy Markdown
Collaborator Author

Rolled into #3278 per Tyler: eva/agent-trading-cards was fast-forwarded to this branch's exact reviewed head edc8bea (no rebase, no force, commits untouched). #3278 now carries the full card-mint key UX work; this PR's diff vs #3278 is empty. Wren's post-merge 10/10/10 review + local redteam apply verbatim to the new #3278 head since the tree is bit-identical.

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