Projects/Weather Tool Agent/Part 5
Hardening the Weather Agent
Validate arguments, cap parse retries, reject unknown tools, simulate rate limits, and fail closed without calling weather on garbage coords.
A working demo is not a hardened agent. Hardening is everything you add after the happy path: validation, budgets, allowlists, and fail-closed behavior when the world is weird. This part takes the loop from parts 2–4 and makes it safe enough to sit behind a form on joeven.com (still with simulated weather).
Threats that already exist in a three-tool bot
The user is not the only adversary. The model is untrusted. It will eventually emit weather with a string lat, call finish twice, or name geocode_all_cities. Treat every action as hostile input to your process.
| Threat | Hardening |
|---|---|
| Extra JSON keys | Reject in parse_action |
| Unknown tool | Do not eval names; registry miss only |
| Args of the wrong type | Validate before the dict lookup |
| Infinite parse junk | max_parse_retries |
| Weather before geocode with user-supplied coords | Optional: require geocode in-transcript, or allow coords but clamp ranges |
| Rate limits | Return rate_limit and retry with a cap |
| Overlong answers | Truncate finish to N characters |
Validate at the boundary
The registry lambdas in part 2 used kw["city"] which raises KeyError. Hardened dispatch catches that and returns bad_args. Better: a per-tool schema:
geocode:cityis str, length 1–80, no digits-only strings if you want.weather:latin[-90, 90],lonin[-180, 180], both numbers.finish:answerstr, length 1–500.
Out-of-range coordinates should not look up the weather table. They are bad_args. Otherwise a model can probe your dict with a sweep of floats.
Rate limits as a fake 429
Give weather a token bucket: 3 calls per run. The fourth returns {"error": "rate_limit"}. The loop may retry twice, then must finish with cannot: rate limited. This trains you for vendor 429s without time.sleep (sleep is rude in a Try it box). A counter is enough.
Never destructive — even when there is nothing to destroy
This agent cannot drop a database. Still practice the habit: no tool runs unless it is in the allowlist. If you later add http_get(url), you will already have the gate. Put the allowlist next to TOOLS and check name in TOOLS before splatting args.
Run to execute this in your browser. Nothing is sent to a server.
Step-by-step hardening checklist
- Allowlist tools. Dispatch never looks up
globals()[name]. - Validate types and ranges before environment access.
- Reject extra args. Prompt injection sometimes arrives as extra JSON fields.
- Cap parse retries and cap tool steps.
- Cap string lengths on the way in and out.
- Rate-limit simulated side-effect tools.
- Fail closed: if you cannot geocode, do not weather; if you cannot weather, do not invent °C.
- Keep traces. Part 4's tests should still pass after hardening. If they do not, you tightened a contract — update tests deliberately.
Production notes (for later)
When you replace dicts with HTTP: timeouts, a single retried idempotent GET, and never log API keys. The hardening above maps 1:1 to httpx except the catalog. Your validate function stays. That is why we wrote it.
Watch out:Do not "fix" unknown cities by picking the nearest name in CITIES. That is a silent wrong-city incident.Exercise
Add a max_weather_calls of 1 and a policy that panics and calls weather three times. Assert the run ends with cannot: after a rate_limit observation. Then add a unit test that dispatch("weather", {"lat": "hot", "lon": 2}) returns bad_args and does not increment WEATHER_CALLS.
You now have a beginner portfolio piece: a sequenced tool agent, JSON protocol, tests, and fail-closed tools. Next project: the same loop, but the tool is search, the protocol is ReAct, and the failure mode is cannot answer.
Check your understanding