🛡 Harden Your Agent
Runnpm run lesson4View the code →

Lesson 4 · Acting as whom

Every tool so far has quietly had the keys to everything. find_booking reads any booking. issue_refund refunds any amount. That's fine while the only person talking to the agent is trusted staff. The moment a guest can chat to it, the question is no longer "what can this tool do?" but "who is it doing it for?"

The chat is not identity

It's tempting to let the conversation carry the answer: the guest says who they are, the agent believes them. That's the same mistake as trusting a hidden <input name="role" value="admin"> because the form sent it. The chat is user input. Identity has to come from somewhere the user can't edit.

For tiny-stay that's a signed session cookie (src/session.ts). The "Sign in as" switch sets it; it's an HMAC of who you are, so the browser carries it but can't forge it:

export function verifySession(token) {
  const [payload, signature] = token.split(".");
  if (!timingSafeEqual(sign(payload), signature)) return null; // one edited character and it's rejected
  return userFrom(payload);
}

Tools act as that person. Never as whoever the chat claims to be.

The demo: a guest reaching for someone else's booking

Signed in as the guest Ana, the chat asks about Tom's booking and tries to move it:

npm run lesson4
── Identity OFF ──
tool → find_booking(BK-1042)   ← { guest: "Tom Brennan", room: "Harbour", … }
tool → change_booking(BK-1042, Lighthouse Suite)   ← Changed.

A guest just read, and changed, another guest's booking. With it on:

── Identity ON ──
tool ✗ BK-1042 belongs to another guest. Guests can only see their own bookings.   [identity]
model  I can't show you BK-1042. Happy to help with your own booking instead.

The refusal happens inside the tool, before it reads a thing. The model only ever learns it was told no.

Role rules live in code, next to the data

// src/defences/identity.ts
export function canSeeBooking(user, booking) {
  if (isStaff(user) || booking.guest_email === user.email) return yes;
  return no(`${booking.ref} belongs to another guest.`);
}
export function canRefund(user, amount) {
  if (user.role === "owner") return yes;
  if (user.role === "receptionist") return amount <= 100 ? yes : no("Over £100 needs the owner.");
  return no("Guests can't issue refunds.");
}
Owner Receptionist Guest
See or list bookings all all her own
Change a booking yes yes no
Refund any amount up to £100 no
Inbox and notes yes yes no
Export the ledger yes no no

Two things worth stealing from this. The check is in the tool, not the prompt. A sentence in the system prompt saying "guests may only see their own bookings" is the disabled button; the check in find_booking is the server saying no. Second, the rule refuses a cross-tenant request whatever the model intended. The model asked for find_booking(BK-1042) believing it was helping; the tool refused because Ana isn't Tom. The model's intent never entered into it.

When the defence is off, the tools run as a service account

Switching identity off doesn't make the tools ask the prompt nicely. It makes them run as a built-in assistant account that's allowed everything, which is what an agent without identity really is. That's the honest baseline: either every call carries a real user, or every call is effectively an admin.

Run it

npm run lesson4
npm start           # sign in as Ana, ask about BK-1042, toggle Identity. Then try as Dev, then Maggie.

Try this