A hardening pass on the support agent

Lesson 5 of 5 in Tool Scoping and Least Privilege: Making the Dangerous Thing Impossible.

Time to do the work on something concrete. The foundations domain walked one complete run of the Fernway Outfitters support agent: five tools, one runtime gate, a ten-call budget.

lookup_order(order_id?, email?)        → order record
carrier_track(tracking_id)             → shipment status from a carrier API
issue_refund(order_id, amount, reason) → refund; gated: amount <= $100
escalate_to_human(summary)             → opens a case for a human agent
close_ticket(order_id, resolution)     → closes the support ticket

That agent was well designed for a foundations lesson and is under-scoped for production. Note where the trifecta legs already sit: it reads private customer data (leg one), and carrier_track returns third-party text straight into the context (leg two, via poisoned observations). Leg three is the question the hardening pass answers.

Hardening pass — the same agent, before and after
SurfaceAs shippedHardenedWhat it buys

lookup_order

Lookup by order_id or email, against the whole order table with a service credential.

Session opens bound to one ticket; the runtime injects that ticket’s customer id. Free lookup by arbitrary email is removed.

Kills bulk enumeration: a hijacked session can read one customer, not the customer list.

carrier_track

Calls the carrier API directly from the agent process; the response text lands in context verbatim.

Fetched through an egress proxy allowlisting the carrier host; the response is parsed into a typed record (status, dates, detail) and wrapped as untrusted data with provenance.

Shrinks the injection channel and stops the tool from doubling as an outbound HTTP client.

issue_refund

Handler checks amount <= 100. Otherwise free: any order id, any amount within the cap, any reason string.

amount <= min(100, order.total), order_id must be one this session looked up, reason an enum of three values, max one refund per session, per-hour tenant rate limit.

Closes the “$100 refund on an $89.50 order” gap and converts mass-refund into one bounded, alerted event.

close_ticket

Any order_id, free-text resolution.

Only the session’s own ticket; resolution an enum. Reversible, so no gate — logging is enough.

Removes a cheap way to hide activity by closing other people’s tickets.

escalate_to_human

Free-text summary shown to the human as the case description.

Unchanged in reach, but the case view renders the trace and the runtime-read order record beside the model’s summary, marked as model-authored.

The safe exit stays frictionless while the human stops judging attacker-influenced prose alone.

Identity and credentials

One long-lived service account for CRM, payments, and carrier lookups.

Three credentials, one per tool, each read-only where possible, minted per session with a short TTL, carrying the tenant and ticket scope.

Ends the confused-deputy setup and bounds any credential leak to minutes and one customer.

Egress

Whatever the process can reach — no network policy stated.

Default-deny outbound; allowlist the CRM, payments, and carrier hosts. No email/webhook tool exists. Agent replies are rendered with external image and link hosts stripped.

Removes trifecta leg three: the destination for stolen data disappears from the architecture.

Budgets and stop

10 tool calls, 40k-token ceiling, 120-second wall clock, then escalate.

Same, plus a per-tenant refund-spend ceiling per hour, an alert on blocked-egress attempts, and a kill switch that drains in-flight sessions.

Bounds cost and gives a human a lever during an incident rather than after it.

Read the “what it buys” column again and notice how little of it is about detecting attacks. The hardened agent is not better at spotting injection than the original — it is worse at being useful to an attacker. That is the shape of a real containment argument.

Be equally honest about what remains. A refund of exactly the order total still fires without a human; a single customer’s data is still readable and could still be paraphrased to that customer in a reply; the carrier’s text can still steer the model’s wording. Residual risk is not a failure of the pass — it is the output of the pass, and it belongs written down next to the detection you rely on to catch it.

Interactive sorting exercise: Sort each mitigation by what it actually does. “Removes a trifecta leg” means the capability is gone for this session. “Shrinks blast radius” means the bad thing can still happen, but smaller. “Detection only” means it tells you afterwards.

Tool: Tool Permission Lab — Run your own hardening pass in the Permission Lab: dial each tool’s scope, watch which attack chains survive, and see what the agent can no longer do for you either.

Interactive flashcard deck.

Interactive checkpoint quiz (2 questions) — open this page in a browser to take it.