diff --git a/src/vouch/capabilities.py b/src/vouch/capabilities.py index 1393d978..6c60d5bb 100644 --- a/src/vouch/capabilities.py +++ b/src/vouch/capabilities.py @@ -17,10 +17,14 @@ _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" +# package.json — not openclaw.plugin.json — is where the openclaw.compat +# floor lives: the manifest deliberately excludes a top-level "openclaw" key +# (see test_openclaw_plugin_manifest.py's dead-dialect-fields check), so +# package.json's openclaw.compat.pluginApi is the only place this can be +# read from. Path is relative to this module: capabilities.py lives at +# src/vouch/capabilities.py; package.json lives at the repo root, three +# levels up (src/vouch/ -> src/ -> repo root). +_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 +94,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 + can detect compat without parsing package.json themselves. Returns an + 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. + because the file moved or this is installed as a standalone wheel + 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..72acbf70 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 +# vouch declares openclaw.compat.pluginApi in package.json (openclaw.plugin +# .json deliberately excludes a top-level "openclaw" key -- see +# test_openclaw_plugin_manifest.py's dead-dialect-fields check). 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 +# compat without parsing package.json. These tests fail CI with a clear # message if the two declarations drift apart. -_MANIFEST_PATH = ( - Path(__file__).resolve().parent.parent / "openclaw.plugin.json" +_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")) +def _package_json_plugin_api() -> str: + 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 - without the other is exactly the "host compat drift" #237 asks CI to - catch.""" + 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() + manifest_range = _package_json_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() == {} @@ -79,9 +80,9 @@ def test_load_host_compat_returns_empty_on_missing_manifest( def test_load_host_compat_returns_empty_on_malformed_manifest( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, ) -> None: - """A malformed manifest must not crash capabilities() -- it's reporting - diagnostic info, not validating the install.""" - bad = tmp_path / "openclaw.plugin.json" + """A malformed package.json must not crash capabilities() -- it's + reporting diagnostic info, not validating the install.""" + 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() == {}