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_addressgetifaddrs scan of br-$USER127.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:

FilenameRole
rpc.sockJSON-RPC backend
admin.sockHTTP admin panel + proxy target
rest.sockREST API endpoint
openapi.sockOpenAPI endpoint
web_<name>.sockNamed 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
Add OpenRPC Spec

Add any OpenRPC spec by local file path or HTTP(S) URL. It will be fetched, validated, and added to the sidebar.

Socket paths, local files, or HTTP(S) URLs.
Overrides the title from the spec.