c9fa2e717b
gates / gates (push) Successful in 18s
felhom-os-apply gains mode 'bundle' (signed, verified by the wrapper itself against the root-owned signers file — or, when that file is missing, only the installer's pinned key, which it then creates) and --install-bundle (the installer's root entry). BUNDLE_FILES is the one table of paths; every check (visudo, sh/bash -n, python, unit sections, RuntimeDirectory guard, nft -c, the route itself) runs before the first write; a failed write or self-check puts every previous copy back. The trust root is never a bundle path (R17). scripts/build-config-bundle.py builds it reproducibly; release-agent.sh publishes it beside the binary. The agent reports the bundle record in system.config_bundle. felhom-opsign signs agent_config_update. 43 wrapper tests (22 mutants red), Go executor tests. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
61 lines
2.4 KiB
Python
61 lines
2.4 KiB
Python
#!/usr/bin/env python3
|
|
"""build-config-bundle.py — build the agent's CONFIG BUNDLE (R-840, `11` §5.4.2).
|
|
|
|
Usage: python3 scripts/build-config-bundle.py <agent-version> <out.json> (prints the bundle's sha256)
|
|
|
|
The bundle is every root-owned file the installer's step 5 writes for the agent (sudoers, wrappers, units), as ONE
|
|
JSON file published beside the binary (felhom-agent/<version>/felhom-config-bundle.json). A box takes it by a signed
|
|
`agent_config_update` job; a new box takes the SAME file from the installer. The list of files is NOT kept here: it is
|
|
`BUNDLE_FILES` in configs/felhom-os-apply, the root wrapper that installs it — one table, so the builder cannot put in a
|
|
path the wrapper would refuse, nor leave out one it expects.
|
|
|
|
Reproducible by construction: no timestamps, sorted keys, the table's order. The same source at the same version gives
|
|
the same sha256 every time (pinned by configs/test_felhom_config_bundle.py).
|
|
"""
|
|
import base64
|
|
import hashlib
|
|
import importlib.machinery
|
|
import importlib.util
|
|
import json
|
|
import pathlib
|
|
import re
|
|
import sys
|
|
|
|
REPO = pathlib.Path(__file__).resolve().parent.parent
|
|
CONFIGS = REPO / "configs"
|
|
|
|
|
|
def load_wrapper(configs=CONFIGS):
|
|
loader = importlib.machinery.SourceFileLoader("osapply_for_bundle", str(configs / "felhom-os-apply"))
|
|
spec = importlib.util.spec_from_loader("osapply_for_bundle", loader)
|
|
mod = importlib.util.module_from_spec(spec)
|
|
loader.exec_module(mod)
|
|
return mod
|
|
|
|
|
|
def build(version, configs=CONFIGS):
|
|
if not re.match(r"^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$", version):
|
|
raise SystemExit(f"build-config-bundle: version {version!r} is not semver")
|
|
w = load_wrapper(configs)
|
|
files = []
|
|
for dest, src, mode, check, policy in w.BUNDLE_FILES:
|
|
data = (configs / src).read_bytes()
|
|
files.append({"path": dest, "source": f"configs/{src}", "mode": oct(mode), "check": check, "policy": policy,
|
|
"sha256": hashlib.sha256(data).hexdigest(), "content_b64": base64.b64encode(data).decode()})
|
|
body = {"format": w.BUNDLE_FORMAT, "agent_version": version, "files": files}
|
|
return (json.dumps(body, indent=1, sort_keys=True) + "\n").encode()
|
|
|
|
|
|
def main(argv):
|
|
if len(argv) != 3:
|
|
print(__doc__, file=sys.stderr)
|
|
return 2
|
|
data = build(argv[1])
|
|
pathlib.Path(argv[2]).write_bytes(data)
|
|
print(hashlib.sha256(data).hexdigest())
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv))
|