Skip to content

fix(observability): gate agent observability on declared ownership, not key custody#1229

Merged
tellaho merged 5 commits into
mainfrom
tho/owner-pane-activity-gate
Jun 24, 2026
Merged

fix(observability): gate agent observability on declared ownership, not key custody#1229
tellaho merged 5 commits into
mainfrom
tho/owner-pane-activity-gate

Conversation

@tellaho

@tellaho tellaho commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

Overview

Category: fix
User Impact: Agent owners can now see their agent's activity feed, memory, and metadata even when the agent runs on another machine — not just when its key lives locally.

Problem: Owner-relevant UI across multiple surfaces gated on seckey-custody (useIsManagedAgent / managed_agents) as a stand-in for ownership. That stand-in is wrong precisely when a declared owner's agent runs elsewhere: they own the agent but hold no local key, so the UI hid their own agent's activity, memory, and metadata.

Solution: Swap the custody proxy for the real ownership signal — the NIP-OA declared owner (isCurrentUserOwner) — across every affected read surface. Encryption stays the true security boundary throughout (owner-key NIP-44 decrypt + relay #p-scoping), so these gates only decide whether the UI bothers to try; loosening them leaks nothing. The edit/archive signing path is untouched — only read gates were widened.

File changes

desktop/src/features/profile/ui/UserProfilePanel.tsx
Owner-aware gate on the profile pane activity feed, plus observer-bridge wiring so a remote-owned agent's stream is subscribed.

desktop/src/features/profile/ui/UserProfilePopover.tsx
Hover popover "view activity" affordance was too loose (showed for any role === "bot"). Now gated on the same canonical viewerIsOwner shape as the other surfaces.

desktop/src/features/profile/ui/UserProfilePanelSections.tsx
MemoryFocusedView prop renamed isOwnerviewerIsOwner for honesty — it was already fed the combined ownership value. No metadata-source change; the section already degrades clean for remote owners.

desktop/src/features/channels/ui/MembersSidebar.tsx, MembersSidebarMemberCard.tsx
Members sidebar "view activity" gate widened to mirror the pane — declared ownership, not backend locality.

desktop/src/features/agent-memory/hooks.ts, ui/MemorySection.tsx
Agent Memory UI gate widened to is_managed || is_declared_owner, in lockstep with the Rust gate.

desktop/src-tauri/src/commands/engrams.rs
get_agent_memory read gate widened to allow a declared owner verified cryptographically via verify_auth_tag (not a raw tag read), only on the non-managed path. Extracted a pure kind0_declares_viewer_owner() helper (behavior-identical) + 5 unit tests driving a verified kind:0 owner through the gate.

desktop/src-tauri/src/commands/identity_archive.rs
Minor wiring to support the declared-owner read path.

desktop/src/features/agents/observerRelayStore.ts
Fixed a pre-existing clobber: knownAgentPubkeys was a module-level global that two co-mounted callers wiped via clear-then-refill. Now scoped per-subscription (keyed by useId(), cleanup on unmount). Tightens posture — stale agents no longer linger trusted.

desktop/src/testing/e2eBridge.ts, desktop/tests/e2e/channels.spec.ts
Seeded a viewer-owned relay-agent fixture (nadia) and a live Playwright assertion exercising the sidebar gate-open path that the prior fixture (mira) never reached.

Reproduction Steps

  1. Run the desktop app as a user who is the declared owner of an agent whose secret key is NOT local (i.e. the agent runs on another machine — owner_pubkey == viewer, not in managed_agents).
  2. Open that agent's profile pane — the activity feed now renders instead of being hidden.
  3. Open the agent's Memory section — it renders the owner-scoped view.
  4. Hover the agent in the members list — the "view activity" affordance appears (previously shown for any bot regardless of ownership; now correctly owner-gated).
  5. Confirm a non-owner viewer sees none of the above for that same agent.

Test plan

  • just ci green: biome (797 files), typecheck, JS 1097/1097, cargo check, clippy -D warnings, Rust 565 + 3 integration (incl. 5 new declared_owner_gate_*), new Playwright spec.
  • Mobile flutter test independently run green on the clean hermit toolchain (384 passed, exit 0). One non-blocking nudge from review: a follow-up push should drop LEFTHOOK_EXCLUDE=mobile-test once the local Skia toolchain quirk is sorted.
  • Manually verified via Playwright: owner pane / sidebar / memory render owner-scoped views for a remote declared owner.

Coordination: buzz://message?channel=a9f57da5-5845-4dac-934c-e9126fdd10be&thread=7990a6c2d315966a52abac3226b2758ef174f65d4b91b96a6741cceed7ccfa75

@tellaho tellaho changed the title Owner-gate agent observability surfaces on declared ownership, not key custody gate agent observability on declared ownership, not key custody Jun 24, 2026
@tellaho tellaho changed the title gate agent observability on declared ownership, not key custody fix(observability): gate agent observability on declared ownership, not key custody Jun 24, 2026
@tellaho tellaho force-pushed the tho/owner-pane-activity-gate branch from e52b7b1 to 0e20323 Compare June 24, 2026 01:20
tellaho added a commit that referenced this pull request Jun 24, 2026
The 'channel intro stays hidden while paginating past the timeline cap'
test seeds ~2160 events and drives ~200 mouse.wheel iterations with 80ms
settle waits. It runs ~24s on fast local hardware but the GH-hosted runner
is ~2.5-3x slower on this loop, leaving near-zero headroom under the 60s
cap. A passing CI run measured 45.5s of the 60s budget; a slower runner
tips it over 60s, which has been blocking PR #1229 (the failing test is
byte-identical to main and unrelated to that PR's changes). Bump to 90s to
restore the ~3x margin CI needs.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
tellaho added a commit that referenced this pull request Jun 24, 2026
The 'channel intro stays hidden while paginating past the timeline cap'
test seeds ~2160 events and drives ~200 mouse.wheel iterations with 80ms
settle waits. It runs ~24s on fast local hardware but the GH-hosted runner
is ~2.5-3x slower on this loop, leaving near-zero headroom under the 60s
cap. A passing CI run measured 45.5s of the 60s budget; a slower runner
tips it over 60s, which has been blocking PR #1229 (the failing test is
byte-identical to main and unrelated to that PR's changes). Bump to 90s to
restore the ~3x margin CI needs.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
@tellaho tellaho marked this pull request as ready for review June 24, 2026 08:05
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w and others added 5 commits June 24, 2026 14:03
The agent profile pane gated the activity feed, memory, and owner fields on `useIsManagedAgent` (does THIS desktop hold the agent's seckey). That is the wrong signal for a remote-running agent: a declared owner who manages the agent on another desktop saw an empty pane even though every real boundary is server-side.

Switch the visibility gates to a combined `viewerIsOwner = isCurrentUserOwner || isOwner` check (NIP-OA declared owner OR locally-managed, so older agents without an advertised owner still work for the managing desktop), and seed the observer bridge from the relay agent when the viewer is the declared owner. This mirrors the composer-area ingress: the bridge subscribes on the owner's own pubkey and decrypts telemetry with the owner's key, so no agent seckey or new crypto is needed. Edit stays seckey-gated (`canEditAgent`) since it genuinely needs the key.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
…lity

The members sidebar gated the "View activity" affordance on `managedAgent?.backend.type === "local"`, the same wrong signal the profile pane carried: a declared owner whose agent runs on another desktop got no activity affordance for that channel member, even though every real boundary is server-side.

Compute `viewerIsOwner` in the parent's `renderMemberCard` from the NIP-OA declared owner (`profile.ownerPubkey == currentPubkey`, already fetched via the batch profiles query) and pass it into the card, then gate `canViewActivity` on `viewerIsOwner || locally-managed`. This mirrors the pane fix and keeps the local-managed path for older agents without an advertised owner. No new wiring is needed: a remote owned agent that's a channel member is already seeded into the observer bridge via the channel agent session candidates, so the opened activity view streams as-is.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
A declared NIP-OA owner whose agent runs elsewhere holds no local seckey, so the managed_agents proxy gate wrongly hid their own memory. Widen both the UI and Rust read gates to also accept the declared-owner path, decrypting with the owner's own key (the agent is the NIP-44 conversation peer, so the agent seckey is never needed). Encryption + relay #p-scoping remain the real boundary; the gate only decides whether to try. Edit/archive (kind:9035) signing path left untouched.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
…iption

Surface #4: the profile hover popover gated the "view activity" affordance on bot-ness alone (role === bot), so it showed for every viewer and flickered by mount context. Gate it on the declared-owner signal too, mirroring the pane/sidebar/memory fixes (viewerIsOwner = isCurrentUserOwner || isOwner). The real boundary stays server-side; this only decides whether to paint the button.

Clobber fix: useManagedAgentObserverBridge keyed the trusted-pubkey set off a single module-level Set that each caller cleared and refilled from its own agent list. Two co-mounted callers (channel screen + profile panel) wiped each other out. Track each subscriber's contribution by React.useId() and recompute the union, with cleanup on unmount.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
…misleading prop

Rust auth-branch test (#2 follow-up): extracted the declared-owner predicate from get_agent_memory into a pure kind0_declares_viewer_owner() and added 5 unit tests driving a verified kind:0 owner declaration through it (opens for verified owner, case-insensitive, refuses non-owner / no-auth-tag / missing kind:0). Uses the same verify_auth_tag crypto path as the live gate; behavior-identical to the inlined block.

Sidebar fixture (#3 follow-up): seeded nadia, a relay-classified bot owned by the mock viewer but with no local managed record, plus a Playwright assertion that the members sidebar exposes view-activity for her. This exercises the declared-owner gate-open path that mira never reached (she was never bot-classified).

Naming rename (#2 follow-up): MemoryFocusedView's prop was named isOwner but fed the combined viewerIsOwner; renamed for honesty, value unchanged.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
@tellaho tellaho force-pushed the tho/owner-pane-activity-gate branch from 0e20323 to 9e6a1ef Compare June 24, 2026 21:05
@tellaho tellaho merged commit e993b9e into main Jun 24, 2026
44 of 46 checks passed
@tellaho tellaho deleted the tho/owner-pane-activity-gate branch June 24, 2026 21:57
tellaho added a commit that referenced this pull request Jun 25, 2026
…ship model

Replay of PR #1061 (tho/activity-ui-polish) onto fresh main: lands the
20 UI-polish commits as one net diff, reconciled with #1229's merged
declared-ownership model (viewerIsOwner = isCurrentUserOwner || isOwner)
and #1089's content-visibility-auto virtualization.

Notable reconciles:
- markdown.tsx: ported the compact/tight variant system into main's
  newer lightbox/spoiler markdown component (rather than overwriting it),
  layering variant density/leading overrides after the base owl-spacing
  so tailwind-merge wins. Dropped the branch's hardcoded text-[15px] in
  favor of main's rem-token text-sm base (post-#1052 zoom-safe scale).
- agentSessionTranscript.ts: pass TranscriptItemContext (not channelId).
- managed_agents: thread avatar_url through ManagedAgentSummary so the
  transcript renders the assistant-bubble avatar from the pinned record
  snapshot; bumped runtime.rs size override 2001 -> 2002 for the +1 line.

Observer-seed screenshots intentionally excluded (separate follow-up).

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
tellaho added a commit that referenced this pull request Jun 30, 2026
…ship model

Replay of PR #1061 (tho/activity-ui-polish) onto fresh main: lands the
20 UI-polish commits as one net diff, reconciled with #1229's merged
declared-ownership model (viewerIsOwner = isCurrentUserOwner || isOwner)
and #1089's content-visibility-auto virtualization.

Notable reconciles:
- markdown.tsx: ported the compact/tight variant system into main's
  newer lightbox/spoiler markdown component (rather than overwriting it),
  layering variant density/leading overrides after the base owl-spacing
  so tailwind-merge wins. Dropped the branch's hardcoded text-[15px] in
  favor of main's rem-token text-sm base (post-#1052 zoom-safe scale).
- agentSessionTranscript.ts: pass TranscriptItemContext (not channelId).
- managed_agents: thread avatar_url through ManagedAgentSummary so the
  transcript renders the assistant-bubble avatar from the pinned record
  snapshot; bumped runtime.rs size override 2001 -> 2002 for the +1 line.

Observer-seed screenshots intentionally excluded (separate follow-up).

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
tellaho added a commit that referenced this pull request Jun 30, 2026
…ship model

Replay of PR #1061 (tho/activity-ui-polish) onto fresh main: lands the
20 UI-polish commits as one net diff, reconciled with #1229's merged
declared-ownership model (viewerIsOwner = isCurrentUserOwner || isOwner)
and #1089's content-visibility-auto virtualization.

Notable reconciles:
- markdown.tsx: ported the compact/tight variant system into main's
  newer lightbox/spoiler markdown component (rather than overwriting it),
  layering variant density/leading overrides after the base owl-spacing
  so tailwind-merge wins. Dropped the branch's hardcoded text-[15px] in
  favor of main's rem-token text-sm base (post-#1052 zoom-safe scale).
- agentSessionTranscript.ts: pass TranscriptItemContext (not channelId).
- managed_agents: thread avatar_url through ManagedAgentSummary so the
  transcript renders the assistant-bubble avatar from the pinned record
  snapshot; bumped runtime.rs size override 2001 -> 2002 for the +1 line.

Observer-seed screenshots intentionally excluded (separate follow-up).

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
tellaho added a commit that referenced this pull request Jun 30, 2026
…ship model

Replay of PR #1061 (tho/activity-ui-polish) onto fresh main: lands the
20 UI-polish commits as one net diff, reconciled with #1229's merged
declared-ownership model (viewerIsOwner = isCurrentUserOwner || isOwner)
and #1089's content-visibility-auto virtualization.

Notable reconciles:
- markdown.tsx: ported the compact/tight variant system into main's
  newer lightbox/spoiler markdown component (rather than overwriting it),
  layering variant density/leading overrides after the base owl-spacing
  so tailwind-merge wins. Dropped the branch's hardcoded text-[15px] in
  favor of main's rem-token text-sm base (post-#1052 zoom-safe scale).
- agentSessionTranscript.ts: pass TranscriptItemContext (not channelId).
- managed_agents: thread avatar_url through ManagedAgentSummary so the
  transcript renders the assistant-bubble avatar from the pinned record
  snapshot; bumped runtime.rs size override 2001 -> 2002 for the +1 line.

Observer-seed screenshots intentionally excluded (separate follow-up).

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@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.

2 participants