Module 6: The Deterministic Shell

The model proposes, the system disposes

Overview

Lesson 1 installed the module's thesis in one sentence: the model proposes, the system disposes. This lesson turns it into the root principle from which all the others come, and it does so in the only way that truly convinces: putting side by side two designs of Mercado's support agent that receive exactly the same model proposals, and measuring how much money each one moves. In the naive design, the LLM's output executes the action directly: the model says "refund" and the money moves, with no one to dispute it. In the shell design, the LLM's output is a proposal that a deterministic layer validates before executing. Same model, same proposals, same errors; the only thing that changes is who has the last word over the money.

The difference you'll measure is brutal and it's the whole point: the naive design paid $5315 and executed six actions —four of them dangerous—; the shell one paid $125 and executed two. The shell didn't make the model better: it made the model stop having the power to execute. That transfer of power —from "the LLM does" to "the LLM suggests and the code decides"— is the root antipattern this module corrects, and seeing it in money makes it impossible to ignore.

Connection with the module. This is the principle lesson, the one that develops lesson 1's thesis all the way down. All the lessons that follow are how it disposes: the structured action (L3) is the format in which the model proposes; the bounded capabilities (L4) are the menu of what it can propose; the rule validation (L5) is the policy it's disposed against; the small core (L6) is how much is left in the model's hands; the pipeline (L7) assembles them. Here we establish why propose and not execute, with money. The boundary with AI Engineering holds: we're not talking about how to make the model propose better (better prompt, tool use, function calling) —that's AI Eng—; we're talking about why, whatever it proposes, it shouldn't execute.

An analogy: the waiter who brings the check and the waiter who charges your card

Imagine two restaurants with two different ways of charging. In the first, the waiter brings the check to your table: a piece of paper with what you ordered and the total. You review it —"I didn't order this", "you overcharged here"— and only then do you pay what you approve. The waiter proposes a charge; you dispose whether it's executed. If the waiter made a mistake, wrote down a dish from another table, or inflated the total, you catch it before the money moves, because there's an approval step between the proposal (the check) and the execution (the payment).

In the second restaurant, the waiter has your card from the moment you arrived and charges directly what he thinks you ordered, without bringing you the check. If he gets it right, nothing happens. But if he makes a mistake —charges you a dish you didn't order, gets the total wrong, or puts the next table's order on your account—, the money has already gone out: there was no approval step where to catch it. Your only option is to complain afterward, fight for a refund, review your statement weeks later and discover the error when it's already a problem.

The two waiters can be equally smart and well-intentioned. The difference isn't in the waiter: it's in whether there's an approval step between the proposal and the execution. The first restaurant has it —the check you review—; the second doesn't. And that's why, even if the two waiters make mistakes with the same frequency, in the first the errors are caught before touching your money and in the second they become charges that already happened. Your support agent is the waiter: the naive design gives it your card (executes directly); the shell design makes it bring the check (proposes, and the code disposes).

Worked example: same model, two designs, the difference in money

Let's measure the transfer of power. We take the same six proposals the probabilistic core generated in lesson 1 —the same good ones and the same bad ones— and run them through two designs. In design A (naive), each model proposal is executed directly: the money moves. To illustrate the damage, we run a validation that only labels what went wrong, but doesn't stop anything —because in the naive design there's nothing to stop it—. In design B (shell), the same validation does decide: only what passes is executed.

# Module 6, Lesson 2: the model PROPOSES, the system DISPOSES.
# We compare TWO designs with the SAME LLM proposals (simulated):
#   A) naive  : the LLM EXECUTES the action directly (touches the money).
#   B) shell  : the LLM PROPOSES and a deterministic layer DISPOSES (validates first).
# No network, no API, no keys. Fixed data.

ORDERS = {
    "A-1001": {"total": 50.00,  "days_since_delivery": 3,  "refunded": False},
    "A-1002": {"total": 120.00, "days_since_delivery": 45, "refunded": False},
    "A-1003": {"total": 30.00,  "days_since_delivery": 5,  "refunded": True},
    "A-1005": {"total": 75.00,  "days_since_delivery": 8,  "refunded": False},
}
MAX_REFUND = 100.00
RETURN_WINDOW_DAYS = 30

