Files
ljwx-deploy/scripts/platform/validate_capability_contracts.py
T

475 lines
18 KiB
Python

#!/usr/bin/env python3
"""Validate capability ownership and gateway contracts."""
from __future__ import annotations
import json
import sys
from pathlib import Path
try:
import jsonschema
except ImportError as exc:
raise SystemExit(
f"缺少依赖 jsonschema,请使用 uvx --with jsonschema --with pyyaml python <script>\n{exc}"
) from exc
try:
import yaml
except ImportError as exc:
raise SystemExit(
f"缺少依赖 PyYAML,请使用 uvx --with pyyaml --with jsonschema python <script>\n{exc}"
) from exc
CAPABILITY_REGISTRY_FILE = Path("platform/assembly/capabilities.yaml")
SERVICE_MAP_FILE = Path("platform/contracts/service-capability-map.yaml")
OWNERSHIP_MATRIX_FILE = Path("platform/contracts/capability-ownership-matrix.yaml")
RELEASE_VERSION_FILE = Path("release/platform-version.yaml")
GATEWAY_REQUEST_SCHEMA_FILE = Path(
"platform/contracts/capability-gateway-request.schema.json"
)
AUDIT_EVENT_SCHEMA_FILE = Path("platform/contracts/audit-event.schema.json")
KNOWN_PLATFORM_SERVICES = {"openclaw", "knowledge-processor", "n8n"}
class ValidationError(Exception):
"""Raised when a platform contract is invalid."""
def load_yaml_mapping(path: Path) -> dict[str, object]:
raw = yaml.safe_load(path.read_text(encoding="utf-8"))
if raw is None:
return {}
if not isinstance(raw, dict):
raise ValidationError(f"{path} 根节点必须是 mapping")
return {str(key): value for key, value in raw.items()}
def load_json_mapping(path: Path) -> dict[str, object]:
raw = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(raw, dict):
raise ValidationError(f"{path} 根节点必须是 object")
return {str(key): value for key, value in raw.items()}
def require_mapping(value: object, path: str) -> dict[str, object]:
if not isinstance(value, dict):
raise ValidationError(f"{path} 必须是 mapping")
return {str(key): item for key, item in value.items()}
def require_list(value: object, path: str) -> list[object]:
if not isinstance(value, list):
raise ValidationError(f"{path} 必须是列表")
return value
def require_non_empty_str(value: object, path: str) -> str:
if not isinstance(value, str) or not value.strip():
raise ValidationError(f"{path} 必须是非空字符串")
return value.strip()
def optional_non_empty_str(value: object, path: str) -> str | None:
if value in ("", None):
return None
return require_non_empty_str(value, path)
def load_validator(path: Path) -> jsonschema.Draft202012Validator:
schema = load_json_mapping(path)
jsonschema.Draft202012Validator.check_schema(schema)
return jsonschema.Draft202012Validator(schema)
def validate_examples() -> list[str]:
errors: list[str] = []
gateway_validator = load_validator(GATEWAY_REQUEST_SCHEMA_FILE)
audit_validator = load_validator(AUDIT_EVENT_SCHEMA_FILE)
gateway_example: dict[str, object] = {
"trace_id": "trace-core-api-001",
"caller_service": "openclaw",
"capability": "customer.lookup",
"profile_name": "default",
"route_id": "knowledge_qa",
"actor_id": "user-001",
"payload": {"customer_id": "acme"},
"meta": {"channel": "web"},
}
audit_example: dict[str, object] = {
"audit_event_id": "audit-20260307-001",
"timestamp": "2026-03-07T12:00:00+00:00",
"component": "ljwx-core-api",
"event_type": "capability_execution",
"trace_id": "trace-core-api-001",
"route_id": "knowledge_qa",
"capability": "customer.lookup",
"success": True,
"caller_service": "openclaw",
"details": {
"duration_ms": 12,
"profile_name": "default",
},
}
for name, validator, payload in (
("capability-gateway-request", gateway_validator, gateway_example),
("audit-event", audit_validator, audit_example),
):
try:
validator.validate(payload)
except jsonschema.ValidationError as exc:
errors.append(f"{name} 示例不符合 schema: {exc.message}")
return errors
def validate_version_references(
capability_registry: dict[str, object],
service_map: dict[str, object],
release_version: dict[str, object],
) -> list[str]:
errors: list[str] = []
capability_version = require_non_empty_str(
capability_registry.get("version"),
"platform/assembly/capabilities.yaml.version",
)
service_map_version = require_non_empty_str(
service_map.get("version"),
"platform/contracts/service-capability-map.yaml.version",
)
config_versions = require_mapping(
release_version.get("config_versions"),
"release/platform-version.yaml.config_versions",
)
expected_capability_ref = (
f"{CAPABILITY_REGISTRY_FILE.as_posix()}@{capability_version}"
)
if config_versions.get("capability_registry") != expected_capability_ref:
errors.append(
"release/platform-version.yaml.config_versions.capability_registry "
f"必须等于 {expected_capability_ref}"
)
expected_service_map_ref = f"{SERVICE_MAP_FILE.as_posix()}@{service_map_version}"
if config_versions.get("service_capability_map") != expected_service_map_ref:
errors.append(
"release/platform-version.yaml.config_versions.service_capability_map "
f"必须等于 {expected_service_map_ref}"
)
ownership_matrix = load_yaml_mapping(OWNERSHIP_MATRIX_FILE)
ownership_matrix_version = require_non_empty_str(
ownership_matrix.get("version"),
"platform/contracts/capability-ownership-matrix.yaml.version",
)
expected_ownership_matrix_ref = (
f"{OWNERSHIP_MATRIX_FILE.as_posix()}@{ownership_matrix_version}"
)
if (
config_versions.get("capability_ownership_matrix")
!= expected_ownership_matrix_ref
):
errors.append(
"release/platform-version.yaml.config_versions.capability_ownership_matrix "
f"必须等于 {expected_ownership_matrix_ref}"
)
return errors
def validate_registry_and_service_map(
capability_registry: dict[str, object],
service_map: dict[str, object],
) -> list[str]:
errors: list[str] = []
raw_capabilities = require_mapping(
capability_registry.get("capabilities"),
"platform/assembly/capabilities.yaml.capabilities",
)
capabilities: dict[str, dict[str, object]] = {}
for capability_name, raw_capability in raw_capabilities.items():
capability = require_mapping(
raw_capability,
f"platform/assembly/capabilities.yaml.capabilities.{capability_name}",
)
capabilities[capability_name] = capability
try:
require_non_empty_str(
capability.get("executor"),
f"platform/assembly/capabilities.yaml.capabilities.{capability_name}.executor",
)
require_non_empty_str(
capability.get("owner_service"),
f"platform/assembly/capabilities.yaml.capabilities.{capability_name}.owner_service",
)
require_non_empty_str(
capability.get("auth"),
f"platform/assembly/capabilities.yaml.capabilities.{capability_name}.auth",
)
require_non_empty_str(
capability.get("audit"),
f"platform/assembly/capabilities.yaml.capabilities.{capability_name}.audit",
)
timeout_ms = capability.get("timeout_ms")
if not isinstance(timeout_ms, int) or timeout_ms <= 0:
errors.append(
"platform/assembly/capabilities.yaml.capabilities."
f"{capability_name}.timeout_ms 必须是正整数"
)
except ValidationError as exc:
errors.append(str(exc))
services = require_mapping(
service_map.get("services"),
"platform/contracts/service-capability-map.yaml.services",
)
providers: dict[str, str] = {}
for service_name, raw_service in services.items():
service = require_mapping(
raw_service,
f"platform/contracts/service-capability-map.yaml.services.{service_name}",
)
for relation_name in ("provides", "consumes"):
entries = require_list(
service.get(relation_name),
f"platform/contracts/service-capability-map.yaml.services.{service_name}.{relation_name}",
)
for index, raw_entry in enumerate(entries):
entry = require_mapping(
raw_entry,
"platform/contracts/service-capability-map.yaml.services."
f"{service_name}.{relation_name}[{index}]",
)
try:
capability_name = require_non_empty_str(
entry.get("capability"),
"platform/contracts/service-capability-map.yaml.services."
f"{service_name}.{relation_name}[{index}].capability",
)
except ValidationError as exc:
errors.append(str(exc))
continue
if capability_name not in capabilities:
errors.append(
f"{service_name}.{relation_name}[{index}] 引用了未注册 capability: "
f"{capability_name}"
)
continue
runtime_facing = entry.get("runtime_facing")
if not isinstance(runtime_facing, bool):
errors.append(
f"{service_name}.{relation_name}[{index}].runtime_facing 必须是 boolean"
)
dispatch_via = optional_non_empty_str(
entry.get("dispatch_via"),
f"{service_name}.{relation_name}[{index}].dispatch_via",
)
if (
dispatch_via
and dispatch_via not in services
and dispatch_via not in KNOWN_PLATFORM_SERVICES
):
errors.append(
f"{service_name}.{relation_name}[{index}].dispatch_via 未知: {dispatch_via}"
)
owner_service = require_non_empty_str(
capabilities[capability_name].get("owner_service"),
f"platform/assembly/capabilities.yaml.capabilities.{capability_name}.owner_service",
)
if relation_name == "provides":
if owner_service != service_name:
errors.append(
f"{service_name}.provides 声明了 {capability_name},但 owner_service 是 "
f"{owner_service}"
)
previous_provider = providers.get(capability_name)
if previous_provider and previous_provider != service_name:
errors.append(
f"capability {capability_name} 被多个服务提供: "
f"{previous_provider}, {service_name}"
)
providers[capability_name] = service_name
continue
if runtime_facing and dispatch_via is None:
errors.append(
f"{service_name}.consumes[{index}] 为 runtime_facing=true 时必须声明 dispatch_via"
)
if dispatch_via and dispatch_via != owner_service:
errors.append(
f"{service_name}.consumes[{index}].dispatch_via={dispatch_via} "
f"必须与 owner_service={owner_service} 一致"
)
for capability_name in capabilities:
if capability_name not in providers:
errors.append(
f"capability {capability_name} 未在 service-capability-map 中声明 provider"
)
return errors
def validate_ownership_matrix_alignment(
capability_registry: dict[str, object],
service_map: dict[str, object],
ownership_matrix: dict[str, object],
) -> list[str]:
errors: list[str] = []
capabilities = require_mapping(
capability_registry.get("capabilities"),
"platform/assembly/capabilities.yaml.capabilities",
)
services = require_mapping(
service_map.get("services"),
"platform/contracts/service-capability-map.yaml.services",
)
groups = require_mapping(
ownership_matrix.get("groups"),
"platform/contracts/capability-ownership-matrix.yaml.groups",
)
allowed_owners: dict[str, set[str]] = {}
for group_name, raw_group in groups.items():
group = require_mapping(
raw_group,
f"platform/contracts/capability-ownership-matrix.yaml.groups.{group_name}",
)
group_services = {
require_non_empty_str(
item,
f"platform/contracts/capability-ownership-matrix.yaml.groups.{group_name}.services[]",
)
for item in require_list(
group.get("services"),
f"platform/contracts/capability-ownership-matrix.yaml.groups.{group_name}.services",
)
}
provided_capabilities = require_list(
group.get("provided_capabilities"),
"platform/contracts/capability-ownership-matrix.yaml.groups."
f"{group_name}.provided_capabilities",
)
for item in provided_capabilities:
capability_name = require_non_empty_str(
item,
"platform/contracts/capability-ownership-matrix.yaml.groups."
f"{group_name}.provided_capabilities[]",
)
allowed_owners.setdefault(capability_name, set()).update(group_services)
for capability_name, raw_capability in capabilities.items():
capability = require_mapping(
raw_capability,
f"platform/assembly/capabilities.yaml.capabilities.{capability_name}",
)
owner_service = require_non_empty_str(
capability.get("owner_service"),
f"platform/assembly/capabilities.yaml.capabilities.{capability_name}.owner_service",
)
permitted_owners = allowed_owners.get(capability_name)
if permitted_owners is None:
continue
if owner_service not in permitted_owners:
errors.append(
f"capability {capability_name} 的 owner_service={owner_service} 不在 ownership matrix 允许范围内: "
f"{sorted(permitted_owners)}"
)
for service_name, raw_service in services.items():
service = require_mapping(
raw_service,
f"platform/contracts/service-capability-map.yaml.services.{service_name}",
)
provides = require_list(
service.get("provides"),
f"platform/contracts/service-capability-map.yaml.services.{service_name}.provides",
)
for index, raw_entry in enumerate(provides):
entry = require_mapping(
raw_entry,
f"platform/contracts/service-capability-map.yaml.services.{service_name}.provides[{index}]",
)
capability_name = require_non_empty_str(
entry.get("capability"),
"platform/contracts/service-capability-map.yaml.services."
f"{service_name}.provides[{index}].capability",
)
permitted_owners = allowed_owners.get(capability_name)
if permitted_owners is None:
continue
if service_name not in permitted_owners:
errors.append(
f"{service_name}.provides[{index}] 声明了 {capability_name},但 ownership matrix 允许 provider 为 "
f"{sorted(permitted_owners)}"
)
return errors
def main() -> int:
try:
capability_registry = load_yaml_mapping(CAPABILITY_REGISTRY_FILE)
service_map = load_yaml_mapping(SERVICE_MAP_FILE)
ownership_matrix = load_yaml_mapping(OWNERSHIP_MATRIX_FILE)
release_version = load_yaml_mapping(RELEASE_VERSION_FILE)
except ValidationError as exc:
print(f"[capability-contracts] {exc}", file=sys.stderr)
return 1
errors = []
try:
errors.extend(
validate_version_references(
capability_registry, service_map, release_version
)
)
errors.extend(
validate_registry_and_service_map(capability_registry, service_map)
)
errors.extend(
validate_ownership_matrix_alignment(
capability_registry, service_map, ownership_matrix
)
)
errors.extend(validate_examples())
except ValidationError as exc:
errors.append(str(exc))
if errors:
for error in errors:
print(f"[capability-contracts] {error}", file=sys.stderr)
return 1
capability_count = len(
require_mapping(
capability_registry.get("capabilities"),
"platform/assembly/capabilities.yaml.capabilities",
)
)
service_count = len(
require_mapping(
service_map.get("services"),
"platform/contracts/service-capability-map.yaml.services",
)
)
print(
"[capability-contracts] valid "
f"capabilities={capability_count} services={service_count}"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())