mountAgents<Route every agent in one call:E>(map,options?): (req,env) =>Promise<Response>
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/livenessand/health/readiness→200 ok, with no actor woken and nothing about what is mounted in the body (seehealth).- A path outside
base→fallback, or404. - 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, aGETon an RPC address, an upgrade to an RPC address) →405with anAllowheader. No actor is woken for any of them.
\{"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
authorizestamps 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.
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<The agents this function serves, keyed by the mount name that addresses them. Written as a function ofE> = (env) =>Readonly<Record<string,ActorNamespace|undefined>>
env so the bindings resolve per request:
Parameters
Returns
Readonly<Record<string, ActorNamespace | undefined>>
MountOptions
Options for mountAgents. Type Parameters
Properties
authorize?
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 aoptionalauthorize?: (req,route) =>MountAuthorizeResult|Promise<MountAuthorizeResult>
Response to reject (nothing is forwarded and no actor is
woken), a MountHeadersInit to admit and stamp, or nothing to
admit as-is.
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?
The path prefix the mount owns. Every address isoptionalbase?:string
<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?
Opt in to cross-origin access, for a browser page served from another origin (anoptionalcors?:MountCorsOptions
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?
What to do with a request whose path is not underoptionalfallback?: (req,env) =>Response|Promise<Response>
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.
Returns
Response | Promise<Response>
Default Value
a 404 — the mount answers for the whole function.
health?
Answeroptionalhealth?:boolean
/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 toauthorize so the policy can decide with
the routing already done.
Properties
actorId
The actor id this name resolves to —readonlyactorId:string
encodeAgentName(name).
method?
The method name, for areadonlyoptionalmethod?:string
rpc request only.
mount
The mount key from the address — a key of the map you returned.readonlymount:string
name
The routing name from the address, decoded (readonlyname:string
"+15550100", not "%2B15550100").
transport
The transport the request’s shape selected.readonlytransport:MountTransport
MountTransport
MountTransport =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"websocket"|"sse"|"rpc"
…/rpc/<method>.
MountCorsOptions
Cross-origin passthrough for MountOptions.cors — opt-in, never a default. Properties credentials?Allow credentialed requests (cookies, TLS client certs). Requires a concreteoptionalcredentials?:boolean
origin, never "*".
Default Value
false
headers?
Request headers a cross-origin caller may send, beyond the CORS-safelisted ones. Default Valueoptionalheaders?: readonlystring[]
["authorization", "content-type", "last-event-id"] — what
the RPC and SSE addresses actually read.
maxAge?
How long (seconds) a browser may cache the preflight answer. Default ValueoptionalmaxAge?:number
origin
origin:The origins allowed, matched exactly.string| readonlystring[]
"*" allows any origin and is
refused in combination with credentials, which browsers reject anyway.
MountAuthorizeResult
MountAuthorizeResult =WhatResponse|MountHeadersInit|void
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 =The headersHeaders|Record<string,string> | [string,string][]
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: