R-880: build-step-bundle.py — the transition bundle for a release whose bundle adds paths (an installed felhom-os-apply refuses unknown paths, R16)
gates / gates (push) Successful in 21s
gates / gates (push) Successful in 21s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env python3
|
||||
"""build-step-bundle.py — the TRANSITION bundle for a release whose bundle ADDS a path (R-880, `11` §5.4.2).
|
||||
|
||||
Usage: python3 scripts/build-step-bundle.py <base-bundle.json> <step-version> <out.json> (prints the sha256)
|
||||
|
||||
WHY. A box's INSTALLED felhom-os-apply checks every path of an incoming bundle against ITS OWN table (rule R16) — so a
|
||||
bundle that adds a path (v0.146.1 adds felhom-priv-apply, the guest hook and the shared-parent files, R-861) is refused
|
||||
by every box still running an older wrapper. The fix is a step: first a bundle the old wrapper accepts that brings ONLY
|
||||
the new felhom-os-apply (the new table), then the release's own bundle, which the new wrapper accepts.
|
||||
|
||||
WHAT IT BUILDS. <base-bundle.json> is the bundle the boxes run now (download it from the package registry, e.g.
|
||||
felhom-agent/0.145.0/felhom-config-bundle.json, and check its sha against the hub's record). The step bundle is that
|
||||
bundle with EXACTLY ONE change: the /usr/local/sbin/felhom-os-apply entry's content is replaced by configs/felhom-os-apply
|
||||
(this tree). Same paths, same modes, same checks, every other byte identical; agent_version is <step-version> (e.g.
|
||||
0.146.1-step1). The old wrapper verifies it like any bundle (signature, sha, R16, content checks, self-check of the new
|
||||
wrapper) — nothing about the trust route changes.
|
||||
|
||||
Pinned by configs/test_felhom_config_bundle.py (StepBundle): same paths as the base, only the wrapper differs, the
|
||||
new wrapper's table is a superset of the base's paths.
|
||||
"""
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
REPO = pathlib.Path(__file__).resolve().parent.parent
|
||||
OSAPPLY_DEST = "/usr/local/sbin/felhom-os-apply"
|
||||
|
||||
|
||||
def build_step(base_bytes, version, new_osapply_bytes):
|
||||
if not re.match(r"^[0-9]+\.[0-9]+\.[0-9]+-[0-9A-Za-z.]+$", version):
|
||||
raise SystemExit(f"build-step-bundle: {version!r} must be a semver with a step suffix, e.g. 0.146.1-step1")
|
||||
base = json.loads(base_bytes)
|
||||
files = base.get("files")
|
||||
if base.get("format") != 1 or not isinstance(files, list):
|
||||
raise SystemExit("build-step-bundle: the base is not a format-1 bundle")
|
||||
hit = [e for e in files if e.get("path") == OSAPPLY_DEST]
|
||||
if len(hit) != 1:
|
||||
raise SystemExit(f"build-step-bundle: the base has {len(hit)} {OSAPPLY_DEST} entries, want exactly 1")
|
||||
hit[0]["content_b64"] = base64.b64encode(new_osapply_bytes).decode()
|
||||
hit[0]["sha256"] = hashlib.sha256(new_osapply_bytes).hexdigest()
|
||||
base["agent_version"] = version
|
||||
return (json.dumps(base, indent=1, sort_keys=True) + "\n").encode()
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) != 4:
|
||||
print(__doc__, file=sys.stderr)
|
||||
return 2
|
||||
data = build_step(pathlib.Path(argv[1]).read_bytes(), argv[2], (REPO / "configs" / "felhom-os-apply").read_bytes())
|
||||
pathlib.Path(argv[3]).write_bytes(data)
|
||||
print(hashlib.sha256(data).hexdigest())
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
Reference in New Issue
Block a user