Skip to content

feat(relay): allow agent owners to edit/manage agent-owned content#1403

Merged
wpfleger96 merged 7 commits into
mainfrom
duncan/human-edit-agent-content
Jun 30, 2026
Merged

feat(relay): allow agent owners to edit/manage agent-owned content#1403
wpfleger96 merged 7 commits into
mainfrom
duncan/human-edit-agent-content

Conversation

@wpfleger96

Copy link
Copy Markdown
Collaborator

What

Humans who own agents (via NIP-OA) can now edit, delete, and manage channels/messages authored by those agents using their own key — no impersonation, no re-signing.

Previously a human owner was blocked at four authz sites because their key didn't match the agent's key. This PR widens each predicate to also accept is_agent_owner(agent, actor) when the primary check fails.

Four relay sites changed

Kind Function Change
40003 validate_edit_ownership (ingest.rs) author == actor → OR is_agent_owner(author, actor)
9005 validate_admin_event (side_effects.rs) After owner/admin check, add is_agent_owner(message_author, actor) fallback
9002 privileged tags validate_admin_event (side_effects.rs) After owner/admin check, add is_agent_owner(channel_owner_agent, actor) fallback
9008 validate_admin_event (side_effects.rs) After owner check, add is_agent_owner(channel_owner_agent, actor) fallback

All four sites reuse the existing is_agent_owner() DB predicate (no schema change). The owning human is not required to be a channel member for 9002/9008/9005 — intentional divergence from kind:9001 (which requires membership). This matches the real use case: a human owns a bot that autonomously created a channel the human was never added to.

Provenance

Free: every edit/delete is its own event signed by the editor's own key. The edit event's pubkey is the human's, distinct from the original agent author — clients can render "edited by owner" by comparing the two.

Reuses existing infrastructure

  • is_agent_owner in buzz-db/src/user.rs (already used for kind:9001 member-remove agent-owner fast-path, NIP-OA auth backfill, observer frame auth)
  • No DB schema change. No new primitives.

Testing

  • 427 existing relay unit tests pass unchanged.
  • Adds authenticate_with_nip_oa() to BuzzTestClient for NIP-OA test setup.
  • Adds buzz-sdk as a dev-dependency of buzz-test-client (needed for compute_auth_tag/parse_auth_tag).
  • Adds 11 e2e tests in e2e_human_edit_agent_content.rs (#[ignore], require live relay):
    • Owner can edit / delete agent message (40003, 9005)
    • Owner can rename / archive / delete agent channel (9002, 9008)
    • Agent can still self-edit message and self-delete channel
    • Third parties are rejected at all four sites

npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 and others added 4 commits June 30, 2026 16:06
Humans who own agents (established via NIP-OA) can now edit, delete,
and manage channels/messages authored by those agents using their own
key — without impersonation or re-signing.

The relay widens four authorization predicates to fall back to
is_agent_owner() when the actor is not the author/owner/admin:

- validate_edit_ownership (kind:40003): author == actor OR owner of agent
- validate_admin_event 9005 (DELETE_EVENT): also accept agent-owner
- validate_admin_event 9002 privileged-tag branch: also accept agent-owner
- validate_admin_event 9008 (DELETE_GROUP): also accept agent-owner

All four sites reuse the existing is_agent_owner() DB predicate and
preserve provenance — the editor's own pubkey is on the edit event.
Non-members are allowed for 9002/9008/9005 when they are agent owners
(intentional divergence from kind:9001, which requires channel membership).

Adds authenticate_with_nip_oa() to BuzzTestClient and eleven e2e tests
covering: owner-allowed, third-party-rejected, and agent-self-allowed
scenarios at each of the four sites.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Fix 1 [blocking]: per-kind validators now run for 40003/9002/9005/9008 regardless
of channel membership. The global check_channel_membership gate blocked non-member
owning humans on private agent channels before any of the four new predicates could
run. Add the four kinds to the skip_membership set (alongside 9021/9007); each
validator independently proves authorization and fails closed.

Fix 2 [blocking]: replace the inline first-owner-only check in 9002 and 9008 with
a new actor_owns_any_owner_agent() helper that checks all active owner-role members.
The previous members.find(role == owner) resolved to the oldest owner by join date,
silently denying a valid owner-of-agent when their agent was not the first owner
(co-ownership, later promotion). The helper also eliminates the duplicated inline
logic between the two call sites.

Fix 3 [blocking]: remove unused RelayMessage import and the stale suppression
comment referencing Duration/Filter that were already absent. These caused a
hard clippy -D warnings failure.

Add four private-channel e2e tests (one per kind) as regression guards for Fix 1.
These tests would have caught the original private-channel blind spot.

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

Adding 40003/9005 to skip_membership (Pass 1 fix) also widened the
self-author fast-path: a removed private-channel member could edit or
delete their own historical messages after access was revoked (Thufir
Pass 2, Option A).

Fix: inside the author==actor branch of validate_edit_ownership (40003)
and the 9005 validator, check membership/open-visibility before
returning Ok. Active members and open-channel authors remain allowed;
non-member self-authors on private channels fall through to the
owner/admin/owner-of-agent check and are rejected there.

The owner-of-agent non-member exception is untouched: it lives in the
author!=actor branch (40003) and the final is_agent_owner fallthrough
(9005), both still reachable for private channels.

Add two E2E regression tests (ignored, require live relay):
- test_removed_author_cannot_edit_own_message_in_private_channel
- test_removed_author_cannot_delete_own_message_in_private_channel

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Whitespace-only formatting drift in side_effects.rs and
e2e_human_edit_agent_content.rs; no logic change.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96 wpfleger96 force-pushed the duncan/human-edit-agent-content branch from 4f3cfd0 to 1ad094d Compare June 30, 2026 20:06
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 and others added 3 commits June 30, 2026 16:49
Wire the relay's owner-of-agent permission gate into the desktop UI so
the controls that were always permitted by the relay now appear in the
client.

Two sites gated:
- Message edit/delete (TimelineMessageList): extend canEdit/canDelete to
  also allow when ownsAuthorAgent() is true for the message author, using
  the ownerPubkey on the author's UserProfileSummary (the same NIP-OA
  record the relay gates on).
