Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,22 @@ jobs:
python3 -c "import yaml, sys; yaml.safe_load(open(sys.argv[1]))" "$f" && echo "OK: $f" || exit 1
done

financial-services:
runs-on: ubuntu-latest
defaults:
run:
working-directory: financial-services
steps:
- uses: actions/checkout@v7

- name: Set up Python 3.11
uses: actions/setup-python@v6
with:
python-version: "3.11"

- name: Run credit engine tests
run: python -m unittest discover -s tests -v

industrial-embodied-ai:
runs-on: ubuntu-latest
defaults:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ End-to-end integration examples showing cMCP, Agent Manifest, and TRACE working
| Example | What it shows | Platform | Compliance |
|---|---|---|---|
| `embodied-action-receipts/` | Fixture-style offline verification for embodied action receipts: accepted chain, missing receipt, signature mismatch and valid controller rejection | Software-only fixtures | TRACE action-receipt evidence boundary |
| `financial-services/` | Credit risk agent: MiFID II escalation deny above EUR 500k with structured policy advice | SEV-SNP / TDX | EU AI Act Art. 9/12, MiFID II Art. 25, DORA Art. 9 |
| `financial-services/` | Corporate credit risk agent: six-step assessment with CDD, exposure and IFRS 9 guardrails on the write | SEV-SNP / TDX | EU AI Act Art. 9/12, CRR Art. 395, EBA/GL/2020/06, EU AML, DORA Art. 9 |
| `healthcare/` | Clinical decision agent: EU AI Act Art. 14 HITL deny on high-risk treatment plans | SEV-SNP / TDX | EU AI Act Art. 14, HIPAA |
| `industrial-embodied-ai/` | Material-movement agent with cMCP authorization, an independent safety-controller boundary and offline-verifiable closed-session evidence | TEE / software-only development mode | OT security and industrial robot safety references |
| `multi-tenant-saas/` | Per-tenant Cedar policy bundles and enforcement modes (advisory vs enforcing) | TDX | GDPR Art. 6, customer contract SLA |
Expand Down
177 changes: 96 additions & 81 deletions financial-services/README.md
Original file line number Diff line number Diff line change
@@ -1,63 +1,67 @@
# financial-services: EU Credit Risk Agent Demo
# financial-services: EU Corporate Credit Risk Agent Demo

End-to-end demo of a credit risk agent processing client financial documents through a cMCP Runtime with Cedar policy enforcement and signed TRACE Trust Records for EU regulatory compliance (EU AI Act, MiFID II, DORA, GDPR).
End-to-end demo of a corporate credit risk agent running a six-step assessment through a cMCP Runtime with Cedar policy enforcement and signed TRACE Trust Records for EU regulatory compliance (EU AI Act, CRR, EBA loan origination, EU AML, IFRS 9, DORA).

The obligor is a fictional German Mittelstand manufacturer. Identifiers are valid in format (LEI check digits per ISO 17442 / ISO 7064 MOD 97-10, German IBANs likewise), the credit bureau is Creditreform on its Bonitätsindex scale, and the guardrails act on the real output of the assessment rather than on a single hard-coded number.

---

## What the demo shows

**1. Cryptographic proof of which tools an AI agent called**
The cMCP Runtime intercepts every MCP tool call, records it in a hash-chained audit log persisted to SQLite, and seals the session into a signed `RuntimeClaim` (the TRACE Trust Record). An auditor can verify after the fact exactly which tools ran, in what order, and what was denied - without trusting the agent process.
The cMCP Runtime intercepts every MCP tool call, records it in a hash-chained audit log persisted to SQLite, and seals the session into a signed `RuntimeClaim` (the TRACE Trust Record). An auditor can verify after the fact exactly which tools ran, in what order, and what was denied, without trusting the agent process.

**2. Cedar policy as machine-readable compliance**
The rules in `policy/allow.cedar` encode the bank's requirements directly. The MiFID II escalation rule blocks any risk report above EUR 500,000 and returns the policy's `@annotation` metadata as structured advice, so the calling system knows *why* and *who must review*.
The rules in `policy/allow.cedar` encode the bank's controls directly. Each forbid returns its `@annotation` metadata as structured advice on deny, so the calling system knows *why* the write was blocked and *who must review*.

