Architecture
Dual-socket design
hero_router follows the hero_sockets convention but is unusual in that it
exposes two Unix sockets instead of one. Both live under
$PATH_SOCKET/hero_router/.
rpc.sock is the machine-callable surface. It speaks JSON-RPC 2.0 over
POST /rpc and exposes the router’s own router.* methods (service listing,
cache inspection, manifest queries). It also serves GET /openrpc.json,
GET /health, and the GET /.well-known/heroservice.json discovery manifest
whose protocol field is openrpc. SDKs, CLIs, and other agents always
connect here.
admin.sock is the browser-facing surface. It serves the HTML admin
dashboard, the /api/* helpers, the Server-Sent Events stream for live
updates, the MCP gateway, and — most importantly — the per-service reverse
proxy at /<service_name>/<webname>/<rest>. The discovery manifest on this
socket advertises protocol: "ui".
These two responsibilities are kept apart on purpose. Anything that answers an
RPC request must never serve HTML from the same socket, and anything that
proxies browser traffic must never expose POST /rpc on the admin surface. The
split makes policy decisions cheap: operators can whitelist or proxy each socket
independently, and automated clients never accidentally reach the dashboard.
The TCP listener(s)
hero_router serve binds one or more TCP listeners (default port 9988), all
serving the same Axum router that backs admin.sock. Anything you can do
over admin.sock you can do over TCP, subject to the IP whitelist. Passing
zero listeners (e.g. legacy --port 0 with no --address) disables TCP
entirely while keeping both Unix sockets bound — the right setting when the
router runs behind another reverse proxy.
The bind address is resolved (when --bind is omitted) via a layered chain:
HERO_MYCELIUM_IP env → ~/hero/cfg/hero_cfg.toml [mycelium].ipv6_address →
getifaddrs scan of br-$USER → 127.0.0.1 fallback. On provisioned
multi-user pods this means each user’s router binds to its own per-user
mycelium IPv6 with no manual config. The chosen source is logged at INFO on
startup.
Override with --bind ADDR:PORT (repeatable). Examples:
hero_router serve --bind :9988 # loopback shorthand
hero_router serve --bind 0.0.0.0:9988 # wildcard IPv4
hero_router serve --bind [::1]:9988 # IPv6 loopback
hero_router serve --bind :9988 --bind [::]:9989 # two listeners
Remote access to a router bound only to loopback must go through an explicit forwarding layer such as SSH, mycelium, or a dedicated HTTPS termination point.
Service discovery
The scanner walks $PATH_SOCKET/**/*.sock on a periodic interval (30
seconds by default) and classifies each socket by filename:
| Filename | Role |
|---|---|
rpc.sock | JSON-RPC backend |
admin.sock | HTTP admin panel + proxy target |
rest.sock | REST API endpoint |
openapi.sock | OpenAPI endpoint |
web_<name>.sock | Named web endpoint |
For every discovered socket the probe (with a 3-second timeout by default)
tries to fetch an OpenRPC document, check GET /health, and pull the
/.well-known/heroservice.json discovery manifest. Successful results are
stored in the in-memory RouterCache together with metadata: title,
description, version, method count, and healthy/unhealthy status.
When the scanner detects a change it emits an SSE event (service.added,
service.changed, service.removed) so browsers refresh their sidebars
without polling.
Manually pinned services — hosts that are not Unix sockets but should still
appear in the dashboard — live in manual_sources.json, default path
~/hero/router/manual_sources.json. The scanner merges them into the same
cache.
Reverse proxy path convention
The proxy mounts at /<service_name>/<webname>/<rest>:
<service_name>— the directory name under$PATH_SOCKET/(e.g.hero_proc,hero_db).<webname>— the socket stem without.sock(e.g.admin,rpc,rest).<rest>— whatever the backend expects after its own root.
Examples:
/hero_proc/admin/api/jobs → hero_proc/admin.sock at path /api/jobs
/hero_db/rpc → hero_db/rpc.sock at path /rpc
/hero_inspector/admin/ → hero_inspector/admin.sock at path /
WebSocket upgrades are forwarded transparently — that is how the Terminal tab’s
PTY stream reaches hero_proc unmodified.
Reserved first-segment names
Because the first path segment is a service name, certain names are reserved for built-in routes and can never be used as service directory names:
home, router, admin, terminal, docs, api, health, favicon.svg,
fragments, service, download, css, js, fonts, mcp, router-logs,
agent, openrpc.json, rpc