Dev.to Security 🔐 Cybersecurity 👁 0 📖 11 min read

Decision Firewall: what building an AI governance framework taught me

A refund-recovery example, measured comparisons and lessons from building Decision Firewall, an open-source reference for AI action authorization and audit. This is an adapted edition of the original article. It preserv

A refund-recovery example, measured comparisons and lessons from building Decision Firewall, an open-source reference for AI action authorization and audit.

This is an adapted edition of the original article. It preserves the section argument and evidence; use the source link for the published version and any interactive material.

Read the original article

01 / The starting problem

A refund is small enough to explain

I picked subscription refunds because the facts are inspectable. A duplicate-charge claim needs two payment records. Cancellation eligibility depends on timing and usage. A prior refund reduces the amount still available.

The example policy allowed verified duplicates and unused subscriptions cancelled within seven days. Eligible amounts up to ₹1,000 could proceed automatically; larger amounts needed review. A ₹10,000 daily automated limit sent excess demand to review. These were demonstration rules, with simulated payments.

Here is an illustrative ₹800 duplicate refund, using the recovery contract I built. The amount is an example, not an additional benchmark result.

Follow one refund through a lost response

  1. 1*Interpret the request.* The model classifies “I was charged twice” as a duplicate-charge claim. That supplies no proof that a second payment exists.
  2. 2*Authorize the exact action.* The ledger verifies the duplicate and ₹800 remaining. Policy checks eligibility and budget. Permission binds the duplicate payment, amount and original destination to the evidence and policy versions.
  3. 3*Dispatch, then lose the response.* The runtime reserves the balance and claims the authorization. The simulator records a successful refund under its idempotency key, but the caller receives no definitive outcome.
  4. 4*Recover without a second refund.* The attempt stays unresolved and retains its reservation. Reconciliation asks the payment adapter about the same key, discovers success and records the outcome. It does not create a new key and try again.

A timeout tells me the response is missing. It does not tell me the refund failed. That distinction survives a process restart only if the key and execution state are durable. The customer’s refund request also needs a status separate from a failed or malformed execution attempt.

This is useful application engineering. A framework can package it; it does not own the idea. Executor contract.

02 / How the plan grew

Every next layer sounded reasonable

I wanted developers to bring their own decision models and business rules. Refunds became a domain pack, with access control and simulated deployment examples testing whether the core travelled beyond payments.

Each next request sounded reasonable. Import old decisions. Keep reviewed labels separate from historical approvals. Compare policies offline. Retrieve examples without leaking evaluation data. I had LangChain’s abstractions and Langfuse’s observability experience in mind.

The plan grew into history ingestion, dataset snapshots and named Python rules, with optional context retrieval and telemetry. Preparation before inference could request missing evidence before spending a model call. All of it had a plausible use.

I had not yet established that another developer benefited from adopting the runtime underneath it. A coherent feature list had become my substitute for that evidence.

The project changed direction when the comparator changed. Scroll the map on small screens. Follow a branch: start · expand · compare · observe · repair · narrow.

03 / What I actually built with

The laptop put useful limits on the design

My GTX 1650 had 4 GB VRAM, alongside roughly 8 GB system RAM. I chose Laya for typed assessments, left Jev out, and required a fixture path that worked without model downloads. That made the control lifecycle testable before inference was available.

I used Python 3.12 and Pydantic for contracts, SQLite for durable authorization state, and a separate simulated payment ledger for effects. Integer minor units kept financial arithmetic out of model output. Ed25519 signatures and canonical JSON supported verifiable audit records. Typer exposed the CLI; FastAPI and Jinja powered an optional refund inspector.

The model adapter could return an assessment but held no execution credentials. Domain code resolved trusted evidence and evaluated policy. The executor owned the downstream effect. Optional OpenTelemetry export sat alongside the required local audit path.

The recorded Laya run used CUDA in FP32: 28.42 seconds to load and 47.45 ms median inference across 34 assessments, including the first. The report pins the checkpoint and SDK. Small hardware shaped the implementation; those timings did not establish model accuracy.

04 / The question I asked too late

Compared with what?

“Is the same foot just sitting at two different places, or genuinely there is some distinction?”

My question during the build conversation.

I asked for numbers with and without the framework. The hard part was choosing “without.” A model directly triggering refunds is an easy opponent. A competent application already validates the ledger and handles uncertain execution.

