Topic 7.4
Validating Arguments and Running Tools Safely
In one line
Tool arguments come from a model that users and documents can steer, so they are untrusted input. Parse them in a try block, check them against the schema, allow-list tool names and paths, and never pass them to eval, a shell or a string-built query.
Think of it like this
A bank clerk receiving a note that says 'pay the bearer 500'. The clerk checks who is asking, that the account exists, that 500 is within the limit, and that the note is not a forgery, even if the note is on official paper. The note (the model's tool call) looks authoritative. The checks (your validation) are what keep the bank safe.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Untrusted input
- Data your program did not create and cannot vouch for. Model output and user text both count.
- Validation
- Checking that a value has the right type, range and shape before using it.
- Allow-list
- A list of the only things permitted. Anything not on it is refused.
- Path traversal
- A trick that uses
..in a path to reach files outside the allowed folder. - eval
- A function that runs text as code. Never use it on model or user text.
- Prompt injection
- Hostile text that tries to make a model follow the attacker's instructions (Phase 10).
Step by step
01Step 1: expect broken input
The first example feeds eight argument strings through check: valid, cut off, missing a field, wrong type, out of range, outside the enum, an extra field, and valid JSON of the wrong kind. Every one must produce a clear message, not a crash.
02Step 2: parse, then validate
json.loads inside try. Then isinstance(args, dict). Then the validator walks the schema. It collects all problems instead of stopping at the first, so the model can fix everything in one retry.
03Step 3: boundaries for values that pick resources
A schema says path is a string. It cannot say 'inside the reports folder'. That is code. safe_path splits the path, applies . and .. by hand and refuses to climb above the base. In real code use your platform's path APIs (resolve, then check the result starts with the base), and also prefer ids over paths.
04Step 4: refuse politely
A refusal is a normal tool result: a short message the model can read. The second example prints refused: ... lines. Topic 7.5 sends them back to the model.
05Step 5: nothing the model writes is ever executed
Arguments are data. They select among things you wrote. They are never code, never a command line, never a SQL string. If a feature seems to need that, redesign the tool with narrower arguments.
Try it yourself
- 1
Add your own bad case
Add a ninth case with
daysset to2.5. Predict the message: which type name does the validator give a non-whole number, and which rule fires first? - 2
Try a sneaky path
In the second example add
"reports/./../../x". Walk throughsafe_pathby hand, writing thepartslist after each segment, then run and compare. - 3
Add a tool safely
Add a second tool
list_filesthat takes no arguments and returns"files in /data/reports". Which two places change (the function and the registry)? Notice that the dispatcher itself stays the same.
Code & diagrams
Hand-written to show the checks. In real projects use Pydantic, Zod or a Jackson plus Bean Validation setup (Topic 4.5). The course's Json class stands in for Jackson in Java.
Expected output
case 1: ok
case 2: rejected: arguments are not valid JSON
case 3: rejected: missing required argument: city
case 4: rejected: days: expected integer, got string
case 5: rejected: days: 30 is above the maximum 7
case 6: rejected: unit: kelvin is not one of celsius, fahrenheit
case 7: rejected: unknown argument: debug
case 8: rejected: arguments must be a JSON objectThe registry is the allow-list. safe_path refuses anything that would leave the base folder.
Expected output
read_file q1.txt: ok: contents of /data/reports/q1.txt
read_file 2026/../q2.txt: ok: contents of /data/reports/q2.txt
read_file ../secrets/keys.txt: refused: path leaves /data/reports
read_file a/../../x: refused: path leaves /data/reports
read_file /etc/passwd: refused: absolute paths are not allowed
read_file 42: refused: path must be a string
delete_all -: refused: unknown tool delete_allBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Concatenate the path
In the second example, replace the whole body of safe_path with return BASE + "/" + relative and run the Python version.
Myth vs fact
Myth
The model is on my side, so its arguments are safe.
Fact
It writes whatever the conversation, including hostile documents, pushes it towards. Validate as you would for a public web form.
Myth
If the schema says string, a path is safe.
Fact
A schema checks the type, not the meaning. Boundaries, ownership and limits are code you write.
Myth
A system prompt saying 'never delete files' protects me.
Fact
A prompt is a request, not a lock. The only reliable control is that no delete tool exists, or the tool refuses.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Validation errors are data: log the tool name, which rule failed and how often. A spike in
unknown argumentafter a prompt change means the model's behaviour moved (Phase 9 evals catch this). - ▸
Reject unknown fields rather than ignoring them. Ignoring hides the model's confusion and can hide an attack that probes your interface.
- ▸
Authority comes from the session, not the arguments. Inject
user_idserver-side into every tool run and remove it from the schema the model sees.
Remember this
- 1
Why this exists. The model writes the arguments, and the model can be wrong (a text where a number was asked for), careless (an unknown field) or steered by someone (prompt injection: hostile text in a user message or a fetched web page that tells the model what to do, Phase 10). In every case, your program receives text and must decide whether it is safe to act on. Skipping this step turns the model into a remote control for your server.
- 2
Step 1: parse inside a try block.
argumentsis a JSON string and may be cut off (finish_reason: length, Topic 2.3) or malformed. Catch the parse error and treat it as a normal failure to report (Topic 7.5), not a crash. Also check the parsed value is a JSON object:["Pune"]is valid JSON and still wrong. - 3
Step 2: validate against the schema. Check required fields are present, every value has the right type, enums contain the value, numbers are in range, strings are not too long, and unknown fields are rejected. The first example is a small validator that returns a list of problems: you will need this list in Topic 7.5 to tell the model what to fix. In production, use a library (Pydantic, Jackson with Bean Validation, Zod, or a JSON Schema validator) as in Topic 4.5.
- 4
Step 3: allow-list. Tool names come from a registry, a table you wrote. If the name is not in it, refuse. Never build a function name from text and call it dynamically. Values that select resources (file paths, table names, URLs, user ids) need their own allow-list or boundary check: the second example resolves a path by hand and refuses anything that would leave the base folder (path traversal:
../segments climbing out of the allowed directory). - 5
Step 4: never execute text. Do not pass argument strings to
eval,exec, a shell, or into SQL or HTML by string concatenation. A 'calculator' tool should parse numbers and operators with a small parser, not calleval. Run database queries with parameters (placeholders), file access with resolved paths and shell commands with an argument list and a fixed program, if at all. Treat tool results as untrusted too: a web page returned by a tool can contain instructions (Phase 10). - 6
Step 5: check authority, not just shape. A valid
user_idmay not belong to the current user. The model should never choose whose data it acts on: take the user from your authenticated session and ignore (or reject) any attempt to pass it as an argument. Finally, limit damage with timeouts and size limits on tool runs (Topic 7.9), because a valid call can still be expensive.
Explain it without notes
Why must tool arguments be treated as untrusted input even though your own model produced them?
List the checks a tool runner should make between receiving a tool call and running the function.
Why is eval on tool arguments dangerous and what do you do instead for a calculator tool?
Practice
Write parse_args(text) that returns (args, error): args is the dict and error is None on success; otherwise args is None and error is "not valid JSON" or "not an object".
Write reject_unknown(args, allowed) that returns a list of messages "unknown argument: <key>" for each key not in allowed, in the order they appear in args.
Trade-offs
- ↔
Strict validation rejects some calls that would have worked and prevents the ones that would have hurt.
- ↔
Collecting all problems helps the model fix a call in one retry and takes slightly more code than stopping at the first.
- ↔
Hand-written checks teach the idea; a validation library saves time and covers more of the schema language.
Done when you can
Done when you parse
argumentsinside a try block and require a JSON object.Done when every tool has a schema and unknown fields are rejected.
Done when tool names come only from a registry.
Done when values that choose resources (paths, ids) are bounded by code, not only by a schema.
Done when no model-written text reaches
eval, a shell or a string-built query.