🛡 Harden Your Agent
Runnpm run harden-checkView the code →

Epilogue · The hardening test suite

Six defences. Each one proved by a terminal demo where you watch it off, then on. Demos are good for learning and useless for CI: nobody reads a transcript on every pull request. So the same fixtures run as a test suite.

npm run harden-check
Hardening check · 29 cases

PASS  boundaries   untrusted text is wrapped with its source
PASS  intent       a tool outside the scope is refused
PASS  intent       normalisation strips comments, hidden elements and invisible characters
PASS  render       the safe renderer blocks remote images, shows link hosts, drops raw HTML
PASS  render       fetch_page re-checks after a redirect into a private address
PASS  identity     a guest can't see another guest's booking
PASS  once         the same refund twice returns the first, not a second
PASS  once         the circuit breaker opens after three errors in a row
PASS  memory       a refund that rests on memory is cross-checked against the booking
PASS  memory       a changed tool definition is detected; the hash ignores key order
…
By defence
  ✓ boundaries   3/3   ✓ intent 4/4   ✓ render 6/6   ✓ identity 5/5   ✓ once 6/6   ✓ memory 5/5

all passed  (29/29)

It tests the code, not the model

This is the whole reason the suite can exist. Every case drives a code-level defence and asserts a yes/no outcome: the scope check refuses an out-of-scope tool, fetch_page refuses a redirect to a private address, canSeeBooking refuses a cross-tenant lookup, the idempotency key collapses a double refund. None of it calls the model.

That's deliberate, and it's the thread running through all six lessons. You cannot unit-test "the model resists a cleverly worded email", because the answer is "usually", and usually isn't a test. You can unit-test the code that holds whatever the model does. So that's where the defences live, and that's what this suite guards.

A nice property falls out: a test that proves "on" works should also prove "off" fails. The suite includes cases that run a defence with the switch off and assert the bad thing happens. If "off" ever stops failing, the test was checking nothing.

check("identity", "the same lookup with identity off is not refused", async () => {
  const leak = await runTool(findBooking, { ref: "BK-1042" }, ctxFor("guest", store, allOff()));
  return expect(leak.ok, "with identity off the tool should run — else this test proves nothing");
});

Drop it into CI

It exits non-zero on any failure, so it's one line in a workflow:

- run: npm run typecheck
- run: npm run harden-check

Add a case every time a real request surprises you. A production incident becomes case thirty, and it never comes back quietly.

The one-page checklist

Before an agent that can act on the world goes near a customer:

The one idea, if you keep one

A prompt is advice to the model. A boundary is code the model runs inside. Lesson 1's label helped and couldn't be trusted; everything after it was code, and could be. When you build your own agent, write the advice, by all means. Then, for everything that reads untrusted text or changes the world, write the code too, and a test that fails without it.

That's the series. If it helped, tell me what you hardened: @georgehanu.

Where to go next