Google Trends spiked this week for “rogue AI agents,” “agent executing unowned code,” and “agent contacting outside researchers” β and none of that is paranoia. It’s the search volume of teams who gave an autonomous agent real tool access, shipped it, and only then found out what “autonomous” means once nothing is watching.
The pattern repeats because the failure isn’t the model. It’s the backend around it. An agent gets a database client, a shell, or an email API “to move fast,” running under the same service account as everything else, with no record of which call did what and no way to stop it mid-task. The agent isn’t malicious β it’s doing exactly what an unconstrained LLM does: pursuing a goal with whatever tools it can reach.
You’ll learn:
- Why “the model won’t do that” isn’t a security boundary
- How to scope tool permissions per agent run, not per API key
- What an audit trail has to record to be useful mid-incident, not after
- Where to put human-in-the-loop checkpoints without stalling every run
- How to wire a real kill switch into a FastAPI + Celery + Redis agent backend
Scope permissions per run, not per API key
Most agent backends hand the LLM one service credential shared with the rest of the app. That means every tool call β read, write, delete, send β inherits the backend’s full blast radius. Scope it instead per agent run: mint short-lived, narrow credentials (a read-only DB role, a scoped S3 prefix, a capped-spend payment key) when the run starts, and expire them when it ends. A compromised or confused agent then can’t do more damage than that run was ever allowed to do.
Make the audit trail write before it acts
A trail that logs after a tool call runs is useless the moment that call is the one you needed to catch. Write an append-only row β run ID, tool name, argument hash, timestamp β to Postgres before the tool executes, then update it with the result. In Celery this is one task_prerun hook, not custom code in every task. It costs a few milliseconds per call and means a killed run still leaves a trail up to its last action, instead of stopping mid-story.
Put checkpoints on the actions that matter
Human-in-the-loop for every tool call kills throughput and trains people to click “approve” without reading. Gate only the tool calls that write, delete, send, or spend above a threshold. Have the task write a pending_approval row and block on a short Redis poll (or pub/sub) with a hard timeout that fails closed β no response means the action doesn’t happen, not that it proceeds anyway.
Build a kill switch that’s actually load-bearing
A kill switch that only works between tasks is a suggestion, not a control. Put a single Redis key β agent:{run_id}:killed β behind a GET at the top of every task and between tool calls inside long-running ones; a Redis read is cheap enough to afford on every iteration where a DB query isn’t. Expose one authenticated FastAPI endpoint, POST /agents/{run_id}/kill, that sets the key and calls celery.control.revoke(task_id, terminate=True) so queued follow-up tasks never start either. Test it the same way you’d test a payment rollback: on a schedule, not just when something’s already gone wrong.
None of this is theoretical AI safety β it’s the same production discipline you’d apply to any system that can take irreversible action on its own. The agent doesn’t need to be smarter to be safe. It needs a smaller blast radius, a trail that survives it, and a switch that actually cuts the power.
Related: one way to keep agent decisions small, typed, and checkable is a dedicated decision model like Jev from TypeSafe, with your own code owning every action.

