Cloudflare Agents SDK Integration
Cloudflare Agents SDK Integration
This document covers the Cloudflare Agents SDK integration — typed agent registry, Zero Trust Authentication (ZTA) middleware, Durable Object routing, and the D1/Neon schema for agent session tracking.
Overview
The worker/agents/ module integrates the official Cloudflare Agents SDK into the Worker’s /agents/* routing. It replaces a custom Durable Object shim with a typed, extensible registry that drives routing, authentication, and permissions from a single source of truth.
sequenceDiagram
participant Browser/MCP as 🌐 Browser / MCP Client
participant Edge as ☁️ CF Edge (Access + WAF)
participant Worker as ⚙️ Worker (hono-app.ts)
participant AuthGate as 🔐 Agent Auth Gate
participant DO as 🤖 Durable Object (MCP_AGENT)
Browser/MCP->>Edge: GET /agents/mcp-agent/default (WebSocket upgrade)
Edge->>Worker: Request (CF-Access JWT verified at edge)
Worker->>Worker: CORS + secureHeaders applied (/agents/* pattern)
Worker->>AuthGate: runAgentAuthGate(request, env)
AuthGate->>AuthGate: authenticateRequestUnified() → IAuthContext
AuthGate->>AuthGate: requireAuth() → 401 if anonymous
AuthGate->>AuthGate: requireTier() → 403 if insufficient
AuthGate->>AuthGate: requireScope() → 403 if scope missing (API key)
AuthGate->>AuthGate: checkRateLimitTiered() → 429 if exceeded
AuthGate->>AuthGate: emitSecurityEvent() → Analytics Engine
AuthGate-->>Worker: { allowed: true, entry, authContext }
Worker->>DO: routeAgentRequest(request, env)
DO-->>Browser/MCP: WebSocket / SSE response
Architecture
Module layout
worker/agents/├── index.ts # Barrel export├── registry.ts # AGENT_REGISTRY — single source of truth├── registry.test.ts # 14 unit tests├── agent-auth.ts # ZTA auth middleware + handleAgentRequest└── agent-auth.test.ts # 12 unit testsRequest flow
hono-app.tsregisterscors()+secureHeaders()on/agents/*before mountingagentRouter.agentRouter(Hono sub-app) matchesGET /agents/:slug/:instanceId/*andPOST /agents/:slug/:instanceId/*.handleAgentRequestcallsrunAgentAuthGate— the full ZTA chain (see below).- On success, the request is forwarded to the Durable Object via
routeAgentRequest(lazy-loadedagentsSDK).
Agent Registry (worker/agents/registry.ts)
The registry is the only place agent metadata lives. All routing, auth enforcement, and route-permission entries are derived from AGENT_REGISTRY.
AgentRegistryEntry shape
interface AgentRegistryEntry { /** UPPER_SNAKE_CASE Durable Object binding key (matches wrangler.toml [[durable_objects]]) */ bindingKey: keyof Env; /** kebab-case URL slug — maps to /agents/<slug>/<instanceId> */ slug: string; /** Human-readable name for dashboards/logs */ displayName: string; /** One-line description */ description: string; /** Minimum tier required. UserTier.Admin = admin-only (default for all agents) */ requiredTier: UserTier; /** Additional scopes required for API-key callers (session users bypass scope checks) */ requiredScopes: string[]; /** Set to false to disable routing without removing from registry */ enabled: boolean; /** Primary transport: 'websocket' (default) or 'sse' */ transport: 'websocket' | 'sse';}Adding a new agent
- Add a
[[durable_objects]]binding inwrangler.toml:[[durable_objects.bindings]]name = "MY_NEW_AGENT"class_name = "MyNewAgent" - Add the DO class export to
worker/worker.ts. - Add an entry to
AGENT_REGISTRYinworker/agents/registry.ts:{bindingKey: 'MY_NEW_AGENT',slug: 'my-new-agent',displayName: 'My New Agent',description: 'Description of what this agent does',requiredTier: UserTier.Admin,requiredScopes: ['agents'],enabled: true,transport: 'websocket',} - Run
validateAgentRegistry()(executed automatically in tests) to catch anybindingKey↔slugdrift.
Registry validation
validateAgentRegistry() asserts that agentNameToBindingKey(entry.slug) === String(entry.bindingKey) for every entry and detects duplicate slugs. Errors are returned as a string[] (empty = valid).
import { validateAgentRegistry } from './registry.ts';
const errors = validateAgentRegistry();if (errors.length > 0) throw new Error(errors.join('\n'));Authentication Middleware (worker/agents/agent-auth.ts)
ZTA auth chain
Every /agents/* request passes through this chain before the Durable Object is invoked:
| Step | Function | Failure response |
|---|---|---|
| 1 | authenticateRequestUnified() | Auth provider error → 503/401 |
| 2 | requireAuth(authContext) | Anonymous → 401 |
| 3 | requireTier(authContext, entry.requiredTier) | Insufficient tier → 403 |
| 4 | requireScope(authContext, ...entry.requiredScopes) | Missing scope (API key only) → 403 |
| 5 | checkRateLimitTiered(env, ip, authContext) | Limit exceeded → 429 + Retry-After |
| 6 | emitSecurityEvent() | (telemetry — never blocks) |
Admin tier short-circuits step 5 (no KV I/O).
Security event telemetry
Every connection attempt — success or denial — emits a structured event to Cloudflare Analytics Engine via AnalyticsService.trackSecurityEvent(). Events include:
eventType:'auth_success'|'auth_failure'|'rate_limit'path,method,tier,userId,authMethod,reason
This feeds real-time ZTA dashboards and SIEM pipelines.
Exported functions
| Function | Description |
|---|---|
runAgentAuthGate(request, env) | Runs the full ZTA chain. Returns AgentAuthResult. |
applyAgentAuthChecks(authContext, entry, request, env) | Tier/scope/rate-limit checks on an already-resolved IAuthContext. Exported for unit testing. |
handleAgentRequest(request, env) | Entry point called by agentRouter. Returns null for non-agent paths. Catches unexpected errors and returns 500. |
Error responses
All error responses use JSON with Content-Type: application/json:
{ "success": false, "error": "<reason>" }| Status | Condition |
|---|---|
| 400 | Bad request (invalid pagination params, invalid UUID) |
| 401 | Anonymous / unauthenticated caller |
| 403 | Authenticated but insufficient tier or missing scope |
| 404 | Unknown slug or path does not match /agents/* |
| 405 | HTTP method not in {GET, POST} |
| 429 | Rate limit exceeded — includes Retry-After, X-RateLimit-* headers |
| 500 | Unexpected internal error (logged via console.error) |
| 503 | SDK import failed or DO binding unavailable |
Route Permission Registry
Agent routes are also spread into ROUTE_PERMISSION_REGISTRY (worker/utils/route-permissions.ts) as a defence-in-depth measure. Even if a request bypasses the agent router, the permission registry will deny it.
AuthScope.Agents
A new AuthScope.Agents = 'agents' scope was introduced in worker/types.ts. It is:
- Admin-only — seeded into
ADMIN_DB.scope_configsviaadmin-migrations/0002_agent_scope_seed.sql. - Required for API-key callers to the
mcp-agent(viarequiredScopes: ['agents']in the registry). - Bypassed for session callers (better-auth sessions —
requireScope()never checks session users).
Database Schema
D1 (migrations/0008_agent_sessions.sql)
CREATE TABLE IF NOT EXISTS agent_sessions ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES user(id), agent_slug TEXT NOT NULL, instance_id TEXT NOT NULL DEFAULT 'default', started_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, ended_at DATETIME, end_reason TEXT, ip_address TEXT, user_agent TEXT);CREATE INDEX idx_agent_sessions_user ON agent_sessions(user_id, started_at DESC);CREATE INDEX idx_agent_sessions_active ON agent_sessions(user_id) WHERE ended_at IS NULL;Neon/Prisma (prisma/schema.prisma)
The AgentSession model is defined with a foreign key to User and a composite index for active-session queries. AgentInvocation and AgentAuditLog models are also included.
Zod schemas (worker/schemas.ts)
| Schema | Description |
|---|---|
AgentSessionRowSchema | Validates a row from the agent_sessions table |
AgentInvocationRowSchema | Validates a row from the agent_invocations table |
AgentAuditLogRowSchema | Validates a row from the agent_audit_log table |
Admin API Endpoints
Admin-only endpoints for managing agent sessions and audit logs are implemented in worker/handlers/admin-agents.ts:
| Method | Path | Description |
|---|---|---|
GET | /admin/agents/sessions | Paginated list of AgentSession records |
GET | /admin/agents/sessions/:id | Single session with nested AgentInvocation records |
GET | /admin/agents/audit | Paginated AgentAuditLog records |
DELETE | /admin/agents/sessions/:id | Terminate an active session (idempotent — 409 if already ended) |
All endpoints require UserTier.Admin and role admin (enforced via checkRoutePermission). All Prisma queries are parameterized — no raw SQL string interpolation.
CORS and Security Headers
Because agentRouter handlers return a Response without calling Hono’s next(), the global CORS and secureHeaders() middleware never runs for /agents/* responses. These are applied explicitly in hono-app.ts before mounting the agent sub-app:
app.use( '/agents/*', cors({ origin: (origin, c) => matchOrigin(origin, c.env), credentials: true }),);app.use('/agents/*', secureHeaders());app.route('/', agentRouter);Testing
Unit tests
| File | Tests | Coverage |
|---|---|---|
worker/agents/registry.test.ts | 14 | Registry integrity, getAgentBySlug, validateAgentRegistry |
worker/agents/agent-auth.test.ts | 12 | Anonymous→401, free→403, admin→allowed, scope enforcement, rate limiting |
worker/handlers/admin-agents.test.ts | 13 | All admin handler paths (403, 400, 404, 409, 200, 503) |
Running agent tests
deno test worker/agents/ --no-checkdeno test worker/handlers/admin-agents.test.ts --no-checkRelated Documentation
- System Architecture — Worker architecture overview
- Zero Trust Architecture — ZTA principles
- Auth Chain Reference —
authenticateRequestUnifieddetails - Admin System — Admin endpoints overview
- Database Setup — D1 and Neon/Prisma setup
- Cloudflare Agents docs — Official SDK reference