WHAT YOU WILL BUILD
One permitted read operation and two rejected requests with no side effects.
A model may propose a tool name and arguments. The application decides whether that proposal is allowed, validates it, performs the operation, and returns its result. A function schema helps describe the interface but does not replace those checks.
Before you start
Use a text editor and Node.js 22 or newer ↗. Check your version with node --version. Save the downloaded file in an empty folder, then open a terminal in that folder.
All data is included. No packages, account, or API key are required. This is a local read-only tool dispatcher. It uses fixed policy text and does not connect to an agent or external account.
Download the lab (.mjs)FOLLOW ALONG
Work through the example.
- 01
Inspect the permitted operation
lookup_policy accepts one category: returns or delivery. Read the dispatcher and identify the checks that happen before the tool function is called.
- 02
Run the rejected requests
The unknown tool and the extra admin argument both throw. The calls counter remains zero, demonstrating that rejected input never reaches this tool body.
- 03
Run the permitted lookup
The final request returns the example policy and increases calls to one. Keep a structured result in a real application so the controller can distinguish success from failure.
- 04
Add another negative case
Test a missing category, an array, and a category outside the allowlist. Verify the call count does not increase. Add authentication and per-resource authorization before adapting this pattern to user data.
THE COMPLETE LAB
Run it locally.
Run this command from the folder containing your downloaded file:
node agent-tool-validation.mjsView or copy the complete JavaScript
// SaveMyToken local lab. Run with Node.js 22 or newer.
import assert from "node:assert/strict";
let calls = 0;
const tools = new Map([["lookup_policy", ({ category }) => {
calls++;
return category === "returns" ? "Unused items: 30 days." : "Delivery: 3 to 5 working days.";
}]]);
function dispatch(name, args) {
if (!tools.has(name)) throw new Error("Unknown tool");
if (!args || typeof args !== "object" || Array.isArray(args)
|| Object.keys(args).length !== 1
|| !["returns", "delivery"].includes(args.category)) throw new Error("Invalid arguments");
return tools.get(name)(args);
}
assert.throws(() => dispatch("delete_data", {}), /Unknown tool/);
assert.throws(() => dispatch("lookup_policy", { category: "returns", admin: true }), /Invalid arguments/);
assert.equal(calls, 0);
console.log(dispatch("lookup_policy", { category: "returns" }));
console.log("Executed tool calls:", calls);
assert.equal(calls, 1);
Expected output for the unchanged example
Unused items: 30 days.
Executed tool calls: 1The assertions also check the baseline behavior. After changing an input, predict the result and update the relevant assertion.
NOW CHANGE ONE THING
Make the example your own.
Add a delivery lookup and a second permitted category assertion. Then attempt an unexpected path argument and verify that it is rejected without execution.
You are done when…
All invalid requests fail before the tool body, while the permitted request executes exactly once.
If something goes wrong
Do not dispatch a supplied name through eval, a shell command, or arbitrary object properties. A real write tool also needs action-specific authorization and protection against duplicate execution.
EXPLAIN WHAT YOU LEARNED
Interview practice
Try answering aloud before opening the reference answer. These are original learning questions, not a record of any employer's interviews.
01Who executes a function call proposed by a model?
For a client-side tool, the application receives the proposal, validates and authorizes it, executes the operation, and returns the result to the model or controller.
Watch for: Generated arguments are a proposal, not proof of permission.
02What belongs in a tool schema?
Give the tool a specific purpose, clear argument names, types, required fields, and constraints. Define how failure is returned so the caller can react appropriately.
Watch for: A broad tool named do_anything makes both selection and permission checks harder.
03Where should argument validation happen?
At the execution boundary before side effects, even if upstream generation used a schema. Validate allowed values and resource scope, not just JSON syntax.
Watch for: A valid ID can still belong to a different user.
04When can a failed call be retried?
Only when the failure is plausibly temporary and the operation is safe to repeat or protected against duplicate effects. Apply attempt and time limits.
Watch for: A timeout does not prove that a remote write never happened.
05What should a tool result contain?
Return the data required for the next decision, an explicit status, and useful bounded error information. Preserve evidence references when the result supports a factual claim.
Watch for: Dumping entire logs or credentials into the model context is unnecessary.
Sources and further reading
The explanation and local exercises were written for SaveMyToken. These references support the underlying concepts; the sample outputs describe only the supplied examples.
Anthropic: tool use ↗JSON Schema: object validation ↗