Agentstypical

Parse, Validate, Authorize, Execute

Four gates, each rejecting a class of problem the others structurally cannot, in an order that is not arbitrary. The reason the ordering matters is the same reason `[[phase-ordering]]` matters in a compiler: a later phase depends on facts an earlier one established, and running them out of order either weakens the check or leaks information.

The question

In what order should I check a model-generated plan, and what does each check actually establish?

SourceLexingTokensParsingASTSemanticsTypedIROptimizeCodegenMachine codeLinkExecute
What the program is here

One plan in four successive forms: raw text; a step tree with spans; a typed tree whose tool names resolve and whose arguments check against declared parameters; and an authorized tree carrying, per step, a recorded decision naming the principal, the action and the resource. Each form is the previous one plus a property, and the property is exactly what the next gate needs in order to decide anything precise.

What this phase may assume or do

Each gate is entitled to assume everything the gates before it established, and nothing that comes after. The type checker may assume the plan parsed and that every step names a registered tool, so it never has to handle a missing tool. The policy engine may assume resolved names and typed arguments, which is what lets it decide on the resource an argument names rather than on a string. The executor may assume all three. A gate that reaches forward — an executor that re-validates, a policy engine that parses — is a sign the ordering is not actually being enforced, and the cost of that is that some path reaches execution without passing everything.

Key points

  • Four gates — parse, resolve-and-type, authorize, execute — each rejecting a class the others structurally cannot express.
  • The ordering is forced by dependency: typing needs resolved names, precise policy needs typed arguments, and execution needs all three because its effects cannot be withdrawn.
  • Record which gate rejected a plan. Four counters with four owners is the highest-value instrumentation in the system and costs one enum field.
  • Authorize the whole plan before any step runs, or a plan whose last step is denied will still have performed its first three.
  • Name-only authorization before typing is the right order when the policy is that the caller may not learn the tool exists.
  • No gate catches a well-formed, permitted plan that is simply wrong. That is what human review and dry runs are for.

Four gates, four classes of problem

The four gates are not four steps of one check. Each one rejects a class of problem the others cannot express. A parser cannot notice that a tool does not exist, because existence is not a property of the input's shape. A type checker cannot notice that a caller lacks permission, because permission is not a property of the arguments. A policy engine cannot notice that the plan is a bad plan, because that is not a property of anything it can see.

Keeping them separate is what makes the diagnostics actionable, and it is the same argument [[semantic-analysis]] makes about why a parser should not check meaning. It is also what makes ownership clear: a spike in parse failures belongs to whoever owns the decoder and the prompt; a spike in resolution failures belongs to whoever owns the tool descriptions; a spike in policy denials is either a scoping bug or the system working exactly as designed, and those two look identical unless the phase is recorded.

The gates, and what each one establishestypical
  1. Generationyou write it
    A byte string proposed by a model.
    A proposal.
  2. Parsebuild time
    A step tree with spans back into the text.
    Shape: which steps, in what order, with which arguments attached to which step.
    The surrounding prose, and any intent expressed only there.
  3. Resolvebuild time
    The tree with every step bound to a registered tool and every argument name bound to a declared parameter.
    That the names refer to something. An unknown tool is now a diagnostic, not a runtime crash.
  4. Typebuild time
    The tree with a checked type on every argument value.
    That each value is the kind the tool declared, within any declared range or enum.
  5. Authorizebuild time
    The typed tree plus one recorded decision per step: principal, action, resource, allow or deny, and the rule that decided.
    That this caller may do this, to this resource, now.
  6. Executerun time
    A running loop: a step index, a result store, and effects in the world.
    Effects, some irreversible.
    The ability to reject anything.

Read it asThe adds column is the contract each gate hands to the next. The loses column has only two entries and they bracket everything: prose meaning is lost at the parser, and the ability to refuse is lost at execution. Everything worth checking has to happen between those two rows.

Why this order

Type checking must follow resolution, because a type check needs the tool's declared parameters, and it cannot have them until the tool name is bound. That is a hard dependency, identical to the one [[name-resolution]] creates before [[type-checking]] in a compiler frontend.

