From e5b394201b0307db669e46b9bc7ac1ee3853c04c Mon Sep 17 00:00:00 2001 From: oluwajuwon omotayo Date: Fri, 26 Jun 2026 19:05:17 +0100 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20add=20comply54=20=E2=86=92=20TRACE?= =?UTF-8?q?=20v0.1=20integration=20(Level=200)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a comply54 integration that converts a ComplianceResult from the comply54 African regulatory compliance library into a signed TRACE v0.1 JWT (Ed25519, Level 0 software-only conformance). What's included: - integration.yaml — schema-validated manifest - src/comply54_to_trace.py — adapter: ComplianceResult JSON → TRACE JWT - tests/test_comply54_to_trace.py — 20 passing tests covering appraisal mapping, envelope fields, comply54 extension claims, and JWT signing - requirements.txt — PyJWT + cryptography - README.md — usage, conformance table, limitations Decision mapping: allow → affirming | audit → advisory | escalate → warning | deny → contraindicated Policy bundle hash: SHA-256 of sorted comply54 pack IDs (reproducible). Conforms to TRACE v0.1 at Level 0. Hardware fields are placeholders. Signed-off-by: oluwajuwon omotayo Signed-off-by: oluwajuwon omotayo --- integrations/comply54/README.md | 144 ++++++++++++++ integrations/comply54/integration.yaml | 17 ++ integrations/comply54/requirements.txt | 2 + integrations/comply54/src/__init__.py | 3 + .../comply54_to_trace.cpython-314.pyc | Bin 0 -> 7666 bytes .../comply54/src/comply54_to_trace.py | 175 +++++++++++++++++ integrations/comply54/tests/__init__.py | 0 .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 188 bytes ...ly54_to_trace.cpython-314-pytest-9.1.1.pyc | Bin 0 -> 26001 bytes .../comply54/tests/test_comply54_to_trace.py | 181 ++++++++++++++++++ 10 files changed, 522 insertions(+) create mode 100644 integrations/comply54/README.md create mode 100644 integrations/comply54/integration.yaml create mode 100644 integrations/comply54/requirements.txt create mode 100644 integrations/comply54/src/__init__.py create mode 100644 integrations/comply54/src/__pycache__/comply54_to_trace.cpython-314.pyc create mode 100644 integrations/comply54/src/comply54_to_trace.py create mode 100644 integrations/comply54/tests/__init__.py create mode 100644 integrations/comply54/tests/__pycache__/__init__.cpython-314.pyc create mode 100644 integrations/comply54/tests/__pycache__/test_comply54_to_trace.cpython-314-pytest-9.1.1.pyc create mode 100644 integrations/comply54/tests/test_comply54_to_trace.py diff --git a/integrations/comply54/README.md b/integrations/comply54/README.md new file mode 100644 index 0000000..01e2806 --- /dev/null +++ b/integrations/comply54/README.md @@ -0,0 +1,144 @@ +# comply54 → TRACE Adapter + +Converts a [comply54](https://github.com/comply54/comply54) `ComplianceResult` JSON into a **signed TRACE v0.1 JWT** (Ed25519). + +comply54 evaluates AI agent actions against African regulatory frameworks (NDPA 2023, CBN Transaction Controls, KDPA 2019, POPIA, and 9 other jurisdictions). This adapter maps the compliance decision into the TRACE attestation format so the policy outcome becomes a cryptographically verifiable evidence record. + +## Conformance Level + +**Level 0 (software-only)** — No hardware TEE attestation. The JWT is signed with Ed25519 and carries all required TRACE envelope fields. Runtime fields use software-simulated placeholders; hardware fields are marked `not-attested`. + +| Check | Status | +|-------|--------| +| `eat_profile` = `tag:agentrust.io,2026:trace-v0.1` | ✅ | +| `iat` (integer Unix timestamp) | ✅ | +| `subject` (SPIFFE URI) | ✅ | +| `cnf.jwk` with Ed25519 public key | ✅ | +| `policy.bundle_hash` (SHA-256 of sorted pack IDs) | ✅ | +| `appraisal.status` mapped from comply54 decision | ✅ | +| Ed25519 signature binding | ✅ | +| Hardware TEE measurement | ❌ Level 0 — placeholder | + +## Decision → Appraisal Mapping + +| comply54 `overall` | TRACE `appraisal.status` | +|--------------------|--------------------------| +| `allow` | `affirming` | +| `audit` | `advisory` | +| `escalate` | `warning` | +| `deny` | `contraindicated` | + +## Usage + +**1. Install dependencies** + +```bash +pip install comply54 PyJWT cryptography +``` + +**2. Generate a comply54 ComplianceResult and save as JSON** + +```python +from comply54 import NigeriaFintechCompliance +import json + +compliance = NigeriaFintechCompliance() +result = compliance.check( + "transfer_funds", + {"amount": 15_000_000, "currency": "NGN"}, + context={"kyc_tier": 3}, +) +with open("result.json", "w") as f: + json.dump(result.model_dump(mode="json"), f, default=str) +``` + +**3. Convert to TRACE JWT** + +```bash +python src/comply54_to_trace.py result.json \ + --agent-id payments-agent \ + --model anthropic/claude-sonnet-4-6 +``` + +Output: `claim.jwt` (signed JWT, compact format) + printed to stdout. + +**4. Inspect the JWT payload** + +```bash +python -c " +import jwt +payload = jwt.decode(open('claim.jwt').read(), options={'verify_signature': False}) +print('eat_profile:', payload['eat_profile']) +print('appraisal: ', payload['appraisal']) +print('comply54: ', payload['comply54']) +" +``` + +**5. Run tests** + +```bash +pip install pytest +python -m pytest integrations/comply54/tests/ -v +``` + +All 20 tests should pass. + +## Key Management + +By default a fresh Ed25519 key is generated per run (suitable for testing and CI). + +For persistent keys, set `TRACE_PRIVATE_KEY_PEM`: + +```bash +# Generate a persistent key +python -c " +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption +key = Ed25519PrivateKey.generate() +print(key.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, NoEncryption()).decode()) +" > signing.pem + +export TRACE_PRIVATE_KEY_PEM="$(cat signing.pem)" +python src/comply54_to_trace.py result.json +``` + +## What is verified + +- `eat_profile` is exactly `tag:agentrust.io,2026:trace-v0.1` +- `policy.bundle_hash` is `sha256:` + hex(SHA-256(JSON-sorted pack IDs)) — reproducible from the same comply54 result +- `appraisal.status` matches the comply54 decision using the mapping table above +- `comply54.audit_id` matches the `audit_id` from the source ComplianceResult +- JWT is signed with Ed25519; public key is embedded in `cnf.jwk` + +## Limitations + +- Hardware attestation fields (`runtime.measurement`, `build_provenance.digest`, `model.weights_digest`) are software-simulated placeholders. Level 1/2 requires running comply54 inside a TEE (AMD SEV-SNP, Intel TDX, or equivalent). +- `transparency` is empty — no SCITT log anchor at Level 0. +- Model identity fields reflect what the caller passes via `--model`. comply54 evaluates policy against the agent's action; it does not independently verify which model ran. + +## comply54 extension claims + +The JWT carries a non-standard `comply54` object with: + +```json +{ + "comply54": { + "audit_id": "uuid", + "overall": "deny", + "jurisdictions": ["NG", "KE", "ZA"], + "packs_evaluated": ["nigeria/cbn", "nigeria/ndpa", "universal/pii-leakage"], + "violations": [ + { + "pack": "nigeria/cbn", + "regulation": "CBN Transaction Controls", + "action": "deny", + "messages": ["CBN NIP cap exceeded: ₦15,000,000 > ₦10,000,000 limit"] + } + ] + } +} +``` + +## Repository + +[comply54/comply54](https://github.com/comply54/comply54) diff --git a/integrations/comply54/integration.yaml b/integrations/comply54/integration.yaml new file mode 100644 index 0000000..c6ca846 --- /dev/null +++ b/integrations/comply54/integration.yaml @@ -0,0 +1,17 @@ +name: comply54 +vendor: comply54 +integrates_with: + - trace +description: >- + Converts a comply54 ComplianceResult into a signed TRACE v0.1 JWT, + mapping African regulatory policy decisions to verifiable attestation claims. +maintainer: + github: kingztech2019 + email: oluwajuwon.falore@sagegreytech.com +repository: https://github.com/comply54/comply54 +homepage: https://comply54.io +license: Apache-2.0 +tier: community +trace_conformance_level: 0 +tested_against: + agentrust-trace: "0.2.0" diff --git a/integrations/comply54/requirements.txt b/integrations/comply54/requirements.txt new file mode 100644 index 0000000..8a1b154 --- /dev/null +++ b/integrations/comply54/requirements.txt @@ -0,0 +1,2 @@ +PyJWT>=2.8.0 +cryptography>=42.0.0 diff --git a/integrations/comply54/src/__init__.py b/integrations/comply54/src/__init__.py new file mode 100644 index 0000000..10a2305 --- /dev/null +++ b/integrations/comply54/src/__init__.py @@ -0,0 +1,3 @@ +from .comply54_to_trace import comply54_to_trace_payload, load_or_generate_key, private_key_to_jwk + +__all__ = ["comply54_to_trace_payload", "load_or_generate_key", "private_key_to_jwk"] diff --git a/integrations/comply54/src/__pycache__/comply54_to_trace.cpython-314.pyc b/integrations/comply54/src/__pycache__/comply54_to_trace.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bd44c15849cda5cab675b3050ee1b4197335e492 GIT binary patch literal 7666 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 literal 0 HcmV?d00001 diff --git a/integrations/comply54/src/comply54_to_trace.py b/integrations/comply54/src/comply54_to_trace.py new file mode 100644 index 0000000..eab8e15 --- /dev/null +++ b/integrations/comply54/src/comply54_to_trace.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +""" +comply54 -> TRACE v0.1 Adapter +Reads a comply54 ComplianceResult JSON and emits a signed TRACE v0.1 JWT (Ed25519). +Conforms to TRACE spec at Level 0 (software-only; no hardware TEE attestation). + +Usage: + python comply54_to_trace.py result.json + python comply54_to_trace.py result.json --agent-id payments-agent --model anthropic/claude-sonnet-4-6 + +The JWT is written to claim.jwt and printed to stdout. +Set TRACE_PRIVATE_KEY_PEM to supply a persistent Ed25519 key; otherwise a +fresh key is generated per run (suitable for testing). +""" + +import argparse +import base64 +import hashlib +import json +import os +import sys +import time +from pathlib import Path + +import jwt +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + + +# ── comply54 appraisal → TRACE appraisal status ─────────────────────────────── + +_APPRAISAL_MAP = { + "allow": "affirming", + "audit": "advisory", + "escalate": "warning", + "deny": "contraindicated", +} + + +# ── Key helpers ─────────────────────────────────────────────────────────────── + +def load_or_generate_key() -> Ed25519PrivateKey: + pem = os.environ.get("TRACE_PRIVATE_KEY_PEM") + if pem: + return serialization.load_pem_private_key(pem.encode(), password=None) + return Ed25519PrivateKey.generate() + + +def private_key_to_jwk(key: Ed25519PrivateKey) -> dict: + raw = key.public_key().public_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, + ) + return { + "kty": "OKP", + "crv": "Ed25519", + "x": base64.urlsafe_b64encode(raw).decode().rstrip("="), + } + + +# ── Mapping ─────────────────────────────────────────────────────────────────── + +def comply54_to_trace_payload(result: dict, agent_id: str, model: str) -> dict: + overall = result.get("overall", "deny") + audit_id = result.get("audit_id", "unknown") + decisions = result.get("decisions", []) + + # Policy bundle hash: deterministic fingerprint of the pack IDs that ran + pack_ids = sorted({d.get("pack", "") for d in decisions if d.get("pack")}) + bundle_input = json.dumps(pack_ids, sort_keys=True).encode() + bundle_hash = f"sha256:{hashlib.sha256(bundle_input).hexdigest()}" + + # Jurisdictions covered by this evaluation + jurisdictions = sorted({d.get("jurisdiction", "") for d in decisions if d.get("jurisdiction")}) + + # Violations summary (embedded as a non-standard extension claim) + violations = [ + { + "pack": d.get("pack"), + "regulation": d.get("regulation"), + "action": d.get("action"), + "messages": d.get("messages", [])[:1], # first message only for compactness + } + for d in decisions + if d.get("action") != "allow" + ] + + appraisal_status = _APPRAISAL_MAP.get(overall, "contraindicated") + + provider, _, model_id = model.partition("/") + + return { + # ── Required TRACE EAT envelope ────────────────────────────────────── + "eat_profile": "tag:agentrust.io,2026:trace-v0.1", + "iat": int(time.time()), + "subject": f"spiffe://comply54.io/agent/{agent_id}", + + # ── Model identity (the agent being governed, not comply54) ────────── + "model": { + "provider": provider or "unknown", + "model_id": model_id or model, + "version": "unknown", + "weights_digest": "sha256:not-attested", + }, + + # ── Runtime (software-only — Level 0) ──────────────────────────────── + "runtime": { + "platform": "software-only", + "measurement": "sha384:" + "0" * 96, + "rim_uri": "https://github.com/comply54/comply54", + }, + + # ── Policy evidence: comply54 pack bundle ──────────────────────────── + "policy": { + "bundle_hash": bundle_hash, + "enforcement_mode": "enforce", + "version": "0.1.0", + }, + + # ── Data classification ─────────────────────────────────────────────── + "data_class": "confidential", + + # ── Build provenance (software-only) ───────────────────────────────── + "build_provenance": { + "slsa_level": 0, + "builder": "https://github.com/comply54/comply54", + "digest": "sha256:not-attested", + }, + + # ── Appraisal: the comply54 decision ───────────────────────────────── + "appraisal": { + "status": appraisal_status, + "verifier": "https://github.com/comply54/comply54", + "policy_ref": f"comply54-v0.1.0/{','.join(pack_ids)}", + }, + + # ── Transparency (no log anchor at Level 0) ─────────────────────────── + "transparency": "", + + # ── comply54-specific extension claims ─────────────────────────────── + "comply54": { + "audit_id": audit_id, + "overall": overall, + "jurisdictions": jurisdictions, + "packs_evaluated": pack_ids, + "violations": violations, + }, + } + + +# ── CLI ─────────────────────────────────────────────────────────────────────── + +def main() -> None: + parser = argparse.ArgumentParser(description="Convert comply54 ComplianceResult to TRACE v0.1 JWT") + parser.add_argument("result_json", help="Path to comply54 ComplianceResult JSON file") + parser.add_argument("--agent-id", default="fintech-agent", help="Agent SPIFFE identity suffix") + parser.add_argument("--model", default="unknown/unknown", help="Model in provider/model-id format") + parser.add_argument("--out", default="claim.jwt", help="Output JWT file (default: claim.jwt)") + args = parser.parse_args() + + with open(args.result_json) as f: + result = json.load(f) + + payload = comply54_to_trace_payload(result, args.agent_id, args.model) + key = load_or_generate_key() + payload["cnf"] = {"jwk": private_key_to_jwk(key)} + + token = jwt.encode(payload, key, algorithm="EdDSA", headers={"alg": "EdDSA", "typ": "JWT"}) + + Path(args.out).write_text(token) + print(token) + + +if __name__ == "__main__": + main() diff --git a/integrations/comply54/tests/__init__.py b/integrations/comply54/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/integrations/comply54/tests/__pycache__/__init__.cpython-314.pyc b/integrations/comply54/tests/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1ce8d1954d6ba0d535b7d18554f6b992cc43a9b7 GIT binary patch literal 188 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 aR|3H2uLbr{BEaV_26hLIJQ4ud?D~JDHe<^G literal 0 HcmV?d00001 diff --git a/integrations/comply54/tests/test_comply54_to_trace.py b/integrations/comply54/tests/test_comply54_to_trace.py new file mode 100644 index 0000000..b35a6c6 --- /dev/null +++ b/integrations/comply54/tests/test_comply54_to_trace.py @@ -0,0 +1,181 @@ +""" +Tests for comply54 -> TRACE v0.1 adapter. +Run: pip install -r requirements.txt && python -m pytest tests/ -v +""" + +import json +import base64 +import pytest +import jwt as pyjwt +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + +import sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).parent.parent / "src")) + +from comply54_to_trace import comply54_to_trace_payload, load_or_generate_key, private_key_to_jwk + + +# ── Fixtures ────────────────────────────────────────────────────────────────── + +ALLOW_RESULT = { + "overall": "allow", + "audit_id": "test-audit-001", + "decisions": [ + {"pack": "nigeria/cbn", "regulation": "CBN Transaction Controls", + "jurisdiction": "NG", "action": "allow", "messages": []}, + {"pack": "nigeria/ndpa", "regulation": "Nigeria Data Protection Act 2023", + "jurisdiction": "NG", "action": "allow", "messages": []}, + {"pack": "universal/pii-leakage", "regulation": "OWASP LLM06", + "jurisdiction": "UNIVERSAL", "action": "allow", "messages": []}, + ], +} + +DENY_RESULT = { + "overall": "deny", + "audit_id": "test-audit-002", + "decisions": [ + {"pack": "nigeria/cbn", "regulation": "CBN Transaction Controls", + "jurisdiction": "NG", "action": "deny", + "messages": ["CBN NIP cap exceeded: ₦15,000,000 > ₦10,000,000 limit"]}, + {"pack": "nigeria/ndpa", "regulation": "Nigeria Data Protection Act 2023", + "jurisdiction": "NG", "action": "allow", "messages": []}, + ], +} + +ESCALATE_RESULT = { + "overall": "escalate", + "audit_id": "test-audit-003", + "decisions": [ + {"pack": "nigeria/nfiu-aml", "regulation": "NFIU AML Guidelines", + "jurisdiction": "NG", "action": "escalate", + "messages": ["Currency Transaction Report required: ₦6,000,000 exceeds ₦5,000,000 threshold"]}, + ], +} + +PAN_AFRICAN_RESULT = { + "overall": "deny", + "audit_id": "test-audit-004", + "decisions": [ + {"pack": "nigeria/cbn", "regulation": "CBN Transaction Controls", "jurisdiction": "NG", "action": "deny", "messages": ["CBN NIP cap exceeded"]}, + {"pack": "kenya/kdpa", "regulation": "Kenya Data Protection Act 2019", "jurisdiction": "KE", "action": "allow", "messages": []}, + {"pack": "south-africa/popia", "regulation": "POPIA", "jurisdiction": "ZA", "action": "allow", "messages": []}, + {"pack": "ghana/dpa", "regulation": "Ghana DPA 2012", "jurisdiction": "GH", "action": "allow", "messages": []}, + ], +} + + +# ── Appraisal mapping ───────────────────────────────────────────────────────── + +class TestAppraisalMapping: + def test_allow_maps_to_affirming(self): + payload = comply54_to_trace_payload(ALLOW_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + assert payload["appraisal"]["status"] == "affirming" + + def test_deny_maps_to_contraindicated(self): + payload = comply54_to_trace_payload(DENY_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + assert payload["appraisal"]["status"] == "contraindicated" + + def test_escalate_maps_to_warning(self): + payload = comply54_to_trace_payload(ESCALATE_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + assert payload["appraisal"]["status"] == "warning" + + +# ── Required TRACE EAT envelope fields ─────────────────────────────────────── + +class TestTraceEnvelope: + def test_eat_profile_present(self): + payload = comply54_to_trace_payload(ALLOW_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + assert payload["eat_profile"] == "tag:agentrust.io,2026:trace-v0.1" + + def test_iat_is_integer(self): + payload = comply54_to_trace_payload(ALLOW_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + assert isinstance(payload["iat"], int) + assert payload["iat"] > 0 + + def test_subject_contains_agent_id(self): + payload = comply54_to_trace_payload(ALLOW_RESULT, "payments-agent", "anthropic/claude-sonnet-4-6") + assert "payments-agent" in payload["subject"] + assert payload["subject"].startswith("spiffe://") + + def test_policy_bundle_hash_is_sha256(self): + payload = comply54_to_trace_payload(DENY_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + assert payload["policy"]["bundle_hash"].startswith("sha256:") + assert len(payload["policy"]["bundle_hash"]) == 71 # "sha256:" + 64 hex chars + + def test_policy_bundle_hash_is_deterministic(self): + p1 = comply54_to_trace_payload(DENY_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + p2 = comply54_to_trace_payload(DENY_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + assert p1["policy"]["bundle_hash"] == p2["policy"]["bundle_hash"] + + def test_runtime_platform_is_software_only(self): + payload = comply54_to_trace_payload(ALLOW_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + assert payload["runtime"]["platform"] == "software-only" + + def test_model_provider_parsed_correctly(self): + payload = comply54_to_trace_payload(ALLOW_RESULT, "agent-1", "openai/gpt-4o") + assert payload["model"]["provider"] == "openai" + assert payload["model"]["model_id"] == "gpt-4o" + + +# ── comply54 extension claims ───────────────────────────────────────────────── + +class TestComply54Claims: + def test_audit_id_preserved(self): + payload = comply54_to_trace_payload(DENY_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + assert payload["comply54"]["audit_id"] == "test-audit-002" + + def test_overall_decision_preserved(self): + payload = comply54_to_trace_payload(DENY_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + assert payload["comply54"]["overall"] == "deny" + + def test_jurisdictions_extracted(self): + payload = comply54_to_trace_payload(PAN_AFRICAN_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + assert "NG" in payload["comply54"]["jurisdictions"] + assert "KE" in payload["comply54"]["jurisdictions"] + assert "ZA" in payload["comply54"]["jurisdictions"] + + def test_packs_evaluated_sorted(self): + payload = comply54_to_trace_payload(ALLOW_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + packs = payload["comply54"]["packs_evaluated"] + assert packs == sorted(packs) + + def test_violations_only_non_allow(self): + payload = comply54_to_trace_payload(DENY_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + for v in payload["comply54"]["violations"]: + assert v["action"] != "allow" + + def test_allow_result_has_no_violations(self): + payload = comply54_to_trace_payload(ALLOW_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + assert len(payload["comply54"]["violations"]) == 0 + + +# ── JWT signing ─────────────────────────────────────────────────────────────── + +class TestJWTSigning: + def test_key_generation_returns_ed25519(self): + key = load_or_generate_key() + assert isinstance(key, Ed25519PrivateKey) + + def test_jwk_has_correct_fields(self): + key = load_or_generate_key() + jwk = private_key_to_jwk(key) + assert jwk["kty"] == "OKP" + assert jwk["crv"] == "Ed25519" + assert "x" in jwk + + def test_signed_jwt_is_decodable(self): + payload = comply54_to_trace_payload(DENY_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + key = load_or_generate_key() + payload["cnf"] = {"jwk": private_key_to_jwk(key)} + token = pyjwt.encode(payload, key, algorithm="EdDSA", headers={"alg": "EdDSA", "typ": "JWT"}) + decoded = pyjwt.decode(token, options={"verify_signature": False}) + assert decoded["eat_profile"] == "tag:agentrust.io,2026:trace-v0.1" + assert decoded["appraisal"]["status"] == "contraindicated" + + def test_signed_jwt_has_three_parts(self): + payload = comply54_to_trace_payload(ALLOW_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + key = load_or_generate_key() + payload["cnf"] = {"jwk": private_key_to_jwk(key)} + token = pyjwt.encode(payload, key, algorithm="EdDSA", headers={"alg": "EdDSA", "typ": "JWT"}) + assert len(token.split(".")) == 3 From 64841cb403e04b5fa84eb2bacfcfa187fdce05fa Mon Sep 17 00:00:00 2001 From: oluwajuwon omotayo Date: Fri, 26 Jun 2026 19:05:53 +0100 Subject: [PATCH 2/2] chore: remove pycache from comply54 integration Signed-off-by: oluwajuwon omotayo Signed-off-by: oluwajuwon omotayo --- integrations/comply54/.gitignore | 2 ++ .../comply54_to_trace.cpython-314.pyc | Bin 7666 -> 0 bytes .../tests/__pycache__/__init__.cpython-314.pyc | Bin 188 -> 0 bytes ...mply54_to_trace.cpython-314-pytest-9.1.1.pyc | Bin 26001 -> 0 bytes 4 files changed, 2 insertions(+) create mode 100644 integrations/comply54/.gitignore delete mode 100644 integrations/comply54/src/__pycache__/comply54_to_trace.cpython-314.pyc delete mode 100644 integrations/comply54/tests/__pycache__/__init__.cpython-314.pyc delete mode 100644 integrations/comply54/tests/__pycache__/test_comply54_to_trace.cpython-314-pytest-9.1.1.pyc diff --git a/integrations/comply54/.gitignore b/integrations/comply54/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/integrations/comply54/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/integrations/comply54/src/__pycache__/comply54_to_trace.cpython-314.pyc b/integrations/comply54/src/__pycache__/comply54_to_trace.cpython-314.pyc deleted file mode 100644 index bd44c15849cda5cab675b3050ee1b4197335e492..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7666 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 diff --git a/integrations/comply54/tests/__pycache__/__init__.cpython-314.pyc b/integrations/comply54/tests/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index 1ce8d1954d6ba0d535b7d18554f6b992cc43a9b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 188 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 aR|3H2uLbr{BEaV_26hLIJQ4ud?D~JDHe<^G