The synthetic comparison used a deliberately minimal model-directed arm and two governed arms, sharing evidence, fault schedules and a simulator with its own protections. With fixture assessments, both application controls and Decision Firewall produced zero contract-violating effects and zero unresolved attempts after recovery. The minimal arm produced 95 violating effects across 170 request trials.

That established the value of controls. It did not distinguish the two ways of implementing them.

On the matched straightforward-success subset, application p95 latency was 26.52 ms versus 67.89 ms for the framework. The paired median added cost was 23.23 ms across 25 pairs. The full run repeated 32 frozen scenarios five times; those repetitions measured runtime variation, not independent business situations. Refund comparison.

My next question should have been about integration effort: if both enforce the same contract, does the package make that contract easier to implement and maintain?

05 / More rows, then a different question

A classifier cannot demonstrate authorization value

Instead, I first asked for more data. The classification comparison accounted for 39,955 BANKING77 and Bitext source rows using a CPU naive Bayes classifier. Wrapping the same classifier left its predictions unchanged. BANKING77 official-test accuracy was 78.60% in every arm; the prepared path added 13.527 ms paired median latency.

There was no consequential action to authorize. I had measured overhead on a task that could not demonstrate the central benefit. Full counts and separate out-of-fold results.

A later tool-use pilot used openai/gpt-oss-20b through OpenRouter because Laya could not fill the conversational roles. It scheduled 90 selected τ-bench and AgentDojo episodes; 78 completed and 12 remained incomplete.

On the attacked AgentDojo subset, the baseline had eight attacker successes in nine completed episodes. Both controlled arms had zero in ten, but completed the legitimate task in only four of those ten. A blocked attack and a completed user task needed separate columns. This selected development pilot was not a full-suite score. Agent pilot.

06 / Where there was supplied functionality

Audit was useful. Telemetry was not a free win.

I thought audit and observability might justify reuse even when decisions matched. The application baseline needed eight explicit event call sites and four operation decorators. The framework integration supplied an observer with no application event hooks.

That was supplied functionality. It also had gaps: the measured framework spans omitted the evaluation ID, policy version, disposition and reasons. The manually instrumented application exported the first three. Neither exported the sensitive canaries. Exporter failure allowed execution to continue; required local audit failure blocked it. Measured observability report.

For the lost-response refund, telemetry helps locate the attempt. The durable record and downstream ledger establish what happened. I can tolerate a missing diagnostic span; I cannot safely forget the unresolved refund and release its reservation.

Signed receipts add a check that recorded content has not changed under a trusted key. They do not prove that the original evidence was true. I kept that distinction, while recognising that telemetry alone did not justify adopting the whole runtime.

07 / Letting another build challenge it

The comparison that changed the recommendation

I had no human participants, so I commissioned a separate Claude Code agent evaluation. One builder used the framework; another implemented equivalent application controls for the same purchase-order requirements. Both then handled a policy change and an investigation.

This was one paired pilot, independent of the framework builder, using the same model for both arms. It gave me a concrete integration comparison:

  • Visible acceptance tests: Framework: 26/26; Application: 26/26
  • Held-out acceptance tests: Framework: 114/115; Application: 114/115
  • Policy-change tests: Framework: 15/15; Application: 15/15
  • Investigation score: Framework: 48/48; Application: 48/48
  • Build tool calls: Framework: 45; Application: 34
  • Policy-change tool calls: Framework: 32; Application: 20
  • Library-contract workarounds: Framework: 8; Application: 0
  • Held-out suite time: Framework: 39.6 s; Application: 4.5 s

The framework required eight library-contract workarounds and seven core-change requests. The builder duplicated checks to obtain specific refusal reasons before calling it. Reuse was fighting the integration.

Both failed the same held-out approval-after-outage case because the spec was ambiguous; no downstream effect occurred. The baseline’s zero workarounds refers to library contracts, not its two test/path adjustments.

The 374-line spec supplied much of the lifecycle design. Investigators had downstream exports and a harness problem prevented operator-CLI use. A spend-limit interruption also affected the framework build. Tool calls are not developer hours, and these conditions limit generalisation. Full comparison and deviations.

“Stop feature expansion and adoption/migration claims.”

The independent evaluator’s recommendation.

08 / What passing tests had missed

I fixed the defects without rewriting the study

The review found four confirmed defects among 21 findings. Approval could execute against evidence the reviewer had not seen. Deactivating a rejecting reviewer could revive an older approval. Oversized integer metadata could strand an already-executed action. The repository tests missed six of nine injected runtime faults.

The mutation result mattered: downstream protections had hidden missing guarantees in the layer I meant to test. A green end-to-end suite was insufficient evidence that each boundary enforced its contract.