# The SAME proposals the probabilistic core generated in lesson 1.
PROPOSALS = [
    {"action": "refund", "order_id": "A-1001", "amount": 50.00},
    {"action": "refund", "order_id": "A-1002", "amount": 120.00},
    {"action": "refund", "order_id": "A-1003", "amount": 30.00},
    {"action": "refund", "order_id": "A-9999", "amount": 40.00},
    {"action": "refund", "order_id": "A-1005", "amount": 5000.00},
    {"action": "refund", "order_id": "A-1005", "amount": 75.00},
]


def validate(proposal):
    oid, amount = proposal["order_id"], proposal["amount"]
    if oid not in ORDERS:
        return (False, "order does not exist")
    order = ORDERS[oid]
    if order["refunded"]:
        return (False, "already refunded")
    if order["days_since_delivery"] > RETURN_WINDOW_DAYS:
        return (False, "outside window")
    if amount > order["total"]:
        return (False, "amount > total")
    if amount > MAX_REFUND:
        return (False, "amount > limit")
    return (True, "approved")


# --- Design A: naive. The LLM executes; the money moves without asking. ---
paid_A = 0.0
executed_A = 0
damage = []
for p in PROPOSALS:
    # No layer disputes it: what the LLM proposed gets executed.
    paid_A += p["amount"]
    executed_A += 1
    ok, reason = validate(p)   # only to LABEL the damage, does NOT stop anything
    if not ok:
        damage.append((p["order_id"], p["amount"], reason))

# --- Design B: shell. The LLM proposes; the deterministic layer disposes. ---
paid_B = 0.0
executed_B = 0
for p in PROPOSALS:
    ok, _ = validate(p)
    if ok:
        paid_B += p["amount"]
        executed_B += 1

print("=== Design A: the LLM EXECUTES directly (naive) ===")
print(f"  actions executed : {executed_A}/{len(PROPOSALS)}")
print(f"  money paid       : {paid_A:.2f}")
print("  damage that got out (real money lost or wrongly moved):")
for oid, amt, reason in damage:
    print(f"    - {oid:<7} {amt:>8.2f}  ({reason})")

print()
print("=== Design B: the LLM PROPOSES, the shell DISPOSES ===")
print(f"  actions executed : {executed_B}/{len(PROPOSALS)}")
print(f"  money paid       : {paid_B:.2f}")

print()
print("=== The difference the shell makes ===")
print(f"  dangerous executions avoided : {executed_A - executed_B}")
print(f"  money protected              : {paid_A - paid_B:.2f}")

What to expect. When you run the file, the output is exactly this:

=== Design A: the LLM EXECUTES directly (naive) ===
  actions executed : 6/6
  money paid       : 5315.00
  damage that got out (real money lost or wrongly moved):
    - A-1002    120.00  (outside window)
    - A-1003     30.00  (already refunded)
    - A-9999     40.00  (order does not exist)
    - A-1005   5000.00  (amount > total)

=== Design B: the LLM PROPOSES, the shell DISPOSES ===
  actions executed : 2/6
  money paid       : 125.00

=== The difference the shell makes ===
  dangerous executions avoided : 4
  money protected              : 5190.00

Compare the two runs, because the difference between them is the whole lesson.

The naive design executed the six proposals. Not because the model was worse —it's the same model, the same proposals—, but because there's no layer between the proposal and the execution. What the model proposed got done. It paid $5315, and the damage list shows what moved that shouldn't have: a refund outside the window ($120), a double refund ($30), a payment to a nonexistent order ($40), and —the one that hurts— a $5000 refund of a $75 order. Notice an important detail: the validation did exist in design A, but it only labeled the damage after it already happened. Validating without veto power isn't a shell; it's an autopsy. Knowing afterward that you over-refunded $5000 doesn't give you back the $5000.

