Unit 04.00: Giving an agent a tool it cannot misuse
A tool handed to an agent is a capability handed to a model. Designing it so that misuse is impossible is cheaper than instructing against misuse.
Read-only, self-validating, non-raising
Three properties make a tool safe to expose. It changes nothing. It validates its own input rather than trusting the caller. And it returns a string on failure instead of raising.
The code defines such a tool and calls it with four inputs, including a hostile one.
from crewai.tools import tool
ACCOUNTS = {"ACC-1187": {"plan": "individual", "opened": "2026-03-02"}}
@tool("lookup_account")
def lookup_account(account_id: str) -> str:
"""Return the record for one account id. Read-only."""
if not account_id.startswith("ACC-"):
return "REFUSED: account_id must start with ACC-"
record = ACCOUNTS.get(account_id)
if record is None:
return f"NOT FOUND: {account_id}"
return str(record)
for candidate in ["ACC-1187", "1187", "ACC-9999", "'; DROP TABLE accounts; --"]:
print(f"{candidate:30} -> {lookup_account.run(account_id=candidate)}")
# Three properties make this safe to hand to a model: it is read-only, it
# validates its own input, and every failure returns a string rather than
# raising. The agent cannot use it to do anything you did not intend.
Every input produces a string the agent can read and act on. The malformed id is refused with a reason, the unknown account returns NOT FOUND, and the injection attempt is simply an id that does not start with ACC-.
The non-raising property matters more than it looks. A tool that raises kills the task, and the agent gets a framework error rather than information - so its retry is uninformed, and it may well try the same thing again.
The mistake this prevents
The mistake is writing tools that trust their arguments because the arguments come from your own system. They come from a model, which chose them from text that may include content from outside your organisation. Validate at the tool boundary, every time.
Takeaway
Make tools read-only where possible, self-validating always, and non-raising without exception. A refusal string teaches the agent something; an exception teaches it nothing.
