Skip to content

feat(relay): gate kind 30178 team-catalog reads behind the shared tag - #3358

Merged
wpfleger96 merged 1 commit into
mainfrom
duncan/relay-team-catalog-shared-gate
Jul 30, 2026
Merged

feat(relay): gate kind 30178 team-catalog reads behind the shared tag#3358
wpfleger96 merged 1 commit into
mainfrom
duncan/relay-team-catalog-shared-gate

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Jul 28, 2026

Copy link
Copy Markdown
Member

Team catalog projections (kind:30178) embed every member's system prompt, so they need the same read gate personas already have: only the author sees an unshared event. The gate was hardcoded to kind:30175 at six read surfaces plus the SQL pushdown, so rather than adding a second special case it becomes kind-generic over SHARED_GATED_KINDS = {30175, 30178}.

Kind 30178

New parameterized-replaceable kind, addressed by (pubkey_o, 30178, team_id). It embeds sanitized member projections instead of referencing kind:30175 heads — a foreign reader of a shared team could not otherwise hydrate members whose own persona events are unshared or, for built-ins, absent entirely. kind:30176's wire body is untouched, so device sync keeps its contract.

Kind-generic shared gate

buzz_core::kind replaces is_persona_shared_kind / is_unshared_persona_event / persona_event_is_shared with SHARED_GATED_KINDS and the kind-agnostic is_shared_gated_kind / is_unshared_gated_event / event_is_shared. Every read surface consults the set:

Surface File
REQ historical delivery + ids lookup crates/buzz-relay/src/handlers/req.rs
Live fan-out crates/buzz-relay/src/handlers/event.rs
COUNT fallback crates/buzz-relay/src/handlers/count.rs
NIP-98 HTTP /query, /count, /search crates/buzz-relay/src/api/bridge.rs
Pre-LIMIT SQL pushdown crates/buzz-db/src/event.rs

The SQL clause generalizes from kind != 30175 to kind NOT IN (...) bound from SHARED_GATED_KINDS, still applied before ORDER BY … LIMIT so a page of newer private events cannot starve an older shared one off the candidate set. EventQuery::persona_reader is renamed shared_gated_reader and needs_persona_filtering to needs_shared_gate_filtering to match.

Because the buzz-core rename has consumers outside the relay, the four desktop call sites of persona_event_is_shared travel with it: desktop/src-tauri/src/commands/personas/pending.rs, desktop/src-tauri/src/event_sync.rs, and two in desktop/src-tauri/src/managed_agents/persona_events.rs. Each call is unchanged apart from the name — the persona shared projection behaves exactly as before.

Ingest validation

validate_persona_envelope splits into two reusable pieces — validate_shared_tag (exactly-two-element ["shared","true"], at most one occurrence) and single_bounded_d_tag (exactly one d tag, non-empty, <=64 chars, no ASCII control characters or whitespace). validate_team_catalog_envelope composes both; personas additionally keep the slug grammar ^[a-z0-9][a-z0-9_-]{0,63}$.

kind:30178 deliberately does not get the slug grammar. Team ids are UUIDs or built-in identifiers such as builtin-team:welcome, and the colon is not slug-legal; rewriting ids to fit would break NIP-33 addressing against the team's own kind:30176 head. The non-empty and exactly-one checks are load-bearing regardless — without them generic NIP-33 storage maps a missing d onto (pubkey_o, 30178, "") and every team overwrites its predecessor.

The exact two-element shared shape is enforced because the SQL visibility clause is JSONB containment (tags @> '[["shared","true"]]'), which would match a three-element superset such as ["shared","true","extra"].

kind:30178 is also added to the Scope::UsersWrite allowlist and to is_global_only_kind, so a stray h tag cannot channel-scope an owner-authored definition.

Deferred

kind:30176 is deliberately not a gate member. Its writers never emit shared, so catalog opt-in semantics do not describe it — it needs owner-private reads driven by an authenticated principal set, tracked as a separate follow-up.

