Building AI Agents from Scratch
Human Approval and Guardrails
Require confirmation before the agent sends, deletes, purchases, or modifies data.
Read-only tools can run. Anything that sends, pays, deletes, or overwrites needs a human in the loop. The model is not that human.
Mark dangerous tools
DANGEROUS = {"write_file", "send_email", "delete_file"}
def write_file(path: str, text: str, approved: bool = False) -> dict:
if not approved:
return {
"ok": False,
"error": "approval required",
"preview": {"path": path, "bytes": len(text)},
}
target = (ROOT / path).resolve()
if ROOT not in target.parents and target != ROOT:
return {"ok": False, "error": "path escapes the project"}
target.write_text(text, encoding="utf-8")
return {"ok": True, "path": path}Pause the loop
When a dangerous tool returns approval required, stop and print the preview. Read a yes/no from the terminal (or a UI). Only then re-invoke with approved=True from your code — do not let the model set that flag itself. Strip approved from the schema the model sees, or ignore it if present.
if name in DANGEROUS:
preview = run_tool(name, arguments_json)
print(preview)
if input("Approve? [y/N] ").strip().lower() != "y":
return json.dumps({"ok": False, "error": "human rejected"})
args = json.loads(arguments_json or "{}")
args["approved"] = True
return json.dumps(REGISTRY[name](**args))Check it
- Ask the agent to write
notes.md. You should see a preview and a prompt. Answern— the file must not exist. - Answer
yon a second try — the file exists with the previewed bytes. - Confirm
list_dirnever asks.