A tiny, loopback-first message relay for multiple AI agents on one machine (and, optionally, across machines). Agents do not talk to each other directly; they talk to the broker, which authenticates, routes, queues and delivers messages. This keeps the system simple: every agent needs only one HTTP client and one shared secret.
flowchart LR
subgraph Machine A [single machine - default deployment]
A1[dsh agent<br/>dsh plugin] -->|HMAC HTTP| B[relay broker<br/>127.0.0.1:19121]
A2[CLI client<br/>relay.mjs] -->|HMAC HTTP| B
A3[Python client<br/>relay_client.py] -->|HMAC HTTP| B
A4[Hermes-style agent<br/>example plugin] -->|HMAC HTTP| B
B -->|routes + queues| B
end
subgraph Machine B [remote agents - advanced, TLS required]
A5[remote agent] -->|HTTPS| B
end
| Component | Location | Role |
|---|---|---|
| Broker | broker/ |
HTTP service: HMAC auth, routing table, message queue (memory + SQLite default + JSONL compatibility), polling/lease API, brute-force lockout, rate limiting |
| dsh plugin | lib/ (Cordis host/client halves) |
Registers five agent_relay_* model tools; background heartbeat + lease polling, per-root sessions, receipts and sidebar status |
| CLI client | adapters/cli/relay.mjs |
Zero-dependency Node client for scripts, cron jobs, Codex/Claude wrappers |
| Python client | adapters/hermes/relay_client.py |
Pure-stdlib Python client for any Python-based agent |
| Setup | setup/setup.js |
init (generate secret + config), start, selfcheck |
sequenceDiagram
participant A as agent-alpha
participant B as broker
participant C as agent-beta
A->>B: POST /v1/messages {origin, target, kind, body}
B-->>A: 200 {message_id, root_id, protocol_version}
C->>B: POST /v1/pull {agent, limit, lease_seconds}
B-->>C: {messages: [<envelope>, lease_token]}
C->>B: POST /v1/lease/renew (optional for long work)
C->>B: POST /v1/ack {message_id, outcome, lease_token}
B-->>A: query /v1/status; replies use parent_id/root_id
- Lease polling, not push. The v2/v3 broker keeps messages until TTL and agents poll with a bounded lease. Expired leases are re-queued; long work can renew its lease. The frozen v1 cursor-polling compatibility layer remains.
- HMAC + timestamp anti-replay. Shared secret signs
method + path + timestamp + body. Timestamp skew > 300 s is rejected. - Idempotency by message id. Senders keep the same
idacross retries; the broker dedups; receivers dedup too. Exactly-once delivery is not guaranteed (at-least-once), but duplicate processing is prevented. - Loopback first. Default bind is 127.0.0.1. Remote mode exists but requires TLS — HMAC authenticates, it does not encrypt.
- No content logging. The broker logs events (ids, errors), never message bodies. The dsh plugin keeps only an in-memory id-level history.
- Single protocol source.
lib/protocol.jsowns v2/v3 canonical JSON, signature and envelope primitives;broker/src/protocol.jsis only a re-export shim. This prevents the two implementations drifting. - Zero dependencies. Broker, CLI and Python client use only the standard
library. The dsh plugin only needs the official
@deepseek-ai/dsh-toolspeer dependency.
- Tested against dsh 0.1.0-rc.6 (web profile plugin loading).
- Wire protocol v2/v3 with v1 compatibility — see PROTOCOL-V2.md and the legacy PROTOCOL.md. Adapters refuse incompatible broker versions during negotiation.