Skip to content

feat(desktop): add buzz-agent-snapshot v1 export/import#1753

Open
wpfleger96 wants to merge 8 commits into
mainfrom
duncan/agent-snapshot-export
Open

feat(desktop): add buzz-agent-snapshot v1 export/import#1753
wpfleger96 wants to merge 8 commits into
mainfrom
duncan/agent-snapshot-export

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Add Phase 3 agent snapshot export and import to the desktop app. An agent's definition, profile, and optionally its decrypted memory can be projected into a portable buzz-agent-snapshot v1 manifest and saved as either .agent.json or .agent.png. The recipient imports the file as a new agent with fresh keys — identity never travels.

Export

New: managed_agents/agent_snapshot.rs

Manifest types, builder, encode/decode, and unit tests:

  • AgentSnapshot / AgentSnapshotDefinition / AgentSnapshotProfile / AgentSnapshotMemory — format discriminator, version, definition + profile + memory sections
  • build_snapshot — projects a ManagedAgentRecord into the manifest; only portable fields included, machine-local and secret fields excluded by construction
  • encode_snapshot_json / decode_snapshot_json — canonical .agent.json encoding
  • encode_snapshot_png / decode_snapshot_png — avatar as PNG image body, manifest in a buzz_agent_snapshot tEXt chunk (base64-encoded JSON); rejects memory-bearing snapshots at the encode boundary (both level != None and non-empty entries)
  • inject_text_chunk — re-encodes an existing PNG to insert the metadata chunk

New: commands/personas/snapshot.rs + snapshot/tests.rs

Export command, import commands, pure-slice resolver/validator helpers, size-cap decoder, and the behavioral-defaults helper:

  • resolve_from_lists — pure resolver over pre-fetched instance/definition slices
  • validate_memory_source — validates memory-source pubkey against the resolved target
  • decode_snapshot_from_bytes — size-caps (5 MiB JSON / 10 MiB PNG) before allocation; double-guarded PNG memory policy; format/version fail-closed
  • resolve_snapshot_import_behavior — pure pub(crate) helper owning all import-time allowlist and behavioral decisions; tests call this directly so they exercise the exact production path
  • export_agent_snapshot Tauri command
  • preview_agent_snapshot_import — decodes and returns a typed preview; no writes
  • confirm_agent_snapshot_import — mints fresh keypair + NIP-OA auth tag; writes persona + managed-agent records; publishes kind:30175 and kind:0; restores memory as fresh kind:30174 events; emits agents-data-changed

Behavioral defaults resolutionresolve_snapshot_import_behavior implements the Keep/Clear contract keyed on the raw allowlist being non-empty (has_source_allowlist), which matches the UI trigger, not on the source mode. Decision table:

Source mode Non-empty list keep=true keep=false
allowlist yes preserve mode + list owner-only + empty
allowlist no Err (reject) Err (reject)
non-allowlist yes preserve mode + list preserve mode + empty
non-allowlist no preserve mode preserve mode
  • Allowlist-mode + empty list: always rejected before key generation (no coherent value to write)
  • Allowlist-mode + non-empty + Clear: downgrade to owner-only (allowlist mode is invalid without entries)
  • Non-allowlist + non-empty + Clear: preserve source mode, empty the list (non-allowlist modes are valid without entries)

The allowlist is normalized via validate_respond_to_allowlist (lowercase, dedup, format validation) as the first step; malformed pubkeys are rejected before any key generation.

New: AgentSnapshotExportDialog.tsx

Export dialog with memory-level radio picker (none / +core / +everything), plaintext-memory warning, and format picker (.agent.json / .agent.png, PNG disabled when memory ≠ none).

UI wiring

PersonaActionsMenuExport snapshot menu entry → usePersonaActions.tsAgentsView.tsxUnifiedAgentsSection.tsx.

Hard rules enforced

PNG memory guardencode_snapshot_png returns Err when memory.level != None OR memory.entries is non-empty. Import side: decode_snapshot_from_bytes enforces the same guard.

Secret exclusionprivate_key_nsec, auth_tag, env_vars, relay_url, acp_command / agent_command / mcp_command, runtime state, and lineage IDs are never serialized into the manifest.

