Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,12 @@ toolchain required.
Broker:

```sh
PEERBUS_LISTEN=0.0.0.0:8080 PEERBUS_TOKENS=t1,t2 PEERBUS_HMAC_SECRET=... \
PEERBUS_LISTEN=0.0.0.0:47821 PEERBUS_TOKENS=t1,t2 PEERBUS_HMAC_SECRET=... \
./peerbus serve
./peerbus audit verify [--db PATH] # walks the blake3 chain
```

`serve` config loads from env: `PEERBUS_LISTEN` (default `127.0.0.1:8080`),
`serve` config loads from env: `PEERBUS_LISTEN` (default `127.0.0.1:47821`),
`PEERBUS_TOKENS` (comma-separated, β‰₯1 required), `PEERBUS_HMAC_SECRET`
(β‰₯ `hmac.MinSecretLen` = 32 bytes), `PEERBUS_DB` (default `peerbus.db`; the
`--db` flag sets the base, env overrides). Missing tokens or a short secret
Expand All @@ -83,9 +83,9 @@ peerbus adapter --adapter=generic # or --adapter=cc
Env: `PEERBUS_URL`, `PEERBUS_NAME`, `PEERBUS_TOKEN`, `PEERBUS_HMAC_SECRET`.
Fail-fast (exit 2): missing `PEERBUS_URL`, missing `PEERBUS_TOKEN`,
`PEERBUS_HMAC_SECRET` shorter than `hmac.MinSecretLen` (32), or empty
`PEERBUS_NAME` when `--adapter=generic` (cc auto-mints
`cc-<host>-<pid>-<rand>` when name is empty). Missing/unknown `--adapter`
is also exit 2.
`PEERBUS_NAME` when `--adapter=generic` (cc auto-mints a friendly
`<adjective>-<noun>-<3-char-base36>` name β€” see `internal/channel`).
Missing/unknown `--adapter` is also exit 2.

## Load-bearing invariants β€” do NOT break

Expand Down
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@ COPY --from=build /out/peerbus /usr/local/bin/peerbus
# volume at /data. PEERBUS_DB should point here (see deploy/compose.yml).
VOLUME ["/data"]
# Broker config is read from PEERBUS_* env (see internal/broker/config.go):
# PEERBUS_LISTEN WS bind host:port (default 127.0.0.1:8080; set
# 0.0.0.0:8080 in a container so the published port
# PEERBUS_LISTEN WS bind host:port (default 127.0.0.1:47821; set
# 0.0.0.0:47821 in a container so the published port
# reaches it)
# PEERBUS_TOKENS comma-separated static bearer tokens (>=1 required)
# PEERBUS_HMAC_SECRET shared end-to-end HMAC-SHA256 secret (min length
# enforced; broker refuses to start otherwise)
# PEERBUS_DB durable SQLite path (point at /data)
# Default WS port. Keep in sync with PEERBUS_LISTEN.
EXPOSE 8080
EXPOSE 47821
# No HEALTHCHECK: the broker exposes no health endpoint/subcommand and the
# distroless image ships no shell or probe tooling. `restart: always` plus
# the broker's crash-on-misconfig (missing token / short HMAC secret exits
Expand Down
21 changes: 4 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,6 @@ Messages are peer-to-peer and out-of-band: peerbus moves messages *between alrea

**Honest taxonomy:** this is a **custom MCP-channel peer bus**. It is *conceptually* A2A-shaped β€” peer agents, asynchronous messages, human escalation handled by the peer rather than the bus β€” but it is **not** an implementation of Zed's [Agent Client Protocol](https://github.com/zed-industries/agent-client-protocol) nor of the Google / Linux Foundation [Agent2Agent (A2A)](https://github.com/a2aproject/A2A) specification. peerbus defines and implements its own small WebSocket wire protocol (see [`docs/wire-protocol.md`](docs/wire-protocol.md)); it borrows the *shape* of A2A-style peer messaging but ships none of those specs' types, handshakes, or guarantees. peerbus is its own bus, not an ACP/A2A implementation.

## Migration: v0.1.0 β†’ v0.2.0 (single binary)

v0.1.0 shipped two binaries (`peerbus-broker` and `peerbus-adapter`). v0.2.0 collapses them into **one** `peerbus` multi-command binary (git/kubectl style). Every flag and every env var inside each subcommand is preserved β€” only the dispatch shell changed. Pre-1.0, this is a breaking CLI rename; the wire protocol, security model, and on-disk store are unchanged.

| v0.1.0 | v0.2.0 |
| --------------------------------------- | --------------------------------- |
| `peerbus-broker serve` | `peerbus serve` |
| `peerbus-broker audit verify` | `peerbus audit verify` |
| `peerbus-adapter --adapter=cc` | `peerbus adapter --adapter=cc` |
| `peerbus-adapter --adapter=generic` | `peerbus adapter --adapter=generic` |

