From d758880f95b547cce84306311bc71e9378dca0e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:22:45 +0000 Subject: [PATCH 1/2] Initial plan From a9d08109ef441ed2683900bc48b6120b90f2f097 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:27:21 +0000 Subject: [PATCH 2/2] fix(capabilities): read openclaw compat from package metadata --- src/vouch/capabilities.py | 28 +++++++++++++++++++++++ src/vouch/models.py | 9 ++++++++ tests/test_capabilities.py | 47 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+) diff --git a/src/vouch/capabilities.py b/src/vouch/capabilities.py index 3fd21c75..6441e955 100644 --- a/src/vouch/capabilities.py +++ b/src/vouch/capabilities.py @@ -7,10 +7,19 @@ from __future__ import annotations +import json +import logging +from pathlib import Path + from . import __version__ from .models import Capabilities from .openclaw.context_engine import describe_engine +_log = logging.getLogger(__name__) + +# package.json is the authoritative source for the OpenClaw pluginApi range. +_PACKAGE_JSON_PATH = Path(__file__).resolve().parent.parent.parent / "package.json" + # The full method surface this implementation exposes. Keep this list in # sync with the MCP server + JSONL server registrations — `test_capabilities` # asserts they match. @@ -76,6 +85,24 @@ ] +def _load_host_compat() -> dict[str, dict[str, str]]: + """Read the `openclaw.compat` block from package.json (#237). + + Surfaced in `kb.capabilities` as `host_compat` so non-OpenClaw clients + can detect compat without parsing package metadata themselves. Returns an + empty dict if package.json is missing or malformed. + """ + try: + package_json = json.loads(_PACKAGE_JSON_PATH.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as e: + _log.debug("package.json unreadable, host_compat will be empty: %s", e) + return {} + compat = package_json.get("openclaw", {}).get("compat") + if not isinstance(compat, dict): + return {} + return {"openclaw": {k: str(v) for k, v in compat.items()}} + + def capabilities() -> Capabilities: retrieval = ["fts5", "substring"] try: @@ -98,4 +125,5 @@ def capabilities() -> Capabilities: "config_path": "retrieval.scope", }, context_engines=[describe_engine()], + host_compat=_load_host_compat(), ) diff --git a/src/vouch/models.py b/src/vouch/models.py index dccf4453..71820caa 100644 --- a/src/vouch/models.py +++ b/src/vouch/models.py @@ -457,3 +457,12 @@ class Capabilities(BaseModel): default_factory=list, description="OpenClaw context engines exposed (see openclaw.plugin.json)", ) + host_compat: dict[str, Any] = Field( + default_factory=dict, + description=( + "Per-host compatibility ranges (#237). Mirrors the " + "`openclaw.compat` block in package.json so non-OpenClaw " + "clients can detect compat without parsing the package metadata, " + 'e.g. {"openclaw": {"pluginApi": ">=2026.6.0"}}.' + ), + ) diff --git a/tests/test_capabilities.py b/tests/test_capabilities.py index 736b20ba..bfef8a11 100644 --- a/tests/test_capabilities.py +++ b/tests/test_capabilities.py @@ -2,6 +2,11 @@ from __future__ import annotations +import json +from pathlib import Path + +import pytest + from vouch import capabilities from vouch.jsonl_server import HANDLERS @@ -15,3 +20,45 @@ def test_capabilities_matches_jsonl_handlers() -> None: f"missing handlers={declared - implemented}, " f"missing capabilities={implemented - declared}" ) + + +_PACKAGE_JSON_PATH = Path(__file__).resolve().parent.parent / "package.json" + + +def _package_plugin_api() -> str: + package_json = json.loads(_PACKAGE_JSON_PATH.read_text(encoding="utf-8")) + return package_json["openclaw"]["compat"]["pluginApi"] + + +def test_capabilities_host_compat_matches_package_json() -> None: + caps = capabilities.capabilities() + package_range = _package_plugin_api() + capabilities_range = caps.host_compat.get("openclaw", {}).get("pluginApi") + assert capabilities_range == package_range, ( + f"host compat drift: package.json declares pluginApi={package_range!r} " + f"but kb.capabilities.host_compat reports {capabilities_range!r}. " + f"Keep both in sync." + ) + + +def test_capabilities_host_compat_present_and_nonempty() -> None: + caps = capabilities.capabilities() + assert "openclaw" in caps.host_compat + assert "pluginApi" in caps.host_compat["openclaw"] + assert caps.host_compat["openclaw"]["pluginApi"].strip() != "" + + +def test_load_host_compat_returns_empty_on_missing_package_json( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, +) -> None: + monkeypatch.setattr(capabilities, "_PACKAGE_JSON_PATH", tmp_path / "does-not-exist.json") + assert capabilities._load_host_compat() == {} + + +def test_load_host_compat_returns_empty_on_malformed_package_json( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, +) -> None: + bad = tmp_path / "package.json" + bad.write_text("{not valid json", encoding="utf-8") + monkeypatch.setattr(capabilities, "_PACKAGE_JSON_PATH", bad) + assert capabilities._load_host_compat() == {}