LLM Decision Containment
JacqOS is a physics engine for business logic. In that frame, this pattern is the wall a player cannot walk through: an LLM-proposed action is a player move, the engine refuses to enact it unless an explicit decision rule ratifies it against policy, and the named invariant is the wall itself.
The Real-World Failure
Section titled “The Real-World Failure”A Chevrolet dealership’s chatbot was tricked into “selling” a new Tahoe for $1. An Air Canada chatbot invented a bereavement refund policy that the airline had to honour in court. Across domains, LLM-powered assistants routinely propose actions that violate the policies the underlying business cares about — not because the models are broken, but because policy enforcement was never the model’s job.
The failure shape is always the same: a model generates a decision; a thin orchestration layer turns that decision into an action; the action reaches the world. There is nothing between the model’s probabilistic output and the effect.
What JacqOS Does About It
Section titled “What JacqOS Does About It”JacqOS makes it structurally impossible for a model’s decision to reach the world without passing through a policy check you wrote and can inspect.
Every model-proposed action lands in the reserved proposal.*
namespace. From there, an ontology decision rule evaluates the
proposal against the policy facts the system knows:
- “Authorize this offer if the proposed price is at or above the pricing floor.”
- “Reject this offer if the proposed price is below the floor.”
Only authorized decisions derive executable intent.*. Rejected
decisions stay in Activity’s Blocked tab where an operator can see
them; no side effect touches the world. (Need a middle tier? Add a
manager-review decision rule — escalation is just another rule over the
same proposal.)
This means:
- A model proposing a $1 Tahoe is isolated to the proposal space. The
would-be
intent.offer_send_requestednever derives, because nodecision.approved.sales.send_offerformed for it. - A model proposing a price at or above the floor flows through authorization and fires the intent. Everyone sees the full decision chain.
- Invariants are a second, independent safety net. Even if something
downstream sent an offer with no approving decision, the named
invariant
offer_sent_requires_authorized_decisionwould make the resulting world state logically inadmissible.
The model is free. The safety is structural.
What You’ll See In Studio
Section titled “What You’ll See In Studio”Open Car Dealership Chat (Live) and drive it from the agent chat — this is the live getting-started demo. A real model proposes offers; the containment plays out live.
- Fair offer → ask for the Tahoe at its $68,900 advertised price.
The
Donetab shows the sent offer. Drill in and the inspector takes you from the executed offer back throughdecision.approved.sales.send_offer, the pricing-floor fact, and the model’s proposal. - Corrupted $1 offer → send the trigger phrase that breaks the
model into proposing the Tahoe for $1. The
Blockedtab shows the refused offer. Drill in and the reason banner names the rejection (below_minimum_price) and shows that no send intent could derive.
Critically, the $1 offer is not blocked because the model produced something bad. The model produced exactly what the attack told it to. It is blocked because the decision rule refused to authorize a $1 offer against a policy you can see. The safety boundary lives in your ontology, not in the prompt.
What It Looks Like In Code
Section titled “What It Looks Like In Code”Model output arrives as structured JSON ({ text, offer }). The mapper
routes the proposed offer into the reserved proposal.* namespace —
evidence, never an action:
// mappings/inbound.rhai — the offer object becomes proposal evidencefn map_observation(obs) { // ... when the model returns an offer object (not null): [ atom("offer.proposal_id", offer.proposal_id), atom("offer.vehicle_id", offer.vehicle_id), atom("offer.price_usd", offer.price_usd), // relays into proposal.offer_suggested ]}A decision rule evaluates the proposal against the pricing-floor policy fact:
rule decision.approved.sales.send_offer(session_id, proposal_id, vehicle_id, price_usd) :- proposal.offer_suggested(_, session_id, proposal_id, vehicle_id, price_usd), dealer.pricing_policy(vehicle_id, _, _, minimum_offer_price_usd), price_usd >= minimum_offer_price_usd.
rule decision.rejected.sales.send_offer(session_id, proposal_id, vehicle_id, price_usd, "below_minimum_price") :- sales.policy.offer_below_minimum_price(session_id, proposal_id, vehicle_id, price_usd).Only an authorized decision derives the executable intent:
rule intent.offer_send_requested(session_id, proposal_id, vehicle_id, price_usd) :- decision.approved.sales.send_offer(session_id, proposal_id, vehicle_id, price_usd).And a named invariant catches any send that never had an authorized decision behind it:
invariant offer_sent_requires_authorized_decision() :- count sales.invariant.offer_sent_without_authorization(_, _, _, _) <= 0.There is no path from raw proposal.offer_suggested to
intent.offer_send_requested. That single rule is the safety boundary.
Make It Yours
Section titled “Make It Yours”The dealership example is one kind of LLM decision containment. The same pattern fits:
- Customer service chatbots — an LLM proposes a refund; a refund-policy decision rule authorizes, escalates, or rejects. See Air Canada Refund Policy for a complete worked example built around the public Air Canada bereavement-policy chatbot failure.
- Incident remediation agents — an LLM proposes a remediation
step; a safety decision rule ensures
no_kill_unsynced_primaryand friends hold before the remediation can fire. - Procurement automation — an LLM proposes a purchase; a spending-authority decision rule gates by amount and vendor tier.
- Compliance screening — an LLM proposes a disposition; a compliance decision rule checks watchlists and jurisdictional rules.
Any time you have an AI proposing a commercial or operational action,
the decision containment pattern fits. To start building, pick up
Build Your First App and scaffold
with jacqos scaffold --pattern decision.
Going deeper
Section titled “Going deeper”For the underlying mechanics — proposal.* namespaces, ratification
rules, and the relay boundary the loader enforces — see:
- Car Dealership Chat (Live) — the full worked example: ontology, mapper, fixtures, and the live CLI path.
- Action Proposals — how to author
decider-relay proposals, the ratification rules that gate them, and
the schema reference for
proposal.*validation.