Files
aios/tests/test_owner_merge_authorization_v2.py
T
AIOS Governed Continuation 0136a53426
autonomy-smoke-tests phase2-pr76 PASS evidence_sha256=c16c68f86ee0c4e8cfd0af3f76078be45441ee151395fbde5800145c08e189b5
autonomy-unit-tests phase2-pr76 PASS evidence_sha256=15bc76da47bdf8218f18fc8a7da65fafd841677d5f754b71a0c9f56e07d84c1b
campaign-approval-gate phase2-pr76 PASS evidence_sha256=ea7ed0516efe6736a8eaee426685e40a2393fbf3ae84104a743f394204c2ec33
git-diff-check phase2-pr76 PASS evidence_sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
py-compile phase2-pr76 PASS evidence_sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
reconciliation-contract phase2-pr76 PASS evidence_sha256=211444cb0c146f5d4a57e13b780cfe7ffb406e8a8bc2e11c02a3a4728f66b0b2
release-state-machine phase2-pr76 PASS evidence_sha256=6cea88752b3e58ae5cee56ff3b1b13eab34117918d6bad9661194c031e2dc157
secret-scan phase2-pr76 PASS evidence_sha256=7cb68dd604dc6cd4904246e16333072d512dba14c096301e08c5a2b78a45bd55
signer-validator-contract phase2-pr76 PASS evidence_sha256=775b7aa27d0c17f48b3feff20c836f14b6ed8ebcb90ec7e0b76d2a13c579268a
aios.owner-final-merge-authorization-v2 real v2 contract tests passed
feat(governed): make Owner merge authority final
2026-09-07 23:14:29 +08:00

328 lines
10 KiB
Python

