Ctrl K
ComunidadGuías…

scripts/check-broker-sdk-licences.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env python3
"""Verify every broker SDK in brokers.lock declares a licence in the AGPL allowlist.

Reads brokers.lock; for each entry, asserts the declared SPDX licence is in
supply-chain/licence-allowlist.yml. When the wheel is installed in the current
environment, cross-checks the wheel metadata licence against the declared value.

Fails on:
  - brokers.lock declares a licence in the denylist
  - brokers.lock declares a licence not in the allowlist
  - installed wheel metadata declares a licence not in the allowlist that also
    disagrees with brokers.lock

PLACEHOLDER entries (future-wave brokers not yet activated) are skipped — they are
gated separately by scripts/check-brokers-lock.py and runtime SDK attestation.

Sub-spec §7.4; acceptance gate #10.
"""

from __future__ import annotations

import importlib.metadata as md
import pathlib
import sys
import tomllib

import yaml

REPO = pathlib.Path(__file__).resolve().parents[1]
BROKERS_LOCK = REPO / "brokers.lock"
ALLOWLIST_FILE = REPO / "supply-chain" / "licence-allowlist.yml"

# Wheel `License` metadata is overwhelmingly free-text, not an SPDX identifier
# (e.g. dhanhq ships "MIT License"). Normalise the common forms to their SPDX id so
# the cross-check compares like with like instead of failing on cosmetic differences.
_FREE_TEXT_TO_SPDX = {
    "mit": "MIT",
    "mit license": "MIT",
    "bsd": "BSD-3-Clause",
    "bsd license": "BSD-3-Clause",
    "apache 2.0": "Apache-2.0",
    "apache-2.0": "Apache-2.0",
    "apache software license": "Apache-2.0",
    "apache license 2.0": "Apache-2.0",
    "the apache software license, version 2.0": "Apache-2.0",
    "mozilla public license 2.0 (mpl 2.0)": "MPL-2.0",
    "isc": "ISC",
    "isc license (iscl)": "ISC",
    "python software foundation license": "Python-2.0",
    "gnu general public license v3 or later (gplv3+)": "GPL-3.0-or-later",
    "gnu affero general public license v3 or later (agpl3+)": "AGPL-3.0-or-later",
}


def _normalise_spdx(raw: str) -> str:
    s = (raw or "").strip()
    return _FREE_TEXT_TO_SPDX.get(s.lower(), s)


def _extract_metadata_licence(name: str) -> str | None:
    """Return the SPDX 'License-Expression' or normalised 'License' field, or None."""
    try:
        meta = md.metadata(name)
    except md.PackageNotFoundError:
        return None
    raw = (meta.get("License-Expression") or meta.get("License") or "").strip()
    return _normalise_spdx(raw) or None


def _is_distribution_installed(name: str) -> bool:
    try:
        md.version(name)
    except md.PackageNotFoundError:
        return False
    return True


def _is_placeholder_entry(entry: dict[str, object]) -> bool:
    """True when an inactive broker wave has not populated its full SDK pin."""
    fields = (
        str(entry.get("version", "")),
        str(entry.get("sha256", "")),
        str(entry.get("licence", "")),
    )
    return any("PLACEHOLDER" in field.upper() for field in fields)


def main() -> int:
    if not BROKERS_LOCK.exists():
        print(f"brokers.lock missing at {BROKERS_LOCK}", file=sys.stderr)
        return 1
    if not ALLOWLIST_FILE.exists():
        print(f"licence allowlist missing at {ALLOWLIST_FILE}", file=sys.stderr)
        return 1

    allow = yaml.safe_load(ALLOWLIST_FILE.read_text(encoding="utf-8")) or {}
    allowed = set(allow.get("allowlist") or [])
    deny = set(allow.get("denylist") or [])
    review = set(allow.get("classification_review") or [])

    data = tomllib.loads(BROKERS_LOCK.read_text(encoding="utf-8"))
    fails: list[str] = []
    checked = 0
    for entry in data.get("broker", []):
        name = entry["name"]
        declared = (entry.get("licence", "") or "").strip()
        if _is_placeholder_entry(entry) or not declared:
            # future-wave broker not yet activated; check-brokers-lock.py gates these
            continue
        checked += 1
        if declared in deny:
            fails.append(f"{name}: brokers.lock declares FORBIDDEN licence {declared!r}")
            continue
        if declared not in allowed:
            if declared in review:
                print(f"REVIEW: {name} declares {declared!r} (needs founder classification)")
                continue
            fails.append(
                f"{name}: brokers.lock declares {declared!r} which is not in the allowlist; "
                f"add it to supply-chain/licence-allowlist.yml with founder approval, "
                f"or use a different SDK"
            )
            continue
        installed = _extract_metadata_licence(name)
        if installed is None:
            if _is_distribution_installed(name):
                print(f"NOTE: {name} installed but has no wheel licence metadata; relying on brokers.lock")
            else:
                print(f"NOTE: {name} not installed here; skipping wheel-metadata cross-check")
            continue
        if installed != declared and installed not in allowed:
            fails.append(
                f"{name}: installed wheel declares {installed!r} but brokers.lock has "
                f"{declared!r}; the installed value is not in the allowlist either"
            )

    if fails:
        for f in fails:
            print(f"FAIL: {f}", file=sys.stderr)
        return 1
    print(f"broker SDK licences OK ({checked} active entries checked)")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())