101 lines
3.4 KiB
Python
101 lines
3.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Build deterministic immutable staging artifact for AIOS Agent Task API."""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
import tarfile
|
|
import tempfile
|
|
|
|
|
|
REQUIRED = (
|
|
"active-runtime/app/aios_agent_task_protocol.py",
|
|
"active-runtime/app/aios_agent_task_api.py",
|
|
"docs/gitops/openapi-agent-task-v1.yaml",
|
|
)
|
|
|
|
|
|
def sha256(path: Path) -> str:
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as source:
|
|
for chunk in iter(lambda: source.read(1024 * 1024), b""):
|
|
digest.update(chunk)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--source-root", required=True, type=Path)
|
|
parser.add_argument("--output-root", required=True, type=Path)
|
|
parser.add_argument("--source-oid", required=True)
|
|
args = parser.parse_args()
|
|
|
|
oid = args.source_oid.strip().lower()
|
|
if len(oid) != 40 or any(ch not in "0123456789abcdef" for ch in oid):
|
|
raise SystemExit("source oid must be exact 40 hex")
|
|
source = args.source_root.resolve(strict=True)
|
|
output = args.output_root.resolve()
|
|
for relative in REQUIRED:
|
|
if not (source / relative).is_file():
|
|
raise SystemExit("required source missing: " + relative)
|
|
|
|
output.mkdir(parents=True, exist_ok=True)
|
|
archive = output / f"aios-agent-task-api-{oid}.tar.gz"
|
|
manifest = {
|
|
"schema": "aios.agent-task-artifact.v1",
|
|
"source_oid": oid,
|
|
"repository_identity": "aios/aios",
|
|
"deployment_state": "STAGING",
|
|
"runtime_mode": "IMMUTABLE_STAGING",
|
|
"entrypoint": "app/aios_agent_task_api.py",
|
|
"listen": "127.0.0.1:18940",
|
|
"plaintext_credentials_in_artifact": False,
|
|
"included_paths": list(REQUIRED),
|
|
}
|
|
|
|
with tempfile.TemporaryDirectory(prefix="agent-task-build-", dir=str(output)) as temp:
|
|
stage = Path(temp) / "runtime"
|
|
for relative in REQUIRED:
|
|
src = source / relative
|
|
if relative.startswith("active-runtime/"):
|
|
dest = stage / Path(relative).relative_to("active-runtime")
|
|
else:
|
|
dest = stage / relative
|
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
dest.write_bytes(src.read_bytes())
|
|
(stage / "manifest.json").write_text(
|
|
json.dumps(manifest, sort_keys=True, indent=2) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
with tarfile.open(archive, "w:gz", format=tarfile.PAX_FORMAT) as tar:
|
|
for path in sorted(stage.rglob("*")):
|
|
arcname = str(path.relative_to(stage.parent)).replace("\\", "/")
|
|
info = tar.gettarinfo(str(path), arcname=arcname)
|
|
info.mtime = 0
|
|
info.uid = info.gid = 0
|
|
info.uname = info.gname = "root"
|
|
if path.is_file():
|
|
with path.open("rb") as fh:
|
|
tar.addfile(info, fh)
|
|
else:
|
|
tar.addfile(info)
|
|
|
|
receipt = {
|
|
**manifest,
|
|
"artifact_path": str(archive),
|
|
"artifact_sha256": sha256(archive),
|
|
}
|
|
receipt_path = archive.with_suffix(archive.suffix + ".receipt.json")
|
|
receipt_path.write_text(
|
|
json.dumps(receipt, sort_keys=True, indent=2) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
print(json.dumps(receipt, sort_keys=True, indent=2))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|