From 78f33e65e939a9ede7462bd56cfd8505b2dff410 Mon Sep 17 00:00:00 2001 From: BIN Zhang Date: Thu, 23 Jul 2026 00:07:24 +0800 Subject: [PATCH 1/4] feat: add external evaluator handoff example Signed-off-by: BIN Zhang --- examples/external-evaluator-handoff/README.md | 99 ++++++++++ .../external_evaluator_handoff.py | 170 ++++++++++++++++++ .../requirements.txt | 2 + .../test_external_evaluator_handoff.py | 119 ++++++++++++ 4 files changed, 390 insertions(+) create mode 100644 examples/external-evaluator-handoff/README.md create mode 100644 examples/external-evaluator-handoff/external_evaluator_handoff.py create mode 100644 examples/external-evaluator-handoff/requirements.txt create mode 100644 examples/external-evaluator-handoff/test_external_evaluator_handoff.py diff --git a/examples/external-evaluator-handoff/README.md b/examples/external-evaluator-handoff/README.md new file mode 100644 index 000000000..1aef9a724 --- /dev/null +++ b/examples/external-evaluator-handoff/README.md @@ -0,0 +1,99 @@ +# External Evaluator Handoff + +> Status: experimental, community-driven example + +This example converts one or more AGT Decision BOMs into a deterministic, +strict-JSON request for a downstream evaluator. It demonstrates an +interface-first boundary: AGT remains the runtime governance and observation +source, while an external system may perform post-execution fitness, +adaptation, stability, or other longitudinal evaluation. + +The example is deliberately offline. It does not call an external service, +change an AGT policy decision, authorize an action, mutate an audit record, or +turn an evaluation result into a governance decision. + +## Why this boundary exists + +Runtime governance and post-execution evaluation answer different questions: + +- AGT answers whether an action was allowed, which policy applied, and what + governance signals were observed. +- A downstream evaluator may compare multiple observed decisions over time and + produce a reviewable assessment. +- That assessment is input to a separate review process. It is not permission + and does not override AGT. + +The handoff builds on AGT's existing +[`DecisionBOM`](../../agent-governance-python/agent-mesh/src/agentmesh/governance/decision_bom.py) +instead of introducing a second audit or policy model. + +## Prerequisites + +- Python 3.11+ +- No API keys or network service required at runtime + +```bash +python -m venv .venv +source .venv/bin/activate +pip install -r examples/external-evaluator-handoff/requirements.txt +``` + +## Run + +From the repository root: + +```bash +python examples/external-evaluator-handoff/external_evaluator_handoff.py +``` + +The script prints one synthetic request. Its shape contains: + +- a content-derived `request_id`; +- source Decision BOM observations; +- only explicitly allowlisted extra fields; +- fixed read-only and zero-authority declarations. + +Example boundary: + +```json +{ + "authority_boundary": { + "evaluation_result_is_governance_decision": false, + "execution_authorized": false, + "policy_decision_overridden": false, + "read_only": true, + "source_records_mutated": false + } +} +``` + +## Test + +```bash +PYTHONPATH=agent-governance-python/agent-mesh/src \ + pytest -q examples/external-evaluator-handoff/test_external_evaluator_handoff.py +``` + +The tests cover deterministic output, exact field allowlisting, source +immutability, timezone rejection, empty-input rejection, strict JSON values, +and the permanent authority boundary. + +## Data and security notes + +- Decision BOM fields can contain policy, context, or trace data. The exporter + therefore includes no optional fields unless their exact names are + allowlisted by the caller. +- The sample uses synthetic identifiers and values. Review tenant, privacy, + retention, and cross-border requirements before exporting real records. +- `source_completeness` describes Decision BOM reconstruction coverage. It does + not prove that an event was correct, authorized, or complete in the real + world. +- A content hash identifies the request bytes; it is not a signature, + attestation, or proof of truth. + +## Prior art and interoperability intent + +The interface boundary was informed by SAEE's Evolution Intelligence Layer: +`https://github.com/joy7758/SAEE`. No SAEE source code, engine implementation, +or runtime dependency is included. The request is framework-neutral so other +external evaluators can consume the same observation boundary. diff --git a/examples/external-evaluator-handoff/external_evaluator_handoff.py b/examples/external-evaluator-handoff/external_evaluator_handoff.py new file mode 100644 index 000000000..23a90d300 --- /dev/null +++ b/examples/external-evaluator-handoff/external_evaluator_handoff.py @@ -0,0 +1,170 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +"""Export AGT Decision BOMs to a read-only external-evaluation request. + +This example deliberately stops at the interoperability boundary. It does not +call an evaluator, mutate AGT records, or turn an evaluation result into a +governance decision. +""" + +from __future__ import annotations + +import hashlib +import json +from collections.abc import Collection, Sequence +from datetime import datetime, timezone +from typing import Any + +from agentmesh.governance.decision_bom import BOMField, BOMFieldCategory, DecisionBOM + + +SCHEMA_VERSION = "0.1" + + +def _utc_timestamp(value: datetime) -> str: + """Return an RFC 3339 UTC timestamp, rejecting timezone-free values.""" + if value.tzinfo is None or value.utcoffset() is None: + raise ValueError("timestamps must include a timezone") + return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") + + +def _export_field(field: BOMField) -> dict[str, Any]: + """Export one explicitly allowlisted BOM field with fail-closed JSON checks.""" + try: + json.dumps(field.value, allow_nan=False) + except (TypeError, ValueError) as exc: + raise ValueError( + f"field {field.name!r} is not strict-JSON serializable" + ) from exc + + return { + "name": field.name, + "category": field.category.value, + "value": field.value, + "source": field.source, + "confidence": field.confidence, + "inferred": field.inferred, + } + + +def build_external_evaluation_request( + decisions: Sequence[DecisionBOM], + *, + generated_at: datetime, + allowed_field_names: Collection[str] = (), +) -> dict[str, Any]: + """Build a deterministic, offline request for a downstream evaluator. + + Args: + decisions: Reconstructed AGT decisions to expose as observations. + generated_at: Time at which this handoff request was created. Callers + must pass a timezone-aware value so replays are unambiguous. + allowed_field_names: Exact Decision BOM field names permitted to cross + the boundary. The default is empty to avoid exporting arbitrary + policy, context, or trace data by accident. + + Returns: + A strict-JSON-compatible dictionary. Its authority boundary is + intentionally fixed: the downstream evaluator receives observations + but cannot authorize actions, override policy, or mutate source records. + + Raises: + ValueError: If no decisions are provided, timestamps are timezone-free, + or an allowlisted value is not strict-JSON serializable. + """ + if not decisions: + raise ValueError("at least one Decision BOM is required") + + generated_at_utc = _utc_timestamp(generated_at) + allowlist = frozenset(allowed_field_names) + observations: list[dict[str, Any]] = [] + + for decision in decisions: + fields = [ + _export_field(field) for field in decision.fields if field.name in allowlist + ] + observations.append( + { + "decision_id": decision.decision_id, + "observed_at": _utc_timestamp(decision.timestamp), + "agent_id": decision.agent_id, + "action_requested": decision.action_requested, + "governance_outcome": decision.outcome, + "source_completeness": decision.completeness_score, + "sources_queried": list(decision.sources_queried), + "fields": fields, + } + ) + + request: dict[str, Any] = { + "schema_version": SCHEMA_VERSION, + "purpose": "post_execution_external_evaluation", + "generated_at": generated_at_utc, + "source": { + "system": "agent-governance-toolkit", + "representation": "decision_bom", + "decision_count": len(observations), + }, + "observations": observations, + "authority_boundary": { + "read_only": True, + "source_records_mutated": False, + "execution_authorized": False, + "policy_decision_overridden": False, + "evaluation_result_is_governance_decision": False, + }, + } + + canonical = json.dumps( + request, + allow_nan=False, + ensure_ascii=False, + separators=(",", ":"), + sort_keys=True, + ).encode("utf-8") + request["request_id"] = f"eval_{hashlib.sha256(canonical).hexdigest()}" + return request + + +def _sample_decisions(now: datetime) -> list[DecisionBOM]: + """Create synthetic Decision BOMs for the runnable example.""" + return [ + DecisionBOM( + decision_id="decision-001", + timestamp=now, + agent_id="did:mesh:synthetic-agent", + action_requested="read_inventory", + outcome="allow", + fields=[ + BOMField( + name="latency_ms", + category=BOMFieldCategory.OUTCOME, + value=42, + source="synthetic_trace", + ), + BOMField( + name="internal_policy_context", + category=BOMFieldCategory.POLICY, + value={"rule": "allow-read"}, + source="synthetic_policy", + ), + ], + sources_queried=["audit", "policy", "trace"], + completeness_score=0.8, + ) + ] + + +def main() -> None: + """Print one synthetic, offline evaluation handoff request.""" + now = datetime.now(timezone.utc) + request = build_external_evaluation_request( + _sample_decisions(now), + generated_at=now, + allowed_field_names={"latency_ms"}, + ) + print(json.dumps(request, indent=2, sort_keys=True)) + + +if __name__ == "__main__": + main() diff --git a/examples/external-evaluator-handoff/requirements.txt b/examples/external-evaluator-handoff/requirements.txt new file mode 100644 index 000000000..794ed3ae4 --- /dev/null +++ b/examples/external-evaluator-handoff/requirements.txt @@ -0,0 +1,2 @@ +agent-governance-toolkit-core>=4.1.0,<6.0 +pytest>=9.1.1,<10.0 diff --git a/examples/external-evaluator-handoff/test_external_evaluator_handoff.py b/examples/external-evaluator-handoff/test_external_evaluator_handoff.py new file mode 100644 index 000000000..a7068f2fd --- /dev/null +++ b/examples/external-evaluator-handoff/test_external_evaluator_handoff.py @@ -0,0 +1,119 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +"""Tests for the external evaluator handoff example.""" + +from __future__ import annotations + +import copy +import importlib.util +from datetime import datetime, timezone +from pathlib import Path + +import pytest +from agentmesh.governance.decision_bom import BOMField, BOMFieldCategory, DecisionBOM + + +MODULE_PATH = Path(__file__).with_name("external_evaluator_handoff.py") +SPEC = importlib.util.spec_from_file_location("external_evaluator_handoff", MODULE_PATH) +assert SPEC and SPEC.loader +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) + + +def _decision(*, value: object = 42) -> DecisionBOM: + observed_at = datetime(2026, 7, 22, 12, 0, tzinfo=timezone.utc) + return DecisionBOM( + decision_id="decision-001", + timestamp=observed_at, + agent_id="did:mesh:synthetic-agent", + action_requested="read_inventory", + outcome="allow", + fields=[ + BOMField( + name="latency_ms", + category=BOMFieldCategory.OUTCOME, + value=value, + source="synthetic_trace", + ), + BOMField( + name="private_context", + category=BOMFieldCategory.CONTEXT, + value="must-not-cross-by-default", + source="synthetic_context", + ), + ], + sources_queried=["audit", "trace"], + completeness_score=0.8, + ) + + +def test_handoff_is_deterministic_allowlisted_and_read_only() -> None: + decision = _decision() + original = copy.deepcopy(decision.to_dict()) + generated_at = datetime(2026, 7, 22, 12, 1, tzinfo=timezone.utc) + + first = MODULE.build_external_evaluation_request( + [decision], + generated_at=generated_at, + allowed_field_names={"latency_ms"}, + ) + second = MODULE.build_external_evaluation_request( + [decision], + generated_at=generated_at, + allowed_field_names={"latency_ms"}, + ) + + assert first == second + assert decision.to_dict() == original + assert first["request_id"].startswith("eval_") + assert first["observations"][0]["fields"] == [ + { + "name": "latency_ms", + "category": "outcome", + "value": 42, + "source": "synthetic_trace", + "confidence": 1.0, + "inferred": False, + } + ] + assert first["authority_boundary"] == { + "read_only": True, + "source_records_mutated": False, + "execution_authorized": False, + "policy_decision_overridden": False, + "evaluation_result_is_governance_decision": False, + } + + +def test_handoff_exports_no_optional_fields_by_default() -> None: + request = MODULE.build_external_evaluation_request( + [_decision()], + generated_at=datetime(2026, 7, 22, 12, 1, tzinfo=timezone.utc), + ) + + assert request["observations"][0]["fields"] == [] + + +def test_handoff_rejects_empty_decision_set() -> None: + with pytest.raises(ValueError, match="at least one Decision BOM"): + MODULE.build_external_evaluation_request( + [], + generated_at=datetime(2026, 7, 22, 12, 1, tzinfo=timezone.utc), + ) + + +def test_handoff_rejects_timezone_free_timestamps() -> None: + with pytest.raises(ValueError, match="timestamps must include a timezone"): + MODULE.build_external_evaluation_request( + [_decision()], + generated_at=datetime(2026, 7, 22, 12, 1), + ) + + +def test_handoff_rejects_non_json_allowlisted_values() -> None: + with pytest.raises(ValueError, match="not strict-JSON serializable"): + MODULE.build_external_evaluation_request( + [_decision(value=object())], + generated_at=datetime(2026, 7, 22, 12, 1, tzinfo=timezone.utc), + allowed_field_names={"latency_ms"}, + ) From 51b5f93bdc1f91f626868447f6046d212dd4991d Mon Sep 17 00:00:00 2001 From: BIN Zhang Date: Thu, 23 Jul 2026 00:57:06 +0800 Subject: [PATCH 2/4] fix: harden external evaluator handoff Signed-off-by: BIN Zhang --- examples/external-evaluator-handoff/README.md | 7 +++++-- .../external_evaluator_handoff.py | 11 +++++++++-- .../test_external_evaluator_handoff.py | 19 ++++++++++++++++++- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/examples/external-evaluator-handoff/README.md b/examples/external-evaluator-handoff/README.md index 1aef9a724..cbdff462a 100644 --- a/examples/external-evaluator-handoff/README.md +++ b/examples/external-evaluator-handoff/README.md @@ -83,13 +83,16 @@ and the permanent authority boundary. - Decision BOM fields can contain policy, context, or trace data. The exporter therefore includes no optional fields unless their exact names are allowlisted by the caller. +- Allowlisted values are normalized into detached strict-JSON copies. Mutating + a constructed request therefore does not mutate the source Decision BOM. - The sample uses synthetic identifiers and values. Review tenant, privacy, retention, and cross-border requirements before exporting real records. - `source_completeness` describes Decision BOM reconstruction coverage. It does not prove that an event was correct, authorized, or complete in the real world. -- A content hash identifies the request bytes; it is not a signature, - attestation, or proof of truth. +- A content hash identifies the canonical payload bytes before `request_id` is + added, so the identifier itself is excluded from its hash input. The hash is + not a signature, attestation, or proof of truth. ## Prior art and interoperability intent diff --git a/examples/external-evaluator-handoff/external_evaluator_handoff.py b/examples/external-evaluator-handoff/external_evaluator_handoff.py index 23a90d300..50dade4db 100644 --- a/examples/external-evaluator-handoff/external_evaluator_handoff.py +++ b/examples/external-evaluator-handoff/external_evaluator_handoff.py @@ -31,7 +31,14 @@ def _utc_timestamp(value: datetime) -> str: def _export_field(field: BOMField) -> dict[str, Any]: """Export one explicitly allowlisted BOM field with fail-closed JSON checks.""" try: - json.dumps(field.value, allow_nan=False) + serialized_value = json.dumps( + field.value, + allow_nan=False, + ensure_ascii=False, + separators=(",", ":"), + sort_keys=True, + ) + exported_value = json.loads(serialized_value) except (TypeError, ValueError) as exc: raise ValueError( f"field {field.name!r} is not strict-JSON serializable" @@ -40,7 +47,7 @@ def _export_field(field: BOMField) -> dict[str, Any]: return { "name": field.name, "category": field.category.value, - "value": field.value, + "value": exported_value, "source": field.source, "confidence": field.confidence, "inferred": field.inferred, diff --git a/examples/external-evaluator-handoff/test_external_evaluator_handoff.py b/examples/external-evaluator-handoff/test_external_evaluator_handoff.py index a7068f2fd..353261c9a 100644 --- a/examples/external-evaluator-handoff/test_external_evaluator_handoff.py +++ b/examples/external-evaluator-handoff/test_external_evaluator_handoff.py @@ -15,7 +15,8 @@ MODULE_PATH = Path(__file__).with_name("external_evaluator_handoff.py") SPEC = importlib.util.spec_from_file_location("external_evaluator_handoff", MODULE_PATH) -assert SPEC and SPEC.loader +if SPEC is None or SPEC.loader is None: + raise RuntimeError(f"could not load external evaluator handoff from {MODULE_PATH}") MODULE = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(MODULE) @@ -94,6 +95,22 @@ def test_handoff_exports_no_optional_fields_by_default() -> None: assert request["observations"][0]["fields"] == [] +def test_handoff_detaches_mutable_allowlisted_values_from_source() -> None: + decision = _decision(value={"nested": ["original"]}) + request = MODULE.build_external_evaluation_request( + [decision], + generated_at=datetime(2026, 7, 22, 12, 1, tzinfo=timezone.utc), + allowed_field_names={"latency_ms"}, + ) + + exported_value = request["observations"][0]["fields"][0]["value"] + exported_value["nested"].append("request-only") + assert decision.fields[0].value == {"nested": ["original"]} + + decision.fields[0].value["nested"].append("source-only") + assert exported_value == {"nested": ["original", "request-only"]} + + def test_handoff_rejects_empty_decision_set() -> None: with pytest.raises(ValueError, match="at least one Decision BOM"): MODULE.build_external_evaluation_request( From 29b954b4cc43dc62ad4ecb53dece4b092a39eb0f Mon Sep 17 00:00:00 2001 From: BIN Zhang Date: Thu, 23 Jul 2026 01:03:06 +0800 Subject: [PATCH 3/4] fix: broaden example test compatibility Signed-off-by: BIN Zhang --- examples/external-evaluator-handoff/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/external-evaluator-handoff/requirements.txt b/examples/external-evaluator-handoff/requirements.txt index 794ed3ae4..1ef56e7d7 100644 --- a/examples/external-evaluator-handoff/requirements.txt +++ b/examples/external-evaluator-handoff/requirements.txt @@ -1,2 +1,2 @@ agent-governance-toolkit-core>=4.1.0,<6.0 -pytest>=9.1.1,<10.0 +pytest>=8.0.0,<10.0 From 42f6c394b095ecfa8fbb576d1b853de6c7081330 Mon Sep 17 00:00:00 2001 From: BIN Zhang Date: Thu, 3 Sep 2026 18:32:07 +0800 Subject: [PATCH 4/4] fix: address external evaluator handoff review Signed-off-by: BIN Zhang --- .../2026-09-03-external-evaluator-handoff.md | 44 +++++++++++++++++++ examples/external-evaluator-handoff/README.md | 9 ++-- .../external_evaluator_handoff.py | 21 +++------ .../requirements.txt | 2 +- .../test_external_evaluator_handoff.py | 15 +++++++ 5 files changed, 72 insertions(+), 19 deletions(-) create mode 100644 docs/dependency-audits/2026-09-03-external-evaluator-handoff.md diff --git a/docs/dependency-audits/2026-09-03-external-evaluator-handoff.md b/docs/dependency-audits/2026-09-03-external-evaluator-handoff.md new file mode 100644 index 000000000..613886a49 --- /dev/null +++ b/docs/dependency-audits/2026-09-03-external-evaluator-handoff.md @@ -0,0 +1,44 @@ +--- +title: External Evaluator Handoff Example Dependencies +last_reviewed: 2026-09-03 +owner: agt-maintainers +--- + +# External Evaluator Handoff Example Dependencies + +## Which Dependencies Changed And Why + +The new `examples/external-evaluator-handoff/requirements.txt` declares two +bounded dependencies for the standalone example: + +- `agent-governance-toolkit-core>=5.0.0,<6.0` supplies the existing + `DecisionBOM` model and the public `sha256_jcs` digest helper. The v5 floor is + required so the example does not teach direct use of a raw cryptographic + primitive outside the SDK boundary. +- `pytest>=8.0.0,<10.0` is used only to run the example's local regression + tests. It is not imported by the runnable example. + +These dependencies are isolated to the example and do not change any AGT +package or repository-wide runtime dependency. + +## Security Advisory Relevance + +This change is not a security-advisory remediation and does not add a new +cryptographic implementation. Content digests are delegated to AGT's existing +public SDK helper. `pytest` is test-only, and the repository's dependency +review and vulnerability checks remain authoritative for the resolved graph. + +## Breaking Change Risk Assessment + +**Risk: low and example-local.** The example requires AGT core v5 because the +public digest helper is part of that supported surface. Users pinned to AGT +core v4 cannot run this example without upgrading, but no existing package, +API, policy, or runtime behavior is changed. The upper bounds keep resolution +within the currently supported major versions. + +## Validation And Rollback + +Validation covers the example test suite, formatting and lint checks, strict +JSON output, documentation links, and the repository dependency and +unauthorized-crypto gates. Rollback consists of removing the standalone +example and this audit record; no production data or migration is involved. diff --git a/examples/external-evaluator-handoff/README.md b/examples/external-evaluator-handoff/README.md index cbdff462a..aff6e5acb 100644 --- a/examples/external-evaluator-handoff/README.md +++ b/examples/external-evaluator-handoff/README.md @@ -1,5 +1,7 @@ # External Evaluator Handoff + + > Status: experimental, community-driven example This example converts one or more AGT Decision BOMs into a deterministic, @@ -90,9 +92,10 @@ and the permanent authority boundary. - `source_completeness` describes Decision BOM reconstruction coverage. It does not prove that an event was correct, authorized, or complete in the real world. -- A content hash identifies the canonical payload bytes before `request_id` is - added, so the identifier itself is excluded from its hash input. The hash is - not a signature, attestation, or proof of truth. +- A content hash generated through AGT's public `sha256_jcs` SDK helper + identifies the canonical payload bytes before `request_id` is added, so the + identifier itself is excluded from its hash input. The hash is not a + signature, attestation, or proof of truth. ## Prior art and interoperability intent diff --git a/examples/external-evaluator-handoff/external_evaluator_handoff.py b/examples/external-evaluator-handoff/external_evaluator_handoff.py index 50dade4db..0f2cd71ef 100644 --- a/examples/external-evaluator-handoff/external_evaluator_handoff.py +++ b/examples/external-evaluator-handoff/external_evaluator_handoff.py @@ -9,15 +9,16 @@ from __future__ import annotations -import hashlib import json from collections.abc import Collection, Sequence from datetime import datetime, timezone from typing import Any +from agentmesh.governance.approval_protocol import sha256_jcs from agentmesh.governance.decision_bom import BOMField, BOMFieldCategory, DecisionBOM +# cspell:ignore utcoffset SCHEMA_VERSION = "0.1" @@ -40,9 +41,7 @@ def _export_field(field: BOMField) -> dict[str, Any]: ) exported_value = json.loads(serialized_value) except (TypeError, ValueError) as exc: - raise ValueError( - f"field {field.name!r} is not strict-JSON serializable" - ) from exc + raise ValueError(f"field {field.name!r} is not strict-JSON serializable") from exc return { "name": field.name, @@ -87,9 +86,7 @@ def build_external_evaluation_request( observations: list[dict[str, Any]] = [] for decision in decisions: - fields = [ - _export_field(field) for field in decision.fields if field.name in allowlist - ] + fields = [_export_field(field) for field in decision.fields if field.name in allowlist] observations.append( { "decision_id": decision.decision_id, @@ -122,14 +119,8 @@ def build_external_evaluation_request( }, } - canonical = json.dumps( - request, - allow_nan=False, - ensure_ascii=False, - separators=(",", ":"), - sort_keys=True, - ).encode("utf-8") - request["request_id"] = f"eval_{hashlib.sha256(canonical).hexdigest()}" + digest = sha256_jcs(request) + request["request_id"] = f"eval_{digest.removeprefix('sha256:')}" return request diff --git a/examples/external-evaluator-handoff/requirements.txt b/examples/external-evaluator-handoff/requirements.txt index 1ef56e7d7..f514b4405 100644 --- a/examples/external-evaluator-handoff/requirements.txt +++ b/examples/external-evaluator-handoff/requirements.txt @@ -1,2 +1,2 @@ -agent-governance-toolkit-core>=4.1.0,<6.0 +agent-governance-toolkit-core>=5.0.0,<6.0 pytest>=8.0.0,<10.0 diff --git a/examples/external-evaluator-handoff/test_external_evaluator_handoff.py b/examples/external-evaluator-handoff/test_external_evaluator_handoff.py index 353261c9a..d0482da87 100644 --- a/examples/external-evaluator-handoff/test_external_evaluator_handoff.py +++ b/examples/external-evaluator-handoff/test_external_evaluator_handoff.py @@ -10,6 +10,7 @@ from pathlib import Path import pytest +from agentmesh.governance.approval_protocol import sha256_jcs from agentmesh.governance.decision_bom import BOMField, BOMFieldCategory, DecisionBOM @@ -86,6 +87,20 @@ def test_handoff_is_deterministic_allowlisted_and_read_only() -> None: } +def test_request_id_uses_the_public_sdk_digest() -> None: + request = MODULE.build_external_evaluation_request( + [_decision()], + generated_at=datetime(2026, 7, 22, 12, 1, tzinfo=timezone.utc), + allowed_field_names={"latency_ms"}, + ) + + payload = copy.deepcopy(request) + request_id = payload.pop("request_id") + expected_digest = sha256_jcs(payload).removeprefix("sha256:") + + assert request_id == f"eval_{expected_digest}" + + def test_handoff_exports_no_optional_fields_by_default() -> None: request = MODULE.build_external_evaluation_request( [_decision()],