Files
aios/scripts/autonomous-release/build_trusted_release_artifact.py
T
AIOS Governed Continuation 0d9d0479bf
autonomy-smoke-tests V2R2C2 release-runtime binding smoke and offline package preflight PASS
autonomy-unit-tests 782 passed, 3 skipped, 154 subtests passed on source branch
git-diff-check governed source diff-check PASS against merged main f1695ce
py-compile all tracked Python source compile PASS
reconciliation-contract completed-replay reconciliation contract preserved; 11 focused tests PASS
secret-scan narrow source and deployable payload secret scan PASS; no credentials included
signer-validator-contract trusted release plan/adapter contract 48 tests PASS; formal Owner signature not asserted here
campaign-approval-gate V2R2C2 candidate binding contract PASS; production approval remains formal-envelope gated
release-state-machine V2R2C2 plan-driven release state contract PASS; no production mutation performed
feat(v2r2c2): bind trusted release runtime and deterministic artifact
2026-08-29 09:37:46 +08:00

91 lines
6.1 KiB
Python

#!/usr/bin/env python3
"""Build a deterministic plan-bound production payload from one clean source tree."""
from __future__ import annotations
import argparse, hashlib, json, shutil, tarfile, tempfile
from pathlib import Path
SOURCE_FILES = (
"active-runtime/app/deployment_state.py",
"active-runtime/app/v2r1c6_privacy_tools.py",
"active-runtime/app/v2r2c2_health_service.py",
"deploy/aios-v2/systemd/aios-v2r2c2-production.service",
)
def canon(value: object) -> bytes:
return (json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode()
def digest(path: Path) -> str:
h = hashlib.sha256()
with path.open("rb") as f:
for block in iter(lambda: f.read(1024 * 1024), b""): h.update(block)
return h.hexdigest()
def require_oid(value: str, label: str) -> str:
if len(value) != 40 or any(c not in "0123456789abcdef" for c in value): raise SystemExit(label + "_invalid")
return value
def require_sha(value: str, label: str) -> str:
if len(value) != 64 or any(c not in "0123456789abcdef" for c in value): raise SystemExit(label + "_invalid")
return value
def build(source: Path, output: Path, *, version: str, source_oid: str, tree_oid: str,
rollback_version: str, rollback_target_oid: str, rollback_artifact_sha256: str,
service: str, port: int) -> dict[str, object]:
if not source.is_dir() or source.is_symlink(): raise SystemExit("source_directory_invalid")
require_oid(source_oid, "source_oid"); require_oid(tree_oid, "tree_oid")
require_oid(rollback_target_oid, "rollback_target_oid"); require_sha(rollback_artifact_sha256, "rollback_artifact_sha256")
if version != "V2R2C2" or rollback_version == version: raise SystemExit("release_version_binding_invalid")
if service != "aios-v2r2c2-production.service" or port != 18928: raise SystemExit("adapter_policy_binding_invalid")
if output.exists(): raise SystemExit("output_exists")
output.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix="trusted-release-build-") as td:
payload = Path(td) / "payload"; payload.mkdir()
for rel in SOURCE_FILES:
source_file = source / rel
if not source_file.is_file() or source_file.is_symlink(): raise SystemExit("required_source_missing:" + rel)
dest = payload / rel; dest.parent.mkdir(parents=True, exist_ok=True); shutil.copyfile(source_file, dest)
(payload / "VERSION").write_text(version + "\n", encoding="utf-8")
build_meta = {"schema": "aios.trusted-release-artifact-build.v1", "version": version,
"source_commit": source_oid, "tree_oid": tree_oid, "production_deployed": False,
"tag_created": False, "baseline_changed": False, "adapter_service": service, "adapter_port": port}
(payload / "BUILD-METADATA.json").write_bytes(canon(build_meta))
rollback = {"schema": "aios.trusted-release-rollback.v1", "version": rollback_version,
"target_oid": rollback_target_oid, "artifact_sha256": rollback_artifact_sha256,
"automatic_rollback": False}
(payload / "ROLLBACK-METADATA.json").write_bytes(canon(rollback))
files = []
for item in sorted(p for p in payload.rglob("*") if p.is_file()):
if item.name != "ARTIFACT-MANIFEST.json":
files.append({"path": "payload/" + item.relative_to(payload).as_posix(), "sha256": digest(item), "size": item.stat().st_size})
manifest = {"schema": "aios.trusted-release-artifact-manifest.v1", "version": version,
"source_commit": source_oid, "tree_oid": tree_oid, "files": files,
"deployable": True, "production_deployed": False}
(payload / "ARTIFACT-MANIFEST.json").write_bytes(canon(manifest))
files.append({"path": "payload/ARTIFACT-MANIFEST.json", "sha256": digest(payload / "ARTIFACT-MANIFEST.json"), "size": (payload / "ARTIFACT-MANIFEST.json").stat().st_size})
tar_path = Path(td) / "artifact.tar"
with tarfile.open(tar_path, "w") as archive:
for item in sorted(p for p in payload.rglob("*") if p.is_file()):
info = archive.gettarinfo(str(item), arcname="payload/" + item.relative_to(payload).as_posix())
info.uid = 0; info.gid = 0; info.uname = "root"; info.gname = "root"; info.mtime = 0; info.mode = 0o644
with item.open("rb") as stream: archive.addfile(info, stream)
shutil.copyfile(tar_path, output)
receipt = {"schema": "aios.trusted-release-artifact-receipt.v1", "version": version,
"source_commit": source_oid, "tree_oid": tree_oid, "artifact_sha256": digest(output),
"artifact_size": output.stat().st_size, "manifest_sha256": hashlib.sha256(canon(manifest)).hexdigest(),
"package_closure_sha256": hashlib.sha256(canon({"source_commit": source_oid, "tree_oid": tree_oid, "files": files})).hexdigest(),
"production_deployed": False, "tag_created": False, "baseline_changed": False,
"service": service, "port": port}
output.with_suffix(".manifest.json").write_bytes(canon(receipt))
return receipt
def main() -> int:
p = argparse.ArgumentParser(); p.add_argument("--source", type=Path, required=True); p.add_argument("--output", type=Path, required=True)
p.add_argument("--version", required=True); p.add_argument("--source-oid", required=True); p.add_argument("--tree-oid", required=True)
p.add_argument("--rollback-version", required=True); p.add_argument("--rollback-target-oid", required=True); p.add_argument("--rollback-artifact-sha256", required=True)
p.add_argument("--service", required=True); p.add_argument("--port", type=int, required=True); a = p.parse_args()
result = build(a.source, a.output, version=a.version, source_oid=a.source_oid, tree_oid=a.tree_oid,
rollback_version=a.rollback_version, rollback_target_oid=a.rollback_target_oid,
rollback_artifact_sha256=a.rollback_artifact_sha256, service=a.service, port=a.port)
print(json.dumps(result, sort_keys=True)); return 0
if __name__ == "__main__": raise SystemExit(main())