feat(desktop): add buzz-agent-snapshot v1 export/import#1753
Open
wpfleger96 wants to merge 8 commits into
Open
feat(desktop): add buzz-agent-snapshot v1 export/import#1753wpfleger96 wants to merge 8 commits into
wpfleger96 wants to merge 8 commits into
Conversation
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>
- 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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 v1manifest and saved as either.agent.jsonor.agent.png. The recipient imports the file as a new agent with fresh keys — identity never travels.Export
New:
managed_agents/agent_snapshot.rsManifest types, builder, encode/decode, and unit tests:
AgentSnapshot/AgentSnapshotDefinition/AgentSnapshotProfile/AgentSnapshotMemory— format discriminator, version, definition + profile + memory sectionsbuild_snapshot— projects aManagedAgentRecordinto the manifest; only portable fields included, machine-local and secret fields excluded by constructionencode_snapshot_json/decode_snapshot_json— canonical.agent.jsonencodingencode_snapshot_png/decode_snapshot_png— avatar as PNG image body, manifest in abuzz_agent_snapshottEXt chunk (base64-encoded JSON); rejects memory-bearing snapshots at the encode boundary (bothlevel != Noneand non-empty entries)inject_text_chunk— re-encodes an existing PNG to insert the metadata chunkNew:
commands/personas/snapshot.rs+snapshot/tests.rsExport 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 slicesvalidate_memory_source— validates memory-source pubkey against the resolved targetdecode_snapshot_from_bytes— size-caps (5 MiB JSON / 10 MiB PNG) before allocation; double-guarded PNG memory policy; format/version fail-closedresolve_snapshot_import_behavior— purepub(crate)helper owning all import-time allowlist and behavioral decisions; tests call this directly so they exercise the exact production pathexport_agent_snapshotTauri commandpreview_agent_snapshot_import— decodes and returns a typed preview; no writesconfirm_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; emitsagents-data-changedBehavioral defaults resolution —
resolve_snapshot_import_behaviorimplements 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:allowlistallowlistThe 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.tsxExport 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
PersonaActionsMenu→Export snapshotmenu entry →usePersonaActions.ts→AgentsView.tsx→UnifiedAgentsSection.tsx.Hard rules enforced
PNG memory guard —
encode_snapshot_pngreturnsErrwhenmemory.level != NoneORmemory.entriesis non-empty. Import side:decode_snapshot_from_bytesenforces the same guard.Secret exclusion —
private_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 coherence —
agents-data-changedemitted after writes; frontend invalidatespersonasQueryKey,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.ts—AgentSnapshotImportPreview(avatarUrlsingle effective field),AgentSnapshotImportConfirm,AgentSnapshotImportResulttypes;previewAgentSnapshotImportandconfirmAgentSnapshotImportfunctionshooks.ts— mutation hooks and re-exported typesAgentSnapshotImportDialog.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-allper item,data-testid="agent-snapshot-import-memory-errors")usePersonaActions.ts— snapshot import state, handler, and multi-query cache invalidationagents.spec.ts— E2E menu assertions includeExport snapshotfor both custom and team-managed personasTests
snapshot/tests.rs— 41 Rust unit tests: format sniff, size caps, PNG memory guards, all nineresolve_snapshot_import_behaviortable 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 shapeagentSnapshotImportDialog.test.mjs— 3 render tests forResultBody: error list rendered withdata-testid, both error strings surface, bounded/scrollable shape (max-h + overflow-y-auto + break-all), full-success omits listtauriPersonas.snapshotImport.test.mjs— frontend API type tests