Tests

  • 19 new ingest.rs unit tests covering the 30178 envelope (UUID and colon d tags, 64-char boundary, non-ASCII bound, empty/valueless/duplicate/missing d, embedded newline, shared false/three-element/duplicate, scope and global-only membership).
  • Persona regressions for the valueless ["d"] shapes, since the d-tag helper is shared by both validators.
  • Existing kind.rs gate tests generalized and extended to assert the gate applies to 30178 as it does to 30175.
  • New crates/buzz-test-client/tests/e2e_team_catalog.rs: 9 WS-level tests over a live relay covering author reads of unshared heads, foreign omission from REQ, ids-lookup denial, COUNT existence-leak, share and unshare transitions, and the mixed-kind filter case.
  • .github/workflows/ci.yml adds --test e2e_team_catalog to the Relay E2E job so the new suite runs.

Docs

docs/nips/NIP-AP.md gains a "Team catalog projection: kind:30178" section and an "Ingest validation: kind:30178" subsection, records the gate as kind-generic, documents 30178 deletion vs. unshare semantics, and adds a security note that sharing a team exposes every member's instructions even when that member's own kind:30175 head is unshared.

@wpfleger96
wpfleger96 requested a review from a team as a code owner July 28, 2026 19:03
@wpfleger96
wpfleger96 force-pushed the duncan/relay-team-catalog-shared-gate branch from 88ea0ca to 5307fed Compare July 28, 2026 19:24
@wpfleger96
wpfleger96 marked this pull request as draft July 28, 2026 19:27
@wpfleger96
wpfleger96 force-pushed the duncan/relay-team-catalog-shared-gate branch from 5307fed to d724013 Compare July 28, 2026 19:52
Team catalog projections carry every member's system prompt, so they need
the same read gate personas already have: only the author sees an unshared
event. The gate was hardcoded to kind 30175 in six read surfaces plus the
SQL pushdown, so instead of adding a second special case it becomes
kind-generic over SHARED_GATED_KINDS = {30175, 30178}.

Team ids embed a colon (builtin-team:welcome), which the persona slug
grammar rejects, so 30178 gets its own d-tag rule: exactly one non-empty
tag, <=64 chars, no control chars or whitespace. Without the exactly-one
and non-empty checks every team collapses onto (pubkey, 30178, "") and
silently overwrites its predecessor.

