Skip to content
Harness Engineering2026-09-1212 min read

AI Agent Trusted Configuration: Who Owns the Budget?

AI agent trusted configuration keeps budgets and prices outside generated proposals. Test top-level and nested overrides against a closed Python schema.

Key takeaways

  • A generated field gains no authority from matching the name of a policy value the application already owns.
  • Rejecting extra fields at the schema boundary stops a proposal from redefining its own budget or unit prices.
  • The application must load budget, prices and snapshot identity from a trusted source, never from the model's output.
  • Testing top-level, nested and aliased override attempts separately shows whether the boundary holds in every shape.
  • An accepted draft is a validated proposal, not evidence that a purchase, write or external action has occurred.

Rod Rivera

Author

AI Agent Trusted Configuration: Who Owns the Budget?

Rod's note — read with a pencil; the margins are for you.

U.S. teaching example: Amounts are fictional USD prices, stored as integer cents. This is a U.S. version of the shop exercise, not a currency conversion or a record of a real charge.

The field that shouldn't matter, and does anyway

Suppose a shopping assistant drafts a replenishment order. Its JSON includes a line for six tubs of vanilla, a line for four tubs of strawberry, and one more field nobody asked for: budget_cents: 999999. The rest of the draft is well-formed. Every quantity matches the shop's real shortages. So here's the question worth sitting with for a second: does that extra field change anything?

It shouldn't, and if your validation code is built correctly, it can't. But plenty of agent integrations get this wrong in a specific, repeatable way, and understanding why requires separating two things that look alike in a JSON blob but are not alike at all: data the model is allowed to propose, and configuration the application already owns before the model ever runs.

The distinction matters because a large language model produces structured output by pattern-matching plausible shapes. It may generate a budget field that looks plausible next to sku and quantity; plausibility does not establish where a value came from. Nothing about the JSON syntax tells you that budget_cents here is fabricated while budget_cents in your application's own configuration store is authoritative. The name is identical. The trust level is not. If your code reads whichever budget_cents happens to arrive in the request, you've let a text generator set your own spending limit.

Two schemas, one boundary

The fix is to define two separate shapes and never let one substitute for the other. A trusted snapshot, loaded from your own inventory system, carries the budget and the prices. A proposal, generated by the model, carries only what it is allowed to influence: which products, in what quantities, with what explanation. Pydantic's strict mode is useful here because it narrows what counts as a valid value for a field, reducing silent type coercion between differently-shaped inputs, though the exact coercion rules differ by field type and by whether the input arrived as Python objects or as JSON text, so the details are worth checking against the installed version rather than assumed from one example (Pydantic: strict mode, https://pydantic.dev/docs/validation/latest/concepts/strict_mode/).

Strict typing alone doesn't solve the ownership problem, though. A budget_cents field typed as a strict integer is still a field the model could try to fill in. The real defense is extra="forbid" on the proposal schema: if the model's JSON includes a key that isn't part of the proposal's declared shape, the whole object fails validation before your business logic ever runs. The proposal schema for an order draft should list schema_version, snapshot_id, currency, lines, and explanation. It should not list budget_cents or price_cents at all. Those live only on the trusted snapshot, loaded by your own code from your own database or configuration store, at a moment the model has no part in.

This is the same shape of problem the Model Context Protocol's authorization specification addresses at the transport layer: a credential that lets a client call a tool is not the same as permission to perform every action that tool exposes, and the two need to stay distinct rather than collapsed into "the request came through, so it's authorized" (Model Context Protocol: authorization specification, https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization). The pattern repeats at the data layer: a JSON field that arrived through a trusted channel is not automatically a trusted value.

Walking the worked case

Consider a small shop tracking three products. This is a teaching example, not a report of any deployed system: a trusted snapshot fixes on-hand stock, replenishment targets, per-tub prices, and a spending budget, all supplied by the application, never by a model.

