Skip to content
Open
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
26 changes: 15 additions & 11 deletions src/vouch/capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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):
Expand Down
37 changes: 19 additions & 18 deletions tests/test_capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."
)
Expand All @@ -68,20 +69,20 @@ 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() == {}


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() == {}
Loading