The shell design executed two. Same six proposals, same validation —but now the validation decides, not just labels—. The two legitimate ones (A-1001 for $50, A-1005 for $75) passed; the four dangerous ones were blocked before moving a cent. It paid $125. The shell didn't change what the model proposed; it changed who has the last word: in design A the model has it, in B the deterministic code has it.

The difference, in the number that matters: $5190 protected and 4 dangerous executions avoided. That's the exact value of moving the power to execute from the model to the shell. And notice that we did nothing to improve the model —we didn't change its prompt, we didn't make it smarter, we didn't reduce its error rate—. We only took away its power to execute directly and gave it to a deterministic layer. That's the essence of the principle: safety doesn't come from a better model, it comes from the model not executing.

Going deeper: why "execute what the model said" is the root antipattern

The naive design has an honest name: coupling the execution to the model's output. And it's the antipattern from which almost all the disasters of AI-native apps that touch state derive. It's worth understanding why it's so tempting and why it's so dangerous.

It's tempting because it's the shortest path. The model already "knows" what needs to be done —it says it in its response—, so connecting that response directly to the action seems to eliminate a useless step. "Why validate again what the model already decided?" The answer is that the model didn't decide anything with a guarantee: it proposed something with a probability of being right. Connecting the proposal directly to the execution treats a probabilistic suggestion as if it were a verified order, and that confusion is the root of the problem.

It's dangerous because actions are irreversible. A bad text can be filtered, corrected, or simply not shown. A bad action —money that went out, an order that was canceled, an account that was deactivated— has already happened. Module 4 protected against bad outputs you display; this module protects against bad actions you execute, and the difference is that in the second case there's no free "undo". That's why the validation has to go before the execution, not after: after is an autopsy, before is a shell.

   ANTIPATTERN (naive):                     PATTERN (shell):

   customer message                         customer message
          │                                        │
          ▼                                        ▼
   ┌─────────────┐                          ┌─────────────┐
   │   the LLM   │                          │   the LLM   │
   │  (proposes) │                          │  (proposes) │
   └─────────────┘                          └─────────────┘
          │                                        │
          │  the output executes                   │  the output is a PROPOSAL
          ▼                                        ▼
   ┌─────────────┐                          ┌─────────────┐
   │   money     │                          │deterministic│  validates against rules
   │  / state    │  ◄── already happened    │    shell    │  ┌──────────────┐
   └─────────────┘                          └─────────────┘  │ passes? no ── blocks
                                                   │         └──────────────┘
                                                   ▼ yes
                                            ┌─────────────┐
                                            │   money     │
                                            │  / state    │  ◄── only the approved
                                            └─────────────┘

The approval step is the heart of the pattern. Look at the two diagrams: the only structural difference is that the pattern inserts a box —the deterministic shell— between the proposal and the effect. That box is the waiter who brings the check, the manager who authorizes, the envelope that clips. It doesn't change what the model proposes; it changes that its proposal goes through a deterministic approval before touching anything. All the rest of the module is what's inside that box: how it receives the proposal (structured action, L3), what actions it even accepts to consider (capabilities, L4), against what it validates them (business rules, L5). But the idea is this: there is a box, and the model can't skip it.

Validating without veto power doesn't count. A subtle error the example makes clear: design A had the validation —the same validate function—, but only used it to label the damage, not to stop it. This happens in real life more than it seems: teams that log "this action seems out of policy" while executing it anyway, and believe they're protected because "we're monitoring it". Monitoring an embezzlement while it happens isn't containing it. The validation is only a shell if it has veto power: if its "no" stops the execution. A "no" that's only noted in a log is an autopsy with good documentation.

