cyberine
Usage

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 it

On 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 chat

CYBERINE_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_URL at a DIFFERENT machine than the one serving the hub - a hub pointed at itself would make /api/events subscribe to itself over HTTP.
  • Cross-host presence goes through the hub, and handles stay unique only per machine (T490). With CYBERINE_BUS_BACKEND=http set and a hub URL present, every announcePresence, heartbeat and tombstone also posts the record to POST /api/bus/presence. roster() (and so resolveAddr, list_team and /team) merges the hub's live records from GET /api/bus/roster with 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 stamps lastBeat with its own clock, so a spoke's clock skew cannot keep a record alive. Both routes use the same auth and bus rate 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 and resolveRecords returns both; a remote record's pid is the other machine's. Hand-verified 2026-10-01 with one loopback hub and two CYBERINE_HOME directories 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 got 400 INVALID_PRESENCE. Two real machines have not been tried.
  • p2p / websocket backend deferred. The discipline that keeps that option open is enforced today: BusAddr has no host field, the hub URL and token are read inside src/store/httpMessageBus.ts and are never threaded through publish / subscribe / inbox, and selectMessageBus() 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/inbox returns at most the newest 1000 envelopes (?limit=, max 5000) and reports truncated: true past that; the file backend's own inbox() 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.