Cross-run comms over HTTP (hub-and-spoke message bus)
The default file bus backend is filesystem IPC: it reaches every process on ONE machine and nothing beyond it. CYBERINEBUSBACKEND=http swaps in a backend that carries the same publish / subscribe...
The default file bus backend is filesystem IPC: it reaches every process on ONE machine and nothing beyond it. CYBERINE_BUS_BACKEND=http swaps in a backend that carries the same publish / subscribe / inbox port over the cyberine web API, so runs, chats and teammates on several machines share one set of mailboxes. The topology is deliberately hub-and-spoke: ONE machine runs the hub, every other machine is a spoke that talks only to it. No peer-to-peer, no discovery, no second hub.
Run recipe
On the HUB machine (the one that keeps the mailboxes), start the web server and mint a token for the spokes:
# Bind beyond loopback so spokes can reach it, and force the auth gate on.
CYBERINE_WEB_REQUIRE_AUTH=1 cyberine web --host 0.0.0.0 --port 41700
# In another shell on the same machine:
cyberine api-token issue spokes # prints the token ONCE - copy itOn every SPOKE machine, point the bus at the hub:
export CYBERINE_BUS_BACKEND=http
export CYBERINE_BUS_HUB_URL=http://hub.local:41700
export CYBERINE_BUS_HUB_TOKEN=cyberine_... # the token issued above
# Nothing else changes: dispatch, chat and teammates use the bus exactly as before.
cyberine run --bg "refresh the provider docs"
cyberine chatCYBERINE_BUS_HUB_URL falls back to CYBERINE_SERVER_URL and CYBERINE_BUS_HUB_TOKEN to CYBERINE_API_TOKEN, so a machine already configured as a remote CLI client (CYBERINE_SERVER_URL=...) only needs CYBERINE_BUS_BACKEND=http.
Check the wiring by hand with the three routes the backend uses (every one is also available under the /v1 alias):
HUB=http://hub.local:41700
TOKEN=cyberine_...
# publish one envelope into a run mailbox
curl -sS -X POST "$HUB/api/bus/publish" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"id":"probe-1","at":0,"from":{"kind":"worker","id":"probe"},"to":{"kind":"run","id":"r1"},"topic":"note","kind":"message","payload":{"text":"hello"}}'
# cold-read that mailbox (sinceMs and limit are optional)
curl -sS "$HUB/api/bus/inbox?kind=run&id=r1" -H "Authorization: Bearer $TOKEN"
# live SSE stream of the same mailbox (or ?topic=run-lifecycle for a broadcast channel)
curl -sS -N "$HUB/api/bus/events?kind=run&id=r1" -H "Authorization: Bearer $TOKEN"The hub itself always relays through its OWN local (file) mailboxes: publish on the hub is a local publish, and /api/bus/events is a local subscribe. The routes are auth-gated by the normal web security layer (an unauthenticated non-loopback caller gets the standard 401 UNAUTHORIZED envelope) and rate-limited in their own bus class (CYBERINE_WEB_RATE_LIMIT_BUS, 600/min per client).
Every method is best-effort, exactly like the file backend: a refused or unreachable hub logs and returns (publish), returns [] (inbox) or retries with capped backoff (subscribe) - it never throws into a run. The client dedupes by envelope id, so a reconnect that replays from the last seen timestamp delivers each envelope once.
Contract coverage: npm run selftest:bus-http runs one backend-agnostic contract (publish to subscribe, inbox cold read, request() / reply() round-trip) against BOTH the file backend and the http backend talking to a real in-process server on an ephemeral loopback port, plus the wrong-token refusal and the silent degradation above.
Known Limitations and Deferred Work
- Hub-and-spoke only, exactly one hub. There is no peer-to-peer path, no hub-to-hub federation and no failover: if the hub is down, spokes degrade to no-op bus calls (runs keep working, cross-run messaging stops). Point
CYBERINE_BUS_HUB_URLat a DIFFERENT machine than the one serving the hub - a hub pointed at itself would make/api/eventssubscribe to itself over HTTP. - Cross-host presence goes through the hub, and handles stay unique only per machine (T490). With
CYBERINE_BUS_BACKEND=httpset and a hub URL present, everyannouncePresence, heartbeat and tombstone also posts the record toPOST /api/bus/presence.roster()(and soresolveAddr,list_teamand/team) merges the hub's live records fromGET /api/bus/rosterwith this machine's own, and the local copy wins for the same address. The hub validates each record: a chat, run or worker address with an id, labels that are plain segments, at most 64 capabilities, and a TTL capped at 10 minutes. It stampslastBeatwith its own clock, so a spoke's clock skew cannot keep a record alive. Both routes use the same auth andbusrate class as the envelope routes. The hub roster fetch times out after 3 s and then falls back to the local roster. Limits: the handle claim file (kmq07) is still per machine, so two machines can mint the same handle andresolveRecordsreturns both; a remote record'spidis the other machine's. Hand-verified 2026-10-01 with one loopback hub and twoCYBERINE_HOMEdirectories standing in for two machines: each side saw the other's record, a spoke tombstone removed it from the hub roster, and an invalid body got400 INVALID_PRESENCE. Two real machines have not been tried. - p2p / websocket backend deferred. The discipline that keeps that option open is enforced today:
BusAddrhas nohostfield, the hub URL and token are read insidesrc/store/httpMessageBus.tsand are never threaded throughpublish/subscribe/inbox, andselectMessageBus()gained exactly one branch. A later transport therefore changes one file plus one selector line, and no caller. - One cold read is capped.
GET /api/bus/inboxreturns at most the newest 1000 envelopes (?limit=, max 5000) and reportstruncated: truepast that; the file backend's owninbox()returns everything. - Two-host verification has NOT been done. Everything above was verified in one process against a real loopback server: no run has yet crossed two machines, and neither latency nor behaviour through a reverse proxy (buffering of the SSE stream in particular) has been measured.
Durable teammate
Enables two complementary approaches so a teammate can receive and act on messages even after it has gone idle or exited. Both share the same core handler (src/store/durableTeammate.ts). Default...
Background shell jobs inside a worker (run_bash background + bash_jobs)
A loop worker can start a long-running shell command without blocking its tool round: runbash with background: true starts the command immediately and returns a job id; bashjobs with action="list"...