**3. EU AI Act Article 12 transparency obligation**
Article 12 requires high-risk AI systems to log sufficient information for post-hoc monitoring. The TRACE record covers the policy version enforced, the audit chain root/tip, per-call decisions, and the runtime attestation - signed by the runtime's key.
**3. Controls that fire on the assessment result, not the request**
The agent screens the client, pulls a bureau report, aggregates group exposure and runs the PD/LGD model, then passes the outcome of those steps (CDD status, IFRS 9 stage, concentration breach, facility amount) into the write call. The Cedar guardrails act on those values, so the deny reflects the actual credit decision.

**4. Attestation-gated data access (DORA Art. 9)**
A Cedar rule forbids confidential (`mnpi`) tools when no attestation evidence is present: confidential financial data only flows through attested runtimes.

---

## Architecture
## The workflow

```
+------------------------------------------------------------------+
| Credit Risk Agent (LLM) |
| agent/credit_risk_agent.py -- JSON-RPC 2.0 over HTTP |
| Credit Risk Agent (agent/credit_risk_agent.py) |
+-------------------------------+----------------------------------+
| tools/call (MCP)
| tools/call (MCP, JSON-RPC 2.0)
v
+------------------------------------------------------------------+
| cMCP Runtime :8443 |
| |
| +---------------+ +------------------+ +------------------+ |
| | Cedar engine | | Catalog checker | | Audit chain + | |
| | allow.cedar | | catalog.json | | TRACE signer | |
| +---------------+ +------------------+ +------------------+ |
| Cedar engine (allow.cedar) | catalog checker | audit + signer |
+-------------------------------+----------------------------------+
| proxied tool call
v
+------------------------------------------------------------------+
| Mock EU Credit Risk MCP Server :8080 |
| server/mock_mcp_server.py |
| finance.document_reader |
| finance.credit_score_lookup |
| finance.risk_report_writer |
| Mock EU Credit Risk MCP Server :8080 |
| 1. finance.document_reader annual financial statements |
| 2. finance.sanctions_screening entity + UBO CDD/AML screen |
| 3. finance.credit_bureau_lookup Creditreform Bonitätsindex |
| 4. finance.exposure_aggregation group exposure vs limit |
| 5. finance.risk_model PD/LGD/EAD, rating, IFRS 9 |
| 6. finance.risk_report_writer write to core banking |
+------------------------------------------------------------------+
```

Tool responses are computed by `credit_engine.py` from a small set of client fixtures, so the server, the tests (`tests/test_credit_engine.py`) and the agent all agree on the same data.

---

## The catalog

`catalog.json` registers three tools with approved definitions, classifications, and definition hashes (`sha256` of the canonical JSON of `approved_definition` - the runtime rejects drifted definitions).
`catalog.json` registers the six tools with approved definitions, classifications, and definition hashes (`sha256` of the canonical JSON of `approved_definition`; the runtime rejects drifted definitions).

| Tool | compliance_domain | sensitivity_level |
|---|---|---|
| `finance.document_reader` | mnpi | confidential |
| `finance.credit_score_lookup` | pii | confidential |
| `finance.sanctions_screening` | pii | confidential |
| `finance.credit_bureau_lookup` | pii | confidential |
| `finance.exposure_aggregation` | internal | confidential |
| `finance.risk_model` | internal | confidential |
| `finance.risk_report_writer` | internal | confidential |

---
Expand All @@ -84,92 +88,93 @@ cd financial-services
CMCP_DEV_MODE=1 cmcp start --config cmcp-config.yaml
```

**Terminal 3 - happy path (EUR 250,000, below the threshold):**
**Terminal 3 - the three scenarios:**

```bash
cd examples
python financial-services/agent/credit_risk_agent.py
```
# A. clean SME, EUR 250k, within limits, performing -> all six steps allow
python agent/credit_risk_agent.py --scenario clean

```
[1/3] Calling finance.document_reader ...
-> decision: allow
[2/3] Calling finance.credit_score_lookup ...
-> decision: allow
[3/3] Calling finance.risk_report_writer ...
-> decision: allow

