README
¶
tailbus
Make your agents reachable from anywhere.
Spin up any process — a Python script, an LLM pipeline, a data service — give it a name,
and it's instantly discoverable by every other agent you're running.
Website · Getting Started · Releases
┌─────────────────────┐
│ tailbus-coord │
│ discovery + relay │
└──────┬─────┬───────┘
peer map │ │ peer map
┌─────────┘ └──────────┐
▼ ▼
┌─────────────────┐ P2P gRPC ┌─────────────────┐
│ tailbusd │◄──────────►│ tailbusd │
│ office-mac │ mTLS │ cloud-vm │
└──┬───┬───┬───────┘ └──┬───┬───────────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
strategy marketing MCP-gw finance engineering
│
▼
Claude / Cursor
Tailscale-style topology for AI agents. Central coordination for discovery, peer-to-peer gRPC for data. No port forwarding, no YAML, no reverse proxies — agents register handles and talk to each other across machines, NATs, and cloud VPCs automatically.
Quick Start
# Install (Linux & macOS, no sudo required)
curl -sSL https://tailbus.co/install | sh
# Start the daemon — login with Google, join the mesh
tailbusd
Write your first agent in Python:
pip install tailbus
from tailbus import AsyncAgent, Manifest
agent = AsyncAgent("finance",
manifest=Manifest(description="Budget queries"))
@agent.on_message
async def handle(msg):
await agent.resolve(msg.session, "Q3 budget: $48,200 remaining")
await agent.run_forever()
That's it — finance is now discoverable by every agent on your mesh, across any machine.
Why tailbus?
Getting agents to talk across machines means solving four problems yourself: networking (stable endpoints, TLS, NAT traversal), discovery (how does agent A find agent B?), identity (authentication and mutual trust), and sessions (structured multi-turn conversations).
Each has a point solution — Tailscale for networking, A2A for protocol, OAuth for auth. But nobody bundles them for the person running 3–10 agents across a laptop, a home server, and a cloud VM who doesn't want to become a DevOps engineer to make them collaborate.
Tailbus handles all four with one install.
Features
Core
- Handle-based addressing — agents register names like
marketingorfinanceand message each other without knowing machines, IPs, or endpoints - @-mention auto-routing — when a message contains
@handle, the daemon auto-opens a session to that agent, wherever it lives; agents recruit each other mid-conversation - Structured sessions — open, exchange messages across multiple turns, and resolve when done; not fire-and-forget API calls
- P2P data plane — messages flow directly between daemons via bidirectional gRPC streams, never through the coord server
- NAT traversal — DERP-style relay with direct connection upgrade; agents behind home NATs, corporate firewalls, or private VPCs connect without port forwarding
- mTLS everywhere — all connections use mutual TLS with Ed25519 identity verification; coord uses TOFU (trust-on-first-use) cert pinning
MCP Gateway
Every tailbus daemon includes a built-in MCP gateway. Add one line to your Claude or Cursor config:
{
"mcpServers": {
"tailbus": {
"url": "http://localhost:1423/mcp"
}
}
}
Every registered handle becomes a callable tool. If your agent declares commands in its manifest, each command becomes a separate tool (e.g., finance.budget, calculator.add). Claude and Cursor can invoke agents on your mesh — including agents on other machines — through one endpoint.
Observability
- Distributed tracing — every session gets a
trace_id; spans are recorded at each hop - Prometheus metrics — counters and histograms at
/metricsfor external monitoring - Real-time TUI dashboard — terminal UI with mesh topology, per-handle health counters (in/out/drops/queue), sessions, and live activity
- Web chat UI — browser-based interface embedded in the daemon for testing and debugging
Reliability
- Delivery ACKs — automatic retry with at-least-once delivery guarantees (5s timeout, 3 retries)
- Message persistence — bbolt-backed store; sessions and pending messages survive daemon restarts
- Sequence numbers — per-session monotonic ordering
- OAuth login — device authorization flow (RFC 8628) with Google; JWT tokens with automatic refresh
- Team admin dashboard — web UI at tailbus.co/dashboard for managing teams, members, nodes, and invites
Developer Experience
- Python SDK — async and sync APIs, zero external dependencies, Python 3.10+
- Stdio JSON-lines bridge —
tailbus agentfor any language; read/write newline-delimited JSON on stdin/stdout - Service manifests — agents declare capabilities, commands (with JSON Schema), tags, and version
- Docker Compose — full mesh with example agents in 30 seconds
How It Works
1. Install and login
curl -sSL https://tailbus.co/install | sh
tailbusd # opens browser → Google login → machine joins mesh
2. Register agents
Agents connect to the local daemon via Unix socket and register a handle:
from tailbus import AsyncAgent
agent = AsyncAgent("marketing")
@agent.on_message
async def handle(msg):
print(f"From {msg.from_handle}: {msg.payload}")
await agent.resolve(msg.session, "Got it!")
await agent.run_forever()
3. Agents talk across machines
# On any machine on your mesh
from tailbus import AsyncAgent
agent = AsyncAgent("strategy")
response = await agent.open_session("marketing", "Draft the Q3 campaign plan")
print(response.payload)
The daemon resolves marketing to its machine, opens a P2P gRPC connection (with mTLS and NAT traversal), and delivers the message. No IPs, no endpoints, no routing config.
4. @-mention to recruit agents mid-session
strategy → "Campaign looks good. @finance can you confirm we have budget?
And @legal we need sign-off on the influencer contracts."
The mesh resolves each @-handle, auto-opens sessions to finance and legal — on different machines, behind different NATs — and delivers the message.
Examples
Docker Compose (30 seconds)
docker compose up --build
Starts: coord + 2 daemons + MCP gateway with web UI (port 8080) + 4 example agents (calculator, echo, orchestrator, LLM assistant).
Open http://localhost:8080 for the chat UI. Test with curl:
# List available agents
curl -s localhost:8080/mcp \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | jq '.result.tools[].name'
# Call the calculator
curl -s localhost:8080/mcp \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"calculator.add","arguments":{"a":2,"b":3}}}' | jq
# Cross-node P2P
curl -s localhost:8080/mcp \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"echo","arguments":{"message":"hello tailbus"}}}' | jq
Multi-Agent LLM Collaboration
Three LLM-powered agents collaborating through the mesh — all using a single local LM Studio instance:
| Agent | Role |
|---|---|
| researcher | Investigates topics, orchestrates the pipeline |
| critic | Reviews findings for accuracy, completeness, bias |
| writer | Synthesizes research + critique into polished output |
cd examples/multi-agent
docker compose up --build
Open http://localhost:8080, click researcher, and ask it to investigate any topic. The pipeline runs: researcher → critic → writer, each as a separate agent on the mesh.
3-Machine Demo
A travel agency scenario across 3 physical machines:
| Machine | Agents |
|---|---|
| A (coord + daemon) | concierge (orchestrator) |
| B (daemon) | flights, hotels (booking) |
| C (daemon) | weather, currency (data) |
See examples/demo/README.md for the full walkthrough.
Python SDK
Full async and sync APIs with zero external dependencies. Python 3.10+.
pip install tailbus
Async
from tailbus import AsyncAgent, Manifest, CommandSpec
import asyncio
async def main():
manifest = Manifest(
description="Marketing agent",
commands=(CommandSpec("campaign", "Run a campaign"),),
tags=("marketing",),
)
async with AsyncAgent("marketing", manifest=manifest) as agent:
await agent.register()
# Discover other agents
handles = await agent.list_handles()
print([h.handle for h in handles])
# Open a session
opened = await agent.open_session("sales", "Need Q4 numbers")
await agent.send(opened.session, "follow-up details")
await agent.resolve(opened.session, "Thanks!")
asyncio.run(main())
Sync
from tailbus import SyncAgent
with SyncAgent("my-agent") as agent:
agent.register()
opened = agent.open_session("sales", "hello")
agent.send(opened.session, "follow-up")
agent.resolve(opened.session, "done")
Handling incoming messages
from tailbus import AsyncAgent, Message
import asyncio
async def main():
async with AsyncAgent("responder") as agent:
await agent.register()
@agent.on_message
async def handler(msg: Message):
print(f"{msg.from_handle}: {msg.payload}")
if msg.message_type == "session_open":
await agent.send(msg.session, "got it!")
await agent.resolve(msg.session)
await agent.run_forever()
asyncio.run(main())
CLI Reference
tailbus [flags] <command> [args]
| Flag | Default | Description |
|---|---|---|
-socket |
/tmp/tailbusd.sock |
Path to local daemon Unix socket |
Auth commands
| Command | Description |
|---|---|
login [--coord addr] |
Device auth flow, save credentials |
logout |
Remove saved credentials |
status |
Show login status, email, token expiry |
Mesh commands
| Command | Description |
|---|---|
register <handle> [-description "..."] [-tags "a,b"] [-version "1.0"] |
Register an agent handle with optional manifest |
introspect <handle> |
Show full service manifest |
list [tags] |
List handles, optionally filtered by tags |
open <from> <to> <message> |
Open a new session |
send <session-id> <from> <message> |
Send within a session |
subscribe <handle> |
Stream incoming messages |
resolve <session-id> <from> [message] |
Close a session |
sessions <handle> |
List sessions for a handle |
dashboard |
Interactive TUI dashboard |
trace <trace-id> |
Show distributed trace spans |
agent |
Stdio JSON-lines bridge |
Stdio Agent Bridge
For languages without a dedicated SDK, tailbus agent provides a JSON-lines bridge over stdin/stdout:
tailbus agent
Inbound (stdin):
{"type":"register","handle":"my-agent","manifest":{"description":"My agent","tags":["demo"]}}
{"type":"open","to":"sales","payload":"Need Q4 numbers"}
{"type":"send","session":"<id>","payload":"follow-up"}
{"type":"resolve","session":"<id>","payload":"done"}
{"type":"list"}
{"type":"introspect","handle":"sales"}
Outbound (stdout):
{"type":"registered","handle":"my-agent"}
{"type":"opened","session":"<id>","message_id":"<id>","trace_id":"<id>"}
{"type":"message","session":"<id>","from":"sales","payload":"...","message_type":"session_open"}
{"type":"error","error":"session not found","request_type":"send"}
Rules: register must be first. content_type defaults to text/plain. trace_id on open is optional (auto-generated). The bridge exits on stdin EOF or SIGINT.
Architecture
proto/tailbus/v1/ Protocol buffer definitions
messages.proto Envelope, ServiceManifest, CommandSpec
agent.proto AgentAPI (daemon ↔ agents, Unix socket)
coord.proto CoordinationAPI (daemon ↔ coord)
transport.proto NodeTransport (daemon ↔ daemon P2P)
internal/
coord/ Coordination server
server.go gRPC server, peer map distribution
store.go SQLite persistence (pure Go, no CGo)
oauth.go RFC 8628 device auth + browser OAuth + OIDC
rest.go REST API for dashboard (/api/v1/)
cors.go CORS middleware
daemon/ Node daemon
daemon.go Main orchestrator
agentserver.go AgentAPI (Unix socket, handle binding)
router.go Message routing (local vs remote)
acktracker.go Delivery ACKs and retry
msgstore.go bbolt persistence (sessions, messages)
metrics.go Prometheus + health endpoints
mcp/ MCP gateway + web chat UI
gateway.go HTTP + SSE, handles → MCP tools
transport/ P2P data plane
grpc.go Bidirectional gRPC (mTLS + relay fallback)
relay/ NAT traversal (DERP-style)
server.go Stream mapping and forwarding
auth/ OAuth credentials + token refresh
identity/ Ed25519 keypairs, mTLS certificates
session/ Session lifecycle state machine
Message flow
- Agent calls
OpenSession/SendMessage/ResolveSessionvia Unix socket AgentServercreates the envelope, records a trace span, passes toMessageRouter- Router checks if the destination handle is local or remote:
- Local → delivers to subscriber channels
- Remote → resolves handle to peer address, sends via
GRPCTransport(mTLS)
- Transport tries direct P2P first, falls back to relay on failure
- Remote daemon receives, delivers to local subscribers, sends ACK back
AckTrackerremoves acknowledged messages; retries unacked (5s timeout, 3 retries)- On restart, pending messages and sessions restore from bbolt
Protocol
| Layer | Transport | Auth |
|---|---|---|
| Agent ↔ Daemon | Unix socket | Token file (mode 0600) |
| Daemon ↔ Coord | TCP + mTLS | TOFU cert pinning + JWT |
| Daemon ↔ Daemon | TCP + mTLS | Peer map verification |
| Daemon ↔ Relay | TCP + mTLS | Peer map verification |
Configuration
All binaries accept TOML config files via -config. Example files in examples/dev/.
Coordination server (tailbus-coord)
listen_addr = ":8443"
data_dir = "/tmp/tailbus-coord"
key_file = "/tmp/tailbus-coord/coord.key"
# auth_tokens = ["changeme"]
# Embedded relay (NAT traversal without a separate binary)
# relay_addr = ":7443"
# relay_advertise_addr = "coord.tailbus.co:7443"
# OAuth (browser-based login)
oauth_http_addr = ":8080"
# external_url = "https://coord.tailbus.co"
# web_app_url = "https://tailbus.co"
# insecure_grpc = false
# jwt_secret = ""
# [[oauth_providers]]
# name = "google"
# issuer = "https://accounts.google.com"
# client_id = "YOUR_CLIENT_ID.apps.googleusercontent.com"
# client_secret = "YOUR_CLIENT_SECRET"
| Field | Default | Description |
|---|---|---|
listen_addr |
:8443 |
gRPC listen address |
data_dir |
. |
SQLite database directory |
key_file |
{data_dir}/coord.key |
Keypair for mTLS (auto-generated) |
auth_tokens |
[] |
Pre-auth tokens for admission control |
oauth_http_addr |
:8080 |
OAuth HTTP listen address |
external_url |
(none) | Public URL for OAuth callbacks |
web_app_url |
https://tailbus.co |
Web app URL for CORS and browser OAuth redirect |
insecure_grpc |
false |
Disable gRPC TLS (for edge TLS termination) |
relay_addr |
(none) | Embedded relay listen address |
relay_advertise_addr |
same as relay_addr |
Address daemons connect to for relay |
jwt_secret |
(auto) | HMAC-SHA256 signing key |
oauth_providers |
[] |
OIDC providers for login |
Node daemon (tailbusd)
node_id = "node-1"
coord_addr = "coord.tailbus.co:8443"
advertise_addr = "127.0.0.1:9443"
listen_addr = ":9443"
socket_path = "/tmp/tailbusd-1.sock"
key_file = "/tmp/tailbusd-node1.key"
metrics_addr = ":9090"
mcp_addr = ":1423"
# auth_token = "changeme"
# credential_file = "~/.tailbus/credentials.json"
| Field | Default | Description |
|---|---|---|
node_id |
hostname | Unique node identifier |
coord_addr |
coord.tailbus.co:8443 |
Coordination server address |
advertise_addr |
(required) | Address other daemons use to reach this node |
listen_addr |
:9443 |
P2P gRPC listen address |
socket_path |
/tmp/tailbusd.sock |
Unix socket for local agents |
key_file |
/tmp/tailbusd-{nodeID}.key |
Node keypair (auto-generated) |
metrics_addr |
:9090 |
Prometheus + health + pprof endpoint |
mcp_addr |
(none) | MCP gateway listen address |
auth_token |
(none) | Pre-shared token (skips OAuth) |
credential_file |
~/.tailbus/credentials.json |
OAuth credential storage |
Relay server (tailbus-relay)
relay_id = "relay-1"
coord_addr = "127.0.0.1:8443"
listen_addr = ":7443"
key_file = "/tmp/tailbus-relay.key"
# auth_token = "changeme"
| Field | Default | Description |
|---|---|---|
relay_id |
relay-{hostname} |
Unique relay identifier |
coord_addr |
127.0.0.1:8443 |
Coordination server address |
listen_addr |
:7443 |
gRPC listen address |
key_file |
auto | Relay keypair (auto-generated) |
auth_token |
(none) | Auth token for coord admission |
All config fields can be overridden with command-line flags. Run any binary with -help.
Self-Hosting
Cloud Deployment (Fly.io)
The public coord server runs on Fly.io at coord.tailbus.co. To deploy your own:
fly apps create my-tailbus-coord
fly volumes create coord_data --region fra --size 1
fly secrets set OAUTH_CLIENT_ID=... OAUTH_CLIENT_SECRET=...
fly deploy --build-target coord
fly certs add coord.my-domain.com
The repo's fly.toml is pre-configured:
- OAuth HTTP on
:8080→ port 443 (Fly edge TLS) - gRPC on
:8443→ port 8443 (TCP passthrough, coord handles mTLS) - Relay on
:7443→ port 7443 (TCP passthrough) - Persistent volume at
/datafor SQLite + keys
From Source
# Prerequisites: Go 1.25+
make build # produces bin/tailbus-coord, bin/tailbusd, bin/tailbus, bin/tailbus-relay
# Start coord
./bin/tailbus-coord -config examples/dev/coord.toml
# Start daemons (separate terminals)
./bin/tailbusd -config examples/dev/daemon1.toml
./bin/tailbusd -config examples/dev/daemon2.toml
# Register agents, exchange messages
./bin/tailbus -socket /tmp/tailbusd-1.sock register marketing
./bin/tailbus -socket /tmp/tailbusd-2.sock register sales
./bin/tailbus -socket /tmp/tailbusd-1.sock open marketing sales "Need Q4 numbers"
Observability
Prometheus Metrics
curl http://localhost:9090/metrics
Counters:
| Metric | Description |
|---|---|
tailbus_messages_routed_total |
Total messages routed |
tailbus_messages_delivered_local_total |
Delivered to local subscribers |
tailbus_messages_sent_remote_total |
Sent to remote peers |
tailbus_sessions_opened_total |
Sessions opened |
tailbus_sessions_resolved_total |
Sessions resolved |
Histograms:
| Metric | Description |
|---|---|
tailbus_message_routing_duration_seconds |
Time to route a message |
tailbus_session_lifetime_seconds |
Session open → resolve duration |
Distributed Tracing
tailbus trace <trace-id>
# Trace 1ee8ae5a-... (6 spans):
# 15:51:35.345 MESSAGE_CREATED msg:42c03d06 node:node-1
# 15:51:35.346 SENT_TO_TRANSPORT msg:42c03d06 node:node-1
# 15:51:35.346 ROUTED_REMOTE msg:42c03d06 node:node-1
# ...
TUI Dashboard
tailbus dashboard
Top panel shows mesh topology (ASCII graph or compact list); bottom panels show handles, sessions, and activity. Each handle displays live health counters:
HANDLES
echo (2 subs) ↓42 ↑38 q:2
calculator (1 subs) ↓10 ↑10
overloaded (1 subs) ↓99 ↑50 q:48 drop:3
- ↓ messages delivered to the handle (in), ↑ messages sent from the handle (out)
- q: current queue depth (yellow when > 50% capacity)
- drop: messages dropped due to full subscriber channels (red)
Keyboard: q quit, r refresh, c clear, Tab toggle topology/detail view.
Health Endpoints
curl http://localhost:9090/healthz # {"status":"ok"}
curl http://localhost:9090/readyz # {"status":"ready"}
curl http://localhost:9090/debug/pprof/ # pprof index
Building External Agents
Agents connect via Unix socket using gRPC. The full interface:
service AgentAPI {
rpc Register(RegisterRequest) returns (RegisterResponse);
rpc IntrospectHandle(IntrospectHandleRequest) returns (IntrospectHandleResponse);
rpc ListHandles(ListHandlesRequest) returns (ListHandlesResponse);
rpc OpenSession(OpenSessionRequest) returns (OpenSessionResponse);
rpc SendMessage(SendMessageRequest) returns (SendMessageResponse);
rpc Subscribe(SubscribeRequest) returns (stream IncomingMessage);
rpc ResolveSession(ResolveSessionRequest) returns (ResolveSessionResponse);
rpc ListSessions(ListSessionsRequest) returns (ListSessionsResponse);
rpc GetNodeStatus(GetNodeStatusRequest) returns (GetNodeStatusResponse);
rpc WatchActivity(WatchActivityRequest) returns (stream ActivityEvent);
rpc GetTrace(GetTraceRequest) returns (GetTraceResponse);
}
Example in Go:
// Read auth token (auto-generated by daemon)
opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
if token, err := os.ReadFile("/tmp/tailbusd.sock.token"); err == nil {
opts = append(opts, grpc.WithPerRPCCredentials(/* Bearer token */))
}
conn, _ := grpc.NewClient("unix:///tmp/tailbusd.sock", opts...)
client := agentpb.NewAgentAPIClient(conn)
client.Register(ctx, &agentpb.RegisterRequest{
Handle: "my-agent",
Manifest: &messagepb.ServiceManifest{
Description: "My agent",
Tags: []string{"demo"},
},
})
stream, _ := client.Subscribe(ctx, &agentpb.SubscribeRequest{Handle: "my-agent"})
for {
msg, _ := stream.Recv()
fmt.Printf("Got: %s\n", msg.Envelope.Payload)
}
Development
make build # Build all binaries
make test # Unit tests
make test-all # All tests including integration
make proto # Regenerate protobuf code (requires protoc)
make clean # Remove binaries and generated code
Integration tests
go test ./internal/ -v -run TestEndToEnd # Full P2P session lifecycle
go test ./internal/ -v -run TestRelayEndToEnd # Relay fallback delivery
Releasing
git tag v0.x.0
git push --tags
goreleaser builds binaries for linux/darwin × amd64/arm64 via GitHub Actions and publishes to Releases.
Built with Go. Open source. tailbus.co
Directories
¶
| Path | Synopsis |
|---|---|
|
api
|
|
|
cmd
|
|
|
tailbus
command
|
|
|
tailbus-coord
command
|
|
|
tailbus-relay
command
|
|
|
tailbusd
command
|
|
|
internal
|
|
|
mcp
Package mcp implements an MCP (Model Context Protocol) gateway that exposes tailbus handles as MCP tools.
|
Package mcp implements an MCP (Model Context Protocol) gateway that exposes tailbus handles as MCP tools. |