"""语言无关的专业软件 capability 契约加载与查询。""" from __future__ import annotations import json import logging import re import threading from dataclasses import dataclass from hashlib import sha256 from pathlib import Path, PurePosixPath from typing import Any from jsonschema import Draft202012Validator, FormatChecker CONTRACT_ROOT = Path(__file__).resolve().parents[1] / "software-contracts" logger = logging.getLogger(__name__) class SoftwareContractError(ValueError): pass @dataclass(frozen=True) class OutputSpec: output_id: str filename: str media_type: str relative_path: str publish: bool required: bool @dataclass(frozen=True) class CapabilityContract: capability: str display_name: str default_enrollment: bool output_namespace: str request_schema: dict[str, Any] input_policy: dict[str, Any] outputs: dict[str, OutputSpec] feature_path: tuple[str, ...] features: dict[str, str] summary: dict[str, Any] legacy_runtime: dict[str, Any] | None def normalize_request(self, request: object) -> tuple[dict[str, Any], str]: errors = sorted( Draft202012Validator( self.request_schema, format_checker=FormatChecker() ).iter_errors(request), key=lambda item: list(item.absolute_path), ) if errors: first = errors[0] location = ".".join(str(item) for item in first.absolute_path) prefix = f"{location}: " if location else "" raise SoftwareContractError(f"invalid {self.capability} request: {prefix}{first.message}") encoded = json.dumps(request, ensure_ascii=False, sort_keys=True, separators=(",", ":")) max_bytes = int(self.request_schema.get("x-maxBytes") or 256 * 1024) if len(encoded.encode("utf-8")) > max_bytes: raise SoftwareContractError(f"{self.capability} request is too large") normalized = json.loads(encoded) return normalized, sha256(encoded.encode("utf-8")).hexdigest() def input_bindings(self, request: dict[str, Any]) -> list[dict[str, Any]]: return list(request.get("inputs") or []) def expected_outputs(self, request: dict[str, Any]) -> dict[str, OutputSpec]: requested = { item.get("key") for item in request.get("outputs") or [] if isinstance(item, dict) and isinstance(item.get("key"), str) } expected = { output_id: spec for output_id, spec in self.outputs.items() if spec.required or output_id in requested } if requested - self.outputs.keys(): raise SoftwareContractError(f"{self.capability} request contains unknown outputs") return expected def output_spec(self, output_id: str) -> OutputSpec: try: return self.outputs[output_id] except KeyError as exc: raise SoftwareContractError("unsupported output artifact identity") from exc def feature(self, request: dict[str, Any]) -> str: value: Any = request for part in self.feature_path: if not isinstance(value, dict): return "" value = value.get(part) return value if isinstance(value, str) else "" def required_adapter_version(self, request: dict[str, Any]) -> str: return self.features.get(self.feature(request), "0.0.0") def summarize(self, request: dict[str, Any]) -> dict[str, Any]: return { "display_name": self.display_name, "title": str(_value_at(request, self.summary.get("title_path") or []) or ""), "formats": [ item.get("format") for item in request.get("outputs") or [] if isinstance(item, dict) ], } def submission_schema(self) -> dict[str, Any]: definitions = self.request_schema.get("$defs") or {} properties = _resolve_local_refs( dict(self.request_schema.get("properties") or {}), definitions ) properties.pop("schema_version", None) required = [ item for item in self.request_schema.get("required") or [] if item != "schema_version" ] return { "type": "object", "properties": properties, "required": required, "additionalProperties": False, } def _value_at(value: object, path: list[str]) -> object: current = value for part in path: if not isinstance(current, dict): return None current = current.get(part) return current def _resolve_local_refs(value: Any, definitions: dict[str, Any]) -> Any: if isinstance(value, list): return [_resolve_local_refs(item, definitions) for item in value] if not isinstance(value, dict): return value if set(value) == {"$ref"} and isinstance(value["$ref"], str): prefix = "#/$defs/" if not value["$ref"].startswith(prefix): raise RuntimeError("software contract contains a non-local schema reference") name = value["$ref"][len(prefix):] if name not in definitions: raise RuntimeError("software contract contains an unknown schema reference") return _resolve_local_refs(definitions[name], definitions) return { key: _resolve_local_refs(item, definitions) for key, item in value.items() } def _load_contract(path: Path) -> CapabilityContract: raw = json.loads(path.read_text(encoding="utf-8")) required = { "capability", "display_name", "default_enrollment", "output_namespace", "request_schema", "input_policy", "outputs", "feature_path", "features", "summary", "legacy_runtime", } if not isinstance(raw, dict) or set(raw) != required: raise RuntimeError(f"invalid software contract fields: {path.name}") capability = raw["capability"] namespace = raw["output_namespace"] if not isinstance(capability, str) or not re.fullmatch(r"[a-z][a-z0-9_.-]+@v[1-9][0-9]*", capability): raise RuntimeError(f"invalid software capability: {path.name}") if not isinstance(namespace, str) or not re.fullmatch(r"[a-z][a-z0-9_-]{0,31}", namespace): raise RuntimeError(f"invalid output namespace: {path.name}") outputs: dict[str, OutputSpec] = {} for output_id, item in raw["outputs"].items(): if not isinstance(item, dict) or set(item) != { "filename", "media_type", "relative_path", "publish", "required" }: raise RuntimeError(f"invalid output spec: {path.name}:{output_id}") filename = item["filename"] relative_path = item["relative_path"] parsed_path = PurePosixPath(relative_path) if isinstance(relative_path, str) else None if ( not isinstance(output_id, str) or not re.fullmatch(r"[a-z][a-z0-9_]{0,63}", output_id) or not isinstance(filename, str) or PurePosixPath(filename).name != filename or parsed_path is None or parsed_path.is_absolute() or ".." in parsed_path.parts or parsed_path.name != filename or parsed_path.as_posix() != relative_path or not isinstance(item["media_type"], str) or not item["media_type"] or not isinstance(item["publish"], bool) or not isinstance(item["required"], bool) ): raise RuntimeError(f"unsafe output spec: {path.name}:{output_id}") outputs[output_id] = OutputSpec(output_id=output_id, **item) if len({item.relative_path for item in outputs.values()}) != len(outputs): raise RuntimeError(f"duplicate output paths: {path.name}") Draft202012Validator.check_schema(raw["request_schema"]) return CapabilityContract( capability=capability, display_name=raw["display_name"], default_enrollment=raw["default_enrollment"], output_namespace=namespace, request_schema=raw["request_schema"], input_policy=raw["input_policy"], outputs=outputs, feature_path=tuple(raw["feature_path"]), features=raw["features"], summary=raw["summary"], legacy_runtime=raw["legacy_runtime"], ) class _ContractRegistry: """Atomically reload contracts after a file change; retain the last valid snapshot.""" def __init__(self, root: Path) -> None: self.root = root self._lock = threading.RLock() self._signature: tuple[tuple[str, int, int], ...] = () self._failed_signature: tuple[tuple[str, int, int], ...] | None = None self._contracts: dict[str, CapabilityContract] = {} self._reload(initial=True) def _files(self) -> list[Path]: return sorted(self.root.glob("*.json")) def _current_signature(self) -> tuple[tuple[str, int, int], ...]: return tuple( (path.name, path.stat().st_mtime_ns, path.stat().st_size) for path in self._files() ) def _reload(self, *, initial: bool = False) -> None: with self._lock: signature = self._current_signature() if not initial and signature in {self._signature, self._failed_signature}: return try: loaded = [_load_contract(path) for path in self._files()] contracts = {item.capability: item for item in loaded} if not contracts or len(contracts) != len(loaded): raise RuntimeError( "software contracts are missing or contain duplicate capabilities" ) except Exception: if initial: raise self._failed_signature = signature logger.exception("Ignoring invalid software contract update") return self._contracts = contracts self._signature = signature self._failed_signature = None def contracts(self) -> dict[str, CapabilityContract]: self._reload() with self._lock: return dict(self._contracts) _registry = _ContractRegistry(CONTRACT_ROOT) def get_contracts() -> dict[str, CapabilityContract]: return _registry.contracts() def supported_capabilities() -> frozenset[str]: return frozenset(get_contracts()) def default_capabilities() -> tuple[str, ...]: return tuple( item.capability for item in get_contracts().values() if item.default_enrollment ) # Import compatibility only. Runtime paths use the query functions above so edits hot reload. CONTRACTS = get_contracts() SUPPORTED_CAPABILITIES = supported_capabilities() DEFAULT_CAPABILITIES = default_capabilities() def get_contract(capability: str) -> CapabilityContract: try: return get_contracts()[capability] except KeyError as exc: raise SoftwareContractError("unsupported capability") from exc def version_at_least(actual: str, required: str) -> bool: def parts(value: str) -> tuple[int, ...]: match = re.match(r"^(\d+)(?:\.(\d+))?(?:\.(\d+))?", value.strip()) return tuple(int(item or 0) for item in match.groups(default="0")) if match else (0, 0, 0) return parts(actual) >= parts(required) def node_supports_request( contract: CapabilityContract, request: dict[str, Any], runtime: dict[str, Any], ) -> bool: capability_runtime = (runtime.get("capability_runtime") or {}).get(contract.capability) if isinstance(capability_runtime, dict): if capability_runtime.get("health") != "ready": return False if int(capability_runtime.get("available_slots") or 0) <= 0: return False actual_version = str(capability_runtime.get("adapter_version") or "0.0.0") advertised_features = capability_runtime.get("features") feature = contract.feature(request) if isinstance(advertised_features, list) and feature not in advertised_features: return False return version_at_least(actual_version, contract.required_adapter_version(request)) # 兼容尚未升级 capability_runtime 的 Node;兼容路径由版本化 contract 声明。 legacy_config = contract.legacy_runtime or {} if not legacy_config: return False legacy = _value_at(runtime, legacy_config.get("detail_path") or []) assumed_version = str(legacy_config.get("assumed_adapter_version") or "0.0.0") slots = int(_value_at(runtime, legacy_config.get("slots_path") or []) or 0) if legacy is None: return ( slots > 0 and version_at_least(assumed_version, contract.required_adapter_version(request)) ) return ( isinstance(legacy, dict) and legacy.get("health") == "ready" and slots > 0 and version_at_least( str(legacy.get("adapter_version") or "0.0.0"), contract.required_adapter_version(request), ) ) def node_available_slots(capability: str, runtime: dict[str, Any]) -> int: contract = get_contract(capability) item = (runtime.get("capability_runtime") or {}).get(capability) if isinstance(item, dict) and item.get("health") == "ready": return max(0, int(item.get("available_slots") or 0)) legacy_config = contract.legacy_runtime or {} if not legacy_config: return 0 legacy = _value_at(runtime, legacy_config.get("detail_path") or []) slots = max(0, int(_value_at(runtime, legacy_config.get("slots_path") or []) or 0)) if legacy is None: return slots if ( isinstance(legacy, dict) and legacy.get("health") == "ready" ): return slots return 0