AI Should Propose. Systems Should Verify.
An AI agent can produce the correct action and still have no authority to execute it.
That distinction becomes critical when agents move beyond generating text and start modifying code, calling APIs, changing infrastructure, triggering workflows, or coordinating other agents.
The question is no longer only:
Can the model determine what should happen?
It is also:
What happens between the action the model proposes and the action the system actually executes?
A useful starting point is:
AI proposes. Systems verify. Infrastructure executes.
The Problem With Direct Execution
Consider an AI coding agent working inside a production environment.
The agent receives a task:
Fix the authentication bug and deploy the change.
It analyzes the repository, modifies the code, runs tests, and produces a deployment command.
The code may be correct.
The tests may pass.
The deployment command may be valid.
None of those facts automatically mean the agent should be allowed to deploy to production.
This is the difference between correctness and authority.
A model can correctly determine what should happen while still being unauthorized to perform that action.
If the same component controls reasoning, authorization, and execution, the trust boundary becomes extremely large:
AI decides what to do → AI decides whether it may do it → AI executes it
A safer architecture separates those responsibilities.
Put a Boundary Between Reasoning and Execution
Instead of allowing the model to directly invoke sensitive infrastructure, treat its output as a proposed action.
User Intent
↓
AI Agent
↓
Proposed Action
↓
System Validation
↓
Policy Check
↓
Execution
↓
Post-Execution Verification
The model produces the request.
The surrounding system evaluates it.
The infrastructure executes it only after the required controls have been satisfied.
For example:
{
"action": "deploy",
"environment": "production",
"service": "auth-service",
"version": "2026.10.04"
}
The control layer can independently check whether the request is structurally valid, whether the target exists, whether the requested operation is permitted, and whether additional controls are required.
The model does not need to make those decisions itself.
Passing Tests Does Not Mean Permission
Imagine the coding agent changes a payment service.
The tests pass.
The security checks pass.
The build succeeds.
The artifact is signed.
The model now proposes:
Deploy to production.
The previous results establish that the software may be technically acceptable.
They do not establish that the current actor is authorized to deploy it.
That gives us an important separation:
Validation → Is this action acceptable as an operation?
Authorization → Is this actor allowed to perform it?
Execution → Perform the approved operation.
These questions belong to different layers.
That separation also makes the architecture easier to audit and change without changing the model's reasoning process.
Powerful Agents Do Not Need Unrestricted Authority
Separating reasoning from execution does not require making the agent weak.
An engineering agent can still have broad capabilities:
- inspect repositories
- create branches
- modify files
- run tests
- inspect logs
- generate deployment plans
- propose infrastructure changes
The system can simply apply stronger controls to higher-impact operations such as:
- production deployment
- credential rotation
- database migration
- destructive infrastructure changes
- financial transactions
The agent remains capable of reasoning about complex operations.
Its capabilities just do not automatically become unrestricted authority.
Think in Actions, Not Just Tools
Another architectural mistake is defining control only around tool access.
For example:
Agent → deploy_tool
Agent → payment_api
Agent → database_api
Tool access is often too coarse.
The more useful question is:
What action is the agent attempting to perform, against which resource, under which constraints?
A control layer can therefore reason about something closer to:
Actor → Action → Resource → Environment → Scope → Context → Policy
Now the system can distinguish between actions that happen to use the same underlying tool.
A deployment API, for example, might be capable of deploying to both staging and production. Access to the API alone does not tell us whether a particular production deployment should be allowed.
The meaningful unit of control is the action and its context, not simply the existence of a tool.
When Agents Start Delegating
The boundary becomes even more important when one agent delegates work to another.
Consider:
User → Orchestrator Agent → Procurement Agent → Payment Service
The procurement agent may determine that a purchase is necessary.
But the fact that the orchestrator requested the purchase does not automatically give the procurement agent unlimited spending authority.
Now the system needs to understand more than the requested action:
Initiator → Delegator → Acting Agent → Action → Resource → Scope
This introduces a deeper authorization problem: how authority is delegated between actors.
That problem deserves its own layer rather than being squeezed into the basic execution model.
A Better Mental Model
Instead of:
User → AI → Everything
think about:
User → Intent → AI Agent → Proposed Action → System Controls → Infrastructure
The AI sits inside the system.
It does not become the system.
That gives the architecture a stable control boundary around a component whose behavior is probabilistic.
The model can change.
The model can be upgraded.
The model can be replaced.
The surrounding execution controls can remain.
The Principle
Giving an AI agent more tools does not automatically mean giving it more authority.
The goal is not to prevent AI from acting.
The goal is to separate the ability to propose an action from the authority to execute it.
AI proposes. Systems verify. Infrastructure executes.
And once agents start acting on behalf of other agents, the next question becomes unavoidable:
Who actually has the authority to perform the action?
Top comments (7)
Agreed on the separation — validation, authorization, and execution being distinct layers is exactly right. The part I'd push further from practice: even when the system verifies, the verifier itself needs receipts. I attack-tested a destructive-command gate for my own coding agent (here) and the failure mode wasn't authorization — it was verification theater: checks that pass their own tests and fold under four bypasses. Stating "systems should verify" is the easy half. The hard half is that verification without tamper-evidence is just another plausible narrative.
Yeah, I think “verification theater” is the more interesting failure mode here. A gate can enforce the policy correctly and still be a weak control if there’s no trustworthy evidence connecting the decision to the action that actually ran.
A Kubernetes example makes this pretty concrete. Imagine an agent proposes deleting a production deployment. The admission layer verifies the namespace, resource, identity, and policy, then records
allowed. But if the agent can mutate the request after that check, or invoke another execution path that isn’t covered by the same policy, the verification was technically correct and still useless.I’d want the receipt to bind the exact request to the verification context: resource UID, namespace, requested operation, principal, policy version, and ideally a digest of the approved payload. The executor can then reject anything whose digest doesn’t match the verified receipt.
That gives you a much stronger boundary: the system isn't just saying “this passed verification.” It can prove that the exact operation being executed is the one that was verified. That’s where tamper-evidence starts becoming part of the control rather than just an audit feature.
Your last line is the whole design principle: tamper-evidence as part of the control, not an audit feature — that's the difference between a receipt and a log entry. Your digest-binding sketch is right, and two residuals close it from practice. First, the receipt needs a writer identity, not just contents: UID, principal, policy version bound to a digest only proves non-mutation if the executor verifies the admission webhook's signature rather than the receipt's presence — a control plane where the webhook and the executor share one trust domain is the shared-dependency problem one layer down. Second, your own first example is the one the digest can't reach: kubectl exec, port-forward, a second controller — none pass the admission webhook, so the receipt never sees them. That's coverage, not binding: the API server's audit stream already records every path, so the invariant to enforce is that every write event reconciles against a gate receipt — an exec with no matching receipt is a finding, not noise. Binding proves the verified thing ran; reconciliation proves nothing else did.
Yeah, I think that distinction between binding and coverage is the important one. A receipt can prove that this operation was authorized without proving that the system didn't reach the same state through another path.
There’s another edge case I’d add though: the receipt can be perfectly bound and still become stale between verification and execution.
Say the gate approves:
Another controller updates the deployment before the executor acts, and the object is now at
resourceVersion=18422. The receipt still verifies cryptographically, but it no longer describes the state the executor is operating against.That makes me think the receipt should bind not only the requested operation, but the relevant state version as well. The executor can then enforce something closer to:
Otherwise we can end up with a slightly uncomfortable situation where the cryptography is perfect, the coverage is complete, and the control still authorizes an operation against state that the verifier never actually evaluated.
And I really like the “exec with no matching receipt is a finding” invariant. That changes reconciliation from an audit report into an actual control loop.
That's TOCTOU — and Kubernetes already ships the closure. Your resourceVersion IS the precondition mechanism: issue the DELETE with resourceVersion=18421 as a precondition, and when another controller moves the object to 18422 the executor's call fails loudly with a conflict instead of succeeding against the wrong state.
The receipt's job then isn't to stop the world from moving — it's to carry the precondition, so time-of-use fails whenever it diverges from time-of-check, and the divergence itself is a sealed event: receipt expired-by-state-change, new gate pass, new receipt, never silent reuse. Worth noticing the symmetry with your fleet-epoch proposal yesterday: both are the same principle at different granularities — consistency is a relation against externally-versioned state (etcd's mod-revision, TSA-anchored heads), never a field the actor asserts.
And your last sentence is the adoption that matters: reconciliation as a control loop instead of an audit report is the difference between findings that file themselves and findings that refuse — a receipt that can't stop the call it doesn't describe is documentation, not a gate.
There’s a failure mode here that’s easy to miss: what happens when reconciliation itself loses visibility into part of the execution path?
Take a Kubernetes cluster where API audit events are reconciled against gate receipts, but a privileged node-level change modifies a workload outside the API server. The reconciliation loop can report a clean result because it only sees the events available to it, not because every mutation was covered. Kubernetes documents this exact class of blind spot with direct kubelet and etcd access.
That makes me think the invariant needs a second part: reconciliation must account for the completeness of its own evidence stream, not just unmatched events. If audit delivery stalls, a source goes silent, or a collector falls behind, the system should mark the control state as unknown and fail closed for operations that depend on that evidence. Otherwise, “no unmatched writes” can quietly become “no writes we managed to observe.”