From 60ddb962bbd2a2c81929089eb08426a67bbc5fb7 Mon Sep 17 00:00:00 2001 From: Ruslan Vrublevskyi Date: Mon, 5 Oct 2026 17:46:33 +0300 Subject: [PATCH 1/2] Add EvidenceBound Core TRACE integration --- .../evidencebound-core-conformance.yml | 96 +++++ integrations/evidencebound-core/README.md | 192 ++++++++++ .../evidencebound-core/evidencebound_trace.py | 212 +++++++++++ .../examples/emit_record.py | 119 ++++++ .../evidencebound-core/integration.yaml | 26 ++ .../evidencebound-core/pyproject.toml | 24 ++ .../tests/test_evidencebound_trace.py | 346 ++++++++++++++++++ marketplace/catalog.json | 25 +- 8 files changed, 1039 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/evidencebound-core-conformance.yml create mode 100644 integrations/evidencebound-core/README.md create mode 100644 integrations/evidencebound-core/evidencebound_trace.py create mode 100644 integrations/evidencebound-core/examples/emit_record.py create mode 100644 integrations/evidencebound-core/integration.yaml create mode 100644 integrations/evidencebound-core/pyproject.toml create mode 100644 integrations/evidencebound-core/tests/test_evidencebound_trace.py diff --git a/.github/workflows/evidencebound-core-conformance.yml b/.github/workflows/evidencebound-core-conformance.yml new file mode 100644 index 0000000..eeca1de --- /dev/null +++ b/.github/workflows/evidencebound-core-conformance.yml @@ -0,0 +1,96 @@ +name: evidencebound-core conformance + +on: + push: + paths: + - "integrations/evidencebound-core/**" + - ".github/workflows/evidencebound-core-conformance.yml" + pull_request: + paths: + - "integrations/evidencebound-core/**" + - ".github/workflows/evidencebound-core-conformance.yml" + schedule: + - cron: "0 6 * * 1" + workflow_dispatch: + +permissions: + contents: read + +jobs: + conformance: + strategy: + fail-fast: false + matrix: + python: ["3.11", "3.12", "3.13"] + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + persist-credentials: false + + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 + with: + python-version: ${{ matrix.python }} + + - name: Install exact released dependencies + run: | + python -m pip install --upgrade pip==26.2.1 + python -m pip install --no-cache-dir \ + "evidencebound-core[crypto]==0.4.0" \ + "agentrust-trace==0.11.0" \ + "agentrust-trace-tests==0.6.2" \ + "pytest==9.1.1" + + - name: Verify released EvidenceBound install + run: | + python - <<'PY' + import importlib.metadata as metadata + + assert metadata.version("evidencebound-core") == "0.4.0" + dist = metadata.distribution("evidencebound-core") + assert not any( + str(path).endswith("direct_url.json") + for path in (dist.files or ()) + ) + PY + + - name: Install only the thin integration locally + run: python -m pip install --no-deps -e "integrations/evidencebound-core" + + - name: Integration tests + run: python -m pytest integrations/evidencebound-core/tests -q + + - name: Emit synthetic TRACE record + run: | + python integrations/evidencebound-core/examples/emit_record.py \ + --out evidencebound.trace.json + + - name: Determine TRACE conformance level + run: | + trace-tests report \ + --record evidencebound.trace.json \ + --max-level 2 \ + --json evidencebound-trace-report.json + python - <<'PY' + import json + from pathlib import Path + + report = json.loads(Path("evidencebound-trace-report.json").read_text()) + assert report["tooling"]["suite"] == "0.6.2" + assert report["tooling"]["library"] == "0.11.0" + assert report["highest_level_passed"] == 0 + assert report["levels"][0]["passed"] is True + assert report["levels"][1]["passed"] is False + assert report["levels"][2]["passed"] is False + PY + + - name: Enforce declared TRACE Level 0 + run: trace-tests verify --record evidencebound.trace.json --level 0 + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: evidencebound-conformance-${{ matrix.os }}-py${{ matrix.python }} + path: | + evidencebound.trace.json + evidencebound-trace-report.json diff --git a/integrations/evidencebound-core/README.md b/integrations/evidencebound-core/README.md new file mode 100644 index 0000000..8ec3526 --- /dev/null +++ b/integrations/evidencebound-core/README.md @@ -0,0 +1,192 @@ +# EvidenceBound Core integration with TRACE + +This integration re-verifies explicit current EvidenceBound state and signed-receipt binding, then emits a signed TRACE v0.2 record only when the released EvidenceBound verifier returns current `ALLOW` with an ACTIVE verification key. + +The acceptance boundary is: + +`historical integrity != current applicability` + +A previously signed EvidenceBound receipt can remain historically authentic while changed, stale, missing, refuted, or policy-drifted current state is no longer applicable. The adapter therefore re-runs EvidenceBound verification at an explicit verification time before emitting TRACE. + +## Released versions reproduced + +The acceptance run uses only released EvidenceBound artifacts: + +- Python: `3.13.1` +- `evidencebound-core==0.4.0` +- `agentrust-trace==0.11.0` +- `agentrust-trace-tests==0.6.2` + +The integration source itself is this directory. EvidenceBound is not installed from GitHub, an editable EvidenceBound checkout, a local wheel, or unreleased source. + +## Clean-room reproduction + +From the repository root on a machine with Python 3.13.1: + +### POSIX shell + +```bash +python3 -m venv .venv-evidencebound +. .venv-evidencebound/bin/activate +python -m pip install --upgrade pip==26.2.1 +python -m pip install --no-cache-dir \ + "evidencebound-core[crypto]==0.4.0" \ + "agentrust-trace==0.11.0" \ + "agentrust-trace-tests==0.6.2" \ + "pytest==9.1.1" + +python - <<'PY' +import importlib.metadata as metadata +import sys + +expected = { + "evidencebound-core": "0.4.0", + "agentrust-trace": "0.11.0", + "agentrust-trace-tests": "0.6.2", +} +print("PYTHON=" + sys.version.replace("\n", " ")) +for package, version in expected.items(): + actual = metadata.version(package) + print(f"{package}={actual}") + assert actual == version + +dist = metadata.distribution("evidencebound-core") +assert not any(str(path).endswith("direct_url.json") for path in (dist.files or ())) +print("EVIDENCEBOUND_DIRECT_URL=absent") +PY + +python -m pip install --no-deps -e "integrations/evidencebound-core" +python -m pytest integrations/evidencebound-core/tests -q +python integrations/evidencebound-core/examples/emit_record.py \ + --out evidencebound.trace.json + +trace-tests report \ + --record evidencebound.trace.json \ + --max-level 2 \ + --json evidencebound-trace-report.json + +trace-tests verify \ + --record evidencebound.trace.json \ + --level 0 +``` + +### PowerShell + +```powershell +python -m venv .venv-evidencebound +$python = ".\.venv-evidencebound\Scripts\python.exe" +& $python -m pip install --upgrade pip==26.2.1 +& $python -m pip install --no-cache-dir ` + "evidencebound-core[crypto]==0.4.0" ` + "agentrust-trace==0.11.0" ` + "agentrust-trace-tests==0.6.2" ` + "pytest==9.1.1" + +@' +import importlib.metadata as metadata +import sys + +expected = { + "evidencebound-core": "0.4.0", + "agentrust-trace": "0.11.0", + "agentrust-trace-tests": "0.6.2", +} +print("PYTHON=" + sys.version.replace("\n", " ")) +for package, version in expected.items(): + actual = metadata.version(package) + print(f"{package}={actual}") + assert actual == version + +dist = metadata.distribution("evidencebound-core") +assert not any(str(path).endswith("direct_url.json") for path in (dist.files or ())) +print("EVIDENCEBOUND_DIRECT_URL=absent") +'@ | & $python - + +& $python -m pip install --no-deps -e "integrations/evidencebound-core" +& $python -m pytest integrations/evidencebound-core/tests -q +& $python integrations/evidencebound-core/examples/emit_record.py ` + --out evidencebound.trace.json + +& ".\.venv-evidencebound\Scripts\trace-tests.exe" report ` + --record evidencebound.trace.json ` + --max-level 2 ` + --json evidencebound-trace-report.json + +& ".\.venv-evidencebound\Scripts\trace-tests.exe" verify ` + --record evidencebound.trace.json ` + --level 0 +``` + +Expected deterministic integration result: + +```text +15 passed +POSITIVE=TRACE_EMITTED +NEGATIVE_STALE=REFUSED +``` + +The conformance report determines the level rather than assuming one in advance. With the released versions above, the reproduced result is: + +```text +Level 0 PASS +Level 1 FAIL +Level 2 FAIL +highest_level_passed = 0 +``` + +Level 1 fails because this integration deliberately declares a software-only runtime and a non-affirming appraisal; the current suite also requires the verifier challenge nonce at Level 1+. Level 2 additionally requires transcript and transparency-anchor evidence that this adapter does not produce. + +## What is verified + +The deterministic tests cover: + +- positive current-state emission; +- changed evidence rejection; +- stale evidence rejection; +- missing evidence rejection; +- explicitly refuted evidence rejection; +- expected-policy drift rejection; +- tampered EvidenceBound signed-receipt rejection; +- unknown EvidenceBound verification-key rejection; +- revoked EvidenceBound verification-key rejection; +- EvidenceBound v0.4.0 retired-key semantics: `VERIFIED_RETIRED_KEY` remains historically verifiable but is refused for fresh TRACE emission; +- EvidenceBound `valid_until` boundary and timezone-equivalent verification instants; +- exact recomputed-result / signed-receipt binding; +- exact signed EvidenceBound envelope digest binding in the TRACE reference; +- TRACE signature verification against the explicit TRACE signing key; +- the current official `agentrust-trace-tests==0.6.2` suite. + +At the EvidenceBound v0.4.0 time boundary, evidence is still current when `verification_time == valid_until`; it becomes stale after that instant. Equivalent timezone-aware instants produce the same applicability result. Naive verification times are refused by the adapter. + +The emitted TRACE record is a **record-producer, software-only Level 0** record. It sets: + +- `runtime.platform: software-only`; +- `policy.enforcement_mode: declared`; +- `appraisal.status: none`; +- `origin.kind: third-party-control-plane`. + +The TRACE signature binds the emitted record. The EvidenceBound reference digest binds the exact signed EvidenceBound receipt envelope used for the current verification. + +## What this does not claim + +This integration does **not** claim: + +- TEE or hardware attestation; +- truth of upstream evidence; +- non-bypassability or mandatory mediation of the caller's wider system; +- certification; +- independent appraisal; +- that a historical EvidenceBound signature makes stale or changed evidence currently applicable; +- that caller-supplied model, subject, runtime-measurement, build-digest, or data-class metadata was independently observed by EvidenceBound; +- production safety or guarantees outside the released packages and deterministic fixture exercised here. + +A TRACE Level 0 PASS is a conformance result for the emitted record and supplied evidence, not a certification of EvidenceBound or of an entire deployment. + +## Files + +- `evidencebound_trace.py` — thin current-state verification and TRACE emission adapter. +- `tests/test_evidencebound_trace.py` — deterministic positive, negative-control, binding, key-state, time-boundary, and signature tests. +- `examples/emit_record.py` — synthetic positive fixture plus stale-evidence negative control. +- `integration.yaml` — AgenTrust integration manifest. + +The fixture authorities, evidence, policy, subject, model metadata, keys, and outcomes are synthetic. diff --git a/integrations/evidencebound-core/evidencebound_trace.py b/integrations/evidencebound-core/evidencebound_trace.py new file mode 100644 index 0000000..8c25f9a --- /dev/null +++ b/integrations/evidencebound-core/evidencebound_trace.py @@ -0,0 +1,212 @@ +"""Thin EvidenceBound Core -> TRACE v0.2 interoperability adapter. + +The adapter re-runs released EvidenceBound verification against explicit current +inputs, authenticates the supplied EvidenceBound signed receipt against that exact +result, and emits a TRACE record only for a current ACTIVE-key ALLOW result. + +It does not treat signatures as proof that upstream evidence is truthful. +""" +from __future__ import annotations + +import hashlib +import time +from dataclasses import dataclass +from datetime import datetime +from typing import Mapping + +import agentrust_trace +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from evidencebound import ( + ActionDecision, + ApplicabilityStatus, + Checkpoint, + EvidenceRecord, + IntegrityStatus, + PolicyBinding, + ReceiptVerifier, + SignedProofReceipt, + SignedReceiptStatus, + canonical_bytes, + verify_checkpoint, + verify_signed_receipt, +) +from evidencebound import __version__ as evidencebound_version + +EAT_PROFILE = "tag:agentrust-io.com,2026:trace-v0.2" +SOURCE_REL = "evidencebound-verification" +SOFTWARE_ONLY = "software-only" + + +class AdapterError(ValueError): + """Raised when current EvidenceBound state cannot support TRACE emission.""" + + +@dataclass(frozen=True, slots=True) +class TraceContext: + """TRACE metadata supplied by the caller rather than asserted by EvidenceBound.""" + + subject: str + model_provider: str + model_id: str + data_class: str + runtime_measurement: str + build_digest: str + appraisal_verifier: str + evidence_resolver: str + evidence_retention: str | None = None + + +def _sha256(data: bytes) -> str: + return "sha256:" + hashlib.sha256(data).hexdigest() + + +def _require_nonempty(value: str, field: str) -> str: + if not isinstance(value, str) or not value.strip(): + raise AdapterError(f"{field} must be non-empty") + return value.strip() + + +def _require_digest(value: str, field: str) -> str: + if not isinstance(value, str) or not value.startswith("sha256:"): + raise AdapterError(f"{field} must be sha256:<64 lowercase hex>") + body = value.removeprefix("sha256:") + if len(body) != 64 or any(ch not in "0123456789abcdef" for ch in body): + raise AdapterError(f"{field} must be sha256:<64 lowercase hex>") + return value + + +def _validate_context(context: TraceContext) -> None: + subject = _require_nonempty(context.subject, "subject") + if not (subject.startswith("spiffe://") or subject.startswith("did:")): + raise AdapterError("subject must be a SPIFFE or DID URI") + _require_nonempty(context.model_provider, "model_provider") + _require_nonempty(context.model_id, "model_id") + _require_nonempty(context.data_class, "data_class") + _require_digest(context.runtime_measurement, "runtime_measurement") + _require_digest(context.build_digest, "build_digest") + verifier = _require_nonempty(context.appraisal_verifier, "appraisal_verifier") + if ":" not in verifier: + raise AdapterError("appraisal_verifier must be an absolute URI") + _require_nonempty(context.evidence_resolver, "evidence_resolver") + + +def signed_receipt_reference_digest(receipt: SignedProofReceipt) -> str: + """Digest the exact signed EvidenceBound envelope deterministically.""" + return _sha256(canonical_bytes(receipt.to_dict())) + + +def policy_binding_digest(checkpoint: Checkpoint) -> str: + """Digest the exact EvidenceBound PolicyBinding carried by the checkpoint.""" + return _sha256(canonical_bytes(checkpoint.policy.to_dict())) + + +def emit_trace_record( + *, + checkpoint: Checkpoint, + signed_receipt: SignedProofReceipt, + evidencebound_verifier: ReceiptVerifier, + current_evidence: Mapping[str, EvidenceRecord], + expected_policy: PolicyBinding, + verification_time: datetime, + context: TraceContext, + trace_signing_key: Ed25519PrivateKey, + issued_at: int | None = None, +) -> dict[str, object]: + """Emit TRACE only for current, authenticated EvidenceBound ALLOW. + + Sequence: + 1. re-run EvidenceBound verification on explicit current inputs; + 2. verify the signed receipt against that exact recomputed result; + 3. require ACTIVE-key VERIFIED status; + 4. require integrity/applicability VERIFIED and action ALLOW; + 5. sign and schema-validate the TRACE record. + """ + _validate_context(context) + if verification_time.tzinfo is None or verification_time.utcoffset() is None: + raise AdapterError("verification_time must be timezone-aware") + + current = verify_checkpoint( + checkpoint, + current_evidence=current_evidence, + expected_policy=expected_policy, + prior_receipt=signed_receipt.receipt, + now=verification_time, + ) + + authenticated = verify_signed_receipt( + signed_receipt, + checkpoint, + evidencebound_verifier, + verification_result=current, + ) + if authenticated.status is not SignedReceiptStatus.VERIFIED: + raise AdapterError( + "EvidenceBound signed receipt is not an ACTIVE-key VERIFIED binding: " + f"{authenticated.status.value}" + ) + + if current.integrity is not IntegrityStatus.VERIFIED: + raise AdapterError(f"EvidenceBound integrity is {current.integrity.value}") + if current.applicability is not ApplicabilityStatus.VERIFIED: + raise AdapterError( + f"EvidenceBound applicability is {current.applicability.value}" + ) + if current.action is not ActionDecision.ALLOW: + raise AdapterError(f"EvidenceBound action is {current.action.value}") + + iat = int(time.time()) if issued_at is None else issued_at + if not isinstance(iat, int) or isinstance(iat, bool): + raise AdapterError("issued_at must be integer epoch seconds") + + reference: dict[str, object] = { + "rel": SOURCE_REL, + "id": checkpoint.checkpoint_id, + "resolver": context.evidence_resolver, + "digest": signed_receipt_reference_digest(signed_receipt), + } + if context.evidence_retention: + reference["retention"] = context.evidence_retention + + record: dict[str, object] = { + "eat_profile": EAT_PROFILE, + "iat": iat, + "subject": context.subject, + "model": { + "provider": context.model_provider, + "model_id": context.model_id, + }, + "runtime": { + "platform": SOFTWARE_ONLY, + "measurement": context.runtime_measurement, + }, + "policy": { + "bundle_hash": policy_binding_digest(checkpoint), + "enforcement_mode": "declared", + "version": checkpoint.policy.version, + }, + "data_class": context.data_class, + "build_provenance": { + "slsa_level": 0, + "digest": context.build_digest, + }, + "appraisal": { + "status": "none", + "verifier": context.appraisal_verifier, + }, + "origin": { + "kind": "third-party-control-plane", + "producer": f"evidencebound-core/{evidencebound_version}", + "source_event_id": checkpoint.checkpoint_id, + }, + "references": [reference], + } + + signed = agentrust_trace.sign_record(record, trace_signing_key) + try: + agentrust_trace.validate_json(signed) + except Exception as exc: + raise AdapterError( + "generated TRACE record failed released schema validation: " + f"{type(exc).__name__}: {exc}" + ) from exc + return signed diff --git a/integrations/evidencebound-core/examples/emit_record.py b/integrations/evidencebound-core/examples/emit_record.py new file mode 100644 index 0000000..9597315 --- /dev/null +++ b/integrations/evidencebound-core/examples/emit_record.py @@ -0,0 +1,119 @@ +"""Synthetic released-package EvidenceBound -> TRACE reproduction fixture.""" +from __future__ import annotations + +import argparse +import hashlib +import json +from dataclasses import replace +from datetime import datetime, timezone +from pathlib import Path + +from agentrust_trace import generate_key +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from evidencebound import ( + Checkpoint, + EvidenceRecord, + KeyState, + PolicyBinding, + ProvenanceRecord, + sign_receipt, + verify_checkpoint, +) +from evidencebound.providers.ed25519 import ( + Ed25519Signer, + Ed25519VerificationKey, + Ed25519Verifier, +) + +from evidencebound_trace import AdapterError, TraceContext, emit_trace_record + +NOW = datetime(2026, 10, 5, 8, 0, tzinfo=timezone.utc) +POLICY = PolicyBinding("policy.synthetic.transfer", "1") +PROVENANCE = ProvenanceRecord("synthetic-fixture", "urn:fixture:evidence:1") + + +def digest(label: str) -> str: + return "sha256:" + hashlib.sha256(label.encode()).hexdigest() + + +def fixture(): + evidence = EvidenceRecord( + "e1", + {"approved": True, "amount": 100}, + PROVENANCE, + valid_until="2026-10-06T00:00:00Z", + ) + checkpoint = Checkpoint( + "cp-agentrust-1", + "synthetic-agent", + (evidence,), + {"decision": "eligible"}, + POLICY, + ) + current = {"e1": evidence} + result = verify_checkpoint( + checkpoint, + current_evidence=current, + expected_policy=POLICY, + now=NOW, + ) + private = Ed25519PrivateKey.generate() + signed = sign_receipt(result.receipt, Ed25519Signer(private, "eb-active")) + verifier = Ed25519Verifier( + {"eb-active": Ed25519VerificationKey(private.public_key(), KeyState.ACTIVE)} + ) + trace_context = TraceContext( + subject="spiffe://example.test/agent/evidencebound-demo", + model_provider="synthetic", + model_id="synthetic-model-v1", + data_class="internal", + runtime_measurement=digest("synthetic-runtime"), + build_digest=digest("evidencebound-trace-adapter-fixture"), + appraisal_verifier="https://evidencebound.org", + evidence_resolver="EvidenceBound", + ) + return checkpoint, current, signed, verifier, trace_context + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--out", required=True) + args = parser.parse_args() + + checkpoint, current, signed, verifier, trace_context = fixture() + trace = emit_trace_record( + checkpoint=checkpoint, + signed_receipt=signed, + evidencebound_verifier=verifier, + current_evidence=current, + expected_policy=POLICY, + verification_time=NOW, + context=trace_context, + trace_signing_key=generate_key(), + ) + Path(args.out).write_text( + json.dumps(trace, sort_keys=True, indent=2), + encoding="utf-8", + ) + print("POSITIVE=TRACE_EMITTED") + + stale = {"e1": replace(current["e1"], valid_until="2026-10-04T00:00:00Z")} + try: + emit_trace_record( + checkpoint=checkpoint, + signed_receipt=signed, + evidencebound_verifier=verifier, + current_evidence=stale, + expected_policy=POLICY, + verification_time=NOW, + context=trace_context, + trace_signing_key=generate_key(), + ) + except AdapterError: + print("NEGATIVE_STALE=REFUSED") + else: + raise SystemExit("negative control failed: stale EvidenceBound state emitted TRACE") + + +if __name__ == "__main__": + main() diff --git a/integrations/evidencebound-core/integration.yaml b/integrations/evidencebound-core/integration.yaml new file mode 100644 index 0000000..ea70db2 --- /dev/null +++ b/integrations/evidencebound-core/integration.yaml @@ -0,0 +1,26 @@ +name: EvidenceBound Core +vendor: EvidenceBound +integrates_with: + - trace +description: >- + Re-verifies explicit current EvidenceBound state and signed-receipt binding, then emits a software-only TRACE record only for current ALLOW. +maintainer: + github: Evalbound +repository: https://github.com/evidencebound/evidencebound-core +homepage: https://evidencebound.org +license: Apache-2.0 +tier: community +marketplace: + category: Evidence & receipts + mark: EB + keywords: + - evidence + - provenance + - authority + - receipts +trace_roles: + - record-producer +trace_conformance_level: 0 +tested_against: + agentrust-trace: "0.11.0" + agentrust-trace-tests: "0.6.2" diff --git a/integrations/evidencebound-core/pyproject.toml b/integrations/evidencebound-core/pyproject.toml new file mode 100644 index 0000000..b8a40ec --- /dev/null +++ b/integrations/evidencebound-core/pyproject.toml @@ -0,0 +1,24 @@ +[build-system] +requires = ["setuptools>=77", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "evidencebound-agentrust-integration" +version = "0.1.0" +requires-python = ">=3.11" +dependencies = [ + "evidencebound-core[crypto]==0.4.0", + "agentrust-trace==0.11.0", +] + +[project.optional-dependencies] +test = [ + "pytest==9.1.1", + "agentrust-trace-tests==0.6.2", +] + +[tool.setuptools] +py-modules = ["evidencebound_trace"] + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/integrations/evidencebound-core/tests/test_evidencebound_trace.py b/integrations/evidencebound-core/tests/test_evidencebound_trace.py new file mode 100644 index 0000000..c6d2eda --- /dev/null +++ b/integrations/evidencebound-core/tests/test_evidencebound_trace.py @@ -0,0 +1,346 @@ +from __future__ import annotations + +import hashlib +import importlib.metadata as metadata +from dataclasses import replace +from datetime import datetime, timedelta, timezone + +import agentrust_trace +import pytest +from agentrust_trace import generate_key +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from evidencebound import ( + Checkpoint, + EvidenceRecord, + KeyState, + PolicyBinding, + ProvenanceRecord, + SignedReceiptStatus, + sign_receipt, + verify_checkpoint, + verify_signed_receipt, +) +from evidencebound.providers.ed25519 import ( + Ed25519Signer, + Ed25519VerificationKey, + Ed25519Verifier, +) + +from evidencebound_trace import ( + AdapterError, + TraceContext, + emit_trace_record, + signed_receipt_reference_digest, +) + +NOW = datetime(2026, 10, 5, 8, 0, tzinfo=timezone.utc) +ISSUED_AT = int(NOW.timestamp()) +POLICY = PolicyBinding("policy.synthetic.transfer", "1") +PROVENANCE = ProvenanceRecord("synthetic-fixture", "urn:fixture:evidence:1") + + +def digest(label: str) -> str: + return "sha256:" + hashlib.sha256(label.encode()).hexdigest() + + +def context() -> TraceContext: + return TraceContext( + subject="spiffe://example.test/agent/evidencebound-demo", + model_provider="synthetic", + model_id="synthetic-model-v1", + data_class="internal", + runtime_measurement=digest("synthetic-runtime"), + build_digest=digest("evidencebound-trace-adapter-fixture"), + appraisal_verifier="https://evidencebound.org", + evidence_resolver="EvidenceBound", + ) + + +def snapshot(*, valid_until: str = "2026-10-06T00:00:00Z") -> EvidenceRecord: + return EvidenceRecord( + "e1", + {"approved": True, "amount": 100}, + PROVENANCE, + valid_until=valid_until, + ) + + +def checkpoint(evidence: EvidenceRecord | None = None) -> Checkpoint: + return Checkpoint( + "cp-agentrust-1", + "synthetic-agent", + (evidence or snapshot(),), + {"decision": "eligible"}, + POLICY, + ) + + +def signed_current_allow( + cp: Checkpoint, + current: dict[str, EvidenceRecord], + *, + key_state: KeyState = KeyState.ACTIVE, + key_id: str = "eb-key", + now: datetime = NOW, +): + result = verify_checkpoint( + cp, + current_evidence=current, + expected_policy=POLICY, + now=now, + ) + private = Ed25519PrivateKey.generate() + signed = sign_receipt(result.receipt, Ed25519Signer(private, key_id)) + verifier = Ed25519Verifier( + {key_id: Ed25519VerificationKey(private.public_key(), key_state)} + ) + return result, signed, verifier + + +def emit( + cp: Checkpoint, + signed, + verifier, + current, + *, + policy: PolicyBinding = POLICY, + verification_time: datetime = NOW, + trace_key=None, +): + return emit_trace_record( + checkpoint=cp, + signed_receipt=signed, + evidencebound_verifier=verifier, + current_evidence=current, + expected_policy=policy, + verification_time=verification_time, + context=context(), + trace_signing_key=trace_key or generate_key(), + issued_at=ISSUED_AT, + ) + + +def test_released_package_identity_and_no_direct_url_for_evidencebound() -> None: + assert metadata.version("evidencebound-core") == "0.4.0" + assert metadata.version("agentrust-trace") == "0.11.0" + assert metadata.version("agentrust-trace-tests") == "0.6.2" + dist = metadata.distribution("evidencebound-core") + assert not any(str(path).endswith("direct_url.json") for path in (dist.files or ())) + + +def test_positive_current_state_emits_trace() -> None: + cp = checkpoint() + current = {"e1": snapshot()} + _, signed, verifier = signed_current_allow(cp, current) + record = emit(cp, signed, verifier, current) + assert record["runtime"]["platform"] == "software-only" + assert record["policy"]["enforcement_mode"] == "declared" + assert record["appraisal"]["status"] == "none" + assert record["origin"]["kind"] == "third-party-control-plane" + assert record["origin"]["producer"] == "evidencebound-core/0.4.0" + assert record["references"][0]["rel"] == "evidencebound-verification" + agentrust_trace.validate_json(record) + + +def test_changed_evidence_rejected() -> None: + cp = checkpoint() + positive = {"e1": snapshot()} + _, signed, verifier = signed_current_allow(cp, positive) + changed = {"e1": replace(snapshot(), payload={"approved": True, "amount": 101})} + with pytest.raises(AdapterError): + emit(cp, signed, verifier, changed) + + +def test_stale_evidence_rejected() -> None: + cp = checkpoint() + positive = {"e1": snapshot()} + _, signed, verifier = signed_current_allow(cp, positive) + stale = {"e1": replace(snapshot(), valid_until="2026-10-04T00:00:00Z")} + with pytest.raises(AdapterError): + emit(cp, signed, verifier, stale) + + +def test_missing_evidence_rejected() -> None: + cp = checkpoint() + positive = {"e1": snapshot()} + _, signed, verifier = signed_current_allow(cp, positive) + with pytest.raises(AdapterError): + emit(cp, signed, verifier, {}) + + +def test_refuted_evidence_rejected() -> None: + cp = checkpoint() + positive = {"e1": snapshot()} + _, signed, verifier = signed_current_allow(cp, positive) + refuted = {"e1": replace(snapshot(), explicitly_refuted=True)} + with pytest.raises(AdapterError): + emit(cp, signed, verifier, refuted) + + +def test_policy_drift_rejected() -> None: + cp = checkpoint() + current = {"e1": snapshot()} + _, signed, verifier = signed_current_allow(cp, current) + wrong_policy = PolicyBinding(POLICY.policy_id, "2") + with pytest.raises(AdapterError): + emit(cp, signed, verifier, current, policy=wrong_policy) + + +def test_tampered_signed_receipt_rejected() -> None: + cp = checkpoint() + current = {"e1": snapshot()} + result, signed, verifier = signed_current_allow(cp, current) + tampered = replace(signed, signature="A" * len(signed.signature)) + checked = verify_signed_receipt( + tampered, + cp, + verifier, + verification_result=result, + ) + assert checked.status is SignedReceiptStatus.INVALID_SIGNATURE + with pytest.raises(AdapterError): + emit(cp, tampered, verifier, current) + + +def test_unknown_key_rejected() -> None: + cp = checkpoint() + current = {"e1": snapshot()} + result, signed, _ = signed_current_allow(cp, current) + unknown = Ed25519Verifier({}) + checked = verify_signed_receipt( + signed, + cp, + unknown, + verification_result=result, + ) + assert checked.status is SignedReceiptStatus.UNKNOWN_KEY + with pytest.raises(AdapterError): + emit(cp, signed, unknown, current) + + +def test_revoked_key_rejected() -> None: + cp = checkpoint() + current = {"e1": snapshot()} + result, signed, verifier = signed_current_allow( + cp, + current, + key_state=KeyState.REVOKED, + key_id="eb-revoked", + ) + checked = verify_signed_receipt( + signed, + cp, + verifier, + verification_result=result, + ) + assert checked.status is SignedReceiptStatus.REVOKED_KEY + with pytest.raises(AdapterError): + emit(cp, signed, verifier, current) + + +def test_retired_key_semantics_match_evidencebound_0_4_0() -> None: + cp = checkpoint() + current = {"e1": snapshot()} + result, signed, verifier = signed_current_allow( + cp, + current, + key_state=KeyState.RETIRED, + key_id="eb-retired", + ) + checked = verify_signed_receipt( + signed, + cp, + verifier, + verification_result=result, + ) + assert checked.status is SignedReceiptStatus.VERIFIED_RETIRED_KEY + with pytest.raises(AdapterError, match="VERIFIED_RETIRED_KEY"): + emit(cp, signed, verifier, current) + + +def test_verification_time_and_timezone_boundary() -> None: + boundary_evidence = snapshot(valid_until="2026-10-05T08:00:00Z") + cp = checkpoint(boundary_evidence) + current = {"e1": boundary_evidence} + _, signed, verifier = signed_current_allow(cp, current, now=NOW) + + equivalent_offset = datetime( + 2026, + 10, + 5, + 11, + 0, + tzinfo=timezone(timedelta(hours=3)), + ) + emit( + cp, + signed, + verifier, + current, + verification_time=equivalent_offset, + ) + + with pytest.raises(AdapterError): + emit( + cp, + signed, + verifier, + current, + verification_time=NOW + timedelta(seconds=1), + ) + + with pytest.raises(AdapterError, match="timezone-aware"): + emit( + cp, + signed, + verifier, + current, + verification_time=datetime(2026, 10, 5, 8, 0), + ) + + +def test_exact_result_receipt_binding() -> None: + cp = checkpoint() + current = {"e1": snapshot()} + _, signed, verifier = signed_current_allow(cp, current) + + changed = {"e1": replace(snapshot(), payload={"approved": True, "amount": 101})} + changed_result = verify_checkpoint( + cp, + current_evidence=changed, + expected_policy=POLICY, + prior_receipt=signed.receipt, + now=NOW, + ) + checked = verify_signed_receipt( + signed, + cp, + verifier, + verification_result=changed_result, + ) + assert checked.status is SignedReceiptStatus.INVALID_BINDING + with pytest.raises(AdapterError): + emit(cp, signed, verifier, changed) + + +def test_trace_reference_binds_exact_signed_receipt() -> None: + cp = checkpoint() + current = {"e1": snapshot()} + _, signed, verifier = signed_current_allow(cp, current) + record = emit(cp, signed, verifier, current) + assert record["references"][0]["digest"] == signed_receipt_reference_digest(signed) + + +def test_trace_record_signature_verifies_with_explicit_signing_key() -> None: + cp = checkpoint() + current = {"e1": snapshot()} + _, signed, verifier = signed_current_allow(cp, current) + trace_key = generate_key() + record = emit(cp, signed, verifier, current, trace_key=trace_key) + checked = agentrust_trace.verify_record( + record, + public_key_or_jwk=trace_key.public_key(), + now=ISSUED_AT, + ) + assert checked.profile == "tag:agentrust-io.com,2026:trace-v0.2" + assert checked.trusted_key_thumbprint diff --git a/marketplace/catalog.json b/marketplace/catalog.json index 3d5af1e..3c9ca24 100644 --- a/marketplace/catalog.json +++ b/marketplace/catalog.json @@ -1,6 +1,6 @@ { "catalog_version": 1, - "count": 44, + "count": 45, "integrations": [ { "name": "Claude Code", @@ -501,6 +501,29 @@ ".epi" ] }, + { + "name": "EvidenceBound Core", + "package_name": "EvidenceBound Core", + "vendor": "EvidenceBound", + "description": "Re-verifies explicit current EvidenceBound state and signed-receipt binding, then emits a software-only TRACE record only for current ALLOW.", + "path": "integrations/evidencebound-core", + "url": "https://github.com/agentrust-io/integrations/tree/main/integrations/evidencebound-core", + "homepage": "https://evidencebound.org", + "repository": "https://github.com/evidencebound/evidencebound-core", + "tier": "community", + "stack": [ + "TRACE" + ], + "category": "Evidence & receipts", + "mark": "EB", + "featured": null, + "keywords": [ + "evidence", + "provenance", + "authority", + "receipts" + ] + }, { "name": "Frequency Agent Execution Assurance", "package_name": "Frequency Agent Execution Assurance", From 93476617683199268340e9215cc3ea48ec7c40e0 Mon Sep 17 00:00:00 2001 From: Ruslan Vrublevskyi Date: Mon, 5 Oct 2026 21:32:26 +0300 Subject: [PATCH 2/2] chore: regenerate integration index --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ab33fd3..b8a3471 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ TRACE only works as a standard if it is genuinely neutral. Integrations are list | [DecisionAssure](integrations/decisionassure/) | DecisionAssure (a1k7) | trace | community | | [Docker Sandbox Kit](integrations/docker-sandbox-kit/) | agentrust-io | trace | community | | [EPI Recorder](integrations/epilabs-epi-recorder/) | EPI Labs | trace, wcm | verified | +| [EvidenceBound Core](integrations/evidencebound-core/) | EvidenceBound | trace | community | | [Google ADK](integrations/google-adk/) | agentrust-io | trace | verified | | [LangChain](integrations/langchain/) | agentrust-io | trace | verified | | [SOVP](integrations/litzki-systems-sovp/) | Litzki Systems | trace | verified |