Skip to main content
mountAgents<E>(map, options?): (req, env) => Promise<Response>
Route every agent in one call: mountAgents returns the fetch handler an umbrella function exports, so the front door you would otherwise hand-write — URL parsing, upgrade checks, credential extraction, name encoding, the hand-off, health routes — is already written, and the only thing left in your function is the policy that is actually yours.
mountAgents returns the handler, not the module’s default export: a function entry must export an OBJECT carrying a fetch handler, so the result goes on the fetch property. Exporting the returned function directly is refused at load time. One address, every transport Callers get one shape, identical across every agent you mount, and the request selects the transport — the path never changes: <base> defaults to /agents, so a support mount serves /agents/support/alice on all three. GET and POST are handed to the agent’s own fetch, where an AgentHttpServer over the agent’s connection engine answers them; the upgrade is handed to the agent’s webSocket. The forwarded request keeps its URL and query string, so the SSE subscribe filter, Last-Event-ID resume, and the /rpc/<method> segment all arrive exactly as the caller wrote them. Names The name segment is decoded and then re-encoded to an actor id with encodeAgentName, so names that are natural for customers — +15550100, an email address, a composite key, a name with accents or emoji — address a persistent instance without the caller thinking about encoding. An actor id is not free-form: only ASCII letters, digits, -, _, and . are addressable, and an id built from anything else belongs to an instance that never activates. encodeAgentName is what stands between a natural name and that constraint — it maps every other character onto the addressable set reversibly, so no two names share an id. Names that are already addressable pass through byte-for-byte, so mounting an existing agent does not re-point it. A hand-written front door addressing the same instances should call encodeAgentName for the identical result. That mapping is part of an actor’s identity and does not change. The id a name first resolves to is where that instance’s state and timers live; re-deriving the rule by hand, or expecting it to change, re-points a name at a different and empty instance. Letter case is significant throughout — alice and Alice are two instances. A name that has no faithful id — one that is empty, one whose escaped id would be longer than MAX_ESCAPED_ACTOR_ID_LENGTH characters, or malformed Unicode — is refused with a 400 naming the problem, on the request that carried it. Nothing is addressed and no actor is woken. How long a name may be depends on the characters in it, since escaping expands them; see MAX_ESCAPED_ACTOR_ID_LENGTH. The map is the whole routing table Only what your map returns is reachable. There is no discovery and no derivation from class names: an unknown mount key is a 404 and no binding is consulted for it. What it answers itself
  • /health/liveness and /health/readiness → 200 ok, with no actor woken and nothing about what is mounted in the body (see health).
  • A path outside base → fallback, or 404.
  • An address the mount owns but cannot resolve — an unknown mount key, a malformed address, an empty name → 404, one body for all of them.
  • A name that will not decode, one whose actor id would exceed MAX_ESCAPED_ACTOR_ID_LENGTH characters once escaped, or one with no faithful actor id → 400, with a message naming the problem.
  • A request whose shape matches no transport (a PUT, a GET on an RPC address, an upgrade to an RPC address) → 405 with an Allow header. No actor is woken for any of them.
Refusals from the mount carry a JSON body: \{"error": "not_found" | "method_not_allowed" | "malformed_name", "message": "…"\}. Answers from the agent — including its own errors — are relayed unchanged. What it does and does not decide The mount adds no policy of its own. It routes, and it runs the one authorize callback you give it — which runs identically on every transport, so no request shape reaches an agent without passing it. Everything past that is the agent’s: its authorize(token, req) resolves claims, its onAttach may still veto, and a mounted agent that overrides no connection seam still refuses sockets exactly as it would unmounted. Two things worth knowing before you trust a header inside an agent:
  • Headers authorize stamps overwrite what the caller sent under the same name, so a stamped identity cannot be forged.
  • Headers it does not stamp are forwarded as the caller sent them. Trust only what you stamp.
The map is read once per request and never cached, so a binding is resolved from the env of the request that uses it and nothing is held between them. Mount names are ordinary path segments, not secrets: they are part of the address every legitimate client already knows. Type Parameters Parameters Returns The handler to expose as your function entry’s fetch. (req, env) => Promise<Response>

AgentMountMap

AgentMountMap<E> = (env) => Readonly<Record<string, ActorNamespace | undefined>>
The agents this function serves, keyed by the mount name that addresses them. Written as a function of env so the bindings resolve per request:
The map is the entire routing table — the mount discovers nothing on its own and derives no names from class names, so an agent is reachable only once it is written here. Type Parameters Parameters Returns Readonly<Record<string, ActorNamespace | undefined>>

MountOptions

