mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
main
10 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9092950620 |
docs(nip-ap): narrow relay support and allow offline start (#8368)
🤖 Two corrections to NIP-AP's agent and team config section. **Relay support.** A client now needs only NIP-CL system channels and labels plus NIP-AR artifacts before storing config on the relay. The four other required advertisements are dropped: read-only guests, a retention exemption, complete-history artifact sync, and telling a never-used artifact address apart from a fully redacted one. No relay advertises them, so a conforming client could never store config on the relay. The retention exemption stays in "History" as the relay's obligation. Without a never-used signal an empty lookup is still `unavailable`, never `absent`, so clients fail closed. **Offline start.** When the relay can't be reached (a transport failure, not a read that returned an answer), a client may start the agent from the last revision it confirmed as present. It still must block if the relay's most recent answer showed that the config is invalid, deleted, redacted, archived, or no longer accessible, and it never uses another source. **Consistency.** "Editing" no longer assumes every relay's guest role is read-only. "Artifact state" points its no-substitution rule at the new offline exception. Related: #8295 --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz> |
||
|
|
349f566e93 |
docs(nip-ap): store agent and team config as NIP-AR artifacts (#8295)
🤖 Stores agent and team config as [NIP-AR](https://github.com/block/buzz/blob/main/docs/nips/NIP-AR.md) artifacts in private [NIP-CL](https://github.com/block/buzz/blob/main/docs/nips/NIP-CL.md) system channels, replacing the owner-encrypted `kind:44300` instruction versions. - **Config channels.** Each agent has an agent config channel holding `buzz.agent.settings` and `buzz.agent.instructions`. Each team has a team config channel holding one `buzz.team` artifact with the team name, team instructions and a `team` tag carrying the exact team id, and no member list. The owner creates both kinds as private system channels with no TTL. Each carries a family label (`agent-config` / `team-config`) and a subject label: the agent pubkey, or `team-` plus a hash of the team id, which clients check against the `team` tag. Discovery queries by creator and subject label. Agents find their teams through a `39002` query that opts in to system channels. The oldest matching channel wins, and channels that aren't selected are never deleted. - **Ownership and teams.** An agent's owner is its relay-verified NIP-OA owner, and a team's owner is its channel's creator. Changing an owner isn't supported. A team's members are the owner's agents in the team config channel. Changes to who is on a team follow the channel's role rules, including add policies, owner/admin-only role changes, and last-owner protection against leaving, removal and demotion. Guests are read-only. Every non-guest member can edit config, including humans and agents that aren't on the team. Edits keep unrecognised JSON fields and tags, and a client whose create collides fetches and edits the existing artifact instead of picking a new `d`. - **Artifact states.** A config artifact is present, invalid, absent, deleted, or unavailable. An artifact is absent only when the relay confirms its address was never used. Redacting every revision does not make an address unused. Only an absent artifact lets a client fall back to local instructions or a definition prompt. Invalid, deleted, and unavailable states block the start. - **History and launch.** History is kept until the owner purges it or a moderator removes a revision. Config channels must be exempt from age-based retention, and keeping only the latest revision is not allowed. A revert is an `op=update` with a full snapshot and keeps the team's name. The instruction resolution order and per-team launch sections are unchanged. - **Limits and lifecycle.** Config content is at most 131,072 bytes after encoding (settings: 65,535), and the signed event must fit the relay's limits. Content is never truncated. Settings exclude credentials, local paths and device state. An unsupported setting is an error, never a silent default. Deleting an agent archives its channel and blocks new launches until it's unarchived. Deleting a team archives its channel. - **Relay prerequisites.** Clients keep config local unless the relay supports system channels, artifacts, read-only guests, the retention exemption, sync with complete history, and telling an artifact address that has never been used apart from one whose revisions have all been redacted. - **Removed.** `kind:44300` and its relay rules, deletion rules and owner-encryption text. `30175`, `30177`, `30178` and the team id format are unchanged. Depends on the NIP-CL label and system-channel stack (#8176, #8180–#8182). --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz> |
||
|
|
acf1ede202 |
docs(nip-ap): add teams and kind:44300 instructions versions (#8191)
🤖 This PR adds two things to NIP-AP: a Teams section, and `kind:44300` instructions versions, an owner-private history of each agent's and each team's instructions. Today instructions are edited in place, so there's no way to see past edits, tie a behavior change to an edit, or roll back. **Teams.** A team is identified by owner, community, and team id, using the same id grammar as the `kind:30178` `d` tag. The section defines what membership means (scoped by owner and community, not channel membership, follows the team's current instructions), how team instructions combine with an agent's own at launch, and the existing rule that an agent in several teams can't start if their instructions differ. How a client stores its private team records stays client-defined, so this doesn't standardize discovering another client's private teams. **`kind:44300`.** A regular event, so every save is a new event and a subject's events are its history. Each event names exactly one subject: an agent (`p`) or a team (`t`). Content is NIP-44 encrypted to the owner as `v1:` plus the raw text, which keeps empty text representable and caps `content` at 218,548 bytes for the 128 KiB text limit. Current is the newest version, and history pages with the `until` + `before_id` cursor so versions saved in the same second are never skipped. Restore and Reset to template append a new version. Launch order for an agent's own instructions is its current version, then the linked `30175` prompt, then a definition-less `30177`. The relay keeps it author-only on every read surface, excludes it from search, and applies the subject filter before `LIMIT`. Deletion requests for these versions must carry `["k", "44300"]` and get the same author-only treatment, since they reveal version ids and deletion times. A `30178` share is a snapshot: saving a version doesn't change an existing share. **Changes to existing spec text:** - Removed the sentence saying `kind:30176`'s body is untouched "so device sync keeps its contract". The current desktop client doesn't sync teams through `30176`. - Reworded the `30178` `d` tag rationale so it no longer refers to a `30176` head. - The "30175 beats 30177" rule now says a current `44300` version beats both for an agent's instructions. - Replaced the stale search bullet. It claimed no shared-gated kind is in an FTS allowlist; search indexes those kinds, and each result passes the per-event visibility check before delivery. Relay and desktop implementation follow in separate PRs once this is approved. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Paul <4a1bfa0013bc6d14a8d600d8bf6392efefbd2a26ac3c96c9b2a106b0d12297ca@buzz.block.builderlab.xyz> Co-authored-by: Paul <4a1bfa0013bc6d14a8d600d8bf6392efefbd2a26ac3c96c9b2a106b0d12297ca@buzz.block.builderlab.xyz> |
||
|
|
dd1c2afc2a |
Discover alternate Buzz ACP commands (#6948)
🤖 I’m Larry. ## Summary Managed-agent definitions can now choose which installed ACP transport will launch the agent. The choice sits beside **Agent harness** while creating or editing an agent, so it is made before first deployment and remains visible afterward. Buzz discovers executable commands named `buzz-*-acp`, keeps **Buzz ACP (default)** as the safe default, and preserves existing custom command values. Discovery and launch share the same resolver, so a command offered in the picker is the command Buzz will execute. ## UX walkthrough ### 1. The ACP command picker appears while creating the agent The default is explicit, and the helper text explains that this selection controls deployment.  ### 2. Installed wrapper commands are offered by name Here Buzz has discovered `buzz-janet-acp` alongside the stock transport.  ### 3. The wrapper is selected before deployment  ### 4. The deployed profile reports the effective command  ### 5. Editing the deployed agent reopens its definition with the choice preserved  ## Details - Definitions carry the selected ACP command through relay publication, snapshots, imports, team adoption, deployment, and later edits. - Existing definitions without this field continue to use `buzz-acp`. - Unknown persisted commands remain visible but unavailable rather than being silently replaced. - Discovery is read-only and never executes candidates. It filters non-files and non-executables, supports Windows `.exe`, `.cmd`, and `.bat` shims, deduplicates aliases, and returns a stable sorted list. - Command names remain portable; Buzz does not persist machine-specific absolute paths. --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Signed-off-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz> Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-authored-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@nostr> Co-authored-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
78618804ec |
Configure ACP session scope per agent (#7578)
## Summary PR #6732 taught the ACP harness to honor `BUZZ_ACP_SESSION_POLICY`, but the desktop exposed that policy as one global experiment. Turning it on changed every managed agent at once, even though conversation scope is part of how an individual agent should behave. Some agents need continuity across a channel, while agents such as validators may need clean context for each thread. This moves the choice onto the agent definition under Advanced as **Conversation context**, with **Entire channel** and **Each thread** options. **Entire channel** remains the default, so existing definitions keep their current behavior. The policy now travels through create and update IPC, returned agent summaries, persona events, snapshots, teams, and community catalog copies. Launches pass the effective definition value to the harness. Editing a running agent uses the same lifecycle as a model edit: it shows the restart-required state, leaves the deployed process untouched, and applies the new policy on restart. The global Thread Scoped ACP Sessions experiment and its desktop command/state have been removed because the setting no longer has a useful global meaning. There is intentionally no migration for the old preview override. Users who enabled it can choose **Each thread** for the agents that need it. Unknown or null policy values degrade to **Entire channel** across stored agents, persona events, and catalog projections. This keeps forward-version data from dropping complete agent records. ### Related issue No matching issue found. Builds on #6732. ### Testing - Focused Playwright create flow selected **Each thread** and verified `create_persona` received `sessionPolicy: "thread"`. - Focused Playwright catalog flow added a thread-scoped agent and verified the local copy stored `session_policy: "thread"`. Generated with Codex --------- Signed-off-by: Salman Mohammed <smohammed@squareup.com> |
||
|
|
114d40d9d3 |
feat(relay): gate kind 30178 team-catalog reads behind the shared tag (#3358)
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.
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
|
||
|
|
ab3af82871 |
feat(relay): add author-only-unless-shared read gate for kind 30175 (#2768)
Kind 30175 persona sync events carry plaintext `system_prompt` and
`respond_to_allowlist`. This PR adds **author-only-unless-shared read
semantics**: events without `["shared","true"]` are visible only to the
author; events with that tag are community-readable.
## What changed
### New read class (kind 30175)
Kind 30175 gets per-event gating at every relay read surface. The
`shared` marker is a **tag**, not a content field, so content bytes
(which double as the `source_version` drift basis) are not affected when
toggling share state.
### `event_visible_to_reader` helper (`handlers/req.rs`)
Centralizes the three per-event access predicates —
`is_author_only_event`, `is_unshared_persona_event`,
`reader_authorized_for_event` — into one `pub(crate)` fn callable from
both WS and HTTP adapters. All result-visibility sites now call this
single helper.
### NIP-98 HTTP bridge (`api/bridge.rs`)
- `POST /query` catchall: replaced the two-step author-only +
result-gated checks with `event_visible_to_reader` (now also covers the
persona shared-gate).
- `POST /count`: added `needs_persona_filtering` to the fast-path guard
(forces per-event fallback when filter can match `kind:30175`) and
replaced both fallback loops' individual checks with
`event_visible_to_reader`.
- FTS `/search` bridge helper: replaced `is_author_only_event` with
`event_visible_to_reader` as defense-in-depth (30175 is not in the FTS
allowlist today; comment at site explains the future-proofing intent).
### Ingest validation (`handlers/ingest.rs`)
`validate_persona_envelope` rejects malformed `shared` tags: wrong
value, missing value, duplicates. Accepts exactly `["shared","true"]`
and tag-absent.
### Kind helpers (`buzz-core/src/kind.rs`)
`is_persona_shared_kind`, `is_unshared_persona_event`,
`filter_can_match_persona_shared_kinds`.
### Tests (`e2e_persona.rs`)
8 unit tests in `kind.rs`, 6 in `ingest.rs`, 8 e2e tests total:
- AC-1–6 covering the gated surfaces
- `test_persona_live_fanout_shared_gate`: reworked with explicit
monotonic `created_at` timestamps (t0 < t1 < t2) and per-step head
assertions, eliminating the NIP-33 event-id tie-break race. Also asserts
foreign live subscription receives nothing on shared→unshared
transition.
- `test_persona_ingest_shared_tag_validation`: added `shared=x` and
missing-value wire-level rejection cases.
- `test_persona_mixed_kind_filter_does_not_leak`: publishes a kind-9
event and asserts it IS returned; absence-only assertion no longer
sufficient.
- `test_persona_http_query_cross_author_gate`: NIP-98 `/query`
cross-author gate (authors filter, kindless `ids` — both blocked; shared
`ids` — passes).
- `test_persona_http_count_cross_author_gate`: NIP-98 `/count`
cross-author gate (foreign sees 1/shared, author sees all, wildcard
checked).
### NIP-AP.md
Replaced aspirational "every relay read chokepoint" wording with an
enumerated list of gated surfaces including NIP-98 `/query`, `/count`,
and FTS/search with their enforcement mechanism named. Added
**Non-goal** note for side-band existence oracles
(reaction/report/deletion target resolution).
## Existing tests
All pre-existing `e2e_persona` tests use `{ids:[event_id]}` or
`{authors:[self]}` filters — author self-reads bypass the gate and are
unaffected.
## Gates
`just check` ✅ | `just test-unit` ✅ | `cargo test -p buzz-relay` ✅ (749
passed, 1 pre-existing failure in
`demo_join_forwarded_arm_round_trips_echo` — flaky on `main`, unrelated
to this PR, verified red at `origin/main` before this branch)
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
|
||
|
|
dfec75b3c0 |
refactor(desktop): remove vestigial MCP toolsets config (#1776)
Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co> |
||
|
|
b2c63291f1 |
feat(relay,desktop): canonicalize agent definitions on kind:30175 (Phase 2) (#1655)
Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co> |
||
|
|
d256935c36 |
feat: publish personas, teams, and managed agents as relay events (#939)
Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Will Pfleger <pfleger.will@gmail.com> |