Put an agent in a Buzz channel
A Martha agent can sit in a Buzz channel as its own member — mentioned like a colleague, answering in-thread, signing its own messages with its own Nostr key. Nothing about the agent changes: the same definition, tools and grants that serve martha chat serve the channel. Only the edge is new.
The edge is martha acp-serve, a channel adapter: it turns an incoming channel event into a Martha turn (ingress) and delivers the answer back to the conversation it came from (egress). The channel's identity lives at the edge — a Nostr key here, a bot token for Slack later — so Martha's reasoning stays channel-agnostic and never holds key material.
Buzz relay ──► buzz-acp ──ACP/stdio──► martha acp-serve ──HTTPS──► Martha (cloud)
▲ (Block's harness) │ │ agent turn
└──────────── buzz messages send ◄──────┘ ◄───────────────────────┘
(signed by the agent's own key)You set this up from whichever product you are already in. Both directions end in the same place.
A. From Buzz Desktop
Buzz Desktop keeps a catalog of agent runtimes — Goose, Claude Code, Codex, Cursor, and any tool you register yourself. Register Martha once and it appears in New agent like the rest:
npm i -g @aiaiai-pt/martha-cli
martha channels install-harness buzzThat writes a custom-harness definition into Buzz Desktop's app-data directory (--print shows it without writing, --app-data <dir> targets a specific install). Restart Buzz Desktop, then New agent → runtime Martha.
What the agent still needs is an answer to which Martha, and which agent am I there — one profile, minted once:
martha principals bootstrap --mode agent --agent <agent-name> --save-profile <name>
martha channels install-harness buzz --use-profile <name>Now Edit Agent needs nothing: the definition names the profile, and the credential behind that name stays in this machine's credentials.json (0600). Never put the secret in the definition file — it is shared config, not a secret store.
A baked profile is a default, not a binding
The definition is shared by every agent on the Martha runtime. Bake a profile and a second Martha agent you create will answer as the first one's Martha agent — its own name and Nostr key in front of the same brain.
For more than one, set MARTHA_PROFILE=<its own profile> in that agent's Edit Agent → env vars. Per-agent env overrides the definition's (readiness.rs layer 3b over layer 2b), so it is still one variable and still no secrets.
Desktop owns everything else: it mints the agent's Nostr keypair into your OS keyring, computes the NIP-OA owner attestation, spawns the harness, and attaches the agent to channels. Martha contributes the adapter and the runtime definition — no vendored binaries, no second agent lifecycle.
The model dropdown is not the model
Buzz will say "Using built-in model options. Could not load live models for …" and offer a model list. Ignore it — a custom runtime has nothing for Desktop to enumerate, and the warning cannot block the agent (a non-builtin runtime's only readiness requirement is that its binary resolves on PATH). Whatever you pick is inert: Desktop passes it as BUZZ_ACP_MODEL, which acp-serve does not read, and session/set_model is acknowledged and ignored. The model that answers is the one on the Martha agent definition (llm_config) — so the model shown in Buzz is display only, and may disagree with reality.
One setting that is not a default
The definition pins BUZZ_ACP_MULTIPLE_EVENT_HANDLING=queue. Desktop's default is steer, which cancels the in-flight turn whenever a new message arrives — right for a local LLM that can weave the new message into what it is already doing, wrong for Martha, whose turn runs remotely and would be lost. Queued events wait for the current answer instead.
B. From Martha
When you are already in Martha and want the agent in a workspace you run:
martha channels connect buzz --agent <agent-name> --relay ws://relay.example:3030This is a preflight plus a launch, not a second transport. It checks every precondition first and exits non-zero rather than half-starting:
Martha CLI preflight — buzz channel: support
PASS martha CLI on PATH /usr/local/bin/martha
PASS that martha can serve ACP acp-serve available
PASS buzz-acp on PATH /usr/local/bin/buzz-acp
PASS buzz CLI on PATH /usr/local/bin/buzz
PASS Martha API + credential https://martha.example/api — service (…)
PASS agent is Martha-hosted cloud
PASS chat takeover (runs_as_chat)
PASS agent principal (bound client) support-buzz
PASS durable credential in env MARTHA_CLIENT_ID=support-buzz
PASS relay reachable http://relay.example:3030 → HTTP 200
PASS channel identity BUZZ_PRIVATE_KEY set (not shown)
PASS channel membership 3 channel(s): support, general, incidents
Ready.Every failing row names the one command that fixes it. Run the checks alone with martha channels preflight buzz --agent <name>; add --channel <uuid> to assert membership of one specific channel rather than any.
A check that cannot be evaluated reads WARN, never FAIL — a service-account credential cannot read admin definitions, so "I couldn't verify this" stays distinct from "this is missing". A preflight that guessed would send you to mint a second principal for an agent that already has one.
Running it somewhere else
Production adapters rarely run on a laptop. --emit prints the same launch as a deployable unit, with secrets referenced rather than inlined:
martha channels connect buzz --agent support --relay wss://relay.example --emit compose > adapter.yml
martha channels connect buzz --agent support --relay wss://relay.example --emit k8s
martha channels connect buzz --agent support --relay wss://relay.example --emit docker--dry-run prints the environment and the exact process it would start, without starting it.
What the agent needs
| Requirement | Why | How |
|---|---|---|
Martha-hosted (--reasoning martha) | the turn runs in Martha | martha agents create --reasoning martha |
runs_as_chat | takeover, so the agent's own tools serve the turn | martha principals bootstrap --mode agent |
| A principal bound to it alone | the credential acts as the agent | same command |
| A channel identity | messages are signed at the edge | Desktop's keyring (A) or BUZZ_PRIVATE_KEY (B) |
| Channel membership | the relay delivers events to it | buzz channels add-member --channel <id> --pubkey <agent pubkey> |
Bind exactly one agent per principal. A client with two agents silently disables chat takeover, which shows up as an agent that reads its mentions and answers with nothing.
Subscription settings
Every Martha agent follows the conversation: the harness definition pins
BUZZ_ACP_SUBSCRIBE=all
BUZZ_ACP_KINDS=9,46010,40007,7so DMs, thread replies and follow-up questions reach it without being re-@mentioned. You set nothing per agent.
Both values travel together on purpose. Buzz's own default is subscribe=mentions, which filters every message that does not #p-tag the agent — fine for a bot you summon, wrong for one you talk to. And all without kinds is a wildcard subscription: typing indicators and presence updates arrive as prompts and the agent starts answering keystrokes. The pinned set is buzz-acp's own mentions-mode set — chat (9), approval (46010), reminder (40007) — plus reactions (7), which the adapter classifies as feedback on an existing answer and answers with silence.
Want an agent that only speaks when named? Set BUZZ_ACP_SUBSCRIBE=mentions on that one agent in Edit Agent → env vars; per-agent env overrides the definition.
Identity, today and next
In direction A the identity is Buzz Desktop's (keyring + NIP-OA attestation). In direction B it comes from BUZZ_PRIVATE_KEY in the adapter's environment. Neither is yet issued by Martha, which means Martha cannot answer "which identities does this agent hold, and can I revoke one?" — that registry is #1119. Until it lands, treat the key as you would any deployment secret.
See also
- Host runner — the same shape for machines instead of conversations
- Chat handover — takeover semantics in detail