> ## Documentation Index
> Fetch the complete documentation index at: https://developers.telnyx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# mountAgents()

> **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.

```ts theme={null}
import { mountAgents } from "@telnyx/edge-runtime/mount";

export default {
  fetch: mountAgents((env: Env) => ({ support: env.SUPPORT }), {
    authorize: async (req, { name }) => {
      const user = await verify(req);
      if (!user) return new Response("unauthorized", { status: 401 });
      return { "x-desk-user": user.id };
    },
  }),
};
```

`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:

| Request | Address | Serves |
| - | - | - |
| `Upgrade: websocket` | `<base>/<mount>/<name>` | the agent socket protocol |
| `GET` | `<base>/<mount>/<name>?subscribe=state` | Server-Sent Events |
| `POST` | `<base>/<mount>/<name>/rpc/<method>` | one RPC call |

`<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`](/docs/agent-sdk/internals/mount/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](/docs/agent-sdk/limits#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](/docs/agent-sdk/limits#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`](/docs/agent-sdk/internals/mount/mountAgents#mountoptions)).
* A path outside `base` → [`fallback`](/docs/agent-sdk/internals/mount/mountAgents#mountoptions), 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](/docs/agent-sdk/limits#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`](/docs/agent-sdk/internals/mount/mountAgents#mountoptions) 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**

| Type Parameter | Description |
| - | - |
| `E` | Your bindings type (the `env` the function is called with). |

**Parameters**

| Parameter | Type | Description |
| - | - | - |
| `map` | [`AgentMountMap`](/docs/agent-sdk/internals/mount/mountAgents#agentmountmap)\<`E`> | The agents this function serves, keyed by mount name; see [AgentMountMap](/docs/agent-sdk/internals/mount/mountAgents#agentmountmap). |
| `options` | [`MountOptions`](/docs/agent-sdk/internals/mount/mountAgents#mountoptions)\<`E`> | Base path, the `authorize` policy, health routes, fall-through, and cross-origin opt-in. |

**Returns**

The handler to expose as your function entry's `fetch`.

(`req`, `env`) => `Promise`\<`Response`>

## AgentMountMap

> **AgentMountMap**\<`E`> = (`env`) => `Readonly`\<`Record`\<`string`, [`ActorNamespace`](/docs/edge-compute/stateful-actors/api-reference/namespace) | `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:

```ts theme={null}
mountAgents((env) => ({ support: env.SUPPORT, billing: env.BILLING }));
```

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**

| Type Parameter | Description |
| - | - |
| `E` | Your bindings type (the `env` the function is called with). |

**Parameters**

| Parameter | Type |
| - | - |
| `env` | `E` |

**Returns**

`Readonly`\<`Record`\<`string`, [`ActorNamespace`](/docs/edge-compute/stateful-actors/api-reference/namespace) | `undefined`>>

## MountOptions

Options for [mountAgents](/docs/agent-sdk/internals/mount/mountAgents).

**Type Parameters**

| Type Parameter |
| - |
| `E` |

**Properties**

**authorize?**

> `optional` **authorize?**: (`req`, `route`) => [`MountAuthorizeResult`](/docs/agent-sdk/internals/mount/mountAgents#mountauthorizeresult) | `Promise`\<[`MountAuthorizeResult`](/docs/agent-sdk/internals/mount/mountAgents#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](/docs/agent-sdk/internals/mount/mountAgents#mountheadersinit) to admit and stamp, or nothing to
admit as-is.

```ts theme={null}
authorize: async (req, { mount, name }) => {
  const user = await verify(req.headers.get("authorization"));
  if (!user) return new Response("unauthorized", { status: 401 });
  return { "x-desk-user": user.id };
}
```

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**

| Parameter | Type | Description |
| - | - | - |
| `req` | `Request` | The inbound request, unmodified. |
| `route` | [`MountRoute`](/docs/agent-sdk/internals/mount/mountAgents#mountroute) | What the mount resolved: the mount key, the decoded name, the actor id, the transport, and (for RPC) the method. |

**Returns**

[`MountAuthorizeResult`](/docs/agent-sdk/internals/mount/mountAgents#mountauthorizeresult) | `Promise`\<[`MountAuthorizeResult`](/docs/agent-sdk/internals/mount/mountAgents#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`](/docs/agent-sdk/internals/mount/mountAgents#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.

```ts theme={null}
export default { fetch: mountAgents(map, { fallback: myRoutes }) };
```

**Parameters**

| Parameter | Type |
| - | - |
| `req` | `Request` |
| `env` | `E` |

**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`](/docs/agent-sdk/internals/mount/mountAgents#mountoptions) so the policy can decide with
the routing already done.

**Properties**

**actorId**

> `readonly` **actorId**: `string`

The actor id this name resolves to — [`encodeAgentName(name)`](/docs/agent-sdk/internals/mount/encodeAgentName).

***

**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`](/docs/agent-sdk/internals/mount/mountAgents#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](/docs/agent-sdk/internals/mount/mountAgents#mountoptions) — 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**

```ts theme={null}
unset — the browser's own default
```

***

**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`](/docs/agent-sdk/internals/mount/mountAgents#mountheadersinit) | `void`

What [`authorize`](/docs/agent-sdk/internals/mount/mountAgents#mountoptions) may answer:

* a `Response` — reject; no actor is woken and nothing is forwarded;
* a [MountHeadersInit](/docs/agent-sdk/internals/mount/mountAgents#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`](/docs/agent-sdk/internals/mount/mountAgents#mountoptions) 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:

```ts theme={null}
return { "x-desk-user": user.id };            // object literal
return [["x-desk-user", user.id]];            // entry pairs
return new Headers({ "x-desk-user": id });    // a built Headers
```
