From 77eeba6eaae882ed69ff7af8ea7d3849792539cb Mon Sep 17 00:00:00 2001 From: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Date: Thu, 6 Aug 2026 21:21:07 -0400 Subject: [PATCH] feat(relay): accept kind:30179 private managed-agent events at ingest Relay-only carve-out of the ingest half of #4999, so the shared relay can accept the private managed-agent head before the desktop half lands. Problem: kind:30179 (NIP-PMA private managed-agent config, owner-encrypted) is fully specified on main since #4593 -- constant, AUTHOR_ONLY_KINDS membership, result-gated reads, req/count/event/bridge author-only gates, parameterized-replaceable storage -- but generic EVENT ingest still rejects it as an unknown kind, so no client can publish one. Every end-to-end exercise of the desktop half currently requires a locally built relay. Change: two semantic lines in ingest.rs, byte-identical to the ingest hunk of #4999 at 6f486e88: - required_scope_for_kind: 30179 requires Scope::UsersWrite, same arm as its public sibling 30177 and the other owner-authored NIP-AP kinds; - is_global_only_kind: 30179 is owner-global, keyed (pubkey, kind, d-tag); a stray h tag must not channel-scope it. The remainder is import reflow plus replacing the guard test with positive assertions (UsersWrite scope, global-only, no h-scope requirement). Why the guard's precondition is met: the removed test pinned "must not enter generic EVENT ingest before privacy and aggregate CAS deploy". Both halves are resolved. Privacy: the author-only read gates for 30179 shipped to main with #4593 (req.rs pre-filter + result gates, count.rs, event.rs fanout, bridge pre-filter) -- only the author can read the event back. Aggregate CAS: #4999 settled generation as advisory -- the g tag is shape-validated, never relay-enforced; last-write-wins per coordinate is the contract of record, so no CAS mechanism is pending on the relay side. Why this is inert to existing relays and clients: no production desktop code on main authors kind:30179 (the codec has zero non-test callers), so this accepts a kind nobody can produce yet. Content is opaque NIP-44 ciphertext to the relay; reads remain author-only; storage semantics are the standard parameterized-replaceable path already exercised by 30175-30178. No schema, config, or migration changes. Contract of record for kind:30179 remains as documented in #4999; the desktop half rebases to pure-desktop scope once this lands. Co-authored-by: Tyler Longwell Signed-off-by: Tyler Longwell --- crates/buzz-relay/src/handlers/ingest.rs | 31 ++++++++++++------------ 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/crates/buzz-relay/src/handlers/ingest.rs b/crates/buzz-relay/src/handlers/ingest.rs index cd9f20b5f4..55a468144c 100644 --- a/crates/buzz-relay/src/handlers/ingest.rs +++ b/crates/buzz-relay/src/handlers/ingest.rs @@ -28,12 +28,13 @@ use buzz_core::kind::{ KIND_NIP29_EDIT_METADATA, KIND_NIP29_JOIN_REQUEST, KIND_NIP29_LEAVE_REQUEST, KIND_NIP29_PUT_USER, KIND_NIP29_REMOVE_USER, KIND_NIP43_LEAVE_REQUEST, KIND_NIP65_RELAY_LIST_METADATA, KIND_PERSONA, KIND_PIN_LIST, KIND_PRESENCE_UPDATE, - KIND_PRODUCT_FEEDBACK, KIND_PROFILE, KIND_PROJECT, KIND_REACTION, KIND_READ_STATE, KIND_REPORT, - KIND_STREAM_MESSAGE, KIND_STREAM_MESSAGE_BOOKMARKED, KIND_STREAM_MESSAGE_DIFF, - KIND_STREAM_MESSAGE_EDIT, KIND_STREAM_MESSAGE_PINNED, KIND_STREAM_MESSAGE_SCHEDULED, - KIND_STREAM_MESSAGE_V2, KIND_STREAM_REMINDER, KIND_TEAM, KIND_TEAM_CATALOG, KIND_TEXT_NOTE, - KIND_USER_STATUS, KIND_WORKFLOW_DEF, KIND_WORKFLOW_TRIGGER, RELAY_ADMIN_ADD_MEMBER, - RELAY_ADMIN_CHANGE_ROLE, RELAY_ADMIN_REMOVE_MEMBER, RELAY_ADMIN_SET_WORKSPACE_PROFILE, + KIND_PRIVATE_MANAGED_AGENT, KIND_PRODUCT_FEEDBACK, KIND_PROFILE, KIND_PROJECT, KIND_REACTION, + KIND_READ_STATE, KIND_REPORT, KIND_STREAM_MESSAGE, KIND_STREAM_MESSAGE_BOOKMARKED, + KIND_STREAM_MESSAGE_DIFF, KIND_STREAM_MESSAGE_EDIT, KIND_STREAM_MESSAGE_PINNED, + KIND_STREAM_MESSAGE_SCHEDULED, KIND_STREAM_MESSAGE_V2, KIND_STREAM_REMINDER, KIND_TEAM, + KIND_TEAM_CATALOG, KIND_TEXT_NOTE, KIND_USER_STATUS, KIND_WORKFLOW_DEF, KIND_WORKFLOW_TRIGGER, + RELAY_ADMIN_ADD_MEMBER, RELAY_ADMIN_CHANGE_ROLE, RELAY_ADMIN_REMOVE_MEMBER, + RELAY_ADMIN_SET_WORKSPACE_PROFILE, }; use buzz_core::tenant::TenantContext; use buzz_core::verification::verify_event; @@ -263,7 +264,7 @@ fn required_scope_for_kind(kind: u32, event: &Event) -> Result Ok(Scope::MessagesWrite), KIND_CONTACT_LIST | KIND_READ_STATE | KIND_USER_STATUS | KIND_AGENT_ENGRAM | KIND_EVENT_REMINDER | KIND_PERSONA | KIND_TEAM | KIND_MANAGED_AGENT - | KIND_TEAM_CATALOG | super::push_lease::KIND_PUSH_LEASE => { + | KIND_PRIVATE_MANAGED_AGENT | KIND_TEAM_CATALOG | super::push_lease::KIND_PUSH_LEASE => { Ok(Scope::UsersWrite) } // NIP-AM: agent turn metrics are agent-authored global events (encrypted to owner). @@ -476,6 +477,7 @@ pub(crate) fn is_global_only_kind(kind: u32) -> bool { // (pubkey, kind, d_tag). A stray `h` tag must not channel-scope them. | KIND_TEAM | KIND_MANAGED_AGENT + | KIND_PRIVATE_MANAGED_AGENT | KIND_TEAM_CATALOG // NIP-34: git events use `a` tags (repo reference), not `h` tags (channel scope). // Parameterized replaceable kinds are keyed by (pubkey, kind, d_tag). @@ -3338,15 +3340,14 @@ mod tests { } #[test] - fn private_managed_agent_kind_remains_rejected_until_atomic_ingest_exists() { - assert!( - required_scope_for_kind( - buzz_core::kind::KIND_PRIVATE_MANAGED_AGENT, - &make_dummy_event(), - ) - .is_err(), - "kind 30179 must not enter generic EVENT ingest before privacy and aggregate CAS deploy" + fn private_managed_agent_kind_is_owner_scoped_global_user_data() { + let event = make_dummy_event(); + assert_eq!( + required_scope_for_kind(KIND_PRIVATE_MANAGED_AGENT, &event), + Ok(Scope::UsersWrite) ); + assert!(is_global_only_kind(KIND_PRIVATE_MANAGED_AGENT)); + assert!(!requires_h_channel_scope(KIND_PRIVATE_MANAGED_AGENT)); } #[test]