diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 42c31f7..85ea68d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,6 +71,22 @@ jobs: if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository run: python validate_artifacts.py + healthcare: + runs-on: ubuntu-latest + defaults: + run: + working-directory: healthcare + steps: + - uses: actions/checkout@v7 + + - name: Set up Python 3.11 + uses: actions/setup-python@v6 + with: + python-version: "3.11" + + - name: Run clinical engine tests + run: python -m unittest discover -s tests -v + embodied-action-receipts: runs-on: ubuntu-latest defaults: diff --git a/README.md b/README.md index 398f162..134328f 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ End-to-end integration examples showing cMCP, Agent Manifest, and TRACE working |---|---|---|---| | `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/` | 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 | +| `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 | | `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/` | 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 | | `startup-tpm/` | 15-minute quickstart on any cloud VM with Trusted Launch | TPM 2.0 | Development / staging | diff --git a/healthcare/README.md b/healthcare/README.md index 02c4505..9c896bd 100644 --- a/healthcare/README.md +++ b/healthcare/README.md @@ -1,6 +1,8 @@ # healthcare: Clinical Decision Support Agent Demo -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). +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). + +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. --- @@ -9,14 +11,17 @@ End-to-end demo of a hospital AI agent processing patient records through a cMCP **1. EU AI Act Article 14 - human oversight for high-risk AI** 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. -**2. HIPAA PHI protection at the tool boundary** -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. +**2. A medication-safety guardrail that fires on the assessment result** +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. + +**3. HIPAA PHI protection at the tool boundary** +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. -**3. Cryptographic proof of the tool call sequence** +**4. Cryptographic proof of the tool call sequence** 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. -**4. Two demo paths** -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. +**5. Three demo scenarios** +`--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). --- @@ -44,6 +49,7 @@ Run without flags for the happy path (all three calls allowed). Run with `--trig | server/mock_mcp_server.py | | ehr.patient_record_lookup | | ehr.clinical_decision_support | + | ehr.drug_interaction_check | | ehr.treatment_plan_writer | +------------------------------------------------------------------+ ``` @@ -72,43 +78,45 @@ cd healthcare CMCP_DEV_MODE=1 cmcp start --config cmcp-config.yaml ``` -**Terminal 3 - happy path:** +**Terminal 3 - the three scenarios:** ```bash cd examples -python healthcare/agent/clinical_decision_agent.py -``` +# A. standard: performing plan, no interaction -> all four steps allow +python healthcare/agent/clinical_decision_agent.py --scenario standard -Expected output: +# B. high-risk patient -> Art. 14 human-oversight block on the write +python healthcare/agent/clinical_decision_agent.py --scenario high-risk +# C. a proposed drug the patient is allergic to -> contraindication block +python healthcare/agent/clinical_decision_agent.py --scenario contraindication ``` -Patient: P-2024-008471 | Risk category: standard - -[1/3] Calling ehr.patient_record_lookup ... - -> decision: allow -[2/3] Calling ehr.clinical_decision_support ... - -> decision: allow -[3/3] Calling ehr.treatment_plan_writer ... - -> decision: allow -Closing session and fetching the signed TRACE Trust Record ... +Standard scenario: -=== TRACE Trust Record (signed RuntimeClaim) === -{ "cmcp_version": "1.0", "trace": {...}, "gateway": {...}, "signature": "..." } ``` - -**HITL path:** - -```bash -python healthcare/agent/clinical_decision_agent.py --trigger-hitl +Scenario: standard | Patient: [redacted PHI] | Risk category: standard + +[1/4] ehr.patient_record_lookup ... + -> decision: allow active dx: E11.9, I10, E78.5 +[2/4] ehr.clinical_decision_support ... + -> decision: allow Type 2 diabetes mellitus, suboptimal glycaemic control +[3/4] ehr.drug_interaction_check ... + -> decision: allow highest_severity=none +[4/4] ehr.treatment_plan_writer ... + -> decision: allow ``` +Contraindication scenario (the patient has a documented sulfonamide allergy, so proposing co-trimoxazole trips a severe contraindication): + ``` -[3/3] Calling ehr.treatment_plan_writer ... +[3/4] ehr.drug_interaction_check ... + -> decision: allow highest_severity=severe +[4/4] ehr.treatment_plan_writer ... -> decision: deny (POLICY_DENY) advice from policy: - id: hitl-high-risk - reason: human-review-required + id: medication-contraindication + reason: severe-contraindication-detected regulation: eu-ai-act-art-14 reviewer_role: attending-physician @@ -133,7 +141,7 @@ permit ( }; ``` -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: +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: ```cedar @id("hitl-high-risk") @@ -150,17 +158,34 @@ forbid ( }; ``` +The second forbid blocks the write when the drug-interaction check returned a severe contraindication: + +```cedar +@id("medication-contraindication") +@reason("severe-contraindication-detected") +@regulation("eu-ai-act-art-14") +@reviewer_role("attending-physician") +forbid ( + principal, + action == Action::"Ehr.treatmentPlanWriter", + resource +) when { + context.arguments has has_severe_contraindication && + context.arguments.has_severe_contraindication == true +}; +``` + 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`. --- ## The TRACE Trust Record -See `trace-output/example-trust-record.json` - captured from a real run of this demo. Key fields: +`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: | Field | Meaning | |---|---| -| `trace.policy.bundle_hash` / `version` | Exactly which Cedar bundle was enforced (`clinical-hipaa-v2.1`) | +| `trace.policy.bundle_hash` / `version` | Exactly which Cedar bundle was enforced (`clinical-safety-v3.0`) | | `trace.data_class` | Highest sensitivity touched in the session (`confidential`) | | `trace.tool_transcript.hash` | Hash of the audit chain tip covering all calls | | `trace.cnf.jwk` | The runtime's Ed25519 signing key (verifies `signature`) | @@ -196,6 +221,16 @@ curl "http://localhost:8443/audit/export?session_id=" | python3 -m json.tool --- +## The tests + +`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. + +```bash +python -m unittest discover -s tests -v +``` + +--- + ## Regulatory Variants This demo uses EU AI Act Art. 14 and HIPAA as its primary policy example. Additional diff --git a/healthcare/agent/clinical_decision_agent.py b/healthcare/agent/clinical_decision_agent.py index 6b2caaa..c81e191 100644 --- a/healthcare/agent/clinical_decision_agent.py +++ b/healthcare/agent/clinical_decision_agent.py @@ -2,15 +2,25 @@ """ Clinical decision support agent demo for hospital AI compliance. -Calls three EHR tools through the cMCP Runtime using JSON-RPC 2.0 over HTTP, -then closes the session to obtain the signed TRACE Trust Record. +Runs a four-step workflow through the cMCP Runtime using JSON-RPC 2.0 over HTTP, +then closes the session to obtain the signed TRACE Trust Record. The agent runs +a drug-interaction check and passes its result into the write call, so the +guardrails act on the real safety outcome. + +Scenarios: + + --scenario standard Add empagliflozin (second-line), standard risk. + Interaction check clean. All four steps allow. + --scenario high-risk Same plan, patient_risk_category=high. The write + is blocked for human oversight (EU AI Act Art. 14). + --scenario contraindication Propose co-trimoxazole for an incidental UTI. + The patient has a documented sulfonamide allergy, + so the interaction check flags a severe + contraindication and the write is blocked. Usage: - python clinical_decision_agent.py [--gateway http://localhost:8443] [--trigger-hitl] - -Without --trigger-hitl: patient_risk_category=standard, all tool calls allowed. -With --trigger-hitl: patient_risk_category=high, the treatment plan write is - denied with EU AI Act Art. 14 advice from the Cedar policy. + python clinical_decision_agent.py [--gateway http://localhost:8443] + [--scenario standard|high-risk|contraindication] """ import argparse @@ -18,18 +28,34 @@ import sys import httpx +if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="replace") + DEFAULT_GATEWAY = "http://localhost:8443" WORKFLOW_ID = "clinical-decision-support" - PATIENT_ID = "P-2024-008471" SYMPTOMS = ["fatigue", "polyuria", "polydipsia", "blurred vision"] -LAB_VALUES = {"fasting_glucose_mmol": 9.2, "hba1c_percent": 8.1, "bmi": 31.4} -DIAGNOSIS = "Type 2 Diabetes Mellitus with Hypertension" -TREATMENT = "Metformin 500mg twice daily; lisinopril 10mg once daily; HbA1c recheck in 3 months" - -def call_tool(client: httpx.Client, gateway: str, tool_name: str, arguments: dict, req_id: int) -> dict: - """POST a tools/call. Returns {"ok": bool, "result"/"error", "session_id"}.""" +SCENARIOS = { + "standard": { + "risk": "standard", + "proposed": ["empagliflozin"], + "treatment": "Continue metformin 500mg BID; add empagliflozin 10mg once daily; HbA1c recheck in 3 months", + }, + "high-risk": { + "risk": "high", + "proposed": ["empagliflozin"], + "treatment": "Continue metformin 500mg BID; add empagliflozin 10mg once daily; HbA1c recheck in 3 months", + }, + "contraindication": { + "risk": "standard", + "proposed": ["co-trimoxazole"], + "treatment": "Add co-trimoxazole 960mg BID for 3 days (incidental urinary tract infection)", + }, +} + + +def call_tool(client, gateway, tool_name, arguments, req_id): payload = { "jsonrpc": "2.0", "id": req_id, @@ -43,20 +69,17 @@ def call_tool(client: httpx.Client, gateway: str, tool_name: str, arguments: dic resp = client.post(f"{gateway}/mcp", json=payload, timeout=30) body = resp.json() if "error" in body: - return {"ok": False, "error": body["error"], "session_id": None} + return {"ok": False, "error": body["error"], "payload": None, "session_id": None} result = body["result"] - return { - "ok": True, - "result": result, - "session_id": result.get("_cmcp", {}).get("session_id"), - } + payload_text = result.get("content", [{}])[0].get("text", "{}") + return {"ok": True, "payload": json.loads(payload_text), + "session_id": result.get("_cmcp", {}).get("session_id")} -def print_outcome(outcome: dict) -> None: +def print_outcome(step, tool, outcome, note=""): + print(f"[{step}] {tool} ...") if outcome["ok"]: - meta = outcome["result"].get("_cmcp", {}) - decision = "advisory_deny" if meta.get("would_have_denied") else "allow" - print(f" -> decision: {decision}") + print(f" -> decision: allow{(' ' + note) if note else ''}") else: data = outcome["error"].get("data", {}) print(f" -> decision: deny ({data.get('error_code', 'unknown')})") @@ -67,48 +90,61 @@ def print_outcome(outcome: dict) -> None: print(f" {key}: {value}") -def close_session(client: httpx.Client, gateway: str, session_id: str) -> dict: +def close_session(client, gateway, session_id): resp = client.post(f"{gateway}/sessions/{session_id}/close", timeout=10) resp.raise_for_status() return resp.json() -def run(gateway: str, trigger_hitl: bool) -> None: - risk_category = "high" if trigger_hitl else "standard" +def run(gateway, scenario): + sc = SCENARIOS[scenario] print(f"Connecting to cMCP Runtime at {gateway}") - print(f"Patient: {PATIENT_ID} | Risk category: {risk_category}") - if trigger_hitl: - print("Mode: --trigger-hitl - the treatment plan write will require HITL approval") + # This is a PHI-handling demo, so the patient identifier is deliberately not + # written to stdout in clear text; it travels only in the tool arguments and + # the signed TRACE record (trace.subject). + print(f"Scenario: {scenario} | Patient: [redacted PHI] | Risk category: {sc['risk']}") print() session_id = None with httpx.Client(headers={"Content-Type": "application/json"}) as client: - print("[1/3] Calling ehr.patient_record_lookup ...") o1 = call_tool(client, gateway, "ehr.patient_record_lookup", {"patient_id": PATIENT_ID, "record_type": "full"}, 1) - print_outcome(o1) + note = "" + if o1["ok"]: + dx = ", ".join(d["icd10"] for d in o1["payload"].get("active_diagnoses", [])) + note = f"active dx: {dx}" + print_outcome("1/4", "ehr.patient_record_lookup", o1, note) session_id = o1.get("session_id") or session_id - print("[2/3] Calling ehr.clinical_decision_support ...") o2 = call_tool(client, gateway, "ehr.clinical_decision_support", - {"patient_id": PATIENT_ID, "presenting_symptoms": SYMPTOMS, - "lab_values": LAB_VALUES}, 2) - print_outcome(o2) + {"patient_id": PATIENT_ID, "presenting_symptoms": SYMPTOMS}, 2) + note = "" + if o2["ok"] and o2["payload"].get("differential"): + note = o2["payload"]["differential"][0]["condition"] + print_outcome("2/4", "ehr.clinical_decision_support", o2, note) session_id = o2.get("session_id") or session_id - print("[3/3] Calling ehr.treatment_plan_writer ...") - o3 = call_tool(client, gateway, "ehr.treatment_plan_writer", - {"patient_id": PATIENT_ID, "diagnosis": DIAGNOSIS, - "treatment": TREATMENT, - "patient_risk_category": risk_category}, 3) - print_outcome(o3) + o3 = call_tool(client, gateway, "ehr.drug_interaction_check", + {"patient_id": PATIENT_ID, "proposed_medications": sc["proposed"]}, 3) + has_contra = bool(o3["payload"].get("has_severe_contraindication")) if o3["ok"] else False + print_outcome("3/4", "ehr.drug_interaction_check", o3, + f"highest_severity={o3['payload'].get('highest_severity')}" if o3["ok"] else "") session_id = o3.get("session_id") or session_id - if not o3["ok"]: + o4 = call_tool(client, gateway, "ehr.treatment_plan_writer", { + "patient_id": PATIENT_ID, + "diagnosis": "Type 2 diabetes mellitus with hypertension", + "treatment": sc["treatment"], + "patient_risk_category": sc["risk"], + "has_severe_contraindication": has_contra, + }, 4) + print_outcome("4/4", "ehr.treatment_plan_writer", o4) + session_id = o4.get("session_id") or session_id + + if not o4["ok"]: print() print(" The treatment plan was NOT written to the EHR.") print(" An attending physician must review and approve before the plan takes effect.") - print(" The audit chain records this deny for EU AI Act Art. 14 evidence.") print() if session_id is None: @@ -130,7 +166,7 @@ def run(gateway: str, trigger_hitl: bool) -> None: parser = argparse.ArgumentParser(description="Clinical decision support agent demo") parser.add_argument("--gateway", default=DEFAULT_GATEWAY, help=f"cMCP Runtime base URL (default: {DEFAULT_GATEWAY})") - parser.add_argument("--trigger-hitl", action="store_true", - help="Set patient_risk_category=high to trigger the EU AI Act Art. 14 HITL deny") + parser.add_argument("--scenario", default="standard", choices=sorted(SCENARIOS), + help="which clinical scenario to run (default: standard)") args = parser.parse_args() - run(args.gateway, args.trigger_hitl) + run(args.gateway, args.scenario) diff --git a/healthcare/catalog.json b/healthcare/catalog.json index 9965de8..3a07a0e 100644 --- a/healthcare/catalog.json +++ b/healthcare/catalog.json @@ -12,13 +12,24 @@ "input_schema": { "type": "object", "properties": { - "patient_id": {"type": "string"}, + "patient_id": { + "type": "string" + }, "record_type": { "type": "string", - "enum": ["demographics", "diagnoses", "medications", "labs", "vitals", "full"] + "enum": [ + "demographics", + "diagnoses", + "medications", + "labs", + "vitals", + "full" + ] } }, - "required": ["patient_id"] + "required": [ + "patient_id" + ] } }, "definition_hash": "sha256:8849c45c3aab0a43885a66b44a836578a2e6c5f006154d5cda2bb72ba5c85be3", @@ -41,11 +52,23 @@ "input_schema": { "type": "object", "properties": { - "patient_id": {"type": "string"}, - "presenting_symptoms": {"type": "array", "items": {"type": "string"}}, - "lab_values": {"type": "object"} + "patient_id": { + "type": "string" + }, + "presenting_symptoms": { + "type": "array", + "items": { + "type": "string" + } + }, + "lab_values": { + "type": "object" + } }, - "required": ["patient_id", "presenting_symptoms"] + "required": [ + "patient_id", + "presenting_symptoms" + ] } }, "definition_hash": "sha256:7a4320c8f4f12c1350ab74bbf266510e6a732f6bbb921ac78c717ba5621f5d20", @@ -55,6 +78,42 @@ "added_at": "2026-06-01T00:00:00Z", "approved_by": "compliance@hospital.example" }, + { + "tool_name": "ehr.drug_interaction_check", + "server": { + "display_name": "Hospital EHR MCP Server", + "url": "http://localhost:8080/mcp", + "tls_fingerprint": "SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "transport": "http-sse" + }, + "approved_definition": { + "description": "Check proposed medications against the patient's current medications and documented allergies", + "input_schema": { + "type": "object", + "properties": { + "patient_id": { + "type": "string" + }, + "proposed_medications": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "patient_id", + "proposed_medications" + ] + } + }, + "definition_hash": "sha256:7d68570e326a9dde2ca0d2f1fe65b5be6e7219fa5d29a3ddd0d75d8b382e5acc", + "compliance_domain": "hipaa_phi", + "requires_baa": true, + "sensitivity_level": "confidential", + "added_at": "2026-06-01T00:00:00Z", + "approved_by": "compliance@hospital.example" + }, { "tool_name": "ehr.treatment_plan_writer", "server": { @@ -68,18 +127,35 @@ "input_schema": { "type": "object", "properties": { - "patient_id": {"type": "string"}, - "diagnosis": {"type": "string"}, - "treatment": {"type": "string"}, + "patient_id": { + "type": "string" + }, + "diagnosis": { + "type": "string" + }, + "treatment": { + "type": "string" + }, "patient_risk_category": { "type": "string", - "enum": ["standard", "high"] + "enum": [ + "standard", + "high" + ] + }, + "has_severe_contraindication": { + "type": "boolean" } }, - "required": ["patient_id", "diagnosis", "treatment", "patient_risk_category"] + "required": [ + "patient_id", + "diagnosis", + "treatment", + "patient_risk_category" + ] } }, - "definition_hash": "sha256:a1b8ce368e99d97299d9d2a5b85798ad88204f7da84747d2dae66c50c0e7b8fa", + "definition_hash": "sha256:a2e568165b6a2d3e9dd3f20f4b1df4fe76bc865113dd3007d7ed6a0d265d1e9d", "compliance_domain": "hipaa_phi", "requires_baa": true, "sensitivity_level": "confidential", diff --git a/healthcare/clinical_engine.py b/healthcare/clinical_engine.py new file mode 100644 index 0000000..bc8451c --- /dev/null +++ b/healthcare/clinical_engine.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +""" +Clinical domain logic for the healthcare example. + +Pure, dependency-free functions plus one coherent patient fixture. The mock EHR +server serves from this module, and tests/test_clinical_engine.py exercises the +logic without a running server, so the record, the differential and the +drug-interaction check all agree on the same patient. + +Everything here is illustrative and fictional. Diagnoses carry ICD-10 codes, +medications carry dosing, and the differential is consistent with the record. +""" + +from __future__ import annotations + +from typing import Any + +# One worked patient: a 54-year-old with type 2 diabetes and hypertension whose +# glycaemic control has drifted (HbA1c 8.1%). +PATIENT: dict[str, Any] = { + "patient_id": "P-2024-008471", + "age": 54, + "sex": "female", + "active_diagnoses": [ + {"icd10": "E11.9", "label": "Type 2 diabetes mellitus without complications"}, + {"icd10": "I10", "label": "Essential (primary) hypertension"}, + {"icd10": "E78.5", "label": "Hyperlipidaemia, unspecified"}, + ], + "current_medications": [ + {"name": "metformin", "dose": "500 mg", "frequency": "twice daily"}, + {"name": "lisinopril", "dose": "10 mg", "frequency": "once daily"}, + {"name": "atorvastatin", "dose": "20 mg", "frequency": "once daily"}, + ], + "allergies": [ + {"substance": "penicillin", "reaction": "urticaria"}, + {"substance": "sulfonamides", "reaction": "rash"}, + ], + "labs": { + "hba1c_percent": 8.1, + "fasting_glucose_mmol": 9.2, + "egfr_ml_min": 78, + "ldl_mmol": 2.9, + "blood_pressure_mmhg": "148/92", + "bmi": 31.4, + }, + "last_visit": "2026-05-28", +} + +# Small illustrative interaction knowledge base. +# Drug classes the patient's allergies contraindicate. +_ALLERGY_CONTRAINDICATIONS = { + "sulfonamides": ["co-trimoxazole", "sulfamethoxazole", "sulfasalazine"], + "penicillin": ["amoxicillin", "ampicillin", "co-amoxiclav", "piperacillin"], +} +# Pairwise drug-drug interactions with the patient's current medications. +_DRUG_DRUG = { + ("lisinopril", "spironolactone"): ("moderate", "additive hyperkalaemia risk"), + ("lisinopril", "potassium chloride"): ("severe", "hyperkalaemia risk"), + ("atorvastatin", "clarithromycin"): ("severe", "increased myopathy/rhabdomyolysis risk"), +} + + +def patient_record(patient_id: str, record_type: str = "full") -> dict[str, Any]: + """Return the patient record, or the requested section of it.""" + p = PATIENT + full = { + "patient_id": patient_id or p["patient_id"], + "age": p["age"], + "sex": p["sex"], + "active_diagnoses": p["active_diagnoses"], + "current_medications": p["current_medications"], + "allergies": p["allergies"], + "labs": p["labs"], + "last_visit": p["last_visit"], + } + sections = { + "demographics": {"patient_id": full["patient_id"], "age": p["age"], "sex": p["sex"]}, + "diagnoses": {"patient_id": full["patient_id"], "active_diagnoses": p["active_diagnoses"]}, + "medications": {"patient_id": full["patient_id"], "current_medications": p["current_medications"]}, + "labs": {"patient_id": full["patient_id"], "labs": p["labs"]}, + "vitals": {"patient_id": full["patient_id"], "blood_pressure_mmhg": p["labs"]["blood_pressure_mmhg"], "bmi": p["labs"]["bmi"]}, + } + out = sections.get(record_type, full) + out["record_type"] = record_type + out["status"] = "retrieved" + return out + + +def clinical_decision_support(patient_id: str, presenting_symptoms: list[str] | None = None) -> dict[str, Any]: + """Differential and recommendations consistent with the patient record.""" + return { + "patient_id": patient_id or PATIENT["patient_id"], + "presenting_symptoms": presenting_symptoms or ["fatigue", "polyuria", "polydipsia"], + "differential": [ + {"condition": "Type 2 diabetes mellitus, suboptimal glycaemic control", + "icd10": "E11.65", "confidence": 0.93}, + {"condition": "Metabolic syndrome", "icd10": "E88.81", "confidence": 0.68}, + ], + "assessment": "HbA1c 8.1% is above the individualised target; first-line metformin " + "alone is insufficient. Consider adding a second-line agent.", + "recommended_actions": [ + "Intensify glycaemic control (add second-line agent)", + "Reinforce lifestyle measures", + "Recheck HbA1c in 3 months", + ], + "status": "completed", + } + + +def drug_interaction_check(patient_id: str, proposed_medications: list[str]) -> dict[str, Any]: + """Check proposed medications against current meds and documented allergies.""" + proposed = [m.strip().lower() for m in (proposed_medications or [])] + allergies = [a["substance"] for a in PATIENT["allergies"]] + current = [m["name"] for m in PATIENT["current_medications"]] + + contraindications: list[dict[str, Any]] = [] + for allergy in allergies: + for drug in _ALLERGY_CONTRAINDICATIONS.get(allergy, []): + if drug in proposed: + contraindications.append({ + "proposed": drug, + "type": "allergy", + "detail": f"contraindicated: documented {allergy} allergy", + "severity": "severe", + }) + + interactions: list[dict[str, Any]] = [] + for cur in current: + for prop in proposed: + hit = _DRUG_DRUG.get((cur, prop)) or _DRUG_DRUG.get((prop, cur)) + if hit: + severity, detail = hit + interactions.append({ + "current": cur, "proposed": prop, + "type": "drug-drug", "detail": detail, "severity": severity, + }) + + flags = contraindications + interactions + has_severe = any(f["severity"] == "severe" for f in flags) + return { + "patient_id": patient_id or PATIENT["patient_id"], + "proposed_medications": proposed, + "contraindications": contraindications, + "interactions": interactions, + "highest_severity": _highest_severity(flags), + "has_severe_contraindication": has_severe, + "status": "completed", + } + + +def _highest_severity(flags: list[dict[str, Any]]) -> str: + order = {"none": 0, "moderate": 1, "severe": 2} + best = "none" + for f in flags: + if order.get(f["severity"], 0) > order[best]: + best = f["severity"] + return best diff --git a/healthcare/policy/allow.cedar b/healthcare/policy/allow.cedar index abb8d23..ac22f33 100644 --- a/healthcare/policy/allow.cedar +++ b/healthcare/policy/allow.cedar @@ -1,50 +1,53 @@ // Cedar policy bundle for hospital clinical decision support agent -// version: clinical-hipaa-v2.1 +// version: clinical-safety-v3.0 // author: compliance@hospital.example // -// Action names follow the cMCP convention: tool_name converted to -// PascalCase per underscore segment, e.g. ehr.treatment_plan_writer -// becomes Action::"Ehr.treatmentPlanWriter". +// Action names follow the cMCP convention: the segment before the dot is +// PascalCase and each underscore segment after it is camelCase, e.g. +// ehr.treatment_plan_writer becomes Action::"Ehr.treatmentPlanWriter". // // There is deliberately NO catch-all permit: anything not explicitly -// permitted below is denied (Cedar default-deny). The workflow_id is -// supplied by the agent via the _cmcp request metadata; calls without -// it are denied. +// permitted below is denied (Cedar default-deny). The workflow_id is supplied +// by the agent via the _cmcp request metadata; calls without it are denied. + +// --- Workflow-scoped permits ------------------------------------------------- -// Rule 1: the clinical-decision-support workflow may look up patient records. permit ( principal, action == Action::"Ehr.patientRecordLookup", resource ) when { - context has workflow_id && - context.workflow_id == "clinical-decision-support" + context has workflow_id && context.workflow_id == "clinical-decision-support" }; -// Rule 2: the clinical-decision-support workflow may run differential diagnosis. permit ( principal, action == Action::"Ehr.clinicalDecisionSupport", resource ) when { - context has workflow_id && - context.workflow_id == "clinical-decision-support" + context has workflow_id && context.workflow_id == "clinical-decision-support" +}; + +permit ( + principal, + action == Action::"Ehr.drugInteractionCheck", + resource +) when { + context has workflow_id && context.workflow_id == "clinical-decision-support" }; -// Rule 3: the clinical-decision-support workflow may write treatment plans. -// Subject to the HITL forbid below. permit ( principal, action == Action::"Ehr.treatmentPlanWriter", resource ) when { - context has workflow_id && - context.workflow_id == "clinical-decision-support" + context has workflow_id && context.workflow_id == "clinical-decision-support" }; -// Rule 4: EU AI Act Article 14 human oversight. Treatment plan writes for -// high-risk patients are blocked until an attending physician approves. -// The annotations are returned to the caller as structured advice on deny. +// --- Human oversight on writing the treatment plan --------------------------- + +// EU AI Act Article 14 human oversight. Treatment plan writes for high-risk +// patients are blocked until an attending physician approves. @id("hitl-high-risk") @reason("human-review-required") @regulation("eu-ai-act-art-14") @@ -58,7 +61,22 @@ forbid ( context.arguments.patient_risk_category == "high" }; -// Rule 5: HIPAA PHI may only flow through an attested runtime. +// A treatment plan carrying a severe drug interaction or allergy +// contraindication cannot be written automatically; it goes to a clinician. +@id("medication-contraindication") +@reason("severe-contraindication-detected") +@regulation("eu-ai-act-art-14") +@reviewer_role("attending-physician") +forbid ( + principal, + action == Action::"Ehr.treatmentPlanWriter", + resource +) when { + context.arguments has has_severe_contraindication && + context.arguments.has_severe_contraindication == true +}; + +// HIPAA PHI may only flow through an attested runtime. @id("require-attested-runtime") @reason("attested-runtime-required") @regulation("hipaa-164-312") @@ -67,6 +85,5 @@ forbid ( action, resource ) when { - context.compliance_domain == "hipaa_phi" && - context.attestation_platform == "unknown" + context.compliance_domain == "hipaa_phi" && context.attestation_platform == "unknown" }; diff --git a/healthcare/policy/manifest.json b/healthcare/policy/manifest.json index 6091d01..dc668d6 100644 --- a/healthcare/policy/manifest.json +++ b/healthcare/policy/manifest.json @@ -1,5 +1,5 @@ { - "version": "clinical-hipaa-v2.1", + "version": "clinical-safety-v3.0", "authored_at": "2026-06-01T00:00:00Z", "author_identity": "compliance@hospital.example", "commit_sha": "0000000000000000000000000000000000000000" diff --git a/healthcare/server/mock_mcp_server.py b/healthcare/server/mock_mcp_server.py index e07c571..0ef4581 100644 --- a/healthcare/server/mock_mcp_server.py +++ b/healthcare/server/mock_mcp_server.py @@ -2,41 +2,40 @@ """ Mock Hospital EHR MCP Server for the healthcare demo. -Serves the three catalog tools with canned responses on port 8080. -Stdlib only -- no dependencies. +Serves the four catalog tools on port 8080. Tool responses come from +``clinical_engine`` so the server, the tests and the agent share one coherent +patient. Stdlib only -- no dependencies. Usage: python healthcare/server/mock_mcp_server.py """ import json +import sys from http.server import BaseHTTPRequestHandler, HTTPServer +from pathlib import Path + +EXAMPLE_DIR = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(EXAMPLE_DIR)) + +import clinical_engine # noqa: E402 PORT = 8080 def _patient_record_lookup(args: dict) -> str: - return json.dumps({ - "patient_id": args.get("patient_id", ""), - "record_type": args.get("record_type", "full"), - "age": 54, - "active_diagnoses": ["essential hypertension"], - "current_medications": ["lisinopril 10mg"], - "last_visit": "2026-05-28", - "status": "retrieved", - }) + return json.dumps(clinical_engine.patient_record( + args.get("patient_id", ""), args.get("record_type", "full"))) def _clinical_decision_support(args: dict) -> str: - return json.dumps({ - "patient_id": args.get("patient_id", ""), - "differential": [ - {"condition": "Type 2 Diabetes Mellitus", "confidence": 0.91}, - {"condition": "Metabolic Syndrome", "confidence": 0.64}, - ], - "recommended_tests": ["oral glucose tolerance test", "lipid panel"], - "status": "completed", - }) + return json.dumps(clinical_engine.clinical_decision_support( + args.get("patient_id", ""), args.get("presenting_symptoms"))) + + +def _drug_interaction_check(args: dict) -> str: + return json.dumps(clinical_engine.drug_interaction_check( + args.get("patient_id", ""), args.get("proposed_medications", []))) def _treatment_plan_writer(args: dict) -> str: @@ -53,6 +52,7 @@ def _treatment_plan_writer(args: dict) -> str: TOOLS = { "ehr.patient_record_lookup": _patient_record_lookup, "ehr.clinical_decision_support": _clinical_decision_support, + "ehr.drug_interaction_check": _drug_interaction_check, "ehr.treatment_plan_writer": _treatment_plan_writer, } diff --git a/healthcare/tests/test_clinical_engine.py b/healthcare/tests/test_clinical_engine.py new file mode 100644 index 0000000..9f8e913 --- /dev/null +++ b/healthcare/tests/test_clinical_engine.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +EXAMPLE_DIR = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(EXAMPLE_DIR)) + +import clinical_engine as ce # noqa: E402 + +PATIENT = "P-2024-008471" + + +class ClinicalEngineTests(unittest.TestCase): + def test_record_diagnoses_carry_icd10(self) -> None: + rec = ce.patient_record(PATIENT, "full") + codes = {d["icd10"] for d in rec["active_diagnoses"]} + self.assertIn("E11.9", codes) # type 2 diabetes + self.assertIn("I10", codes) # hypertension + + def test_record_sections_are_scoped(self) -> None: + meds = ce.patient_record(PATIENT, "medications") + self.assertIn("current_medications", meds) + self.assertNotIn("labs", meds) + + def test_differential_matches_the_record(self) -> None: + cds = ce.clinical_decision_support(PATIENT, ["fatigue"]) + top = cds["differential"][0]["condition"].lower() + self.assertIn("diabetes", top) + + def test_appropriate_second_line_is_safe(self) -> None: + check = ce.drug_interaction_check(PATIENT, ["empagliflozin"]) + self.assertFalse(check["has_severe_contraindication"]) + self.assertEqual(check["highest_severity"], "none") + + def test_sulfonamide_allergy_is_a_severe_contraindication(self) -> None: + check = ce.drug_interaction_check(PATIENT, ["co-trimoxazole"]) + self.assertTrue(check["has_severe_contraindication"]) + self.assertEqual(check["contraindications"][0]["type"], "allergy") + + def test_drug_drug_interaction_is_detected(self) -> None: + check = ce.drug_interaction_check(PATIENT, ["potassium chloride"]) + self.assertTrue(any(i["type"] == "drug-drug" for i in check["interactions"])) + self.assertEqual(check["highest_severity"], "severe") + + +if __name__ == "__main__": + unittest.main() diff --git a/healthcare/trace-output/contraindication-trust-record.json b/healthcare/trace-output/contraindication-trust-record.json new file mode 100644 index 0000000..44b39c5 --- /dev/null +++ b/healthcare/trace-output/contraindication-trust-record.json @@ -0,0 +1,104 @@ +{ + "cmcp_version": "1.0", + "trace": { + "eat_profile": "tag:agentrust.io,2026:trace-v0.1", + "iat": 1784651780, + "subject": "spiffe://cmcp.gateway/tee/9a013bf66a5aa07d", + "runtime": { + "platform": "software-only", + "measurement": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "firmware_version": "software-only-dev-mode" + }, + "policy": { + "bundle_hash": "sha256:44ec5f0c1fcdb7c0108dc3e4f73ec2ec73f02705712656cd2bc718ccc72164c4", + "enforcement_mode": "enforce", + "version": "clinical-safety-v3.0" + }, + "data_class": "confidential", + "tool_transcript": { + "hash": "sha256:8fd7fb6b8abcb83c48b6c3786d55f797a8df5515ebf91e7a868267b08f7b6326", + "call_count": 4, + "entries": [ + { + "tool_name": "ehr.patient_record_lookup", + "data_class": "confidential", + "decision": "allow" + }, + { + "tool_name": "ehr.clinical_decision_support", + "data_class": "confidential", + "decision": "allow" + }, + { + "tool_name": "ehr.drug_interaction_check", + "data_class": "confidential", + "decision": "allow" + }, + { + "tool_name": "ehr.treatment_plan_writer", + "data_class": "confidential", + "decision": "deny" + } + ] + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "mgE79mpaoH14uN7ZMgcitNBkOAy5K9vpQZHwVuu8v8U", + "kid": "cmcp-9a013bf6" + } + } + }, + "gateway": { + "session_id": "1dce4c3e-914f-4210-974d-1f4ff98d11c1", + "gateway_version": "0.3.0", + "sequence_number": 6, + "prev_claim_hash": "sha256:6dcbd4bc631a2b9fe2f328616578cb35101eab6e4fc5f2e69bfead33a1139acd", + "audit_chain": { + "root": "a41e69eb907600e8418f4c6540a527707ea4e2a0ba05a21faec9ecf2eefd3a2b", + "tip": "8fd7fb6b8abcb83c48b6c3786d55f797a8df5515ebf91e7a868267b08f7b6326", + "length": 6 + }, + "call_summary": { + "tool_calls_total": 4, + "tool_calls_allowed": 3, + "tool_calls_denied": 1, + "tool_calls_faulted": 0, + "tools_invoked": [ + "ehr.clinical_decision_support", + "ehr.drug_interaction_check", + "ehr.patient_record_lookup", + "ehr.treatment_plan_writer" + ], + "session_max_sensitivity": "confidential", + "call_graph_summary": { + "compliance_domains_touched": [ + "hipaa_phi" + ], + "cross_boundary_events": [], + "edges_represent": "Edges represent temporal adjacency (call order), not data provenance. A -> B means B was called immediately after A within this session." + } + }, + "catalog": { + "hash": "sha256:174412ac2254484e3fa65a8c0b29495d5be5e2648e4b3b02398a3f0c9b82f3c7", + "drift_detected": false + }, + "attestation_generated_at": "2026-07-21T16:35:29.936660+00:00", + "attestation_validity_seconds": 86400, + "attestation_stale": false, + "catalog_exceptions": [], + "call_log_summary": { + "total_calls": 4, + "tools_called": [ + "ehr.patient_record_lookup", + "ehr.clinical_decision_support", + "ehr.drug_interaction_check", + "ehr.treatment_plan_writer" + ], + "suspicious_sequences_detected": 0 + }, + "kill_switch_triggered": false + }, + "signature": "zdLYpcYSzetbFPhSCjbDjmcPTZcOharv6QNgCN-wtJpeQ2nFM_yDRNiMTGDGkxv5OjCsAGlfuVzVAr2DXITCBA" +} diff --git a/healthcare/trace-output/example-trust-record.json b/healthcare/trace-output/example-trust-record.json deleted file mode 100644 index 396ccc9..0000000 --- a/healthcare/trace-output/example-trust-record.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "cmcp_version": "1.0", - "trace": { - "eat_profile": "tag:agentrust.io,2026:trace-v0.1", - "iat": 1781193632, - "subject": "spiffe://cmcp.gateway/session/f2574e45-58b2-480f-919d-50aa65d07d31", - "runtime": { - "platform": "tpm2", - "measurement": "sha256:0000000000000000000000000000000000000000000000000000000000000000", - "firmware_version": "software-only-dev-mode" - }, - "policy": { - "bundle_hash": "sha256:bab3158a32ccadfecfd69d5edcc4078fcd1f2a0814c4f33069e352754ed69d9c", - "enforcement_mode": "enforce", - "version": "clinical-hipaa-v2.1" - }, - "data_class": "confidential", - "tool_transcript": { - "hash": "sha256:237bef498e84cd658ca00a7673bb114192a110940ed821b44ae4f8c81cf247f5", - "call_count": 3 - }, - "cnf": { - "jwk": { - "kty": "OKP", - "crv": "Ed25519", - "x": "g2zyVqHtTV9CtQBkS4PS971CMd1ilK0CtihOpuy9v14", - "kid": "cmcp-836cf256" - } - } - }, - "gateway": { - "session_id": "f2574e45-58b2-480f-919d-50aa65d07d31", - "gateway_version": "unknown", - "sequence_number": 3, - "prev_claim_hash": "sha256:dc612793436ff47741f94d67fa3aebf3d126741990cf48a4a29ec7baf63db9f9", - "audit_chain": { - "root": "43656adea27dbd3cd2adefbd0364a70172938185cf6bb61397d21c6bcf4b1761", - "tip": "237bef498e84cd658ca00a7673bb114192a110940ed821b44ae4f8c81cf247f5", - "length": 5 - }, - "call_summary": { - "tool_calls_total": 3, - "tool_calls_allowed": 3, - "tool_calls_denied": 0, - "tool_calls_faulted": 0, - "tools_invoked": [ - "ehr.clinical_decision_support", - "ehr.patient_record_lookup", - "ehr.treatment_plan_writer" - ], - "session_max_sensitivity": "confidential", - "call_graph_summary": { - "compliance_domains_touched": [ - "hipaa_phi" - ], - "cross_boundary_events": [], - "edges_represent": "Edges represent temporal adjacency (call order), not data provenance. A -> B means B was called immediately after A within this session." - } - }, - "catalog": { - "hash": "sha256:0c8009e105049f68d0f7054e254192e43eec7d567cd1fa45542450d268369647", - "drift_detected": false - }, - "attestation_generated_at": "2026-06-11T16:00:17.584902+00:00", - "attestation_validity_seconds": 86400, - "attestation_stale": false, - "catalog_exceptions": [], - "call_log_summary": { - "total_calls": 3, - "tools_called": [ - "ehr.patient_record_lookup", - "ehr.clinical_decision_support", - "ehr.treatment_plan_writer" - ], - "suspicious_sequences_detected": 0 - } - }, - "signature": "49A-mPB9eY6y5396rtf3-2jn8IsCfvhf5OoRWlBLU9m94MUvK9UYZ6n76dDmqt2F9Bo4xYMe_t7ypuLfgqHhDQ" -} diff --git a/healthcare/trace-output/high-risk-trust-record.json b/healthcare/trace-output/high-risk-trust-record.json new file mode 100644 index 0000000..240049a --- /dev/null +++ b/healthcare/trace-output/high-risk-trust-record.json @@ -0,0 +1,104 @@ +{ + "cmcp_version": "1.0", + "trace": { + "eat_profile": "tag:agentrust.io,2026:trace-v0.1", + "iat": 1784651776, + "subject": "spiffe://cmcp.gateway/tee/9a013bf66a5aa07d", + "runtime": { + "platform": "software-only", + "measurement": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "firmware_version": "software-only-dev-mode" + }, + "policy": { + "bundle_hash": "sha256:44ec5f0c1fcdb7c0108dc3e4f73ec2ec73f02705712656cd2bc718ccc72164c4", + "enforcement_mode": "enforce", + "version": "clinical-safety-v3.0" + }, + "data_class": "confidential", + "tool_transcript": { + "hash": "sha256:8a4059c424b2faba859ab98d7a1f9faadeff9dac30cafb2d650e381a699dd90b", + "call_count": 4, + "entries": [ + { + "tool_name": "ehr.patient_record_lookup", + "data_class": "confidential", + "decision": "allow" + }, + { + "tool_name": "ehr.clinical_decision_support", + "data_class": "confidential", + "decision": "allow" + }, + { + "tool_name": "ehr.drug_interaction_check", + "data_class": "confidential", + "decision": "allow" + }, + { + "tool_name": "ehr.treatment_plan_writer", + "data_class": "confidential", + "decision": "deny" + } + ] + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "mgE79mpaoH14uN7ZMgcitNBkOAy5K9vpQZHwVuu8v8U", + "kid": "cmcp-9a013bf6" + } + } + }, + "gateway": { + "session_id": "8c03dc11-7ef5-49ce-9561-85006c0a4669", + "gateway_version": "0.3.0", + "sequence_number": 5, + "prev_claim_hash": "sha256:6344f83e8397052bbd023f03a3789fa01788ea4f4efca6e6ae56365c66a110b0", + "audit_chain": { + "root": "c4532af6f2eebab2a9b7f47a4f90d099eb3ad21ffca08daa8b1039bf6862fb21", + "tip": "8a4059c424b2faba859ab98d7a1f9faadeff9dac30cafb2d650e381a699dd90b", + "length": 6 + }, + "call_summary": { + "tool_calls_total": 4, + "tool_calls_allowed": 3, + "tool_calls_denied": 1, + "tool_calls_faulted": 0, + "tools_invoked": [ + "ehr.clinical_decision_support", + "ehr.drug_interaction_check", + "ehr.patient_record_lookup", + "ehr.treatment_plan_writer" + ], + "session_max_sensitivity": "confidential", + "call_graph_summary": { + "compliance_domains_touched": [ + "hipaa_phi" + ], + "cross_boundary_events": [], + "edges_represent": "Edges represent temporal adjacency (call order), not data provenance. A -> B means B was called immediately after A within this session." + } + }, + "catalog": { + "hash": "sha256:174412ac2254484e3fa65a8c0b29495d5be5e2648e4b3b02398a3f0c9b82f3c7", + "drift_detected": false + }, + "attestation_generated_at": "2026-07-21T16:35:29.936660+00:00", + "attestation_validity_seconds": 86400, + "attestation_stale": false, + "catalog_exceptions": [], + "call_log_summary": { + "total_calls": 4, + "tools_called": [ + "ehr.patient_record_lookup", + "ehr.clinical_decision_support", + "ehr.drug_interaction_check", + "ehr.treatment_plan_writer" + ], + "suspicious_sequences_detected": 0 + }, + "kill_switch_triggered": false + }, + "signature": "z_O7rgrzXJB0eRmQ9yOWIKjjnUcvB_UCHH3JyUkyXqy-Aum_NBv_r-OH8UDD21QCkuMhNfkW4gTULlO5aaGCCA" +} diff --git a/healthcare/trace-output/standard-trust-record.json b/healthcare/trace-output/standard-trust-record.json new file mode 100644 index 0000000..b17b2b6 --- /dev/null +++ b/healthcare/trace-output/standard-trust-record.json @@ -0,0 +1,104 @@ +{ + "cmcp_version": "1.0", + "trace": { + "eat_profile": "tag:agentrust.io,2026:trace-v0.1", + "iat": 1784651772, + "subject": "spiffe://cmcp.gateway/tee/9a013bf66a5aa07d", + "runtime": { + "platform": "software-only", + "measurement": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "firmware_version": "software-only-dev-mode" + }, + "policy": { + "bundle_hash": "sha256:44ec5f0c1fcdb7c0108dc3e4f73ec2ec73f02705712656cd2bc718ccc72164c4", + "enforcement_mode": "enforce", + "version": "clinical-safety-v3.0" + }, + "data_class": "confidential", + "tool_transcript": { + "hash": "sha256:b031e6ee9bcdc71747d2faf34c15b1e06ceb1ff5bee87e95ff5dc96d90355e90", + "call_count": 4, + "entries": [ + { + "tool_name": "ehr.patient_record_lookup", + "data_class": "confidential", + "decision": "allow" + }, + { + "tool_name": "ehr.clinical_decision_support", + "data_class": "confidential", + "decision": "allow" + }, + { + "tool_name": "ehr.drug_interaction_check", + "data_class": "confidential", + "decision": "allow" + }, + { + "tool_name": "ehr.treatment_plan_writer", + "data_class": "confidential", + "decision": "allow" + } + ] + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "mgE79mpaoH14uN7ZMgcitNBkOAy5K9vpQZHwVuu8v8U", + "kid": "cmcp-9a013bf6" + } + } + }, + "gateway": { + "session_id": "be8094f5-9939-4995-b0c7-612429626983", + "gateway_version": "0.3.0", + "sequence_number": 4, + "prev_claim_hash": "sha256:185ab93cf14ec485f6cb573c825a40dbb0f9358fc2dd11dfe81ceea6bcc0876b", + "audit_chain": { + "root": "93b80590e0903464c38e3fe002a3bec306052e1933c91a03edeb45486cc5e0e0", + "tip": "b031e6ee9bcdc71747d2faf34c15b1e06ceb1ff5bee87e95ff5dc96d90355e90", + "length": 6 + }, + "call_summary": { + "tool_calls_total": 4, + "tool_calls_allowed": 4, + "tool_calls_denied": 0, + "tool_calls_faulted": 0, + "tools_invoked": [ + "ehr.clinical_decision_support", + "ehr.drug_interaction_check", + "ehr.patient_record_lookup", + "ehr.treatment_plan_writer" + ], + "session_max_sensitivity": "confidential", + "call_graph_summary": { + "compliance_domains_touched": [ + "hipaa_phi" + ], + "cross_boundary_events": [], + "edges_represent": "Edges represent temporal adjacency (call order), not data provenance. A -> B means B was called immediately after A within this session." + } + }, + "catalog": { + "hash": "sha256:174412ac2254484e3fa65a8c0b29495d5be5e2648e4b3b02398a3f0c9b82f3c7", + "drift_detected": false + }, + "attestation_generated_at": "2026-07-21T16:35:29.936660+00:00", + "attestation_validity_seconds": 86400, + "attestation_stale": false, + "catalog_exceptions": [], + "call_log_summary": { + "total_calls": 4, + "tools_called": [ + "ehr.patient_record_lookup", + "ehr.clinical_decision_support", + "ehr.drug_interaction_check", + "ehr.treatment_plan_writer" + ], + "suspicious_sequences_detected": 0 + }, + "kill_switch_triggered": false + }, + "signature": "rxnRxuZVljzM1Juop54J2uCouHtIoP_7RG44PRyWcbWT2hAYa9yo5cPkYSNbVOm3wnr-grLs__yIeuzp6SqoDw" +}