Authorization following typing is a softer dependency, and it is where the interesting argument is. A policy engine can decide on a tool name alone — "this principal may not call refund" — but it cannot decide anything about *this* refund without knowing that order_id is a well-formed id belonging to an order this caller owns and that amount_cents is within a limit. Precise, resource-level policy needs typed arguments, so it runs after the type checker.

Execution last is not a preference. It is the one gate whose effects cannot be withdrawn by a later rejection, and everything about the ordering follows from that asymmetry.

Swapping two gates, and what it costs
Before
parse -> authorize(name only) -> resolve -> type -> execute
After
parse -> resolve -> type -> authorize(principal, action, resource) -> execute
Legal only when

Moving authorization after typing preserves the accepted set only if the policy is expressible over resolved names and typed arguments, and if the type checker is total — it must reject cleanly on any input rather than crashing, since it now runs on plans that authorization would have refused. Under those conditions the same plans are accepted and the denials are strictly more precise, because the decision can name the resource rather than only the verb.

Illegal when

The policy is "this principal may not learn that this tool exists". Type-checking first leaks the tool's existence and its whole parameter shape through the diagnostic, so the correct order is a name-only authorization gate first, returning a deliberately uninformative refusal. That is the same tension between a helpful message and a message that reveals too much that [[diagnostic-quality]] describes, and here it has a security consequence rather than an ergonomic one.

What each gate rejects, and who owns the fix

Recording which gate rejected a plan turns an undifferentiated failure rate into four signals with four owners. This is the single highest-value piece of instrumentation in an agent system, and it costs one enum field.

The last row is the one that has no gate. A plan that passes all four and still does the wrong thing is a semantic failure, and no static phase catches it. The available responses are a dry run, a human review of the rendered plan, or a narrower tool — all of which are cheaper to reach for once you know the other four gates are not the problem.

Rejection classestypical
GateRejectsOwner of the fix
ParseOutput that is not a plan at all: truncation, prose wrapped around it, a malformed valueDecoder settings, prompt, output format
ResolveA step naming a tool that does not exist, or an argument the tool never declaredTool descriptions and the registry
TypeAn argument of the wrong kind, out of range, or not a declared enum memberSchemas, and the descriptions the model is shown
AuthorizeA permitted-looking action this principal may not perform on this resourcePolicy, or the agent's granted scope
(none)A well-formed, permitted plan that is simply the wrong planHuman review, dry run, or a narrower tool

Authorize the plan, not the call

The most common structural mistake is to check permissions inside each tool. It looks equivalent and it is not: a plan whose fourth step will be denied still runs its first three, and there is no rule that the destructive step comes last. By the time the denial arrives, the deletion has happened.

Authorizing the whole plan before any step runs converts that into a single refusal with nothing applied. It also lets policy see the shape of the sequence, which is where a class of real problems lives — read a private document, then send an email — that no per-call check can see, because each call is individually permitted.

The cost is real and should be stated. Whole-plan authorization requires deciding on arguments that later steps have not computed yet: step 3 acts on $2, and $2 does not exist. The honest resolutions are to authorize conservatively on what is statically known and re-check the resolved resource at execution, or to restrict the plan language so that resource identity is always statically visible. [[permissions-authn-authz]] in Agentic AI covers the policy side; the compiler-shaped observation is that this is exactly a static-versus-dynamic tradeoff, and it has the shape [[gradual-typing]] describes: a static check that cannot cover every case, plus a runtime check at the boundary where the static one gave up.

Gates before effects
step treeresolvedtypedhigh risklow riskapprovedModel outputParseResolve (registry)Type check (schemas)Authorize (policy)Human approvalStructured diagnosticsExecuteEffects in the world
UserLLMAgentToolDataDecisionHumanGuardrail

How it works

