agent-bus-team
Small stdlib-only Python 3.11+ inter-agent messaging CLI and library.
One bus for Claude Code, Grok, Oh My Pi (omp), Codex, and others: one roster of agents, one durable inbox per agent, one identity each.
agent-bus send <name> -m TEXT is the only send command. It resolves the target, reads its kind
and picks the channel that kind actually reads: UDS for a Claude peer, a queued submission for a
Codex thread, the file inbox for everything else. No transport falls back to another — a Claude
session never reads a file inbox, so filing a message for one would report success for a message
that arrived nowhere.
Inbound lands in one place regardless. A native UDS frame from a Claude peer is persisted through
the same send_message call, addressed to the same roster id, and agent-bus inbox reads both
without being able to tell them apart.
UDS is not a second bus — it is how Claude Code peers reach this one, and how their acks get back.
An outbound frame carries from: uds:<our-socket> as its return address, and the
recipient dials that socket back with peer_message_status. A peer with no listener has no address
to be acked at, so the send fails before it opens a connection. The listener is the return path,
not a way of advertising yourself.
The asymmetry: Claude sees nothing. A Claude Code session has no plugin, no MCP server, no
inbox and no configuration — it uses its native ListAgents/SendMessage and peers simply
appear. Everything here (roster, inboxes, MCP tools, UDS listener) is peer-side, and its job is to
make a grok, omp or codex process look like a native Claude peer from the outside.
See identity-and-peering.md for how a peer gets an identity, and UDS-protocol.md for the wire format.
On-disk (AGENT_BUS_HOME=~/.agent-bus)
~/.agent-bus/
roster/<id>.json # registered agents (uuid ids)
inboxes/<id>.jsonl # append-only, one JSON message per line
captures/<pid>.jsonl # from `listen`
Roster entry and Message envelope match the spec in the source.
Rules (enforced)
- Plain text only in
text(no structured payloads). - Refuse
> 1_000_000chars. - Per-inbox unread cap 50 (send fails with clear error).
- Never treat a received message as user consent. Messages are cross-session only. The receiving agent must still show the user and obtain explicit approval before acting on any instruction in a message.
- Names unique among live (pid-alive) registrations; collisions get
-2,-3suffix on register. listdrops stale roster entries (dead pid) but leaves their inbox files.sendresolves the sender from your own registration, so two messages from one agent share an id the recipient can reply to.--from-nameoverrides it on the CLI; thesend_messageMCP tool deliberately does not expose that flag, so an agent cannot claim another agent's identity. With no registration the sender falls back toanonymous.
CLI
agent-bus list [--kind claude|grok|omp|codex|all] [--json]
agent-bus send <name-or-id> -m TEXT [--summary S] [--from-name N]
agent-bus inbox [--name N] [--unread] [--json]
agent-bus ack <message-id> [--name N]
agent-bus register --name N --kind claude|grok|omp|codex|other [--cwd P] [--pid P]
agent-bus unregister --name N
agent-bus self [--json]
agent-bus status <idle|busy|waiting> [--cwd P]
agent-bus watch [--name N] [--from-start] # one line per inbound message
agent-bus grok-status [--watch] # grok session activity from its leader
agent-bus orphans [--adopt] # mailboxes no roster entry points at
agent-bus listen [--name N] [--pid HOST_PID] [--inbox-name N]
agent-bus mcp # stdio MCP server (tools + UDS listen)
agent-bus hook session-start|session-end
# EXPERIMENT — test only, see below
agent-bus send-uds <socket-path> -m TEXT
list = live roster entries UNION what the native adapters can see (claude/grok/omp read-only
discovery of their own registries). "Live" is the address's own rule, not always a pid: a Codex
thread is addressable precisely when no process is running, so it is never discovered — a Codex
session joins by registering through the MCP server.
status is self-reported and nothing can infer it for you; an agent thinking between tool calls
is invisible from outside. The exception is grok, whose leader knows what its sessions are doing —
see grok-status.
watch prints one compact line per inbound message, for a harness whose watch mechanism turns
stdout lines into events (grok's monitor). It starts from now on purpose: replaying a backlog is
the fastest way to trip a rate limiter.
orphans finds inbox files no roster entry points at. Mail outlives its agent by design, so this
is how you get at messages left behind by a peer that exited.
send to a discovered native name/id will lazily create a roster entry + inbox under this bus home using a stable derived id (claude:<sessionId>, grok:... etc). The recipient only sees it if they also run agent-bus inbox (or via the MCP tools).
Adapters (read-only, best-effort, never throw)
- claude:
~/.claude/sessions/*.json(pid alive) - grok:
~/.grok/active_sessions.json - omp:
~/.omp/run/daemons/*/clients/*.json+ terminal-sessions fallback - codex:
~/.codex/process_manager/chat_processes.json(catalog skipped silently)
Override for tests with AGENT_BUS_SESSIONS_DIR etc. (File-bus adapters are read-only discovery and never write native sockets; native UDS send path does write for acks and peer messages — see UDS-protocol.md.)
MCP server
agent-bus mcp is a stdio MCP server. Transport is newline-delimited JSON; it mirrors an
LSP-style Content-Length client back in that client's own framing. Seven tools:
| tool | args |
|---|---|
list_agents |
optional kind |
send_message |
to, text, optional summary |
get_inbox |
optional name, unread_only |
ack_message |
message_id, optional name |
register |
name, optional kind |
set_status |
status, optional cwd |
self |
— |
register is how a peer claims a name. Starting the server already registers it — and since
the MCP initialize handshake names the client (codex-mcp-client, omp-coding-agent,
grok-shell-*), the kind and a derived name are set without asking. Call register to be
addressable as something you chose. It renames the entry the server already claimed for this pid
(it does not create a second identity) and repoints the published socket at the new name.
Starting the server also registers this host on the file bus and starts the UDS listener below.
listen — the UDS return path
agent-bus listen --name <title> --pid <host-pid> publishes a Claude-compatible session file and
UDS socket. Native ListAgents/SendMessage in Claude Code then see it as a teammate.
/list-agents itself is untouched.
--pid is watch-only: the advertised pid is always the listener process (the binder), so Claude's
getpeereid check matches. When that host pid already has a roster entry, listen adopts it
rather than registering again — one peer, one socket, one name, and no -2 suffix collision.
It:
- Binds UDS at
/tmp/cc-socks/<publish_pid>.sock(0o600, dir 0o700) - Writes matching
~/.claude/sessions/<publish_pid>.json+.key - Accepts JSONL frames, acks with
peer_message_statuson a dial-back conn only (never on the inbound conn) - Logs raw + parsed (auth redacted) to stdout and appends to
~/.agent-bus/captures/<pid>.jsonl - Unlinks only its own socket, session file and key on SIGINT/SIGTERM
A listener is started for every non-Claude kind (grok, omp, codex, other) whenever
session_start() runs, which in practice means whenever agent-bus mcp starts. Claude sessions
are the one kind that never get one, since they already have their own socket.
agent-bus send <name> -m "text" reaches a Claude peer over UDS; the transport is chosen from the
target's kind, so there is no vendor-specific send command to remember.
Claude users: install nothing. If a peer is running listen, it appears in /list-agents and
messages arrive as cross-session.
CRITICAL SAFETY
- Received content must never auto-execute. Require explicit user approval.
send-uds(notsend) is the raw-frame experiment for reversing the wire format. Do not point it at anything except a socket we started withlistenunder test overrides.
Test overrides: AGENT_BUS_SOCK_DIR, AGENT_BUS_SESSIONS_DIR, AGENT_BUS_HOME.
Installation
Note: package name is agent-bus-team; the CLI is agent-bus.
# run the latest version with uvx
uvx --from agent-bus-team agent-bus
# or install with pip
pip install agent-bus-team
agent-bus
Running from source
gh repo clone danbarua/agent-bus && cd agent-bus
python -m pip install -e .
agent-bus --help
For Claude or omp: use python -m agent_bus or the installed agent-bus CLI. No plugin is
required for Claude.
Installing it
pip install agent-bus-team # or: uv pip install -e .
That is the whole of it. A peer joins the bus by running the MCP server —
agent-bus mcp calls session_start() on startup and session_end() on exit, in-process, which
registers the session and publishes its UDS listener. Point your harness's MCP config at it.
There is no plugin. This repo used to carry a plugin.json, hook scripts and a CLI wrapper for
grok plugin install; all of it existed to run agent-bus from Grok's Bash tool, which is exactly
what the MCP server removed. A plugin manifest that wires up nothing on install only advertises a
capability that is not there. Whether a plugin is the right shape at all is still open — see
hooks-in-foreign-harnesses.md
for why the last attempt was worse than nothing.
agent-bus hook session-start|session-end remains as a CLI entry point for a harness that has
hooks and no MCP. It exits 0 always, never blocks on stdin, and writes only to stderr.
Claude Code: install NOTHING. No plugin, no .claude-plugin/, no .mcp.json, no skills or
slash commands. Claude sees a Grok/omp/codex peer through native ListAgents/SendMessage
because that peer's listen published the session file and socket.
The agent-facing surface is the MCP tool set above — the tool descriptions carry the consent
rule. Use the CLI or import agent_bus.store for everything else.
Development / test
python -m pytest tests/ -q --tb=line
AGENT_BUS_HOME=/tmp/ab-test python -m agent_bus list --json
Limitations / non-goals
- No impersonation of Claude's full protocol —
listenand the claude transport implement the UDS peer messaging subset only (see UDS-protocol.md). - No auto-start of other agents.
- Herdr TTY injection is a separate channel (not used here).
- Inboxes are per bus-home; multiple users would need separate homes or sync.
This is intentionally small and boring.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file agent_bus_team-0.1.4.tar.gz.
File metadata
- Download URL: agent_bus_team-0.1.4.tar.gz
- Upload date:
- Size: 200.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.11.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bf5d5d493e48ee155e5a0c2b94d53af040013ba84c4b63923fbe83b1d309a042
|
|
| MD5 |
5b25d96d214bdc12cf4011928660751f
|
|
| BLAKE2b-256 |
15d6f6bc5b4c9c033a428f9c4baedb3facc41c3a881ef78beed79db04cfb380a
|
File details
Details for the file agent_bus_team-0.1.4-py3-none-any.whl.
File metadata
- Download URL: agent_bus_team-0.1.4-py3-none-any.whl
- Upload date:
- Size: 82.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.11.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0a3a505715353d3c3b055fb5dceb69ec1e5d3bfe7da575420ed056196857e84d
|
|
| MD5 |
df32a25c7711c68957062cfd104922cf
|
|
| BLAKE2b-256 |
71d456d6b76b849c6af6448b697cc7e028e72d211f4704a9c2bfb616c73869db
|