Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9adec87c4b |
@@ -24,6 +24,25 @@ def _safe_oid(value: str, field: str) -> str:
|
||||
return value
|
||||
|
||||
|
||||
def _safe_branch(value: str) -> str:
|
||||
branch = str(value or "")
|
||||
if (
|
||||
not branch
|
||||
or len(branch) > 240
|
||||
or branch != branch.strip()
|
||||
or branch.startswith(("/", "-", "."))
|
||||
or branch.endswith(("/", ".", ".lock"))
|
||||
or ".." in branch
|
||||
or "//" in branch
|
||||
or "@{" in branch
|
||||
or "\\" in branch
|
||||
or any(part in {"", ".", ".."} for part in branch.split("/"))
|
||||
or any(ch not in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._/-" for ch in branch)
|
||||
):
|
||||
raise GiteaTransportError("BRANCH_INVALID")
|
||||
return branch
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class GiteaMachineTransport:
|
||||
api_base: str
|
||||
@@ -91,6 +110,7 @@ class GiteaMachineTransport:
|
||||
return self.repository_info()
|
||||
|
||||
def read_branch(self, branch: str) -> str | None:
|
||||
branch = _safe_branch(branch)
|
||||
owner, name = self.repository_id.split("/", 1)
|
||||
try:
|
||||
value = self._request("GET", f"/repos/{quote(owner)}/{quote(name)}/branches/{quote(branch, safe='/')}")[1]
|
||||
@@ -136,6 +156,99 @@ class GiteaMachineTransport:
|
||||
finally:
|
||||
askpass.unlink(missing_ok=True)
|
||||
|
||||
def fetch_branch_exact(self, branch: str, expected_oid: str) -> str:
|
||||
"""Fetch one exact remote branch tip into a quarantine ref.
|
||||
|
||||
This never updates ``main`` or any caller-visible branch. The remote
|
||||
ref is read before and after the fetch so a concurrent branch move
|
||||
cannot be silently admitted.
|
||||
"""
|
||||
|
||||
branch = _safe_branch(branch)
|
||||
expected_oid = _safe_oid(expected_oid, "expected_oid")
|
||||
remote_before = self.read_branch(branch)
|
||||
if remote_before != expected_oid:
|
||||
raise GiteaTransportError("GITEA_SOURCE_REF_MISMATCH")
|
||||
target_ref = f"refs/aios/release-sources/{expected_oid}"
|
||||
|
||||
current = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
f"--git-dir={self.authority_git_dir}",
|
||||
"rev-parse",
|
||||
"--verify",
|
||||
f"{target_ref}^{{commit}}",
|
||||
],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
if current.returncode == 0 and current.stdout.strip().lower() == expected_oid:
|
||||
remote_after = self.read_branch(branch)
|
||||
if remote_after != expected_oid:
|
||||
raise GiteaTransportError("GITEA_SOURCE_REF_MOVED")
|
||||
return expected_oid
|
||||
|
||||
askpass, additions = self._askpass()
|
||||
environment = os.environ.copy()
|
||||
environment.update(additions)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
f"--git-dir={self.authority_git_dir}",
|
||||
"fetch",
|
||||
"--no-tags",
|
||||
"--no-write-fetch-head",
|
||||
self.remote_url,
|
||||
f"refs/heads/{branch}:{target_ref}",
|
||||
],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
env=environment,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise GiteaTransportError("GITEA_SOURCE_FETCH_FAILED")
|
||||
finally:
|
||||
askpass.unlink(missing_ok=True)
|
||||
|
||||
local = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
f"--git-dir={self.authority_git_dir}",
|
||||
"rev-parse",
|
||||
"--verify",
|
||||
f"{target_ref}^{{commit}}",
|
||||
],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
local_oid = local.stdout.strip().lower()
|
||||
if local.returncode != 0 or local_oid != expected_oid:
|
||||
subprocess.run(
|
||||
[
|
||||
"git",
|
||||
f"--git-dir={self.authority_git_dir}",
|
||||
"update-ref",
|
||||
"-d",
|
||||
target_ref,
|
||||
],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
raise GiteaTransportError("GITEA_SOURCE_FETCH_MISMATCH")
|
||||
remote_after = self.read_branch(branch)
|
||||
if remote_after != expected_oid:
|
||||
raise GiteaTransportError("GITEA_SOURCE_REF_MOVED")
|
||||
return expected_oid
|
||||
|
||||
def compare_and_swap_branch(self, branch: str, expected_oid: str, new_oid: str) -> str:
|
||||
_safe_oid(expected_oid, "expected_oid")
|
||||
_safe_oid(new_oid, "new_oid")
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
import hashlib, hmac, json, os, re, subprocess, sys, time, urllib.parse, urllib.request, uuid
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
from baseline_authority import BaselineAuthority, BaselineAuthorityError
|
||||
from silent_release_controller import ReleaseCandidate
|
||||
@@ -114,6 +115,9 @@ TRUSTED_BUILDER_SOCKET = Path(os.getenv(
|
||||
TRUSTED_BUILDER_PUBLIC_KEYRING = Path(os.getenv(
|
||||
'AIOS_TRUSTED_CANDIDATE_BUILDER_PUBLIC_KEYRING', ''
|
||||
))
|
||||
TRUSTED_BUILDER_SOURCE_ADMISSION_ROOT = str(os.getenv(
|
||||
'AIOS_TRUSTED_BUILDER_SOURCE_ADMISSION_ROOT', ''
|
||||
)).strip()
|
||||
TRUSTED_RELEASE_POLICY = Path(os.getenv(
|
||||
'AIOS_SILENT_RELEASE_TRUSTED_POLICY',
|
||||
str(WS / 'config' / 'gitops' / 'silent-release-policy.json'),
|
||||
@@ -253,6 +257,14 @@ class _HostTrustedCandidateBuilderAdapter:
|
||||
nonce=secrets.token_hex(32),
|
||||
)
|
||||
except BuilderError as exc:
|
||||
code = str(exc)
|
||||
if code in {'SOURCE_ADMISSION_REQUIRED', 'RECIPE_UNKNOWN'} or (
|
||||
code == 'REQUEST_RELEASE_BINDING_INVALID'
|
||||
and not intent.get('version')
|
||||
):
|
||||
raise CapabilityAuthorizationError(
|
||||
'release source admission required'
|
||||
) from exc
|
||||
raise TrustedCandidateBuilderUnavailable(
|
||||
'trusted builder request failed'
|
||||
) from exc
|
||||
@@ -290,6 +302,7 @@ def _build_silent_release_service() -> SilentReleaseActionService:
|
||||
builder_attestation_verifier = None
|
||||
trusted_policy = None
|
||||
builder_readiness_probe = None
|
||||
source_admission_resolver = None
|
||||
if TRUSTED_BUILDER_ENABLED:
|
||||
# Every path is opt-in. In particular, this API receives its own
|
||||
# client HMAC credential, never the builder's signing private key.
|
||||
@@ -317,6 +330,31 @@ def _build_silent_release_service() -> SilentReleaseActionService:
|
||||
client_hmac = (
|
||||
TRUSTED_BUILDER_CREDENTIALS_DIRECTORY / 'builder_hmac'
|
||||
).read_bytes().strip()
|
||||
if TRUSTED_BUILDER_SOURCE_ADMISSION_ROOT:
|
||||
from release_source_admission import ReleaseSourceAdmissionStore
|
||||
|
||||
try:
|
||||
source_admission_hmac = (
|
||||
TRUSTED_BUILDER_CREDENTIALS_DIRECTORY
|
||||
/ 'source_admission_hmac'
|
||||
).read_bytes().strip()
|
||||
except OSError as exc:
|
||||
raise CapabilityAuthorizationError(
|
||||
'release source admission credential is unavailable'
|
||||
) from exc
|
||||
source_store = ReleaseSourceAdmissionStore(
|
||||
Path(TRUSTED_BUILDER_SOURCE_ADMISSION_ROOT),
|
||||
repository=builder_config.repository,
|
||||
policy_sha256=builder_config.policy_sha256,
|
||||
hmac_key=source_admission_hmac,
|
||||
expected_owner_uid=0,
|
||||
writable=False,
|
||||
)
|
||||
builder_config = replace(
|
||||
builder_config,
|
||||
source_admission_resolver=source_store.resolve_recipe,
|
||||
)
|
||||
source_admission_resolver = source_store.lookup_source
|
||||
client_keyring = BuilderKeyring(builder_hmac=client_hmac)
|
||||
client = UnixTrustedCandidateBuilderClient(
|
||||
builder_config,
|
||||
@@ -365,6 +403,7 @@ def _build_silent_release_service() -> SilentReleaseActionService:
|
||||
builder_attestation_verifier=builder_attestation_verifier,
|
||||
trusted_policy=trusted_policy,
|
||||
builder_readiness_probe=builder_readiness_probe,
|
||||
source_admission_resolver=source_admission_resolver,
|
||||
allow_legacy_candidate_input=False,
|
||||
expected_repository=REPOSITORY_IDENTITY,
|
||||
policy_sha256=policy_sha,
|
||||
|
||||
@@ -15,7 +15,7 @@ import struct
|
||||
import subprocess
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, replace
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Mapping, Protocol
|
||||
|
||||
@@ -133,6 +133,7 @@ class TrustedCandidateBuilderConfig:
|
||||
recipes: Mapping[str, Mapping[str, str]]
|
||||
governed_recipes: Mapping[str, Mapping[str, str]]
|
||||
policy_sha256: str
|
||||
source_admission_resolver: Callable[[str, str | None, str], Mapping[str, str]] | None = None
|
||||
|
||||
@classmethod
|
||||
def from_policy(cls, policy: Mapping[str, Any]) -> "TrustedCandidateBuilderConfig":
|
||||
@@ -227,14 +228,63 @@ class TrustedCandidateBuilderConfig:
|
||||
return recipe
|
||||
|
||||
def resolve_recipe(self, version: str, candidate_id: str | None, source_commit: str) -> Mapping[str, str]:
|
||||
recipe = self.fixed_recipe(version)
|
||||
# Intent-only callers do not choose a candidate identity. The
|
||||
# policy-owned recipe supplies it; explicit worker requests are
|
||||
# still checked against the same immutable recipe.
|
||||
effective_candidate_id = str(candidate_id or recipe.get("candidate_id") or "")
|
||||
if {"version": recipe.get("version"), "candidate_id": recipe.get("candidate_id"), "source_commit": recipe.get("source_commit")} != {"version": version, "candidate_id": effective_candidate_id, "source_commit": source_commit}:
|
||||
recipe = self.recipes.get(version)
|
||||
if recipe is not None:
|
||||
resolved = self.fixed_recipe(version)
|
||||
# Fixed/governed-derived policy recipes remain immutable. A
|
||||
# source admission can never override one of these identities.
|
||||
effective_candidate_id = str(candidate_id or resolved.get("candidate_id") or "")
|
||||
if {
|
||||
"version": resolved.get("version"),
|
||||
"candidate_id": resolved.get("candidate_id"),
|
||||
"source_commit": resolved.get("source_commit"),
|
||||
} != {
|
||||
"version": version,
|
||||
"candidate_id": effective_candidate_id,
|
||||
"source_commit": source_commit,
|
||||
}:
|
||||
raise BuilderError("REQUEST_RELEASE_BINDING_INVALID")
|
||||
return resolved
|
||||
|
||||
resolver = self.source_admission_resolver
|
||||
if resolver is None:
|
||||
raise BuilderError("RECIPE_UNKNOWN")
|
||||
try:
|
||||
admitted = dict(resolver(version, candidate_id, source_commit))
|
||||
except Exception as exc:
|
||||
raise BuilderError("SOURCE_ADMISSION_REQUIRED") from exc
|
||||
required = {
|
||||
"mode",
|
||||
"task_id",
|
||||
"candidate_id",
|
||||
"base_oid",
|
||||
"source_commit",
|
||||
"version",
|
||||
"purpose",
|
||||
}
|
||||
if (
|
||||
set(admitted) != required
|
||||
or admitted.get("mode") != "governed-source-admission"
|
||||
or admitted.get("version") != version
|
||||
or admitted.get("source_commit") != source_commit
|
||||
or not isinstance(admitted.get("candidate_id"), str)
|
||||
or not admitted.get("candidate_id")
|
||||
):
|
||||
raise BuilderError("SOURCE_ADMISSION_INVALID")
|
||||
effective_candidate_id = str(candidate_id or admitted["candidate_id"])
|
||||
if effective_candidate_id != admitted["candidate_id"]:
|
||||
raise BuilderError("REQUEST_RELEASE_BINDING_INVALID")
|
||||
return recipe
|
||||
for name in ("base_oid", "source_commit"):
|
||||
value = admitted.get(name)
|
||||
if (
|
||||
not isinstance(value, str)
|
||||
or len(value) != 40
|
||||
or any(char not in "0123456789abcdef" for char in value.lower())
|
||||
):
|
||||
raise BuilderError("SOURCE_ADMISSION_INVALID")
|
||||
if admitted.get("purpose") != "AIOS_TRUSTED_RELEASE_BUILDER":
|
||||
raise BuilderError("SOURCE_ADMISSION_INVALID")
|
||||
return admitted
|
||||
|
||||
|
||||
def _invalid_governed_recipes() -> Mapping[str, Mapping[str, str]]:
|
||||
@@ -776,6 +826,30 @@ def _main() -> None:
|
||||
if not credentials:
|
||||
raise SystemExit("CREDENTIALS_DIRECTORY is required")
|
||||
keyring = BuilderKeyring.from_credentials_directory(Path(credentials))
|
||||
source_admission_root = os.environ.get(
|
||||
"AIOS_TRUSTED_BUILDER_SOURCE_ADMISSION_ROOT", ""
|
||||
).strip()
|
||||
if source_admission_root:
|
||||
from release_source_admission import ReleaseSourceAdmissionStore
|
||||
|
||||
try:
|
||||
source_admission_hmac = (
|
||||
Path(credentials) / "source_admission_hmac"
|
||||
).read_bytes().strip()
|
||||
except OSError as exc:
|
||||
raise BuilderError("SOURCE_ADMISSION_HMAC_UNAVAILABLE") from exc
|
||||
source_store = ReleaseSourceAdmissionStore(
|
||||
Path(source_admission_root),
|
||||
repository=config.repository,
|
||||
policy_sha256=config.policy_sha256,
|
||||
hmac_key=source_admission_hmac,
|
||||
expected_owner_uid=0,
|
||||
writable=False,
|
||||
)
|
||||
config = replace(
|
||||
config,
|
||||
source_admission_resolver=source_store.resolve_recipe,
|
||||
)
|
||||
signer = CredentialPathEd25519Signer(
|
||||
keyring.signing_private_key_path,
|
||||
key_id=os.environ["AIOS_TRUSTED_BUILDER_KEY_ID"],
|
||||
|
||||
@@ -0,0 +1,561 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Governed admission of an exact remote Git commit into the release source closure.
|
||||
|
||||
The admitter is intentionally separate from the trusted candidate builder:
|
||||
it may use a narrowly-scoped Gitea read/fetch credential, while the builder
|
||||
remains PrivateNetwork=true and reads only the authoritative Git object store
|
||||
plus immutable admission records.
|
||||
|
||||
An admission never moves ``main`` and never treats an arbitrary caller-supplied
|
||||
OID as trusted. It binds:
|
||||
remote branch ref -> exact full commit OID -> authoritative Git object/tree
|
||||
-> current authoritative main ancestry -> immutable release recipe metadata.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping, Protocol
|
||||
|
||||
from authoritative_git import (
|
||||
AuthoritativeGitError,
|
||||
authoritative_head_oid,
|
||||
authoritative_lineage,
|
||||
run_authoritative_git,
|
||||
)
|
||||
from aios_gitea_machine_transport import GiteaMachineTransport, GiteaTransportError
|
||||
from release_version import validate_release_version
|
||||
|
||||
|
||||
SCHEMA = "aios.release-source-admission.v1"
|
||||
RECIPE_MODE = "governed-source-admission"
|
||||
_OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
_DIGEST_RE = re.compile(r"^[0-9a-f]{64}$")
|
||||
_IDENTIFIER_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,199}$")
|
||||
_BRANCH_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/-]{0,239}$")
|
||||
_REF_PREFIX = "refs/heads/"
|
||||
|
||||
|
||||
class ReleaseSourceAdmissionError(RuntimeError):
|
||||
"""A stable fail-closed source-admission error code."""
|
||||
|
||||
|
||||
def _canonical(value: Mapping[str, Any]) -> bytes:
|
||||
return json.dumps(
|
||||
dict(value),
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
ensure_ascii=True,
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def _oid(value: object, field: str) -> str:
|
||||
text = str(value or "").strip().lower()
|
||||
if not _OID_RE.fullmatch(text):
|
||||
raise ReleaseSourceAdmissionError(f"{field.upper()}_INVALID")
|
||||
return text
|
||||
|
||||
|
||||
def _digest(value: object, field: str) -> str:
|
||||
text = str(value or "").strip().lower()
|
||||
if not _DIGEST_RE.fullmatch(text):
|
||||
raise ReleaseSourceAdmissionError(f"{field.upper()}_INVALID")
|
||||
return text
|
||||
|
||||
|
||||
def _identifier(value: object, field: str) -> str:
|
||||
text = str(value or "").strip()
|
||||
if not _IDENTIFIER_RE.fullmatch(text) or ".." in text or "\x00" in text:
|
||||
raise ReleaseSourceAdmissionError(f"{field.upper()}_INVALID")
|
||||
return text
|
||||
|
||||
|
||||
def _source_ref(value: object) -> str:
|
||||
text = str(value or "").strip()
|
||||
if not text.startswith(_REF_PREFIX):
|
||||
raise ReleaseSourceAdmissionError("SOURCE_REF_INVALID")
|
||||
branch = text[len(_REF_PREFIX):]
|
||||
if (
|
||||
not _BRANCH_RE.fullmatch(branch)
|
||||
or ".." in branch
|
||||
or "//" in branch
|
||||
or "@{" in branch
|
||||
or branch.endswith(("/", ".", ".lock"))
|
||||
or any(part in {"", ".", ".."} for part in branch.split("/"))
|
||||
):
|
||||
raise ReleaseSourceAdmissionError("SOURCE_REF_INVALID")
|
||||
return text
|
||||
|
||||
|
||||
def _version(value: object) -> str:
|
||||
try:
|
||||
return validate_release_version(value)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ReleaseSourceAdmissionError("VERSION_INVALID") from exc
|
||||
|
||||
|
||||
def _version_slug(version: str) -> str:
|
||||
value = re.sub(r"[^A-Za-z0-9]+", "-", version.lower()).strip("-")
|
||||
if not value:
|
||||
raise ReleaseSourceAdmissionError("VERSION_INVALID")
|
||||
return value[:80]
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
def _secure_stat(path: Path, *, expected_owner_uid: int | None, directory: bool) -> None:
|
||||
try:
|
||||
info = path.lstat()
|
||||
except OSError as exc:
|
||||
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_STORE_UNAVAILABLE") from exc
|
||||
if path.is_symlink():
|
||||
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_STORE_UNSAFE")
|
||||
if directory and not path.is_dir():
|
||||
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_STORE_UNSAFE")
|
||||
if not directory and not path.is_file():
|
||||
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_STORE_UNSAFE")
|
||||
if expected_owner_uid is not None and info.st_uid != expected_owner_uid:
|
||||
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_OWNER_MISMATCH")
|
||||
# Group/other write would allow an untrusted process to forge release input.
|
||||
if info.st_mode & 0o022:
|
||||
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_PERMISSIONS_INVALID")
|
||||
|
||||
|
||||
def _validate_record(
|
||||
value: Mapping[str, Any],
|
||||
*,
|
||||
repository: str,
|
||||
policy_sha256: str,
|
||||
hmac_key: bytes,
|
||||
) -> dict[str, Any]:
|
||||
if not isinstance(value, Mapping):
|
||||
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_RECORD_INVALID")
|
||||
required = {
|
||||
"schema",
|
||||
"admission_id",
|
||||
"repository",
|
||||
"task_id",
|
||||
"version",
|
||||
"candidate_id",
|
||||
"base_oid",
|
||||
"source_ref",
|
||||
"source_commit",
|
||||
"remote_ref_oid",
|
||||
"tree_oid",
|
||||
"purpose",
|
||||
"policy_sha256",
|
||||
"admitted_at",
|
||||
"mac",
|
||||
}
|
||||
if set(value) != required or value.get("schema") != SCHEMA:
|
||||
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_RECORD_INVALID")
|
||||
record = dict(value)
|
||||
if _identifier(record["repository"], "repository") != repository:
|
||||
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_REPOSITORY_MISMATCH")
|
||||
_identifier(record["task_id"], "task_id")
|
||||
record["version"] = _version(record["version"])
|
||||
_identifier(record["candidate_id"], "candidate_id")
|
||||
_identifier(record["purpose"], "purpose")
|
||||
_identifier(record["admission_id"], "admission_id")
|
||||
record["base_oid"] = _oid(record["base_oid"], "base_oid")
|
||||
record["source_commit"] = _oid(record["source_commit"], "source_commit")
|
||||
record["remote_ref_oid"] = _oid(record["remote_ref_oid"], "remote_ref_oid")
|
||||
record["tree_oid"] = _oid(record["tree_oid"], "tree_oid")
|
||||
record["source_ref"] = _source_ref(record["source_ref"])
|
||||
record["policy_sha256"] = _digest(record["policy_sha256"], "policy_sha256")
|
||||
if record["policy_sha256"] != policy_sha256:
|
||||
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_POLICY_MISMATCH")
|
||||
if record["remote_ref_oid"] != record["source_commit"]:
|
||||
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_REMOTE_MISMATCH")
|
||||
admitted_at = record.get("admitted_at")
|
||||
if not isinstance(admitted_at, str) or not admitted_at.endswith("Z") or len(admitted_at) < 20:
|
||||
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_TIME_INVALID")
|
||||
supplied_mac = record.get("mac")
|
||||
if (
|
||||
not isinstance(supplied_mac, str)
|
||||
or not re.fullmatch(r"[0-9a-f]{64}", supplied_mac)
|
||||
):
|
||||
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_MAC_INVALID")
|
||||
unsigned = {key: value for key, value in record.items() if key != "mac"}
|
||||
expected_mac = hmac.new(hmac_key, _canonical(unsigned), hashlib.sha256).hexdigest()
|
||||
if not hmac.compare_digest(supplied_mac, expected_mac):
|
||||
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_MAC_INVALID")
|
||||
return record
|
||||
|
||||
|
||||
class ReleaseSourceBackend(Protocol):
|
||||
def authoritative_main_oid(self) -> str: ...
|
||||
def remote_ref_oid(self, source_ref: str) -> str: ...
|
||||
def fetch_exact_ref(self, source_ref: str, source_commit: str) -> str: ...
|
||||
def tree_oid(self, source_commit: str) -> str: ...
|
||||
def require_lineage(self, base_oid: str, source_commit: str) -> None: ...
|
||||
|
||||
|
||||
class ReleaseSourceAdmissionStore:
|
||||
"""Durable immutable admission records keyed by exact source commit."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
root: str | os.PathLike[str],
|
||||
*,
|
||||
repository: str,
|
||||
policy_sha256: str,
|
||||
hmac_key: bytes,
|
||||
expected_owner_uid: int | None = None,
|
||||
writable: bool = False,
|
||||
) -> None:
|
||||
self.root = Path(root)
|
||||
self.repository = _identifier(repository, "repository")
|
||||
self.policy_sha256 = _digest(policy_sha256, "policy_sha256")
|
||||
if not isinstance(hmac_key, (bytes, bytearray)) or len(hmac_key) < 32:
|
||||
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_HMAC_INVALID")
|
||||
self.hmac_key = bytes(hmac_key)
|
||||
self.expected_owner_uid = expected_owner_uid
|
||||
self.writable = bool(writable)
|
||||
if self.writable and not self.root.exists():
|
||||
self.root.mkdir(parents=True, mode=0o750)
|
||||
if self.root.exists():
|
||||
_secure_stat(
|
||||
self.root,
|
||||
expected_owner_uid=self.expected_owner_uid,
|
||||
directory=True,
|
||||
)
|
||||
|
||||
def _path(self, source_commit: str) -> Path:
|
||||
return self.root / f"{_oid(source_commit, 'source_commit')}.json"
|
||||
|
||||
def register(self, record: Mapping[str, Any]) -> dict[str, Any]:
|
||||
if not self.writable:
|
||||
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_STORE_READ_ONLY")
|
||||
supplied = dict(record)
|
||||
if "mac" not in supplied:
|
||||
supplied["mac"] = hmac.new(
|
||||
self.hmac_key,
|
||||
_canonical(supplied),
|
||||
hashlib.sha256,
|
||||
).hexdigest()
|
||||
checked = _validate_record(
|
||||
supplied,
|
||||
repository=self.repository,
|
||||
policy_sha256=self.policy_sha256,
|
||||
hmac_key=self.hmac_key,
|
||||
)
|
||||
path = self._path(checked["source_commit"])
|
||||
encoded = _canonical(checked) + b"\n"
|
||||
if path.exists():
|
||||
_secure_stat(
|
||||
path,
|
||||
expected_owner_uid=self.expected_owner_uid,
|
||||
directory=False,
|
||||
)
|
||||
try:
|
||||
existing = path.read_bytes()
|
||||
except OSError as exc:
|
||||
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_STORE_UNAVAILABLE") from exc
|
||||
if existing == encoded:
|
||||
return dict(checked)
|
||||
try:
|
||||
previous = _validate_record(
|
||||
json.loads(existing),
|
||||
repository=self.repository,
|
||||
policy_sha256=self.policy_sha256,
|
||||
hmac_key=self.hmac_key,
|
||||
)
|
||||
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
|
||||
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_RECORD_INVALID") from exc
|
||||
comparable_previous = {
|
||||
key: value
|
||||
for key, value in previous.items()
|
||||
if key not in {"admitted_at", "mac"}
|
||||
}
|
||||
comparable_checked = {
|
||||
key: value
|
||||
for key, value in checked.items()
|
||||
if key not in {"admitted_at", "mac"}
|
||||
}
|
||||
if comparable_previous == comparable_checked:
|
||||
return dict(previous)
|
||||
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_CONFLICT")
|
||||
self.root.mkdir(parents=True, exist_ok=True, mode=0o750)
|
||||
_secure_stat(
|
||||
self.root,
|
||||
expected_owner_uid=self.expected_owner_uid,
|
||||
directory=True,
|
||||
)
|
||||
fd, raw_name = tempfile.mkstemp(
|
||||
prefix=f".{checked['source_commit']}.",
|
||||
suffix=".tmp",
|
||||
dir=self.root,
|
||||
)
|
||||
temp_path = Path(raw_name)
|
||||
try:
|
||||
os.fchmod(fd, 0o640)
|
||||
with os.fdopen(fd, "wb", closefd=True) as handle:
|
||||
handle.write(encoded)
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
os.replace(temp_path, path)
|
||||
os.chmod(path, 0o640)
|
||||
directory_fd = os.open(self.root, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
|
||||
try:
|
||||
os.fsync(directory_fd)
|
||||
finally:
|
||||
os.close(directory_fd)
|
||||
except Exception:
|
||||
try:
|
||||
os.close(fd)
|
||||
except OSError:
|
||||
pass
|
||||
temp_path.unlink(missing_ok=True)
|
||||
raise
|
||||
_secure_stat(
|
||||
path,
|
||||
expected_owner_uid=self.expected_owner_uid,
|
||||
directory=False,
|
||||
)
|
||||
return dict(checked)
|
||||
|
||||
def lookup_source(self, source_commit: str) -> dict[str, Any] | None:
|
||||
path = self._path(source_commit)
|
||||
if not path.exists():
|
||||
return None
|
||||
_secure_stat(
|
||||
path,
|
||||
expected_owner_uid=self.expected_owner_uid,
|
||||
directory=False,
|
||||
)
|
||||
try:
|
||||
raw = path.read_bytes()
|
||||
if len(raw) > 64 * 1024:
|
||||
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_RECORD_INVALID")
|
||||
decoded = json.loads(raw)
|
||||
except ReleaseSourceAdmissionError:
|
||||
raise
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_RECORD_INVALID") from exc
|
||||
return _validate_record(
|
||||
decoded,
|
||||
repository=self.repository,
|
||||
policy_sha256=self.policy_sha256,
|
||||
hmac_key=self.hmac_key,
|
||||
)
|
||||
|
||||
def resolve_recipe(
|
||||
self,
|
||||
version: str,
|
||||
candidate_id: str | None,
|
||||
source_commit: str,
|
||||
) -> dict[str, str]:
|
||||
record = self.lookup_source(source_commit)
|
||||
if record is None:
|
||||
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_NOT_FOUND")
|
||||
if record["version"] != version:
|
||||
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_VERSION_MISMATCH")
|
||||
supplied_candidate = str(candidate_id or "")
|
||||
if supplied_candidate and supplied_candidate != record["candidate_id"]:
|
||||
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_CANDIDATE_MISMATCH")
|
||||
return {
|
||||
"mode": RECIPE_MODE,
|
||||
"task_id": record["task_id"],
|
||||
"candidate_id": record["candidate_id"],
|
||||
"base_oid": record["base_oid"],
|
||||
"source_commit": record["source_commit"],
|
||||
"version": record["version"],
|
||||
"purpose": record["purpose"],
|
||||
}
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class GiteaReleaseSourceBackend:
|
||||
transport: GiteaMachineTransport
|
||||
git_dir: Path
|
||||
|
||||
def authoritative_main_oid(self) -> str:
|
||||
return authoritative_head_oid(git_dir=self.git_dir)
|
||||
|
||||
def remote_ref_oid(self, source_ref: str) -> str:
|
||||
ref = _source_ref(source_ref)
|
||||
branch = ref[len(_REF_PREFIX):]
|
||||
value = self.transport.read_branch(branch)
|
||||
if value is None:
|
||||
raise ReleaseSourceAdmissionError("SOURCE_REF_NOT_FOUND")
|
||||
return _oid(value, "remote_ref_oid")
|
||||
|
||||
def fetch_exact_ref(self, source_ref: str, source_commit: str) -> str:
|
||||
ref = _source_ref(source_ref)
|
||||
branch = ref[len(_REF_PREFIX):]
|
||||
try:
|
||||
value = self.transport.fetch_branch_exact(
|
||||
branch,
|
||||
_oid(source_commit, "source_commit"),
|
||||
)
|
||||
except GiteaTransportError as exc:
|
||||
raise ReleaseSourceAdmissionError("SOURCE_FETCH_FAILED") from exc
|
||||
return _oid(value, "fetched_source_commit")
|
||||
|
||||
def tree_oid(self, source_commit: str) -> str:
|
||||
source = _oid(source_commit, "source_commit")
|
||||
try:
|
||||
result = run_authoritative_git(
|
||||
["rev-parse", "--verify", f"{source}^{{tree}}"],
|
||||
git_dir=self.git_dir,
|
||||
)
|
||||
except AuthoritativeGitError as exc:
|
||||
raise ReleaseSourceAdmissionError("SOURCE_COMMIT_NOT_FOUND") from exc
|
||||
return _oid(result.stdout.strip().lower(), "tree_oid")
|
||||
|
||||
def require_lineage(self, base_oid: str, source_commit: str) -> None:
|
||||
try:
|
||||
authoritative_lineage(
|
||||
_oid(base_oid, "base_oid"),
|
||||
_oid(source_commit, "source_commit"),
|
||||
git_dir=self.git_dir,
|
||||
)
|
||||
except AuthoritativeGitError as exc:
|
||||
raise ReleaseSourceAdmissionError("SOURCE_LINEAGE_MISMATCH") from exc
|
||||
|
||||
|
||||
class ReleaseSourceAdmitter:
|
||||
def __init__(
|
||||
self,
|
||||
store: ReleaseSourceAdmissionStore,
|
||||
backend: ReleaseSourceBackend,
|
||||
*,
|
||||
purpose: str = "AIOS_TRUSTED_RELEASE_BUILDER",
|
||||
) -> None:
|
||||
if not store.writable:
|
||||
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_STORE_READ_ONLY")
|
||||
self.store = store
|
||||
self.backend = backend
|
||||
self.purpose = _identifier(purpose, "purpose")
|
||||
|
||||
def admit(
|
||||
self,
|
||||
*,
|
||||
version: str,
|
||||
task_id: str,
|
||||
source_ref: str,
|
||||
source_commit: str,
|
||||
) -> dict[str, Any]:
|
||||
version_value = _version(version)
|
||||
task_value = _identifier(task_id, "task_id")
|
||||
ref_value = _source_ref(source_ref)
|
||||
source_value = _oid(source_commit, "source_commit")
|
||||
|
||||
remote_before = self.backend.remote_ref_oid(ref_value)
|
||||
if remote_before != source_value:
|
||||
raise ReleaseSourceAdmissionError("SOURCE_REF_MOVED")
|
||||
base_oid = _oid(self.backend.authoritative_main_oid(), "base_oid")
|
||||
fetched = _oid(
|
||||
self.backend.fetch_exact_ref(ref_value, source_value),
|
||||
"fetched_source_commit",
|
||||
)
|
||||
if fetched != source_value:
|
||||
raise ReleaseSourceAdmissionError("SOURCE_FETCH_MISMATCH")
|
||||
remote_after = self.backend.remote_ref_oid(ref_value)
|
||||
if remote_after != source_value:
|
||||
raise ReleaseSourceAdmissionError("SOURCE_REF_MOVED")
|
||||
self.backend.require_lineage(base_oid, source_value)
|
||||
tree_oid = _oid(self.backend.tree_oid(source_value), "tree_oid")
|
||||
|
||||
candidate_id = f"rc-{_version_slug(version_value)}-{source_value[:12]}"
|
||||
admission_id = "source-admission-" + hashlib.sha256(
|
||||
f"{self.store.repository}\0{version_value}\0{source_value}\0{ref_value}".encode("utf-8")
|
||||
).hexdigest()[:24]
|
||||
record = {
|
||||
"schema": SCHEMA,
|
||||
"admission_id": admission_id,
|
||||
"repository": self.store.repository,
|
||||
"task_id": task_value,
|
||||
"version": version_value,
|
||||
"candidate_id": candidate_id,
|
||||
"base_oid": base_oid,
|
||||
"source_ref": ref_value,
|
||||
"source_commit": source_value,
|
||||
"remote_ref_oid": remote_after,
|
||||
"tree_oid": tree_oid,
|
||||
"purpose": self.purpose,
|
||||
"policy_sha256": self.store.policy_sha256,
|
||||
"admitted_at": _now(),
|
||||
}
|
||||
return self.store.register(record)
|
||||
|
||||
|
||||
def _sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
admit = subparsers.add_parser("admit")
|
||||
admit.add_argument("--version", required=True)
|
||||
admit.add_argument("--task-id", required=True)
|
||||
admit.add_argument("--source-ref", required=True)
|
||||
admit.add_argument("--source-commit", required=True)
|
||||
admit.add_argument("--policy", required=True, type=Path)
|
||||
admit.add_argument("--store-root", required=True, type=Path)
|
||||
arguments = parser.parse_args()
|
||||
|
||||
if arguments.command != "admit":
|
||||
raise SystemExit("unsupported command")
|
||||
repository = os.environ.get("AIOS_GITOPS_REPOSITORY_IDENTITY", "")
|
||||
api_base = os.environ.get("AIOS_GITEA_API_BASE", "")
|
||||
remote_url = os.environ.get("AIOS_GITEA_REMOTE_URL", "")
|
||||
raw_git_dir = os.environ.get("AIOS_GITOPS_AUTHORITATIVE_GIT_DIR", "")
|
||||
if not repository or not api_base or not remote_url or not raw_git_dir:
|
||||
raise SystemExit("release source admission environment is incomplete")
|
||||
git_dir = Path(raw_git_dir)
|
||||
transport = GiteaMachineTransport(
|
||||
api_base=api_base,
|
||||
repository_id=repository,
|
||||
remote_url=remote_url,
|
||||
authority_git_dir=git_dir,
|
||||
credential_name=os.environ.get(
|
||||
"AIOS_GITEA_MACHINE_CREDENTIAL_NAME",
|
||||
"gitea-machine-token",
|
||||
),
|
||||
)
|
||||
credentials = os.environ.get("CREDENTIALS_DIRECTORY", "")
|
||||
if not credentials:
|
||||
raise SystemExit("CREDENTIALS_DIRECTORY is required")
|
||||
try:
|
||||
admission_hmac = (Path(credentials) / "source_admission_hmac").read_bytes().strip()
|
||||
except OSError as exc:
|
||||
raise SystemExit("source_admission_hmac credential is required") from exc
|
||||
store = ReleaseSourceAdmissionStore(
|
||||
arguments.store_root,
|
||||
repository=repository,
|
||||
policy_sha256=_sha256_file(arguments.policy),
|
||||
hmac_key=admission_hmac,
|
||||
expected_owner_uid=os.geteuid() if hasattr(os, "geteuid") else None,
|
||||
writable=True,
|
||||
)
|
||||
result = ReleaseSourceAdmitter(
|
||||
store,
|
||||
GiteaReleaseSourceBackend(transport=transport, git_dir=git_dir),
|
||||
).admit(
|
||||
version=arguments.version,
|
||||
task_id=arguments.task_id,
|
||||
source_ref=arguments.source_ref,
|
||||
source_commit=arguments.source_commit,
|
||||
)
|
||||
print(json.dumps(result, sort_keys=True, separators=(",", ":")))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_main()
|
||||
@@ -3531,6 +3531,7 @@ class SilentReleaseActionService:
|
||||
trusted_policy: TrustedReleasePolicy | None = None,
|
||||
orchestrator: ReleaseOrchestrator | None = None,
|
||||
builder_readiness_probe: Callable[[], Mapping[str, Any]] | None = None,
|
||||
source_admission_resolver: Callable[[str], Mapping[str, Any] | None] | None = None,
|
||||
allow_legacy_candidate_input: bool = False,
|
||||
expected_repository: str = "",
|
||||
policy_sha256: str = "",
|
||||
@@ -3547,6 +3548,7 @@ class SilentReleaseActionService:
|
||||
self.trusted_policy = trusted_policy
|
||||
self.orchestrator = orchestrator
|
||||
self.builder_readiness_probe = builder_readiness_probe
|
||||
self.source_admission_resolver = source_admission_resolver
|
||||
self.allow_legacy_candidate_input = bool(
|
||||
allow_legacy_candidate_input
|
||||
)
|
||||
@@ -3638,6 +3640,40 @@ class SilentReleaseActionService:
|
||||
version = str(payload.get("version") or "")
|
||||
if version:
|
||||
_validate_identifier("version", version)
|
||||
|
||||
source_admission: dict[str, Any] | None = None
|
||||
resolver = self.source_admission_resolver
|
||||
if resolver is not None:
|
||||
try:
|
||||
raw_admission = resolver(source_commit)
|
||||
except Exception as exc:
|
||||
raise CapabilityAuthorizationError(
|
||||
"release source admission is invalid"
|
||||
) from exc
|
||||
if raw_admission is not None:
|
||||
source_admission = dict(raw_admission)
|
||||
admitted_source = str(
|
||||
source_admission.get("source_commit") or ""
|
||||
)
|
||||
admitted_version = str(source_admission.get("version") or "")
|
||||
admission_id = str(
|
||||
source_admission.get("admission_id") or ""
|
||||
)
|
||||
if (
|
||||
admitted_source != source_commit
|
||||
or not admitted_version
|
||||
or not admission_id
|
||||
):
|
||||
raise CapabilityAuthorizationError(
|
||||
"release source admission is invalid"
|
||||
)
|
||||
_validate_identifier("version", admitted_version)
|
||||
_validate_identifier("admission_id", admission_id)
|
||||
if version and version != admitted_version:
|
||||
raise CapabilityAuthorizationError(
|
||||
"release source admission version mismatch"
|
||||
)
|
||||
version = admitted_version
|
||||
intent_id = "intent-" + hashlib.sha256(
|
||||
f"{event_id}\0{source_commit}".encode("utf-8")
|
||||
).hexdigest()[:24]
|
||||
@@ -3657,12 +3693,16 @@ class SilentReleaseActionService:
|
||||
}
|
||||
if version:
|
||||
record["version"] = version
|
||||
if source_admission is not None:
|
||||
record["source_admission_id"] = source_admission["admission_id"]
|
||||
if was_existing:
|
||||
existing = json.loads(path.read_text(encoding="utf-8"))
|
||||
if (
|
||||
existing.get("event_id") != event_id
|
||||
or existing.get("source_commit") != source_commit
|
||||
or str(existing.get("version") or "") != version
|
||||
or str(existing.get("source_admission_id") or "")
|
||||
!= str(record.get("source_admission_id") or "")
|
||||
):
|
||||
raise SagaConflict(
|
||||
"release intent id is bound to different evidence"
|
||||
@@ -3742,14 +3782,23 @@ class SilentReleaseActionService:
|
||||
)
|
||||
_durable_json(path, record)
|
||||
return self._intent_response(record, idempotent=False)
|
||||
except CapabilityAuthorizationError:
|
||||
record.update(
|
||||
{
|
||||
"failure_code": "TRUSTED_BUILDER_REJECTED",
|
||||
"retryable": False,
|
||||
"status": "BUILDER_TERMINAL_FAILURE",
|
||||
}
|
||||
)
|
||||
except CapabilityAuthorizationError as exc:
|
||||
if str(exc) == "release source admission required":
|
||||
record.update(
|
||||
{
|
||||
"failure_code": "SOURCE_ADMISSION_REQUIRED",
|
||||
"retryable": False,
|
||||
"status": "SOURCE_ADMISSION_REQUIRED",
|
||||
}
|
||||
)
|
||||
else:
|
||||
record.update(
|
||||
{
|
||||
"failure_code": "TRUSTED_BUILDER_REJECTED",
|
||||
"retryable": False,
|
||||
"status": "BUILDER_TERMINAL_FAILURE",
|
||||
}
|
||||
)
|
||||
_durable_json(path, record)
|
||||
return self._intent_response(record, idempotent=False)
|
||||
except (ConnectionError, OSError, TimeoutError):
|
||||
|
||||
@@ -7,8 +7,10 @@ Environment=AIOS_TRUSTED_CANDIDATE_BUILDER_SOCKET=/run/aios-trusted-candidate-bu
|
||||
Environment=AIOS_TRUSTED_CANDIDATE_BUILDER_POLICY=/opt/aios/gitops-model-api/<VERIFIED_PACKAGE_OID>/runtime/config/gitops/trusted-candidate-builder-policy.json
|
||||
Environment=AIOS_TRUSTED_CANDIDATE_BUILDER_PUBLIC_KEYRING=/etc/aios-gitops/trusted-builder/public-keyring.json
|
||||
Environment=AIOS_TRUSTED_CANDIDATE_BUILDER_CREDENTIALS_DIRECTORY=%d
|
||||
Environment=AIOS_TRUSTED_BUILDER_SOURCE_ADMISSION_ROOT=/var/lib/aios-release-source-admission
|
||||
Environment=AIOS_SILENT_RELEASE_TRUSTED_POLICY=/opt/aios/gitops-model-api/<VERIFIED_PACKAGE_OID>/runtime/config/gitops/trusted-release-policy.json
|
||||
LoadCredential=builder_hmac:/etc/aios-gitops/credential-references/trusted-candidate-builder-hmac
|
||||
LoadCredential=source_admission_hmac:/etc/aios-gitops/credential-references/release-source-admission-hmac
|
||||
# The credential source above is a reference only; no secret value belongs here.
|
||||
# Keep execution shadow-disabled and do not add Owner/deploy/tag/baseline switches.
|
||||
Environment=AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
|
||||
|
||||
@@ -27,6 +27,7 @@ Environment=AIOS_GITOPS_DEPLOYMENT_STATE=STAGING
|
||||
Environment=AIOS_GITOPS_MODEL_API_SERVICE=aios-gitops-model-api-v2r2c1-staging.service
|
||||
Environment=AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
|
||||
Environment=AIOS_SILENT_RELEASE_STATE_ROOT=/var/lib/aios-gitops/model-api-staging/silent-release
|
||||
Environment=AIOS_TRUSTED_BUILDER_SOURCE_ADMISSION_ROOT=/var/lib/aios-release-source-admission
|
||||
Environment=AIOS_BRIDGE_V2_URL=http://127.0.0.1:18892
|
||||
Environment=AIOS_GITOPS_HOST=127.0.0.1
|
||||
Environment=AIOS_GITOPS_PORT=18893
|
||||
@@ -44,7 +45,7 @@ ProtectControlGroups=true
|
||||
LockPersonality=true
|
||||
RestrictSUIDSGID=true
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
ReadOnlyPaths=@RUNTIME_ROOT@ /data/aios-gitops/repo.git /var/lib/aios-gitops-runtime-anchor/baseline
|
||||
ReadOnlyPaths=@RUNTIME_ROOT@ /data/aios-gitops/repo.git /var/lib/aios-gitops-runtime-anchor/baseline -/var/lib/aios-release-source-admission
|
||||
ReadWritePaths=/var/lib/aios-gitops/model-api-staging
|
||||
|
||||
[Install]
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
[Unit]
|
||||
Description=AIOS governed release source admission
|
||||
After=network-online.target aios-gitea.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=root
|
||||
Group=gitops
|
||||
UMask=0027
|
||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||
Environment=AIOS_GITOPS_REPOSITORY_IDENTITY=aios/aios
|
||||
Environment=AIOS_GITOPS_AUTHORITATIVE_GIT_DIR=/data/aios-gitops/repo.git
|
||||
Environment=AIOS_GITEA_API_BASE=https://g.cnnetmkt.top/git/api/v1
|
||||
Environment=AIOS_GITEA_REMOTE_URL=https://g.cnnetmkt.top/git/aios/aios.git
|
||||
Environment=AIOS_GITEA_MACHINE_CREDENTIAL_NAME=gitea-machine-token
|
||||
EnvironmentFile=/run/aios-release-source-admission/request.env
|
||||
LoadCredentialEncrypted=gitea-machine-token:/etc/aios/credentials/gitea-machine.token.cred
|
||||
LoadCredentialEncrypted=source_admission_hmac:/etc/credstore.encrypted/aios-release-source-admission/source_admission_hmac
|
||||
ExecStart=/usr/bin/python3 -B /opt/aios/release-source-admission/current/app/release_source_admission.py admit --version ${AIOS_RELEASE_VERSION} --task-id ${AIOS_RELEASE_TASK_ID} --source-ref ${AIOS_RELEASE_SOURCE_REF} --source-commit ${AIOS_RELEASE_SOURCE_COMMIT} --policy /opt/aios/trusted-candidate-builder/current/config/gitops/trusted-candidate-builder-policy.json --store-root /var/lib/aios-release-source-admission
|
||||
ExecStartPost=/usr/bin/rm -f /run/aios-release-source-admission/request.env
|
||||
RuntimeDirectory=aios-release-source-admission
|
||||
RuntimeDirectoryMode=0750
|
||||
StateDirectory=aios-release-source-admission
|
||||
StateDirectoryMode=0750
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
LockPersonality=true
|
||||
RestrictSUIDSGID=true
|
||||
RestrictRealtime=true
|
||||
MemoryDenyWriteExecute=true
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
CapabilityBoundingSet=
|
||||
ReadOnlyPaths=/opt/aios/release-source-admission /opt/aios/trusted-candidate-builder /etc/aios/credentials /etc/credstore.encrypted
|
||||
ReadWritePaths=/data/aios-gitops/repo.git /var/lib/aios-release-source-admission /run/aios-release-source-admission
|
||||
@@ -10,10 +10,12 @@ Group=root
|
||||
UMask=0077
|
||||
LoadCredentialEncrypted=signing_private_key:/etc/credstore.encrypted/aios-trusted-candidate-builder/signing_private_key
|
||||
LoadCredentialEncrypted=builder_hmac:/etc/credstore.encrypted/aios-trusted-candidate-builder/builder_hmac
|
||||
LoadCredentialEncrypted=source_admission_hmac:/etc/credstore.encrypted/aios-release-source-admission/source_admission_hmac
|
||||
Environment=AIOS_TRUSTED_BUILDER_POLICY=/opt/aios/trusted-candidate-builder/current/config/gitops/trusted-candidate-builder-policy.json
|
||||
Environment=AIOS_TRUSTED_BUILDER_GIT_DIR=/data/aios-gitops/repo.git
|
||||
Environment=AIOS_TRUSTED_BUILDER_EVIDENCE_ROOT=/var/lib/aios-trusted-candidate-builder/evidence
|
||||
Environment=AIOS_TRUSTED_BUILDER_NONCE_LEDGER=/var/lib/aios-trusted-candidate-builder/nonce-ledger.json
|
||||
Environment=AIOS_TRUSTED_BUILDER_SOURCE_ADMISSION_ROOT=/var/lib/aios-release-source-admission
|
||||
Environment=AIOS_TRUSTED_BUILDER_KEY_ID=builder-attestation-ed25519-g1
|
||||
Environment=AIOS_TRUSTED_BUILDER_KEY_GENERATION=1
|
||||
Environment=PYTHONDONTWRITEBYTECODE=1
|
||||
@@ -37,7 +39,7 @@ MemoryDenyWriteExecute=true
|
||||
RestrictSUIDSGID=true
|
||||
SystemCallArchitectures=native
|
||||
CapabilityBoundingSet=
|
||||
ReadOnlyPaths=/opt/aios/trusted-candidate-builder /data/aios-gitops/repo.git /var/lib/aios-trusted-candidate-builder/evidence
|
||||
ReadOnlyPaths=/opt/aios/trusted-candidate-builder /data/aios-gitops/repo.git /var/lib/aios-trusted-candidate-builder/evidence -/var/lib/aios-release-source-admission
|
||||
ReadWritePaths=/var/lib/aios-trusted-candidate-builder
|
||||
|
||||
[Install]
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
from types import SimpleNamespace
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
import aios_gitops_job_executor as executor_module
|
||||
from aios_authority_git_executor import AuthorityGitError, AuthorityGitExecutor
|
||||
import aios_gitea_machine_transport as gitea_transport_module
|
||||
from aios_gitea_machine_transport import GiteaMachineTransport
|
||||
from aios_gitea_machine_broker import CampaignCapabilityReceipt, GiteaMachineBroker, MachineCredentialProjection
|
||||
from aios_gitops_job_controller import GitOpsJobController, JobError, JobStore, JobState
|
||||
@@ -162,3 +164,77 @@ def test_machine_transport_branch_cas_is_not_force_push(monkeypatch):
|
||||
monkeypatch.setattr(GiteaMachineTransport, "_push_branch_fast_forward", lambda _self, branch, new: calls.append((branch, new)))
|
||||
assert transport.compare_and_swap_branch(executor_module.BRANCH, OLD, NEW) == NEW
|
||||
assert calls == [(executor_module.BRANCH, NEW)]
|
||||
|
||||
|
||||
def test_machine_transport_fetches_exact_branch_tip_into_quarantine_ref(monkeypatch, tmp_path):
|
||||
transport = GiteaMachineTransport(
|
||||
"http://127.0.0.1:3000/api/v1",
|
||||
"aios/aios",
|
||||
"https://g.cnnetmkt.top/git/aios/aios.git",
|
||||
tmp_path / "repo.git",
|
||||
)
|
||||
reads = iter([NEW, NEW])
|
||||
monkeypatch.setattr(
|
||||
GiteaMachineTransport,
|
||||
"read_branch",
|
||||
lambda _self, _branch: next(reads),
|
||||
)
|
||||
askpass = tmp_path / "askpass"
|
||||
askpass.write_text("#!/bin/sh\n", encoding="utf-8")
|
||||
monkeypatch.setattr(
|
||||
GiteaMachineTransport,
|
||||
"_askpass",
|
||||
lambda _self: (askpass, {}),
|
||||
)
|
||||
calls = []
|
||||
|
||||
def run(argv, **kwargs):
|
||||
calls.append(list(argv))
|
||||
if "fetch" in argv:
|
||||
return SimpleNamespace(returncode=0, stdout="", stderr="")
|
||||
if "rev-parse" in argv:
|
||||
# Before fetch the quarantine ref is absent; after fetch it resolves.
|
||||
seen_fetch = any("fetch" in call for call in calls[:-1])
|
||||
return SimpleNamespace(
|
||||
returncode=0 if seen_fetch else 1,
|
||||
stdout=(NEW + "\n") if seen_fetch else "",
|
||||
stderr="",
|
||||
)
|
||||
if "update-ref" in argv:
|
||||
return SimpleNamespace(returncode=0, stdout="", stderr="")
|
||||
raise AssertionError(argv)
|
||||
|
||||
monkeypatch.setattr(gitea_transport_module.subprocess, "run", run)
|
||||
|
||||
assert transport.fetch_branch_exact(executor_module.BRANCH, NEW) == NEW
|
||||
fetch = next(call for call in calls if "fetch" in call)
|
||||
assert "--no-tags" in fetch
|
||||
assert "--no-write-fetch-head" in fetch
|
||||
assert f"refs/heads/{executor_module.BRANCH}:refs/aios/release-sources/{NEW}" in fetch
|
||||
assert not any("--force" in item or item == "-f" for item in fetch)
|
||||
|
||||
|
||||
def test_machine_transport_does_not_fetch_when_remote_tip_moved(monkeypatch, tmp_path):
|
||||
transport = GiteaMachineTransport(
|
||||
"http://127.0.0.1:3000/api/v1",
|
||||
"aios/aios",
|
||||
"https://g.cnnetmkt.top/git/aios/aios.git",
|
||||
tmp_path / "repo.git",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
GiteaMachineTransport,
|
||||
"read_branch",
|
||||
lambda _self, _branch: OLD,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
gitea_transport_module.subprocess,
|
||||
"run",
|
||||
lambda *args, **kwargs: (_ for _ in ()).throw(
|
||||
AssertionError("git must not run after remote mismatch")
|
||||
),
|
||||
)
|
||||
with pytest.raises(
|
||||
gitea_transport_module.GiteaTransportError,
|
||||
match="GITEA_SOURCE_REF_MISMATCH",
|
||||
):
|
||||
transport.fetch_branch_exact(executor_module.BRANCH, NEW)
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
|
||||
|
||||
from release_source_admission import ( # noqa: E402
|
||||
ReleaseSourceAdmissionError,
|
||||
ReleaseSourceAdmissionStore,
|
||||
ReleaseSourceAdmitter,
|
||||
)
|
||||
|
||||
|
||||
MAIN = "1" * 40
|
||||
SOURCE = "2" * 40
|
||||
TREE = "3" * 40
|
||||
POLICY = "4" * 64
|
||||
REF = "refs/heads/agent/baseline-1.1-principal-recovery-20260908"
|
||||
VERSION = "V2R2C4"
|
||||
TASK = "AIOS-BASELINE-1.1-PRODUCTION-CLOSURE"
|
||||
|
||||
|
||||
class FakeBackend:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
remote: str = SOURCE,
|
||||
fetched: str = SOURCE,
|
||||
tree: str = TREE,
|
||||
main: str = MAIN,
|
||||
lineage: bool = True,
|
||||
) -> None:
|
||||
self.remote = remote
|
||||
self.fetched = fetched
|
||||
self.tree = tree
|
||||
self.main = main
|
||||
self.lineage = lineage
|
||||
self.fetch_calls: list[tuple[str, str]] = []
|
||||
self.lineage_calls: list[tuple[str, str]] = []
|
||||
|
||||
def authoritative_main_oid(self) -> str:
|
||||
return self.main
|
||||
|
||||
def remote_ref_oid(self, source_ref: str) -> str:
|
||||
assert source_ref == REF
|
||||
return self.remote
|
||||
|
||||
def fetch_exact_ref(self, source_ref: str, source_commit: str) -> str:
|
||||
self.fetch_calls.append((source_ref, source_commit))
|
||||
return self.fetched
|
||||
|
||||
def tree_oid(self, source_commit: str) -> str:
|
||||
assert source_commit == SOURCE
|
||||
return self.tree
|
||||
|
||||
def require_lineage(self, base_oid: str, source_commit: str) -> None:
|
||||
self.lineage_calls.append((base_oid, source_commit))
|
||||
if not self.lineage:
|
||||
raise ReleaseSourceAdmissionError("SOURCE_LINEAGE_MISMATCH")
|
||||
|
||||
|
||||
def make_store(tmp_path: Path, *, writable: bool = True) -> ReleaseSourceAdmissionStore:
|
||||
owner = os.geteuid() if hasattr(os, "geteuid") else None
|
||||
return ReleaseSourceAdmissionStore(
|
||||
tmp_path / "admissions",
|
||||
repository="aios/aios",
|
||||
policy_sha256=POLICY,
|
||||
hmac_key=b"h" * 32,
|
||||
expected_owner_uid=owner,
|
||||
writable=writable,
|
||||
)
|
||||
|
||||
|
||||
def test_admit_binds_remote_ref_fetch_tree_main_lineage_and_recipe(tmp_path: Path) -> None:
|
||||
store = make_store(tmp_path)
|
||||
backend = FakeBackend()
|
||||
|
||||
record = ReleaseSourceAdmitter(store, backend).admit(
|
||||
version=VERSION,
|
||||
task_id=TASK,
|
||||
source_ref=REF,
|
||||
source_commit=SOURCE,
|
||||
)
|
||||
|
||||
assert record["schema"] == "aios.release-source-admission.v1"
|
||||
assert record["repository"] == "aios/aios"
|
||||
assert record["source_commit"] == SOURCE
|
||||
assert record["remote_ref_oid"] == SOURCE
|
||||
assert record["source_ref"] == REF
|
||||
assert record["base_oid"] == MAIN
|
||||
assert record["tree_oid"] == TREE
|
||||
assert record["version"] == VERSION
|
||||
assert record["task_id"] == TASK
|
||||
assert record["candidate_id"] == "rc-v2r2c4-" + SOURCE[:12]
|
||||
assert backend.fetch_calls == [(REF, SOURCE)]
|
||||
assert backend.lineage_calls == [(MAIN, SOURCE)]
|
||||
|
||||
recipe = store.resolve_recipe(VERSION, None, SOURCE)
|
||||
assert recipe == {
|
||||
"mode": "governed-source-admission",
|
||||
"task_id": TASK,
|
||||
"candidate_id": record["candidate_id"],
|
||||
"base_oid": MAIN,
|
||||
"source_commit": SOURCE,
|
||||
"version": VERSION,
|
||||
"purpose": "AIOS_TRUSTED_RELEASE_BUILDER",
|
||||
}
|
||||
|
||||
|
||||
def test_repeated_exact_admission_is_idempotent(tmp_path: Path) -> None:
|
||||
store = make_store(tmp_path)
|
||||
admitter = ReleaseSourceAdmitter(store, FakeBackend())
|
||||
|
||||
first = admitter.admit(
|
||||
version=VERSION,
|
||||
task_id=TASK,
|
||||
source_ref=REF,
|
||||
source_commit=SOURCE,
|
||||
)
|
||||
second = admitter.admit(
|
||||
version=VERSION,
|
||||
task_id=TASK,
|
||||
source_ref=REF,
|
||||
source_commit=SOURCE,
|
||||
)
|
||||
|
||||
assert second == first
|
||||
assert len(list((tmp_path / "admissions").glob("*.json"))) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("backend", "error"),
|
||||
(
|
||||
(FakeBackend(remote="5" * 40), "SOURCE_REF_MOVED"),
|
||||
(FakeBackend(fetched="5" * 40), "SOURCE_FETCH_MISMATCH"),
|
||||
(FakeBackend(lineage=False), "SOURCE_LINEAGE_MISMATCH"),
|
||||
),
|
||||
)
|
||||
def test_admission_rejects_moved_fetch_mismatch_or_non_descendant(
|
||||
tmp_path: Path,
|
||||
backend: FakeBackend,
|
||||
error: str,
|
||||
) -> None:
|
||||
store = make_store(tmp_path)
|
||||
with pytest.raises(ReleaseSourceAdmissionError, match=error):
|
||||
ReleaseSourceAdmitter(store, backend).admit(
|
||||
version=VERSION,
|
||||
task_id=TASK,
|
||||
source_ref=REF,
|
||||
source_commit=SOURCE,
|
||||
)
|
||||
assert not list((tmp_path / "admissions").glob("*.json"))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"source_ref",
|
||||
(
|
||||
"agent/bad",
|
||||
"refs/tags/v1",
|
||||
"refs/heads/../main",
|
||||
"refs/heads/agent//bad",
|
||||
"refs/heads/agent/bad.lock",
|
||||
),
|
||||
)
|
||||
def test_admission_rejects_unsafe_or_non_head_refs(
|
||||
tmp_path: Path, source_ref: str
|
||||
) -> None:
|
||||
store = make_store(tmp_path)
|
||||
with pytest.raises(ReleaseSourceAdmissionError, match="SOURCE_REF_INVALID"):
|
||||
ReleaseSourceAdmitter(store, FakeBackend()).admit(
|
||||
version=VERSION,
|
||||
task_id=TASK,
|
||||
source_ref=source_ref,
|
||||
source_commit=SOURCE,
|
||||
)
|
||||
|
||||
|
||||
def test_store_rejects_caller_candidate_rebinding(tmp_path: Path) -> None:
|
||||
store = make_store(tmp_path)
|
||||
record = ReleaseSourceAdmitter(store, FakeBackend()).admit(
|
||||
version=VERSION,
|
||||
task_id=TASK,
|
||||
source_ref=REF,
|
||||
source_commit=SOURCE,
|
||||
)
|
||||
with pytest.raises(
|
||||
ReleaseSourceAdmissionError,
|
||||
match="SOURCE_ADMISSION_CANDIDATE_MISMATCH",
|
||||
):
|
||||
store.resolve_recipe(VERSION, "rc-attacker", SOURCE)
|
||||
assert record["candidate_id"] != "rc-attacker"
|
||||
|
||||
|
||||
def test_store_rejects_policy_change_and_record_tamper(tmp_path: Path) -> None:
|
||||
store = make_store(tmp_path)
|
||||
record = ReleaseSourceAdmitter(store, FakeBackend()).admit(
|
||||
version=VERSION,
|
||||
task_id=TASK,
|
||||
source_ref=REF,
|
||||
source_commit=SOURCE,
|
||||
)
|
||||
path = tmp_path / "admissions" / f"{SOURCE}.json"
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
payload["task_id"] = "other-task"
|
||||
path.write_text(
|
||||
json.dumps(payload, sort_keys=True, separators=(",", ":")) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
ReleaseSourceAdmissionError,
|
||||
match="SOURCE_ADMISSION_MAC_INVALID",
|
||||
):
|
||||
store.lookup_source(SOURCE)
|
||||
|
||||
owner = os.geteuid() if hasattr(os, "geteuid") else None
|
||||
other_policy = ReleaseSourceAdmissionStore(
|
||||
tmp_path / "admissions",
|
||||
repository="aios/aios",
|
||||
policy_sha256="6" * 64,
|
||||
hmac_key=b"h" * 32,
|
||||
expected_owner_uid=owner,
|
||||
writable=False,
|
||||
)
|
||||
with pytest.raises(
|
||||
ReleaseSourceAdmissionError,
|
||||
match="SOURCE_ADMISSION_POLICY_MISMATCH",
|
||||
):
|
||||
other_policy.lookup_source(SOURCE)
|
||||
|
||||
|
||||
def test_store_rejects_group_writable_record(tmp_path: Path) -> None:
|
||||
store = make_store(tmp_path)
|
||||
ReleaseSourceAdmitter(store, FakeBackend()).admit(
|
||||
version=VERSION,
|
||||
task_id=TASK,
|
||||
source_ref=REF,
|
||||
source_commit=SOURCE,
|
||||
)
|
||||
path = tmp_path / "admissions" / f"{SOURCE}.json"
|
||||
os.chmod(path, 0o660)
|
||||
with pytest.raises(
|
||||
ReleaseSourceAdmissionError,
|
||||
match="SOURCE_ADMISSION_PERMISSIONS_INVALID",
|
||||
):
|
||||
store.lookup_source(SOURCE)
|
||||
@@ -726,3 +726,69 @@ def test_systemd_shadow_unit_has_both_gates_and_no_secret_environment():
|
||||
assert "PRIVATE_KEY=" not in unit
|
||||
assert "TOKEN=" not in unit
|
||||
assert "PASSWORD=" not in unit
|
||||
|
||||
|
||||
def test_release_intent_derives_version_from_governed_source_admission(tmp_path):
|
||||
service, _ = trusted_service(tmp_path)
|
||||
service.source_admission_resolver = lambda source_commit: {
|
||||
"schema": "aios.release-source-admission.v1",
|
||||
"admission_id": "source-admission-test-001",
|
||||
"source_commit": source_commit,
|
||||
"version": "V2R2C4",
|
||||
}
|
||||
|
||||
result = service.create_release_intent(
|
||||
{"event_id": "webhook-admitted-source-001", "source_commit": COMMIT}
|
||||
)
|
||||
|
||||
assert result["version"] == "V2R2C4"
|
||||
assert result["source_admission_id"] == "source-admission-test-001"
|
||||
intent_path = service.intents_root / f"{result['intent_id']}.json"
|
||||
persisted = json.loads(intent_path.read_text(encoding="utf-8"))
|
||||
assert persisted["version"] == "V2R2C4"
|
||||
assert persisted["source_admission_id"] == "source-admission-test-001"
|
||||
|
||||
|
||||
def test_release_intent_rejects_version_rebinding_against_source_admission(tmp_path):
|
||||
service, _ = trusted_service(tmp_path)
|
||||
service.source_admission_resolver = lambda source_commit: {
|
||||
"schema": "aios.release-source-admission.v1",
|
||||
"admission_id": "source-admission-test-002",
|
||||
"source_commit": source_commit,
|
||||
"version": "V2R2C4",
|
||||
}
|
||||
|
||||
with pytest.raises(
|
||||
CapabilityAuthorizationError,
|
||||
match="release source admission version mismatch",
|
||||
):
|
||||
service.create_release_intent(
|
||||
{
|
||||
"event_id": "webhook-admitted-source-002",
|
||||
"source_commit": COMMIT,
|
||||
"version": "V2R2C5",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_release_intent_surfaces_source_admission_required_as_typed_blocker(tmp_path):
|
||||
service, _ = trusted_service(tmp_path)
|
||||
|
||||
class AdmissionRequiredBuilder:
|
||||
def build(self, _payload):
|
||||
raise CapabilityAuthorizationError(
|
||||
"release source admission required"
|
||||
)
|
||||
|
||||
service.candidate_builder = AdmissionRequiredBuilder()
|
||||
result = service.create_release_intent(
|
||||
{
|
||||
"event_id": "webhook-source-admission-required-001",
|
||||
"source_commit": COMMIT,
|
||||
}
|
||||
)
|
||||
|
||||
assert result["status"] == "SOURCE_ADMISSION_REQUIRED"
|
||||
assert result["failure_code"] == "SOURCE_ADMISSION_REQUIRED"
|
||||
assert result["retryable"] is False
|
||||
assert result["side_effects_executed"] is False
|
||||
|
||||
@@ -590,3 +590,81 @@ def test_systemd_socket_and_service_are_af_unix_hardened_and_credential_bound()
|
||||
assert policy["allowed_peer_cgroups"] == [
|
||||
"/system.slice/aios-gitops-model-api-v2r2c1-staging.service"
|
||||
]
|
||||
|
||||
|
||||
def test_governed_source_admission_resolves_unknown_version_without_overriding_fixed_recipe() -> None:
|
||||
policy = load_builder_policy(POLICY_PATH)
|
||||
calls: list[tuple[str, str | None, str]] = []
|
||||
|
||||
def resolver(version: str, candidate_id: str | None, source_commit: str):
|
||||
calls.append((version, candidate_id, source_commit))
|
||||
return {
|
||||
"mode": "governed-source-admission",
|
||||
"task_id": "AIOS-BASELINE-1.1-PRODUCTION-CLOSURE",
|
||||
"candidate_id": "rc-v2r2c4-" + source_commit[:12],
|
||||
"base_oid": "1" * 40,
|
||||
"source_commit": source_commit,
|
||||
"version": version,
|
||||
"purpose": "AIOS_TRUSTED_RELEASE_BUILDER",
|
||||
}
|
||||
|
||||
config = dataclasses.replace(
|
||||
TrustedCandidateBuilderConfig.from_policy(policy),
|
||||
source_admission_resolver=resolver,
|
||||
)
|
||||
client = UnixTrustedCandidateBuilderClient(
|
||||
config,
|
||||
BuilderKeyring(builder_hmac=b"h" * 32),
|
||||
clock=lambda: NOW,
|
||||
)
|
||||
source = "2" * 40
|
||||
|
||||
request = client.create_request(
|
||||
"V2R2C4",
|
||||
None,
|
||||
source,
|
||||
nonce="z" * 32,
|
||||
)
|
||||
|
||||
assert request["candidate_id"] == "rc-v2r2c4-" + source[:12]
|
||||
assert request["base_oid"] == "1" * 40
|
||||
assert request["source_commit"] == source
|
||||
assert request["task_id"] == "AIOS-BASELINE-1.1-PRODUCTION-CLOSURE"
|
||||
assert calls == [("V2R2C4", None, source)]
|
||||
|
||||
with pytest.raises(BuilderError, match="REQUEST_RELEASE_BINDING_INVALID"):
|
||||
config.resolve_recipe(
|
||||
"V2R2C2",
|
||||
None,
|
||||
source,
|
||||
)
|
||||
# Fixed policy versions never fall through to the admission resolver.
|
||||
assert calls == [("V2R2C4", None, source)]
|
||||
|
||||
|
||||
def test_unknown_release_source_requires_explicit_admission() -> None:
|
||||
config = TrustedCandidateBuilderConfig.from_policy(load_builder_policy(POLICY_PATH))
|
||||
with pytest.raises(BuilderError, match="RECIPE_UNKNOWN"):
|
||||
config.resolve_recipe("V2R2C4", None, "2" * 40)
|
||||
|
||||
|
||||
def test_governed_source_admission_rejects_caller_candidate_rebinding() -> None:
|
||||
policy = load_builder_policy(POLICY_PATH)
|
||||
|
||||
def resolver(version: str, candidate_id: str | None, source_commit: str):
|
||||
return {
|
||||
"mode": "governed-source-admission",
|
||||
"task_id": "task-admitted-source",
|
||||
"candidate_id": "rc-v2r2c4-" + source_commit[:12],
|
||||
"base_oid": "1" * 40,
|
||||
"source_commit": source_commit,
|
||||
"version": version,
|
||||
"purpose": "AIOS_TRUSTED_RELEASE_BUILDER",
|
||||
}
|
||||
|
||||
config = dataclasses.replace(
|
||||
TrustedCandidateBuilderConfig.from_policy(policy),
|
||||
source_admission_resolver=resolver,
|
||||
)
|
||||
with pytest.raises(BuilderError, match="REQUEST_RELEASE_BINDING_INVALID"):
|
||||
config.resolve_recipe("V2R2C4", "rc-attacker", "2" * 40)
|
||||
|
||||
Reference in New Issue
Block a user