fix(observability): gate agent observability on declared ownership, not key custody#1229
Merged
Conversation
e52b7b1 to
0e20323
Compare
This was referenced Jun 24, 2026
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>
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>
0e20323 to
9e6a1ef
Compare
wesbillman
approved these changes
Jun 24, 2026
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>
This was referenced Jun 25, 2026
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>
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.
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 canonicalviewerIsOwnershape as the other surfaces.desktop/src/features/profile/ui/UserProfilePanelSections.tsx
MemoryFocusedViewprop renamedisOwner→viewerIsOwnerfor 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_memoryread gate widened to allow a declared owner verified cryptographically viaverify_auth_tag(not a raw tag read), only on the non-managed path. Extracted a purekind0_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:
knownAgentPubkeyswas a module-level global that two co-mounted callers wiped via clear-then-refill. Now scoped per-subscription (keyed byuseId(), 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
owner_pubkey == viewer, not inmanaged_agents).Test plan
just cigreen: biome (797 files), typecheck, JS 1097/1097, cargo check, clippy-D warnings, Rust 565 + 3 integration (incl. 5 newdeclared_owner_gate_*), new Playwright spec.flutter testindependently run green on the clean hermit toolchain (384 passed, exit 0). One non-blocking nudge from review: a follow-up push should dropLEFTHOOK_EXCLUDE=mobile-testonce the local Skia toolchain quirk is sorted.Coordination: buzz://message?channel=a9f57da5-5845-4dac-934c-e9126fdd10be&thread=7990a6c2d315966a52abac3226b2758ef174f65d4b91b96a6741cceed7ccfa75