Keep offline package verification reproducible with cold caches (U5)

Why:
An immutable npm lock caches tarballs but packed consumers also require resolver metadata before candidate execution.

Evidence:
SELF host replay passed 57 composition and CI credential cases; real npm 10 and 11 cold-cache consumers passed without lock changes.

Impact:
Acquire only installed exact-version npmjs metadata with clean config, preserve cache paths for offline execution, adopt the repaired Frontend source and record immutable CI fixtures.

Refs:
R6-R8, R14; dependency-workspace U4/U5; UI 63d289e and Frontend 5467517
This commit is contained in:
2026-10-14 01:44:49 +00:00
parent 222687f149
commit 01b7799663
8 changed files with 364 additions and 18 deletions
+45
View File
@@ -25,6 +25,8 @@ SHA = re.compile(r"[0-9a-f]{40}\Z")
IDS = re.compile(r"[A-Za-z0-9][A-Za-z0-9_-]*\Z")
BACKEND = "Agent-System-Backend"
FRONTEND = "Agent-System-Frontend"
NPM_NAME = re.compile(r"(?:@[a-z0-9][a-z0-9._~-]*/)?[a-z0-9][a-z0-9._~-]*\Z")
NPM_VERSION = re.compile(r"[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?\Z")
class CompositionError(Exception):
@@ -165,6 +167,11 @@ def execution_environment(env):
or upper in {"SSH_AUTH_SOCK", "SSH_AGENT_PID", "NODE_OPTIONS", "BASH_ENV", "ENV", "GOFLAGS", "GIT_ASKPASS", "SSH_ASKPASS", "CONTRACTS_TARBALL", "UI_TARBALL"}):
del clean[key]
clean.update(GOWORK="off", GOTOOLCHAIN="local", GOFLAGS="", CGO_ENABLED="0")
# Cache location is transport-independent state, shared by acquisition and
# offline consumers. Other npm settings can carry credentials or hooks.
for key in ("npm_config_cache", "NPM_CONFIG_CACHE"):
if env.get(key):
clean[key] = env[key]
return clean
@@ -251,6 +258,24 @@ def go_dependencies(source, fetch_env):
"downloads": [{key: value[key] for key in ("Path", "Version", "Sum", "GoModSum", "Origin") if key in value} for value in downloads]}
def npm_registry_package(location, package):
resolved = package.get("resolved", "")
if not isinstance(resolved, str):
raise CompositionError("invalid npm lock dependency")
if not resolved.startswith(("https://", "http://")):
return None
registry = urlsplit(resolved)
if (registry.scheme != "https" or registry.netloc != "registry.npmjs.org" or
registry.username or registry.password or registry.query or registry.fragment):
raise CompositionError("npm tarball source is outside the reviewed public registry")
name = package.get("name") or location.rsplit("node_modules/", 1)[-1]
version = package.get("version", "")
if (not isinstance(name, str) or not NPM_NAME.fullmatch(name) or
not isinstance(version, str) or not NPM_VERSION.fullmatch(version)):
raise CompositionError("invalid locked registry package name or version")
return name + "@" + version
def npm_dependencies(source, components, fetch_env):
path = source / "package-lock.json"
lock = json.loads(path.read_text())
@@ -258,7 +283,13 @@ def npm_dependencies(source, components, fetch_env):
raise CompositionError("frontend requires a complete npm lock")
approved = {component["repo"] for component in components.values()}
packages = {}
registry_packages = []
for name, package in lock["packages"].items():
if name:
safe_relative(source, name)
registry_package = npm_registry_package(name, package)
if registry_package:
registry_packages.append((name, registry_package))
if package.get("link"):
raise CompositionError("formal npm build forbids workspace links")
for value in [package.get("resolved", ""), *package.get("dependencies", {}).values()]:
@@ -273,6 +304,20 @@ def npm_dependencies(source, components, fetch_env):
packages[name] = {key: package[key] for key in ("version", "resolved", "integrity") if key in package}
before = digest(path)
run(["npm", "ci", "--ignore-scripts", "--no-audit", "--no-fund"], source, fetch_env)
# npm ci normally stores tarballs without the packuments required when a
# disposable packed-package consumer re-resolves peers offline. Fetch the
# installed locked versions before candidate execution, from a fixed public
# registry with empty config and no acquisition credentials or candidate rc.
metadata_env = execution_environment(fetch_env)
with tempfile.TemporaryDirectory(prefix="composition-npm-metadata-") as directory:
isolated = Path(directory)
global_config = isolated / "empty-global.npmrc"
global_config.write_text("")
selected = {spec for location, spec in registry_packages if (source / location).is_dir()}
if selected:
run(["npm", "cache", "add", *sorted(selected), "--ignore-scripts",
"--registry=https://registry.npmjs.org", "--userconfig=/dev/null",
"--globalconfig=" + str(global_config)], isolated, metadata_env)
if digest(path) != before:
raise CompositionError("npm acquisition changed committed lock")
result = {"lock_sha256": before, "packages": packages}