Files

140 lines
8.4 KiB
Python

"""Versioned runtime operation contracts for the Authoring compiler."""
from __future__ import annotations
from hashlib import sha256
import json
from typing import Any
from jsonschema import Draft202012Validator
from jsonschema.exceptions import SchemaError
class OperationContractError(ValueError):
pass
def canonical_hash(value: dict[str, Any]) -> str:
return sha256(json.dumps(value, ensure_ascii=True, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()
def _is_closed(schema: Any) -> bool:
if not isinstance(schema, dict):
return False
if schema.get("type") == "object" and schema.get("additionalProperties") is not False:
return False
for name in ("properties", "$defs", "definitions"):
values = schema.get(name)
if isinstance(values, dict) and not all(_is_closed(value) for value in values.values()):
return False
for name in ("items", "additionalItems"):
value = schema.get(name)
if isinstance(value, dict) and not _is_closed(value):
return False
for name in ("oneOf", "anyOf", "allOf"):
values = schema.get(name)
if values is not None and (not isinstance(values, list) or not all(_is_closed(value) for value in values)):
return False
return True
def is_authoring_schema_closed(schema: Any) -> bool:
"""Whether an operation can safely be exposed in the Authoring whitelist."""
return _is_closed(schema)
def validate_operation_contract(contract: dict[str, Any]) -> None:
required = {
"atomic_id", "contract_version", "fragment_shape", "author_params_schema", "selector_policy",
"server_injected_paths", "reference_policy", "semantic_preflight", "candidate_verifiers",
"runtime_capability",
}
if not required.issubset(contract) or set(contract) - required - {"contract_hash", "registry_revision"}:
raise OperationContractError("Operation contract has unknown or missing fields")
if not isinstance(contract["atomic_id"], str) or not contract["atomic_id"]:
raise OperationContractError("Operation contract atomic_id is invalid")
if not isinstance(contract["contract_version"], str) or not contract["contract_version"]:
raise OperationContractError("Operation contract version is missing")
shape = contract["fragment_shape"]
if not isinstance(shape, dict) or set(shape) != {"sketch", "params", "selector_tokens"}:
raise OperationContractError("Operation contract fragment_shape is invalid")
if shape["sketch"] not in {"required", "forbidden"} or shape["params"] != "required_object" or shape["selector_tokens"] not in {"required", "forbidden"}:
raise OperationContractError("Operation contract fragment_shape is unsupported")
params = contract["author_params_schema"]
if not _is_closed(params):
raise OperationContractError("Operation author_params_schema must recursively close all objects")
try:
Draft202012Validator.check_schema(params)
except SchemaError as error:
raise OperationContractError("Operation author_params_schema is invalid JSON Schema") from error
selector = contract["selector_policy"]
if not isinstance(selector, dict) or set(selector) != {"slot", "token_kind", "min_items", "max_items", "snapshot_bound"}:
raise OperationContractError("Operation selector_policy is invalid")
if shape["selector_tokens"] == "forbidden":
if any(selector[key] not in {None, 0, False, ""} for key in selector):
raise OperationContractError("Selector policy must be empty when selectors are forbidden")
else:
if not isinstance(selector["slot"], str) or not selector["slot"] or selector["token_kind"] not in {"face", "edge", "plane", "axis", "body"}:
raise OperationContractError("Selector policy has no concrete slot/token kind")
if not isinstance(selector["min_items"], int) or not isinstance(selector["max_items"], int) or not 1 <= selector["min_items"] <= selector["max_items"] <= 64 or selector["snapshot_bound"] is not True:
raise OperationContractError("Selector policy bounds are invalid")
if not isinstance(contract["server_injected_paths"], list) or not all(isinstance(item, str) and item for item in contract["server_injected_paths"]):
raise OperationContractError("Operation server injection paths are invalid")
injected = contract["server_injected_paths"]
expected_injected = [selector["slot"]] if shape["selector_tokens"] == "required" else []
if injected != expected_injected:
raise OperationContractError("Operation selector policy and server injection paths disagree")
if shape["selector_tokens"] == "required" and selector["slot"].startswith("params."):
injected_param = selector["slot"].removeprefix("params.")
properties = params.get("properties") if isinstance(params.get("properties"), dict) else {}
if injected_param in properties:
raise OperationContractError("Server-injected selector must not be author-owned")
reference = contract["reference_policy"]
if not isinstance(reference, dict) or reference.get("mode") not in {"none", "snapshot_bound"}:
raise OperationContractError("Operation reference policy is invalid")
if reference["mode"] == "none" and set(reference) != {"mode"}:
raise OperationContractError("Reference policy must be empty when references are forbidden")
if reference["mode"] == "snapshot_bound":
if set(reference) != {"mode", "slot", "token_kind", "min_items", "max_items", "snapshot_bound"}:
raise OperationContractError("Snapshot-bound reference policy is invalid")
slot = reference["slot"]
if (
not isinstance(slot, str)
or (slot != "feature.selectors" and not slot.startswith("params."))
or reference["token_kind"] not in {"face", "edge", "plane", "axis", "body", "feature"}
or not isinstance(reference["min_items"], int)
or not isinstance(reference["max_items"], int)
or not 0 <= reference["min_items"] <= reference["max_items"] <= 64
or reference["snapshot_bound"] is not True
):
raise OperationContractError("Snapshot-bound reference policy has invalid bounds")
if slot == "feature.selectors":
if shape["selector_tokens"] != "required" or selector["token_kind"] != reference["token_kind"]:
raise OperationContractError("Feature selector reference policy disagrees with selector policy")
else:
reference_name = slot.removeprefix("params.")
properties = params.get("properties") if isinstance(params.get("properties"), dict) else {}
reference_schema = properties.get(reference_name)
if not isinstance(reference_schema, dict) or reference_schema.get("type") not in {"array", "string"}:
raise OperationContractError("Snapshot-bound reference slot is absent from author params schema")
if reference_schema.get("type") == "string" and reference["max_items"] > 1:
raise OperationContractError("Scalar snapshot-bound reference must allow at most one item")
if not isinstance(contract["semantic_preflight"], list) or not all(isinstance(item, str) and item for item in contract["semantic_preflight"]):
raise OperationContractError("Operation semantic preflight is invalid")
if len(set(contract["semantic_preflight"])) != len(contract["semantic_preflight"]):
raise OperationContractError("Operation semantic preflight names are duplicated")
if not isinstance(contract["candidate_verifiers"], list) or not all(isinstance(item, str) and item for item in contract["candidate_verifiers"]):
raise OperationContractError("Operation candidate_verifiers are invalid")
if len(set(contract["candidate_verifiers"])) != len(contract["candidate_verifiers"]):
raise OperationContractError("Operation candidate verifiers are duplicated")
runtime_capability = contract["runtime_capability"]
required_capability_flags = {
"body_mutating", "requires_active_body", "replayable", "requires_selector", "open_profile_ok",
}
if (
not isinstance(runtime_capability, dict)
or set(runtime_capability) != required_capability_flags
or not all(isinstance(value, bool) for value in runtime_capability.values())
):
raise OperationContractError("Operation runtime capability is invalid")