JJoeven

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.

ThreatHardening
Extra JSON keysReject in parse_action
Unknown toolDo not eval names; registry miss only
Args of the wrong typeValidate before the dict lookup
Infinite parse junkmax_parse_retries
Weather before geocode with user-supplied coordsOptional: require geocode in-transcript, or allow coords but clamp ranges
Rate limitsReturn rate_limit and retry with a cap
Overlong answersTruncate 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: city is str, length 1–80, no digits-only strings if you want.
  • weather: lat in [-90, 90], lon in [-180, 180], both numbers.
  • finish: answer str, 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.

Live Pythonpython
Output
Run to execute this in your browser. Nothing is sent to a server.

Step-by-step hardening checklist

  1. Allowlist tools. Dispatch never looks up globals()[name].
  2. Validate types and ranges before environment access.
  3. Reject extra args. Prompt injection sometimes arrives as extra JSON fields.
  4. Cap parse retries and cap tool steps.
  5. Cap string lengths on the way in and out.
  6. Rate-limit simulated side-effect tools.
  7. Fail closed: if you cannot geocode, do not weather; if you cannot weather, do not invent °C.
  8. 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

What does fail-closed mean for the weather agent?