mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-25 06:33:13 +08:00
The v0.12.0 release run's license gate blocked @img/sharp-wasm32@0.35.5 (Apache-2.0 AND LGPL-3.0-or-later AND MIT). sharp sat in graph-ui's tree only because @huggingface/transformers lists it for its Node image path. CBM Lens runs Transformers in the browser (a tokenizer and a causal LM on WebGPU), never processes images, and the Transformers web build already replaces sharp with an empty object. Yet sharp installed 14 LGPL packages (10 @img/sharp-libvips-*, 3 @img/sharp-win32-*, @img/sharp-wasm32), and the gate let them through two ways: - a reviewed exception for exactly @img/sharp-wasm32@0.35.0 ("installed but not shipped"), which the 0.35.5 bump in #2632 tripped; - collect() skipped every os/cpu-restricted package as "build tooling", which hid all ten LGPL libvips binaries. A non-permissive license has no place in the tree, shipped or not: - sharp now resolves to graph-ui/stubs/sharp, our own MIT stand-in that throws if anything ever calls it (direct dependency file:./stubs/sharp plus override "sharp": "$sharp"). The whole sharp family leaves the lockfile: 441 -> 411 packages, zero LGPL/GPL left (audited). - ui_browser_licenses.py loses the exception and keeps only the browser build evidence check; license-gate-check-npm.py validates that evidence unconditionally, checks platform-restricted packages like any other, and has no exclusion path; gen-ui-licenses.py drops the exception section. - tests: the exception tests become guards: an unshipped LGPL package blocks, a platform-restricted LGPL package blocks, a platform-restricted MIT package passes. Against the old scripts both LGPL guards fail. The shipped browser bundle is unchanged: rebuilt, it differs from main's only by the build's own -dirty version marker. License gate stage 3 on this tree: 211 production packages, all allow-listed. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
102 lines
5.1 KiB
Python
102 lines
5.1 KiB
Python
"""Build evidence for the UI browser pack: what ships is exactly the bundler output.
|
|
|
|
There are NO license exceptions. Every package in the production dependency
|
|
tree must carry an allow-listed license whether or not it reaches the bundle
|
|
(license-gate-check-npm.py); this module only proves the bundle itself: every
|
|
asset is inventoried bundler output, and neither sharp nor Node inference code
|
|
is in the app or worker graph. Missing evidence, new payloads or stale builds
|
|
fail closed.
|
|
"""
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path, PurePosixPath
|
|
import re
|
|
import sys
|
|
|
|
|
|
BROWSER_ENTRY = "node_modules/@huggingface/transformers/dist/transformers.web.js"
|
|
# Transformers 4.2.0's published browser distribution contains the upstream
|
|
# empty `ignore-modules:sharp` stub. Updating it requires a new source review.
|
|
BROWSER_SHA256 = "25e0cbdf5df922996299fcd2cf835101ba979b134389a0dcc54f92022ca7e0ff"
|
|
SHARP_INPUT = re.compile(r"(^|[/\\])(sharp|@img[/\\]sharp[^/\\?]*)([/\\?]|$)")
|
|
|
|
|
|
def sha256(path):
|
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
|
|
|
|
def checked_path(root, name):
|
|
path = PurePosixPath(name)
|
|
if not name or path.is_absolute() or ".." in path.parts or "\\" in name or ":" in name:
|
|
raise ValueError("invalid path in browser build evidence")
|
|
return root.joinpath(*path.parts)
|
|
|
|
|
|
def validate_browser_build(ui_dir):
|
|
"""Raise ValueError unless the current app + worker build evidence holds."""
|
|
root = Path(ui_dir)
|
|
try:
|
|
proof = json.loads((root / ".vite/license-build.json").read_text())
|
|
if proof["version"] != 1 or proof["worker_builds"] < 1:
|
|
raise ValueError("app and worker build inventories are required")
|
|
if proof["lock_sha256"] != sha256(root / "package-lock.json"):
|
|
raise ValueError("browser build lockfile changed")
|
|
inputs, outputs = proof["inputs"], proof["outputs"]
|
|
if not inputs or not outputs:
|
|
raise ValueError("empty browser build inventory")
|
|
for name, expected in inputs.items():
|
|
if sha256(checked_path(root, name)) != expected:
|
|
raise ValueError(f"browser build input changed: {name}")
|
|
for name in [*proof["modules"], *proof["externals"], *inputs]:
|
|
if SHARP_INPUT.search(name.replace("\0", "/")):
|
|
raise ValueError("Sharp code appears in the browser build graph")
|
|
if "transformers.node." in name or "onnxruntime-node" in name:
|
|
raise ValueError("Node inference code appears in the browser build graph")
|
|
# A browser build must resolve this reviewed export, including in the
|
|
# inference worker. Do not infer it from npm's dependency declarations.
|
|
meta = json.loads((root / "node_modules/@huggingface/transformers/package.json").read_text())
|
|
if (meta["version"] != "4.2.0" or
|
|
meta["exports"]["default"]["default"] != "./dist/transformers.web.js" or
|
|
BROWSER_ENTRY not in proof["modules"] or
|
|
inputs.get(BROWSER_ENTRY) != BROWSER_SHA256):
|
|
raise ValueError("Transformers browser export needs a new license review")
|
|
dist = root / "dist"
|
|
actual = {p.relative_to(dist).as_posix() for p in dist.rglob("*") if p.is_file()}
|
|
if actual != set(outputs):
|
|
raise ValueError("browser build asset inventory changed")
|
|
for name, expected in outputs.items():
|
|
path = checked_path(dist, name)
|
|
if sha256(path) != expected:
|
|
raise ValueError(f"browser build output changed: {name}")
|
|
suffix = path.suffix.lower()
|
|
# Hashing a public copy does not make it a bundler output. Bind
|
|
# generated HTML/CSS to bytes observed in the Rollup output hook.
|
|
generated = proof["generated_assets"].get(name)
|
|
if suffix == ".css" and generated == expected:
|
|
continue
|
|
if (name == "index.html" and generated == expected and
|
|
"index.html" in inputs and "index.html" in proof["modules"]):
|
|
continue
|
|
if suffix in {".js", ".mjs"} and name in proof["chunks"]:
|
|
continue
|
|
# ONNX ships a prebuilt runtime module and Wasm via Vite URL
|
|
# assets. Accept only an exact copy of the inventoried package
|
|
# input, never a filename or extension as evidence of provenance.
|
|
source = proof["runtime_inputs"].get(name, "")
|
|
if (suffix in {".wasm", ".js", ".mjs"} and
|
|
source.startswith("node_modules/onnxruntime-web/dist/") and
|
|
inputs.get(source) == expected):
|
|
continue
|
|
raise ValueError(f"browser asset has unknown provenance: {name}")
|
|
except (OSError, KeyError, TypeError, json.JSONDecodeError) as exc:
|
|
raise ValueError("browser build evidence is missing or incomplete; run npm run build") from exc
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
validate_browser_build(sys.argv[1])
|
|
except ValueError as error:
|
|
print(f"BLOCKED: {error}", file=sys.stderr)
|
|
sys.exit(1)
|
|
print("OK: current application, worker, and copied-runtime build evidence verified")
|