# Cloudflare Workers

**A Worker's `fetch()` leaves on Cloudflare's anycast IP, not your agent's — so the moment your agent lands on the edge, it loses the identity that made it an agent.**

`whisper-edge` gives it back: verify any Whisper identity with zero setup, and egress through a real routable `/128` with one secret — plus a drop-in MCP tool set for the Agents SDK.

## The problem: workerd has no raw egress identity

`workerd` (the Workers runtime) gives you `fetch()` and, behind the `cloudflare:sockets` flag, a raw TCP `connect()` — but no `bind()`, no source-address control, and no UDP, so WireGuard (Whisper's Tier‑1) is off the table entirely. Worse, `cloudflare:sockets`' `startTls()` pins the TLS SNI to the socket you already opened, so you cannot open a TCP connection to a SOCKS5/CONNECT proxy and then negotiate a *second*, nested TLS session to the real HTTPS origin inside it — the standard way a proxy client reaches an `https://` target (RFC 9110 §9.3.6, the `CONNECT` method) simply doesn't compose on workerd. That rules out Whisper's Tier‑1.5 SOCKS5/CONNECT egress for any `https://` target from a Worker.

**With stock tools** — proving the pain, no Whisper involved:

```ts
// A plain Worker, no Whisper. Every outbound request looks like Cloudflare, not your agent.
export default {
  async fetch() {
    const r = await fetch("https://api.ipify.org?format=json");
    return Response.json(await r.json());
    // → {"ip":"104.21.63.10"}  — a Cloudflare anycast address, unrelated to any agent
  },
};
```

There is no `dig`, `curl`, or `ip` binary inside `workerd` to fix this from within — the sandbox has no shell and no raw sockets worth using. The fix has to be an HTTPS-native protocol, which is exactly what Whisper's edge SDK and forward gateway are.

## Keyless tier: verify + RDAP, one `fetch()`

Before you route anything through an agent, you can check whether an address or hostname *is* a real Whisper agent identity — no API key, because this is public data: reverse DNS, a DNSSEC-validated forward-confirm, a DANE `TLSA` pin, and (per `whisper verify --trustless`) a transparency-log inclusion check.

Inside the Worker, `whisper-edge` runs the whole chain — reverse DNS, a DNSSEC-validated forward-confirm, the DANE `TLSA` pin, and a transparency-log check — in one `fetch()`, no resolver library and no crypto:

```ts
import { verify, verifyDetails, rdap } from "whisper-edge";

export default {
  async fetch(request: Request): Promise<Response> {
    const addr = new URL(request.url).searchParams.get("addr");
    if (!addr) return new Response("usage: ?addr=<agent /128 or FQDN>\n", { status: 400 });
    const ok = await verify(addr);                       // boolean
    return Response.json({ address: addr, is_whisper_agent: ok, details: ok ? await verifyDetails(addr) : null });
  },
};
```

`verifyDetails()` returns the individual verdicts (`dane_ok`, `dnssec_ok`, `ptr_ok`, …) so you can log *why* a check failed instead of a bare `false` — see [`sdk-edge`](/docs/sdk-edge) for the full type. The raw records behind those verdicts, and how to re-derive them from a terminal, are in [Verify an agent](/docs/verify).

## Egress: the forward gateway, not raw sockets

Since `cloudflare:sockets` can't nest TLS for a `CONNECT` tunnel, `whisper-edge`'s Cloudflare-egress path uses `transport: "forward"`: one HTTPS request, to `forward.whisper.online/forward`, carrying your API key and the target URL inside the (already-terminated) TLS session — no tunneling required. The gateway dials the real origin itself, sourced from your agent's own `/128` out of `2a04:2a01::/32` (announced by `AS219419`) — the same address binding every Whisper egress tier uses — and streams the response back over the same HTTPS connection it received the request on. One hop, standard HTTP semantics (RFC 9110), works identically on every runtime that only has `fetch()` — which is every edge runtime, not just Workers. Full protocol detail: [`forward-gateway`](/docs/forward-gateway).

**With stock tools** — the gateway is just HTTPS, so you can drive it with `curl` from anywhere to sanity-check a target before wiring it into a Worker:

```bash
curl -s -X POST https://forward.whisper.online/forward \
  -H "Authorization: Basic $(printf 'w:whisper_live_your_key' | base64)" \
  -H "X-Whisper-Target: https://api.ipify.org?format=json"
# → {"ip":"2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4"}
#   the reply also carries  X-Whisper-Egress-Source: 2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4
```

**With Whisper**, the same call from inside a Worker:

```ts
import { agentEgress } from "whisper-edge";

export default {
  async fetch(request: Request, env: { WHISPER_API_KEY: string }): Promise<Response> {
    const egress = await agentEgress(env.WHISPER_API_KEY, "acef2002a323d40d4", { transport: "forward" });
    try {
      const res = await egress.fetch("https://api.ipify.org?format=json");
      return Response.json(await res.json());
      // → {"ip":"2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4"} — the agent's own /128, not Cloudflare's edge
    } finally {
      egress.close();
    }
  },
};
```

The full two-tier reference Worker (`?addr=` for keyless verify, `?op=` for the control plane) is in [`whisper-examples/cloudflare/worker.ts`](https://github.com/whisper-sec/whisper-examples/tree/main/cloudflare):

```bash
npm i
wrangler secret put WHISPER_API_KEY   # optional — keyless endpoints work without it
wrangler deploy
```

## Agents SDK: `registerWhisperTools()`

If your Worker *is* an agent — a Cloudflare [Agents SDK](https://developers.cloudflare.com/agents/) `McpAgent` running as a Durable Object — `whisper-examples/cloudflare/agents-sdk` adds Whisper as four MCP tools with one function call, so the LLM driving the agent gets identity + egress as tool calls, never as code it has to write itself:

```js
import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { registerWhisperTools } from "./whisper-mcp.js";

export class WhisperMcp extends McpAgent {
  server = new McpServer({ name: "whisper", version: "1.0.0" });
  async init() {
    registerWhisperTools(this.server, this.env);   // + your own tools
  }
}
```

| tool | tier | what it does |
|---|---|---|
| `whisper_verify` | keyless | is this IPv6 `/128` or FQDN a real Whisper agent (PTR + DANE + DNSSEC + transparency)? |
| `whisper_rdap` | keyless | the RDAP registration record for an agent `/128` |
| `whisper_agents` | keyed | control plane: `register` / `identity` / `list` / `policy` / `logs` / `connect` / `revoke` |
| `whisper_egress_fetch` | keyed | fetch a URL sourced from the agent's routable `/128`, via `transport:"forward"` |

The keyed tools return a clear "set `WHISPER_API_KEY`" message rather than a stack trace when no key is configured — the keyless half is always fully functional so the tool set is useful even before you provision a key. `wrangler secret put WHISPER_API_KEY` unlocks the other two; the key lives in the Worker's secret store and the model never sees it. Point any MCP client (or another Agent via `this.mcp.connect(url)`) at `https://<your-worker>/mcp` (streamable HTTP) or `/sse`.

> Both example Workers — the plain two-tier fetch handler and the Agents-SDK MCP server — live under [`github.com/whisper-sec/whisper-examples/tree/main/cloudflare`](https://github.com/whisper-sec/whisper-examples/tree/main/cloudflare), proven end-to-end against a provisioned demo agent.

## Next

[`sdk-edge`](/docs/sdk-edge) — the full `whisper-edge` API (`verify`, `resolve`, `rdap`, `control`, `agentEgress`) · [`forward-gateway`](/docs/forward-gateway) — the HTTPS egress protocol behind `transport:"forward"` · [`mcp`](/docs/mcp) — the standalone Whisper MCP server for non-Workers hosts.