from __future__ import annotations
from dataclasses import replace
import json
import os
from pathlib import Path
import pytest
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from aios_owner_merge_authorization import (
GovernedOwnerMergeAdmission,
OwnerMergeApprovalAuthority,
OwnerMergeApprovalError,
OwnerMergeApprovalPolicy,
OwnerMergeApprovalVerifier,
OwnerMergeLiveState,
OwnerMergeOidReadback,
SQLiteOwnerMergeNonceLedger,
)
from aios_single_host_authority import (
RegistryOwnerApprovalVerifier,
ssh_fingerprint,
ssh_public_line,
)
NOW = 2_000_000_000
REPOSITORY = "aios/aios"
PR_NUMBER = 75
HEAD = "a" * 40
BASE = "b" * 40
MERGED = "c" * 40
CHECKS = ("contract", "security", "unit")
CHECK_CONTRACT = "d" * 64
LINEAGE = "e" * 64
def _write_key(path: Path, key: Ed25519PrivateKey) -> None:
path.write_bytes(
key.private_bytes(
serialization.Encoding.PEM,
serialization.PrivateFormat.PKCS8,
serialization.NoEncryption(),
)
)
os.chmod(path, 0o600)
class RecordingExecutor:
def __init__(self, result: str = MERGED) -> None:
self.result = result
self.calls: list[dict[str, object]] = []
def execute(self, approval: dict[str, object]) -> str:
self.calls.append(dict(approval))
return self.result
class RecordingReconciler:
def __init__(self, oid: str = MERGED) -> None:
self.oid = oid
self.calls: list[str] = []
def readback(self, merge_oid: str) -> OwnerMergeOidReadback:
self.calls.append(merge_oid)
return OwnerMergeOidReadback(
authority_main_oid=self.oid,
gitea_main_oid=self.oid,
api_main_oid=self.oid,
)
class UnavailableProofVerifier:
def verify(self, **_kwargs: object) -> bool:
raise RuntimeError("default signer/verifier unavailable")
@pytest.fixture
def approval_env(tmp_path: Path):
owner_key = Ed25519PrivateKey.generate()
owner_path = tmp_path / "owner.key"
_write_key(owner_path, owner_key)
registry_path = tmp_path / "registry.json"
registry_path.write_text(
json.dumps(
{
"schema": "aios.single-host-signer-registry.v1",
"authority_generation": 1,
"signers": [
{
"signer_id": "local-owner-authority",
"role": "OWNER_AUTHORITY",
"provider": "local_unix_service",
"fingerprint": ssh_fingerprint(owner_key.public_key()),
"public_key": ssh_public_line(owner_key.public_key(), "owner"),
"socket": "/run/aios/owner-authority.sock",
"secret_ref": "aios-secret://owner",
"status": "ACTIVE",
"generation": 1,
"parent_signer_id": "",
"parent_fingerprint": "",
}
],
},
sort_keys=True,
)
+ "\n"
)
os.chmod(registry_path, 0o644)
policy = OwnerMergeApprovalPolicy(
repository=REPOSITORY,
pr_number=PR_NUMBER,
pr_head_oid=HEAD,
expected_main_oid=BASE,
operation="merge.pull-request",
required_check_contract_digest=CHECK_CONTRACT,
required_checks=CHECKS,
owner_identity="owner:aios-single-host",
audit_lineage_digest=LINEAGE,
maximum_ttl_seconds=300,
)
authority = OwnerMergeApprovalAuthority(
owner_path,
policy,
require_root_owner=False,
clock=lambda: NOW,
token_urlsafe=lambda _size: "fixed-nonce-material",
)
artifact = authority.issue()
registry_verifier = RegistryOwnerApprovalVerifier(
registry_path,
require_root_owner=False,
)
return {
"policy": policy,
"artifact": artifact,
"registry_verifier": registry_verifier,
"registry_path": registry_path,
"owner_path": owner_path,
"nonce_path": tmp_path / "nonces.sqlite3",
}
def _live_state(**updates: object) -> OwnerMergeLiveState:
raw = {
"repository": REPOSITORY,
"pr_number": PR_NUMBER,
"pr_head_oid": HEAD,
"main_oid": BASE,
"required_check_contract_digest": CHECK_CONTRACT,
"check_statuses": {name: True for name in CHECKS},
}
raw.update(updates)
return OwnerMergeLiveState(**raw)
def _admission(env, *, live: OwnerMergeLiveState | None = None, primary=None, recovery=None, reconciler=None):
verifier = OwnerMergeApprovalVerifier(
primary=primary or env["registry_verifier"],
recovery=recovery,
clock=lambda: NOW,
)
executor = RecordingExecutor()
reconciler = reconciler or RecordingReconciler()
admission = GovernedOwnerMergeAdmission(
policy=env["policy"],
verifier=verifier,
live_state_reader=lambda: live or _live_state(),
executor=executor,
reconciler=reconciler,
nonce_ledger=SQLiteOwnerMergeNonceLedger(env["nonce_path"]),
clock=lambda: NOW,
)
return admission, executor, reconciler
def test_valid_owner_approval_with_exact_live_binding_enters_governed_merge(approval_env):
admission, executor, reconciler = _admission(approval_env)
receipt = admission.admit(approval_env["artifact"])
assert receipt.merge_oid == MERGED
assert receipt.owner_identity == "owner:aios-single-host"
assert len(executor.calls) == 1
assert reconciler.calls == [MERGED]
@pytest.mark.parametrize(
("live", "code"),
[
(_live_state(pr_head_oid="f" * 40), "OWNER_MERGE_HEAD_DRIFT"),
(_live_state(main_oid="f" * 40), "OWNER_MERGE_BASE_DRIFT"),
],
)
def test_head_or_main_drift_invalidates_owner_approval_before_execution(approval_env, live, code):
admission, executor, _reconciler = _admission(approval_env, live=live)
with pytest.raises(OwnerMergeApprovalError, match=code):
admission.admit(approval_env["artifact"])
assert executor.calls == []
def test_expired_owner_approval_is_rejected_before_execution(approval_env):
verifier = OwnerMergeApprovalVerifier(
primary=approval_env["registry_verifier"],
clock=lambda: NOW + 301,
)
executor = RecordingExecutor()
admission = GovernedOwnerMergeAdmission(
policy=approval_env["policy"],
verifier=verifier,
live_state_reader=_live_state,
executor=executor,
reconciler=RecordingReconciler(),
nonce_ledger=SQLiteOwnerMergeNonceLedger(approval_env["nonce_path"]),
clock=lambda: NOW + 301,
)
with pytest.raises(OwnerMergeApprovalError, match="OWNER_MERGE_APPROVAL_EXPIRED"):
admission.admit(approval_env["artifact"])
assert executor.calls == []
def test_changed_policy_scope_requires_a_formal_owner_rebind(approval_env):
replacement = replace(approval_env["policy"], expected_main_oid="f" * 40)
verifier = OwnerMergeApprovalVerifier(
primary=approval_env["registry_verifier"],
clock=lambda: NOW,
)
with pytest.raises(OwnerMergeApprovalError, match="OWNER_MERGE_APPROVAL_BASE_BINDING_INVALID"):
verifier.verify(approval_env["artifact"], replacement)
def test_nonce_replay_cannot_create_a_second_merge_side_effect(approval_env):
admission, executor, _reconciler = _admission(approval_env)
admission.admit(approval_env["artifact"])
with pytest.raises(OwnerMergeApprovalError, match="OWNER_MERGE_NONCE_REPLAY"):
admission.admit(approval_env["artifact"])
assert len(executor.calls) == 1
def test_non_owner_proof_is_rejected_even_when_payload_claims_owner_identity(approval_env):
attacker_path = approval_env["owner_path"].parent / "attacker.key"
_write_key(attacker_path, Ed25519PrivateKey.generate())
attacker = OwnerMergeApprovalAuthority(
attacker_path,
approval_env["policy"],
require_root_owner=False,
clock=lambda: NOW,
token_urlsafe=lambda _size: "attacker-nonce-material",
)
admission, executor, _reconciler = _admission(approval_env)
with pytest.raises(OwnerMergeApprovalError, match="OWNER_MERGE_APPROVAL_SIGNATURE_INVALID"):
admission.admit(attacker.issue())
assert executor.calls == []
def test_recovery_verifier_can_prove_existing_owner_approval_when_primary_is_unavailable(approval_env):
recovery = approval_env["registry_verifier"]
admission, executor, _reconciler = _admission(
approval_env,
primary=UnavailableProofVerifier(),
recovery=recovery,
)
receipt = admission.admit(approval_env["artifact"])
assert receipt.proof_path == "recovery"
assert len(executor.calls) == 1
def test_missing_trusted_owner_proof_fails_closed(approval_env):
admission, executor, _reconciler = _admission(approval_env)
with pytest.raises(OwnerMergeApprovalError, match="OWNER_MERGE_APPROVAL_ARTIFACT_INVALID"):
admission.admit({})
assert executor.calls == []
def test_owner_approval_never_converts_failed_technical_checks_to_pass(approval_env):
statuses = {name: True for name in CHECKS}
statuses["security"] = False
admission, executor, _reconciler = _admission(
approval_env,
live=_live_state(check_statuses=statuses),
)
with pytest.raises(OwnerMergeApprovalError, match="OWNER_MERGE_REQUIRED_CHECKS_FAILED"):
admission.admit(approval_env["artifact"])
assert executor.calls == []
def test_post_merge_oid_disagreement_is_not_reported_as_success_and_is_not_retried(approval_env):
admission, executor, _reconciler = _admission(
approval_env,
reconciler=RecordingReconciler(oid="f" * 40),
)
with pytest.raises(OwnerMergeApprovalError, match="OWNER_MERGE_OID_RECONCILIATION_REQUIRED"):
admission.admit(approval_env["artifact"])
with pytest.raises(OwnerMergeApprovalError, match="OWNER_MERGE_NONCE_REPLAY"):
admission.admit(approval_env["artifact"])
assert len(executor.calls) == 1
def test_malformed_executor_result_is_unknown_and_cannot_be_retried(approval_env):
admission, executor, _reconciler = _admission(approval_env)
executor.result = "not-an-oid"
with pytest.raises(OwnerMergeApprovalError, match="OWNER_MERGE_EXECUTOR_RESULT_INVALID"):
admission.admit(approval_env["artifact"])
with pytest.raises(OwnerMergeApprovalError, match="OWNER_MERGE_NONCE_REPLAY"):
admission.admit(approval_env["artifact"])
assert len(executor.calls) == 1