Identity never travels — fresh keypair minted per import. Source pubkey, nsec, auth_tag, relay_url, and lineage are never consumed.

Cache coherenceagents-data-changed emitted after writes; frontend invalidates personasQueryKey, managedAgentsQueryKey, and ['user-profile', newPubkey] on confirm success.

Import byte cap — oversized inputs (>5 MiB JSON, >10 MiB PNG) rejected before decode allocation in both preview and confirm.

TypeScript

  • tauriPersonas.tsAgentSnapshotImportPreview (avatarUrl single effective field), AgentSnapshotImportConfirm, AgentSnapshotImportResult types; previewAgentSnapshotImport and confirmAgentSnapshotImport functions
  • hooks.ts — mutation hooks and re-exported types
  • AgentSnapshotImportDialog.tsx — preview/confirm dialog; allowlist Keep/Clear choice; memory warning; result display with vertically-bounded scrollable error list (max-h-32 overflow-y-auto, break-all per item, data-testid="agent-snapshot-import-memory-errors")
  • usePersonaActions.ts — snapshot import state, handler, and multi-query cache invalidation
  • agents.spec.ts — E2E menu assertions include Export snapshot for both custom and team-managed personas

Tests

  • snapshot/tests.rs — 41 Rust unit tests: format sniff, size caps, PNG memory guards, all nine resolve_snapshot_import_behavior table cells (allowlist/non-allowlist × empty/non-empty × keep/clear plus empty-allowlist-mode rejection), normalization/dedup, malformed pubkey rejection, out-of-range parallelism, avatar fallback, identity stripping, memory slug semantics, partial-failure result shape
  • agentSnapshotImportDialog.test.mjs — 3 render tests for ResultBody: error list rendered with data-testid, both error strings surface, bounded/scrollable shape (max-h + overflow-y-auto + break-all), full-success omits list
  • tauriPersonas.snapshotImport.test.mjs — frontend API type tests

Introduce the Phase 3 agent snapshot export for the unified-agent model.
An agent's definition, profile, and optionally its decrypted memory can
be projected into a portable `buzz-agent-snapshot v1` manifest, then
saved as either a `.agent.json` file (canonical, supports all memory
levels) or a `.agent.png` file (avatar image with manifest embedded in
a `buzz_agent_snapshot` tEXt chunk, no-memory only).

Core additions:
- `managed_agents/agent_snapshot.rs` — manifest types (`AgentSnapshot`,
  `AgentSnapshotDefinition`, `AgentSnapshotProfile`, `AgentSnapshotMemory`),
  `build_snapshot` builder, JSON and PNG encode/decode, avatar data-URL
  helper, PNG tEXt chunk injector, and 21 unit tests covering round-trips,
  the PNG memory guard, and the full secret exclusion list.
- `commands/personas/mod.rs` — `export_agent_snapshot` Tauri command:
  resolves the record, decodes any avatar data-URL, calls
  `get_agent_memory` for `+core` / `+everything` levels, builds the
  manifest, and saves via `save_bytes_with_dialog`.
- UI — `AgentSnapshotExportDialog` with memory-level radio picker
  (none / +core / +everything), plaintext-memory warning on +core/+everything,
  format picker (JSON / PNG, PNG disabled when memory != none), and
  `Export agent snapshot` entry in `PersonaActionsMenu`.

Hard rules enforced and test-asserted:
- PNG memory guard: `encode_snapshot_png` returns `Err` when
  `memory.level != None`; the Tauri command also rejects eagerly.
- Secret exclusion: nsec, auth_tag, env_vars, relay_url, acp/agent/mcp
  commands, runtime state, and lineage IDs are absent by construction and
  each asserted in a dedicated unit test.

Import (PR 2) is not in scope here; `parse_persona_files` and
`create_persona` are untouched.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96 wpfleger96 requested a review from a team as a code owner July 11, 2026 02:59
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 and others added 2 commits July 10, 2026 23:20
- CRITICAL: export now resolves both keyless definitions (by slug) and
  keyed instances (by pubkey/slug) via resolve_snapshot_export_target
  helper in snapshot.rs. Memory-bearing exports require an explicit
  memory_source_pubkey, validated server-side against the definition's
  slug (persona_id linkage). UI forwards the profileAgent pubkey; memory
  levels are disabled in the dialog when no linked agent instance exists.
