From f5f184b1578f7f356383f62283532b59121a86ee Mon Sep 17 00:00:00 2001 From: "Altru.dev" Date: Sun, 6 Sep 2026 08:38:14 -0700 Subject: [PATCH 1/2] fix(provenance): refuse conflicting tool schema aliases Closes #297. Signed-off-by: Altru.dev --- src/agentrust_trace/provenance.py | 8 ++++ tests/test_provenance_dual_input_schema.py | 56 ++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 tests/test_provenance_dual_input_schema.py diff --git a/src/agentrust_trace/provenance.py b/src/agentrust_trace/provenance.py index a548deb..3625d34 100644 --- a/src/agentrust_trace/provenance.py +++ b/src/agentrust_trace/provenance.py @@ -109,6 +109,14 @@ def tool_catalog_hash(tools: list[dict[str, Any]]) -> str: for index, t in enumerate(tools): if not isinstance(t, dict): raise ProvenanceError(f"tools[{index}] must be an object, got {type(t).__name__}") + if ( + "input_schema" in t + and "inputSchema" in t + and t["input_schema"] != t["inputSchema"] + ): + raise ProvenanceError( + f"tools[{index}] carries conflicting input_schema and inputSchema values" + ) normalized = sorted( ( { diff --git a/tests/test_provenance_dual_input_schema.py b/tests/test_provenance_dual_input_schema.py new file mode 100644 index 0000000..3edb8f6 --- /dev/null +++ b/tests/test_provenance_dual_input_schema.py @@ -0,0 +1,56 @@ +"""Regression coverage for conflicting MCP tool schema aliases (#297).""" + +from __future__ import annotations + +import pytest + +from agentrust_trace.provenance import ProvenanceError, check_tool_catalog, tool_catalog_hash + + +def _schema(*, additional_properties: bool) -> dict[str, object]: + return { + "type": "object", + "properties": {"path": {"type": "string"}}, + "additionalProperties": additional_properties, + } + + +def _tool(schema: dict[str, object]) -> dict[str, object]: + return { + "name": "read_file", + "description": "Read a file from the sandboxed workspace", + "input_schema": schema, + } + + +def test_equal_schema_aliases_keep_the_existing_wire_compatibility() -> None: + schema = _schema(additional_properties=False) + snake = _tool(schema) + both = {**snake, "inputSchema": schema.copy()} + + assert tool_catalog_hash([both]) == tool_catalog_hash([snake]) + + +def test_conflicting_schema_aliases_are_refused_before_hashing() -> None: + live = _tool(_schema(additional_properties=False)) + live["inputSchema"] = _schema(additional_properties=True) + + with pytest.raises(ProvenanceError, match="conflicting input_schema and inputSchema"): + tool_catalog_hash([live]) + + +def test_check_tool_catalog_cannot_accept_a_decoy_snake_case_schema() -> None: + signed_shape = _tool(_schema(additional_properties=False)) + record = { + "tool_catalog": { + "hash": tool_catalog_hash([signed_shape]), + "tool_count": 1, + } + } + live = { + **signed_shape, + "inputSchema": _schema(additional_properties=True), + } + + with pytest.raises(ProvenanceError, match="conflicting input_schema and inputSchema"): + check_tool_catalog(record, [live]) From 427952e2dee3646b44debb4dbaaef97bbea097ca Mon Sep 17 00:00:00 2001 From: Imran Siddique Date: Sun, 6 Sep 2026 09:58:46 -0700 Subject: [PATCH 2/2] fix: compare dual tool schema aliases by canonical bytes Signed-off-by: Imran Siddique --- src/agentrust_trace/provenance.py | 2 +- tests/test_alias_canonical_comparison.py | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 tests/test_alias_canonical_comparison.py diff --git a/src/agentrust_trace/provenance.py b/src/agentrust_trace/provenance.py index 3625d34..cff6f2a 100644 --- a/src/agentrust_trace/provenance.py +++ b/src/agentrust_trace/provenance.py @@ -112,7 +112,7 @@ def tool_catalog_hash(tools: list[dict[str, Any]]) -> str: if ( "input_schema" in t and "inputSchema" in t - and t["input_schema"] != t["inputSchema"] + and anchor_bytes(t["input_schema"]) != anchor_bytes(t["inputSchema"]) ): raise ProvenanceError( f"tools[{index}] carries conflicting input_schema and inputSchema values" diff --git a/tests/test_alias_canonical_comparison.py b/tests/test_alias_canonical_comparison.py new file mode 100644 index 0000000..3c0d6e0 --- /dev/null +++ b/tests/test_alias_canonical_comparison.py @@ -0,0 +1,22 @@ +"""Alias equality follows the bytes hashed, including JSON type distinctions.""" +import pytest + +from agentrust_trace.provenance import ProvenanceError, check_tool_catalog, tool_catalog_hash + + +@pytest.mark.parametrize("left,right", [ + ({"const": True}, {"const": 1}), + ({"const": False}, {"const": 0}), + ({"properties": {"x": {"enum": [1]}}}, {"properties": {"x": {"enum": [True]}}}), +]) +def test_python_equal_but_distinct_json_aliases_are_refused(left, right): + signed = {"name": "read", "description": "Read", "input_schema": left} + record = {"tool_catalog": {"hash": tool_catalog_hash([signed]), "tool_count": 1}} + with pytest.raises(ProvenanceError, match="conflicting input_schema and inputSchema"): + check_tool_catalog(record, [{**signed, "inputSchema": right}]) + + +def test_alias_object_key_order_does_not_change_the_hash(): + tool = {"name": "read", "input_schema": {"type": "object", "additionalProperties": False}} + dual = {**tool, "inputSchema": {"additionalProperties": False, "type": "object"}} + assert tool_catalog_hash([dual]) == tool_catalog_hash([tool])