Skip to content

feat(relay): channel-gated agent work status kind (NIP-AW, 30181) - #6582

Open
LucasMoskun wants to merge 1 commit into
block:mainfrom
LucasMoskun:feat/agent-work-status-kind
Open

feat(relay): channel-gated agent work status kind (NIP-AW, 30181)#6582
LucasMoskun wants to merge 1 commit into
block:mainfrom
LucasMoskun:feat/agent-work-status-kind

Conversation

@LucasMoskun

Copy link
Copy Markdown

Problem

Buzz Desktop shows a live "Working…" badge from ephemeral typing indicators, but there is no persistent, queryable answer to "what is this agent doing right now?" Operators today either SSH into agent hosts or run side-channel collectors to get a live fleet view. The buzz-acp harness already sees everything it drives — turn lifecycle, tool calls, streaming — it just never tells anyone.

The privacy bar this PR holds: an agent's work status should be exactly as private as the channel it describes. Anyone who can read the channel's messages may see its status; nobody else can, enforced server-side. Kind 30315 (NIP-38) cannot meet that bar — it is a global-only kind on Buzz relays, readable by any authenticated relay member. This PR deliberately stays relay-gated plaintext (like channel messages themselves); encrypted lanes remain NIP-AO's job and out of scope here.

What this adds

A new parameterized-replaceable kind 30181, KIND_AGENT_WORK_STATUS (spec: docs/nips/NIP-AW.md), with h = d = channel UUID:

  • buzz-core: kind constant + registry entry.
  • buzz-relay (registration only — no new access-control machinery): MessagesWrite scope; requires_h_channel_scope so writes and reads inherit the existing NIP-29 channel-membership gate; an envelope check rejecting dh (a divergent d would allow multiple snapshots per channel or cross-channel coordinate collisions); metrics label. NIP-33 replacement keeps at most one live snapshot per (agent, channel) — a status lane, not a history log.
  • buzz-acp: opt-in --public-status / BUZZ_ACP_PUBLIC_STATUS flag. A second consumer of the observer bus projects frames into signed status events: turn status, model id, timestamps, and tool-call titles only. Tool arguments, file paths, prompts, streamed text, and error messages never enter the payload — the projection is whitelist-shaped by construction and unit tests assert secrets stay out. Publication is paced (urgent on start/complete/error, 5 s floor otherwise, 3 s budget) and publish failure is never fatal to the turn.

Any channel-authorized client can then render live agent lanes with one REQ — no collectors, no new HTTP surface, matching the "prefer Nostr events" guidance in CONTRIBUTING.

Testing

  • Unit: cargo test -p buzz-core -p buzz-acp green (262 / 811); -p buzz-relay green except 8 pre-existing api::media/api::admin failures that fail identically on untouched main in this environment.
  • New relay unit tests: scope/requires_h/global-only registration, d/h mismatch + non-UUID + missing-d rejection.
  • New acp unit test pins the envelope (kind 30181, h and d = channel).
  • Live e2e (test_agent_work_status_channel_gating, run against a local relay + Postgres + Redis): member write accepted → dh rejected → missing h rejected → non-member write rejected (restricted: not a channel member) → member REQ returns exactly the one snapshot → non-member REQ is answered with CLOSED "restricted: not a channel member".
  • cargo fmt --check and cargo clippy --all-targets clean on the four touched crates.

Related PRs

Closest existing work found: #446 (original NIP-38 kind:30315 support — this PR exists because 30315 is global-only), #5098 / #4769 (desktop/diagnostic observability — complementary, different lane). None add a channel-gated status kind.

🤖 Generated with Claude Code

Buzz Desktop shows a live "Working…" badge from ephemeral typing events,
but there is no queryable answer to "what is this agent doing right now?"
The buzz-acp harness already sees every turn it drives; this adds the
opt-in lane that publishes a redacted projection of that activity to
exactly the people who may read the channel it happens in.

New parameterized-replaceable kind 30181 (docs/nips/NIP-AW.md):
h = d = channel UUID, so each agent stores at most one live snapshot per
channel and both writes and reads inherit the existing NIP-29
channel-membership gate — the relay needs no new access-control machinery,
only registration:

- buzz-core: KIND_AGENT_WORK_STATUS constant + registry entry.
- buzz-relay: MessagesWrite scope, requires_h_channel_scope membership
  gating, and an envelope check rejecting d != h (a divergent d would
  allow several snapshots per channel or cross-channel coordinate
  collisions). Metric label registered for the new kind.
- buzz-acp: --public-status / BUZZ_ACP_PUBLIC_STATUS projects the
  observer bus into signed status events — turn status, model id, and
  tool-call titles only; tool arguments, prompts, streamed text, and
  error messages never enter the payload by construction. Publication
  is paced (urgent on start/complete/error, 5s floor otherwise) and
  never fatal to the turn.

Deliberately NOT kind 30315 (NIP-38): user status is a global-only kind
on Buzz relays — any authenticated relay member can read it — which
cannot honor the bar that work status be exactly as private as the
channel it describes.

Tested: unit tests in buzz-core/buzz-relay/buzz-acp, plus a live e2e
(test_agent_work_status_channel_gating) proving member write + read,
d != h rejection, missing-h rejection, non-member write rejection, and
CLOSED "restricted: not a channel member" for non-member reads.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: LucasMoskun <contact@lucasmoskun.com>
@LucasMoskun
LucasMoskun requested a review from a team as a code owner August 22, 2026 21:33
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.

1 participant