Skip to content

Commit ffd84f6

Browse files
npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7ftellaho
andcommitted
Merge origin/main into tho/agent-profile-sidebar
Resolves conflicts in channels/ui (ChannelPane, ChannelScreen, useChannelAgentSessions, useChannelPanelHistoryState). Re-applies HEAD profile-panel deltas (UserProfilePanel tab/onTabChange, AgentSessionThreadPanel activity-list ternary) on top of main's reformatted auxiliary-pane render chain. Extracts the repeated RightAuxiliaryPane wrapper into a local wrapAux helper to keep ChannelPane under the 1000-line size guard. Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2 parents 4fd4472 + dd5592e commit ffd84f6

290 files changed

Lines changed: 5927 additions & 5422 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

ARCHITECTURE.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -718,11 +718,14 @@ Subcommands:
718718
719719
| Subcommand | Purpose |
720720
|------------|---------|
721-
| `add-member` | Add a pubkey to the relay membership list (`--pubkey`, `--role`) |
721+
| `add-member` | Add a pubkey to the relay membership list (`--pubkey`, `--role`); accepts npub or hex; publishes kind:13534 roster |
722+
| `remove-member` | Remove a pubkey from the relay membership list (`--pubkey`, optional `--role` guard); publishes kind:13534 roster |
722723
| `list-members` | List all relay members |
723724
| `generate-key` | Generate a new Nostr keypair (for bootstrapping) |
724725
| `reconcile-channels` | Emit kind:39000/39002 discovery events for channels missing them (idempotent) |
725726
727+
The `buzz-admin` binary is shipped in the relay Docker image (`/usr/local/bin/buzz-admin`) and is the recommended way to manage relay membership in production. Use `./run.sh add-member`, `./run.sh remove-member`, and `./run.sh list-members` in Docker Compose deployments.
728+
726729
---
727730
728731
### buzz-test-client — Integration Test Harness

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
# Changelog
22