- IMPORTANT: encode_snapshot_png now rejects unless level==None AND
  entries.is_empty() — closes the inconsistent-state bypass where
  level:None + non-empty entries would leak plaintext memory.
- IMPORTANT: secret-exclusion test fixture now populates every excluded
  field with a unique sentinel (persona_id, persona_team_dir,
  persona_name_in_team, BackendKind::Provider with secret config,
  backend_agent_id, provider_binary_path, last_error, last_error_code).
  Tests assert both field names and sentinel values are absent.
- Split export_agent_snapshot command to personas/snapshot.rs so
  personas/mod.rs stays under the 1105-line gate.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
… override

Two Pass 2 export-checkpoint fixes on PR #1753:

1. Refactor resolver and memory-source validator into pure slice-based
   helpers (`resolve_from_lists`, `validate_memory_source`) so they can
   be unit-tested without an AppHandle. The Tauri command loads the stores
   once and calls the helpers directly; the now-unused public
   `resolve_snapshot_export_target` wrapper is removed.

   Add 6 regression tests in `commands/personas/snapshot.rs`:
   - Joint happy path: keyless definition (slug="my-agent") + keyed
     instance (slug=None, pubkey="instance-pk", persona_id="my-agent") —
     resolving "my-agent" returns the definition, then "instance-pk"
     validates as the memory source against the same instance slice.
   - Resolver: pubkey lookup finds instance; unknown id errors.
   - Validator: empty pubkey fails closed; instance linked to wrong
     definition fails closed; direct-instance cross-agent pairing fails
     closed.

2. Remove the stale `personas/mod.rs` 1105-line override from
   `desktop/scripts/check-file-sizes.mjs`. The command was split into
   `snapshot.rs` in a prior commit; `personas/mod.rs` is now 977 lines
   and the default 1000-line gate applies. Import must live in its own
   module, not regrow `personas/mod.rs`.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96 wpfleger96 changed the title feat(desktop): add buzz-agent-snapshot v1 export feat(desktop): add buzz-agent-snapshot v1 export/import Jul 11, 2026
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 and others added 5 commits July 10, 2026 23:43
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Adds preview+confirm import flow for .agent.json and .agent.png snapshot
files. Two new Tauri commands (preview_agent_snapshot_import,
confirm_agent_snapshot_import) and a matching TypeScript UI layer.

PNG memory policy: any .agent.png carrying memory (level != none, or
none + non-empty entries) is rejected at decode time — plaintext memory
travels only via .agent.json after explicit preview/confirmation.

Rust:
- decode_snapshot_from_bytes: double-guard on PNG memory (level + entries)
- preview_agent_snapshot_import: sniffs format, decodes, returns preview
  (allowlist warning, memory warning, entry count)
- confirm_agent_snapshot_import: mints fresh keypair, publishes agent,
  re-encrypts and writes memory entries under new conversation key
- snapshot/tests.rs: 26 new tests (format sniff, PNG memory guards, JSON
  memory acceptance, allowlist surfacing, distinct pubkeys, identity
  stripping, partial/full memory result shape)
- pending.rs: widen retain_persona_pending visibility to pub(in crate::commands::personas)

TypeScript:
- tauriPersonas.ts: AgentSnapshotImportPreview, AgentSnapshotImportConfirm,
  AgentSnapshotImportResult types + previewAgentSnapshotImport,
  confirmAgentSnapshotImport API functions
- hooks.ts: usePreviewAgentSnapshotImportMutation,
  useConfirmAgentSnapshotImportMutation, re-exported import types
- AgentSnapshotImportDialog.tsx: full preview/confirm dialog (allowlist
  warning, memory warning, result display)
- usePersonaActions.ts: import state, handleImportSnapshotFile,
  handleConfirmSnapshotImport, closeSnapshotImportDialog
- AgentsView.tsx + UnifiedAgentsSection.tsx: wire dialog + file input into
  NewAgentCard (Import agent snapshot menu item)

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Resolve all five IMPORTANT findings from Thufir's Pass 3 review of
PR #1753 plus the two hardening items:

