Task 5 · 8 tasks
Hooks and interrupts
Pause the agent for human approval before any destructive SQL reaches the database.
The “Rogue Agent” problem
During testing the agent cheerfully ran a DELETE. Alice wants a human in the loop: reads are fine, anything that changes data needs explicit approval.
Build it
Write an approval hook
Challenge
Finish
ApprovalHook: before anyquery_dbcall whose SQL contains DELETE, UPDATE, INSERT, DROP, ALTER or TRUNCATE, pause and ask a human. Anything butycancels the call.Work in
phase1/starter/t5_hooks_interrupts.py(look forTODO; an unfinished one prints[starter] TODO …) and run it withuv run bootcamp.py phase1 t5 --starter. The reference solution isphase1/hooks.py;uv run bootcamp.py phase1 t5runs it.Hint 1
Hooks are typed lifecycle callbacks. You want
BeforeToolCallEvent, registered from aHookProvider. The event caninterrupt()the agent and can cancel the tool. See Strands: hooks and interrupts.Hint 2
Read the tool name from
event.tool_use["name"]and the SQL fromevent.tool_use["input"]["query"]. The interrupt's return value is the human's answer.pseudocoderead onlydef approve(self, event): if <not query_db>: return if <destructive>: answer = event.interrupt("approval-required", reason={"query": query}) if answer != "y": event.cancel_tool = "..."Solution
Paste each block below into
phase1/starter/t5_hooks_interrupts.pyin place of the definition with the same name (the TODO), including any import line that comes with it. Nothing else in the file changes.phase1/starter/t5_hooks_interrupts.py (keyword version) · replace ApprovalHookclass ApprovalHook(HookProvider): def register_hooks(self, registry: HookRegistry, **kwargs) -> None: registry.add_callback(BeforeToolCallEvent, self.approve) def approve(self, event: BeforeToolCallEvent) -> None: if event.tool_use.get("name") != "query_db": return query = event.tool_use["input"].get("query", "") if any(word in query.upper() for word in DESTRUCTIVE_KEYWORDS): approval = event.interrupt("approval-required", reason={"query": query}) if str(approval).strip().lower() != "y": event.cancel_tool = "User denied permission to run this query"The reference goes further: instead of searching for keywords it asks
shared/sql_guard.py, an allowlist that only treats SELECT, EXPLAIN, read-only WITH queries and schema PRAGMAs such asPRAGMA table_info(employees)as reads, ignoring words inside string literals and comments:phase1/hooks.pyclass ApprovalHook(HookProvider): """Interrupts any destructive `query_db` call until a human answers 'y'.""" def register_hooks(self, registry: HookRegistry, **kwargs) -> None: """Run `approve` before every tool call.""" registry.add_callback(BeforeToolCallEvent, self.approve) def approve(self, event: BeforeToolCallEvent) -> None: """Ask for approval when a `query_db` call may write; cancel the call unless the answer is 'y'.""" if event.tool_use.get("name") != "query_db": return query = event.tool_use["input"].get("query", "") if sql_guard.is_write(query): approval = event.interrupt("approval-required", reason={"query": query}) if str(approval).strip().lower() != "y": event.cancel_tool = "User denied permission to run this query"Answer the interrupt
Challenge
In the same starter file, when the agent stops for an interrupt, ask the human and resume the agent with the answers (
make_responder).Hint 1
An interrupted run returns with
result.stop_reason == "interrupt"and a list ofresult.interrupts, each with anid,nameandreason.Hint 2
Resume by calling the agent again with a list of
{"interruptResponse": {"interruptId": ..., "response": ...}}. Loop: one answer may trigger another interrupt.Solution
Paste each block below into
phase1/starter/t5_hooks_interrupts.pyin place of the definition with the same name (the TODO), including any import line that comes with it. Nothing else in the file changes.phase1/starter/t5_hooks_interrupts.py · replace make_responder()def make_responder(agent: Agent): def respond(prompt: str): result = agent(prompt) while result.stop_reason == "interrupt": responses = [ {"interruptResponse": {"interruptId": i.id, "response": ask_approval(i.reason["query"])}} for i in result.interrupts if i.name == "approval-required" ] result = agent(responses) return result return respondTry it, safely
terminal · your starter fileuv run bootcamp.py phase1 t5 --starter "Delete employee with ID 5"terminal · reference solutionuv run bootcamp.py phase1 t5 "Delete employee with ID 5"One-shot runs auto-deny destructive queries; leave out the prompt to be asked
y/N.Task 5 adds
APPROVAL_NOTEto the system prompt: it tells the model a human approves every destructive query, so the model calls the tool and your hook does the asking. Without it, a cautious model asks for confirmation in chat and the hook never fires.The database is your own copy, so approving a delete only affects you. Delete
phase1/datastream_corp.dband re-runuv run bootcamp.py phase1 t0for a fresh one.Experiments
- Keyword matching is crude. Ask “Show the updated_at column of employee 5” with the keyword version:
updated_atcontainsUPDATE, so a harmless read asks for approval. Can you phrase a request that changes data but slips past the list? Compare with the reference (sql_guard). - Why does the deployed agent in Phase 2 block instead of asking? Who would answer?
- Keyword matching is crude. Ask “Show the updated_at column of employee 5” with the keyword version:
Check your work
Phase 1 has no automated test: you check it by running the task and looking for the result below.
“Delete employee with ID 5” shows a [tool] query_db: DELETE FROM employees ... line, then the approval question (one-shot: an [auto-deny] line) and [tool] query_db (cancelled); the agent reports that permission was denied. Answering y in chat runs it instead (no (cancelled)). With the reference guard, reads (SELECT, EXPLAIN, read-only WITH, and schema PRAGMAs such as PRAGMA table_info(employees)) run without a prompt; the keyword version may also stop harmless reads that mention a keyword, such as updated_at.
Failed? Run the same prompt on the reference (drop --starter). If the reference fails too, it's the model: rerun or try another MODEL_ID (uv run bootcamp.py llm models).
Under the hood
Hooks are typed lifecycle callbacks (before/after model call, before/after tool call, and more). event.interrupt() suspends the agent loop and surfaces a named interrupt to the caller; the response you send back becomes the return value inside the hook. Setting event.cancel_tool skips the tool and returns your message to the model as the tool result.