The steps, in the order the compiler takes them.

  • Parse the model output against the plan grammar; on failure return spans and expected-token diagnostics without touching any tool.
  • Resolve each step name against the tool registry and each argument name against the tool's declared parameters; unresolved names are diagnostics.
  • Type-check every argument value against its declared parameter type, including ranges and enum membership.
  • Walk the typed plan and ask the policy engine for a decision per step, passing principal, action and the resource the arguments name; record every decision including the allows.
  • If any step is denied, refuse the whole plan and return the denials without executing anything.
  • Route plans above a risk threshold to human approval, rendering the plan as text rather than as raw model output.
  • Execute steps in order, re-checking authorization for any resource that was only resolvable at run time, and stop the plan on the first failure rather than continuing.

How it breaks

What the engineer observes when it goes wrong — not what goes wrong internally.

  • A plan is half-applied: three steps ran, the fourth was denied, and the system is now in a state no one designed — the classic symptom of authorizing per call instead of per plan.
  • Every rejection surfaces as one generic "invalid request", so a two-week regression caused by a tool description change is indistinguishable from a model regression.
  • A policy denial is fed back to the model as a repair prompt, and the model dutifully rewrites the same forbidden action in a different shape, consuming the retry budget without ever being able to succeed.
  • The refusal message names a tool the caller was not supposed to know exists, and the information leak is invisible because it looks like a helpful error.
  • Authorization runs on the plan only, a later step's resource is computed at run time from $2, and the executed action touches a resource policy never saw.
  • The executor re-validates everything defensively, so the gates are no longer load-bearing, and a path that skips them entirely is added later without anyone noticing the guarantee is gone.

When it helps

  • Plans with irreversible steps, where whole-plan refusal is the difference between nothing happening and half of it happening.
  • Multi-tenant systems, where the resource an argument names determines whether the action is permitted at all.
  • Any system that needs an audit trail: the recorded per-step decisions are the trail, and recording the allows matters as much as recording the denials.
  • Diagnosing a regression, because the gate distribution moves before the success rate does.

When it hurts

  • Single-step, read-only interactions, where four gates over one permitted call is ceremony with nothing to protect.
  • Plans whose later resources genuinely cannot be known statically, where whole-plan authorization is either too conservative to be useful or too optimistic to be a guarantee.
  • Very high call rates with a human-approval gate in the path, where the approval queue becomes the system's actual throughput limit and approvals degrade into rubber-stamping.

What it costs

Every one of these is paid by something.

  • Whole-plan authorization buys all-or-nothing refusal and costs precision: steps acting on results not yet computed must be authorized conservatively, and conservative denials refuse plans that would have been fine.
  • Separate gates buy precise diagnostics and ownership, and cost implementation surface — four passes, four sets of tests, and four places the tool registry's shape leaks into.
  • Informative rejections buy fast repair and cost information: an error precise enough for the model to fix is precise enough to enumerate your tools and their parameters to whoever is driving the model.
  • A human-approval gate buys catching the failures no static phase can, and costs latency and, at volume, the quality of the approvals themselves.

What else you could do

What a different compiler or language does instead, and when that is better.

  • Per-call authorization inside each tool: simpler and correct when every step is independently reversible, and unable to prevent a partially applied plan.
  • Capability-based execution — hand the executor a token scoped to exactly the resources the plan named, and let the downstream service enforce it. Moves the check to the service that owns the resource, at the cost of needing per-plan credential minting.
  • Sandboxed execution with no policy engine, relying on the sandbox having no dangerous capability at all. The strongest containment story and the weakest explanation of what the agent was trying to do.
  • Optimistic execution with compensation: run, and undo on denial. Viable only where every effect has a genuine inverse, which is rarer than it looks — an email has none.

See it for yourself

The flag, dump or tool that shows you this directly.

  • Add a rejected_at field with the four gate values plus none, and chart the distribution. It will tell you about regressions before your success metric does.
  • Log every policy decision including the allows, with principal, action, resource and the rule that decided. Denial-only logs cannot answer "what was this agent permitted to do last Tuesday".
  • Run the plan through a dry-run executor that resolves and types every step and performs no effects; diff the resolved resources against what the real run touched.
  • Deliberately submit a plan whose last step is denied and confirm nothing ran. If steps one to three executed, authorization is per call and not per plan, whatever the design document says.
  • For the compiler-side version of the same ordering argument, /compilers/pipeline stops at the first failing stage and names it, which is why AtlasLang does not report type errors in a file that did not parse.

