Skip to content

Commit 09bcd1b

Browse files
feat(healthcare): coherent patient, drug-interaction check, contraindication gate
Deepen the clinical realism of the healthcare example while keeping the FDA/NHS/MOH jurisdiction variants untouched. - Replace the thin canned EHR data with one coherent patient: ICD-10-coded diagnoses, medications with dosing, documented allergies and labs. The clinical_decision_support differential now matches the record. - Add ehr.drug_interaction_check, which screens a proposed treatment against the patient's current medications and allergies. The agent passes its result into the write call. - Add a medication-contraindication forbid: a severe interaction or allergy contraindication blocks the treatment-plan write, alongside the existing EU AI Act Art. 14 high-risk human-oversight rule. - Three scenarios (standard / high-risk / contraindication), each with a signed TRACE record regenerated from a real run. - New clinical_engine module + unit tests and a CI job. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a4dae46 commit 09bcd1b

14 files changed

Lines changed: 832 additions & 216 deletions

.github/workflows/ci.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,22 @@ jobs:
7171
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
7272
run: python validate_artifacts.py
7373

74+
healthcare:
75+
runs-on: ubuntu-latest
76+
defaults:
77+
run:
78+
working-directory: healthcare
79+
steps:
80+
- uses: actions/checkout@v7
81+
82+
- name: Set up Python 3.11
83+
uses: actions/setup-python@v6
84+
with:
85+
python-version: "3.11"
86+
87+
- name: Run clinical engine tests
88+
run: python -m unittest discover -s tests -v
89+
7490
embodied-action-receipts:
7591
runs-on: ubuntu-latest
7692
defaults:

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ End-to-end integration examples showing cMCP, Agent Manifest, and TRACE working
1313
|---|---|---|---|
1414
| `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 |
1515
| `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 |
16-
| `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 |
16+
| `healthcare/` | Clinical agent on a coherent ICD-10 patient: drug-interaction check feeds EU AI Act Art. 14 HITL and contraindication denies | SEV-SNP / TDX | EU AI Act Art. 14, HIPAA |
1717
| `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 |
1818
| `multi-tenant-saas/` | HR SaaS with an EU tenant (enforcing GDPR residency/Art. 9) and a US tenant (advisory) on one catalog | TDX | GDPR Art. 6/9/44, customer DPA |
1919
| `startup-tpm/` | 15-minute quickstart on any cloud VM with Trusted Launch | TPM 2.0 | Development / staging |

healthcare/README.md

Lines changed: 67 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# healthcare: Clinical Decision Support Agent Demo
22

3-
End-to-end demo of a hospital AI agent processing patient records through a cMCP Runtime with Cedar policy enforcement and signed TRACE Trust Records for healthcare regulatory compliance (EU AI Act Art. 14, HIPAA).
3+
End-to-end demo of a hospital AI agent running a clinical assessment through a cMCP Runtime with Cedar policy enforcement and signed TRACE Trust Records for healthcare regulatory compliance (EU AI Act Art. 14, HIPAA).
4+
5+
The worked patient is a fictional 54-year-old with type 2 diabetes and hypertension. Diagnoses carry ICD-10 codes, medications carry dosing, and the agent runs a drug-interaction check whose result feeds the human-oversight guardrails, so a deny reflects the real safety outcome of the plan.
46

57
---
68

@@ -9,14 +11,17 @@ End-to-end demo of a hospital AI agent processing patient records through a cMCP
911
**1. EU AI Act Article 14 - human oversight for high-risk AI**
1012
The Cedar policy blocks any treatment plan write where `patient_risk_category == "high"`. The deny response carries the policy's `@annotation` metadata as structured advice (`regulation: eu-ai-act-art-14`, `reviewer_role: attending-physician`), and the audit chain records the deny as machine-readable Art. 14 evidence.
1113

12-
**2. HIPAA PHI protection at the tool boundary**
13-
All three tools are classified `compliance_domain: hipaa_phi` in the attested catalog. A Cedar rule forbids PHI tools when no attestation evidence is present, enforcing "PHI only flows through attested runtimes" at the policy layer.
14+
**2. A medication-safety guardrail that fires on the assessment result**
15+
The agent runs `ehr.drug_interaction_check` against the patient's current medications and documented allergies, then passes `has_severe_contraindication` into the write call. A Cedar rule blocks the write when a severe contraindication is present, so the guardrail acts on the actual interaction result rather than on a static flag.
16+
17+
**3. HIPAA PHI protection at the tool boundary**
18+
All four tools are classified `compliance_domain: hipaa_phi` in the attested catalog. A Cedar rule forbids PHI tools when no attestation evidence is present, enforcing "PHI only flows through attested runtimes" at the policy layer.
1419

