FabricFabricHarness
Building Agents

HTTP applications

Mount authenticated Fetch routes and middleware around Fabric jobs, agents, channels, health, and identity.

Use the application surface when one deployed service needs Fabric routes plus product-specific HTTP endpoints. Routes and middleware live in .fabricharness/config.ts, so fh dev and Node-derived build artifacts run the same application.

Diagram flow: HTTP request leads to Public middleware; PUB leads to Health, rate limit, and authentication; SYS leads to Principal, tenant, and permission; RBAC leads to Authenticated middleware; APP leads to Route owner; CHOICE leads Application Fetch route handler; CHOICE leads Fabric Job, agent, channel, MCP, or admin; CUSTOM leads to HTTP response; CORE leads to RES; RES leads to APP; APP leads to PUB.
Text alternative and Mermaid source

Diagram flow: HTTP request leads to Public middleware; PUB leads to Health, rate limit, and authentication; SYS leads to Principal, tenant, and permission; RBAC leads to Authenticated middleware; APP leads to Route owner; CHOICE leads Application Fetch route handler; CHOICE leads Fabric Job, agent, channel, MCP, or admin; CUSTOM leads to HTTP response; CORE leads to RES; RES leads to APP; APP leads to PUB.

flowchart LR
  REQ[HTTP request] --> PUB[Public middleware]
  PUB --> SYS[Health, rate limit, and authentication]
  SYS --> RBAC[Principal, tenant, and permission]
  RBAC --> APP[Authenticated middleware]
  APP --> CHOICE{Route owner}
  CHOICE -->|Application| CUSTOM[Fetch route handler]
  CHOICE -->|Fabric| CORE[Job, agent, channel, MCP, or admin]
  CUSTOM --> RES[HTTP response]
  CORE --> RES
  RES --> APP
  APP --> PUB

  classDef public fill:#f3f4f6,stroke:#6b7280,color:#111827
  classDef identity fill:#fef3c7,stroke:#d97706,color:#422006
  classDef fabric fill:#dbeafe,stroke:#2563eb,color:#172554
  classDef custom fill:#dcfce7,stroke:#16a34a,color:#052e16
  class PUB public
  class SYS,RBAC identity
  class CORE fabric
  class CUSTOM custom

Define routes

.fabricharness/config.ts
import { defineApplication, type FabricHarnessConfig } from '@fabric-harness/node';

const application = defineApplication({
  routes: [
    {
      method: 'POST',
      path: '/api/customers/:customerId/message',
      permission: 'agent:invoke',
      async handler(request, context) {
        const body = await request.json() as {
          message: string;
          deliveryId?: string;
        };
        const receipt = await context.dispatch(
          {
            agent: 'support',
            id: context.params.customerId!,
            message: { kind: 'user', body: body.message },
          },
          body.deliveryId ? { idempotencyKey: body.deliveryId } : undefined,
        );
        return Response.json(receipt, { status: 202 });
      },
    },
    {
      method: 'POST',
      path: '/api/reports',
      permission: 'agent:invoke',
      async handler(request, context) {
        const input = await request.json();
        return Response.json(
          await context.invoke({ job: 'report', input }),
          { status: 202 },
        );
      },
    },
  ],
});

export default {
  application,
} satisfies FabricHarnessConfig;

Route paths support named :parameters and a trailing * wildcard. The handler uses the standard Web Request and returns a Web Response. Its context contains:

FieldPurpose
principal, actor, tenantIdServer-validated identity and tenant; request bodies cannot replace them.
paramsDecoded named path parameters.
dispatch()Durable admission to a persistent agent with optional idempotency key.
invoke()Ambient finite-job admission with tenant, actor, and parent correlation.
storesThe configured session, submission, conversation, and attachment stores.
signalAborts when the client aborts the request.
requestIdCorrelation id for application logs and response headers.

Custom routes inherit the normal authentication, tenant binding, rate limit, and authorization pipeline. Set permission to the closest built-in permission. It defaults to session:read for GET and other non-Fabric paths. Application hooks do not accept identity from payload fields.

Add middleware

Authenticated middleware receives the complete request context and wraps both custom and built-in routes. Calling next() once continues the chain. For a custom route, next() returns its Response, so middleware can add headers or replace it. For a streaming or built-in Node route, next() completes when the response finishes.

middleware: [async (request, context, next) => {
  const started = performance.now();
  const response = await next();
  await audit.record({
    requestId: context.requestId,
    principalId: context.principal.id,
    tenantId: context.tenantId,
    method: request.method,
    path: new URL(request.url).pathname,
    durationMs: performance.now() - started,
  });
  if (!response) return;
  const headers = new Headers(response.headers);
  headers.set('x-fabric-request-id', context.requestId);
  return new Response(response.body, { status: response.status, headers });
}],

publicMiddleware wraps health, rate limiting, and authentication. It intentionally receives no principal or stores. Use it for request timing, trusted ingress normalization, and global response headers, not authorization decisions.

publicMiddleware: [async (request, context, next) => {
  const response = await next();
  console.log(context.requestId, request.method, new URL(request.url).pathname);
  return response;
}],

Middleware and handlers share one cached request body. Reading request.clone().json() in middleware does not consume the bytes later used by job, channel, webhook-signature, or custom route handling. The configured maxBodyBytes limit still applies.

Route ownership

Fabric reserves /health, /ready, /jobs, /agents, /channels, /sessions, /runs, /mcp, /admin, /builds, /dispatch, and /openapi.json. A custom route beneath a reserved root or a duplicate method/path fails before the server listens. This prevents an application update from silently replacing approval, health, or agent behavior.

The complete runnable workspace is in examples/application-routes.