From f798b2454fec918b8957046ed531bbd072baada2 Mon Sep 17 00:00:00 2001 From: Imran Siddique Date: Sun, 7 Jun 2026 10:14:55 -0700 Subject: [PATCH] fix(policy): add attestation_platform to Cedar context at evaluation time (POLICY-005, #162) Closes #162. Co-Authored-By: Claude Sonnet 4.6 --- src/cmcp_gateway/mcp/proxy.py | 3 +++ tests/unit/test_mcp_proxy.py | 40 +++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/cmcp_gateway/mcp/proxy.py b/src/cmcp_gateway/mcp/proxy.py index 291d362a..f741d1ab 100644 --- a/src/cmcp_gateway/mcp/proxy.py +++ b/src/cmcp_gateway/mcp/proxy.py @@ -71,6 +71,7 @@ def __init__( attestation_generated_at: datetime | None = None, attestation_validity_seconds: int = 86400, catalog_hash: str | None = None, + attestation_platform: str = "unknown", ) -> None: self._catalog = catalog self._policy = policy_evaluator @@ -82,6 +83,7 @@ def __init__( self._attestation_generated_at = attestation_generated_at self._attestation_validity_seconds = attestation_validity_seconds self._catalog_hash = catalog_hash or catalog.catalog_hash + self._attestation_platform = attestation_platform # Build AGT GovernancePolicy from cMCP catalog allowed_tools = list(catalog.entries.keys()) @@ -158,6 +160,7 @@ def _build_cedar_context( "baa_covered": (not entry.requires_baa) if entry else False, "destination_class": "external", "session_max_sensitivity": self._session.max_sensitivity, + "attestation_platform": self._attestation_platform, } if workflow_id is not None: ctx["workflow_id"] = workflow_id diff --git a/tests/unit/test_mcp_proxy.py b/tests/unit/test_mcp_proxy.py index 8b6ffcbe..9bb973dc 100644 --- a/tests/unit/test_mcp_proxy.py +++ b/tests/unit/test_mcp_proxy.py @@ -207,6 +207,46 @@ async def test_cedar_context_includes_arguments(): assert ctx["arguments"] == args +# ── POLICY-005 (issue #162): attestation_platform in Cedar context ──────────── + +@pytest.mark.asyncio +async def test_cedar_context_includes_attestation_platform(): + """POLICY-005 (issue #162) — attestation_platform must be in Cedar context so + policies can restrict calls to hardware-attested callers only.""" + from cmcp_gateway.mcp.proxy import CMCPProxy + + evaluator = _make_evaluator() + cfg = Config() + cfg.attestation = AttestationConfig(enforcement_mode=EnforcementMode.ENFORCING) + session = SessionState(session_id="sess-001") + chain = AuditChain("sess-001") + + with patch("cmcp_gateway.mcp.proxy.MCPGateway"), \ + patch("cmcp_gateway.mcp.proxy.MCPResponseScanner"): + proxy = CMCPProxy( + _make_catalog(), evaluator, session, chain, cfg, + attestation_platform="amd-sev-snp", + ) + proxy._mcp_gateway = MagicMock() + proxy._mcp_gateway.call_tool = AsyncMock(return_value=MagicMock( + sensitivity_tags=[], injection_detected=False + )) + + await proxy.call_tool("c1", "test.tool", {}) + ctx = evaluator.evaluate.call_args[0][0] + assert ctx["attestation_platform"] == "amd-sev-snp" + + +@pytest.mark.asyncio +async def test_cedar_context_attestation_platform_default_is_unknown(): + """POLICY-005 — default attestation_platform is 'unknown' when not specified.""" + evaluator = _make_evaluator() + proxy, _, _ = _make_proxy(evaluator=evaluator) + await proxy.call_tool("c1", "test.tool", {}) + ctx = evaluator.evaluate.call_args[0][0] + assert ctx["attestation_platform"] == "unknown" + + # ── POLICY-005: request_payload_hash in all audit entries ──────────────────── @pytest.mark.asyncio