32 — Fraud Gate (YAML scenarios)
Goal: This reference sample shows a classic DIR gate where each payment scenario is driven from scenarios.yaml, the fraud analyst follows the full ROA lifecycle (Explain, Policy, deterministic Self-Check, PolicyProposal), and the DIM (validate_proposal) authorizes or blocks execution—including a JIT snapshot vs live check using verify_drift from dir_core so a post-decision risk flag (TOCTOU) cannot settle an outdated ALLOW. AgentRegistry handshake, ContextStore, StorageBundle audit, and idempotent mock settlement are wired through shared.bootstrap.setup_environment exactly as the Sample Development Guide requires.
Mechanisms: AgentRegistry, ContextStore, ROA, validate_proposal, custom DIM validators (verify_drift, hard limit), AuditStore idempotency cache, decision_audit_events, context_session.
Repository layout (what lives where)
| Layer | Location | Role |
|---|---|---|
| DIR kernel | src/dir_core/ (installed as dir-runtime) |
new_dfid, validate_proposal, verify_drift, PolicyProposal, AgentRegistry, ContextStore, storage protocols |
| Sample adapters | samples/shared/ |
setup_environment, load_yaml_config, LLM clients, StorageBundle wiring |
| Business logic (this sample) | agent.py, dim.py, schemas.py |
ROA cycle, DIM custom validators, YAML types and load_scenarios() |
| Where results go | SQLite under data/ (from config.yaml), via telemetry.py |
Append-only decision_audit_events, idempotency_cache, context_session, agent_registry |
| HTML audit report | report_generator.py, results/*.html (gitignored) |
Offline report from canonical telemetry (Sample Development Guide §17) |
| Mocks (no production deps) | mocks/ |
In-memory risk rows, deterministic LLM strategy, fake PSP settlement — see mocks/README.md |
Use cases
---
title: Fraud gate — who does what
config:
layout: elk
theme: neutral
look: classic
---
flowchart TB
subgraph Actors["Actors"]
Customer["Cardholder / merchant"]
RiskOps["Risk operations"]
end
subgraph System["DIR sample"]
Gate["Fraud ROA agent + DIM"]
Store["SQLite StorageBundle"]
end
Customer -->|"payment attempt"| Gate
RiskOps -->|"account flag (drift demo)"| Gate
Gate -->|"audit + idempotent settle"| Store
classDef userSpace fill:#E8EAF6,stroke:#3F51B5,stroke-width:2px,color:#1A237E,font-weight:bold;
classDef kernelSpace fill:#E8F5E9,stroke:#388E3C,stroke-width:2px,color:#1B5E20,font-weight:bold;
class Customer,RiskOps userSpace;
class Gate,Store kernelSpace;
Architecture
---
title: Fraud gate — User Space vs Kernel Space
config:
layout: elk
theme: neutral
look: classic
---
flowchart TB
subgraph US["User Space"]
LLM["LLMClient (Ollama / Gemini / Mock)"]
Agent["Fraud ROA cycle"]
LLM --> Agent
Agent -->|"PolicyProposal"| Wall
end
Wall{{"The Wall — DIM + JIT"}}
subgraph KS["Kernel Space"]
DIM["validate_proposal"]
JIT["verify_drift + hard limit"]
Exec["Idempotent settlement log"]
DIM --> JIT
JIT --> Exec
end
Wall --> DIM
subgraph Persist["Persistence"]
AR["agent_registry"]
CS["context_session"]
AUD["decision_audit_events"]
IDY["idempotency_cache"]
end
Exec --> AUD
Exec --> IDY
Agent -.-> AR
Agent -.-> CS
classDef userSpace fill:#E8EAF6,stroke:#3F51B5,stroke-width:2px,color:#1A237E,font-weight:bold;
classDef kernelSpace fill:#E8F5E9,stroke:#388E3C,stroke-width:2px,color:#1B5E20,font-weight:bold;
classDef wallStyle fill:#37474F,color:#fff,stroke:#263238,stroke-width:2px;
classDef infraSpace fill:#FFF3E0,stroke:#F57C00,stroke-width:2px,color:#E65100,font-weight:bold;
class LLM,Agent userSpace;
class DIM,JIT,Exec kernelSpace;
class Wall wallStyle;
class AR,CS,AUD,IDY infraSpace;
Execution flow
---
title: One scenario end-to-end
config:
layout: elk
theme: neutral
look: classic
---
sequenceDiagram
participant Run as run.py
participant Boot as setup_environment
participant Reg as AgentRegistry
participant Store as ContextStore
participant LLM as LLMClient
participant Agent as ROA cycle
participant DIM as validate_proposal
participant Aud as AuditStore
Run->>Boot: load config + StorageBundle + LLM
Run->>Reg: handshake(agent_id, contract)
Run->>Store: update_session(dfid, transaction)
Run->>LLM: Explain prompt
LLM-->>Agent: explain JSON
Run->>LLM: Policy prompt
LLM-->>Agent: policy JSON
Agent->>Agent: Self-Check vs contract
Agent-->>Run: PolicyProposal
Run->>DIM: proposal + snapshot_user + live_risk
DIM-->>Run: ACCEPT or REJECT
Run->>Aud: AGENT_DECISION (+ PAYMENT on ALLOW)
How to run
From the repository root (after pip install -e . and pip install pyyaml; add psycopg2-binary only if you switch the sample to PostgreSQL):
Ollama (local)
ollama serve
ollama pull gemma3:4b
python samples/32_fraud_gate/run.py
Gemini (cloud)
Set GOOGLE_API_KEY or GEMINI_API_KEY, then set in config.yaml under llm_defaults a gemini-… model and provider: gemini (see samples/31_finance_trading/config.yaml for the pattern).
Mock (no API keys, no network)
USE_MOCK_LLM=1 python samples/32_fraud_gate/run.py
If Ollama or Gemini is selected but unreachable or misconfigured, run.py rebuilds the LLM with force_mock=True so the scenario sweep still finishes deterministically.
Configuration
Annotated excerpts (full files are config.yaml and scenarios.yaml next to run.py).
database—provider: sqliteanddb_pathanchored to this folder viaconfig_path(seesamples/shared/bootstrap.py).simulation.run_id— becomessimulation_idon every audit row so runs are groupable in SQL.llm_defaults— Ollama or Gemini defaults; mock is driven byUSE_MOCK_LLM=1orprovider: mock.contracts.provider: yaml— contracts load from the sameconfig.yaml(agentslist).agents[].contract— canonicalResponsibilityContractincludingallowed_policy_types: [ALLOW, BLOCK, CHALLENGE]andescalate_on_uncertaintyfor the Self-Check stage.jit_validator.global_max_limit— hard cap passed into DIM asglobal_max_limitin the validation context.fraud_gate.fallback_rules— shared thresholds for deterministic fallback parsing and formocks/llm_mock_strategy.py(make_mock_strategy) when the mock client is active.
Database storage
| Canonical table | Written by | Content |
|---|---|---|
agent_registry |
AgentRegistry.handshake |
Contract JSON, priority, session token |
context_session |
ContextStore.update_session |
DFID-scoped transaction + scenario label |
decision_audit_events |
bundle.decision_audit via AuditStore |
SIMULATION_START, SIMULATION_END, AGENT_DECISION, PAYMENT_GATEWAY_ALLOW |
idempotency_cache |
AuditStore.save_idempotent_result |
SHA-256 key for mock settlement replay |
SQLite — filter one run
SELECT dfid, event, detail_json
FROM decision_audit_events
WHERE json_extract(detail_json, '$.simulation_id') = 'fraud_gate_demo'
ORDER BY id;
PostgreSQL — same filter
SELECT dfid, event, detail_json
FROM decision_audit_events
WHERE detail_json->>'simulation_id' = 'fraud_gate_demo'
ORDER BY id;
Expected output
With USE_MOCK_LLM=1 you should see, for each scenario, DFID-prefixed lines from log_with_dfid: two LLM round-trips (Explain then Policy), then DIM: ACCEPT … or DIM: REJECT …, then either PaymentGateway (mock): ALLOW … cached=False on a successful idempotent first settlement, PaymentGateway (mock): BLOCK … when the policy is not ALLOW, or No payment execution when DIM rejects (drift scenario). The run ends with SUMMARY: finished 3 scenarios (simulation_id=fraud_gate_demo) and a Report: …/results/report_<UTC>_<slug>.html line.
Regenerating the HTML report
Each run appends audit rows to the SQLite file configured in config.yaml. run.py generates a self-contained dark-theme HTML report under results/ (Sample Development Guide §17) using only canonical telemetry plus registry and context snapshots. The generator isolates the latest SIMULATION_START … SIMULATION_END window for the chosen simulation_id, so reruns with the same simulation.run_id do not mix older scenarios into the report.
Regenerate without executing the sample:
python samples/32_fraud_gate/report_generator.py --simulation-id fraud_gate_demo
Omit --simulation-id to use the most recent SIMULATION_START in the database. Optional: --output-path path/to/report.html and --config path/to/config.yaml.