Skip to content
Back to skills

Buzz

ASecurity

Building and operating Buzz agents — env-var configuration, the ACP stdio adapter, Nostr identity and event signing, joining a NIP-29 relay group, and rendering skills into a Buzz persona. Read when scaffolding with `--framework buzz`, when an agent needs to speak in a Buzz group, when deciding how a Buzz agent should be configured or deployed, or when a relay connection or signature is being rejected.

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 2, 2026
ai-agentspythonrustdebugginggitapi

Works with

  • terminal
  • cli
  • api

Security analysis

A100/100

Scanned October 2, 2026

npx -y skills add Folken2/nuvel --skill buzz --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Buzz?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Buzz
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/folken2-buzz/badge)](https://www.skillsdirectory.com/skills/folken2-buzz)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: buzz
description: Building and operating Buzz agents — env-var configuration, the ACP stdio adapter, Nostr identity and event signing, joining a NIP-29 relay group, and rendering skills into a Buzz persona. Read when scaffolding with `--framework buzz`, when an agent needs to speak in a Buzz group, when deciding how a Buzz agent should be configured or deployed, or when a relay connection or signature is being rejected.
---

# Buzz agents

A Buzz agent is deliberately small: no web framework, no agent SDK, no config
file. It is a package with four entrypoints over one runtime, configured
entirely from the environment.

```
python -m <pkg>.cli "…"        one-shot / REPL
python -m <pkg>.acp            ACP subprocess (stdio JSON-RPC) — editors
python -m <pkg>.buzz_relay     join a Buzz group and answer in chat
python -m <pkg>.nostr_identity print the agent's npub
```

All of them drive `acp/runtime.py::AgentRuntime`. If a turn behaves differently
in chat than in the terminal, that's a bug — there is only one loop.

## The shape

| Module | Owns |
|---|---|
| `agent.py` | `BuzzConfig.from_env()`, the `Tool` type, `build_agent()` |
| `acp/runtime.py` | sessions, the model↔tool loop, streaming translation |
| `acp/server.py` | the ACP protocol surface |
| `skills/` | `SKILL.md` discovery, `list_skills` / `read_skill` |
| `nostr_identity.py` | secp256k1 keygen, BIP-340 signing, bech32, NIP-01 events |
| `buzz_relay.py` | the NIP-29 group connection |
| `buzz_persona.py` | skills → the published persona |

## Configuration is the environment

There is no settings file to thread through. `BuzzConfig.from_env()` is the one
place model config is resolved, and `validate()` is the one place it's checked:

```python
cfg = BuzzConfig.from_env()
problems = cfg.validate()      # [] means ready
```

`BUZZ_AGENT_PROVIDER` selects a base URL and a key env from `PROVIDERS`;
`BUZZ_AGENT_BASE_URL` overrides it for anything OpenAI-compatible (a gateway,
vLLM, LM Studio). The provider prefix on a model id is stripped when it names
the provider being called, so `openrouter/moonshotai/kimi-k2.5` and
`moonshotai/kimi-k2.5` both work — the nuvel-wide ids carry the prefix for
LiteLLM, the raw HTTP API doesn't want it.

**Adding a provider** means one line in `PROVIDERS` — `(base_url, key_env)` —
as long as it serves `/chat/completions`. It doesn't mean a new client.

## Tools are callables plus a schema

```python
from <pkg>.agent import Tool

Tool(
    name="get_status",
    description="Look up the current status of a job.",
    parameters={"type": "object",
                "properties": {"job_id": {"type": "string"}},
                "required": ["job_id"]},
    handler=get_status,        # sync or async
)
```

The loop calls the handler with decoded arguments as kwargs and JSON-encodes
whatever comes back. A raising handler is *not* fatal: the exception is
serialized as `{"status": "error", "message": …}` and handed to the model,
which usually recovers by trying something else. Reserve hard failures for
config problems the model can't route around.

`BUZZ_AGENT_MAX_TOOL_ITERATIONS` bounds the model↔tool round trips per turn
(default 8). Hitting it emits a visible "stopped" message rather than
silently truncating — if you see it in logs, the agent is stuck in a loop, not
working hard.

## Skills, and the persona that follows from them

Skills are `skills/<slug>/SKILL.md` in the Anthropic format. Only frontmatter
is loaded at startup; bodies arrive via `read_skill`. `BUZZ_SKILLS_DIR` points
the loader somewhere else (a mounted volume, a shared skills repo).

`buzz_persona.py` renders those same skills into the kind-0 profile the relay
publishes and into the `to_intro()` blurb for the group. **Don't hand-maintain
a capability list** — it drifts within a week. Add the skill; the persona
follows.

## Identity: the agent is its key

Everything published is a Nostr event signed with a secp256k1 key. The
implementation is in-repo and dependency-free (BIP-340 Schnorr, bech32 for
`npub…`/`nsec…`, NIP-01 event ids). `load_identity()` resolves:

1. `NOSTR_PRIVATE_KEY` (hex or `nsec1…`);
2. `NOSTR_KEY_FILE` (default `.nostr/identity.json`) if present;
3. otherwise generate, persist at mode `0600`, and keep it.

The npub *is* the agent's identity to everyone in the group. Losing the key
means becoming a stranger; leaking it means someone else can be the agent.
Pin it via `NOSTR_PRIVATE_KEY` from a secret manager in production, and keep
the key file gitignored everywhere else.

## The relay loop

Buzz groups are NIP-29: chat is a kind-9 event with an `h` tag naming the
group; the relay is a WebSocket speaking NIP-01 frames.

```
REQ  {"kinds":[9], "#h":[group], "since": now}   subscribe
EVENT ["EVENT", <signed kind-9>]                  publish
```

`BuzzRelay` verifies every inbound event's id and signature before acting on
it — a relay is an untrusted intermediary, and an unsigned "message" is just
a string someone sent you. It then applies `BUZZ_REPLY_POLICY`:

- `mention` (default) — a `p` tag naming the agent, its npub in the body, or
  `@handle`;
- `all` — every message in the group.

Start with `mention`. An agent on `all` in a busy group is both expensive and
socially exhausting.

The whole group shares one session id (`buzz-<group>`), so the agent follows
the conversation rather than treating each message as a cold start. Turns are
collected and published as a single message — chat isn't a streaming medium.

Connection drops are normal: `run()` reconnects with exponential backoff to 60s,
and a failed turn is logged and skipped rather than killing the worker.

## Debugging

| Symptom | Look at |
|---|---|
| `config: No API key…` on start | `validate()` — the provider's key env isn't set |
| Relay accepts, nothing happens | `BUZZ_REPLY_POLICY=mention` and nobody mentioned the agent |
| `relay rejected <id>` in logs | the relay's `OK` frame carries the reason; usually group membership |
| "bad id or signature" on inbound | clock skew or a relay mangling `content`; ids cover the exact bytes |
| Editor sees no agent | stdout must carry JSON-RPC only — a stray `print` corrupts the stream |

That last one is worth internalizing: `acp/__main__.py` repoints `sys.stdout`
at stderr before importing anything else, precisely so a library's banner
can't break the protocol. Log to stderr, always.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.