From 0f8c3c931213a192ea298607df7036405f7c8a55 Mon Sep 17 00:00:00 2001 From: Abhinaysai Kamineni <66816045+askmy-stack@users.noreply.github.com> Date: Sat, 11 Jul 2026 14:29:48 -0400 Subject: [PATCH] Validate snapshot schema version on read. Reject unsupported tool_semantics_version values with an upgrade hint so future snapshot formats fail loudly instead of silently mis-parsing. Co-authored-by: Cursor --- src/tool_semantics/scanner.py | 24 ++++++++++++++++++++++-- tests/test_scanner.py | 24 +++++++++++++++++++++++- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/tool_semantics/scanner.py b/src/tool_semantics/scanner.py index 6b9d23a..19a61eb 100644 --- a/src/tool_semantics/scanner.py +++ b/src/tool_semantics/scanner.py @@ -11,6 +11,10 @@ class ManifestError(ValueError): """Raised when a manifest cannot be normalized.""" +SUPPORTED_SNAPSHOT_VERSIONS = frozenset({"0.1"}) +CURRENT_SNAPSHOT_VERSION = "0.1" + + def _normalize_parameters(input_schema: dict[str, Any]) -> list[ToolParameter]: properties = input_schema.get("properties", {}) required = set(input_schema.get("required", [])) @@ -51,12 +55,15 @@ def capture_manifest(path: Path) -> InterfaceSnapshot: input_schema = raw_tool.get("inputSchema", {"type": "object", "properties": {}}) if not isinstance(input_schema, dict): raise ManifestError(f"Tool '{raw_tool['name']}' inputSchema must be an object") + output_schema = raw_tool.get("outputSchema") + if output_schema is not None and not isinstance(output_schema, dict): + raise ManifestError(f"Tool '{raw_tool['name']}' outputSchema must be an object") tools.append( ToolContract( name=raw_tool["name"], description=str(raw_tool.get("description", "")), parameters=_normalize_parameters(input_schema), - output_schema=raw_tool.get("outputSchema"), + output_schema=output_schema, risk=raw_tool.get("risk", "unknown"), ) ) @@ -75,8 +82,21 @@ def write_snapshot(snapshot: InterfaceSnapshot, output: Path) -> None: output.write_text(snapshot.model_dump_json(indent=2, by_alias=True) + "\n", encoding="utf-8") +def _validate_snapshot_version(version: str) -> None: + if version in SUPPORTED_SNAPSHOT_VERSIONS: + return + supported = ", ".join(sorted(SUPPORTED_SNAPSHOT_VERSIONS)) + raise ManifestError( + f"Unsupported tool_semantics_version {version!r}. " + f"Supported versions: {supported}. " + f"Upgrade Tool-Semantics or re-capture snapshots with version {CURRENT_SNAPSHOT_VERSION}." + ) + + def read_snapshot(path: Path) -> InterfaceSnapshot: try: - return InterfaceSnapshot.model_validate_json(path.read_text(encoding="utf-8")) + snapshot = InterfaceSnapshot.model_validate_json(path.read_text(encoding="utf-8")) except (OSError, ValueError) as exc: raise ManifestError(f"Unable to read snapshot: {exc}") from exc + _validate_snapshot_version(snapshot.tool_semantics_version) + return snapshot diff --git a/tests/test_scanner.py b/tests/test_scanner.py index 03bbf4d..897dd84 100644 --- a/tests/test_scanner.py +++ b/tests/test_scanner.py @@ -2,7 +2,7 @@ import pytest -from tool_semantics.scanner import ManifestError, capture_manifest +from tool_semantics.scanner import ManifestError, capture_manifest, read_snapshot, write_snapshot def test_capture_manifest() -> None: @@ -71,3 +71,25 @@ def test_capture_rejects_non_object_parameter_schema(tmp_path: Path) -> None: match="Schema for parameter 'query' must be an object", ): capture_manifest(bad) + + +def test_read_snapshot_accepts_supported_version(tmp_path: Path) -> None: + snapshot = capture_manifest(Path("examples/github_server_v1.json")) + path = tmp_path / "snap.json" + write_snapshot(snapshot, path) + loaded = read_snapshot(path) + assert loaded.tool_semantics_version == "0.1" + assert loaded.server_name == "github-demo" + + +def test_read_snapshot_rejects_unsupported_version(tmp_path: Path) -> None: + path = tmp_path / "future.json" + path.write_text( + ( + '{"tool_semantics_version": "9.9", "protocol": "manifest", ' + '"server_name": "demo", "tools": []}' + ), + encoding="utf-8", + ) + with pytest.raises(ManifestError, match="Unsupported tool_semantics_version '9.9'"): + read_snapshot(path)