1. Route allowlist/behavioral-defaults through resolve_mint_behavioral_defaults
   before any write. Keep validates mode+allowlist and rejects empty-allowlist
   mode. Clear always downgrades to RespondTo::OwnerOnly with empty list,
   preventing an invalid empty-allowlist definition from being persisted.
   PersonaRecord and ManagedAgentRecord now use consistent resolved values
   for respond_to, respond_to_allowlist, and parallelism.

2. Derive effective_avatar as avatar_data_url.or(avatar_url) and propagate
   consistently through PersonaRecord.avatar_url, ManagedAgentRecord.avatar_url,
   and the sync_managed_agent_profile call. Preview exposes a single avatarUrl
   field (data URL wins, URL fallback otherwise) instead of avatarDataUrl.

3. Emit agents-data-changed after persona+managed-agent writes. Frontend
   invalidates managedAgentsQueryKey and user-profile cache after confirm,
   in addition to personasQueryKey.

4. Render result.memoryErrors as a bounded <ul> with
   data-testid="agent-snapshot-import-memory-errors" in ResultBody. Export
   ResultBody as a named export for hook-free source-path testing. Add
   agentSnapshotImportDialog.test.mjs with three render tests proving the
   error list is present, strings surface, and full-success omits the list.

5. Update exact menu arrays in agents.spec.ts to include Export snapshot
   for both custom and team-managed personas. Remove the stale
   name: 'Export' absence check. 10/10 agents E2E pass locally.

6. Fix stale kind:30078 comments to kind:30177 in snapshot.rs.

7. Add MAX_SNAPSHOT_JSON_BYTES (5 MiB) and MAX_SNAPSHOT_PNG_BYTES (10 MiB)
   size caps in decode_snapshot_from_bytes before decode allocation. Tests
   added for both rejection branches.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Two source-confirmed blockers from Paul's verification of 825eeb1:

1. Normalize snapshot allowlist via validate_respond_to_allowlist before
   passing to resolve_mint_behavioral_defaults. The function contract
   (types.rs:813-826) treats input_allowlist as already-normalized and
   only calls the validator on the definition-fallback branch; raw snapshot
   pubkeys (uppercase, padded, duplicated) would persist unvalidated.

   Fix keep_allowlist=false semantics: preserve non-allowlist source modes
   (e.g. 'anyone') when the UI never showed a Keep/Clear choice. Only
   downgrade to owner-only when the user explicitly chose Clear on an
   allowlist-mode snapshot.

   Consolidate into a single resolve_mint_behavioral_defaults call covering
   all four fields (respond_to, allowlist, toolsets, parallelism) so
   PersonaRecord and ManagedAgentRecord always use one consistent struct.

   New tests: uppercase normalization + dedup, malformed pubkey rejection,
   anyone-mode preservation, Keep success, Clear downgrade, empty
   allowlist-mode rejection, out-of-range parallelism.

2. Bound the memory-error list in ResultBody with max-h-32 + overflow-y-auto
   so a large partial import cannot grow the dialog without bound. Replace
   truncate with break-all so the full relay error text is readable. Extend
   the render test to assert both the bounding shape (max-h, overflow-y-auto)
   and that items use break-all rather than truncate.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…owlist and non-allowlist+Clear paths

Extract a pure pub(crate) helper resolve_snapshot_import_behavior that
owns all import-time allowlist and behavioral decisions. Tests now call
the production path directly rather than reconstructing the logic inline.

Decision table implemented:
- allowlist mode + empty list: reject for both keep values (no coherent
  value to write; UI never showed a choice)
- allowlist mode + non-empty + keep=true: preserve mode + list
- allowlist mode + non-empty + keep=false: downgrade to owner-only +
  empty (allowlist mode is invalid without entries)
- non-allowlist mode + non-empty + keep=true: preserve mode + list
- non-allowlist mode + non-empty + keep=false: preserve mode + empty
  (non-allowlist modes are valid without entries; no downgrade)
- non-allowlist mode + empty: preserve mode regardless of keep

Remove stale import_allowlist_clear_and_keep_are_enforced_server_side
test that called resolve_mint_behavioral_defaults directly and had the
wrong model for Clear (always owner-only regardless of source mode).
Replace with nine targeted tests that cover each table cell plus
normalization and malformed-pubkey rejection.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
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