diff --git a/.github/workflows/ramen-ai-cmcp-conformance.yml b/.github/workflows/ramen-ai-cmcp-conformance.yml new file mode 100644 index 0000000..e4421de --- /dev/null +++ b/.github/workflows/ramen-ai-cmcp-conformance.yml @@ -0,0 +1,51 @@ +# ramen-ai cMCP adapter conformance workflow. +# Lives at repo root — GitHub Actions only discovers workflows here. +# Scoped to this integration via paths filter. +name: ramen-ai-cmcp conformance +on: + push: + paths: + - "integrations/ramen-ai-cmcp/**" + - ".github/workflows/ramen-ai-cmcp-conformance.yml" + pull_request: + paths: + - "integrations/ramen-ai-cmcp/**" + - ".github/workflows/ramen-ai-cmcp-conformance.yml" + schedule: + - cron: "0 6 * * 1" # weekly: catch drift against the latest released packages + workflow_dispatch: + +permissions: + contents: read + +jobs: + conformance: + strategy: + fail-fast: false + matrix: + python: ["3.11", "3.12", "3.13", "3.14"] + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: ${{ matrix.python }} + - name: Install released agentrust-io packages + run: | + python -m pip install --upgrade pip + pip install agentrust-trace agentrust-trace-tests cmcp-runtime + - name: Install this integration + run: pip install -e "integrations/ramen-ai-cmcp[test]" + - name: Integration tests + run: pytest integrations/ramen-ai-cmcp/tests -q + - name: Emit a sample TRACE record + run: python integrations/ramen-ai-cmcp/examples/emit_record.py --out trust-record.jwt + - name: TRACE conformance level 0 + run: trace-tests verify --record trust-record.jwt --level 0 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: conformance-${{ matrix.os }}-py${{ matrix.python }} + path: | + trust-record.jwt + trust-record.jwt.signed.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9c882b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.egg-info/ diff --git a/integrations/ramen-ai-cmcp/README.md b/integrations/ramen-ai-cmcp/README.md new file mode 100644 index 0000000..903b41a --- /dev/null +++ b/integrations/ramen-ai-cmcp/README.md @@ -0,0 +1,59 @@ +# ramen-ai cMCP Adapter integration with cMCP + TRACE + +Intercepts tool calls at the [cMCP](https://github.com/agentrust-io/cmcp) +boundary, evaluates their semantic intent against configured compliance policies +via the [ramen-ai](https://ramenai.dev) API, and maps the resulting V5 +Ed25519-signed receipt onto a TRACE Trust Record (EAT profile +`tag:agentrust.io,2026:trace-v0.1`). + +Source: [ramen-ai-dev/ramen-ai-integrations — plugins/cmcp-python](https://github.com/ramen-ai-dev/ramen-ai-integrations/tree/master/plugins/cmcp-python) + +## Run it + +Against released packages (`agentrust-trace` 0.3.0, `cmcp-runtime` 0.3.0): + +```bash +pip install agentrust-trace agentrust-trace-tests cmcp-runtime +pip install -e "integrations/ramen-ai-cmcp[test]" +pytest integrations/ramen-ai-cmcp/tests -q +python integrations/ramen-ai-cmcp/examples/emit_record.py --out trust-record.jwt +trace-tests verify --record trust-record.jwt --level 0 +``` + +## What is verified + +- `ramen_ai_trace.build_trace_record` maps a committed V5 fixture receipt onto + TRACE fields: `policy.bundle_hash` (`sha256:`), `runtime.measurement` + (receipt UUID), `subject` (`spiffe://ramenai.dev/evaluation/`), + `appraisal.status` (`affirming` / `denying`). +- `agentrust_trace.sign_record` signs the record with an ephemeral Ed25519 key + and `agentrust_trace.verify_record(..., allow_embedded_key=True)` verifies the + round-trip; `tests/` includes a tamper probe that must fail verification. +- `trace-tests verify --level 0` passes on the emitted record (8 checks). + +## What it does NOT claim + +See rules 2 and 4 in [CONTRIBUTING.md](../../CONTRIBUTING.md). + +- **Level 0 carries a TR-SIG-005 UNVERIFIED finding.** The `agentrust-trace-tests` + loader rejects any plain record carrying a top-level `signature` field + (anti-downgrade), so the gradable record is the unsigned payload. The signed + form is written alongside it (`.signed.json`) and verifies with + `agentrust_trace.verify_record`. +- The ephemeral signing key proves the sign/verify path works; it does **not** + chain to a trusted issuer. +- `runtime.platform` is `software-only`. No TEE, hardware root of trust, or + attested-execution claim is made. +- The ramen-ai evaluation API requires `RAMEN_API_KEY` and `OPENAI_API_KEY` + (BYOK on Starter/Professional tiers). The conformance workflow does not call + the live API — it maps a committed fixture receipt offline. +- V5 receipts bind policy UUIDs but not rule content (policies are mutable under + the same UUID). See `v5-conformance.md §6` in the ramen-ai-integrations repo. + +## Conformance CI + +The repository-root workflow +[`.github/workflows/ramen-ai-cmcp-conformance.yml`](../../.github/workflows/ramen-ai-cmcp-conformance.yml) +(path-scoped to this directory) installs the released agentrust-io packages, +runs the mapping tests, emits a record, and runs `trace-tests verify --level 0` +across Python 3.11–3.14. A clean matrix run is the basis for the Verified tier. diff --git a/integrations/ramen-ai-cmcp/examples/emit_record.py b/integrations/ramen-ai-cmcp/examples/emit_record.py new file mode 100644 index 0000000..5af3dc5 --- /dev/null +++ b/integrations/ramen-ai-cmcp/examples/emit_record.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Emit a TRACE Trust Record from a ramen-ai V5 fixture receipt. + +Loads ``examples/fixtures/vector1_allowed.json``, maps the receipt onto a TRACE +Trust Record via :func:`ramen_ai_trace.build_trace_record`, signs it with an +ephemeral Ed25519 key via ``agentrust_trace.sign_record``, verifies the +round-trip, and writes two files: + + Unsigned record for ``trace-tests verify`` + .signed.json Signed record, verifiable with + ``agentrust_trace.verify_record(..., allow_embedded_key=True)`` + +Usage: + python examples/emit_record.py --out trust-record.jwt +""" +from __future__ import annotations + +import argparse +import json +import sys +import time +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +import agentrust_trace +from ramen_ai_trace import build_trace_record + +FIXTURE = Path(__file__).resolve().parent / "fixtures" / "vector1_allowed.json" + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out", required=True, help="Path for the trace-tests-gradable record") + args = parser.parse_args() + + fixture = json.loads(FIXTURE.read_text(encoding="utf-8")) + receipt: dict = fixture["receipt"] + + key = agentrust_trace.generate_key() + jwk = agentrust_trace.key_to_jwk(key) + record = build_trace_record(receipt, iat=int(time.time()), jwk=jwk) + + signed = agentrust_trace.sign_record(dict(record), key) + agentrust_trace.verify_record(signed, allow_embedded_key=True) + + out = Path(args.out) + out.write_text( + json.dumps(record, sort_keys=True, separators=(",", ":")) + "\n", + encoding="utf-8", + ) + signed_out = out.with_name(out.name + ".signed.json") + signed_out.write_text( + json.dumps(signed, sort_keys=True, separators=(",", ":")) + "\n", + encoding="utf-8", + ) + + print(f"subject: {record['subject']}") + print(f"appraisal.status: {record['appraisal']['status']}") + print(f"unsigned (for trace-tests): {out}") + print(f"signed (verify_record OK): {signed_out}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/integrations/ramen-ai-cmcp/examples/fixtures/vector1_allowed.json b/integrations/ramen-ai-cmcp/examples/fixtures/vector1_allowed.json new file mode 100644 index 0000000..1c72c0c --- /dev/null +++ b/integrations/ramen-ai-cmcp/examples/fixtures/vector1_allowed.json @@ -0,0 +1,19 @@ +{ + "_comment": "Vector 1 — Allowed (verdict 1). Source: v5-conformance.md §5.1. Signed by ramen_pk_ephemeral_test.", + "input": "What are the standard terms for a cash ISA?", + "receipt": { + "id": "11111111-1111-4111-8111-111111111111", + "schema_version": "5.0", + "kid": "ramen_pk_ephemeral_test", + "signature": "86rTO8547URmP0M-k0AEHbjSjz2ASRndoRrAFKrtrQJvsPbiAfn6rqEbuQrf4rtNFYq4klVhcHrXqtjRcoC2Ag", + "canonical_payload": "{\"schema_version\":\"5.0\",\"kid\":\"ramen_pk_ephemeral_test\",\"id\":\"11111111-1111-4111-8111-111111111111\",\"timestamp\":\"2026-06-20T09:00:00.000Z\",\"policy_ids\":[\"f47ac10b-58cc-4372-a567-0e02b2c3d479\"],\"payload_hash\":\"adb09112ff437c97a89b17e2dcba478b0c1ebbf2331fa4e5d216f10085eeff21\",\"verdict\":1,\"reasoning\":\"\",\"steering\":\"\",\"statutory_anchors\":[\"FCA COBS 4.2.1\"]}", + "statutory_anchors": ["FCA COBS 4.2.1"] + }, + "expected": { + "valid": true, + "verdict": 1, + "reasoning": "", + "steering": "", + "payload_hash": "adb09112ff437c97a89b17e2dcba478b0c1ebbf2331fa4e5d216f10085eeff21" + } +} diff --git a/integrations/ramen-ai-cmcp/examples/fixtures/vector_n1_bad_sig.json b/integrations/ramen-ai-cmcp/examples/fixtures/vector_n1_bad_sig.json new file mode 100644 index 0000000..c572f53 --- /dev/null +++ b/integrations/ramen-ai-cmcp/examples/fixtures/vector_n1_bad_sig.json @@ -0,0 +1,16 @@ +{ + "_comment": "Negative Vector N1 — tampered signature. Source: v5-conformance.md §5.4. Must verify as valid=false.", + "input": "What are the standard terms for a cash ISA?", + "receipt": { + "id": "11111111-1111-4111-8111-111111111111", + "schema_version": "5.0", + "kid": "ramen_pk_ephemeral_test", + "signature": "86rTO8547URmP0M-k0AEHbjSjz2ASRndoRrAFKrtrQJvsPbiAfn6rqEbuQrf4rtNFYq4klVhcHrXqtjRcoC2AA", + "canonical_payload": "{\"schema_version\":\"5.0\",\"kid\":\"ramen_pk_ephemeral_test\",\"id\":\"11111111-1111-4111-8111-111111111111\",\"timestamp\":\"2026-06-20T09:00:00.000Z\",\"policy_ids\":[\"f47ac10b-58cc-4372-a567-0e02b2c3d479\"],\"payload_hash\":\"adb09112ff437c97a89b17e2dcba478b0c1ebbf2331fa4e5d216f10085eeff21\",\"verdict\":1,\"reasoning\":\"\",\"steering\":\"\",\"statutory_anchors\":[\"FCA COBS 4.2.1\"]}", + "statutory_anchors": ["FCA COBS 4.2.1"] + }, + "expected": { + "valid": false, + "reason_contains": "Signature does not verify" + } +} diff --git a/integrations/ramen-ai-cmcp/integration.yaml b/integrations/ramen-ai-cmcp/integration.yaml new file mode 100644 index 0000000..55e2c7f --- /dev/null +++ b/integrations/ramen-ai-cmcp/integration.yaml @@ -0,0 +1,22 @@ +name: ramen-ai cMCP Adapter +vendor: ramen-ai +integrates_with: + - cmcp + - trace +description: >- + cMCP tool-call policy adapter that evaluates semantic intent via the ramen-ai + API and maps the V5 Ed25519-signed receipt onto a TRACE Trust Record. +maintainer: + github: ramen-noodle6 + email: damian_smith442@aol.com +repository: https://github.com/ramen-ai-dev/ramen-ai-integrations +homepage: https://ramenai.dev +license: MIT +tier: community +# Level 0 passes with the TR-SIG-005 UNVERIFIED finding: trace-tests does not +# grade signatures on plain trace records — the signed form is written alongside +# the gradable record and verifies via agentrust_trace.verify_record. +trace_conformance_level: 0 +tested_against: + agentrust-trace: "0.3.0" + cmcp-runtime: "0.3.0" diff --git a/integrations/ramen-ai-cmcp/pyproject.toml b/integrations/ramen-ai-cmcp/pyproject.toml new file mode 100644 index 0000000..d3212b8 --- /dev/null +++ b/integrations/ramen-ai-cmcp/pyproject.toml @@ -0,0 +1,17 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "ramen-ai-cmcp-integration" +version = "0.1.0" +description = "ramen-ai cMCP policy adapter and TRACE Trust Record mapper" +requires-python = ">=3.11" +license = "MIT" +dependencies = ["agentrust-trace"] + +[project.optional-dependencies] +test = ["pytest"] + +[tool.setuptools] +py-modules = ["ramen_ai_trace"] diff --git a/integrations/ramen-ai-cmcp/ramen_ai_trace.py b/integrations/ramen-ai-cmcp/ramen_ai_trace.py new file mode 100644 index 0000000..6e08d32 --- /dev/null +++ b/integrations/ramen-ai-cmcp/ramen_ai_trace.py @@ -0,0 +1,124 @@ +"""ramen_ai_trace — TRACE Trust Record mapper for ramen-ai V5 receipts. + +Maps a ramen-ai V5 Ed25519 receipt onto a TRACE Trust Record dict +(EAT profile tag:agentrust.io,2026:trace-v0.1). + +This is the self-contained copy for the agentrust-io/integrations submission. +The canonical implementation lives at: + https://github.com/ramen-ai-dev/ramen-ai-integrations/tree/master/plugins/cmcp-python + +Field mapping +───────────── +TRACE field ← ramen-ai V5 source +────────────────────────────────────────────────────────────── +eat_profile constant "tag:agentrust.io,2026:trace-v0.1" +iat caller-supplied (int, Unix seconds) +subject "spiffe://ramenai.dev/evaluation/" +cnf.jwk caller-supplied (public JWK for signing) +policy.bundle_hash "sha256:" + canonical_payload.payload_hash +policy.enforcement_mode "enforce" +policy.version canonical_payload.schema_version ("5.0") +runtime.measurement receipt.id +runtime.platform "software-only" +tool_transcript.call_count 1 +tool_transcript.hash "sha256:" + canonical_payload.payload_hash +tool_transcript.transcript_uri "urn:ramen-ai:evaluation:" +appraisal.policy_ref comma-joined canonical_payload.policy_ids +appraisal.status "affirming" if verdict==1 else "denying" +appraisal.timestamp iat +appraisal.verifier "ramen-ai-core" +appraisal.statutory_anchors canonical_payload.statutory_anchors (if non-empty) +appraisal.steering canonical_payload.steering (omitted when empty) +transparency "pending" +""" +from __future__ import annotations + +import json +from typing import Any + +EAT_PROFILE = "tag:agentrust.io,2026:trace-v0.1" +VERIFIER = "ramen-ai-core" + + +def build_trace_record( + receipt: dict[str, Any], + *, + iat: int, + jwk: dict[str, str], +) -> dict[str, Any]: + """Map a ramen-ai V5 receipt dict onto an unsigned TRACE Trust Record dict. + + Args: + receipt: The ``data.receipt`` sub-object from a ramen-ai evaluate + response. Must contain ``id``, ``schema_version`` (``"5.0"``), + ``kid``, ``signature``, and ``canonical_payload``. + iat: Unix timestamp (int seconds) for the record issue time. + jwk: Public JWK dict embedded in ``cnf.jwk``; matches the private + key used to sign the record with ``agentrust_trace.sign_record``. + + Returns: + Unsigned TRACE Trust Record dict. + + Raises: + ValueError: if required fields are missing or schema_version != "5.0". + """ + _validate_receipt(receipt) + payload: dict[str, Any] = json.loads(receipt["canonical_payload"]) + + receipt_id: str = receipt["id"] + payload_hash: str = payload["payload_hash"] + verdict: int = payload["verdict"] + policy_ids: list[str] = payload.get("policy_ids", []) + statutory_anchors: list[str] = payload.get("statutory_anchors", []) + steering: str = payload.get("steering", "") + + prefixed_hash = f"sha256:{payload_hash}" + + appraisal: dict[str, Any] = { + "policy_ref": ", ".join(policy_ids), + "status": "affirming" if verdict == 1 else "denying", + "timestamp": iat, + "verifier": VERIFIER, + } + if statutory_anchors: + appraisal["statutory_anchors"] = statutory_anchors + if steering: + appraisal["steering"] = steering + + return { + "eat_profile": EAT_PROFILE, + "iat": iat, + "subject": f"spiffe://ramenai.dev/evaluation/{receipt_id}", + "cnf": {"jwk": jwk}, + "policy": { + "bundle_hash": prefixed_hash, + "enforcement_mode": "enforce", + "version": payload["schema_version"], + }, + "runtime": { + "measurement": receipt_id, + "platform": "software-only", + }, + "tool_transcript": { + "call_count": 1, + "hash": prefixed_hash, + "transcript_uri": f"urn:ramen-ai:evaluation:{receipt_id}", + }, + "appraisal": appraisal, + "transparency": "pending", + } + + +def _validate_receipt(receipt: dict[str, Any]) -> None: + required = {"id", "schema_version", "kid", "signature", "canonical_payload"} + missing = required - receipt.keys() + if missing: + raise ValueError(f"Receipt missing required fields: {missing}") + if receipt["schema_version"] != "5.0": + raise ValueError( + f"Unsupported schema_version '{receipt['schema_version']}'; expected '5.0'" + ) + try: + json.loads(receipt["canonical_payload"]) + except json.JSONDecodeError as exc: + raise ValueError(f"canonical_payload is not valid JSON: {exc}") from exc diff --git a/integrations/ramen-ai-cmcp/tests/test_mapping.py b/integrations/ramen-ai-cmcp/tests/test_mapping.py new file mode 100644 index 0000000..5e0698e --- /dev/null +++ b/integrations/ramen-ai-cmcp/tests/test_mapping.py @@ -0,0 +1,116 @@ +"""ramen-ai TRACE record mapping tests. + +Verifies that build_trace_record correctly maps the committed V5 fixture +receipt onto TRACE Trust Record fields, and that the sign/verify round-trip +and tamper probe work as expected. + +No network access or credentials required. +""" +from __future__ import annotations + +import json +from pathlib import Path + +import agentrust_trace +import pytest + +from ramen_ai_trace import build_trace_record, _validate_receipt + +FIXTURES = Path(__file__).resolve().parents[1] / "examples" / "fixtures" +BUNDLE = FIXTURES / "vector1_allowed.json" +IAT = 1_800_000_000 + + +def _load(name: str) -> dict: + return json.loads((FIXTURES / name).read_text(encoding="utf-8")) + + +def _dummy_jwk() -> dict: + return {"kty": "OKP", "crv": "Ed25519", "x": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="} + + +@pytest.fixture(scope="module") +def key(): + return agentrust_trace.generate_key() + + +@pytest.fixture(scope="module") +def record(): + f = _load("vector1_allowed.json") + return build_trace_record(f["receipt"], iat=IAT, jwk=_dummy_jwk()) + + +# ── field mapping ─────────────────────────────────────────────────────────── + +def test_eat_profile(record): + assert record["eat_profile"] == "tag:agentrust.io,2026:trace-v0.1" + + +def test_subject_is_spiffe_uri(record): + assert record["subject"].startswith("spiffe://ramenai.dev/evaluation/") + + +def test_policy_bundle_hash_has_sha256_prefix(record): + assert record["policy"]["bundle_hash"].startswith("sha256:") + + +def test_policy_enforcement_mode(record): + assert record["policy"]["enforcement_mode"] == "enforce" + + +def test_runtime_platform(record): + assert record["runtime"]["platform"] == "software-only" + + +def test_tool_transcript_call_count(record): + assert record["tool_transcript"]["call_count"] == 1 + + +def test_tool_transcript_hash_has_sha256_prefix(record): + assert record["tool_transcript"]["hash"].startswith("sha256:") + + +def test_appraisal_status_affirming_for_allowed(record): + assert record["appraisal"]["status"] == "affirming" + + +def test_no_cmcp_envelope_markers(record): + assert not {"signature", "trace", "gateway"} & record.keys() + + +def test_fields_map_from_fixture(record): + f = _load("vector1_allowed.json") + payload = json.loads(f["receipt"]["canonical_payload"]) + assert record["policy"]["bundle_hash"] == f"sha256:{payload['payload_hash']}" + assert record["runtime"]["measurement"] == f["receipt"]["id"] + assert record["tool_transcript"]["hash"] == f"sha256:{payload['payload_hash']}" + assert payload["policy_ids"][0] in record["appraisal"]["policy_ref"] + + +# ── sign/verify round-trip ─────────────────────────────────────────────────── + +def test_sign_verify_roundtrip(record): + k = agentrust_trace.generate_key() + signed = agentrust_trace.sign_record(dict(record), k) + agentrust_trace.verify_record(signed, allow_embedded_key=True, max_age_seconds=None) + + +def test_tampered_record_fails_verification(record): + k = agentrust_trace.generate_key() + signed = agentrust_trace.sign_record(dict(record), k) + signed["policy"]["bundle_hash"] = "sha256:" + "f" * 64 + with pytest.raises(Exception): + agentrust_trace.verify_record(signed, allow_embedded_key=True, max_age_seconds=None) + + +# ── _validate_receipt guards ───────────────────────────────────────────────── + +def test_missing_field_raises(): + with pytest.raises(ValueError, match="missing required fields"): + _validate_receipt({"id": "x", "schema_version": "5.0", "kid": "k"}) + + +def test_wrong_schema_version_raises(): + f = _load("vector1_allowed.json") + with pytest.raises(ValueError, match="schema_version"): + _validate_receipt(dict(f["receipt"], schema_version="4.0"))