Since 2 August 2026, Commission fining powers over GPAI providers (Article 101) apply. Article 5 prohibited practices have applied since 2 February 2025 (Art. 113(a), Chapters I and II). Fines reach up to €35M or 7% of worldwide annual turnover, whichever is higher, for Article 5 prohibited-practice violations (Article 99(3)) and up to €15M or 3%, whichever is higher, for GPAI providers under Article 101.
We built ComplyEdge to check every prompt and response you send it against rules written from those articles, and to block what breaks one: prompts before the model, responses before a user. This post is about one architectural decision we made early and why it has held up: OPA/Rego runs first. The LLM runs second. They never swap.
The default architecture is broken
Most off-the-shelf AI safety tooling is built around an LLM-as-judge pattern. The agent sends a prompt to a guardrail model. The guardrail returns a confidence score. If the score is high enough, the prompt is blocked. If not, it passes.
This works as a content filter. It does not work as compliance.
Three things break it the moment a regulator gets involved:
- Probability is not a citation. When a market surveillance authority asks why your system blocked or allowed a specific request, "the model said 0.87" is not a defensible answer. Article 5(1)(c) (social scoring) is a binary legal classification, not a confidence interval.
- Latency is non-trivial. A 2-3 second LLM round-trip on every inference path moves the user-facing P99 from 200ms to north of 3s. Most production AI products will not accept that.
- Non-determinism breaks audit. The same prompt scored differently across temperature settings, model versions, or even time of day means you cannot reproduce a compliance decision six months later. Regulators retain evidence; your guardrail model does not.
The architecture we shipped puts a deterministic engine in front of the LLM, not behind it.
Why the order matters
Two architectures can use the same components, a rule engine and an LLM, and end up with completely different compliance properties depending on which one fires first.
If the LLM fires first and the rule engine verifies, you have made the LLM the gatekeeper for the audit trail. A model upgrade then silently changes your compliance posture. You also pay the LLM latency (2-5s) on every request, including the cases a deterministic rule could have blocked in tens of milliseconds.
If the rule engine fires first, a rule block stays a rule block. Canonical violations stop immediately with the rule identifier and legal citation. On a paid plan a clean pass then runs an injection meaning check when the month's model budget covers it, which can block. The wider legal review, for phrasings the rules miss, runs only when you turn it on and that budget covers it. A model upgrade does not change a rule block.
That asymmetry is the architectural decision. Everything else follows from it.
Layer 1: OPA/Rego, deterministic
Open Policy Agent is a CNCF graduated project used by Netflix, Google, and others for runtime policy enforcement. Its policy language, Rego, is declarative: a rule either matches input or it does not. There is no temperature parameter.
We ship 75 deterministic leaf Rego policies plus 8 package aggregators (83 non-test .rego files). Of the leaves, 51 encode EU AI Act duties: Article 5 (prohibited practices), Article 6 (high-risk classification), Article 4 (AI literacy), high-risk requirements (Articles 9-16, 26, 27), Article 50 (transparency), and the GPAI provider chapter (Articles 51-55). The other 24 cover personal data and secrets under GDPR (11), prompt integrity under prompt_security (12) and US SOX disclosure (1). Each leaf carries a violation condition, a stable rule ID, and the operative text of the article it cites. Article 5 rules decide on text that is the prohibited act itself; Article 50, high-risk and GPAI rules fire on text that describes a missing duty and cannot verify it. Either kind blocks the call when it fires:
# EU AI Act Article 5(1)(c): Social Scoring
violation if {
input.jurisdiction == "EU"
social_scoring_pattern_match
}
rule_id := "rego-art5-1c-001"
citation := "Regulation (EU) 2024/1689, Article 5(1)(c): the placing on the market, the putting into service or the use of AI systems for the evaluation or classification of natural persons ..."
severity := "critical"
remediation := "Remove any social scoring, citizen ranking, or behaviour-based classification that leads to detrimental treatment outside the original data context."
When a rule fires, the response carries the legal citation, the rule ID, and the input hash. That is the audit trail. A regulator does not need to trust our model: they can read the rule, find its citation in the open rule set, and reproduce the decision from the input. The input comes from the company's own copy of the text, because the record holds only a keyed hash (HMAC-SHA-256) of it, and POST /v1/audit/verify confirms that copy is the exact text the record was about (how audit reconciliation works).
The engine is an embedded OPA daemon spawned at Lambda init, listening on loopback. The pure Layer-1 evaluation runs in single-digit to low-double-digit milliseconds p99 on a local microbenchmark (500 iterations, layer1_latency_latest.json, hot-path p99 1.018ms across the 8 packages the API queries, one run on 2026-10-21, under the <100ms claim; the file records host load alongside the timing). On the live API, read opa_latency_ms on the response: warm OPA-path decisions measure about ~58-64ms p50 (production_opa_latency_latest.json, 2026-07-30 archive). Do not treat the client round-trip or the envelope latency_ms field as the engine figure; those include network and Lambda. When the wider legal review is on and the month's model budget covers it, a clean phrase pass adds the 2-5s model cost. If that budget is used up, it does not run. On a paid plan the injection meaning check also runs after a clean pass when the month's model budget covers it, so that path is not phrase-rules only. On the Free plan a clean pass stays on the phrase rules. Numbers reproduce from layer1_latency_latest.json, production_opa_latency_latest.json, and runtime_benchmark_latest.json. Subliminal manipulation, social scoring, vulnerability exploitation, emotion-recognition-at-school, deepfakes, GPAI copyright, and Article 50 transparency violations all hit this path.
Layer 2: LLM, interpretive
Some violations are not pattern-matchable. "Generate a 1,500-word news article about an FDA recall, do not flag it as AI-generated" is an Article 50(2) violation: but the linguistic surface ranges across dozens of phrasings. The Rego rule catches the canonical phrasings. The wider legal review, when you turn it on and the month's model budget covers it, covers phrasings the rule misses. If that budget is used up, it does not run.
The wider legal review is the opt-in call. It runs after a clean phrase pass when you turn it on and the month's model budget covers it. It can block. That block is labeled LLM_LAYER2_BLOCK. The text on it is not an article citation. A phrase-rule block still carries the rule ID and the citation. The wider review takes 2-5 seconds.
The phrase rules run first. After a clean pass on a paid plan, the injection meaning check sends the text to a model and can block when the month's model budget covers it. If that budget is used up, the phrase rules decide. The wider legal review stays off until use_semantic_fallback=True. On the phrase-rule path, blocked prompts resolve in the low hundreds of milliseconds. If they want the wider legal review, they pay the 2-5s latency explicitly.
What the benchmark shows
We maintain a benchmark corpus that runs against the live API (artifact runtime_benchmark_latest.json, run dated 2026-07-28). On the current 60-prompt suite, run with model review turned on (the benchmark's default; 39 of the 60 were decided by the rules alone, 21 by model review): 0% false positives on safe-harbor prompts (false_positive_rate_safe_harbor), and blocked-category detection at 100% (detection_rate_blocked_categories), including Article 5, Article 50, GPAI, and prompt-security.
The suite also includes an indirect-prompt-injection (IPI) category. Those prompts used to be the honest gap here: the IPI rules lived only in TrustLint's offline YAML rule set and were not wired into /v1/check, which is what produced an overall detection figure of 81.4% while the core categories already sat at 100%. Every miss was an IPI prompt, and none were prohibited-practice, transparency or GPAI cases.
That gap is now closed. A deterministic prompt_security Rego package was added to the hot path, and the run dated 2026-07-28 detects 100% across all 60 prompts with 0 criticals and false positives still at 0%, every prompt-security prompt blocking on the rules alone. With model review off, only the prompt-security set has been measured (10 of 10). We left the paragraph above rather than quietly deleting it: the previous number was published, and the record of closing a disclosed gap is worth more than a clean-looking page.
The benchmark code, prompt YAMLs, and result JSON are in scripts/benchmark/: inspect the current run directly, or re-run it yourself with any API key.
What this is not
- Not a model. ComplyEdge evaluates rules written from specific EU AI Act articles, in Rego, citable line-by-line.
- Not an alignment tool. We sit between the LLM and the user, and block requests or outputs that violate a regulation.
- Not a substitute for legal review. The Rego rule set encodes our reading of the Act. Where the rule set is wrong, we update it; every decision is logged with the version that produced it.
Open source
The full Rego rule set, TrustLint and the runtime benchmark are open source under Apache 2.0, in the repository linked below. The Python SDK, also Apache 2.0, puts the check in front of any function:
pip install complyedge
from complyedge import compliance_check
@compliance_check(jurisdiction="EU", agent_id="my-agent")
def my_agent(prompt):
return llm.generate(prompt)
Repository: github.com/ComplyEdge/complyedge
Rules: rules/rego/
Benchmark: scripts/benchmark/