33 - Business Case: Insurance Underwriting (Topology C)
Goal: Demonstrate the Digital Underwriter use case with a full ROA agent (Explain → Policy → Self-Check) backed by LLM. Config-driven via config.yaml (same convention as samples 31, 32, 35). The system commits to the Decision Ledger only when the agent provides a valid cryptographic Evidence Hash proving compliance with the Responsibility Contract.
Day Two prevention: Unverified agent decisions (hallucinations, rule violations, forged proofs) never become binding contracts. The DIM (Proof Checker) recalculates the Evidence Hash using authoritative sources and rejects any mismatch (Zero Trust).
ROA/DIR: DIR Topologies §3 C (DL+PCI) - suited to compliance-heavy operations, high-value transfers, and formal verification where every decision must be cryptographically provable.
Configuration: Underwriting rules, LLM, agent contract, and email_processing (gates, paths, FX stub) live in config.yaml, same convention as samples/31_finance_trading and samples/32_fraud_gate.
Bindable TiV ceiling (max_tiv): run.py and orchestrator.py build UnderwritingContract with max_tiv from agents[0].contract.max_tiv, then underwriting.max_tiv, then code default 2_000_000. The committed sample uses 3,000,000 on the agent contract (see config.yaml).
What run.py does: bootstraps LLM and canonical StorageBundle via samples.shared.bootstrap.setup_environment, performs AgentRegistry.handshake, loads every matching *.md under emails/, runs the email orchestrator (SQLite under database.db_path + HTML report under results/), and opens the new report in the browser.
Email pipeline: flow and what is tested
Each markdown fixture is one DecisionFlow with its own DFID, from ingest through optional mock policy bind.
Order of steps
- Ingest —
email_fixture_ingestloads the markdown and builds a coarseClientApplication(subject, body, industry hints, revenue proxy from table TiV × FX). Broker TiV for authority is not taken from these table regexes; the kernel waits for LLM extraction (requested_tiv_usdis filled after extraction). - Pre-agent gates — Only configurable
injection_patternson the raw email body (default config: empty). The audit/timeline eventKERNEL_GATES_PASSEDmeans this step only; territory andmax_tivare not evaluated until after extraction (step 4). - Submission extraction (LLM) —
BROKER_REQUESTED_TIV_USD+STATED_TERRITORIES. With MockLLM, TiV is derived from the pipe-table row whose first cell is Total Insurable Values (**Total: GBP …**or a bold**USD …**amount in that row). Failure here ends asREJECTED/EXTRACTION_FAILED. - Post-extraction gates (deterministic) — On agent-extracted territory text vs
prohibited_territories, then TiV vscontract.max_tiv: - Both territory and authority fail →
CONTRACT_VIOLATION(REJECTED). - Only prohibited geography →
PROHIBITED_TERRITORY(REJECTED). - Only TiV above
max_tiv→AUTHORITY_CEILING(ESCALATED, not bound). - If gates pass → Explain → Policy → Self-Check → PCI → DIM → ledger → mock bind →
BOUND/POLICY_BOUND.
Decision-path logs use log_with_dfid where a DFID exists. Telemetry rows include simulation_id (from simulation.run_id in config.yaml) for run grouping. Default SQLite path: data/underwriting.dir.sqlite (see database in config.yaml). Override with UNDERWRITING_AUDIT_DB (sets database.db_path before bootstrap).
London Market–style fixtures (included, fictional names & URLs)
File (under emails/) |
What it exercises | Typical final status | reason_code |
|---|---|---|---|
(EXT) Beaconmere Advisory Ltd - Standard Renewal.md |
TiV under max_tiv, UK-only wording |
BOUND | POLICY_BOUND |
(EXT) Cryowest Distribution Ltd - High Limit Facility.md |
TiV far above max_tiv, allowed geographies |
ESCALATED | AUTHORITY_CEILING |
(EXT) Nexora Commodities FZE - MENA Property Enquiry.md |
Syria/Damascus in extraction + TiV above delegated max_tiv |
REJECTED | CONTRACT_VIOLATION (combined message) |
(EXT) Crimson Lane Retail Ltd - Renewal Broker Notes.md |
Misleading “UK-only” workflow note; embedded prompt-injection (MIME/TNEF-style junk) asking to bypass rules; factual schedule still shows Damascus, Syria + TiV above max_tiv |
REJECTED | CONTRACT_VIOLATION |
Exact LLM wording can vary with a real model; with MockLLM the outcomes above are stable. Fixtures are processed in alphabetical order by filename.
How to Run
From the repository root:
# 1. Install dependencies (includes PyYAML + markdown for formatted HTML report)
pip install -e ".[samples]"
# 2. Email pipeline (default) — MockLLM, no Ollama
USE_MOCK_LLM=1 python samples/33_insurance_underwriting/run.py
If you install without extras, add pip install markdown so the report renders broker tables and body as HTML instead of a plain <pre> fallback.
With Ollama (real LLM):
ollama serve
ollama pull gemma3:4b
python samples/33_insurance_underwriting/run.py
Audit database path (optional):
set UNDERWRITING_AUDIT_DB=D:\tmp\uw_audit.sqlite
python samples/33_insurance_underwriting/run.py
Each run appends DFID-tagged rows to SQLite, logs a per-email timeline, and writes a new report under results/report_YYYY-MM-DD_HHMM_emails.html (UTC).
Configuration (config.yaml)
All underwriting rules, LLM, agent, and email_processing live in config.yaml. The block below matches the sample layout (comments may differ slightly in the repo file).
database:
provider: sqlite
db_path: "data/underwriting.dir.sqlite"
simulation:
run_id: "uw_email_batch_001"
underwriting:
max_tiv: 2000000
prohibited_industries: ["Fireworks", "CryptoMining"]
llm_defaults:
model: "gemma3:4b"
base_url: "http://localhost:11434"
timeout: 60
email_processing:
emails_dir: "emails"
currency_fx_to_usd: { GBP: 1.0, USD: 1.0, EUR: 1.0 }
prohibited_territories: ["syrian arab republic", "syria", "damascus"]
injection_patterns: []
agents:
- agent_id: "underwriter_agent"
version: "1.0.0"
priority: 10
contract:
role: EXECUTOR
mission: "You are an insurance underwriter..."
authorized_instruments: []
allowed_policy_types: ["BIND", "DECLINE"]
escalate_on_uncertainty: 0.65
max_drawdown_limit: 0.05
wake_up_threshold_pct: 0.5
parent_agent_id: null
max_tiv: 3000000
prohibited_industries: ["Fireworks", "CryptoMining"]
| Section | Purpose |
|---|---|
| database | Canonical StorageBundle SQLite path (anchored to config.yaml directory) |
| simulation | run_id — propagated as simulation_id on audit rows |
| contracts | Optional — defaults to the same YAML path passed to setup_environment |
| underwriting | max_tiv, prohibited_industries — defaults when building domain contract helpers |
| llm_defaults | Ollama or mock (USE_MOCK_LLM=1, or unreachable live LLM falls back to mock) |
| agents | DIR ResponsibilityContract fields under contract, plus underwriting max_tiv |
| email_processing | emails_dir, currency_fx_to_usd, prohibited_territories, injection_patterns |
email_processing (default path)
| Key | Purpose |
|---|---|
emails_dir |
Subfolder of the sample with markdown email fixtures |
| (persistence) | Use database.db_path — canonical decision_audit_events and idempotency tables |
currency_fx_to_usd |
Rates for MockLLM / helpers when turning table TiV into USD (demo uses 1.0 for GBP fixtures) |
prohibited_territories |
Case-insensitive substrings vs agent-extracted territories after extraction. If both territory and max_tiv fail, the kernel returns CONTRACT_VIOLATION; territory-only → PROHIBITED_TERRITORY. |
injection_patterns |
Optional substrings for ABORTED (PROMPT_INJECTION) before any LLM call; default config leaves this empty so injection is handled by extraction contract + kernel |
When Explain / Policy / PCI appear (DIR)
- Submission-facts extraction (broker TiV + stated territories) is a User Space LLM step; the kernel then applies deterministic post-extraction gates (
prohibited_territories,max_tiv).injection_patternsrun earlier, before the first LLM call. - Explain → Policy → Self-Check → PCI runs only if those gates pass. If the flow stops at a gate, there is no
PolicyProposal/ PCI yet—only extracted facts. The HTML report shows Agent: submission facts extraction only in that case. PolicyProposalis the agent’s structured output, serialized as JSON in the PCIintent_payload. It includes binding fields (total_insured_value,premium,industry) and observability fields (justification,confidenceper DIR-minified §3.9). In this sample, Evidence_Hash is computed from the binding subset only; justification and confidence are still stored in the PCI for audit and reporting.
Architecture
Diagram 1: System overview
emails/*.md is ingested by orchestrator.py (contract and gates from config.yaml).
---
config:
layout: elk
---
flowchart TB
CFG["config.yaml"]
subgraph DEF["Email orchestrator"]
MD["emails/*.md"]
PL["orchestrator.py — ingest, gates, extract, ROA, DIM, bind"]
MD --> PL
end
subgraph US["USER SPACE"]
ROA["ROAUnderwriterAgent"]
end
WALL{{"THE WALL"}}
subgraph KS["KERNEL SPACE"]
REG["AgentRegistry"]
CS["ContextStore"]
DIM["DIM"]
LEDGER["DecisionLedger"]
end
CFG --> REG
CFG --> PL
PL --> ROA
ROA -->|"PCI + evidence_hash"| WALL
WALL --> DIM
REG -->|contract_hash| DIM
CS -->|context_hash| DIM
DIM -->|verified| LEDGER
style US fill:#fffde7,stroke:#f9a825,color:#333
style KS fill:#e8f5e9,stroke:#388e3c,color:#333
style WALL fill:#37474f,color:#fff
style DEF fill:#e3f2fd,stroke:#1565c0,color:#0d47a1
Diagram 2: One email DecisionFlow (happy path)
If a post-extraction gate fails, the flow stops before Explain / Policy / PCI.
sequenceDiagram
participant P as orchestrator
participant A as ROAUnderwriterAgent
participant D as DIM
participant L as DecisionLedger
P->>P: parse markdown → ClientApplication
P->>P: pre-agent gates (injection_patterns)
P->>A: extract_submission_facts (TiV + territories)
P->>P: post-extraction gates (territory, max_tiv)
alt gates OK
P->>A: run_decision_cycle → Explain, Policy, Self-Check, PCI
A->>D: PCI
D->>D: hash + business rules (industry, TiV vs max_tiv)
D->>L: append if Policy Bound
P->>P: mock bind API
end
Components
Kernel Space
| Component | Purpose |
|---|---|
| AgentRegistry | Stores the Underwriting Policy (Responsibility Contract): delegated max_tiv (from config; 3,000,000 in the committed sample), prohibited industries from contract |
| ContextStore | Holds the Client Application state (business_type, revenue, industry) |
| DecisionLedger | Append-only list storing only verified decisions |
| DecisionIntegrityModule (DIM) | Proof Checker: recalculates Evidence Hash, rejects on mismatch (Zero Trust) |
User Space
| Component | Purpose |
|---|---|
| ROAUnderwriterAgent | Full ROA agent: Explain(LLM) → Policy(LLM) → Self-Check → PCI with evidence_hash |
ROA Lifecycle (Explain → Policy → Self-Check)
- Explain: LLM interprets client application (narrative, signals, risks, opportunities)
- Policy: LLM proposes TOTAL_INSURED_VALUE (TiV), PREMIUM, INDUSTRY (structured output)
- Self-Check: Deterministic check (prohibited industry, TiV vs
max_tiv): agent may still emit; DIM enforces - PCI: Build ProofCarryingIntent with evidence_hash, submit to DIM
Evidence Hash Formula
Evidence_Hash = SHA256(DFID || Context_Hash || Contract_Hash || Proposal_Params)
- Context_Hash = SHA256(canonical JSON of ClientApplication)
- Contract_Hash = SHA256(canonical JSON of UnderwritingContract)
- Proposal_Params = canonical JSON of policy_proposal (total_insured_value, premium, industry)
The DIM recalculates this using authoritative Registry and ContextStore data. It never trusts the agent's claimed hash.
HTML report
Each run writes results/report_YYYY-MM-DD_HHMM_emails.html (UTC): per-email rendered markdown body, processing timeline (ingest → gates → extraction → ROA if any → DIM → bind), outcome and reason_code.
Install markdown (or pip install -e ".[samples]") so pipe tables and bold render cleanly instead of a plain <pre> fallback.
File Structure
samples/33_insurance_underwriting/
├── README.md
├── __init__.py
├── config.yaml # database, simulation, contracts, LLM, agents, email_processing
├── run.py # Bootstrap, handshake, orchestrator, report
├── orchestrator.py # DFID per email, gates, ROA, DIM, mock bind, telemetry
├── telemetry.py # SIMULATION_START/END and underwriting audit helpers
├── agent.py # ROAUnderwriterAgent: Explain → Policy → Self-Check → PCI
├── schemas.py # UnderwritingContract, ClientApplication, PolicyProposal
├── email_fixture_ingest.py
├── gates.py
├── kernel.py # DecisionIntegrityModule, PCI helpers
├── policy_binding.py
├── mocks/
│ ├── __init__.py
│ └── llm_mock_strategy.py
├── emails/
├── data/ # gitignored — SQLite from database.db_path
├── results/ # gitignored — report_*.html
└── report_generator.py
Expected console output
Digital Underwriter — email orchestrator (Topology C + mock bind)- For each processed file: a
[Email] …section withDFID, step timeline, thenFinal: BOUND (POLICY_BOUND)/ESCALATED (AUTHORITY_CEILING)/REJECTED (...) Summary: ledger count (verified binds only), audit DB path, path toresults/report_*_emails.html- With mock LLM:
Using MockLLMClient(from bootstrap) andContract loaded: ...
The browser opens the generated HTML report automatically.
Example runs
Default (excerpt):
======================================================================
Digital Underwriter - Email pipeline (Topology C + mock bind)
======================================================================
[Email] (EXT) Beaconmere Advisory Ltd - Standard Renewal.md
DFID: ...
-> ... -> BIND_SUCCEEDED: CLOSED - ...
Final: BOUND (POLICY_BOUND)
Policy ref: POL-...
======================================================================
Summary
======================================================================
Ledger entries (verified only): 1
...
HTML report: .../results/simulation_report_YYYY-MM-DD_HHMM.html
Technical notes
- Imports:
run.pyprepends the reposrc/directory tosys.pathsodir_core.utils.config_loaderanddir_coreresolve when you runpython samples/33_insurance_underwriting/run.pywithout an editable install;pip install -e ".[samples]"is still recommended. - Real LLM: Ollama + model from
llm_defaults(e.g. Gemma). SetUSE_MOCK_LLM=1for deterministic runs without Ollama. - Env (see
run.pydocstring):USE_MOCK_LLM,UNDERWRITING_AUDIT_DB,LOG_LEVEL(e.g.DEBUG). - Zero API keys when using local Ollama only.
- Reports: Each run creates a new timestamped file under
results/.
Summary
| Aspect | Description |
|---|---|
| Topology | C (Decision Ledger & Proof-Carrying Intents) |
| Use case | Digital Underwriter: TiV-aware proposals with cryptographic proof of compliance |
| Agent | Full ROA: Explain(LLM) → Policy(LLM) → Self-Check → PCI |
| Config | config.yaml: underwriting, llm_defaults, agents, email_processing |
| Input | Markdown emails under emails/ + contract / gates from config |
| Output | BOUND / ESCALATED / REJECTED with codes such as POLICY_BOUND, AUTHORITY_CEILING, CONTRACT_VIOLATION, PROHIBITED_TERRITORY, EXTRACTION_FAILED, or DIM strings (e.g. TIV Exceeds Contract Max, Prohibited Industry, Evidence Invalid) if the flow reaches DIM |
| Logic | email_fixture_ingest → optional pre-LLM injection_patterns scan → LLM extraction → post-extraction territory / max_tiv gates → optional full ROA → DIM → ledger → policy_binding mock bind |
| Goal | Day Two prevention: unverified decisions do not reach the ledger or bind API |