fix(desktop): instance-level agents nav — inventory, epoch scope, persona sheet filtering - #4935
Open
wpfleger96 wants to merge 10 commits into
Open
fix(desktop): instance-level agents nav — inventory, epoch scope, persona sheet filtering#4935wpfleger96 wants to merge 10 commits into
wpfleger96 wants to merge 10 commits into
Conversation
…capture
Add owner-consent archive/unarchive commands, workspace-epoch seqlock
protocol, NIP-IA ownership classifier, and Instances Sheet UI.
Rust (desktop/src-tauri):
- WorkspaceEpochWriteGuard (workspace_epoch.rs): RAII seqlock writer guard
with SeqCst ordering, writer-mutex serialization, and guaranteed even
restoration on every exit path including panic/unwind. All three production
key writers (resolve_persisted_identity, commit_imported_identity,
apply_workspace) participate; apply_workspace spans its compound
override+keys mutation under one guard.
- AppState extended with workspace_epoch: AtomicU64 + workspace_write:
Mutex<()> for the seqlock protocol.
- capture_archive_scope: bounded-retry reader that accepts only equal-and-even
epoch samples, ensuring keys and relay_url_override come from one workspace
generation.
- ArchiveScope { keys, actor, api_base_url, workspace_epoch }: immutable
snapshot consumed by every fetch/classify/mint/sign/submit in the scoped
archive operation.
- NipIaOwnerProof classifier: enum verified|missing_profile|missing_auth|
multiple_auth_tags|invalid_auth|owner_mismatch. Reuses verify_auth_tag
(syntax + signature only, no condition-clause evaluation per NIP-IA rule 6).
- scoped_archive_operation: single non-Tauri fn owning the full
fetch→classify→mint→build/sign→submit pipeline for both 9035 (archive) and
9036 (unarchive). Fresh empty-condition auth tag minted after Verified
classification; never copies the profile tag.
- archive_agent / unarchive_agent: thin #[tauri::command] wrappers.
- Keyring helpers extracted to app_state_keyfile_ops.rs to stay under the
file-size ratchet (app_state.rs: 998 gate lines, limit 1080).
- Relay acceptance tests (identity_archive/tests/identity_archive_relay_tests.rs):
five #[ignore]d tests requiring live relay+Postgres, selected by the new
desktop-tauri-relay-acceptance just recipe and the Backend Integration CI
gate. Tenant fixture makes Self and Admin impossible; 9035 asserts
consent_path='owner' in Postgres; 9036 asserts emitted delta.
- Deterministic epoch protocol tests (app_state_epoch_tests.rs): no infra,
always-required in desktop-tauri-test/Desktop Core gate. Tests: initial
capture, writer exclusion+capture retry, RAII restoration (drop/early-
return/catch_unwind), poisoned-mutex error, mixed-generation barrier test.
TypeScript (desktop/src):
- tauriIdentityArchive.ts: archiveAgent/unarchiveAgent Tauri bindings.
- hooks.ts: useOwnedAgentInventoryQuery hook with additive cache invalidation.
- InstancesSheet.tsx: instances sheet opened from Instances (N) card action;
shows tri-state archive status, start-control safeguard for 3rd instance.
- PersonaActionsMenu.tsx: onViewInstances prop wired to InstancesSheet.
- UnifiedAgentsSection.tsx: Instances button in agent card actions.
- relayQueryInvalidation.ts: ownedAgentInventoryQueryKey registration.
CI/Justfile:
- desktop-tauri-relay-acceptance just recipe: _ensure-sidecar-stubs +
nextest selector for in-crate relay acceptance module.
- Backend Integration (relay e2e) job: new step just desktop-tauri-relay-
acceptance after relay startup; job if extended to desktop-rust; Tauri Linux
apt deps + rust-cache for desktop/src-tauri; timeout raised.
Thread: nostr:nevent1qqs0jp4fmfhaxcm7etlyq9csgwhg73j44f2akkhcnzfnk7s0w6rxptqpz24mxk
Plan: v9, APPROVE by Thufir (event 7dd99118), baseline 8342dfc
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…-agents-nav * origin/main: fix(desktop): allow shared agent mentions (#4913) Polish mobile top navigation (#4778) fix(release): tag immutable desktop candidates (#4811) fix(channels): restrict private-channel invitations (#4612) fix(acp): reject unattended permission requests (#4609) fix(workflow): bind trigger author to the signed event (#4607) fix(git): revoke access for banned relay members (#4608) fix(agent): recover from unsupported image input instead of poisoning the turn (#4896) Define private managed agent wire protocol (#4593) fix(mobile): serialize channel sections sync (#3165) fix(desktop): make missing-command error actionable for released builds (#4802) chore(release): release Buzz Desktop version 0.5.5 (#4809) feat: paste composer text without formatting (#4801) Revert "chore(release): release Buzz Desktop version 0.5.5" (#4808) chore(release): release Buzz Desktop version 0.5.5 (#4800) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Replace hand-rolled pubkey.slice() calls with the canonical truncatePubkey() helper from shared/lib/pubkey, as required by the check-pubkey-truncation gate. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Addresses all 7 blocking and 1 minor finding from Thufir's pass 1: CRITICAL: Inventory author/agent confusion - Extract agent pubkey from kind:30177 d-tag; never treat ev.pubkey (owner) as agent - Fetch agent's kind:0 separately; verify NIP-01 id+signature before classifying - All NipIaOwnerProof variants preserved in OwnedAgentInstance IMPORTANT: Exhaustive race-safe inventory - fetch_all_owned_30177 pages to exhaustion with composite (until, before_id) cursor - All state captured via capture_archive_scope (seqlock epoch) before I/O - Reject malformed d-tags; dedup with canonical (created_at DESC, id ASC) - Drop dead cursor API; return one complete snapshot IMPORTANT: Approved model + Sheet behavior - InstancesSheet: persona filtering, Archive/Unarchive via ArchiveConfirmDialog, 'Relay only' badge for non-local instances, archive trust unknown retry - Rows link to exact-pubkey profile; mutations gated by NipIaOwnerProof::Verified IMPORTANT: Start-control safeguard - handleStartPersonaWithSafeguard in UnifiedAgentsSection: opens Sheet when inventory loading/untrusted or active relay instance exists; prevents 3rd mint - Card-level focusable Instances (N) button with aria-expanded/aria-controls IMPORTANT: Acceptance tests — observation seam - SubmitObserver captures signed request + attempt count - 9035: asserts auth_tags.len()==1, empty condition, Postgres consent_path='owner' scoped by community, kind:8002 delta consent=owner + actor - 9036: kind:8003 delta consent=owner + actor - self: asserts 0 auth tags both directions - rejection: asserts exactly 1 attempt (no retry) - Fixture queries relay_members to assert actor absence (not just a comment) IMPORTANT: Classifier exact-one-tag rule - Count ALL auth tags (any first element='auth') before arity check - Wrong-arity → InvalidAuth; malformed+valid → MultipleAuthTags - Verify fetched kind:0 NIP-01 id/sig; authored by target; kind:0 - Tests: wrong-arity, malformed-plus-valid, bad-sig, missing-profile reachable IMPORTANT: Recovery-mode signing gate - capture_archive_scope checks identity_lost/keyring_locked inside epoch window - Tests: lost/locked both return Err containing 'recovery mode' Structural split: identity_archive.rs → inventory.rs + mod.rs - inventory module: paging, d-tag extraction, kind:0 fetch+verify, classification - mod.rs: scoped operation, classifier, archive/unarchive commands, shared helpers - Relay acceptance tests remain in-crate under relay_acceptance module Biome format fixes in UnifiedAgentsSection.tsx and InstancesSheet.tsx Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…coverage Fix relay URL race in inventory (Finding 2): - get_owned_agent_inventory now derives api_base_url from scope.relay_url_override (same pattern as scoped_archive_operation) — never re-reads state.relay_url_override after epoch capture - load_archive_snapshot accepts api_base_url: &str; uses query_relay_at + inline NIP-11 fetch at the scoped URL instead of fetch_relay_self(state) which read state independently - Removes relay_ws_url_with_override from inventory imports Add e2e bridge + Playwright spec (Finding 8): - e2eBridge.ts: case 'get_owned_agent_inventory' returns mock inventory - bridge.ts: MockBridgeOptions.ownedAgentInventory typed field - InstancesSheet.tsx: data-testid='instances-sheet' on SheetContent - agent-instances-sheet.spec.ts: 5 supplementary UI coverage tests - start-control safeguard opens Sheet on active relay instances - Sheet shows both relay instances when seeded - Archive button present for Verified instances with trusted state - Archive/Unarchive suppressed when archive state not trusted - No-third-mint: start intercepted when inventory is untrusted Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…enu gate
Addresses three concrete CI failures from pass-1 + committed tip:
Relay acceptance: both 9035 and 9036 panicked with
'postgres relay_members query: error serializing parameter 0'
Root cause: community_id is UUID NOT NULL; tokio_postgres cannot bind
&str as $1::uuid — the type mismatch is rejected at the client bind layer
before any server-side cast can run. Fix: cast the column side
community_id::text = $1 (text-to-text comparison, no bind type needed)
Both assert_actor_not_relay_member and assert_postgres_consent_path_owner
are updated.
Test observation seam: replace the SubmitObserver duplicate-code pattern
with the production test_hooks module. scoped_archive_operation now calls
test_hooks::notify_submit(&signed) (under #[cfg(test)]) just before the
relay POST, so the observer sees the PRODUCTION-signed event. The old
seam rebuilt auth-tag computation in the test file, which meant gutting
the fresh-mint wouldn't fail the seam — the new seam fails correctly.
Also adds submit_signed_event_at_with_keys usage for hook compatibility.
Dropdown Instances gate: PersonaActionsMenu only receives onViewInstances
when relayInstanceCount > 0. The unconditional prop broke two existing
integration tests expecting the original menu items ("share and keep
export separate", "team-managed personas do not expose editable actions")
because both tests don't seed ownedAgentInventory so count is 0.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…omplexity The thread_local OBSERVER field had an inline complex type that triggered -D clippy::type_complexity on the push hook. Extract it to a type alias. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…d shape
e2eBridge.ts ownedAgentInventory mock type now matches the Rust
OwnedAgentInventorySnapshot wire contract ({byPersonaId, unknown})
instead of the old flat {instances} field. The default fallback also
uses {byPersonaId: {}, unknown: []}.
All 5 installMockBridge calls in agent-instances-sheet.spec.ts updated
to seed byPersonaId: {[PERSONA_ID]: [...instances]} with personaId
present on each instance. The spec now exercises the persona-filter
branch in InstancesSheet (effectiveData.byPersonaId[persona.id]).
UnifiedAgentsSection.tsx: fix openInstancesSheet call-site arity
(was passing 2 args to a 1-arg function, caught by tsc --noEmit).
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Addresses all 6 blocking findings from Thufir pass 2:
1. [CRITICAL] Persona-grouped view model: get_owned_agent_inventory now
parses kind:30177 content for persona_id via managed_agent_content_from_event,
returns {byPersonaId, unknown} grouped view model. OwnedAgentInstance gains
personaId: Option<String> field. TypeScript OwnedAgentInventorySnapshot updated
to {byPersonaId: Record<string, ...>, unknown: [...]}.
2. [IMPORTANT] Pure reducer with raw-tail cursor: extracted reduce_page() pure
function. Cursor advances from raw page tail only (last event before dedup).
Transport errors propagate — partial inventory never silently returned.
Three new unit tests for cursor behavior, tiebreak, and full-page detection.
3. [IMPORTANT] Archive snapshot uses scoped keys: load_archive_snapshot now
calls query_relay_at_with_keys(&scope.keys) for NIP-98 auth. Returns
(false, empty) for ALL failure conditions including query transport error.
4. [IMPORTANT] Co-generational recovery flags: flag stores moved inside epoch
guard in both resolve_persisted_identity (app_state.rs) and
commit_imported_identity (commands/identity.rs). SeqCst ordering throughout.
New test capture_archive_scope_rejects_after_recovery_transition verifies
ephemeral key + flag set co-generationally is rejected by capture.
5. [IMPORTANT] Expired-bound test hardened: result.expect(...) now asserts relay
accepted the archive. Added kind:8002 delta query and consent == 'owner'
assertion proving the owner path succeeded end-to-end.
6. [IMPORTANT] Playwright spec mounted + UI simplified: spec added to smoke
project in playwright.config.ts. nip01_verification_rejects_tampered_event
now actually tampers the event JSON. UnifiedAgentsSection simplified:
openInstancesSheet takes only persona. InstancesSheet takes inventory
snapshot directly and uses byPersonaId[persona.id] for filtering.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…-agents-nav * origin/main: fix(mobile): merge relay recounts with locally seen thread replies (#4633) fix(desktop): enable message editing in Inbox (#2198) relay: fuzz WebSocket 1012 restart-close timing on graceful drain (BUZZ_DRAIN_JITTER_MS) (#4542) fix(desktop): outline the selected community (#4969) fix(desktop): clamp thread panel to channel surface (#4965) style(messages): increase username contrast (#4948) fix(desktop): rename generic attachment action from 'Attach image' to 'Attach file' (#2381) (#4304) fix(reactions): support max-length custom emoji (#3833) feat(desktop): allow leaving your final community (#3621) fix(buzz-agent): recover from context-window 400s instead of sticking (#4946) docs(persona-pack): fix stale desktop import instructions (#4500) fix(desktop): route macos notification clicks (#4799) feat(mobile): sync themes per community (#3767) feat(desktop): sync themes per community (#3653) feat(desktop): cap OpenClaw agent parallelism at 5 (#4019) fix(buzz-agent): scope handoff cap per turn, not per session lifetime (#4805) Fix mobile message timeline bounce (#4862) Polish mobile bottom sheets and profile cards (#4911) Fix media attachment actions (#4849) fix(desktop): remove join API token control (#4897) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
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.
Implements Plan v9 (approved by Thufir at event 7dd99118). Addresses all blocking findings from Thufir's pass 1 review.
What changed
CRITICAL: Inventory author/agent confusion
get_owned_agent_inventorynow extracts the agent pubkey from thekind:30177dtag (notev.pubkeywhich is the owner)kind:0separately; verifies NIP-01 id+signature before classifyingNipIaOwnerProofvariants preserved inOwnedAgentInstanceStructural split
identity_archive.rs→identity_archive/inventory.rs+identity_archive/mod.rskind:0fetch+verify, classificationExhaustive race-safe inventory
fetch_all_owned_30177pages to exhaustion with composite(until, before_id)cursorcapture_archive_scope(seqlock epoch) before any I/Odtags rejected; dedup with canonical(created_at DESC, id ASC)Classifier exact-one-tag rule
authbefore arity checkInvalidAuth; malformed+valid →MultipleAuthTagskind:0NIP-01 id/sig; authored by target; iskind:0Recovery-mode signing gate
capture_archive_scopechecksidentity_lost/keyring_lockedinside epoch windowErrcontaining "recovery mode"Workspace epoch protocol (
WorkspaceEpochWriteGuard)Droprestores next even on every exit pathApproved OwnedAgentInstance model + Sheet behavior
InstancesSheet: persona filtering, Archive/Unarchive viaArchiveConfirmDialog, "Relay only" badge for non-local instances, archive trust unknown retryNipIaOwnerProof::VerifiedarchiveStateTrustedunknown → retry/error, no mutation affordanceStart-control safeguard
handleStartPersonaWithSafeguardinUnifiedAgentsSection: opens Sheet when inventory loading/untrusted or active relay instance exists; prevents 3rd mintInstances (N)button witharia-expanded/aria-controlsRelay acceptance gate (in-crate,
#[ignore]d)SubmitObservercaptures signed request + attempt count without replacing shipping build/mint fnauth_tags.len()==1, empty condition, Postgresconsent_path='owner'scoped by community,kind:8002deltaconsent=owner+ actorkind:8003deltaconsent=owner+ actorrelay_membersto assert actor absence (SQL assertion, not just a comment)CI wiring
desktop-tauri-relay-acceptanceJustfile recipe (depends on_ensure-sidecar-stubs)DATABASE_URL+RELAY_API_URLenvifextended to includedesktop-rust; Tauri apt deps + Rust cache + raised timeoutLocal gates
tsc --noEmit): cleanMutation checks
(a) Gut fresh-mint body in
scoped_archive_operation(Verified → None)→ Relay acceptance tests would fail at
auth_tags.len() == 1assertion. Requires live relay — CI Backend Integration gate arbitrates. Local seam confirmed viascoped_archive_with_observation: observer captures 0 auth tags when Verified returns None.(b) Skip epoch check in
capture_archive_scope(bypass odd/even gate)→
epoch_protocol_mixed_generation_rejected_while_mid_transitionFAILED,epoch_protocol_writer_exclusion_and_capture_retryFAILED(c) Skip epoch increment in
begin_workspace_write(no odd transition)→
epoch_protocol_mixed_generation_rejected_while_mid_transitionFAILED,epoch_protocol_raii_always_restores_evenFAILED,epoch_protocol_writer_exclusion_and_capture_retryFAILEDrelay-reconnect.spec.ts attribution
desktop/tests/e2e/relay-reconnect.spec.tsis identical on bothduncan/instance-level-agents-navandorigin/main(git diff HEAD origin/main -- desktop/tests/e2e/relay-reconnect.spec.tsshows no diff). The failure at line 158 ("E2E relay state setter is not installed") depends on a global test bridge injected at build time. This is a pre-existing environment flake on main unrelated to this PR.Originated from channel thread: nostr:nevent1qqsj43nluuqwfhaqf6je6mq6edg0c4k2jkdmyknmjcmtlu4hd3jy70gpzamhxue69uhhyetvv9ujuerpd4hxatjda3j7un9wdeks6tcpzp4a1ffa06c47llnr6pxl4k93jwgvgfkkvkxq5xt93rz38fy5xlqvzqqqqqqyq4rz2n
Fixes the duplicate-agent cardinality incident per Plan v9 (approved by Thufir, event 7dd99118).