← Files Aivana Dataverse App ArchitectARCHIVED FILE

scripts/execution_manifest.py

4.28 KB · Sep 30, 2026 · 23:15 UTC

↓ Download file

"""One fail-closed contract for the CLI and MCP execution paths."""
import hashlib
import json
import re
from pathlib import Path
from urllib.parse import urlsplit
from blueprint_schema import load_blueprint

IDENTIFIER = r"[A-Za-z][A-Za-z0-9_]*"
ENTITY = rf"EntityDefinitions\(LogicalName='{IDENTIFIER}'\)"
ATTRIBUTE = rf"{ENTITY}/Attributes\(LogicalName='{IDENTIFIER}'\)"
RELATIONSHIP = rf"RelationshipDefinitions\(SchemaName='{IDENTIFIER}'\)"
PATHS = {
    "POST": [r"EntityDefinitions", rf"{ENTITY}/Attributes", r"RelationshipDefinitions"],
    "PATCH": [ENTITY, ATTRIBUTE, RELATIONSHIP],
}


def normalized_url(value):
    if not isinstance(value, str):
        raise ValueError("targetUrl must be an HTTPS origin")
    parsed = urlsplit(value)
    if (parsed.scheme != "https" or not parsed.hostname or not re.fullmatch(r"[A-Za-z0-9.-]+", parsed.hostname) or parsed.username or parsed.password
            or parsed.query or parsed.fragment or parsed.path not in ("", "/") or parsed.port not in (None, 443)):
        raise ValueError("targetUrl must be an HTTPS origin without credentials, path, query or custom port")
    return f"https://{parsed.hostname.lower()}"


def valid_solution(value):
    return (isinstance(value, str) and re.fullmatch(IDENTIFIER, value) is not None
            and value.lower() not in {"default", "activesolution", "active", "system"})


def operation_issues(operations):
    if not isinstance(operations, list) or not operations:
        return ["operations must be a non-empty list"]
    issues = []
    for index, op in enumerate(operations):
        prefix = f"operations[{index}]"
        if not isinstance(op, dict):
            issues.append(f"{prefix} must be an object")
            continue
        method, path = op.get("method"), op.get("path")
        if (op.get("type") != "webApiMetadata" or not isinstance(method, str)
                or not isinstance(path, str) or not any(re.fullmatch(p, path) for p in PATHS.get(method, []))):
            issues.append(f"{prefix}: unsupported metadata method/path")
        if not isinstance(op.get("payload"), dict) or not op["payload"]:
            issues.append(f"{prefix}: non-empty payload object required")
        elif method == "POST" and not re.fullmatch(IDENTIFIER, str(op["payload"].get("SchemaName", ""))):
            issues.append(f"{prefix}: POST requires SchemaName")
    return issues


def load_manifest(path):
    file = Path(path).resolve()
    raw = file.read_bytes()
    digest = hashlib.sha256(raw).hexdigest()
    data = json.loads(raw.decode("utf-8-sig"))
    if not isinstance(data, dict):
        return {}, ["manifest must be an object"], digest
    issues = []
    for key in ("approvedBy", "changeId", "blueprintPath"):
        if not isinstance(data.get(key), str) or not data[key].strip():
            issues.append(f"{key} must be a non-empty string")
    try:
        target = normalized_url(data.get("targetUrl"))
    except ValueError as exc:
        issues.append(str(exc))
        target = None
    if not valid_solution(data.get("solutionUniqueName")):
        issues.append("solutionUniqueName must identify a custom solution; Default/System/Active forbidden")
    if data.get("allowDefaultSolution", False) is not False:
        issues.append("allowDefaultSolution must be false")
    if type(data.get("writeApproved", False)) is not bool:
        issues.append("writeApproved must be boolean")
    issues.extend(operation_issues(data.get("operations")))
    if isinstance(data.get("blueprintPath"), str) and data["blueprintPath"]:
        blueprint_path = file.parent / data["blueprintPath"]
        try:
            bp = load_blueprint(blueprint_path)
            actual_hash = hashlib.sha256(blueprint_path.read_bytes()).hexdigest()
            if data.get("blueprintHash") != actual_hash:
                issues.append("blueprintHash missing or different from blueprint bytes")
            if bp.get("solution", {}).get("uniqueName") != data.get("solutionUniqueName"):
                issues.append("blueprint solution differs from manifest")
            if normalized_url(bp.get("solution", {}).get("environment")) != target:
                issues.append("blueprint environment differs from manifest")
        except (OSError, ValueError, TypeError) as exc:
            issues.append(f"blueprint invalid: {exc}")
    return data, issues, digest

SHA-256: d12651a6bf3726979e686eb0a23a53430787194cd28380329959ad7aaac777d3