Merge pull request 'fix(release): close owner auth bootstrap and production executor' (#51) from agent/v2r1c7-production-executor-bootstrap-20260807-r2 into main
Reviewed-on: #51
This commit was merged in pull request #51.
This commit is contained in:
+652
@@ -0,0 +1,652 @@
|
||||
#!/usr/bin/env python3
|
||||
"""One-shot governed executor for an already approved exact release binding.
|
||||
|
||||
This module deliberately does not build release artifacts. The build/staging
|
||||
evidence is completed before Owner production approval. Production execution
|
||||
consumes that immutable binding, revalidates it, and performs narrow
|
||||
side-effects through adapters in a durable fail-closed order.
|
||||
|
||||
Adapter protocol:
|
||||
stdin: JSON request with operation = "probe" | "apply" | "rollback"
|
||||
stdout: one JSON object on the final non-empty line
|
||||
shell: never used
|
||||
|
||||
The executor never rewrites tags, never writes refs/heads/main, and records
|
||||
Baseline last.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import dataclasses
|
||||
import fcntl
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping, Sequence
|
||||
|
||||
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
SHA_RE = re.compile(r"^[0-9a-f]{64}$")
|
||||
PLAN_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
|
||||
|
||||
REQUIRED_V2R1C7_MEMBERS = {
|
||||
"payload/VERSION",
|
||||
"payload/active-runtime/app/deployment_state.py",
|
||||
"payload/active-runtime/app/v2r1c6_privacy_tools.py",
|
||||
"payload/active-runtime/app/v2r1c7_health_service.py",
|
||||
"payload/deploy/aios-v2/systemd/aios-v2r1c7-production.service",
|
||||
"payload/deploy/aios-v2/v2r1c7-runtime-config.json",
|
||||
"payload/deploy/aios-v2/v2r1c7-rollback.json",
|
||||
"payload/scripts/probe_v2r1c7_health.py",
|
||||
"payload/scripts/rollback_v2r1c7_release.sh",
|
||||
"payload/BUILD-METADATA.json",
|
||||
"payload/ARTIFACT-MANIFEST.json",
|
||||
"payload/ROLLBACK-METADATA.json",
|
||||
}
|
||||
|
||||
class ReleaseBlocked(RuntimeError):
|
||||
pass
|
||||
|
||||
class ManualOwnerReviewRequired(ReleaseBlocked):
|
||||
pass
|
||||
|
||||
class ReconciliationRequired(ReleaseBlocked):
|
||||
pass
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class Binding:
|
||||
version: str
|
||||
target_oid: str
|
||||
evidence_closure_oid: str
|
||||
artifact_sha256: str
|
||||
approval_plan_id: str
|
||||
artifact_path: str
|
||||
artifact_size: int
|
||||
production_service: str
|
||||
production_port: int
|
||||
repository_identity: str = "aios/aios"
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.version or not OID_RE.fullmatch(self.target_oid):
|
||||
raise ValueError("invalid release target binding")
|
||||
if not OID_RE.fullmatch(self.evidence_closure_oid):
|
||||
raise ValueError("invalid evidence closure OID")
|
||||
if not SHA_RE.fullmatch(self.artifact_sha256):
|
||||
raise ValueError("invalid artifact SHA-256")
|
||||
if not PLAN_RE.fullmatch(self.approval_plan_id):
|
||||
raise ValueError("invalid approval plan id")
|
||||
if self.artifact_size <= 0:
|
||||
raise ValueError("invalid artifact size")
|
||||
if not self.production_service:
|
||||
raise ValueError("production service is required")
|
||||
if not 1024 <= self.production_port <= 65535:
|
||||
raise ValueError("invalid production port")
|
||||
if self.repository_identity != "aios/aios":
|
||||
raise ValueError("unexpected repository identity")
|
||||
|
||||
@property
|
||||
def tag(self) -> str:
|
||||
return self.version
|
||||
|
||||
@property
|
||||
def fingerprint(self) -> str:
|
||||
payload = {
|
||||
"version": self.version,
|
||||
"target_oid": self.target_oid,
|
||||
"evidence_closure_oid": self.evidence_closure_oid,
|
||||
"artifact_sha256": self.artifact_sha256,
|
||||
"approval_plan_id": self.approval_plan_id,
|
||||
"artifact_size": self.artifact_size,
|
||||
"production_service": self.production_service,
|
||||
"production_port": self.production_port,
|
||||
"repository_identity": self.repository_identity,
|
||||
}
|
||||
return hashlib.sha256(canonical_json(payload)).hexdigest()
|
||||
|
||||
def request(self, operation: str) -> dict[str, Any]:
|
||||
return {
|
||||
"schema": "aios.production-executor-request.v1",
|
||||
"operation": operation,
|
||||
"binding_fingerprint": self.fingerprint,
|
||||
"version": self.version,
|
||||
"tag": self.tag,
|
||||
"target_oid": self.target_oid,
|
||||
"evidence_closure_oid": self.evidence_closure_oid,
|
||||
"artifact_sha256": self.artifact_sha256,
|
||||
"artifact_path": self.artifact_path,
|
||||
"artifact_size": self.artifact_size,
|
||||
"approval_plan_id": self.approval_plan_id,
|
||||
"production_service": self.production_service,
|
||||
"production_port": self.production_port,
|
||||
"repository_identity": self.repository_identity,
|
||||
}
|
||||
|
||||
|
||||
def canonical_json(value: object) -> bytes:
|
||||
return (json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
|
||||
|
||||
|
||||
def secure_json(path: Path) -> dict[str, Any]:
|
||||
st = path.lstat()
|
||||
if not stat.S_ISREG(st.st_mode) or path.is_symlink():
|
||||
raise ReleaseBlocked(f"non-regular authorization file: {path}")
|
||||
if st.st_uid != 0:
|
||||
raise ReleaseBlocked(f"authorization file must be root-owned: {path}")
|
||||
if st.st_mode & 0o022:
|
||||
raise ReleaseBlocked(f"authorization file is group/world writable: {path}")
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(data, dict):
|
||||
raise ReleaseBlocked("authorization receipt must be a JSON object")
|
||||
return data
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise ReleaseBlocked("artifact must be a regular non-symlink file")
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as source:
|
||||
for block in iter(lambda: source.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def run_git(git_dir: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(
|
||||
["git", f"--git-dir={git_dir}", *args],
|
||||
check=check,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
|
||||
|
||||
def verify_git_lineage(git_dir: Path, binding: Binding) -> dict[str, Any]:
|
||||
authority_main = run_git(git_dir, "rev-parse", "refs/heads/main").stdout.strip()
|
||||
if not OID_RE.fullmatch(authority_main):
|
||||
raise ReleaseBlocked("Authority main OID is invalid")
|
||||
|
||||
target_type = run_git(git_dir, "cat-file", "-t", binding.target_oid).stdout.strip()
|
||||
if target_type != "commit":
|
||||
raise ReleaseBlocked("target_oid is not a commit")
|
||||
|
||||
evidence_type = run_git(
|
||||
git_dir, "cat-file", "-t", binding.evidence_closure_oid
|
||||
).stdout.strip()
|
||||
if evidence_type != "commit":
|
||||
raise ReleaseBlocked("evidence_closure_oid is not a commit")
|
||||
|
||||
# A control-plane-only patch may advance main after release approval.
|
||||
# The approved release target remains valid only while it is still on main.
|
||||
target_on_main = run_git(
|
||||
git_dir,
|
||||
"merge-base",
|
||||
"--is-ancestor",
|
||||
binding.target_oid,
|
||||
authority_main,
|
||||
check=False,
|
||||
).returncode == 0
|
||||
if not target_on_main:
|
||||
raise ManualOwnerReviewRequired(
|
||||
"approved target is no longer an ancestor of Authority main"
|
||||
)
|
||||
|
||||
target_in_evidence = run_git(
|
||||
git_dir,
|
||||
"merge-base",
|
||||
"--is-ancestor",
|
||||
binding.target_oid,
|
||||
binding.evidence_closure_oid,
|
||||
check=False,
|
||||
).returncode == 0
|
||||
if not target_in_evidence:
|
||||
raise ReleaseBlocked("approved target is not an ancestor of evidence closure")
|
||||
|
||||
return {
|
||||
"authority_main_oid": authority_main,
|
||||
"target_oid": binding.target_oid,
|
||||
"target_is_ancestor_of_main": True,
|
||||
"evidence_closure_oid": binding.evidence_closure_oid,
|
||||
"evidence_object_type": evidence_type,
|
||||
"target_is_ancestor_of_evidence": True,
|
||||
}
|
||||
|
||||
|
||||
def verify_artifact(binding: Binding) -> dict[str, Any]:
|
||||
path = Path(binding.artifact_path)
|
||||
if path.stat().st_size != binding.artifact_size:
|
||||
raise ReleaseBlocked("artifact size mismatch")
|
||||
actual = sha256_file(path)
|
||||
if actual != binding.artifact_sha256:
|
||||
raise ReleaseBlocked("artifact SHA-256 mismatch")
|
||||
|
||||
if binding.version == "V2R1C7":
|
||||
# Python tarfile may not have zstd support on the target host, so
|
||||
# decompress into a private temporary tar and inspect it there.
|
||||
with tempfile.TemporaryDirectory(prefix="aios-artifact-verify-") as temp_dir:
|
||||
tar_path = Path(temp_dir) / "release.tar"
|
||||
with tar_path.open("wb") as output:
|
||||
completed = subprocess.run(
|
||||
["zstd", "-dc", str(path)],
|
||||
check=False,
|
||||
stdout=output,
|
||||
stderr=subprocess.PIPE,
|
||||
timeout=60,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise ReleaseBlocked("artifact zstd decompression failed")
|
||||
import tarfile
|
||||
with tarfile.open(tar_path, "r:") as archive:
|
||||
names: set[str] = set()
|
||||
for member in archive.getmembers():
|
||||
candidate = Path(member.name)
|
||||
if (
|
||||
candidate.is_absolute()
|
||||
or ".." in candidate.parts
|
||||
or member.issym()
|
||||
or member.islnk()
|
||||
or not (member.isfile() or member.isdir())
|
||||
):
|
||||
raise ReleaseBlocked(f"unsafe artifact member: {member.name}")
|
||||
names.add(member.name)
|
||||
missing = sorted(REQUIRED_V2R1C7_MEMBERS - names)
|
||||
if missing:
|
||||
raise ReleaseBlocked(
|
||||
"artifact missing required members: " + ",".join(missing)
|
||||
)
|
||||
return {
|
||||
"artifact_path": str(path),
|
||||
"artifact_sha256": actual,
|
||||
"artifact_size": binding.artifact_size,
|
||||
"verified": True,
|
||||
}
|
||||
|
||||
|
||||
def verify_authorization(receipt: Mapping[str, Any], binding: Binding) -> dict[str, Any]:
|
||||
if receipt.get("accepted") is not True:
|
||||
raise ReleaseBlocked("Owner production authorization is not accepted")
|
||||
if receipt.get("binding_replaced") not in (None, False):
|
||||
raise ManualOwnerReviewRequired("Owner binding was replaced")
|
||||
expected = {
|
||||
"version": binding.version,
|
||||
"target_oid": binding.target_oid,
|
||||
"evidence_closure_oid": binding.evidence_closure_oid,
|
||||
"artifact_sha256": binding.artifact_sha256,
|
||||
"approval_plan_id": binding.approval_plan_id,
|
||||
}
|
||||
for key, value in expected.items():
|
||||
if receipt.get(key) != value:
|
||||
raise ManualOwnerReviewRequired(f"Owner authorization mismatch: {key}")
|
||||
return {"accepted": True, **expected}
|
||||
|
||||
|
||||
class DurableState:
|
||||
def __init__(self, root: Path, binding: Binding):
|
||||
self.root = root
|
||||
self.root.mkdir(parents=True, exist_ok=True)
|
||||
os.chmod(self.root, 0o700)
|
||||
self.path = self.root / "execution.json"
|
||||
self.binding = binding
|
||||
self.value: dict[str, Any]
|
||||
if self.path.exists():
|
||||
existing = json.loads(self.path.read_text(encoding="utf-8"))
|
||||
if existing.get("binding_fingerprint") != binding.fingerprint:
|
||||
raise ManualOwnerReviewRequired(
|
||||
"state root contains a different production binding"
|
||||
)
|
||||
self.value = existing
|
||||
else:
|
||||
self.value = {
|
||||
"schema": "aios.production-execution-state.v1",
|
||||
"binding_fingerprint": binding.fingerprint,
|
||||
"version": binding.version,
|
||||
"target_oid": binding.target_oid,
|
||||
"state": "AUTHORIZED",
|
||||
"completed": {},
|
||||
"history": [],
|
||||
}
|
||||
self.save()
|
||||
|
||||
def save(self) -> None:
|
||||
temporary = self.path.with_suffix(".tmp")
|
||||
temporary.write_bytes(canonical_json(self.value))
|
||||
os.chmod(temporary, 0o600)
|
||||
os.replace(temporary, self.path)
|
||||
|
||||
def record(self, state: str, step: str, result: Mapping[str, Any]) -> None:
|
||||
now = time.time()
|
||||
sanitized = json.loads(json.dumps(result))
|
||||
self.value["state"] = state
|
||||
self.value["completed"][step] = sanitized
|
||||
self.value["history"].append(
|
||||
{"at": now, "state": state, "step": step, "result": sanitized}
|
||||
)
|
||||
self.save()
|
||||
|
||||
def fail(self, state: str, reason: str) -> None:
|
||||
self.value["state"] = state
|
||||
self.value["history"].append(
|
||||
{"at": time.time(), "state": state, "reason": reason}
|
||||
)
|
||||
self.save()
|
||||
|
||||
|
||||
class FileLock:
|
||||
def __init__(self, path: Path):
|
||||
self.path = path
|
||||
self.handle = None
|
||||
|
||||
def __enter__(self) -> "FileLock":
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.handle = self.path.open("a+", encoding="utf-8")
|
||||
os.chmod(self.path, 0o600)
|
||||
try:
|
||||
fcntl.flock(
|
||||
self.handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB
|
||||
)
|
||||
except BlockingIOError as exc:
|
||||
raise ReleaseBlocked("production executor is already running") from exc
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb) -> None:
|
||||
if self.handle is not None:
|
||||
fcntl.flock(self.handle.fileno(), fcntl.LOCK_UN)
|
||||
self.handle.close()
|
||||
|
||||
|
||||
def parse_command(values: Sequence[str]) -> list[str]:
|
||||
command = [str(x) for x in values if str(x)]
|
||||
if not command:
|
||||
raise ValueError("adapter command is required")
|
||||
if not os.path.isabs(command[0]):
|
||||
raise ValueError("adapter executable must be an absolute path")
|
||||
return command
|
||||
|
||||
|
||||
def run_adapter(command: Sequence[str], request: Mapping[str, Any]) -> dict[str, Any]:
|
||||
completed = subprocess.run(
|
||||
list(command),
|
||||
input=canonical_json(request),
|
||||
check=False,
|
||||
capture_output=True,
|
||||
timeout=180,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise ReleaseBlocked(
|
||||
f"adapter failed: {Path(command[0]).name} rc={completed.returncode}"
|
||||
)
|
||||
lines = [line for line in completed.stdout.decode("utf-8").splitlines() if line.strip()]
|
||||
if not lines:
|
||||
raise ReleaseBlocked("adapter returned no JSON result")
|
||||
try:
|
||||
value = json.loads(lines[-1])
|
||||
except ValueError as exc:
|
||||
raise ReleaseBlocked("adapter returned invalid JSON") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise ReleaseBlocked("adapter result must be an object")
|
||||
return value
|
||||
|
||||
|
||||
def require_exact_response(result: Mapping[str, Any], binding: Binding) -> None:
|
||||
for key, expected in {
|
||||
"version": binding.version,
|
||||
"target_oid": binding.target_oid,
|
||||
"evidence_closure_oid": binding.evidence_closure_oid,
|
||||
"artifact_sha256": binding.artifact_sha256,
|
||||
"approval_plan_id": binding.approval_plan_id,
|
||||
}.items():
|
||||
if key in result and result[key] != expected:
|
||||
raise ManualOwnerReviewRequired(f"adapter binding mismatch: {key}")
|
||||
|
||||
|
||||
def tag_step(command: Sequence[str], binding: Binding) -> dict[str, Any]:
|
||||
request = binding.request("probe")
|
||||
probe = run_adapter(command, request)
|
||||
require_exact_response(probe, binding)
|
||||
state = probe.get("status")
|
||||
if state == "CONFLICT":
|
||||
raise ManualOwnerReviewRequired("same-name tag conflicts with approved binding")
|
||||
if state == "INDETERMINATE":
|
||||
raise ReconciliationRequired("tag publication state is indeterminate")
|
||||
if state not in {"APPLIED_VERIFIED", "NOT_APPLIED", "PARTIAL_LOCAL"}:
|
||||
raise ReleaseBlocked(f"unsupported tag probe status: {state}")
|
||||
if state != "APPLIED_VERIFIED":
|
||||
applied = run_adapter(command, binding.request("apply"))
|
||||
require_exact_response(applied, binding)
|
||||
if applied.get("status") == "CONFLICT":
|
||||
raise ManualOwnerReviewRequired("tag conflict during publication")
|
||||
probe = run_adapter(command, request)
|
||||
require_exact_response(probe, binding)
|
||||
if probe.get("status") != "APPLIED_VERIFIED":
|
||||
raise ReconciliationRequired("tag was not independently verified")
|
||||
if probe.get("peeled_target_oid") != binding.target_oid:
|
||||
raise ManualOwnerReviewRequired("published tag peeled target mismatch")
|
||||
tag_oid = probe.get("tag_oid")
|
||||
if not isinstance(tag_oid, str) or not OID_RE.fullmatch(tag_oid):
|
||||
raise ReleaseBlocked("verified annotated tag object OID missing")
|
||||
return dict(probe)
|
||||
|
||||
|
||||
def deploy_step(command: Sequence[str], binding: Binding) -> dict[str, Any]:
|
||||
probe = run_adapter(command, binding.request("probe"))
|
||||
require_exact_response(probe, binding)
|
||||
if probe.get("status") == "ACTIVE_VERIFIED":
|
||||
return dict(probe)
|
||||
if probe.get("status") == "CONFLICT":
|
||||
raise ManualOwnerReviewRequired("production runtime has conflicting binding")
|
||||
applied = run_adapter(command, binding.request("apply"))
|
||||
require_exact_response(applied, binding)
|
||||
probe = run_adapter(command, binding.request("probe"))
|
||||
require_exact_response(probe, binding)
|
||||
if probe.get("status") != "ACTIVE_VERIFIED":
|
||||
try:
|
||||
run_adapter(command, binding.request("rollback"))
|
||||
except Exception:
|
||||
pass
|
||||
raise ReconciliationRequired("production runtime did not verify after apply")
|
||||
if (
|
||||
probe.get("deployment_state") != "ACTIVE"
|
||||
or probe.get("production_deployed") is not True
|
||||
):
|
||||
raise ReconciliationRequired("production health contract is not ACTIVE")
|
||||
return dict(probe)
|
||||
|
||||
|
||||
def finalize_step(
|
||||
command: Sequence[str],
|
||||
binding: Binding,
|
||||
*,
|
||||
name: str,
|
||||
tag_oid: str,
|
||||
) -> dict[str, Any]:
|
||||
request = binding.request("probe")
|
||||
request["tag_oid"] = tag_oid
|
||||
probe = run_adapter(command, request)
|
||||
require_exact_response(probe, binding)
|
||||
if probe.get("status") == "CONFLICT":
|
||||
raise ManualOwnerReviewRequired(f"{name} has conflicting release binding")
|
||||
if probe.get("status") != "VERIFIED":
|
||||
apply_request = binding.request("apply")
|
||||
apply_request["tag_oid"] = tag_oid
|
||||
applied = run_adapter(command, apply_request)
|
||||
require_exact_response(applied, binding)
|
||||
probe = run_adapter(command, request)
|
||||
require_exact_response(probe, binding)
|
||||
if probe.get("status") != "VERIFIED":
|
||||
raise ReconciliationRequired(f"{name} did not verify after apply")
|
||||
if probe.get("tag_oid") not in (None, tag_oid):
|
||||
raise ManualOwnerReviewRequired(f"{name} tag object mismatch")
|
||||
return dict(probe)
|
||||
|
||||
|
||||
def execute(
|
||||
*,
|
||||
binding: Binding,
|
||||
authorization_path: Path,
|
||||
repository: Path,
|
||||
state_root: Path,
|
||||
tag_command: Sequence[str],
|
||||
deploy_command: Sequence[str],
|
||||
registry_command: Sequence[str],
|
||||
baseline_command: Sequence[str],
|
||||
) -> dict[str, Any]:
|
||||
authorization = verify_authorization(secure_json(authorization_path), binding)
|
||||
lineage = verify_git_lineage(repository, binding)
|
||||
artifact = verify_artifact(binding)
|
||||
state = DurableState(state_root, binding)
|
||||
with FileLock(state_root / "executor.lock"):
|
||||
state.record(
|
||||
"PREFLIGHT_VERIFIED",
|
||||
"preflight",
|
||||
{"authorization": authorization, "lineage": lineage, "artifact": artifact},
|
||||
)
|
||||
tag: dict[str, Any] | None = None
|
||||
runtime: dict[str, Any] | None = None
|
||||
try:
|
||||
tag = tag_step(tag_command, binding)
|
||||
state.record("TAG_VERIFIED", "tag", tag)
|
||||
|
||||
runtime = deploy_step(deploy_command, binding)
|
||||
state.record("RUNTIME_VERIFIED", "runtime", runtime)
|
||||
|
||||
registry = finalize_step(
|
||||
registry_command,
|
||||
binding,
|
||||
name="registry",
|
||||
tag_oid=tag["tag_oid"],
|
||||
)
|
||||
state.record("REGISTRY_VERIFIED", "registry", registry)
|
||||
|
||||
baseline = finalize_step(
|
||||
baseline_command,
|
||||
binding,
|
||||
name="baseline",
|
||||
tag_oid=tag["tag_oid"],
|
||||
)
|
||||
state.record("BASELINE_VERIFIED", "baseline", baseline)
|
||||
|
||||
# Final readback: no mutation, all four planes must still agree.
|
||||
final_tag = tag_step(tag_command, binding)
|
||||
final_runtime = run_adapter(deploy_command, binding.request("probe"))
|
||||
final_registry_request = binding.request("probe")
|
||||
final_registry_request["tag_oid"] = tag["tag_oid"]
|
||||
final_registry = run_adapter(registry_command, final_registry_request)
|
||||
final_baseline_request = binding.request("probe")
|
||||
final_baseline_request["tag_oid"] = tag["tag_oid"]
|
||||
final_baseline = run_adapter(baseline_command, final_baseline_request)
|
||||
for value in (final_runtime, final_registry, final_baseline):
|
||||
require_exact_response(value, binding)
|
||||
if final_runtime.get("status") != "ACTIVE_VERIFIED":
|
||||
raise ReconciliationRequired("final runtime readback failed")
|
||||
if final_registry.get("status") != "VERIFIED":
|
||||
raise ReconciliationRequired("final Registry readback failed")
|
||||
if final_baseline.get("status") != "VERIFIED":
|
||||
raise ReconciliationRequired("final Baseline readback failed")
|
||||
final = {
|
||||
"status": "PRODUCTION_ACCEPTED",
|
||||
"version": binding.version,
|
||||
"target_oid": binding.target_oid,
|
||||
"evidence_closure_oid": binding.evidence_closure_oid,
|
||||
"artifact_sha256": binding.artifact_sha256,
|
||||
"approval_plan_id": binding.approval_plan_id,
|
||||
"tag_oid": tag["tag_oid"],
|
||||
"tag": final_tag,
|
||||
"runtime": final_runtime,
|
||||
"registry": final_registry,
|
||||
"baseline": final_baseline,
|
||||
"force_push_used": False,
|
||||
"tag_rewritten": False,
|
||||
"direct_main_push": False,
|
||||
"secrets_committed": False,
|
||||
}
|
||||
state.record("COMPLETED", "final_consistency", final)
|
||||
return final
|
||||
except ManualOwnerReviewRequired as exc:
|
||||
state.fail("MANUAL_OWNER_REVIEW_REQUIRED", str(exc))
|
||||
raise
|
||||
except ReconciliationRequired as exc:
|
||||
state.fail("RECONCILIATION_REQUIRED", str(exc))
|
||||
raise
|
||||
except Exception as exc:
|
||||
# Once a formal tag has been verified, no later error is a clean
|
||||
# pre-side-effect failure. Preserve the tag and require idempotent
|
||||
# reconciliation rather than pretending the release never started.
|
||||
if tag is not None:
|
||||
reason = f"{type(exc).__name__}: {exc}"
|
||||
state.fail("RECONCILIATION_REQUIRED", reason)
|
||||
raise ReconciliationRequired(reason) from exc
|
||||
state.fail("FAIL_CLOSED", f"{type(exc).__name__}: {exc}")
|
||||
raise
|
||||
|
||||
|
||||
def parser() -> argparse.ArgumentParser:
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--authorization", required=True, type=Path)
|
||||
p.add_argument("--repository", required=True, type=Path)
|
||||
p.add_argument("--state-root", required=True, type=Path)
|
||||
p.add_argument("--artifact", required=True, type=Path)
|
||||
p.add_argument("--artifact-size", required=True, type=int)
|
||||
p.add_argument("--version", required=True)
|
||||
p.add_argument("--target-oid", required=True)
|
||||
p.add_argument("--evidence-closure-oid", required=True)
|
||||
p.add_argument("--artifact-sha256", required=True)
|
||||
p.add_argument("--approval-plan-id", required=True)
|
||||
p.add_argument("--production-service", required=True)
|
||||
p.add_argument("--production-port", required=True, type=int)
|
||||
p.add_argument("--repository-identity", default="aios/aios")
|
||||
p.add_argument("--tag-adapter", required=True, nargs="+")
|
||||
p.add_argument("--deploy-adapter", required=True, nargs="+")
|
||||
p.add_argument("--registry-adapter", required=True, nargs="+")
|
||||
p.add_argument("--baseline-adapter", required=True, nargs="+")
|
||||
return p
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parser().parse_args()
|
||||
binding = Binding(
|
||||
version=args.version,
|
||||
target_oid=args.target_oid,
|
||||
evidence_closure_oid=args.evidence_closure_oid,
|
||||
artifact_sha256=args.artifact_sha256,
|
||||
approval_plan_id=args.approval_plan_id,
|
||||
artifact_path=str(args.artifact),
|
||||
artifact_size=args.artifact_size,
|
||||
production_service=args.production_service,
|
||||
production_port=args.production_port,
|
||||
repository_identity=args.repository_identity,
|
||||
)
|
||||
try:
|
||||
result = execute(
|
||||
binding=binding,
|
||||
authorization_path=args.authorization,
|
||||
repository=args.repository,
|
||||
state_root=args.state_root,
|
||||
tag_command=parse_command(args.tag_adapter),
|
||||
deploy_command=parse_command(args.deploy_adapter),
|
||||
registry_command=parse_command(args.registry_adapter),
|
||||
baseline_command=parse_command(args.baseline_adapter),
|
||||
)
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
return 0
|
||||
except ManualOwnerReviewRequired as exc:
|
||||
print(json.dumps({"status": "MANUAL_OWNER_REVIEW_REQUIRED", "reason": str(exc)}, sort_keys=True))
|
||||
return 20
|
||||
except ReconciliationRequired as exc:
|
||||
print(json.dumps({"status": "RECONCILIATION_REQUIRED", "reason": str(exc)}, sort_keys=True))
|
||||
return 21
|
||||
except Exception as exc:
|
||||
print(json.dumps({"status": "FAIL_CLOSED", "error": type(exc).__name__, "reason": str(exc)}, sort_keys=True))
|
||||
return 22
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Executable
+293
@@ -0,0 +1,293 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Thin production tag adapter over AIOS's existing mature TagPublisher.
|
||||
|
||||
This adapter deliberately does not call TagPublisher.publish(), because that
|
||||
legacy method also mutates Registry/Baseline. The production executor requires:
|
||||
Tag -> runtime ACTIVE -> Registry -> Baseline.
|
||||
|
||||
`apply` reuses TagPublisher._verify_publication() under the publisher's durable
|
||||
per-version lock. That method performs the existing fail-closed annotated-tag,
|
||||
network remote, Gitea API, peeled-target and listing verification, but stops
|
||||
before Registry/Baseline mutation.
|
||||
|
||||
`probe` is read-only.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping
|
||||
|
||||
from gitea_tag_publication import (
|
||||
GiteaTagClient,
|
||||
GitTagBackend,
|
||||
PublicationConflict,
|
||||
PublicationManualOwnerReviewRequired,
|
||||
PublicationPending,
|
||||
PublicationReconciliationRequired,
|
||||
PublicationRequest,
|
||||
PublicationState,
|
||||
TagPublisher,
|
||||
)
|
||||
|
||||
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
PLAN_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
|
||||
|
||||
|
||||
def load_request() -> dict[str, Any]:
|
||||
value = json.load(sys.stdin)
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError("request_object_required")
|
||||
if value.get("schema") != "aios.production-executor-request.v1":
|
||||
raise ValueError("request_schema_invalid")
|
||||
if not re.fullmatch(r"V\d+R\d+C\d+(?:-[A-Za-z0-9_.-]+)?", str(value.get("version", ""))):
|
||||
raise ValueError("version_invalid")
|
||||
for name in ("target_oid", "evidence_closure_oid"):
|
||||
if not OID_RE.fullmatch(str(value.get(name, ""))):
|
||||
raise ValueError(f"{name}_invalid")
|
||||
if not PLAN_RE.fullmatch(str(value.get("approval_plan_id", ""))):
|
||||
raise ValueError("approval_plan_id_invalid")
|
||||
if value.get("repository_identity") != "aios/aios":
|
||||
raise ValueError("repository_identity_invalid")
|
||||
return value
|
||||
|
||||
|
||||
def publication_request(value: Mapping[str, Any]) -> PublicationRequest:
|
||||
return PublicationRequest(
|
||||
str(value["version"]),
|
||||
str(value["target_oid"]),
|
||||
str(value["evidence_closure_oid"]),
|
||||
str(value["approval_plan_id"]),
|
||||
)
|
||||
|
||||
|
||||
def response(value: Mapping[str, Any], status: str, **extra: Any) -> dict[str, Any]:
|
||||
return {
|
||||
"status": status,
|
||||
"version": value["version"],
|
||||
"target_oid": value["target_oid"],
|
||||
"evidence_closure_oid": value["evidence_closure_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"approval_plan_id": value["approval_plan_id"],
|
||||
**extra,
|
||||
}
|
||||
|
||||
|
||||
def build(args: argparse.Namespace) -> tuple[TagPublisher, GitTagBackend, GiteaTagClient]:
|
||||
git = GitTagBackend(args.workspace, authoritative_repo=args.repository)
|
||||
gitea = GiteaTagClient(
|
||||
args.gitea_api,
|
||||
args.gitea_repo,
|
||||
args.gitea_token_file,
|
||||
token_line=args.gitea_token_line,
|
||||
)
|
||||
publisher = TagPublisher(
|
||||
args.workspace,
|
||||
git,
|
||||
gitea,
|
||||
args.remote,
|
||||
state_root=args.state_root,
|
||||
)
|
||||
return publisher, git, gitea
|
||||
|
||||
|
||||
def _gitea_lookup(gitea: GiteaTagClient, version: str) -> tuple[str, str] | None:
|
||||
try:
|
||||
return gitea.lookup_tag(version)
|
||||
except RuntimeError as exc:
|
||||
if "HTTP 404" in str(exc):
|
||||
return None
|
||||
raise
|
||||
|
||||
|
||||
def probe(
|
||||
args: argparse.Namespace,
|
||||
value: Mapping[str, Any],
|
||||
publisher: TagPublisher,
|
||||
git: GitTagBackend,
|
||||
gitea: GiteaTagClient,
|
||||
) -> dict[str, Any]:
|
||||
request = publication_request(value)
|
||||
request.validate()
|
||||
git.validate_evidence_lineage(request)
|
||||
|
||||
local_oid = git._rev(f"refs/tags/{request.version}")
|
||||
local_target = git._rev(f"refs/tags/{request.version}^{{commit}}")
|
||||
remote = git.remote_tag(args.remote, request.version)
|
||||
api = _gitea_lookup(gitea, request.version)
|
||||
listing = False
|
||||
if api is not None:
|
||||
listing = gitea.tag_visible_in_listing(request.version, request.target_oid)
|
||||
|
||||
if local_oid is None and remote is None and api is None:
|
||||
return response(value, "NOT_APPLIED")
|
||||
|
||||
if local_oid is not None:
|
||||
if local_target is None or local_oid == local_target:
|
||||
return response(value, "CONFLICT", reason="local_tag_not_annotated")
|
||||
if local_target != request.target_oid:
|
||||
return response(
|
||||
value,
|
||||
"CONFLICT",
|
||||
reason="local_peeled_target_mismatch",
|
||||
tag_oid=local_oid,
|
||||
peeled_target_oid=local_target,
|
||||
)
|
||||
|
||||
# A remote/Gitea tag without the Authority tag object is not safe to
|
||||
# recreate, because a new annotated tag would have a different object OID.
|
||||
if local_oid is None and (remote is not None or api is not None):
|
||||
return response(
|
||||
value,
|
||||
"INDETERMINATE",
|
||||
reason="remote_or_gitea_tag_exists_without_authority_tag_object",
|
||||
)
|
||||
|
||||
if remote is not None:
|
||||
if remote[0] != local_oid or remote[1] != request.target_oid:
|
||||
return response(
|
||||
value,
|
||||
"CONFLICT",
|
||||
reason="network_remote_tag_mismatch",
|
||||
tag_oid=local_oid,
|
||||
peeled_target_oid=local_target,
|
||||
remote_tag_oid=remote[0],
|
||||
remote_peeled_target_oid=remote[1],
|
||||
)
|
||||
|
||||
if api is not None:
|
||||
if api[0] != local_oid or api[1] != request.target_oid:
|
||||
return response(
|
||||
value,
|
||||
"CONFLICT",
|
||||
reason="gitea_api_tag_mismatch",
|
||||
tag_oid=local_oid,
|
||||
peeled_target_oid=local_target,
|
||||
gitea_tag_oid=api[0],
|
||||
gitea_peeled_target_oid=api[1],
|
||||
)
|
||||
|
||||
if (
|
||||
local_oid is not None
|
||||
and remote is not None
|
||||
and api is not None
|
||||
and listing
|
||||
):
|
||||
return response(
|
||||
value,
|
||||
"APPLIED_VERIFIED",
|
||||
tag_oid=local_oid,
|
||||
peeled_target_oid=local_target,
|
||||
remote_tag_oid=remote[0],
|
||||
remote_peeled_target_oid=remote[1],
|
||||
gitea_tag_oid=api[0],
|
||||
gitea_peeled_target_oid=api[1],
|
||||
gitea_listing_visible=True,
|
||||
)
|
||||
|
||||
if local_oid is not None and remote is None and api is None:
|
||||
return response(
|
||||
value,
|
||||
"PARTIAL_LOCAL",
|
||||
tag_oid=local_oid,
|
||||
peeled_target_oid=local_target,
|
||||
)
|
||||
|
||||
return response(
|
||||
value,
|
||||
"INDETERMINATE",
|
||||
reason="tag_visibility_partial",
|
||||
tag_oid=local_oid,
|
||||
peeled_target_oid=local_target,
|
||||
)
|
||||
|
||||
|
||||
def apply(
|
||||
args: argparse.Namespace,
|
||||
value: Mapping[str, Any],
|
||||
publisher: TagPublisher,
|
||||
git: GitTagBackend,
|
||||
gitea: GiteaTagClient,
|
||||
) -> dict[str, Any]:
|
||||
before = probe(args, value, publisher, git, gitea)
|
||||
if before["status"] in {"APPLIED_VERIFIED", "CONFLICT", "INDETERMINATE"}:
|
||||
return before
|
||||
|
||||
request = publication_request(value)
|
||||
with publisher.store.lock(request.version):
|
||||
request.validate()
|
||||
publisher._assert_fingerprint(request)
|
||||
publisher.store.record(request, PublicationState.PENDING)
|
||||
publisher._verify_publication(
|
||||
request,
|
||||
allow_push=True,
|
||||
previously_deployed=False,
|
||||
)
|
||||
return probe(args, value, publisher, git, gitea)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--workspace", type=Path, default=Path(os.environ["AIOS_GITOPS_CONTROLLER_WORKSPACE"]))
|
||||
parser.add_argument("--repository", type=Path, default=Path(os.environ["AIOS_GITOPS_AUTHORITATIVE_GIT_DIR"]))
|
||||
parser.add_argument("--remote", default=os.getenv("AIOS_GITOPS_GITEA_REMOTE", "gitea"))
|
||||
parser.add_argument("--gitea-api", default=os.getenv("AIOS_GITOPS_GITEA_API_URL", ""))
|
||||
parser.add_argument("--gitea-repo", default=os.getenv("AIOS_GITOPS_GITEA_REPOSITORY", ""))
|
||||
parser.add_argument(
|
||||
"--gitea-token-file",
|
||||
type=Path,
|
||||
default=Path(os.getenv("AIOS_GITOPS_GITEA_TOKEN_FILE", "")),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--gitea-token-line",
|
||||
type=int,
|
||||
default=int(os.getenv("AIOS_GITOPS_GITEA_TOKEN_LINE", "1")),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--state-root",
|
||||
type=Path,
|
||||
default=Path(
|
||||
os.getenv(
|
||||
"AIOS_GITOPS_PUBLICATION_STATE_DIR",
|
||||
"/var/lib/aios-gitops/tag-publications",
|
||||
)
|
||||
),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
value = load_request()
|
||||
publisher, git, gitea = build(args)
|
||||
operation = value.get("operation")
|
||||
if operation == "probe":
|
||||
result = probe(args, value, publisher, git, gitea)
|
||||
elif operation == "apply":
|
||||
result = apply(args, value, publisher, git, gitea)
|
||||
else:
|
||||
raise ValueError("unsupported_operation")
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except PublicationManualOwnerReviewRequired as exc:
|
||||
print(json.dumps({"status": "CONFLICT", "reason": str(exc)}, sort_keys=True))
|
||||
raise SystemExit(20)
|
||||
except PublicationConflict as exc:
|
||||
print(json.dumps({"status": "CONFLICT", "reason": str(exc)}, sort_keys=True))
|
||||
raise SystemExit(20)
|
||||
except (PublicationPending, PublicationReconciliationRequired) as exc:
|
||||
print(json.dumps({"status": "INDETERMINATE", "reason": str(exc)}, sort_keys=True))
|
||||
raise SystemExit(21)
|
||||
except Exception as exc:
|
||||
print(
|
||||
json.dumps(
|
||||
{"status": "INDETERMINATE", "error": type(exc).__name__, "reason": str(exc)},
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
raise SystemExit(22)
|
||||
+642
@@ -0,0 +1,642 @@
|
||||
#!/usr/bin/env python3
|
||||
"""One-shot local Owner authentication ingress for AIOS.
|
||||
|
||||
This module deliberately does not mint a reusable Owner token.
|
||||
|
||||
Authentication is based on a real local login session:
|
||||
- a root-run one-shot server owns a Unix socket;
|
||||
- the client must run as the configured Owner OS account;
|
||||
- the server validates SO_PEERCRED, Linux audit loginuid and the controlling TTY;
|
||||
- the Owner must interactively confirm the exact task/branch/base request;
|
||||
- the server constructs a narrow OwnerPrincipal and invokes the existing
|
||||
OwnerAuthorizationBroker;
|
||||
- one successful authorization consumes the service lifetime.
|
||||
|
||||
The resulting principal is fixed to one non-wildcard intake scope
|
||||
(`commit:create` for the current deployment) and cannot carry production,
|
||||
tag, baseline, main or history-rewrite capability.
|
||||
|
||||
This is a trust-root ingress, not a Git writer and not a production executor.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import fcntl
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import pwd
|
||||
import re
|
||||
import secrets
|
||||
import socket
|
||||
import stat
|
||||
import struct
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from typing import Any, Mapping
|
||||
|
||||
|
||||
BINDING_SCHEMA = "aios.local-owner-auth-ingress.v1"
|
||||
CLIENT_BEGIN_SCHEMA = "aios.local-owner-auth-client-begin.v1"
|
||||
CHALLENGE_SCHEMA = "aios.local-owner-auth-challenge.v1"
|
||||
CLIENT_CONFIRM_SCHEMA = "aios.local-owner-auth-client-confirm.v1"
|
||||
RESULT_SCHEMA = "aios.local-owner-auth-result.v1"
|
||||
|
||||
ACTION = "AUTHORIZE_TASK_WRITER_PROVISIONING"
|
||||
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
IDENTITY_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._@:+/-]{1,254}$")
|
||||
SCOPE_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{1,127}$")
|
||||
TASK_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{1,255}$")
|
||||
BRANCH_RE = re.compile(r"^agent/[A-Za-z0-9._/-]{1,220}$")
|
||||
|
||||
MAX_FRAME = 64 * 1024
|
||||
CHALLENGE_TTL_SECONDS = 120
|
||||
|
||||
EXPECTED_ALLOWED_OPERATIONS = frozenset(
|
||||
{
|
||||
"create_worktree",
|
||||
"create_target_branch_if_absent",
|
||||
"commit_to_target_branch",
|
||||
"push_target_branch",
|
||||
}
|
||||
)
|
||||
EXPECTED_DENIED_OPERATIONS = frozenset(
|
||||
{
|
||||
"direct_main_push",
|
||||
"force_push",
|
||||
"tag_create",
|
||||
"tag_update",
|
||||
"tag_delete",
|
||||
"branch_delete",
|
||||
"repository_admin",
|
||||
"baseline_write",
|
||||
"production_deploy",
|
||||
"production_marker_write",
|
||||
"history_rewrite",
|
||||
}
|
||||
)
|
||||
|
||||
SENSITIVE_FRAGMENTS = (
|
||||
"authorization_header",
|
||||
"bearer",
|
||||
"client_secret",
|
||||
"password",
|
||||
"private_key",
|
||||
"raw_secret",
|
||||
"raw_token",
|
||||
"secret_value",
|
||||
)
|
||||
|
||||
|
||||
class BootstrapError(RuntimeError):
|
||||
def __init__(self, code: str, status: int = 1) -> None:
|
||||
super().__init__(code)
|
||||
self.code = code
|
||||
self.status = status
|
||||
|
||||
|
||||
def canonical_json(value: Any) -> bytes:
|
||||
return (
|
||||
json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
|
||||
+ "\n"
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def sha256_json(value: Any) -> str:
|
||||
return hashlib.sha256(canonical_json(value)).hexdigest()
|
||||
|
||||
|
||||
def atomic_json(path: Path, value: Mapping[str, Any], mode: int = 0o600) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if path.parent.is_symlink() or path.is_symlink():
|
||||
raise BootstrapError("STATE_SYMLINK_REJECTED")
|
||||
fd, name = tempfile.mkstemp(prefix=f".{path.name}.", dir=str(path.parent))
|
||||
try:
|
||||
os.fchmod(fd, mode)
|
||||
with os.fdopen(fd, "wb", closefd=True) as stream:
|
||||
stream.write(canonical_json(dict(value)))
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
os.replace(name, path)
|
||||
os.chmod(path, mode)
|
||||
dfd = os.open(path.parent, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
|
||||
try:
|
||||
os.fsync(dfd)
|
||||
finally:
|
||||
os.close(dfd)
|
||||
finally:
|
||||
try:
|
||||
os.unlink(name)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def secure_root_file(path: Path) -> dict[str, Any]:
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise BootstrapError("BINDING_FILE_INVALID")
|
||||
st = path.lstat()
|
||||
if st.st_uid != 0:
|
||||
raise BootstrapError("BINDING_FILE_ROOT_OWNER_REQUIRED")
|
||||
if stat.S_IMODE(st.st_mode) & 0o077:
|
||||
raise BootstrapError("BINDING_FILE_PERMISSIONS_INVALID")
|
||||
if st.st_size <= 0 or st.st_size > 1_048_576:
|
||||
raise BootstrapError("BINDING_FILE_SIZE_INVALID")
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise BootstrapError("BINDING_JSON_INVALID") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise BootstrapError("BINDING_JSON_INVALID")
|
||||
return value
|
||||
|
||||
|
||||
def validate_binding(value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
if set(value) != {
|
||||
"schema",
|
||||
"owner_identity",
|
||||
"owner_role",
|
||||
"authenticated_scope",
|
||||
"request",
|
||||
}:
|
||||
raise BootstrapError("BINDING_FIELDS_REJECTED")
|
||||
if value["schema"] != BINDING_SCHEMA:
|
||||
raise BootstrapError("BINDING_SCHEMA_REJECTED")
|
||||
identity = value["owner_identity"]
|
||||
if not isinstance(identity, str) or not IDENTITY_RE.fullmatch(identity):
|
||||
raise BootstrapError("OWNER_IDENTITY_INVALID")
|
||||
if value["owner_role"] != "owner":
|
||||
raise BootstrapError("OWNER_ROLE_INVALID")
|
||||
scope = value["authenticated_scope"]
|
||||
if (
|
||||
not isinstance(scope, str)
|
||||
or not SCOPE_RE.fullmatch(scope)
|
||||
or scope == "writer:provision"
|
||||
or scope == "*"
|
||||
or scope.endswith(":*")
|
||||
):
|
||||
raise BootstrapError("OWNER_SCOPE_INVALID")
|
||||
|
||||
request = value["request"]
|
||||
if not isinstance(request, dict):
|
||||
raise BootstrapError("REQUEST_INVALID")
|
||||
expected_fields = {
|
||||
"action",
|
||||
"task_id",
|
||||
"repository",
|
||||
"base_ref",
|
||||
"base_oid",
|
||||
"target_branch",
|
||||
"one_time_use",
|
||||
"expires_in_seconds",
|
||||
"production_change_authorized",
|
||||
"tag_change_authorized",
|
||||
"baseline_change_authorized",
|
||||
"main_write_authorized",
|
||||
"allowed_operations",
|
||||
"denied_operations",
|
||||
}
|
||||
if set(request) != expected_fields:
|
||||
raise BootstrapError("REQUEST_FIELDS_REJECTED")
|
||||
if request["action"] != ACTION:
|
||||
raise BootstrapError("REQUEST_ACTION_REJECTED")
|
||||
if request["repository"] != "aios/aios":
|
||||
raise BootstrapError("REPOSITORY_REJECTED")
|
||||
if request["base_ref"] != "refs/heads/main":
|
||||
raise BootstrapError("BASE_REF_REJECTED")
|
||||
if not isinstance(request["base_oid"], str) or not OID_RE.fullmatch(request["base_oid"]):
|
||||
raise BootstrapError("BASE_OID_REJECTED")
|
||||
if (
|
||||
not isinstance(request["target_branch"], str)
|
||||
or not BRANCH_RE.fullmatch(request["target_branch"])
|
||||
):
|
||||
raise BootstrapError("TARGET_BRANCH_REJECTED")
|
||||
if (
|
||||
not isinstance(request["task_id"], str)
|
||||
or not TASK_RE.fullmatch(request["task_id"])
|
||||
):
|
||||
raise BootstrapError("TASK_ID_REJECTED")
|
||||
if request["one_time_use"] is not True:
|
||||
raise BootstrapError("ONE_TIME_USE_REQUIRED")
|
||||
expiry = request["expires_in_seconds"]
|
||||
if (
|
||||
not isinstance(expiry, int)
|
||||
or isinstance(expiry, bool)
|
||||
or not 60 <= expiry <= 900
|
||||
):
|
||||
raise BootstrapError("EXPIRY_REJECTED")
|
||||
for field in (
|
||||
"production_change_authorized",
|
||||
"tag_change_authorized",
|
||||
"baseline_change_authorized",
|
||||
"main_write_authorized",
|
||||
):
|
||||
if request[field] is not False:
|
||||
raise BootstrapError("CONSEQUENTIAL_CAPABILITY_REJECTED")
|
||||
if (
|
||||
not isinstance(request["allowed_operations"], list)
|
||||
or frozenset(request["allowed_operations"]) != EXPECTED_ALLOWED_OPERATIONS
|
||||
):
|
||||
raise BootstrapError("ALLOWED_OPERATIONS_MISMATCH")
|
||||
if (
|
||||
not isinstance(request["denied_operations"], list)
|
||||
or frozenset(request["denied_operations"]) != EXPECTED_DENIED_OPERATIONS
|
||||
):
|
||||
raise BootstrapError("DENIED_OPERATIONS_MISMATCH")
|
||||
return {
|
||||
"schema": BINDING_SCHEMA,
|
||||
"owner_identity": identity,
|
||||
"owner_role": "owner",
|
||||
"authenticated_scope": scope,
|
||||
"request": dict(request),
|
||||
}
|
||||
|
||||
|
||||
def _status_uids(pid: int) -> tuple[int, int, int, int]:
|
||||
try:
|
||||
lines = Path(f"/proc/{pid}/status").read_text(encoding="utf-8").splitlines()
|
||||
except OSError as exc:
|
||||
raise BootstrapError("PEER_STATUS_UNAVAILABLE") from exc
|
||||
for line in lines:
|
||||
if line.startswith("Uid:"):
|
||||
fields = line.split()[1:]
|
||||
if len(fields) != 4:
|
||||
break
|
||||
return tuple(int(item) for item in fields) # type: ignore[return-value]
|
||||
raise BootstrapError("PEER_UID_STATUS_INVALID")
|
||||
|
||||
|
||||
def _loginuid(pid: int) -> int:
|
||||
try:
|
||||
raw = Path(f"/proc/{pid}/loginuid").read_text(encoding="ascii").strip()
|
||||
value = int(raw)
|
||||
except (OSError, ValueError) as exc:
|
||||
raise BootstrapError("PEER_LOGINUID_UNAVAILABLE") from exc
|
||||
if value == 4294967295:
|
||||
raise BootstrapError("PEER_LOGIN_SESSION_REQUIRED")
|
||||
return value
|
||||
|
||||
|
||||
def _tty_path(pid: int) -> Path:
|
||||
try:
|
||||
value = os.readlink(f"/proc/{pid}/fd/0")
|
||||
except OSError as exc:
|
||||
raise BootstrapError("PEER_INTERACTIVE_TTY_REQUIRED") from exc
|
||||
if not (value.startswith("/dev/pts/") or value.startswith("/dev/tty")):
|
||||
raise BootstrapError("PEER_INTERACTIVE_TTY_REQUIRED")
|
||||
path = Path(value)
|
||||
try:
|
||||
if not stat.S_ISCHR(path.stat().st_mode):
|
||||
raise BootstrapError("PEER_INTERACTIVE_TTY_REQUIRED")
|
||||
except OSError as exc:
|
||||
raise BootstrapError("PEER_INTERACTIVE_TTY_REQUIRED") from exc
|
||||
return path
|
||||
|
||||
|
||||
def verify_peer_session(pid: int, uid: int, gid: int, owner_identity: str) -> dict[str, Any]:
|
||||
try:
|
||||
owner = pwd.getpwnam(owner_identity)
|
||||
except KeyError as exc:
|
||||
raise BootstrapError("OWNER_OS_ACCOUNT_MISSING") from exc
|
||||
if uid != owner.pw_uid:
|
||||
raise BootstrapError("PEER_OWNER_UID_MISMATCH")
|
||||
uids = _status_uids(pid)
|
||||
if any(item != owner.pw_uid for item in uids):
|
||||
raise BootstrapError("PEER_UID_TRANSITION_REJECTED")
|
||||
loginuid = _loginuid(pid)
|
||||
if loginuid != owner.pw_uid:
|
||||
# This deliberately rejects sudo/runuser from another authenticated
|
||||
# account; the session itself must have logged in as the Owner.
|
||||
raise BootstrapError("PEER_LOGINUID_OWNER_MISMATCH")
|
||||
tty = _tty_path(pid)
|
||||
if tty.stat().st_uid != owner.pw_uid:
|
||||
raise BootstrapError("PEER_TTY_OWNER_MISMATCH")
|
||||
return {
|
||||
"pid": pid,
|
||||
"uid": uid,
|
||||
"gid": gid,
|
||||
"loginuid": loginuid,
|
||||
"tty": str(tty),
|
||||
"owner_identity": owner_identity,
|
||||
}
|
||||
|
||||
|
||||
def read_frame(connection: socket.socket) -> dict[str, Any]:
|
||||
data = bytearray()
|
||||
while len(data) <= MAX_FRAME:
|
||||
block = connection.recv(4096)
|
||||
if not block:
|
||||
break
|
||||
data.extend(block)
|
||||
if b"\n" in block:
|
||||
break
|
||||
if not data or len(data) > MAX_FRAME:
|
||||
raise BootstrapError("PROTOCOL_FRAME_INVALID")
|
||||
raw = bytes(data).split(b"\n", 1)[0]
|
||||
try:
|
||||
value = json.loads(raw)
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise BootstrapError("PROTOCOL_JSON_INVALID") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise BootstrapError("PROTOCOL_JSON_INVALID")
|
||||
return value
|
||||
|
||||
|
||||
def write_frame(connection: socket.socket, value: Mapping[str, Any]) -> None:
|
||||
connection.sendall(canonical_json(dict(value)))
|
||||
|
||||
|
||||
def safe_result(value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
for key in value:
|
||||
lowered = str(key).lower()
|
||||
if any(fragment in lowered for fragment in SENSITIVE_FRAGMENTS):
|
||||
raise BootstrapError("BROKER_RESPONSE_SENSITIVE_DATA_REJECTED")
|
||||
allowed = {
|
||||
"status",
|
||||
"approval_plan_id",
|
||||
"grant_id",
|
||||
"grant_digest_sha256",
|
||||
"branch_oid",
|
||||
"branch_ref_created",
|
||||
"grant_consumed",
|
||||
"owner_authorization_event_id",
|
||||
"owner_authorization_reference",
|
||||
"owner_authorization_digest_sha256",
|
||||
"owner_authorization_consumed",
|
||||
"owner_authorization_idempotent",
|
||||
}
|
||||
return {key: value.get(key) for key in sorted(allowed) if key in value}
|
||||
|
||||
|
||||
def request_summary(binding: Mapping[str, Any]) -> dict[str, Any]:
|
||||
request = binding["request"]
|
||||
return {
|
||||
"owner_identity": binding["owner_identity"],
|
||||
"authenticated_scope": binding["authenticated_scope"],
|
||||
"task_id": request["task_id"],
|
||||
"repository": request["repository"],
|
||||
"base_ref": request["base_ref"],
|
||||
"base_oid": request["base_oid"],
|
||||
"target_branch": request["target_branch"],
|
||||
"expires_in_seconds": request["expires_in_seconds"],
|
||||
"allowed_operations": sorted(request["allowed_operations"]),
|
||||
"denied_operations": sorted(request["denied_operations"]),
|
||||
"production_change_authorized": False,
|
||||
"tag_change_authorized": False,
|
||||
"baseline_change_authorized": False,
|
||||
"main_write_authorized": False,
|
||||
}
|
||||
|
||||
|
||||
def _ensure_runtime_paths(socket_path: Path, state_root: Path, owner_identity: str) -> None:
|
||||
if os.geteuid() != 0:
|
||||
raise BootstrapError("SERVER_ROOT_REQUIRED")
|
||||
owner = pwd.getpwnam(owner_identity)
|
||||
for directory, mode, uid, gid in (
|
||||
(socket_path.parent, 0o750, 0, owner.pw_gid),
|
||||
(state_root, 0o700, 0, 0),
|
||||
):
|
||||
if directory.is_symlink():
|
||||
raise BootstrapError("RUNTIME_DIRECTORY_SYMLINK_REJECTED")
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
os.chown(directory, uid, gid)
|
||||
os.chmod(directory, mode)
|
||||
if socket_path.exists() or socket_path.is_symlink():
|
||||
raise BootstrapError("SOCKET_ALREADY_EXISTS")
|
||||
|
||||
|
||||
def _lock(path: Path):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
fd = os.open(path, os.O_RDWR | os.O_CREAT | getattr(os, "O_NOFOLLOW", 0), 0o600)
|
||||
os.fchmod(fd, 0o600)
|
||||
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
return fd
|
||||
|
||||
|
||||
def invoke_broker(binding: Mapping[str, Any]) -> dict[str, Any]:
|
||||
# Import only on the root server side. The bootstrap package does not carry
|
||||
# a second broker/provisioner implementation; it reuses current main.
|
||||
from owner_authorization_broker import ( # type: ignore
|
||||
OwnerAuthorizationBroker,
|
||||
OwnerAuthorizationBrokerConfig,
|
||||
OwnerPrincipal,
|
||||
)
|
||||
|
||||
config = OwnerAuthorizationBrokerConfig.from_environment()
|
||||
identity = str(binding["owner_identity"])
|
||||
scope = str(binding["authenticated_scope"])
|
||||
if identity not in config.allowed_owner_identities:
|
||||
raise BootstrapError("OWNER_IDENTITY_NOT_ALLOWED_BY_LIVE_BROKER")
|
||||
if scope not in config.allowed_intake_scopes:
|
||||
raise BootstrapError("OWNER_SCOPE_NOT_ALLOWED_BY_LIVE_BROKER")
|
||||
if config.max_expiry_seconds < int(binding["request"]["expires_in_seconds"]):
|
||||
raise BootstrapError("LIVE_BROKER_EXPIRY_TOO_SMALL")
|
||||
principal = OwnerPrincipal(
|
||||
identity=identity,
|
||||
role="owner",
|
||||
authenticated_scope=scope,
|
||||
token_scopes=frozenset({scope}),
|
||||
credential_id="",
|
||||
)
|
||||
return OwnerAuthorizationBroker(config=config).authorize_and_provision(
|
||||
binding["request"], principal
|
||||
)
|
||||
|
||||
|
||||
def serve(binding_path: Path, socket_path: Path, state_root: Path, timeout: int) -> int:
|
||||
binding = validate_binding(secure_root_file(binding_path))
|
||||
identity = str(binding["owner_identity"])
|
||||
_ensure_runtime_paths(socket_path, state_root, identity)
|
||||
lock_fd = _lock(state_root / "bootstrap.lock")
|
||||
listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
try:
|
||||
listener.bind(str(socket_path))
|
||||
owner = pwd.getpwnam(identity)
|
||||
os.chown(socket_path, 0, owner.pw_gid)
|
||||
os.chmod(socket_path, 0o660)
|
||||
listener.listen(1)
|
||||
listener.settimeout(timeout)
|
||||
|
||||
connection, _ = listener.accept()
|
||||
with connection:
|
||||
raw_peer = connection.getsockopt(
|
||||
socket.SOL_SOCKET, socket.SO_PEERCRED, struct.calcsize("3i")
|
||||
)
|
||||
pid, uid, gid = struct.unpack("3i", raw_peer)
|
||||
session = verify_peer_session(pid, uid, gid, identity)
|
||||
|
||||
begin = read_frame(connection)
|
||||
if begin != {"schema": CLIENT_BEGIN_SCHEMA, "action": "BEGIN"}:
|
||||
raise BootstrapError("CLIENT_BEGIN_REJECTED")
|
||||
|
||||
challenge = secrets.token_hex(12)
|
||||
challenge_issued = time.monotonic()
|
||||
summary = request_summary(binding)
|
||||
write_frame(
|
||||
connection,
|
||||
{
|
||||
"schema": CHALLENGE_SCHEMA,
|
||||
"challenge": challenge,
|
||||
"expires_in_seconds": CHALLENGE_TTL_SECONDS,
|
||||
"request_digest_sha256": sha256_json(binding["request"]),
|
||||
"summary": summary,
|
||||
},
|
||||
)
|
||||
|
||||
confirm = read_frame(connection)
|
||||
expected_confirmation = f"AUTHORIZE {challenge}"
|
||||
if (
|
||||
confirm.get("schema") != CLIENT_CONFIRM_SCHEMA
|
||||
or confirm.get("challenge") != challenge
|
||||
or confirm.get("confirmation") != expected_confirmation
|
||||
):
|
||||
raise BootstrapError("OWNER_INTERACTIVE_CONFIRMATION_REJECTED")
|
||||
if time.monotonic() - challenge_issued > CHALLENGE_TTL_SECONDS:
|
||||
raise BootstrapError("OWNER_CONFIRMATION_EXPIRED")
|
||||
|
||||
result = safe_result(invoke_broker(binding))
|
||||
receipt = {
|
||||
"schema": "aios.local-owner-auth-ingress-receipt.v1",
|
||||
"status": "CONSUMED",
|
||||
"owner_identity": identity,
|
||||
"owner_role": "owner",
|
||||
"authenticated_scope": binding["authenticated_scope"],
|
||||
"peer": session,
|
||||
"request_digest_sha256": sha256_json(binding["request"]),
|
||||
"binding_digest_sha256": sha256_json(binding),
|
||||
"challenge_digest_sha256": hashlib.sha256(
|
||||
challenge.encode("ascii")
|
||||
).hexdigest(),
|
||||
"broker": result,
|
||||
"completed_at_unix": int(time.time()),
|
||||
}
|
||||
event_id = str(
|
||||
result.get("owner_authorization_event_id")
|
||||
or hashlib.sha256(
|
||||
canonical_json(
|
||||
{
|
||||
"peer": session,
|
||||
"request": sha256_json(binding["request"]),
|
||||
}
|
||||
)
|
||||
).hexdigest()[:32]
|
||||
)
|
||||
atomic_json(state_root / "events" / f"{event_id}.json", receipt)
|
||||
write_frame(
|
||||
connection,
|
||||
{
|
||||
"schema": RESULT_SCHEMA,
|
||||
"ok": True,
|
||||
"result": result,
|
||||
},
|
||||
)
|
||||
return 0
|
||||
except (BootstrapError, TimeoutError, socket.timeout) as exc:
|
||||
code = exc.code if isinstance(exc, BootstrapError) else "OWNER_BOOTSTRAP_TIMEOUT"
|
||||
try:
|
||||
if "connection" in locals():
|
||||
write_frame(
|
||||
connection,
|
||||
{
|
||||
"schema": RESULT_SCHEMA,
|
||||
"ok": False,
|
||||
"error": code,
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
atomic_json(
|
||||
state_root / "last-failure.json",
|
||||
{
|
||||
"schema": "aios.local-owner-auth-ingress-failure.v1",
|
||||
"status": "FAILED",
|
||||
"error": code,
|
||||
"binding_digest_sha256": sha256_json(binding),
|
||||
"failed_at_unix": int(time.time()),
|
||||
},
|
||||
)
|
||||
return 2
|
||||
finally:
|
||||
listener.close()
|
||||
try:
|
||||
socket_path.unlink()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
try:
|
||||
fcntl.flock(lock_fd, fcntl.LOCK_UN)
|
||||
finally:
|
||||
os.close(lock_fd)
|
||||
|
||||
|
||||
def client(socket_path: Path) -> int:
|
||||
# The server performs the authoritative peer/session checks. The client
|
||||
# also refuses non-interactive execution to make accidental automation hard.
|
||||
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
||||
raise BootstrapError("CLIENT_INTERACTIVE_TTY_REQUIRED")
|
||||
connection = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
connection.connect(str(socket_path))
|
||||
with connection:
|
||||
write_frame(connection, {"schema": CLIENT_BEGIN_SCHEMA, "action": "BEGIN"})
|
||||
challenge = read_frame(connection)
|
||||
if challenge.get("schema") != CHALLENGE_SCHEMA:
|
||||
raise BootstrapError("CHALLENGE_INVALID")
|
||||
summary = challenge.get("summary")
|
||||
if not isinstance(summary, dict):
|
||||
raise BootstrapError("CHALLENGE_INVALID")
|
||||
|
||||
print("\nAIOS one-shot Owner authorization request:")
|
||||
print(json.dumps(summary, indent=2, sort_keys=True))
|
||||
print("\nThis authorizes only the task writer shown above.")
|
||||
phrase = f"AUTHORIZE {challenge['challenge']}"
|
||||
typed = input(f"Type exactly:\n{phrase}\n> ").strip()
|
||||
if typed != phrase:
|
||||
raise BootstrapError("OWNER_CONFIRMATION_MISMATCH")
|
||||
write_frame(
|
||||
connection,
|
||||
{
|
||||
"schema": CLIENT_CONFIRM_SCHEMA,
|
||||
"challenge": challenge["challenge"],
|
||||
"confirmation": typed,
|
||||
},
|
||||
)
|
||||
result = read_frame(connection)
|
||||
print(json.dumps(result, indent=2, sort_keys=True))
|
||||
return 0 if result.get("ok") is True else 3
|
||||
|
||||
|
||||
def parser() -> argparse.ArgumentParser:
|
||||
value = argparse.ArgumentParser()
|
||||
mode = value.add_mutually_exclusive_group(required=True)
|
||||
mode.add_argument("--serve", action="store_true")
|
||||
mode.add_argument("--client", action="store_true")
|
||||
value.add_argument("--binding", type=Path)
|
||||
value.add_argument(
|
||||
"--socket",
|
||||
type=Path,
|
||||
default=Path("/run/aios-owner-bootstrap/owner.sock"),
|
||||
)
|
||||
value.add_argument(
|
||||
"--state-root",
|
||||
type=Path,
|
||||
default=Path("/var/lib/aios-gitops/owner-local-auth-ingress"),
|
||||
)
|
||||
value.add_argument("--timeout", type=int, default=300)
|
||||
return value
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = parser().parse_args(argv)
|
||||
if args.serve:
|
||||
if args.binding is None:
|
||||
raise SystemExit("--binding is required with --serve")
|
||||
return serve(args.binding, args.socket, args.state_root, args.timeout)
|
||||
return client(args.socket)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except BootstrapError as exc:
|
||||
print(json.dumps({"status": "BLOCKED", "error": exc.code}), file=sys.stderr)
|
||||
raise SystemExit(exc.status)
|
||||
+410
@@ -0,0 +1,410 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Atomic Registry/Baseline finalizer for an already ACTIVE exact release.
|
||||
|
||||
This adapter never writes Git refs. It is intended for the AIOS runtime
|
||||
Registry and authoritative runtime-anchor Baseline only.
|
||||
|
||||
Registry is finalized before Baseline. Baseline apply independently verifies:
|
||||
- exact Authority annotated tag and peeled target
|
||||
- exact ACTIVE production health
|
||||
- exact V2R1C7 Registry record
|
||||
- exact previous Baseline file SHA-256 (CAS)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping
|
||||
|
||||
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
SHA_RE = re.compile(r"^[0-9a-f]{64}$")
|
||||
PLAN_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as source:
|
||||
for block in iter(lambda: source.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def atomic_json(path: Path, value: Mapping[str, Any], mode: int = 0o640) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
# Existing runtime-authority files may be root:gitops and read-only to
|
||||
# non-root readers. Preserve their owner/group/mode across os.replace().
|
||||
if path.exists():
|
||||
metadata = path.lstat()
|
||||
if not stat.S_ISREG(metadata.st_mode) or path.is_symlink():
|
||||
raise RuntimeError("ATOMIC_TARGET_NOT_REGULAR")
|
||||
target_mode = stat.S_IMODE(metadata.st_mode)
|
||||
target_uid = metadata.st_uid
|
||||
target_gid = metadata.st_gid
|
||||
else:
|
||||
parent = path.parent.stat()
|
||||
target_mode = mode
|
||||
target_uid = 0 if os.geteuid() == 0 else os.geteuid()
|
||||
target_gid = parent.st_gid
|
||||
|
||||
fd, name = tempfile.mkstemp(prefix=f".{path.name}.", dir=str(path.parent))
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as output:
|
||||
json.dump(dict(value), output, sort_keys=True, indent=2)
|
||||
output.write("\n")
|
||||
output.flush()
|
||||
os.fsync(output.fileno())
|
||||
os.chmod(name, target_mode)
|
||||
if os.geteuid() == 0:
|
||||
os.chown(name, target_uid, target_gid)
|
||||
os.replace(name, path)
|
||||
dfd = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
|
||||
try:
|
||||
os.fsync(dfd)
|
||||
finally:
|
||||
os.close(dfd)
|
||||
finally:
|
||||
try:
|
||||
os.unlink(name)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def load_request() -> dict[str, Any]:
|
||||
value = json.load(sys.stdin)
|
||||
if not isinstance(value, dict) or value.get("schema") != "aios.production-executor-request.v1":
|
||||
raise ValueError("request_schema_invalid")
|
||||
if value.get("repository_identity") != "aios/aios":
|
||||
raise ValueError("repository_identity_invalid")
|
||||
if not re.fullmatch(r"V\d+R\d+C\d+(?:-[A-Za-z0-9_.-]+)?", str(value.get("version", ""))):
|
||||
raise ValueError("version_invalid")
|
||||
for name in ("target_oid", "evidence_closure_oid"):
|
||||
if not OID_RE.fullmatch(str(value.get(name, ""))):
|
||||
raise ValueError(f"{name}_invalid")
|
||||
if not SHA_RE.fullmatch(str(value.get("artifact_sha256", ""))):
|
||||
raise ValueError("artifact_sha256_invalid")
|
||||
if not PLAN_RE.fullmatch(str(value.get("approval_plan_id", ""))):
|
||||
raise ValueError("approval_plan_id_invalid")
|
||||
if "tag_oid" in value and not OID_RE.fullmatch(str(value["tag_oid"])):
|
||||
raise ValueError("tag_oid_invalid")
|
||||
return value
|
||||
|
||||
|
||||
def exact_response(value: Mapping[str, Any], status: str, **extra: Any) -> dict[str, Any]:
|
||||
return {
|
||||
"status": status,
|
||||
"version": value["version"],
|
||||
"target_oid": value["target_oid"],
|
||||
"evidence_closure_oid": value["evidence_closure_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"approval_plan_id": value["approval_plan_id"],
|
||||
**extra,
|
||||
}
|
||||
|
||||
|
||||
def git(repository: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(
|
||||
["git", f"--git-dir={repository}", *args],
|
||||
check=check,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
|
||||
|
||||
def verify_local_tag(repository: Path, value: Mapping[str, Any]) -> tuple[str, str]:
|
||||
tag_oid = git(repository, "rev-parse", "--verify", f"refs/tags/{value['version']}").stdout.strip()
|
||||
peeled = git(repository, "rev-parse", "--verify", f"refs/tags/{value['version']}^{{commit}}").stdout.strip()
|
||||
if tag_oid == peeled or peeled != value["target_oid"]:
|
||||
raise RuntimeError("FORMAL_TAG_BINDING_MISMATCH")
|
||||
requested = value.get("tag_oid")
|
||||
if requested and requested != tag_oid:
|
||||
raise RuntimeError("FORMAL_TAG_OBJECT_MISMATCH")
|
||||
return tag_oid, peeled
|
||||
|
||||
|
||||
def health(url: str, value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=5) as response:
|
||||
body = json.loads(response.read().decode("utf-8"))
|
||||
status = response.status
|
||||
except (urllib.error.URLError, TimeoutError, ValueError) as exc:
|
||||
raise RuntimeError("PRODUCTION_HEALTH_UNAVAILABLE") from exc
|
||||
if not (
|
||||
status == 200
|
||||
and body.get("version") == value["version"]
|
||||
and body.get("commit_oid") == value["target_oid"]
|
||||
and body.get("artifact_sha256") == value["artifact_sha256"]
|
||||
and body.get("deployment_state") == "ACTIVE"
|
||||
and body.get("production_deployed") is True
|
||||
):
|
||||
raise RuntimeError("PRODUCTION_HEALTH_BINDING_MISMATCH")
|
||||
return body
|
||||
|
||||
|
||||
def registry_record(value: Mapping[str, Any], tag_oid: str, production_health: Mapping[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"schema": "aios.production-registry.v2",
|
||||
"version": value["version"],
|
||||
"repository": "aios/aios",
|
||||
"target_oid": value["target_oid"],
|
||||
"tag": value["version"],
|
||||
"tag_oid": tag_oid,
|
||||
"tag_object": tag_oid,
|
||||
"peeled_target_oid": value["target_oid"],
|
||||
"evidence_closure_oid": value["evidence_closure_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"approval_plan_id": value["approval_plan_id"],
|
||||
"production_service": value["production_service"],
|
||||
"production_port": value["production_port"],
|
||||
"registered": True,
|
||||
"enabled": True,
|
||||
"production_deployed": True,
|
||||
"status": "PRODUCTION_DEPLOYED",
|
||||
"deployment_state": production_health["deployment_state"],
|
||||
"updated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
}
|
||||
|
||||
|
||||
def registry_matches(record: Mapping[str, Any], value: Mapping[str, Any], tag_oid: str) -> bool:
|
||||
return all(
|
||||
record.get(key) == expected
|
||||
for key, expected in {
|
||||
"version": value["version"],
|
||||
"target_oid": value["target_oid"],
|
||||
"tag_oid": tag_oid,
|
||||
"evidence_closure_oid": value["evidence_closure_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"approval_plan_id": value["approval_plan_id"],
|
||||
"production_service": value["production_service"],
|
||||
"production_port": value["production_port"],
|
||||
"production_deployed": True,
|
||||
}.items()
|
||||
) and record.get("registered") is True and record.get("enabled") is True
|
||||
|
||||
|
||||
def baseline_matches(record: Mapping[str, Any], value: Mapping[str, Any], tag_oid: str) -> bool:
|
||||
current = record.get("current_baseline") or record.get("version")
|
||||
return current == value["version"] and all(
|
||||
record.get(key) == expected
|
||||
for key, expected in {
|
||||
"target_oid": value["target_oid"],
|
||||
"tag_oid": tag_oid,
|
||||
"evidence_closure_oid": value["evidence_closure_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"approval_plan_id": value["approval_plan_id"],
|
||||
"production_service": value["production_service"],
|
||||
"production_port": value["production_port"],
|
||||
"status": "PRODUCTION_DEPLOYED",
|
||||
}.items()
|
||||
)
|
||||
|
||||
|
||||
def registry_probe(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
tag_oid, _ = verify_local_tag(args.repository, value)
|
||||
path = args.registry_root / f"{value['version']}.json"
|
||||
if not path.exists():
|
||||
return exact_response(value, "NOT_APPLIED", tag_oid=tag_oid)
|
||||
record = json.loads(path.read_text(encoding="utf-8"))
|
||||
if registry_matches(record, value, tag_oid):
|
||||
return exact_response(
|
||||
value,
|
||||
"VERIFIED",
|
||||
tag_oid=tag_oid,
|
||||
registry_path=str(path),
|
||||
registry_sha256=sha256_file(path),
|
||||
)
|
||||
return exact_response(value, "CONFLICT", tag_oid=tag_oid, reason="registry_binding_mismatch")
|
||||
|
||||
|
||||
def registry_apply(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
existing = registry_probe(args, value)
|
||||
if existing["status"] in {"VERIFIED", "CONFLICT"}:
|
||||
return existing
|
||||
tag_oid, _ = verify_local_tag(args.repository, value)
|
||||
production_health = health(args.health_url, value)
|
||||
path = args.registry_root / f"{value['version']}.json"
|
||||
atomic_json(path, registry_record(value, tag_oid, production_health))
|
||||
return registry_probe(args, value)
|
||||
|
||||
|
||||
def secure_baseline_authority_file(path: Path) -> None:
|
||||
if not path.is_file() or path.is_symlink():
|
||||
raise RuntimeError("BASELINE_AUTHORITY_FILE_INVALID")
|
||||
baseline_stat = path.lstat()
|
||||
if baseline_stat.st_uid != 0 or baseline_stat.st_mode & 0o022:
|
||||
raise RuntimeError("BASELINE_AUTHORITY_PERMISSIONS_INVALID")
|
||||
|
||||
|
||||
def baseline_probe(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
tag_oid, _ = verify_local_tag(args.repository, value)
|
||||
secure_baseline_authority_file(args.baseline_path)
|
||||
record = json.loads(args.baseline_path.read_text(encoding="utf-8"))
|
||||
if baseline_matches(record, value, tag_oid):
|
||||
return exact_response(
|
||||
value,
|
||||
"VERIFIED",
|
||||
tag_oid=tag_oid,
|
||||
baseline_path=str(args.baseline_path),
|
||||
baseline_sha256=sha256_file(args.baseline_path),
|
||||
)
|
||||
current = record.get("current_baseline") or record.get("version")
|
||||
current_sha = sha256_file(args.baseline_path)
|
||||
if (
|
||||
current == args.expected_previous_baseline
|
||||
and current_sha == args.expected_previous_baseline_sha256
|
||||
):
|
||||
return exact_response(
|
||||
value,
|
||||
"NOT_APPLIED",
|
||||
tag_oid=tag_oid,
|
||||
previous_baseline=current,
|
||||
previous_baseline_sha256=current_sha,
|
||||
)
|
||||
return exact_response(
|
||||
value,
|
||||
"CONFLICT",
|
||||
tag_oid=tag_oid,
|
||||
reason="baseline_precondition_or_binding_mismatch",
|
||||
observed_baseline=current,
|
||||
observed_sha256=current_sha,
|
||||
)
|
||||
|
||||
|
||||
def baseline_apply(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
existing = baseline_probe(args, value)
|
||||
if existing["status"] in {"VERIFIED", "CONFLICT"}:
|
||||
return existing
|
||||
|
||||
tag_oid, _ = verify_local_tag(args.repository, value)
|
||||
production_health = health(args.health_url, value)
|
||||
|
||||
registry_path = args.registry_root / f"{value['version']}.json"
|
||||
if not registry_path.is_file():
|
||||
raise RuntimeError("REGISTRY_NOT_FINALIZED")
|
||||
registry = json.loads(registry_path.read_text(encoding="utf-8"))
|
||||
if not registry_matches(registry, value, tag_oid):
|
||||
raise RuntimeError("REGISTRY_BINDING_NOT_VERIFIED")
|
||||
|
||||
previous = json.loads(args.baseline_path.read_text(encoding="utf-8"))
|
||||
if sha256_file(args.baseline_path) != args.expected_previous_baseline_sha256:
|
||||
raise RuntimeError("BASELINE_CAS_MISMATCH")
|
||||
|
||||
generation = max(
|
||||
int(previous.get("generation", 0) or 0),
|
||||
int(previous.get("authority_generation", 0) or 0),
|
||||
) + 1
|
||||
record = {
|
||||
"schema": "aios.production-baseline.v2",
|
||||
"source_identity": "aios-gitops-runtime-anchor-baseline-authority-v1",
|
||||
"current_baseline": value["version"],
|
||||
"version": value["version"],
|
||||
"previous_baseline": args.expected_previous_baseline,
|
||||
"previous_version": args.expected_previous_baseline,
|
||||
"status": "PRODUCTION_DEPLOYED",
|
||||
"target_oid": value["target_oid"],
|
||||
"peeled_target_oid": value["target_oid"],
|
||||
"tag_oid": tag_oid,
|
||||
"evidence_closure_oid": value["evidence_closure_oid"],
|
||||
"approval_plan_id": value["approval_plan_id"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"production_artifact_sha256": value["artifact_sha256"],
|
||||
"production_service": value["production_service"],
|
||||
"production_port": value["production_port"],
|
||||
"production_deployed": True,
|
||||
"deployment_state": production_health["deployment_state"],
|
||||
"registry_digest": sha256_file(registry_path),
|
||||
"registry_source": str(registry_path),
|
||||
"generation": generation,
|
||||
"authority_generation": generation,
|
||||
# The actual previously deployed production baseline is the rollback
|
||||
# target. V2R1C6 has a formal tag but was never production-deployed.
|
||||
"rollback_version": args.expected_previous_baseline,
|
||||
"rollback_tag_oid": previous.get("tag_oid"),
|
||||
"rollback_target_oid": previous.get("target_oid"),
|
||||
"promoted_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
"promoted_by": "aios-approved-production-release-executor",
|
||||
}
|
||||
atomic_json(args.baseline_path, record)
|
||||
return baseline_probe(args, value)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--mode",
|
||||
choices=("registry", "baseline"),
|
||||
default=os.environ.get("AIOS_RELEASE_METADATA_MODE"),
|
||||
required=os.environ.get("AIOS_RELEASE_METADATA_MODE") is None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--repository",
|
||||
type=Path,
|
||||
default=Path(os.environ["AIOS_GITOPS_AUTHORITATIVE_GIT_DIR"]),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--registry-root",
|
||||
type=Path,
|
||||
default=Path(os.environ["AIOS_RUNTIME_REGISTRY_ROOT"]),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--baseline-path",
|
||||
type=Path,
|
||||
default=Path(os.environ["AIOS_RUNTIME_BASELINE_PATH"]),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--expected-previous-baseline",
|
||||
default=os.environ.get("AIOS_PREVIOUS_BASELINE", "V2R1C5"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--expected-previous-baseline-sha256",
|
||||
default=os.environ.get("AIOS_PREVIOUS_BASELINE_SHA256"),
|
||||
required=os.environ.get("AIOS_PREVIOUS_BASELINE_SHA256") is None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--health-url",
|
||||
default=os.environ.get("AIOS_RELEASE_HEALTH_URL", "http://127.0.0.1:18927/health"),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
value = load_request()
|
||||
operation = value.get("operation")
|
||||
if args.mode == "registry":
|
||||
if operation == "probe":
|
||||
result = registry_probe(args, value)
|
||||
elif operation == "apply":
|
||||
result = registry_apply(args, value)
|
||||
else:
|
||||
raise ValueError("unsupported_operation")
|
||||
else:
|
||||
if operation == "probe":
|
||||
result = baseline_probe(args, value)
|
||||
elif operation == "apply":
|
||||
result = baseline_apply(args, value)
|
||||
else:
|
||||
raise ValueError("unsupported_operation")
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as exc:
|
||||
print(
|
||||
json.dumps(
|
||||
{"status": "FAIL_CLOSED", "error": type(exc).__name__, "reason": str(exc)},
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
raise SystemExit(2)
|
||||
+455
@@ -0,0 +1,455 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Concrete V2R1C7 production deployment adapter.
|
||||
|
||||
The adapter consumes only an already approved immutable artifact. It never
|
||||
writes Git refs, Registry, or Baseline. A rollback only tears down the V2R1C7
|
||||
runtime and leaves V2R1C5/V2R1C6 untouched.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import grp
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import pwd
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping
|
||||
|
||||
VERSION = "V2R1C7"
|
||||
SERVICE = "aios-v2r1c7-production.service"
|
||||
PORT = 18927
|
||||
RUNTIME_USER = "aios-v2r1c7"
|
||||
RUNTIME_GROUP = "aios-v2r1c7"
|
||||
|
||||
REQUIRED_MEMBERS = {
|
||||
"payload/VERSION",
|
||||
"payload/active-runtime/app/deployment_state.py",
|
||||
"payload/active-runtime/app/v2r1c6_privacy_tools.py",
|
||||
"payload/active-runtime/app/v2r1c7_health_service.py",
|
||||
"payload/deploy/aios-v2/systemd/aios-v2r1c7-production.service",
|
||||
"payload/deploy/aios-v2/v2r1c7-runtime-config.json",
|
||||
"payload/deploy/aios-v2/v2r1c7-rollback.json",
|
||||
"payload/scripts/probe_v2r1c7_health.py",
|
||||
"payload/scripts/rollback_v2r1c7_release.sh",
|
||||
"payload/BUILD-METADATA.json",
|
||||
"payload/ARTIFACT-MANIFEST.json",
|
||||
"payload/ROLLBACK-METADATA.json",
|
||||
}
|
||||
|
||||
|
||||
def load_request() -> dict[str, Any]:
|
||||
value = json.load(sys.stdin)
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError("request_object_required")
|
||||
if value.get("schema") != "aios.production-executor-request.v1":
|
||||
raise ValueError("request_schema_invalid")
|
||||
if value.get("version") != VERSION:
|
||||
raise ValueError("version_mismatch")
|
||||
if value.get("production_service") != SERVICE:
|
||||
raise ValueError("service_mismatch")
|
||||
if int(value.get("production_port", -1)) != PORT:
|
||||
raise ValueError("port_mismatch")
|
||||
return value
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise ValueError("artifact_regular_file_required")
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def atomic_json(path: Path, value: Mapping[str, Any], *, mode: int, gid: int | None = None) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = path.with_name(path.name + ".tmp")
|
||||
temporary.write_text(
|
||||
json.dumps(dict(value), sort_keys=True, separators=(",", ":")) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
os.chmod(temporary, mode)
|
||||
if gid is not None:
|
||||
os.chown(temporary, 0, gid)
|
||||
os.replace(temporary, path)
|
||||
|
||||
|
||||
def atomic_text(path: Path, text: str, *, mode: int, gid: int | None = None) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = path.with_name(path.name + ".tmp")
|
||||
temporary.write_text(text, encoding="utf-8")
|
||||
os.chmod(temporary, mode)
|
||||
if gid is not None:
|
||||
os.chown(temporary, 0, gid)
|
||||
os.replace(temporary, path)
|
||||
|
||||
|
||||
def systemctl(*args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(
|
||||
["systemctl", *args],
|
||||
check=check,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=45,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
|
||||
|
||||
def identity() -> tuple[int, int]:
|
||||
try:
|
||||
user = pwd.getpwnam(RUNTIME_USER)
|
||||
group = grp.getgrnam(RUNTIME_GROUP)
|
||||
except KeyError as exc:
|
||||
raise RuntimeError("RUNTIME_IDENTITY_MISSING") from exc
|
||||
if user.pw_gid != group.gr_gid:
|
||||
raise RuntimeError("RUNTIME_IDENTITY_GROUP_MISMATCH")
|
||||
return user.pw_uid, group.gr_gid
|
||||
|
||||
|
||||
def safe_extract(artifact: Path, destination: Path) -> set[str]:
|
||||
with tempfile.TemporaryDirectory(prefix="aios-v2r1c7-unpack-") as temp:
|
||||
tar_path = Path(temp) / "release.tar"
|
||||
with tar_path.open("wb") as output:
|
||||
completed = subprocess.run(
|
||||
["zstd", "-dc", str(artifact)],
|
||||
check=False,
|
||||
stdout=output,
|
||||
stderr=subprocess.PIPE,
|
||||
timeout=60,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise RuntimeError("ARTIFACT_DECOMPRESSION_FAILED")
|
||||
with tarfile.open(tar_path, "r:") as archive:
|
||||
members = archive.getmembers()
|
||||
names = {m.name for m in members}
|
||||
missing = REQUIRED_MEMBERS - names
|
||||
if missing:
|
||||
raise RuntimeError("ARTIFACT_MEMBERS_MISSING:" + ",".join(sorted(missing)))
|
||||
for member in members:
|
||||
relative = Path(member.name)
|
||||
if (
|
||||
relative.is_absolute()
|
||||
or ".." in relative.parts
|
||||
or member.issym()
|
||||
or member.islnk()
|
||||
or not (member.isfile() or member.isdir())
|
||||
):
|
||||
raise RuntimeError("UNSAFE_ARTIFACT_MEMBER:" + member.name)
|
||||
if not member.name.startswith("payload/"):
|
||||
raise RuntimeError("UNEXPECTED_ARTIFACT_ROOT:" + member.name)
|
||||
payload = destination
|
||||
payload.mkdir(parents=True, exist_ok=True)
|
||||
for member in members:
|
||||
rel = Path(member.name).relative_to("payload")
|
||||
if str(rel) == ".":
|
||||
continue
|
||||
target = payload / rel
|
||||
resolved_parent = target.parent.resolve()
|
||||
resolved_parent.relative_to(payload.resolve())
|
||||
if member.isdir():
|
||||
target.mkdir(parents=True, exist_ok=True)
|
||||
os.chmod(target, 0o755)
|
||||
continue
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
source = archive.extractfile(member)
|
||||
if source is None:
|
||||
raise RuntimeError("ARTIFACT_MEMBER_READ_FAILED")
|
||||
with target.open("wb") as output:
|
||||
shutil.copyfileobj(source, output)
|
||||
# Scripts must be executable; all other payload files are read-only.
|
||||
mode = 0o755 if rel.parts and rel.parts[0] == "scripts" else 0o644
|
||||
os.chmod(target, mode)
|
||||
return names
|
||||
|
||||
|
||||
def health(url: str, expected: Mapping[str, Any]) -> dict[str, Any]:
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=5) as response:
|
||||
body = json.loads(response.read().decode("utf-8"))
|
||||
status = response.status
|
||||
except (urllib.error.URLError, TimeoutError, ValueError):
|
||||
return {"status": "UNAVAILABLE"}
|
||||
exact = (
|
||||
status == 200
|
||||
and body.get("version") == expected["version"]
|
||||
and body.get("commit_oid") == expected["target_oid"]
|
||||
and body.get("artifact_sha256") == expected["artifact_sha256"]
|
||||
and body.get("deployment_state") == "ACTIVE"
|
||||
and body.get("production_deployed") is True
|
||||
)
|
||||
if not exact:
|
||||
return {
|
||||
"status": "CONFLICT",
|
||||
"http_status": status,
|
||||
"version": body.get("version"),
|
||||
"target_oid": body.get("commit_oid"),
|
||||
"artifact_sha256": body.get("artifact_sha256"),
|
||||
"deployment_state": body.get("deployment_state"),
|
||||
"production_deployed": body.get("production_deployed"),
|
||||
}
|
||||
return {
|
||||
"status": "ACTIVE_VERIFIED",
|
||||
"http_status": 200,
|
||||
"version": body["version"],
|
||||
"target_oid": body["commit_oid"],
|
||||
"artifact_sha256": body["artifact_sha256"],
|
||||
"deployment_state": body["deployment_state"],
|
||||
"production_deployed": True,
|
||||
}
|
||||
|
||||
|
||||
def paths(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Path]:
|
||||
release_id = (
|
||||
f"{VERSION}-{str(value['target_oid'])[:12]}-"
|
||||
f"{str(value['artifact_sha256'])[:12]}"
|
||||
)
|
||||
return {
|
||||
"release_dir": args.release_root / release_id,
|
||||
"current_link": args.current_link,
|
||||
"env_file": args.env_file,
|
||||
"state_file": args.state_file,
|
||||
"unit_file": args.unit_file,
|
||||
}
|
||||
|
||||
|
||||
def probe(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
p = paths(args, value)
|
||||
url = f"http://127.0.0.1:{PORT}/health"
|
||||
active = systemctl("is-active", "--quiet", SERVICE, check=False).returncode == 0
|
||||
if not active:
|
||||
# A stale exact release directory is safe to reuse; it is not a deployment.
|
||||
return {
|
||||
"status": "NOT_APPLIED",
|
||||
"version": VERSION,
|
||||
"target_oid": value["target_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"production_deployed": False,
|
||||
"deployment_state": "ROLLED_BACK"
|
||||
if p["state_file"].exists()
|
||||
and "ROLLED_BACK" in p["state_file"].read_text(encoding="utf-8")
|
||||
else "ABSENT",
|
||||
}
|
||||
result = health(url, value)
|
||||
if result["status"] == "UNAVAILABLE":
|
||||
return {
|
||||
"status": "CONFLICT",
|
||||
"version": VERSION,
|
||||
"target_oid": value["target_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"reason": "service_active_health_unavailable",
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def install_release(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
_, gid = identity()
|
||||
artifact = Path(str(value["artifact_path"]))
|
||||
if artifact.stat().st_size != int(value["artifact_size"]):
|
||||
raise RuntimeError("ARTIFACT_SIZE_MISMATCH")
|
||||
if sha256_file(artifact) != value["artifact_sha256"]:
|
||||
raise RuntimeError("ARTIFACT_DIGEST_MISMATCH")
|
||||
|
||||
p = paths(args, value)
|
||||
release_dir = p["release_dir"]
|
||||
marker = release_dir / "DEPLOYMENT-BINDING.json"
|
||||
marker_value = {
|
||||
"schema": "aios.runtime-release-binding.v1",
|
||||
"version": VERSION,
|
||||
"target_oid": value["target_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"approval_plan_id": value["approval_plan_id"],
|
||||
}
|
||||
|
||||
if release_dir.exists():
|
||||
if not marker.is_file():
|
||||
raise RuntimeError("EXISTING_RELEASE_DIR_UNTRUSTED")
|
||||
current_marker = json.loads(marker.read_text(encoding="utf-8"))
|
||||
if current_marker != marker_value:
|
||||
raise RuntimeError("EXISTING_RELEASE_DIR_BINDING_CONFLICT")
|
||||
else:
|
||||
release_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||
temp_dir = Path(
|
||||
tempfile.mkdtemp(prefix=release_dir.name + ".", dir=str(release_dir.parent))
|
||||
)
|
||||
try:
|
||||
safe_extract(artifact, temp_dir)
|
||||
atomic_json(
|
||||
temp_dir / "DEPLOYMENT-BINDING.json",
|
||||
marker_value,
|
||||
mode=0o644,
|
||||
)
|
||||
os.rename(temp_dir, release_dir)
|
||||
except Exception:
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
raise
|
||||
|
||||
if (release_dir / "VERSION").read_text(encoding="utf-8").strip() != VERSION:
|
||||
raise RuntimeError("RELEASE_VERSION_FILE_MISMATCH")
|
||||
|
||||
# Only replace our dedicated V2R1C7 link. Never touch previous versions.
|
||||
current_link = p["current_link"]
|
||||
if current_link.exists() and not current_link.is_symlink():
|
||||
raise RuntimeError("CURRENT_RELEASE_PATH_NOT_SYMLINK")
|
||||
if current_link.is_symlink():
|
||||
existing = current_link.resolve()
|
||||
if existing != release_dir.resolve():
|
||||
raise RuntimeError("CURRENT_RELEASE_LINK_CONFLICT")
|
||||
else:
|
||||
current_link.parent.mkdir(parents=True, exist_ok=True)
|
||||
new_link = current_link.with_name(current_link.name + ".new")
|
||||
try:
|
||||
new_link.unlink()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
new_link.symlink_to(release_dir)
|
||||
os.replace(new_link, current_link)
|
||||
|
||||
state_value = {
|
||||
"version": VERSION,
|
||||
"commit_oid": value["target_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"deployment_state": "ACTIVE",
|
||||
}
|
||||
atomic_json(p["state_file"], state_value, mode=0o640, gid=gid)
|
||||
env = (
|
||||
f"RELEASE_VERSION={VERSION}\n"
|
||||
f"TARGET_OID={value['target_oid']}\n"
|
||||
f"ARTIFACT_SHA256={value['artifact_sha256']}\n"
|
||||
"DEPLOYMENT_STATE=ACTIVE\n"
|
||||
f"AIOS_V2R1C7_PORT={PORT}\n"
|
||||
f"DEPLOYMENT_STATE_FILE={p['state_file']}\n"
|
||||
"AIOS_GITOPS_WRITES_ENABLED=false\n"
|
||||
)
|
||||
atomic_text(p["env_file"], env, mode=0o640, gid=gid)
|
||||
|
||||
unit_source = (
|
||||
release_dir
|
||||
/ "deploy"
|
||||
/ "aios-v2"
|
||||
/ "systemd"
|
||||
/ "aios-v2r1c7-production.service"
|
||||
)
|
||||
unit_text = unit_source.read_text(encoding="utf-8")
|
||||
if "aios-v2r1c7-production.service" in unit_text:
|
||||
# Description text is irrelevant; service identity is the destination.
|
||||
pass
|
||||
atomic_text(p["unit_file"], unit_text, mode=0o644)
|
||||
|
||||
verify = subprocess.run(
|
||||
["systemd-analyze", "verify", str(p["unit_file"])],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
if verify.returncode != 0:
|
||||
raise RuntimeError("PRODUCTION_SYSTEMD_VERIFY_FAILED")
|
||||
|
||||
systemctl("daemon-reload")
|
||||
systemctl("enable", SERVICE)
|
||||
systemctl("restart", SERVICE)
|
||||
|
||||
deadline = time.time() + args.health_timeout
|
||||
result = {"status": "UNAVAILABLE"}
|
||||
while time.time() < deadline:
|
||||
result = health(f"http://127.0.0.1:{PORT}/health", value)
|
||||
if result["status"] == "ACTIVE_VERIFIED":
|
||||
break
|
||||
if result["status"] == "CONFLICT":
|
||||
break
|
||||
time.sleep(1)
|
||||
if result["status"] != "ACTIVE_VERIFIED":
|
||||
raise RuntimeError("PRODUCTION_HEALTH_VERIFY_FAILED")
|
||||
|
||||
# Restart recovery is part of production acceptance, not just staging.
|
||||
systemctl("restart", SERVICE)
|
||||
deadline = time.time() + args.health_timeout
|
||||
while time.time() < deadline:
|
||||
result = health(f"http://127.0.0.1:{PORT}/health", value)
|
||||
if result["status"] == "ACTIVE_VERIFIED":
|
||||
break
|
||||
if result["status"] == "CONFLICT":
|
||||
break
|
||||
time.sleep(1)
|
||||
if result["status"] != "ACTIVE_VERIFIED":
|
||||
raise RuntimeError("PRODUCTION_RESTART_RECOVERY_FAILED")
|
||||
return result
|
||||
|
||||
|
||||
def rollback(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
_, gid = identity()
|
||||
p = paths(args, value)
|
||||
systemctl("stop", SERVICE, check=False)
|
||||
systemctl("disable", SERVICE, check=False)
|
||||
state = {
|
||||
"version": VERSION,
|
||||
"commit_oid": value["target_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"deployment_state": "ROLLED_BACK",
|
||||
}
|
||||
atomic_json(p["state_file"], state, mode=0o640, gid=gid)
|
||||
return {
|
||||
"status": "ROLLED_BACK",
|
||||
"version": VERSION,
|
||||
"target_oid": value["target_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"deployment_state": "ROLLED_BACK",
|
||||
"production_deployed": False,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--release-root", type=Path, default=Path("/opt/aios/releases"))
|
||||
p.add_argument("--current-link", type=Path, default=Path("/opt/aios/v2r1c7"))
|
||||
p.add_argument("--env-file", type=Path, default=Path("/etc/aios/aios-v2r1c7.env"))
|
||||
p.add_argument(
|
||||
"--state-file",
|
||||
type=Path,
|
||||
default=Path("/var/lib/aios/v2r1c7/deployment-state.json"),
|
||||
)
|
||||
p.add_argument(
|
||||
"--unit-file",
|
||||
type=Path,
|
||||
default=Path("/etc/systemd/system/aios-v2r1c7-production.service"),
|
||||
)
|
||||
p.add_argument("--health-timeout", type=int, default=30)
|
||||
args = p.parse_args()
|
||||
value = load_request()
|
||||
operation = value.get("operation")
|
||||
if operation == "probe":
|
||||
result = probe(args, value)
|
||||
elif operation == "apply":
|
||||
result = install_release(args, value)
|
||||
elif operation == "rollback":
|
||||
result = rollback(args, value)
|
||||
else:
|
||||
raise ValueError("unsupported_operation")
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as exc:
|
||||
print(
|
||||
json.dumps(
|
||||
{"status": "FAIL_CLOSED", "error": type(exc).__name__, "reason": str(exc)},
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
raise SystemExit(2)
|
||||
@@ -0,0 +1,38 @@
|
||||
[Unit]
|
||||
Description=AIOS governed approved production release executor
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=root
|
||||
Group=root
|
||||
UMask=0077
|
||||
Environment=PYTHONDONTWRITEBYTECODE=1
|
||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||
EnvironmentFile=/etc/aios-gitops/approved-release-executor.env
|
||||
ExecStart=/usr/bin/python3 /usr/libexec/aios-approved-release-executor/current/active-runtime/app/approved_release_executor.py \
|
||||
--authorization ${AIOS_RELEASE_AUTHORIZATION} \
|
||||
--repository ${AIOS_GITOPS_AUTHORITATIVE_GIT_DIR} \
|
||||
--state-root ${AIOS_RELEASE_EXECUTION_STATE_ROOT} \
|
||||
--artifact ${AIOS_RELEASE_ARTIFACT} \
|
||||
--artifact-size ${AIOS_RELEASE_ARTIFACT_SIZE} \
|
||||
--version ${AIOS_RELEASE_VERSION} \
|
||||
--target-oid ${AIOS_RELEASE_TARGET_OID} \
|
||||
--evidence-closure-oid ${AIOS_RELEASE_EVIDENCE_CLOSURE_OID} \
|
||||
--artifact-sha256 ${AIOS_RELEASE_ARTIFACT_SHA256} \
|
||||
--approval-plan-id ${AIOS_RELEASE_APPROVAL_PLAN_ID} \
|
||||
--production-service aios-v2r1c7-production.service \
|
||||
--production-port 18927 \
|
||||
--tag-adapter /usr/bin/python3 /usr/libexec/aios-approved-release-executor/current/active-runtime/app/governed_tag_adapter.py \
|
||||
--deploy-adapter /usr/bin/python3 /usr/libexec/aios-approved-release-executor/current/active-runtime/app/v2r1c7_production_adapter.py \
|
||||
--registry-adapter /usr/bin/env AIOS_RELEASE_METADATA_MODE=registry /usr/bin/python3 /usr/libexec/aios-approved-release-executor/current/active-runtime/app/release_metadata_adapter.py \
|
||||
--baseline-adapter /usr/bin/env AIOS_RELEASE_METADATA_MODE=baseline /usr/bin/python3 /usr/libexec/aios-approved-release-executor/current/active-runtime/app/release_metadata_adapter.py
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectHome=true
|
||||
ProtectSystem=full
|
||||
ReadWritePaths=/etc/aios /etc/systemd/system /opt/aios /var/lib/aios /var/lib/aios-gitops /var/lib/aios-gitops-runtime-anchor /data/aios-gitops
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,63 @@
|
||||
# AIOS Approved Production Release Executor
|
||||
|
||||
Status: implementation artifact for governed protected-PR integration.
|
||||
|
||||
## Why this exists
|
||||
|
||||
The deployed `aios-silent-release-controller` is intentionally shadow-only.
|
||||
It rejects production execution unless a trusted builder/orchestrator factory is
|
||||
wired. V2R1C7 already completed post-merge deterministic build, staging,
|
||||
rollback, evidence closure and exact Owner approval, so rebuilding inside the
|
||||
production executor is both unnecessary and a deadlock source.
|
||||
|
||||
This patch adds a separate one-shot production executor. It **does not turn the
|
||||
shadow controller into a production service**.
|
||||
|
||||
## Exact V2R1C7 binding
|
||||
|
||||
- target: `c7adece1cc8beffe7c718d53c775898b45d6c7f8`
|
||||
- evidence: `8c2898baf07195b9861ed2ff74f03a71674ec9e9`
|
||||
- artifact: `ce09a46f28b453634784abceda4597fd51c3aa0838e72a5ad5fefd03d58b949e`
|
||||
- approval plan: `sha256:c8199d638c2bb2e4896ca9b219ca52db79713bee007df05653ab348905d6cf28`
|
||||
|
||||
The executor requires a root-owned accepted authorization receipt that matches
|
||||
all five fields. It does not accept caller-supplied replacement evidence.
|
||||
|
||||
## Execution order
|
||||
|
||||
1. exact authorization + artifact + Git lineage preflight
|
||||
2. annotated V2R1C7 Tag publication and Authority/network/Gitea verification
|
||||
3. deploy exact artifact; health must report ACTIVE and exact target/digest
|
||||
4. Registry atomic finalization
|
||||
5. authoritative Production Baseline CAS finalization **last**
|
||||
6. read back Tag/runtime/Registry/Baseline and require exact consistency
|
||||
|
||||
## Tag implementation
|
||||
|
||||
`governed_tag_adapter.py` is a thin adapter over the repository's existing
|
||||
`gitea_tag_publication.TagPublisher`. It intentionally calls only the existing
|
||||
tag verification/publication subroutine and never calls legacy
|
||||
`TagPublisher.publish()` because that legacy operation finalizes Baseline before
|
||||
the product runtime is ACTIVE.
|
||||
|
||||
## Runtime finalization
|
||||
|
||||
`release_metadata_adapter.py` writes no Git refs. Registry and Baseline are
|
||||
runtime state only and are atomic/CAS guarded. Baseline apply independently
|
||||
requires exact ACTIVE health, exact formal tag, exact Registry, and the exact
|
||||
previous Baseline SHA-256.
|
||||
|
||||
For this release the previous **production** baseline is V2R1C5. V2R1C6 has a
|
||||
formal tag but was never deployed, so rollback must return to V2R1C5.
|
||||
|
||||
## Governance
|
||||
|
||||
- force push: forbidden
|
||||
- tag rewrite/delete: forbidden
|
||||
- direct main push: forbidden
|
||||
- V2R1C5/V2R1C6 mutation: forbidden
|
||||
- plaintext secrets in Git: forbidden
|
||||
- exact binding replacement: manual Owner review required
|
||||
- after a verified Tag, uncertain errors become reconciliation-required
|
||||
- global `AIOS_GITOPS_WRITES_ENABLED` stays false; this executor is a narrow,
|
||||
separately installed production capability.
|
||||
@@ -0,0 +1,53 @@
|
||||
# AIOS Local Owner Authentication Ingress
|
||||
|
||||
This component closes the Owner authentication bootstrap gap without creating a
|
||||
long-lived Owner PAT.
|
||||
|
||||
## Trust model
|
||||
|
||||
The ingress is deliberately one-shot and local-only.
|
||||
|
||||
1. A root-run server creates a Unix socket.
|
||||
2. The peer must be the configured OS Owner account (`aiman`).
|
||||
3. The server verifies Unix `SO_PEERCRED`, all four process UIDs, Linux audit
|
||||
`loginuid`, and ownership of the controlling TTY.
|
||||
4. A `sudo -u aiman` / `runuser` process from another authenticated login is
|
||||
rejected because its audit `loginuid` does not equal the Owner UID.
|
||||
5. The human Owner sees the exact task/base/branch/capability request and types
|
||||
a random one-time challenge.
|
||||
6. The server constructs only:
|
||||
- identity: `aiman`
|
||||
- role: `owner`
|
||||
- authenticated scope: `commit:create`
|
||||
- token scopes: [`commit:create`]
|
||||
7. It calls the existing `OwnerAuthorizationBroker`; it does not call the
|
||||
provisioner directly.
|
||||
8. It exits and removes the socket after the single authorization attempt.
|
||||
|
||||
No reusable token is minted and no production/tag/baseline/main capability is
|
||||
granted.
|
||||
|
||||
## Current V2R1C7 bootstrap binding
|
||||
|
||||
The bootstrap request is expected to authorize only:
|
||||
|
||||
- task: `V2R1C7_PRODUCTION_EXECUTOR_BOOTSTRAP`
|
||||
- branch: `agent/v2r1c7-production-executor-bootstrap-20260807-r2`
|
||||
- base: exact live `refs/heads/main`
|
||||
- repository: `aios/aios`
|
||||
- scope: `commit:create`
|
||||
- TTL: <= 900 seconds
|
||||
- production/tag/baseline/main write: false
|
||||
|
||||
## Installation model
|
||||
|
||||
For the current trust-root bootstrap, the exact SHA-verified script may be
|
||||
staged under `/run/aios-owner-bootstrap/` and run by a transient root systemd
|
||||
service. This staging step has no Git/Tag/Registry/Baseline side effect.
|
||||
|
||||
The Owner must connect from a real interactive login session as the OS account
|
||||
`aiman`.
|
||||
|
||||
After the governed writer is issued, the same module and tests are committed
|
||||
through the resulting protected engineering PR so future bootstrap uses code
|
||||
from main rather than an out-of-band artifact.
|
||||
@@ -0,0 +1,245 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
|
||||
|
||||
from approved_release_executor import (
|
||||
Binding,
|
||||
DurableState,
|
||||
ManualOwnerReviewRequired,
|
||||
ReconciliationRequired,
|
||||
deploy_step,
|
||||
execute,
|
||||
tag_step,
|
||||
verify_authorization,
|
||||
)
|
||||
|
||||
|
||||
TARGET = "c7adece1cc8beffe7c718d53c775898b45d6c7f8"
|
||||
EVIDENCE = "8c2898baf07195b9861ed2ff74f03a71674ec9e9"
|
||||
ARTIFACT = "ce09a46f28b453634784abceda4597fd51c3aa0838e72a5ad5fefd03d58b949e"
|
||||
PLAN = "sha256:c8199d638c2bb2e4896ca9b219ca52db79713bee007df05653ab348905d6cf28"
|
||||
TAG_OID = "1" * 40
|
||||
|
||||
|
||||
def binding(tmp_path: Path) -> Binding:
|
||||
artifact = tmp_path / "release.tar.zst"
|
||||
artifact.write_bytes(b"x")
|
||||
return Binding(
|
||||
version="VTEST",
|
||||
target_oid=TARGET,
|
||||
evidence_closure_oid=EVIDENCE,
|
||||
artifact_sha256=ARTIFACT,
|
||||
approval_plan_id=PLAN,
|
||||
artifact_path=str(artifact),
|
||||
artifact_size=1,
|
||||
production_service="test.service",
|
||||
production_port=18927,
|
||||
)
|
||||
|
||||
|
||||
def authorization(value: Binding) -> dict:
|
||||
return {
|
||||
"accepted": True,
|
||||
"binding_replaced": False,
|
||||
"version": value.version,
|
||||
"target_oid": value.target_oid,
|
||||
"evidence_closure_oid": value.evidence_closure_oid,
|
||||
"artifact_sha256": value.artifact_sha256,
|
||||
"approval_plan_id": value.approval_plan_id,
|
||||
}
|
||||
|
||||
|
||||
def test_owner_authorization_is_exact(tmp_path):
|
||||
value = binding(tmp_path)
|
||||
assert verify_authorization(authorization(value), value)["accepted"] is True
|
||||
wrong = authorization(value)
|
||||
wrong["artifact_sha256"] = "0" * 64
|
||||
with pytest.raises(ManualOwnerReviewRequired):
|
||||
verify_authorization(wrong, value)
|
||||
|
||||
|
||||
def test_durable_state_rejects_different_binding(tmp_path):
|
||||
first = binding(tmp_path)
|
||||
DurableState(tmp_path / "state", first)
|
||||
second = Binding(
|
||||
**{
|
||||
**first.__dict__,
|
||||
"approval_plan_id": "sha256:" + "2" * 64,
|
||||
}
|
||||
)
|
||||
with pytest.raises(ManualOwnerReviewRequired):
|
||||
DurableState(tmp_path / "state", second)
|
||||
|
||||
|
||||
def test_tag_conflict_never_applies(monkeypatch, tmp_path):
|
||||
value = binding(tmp_path)
|
||||
calls = []
|
||||
|
||||
def fake(command, request):
|
||||
calls.append(request["operation"])
|
||||
return {
|
||||
"status": "CONFLICT",
|
||||
"version": value.version,
|
||||
"target_oid": value.target_oid,
|
||||
"artifact_sha256": value.artifact_sha256,
|
||||
}
|
||||
|
||||
monkeypatch.setattr("approved_release_executor.run_adapter", fake)
|
||||
with pytest.raises(ManualOwnerReviewRequired):
|
||||
tag_step(["/tag"], value)
|
||||
assert calls == ["probe"]
|
||||
|
||||
|
||||
def test_runtime_failure_attempts_rollback_after_apply(monkeypatch, tmp_path):
|
||||
value = binding(tmp_path)
|
||||
calls = []
|
||||
probes = 0
|
||||
|
||||
def fake(command, request):
|
||||
nonlocal probes
|
||||
calls.append(request["operation"])
|
||||
if request["operation"] == "probe":
|
||||
probes += 1
|
||||
if probes == 1:
|
||||
return {
|
||||
"status": "NOT_APPLIED",
|
||||
"version": value.version,
|
||||
"target_oid": value.target_oid,
|
||||
"artifact_sha256": value.artifact_sha256,
|
||||
}
|
||||
return {
|
||||
"status": "NOT_APPLIED",
|
||||
"version": value.version,
|
||||
"target_oid": value.target_oid,
|
||||
"artifact_sha256": value.artifact_sha256,
|
||||
}
|
||||
if request["operation"] == "rollback":
|
||||
return {
|
||||
"status": "ROLLED_BACK",
|
||||
"version": value.version,
|
||||
"target_oid": value.target_oid,
|
||||
"artifact_sha256": value.artifact_sha256,
|
||||
}
|
||||
return {
|
||||
"status": "APPLY_STARTED",
|
||||
"version": value.version,
|
||||
"target_oid": value.target_oid,
|
||||
"artifact_sha256": value.artifact_sha256,
|
||||
}
|
||||
|
||||
monkeypatch.setattr("approved_release_executor.run_adapter", fake)
|
||||
with pytest.raises(ReconciliationRequired):
|
||||
deploy_step(["/deploy"], value)
|
||||
assert calls == ["probe", "apply", "probe", "rollback"]
|
||||
|
||||
|
||||
def test_execute_orders_registry_before_baseline_and_baseline_last(
|
||||
monkeypatch, tmp_path
|
||||
):
|
||||
value = binding(tmp_path)
|
||||
auth_path = tmp_path / "authorization.json"
|
||||
auth_path.write_text(json.dumps(authorization(value)))
|
||||
auth_path.chmod(0o600)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"approved_release_executor.secure_json",
|
||||
lambda path: json.loads(path.read_text(encoding="utf-8")),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"approved_release_executor.verify_git_lineage",
|
||||
lambda repo, binding: {"verified": True},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"approved_release_executor.verify_artifact",
|
||||
lambda binding: {"verified": True},
|
||||
)
|
||||
|
||||
calls = []
|
||||
tag_applied = False
|
||||
runtime_applied = False
|
||||
registry_applied = False
|
||||
baseline_applied = False
|
||||
|
||||
def fake(command, request):
|
||||
nonlocal tag_applied, runtime_applied, registry_applied, baseline_applied
|
||||
name = command[0]
|
||||
operation = request["operation"]
|
||||
calls.append((name, operation))
|
||||
|
||||
common = {
|
||||
"version": value.version,
|
||||
"target_oid": value.target_oid,
|
||||
"artifact_sha256": value.artifact_sha256,
|
||||
}
|
||||
if name == "/tag":
|
||||
if operation == "apply":
|
||||
tag_applied = True
|
||||
if tag_applied:
|
||||
return {
|
||||
**common,
|
||||
"status": "APPLIED_VERIFIED",
|
||||
"tag_oid": TAG_OID,
|
||||
"peeled_target_oid": value.target_oid,
|
||||
}
|
||||
return {**common, "status": "NOT_APPLIED"}
|
||||
|
||||
if name == "/deploy":
|
||||
if operation == "apply":
|
||||
runtime_applied = True
|
||||
if operation == "rollback":
|
||||
return {**common, "status": "ROLLED_BACK"}
|
||||
if runtime_applied:
|
||||
return {
|
||||
**common,
|
||||
"status": "ACTIVE_VERIFIED",
|
||||
"deployment_state": "ACTIVE",
|
||||
"production_deployed": True,
|
||||
}
|
||||
return {**common, "status": "NOT_APPLIED"}
|
||||
|
||||
if name == "/registry":
|
||||
if operation == "apply":
|
||||
registry_applied = True
|
||||
return {
|
||||
**common,
|
||||
"status": "VERIFIED" if registry_applied else "NOT_APPLIED",
|
||||
"tag_oid": TAG_OID if registry_applied else None,
|
||||
}
|
||||
|
||||
if name == "/baseline":
|
||||
if operation == "apply":
|
||||
assert registry_applied is True
|
||||
assert runtime_applied is True
|
||||
baseline_applied = True
|
||||
return {
|
||||
**common,
|
||||
"status": "VERIFIED" if baseline_applied else "NOT_APPLIED",
|
||||
"tag_oid": TAG_OID if baseline_applied else None,
|
||||
}
|
||||
raise AssertionError(name)
|
||||
|
||||
monkeypatch.setattr("approved_release_executor.run_adapter", fake)
|
||||
|
||||
result = execute(
|
||||
binding=value,
|
||||
authorization_path=auth_path,
|
||||
repository=tmp_path / "repo.git",
|
||||
state_root=tmp_path / "state",
|
||||
tag_command=["/tag"],
|
||||
deploy_command=["/deploy"],
|
||||
registry_command=["/registry"],
|
||||
baseline_command=["/baseline"],
|
||||
)
|
||||
|
||||
assert result["status"] == "PRODUCTION_ACCEPTED"
|
||||
assert calls.index(("/registry", "apply")) < calls.index(("/baseline", "apply"))
|
||||
assert calls.index(("/deploy", "apply")) < calls.index(("/registry", "apply"))
|
||||
state = json.loads((tmp_path / "state" / "execution.json").read_text())
|
||||
assert state["state"] == "COMPLETED"
|
||||
@@ -0,0 +1,152 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import types
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
|
||||
|
||||
import owner_local_auth_ingress as ingress
|
||||
|
||||
|
||||
TARGET = "c7adece1cc8beffe7c718d53c775898b45d6c7f8"
|
||||
|
||||
|
||||
def binding():
|
||||
return {
|
||||
"schema": ingress.BINDING_SCHEMA,
|
||||
"owner_identity": "aiman",
|
||||
"owner_role": "owner",
|
||||
"authenticated_scope": "commit:create",
|
||||
"request": {
|
||||
"action": ingress.ACTION,
|
||||
"task_id": "V2R1C7_PRODUCTION_EXECUTOR_BOOTSTRAP",
|
||||
"repository": "aios/aios",
|
||||
"base_ref": "refs/heads/main",
|
||||
"base_oid": TARGET,
|
||||
"target_branch": "agent/v2r1c7-production-executor-bootstrap-20260807-r2",
|
||||
"one_time_use": True,
|
||||
"expires_in_seconds": 300,
|
||||
"production_change_authorized": False,
|
||||
"tag_change_authorized": False,
|
||||
"baseline_change_authorized": False,
|
||||
"main_write_authorized": False,
|
||||
"allowed_operations": sorted(ingress.EXPECTED_ALLOWED_OPERATIONS),
|
||||
"denied_operations": sorted(ingress.EXPECTED_DENIED_OPERATIONS),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_exact_binding_passes():
|
||||
result = ingress.validate_binding(binding())
|
||||
assert result["owner_identity"] == "aiman"
|
||||
assert result["authenticated_scope"] == "commit:create"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("path", "value", "error"),
|
||||
[
|
||||
(("owner_identity",), "owner", None),
|
||||
(("authenticated_scope",), "*", "OWNER_SCOPE_INVALID"),
|
||||
(("request", "target_branch"), "patch/not-allowed", "TARGET_BRANCH_REJECTED"),
|
||||
(("request", "production_change_authorized"), True, "CONSEQUENTIAL_CAPABILITY_REJECTED"),
|
||||
(("request", "tag_change_authorized"), True, "CONSEQUENTIAL_CAPABILITY_REJECTED"),
|
||||
(("request", "baseline_change_authorized"), True, "CONSEQUENTIAL_CAPABILITY_REJECTED"),
|
||||
(("request", "main_write_authorized"), True, "CONSEQUENTIAL_CAPABILITY_REJECTED"),
|
||||
(("request", "expires_in_seconds"), 901, "EXPIRY_REJECTED"),
|
||||
],
|
||||
)
|
||||
def test_negative_binding_contract(path, value, error):
|
||||
payload = binding()
|
||||
target = payload
|
||||
for item in path[:-1]:
|
||||
target = target[item]
|
||||
target[path[-1]] = value
|
||||
if error is None:
|
||||
# Structurally valid but not the deployment allowlisted identity;
|
||||
# the live Broker performs that final allowlist check.
|
||||
assert ingress.validate_binding(payload)["owner_identity"] == "owner"
|
||||
else:
|
||||
with pytest.raises(ingress.BootstrapError, match=error):
|
||||
ingress.validate_binding(payload)
|
||||
|
||||
|
||||
def test_request_summary_never_grants_consequential_capability():
|
||||
summary = ingress.request_summary(ingress.validate_binding(binding()))
|
||||
assert summary["production_change_authorized"] is False
|
||||
assert summary["tag_change_authorized"] is False
|
||||
assert summary["baseline_change_authorized"] is False
|
||||
assert summary["main_write_authorized"] is False
|
||||
|
||||
|
||||
def test_safe_result_rejects_secret_material():
|
||||
with pytest.raises(ingress.BootstrapError, match="SENSITIVE_DATA"):
|
||||
ingress.safe_result({"status": "READY", "raw_token": "secret"})
|
||||
|
||||
|
||||
def test_safe_result_keeps_only_allowlisted_fields():
|
||||
result = ingress.safe_result(
|
||||
{
|
||||
"status": "READY_FOR_CODE_AUTHORING",
|
||||
"grant_id": "grant-1",
|
||||
"some_untrusted_extra": "drop-me",
|
||||
}
|
||||
)
|
||||
assert result == {
|
||||
"grant_id": "grant-1",
|
||||
"status": "READY_FOR_CODE_AUTHORING",
|
||||
}
|
||||
|
||||
|
||||
def test_invoke_broker_constructs_narrow_principal(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
class Config:
|
||||
allowed_owner_identities = frozenset({"aiman"})
|
||||
allowed_intake_scopes = frozenset({"commit:create"})
|
||||
max_expiry_seconds = 900
|
||||
|
||||
@classmethod
|
||||
def from_environment(cls):
|
||||
return cls()
|
||||
|
||||
class Principal:
|
||||
def __init__(self, **kwargs):
|
||||
captured["principal"] = kwargs
|
||||
|
||||
class Broker:
|
||||
def __init__(self, config):
|
||||
captured["config"] = config
|
||||
|
||||
def authorize_and_provision(self, request, principal):
|
||||
captured["request"] = request
|
||||
return {
|
||||
"status": "READY_FOR_CODE_AUTHORING",
|
||||
"grant_id": "grant-test",
|
||||
}
|
||||
|
||||
module = types.SimpleNamespace(
|
||||
OwnerAuthorizationBroker=Broker,
|
||||
OwnerAuthorizationBrokerConfig=Config,
|
||||
OwnerPrincipal=Principal,
|
||||
)
|
||||
monkeypatch.setitem(sys.modules, "owner_authorization_broker", module)
|
||||
result = ingress.invoke_broker(ingress.validate_binding(binding()))
|
||||
assert result["status"] == "READY_FOR_CODE_AUTHORING"
|
||||
assert captured["principal"] == {
|
||||
"identity": "aiman",
|
||||
"role": "owner",
|
||||
"authenticated_scope": "commit:create",
|
||||
"token_scopes": frozenset({"commit:create"}),
|
||||
"credential_id": "",
|
||||
}
|
||||
|
||||
|
||||
def test_confirmation_phrase_is_exact():
|
||||
challenge = "abc123"
|
||||
assert f"AUTHORIZE {challenge}" == "AUTHORIZE abc123"
|
||||
@@ -0,0 +1,116 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
|
||||
|
||||
import release_metadata_adapter as adapter
|
||||
|
||||
|
||||
TARGET = "c7adece1cc8beffe7c718d53c775898b45d6c7f8"
|
||||
EVIDENCE = "8c2898baf07195b9861ed2ff74f03a71674ec9e9"
|
||||
ARTIFACT = "ce09a46f28b453634784abceda4597fd51c3aa0838e72a5ad5fefd03d58b949e"
|
||||
PLAN = "sha256:c8199d638c2bb2e4896ca9b219ca52db79713bee007df05653ab348905d6cf28"
|
||||
TAG = "1" * 40
|
||||
|
||||
|
||||
def value():
|
||||
return {
|
||||
"version": "V2R1C7",
|
||||
"target_oid": TARGET,
|
||||
"evidence_closure_oid": EVIDENCE,
|
||||
"artifact_sha256": ARTIFACT,
|
||||
"approval_plan_id": PLAN,
|
||||
"production_service": "aios-v2r1c7-production.service",
|
||||
"production_port": 18927,
|
||||
}
|
||||
|
||||
|
||||
def old_baseline(path: Path) -> str:
|
||||
data = {
|
||||
"current_baseline": "V2R1C5",
|
||||
"version": "V2R1C5",
|
||||
"target_oid": "a" * 40,
|
||||
"tag_oid": "b" * 40,
|
||||
"generation": 2,
|
||||
"authority_generation": 2,
|
||||
"status": "PRODUCTION_DEPLOYED",
|
||||
}
|
||||
path.write_text(json.dumps(data, sort_keys=True) + "\n", encoding="utf-8")
|
||||
return adapter.sha256_file(path)
|
||||
|
||||
|
||||
def test_baseline_record_uses_real_previous_production_baseline(tmp_path, monkeypatch):
|
||||
baseline = tmp_path / "baseline.json"
|
||||
previous_sha = old_baseline(baseline)
|
||||
registry_root = tmp_path / "versions"
|
||||
registry_root.mkdir()
|
||||
args = type("A", (), {
|
||||
"repository": tmp_path / "repo.git",
|
||||
"registry_root": registry_root,
|
||||
"baseline_path": baseline,
|
||||
"expected_previous_baseline": "V2R1C5",
|
||||
"expected_previous_baseline_sha256": previous_sha,
|
||||
"health_url": "http://127.0.0.1:18927/health",
|
||||
})()
|
||||
monkeypatch.setattr(adapter, "verify_local_tag", lambda *a: (TAG, TARGET))
|
||||
monkeypatch.setattr(adapter, "secure_baseline_authority_file", lambda path: None)
|
||||
monkeypatch.setattr(adapter, "health", lambda *a: {
|
||||
"deployment_state": "ACTIVE",
|
||||
"production_deployed": True,
|
||||
})
|
||||
registry = adapter.registry_record(value(), TAG, {"deployment_state": "ACTIVE"})
|
||||
adapter.atomic_json(registry_root / "V2R1C7.json", registry)
|
||||
result = adapter.baseline_apply(args, value())
|
||||
assert result["status"] == "VERIFIED"
|
||||
current = json.loads(baseline.read_text())
|
||||
assert current["previous_baseline"] == "V2R1C5"
|
||||
assert current["rollback_version"] == "V2R1C5"
|
||||
assert current["rollback_target_oid"] == "a" * 40
|
||||
assert current["status"] == "PRODUCTION_DEPLOYED"
|
||||
|
||||
|
||||
def test_baseline_cas_conflict_fails_closed(tmp_path, monkeypatch):
|
||||
baseline = tmp_path / "baseline.json"
|
||||
previous_sha = old_baseline(baseline)
|
||||
baseline.write_text('{"current_baseline":"OTHER"}\n', encoding="utf-8")
|
||||
args = type("A", (), {
|
||||
"repository": tmp_path / "repo.git",
|
||||
"registry_root": tmp_path / "versions",
|
||||
"baseline_path": baseline,
|
||||
"expected_previous_baseline": "V2R1C5",
|
||||
"expected_previous_baseline_sha256": previous_sha,
|
||||
"health_url": "http://127.0.0.1:18927/health",
|
||||
})()
|
||||
monkeypatch.setattr(adapter, "verify_local_tag", lambda *a: (TAG, TARGET))
|
||||
monkeypatch.setattr(adapter, "secure_baseline_authority_file", lambda path: None)
|
||||
result = adapter.baseline_probe(args, value())
|
||||
assert result["status"] == "CONFLICT"
|
||||
|
||||
|
||||
def test_registry_record_binds_all_exact_release_fields():
|
||||
record = adapter.registry_record(value(), TAG, {"deployment_state": "ACTIVE"})
|
||||
assert record["target_oid"] == TARGET
|
||||
assert record["artifact_sha256"] == ARTIFACT
|
||||
assert record["evidence_closure_oid"] == EVIDENCE
|
||||
assert record["approval_plan_id"] == PLAN
|
||||
assert record["tag_oid"] == TAG
|
||||
assert record["production_deployed"] is True
|
||||
|
||||
|
||||
def test_atomic_json_preserves_existing_mode(tmp_path):
|
||||
target = tmp_path / "current.json"
|
||||
target.write_text('{"old":true}\n', encoding="utf-8")
|
||||
target.chmod(0o440)
|
||||
before = target.stat()
|
||||
adapter.atomic_json(target, {"new": True})
|
||||
after = target.stat()
|
||||
assert after.st_mode & 0o777 == 0o440
|
||||
assert after.st_uid == before.st_uid
|
||||
assert after.st_gid == before.st_gid
|
||||
Reference in New Issue
Block a user