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
3 changes: 3 additions & 0 deletions LIMITATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ The audit chain records a `response_payload_hash` for each tool call and an `evi

Tool servers do not sign their individual responses. A `tls-pinned` entry proves the response came from a server holding the catalog-pinned certificate but does not prevent the server itself from later denying it produced a specific response. For strong non-repudiation, configure non-placeholder TLS fingerprints for all upstream servers so all evidence is `tls-pinned`, and treat the TEE attestation as the binding authority for what the gateway recorded.

**Agent Manifest identity binding (issue #302)**
When configured, cMCP verifies a signed Agent Manifest against a trusted issuer key, checks that the authenticated agent subject equals `manifest.agent_id`, and requires the manifest's policy and catalog hashes to match the loaded runtime hashes. The Trust Record carries this as `gateway.agent_identity`. This proves that the session was bound to the reviewed manifest identity and approved hashes. It does not prove that the agent behaved correctly, that the model output was safe, or that the same logical agent persisted across restarts. The first implementation takes the authenticated subject from configuration; production deployments should source it from the agent SVID/mTLS identity path. Trust in the manifest issuer key remains an out-of-band PKI concern.

**LLM inference and model output**
cMCP intercepts tool calls at the MCP protocol boundary. It does not observe or modify LLM inference, the contents of the agent's context window, or model outputs that do not produce a tool call. A model could hallucinate a response, leak sensitive context in a chat reply, or receive a poisoned tool response that influences subsequent reasoning -- none of these are visible to the gateway. cMCP controls the tool boundary, not the model boundary.

Expand Down
22 changes: 22 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ attestation:
# AMD measurement register hex for SEV-SNP).
# expected_measurement: "sha256:..."

agent_manifest:
# Optional signed Agent Manifest binding (#302). When set, the runtime
# verifies the manifest issuer signature, checks that the authenticated
# agent subject matches manifest.agent_id, and requires the manifest's
# policy/catalog hashes to match the loaded runtime hashes before sessions
# can be created.
path: ./agent-manifest.json
trust_anchor_path: ./manifest-public-key.json
authenticated_subject: spiffe://factory.example/agent/material-movement/dev

# Path to the directory containing .cedar policy files and manifest.json.
# Must not contain '..' components. Relative paths are resolved from the
# working directory at startup.
Expand Down Expand Up @@ -72,6 +82,16 @@ policy_reload_interval_seconds: 0
| `staleness_policy` | string | `fail_closed` | Action when attestation validity expires. Valid values: `fail_closed` (terminate sessions), `warn_only` (allow sessions, mark claims as stale). |
| `expected_measurement` | string | none | Optional. Expected TEE measurement value. If set, the runtime verifies the hardware measurement matches this string at startup and exits with a non-zero status if it does not. |

### agent_manifest

All fields are optional as a group. If `path` is set, `trust_anchor_path` must also be set. When the block is configured, cMCP fails closed on manifest signature failure, subject mismatch, policy hash drift, or catalog hash drift.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `path` | string | none | Path to the signed Agent Manifest JSON document. Path traversal (`..` components) is rejected. |
| `trust_anchor_path` | string | none | Path to a JSON trust anchor containing the issuer Ed25519 public key, either as `{ "key_id": "...", "public_key_base64url": "..." }` or `{ "keys": [...] }`. |
| `authenticated_subject` | string | none | SPIFFE URI for the authenticated agent subject. This must equal `manifest.agent_id`. In production this should come from the agent SVID/mTLS identity; the config field is the current runtime input for that subject. |

### top-level fields

| Field | Type | Default | Description |
Expand All @@ -91,6 +111,7 @@ Environment variables control secrets and mode flags that must not appear in con
| `CMCP_DEV_MODE=1` | Enables software-only attestation. No hardware TEE required. TRACE Claims will show `partially_verified` status. Required when `provider` is `software-only`. | `attestation.provider` (forces software-only) |
| `CMCP_BEARER_TOKEN` | Optional bearer token for runtime HTTP auth. If set, all requests to the runtime must include `Authorization: Bearer <token>`. If unset, no bearer auth is enforced. | none |
| `OPAQUE_ATTESTATION_URL` | Enables the Opaque Managed Runtime provider. Must be set to the Opaque attestation service URL. Required when `provider` is `opaque` or `auto` on Opaque infrastructure. | enables `opaque` provider detection |
| `CMCP_POLICY_HASH` | SHA-256 hash of the approved policy bundle. Required in non-dev mode and checked by startup before Agent Manifest binding. The gateway fails closed at startup if this is unset and `CMCP_DEV_MODE` is not `1`. Format: `sha256:<hex>`. | none (startup policy integrity check) |
| `CMCP_CATALOG_HASH` | SHA-256 hash of the approved `catalog.json`. Required in non-dev mode. The gateway fails closed at startup if this is unset and `CMCP_DEV_MODE` is not `1`. Format: `sha256:<hex>`. | none (additional startup check) |

## Enforcement modes
Expand Down Expand Up @@ -129,6 +150,7 @@ catalog_path: ./catalog.json

- Set `attestation.enforcement_mode` to `enforcing`. Advisory mode provides no blocking protection against policy violations.
- Set `CMCP_CATALOG_HASH` to the SHA-256 of the approved `catalog.json`. The gateway fails closed at startup if this is unset in non-dev mode, but setting it explicitly pins the approved catalog hash and prevents silent substitution.
- Configure `agent_manifest.path`, `agent_manifest.trust_anchor_path`, and `agent_manifest.authenticated_subject` for agents with signed manifests. The runtime will refuse to start if the signed manifest does not bind the authenticated agent subject to the loaded policy bundle and catalog hashes.
- Set `attestation.expected_measurement` to the expected TEE measurement for your deployment. Without this, a different binary could be deployed and would still produce valid attestation reports.
- Use a real TEE provider (`tpm`, `sev-snp`, `tdx`, or `opaque`), not `software-only`. Software-only mode does not provide a hardware root of trust and leaves threat classes T1 through T4 open.
- Rotate the TEE signing key by performing a full enclave restart on a regular schedule. The signing key is hardware-sealed per enclave instance; rotation requires restart.
2 changes: 1 addition & 1 deletion docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ The response is a signed `GatewayClaim`. It looks like:
"trace": {
"eat_profile": "tag:agentrust.io,2026:trace-v0.1",
"iat": 1749081600,
"subject": "spiffe://cmcp.gateway/session/demo-session-001",
"subject": "spiffe://cmcp.gateway/tee/<gateway-id>",
"runtime": {
"platform": "tpm2",
"measurement": "sha256:0000000000000000000000000000000000000000000000000000000000000000",
Expand Down
2 changes: 2 additions & 0 deletions docs/spec/error-codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ This is the normative registry for all error codes used across the cMCP Runtime.
| `ATTESTATION_PROVIDER_UNSUPPORTED` | 500 | FATAL | No supported TEE provider detected and `CMCP_DEV_MODE` is not set | [attestation.md §1.1](attestation.md) |
| `POLICY_HASH_MISMATCH` | 500 | FATAL | Measured policy bundle hash does not match deployment manifest | [failure-modes.md FM-4](failure-modes.md) |
| `CATALOG_HASH_MISMATCH` | 500 | FATAL | Measured catalog hash does not match deployment manifest | [attestation.md §5](attestation.md) |
| `AGENT_MANIFEST_BINDING_FAILED` | 500 | FATAL | Signed Agent Manifest signature, authenticated subject, policy hash, or catalog hash did not match the runtime session inputs | [session-policy.md](session-policy.md) |
| `TOOL_NOT_IN_CATALOG` | 403 | WARN | Agent requested a tool not present in the attested catalog | [cedar-policy.md](cedar-policy.md) |
| `POLICY_DENY` | 403 | INFO | Cedar policy evaluation returned deny for this call | [cedar-policy.md](cedar-policy.md) |
| `CATALOG_TOOL_NAME_COLLISION` | 500 | FATAL | Two catalog entries register the same `tool_name` | [tool-identity.md](tool-identity.md) |
Expand All @@ -35,6 +36,7 @@ The following error codes are defined and documented in [verification-library.md
| `PUBLIC_KEY_NOT_BOUND` |
| `POLICY_HASH_MISMATCH` |
| `CATALOG_HASH_MISMATCH` |
| `AGENT_MANIFEST_MISMATCH` |
| `ATTESTATION_STALE` |
| `CHAIN_BROKEN` |
| `CLAIM_MALFORMED` |
Expand Down
39 changes: 39 additions & 0 deletions docs/spec/session-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,44 @@ In practice: session `max_duration_seconds` is set to `min(configured_session_ma

This means a long-running agent that handles high-sensitivity data early in its session will face increasingly tight call restrictions as the session progresses - both because `max_sensitivity` is monotonically increasing and because the TRACE token TTL is monotonically decreasing. Deployments should size TRACE token lifetimes to match expected task durations.

## Agent Manifest Identity Binding

When `agent_manifest` is configured, session creation is bound to a signed Agent Manifest. The runtime loads the manifest and issuer trust anchor, delegates Agent Manifest signature and artifact verification to the `agent-manifest` SDK `verify_manifest()` API, and extracts:

- `manifest_id`
- `agent_id`
- `artifacts.policy_bundle.hash`
- `artifacts.tool_manifest.catalog_hash`

The authenticated agent subject for the session MUST be a SPIFFE URI and MUST equal `manifest.agent_id`. In the current HTTP runtime this subject is supplied by `agent_manifest.authenticated_subject`; production deployments SHOULD wire this value from the inbound agent SVID/mTLS identity. The runtime records the provenance of this assertion in `gateway.agent_identity.subject_source`: `config` for configured input, `svid` for transport/mTLS SVID, or `manifest-dev` for the development-mode fallback that uses `manifest.agent_id`. If the subject, manifest signature, manifest expiry, policy hash, or catalog hash does not match, the runtime fails closed before serving the session.

The Agent Manifest signing pre-image uses the manifest `signed_fields` subset. For HITL workflows, `hitl_record.approvals` is normalized to an empty array before signing so approvals can be attached after the manifest is issued without invalidating the manifest signature.

This binding answers "who acted" for the session. It does not replace `trace.subject`, which identifies the cMCP gateway's TEE-backed issuing identity. The session identifier remains in `gateway.session_id`. Instead, the TRACE Trust Record carries `gateway.agent_identity` alongside the gateway subject:

```json
{
"trace": {
"subject": "spiffe://cmcp.gateway/tee/<gateway-id>"
},
"gateway": {
"session_id": "<session-id>",
"agent_identity": {
"manifest_id": "<manifest UUID>",
"agent_id": "spiffe://factory.example/agent/material-movement/dev",
"authenticated_subject": "spiffe://factory.example/agent/material-movement/dev",
"subject_source": "config",
"issuer": "spiffe://factory.example/signing-authority/development",
"issuer_key_id": "<sha256 of issuer public key>",
"policy_bundle_hash": "sha256:<manifest policy hash>",
"tool_catalog_hash": "sha256:<manifest catalog hash>"
}
}
}
```

Offline verifiers SHOULD cross-check `gateway.agent_identity` against the signed manifest and trusted issuer key. This keeps the runtime boundary check and the evidence artifact self-checking.

## TRACE Claim Fields from Session State

The following fields from session state are included in the TRACE attestation record for the session (written at session close):
Expand All @@ -151,3 +189,4 @@ The following fields from session state are included in the TRACE attestation re
|-------|------|-------------|
| `session_max_sensitivity` | string | The highest `max_sensitivity` value reached during the session. |
| `session_reset_count` | integer | Number of times `POST /session/reset` was called during the session lifetime. Normally `0`; a non-zero value warrants review. |
| `agent_identity` | object | Optional Agent Manifest binding: manifest ID, bound agent ID, authenticated subject, subject source, issuer key ID, policy hash, and catalog hash. Present only when `agent_manifest` is configured and verified. |
26 changes: 20 additions & 6 deletions docs/spec/verification-library.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ def verify_trace_claim(
claim_json: dict,
approved: ApprovedHashes,
max_attestation_age_seconds: int = 86400,
*,
trusted_public_key_hex: Optional[str] = None,
agent_manifest: Optional[dict] = None,
trusted_agent_manifest_keys: Optional[dict[str, bytes]] = None,
) -> VerificationResult:
"""
Verify a TRACE Claim without trusting the operator.
Expand All @@ -55,15 +59,24 @@ def verify_trace_claim(
2. Verify signature over canonical claim body using tee_public_key
3. Check policy_bundle.hash against approved.policy_bundle_hash
4. Check tool_catalog.hash against approved.tool_catalog_hash
5. Check attestation freshness (timestamp within max_attestation_age_seconds)
6. Verify audit chain continuity (audit_chain_root, audit_chain_tip)
5. If agent_manifest and trusted_agent_manifest_keys are provided, verify
the Agent Manifest issuer signature with agent-manifest SDK
verify_manifest() and cross-check gateway.agent_identity:
manifest_id, agent_id/authenticated_subject, subject_source, policy hash,
catalog hash, and manifest expiry.
6. Check attestation freshness (timestamp within max_attestation_age_seconds)
7. Verify audit chain continuity (audit_chain_root, audit_chain_tip)

Returns VerificationResult with status and details.
"""
...
```

## Per-Provider Verification Steps
...
```

`trusted_agent_manifest_keys` keeps cMCP's runtime-facing shape as raw Ed25519
public key bytes keyed by issuer `key_id`; the verifier base64url-encodes those
keys when calling the Agent Manifest SDK.

## Per-Provider Verification Steps

### TPM Verification

Expand Down Expand Up @@ -116,6 +129,7 @@ VerificationError enum:
- PUBLIC_KEY_NOT_BOUND: tee_public_key is not bound to the attestation_report (measurement mismatch or quote verification failed)
- POLICY_HASH_MISMATCH: policy_bundle.hash != approved.policy_bundle_hash
- CATALOG_HASH_MISMATCH: tool_catalog.hash != approved.tool_catalog_hash
- AGENT_MANIFEST_MISMATCH: gateway.agent_identity does not match the signed Agent Manifest, the manifest signature is invalid, or trusted issuer keys were not supplied for a requested manifest check
- ATTESTATION_STALE: attestation_generated_at is older than max_attestation_age_seconds
- CHAIN_BROKEN: audit_chain_root -> audit_chain_tip traversal fails (missing entries or hash mismatch)
- CLAIM_MALFORMED: claim_json fails JSON Schema validation against the TRACE Claim schema
Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ classifiers = [
requires-python = ">=3.11"
dependencies = [
"agentrust-trace>=0.1",
# Replace with a version constraint after the next SDK release exports verify_manifest.
"agent-manifest @ git+https://github.com/agentrust-io/agent-manifest.git@1297c223d68fdaf95ac9438d9de844597281a3c2#subdirectory=python",
"cryptography>=42.0",
"pyyaml>=6.0",
"httpx>=0.27",
Expand Down Expand Up @@ -62,6 +64,9 @@ Documentation = "https://github.com/agentrust-io/cmcp/tree/main/docs"
[tool.hatch.build.targets.wheel]
packages = ["src/cmcp_runtime", "src/cmcp_verify"]

[tool.hatch.metadata]
allow-direct-references = true

[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
Expand Down
44 changes: 44 additions & 0 deletions schemas/trace-claim.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,50 @@
"drift_detected": { "type": "boolean" }
}
},
"agent_identity": {
"type": "object",
"additionalProperties": false,
"required": [
"manifest_id",
"agent_id",
"authenticated_subject",
"subject_source",
"issuer",
"issuer_key_id",
"policy_bundle_hash",
"tool_catalog_hash"
],
"description": "Optional Agent Manifest binding carried by issue #302. The runtime emits this only after the signed manifest, authenticated agent subject, policy hash, and catalog hash have been checked at session creation.",
"properties": {
"manifest_id": { "type": "string" },
"agent_id": {
"type": "string",
"pattern": "^spiffe://"
},
"authenticated_subject": {
"type": "string",
"pattern": "^spiffe://"
},
"subject_source": {
"type": "string",
"enum": ["config", "svid", "manifest-dev"],
"description": "Source of the authenticated_subject assertion. config means configured runtime input, svid means transport/mTLS SVID, and manifest-dev means dev-mode fallback from manifest.agent_id."
},
"issuer": {
"type": "string",
"pattern": "^spiffe://"
},
"issuer_key_id": { "type": "string" },
"policy_bundle_hash": {
"type": "string",
"pattern": "^sha(256|384):[0-9a-f]+"
},
"tool_catalog_hash": {
"type": "string",
"pattern": "^sha(256|384):[0-9a-f]+"
}
}
},
"attestation_generated_at": {
"type": "string",
"format": "date-time"
Expand Down
Loading