The weight of the shell is calibrated to the action's reversibility. Not every model action needs the same containment, and understanding why refines the design. What makes the naive design dangerous is that this example's actions —moving money— are irreversible: once the refund is paid, there's no free "undo". For an irreversible action, the validation has to go before executing, because afterward there's no remedy. But there are model actions that are reversible or low-risk —saving a draft, forwarding an email, flagging something for review— where an error is corrected at no real cost. The design rule that follows: the amount of shell an action needs is proportional to its blast radius. Refunding demands the complete pipeline of gates you'll see in the following lessons; forwarding an invoice demands only a basic check. Confusing the two extremes costs both ways: putting little shell over an irreversible action is design A's disaster; putting heavy shell over a trivial action is useless friction. The discipline is to reserve the weight of containment for where the damage of a hallucination really matters —the irreversible, what touches money or critical state— and lighten it where it doesn't. In this module we work the hard case (the refund) because it's the one that sets the pattern; the easy case is the same pattern with fewer rules.

Common mistakes

Connecting the model's output directly to the API that touches money. What happens: the agent detects that a refund is needed and calls the refund API directly with what the model said. It's design A. It works in the tests —the model proposed well almost always— until the day it proposes $5000 or a nonexistent order, and by then the money has already gone out. Why it happens: it's the shortest path and it seems unnecessary to validate "what the model already decided". How to detect it: if between the model's output and the effect on the money there's no layer with veto power, you have the antipattern. How to fix it: insert the shell —the box in the diagram— and make the model's output a proposal the shell validates, not an order you execute.

Putting the validation after executing, not before. What happens: the team does validate, but does it after moving the money —"we refund and then we check if it was right"—. Since the actions are irreversible, the late validation only discovers the damage; it doesn't prevent it. Why it happens: sometimes by design (it seems simpler to execute first), sometimes by a bad ordering of the operations. How to detect it: in your flow, does the if that decides whether the action is valid run before or after the line that executes it? If it's after, you're doing autopsies. How to fix it: the validation goes before the execution, always. The action is only executed in the branch where the validation passed.

Confusing monitoring with containing. What happens: the system logs every suspicious action —"out-of-policy refund detected"— but executes it anyway, and the team believes it's protected because "it has observability". It's exactly design A: validation that labels but doesn't veto. Why it happens: logging is easy to add and gives a false sense of control. How to detect it: ask yourself whether your "dangerous action" alerts fire before the action happens (and stop it) or after (and only report it). How to fix it: the validation has to have veto power —its "no" stops the execution—. Monitoring is fine and useful (the audit log from lesson 7 uses it), but it doesn't replace containment: they're distinct things, and only the second protects the money.

Exercises

Exercise 1 — The two waiters. Explain, with the two-restaurants analogy, why the naive design (design A) and the shell design (design B) can use the same model and still one pay $5315 and the other $125. What element does the second restaurant have that the first doesn't, and what does it correspond to in the code?

See solution

The two designs use the same model because the model proposes exactly the same in both —the same six actions, with the same four errors—. The difference isn't in the model; it's in whether there's an approval step between the proposal and the execution.

The second restaurant (design B, the shell) has what the first (design A, the naive) doesn't: the check you review before paying. The waiter (the model) proposes a charge; you (the deterministic shell) review it and approve only the correct one. In the first restaurant, the waiter has your card and charges directly: its errors become charges that already happened.

In the code, that "approval step" is the difference between the two branches: in design A, paid_A += p["amount"] runs for every proposal (it charges directly); in design B, paid_B += p["amount"] runs only inside if ok: (it charges only the approved). That condition —the validation with veto power before moving the money— is the check you review. Without it, the model has your card; with it, the model brings you the check.

Exercise 2 — The validation that arrives late. In design A, the validate function does get called —to build the damage list—, but doesn't prevent a single payment. Explain why "validating after executing" doesn't protect the money, and give an example from the code where you can see that design A's validation has no veto power.

See solution