15-
**3. Cryptographic proof of the tool call sequence**
20+
**4. Cryptographic proof of the tool call sequence**
1621
Every call is recorded in a hash-chained audit log persisted to SQLite. Closing the session seals the chain into a signed `RuntimeClaim` (the TRACE Trust Record): which tools ran, in what order, what was denied - verifiable without trusting the agent process.
1722

18-
**4. Two demo paths**
19-
Run without flags for the happy path (all three calls allowed). Run with `--trigger-hitl` to see the Art. 14 block fire with the advice payload.
23+
**5. Three demo scenarios**
24+
`--scenario standard` (all four calls allowed), `--scenario high-risk` (Art. 14 block on a high-risk patient), `--scenario contraindication` (a proposed drug that the patient is allergic to blocks the write).
2025

2126
---
2227

@@ -44,6 +49,7 @@ Run without flags for the happy path (all three calls allowed). Run with `--trig
4449
| server/mock_mcp_server.py |
4550
| ehr.patient_record_lookup |
4651
| ehr.clinical_decision_support |
52+
| ehr.drug_interaction_check |
4753
| ehr.treatment_plan_writer |
4854
+------------------------------------------------------------------+
4955
```
@@ -72,43 +78,45 @@ cd healthcare
7278
CMCP_DEV_MODE=1 cmcp start --config cmcp-config.yaml
7379
```
7480

75-
**Terminal 3 - happy path:**
81+
**Terminal 3 - the three scenarios:**
7682

7783
```bash
7884
cd examples
79-
python healthcare/agent/clinical_decision_agent.py
80-
```
85+
# A. standard: performing plan, no interaction -> all four steps allow
86+
python healthcare/agent/clinical_decision_agent.py --scenario standard
8187

82-
Expected output:
88+
# B. high-risk patient -> Art. 14 human-oversight block on the write
89+
python healthcare/agent/clinical_decision_agent.py --scenario high-risk
8390

91+
# C. a proposed drug the patient is allergic to -> contraindication block
92+
python healthcare/agent/clinical_decision_agent.py --scenario contraindication
8493
```
85-
Patient: P-2024-008471 | Risk category: standard
86-
87-
[1/3] Calling ehr.patient_record_lookup ...
88-
-> decision: allow
89-
[2/3] Calling ehr.clinical_decision_support ...
90-
-> decision: allow
91-
[3/3] Calling ehr.treatment_plan_writer ...
92-
-> decision: allow
9394

94-
Closing session <id> and fetching the signed TRACE Trust Record ...
95+
Standard scenario:
9596

