"""语言无关的专业软件 capability 契约加载与查询。""" from __future__ import annotations import json import re 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" 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"], ) _loaded_contracts = [_load_contract(path) for path in sorted(CONTRACT_ROOT.glob("*.json"))] CONTRACTS = {contract.capability: contract for contract in _loaded_contracts} if not CONTRACTS or len(CONTRACTS) != len(_loaded_contracts): raise RuntimeError("software contracts are missing or contain duplicate capabilities") SUPPORTED_CAPABILITIES = frozenset(CONTRACTS) DEFAULT_CAPABILITIES = tuple( item.capability for item in CONTRACTS.values() if item.default_enrollment ) def get_contract(capability: str) -> CapabilityContract: try: return 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