If you wired an adapter in `.mcp.json`, swap `"command": "peerbus-adapter", "args": ["--adapter=cc"]` for `"command": "peerbus", "args": ["adapter", "--adapter=cc"]` (note: **two** args now, since `adapter` is a subcommand). The Docker image still runs the broker by default β€” its `ENTRYPOINT`+`CMD` is now `peerbus serve`.

## How It Works

Two parts: a **broker** and **adapters**.
Expand Down Expand Up @@ -87,7 +74,7 @@ Broker configuration (struct defaults, overridden by env):

| Env var | Meaning |
| --------------------- | --------------------------------------------------------------- |
| `PEERBUS_LISTEN` | WS server bind address (`host:port`, default `127.0.0.1:8080`). |
| `PEERBUS_LISTEN` | WS server bind address (`host:port`, default `127.0.0.1:47821`). |
| `PEERBUS_TOKENS` | Comma-separated accepted static bearer tokens (at least one). |
| `PEERBUS_HMAC_SECRET` | Shared end-to-end HMAC-SHA256 secret (min 32 bytes enforced). |
| `PEERBUS_DB` | Durable SQLite store path (default `peerbus.db`). |
Expand Down Expand Up @@ -115,7 +102,7 @@ The same `peerbus` binary runs the adapter β€” pick the mode at launch with `pee
"command": "peerbus",
"args": ["adapter", "--adapter=generic"],
"env": {
"PEERBUS_URL": "ws://broker-host:8080",
"PEERBUS_URL": "ws://broker-host:47821",
"PEERBUS_NAME": "hermes-prod",
"PEERBUS_TOKEN": "<static bearer token>",
"PEERBUS_HMAC_SECRET": "<shared end-to-end HMAC secret>"
Expand All @@ -127,7 +114,7 @@ The same `peerbus` binary runs the adapter β€” pick the mode at launch with `pee

Tools: `bus.send` (direct), `bus.broadcast` (fan-out), `bus.peers` (list), `bus.drain` (return + ack pending β€” the host calls this on its own schedule). Full guide: [`docs/integrations/generic-adapter.md`](docs/integrations/generic-adapter.md). Recommended timed self-drain + escalation pattern for Hermes: [`docs/integrations/hermes-drain-skill.md`](docs/integrations/hermes-drain-skill.md).

**An interactive Claude Code session** uses `peerbus adapter --adapter=cc` instead. It is the MCP `claude/channel` server; inbound is a push-wake that creates a turn in an idle session (no `bus.drain`). Register it in `.mcp.json` as a server named `peerbus`, same env vars as generic but leave `PEERBUS_NAME` empty to auto-register `cc-<host>-<pid>-<rand>`:
**An interactive Claude Code session** uses `peerbus adapter --adapter=cc` instead. It is the MCP `claude/channel` server; inbound is a push-wake that creates a turn in an idle session (no `bus.drain`). Register it in `.mcp.json` as a server named `peerbus`, same env vars as generic but leave `PEERBUS_NAME` empty to auto-register a friendly `<adjective>-<noun>-<3-char-suffix>` name (e.g. `wild-wasp-3kx`). On startup the adapter pushes a system-kind notification announcing its bound name, and `bus.peers` returns `{ self, peers }` so the session always knows its own bus identity:

