Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2646286de |
@@ -0,0 +1,352 @@
|
||||
"""Cryptographic Owner-approval boundary for governed existing-branch publication.
|
||||
|
||||
Only a complete signed envelope from an authoritative trust-root adapter is
|
||||
accepted. References, chat assertions, and locally supplied keys are not an
|
||||
approval. Missing public-key or revocation evidence fails closed.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
from typing import Any, Mapping, Protocol
|
||||
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
||||
|
||||
|
||||
APPROVAL_SCHEMA = "aios.existing-branch-owner-approval.v1"
|
||||
PUBLISH_OPERATION = "publishExistingBranchGoverned"
|
||||
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
DIGEST_RE = re.compile(r"^[0-9a-f]{64}$")
|
||||
IDENTIFIER_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._@:+/-]{0,254}$")
|
||||
BRANCH_RE = re.compile(r"^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$")
|
||||
|
||||
|
||||
class OwnerApprovalRejected(RuntimeError):
|
||||
"""Stable, audit-safe error for a rejected Owner approval."""
|
||||
|
||||
def __init__(self, code: str) -> None:
|
||||
super().__init__(code)
|
||||
self.code = code
|
||||
|
||||
|
||||
def canonical_json(value: Any) -> bytes:
|
||||
"""Encode a signed payload without implementation-dependent spacing."""
|
||||
try:
|
||||
encoded = json.dumps(
|
||||
value,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
ensure_ascii=False,
|
||||
allow_nan=False,
|
||||
)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_CANONICALIZATION_REJECTED") from exc
|
||||
return (encoded + "\n").encode("utf-8")
|
||||
|
||||
|
||||
def digest_json(value: Any) -> str:
|
||||
return hashlib.sha256(canonical_json(value)).hexdigest()
|
||||
|
||||
|
||||
def _require_identifier(field: str, value: object) -> str:
|
||||
if not isinstance(value, str) or not IDENTIFIER_RE.fullmatch(value):
|
||||
raise OwnerApprovalRejected(f"OWNER_APPROVAL_{field.upper()}_REJECTED")
|
||||
return value
|
||||
|
||||
|
||||
def _require_digest(field: str, value: object) -> str:
|
||||
if not isinstance(value, str) or not DIGEST_RE.fullmatch(value):
|
||||
raise OwnerApprovalRejected(f"OWNER_APPROVAL_{field.upper()}_REJECTED")
|
||||
return value
|
||||
|
||||
|
||||
def _require_oid(field: str, value: object) -> str:
|
||||
if not isinstance(value, str) or not OID_RE.fullmatch(value):
|
||||
raise OwnerApprovalRejected(f"OWNER_APPROVAL_{field.upper()}_REJECTED")
|
||||
return value
|
||||
|
||||
|
||||
def _parse_utc(field: str, value: object) -> datetime:
|
||||
if not isinstance(value, str):
|
||||
raise OwnerApprovalRejected(f"OWNER_APPROVAL_{field.upper()}_REJECTED")
|
||||
try:
|
||||
instant = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
except ValueError as exc:
|
||||
raise OwnerApprovalRejected(f"OWNER_APPROVAL_{field.upper()}_REJECTED") from exc
|
||||
if instant.tzinfo is None:
|
||||
raise OwnerApprovalRejected(f"OWNER_APPROVAL_{field.upper()}_REJECTED")
|
||||
return instant.astimezone(timezone.utc)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PublicationApprovalBinding:
|
||||
repository: str
|
||||
branch: str
|
||||
expected_branch_oid: str
|
||||
operation: str
|
||||
request_digest: str
|
||||
idempotency_digest: str
|
||||
|
||||
def validate(self) -> "PublicationApprovalBinding":
|
||||
_require_identifier("repository", self.repository)
|
||||
if not BRANCH_RE.fullmatch(self.branch):
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_BRANCH_REJECTED")
|
||||
_require_oid("expected_branch_oid", self.expected_branch_oid)
|
||||
if self.operation != PUBLISH_OPERATION:
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_OPERATION_REJECTED")
|
||||
_require_digest("request_digest", self.request_digest)
|
||||
_require_digest("idempotency_digest", self.idempotency_digest)
|
||||
return self
|
||||
|
||||
@property
|
||||
def as_dict(self) -> dict[str, str]:
|
||||
return {
|
||||
"repository": self.repository,
|
||||
"branch": self.branch,
|
||||
"expected_branch_oid": self.expected_branch_oid,
|
||||
"operation": self.operation,
|
||||
"request_digest": self.request_digest,
|
||||
"idempotency_digest": self.idempotency_digest,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class OwnerApprovalEnvelope:
|
||||
approval_id: str
|
||||
repository: str
|
||||
branch: str
|
||||
expected_branch_oid: str
|
||||
operation: str
|
||||
issued_at: str
|
||||
not_before: str
|
||||
expires_at: str
|
||||
nonce: str
|
||||
owner_identity: str
|
||||
trust_domain: str
|
||||
key_id: str
|
||||
request_digest: str
|
||||
idempotency_digest: str
|
||||
signature: str
|
||||
|
||||
@property
|
||||
def canonical_payload(self) -> dict[str, str]:
|
||||
return {
|
||||
"schema": APPROVAL_SCHEMA,
|
||||
"approval_id": self.approval_id,
|
||||
"repository": self.repository,
|
||||
"branch": self.branch,
|
||||
"expected_branch_oid": self.expected_branch_oid,
|
||||
"operation": self.operation,
|
||||
"issued_at": self.issued_at,
|
||||
"not_before": self.not_before,
|
||||
"expires_at": self.expires_at,
|
||||
"nonce": self.nonce,
|
||||
"owner_identity": self.owner_identity,
|
||||
"trust_domain": self.trust_domain,
|
||||
"key_id": self.key_id,
|
||||
"request_digest": self.request_digest,
|
||||
"idempotency_digest": self.idempotency_digest,
|
||||
}
|
||||
|
||||
def canonical_bytes(self) -> bytes:
|
||||
return canonical_json(self.canonical_payload)
|
||||
|
||||
@property
|
||||
def fingerprint(self) -> str:
|
||||
return hashlib.sha256(self.canonical_bytes()).hexdigest()
|
||||
|
||||
@property
|
||||
def binding(self) -> PublicationApprovalBinding:
|
||||
return PublicationApprovalBinding(
|
||||
repository=self.repository,
|
||||
branch=self.branch,
|
||||
expected_branch_oid=self.expected_branch_oid,
|
||||
operation=self.operation,
|
||||
request_digest=self.request_digest,
|
||||
idempotency_digest=self.idempotency_digest,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_mapping(cls, value: object) -> "OwnerApprovalEnvelope":
|
||||
if not isinstance(value, Mapping):
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_ENVELOPE_REQUIRED")
|
||||
expected_fields = {
|
||||
"schema", "approval_id", "repository", "branch", "expected_branch_oid",
|
||||
"operation", "issued_at", "not_before", "expires_at", "nonce",
|
||||
"owner_identity", "trust_domain", "key_id", "request_digest",
|
||||
"idempotency_digest", "signature",
|
||||
}
|
||||
if set(value) != expected_fields or value.get("schema") != APPROVAL_SCHEMA:
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_ENVELOPE_REJECTED")
|
||||
envelope = cls(
|
||||
approval_id=_require_identifier("approval_id", value["approval_id"]),
|
||||
repository=_require_identifier("repository", value["repository"]),
|
||||
branch=str(value["branch"]),
|
||||
expected_branch_oid=_require_oid("expected_branch_oid", value["expected_branch_oid"]),
|
||||
operation=str(value["operation"]),
|
||||
issued_at=str(value["issued_at"]),
|
||||
not_before=str(value["not_before"]),
|
||||
expires_at=str(value["expires_at"]),
|
||||
nonce=_require_identifier("nonce", value["nonce"]),
|
||||
owner_identity=_require_identifier("owner_identity", value["owner_identity"]),
|
||||
trust_domain=_require_identifier("trust_domain", value["trust_domain"]),
|
||||
key_id=_require_identifier("key_id", value["key_id"]),
|
||||
request_digest=_require_digest("request_digest", value["request_digest"]),
|
||||
idempotency_digest=_require_digest("idempotency_digest", value["idempotency_digest"]),
|
||||
signature=str(value["signature"]),
|
||||
)
|
||||
envelope.binding.validate()
|
||||
_parse_utc("issued_at", envelope.issued_at)
|
||||
_parse_utc("not_before", envelope.not_before)
|
||||
_parse_utc("expires_at", envelope.expires_at)
|
||||
if not envelope.signature:
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_SIGNATURE_REJECTED")
|
||||
return envelope
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TrustRootEvidence:
|
||||
"""Public-key and revocation evidence from the authoritative trust root."""
|
||||
|
||||
key_id: str
|
||||
owner_identity: str
|
||||
trust_domain: str
|
||||
public_key: bytes | None
|
||||
authority_confirmed: bool
|
||||
revocation_state: str
|
||||
|
||||
|
||||
class AuthoritativeTrustRootAdapter(Protocol):
|
||||
def resolve(
|
||||
self, *, key_id: str, owner_identity: str, trust_domain: str
|
||||
) -> TrustRootEvidence | None: ...
|
||||
|
||||
|
||||
class ReplayLedger(Protocol):
|
||||
def consume(
|
||||
self, approval: OwnerApprovalEnvelope, binding: PublicationApprovalBinding
|
||||
) -> None: ...
|
||||
|
||||
|
||||
class DurableNonceReplayLedger:
|
||||
"""Single-use nonce storage that remains conservative across restarts."""
|
||||
|
||||
def __init__(self, root: Path) -> None:
|
||||
self.root = Path(root)
|
||||
|
||||
def consume(
|
||||
self, approval: OwnerApprovalEnvelope, binding: PublicationApprovalBinding
|
||||
) -> None:
|
||||
binding.validate()
|
||||
if self.root.is_symlink():
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_REPLAY_LEDGER_REJECTED")
|
||||
try:
|
||||
self.root.mkdir(parents=True, exist_ok=True)
|
||||
if not self.root.is_dir() or self.root.is_symlink():
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_REPLAY_LEDGER_REJECTED")
|
||||
name = hashlib.sha256(approval.nonce.encode("utf-8")).hexdigest()
|
||||
record_path = self.root / name
|
||||
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
|
||||
if hasattr(os, "O_NOFOLLOW"):
|
||||
flags |= os.O_NOFOLLOW
|
||||
descriptor = os.open(record_path, flags, 0o600)
|
||||
except FileExistsError as exc:
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_REPLAY_REJECTED") from exc
|
||||
except OwnerApprovalRejected:
|
||||
raise
|
||||
except OSError as exc:
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_REPLAY_LEDGER_REJECTED") from exc
|
||||
record = {
|
||||
"schema": "aios.existing-branch-owner-approval-replay.v1",
|
||||
"nonce_sha256": name,
|
||||
"approval_fingerprint": approval.fingerprint,
|
||||
"binding_digest": digest_json(binding.as_dict),
|
||||
}
|
||||
try:
|
||||
with os.fdopen(descriptor, "wb", closefd=True) as stream:
|
||||
stream.write(canonical_json(record))
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
directory = os.open(self.root, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
|
||||
try:
|
||||
os.fsync(directory)
|
||||
finally:
|
||||
os.close(directory)
|
||||
except OSError as exc:
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_REPLAY_LEDGER_REJECTED") from exc
|
||||
|
||||
|
||||
class OwnerApprovalVerifier:
|
||||
def __init__(
|
||||
self,
|
||||
trust_root: AuthoritativeTrustRootAdapter,
|
||||
*,
|
||||
allowed_trust_domains: set[str] | frozenset[str],
|
||||
maximum_lifetime_seconds: int = 900,
|
||||
) -> None:
|
||||
if not allowed_trust_domains or maximum_lifetime_seconds < 1:
|
||||
raise ValueError("OWNER_APPROVAL_VERIFIER_CONFIGURATION_REJECTED")
|
||||
self.trust_root = trust_root
|
||||
self.allowed_trust_domains = frozenset(allowed_trust_domains)
|
||||
self.maximum_lifetime_seconds = maximum_lifetime_seconds
|
||||
|
||||
def verify(
|
||||
self,
|
||||
value: object,
|
||||
binding: PublicationApprovalBinding,
|
||||
*,
|
||||
now: datetime | None = None,
|
||||
) -> OwnerApprovalEnvelope:
|
||||
binding.validate()
|
||||
approval = OwnerApprovalEnvelope.from_mapping(value)
|
||||
if approval.binding != binding:
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_BINDING_REJECTED")
|
||||
instant = datetime.now(timezone.utc) if now is None else now
|
||||
if instant.tzinfo is None:
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_TIME_REJECTED")
|
||||
instant = instant.astimezone(timezone.utc)
|
||||
issued_at = _parse_utc("issued_at", approval.issued_at)
|
||||
not_before = _parse_utc("not_before", approval.not_before)
|
||||
expires_at = _parse_utc("expires_at", approval.expires_at)
|
||||
if not issued_at <= not_before <= expires_at:
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_TIME_REJECTED")
|
||||
if not not_before <= instant <= expires_at:
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_TIME_REJECTED")
|
||||
if (expires_at - issued_at).total_seconds() > self.maximum_lifetime_seconds:
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_TIME_REJECTED")
|
||||
if approval.trust_domain not in self.allowed_trust_domains:
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_TRUST_DOMAIN_REJECTED")
|
||||
evidence = self.trust_root.resolve(
|
||||
key_id=approval.key_id,
|
||||
owner_identity=approval.owner_identity,
|
||||
trust_domain=approval.trust_domain,
|
||||
)
|
||||
if evidence is None or not evidence.authority_confirmed:
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_TRUST_ROOT_EVIDENCE_MISSING")
|
||||
if (
|
||||
evidence.key_id != approval.key_id
|
||||
or evidence.owner_identity != approval.owner_identity
|
||||
or evidence.trust_domain != approval.trust_domain
|
||||
):
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_TRUST_ROOT_BINDING_REJECTED")
|
||||
if evidence.revocation_state == "REVOKED":
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_REVOKED")
|
||||
if evidence.revocation_state != "ACTIVE":
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_REVOCATION_EVIDENCE_MISSING")
|
||||
if evidence.public_key is None or len(evidence.public_key) != 32:
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_TRUST_ROOT_EVIDENCE_MISSING")
|
||||
try:
|
||||
signature = base64.b64decode(approval.signature, validate=True)
|
||||
Ed25519PublicKey.from_public_bytes(evidence.public_key).verify(
|
||||
signature, approval.canonical_bytes()
|
||||
)
|
||||
except (ValueError, TypeError, InvalidSignature) as exc:
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_SIGNATURE_REJECTED") from exc
|
||||
return approval
|
||||
@@ -0,0 +1,331 @@
|
||||
"""Narrow governed publisher for an already-existing task branch.
|
||||
|
||||
Its only writing port is a compare-and-swap publication after a complete
|
||||
signed Owner approval, a stable remote observation, and a single-use nonce
|
||||
claim. It neither owns trust material nor accepts direct reference strings.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import re
|
||||
from typing import Any, Mapping, Protocol
|
||||
|
||||
from aios_existing_branch_approval import (
|
||||
OwnerApprovalEnvelope,
|
||||
OwnerApprovalRejected,
|
||||
OwnerApprovalVerifier,
|
||||
PublicationApprovalBinding,
|
||||
ReplayLedger,
|
||||
canonical_json,
|
||||
digest_json,
|
||||
)
|
||||
|
||||
|
||||
REQUEST_SCHEMA = "aios.existing-branch-publication.v1"
|
||||
PUBLISH_OPERATION = "publishExistingBranchGoverned"
|
||||
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
BRANCH_RE = re.compile(r"^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$")
|
||||
|
||||
|
||||
class ExistingBranchPublicationRejected(RuntimeError):
|
||||
def __init__(self, code: str) -> None:
|
||||
super().__init__(code)
|
||||
self.code = code
|
||||
|
||||
|
||||
class RemoteOidConflict(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class RemoteObservationAmbiguous(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ExistingBranchPublicationRequest:
|
||||
repository: str
|
||||
branch: str
|
||||
expected_branch_oid: str
|
||||
idempotency_key: str
|
||||
payload: Mapping[str, Any]
|
||||
|
||||
@classmethod
|
||||
def from_mapping(cls, value: object) -> "ExistingBranchPublicationRequest":
|
||||
if not isinstance(value, Mapping):
|
||||
raise ExistingBranchPublicationRejected("PUBLICATION_REQUEST_REJECTED")
|
||||
expected_fields = {
|
||||
"schema",
|
||||
"repository",
|
||||
"branch",
|
||||
"expected_branch_oid",
|
||||
"operation",
|
||||
"idempotency_key",
|
||||
"payload",
|
||||
}
|
||||
if set(value) != expected_fields or value.get("schema") != REQUEST_SCHEMA:
|
||||
raise ExistingBranchPublicationRejected("PUBLICATION_REQUEST_REJECTED")
|
||||
repository = value["repository"]
|
||||
branch = value["branch"]
|
||||
expected_oid = value["expected_branch_oid"]
|
||||
key = value["idempotency_key"]
|
||||
payload = value["payload"]
|
||||
if (
|
||||
not isinstance(repository, str)
|
||||
or repository != "aios/aios"
|
||||
or not BRANCH_RE.fullmatch(str(branch))
|
||||
or not isinstance(expected_oid, str)
|
||||
or not OID_RE.fullmatch(expected_oid)
|
||||
or value.get("operation") != PUBLISH_OPERATION
|
||||
or not isinstance(key, str)
|
||||
or not key.strip()
|
||||
or not isinstance(payload, Mapping)
|
||||
):
|
||||
raise ExistingBranchPublicationRejected("PUBLICATION_REQUEST_REJECTED")
|
||||
try:
|
||||
canonical_json(payload)
|
||||
except OwnerApprovalRejected as exc:
|
||||
raise ExistingBranchPublicationRejected("PUBLICATION_PAYLOAD_REJECTED") from exc
|
||||
return cls(repository, str(branch), expected_oid, key, dict(payload))
|
||||
|
||||
@property
|
||||
def request_digest(self) -> str:
|
||||
return digest_json(
|
||||
{
|
||||
"schema": REQUEST_SCHEMA,
|
||||
"repository": self.repository,
|
||||
"branch": self.branch,
|
||||
"expected_branch_oid": self.expected_branch_oid,
|
||||
"operation": PUBLISH_OPERATION,
|
||||
"payload": self.payload,
|
||||
}
|
||||
)
|
||||
|
||||
@property
|
||||
def idempotency_digest(self) -> str:
|
||||
return digest_json(
|
||||
{
|
||||
"idempotency_key": self.idempotency_key,
|
||||
"request_digest": self.request_digest,
|
||||
}
|
||||
)
|
||||
|
||||
@property
|
||||
def approval_binding(self) -> PublicationApprovalBinding:
|
||||
return PublicationApprovalBinding(
|
||||
repository=self.repository,
|
||||
branch=self.branch,
|
||||
expected_branch_oid=self.expected_branch_oid,
|
||||
operation=PUBLISH_OPERATION,
|
||||
request_digest=self.request_digest,
|
||||
idempotency_digest=self.idempotency_digest,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RemoteBranchObservation:
|
||||
state: str
|
||||
git_oid: str | None
|
||||
api_oid: str | None
|
||||
expected_commit_visible: bool | None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RemotePublicationEvidence:
|
||||
authority_oid: str | None
|
||||
gitea_git_oid: str | None
|
||||
gitea_api_oid: str | None
|
||||
commit_visible: bool | None
|
||||
|
||||
|
||||
class ExistingBranchRemotePort(Protocol):
|
||||
def observe(self, branch: str) -> RemoteBranchObservation: ...
|
||||
|
||||
def publish_compare_and_swap(
|
||||
self, branch: str, expected_branch_oid: str, commit_oid: str
|
||||
) -> RemotePublicationEvidence: ...
|
||||
|
||||
|
||||
class AuthorityBranchPort(Protocol):
|
||||
def read_branch_oid(self, branch: str) -> str | None: ...
|
||||
|
||||
|
||||
class CredentialProjection(Protocol):
|
||||
def project(self) -> None: ...
|
||||
|
||||
def revoke(self) -> None: ...
|
||||
|
||||
|
||||
class PublicationWorkspace(Protocol):
|
||||
def head_oid(self) -> str: ...
|
||||
|
||||
def commit_payload(self, payload: Mapping[str, Any]) -> str: ...
|
||||
|
||||
|
||||
class DetachedWorkspaceFactory(Protocol):
|
||||
def open_detached(self, expected_branch_oid: str) -> PublicationWorkspace: ...
|
||||
|
||||
|
||||
class AuditSink(Protocol):
|
||||
def record(self, event: Mapping[str, Any]) -> None: ...
|
||||
|
||||
|
||||
class ExistingBranchPublisher:
|
||||
"""Validate once, observe once, then make one bounded publication attempt."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
verifier: OwnerApprovalVerifier,
|
||||
replay_ledger: ReplayLedger,
|
||||
authority: AuthorityBranchPort,
|
||||
remote: ExistingBranchRemotePort,
|
||||
credentials: CredentialProjection,
|
||||
workspaces: DetachedWorkspaceFactory,
|
||||
audit: AuditSink,
|
||||
) -> None:
|
||||
self.verifier = verifier
|
||||
self.replay_ledger = replay_ledger
|
||||
self.authority = authority
|
||||
self.remote = remote
|
||||
self.credentials = credentials
|
||||
self.workspaces = workspaces
|
||||
self.audit = audit
|
||||
|
||||
def _audit(
|
||||
self,
|
||||
*,
|
||||
status: str,
|
||||
request: ExistingBranchPublicationRequest,
|
||||
approval: OwnerApprovalEnvelope | None = None,
|
||||
commit_oid: str | None = None,
|
||||
) -> None:
|
||||
event: dict[str, Any] = {
|
||||
"event": "governed_existing_branch_publication",
|
||||
"status": status,
|
||||
"repository": request.repository,
|
||||
"branch": request.branch,
|
||||
"expected_branch_oid": request.expected_branch_oid,
|
||||
"operation": PUBLISH_OPERATION,
|
||||
"request_digest": request.request_digest,
|
||||
"idempotency_digest": request.idempotency_digest,
|
||||
}
|
||||
if approval is not None:
|
||||
event.update(
|
||||
{
|
||||
"approval_id": approval.approval_id,
|
||||
"owner_identity": approval.owner_identity,
|
||||
"trust_domain": approval.trust_domain,
|
||||
"approval_fingerprint": approval.fingerprint,
|
||||
}
|
||||
)
|
||||
if commit_oid is not None:
|
||||
event["commit_oid"] = commit_oid
|
||||
self.audit.record(event)
|
||||
|
||||
def _preflight_remote(
|
||||
self, request: ExistingBranchPublicationRequest
|
||||
) -> RemoteBranchObservation | dict[str, str]:
|
||||
try:
|
||||
authority_oid = self.authority.read_branch_oid(request.branch)
|
||||
except Exception:
|
||||
return {"status": "RECONCILIATION_REQUIRED"}
|
||||
if authority_oid != request.expected_branch_oid:
|
||||
return {"status": "MANUAL_OWNER_REVIEW_REQUIRED"}
|
||||
try:
|
||||
observation = self.remote.observe(request.branch)
|
||||
except RemoteObservationAmbiguous:
|
||||
return {"status": "RECONCILIATION_REQUIRED"}
|
||||
if observation.state == "AMBIGUOUS":
|
||||
return {"status": "RECONCILIATION_REQUIRED"}
|
||||
if observation.state != "PRESENT":
|
||||
return {"status": "MANUAL_OWNER_REVIEW_REQUIRED"}
|
||||
if (
|
||||
observation.git_oid is not None
|
||||
and observation.api_oid is not None
|
||||
and observation.git_oid != observation.api_oid
|
||||
):
|
||||
return {"status": "RECONCILIATION_REQUIRED"}
|
||||
if (
|
||||
observation.git_oid != request.expected_branch_oid
|
||||
or observation.api_oid != request.expected_branch_oid
|
||||
):
|
||||
return {"status": "MANUAL_OWNER_REVIEW_REQUIRED"}
|
||||
if observation.expected_commit_visible is not True:
|
||||
return {"status": "RECONCILIATION_REQUIRED"}
|
||||
return observation
|
||||
|
||||
def publish(
|
||||
self, request_value: object, approval_value: object
|
||||
) -> dict[str, str | bool]:
|
||||
request = ExistingBranchPublicationRequest.from_mapping(request_value)
|
||||
try:
|
||||
approval = self.verifier.verify(approval_value, request.approval_binding)
|
||||
except OwnerApprovalRejected:
|
||||
self._audit(status="REJECTED", request=request)
|
||||
raise
|
||||
preflight = self._preflight_remote(request)
|
||||
if isinstance(preflight, dict):
|
||||
self._audit(status=preflight["status"], request=request, approval=approval)
|
||||
return preflight
|
||||
self.replay_ledger.consume(approval, request.approval_binding)
|
||||
workspace = self.workspaces.open_detached(request.expected_branch_oid)
|
||||
if workspace.head_oid() != request.expected_branch_oid:
|
||||
self._audit(status="REJECTED", request=request, approval=approval)
|
||||
raise ExistingBranchPublicationRejected("DETACHED_WORKSPACE_HEAD_REJECTED")
|
||||
projected = False
|
||||
try:
|
||||
self.credentials.project()
|
||||
projected = True
|
||||
commit_oid = workspace.commit_payload(request.payload)
|
||||
if not isinstance(commit_oid, str) or not OID_RE.fullmatch(commit_oid):
|
||||
raise ExistingBranchPublicationRejected("PUBLICATION_COMMIT_REJECTED")
|
||||
try:
|
||||
evidence = self.remote.publish_compare_and_swap(
|
||||
request.branch, request.expected_branch_oid, commit_oid
|
||||
)
|
||||
except RemoteOidConflict:
|
||||
self._audit(
|
||||
status="MANUAL_OWNER_REVIEW_REQUIRED",
|
||||
request=request,
|
||||
approval=approval,
|
||||
commit_oid=commit_oid,
|
||||
)
|
||||
return {"status": "MANUAL_OWNER_REVIEW_REQUIRED"}
|
||||
except RemoteObservationAmbiguous:
|
||||
self._audit(
|
||||
status="RECONCILIATION_REQUIRED",
|
||||
request=request,
|
||||
approval=approval,
|
||||
commit_oid=commit_oid,
|
||||
)
|
||||
return {"status": "RECONCILIATION_REQUIRED"}
|
||||
if (
|
||||
evidence.authority_oid != commit_oid
|
||||
or evidence.gitea_git_oid != commit_oid
|
||||
or evidence.gitea_api_oid != commit_oid
|
||||
or evidence.commit_visible is not True
|
||||
):
|
||||
self._audit(
|
||||
status="RECONCILIATION_REQUIRED",
|
||||
request=request,
|
||||
approval=approval,
|
||||
commit_oid=commit_oid,
|
||||
)
|
||||
return {"status": "RECONCILIATION_REQUIRED"}
|
||||
self._audit(
|
||||
status="PUBLISHED",
|
||||
request=request,
|
||||
approval=approval,
|
||||
commit_oid=commit_oid,
|
||||
)
|
||||
return {
|
||||
"status": "PUBLISHED",
|
||||
"commit_oid": commit_oid,
|
||||
"authority_oid": evidence.authority_oid,
|
||||
"gitea_git_oid": evidence.gitea_git_oid,
|
||||
"gitea_api_oid": evidence.gitea_api_oid,
|
||||
"commit_visible": evidence.commit_visible,
|
||||
}
|
||||
finally:
|
||||
if projected:
|
||||
self.credentials.revoke()
|
||||
@@ -0,0 +1,285 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import json
|
||||
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||
import pytest
|
||||
|
||||
from aios_existing_branch_approval import (
|
||||
DurableNonceReplayLedger,
|
||||
OwnerApprovalEnvelope,
|
||||
OwnerApprovalRejected,
|
||||
OwnerApprovalVerifier,
|
||||
TrustRootEvidence,
|
||||
)
|
||||
from aios_existing_branch_publisher import (
|
||||
ExistingBranchPublicationRejected,
|
||||
ExistingBranchPublisher,
|
||||
RemoteBranchObservation,
|
||||
RemotePublicationEvidence,
|
||||
)
|
||||
|
||||
|
||||
BASE_OID = "a" * 40
|
||||
COMMIT_OID = "b" * 40
|
||||
BRANCH = "agent/owner-approval-contract-20260810"
|
||||
|
||||
|
||||
class TrustRoot:
|
||||
def __init__(self, public_key: bytes, *, revocation: str = "ACTIVE") -> None:
|
||||
self.public_key = public_key
|
||||
self.revocation = revocation
|
||||
self.calls: list[str] = []
|
||||
|
||||
def resolve(self, *, key_id: str, owner_identity: str, trust_domain: str):
|
||||
self.calls.append("trust-root")
|
||||
return TrustRootEvidence(
|
||||
key_id=key_id,
|
||||
owner_identity=owner_identity,
|
||||
trust_domain=trust_domain,
|
||||
public_key=self.public_key,
|
||||
authority_confirmed=True,
|
||||
revocation_state=self.revocation,
|
||||
)
|
||||
|
||||
|
||||
class Credentials:
|
||||
def __init__(self, events: list[str]) -> None:
|
||||
self.events = events
|
||||
|
||||
def project(self) -> None:
|
||||
self.events.append("credential-project")
|
||||
|
||||
def revoke(self) -> None:
|
||||
self.events.append("credential-revoke")
|
||||
|
||||
|
||||
class Workspace:
|
||||
def __init__(self, events: list[str]) -> None:
|
||||
self.events = events
|
||||
|
||||
def head_oid(self) -> str:
|
||||
self.events.append("workspace-head")
|
||||
return BASE_OID
|
||||
|
||||
def commit_payload(self, payload):
|
||||
self.events.append("workspace-commit")
|
||||
return COMMIT_OID
|
||||
|
||||
|
||||
class Workspaces:
|
||||
def __init__(self, events: list[str]) -> None:
|
||||
self.events = events
|
||||
|
||||
def open_detached(self, expected_branch_oid: str) -> Workspace:
|
||||
assert expected_branch_oid == BASE_OID
|
||||
self.events.append("workspace-open")
|
||||
return Workspace(self.events)
|
||||
|
||||
|
||||
class Remote:
|
||||
def __init__(self, events: list[str]) -> None:
|
||||
self.events = events
|
||||
self.observation = RemoteBranchObservation("PRESENT", BASE_OID, BASE_OID, True)
|
||||
self.authority_oid = BASE_OID
|
||||
self.pushes: list[tuple[str, str, str]] = []
|
||||
|
||||
def read_branch_oid(self, branch: str) -> str:
|
||||
self.events.append("authority-observe")
|
||||
assert branch == BRANCH
|
||||
return self.authority_oid
|
||||
|
||||
def observe(self, branch: str) -> RemoteBranchObservation:
|
||||
self.events.append("remote-observe")
|
||||
assert branch == BRANCH
|
||||
return self.observation
|
||||
|
||||
def publish_compare_and_swap(self, branch: str, expected: str, commit: str):
|
||||
self.events.append("remote-publish")
|
||||
self.pushes.append((branch, expected, commit))
|
||||
return RemotePublicationEvidence(COMMIT_OID, COMMIT_OID, COMMIT_OID, True)
|
||||
|
||||
|
||||
class Audit:
|
||||
def __init__(self) -> None:
|
||||
self.rows: list[dict] = []
|
||||
|
||||
def record(self, event) -> None:
|
||||
self.rows.append(dict(event))
|
||||
|
||||
|
||||
def request(*, payload=None, branch=BRANCH, expected_oid=BASE_OID):
|
||||
return {
|
||||
"schema": "aios.existing-branch-publication.v1",
|
||||
"repository": "aios/aios",
|
||||
"branch": branch,
|
||||
"expected_branch_oid": expected_oid,
|
||||
"operation": "publishExistingBranchGoverned",
|
||||
"idempotency_key": "owner-approval-binding-20260810",
|
||||
"payload": {"files": ["active-runtime/app/example.py"]} if payload is None else payload,
|
||||
}
|
||||
|
||||
|
||||
def signed_approval(private: Ed25519PrivateKey, request_value, **replacements):
|
||||
from aios_existing_branch_publisher import ExistingBranchPublicationRequest
|
||||
|
||||
parsed = ExistingBranchPublicationRequest.from_mapping(request_value)
|
||||
now = datetime.now(timezone.utc)
|
||||
raw = {
|
||||
"schema": "aios.existing-branch-owner-approval.v1",
|
||||
"approval_id": "approval-owner-binding-20260810",
|
||||
"repository": parsed.repository,
|
||||
"branch": parsed.branch,
|
||||
"expected_branch_oid": parsed.expected_branch_oid,
|
||||
"operation": "publishExistingBranchGoverned",
|
||||
"issued_at": (now - timedelta(seconds=30)).isoformat(),
|
||||
"not_before": (now - timedelta(seconds=1)).isoformat(),
|
||||
"expires_at": (now + timedelta(minutes=5)).isoformat(),
|
||||
"nonce": "nonce-owner-binding-20260810",
|
||||
"owner_identity": "owner.aios",
|
||||
"trust_domain": "aios-owner-root",
|
||||
"key_id": "owner-key-20260810",
|
||||
"request_digest": parsed.request_digest,
|
||||
"idempotency_digest": parsed.idempotency_digest,
|
||||
"signature": "AA==",
|
||||
}
|
||||
raw.update(replacements)
|
||||
envelope = OwnerApprovalEnvelope.from_mapping(raw)
|
||||
raw["signature"] = base64.b64encode(private.sign(envelope.canonical_bytes())).decode("ascii")
|
||||
return raw
|
||||
|
||||
|
||||
def service(tmp_path, *, revocation="ACTIVE"):
|
||||
private = Ed25519PrivateKey.generate()
|
||||
public_key = private.public_key().public_bytes(
|
||||
serialization.Encoding.Raw, serialization.PublicFormat.Raw
|
||||
)
|
||||
root = TrustRoot(public_key, revocation=revocation)
|
||||
events: list[str] = []
|
||||
remote = Remote(events)
|
||||
audit = Audit()
|
||||
publisher = ExistingBranchPublisher(
|
||||
verifier=OwnerApprovalVerifier(root, allowed_trust_domains={"aios-owner-root"}),
|
||||
replay_ledger=DurableNonceReplayLedger(tmp_path / "replay"),
|
||||
authority=remote,
|
||||
remote=remote,
|
||||
credentials=Credentials(events),
|
||||
workspaces=Workspaces(events),
|
||||
audit=audit,
|
||||
)
|
||||
return private, root, events, remote, audit, publisher
|
||||
|
||||
|
||||
def test_valid_signed_owner_approval_is_verified_before_capability_projection(tmp_path):
|
||||
private, root, events, remote, audit, publisher = service(tmp_path)
|
||||
value = request()
|
||||
approval = signed_approval(private, value)
|
||||
|
||||
result = publisher.publish(value, approval)
|
||||
|
||||
assert result["status"] == "PUBLISHED"
|
||||
assert result["commit_oid"] == COMMIT_OID
|
||||
assert remote.pushes == [(BRANCH, BASE_OID, COMMIT_OID)]
|
||||
assert events == [
|
||||
"authority-observe",
|
||||
"remote-observe",
|
||||
"workspace-open",
|
||||
"workspace-head",
|
||||
"credential-project",
|
||||
"workspace-commit",
|
||||
"remote-publish",
|
||||
"credential-revoke",
|
||||
]
|
||||
assert root.calls == ["trust-root"]
|
||||
assert approval["signature"] not in json.dumps(audit.rows)
|
||||
assert "signature" not in audit.rows[-1]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"case",
|
||||
[
|
||||
"invalid_signature",
|
||||
"expired",
|
||||
"revoked",
|
||||
"missing_key_evidence",
|
||||
"missing_revocation_evidence",
|
||||
"wrong_repository",
|
||||
"wrong_branch",
|
||||
"wrong_oid",
|
||||
"wrong_operation",
|
||||
"arbitrary_reference",
|
||||
],
|
||||
)
|
||||
def test_invalid_owner_approval_inputs_are_rejected_without_publication(tmp_path, case):
|
||||
private, root, events, remote, _, publisher = service(
|
||||
tmp_path, revocation="REVOKED" if case == "revoked" else "ACTIVE"
|
||||
)
|
||||
value = request()
|
||||
approval = signed_approval(private, value)
|
||||
if case == "invalid_signature":
|
||||
approval["signature"] = "AAAA"
|
||||
elif case == "expired":
|
||||
approval = signed_approval(
|
||||
private,
|
||||
value,
|
||||
issued_at="2000-01-01T00:00:00+00:00",
|
||||
not_before="2000-01-01T00:00:01+00:00",
|
||||
expires_at="2000-01-01T00:01:00+00:00",
|
||||
)
|
||||
elif case == "wrong_repository":
|
||||
approval = signed_approval(private, value, repository="other/repository")
|
||||
elif case == "wrong_branch":
|
||||
approval = signed_approval(private, value, branch="agent/another-approved-branch")
|
||||
elif case == "wrong_oid":
|
||||
approval = signed_approval(private, value, expected_branch_oid="c" * 40)
|
||||
elif case == "wrong_operation":
|
||||
approval["operation"] = "differentOperation"
|
||||
elif case == "arbitrary_reference":
|
||||
approval = "owner-approval-reference-123"
|
||||
elif case == "missing_key_evidence":
|
||||
root.public_key = None
|
||||
elif case == "missing_revocation_evidence":
|
||||
root.revocation = "UNKNOWN"
|
||||
|
||||
with pytest.raises((OwnerApprovalRejected, ExistingBranchPublicationRejected)):
|
||||
publisher.publish(value, approval)
|
||||
|
||||
assert remote.pushes == []
|
||||
assert "credential-project" not in events
|
||||
assert "workspace-open" not in events
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"changed",
|
||||
[
|
||||
lambda value: {**value, "branch": "agent/approval-replay-other-branch"},
|
||||
lambda value: {**value, "expected_branch_oid": "c" * 40},
|
||||
lambda value: {**value, "payload": {"files": ["other.py"]}},
|
||||
],
|
||||
)
|
||||
def test_signed_approval_cannot_be_replayed_for_another_branch_oid_or_payload(tmp_path, changed):
|
||||
private, _, events, remote, _, publisher = service(tmp_path)
|
||||
original = request()
|
||||
approval = signed_approval(private, original)
|
||||
|
||||
with pytest.raises(OwnerApprovalRejected, match="OWNER_APPROVAL_BINDING_REJECTED"):
|
||||
publisher.publish(changed(original), approval)
|
||||
|
||||
assert remote.pushes == []
|
||||
assert events == []
|
||||
|
||||
|
||||
def test_signed_approval_nonce_is_single_use(tmp_path):
|
||||
private, _, events, remote, _, publisher = service(tmp_path)
|
||||
value = request()
|
||||
approval = signed_approval(private, value)
|
||||
|
||||
assert publisher.publish(value, approval)["status"] == "PUBLISHED"
|
||||
with pytest.raises(OwnerApprovalRejected, match="OWNER_APPROVAL_REPLAY_REJECTED"):
|
||||
publisher.publish(value, approval)
|
||||
|
||||
assert len(remote.pushes) == 1
|
||||
assert events.count("credential-project") == 1
|
||||
@@ -0,0 +1,72 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
|
||||
from aios_existing_branch_publisher import (
|
||||
ExistingBranchPublisher,
|
||||
RemoteBranchObservation,
|
||||
)
|
||||
from test_existing_branch_owner_approval import (
|
||||
BASE_OID,
|
||||
COMMIT_OID,
|
||||
request,
|
||||
service,
|
||||
signed_approval,
|
||||
)
|
||||
|
||||
|
||||
def test_remote_oid_conflict_requires_manual_owner_review_before_capability_projection(tmp_path):
|
||||
private, _, events, remote, _, publisher = service(tmp_path)
|
||||
remote.observation = RemoteBranchObservation("PRESENT", "c" * 40, "c" * 40, True)
|
||||
value = request()
|
||||
|
||||
result = publisher.publish(value, signed_approval(private, value))
|
||||
|
||||
assert result == {"status": "MANUAL_OWNER_REVIEW_REQUIRED"}
|
||||
assert events == ["authority-observe", "remote-observe"]
|
||||
assert remote.pushes == []
|
||||
|
||||
|
||||
def test_ambiguous_remote_requires_reconciliation_after_one_observation(tmp_path):
|
||||
private, _, events, remote, _, publisher = service(tmp_path)
|
||||
remote.observation = RemoteBranchObservation("AMBIGUOUS", None, None, None)
|
||||
value = request()
|
||||
|
||||
result = publisher.publish(value, signed_approval(private, value))
|
||||
|
||||
assert result == {"status": "RECONCILIATION_REQUIRED"}
|
||||
assert events == ["authority-observe", "remote-observe"]
|
||||
assert remote.pushes == []
|
||||
|
||||
|
||||
def test_authority_oid_conflict_requires_manual_owner_review(tmp_path):
|
||||
private, _, events, remote, _, publisher = service(tmp_path)
|
||||
remote.authority_oid = "c" * 40
|
||||
value = request()
|
||||
|
||||
result = publisher.publish(value, signed_approval(private, value))
|
||||
|
||||
assert result == {"status": "MANUAL_OWNER_REVIEW_REQUIRED"}
|
||||
assert events == ["authority-observe"]
|
||||
assert remote.pushes == []
|
||||
|
||||
|
||||
def test_success_reports_all_remote_agreement_fields(tmp_path):
|
||||
private, _, _, _, _, publisher = service(tmp_path)
|
||||
value = request()
|
||||
|
||||
result = publisher.publish(value, signed_approval(private, value))
|
||||
|
||||
assert result == {
|
||||
"status": "PUBLISHED",
|
||||
"commit_oid": COMMIT_OID,
|
||||
"authority_oid": COMMIT_OID,
|
||||
"gitea_git_oid": COMMIT_OID,
|
||||
"gitea_api_oid": COMMIT_OID,
|
||||
"commit_visible": True,
|
||||
}
|
||||
|
||||
|
||||
def test_publisher_exposes_only_compare_and_swap_publication_path():
|
||||
source = inspect.getsource(ExistingBranchPublisher).lower()
|
||||
assert "force" not in source
|
||||
Reference in New Issue
Block a user