Publish exact C5 issuer V3 bootstrap source
autonomy-smoke-tests Exact governed tree; evidence 7dbba7ff168e72af
autonomy-unit-tests Exact governed tree; evidence 7dbba7ff168e72af
campaign-approval-gate Exact governed tree; evidence 7dbba7ff168e72af
git-diff-check Exact governed tree; evidence 7dbba7ff168e72af
py-compile Exact governed tree; evidence 7dbba7ff168e72af
reconciliation-contract Exact governed tree; evidence 7dbba7ff168e72af
release-state-machine Exact governed tree; evidence 7dbba7ff168e72af
secret-scan Exact governed tree; evidence 7029b3eafb209066
signer-validator-contract Exact governed tree; evidence 7dbba7ff168e72af
autonomy-smoke-tests Exact governed tree; evidence 7dbba7ff168e72af
autonomy-unit-tests Exact governed tree; evidence 7dbba7ff168e72af
campaign-approval-gate Exact governed tree; evidence 7dbba7ff168e72af
git-diff-check Exact governed tree; evidence 7dbba7ff168e72af
py-compile Exact governed tree; evidence 7dbba7ff168e72af
reconciliation-contract Exact governed tree; evidence 7dbba7ff168e72af
release-state-machine Exact governed tree; evidence 7dbba7ff168e72af
secret-scan Exact governed tree; evidence 7029b3eafb209066
signer-validator-contract Exact governed tree; evidence 7dbba7ff168e72af
This commit was merged in pull request #89.
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
"""Read-only V3 issuer scope over the admitted producer's durable receipt.
|
||||
|
||||
The V1/V2 provider remains authoritative for those schemas. V3 validation
|
||||
and Job readback are delegated to the existing admission producer contract;
|
||||
this module never reserves a Job or changes the canonical plan digest.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import stat
|
||||
from typing import Any, Callable, Mapping
|
||||
|
||||
from aios_governed_task_bootstrap import GovernedTaskBootstrap
|
||||
from aios_task_admission_bootstrap import TaskAdmissionError
|
||||
from aios_task_admission_live_adapters import (
|
||||
ProducerReceiptError,
|
||||
_FLProducerReceiptReserver,
|
||||
load_v3_plan,
|
||||
validate_v3_plan,
|
||||
)
|
||||
from governed_runtime_scope import (
|
||||
GovernedScopeError,
|
||||
RuntimeScope,
|
||||
_branch,
|
||||
_generation,
|
||||
_hex64,
|
||||
_job_id,
|
||||
_normalize_caps,
|
||||
_normalize_files,
|
||||
_oid40,
|
||||
_task_id,
|
||||
_trust_digest,
|
||||
)
|
||||
|
||||
|
||||
def _deny_mutation(*_args: Any, **_kwargs: Any) -> None:
|
||||
raise GovernedScopeError("ISSUER_PRODUCER_MUTATION_FORBIDDEN")
|
||||
|
||||
|
||||
def _read_only_bootstrap(root: Path) -> GovernedTaskBootstrap:
|
||||
"""Construct only the read-only binding view; __init__ creates directories."""
|
||||
root = Path(root)
|
||||
for path in (root, root / "task-bindings", root / "jobs"):
|
||||
metadata = path.lstat()
|
||||
if (stat.S_ISLNK(metadata.st_mode) or not stat.S_ISDIR(metadata.st_mode)
|
||||
or stat.S_IMODE(metadata.st_mode) & 0o022):
|
||||
raise GovernedScopeError("PRODUCER_STATE_ROOT_INVALID")
|
||||
view = object.__new__(GovernedTaskBootstrap)
|
||||
view.root = root
|
||||
view.task_meta_root = root / "task-bindings"
|
||||
view.job_root = root / "jobs"
|
||||
view.create = _deny_mutation
|
||||
view._store = _deny_mutation
|
||||
view._write = _deny_mutation
|
||||
return view
|
||||
|
||||
|
||||
class IssuerV3ReceiptScopeProvider:
|
||||
"""Delegate legacy plans, and resolve V3 with an existing Job only."""
|
||||
|
||||
def __init__(
|
||||
self, *, legacy: Any, producer_state_root: Path, plan_root: Path,
|
||||
grant_reader: Callable[..., Mapping[str, Any]],
|
||||
plan_loader: Callable[[Path, Mapping[str, Any]], Mapping[str, Any]] = load_v3_plan,
|
||||
) -> None:
|
||||
self.legacy = legacy
|
||||
self.producer_state_root = Path(producer_state_root)
|
||||
self.plan_root = Path(plan_root)
|
||||
self.grant_reader = grant_reader
|
||||
self.plan_loader = plan_loader
|
||||
|
||||
def inspect_v3_binding(
|
||||
self, *, grant_raw: Mapping[str, Any], plan: Mapping[str, Any],
|
||||
) -> tuple[str, Mapping[str, Any]]:
|
||||
"""Validate original bytes and durable receipt without Task mutation.
|
||||
|
||||
This method can inspect an expired historical fixture. ``resolve``
|
||||
separately enforces live Task and grant state before issuing scope.
|
||||
"""
|
||||
try:
|
||||
original = self.plan_loader(self.plan_root, grant_raw)
|
||||
if dict(original) != dict(plan):
|
||||
raise GovernedScopeError("EXECUTION_PLAN_READBACK_CONFLICT")
|
||||
# Keep the main V3 validator in the issuer contract even when a
|
||||
# test supplies an isolated loader. It binds the original plan,
|
||||
# including risk, hashes, capabilities and execution digest.
|
||||
from aios_task_admission_live_adapters import canonical_json_bytes
|
||||
validate_v3_plan(grant=grant_raw, raw=canonical_json_bytes(original))
|
||||
bootstrap = _read_only_bootstrap(self.producer_state_root)
|
||||
runtime = type("ReadOnlyIssuerRuntime", (), {
|
||||
"bootstrap": bootstrap, "bindings": {},
|
||||
})()
|
||||
reserver = _FLProducerReceiptReserver(runtime)
|
||||
job_id = reserver.existing_v3_job(grant_raw, original)
|
||||
if job_id is None:
|
||||
raise GovernedScopeError("PRODUCER_RECEIPT_MISSING")
|
||||
readback = reserver.readbacks.get(str(grant_raw["task_id"]))
|
||||
proof = readback.get("reservation_readback") if isinstance(readback, Mapping) else None
|
||||
if not isinstance(proof, Mapping) or proof.get("job_id") != job_id:
|
||||
raise GovernedScopeError("PRODUCER_RECEIPT_BINDING_INVALID")
|
||||
return job_id, dict(proof)
|
||||
except (TaskAdmissionError, ProducerReceiptError, OSError, KeyError, TypeError, ValueError) as exc:
|
||||
raise GovernedScopeError(str(exc) or "PRODUCER_RECEIPT_INVALID") from exc
|
||||
|
||||
def resolve(
|
||||
self, *, task: Any, execution: Any, grant: Any,
|
||||
plan: Mapping[str, Any], trust: Any, now: int,
|
||||
) -> RuntimeScope:
|
||||
if plan.get("schema") != "aios.task-admission-execution-plan.v3":
|
||||
return self.legacy.resolve(
|
||||
task=task, execution=execution, grant=grant,
|
||||
plan=plan, trust=trust, now=now,
|
||||
)
|
||||
|
||||
task_id = _task_id(task.task_id)
|
||||
exec_job = _job_id(execution.external_job_id)
|
||||
exec_digest = _hex64(execution.execution_digest, "execution_digest")
|
||||
exec_branch = _branch(execution.target_branch)
|
||||
exec_generation = _generation(execution.generation)
|
||||
grant_raw = self.grant_reader(
|
||||
task_id=task_id, execution_digest=exec_digest,
|
||||
repository=execution.repository, target_branch=exec_branch,
|
||||
generation=exec_generation, now=int(now),
|
||||
)
|
||||
producer_job, _proof = self.inspect_v3_binding(
|
||||
grant_raw=grant_raw, plan=plan,
|
||||
)
|
||||
if exec_job != producer_job:
|
||||
raise GovernedScopeError("EXECUTION_PRODUCER_JOB_MISMATCH")
|
||||
if str(task.status).upper() not in {"CLAIMED", "RUNNING"}:
|
||||
raise GovernedScopeError("TASK_NOT_CLAIMED")
|
||||
if grant.status != "ADMITTED" or int(grant.expires_at) <= int(now):
|
||||
raise GovernedScopeError("GRANT_EXPIRED_OR_NOT_ADMITTED")
|
||||
expected = {
|
||||
"task_id": task_id,
|
||||
"repository": execution.repository,
|
||||
"target_branch": exec_branch,
|
||||
"generation": exec_generation,
|
||||
"execution_digest": exec_digest,
|
||||
"authority_base_oid": _oid40(grant.authority_base_oid, "authority_base_oid"),
|
||||
"grant_id": grant.grant_id,
|
||||
}
|
||||
if (execution.repository != "aios/aios"
|
||||
or execution.task_id != task_id
|
||||
or any(grant_raw.get(key) != value for key, value in expected.items())):
|
||||
raise GovernedScopeError("V3_GRANT_EXECUTION_BINDING_MISMATCH")
|
||||
caps = _normalize_caps(grant.requested_capabilities)
|
||||
files = _normalize_files(grant.file_allowlist)
|
||||
if (caps != _normalize_caps(grant_raw["capabilities"])
|
||||
or files != _normalize_files(grant_raw["file_allowlist"])):
|
||||
raise GovernedScopeError("V3_GRANT_SCOPE_MISMATCH")
|
||||
if (not trust.issuer_id or not trust.key_id or not trust.public_key
|
||||
or "TASK_PRINCIPAL" not in str(trust.purpose).upper().replace("-", "_")
|
||||
or not str(trust.bridge_id).strip()):
|
||||
raise GovernedScopeError("TRUST_IDENTITY_INVALID")
|
||||
trust_digest = _trust_digest(trust)
|
||||
if (trust.scope_manifest_sha256 is not None
|
||||
and _hex64(trust.scope_manifest_sha256, "scope_manifest_sha256") != trust_digest):
|
||||
raise GovernedScopeError("TRUST_MANIFEST_DIGEST_MISMATCH")
|
||||
return RuntimeScope(
|
||||
scope_id=f"governed-runtime:{task_id}:g{exec_generation}:{exec_digest[:16]}",
|
||||
task_id=task_id, external_job_id=producer_job,
|
||||
repository=execution.repository, target_branch=exec_branch,
|
||||
generation=exec_generation, execution_digest=exec_digest,
|
||||
capabilities=caps, trust_manifest_sha256=trust_digest,
|
||||
grant_id=grant.grant_id, grant_expires_at=int(grant.expires_at),
|
||||
)
|
||||
@@ -24,6 +24,19 @@ class BuilderError(RuntimeError):
|
||||
"""A stable, non-sensitive error code returned by the builder boundary."""
|
||||
|
||||
|
||||
C5_INTEGRATION_CONTRACT_FIELDS = frozenset({
|
||||
"integration_source_commit", "integration_tree_oid",
|
||||
"publication_source_commit", "publication_tree_oid",
|
||||
"publication_plan_sha256", "repair_source_commit", "repair_tree_oid",
|
||||
})
|
||||
C5_INTEGRATION_FILES = {
|
||||
"active-runtime/app/aios_capability_contract.py": ("publication_source_commit", "67d8a3a296110e20a4ea59863cd7982b6bdada71abd457c0f7cec480b06d278a"),
|
||||
"tests/test_v2r2c5_capability_contract.py": ("publication_source_commit", "f91af641528ea9a625ca45a46b6c26f5dba568e6b69cba819ca33ab5cba28de3"),
|
||||
"active-runtime/app/aios_baseline/service.py": ("repair_source_commit", "3c3918aec5503947b48ef4c73e34cac8f0d33fdfb99cfcd5ca8af699b663da72"),
|
||||
"tests/baseline_v1_1/test_loop.py": ("repair_source_commit", "f867e92b9ec9b4dff248e6ca38280848458a535c610dc6d9fea2b270c29b664a"),
|
||||
}
|
||||
|
||||
|
||||
def _canonical_json(value: Mapping[str, Any]) -> bytes:
|
||||
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("utf-8")
|
||||
|
||||
@@ -135,6 +148,7 @@ class TrustedCandidateBuilderConfig:
|
||||
policy_sha256: str
|
||||
source_admission_resolver: Callable[[str, str | None, str], Mapping[str, str]] | None = None
|
||||
migration_plan_resolver: Callable[[str, str, str], Any] | None = None
|
||||
c5_contract: Mapping[str, str] | None = None
|
||||
|
||||
@classmethod
|
||||
def from_policy(cls, policy: Mapping[str, Any]) -> "TrustedCandidateBuilderConfig":
|
||||
@@ -145,16 +159,52 @@ class TrustedCandidateBuilderConfig:
|
||||
"allowed_peer_runtime_prefix", "allowed_peer_runtime_suffix",
|
||||
"required_checks", "evidence_layout", "recipes", "governed_recipes",
|
||||
}
|
||||
if set(policy) != required or policy.get("schema") != "aios.trusted-candidate-builder-policy.v1":
|
||||
is_integration = policy.get("schema") == "aios.trusted-candidate-builder-policy.c5-integration.v1"
|
||||
is_c5 = is_integration or policy.get("schema") == "aios.trusted-candidate-builder-policy.c5.v1"
|
||||
if set(policy) != (required | {"c5_contract"} if is_c5 else required) or policy.get("schema") not in {"aios.trusted-candidate-builder-policy.v1", "aios.trusted-candidate-builder-policy.c5.v1", "aios.trusted-candidate-builder-policy.c5-integration.v1"}:
|
||||
raise BuilderError("POLICY_SCHEMA_INVALID")
|
||||
recipes = policy.get("recipes")
|
||||
if not isinstance(recipes, Mapping) or set(recipes) != {"V2R2C2", "V2R2C3"}:
|
||||
expected_recipes = {"V2R2C2", "V2R2C3", "V2R2C5"} if is_c5 else {"V2R2C2", "V2R2C3"}
|
||||
if not isinstance(recipes, Mapping) or set(recipes) != expected_recipes:
|
||||
raise BuilderError("POLICY_RECIPES_INVALID")
|
||||
c2, c3 = recipes["V2R2C2"], recipes["V2R2C3"]
|
||||
if not isinstance(c2, Mapping) or set(c2) != {"mode", "task_id", "candidate_id", "base_oid", "source_commit", "version", "purpose"} or c2.get("mode") != "fixed":
|
||||
raise BuilderError("POLICY_V2R2C2_INVALID")
|
||||
if not isinstance(c3, Mapping) or set(c3) != {"mode", "derives_from", "version", "purpose"} or c3.get("mode") != "governed-derived-only" or c3.get("derives_from") != "V2R2C2":
|
||||
raise BuilderError("POLICY_V2R2C3_INVALID")
|
||||
c5_contract = None
|
||||
if is_c5:
|
||||
c5 = recipes["V2R2C5"]
|
||||
expected_mode = "governed-source-integration-only" if is_integration else "governed-source-admission-only"
|
||||
if not isinstance(c5, Mapping) or set(c5) != {"mode", "version", "purpose"} or c5.get("mode") != expected_mode or c5.get("version") != "V2R2C5" or c5.get("purpose") != "AIOS_TRUSTED_RELEASE_BUILDER":
|
||||
raise BuilderError("POLICY_V2R2C5_INVALID")
|
||||
required_c5 = {"task_id", "authority_base_oid", "accepted_base_oid", "rollback_version", "rollback_target_oid", "rollback_artifact_sha256"}
|
||||
if is_integration:
|
||||
required_c5 |= C5_INTEGRATION_CONTRACT_FIELDS
|
||||
value = policy.get("c5_contract")
|
||||
if not isinstance(value, Mapping) or set(value) != required_c5:
|
||||
raise BuilderError("POLICY_V2R2C5_CONTRACT_INVALID")
|
||||
if any(not isinstance(v, str) for v in value.values()):
|
||||
raise BuilderError("POLICY_V2R2C5_CONTRACT_INVALID")
|
||||
c5_contract = dict(value)
|
||||
oid_fields = {"authority_base_oid", "accepted_base_oid", "rollback_target_oid"}
|
||||
if is_integration:
|
||||
oid_fields |= {
|
||||
"integration_source_commit", "integration_tree_oid",
|
||||
"publication_source_commit", "publication_tree_oid",
|
||||
"repair_source_commit", "repair_tree_oid",
|
||||
}
|
||||
sha_fields = {"rollback_artifact_sha256"}
|
||||
if is_integration:
|
||||
sha_fields.add("publication_plan_sha256")
|
||||
if (
|
||||
not c5_contract["task_id"]
|
||||
or c5_contract["rollback_version"] != "V2R2C4"
|
||||
or c5_contract["accepted_base_oid"] != c5_contract["rollback_target_oid"]
|
||||
or any(len(c5_contract[name]) != 40 or any(ch not in "0123456789abcdef" for ch in c5_contract[name]) for name in oid_fields)
|
||||
or any(len(c5_contract[name]) != 64 or any(ch not in "0123456789abcdef" for ch in c5_contract[name]) for name in sha_fields)
|
||||
):
|
||||
raise BuilderError("POLICY_V2R2C5_CONTRACT_INVALID")
|
||||
if (
|
||||
policy["repository"] != "aios/aios"
|
||||
or policy["operation"] != "BuildTrustedCandidateV1"
|
||||
@@ -210,6 +260,7 @@ class TrustedCandidateBuilderConfig:
|
||||
recipes={name: dict(value) for name, value in recipes.items()},
|
||||
governed_recipes={name: dict(value) for name, value in policy["governed_recipes"].items()} if isinstance(policy["governed_recipes"], Mapping) else _invalid_governed_recipes(),
|
||||
policy_sha256=hashlib.sha256(_canonical_json(dict(policy))).hexdigest(),
|
||||
c5_contract=c5_contract,
|
||||
)
|
||||
|
||||
def fixed_recipe(self, name: str) -> Mapping[str, str]:
|
||||
@@ -231,6 +282,36 @@ class TrustedCandidateBuilderConfig:
|
||||
def resolve_recipe(self, version: str, candidate_id: str | None, source_commit: str) -> Mapping[str, str]:
|
||||
recipe = self.recipes.get(version)
|
||||
if recipe is not None:
|
||||
if recipe.get("mode") in {"governed-source-admission-only", "governed-source-integration-only"}:
|
||||
if version != "V2R2C5" or self.c5_contract is None:
|
||||
raise BuilderError("SOURCE_ADMISSION_REQUIRED")
|
||||
resolver = self.source_admission_resolver
|
||||
if resolver is None:
|
||||
raise BuilderError("SOURCE_ADMISSION_REQUIRED")
|
||||
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", "tree_oid"}
|
||||
if set(admitted) != required or admitted.get("mode") != "governed-source-admission" or admitted.get("version") != version or admitted.get("source_commit") != source_commit or admitted.get("purpose") != "AIOS_TRUSTED_RELEASE_BUILDER":
|
||||
raise BuilderError("SOURCE_ADMISSION_INVALID")
|
||||
if (not isinstance(admitted.get("candidate_id"), str) or not admitted.get("candidate_id")
|
||||
or admitted.get("task_id") != self.c5_contract["task_id"]
|
||||
or admitted.get("base_oid") != self.c5_contract["authority_base_oid"]):
|
||||
raise BuilderError("SOURCE_ADMISSION_BINDING_INVALID")
|
||||
effective = str(candidate_id or admitted["candidate_id"])
|
||||
if effective != admitted["candidate_id"]:
|
||||
raise BuilderError("REQUEST_RELEASE_BINDING_INVALID")
|
||||
for name in ("base_oid", "source_commit", "tree_oid"):
|
||||
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 recipe.get("mode") == "governed-source-integration-only" and (
|
||||
source_commit != self.c5_contract["integration_source_commit"]
|
||||
or admitted["tree_oid"] != self.c5_contract["integration_tree_oid"]
|
||||
):
|
||||
raise BuilderError("SOURCE_INTEGRATION_BINDING_INVALID")
|
||||
return {**admitted, **self.c5_contract}
|
||||
resolved = self.fixed_recipe(version)
|
||||
# Fixed/governed-derived policy recipes remain immutable. A
|
||||
# source admission can never override one of these identities.
|
||||
@@ -247,6 +328,8 @@ class TrustedCandidateBuilderConfig:
|
||||
raise BuilderError("REQUEST_RELEASE_BINDING_INVALID")
|
||||
return resolved
|
||||
|
||||
if self.c5_contract is not None:
|
||||
raise BuilderError("RECIPE_UNKNOWN")
|
||||
resolver = self.source_admission_resolver
|
||||
if resolver is None:
|
||||
raise BuilderError("RECIPE_UNKNOWN")
|
||||
@@ -263,8 +346,9 @@ class TrustedCandidateBuilderConfig:
|
||||
"version",
|
||||
"purpose",
|
||||
}
|
||||
allowed_fields = (required, required | {"tree_oid"}) if version == "V2R2C5" else (required,)
|
||||
if (
|
||||
set(admitted) != required
|
||||
set(admitted) not in allowed_fields
|
||||
or admitted.get("mode") != "governed-source-admission"
|
||||
or admitted.get("version") != version
|
||||
or admitted.get("source_commit") != source_commit
|
||||
@@ -275,7 +359,7 @@ class TrustedCandidateBuilderConfig:
|
||||
effective_candidate_id = str(candidate_id or admitted["candidate_id"])
|
||||
if effective_candidate_id != admitted["candidate_id"]:
|
||||
raise BuilderError("REQUEST_RELEASE_BINDING_INVALID")
|
||||
for name in ("base_oid", "source_commit"):
|
||||
for name in ("base_oid", "source_commit", *(["tree_oid"] if "tree_oid" in admitted else [])):
|
||||
value = admitted.get(name)
|
||||
if (
|
||||
not isinstance(value, str)
|
||||
@@ -395,7 +479,44 @@ class ServerOwnedEvidenceSource:
|
||||
raise BuilderError("COLLECTOR_EVIDENCE_INVALID")
|
||||
return value
|
||||
|
||||
def _validate_integration_source(self, recipe: Mapping[str, str]) -> None:
|
||||
if "integration_source_commit" not in recipe:
|
||||
return
|
||||
|
||||
def read(*arguments: str, binary: bool = False):
|
||||
try:
|
||||
result = self._git_runner(
|
||||
["git", f"--git-dir={self._authority_git_dir}", *arguments],
|
||||
check=True, capture_output=True, text=not binary, timeout=15,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
return result.stdout if binary else result.stdout.strip()
|
||||
except (OSError, subprocess.SubprocessError) as exc:
|
||||
raise BuilderError("COLLECTOR_INTEGRATION_UNAVAILABLE") from exc
|
||||
|
||||
source = recipe["source_commit"]
|
||||
if source != recipe["integration_source_commit"]:
|
||||
raise BuilderError("COLLECTOR_INTEGRATION_MISMATCH")
|
||||
for commit_field, tree_field in (
|
||||
("integration_source_commit", "integration_tree_oid"),
|
||||
("publication_source_commit", "publication_tree_oid"),
|
||||
("repair_source_commit", "repair_tree_oid"),
|
||||
):
|
||||
if read("rev-parse", f"{recipe[commit_field]}^{{tree}}") != recipe[tree_field]:
|
||||
raise BuilderError("COLLECTOR_INTEGRATION_MISMATCH")
|
||||
if read("show", "-s", "--format=%P", source) != recipe["authority_base_oid"]:
|
||||
raise BuilderError("COLLECTOR_INTEGRATION_MISMATCH")
|
||||
changed = read("diff", "--name-only", recipe["authority_base_oid"], source).splitlines()
|
||||
if sorted(changed) != sorted(C5_INTEGRATION_FILES):
|
||||
raise BuilderError("COLLECTOR_INTEGRATION_FILE_SET_MISMATCH")
|
||||
for path, (component, digest) in C5_INTEGRATION_FILES.items():
|
||||
integrated = read("show", f"{source}:{path}", binary=True)
|
||||
original = read("show", f"{recipe[component]}:{path}", binary=True)
|
||||
if integrated != original or hashlib.sha256(integrated).hexdigest() != digest:
|
||||
raise BuilderError("COLLECTOR_INTEGRATION_BYTES_MISMATCH")
|
||||
|
||||
def __call__(self, recipe_name: str, recipe: Mapping[str, str]) -> Mapping[str, Any]:
|
||||
self._validate_integration_source(recipe)
|
||||
try:
|
||||
tree_result = self._git_runner(
|
||||
["git", f"--git-dir={self._authority_git_dir}", "rev-parse", f"{recipe['source_commit']}^{{tree}}"],
|
||||
@@ -412,6 +533,16 @@ class ServerOwnedEvidenceSource:
|
||||
raise BuilderError("COLLECTOR_EVIDENCE_UNAVAILABLE") from exc
|
||||
if getattr(ancestry_result, "returncode", 1) != 0:
|
||||
raise BuilderError("COLLECTOR_GIT_MISMATCH")
|
||||
if recipe.get("tree_oid") is not None and recipe.get("tree_oid") != tree_oid:
|
||||
raise BuilderError("COLLECTOR_GIT_MISMATCH")
|
||||
if recipe.get("accepted_base_oid") is not None:
|
||||
accepted_ancestry = self._git_runner(
|
||||
["git", f"--git-dir={self._authority_git_dir}", "merge-base", "--is-ancestor", recipe["accepted_base_oid"], recipe["source_commit"]],
|
||||
check=False, capture_output=True, text=True, timeout=15,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
if getattr(accepted_ancestry, "returncode", 1) != 0:
|
||||
raise BuilderError("COLLECTOR_GIT_MISMATCH")
|
||||
|
||||
receipt = self._json(self._regular_path(f"{recipe_name}/receipt.json"))
|
||||
if set(receipt) != {"schema", "version", "candidate", "release_plan"}:
|
||||
@@ -488,6 +619,13 @@ class ServerOwnedEvidenceSource:
|
||||
or rollback.get("rollback_sha256") != deployment.get("rollback_sha256")
|
||||
):
|
||||
raise BuilderError("COLLECTOR_ROLLBACK_MISMATCH")
|
||||
if recipe.get("rollback_version") is not None:
|
||||
if deployment.get("rollback_target") != {
|
||||
"version": recipe["rollback_version"],
|
||||
"target_oid": recipe["rollback_target_oid"],
|
||||
"artifact_sha256": recipe["rollback_artifact_sha256"],
|
||||
}:
|
||||
raise BuilderError("COLLECTOR_ROLLBACK_MISMATCH")
|
||||
if (
|
||||
digests["gitea"] != hashlib.sha256(_canonical_json(dict(gitea))).hexdigest()
|
||||
or digests["rollback"] != hashlib.sha256(_canonical_json(dict(rollback))).hexdigest()
|
||||
|
||||
@@ -18,7 +18,10 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import dataclasses
|
||||
import fcntl
|
||||
try:
|
||||
import fcntl
|
||||
except ModuleNotFoundError: # pragma: no cover - production host is POSIX
|
||||
fcntl = None
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
@@ -376,6 +379,8 @@ class FileLock:
|
||||
self.handle = None
|
||||
|
||||
def __enter__(self) -> "FileLock":
|
||||
if fcntl is None:
|
||||
raise ReleaseBlocked("production executor file locking requires POSIX")
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.handle = self.path.open("a+", encoding="utf-8")
|
||||
os.chmod(self.path, 0o600)
|
||||
@@ -388,7 +393,7 @@ class FileLock:
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb) -> None:
|
||||
if self.handle is not None:
|
||||
if self.handle is not None and fcntl is not None:
|
||||
fcntl.flock(self.handle.fileno(), fcntl.LOCK_UN)
|
||||
self.handle.close()
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Fixed registered composition of the existing shadow API and renewal store."""
|
||||
import base64
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import runpy
|
||||
import sys
|
||||
|
||||
ORIGINAL_SHA = 'f6393fe614239036a1d8235c98c1f455dc32b700107d88f48190c4f61dae617e'
|
||||
|
||||
|
||||
def service_type(types, renewal, public_key):
|
||||
class RegisteredService(types.SilentReleaseActionService):
|
||||
def __init__(self, root, **kwargs):
|
||||
if 'completion_approvals' in kwargs:
|
||||
raise renewal.RenewalRejected('FIXED_COMPLETION_DEPENDENCY_REQUIRED')
|
||||
original = types.SignedApprovalStore(Path(root) / 'approvals')
|
||||
completion = renewal.RegisteredCompletionApprovalStore(
|
||||
original, public_key, types.SignedReleasePlanApproval,
|
||||
)
|
||||
super().__init__(root, completion_approvals=completion, **kwargs)
|
||||
return RegisteredService
|
||||
|
||||
|
||||
def main():
|
||||
# Original Round3 startup and verifier remain loaded before the new root
|
||||
# is added. The C5 types are a separate registered module as in R2.
|
||||
import silent_release_hardening as original
|
||||
import hashlib
|
||||
if hashlib.sha256(Path(original.__file__).read_bytes()).hexdigest() != ORIGINAL_SHA:
|
||||
raise RuntimeError('ROUND3_HARDENING_DRIFT')
|
||||
if not getattr(original.BuilderAttestationVerifier.verify,
|
||||
'__aios_round3_rollback_contract_patch__', False):
|
||||
raise RuntimeError('ROUND3_VERIFIER_NOT_ACTIVE')
|
||||
import c5_completion_renewal as renewal
|
||||
renewal.configure_registered_bindings()
|
||||
root = renewal.AUTHORITY_ROOT
|
||||
sys.path.insert(0, str(root))
|
||||
if Path(__file__) != root / 'c5_completion_api.py':
|
||||
raise renewal.RenewalRejected('REGISTERED_API_WRAPPER_REQUIRED')
|
||||
policy = renewal.RegisteredCompletionAuthority()()
|
||||
parent = json.loads(renewal.secure_bytes(Path(policy['predecessor_snapshot'])))
|
||||
keyring = json.loads(renewal.secure_bytes(root / 'approval-public-keyring.json'))
|
||||
if keyring.get('algorithm') != 'Ed25519' or parent['key_id'] in keyring.get('revoked', []):
|
||||
raise renewal.RenewalRejected('REGISTERED_APPROVAL_KEY_REQUIRED')
|
||||
public_key = base64.b64decode(keyring['keys'][parent['key_id']], validate=True)
|
||||
renewal.verify_signature(parent, public_key)
|
||||
spec = importlib.util.spec_from_file_location('_registered_c5_completion_types',
|
||||
root / 'silent_release_hardening.py')
|
||||
types = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = types
|
||||
spec.loader.exec_module(types)
|
||||
loaded = runpy.run_path(str(root / 'original_api.py'), run_name='_registered_original_api')
|
||||
g = loaded['main'].__globals__
|
||||
if g['SILENT_RELEASE_EXECUTION_ENABLED'] is not False or Path(g['SILENT_RELEASE_STATE_ROOT']) != renewal.STATE_ROOT:
|
||||
raise renewal.RenewalRejected('EXISTING_SHADOW_PLANE_REQUIRED')
|
||||
g['ReleasePlan'] = types.ReleasePlan
|
||||
g['TrustedCandidateBundle'] = types.TrustedCandidateBundle
|
||||
g['SilentReleaseActionService'] = service_type(types, renewal, public_key)
|
||||
g['TRUSTED_RELEASE_POLICY'] = renewal.TRUSTED_RELEASE_POLICY_PATH
|
||||
service = g['_build_silent_release_service']()
|
||||
if service.execution_enabled or service.policy_sha256 != renewal.TRUSTED_RELEASE_POLICY_SHA256:
|
||||
raise renewal.RenewalRejected('EXACT_API_POLICY_REQUIRED')
|
||||
# Preserve the exact candidate boundary from the existing registered R2.
|
||||
def reject_candidate_creation(payload):
|
||||
raise original.CapabilityAuthorizationError('completion mode cannot create or replace candidates')
|
||||
service.create_release_candidate = reject_candidate_creation
|
||||
g['_SILENT_RELEASE_SERVICE'] = service
|
||||
g['main']()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,100 @@
|
||||
"""Completion-only fixed tag and metadata adapter launcher, no runtime effects."""
|
||||
import argparse
|
||||
import base64
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import runpy
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
UNIT = 'aios-c5-exact-production-host-r1.service'
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('adapter')
|
||||
parser.add_argument('mode', choices=('tag', 'registry', 'baseline'))
|
||||
args = parser.parse_args()
|
||||
import c5_completion_renewal as renewal
|
||||
renewal.configure_registered_bindings()
|
||||
root = renewal.AUTHORITY_ROOT
|
||||
sys.path.insert(0, str(root))
|
||||
from silent_release_hardening import SignedApprovalStore, SignedReleasePlanApproval
|
||||
if Path(__file__) != root / 'c5_completion_phase.py' or os.geteuid() != 0:
|
||||
raise renewal.RenewalRejected('REGISTERED_COMPLETION_PHASE_REQUIRED')
|
||||
policy = renewal.RegisteredCompletionAuthority()()
|
||||
credentials = Path(os.environ.get('CREDENTIALS_DIRECTORY', ''))
|
||||
if credentials != Path('/run/credentials') / UNIT:
|
||||
raise renewal.RenewalRejected('FIXED_COMPLETION_PURPOSE_UNIT_REQUIRED')
|
||||
if not any(line.split(':', 2)[-1].rstrip('/').endswith('/' + UNIT)
|
||||
for line in Path('/proc/self/cgroup').read_text().splitlines()):
|
||||
raise renewal.RenewalRejected('FIXED_COMPLETION_CGROUP_REQUIRED')
|
||||
module = 'governed_tag_adapter.py' if args.mode == 'tag' else 'release_metadata_adapter.py'
|
||||
if Path(args.adapter) != ROOT / module:
|
||||
raise renewal.RenewalRejected('FIXED_COMPLETION_ADAPTER_REQUIRED')
|
||||
value = json.load(sys.stdin)
|
||||
fixed = {'schema': 'aios.production-executor-request.v1', 'version': 'V2R2C5',
|
||||
'target_oid': renewal.SOURCE, 'evidence_closure_oid': renewal.SOURCE,
|
||||
'artifact_sha256': renewal.ARTIFACT,
|
||||
'approval_plan_id': 'sha256:d8f64b8b92f9fdb17f5c6053b76a68ee5bcbc2f266d91438c4db7893463079f8',
|
||||
'repository_identity': 'aios/aios', 'production_service': 'aios-v2r2c2-production.service',
|
||||
'production_port': 18928}
|
||||
if args.mode != 'tag':
|
||||
fixed['tag_oid'] = renewal.TAG_OID
|
||||
if (set(value) != set(fixed) | {'operation'} or value['operation'] not in {'probe', 'apply'}
|
||||
or any(value[k] != v for k, v in fixed.items())):
|
||||
raise renewal.RenewalRejected('EXACT_COMPLETION_REQUEST_REQUIRED')
|
||||
parent = json.loads(renewal.secure_bytes(Path(policy['predecessor_snapshot'])))
|
||||
ring = json.loads(renewal.secure_bytes(root / 'approval-public-keyring.json'))
|
||||
if parent['key_id'] in ring.get('revoked', []):
|
||||
raise renewal.RenewalRejected('APPROVAL_KEY_REVOKED')
|
||||
public_key = base64.b64decode(ring['keys'][parent['key_id']], validate=True)
|
||||
approval = renewal.RegisteredCompletionApprovalStore(
|
||||
SignedApprovalStore(renewal.STATE_ROOT / 'approvals'), public_key, SignedReleasePlanApproval,
|
||||
).load(renewal.CID)
|
||||
if approval.fingerprint == renewal.OLD_FP:
|
||||
raise renewal.RenewalRejected('FRESH_COMPLETION_SUCCESSOR_REQUIRED')
|
||||
# Loading the store verifies both actual signatures, registered fixed nonce,
|
||||
# append-only chain and current successor lifetime. The historical proof
|
||||
# intentionally remains bound to the original approval, never rewritten.
|
||||
runtime = json.load(urllib.request.urlopen('http://127.0.0.1:18928/health', timeout=10))
|
||||
if any(runtime.get(k) != v for k, v in {
|
||||
'version': 'V2R2C5', 'commit_oid': renewal.SOURCE, 'artifact_sha256': renewal.ARTIFACT,
|
||||
'deployment_state': 'ACTIVE', 'production_deployed': True}.items()):
|
||||
raise renewal.RenewalRejected('CURRENT_C5_RUNTIME_REQUIRED')
|
||||
# Exact signed copies of these historical helpers are read-only here.
|
||||
# Never call their activation, run(), or verification-phase effect methods.
|
||||
import production_verification_phase_r1 as business
|
||||
import public_business_acceptance_probe_r1 as public
|
||||
business.RegisteredVerificationPhase.business_proof(None, True)
|
||||
public.reconcile()
|
||||
os.environ.clear()
|
||||
os.environ.update(PATH='/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
|
||||
PYTHONDONTWRITEBYTECODE='1', AIOS_GITOPS_WRITES_ENABLED='false',
|
||||
AIOS_GITOPS_LEGACY_GLOBAL_WRITES_ENABLED='false',
|
||||
AIOS_GITOPS_CONTROLLER_WORKSPACE=str(renewal.CONTROLLER_WORKSPACE),
|
||||
AIOS_GITOPS_AUTHORITATIVE_GIT_DIR=str(renewal.AUTHORITATIVE_GIT_DIR),
|
||||
AIOS_GITOPS_REPOSITORY_IDENTITY='aios/aios',
|
||||
AIOS_RUNTIME_REGISTRY_ROOT=str(renewal.RUNTIME_REGISTRY_ROOT),
|
||||
AIOS_RUNTIME_BASELINE_PATH=str(renewal.RUNTIME_BASELINE_PATH))
|
||||
if args.mode == 'tag':
|
||||
os.environ.update(GIT_COMMITTER_NAME='owner', GIT_COMMITTER_EMAIL='owner@cnnetmkt.top',
|
||||
GIT_COMMITTER_DATE='1790826063 +0000', GIT_TERMINAL_PROMPT='0',
|
||||
GIT_ASKPASS=str(root / 'publisher-askpass.py'))
|
||||
sys.argv = [module, '--workspace', str(renewal.CONTROLLER_WORKSPACE),
|
||||
'--repository', str(renewal.AUTHORITATIVE_GIT_DIR), '--remote', 'gitea-http',
|
||||
'--gitea-api', 'http://127.0.0.1:3000/api/v1', '--gitea-repo', 'aios/aios',
|
||||
'--gitea-token-file', str(credentials / 'gitea-release-publisher'),
|
||||
'--state-root', str(renewal.HISTORICAL_ROOT / 'tag-publication')]
|
||||
else:
|
||||
sys.argv = [module, '--mode', args.mode, '--expected-previous-baseline', 'V2R2C4',
|
||||
'--expected-previous-baseline-sha256', 'de0c7008facc44c5061239ee490135b58e521989952918f84a57cecb3358483c',
|
||||
'--health-url', 'http://127.0.0.1:18928/health']
|
||||
sys.stdin = io.StringIO(json.dumps(value))
|
||||
runpy.run_path(str(root / module), run_name='__main__')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,630 @@
|
||||
"""Append-only C5 completion renewal boundary; activation requires registration.
|
||||
|
||||
The exact Owner-registered API, signer and completion host must call this
|
||||
boundary. This module does not grant authority or perform deployment effects.
|
||||
"""
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import stat
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
||||
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
|
||||
|
||||
CID = 'rc-v2r2c5-5f65e458957d'
|
||||
# Immutable release content remains compiled into the validator. Renewable
|
||||
# authority identities and filesystem locations are installed only from the
|
||||
# digest-bound, root-owned registration file (see configure_registered_bindings).
|
||||
OLD_FP = '0' * 64
|
||||
SOURCE = '5f65e458957dd51c9973cfa956a624f3cbf54990'
|
||||
TREE = 'ddd239d43d63a7fbd3cff46119c71466c9443737'
|
||||
ARTIFACT = '7f9d8409f07a5e0d36f5db7c55eb82ea70935fa731e057998f29cd1a5ca0a9ad'
|
||||
PLAN = '370521afcf9b2288f6ad3803ca3554a46ee05d2042d53b19b4f4cb80e37a9684'
|
||||
MUTABLE = {'issued_at', 'not_before', 'expires_at', 'nonce', 'fingerprint', 'signature'}
|
||||
CAPABILITIES = ['baseline_registry_finalize', 'runtime_switch', 'tag_publish']
|
||||
AUTHORITY_ROOT = Path('/unconfigured/c5-completion-authority')
|
||||
STATE_ROOT = Path('/unconfigured/c5-completion-state')
|
||||
OWNER_KEY = 'UNCONFIGURED'
|
||||
TAG_OID = '6d1745ff496103db7e721a2b2bc624fda52c5dc8'
|
||||
SAGA_HEAD = '0' * 64
|
||||
HISTORICAL_ROOT = Path('/unconfigured/c5-completion-history')
|
||||
CONTROLLER_WORKSPACE = Path('/unconfigured/c5-controller-workspace')
|
||||
AUTHORITATIVE_GIT_DIR = Path('/unconfigured/c5-authoritative-git')
|
||||
RUNTIME_REGISTRY_ROOT = Path('/unconfigured/c5-runtime-registry')
|
||||
RUNTIME_BASELINE_PATH = Path('/unconfigured/c5-runtime-baseline.json')
|
||||
TRUSTED_RELEASE_POLICY_PATH = Path('/unconfigured/c5-trusted-release-policy.json')
|
||||
TRUSTED_RELEASE_POLICY_SHA256 = '0' * 64
|
||||
BINDINGS_PATH_ENV = 'AIOS_C5_COMPLETION_BINDINGS_PATH'
|
||||
BINDINGS_SHA256_ENV = 'AIOS_C5_COMPLETION_BINDINGS_SHA256'
|
||||
_BINDINGS_DIGEST = None
|
||||
RENEWAL_SOURCE_FILES = ('c5_completion_renewal.py', 'c5_release_execution_host.py',
|
||||
'silent_release_hardening.py', 'c5_completion_signer.py',
|
||||
'c5_completion_api.py', 'c5_completion_phase.py') # gitleaks:allow -- filenames only
|
||||
PHASE_DEPENDENCIES = ('governed_tag_adapter.py', 'release_metadata_adapter.py',
|
||||
'production_verification_phase_r1.py', 'public_business_acceptance_probe_r1.py',
|
||||
'publisher-askpass.py', 'c5_staging_installation.py',
|
||||
'aios_single_host_authority.py', 'registered_owner_signer_adapter.py')
|
||||
|
||||
|
||||
class RenewalRejected(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def canonical(value):
|
||||
return json.dumps(value, sort_keys=True, separators=(',', ':'), allow_nan=False).encode()
|
||||
|
||||
|
||||
def digest(raw):
|
||||
return hashlib.sha256(raw).hexdigest()
|
||||
|
||||
|
||||
def validate_completion_lineage(gate):
|
||||
"""Bind a current completion-only journal suffix to the frozen Saga15 head.
|
||||
|
||||
The suffix is authenticated by the enclosing registered Owner contract;
|
||||
these checks preserve its origin and exclude deployment/source transitions.
|
||||
"""
|
||||
revision = gate.get('saga_revision')
|
||||
suffix = gate.get('completion_journal_suffix', [])
|
||||
observed = gate.get('observed_at')
|
||||
if (type(revision) is not int or revision < 15 or not isinstance(suffix, list)
|
||||
or type(observed) not in {int, float} or not math.isfinite(observed)):
|
||||
raise RenewalRejected('COMPLETION_LINEAGE_INVALID')
|
||||
if revision == 15:
|
||||
if gate.get('saga_head') != SAGA_HEAD or suffix:
|
||||
raise RenewalRejected('COMPLETION_LINEAGE_INVALID')
|
||||
return
|
||||
if not 0 < len(suffix) <= 64 or len(suffix) != revision - 15:
|
||||
raise RenewalRejected('COMPLETION_LINEAGE_INVALID')
|
||||
allowed = {
|
||||
'owner_registered_completion_successor_tag_reconciliation': ('TAG_PENDING', {'RECONCILIATION_REQUIRED'}),
|
||||
'owner_registered_tag_capability_successor_verified': ('TAG_VERIFIED', {'RECONCILIATION_REQUIRED', 'TAG_PENDING'}),
|
||||
'baseline_registry_finalization_pending': ('BASELINE_REGISTRY_PENDING', {'TAG_VERIFIED'}),
|
||||
'owner_registered_completion_successor_metadata_reconciliation': ('BASELINE_REGISTRY_PENDING', {'RECONCILIATION_REQUIRED'}),
|
||||
'release_reconciliation_required': ('RECONCILIATION_REQUIRED', {'TAG_PENDING', 'TAG_VERIFIED', 'BASELINE_REGISTRY_PENDING'}),
|
||||
}
|
||||
previous, state = SAGA_HEAD, 'RECONCILIATION_REQUIRED'
|
||||
for offset, event in enumerate(suffix, 16):
|
||||
if (not isinstance(event, dict)
|
||||
or set(event) != {'schema_version', 'sequence', 'previous_hash', 'payload', 'event_hash'}
|
||||
or type(event['schema_version']) is not int or event['schema_version'] != 1
|
||||
or type(event['sequence']) is not int or event['sequence'] != offset + 1
|
||||
or event['previous_hash'] != previous):
|
||||
raise RenewalRejected('COMPLETION_LINEAGE_INVALID')
|
||||
payload = event['payload']
|
||||
if (not isinstance(payload, dict)
|
||||
or set(payload) != {'candidate_id', 'release_plan_id', 'revision', 'state', 'event', 'timestamp', 'details'}
|
||||
or payload['candidate_id'] != CID
|
||||
or payload['release_plan_id'] != 'pending-' + CID
|
||||
or type(payload['revision']) is not int or payload['revision'] != offset
|
||||
or not isinstance(payload['details'], dict)
|
||||
or not isinstance(payload['event'], str) or payload['event'] not in allowed):
|
||||
raise RenewalRejected('COMPLETION_LINEAGE_INVALID')
|
||||
target, origins = allowed[payload['event']]
|
||||
if payload['state'] != target or state not in origins:
|
||||
raise RenewalRejected('COMPLETION_LINEAGE_INVALID')
|
||||
if ('release_plan_sha256' in payload['details']
|
||||
and payload['details']['release_plan_sha256'] != PLAN):
|
||||
raise RenewalRejected('COMPLETION_LINEAGE_INVALID')
|
||||
try:
|
||||
if timestamp(payload['timestamp']) > gate['observed_at']:
|
||||
raise RenewalRejected('COMPLETION_LINEAGE_INVALID')
|
||||
actual = digest(canonical({key: event[key] for key in ('sequence', 'previous_hash', 'payload')}))
|
||||
except (TypeError, ValueError, AttributeError, KeyError) as exc:
|
||||
raise RenewalRejected('COMPLETION_LINEAGE_INVALID') from exc
|
||||
if event['event_hash'] != actual:
|
||||
raise RenewalRejected('COMPLETION_LINEAGE_INVALID')
|
||||
previous, state = actual, target
|
||||
if gate.get('saga_head') != previous:
|
||||
raise RenewalRejected('COMPLETION_LINEAGE_INVALID')
|
||||
|
||||
|
||||
def timestamp(value):
|
||||
parsed = datetime.fromisoformat(value.replace('Z', '+00:00'))
|
||||
if parsed.tzinfo is None or parsed.utcoffset().total_seconds() != 0:
|
||||
raise RenewalRejected('UTC_REQUIRED')
|
||||
return parsed.timestamp()
|
||||
|
||||
|
||||
def verify_signature(approval, public_key):
|
||||
payload = {k: v for k, v in approval.items() if k not in {'signature', 'fingerprint'}}
|
||||
raw = canonical(payload)
|
||||
if approval.get('fingerprint') != digest(raw):
|
||||
raise RenewalRejected('APPROVAL_DIGEST_INVALID')
|
||||
Ed25519PublicKey.from_public_bytes(public_key).verify(
|
||||
base64.b64decode(approval['signature'], validate=True), raw
|
||||
)
|
||||
|
||||
|
||||
def validate_successor(old, new, public_key, *, now):
|
||||
"""Validate historical predecessor and fresh identical-binding successor.
|
||||
|
||||
Historical signature verification does not authorize an expired effect.
|
||||
The caller must separately verify its fresh registered Owner capability.
|
||||
"""
|
||||
verify_signature(old, public_key)
|
||||
verify_signature(new, public_key)
|
||||
exact = {'candidate_id': CID, 'source_commit': SOURCE, 'tree_oid': TREE,
|
||||
'artifact_sha256': ARTIFACT, 'release_plan_sha256': PLAN,
|
||||
'fingerprint': OLD_FP, 'capabilities': CAPABILITIES}
|
||||
if any(old.get(k) != v for k, v in exact.items()):
|
||||
raise RenewalRejected('EXACT_PREDECESSOR_REQUIRED')
|
||||
if set(old) != set(new) or any(new[k] != old[k] for k in set(old) - MUTABLE):
|
||||
raise RenewalRejected('IMMUTABLE_APPROVAL_BINDING_DRIFT')
|
||||
if timestamp(old['expires_at']) > now:
|
||||
raise RenewalRejected('PREDECESSOR_NOT_EXPIRED')
|
||||
issued, not_before, expires = (timestamp(new[k]) for k in ['issued_at', 'not_before', 'expires_at'])
|
||||
if not issued <= not_before <= now < expires or not 0 < expires - issued <= 900:
|
||||
raise RenewalRejected('FRESH_BOUNDED_APPROVAL_REQUIRED')
|
||||
if new['nonce'] == old['nonce'] or new['fingerprint'] == old['fingerprint']:
|
||||
raise RenewalRejected('FRESH_APPROVAL_IDENTITY_REQUIRED')
|
||||
return new['fingerprint']
|
||||
|
||||
|
||||
def secure_bytes(path, owner_uid=0):
|
||||
path = Path(path)
|
||||
descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
|
||||
with os.fdopen(descriptor, 'rb') as stream:
|
||||
metadata = os.fstat(stream.fileno())
|
||||
if not stat.S_ISREG(metadata.st_mode) or metadata.st_uid != owner_uid or metadata.st_mode & 0o022:
|
||||
raise RenewalRejected('UNSAFE_RENEWAL_FILE')
|
||||
return stream.read()
|
||||
|
||||
|
||||
def _safe_parents(path):
|
||||
"""Reject writable or symlinked ancestors before opening authority files."""
|
||||
path = Path(path)
|
||||
if not path.is_absolute() or '..' in path.parts:
|
||||
raise RenewalRejected('ABSOLUTE_CANONICAL_PATH_REQUIRED')
|
||||
for parent in path.parents:
|
||||
info = parent.lstat()
|
||||
if not stat.S_ISDIR(info.st_mode) or info.st_uid != 0 or info.st_mode & 0o022:
|
||||
raise RenewalRejected('UNSAFE_AUTHORITY_ANCESTOR')
|
||||
|
||||
|
||||
def _sha(value):
|
||||
return isinstance(value, str) and len(value) == 64 and all(c in '0123456789abcdef' for c in value)
|
||||
|
||||
|
||||
def _oid(value):
|
||||
return isinstance(value, str) and len(value) == 40 and all(c in '0123456789abcdef' for c in value)
|
||||
|
||||
|
||||
def configure_registered_bindings(path=None, expected_sha256=None, *, owner_uid=0):
|
||||
"""Install renewable completion bindings from one authenticated file.
|
||||
|
||||
No historical approval fingerprint, Saga head, Owner key, or authority/state
|
||||
path is a runtime default. The service registration must provide both the
|
||||
absolute file path and its SHA-256 digest.
|
||||
"""
|
||||
global _BINDINGS_DIGEST
|
||||
path = Path(path or os.environ.get(BINDINGS_PATH_ENV, ''))
|
||||
expected_sha256 = expected_sha256 or os.environ.get(BINDINGS_SHA256_ENV, '')
|
||||
if not path.is_absolute() or not _sha(expected_sha256):
|
||||
raise RenewalRejected('REGISTERED_COMPLETION_BINDINGS_REQUIRED')
|
||||
_safe_parents(path)
|
||||
raw = secure_bytes(path, owner_uid=owner_uid)
|
||||
if digest(raw) != expected_sha256:
|
||||
raise RenewalRejected('COMPLETION_BINDINGS_DIGEST_MISMATCH')
|
||||
if _BINDINGS_DIGEST is not None and _BINDINGS_DIGEST != expected_sha256:
|
||||
raise RenewalRejected('COMPLETION_BINDINGS_REBIND_FORBIDDEN')
|
||||
try:
|
||||
value = json.loads(raw)
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise RenewalRejected('COMPLETION_BINDINGS_INVALID') from exc
|
||||
fields = {
|
||||
'schema', 'authority_root', 'state_root', 'owner_key_id',
|
||||
'predecessor_fingerprint', 'saga_head', 'historical_root',
|
||||
'controller_workspace', 'authoritative_git_dir',
|
||||
'runtime_registry_root', 'runtime_baseline_path',
|
||||
'trusted_release_policy_path', 'trusted_release_policy_sha256',
|
||||
}
|
||||
if not isinstance(value, dict) or set(value) != fields or value.get('schema') != 'aios.c5.completion-bindings.v1':
|
||||
raise RenewalRejected('COMPLETION_BINDINGS_SCHEMA_INVALID')
|
||||
authority_root = Path(value['authority_root'])
|
||||
state_root = Path(value['state_root'])
|
||||
governed_paths = {
|
||||
name: Path(value[name]) for name in (
|
||||
'historical_root', 'controller_workspace', 'authoritative_git_dir',
|
||||
'runtime_registry_root', 'runtime_baseline_path', 'trusted_release_policy_path',
|
||||
)
|
||||
}
|
||||
owner_key_id = value['owner_key_id']
|
||||
try:
|
||||
owner_key_valid = (isinstance(owner_key_id, str)
|
||||
and owner_key_id.startswith('SHA256:')
|
||||
and len(base64.b64decode(owner_key_id[7:], validate=True)) == 32)
|
||||
except (ValueError, TypeError):
|
||||
owner_key_valid = False
|
||||
all_paths = (authority_root, state_root, *governed_paths.values())
|
||||
if (any(not item.is_absolute() or '..' in item.parts for item in all_paths)
|
||||
or not _sha(value['predecessor_fingerprint']) or not _sha(value['saga_head'])
|
||||
or not _sha(value['trusted_release_policy_sha256'])
|
||||
or not owner_key_valid):
|
||||
raise RenewalRejected('COMPLETION_BINDINGS_VALUE_INVALID')
|
||||
if path.parent != authority_root:
|
||||
raise RenewalRejected('COMPLETION_BINDINGS_ROOT_MISMATCH')
|
||||
global AUTHORITY_ROOT, STATE_ROOT, OWNER_KEY, OLD_FP, SAGA_HEAD
|
||||
global HISTORICAL_ROOT, CONTROLLER_WORKSPACE, AUTHORITATIVE_GIT_DIR
|
||||
global RUNTIME_REGISTRY_ROOT, RUNTIME_BASELINE_PATH
|
||||
global TRUSTED_RELEASE_POLICY_PATH, TRUSTED_RELEASE_POLICY_SHA256
|
||||
AUTHORITY_ROOT = authority_root
|
||||
STATE_ROOT = state_root
|
||||
OWNER_KEY = value['owner_key_id']
|
||||
OLD_FP = value['predecessor_fingerprint']
|
||||
SAGA_HEAD = value['saga_head']
|
||||
HISTORICAL_ROOT = governed_paths['historical_root']
|
||||
CONTROLLER_WORKSPACE = governed_paths['controller_workspace']
|
||||
AUTHORITATIVE_GIT_DIR = governed_paths['authoritative_git_dir']
|
||||
RUNTIME_REGISTRY_ROOT = governed_paths['runtime_registry_root']
|
||||
RUNTIME_BASELINE_PATH = governed_paths['runtime_baseline_path']
|
||||
TRUSTED_RELEASE_POLICY_PATH = governed_paths['trusted_release_policy_path']
|
||||
TRUSTED_RELEASE_POLICY_SHA256 = value['trusted_release_policy_sha256']
|
||||
_BINDINGS_DIGEST = expected_sha256
|
||||
return dict(value)
|
||||
|
||||
|
||||
class RegisteredCompletionAuthority:
|
||||
"""Fixed root registration, Owner signature and immutable evidence verifier.
|
||||
|
||||
The completed production proofs are historical facts. This fresh authority
|
||||
permits only approval renewal and remaining metadata effects; it cannot
|
||||
authorize another runtime switch or public acceptance Task.
|
||||
"""
|
||||
|
||||
@property
|
||||
def root(self):
|
||||
return AUTHORITY_ROOT
|
||||
|
||||
@property
|
||||
def state_root(self):
|
||||
return STATE_ROOT
|
||||
|
||||
def __call__(self):
|
||||
from c5_staging_installation import RegistryOwnerVerifier
|
||||
root = AUTHORITY_ROOT
|
||||
_safe_parents(root / 'contract.json')
|
||||
envelope = json.loads(secure_bytes(root / 'contract.json'))
|
||||
if set(envelope) != {'payload', 'signature'}:
|
||||
raise RenewalRejected('EXACT_AUTHORITY_ENVELOPE_REQUIRED')
|
||||
p = envelope['payload']
|
||||
fields = {'schema', 'host', 'repository', 'candidate_id', 'source_commit', 'tree_oid',
|
||||
'artifact_sha256', 'release_plan_sha256', 'state_root', 'predecessor_fingerprint',
|
||||
'predecessor_sha256', 'predecessor_uid', 'predecessor_snapshot', 'approval_nonce',
|
||||
'issued_at', 'expires_at', 'owner_key_id', 'owner_namespace', 'allowed_effects',
|
||||
'runtime_effects_allowed', 'files', 'gate_path', 'gate_observed_at',
|
||||
'governed_source_commit', 'governed_source_tree'}
|
||||
if not isinstance(p, dict) or set(p) != fields:
|
||||
raise RenewalRejected('EXACT_AUTHORITY_SCHEMA_REQUIRED')
|
||||
exact = {'schema': 'aios.c5.registered-completion-authority.v1', 'host': 'cn-network-mkt',
|
||||
'repository': 'aios/aios', 'candidate_id': CID, 'source_commit': SOURCE,
|
||||
'tree_oid': TREE, 'artifact_sha256': ARTIFACT, 'release_plan_sha256': PLAN,
|
||||
'state_root': str(STATE_ROOT), 'predecessor_fingerprint': OLD_FP,
|
||||
'owner_key_id': OWNER_KEY, 'owner_namespace': 'aios-owner-approval-v1',
|
||||
'allowed_effects': ['approval_successor_append', 'baseline_finalize', 'registry_finalize', 'tag_publish'],
|
||||
'runtime_effects_allowed': False}
|
||||
if any(p[k] != v for k, v in exact.items()) or type(p['runtime_effects_allowed']) is not bool:
|
||||
raise RenewalRejected('EXACT_COMPLETION_AUTHORITY_REQUIRED')
|
||||
now = time.time()
|
||||
if (type(p['issued_at']) is not int or type(p['expires_at']) is not int
|
||||
or not p['issued_at'] <= now < p['expires_at']
|
||||
or not 0 < p['expires_at'] - p['issued_at'] <= 1800
|
||||
or type(p['gate_observed_at']) not in {int, float}
|
||||
or not math.isfinite(p['gate_observed_at'])
|
||||
or not p['issued_at'] - 60 <= p['gate_observed_at'] <= p['issued_at']):
|
||||
raise RenewalRejected('FRESH_OWNER_AUTHORITY_REQUIRED')
|
||||
if (not _sha(p['approval_nonce']) or not _sha(p['predecessor_sha256'])
|
||||
or type(p['predecessor_uid']) is not int or p['predecessor_uid'] not in {0, 1000}
|
||||
or any(not isinstance(p[k], str) or len(p[k]) != 40
|
||||
or any(c not in '0123456789abcdef' for c in p[k])
|
||||
for k in ('governed_source_commit', 'governed_source_tree'))):
|
||||
raise RenewalRejected('AUTHORITY_IDENTITIES_INVALID')
|
||||
verifier = RegistryOwnerVerifier(Path('/etc/aios/single-host-signer-registry.json'),
|
||||
owner_signer_id='local-owner-authority')
|
||||
if not verifier.verify(namespace=p['owner_namespace'], payload=canonical(p),
|
||||
signature=envelope['signature'], key_id=p['owner_key_id']):
|
||||
raise RenewalRejected('OWNER_SIGNATURE_REJECTED')
|
||||
payload_sha = digest(canonical(p))
|
||||
registered = json.loads(secure_bytes(root / 'registration.json'))
|
||||
if (set(registered) != {'schema', 'status', 'payload_sha256', 'approval_nonce'}
|
||||
or registered != {'schema': 'aios.c5.completion-registration.v1', 'status': 'REGISTERED',
|
||||
'payload_sha256': payload_sha, 'approval_nonce': p['approval_nonce']}):
|
||||
raise RenewalRejected('ROOT_REGISTRATION_REQUIRED')
|
||||
if (root / 'rollback.json').exists():
|
||||
raise RenewalRejected('REGISTRATION_ROLLED_BACK')
|
||||
pins = p['files']
|
||||
if not isinstance(pins, list) or not pins:
|
||||
raise RenewalRejected('SIGNED_FILE_CLOSURE_REQUIRED')
|
||||
checked = {}
|
||||
for item in pins:
|
||||
if (not isinstance(item, dict) or set(item) != {'path', 'sha256', 'uid'}
|
||||
or not _sha(item['sha256']) or type(item['uid']) is not int or item['uid'] != 0):
|
||||
raise RenewalRejected('SIGNED_FILE_PIN_INVALID')
|
||||
path = Path(item['path'])
|
||||
if path.parent != root or str(path) in checked:
|
||||
raise RenewalRejected('SIGNED_FILE_PATH_INVALID')
|
||||
_safe_parents(path)
|
||||
raw = secure_bytes(path)
|
||||
if digest(raw) != item['sha256']:
|
||||
raise RenewalRejected('SIGNED_FILE_BYTES_DRIFT')
|
||||
checked[str(path)] = raw
|
||||
required = {str(root / name) for name in (*RENEWAL_SOURCE_FILES, *PHASE_DEPENDENCIES, 'source-material.json',
|
||||
'historical-production-proof.json', 'predecessor-approval.json', 'current-gate.json',
|
||||
'approval-public-keyring.json', 'original_api.py')}
|
||||
if not required <= set(checked):
|
||||
raise RenewalRejected('INCOMPLETE_REGISTERED_CLOSURE')
|
||||
if Path(__file__) != root / 'c5_completion_renewal.py':
|
||||
raise RenewalRejected('UNREGISTERED_AUTHORITY_MODULE')
|
||||
if (p['predecessor_snapshot'] != str(root / 'predecessor-approval.json')
|
||||
or p['gate_path'] != str(root / 'current-gate.json')
|
||||
or digest(checked[p['predecessor_snapshot']]) != p['predecessor_sha256']):
|
||||
raise RenewalRejected('REGISTERED_SNAPSHOT_DRIFT')
|
||||
material = json.loads(checked[str(root / 'source-material.json')])
|
||||
if (material.get('status') != 'MATERIALIZED' or material.get('task_status') != 'SUCCEEDED'
|
||||
or material.get('remote_verified') is not True
|
||||
or material.get('source_commit') != p['governed_source_commit']
|
||||
or material.get('source_tree') != p['governed_source_tree']
|
||||
or material.get('file_sha256') != {
|
||||
name: digest(checked[str(root / name)])
|
||||
for name in RENEWAL_SOURCE_FILES}):
|
||||
raise RenewalRejected('GOVERNED_CODE_PROVENANCE_REQUIRED')
|
||||
historical = json.loads(checked[str(root / 'historical-production-proof.json')])
|
||||
if (historical.get('candidate_id') != CID or historical.get('approval_fingerprint') != OLD_FP
|
||||
or historical.get('release_plan_sha256') != PLAN
|
||||
or historical.get('status') != 'COMPLETED'
|
||||
or any(historical.get(k) is not True for k in (
|
||||
'public_business_verified', 'real_c4_rollback_verified', 'c5_restore_verified'))):
|
||||
raise RenewalRejected('COMPLETED_PRODUCTION_PROOF_REQUIRED')
|
||||
gate = json.loads(checked[p['gate_path']])
|
||||
expected_gate = {'candidate_id': CID, 'source_commit': SOURCE, 'artifact_sha256': ARTIFACT,
|
||||
'release_plan_sha256': PLAN,
|
||||
'local_tag_oid': TAG_OID, 'runtime_verified': True, 'business_verified': True,
|
||||
'global_writes_enabled': False, 'observed_at': p['gate_observed_at']}
|
||||
if (any(gate.get(k) != v for k, v in expected_gate.items())
|
||||
or any(type(gate.get(k)) is not bool for k in (
|
||||
'runtime_verified', 'business_verified', 'global_writes_enabled'))
|
||||
or gate.get('remote_tag_oid') not in {None, TAG_OID}):
|
||||
raise RenewalRejected('EXACT_POSTVERIFICATION_GATE_REQUIRED')
|
||||
validate_completion_lineage(gate)
|
||||
return {**p, 'payload_sha256': payload_sha}
|
||||
|
||||
def validate_host(self, *, state_root, candidate_id, source_commit, tree_oid,
|
||||
artifact_sha256, release_plan_sha256):
|
||||
self()
|
||||
if (Path(state_root), candidate_id, source_commit, tree_oid, artifact_sha256, release_plan_sha256) != (
|
||||
STATE_ROOT, CID, SOURCE, TREE, ARTIFACT, PLAN):
|
||||
raise RenewalRejected('COMPLETION_HOST_BINDING_DRIFT')
|
||||
|
||||
def deny_runtime_effect(self, **kwargs):
|
||||
self()
|
||||
raise RenewalRejected('COMPLETION_RUNTIME_EFFECT_FORBIDDEN')
|
||||
|
||||
|
||||
class CompletionRuntimeGuard:
|
||||
def __init__(self, authority):
|
||||
if type(authority) is not RegisteredCompletionAuthority:
|
||||
raise RenewalRejected('REGISTERED_COMPLETION_AUTHORITY_REQUIRED')
|
||||
self.authority = authority
|
||||
|
||||
def execute(self, **kwargs):
|
||||
return self.authority.deny_runtime_effect(**kwargs)
|
||||
|
||||
|
||||
def append_file(path, raw):
|
||||
"""An existing record must be identical; it is never replaced."""
|
||||
path = Path(path)
|
||||
try:
|
||||
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
|
||||
except FileExistsError:
|
||||
if secure_bytes(path, owner_uid=os.geteuid()) != raw:
|
||||
raise RenewalRejected('APPEND_ONLY_RECORD_CONFLICT')
|
||||
return False
|
||||
with os.fdopen(fd, 'wb') as stream:
|
||||
stream.write(raw)
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
fd = os.open(path.parent, os.O_DIRECTORY)
|
||||
try:
|
||||
os.fsync(fd)
|
||||
finally:
|
||||
os.close(fd)
|
||||
return True
|
||||
|
||||
|
||||
def publish_successor(directory, old, new, public_key, *, now, registration_digest, predecessor_bytes):
|
||||
"""Append records inside the SAME plane under a verified caller lock.
|
||||
|
||||
Registration validation, current Saga CAS, production/business readbacks,
|
||||
and permission to call this function remain mandatory caller gates.
|
||||
"""
|
||||
fp = validate_successor(old, new, public_key, now=now)
|
||||
if json.loads(predecessor_bytes) != old:
|
||||
raise RenewalRejected('PREDECESSOR_BYTES_DRIFT')
|
||||
predecessor_bytes_digest = digest(predecessor_bytes)
|
||||
if len(registration_digest) != 64 or any(c not in '0123456789abcdef' for c in registration_digest):
|
||||
raise RenewalRejected('REGISTERED_DIGEST_REQUIRED')
|
||||
directory = Path(directory)
|
||||
metadata = directory.lstat()
|
||||
if not stat.S_ISDIR(metadata.st_mode) or metadata.st_uid != os.geteuid() or metadata.st_mode & 0o022:
|
||||
raise RenewalRejected('UNSAFE_APPEND_DIRECTORY')
|
||||
receipt = {'schema': 'aios.c5.completion-approval-successor.v1', 'candidate_id': CID,
|
||||
'predecessor_fingerprint': OLD_FP, 'predecessor_bytes_sha256': predecessor_bytes_digest,
|
||||
'successor_fingerprint': fp, 'successor_sha256': digest(canonical(new)),
|
||||
'owner_registration_payload_sha256': registration_digest,
|
||||
'completion_only': True, 'runtime_effects_allowed': False}
|
||||
# A crash after the first append is unresolved until its exact receipt is
|
||||
# reconciled. A loader must require BOTH files; no mutable current pointer.
|
||||
append_file(directory / (fp + '.approval.json'), canonical(new))
|
||||
append_file(directory / (fp + '.receipt.json'), canonical(receipt))
|
||||
return receipt
|
||||
|
||||
|
||||
class CompletionApprovalStore:
|
||||
"""Explicit store dependency for a registered completion host/API.
|
||||
|
||||
The original store and original candidate approval remain untouched.
|
||||
No global class replacement or expired approval verification is used.
|
||||
An authenticated registration fixes ONE successor nonce and immutable
|
||||
receipt path before signing. The host must separately enforce TAG_PENDING
|
||||
and forbid runtime effects; this store does not execute a Saga.
|
||||
"""
|
||||
|
||||
def __init__(self, original, directory, public_key, authority, approval_type):
|
||||
self.original = original
|
||||
self.directory = Path(directory)
|
||||
self.public_key = public_key
|
||||
self.authority = authority
|
||||
self.approval_type = approval_type
|
||||
|
||||
def _policy(self):
|
||||
# authority must be the fixed registered verifier, not a caller-supplied
|
||||
# HTTP callback. Wiring it is outside this unregistered draft.
|
||||
policy = self.authority()
|
||||
if policy.get('candidate_id') != CID or policy.get('predecessor_fingerprint') != OLD_FP:
|
||||
raise RenewalRejected('REGISTERED_EXACT_POLICY_REQUIRED')
|
||||
nonce = policy.get('approval_nonce', '')
|
||||
if not isinstance(nonce, str) or len(nonce) != 64 or any(c not in '0123456789abcdef' for c in nonce):
|
||||
raise RenewalRejected('REGISTERED_NONCE_REQUIRED')
|
||||
return policy, self.directory / (nonce + '.activation.json')
|
||||
|
||||
def _parent(self):
|
||||
old = self.original.load(CID)
|
||||
policy, _ = self._policy()
|
||||
owner = policy.get('predecessor_uid')
|
||||
if type(owner) is not int or owner not in {0, 1000}:
|
||||
raise RenewalRejected('REGISTERED_PREDECESSOR_OWNER_REQUIRED')
|
||||
raw = secure_bytes(self.original.root / (CID + '.json'), owner_uid=owner)
|
||||
if json.loads(raw) != old.to_dict() or digest(raw) != policy.get('predecessor_sha256'):
|
||||
raise RenewalRejected('PREDECESSOR_FILE_DRIFT')
|
||||
return old.to_dict(), raw
|
||||
|
||||
def load(self, candidate_id):
|
||||
if candidate_id != CID:
|
||||
return self.original.load(candidate_id)
|
||||
policy, activation = self._policy()
|
||||
if not activation.exists():
|
||||
return self.original.load(candidate_id)
|
||||
receipt = json.loads(secure_bytes(activation, owner_uid=policy['predecessor_uid']))
|
||||
expected = {'schema', 'candidate_id', 'predecessor_fingerprint', 'predecessor_bytes_sha256',
|
||||
'successor_fingerprint', 'successor_sha256', 'owner_registration_payload_sha256',
|
||||
'completion_only', 'runtime_effects_allowed'}
|
||||
if set(receipt) != expected or receipt.get('owner_registration_payload_sha256') != policy['payload_sha256']:
|
||||
raise RenewalRejected('ACTIVATION_RECEIPT_INVALID')
|
||||
fp = receipt.get('successor_fingerprint', '')
|
||||
if len(fp) != 64 or any(c not in '0123456789abcdef' for c in fp):
|
||||
raise RenewalRejected('SUCCESSOR_FINGERPRINT_INVALID')
|
||||
raw = secure_bytes(self.directory / (fp + '.approval.json'), owner_uid=policy['predecessor_uid'])
|
||||
new = json.loads(raw)
|
||||
parent, parent_raw = self._parent()
|
||||
valid = validate_successor(parent, new, self.public_key, now=datetime.now(timezone.utc).timestamp())
|
||||
exact = {'candidate_id': CID, 'predecessor_fingerprint': OLD_FP,
|
||||
'predecessor_bytes_sha256': digest(parent_raw), 'successor_fingerprint': valid,
|
||||
'successor_sha256': digest(raw), 'completion_only': True, 'runtime_effects_allowed': False}
|
||||
if any(receipt.get(k) != v for k, v in exact.items()) or new['nonce'] != policy['approval_nonce']:
|
||||
raise RenewalRejected('SUCCESSOR_ACTIVATION_DRIFT')
|
||||
durable = json.loads(secure_bytes(self.directory / (fp + '.receipt.json'), owner_uid=policy['predecessor_uid']))
|
||||
if durable != receipt:
|
||||
raise RenewalRejected('DURABLE_RECEIPT_DRIFT')
|
||||
return self.approval_type.from_dict(new)
|
||||
|
||||
def save(self, candidate_id, approval):
|
||||
if candidate_id != CID:
|
||||
return self.original.save(candidate_id, approval)
|
||||
policy, activation = self._policy()
|
||||
parent, raw = self._parent()
|
||||
if approval.fingerprint == parent['fingerprint']:
|
||||
return self.original.save(candidate_id, approval)
|
||||
new = approval.to_dict()
|
||||
if new.get('nonce') != policy['approval_nonce']:
|
||||
raise RenewalRejected('REGISTERED_NONCE_MISMATCH')
|
||||
# The caller supplies its authenticated registered effect lock and
|
||||
# invokes the existing ApprovalVerifier before this store boundary.
|
||||
receipt = publish_successor(self.directory, parent, new, self.public_key,
|
||||
now=datetime.now(timezone.utc).timestamp(),
|
||||
registration_digest=policy['payload_sha256'], predecessor_bytes=raw)
|
||||
created = append_file(activation, canonical(receipt))
|
||||
if self.load(candidate_id).fingerprint != approval.fingerprint:
|
||||
raise RenewalRejected('SUCCESSOR_READBACK_FAILED')
|
||||
return activation, created
|
||||
|
||||
|
||||
class RegisteredCompletionApprovalStore(CompletionApprovalStore):
|
||||
"""Production dependency; no request-selected root or authority callback."""
|
||||
|
||||
def __init__(self, original, public_key, approval_type):
|
||||
if Path(original.root) != STATE_ROOT / 'approvals':
|
||||
raise RenewalRejected('SAME_APPROVAL_PLANE_REQUIRED')
|
||||
super().__init__(original, STATE_ROOT / 'approvals' / 'c5-completion-successors',
|
||||
public_key, RegisteredCompletionAuthority(), approval_type)
|
||||
|
||||
|
||||
class CompletionSignerMixin:
|
||||
"""Narrow additional method of the EXISTING credential-bound signer.
|
||||
|
||||
Activation must register this exact mixin, verifier and namespace snapshot
|
||||
pins. The encrypted private key stays in the existing signer's unit. This
|
||||
draft supplies no activation, credential access or HTTP route.
|
||||
"""
|
||||
|
||||
def sign_exact_completion_successor(self, request):
|
||||
expected = {'schema', 'operation', 'candidate_id', 'predecessor_fingerprint'}
|
||||
if (set(request) != expected or request.get('operation') != 'renew_exact_completion_approval'
|
||||
or request.get('candidate_id') != CID or request.get('predecessor_fingerprint') != OLD_FP
|
||||
or request.get('schema') != 'aios.exact-release-signer.request.v1'):
|
||||
raise RenewalRejected('EXACT_SIGNER_RENEWAL_REQUEST_REQUIRED')
|
||||
policy = self.completion_authority()
|
||||
now = datetime.now(timezone.utc).timestamp()
|
||||
if (policy.get('candidate_id') != CID or policy.get('predecessor_fingerprint') != OLD_FP
|
||||
or not policy['issued_at'] <= now < policy['expires_at']
|
||||
or not 0 <= now - policy['gate_observed_at'] <= 60):
|
||||
raise RenewalRejected('FRESH_REGISTERED_COMPLETION_GATE_REQUIRED')
|
||||
old_raw = secure_bytes(policy['predecessor_snapshot'], owner_uid=0)
|
||||
if digest(old_raw) != policy['predecessor_sha256']:
|
||||
raise RenewalRejected('REGISTERED_SIGNER_PREIMAGE_DRIFT')
|
||||
old = json.loads(old_raw)
|
||||
public_key = self.private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
verify_signature(old, public_key)
|
||||
exact = {'candidate_id': CID, 'source_commit': SOURCE, 'tree_oid': TREE,
|
||||
'artifact_sha256': ARTIFACT, 'release_plan_sha256': PLAN,
|
||||
'fingerprint': OLD_FP, 'capabilities': CAPABILITIES,
|
||||
'key_id': self.policy.key_id, 'owner_identity': self.policy.owner_identity,
|
||||
'trust_domain': self.policy.trust_domain,
|
||||
'owner_approval_record_oid': self.authorization_oid}
|
||||
if any(old.get(k) != v for k, v in exact.items()) or timestamp(old['expires_at']) > now:
|
||||
raise RenewalRejected('EXACT_EXPIRED_SIGNER_PREDECESSOR_REQUIRED')
|
||||
nonce = policy['approval_nonce']
|
||||
if len(nonce) != 64 or any(c not in '0123456789abcdef' for c in nonce) or nonce == old['nonce']:
|
||||
raise RenewalRejected('FRESH_REGISTERED_SIGNER_NONCE_REQUIRED')
|
||||
output = self.state_root / 'completion-renewals' / (nonce + '.json')
|
||||
if output.exists():
|
||||
result = json.loads(secure_bytes(output, owner_uid=0))
|
||||
if (result.get('registration_payload_sha256') != policy['payload_sha256']
|
||||
or result['approval']['nonce'] != nonce):
|
||||
raise RenewalRejected('SIGNER_REPLAY_CONFLICT')
|
||||
validate_successor(old, result['approval'], public_key, now=now)
|
||||
return {**result, 'status': 'SIGNED_IDEMPOTENT'}
|
||||
expiry = min(now + 600, policy['expires_at'] - 5)
|
||||
if expiry - now < 120:
|
||||
raise RenewalRejected('INSUFFICIENT_SIGNER_WINDOW')
|
||||
def utc(value):
|
||||
return datetime.fromtimestamp(value, timezone.utc).replace(microsecond=0).isoformat().replace('+00:00', 'Z')
|
||||
payload = {k:v for k,v in old.items() if k not in {'fingerprint', 'signature'}}
|
||||
payload.update(issued_at=utc(now), not_before=utc(now), expires_at=utc(expiry), nonce=nonce)
|
||||
raw = canonical(payload)
|
||||
# All candidate, plan, historical signature and registered freshness
|
||||
# checks precede this only signing operation.
|
||||
approval = {**payload, 'fingerprint': digest(raw),
|
||||
'signature': base64.b64encode(self.private_key.sign(raw)).decode()}
|
||||
validate_successor(old, approval, public_key, now=now)
|
||||
result = {'schema': 'aios.exact-release-signer.response.v1', 'status': 'SIGNED',
|
||||
'approval': approval, 'registration_payload_sha256': policy['payload_sha256'],
|
||||
'predecessor_fingerprint': OLD_FP, 'completion_only': True, 'arbitrary_signing': False}
|
||||
# This registered directory must be created by the activation consumer
|
||||
# inside the existing signer state root, never a request path.
|
||||
append_file(output, canonical(result))
|
||||
return result
|
||||
@@ -0,0 +1,58 @@
|
||||
"""Registered fixed extension of the existing credential-bound signer unit."""
|
||||
import importlib.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from c5_completion_renewal import (
|
||||
CompletionSignerMixin, RegisteredCompletionAuthority,
|
||||
RenewalRejected, secure_bytes, digest,
|
||||
)
|
||||
|
||||
ORIGINAL = Path('/usr/local/libexec/aios_exact_release_signer.py')
|
||||
ORIGINAL_SHA = '6e2a5a9df0edfe566175e5b556d15901b2c8fe4484388ad74af2ac64f810c930'
|
||||
|
||||
|
||||
def signer_class(original):
|
||||
class CompletionSigner(CompletionSignerMixin, original.ExactReleaseSigner):
|
||||
def completion_authority(self):
|
||||
return RegisteredCompletionAuthority()()
|
||||
|
||||
def handle(self, request):
|
||||
if request.get('operation') == 'renew_exact_completion_approval':
|
||||
return self.sign_exact_completion_successor(request)
|
||||
return super().handle(request)
|
||||
return CompletionSigner
|
||||
|
||||
|
||||
def main():
|
||||
import c5_completion_renewal as renewal
|
||||
renewal.configure_registered_bindings()
|
||||
if Path(__file__) != renewal.AUTHORITY_ROOT / 'c5_completion_signer.py':
|
||||
raise RenewalRejected('REGISTERED_SIGNER_WRAPPER_REQUIRED')
|
||||
RegisteredCompletionAuthority()()
|
||||
if digest(secure_bytes(ORIGINAL)) != ORIGINAL_SHA:
|
||||
raise RenewalRejected('ORIGINAL_SIGNER_PREIMAGE_DRIFT')
|
||||
spec = importlib.util.spec_from_file_location('c5_original_exact_release_signer', ORIGINAL)
|
||||
original = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = original
|
||||
spec.loader.exec_module(original)
|
||||
credentials = Path(os.environ.get('CREDENTIALS_DIRECTORY', ''))
|
||||
if not credentials.is_absolute() or credentials.parent != Path('/run/credentials'):
|
||||
raise RenewalRejected('EXISTING_SIGNER_PURPOSE_CREDENTIAL_REQUIRED')
|
||||
if credentials.name != 'aios-exact-release-signer.service':
|
||||
raise RenewalRejected('EXISTING_SIGNER_UNIT_REQUIRED')
|
||||
cgroup = Path('/proc/self/cgroup').read_text()
|
||||
if not any(line.split(':', 2)[-1].rstrip('/').endswith('/aios-exact-release-signer.service')
|
||||
for line in cgroup.splitlines()):
|
||||
raise RenewalRejected('EXISTING_SIGNER_CGROUP_REQUIRED')
|
||||
signer = signer_class(original)(
|
||||
Path('/etc/aios-gitops/exact-release-signer-policy.json'),
|
||||
credentials / 'owner_release_private_key',
|
||||
)
|
||||
original.serve(Path('/run/aios-exact-release-signer/signer.sock'), signer)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,503 @@
|
||||
"""Exact C5 runtime-switch adapter for the silent-release Saga.
|
||||
|
||||
The adapter owns only the production deployment phase. Tag publication and
|
||||
Registry/Baseline finalization remain separate ``ReleaseOrchestrator`` Saga
|
||||
steps. A host must register immutable executable bytes and provide the real
|
||||
HMAC-authenticated source-admission store plus the signed Owner approval
|
||||
verifier before this adapter can be constructed.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import stat
|
||||
import subprocess
|
||||
from typing import Any, Mapping
|
||||
|
||||
from release_source_admission import ReleaseSourceAdmissionStore
|
||||
from silent_release_controller import ReleaseCandidate
|
||||
from silent_release_hardening import (
|
||||
CapabilityAuthorizationError,
|
||||
CapabilityLedger,
|
||||
ClaimProbeResult,
|
||||
ReleasePlan,
|
||||
RuntimeSwitchController,
|
||||
SignedApprovalVerifier,
|
||||
SignedReleasePlanApproval,
|
||||
_canonical_json,
|
||||
)
|
||||
|
||||
|
||||
C5_VERSION = "V2R2C5"
|
||||
C5_PREPUBLICATION_SOURCE_COMMIT = "396ea9811124736aa140d2810ab53eaefb6b5c1c"
|
||||
C4_ROLLBACK_TARGET = {
|
||||
"version": "V2R2C4",
|
||||
"target_oid": "e5f8209f57b43fc39e55c4555776e6362ce326eb",
|
||||
"artifact_sha256": "0dfc0461f7f9aa25cfbfc37008dcbd2d48d5341383574163b7bba102852062c4",
|
||||
}
|
||||
C4_ROLLBACK_ARTIFACT_PATH = Path(
|
||||
"/var/lib/aios-gitops/release-only/"
|
||||
"v2r2c4-e5f8209f57b4/artifact.tar.gz"
|
||||
)
|
||||
C5_PRODUCTION_ENVIRONMENT_FILE = Path("/etc/aios/v2r2c2.env")
|
||||
C5_PRODUCTION_SERVICE = "aios-v2r2c2-production.service"
|
||||
C5_PRODUCTION_PORT = 18928
|
||||
REQUEST_SCHEMA = "aios.c5.exact-production-deployment-request.v1"
|
||||
HEX64 = re.compile(r"^[0-9a-f]{64}$")
|
||||
|
||||
|
||||
class C5DeploymentBindingError(CapabilityAuthorizationError):
|
||||
"""C5 source, plan, approval, executor, or receipt binding is invalid."""
|
||||
|
||||
|
||||
class C5DeploymentOutcomeUnknown(RuntimeError):
|
||||
"""The deployment effect must be reconciled before any retry."""
|
||||
|
||||
|
||||
class C5DeploymentConflict(RuntimeError):
|
||||
"""The observed runtime is bound to a different release."""
|
||||
|
||||
|
||||
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 _run_registered_adapter(
|
||||
command: tuple[str, ...], request: Mapping[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""Invoke only the code-pinned process and accept one JSON object."""
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
list(command),
|
||||
input=_canonical_json(dict(request)),
|
||||
check=False,
|
||||
capture_output=True,
|
||||
timeout=180,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError) as exc:
|
||||
raise C5DeploymentOutcomeUnknown(
|
||||
"C5_DEPLOYMENT_EXECUTOR_UNAVAILABLE"
|
||||
) from exc
|
||||
if completed.returncode != 0:
|
||||
raise C5DeploymentOutcomeUnknown(
|
||||
"C5_DEPLOYMENT_EXECUTOR_FAILED"
|
||||
)
|
||||
try:
|
||||
lines = [
|
||||
line
|
||||
for line in completed.stdout.decode("utf-8").splitlines()
|
||||
if line.strip()
|
||||
]
|
||||
value = json.loads(lines[-1]) if lines else None
|
||||
except (UnicodeError, ValueError) as exc:
|
||||
raise C5DeploymentOutcomeUnknown(
|
||||
"C5_DEPLOYMENT_EXECUTOR_RESPONSE_INVALID"
|
||||
) from exc
|
||||
if not isinstance(value, dict):
|
||||
raise C5DeploymentOutcomeUnknown(
|
||||
"C5_DEPLOYMENT_EXECUTOR_RESPONSE_INVALID"
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def _secure_registered_file(
|
||||
path: Path,
|
||||
expected_sha256: str,
|
||||
*,
|
||||
require_root_owner: bool,
|
||||
) -> None:
|
||||
path = Path(path)
|
||||
if (
|
||||
not path.is_absolute()
|
||||
or path.is_symlink()
|
||||
or path.resolve(strict=False) != path
|
||||
):
|
||||
raise C5DeploymentBindingError("C5_EXECUTOR_PATH_INVALID")
|
||||
try:
|
||||
info = path.stat()
|
||||
except OSError as exc:
|
||||
raise C5DeploymentBindingError("C5_EXECUTOR_PATH_INVALID") from exc
|
||||
if not stat.S_ISREG(info.st_mode):
|
||||
raise C5DeploymentBindingError("C5_EXECUTOR_PATH_INVALID")
|
||||
if os.name != "nt" and stat.S_IMODE(info.st_mode) & 0o022:
|
||||
raise C5DeploymentBindingError("C5_EXECUTOR_PATH_WRITABLE")
|
||||
if require_root_owner:
|
||||
if not hasattr(os, "geteuid") or info.st_uid != 0:
|
||||
raise C5DeploymentBindingError("C5_EXECUTOR_OWNER_INVALID")
|
||||
parent = path.parent
|
||||
while True:
|
||||
parent_info = parent.stat()
|
||||
if (
|
||||
parent.is_symlink()
|
||||
or parent_info.st_uid != 0
|
||||
or stat.S_IMODE(parent_info.st_mode) & 0o022
|
||||
):
|
||||
raise C5DeploymentBindingError(
|
||||
"C5_EXECUTOR_PARENT_SECURITY_INVALID"
|
||||
)
|
||||
if parent == parent.parent:
|
||||
break
|
||||
parent = parent.parent
|
||||
if _sha256_file(path) != expected_sha256:
|
||||
raise C5DeploymentBindingError("C5_EXECUTOR_CODE_MISMATCH")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RegisteredC5DeploymentExecutor:
|
||||
"""Pinned host configuration for one concrete process adapter.
|
||||
|
||||
``adapter_path`` is optional for a native executable. When present the
|
||||
command is exactly ``executable adapter_path``; arbitrary argv and Python
|
||||
callables are intentionally unsupported.
|
||||
"""
|
||||
|
||||
executor_id: str
|
||||
executable: Path
|
||||
executable_sha256: str
|
||||
artifact_path: Path
|
||||
rollback_artifact_path: Path
|
||||
production_environment_file: Path
|
||||
adapter_path: Path | None = None
|
||||
adapter_sha256: str | None = None
|
||||
require_root_owner: bool = True
|
||||
execution_scope: str = "PRODUCTION"
|
||||
isolation_root: Path | None = None
|
||||
rollback_target: Mapping[str, str] | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.executor_id or len(self.executor_id) > 255:
|
||||
raise C5DeploymentBindingError("C5_EXECUTOR_ID_INVALID")
|
||||
if HEX64.fullmatch(self.executable_sha256) is None:
|
||||
raise C5DeploymentBindingError("C5_EXECUTOR_DIGEST_INVALID")
|
||||
if (self.adapter_path is None) != (self.adapter_sha256 is None):
|
||||
raise C5DeploymentBindingError("C5_EXECUTOR_ADAPTER_BINDING_INVALID")
|
||||
if (
|
||||
self.adapter_sha256 is not None
|
||||
and HEX64.fullmatch(self.adapter_sha256) is None
|
||||
):
|
||||
raise C5DeploymentBindingError("C5_EXECUTOR_DIGEST_INVALID")
|
||||
if not Path(self.artifact_path).is_absolute():
|
||||
raise C5DeploymentBindingError("C5_ARTIFACT_PATH_INVALID")
|
||||
if self.execution_scope not in {"PRODUCTION", "ISOLATED_REHEARSAL"}:
|
||||
raise C5DeploymentBindingError("C5_EXECUTION_SCOPE_INVALID")
|
||||
target = dict(self.rollback_target or C4_ROLLBACK_TARGET)
|
||||
if target.keys() != C4_ROLLBACK_TARGET.keys() or target["version"] != C4_ROLLBACK_TARGET["version"] or target["target_oid"] != C4_ROLLBACK_TARGET["target_oid"] or HEX64.fullmatch(str(target["artifact_sha256"])) is None:
|
||||
raise C5DeploymentBindingError("C5_ROLLBACK_TARGET_INVALID")
|
||||
if self.execution_scope == "PRODUCTION":
|
||||
if target != C4_ROLLBACK_TARGET:
|
||||
raise C5DeploymentBindingError("C5_ROLLBACK_TARGET_INVALID")
|
||||
if Path(self.rollback_artifact_path) != C4_ROLLBACK_ARTIFACT_PATH:
|
||||
raise C5DeploymentBindingError("C5_ROLLBACK_ARTIFACT_PATH_INVALID")
|
||||
if Path(self.production_environment_file) != C5_PRODUCTION_ENVIRONMENT_FILE:
|
||||
raise C5DeploymentBindingError("C5_PRODUCTION_ENVIRONMENT_FILE_INVALID")
|
||||
if self.isolation_root is not None:
|
||||
raise C5DeploymentBindingError("C5_ISOLATION_ROOT_INVALID")
|
||||
else:
|
||||
if self.isolation_root is None or not Path(self.isolation_root).is_absolute():
|
||||
raise C5DeploymentBindingError("C5_ISOLATION_ROOT_INVALID")
|
||||
root = Path(self.isolation_root).resolve(strict=False)
|
||||
for bound_path in (self.rollback_artifact_path, self.production_environment_file):
|
||||
path = Path(bound_path)
|
||||
if not path.is_absolute() or path.resolve(strict=False) == root:
|
||||
raise C5DeploymentBindingError("C5_REHEARSAL_BINDING_INVALID")
|
||||
try:
|
||||
path.resolve(strict=False).relative_to(root)
|
||||
except ValueError as exc:
|
||||
raise C5DeploymentBindingError("C5_REHEARSAL_BINDING_INVALID") from exc
|
||||
_secure_registered_file(
|
||||
Path(self.rollback_artifact_path),
|
||||
target["artifact_sha256"],
|
||||
require_root_owner=self.require_root_owner,
|
||||
)
|
||||
object.__setattr__(self, "rollback_target", target)
|
||||
self.verify_code()
|
||||
|
||||
def verify_code(self) -> None:
|
||||
_secure_registered_file(
|
||||
Path(self.executable),
|
||||
self.executable_sha256,
|
||||
require_root_owner=self.require_root_owner,
|
||||
)
|
||||
if self.adapter_path is not None:
|
||||
_secure_registered_file(
|
||||
Path(self.adapter_path),
|
||||
str(self.adapter_sha256),
|
||||
require_root_owner=self.require_root_owner,
|
||||
)
|
||||
|
||||
@property
|
||||
def command(self) -> tuple[str, ...]:
|
||||
values = [str(Path(self.executable))]
|
||||
if self.adapter_path is not None:
|
||||
values.append(str(Path(self.adapter_path)))
|
||||
return tuple(values)
|
||||
|
||||
|
||||
class C5ExactDeploymentAdapter:
|
||||
"""Callable runtime-switch phase with exact C5 admission and approval."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
candidate: ReleaseCandidate,
|
||||
release_plan: ReleasePlan,
|
||||
approval: SignedReleasePlanApproval,
|
||||
approval_verifier: SignedApprovalVerifier,
|
||||
source_admissions: ReleaseSourceAdmissionStore,
|
||||
executor: RegisteredC5DeploymentExecutor,
|
||||
) -> None:
|
||||
if not isinstance(approval_verifier, SignedApprovalVerifier):
|
||||
raise C5DeploymentBindingError("C5_APPROVAL_VERIFIER_REQUIRED")
|
||||
if not isinstance(source_admissions, ReleaseSourceAdmissionStore):
|
||||
raise C5DeploymentBindingError("C5_SOURCE_ADMISSION_STORE_REQUIRED")
|
||||
self.candidate = candidate
|
||||
self.release_plan = release_plan
|
||||
self.approval = approval
|
||||
self.approval_verifier = approval_verifier
|
||||
self.source_admissions = source_admissions
|
||||
self.executor = executor
|
||||
self._binding = self._verify_binding(candidate, release_plan)
|
||||
|
||||
@staticmethod
|
||||
def _global_writes_disabled() -> bool:
|
||||
return os.environ.get("AIOS_GITOPS_WRITES_ENABLED", "false").lower() == "false"
|
||||
|
||||
def _verify_binding(
|
||||
self,
|
||||
candidate: ReleaseCandidate,
|
||||
plan: ReleasePlan,
|
||||
) -> dict[str, Any]:
|
||||
if candidate.fingerprint != self.candidate.fingerprint or plan.sha256 != self.release_plan.sha256:
|
||||
raise C5DeploymentBindingError("C5_RUNTIME_INPUT_BINDING_MISMATCH")
|
||||
plan.validate()
|
||||
if plan.tag.version != C5_VERSION or plan.finalization.version != C5_VERSION:
|
||||
raise C5DeploymentBindingError("C5_VERSION_BINDING_INVALID")
|
||||
if candidate.source_commit == C5_PREPUBLICATION_SOURCE_COMMIT:
|
||||
raise C5DeploymentBindingError("C5_PREPUBLICATION_SOURCE_FORBIDDEN")
|
||||
rollback_target = dict(self.executor.rollback_target or C4_ROLLBACK_TARGET)
|
||||
if plan.deployment.rollback_target != rollback_target:
|
||||
raise C5DeploymentBindingError("C5_ROLLBACK_TARGET_INVALID")
|
||||
if plan.deployment.service != C5_PRODUCTION_SERVICE:
|
||||
raise C5DeploymentBindingError("C5_PRODUCTION_SERVICE_INVALID")
|
||||
if plan.deployment.writer_expected is not False:
|
||||
raise C5DeploymentBindingError("C5_LEGACY_WRITES_MUST_REMAIN_DISABLED")
|
||||
if not self._global_writes_disabled():
|
||||
raise C5DeploymentBindingError("C5_LEGACY_WRITES_MUST_REMAIN_DISABLED")
|
||||
|
||||
admitted = self.source_admissions.resolve_recipe(
|
||||
C5_VERSION,
|
||||
candidate.candidate_id,
|
||||
candidate.source_commit,
|
||||
)
|
||||
if (
|
||||
admitted.get("source_commit") != candidate.source_commit
|
||||
or admitted.get("tree_oid") != candidate.tree_oid
|
||||
):
|
||||
raise C5DeploymentBindingError("C5_ADMITTED_SOURCE_BINDING_INVALID")
|
||||
if (
|
||||
plan.deployment.source_commit != candidate.source_commit
|
||||
or plan.deployment.tree_oid != candidate.tree_oid
|
||||
or plan.deployment.artifact_sha256 != candidate.artifact_sha256
|
||||
or plan.deployment.manifest_sha256 != candidate.manifest_sha256
|
||||
):
|
||||
raise C5DeploymentBindingError("C5_CANDIDATE_PLAN_BINDING_INVALID")
|
||||
self.approval_verifier.verify_release_plan(
|
||||
self.approval,
|
||||
candidate,
|
||||
plan,
|
||||
plan.policy_sha256,
|
||||
)
|
||||
|
||||
artifact = Path(self.executor.artifact_path)
|
||||
_secure_registered_file(
|
||||
artifact,
|
||||
candidate.artifact_sha256,
|
||||
require_root_owner=self.executor.require_root_owner,
|
||||
)
|
||||
artifact_size = artifact.stat().st_size
|
||||
if artifact_size <= 0:
|
||||
raise C5DeploymentBindingError("C5_ARTIFACT_BYTES_MISMATCH")
|
||||
self.executor.verify_code()
|
||||
return {
|
||||
"version": C5_VERSION,
|
||||
"candidate_id": candidate.candidate_id,
|
||||
"source_commit": candidate.source_commit,
|
||||
"target_oid": candidate.source_commit,
|
||||
"tree_oid": candidate.tree_oid,
|
||||
"artifact_sha256": candidate.artifact_sha256,
|
||||
"artifact_path": str(artifact),
|
||||
"artifact_size": artifact_size,
|
||||
"manifest_sha256": candidate.manifest_sha256,
|
||||
"deployment_plan_sha256": plan.deployment.sha256,
|
||||
"release_plan_sha256": plan.sha256,
|
||||
"policy_sha256": plan.policy_sha256,
|
||||
"approval_plan_id": self.approval.approval_plan_id,
|
||||
"approval_fingerprint": self.approval.fingerprint,
|
||||
"owner_approval_record_oid": self.approval.owner_approval_record_oid,
|
||||
"evidence_closure_oid": self.approval.evidence_closure_oid,
|
||||
"repository_identity": plan.deployment.repository_identity,
|
||||
"production_service": plan.deployment.service,
|
||||
"production_port": C5_PRODUCTION_PORT,
|
||||
"systemd_unit_sha256": plan.deployment.systemd_unit_sha256,
|
||||
"dropin_sha256": plan.deployment.dropin_sha256,
|
||||
"rollback_target": rollback_target,
|
||||
"rollback_artifact_path": str(
|
||||
self.executor.rollback_artifact_path
|
||||
),
|
||||
"production_environment_file": str(
|
||||
self.executor.production_environment_file
|
||||
),
|
||||
"writer_expected": False,
|
||||
"global_legacy_writes_enabled": False,
|
||||
"registered_executor_id": self.executor.executor_id,
|
||||
"executor_executable_sha256": self.executor.executable_sha256,
|
||||
"executor_adapter_sha256": self.executor.adapter_sha256,
|
||||
}
|
||||
|
||||
def _request(self, operation: str) -> dict[str, Any]:
|
||||
return {
|
||||
"schema": REQUEST_SCHEMA,
|
||||
"operation": operation,
|
||||
**self._binding,
|
||||
}
|
||||
|
||||
def _invoke(self, operation: str) -> dict[str, Any]:
|
||||
if not self._global_writes_disabled():
|
||||
raise C5DeploymentBindingError("C5_LEGACY_WRITES_MUST_REMAIN_DISABLED")
|
||||
self.executor.verify_code()
|
||||
value = _run_registered_adapter(
|
||||
self.executor.command, self._request(operation)
|
||||
)
|
||||
for key, expected in self._binding.items():
|
||||
if value.get(key) != expected:
|
||||
raise C5DeploymentBindingError(f"C5_DEPLOYMENT_RECEIPT_MISMATCH:{key}")
|
||||
if value.get("writer_disabled") is not True:
|
||||
raise C5DeploymentBindingError("C5_DEPLOYMENT_WRITER_STATE_INVALID")
|
||||
receipt = value.get("deployment_receipt")
|
||||
if not isinstance(receipt, Mapping) or not receipt:
|
||||
raise C5DeploymentBindingError("C5_DEPLOYMENT_RECEIPT_REQUIRED")
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def _classify(value: Mapping[str, Any]) -> ClaimProbeResult:
|
||||
status = value.get("status")
|
||||
if (
|
||||
status == "ACTIVE_VERIFIED"
|
||||
and value.get("verified") is True
|
||||
and value.get("deployment_state") == "ACTIVE"
|
||||
and value.get("production_deployed") is True
|
||||
and value.get("writer_disabled") is True
|
||||
and value.get("global_legacy_writes_enabled") is False
|
||||
):
|
||||
return ClaimProbeResult.APPLIED_VERIFIED
|
||||
if status == "NOT_APPLIED":
|
||||
return ClaimProbeResult.NOT_APPLIED
|
||||
if status == "CONFLICT":
|
||||
return ClaimProbeResult.CONFLICT
|
||||
return ClaimProbeResult.INDETERMINATE
|
||||
|
||||
def recovery_probe(self) -> ClaimProbeResult:
|
||||
"""Read-only exact probe used by ``CapabilityLedger`` after UNKNOWN."""
|
||||
try:
|
||||
self._verify_binding(self.candidate, self.release_plan)
|
||||
return self._classify(self._invoke("probe"))
|
||||
except C5DeploymentConflict:
|
||||
return ClaimProbeResult.CONFLICT
|
||||
except Exception:
|
||||
return ClaimProbeResult.INDETERMINATE
|
||||
|
||||
def rollback(
|
||||
self,
|
||||
*,
|
||||
candidate: ReleaseCandidate,
|
||||
plan: ReleasePlan,
|
||||
) -> dict[str, Any]:
|
||||
"""Execute only the explicit Saga rollback phase and verify exact C4."""
|
||||
self._verify_binding(candidate, plan)
|
||||
value = self._invoke("rollback")
|
||||
if (
|
||||
value.get("status") != "ROLLED_BACK_VERIFIED"
|
||||
or value.get("verified") is not True
|
||||
or value.get("rollback_verified") is not True
|
||||
or value.get("rollback_version") != self.executor.rollback_target["version"]
|
||||
or value.get("rollback_target_oid")
|
||||
!= self.executor.rollback_target["target_oid"]
|
||||
or value.get("rollback_artifact_sha256")
|
||||
!= self.executor.rollback_target["artifact_sha256"]
|
||||
):
|
||||
raise C5DeploymentOutcomeUnknown("C5_ROLLBACK_OUTCOME_UNKNOWN")
|
||||
return {
|
||||
"verified": True,
|
||||
"rollback_verified": True,
|
||||
"rollback_version": self.executor.rollback_target["version"],
|
||||
"rollback_target_oid": self.executor.rollback_target["target_oid"],
|
||||
"rollback_artifact_sha256": self.executor.rollback_target["artifact_sha256"],
|
||||
"writer_disabled": True,
|
||||
}
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
*,
|
||||
candidate: ReleaseCandidate,
|
||||
plan: ReleasePlan,
|
||||
) -> dict[str, Any]:
|
||||
self._verify_binding(candidate, plan)
|
||||
probe = self._invoke("probe")
|
||||
classification = self._classify(probe)
|
||||
if classification == ClaimProbeResult.CONFLICT:
|
||||
raise C5DeploymentConflict("C5_RUNTIME_BINDING_CONFLICT")
|
||||
if classification == ClaimProbeResult.INDETERMINATE:
|
||||
raise C5DeploymentOutcomeUnknown("C5_DEPLOYMENT_OUTCOME_UNKNOWN")
|
||||
if classification == ClaimProbeResult.NOT_APPLIED:
|
||||
applied = self._invoke("apply")
|
||||
if applied.get("status") == "CONFLICT":
|
||||
raise C5DeploymentConflict("C5_RUNTIME_BINDING_CONFLICT")
|
||||
if applied.get("status") not in {"APPLIED", "ACTIVE_VERIFIED"}:
|
||||
raise C5DeploymentOutcomeUnknown("C5_DEPLOYMENT_OUTCOME_UNKNOWN")
|
||||
probe = self._invoke("probe")
|
||||
classification = self._classify(probe)
|
||||
if classification == ClaimProbeResult.CONFLICT:
|
||||
raise C5DeploymentConflict("C5_RUNTIME_BINDING_CONFLICT")
|
||||
if classification != ClaimProbeResult.APPLIED_VERIFIED:
|
||||
raise C5DeploymentOutcomeUnknown("C5_DEPLOYMENT_OUTCOME_UNKNOWN")
|
||||
return {
|
||||
"verified": True,
|
||||
"version": C5_VERSION,
|
||||
"candidate_id": candidate.candidate_id,
|
||||
"source_commit": candidate.source_commit,
|
||||
"tree_oid": candidate.tree_oid,
|
||||
"artifact_sha256": candidate.artifact_sha256,
|
||||
"release_plan_sha256": plan.sha256,
|
||||
"approval_fingerprint": self.approval.fingerprint,
|
||||
"rollback_target": dict(self.executor.rollback_target),
|
||||
"writer_disabled": True,
|
||||
"global_legacy_writes_enabled": False,
|
||||
"registered_executor_id": self.executor.executor_id,
|
||||
"deployment_receipt_sha256": hashlib.sha256(
|
||||
_canonical_json(dict(probe))
|
||||
).hexdigest(),
|
||||
}
|
||||
|
||||
def controller(
|
||||
self,
|
||||
ledger: CapabilityLedger,
|
||||
*,
|
||||
lease_seconds: float = 300,
|
||||
) -> RuntimeSwitchController:
|
||||
"""Bind this exact adapter to the existing capability controller."""
|
||||
return RuntimeSwitchController(
|
||||
ledger,
|
||||
self,
|
||||
recovery_probe=self.recovery_probe,
|
||||
executor_id=self.executor.executor_id,
|
||||
lease_seconds=lease_seconds,
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,688 @@
|
||||
"""Owner-registered dispatch boundary for the fixed-scope C5 installer.
|
||||
|
||||
The registration authorizes one exact signed installation contract and one
|
||||
exact set of installed executor bytes. It is not an execution receipt; the
|
||||
wrapped :class:`Installer` remains the component that durably records and
|
||||
reconciles installation state.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import socket
|
||||
import stat
|
||||
import sys
|
||||
import time
|
||||
from typing import Any, Callable, Mapping
|
||||
|
||||
import c5_staging_installation as _installer_module
|
||||
from c5_staging_installation import (
|
||||
BRIDGE_EFFECTS,
|
||||
BRIDGE_TARGET,
|
||||
HOST,
|
||||
InstallationError,
|
||||
InstallationOutcomeUnknown,
|
||||
ISSUER_EFFECTS,
|
||||
ISSUER_TARGET,
|
||||
OWNER_NAMESPACE,
|
||||
R14_SOURCE_COMMIT,
|
||||
REPOSITORY,
|
||||
SUCCESSOR_SCHEMA,
|
||||
Installer,
|
||||
RegistryOwnerVerifier,
|
||||
canonical_bytes,
|
||||
verify_signed_contract,
|
||||
)
|
||||
from registered_owner_signer_adapter import RegisteredOwnerSignerAdapter
|
||||
|
||||
|
||||
SCHEMA = "aios.c5.registered-installer-executor.v1"
|
||||
PROVENANCE_SCHEMA = "aios.c5.executor-provenance.v1"
|
||||
EXECUTION_DIGEST = "0779fa615e8e7971a8dcfccf0f295b1323cd9b64787e174aea4fd2affc414339"
|
||||
BRIDGE_PORT = 18892
|
||||
ISSUER_PORT = 18946
|
||||
CAPABILITIES = frozenset({"install", "rollback", "reconcile"})
|
||||
TARGET_EFFECTS = {BRIDGE_TARGET: BRIDGE_EFFECTS, ISSUER_TARGET: ISSUER_EFFECTS}
|
||||
MAX_TTL_SECONDS = 900
|
||||
MAX_CODE_BYTES = 16 * 1024 * 1024
|
||||
DEFAULT_REGISTRY = Path("/etc/aios/single-host-signer-registry.json")
|
||||
DEFAULT_PROVENANCE_MANIFEST = Path("/etc/aios/c5-executor-provenance.json")
|
||||
HEX40 = re.compile(r"^[0-9a-f]{40}$")
|
||||
HEX64 = re.compile(r"^[0-9a-f]{64}$")
|
||||
NONCE = re.compile(r"^[A-Za-z0-9._:\-]{16,128}$")
|
||||
SCOPE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:\-]{2,127}$")
|
||||
|
||||
|
||||
class ExecutorRegistrationError(RuntimeError):
|
||||
def __init__(self, code: str) -> None:
|
||||
super().__init__(code)
|
||||
self.code = code
|
||||
|
||||
|
||||
def _fail(code: str) -> None:
|
||||
raise ExecutorRegistrationError(code)
|
||||
|
||||
|
||||
def _sha256_bytes(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def _require_digest(value: Any, pattern: re.Pattern[str], code: str) -> str:
|
||||
if not isinstance(value, str) or pattern.fullmatch(value) is None:
|
||||
_fail(code)
|
||||
return value
|
||||
|
||||
|
||||
def _strict_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
||||
result: dict[str, Any] = {}
|
||||
for key, value in pairs:
|
||||
if key in result:
|
||||
_fail("EXECUTOR_JSON_DUPLICATE_FIELD")
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
def _secure_bytes(
|
||||
path: Path, *, require_root_owner: bool = True, maximum: int = MAX_CODE_BYTES,
|
||||
) -> bytes:
|
||||
path = Path(path)
|
||||
if not path.is_absolute():
|
||||
_fail("EXECUTOR_CODE_PATH_INVALID")
|
||||
try:
|
||||
if path.is_symlink() or path.resolve(strict=True) != path:
|
||||
_fail("EXECUTOR_CODE_PATH_INVALID")
|
||||
before = path.stat()
|
||||
except OSError as exc:
|
||||
raise ExecutorRegistrationError("EXECUTOR_CODE_PATH_INVALID") from exc
|
||||
flags = os.O_RDONLY
|
||||
if hasattr(os, "O_NOFOLLOW"):
|
||||
flags |= os.O_NOFOLLOW
|
||||
try:
|
||||
fd = os.open(path, flags)
|
||||
except OSError as exc:
|
||||
raise ExecutorRegistrationError("EXECUTOR_CODE_PATH_INVALID") from exc
|
||||
try:
|
||||
current = os.fstat(fd)
|
||||
if (
|
||||
not stat.S_ISREG(current.st_mode)
|
||||
or current.st_dev != before.st_dev
|
||||
or current.st_ino != before.st_ino
|
||||
or current.st_size < 1
|
||||
or current.st_size > maximum
|
||||
):
|
||||
_fail("EXECUTOR_CODE_FILE_INVALID")
|
||||
if require_root_owner:
|
||||
if os.name != "posix" or current.st_uid != 0:
|
||||
_fail("EXECUTOR_CODE_PATH_OWNER_INVALID")
|
||||
if stat.S_IMODE(current.st_mode) & 0o022:
|
||||
_fail("EXECUTOR_CODE_PATH_WRITABLE")
|
||||
parent = path.parent
|
||||
while True:
|
||||
parent_stat = parent.stat()
|
||||
if parent.is_symlink() or parent_stat.st_uid != 0:
|
||||
_fail("EXECUTOR_CODE_PATH_OWNER_INVALID")
|
||||
if stat.S_IMODE(parent_stat.st_mode) & 0o022:
|
||||
_fail("EXECUTOR_CODE_PATH_WRITABLE")
|
||||
if parent == parent.parent:
|
||||
break
|
||||
parent = parent.parent
|
||||
chunks: list[bytes] = []
|
||||
remaining = current.st_size
|
||||
while remaining:
|
||||
chunk = os.read(fd, min(remaining, 1024 * 1024))
|
||||
if not chunk:
|
||||
_fail("EXECUTOR_CODE_FILE_INVALID")
|
||||
chunks.append(chunk)
|
||||
remaining -= len(chunk)
|
||||
if os.read(fd, 1):
|
||||
_fail("EXECUTOR_CODE_FILE_INVALID")
|
||||
return b"".join(chunks)
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CodeProvenance:
|
||||
environment: str
|
||||
instance_id: str
|
||||
integration_commit: str
|
||||
integration_tree: str
|
||||
installer_module_path: Path
|
||||
installer_script_path: Path
|
||||
executor_module_path: Path
|
||||
installer_module_sha256: str
|
||||
installer_script_sha256: str
|
||||
executor_module_sha256: str
|
||||
authority_manifest_sha256: str
|
||||
|
||||
|
||||
def _actual_installer_module_path() -> Path:
|
||||
value = getattr(_installer_module, "__file__", None)
|
||||
if not isinstance(value, str):
|
||||
_fail("EXECUTOR_LOADED_INSTALLER_PATH_INVALID")
|
||||
return Path(value).resolve(strict=True)
|
||||
|
||||
|
||||
def _actual_executor_module_path() -> Path:
|
||||
return Path(__file__).resolve(strict=True)
|
||||
|
||||
|
||||
def load_code_provenance(
|
||||
manifest: Path, *, require_root_owner: bool = True,
|
||||
) -> CodeProvenance:
|
||||
"""Load identity from an authority manifest and actual loaded paths."""
|
||||
manifest = Path(manifest)
|
||||
raw = _secure_bytes(manifest, require_root_owner=require_root_owner)
|
||||
try:
|
||||
value = json.loads(raw, object_pairs_hook=_strict_object)
|
||||
except (UnicodeError, json.JSONDecodeError) as exc:
|
||||
raise ExecutorRegistrationError("EXECUTOR_PROVENANCE_INVALID") from exc
|
||||
if (
|
||||
not isinstance(value, dict)
|
||||
or set(value) != {
|
||||
"schema", "environment", "instance_id", "integration_commit",
|
||||
"integration_tree", "entries",
|
||||
}
|
||||
or value["schema"] != PROVENANCE_SCHEMA
|
||||
or not isinstance(value["entries"], dict)
|
||||
or set(value["entries"]) != {
|
||||
"installer_module", "installer_script", "executor_module",
|
||||
}
|
||||
):
|
||||
_fail("EXECUTOR_PROVENANCE_INVALID")
|
||||
expected_paths = {
|
||||
"installer_module": _actual_installer_module_path(),
|
||||
"executor_module": _actual_executor_module_path(),
|
||||
}
|
||||
resolved: dict[str, Path] = {}
|
||||
hashes: dict[str, str] = {}
|
||||
for label, entry in value["entries"].items():
|
||||
if not isinstance(entry, dict) or set(entry) != {"path", "sha256"}:
|
||||
_fail("EXECUTOR_PROVENANCE_INVALID")
|
||||
if not isinstance(entry["path"], str):
|
||||
_fail("EXECUTOR_PROVENANCE_INVALID")
|
||||
path = Path(entry["path"])
|
||||
if not path.is_absolute():
|
||||
_fail("EXECUTOR_CODE_PATH_INVALID")
|
||||
try:
|
||||
canonical = path.resolve(strict=True)
|
||||
except OSError as exc:
|
||||
raise ExecutorRegistrationError("EXECUTOR_CODE_PATH_INVALID") from exc
|
||||
if path != canonical:
|
||||
_fail("EXECUTOR_CODE_PATH_INVALID")
|
||||
if label in expected_paths and canonical != expected_paths[label]:
|
||||
_fail("EXECUTOR_PROVENANCE_LOADED_PATH_MISMATCH")
|
||||
if label == "installer_script" and canonical.name != "c5_staging_installation.py":
|
||||
_fail("EXECUTOR_INSTALLER_SCRIPT_PATH_INVALID")
|
||||
digest = _sha256_bytes(
|
||||
_secure_bytes(canonical, require_root_owner=require_root_owner)
|
||||
)
|
||||
if entry["sha256"] != digest:
|
||||
_fail("EXECUTOR_PROVENANCE_CODE_MISMATCH")
|
||||
resolved[label] = canonical
|
||||
hashes[label] = digest
|
||||
if (
|
||||
not isinstance(value["environment"], str)
|
||||
or SCOPE_ID.fullmatch(value["environment"]) is None
|
||||
or not isinstance(value["instance_id"], str)
|
||||
or SCOPE_ID.fullmatch(value["instance_id"]) is None
|
||||
):
|
||||
_fail("EXECUTOR_PROVENANCE_SCOPE_INVALID")
|
||||
return CodeProvenance(
|
||||
environment=value["environment"],
|
||||
instance_id=value["instance_id"],
|
||||
integration_commit=_require_digest(
|
||||
value["integration_commit"], HEX40,
|
||||
"EXECUTOR_INTEGRATION_COMMIT_INVALID",
|
||||
),
|
||||
integration_tree=_require_digest(
|
||||
value["integration_tree"], HEX40,
|
||||
"EXECUTOR_INTEGRATION_TREE_INVALID",
|
||||
),
|
||||
installer_module_path=resolved["installer_module"],
|
||||
installer_script_path=resolved["installer_script"],
|
||||
executor_module_path=resolved["executor_module"],
|
||||
installer_module_sha256=hashes["installer_module"],
|
||||
installer_script_sha256=hashes["installer_script"],
|
||||
executor_module_sha256=hashes["executor_module"],
|
||||
authority_manifest_sha256=_sha256_bytes(raw),
|
||||
)
|
||||
|
||||
|
||||
def _contract_binding(payload: Mapping[str, Any], digest: str) -> dict[str, Any]:
|
||||
return {
|
||||
"target": payload["target"],
|
||||
"contract_digest": digest,
|
||||
"old_snapshot_sha256": _sha256_bytes(canonical_bytes(payload["existing"])),
|
||||
"new_manifest_sha256": _sha256_bytes(canonical_bytes({
|
||||
"release_root": payload["release_root"],
|
||||
"files": payload["files"],
|
||||
"readiness": payload["readiness"],
|
||||
})),
|
||||
"rollback_sha256": _sha256_bytes(canonical_bytes(payload["rollback"])),
|
||||
"contract_issued_at": payload["issued_at"],
|
||||
"contract_expires_at": payload["expires_at"],
|
||||
"contract_nonce": payload["nonce"],
|
||||
}
|
||||
|
||||
|
||||
def _issuer_dependency(payload: Mapping[str, Any], digest: str) -> dict[str, Any]:
|
||||
return {
|
||||
"target": payload["target"],
|
||||
"contract_digest": digest,
|
||||
"source_commit": payload["source_commit"],
|
||||
"support_commit": payload["support_commit"],
|
||||
"owner_signer_id": payload["owner_signer_id"],
|
||||
"owner_key_id": payload["owner_key_id"],
|
||||
"contract_nonce": payload["nonce"],
|
||||
"contract_issued_at": payload["issued_at"],
|
||||
"contract_expires_at": payload["expires_at"],
|
||||
"rollback_sha256": _sha256_bytes(canonical_bytes(payload["rollback"])),
|
||||
}
|
||||
|
||||
|
||||
def _registration_payload(
|
||||
*, executor_id: Any, nonce: Any, issued_at: Any, expires_at: Any,
|
||||
contract_payload: Mapping[str, Any], contract_digest: str,
|
||||
provenance: CodeProvenance,
|
||||
issuer_dependency: Mapping[str, Any] | None,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"schema": SCHEMA,
|
||||
"executor_id": executor_id,
|
||||
"owner_signer_id": contract_payload["owner_signer_id"],
|
||||
"owner_key_id": contract_payload["owner_key_id"],
|
||||
"host": HOST,
|
||||
"repository": REPOSITORY,
|
||||
"environment": provenance.environment,
|
||||
"instance_id": provenance.instance_id,
|
||||
"source_commit": R14_SOURCE_COMMIT,
|
||||
"support_commit": contract_payload["support_commit"],
|
||||
"integration_commit": provenance.integration_commit,
|
||||
"integration_tree": provenance.integration_tree,
|
||||
"installer_module_sha256": provenance.installer_module_sha256,
|
||||
"installer_script_sha256": provenance.installer_script_sha256,
|
||||
"executor_module_sha256": provenance.executor_module_sha256,
|
||||
"authority_manifest_sha256": provenance.authority_manifest_sha256,
|
||||
"execution_digest": EXECUTION_DIGEST,
|
||||
"bridge_port": BRIDGE_PORT,
|
||||
"issuer_port": ISSUER_PORT,
|
||||
"capabilities": sorted(CAPABILITIES),
|
||||
"effects": sorted(contract_payload["effects"]),
|
||||
"issuer_dependency": dict(issuer_dependency) if issuer_dependency is not None else None,
|
||||
"issued_at": issued_at,
|
||||
"expires_at": expires_at,
|
||||
"nonce": nonce,
|
||||
**_contract_binding(contract_payload, contract_digest),
|
||||
}
|
||||
|
||||
|
||||
def _validate_issuer_dependency(
|
||||
dependency: Any, contract_payload: Mapping[str, Any],
|
||||
) -> None:
|
||||
if contract_payload["target"] == ISSUER_TARGET:
|
||||
if dependency is not None:
|
||||
_fail("EXECUTOR_ISSUER_DEPENDENCY_INVALID")
|
||||
return
|
||||
if (
|
||||
not isinstance(dependency, Mapping)
|
||||
or set(dependency) != {
|
||||
"target", "contract_digest", "source_commit", "support_commit",
|
||||
"owner_signer_id", "owner_key_id", "contract_nonce",
|
||||
"contract_issued_at", "contract_expires_at", "rollback_sha256",
|
||||
}
|
||||
or dependency["target"] != ISSUER_TARGET
|
||||
or dependency["source_commit"] != contract_payload["source_commit"]
|
||||
or dependency["support_commit"] != contract_payload["support_commit"]
|
||||
or dependency["owner_signer_id"] != contract_payload["owner_signer_id"]
|
||||
or dependency["owner_key_id"] != contract_payload["owner_key_id"]
|
||||
or type(dependency["contract_issued_at"]) is not int
|
||||
or type(dependency["contract_expires_at"]) is not int
|
||||
):
|
||||
_fail("EXECUTOR_ISSUER_DEPENDENCY_INVALID")
|
||||
_require_digest(
|
||||
dependency["contract_digest"], HEX64,
|
||||
"EXECUTOR_ISSUER_DEPENDENCY_INVALID",
|
||||
)
|
||||
_require_digest(
|
||||
dependency["rollback_sha256"], HEX64,
|
||||
"EXECUTOR_ISSUER_DEPENDENCY_INVALID",
|
||||
)
|
||||
|
||||
|
||||
def _validate_payload(
|
||||
payload: Mapping[str, Any], *, now: int, provenance: CodeProvenance,
|
||||
contract_payload: Mapping[str, Any], contract_digest: str,
|
||||
issuer_dependency: Mapping[str, Any] | None,
|
||||
) -> None:
|
||||
expected = _registration_payload(
|
||||
executor_id=payload.get("executor_id"), nonce=payload.get("nonce"),
|
||||
issued_at=payload.get("issued_at"), expires_at=payload.get("expires_at"),
|
||||
contract_payload=contract_payload, contract_digest=contract_digest,
|
||||
provenance=provenance, issuer_dependency=issuer_dependency,
|
||||
)
|
||||
if set(payload) != set(expected) or any(
|
||||
payload.get(key) != value for key, value in expected.items()
|
||||
):
|
||||
_fail("EXECUTOR_REGISTRATION_BINDING_INVALID")
|
||||
if (
|
||||
not isinstance(payload["executor_id"], str)
|
||||
or not payload["executor_id"]
|
||||
or not isinstance(payload["nonce"], str)
|
||||
or NONCE.fullmatch(payload["nonce"]) is None
|
||||
):
|
||||
_fail("EXECUTOR_REGISTRATION_IDENTITY_INVALID")
|
||||
if (
|
||||
type(payload["issued_at"]) is not int
|
||||
or type(payload["expires_at"]) is not int
|
||||
or payload["issued_at"] > now
|
||||
or now >= payload["expires_at"]
|
||||
or not 1 <= payload["expires_at"] - payload["issued_at"] <= MAX_TTL_SECONDS
|
||||
or payload["issued_at"] < contract_payload["issued_at"]
|
||||
or payload["expires_at"] > contract_payload["expires_at"]
|
||||
):
|
||||
_fail("EXECUTOR_REGISTRATION_EXPIRED")
|
||||
if payload["support_commit"] != payload["integration_commit"]:
|
||||
_fail("EXECUTOR_COMMIT_BINDING_INVALID")
|
||||
if (
|
||||
issuer_dependency is not None
|
||||
and payload["expires_at"] > issuer_dependency["contract_expires_at"]
|
||||
):
|
||||
_fail("EXECUTOR_REGISTRATION_EXPIRED")
|
||||
if (
|
||||
not isinstance(payload["environment"], str)
|
||||
or SCOPE_ID.fullmatch(payload["environment"]) is None
|
||||
or not isinstance(payload["instance_id"], str)
|
||||
or SCOPE_ID.fullmatch(payload["instance_id"]) is None
|
||||
):
|
||||
_fail("EXECUTOR_ENVIRONMENT_BINDING_INVALID")
|
||||
if payload["source_commit"] != R14_SOURCE_COMMIT:
|
||||
_fail("EXECUTOR_SOURCE_COMMIT_INVALID")
|
||||
if (
|
||||
contract_payload["schema"] != SUCCESSOR_SCHEMA
|
||||
or contract_payload["source_commit"] != R14_SOURCE_COMMIT
|
||||
):
|
||||
_fail("EXECUTOR_CONTRACT_GENERATION_INVALID")
|
||||
effects = TARGET_EFFECTS.get(payload["target"])
|
||||
if (
|
||||
effects is None
|
||||
or not isinstance(payload["effects"], list)
|
||||
or frozenset(payload["effects"]) != effects
|
||||
or len(payload["effects"]) != len(effects)
|
||||
):
|
||||
_fail("EXECUTOR_EFFECTS_INVALID")
|
||||
if (
|
||||
not isinstance(payload["capabilities"], list)
|
||||
or frozenset(payload["capabilities"]) != CAPABILITIES
|
||||
or len(payload["capabilities"]) != len(CAPABILITIES)
|
||||
):
|
||||
_fail("EXECUTOR_CAPABILITIES_INVALID")
|
||||
_validate_issuer_dependency(issuer_dependency, contract_payload)
|
||||
for name in (
|
||||
"installer_module_sha256", "installer_script_sha256",
|
||||
"executor_module_sha256", "authority_manifest_sha256",
|
||||
"execution_digest", "contract_digest", "old_snapshot_sha256",
|
||||
"new_manifest_sha256", "rollback_sha256",
|
||||
):
|
||||
_require_digest(payload[name], HEX64, f"EXECUTOR_{name.upper()}_INVALID")
|
||||
|
||||
|
||||
def _verified_contract(
|
||||
contract: Path, installer: Installer, *, now: int,
|
||||
) -> tuple[dict[str, Any], str]:
|
||||
return verify_signed_contract(
|
||||
contract, installer.registry_path, now=now,
|
||||
require_root_owner=installer.require_root_owner,
|
||||
_layout=installer.layout,
|
||||
)
|
||||
|
||||
|
||||
def _verified_issuer_dependency(
|
||||
issuer_contract: Path | None, installer: Installer, *, now: int,
|
||||
contract_payload: Mapping[str, Any],
|
||||
) -> dict[str, Any] | None:
|
||||
if contract_payload["target"] == ISSUER_TARGET:
|
||||
if issuer_contract is not None:
|
||||
_fail("EXECUTOR_ISSUER_DEPENDENCY_INVALID")
|
||||
return None
|
||||
if issuer_contract is None:
|
||||
_fail("EXECUTOR_ISSUER_DEPENDENCY_REQUIRED")
|
||||
payload, digest = _verified_contract(issuer_contract, installer, now=now)
|
||||
if payload["schema"] != SUCCESSOR_SCHEMA or payload["source_commit"] != R14_SOURCE_COMMIT:
|
||||
_fail("EXECUTOR_ISSUER_DEPENDENCY_INVALID")
|
||||
dependency = _issuer_dependency(payload, digest)
|
||||
_validate_issuer_dependency(dependency, contract_payload)
|
||||
return dependency
|
||||
|
||||
|
||||
def issue_registration(
|
||||
*, executor_id: str, nonce: str, ttl_seconds: int, contract: Path,
|
||||
installer: Installer, provenance_manifest: Path,
|
||||
registered_signer: RegisteredOwnerSignerAdapter,
|
||||
clock: Callable[[], int], issuer_contract: Path | None = None,
|
||||
require_root_owner: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
"""Issue authorization after deriving every scope field from trusted input."""
|
||||
if type(installer) is not Installer:
|
||||
_fail("EXECUTOR_INSTALLER_TYPE_INVALID")
|
||||
if not isinstance(registered_signer, RegisteredOwnerSignerAdapter):
|
||||
_fail("REGISTERED_SIGNER_ADAPTER_REQUIRED")
|
||||
if type(ttl_seconds) is not int or not 1 <= ttl_seconds <= MAX_TTL_SECONDS:
|
||||
_fail("EXECUTOR_REGISTRATION_TTL_INVALID")
|
||||
now = int(clock())
|
||||
provenance = load_code_provenance(
|
||||
provenance_manifest, require_root_owner=require_root_owner,
|
||||
)
|
||||
contract_payload, contract_digest = _verified_contract(contract, installer, now=now)
|
||||
dependency = _verified_issuer_dependency(
|
||||
issuer_contract, installer, now=now, contract_payload=contract_payload,
|
||||
)
|
||||
payload = _registration_payload(
|
||||
executor_id=executor_id, nonce=nonce, issued_at=now,
|
||||
expires_at=now + ttl_seconds, contract_payload=contract_payload,
|
||||
contract_digest=contract_digest, provenance=provenance,
|
||||
issuer_dependency=dependency,
|
||||
)
|
||||
_validate_payload(
|
||||
payload, now=now, provenance=provenance,
|
||||
contract_payload=contract_payload, contract_digest=contract_digest,
|
||||
issuer_dependency=dependency,
|
||||
)
|
||||
if registered_signer.fingerprint != payload["owner_key_id"]:
|
||||
_fail("EXECUTOR_SIGNER_BINDING_INVALID")
|
||||
signature = registered_signer._sign_canonical_payload(
|
||||
OWNER_NAMESPACE, canonical_bytes(payload),
|
||||
)
|
||||
return {"payload": payload, "signature": signature}
|
||||
|
||||
|
||||
def load_registration(path: Path, *, require_root_owner: bool = True) -> dict[str, Any]:
|
||||
raw = _secure_bytes(
|
||||
Path(path), require_root_owner=require_root_owner, maximum=1024 * 1024,
|
||||
)
|
||||
try:
|
||||
value = json.loads(raw, object_pairs_hook=_strict_object)
|
||||
except (UnicodeError, json.JSONDecodeError) as exc:
|
||||
raise ExecutorRegistrationError("EXECUTOR_REGISTRATION_INVALID") from exc
|
||||
if not isinstance(value, dict) or set(value) != {"payload", "signature"}:
|
||||
_fail("EXECUTOR_REGISTRATION_FIELDS_INVALID")
|
||||
return value
|
||||
|
||||
|
||||
class RegisteredInstallerExecutor:
|
||||
def __init__(
|
||||
self, *, owner_verifier: RegistryOwnerVerifier, installer: Installer,
|
||||
provenance_manifest: Path, clock: Callable[[], int],
|
||||
host: Callable[[], str] = socket.gethostname,
|
||||
require_root_owner: bool = True,
|
||||
) -> None:
|
||||
if type(installer) is not Installer:
|
||||
_fail("EXECUTOR_INSTALLER_TYPE_INVALID")
|
||||
if not isinstance(owner_verifier, RegistryOwnerVerifier):
|
||||
_fail("EXECUTOR_OWNER_VERIFIER_INVALID")
|
||||
self.owner_verifier = owner_verifier
|
||||
self.installer = installer
|
||||
self.provenance_manifest = Path(provenance_manifest)
|
||||
self.clock = clock
|
||||
self.host = host
|
||||
self.require_root_owner = require_root_owner
|
||||
self.provenance = load_code_provenance(
|
||||
self.provenance_manifest, require_root_owner=require_root_owner,
|
||||
)
|
||||
|
||||
def _check_code_provenance(self) -> None:
|
||||
current = load_code_provenance(
|
||||
self.provenance_manifest, require_root_owner=self.require_root_owner,
|
||||
)
|
||||
if current != self.provenance:
|
||||
_fail("EXECUTOR_CODE_PROVENANCE_CHANGED")
|
||||
|
||||
def _verify(
|
||||
self, registration: Mapping[str, Any], contract: Path,
|
||||
) -> tuple[dict[str, Any], str, dict[str, Any]]:
|
||||
self._check_code_provenance()
|
||||
if (
|
||||
not isinstance(registration, Mapping)
|
||||
or set(registration) != {"payload", "signature"}
|
||||
or not isinstance(registration["payload"], Mapping)
|
||||
):
|
||||
_fail("EXECUTOR_REGISTRATION_FIELDS_INVALID")
|
||||
if self.host() != HOST:
|
||||
_fail("EXECUTOR_HOST_MISMATCH")
|
||||
now = int(self.clock())
|
||||
contract_payload, contract_digest = _verified_contract(
|
||||
contract, self.installer, now=now,
|
||||
)
|
||||
payload = dict(registration["payload"])
|
||||
dependency = payload.get("issuer_dependency")
|
||||
_validate_payload(
|
||||
payload, now=now, provenance=self.provenance,
|
||||
contract_payload=contract_payload, contract_digest=contract_digest,
|
||||
issuer_dependency=dependency,
|
||||
)
|
||||
if payload["owner_key_id"] != self.owner_verifier.record.fingerprint:
|
||||
_fail("EXECUTOR_OWNER_KEY_BINDING_INVALID")
|
||||
signature = registration["signature"]
|
||||
if (
|
||||
not isinstance(signature, str)
|
||||
or not self.owner_verifier.verify(
|
||||
namespace=OWNER_NAMESPACE, payload=canonical_bytes(payload),
|
||||
signature=signature, key_id=payload["owner_key_id"],
|
||||
)
|
||||
):
|
||||
_fail("EXECUTOR_OWNER_SIGNATURE_INVALID")
|
||||
return payload, contract_digest, contract_payload
|
||||
|
||||
def dispatch(
|
||||
self, registration: Mapping[str, Any], contract: Path,
|
||||
artifact: Path, operation: str, *,
|
||||
issuer_contract: Path | None = None,
|
||||
issuer_artifact: Path | None = None,
|
||||
) -> dict[str, Any]:
|
||||
payload, contract_digest, contract_payload = self._verify(registration, contract)
|
||||
capability = "install" if operation in {"preflight", "install"} else operation
|
||||
if capability not in CAPABILITIES:
|
||||
_fail("EXECUTOR_OPERATION_INVALID")
|
||||
if capability not in payload["capabilities"]:
|
||||
_fail("EXECUTOR_CAPABILITY_INVALID")
|
||||
if operation == "rollback" and contract_payload["target"] == BRIDGE_TARGET:
|
||||
if issuer_artifact is None:
|
||||
_fail("EXECUTOR_ISSUER_DEPENDENCY_REQUIRED")
|
||||
actual_dependency = _verified_issuer_dependency(
|
||||
issuer_contract, self.installer, now=int(self.clock()),
|
||||
contract_payload=contract_payload,
|
||||
)
|
||||
if actual_dependency != payload["issuer_dependency"]:
|
||||
_fail("EXECUTOR_ISSUER_DEPENDENCY_MISMATCH")
|
||||
elif issuer_contract is not None or issuer_artifact is not None:
|
||||
_fail("EXECUTOR_ISSUER_DEPENDENCY_UNEXPECTED")
|
||||
if operation == "preflight":
|
||||
result = self.installer.preflight(contract, artifact)
|
||||
elif operation == "install":
|
||||
result = self.installer.apply(contract, artifact)
|
||||
elif operation == "rollback":
|
||||
result = self.installer.rollback_applied(
|
||||
contract, artifact, issuer_contract=issuer_contract,
|
||||
issuer_artifact=issuer_artifact,
|
||||
)
|
||||
else:
|
||||
result = self.installer.reconcile_unknown(contract, artifact)
|
||||
return {
|
||||
**result,
|
||||
"operation": operation,
|
||||
"executor_id": payload["executor_id"],
|
||||
"contract_digest": contract_digest,
|
||||
"code_binding": {
|
||||
"integration_commit": payload["integration_commit"],
|
||||
"integration_tree": payload["integration_tree"],
|
||||
"installer_module_sha256": payload["installer_module_sha256"],
|
||||
"installer_script_sha256": payload["installer_script_sha256"],
|
||||
"executor_module_sha256": payload["executor_module_sha256"],
|
||||
},
|
||||
"registration_policy_digest": _sha256_bytes(canonical_bytes(payload)),
|
||||
}
|
||||
|
||||
|
||||
def _parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Run one Owner-registered fixed-scope C5 installer operation.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"operation", choices=("preflight", "install", "rollback", "reconcile"),
|
||||
)
|
||||
parser.add_argument("--registration", type=Path, required=True)
|
||||
parser.add_argument("--contract", type=Path, required=True)
|
||||
parser.add_argument("--artifact", type=Path, required=True)
|
||||
parser.add_argument("--issuer-contract", type=Path)
|
||||
parser.add_argument("--issuer-artifact", type=Path)
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
"""Production CLI with fixed registry and provenance authority paths."""
|
||||
args = _parser().parse_args(argv)
|
||||
try:
|
||||
registration = load_registration(args.registration)
|
||||
payload = registration.get("payload")
|
||||
if not isinstance(payload, Mapping) or not isinstance(payload.get("owner_signer_id"), str):
|
||||
_fail("EXECUTOR_REGISTRATION_FIELDS_INVALID")
|
||||
installer = Installer(DEFAULT_REGISTRY)
|
||||
verifier = RegistryOwnerVerifier(
|
||||
DEFAULT_REGISTRY, owner_signer_id=payload["owner_signer_id"],
|
||||
)
|
||||
executor = RegisteredInstallerExecutor(
|
||||
owner_verifier=verifier, installer=installer,
|
||||
provenance_manifest=DEFAULT_PROVENANCE_MANIFEST,
|
||||
clock=lambda: int(time.time()),
|
||||
)
|
||||
result = executor.dispatch(
|
||||
registration, args.contract, args.artifact, args.operation,
|
||||
issuer_contract=args.issuer_contract,
|
||||
issuer_artifact=args.issuer_artifact,
|
||||
)
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
return 0
|
||||
except InstallationOutcomeUnknown as exc:
|
||||
print(
|
||||
json.dumps({"status": "UNKNOWN", "error": exc.code}, sort_keys=True),
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 3
|
||||
except (ExecutorRegistrationError, InstallationError, OSError, ValueError, TypeError) as exc:
|
||||
print(
|
||||
json.dumps(
|
||||
{"status": "REJECTED", "error": getattr(exc, "code", str(exc))},
|
||||
sort_keys=True,
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,977 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Root-owned one-shot assembly for the exact V2R2C5 release Saga.
|
||||
|
||||
This module only assembles existing hardened Saga phases. Each mutating phase
|
||||
is a SHA-pinned process adapter; no caller-provided Python callable is accepted.
|
||||
The staging HTTP service remains shadow-only and does not import this launcher.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from dataclasses import dataclass
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import stat
|
||||
import subprocess
|
||||
import time
|
||||
from typing import Any, Mapping, Sequence
|
||||
|
||||
import approved_release_executor as approved_executor_module
|
||||
from approved_release_executor import run_adapter
|
||||
import c5_exact_deployment_adapter as exact_deployment_module
|
||||
from c5_exact_deployment_adapter import (
|
||||
C4_ROLLBACK_ARTIFACT_PATH,
|
||||
C4_ROLLBACK_TARGET,
|
||||
C5ExactDeploymentAdapter,
|
||||
C5_PRODUCTION_ENVIRONMENT_FILE,
|
||||
C5_PRODUCTION_PORT,
|
||||
C5_PRODUCTION_SERVICE,
|
||||
C5_VERSION,
|
||||
RegisteredC5DeploymentExecutor,
|
||||
)
|
||||
from release_source_admission import open_source_admission_store
|
||||
from silent_release_controller import ReleaseCandidate, ReleaseCandidateStore
|
||||
from silent_release_hardening import (
|
||||
ApprovalKeyRing,
|
||||
ApprovalRevocationStore,
|
||||
BaselineController,
|
||||
CapabilityAuthorizationError,
|
||||
CapabilityLedger,
|
||||
ClaimProbeResult,
|
||||
GovernedFinalizationCoordinator,
|
||||
HardenedTagController,
|
||||
PreSwitchValidator,
|
||||
RegistryController,
|
||||
ReleaseOrchestrator,
|
||||
RecoverableReleaseError,
|
||||
ReleasePlan,
|
||||
ReleasePlanStore,
|
||||
ReleaseSagaStore,
|
||||
SignedApprovalStore,
|
||||
SignedApprovalVerifier,
|
||||
TagPlan,
|
||||
TagPublicationConflict,
|
||||
TagPublicationProbeEvidence,
|
||||
TrustedReleasePolicy,
|
||||
)
|
||||
|
||||
|
||||
SCHEMA = "aios.c5.release-execution-host.v1"
|
||||
PROVENANCE_SCHEMA = "aios.c5.release-execution-provenance.v1"
|
||||
REQUEST_SCHEMA = "aios.production-executor-request.v1"
|
||||
REPOSITORY = "aios/aios"
|
||||
SCOPES = frozenset({"PRODUCTION", "ISOLATED_REHEARSAL"})
|
||||
SHA256 = __import__("re").compile(r"^[0-9a-f]{64}$")
|
||||
OID = __import__("re").compile(r"^[0-9a-f]{40}$")
|
||||
|
||||
|
||||
class C5ExecutionHostBlocked(RuntimeError):
|
||||
"""Exact host assembly or phase verification failed closed."""
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as source:
|
||||
for chunk in iter(lambda: source.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _secure_file(
|
||||
path: Path,
|
||||
expected_sha256: str,
|
||||
*,
|
||||
expected_owner_uid: int | None,
|
||||
) -> Path:
|
||||
path = Path(path)
|
||||
if (
|
||||
not path.is_absolute()
|
||||
or path.is_symlink()
|
||||
or path.resolve(strict=False) != path
|
||||
):
|
||||
raise C5ExecutionHostBlocked("HOST_FILE_PATH_INVALID")
|
||||
try:
|
||||
info = path.stat()
|
||||
except OSError as exc:
|
||||
raise C5ExecutionHostBlocked("HOST_FILE_UNAVAILABLE") from exc
|
||||
if not stat.S_ISREG(info.st_mode) or (
|
||||
os.name != "nt" and stat.S_IMODE(info.st_mode) & 0o022
|
||||
):
|
||||
raise C5ExecutionHostBlocked("HOST_FILE_SECURITY_INVALID")
|
||||
if expected_owner_uid is not None and info.st_uid != expected_owner_uid:
|
||||
raise C5ExecutionHostBlocked("HOST_FILE_OWNER_INVALID")
|
||||
if expected_owner_uid is not None:
|
||||
parent = path.parent
|
||||
while True:
|
||||
parent_info = parent.stat()
|
||||
if (
|
||||
parent.is_symlink()
|
||||
or parent_info.st_uid != expected_owner_uid
|
||||
or stat.S_IMODE(parent_info.st_mode) & 0o022
|
||||
):
|
||||
raise C5ExecutionHostBlocked("HOST_FILE_PARENT_SECURITY_INVALID")
|
||||
if parent == parent.parent:
|
||||
break
|
||||
parent = parent.parent
|
||||
if not SHA256.fullmatch(expected_sha256) or _sha256(path) != expected_sha256:
|
||||
raise C5ExecutionHostBlocked("HOST_FILE_DIGEST_MISMATCH")
|
||||
return path
|
||||
|
||||
|
||||
def _absolute(value: Any, name: str) -> Path:
|
||||
path = Path(str(value or ""))
|
||||
if not path.is_absolute():
|
||||
raise C5ExecutionHostBlocked(f"{name}_PATH_INVALID")
|
||||
return path
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RegisteredPhaseCommand:
|
||||
executable: Path
|
||||
executable_sha256: str
|
||||
adapter_path: Path
|
||||
adapter_sha256: str
|
||||
arguments: tuple[str, ...] = ()
|
||||
|
||||
@classmethod
|
||||
def from_mapping(cls, raw: Any) -> "RegisteredPhaseCommand":
|
||||
if not isinstance(raw, Mapping) or set(raw) != {
|
||||
"executable",
|
||||
"executable_sha256",
|
||||
"adapter_path",
|
||||
"adapter_sha256",
|
||||
"arguments",
|
||||
}:
|
||||
raise C5ExecutionHostBlocked("PHASE_COMMAND_SCHEMA_INVALID")
|
||||
arguments = raw["arguments"]
|
||||
if not isinstance(arguments, list) or not all(
|
||||
isinstance(item, str) and item and "\0" not in item
|
||||
for item in arguments
|
||||
):
|
||||
raise C5ExecutionHostBlocked("PHASE_COMMAND_ARGUMENTS_INVALID")
|
||||
return cls(
|
||||
executable=_absolute(raw["executable"], "PHASE_EXECUTABLE"),
|
||||
executable_sha256=str(raw["executable_sha256"]),
|
||||
adapter_path=_absolute(raw["adapter_path"], "PHASE_ADAPTER"),
|
||||
adapter_sha256=str(raw["adapter_sha256"]),
|
||||
arguments=tuple(arguments),
|
||||
)
|
||||
|
||||
def verify(self, *, expected_owner_uid: int | None) -> None:
|
||||
_secure_file(
|
||||
self.executable,
|
||||
self.executable_sha256,
|
||||
expected_owner_uid=expected_owner_uid,
|
||||
)
|
||||
_secure_file(
|
||||
self.adapter_path,
|
||||
self.adapter_sha256,
|
||||
expected_owner_uid=expected_owner_uid,
|
||||
)
|
||||
|
||||
@property
|
||||
def command(self) -> tuple[str, ...]:
|
||||
return (str(self.executable), str(self.adapter_path), *self.arguments)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class C5HostProvenance:
|
||||
environment: str
|
||||
instance_id: str
|
||||
authority_commit: str
|
||||
authority_tree: str
|
||||
entries: Mapping[str, tuple[Path, str]]
|
||||
manifest_sha256: str
|
||||
|
||||
@classmethod
|
||||
def load(
|
||||
cls,
|
||||
path: Path,
|
||||
*,
|
||||
expected_sha256: str,
|
||||
expected_owner_uid: int | None = 0,
|
||||
) -> "C5HostProvenance":
|
||||
path = _secure_file(
|
||||
Path(path), expected_sha256,
|
||||
expected_owner_uid=expected_owner_uid,
|
||||
)
|
||||
try:
|
||||
raw = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError) as exc:
|
||||
raise C5ExecutionHostBlocked("HOST_PROVENANCE_INVALID") from exc
|
||||
labels = {
|
||||
"host", "exact_deployment", "deployment", "approved_executor",
|
||||
"tag", "registry", "baseline",
|
||||
}
|
||||
if (
|
||||
not isinstance(raw, Mapping)
|
||||
or set(raw) != {
|
||||
"schema", "environment", "instance_id",
|
||||
"authority_commit", "authority_tree", "entries",
|
||||
}
|
||||
or raw["schema"] != PROVENANCE_SCHEMA
|
||||
or raw["environment"] not in SCOPES
|
||||
or not isinstance(raw["entries"], Mapping)
|
||||
or set(raw["entries"]) != labels
|
||||
):
|
||||
raise C5ExecutionHostBlocked("HOST_PROVENANCE_INVALID")
|
||||
instance_id = str(raw["instance_id"] or "")
|
||||
if not instance_id or len(instance_id) > 128:
|
||||
raise C5ExecutionHostBlocked("HOST_PROVENANCE_INSTANCE_INVALID")
|
||||
authority_commit = str(raw["authority_commit"])
|
||||
authority_tree = str(raw["authority_tree"])
|
||||
if not OID.fullmatch(authority_commit) or not OID.fullmatch(authority_tree):
|
||||
raise C5ExecutionHostBlocked("HOST_PROVENANCE_AUTHORITY_INVALID")
|
||||
actual_loaded = {
|
||||
"host": Path(__file__).resolve(strict=True),
|
||||
"exact_deployment": Path(exact_deployment_module.__file__).resolve(strict=True),
|
||||
"approved_executor": Path(approved_executor_module.__file__).resolve(strict=True),
|
||||
}
|
||||
entries: dict[str, tuple[Path, str]] = {}
|
||||
for label, item in raw["entries"].items():
|
||||
if not isinstance(item, Mapping) or set(item) != {"path", "sha256"}:
|
||||
raise C5ExecutionHostBlocked("HOST_PROVENANCE_ENTRY_INVALID")
|
||||
entry_path = _absolute(item["path"], "HOST_PROVENANCE_ENTRY")
|
||||
digest = str(item["sha256"])
|
||||
_secure_file(
|
||||
entry_path, digest, expected_owner_uid=expected_owner_uid
|
||||
)
|
||||
if label in actual_loaded and entry_path != actual_loaded[label]:
|
||||
raise C5ExecutionHostBlocked("HOST_PROVENANCE_LOADED_PATH_MISMATCH")
|
||||
entries[label] = (entry_path, digest)
|
||||
return cls(
|
||||
environment=str(raw["environment"]),
|
||||
instance_id=instance_id,
|
||||
authority_commit=authority_commit,
|
||||
authority_tree=authority_tree,
|
||||
entries=entries,
|
||||
manifest_sha256=expected_sha256,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class C5ExecutionHostConfig:
|
||||
host_provenance: C5HostProvenance
|
||||
executor_authority_commit: str
|
||||
executor_authority_tree: str
|
||||
execution_scope: str
|
||||
instance_id: str
|
||||
provenance: Mapping[str, Any]
|
||||
candidate_id: str
|
||||
source_commit: str
|
||||
tree_oid: str
|
||||
artifact_sha256: str
|
||||
release_plan_sha256: str
|
||||
approval_fingerprint: str
|
||||
state_root: Path
|
||||
trusted_policy_path: Path
|
||||
trusted_policy_sha256: str
|
||||
approval_keyring_path: Path
|
||||
approval_keyring_sha256: str
|
||||
source_admission_root: Path
|
||||
source_admission_policy_path: Path
|
||||
source_admission_policy_sha256: str
|
||||
source_admission_task_id: str
|
||||
source_admission_hmac_path: Path
|
||||
source_admission_hmac_sha256: str
|
||||
deployment_executable: Path
|
||||
deployment_executable_sha256: str
|
||||
deployment_adapter_path: Path
|
||||
deployment_adapter_sha256: str
|
||||
artifact_path: Path
|
||||
rollback_target: Mapping[str, str]
|
||||
rollback_artifact_path: Path
|
||||
production_environment_file: Path
|
||||
authoritative_git_dir: Path
|
||||
git_executable: Path
|
||||
git_executable_sha256: str
|
||||
tag_command: RegisteredPhaseCommand
|
||||
registry_command: RegisteredPhaseCommand
|
||||
baseline_command: RegisteredPhaseCommand
|
||||
registry_record_path: Path
|
||||
baseline_record_path: Path
|
||||
|
||||
@classmethod
|
||||
def load(
|
||||
cls,
|
||||
path: Path,
|
||||
*,
|
||||
expected_sha256: str,
|
||||
host_provenance: C5HostProvenance,
|
||||
expected_owner_uid: int | None = 0,
|
||||
) -> "C5ExecutionHostConfig":
|
||||
path = _secure_file(
|
||||
Path(path),
|
||||
expected_sha256,
|
||||
expected_owner_uid=expected_owner_uid,
|
||||
)
|
||||
try:
|
||||
raw = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError) as exc:
|
||||
raise C5ExecutionHostBlocked("HOST_CONFIG_INVALID") from exc
|
||||
keys = {
|
||||
"schema", "version", "execution_scope", "instance_id",
|
||||
"executor_authority_commit", "executor_authority_tree",
|
||||
"provenance", "candidate_id",
|
||||
"source_commit", "tree_oid", "artifact_sha256",
|
||||
"release_plan_sha256", "approval_fingerprint", "state_root",
|
||||
"trusted_policy_path", "trusted_policy_sha256",
|
||||
"approval_keyring_path", "approval_keyring_sha256",
|
||||
"source_admission_root", "source_admission_policy_path",
|
||||
"source_admission_policy_sha256", "source_admission_task_id", "source_admission_hmac_path",
|
||||
"source_admission_hmac_sha256", "deployment_executable",
|
||||
"deployment_executable_sha256", "deployment_adapter_path",
|
||||
"deployment_adapter_sha256", "artifact_path",
|
||||
"rollback_target", "rollback_artifact_path",
|
||||
"production_environment_file", "production_service",
|
||||
"production_port", "repository_identity", "global_legacy_writes_enabled",
|
||||
"authoritative_git_dir", "git_executable", "git_executable_sha256",
|
||||
"tag_command", "registry_command", "baseline_command",
|
||||
"registry_record_path", "baseline_record_path",
|
||||
}
|
||||
if not isinstance(raw, Mapping) or set(raw) != keys:
|
||||
raise C5ExecutionHostBlocked("HOST_CONFIG_SCHEMA_INVALID")
|
||||
rollback_artifact_path = _absolute(raw["rollback_artifact_path"], "ROLLBACK_ARTIFACT")
|
||||
production_environment_file = _absolute(raw["production_environment_file"], "PRODUCTION_ENVIRONMENT_FILE")
|
||||
exact = (
|
||||
raw["schema"] == SCHEMA
|
||||
and raw["version"] == C5_VERSION
|
||||
and raw["execution_scope"] in SCOPES
|
||||
and isinstance(raw["rollback_target"], Mapping)
|
||||
and raw["production_service"] == C5_PRODUCTION_SERVICE
|
||||
and raw["production_port"] == C5_PRODUCTION_PORT
|
||||
and raw["repository_identity"] == REPOSITORY
|
||||
and raw["global_legacy_writes_enabled"] is False
|
||||
)
|
||||
if not exact:
|
||||
raise C5ExecutionHostBlocked("HOST_CONFIG_BINDING_INVALID")
|
||||
provenance = raw["provenance"]
|
||||
expected_provenance = (
|
||||
{
|
||||
"environment": "ISOLATED_REHEARSAL",
|
||||
"fixture": True,
|
||||
"r14_source_receipt": False,
|
||||
}
|
||||
if raw["execution_scope"] == "ISOLATED_REHEARSAL"
|
||||
else {
|
||||
"environment": "PRODUCTION",
|
||||
"fixture": False,
|
||||
"r14_source_receipt": True,
|
||||
}
|
||||
)
|
||||
if provenance != expected_provenance:
|
||||
raise C5ExecutionHostBlocked("HOST_PROVENANCE_INVALID")
|
||||
instance_id = str(raw["instance_id"] or "")
|
||||
if not instance_id or len(instance_id) > 128 or any(
|
||||
character not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.-"
|
||||
for character in instance_id
|
||||
):
|
||||
raise C5ExecutionHostBlocked("HOST_INSTANCE_ID_INVALID")
|
||||
for name, pattern in (
|
||||
("executor_authority_commit", OID),
|
||||
("executor_authority_tree", OID),
|
||||
("source_commit", OID), ("tree_oid", OID),
|
||||
("artifact_sha256", SHA256),
|
||||
("release_plan_sha256", SHA256),
|
||||
("approval_fingerprint", SHA256),
|
||||
):
|
||||
if pattern.fullmatch(str(raw[name])) is None:
|
||||
raise C5ExecutionHostBlocked(f"{name.upper()}_INVALID")
|
||||
value = cls(
|
||||
host_provenance=host_provenance,
|
||||
executor_authority_commit=str(raw["executor_authority_commit"]),
|
||||
executor_authority_tree=str(raw["executor_authority_tree"]),
|
||||
execution_scope=str(raw["execution_scope"]),
|
||||
instance_id=instance_id,
|
||||
provenance=dict(provenance),
|
||||
candidate_id=str(raw["candidate_id"]),
|
||||
source_commit=str(raw["source_commit"]),
|
||||
tree_oid=str(raw["tree_oid"]),
|
||||
artifact_sha256=str(raw["artifact_sha256"]),
|
||||
release_plan_sha256=str(raw["release_plan_sha256"]),
|
||||
approval_fingerprint=str(raw["approval_fingerprint"]),
|
||||
state_root=_absolute(raw["state_root"], "STATE_ROOT"),
|
||||
trusted_policy_path=_absolute(raw["trusted_policy_path"], "TRUSTED_POLICY"),
|
||||
trusted_policy_sha256=str(raw["trusted_policy_sha256"]),
|
||||
approval_keyring_path=_absolute(raw["approval_keyring_path"], "APPROVAL_KEYRING"),
|
||||
approval_keyring_sha256=str(raw["approval_keyring_sha256"]),
|
||||
source_admission_root=_absolute(raw["source_admission_root"], "SOURCE_ADMISSION_ROOT"),
|
||||
source_admission_policy_path=_absolute(raw["source_admission_policy_path"], "SOURCE_ADMISSION_POLICY"),
|
||||
source_admission_policy_sha256=str(raw["source_admission_policy_sha256"]),
|
||||
source_admission_task_id=str(raw["source_admission_task_id"]),
|
||||
source_admission_hmac_path=_absolute(raw["source_admission_hmac_path"], "SOURCE_ADMISSION_HMAC"),
|
||||
source_admission_hmac_sha256=str(raw["source_admission_hmac_sha256"]),
|
||||
deployment_executable=_absolute(raw["deployment_executable"], "DEPLOYMENT_EXECUTABLE"),
|
||||
deployment_executable_sha256=str(raw["deployment_executable_sha256"]),
|
||||
deployment_adapter_path=_absolute(raw["deployment_adapter_path"], "DEPLOYMENT_ADAPTER"),
|
||||
deployment_adapter_sha256=str(raw["deployment_adapter_sha256"]),
|
||||
artifact_path=_absolute(raw["artifact_path"], "ARTIFACT"),
|
||||
rollback_target=dict(raw["rollback_target"]),
|
||||
rollback_artifact_path=rollback_artifact_path,
|
||||
production_environment_file=production_environment_file,
|
||||
authoritative_git_dir=_absolute(raw["authoritative_git_dir"], "AUTHORITATIVE_GIT_DIR"),
|
||||
git_executable=_absolute(raw["git_executable"], "GIT_EXECUTABLE"),
|
||||
git_executable_sha256=str(raw["git_executable_sha256"]),
|
||||
tag_command=RegisteredPhaseCommand.from_mapping(raw["tag_command"]),
|
||||
registry_command=RegisteredPhaseCommand.from_mapping(raw["registry_command"]),
|
||||
baseline_command=RegisteredPhaseCommand.from_mapping(raw["baseline_command"]),
|
||||
registry_record_path=_absolute(raw["registry_record_path"], "REGISTRY_RECORD"),
|
||||
baseline_record_path=_absolute(raw["baseline_record_path"], "BASELINE_RECORD"),
|
||||
)
|
||||
if (not value.source_admission_task_id
|
||||
or len(value.source_admission_task_id) > 160
|
||||
or any(character not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.:-"
|
||||
for character in value.source_admission_task_id)):
|
||||
raise C5ExecutionHostBlocked("SOURCE_ADMISSION_TASK_ID_INVALID")
|
||||
if value.execution_scope == "ISOLATED_REHEARSAL":
|
||||
rehearsal_root = value.state_root.resolve(strict=False)
|
||||
lowered_parts = {part.lower() for part in rehearsal_root.parts}
|
||||
if (
|
||||
"isolated-rehearsal" not in lowered_parts
|
||||
or value.instance_id not in rehearsal_root.parts
|
||||
):
|
||||
raise C5ExecutionHostBlocked("REHEARSAL_ROOT_NOT_ISOLATED")
|
||||
for fixture_path in (
|
||||
value.source_admission_root,
|
||||
value.artifact_path,
|
||||
value.rollback_artifact_path,
|
||||
value.production_environment_file,
|
||||
value.registry_record_path,
|
||||
value.baseline_record_path,
|
||||
):
|
||||
try:
|
||||
fixture_path.resolve(strict=False).relative_to(rehearsal_root)
|
||||
except ValueError as exc:
|
||||
raise C5ExecutionHostBlocked(
|
||||
"REHEARSAL_FIXTURE_OUTSIDE_ISOLATION_ROOT"
|
||||
) from exc
|
||||
if value.execution_scope == "ISOLATED_REHEARSAL":
|
||||
if set(value.rollback_target) != set(C4_ROLLBACK_TARGET) or value.rollback_target.get("version") != C4_ROLLBACK_TARGET["version"] or value.rollback_target.get("target_oid") != C4_ROLLBACK_TARGET["target_oid"]:
|
||||
raise C5ExecutionHostBlocked("REHEARSAL_ROLLBACK_TARGET_INVALID")
|
||||
if SHA256.fullmatch(str(value.rollback_target.get("artifact_sha256", ""))) is None:
|
||||
raise C5ExecutionHostBlocked("REHEARSAL_ROLLBACK_TARGET_INVALID")
|
||||
if (
|
||||
host_provenance.environment != value.execution_scope
|
||||
or host_provenance.instance_id != value.instance_id
|
||||
or host_provenance.authority_commit
|
||||
!= value.executor_authority_commit
|
||||
or host_provenance.authority_tree != value.executor_authority_tree
|
||||
):
|
||||
raise C5ExecutionHostBlocked("HOST_PROVENANCE_BINDING_MISMATCH")
|
||||
if value.execution_scope == "PRODUCTION" and (
|
||||
value.rollback_target != C4_ROLLBACK_TARGET
|
||||
or value.rollback_artifact_path != C4_ROLLBACK_ARTIFACT_PATH
|
||||
or value.production_environment_file != C5_PRODUCTION_ENVIRONMENT_FILE
|
||||
):
|
||||
raise C5ExecutionHostBlocked("HOST_CONFIG_BINDING_INVALID")
|
||||
expected_adapter_paths = {
|
||||
"deployment": value.deployment_adapter_path,
|
||||
"tag": value.tag_command.adapter_path,
|
||||
"registry": value.registry_command.adapter_path,
|
||||
"baseline": value.baseline_command.adapter_path,
|
||||
}
|
||||
if any(
|
||||
host_provenance.entries[label][0] != path
|
||||
for label, path in expected_adapter_paths.items()
|
||||
):
|
||||
raise C5ExecutionHostBlocked("HOST_PROVENANCE_ADAPTER_MISMATCH")
|
||||
production_names = {
|
||||
"deployment": "c5_production_deployment_adapter.py",
|
||||
"tag": "governed_tag_adapter.py",
|
||||
"registry": "release_metadata_adapter.py",
|
||||
"baseline": "release_metadata_adapter.py",
|
||||
}
|
||||
observed_names = {
|
||||
label: path.name for label, path in expected_adapter_paths.items()
|
||||
}
|
||||
if value.execution_scope == "PRODUCTION":
|
||||
if observed_names != production_names:
|
||||
raise C5ExecutionHostBlocked("PRODUCTION_ADAPTER_IDENTITY_INVALID")
|
||||
elif any(
|
||||
observed_names[label] == production_names[label]
|
||||
for label in production_names
|
||||
):
|
||||
raise C5ExecutionHostBlocked("REHEARSAL_PRODUCTION_ADAPTER_FORBIDDEN")
|
||||
value.verify_files(expected_owner_uid=expected_owner_uid)
|
||||
return value
|
||||
|
||||
def verify_files(self, *, expected_owner_uid: int | None) -> None:
|
||||
for path, digest in (
|
||||
(self.trusted_policy_path, self.trusted_policy_sha256),
|
||||
(self.approval_keyring_path, self.approval_keyring_sha256),
|
||||
(self.source_admission_policy_path, self.source_admission_policy_sha256),
|
||||
(self.source_admission_hmac_path, self.source_admission_hmac_sha256),
|
||||
(self.git_executable, self.git_executable_sha256),
|
||||
):
|
||||
_secure_file(path, digest, expected_owner_uid=expected_owner_uid)
|
||||
for command in (
|
||||
self.tag_command, self.registry_command, self.baseline_command
|
||||
):
|
||||
command.verify(expected_owner_uid=expected_owner_uid)
|
||||
|
||||
|
||||
def _load_policy(path: Path) -> TrustedReleasePolicy:
|
||||
raw = json.loads(path.read_text(encoding="utf-8"))
|
||||
required = {
|
||||
"services", "release_roots", "cutover_links", "dropin_paths",
|
||||
"authoritative_git_dirs", "repository_identities",
|
||||
}
|
||||
if not isinstance(raw, Mapping) or set(raw) != required:
|
||||
raise C5ExecutionHostBlocked("TRUSTED_POLICY_SCHEMA_INVALID")
|
||||
values: dict[str, frozenset[str]] = {}
|
||||
for name in required:
|
||||
items = raw[name]
|
||||
if not isinstance(items, list) or not items or not all(
|
||||
isinstance(item, str) and item for item in items
|
||||
):
|
||||
raise C5ExecutionHostBlocked("TRUSTED_POLICY_VALUES_INVALID")
|
||||
values[name] = frozenset(items)
|
||||
return TrustedReleasePolicy(**values)
|
||||
|
||||
|
||||
def _phase_request(candidate: ReleaseCandidate, plan: ReleasePlan, approval, operation: str) -> dict[str, Any]:
|
||||
return {
|
||||
"schema": REQUEST_SCHEMA,
|
||||
"operation": operation,
|
||||
"version": C5_VERSION,
|
||||
"target_oid": candidate.source_commit,
|
||||
"evidence_closure_oid": approval.evidence_closure_oid,
|
||||
"artifact_sha256": candidate.artifact_sha256,
|
||||
"approval_plan_id": approval.approval_plan_id,
|
||||
"repository_identity": plan.deployment.repository_identity,
|
||||
"production_service": plan.deployment.service,
|
||||
"production_port": C5_PRODUCTION_PORT,
|
||||
}
|
||||
|
||||
|
||||
def _checked_phase_response(
|
||||
value: Mapping[str, Any], request: Mapping[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
for name in (
|
||||
"version", "target_oid", "evidence_closure_oid",
|
||||
"artifact_sha256", "approval_plan_id",
|
||||
):
|
||||
if value.get(name) != request[name]:
|
||||
raise C5ExecutionHostBlocked(f"PHASE_RECEIPT_MISMATCH:{name}")
|
||||
return dict(value)
|
||||
|
||||
|
||||
class _PreSwitchProbe:
|
||||
def __init__(self, config: C5ExecutionHostConfig, source_admissions):
|
||||
self.config = config
|
||||
self.source_admissions = source_admissions
|
||||
|
||||
def _git(self, *args: str) -> str:
|
||||
completed = subprocess.run(
|
||||
[str(self.config.git_executable), f"--git-dir={self.config.authoritative_git_dir}", *args],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=45,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise C5ExecutionHostBlocked("PRE_SWITCH_GIT_UNAVAILABLE")
|
||||
return completed.stdout.strip()
|
||||
|
||||
def collect(self, candidate: ReleaseCandidate, plan) -> Mapping[str, Any]:
|
||||
admitted = self.source_admissions.resolve_recipe(
|
||||
C5_VERSION, candidate.candidate_id, candidate.source_commit
|
||||
)
|
||||
tree = self._git("rev-parse", f"{candidate.source_commit}^{{tree}}")
|
||||
artifact_ok = (
|
||||
self.config.artifact_path.is_file()
|
||||
and _sha256(self.config.artifact_path) == candidate.artifact_sha256
|
||||
)
|
||||
rollback_path = (
|
||||
C4_ROLLBACK_ARTIFACT_PATH
|
||||
if self.config.execution_scope == "PRODUCTION"
|
||||
else self.config.rollback_artifact_path
|
||||
)
|
||||
rollback_target = (
|
||||
C4_ROLLBACK_TARGET
|
||||
if self.config.execution_scope == "PRODUCTION"
|
||||
else self.config.rollback_target
|
||||
)
|
||||
rollback_ok = (
|
||||
rollback_path.is_file()
|
||||
and _sha256(rollback_path) == rollback_target["artifact_sha256"]
|
||||
)
|
||||
return {
|
||||
"source_commit": candidate.source_commit,
|
||||
"tree_oid": tree,
|
||||
"artifact_sha256": candidate.artifact_sha256 if artifact_ok else "",
|
||||
"manifest_sha256": candidate.manifest_sha256,
|
||||
"deployment_plan_sha256": plan.sha256,
|
||||
"policy_sha256": plan.policy_sha256,
|
||||
"rollback_ready": rollback_ok,
|
||||
"git_refs_consistent": (
|
||||
admitted.get("source_commit") == candidate.source_commit
|
||||
and admitted.get("tree_oid") == tree
|
||||
),
|
||||
"repository_identity": plan.repository_identity,
|
||||
"writer_disabled": os.environ.get(
|
||||
"AIOS_GITOPS_WRITES_ENABLED", "false"
|
||||
).lower() == "false",
|
||||
}
|
||||
|
||||
|
||||
class _TagPhase:
|
||||
def __init__(self, command: RegisteredPhaseCommand, candidate, plan, approval, owner_uid):
|
||||
self.command_config = command
|
||||
self.candidate = candidate
|
||||
self.plan = plan
|
||||
self.approval = approval
|
||||
self.owner_uid = owner_uid
|
||||
|
||||
def _call(self, operation: str) -> dict[str, Any]:
|
||||
self.command_config.verify(expected_owner_uid=self.owner_uid)
|
||||
request = _phase_request(self.candidate, self.plan, self.approval, operation)
|
||||
return _checked_phase_response(
|
||||
run_adapter(self.command_config.command, request), request
|
||||
)
|
||||
|
||||
def probe_evidence(self, tag_plan: TagPlan) -> TagPublicationProbeEvidence:
|
||||
value = self._call("probe")
|
||||
status = value.get("status")
|
||||
listing = value.get("gitea_listing_visible")
|
||||
if status == "NOT_APPLIED":
|
||||
listing = False
|
||||
return TagPublicationProbeEvidence(
|
||||
repository_identity=tag_plan.repository_identity,
|
||||
remote_reachable=True,
|
||||
gitea_api_reachable=True,
|
||||
gitea_listing_reachable=True,
|
||||
remote_tag_oid=value.get("remote_tag_oid"),
|
||||
annotated_tag_object_oid=value.get("tag_oid"),
|
||||
remote_peeled_target_oid=value.get("remote_peeled_target_oid"),
|
||||
gitea_api_tag_oid=value.get("gitea_tag_oid"),
|
||||
gitea_api_peeled_target_oid=value.get("gitea_peeled_target_oid"),
|
||||
gitea_listing_visible=listing,
|
||||
gitea_listing_tag_oid=(value.get("gitea_tag_oid") if listing else None),
|
||||
gitea_listing_peeled_target_oid=(value.get("gitea_peeled_target_oid") if listing else None),
|
||||
observed_at=time.time(),
|
||||
)
|
||||
|
||||
def __call__(self, *, tag_plan: TagPlan) -> Mapping[str, Any]:
|
||||
value = self._call("probe")
|
||||
if value.get("status") in {"NOT_APPLIED", "PARTIAL_LOCAL"}:
|
||||
value = self._call("apply")
|
||||
value = self._call("probe")
|
||||
if value.get("status") == "CONFLICT":
|
||||
raise TagPublicationConflict(
|
||||
"tag publication conflicts with the exact C5 plan"
|
||||
)
|
||||
if value.get("status") != "APPLIED_VERIFIED":
|
||||
raise RecoverableReleaseError(
|
||||
"tag publication outcome is indeterminate"
|
||||
)
|
||||
proof = self.probe_evidence(tag_plan)
|
||||
if proof.remote_tag_oid != tag_plan.expected_tag_oid:
|
||||
raise C5ExecutionHostBlocked("TAG_OBJECT_BINDING_MISMATCH")
|
||||
return {
|
||||
"verified": True,
|
||||
"evidence_closure_oid": self.approval.evidence_closure_oid,
|
||||
"approval_plan_id": self.approval.approval_plan_id,
|
||||
"repository_identity": tag_plan.repository_identity,
|
||||
"remote_tag_oid": proof.remote_tag_oid,
|
||||
"remote_peeled_target_oid": proof.remote_peeled_target_oid,
|
||||
"gitea_api_tag_oid": proof.gitea_api_tag_oid,
|
||||
"gitea_api_peeled_target_oid": proof.gitea_api_peeled_target_oid,
|
||||
"gitea_listing_visible": proof.gitea_listing_visible,
|
||||
}
|
||||
|
||||
|
||||
class _MetadataPhase:
|
||||
def __init__(self, *, mode: str, command: RegisteredPhaseCommand, record_path: Path,
|
||||
candidate, plan, approval, tag_phase: _TagPhase, owner_uid):
|
||||
self.mode = mode
|
||||
self.command_config = command
|
||||
self.record_path = record_path
|
||||
self.candidate = candidate
|
||||
self.plan = plan
|
||||
self.approval = approval
|
||||
self.tag_phase = tag_phase
|
||||
self.owner_uid = owner_uid
|
||||
|
||||
def _call(self, operation: str) -> dict[str, Any]:
|
||||
self.command_config.verify(expected_owner_uid=self.owner_uid)
|
||||
request = _phase_request(self.candidate, self.plan, self.approval, operation)
|
||||
request["tag_oid"] = self.plan.tag.expected_tag_oid
|
||||
return _checked_phase_response(run_adapter(self.command_config.command, request), request)
|
||||
|
||||
def __call__(self, *, finalization_plan) -> Mapping[str, Any]:
|
||||
value = self._call("probe")
|
||||
if value.get("status") == "NOT_APPLIED":
|
||||
self._call("apply")
|
||||
value = self._call("probe")
|
||||
if value.get("status") != "VERIFIED":
|
||||
raise C5ExecutionHostBlocked(f"{self.mode.upper()}_FINALIZATION_NOT_VERIFIED")
|
||||
try:
|
||||
record = json.loads(self.record_path.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError) as exc:
|
||||
raise C5ExecutionHostBlocked(f"{self.mode.upper()}_RECORD_INVALID") from exc
|
||||
expected_record = (
|
||||
finalization_plan.registry_expected_record
|
||||
if self.mode == "registry"
|
||||
else finalization_plan.baseline_expected_record
|
||||
)
|
||||
if any(record.get(key) != item for key, item in expected_record.items()):
|
||||
raise C5ExecutionHostBlocked(f"{self.mode.upper()}_RECORD_BINDING_MISMATCH")
|
||||
tag = self.tag_phase.probe_evidence(self.plan.tag)
|
||||
if (
|
||||
tag.remote_tag_oid != self.plan.tag.expected_tag_oid
|
||||
or tag.remote_peeled_target_oid != self.candidate.source_commit
|
||||
or tag.gitea_api_peeled_target_oid != self.candidate.source_commit
|
||||
or tag.gitea_listing_visible is not True
|
||||
):
|
||||
raise C5ExecutionHostBlocked("FINALIZATION_TAG_PROOF_INVALID")
|
||||
proof = {
|
||||
"verified": True,
|
||||
"tag_verified": True,
|
||||
"remote_tag_oid": tag.remote_tag_oid,
|
||||
"remote_peeled_target_oid": tag.remote_peeled_target_oid,
|
||||
"gitea_api_peeled_target_oid": tag.gitea_api_peeled_target_oid,
|
||||
"gitea_listing_visible": True,
|
||||
"evidence_closure_oid": self.approval.evidence_closure_oid,
|
||||
"approval_plan_id": self.approval.approval_plan_id,
|
||||
f"{self.mode}_target_oid": self.candidate.source_commit,
|
||||
}
|
||||
if self.mode == "baseline":
|
||||
proof.update(
|
||||
{
|
||||
"registry_record": finalization_plan.registry_expected_record,
|
||||
"baseline_record": finalization_plan.baseline_expected_record,
|
||||
"baseline_written_last": True,
|
||||
}
|
||||
)
|
||||
return proof
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class C5ExecutionHost:
|
||||
config: C5ExecutionHostConfig
|
||||
orchestrator: ReleaseOrchestrator
|
||||
|
||||
def execute(self) -> dict[str, Any]:
|
||||
result = dict(self.orchestrator.execute(self.config.candidate_id))
|
||||
production_receipt = (
|
||||
self.config.execution_scope == "PRODUCTION"
|
||||
and result.get("state") == "COMPLETED"
|
||||
)
|
||||
return {
|
||||
**result,
|
||||
"execution_scope": self.config.execution_scope,
|
||||
"instance_id": self.config.instance_id,
|
||||
"provenance": dict(self.config.provenance),
|
||||
"host_authority_commit": self.config.host_provenance.authority_commit,
|
||||
"host_authority_tree": self.config.host_provenance.authority_tree,
|
||||
"host_provenance_sha256": self.config.host_provenance.manifest_sha256,
|
||||
"fixture_evidence": self.config.execution_scope == "ISOLATED_REHEARSAL",
|
||||
"production_receipt": production_receipt,
|
||||
"public_business_e2e_verified": False,
|
||||
"production_acceptance": False,
|
||||
}
|
||||
|
||||
|
||||
def _validate_source_admission_task(config, candidate, admitted) -> None:
|
||||
if admitted.get("task_id") != config.source_admission_task_id:
|
||||
raise C5ExecutionHostBlocked("SOURCE_ADMISSION_TASK_BINDING_INVALID")
|
||||
|
||||
|
||||
def build_execution_host(
|
||||
config: C5ExecutionHostConfig,
|
||||
*,
|
||||
expected_owner_uid: int | None = 0,
|
||||
completion_approvals: Any = None,
|
||||
) -> C5ExecutionHost:
|
||||
if os.environ.get("AIOS_GITOPS_WRITES_ENABLED", "false").lower() != "false":
|
||||
raise C5ExecutionHostBlocked("LEGACY_GLOBAL_WRITES_ENABLED")
|
||||
config.verify_files(expected_owner_uid=expected_owner_uid)
|
||||
candidates = ReleaseCandidateStore(config.state_root / "candidates")
|
||||
plans = ReleasePlanStore(config.state_root / "release-plans")
|
||||
approvals = SignedApprovalStore(config.state_root / "approvals")
|
||||
completion_authority = None
|
||||
if completion_approvals is not None:
|
||||
from c5_completion_renewal import (
|
||||
RegisteredCompletionApprovalStore, RegisteredCompletionAuthority,
|
||||
CompletionRuntimeGuard,
|
||||
)
|
||||
if (type(completion_approvals) is not RegisteredCompletionApprovalStore
|
||||
or type(completion_approvals.authority) is not RegisteredCompletionAuthority
|
||||
or expected_owner_uid != 0 or config.execution_scope != "PRODUCTION"
|
||||
or Path(__file__) != completion_approvals.authority.root / "c5_release_execution_host.py"
|
||||
or Path(completion_approvals.original.root) != approvals.root):
|
||||
raise C5ExecutionHostBlocked("REGISTERED_COMPLETION_DEPENDENCY_REQUIRED")
|
||||
completion_authority = completion_approvals.authority
|
||||
completion_authority.validate_host(
|
||||
state_root=config.state_root, candidate_id=config.candidate_id,
|
||||
source_commit=config.source_commit, tree_oid=config.tree_oid,
|
||||
artifact_sha256=config.artifact_sha256,
|
||||
release_plan_sha256=config.release_plan_sha256,
|
||||
)
|
||||
approvals = completion_approvals
|
||||
sagas = ReleaseSagaStore(config.state_root / "sagas")
|
||||
candidate = candidates.load(config.candidate_id)
|
||||
plan = plans.load(config.candidate_id).validate()
|
||||
approval = approvals.load(config.candidate_id)
|
||||
observed = (
|
||||
candidate.source_commit, candidate.tree_oid, candidate.artifact_sha256,
|
||||
plan.sha256, approval.fingerprint,
|
||||
)
|
||||
expected = (
|
||||
config.source_commit, config.tree_oid, config.artifact_sha256,
|
||||
config.release_plan_sha256, config.approval_fingerprint,
|
||||
)
|
||||
if observed != expected or plan.tag.version != C5_VERSION:
|
||||
raise C5ExecutionHostBlocked("HOST_DURABLE_BINDING_MISMATCH")
|
||||
if (
|
||||
plan.policy_sha256 != config.trusted_policy_sha256
|
||||
or Path(plan.deployment.authoritative_git_dir)
|
||||
!= config.authoritative_git_dir
|
||||
):
|
||||
raise C5ExecutionHostBlocked("HOST_ACTUAL_PLAN_BINDING_MISMATCH")
|
||||
policy = _load_policy(config.trusted_policy_path)
|
||||
policy.validate_deployment(plan.deployment)
|
||||
verifier = SignedApprovalVerifier(
|
||||
ApprovalKeyRing.from_file(config.approval_keyring_path),
|
||||
ApprovalRevocationStore(config.state_root / "revocations"),
|
||||
)
|
||||
verifier.verify_release_plan(approval, candidate, plan, plan.policy_sha256)
|
||||
hmac_key = config.source_admission_hmac_path.read_bytes().strip()
|
||||
source_admissions = open_source_admission_store(
|
||||
config.source_admission_root,
|
||||
repository=REPOSITORY,
|
||||
policy=config.source_admission_policy_path,
|
||||
hmac_key=hmac_key,
|
||||
expected_owner_uid=expected_owner_uid,
|
||||
writable=False,
|
||||
)
|
||||
admitted = source_admissions.resolve_recipe(
|
||||
C5_VERSION, candidate.candidate_id, candidate.source_commit
|
||||
)
|
||||
_validate_source_admission_task(config, candidate, admitted)
|
||||
deployment_executor = RegisteredC5DeploymentExecutor(
|
||||
executor_id="c5-production-runtime-switch-v1",
|
||||
executable=config.deployment_executable,
|
||||
executable_sha256=config.deployment_executable_sha256,
|
||||
adapter_path=config.deployment_adapter_path,
|
||||
adapter_sha256=config.deployment_adapter_sha256,
|
||||
artifact_path=config.artifact_path,
|
||||
rollback_artifact_path=config.rollback_artifact_path,
|
||||
production_environment_file=config.production_environment_file,
|
||||
execution_scope=config.execution_scope,
|
||||
isolation_root=(config.state_root if config.execution_scope == "ISOLATED_REHEARSAL" else None),
|
||||
rollback_target=config.rollback_target,
|
||||
require_root_owner=expected_owner_uid == 0,
|
||||
)
|
||||
runtime = C5ExactDeploymentAdapter(
|
||||
candidate=candidate,
|
||||
release_plan=plan,
|
||||
approval=approval,
|
||||
approval_verifier=verifier,
|
||||
source_admissions=source_admissions,
|
||||
executor=deployment_executor,
|
||||
)
|
||||
ledger = CapabilityLedger(config.state_root / "capabilities")
|
||||
tag_phase = _TagPhase(
|
||||
config.tag_command, candidate, plan, approval, expected_owner_uid
|
||||
)
|
||||
tag = HardenedTagController(
|
||||
ledger,
|
||||
tag_phase,
|
||||
expected_repository=REPOSITORY,
|
||||
publication_probe=tag_phase.probe_evidence,
|
||||
executor_id="c5-production-tag-v1",
|
||||
)
|
||||
registry_adapter = _MetadataPhase(
|
||||
mode="registry", command=config.registry_command,
|
||||
record_path=config.registry_record_path, candidate=candidate,
|
||||
plan=plan, approval=approval, tag_phase=tag_phase,
|
||||
owner_uid=expected_owner_uid,
|
||||
)
|
||||
baseline_adapter = _MetadataPhase(
|
||||
mode="baseline", command=config.baseline_command,
|
||||
record_path=config.baseline_record_path, candidate=candidate,
|
||||
plan=plan, approval=approval, tag_phase=tag_phase,
|
||||
owner_uid=expected_owner_uid,
|
||||
)
|
||||
finalizer = GovernedFinalizationCoordinator(
|
||||
RegistryController(ledger, registry_adapter, executor_id="c5-production-registry-v1"),
|
||||
BaselineController(ledger, baseline_adapter, executor_id="c5-production-baseline-v1"),
|
||||
)
|
||||
|
||||
def actual_plan_factory(actual_candidate: ReleaseCandidate) -> ReleasePlan:
|
||||
actual = plans.load(actual_candidate.candidate_id).validate()
|
||||
runtime._verify_binding(actual_candidate, actual)
|
||||
return actual
|
||||
|
||||
def runtime_verify(*, candidate: ReleaseCandidate, plan: ReleasePlan) -> Mapping[str, Any]:
|
||||
runtime._verify_binding(candidate, plan)
|
||||
result = runtime._invoke("probe")
|
||||
if runtime._classify(result) != ClaimProbeResult.APPLIED_VERIFIED:
|
||||
raise C5ExecutionHostBlocked("RUNTIME_VERIFY_INDETERMINATE")
|
||||
return {
|
||||
"verified": True,
|
||||
"source_commit": candidate.source_commit,
|
||||
"tree_oid": candidate.tree_oid,
|
||||
"artifact_sha256": candidate.artifact_sha256,
|
||||
"manifest_sha256": candidate.manifest_sha256,
|
||||
"repository_identity": plan.deployment.repository_identity,
|
||||
"authoritative_git_dir": plan.deployment.authoritative_git_dir,
|
||||
"writer_disabled": True,
|
||||
}
|
||||
|
||||
orchestrator = ReleaseOrchestrator(
|
||||
candidates=candidates,
|
||||
plans=plans,
|
||||
approvals=approvals,
|
||||
sagas=sagas,
|
||||
approval_verifier=verifier,
|
||||
trusted_policy=policy,
|
||||
actual_plan_factory=actual_plan_factory,
|
||||
pre_switch_validator=PreSwitchValidator(
|
||||
_PreSwitchProbe(config, source_admissions)
|
||||
),
|
||||
runtime_switch=(CompletionRuntimeGuard(completion_authority) if completion_authority
|
||||
else runtime.controller(ledger)),
|
||||
runtime_verify=runtime_verify,
|
||||
runtime_rollback=(completion_authority.deny_runtime_effect if completion_authority
|
||||
else runtime.rollback),
|
||||
tag_publish=tag,
|
||||
finalizer=finalizer,
|
||||
)
|
||||
return C5ExecutionHost(config=config, orchestrator=orchestrator)
|
||||
|
||||
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--config", type=Path, required=True)
|
||||
parser.add_argument("--config-sha256", required=True)
|
||||
parser.add_argument("--provenance", type=Path, required=True)
|
||||
parser.add_argument("--provenance-sha256", required=True)
|
||||
args = parser.parse_args(argv)
|
||||
provenance = C5HostProvenance.load(
|
||||
args.provenance,
|
||||
expected_sha256=args.provenance_sha256,
|
||||
expected_owner_uid=0,
|
||||
)
|
||||
config = C5ExecutionHostConfig.load(
|
||||
args.config,
|
||||
expected_sha256=args.config_sha256,
|
||||
host_provenance=provenance,
|
||||
expected_owner_uid=0,
|
||||
)
|
||||
result = build_execution_host(config, expected_owner_uid=0).execute()
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except (C5ExecutionHostBlocked, CapabilityAuthorizationError) as exc:
|
||||
print(json.dumps({"status": "FAIL_CLOSED", "error": type(exc).__name__}))
|
||||
raise SystemExit(2)
|
||||
@@ -35,6 +35,8 @@ from aios_single_host_authority import (
|
||||
from registered_owner_signer_adapter import RegisteredOwnerSignerAdapter
|
||||
|
||||
SCHEMA = "aios.c5-staging-installation-authority.v3"
|
||||
SUCCESSOR_SCHEMA = "aios.c5-staging-installation-authority.v4"
|
||||
R14_SOURCE_COMMIT = "396ea9811124736aa140d2810ab53eaefb6b5c1c"
|
||||
REQUEST_SCHEMA = "aios.c5-staging-installation-request.v2"
|
||||
HOST = "cn-network-mkt"
|
||||
REPOSITORY = "aios/aios"
|
||||
@@ -47,13 +49,15 @@ ISSUER_UNIT = "aios-task-principal-issuer.service"
|
||||
ISSUER_PORT = 18946
|
||||
ISSUER_RUNTIME_URL = "http://127.0.0.1:18946/health"
|
||||
RELEASE_BASE = Path("/var/lib/aios-gitops/bridge-v2-staging/releases")
|
||||
BOOTSTRAP_RELEASE_NAME = re.compile(r"c5-bootstrap-r[1-9][0-9]*-releases\Z")
|
||||
BOOTSTRAP_SOURCE_NAME = "c5-source-b915386ccbed46a7ae77220887da8ae666649a79"
|
||||
DROPIN = (
|
||||
Path("/etc/systemd/system") / f"{UNIT}.d"
|
||||
/ "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-c5-source.conf"
|
||||
/ ("~" * 128 + "-c5-r14-source.conf")
|
||||
)
|
||||
ISSUER_DROPIN = (
|
||||
Path("/etc/systemd/system") / f"{ISSUER_UNIT}.d"
|
||||
/ "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-c5-source.conf"
|
||||
/ ("~" * 128 + "-c5-r14-source.conf")
|
||||
)
|
||||
# Replay state must not sit below a directory owned by the staging service.
|
||||
STATE_ROOT = Path("/var/lib/aios-c5-staging-installation")
|
||||
@@ -99,20 +103,6 @@ _PRINCIPAL_CONFIG_FIELDS = frozenset(
|
||||
"require_root_plan_owner", "grant_db_allowed_group_gid",
|
||||
}
|
||||
)
|
||||
_PRINCIPAL_FIXED = {
|
||||
"schema": "aios.c5-task-principal-activation.v1",
|
||||
"task_id": "TASK-V2R2C5-SOURCE-PUBLICATION-20260926-R1",
|
||||
"agent_id": "agent/codex-c5-release",
|
||||
"execution_digest": "1ba534d551991e8c99014285eeea36afcbc1500686035cb8bcd7f9f4efd6215e",
|
||||
"store_path": "/var/lib/aios-gitops/c5-source-principal/enrollment.sqlite3",
|
||||
"grant_db": "/var/lib/aios-gitops/bridge-v2-staging/task-admission.sqlite3",
|
||||
"producer_state_root": "/var/lib/aios-gitops/bridge-v2-staging",
|
||||
"owner_registry": "/etc/aios/single-host-signer-registry.json",
|
||||
"owner_signer_id": "local-owner-authority",
|
||||
"registered_signer_command": "/usr/local/libexec/aios-grant-signer-adapter",
|
||||
}
|
||||
|
||||
|
||||
class InstallationError(RuntimeError):
|
||||
def __init__(self, code: str) -> None:
|
||||
super().__init__(code)
|
||||
@@ -281,6 +271,21 @@ def _absolute_safe_path(value: Any) -> bool:
|
||||
return path.is_absolute() and ".." not in path.parts and "." not in path.parts
|
||||
|
||||
|
||||
def _trusted_prior_config_root(path: Path, layout: _Layout) -> bool:
|
||||
if layout.release_base in path.parents:
|
||||
return True
|
||||
release = path.parent
|
||||
while release != release.parent:
|
||||
if release.name == BOOTSTRAP_SOURCE_NAME:
|
||||
bootstrap = release.parent
|
||||
return (
|
||||
bool(BOOTSTRAP_RELEASE_NAME.fullmatch(bootstrap.name))
|
||||
and bootstrap.parent == layout.release_base.parent.parent
|
||||
)
|
||||
release = release.parent
|
||||
return False
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class _Layout:
|
||||
release_base: Path = RELEASE_BASE
|
||||
@@ -357,12 +362,31 @@ def _public_key(value: str) -> Ed25519PublicKey:
|
||||
|
||||
|
||||
def _validate_principal_config(
|
||||
raw: bytes, *, root: Path, release_files: set[str],
|
||||
) -> None:
|
||||
raw: bytes, *, root: Path, release_files: set[str], successor: bool,
|
||||
) -> Mapping[str, Any]:
|
||||
config = _json_bytes(raw)
|
||||
if set(config) != _PRINCIPAL_CONFIG_FIELDS or canonical_bytes(config) != raw:
|
||||
_fail("PRINCIPAL_CONFIG_CANONICAL_INVALID")
|
||||
if any(config.get(key) != value for key, value in _PRINCIPAL_FIXED.items()):
|
||||
if (
|
||||
config.get("schema") != "aios.c5-task-principal-activation.v1"
|
||||
or not isinstance(config.get("task_id"), str)
|
||||
or not _IDENTIFIER.fullmatch(config["task_id"])
|
||||
or not isinstance(config.get("agent_id"), str)
|
||||
or not _IDENTIFIER.fullmatch(config["agent_id"])
|
||||
or not isinstance(config.get("execution_digest"), str)
|
||||
or not _HEX64.fullmatch(config["execution_digest"])
|
||||
or not isinstance(config.get("owner_signer_id"), str)
|
||||
or not _IDENTIFIER.fullmatch(config["owner_signer_id"])
|
||||
):
|
||||
_fail("PRINCIPAL_CONFIG_SCOPE_INVALID")
|
||||
if successor and any(
|
||||
config.get(name) != (root / relative).as_posix()
|
||||
for name, relative in {
|
||||
"plan_dir": "plans", "policy_artifact": "policy.json",
|
||||
"policy_revocations": "policy-revocations.json",
|
||||
"epoch_root": "epoch", "epoch_revocations": "epoch-revocations.json",
|
||||
}.items()
|
||||
):
|
||||
_fail("PRINCIPAL_CONFIG_SCOPE_INVALID")
|
||||
if config.get("require_root_plan_owner") is not True:
|
||||
_fail("PRINCIPAL_CONFIG_SCOPE_INVALID")
|
||||
@@ -391,6 +415,7 @@ def _validate_principal_config(
|
||||
_fail("PRINCIPAL_CONFIG_REFERENCED_TREE_MISSING")
|
||||
if root_text in {config[name] for name in path_fields}:
|
||||
_fail("PRINCIPAL_CONFIG_PATH_INVALID")
|
||||
return config
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -487,6 +512,19 @@ class SystemdBackend:
|
||||
def hostname(self) -> str:
|
||||
return socket.gethostname()
|
||||
|
||||
def verify_dropin_precedence(self, profile: _TargetProfile, dropin: Path) -> None:
|
||||
active_paths = self._run(
|
||||
["systemctl", "show", profile.unit, "--property=DropInPaths", "--value"]
|
||||
).stdout.strip()
|
||||
try:
|
||||
paths = shlex.split(active_paths, posix=True)
|
||||
except ValueError as exc:
|
||||
raise InstallationError("SYSTEMD_DROPIN_PATHS_INVALID") from exc
|
||||
if any(not PurePosixPath(path).is_absolute() or not path.endswith(".conf") for path in paths):
|
||||
_fail("SYSTEMD_DROPIN_PATHS_INVALID")
|
||||
if any(PurePosixPath(path).name > dropin.name for path in paths):
|
||||
_fail("STAGING_DROPIN_PRECEDENCE_CONFLICT")
|
||||
|
||||
@staticmethod
|
||||
def _run(command: list[str], *, check: bool = True) -> subprocess.CompletedProcess[str]:
|
||||
try:
|
||||
@@ -677,11 +715,12 @@ def _validate_payload(payload: Mapping[str, Any], now: int, *, layout: _Layout)
|
||||
}
|
||||
if not isinstance(payload, Mapping) or set(payload) != required:
|
||||
_fail("INSTALL_CONTRACT_FIELDS_INVALID")
|
||||
successor = payload.get("schema") == SUCCESSOR_SCHEMA
|
||||
profile = _profile(payload.get("target"))
|
||||
if not isinstance(payload.get("support_commit"), str):
|
||||
_fail("INSTALL_IDENTITY_INVALID")
|
||||
if (
|
||||
payload["schema"] != SCHEMA or payload["owner_namespace"] != OWNER_NAMESPACE
|
||||
payload["schema"] not in {SCHEMA, SUCCESSOR_SCHEMA} or payload["owner_namespace"] != OWNER_NAMESPACE
|
||||
or payload["host"] != HOST or payload["repository"] != REPOSITORY
|
||||
or payload["unit"] != profile.unit or payload["deployment_state"] != "STAGING"
|
||||
or payload["release_root"] != _release_root(payload["support_commit"], layout).as_posix()
|
||||
@@ -695,6 +734,8 @@ def _validate_payload(payload: Mapping[str, Any], now: int, *, layout: _Layout)
|
||||
or not _HEX40.fullmatch(payload["support_commit"])
|
||||
):
|
||||
_fail("INSTALL_IDENTITY_INVALID")
|
||||
if successor and payload["source_commit"] != R14_SOURCE_COMMIT:
|
||||
_fail("INSTALL_SOURCE_COMMIT_MISMATCH")
|
||||
if not isinstance(payload["listen"], Mapping) or dict(payload["listen"]) != {"host": "127.0.0.1", "port": profile.port}:
|
||||
_fail("INSTALL_LISTEN_INVALID")
|
||||
if (
|
||||
@@ -719,16 +760,21 @@ def _validate_payload(payload: Mapping[str, Any], now: int, *, layout: _Layout)
|
||||
new_module = (root / "app" / profile.module_name).as_posix()
|
||||
principal_path = (root / "principal-activation.json").as_posix()
|
||||
has_principal_config = principal_path in by_path
|
||||
if payload["schema"] == SUCCESSOR_SCHEMA and not has_principal_config:
|
||||
_fail("PRINCIPAL_CONFIG_REQUIRED")
|
||||
new_activation = (
|
||||
(root / "source-activation.json").as_posix()
|
||||
if profile.name == BRIDGE_TARGET else None
|
||||
)
|
||||
new_principal_config = principal_path if has_principal_config else None
|
||||
existing = payload["existing"]
|
||||
if not isinstance(existing, Mapping) or set(existing) != {
|
||||
existing_fields = {
|
||||
"unit_active", "effective_config_sha256", "dropin", "pythonpath", "source_files",
|
||||
"activation_file", "principal_scope_config",
|
||||
}:
|
||||
}
|
||||
if not isinstance(existing, Mapping) or set(existing) != (
|
||||
existing_fields | {"config_files"} if successor else existing_fields
|
||||
):
|
||||
_fail("EXISTING_SNAPSHOT_INVALID")
|
||||
if (
|
||||
existing["unit_active"] is not True
|
||||
@@ -741,10 +787,37 @@ def _validate_payload(payload: Mapping[str, Any], now: int, *, layout: _Layout)
|
||||
or (":" in item and not layout.test_only)
|
||||
for item in existing["pythonpath"]
|
||||
)
|
||||
or existing["activation_file"] is not None
|
||||
or existing["principal_scope_config"] is not None
|
||||
):
|
||||
_fail("EXISTING_SNAPSHOT_INVALID")
|
||||
if successor:
|
||||
if any(
|
||||
value is not None and not isinstance(value, str)
|
||||
for value in (existing["activation_file"], existing["principal_scope_config"])
|
||||
):
|
||||
_fail("EXISTING_CONFIG_SNAPSHOT_INVALID")
|
||||
config_paths = {
|
||||
value for value in (
|
||||
existing["activation_file"], existing["principal_scope_config"]
|
||||
) if value is not None
|
||||
}
|
||||
if not config_paths or not isinstance(existing["config_files"], list):
|
||||
_fail("EXISTING_CONFIG_SNAPSHOT_INVALID")
|
||||
records = tuple(FileRecord.from_mapping(item) for item in existing["config_files"])
|
||||
if len(records) != len(config_paths) or {record.path for record in records} != config_paths:
|
||||
_fail("EXISTING_CONFIG_SNAPSHOT_INVALID")
|
||||
for record in records:
|
||||
path = Path(record.path)
|
||||
if (
|
||||
record.kind != "existing_config"
|
||||
or not _absolute_safe_path(record.path)
|
||||
or not _trusted_prior_config_root(path, layout)
|
||||
or record.mode not in _SAFE_EXISTING_SOURCE_MODES
|
||||
or type(record.size) is not int or record.size < 0
|
||||
or not isinstance(record.sha256, str) or not _HEX64.fullmatch(record.sha256)
|
||||
):
|
||||
_fail("EXISTING_CONFIG_SNAPSHOT_INVALID")
|
||||
elif existing["activation_file"] is not None or existing["principal_scope_config"] is not None:
|
||||
_fail("EXISTING_SNAPSHOT_INVALID")
|
||||
old_paths = tuple(existing["pythonpath"])
|
||||
if (root / "app").as_posix() in old_paths:
|
||||
_fail("EXISTING_PYTHONPATH_ALREADY_ACTIVATED")
|
||||
@@ -887,13 +960,39 @@ def _verified_artifact(
|
||||
):
|
||||
_fail("DROPIN_CONTENT_INVALID")
|
||||
if principal_config:
|
||||
_validate_principal_config(
|
||||
principal = _validate_principal_config(
|
||||
result["principal-activation.json"], root=root,
|
||||
successor=payload["schema"] == SUCCESSOR_SCHEMA,
|
||||
release_files={
|
||||
item.path for item in records
|
||||
if item.path != profile.dropin(layout).as_posix()
|
||||
},
|
||||
)
|
||||
if payload["schema"] == SUCCESSOR_SCHEMA:
|
||||
digest = principal["execution_digest"]
|
||||
plan = result.get(f"plans/{digest}.json")
|
||||
if plan is None or _sha(plan) != digest:
|
||||
_fail("PRINCIPAL_PLAN_DIGEST_MISMATCH")
|
||||
if payload["schema"] == SUCCESSOR_SCHEMA:
|
||||
activation_raw = result.get("source-activation.json")
|
||||
policy_raw = result.get("policy.json")
|
||||
if activation_raw is None or policy_raw is None or "admission-activation.json" not in result:
|
||||
_fail("SOURCE_ACTIVATION_REFERENCES_MISSING")
|
||||
activation = _json_bytes(activation_raw)
|
||||
if (
|
||||
canonical_bytes(activation) != activation_raw
|
||||
or set(activation) != {
|
||||
"schema", "policy_artifact", "policy_sha256", "policy_revocations",
|
||||
"admission_activation", "expected_agent_id",
|
||||
}
|
||||
or activation["schema"] != "aios.source-publication-host-activation.v1"
|
||||
or activation["expected_agent_id"] != principal["agent_id"]
|
||||
or activation["policy_artifact"] != (root / "policy.json").as_posix()
|
||||
or activation["policy_sha256"] != _sha(policy_raw)
|
||||
or activation["policy_revocations"] != (root / "policy-revocations.json").as_posix()
|
||||
or activation["admission_activation"] != (root / "admission-activation.json").as_posix()
|
||||
):
|
||||
_fail("SOURCE_ACTIVATION_SCOPE_INVALID")
|
||||
return result
|
||||
|
||||
|
||||
@@ -921,7 +1020,8 @@ def _request_payload(
|
||||
root = _release_root(request.support_commit, layout)
|
||||
profile = _profile(request.target)
|
||||
return {
|
||||
"schema": SCHEMA, "target": request.target, "contract_id": request.contract_id,
|
||||
"schema": SUCCESSOR_SCHEMA if "config_files" in request.existing else SCHEMA,
|
||||
"target": request.target, "contract_id": request.contract_id,
|
||||
"owner_signer_id": request.owner_signer_id, "owner_key_id": owner_key_id,
|
||||
"owner_namespace": OWNER_NAMESPACE, "host": HOST, "repository": REPOSITORY,
|
||||
"source_commit": request.source_commit, "support_commit": request.support_commit,
|
||||
@@ -991,7 +1091,37 @@ class Installer:
|
||||
self.layout = _test_layout if _test_layout is not None else _Layout()
|
||||
|
||||
def _state_path(self, payload: Mapping[str, Any]) -> Path:
|
||||
return self.layout.state_root / f"{payload['target']}-support-{payload['support_commit']}.json"
|
||||
legacy = self.layout.state_root / f"{payload['target']}-support-{payload['support_commit']}.json"
|
||||
# Preserve the historical support-keyed record for its original
|
||||
# contract. Once that contract is durably rolled back, a fresh
|
||||
# contract receives its own immutable state record; APPLIED/UNKNOWN
|
||||
# records remain a barrier and are never bypassed by this split.
|
||||
if legacy.is_file():
|
||||
prior = self._read_state(legacy)
|
||||
if (
|
||||
prior is not None
|
||||
and prior.get("status") == "ROLLED_BACK"
|
||||
and prior.get("contract_id") != payload.get("contract_id")
|
||||
):
|
||||
successor = self.layout.state_root / (
|
||||
f"{payload['target']}-support-{payload['support_commit']}-contract-"
|
||||
f"{_sha(str(payload['contract_id']).encode())}.json"
|
||||
)
|
||||
if successor.is_file():
|
||||
return successor
|
||||
prefix = f"{payload['target']}-support-{payload['support_commit']}-contract-"
|
||||
for candidate in self.layout.state_root.glob(prefix + "*.json"):
|
||||
if candidate.is_symlink() or not candidate.is_file():
|
||||
_fail("INSTALL_STATE_INVALID")
|
||||
observed = self._read_state(candidate)
|
||||
if observed is None or observed.get("contract_id") == payload.get("contract_id"):
|
||||
continue
|
||||
if observed.get("status") in {"APPLIED", "UNKNOWN"}:
|
||||
_fail("INSTALL_PRIOR_ATTEMPT_NOT_APPLIED")
|
||||
if observed.get("status") not in {"ROLLED_BACK"}:
|
||||
_fail("INSTALL_PRIOR_ATTEMPT_NOT_APPLIED")
|
||||
return successor
|
||||
return legacy
|
||||
|
||||
def _nonce_path(self, payload: Mapping[str, Any]) -> Path:
|
||||
return self.layout.state_root / "nonces" / f"{_sha(payload['nonce'].encode())}.json"
|
||||
@@ -1024,6 +1154,8 @@ class Installer:
|
||||
existing = payload["existing"]
|
||||
profile = _profile(payload["target"])
|
||||
dropin = profile.dropin(self.layout)
|
||||
if isinstance(self.backend, SystemdBackend):
|
||||
self.backend.verify_dropin_precedence(profile, dropin)
|
||||
snapshot = self.backend.unit_snapshot(profile)
|
||||
if (
|
||||
snapshot.active is not True
|
||||
@@ -1047,6 +1179,8 @@ class Installer:
|
||||
_fail("EXISTING_DROPIN_MISMATCH")
|
||||
for item in existing["source_files"]:
|
||||
self._verify_file(FileRecord.from_mapping(item))
|
||||
for item in existing.get("config_files", ()):
|
||||
self._verify_file(FileRecord.from_mapping(item))
|
||||
return backup
|
||||
|
||||
def _verify_readiness(
|
||||
@@ -1262,6 +1396,8 @@ class Installer:
|
||||
_fail("ROLLBACK_EFFECTIVE_CONFIG_MISMATCH")
|
||||
for item in existing["source_files"]:
|
||||
self._verify_file(FileRecord.from_mapping(item))
|
||||
for item in existing.get("config_files", ()):
|
||||
self._verify_file(FileRecord.from_mapping(item))
|
||||
self._verify_readiness(prepared.payload["rollback"]["readiness"], profile)
|
||||
if not profile.installs_release:
|
||||
self._verify_release_tree(prepared.payload, profile)
|
||||
|
||||
@@ -83,6 +83,8 @@ class CommittedTreeSourceReader:
|
||||
|
||||
_OID = re.compile(r"^[0-9a-f]{40}$")
|
||||
_SHA = re.compile(r"^[0-9a-f]{64}$")
|
||||
_SAFE_CODE = re.compile(r"^(?:TASK_PRINCIPAL|EXECUTION_PLAN|GOVERNED)_[A-Z0-9_]{3,120}$")
|
||||
_SAFE_OPERATION = re.compile(r"^[A-Za-z][A-Za-z0-9]{1,63}$")
|
||||
|
||||
|
||||
def _oid(value: Any) -> str:
|
||||
@@ -133,6 +135,7 @@ class GovernedPublicationClient:
|
||||
runtime_fingerprint: str,
|
||||
process_start_identity: str,
|
||||
commit_message: str = "governed source publication",
|
||||
diagnostic_sink: Callable[[Mapping[str, Any]], None] | None = None,
|
||||
) -> None:
|
||||
if (
|
||||
writer_executor is None
|
||||
@@ -166,6 +169,36 @@ class GovernedPublicationClient:
|
||||
self.runtime_fingerprint = runtime_fingerprint
|
||||
self.process_start_identity = process_start_identity
|
||||
self.commit_message = commit_message
|
||||
if diagnostic_sink is not None and not callable(diagnostic_sink):
|
||||
raise ValueError("diagnostic_sink_invalid")
|
||||
self.diagnostic_sink = diagnostic_sink
|
||||
|
||||
def _record_uncertain_effect(self, *, exc: Exception, task_id: str,
|
||||
stage: str) -> None:
|
||||
"""Emit only allowlisted facts; never serialize an exception or body."""
|
||||
if self.diagnostic_sink is None:
|
||||
return
|
||||
raw_status = getattr(exc, "status", None)
|
||||
status = raw_status if type(raw_status) is int and 100 <= raw_status <= 599 else None
|
||||
raw_code = getattr(exc, "error_code", None)
|
||||
code = raw_code if isinstance(raw_code, str) and _SAFE_CODE.fullmatch(raw_code) else None
|
||||
raw_operation = getattr(exc, "operation", None)
|
||||
operation = (
|
||||
raw_operation if isinstance(raw_operation, str)
|
||||
and _SAFE_OPERATION.fullmatch(raw_operation) else stage
|
||||
)
|
||||
diagnostic = {
|
||||
"task_id": task_id, "operation": operation,
|
||||
"http_status": status,
|
||||
"error_code": (
|
||||
"transport_unknown" if status is None else (code or "issuer_error_unknown")
|
||||
),
|
||||
}
|
||||
try:
|
||||
self.diagnostic_sink(diagnostic)
|
||||
except Exception:
|
||||
# Diagnostic observation must never change the uncertainty policy.
|
||||
pass
|
||||
|
||||
def _validate_admission(self, intent: SourcePublicationIntent, receipt: Mapping[str, Any]) -> Mapping[str, Any]:
|
||||
if not isinstance(receipt, Mapping):
|
||||
@@ -233,8 +266,10 @@ class GovernedPublicationClient:
|
||||
manifest_id=self.manifest_id, runtime_fingerprint=self.runtime_fingerprint,
|
||||
process_start_identity=self.process_start_identity,
|
||||
)
|
||||
stage = "acquireWriterLease"
|
||||
try:
|
||||
session = self.writer_executor.acquire(request=request, job_id=job_id)
|
||||
stage = "commitFiles"
|
||||
commit_result = self.writer_executor.commit_and_readback(
|
||||
session=session, request=request, files=files,
|
||||
expected_sha256=hashes, message=self.commit_message,
|
||||
@@ -251,6 +286,7 @@ class GovernedPublicationClient:
|
||||
# A transport exception after a mutation is indeterminate. Never
|
||||
# resubmit it here; the caller must reconcile through the existing
|
||||
# Job/recovery service.
|
||||
self._record_uncertain_effect(exc=exc, task_id=intent.task_id, stage=stage)
|
||||
raise GovernedPublicationError("PUBLICATION_EFFECT_UNKNOWN") from exc
|
||||
if not isinstance(readback, Mapping) or readback.get("authenticated") is not True:
|
||||
raise GovernedPublicationError("PUBLICATION_AUTHENTICATED_READBACK_REQUIRED")
|
||||
@@ -326,6 +362,9 @@ class GovernedPublicationClient:
|
||||
except Exception as exc:
|
||||
# A lost result response may have changed the durable Task. Never
|
||||
# re-register, reclaim or retry here.
|
||||
self._record_uncertain_effect(
|
||||
exc=exc, task_id=intent.task_id, stage="submitTaskResult",
|
||||
)
|
||||
raise GovernedPublicationError("PUBLICATION_EFFECT_UNKNOWN") from exc
|
||||
publication.update(
|
||||
task_result=task_result,
|
||||
|
||||
@@ -553,7 +553,7 @@ class ReleaseSourceAdmissionStore:
|
||||
supplied_candidate = str(candidate_id or "")
|
||||
if supplied_candidate and supplied_candidate != record["candidate_id"]:
|
||||
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_CANDIDATE_MISMATCH")
|
||||
return {
|
||||
result = {
|
||||
"mode": RECIPE_MODE,
|
||||
"task_id": record["task_id"],
|
||||
"candidate_id": record["candidate_id"],
|
||||
@@ -562,6 +562,9 @@ class ReleaseSourceAdmissionStore:
|
||||
"version": record["version"],
|
||||
"purpose": record["purpose"],
|
||||
}
|
||||
if version == "V2R2C5":
|
||||
result["tree_oid"] = record["tree_oid"]
|
||||
return result
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
|
||||
@@ -434,6 +434,7 @@ class DeploymentPlan:
|
||||
health_contract_version: str = ""
|
||||
timeout_policy: dict[str, Any] = field(default_factory=dict)
|
||||
retry_policy: dict[str, Any] = field(default_factory=dict)
|
||||
rollback_target: dict[str, str] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_validate_identifier("service", self.service)
|
||||
@@ -463,12 +464,20 @@ class DeploymentPlan:
|
||||
raise ValueError("timeout_policy must be an object")
|
||||
if not isinstance(self.retry_policy, dict):
|
||||
raise ValueError("retry_policy must be an object")
|
||||
if not isinstance(self.rollback_target, dict):
|
||||
raise ValueError("rollback_target must be an object")
|
||||
if self.rollback_target:
|
||||
if set(self.rollback_target) != {"version", "target_oid", "artifact_sha256"}:
|
||||
raise ValueError("rollback_target fields invalid")
|
||||
_validate_identifier("rollback_target.version", self.rollback_target["version"])
|
||||
_validate_oid("rollback_target.target_oid", self.rollback_target["target_oid"])
|
||||
_validate_sha("rollback_target.artifact_sha256", self.rollback_target["artifact_sha256"])
|
||||
if self.writer_expected is not False:
|
||||
raise ValueError("production writer must remain disabled")
|
||||
|
||||
@property
|
||||
def canonical_payload(self) -> dict[str, Any]:
|
||||
return {
|
||||
payload = {
|
||||
"artifact_sha256": self.artifact_sha256,
|
||||
"authoritative_git_dir": self.authoritative_git_dir,
|
||||
"baseline_before_sha256": self.baseline_before_sha256,
|
||||
@@ -497,6 +506,11 @@ class DeploymentPlan:
|
||||
"writer_expected": self.writer_expected,
|
||||
"registry_before_sha256": self.registry_before_sha256,
|
||||
}
|
||||
# Preserve historical deployment digests when the extension is absent.
|
||||
# An explicit target becomes part of both deployment and approval hashes.
|
||||
if self.rollback_target:
|
||||
payload["rollback_target"] = dict(self.rollback_target)
|
||||
return payload
|
||||
|
||||
@property
|
||||
def sha256(self) -> str:
|
||||
@@ -4031,6 +4045,7 @@ class SilentReleaseActionService:
|
||||
policy_sha256: str = "",
|
||||
max_builder_attempts: int = 3,
|
||||
builder_attempt_timeout_seconds: int = 180,
|
||||
completion_approvals: Any = None,
|
||||
current_authority_guard: Callable[
|
||||
[SignedReleasePlanApproval, str, str], None
|
||||
]
|
||||
@@ -4078,6 +4093,15 @@ class SilentReleaseActionService:
|
||||
self.sagas = ReleaseSagaStore(self.root / "sagas")
|
||||
self.approvals_root = self.root / "approvals"
|
||||
self.approvals = SignedApprovalStore(self.approvals_root)
|
||||
if completion_approvals is not None:
|
||||
from c5_completion_renewal import RegisteredCompletionApprovalStore
|
||||
if (type(completion_approvals) is not RegisteredCompletionApprovalStore
|
||||
or self.root != completion_approvals.authority.state_root
|
||||
or completion_approvals.original.root != self.approvals_root
|
||||
or approval_verifier is None or execution_enabled):
|
||||
raise CapabilityAuthorizationError("registered shadow completion approval dependency required")
|
||||
completion_approvals.authority()
|
||||
self.approvals = completion_approvals
|
||||
self.intents_root = self.root / "intents"
|
||||
self.acceptance_root = self.root / "acceptance"
|
||||
self.notifications_root = self.root / "notifications"
|
||||
|
||||
@@ -29,6 +29,7 @@ import base64
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
import subprocess
|
||||
import time
|
||||
@@ -63,6 +64,17 @@ class LiveProtocolUnavailable(LiveProtocolError):
|
||||
pass
|
||||
|
||||
|
||||
class IssuerResponseError(LiveProtocolError):
|
||||
"""Safe, structured issuer failure metadata for isolated diagnostics."""
|
||||
|
||||
def __init__(self, message: str, *, status: int, error_code: str | None,
|
||||
operation: str) -> None:
|
||||
super().__init__(message)
|
||||
self.status = status
|
||||
self.error_code = error_code
|
||||
self.operation = operation
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class HttpResult:
|
||||
status: int
|
||||
@@ -568,8 +580,14 @@ class LiveTaskPrincipalClient:
|
||||
break
|
||||
time.sleep(1)
|
||||
status = 0 if last is None else last.status
|
||||
raise LiveProtocolError(
|
||||
f"TaskPrincipal assertion issue failed operation={operation} HTTP {status}"
|
||||
raw_code = None if last is None else last.body.get("error")
|
||||
error_code = (
|
||||
raw_code if isinstance(raw_code, str)
|
||||
and re.fullmatch(r"(?:TASK_PRINCIPAL|EXECUTION_PLAN|GOVERNED)_[A-Z0-9_]{3,120}", raw_code) else None
|
||||
)
|
||||
raise IssuerResponseError(
|
||||
f"TaskPrincipal assertion issue failed operation={operation} HTTP {status}",
|
||||
status=status, error_code=error_code, operation=operation,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ import re
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
@@ -405,51 +407,85 @@ def run_secret_scan_provider(
|
||||
tree_oid: str,
|
||||
store_id: str = "secret_scan",
|
||||
provider_identity: str = "aios.secret-scanner",
|
||||
artifact_path: Path | None = None,
|
||||
) -> tuple[Mapping[str, Any], str]:
|
||||
"""Execute the real secret scanner (gitleaks) and produce a root receipt."""
|
||||
if not Path(gitleaks_bin).is_file():
|
||||
raise TrustedProviderExecutionError("SECRET_SCANNER_UNAVAILABLE")
|
||||
artifact_sha = None
|
||||
if artifact_path is not None:
|
||||
artifact_sha = _validate_artifact_scan_target(artifact_path, target)
|
||||
# --no-git scans the working tree as a filesystem (does not require a .git
|
||||
# dir), so an exported release checkout can be scanned faithfully.
|
||||
with tempfile.TemporaryDirectory(prefix="aios-secret-report-") as report_root:
|
||||
report = Path(report_root) / "findings.json"
|
||||
scanned = _run(
|
||||
[
|
||||
gitleaks_bin, "detect",
|
||||
"--source", str(target),
|
||||
"--no-git",
|
||||
"--no-banner",
|
||||
"--exit-code", "0",
|
||||
"--exit-code", "1",
|
||||
"--report-format", "json", "--report-path", str(report),
|
||||
],
|
||||
cwd=target,
|
||||
timeout=1800,
|
||||
)
|
||||
warnings = 0
|
||||
# gitleaks exits 1 when findings are present; with --exit-code 0 it returns 0
|
||||
# but prints findings. Count lines that indicate a finding.
|
||||
finding_lines = [
|
||||
ln for ln in scanned.stdout.splitlines()
|
||||
if ("Finding:" in ln and ":" in ln and "RuleID" not in ln)
|
||||
] + [
|
||||
ln for ln in scanned.stderr.splitlines()
|
||||
if ("Finding:" in ln and ":" in ln and "RuleID" not in ln)
|
||||
]
|
||||
artifact_sha = None
|
||||
if target.is_file():
|
||||
artifact_sha = hashlib.sha256(target.read_bytes()).hexdigest()
|
||||
try:
|
||||
findings = json.loads(report.read_text())
|
||||
except (OSError, ValueError) as exc:
|
||||
raise TrustedProviderExecutionError("SECRET_SCANNER_REPORT_INVALID") from exc
|
||||
if not isinstance(findings, list) or any(not isinstance(v, dict) for v in findings):
|
||||
raise TrustedProviderExecutionError("SECRET_SCANNER_REPORT_INVALID")
|
||||
report_sha = hashlib.sha256(report.read_bytes()).hexdigest()
|
||||
if artifact_path is not None and _validate_artifact_scan_target(artifact_path, target) != artifact_sha:
|
||||
raise TrustedProviderExecutionError("SECRET_SCAN_ARTIFACT_CHANGED")
|
||||
# Passed only when the scanner actually ran cleanly (exit 0) AND found zero
|
||||
# leaks. A scanner crash (non-zero exit) is never a pass.
|
||||
clean_run = scanned.returncode == 0
|
||||
result = {
|
||||
"passed": clean_run and len(finding_lines) == 0,
|
||||
"passed": clean_run and len(findings) == 0,
|
||||
"scanner": "gitleaks",
|
||||
"exit_code": scanned.returncode,
|
||||
"target": str(target),
|
||||
"findings": len(finding_lines),
|
||||
"findings": len(findings),
|
||||
"report_sha256": report_sha,
|
||||
"artifact_sha256": artifact_sha,
|
||||
"clean_run": clean_run,
|
||||
}
|
||||
return result, _receipt_ref_for(result, "secret_scan")
|
||||
|
||||
|
||||
def _validate_artifact_scan_target(artifact: Path, target: Path) -> str:
|
||||
"""Bind a real filesystem scan to every byte of its exact archive payload."""
|
||||
if artifact.is_symlink() or not artifact.is_file() or target.is_symlink() or not target.is_dir():
|
||||
raise TrustedProviderExecutionError("SECRET_SCAN_ARTIFACT_TARGET_INVALID")
|
||||
raw = artifact.read_bytes()
|
||||
import io
|
||||
expected: set[str] = set()
|
||||
try:
|
||||
with tarfile.open(fileobj=io.BytesIO(raw), mode="r:*") as archive:
|
||||
members = archive.getmembers()
|
||||
if not members or len(members) > 20_000 or sum(m.size for m in members) > 2 * 1024**3:
|
||||
raise TrustedProviderExecutionError("SECRET_SCAN_ARCHIVE_LIMIT")
|
||||
for member in members:
|
||||
parts = Path(member.name).parts
|
||||
if not member.isfile() or member.name.startswith("/") or "\\" in member.name or not parts or any(p in {".", ".."} for p in parts) or member.name in expected:
|
||||
raise TrustedProviderExecutionError("SECRET_SCAN_ARCHIVE_INVALID")
|
||||
path = target.joinpath(*parts)
|
||||
if path.is_symlink() or not path.is_file() or not path.resolve().is_relative_to(target.resolve()):
|
||||
raise TrustedProviderExecutionError("SECRET_SCAN_PAYLOAD_MISMATCH")
|
||||
if archive.extractfile(member).read() != path.read_bytes():
|
||||
raise TrustedProviderExecutionError("SECRET_SCAN_PAYLOAD_MISMATCH")
|
||||
expected.add(member.name)
|
||||
except (tarfile.TarError, OSError) as exc:
|
||||
raise TrustedProviderExecutionError("SECRET_SCAN_ARCHIVE_INVALID") from exc
|
||||
actual = {p.relative_to(target).as_posix() for p in target.rglob("*") if p.is_file()}
|
||||
if actual != expected or any(p.is_symlink() for p in target.rglob("*")):
|
||||
raise TrustedProviderExecutionError("SECRET_SCAN_PAYLOAD_MISMATCH")
|
||||
return hashlib.sha256(raw).hexdigest()
|
||||
|
||||
|
||||
def _receipt_ref_for(result: Mapping[str, Any], kind: str) -> str:
|
||||
run_id = hashlib.sha256(canonical_json_bytes(result)).hexdigest()[:16]
|
||||
return f"run-{run_id}/receipt.json"
|
||||
@@ -531,6 +567,7 @@ def _run_provider_executor(
|
||||
target=repo_root,
|
||||
source_oid=source_oid,
|
||||
tree_oid=tree_oid,
|
||||
artifact_path=(Path(os.environ["AIOS_RELEASE_ARTIFACT"]) if os.environ.get("AIOS_RELEASE_ARTIFACT") else None),
|
||||
)
|
||||
if kind == "gitea":
|
||||
gitea_api_url = os.environ.get("AIOS_GITEA_API_URL", "").strip()
|
||||
@@ -601,9 +638,14 @@ if __name__ == "__main__":
|
||||
parser.add_argument("--store-root", required=True)
|
||||
parser.add_argument("--receipt-ref", required=True)
|
||||
parser.add_argument("--repo-root", default=None, help="Real source checkout to execute against")
|
||||
parser.add_argument("--artifact-path", type=Path, default=None)
|
||||
args = parser.parse_args()
|
||||
if args.repo_root:
|
||||
os.environ["AIOS_REPO_ROOT"] = args.repo_root
|
||||
if args.artifact_path:
|
||||
if args.kind != "secret_scan":
|
||||
raise SystemExit("ARTIFACT_BINDING_REQUIRES_SECRET_SCAN")
|
||||
os.environ["AIOS_RELEASE_ARTIFACT"] = str(args.artifact_path)
|
||||
try:
|
||||
digest = produce(
|
||||
args.kind,
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# V2R2C5 C5 mainline consolidation source record
|
||||
|
||||
## Scope and authority boundary
|
||||
|
||||
This maintenance candidate is reconciled onto current main
|
||||
`e33664f03302fd9de7635afdd8aa9339a50e5f0a` (tree
|
||||
`92ece1e9646ac2d12b4b467c82b1279ac1e11608`) and preserves its V2R2C6 source
|
||||
and security changes. The original reviewed selection was made against
|
||||
`b915386ccbed46a7ae77220887da8ae666649a79`. Current main already contains the
|
||||
product source commit; the remaining five reviewed source commits and isolated
|
||||
fixtures are carried by this candidate. This is a source and test consolidation
|
||||
only. It does not create a production baseline, update the runtime registry,
|
||||
activate a Task or Agent, publish a tag, or authorize a protected merge. The
|
||||
production source, tag, artifact, and existing baseline records remain immutable
|
||||
inputs to their validators.
|
||||
|
||||
| Layer | Commit | Current-main relationship |
|
||||
| --- | --- | --- |
|
||||
| installer / G1 | `7e14abeb6b91978ce16f3d903b6c0714af302991` | integrated by this candidate |
|
||||
| builder | `8fc61bd11213bf22cc795ba933a5f367f8831f82` | integrated by this candidate |
|
||||
| completion | `66530975f52a4d6f1449f18d23af808e0c9339bb` | integrated by this candidate |
|
||||
| deployment | `5443c93f00ce879e1025a53258557788da4dbcec` | integrated by this candidate |
|
||||
| boundary / provider | `74c0eb75348b752c82f45e53759edfff5a47e3ef` | integrated by this candidate |
|
||||
| product | `5f65e458957dd51c9973cfa956a624f3cbf54990` | already an ancestor of current main |
|
||||
|
||||
The reviewed selection manifest contained 41 paths and eight non-identical
|
||||
overlap records. No receipt, journal, live baseline, runtime registry record,
|
||||
or other generated production state was used as source.
|
||||
|
||||
## Per-file decisions
|
||||
|
||||
| Classification | Paths | Decision |
|
||||
| --- | --- | --- |
|
||||
| source | `active-runtime/app/aios_baseline/service.py`; `aios_capability_contract.py`; `aios_task_principal_v3_receipt_provider.py`; `approved_release_executor.py`; `c5_exact_deployment_adapter.py`; `c5_registered_installer_executor.py`; `c5_staging_installation.py`; `governed_publication_client.py`; `release_source_admission.py`; `task_plane/live_governed_writer_adapters.py` | Integrated G1/product behavior and fail-closed validators. |
|
||||
| builder source | `active-runtime/app/aios_trusted_candidate_builder.py` | Kept the later builder contract and integration validation. Replaced historical live policy identity constants with values read from the governed policy. |
|
||||
| completion source | `active-runtime/app/c5_completion_api.py`; `c5_completion_phase.py`; `c5_completion_renewal.py`; `c5_completion_signer.py`; `silent_release_hardening.py` | Retained completion-only successor validation. Authority root, state root, Owner key, predecessor fingerprint, and historical Saga head now require a root-owned digest-bound bindings file. |
|
||||
| shared completion/boundary source | `active-runtime/app/c5_release_execution_host.py` | Retained the completion host and added the boundary integration-policy checks. Source-admission Task identity is now supplied by the digest-bound host configuration. |
|
||||
| deployment source | `active-runtime/app/c5_production_deployment_adapter.py` | Applied the later strict symlink-resolution check to the integrated installer version. |
|
||||
| provider source | `active-runtime/app/trusted_evidence_providers.py` | Retained artifact-to-extracted-tree binding and structured gitleaks JSON handling. |
|
||||
| helper | `scripts/build_c5_live_principal_overlay.py`; `scripts/build_c5_staging_artifact.py`; `scripts/probe_issuer_v3_r2_readonly.py` | Kept deterministic/read-only helper behavior. Overlay creation now requires explicit Task and Agent bindings; the checked-in example uses synthetic fixture identities. The staging artifact producer emits the final consumer's fixed 128-tilde `c5-r14-source.conf` drop-in path. |
|
||||
| fixture | `tests/fixtures/c5-r14-execution-plan.json`; `tests/fixtures/issuer-v3-base/*`; `tests/fixtures/trusted-candidate-builder-policy-c5.json` | Kept only isolated synthetic Task/Agent identities. Moved the C5 builder policy from runtime config to test fixtures. |
|
||||
| tests | `tests/baseline_v1_1/test_claim_heartbeat.py`; `tests/baseline_v1_1/test_loop.py`; `tests/task_plane/test_live_governed_writer_adapters.py`; `tests/test_c5_builder_contract.py`; `tests/test_c5_completion_renewal.py`; `tests/test_c5_exact_deployment_adapter.py`; `tests/test_c5_production_deployment_adapter.py`; `tests/test_c5_registered_installer_executor.py`; `tests/test_c5_release_execution_host.py`; `tests/test_c5_staging_artifact.py`; `tests/test_c5_staging_installation.py`; `tests/test_governed_publication_client.py`; `tests/test_issuer_v3_overlay_builder.py`; `tests/test_issuer_v3_receipt_provider.py`; `tests/test_signer_contract_compatibility.py`; `tests/test_trusted_candidate_builder.py`; `tests/test_trusted_evidence_providers_harness.py`; `tests/test_v2r2c5_capability_contract.py` | Integrated the selected regression coverage and resolved overlap tests in favor of the combined security contract. Execution-enabled heartbeat fixtures explicitly install trusted identity and evidence verifiers. The staging-artifact fixture builds a disposable Git source repository and never reads a live release tree. |
|
||||
|
||||
## Overlap resolution
|
||||
|
||||
1. Builder source and tests use the later builder contract while preserving G1
|
||||
admission and rollback validation.
|
||||
2. Deployment source and tests retain the integrated adapter and add the later
|
||||
strict symlink target checks.
|
||||
3. The release host retains completion-only support and the boundary/provider
|
||||
integration checks; the boundary host was not accepted wholesale because it
|
||||
would remove completion behavior.
|
||||
4. `silent_release_hardening.py` retains the completion dependency and derives
|
||||
its state-root check from the registered completion authority while keeping
|
||||
the C6 standing-authorization guard and credential fencing.
|
||||
5. The six current-main overlaps were reconciled without whole-file selection:
|
||||
the baseline evidence changes were identical, the capability vocabulary kept
|
||||
the C6 digest additions, staging tests kept C6 file-mode fixtures, and the C5
|
||||
completion dependency was combined with C6 authorization guards.
|
||||
6. The unsigned staging producer and the final consumer now share the governed
|
||||
fixed drop-in identity. The consumer's path, mode, archive, hash, and signature
|
||||
validators remain unchanged and fail closed on any other producer path.
|
||||
|
||||
## Excluded ephemeral material
|
||||
|
||||
The candidate contains no live one-time Task or Agent identity, TTL, nonce,
|
||||
approval fingerprint, activation file, or default historical Saga-head binding.
|
||||
It also contains no copied receipt, live journal, baseline, registry projection,
|
||||
signer credential, or production service state. Immutable source/tree/artifact,
|
||||
lineage, hash, expiry, authentication, and compare-and-swap checks remain in the
|
||||
code. Test-only identities are explicitly named `fixture-*` or `FIXTURE-*`.
|
||||
|
||||
## Verification record
|
||||
|
||||
Verification is recorded against the committed candidate and its exported
|
||||
snapshot. Windows results are treated as development evidence only where POSIX
|
||||
ownership, mode, or symlink semantics are unavailable. The authoritative full
|
||||
regression is run from the exported tree in a disposable Linux unit with empty
|
||||
capabilities, private network and temporary directories, a read-only system,
|
||||
and no signer, live configuration, or Task database access.
|
||||
@@ -6,9 +6,12 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import io
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import tarfile
|
||||
from typing import Any
|
||||
|
||||
|
||||
@@ -23,6 +26,74 @@ PINNED = {
|
||||
"governed_runtime_scope.py": "c56bd709c1e0c8956a5be7a4a8dc2a12591bdec2b84dcfba5c53f6bced746bcb",
|
||||
}
|
||||
|
||||
# The currently loaded generic issuer adapter/scope, verified independently
|
||||
# before preparing a V3-only overlay. They remain the V1/V2 implementation.
|
||||
ISSUER_V3_BASE = (
|
||||
"/var/lib/aios-gitops/bridge-v2-staging/releases/"
|
||||
"p0-task-principal-governed-runtime-20260910-02/app"
|
||||
)
|
||||
ISSUER_V3_BASE_HASHES = {
|
||||
"aios_task_principal_admission.py": "d56fb0078cfcb907f2afb35f0163051a962a89520b63f6c53149d478fc7777c8",
|
||||
"aios_task_principal_governed_adapter.py": "1f8fae5c4462b333f55b471acedd1bbc680b29f0c51fd0318ddb6f5626671933",
|
||||
"governed_runtime_scope.py": "04680f00111893411456ac6e5c3c557feedb824f9e568629e7d2904cee81a371",
|
||||
}
|
||||
ISSUER_V3_SOURCE_FILES = (
|
||||
"active-runtime/app/aios_task_principal_v3_receipt_provider.py",
|
||||
"active-runtime/app/aios_task_admission_live_adapters.py",
|
||||
"active-runtime/app/aios_task_admission_bootstrap.py",
|
||||
"active-runtime/app/aios_governed_task_bootstrap.py",
|
||||
"active-runtime/app/aios_gitops_job_controller.py",
|
||||
"active-runtime/app/aios_gitops_job_identity.py",
|
||||
"active-runtime/app/aios_online_agent_execution.py",
|
||||
)
|
||||
|
||||
ISSUER_V3_ADAPTER_OVERLAY = '''"""V3 receipt-bound overlay; legacy V1/V2 adapter is hash-pinned."""
|
||||
from __future__ import annotations
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
import stat
|
||||
import sys
|
||||
|
||||
BASE_ROOT = Path({base!r})
|
||||
BASE_HASHES = {hashes!r}
|
||||
for name, expected in BASE_HASHES.items():
|
||||
source = BASE_ROOT / name
|
||||
metadata = source.lstat()
|
||||
if (stat.S_ISLNK(metadata.st_mode) or not stat.S_ISREG(metadata.st_mode)
|
||||
or stat.S_IMODE(metadata.st_mode) & 0o022
|
||||
or hashlib.sha256(source.read_bytes()).hexdigest() != expected):
|
||||
raise RuntimeError("ISSUER_V3_BASE_MODULE_DRIFT:" + name)
|
||||
BASE = BASE_ROOT / "aios_task_principal_governed_adapter.py"
|
||||
spec = importlib.util.spec_from_file_location("_issuer_v3_pinned_base_adapter", BASE)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError("ISSUER_V3_BASE_ADAPTER_UNLOADABLE")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
import governed_runtime_scope as _issuer_scope
|
||||
if Path(_issuer_scope.__file__).resolve() != (BASE_ROOT / "governed_runtime_scope.py").resolve():
|
||||
raise RuntimeError("ISSUER_V3_BASE_SCOPE_PATH_DRIFT")
|
||||
for name, value in module.__dict__.items():
|
||||
if not name.startswith("__"):
|
||||
globals()[name] = value
|
||||
|
||||
from aios_task_principal_v3_receipt_provider import IssuerV3ReceiptScopeProvider
|
||||
|
||||
class GovernedScopeRuntimeAdapter(module.GovernedScopeRuntimeAdapter):
|
||||
def __init__(self, config, *, audience_builder, provider=None, operations=None):
|
||||
super().__init__(config, audience_builder=audience_builder,
|
||||
provider=provider, operations=operations)
|
||||
root = os.environ.get("AIOS_GOVERNED_PRODUCER_STATE_ROOT", "").strip()
|
||||
if root:
|
||||
self.provider = IssuerV3ReceiptScopeProvider(
|
||||
legacy=self.provider, producer_state_root=Path(root),
|
||||
plan_root=self.plans.root,
|
||||
grant_reader=self.grants.require_current_grant,
|
||||
)
|
||||
'''
|
||||
|
||||
EFFECTIVE_ADMISSION_SOURCE = (
|
||||
"/var/lib/aios-gitops/bridge-v2-staging/releases/"
|
||||
"p0-task-principal-governed-runtime-20260910-02/app/"
|
||||
@@ -44,7 +115,7 @@ BASE_SOURCE = Path(
|
||||
"aios_task_principal_admission.py"
|
||||
)
|
||||
BASE_SOURCE_SHA256 = "d56fb0078cfcb907f2afb35f0163051a962a89520b63f6c53149d478fc7777c8"
|
||||
C5_TASK_ID = "TASK-V2R2C5-SOURCE-PUBLICATION-20260926-R1"
|
||||
C5_TASK_ID = "__AIOS_GOVERNED_TASK_ID__"
|
||||
|
||||
|
||||
def _load_base():
|
||||
@@ -182,8 +253,8 @@ AIOS_C5_PRINCIPAL_SCOPE_CONFIG=/var/lib/aios-gitops/bridge-v2-staging/releases/R
|
||||
|
||||
ACTIVATION_EXAMPLE = {
|
||||
"schema": "aios.c5-task-principal-activation.v1",
|
||||
"task_id": "TASK-V2R2C5-SOURCE-PUBLICATION-20260926-R1",
|
||||
"agent_id": "agent/codex-c5-release",
|
||||
"task_id": "fixture-governed-task-id",
|
||||
"agent_id": "agent/fixture-c5-release",
|
||||
"execution_digest": "1ba534d551991e8c99014285eeea36afcbc1500686035cb8bcd7f9f4efd6215e",
|
||||
"store_path": "/var/lib/aios-gitops/c5-source-principal/enrollment.sqlite3",
|
||||
"grant_db": "/var/lib/aios-gitops/bridge-v2-staging/task-admission.sqlite3",
|
||||
@@ -249,16 +320,105 @@ def build_issuer_overlay(source: Path, target: Path) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def build_issuer_v3_overlay(
|
||||
*, adapter_source_root: Path, output_root: Path,
|
||||
source_commit: str, repository_root: Path,
|
||||
) -> dict[str, Any]:
|
||||
"""Build from one immutable source commit and pinned live base inputs."""
|
||||
source_commit = source_commit.strip().lower()
|
||||
if len(source_commit) != 40 or any(c not in "0123456789abcdef" for c in source_commit):
|
||||
raise SystemExit("ISSUER_V3_SOURCE_COMMIT_INVALID")
|
||||
repository_root = repository_root.resolve()
|
||||
commit = subprocess.run(
|
||||
["git", "-C", str(repository_root), "rev-parse", "--verify", source_commit + "^{commit}"],
|
||||
check=True, capture_output=True, text=True,
|
||||
).stdout.strip()
|
||||
if commit != source_commit:
|
||||
raise SystemExit("ISSUER_V3_SOURCE_COMMIT_MISMATCH")
|
||||
adapter_source_root = adapter_source_root.resolve()
|
||||
for name, expected in ISSUER_V3_BASE_HASHES.items():
|
||||
source = adapter_source_root / name
|
||||
if source.is_symlink() or not source.is_file() or sha256(source) != expected:
|
||||
raise SystemExit("ISSUER_V3_BASE_HASH_MISMATCH:" + name)
|
||||
output_root = output_root.resolve()
|
||||
if output_root.exists() and any(output_root.iterdir()):
|
||||
raise SystemExit("OUTPUT_ROOT_NOT_EMPTY")
|
||||
app = output_root / "app"
|
||||
app.mkdir(parents=True, exist_ok=True)
|
||||
files: dict[str, str] = {}
|
||||
for path in ISSUER_V3_SOURCE_FILES:
|
||||
raw = subprocess.run(
|
||||
["git", "-C", str(repository_root), "show", source_commit + ":" + path],
|
||||
check=True, capture_output=True,
|
||||
).stdout
|
||||
target = app / Path(path).name
|
||||
target.write_bytes(raw)
|
||||
files["app/" + target.name] = hashlib.sha256(raw).hexdigest()
|
||||
overlay = ISSUER_V3_ADAPTER_OVERLAY.format(
|
||||
base=ISSUER_V3_BASE,
|
||||
hashes=ISSUER_V3_BASE_HASHES,
|
||||
).encode("utf-8")
|
||||
target = app / "aios_task_principal_governed_adapter.py"
|
||||
target.write_bytes(overlay)
|
||||
files["app/" + target.name] = hashlib.sha256(overlay).hexdigest()
|
||||
file_manifest_digest = hashlib.sha256(json.dumps(
|
||||
files, sort_keys=True, separators=(",", ":"),
|
||||
).encode()).hexdigest()
|
||||
archive = output_root / "issuer-v3-overlay.tar"
|
||||
with tarfile.open(archive, mode="w", format=tarfile.USTAR_FORMAT) as bundle:
|
||||
for name in sorted(files):
|
||||
raw = (output_root / name).read_bytes()
|
||||
entry = tarfile.TarInfo(name)
|
||||
entry.size = len(raw)
|
||||
entry.mode = 0o444
|
||||
entry.uid = entry.gid = entry.mtime = 0
|
||||
entry.uname = entry.gname = ""
|
||||
bundle.addfile(entry, io.BytesIO(raw))
|
||||
artifact_digest = sha256(archive)
|
||||
manifest = {
|
||||
"schema": "aios.issuer-v3-receipt-overlay.v1",
|
||||
"source_commit": source_commit,
|
||||
"artifact_sha256": artifact_digest,
|
||||
"artifact_filename": archive.name,
|
||||
"file_manifest_sha256": file_manifest_digest,
|
||||
"files": files,
|
||||
"base_module_root": ISSUER_V3_BASE,
|
||||
"base_module_sha256": ISSUER_V3_BASE_HASHES,
|
||||
"provider": "aios_task_principal_v3_receipt_provider.IssuerV3ReceiptScopeProvider",
|
||||
"activation_effect_performed": False,
|
||||
}
|
||||
(output_root / "overlay-manifest.json").write_text(
|
||||
json.dumps(manifest, sort_keys=True, indent=2) + "\n", encoding="utf-8",
|
||||
)
|
||||
return manifest
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--principal-source-root", type=Path, required=True)
|
||||
parser.add_argument("--principal-source-root", type=Path)
|
||||
parser.add_argument(
|
||||
"--issuer-source", type=Path,
|
||||
help="Exact issuer server source; defaults to principal-source-root/aios_task_principal_issuer_server.py",
|
||||
)
|
||||
parser.add_argument("--adapter-source-root", type=Path, required=True)
|
||||
parser.add_argument("--adapter-source-root", type=Path)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
parser.add_argument("--issuer-v3-only", action="store_true")
|
||||
parser.add_argument("--source-commit")
|
||||
parser.add_argument("--task-id")
|
||||
parser.add_argument("--agent-id")
|
||||
args = parser.parse_args()
|
||||
if args.issuer_v3_only:
|
||||
if args.adapter_source_root is None or not args.source_commit:
|
||||
parser.error("--issuer-v3-only requires --adapter-source-root and --source-commit")
|
||||
build_issuer_v3_overlay(
|
||||
adapter_source_root=args.adapter_source_root,
|
||||
output_root=args.output_root,
|
||||
source_commit=args.source_commit,
|
||||
repository_root=Path(__file__).resolve().parents[1],
|
||||
)
|
||||
return 0
|
||||
if args.principal_source_root is None or args.adapter_source_root is None:
|
||||
parser.error("--principal-source-root and --adapter-source-root are required")
|
||||
principal = args.principal_source_root.resolve()
|
||||
adapter = args.adapter_source_root.resolve()
|
||||
output = args.output_root.resolve()
|
||||
@@ -274,7 +434,15 @@ def main() -> int:
|
||||
"BASE_HASH_MISMATCH:aios_task_principal_admission.py:" + observed
|
||||
)
|
||||
admission_target = app / "aios_task_principal_admission.py"
|
||||
admission_target.write_text(ADMISSION_OVERLAY, encoding="utf-8", newline="")
|
||||
if not args.task_id or not args.agent_id:
|
||||
raise SystemExit("TASK_AND_AGENT_BINDINGS_REQUIRED")
|
||||
if any(character not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.:-/"
|
||||
for value in (args.task_id, args.agent_id) for character in value):
|
||||
raise SystemExit("TASK_OR_AGENT_BINDING_INVALID")
|
||||
admission_target.write_text(
|
||||
ADMISSION_OVERLAY.replace("__AIOS_GOVERNED_TASK_ID__", args.task_id),
|
||||
encoding="utf-8", newline="",
|
||||
)
|
||||
|
||||
outputs: dict[str, Any] = {
|
||||
"aios_task_principal_admission.py": {
|
||||
@@ -317,14 +485,15 @@ def main() -> int:
|
||||
env_path = config_dir / "c5-principal-scope.env.example"
|
||||
env_path.write_text(ENV_EXAMPLE, encoding="utf-8", newline="")
|
||||
activation_path = config_dir / "principal-activation.example.json"
|
||||
activation = {**ACTIVATION_EXAMPLE, "task_id": args.task_id, "agent_id": args.agent_id}
|
||||
activation_path.write_text(
|
||||
json.dumps(ACTIVATION_EXAMPLE, sort_keys=True, separators=(",", ":")),
|
||||
json.dumps(activation, sort_keys=True, separators=(",", ":")),
|
||||
encoding="utf-8", newline="",
|
||||
)
|
||||
|
||||
manifest = {
|
||||
"schema": "aios.c5-task-principal-overlay.v1",
|
||||
"task_id": "TASK-V2R2C5-SOURCE-PUBLICATION-20260926-R1",
|
||||
"task_id": args.task_id,
|
||||
"execution_digest": "1ba534d551991e8c99014285eeea36afcbc1500686035cb8bcd7f9f4efd6215e",
|
||||
"files": outputs,
|
||||
"entrypoints": {
|
||||
|
||||
@@ -29,7 +29,7 @@ UNIT = "aios-chatgpt-bridge-v2-staging.service"
|
||||
BRIDGE_TARGET = "bridge-18892"
|
||||
ISSUER_TARGET = "principal-issuer-18946"
|
||||
ISSUER_UNIT = "aios-task-principal-issuer.service"
|
||||
DROPIN_NAME = "~" * 32 + "-c5-source.conf"
|
||||
DROPIN_NAME = "~" * 128 + "-c5-r14-source.conf"
|
||||
DROPIN = f"/etc/systemd/system/{UNIT}.d/{DROPIN_NAME}"
|
||||
ISSUER_DROPIN = f"/etc/systemd/system/{ISSUER_UNIT}.d/{DROPIN_NAME}"
|
||||
RUNTIME_URL = "http://127.0.0.1:18892/v2/runtime"
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
"""Read the expired R2 V3 plan and producer binding without a write path."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "tests" / "fixtures" / "issuer-v3-base"))
|
||||
sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
|
||||
|
||||
from governed_runtime_scope import ( # noqa: E402
|
||||
ExecutionSnapshot, GrantSnapshot, GovernedScopeError, TaskSnapshot, TrustSnapshot,
|
||||
)
|
||||
from aios_task_principal_v3_receipt_provider import IssuerV3ReceiptScopeProvider # noqa: E402
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--grant-db", type=Path, required=True)
|
||||
parser.add_argument("--plan-dir", type=Path, required=True)
|
||||
parser.add_argument("--producer-root", type=Path, required=True)
|
||||
parser.add_argument("--task-id", required=True)
|
||||
parser.add_argument("--execution-digest", required=True)
|
||||
parser.add_argument("--job-id", required=True)
|
||||
args = parser.parse_args()
|
||||
if len(args.execution_digest) != 64 or any(c not in "0123456789abcdef" for c in args.execution_digest):
|
||||
raise SystemExit("EXECUTION_DIGEST_INVALID")
|
||||
# mode=ro does not create the database; no producer or issuer service call.
|
||||
with sqlite3.connect(f"file:{args.grant_db.as_posix()}?mode=ro", uri=True) as db:
|
||||
rows = db.execute("SELECT grant_json FROM task_admission_grants").fetchall()
|
||||
matches = [
|
||||
json.loads(row[0]) for row in rows
|
||||
if json.loads(row[0]).get("task_id") == args.task_id
|
||||
and json.loads(row[0]).get("execution_digest") == args.execution_digest
|
||||
]
|
||||
if len(matches) != 1:
|
||||
raise SystemExit("R2_EXACT_GRANT_NOT_UNIQUE")
|
||||
grant = matches[0]
|
||||
raw_plan = (args.plan_dir / (args.execution_digest + ".json")).read_bytes()
|
||||
plan = json.loads(raw_plan)
|
||||
if hashlib.sha256(raw_plan).hexdigest() != args.execution_digest:
|
||||
raise SystemExit("R2_ORIGINAL_PLAN_DIGEST_MISMATCH")
|
||||
provider = IssuerV3ReceiptScopeProvider(
|
||||
legacy=None, producer_state_root=args.producer_root,
|
||||
plan_root=args.plan_dir, grant_reader=lambda **_kwargs: grant,
|
||||
)
|
||||
job_id, proof = provider.inspect_v3_binding(grant_raw=grant, plan=plan)
|
||||
if job_id != args.job_id or proof.get("job_id") != args.job_id:
|
||||
raise SystemExit("R2_PRODUCER_JOB_MISMATCH")
|
||||
task = TaskSnapshot(args.task_id, "EXPIRED")
|
||||
execution = ExecutionSnapshot(
|
||||
args.task_id, args.job_id, grant["repository"], grant["target_branch"],
|
||||
grant["generation"], args.execution_digest,
|
||||
)
|
||||
snapshot = GrantSnapshot(
|
||||
grant["grant_id"], "ADMITTED", args.task_id, grant["repository"],
|
||||
grant["authority_base_oid"], grant["target_branch"],
|
||||
grant["generation"], args.execution_digest, tuple(grant["capabilities"]),
|
||||
tuple(grant["file_allowlist"]), int(grant["expires_at"]),
|
||||
)
|
||||
stale_code = None
|
||||
try:
|
||||
provider.resolve(
|
||||
task=task, execution=execution, grant=snapshot, plan=plan,
|
||||
trust=TrustSnapshot("fixture", "fixture", "fixture", "TASK_PRINCIPAL", "fixture"),
|
||||
now=int(grant["expires_at"]) + 1,
|
||||
)
|
||||
except GovernedScopeError as exc:
|
||||
stale_code = str(exc)
|
||||
if stale_code != "TASK_NOT_CLAIMED":
|
||||
raise SystemExit("R2_EXPIRED_STATE_NOT_REJECTED")
|
||||
print(json.dumps({
|
||||
"schema": plan.get("schema"), "execution_digest": args.execution_digest,
|
||||
"producer_job_id": job_id, "receipt_binding": "EXACT_READBACK",
|
||||
"expired_state_rejection": stale_code, "mutation_performed": False,
|
||||
}, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -6,7 +6,7 @@ import pytest
|
||||
|
||||
from app.task_plane.api import TaskPlaneService
|
||||
from app.task_plane.errors import ConflictError, LockedError
|
||||
from app.task_plane.policy import ALL_ACTION_CAPABILITIES, PolicyEngine
|
||||
from app.task_plane.policy import ALL_ACTION_CAPABILITIES, IdentityVerifier, PolicyEngine
|
||||
from app.task_plane.store import TaskPlaneStore
|
||||
|
||||
|
||||
@@ -19,7 +19,9 @@ def claimed_task():
|
||||
service = TaskPlaneService(
|
||||
store,
|
||||
policy=PolicyEngine(
|
||||
identity_verifier=IdentityVerifier({"anon", IDENTITY}),
|
||||
execution_enabled=True,
|
||||
evidence_attestation_verifier=lambda _envelope: True,
|
||||
identity_capabilities={"*": set(ALL_ACTION_CAPABILITIES)},
|
||||
),
|
||||
)
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"authority_base_oid":"b915386ccbed46a7ae77220887da8ae666649a79","capabilities":["agent-task:control","governed.task.create","governed.write.files","task.principal.issue","writer.lease"],"file_allowlist":["active-runtime/app/aios_capability_contract.py","tests/test_v2r2c5_capability_contract.py"],"file_sha256":{"active-runtime/app/aios_capability_contract.py":"67d8a3a296110e20a4ea59863cd7982b6bdada71abd457c0f7cec480b06d278a","tests/test_v2r2c5_capability_contract.py":"f91af641528ea9a625ca45a46b6c26f5dba568e6b69cba819ca33ab5cba28de3"},"generation":1,"repository":"aios/aios","risk":"engineering","schema":"aios.task-admission-execution-plan.v3","target_branch":"fixture/c5-r14-source-publication","task_id":"fixture-c5-r14-source-publication"}
|
||||
@@ -0,0 +1,180 @@
|
||||
from __future__ import annotations
|
||||
|
||||
"""Versioned TaskPrincipal admission overlay for governed runtime scopes.
|
||||
|
||||
This module DOES NOT fork or modify FINAL_SOURCE. It loads the existing exact9
|
||||
admission chain from its immutable versioned path, then applies two surgical
|
||||
runtime patches to the underlying principal_legacy module:
|
||||
|
||||
1. static-first `_scope_and_trust` fallback, only for exact task-binding mismatch;
|
||||
2. assertion TTL pre-clamp for runtime grant-backed scopes.
|
||||
|
||||
The original exact9/r10/legacy chain remains the source of every other behavior.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping
|
||||
import importlib.util
|
||||
import os
|
||||
|
||||
from aios_task_principal_governed_adapter import (
|
||||
GovernedScopeRuntimeAdapter,
|
||||
config_from_environment,
|
||||
)
|
||||
from governed_scope_hook import (
|
||||
clamp_assertion_expiry,
|
||||
is_exact_scope_binding_mismatch,
|
||||
static_first_scope,
|
||||
)
|
||||
|
||||
DEFAULT_EXACT9_ADMISSION = (
|
||||
"/var/lib/aios-gitops/bridge-v2-staging/releases/"
|
||||
"formal-lineage-exact9-principal-20260907-01/app/"
|
||||
"aios_task_principal_admission.py"
|
||||
)
|
||||
BASE_PATH = Path(
|
||||
os.environ.get("AIOS_GOVERNED_EXACT9_ADMISSION_BASE", DEFAULT_EXACT9_ADMISSION)
|
||||
)
|
||||
|
||||
if not BASE_PATH.is_file():
|
||||
raise RuntimeError(f"GOVERNED_EXACT9_BASE_MISSING:{BASE_PATH}")
|
||||
|
||||
_spec = importlib.util.spec_from_file_location(
|
||||
"_aios_task_principal_exact9_base_for_governed", BASE_PATH
|
||||
)
|
||||
if _spec is None or _spec.loader is None:
|
||||
raise RuntimeError("GOVERNED_EXACT9_BASE_SPEC_INVALID")
|
||||
_base = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(_base)
|
||||
|
||||
# Preserve the complete existing admission API surface.
|
||||
for _name, _value in _base.__dict__.items():
|
||||
if not _name.startswith("__"):
|
||||
globals()[_name] = _value
|
||||
|
||||
_legacy = _base._legacy
|
||||
_ORIGINAL_SCOPE_AND_TRUST = _legacy._scope_and_trust
|
||||
_ORIGINAL_ISSUE = _legacy.TaskPrincipalIssuer.issue
|
||||
_GOVERNED_RUNTIME_ADAPTER: GovernedScopeRuntimeAdapter | None = None
|
||||
|
||||
|
||||
def _audience(trust: Mapping[str, Any], operation: str) -> str:
|
||||
bridge_id = str(trust.get("bridge_id", "")).strip()
|
||||
operation = str(operation).strip()
|
||||
if not bridge_id or not operation:
|
||||
raise RuntimeError("GOVERNED_AUDIENCE_INPUT_INVALID")
|
||||
return f"aios.gitops.bridge.v2/{bridge_id}/{operation}"
|
||||
|
||||
|
||||
def set_governed_runtime_adapter(
|
||||
adapter: GovernedScopeRuntimeAdapter | None,
|
||||
) -> None:
|
||||
global _GOVERNED_RUNTIME_ADAPTER
|
||||
_GOVERNED_RUNTIME_ADAPTER = adapter
|
||||
|
||||
|
||||
def governed_runtime_adapter() -> GovernedScopeRuntimeAdapter | None:
|
||||
return _GOVERNED_RUNTIME_ADAPTER
|
||||
|
||||
|
||||
def _governed_scope_and_trust(
|
||||
con, trust, task, now, *, lease_token=None, pepper=None
|
||||
):
|
||||
def _static():
|
||||
return _legacy._static_scope_and_trust(con, trust, task)
|
||||
|
||||
def _governed():
|
||||
if _GOVERNED_RUNTIME_ADAPTER is None:
|
||||
# Preserve the original denial when governed runtime mode is off.
|
||||
_legacy._fail("SCOPE_TASK_BINDING_MISMATCH")
|
||||
execution = _legacy._execution(task)
|
||||
return _GOVERNED_RUNTIME_ADAPTER.resolve_row(
|
||||
task=task, execution=execution, trust=trust, now=now
|
||||
)
|
||||
|
||||
scope = static_first_scope(
|
||||
static_resolver=_static,
|
||||
governed_resolver=_governed,
|
||||
)
|
||||
claim_digest = _legacy._claim(task, lease_token, pepper, now)
|
||||
_legacy._check_revocations(con, task, scope, trust, None)
|
||||
return scope, claim_digest
|
||||
|
||||
|
||||
def _maybe_clamp_request_ttl(self, request):
|
||||
if _GOVERNED_RUNTIME_ADAPTER is None:
|
||||
return request
|
||||
|
||||
x = _legacy._json_object(request, "ISSUE_REQUEST_INVALID")
|
||||
task_id = _legacy._text(x.get("task_id"), "task_id")
|
||||
ttl = _legacy._integer(
|
||||
x.get("requested_ttl_seconds"), "requested_ttl_seconds", 1, 300
|
||||
)
|
||||
now = _legacy._now_epoch(self.clock)
|
||||
|
||||
with _legacy._db(self.db_path) as con:
|
||||
_legacy._ready(con)
|
||||
task = con.execute(
|
||||
"SELECT * FROM tasks WHERE task_id=?", (task_id,)
|
||||
).fetchone()
|
||||
if task is None:
|
||||
return request
|
||||
|
||||
try:
|
||||
_legacy._static_scope_and_trust(con, self.trust, task)
|
||||
# Historical exact9/static path: retain original TTL semantics.
|
||||
return request
|
||||
except Exception as exc:
|
||||
if not is_exact_scope_binding_mismatch(exc):
|
||||
raise
|
||||
|
||||
execution = _legacy._execution(task)
|
||||
scope = _GOVERNED_RUNTIME_ADAPTER.resolve_row(
|
||||
task=task, execution=execution, trust=self.trust, now=now
|
||||
)
|
||||
lease_expires_at = _legacy._lease_expiry(task["lease_expires_at"])
|
||||
expires_at = clamp_assertion_expiry(
|
||||
normal_expires_at=now + ttl,
|
||||
scope=scope,
|
||||
task_lease_expires_at=lease_expires_at,
|
||||
now=now,
|
||||
)
|
||||
effective_ttl = int(expires_at) - int(now)
|
||||
if effective_ttl < 1 or effective_ttl > ttl:
|
||||
_legacy._fail("TTL_INVALID")
|
||||
|
||||
# Preserve all caller fields; change only the TTL downward.
|
||||
adjusted = dict(x)
|
||||
adjusted["requested_ttl_seconds"] = effective_ttl
|
||||
return adjusted
|
||||
|
||||
|
||||
def _governed_issue(self, request, task_lease):
|
||||
return _ORIGINAL_ISSUE(
|
||||
self, _maybe_clamp_request_ttl(self, request), task_lease
|
||||
)
|
||||
|
||||
|
||||
# Patch ONLY the legacy entrypoints used by issue() and verify().
|
||||
_legacy._scope_and_trust = _governed_scope_and_trust
|
||||
_legacy.TaskPrincipalIssuer.issue = _governed_issue
|
||||
|
||||
# Also expose the patched symbols through this admission module for diagnostics.
|
||||
globals()["_scope_and_trust"] = _governed_scope_and_trust
|
||||
globals()["TaskPrincipalIssuer"] = _legacy.TaskPrincipalIssuer
|
||||
|
||||
_cfg = config_from_environment()
|
||||
if _cfg is not None:
|
||||
set_governed_runtime_adapter(
|
||||
GovernedScopeRuntimeAdapter(
|
||||
_cfg,
|
||||
audience_builder=_audience,
|
||||
operations=dict(_legacy.OPERATIONS),
|
||||
)
|
||||
)
|
||||
|
||||
GOVERNED_RUNTIME_SCOPE_OVERLAY = {
|
||||
"schema": "aios.governed-runtime-scope-overlay.v1",
|
||||
"base_admission": str(BASE_PATH),
|
||||
"enabled": _cfg is not None,
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
from __future__ import annotations
|
||||
|
||||
"""Read-only live adapters for governed runtime TaskPrincipal scopes."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Mapping
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import stat
|
||||
|
||||
from governed_runtime_scope import (
|
||||
ExecutionSnapshot, GovernedRuntimeScopeProvider, GrantSnapshot,
|
||||
TaskSnapshot, TrustSnapshot, sha256_json,
|
||||
)
|
||||
|
||||
|
||||
class GovernedAdapterError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def _value(obj: Any, name: str, default: Any = None) -> Any:
|
||||
if isinstance(obj, Mapping):
|
||||
return obj.get(name, default)
|
||||
try:
|
||||
return obj[name]
|
||||
except (KeyError, IndexError, TypeError):
|
||||
return getattr(obj, name, default)
|
||||
|
||||
|
||||
def _safe_regular(
|
||||
path: Path, *, allow_group_write_gid: int | None = None
|
||||
) -> os.stat_result:
|
||||
st = os.lstat(path)
|
||||
if stat.S_ISLNK(st.st_mode) or not stat.S_ISREG(st.st_mode):
|
||||
raise GovernedAdapterError(f"UNSAFE_FILE:{path}")
|
||||
mode = stat.S_IMODE(st.st_mode)
|
||||
if mode & 0o002:
|
||||
raise GovernedAdapterError(f"WORLD_WRITABLE_FILE:{path}")
|
||||
if mode & 0o020 and (
|
||||
allow_group_write_gid is None or st.st_gid != int(allow_group_write_gid)
|
||||
):
|
||||
raise GovernedAdapterError(f"UNEXPECTED_GROUP_WRITABLE_FILE:{path}")
|
||||
return st
|
||||
|
||||
|
||||
def _safe_plan_dir(path: Path, *, require_root_owner: bool = True) -> os.stat_result:
|
||||
st = os.lstat(path)
|
||||
if stat.S_ISLNK(st.st_mode) or not stat.S_ISDIR(st.st_mode):
|
||||
raise GovernedAdapterError(f"UNSAFE_PLAN_DIR:{path}")
|
||||
if require_root_owner and st.st_uid != 0:
|
||||
raise GovernedAdapterError(f"PLAN_DIR_NOT_ROOT_OWNED:{path}")
|
||||
if stat.S_IMODE(st.st_mode) & 0o022:
|
||||
raise GovernedAdapterError(f"PLAN_DIR_WRITABLE_BY_GROUP_OR_WORLD:{path}")
|
||||
return st
|
||||
|
||||
|
||||
class ReadOnlyTaskAdmissionGrantStore:
|
||||
def __init__(
|
||||
self, db_path: str | Path, *,
|
||||
allowed_group_write_gid: int | None = None
|
||||
):
|
||||
self.db_path = Path(db_path)
|
||||
self.allowed_group_write_gid = allowed_group_write_gid
|
||||
|
||||
def _connect(self) -> sqlite3.Connection:
|
||||
_safe_regular(
|
||||
self.db_path, allow_group_write_gid=self.allowed_group_write_gid
|
||||
)
|
||||
con = sqlite3.connect(
|
||||
f"file:{self.db_path.as_posix()}?mode=ro",
|
||||
uri=True, timeout=3.0
|
||||
)
|
||||
con.row_factory = sqlite3.Row
|
||||
return con
|
||||
|
||||
def require_current_grant(
|
||||
self, *, task_id: str, execution_digest: str, repository: str,
|
||||
target_branch: str, generation: int, now: int
|
||||
) -> Mapping[str, Any]:
|
||||
candidates: list[tuple[tuple[int, int, str], dict[str, Any]]] = []
|
||||
with self._connect() as con:
|
||||
rows = con.execute(
|
||||
"SELECT grant_id, grant_json FROM task_admission_grants"
|
||||
).fetchall()
|
||||
|
||||
for row in rows:
|
||||
try:
|
||||
grant = json.loads(str(row["grant_json"]))
|
||||
except Exception as exc:
|
||||
raise GovernedAdapterError("GRANT_STORE_JSON_INVALID") from exc
|
||||
if grant.get("schema") != "aios.task-admission-grant.v1":
|
||||
continue
|
||||
if (
|
||||
str(grant.get("task_id")) != str(task_id)
|
||||
or str(grant.get("execution_digest")) != str(execution_digest)
|
||||
or str(grant.get("repository")) != str(repository)
|
||||
or str(grant.get("target_branch")) != str(target_branch)
|
||||
or int(grant.get("generation", 0)) != int(generation)
|
||||
):
|
||||
continue
|
||||
if str(grant.get("grant_id")) != str(row["grant_id"]):
|
||||
raise GovernedAdapterError("GRANT_ID_STORAGE_MISMATCH")
|
||||
expires = int(grant.get("expires_at", 0))
|
||||
issued = int(grant.get("issued_at", 0))
|
||||
if expires <= int(now):
|
||||
continue
|
||||
candidates.append(((issued, expires, str(grant["grant_id"])), grant))
|
||||
|
||||
if not candidates:
|
||||
raise GovernedAdapterError("CURRENT_ADMITTED_GRANT_NOT_FOUND")
|
||||
candidates.sort(key=lambda item: item[0], reverse=True)
|
||||
best_key, best = candidates[0]
|
||||
if len(candidates) > 1 and candidates[1][0][:2] == best_key[:2]:
|
||||
raise GovernedAdapterError("CURRENT_ADMITTED_GRANT_AMBIGUOUS")
|
||||
return best
|
||||
|
||||
|
||||
class ReadOnlyExecutionPlanStore:
|
||||
def __init__(self, root: str | Path, *, require_root_owner: bool = True):
|
||||
self.root = Path(root)
|
||||
self.require_root_owner = require_root_owner
|
||||
|
||||
def require_plan(self, execution_digest: str) -> Mapping[str, Any]:
|
||||
_safe_plan_dir(self.root, require_root_owner=self.require_root_owner)
|
||||
digest = str(execution_digest).strip().lower()
|
||||
if len(digest) != 64 or any(c not in "0123456789abcdef" for c in digest):
|
||||
raise GovernedAdapterError("EXECUTION_DIGEST_INVALID")
|
||||
path = self.root / f"{digest}.json"
|
||||
st = _safe_regular(path)
|
||||
if self.require_root_owner and st.st_uid != 0:
|
||||
raise GovernedAdapterError("EXECUTION_PLAN_NOT_ROOT_OWNED")
|
||||
if stat.S_IMODE(st.st_mode) & 0o022:
|
||||
raise GovernedAdapterError("EXECUTION_PLAN_WRITABLE_BY_GROUP_OR_WORLD")
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except Exception as exc:
|
||||
raise GovernedAdapterError("EXECUTION_PLAN_JSON_INVALID") from exc
|
||||
if not isinstance(value, Mapping):
|
||||
raise GovernedAdapterError("EXECUTION_PLAN_NOT_OBJECT")
|
||||
if sha256_json(value) != digest:
|
||||
raise GovernedAdapterError("EXECUTION_PLAN_DIGEST_MISMATCH")
|
||||
return value
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GovernedScopeRuntimeConfig:
|
||||
grant_db: Path
|
||||
plan_dir: Path
|
||||
grant_db_allowed_group_gid: int | None = None
|
||||
require_root_plan_owner: bool = True
|
||||
|
||||
|
||||
class GovernedScopeRuntimeAdapter:
|
||||
def __init__(
|
||||
self, config: GovernedScopeRuntimeConfig, *,
|
||||
audience_builder: Callable[[Mapping[str, Any], str], str],
|
||||
provider: GovernedRuntimeScopeProvider | None = None,
|
||||
operations: Mapping[str, str] | None = None,
|
||||
):
|
||||
self.grants = ReadOnlyTaskAdmissionGrantStore(
|
||||
config.grant_db,
|
||||
allowed_group_write_gid=config.grant_db_allowed_group_gid
|
||||
)
|
||||
self.plans = ReadOnlyExecutionPlanStore(
|
||||
config.plan_dir, require_root_owner=config.require_root_plan_owner
|
||||
)
|
||||
self.provider = provider or GovernedRuntimeScopeProvider()
|
||||
self.audience_builder = audience_builder
|
||||
self.operations = dict(operations or {})
|
||||
|
||||
def resolve_row(
|
||||
self, *, task: Any, execution: Any,
|
||||
trust: Mapping[str, Any], now: int
|
||||
) -> dict[str, Any]:
|
||||
task_id = str(_value(task, "task_id", "")).strip()
|
||||
status = str(_value(task, "status", "")).strip()
|
||||
exec_snap = ExecutionSnapshot(
|
||||
task_id=task_id,
|
||||
external_job_id=str(_value(execution, "external_job_id", "")).strip(),
|
||||
repository=str(_value(execution, "repository", "")).strip(),
|
||||
target_branch=str(_value(execution, "target_branch", "")).strip(),
|
||||
generation=int(_value(execution, "generation", 0)),
|
||||
execution_digest=str(_value(execution, "execution_digest", "")).strip(),
|
||||
)
|
||||
grant_raw = self.grants.require_current_grant(
|
||||
task_id=task_id,
|
||||
execution_digest=exec_snap.execution_digest,
|
||||
repository=exec_snap.repository,
|
||||
target_branch=exec_snap.target_branch,
|
||||
generation=exec_snap.generation,
|
||||
now=now,
|
||||
)
|
||||
caps = (
|
||||
grant_raw.get("capabilities")
|
||||
or grant_raw.get("requested_capabilities")
|
||||
or []
|
||||
)
|
||||
files = grant_raw.get("file_allowlist") or []
|
||||
grant = GrantSnapshot(
|
||||
grant_id=str(grant_raw["grant_id"]),
|
||||
status="ADMITTED",
|
||||
task_id=str(grant_raw["task_id"]),
|
||||
repository=str(grant_raw["repository"]),
|
||||
authority_base_oid=str(grant_raw["authority_base_oid"]),
|
||||
target_branch=str(grant_raw["target_branch"]),
|
||||
generation=int(grant_raw["generation"]),
|
||||
execution_digest=str(grant_raw["execution_digest"]),
|
||||
requested_capabilities=tuple(map(str, caps)),
|
||||
file_allowlist=tuple(map(str, files)),
|
||||
expires_at=int(grant_raw["expires_at"]),
|
||||
)
|
||||
plan = self.plans.require_plan(exec_snap.execution_digest)
|
||||
|
||||
trust_manifest = dict(trust)
|
||||
trust_snap = TrustSnapshot(
|
||||
issuer_id=str(trust.get("issuer_id", "")),
|
||||
key_id=str(trust.get("key_id", "")),
|
||||
public_key=str(trust.get("public_key", "")),
|
||||
purpose=str(trust.get("purpose", "")),
|
||||
bridge_id=str(trust.get("bridge_id", "")),
|
||||
scope_manifest_sha256=sha256_json(trust_manifest),
|
||||
manifest=trust_manifest,
|
||||
)
|
||||
derived = self.provider.resolve(
|
||||
task=TaskSnapshot(task_id, status),
|
||||
execution=exec_snap,
|
||||
grant=grant,
|
||||
plan=plan,
|
||||
trust=trust_snap,
|
||||
now=int(now),
|
||||
)
|
||||
|
||||
if self.operations:
|
||||
allowed = set(derived.capabilities)
|
||||
audiences = sorted({
|
||||
str(self.audience_builder(trust, operation))
|
||||
for operation, capability in self.operations.items()
|
||||
if capability in allowed
|
||||
})
|
||||
else:
|
||||
# Backward-compatible generic mode. Live issuer integration should
|
||||
# supply OPERATIONS so audience identities are operation-level.
|
||||
audiences = sorted({
|
||||
str(self.audience_builder(trust, capability))
|
||||
for capability in derived.capabilities
|
||||
})
|
||||
if not audiences or any(not a for a in audiences):
|
||||
raise GovernedAdapterError("AUDIENCE_BUILD_FAILED")
|
||||
|
||||
return {
|
||||
"scope_id": derived.scope_id,
|
||||
"task_id": derived.task_id,
|
||||
"external_job_id": derived.external_job_id,
|
||||
"repository": derived.repository,
|
||||
"target_branch": derived.target_branch,
|
||||
"generation": derived.generation,
|
||||
"execution_digest": derived.execution_digest,
|
||||
"capabilities_json": json.dumps(
|
||||
list(derived.capabilities), ensure_ascii=False,
|
||||
sort_keys=True, separators=(",", ":")
|
||||
),
|
||||
"audiences_json": json.dumps(
|
||||
audiences, ensure_ascii=False,
|
||||
sort_keys=True, separators=(",", ":")
|
||||
),
|
||||
"trust_manifest_sha256": derived.trust_manifest_sha256,
|
||||
"grant_id": derived.grant_id,
|
||||
"grant_expires_at": derived.grant_expires_at,
|
||||
"scope_origin": derived.origin,
|
||||
}
|
||||
|
||||
|
||||
def config_from_environment() -> GovernedScopeRuntimeConfig | None:
|
||||
if os.environ.get("AIOS_GOVERNED_RUNTIME_SCOPE_ENABLED", "").strip() != "1":
|
||||
return None
|
||||
db = os.environ.get("AIOS_GOVERNED_TASK_ADMISSION_DB", "").strip()
|
||||
plan = os.environ.get("AIOS_GOVERNED_EXECUTION_PLAN_DIR", "").strip()
|
||||
if not db or not plan:
|
||||
raise GovernedAdapterError("GOVERNED_SCOPE_ENV_INCOMPLETE")
|
||||
gid_raw = os.environ.get(
|
||||
"AIOS_GOVERNED_GRANT_DB_ALLOWED_GROUP_GID", ""
|
||||
).strip()
|
||||
return GovernedScopeRuntimeConfig(
|
||||
grant_db=Path(db),
|
||||
plan_dir=Path(plan),
|
||||
grant_db_allowed_group_gid=int(gid_raw) if gid_raw else None,
|
||||
)
|
||||
@@ -0,0 +1,315 @@
|
||||
from __future__ import annotations
|
||||
|
||||
"""Pure fail-closed runtime scope derivation for newly authorized governed tasks.
|
||||
|
||||
Supports:
|
||||
- aios.task-admission.execution-plan.v1
|
||||
- aios.task-admission-execution-plan.v2 (live Round-3)
|
||||
|
||||
The ORIGINAL materialized plan mapping is always hashed for execution identity.
|
||||
Schema adaptation is read-only parsing and never rewrites canonical plan bytes.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Mapping, Sequence
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
|
||||
HEX64 = re.compile(r"^[0-9a-f]{64}$")
|
||||
OID40 = re.compile(r"^[0-9a-f]{40}$")
|
||||
TASK_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:@/-]{2,255}$")
|
||||
BRANCH_RE = re.compile(r"^(?:agent|task)/[A-Za-z0-9._/-]+$")
|
||||
JOB_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:@/-]{2,255}$")
|
||||
GRANT_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:@/-]{2,255}$")
|
||||
PLAN_V1 = "aios.task-admission.execution-plan.v1"
|
||||
PLAN_V2 = "aios.task-admission-execution-plan.v2"
|
||||
|
||||
|
||||
class GovernedScopeError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TaskSnapshot:
|
||||
task_id: str
|
||||
status: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ExecutionSnapshot:
|
||||
task_id: str
|
||||
external_job_id: str
|
||||
repository: str
|
||||
target_branch: str
|
||||
generation: int
|
||||
execution_digest: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GrantSnapshot:
|
||||
grant_id: str
|
||||
status: str
|
||||
task_id: str
|
||||
repository: str
|
||||
authority_base_oid: str
|
||||
target_branch: str
|
||||
generation: int
|
||||
execution_digest: str
|
||||
requested_capabilities: tuple[str, ...]
|
||||
file_allowlist: tuple[str, ...]
|
||||
expires_at: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TrustSnapshot:
|
||||
issuer_id: str
|
||||
key_id: str
|
||||
public_key: str
|
||||
purpose: str
|
||||
bridge_id: str
|
||||
scope_manifest_sha256: str | None = None
|
||||
manifest: Mapping[str, Any] | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RuntimeScope:
|
||||
scope_id: str
|
||||
task_id: str
|
||||
external_job_id: str
|
||||
repository: str
|
||||
target_branch: str
|
||||
generation: int
|
||||
execution_digest: str
|
||||
capabilities: tuple[str, ...]
|
||||
trust_manifest_sha256: str
|
||||
grant_id: str
|
||||
grant_expires_at: int
|
||||
origin: str = "TASK_ADMISSION_GRANT"
|
||||
|
||||
|
||||
def canonical_json_bytes(value: Any) -> bytes:
|
||||
return json.dumps(
|
||||
value, ensure_ascii=False, sort_keys=True, separators=(",", ":")
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def sha256_json(value: Any) -> str:
|
||||
return hashlib.sha256(canonical_json_bytes(value)).hexdigest()
|
||||
|
||||
|
||||
def _task_id(value: Any) -> str:
|
||||
s = str(value).strip()
|
||||
if not TASK_RE.fullmatch(s):
|
||||
raise GovernedScopeError("TASK_ID_INVALID")
|
||||
return s
|
||||
|
||||
|
||||
def _job_id(value: Any) -> str:
|
||||
s = str(value).strip()
|
||||
if not JOB_RE.fullmatch(s) or s.lower() in {
|
||||
"tbd", "todo", "unknown", "placeholder", "none", "null"
|
||||
}:
|
||||
raise GovernedScopeError("EXTERNAL_JOB_ID_INVALID")
|
||||
return s
|
||||
|
||||
|
||||
def _branch(value: Any) -> str:
|
||||
s = str(value).strip()
|
||||
if not BRANCH_RE.fullmatch(s) or ".." in s or "//" in s:
|
||||
raise GovernedScopeError("TARGET_BRANCH_INVALID")
|
||||
return s
|
||||
|
||||
|
||||
def _hex64(value: Any, name: str) -> str:
|
||||
s = str(value).strip().lower()
|
||||
if not HEX64.fullmatch(s):
|
||||
raise GovernedScopeError(f"{name.upper()}_INVALID")
|
||||
return s
|
||||
|
||||
|
||||
def _oid40(value: Any, name: str) -> str:
|
||||
s = str(value).strip().lower()
|
||||
if not OID40.fullmatch(s):
|
||||
raise GovernedScopeError(f"{name.upper()}_INVALID")
|
||||
return s
|
||||
|
||||
|
||||
def _generation(value: Any) -> int:
|
||||
try:
|
||||
n = int(value)
|
||||
except (TypeError, ValueError):
|
||||
raise GovernedScopeError("GENERATION_INVALID")
|
||||
if n < 1:
|
||||
raise GovernedScopeError("GENERATION_INVALID")
|
||||
return n
|
||||
|
||||
|
||||
def _normalize_caps(values: Sequence[Any]) -> tuple[str, ...]:
|
||||
caps = tuple(sorted({str(v).strip() for v in values if str(v).strip()}))
|
||||
if not caps:
|
||||
raise GovernedScopeError("CAPABILITIES_EMPTY")
|
||||
return caps
|
||||
|
||||
|
||||
def _normalize_files(values: Sequence[Any]) -> tuple[str, ...]:
|
||||
files = []
|
||||
for raw in values:
|
||||
p = str(raw).strip().replace("\\", "/")
|
||||
if not p or p == "." or p.startswith("/") or p.endswith("/"):
|
||||
raise GovernedScopeError(f"FILE_ALLOWLIST_PATH_INVALID:{p}")
|
||||
parts = p.split("/")
|
||||
if any(part in ("", ".", "..") for part in parts) or ".git" in parts:
|
||||
raise GovernedScopeError(f"FILE_ALLOWLIST_PATH_INVALID:{p}")
|
||||
files.append(p)
|
||||
if not files or len(set(files)) != len(files):
|
||||
raise GovernedScopeError("FILE_ALLOWLIST_INVALID")
|
||||
return tuple(sorted(files))
|
||||
|
||||
|
||||
def _trust_digest(trust: TrustSnapshot) -> str:
|
||||
if trust.manifest is not None:
|
||||
return sha256_json(dict(trust.manifest))
|
||||
return sha256_json({
|
||||
"issuer_id": trust.issuer_id,
|
||||
"key_id": trust.key_id,
|
||||
"public_key": trust.public_key,
|
||||
"purpose": trust.purpose,
|
||||
"bridge_id": trust.bridge_id,
|
||||
})
|
||||
|
||||
|
||||
def _parse_plan(plan: Mapping[str, Any]) -> dict[str, Any]:
|
||||
schema = plan.get("schema")
|
||||
if schema not in {PLAN_V1, PLAN_V2}:
|
||||
raise GovernedScopeError("EXECUTION_PLAN_SCHEMA_INVALID")
|
||||
|
||||
raw_files = plan.get("files") if schema == PLAN_V1 else plan.get("file_allowlist")
|
||||
files = _normalize_files(raw_files or [])
|
||||
|
||||
if schema == PLAN_V1:
|
||||
purpose = str(plan.get("purpose", "")).strip()
|
||||
if not purpose:
|
||||
raise GovernedScopeError("EXECUTION_PLAN_PURPOSE_REQUIRED")
|
||||
plan_capabilities = None
|
||||
else:
|
||||
purpose = PLAN_V2
|
||||
plan_capabilities = _normalize_caps(plan.get("capabilities") or [])
|
||||
file_sha256 = plan.get("file_sha256")
|
||||
if not isinstance(file_sha256, Mapping):
|
||||
raise GovernedScopeError("EXECUTION_PLAN_FILE_SHA256_REQUIRED")
|
||||
if set(map(str, file_sha256.keys())) != set(files):
|
||||
raise GovernedScopeError("EXECUTION_PLAN_FILE_SHA256_COVERAGE_MISMATCH")
|
||||
for path, digest in file_sha256.items():
|
||||
_hex64(digest, f"file_sha256:{path}")
|
||||
|
||||
return {
|
||||
"task_id": _task_id(plan.get("task_id")),
|
||||
"repository": str(plan.get("repository", "")).strip(),
|
||||
"target_branch": _branch(plan.get("target_branch")),
|
||||
"generation": _generation(plan.get("generation")),
|
||||
"external_job_id": _job_id(plan.get("external_job_id")),
|
||||
"files": files,
|
||||
"authority_base_oid": _oid40(
|
||||
plan.get("authority_base_oid"), "authority_base_oid"
|
||||
),
|
||||
"purpose": purpose,
|
||||
"capabilities": plan_capabilities,
|
||||
}
|
||||
|
||||
|
||||
class GovernedRuntimeScopeProvider:
|
||||
def resolve(
|
||||
self, *, task: TaskSnapshot, execution: ExecutionSnapshot,
|
||||
grant: GrantSnapshot, plan: Mapping[str, Any],
|
||||
trust: TrustSnapshot, now: int,
|
||||
) -> RuntimeScope:
|
||||
task_id = _task_id(task.task_id)
|
||||
if str(task.status).upper() not in {"CLAIMED", "RUNNING"}:
|
||||
raise GovernedScopeError("TASK_NOT_CLAIMED")
|
||||
|
||||
exec_task = _task_id(execution.task_id)
|
||||
exec_job = _job_id(execution.external_job_id)
|
||||
exec_branch = _branch(execution.target_branch)
|
||||
exec_generation = _generation(execution.generation)
|
||||
exec_digest = _hex64(execution.execution_digest, "execution_digest")
|
||||
if execution.repository != "aios/aios":
|
||||
raise GovernedScopeError("EXECUTION_REPOSITORY_INVALID")
|
||||
|
||||
grant_id = str(grant.grant_id).strip()
|
||||
if not GRANT_RE.fullmatch(grant_id):
|
||||
raise GovernedScopeError("GRANT_ID_INVALID")
|
||||
if str(grant.status).upper() != "ADMITTED":
|
||||
raise GovernedScopeError("GRANT_NOT_ADMITTED")
|
||||
if int(now) >= int(grant.expires_at):
|
||||
raise GovernedScopeError("GRANT_EXPIRED")
|
||||
|
||||
grant_task = _task_id(grant.task_id)
|
||||
grant_branch = _branch(grant.target_branch)
|
||||
grant_generation = _generation(grant.generation)
|
||||
grant_digest = _hex64(grant.execution_digest, "grant_execution_digest")
|
||||
grant_base = _oid40(grant.authority_base_oid, "grant_authority_base_oid")
|
||||
grant_caps = _normalize_caps(grant.requested_capabilities)
|
||||
grant_files = _normalize_files(grant.file_allowlist)
|
||||
if grant.repository != "aios/aios":
|
||||
raise GovernedScopeError("GRANT_REPOSITORY_INVALID")
|
||||
|
||||
p = _parse_plan(plan)
|
||||
# Hash the ORIGINAL plan object. v2 field adaptation is parsing only.
|
||||
if sha256_json(plan) != exec_digest:
|
||||
raise GovernedScopeError("EXECUTION_PLAN_DIGEST_MISMATCH")
|
||||
|
||||
expected = (
|
||||
task_id, execution.repository, exec_branch, exec_generation, exec_digest
|
||||
)
|
||||
if (
|
||||
exec_task, grant.repository, grant_branch, grant_generation, grant_digest
|
||||
) != expected:
|
||||
raise GovernedScopeError("GRANT_EXECUTION_BINDING_MISMATCH")
|
||||
if grant_task != task_id:
|
||||
raise GovernedScopeError("GRANT_TASK_BINDING_MISMATCH")
|
||||
|
||||
if (
|
||||
p["task_id"], p["repository"], p["target_branch"], p["generation"]
|
||||
) != (task_id, execution.repository, exec_branch, exec_generation):
|
||||
raise GovernedScopeError("PLAN_TASK_BINDING_MISMATCH")
|
||||
if p["external_job_id"] != exec_job:
|
||||
raise GovernedScopeError("PLAN_EXTERNAL_JOB_BINDING_MISMATCH")
|
||||
if p["files"] != grant_files:
|
||||
raise GovernedScopeError("PLAN_FILE_ALLOWLIST_MISMATCH")
|
||||
if p["authority_base_oid"] != grant_base:
|
||||
raise GovernedScopeError("PLAN_AUTHORITY_BASE_MISMATCH")
|
||||
if p["capabilities"] is not None and p["capabilities"] != grant_caps:
|
||||
raise GovernedScopeError("PLAN_CAPABILITIES_MISMATCH")
|
||||
|
||||
if not trust.issuer_id or not trust.key_id or not trust.public_key:
|
||||
raise GovernedScopeError("TRUST_IDENTITY_INVALID")
|
||||
if "TASK_PRINCIPAL" not in str(trust.purpose).upper().replace("-", "_"):
|
||||
raise GovernedScopeError("TRUST_PURPOSE_INVALID")
|
||||
if not str(trust.bridge_id).strip():
|
||||
raise GovernedScopeError("TRUST_BRIDGE_ID_INVALID")
|
||||
|
||||
trust_digest = _trust_digest(trust)
|
||||
if trust.scope_manifest_sha256 is not None:
|
||||
expected_trust_digest = _hex64(
|
||||
trust.scope_manifest_sha256, "scope_manifest_sha256"
|
||||
)
|
||||
if expected_trust_digest != trust_digest:
|
||||
raise GovernedScopeError("TRUST_MANIFEST_DIGEST_MISMATCH")
|
||||
|
||||
return RuntimeScope(
|
||||
scope_id=(
|
||||
f"governed-runtime:{task_id}:g{exec_generation}:"
|
||||
f"{exec_digest[:16]}"
|
||||
),
|
||||
task_id=task_id,
|
||||
external_job_id=exec_job,
|
||||
repository=execution.repository,
|
||||
target_branch=exec_branch,
|
||||
generation=exec_generation,
|
||||
execution_digest=exec_digest,
|
||||
capabilities=grant_caps,
|
||||
trust_manifest_sha256=trust_digest,
|
||||
grant_id=grant_id,
|
||||
grant_expires_at=int(grant.expires_at),
|
||||
)
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"allowed_peer_cgroups": ["/system.slice/fixture-c5-builder.service"],
|
||||
"allowed_peer_executable_prefix": "/usr/bin/python3",
|
||||
"allowed_peer_runtime_prefix": "/opt/aios/gitops-model-api/",
|
||||
"allowed_peer_runtime_suffix": "/runtime/app/aios_model_api_staging.py",
|
||||
"allowed_peer_uid": 1000,
|
||||
"c5_contract": {
|
||||
"accepted_base_oid": "e5f8209f57b43fc39e55c4555776e6362ce326eb",
|
||||
"authority_base_oid": "b915386ccbed46a7ae77220887da8ae666649a79",
|
||||
"rollback_artifact_sha256": "0dfc0461f7f9aa25cfbfc37008dcbd2d48d5341383574163b7bba102852062c4",
|
||||
"rollback_target_oid": "e5f8209f57b43fc39e55c4555776e6362ce326eb",
|
||||
"rollback_version": "V2R2C4",
|
||||
"task_id": "fixture-c5-source-admission"
|
||||
},
|
||||
"evidence_layout": {
|
||||
"artifact": "{version}/artifact.tar.gz",
|
||||
"canary": "{version}/canary.json",
|
||||
"gitea": "{version}/gitea.json",
|
||||
"manifest": "{version}/manifest.json",
|
||||
"package_closure": "{version}/checksums.sha256",
|
||||
"policy": "{version}/trusted-release-policy.json",
|
||||
"rollback": "{version}/rollback.json",
|
||||
"secret_scan": "{version}/secret-scan.json",
|
||||
"tests": "{version}/tests.json"
|
||||
},
|
||||
"governed_recipes": {
|
||||
"V2R2C3": {
|
||||
"base_oid": "5cf20128d8be2d10aef87b09f8f7a2aecb2000f3",
|
||||
"builder_engineering_oid": "49d30a2510038975ddddc5e56c1b9fdc44fc85fe",
|
||||
"candidate_id": "fixture-v2r2c3-candidate",
|
||||
"derived_from_v2r2c2_attestation": "81874204c86a3f4d8578a78d31328e15ebed8b69f8696a26038e1caa91db1b35",
|
||||
"mode": "governed-derived",
|
||||
"purpose": "AIOS_TRUSTED_RELEASE_BUILDER",
|
||||
"source_commit": "4420b2afcb2a1eaf821ed2c4acc86331e063eb5d",
|
||||
"task_id": "fixture-v2r2c3-task",
|
||||
"version": "V2R2C3"
|
||||
}
|
||||
},
|
||||
"maximum_request_age_seconds": 60,
|
||||
"operation": "BuildTrustedCandidateV1",
|
||||
"readiness_operation": "ProbeTrustedCandidateBuilderV1",
|
||||
"recipes": {
|
||||
"V2R2C2": {
|
||||
"base_oid": "0136a5342618d8d6e31424e81c83cb9235d2115b",
|
||||
"candidate_id": "fixture-v2r2c2-candidate",
|
||||
"mode": "fixed",
|
||||
"purpose": "AIOS_TRUSTED_RELEASE_BUILDER",
|
||||
"source_commit": "4642b74af62c89a505ae49241e805a5a88ca897f",
|
||||
"task_id": "fixture-v2r2c2-task",
|
||||
"version": "V2R2C2"
|
||||
},
|
||||
"V2R2C3": {
|
||||
"derives_from": "V2R2C2",
|
||||
"mode": "governed-derived-only",
|
||||
"purpose": "AIOS_TRUSTED_RELEASE_BUILDER",
|
||||
"version": "V2R2C3"
|
||||
},
|
||||
"V2R2C5": {
|
||||
"mode": "governed-source-admission-only",
|
||||
"purpose": "AIOS_TRUSTED_RELEASE_BUILDER",
|
||||
"version": "V2R2C5"
|
||||
}
|
||||
},
|
||||
"repository": "aios/aios",
|
||||
"request_schema": "aios.trusted-candidate-builder.request.v1",
|
||||
"request_timeout_seconds": 30,
|
||||
"required_checks": ["authoritative_git", "artifact", "canary", "gitea", "manifest", "package_closure", "rollback", "secret_scan", "tests"],
|
||||
"response_schema": "aios.trusted-candidate-builder.response.v1",
|
||||
"schema": "aios.trusted-candidate-builder-policy.c5.v1"
|
||||
}
|
||||
@@ -16,6 +16,7 @@ from app.task_plane.live_governed_writer_adapters import (
|
||||
LiveProtocolConfig,
|
||||
LiveProtocolConflict,
|
||||
LiveProtocolError,
|
||||
IssuerResponseError,
|
||||
LiveTaskPrincipalClient,
|
||||
SecretHandleStore,
|
||||
)
|
||||
@@ -212,6 +213,30 @@ class LiveAgentTaskContractTests(unittest.TestCase):
|
||||
"TaskLease lease-token-secret-12345",
|
||||
)
|
||||
|
||||
def test_issuer_rejection_exposes_only_stable_response_facts(self):
|
||||
class Rejected(RecordingTransport):
|
||||
def request(self, method, url, **kwargs):
|
||||
if url.endswith("/v1/task-principal/assertions"):
|
||||
return HttpResult(403, {
|
||||
"error": "EXECUTION_PLAN_SCHEMA_INVALID",
|
||||
"detail": "token=secret-value",
|
||||
})
|
||||
return super().request(method, url, **kwargs)
|
||||
|
||||
handle = self.secrets.put("task-lease", "lease-token-secret-12345")
|
||||
principal = LiveTaskPrincipalClient(
|
||||
self.cfg, transport=Rejected(), secrets_store=self.secrets,
|
||||
)
|
||||
with self.assertRaises(IssuerResponseError) as caught:
|
||||
principal.issue_assertion(
|
||||
task_id="task-live-1", lease_handle=handle,
|
||||
operation="acquireWriterLease", bridge_request={}, request_id="req-1",
|
||||
)
|
||||
self.assertEqual(caught.exception.status, 403)
|
||||
self.assertEqual(caught.exception.error_code, "EXECUTION_PLAN_SCHEMA_INVALID")
|
||||
self.assertEqual(caught.exception.operation, "acquireWriterLease")
|
||||
self.assertNotIn("secret-value", str(caught.exception))
|
||||
|
||||
|
||||
class LiveBridgeContinuationTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
"""Isolated C5 build contract tests; these are not deployment readiness receipts."""
|
||||
import dataclasses
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||
|
||||
from aios_trusted_candidate_builder import (
|
||||
BuilderError, BuilderKeyring, DeterministicEvidenceCollector, PeerIdentity,
|
||||
ServerOwnedEvidenceSource, TrustedCandidateBuilder, TrustedCandidateBuilderConfig,
|
||||
UnixTrustedCandidateBuilderClient, load_builder_policy,
|
||||
)
|
||||
from release_source_admission import ReleaseSourceAdmitter
|
||||
from silent_release_hardening import (
|
||||
BuilderAttestation, BuilderAttestationKeyRecord, BuilderAttestationKeyRing,
|
||||
BuilderAttestationVerifier, ReleasePlan, TrustedCandidateBundle,
|
||||
PreSwitchValidator, StaleApprovalError,
|
||||
)
|
||||
from silent_release_controller import ReleaseCandidate
|
||||
from test_release_source_admission import FakeBackend, REF, make_store
|
||||
from test_trusted_candidate_builder import NOW, passing_bundle
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE = "2" * 40 # A test publication, never the prepublication candidate OID.
|
||||
AUTHORITY_BASE = "b915386ccbed46a7ae77220887da8ae666649a79"
|
||||
ACCEPTED_BASE = "e5f8209f57b43fc39e55c4555776e6362ce326eb"
|
||||
C5_POLICY = ROOT / "tests/fixtures/trusted-candidate-builder-policy-c5.json"
|
||||
|
||||
|
||||
def canonical(value):
|
||||
return json.dumps(value, sort_keys=True, separators=(",", ":")).encode()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def c5(tmp_path):
|
||||
config = TrustedCandidateBuilderConfig.from_policy(load_builder_policy(
|
||||
C5_POLICY
|
||||
))
|
||||
raw = passing_bundle("V2R2C5", {
|
||||
"base_oid": SOURCE, "candidate_id": "rc-v2r2c5-" + SOURCE[:12]
|
||||
})
|
||||
candidate = ReleaseCandidate.from_dict(raw["candidate"])
|
||||
plan = ReleasePlan.from_dict(raw["release_plan"])
|
||||
store = make_store(tmp_path)
|
||||
record = ReleaseSourceAdmitter(store, FakeBackend(
|
||||
main=AUTHORITY_BASE, tree=candidate.tree_oid,
|
||||
)).admit(version="V2R2C5", task_id=config.c5_contract["task_id"],
|
||||
source_ref=REF, source_commit=SOURCE)
|
||||
config = dataclasses.replace(config, source_admission_resolver=store.resolve_recipe)
|
||||
recipe = config.resolve_recipe("V2R2C5", None, SOURCE)
|
||||
evidence = tmp_path / "evidence"
|
||||
(tmp_path / "repo.git").mkdir()
|
||||
version = evidence / "V2R2C5"
|
||||
version.mkdir(parents=True)
|
||||
evidence.chmod(0o700)
|
||||
|
||||
def write(name, content):
|
||||
data = content if isinstance(content, bytes) else canonical(content)
|
||||
path = version / name
|
||||
path.write_bytes(data)
|
||||
path.chmod(0o600)
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
tests = {"passed": True, "receipt": "isolated-tests"}
|
||||
canary = {"passed": True, "receipt": "isolated-canary"}
|
||||
git = {"consistent": True, "repository": "aios/aios",
|
||||
"source_commit": SOURCE, "tree_oid": candidate.tree_oid}
|
||||
gitea = {**git, "remote_ref_oid": SOURCE}
|
||||
rollback = {
|
||||
"ready": True, "source_commit": SOURCE,
|
||||
"rollback_sha256": plan.deployment.rollback_sha256,
|
||||
}
|
||||
artifact = write("artifact.tar.gz", b"isolated C5 artifact")
|
||||
manifest = write("manifest.json", {"artifact_sha256": artifact})
|
||||
closure = write("checksums.sha256", b"isolated closure")
|
||||
policy = write("trusted-release-policy.json", b"isolated release policy")
|
||||
for name, value in (("tests", tests), ("canary", canary), ("gitea", gitea),
|
||||
("rollback", rollback),
|
||||
("secret-scan", {"passed": True, "artifact_sha256": artifact})):
|
||||
write(name + ".json", value)
|
||||
candidate.artifact_sha256 = artifact
|
||||
candidate.manifest_sha256 = manifest
|
||||
candidate.tests, candidate.canary = tests, canary
|
||||
candidate.git_consistency, candidate.gitea_consistency = git, gitea
|
||||
plan = dataclasses.replace(
|
||||
plan,
|
||||
deployment=dataclasses.replace(plan.deployment, artifact_sha256=artifact,
|
||||
manifest_sha256=manifest, package_closure_sha256=closure, policy_sha256=policy,
|
||||
rollback_target={"version": "V2R2C4", "target_oid": ACCEPTED_BASE,
|
||||
"artifact_sha256": config.c5_contract["rollback_artifact_sha256"]}),
|
||||
tag=dataclasses.replace(plan.tag, version="V2R2C5"),
|
||||
finalization=dataclasses.replace(plan.finalization, version="V2R2C5"),
|
||||
policy_sha256=policy,
|
||||
)
|
||||
write("receipt.json", {"schema": "aios.trusted-candidate-builder.evidence.v1",
|
||||
"version": "V2R2C5", "candidate": candidate.to_dict(), "release_plan": plan.to_dict()})
|
||||
git_state = {"tree": candidate.tree_oid, "rejected_base": None, "queries": []}
|
||||
|
||||
def git_runner(argv, **_kwargs):
|
||||
git_state["queries"].append(argv)
|
||||
if "rev-parse" in argv:
|
||||
return SimpleNamespace(stdout=git_state["tree"] + "\n", returncode=0)
|
||||
return SimpleNamespace(stdout="", returncode=int(argv[-2] == git_state["rejected_base"]))
|
||||
|
||||
source = ServerOwnedEvidenceSource(config, tmp_path / "repo.git", evidence,
|
||||
expected_owner_uid=None, git_runner=git_runner)
|
||||
return SimpleNamespace(config=config, recipe=recipe, source=source, record=record,
|
||||
store=store, git=git_state, write=write, rollback=rollback, version=version)
|
||||
|
||||
|
||||
def test_c5_authenticated_admission_to_signed_build(c5):
|
||||
key = Ed25519PrivateKey.generate()
|
||||
|
||||
class Signer:
|
||||
key_id = "isolated-c5-builder"
|
||||
key_generation = 1
|
||||
|
||||
def sign_for_purpose(self, purpose, payload):
|
||||
assert purpose == "AIOS_TRUSTED_RELEASE_BUILDER"
|
||||
return key.sign(payload)
|
||||
|
||||
config = c5.config
|
||||
keyring = BuilderKeyring(builder_hmac=b"h" * 32)
|
||||
server = TrustedCandidateBuilder(config, keyring,
|
||||
collector=DeterministicEvidenceCollector(config, evidence_source=c5.source),
|
||||
signer=Signer(), peer_identity_resolver=lambda _: PeerIdentity(pid=77, uid=1000, gid=1000),
|
||||
cgroup_resolver=lambda _: config.allowed_peer_cgroups[0],
|
||||
process_identity_resolver=lambda _: ("/usr/bin/python3.12", (
|
||||
"/usr/bin/python3", "/opt/aios/gitops-model-api/test/runtime/app/aios_model_api_staging.py")),
|
||||
clock=lambda: NOW)
|
||||
client = UnixTrustedCandidateBuilderClient(config, keyring, clock=lambda: NOW)
|
||||
response = server.handle_request(client.create_request("V2R2C5", None, SOURCE,
|
||||
nonce="c5-isolated-contract-000000000001"), connection=object())
|
||||
public = key.public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
|
||||
verifier = BuilderAttestationVerifier(BuilderAttestationKeyRing([
|
||||
BuilderAttestationKeyRecord(key_id=Signer.key_id, public_key=public, algorithm="Ed25519",
|
||||
purpose="AIOS_TRUSTED_RELEASE_BUILDER", generation=1,
|
||||
not_before="2020-01-01T00:00:00Z", expires_at="2099-01-01T00:00:00Z", status="ACTIVE")
|
||||
], require_attestation_metadata=True))
|
||||
raw = response["silent_release_bundle"]
|
||||
bundle = TrustedCandidateBundle(ReleaseCandidate.from_dict(raw["candidate"]),
|
||||
ReleasePlan.from_dict(raw["release_plan"]), raw["checks"],
|
||||
BuilderAttestation.from_dict(raw["attestation"], require_fingerprint=True))
|
||||
assert bundle.validate(verifier).candidate.source_commit == SOURCE
|
||||
assert response["task_id"] == c5.config.c5_contract["task_id"]
|
||||
queried_bases = {q[-2] for q in c5.git["queries"] if "merge-base" in q}
|
||||
assert queried_bases == {AUTHORITY_BASE, ACCEPTED_BASE}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("base", [AUTHORITY_BASE, ACCEPTED_BASE])
|
||||
def test_c5_rejects_either_broken_lineage(c5, base):
|
||||
c5.git["rejected_base"] = base
|
||||
with pytest.raises(BuilderError, match="COLLECTOR_GIT_MISMATCH"):
|
||||
c5.source("V2R2C5", c5.recipe)
|
||||
|
||||
|
||||
def test_c5_rejects_tree_disagreement_with_authenticated_admission(c5):
|
||||
c5.git["tree"] = "f" * 40
|
||||
with pytest.raises(BuilderError, match="COLLECTOR_GIT_MISMATCH"):
|
||||
c5.source("V2R2C5", c5.recipe)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field,value", [
|
||||
("target_oid", "f" * 40),
|
||||
("version", "V2R2C3"),
|
||||
("artifact_sha256", "f" * 64),
|
||||
])
|
||||
def test_c5_rejects_wrong_rollback_target(c5, field, value):
|
||||
receipt = json.loads((c5.version / "receipt.json").read_text())
|
||||
receipt["release_plan"]["deployment"]["rollback_target"][field] = value
|
||||
c5.write("receipt.json", receipt)
|
||||
with pytest.raises(BuilderError, match="COLLECTOR_ROLLBACK_MISMATCH"):
|
||||
c5.source("V2R2C5", c5.recipe)
|
||||
|
||||
|
||||
def test_c5_rejects_changed_artifact(c5):
|
||||
c5.write("artifact.tar.gz", b"substituted artifact")
|
||||
with pytest.raises(BuilderError, match="COLLECTOR_DIGEST_MISMATCH"):
|
||||
c5.source("V2R2C5", c5.recipe)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", ["V2R2C4", "V2R2C6", "unknown"])
|
||||
def test_c5_policy_rejects_unknown_recipe_before_resolver(c5, version):
|
||||
def unexpected(*args):
|
||||
pytest.fail("Unknown recipe reached source admission resolver")
|
||||
config = dataclasses.replace(c5.config, source_admission_resolver=unexpected)
|
||||
with pytest.raises(BuilderError, match="RECIPE_UNKNOWN"):
|
||||
config.resolve_recipe(version, None, SOURCE)
|
||||
|
||||
|
||||
def test_old_policy_can_consume_new_authenticated_c5_tree_projection(c5):
|
||||
config = TrustedCandidateBuilderConfig.from_policy(load_builder_policy(
|
||||
ROOT / "config/gitops/trusted-candidate-builder-policy.json"
|
||||
))
|
||||
config = dataclasses.replace(config, source_admission_resolver=c5.store.resolve_recipe)
|
||||
assert config.resolve_recipe("V2R2C5", None, SOURCE)["tree_oid"] == c5.record["tree_oid"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field,value", [
|
||||
("source_commit", "f" * 40), ("version", "V2R2C4"),
|
||||
("tree_oid", "invalid"), ("candidate_id", ""),
|
||||
("purpose", "unapproved-purpose"),
|
||||
])
|
||||
def test_c5_rejects_admission_coordinate_substitution(c5, field, value):
|
||||
admitted = c5.store.resolve_recipe("V2R2C5", None, SOURCE)
|
||||
admitted[field] = value
|
||||
config = dataclasses.replace(c5.config, source_admission_resolver=lambda *_: admitted)
|
||||
expected = "SOURCE_ADMISSION_BINDING_INVALID" if field == "candidate_id" else "SOURCE_ADMISSION_INVALID"
|
||||
with pytest.raises(BuilderError, match=expected):
|
||||
config.resolve_recipe("V2R2C5", None, SOURCE)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field,value", [
|
||||
("task_id", ""), ("authority_base_oid", "substituted"),
|
||||
("accepted_base_oid", "f" * 40), ("rollback_version", "other"),
|
||||
("rollback_target_oid", "f" * 40), ("rollback_artifact_sha256", "substituted"),
|
||||
])
|
||||
def test_c5_policy_rejects_malformed_or_inconsistent_governed_contract(field, value):
|
||||
policy = dict(load_builder_policy(C5_POLICY))
|
||||
policy["c5_contract"][field] = value
|
||||
with pytest.raises(BuilderError, match="POLICY_V2R2C5_CONTRACT_INVALID"):
|
||||
TrustedCandidateBuilderConfig.from_policy(policy)
|
||||
|
||||
|
||||
def test_rollback_target_is_hashed_and_invalidates_old_approval(c5):
|
||||
receipt = c5.source("V2R2C5", c5.recipe)
|
||||
plan = ReleasePlan.from_dict(receipt["release_plan"])
|
||||
candidate = ReleaseCandidate.from_dict(receipt["candidate"])
|
||||
changed = dataclasses.replace(plan.deployment,
|
||||
rollback_target={**plan.deployment.rollback_target, "target_oid": "f" * 40})
|
||||
assert changed.sha256 != plan.deployment.sha256
|
||||
assert dataclasses.replace(plan, deployment=changed).sha256 != plan.sha256
|
||||
approval = SimpleNamespace(candidate_id=candidate.candidate_id, source_commit=SOURCE,
|
||||
tree_oid=candidate.tree_oid, artifact_sha256=candidate.artifact_sha256,
|
||||
manifest_sha256=candidate.manifest_sha256,
|
||||
deployment_plan_sha256=plan.deployment.sha256, policy_sha256=plan.policy_sha256)
|
||||
with pytest.raises(StaleApprovalError, match="APPROVAL_STALE_REAPPROVAL_REQUIRED"):
|
||||
PreSwitchValidator(None).validate(candidate, approval, changed, expected_repository="aios/aios")
|
||||
|
||||
|
||||
def test_legacy_deployment_digest_does_not_gain_empty_extension(c5):
|
||||
plan = ReleasePlan.from_dict(c5.source("V2R2C5", c5.recipe)["release_plan"])
|
||||
legacy = dataclasses.replace(plan.deployment, rollback_target={})
|
||||
payload = legacy.canonical_payload
|
||||
assert "rollback_target" not in payload
|
||||
assert legacy.sha256 == hashlib.sha256(canonical(payload)).hexdigest()
|
||||
assert legacy.from_dict(legacy.to_dict()).sha256 == legacy.sha256
|
||||
@@ -0,0 +1,584 @@
|
||||
import base64
|
||||
import copy
|
||||
import os
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
|
||||
import c5_completion_renewal as renewal
|
||||
|
||||
|
||||
def test_registered_bindings_require_digest_and_install_no_live_defaults(monkeypatch):
|
||||
root = Path.cwd() / 'registered'
|
||||
value = {
|
||||
'schema': 'aios.c5.completion-bindings.v1',
|
||||
'authority_root': str(root / 'c5'),
|
||||
'state_root': str(root / 'state'),
|
||||
'owner_key_id': 'SHA256:' + base64.b64encode(b'k' * 32).decode(),
|
||||
'predecessor_fingerprint': '1' * 64,
|
||||
'saga_head': '2' * 64,
|
||||
'historical_root': str(root / 'history'),
|
||||
'controller_workspace': str(root / 'workspace'),
|
||||
'authoritative_git_dir': str(root / 'repo.git'),
|
||||
'runtime_registry_root': str(root / 'registry'),
|
||||
'runtime_baseline_path': str(root / 'baseline.json'),
|
||||
'trusted_release_policy_path': str(root / 'policy.json'),
|
||||
'trusted_release_policy_sha256': '3' * 64,
|
||||
}
|
||||
raw = renewal.canonical(value)
|
||||
monkeypatch.setattr(renewal, '_safe_parents', lambda _path: None)
|
||||
monkeypatch.setattr(renewal, 'secure_bytes', lambda _path, owner_uid=0: raw)
|
||||
monkeypatch.setattr(renewal, '_BINDINGS_DIGEST', None)
|
||||
result = renewal.configure_registered_bindings(
|
||||
root / 'c5' / 'completion-bindings.json', renewal.digest(raw),
|
||||
)
|
||||
assert result == value
|
||||
assert renewal.AUTHORITY_ROOT == root / 'c5'
|
||||
assert renewal.STATE_ROOT == root / 'state'
|
||||
with pytest.raises(renewal.RenewalRejected, match='DIGEST_MISMATCH'):
|
||||
renewal.configure_registered_bindings(
|
||||
root / 'c5' / 'completion-bindings.json', 'f' * 64,
|
||||
)
|
||||
|
||||
|
||||
def signed(payload, key):
|
||||
raw = renewal.canonical(payload)
|
||||
return {**payload, 'fingerprint': renewal.digest(raw),
|
||||
'signature': base64.b64encode(key.sign(raw)).decode()}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def materials(monkeypatch):
|
||||
key = Ed25519PrivateKey.generate()
|
||||
public = key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
payload = {'candidate_id': renewal.CID, 'source_commit': renewal.SOURCE,
|
||||
'tree_oid': renewal.TREE, 'artifact_sha256': renewal.ARTIFACT,
|
||||
'release_plan_sha256': renewal.PLAN, 'capabilities': renewal.CAPABILITIES,
|
||||
'issued_at': '2026-10-01T07:47:47Z', 'not_before': '2026-10-01T07:47:47Z',
|
||||
'expires_at': '2026-10-01T08:47:47Z', 'nonce': 'historical', 'key_id': 'fixture',
|
||||
'owner_identity': 'fixture-owner', 'trust_domain': 'fixture-domain',
|
||||
'owner_approval_record_oid': 'a'*40}
|
||||
old = signed(payload, key)
|
||||
monkeypatch.setattr(renewal, 'OLD_FP', old['fingerprint'])
|
||||
successor = {**payload, 'issued_at': '2026-10-01T08:50:00Z',
|
||||
'not_before': '2026-10-01T08:50:00Z', 'expires_at': '2026-10-01T09:00:00Z', 'nonce': 'fresh'}
|
||||
return key, public, old, signed(successor, key), renewal.timestamp('2026-10-01T08:51:00Z')
|
||||
|
||||
|
||||
def test_valid_chain_and_append_preserve_original(tmp_path, materials):
|
||||
key, public, old, new, now = materials
|
||||
historical = renewal.canonical(old)
|
||||
path = tmp_path / 'original.json';path.write_bytes(historical)
|
||||
receipt = renewal.publish_successor(tmp_path, old, new, public, now=now,
|
||||
registration_digest='1'*64, predecessor_bytes=historical)
|
||||
assert path.read_bytes() == historical
|
||||
assert receipt['runtime_effects_allowed'] is False
|
||||
assert receipt['completion_only'] is True
|
||||
assert renewal.publish_successor(tmp_path, old, new, public, now=now,
|
||||
registration_digest='1'*64, predecessor_bytes=historical) == receipt
|
||||
|
||||
|
||||
@pytest.mark.parametrize('field,value', [
|
||||
('candidate_id','other'), ('source_commit','f'*40), ('tree_oid','f'*40),
|
||||
('artifact_sha256','f'*64), ('release_plan_sha256','f'*64), ('key_id','other'),
|
||||
('capabilities',['tag_publish']), ('nonce','historical'),
|
||||
('expires_at','2026-10-01T08:51:00Z'), ('expires_at','2026-10-01T09:50:00Z'),
|
||||
('issued_at','2026-10-01T08:52:00Z'), ('not_before','2026-10-01T08:52:00Z'),
|
||||
])
|
||||
def test_signed_drift_or_stale_rejected(materials, field, value):
|
||||
key, public, old, new, now = materials
|
||||
payload = {k:v for k,v in new.items() if k not in {'fingerprint','signature'}}
|
||||
payload[field] = value
|
||||
with pytest.raises(renewal.RenewalRejected):
|
||||
renewal.validate_successor(old, signed(payload,key), public, now=now)
|
||||
|
||||
|
||||
def test_live_predecessor_cannot_be_superseded(materials):
|
||||
key, public, old, new, now = materials
|
||||
with pytest.raises(renewal.RenewalRejected, match='PREDECESSOR_NOT_EXPIRED'):
|
||||
renewal.validate_successor(old, new, public, now=renewal.timestamp('2026-10-01T08:47:00Z'))
|
||||
|
||||
|
||||
def test_tampered_signature_and_unexpected_field_rejected(materials):
|
||||
key, public, old, new, now = materials
|
||||
altered = copy.deepcopy(new);altered['source_commit'] = 'f'*40
|
||||
with pytest.raises(renewal.RenewalRejected, match='DIGEST_INVALID'):
|
||||
renewal.validate_successor(old, altered, public, now=now)
|
||||
altered = {k:v for k,v in new.items() if k not in {'fingerprint','signature'}}
|
||||
altered['new_authority'] = True
|
||||
with pytest.raises(renewal.RenewalRejected, match='BINDING_DRIFT'):
|
||||
renewal.validate_successor(old, signed(altered,key), public, now=now)
|
||||
|
||||
|
||||
def test_conflicting_receipt_is_not_replaced(tmp_path, materials):
|
||||
_, public, old, new, now = materials
|
||||
receipt = tmp_path/(new['fingerprint']+'.receipt.json')
|
||||
receipt.write_bytes(b'old audit');receipt.chmod(0o600)
|
||||
with pytest.raises(renewal.RenewalRejected, match='APPEND_ONLY_RECORD_CONFLICT'):
|
||||
renewal.publish_successor(tmp_path, old, new, public, now=now,
|
||||
registration_digest='1'*64, predecessor_bytes=renewal.canonical(old))
|
||||
assert receipt.read_bytes() == b'old audit'
|
||||
|
||||
|
||||
def test_symlink_and_world_writable_directory_rejected(tmp_path, materials):
|
||||
_, public, old, new, now = materials
|
||||
original = tmp_path/'preserved';original.write_bytes(b'unchanged');original.chmod(0o600)
|
||||
alias = tmp_path/(new['fingerprint']+'.approval.json');alias.symlink_to(original)
|
||||
with pytest.raises(OSError):
|
||||
renewal.publish_successor(tmp_path, old, new, public, now=now,
|
||||
registration_digest='1'*64, predecessor_bytes=renewal.canonical(old))
|
||||
assert original.read_bytes() == b'unchanged'
|
||||
alias.unlink();tmp_path.chmod(0o777)
|
||||
with pytest.raises(renewal.RenewalRejected, match='UNSAFE_APPEND_DIRECTORY'):
|
||||
renewal.publish_successor(tmp_path, old, new, public, now=now,
|
||||
registration_digest='1'*64, predecessor_bytes=renewal.canonical(old))
|
||||
|
||||
|
||||
def test_predecessor_raw_bytes_must_match_signed_parent(tmp_path, materials):
|
||||
_, public, old, new, now = materials
|
||||
with pytest.raises(renewal.RenewalRejected, match='PREDECESSOR_BYTES_DRIFT'):
|
||||
renewal.publish_successor(tmp_path, old, new, public, now=now,
|
||||
registration_digest='1'*64, predecessor_bytes=b'{}')
|
||||
assert list(tmp_path.iterdir()) == []
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def store_fixture(tmp_path, materials, monkeypatch):
|
||||
key, public, old, new, now = materials
|
||||
new_payload = {k:v for k,v in new.items() if k not in {'fingerprint','signature'}}
|
||||
new_payload['nonce'] = 'a'*64
|
||||
new = signed(new_payload,key)
|
||||
class Frozen(datetime):
|
||||
@classmethod
|
||||
def now(cls, tz=None):
|
||||
return datetime.fromtimestamp(now, timezone.utc)
|
||||
monkeypatch.setattr(renewal,'datetime',Frozen)
|
||||
class Approval:
|
||||
def __init__(self,value):self.value=value;self.fingerprint=value['fingerprint']
|
||||
def to_dict(self):return self.value
|
||||
@classmethod
|
||||
def from_dict(cls,value):return cls(value)
|
||||
class Original:
|
||||
def __init__(self,root):self.root=root
|
||||
def load(self,candidate_id):return Approval(old)
|
||||
def save(self,candidate_id,approval):
|
||||
assert approval.fingerprint == old['fingerprint']
|
||||
return self.root/(candidate_id+'.json'),False
|
||||
origin=tmp_path/'original';origin.mkdir(mode=0o700)
|
||||
raw=renewal.canonical(old)
|
||||
(origin/(renewal.CID+'.json')).write_bytes(raw)
|
||||
(origin/(renewal.CID+'.json')).chmod(0o600)
|
||||
directory=tmp_path/'renewals';directory.mkdir(mode=0o700)
|
||||
policy={'candidate_id':renewal.CID,'predecessor_fingerprint':old['fingerprint'],
|
||||
'approval_nonce':'a'*64,'payload_sha256':'1'*64,
|
||||
'predecessor_uid':os.geteuid(),'predecessor_sha256':renewal.digest(raw)}
|
||||
store=renewal.CompletionApprovalStore(Original(origin),directory,public,lambda:policy,Approval)
|
||||
return store,Approval(new),Approval(old),policy
|
||||
|
||||
|
||||
def test_explicit_store_activation_and_replay(store_fixture):
|
||||
store,new,old,policy=store_fixture
|
||||
original=(store.original.root/(renewal.CID+'.json')).read_bytes()
|
||||
assert store.load(renewal.CID).fingerprint==old.fingerprint
|
||||
path,created=store.save(renewal.CID,new)
|
||||
assert created and path.is_file()
|
||||
assert store.load(renewal.CID).fingerprint==new.fingerprint
|
||||
assert store.save(renewal.CID,new)[1] is False
|
||||
assert (store.original.root/(renewal.CID+'.json')).read_bytes()==original
|
||||
|
||||
|
||||
def test_store_rejects_wrong_registered_nonce(store_fixture):
|
||||
store,new,old,policy=store_fixture
|
||||
policy['approval_nonce']='b'*64
|
||||
with pytest.raises(renewal.RenewalRejected,match='REGISTERED_NONCE_MISMATCH'):
|
||||
store.save(renewal.CID,new)
|
||||
assert list(store.directory.iterdir())==[]
|
||||
|
||||
|
||||
def test_store_detects_parent_file_change(store_fixture):
|
||||
store,new,old,policy=store_fixture
|
||||
path=store.original.root/(renewal.CID+'.json')
|
||||
path.write_bytes(renewal.canonical({**old.to_dict(),'nonce':'altered'}))
|
||||
with pytest.raises(renewal.RenewalRejected,match='PREDECESSOR_FILE_DRIFT'):
|
||||
store.save(renewal.CID,new)
|
||||
|
||||
|
||||
def test_partial_append_is_not_active(store_fixture):
|
||||
store,new,old,policy=store_fixture
|
||||
renewal.append_file(store.directory/(new.fingerprint+'.approval.json'),renewal.canonical(new.to_dict()))
|
||||
assert store.load(renewal.CID).fingerprint==old.fingerprint
|
||||
|
||||
|
||||
def test_active_receipt_tamper_fails_closed(store_fixture):
|
||||
store,new,old,policy=store_fixture
|
||||
path,_=store.save(renewal.CID,new)
|
||||
receipt=__import__('json').loads(path.read_bytes());receipt['runtime_effects_allowed']=True
|
||||
path.write_bytes(renewal.canonical(receipt))
|
||||
with pytest.raises(renewal.RenewalRejected,match='SUCCESSOR_ACTIVATION_DRIFT'):
|
||||
store.load(renewal.CID)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def signer_fixture(tmp_path, materials, monkeypatch):
|
||||
key,public,old,new,now=materials
|
||||
class Frozen(datetime):
|
||||
@classmethod
|
||||
def now(cls,tz=None):return datetime.fromtimestamp(now,timezone.utc)
|
||||
monkeypatch.setattr(renewal,'datetime',Frozen)
|
||||
snapshot=tmp_path/'parent.json';snapshot.write_bytes(renewal.canonical(old));snapshot.chmod(0o600)
|
||||
policy={'candidate_id':renewal.CID,'predecessor_fingerprint':old['fingerprint'],
|
||||
'issued_at':now-30,'expires_at':now+900,'gate_observed_at':now-1,
|
||||
'predecessor_snapshot':str(snapshot),'predecessor_sha256':renewal.digest(snapshot.read_bytes()),
|
||||
'approval_nonce':'b'*64,'payload_sha256':'1'*64}
|
||||
class SigningKey:
|
||||
count=0
|
||||
def public_key(self):return key.public_key()
|
||||
def sign(self,raw):self.count+=1;return key.sign(raw)
|
||||
class Signer(renewal.CompletionSignerMixin):pass
|
||||
signer=Signer();signer.private_key=SigningKey();signer.authorization_oid='a'*40
|
||||
signer.policy=type('Policy',(),{'key_id':'fixture','owner_identity':'fixture-owner','trust_domain':'fixture-domain'})()
|
||||
signer.state_root=tmp_path;(tmp_path/'completion-renewals').mkdir(mode=0o700)
|
||||
signer.completion_authority=lambda:policy
|
||||
request={'schema':'aios.exact-release-signer.request.v1','operation':'renew_exact_completion_approval',
|
||||
'candidate_id':renewal.CID,'predecessor_fingerprint':old['fingerprint']}
|
||||
return signer,policy,request,old
|
||||
|
||||
|
||||
def test_purpose_signer_signs_once_and_replays(signer_fixture):
|
||||
signer,policy,request,old=signer_fixture
|
||||
result=signer.sign_exact_completion_successor(request)
|
||||
assert result['status']=='SIGNED' and result['completion_only']
|
||||
assert signer.private_key.count==1
|
||||
assert result['approval']['nonce']==policy['approval_nonce']
|
||||
assert signer.sign_exact_completion_successor(request)['status']=='SIGNED_IDEMPOTENT'
|
||||
assert signer.private_key.count==1
|
||||
|
||||
|
||||
@pytest.mark.parametrize('change',[{'candidate_id':'other'},{'extra':'arbitrary'},{'operation':'sign_anything'}])
|
||||
def test_purpose_signer_rejects_arbitrary_request_before_sign(signer_fixture,change):
|
||||
signer,policy,request,old=signer_fixture
|
||||
with pytest.raises(renewal.RenewalRejected,match='EXACT_SIGNER_RENEWAL_REQUEST_REQUIRED'):
|
||||
signer.sign_exact_completion_successor({**request,**change})
|
||||
assert signer.private_key.count==0
|
||||
|
||||
|
||||
@pytest.mark.parametrize('field,value',[('gate_observed_at',0),('expires_at',0),('predecessor_sha256','f'*64),('approval_nonce','historical')])
|
||||
def test_purpose_signer_rejects_stale_gate_and_preimage_before_sign(signer_fixture,field,value):
|
||||
signer,policy,request,old=signer_fixture;policy[field]=value
|
||||
with pytest.raises(renewal.RenewalRejected):signer.sign_exact_completion_successor(request)
|
||||
assert signer.private_key.count==0
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def registered_authority(tmp_path, monkeypatch):
|
||||
import sys
|
||||
import types
|
||||
root = tmp_path / 'registered'; root.mkdir(mode=0o700)
|
||||
monkeypatch.setattr(renewal, 'AUTHORITY_ROOT', root)
|
||||
monkeypatch.setattr(renewal, '__file__', str(root / 'c5_completion_renewal.py'))
|
||||
# pytest scratch parents are not production paths; preserve the real file
|
||||
# checks but substitute this ancestry fixture only in the isolated test.
|
||||
monkeypatch.setattr(renewal, '_safe_parents', lambda path: None)
|
||||
monkeypatch.setattr(renewal.time, 'time', lambda: 1100)
|
||||
verifier = types.SimpleNamespace(verify=lambda **kw: kw['signature'] == 'fixture-valid')
|
||||
monkeypatch.setitem(sys.modules, 'c5_staging_installation', types.SimpleNamespace(
|
||||
RegistryOwnerVerifier=lambda *a, **kw: verifier))
|
||||
files = {
|
||||
'c5_completion_renewal.py': b'fixture registered authority module',
|
||||
'c5_release_execution_host.py': b'fixture typed host',
|
||||
'silent_release_hardening.py': b'fixture typed API',
|
||||
'c5_completion_signer.py': b'fixture exact signer wrapper',
|
||||
'c5_completion_api.py': b'fixture typed composition',
|
||||
'c5_completion_phase.py': b'fixture fixed completion phase',
|
||||
'approval-public-keyring.json': b'fixture public keyring',
|
||||
'original_api.py': b'fixture preserved original API',
|
||||
'predecessor-approval.json': b'fixture historical bytes',
|
||||
'historical-production-proof.json': renewal.canonical({
|
||||
'candidate_id': renewal.CID, 'approval_fingerprint': renewal.OLD_FP,
|
||||
'release_plan_sha256': renewal.PLAN, 'status': 'COMPLETED',
|
||||
'public_business_verified': True, 'real_c4_rollback_verified': True, 'c5_restore_verified': True}),
|
||||
'current-gate.json': renewal.canonical({
|
||||
'candidate_id': renewal.CID, 'source_commit': renewal.SOURCE,
|
||||
'artifact_sha256': renewal.ARTIFACT, 'release_plan_sha256': renewal.PLAN,
|
||||
'saga_revision': 15, 'saga_head': renewal.SAGA_HEAD, 'local_tag_oid': renewal.TAG_OID,
|
||||
'remote_tag_oid': None, 'runtime_verified': True, 'business_verified': True,
|
||||
'global_writes_enabled': False, 'observed_at': 1000}),
|
||||
}
|
||||
for name in renewal.PHASE_DEPENDENCIES: files[name] = b'fixture signed dependency'
|
||||
files['source-material.json'] = renewal.canonical({
|
||||
'status': 'MATERIALIZED', 'task_status': 'SUCCEEDED', 'remote_verified': True,
|
||||
'source_commit': '1'*40, 'source_tree': '2'*40,
|
||||
'file_sha256': {n: renewal.digest(files[n]) for n in renewal.RENEWAL_SOURCE_FILES}})
|
||||
for name, raw in files.items():
|
||||
(root/name).write_bytes(raw); (root/name).chmod(0o600)
|
||||
payload = {'schema': 'aios.c5.registered-completion-authority.v1', 'host': 'cn-network-mkt',
|
||||
'repository': 'aios/aios', 'candidate_id': renewal.CID, 'source_commit': renewal.SOURCE,
|
||||
'tree_oid': renewal.TREE, 'artifact_sha256': renewal.ARTIFACT, 'release_plan_sha256': renewal.PLAN,
|
||||
'state_root': str(renewal.STATE_ROOT), 'predecessor_fingerprint': renewal.OLD_FP,
|
||||
'predecessor_sha256': renewal.digest(files['predecessor-approval.json']), 'predecessor_uid': 1000,
|
||||
'predecessor_snapshot': str(root/'predecessor-approval.json'), 'approval_nonce': '3'*64,
|
||||
'issued_at': 1000, 'expires_at': 1600, 'owner_key_id': renewal.OWNER_KEY,
|
||||
'owner_namespace': 'aios-owner-approval-v1',
|
||||
'allowed_effects': ['approval_successor_append','baseline_finalize','registry_finalize','tag_publish'],
|
||||
'runtime_effects_allowed': False,
|
||||
'files': [{'path': str(root/n), 'sha256': renewal.digest(raw), 'uid': 0} for n, raw in files.items()],
|
||||
'gate_path': str(root/'current-gate.json'), 'gate_observed_at': 1000,
|
||||
'governed_source_commit': '1'*40, 'governed_source_tree': '2'*40}
|
||||
def seal(signature='fixture-valid'):
|
||||
(root/'contract.json').write_bytes(renewal.canonical({'payload': payload, 'signature': signature}))
|
||||
(root/'registration.json').write_bytes(renewal.canonical({
|
||||
'schema': 'aios.c5.completion-registration.v1', 'status': 'REGISTERED',
|
||||
'payload_sha256': renewal.digest(renewal.canonical(payload)), 'approval_nonce': payload['approval_nonce']}))
|
||||
(root/'contract.json').chmod(0o600); (root/'registration.json').chmod(0o600)
|
||||
seal()
|
||||
return root, payload, seal, renewal.RegisteredCompletionAuthority()
|
||||
|
||||
|
||||
def completion_gate():
|
||||
gate = {'saga_revision': 20, 'observed_at': 1000, 'completion_journal_suffix': []}
|
||||
previous = renewal.SAGA_HEAD
|
||||
pairs = [
|
||||
('owner_registered_completion_successor_tag_reconciliation', 'TAG_PENDING'),
|
||||
('release_reconciliation_required', 'RECONCILIATION_REQUIRED'),
|
||||
('owner_registered_tag_capability_successor_verified', 'TAG_VERIFIED'),
|
||||
('baseline_registry_finalization_pending', 'BASELINE_REGISTRY_PENDING'),
|
||||
('release_reconciliation_required', 'RECONCILIATION_REQUIRED'),
|
||||
]
|
||||
for revision, (name, state) in enumerate(pairs, 16):
|
||||
event = {'schema_version': 1, 'sequence': revision + 1, 'previous_hash': previous,
|
||||
'payload': {'candidate_id': renewal.CID, 'release_plan_id': 'pending-' + renewal.CID,
|
||||
'revision': revision, 'state': state, 'event': name,
|
||||
'timestamp': '1970-01-01T00:10:00Z', 'details': {}}}
|
||||
event['event_hash'] = renewal.digest(renewal.canonical({
|
||||
key: event[key] for key in ('sequence', 'previous_hash', 'payload')}))
|
||||
gate['completion_journal_suffix'].append(event)
|
||||
previous = event['event_hash']
|
||||
gate['saga_head'] = previous
|
||||
return gate
|
||||
|
||||
|
||||
def test_registered_authority_accepts_authenticated_post_tag_lineage(registered_authority):
|
||||
import json
|
||||
root, payload, seal, authority = registered_authority
|
||||
path = root / 'current-gate.json'
|
||||
gate = json.loads(path.read_bytes())
|
||||
gate.update(completion_gate())
|
||||
path.write_bytes(renewal.canonical(gate))
|
||||
next(item for item in payload['files'] if item['path'] == str(path))['sha256'] = renewal.digest(path.read_bytes())
|
||||
seal()
|
||||
assert authority()['payload_sha256'] == renewal.digest(renewal.canonical(payload))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('fault', [
|
||||
'head', 'origin', 'gap', 'truncated', 'revision_bool', 'sequence_bool', 'candidate',
|
||||
'plan', 'payload_hash', 'runtime_event', 'state', 'future', 'observation_bool', 'observation_nan',
|
||||
])
|
||||
def test_completion_lineage_rejects_corruption_or_noncompletion_effect(fault):
|
||||
gate = completion_gate()
|
||||
first = gate['completion_journal_suffix'][0]
|
||||
if fault == 'head': gate['saga_head'] = '0' * 64
|
||||
elif fault == 'origin': first['previous_hash'] = '0' * 64
|
||||
elif fault == 'gap': first['sequence'] = 18
|
||||
elif fault == 'truncated': gate['completion_journal_suffix'].pop()
|
||||
elif fault == 'revision_bool': gate['saga_revision'] = True
|
||||
elif fault == 'sequence_bool': first['sequence'] = True
|
||||
elif fault == 'candidate': first['payload']['candidate_id'] = 'other'
|
||||
elif fault == 'plan': first['payload']['details']['release_plan_sha256'] = '0' * 64
|
||||
elif fault == 'payload_hash': first['payload']['details']['tampered'] = True
|
||||
elif fault == 'runtime_event': first['payload']['event'] = 'runtime_switch_pending'
|
||||
elif fault == 'state': first['payload']['state'] = 'SWITCH_PENDING'
|
||||
elif fault == 'future': first['payload']['timestamp'] = '2026-10-01T12:00:00Z'
|
||||
elif fault == 'observation_bool': gate['observed_at'] = True
|
||||
elif fault == 'observation_nan': gate['observed_at'] = float('nan')
|
||||
with pytest.raises(renewal.RenewalRejected): renewal.validate_completion_lineage(gate)
|
||||
|
||||
|
||||
def test_registered_authority_validates_closure_and_denies_runtime(registered_authority):
|
||||
root, payload, _, authority = registered_authority
|
||||
assert authority()['payload_sha256'] == renewal.digest(renewal.canonical(payload))
|
||||
authority.validate_host(state_root=renewal.STATE_ROOT, candidate_id=renewal.CID,
|
||||
source_commit=renewal.SOURCE, tree_oid=renewal.TREE, artifact_sha256=renewal.ARTIFACT,
|
||||
release_plan_sha256=renewal.PLAN)
|
||||
with pytest.raises(renewal.RenewalRejected, match='RUNTIME_EFFECT_FORBIDDEN'):
|
||||
authority.deny_runtime_effect()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('field,value', [
|
||||
('candidate_id', 'other'), ('artifact_sha256','f'*64), ('release_plan_sha256','f'*64),
|
||||
('state_root','/tmp/second-plane'), ('runtime_effects_allowed',True),
|
||||
('owner_key_id','other-key'), ('owner_namespace','other-namespace'),
|
||||
('expires_at',1100), ('issued_at',1101), ('expires_at',3000),
|
||||
('approval_nonce','bad'), ('predecessor_uid',1), ('gate_observed_at',939),
|
||||
('governed_source_commit','bad')])
|
||||
def test_registered_authority_signed_drift_rejected(registered_authority, field, value):
|
||||
_, payload, seal, authority = registered_authority
|
||||
payload[field] = value; seal()
|
||||
with pytest.raises(renewal.RenewalRejected): authority()
|
||||
|
||||
|
||||
def test_registered_authority_rejects_signature_registration_and_rollback(registered_authority):
|
||||
root, _, seal, authority = registered_authority
|
||||
seal('invalid')
|
||||
with pytest.raises(renewal.RenewalRejected, match='SIGNATURE_REJECTED'): authority()
|
||||
seal(); (root/'registration.json').write_bytes(b'{}')
|
||||
with pytest.raises(renewal.RenewalRejected, match='REGISTRATION_REQUIRED'): authority()
|
||||
seal(); (root/'rollback.json').write_bytes(b'{}')
|
||||
with pytest.raises(renewal.RenewalRejected, match='ROLLED_BACK'): authority()
|
||||
|
||||
|
||||
def test_registered_authority_rejects_code_drift_and_symlink(registered_authority):
|
||||
root, _, _, authority = registered_authority
|
||||
path = root/'c5_completion_renewal.py'; original = path.read_bytes()
|
||||
path.write_bytes(b'altered')
|
||||
with pytest.raises(renewal.RenewalRejected, match='BYTES_DRIFT'): authority()
|
||||
path.unlink(); target=root/'target'; target.write_bytes(original); path.symlink_to(target)
|
||||
with pytest.raises(OSError): authority()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file,field,value', [
|
||||
('source-material.json','task_status','FAILED'),
|
||||
('source-material.json','remote_verified',False),
|
||||
('historical-production-proof.json','real_c4_rollback_verified',False),
|
||||
('historical-production-proof.json','approval_fingerprint','f'*64),
|
||||
('current-gate.json','business_verified',False),
|
||||
('current-gate.json','saga_revision',14),
|
||||
('current-gate.json','remote_tag_oid','f'*40)])
|
||||
def test_registered_authority_rejects_signed_false_evidence(registered_authority, file, field, value):
|
||||
import json
|
||||
root, payload, seal, authority=registered_authority
|
||||
path=root/file; data=json.loads(path.read_bytes()); data[field]=value
|
||||
raw=renewal.canonical(data); path.write_bytes(raw)
|
||||
for pin in payload['files']:
|
||||
if pin['path']==str(path): pin['sha256']=renewal.digest(raw)
|
||||
seal()
|
||||
with pytest.raises(renewal.RenewalRejected): authority()
|
||||
|
||||
|
||||
def test_registered_store_cannot_select_another_plane(tmp_path):
|
||||
from types import SimpleNamespace
|
||||
with pytest.raises(renewal.RenewalRejected, match='SAME_APPROVAL_PLANE_REQUIRED'):
|
||||
renewal.RegisteredCompletionApprovalStore(SimpleNamespace(root=tmp_path), b'', object)
|
||||
|
||||
|
||||
|
||||
def test_completion_runtime_guard_uses_execute_contract(registered_authority):
|
||||
_, _, _, authority = registered_authority
|
||||
with pytest.raises(renewal.RenewalRejected, match='RUNTIME_EFFECT_FORBIDDEN'):
|
||||
renewal.CompletionRuntimeGuard(authority).execute(candidate=object(), plan=object())
|
||||
with pytest.raises(renewal.RenewalRejected, match='AUTHORITY_REQUIRED'):
|
||||
renewal.CompletionRuntimeGuard(lambda: {})
|
||||
|
||||
|
||||
def test_typed_host_rejects_callback_before_approval_load(tmp_path, monkeypatch):
|
||||
import c5_release_execution_host as host
|
||||
from types import SimpleNamespace
|
||||
monkeypatch.setenv('AIOS_GITOPS_WRITES_ENABLED','false')
|
||||
config=SimpleNamespace(verify_files=lambda **kw: None, state_root=tmp_path)
|
||||
with pytest.raises(host.C5ExecutionHostBlocked, match='REGISTERED_COMPLETION_DEPENDENCY_REQUIRED'):
|
||||
host.build_execution_host(config, completion_approvals=lambda: {})
|
||||
|
||||
|
||||
def test_api_rejects_untyped_completion_store(tmp_path):
|
||||
from silent_release_hardening import SilentReleaseActionService, CapabilityAuthorizationError
|
||||
with pytest.raises(CapabilityAuthorizationError, match='completion approval dependency'):
|
||||
SilentReleaseActionService(tmp_path, completion_approvals=object())
|
||||
|
||||
|
||||
def test_signer_wrapper_preserves_existing_dispatch():
|
||||
from types import SimpleNamespace
|
||||
from c5_completion_signer import signer_class
|
||||
class Original:
|
||||
def handle(self, request): return {'original_operation': request['operation']}
|
||||
signer=signer_class(SimpleNamespace(ExactReleaseSigner=Original))()
|
||||
assert signer.handle({'operation':'status'}) == {'original_operation':'status'}
|
||||
with pytest.raises(renewal.RenewalRejected, match='EXACT_SIGNER_RENEWAL_REQUEST_REQUIRED'):
|
||||
signer.handle({'operation':'renew_exact_completion_approval', 'arbitrary':True})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def real_api_roundtrip(tmp_path, monkeypatch):
|
||||
import dataclasses
|
||||
from datetime import timedelta
|
||||
from test_c5_release_execution_host import _plan, _approval
|
||||
import c5_exact_deployment_adapter as exact
|
||||
import silent_release_hardening as core
|
||||
from silent_release_controller import ReleaseCandidate, ReleaseCandidateStore
|
||||
private = Ed25519PrivateKey.generate()
|
||||
public = private.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
policy_sha = '1'*64
|
||||
plan = _plan(renewal.SOURCE, renewal.TREE, renewal.ARTIFACT,
|
||||
tmp_path/'repo.git', policy_sha, exact.C4_ROLLBACK_TARGET)
|
||||
monkeypatch.setattr(renewal,'PLAN',plan.sha256)
|
||||
candidate=ReleaseCandidate(candidate_id=renewal.CID, source_commit=renewal.SOURCE,
|
||||
tree_oid=renewal.TREE, artifact_sha256=renewal.ARTIFACT,
|
||||
manifest_sha256=plan.deployment.manifest_sha256)
|
||||
now=datetime.now(timezone.utc)
|
||||
def sign(approval):
|
||||
return dataclasses.replace(approval,signature=base64.b64encode(
|
||||
private.sign(approval.canonical_bytes())).decode())
|
||||
old=sign(dataclasses.replace(_approval(candidate,plan,private),
|
||||
capabilities=tuple(renewal.CAPABILITIES),
|
||||
issued_at=(now-timedelta(minutes=70)).isoformat(),
|
||||
not_before=(now-timedelta(minutes=70)).isoformat(),
|
||||
expires_at=(now-timedelta(minutes=10)).isoformat()))
|
||||
new=sign(dataclasses.replace(old,issued_at=(now-timedelta(seconds=5)).isoformat(),
|
||||
not_before=(now-timedelta(seconds=5)).isoformat(),
|
||||
expires_at=(now+timedelta(seconds=595)).isoformat(),nonce='f'*64))
|
||||
monkeypatch.setattr(renewal,'OLD_FP',old.fingerprint)
|
||||
monkeypatch.setattr(renewal,'STATE_ROOT',tmp_path)
|
||||
original=core.SignedApprovalStore(tmp_path/'approvals')
|
||||
original.save(renewal.CID,old)
|
||||
raw=(original.root/(renewal.CID+'.json')).read_bytes()
|
||||
policy={'candidate_id':renewal.CID,'predecessor_fingerprint':old.fingerprint,
|
||||
'approval_nonce':'f'*64,'payload_sha256':'1'*64,'predecessor_uid':os.geteuid(),
|
||||
'predecessor_sha256':renewal.digest(raw)}
|
||||
class FixtureAuthority:
|
||||
state_root = tmp_path
|
||||
def __call__(self):return policy
|
||||
monkeypatch.setattr(renewal,'RegisteredCompletionAuthority',FixtureAuthority)
|
||||
directory=original.root/'c5-completion-successors';directory.mkdir(mode=0o700)
|
||||
store=renewal.RegisteredCompletionApprovalStore(original,public,core.SignedReleasePlanApproval)
|
||||
ReleaseCandidateStore(tmp_path/'candidates').save(candidate)
|
||||
core.ReleasePlanStore(tmp_path/'release-plans').save(renewal.CID,plan)
|
||||
sagas=core.ReleaseSagaStore(tmp_path/'sagas')
|
||||
saga=sagas.create(renewal.CID,old.release_plan_id)
|
||||
sagas.compare_and_set(renewal.CID,expected_revision=saga.revision,
|
||||
expected_states={core.SagaState.PREPARING},target=core.SagaState.RECONCILIATION_REQUIRED,
|
||||
event='isolated_existing_partial_tag')
|
||||
verifier=core.SignedApprovalVerifier(core.ApprovalKeyRing({'c5-owner-key-001':public}),
|
||||
core.ApprovalRevocationStore(tmp_path/'revocations'))
|
||||
service=core.SilentReleaseActionService(tmp_path,approval_verifier=verifier,
|
||||
policy_sha256=policy_sha,completion_approvals=store)
|
||||
return service,store,old,new,raw,private
|
||||
|
||||
|
||||
def test_actual_api_verifier_append_successor_roundtrip(real_api_roundtrip):
|
||||
service,store,old,new,raw,_=real_api_roundtrip
|
||||
response=service.submit_owner_approval(new.to_dict())
|
||||
assert response['status']=='VERIFIED_AND_STORED'
|
||||
assert response['saga_state']=='RECONCILIATION_REQUIRED'
|
||||
assert response['side_effects_executed'] is False
|
||||
assert response['shadow_mode'] is True
|
||||
assert store.load(renewal.CID).fingerprint==new.fingerprint
|
||||
assert service.submit_owner_approval(new.to_dict())['status']=='IDEMPOTENT'
|
||||
assert (store.original.root/(renewal.CID+'.json')).read_bytes()==raw
|
||||
|
||||
|
||||
@pytest.mark.parametrize('failure',['expired','tampered','wrong_binding','revoked'])
|
||||
def test_actual_api_verifier_rejects_before_successor_append(real_api_roundtrip,failure):
|
||||
import dataclasses
|
||||
import silent_release_hardening as core
|
||||
service,store,old,new,raw,key=real_api_roundtrip
|
||||
value=new.to_dict()
|
||||
if failure=='expired':value=old.to_dict()
|
||||
if failure=='tampered':value['signature']=base64.b64encode(b'0'*64).decode()
|
||||
if failure=='wrong_binding':
|
||||
bad=dataclasses.replace(new,artifact_sha256='0'*64)
|
||||
bad=dataclasses.replace(bad,signature=base64.b64encode(key.sign(bad.canonical_bytes())).decode())
|
||||
value=bad.to_dict()
|
||||
if failure=='revoked':service.approval_verifier.keyring.revoke(new.key_id)
|
||||
with pytest.raises(core.CapabilityAuthorizationError):service.submit_owner_approval(value)
|
||||
assert list(store.directory.iterdir())==[]
|
||||
assert (store.original.root/(renewal.CID+'.json')).read_bytes()==raw
|
||||
@@ -0,0 +1,427 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import dataclasses
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||
|
||||
import c5_exact_deployment_adapter as c5
|
||||
import release_source_admission
|
||||
from release_source_admission import ReleaseSourceAdmissionStore
|
||||
from silent_release_controller import ReleaseCandidate
|
||||
from silent_release_hardening import (
|
||||
ApprovalKeyRing,
|
||||
ApprovalRevocationStore,
|
||||
CapabilityLedger,
|
||||
DeploymentPlan,
|
||||
FinalizationPlan,
|
||||
ReleasePlan,
|
||||
SignedApprovalVerifier,
|
||||
SignedReleasePlanApproval,
|
||||
TagPlan,
|
||||
)
|
||||
|
||||
|
||||
SOURCE = "1" * 40
|
||||
TREE = "2" * 40
|
||||
MANIFEST = "3" * 64
|
||||
POLICY = "4" * 64
|
||||
TAG_OID = "5" * 40
|
||||
EVIDENCE = "6" * 40
|
||||
OWNER_RECORD = "7" * 40
|
||||
BASELINE = "8" * 64
|
||||
REGISTRY = "9" * 64
|
||||
TAG_REFSET = "a" * 64
|
||||
PACKAGE = "b" * 64
|
||||
SYSTEMD = "c" * 64
|
||||
ABSENT = "d" * 64
|
||||
CANDIDATE_ID = "c5-candidate-final-publication-001"
|
||||
APPROVAL_PLAN_ID = "c5-owner-plan-final-publication-001"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _windows_has_no_posix_mode_model(monkeypatch):
|
||||
if os.name == "nt":
|
||||
monkeypatch.setattr(
|
||||
release_source_admission,
|
||||
"_secure_stat",
|
||||
lambda *args, **kwargs: None,
|
||||
)
|
||||
|
||||
|
||||
def _sha(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def _plan(source: str, tree: str, artifact_sha256: str) -> ReleasePlan:
|
||||
deployment = DeploymentPlan(
|
||||
service=c5.C5_PRODUCTION_SERVICE,
|
||||
release_path=f"/data/aios-gitops/runtime/releases/{source}",
|
||||
release_root="/data/aios-gitops/runtime/releases",
|
||||
cutover_link=(
|
||||
"/etc/systemd/system/aios-gitops-model-api.service.d/production.conf"
|
||||
),
|
||||
dropin_path=(
|
||||
f"/data/aios-gitops/runtime/releases/{source}/deployment/production.conf"
|
||||
),
|
||||
dropin_sha256=SYSTEMD,
|
||||
systemd_unit_sha256=SYSTEMD,
|
||||
rollback_path="/usr/libexec/aios-gitops-runtime-anchor",
|
||||
rollback_sha256="e" * 64,
|
||||
source_commit=source,
|
||||
tree_oid=tree,
|
||||
artifact_sha256=artifact_sha256,
|
||||
manifest_sha256=MANIFEST,
|
||||
package_closure_sha256=PACKAGE,
|
||||
authoritative_git_dir="/data/aios-gitops/repo.git",
|
||||
repository_identity="aios/aios",
|
||||
writer_expected=False,
|
||||
baseline_before_sha256=BASELINE,
|
||||
registry_before_sha256=REGISTRY,
|
||||
production_tag_refset_before_sha256=TAG_REFSET,
|
||||
health_contract_version="silent-release-health/v1",
|
||||
timeout_policy={"health_seconds": 30},
|
||||
retry_policy={"health_attempts": 3},
|
||||
policy_sha256=POLICY,
|
||||
health_contract={
|
||||
"runtime_commit": source,
|
||||
"writer": False,
|
||||
"repository_identity": "aios/aios",
|
||||
},
|
||||
rollback_target=dict(c5.C4_ROLLBACK_TARGET),
|
||||
)
|
||||
return ReleasePlan(
|
||||
deployment=deployment,
|
||||
tag=TagPlan(
|
||||
version=c5.C5_VERSION,
|
||||
tag_type="annotated",
|
||||
expected_tag_oid=TAG_OID,
|
||||
expected_target_oid=source,
|
||||
evidence_closure_oid=EVIDENCE,
|
||||
approval_plan_id=APPROVAL_PLAN_ID,
|
||||
repository_identity="aios/aios",
|
||||
gitea_owner="aios",
|
||||
gitea_repository="aios",
|
||||
current_remote_tag_oid=None,
|
||||
remote_absent_evidence_sha256=ABSENT,
|
||||
verification_contract="git+api+listing/v1",
|
||||
),
|
||||
finalization=FinalizationPlan(
|
||||
version=c5.C5_VERSION,
|
||||
expected_tag_oid=TAG_OID,
|
||||
expected_target_oid=source,
|
||||
expected_previous_baseline_sha256=BASELINE,
|
||||
registry_expected_record={"version": c5.C5_VERSION, "target_oid": source},
|
||||
baseline_expected_record={"version": c5.C5_VERSION, "target_oid": source},
|
||||
final_consistency_contract="registry=baseline=tag=v1",
|
||||
),
|
||||
policy_sha256=POLICY,
|
||||
)
|
||||
|
||||
|
||||
def _approval(
|
||||
candidate: ReleaseCandidate,
|
||||
plan: ReleasePlan,
|
||||
private: Ed25519PrivateKey,
|
||||
) -> SignedReleasePlanApproval:
|
||||
now = datetime.now(timezone.utc)
|
||||
value = SignedReleasePlanApproval(
|
||||
release_plan_id="c5-release-plan-final-publication-001",
|
||||
candidate_id=candidate.candidate_id,
|
||||
source_commit=candidate.source_commit,
|
||||
tree_oid=candidate.tree_oid,
|
||||
artifact_sha256=candidate.artifact_sha256,
|
||||
manifest_sha256=candidate.manifest_sha256,
|
||||
deployment_plan_sha256=plan.deployment.sha256,
|
||||
policy_sha256=plan.policy_sha256,
|
||||
approval_plan_id=APPROVAL_PLAN_ID,
|
||||
evidence_closure_oid=EVIDENCE,
|
||||
owner_approval_record_oid=OWNER_RECORD,
|
||||
capabilities=("runtime_switch",),
|
||||
issued_at=(now - timedelta(minutes=1)).isoformat().replace("+00:00", "Z"),
|
||||
not_before=(now - timedelta(minutes=1)).isoformat().replace("+00:00", "Z"),
|
||||
expires_at=(now + timedelta(minutes=30)).isoformat().replace("+00:00", "Z"),
|
||||
nonce="c5-exact-deployment-nonce-0001",
|
||||
key_id="c5-owner-key-001",
|
||||
signature="",
|
||||
owner_identity="owner:imruyi",
|
||||
trust_domain="aios.production",
|
||||
release_plan_sha256=plan.sha256,
|
||||
)
|
||||
return dataclasses.replace(
|
||||
value,
|
||||
signature=base64.b64encode(private.sign(value.canonical_bytes())).decode(),
|
||||
)
|
||||
|
||||
|
||||
def _admission_store(
|
||||
root: Path,
|
||||
candidate: ReleaseCandidate,
|
||||
) -> ReleaseSourceAdmissionStore:
|
||||
root.mkdir(mode=0o700)
|
||||
root.chmod(0o700)
|
||||
key = b"c5-source-admission-test-key-000000000000"
|
||||
record = {
|
||||
"schema": "aios.release-source-admission.v1",
|
||||
"admission_id": "c5-final-source-admission-001",
|
||||
"repository": "aios/aios",
|
||||
"task_id": "TASK-V2R2C5-FINAL-PUBLICATION-001",
|
||||
"version": c5.C5_VERSION,
|
||||
"candidate_id": candidate.candidate_id,
|
||||
"base_oid": c5.C4_ROLLBACK_TARGET["target_oid"],
|
||||
"source_ref": "refs/heads/agent/v2r2c5-final-publication",
|
||||
"source_commit": candidate.source_commit,
|
||||
"remote_ref_oid": candidate.source_commit,
|
||||
"tree_oid": candidate.tree_oid,
|
||||
"purpose": "AIOS_TRUSTED_RELEASE_BUILDER",
|
||||
"policy_sha256": POLICY,
|
||||
"admitted_at": "2026-09-30T00:00:00Z",
|
||||
}
|
||||
record["mac"] = hmac.new(
|
||||
key,
|
||||
release_source_admission._canonical(record),
|
||||
hashlib.sha256,
|
||||
).hexdigest()
|
||||
path = root / f"{candidate.source_commit}.json"
|
||||
path.write_text(json.dumps(record, sort_keys=True) + "\n", encoding="utf-8")
|
||||
path.chmod(0o600)
|
||||
return ReleaseSourceAdmissionStore(
|
||||
root,
|
||||
repository="aios/aios",
|
||||
policy_sha256=POLICY,
|
||||
hmac_key=key,
|
||||
writable=False,
|
||||
)
|
||||
|
||||
|
||||
def _adapter_script(path: Path, state: Path, calls: Path, mode: str) -> None:
|
||||
path.write_text(
|
||||
"import json, pathlib, sys\n"
|
||||
f"state=pathlib.Path({str(state)!r})\n"
|
||||
f"calls=pathlib.Path({str(calls)!r})\n"
|
||||
f"mode={mode!r}\n"
|
||||
"request=json.loads(sys.stdin.buffer.read())\n"
|
||||
"with calls.open('a', encoding='utf-8') as out: out.write(request['operation']+'\\n')\n"
|
||||
"binding={k:v for k,v in request.items() if k not in {'schema','operation'}}\n"
|
||||
"result={**binding,'writer_disabled':True,'deployment_receipt':{'adapter':'registered-c5','operation':request['operation']}}\n"
|
||||
"if mode == 'bad-receipt': result.pop('tree_oid')\n"
|
||||
"if request['operation']=='probe':\n"
|
||||
" if mode == 'unknown':\n"
|
||||
" result['status']='NOT_APPLIED' if len(calls.read_text(encoding='utf-8').splitlines()) == 1 else 'INDETERMINATE'\n"
|
||||
" elif state.exists(): result.update(status='ACTIVE_VERIFIED',verified=True,deployment_state='ACTIVE',production_deployed=True)\n"
|
||||
" else: result['status']='NOT_APPLIED'\n"
|
||||
"elif request['operation']=='apply':\n"
|
||||
" if mode == 'unknown': result['status']='EXECUTION_OUTCOME_UNKNOWN'\n"
|
||||
" else:\n"
|
||||
" state.write_text(request['release_plan_sha256'], encoding='utf-8')\n"
|
||||
" result['status']='APPLIED'\n"
|
||||
"elif request['operation']=='rollback':\n"
|
||||
" result.update(status='ROLLED_BACK_VERIFIED',verified=True,rollback_verified=True,rollback_version='V2R2C4',rollback_target_oid='e5f8209f57b43fc39e55c4555776e6362ce326eb',rollback_artifact_sha256='0dfc0461f7f9aa25cfbfc37008dcbd2d48d5341383574163b7bba102852062c4')\n"
|
||||
"else: raise SystemExit(2)\n"
|
||||
"print(json.dumps(result, sort_keys=True))\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
path.chmod(0o600)
|
||||
|
||||
|
||||
def _fixture(tmp_path: Path, *, mode: str = "success", source: str = SOURCE):
|
||||
artifact = tmp_path / "c5-release.tar.zst"
|
||||
artifact.write_bytes(b"exact-c5-final-governed-artifact")
|
||||
artifact.chmod(0o600)
|
||||
artifact_sha256 = _sha(artifact)
|
||||
candidate = ReleaseCandidate(
|
||||
candidate_id=CANDIDATE_ID,
|
||||
source_commit=source,
|
||||
tree_oid=TREE,
|
||||
artifact_sha256=artifact_sha256,
|
||||
manifest_sha256=MANIFEST,
|
||||
)
|
||||
plan = _plan(source, TREE, artifact_sha256)
|
||||
private = Ed25519PrivateKey.generate()
|
||||
public = private.public_key().public_bytes(
|
||||
serialization.Encoding.Raw,
|
||||
serialization.PublicFormat.Raw,
|
||||
)
|
||||
approval = _approval(candidate, plan, private)
|
||||
verifier = SignedApprovalVerifier(
|
||||
ApprovalKeyRing({"c5-owner-key-001": public}),
|
||||
ApprovalRevocationStore(tmp_path / "revocations"),
|
||||
allowed_trust_domains={"aios.production"},
|
||||
)
|
||||
admissions = _admission_store(tmp_path / "admissions", candidate)
|
||||
script = tmp_path / "registered_c5_adapter.py"
|
||||
state = tmp_path / "runtime-state"
|
||||
calls = tmp_path / "calls.log"
|
||||
_adapter_script(script, state, calls, mode)
|
||||
executable = Path(sys.executable).resolve()
|
||||
executor = c5.RegisteredC5DeploymentExecutor(
|
||||
executor_id="registered-c5-production-adapter-001",
|
||||
executable=executable,
|
||||
executable_sha256=_sha(executable),
|
||||
adapter_path=script.resolve(),
|
||||
adapter_sha256=_sha(script),
|
||||
artifact_path=artifact.resolve(),
|
||||
rollback_artifact_path=c5.C4_ROLLBACK_ARTIFACT_PATH,
|
||||
production_environment_file=c5.C5_PRODUCTION_ENVIRONMENT_FILE,
|
||||
require_root_owner=False,
|
||||
)
|
||||
return candidate, plan, approval, verifier, admissions, executor, calls
|
||||
|
||||
|
||||
def _build_adapter(parts) -> c5.C5ExactDeploymentAdapter:
|
||||
candidate, plan, approval, verifier, admissions, executor, _calls = parts
|
||||
return c5.C5ExactDeploymentAdapter(
|
||||
candidate=candidate,
|
||||
release_plan=plan,
|
||||
approval=approval,
|
||||
approval_verifier=verifier,
|
||||
source_admissions=admissions,
|
||||
executor=executor,
|
||||
)
|
||||
|
||||
|
||||
def test_runtime_switch_executes_only_registered_exact_deployment_and_is_idempotent(
|
||||
tmp_path,
|
||||
):
|
||||
parts = _fixture(tmp_path)
|
||||
candidate, plan, *_rest, calls = parts
|
||||
adapter = _build_adapter(parts)
|
||||
controller = adapter.controller(CapabilityLedger(tmp_path / "ledger"))
|
||||
|
||||
first = controller.execute(
|
||||
adapter.approval,
|
||||
operation=f"runtime:{plan.sha256}",
|
||||
candidate=candidate,
|
||||
plan=plan,
|
||||
)
|
||||
second = controller.execute(
|
||||
adapter.approval,
|
||||
operation=f"runtime:{plan.sha256}",
|
||||
candidate=candidate,
|
||||
plan=plan,
|
||||
)
|
||||
|
||||
assert first["status"] == "EXECUTED"
|
||||
assert first["result"]["verified"] is True
|
||||
assert first["result"]["rollback_target"] == c5.C4_ROLLBACK_TARGET
|
||||
assert first["result"]["global_legacy_writes_enabled"] is False
|
||||
assert second["status"] == "IDEMPOTENT"
|
||||
assert calls.read_text(encoding="utf-8").splitlines() == ["probe", "apply", "probe"]
|
||||
|
||||
|
||||
def test_unknown_effect_reconciles_and_never_reapplies(tmp_path):
|
||||
parts = _fixture(tmp_path, mode="unknown")
|
||||
candidate, plan, *_rest, calls = parts
|
||||
adapter = _build_adapter(parts)
|
||||
controller = adapter.controller(CapabilityLedger(tmp_path / "ledger"))
|
||||
operation = f"runtime:{plan.sha256}"
|
||||
|
||||
with pytest.raises(c5.C5DeploymentOutcomeUnknown, match="OUTCOME_UNKNOWN"):
|
||||
controller.execute(
|
||||
adapter.approval,
|
||||
operation=operation,
|
||||
candidate=candidate,
|
||||
plan=plan,
|
||||
)
|
||||
reconciled = controller.execute(
|
||||
adapter.approval,
|
||||
operation=operation,
|
||||
candidate=candidate,
|
||||
plan=plan,
|
||||
)
|
||||
|
||||
assert reconciled["status"] == "RECONCILIATION_REQUIRED"
|
||||
assert calls.read_text(encoding="utf-8").splitlines() == ["probe", "apply", "probe"]
|
||||
|
||||
|
||||
def test_wrong_rollback_target_fails_before_registered_executor(tmp_path):
|
||||
parts = list(_fixture(tmp_path))
|
||||
plan = parts[1]
|
||||
parts[1] = dataclasses.replace(
|
||||
plan,
|
||||
deployment=dataclasses.replace(
|
||||
plan.deployment,
|
||||
rollback_target={
|
||||
**c5.C4_ROLLBACK_TARGET,
|
||||
"target_oid": "f" * 40,
|
||||
},
|
||||
),
|
||||
)
|
||||
with pytest.raises(c5.C5DeploymentBindingError, match="ROLLBACK_TARGET_INVALID"):
|
||||
_build_adapter(tuple(parts))
|
||||
assert not parts[-1].exists()
|
||||
|
||||
|
||||
def test_rehearsal_executor_rejects_bindings_outside_exact_isolation_root(tmp_path):
|
||||
parts = _fixture(tmp_path)
|
||||
_candidate, _plan, _approval, _verifier, _admissions, executor, _calls = parts
|
||||
rehearsal_root = (tmp_path / "isolated-rehearsal" / "instance-001").resolve()
|
||||
rehearsal_root.mkdir(parents=True)
|
||||
with pytest.raises(c5.C5DeploymentBindingError, match="C5_REHEARSAL_BINDING_INVALID"):
|
||||
dataclasses.replace(
|
||||
executor,
|
||||
execution_scope="ISOLATED_REHEARSAL",
|
||||
isolation_root=rehearsal_root,
|
||||
rollback_artifact_path=tmp_path / "outside-c4.tar.gz",
|
||||
production_environment_file=rehearsal_root / "v2r2c2.env",
|
||||
rollback_target={**c5.C4_ROLLBACK_TARGET, "artifact_sha256": "f" * 64},
|
||||
)
|
||||
|
||||
|
||||
def test_prepublication_installer_source_cannot_become_final_c5_binding(tmp_path):
|
||||
parts = _fixture(tmp_path, source=c5.C5_PREPUBLICATION_SOURCE_COMMIT)
|
||||
with pytest.raises(c5.C5DeploymentBindingError, match="PREPUBLICATION_SOURCE_FORBIDDEN"):
|
||||
_build_adapter(parts)
|
||||
assert not parts[-1].exists()
|
||||
|
||||
|
||||
def test_receipt_must_echo_tree_plan_approval_and_rollback_binding(tmp_path):
|
||||
parts = _fixture(tmp_path, mode="bad-receipt")
|
||||
candidate, plan, *_rest, calls = parts
|
||||
adapter = _build_adapter(parts)
|
||||
controller = adapter.controller(CapabilityLedger(tmp_path / "ledger"))
|
||||
with pytest.raises(c5.C5DeploymentBindingError, match="RECEIPT_MISMATCH:tree_oid"):
|
||||
controller.execute(
|
||||
adapter.approval,
|
||||
operation=f"runtime:{plan.sha256}",
|
||||
candidate=candidate,
|
||||
plan=plan,
|
||||
)
|
||||
assert calls.read_text(encoding="utf-8").splitlines() == ["probe"]
|
||||
|
||||
|
||||
def test_global_legacy_writes_true_blocks_before_executor(tmp_path, monkeypatch):
|
||||
parts = _fixture(tmp_path)
|
||||
monkeypatch.setenv("AIOS_GITOPS_WRITES_ENABLED", "true")
|
||||
with pytest.raises(c5.C5DeploymentBindingError, match="LEGACY_WRITES"):
|
||||
_build_adapter(parts)
|
||||
assert not parts[-1].exists()
|
||||
|
||||
|
||||
def test_explicit_saga_rollback_requires_exact_c4_verified_receipt(tmp_path):
|
||||
parts = _fixture(tmp_path)
|
||||
candidate, plan, *_rest, calls = parts
|
||||
adapter = _build_adapter(parts)
|
||||
|
||||
result = adapter.rollback(candidate=candidate, plan=plan)
|
||||
|
||||
assert result == {
|
||||
"verified": True,
|
||||
"rollback_verified": True,
|
||||
"rollback_version": "V2R2C4",
|
||||
"rollback_target_oid": c5.C4_ROLLBACK_TARGET["target_oid"],
|
||||
"rollback_artifact_sha256": c5.C4_ROLLBACK_TARGET[
|
||||
"artifact_sha256"
|
||||
],
|
||||
"writer_disabled": True,
|
||||
}
|
||||
assert calls.read_text(encoding="utf-8").splitlines() == ["rollback"]
|
||||
@@ -525,6 +525,7 @@ def test_overlay_builder_patches_one_shared_admission_module(tmp_path, monkeypat
|
||||
monkeypatch.setattr(sys, "argv", [
|
||||
str(builder_path), "--principal-source-root", str(principal),
|
||||
"--adapter-source-root", str(adapter), "--output-root", str(output),
|
||||
"--task-id", "fixture-governed-task", "--agent-id", "agent/fixture-c5",
|
||||
])
|
||||
assert builder.main() == 0
|
||||
manifest = json.loads((output / "overlay-manifest.json").read_text())
|
||||
|
||||
@@ -0,0 +1,408 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tarfile
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
import c5_production_deployment_adapter as production
|
||||
|
||||
|
||||
def _sha(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def _request(tmp_path: Path) -> dict:
|
||||
artifact = tmp_path / "artifact.tar.gz"
|
||||
artifact.write_bytes(b"placeholder")
|
||||
return {
|
||||
"schema": production.SCHEMA,
|
||||
"operation": "probe",
|
||||
"version": production.VERSION,
|
||||
"candidate_id": "rc-v2r2c5-final-001",
|
||||
"source_commit": "1" * 40,
|
||||
"target_oid": "1" * 40,
|
||||
"tree_oid": "2" * 40,
|
||||
"artifact_sha256": _sha(b"placeholder"),
|
||||
"artifact_path": str(artifact.resolve()).replace("\\", "/"),
|
||||
"artifact_size": len(b"placeholder"),
|
||||
"manifest_sha256": "3" * 64,
|
||||
"deployment_plan_sha256": "4" * 64,
|
||||
"release_plan_sha256": "5" * 64,
|
||||
"policy_sha256": "6" * 64,
|
||||
"approval_plan_id": "c5-owner-plan-final-001",
|
||||
"approval_fingerprint": "7" * 64,
|
||||
"owner_approval_record_oid": "8" * 40,
|
||||
"evidence_closure_oid": "9" * 40,
|
||||
"repository_identity": production.REPOSITORY,
|
||||
"production_service": production.SERVICE,
|
||||
"production_port": production.PORT,
|
||||
"systemd_unit_sha256": "a" * 64,
|
||||
"dropin_sha256": "a" * 64,
|
||||
"rollback_target": dict(production.ROLLBACK),
|
||||
"rollback_artifact_path": str(production.ROLLBACK_ARTIFACT),
|
||||
"production_environment_file": str(production.ENV_FILE),
|
||||
"writer_expected": False,
|
||||
"global_legacy_writes_enabled": False,
|
||||
"registered_executor_id": "c5-production-adapter-001",
|
||||
"executor_executable_sha256": "c" * 64,
|
||||
"executor_adapter_sha256": "d" * 64,
|
||||
}
|
||||
|
||||
|
||||
def _layout(tmp_path: Path) -> production.HostLayout:
|
||||
return production.HostLayout(
|
||||
release_root=tmp_path / "release-only",
|
||||
current_link=tmp_path / "opt" / "v2r2c2",
|
||||
environment_file=tmp_path / "etc" / "v2r2c2.env",
|
||||
state_file=tmp_path / "state" / "deployment-state.json",
|
||||
unit_file=tmp_path / "systemd" / production.SERVICE,
|
||||
rollback_runtime_root=tmp_path / "c4" / "runtime-root",
|
||||
rollback_root=tmp_path / "rollback-record",
|
||||
)
|
||||
|
||||
|
||||
def test_link_target_resolves_real_predecessor_symlink(tmp_path):
|
||||
layout = _layout(tmp_path)
|
||||
layout.rollback_runtime_root.mkdir(parents=True)
|
||||
layout.current_link.parent.mkdir(parents=True)
|
||||
layout.current_link.symlink_to(layout.rollback_runtime_root, target_is_directory=True)
|
||||
assert production._link_target(layout) == layout.rollback_runtime_root.resolve(strict=True)
|
||||
|
||||
|
||||
def test_link_target_resolves_relative_symlink(tmp_path):
|
||||
from dataclasses import replace
|
||||
|
||||
target = tmp_path / "runtime-root"
|
||||
target.mkdir()
|
||||
link = tmp_path / "current"
|
||||
link.symlink_to("runtime-root", target_is_directory=True)
|
||||
assert production._link_target(replace(_layout(tmp_path), current_link=link)) == target
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind", ["missing", "directory", "dangling", "cycle"])
|
||||
def test_link_target_rejects_unresolved_or_non_symlink_identity(tmp_path, kind):
|
||||
from dataclasses import replace
|
||||
|
||||
link = tmp_path / "current"
|
||||
if kind == "directory":
|
||||
link.mkdir()
|
||||
elif kind == "dangling":
|
||||
link.symlink_to(tmp_path / "absent", target_is_directory=True)
|
||||
elif kind == "cycle":
|
||||
link.symlink_to(link, target_is_directory=True)
|
||||
assert production._link_target(replace(_layout(tmp_path), current_link=link)) is None
|
||||
|
||||
|
||||
def test_request_rejects_wrong_production_env_and_global_write_state(tmp_path):
|
||||
request = _request(tmp_path)
|
||||
request["production_environment_file"] = "/etc/aios/aios-v2r2c2.env"
|
||||
with pytest.raises(
|
||||
production.C5ProductionAdapterBlocked,
|
||||
match="ENVIRONMENT_FILE_BINDING_INVALID",
|
||||
):
|
||||
production._validate_request(request)
|
||||
|
||||
request = _request(tmp_path)
|
||||
request["global_legacy_writes_enabled"] = True
|
||||
with pytest.raises(
|
||||
production.C5ProductionAdapterBlocked,
|
||||
match="LEGACY_WRITES_BINDING_INVALID",
|
||||
):
|
||||
production._validate_request(request)
|
||||
|
||||
|
||||
def test_probe_accepts_only_exact_c4_predecessor_as_not_applied(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
binding = production._validate_request(_request(tmp_path))
|
||||
layout = _layout(tmp_path)
|
||||
layout.rollback_runtime_root.mkdir(parents=True)
|
||||
monkeypatch.setattr(
|
||||
production,
|
||||
"_link_target",
|
||||
lambda _layout: layout.rollback_runtime_root.resolve(),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
production,
|
||||
"_validate_rollback_material",
|
||||
lambda _layout: "[Service]",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
production,
|
||||
"_systemctl",
|
||||
lambda *args, **kwargs: SimpleNamespace(returncode=0),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
production,
|
||||
"_health",
|
||||
lambda _port: {
|
||||
"version": production.ROLLBACK["version"],
|
||||
"commit_oid": production.ROLLBACK["target_oid"],
|
||||
"artifact_sha256": production.ROLLBACK["artifact_sha256"],
|
||||
"deployment_state": "ACTIVE",
|
||||
"production_deployed": True,
|
||||
},
|
||||
)
|
||||
|
||||
result = production.probe(binding, layout)
|
||||
|
||||
assert result["status"] == "NOT_APPLIED"
|
||||
assert result["writer_disabled"] is True
|
||||
assert result["deployment_receipt"]["operation"] == "probe"
|
||||
|
||||
|
||||
def test_probe_maps_active_but_unobservable_runtime_to_indeterminate(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
binding = production._validate_request(_request(tmp_path))
|
||||
monkeypatch.setattr(
|
||||
production,
|
||||
"_systemctl",
|
||||
lambda *args, **kwargs: SimpleNamespace(returncode=0),
|
||||
)
|
||||
monkeypatch.setattr(production, "_health", lambda _port: None)
|
||||
|
||||
result = production.probe(binding, _layout(tmp_path))
|
||||
|
||||
assert result["status"] == "INDETERMINATE"
|
||||
|
||||
|
||||
def test_probe_requires_health_link_state_and_writer_disabled_env(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
binding = production._validate_request(_request(tmp_path))
|
||||
layout = _layout(tmp_path)
|
||||
desired = production._target(binding, rollback=False)
|
||||
release = production._release_dir(layout, desired)
|
||||
release.mkdir(parents=True)
|
||||
monkeypatch.setattr(
|
||||
production, "_link_target", lambda _layout: release.resolve()
|
||||
)
|
||||
layout.environment_file.parent.mkdir(parents=True)
|
||||
layout.environment_file.write_text(
|
||||
"AIOS_GITOPS_WRITES_ENABLED=false\n", encoding="utf-8"
|
||||
)
|
||||
layout.state_file.parent.mkdir(parents=True)
|
||||
layout.state_file.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"release_plan_sha256": binding["release_plan_sha256"],
|
||||
"candidate_id": binding["candidate_id"],
|
||||
"tree_oid": binding["tree_oid"],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
production,
|
||||
"_systemctl",
|
||||
lambda *args, **kwargs: SimpleNamespace(returncode=0),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
production,
|
||||
"_health",
|
||||
lambda _port: {
|
||||
"version": production.VERSION,
|
||||
"commit_oid": binding["target_oid"],
|
||||
"artifact_sha256": binding["artifact_sha256"],
|
||||
"deployment_state": "ACTIVE",
|
||||
"production_deployed": True,
|
||||
},
|
||||
)
|
||||
|
||||
assert production.probe(binding, layout)["status"] == "ACTIVE_VERIFIED"
|
||||
layout.environment_file.write_text(
|
||||
"AIOS_GITOPS_WRITES_ENABLED=true\n", encoding="utf-8"
|
||||
)
|
||||
assert production.probe(binding, layout)["status"] == "INDETERMINATE"
|
||||
|
||||
|
||||
def test_install_extracts_bound_runtime_and_rejects_marker_conflict(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
chown_calls = []
|
||||
monkeypatch.setattr(
|
||||
production.os,
|
||||
"chown",
|
||||
lambda path, uid, gid: chown_calls.append((Path(path), uid, gid)),
|
||||
raising=False,
|
||||
)
|
||||
request = _request(tmp_path)
|
||||
unit = (
|
||||
"[Service]\n"
|
||||
f"EnvironmentFile={production.ENV_FILE}\n"
|
||||
"ExecStart=/usr/bin/python3 app.py\n"
|
||||
).encode()
|
||||
package = json.dumps(
|
||||
{
|
||||
"source_commit": request["source_commit"],
|
||||
"tree_oid": request["tree_oid"],
|
||||
},
|
||||
sort_keys=True,
|
||||
).encode()
|
||||
members = {
|
||||
"payload/VERSION": (production.VERSION + "\n").encode(),
|
||||
"payload/governed-runtime-package.json": package,
|
||||
f"payload/deploy/aios-v2/systemd/{production.SERVICE}": unit,
|
||||
}
|
||||
artifact = Path(request["artifact_path"])
|
||||
with tarfile.open(artifact, "w:gz") as archive:
|
||||
for name, data in members.items():
|
||||
info = tarfile.TarInfo(name)
|
||||
info.size = len(data)
|
||||
archive.addfile(info, io.BytesIO(data))
|
||||
request["artifact_sha256"] = production._sha256(artifact)
|
||||
request["artifact_size"] = artifact.stat().st_size
|
||||
request["systemd_unit_sha256"] = _sha(unit)
|
||||
request["dropin_sha256"] = _sha(unit)
|
||||
binding = production._validate_request(request)
|
||||
layout = _layout(tmp_path)
|
||||
|
||||
release = production._install_release(
|
||||
binding, production._target(binding, rollback=False), layout
|
||||
)
|
||||
|
||||
if production.os.name == "posix":
|
||||
assert len(chown_calls) == 1
|
||||
assert chown_calls[0][1:] == (0, 0)
|
||||
assert (release / "VERSION").read_text().strip() == production.VERSION
|
||||
assert release.parent.name == "v2r2c5-" + request["target_oid"][:12]
|
||||
marker = release.parent / "DEPLOYMENT-BINDING.json"
|
||||
value = json.loads(marker.read_text(encoding="utf-8"))
|
||||
value["target_oid"] = "f" * 40
|
||||
marker.write_text(json.dumps(value), encoding="utf-8")
|
||||
with pytest.raises(
|
||||
production.C5ProductionAdapterBlocked,
|
||||
match="EXISTING_RELEASE_BINDING_CONFLICT",
|
||||
):
|
||||
production._install_release(
|
||||
binding, production._target(binding, rollback=False), layout
|
||||
)
|
||||
|
||||
|
||||
def test_apply_verifies_before_switch_and_restores_exact_predecessor_on_failure(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
request = _request(tmp_path)
|
||||
layout = _layout(tmp_path)
|
||||
predecessor = layout.rollback_runtime_root
|
||||
predecessor.mkdir(parents=True)
|
||||
desired = production._release_dir(
|
||||
layout, production._target(request, rollback=False)
|
||||
)
|
||||
desired.mkdir(parents=True)
|
||||
new_unit = (
|
||||
"[Service]\nUser=aios-runtime\n"
|
||||
f"EnvironmentFile={production.ENV_FILE}\n"
|
||||
)
|
||||
unit_source = desired / "deploy" / "aios-v2" / "systemd" / production.SERVICE
|
||||
unit_source.parent.mkdir(parents=True)
|
||||
unit_source.write_bytes(new_unit.encode())
|
||||
request["systemd_unit_sha256"] = production._sha256(unit_source)
|
||||
request["dropin_sha256"] = request["systemd_unit_sha256"]
|
||||
binding = production._validate_request(request)
|
||||
|
||||
old = {
|
||||
layout.environment_file: "old-env\n",
|
||||
layout.state_file: "old-state\n",
|
||||
layout.unit_file: "old-unit\n",
|
||||
}
|
||||
for path, value in old.items():
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(value, encoding="utf-8")
|
||||
monkeypatch.setattr(
|
||||
production,
|
||||
"ROLLBACK_UNIT_SHA256",
|
||||
production._sha256(layout.unit_file),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
production, "_install_release", lambda *_args: desired
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
production, "_validate_rollback_material", lambda _layout: "unit"
|
||||
)
|
||||
monkeypatch.setattr(production, "_runtime_gid", lambda _text: 123)
|
||||
chown_calls = []
|
||||
monkeypatch.setattr(
|
||||
production.os,
|
||||
"chown",
|
||||
lambda path, uid, gid: chown_calls.append((Path(path), uid, gid)),
|
||||
raising=False,
|
||||
)
|
||||
monkeypatch.setattr(os, "readlink", lambda _path: str(predecessor))
|
||||
|
||||
link = {"target": predecessor.resolve()}
|
||||
monkeypatch.setattr(
|
||||
production, "_link_target", lambda _layout: link["target"]
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
production,
|
||||
"_atomic_symlink",
|
||||
lambda _link, target: link.__setitem__("target", Path(target).resolve()),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
production,
|
||||
"_health",
|
||||
lambda _port: {
|
||||
"version": production.ROLLBACK["version"],
|
||||
"commit_oid": production.ROLLBACK["target_oid"],
|
||||
"artifact_sha256": production.ROLLBACK["artifact_sha256"],
|
||||
"deployment_state": "ACTIVE",
|
||||
"production_deployed": True,
|
||||
},
|
||||
)
|
||||
|
||||
events = []
|
||||
|
||||
def verify_before_switch(path):
|
||||
assert path == unit_source
|
||||
assert link["target"] == predecessor.resolve()
|
||||
assert all(path.read_text(encoding="utf-8") == value for path, value in old.items())
|
||||
events.append("verified")
|
||||
|
||||
monkeypatch.setattr(production, "_verify_unit", verify_before_switch)
|
||||
restart_calls = {"count": 0}
|
||||
|
||||
def systemctl(*args, **_kwargs):
|
||||
events.append("systemctl:" + ":".join(args))
|
||||
if args and args[0] == "restart":
|
||||
restart_calls["count"] += 1
|
||||
if restart_calls["count"] == 1:
|
||||
raise production.C5ProductionAdapterBlocked(
|
||||
"INJECTED_RESTART_FAILURE"
|
||||
)
|
||||
return SimpleNamespace(returncode=0)
|
||||
|
||||
monkeypatch.setattr(production, "_systemctl", systemctl)
|
||||
|
||||
with pytest.raises(
|
||||
production.C5ProductionAdapterBlocked,
|
||||
match="INJECTED_RESTART_FAILURE",
|
||||
):
|
||||
production._activate(
|
||||
binding, production._target(binding, rollback=False), layout
|
||||
)
|
||||
|
||||
assert events[0] == "verified"
|
||||
assert link["target"] == predecessor.resolve()
|
||||
assert all(path.read_text(encoding="utf-8") == value for path, value in old.items())
|
||||
record = json.loads(
|
||||
(layout.rollback_root / "record.json").read_text(encoding="utf-8")
|
||||
)
|
||||
assert record["candidate_id"] == binding["candidate_id"]
|
||||
for value in record["files"].values():
|
||||
backup = Path(value["backup"])
|
||||
assert production._sha256(backup) == value["sha256"]
|
||||
if os.name == "posix":
|
||||
runtime_owned = [
|
||||
(path, uid, gid)
|
||||
for path, uid, gid in chown_calls
|
||||
if uid == 0 and gid == 123
|
||||
]
|
||||
assert len(runtime_owned) == 2
|
||||
@@ -0,0 +1,449 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
import pytest
|
||||
|
||||
import c5_registered_installer_executor as ex
|
||||
import c5_staging_installation as c5
|
||||
from registered_owner_signer_adapter import RegisteredOwnerSignerAdapter
|
||||
import test_c5_staging_installation as fixtures
|
||||
|
||||
|
||||
def _sha(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def _write_provenance(
|
||||
tmp_path: Path, *, integration_commit: str = fixtures.SUPPORT,
|
||||
installer_module: Path | None = None,
|
||||
environment: str = "ENGINEERING_TEST", name: str = "executor-provenance.json",
|
||||
) -> Path:
|
||||
module = Path(c5.__file__).resolve() if installer_module is None else installer_module.resolve()
|
||||
script = (Path(__file__).parents[1] / "scripts" / "c5_staging_installation.py").resolve()
|
||||
wrapper = Path(ex.__file__).resolve()
|
||||
value = {
|
||||
"schema": ex.PROVENANCE_SCHEMA,
|
||||
"environment": environment,
|
||||
"instance_id": "pytest-c5-registered-installer",
|
||||
"integration_commit": integration_commit,
|
||||
"integration_tree": "b" * 40,
|
||||
"entries": {
|
||||
"installer_module": {"path": str(module), "sha256": _sha(module)},
|
||||
"installer_script": {"path": str(script), "sha256": _sha(script)},
|
||||
"executor_module": {"path": str(wrapper), "sha256": _sha(wrapper)},
|
||||
},
|
||||
}
|
||||
path = tmp_path / name
|
||||
path.write_text(json.dumps(value), encoding="utf-8")
|
||||
return path.resolve()
|
||||
|
||||
|
||||
def _authority(case):
|
||||
verifier = c5.RegistryOwnerVerifier(
|
||||
case.registry,
|
||||
owner_signer_id="local-owner-authority",
|
||||
require_root_owner=False,
|
||||
)
|
||||
signer = RegisteredOwnerSignerAdapter(
|
||||
"test-registered-owner-signer",
|
||||
key_id=verifier.record.fingerprint,
|
||||
owner_verifier=verifier,
|
||||
require_root=False,
|
||||
test_mode=True,
|
||||
runner=fixtures.SigningRunner(case.key),
|
||||
)
|
||||
return verifier, signer
|
||||
|
||||
|
||||
def _registration(
|
||||
case, manifest: Path, *, issuer_contract: Path | None = None,
|
||||
nonce: str = "registered-executor-nonce-0001",
|
||||
):
|
||||
verifier, signer = _authority(case)
|
||||
registration = ex.issue_registration(
|
||||
executor_id="c5-registered-installer-r14",
|
||||
nonce=nonce,
|
||||
ttl_seconds=40,
|
||||
contract=case.contract,
|
||||
installer=case.installer,
|
||||
provenance_manifest=manifest,
|
||||
registered_signer=signer,
|
||||
clock=lambda: fixtures.NOW,
|
||||
issuer_contract=issuer_contract,
|
||||
require_root_owner=False,
|
||||
)
|
||||
executor = ex.RegisteredInstallerExecutor(
|
||||
owner_verifier=verifier,
|
||||
installer=case.installer,
|
||||
provenance_manifest=manifest,
|
||||
clock=lambda: fixtures.NOW,
|
||||
host=lambda: c5.HOST,
|
||||
require_root_owner=False,
|
||||
)
|
||||
return registration, executor
|
||||
|
||||
|
||||
class PairBackend:
|
||||
"""Engineering backend for the two-target installer state machine.
|
||||
|
||||
It models both target readbacks but does not claim to be a durable host or
|
||||
systemd receipt. The separate rehearsal plan covers that boundary.
|
||||
"""
|
||||
|
||||
def __init__(self, bridge, issuer) -> None:
|
||||
self.cases = {c5.BRIDGE_TARGET: bridge, c5.ISSUER_TARGET: issuer}
|
||||
self.host = c5.HOST
|
||||
self.reloads = 0
|
||||
self.restarts = []
|
||||
|
||||
def hostname(self) -> str:
|
||||
return self.host
|
||||
|
||||
def _case(self, profile):
|
||||
return self.cases[profile.name]
|
||||
|
||||
def _is_new(self, profile) -> bool:
|
||||
case = self._case(profile)
|
||||
dropin = profile.dropin(case.layout)
|
||||
return dropin.exists() and dropin.read_bytes() == case.new_dropin
|
||||
|
||||
def unit_snapshot(self, profile) -> c5.UnitSnapshot:
|
||||
case = self._case(profile)
|
||||
if self._is_new(profile):
|
||||
return c5.UnitSnapshot(
|
||||
True, fixtures.sha(b"new-effective-config"),
|
||||
case.new_pythonpath, case.activation_path,
|
||||
case.principal_config_path,
|
||||
)
|
||||
return c5.UnitSnapshot(
|
||||
True, case.old_config_hash, case.old_pythonpath,
|
||||
case.payload["existing"]["activation_file"],
|
||||
case.payload["existing"]["principal_scope_config"],
|
||||
)
|
||||
|
||||
def daemon_reload(self) -> None:
|
||||
self.reloads += 1
|
||||
|
||||
def restart(self, profile) -> None:
|
||||
self.restarts.append(profile.name)
|
||||
|
||||
def runtime(self, *, url: str, timeout_seconds: int):
|
||||
assert timeout_seconds == 3
|
||||
profile = next(
|
||||
item for item in (c5._BRIDGE_PROFILE, c5._ISSUER_PROFILE)
|
||||
if item.runtime_url == url
|
||||
)
|
||||
case = self._case(profile)
|
||||
if self._is_new(profile):
|
||||
return {"status": "bad"} if case.backend_fail_new else dict(case.new_runtime)
|
||||
return {"status": "bad"} if case.backend_fail_old else dict(case.old_runtime)
|
||||
|
||||
|
||||
def _successor_pair(tmp_path: Path):
|
||||
"""Create contracts for the exact shared tree used by both R14 targets."""
|
||||
issuer = fixtures.make_successor_issuer_case(tmp_path)
|
||||
issuer.layout.issuer_dropin.unlink()
|
||||
issuer.payload["existing"]["dropin"] = {"exists": False, "sha256": None}
|
||||
issuer.contract = tmp_path / "issuer-contract.json"
|
||||
issuer.artifact = tmp_path / "issuer-artifact.tar"
|
||||
fixtures.write_tar(issuer.artifact, [
|
||||
(path.relative_to(issuer.root).as_posix(), path.read_bytes(), 0o644, "file")
|
||||
for path in sorted(issuer.root.rglob("*")) if path.is_file()
|
||||
] + [("dropin.conf", issuer.new_dropin, 0o644, "file")])
|
||||
fixtures.resign(issuer)
|
||||
|
||||
bridge = fixtures.Case()
|
||||
bridge.profile = c5._BRIDGE_PROFILE
|
||||
bridge.layout = issuer.layout
|
||||
bridge.root = issuer.root
|
||||
bridge.key = issuer.key
|
||||
bridge.registry = issuer.registry
|
||||
bridge.old_dropin = issuer.old_dropin
|
||||
bridge.layout.dropin.unlink()
|
||||
bridge.old_pythonpath = issuer.old_pythonpath
|
||||
bridge.old_config_hash = issuer.old_config_hash
|
||||
bridge.old_module = tmp_path / "old-runtime" / "app" / "aios_chatgpt_bridge_v2_server.py"
|
||||
bridge.old_module_bytes = bridge.old_module.read_bytes()
|
||||
bridge.new_module = bridge.root / "app" / "aios_chatgpt_bridge_v2_server.py"
|
||||
bridge.new_module_bytes = bridge.new_module.read_bytes()
|
||||
bridge.activation = bridge.root / "source-activation.json"
|
||||
bridge.activation_path = bridge.activation.as_posix()
|
||||
bridge.principal_config_path = (bridge.root / "principal-activation.json").as_posix()
|
||||
bridge.new_pythonpath = ((bridge.root / "app").as_posix(), *bridge.old_pythonpath)
|
||||
bridge.new_dropin = c5._dropin_bytes(
|
||||
bridge.root, bridge.old_pythonpath,
|
||||
target=c5.BRIDGE_TARGET, principal_config=True,
|
||||
)
|
||||
bridge.old_runtime = {
|
||||
"status": "active", "health": 200, "deployment_state": "STAGING",
|
||||
"repository": c5.REPOSITORY, "control_plane_target_oid": "1" * 40,
|
||||
}
|
||||
bridge.new_runtime = {
|
||||
"status": "active", "health": 200, "deployment_state": "STAGING",
|
||||
"repository": c5.REPOSITORY, "governed_writes_enabled": True,
|
||||
"source_module_path": bridge.new_module.as_posix(),
|
||||
"source_module_sha256": fixtures.sha(bridge.new_module_bytes),
|
||||
}
|
||||
previous = bridge.layout.release_base / "previous-bridge"
|
||||
previous.mkdir()
|
||||
old_activation = previous / "source-activation.json"
|
||||
old_activation.write_bytes(b'{"schema":"previous-source-activation"}\n')
|
||||
bridge.payload = copy.deepcopy(issuer.payload)
|
||||
bridge.payload.update(
|
||||
target=c5.BRIDGE_TARGET,
|
||||
contract_id="c5-successor-bridge-pair-01",
|
||||
unit=c5.UNIT,
|
||||
listen={"host": "127.0.0.1", "port": c5.PORT},
|
||||
effects=sorted(c5.BRIDGE_EFFECTS),
|
||||
nonce="nonce-c5-successor-bridge-pair-0001",
|
||||
)
|
||||
bridge.payload["existing"] = {
|
||||
"unit_active": True,
|
||||
"effective_config_sha256": bridge.old_config_hash,
|
||||
"dropin": {"exists": False, "sha256": None},
|
||||
"pythonpath": list(bridge.old_pythonpath),
|
||||
"source_files": [fixtures.file_record(
|
||||
bridge.old_module.as_posix(), bridge.old_module_bytes, "existing_source",
|
||||
)],
|
||||
"activation_file": old_activation.as_posix(),
|
||||
"principal_scope_config": None,
|
||||
"config_files": [fixtures.file_record(
|
||||
old_activation.as_posix(), old_activation.read_bytes(), "existing_config",
|
||||
)],
|
||||
}
|
||||
bridge.payload["readiness"] = {
|
||||
"url": c5.RUNTIME_URL, "timeout_seconds": 3, "unit_active": True,
|
||||
"pythonpath": list(bridge.new_pythonpath),
|
||||
"activation_file": bridge.activation_path,
|
||||
"principal_scope_config": bridge.principal_config_path,
|
||||
"module_path": bridge.new_module.as_posix(),
|
||||
"module_sha256": fixtures.sha(bridge.new_module_bytes),
|
||||
"runtime_fields": bridge.new_runtime,
|
||||
}
|
||||
bridge.payload["rollback"] = {
|
||||
"strategy": "restore-exact-dropin-and-restart",
|
||||
"dropin_restore": "exact",
|
||||
"readiness": {
|
||||
"url": c5.RUNTIME_URL, "timeout_seconds": 3, "unit_active": True,
|
||||
"pythonpath": list(bridge.old_pythonpath),
|
||||
"activation_file": old_activation.as_posix(),
|
||||
"principal_scope_config": None,
|
||||
"module_path": bridge.old_module.as_posix(),
|
||||
"module_sha256": fixtures.sha(bridge.old_module_bytes),
|
||||
"runtime_fields": bridge.old_runtime,
|
||||
},
|
||||
}
|
||||
root_records = [
|
||||
fixtures.file_record(path.as_posix(), path.read_bytes(), "release_file")
|
||||
for path in sorted(bridge.root.rglob("*")) if path.is_file()
|
||||
]
|
||||
bridge.files = root_records + [fixtures.file_record(
|
||||
bridge.layout.dropin.as_posix(), bridge.new_dropin, "systemd_dropin",
|
||||
)]
|
||||
bridge.payload["files"] = bridge.files
|
||||
bridge.contract = tmp_path / "bridge-contract.json"
|
||||
bridge.artifact = tmp_path / "bridge-artifact.tar"
|
||||
fixtures.write_tar(bridge.artifact, [
|
||||
(path.relative_to(bridge.root).as_posix(), path.read_bytes(), 0o644, "file")
|
||||
for path in sorted(bridge.root.rglob("*")) if path.is_file()
|
||||
] + [("dropin.conf", bridge.new_dropin, 0o644, "file")])
|
||||
fixtures.resign(bridge)
|
||||
|
||||
bridge.backend_fail_new = bridge.backend_fail_old = False
|
||||
issuer.backend_fail_new = issuer.backend_fail_old = False
|
||||
backend = PairBackend(bridge, issuer)
|
||||
for case in (bridge, issuer):
|
||||
case.backend = backend
|
||||
case.installer = c5.Installer(
|
||||
case.registry, backend=backend, clock=lambda: fixtures.NOW,
|
||||
sleeper=lambda _seconds: None, require_root_owner=False,
|
||||
_test_layout=case.layout,
|
||||
)
|
||||
shutil.rmtree(bridge.root)
|
||||
return bridge, issuer
|
||||
|
||||
|
||||
def test_provenance_rejects_caller_substituted_installer_module(tmp_path: Path) -> None:
|
||||
fake = tmp_path / "c5_staging_installation.py"
|
||||
fake.write_text("# not the loaded installer\n", encoding="utf-8")
|
||||
manifest = _write_provenance(tmp_path, installer_module=fake)
|
||||
with pytest.raises(ex.ExecutorRegistrationError, match="LOADED_PATH_MISMATCH"):
|
||||
ex.load_code_provenance(manifest, require_root_owner=False)
|
||||
|
||||
|
||||
def test_real_signature_preflight_binds_contract_policy_and_issuer_effects(tmp_path: Path) -> None:
|
||||
case = fixtures.make_successor_issuer_case(tmp_path)
|
||||
manifest = _write_provenance(tmp_path)
|
||||
registration, executor = _registration(case, manifest)
|
||||
|
||||
result = executor.dispatch(
|
||||
registration, case.contract, case.artifact, "preflight",
|
||||
)
|
||||
|
||||
assert result["status"] == "PREFLIGHT_PASS"
|
||||
assert registration["payload"]["capabilities"] == ["install", "reconcile", "rollback"]
|
||||
assert set(registration["payload"]["effects"]) == c5.ISSUER_EFFECTS
|
||||
assert registration["payload"]["support_commit"] == registration["payload"]["integration_commit"]
|
||||
assert registration["payload"]["issuer_dependency"] is None
|
||||
assert registration["payload"]["contract_nonce"] == case.payload["nonce"]
|
||||
assert registration["payload"]["contract_expires_at"] == case.payload["expires_at"]
|
||||
assert "receipt" not in result
|
||||
|
||||
|
||||
def test_fixture_registration_is_rejected_by_a_production_manifest(tmp_path: Path) -> None:
|
||||
case = fixtures.make_successor_issuer_case(tmp_path)
|
||||
fixture_manifest = _write_provenance(tmp_path)
|
||||
production_manifest = _write_provenance(
|
||||
tmp_path, environment="PRODUCTION", name="production-provenance.json",
|
||||
)
|
||||
registration, _executor = _registration(case, fixture_manifest)
|
||||
verifier, _signer = _authority(case)
|
||||
production_executor = ex.RegisteredInstallerExecutor(
|
||||
owner_verifier=verifier,
|
||||
installer=case.installer,
|
||||
provenance_manifest=production_manifest,
|
||||
clock=lambda: fixtures.NOW,
|
||||
host=lambda: c5.HOST,
|
||||
require_root_owner=False,
|
||||
)
|
||||
|
||||
with pytest.raises(ex.ExecutorRegistrationError, match="REGISTRATION_BINDING_INVALID"):
|
||||
production_executor.dispatch(
|
||||
registration, case.contract, case.artifact, "preflight",
|
||||
)
|
||||
|
||||
|
||||
def test_wrapped_two_target_apply_replay_and_exact_paired_rollback(tmp_path: Path) -> None:
|
||||
bridge, issuer = _successor_pair(tmp_path)
|
||||
manifest = _write_provenance(tmp_path)
|
||||
bridge_registration, bridge_executor = _registration(
|
||||
bridge, manifest, issuer_contract=issuer.contract,
|
||||
nonce="registered-bridge-pair-0001",
|
||||
)
|
||||
issuer_registration, issuer_executor = _registration(
|
||||
issuer, manifest, nonce="registered-issuer-pair-0001",
|
||||
)
|
||||
|
||||
assert bridge_executor.dispatch(
|
||||
bridge_registration, bridge.contract, bridge.artifact, "preflight",
|
||||
)["status"] == "PREFLIGHT_PASS"
|
||||
assert bridge_executor.dispatch(
|
||||
bridge_registration, bridge.contract, bridge.artifact, "install",
|
||||
)["status"] == "APPLIED"
|
||||
assert bridge_executor.dispatch(
|
||||
bridge_registration, bridge.contract, bridge.artifact, "install",
|
||||
)["status"] == "IDEMPOTENT_REPLAY"
|
||||
assert issuer_executor.dispatch(
|
||||
issuer_registration, issuer.contract, issuer.artifact, "preflight",
|
||||
)["status"] == "PREFLIGHT_PASS"
|
||||
assert issuer_executor.dispatch(
|
||||
issuer_registration, issuer.contract, issuer.artifact, "install",
|
||||
)["status"] == "APPLIED"
|
||||
assert issuer_executor.dispatch(
|
||||
issuer_registration, issuer.contract, issuer.artifact, "install",
|
||||
)["status"] == "IDEMPOTENT_REPLAY"
|
||||
assert issuer_executor.dispatch(
|
||||
issuer_registration, issuer.contract, issuer.artifact, "rollback",
|
||||
)["status"] == "ROLLED_BACK"
|
||||
assert issuer_executor.dispatch(
|
||||
issuer_registration, issuer.contract, issuer.artifact, "rollback",
|
||||
)["status"] == "IDEMPOTENT_ROLLBACK"
|
||||
assert bridge_executor.dispatch(
|
||||
bridge_registration, bridge.contract, bridge.artifact, "rollback",
|
||||
issuer_contract=issuer.contract, issuer_artifact=issuer.artifact,
|
||||
)["status"] == "ROLLED_BACK"
|
||||
assert bridge_executor.dispatch(
|
||||
bridge_registration, bridge.contract, bridge.artifact, "rollback",
|
||||
issuer_contract=issuer.contract, issuer_artifact=issuer.artifact,
|
||||
)["status"] == "IDEMPOTENT_ROLLBACK"
|
||||
assert not bridge.root.exists()
|
||||
|
||||
|
||||
def test_wrapped_unknown_and_idempotent_reconciliation(tmp_path: Path) -> None:
|
||||
case = fixtures.make_successor_issuer_case(tmp_path)
|
||||
manifest = _write_provenance(tmp_path)
|
||||
registration, executor = _registration(case, manifest)
|
||||
case.backend.fail_new = True
|
||||
case.backend.fail_old = True
|
||||
|
||||
with pytest.raises(c5.InstallationOutcomeUnknown, match="INSTALLATION_OUTCOME_UNKNOWN"):
|
||||
executor.dispatch(registration, case.contract, case.artifact, "install")
|
||||
state = json.loads(case.installer._state_path(case.payload).read_text())
|
||||
assert state["status"] == "UNKNOWN"
|
||||
|
||||
case.backend.fail_old = False
|
||||
assert executor.dispatch(
|
||||
registration, case.contract, case.artifact, "reconcile",
|
||||
)["status"] == "ROLLED_BACK"
|
||||
assert executor.dispatch(
|
||||
registration, case.contract, case.artifact, "reconcile",
|
||||
)["status"] == "IDEMPOTENT_RECONCILIATION"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field", ["old", "new", "rollback", "ttl", "nonce"])
|
||||
def test_registration_rejects_resigned_contract_scope_drift_before_installer(
|
||||
tmp_path: Path, field: str,
|
||||
) -> None:
|
||||
case = fixtures.make_successor_issuer_case(tmp_path)
|
||||
manifest = _write_provenance(tmp_path)
|
||||
registration, executor = _registration(case, manifest)
|
||||
if field == "old":
|
||||
case.payload["existing"]["effective_config_sha256"] = "f" * 64
|
||||
elif field == "new":
|
||||
case.payload["readiness"]["runtime_fields"]["manifest_revision"] = 2
|
||||
elif field == "rollback":
|
||||
case.payload["rollback"]["readiness"]["runtime_fields"]["rollback_revision"] = 2
|
||||
elif field == "ttl":
|
||||
case.payload["expires_at"] += 1
|
||||
else:
|
||||
case.payload["nonce"] = "nonce-c5-issuer-installation-0002"
|
||||
fixtures.resign(case)
|
||||
|
||||
with pytest.raises(ex.ExecutorRegistrationError, match="REGISTRATION_BINDING_INVALID"):
|
||||
executor.dispatch(registration, case.contract, case.artifact, "preflight")
|
||||
assert case.backend.reloads == case.backend.restarts == 0
|
||||
|
||||
|
||||
def test_issue_rejects_support_commit_different_from_manifest_commit(tmp_path: Path) -> None:
|
||||
case = fixtures.make_successor_issuer_case(tmp_path)
|
||||
manifest = _write_provenance(tmp_path, integration_commit="a" * 40)
|
||||
verifier, signer = _authority(case)
|
||||
del verifier
|
||||
with pytest.raises(ex.ExecutorRegistrationError, match="COMMIT_BINDING_INVALID"):
|
||||
ex.issue_registration(
|
||||
executor_id="c5-registered-installer-r14",
|
||||
nonce="registered-executor-nonce-0001",
|
||||
ttl_seconds=40,
|
||||
contract=case.contract,
|
||||
installer=case.installer,
|
||||
provenance_manifest=manifest,
|
||||
registered_signer=signer,
|
||||
clock=lambda: fixtures.NOW,
|
||||
require_root_owner=False,
|
||||
)
|
||||
|
||||
|
||||
def test_bridge_rollback_rejects_unregistered_issuer_dependency(tmp_path: Path) -> None:
|
||||
bridge, issuer = _successor_pair(tmp_path)
|
||||
manifest = _write_provenance(tmp_path)
|
||||
registration, executor = _registration(
|
||||
bridge, manifest, issuer_contract=issuer.contract,
|
||||
)
|
||||
executor.dispatch(registration, bridge.contract, bridge.artifact, "install")
|
||||
other = copy.deepcopy(issuer.payload)
|
||||
other["nonce"] = "nonce-c5-other-issuer-contract-0001"
|
||||
issuer.payload = other
|
||||
fixtures.resign(issuer)
|
||||
|
||||
with pytest.raises(ex.ExecutorRegistrationError, match="DEPENDENCY_MISMATCH"):
|
||||
executor.dispatch(
|
||||
registration, bridge.contract, bridge.artifact, "rollback",
|
||||
issuer_contract=issuer.contract, issuer_artifact=issuer.artifact,
|
||||
)
|
||||
state = json.loads(bridge.installer._state_path(bridge.payload).read_text())
|
||||
assert state["status"] == "APPLIED"
|
||||
@@ -0,0 +1,425 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import dataclasses
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||
import pytest
|
||||
|
||||
import c5_exact_deployment_adapter as exact
|
||||
import c5_release_execution_host as host
|
||||
import release_source_admission
|
||||
from silent_release_controller import ReleaseCandidate, ReleaseCandidateStore
|
||||
from silent_release_hardening import (
|
||||
ApprovalKeyRing,
|
||||
DeploymentPlan,
|
||||
FinalizationPlan,
|
||||
ReleasePlan,
|
||||
ReleasePlanStore,
|
||||
ReleaseSagaStore,
|
||||
SagaState,
|
||||
SignedApprovalStore,
|
||||
SignedReleasePlanApproval,
|
||||
TagPlan,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("task", ["fixture-governed-task", "other-task"])
|
||||
def test_source_admission_requires_configured_governed_task(task):
|
||||
config = SimpleNamespace(source_admission_task_id="fixture-governed-task")
|
||||
candidate = SimpleNamespace(source_commit="1" * 40, tree_oid="2" * 40)
|
||||
if task == config.source_admission_task_id:
|
||||
host._validate_source_admission_task(config, candidate, {"task_id": task})
|
||||
else:
|
||||
with pytest.raises(host.C5ExecutionHostBlocked, match="SOURCE_ADMISSION_TASK_BINDING_INVALID"):
|
||||
host._validate_source_admission_task(config, candidate, {"task_id": task})
|
||||
|
||||
|
||||
def test_configured_task_cannot_be_replaced_by_historical_task():
|
||||
config = SimpleNamespace(source_admission_task_id="fixture-current-task")
|
||||
candidate = SimpleNamespace(source_commit="1" * 40, tree_oid="2" * 40)
|
||||
with pytest.raises(host.C5ExecutionHostBlocked, match="SOURCE_ADMISSION_TASK_BINDING_INVALID"):
|
||||
host._validate_source_admission_task(config, candidate, {"task_id": "fixture-expired-task"})
|
||||
|
||||
|
||||
def _sha(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def _write(path: Path, data: bytes) -> Path:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(data)
|
||||
path.chmod(0o600)
|
||||
return path.resolve()
|
||||
|
||||
|
||||
def _git_fixture(tmp_path: Path) -> tuple[Path, Path, str, str]:
|
||||
raw = shutil.which("git")
|
||||
if raw is None:
|
||||
pytest.skip("git is required")
|
||||
git = Path(raw).resolve()
|
||||
repository = tmp_path / "authority.git"
|
||||
repository.mkdir()
|
||||
subprocess.run([str(git), "init"], cwd=repository, check=True, capture_output=True)
|
||||
subprocess.run([str(git), "config", "user.email", "fixture@example.invalid"], cwd=repository, check=True)
|
||||
subprocess.run([str(git), "config", "user.name", "fixture"], cwd=repository, check=True)
|
||||
(repository / "fixture.txt").write_text("isolated rehearsal\n", encoding="utf-8")
|
||||
subprocess.run([str(git), "add", "fixture.txt"], cwd=repository, check=True)
|
||||
subprocess.run([str(git), "commit", "-m", "fixture"], cwd=repository, check=True, capture_output=True)
|
||||
source = subprocess.run([str(git), "rev-parse", "HEAD"], cwd=repository, check=True, capture_output=True, text=True).stdout.strip()
|
||||
tree = subprocess.run([str(git), "rev-parse", "HEAD^{tree}"], cwd=repository, check=True, capture_output=True, text=True).stdout.strip()
|
||||
return git, (repository / ".git").resolve(), source, tree
|
||||
|
||||
|
||||
def _plan(source: str, tree: str, artifact_sha: str, git_dir: Path, policy_sha: str, rollback: dict) -> ReleasePlan:
|
||||
deployment = DeploymentPlan(
|
||||
service=exact.C5_PRODUCTION_SERVICE,
|
||||
release_path=f"/data/aios-gitops/runtime/releases/{source}",
|
||||
release_root="/data/aios-gitops/runtime/releases",
|
||||
cutover_link="/etc/systemd/system/aios-gitops-model-api.service.d/production.conf",
|
||||
dropin_path=f"/data/aios-gitops/runtime/releases/{source}/deployment/production.conf",
|
||||
dropin_sha256="1" * 64,
|
||||
systemd_unit_sha256="1" * 64,
|
||||
rollback_path="/usr/libexec/aios-gitops-runtime-anchor",
|
||||
rollback_sha256="2" * 64,
|
||||
source_commit=source,
|
||||
tree_oid=tree,
|
||||
artifact_sha256=artifact_sha,
|
||||
manifest_sha256="3" * 64,
|
||||
package_closure_sha256="4" * 64,
|
||||
authoritative_git_dir=str(git_dir),
|
||||
repository_identity=host.REPOSITORY,
|
||||
writer_expected=False,
|
||||
baseline_before_sha256="5" * 64,
|
||||
registry_before_sha256="6" * 64,
|
||||
production_tag_refset_before_sha256="7" * 64,
|
||||
health_contract_version="silent-release-health/v1",
|
||||
timeout_policy={"health_seconds": 30},
|
||||
retry_policy={"health_attempts": 3},
|
||||
policy_sha256=policy_sha,
|
||||
health_contract={"runtime_commit": source, "writer": False, "repository_identity": host.REPOSITORY},
|
||||
rollback_target=dict(rollback),
|
||||
)
|
||||
tag_oid = "8" * 40
|
||||
approval_plan = "sha256:" + "9" * 64
|
||||
return ReleasePlan(
|
||||
deployment=deployment,
|
||||
tag=TagPlan(
|
||||
version=exact.C5_VERSION,
|
||||
tag_type="annotated",
|
||||
expected_tag_oid=tag_oid,
|
||||
expected_target_oid=source,
|
||||
evidence_closure_oid="a" * 40,
|
||||
approval_plan_id=approval_plan,
|
||||
repository_identity=host.REPOSITORY,
|
||||
gitea_owner="aios",
|
||||
gitea_repository="aios",
|
||||
current_remote_tag_oid=None,
|
||||
remote_absent_evidence_sha256="b" * 64,
|
||||
verification_contract="git+api+listing/v1",
|
||||
),
|
||||
finalization=FinalizationPlan(
|
||||
version=exact.C5_VERSION,
|
||||
expected_tag_oid=tag_oid,
|
||||
expected_target_oid=source,
|
||||
expected_previous_baseline_sha256="5" * 64,
|
||||
registry_expected_record={"version": exact.C5_VERSION, "target_oid": source},
|
||||
baseline_expected_record={"version": exact.C5_VERSION, "target_oid": source},
|
||||
final_consistency_contract="registry=baseline=tag=v1",
|
||||
),
|
||||
policy_sha256=policy_sha,
|
||||
)
|
||||
|
||||
|
||||
def _approval(candidate, plan, private) -> SignedReleasePlanApproval:
|
||||
now = datetime.now(timezone.utc)
|
||||
value = SignedReleasePlanApproval(
|
||||
release_plan_id="c5-isolated-rehearsal-plan-001",
|
||||
candidate_id=candidate.candidate_id,
|
||||
source_commit=candidate.source_commit,
|
||||
tree_oid=candidate.tree_oid,
|
||||
artifact_sha256=candidate.artifact_sha256,
|
||||
manifest_sha256=candidate.manifest_sha256,
|
||||
deployment_plan_sha256=plan.deployment.sha256,
|
||||
policy_sha256=plan.policy_sha256,
|
||||
approval_plan_id=plan.tag.approval_plan_id,
|
||||
evidence_closure_oid=plan.tag.evidence_closure_oid,
|
||||
owner_approval_record_oid="c" * 40,
|
||||
capabilities=("runtime_switch", "tag_publish", "baseline_registry_finalize"),
|
||||
expires_at=(now + timedelta(minutes=30)).isoformat().replace("+00:00", "Z"),
|
||||
nonce="c5-isolated-rehearsal-nonce-0001",
|
||||
key_id="c5-owner-key-001",
|
||||
signature="",
|
||||
owner_identity="owner:fixture",
|
||||
trust_domain="aios.production",
|
||||
issued_at=(now - timedelta(minutes=1)).isoformat().replace("+00:00", "Z"),
|
||||
not_before=(now - timedelta(minutes=1)).isoformat().replace("+00:00", "Z"),
|
||||
release_plan_sha256=plan.sha256,
|
||||
)
|
||||
return dataclasses.replace(value, signature=base64.b64encode(private.sign(value.canonical_bytes())).decode())
|
||||
|
||||
|
||||
def _phase_script(path: Path) -> Path:
|
||||
return _write(
|
||||
path,
|
||||
(
|
||||
"import json,sys\n"
|
||||
"mode=sys.argv[1] if len(sys.argv)>1 else 'deployment'\n"
|
||||
"v=json.load(sys.stdin)\n"
|
||||
"if v['schema']=='aios.c5.exact-production-deployment-request.v1':\n"
|
||||
" r={k:x for k,x in v.items() if k not in {'schema','operation'}}\n"
|
||||
" r.update(status='ACTIVE_VERIFIED',verified=True,deployment_state='ACTIVE',production_deployed=True,writer_disabled=True,deployment_receipt={'scope':'fixture'})\n"
|
||||
"elif mode=='tag':\n"
|
||||
" r={k:v[k] for k in ('version','target_oid','evidence_closure_oid','artifact_sha256','approval_plan_id')}\n"
|
||||
" r.update(status='APPLIED_VERIFIED',tag_oid='8'*40,peeled_target_oid=v['target_oid'],remote_tag_oid='8'*40,remote_peeled_target_oid=v['target_oid'],gitea_tag_oid='8'*40,gitea_peeled_target_oid=v['target_oid'],gitea_listing_visible=True)\n"
|
||||
"else:\n"
|
||||
" r={k:v[k] for k in ('version','target_oid','evidence_closure_oid','artifact_sha256','approval_plan_id')}\n"
|
||||
" r.update(status='VERIFIED',tag_oid='8'*40)\n"
|
||||
"print(json.dumps(r,sort_keys=True))\n"
|
||||
).encode(),
|
||||
)
|
||||
|
||||
|
||||
def _command(executable: Path, script: Path, mode: str) -> dict:
|
||||
return {
|
||||
"executable": str(executable),
|
||||
"executable_sha256": _sha(executable),
|
||||
"adapter_path": str(script),
|
||||
"adapter_sha256": _sha(script),
|
||||
"arguments": [mode],
|
||||
}
|
||||
|
||||
|
||||
def test_isolated_rehearsal_uses_real_saga_and_cannot_emit_production_receipt(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(release_source_admission, "_secure_stat", lambda *args, **kwargs: None)
|
||||
instance_id = "c5-g2-fixture-001"
|
||||
root = tmp_path / "isolated-rehearsal" / instance_id
|
||||
root.mkdir(parents=True)
|
||||
git, repository, source, tree = _git_fixture(root)
|
||||
artifact = _write(root / "artifact.tar.gz", b"isolated-c5-artifact")
|
||||
rollback_artifact = _write(root / "rollback-c4.tar.gz", b"isolated-c4-artifact")
|
||||
rollback = {**exact.C4_ROLLBACK_TARGET, "artifact_sha256": _sha(rollback_artifact)}
|
||||
environment = root / "v2r2c2.env"
|
||||
|
||||
trusted_policy = _write(
|
||||
root / "trusted-policy.json",
|
||||
json.dumps(
|
||||
{
|
||||
"services": [exact.C5_PRODUCTION_SERVICE],
|
||||
"release_roots": ["/data/aios-gitops/runtime/releases"],
|
||||
"cutover_links": ["/etc/systemd/system/aios-gitops-model-api.service.d/production.conf"],
|
||||
"dropin_paths": [f"/data/aios-gitops/runtime/releases/{source}/deployment/production.conf"],
|
||||
"authoritative_git_dirs": [str(repository)],
|
||||
"repository_identities": [host.REPOSITORY],
|
||||
}, sort_keys=True
|
||||
).encode(),
|
||||
)
|
||||
policy_sha = _sha(trusted_policy)
|
||||
plan = _plan(source, tree, _sha(artifact), repository, policy_sha, rollback)
|
||||
candidate = ReleaseCandidate(
|
||||
candidate_id="c5-isolated-rehearsal-candidate-001",
|
||||
source_commit=source,
|
||||
tree_oid=tree,
|
||||
artifact_sha256=_sha(artifact),
|
||||
manifest_sha256=plan.deployment.manifest_sha256,
|
||||
)
|
||||
private = Ed25519PrivateKey.generate()
|
||||
approval = _approval(candidate, plan, private)
|
||||
keyring = ApprovalKeyRing(
|
||||
{"c5-owner-key-001": private.public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)}
|
||||
)
|
||||
keyring_path = root / "approval-keyring.json"
|
||||
keyring.save_public(keyring_path)
|
||||
keyring_path.chmod(0o600)
|
||||
|
||||
ReleaseCandidateStore(root / "candidates").save(candidate)
|
||||
ReleasePlanStore(root / "release-plans").save(candidate.candidate_id, plan)
|
||||
SignedApprovalStore(root / "approvals").save(candidate.candidate_id, approval)
|
||||
sagas = ReleaseSagaStore(root / "sagas")
|
||||
saga = sagas.create(candidate.candidate_id, approval.release_plan_id)
|
||||
sagas.compare_and_set(candidate.candidate_id, expected_revision=saga.revision, expected_states={SagaState.PREPARING}, target=SagaState.APPROVED, event="fixture_approval_bound")
|
||||
|
||||
admission_policy = _write(root / "source-policy.json", b"fixture-source-policy\n")
|
||||
hmac_path = _write(root / "source-admission-hmac", b"fixture-hmac-key-000000000000000000000")
|
||||
admission_root = root / "source-admissions"
|
||||
admission_root.mkdir()
|
||||
record = {
|
||||
"schema": "aios.release-source-admission.v1", "admission_id": "c5-fixture-admission-001",
|
||||
"repository": host.REPOSITORY, "task_id": "FIXTURE-NOT-R14", "version": exact.C5_VERSION,
|
||||
"candidate_id": candidate.candidate_id, "base_oid": rollback["target_oid"],
|
||||
"source_ref": "refs/heads/fixture/c5", "source_commit": source,
|
||||
"remote_ref_oid": source, "tree_oid": tree, "purpose": "AIOS_TRUSTED_RELEASE_BUILDER",
|
||||
"policy_sha256": _sha(admission_policy), "admitted_at": "2026-09-30T00:00:00Z",
|
||||
}
|
||||
record["mac"] = hmac.new(hmac_path.read_bytes().strip(), release_source_admission._canonical(record), hashlib.sha256).hexdigest()
|
||||
_write(admission_root / f"{source}.json", (json.dumps(record, sort_keys=True) + "\n").encode())
|
||||
|
||||
script = _phase_script(root / "registered-phase-adapter.py")
|
||||
executable = Path(sys.executable).resolve()
|
||||
registry_record = _write(root / "registry.json", json.dumps(plan.finalization.registry_expected_record).encode())
|
||||
baseline_record = _write(root / "baseline.json", json.dumps(plan.finalization.baseline_expected_record).encode())
|
||||
config_value = {
|
||||
"schema": host.SCHEMA, "version": exact.C5_VERSION,
|
||||
"execution_scope": "ISOLATED_REHEARSAL", "instance_id": instance_id,
|
||||
"executor_authority_commit": "a" * 40,
|
||||
"executor_authority_tree": "b" * 40,
|
||||
"provenance": {"environment": "ISOLATED_REHEARSAL", "fixture": True, "r14_source_receipt": False},
|
||||
"candidate_id": candidate.candidate_id, "source_commit": source, "tree_oid": tree,
|
||||
"artifact_sha256": candidate.artifact_sha256, "release_plan_sha256": plan.sha256,
|
||||
"approval_fingerprint": approval.fingerprint, "state_root": str(root.resolve()),
|
||||
"trusted_policy_path": str(trusted_policy), "trusted_policy_sha256": _sha(trusted_policy),
|
||||
"approval_keyring_path": str(keyring_path.resolve()), "approval_keyring_sha256": _sha(keyring_path),
|
||||
"source_admission_root": str(admission_root.resolve()),
|
||||
"source_admission_policy_path": str(admission_policy), "source_admission_policy_sha256": _sha(admission_policy),
|
||||
"source_admission_task_id": "FIXTURE-NOT-R14",
|
||||
"source_admission_hmac_path": str(hmac_path), "source_admission_hmac_sha256": _sha(hmac_path),
|
||||
"deployment_executable": str(executable), "deployment_executable_sha256": _sha(executable),
|
||||
"deployment_adapter_path": str(script), "deployment_adapter_sha256": _sha(script),
|
||||
"artifact_path": str(artifact), "rollback_target": rollback,
|
||||
"rollback_artifact_path": str(rollback_artifact), "production_environment_file": str(environment),
|
||||
"production_service": exact.C5_PRODUCTION_SERVICE, "production_port": exact.C5_PRODUCTION_PORT,
|
||||
"repository_identity": host.REPOSITORY, "global_legacy_writes_enabled": False,
|
||||
"authoritative_git_dir": str(repository), "git_executable": str(git), "git_executable_sha256": _sha(git),
|
||||
"tag_command": _command(executable, script, "tag"),
|
||||
"registry_command": _command(executable, script, "registry"),
|
||||
"baseline_command": _command(executable, script, "baseline"),
|
||||
"registry_record_path": str(registry_record), "baseline_record_path": str(baseline_record),
|
||||
}
|
||||
config_path = _write(root / "execution-config.json", (json.dumps(config_value, sort_keys=True) + "\n").encode())
|
||||
provenance_value = {
|
||||
"schema": host.PROVENANCE_SCHEMA,
|
||||
"environment": "ISOLATED_REHEARSAL",
|
||||
"instance_id": instance_id,
|
||||
"authority_commit": config_value["executor_authority_commit"],
|
||||
"authority_tree": config_value["executor_authority_tree"],
|
||||
"entries": {
|
||||
"host": {"path": str(Path(host.__file__).resolve()), "sha256": _sha(Path(host.__file__).resolve())},
|
||||
"exact_deployment": {"path": str(Path(exact.__file__).resolve()), "sha256": _sha(Path(exact.__file__).resolve())},
|
||||
"deployment": {"path": str(script), "sha256": _sha(script)},
|
||||
"approved_executor": {"path": str(Path(host.approved_executor_module.__file__).resolve()), "sha256": _sha(Path(host.approved_executor_module.__file__).resolve())},
|
||||
"tag": {"path": str(script), "sha256": _sha(script)},
|
||||
"registry": {"path": str(script), "sha256": _sha(script)},
|
||||
"baseline": {"path": str(script), "sha256": _sha(script)},
|
||||
},
|
||||
}
|
||||
provenance_path = _write(root / "host-provenance.json", (json.dumps(provenance_value, sort_keys=True) + "\n").encode())
|
||||
provenance = host.C5HostProvenance.load(provenance_path, expected_sha256=_sha(provenance_path), expected_owner_uid=None)
|
||||
config = host.C5ExecutionHostConfig.load(
|
||||
config_path,
|
||||
expected_sha256=_sha(config_path),
|
||||
host_provenance=provenance,
|
||||
expected_owner_uid=None,
|
||||
)
|
||||
assert config.executor_authority_commit != config.source_commit
|
||||
assert config.executor_authority_tree != config.tree_oid
|
||||
|
||||
wrong_executor_authority = {
|
||||
**config_value,
|
||||
"executor_authority_commit": "c" * 40,
|
||||
}
|
||||
wrong_executor_config = _write(
|
||||
root / "wrong-executor-authority.json",
|
||||
(json.dumps(wrong_executor_authority, sort_keys=True) + "\n").encode(),
|
||||
)
|
||||
with pytest.raises(
|
||||
host.C5ExecutionHostBlocked,
|
||||
match="HOST_PROVENANCE_BINDING_MISMATCH",
|
||||
):
|
||||
host.C5ExecutionHostConfig.load(
|
||||
wrong_executor_config,
|
||||
expected_sha256=_sha(wrong_executor_config),
|
||||
host_provenance=provenance,
|
||||
expected_owner_uid=None,
|
||||
)
|
||||
|
||||
production_claim = {
|
||||
**config_value,
|
||||
"execution_scope": "PRODUCTION",
|
||||
"provenance": {
|
||||
"environment": "PRODUCTION",
|
||||
"fixture": False,
|
||||
"r14_source_receipt": True,
|
||||
},
|
||||
}
|
||||
production_config = _write(
|
||||
root / "production-claim.json",
|
||||
(json.dumps(production_claim, sort_keys=True) + "\n").encode(),
|
||||
)
|
||||
with pytest.raises(
|
||||
host.C5ExecutionHostBlocked,
|
||||
match="HOST_PROVENANCE_BINDING_MISMATCH",
|
||||
):
|
||||
host.C5ExecutionHostConfig.load(
|
||||
production_config,
|
||||
expected_sha256=_sha(production_config),
|
||||
host_provenance=provenance,
|
||||
expected_owner_uid=None,
|
||||
)
|
||||
|
||||
escaped_rehearsal = {**config_value, "rollback_artifact_path": str(tmp_path / "outside-c4.tar.gz")}
|
||||
escaped_path = _write(
|
||||
root / "escaped-rehearsal.json",
|
||||
(json.dumps(escaped_rehearsal, sort_keys=True) + "\n").encode(),
|
||||
)
|
||||
with pytest.raises(host.C5ExecutionHostBlocked, match="REHEARSAL_FIXTURE_OUTSIDE_ISOLATION_ROOT"):
|
||||
host.C5ExecutionHostConfig.load(
|
||||
escaped_path,
|
||||
expected_sha256=_sha(escaped_path),
|
||||
host_provenance=provenance,
|
||||
expected_owner_uid=None,
|
||||
)
|
||||
|
||||
production_with_rehearsal_bindings = {
|
||||
**config_value,
|
||||
"execution_scope": "PRODUCTION",
|
||||
"provenance": {"environment": "PRODUCTION", "fixture": False, "r14_source_receipt": True},
|
||||
}
|
||||
production_provenance = dataclasses.replace(provenance, environment="PRODUCTION")
|
||||
production_with_rehearsal_path = _write(
|
||||
root / "production-with-rehearsal-bindings.json",
|
||||
(json.dumps(production_with_rehearsal_bindings, sort_keys=True) + "\n").encode(),
|
||||
)
|
||||
with pytest.raises(host.C5ExecutionHostBlocked, match="HOST_CONFIG_BINDING_INVALID"):
|
||||
host.C5ExecutionHostConfig.load(
|
||||
production_with_rehearsal_path,
|
||||
expected_sha256=_sha(production_with_rehearsal_path),
|
||||
host_provenance=production_provenance,
|
||||
expected_owner_uid=None,
|
||||
)
|
||||
|
||||
result = host.build_execution_host(config, expected_owner_uid=None).execute()
|
||||
|
||||
assert result["state"] == "COMPLETED"
|
||||
assert result["execution_scope"] == "ISOLATED_REHEARSAL"
|
||||
assert result["fixture_evidence"] is True
|
||||
assert result["production_receipt"] is False
|
||||
assert result["public_business_e2e_verified"] is False
|
||||
assert result["production_acceptance"] is False
|
||||
assert result["provenance"]["r14_source_receipt"] is False
|
||||
|
||||
|
||||
def test_rehearsal_result_label_can_never_be_a_production_receipt():
|
||||
fake_provenance = type(
|
||||
"HostProvenance",
|
||||
(),
|
||||
{
|
||||
"authority_commit": "a" * 40,
|
||||
"authority_tree": "b" * 40,
|
||||
"manifest_sha256": "c" * 64,
|
||||
},
|
||||
)()
|
||||
fake = type("Config", (), {"candidate_id": "fixture", "execution_scope": "ISOLATED_REHEARSAL", "instance_id": "x", "provenance": {"fixture": True}, "host_provenance": fake_provenance})()
|
||||
execution = host.C5ExecutionHost(fake, type("Orchestrator", (), {"execute": lambda self, _candidate: {"state": "COMPLETED"}})())
|
||||
result = execution.execute()
|
||||
assert result["production_receipt"] is False
|
||||
@@ -4,7 +4,7 @@ import hashlib
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
@@ -19,25 +19,84 @@ def _sha(data: bytes) -> str:
|
||||
|
||||
|
||||
def _fixture(tmp_path: Path) -> tuple[Path, str, Path, Path, Path]:
|
||||
repo = Path(__file__).parents[1]
|
||||
source_repo = Path(__file__).parents[1]
|
||||
repo = tmp_path / "source"
|
||||
repo.mkdir()
|
||||
subprocess.run(["git", "init", "--quiet", str(repo)], check=True)
|
||||
subprocess.run(
|
||||
["git", "-C", str(repo), "config", "user.name", "C5 fixture"], check=True
|
||||
)
|
||||
subprocess.run(
|
||||
["git", "-C", str(repo), "config", "user.email", "c5-fixture.invalid"],
|
||||
check=True,
|
||||
)
|
||||
|
||||
source_paths = (
|
||||
"active-runtime/app/source_publication_runtime.py",
|
||||
"active-runtime/app/governed_source_publication_policy.py",
|
||||
"active-runtime/app/registered_owner_signer_adapter.py",
|
||||
"active-runtime/app/aios_single_host_authority.py",
|
||||
"active-runtime/app/c5_staging_installation.py",
|
||||
)
|
||||
for relative in source_paths:
|
||||
destination = repo / relative
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
destination.write_bytes((source_repo / relative).read_bytes())
|
||||
|
||||
bridge_base = b'"""Deterministic C5 bridge base fixture."""\n'
|
||||
bridge_builder_source = (
|
||||
source_repo / "scripts" / "build_c5_live_bridge_overlay.py"
|
||||
).read_text(encoding="utf-8")
|
||||
bridge_builder_source, replacements = re.subn(
|
||||
r'(?m)^BASE_SHA256 = "[0-9a-f]{64}"$',
|
||||
f'BASE_SHA256 = "{_sha(bridge_base)}"',
|
||||
bridge_builder_source,
|
||||
count=1,
|
||||
)
|
||||
assert replacements == 1
|
||||
bridge_builder = repo / "scripts" / "build_c5_live_bridge_overlay.py"
|
||||
bridge_builder.parent.mkdir(parents=True)
|
||||
bridge_builder.write_text(bridge_builder_source, encoding="utf-8")
|
||||
|
||||
subprocess.run(["git", "-C", str(repo), "add", "--all"], check=True)
|
||||
subprocess.run(
|
||||
["git", "-C", str(repo), "commit", "--quiet", "-m", "C5 test source"],
|
||||
check=True,
|
||||
)
|
||||
support = subprocess.check_output(
|
||||
["git", "-C", str(repo), "rev-parse", "HEAD"], text=True
|
||||
).strip()
|
||||
live = repo.parent / "live-release"
|
||||
overlay = tmp_path / "staging-overlay" / "app"
|
||||
snapshot = overlay.parent.parent / "staging-source-snapshot"
|
||||
source_snapshot = overlay.parent.parent / "staging-source-snapshot"
|
||||
overlay.mkdir(parents=True)
|
||||
snapshot.mkdir(parents=True)
|
||||
for name in (
|
||||
"aios_chatgpt_bridge_v2_server.py",
|
||||
"aios_task_admission_bootstrap.py",
|
||||
"aios_task_admission_live_adapters.py",
|
||||
):
|
||||
shutil.copy2(live / "staging-overlay" / "app" / name, overlay / name)
|
||||
shutil.copy2(live / "staging-source-snapshot" / name, snapshot / name)
|
||||
shutil.copy2(
|
||||
live / "staging-overlay" / "c5-epoch-v2-overlay.result.json",
|
||||
overlay.parent / "c5-epoch-v2-overlay.result.json",
|
||||
source_snapshot.mkdir(parents=True)
|
||||
fixture_modules = {
|
||||
"aios_chatgpt_bridge_v2_server.py": (
|
||||
bridge_base,
|
||||
b'"""Deterministic C5 bridge overlay fixture."""\n',
|
||||
),
|
||||
"aios_task_admission_bootstrap.py": (
|
||||
b'"""Deterministic C5 admission bootstrap base fixture."""\n',
|
||||
b'"""Deterministic C5 admission bootstrap overlay fixture."""\n',
|
||||
),
|
||||
"aios_task_admission_live_adapters.py": (
|
||||
b'"""Deterministic C5 admission adapters base fixture."""\n',
|
||||
b'"""Deterministic C5 admission adapters overlay fixture."""\n',
|
||||
),
|
||||
}
|
||||
overlay_result: dict[str, dict[str, int | str]] = {}
|
||||
for name, (base, result) in fixture_modules.items():
|
||||
(source_snapshot / name).write_bytes(base)
|
||||
(overlay / name).write_bytes(result)
|
||||
if name != "aios_chatgpt_bridge_v2_server.py":
|
||||
overlay_result[name] = {
|
||||
"base_sha256": _sha(base),
|
||||
"base_bytes": len(base),
|
||||
"result_sha256": _sha(result),
|
||||
"result_bytes": len(result),
|
||||
}
|
||||
(overlay.parent / "c5-epoch-v2-overlay.result.json").write_text(
|
||||
json.dumps({"files": overlay_result}, sort_keys=True), encoding="utf-8"
|
||||
)
|
||||
authority = tmp_path / "authority"
|
||||
authority.mkdir()
|
||||
@@ -51,7 +110,7 @@ def _fixture(tmp_path: Path) -> tuple[Path, str, Path, Path, Path]:
|
||||
"policy_sha256": _sha(policy_bytes),
|
||||
"policy_revocations": f"{root}/policy-revocations.json",
|
||||
"admission_activation": f"{root}/admission-activation.json",
|
||||
"expected_agent_id": "agent/codex-c5-release",
|
||||
"expected_agent_id": "agent/fixture-c5-release",
|
||||
}
|
||||
(authority / "source-activation.json").write_bytes(json.dumps(source_activation).encode())
|
||||
(authority / "policy-revocations.json").write_bytes(b"{}")
|
||||
@@ -92,6 +151,11 @@ def test_builds_regular_only_archive_and_typed_request(tmp_path: Path) -> None:
|
||||
paths = {record["path"] for record in request["files"]}
|
||||
assert any(path.endswith("/app/c5_source_authority.py") for path in paths)
|
||||
assert any(path.endswith("/app/aios_task_admission_live_adapters.py") for path in paths)
|
||||
assert (
|
||||
"/etc/systemd/system/aios-chatgpt-bridge-v2-staging.service.d/"
|
||||
+ "~" * 128
|
||||
+ "-c5-r14-source.conf"
|
||||
) in paths
|
||||
with tarfile.open(artifact, "r:") as bundle:
|
||||
members = bundle.getmembers()
|
||||
assert members
|
||||
|
||||
@@ -28,13 +28,36 @@ from registered_owner_signer_adapter import RegisteredOwnerSignerAdapter
|
||||
NOW = 1050
|
||||
SOURCE = "536a2af0f07962495c88526061ce43fb27994c15"
|
||||
SUPPORT = "04a5e3cbfecf8156074f3a68cd72b678c06ace11"
|
||||
R14_PRINCIPAL_FIXTURE = {
|
||||
"task_id": "fixture-c5-r14-source-publication",
|
||||
"agent_id": "fixture-c5-r14-agent",
|
||||
"execution_digest": "81045739c0adf64aab8d0ee13f211d60ab2ca710478140df56a78c96df8e3a62",
|
||||
}
|
||||
|
||||
|
||||
def sha(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("profile", [c5._BRIDGE_PROFILE, c5._ISSUER_PROFILE])
|
||||
def test_systemd_dropin_precedence_rejects_bootstrap_override(monkeypatch, profile):
|
||||
backend = c5.SystemdBackend()
|
||||
parent = Path("/etc/systemd/system") / (profile.unit + ".d")
|
||||
active = parent / ("~" * 67 + "-c5-bootstrap-admission-20261001-r10.conf")
|
||||
monkeypatch.setattr(backend, "_run", lambda command: SimpleNamespace(stdout=active.as_posix()))
|
||||
with pytest.raises(c5.InstallationError, match="STAGING_DROPIN_PRECEDENCE_CONFLICT"):
|
||||
backend.verify_dropin_precedence(profile, parent / ("~" * 64 + "-c5-r14-source.conf"))
|
||||
backend.verify_dropin_precedence(profile, profile.dropin(c5._Layout()))
|
||||
later = parent / ("~" * 129 + "-unexpected-override.conf")
|
||||
monkeypatch.setattr(backend, "_run", lambda command: SimpleNamespace(stdout=later.as_posix()))
|
||||
with pytest.raises(c5.InstallationError, match="STAGING_DROPIN_PRECEDENCE_CONFLICT"):
|
||||
backend.verify_dropin_precedence(profile, profile.dropin(c5._Layout()))
|
||||
|
||||
|
||||
def file_record(path: str, data: bytes, kind: str, mode: int = 0o644) -> dict:
|
||||
fixture = Path(path)
|
||||
if fixture.is_file() and fixture.read_bytes() == data:
|
||||
fixture.chmod(mode)
|
||||
return {
|
||||
"path": path,
|
||||
"sha256": sha(data),
|
||||
@@ -54,9 +77,9 @@ def principal_material(root: Path) -> tuple[bytes, dict[str, bytes]]:
|
||||
}
|
||||
config = {
|
||||
"schema": "aios.c5-task-principal-activation.v1",
|
||||
"task_id": "TASK-V2R2C5-SOURCE-PUBLICATION-20260926-R1",
|
||||
"agent_id": "agent/codex-c5-release",
|
||||
"execution_digest": "1ba534d551991e8c99014285eeea36afcbc1500686035cb8bcd7f9f4efd6215e",
|
||||
"task_id": "fixture-c5-source-publication",
|
||||
"agent_id": "fixture-c5-agent",
|
||||
"execution_digest": "1" * 64,
|
||||
"store_path": "/var/lib/aios-gitops/c5-source-principal/enrollment.sqlite3",
|
||||
"grant_db": "/var/lib/aios-gitops/bridge-v2-staging/task-admission.sqlite3",
|
||||
"plan_dir": (root / "plans").as_posix(),
|
||||
@@ -155,8 +178,8 @@ class FakeBackend:
|
||||
True,
|
||||
self.case.old_config_hash,
|
||||
self.case.old_pythonpath,
|
||||
None,
|
||||
None,
|
||||
self.case.payload["existing"]["activation_file"],
|
||||
self.case.payload["existing"]["principal_scope_config"],
|
||||
)
|
||||
|
||||
def daemon_reload(self) -> None:
|
||||
@@ -463,6 +486,131 @@ def make_issuer_case(tmp_path: Path) -> Case:
|
||||
return case
|
||||
|
||||
|
||||
def make_successor_bridge_case(tmp_path: Path) -> Case:
|
||||
case = make_case(tmp_path)
|
||||
digest = R14_PRINCIPAL_FIXTURE["execution_digest"]
|
||||
plan = (Path(__file__).parent / "fixtures" / "c5-r14-execution-plan.json").read_bytes()
|
||||
assert sha(plan) == digest
|
||||
previous = case.layout.release_base / "previous-release"
|
||||
previous.mkdir()
|
||||
old_activation = previous / "source-activation.json"
|
||||
old_activation.write_bytes(b'{"schema":"previous-source-activation"}\n')
|
||||
case.payload["schema"] = c5.SUCCESSOR_SCHEMA
|
||||
case.payload["source_commit"] = c5.R14_SOURCE_COMMIT
|
||||
case.payload["existing"]["activation_file"] = old_activation.as_posix()
|
||||
case.payload["existing"]["config_files"] = [
|
||||
file_record(old_activation.as_posix(), old_activation.read_bytes(), "existing_config")
|
||||
]
|
||||
case.payload["rollback"]["readiness"]["activation_file"] = old_activation.as_posix()
|
||||
|
||||
config_raw, supporting = principal_material(case.root)
|
||||
config = json.loads(config_raw)
|
||||
config.update(
|
||||
task_id=R14_PRINCIPAL_FIXTURE["task_id"],
|
||||
agent_id=R14_PRINCIPAL_FIXTURE["agent_id"],
|
||||
execution_digest=digest,
|
||||
)
|
||||
supporting.pop("plans/task.json")
|
||||
supporting[f"plans/{digest}.json"] = plan
|
||||
supporting["admission-activation.json"] = c5.canonical_bytes(
|
||||
{"schema": "aios.task-admission-host-activation.v1"}
|
||||
)
|
||||
config_raw = c5.canonical_bytes(config)
|
||||
activation_raw = c5.canonical_bytes({
|
||||
"schema": "aios.source-publication-host-activation.v1",
|
||||
"policy_artifact": (case.root / "policy.json").as_posix(),
|
||||
"policy_sha256": sha(supporting["policy.json"]),
|
||||
"policy_revocations": (case.root / "policy-revocations.json").as_posix(),
|
||||
"admission_activation": (case.root / "admission-activation.json").as_posix(),
|
||||
"expected_agent_id": R14_PRINCIPAL_FIXTURE["agent_id"],
|
||||
})
|
||||
case.activation_bytes = activation_raw
|
||||
case.principal_config_path = (case.root / "principal-activation.json").as_posix()
|
||||
case.new_dropin = c5._dropin_bytes(
|
||||
case.root, case.old_pythonpath,
|
||||
target=c5.BRIDGE_TARGET, principal_config=True,
|
||||
)
|
||||
case.payload["readiness"]["principal_scope_config"] = case.principal_config_path
|
||||
members = [
|
||||
("app/aios_chatgpt_bridge_v2_server.py", case.new_module_bytes),
|
||||
("source-activation.json", activation_raw),
|
||||
("principal-activation.json", config_raw),
|
||||
*sorted(supporting.items()),
|
||||
]
|
||||
case.files = [
|
||||
file_record((case.root / name).as_posix(), data, "release_file")
|
||||
for name, data in members
|
||||
] + [file_record(case.layout.dropin.as_posix(), case.new_dropin, "systemd_dropin")]
|
||||
case.payload["files"] = case.files
|
||||
write_tar(case.artifact, [
|
||||
(name, data, 0o644, "file") for name, data in members
|
||||
] + [("dropin.conf", case.new_dropin, 0o644, "file")])
|
||||
resign(case)
|
||||
return case
|
||||
|
||||
|
||||
def make_successor_issuer_case(tmp_path: Path) -> Case:
|
||||
"""Build the R14 successor issuer case used by installer and seam tests."""
|
||||
case = make_issuer_case(tmp_path)
|
||||
digest = R14_PRINCIPAL_FIXTURE["execution_digest"]
|
||||
plan = (Path(__file__).parent / "fixtures" / "c5-r14-execution-plan.json").read_bytes()
|
||||
previous = case.layout.release_base / "previous-issuer"
|
||||
previous.mkdir()
|
||||
old_config = previous / "principal-activation.json"
|
||||
old_config.write_bytes(b'{"schema":"previous-principal-activation"}\n')
|
||||
case.payload["schema"] = c5.SUCCESSOR_SCHEMA
|
||||
case.payload["source_commit"] = c5.R14_SOURCE_COMMIT
|
||||
case.payload["existing"]["principal_scope_config"] = old_config.as_posix()
|
||||
case.payload["existing"]["config_files"] = [
|
||||
file_record(old_config.as_posix(), old_config.read_bytes(), "existing_config")
|
||||
]
|
||||
case.payload["rollback"]["readiness"]["principal_scope_config"] = old_config.as_posix()
|
||||
case.old_runtime.update(
|
||||
c5_scope_enabled=True, c5_scope_role="ISSUER", c5_scope_enrolled=False,
|
||||
)
|
||||
case.payload["rollback"]["readiness"]["runtime_fields"] = dict(case.old_runtime)
|
||||
|
||||
config_path = Path(case.principal_config_path)
|
||||
config = json.loads(config_path.read_bytes())
|
||||
config.update(
|
||||
task_id=R14_PRINCIPAL_FIXTURE["task_id"],
|
||||
agent_id=R14_PRINCIPAL_FIXTURE["agent_id"],
|
||||
execution_digest=digest,
|
||||
)
|
||||
config_path.write_bytes(c5.canonical_bytes(config))
|
||||
(case.root / "plans" / "task.json").unlink()
|
||||
(case.root / "plans" / f"{digest}.json").write_bytes(plan)
|
||||
admission = case.root / "admission-activation.json"
|
||||
admission.write_bytes(
|
||||
c5.canonical_bytes({"schema": "aios.task-admission-host-activation.v1"})
|
||||
)
|
||||
activation = {
|
||||
"schema": "aios.source-publication-host-activation.v1",
|
||||
"policy_artifact": (case.root / "policy.json").as_posix(),
|
||||
"policy_sha256": sha((case.root / "policy.json").read_bytes()),
|
||||
"policy_revocations": (case.root / "policy-revocations.json").as_posix(),
|
||||
"admission_activation": admission.as_posix(),
|
||||
"expected_agent_id": R14_PRINCIPAL_FIXTURE["agent_id"],
|
||||
}
|
||||
case.activation.write_bytes(c5.canonical_bytes(activation))
|
||||
case.files = [
|
||||
file_record(path.as_posix(), path.read_bytes(), "release_file")
|
||||
for path in sorted(case.root.rglob("*")) if path.is_file()
|
||||
] + [
|
||||
file_record(
|
||||
case.layout.issuer_dropin.as_posix(), case.new_dropin,
|
||||
"systemd_dropin",
|
||||
)
|
||||
]
|
||||
case.payload["files"] = case.files
|
||||
write_tar(case.artifact, [
|
||||
(path.relative_to(case.root).as_posix(), path.read_bytes(), 0o644, "file")
|
||||
for path in sorted(case.root.rglob("*")) if path.is_file()
|
||||
] + [("dropin.conf", case.new_dropin, 0o644, "file")])
|
||||
resign(case)
|
||||
return case
|
||||
|
||||
|
||||
def resign(case: Case) -> None:
|
||||
signature = sign_sshsig(case.key, c5.canonical_bytes(case.payload), namespace=OWNER_NAMESPACE)
|
||||
case.contract.write_text(
|
||||
@@ -663,6 +811,126 @@ def test_apply_and_same_contract_replay_verify_bytes_and_runtime(tmp_path: Path)
|
||||
case.installer.apply(case.contract, case.artifact)
|
||||
|
||||
|
||||
def test_successor_install_pins_old_activation_and_rolls_back(tmp_path: Path) -> None:
|
||||
case = make_successor_bridge_case(tmp_path)
|
||||
old_activation = Path(case.payload["existing"]["activation_file"])
|
||||
old_bytes = old_activation.read_bytes()
|
||||
|
||||
assert case.installer.preflight(case.contract, case.artifact)["status"] == "PREFLIGHT_PASS"
|
||||
case.backend.fail_new = True
|
||||
with pytest.raises(c5.InstallationError, match="ROLLBACK_VERIFIED"):
|
||||
case.installer.apply(case.contract, case.artifact)
|
||||
assert old_activation.read_bytes() == old_bytes
|
||||
assert case.layout.dropin.read_bytes() == case.old_dropin
|
||||
assert not case.root.exists()
|
||||
|
||||
|
||||
def test_successor_rejects_old_config_drift_before_side_effect(tmp_path: Path) -> None:
|
||||
case = make_successor_bridge_case(tmp_path)
|
||||
old_activation = Path(case.payload["existing"]["activation_file"])
|
||||
old_activation.write_bytes(b"changed\n")
|
||||
|
||||
with pytest.raises(c5.InstallationError, match="INSTALLED_FILE_MISMATCH"):
|
||||
case.installer.preflight(case.contract, case.artifact)
|
||||
assert case.backend.reloads == case.backend.restarts == 0
|
||||
assert case.layout.dropin.read_bytes() == case.old_dropin
|
||||
|
||||
|
||||
@pytest.mark.parametrize("generation", ("r8", "r9", "r10"))
|
||||
def test_successor_accepts_registered_bootstrap_config_root(
|
||||
tmp_path: Path, generation: str,
|
||||
) -> None:
|
||||
case = make_successor_bridge_case(tmp_path)
|
||||
old = Path(case.payload["existing"]["activation_file"])
|
||||
legacy_root = (
|
||||
case.layout.release_base.parent.parent
|
||||
/ f"c5-bootstrap-{generation}-releases"
|
||||
/ c5.BOOTSTRAP_SOURCE_NAME
|
||||
)
|
||||
legacy_root.mkdir(parents=True)
|
||||
moved = legacy_root / old.name
|
||||
old.replace(moved)
|
||||
case.payload["existing"]["activation_file"] = moved.as_posix()
|
||||
case.payload["existing"]["config_files"][0]["path"] = moved.as_posix()
|
||||
case.payload["rollback"]["readiness"]["activation_file"] = moved.as_posix()
|
||||
resign(case)
|
||||
assert case.installer.preflight(case.contract, case.artifact)["status"] == "PREFLIGHT_PASS"
|
||||
|
||||
|
||||
def test_successor_rejects_other_external_config_root(tmp_path: Path) -> None:
|
||||
case = make_successor_bridge_case(tmp_path)
|
||||
old = Path(case.payload["existing"]["activation_file"])
|
||||
other = case.layout.release_base.parent.parent / "unregistered-release" / old.name
|
||||
other.parent.mkdir(parents=True)
|
||||
old.replace(other)
|
||||
case.payload["existing"]["activation_file"] = other.as_posix()
|
||||
case.payload["existing"]["config_files"][0]["path"] = other.as_posix()
|
||||
case.payload["rollback"]["readiness"]["activation_file"] = other.as_posix()
|
||||
resign(case)
|
||||
with pytest.raises(c5.InstallationError, match="EXISTING_CONFIG_SNAPSHOT_INVALID"):
|
||||
case.installer.preflight(case.contract, case.artifact)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("directory", (
|
||||
"c5-bootstrap-r9-releases/other-source",
|
||||
"c5-bootstrap-test-releases/c5-source-b915386ccbed46a7ae77220887da8ae666649a79",
|
||||
"unregistered/c5-bootstrap-r9-releases/c5-source-b915386ccbed46a7ae77220887da8ae666649a79",
|
||||
))
|
||||
def test_successor_rejects_lookalike_bootstrap_config_root(
|
||||
tmp_path: Path, directory: str,
|
||||
) -> None:
|
||||
case = make_successor_bridge_case(tmp_path)
|
||||
old = Path(case.payload["existing"]["activation_file"])
|
||||
other = case.layout.release_base.parent.parent / directory / old.name
|
||||
other.parent.mkdir(parents=True)
|
||||
old.replace(other)
|
||||
case.payload["existing"]["activation_file"] = other.as_posix()
|
||||
case.payload["existing"]["config_files"][0]["path"] = other.as_posix()
|
||||
case.payload["rollback"]["readiness"]["activation_file"] = other.as_posix()
|
||||
resign(case)
|
||||
with pytest.raises(c5.InstallationError, match="EXISTING_CONFIG_SNAPSHOT_INVALID"):
|
||||
case.installer.preflight(case.contract, case.artifact)
|
||||
|
||||
|
||||
def test_successor_dropin_takes_precedence_over_current_r12_and_promotion() -> None:
|
||||
assert c5.DROPIN.name > ("~" * 39 + "-c5-r12-source.conf")
|
||||
assert c5.ISSUER_DROPIN.name > ("~" * 50 + "-c5-promotion-issuer-20260928.conf")
|
||||
|
||||
|
||||
def test_successor_rejects_a_different_plan_even_with_matching_manifest(tmp_path: Path) -> None:
|
||||
case = make_successor_bridge_case(tmp_path)
|
||||
digest = R14_PRINCIPAL_FIXTURE["execution_digest"]
|
||||
plan_name = f"plans/{digest}.json"
|
||||
replacement = b'{"other":"plan"}'
|
||||
with tarfile.open(case.artifact, "r") as source:
|
||||
members = [
|
||||
(item.name, source.extractfile(item).read(), item.mode, "file")
|
||||
for item in source.getmembers()
|
||||
]
|
||||
members = [
|
||||
(name, replacement if name == plan_name else data, mode, kind)
|
||||
for name, data, mode, kind in members
|
||||
]
|
||||
for record in case.payload["files"]:
|
||||
if record["path"] == (case.root / plan_name).as_posix():
|
||||
record.update(sha256=sha(replacement), size=len(replacement))
|
||||
write_tar(case.artifact, members)
|
||||
resign(case)
|
||||
with pytest.raises(c5.InstallationError, match="PRINCIPAL_PLAN_DIGEST_MISMATCH"):
|
||||
case.installer.preflight(case.contract, case.artifact)
|
||||
|
||||
|
||||
def test_successor_issuer_preserves_promotion_config_on_failed_cutover(tmp_path: Path) -> None:
|
||||
case = make_successor_issuer_case(tmp_path)
|
||||
old_config = Path(case.payload["existing"]["principal_scope_config"])
|
||||
assert case.installer.preflight(case.contract, case.artifact)["status"] == "PREFLIGHT_PASS"
|
||||
case.backend.fail_new = True
|
||||
with pytest.raises(c5.InstallationError, match="ROLLBACK_VERIFIED"):
|
||||
case.installer.apply(case.contract, case.artifact)
|
||||
assert old_config.read_bytes() == b'{"schema":"previous-principal-activation"}\n'
|
||||
assert case.layout.issuer_dropin.read_bytes() == case.old_dropin
|
||||
|
||||
|
||||
def test_future_bridge_contract_requires_governed_writes_readiness(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
@@ -726,6 +994,110 @@ def test_different_nonce_or_digest_conflicts_after_install(tmp_path: Path) -> No
|
||||
case.installer.apply(case.contract, case.artifact)
|
||||
|
||||
|
||||
def test_fresh_contract_after_rolled_back_support_reuses_no_terminal_state(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
case = make_case(tmp_path)
|
||||
case.backend.fail_new = True
|
||||
with pytest.raises(c5.InstallationError, match="ROLLBACK_VERIFIED"):
|
||||
case.installer.apply(case.contract, case.artifact)
|
||||
old_contract = case.payload["contract_id"]
|
||||
case.payload["contract_id"] = "c5-installation-fresh-after-rollback"
|
||||
case.payload["nonce"] = "nonce-c5-installation-fresh-0001"
|
||||
resign(case)
|
||||
case.backend.fail_new = False
|
||||
assert case.installer.apply(case.contract, case.artifact)["status"] == "APPLIED"
|
||||
assert case.payload["contract_id"] != old_contract
|
||||
assert case.installer._state_path(case.payload) != (
|
||||
case.layout.state_root / f"{case.payload['target']}-support-{case.payload['support_commit']}.json"
|
||||
)
|
||||
|
||||
|
||||
def test_rolled_back_original_contract_remains_terminal_barrier(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
case = make_case(tmp_path)
|
||||
case.backend.fail_new = True
|
||||
with pytest.raises(c5.InstallationError, match="ROLLBACK_VERIFIED"):
|
||||
case.installer.apply(case.contract, case.artifact)
|
||||
case.backend.fail_new = False
|
||||
with pytest.raises(c5.InstallationError, match="INSTALL_PRIOR_ATTEMPT_NOT_APPLIED"):
|
||||
case.installer.apply(case.contract, case.artifact)
|
||||
|
||||
|
||||
def test_applied_support_rejects_different_contract_identity(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
case = make_case(tmp_path)
|
||||
assert case.installer.apply(case.contract, case.artifact)["status"] == "APPLIED"
|
||||
case.payload["contract_id"] = "c5-installation-different-contract"
|
||||
case.payload["nonce"] = "nonce-c5-installation-different-0001"
|
||||
resign(case)
|
||||
with pytest.raises(c5.InstallationError, match="INSTALL_REPLAY_CONFLICT"):
|
||||
case.installer.apply(case.contract, case.artifact)
|
||||
|
||||
|
||||
def test_successive_rolled_back_contract_generations_can_progress(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
case = make_case(tmp_path)
|
||||
case.backend.fail_new = True
|
||||
with pytest.raises(c5.InstallationError, match="ROLLBACK_VERIFIED"):
|
||||
case.installer.apply(case.contract, case.artifact)
|
||||
case.backend.fail_new = False
|
||||
for generation in ("a", "b"):
|
||||
case.payload["contract_id"] = f"c5-installation-generation-{generation}"
|
||||
case.payload["nonce"] = f"nonce-c5-installation-generation-{generation}-0001"
|
||||
resign(case)
|
||||
case.backend.fail_new = True
|
||||
with pytest.raises(c5.InstallationError, match="ROLLBACK_VERIFIED"):
|
||||
case.installer.apply(case.contract, case.artifact)
|
||||
case.backend.fail_new = False
|
||||
|
||||
|
||||
def test_successor_contract_is_blocked_by_unknown_generation(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
case = make_case(tmp_path)
|
||||
case.backend.fail_new = True
|
||||
with pytest.raises(c5.InstallationError, match="ROLLBACK_VERIFIED"):
|
||||
case.installer.apply(case.contract, case.artifact)
|
||||
case.backend.fail_old = False
|
||||
case.payload["contract_id"] = "c5-installation-generation-unknown"
|
||||
case.payload["nonce"] = "nonce-c5-installation-generation-unknown-0001"
|
||||
resign(case)
|
||||
case.backend.fail_new = True
|
||||
case.backend.fail_old = True
|
||||
with pytest.raises(c5.InstallationOutcomeUnknown):
|
||||
case.installer.apply(case.contract, case.artifact)
|
||||
case.backend.fail_new = False
|
||||
case.backend.fail_old = False
|
||||
case.payload["contract_id"] = "c5-installation-successor-after-unknown"
|
||||
case.payload["nonce"] = "nonce-c5-installation-successor-unknown-0001"
|
||||
resign(case)
|
||||
with pytest.raises(c5.InstallationError, match="INSTALL_PRIOR_ATTEMPT_NOT_APPLIED"):
|
||||
case.installer.apply(case.contract, case.artifact)
|
||||
|
||||
|
||||
def test_successor_contract_is_blocked_by_applied_generation(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
case = make_case(tmp_path)
|
||||
case.backend.fail_new = True
|
||||
with pytest.raises(c5.InstallationError, match="ROLLBACK_VERIFIED"):
|
||||
case.installer.apply(case.contract, case.artifact)
|
||||
case.backend.fail_new = False
|
||||
case.payload["contract_id"] = "c5-installation-generation-applied"
|
||||
case.payload["nonce"] = "nonce-c5-installation-generation-applied-0001"
|
||||
resign(case)
|
||||
assert case.installer.apply(case.contract, case.artifact)["status"] == "APPLIED"
|
||||
case.payload["contract_id"] = "c5-installation-successor-after-applied"
|
||||
case.payload["nonce"] = "nonce-c5-installation-successor-applied-0001"
|
||||
resign(case)
|
||||
with pytest.raises(c5.InstallationError, match="INSTALL_PRIOR_ATTEMPT_NOT_APPLIED"):
|
||||
case.installer.apply(case.contract, case.artifact)
|
||||
|
||||
|
||||
def test_existing_release_directory_is_never_overwritten(tmp_path: Path) -> None:
|
||||
case = make_case(tmp_path)
|
||||
case.root.mkdir()
|
||||
@@ -867,7 +1239,7 @@ class SigningRunner:
|
||||
return SimpleNamespace(returncode=0)
|
||||
|
||||
|
||||
def producer_case(tmp_path: Path, ttl: int = 100):
|
||||
def producer_case(tmp_path: Path, monkeypatch, ttl: int = 100):
|
||||
key = Ed25519PrivateKey.generate()
|
||||
runner = SigningRunner(key)
|
||||
registry = tmp_path / "producer-registry.json"
|
||||
@@ -885,14 +1257,23 @@ def producer_case(tmp_path: Path, ttl: int = 100):
|
||||
test_mode=True,
|
||||
runner=runner,
|
||||
)
|
||||
root = c5.RELEASE_BASE / f"c5-source-{SUPPORT}"
|
||||
layout = c5._Layout(
|
||||
release_base=tmp_path / "var" / "releases",
|
||||
dropin=tmp_path / "etc" / f"{c5.UNIT}.d" / c5.DROPIN.name,
|
||||
issuer_dropin=tmp_path / "etc" / f"{c5.ISSUER_UNIT}.d" / c5.ISSUER_DROPIN.name,
|
||||
state_root=tmp_path / "var" / "state",
|
||||
test_only=True,
|
||||
)
|
||||
monkeypatch.setattr(c5, "_Layout", lambda: layout)
|
||||
root = layout.release_base / f"c5-source-{SUPPORT}"
|
||||
module = root / "app" / "aios_chatgpt_bridge_v2_server.py"
|
||||
activation = root / "source-activation.json"
|
||||
old_module = Path("/opt/aios/current/app/aios_chatgpt_bridge_v2_server.py")
|
||||
old_root = tmp_path / "opt" / "aios" / "current"
|
||||
old_module = old_root / "app" / "aios_chatgpt_bridge_v2_server.py"
|
||||
module_bytes = b"new\n"
|
||||
activation_bytes = b"{}\n"
|
||||
old_bytes = b"old\n"
|
||||
old_paths = ("/opt/aios/current", "/opt/aios/current/app")
|
||||
old_paths = (old_root.as_posix(), (old_root / "app").as_posix())
|
||||
dropin = c5._dropin_bytes(root, old_paths)
|
||||
new_runtime = {
|
||||
"status": "active", "health": 200, "deployment_state": "STAGING",
|
||||
@@ -912,7 +1293,7 @@ def producer_case(tmp_path: Path, ttl: int = 100):
|
||||
files=(
|
||||
c5.FileRecord(**file_record(module.as_posix(), module_bytes, "release_file")),
|
||||
c5.FileRecord(**file_record(activation.as_posix(), activation_bytes, "release_file")),
|
||||
c5.FileRecord(**file_record(c5.DROPIN.as_posix(), dropin, "systemd_dropin")),
|
||||
c5.FileRecord(**file_record(layout.dropin.as_posix(), dropin, "systemd_dropin")),
|
||||
),
|
||||
existing={
|
||||
"unit_active": True,
|
||||
@@ -950,11 +1331,11 @@ def producer_case(tmp_path: Path, ttl: int = 100):
|
||||
("dropin.conf", dropin, 0o644, "file"),
|
||||
],
|
||||
)
|
||||
return request, artifact, registry, signer, runner
|
||||
return request, artifact, registry, signer, runner, layout
|
||||
|
||||
|
||||
def test_typed_producer_validates_then_signs_and_consumer_verifies(tmp_path: Path) -> None:
|
||||
request, artifact, registry, signer, runner = producer_case(tmp_path)
|
||||
def test_typed_producer_validates_then_signs_and_consumer_verifies(tmp_path: Path, monkeypatch) -> None:
|
||||
request, artifact, registry, signer, runner, layout = producer_case(tmp_path, monkeypatch)
|
||||
outer = c5.issue_signed_contract(
|
||||
request,
|
||||
artifact,
|
||||
@@ -971,12 +1352,13 @@ def test_typed_producer_validates_then_signs_and_consumer_verifies(tmp_path: Pat
|
||||
registry,
|
||||
now=NOW,
|
||||
require_root_owner=False,
|
||||
_layout=layout,
|
||||
)
|
||||
assert payload["owner_key_id"] == signer.fingerprint
|
||||
|
||||
|
||||
def test_typed_producer_rejects_before_private_signer_call(tmp_path: Path) -> None:
|
||||
request, artifact, registry, signer, runner = producer_case(tmp_path, ttl=901)
|
||||
def test_typed_producer_rejects_before_private_signer_call(tmp_path: Path, monkeypatch) -> None:
|
||||
request, artifact, registry, signer, runner, _layout = producer_case(tmp_path, monkeypatch, ttl=901)
|
||||
with pytest.raises(c5.InstallationError, match="INSTALL_TTL_OR_NONCE_INVALID"):
|
||||
c5.issue_signed_contract(
|
||||
request,
|
||||
@@ -996,5 +1378,5 @@ def test_production_cli_has_no_test_backend_or_arbitrary_root() -> None:
|
||||
assert "--test-backend" not in source
|
||||
assert "--release-root" not in source
|
||||
assert c5.DROPIN == Path("/etc/systemd/system") / f"{c5.UNIT}.d" / (
|
||||
"~" * 32 + "-c5-source.conf"
|
||||
"~" * 128 + "-c5-r14-source.conf"
|
||||
)
|
||||
|
||||
@@ -70,7 +70,8 @@ class Writer:
|
||||
}
|
||||
|
||||
|
||||
def client(writer: Writer, files=None, readback=None, *, resolver=None, git=False) -> GovernedPublicationClient:
|
||||
def client(writer: Writer, files=None, readback=None, *, resolver=None, git=False,
|
||||
diagnostic_sink=None) -> GovernedPublicationClient:
|
||||
i = intent()
|
||||
source = files or {p: p.encode() for p in i.file_allowlist}
|
||||
evidence = readback or {
|
||||
@@ -90,6 +91,7 @@ def client(writer: Writer, files=None, readback=None, *, resolver=None, git=Fals
|
||||
authenticated_readback=lambda **_kwargs: evidence,
|
||||
subject="aios-autonomous-admin", manifest_id="manifest:c5",
|
||||
runtime_fingerprint="runtime:c5", process_start_identity="proc:c5",
|
||||
diagnostic_sink=diagnostic_sink,
|
||||
)
|
||||
|
||||
|
||||
@@ -162,6 +164,53 @@ def test_transport_failure_is_unknown_and_never_retried():
|
||||
assert [call[0] for call in writer.calls] == ["acquire"]
|
||||
|
||||
|
||||
def test_isolated_diagnostic_records_only_safe_issuer_fields():
|
||||
class IssuerFailure(RuntimeError):
|
||||
status = 403
|
||||
error_code = "EXECUTION_PLAN_SCHEMA_INVALID"
|
||||
operation = "acquireWriterLease"
|
||||
|
||||
evidence = []
|
||||
writer = Writer(fail=IssuerFailure("token=secret-value signature=secret-value"))
|
||||
i = intent()
|
||||
with pytest.raises(GovernedPublicationError, match="PUBLICATION_EFFECT_UNKNOWN"):
|
||||
client(writer, diagnostic_sink=evidence.append).publish(i, admission(i))
|
||||
assert evidence == [{
|
||||
"task_id": i.task_id, "operation": "acquireWriterLease",
|
||||
"http_status": 403, "error_code": "EXECUTION_PLAN_SCHEMA_INVALID",
|
||||
}]
|
||||
assert "secret-value" not in str(evidence)
|
||||
assert [call[0] for call in writer.calls] == ["acquire"]
|
||||
|
||||
|
||||
def test_isolated_diagnostic_marks_transport_unknown_without_inventing_http():
|
||||
evidence = []
|
||||
i = intent()
|
||||
with pytest.raises(GovernedPublicationError, match="PUBLICATION_EFFECT_UNKNOWN"):
|
||||
client(Writer(fail=TimeoutError("token=secret-value")),
|
||||
diagnostic_sink=evidence.append).publish(i, admission(i))
|
||||
assert evidence == [{
|
||||
"task_id": i.task_id, "operation": "acquireWriterLease",
|
||||
"http_status": None, "error_code": "transport_unknown",
|
||||
}]
|
||||
assert "secret-value" not in str(evidence)
|
||||
|
||||
|
||||
def test_isolated_diagnostic_rejects_unrecognized_response_code():
|
||||
class UntrustedCode(RuntimeError):
|
||||
status = 403
|
||||
error_code = "TOKEN_SECRET_VALUE"
|
||||
|
||||
evidence = []
|
||||
i = intent()
|
||||
with pytest.raises(GovernedPublicationError, match="PUBLICATION_EFFECT_UNKNOWN"):
|
||||
client(Writer(fail=UntrustedCode("secret")),
|
||||
diagnostic_sink=evidence.append).publish(i, admission(i))
|
||||
assert evidence[0]["http_status"] == 403
|
||||
assert evidence[0]["error_code"] == "issuer_error_unknown"
|
||||
assert "TOKEN_SECRET_VALUE" not in str(evidence)
|
||||
|
||||
|
||||
def test_missing_authenticated_readback_fails_closed():
|
||||
writer = Writer()
|
||||
i = intent()
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
"""Immutable V3 issuer overlay assembly from a committed source tree."""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tarfile
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"build_c5_live_principal_overlay", ROOT / "scripts" / "build_c5_live_principal_overlay.py",
|
||||
)
|
||||
assert spec is not None and spec.loader is not None
|
||||
builder = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(builder)
|
||||
|
||||
|
||||
def _commit() -> str:
|
||||
return subprocess.run(
|
||||
["git", "rev-parse", "HEAD"], cwd=ROOT, check=True,
|
||||
capture_output=True, text=True,
|
||||
).stdout.strip()
|
||||
|
||||
|
||||
def test_overlay_archive_is_deterministic_and_hash_bound(tmp_path):
|
||||
fixture = ROOT / "tests" / "fixtures" / "issuer-v3-base"
|
||||
manifests = [builder.build_issuer_v3_overlay(
|
||||
adapter_source_root=fixture, output_root=tmp_path / str(i),
|
||||
source_commit=_commit(), repository_root=ROOT,
|
||||
) for i in (1, 2)]
|
||||
assert manifests[0]["artifact_sha256"] == manifests[1]["artifact_sha256"]
|
||||
assert manifests[0]["file_manifest_sha256"] == manifests[1]["file_manifest_sha256"]
|
||||
archive = tmp_path / "1" / manifests[0]["artifact_filename"]
|
||||
assert builder.sha256(archive) == manifests[0]["artifact_sha256"]
|
||||
with tarfile.open(archive, "r") as bundle:
|
||||
assert set(bundle.getnames()) == set(manifests[0]["files"])
|
||||
for member in bundle:
|
||||
assert member.uid == member.gid == member.mtime == 0
|
||||
assert member.mode == 0o444
|
||||
assert builder.hashlib.sha256(bundle.extractfile(member).read()).hexdigest() == (
|
||||
manifests[0]["files"][member.name]
|
||||
)
|
||||
|
||||
|
||||
def test_base_snapshot_drift_is_rejected_before_assembly(tmp_path):
|
||||
source = ROOT / "tests" / "fixtures" / "issuer-v3-base"
|
||||
copied = tmp_path / "base"
|
||||
copied.mkdir()
|
||||
for item in source.glob("*.py"):
|
||||
(copied / item.name).write_bytes(item.read_bytes())
|
||||
(copied / "governed_runtime_scope.py").write_bytes(b"changed")
|
||||
with pytest.raises(SystemExit, match="ISSUER_V3_BASE_HASH_MISMATCH"):
|
||||
builder.build_issuer_v3_overlay(
|
||||
adapter_source_root=copied, output_root=tmp_path / "out",
|
||||
source_commit=_commit(), repository_root=ROOT,
|
||||
)
|
||||
@@ -0,0 +1,227 @@
|
||||
"""Isolated issuer V3 readback against the real V1/V2 base snapshots."""
|
||||
from dataclasses import asdict, replace
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parent
|
||||
sys.path.insert(0, str(HERE / "fixtures" / "issuer-v3-base"))
|
||||
sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
|
||||
|
||||
import governed_runtime_scope as scope # noqa: E402
|
||||
import aios_task_admission_live_adapters as admission_adapter # noqa: E402
|
||||
from aios_task_principal_v3_receipt_provider import ( # noqa: E402
|
||||
IssuerV3ReceiptScopeProvider, _read_only_bootstrap,
|
||||
)
|
||||
from test_generic_reserve_before_admit import ( # noqa: E402
|
||||
plan_for, write_plan, runtime_for, bound_provider, RecordingTransport,
|
||||
)
|
||||
|
||||
|
||||
pytestmark = pytest.mark.skipif(os.name != "posix", reason="producer store requires POSIX")
|
||||
|
||||
|
||||
class Legacy:
|
||||
def __init__(self):
|
||||
self.calls = 0
|
||||
|
||||
def resolve(self, **_kwargs):
|
||||
self.calls += 1
|
||||
return "LEGACY"
|
||||
|
||||
|
||||
def fixture(tmp_path, monkeypatch):
|
||||
plan = plan_for()
|
||||
grant = write_plan(tmp_path / "plans", plan)
|
||||
events = []
|
||||
runtime = runtime_for(tmp_path, plan, monkeypatch, events)
|
||||
transport = RecordingTransport([])
|
||||
port = admission_adapter.AgentTaskAdmissionPort(
|
||||
transport, execution_plan_provider=bound_provider(runtime, tmp_path / "plans")
|
||||
)
|
||||
assert port.admit(grant)["state"] == "QUEUED"
|
||||
job_id = runtime.bootstrap.get_binding(grant.task_id).job_id
|
||||
raw = asdict(grant)
|
||||
legacy = Legacy()
|
||||
provider = IssuerV3ReceiptScopeProvider(
|
||||
legacy=legacy, producer_state_root=tmp_path / "runtime",
|
||||
plan_root=tmp_path / "plans", grant_reader=lambda **_kwargs: raw,
|
||||
plan_loader=lambda root, g: admission_adapter.validate_v3_plan(
|
||||
grant=g, raw=(root / (g["execution_digest"] + ".json")).read_bytes(),
|
||||
),
|
||||
)
|
||||
task = scope.TaskSnapshot(grant.task_id, "RUNNING")
|
||||
execution = scope.ExecutionSnapshot(
|
||||
grant.task_id, job_id, grant.repository, grant.target_branch,
|
||||
grant.generation, grant.execution_digest,
|
||||
)
|
||||
grant_snapshot = scope.GrantSnapshot(
|
||||
grant.grant_id, "ADMITTED", grant.task_id, grant.repository,
|
||||
grant.authority_base_oid, grant.target_branch, grant.generation,
|
||||
grant.execution_digest, tuple(grant.capabilities),
|
||||
tuple(grant.file_allowlist), grant.expires_at,
|
||||
)
|
||||
trust = scope.TrustSnapshot(
|
||||
issuer_id="issuer", key_id="key", public_key="public",
|
||||
purpose="TASK_PRINCIPAL", bridge_id="bridge",
|
||||
)
|
||||
return locals()
|
||||
|
||||
|
||||
def resolve(f, **changes):
|
||||
values = {key: f[key] for key in ("task", "execution", "grant_snapshot", "plan", "trust")}
|
||||
values.update(changes)
|
||||
return f["provider"].resolve(
|
||||
task=values["task"], execution=values["execution"],
|
||||
grant=values["grant_snapshot"], plan=values["plan"],
|
||||
trust=values["trust"], now=f["grant"].issued_at + 1,
|
||||
)
|
||||
|
||||
|
||||
def test_v3_receipt_scope_preserves_original_digest_and_job(tmp_path, monkeypatch):
|
||||
f = fixture(tmp_path, monkeypatch)
|
||||
result = resolve(f)
|
||||
assert result.external_job_id == f["job_id"]
|
||||
assert result.execution_digest == f["grant"].execution_digest
|
||||
assert result.execution_digest == hashlib.sha256(
|
||||
admission_adapter.canonical_json_bytes(f["plan"])
|
||||
).hexdigest()
|
||||
assert f["legacy"].calls == 0
|
||||
job, proof = f["provider"].inspect_v3_binding(
|
||||
grant_raw=f["raw"], plan=f["plan"],
|
||||
)
|
||||
assert job == proof["job_id"] == f["job_id"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("schema", [scope.PLAN_V1, scope.PLAN_V2])
|
||||
def test_v1_v2_remain_exact_legacy_dispatch(tmp_path, monkeypatch, schema):
|
||||
f = fixture(tmp_path, monkeypatch)
|
||||
assert resolve(f, plan={"schema": schema}) == "LEGACY"
|
||||
assert f["legacy"].calls == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("schema", [scope.PLAN_V1, scope.PLAN_V2])
|
||||
def test_v1_v2_actual_scope_equals_hash_pinned_legacy_provider(tmp_path, monkeypatch, schema):
|
||||
f = fixture(tmp_path, monkeypatch)
|
||||
plan = dict(f["plan"])
|
||||
plan["schema"] = schema
|
||||
plan["external_job_id"] = f["job_id"]
|
||||
if schema == scope.PLAN_V1:
|
||||
plan["files"] = list(plan["file_allowlist"])
|
||||
plan["purpose"] = "governed source publication"
|
||||
digest = scope.sha256_json(plan)
|
||||
execution = replace(f["execution"], execution_digest=digest)
|
||||
grant_snapshot = replace(f["grant_snapshot"], execution_digest=digest)
|
||||
legacy = scope.GovernedRuntimeScopeProvider()
|
||||
provider = IssuerV3ReceiptScopeProvider(
|
||||
legacy=legacy, producer_state_root=tmp_path / "runtime",
|
||||
plan_root=tmp_path / "plans", grant_reader=lambda **_kwargs: None,
|
||||
)
|
||||
args = dict(
|
||||
task=f["task"], execution=execution, grant=grant_snapshot,
|
||||
plan=plan, trust=f["trust"], now=f["grant"].issued_at + 1,
|
||||
)
|
||||
assert provider.resolve(**args) == legacy.resolve(**args)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field,value", [
|
||||
("task_id", "task-wrong"),
|
||||
("repository", "other/repo"),
|
||||
("target_branch", "agent/other-branch"),
|
||||
("generation", 2),
|
||||
("authority_base_oid", "a" * 40),
|
||||
("file_allowlist", ["active-runtime/app/other.py"]),
|
||||
("file_sha256", {"active-runtime/app/task_plane/api.py": "a" * 64}),
|
||||
("capabilities", ["other.capability"]),
|
||||
("risk", "high"),
|
||||
])
|
||||
def test_v3_wrong_canonical_plan_is_rejected(tmp_path, monkeypatch, field, value):
|
||||
f = fixture(tmp_path, monkeypatch)
|
||||
bad = dict(f["plan"])
|
||||
bad[field] = value
|
||||
with pytest.raises((scope.GovernedScopeError, admission_adapter.ProducerReceiptError)):
|
||||
resolve(f, plan=bad)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field,value", [
|
||||
("task_id", "task-wrong"),
|
||||
("repository", "other/repo"),
|
||||
("target_branch", "agent/wrong"),
|
||||
("generation", 99),
|
||||
("execution_digest", "a" * 64),
|
||||
])
|
||||
def test_execution_identity_mismatch_is_rejected(tmp_path, monkeypatch, field, value):
|
||||
f = fixture(tmp_path, monkeypatch)
|
||||
with pytest.raises(scope.GovernedScopeError):
|
||||
resolve(f, execution=replace(f["execution"], **{field: value}))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field,value", [
|
||||
("authority_base_oid", "a" * 40),
|
||||
("requested_capabilities", ("other.capability",)),
|
||||
("file_allowlist", ("active-runtime/app/other.py",)),
|
||||
])
|
||||
def test_grant_snapshot_scope_mismatch_is_rejected(tmp_path, monkeypatch, field, value):
|
||||
f = fixture(tmp_path, monkeypatch)
|
||||
with pytest.raises(scope.GovernedScopeError):
|
||||
resolve(f, grant_snapshot=replace(f["grant_snapshot"], **{field: value}))
|
||||
|
||||
|
||||
def test_missing_producer_receipt_and_forged_job_fail_closed(tmp_path, monkeypatch):
|
||||
f = fixture(tmp_path, monkeypatch)
|
||||
with pytest.raises(scope.GovernedScopeError, match="EXECUTION_PRODUCER_JOB_MISMATCH"):
|
||||
resolve(f, execution=replace(f["execution"], external_job_id="job-forged"))
|
||||
# An absent binding is corruption, not a prompt to call producer.create().
|
||||
f["runtime"].bootstrap._metadata(f["grant"].task_id).unlink()
|
||||
with pytest.raises(scope.GovernedScopeError, match="PRODUCER_RECEIPT_MISSING"):
|
||||
resolve(f)
|
||||
|
||||
|
||||
def test_producer_binding_conflict_is_rejected(tmp_path, monkeypatch):
|
||||
f = fixture(tmp_path, monkeypatch)
|
||||
metadata = f["runtime"].bootstrap._metadata(f["grant"].task_id)
|
||||
value = json.loads(metadata.read_text())
|
||||
value["target_branch"] = "agent/forged"
|
||||
metadata.write_text(json.dumps(value) + "\n")
|
||||
with pytest.raises(scope.GovernedScopeError, match="DURABLE_BINDING_CONFLICT"):
|
||||
resolve(f)
|
||||
|
||||
|
||||
def test_exact_replay_reads_without_new_producer_effect(tmp_path, monkeypatch):
|
||||
f = fixture(tmp_path, monkeypatch)
|
||||
db_path, = (tmp_path / "runtime" / "jobs").rglob("state.sqlite3")
|
||||
def rows():
|
||||
with sqlite3.connect(db_path.as_uri() + "?mode=ro", uri=True) as db:
|
||||
return db.execute("SELECT job_id, state, side_effect_state FROM jobs").fetchall()
|
||||
before = rows()
|
||||
creates_before = f["events"].count("bootstrap.create")
|
||||
assert resolve(f) == resolve(f)
|
||||
assert rows() == before
|
||||
assert f["events"].count("bootstrap.create") == creates_before
|
||||
|
||||
|
||||
def test_expired_task_and_grant_fail_closed_after_readonly_binding(tmp_path, monkeypatch):
|
||||
f = fixture(tmp_path, monkeypatch)
|
||||
assert f["provider"].inspect_v3_binding(
|
||||
grant_raw=f["raw"], plan=f["plan"],
|
||||
)[0] == f["job_id"]
|
||||
with pytest.raises(scope.GovernedScopeError, match="TASK_NOT_CLAIMED"):
|
||||
resolve(f, task=replace(f["task"], status="EXPIRED"))
|
||||
f["grant_snapshot"] = replace(f["grant_snapshot"], expires_at=f["grant"].issued_at)
|
||||
with pytest.raises(scope.GovernedScopeError, match="GRANT_EXPIRED_OR_NOT_ADMITTED"):
|
||||
resolve(f)
|
||||
|
||||
|
||||
def test_readonly_view_denies_all_mutating_producer_entries(tmp_path, monkeypatch):
|
||||
f = fixture(tmp_path, monkeypatch)
|
||||
view = _read_only_bootstrap(tmp_path / "runtime")
|
||||
for operation in (view.create, view._store, view._write):
|
||||
with pytest.raises(scope.GovernedScopeError, match="MUTATION_FORBIDDEN"):
|
||||
operation({})
|
||||
assert resolve(f).external_job_id == f["job_id"]
|
||||
@@ -108,6 +108,8 @@ class SignerContractCompatibilityTests(unittest.TestCase):
|
||||
|
||||
def test_real_signer_help_and_self_test_are_available(self) -> None:
|
||||
signer = "/usr/local/libexec/aios-grant-signer-adapter"
|
||||
if os.environ.get("AIOS_TEST_LIVE_SIGNER") != "1":
|
||||
self.skipTest("live signer compatibility requires AIOS_TEST_LIVE_SIGNER=1")
|
||||
if not Path(signer).is_file():
|
||||
self.skipTest("deployed signer is not present")
|
||||
if os.geteuid() != 0:
|
||||
|
||||
@@ -722,6 +722,118 @@ def test_unknown_release_source_requires_explicit_admission() -> None:
|
||||
config.resolve_recipe("V2R2C4", None, "2" * 40)
|
||||
|
||||
|
||||
def test_c5_successor_recipe_binds_authenticated_admission_and_contract() -> None:
|
||||
policy_path = ROOT / "tests" / "fixtures" / "trusted-candidate-builder-policy-c5.json"
|
||||
config = TrustedCandidateBuilderConfig.from_policy(load_builder_policy(policy_path))
|
||||
source = "2" * 40
|
||||
admitted = {
|
||||
"mode": "governed-source-admission", "task_id": config.c5_contract["task_id"],
|
||||
"candidate_id": "rc-v2r2c5-222222222222", "base_oid": config.c5_contract["authority_base_oid"],
|
||||
"source_commit": source, "version": "V2R2C5", "purpose": "AIOS_TRUSTED_RELEASE_BUILDER",
|
||||
"tree_oid": "3" * 40,
|
||||
}
|
||||
config = dataclasses.replace(config, source_admission_resolver=lambda *_: dict(admitted))
|
||||
recipe = config.resolve_recipe("V2R2C5", admitted["candidate_id"], source)
|
||||
assert recipe["tree_oid"] == "3" * 40
|
||||
assert recipe["accepted_base_oid"] == "e5f8209f57b43fc39e55c4555776e6362ce326eb"
|
||||
assert recipe["rollback_artifact_sha256"] == "0dfc0461f7f9aa25cfbfc37008dcbd2d48d5341383574163b7bba102852062c4"
|
||||
|
||||
|
||||
def integration_policy():
|
||||
policy = load_builder_policy(ROOT / "tests/fixtures/trusted-candidate-builder-policy-c5.json")
|
||||
policy["schema"] = "aios.trusted-candidate-builder-policy.c5-integration.v1"
|
||||
policy["recipes"]["V2R2C5"]["mode"] = "governed-source-integration-only"
|
||||
policy["c5_contract"].update({
|
||||
"integration_source_commit": "5f65e458957dd51c9973cfa956a624f3cbf54990",
|
||||
"integration_tree_oid": "ddd239d43d63a7fbd3cff46119c71466c9443737",
|
||||
"publication_source_commit": "d9597cba91abff90288186bb20b882c4d706a55d",
|
||||
"publication_tree_oid": "0597372a2b60991433b9920e50dcc45eac7f2c2e",
|
||||
"publication_plan_sha256": "0779fa615e8e7971a8dcfccf0f295b1323cd9b64787e174aea4fd2affc414339",
|
||||
"repair_source_commit": "c74db7a426eb74732ed476687314bd52cf140f3b",
|
||||
"repair_tree_oid": "9042109dc12158defa59031e9aea0b08777bd492",
|
||||
})
|
||||
return policy
|
||||
|
||||
|
||||
def integration_admission():
|
||||
contract = integration_policy()["c5_contract"]
|
||||
return {
|
||||
"mode": "governed-source-admission", "task_id": contract["task_id"],
|
||||
"candidate_id": "rc-v2r2c5-5f65e458957d", "base_oid": contract["authority_base_oid"],
|
||||
"source_commit": contract["integration_source_commit"], "version": "V2R2C5",
|
||||
"purpose": "AIOS_TRUSTED_RELEASE_BUILDER", "tree_oid": contract["integration_tree_oid"],
|
||||
}
|
||||
|
||||
|
||||
def test_c5_integration_recipe_retains_frozen_publication_and_independent_repair():
|
||||
config = TrustedCandidateBuilderConfig.from_policy(integration_policy())
|
||||
admission = integration_admission()
|
||||
config = dataclasses.replace(config, source_admission_resolver=lambda *_: dict(admission))
|
||||
recipe = config.resolve_recipe("V2R2C5", admission["candidate_id"], admission["source_commit"])
|
||||
assert recipe["publication_source_commit"] == "d9597cba91abff90288186bb20b882c4d706a55d"
|
||||
assert recipe["repair_source_commit"] == "c74db7a426eb74732ed476687314bd52cf140f3b"
|
||||
assert recipe["source_commit"] != recipe["publication_source_commit"]
|
||||
assert recipe["rollback_version"] == "V2R2C4"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field", sorted(builder_runtime.C5_INTEGRATION_CONTRACT_FIELDS))
|
||||
def test_c5_integration_policy_rejects_component_identity_drift(field):
|
||||
policy = integration_policy()
|
||||
policy["c5_contract"][field] = "wrong"
|
||||
with pytest.raises(BuilderError, match="POLICY_V2R2C5_CONTRACT_INVALID"):
|
||||
TrustedCandidateBuilderConfig.from_policy(policy)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field", ["task_id", "base_oid", "source_commit", "tree_oid", "candidate_id"])
|
||||
def test_c5_integration_rejects_wrong_admission(field):
|
||||
config = TrustedCandidateBuilderConfig.from_policy(integration_policy())
|
||||
admission = integration_admission()
|
||||
source, candidate = admission["source_commit"], admission["candidate_id"]
|
||||
admission[field] = "f" * 40 if field.endswith("oid") or field == "source_commit" else "wrong"
|
||||
config = dataclasses.replace(config, source_admission_resolver=lambda *_: dict(admission))
|
||||
with pytest.raises(BuilderError):
|
||||
config.resolve_recipe("V2R2C5", candidate, source)
|
||||
|
||||
|
||||
def test_c5_integration_collector_rejects_undeclared_source_changes(tmp_path):
|
||||
config = TrustedCandidateBuilderConfig.from_policy(integration_policy())
|
||||
admission = integration_admission()
|
||||
config = dataclasses.replace(config, source_admission_resolver=lambda *_: dict(admission))
|
||||
recipe = config.resolve_recipe("V2R2C5", admission["candidate_id"], admission["source_commit"])
|
||||
trees = {recipe[c]: recipe[t] for c, t in [
|
||||
("integration_source_commit", "integration_tree_oid"),
|
||||
("publication_source_commit", "publication_tree_oid"),
|
||||
("repair_source_commit", "repair_tree_oid"),
|
||||
]}
|
||||
def run(argv, **kwargs):
|
||||
operation = argv[2]
|
||||
if operation == "rev-parse":
|
||||
return SimpleNamespace(stdout=trees[argv[3].split("^")[0]] + "\n")
|
||||
if operation == "show":
|
||||
return SimpleNamespace(stdout=recipe["authority_base_oid"] + "\n")
|
||||
assert operation == "diff"
|
||||
return SimpleNamespace(stdout="\n".join([*builder_runtime.C5_INTEGRATION_FILES, "unexpected.py"]))
|
||||
source = ServerOwnedEvidenceSource(config, authority_git_dir=tmp_path, evidence_root=tmp_path, git_runner=run)
|
||||
with pytest.raises(BuilderError, match="COLLECTOR_INTEGRATION_FILE_SET_MISMATCH"):
|
||||
source._validate_integration_source(recipe)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field", ["task_id", "base_oid"])
|
||||
def test_c5_successor_recipe_rejects_wrong_authenticated_binding(field: str) -> None:
|
||||
policy_path = ROOT / "tests" / "fixtures" / "trusted-candidate-builder-policy-c5.json"
|
||||
config = TrustedCandidateBuilderConfig.from_policy(load_builder_policy(policy_path))
|
||||
source = "2" * 40
|
||||
admitted = {
|
||||
"mode": "governed-source-admission", "task_id": config.c5_contract["task_id"],
|
||||
"candidate_id": "rc-v2r2c5-222222222222", "base_oid": config.c5_contract["authority_base_oid"],
|
||||
"source_commit": source, "version": "V2R2C5", "purpose": "AIOS_TRUSTED_RELEASE_BUILDER", "tree_oid": "3" * 40,
|
||||
}
|
||||
admitted[field] = "wrong" if field == "task_id" else "f" * 40
|
||||
config = dataclasses.replace(config, source_admission_resolver=lambda *_: dict(admitted))
|
||||
with pytest.raises(BuilderError):
|
||||
config.resolve_recipe("V2R2C5", admitted["candidate_id"], source)
|
||||
|
||||
|
||||
def test_governed_source_admission_rejects_caller_candidate_rebinding() -> None:
|
||||
policy = load_builder_policy(POLICY_PATH)
|
||||
|
||||
|
||||
@@ -2,9 +2,16 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import subprocess
|
||||
import tarfile
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
|
||||
from trusted_evidence_providers import _run, _tests_env
|
||||
import trusted_evidence_providers as providers
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
@@ -31,3 +38,59 @@ def test_run_extra_env_reaches_pytest_child_process():
|
||||
result = _run(probe, extra_env={"PROBE_EXTRA": "v2r2c2-gatew-provider-harness"})
|
||||
assert result.returncode == 0
|
||||
assert result.stdout.strip() == "v2r2c2-gatew-provider-harness"
|
||||
|
||||
|
||||
def artifact_target(tmp_path):
|
||||
target = tmp_path / "extracted"
|
||||
path = target / "payload" / "module.py"
|
||||
path.parent.mkdir(parents=True)
|
||||
path.write_bytes(b"print('source')\n")
|
||||
artifact = tmp_path / "artifact.tar"
|
||||
with tarfile.open(artifact, "w") as archive:
|
||||
archive.add(path, arcname="payload/module.py")
|
||||
return artifact, target, path
|
||||
|
||||
|
||||
def test_archive_scan_binding_rejects_changed_and_extra_payload(tmp_path):
|
||||
artifact, target, path = artifact_target(tmp_path)
|
||||
assert providers._validate_artifact_scan_target(artifact, target) == hashlib.sha256(artifact.read_bytes()).hexdigest()
|
||||
path.write_bytes(b"different\n")
|
||||
with pytest.raises(providers.TrustedProviderExecutionError, match="PAYLOAD_MISMATCH"):
|
||||
providers._validate_artifact_scan_target(artifact, target)
|
||||
path.write_bytes(b"print('source')\n")
|
||||
(target / "extra").write_bytes(b"extra")
|
||||
with pytest.raises(providers.TrustedProviderExecutionError, match="PAYLOAD_MISMATCH"):
|
||||
providers._validate_artifact_scan_target(artifact, target)
|
||||
|
||||
|
||||
def test_archive_scan_binding_rejects_traversal(tmp_path):
|
||||
artifact, target, path = artifact_target(tmp_path)
|
||||
with tarfile.open(artifact, "w") as archive:
|
||||
item = tarfile.TarInfo("../outside")
|
||||
item.size = 1
|
||||
archive.addfile(item, io.BytesIO(b"x"))
|
||||
with pytest.raises(providers.TrustedProviderExecutionError, match="ARCHIVE_INVALID"):
|
||||
providers._validate_artifact_scan_target(artifact, target)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("findings,exit_code,expected", [([], 0, True), ([{"RuleID": "leak"}], 1, False), ([{"RuleID": "leak"}], 0, False), ([], 2, False)])
|
||||
def test_scanner_uses_structured_findings_and_real_exit(monkeypatch, tmp_path, findings, exit_code, expected):
|
||||
artifact, target, path = artifact_target(tmp_path)
|
||||
scanner = tmp_path / "scanner"
|
||||
scanner.write_bytes(b"fixture")
|
||||
def run(argv, **kwargs):
|
||||
Path(argv[argv.index("--report-path") + 1]).write_text(json.dumps(findings))
|
||||
return subprocess.CompletedProcess(argv, exit_code, "", "")
|
||||
monkeypatch.setattr(providers, "_run", run)
|
||||
result, _ = providers.run_secret_scan_provider(gitleaks_bin=str(scanner), target=target, source_oid="1" * 40, tree_oid="2" * 40, artifact_path=artifact)
|
||||
assert result["passed"] is expected
|
||||
assert result["findings"] == len(findings)
|
||||
assert result["artifact_sha256"] == hashlib.sha256(artifact.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def test_scanner_missing_report_cannot_pass(monkeypatch, tmp_path):
|
||||
scanner = tmp_path / "scanner"
|
||||
scanner.write_bytes(b"fixture")
|
||||
monkeypatch.setattr(providers, "_run", lambda argv, **kwargs: subprocess.CompletedProcess(argv, 0, "", ""))
|
||||
with pytest.raises(providers.TrustedProviderExecutionError, match="REPORT_INVALID"):
|
||||
providers.run_secret_scan_provider(gitleaks_bin=str(scanner), target=tmp_path, source_oid="1" * 40, tree_oid="2" * 40)
|
||||
|
||||
Reference in New Issue
Block a user