Brio is a mobile control plane for Hermes Agent.
The preferred UX is:
- Open the mobile app.
- Sign in to the Brio relay.
- Generate a setup command.
- Run that command on the Hermes machine.
- It installs the slim
brioconnector, enables the Hermes API server, enrolls the machine with the relay, and installs/starts the connector service — the agent appears in the app.
Hermes Agent stays completely stock: the connector is a small Go binary in
this repo that keeps an outbound WebSocket tunnel to the relay and forwards
a fixed set of request paths to Hermes' local API server
(http://127.0.0.1:8642). It also proxies an opaque, multiplexed channel to
the native hermes serve /api/ws gateway for real-time conversations.
There is no local HTTP server in the connector.
apps/mobile- Expo React Native app.apps/relay- Go relay/control-plane service for remote connections.apps/connect- thebrioconnector binary (Go).packages/protocol- Shared JSON protocol schemas.
- Go
1.26.6(relay and connector). - Node.js and npm (mobile app).
- Hermes Agent on the target machine (stock; no fork needed).
hermes serveon loopback for real-time conversations and Command Center controls. Older installations can use the documented degraded REST/SSE mode.
Postgres is optional. The relay uses in-memory development storage when
BRIO_DATABASE_URL is unset.
The relay reliability and security invariants, their T3 Connect sources, and
the remaining single-replica/data-path limitations are documented in
docs/relay-practices.md.
make setup
make checkStart the relay and mobile app:
make dev-relay
make dev-mobileIn the app, sign in to the relay and generate a setup command. Run that command on the Hermes machine.
On the Hermes machine:
curl -fsSL https://github.com/0bkevin/brio/releases/latest/download/install.sh \
| BRIO_RELAY_URL="http://127.0.0.1:8082" \
BRIO_ENROLL_CODE="ABCD1234" \
BRIO_AGENT_NAME="Hermes" \
shThe installer downloads the release binary (with checksum verification) and
runs brio setup. Setup merges API_SERVER_ENABLED=true,
API_SERVER_HOST/PORT, and API_SERVER_KEY into ~/.hermes/.env
(preserving unrelated keys and any existing API key), claims the enrollment
code, writes its state to ~/.brio/connect.env, and installs/starts the
background service. Restart Hermes if it was already running so the API
server picks up the new settings.
For goals, heartbeats, background tasks, and agent controls, start the Hermes
control plane with a shared local token. The connector also accepts the token
from HERMES_DASHBOARD_SESSION_TOKEN in ~/.hermes/.env:
HERMES_DASHBOARD_SESSION_TOKEN=replace-with-a-random-local-token \
hermes serve --host 127.0.0.1 --port 9119The control server stays loopback-only. The connector keeps one persistent JSON-RPC/WebSocket connection so background completion events, agent ownership, and scheduled heartbeats survive mobile app reconnects.
On the Hermes machine you can also manage the connector directly:
brio setup --relay-url <relay-url> --code <code> \
--composer-root /path/to/project # enroll and allow @ references
brio connect # run the tunnel in the foreground
brio status # service + relay + tunnel credentials
brio recover --relay-url <url> --agent-id <id> \
--device-token <owner-device-token> --restart # recover credentials
brio install / uninstall / start / stop / restart # service lifecycleThe service is a user-level LaunchAgent (app.brio.connect) on macOS, a user
systemd unit (Restart=always) on Linux, and a schtasks ONLOGON task on
Windows; it runs brio connect with the home directory as working
directory.
Everything rides the relay tunnel. Per request frame:
Relay credentials never appear in current-client WebSocket URLs: the Go
connector sends its companion token in Authorization, while Mobile uses a
role-bound WebSocket subprotocol because browser WebSockets cannot set request
headers. The relay temporarily accepts the legacy ?token= form so already
installed connectors can upgrade without an outage only when
BRIO_ALLOW_LEGACY_QUERY_TOKENS=true is explicitly configured.
Conversation traffic uses a persistent channel_open / channel_data /
channel_close flow on that same authenticated tunnel. The connector opens
Hermes /api/ws locally and injects HERMES_CONTROL_TOKEN; gateway JSON-RPC
payloads remain opaque to Brio and the credential never reaches Mobile or the
relay. Channels share the tunnel's bounded queues and reconnect automatically.
Hermes event sequence replay removes duplicates and fills bounded reconnect
gaps; after every reconnect Brio also calls session.resume with the durable
session id so Hermes rebinds the live session transport and returns its
authoritative in-flight state.
- Forwarded to the stock Hermes API server with
Authorization: Bearer API_SERVER_KEY(replacing any frame credentials):/v1/responses,/v1/runs...,/api/jobs...,/v1/capabilities,/api/sessions,/api/sessions/{id}/messages,/health, plus the legacy aliases/chat/responsesand/capabilities. Hermes remains responsible for session filtering, pagination, profiles, and compression lineage. SSE responses stream as complete UTF-8-safestream_chunkframes and finish with an emptystream_endmarker; the mobile client parses the SSE stream into the terminal Responses API object. - Served locally by brio from
~/.hermes:/v1/memoryGET/PUT (legacy/memory) with atomic 0600 writes tomemories/MEMORY.mdandUSER.md.HERMES_HOMEis respected. The same routes under/p/<profile>/..., plus the full/api/profiles*management surface, operate on that profile's home instead. - Served locally by brio for the mobile composer: bounded chunked attachments,
context and command completion, prompt preparation, and redirect under
/attachments...and/composer.... File, folder, and Git references are limited to repeatable--composer-rootvalues (persisted asBRIO_COMPOSER_ROOTS); sensitive paths and private-network URL targets are rejected. - Served by brio through the official
hermes servecontrol connection:/control/rpc,/control/command,/control/background, and/control/events. These requireHERMES_CONTROL_TOKEN(orHERMES_DASHBOARD_SESSION_TOKEN) and default toHERMES_CONTROL_BASE=http://127.0.0.1:9119. Under/p/<profile>/...they require a per-profile control override (see Hermes Profiles). - Proxied transparently from Mobile to the official
hermes servegateway:/api/ws, or/p/<profile>/api/wswith a dedicated per-profile endpoint. Chat prompts use native JSON-RPC and consumemessage.*,tool.*, approval, reasoning, usage, and lifecycle events without polling. If the gateway is unavailable because the installed Hermes version predateshermes serve, Mobile explicitly entersdegradedstate and uses/v1/runs/{id}/eventsSSE plus REST mutations. The degraded path is still event-driven; it never polls run status on a fixed interval. - Everything else returns a 404-style error frame. The old file/config/ gateway/skills/tools/logs endpoints are intentionally gone.
One Brio environment (a connected machine) can host many Hermes profiles —
isolated agents with their own config, keys, memory, sessions, and gateway.
Identity is always the triple environmentId + profileName (+ sessionId);
threads, composer drafts, relay caches, and deep links are keyed per profile
so nothing is ever reused between two agents on the same machine.
Brio operates on the REAL Hermes profile layout (hermes_cli/profiles.py):
the sticky selection lives in <home>/active_profile, per-profile metadata
in <profile>/profile.yaml, gateway runtime state in
<profile>/gateway_state.json, and name validation/reserved aliases match
Hermes exactly. Reads are served from the filesystem; every mutation is
delegated to the installed stock hermes CLI with HERMES_HOME scoped to
the connector home, behind an injectable runner — so bundled-skill seeding,
standard directories, wrapper command aliases, managed gateway services,
Honcho host migration, s6 registration, and future Hermes invariants stay
authoritative rather than re-implemented in parallel.
The Manage tab exposes:
- list/show, create (blank /
--clone/--clone-all/--clone-from), use (sticky default), describe, rename, delete — mirroringhermes profilesemantics including typed-name confirmation at Brio's boundary; rename/delete update or remove command aliases and managed services because the CLI performs them natively. Failed creates validate before anything is invoked, leaving no orphan directory. - SOUL.md/description editing plus a per-profile setup command.
- Gateway Start/Stop/Restart per profile through real
hermes [-p <name>] gateway <action>semantics; multiplex conflicts surface verbatim as Hermes errors (the multiplexer is owned by the default gateway). - Export/import of real Hermes archives via the CLI (
profile export/profile import), which are credential-free by design; previews list files and env var NAMES only. Importing is two-phase: the preview issues apreview_tokenbound to the exact sanitized payload + target, and apply must echo it; archives that carry.env/.env.*/auth.jsonadditionally require an explicitallow_secretsconsent checkbox (values never leave the machine). Replacement imports are not offered — existing targets are rejected, matching stock Hermes. - Profile distributions from git URLs (
file://, github shorthand, https/ssh,#refpins for branches/tags/commits) or local checkouts: a safe staged preview parsesdistribution.yaml(name, version, hermes_requires, env_requires, distribution_owned), rejects symlinks, honors Hermes' hard USER_OWNED_EXCLUDE set (memories, sessions, .env/auth.json, state databases, caches, ...) and bounded file/size budgets, issues its ownpreview_tokendigest over the staged tree, and apply verifies that digest before runninghermes profile installagainst the same validated tree. The original URL/path (including #ref) is preserved as the installed manifest's source so future updates re-pull correctly.
Profile-scoped requests use the /p/<profile>/... prefix: chat, sessions,
runs, and jobs authenticate with that profile's own API_SERVER_KEY
(fail-closed when missing), unknown profiles get 404s before any upstream
call, and memory endpoints read/write that profile's home. Cross-profile
session importing pins the conversation to its source profile. Deep links
(brio://chat?agent=&profile=&session=) resolve to exactly one environment,
switch its profile, and open the named session; links for other
environments are dropped.
Per-profile Command Center support requires a dedicated hermes serve for
that profile; configure it in ~/.brio/connect.env as
HERMES_CONTROL_BASE_<ENCODED>. Simple [a-z0-9]+ names keep the legacy
raw uppercase key (CODER); separator names use a versioned form —
research-bot → V1_72657365617263682d626f74 (V1_ plus hex) — and
ambiguous legacy raw keys like RESEARCH_BOT or RESEARCH_HBOT are
rejected rather than misrouted. Without an override, profile-scoped control
requests fail closed instead of mixing another profile's control state.
Operational notes: archive transfers cap the raw export at 6 MiB so the
base64-encoded frame (~8 MiB) always fits inside the connector's 10 MiB
response frame limit; larger machines should copy hermes profile export
output directly. The hermes CLI and git must be installed on the agent
machine for mutations and git-URL distributions.
The app can also reach a Hermes API server directly on a LAN or private
network (for example over Tailscale): point it at the machine's
http://<host>:8642 with the API_SERVER_KEY from ~/.hermes/.env.
Plain chat and native session history work in direct mode. Connector-backed
composer features (attachments, context expansion, dynamic commands, and
redirect) require the Brio connector. Internet-facing endpoints must terminate
HTTPS before the API server.
Hermes' API server and hermes serve gateway are separate transports (normally
ports 8642 and 9119) with separate credentials. A direct pairing payload may
therefore provide gateway_url plus gateway_token to enable native /api/ws;
both fields are required together. Without them Brio deliberately uses the
REST/SSE degraded conversation path instead of reusing API_SERVER_KEY against
the wrong service. Static gateway tokens are intended for a loopback/private
tunnel; current public Hermes gateways use short-lived OAuth WebSocket tickets.
The root Makefile reads .env automatically if it exists. Start from:
cp .env.example .envCommon values:
BRIO_RELAY_ADDR- relay bind address, default127.0.0.1:8082.BRIO_DATABASE_URL- optional Postgres URL for relay persistence.BRIO_INSECURE_DEV_MODE- explicitly enables unverified email sign-in and unrestricted browser origins.make dev-relaysets the equivalent CLI flag; never enable it on a deployed relay.BRIO_ALLOW_LEGACY_QUERY_TOKENS- migration-only opt-in for old clients that put relay credentials in WebSocket query strings. Current clients use headers/subprotocols; leave this disabled after upgrades.BRIO_RELAY_TRUSTED_PROXY_CIDRS- comma-separated reverse-proxy networks allowed to supplyX-Forwarded-Forfor rate limiting. Forwarded addresses are ignored unless the direct TCP peer matches this list.- Production requests outside loopback require HTTPS/WSS. TLS-terminating proxies must be listed in
BRIO_RELAY_TRUSTED_PROXY_CIDRSand sendX-Forwarded-Proto: https; plaintext/healthremains available for load-balancer probes. - Current Mobile and connector clients also reject remote plaintext relay URLs and relay HTTP redirects before sending enrollment codes or long-lived credentials. Plain HTTP remains available only for explicit loopback development URLs.
BRIO_RELAY_ALLOWED_ORIGINS- comma-separated origins allowed for relay CORS and WebSocket upgrades. Include the relay's own public HTTPS origin for React Native Android, which sends that origin by default, plus any separate browser app origins. Origins are denied when this is unset unless insecure development mode is explicitly enabled.BRIO_CLERK_SECRET_KEYorBRIO_CLERK_JWT_KEY- enables verified Clerk identity onPOST /auth/devices. Also set the exactBRIO_CLERK_ISSUERandBRIO_CLERK_JWT_AUDIENCE;BRIO_CLERK_AUTHORIZED_PARTIESoptionally restricts the Clerkazpclaim. The matching Mobile build usesEXPO_PUBLIC_CLERK_PUBLISHABLE_KEY,EXPO_PUBLIC_CLERK_JWT_TEMPLATE, andEXPO_PUBLIC_BRIO_RELAY_URL.BRIO_DEVICE_REGISTRATION_KEY- operations-only fallback forPOST /auth/devicesthroughAuthorization: Bearer ...orX-Brio-Registration-Key. With neither verified identity, this key, nor insecure development mode configured, device registration fails closed. Never embed this key in a public app build.EXPO_PUBLIC_BRIO_DEV_AUTH- exposes Mobile's unverified email form for local development. Production builds leave this unset and use the configured Clerk account flow.- Signing out closes cached relay sockets and best-effort revokes the current device token before clearing local session state.
HERMES_CONTROL_BASE-hermes servebase URL, defaulthttp://127.0.0.1:9119.HERMES_CONTROL_TOKEN- session token shared withhermes servethroughHERMES_DASHBOARD_SESSION_TOKEN.
make check runs:
go test ./apps/connect/... ./apps/relay/...sh -n scripts/install.sh && sh scripts/install_test.shnpm run lintnpm run typechecknpm run export:web
The web export is written to /tmp/brio-web-export by default.
Pushing a v* tag triggers .github/workflows/release.yml: it validates the
repo (make check), cross-compiles the connector for
linux/darwin/windows on amd64/arm64 (CGO_ENABLED=0), and publishes a
GitHub release with the binaries, scripts/install.sh, and a
checksums.txt manifest. The installer downloads the release binary and
verifies its checksum.
POST /auth/devices- exchange a verified account token for a revocable device token. Explicit development/registration-key modes can issue an unverified device token instead.GET /me- inspect the authenticated device/user.GET /devices- list devices for the authenticated user.DELETE /devices/{id}- revoke a device token.GET /agents- list agents owned by the authenticated user.POST /enrollments- create a short-lived enrollment code for a user.POST /enrollments/{code}/claim- claim an enrollment code from a Hermes machine.POST /agents/{id}/recover- owner-authenticated recovery path that returns a fresh relay pairing code and connector token.POST /pairings- create a short-lived pairing record.GET /pairings/{code}- inspect a pairing record.POST /pairings/{code}/claim- claim a pairing once with a device token.GET /tunnel/companion/{agentID}- authenticated connector WebSocket tunnel; the connector sends its credential inAuthorization.GET /tunnel/mobile/{agentID}- authenticated mobile WebSocket tunnel; browser clients use a role-bound WebSocket subprotocol and the relay echoes only the fixed, credential-free protocol.
The relay routes request frames to one connected connector and records the requesting mobile peer by frame ID. Response, error, and stream frames from the connector are delivered only to that requesting peer. Pending relay requests expire after six minutes if the connector does not finish.
Chat requests use Hermes' Responses API SSE stream. The connector preserves
the SSE bytes in stream_chunk frames and finishes with stream_end. The
mobile app renders response.output_text.delta events incrementally and
falls back to ordinary JSON responses for older Hermes installations.
If a Hermes machine loses its ~/.brio state, recover the agent through the
relay and restart the connector with the returned token (see brio recover
above). The mobile app includes the same recovery flow.
The relay ships as a Docker image (apps/relay/Dockerfile). The current Oracle
deployment stack lives under deploy/oracle, while the AWS Copilot manifest
remains under copilot/. No production relay URL is embedded in Mobile or the
installer: deploy the service, configure Postgres and verified identity, then
provide its HTTPS URL through EXPO_PUBLIC_BRIO_RELAY_URL and generated
enrollment commands.