3+
## v0.3.33
4+
5+
- fix(desktop): always use legacy keyring for blob entry on macOS ([#1271](https://github.com/block/buzz/pull/1271)) ([`e7c3638fe`](https://github.com/block/buzz/commit/e7c3638fe984e7785a6e51e547b5844e894ed013))
6+
- chore(release): release Buzz Relay version 0.1.1 ([#1269](https://github.com/block/buzz/pull/1269)) ([`68a0cc850`](https://github.com/block/buzz/commit/68a0cc8506be4ea1fb110b65eec0787e4cd84378))
7+
- perf(desktop): consolidate keychain secrets into a single blob entry ([#1267](https://github.com/block/buzz/pull/1267)) ([`fa942cb51`](https://github.com/block/buzz/commit/fa942cb51d0714cba54747bcc7197c14e770c338))
8+
- feat(desktop): re-snapshot persona config on every agent spawn ([#1268](https://github.com/block/buzz/pull/1268)) ([`048e8fdc0`](https://github.com/block/buzz/commit/048e8fdc004322e5feccc4fd76c8fc0dc87d91f0))
9+
- feat(relay): add buzz-admin member management CLI with NIP-43 roster publish ([#1265](https://github.com/block/buzz/pull/1265)) ([`0cee0435f`](https://github.com/block/buzz/commit/0cee0435f7f955a42d7824f1967e4f4fc6a02f74))
10+
- fix(desktop): fall back to old keychain when DPK unavailable (unsigned builds) ([#1266](https://github.com/block/buzz/pull/1266)) ([`958ac7aaa`](https://github.com/block/buzz/commit/958ac7aaaae90da76c864af744242f29ee4f08de))
11+
- Align channel management panel with profile ([#1066](https://github.com/block/buzz/pull/1066)) ([`9f35b0188`](https://github.com/block/buzz/commit/9f35b018803f56b2330bfb79de4ab19f18d80256))
12+
- fix(relay): multi-pod subscription coherence (one access-gated fan-out path + cross-pod cache invalidation + REQ/COUNT DB guard) ([#1261](https://github.com/block/buzz/pull/1261)) ([`628445429`](https://github.com/block/buzz/commit/6284454298cb5be72cefbeaf13abed04afe3cc9e))
13+
- fix(desktop): switch macOS keychain to Data Protection Keychain ([#1264](https://github.com/block/buzz/pull/1264)) ([`35522311a`](https://github.com/block/buzz/commit/35522311a963b54f32df864d338ab7d75102247f))
14+
- fix(desktop): use IPv4 loopback for media proxy URLs ([#1245](https://github.com/block/buzz/pull/1245)) ([`cee2c5f26`](https://github.com/block/buzz/commit/cee2c5f2634ae7d15262a2ec07cccacc8460cd0e))
15+
- perf(relay/git): stream read-path, manifest info/refs fast path, idx sidecar (A/B/C) ([#1240](https://github.com/block/buzz/pull/1240)) ([`856815994`](https://github.com/block/buzz/commit/856815994c3441268eb48d2264aeb3492b701fe5))
16+
17+
318
## v0.3.32
419

520
- fix(desktop): restore solid dot for top-level channel unreads ([#1253](https://github.com/block/buzz/pull/1253)) ([`9d3bfd38b`](https://github.com/block/buzz/commit/9d3bfd38b0876cd1bdf5bb90d6639f200ceb12e0))

Cargo.lock

Lines changed: 10 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Dockerfile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ COPY --from=planner /build/recipe.json recipe.json
4444
RUN cargo chef cook --release --recipe-path recipe.json
4545
COPY . .
4646
RUN cargo build --release --locked -p buzz-relay --bin buzz-relay \
47-
&& strip target/release/buzz-relay
47+
-p buzz-admin --bin buzz-admin \
48+
&& strip target/release/buzz-relay \
49+
&& strip target/release/buzz-admin
4850

4951
# ─── Stage 4: web bundle (pnpm + vite) ──────────────────────────────────────
5052
# Independent of the Rust layers so a CSS change doesn't bust Rust cache and
@@ -82,6 +84,7 @@ RUN apt-get update \
8284
--create-home --shell /usr/sbin/nologin buzz
8385

8486
COPY --from=builder /build/target/release/buzz-relay /usr/local/bin/buzz-relay
87+
COPY --from=builder /build/target/release/buzz-admin /usr/local/bin/buzz-admin
8588
COPY --from=web-builder /build/web/dist /srv/buzz/web
8689

8790
ENV BUZZ_WEB_DIR=/srv/buzz/web

NOSTR.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,117 @@ is dual-sourced: local snapshot metadata plus upstream edit events (kind:40003
471471

472472
---
473473

474+
## Relay Membership (NIP-43)
475+
476+
When `BUZZ_REQUIRE_RELAY_MEMBERSHIP=true`, every authenticated connection is checked against the
477+
`relay_members` table. Only pubkeys with a row in that table may use the relay. The relay owner
478+
is bootstrapped automatically from `RELAY_OWNER_PUBKEY` on startup.
479+
480+
### CLI: Managing Members
481+
482+
Use `buzz-admin` — the operator CLI shipped in the relay image — to manage relay membership.
483+
In a Docker Compose deployment, use `run.sh`:
484+
485+
```bash
486+
# Add a member (accepts bech32 npub or 64-char hex; default role: member)
487+
./run.sh add-member npub1abc...
488+
./run.sh add-member <64-char-hex-pubkey>
489+
./run.sh add-member npub1abc... --role admin
490+
491+
# Remove a member
492+
./run.sh remove-member npub1abc...
493+
./run.sh remove-member npub1abc... --role member # only removes if role matches
494+
495+
# List all members
496+
./run.sh list-members
497+
```
498+
499+
Or invoke `buzz-admin` directly inside the container:
500+
501+
```bash
502+
docker compose exec relay buzz-admin add-member --pubkey npub1abc...
503+
docker compose exec relay buzz-admin add-member --pubkey npub1abc... --role admin
504+
docker compose exec relay buzz-admin remove-member --pubkey npub1abc...
505+
docker compose exec relay buzz-admin list-members
506+
```
507+
508+
**Exit codes:**
509+
510+
| Code | Meaning |
511+
|------|---------|
512+
| 0 | Success |
513+
| 1 | Validation error (bad pubkey, bad role, usage error) |
514+
| 2 | Not found (remove: member does not exist) |
515+
| 3 | Cannot remove relay owner (use `RELAY_OWNER_PUBKEY` to change owner) |
516+
| 4 | Role mismatch (`--role` check failed) |
517+
| 5 | DB/Redis/internal error |
518+
519+
**Required environment variables for member management:**
520+
521+
| Variable | Notes |
522+
|----------|-------|
523+
| `DATABASE_URL` | Postgres connection string |
524+
| `REDIS_URL` | Redis connection string |
525+
| `BUZZ_RELAY_PRIVATE_KEY` | Hex private key — required to sign kind:13534 events |
526+
527+
### NIP-43 Admin Events (WebSocket)
528+
529+
Relay membership can also be managed over WebSocket using NIP-43 admin events. These require
530+
the sender to be authenticated (NIP-42) as the relay owner or an admin.
531+
532+
| Kind | Action | Required tags |
533+
|------|--------|---------------|
534+
| 9030 | Add member | `["p", "<hex-pubkey>"]`, optional `["role", "member\|admin"]` |
535+
| 9031 | Remove member | `["p", "<hex-pubkey>"]`, optional `["role", "member\|admin"]` |
536+
| 9032 | Change role | `["p", "<hex-pubkey>"]`, `["role", "member\|admin"]` |
537+
538+
Example using `nak`:
539+
540+
```bash
541+
# Add a member (owner or admin must sign)
542+
nak event -k 9030 \
543+
--tag "p=<target-hex-pubkey>" \
544+
--tag "role=member" \
545+
--auth --sec <owner-or-admin-privkey> \
546+
ws://localhost:3000
547+
548+
# Remove a member
549+
nak event -k 9031 \
550+
--tag "p=<target-hex-pubkey>" \
551+
--auth --sec <owner-or-admin-privkey> \
552+
ws://localhost:3000
553+
554+
# Change a member's role to admin
555+
nak event -k 9032 \
556+
--tag "p=<target-hex-pubkey>" \
557+
--tag "role=admin" \
558+
--auth --sec <owner-or-admin-privkey> \
559+
ws://localhost:3000
560+
```
561+
562+
After each add/remove/role-change, the relay publishes a kind:13534 membership list event
563+
(relay-signed, NIP-70 protected) that clients can subscribe to:
564+
565+
```bash
566+
# Subscribe to the live membership roster
567+
nak req -k 13534 --auth --sec <privkey> ws://localhost:3000
568+
```
569+
570+
### Known Limitations
571+
572+
1. **CLI intentionally does not emit kind 8000/8001 deltas**`publish_nip43_delta` is
573+
in-process-only (no Redis hop), so a sidecar call stores but never pushes. The 13534 list
574+
snapshot is the authoritative roster and rides Redis to live clients. Do not wire a delta call
575+
that passes in-process tests and silently no-ops in the deployed `compose exec` path.
576+
577+
2. **The `custom_created_at = max(now, newest_existing_13534 + 1s)` bump defeats same-second
578+
domination for serial invocations; it does NOT serialize concurrent CLI processes** — two
579+
near-simultaneous adds can read the same newest timestamp and collide on the bumped second.
580+
`run.sh` serialization is the guard against parallel adds (e.g. `xargs -P`). When adding
581+
multiple members in a loop, add `sleep 1` between invocations.
582+
583+
---
584+
474585
## Relay Environment Variables (NIP-29 relevant)
475586

476587
| Variable | Required | Default | Description |

crates/buzz-acp/src/acp.rs

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ use crate::observer::{ObserverContext, ObserverHandle};
1919
/// Lines exceeding this limit are rejected to prevent OOM from rogue agents.
2020
const MAX_LINE_SIZE: usize = 10_000_000; // 10 MB
2121

22-
// ─── Public types ────────────────────────────────────────────────────────────
23-
2422
/// An MCP server configuration passed to `session/new`.
2523
///
2624
/// Corresponds to the `McpServerStdio` variant in the ACP schema.
@@ -106,8 +104,6 @@ pub enum AcpError {
106104
AgentError(String),
107105
}
108106

109-
// ─── AcpClient ───────────────────────────────────────────────────────────────
110-
111107
/// ACP client that owns an agent subprocess and communicates over its stdio.
112108
///
113109
/// One `AcpClient` per agent process. Multiple sessions can be created on the
@@ -151,8 +147,6 @@ pub struct AcpClient {
151147
}
152148

153149
impl AcpClient {
154-
// ── Lifecycle ─────────────────────────────────────────────────────────
155-
156150
/// Kill the agent subprocess and wait for it to exit (no zombies).
157151
///
158152
/// `Drop` only calls `start_kill()` (sends SIGKILL but doesn't reap).
@@ -568,8 +562,6 @@ impl AcpClient {
568562
self.parse_stop_reason(&result)
569563
}
570564

571-
// ── Internal helpers ──────────────────────────────────────────────────
572-
573565
/// Serialize `value` as a single NDJSON line and flush to the agent's stdin.
574566
///
575567
/// Bounded by a 30-second write timeout. If the agent stops reading stdin
@@ -1090,8 +1082,6 @@ impl AcpClient {
10901082
}
10911083
}
10921084

1093-
// ─── Permission response constructors ────────────────────────────────────────
1094-
10951085
/// Build `session/prompt` params from one or more text content blocks.
10961086
fn build_prompt_params(session_id: &str, prompt_blocks: &[&str]) -> serde_json::Value {
10971087
let blocks: Vec<serde_json::Value> = prompt_blocks
@@ -1122,8 +1112,6 @@ fn permission_response_cancelled(id: &serde_json::Value) -> serde_json::Value {
11221112
})
11231113
}
11241114

1125-
// ─── Session response types ───────────────────────────────────────────────────
1126-
11271115
/// Full `session/new` response — session ID plus the raw JSON result.
11281116
///
11291117
/// Callers use the extractor helpers to pull model info from `raw`.
@@ -1215,8 +1203,6 @@ pub fn resolve_model_switch_method(
12151203
None
12161204
}
12171205

1218-
// ─── Drop: kill child process ─────────────────────────────────────────────────
1219-
12201206
impl Drop for AcpClient {
12211207
fn drop(&mut self) {
12221208
// Best-effort SIGKILL + reap. We cannot `await` in Drop (sync context).
@@ -1258,14 +1244,10 @@ fn kill_process_group(_pid: u32) -> bool {
12581244
false
12591245
}
12601246

1261-
// ─── Tests ───────────────────────────────────────────────────────────────────
1262-
12631247
#[cfg(test)]
12641248
mod tests {
12651249
use super::*;
12661250

1267-
// ── StopReason parsing ────────────────────────────────────────────────
1268-
12691251
#[test]
12701252
fn stop_reason_parses_all_known_values() {
12711253
assert_eq!(StopReason::from_str("end_turn"), Some(StopReason::EndTurn));
@@ -1310,8 +1292,6 @@ mod tests {
13101292
assert_eq!(StopReason::from_str("Refusal"), Some(StopReason::Refusal));
13111293
}
13121294

1313-
// ── Permission option finding ─────────────────────────────────────────
1314-
13151295
#[test]
13161296
fn find_allow_once_by_kind_not_by_option_id() {
13171297
// optionId values are intentionally non-obvious to prove we don't hardcode them.
@@ -1371,8 +1351,6 @@ mod tests {
13711351
assert_eq!(reject_once.unwrap()["optionId"].as_str(), Some("rej-x"));
13721352
}
13731353

1374-
// ── JSON-RPC message construction ─────────────────────────────────────
1375-
13761354
#[test]
13771355
fn request_has_id_field() {
13781356
let id: u64 = 42;
@@ -1563,8 +1541,6 @@ mod tests {
15631541
assert_eq!(msg["params"]["sessionId"].as_str(), Some("sess_xyz789"));
15641542
}
15651543

1566-
// ── String ID handling (Fix 1) ────────────────────────────────────────
1567-
15681544
#[test]
15691545
fn permission_request_with_string_id() {
15701546
// Verify that permission response uses the same ID type as the request.
@@ -1617,8 +1593,6 @@ mod tests {
16171593
assert!(cancelled_numeric["id"].is_number());
16181594
}
16191595

1620-
// ── Model extractor tests ─────────────────────────────────────────────
1621-
16221596
#[test]
16231597
fn extract_model_config_options_finds_model_category() {
16241598
let result = serde_json::json!({
@@ -1685,8 +1659,6 @@ mod tests {
16851659
assert!(super::extract_model_state(&result).is_none());
16861660
}
16871661

1688-
// ── resolve_model_switch_method tests ─────────────────────────────────
1689-
16901662
#[test]
16911663
fn resolve_prefers_stable_over_unstable() {
16921664
let result = serde_json::json!({
@@ -1783,8 +1755,6 @@ mod tests {
17831755
);
17841756
}
17851757

1786-
// ── Error variant display ─────────────────────────────────────────────
1787-
17881758
#[test]
17891759
fn idle_timeout_error_includes_duration() {
17901760
let err = AcpError::IdleTimeout(std::time::Duration::from_secs(320));
@@ -1805,8 +1775,6 @@ mod tests {
18051775
);
18061776
}
18071777

1808-
// ── Async integration tests with real subprocess ──────────────────────
1809-
18101778
async fn spawn_script(script: &str) -> AcpClient {
18111779
AcpClient::spawn("bash", &["-c".into(), script.into()], &[])
18121780
.await
@@ -1977,8 +1945,6 @@ mod tests {
19771945
assert_eq!(result.unwrap()["worked"], serde_json::json!(true));
19781946
}
19791947

1980-
// ── Keepalive / tool-call idle reset tests (PR #935 fix) ─────────────
1981-
19821948
#[tokio::test]
19831949
async fn keepalive_resets_idle_past_deadline() {
19841950
// Keepalive session/update lines every 50ms against a 100ms idle deadline.
@@ -2044,8 +2010,6 @@ mod tests {
20442010
);
20452011
}
20462012

2047-
// ── session_new_full systemPrompt serialization ──────────────────────
2048-
20492013
#[tokio::test]
20502014
async fn session_new_full_includes_system_prompt_when_some() {
20512015
// Script: respond to initialize, then echo back the session/new request.

crates/buzz-acp/src/base_prompt.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,13 @@ Run `buzz --help` or `buzz <group> --help` for full usage.
3434

3535
### Threading
3636

37-
- **To a human** (updates, questions, deliverables): Use `--reply-to <thread-root-id>` (from your `[Context]` block) and `@mention` the human. Keeps messages at layer 1 where humans read.
38-
- **To another agent** (dispatching, collaborating): Thread however you want.
39-
- **When in doubt**, reply to thread root.
40-
- **Thread scope:** Respond in the thread where you were tagged. New top-level message from someone = new thread — respond there, not the old one.
41-
- **New topic → new top-level message.** Don't graft unrelated work onto an existing thread.
37+
Use the reply destination supplied in the `[Context]` block for ordinary replies in this turn. Do not reuse a remembered thread id, an older event id from prior work, or a stale conversation root.
38+
39+
For human-facing work, keep the conversation flat and easy to read. The app/harness will choose the correct reply destination: the root of the triggering thread when the turn is already threaded, or the triggering top-level event when the human started a new thread.
40+
41+
For agent-to-agent coordination with no human in the loop, deeper nesting is allowed when it helps preserve task structure. Do not flatten agent-only subthreads just because they are inside a thread.
42+
43+
When in doubt, prefer the reply destination explicitly supplied in `[Context]`. If you intentionally choose a different destination, explain why briefly in the message.
4244

4345
### General
4446

0 commit comments

Comments
 (0)