The 0.3.1 fixes bound review to the inspected evaluation, preserved rejection and handled large integers losslessly. Direct invariant tests detected all nine targeted mutations; recorded Windows verification had 298 passing tests, including 22 corrective tests. Corrective release.

I fixed the defects and kept the earlier comparison unchanged. Repairing correctness did not retroactively demonstrate easier integration.

09 / Where I would spend less

A useful control can still be an expensive abstraction

The global SQLite write lock still spans adapter I/O. It explains the roughly ninefold suite-time difference in the pilot, not a universal per-request slowdown. Fixing it safely would require a different concurrency and recovery design.

I chose to document the limit rather than start another architecture project to rescue the adoption argument. History ingestion and model-context budgets could wait too. I had built those surrounding features before proving the central abstraction helped an unfamiliar integrator.

The review exposed assumptions inside the supposedly generic interface: a message-shaped submission contract, one caller identity per runtime instance and a fixed approval lifetime. Moving code into a shared package had moved those assumptions into someone else’s application.

That is the distinction I now look for. The refund needs recovery after a lost response. Whether recovery belongs in this dependency is a separate engineering decision.

10 / What I kept

A reference implementation is a legitimate outcome

I kept Decision Firewall as an open-source reference implementation and conformance kit. The review verified action/evidence binding and recovery without replacement dispatch. A capacity-one resource admitted exactly one dispatch across eight operating-system processes. Historical replay called neither the evidence resolver nor the executor.

Those are useful behaviours to inspect in runnable code. The repository includes the reports and failure cases, so a builder can study the contract without accepting an adoption recommendation.

Start with the model-free path using Python 3.12, the tested runtime. Activate the virtual environment for your shell before installing:

Local, simulated effects only

git clone https://github.com/asadani/decision-firewall.git\
cd decision-firewall\
python -m venv .venv\
# Activate .venv for your shell, then:\
python -m pip install -r requirements-core.lock\
python -m pip install --no-deps -e .\
firewall framework --domain refunds demo\
firewall conformance decision_firewall.conformance_fixtures:deployments --save conformance.json

After running it, inspect which exact action the authorization binds and how the execution attempt connects to its receipt. Then read the 17 declared deployment conformance checks: which cases pass, which are unsupported, and what the adapter must guarantee.

For an application of my own, I would turn the lost-response example into a test: restart after downstream success, reconcile the original key, then assert one effect and no remaining reservation once success is resolved. Passing a happy-path refund would not answer that question. Production identity and protection against bypassing the controls still need deployment integration.

October 5 follow-up: checks without the runtime

After publishing this, someone asked whether the conformance checks could run against plain application controls without the runtime. They couldn’t. That exposed a gap in calling the project a conformance kit: running its checks still meant using my framework.

I extracted a small independent package covering stale approval, duplicate effects and recovery after a lost response. In the new development pilot, both the existing application and Decision Firewall passed all three scenarios. Six deliberately injected behavioural faults, three in each implementation, were detected. These are separate results from the earlier paired study.

The package needs no Decision Firewall runtime or model. From a repository checkout, in a Python 3.12 virtual environment, this runs the plain application's simulated example:

Independent checks, simulated application

python -m pip install ./conformance\
python -m control_conformance benchmarks.control_conformance.adapters:application --output report.json

Testing another application still takes work: its test adapter must expose the controls and an independent record of downstream effects. The adapter guide defines that contract. The examples use one simulated export domain; recovery reconstructs state in a fresh interpreter, without testing an operating-system crash. Three scenarios do not establish complete conformance coverage or production safety.

This doesn’t establish that the framework saves work. It makes a smaller contribution available: someone can test these behaviours without adopting my runtime. That feels like a better fit for where the evidence led me.

11 / What I would change next time

Write the comparison before the expansion

Next time I would build one consequential workflow and a competent application comparator before adding the developer platform. I would write down the proposed benefit first, then choose a measurement capable of finding it.

If the claim is easier integration, classification accuracy cannot establish it. If the claim is faster investigation, giving both investigators an answer-bearing export makes a weak test. Several integrations and human participants would be needed to investigate human-productivity benefits.

My useful contribution was pressing those questions, although I asked them late. I started by separating a model’s assessment from permission to act. I ended by separating a useful control from an unproven reason to adopt a framework.

The refund still needs to survive a lost response. The library around that behaviour has to earn its place.

Read the original on Anuj Sadani | Tech

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.