Kind 30176 is deliberately left out of the gate: its reads need an
authenticated principal set rather than a boolean tag, tracked separately.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96
wpfleger96 force-pushed the duncan/relay-team-catalog-shared-gate branch from d724013 to da06aa4 Compare July 28, 2026 20:32
@wpfleger96
wpfleger96 marked this pull request as ready for review July 28, 2026 21:17
@wpfleger96
wpfleger96 merged commit 114d40d into main Jul 30, 2026
53 of 55 checks passed
@wpfleger96
wpfleger96 deleted the duncan/relay-team-catalog-shared-gate branch July 30, 2026 21:53
tellaho pushed a commit that referenced this pull request Jul 30, 2026
* origin/main:
  fix(desktop): allow linux-only media items as dead code off-linux (#3811)
  fix(desktop): report authenticated relay recovery (#3812)
  fix(desktop): don't gate hover affordances on the hover media query (#3657)
  feat(relay): gate kind 30178 team-catalog reads behind the shared tag (#3358)
  test(desktop): click visible thread collapse guide (#3800)
  feat(desktop): raise the install ceiling and make installs observable (#3368)
  fix(db): isolate usage metrics advisory-lock test on scratch DB (#3670)
  Add Devin as a preset ACP harness (#3225)
  feat(desktop): improve agent activity header ui (#3321)
  perf(presence): reduce heartbeat frequency (#3783)
  Tighten continuation message rows (#3724)
  Fix video reviews in thread replies (#3719)
  feat(release): make desktop releases immutable (#3568)
  Make relay reconnect backoff authoritative (#3774)
  feat(desktop): add password-protected backups in settings (#3701)
  fix(desktop): reuse profiles when joining communities (#2155)

Signed-off-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
brow pushed a commit that referenced this pull request Jul 31, 2026
* origin/main: (70 commits)
  fix(catalog): update Amp tagline (#3806)
  fix(desktop): channel topic and membership metadata cleanup (#3642)
  fix(desktop): align data deletion labels (#2230)
  fix(relay): align NIP-11 max_limit with REQ ceiling (#3635)
  fix(desktop): allow linux-only media items as dead code off-linux (#3811)
  fix(desktop): report authenticated relay recovery (#3812)
  fix(desktop): don't gate hover affordances on the hover media query (#3657)
  feat(relay): gate kind 30178 team-catalog reads behind the shared tag (#3358)
  test(desktop): click visible thread collapse guide (#3800)
  feat(desktop): raise the install ceiling and make installs observable (#3368)
  fix(db): isolate usage metrics advisory-lock test on scratch DB (#3670)
  Add Devin as a preset ACP harness (#3225)
  feat(desktop): improve agent activity header ui (#3321)
  perf(presence): reduce heartbeat frequency (#3783)
  Tighten continuation message rows (#3724)
  Fix video reviews in thread replies (#3719)
  feat(release): make desktop releases immutable (#3568)
  Make relay reconnect backoff authoritative (#3774)
  feat(desktop): add password-protected backups in settings (#3701)
  fix(desktop): reuse profiles when joining communities (#2155)
  ...

Signed-off-by: npub15w828kxsxu2684ynste0uah2jwkgatd99flt7ds4523hzm8ju6cshdr8hh <a38ea3d8d03715a3d49382f2fe76ea93ac8eada52a7ebf3615a2a3716cf2e6b1@buzz.block.builderlab.xyz>
adrienlacombe added a commit to adrienlacombe/buzz that referenced this pull request Jul 31, 2026
Three findings from the first live sync, which merged nothing and reported
success on both stages while the fork sat 17 commits behind.

**The handoff had no receiver.** `Sync this fork with upstream` was
`disabled_manually`. The 01:30 stage hit conflicts, aborted, and handed off
exactly as designed — to a disabled workflow. No PR, no issue, no red badge,
because a conflicted handoff *is* success for the first stage. Re-enabled, and
recorded in the repo-settings table with the one command that reveals it, since
`gh workflow list --all` is the only place that state shows.

**The two stages ran out of order.** GitHub delays scheduled runs under load: the
01:30 job started at 02:17, after the 02:00 slot the agentic stage held, so the
deterministic stage was no longer first. Moved the agentic stage to 05:00 — a
30-minute gap is not a guarantee, 3.5 hours is a margin.

**Kind 30178 collided.** Upstream shipped `KIND_TEAM_CATALOG = 30178` (block#3358)
against this fork's `KIND_STARKNET_WALLET_BINDING = 30178`: two unrelated schemas
on one integer, so `ingest_event_inner` would run both the on-chain attestation
verifier and `validate_team_catalog_envelope` on every such event and one would
always reject the other's traffic. No text resolution fixes that.

Settled rather than left to per-merge judgement: kinds 30900-30999 are reserved
for this fork, upstream keeps the integer whenever it claims one first, and the
fork's constant moves. Upstream's parameterized-replaceable kinds cluster at
30174-30178 and grow upward, so anything the fork puts near them gets claimed
eventually — reserving a block away from that path is what stops this recurring.
The rule is in AGENTS.md with the full move checklist, and the sync prompt now
points at it and says not to escalate it as ambiguous.

No merge in this commit. The 17-commit merge and the 30178 move are the sync
workflow's job, dispatched next.

Signed-off-by: adrienlacombe <6303520+adrienlacombe@users.noreply.github.com>
wpfleger96 pushed a commit that referenced this pull request Jul 31, 2026
…chive

* origin/main: (25 commits)
  feat(desktop): import local Pocket voices (#3259)
  fix(desktop): open profiles from avatars (#3751)
  refactor(voice): extract reusable Pocket primitives + Pocket voice settings (relands #2467 + #3208) (#3910)
  docs: add VISION_REMOTE_AGENTS.md (#3924)
  feat(desktop): auto-enable huddle transcription for agents (#3180)
  feat(agent): optional reply guard reminds a silent turn to publish (#3763)
  feat(desktop): upgrade Pocket TTS model (#3266)
  feat(desktop): delete a message by clearing its edit to empty (#3813)
  feat(relay): raise hosted community limit to five (#3829)
  feat(desktop): locally stored NIP-49 encrypted key backup (#2937)
  fix(catalog): update Amp tagline (#3806)
  fix(desktop): channel topic and membership metadata cleanup (#3642)
  fix(desktop): align data deletion labels (#2230)
  fix(relay): align NIP-11 max_limit with REQ ceiling (#3635)
  fix(desktop): allow linux-only media items as dead code off-linux (#3811)
  fix(desktop): report authenticated relay recovery (#3812)
  fix(desktop): don't gate hover affordances on the hover media query (#3657)
  feat(relay): gate kind 30178 team-catalog reads behind the shared tag (#3358)
  test(desktop): click visible thread collapse guide (#3800)
  feat(desktop): raise the install ceiling and make installs observable (#3368)
  ...

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

# Conflicts:
#	desktop/src/testing/e2eBridge.ts
#	desktop/tests/helpers/bridge.ts
wpfleger96 pushed a commit that referenced this pull request Jul 31, 2026
…chive

* origin/main: (25 commits)
  feat(desktop): import local Pocket voices (#3259)
  fix(desktop): open profiles from avatars (#3751)
  refactor(voice): extract reusable Pocket primitives + Pocket voice settings (relands #2467 + #3208) (#3910)
  docs: add VISION_REMOTE_AGENTS.md (#3924)
  feat(desktop): auto-enable huddle transcription for agents (#3180)
  feat(agent): optional reply guard reminds a silent turn to publish (#3763)
  feat(desktop): upgrade Pocket TTS model (#3266)
  feat(desktop): delete a message by clearing its edit to empty (#3813)
  feat(relay): raise hosted community limit to five (#3829)
  feat(desktop): locally stored NIP-49 encrypted key backup (#2937)
  fix(catalog): update Amp tagline (#3806)
  fix(desktop): channel topic and membership metadata cleanup (#3642)
  fix(desktop): align data deletion labels (#2230)
  fix(relay): align NIP-11 max_limit with REQ ceiling (#3635)
  fix(desktop): allow linux-only media items as dead code off-linux (#3811)
  fix(desktop): report authenticated relay recovery (#3812)
  fix(desktop): don't gate hover affordances on the hover media query (#3657)
  feat(relay): gate kind 30178 team-catalog reads behind the shared tag (#3358)
  test(desktop): click visible thread collapse guide (#3800)
  feat(desktop): raise the install ceiling and make installs observable (#3368)
  ...

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

# Conflicts:
#	desktop/src/testing/e2eBridge.ts
#	desktop/tests/helpers/bridge.ts
joahg added a commit to joahg/buzz-dev-mode that referenced this pull request Jul 31, 2026
…-style

* origin/main: (22 commits)
  feat(desktop): import local Pocket voices (block#3259)
  fix(desktop): open profiles from avatars (block#3751)
  refactor(voice): extract reusable Pocket primitives + Pocket voice settings (relands block#2467 + block#3208) (block#3910)
  docs: add VISION_REMOTE_AGENTS.md (block#3924)
  feat(desktop): auto-enable huddle transcription for agents (block#3180)
  feat(agent): optional reply guard reminds a silent turn to publish (block#3763)
  feat(desktop): upgrade Pocket TTS model (block#3266)
  feat(desktop): delete a message by clearing its edit to empty (block#3813)
  feat(relay): raise hosted community limit to five (block#3829)
  feat(desktop): locally stored NIP-49 encrypted key backup (block#2937)
  fix(catalog): update Amp tagline (block#3806)
  fix(desktop): channel topic and membership metadata cleanup (block#3642)
  fix(desktop): align data deletion labels (block#2230)
  fix(relay): align NIP-11 max_limit with REQ ceiling (block#3635)
  fix(desktop): allow linux-only media items as dead code off-linux (block#3811)
  fix(desktop): report authenticated relay recovery (block#3812)
  fix(desktop): don't gate hover affordances on the hover media query (block#3657)
  feat(relay): gate kind 30178 team-catalog reads behind the shared tag (block#3358)
  test(desktop): click visible thread collapse guide (block#3800)
  feat(desktop): raise the install ceiling and make installs observable (block#3368)
  ...

Amp-Thread-ID: https://ampcode.com/threads/T-019fb8e1-6ece-72a7-8808-9b12e0f7e833
Co-authored-by: Amp <amp@ampcode.com>
Signed-off-by: Joah Gerstenberg <joah@squareup.com>

# Conflicts:
#	desktop/src-tauri/src/linux_media.rs
#	desktop/src/app/AppShell.tsx
kbst9 added a commit to kbst9/buzz that referenced this pull request Jul 31, 2026
Upstream assigned kind 30178 to its new KIND_TEAM_CATALOG (block#3358),
colliding with the fork's swarm definitions. Move swarms to 30978 --
outside upstream's sequential 3017x allocation block so the race cannot
recur. Stored prod events need a matching kind UPDATE at deploy time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: kbst9 <kevinbsteiner@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