diff --git a/src/vouch/capabilities.py b/src/vouch/capabilities.py index 1393d978..677f5de6 100644 --- a/src/vouch/capabilities.py +++ b/src/vouch/capabilities.py @@ -17,10 +17,12 @@ _log = logging.getLogger(__name__) -# Path to the plugin manifest, relative to this module. capabilities.py lives -# at src/vouch/capabilities.py; openclaw.plugin.json lives at the repo root, -# three levels up (src/vouch/ -> src/ -> repo root). -_PLUGIN_MANIFEST_PATH = Path(__file__).resolve().parent.parent.parent / "openclaw.plugin.json" +# Path to the loader-facing package.json at the repo root, three levels up +# (src/vouch/ -> src/ -> repo root). The 2026.6 openclaw repackage moved the +# `openclaw.compat.pluginApi` floor out of openclaw.plugin.json (whose +# `openclaw.*` fields the current loader ignores) and into package.json, so +# that is now the single source of truth for host compat (#237). +_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` @@ -90,19 +92,19 @@ def _load_host_compat() -> dict[str, dict[str, str]]: - """Read the `openclaw.compat` block from openclaw.plugin.json (#237). + """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 the manifest themselves. Returns an - empty dict (rather than raising) if the manifest is missing or + empty dict (rather than raising) if package.json is missing or malformed — capabilities() must never fail to report basic info just because the manifest moved or this is installed as a standalone wheel - without the manifest packaged alongside it. + without package.json packaged alongside it. """ try: - manifest = json.loads(_PLUGIN_MANIFEST_PATH.read_text(encoding="utf-8")) + manifest = json.loads(_PACKAGE_JSON_PATH.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError) as e: - _log.debug("openclaw.plugin.json unreadable, host_compat will be empty: %s", e) + _log.debug("package.json unreadable, host_compat will be empty: %s", e) return {} compat = manifest.get("openclaw", {}).get("compat") if not isinstance(compat, dict): diff --git a/tests/test_capabilities.py b/tests/test_capabilities.py index 6908a3cb..cd133a8e 100644 --- a/tests/test_capabilities.py +++ b/tests/test_capabilities.py @@ -24,31 +24,32 @@ def test_capabilities_matches_jsonl_handlers() -> None: # --- host_compat drift detection (#237) ----------------------------------- # -# vouch declares openclaw.compat.pluginApi in openclaw.plugin.json. The same -# value must surface in kb.capabilities so non-OpenClaw clients can detect -# compat without parsing the manifest. These tests fail CI with a clear -# message if the two declarations drift apart. - -_MANIFEST_PATH = ( - Path(__file__).resolve().parent.parent / "openclaw.plugin.json" +# vouch declares openclaw.compat.pluginApi in package.json (the loader-facing +# manifest the 2026.6 repackage moved it to). The same value must surface in +# kb.capabilities so non-OpenClaw clients can detect compat without parsing +# the manifest. These tests fail CI with a clear message if the two +# declarations drift apart. + +_PACKAGE_JSON_PATH = ( + Path(__file__).resolve().parent.parent / "package.json" ) def _manifest_plugin_api() -> str: - manifest = json.loads(_MANIFEST_PATH.read_text(encoding="utf-8")) + manifest = json.loads(_PACKAGE_JSON_PATH.read_text(encoding="utf-8")) return manifest["openclaw"]["compat"]["pluginApi"] def test_capabilities_host_compat_matches_openclaw_manifest() -> None: """kb.capabilities.host_compat.openclaw.pluginApi must equal the - pluginApi range declared in openclaw.plugin.json. A bump in one file + pluginApi range declared in package.json. A bump in one file without the other is exactly the "host compat drift" #237 asks CI to catch.""" caps = capabilities.capabilities() manifest_range = _manifest_plugin_api() capabilities_range = caps.host_compat.get("openclaw", {}).get("pluginApi") assert capabilities_range == manifest_range, ( - f"host compat drift: openclaw.plugin.json declares pluginApi=" + f"host compat drift: package.json declares pluginApi=" f"{manifest_range!r} but kb.capabilities.host_compat reports " f"{capabilities_range!r}. Keep both in sync." ) @@ -68,10 +69,10 @@ def test_load_host_compat_returns_empty_on_missing_manifest( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, ) -> None: """_load_host_compat must degrade gracefully (empty dict, no raise) if - the manifest is absent -- e.g. installed as a standalone wheel without - openclaw.plugin.json packaged alongside it.""" + package.json is absent -- e.g. installed as a standalone wheel without + package.json packaged alongside it.""" monkeypatch.setattr( - capabilities, "_PLUGIN_MANIFEST_PATH", tmp_path / "does-not-exist.json" + capabilities, "_PACKAGE_JSON_PATH", tmp_path / "does-not-exist.json" ) assert capabilities._load_host_compat() == {} @@ -81,7 +82,7 @@ def test_load_host_compat_returns_empty_on_malformed_manifest( ) -> None: """A malformed manifest must not crash capabilities() -- it's reporting diagnostic info, not validating the install.""" - bad = tmp_path / "openclaw.plugin.json" + bad = tmp_path / "package.json" bad.write_text("{not valid json", encoding="utf-8") - monkeypatch.setattr(capabilities, "_PLUGIN_MANIFEST_PATH", bad) + monkeypatch.setattr(capabilities, "_PACKAGE_JSON_PATH", bad) assert capabilities._load_host_compat() == {}