scripts/check-broker-sdk-licences.py
Rama main · réplica de lectura del repositorio original.
#!/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())