96-
=== TRACE Trust Record (signed RuntimeClaim) ===
97-
{ "cmcp_version": "1.0", "trace": {...}, "gateway": {...}, "signature": "..." }
9897
```
99-
100-
**HITL path:**
101-
102-
```bash
103-
python healthcare/agent/clinical_decision_agent.py --trigger-hitl
98+
Scenario: standard | Patient: P-2024-008471 | Risk category: standard
99+
100+
[1/4] ehr.patient_record_lookup ...
101+
-> decision: allow active dx: E11.9, I10, E78.5
102+
[2/4] ehr.clinical_decision_support ...
103+
-> decision: allow Type 2 diabetes mellitus, suboptimal glycaemic control
104+
[3/4] ehr.drug_interaction_check ...
105+
-> decision: allow highest_severity=none
106+
[4/4] ehr.treatment_plan_writer ...
107+
-> decision: allow
104108
```
105109

110+
Contraindication scenario (the patient has a documented sulfonamide allergy, so proposing co-trimoxazole trips a severe contraindication):
111+
106112
```
107-
[3/3] Calling ehr.treatment_plan_writer ...
113+
[3/4] ehr.drug_interaction_check ...
114+
-> decision: allow highest_severity=severe
115+
[4/4] ehr.treatment_plan_writer ...
108116
-> decision: deny (POLICY_DENY)
109117
advice from policy:
110-
id: hitl-high-risk
111-
reason: human-review-required
118+
id: medication-contraindication
119+
reason: severe-contraindication-detected
112120
regulation: eu-ai-act-art-14
113121
reviewer_role: attending-physician
114122
@@ -133,7 +141,7 @@ permit (
133141
};
134142
```
135143

136-
On top of the workflow-scoped permits sit two forbid rules. Annotations on a `forbid` are returned to the caller as structured advice when that rule causes a deny:
144+
On top of the workflow-scoped permits sit three forbid rules (high-risk human oversight, severe medication contraindication, and the HIPAA attestation gate). Annotations on a `forbid` are returned to the caller as structured advice when that rule causes a deny:
137145

138146
```cedar
139147
@id("hitl-high-risk")
@@ -150,17 +158,34 @@ forbid (
150158
};
151159
```
152160

161+
The second forbid blocks the write when the drug-interaction check returned a severe contraindication:
162+
163+
```cedar
164+
@id("medication-contraindication")
165+
@reason("severe-contraindication-detected")
166+
@regulation("eu-ai-act-art-14")
167+
@reviewer_role("attending-physician")
168+
forbid (
169+
principal,
170+
action == Action::"Ehr.treatmentPlanWriter",
171+
resource
172+
) when {
173+
context.arguments has has_severe_contraindication &&
174+
context.arguments.has_severe_contraindication == true
175+
};
176+
```
177+
153178
Action names follow the cMCP convention: `ehr.treatment_plan_writer` becomes `Action::"Ehr.treatmentPlanWriter"` (PascalCase per underscore segment). Tool arguments are available under `context.arguments`.
154179

155180
---
156181

157182
## The TRACE Trust Record
158183

159-
See `trace-output/example-trust-record.json` - captured from a real run of this demo. Key fields:
184+
`trace-output/` holds one signed record per scenario (`standard-trust-record.json`, `high-risk-trust-record.json`, `contraindication-trust-record.json`), captured from real runs. Verify one with `cmcp verify trace-output/high-risk-trust-record.json` (schema, signature and audit chain pass; hardware attestation fails in software-only dev mode). Key fields:
160185

161186
| Field | Meaning |
162187
|---|---|
163-
| `trace.policy.bundle_hash` / `version` | Exactly which Cedar bundle was enforced (`clinical-hipaa-v2.1`) |
188+
| `trace.policy.bundle_hash` / `version` | Exactly which Cedar bundle was enforced (`clinical-safety-v3.0`) |
164189
| `trace.data_class` | Highest sensitivity touched in the session (`confidential`) |
165190
| `trace.tool_transcript.hash` | Hash of the audit chain tip covering all calls |
166191
| `trace.cnf.jwk` | The runtime's Ed25519 signing key (verifies `signature`) |
@@ -196,6 +221,16 @@ curl "http://localhost:8443/audit/export?session_id=<id>" | python3 -m json.tool
196221

197222
---
198223

224+
## The tests
225+
226+
`tests/test_clinical_engine.py` checks that diagnoses carry ICD-10 codes, that the differential matches the record, that an appropriate second-line agent is safe, and that a sulfonamide allergy and a drug-drug interaction are both detected.
227+
228+
```bash
229+
python -m unittest discover -s tests -v
230+
```
231+
232+
---
233+
199234
## Regulatory Variants
200235

201236
This demo uses EU AI Act Art. 14 and HIPAA as its primary policy example. Additional

healthcare/agent/clinical_decision_agent.py

Lines changed: 80 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,60 @@
22
"""
33
Clinical decision support agent demo for hospital AI compliance.
44
5-
Calls three EHR tools through the cMCP Runtime using JSON-RPC 2.0 over HTTP,
6-
then closes the session to obtain the signed TRACE Trust Record.
5+
Runs a four-step workflow through the cMCP Runtime using JSON-RPC 2.0 over HTTP,
6+
then closes the session to obtain the signed TRACE Trust Record. The agent runs
7+
a drug-interaction check and passes its result into the write call, so the
8+
guardrails act on the real safety outcome.
9+
10+
Scenarios:
11+
12+
--scenario standard Add empagliflozin (second-line), standard risk.
13+
Interaction check clean. All four steps allow.
14+
--scenario high-risk Same plan, patient_risk_category=high. The write
15+
is blocked for human oversight (EU AI Act Art. 14).
16+
--scenario contraindication Propose co-trimoxazole for an incidental UTI.
17+
The patient has a documented sulfonamide allergy,
18+
so the interaction check flags a severe
19+
contraindication and the write is blocked.
720
821
Usage:
9-
python clinical_decision_agent.py [--gateway http://localhost:8443] [--trigger-hitl]
10-
11-
Without --trigger-hitl: patient_risk_category=standard, all tool calls allowed.
12-
With --trigger-hitl: patient_risk_category=high, the treatment plan write is
13-
denied with EU AI Act Art. 14 advice from the Cedar policy.
22+
python clinical_decision_agent.py [--gateway http://localhost:8443]
23+
[--scenario standard|high-risk|contraindication]
1424
"""
1525

1626
import argparse
1727
import json
1828
import sys
1929
import httpx
2030

31+
if hasattr(sys.stdout, "reconfigure"):
32+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
33+
2134
DEFAULT_GATEWAY = "http://localhost:8443"
2235
WORKFLOW_ID = "clinical-decision-support"
23-
2436
PATIENT_ID = "P-2024-008471"
2537
SYMPTOMS = ["fatigue", "polyuria", "polydipsia", "blurred vision"]
26-
LAB_VALUES = {"fasting_glucose_mmol": 9.2, "hba1c_percent": 8.1, "bmi": 31.4}
27-
DIAGNOSIS = "Type 2 Diabetes Mellitus with Hypertension"
28-
TREATMENT = "Metformin 500mg twice daily; lisinopril 10mg once daily; HbA1c recheck in 3 months"
2938

30-
31-
def call_tool(client: httpx.Client, gateway: str, tool_name: str, arguments: dict, req_id: int) -> dict:
32-
"""POST a tools/call. Returns {"ok": bool, "result"/"error", "session_id"}."""
39+
SCENARIOS = {
40+
"standard": {
41+
"risk": "standard",
42+
"proposed": ["empagliflozin"],
43+
"treatment": "Continue metformin 500mg BID; add empagliflozin 10mg once daily; HbA1c recheck in 3 months",
44+
},
45+
"high-risk": {
46+
"risk": "high",
47+
"proposed": ["empagliflozin"],
48+
"treatment": "Continue metformin 500mg BID; add empagliflozin 10mg once daily; HbA1c recheck in 3 months",
49+
},
50+
"contraindication": {
51+
"risk": "standard",
52+
"proposed": ["co-trimoxazole"],
53+
"treatment": "Add co-trimoxazole 960mg BID for 3 days (incidental urinary tract infection)",
54+
},
55+
}
56+
57+
58+
def call_tool(client, gateway, tool_name, arguments, req_id):
3359
payload = {
3460
"jsonrpc": "2.0",
3561
"id": req_id,
@@ -43,20 +69,17 @@ def call_tool(client: httpx.Client, gateway: str, tool_name: str, arguments: dic
4369
resp = client.post(f"{gateway}/mcp", json=payload, timeout=30)
4470
body = resp.json()
4571
if "error" in body:
46-
return {"ok": False, "error": body["error"], "session_id": None}
72+
return {"ok": False, "error": body["error"], "payload": None, "session_id": None}
4773
result = body["result"]
48-
return {
49-
"ok": True,
50-
"result": result,
51-
"session_id": result.get("_cmcp", {}).get("session_id"),
52-
}
74+
payload_text = result.get("content", [{}])[0].get("text", "{}")
75+
return {"ok": True, "payload": json.loads(payload_text),
76+
"session_id": result.get("_cmcp", {}).get("session_id")}
5377

5478

55-
def print_outcome(outcome: dict) -> None:
79+
def print_outcome(step, tool, outcome, note=""):
80+
print(f"[{step}] {tool} ...")
5681
if outcome["ok"]:
57-
meta = outcome["result"].get("_cmcp", {})
58-
decision = "advisory_deny" if meta.get("would_have_denied") else "allow"
59-
print(f" -> decision: {decision}")
82+
print(f" -> decision: allow{(' ' + note) if note else ''}")
6083
else:
6184
data = outcome["error"].get("data", {})
6285
print(f" -> decision: deny ({data.get('error_code', 'unknown')})")
@@ -67,48 +90,58 @@ def print_outcome(outcome: dict) -> None:
6790
print(f" {key}: {value}")
6891

6992

70-
def close_session(client: httpx.Client, gateway: str, session_id: str) -> dict:
93+
def close_session(client, gateway, session_id):
7194
resp = client.post(f"{gateway}/sessions/{session_id}/close", timeout=10)
7295
resp.raise_for_status()
7396
return resp.json()
7497

7598

76-
def run(gateway: str, trigger_hitl: bool) -> None:
77-
risk_category = "high" if trigger_hitl else "standard"
99+
def run(gateway, scenario):
100+
sc = SCENARIOS[scenario]
78101
print(f"Connecting to cMCP Runtime at {gateway}")
79-
print(f"Patient: {PATIENT_ID} | Risk category: {risk_category}")
80-
if trigger_hitl:
81-
print("Mode: --trigger-hitl - the treatment plan write will require HITL approval")
102+
print(f"Scenario: {scenario} | Patient: {PATIENT_ID} | Risk category: {sc['risk']}")
82103
print()
83104

84105
session_id = None
85106
with httpx.Client(headers={"Content-Type": "application/json"}) as client:
86-
print("[1/3] Calling ehr.patient_record_lookup ...")
87107
o1 = call_tool(client, gateway, "ehr.patient_record_lookup",
88108
{"patient_id": PATIENT_ID, "record_type": "full"}, 1)
89-
print_outcome(o1)
109+
note = ""
110+
if o1["ok"]:
111+
dx = ", ".join(d["icd10"] for d in o1["payload"].get("active_diagnoses", []))
112+
note = f"active dx: {dx}"
113+
print_outcome("1/4", "ehr.patient_record_lookup", o1, note)
90114
session_id = o1.get("session_id") or session_id
91115

92-
print("[2/3] Calling ehr.clinical_decision_support ...")
93116
o2 = call_tool(client, gateway, "ehr.clinical_decision_support",
94-
{"patient_id": PATIENT_ID, "presenting_symptoms": SYMPTOMS,
95-
"lab_values": LAB_VALUES}, 2)
96-
print_outcome(o2)
117+
{"patient_id": PATIENT_ID, "presenting_symptoms": SYMPTOMS}, 2)
118+
note = ""
119+
if o2["ok"] and o2["payload"].get("differential"):
120+
note = o2["payload"]["differential"][0]["condition"]
121+
print_outcome("2/4", "ehr.clinical_decision_support", o2, note)
97122
session_id = o2.get("session_id") or session_id
98123

99-
print("[3/3] Calling ehr.treatment_plan_writer ...")
100-
o3 = call_tool(client, gateway, "ehr.treatment_plan_writer",
101-
{"patient_id": PATIENT_ID, "diagnosis": DIAGNOSIS,
102-
"treatment": TREATMENT,
103-
"patient_risk_category": risk_category}, 3)
104-
print_outcome(o3)
124+
o3 = call_tool(client, gateway, "ehr.drug_interaction_check",
125+
{"patient_id": PATIENT_ID, "proposed_medications": sc["proposed"]}, 3)
126+
has_contra = bool(o3["payload"].get("has_severe_contraindication")) if o3["ok"] else False
127+
print_outcome("3/4", "ehr.drug_interaction_check", o3,
128+
f"highest_severity={o3['payload'].get('highest_severity')}" if o3["ok"] else "")
105129
session_id = o3.get("session_id") or session_id
106130

107-
if not o3["ok"]:
131+
o4 = call_tool(client, gateway, "ehr.treatment_plan_writer", {
132+
"patient_id": PATIENT_ID,
133+
"diagnosis": "Type 2 diabetes mellitus with hypertension",
134+
"treatment": sc["treatment"],
135+
"patient_risk_category": sc["risk"],
136+
"has_severe_contraindication": has_contra,
137+
}, 4)
138+
print_outcome("4/4", "ehr.treatment_plan_writer", o4)
139+
session_id = o4.get("session_id") or session_id
140+
141+
if not o4["ok"]:
108142
print()
109143
print(" The treatment plan was NOT written to the EHR.")
110144
print(" An attending physician must review and approve before the plan takes effect.")
111-
print(" The audit chain records this deny for EU AI Act Art. 14 evidence.")
112145

113146
print()
114147
if session_id is None:
@@ -130,7 +163,7 @@ def run(gateway: str, trigger_hitl: bool) -> None:
130163
parser = argparse.ArgumentParser(description="Clinical decision support agent demo")
131164
parser.add_argument("--gateway", default=DEFAULT_GATEWAY,
132165
help=f"cMCP Runtime base URL (default: {DEFAULT_GATEWAY})")
133-
parser.add_argument("--trigger-hitl", action="store_true",
134-
help="Set patient_risk_category=high to trigger the EU AI Act Art. 14 HITL deny")
166+
parser.add_argument("--scenario", default="standard", choices=sorted(SCENARIOS),
167+
help="which clinical scenario to run (default: standard)")
135168
args = parser.parse_args()
136-
run(args.gateway, args.trigger_hitl)
169+
run(args.gateway, args.scenario)

0 commit comments

Comments
 (0)