your OpenAI/Anthropic SDK or tool ───┐
│ HTTP (localhost)
▼
┌─────────────────────────┐
│ Hono proxy │
│ /v1/chat/completions │
│ /v1/messages │
│ /v1/messages/count_tokens
└───────────┬─────────────┘
│ calls into
▼
┌─────────────────────────┐
│ HatchClient │
│ - fresh Noise XX │
│ - register-caps │
│ - subscribe │
│ - send + collect │
└───────────┬─────────────┘
│ WebSocket + Noise
▼
wss://hatch.metaaivm.com/v1/noise?...
│
▼
┌─────────────────────────┐
│ per-user Hatch VM │
│ (DAEMON HTTP router) │
│ - /chat/stream │
│ - /chat/subscribe │
│ - /feed /ideas /goals │
│ - /connectors ... │
└───────────┬─────────────┘
│ runs the real agent
▼
Muse Spark (model)
+ agent memory
+ app connectors
+ attached files, etc.
Standard Hono + @hono/node-server. Two shape adapters:
- OpenAI shape —
POST /v1/chat/completions. Flattensmessages[]→ one prompt. Returns either a single JSONchat.completionor an SSE stream ofchat.completion.chunks terminated bydata: [DONE]. - Anthropic shape —
POST /v1/messages. Same flattening. Returns either a singlemessageJSON or SSE events matching Anthropic'smessage_start / content_block_* / message_delta / message_stopsequence. - Also
GET /v1/models,GET /healthz,POST /v1/messages/count_tokens(stub).
Auth: optional MUSE_PROXY_KEY env var checked against Authorization: Bearer <key> or x-api-key: <key>. Unset → open server (dev only).
HatchClient opens one Noise WS per request. Methods:
request(verb, path, body?)— fires one ApplicationRequest, returns the stream_id._recvOne()— pulls one assembled frame (handles bothresponse/body_chunkstreams and self-contained JSON events on subscribe streams).collectUntil(pred, deadlineMs)— loop that keeps calling_recvOneuntil a predicate matches.
sendAndCollectReply(userText, timezone, listenMs, sessionState?) orchestrates the sequence:
POST /api/nodes/registerwithnode_id = sessionState.nodeId(stable per API session). Required for side-chat event delivery.POST /client/register-capabilitieswith the samenodeIdasclient_id.POST /chat/subscribe— scoped (session_id) for side chats, global otherwise.POST /chat/streamwith the user text +session_idfor side chats.- Collect
delta.text_appendevents untildelta.message_done/message.assistant. - Side-chat fallback: if no deltas arrived (first-message race),
GET /chat/history?session_id=Xand extract the assistant reply.
Payloads larger than 48KB are automatically split across multiple NoiseTransportFrame chunks (see src/hatch/transport.ts).
SessionStore maps API-session keys → {sessionId, nodeId} with 24h sliding TTL + LRU eviction. Session key derivation (deriveSessionKey):
X-Muse-Session: <value>header →h:<value>→ dedicated side chat on muse.- else
Authorization: Bearer <token>→a:<sha256-16>→ dedicated per-API-key side chat. - else →
default→ shared main chat (nosession_idsent, backward compatible).
sessionId is a client-chosen UUID generated once per key; first /chat/stream with that id creates the side chat on muse, subsequent uses continue it.
- WebSocket to
wss://hatch.metaaivm.com/v1/noise?vm_id=&auth_token=¬ary_token=&app_id=hatch-web&request_id=. - 3-message Noise XX handshake (initiator) — hand-rolled on
@noble/curves(X25519),@noble/ciphers(AES-GCM),@noble/hashes(SHA-256 HKDF). - Each WS binary frame = one AES-GCM ciphertext of one
NoiseTransportFrameprotobuf. - The WS upgrade uses Node 22
globalThis.WebSocket; TLS fingerprinting for HTTP calls usescycletls(see §5 below).
Per-user VM at <vm_id>.metaaivm.com, fronted by the shared LB hatch.metaaivm.com. Runs a small internal HTTP router exposing JSON endpoints. The real agent (Muse Spark model + tools + memory) is behind the DAEMON service. SENTINEL/VAULT/AUTHD are support services only /healthz-reachable via noise.
Before the Noise WS can open, we need four HTTP POSTs on muse.ai with the user's session cookies:
/api/hatch/lease-vm— assigns a VM (403 if one already assigned; fall back to scraping the HTML)./api/hatch/vm/wake— wakes the specific VM./api/hatch/token— short-lived EdDSA JWT bound to the VM./api/hatch/noise-notary-token— server-side attestation of the VM's Noise key chain.
All four responses + a locally-generated request_id get combined into the final wss:// URL.
HTTP calls use cycletls for HTTP; the WS upgrade itself uses Node's native globalThis.WebSocket.
Each incoming OpenAI/Anthropic request → one fresh Noise handshake → one send-and-collect round-trip. Handshake overhead ~500ms per request. Not pooled.
The Hono server is async-native. Hatch work runs inside an async flow with streaming deltas pushed through Node.js readable streams to the SSE generator.
- Persistent across runs:
~/.ssh/*(your git keys),./storage_state.json(your muse.ai cookies),./session.json(last-bootstrapped tokens). - Rebuilt per run: everything else. Noise keys are per-handshake; stream_ids per-connection; the WS session itself is per-request.