Options for mountAgents. Type Parameters Properties authorize?
optional authorize?: (req, route) => MountAuthorizeResult | Promise<MountAuthorizeResult>
Your one line of policy: decide whether this request may reach the agent, and what identity to stamp on it. Runs after the address resolves and BEFORE anything touches an actor, on every transport — a WebSocket upgrade, an SSE stream, and each RPC POST alike. Return a Response to reject (nothing is forwarded and no actor is woken), a MountHeadersInit to admit and stamp, or nothing to admit as-is.
This is edge authentication, and it composes with the agent’s own authorize(token, req) seam rather than replacing it: the stamped headers are added to the forwarded request, the caller’s credential (?token=, or an Authorization header) rides through untouched, and the agent still resolves its own claims from both. Stamp identity here; decide grants there. A stamped header always wins: it overwrites any header of the same name the caller sent, so a client cannot forge an identity your agent trusts. Headers you do NOT stamp are forwarded as the caller sent them — trust only the ones you stamp. A throw from this callback is not an admission: it propagates, and the request never reaches an actor. Reading the body here is safe — verify a signature over it, or check an RPC’s argument shape. The agent is handed an independent copy, so a body consumed here still arrives there intact. That copy is made only for a request that carries a body (never for a WebSocket upgrade or a GET), and it does mean the body is buffered while both copies are outstanding: on a large streamed upload, prefer deciding from the headers. Parameters Returns MountAuthorizeResult | Promise<MountAuthorizeResult>
base?
optional base?: string
The path prefix the mount owns. Every address is <base>/<mount>/<name>. A leading slash is optional and a trailing one is ignored, so "agents", "/agents", and "/agents/" are the same base. Use "/" to mount at the root. Default Value "/agents"
cors?
optional cors?: MountCorsOptions
Opt in to cross-origin access, for a browser page served from another origin (an EventSource on the SSE address, a fetch on the RPC one). Absent — the default — the mount sends no CORS headers at all and answers OPTIONS with 405, so a cross-origin page cannot reach it. This is a passthrough and nothing more: it does not authenticate, and it never substitutes for authorize.
fallback?
optional fallback?: (req, env) => Response | Promise<Response>
What to do with a request whose path is not under base. This is how the mount composes inside a hand-written fetch: the paths you own fall through to your handler, the agent addresses do not.
Parameters Returns Response | Promise<Response> Default Value a 404 — the mount answers for the whole function.
health?
optional health?: boolean
Answer /health/liveness and /health/readiness with 200 ok. These are answered by the mount itself — no actor is addressed, no authorize runs, and the body carries nothing about what is mounted. Set false when the surrounding function owns its own health routes. Default Value true

MountRoute

What the mount resolved from the request, handed to authorize so the policy can decide with the routing already done. Properties actorId
readonly actorId: string
The actor id this name resolves to — encodeAgentName(name).
method?
readonly optional method?: string
The method name, for a rpc request only.
mount
readonly mount: string
The mount key from the address — a key of the map you returned.
name
readonly name: string
The routing name from the address, decoded ("+15550100", not "%2B15550100").
transport
readonly transport: MountTransport
The transport the request’s shape selected.

MountTransport

MountTransport = "websocket" | "sse" | "rpc"
The transport a request selected, as resolved from its shape (never from the path): a WebSocket upgrade, a GET for Server-Sent Events, or a POST to …/rpc/<method>.

MountCorsOptions

Cross-origin passthrough for MountOptions.cors — opt-in, never a default. Properties credentials?
optional credentials?: boolean
Allow credentialed requests (cookies, TLS client certs). Requires a concrete origin, never "*". Default Value false
headers?
optional headers?: readonly string[]
Request headers a cross-origin caller may send, beyond the CORS-safelisted ones. Default Value ["authorization", "content-type", "last-event-id"] — what the RPC and SSE addresses actually read.
maxAge?
optional maxAge?: number
How long (seconds) a browser may cache the preflight answer. Default Value

origin
origin: string | readonly string[]
The origins allowed, matched exactly. "*" allows any origin and is refused in combination with credentials, which browsers reject anyway.

MountAuthorizeResult

MountAuthorizeResult = Response | MountHeadersInit | void
What authorize may answer:
  • a Response — reject; no actor is woken and nothing is forwarded;
  • a MountHeadersInit — admit, stamping those headers onto the forwarded request (each one overwrites any same-named header the caller sent);
  • nothing — admit, stamping nothing.

MountHeadersInit

MountHeadersInit = Headers | Record<string, string> | [string, string][]
The headers authorize may stamp — the same shapes the Headers constructor accepts, so the ergonomic literal is the common case and a built Headers works when a policy needs one: