A Gateway Pattern for Safe AI Agent Actions
In this article
- The one rule everything else follows from
- The agent never holds the credential
- The stop is not one switch
- The guard is read at the moment of the write
- The agent cannot lift its own restraint
- Limits live in the layer that enforces them
- The schema is part of the enforcement
- A claim is not an outcome
- Microsoft reaches the same conclusion
- What this maps to outside a house

Read-only agents are comparatively easy to contain. The design changes when the agent can call an API that modifies state, and most designs at that point quietly assume the model will behave.
I have spent the last two months building a household assistant that controls real devices, which turns out to be an unusually unforgiving test environment. When an enterprise agent writes to the wrong record you find out at the next reconciliation. When this one gets it wrong, something physical happens in front of me within a second. That shortens the feedback loop considerably.
What follows is the pattern that came out of it. Nothing here is specific to home automation, and all of it applies to an agent that files tickets, changes configuration, or writes to a production API.
The one rule everything else follows from
The model is an untrusted client that proposes actions. Something else decides whether they happen.
That sounds obvious written down. It is routinely violated in practice, because the convenient way to build an agent is to give it a credential and a set of tools, and then write a careful system prompt about what it should and should not do. The prompt is where the safety lives, and the prompt is the one component an adversary, a confused user, or the model itself can influence.
Everything below is a consequence of moving enforcement out of the model’s reach.
The agent never holds the credential
Actions go through a separate service with its own process, its own identity on the target system, its own key for the agent, its own audit log and its own network policy. In my case the target system’s account is non-admin and restricted to local access, which bounds what the credential can do even if everything above it is compromised.
The agent holds a key to the gateway. It does not hold a key to the house.
This sounds like plumbing and it is the difference between an incident and a catastrophe. If the agent’s context is poisoned, if a tool call is malformed, if the model is simply wrong, the blast radius is whatever the gateway permits rather than whatever the target system permits.
The stop is not one switch
A single kill switch is a single point of failure in both directions: it can fail to stop, and it can stop more than intended. What I ended up with is four independent stops, documented in the gateway’s own docstring because that is where someone will look at three in the morning:
| Layer | How you pull it | What it leaves working |
|---|---|---|
| Flag | Set the gateway’s enabled flag to false, or turn the tools off in the agent | Everything else |
| Scale | Scale the deployment to zero | Reading the house |
| Policy | Delete its network policy | Reading the house |
| Credential | Revoke its token on the target system | Reading the house |
The column on the right is the design goal. Each layer stops the agent changing anything while leaving it able to observe, which means the degraded mode is a system that still answers questions and refuses to act. An agent that goes completely dark when you pull the handle is an agent nobody will pull the handle on.
They fail through different control paths on purpose. The flag is a config change. Scaling to zero is an orchestration change. The network policy is a cluster change. Revoking the token is a change on the target system itself. They are independent paths rather than independent administrative trust boundaries: a cluster administrator could reach both the scale and the network policy, which is worth knowing when you decide who holds which.
The guard is read at the moment of the write
The gateway reads a switch before every single write, from the target system, at the last possible moment. Not the state the request started with. Never a cached value. Never a default.
# THE SWITCH IS READ AGAIN AT THE LAST MOMENT, from Home Assistant, for
# every write: not the state the request started with, and never a
# default. Only an explicit "on" lets a command through; unknown,
# unavailable, missing or unreadable all refuse.
The refusal set is the part worth copying. Only an explicit “on” passes. Unknown refuses. Unavailable refuses. A switch that was configured and has since gone missing refuses. Unreachable refuses.
That last case is the one people get wrong. If the gateway cannot reach the system that holds the guard, the tempting behaviour is to proceed, because the alternative is an outage every time the network hiccups. Failing closed means accepting those refusals as the price of the guarantee, and the guarantee is worth more than the convenience.
The agent cannot lift its own restraint
A kill switch the agent can reach is not a kill switch. If the guard is an object on the target system, and the agent has any ability to write to that system, then sooner or later a scene, a service call, a borrowed token or a creative tool invocation turns it back on.
The target system records which account caused every state change. So the gateway checks not only whether the switch is on, but who turned it on:
# ROBIK CANNOT SWITCH HIMSELF BACK ON. An "on" whose last change was made
# by one of Robik's own users is not a person's "on": it counts as off
# until a person turns it on.
An “on” whose most recent change came from one of the agent’s own accounts counts as off. Only a human turning it on makes it count. There is a companion rule that no write may ever name the guard entity, whatever service it claims to call, enforced by inspecting the serialised payload before it is sent.
The code comment that follows that block is the part I am most pleased with, because it concedes the limit:
It is an operational control, not a security boundary.
It is a strong operational control. It catches the token used outside this code, and a scene or service call that sets the helper on. It is not a security boundary, because anyone who can write arbitrary state on the target system can also impersonate a person. The actual boundary is scaling the gateway to zero and revoking its token. Writing that distinction down in the code stops a future reader, including me, from over-trusting the mechanism.
Limits live in the layer that enforces them
The agent’s tools are told it may change at most three rooms in a turn. The gateway enforces the same limit independently:
# Rooms changed per conversation turn, enforced HERE as well as in
# Robik's tools: the limit must not live only in the layer it limits.
That last clause generalises to almost every agent design I have reviewed. A constraint expressed only in the prompt or only in the tool description is a request. The model will usually honour it, which is worse than if it never did, because the failures are rare enough to be surprising and you will have stopped checking.
There is a second limit of a different kind: the same device may be changed at most four times in two minutes. Each of those changes can be individually valid and the sequence still be wrong, because an oscillation is a property of the series rather than of any member of it. “Stop” is never rate limited, because the one command you never want to throttle is the one that halts something.
The schema is part of the enforcement
The request models bound what can even be expressed. Brightness is an integer from 1 to 100. A shutter position is an integer from 0 to 100. Every string field has a maximum length. A room name cannot be eighty characters of injected instructions.
This costs nothing and removes an entire class of problem before any logic runs. The model cannot ask for something out of range, because the request will not deserialise.
A claim is not an outcome
The gateway reads the device back after the write and before the agent is allowed to say it did anything. Every request returns executed, denied or failed, with an audit identifier, and the audit line records the actor the agent supplied alongside what the system showed afterwards.
This is worth more care than it tends to get. A language model asked to perform an action and report on it will report success, because reporting success is what the shape of the conversation calls for. Unless the claim is checked against the world, you have built a system that is confidently wrong at exactly the moments you most need it right. I have written separately about the same failure in the read path, where a contradiction in assembled facts produced a confident and false answer.
The general form: never let the agent be the source of truth about whether its own action succeeded.
Microsoft reaches the same conclusion
Worth noting, because it suggests this is not a private opinion about a household toy.
Microsoft Foundry’s toolbox lets you mark a tool as requiring approval, with require_approval set to always on the tool’s metadata. The documentation is then unusually direct about what that setting does and does not do:
The toolbox MCP endpoint doesn’t block
tools/callwhenrequire_approvalisalways. Your agent runtime must enforce the setting before every invocation. A system-prompt instruction alone doesn’t enforce approval.
That is the same argument as the section above, stated by the platform vendor about their own product. The flag expresses an intention. The runtime, which is the component outside the model, is what turns it into a constraint. Microsoft goes further and advises using require_approval: never unless your runtime can really pause the pending call, collect a decision, and resume or reject that exact call, which is a fair test of whether an approval mechanism is real.
What this maps to outside a house
Change the nouns and the pattern holds for the agents organisations are actually building this year.
An agent that files and transitions tickets should authenticate, with a workload or managed identity rather than a static key, to a service that files tickets, and should never hold a service account on the tracker itself. An agent that changes cloud configuration should propose to something holding the deployment credential, with its own policy about what may be changed and a read-back before it claims the change landed. An agent that writes to a customer record should be bounded by a schema that cannot express a cross-tenant identifier.
And in every case: a stop that leaves reading alive, a guard read at the moment of the write rather than at the start of the plan, limits in the enforcing layer as well as the prompt, and no path by which the agent can widen its own permissions.
None of this requires the model to be well behaved. That is the entire point. The model is a component that proposes, and like any untrusted client, the useful question is not whether it will misbehave but what happens when it does.
Planning an Azure OpenAI upgrade?
The fixed-scope Azure AI Platform Architecture Review covers the target resource model, private networking, access boundaries, model governance, gateway controls, and operational evidence before the change reaches production.