Plausible wrong readings

Stated the way a confident engineer states them.

  • "Validation and authorization are the same check." Validation asks whether the request is well-formed. Authorization asks whether this caller may make it. A perfectly valid request from the wrong principal is the entire point of the second gate.
  • "If each tool checks its own permissions we are covered." You are covered against calling a forbidden tool, and not against a plan that performs three permitted destructive steps before reaching the forbidden one.
  • "The order does not matter as long as all the checks run." It matters twice: precise policy needs typed arguments, and an informative type error leaks a tool the caller was not allowed to know about.
  • "Recording allows is noise; log the denials." The allows are the audit trail. A question about what an agent was permitted to do cannot be answered from denials.

Misconceptions

The claim, and what is actually true.

Authorization can be folded into schema validation.
A schema constrains values. Authorization is a decision about a principal, an action and a resource, and none of those three are properties of the argument object's shape.
Checking twice is safer than checking once in the right place.
Defensive re-checking in the executor makes the earlier gates non-load-bearing, and a path that bypasses them can then be added without any test failing. Redundancy in a validation chain hides which link is actually holding.
If the plan is denied, nothing happened.
Only if authorization ran over the whole plan before execution. With per-call checks, everything before the denied step has already happened.

Go deeper

The same idea at increasing depth. Stop wherever it stops being useful.

overview

Four gates in front of every effect. Parse: is this a plan at all? Resolve: do these tools and arguments exist? Type: are the argument values the right kind? Authorize: may this caller do this to this resource? Only then execute. The order is not arbitrary — each gate needs what the one before it established, and execution comes last because its effects cannot be taken back by a later rejection.

practical

Two changes carry most of the value. First, record which gate rejected each plan; four counters with four owners will diagnose regressions your success metric cannot see. Second, authorize the whole plan before running any of it, and verify that with a deliberate test — submit a plan whose last step is denied and confirm nothing ran. If earlier steps executed, your authorization is per call regardless of what the design says. And never feed a policy denial back to the model as a repair prompt: it will rewrite the shape of an action it is not allowed to take, forever.

advanced

The hard case is a plan whose later steps act on resources that do not exist yet — step 3 operates on $2, and $2 is whatever step 2 returned. Whole-plan authorization then has to choose between being conservative enough to be a guarantee and permissive enough to be useful. The three honest resolutions are: restrict the plan language so resource identity is always statically visible; authorize statically on what is known and re-check the resolved resource at the moment of use; or mint a capability scoped to what the plan named and let the downstream service enforce it. All three are recognisable: this is the static-versus-dynamic checking tradeoff, and it behaves exactly the way gradual typing does — a static check that cannot cover every case, plus a runtime check at precisely the boundary where the static one gave up. The failure mode is also the same one gradual typing has: if the runtime check at the boundary is missing, the static guarantee was never sound in the first place.

How much this depends on

Nothing in this domain is true of every compiler. These say how much.

typicalThe four-gate ordering describes systems with a tool registry and a policy engine, which is the mainstream shape in 2026. Systems built around a sandboxed code interpreter invert the emphasis: there is no per-tool policy gate at all, and containment comes from the sandbox having no credentials. Both are defensible; what does not work is claiming the guarantees of one while building the other.
simplifiedDrawn as a straight line for one turn. Real agents loop, and each loop re-runs every gate on new output generated from context that now includes tool results. The per-turn picture is accurate; the session-level picture has a feedback edge that no single-turn gate covers, which is where [[indirect-prompt-injection]]-style problems live.

If you were asked this in an interview

  • Order these and justify each dependency: parse, execute, authorize, type-check, resolve names.
  • A plan's fourth step is denied by policy. What should have happened to the first three, and what does your answer imply about where authorization runs?
  • When should authorization run before type checking rather than after?

Connections