Closing session <id> and fetching the signed TRACE Trust Record ...
# B. EUR 750k, exceeds delegated authority and breaches the concentration limit
python agent/credit_risk_agent.py --scenario large-exposure

# C. a beneficial owner matches a sanctions list -> CDD does not clear
python agent/credit_risk_agent.py --scenario sanctions-hit
```

**Escalation path (EUR 750,000):**
### Scenario A - clean (`clean`)

```bash
python financial-services/agent/credit_risk_agent.py --amount-eur 750000
```
Rheintal Präzisionstechnik GmbH, EUR 250,000. CDD clears, Creditreform index 178 (very good), aggregate exposure within the limit, IFRS 9 stage 1. All six steps allow and the report is written.

### Scenario B - large exposure (`large-exposure`)

Nordwind Logistik AG, EUR 750,000. The obligor is strong, but the facility exceeds the EUR 500,000 delegated lending authority *and* pushes aggregate group exposure past the single-obligor concentration limit. The write is denied:

```
[3/3] Calling finance.risk_report_writer ...
[6/6] finance.risk_report_writer ...
-> decision: deny (POLICY_DENY)
advice from policy:
id: large-exposure-hitl
id: delegated-authority-hitl
reason: human-review-required
regulation: mifid-ii-art-25
escalation_threshold_eur: 500000
regulation: eba-gl-2020-06
delegated_authority_limit_eur: 500000
```

### Scenario C - sanctions hit (`sanctions-hit`)

The risk report was NOT written to the core banking system.
Meridian Trading DMCC, EUR 200,000. Sanctions screening returns a beneficial-owner match, so `cdd_cleared` stays false. Even though the agent proceeds to the write, the runtime blocks it, so the CDD gate is enforced by the runtime rather than left to the agent's good behaviour:

```
[6/6] finance.risk_report_writer ...
-> decision: deny (POLICY_DENY)
advice from policy:
id: cdd-clearance-required
reason: cdd-clearance-required
regulation: eu-aml-regulation-2024-1624
```

---

## The Cedar policy

`policy/allow.cedar` has no catch-all permit: each tool is explicitly permitted only for the `credit-risk-analyst` workflow (declared by the agent via `_cmcp.workflow_id`). Wrong workflow, missing workflow, or an unlisted action is denied by Cedar's default-deny:

```cedar
permit (
principal,
action == Action::"Finance.documentReader",
resource
) when {
context has workflow_id &&
context.workflow_id == "credit-risk-analyst"
};
```
`policy/allow.cedar` has no catch-all permit: each tool is explicitly permitted only for the `credit-risk-analyst` workflow (declared by the agent via `_cmcp.workflow_id`). On top of the workflow-scoped permits, four guardrails forbid writing the assessment when a control fails:

On top of the workflow-scoped permits, the escalation rule:

```cedar
@id("large-exposure-hitl")
@reason("human-review-required")
@regulation("mifid-ii-art-25")
@escalation_threshold_eur("500000")
forbid (
principal,
action == Action::"Finance.riskReportWriter",
resource
) when {
context.arguments has amount_eur &&
context.arguments.amount_eur > 500000
};
```
| `@id` | Fires when | Regulation |
|---|---|---|
| `cdd-clearance-required` | `cdd_cleared` is not true (sanctions/PEP hit) | EU AML Regulation (EU) 2024/1624 |
| `delegated-authority-hitl` | `amount_eur > 500000` | EBA/GL/2020/06 (loan origination) |
| `large-exposure-concentration` | `breaches_concentration_limit` is true | CRR Art. 395 (large exposures) |
| `credit-impaired-manual-review` | `ifrs9_stage == 3` | IFRS 9 |
| `require-attested-runtime` | `mnpi` tool with no attestation evidence | DORA Art. 9 |

Action names follow the cMCP convention: `finance.risk_report_writer` becomes `Action::"Finance.riskReportWriter"` (the segment before the dot PascalCase, each underscore segment after it camelCase). Tool arguments are available under `context.arguments`; the `@annotation` values are returned in the deny response's `error.data.advice`.

Action names follow the cMCP convention: `finance.risk_report_writer` becomes `Action::"Finance.riskReportWriter"` (PascalCase per underscore segment). Tool arguments are available under `context.arguments`; the `@annotation` values are returned in the deny response's `error.data.advice`.
> On the concentration limit: the CRR Art. 395 regulatory ceiling is 25% of Tier 1 capital, far larger than any single facility here. Banks implement it through a tighter internal single-obligor risk-appetite limit (EUR 2,000,000 in this demo); the runtime enforces the internal limit and cites CRR Art. 395 as the framework it implements.

---

## The TRACE Trust Record
## The TRACE Trust Records

See `trace-output/example-trust-record.json` - captured from a real run. Structure: `{"cmcp_version", "trace": {...}, "gateway": {...}, "signature"}`.
`trace-output/` holds one signed record per scenario, captured from real runs:

| File | Result |
|---|---|
| `clean-trust-record.json` | 6 calls, 6 allowed, 0 denied |
| `large-exposure-trust-record.json` | 6 calls, 5 allowed, 1 denied |
| `sanctions-hit-trust-record.json` | 6 calls, 5 allowed, 1 denied |

Verify one (schema, signature, audit chain, and hashes pass; hardware attestation fails in software-only dev mode):

```bash
cmcp verify trace-output/large-exposure-trust-record.json
```

| TRACE field | EU AI Act | MiFID II | DORA | GDPR |
| TRACE field | EU AI Act | CRR / EBA | DORA | GDPR |
|---|---|---|---|---|
| `trace.policy.bundle_hash` | Art. 9 - risk mgmt system version | Art. 25 - controls documentation | Art. 9 - change management | - |
| `trace.policy.version` | Art. 12 - log versioning | Art. 25 - audit trail | Art. 11 - ICT change log | - |
| `trace.policy.bundle_hash` | Art. 9 - risk mgmt system version | CRR Art. 395 controls | Art. 9 - change management | - |
| `trace.policy.version` | Art. 12 - log versioning | EBA/GL/2020/06 governance | Art. 11 - ICT change log | - |
| `trace.runtime` + `signature` | Art. 12 - tamper-evident logging | - | Art. 9 - security of ICT systems | Art. 32 - security of processing |
| `gateway.call_summary` | Art. 12 - post-hoc review data | Art. 25 - basis of recommendation | Art. 17 - incident management | Art. 5(1)(c) - data minimisation |
| `gateway.call_summary` | Art. 12 - post-hoc review data | EBA/GL/2020/06 basis of decision | Art. 17 - incident management | Art. 5(1)(c) - data minimisation |
| `trace.data_class` | Art. 10 - data governance | - | - | Art. 5(1)(b) - purpose limitation |
| `trace.subject` | Art. 12 - traceability to run | Art. 25 - audit trail | Art. 17 - incident traceability | Art. 5(1)(f) - accountability |

Export the full audit chain for a closed session:

Expand All @@ -179,10 +184,20 @@ curl "http://localhost:8443/audit/export?session_id=<id>" | python3 -m json.tool

---

## The tests

`tests/test_credit_engine.py` exercises the domain logic without a running server: LEI check digits, CDD clear vs hit, the Creditreform scale, exposure aggregation on both sides of the limit, and IFRS 9 staging.

```bash
python -m unittest discover -s tests -v
```

---

## Extending this example

- **Real MCP server:** point `server.url` in `catalog.json` at your endpoint; get the TLS fingerprint with `openssl s_client -connect host:443 | openssl x509 -fingerprint -sha256 -noout`.
- **Change the threshold:** edit the `when` clause and the `@escalation_threshold_eur` annotation together.
- **Change a threshold:** edit the `when` clause and the matching `@annotation` together (e.g. the delegated-authority limit).
- **Hardware attestation:** drop `CMCP_DEV_MODE=1` on a VM with TPM 2.0 / SEV-SNP / TDX.
- **Production hardening:** set `CMCP_BEARER_TOKEN`, `CMCP_POLICY_HASH`, `CMCP_CATALOG_HASH`.

Expand Down
Loading