Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions src/tool_semantics/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", []))
Expand Down Expand Up @@ -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"),
)
)
Expand All @@ -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
24 changes: 23 additions & 1 deletion tests/test_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Loading