security

Hardening a public agent endpoint

The Copilot on this site is an unauthenticated, tool-using agent on an endpoint that spends money per request. Every control that holds it up is linked to the file that implements it and the test that would catch its removal.

6 controls with a linked file and a test
3 rate-limit tiers, all fail-closed
0 model calls on the public MCP route

The Copilot on this site is a public, unauthenticated, tool-using agent on an endpoint that spends money per request — the same shape as the agent surfaces I build on platforms, at a size where I own every line of it.

No login, no human in the loop, no one watching a dashboard. Every control has to hold against an anonymous caller and against a model that can be argued with, and it has to hold overnight.

I worked the trust boundary a class at a time: prompt injection through retrieved and page text, tool-call abuse turning the navigation tool into an open redirect, unbounded request bodies on an endpoint whose Content-Length is client-controlled, and volumetric cost abuse. Each class got a control that fails closed and a test that fails the build.

Six controls, each linked below to its implementation and its test. The limiter denies rather than serves when KV is unreachable, the only side-effecting tool resolves against a closed allowlist server-side before anything is streamed, and the public MCP route answers from a bundled index while making no model calls at all.

Architecture
Anonymous caller no auth, any origin
Origin + body cap cancelled mid-read
under cap
Rate limiter 3 tiers, fail-closed
under quota
Agent loop 3 steps, 1200 tokens
Tools resolved server-side
Streamed reply

// an unreachable counter denies the request, it does not wave it through

// retrieved and page text enters the prompt as data, never as instructions

Tradeoffs
  • Failing closed means a KV outage returns 503 instead of serving the assistant unmetered — availability traded for a bill I can predict.
  • The CSP is still Report-Only. Enforcing it would block Astro's per-build inline hydration scripts until they are hashed, and a policy that breaks the page is worse than one being observed.
  • The rate-limit counters are read-modify-write on KV, so under concurrency they under-count — and a contended write is tolerated rather than denied, since the shared daily key collides under ordinary multi-user traffic. Exact enforcement needs a Durable Object, which this traffic does not justify.
Prompt Injection Rate Limiting Threat Modeling Cloudflare Workers MCP

Forward this case study:

Want something like this built, or want to talk about the work?

Back to selected work