SKUOn handTargetRequired tubsPrice per tubLine cost
vanilla286250 cents1,500 cents
chocolate1260300 cents0 cents
strawberry154275 cents1,100 cents

Required quantity per product is max(0, target - on_hand). Summed line cost is 2,600 cents against a configured budget of 3,000 cents. Those numbers come from the snapshot, not from anything the model proposes. This worked case follows the structure used in an existing course lesson on validating business decisions and bounding a repair loop (see /courses/zeo-itam-autumn-2026), which builds the full Pydantic models step by step; here we focus on the ownership question specifically.

The proposal schema forbids extra fields, so a draft that tries to add budget_cents: 999999 fails schema validation outright, before any business rule runs. That's the first line of defense. But suppose the model is cleverer, and instead of adding a top-level field, it nests the value: {"policy": {"budget_cents": 999999}, "lines": [...]}, or it uses a synonym like spending_limit hoping your code greps for something budget-shaped. Those top-level variants are rejected by a closed proposal model. Nested line models must also forbid extra fields: the outer model's setting does not automatically configure every independently defined nested model. An explanation string can still mention a budget, so downstream code must never parse that prose into policy. The validator that computes the total reads budget_cents from the trusted snapshot object, a completely separate Python value with its own provenance, never from anything inside the parsed draft.

Proposal data cannot promote itself to policy

The diagram's point is structural: the draft and the snapshot are two inputs to a comparison, not one input that gets trusted wholesale. The arrow from the draft goes only into a schema check and then into a line-by-line comparison. It never reaches the box that decides what the budget is.

Running the override attempts one at a time

It helps to test each override shape separately rather than lump them into one messy fixture, because a single object with six problems tells you little about which specific check caught which specific problem.

Attempt a top-level override

Send a draft identical to the accepted one, but with budget_cents: 999999 added at the top level. Because the proposal model uses extra="forbid", this fails at schema validation with an "extra fields not permitted" error, before the comparison logic runs at all.

Attempt a nested override

Wrap the value inside a policy object: {"policy": {"budget_cents": 999999}, "lines": [...]}. The proposal schema has no policy field either, so this also fails schema validation, for the identical reason as the top-level case: the shape simply doesn't accept it.

Attempt an alias

Rename the field to something plausible like spending_limit or authorized_total. Same result. The rejection isn't keyed to the literal string budget_cents; it's keyed to the fact that the proposal schema enumerates a fixed, closed set of fields, and none of them concern policy.

Confirm the snapshot budget is unmoved

After each rejected attempt, re-run the validator against the trusted snapshot with an unmodified, correctly-shaped draft. It should still compute the same 2,600 cents total against the same 3,000 cents budget, showing that none of the three attack shapes leaked through to change what the application actually believes the budget is.

Here is a local Pydantic v2 check of that structural rule. Both models inherit the closed configuration. This intentionally short schema illustrates field ownership; it does not implement the course's full inventory validator.

python
from copy import deepcopy
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field, ValidationError

class ClosedModel(BaseModel):
    model_config = ConfigDict(strict=True, extra="forbid")

class Line(ClosedModel):
    sku: str
    quantity: int = Field(ge=1, le=50)

class Proposal(ClosedModel):
    schema_version: Literal["v1"]
    snapshot_id: str
    currency: Literal["USD"]
    lines: list[Line]
    explanation: str

snapshot = {"budget_cents": 3000, "prices": {"vanilla": 250, "strawberry": 275}}
baseline = deepcopy(snapshot)
good = {"schema_version": "v1", "snapshot_id": "stock-17", "currency": "USD",
        "lines": [{"sku": "vanilla", "quantity": 6},
                  {"sku": "strawberry", "quantity": 4}],
        "explanation": "Restore target stock"}
attacks = [dict(good, budget_cents=999999),
           dict(good, policy={"budget_cents": 999999}),
           dict(good, spending_limit=999999)]
