Skip to content
JacqOS
Get started

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.

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.

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_requested never derives, because no decision.approved.sales.send_offer formed 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_decision would make the resulting world state logically inadmissible.

The model is free. The safety is structural.

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 Done tab shows the sent offer. Drill in and the inspector takes you from the executed offer back through decision.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 Blocked tab 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.

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 evidence
fn 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.

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_primary and 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.

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.