"Validating after executing" doesn't protect the money because the actions are irreversible: by the time the validation discovers the $5000 refund was wrong, the money has already gone out. Late validation produces knowledge (you know what was wrong), but doesn't produce containment (it didn't prevent it from happening). It's an autopsy: it tells you what the patient died of, it doesn't keep them alive.

In design A's code it shows clearly in the order of the lines: first paid_A += p["amount"] (the payment is executed), and then ok, reason = validate(p) followed by if not ok: damage.append(...). The validation runs after the payment, and its result is only used to add to a list (damage), never to revert or prevent the paid_A += ... that already occurred. There's no branch where validate's False prevents the payment. Compare with design B, where validate runs first and the payment is inside if ok: —there, yes, the False vetoes the payment—. The lesson: the validation is only a shell if it runs before and its "no" stops the execution.

Exercise 3 — Where to put the box. A colleague proposes this design for Mercado's "describe your product" generator: the LLM writes the description, publishes it directly to the catalog, and in parallel a process reviews the published descriptions and takes down the ones that violate the policy. Identify the antipattern, say what error from this lesson it commits, and propose the redesign.

See solution

The antipattern is that the publication is coupled to the model's output (the LLM publishes directly) and the validation runs after (the process reviews what's already published). It commits two of the lesson's errors at once: connecting the model's output directly to the action (publishing is the action that touches state) and putting the validation after executing, not before. The result: every description that violates the policy —a forbidden claim, an offensive text— is published and visible to customers for however long the review process takes to take it down. The damage (a false or forbidden description seen by customers) already occurred, even if it's corrected afterward.

The redesign moves the box: the LLM proposes the description, a deterministic shell validates it before publishing (length within the limit?, no forbidden claims?, no offensive language?), and it's published only if it passes. The description that violates the policy never reaches the catalog, instead of reaching it and being taken down afterward. The validation goes from being an autopsy (reviewing what's published) to being a shell (approving before publishing). Note: here the content check (claims, length, language) is part output guardrail (M4) and part action shell (publishing); what the lesson contributes is the order —validate before the publishing action touches the state, not after—.

Summary and next step

In this lesson you turned the module's thesis into a measured principle: the model proposes, the system disposes. You put side by side two designs with the same model proposals —the naive, where the LLM executes directly, and the shell, where the LLM proposes and the code disposes— and measured the difference: $5315 paid vs $125, four dangerous executions vs zero, $5190 protected. You saw that safety didn't come from improving the model (it's the same model) but from taking away its power to execute; that the validation only counts if it goes before the execution and has veto power (validating after is an autopsy, monitoring isn't containing); and that the whole pattern reduces to inserting a box —the deterministic shell— between the proposal and the effect, a box the model can't skip.

Before moving on you should be able to: explain why coupling the execution to the model's output is the root antipattern; argue why the validation goes before and not after; distinguish monitoring (labeling) from containing (vetoing); and draw the shell's box between the proposal and the effect.

Lesson 3 opens the box and starts with its input: the structured action. If the model is going to propose and not execute, in what format does it propose? The answer isn't "free text someone interprets" nor "code that gets run", but a named command with typed fields{action: "refund", order_id: ..., amount: ...}— that a deterministic dispatcher can validate and dispatch. You'll see, executed, why a structured proposal is dispatchable and safe, while free text and invented actions are rejected at the channel before reaching the rule validation.

Resources

  • Anthropic, "Building Effective Agents" (2024) — anthropic.com/engineering/building-effective-agents. The distinction between a model that suggests an action and a system that decides to execute it is the axis of the safe-agent design the article describes. In English.
  • Martin Fowler and Bharani Subramaniam, "Emerging Patterns in Building GenAI Apps" — martinfowler.com/articles/gen-ai-patterns. The pattern of keeping the human or the code in the approval loop before an irreversible action is the general version of "the model proposes, the system disposes". In English.
  • Chip Huyen, AI Engineering (O'Reilly, 2024). Its discussion of the control of an agent's actions —and of why actions with side effects need an approval step— is the conceptual basis of this lesson. In English.
  • The least privilege principle and the separation of duties pattern from classic security: whoever proposes isn't whoever authorizes. Lesson 4 develops it as bounded capabilities. Any introductory security reference covers it.