nested = deepcopy(good)
nested["lines"][0]["price_cents"] = 1
attacks.append(nested)
for payload in attacks:
    try:
        Proposal.model_validate(payload)
    except ValidationError as exc:
        assert all(e["type"] == "extra_forbidden" for e in exc.errors())
    else:
        raise AssertionError("Override accepted")
    assert snapshot == baseline
accepted = Proposal.model_validate(good)
total = sum(line.quantity * snapshot["prices"][line.sku] for line in accepted.lines)
assert total == 2600 <= snapshot["budget_cents"]
print("Four overrides rejected; trusted price calculation remains 2600 cents")

The successful control matters: a validator that rejects every request would also reject all four attacks. Here the ordinary proposal is still structurally accepted and uses the separate price map. Duplicate lines, current snapshot identity and exact replenishment quantities need the additional business checks from the course.

A parallel test worth running is altering a unit price instead of the budget. Change one product's price_cents inside the draft while leaving quantities alone. Because price_cents isn't part of the proposal schema either, the modified value never reaches the cost calculation. The trusted catalog's price of 250 cents per vanilla tub is what gets multiplied, regardless of what number the model's JSON happened to include.

A rejected field can still leak through string handling

If your code accepts the raw draft as a Python dict before validating it, and any downstream function reads draft.get("budget_cents", default) directly from that dict rather than from the validated model, the schema's extra="forbid" protection is bypassed entirely. Validate first, then only ever read from the validated object, never from the original parsed JSON.

What would show this boundary has failed

It's worth being specific about what would count as evidence the separation isn't working, so the claim stays falsifiable rather than just asserted. If a test draft containing an extra budget_cents, policy.budget_cents, or spending_limit field is accepted rather than rejected, the schema's extra="forbid" setting either isn't applied to the right model or has been loosened somewhere. If an accepted draft's computed total changes when the draft's own (schema-rejected) price field is present versus absent, then some code path is reading from the wrong object. And if the validator ever raises no error but returns a total that doesn't match the manual arithmetic from the trusted snapshot, the bug is in the comparison logic itself, not in the boundary concept. Any of these would be a concrete, checkable failure, not a vague sense that something feels off.

It's also worth being honest about what this kind of check does not prove. A validator that rejects three crafted JSON payloads in an in-process test demonstrates that your Python code enforces its own schema, in that program, for those inputs. It says nothing about a different service, a different SDK version, or a supplier's API behaving the same way when it constructs a similar request. If a proposal ever passes through a message queue, a webhook, or a third-party orchestration layer before your validator sees it, you need to check that no intermediate step re-serializes the draft in a way that merges fields, and that nobody downstream trusts an intermediate representation instead of your final validated object.

Quick check — A draft includes budget_cents: 999999 alongside correctly-computed order lines. What happens?

Check the decision

Closed schemas reject top-level policy fields and nested line prices while explanation text remains untrusted

Naming the owner before you ship the schema

The practical next step for anyone integrating a model into a workflow that touches money, inventory, or any other consequential state is to write down, before drafting the schema, exactly which fields the application owns and who updates them. That list becomes your extra="forbid" boundary. Budget, prices, permissions, and identity fields belong to configuration the application loads independently, typically from a database, an environment-scoped settings file, or an internal service, with a named owner and a defined update path. Everything else the model proposes gets its own schema, checked against that configuration, never merged with it before validation.

This is also where handoffs between people matter. Whoever hands off an integration to another engineer should be able to name, in one sentence, which values are configuration and who is authorized to change them, rather than leaving that boundary implicit in code that happens to work today. A schema that forbids extra fields is a technical enforcement of an organizational answer to that question, and the two need to agree.

Work through the full validation and bounded-repair lesson

See the complete Pydantic models, the seven-case rejection test, and the two-attempt repair loop this worked example draws from.

Ready to put an agent to work?

Join the Prof Rod newsletter for one educational lesson a week, with worked examples attached. It is free to register for and separate from the Zero Employee community.