- Channel manage/delete (ChannelManagementSheet): fetch profiles for
  owner-role channel members via useUsersBatchQuery; set
  canManageOwnedAgentChannel when any owner-role member is an agent whose
  ownerPubkey matches the current user. OR this into canManageChannel and
  isOwner so the Edit quick-action and Delete button both appear.

Both gates use ownerPubkey (NIP-OA, server-authoritative) rather than
the local managed-agents list, keeping the client and relay in sync by
construction.

Adds an ownsAuthorAgent() helper in identity.ts and a Playwright spec
(human-edit-agent-content.spec.ts) with four tests: owned-agent message
shows Edit/Delete, unowned agent message does not, channel with owned
agent as owner shows Edit, channel with no ownership claim does not.

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

Fix 1 (IMPORTANT): Extract canManageMessageForCurrentUser as a shared
helper in features/messages/lib/canManageMessage.ts and wire it into
MessageThreadPanel (both thread-head and reply sites), replacing the
old private canManageMessage that only matched self-author. Eliminates
the duplication that caused the thread-panel false-negative.

Fix 2 (IMPORTANT): Extend Playwright spec to actually drive the
controls — edit an owned-agent message and assert new content renders,
delete and assert the row is gone, edit channel name via the
owned-agent-owner path and assert the updated title, negative case
confirms unowned agent shows no Edit/Delete items in the dropdown.
Add delete_message to the mock bridge so the delete mutation round-
trips cleanly in tests. Register spec in playwright.config.ts smoke
project so CI picks it up.

Fix 3 (MINOR): Rename isOwner → isSelfOwner + canDeleteChannel in
ChannelManagementSheet so the name reflects the capability not the
membership state; sub-component prop name (isOwner) and downstream
usage unchanged.

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

Thread-panel regression test: replace the full delete-confirm-assert-gone
flow (which cannot cross the React Query cache boundary from the thread panel
to the main timeline in the E2E harness) with the alertdialog-only assertion
Thufir explicitly accepted. The alertdialog appearing proves
canManageMessageForCurrentUser is wired into MessageThreadPanel — the exact
regression guard from pass 1. Full delete/cache behavior is already covered
by the main-timeline delete test.

isOwner rename: eliminate the misleading prop name throughout the channel
management stack. ChannelManagementPanelContent and
ChannelManagementModerationActions both rename isOwner to canDeleteChannel
in their props interfaces, destructures, and usages. The two outer call
sites in ChannelManagementSheet updated accordingly. No behavior change.

Tombstone comment: remove the stale canManageMessage comment from
MessageThreadPanel.tsx that predated the shared-helper extraction.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96 wpfleger96 merged commit 0042c8e into main Jun 30, 2026
29 checks passed
@wpfleger96 wpfleger96 deleted the duncan/human-edit-agent-content branch June 30, 2026 22:31
tellaho pushed a commit that referenced this pull request Jul 1, 2026
…-preview

* origin/main:
  fix(relay): enable Redis TLS for rediss:// (ElastiCache) (#1417)
  chore(release): release Buzz Desktop version 0.3.40 (#1414)
  fix(desktop): stabilize channel-timeline scrollback with per-row height reserves (#1413)
  fix(sidebar): trim working badge label and name working agents in tooltip (#1408)
  Mobile tab bar polish (#1368)
  feat(desktop): let thread pane expand on ultrawide monitors (#1407)
  chore(release): release Buzz Desktop version 0.3.39 (#1410)
  fix: close cross-process keychain race and namespace dev-build nest (#1409)
  feat(relay): allow agent owners to edit/manage agent-owned content (#1403)
  fix(media): support IRSA/credential-chain S3 auth and configurable signing region (#1406)
  fix(desktop): fold baked build env into in-process model discovery (#1376)
  docs: link VISION_ACTIVITY from the VISION index (#1405)
tellaho pushed a commit that referenced this pull request Jul 1, 2026
…vity-embed

* origin/main:
  fix(relay): enable Redis TLS for rediss:// (ElastiCache) (#1417)
  chore(release): release Buzz Desktop version 0.3.40 (#1414)
  fix(desktop): stabilize channel-timeline scrollback with per-row height reserves (#1413)
  fix(sidebar): trim working badge label and name working agents in tooltip (#1408)
  Mobile tab bar polish (#1368)
  feat(desktop): let thread pane expand on ultrawide monitors (#1407)
  chore(release): release Buzz Desktop version 0.3.39 (#1410)
  fix: close cross-process keychain race and namespace dev-build nest (#1409)
  feat(relay): allow agent owners to edit/manage agent-owned content (#1403)
  fix(media): support IRSA/credential-chain S3 auth and configurable signing region (#1406)
  fix(desktop): fold baked build env into in-process model discovery (#1376)
  docs: link VISION_ACTIVITY from the VISION index (#1405)
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