```json
{
Expand All @@ -136,7 +123,7 @@ Tools: `bus.send` (direct), `bus.broadcast` (fan-out), `bus.peers` (list), `bus.
"command": "peerbus",
"args": ["adapter", "--adapter=cc"],
"env": {
"PEERBUS_URL": "ws://broker-host:8080",
"PEERBUS_URL": "ws://broker-host:47821",
"PEERBUS_NAME": "",
"PEERBUS_TOKEN": "<static bearer token>",
"PEERBUS_HMAC_SECRET": "<shared end-to-end HMAC secret>"
Expand Down
12 changes: 5 additions & 7 deletions cmd/peerbus/adapter.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package main

// `adapter` subcommand. Ported verbatim from the v0.1.0
// cmd/peerbus-adapter/main.go β€” same --adapter dispatch, same env vars, same
// `adapter` subcommand: --adapter dispatch over PEERBUS_* env, with
// fail-fast guards (missing URL/Token, short HMAC, empty generic name).

import (
Expand All @@ -20,8 +19,7 @@ import (
)

// envClientConfig is split out so the env→ClientConfig mapping is testable
// and the variable names live in exactly one place. Matches v0.1.0
// peerbus-adapter behaviour verbatim.
// and the variable names live in exactly one place.
func envClientConfig() adapter.ClientConfig {
return adapter.ClientConfig{
URL: os.Getenv("PEERBUS_URL"),
Expand All @@ -33,8 +31,7 @@ func envClientConfig() adapter.ClientConfig {

// adapterRun parses adapter flags, builds the broker client config from the
// environment, resolves + constructs the mode, and runs it until a
// termination signal or the host closes stdio. Returns a process exit code.
// Behaviour mirrors v0.1.0 `peerbus-adapter --adapter=<mode>` exactly:
// termination signal or the host closes stdio. Returns a process exit code:
//
// - missing or unknown --adapter β†’ exit 2
// - missing PEERBUS_URL or PEERBUS_TOKEN β†’ exit 2
Expand Down Expand Up @@ -98,7 +95,8 @@ func adapterRun(args []string, stdout, stderr io.Writer) int {
// Generic mode binds a fixed peer name and has no auto-name fallback
// (only cc auto-generates one). An empty PEERBUS_NAME there is rejected
// at broker register on every attempt β†’ reconnect spin; fail fast
// instead. cc tolerates an empty name (mints cc-<host>-<pid>-<rand>).
// instead. cc tolerates an empty name (mints a friendly
// <adjective>-<noun>-<3 base36> name; see internal/channel.UniqueName).
if *mode == "generic" && cfg.Name == "" {
_, _ = fmt.Fprintln(stderr, "peerbus: PEERBUS_NAME is required for --adapter=generic")
return 2
Expand Down
16 changes: 6 additions & 10 deletions cmd/peerbus/broker.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package main

// Broker subcommands: `serve` and `audit verify`. Ported verbatim from the
// v0.1.0 cmd/peerbus-broker/main.go β€” same flags, same env precedence, same
// exit codes (audit verify still exits 0 intact / 1 break / 2 operational).
// Broker subcommands: `serve` and `audit verify`. Exit codes for audit
// verify: 0 intact, 1 a break was found, 2 operational error.

import (
"context"
Expand All @@ -20,15 +19,13 @@ import (
"github.com/nnemirovsky/peerbus/internal/version"
)

// defaultDBPath is the store location used when --db is not given. Matches
// the v0.1.0 broker default.
// defaultDBPath is the store location used when --db is not given.
const defaultDBPath = "peerbus.db"

// brokerServe parses serve-subcommand flags, loads broker config from env
// (env-overrides-struct precedence; see internal/broker.LoadConfig), and
// runs the WebSocket broker until SIGINT/SIGTERM. Exit 0 on clean shutdown,
// 2 on config/operational error. Behaviour mirrors v0.1.0 `peerbus-broker
// serve` exactly.
// 2 on config/operational error.
func brokerServe(args []string, stdout, stderr io.Writer) int {
fs := flag.NewFlagSet("peerbus serve", flag.ContinueOnError)
fs.SetOutput(stderr)
Expand Down Expand Up @@ -84,9 +81,8 @@ func brokerServe(args []string, stdout, stderr io.Writer) int {

// brokerAuditVerify implements the `audit verify` subcommand. The first
// positional arg must be the literal verb "verify"; the --db flag is
// accepted EITHER before or after the verb (matches the v0.1.0 broker's
// flag-before-subcommand calling convention and its tests which pass
// `--db PATH audit verify` and bare `audit`).
// accepted EITHER before or after the verb so callers may pass either
// `audit verify --db PATH` or `--db PATH audit verify`.
//
// Exit codes: 0 chain intact, 1 a break was found, 2 usage/operational
// error.
Expand Down
19 changes: 9 additions & 10 deletions cmd/peerbus/main.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
// Command peerbus is the single multi-command binary for the peerbus
// project. It collapses the v0.1.0 cmd/peerbus-broker and cmd/peerbus-adapter
// into one git/kubectl-style dispatcher.
// project: one git/kubectl-style dispatcher for the broker and adapter
// subcommands.
//
// Subcommands (each preserves its v0.1.0 flag/env contract verbatim):
// Subcommands:
//
// serve start the WebSocket broker (token auth + peer
// registry + direct/broadcast routing, offline
// queue, ack/redelivery). Was: peerbus-broker serve.
// audit verify [--db PATH] walk the blake3 hash-chain audit log and report
// any break. Was: peerbus-broker audit verify.
// queue, ack/redelivery).
// audit verify [--db PATH] walk the blake3 hash-chain audit log and
// report any break.
// adapter --adapter=<mode> run the adapter (mode resolved through the
// additive --adapter dispatch registry; today:
// cc | generic). Was: peerbus-adapter --adapter=<mode>.
// cc | generic).
//
// Top-level flags:
//
Expand Down Expand Up @@ -41,9 +41,8 @@ func main() {
// (brokerServe, brokerAuditVerify, adapterRun) is also independently
// testable.
func dispatch(args []string, stdout, stderr io.Writer) int {
// Top-level --version MUST work BEFORE subcommand parsing β€” matches the
// v0.1.0 behaviour of both old mains (`peerbus-broker --version` and
// `peerbus-adapter --version` both printed version and exited 0).
// Top-level --version MUST work BEFORE subcommand parsing so
// `peerbus --version` is answerable without a subcommand.
if len(args) > 0 {
switch args[0] {
case "--version", "-version":
Expand Down
3 changes: 1 addition & 2 deletions cmd/peerbus/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import (
)

// TestTopLevelVersion: `peerbus --version` prints the version and exits 0,
// WITHOUT requiring a subcommand. Matches the v0.1.0 behaviour of both old
// mains (both supported `--version` as a top-level flag).
// WITHOUT requiring a subcommand.
func TestTopLevelVersion(t *testing.T) {
var out, errb bytes.Buffer
if code := dispatch([]string{"--version"}, &out, &errb); code != 0 {
Expand Down
6 changes: 3 additions & 3 deletions deploy/compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ services:
# adapters on other machines can dial in. Keep the container-side port in
# sync with PEERBUS_LISTEN below.
ports:
- "8080:8080"
- "47821:47821"
environment:
# WS bind address inside the container. Bind 0.0.0.0 so the published
# port reaches it (the in-code default 127.0.0.1:8080 is loopback-only).
PEERBUS_LISTEN: "0.0.0.0:8080"
# port reaches it (the in-code default 127.0.0.1:47821 is loopback-only).
PEERBUS_LISTEN: "0.0.0.0:47821"
# Comma-separated static bearer tokens. Provide via .env / secret store,
# NOT inline. At least one token is required or the broker won't start.
PEERBUS_TOKENS: "${PEERBUS_TOKENS:?set PEERBUS_TOKENS out-of-band}"
Expand Down
4 changes: 2 additions & 2 deletions deploy/peerbus-broker.run
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ if [ -r /etc/peerbus/broker.env ]; then
fi

# Bind a routable address so adapters on other machines can dial in. The
# in-code default (127.0.0.1:8080) is loopback-only.
export PEERBUS_LISTEN="${PEERBUS_LISTEN:-0.0.0.0:8080}"
# in-code default (127.0.0.1:47821) is loopback-only.
export PEERBUS_LISTEN="${PEERBUS_LISTEN:-0.0.0.0:47821}"

# Durable SQLite store on persistent disk (survives restarts: the
# at-least-once + audit guarantee). Match the path to your volume mount.
Expand Down
10 changes: 2 additions & 8 deletions docs/integrations/generic-adapter.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
# Integration: the generic MCP adapter (`peerbus adapter --adapter=generic`)

> **v0.2.0 rename.** v0.1.0 invoked this as `peerbus-adapter
> --adapter=generic`; v0.2.0 ships ONE multi-command `peerbus` binary, so the
> invocation is now `peerbus adapter --adapter=generic` (`adapter` is a
> subcommand; `--adapter=<mode>` is its flag). Flags, env vars, and behaviour
> are otherwise unchanged.

How any agent runtime β€” Hermes, OpenClaw, Codex CLI, a bespoke bot β€” joins
the peerbus fabric. This is the universal path: every agent except a real
interactive Claude Code session uses it. (Claude Code has its own push-wake
Expand Down Expand Up @@ -42,7 +36,7 @@ Register the adapter as a stdio MCP server in the host's MCP config. Shape:
"command": "peerbus",
"args": ["adapter", "--adapter=generic"],
"env": {
"PEERBUS_URL": "ws://broker-host:8080",
"PEERBUS_URL": "ws://broker-host:47821",
"PEERBUS_NAME": "hermes-prod",
"PEERBUS_TOKEN": "<static bearer token>",
"PEERBUS_HMAC_SECRET": "<shared end-to-end HMAC secret>"
Expand Down Expand Up @@ -75,7 +69,7 @@ The generic adapter advertises exactly four tools:
| --------------- | ------------------------ | ----------------------------------------------------------------------------------------------- |
| `bus.send` | `to` (string), `body` (object) | Direct message to one peer. Body is opaque application JSON, hashed verbatim. |
| `bus.broadcast` | `body` (object) | Fan-out to every currently-registered peer except yourself. No backfill for late joiners. |
| `bus.peers` | β€” | List the peers currently registered on the bus. |
| `bus.peers` | β€” | Return `{self, peers}`: this adapter's bound name plus the other peers currently registered. |
| `bus.drain` | β€” | Return **and acknowledge** every message received since the last drain. |

`bus.drain` is the entire inbound path for a generic peer. It returns each
Expand Down
Loading
Loading