Skip to content
Draft
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
28 changes: 28 additions & 0 deletions src/vouch/capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand All @@ -98,4 +125,5 @@ def capabilities() -> Capabilities:
"config_path": "retrieval.scope",
},
context_engines=[describe_engine()],
host_compat=_load_host_compat(),
)
9 changes: 9 additions & 0 deletions src/vouch/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}}.'
),
)
47 changes: 47 additions & 0 deletions tests/test_capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

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