From 9f192f175825be4eeda517ba7265b80700828011 Mon Sep 17 00:00:00 2001
From: Vibherpunk <216434340+Vibherpunk@users.noreply.github.com>
Date: Fri, 24 Jul 2026 10:33:59 -0700
Subject: [PATCH 1/7] =?UTF-8?q?feat(desktop):=20channel=20tools=20panel=20?=
=?UTF-8?q?=E2=80=94=20manage=20a=20channel's=20skills=20&=20MCP=20servers?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Adds a GUI, in the channel header, for viewing and adding the skills and MCP
servers available to agents in a channel. A channel maps to a Harbor room
(~/.buzz/channel-tools.toml); the room is the source of truth, enforced
server-side by Harbor's gate. The panel is a window onto that model — it shells
(in Rust) to the same 'harbor' binary buzz-acp uses, so panel, CLI, and
enforcement all agree on one policy.
- commands/channel_tools.rs: 5 Tauri commands wrapping harbor's audited verbs
(channel-tools --json read; skills-list --json for the picker;
skill-room-add / skill-install --room / mcp-add --room mutations). Runs in
spawn_blocking over std::process (desktop crate has no tokio process
feature), passes values as distinct argv, rejects flag-like inputs.
- tauriChannelTools.ts: typed invoke wrappers.
- ChannelToolsDialog.tsx: native-styled dialog — current skills/MCP list,
add-existing-from-pool picker, install-new-from-path, add-MCP form; honest
'not Harbor-scoped' state for unmapped channels.
- ChannelMembersBar: a 'Channel tools' button (inline + compact variants).
Adds a skill to a channel first (via its room), which is exactly what makes it
available in that channel. Verified: cargo check, tsc, biome, clippy, rustfmt
all clean. Formatters run manually (repo lefthook needs 'just', unavailable).
---
.../src-tauri/src/commands/channel_tools.rs | 207 +++++++
desktop/src-tauri/src/commands/mod.rs | 2 +
desktop/src-tauri/src/lib.rs | 5 +
.../channels/ui/ChannelMembersBar.tsx | 34 +-
.../channels/ui/ChannelToolsDialog.tsx | 513 ++++++++++++++++++
desktop/src/shared/api/tauriChannelTools.ts | 92 ++++
6 files changed, 852 insertions(+), 1 deletion(-)
create mode 100644 desktop/src-tauri/src/commands/channel_tools.rs
create mode 100644 desktop/src/features/channels/ui/ChannelToolsDialog.tsx
create mode 100644 desktop/src/shared/api/tauriChannelTools.ts
diff --git a/desktop/src-tauri/src/commands/channel_tools.rs b/desktop/src-tauri/src/commands/channel_tools.rs
new file mode 100644
index 00000000000..444afbaac9d
--- /dev/null
+++ b/desktop/src-tauri/src/commands/channel_tools.rs
@@ -0,0 +1,207 @@
+//! Channel-scoped tools GUI bridge.
+//!
+//! Harbor is the source of truth for which skills + MCP servers a channel
+//! exposes: buzz-acp maps a channel to a Harbor *room* via
+//! `~/.buzz/channel-tools.toml`, and the room's skill/MCP allowlist is enforced
+//! server-side by Harbor's gate. These commands are the GUI's window onto that
+//! model — they shell out to the same `harbor` binary buzz-acp uses, so the
+//! panel, the CLI, and enforcement all read and write ONE policy rather than
+//! three divergent notions of "what's in this channel".
+//!
+//! Read commands (`*_get`, `*_pool_skills`) return `harbor ... --json` verbatim
+//! for the frontend to render. Mutations (`*_add_*`) wrap Harbor's own audited
+//! verbs (`skill-install` / `skill-room-add` / `mcp-add --room`); Harbor applies
+//! its confirm policy and token budget, so this layer never bypasses the gate.
+//!
+//! The desktop Tauri crate does not enable tokio's `process` feature, so every
+//! subprocess runs inside `spawn_blocking` over synchronous `std::process`,
+//! mirroring `agent_model_process.rs`. Values are passed as distinct argv
+//! entries (never a shell string), and any value that could be reinterpreted as
+//! a flag is rejected up front.
+
+use std::path::{Path, PathBuf};
+use std::process::{Command, Output};
+
+use serde_json::Value;
+
+use crate::managed_agents::login_shell_path;
+
+/// The channel-tools policy file buzz-acp reads (env override mirrors
+/// buzz-acp's `BUZZ_ACP_CHANNEL_TOOLS`, else `~/.buzz/channel-tools.toml`).
+fn policy_path() -> PathBuf {
+ if let Ok(p) = std::env::var("BUZZ_ACP_CHANNEL_TOOLS") {
+ if !p.trim().is_empty() {
+ return PathBuf::from(p);
+ }
+ }
+ let mut base = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
+ base.push(".buzz");
+ base.push("channel-tools.toml");
+ base
+}
+
+/// Resolve the harbor binary the same way buzz-acp does: the policy's
+/// top-level `harbor_command`, else `harbor` on PATH.
+fn harbor_binary(policy: &Path) -> String {
+ if let Ok(text) = std::fs::read_to_string(policy) {
+ if let Ok(toml::Value::Table(table)) = text.parse::() {
+ if let Some(toml::Value::String(cmd)) = table.get("harbor_command") {
+ if !cmd.trim().is_empty() {
+ return cmd.clone();
+ }
+ }
+ }
+ }
+ "harbor".to_string()
+}
+
+/// Reject a value that is empty or could be reinterpreted as a CLI flag. Values
+/// reach harbor as separate argv entries (no shell), so the only injection
+/// vector is a leading `-` being parsed as an option — guard exactly that.
+fn safe_arg(label: &str, value: &str) -> Result {
+ let v = value.trim();
+ if v.is_empty() {
+ return Err(format!("{label} must not be empty"));
+ }
+ if v.starts_with('-') {
+ return Err(format!("{label} must not start with '-'"));
+ }
+ Ok(v.to_string())
+}
+
+/// Run `harbor ` with the login-shell PATH so the binary resolves, off
+/// the async runtime (the desktop crate lacks tokio's `process` feature).
+async fn run_harbor(args: Vec) -> Result
+> [!NOTE]
+> **This is a fork of [`block/buzz`](https://github.com/block/buzz).** The one thing
+> it adds is **channel-scoped tools**: map a channel to a [Harbor](https://github.com/TDH-Labs/Harbor)
+> room so every agent working there is confined to exactly that room's skills and MCP
+> tools — enforced server-side, managed from a **Channel tools** panel in the channel
+> header. Everything else is unmodified upstream Buzz.
+> **→ [What this fork adds](docs/CHANNEL_SCOPED_TOOLS.md)**
+
diff --git a/docs/CHANNEL_SCOPED_TOOLS.md b/docs/CHANNEL_SCOPED_TOOLS.md
new file mode 100644
index 00000000000..5597fb8aec1
--- /dev/null
+++ b/docs/CHANNEL_SCOPED_TOOLS.md
@@ -0,0 +1,96 @@
+# Channel-scoped tools (this fork's addition)
+
+> **What this fork adds over upstream [`block/buzz`](https://github.com/block/buzz):**
+> the ability to scope an agent's skills and MCP tools **per channel**, so the
+> *channel* — not the agent — is the unit of capability. Everything else in this
+> repo is unmodified upstream Buzz.
+
+## The idea
+
+In stock Buzz, whatever skills and extensions an agent is configured with travel
+with that agent into every channel — the same everywhere, ungoverned per channel.
+This fork makes a **channel** map to a **[Harbor](https://github.com/TDH-Labs/Harbor)
+room**; the room is the source of truth for the skills and MCP servers agents may
+use when they work in that channel. Scope a channel once, and *every* agent that
+responds there — now or later — is confined to exactly that toolset, enforced
+server-side and audited. Nothing leaks in from the harness; nothing leaks across
+channels.
+
+A `#legal` channel exposes the legal room's skills; a `#bookkeeping` channel
+exposes the bookkeeping room's; an agent moving between them picks up each
+channel's tools as it goes. Multiple agents in one channel all share that
+channel's scope.
+
+## How it works
+
+A small policy file, `~/.buzz/channel-tools.toml`, maps each channel (by name or
+UUID) to a Harbor room:
+
+```toml
+harbor_command = "harbor" # binary used for `room` entries
+
+[channels.legal]
+room = "legal" # → expands to: harbor mcp-server --room=legal
+
+[channels.bookkeeping]
+room = "bookkeeping"
+```
+
+When `buzz-acp` creates a session for a channel, it looks the channel up in that
+file and **replaces** the session's MCP servers with the channel's scope:
+
+```
+channel has an entry? → session gets exactly `harbor mcp-server --room=`
+no entry → session keeps the harness's own default extensions
+```
+
+That single MCP server exposes the room's skills as `list_skills` / `read_skill`,
+gated by room and token budget. The lookup keys on the **channel**, not the
+agent — so five agents answering in `#legal` all get the `legal` room's skills,
+and none can load a skill from another room.
+
+Because scoping happens where the agent *runs* (your machine's `buzz-acp` reading
+your local policy), it applies in **any** community the agent works in —
+self-hosted or Block-hosted. The relay only moves messages.
+
+## What changed vs. upstream
+
+**Enforcement — `crates/buzz-acp/`:**
+| File | Change |
+|------|--------|
+| `src/channel_tools.rs` | **new** — `ChannelTools::load/resolve` reads the policy file and expands a `room` entry to `harbor mcp-server --room=` (or an explicit inline server list) |
+| `src/config.rs` | `--channel-tools` flag / `BUZZ_ACP_CHANNEL_TOOLS` env → policy path |
+| `src/lib.rs` | loads the policy at startup |
+| `src/pool.rs` | the per-session MCP **replace** override |
+
+**Management GUI — `desktop/`:**
+| File | Change |
+|------|--------|
+| `src-tauri/src/commands/channel_tools.rs` | **new** — Tauri commands that shell to `harbor` to read a channel's tools, add/remove skills & MCP servers, and scope a channel on the fly |
+| `src/features/channels/ui/ChannelToolsDialog.tsx` | **new** — the "Channel tools" panel |
+| `src/features/channels/ui/ChannelMembersBar.tsx` | the **Channel tools** button in the channel header |
+| `src/shared/api/tauriChannelTools.ts` | **new** — the typed API bridge |
+
+## Using it
+
+1. **Install Harbor** (the control plane): see
+ [TDH-Labs/Harbor](https://github.com/TDH-Labs/Harbor). It provides the
+ `harbor` command this fork calls.
+2. **Point `buzz-acp` at a policy file** via `--channel-tools ~/.buzz/channel-tools.toml`
+ (or `BUZZ_ACP_CHANNEL_TOOLS`).
+3. **Manage per channel from the GUI:** open a channel → **Channel tools** in the
+ header → add or remove skills and MCP servers. The first add scopes the
+ channel automatically (no hand-edited config). Or use the CLI:
+ `harbor channel-tools --map`, `harbor channel-tools `.
+
+Full Harbor-side guide: **[Harbor → Buzz](https://github.com/TDH-Labs/Harbor/blob/main/docs/BUZZ.md)**.
+
+## Notes
+
+- **Scopes are keyed by channel name, globally on your machine.** A channel named
+ `legal` in two communities gets the same scope; key by UUID to differ them.
+- **The scope replaces, it doesn't merge** — a scoped channel exposes exactly the
+ room's tools, so the room must contain everything that channel's work needs.
+- Harbor is one provider of the scoped command; the policy format is
+ provider-agnostic (`harbor_command` plus an explicit `mcp` list are both
+ supported), which is the intended shape for upstreaming.
From 924515dd8ecce47f9cfc23c3e658084937bcc4dc Mon Sep 17 00:00:00 2001
From: Vibherpunk <216434340+Vibherpunk@users.noreply.github.com>
Date: Sat, 25 Jul 2026 08:36:41 -0700
Subject: [PATCH 6/7] =?UTF-8?q?docs:=20proposal=20=E2=80=94=20channel-scop?=
=?UTF-8?q?ed=20personas=20(extends=20channel-tools)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Design proposal (NOT built, NOT approved) to add optional persona/persona_file to
ChannelToolsEntry, so one identity can serve different system prompts per channel —
replacing (not additive), fatal-on-error, composes with tool-scoping. Genericized
from an internal hand-off spec; no personal/fleet specifics.
---
docs/CHANNEL_PERSONAS_PROPOSAL.md | 131 ++++++++++++++++++++++++++++++
1 file changed, 131 insertions(+)
create mode 100644 docs/CHANNEL_PERSONAS_PROPOSAL.md
diff --git a/docs/CHANNEL_PERSONAS_PROPOSAL.md b/docs/CHANNEL_PERSONAS_PROPOSAL.md
new file mode 100644
index 00000000000..ca031afd42e
--- /dev/null
+++ b/docs/CHANNEL_PERSONAS_PROPOSAL.md
@@ -0,0 +1,131 @@
+# Proposal: channel-scoped personas (extends `channel-tools.toml`)
+
+> **Status: proposal — not built, not approved for implementation.** This touches
+> shared `buzz-acp` core session-creation logic; do not implement without explicit
+> sign-off and the testing bar below. Companion to the channel-scoped *tools*
+> feature (`crates/buzz-acp/src/channel_tools.rs` + the desktop panel).
+
+## Problem this solves
+
+Today one Buzz agent = one Nostr identity = one fixed `system_prompt` (set in
+`managed-agents.json`), used everywhere that identity appears. Each identity also
+gets a separate, persistent runtime process **per relay/community it's in**
+(`ManagedAgentRuntimeKey{pubkey, relay_url}`), so process count scales with
+identities × communities.
+
+Consolidating *personas* (writing fewer, broader prompts) would cut that overhead
+— but at a real cost. A safety-conscious fleet depends on each domain having a
+**narrow, non-diluted, individually-auditable prompt** with its own hard-coded
+approval boundaries. Merging prompts risks blurring those boundaries — a real
+regression for sensitive domains (e.g. finance), not just an efficiency tradeoff.
+
+This proposal decouples the two: keep narrow, separate, fully-independent prompts
+— but let **one identity/process serve several of them**, swapping which one
+applies based on which channel a session is created in.
+
+## Design: extend `ChannelToolsEntry`, don't build a parallel mechanism
+
+`channel_tools.rs` already resolves a per-channel policy (by UUID, then by name)
+at session-creation time in `pool.rs`, strict/replacing, fatal on load error.
+Extend the same struct and the same resolution path rather than adding a second,
+independently-loaded config file that must stay in sync with the first.
+
+```rust
+// channel_tools.rs — add one optional field to the existing entry
+struct ChannelToolsEntry {
+ room: Option,
+ harbor_command: Option,
+ mcp: Vec,
+ persona: Option, // NEW: inline system_prompt override
+ persona_file: Option, // NEW: load from file instead (long prompts)
+ // mutually exclusive with `persona`; same validation pattern as
+ // `cron`/`interval` in buzz-workflow's Schedule trigger
+}
+```
+
+```toml
+# ~/.buzz/channel-tools.toml — same file, new optional key per entry
+[channels.bookkeeping]
+room = "bookkeeping"
+persona_file = "~/.buzz/personas/accountant.md"
+
+[channels.marketing]
+room = "marketing"
+persona_file = "~/.buzz/personas/marketing.md"
+
+[channels.support]
+room = "support"
+# no persona key — falls back to this identity's base managed-agents.json
+# system_prompt, exactly like today. Backward compatible by default.
+```
+
+## Semantics (non-negotiable in v1 — matches the tool-scoping philosophy exactly)
+
+- **Replacing, not additive.** A channel entry with `persona`/`persona_file` set
+ **fully replaces** the base `system_prompt` for sessions created in that channel
+ — not layered underneath, not appended, not merged. This is the load-bearing
+ safety property: each channel's behavior stays a complete, self-contained,
+ independently-reviewable prompt, identical in spirit to the separate-agent model.
+ Do **not** build an additive "base + specialization layer" version — it
+ reintroduces prompt-dilution risk and adds a fixed token cost to every turn (the
+ shared base is paid for even when irrelevant to the channel).
+- **Fatal on load error.** Missing `persona_file`, malformed TOML, or both
+ `persona` and `persona_file` set → hard failure at startup, same as the existing
+ "never silently degrade to no policy" rule. A channel silently running the wrong
+ persona is worse than the process refusing to start.
+- **No entry = today's behavior.** Channels without a persona key keep using the
+ identity's normal `managed-agents.json` prompt. Nothing changes for channels that
+ don't opt in.
+- **Composes with existing tool-scoping.** `room`/`mcp` and `persona` can and should
+ appear together in one entry — one channel definition should describe everything
+ that channel does (tools *and* behavior), not require two separate config files.
+
+## Explicitly out of scope for v1 (real gaps — flag for a future spec)
+
+- **`respond_to`/`respond_to_allowlist` stay per-identity, not per-channel.** If one
+ channel's persona genuinely needs a different trust boundary than another channel
+ on the same shared identity, v1 doesn't solve that. Some identity splits may exist
+ *because* of a respond_to difference, not just a prompt difference.
+- **Nostr profile (kind:0) stays one-per-identity.** A shared identity's display
+ name/avatar is the same across channels with different personas — a persona named
+ one thing can visually appear while behaving as another in a different channel.
+ Not fatal, but decide it explicitly rather than discover it live.
+- **Template variables in `persona`** (channel name, date, etc.) — punt unless a
+ real need appears.
+
+## Migration path (once built and approved)
+
+1. Pick ONE low-stakes consolidation candidate first — two pure-advisory agents with
+ no action-capable surface, where the safety argument doesn't apply to either.
+2. Move each one's exact, current `system_prompt` **verbatim** into its own
+ `persona_file`, unchanged — this migration moves *where the prompt lives*, not
+ *what it says*.
+3. Point both channels' entries at the SAME underlying identity (retire one pubkey,
+ keep the other; repoint the retired one's channel memberships to the survivor).
+4. Verify byte-for-byte behavior parity in the surviving channels before trimming
+ anything else.
+5. Only after that pilot holds, consider wider consolidation — treat any agent with
+ a hard behavioral guarantee (e.g. "structurally cannot publish") as a "needs
+ care" case, not a clean first candidate.
+
+## Testing bar
+
+Mirror the tool-scoping suite exactly (unit tests + clippy clean). Cover:
+resolution precedence (UUID over name), fatal-load-error paths, the
+mutual-exclusivity check on `persona`/`persona_file`, and a real **integration test
+proving two different channels on one identity produce genuinely different,
+non-bleeding system prompts in two concurrent sessions** — cross-channel state
+bleeding on a shared process is the one truly new risk versus the tool-scoping
+precedent, and nothing built so far exercises it.
+
+## What this does and doesn't fix
+
+- **Fixes:** process/relay-pair overhead, *if* identities get consolidated using
+ this mechanism (necessary but not sufficient — someone still decides which
+ identities to merge and does the migration above).
+- **Does not fix:** every identity getting a runtime pair on every known community
+ regardless of relevance. That's an orthogonal problem (relay-pair *scoping*, not
+ persona-*count*) needing its own investigation into the app's pair-discovery.
+- **Does not change per-turn token cost** in the replacing design — each turn still
+ pays for one complete, non-diluted prompt. The savings are process/connection/
+ memory overhead, not LLM API spend.
From 4c72e5da93f87d1f2561f34292daba00f2e95407 Mon Sep 17 00:00:00 2001
From: Vibherpunk <216434340+Vibherpunk@users.noreply.github.com>
Date: Mon, 27 Jul 2026 07:44:13 -0700
Subject: [PATCH 7/7] =?UTF-8?q?docs:=20KNOWN=5FBUGS=20=E2=80=94=20personas?=
=?UTF-8?q?-in-rooms=20is=20a=20known=20defect=20(confuses=20agents),=20ro?=
=?UTF-8?q?lled=20back=20to=20924515dd8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The channel-scoped-personas feature (a9217e6b1) auto-derived a channel persona
from a Harbor room at spawn time. It confused agents by silently overriding the
system prompt with room state they could not see, making prompt provenance
opaque and behavior inconsistent. Rolled back to 924515dd8 (last commit before
the persona code). Personas relocated out of Harbor and staged as actual
owner-reviewed agents. Channel-scoped tools (skills/MCP) are preserved.
---
KNOWN_BUGS.md | 45 +++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 45 insertions(+)
create mode 100644 KNOWN_BUGS.md
diff --git a/KNOWN_BUGS.md b/KNOWN_BUGS.md
new file mode 100644
index 00000000000..742174db71c
--- /dev/null
+++ b/KNOWN_BUGS.md
@@ -0,0 +1,45 @@
+# Known Bugs
+
+## personas-in-rooms — known defect, confuses agents
+
+**Status:** Rolled back. Do not re-land.
+**Introduced by:** `a9217e6b1` — *feat(buzz-acp): channel-scoped personas*
+**Rolled back to:** `924515dd8` — *docs: proposal — channel-scoped personas* (the
+last commit before the persona code landed in `crates/buzz-acp/src/channel_tools.rs`
++ `pool.rs` and the desktop persona panel/commands).
+
+### What it did
+
+The "channel-scoped personas" feature auto-derived a Buzz channel's persona
+(system prompt) from whatever persona file lived in the mapped Harbor room,
+resolved per-channel at agent spawn time.
+
+### Why it's a bug
+
+It **confuses the agents**. A channel's system prompt stopped being a single,
+owned, reviewable instruction set — it was silently overridden/replaced by
+room state the agent could not see or reason about. Behavior became
+inconsistent depending on room contents, and prompt provenance was opaque.
+
+### Remediation applied
+
+- Fork rolled back to `924515dd8`; built and installed (no persona code in
+ `buzz-acp` or `buzz-desktop`).
+- Harbor room persona files relocated out of Harbor and staged as first-class
+ **actual agents** (owner-reviewed identities with explicit system prompts)
+ instead of room-derived persona overrides.
+- All other Buzz installs on the machine removed; only the rolled-back build
+ remains.
+
+### Scope
+
+Only the **personas-in-rooms** surface is reverted. The fork's other addition —
+**channel-scoped tools** (skills + MCP servers per channel via
+`channel-tools.toml`) — is preserved; it predates `a9217e6b1` and does not
+confuse agents.
+
+### Recommendation
+
+If per-channel instructions are needed, model them as **actual agents**
+(owner-reviewed identities with explicit system prompts), never as
+room-derived persona overrides.