Mounting agents in your own server
Serve the persistent-agent HTTP surface from an application that already owns its routes, using createFabricAgentRouter.
Harness serves agents from its own runtimes, fh dev, the Node build artifact,
the Cloudflare worker. createFabricAgentRouter() covers the other direction. An
application that already owns an HTTP surface, and wants Harness agents on it,
under its own paths and behind its own middleware.
import { createFabricAgentRouter } from '@fabric-harness/sdk';
const router = createFabricAgentRouter(backend, { basePath: '/api' });
const handled = await router.handle(request);
if (handled) return handled;
// …your application keeps everything else.The router is runtime-neutral, Request, Response, URL, and plain objects,
so it runs on Node, Cloudflare, Deno, Bun, or any fetch-capable host, and mounts
in any framework that can hand it a Request.
Two entry points
| Method | Unmatched path | Use when |
|---|---|---|
handle(request) | resolves undefined | You compose routes and want to fall through. |
fetch(request) | resolves 404 | Your framework expects a total handler. |
What the router owns
Everything a client observes. Route parsing, method dispatch, request
validation, response shape, status codes, and the 202 admission receipt. The
streaming protocol reference documents that
wire contract, and the router is the same implementation the built-in runtimes
follow. Mounting agents yourself doesn't put you on a different protocol.
What the backend owns
A FabricAgentRouterBackend supplies durable submission, conversation reads, and
lifecycle:
import type { FabricAgentRouterBackend } from '@fabric-harness/sdk';
const backend: FabricAgentRouterBackend = {
async resolveAgent(name) {
return { found: registry.has(name), exposed: true };
},
async admit(input) {
// Capture the stream position BEFORE admitting.
const offset = await streams.nextOffset(input);
const submissionId = await runner.admit(input);
return { submissionId, offset };
},
async readConversation(address) { /* … */ },
async getSubmission(address) { /* … */ },
async abort(address) { /* … */ },
};loadInstance, deleteInstance, streamUpdates, and handleSchedules are
optional. A route whose backend method is absent returns 404, and updatesUrl appears in the receipt only when
streamUpdates exists. The router never points a client at a route the host
doesn't serve.
Two invariants worth stating
Capture the offset before admitting. The receipt's offset is the position
a client resumes from; reading it must return this delivery and the reply.
Returning "0" instead makes every client re-read the whole conversation on
every send.
An unexposed agent must answer like a missing one. resolveAgent returning
{ found: true, exposed: false } produces exactly the 404 that
{ found: false } does, so probing cannot enumerate private agents.
Idempotent admission capability
Keyed clients send Idempotency-Key. A backend must explicitly declare
supportsIdempotentAdmission: true before the router will forward keyed requests;
otherwise it returns 501. The backend receives
input.idempotencyKey and returns deduplicated with the original receipt offset and
timestamp. Use the SDK submission runner with durable persistence for atomic admission,
conflict rejection, and restart replay. Authentication stays with your host. Validate
the incoming request through your existing middleware, derive actor and tenant from
that authenticated context, then pass those identities to the runner before admitting
any work or returning a receipt to the client.
Malformed keys return 400. A backend FabricError with PERSISTENCE_CONFLICT becomes
409 with its public-safe error envelope. All other unexpected backend failures remain
generic 500 responses. See React admission
for key retention and cancellation semantics.
Errors
A backend method that throws an unexpected error becomes 500 with a generic body. Storage and
execution details, connection strings, stack traces, internal identifiers,
never reach the caller. Map your own domain failures to responses inside the
backend if you need callers to distinguish them.
Mount paths
basePath is stripped before matching, so the backend never learns where the
router lives. URLs in the receipt are relative to the router, which means the
host adds its configured prefix.
When not to use this
The Node and Cloudflare fh build targets already serve this surface. Use the
mountable router to embed Harness agents in an existing application server.
See also
examples/with-mounted-agent-router: a runnable version with an in-memory backend.- Streaming protocol: the wire contract this router implements.
- HTTP applications: adding your own routes to a Harness-owned server instead.