diff --git a/CLAUDE.md b/CLAUDE.md index 636b8deb..bf139926 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -173,12 +173,12 @@ Two files make the vouch repo loadable directly as an OpenClaw plugin * [`openclaw.plugin.json`](./openclaw.plugin.json) — the manifest the loader parses: `id` + `configSchema` (required), `kind: context-engine`, - `version`, and `skills` (directories under `adapters/openclaw/skills/`, - one SKILL.md each; OpenClaw publishes them as skills *and* slash - commands). + `version`, the `openclaw.compat.pluginApi` floor, and `skills` + (directories under `adapters/openclaw/skills/`, one SKILL.md each; + OpenClaw publishes them as skills *and* slash commands). * [`package.json`](./package.json) — loader-facing only: the `openclaw.extensions` pointer at the JS entry module and the - `openclaw.compat.pluginApi` floor. The Python package stays in + matching `openclaw.compat.pluginApi` floor. The Python package stays in `pyproject.toml`. Invariants, all enforced by `tests/test_openclaw_plugin_manifest.py`: diff --git a/openclaw.plugin.json b/openclaw.plugin.json index ffcd470d..b2d853d6 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -4,6 +4,11 @@ "version": "1.2.2", "description": "Git-native, review-gated knowledge base. Registers vouch's context engine (cited retrieval + salience reflex + hot memory) and the vouch skills. The kb.* MCP server is deployment config: `openclaw mcp add vouch -- vouch serve`.", "kind": "context-engine", + "openclaw": { + "compat": { + "pluginApi": ">=2026.6.0" + } + }, "skills": [ "adapters/openclaw/skills" ], diff --git a/schemas/capabilities.schema.json b/schemas/capabilities.schema.json index b986604e..803f9411 100644 --- a/schemas/capabilities.schema.json +++ b/schemas/capabilities.schema.json @@ -12,6 +12,17 @@ "title": "Context Engines", "type": "array" }, + "host_compat": { + "additionalProperties": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "description": "Host compatibility metadata keyed by host/plugin id.", + "title": "Host Compat", + "type": "object" + }, "knowledge_capability": { "additionalProperties": true, "title": "Knowledge Capability", diff --git a/src/vouch/capabilities.py b/src/vouch/capabilities.py index 3fd21c75..f5b5d642 100644 --- a/src/vouch/capabilities.py +++ b/src/vouch/capabilities.py @@ -11,6 +11,8 @@ from .models import Capabilities from .openclaw.context_engine import describe_engine +OPENCLAW_PLUGIN_API = ">=2026.6.0" + # The full method surface this implementation exposes. Keep this list in # sync with the MCP server + JSONL server registrations — `test_capabilities` # asserts they match. @@ -98,4 +100,5 @@ def capabilities() -> Capabilities: "config_path": "retrieval.scope", }, context_engines=[describe_engine()], + host_compat={"openclaw": {"pluginApi": OPENCLAW_PLUGIN_API}}, ) diff --git a/src/vouch/models.py b/src/vouch/models.py index dccf4453..2d7fd401 100644 --- a/src/vouch/models.py +++ b/src/vouch/models.py @@ -457,3 +457,7 @@ class Capabilities(BaseModel): default_factory=list, description="OpenClaw context engines exposed (see openclaw.plugin.json)", ) + host_compat: dict[str, dict[str, str]] = Field( + default_factory=dict, + description="Host compatibility metadata keyed by host/plugin id.", + ) diff --git a/tests/test_capabilities.py b/tests/test_capabilities.py index 736b20ba..19fb8197 100644 --- a/tests/test_capabilities.py +++ b/tests/test_capabilities.py @@ -2,9 +2,20 @@ from __future__ import annotations +import json +from pathlib import Path + from vouch import capabilities from vouch.jsonl_server import HANDLERS +REPO_ROOT = Path(__file__).resolve().parents[1] +MANIFEST_PATH = REPO_ROOT / "openclaw.plugin.json" + + +def _manifest_plugin_api() -> str: + manifest = json.loads(MANIFEST_PATH.read_text(encoding="utf-8")) + return manifest["openclaw"]["compat"]["pluginApi"] + def test_capabilities_matches_jsonl_handlers() -> None: caps = capabilities.capabilities() @@ -15,3 +26,15 @@ def test_capabilities_matches_jsonl_handlers() -> None: f"missing handlers={declared - implemented}, " f"missing capabilities={implemented - declared}" ) + + +def test_capabilities_host_compat_matches_openclaw_manifest() -> None: + caps = capabilities.capabilities() + manifest_range = _manifest_plugin_api() + assert caps.host_compat["openclaw"]["pluginApi"] == manifest_range + + +def test_capabilities_host_compat_present_and_nonempty() -> None: + caps = capabilities.capabilities() + assert "openclaw" in caps.host_compat + assert caps.host_compat["openclaw"]["pluginApi"] diff --git a/tests/test_openclaw_plugin_manifest.py b/tests/test_openclaw_plugin_manifest.py index d67b35bc..7aee762b 100644 --- a/tests/test_openclaw_plugin_manifest.py +++ b/tests/test_openclaw_plugin_manifest.py @@ -101,6 +101,12 @@ def test_package_json_declares_entry_and_compat(package_json: dict) -> None: assert openclaw["compat"]["pluginApi"] +def test_manifest_declares_openclaw_compat(manifest: dict, package_json: dict) -> None: + assert manifest["openclaw"]["compat"]["pluginApi"] == ( + package_json["openclaw"]["compat"]["pluginApi"] + ) + + def test_manifest_skills_are_publishable_dirs(manifest: dict) -> None: """Each skills entry must resolve to dirs OpenClaw can publish. @@ -144,7 +150,6 @@ def test_manifest_carries_no_dead_dialect_fields(manifest: dict) -> None: "mcpServers", "shared_deps", "excluded_from_install", - "openclaw", "contracts", ) for dead in dead_fields: