Merge pull request 'gitops: Silent Release execution readiness' (#13) from patch/silent-release-execution-readiness-20260724 into main
This commit was merged in pull request #13.
This commit is contained in:
@@ -100,10 +100,14 @@ READ_ONLY_GET_PATHS = frozenset({
|
||||
'/admin/ai-evolution/api/data',
|
||||
'/api/release-consistency', '/api/tag-publication-status',
|
||||
'/api/release-acceptance',
|
||||
'/getReleaseCandidate', '/getApprovalEnvelope', '/getReleaseSaga',
|
||||
'/getReleaseCandidate', '/getReleasePlan', '/getReleaseAcceptance',
|
||||
'/getApprovalEnvelope', '/getReleaseSaga', '/getCapabilityStatus',
|
||||
'/getRecoveryStatus',
|
||||
})
|
||||
SILENT_RELEASE_POST_PATHS = frozenset({
|
||||
'/createReleaseCandidate',
|
||||
'/createReleaseIntent',
|
||||
'/submitSignedApproval',
|
||||
'/submitOwnerApproval',
|
||||
'/startDeployment',
|
||||
})
|
||||
@@ -533,8 +537,11 @@ class H(BaseHTTPRequestHandler):
|
||||
return self.sendj(publication_publisher().run_release_acceptance(request))
|
||||
if path in (
|
||||
'/getReleaseCandidate',
|
||||
'/getReleasePlan',
|
||||
'/getReleaseAcceptance',
|
||||
'/getApprovalEnvelope',
|
||||
'/getReleaseSaga',
|
||||
'/getCapabilityStatus',
|
||||
):
|
||||
candidate_id=(q.get('candidate_id') or [''])[0]
|
||||
if not candidate_id:
|
||||
@@ -548,7 +555,19 @@ class H(BaseHTTPRequestHandler):
|
||||
return self.sendj(
|
||||
service.get_approval_envelope(candidate_id)
|
||||
)
|
||||
if path == '/getReleasePlan':
|
||||
return self.sendj(service.get_release_plan(candidate_id))
|
||||
if path == '/getReleaseAcceptance':
|
||||
return self.sendj(service.get_acceptance(candidate_id))
|
||||
if path == '/getCapabilityStatus':
|
||||
return self.sendj(
|
||||
service.get_capability_status(candidate_id)
|
||||
)
|
||||
return self.sendj(service.get_release_saga(candidate_id))
|
||||
if path == '/getRecoveryStatus':
|
||||
return self.sendj(
|
||||
silent_release_service().get_recovery_status()
|
||||
)
|
||||
# Admin AI Evolution Dashboard
|
||||
if path in ('/admin/ai-evolution','/admin/ai-evolution/'):
|
||||
return self.ai_evolution_page()
|
||||
@@ -582,7 +601,13 @@ class H(BaseHTTPRequestHandler):
|
||||
return self.sendj(
|
||||
silent_release_service().create_release_candidate(data)
|
||||
)
|
||||
if path == '/submitOwnerApproval':
|
||||
if path == '/createReleaseIntent':
|
||||
a=self.need('release:candidate')
|
||||
if not a: return
|
||||
return self.sendj(
|
||||
silent_release_service().create_release_intent(data)
|
||||
)
|
||||
if path in ('/submitOwnerApproval','/submitSignedApproval'):
|
||||
a=self.need('release:approve')
|
||||
if not a: return
|
||||
return self.sendj(
|
||||
|
||||
@@ -0,0 +1,564 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execution-readiness adapters and the fail-closed Shadow controller service."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import dataclasses
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from http import HTTPStatus
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Mapping, Protocol
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
|
||||
Ed25519PrivateKey,
|
||||
)
|
||||
|
||||
from silent_release_controller import _sanitize, utc_now
|
||||
from silent_release_hardening import (
|
||||
BuilderAttestation,
|
||||
CapabilityAuthorizationError,
|
||||
ReleaseRecoveryWorker,
|
||||
SignedApprovalVerifier,
|
||||
SignedReleasePlanApproval,
|
||||
SilentReleaseActionService,
|
||||
TrustedCandidateBundle,
|
||||
_canonical_json,
|
||||
_durable_json,
|
||||
_safe_child,
|
||||
)
|
||||
|
||||
|
||||
class BuilderSigner(Protocol):
|
||||
@property
|
||||
def key_id(self) -> str:
|
||||
...
|
||||
|
||||
def sign(self, payload: bytes) -> bytes:
|
||||
...
|
||||
|
||||
|
||||
class LocalTestBuilderSigner:
|
||||
"""Ephemeral in-memory signer. It is forbidden for production execution."""
|
||||
|
||||
test_only = True
|
||||
|
||||
def __init__(self, key_id: str, private_key: Ed25519PrivateKey):
|
||||
self._key_id = str(key_id)
|
||||
self._private_key = private_key
|
||||
|
||||
@property
|
||||
def key_id(self) -> str:
|
||||
return self._key_id
|
||||
|
||||
@classmethod
|
||||
def generate(
|
||||
cls, key_id: str = "shadow-builder-test-key"
|
||||
) -> "LocalTestBuilderSigner":
|
||||
return cls(key_id, Ed25519PrivateKey.generate())
|
||||
|
||||
@property
|
||||
def public_key(self):
|
||||
return self._private_key.public_key()
|
||||
|
||||
def sign(self, payload: bytes) -> bytes:
|
||||
return self._private_key.sign(payload)
|
||||
|
||||
|
||||
class ExternalKmsBuilderSigner:
|
||||
"""Signer facade whose private operation remains in KMS/HSM."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
key_id: str,
|
||||
credential_reference: str,
|
||||
sign_callback: Callable[[str, bytes], bytes],
|
||||
):
|
||||
if not str(credential_reference).startswith(
|
||||
("kms://", "hsm://", "secret-manager://")
|
||||
):
|
||||
raise ValueError("production builder requires a KMS/HSM reference")
|
||||
self._key_id = str(key_id)
|
||||
self.credential_reference = str(credential_reference)
|
||||
self._sign_callback = sign_callback
|
||||
|
||||
@property
|
||||
def key_id(self) -> str:
|
||||
return self._key_id
|
||||
|
||||
def sign(self, payload: bytes) -> bytes:
|
||||
return bytes(self._sign_callback(self.credential_reference, payload))
|
||||
|
||||
|
||||
class SignedTrustedBuilderAdapter:
|
||||
"""Signs evidence produced by a trusted collector, never caller evidence."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
collector: Callable[[Mapping[str, Any]], TrustedCandidateBundle],
|
||||
signer: BuilderSigner,
|
||||
):
|
||||
self.collector = collector
|
||||
self.signer = signer
|
||||
|
||||
def build(self, intent: Mapping[str, Any]) -> TrustedCandidateBundle:
|
||||
forbidden = {
|
||||
"artifact_sha256",
|
||||
"canary",
|
||||
"checks",
|
||||
"manifest_sha256",
|
||||
"tests",
|
||||
"tree_oid",
|
||||
}
|
||||
if forbidden.intersection(intent):
|
||||
raise CapabilityAuthorizationError(
|
||||
"trusted builder rejects caller-supplied evidence"
|
||||
)
|
||||
unsigned = self.collector(dict(intent))
|
||||
if unsigned.attestation is not None:
|
||||
raise CapabilityAuthorizationError(
|
||||
"collector cannot self-attest its evidence"
|
||||
)
|
||||
candidate = unsigned.candidate
|
||||
test_sha = hashlib.sha256(
|
||||
_canonical_json(_sanitize(candidate.tests))
|
||||
).hexdigest()
|
||||
canary_sha = hashlib.sha256(
|
||||
_canonical_json(_sanitize(candidate.canary))
|
||||
).hexdigest()
|
||||
attestation_id = "builder-attestation-" + hashlib.sha256(
|
||||
(
|
||||
candidate.candidate_id
|
||||
+ "\0"
|
||||
+ candidate.source_commit
|
||||
+ "\0"
|
||||
+ candidate.artifact_sha256
|
||||
).encode("utf-8")
|
||||
).hexdigest()[:24]
|
||||
attestation = BuilderAttestation(
|
||||
builder_attestation_id=attestation_id,
|
||||
builder_key_id=self.signer.key_id,
|
||||
source_commit=candidate.source_commit,
|
||||
tree_oid=candidate.tree_oid,
|
||||
artifact_sha256=candidate.artifact_sha256,
|
||||
manifest_sha256=candidate.manifest_sha256,
|
||||
package_closure_sha256=(
|
||||
unsigned.release_plan.deployment.package_closure_sha256
|
||||
),
|
||||
test_evidence_sha256=test_sha,
|
||||
canary_evidence_sha256=canary_sha,
|
||||
issued_at=utc_now(),
|
||||
signature="",
|
||||
)
|
||||
attestation = dataclasses.replace(
|
||||
attestation,
|
||||
signature=base64.b64encode(
|
||||
self.signer.sign(attestation.canonical_bytes())
|
||||
).decode("ascii"),
|
||||
)
|
||||
return dataclasses.replace(unsigned, attestation=attestation)
|
||||
|
||||
|
||||
class ApprovalBackendAdapter(Protocol):
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
...
|
||||
|
||||
def verify(
|
||||
self,
|
||||
approval: SignedReleasePlanApproval,
|
||||
*,
|
||||
candidate,
|
||||
release_plan,
|
||||
policy_sha256: str,
|
||||
) -> SignedReleasePlanApproval:
|
||||
...
|
||||
|
||||
|
||||
class LocalTestApprovalSigner:
|
||||
"""Test-only approval signer used solely for Shadow acceptance."""
|
||||
|
||||
test_only = True
|
||||
|
||||
def __init__(self, key_id: str, private_key: Ed25519PrivateKey):
|
||||
self.key_id = str(key_id)
|
||||
self.private_key = private_key
|
||||
|
||||
@classmethod
|
||||
def generate(
|
||||
cls, key_id: str = "shadow-owner-test-key"
|
||||
) -> "LocalTestApprovalSigner":
|
||||
return cls(key_id, Ed25519PrivateKey.generate())
|
||||
|
||||
def sign(
|
||||
self, approval: SignedReleasePlanApproval
|
||||
) -> SignedReleasePlanApproval:
|
||||
unsigned = dataclasses.replace(
|
||||
approval, key_id=self.key_id, signature=""
|
||||
)
|
||||
return dataclasses.replace(
|
||||
unsigned,
|
||||
signature=base64.b64encode(
|
||||
self.private_key.sign(unsigned.canonical_bytes())
|
||||
).decode("ascii"),
|
||||
)
|
||||
|
||||
|
||||
class ProductionApprovalBackendAdapter:
|
||||
"""Verification-only production adapter configured by a secret/KMS ref."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
verifier: SignedApprovalVerifier,
|
||||
credential_reference: str,
|
||||
):
|
||||
if not str(credential_reference).startswith(
|
||||
("kms://", "hsm://", "secret-manager://")
|
||||
):
|
||||
raise ValueError(
|
||||
"production approval backend requires a KMS/secret reference"
|
||||
)
|
||||
self.verifier = verifier
|
||||
self.credential_reference = str(credential_reference)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
return True
|
||||
|
||||
def verify(
|
||||
self,
|
||||
approval: SignedReleasePlanApproval,
|
||||
*,
|
||||
candidate,
|
||||
release_plan,
|
||||
policy_sha256: str,
|
||||
) -> SignedReleasePlanApproval:
|
||||
return self.verifier.verify_release_plan(
|
||||
approval, candidate, release_plan, policy_sha256
|
||||
)
|
||||
|
||||
|
||||
class UnavailableProductionApprovalBackend:
|
||||
credential_reference = None
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
return False
|
||||
|
||||
def verify(self, *args, **kwargs):
|
||||
raise CapabilityAuthorizationError(
|
||||
"production approval backend is not configured"
|
||||
)
|
||||
|
||||
|
||||
class LeaderLock:
|
||||
def __init__(self, path: Path):
|
||||
self.path = Path(path)
|
||||
self.handle = None
|
||||
|
||||
def __enter__(self):
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
os.chmod(self.path.parent, 0o700)
|
||||
self.handle = self.path.open("a+", encoding="utf-8")
|
||||
os.chmod(self.path, 0o600)
|
||||
try:
|
||||
import fcntl
|
||||
|
||||
fcntl.flock(
|
||||
self.handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB
|
||||
)
|
||||
except BlockingIOError as exc:
|
||||
self.handle.close()
|
||||
self.handle = None
|
||||
raise RuntimeError("silent release leader is already active") from exc
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
if self.handle is not None:
|
||||
try:
|
||||
import fcntl
|
||||
|
||||
fcntl.flock(self.handle.fileno(), fcntl.LOCK_UN)
|
||||
finally:
|
||||
self.handle.close()
|
||||
self.handle = None
|
||||
|
||||
|
||||
class PersistentSilentReleaseController:
|
||||
"""Idempotent main-event controller with durable startup/recovery reports."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
root: Path,
|
||||
service: SilentReleaseActionService,
|
||||
*,
|
||||
main_probe: Callable[[], str],
|
||||
recovery_worker: ReleaseRecoveryWorker | None = None,
|
||||
):
|
||||
self.root = Path(root)
|
||||
self.service = service
|
||||
self.main_probe = main_probe
|
||||
self.recovery_worker = recovery_worker
|
||||
self.leader_path = self.root / "controller.leader.lock"
|
||||
self.status_path = self.root / "controller-status.json"
|
||||
|
||||
def startup_verify(self) -> dict[str, Any]:
|
||||
checked = 0
|
||||
failed = 0
|
||||
for journal in sorted(self.service.sagas.root.glob("*.events.jsonl")):
|
||||
candidate_id = journal.name[: -len(".events.jsonl")]
|
||||
saga = self.service.sagas.load(candidate_id)
|
||||
checked += 1
|
||||
if not saga.journal_integrity:
|
||||
failed += 1
|
||||
status = {
|
||||
"checked_sagas": checked,
|
||||
"execution_enabled": self.service.execution_enabled,
|
||||
"failed_closed": failed,
|
||||
"shadow_mode": not self.service.execution_enabled,
|
||||
"side_effects_executed": False,
|
||||
"status": "READY" if failed == 0 else "FAIL_CLOSED",
|
||||
}
|
||||
_durable_json(self.status_path, status)
|
||||
return status
|
||||
|
||||
def run_once(self) -> dict[str, Any]:
|
||||
with LeaderLock(self.leader_path):
|
||||
startup = self.startup_verify()
|
||||
if startup["status"] != "READY":
|
||||
result = {
|
||||
"execution_enabled": self.service.execution_enabled,
|
||||
"intent": None,
|
||||
"notification": None,
|
||||
"recovery": None,
|
||||
"shadow_mode": not self.service.execution_enabled,
|
||||
"side_effects_executed": False,
|
||||
"startup": startup,
|
||||
"status": "FAIL_CLOSED",
|
||||
}
|
||||
_durable_json(self.status_path, result)
|
||||
return _sanitize(result)
|
||||
source_commit = str(self.main_probe())
|
||||
event_id = f"main-{source_commit}"
|
||||
intent = self.service.create_release_intent(
|
||||
{"event_id": event_id, "source_commit": source_commit}
|
||||
)
|
||||
notification = None
|
||||
if intent.get("candidate_id"):
|
||||
notification = self.service.ensure_owner_notification(
|
||||
str(intent["candidate_id"])
|
||||
)
|
||||
recovery = (
|
||||
self.recovery_worker.run_once()
|
||||
if self.recovery_worker is not None
|
||||
else self.service.get_recovery_status()
|
||||
)
|
||||
result = {
|
||||
"execution_enabled": self.service.execution_enabled,
|
||||
"intent": intent,
|
||||
"notification": notification,
|
||||
"recovery": recovery,
|
||||
"shadow_mode": not self.service.execution_enabled,
|
||||
"side_effects_executed": False,
|
||||
"startup": startup,
|
||||
}
|
||||
_durable_json(self.status_path, result)
|
||||
return _sanitize(result)
|
||||
|
||||
|
||||
class ShadowActionHandler(BaseHTTPRequestHandler):
|
||||
service: SilentReleaseActionService
|
||||
controller: PersistentSilentReleaseController
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
return
|
||||
|
||||
def _json(self, payload: Mapping[str, Any], status=HTTPStatus.OK):
|
||||
body = json.dumps(_sanitize(dict(payload)), sort_keys=True).encode()
|
||||
self.send_response(int(status))
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def _body(self) -> dict[str, Any]:
|
||||
size = int(self.headers.get("Content-Length", "0"))
|
||||
if size > 1024 * 1024:
|
||||
raise ValueError("request body is too large")
|
||||
return json.loads(self.rfile.read(size) or b"{}")
|
||||
|
||||
def do_GET(self):
|
||||
parsed = urlparse(self.path)
|
||||
query = parse_qs(parsed.query)
|
||||
candidate_id = str((query.get("candidate_id") or [""])[0])
|
||||
try:
|
||||
if parsed.path == "/healthz":
|
||||
return self._json(
|
||||
{
|
||||
"execution_enabled": False,
|
||||
"shadow_mode": True,
|
||||
"side_effects_executed": False,
|
||||
"status": "ok",
|
||||
}
|
||||
)
|
||||
routes = {
|
||||
"/getReleaseCandidate": self.service.get_release_candidate,
|
||||
"/getReleasePlan": self.service.get_release_plan,
|
||||
"/getReleaseAcceptance": self.service.get_acceptance,
|
||||
"/getReleaseSaga": self.service.get_release_saga,
|
||||
"/getCapabilityStatus": self.service.get_capability_status,
|
||||
}
|
||||
if parsed.path in routes:
|
||||
return self._json(routes[parsed.path](candidate_id))
|
||||
if parsed.path == "/getRecoveryStatus":
|
||||
return self._json(self.service.get_recovery_status())
|
||||
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
|
||||
except (ValueError, FileNotFoundError) as exc:
|
||||
self._json(
|
||||
{"error": type(exc).__name__}, HTTPStatus.BAD_REQUEST
|
||||
)
|
||||
|
||||
def do_POST(self):
|
||||
parsed = urlparse(self.path)
|
||||
try:
|
||||
body = self._body()
|
||||
if parsed.path == "/createReleaseIntent":
|
||||
return self._json(self.service.create_release_intent(body))
|
||||
if parsed.path in {
|
||||
"/submitSignedApproval",
|
||||
"/submitOwnerApproval",
|
||||
}:
|
||||
return self._json(self.service.submit_owner_approval(body))
|
||||
if parsed.path == "/startDeployment":
|
||||
return self._json(
|
||||
self.service.start_deployment(
|
||||
str(body.get("candidate_id") or "")
|
||||
)
|
||||
)
|
||||
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
|
||||
except (ValueError, FileNotFoundError, CapabilityAuthorizationError) as exc:
|
||||
self._json(
|
||||
{"error": type(exc).__name__}, HTTPStatus.BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
def git_main_probe(repository: Path) -> Callable[[], str]:
|
||||
repository = Path(repository)
|
||||
|
||||
def probe() -> str:
|
||||
completed = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
f"--git-dir={repository}",
|
||||
"rev-parse",
|
||||
"refs/heads/main",
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=15,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
return completed.stdout.strip()
|
||||
|
||||
return probe
|
||||
|
||||
|
||||
def serve_shadow(
|
||||
*,
|
||||
state_root: Path,
|
||||
repository: Path,
|
||||
host: str,
|
||||
port: int,
|
||||
poll_seconds: float,
|
||||
) -> None:
|
||||
service = SilentReleaseActionService.shadow(state_root)
|
||||
controller = PersistentSilentReleaseController(
|
||||
state_root,
|
||||
service,
|
||||
main_probe=git_main_probe(repository),
|
||||
)
|
||||
controller.run_once()
|
||||
stop = threading.Event()
|
||||
|
||||
def controller_loop() -> None:
|
||||
while not stop.wait(poll_seconds):
|
||||
try:
|
||||
controller.run_once()
|
||||
except Exception as exc: # fail closed and keep the API available
|
||||
_durable_json(
|
||||
controller.status_path,
|
||||
{
|
||||
"error": type(exc).__name__,
|
||||
"execution_enabled": False,
|
||||
"shadow_mode": True,
|
||||
"side_effects_executed": False,
|
||||
"status": "FAIL_CLOSED",
|
||||
},
|
||||
)
|
||||
|
||||
handler = type(
|
||||
"ConfiguredShadowActionHandler",
|
||||
(ShadowActionHandler,),
|
||||
{"service": service, "controller": controller},
|
||||
)
|
||||
server = ThreadingHTTPServer((host, port), handler)
|
||||
api_thread = threading.Thread(
|
||||
target=server.serve_forever,
|
||||
name="silent-release-shadow-api",
|
||||
daemon=False,
|
||||
)
|
||||
controller_thread = threading.Thread(
|
||||
target=controller_loop,
|
||||
name="silent-release-shadow-controller",
|
||||
daemon=True,
|
||||
)
|
||||
api_thread.start()
|
||||
controller_thread.start()
|
||||
try:
|
||||
while api_thread.is_alive():
|
||||
api_thread.join(timeout=1)
|
||||
finally:
|
||||
stop.set()
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
controller_thread.join(timeout=max(1.0, poll_seconds + 1.0))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--state-root", required=True, type=Path)
|
||||
parser.add_argument("--repository", required=True, type=Path)
|
||||
parser.add_argument("--host", default="127.0.0.1")
|
||||
parser.add_argument("--port", default=18891, type=int)
|
||||
parser.add_argument("--poll-seconds", default=30.0, type=float)
|
||||
arguments = parser.parse_args()
|
||||
if arguments.poll_seconds <= 0:
|
||||
raise SystemExit("poll seconds must be positive")
|
||||
if os.environ.get("AIOS_GITOPS_WRITES_ENABLED", "false").lower() != "false":
|
||||
raise SystemExit("GitOps writes must remain disabled")
|
||||
if (
|
||||
os.environ.get(
|
||||
"AIOS_SILENT_RELEASE_EXECUTION_ENABLED", "false"
|
||||
).lower()
|
||||
!= "false"
|
||||
):
|
||||
raise SystemExit("Silent Release execution must remain disabled")
|
||||
serve_shadow(
|
||||
state_root=arguments.state_root,
|
||||
repository=arguments.repository,
|
||||
host=arguments.host,
|
||||
port=arguments.port,
|
||||
poll_seconds=arguments.poll_seconds,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"schema_version": 3,
|
||||
"schema_version": 4,
|
||||
"automatic": [
|
||||
"build",
|
||||
"test",
|
||||
@@ -66,12 +66,38 @@
|
||||
"trusted_candidate_builder_required": true,
|
||||
"actual_plan_regeneration_required": true,
|
||||
"startup_fail_closed_on_missing_component": true,
|
||||
"recovery_worker_shadow_default": true
|
||||
"recovery_worker_shadow_default": true,
|
||||
"production_approval_backend_required": true,
|
||||
"production_builder_signer": "kms-hsm-secret-manager-only",
|
||||
"production_execution_without_backend": "unavailable"
|
||||
},
|
||||
"durability": {
|
||||
"journal": "sha256-hash-chain",
|
||||
"probe_journal": "sha256-hash-chain",
|
||||
"exclusive_capability_claims": true,
|
||||
"side_effect_probe_before_lease_takeover": true,
|
||||
"side_effect_probe_before_failed_recoverable_retry": true,
|
||||
"probe_results": [
|
||||
"NOT_APPLIED",
|
||||
"APPLIED_VERIFIED",
|
||||
"INDETERMINATE",
|
||||
"CONFLICT"
|
||||
],
|
||||
"probe_operation_digest_required": true,
|
||||
"probe_freshness_required": true,
|
||||
"tamper_result": "MANUAL_OWNER_REVIEW_REQUIRED"
|
||||
},
|
||||
"tag_pending": {
|
||||
"required_observations": [
|
||||
"remote_git_ref",
|
||||
"annotated_tag_object",
|
||||
"peeled_target",
|
||||
"gitea_api",
|
||||
"gitea_listing",
|
||||
"repository_identity"
|
||||
],
|
||||
"indeterminate_result": "RECONCILIATION_REQUIRED",
|
||||
"conflict_result": "MANUAL_OWNER_REVIEW_REQUIRED",
|
||||
"automatic_reconciliation": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
[Unit]
|
||||
Description=AIOS Silent Release Shadow Controller
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=aiman
|
||||
Group=gitops
|
||||
UMask=0077
|
||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||
Environment=AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
|
||||
Environment=PYTHONDONTWRITEBYTECODE=1
|
||||
ExecStart=/usr/bin/python3 /usr/libexec/aios-silent-release-shadow/current/active-runtime/app/silent_release_execution.py --state-root /var/lib/aios-silent-release-shadow --repository /data/aios-gitops/repo.git --host 127.0.0.1 --port 18891 --poll-seconds 30
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ReadWritePaths=/var/lib/aios-silent-release-shadow
|
||||
ReadOnlyPaths=/data/aios-gitops/repo.git /usr/libexec/aios-silent-release-shadow
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,107 @@
|
||||
# AIOS Silent Release Execution Readiness
|
||||
|
||||
## Scope and stop point
|
||||
|
||||
This change makes the controller execution-ready while deploying only a
|
||||
localhost-bound Shadow control plane. Both production gates remain false:
|
||||
|
||||
```text
|
||||
AIOS_GITOPS_WRITES_ENABLED=false
|
||||
AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
|
||||
```
|
||||
|
||||
Shadow completion is evidence preparation, not a production release. It cannot
|
||||
write a production tag, switch a business runtime, or mutate Baseline/Registry.
|
||||
|
||||
## Recovery state machine
|
||||
|
||||
Every expired or `FAILED_RECOVERABLE` capability requires fresh, operation-bound
|
||||
probe evidence before another adapter may run:
|
||||
|
||||
```text
|
||||
NOT_APPLIED -> one new exclusive claim may retry
|
||||
APPLIED_VERIFIED -> mark COMPLETED without adapter replay
|
||||
INDETERMINATE -> RECONCILIATION_REQUIRED; no automatic writes
|
||||
CONFLICT -> MANUAL_OWNER_REVIEW_REQUIRED; no automatic writes
|
||||
```
|
||||
|
||||
Probe evidence binds the immutable operation digest, is freshness checked, and
|
||||
is appended to a separate SHA-256 hash-chained journal. Completion still
|
||||
requires the active claim ID. File locks and leases ensure that concurrent
|
||||
recovery workers cannot execute the same adapter.
|
||||
|
||||
`TAG_PENDING` uses independent remote ref, annotated object, peeled target,
|
||||
Gitea API, Gitea listing, and repository identity evidence. Exact absence may
|
||||
roll back a switched runtime. Exact publication continues to finalization.
|
||||
Unavailable/divergent evidence stops at reconciliation; a different OID or
|
||||
target stops for Owner review. No branch can force, delete, or rewrite a tag.
|
||||
|
||||
## Trusted build and approval
|
||||
|
||||
Callers submit only an event ID and exact source commit. Test, canary, artifact,
|
||||
manifest, rollback, secret scan, Git and Gitea evidence are generated by the
|
||||
trusted builder. The builder emits an Ed25519 attestation binding:
|
||||
|
||||
- builder attestation/key IDs;
|
||||
- source commit and tree OID;
|
||||
- artifact, manifest and package-closure SHA-256;
|
||||
- test and canary evidence SHA-256;
|
||||
- issuance time and signature.
|
||||
|
||||
The controller contains only trusted public keys. Production signing is exposed
|
||||
through a KMS/HSM/secret-manager adapter; a local private-key adapter is marked
|
||||
test-only and is used only for ephemeral Shadow acceptance.
|
||||
|
||||
Owner approvals are immutable, time-bounded, revocable, trust-domain bound, and
|
||||
single-use per capability ledger. If the production approval backend reference
|
||||
is absent, production execution is unavailable while Shadow remains available.
|
||||
Caller-supplied `owner_id` is never a fallback.
|
||||
|
||||
## Persistent controller and API
|
||||
|
||||
`aios-silent-release-controller.service` runs independently from the business
|
||||
GitOps API. It uses a single leader lock, durable intents/candidates/Sagas,
|
||||
startup journal verification, idempotent main events, one notification record,
|
||||
and recovery reporting. The final deployed service is report-only Shadow mode.
|
||||
|
||||
The Action contract exposes:
|
||||
|
||||
- `createReleaseIntent`
|
||||
- `getReleaseCandidate`
|
||||
- `getReleasePlan`
|
||||
- `getReleaseAcceptance`
|
||||
- `submitSignedApproval`
|
||||
- `getReleaseSaga`
|
||||
- `getCapabilityStatus`
|
||||
- `startDeployment`
|
||||
- `getRecoveryStatus`
|
||||
|
||||
Every Shadow response declares `execution_enabled=false`, `shadow_mode=true`
|
||||
and `side_effects_executed=false`. `startDeployment` returns a simulated
|
||||
`SHADOW_COMPLETED` result and never claims production capabilities.
|
||||
|
||||
## Migration and rollback
|
||||
|
||||
1. Merge through a governed pull request.
|
||||
2. Build from a clean detached worktree at the exact merge commit.
|
||||
3. Verify artifact, manifest, package closure, tests, live fixture, no-.git
|
||||
canary, rollback archive and secret scan.
|
||||
4. Install a versioned Shadow release without changing the business runtime.
|
||||
5. Start the separate localhost-only controller with both gates false.
|
||||
6. Run Shadow candidates and freeze production PID/tag/Baseline/Registry hashes.
|
||||
7. Remove ephemeral test keys and temporary repositories.
|
||||
|
||||
Rollback stops and disables only the Shadow controller and restores/removes its
|
||||
versioned `current` link. The existing production API, tag set, Baseline and
|
||||
Registry are not involved.
|
||||
|
||||
## Future activation envelope
|
||||
|
||||
Activation requires a new one-time Owner approval binding the exact controller
|
||||
commit, artifact/manifest/policy/unit hashes, trusted key IDs, KMS/secret
|
||||
references, release and rollback roots, authoritative Git/repository identity,
|
||||
Shadow candidate/evidence IDs, test results, activation plan and rollback plan.
|
||||
That approval may only flip
|
||||
`AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false` to `true`; each real release still
|
||||
requires its own signed ReleasePlan. This change does not request or consume
|
||||
that activation approval.
|
||||
@@ -172,6 +172,13 @@ components:
|
||||
candidate_id: {type: string, pattern: '^[A-Za-z0-9][A-Za-z0-9_.-]{2,127}$'}
|
||||
source_commit: {type: string, pattern: '^[0-9a-f]{40}$'}
|
||||
version: {type: string, pattern: '^V[0-9]+R[0-9]+C[0-9]+(?:[-.][A-Za-z0-9][A-Za-z0-9.-]*)?$'}
|
||||
ReleaseIntentInput:
|
||||
type: object
|
||||
required: [event_id, source_commit]
|
||||
additionalProperties: false
|
||||
properties:
|
||||
event_id: {type: string, pattern: '^[A-Za-z0-9][A-Za-z0-9_.:-]{2,255}$'}
|
||||
source_commit: {type: string, pattern: '^[0-9a-f]{40}$'}
|
||||
SignedReleasePlanApproval:
|
||||
type: object
|
||||
required: [release_plan_id, candidate_id, source_commit, tree_oid, artifact_sha256, manifest_sha256, deployment_plan_sha256, release_plan_sha256, policy_sha256, approval_plan_id, evidence_closure_oid, owner_approval_record_oid, capabilities, owner_identity, trust_domain, issued_at, not_before, expires_at, nonce, key_id, signature]
|
||||
@@ -209,6 +216,20 @@ components:
|
||||
properties:
|
||||
candidate_id: {type: string}
|
||||
paths:
|
||||
/createReleaseIntent:
|
||||
post:
|
||||
operationId: createReleaseIntent
|
||||
summary: Create an idempotent intent; all build evidence comes from the trusted builder
|
||||
security: [{bearerAuth: []}]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/ReleaseIntentInput'}
|
||||
responses:
|
||||
'200': {description: Intent created or idempotently returned}
|
||||
'400': {description: Caller evidence or invalid intent rejected}
|
||||
x-openai-isConsequential: true
|
||||
/createReleaseCandidate:
|
||||
post:
|
||||
operationId: createReleaseCandidate
|
||||
@@ -233,6 +254,22 @@ paths:
|
||||
responses:
|
||||
'200': {description: Candidate}
|
||||
'404': {description: Candidate not found}
|
||||
/getReleasePlan:
|
||||
get:
|
||||
operationId: getReleasePlan
|
||||
parameters:
|
||||
- {in: query, name: candidate_id, required: true, schema: {type: string}}
|
||||
responses:
|
||||
'200': {description: Immutable release plan}
|
||||
'404': {description: Candidate not found}
|
||||
/getReleaseAcceptance:
|
||||
get:
|
||||
operationId: getReleaseAcceptance
|
||||
parameters:
|
||||
- {in: query, name: candidate_id, required: true, schema: {type: string}}
|
||||
responses:
|
||||
'200': {description: Builder attestation and acceptance checks}
|
||||
'404': {description: Candidate not found}
|
||||
/getApprovalEnvelope:
|
||||
get:
|
||||
operationId: getApprovalEnvelope
|
||||
@@ -258,6 +295,20 @@ paths:
|
||||
'403': {description: Signature, key, expiry, or revocation failure}
|
||||
'409': {description: Conflicting release plan}
|
||||
x-openai-isConsequential: true
|
||||
/submitSignedApproval:
|
||||
post:
|
||||
operationId: submitSignedApproval
|
||||
summary: Alias for immutable signed owner approval submission
|
||||
security: [{bearerAuth: []}]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/SignedReleasePlanApproval'}
|
||||
responses:
|
||||
'200': {description: Signature verified and approval stored}
|
||||
'403': {description: Production approval backend unavailable or signature invalid}
|
||||
x-openai-isConsequential: true
|
||||
/startDeployment:
|
||||
post:
|
||||
operationId: startDeployment
|
||||
@@ -282,6 +333,18 @@ paths:
|
||||
responses:
|
||||
'200': {description: Saga snapshot}
|
||||
'404': {description: Saga not found}
|
||||
/getCapabilityStatus:
|
||||
get:
|
||||
operationId: getCapabilityStatus
|
||||
parameters:
|
||||
- {in: query, name: candidate_id, required: true, schema: {type: string}}
|
||||
responses:
|
||||
'200': {description: Durable single-use capability records}
|
||||
/getRecoveryStatus:
|
||||
get:
|
||||
operationId: getRecoveryStatus
|
||||
responses:
|
||||
'200': {description: Fail-closed recovery classification}
|
||||
/api/base:
|
||||
get:
|
||||
operationId: getBaseline
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build a deterministic, secret-scanned Silent Release evidence package."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
AUTHORIZATION_PATTERN = re.compile(
|
||||
rb"Authorization:\s*(?:Bearer|token)\s+([A-Za-z0-9._~+/=-]{16,})",
|
||||
re.I,
|
||||
)
|
||||
SECRET_PATTERNS = (
|
||||
re.compile(rb"(?:ghp|glpat)_[A-Za-z0-9_-]{20,}", re.I),
|
||||
re.compile(
|
||||
rb"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----\r?\n"
|
||||
rb"[A-Za-z0-9+/=\r\n]{64,}\r?\n"
|
||||
rb"-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----"
|
||||
),
|
||||
)
|
||||
ALLOWED_AUTHORIZATION_PLACEHOLDER_SHA256 = frozenset(
|
||||
{
|
||||
"009f7191815ec615d09d0698457d048432e9a73f62ab4f26af1875009d0afbbc",
|
||||
"599a7f359d1e11124054f8afeae201f2d265b8988d91cb5b7d4dc4c9e2225c30",
|
||||
"f39dac6cbaba535e2c207cd0cd8f154974223c848f727f98b3564cea569b41cf",
|
||||
}
|
||||
)
|
||||
TARGET_TESTS = (
|
||||
"tests/test_silent_release_execution_readiness.py",
|
||||
"tests/test_silent_release_controller.py",
|
||||
"tests/test_silent_release_hardening.py",
|
||||
"tests/test_silent_release_production_contract.py",
|
||||
"tests/test_chatgpt_actions_tag.py",
|
||||
"tests/test_gitea_tag_publication.py",
|
||||
"tests/test_authoritative_git_context.py",
|
||||
)
|
||||
|
||||
|
||||
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 atomic_json(path: Path, payload: dict[str, Any]) -> None:
|
||||
temporary = path.with_suffix(path.suffix + ".tmp")
|
||||
with temporary.open("w", encoding="utf-8", newline="\n") as output:
|
||||
json.dump(payload, output, sort_keys=True, separators=(",", ":"))
|
||||
output.write("\n")
|
||||
output.flush()
|
||||
os.fsync(output.fileno())
|
||||
os.replace(temporary, path)
|
||||
|
||||
|
||||
def redact_output(value: str) -> str:
|
||||
value = re.sub(
|
||||
r"(?i)(Authorization:\s*(?:Bearer|token)\s+)"
|
||||
r"[A-Za-z0-9._~+/=-]{8,}",
|
||||
r"\1<redacted>",
|
||||
str(value),
|
||||
)
|
||||
value = re.sub(
|
||||
r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----.*?"
|
||||
r"-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----",
|
||||
"<redacted-private-key>",
|
||||
value,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def git(git_dir: Path, *arguments: str) -> str:
|
||||
completed = subprocess.run(
|
||||
["git", f"--git-dir={git_dir}", *arguments],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
return completed.stdout.strip()
|
||||
|
||||
|
||||
def run_evidence(
|
||||
command: list[str],
|
||||
*,
|
||||
cwd: Path,
|
||||
output: Path,
|
||||
timeout: int,
|
||||
) -> dict[str, Any]:
|
||||
completed = subprocess.run(
|
||||
command,
|
||||
cwd=cwd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
env={
|
||||
"HOME": str(cwd / ".builder-home"),
|
||||
"PATH": os.environ.get("PATH", ""),
|
||||
"PYTHONDONTWRITEBYTECODE": "1",
|
||||
},
|
||||
)
|
||||
record = {
|
||||
"command": command,
|
||||
"exit_code": completed.returncode,
|
||||
"stderr": redact_output(completed.stderr[-20000:]),
|
||||
"stdout": redact_output(completed.stdout[-20000:]),
|
||||
}
|
||||
atomic_json(output, record)
|
||||
if completed.returncode != 0:
|
||||
raise RuntimeError(f"trusted builder command failed: {command[0]}")
|
||||
return record
|
||||
|
||||
|
||||
def secret_scan(root: Path) -> dict[str, Any]:
|
||||
findings: list[str] = []
|
||||
for path in sorted(item for item in root.rglob("*") if item.is_file()):
|
||||
if path.stat().st_size > 5 * 1024 * 1024:
|
||||
continue
|
||||
data = path.read_bytes()
|
||||
authorization_tokens = AUTHORIZATION_PATTERN.findall(data)
|
||||
unsafe_authorization = any(
|
||||
hashlib.sha256(token.lower()).hexdigest()
|
||||
not in ALLOWED_AUTHORIZATION_PLACEHOLDER_SHA256
|
||||
for token in authorization_tokens
|
||||
)
|
||||
if unsafe_authorization or any(
|
||||
pattern.search(data) for pattern in SECRET_PATTERNS
|
||||
):
|
||||
findings.append(path.relative_to(root).as_posix())
|
||||
return {"findings": findings, "passed": not findings}
|
||||
|
||||
|
||||
def build(git_dir: Path, commit: str, output: Path) -> dict[str, Any]:
|
||||
commit = git(git_dir, "rev-parse", f"{commit}^{{commit}}")
|
||||
if not OID_RE.fullmatch(commit):
|
||||
raise ValueError("commit must resolve to a full OID")
|
||||
tree_oid = git(git_dir, "rev-parse", f"{commit}^{{tree}}")
|
||||
if output.exists():
|
||||
raise FileExistsError("artifact output already exists")
|
||||
output.mkdir(parents=True, mode=0o700)
|
||||
source_tar = output / "source.tar"
|
||||
subprocess.run(
|
||||
[
|
||||
"git",
|
||||
f"--git-dir={git_dir}",
|
||||
"archive",
|
||||
"--format=tar",
|
||||
f"--output={source_tar}",
|
||||
commit,
|
||||
],
|
||||
check=True,
|
||||
timeout=120,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
canary = output / "canary-src"
|
||||
canary.mkdir(mode=0o700)
|
||||
with tarfile.open(source_tar, "r") as archive:
|
||||
archive.extractall(canary, filter="data")
|
||||
if any(path.name == ".git" for path in canary.rglob(".git")):
|
||||
raise RuntimeError("no-.git canary contains Git metadata")
|
||||
|
||||
test_record = run_evidence(
|
||||
["python3", "-m", "pytest", "-q", *TARGET_TESTS],
|
||||
cwd=canary,
|
||||
output=output / "test-evidence.json",
|
||||
timeout=600,
|
||||
)
|
||||
fixture_record = run_evidence(
|
||||
["bash", "tests/run_live_gitea_fixture.sh"],
|
||||
cwd=canary,
|
||||
output=output / "live-fixture-evidence.json",
|
||||
timeout=600,
|
||||
)
|
||||
canary_record = {
|
||||
"git_directory_present": False,
|
||||
"live_fixture_exit_code": fixture_record["exit_code"],
|
||||
"passed": True,
|
||||
"target_test_exit_code": test_record["exit_code"],
|
||||
}
|
||||
atomic_json(output / "canary-evidence.json", canary_record)
|
||||
|
||||
scan = secret_scan(canary)
|
||||
atomic_json(output / "secret-scan.json", scan)
|
||||
if not scan["passed"]:
|
||||
raise RuntimeError("secret scan failed")
|
||||
|
||||
manifest_files: list[dict[str, Any]] = []
|
||||
tracked = git(git_dir, "ls-tree", "-r", "--name-only", commit).splitlines()
|
||||
for relative in tracked:
|
||||
path = canary / relative
|
||||
if path.is_file():
|
||||
manifest_files.append(
|
||||
{
|
||||
"path": relative,
|
||||
"sha256": sha256(path),
|
||||
"size": path.stat().st_size,
|
||||
}
|
||||
)
|
||||
manifest = {
|
||||
"commit": commit,
|
||||
"file_count": len(manifest_files),
|
||||
"files": manifest_files,
|
||||
"tree_oid": tree_oid,
|
||||
}
|
||||
atomic_json(output / "manifest.json", manifest)
|
||||
shutil.copyfile(source_tar, output / "rollback-source.tar")
|
||||
|
||||
closure = {
|
||||
"artifact_sha256": sha256(source_tar),
|
||||
"canary_evidence_sha256": sha256(output / "canary-evidence.json"),
|
||||
"commit": commit,
|
||||
"live_fixture_evidence_sha256": sha256(
|
||||
output / "live-fixture-evidence.json"
|
||||
),
|
||||
"manifest_sha256": sha256(output / "manifest.json"),
|
||||
"no_git_canary": True,
|
||||
"rollback_artifact_sha256": sha256(
|
||||
output / "rollback-source.tar"
|
||||
),
|
||||
"secret_scan_sha256": sha256(output / "secret-scan.json"),
|
||||
"test_evidence_sha256": sha256(output / "test-evidence.json"),
|
||||
"tree_oid": tree_oid,
|
||||
}
|
||||
atomic_json(output / "package-closure.json", closure)
|
||||
closure["package_closure_sha256"] = sha256(
|
||||
output / "package-closure.json"
|
||||
)
|
||||
checksummed = sorted(
|
||||
path
|
||||
for path in output.iterdir()
|
||||
if path.is_file() and path.name != "checksums.sha256"
|
||||
)
|
||||
with (output / "checksums.sha256").open(
|
||||
"w", encoding="utf-8", newline="\n"
|
||||
) as checksums:
|
||||
for path in checksummed:
|
||||
checksums.write(f"{sha256(path)} {path.name}\n")
|
||||
return closure
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--git-dir", required=True, type=Path)
|
||||
parser.add_argument("--commit", required=True)
|
||||
parser.add_argument("--output", required=True, type=Path)
|
||||
arguments = parser.parse_args()
|
||||
result = build(
|
||||
arguments.git_dir.resolve(),
|
||||
arguments.commit,
|
||||
arguments.output.resolve(),
|
||||
)
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,470 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run two zero-side-effect Silent Release candidates in Shadow mode."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import dataclasses
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
|
||||
Ed25519PrivateKey,
|
||||
)
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
|
||||
|
||||
from silent_release_controller import ReleaseCandidate, ReleaseState # noqa: E402
|
||||
from silent_release_execution import ( # noqa: E402
|
||||
LocalTestApprovalSigner,
|
||||
LocalTestBuilderSigner,
|
||||
PersistentSilentReleaseController,
|
||||
SignedTrustedBuilderAdapter,
|
||||
)
|
||||
from silent_release_hardening import ( # noqa: E402
|
||||
ApprovalKeyRing,
|
||||
ApprovalRevocationStore,
|
||||
BuilderAttestationVerifier,
|
||||
DeploymentPlan,
|
||||
FinalizationPlan,
|
||||
ReleasePlan,
|
||||
SignedApprovalVerifier,
|
||||
SignedReleasePlanApproval,
|
||||
SilentReleaseActionService,
|
||||
TagPlan,
|
||||
TagPublicationProbeEvidence,
|
||||
TrustedCandidateBundle,
|
||||
TrustedReleasePolicy,
|
||||
classify_tag_publication,
|
||||
)
|
||||
|
||||
|
||||
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 write_json(path: Path, value: object) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = path.with_suffix(path.suffix + ".tmp")
|
||||
temporary.write_text(
|
||||
json.dumps(value, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
temporary.replace(path)
|
||||
|
||||
|
||||
def oid(seed: str) -> str:
|
||||
return hashlib.sha1(seed.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def candidate_id(event_id: str, source_commit: str) -> str:
|
||||
return "rc-shadow-" + hashlib.sha256(
|
||||
f"{event_id}\0{source_commit}".encode("utf-8")
|
||||
).hexdigest()[:20]
|
||||
|
||||
|
||||
def make_plan(arguments: argparse.Namespace, candidate: str) -> ReleasePlan:
|
||||
version = "shadow-" + candidate.removeprefix("rc-shadow-")
|
||||
tag_oid = oid("tag:" + candidate)
|
||||
approval_plan_id = "shadow-plan-" + candidate.removeprefix("rc-shadow-")
|
||||
release_path = str(Path(arguments.release_path))
|
||||
previous_baseline = arguments.baseline_before_sha256
|
||||
return ReleasePlan(
|
||||
deployment=DeploymentPlan(
|
||||
service="aios-silent-release-controller",
|
||||
release_path=release_path,
|
||||
release_root=str(Path(release_path).parent.parent),
|
||||
cutover_link=str(Path(release_path).parent.parent / "current"),
|
||||
dropin_path=str(
|
||||
Path(release_path)
|
||||
/ "deploy"
|
||||
/ "systemd"
|
||||
/ "aios-silent-release-controller.service"
|
||||
),
|
||||
dropin_sha256=arguments.systemd_unit_sha256,
|
||||
systemd_unit_sha256=arguments.systemd_unit_sha256,
|
||||
rollback_path=str(Path(arguments.rollback_artifact)),
|
||||
rollback_sha256=arguments.artifact_sha256,
|
||||
source_commit=arguments.source_commit,
|
||||
tree_oid=arguments.tree_oid,
|
||||
artifact_sha256=arguments.artifact_sha256,
|
||||
manifest_sha256=arguments.manifest_sha256,
|
||||
package_closure_sha256=arguments.package_closure_sha256,
|
||||
authoritative_git_dir=str(Path(arguments.repository)),
|
||||
repository_identity=arguments.repository_identity,
|
||||
writer_expected=False,
|
||||
baseline_before_sha256=previous_baseline,
|
||||
registry_before_sha256=arguments.registry_before_sha256,
|
||||
production_tag_refset_before_sha256=(
|
||||
arguments.tag_refset_before_sha256
|
||||
),
|
||||
health_contract_version="shadow-health/v1",
|
||||
timeout_policy={"health_seconds": 10},
|
||||
retry_policy={"health_attempts": 1},
|
||||
policy_sha256=arguments.policy_sha256,
|
||||
health_contract={
|
||||
"execution_enabled": False,
|
||||
"shadow_mode": True,
|
||||
"side_effects_executed": False,
|
||||
},
|
||||
),
|
||||
tag=TagPlan(
|
||||
version=version,
|
||||
tag_type="annotated",
|
||||
expected_tag_oid=tag_oid,
|
||||
expected_target_oid=arguments.source_commit,
|
||||
evidence_closure_oid=arguments.source_commit,
|
||||
approval_plan_id=approval_plan_id,
|
||||
repository_identity=arguments.repository_identity,
|
||||
gitea_owner="aios",
|
||||
gitea_repository="aios",
|
||||
current_remote_tag_oid=None,
|
||||
remote_absent_evidence_sha256=hashlib.sha256(
|
||||
f"shadow-only:{candidate}".encode("utf-8")
|
||||
).hexdigest(),
|
||||
verification_contract="shadow-no-tag-write/v1",
|
||||
),
|
||||
finalization=FinalizationPlan(
|
||||
version=version,
|
||||
expected_tag_oid=tag_oid,
|
||||
expected_target_oid=arguments.source_commit,
|
||||
expected_previous_baseline_sha256=previous_baseline,
|
||||
registry_expected_record={
|
||||
"shadow_only": True,
|
||||
"target_oid": arguments.source_commit,
|
||||
},
|
||||
baseline_expected_record={
|
||||
"shadow_only": True,
|
||||
"target_oid": arguments.source_commit,
|
||||
},
|
||||
final_consistency_contract="shadow-no-finalization/v1",
|
||||
),
|
||||
policy_sha256=arguments.policy_sha256,
|
||||
)
|
||||
|
||||
|
||||
def make_candidate(
|
||||
arguments: argparse.Namespace, event_id: str
|
||||
) -> ReleaseCandidate:
|
||||
return ReleaseCandidate(
|
||||
candidate_id=candidate_id(event_id, arguments.source_commit),
|
||||
source_commit=arguments.source_commit,
|
||||
tree_oid=arguments.tree_oid,
|
||||
artifact_sha256=arguments.artifact_sha256,
|
||||
manifest_sha256=arguments.manifest_sha256,
|
||||
tests={"passed": True, "source": "trusted-artifact-evidence"},
|
||||
canary={"passed": True, "without_git": True},
|
||||
rollback_ready=True,
|
||||
git_consistency={"consistent": True},
|
||||
gitea_consistency={"consistent": True},
|
||||
approval_state="PENDING",
|
||||
state=ReleaseState.READY_FOR_APPROVAL,
|
||||
)
|
||||
|
||||
|
||||
def sign_approval(
|
||||
service: SilentReleaseActionService,
|
||||
signer: LocalTestApprovalSigner,
|
||||
candidate: str,
|
||||
nonce_suffix: str,
|
||||
) -> SignedReleasePlanApproval:
|
||||
release_candidate = service.candidates.load(candidate)
|
||||
plan = service.release_plans.load(candidate)
|
||||
now = datetime.now(timezone.utc)
|
||||
unsigned = SignedReleasePlanApproval(
|
||||
release_plan_id="shadow-release-" + candidate.removeprefix("rc-"),
|
||||
candidate_id=candidate,
|
||||
source_commit=release_candidate.source_commit,
|
||||
tree_oid=release_candidate.tree_oid,
|
||||
artifact_sha256=release_candidate.artifact_sha256,
|
||||
manifest_sha256=release_candidate.manifest_sha256,
|
||||
deployment_plan_sha256=plan.deployment.sha256,
|
||||
release_plan_sha256=plan.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=oid("shadow-owner:" + candidate),
|
||||
capabilities=(
|
||||
"runtime_switch",
|
||||
"tag_publish",
|
||||
"baseline_registry_finalize",
|
||||
),
|
||||
owner_identity="owner:shadow-acceptance-test",
|
||||
trust_domain="aios.shadow",
|
||||
issued_at=now.isoformat().replace("+00:00", "Z"),
|
||||
not_before=now.isoformat().replace("+00:00", "Z"),
|
||||
expires_at=(now + timedelta(minutes=10))
|
||||
.isoformat()
|
||||
.replace("+00:00", "Z"),
|
||||
nonce="shadow-nonce-" + nonce_suffix,
|
||||
key_id=signer.key_id,
|
||||
signature="",
|
||||
)
|
||||
return signer.sign(unsigned)
|
||||
|
||||
|
||||
def tag_probe_results(plan: TagPlan) -> dict[str, str]:
|
||||
exact = TagPublicationProbeEvidence(
|
||||
repository_identity=plan.repository_identity,
|
||||
remote_reachable=True,
|
||||
gitea_api_reachable=True,
|
||||
gitea_listing_reachable=True,
|
||||
remote_tag_oid=plan.expected_tag_oid,
|
||||
annotated_tag_object_oid=plan.expected_tag_oid,
|
||||
remote_peeled_target_oid=plan.expected_target_oid,
|
||||
gitea_api_tag_oid=plan.expected_tag_oid,
|
||||
gitea_api_peeled_target_oid=plan.expected_target_oid,
|
||||
gitea_listing_visible=True,
|
||||
gitea_listing_tag_oid=plan.expected_tag_oid,
|
||||
gitea_listing_peeled_target_oid=plan.expected_target_oid,
|
||||
observed_at=1,
|
||||
)
|
||||
absent = dataclasses.replace(
|
||||
exact,
|
||||
remote_tag_oid=None,
|
||||
annotated_tag_object_oid=None,
|
||||
remote_peeled_target_oid=None,
|
||||
gitea_api_tag_oid=None,
|
||||
gitea_api_peeled_target_oid=None,
|
||||
gitea_listing_visible=False,
|
||||
gitea_listing_tag_oid=None,
|
||||
gitea_listing_peeled_target_oid=None,
|
||||
)
|
||||
indeterminate = dataclasses.replace(exact, gitea_api_reachable=False)
|
||||
conflict = dataclasses.replace(exact, remote_tag_oid=oid("conflict"))
|
||||
return {
|
||||
"APPLIED_VERIFIED": classify_tag_publication(exact, plan).value,
|
||||
"CONFLICT": classify_tag_publication(conflict, plan).value,
|
||||
"INDETERMINATE": classify_tag_publication(
|
||||
indeterminate, plan
|
||||
).value,
|
||||
"NOT_APPLIED": classify_tag_publication(absent, plan).value,
|
||||
}
|
||||
|
||||
|
||||
def parser() -> argparse.ArgumentParser:
|
||||
value = argparse.ArgumentParser()
|
||||
value.add_argument("--state-root", required=True, type=Path)
|
||||
value.add_argument("--repository", required=True, type=Path)
|
||||
value.add_argument("--release-path", required=True, type=Path)
|
||||
value.add_argument("--rollback-artifact", required=True, type=Path)
|
||||
value.add_argument("--source-commit", required=True)
|
||||
value.add_argument("--tree-oid", required=True)
|
||||
value.add_argument("--artifact-sha256", required=True)
|
||||
value.add_argument("--manifest-sha256", required=True)
|
||||
value.add_argument("--package-closure-sha256", required=True)
|
||||
value.add_argument("--policy-sha256", required=True)
|
||||
value.add_argument("--systemd-unit-sha256", required=True)
|
||||
value.add_argument("--baseline-before-sha256", required=True)
|
||||
value.add_argument("--registry-before-sha256", required=True)
|
||||
value.add_argument("--tag-refset-before-sha256", required=True)
|
||||
value.add_argument("--repository-identity", default="aios/aios")
|
||||
value.add_argument("--output", required=True, type=Path)
|
||||
return value
|
||||
|
||||
|
||||
def main() -> None:
|
||||
arguments = parser().parse_args()
|
||||
if sha256_file(arguments.rollback_artifact) != arguments.artifact_sha256:
|
||||
raise SystemExit("rollback artifact hash mismatch")
|
||||
unit_path = (
|
||||
arguments.release_path
|
||||
/ "deploy"
|
||||
/ "systemd"
|
||||
/ "aios-silent-release-controller.service"
|
||||
)
|
||||
if sha256_file(unit_path) != arguments.systemd_unit_sha256:
|
||||
raise SystemExit("systemd unit hash mismatch")
|
||||
|
||||
builder_signer = LocalTestBuilderSigner.generate()
|
||||
builder_public = builder_signer.public_key.public_bytes(
|
||||
serialization.Encoding.Raw,
|
||||
serialization.PublicFormat.Raw,
|
||||
)
|
||||
owner_private = Ed25519PrivateKey.generate()
|
||||
owner_public = owner_private.public_key().public_bytes(
|
||||
serialization.Encoding.Raw,
|
||||
serialization.PublicFormat.Raw,
|
||||
)
|
||||
owner_signer = LocalTestApprovalSigner(
|
||||
"shadow-owner-ephemeral", owner_private
|
||||
)
|
||||
|
||||
def collector(intent):
|
||||
event_id = str(intent["event_id"])
|
||||
candidate = make_candidate(arguments, event_id)
|
||||
return TrustedCandidateBundle(
|
||||
candidate=candidate,
|
||||
release_plan=make_plan(arguments, candidate.candidate_id),
|
||||
checks={
|
||||
"authoritative_git": True,
|
||||
"artifact": True,
|
||||
"canary": True,
|
||||
"gitea": True,
|
||||
"manifest": True,
|
||||
"package_closure": True,
|
||||
"rollback": True,
|
||||
"secret_scan": True,
|
||||
"tests": True,
|
||||
},
|
||||
)
|
||||
|
||||
release_root = str(arguments.release_path.parent.parent)
|
||||
dropin = str(
|
||||
arguments.release_path
|
||||
/ "deploy"
|
||||
/ "systemd"
|
||||
/ "aios-silent-release-controller.service"
|
||||
)
|
||||
policy = TrustedReleasePolicy(
|
||||
services=frozenset({"aios-silent-release-controller"}),
|
||||
release_roots=frozenset({release_root}),
|
||||
cutover_links=frozenset({str(Path(release_root) / "current")}),
|
||||
dropin_paths=frozenset({dropin}),
|
||||
authoritative_git_dirs=frozenset({str(arguments.repository)}),
|
||||
repository_identities=frozenset(
|
||||
{arguments.repository_identity}
|
||||
),
|
||||
)
|
||||
verifier = SignedApprovalVerifier(
|
||||
ApprovalKeyRing({"shadow-owner-ephemeral": owner_public}),
|
||||
ApprovalRevocationStore(arguments.state_root / "revocations"),
|
||||
allowed_trust_domains={"aios.shadow"},
|
||||
)
|
||||
service = SilentReleaseActionService(
|
||||
arguments.state_root,
|
||||
execution_enabled=False,
|
||||
approval_verifier=verifier,
|
||||
candidate_builder=SignedTrustedBuilderAdapter(
|
||||
collector, builder_signer
|
||||
),
|
||||
builder_attestation_verifier=BuilderAttestationVerifier(
|
||||
{builder_signer.key_id: builder_public}
|
||||
),
|
||||
trusted_policy=policy,
|
||||
expected_repository=arguments.repository_identity,
|
||||
policy_sha256=arguments.policy_sha256,
|
||||
)
|
||||
|
||||
first_event = "main-" + arguments.source_commit
|
||||
first = service.create_release_intent(
|
||||
{
|
||||
"event_id": first_event,
|
||||
"source_commit": arguments.source_commit,
|
||||
}
|
||||
)
|
||||
first_duplicate = service.create_release_intent(
|
||||
{
|
||||
"event_id": first_event,
|
||||
"source_commit": arguments.source_commit,
|
||||
}
|
||||
)
|
||||
first_notification = service.ensure_owner_notification(
|
||||
first["candidate_id"]
|
||||
)
|
||||
repeated_notification = service.ensure_owner_notification(
|
||||
first["candidate_id"]
|
||||
)
|
||||
first_approval = sign_approval(
|
||||
service, owner_signer, first["candidate_id"], "candidate-one"
|
||||
)
|
||||
first_submit = service.submit_owner_approval(
|
||||
first_approval.to_dict()
|
||||
)
|
||||
first_submit_repeated = service.submit_owner_approval(
|
||||
first_approval.to_dict()
|
||||
)
|
||||
first_start = service.start_deployment(first["candidate_id"])
|
||||
|
||||
second = service.create_release_intent(
|
||||
{
|
||||
"event_id": "shadow-indeterminate-" + arguments.source_commit,
|
||||
"source_commit": arguments.source_commit,
|
||||
}
|
||||
)
|
||||
second_plan = service.release_plans.load(second["candidate_id"])
|
||||
probe_results = tag_probe_results(second_plan.tag)
|
||||
|
||||
first_controller = PersistentSilentReleaseController(
|
||||
arguments.state_root,
|
||||
service,
|
||||
main_probe=lambda: arguments.source_commit,
|
||||
)
|
||||
controller_first = first_controller.run_once()
|
||||
controller_restarted = PersistentSilentReleaseController(
|
||||
arguments.state_root,
|
||||
service,
|
||||
main_probe=lambda: arguments.source_commit,
|
||||
).run_once()
|
||||
|
||||
report = {
|
||||
"approval_backend": "ephemeral-shadow-test-only",
|
||||
"artifact_sha256": arguments.artifact_sha256,
|
||||
"builder_attestation": "VERIFIED",
|
||||
"candidate_count": 2,
|
||||
"candidates": [
|
||||
{
|
||||
"candidate_id": first["candidate_id"],
|
||||
"duplicate_intent_idempotent": first_duplicate["idempotent"],
|
||||
"notification_idempotent": (
|
||||
repeated_notification["idempotent"]
|
||||
),
|
||||
"notification_initial_idempotent": (
|
||||
first_notification["idempotent"]
|
||||
),
|
||||
"replayed_approval_status": (
|
||||
first_submit_repeated["status"]
|
||||
),
|
||||
"shadow_start": first_start,
|
||||
"signed_approval_status": first_submit["status"],
|
||||
},
|
||||
{
|
||||
"candidate_id": second["candidate_id"],
|
||||
"tag_publication_probe": "INDETERMINATE",
|
||||
},
|
||||
],
|
||||
"controller_restart_idempotent": (
|
||||
controller_restarted["intent"]["idempotent"]
|
||||
),
|
||||
"controller_restart_notification_idempotent": (
|
||||
controller_restarted["notification"]["idempotent"]
|
||||
),
|
||||
"controller_status": controller_first["startup"]["status"],
|
||||
"execution_enabled": False,
|
||||
"manifest_sha256": arguments.manifest_sha256,
|
||||
"package_closure_sha256": arguments.package_closure_sha256,
|
||||
"private_key_files_written": False,
|
||||
"production_tag": "NOT_EXECUTED",
|
||||
"repository_identity": arguments.repository_identity,
|
||||
"shadow_mode": True,
|
||||
"side_effects_executed": False,
|
||||
"source_commit": arguments.source_commit,
|
||||
"tag_probe_outcomes": probe_results,
|
||||
"tree_oid": arguments.tree_oid,
|
||||
}
|
||||
write_json(arguments.output, report)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"candidate_count": 2,
|
||||
"execution_enabled": False,
|
||||
"output": str(arguments.output),
|
||||
"shadow_mode": True,
|
||||
"side_effects_executed": False,
|
||||
"status": "SHADOW_ACCEPTANCE_PASSED",
|
||||
},
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,728 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import json
|
||||
import sys
|
||||
import threading
|
||||
import urllib.request
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from http.server import ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
|
||||
Ed25519PrivateKey,
|
||||
)
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
|
||||
from silent_release_hardening import ( # noqa: E402
|
||||
CapabilityAuthorizationError,
|
||||
CapabilityController,
|
||||
CapabilityLedger,
|
||||
ClaimProbeResult,
|
||||
ApprovalKeyRing,
|
||||
ApprovalRevocationStore,
|
||||
BuilderAttestationVerifier,
|
||||
DeploymentPlan,
|
||||
FinalizationPlan,
|
||||
ProbeEvidence,
|
||||
ReleasePlan,
|
||||
SignedApprovalVerifier,
|
||||
SignedReleasePlanApproval,
|
||||
SilentReleaseActionService,
|
||||
TagPlan,
|
||||
TagPublicationProbeEvidence,
|
||||
TrustedCandidateBundle,
|
||||
TrustedReleasePolicy,
|
||||
classify_tag_publication,
|
||||
)
|
||||
from silent_release_controller import ReleaseCandidate, ReleaseState # noqa: E402
|
||||
from silent_release_execution import ( # noqa: E402
|
||||
LocalTestApprovalSigner,
|
||||
LocalTestBuilderSigner,
|
||||
PersistentSilentReleaseController,
|
||||
ShadowActionHandler,
|
||||
SignedTrustedBuilderAdapter,
|
||||
UnavailableProductionApprovalBackend,
|
||||
)
|
||||
from build_silent_release_artifact import ( # noqa: E402
|
||||
redact_output,
|
||||
secret_scan,
|
||||
)
|
||||
|
||||
|
||||
COMMIT = "1" * 40
|
||||
TAG_OID = "2" * 40
|
||||
|
||||
|
||||
def approval() -> SignedReleasePlanApproval:
|
||||
return SignedReleasePlanApproval(
|
||||
release_plan_id="release-plan-readiness-001",
|
||||
candidate_id="rc-readiness-001",
|
||||
source_commit=COMMIT,
|
||||
tree_oid="3" * 40,
|
||||
artifact_sha256="4" * 64,
|
||||
manifest_sha256="5" * 64,
|
||||
deployment_plan_sha256="6" * 64,
|
||||
policy_sha256="7" * 64,
|
||||
approval_plan_id="owner-plan-readiness-001",
|
||||
evidence_closure_oid="8" * 40,
|
||||
owner_approval_record_oid="9" * 40,
|
||||
capabilities=(
|
||||
"runtime_switch",
|
||||
"tag_publish",
|
||||
"baseline_registry_finalize",
|
||||
),
|
||||
expires_at="2099-01-01T00:00:00Z",
|
||||
nonce="nonce-readiness-000000001",
|
||||
key_id="owner-key-readiness-1",
|
||||
signature="test-signature",
|
||||
)
|
||||
|
||||
|
||||
def tag_plan() -> TagPlan:
|
||||
return TagPlan(
|
||||
version="shadow-readiness-001",
|
||||
tag_type="annotated",
|
||||
expected_tag_oid=TAG_OID,
|
||||
expected_target_oid=COMMIT,
|
||||
evidence_closure_oid="8" * 40,
|
||||
approval_plan_id="owner-plan-readiness-001",
|
||||
repository_identity="aios/aios",
|
||||
gitea_owner="aios",
|
||||
gitea_repository="aios",
|
||||
current_remote_tag_oid=None,
|
||||
remote_absent_evidence_sha256="a" * 64,
|
||||
verification_contract="git+api+listing/v1",
|
||||
)
|
||||
|
||||
|
||||
def make_failed_claim(
|
||||
ledger: CapabilityLedger,
|
||||
*,
|
||||
operation: str,
|
||||
) -> dict:
|
||||
claimed = ledger.claim(
|
||||
approval(),
|
||||
"runtime_switch",
|
||||
operation=operation,
|
||||
executor_id="worker-initial",
|
||||
lease_seconds=10,
|
||||
now=100,
|
||||
)
|
||||
ledger.fail_recoverable(
|
||||
approval(),
|
||||
"runtime_switch",
|
||||
operation=operation,
|
||||
claim_id=claimed["claim_id"],
|
||||
reason="adapter_error",
|
||||
)
|
||||
return claimed
|
||||
|
||||
|
||||
def test_failed_recoverable_not_applied_retries_once(tmp_path):
|
||||
ledger = CapabilityLedger(
|
||||
tmp_path / "ledger", maximum_probe_age_seconds=30
|
||||
)
|
||||
make_failed_claim(ledger, operation="runtime-retry")
|
||||
calls: list[str] = []
|
||||
controller = CapabilityController(
|
||||
capability="runtime_switch",
|
||||
ledger=ledger,
|
||||
adapter=lambda: calls.append("adapter") or {"verified": True},
|
||||
recovery_probe=lambda: ClaimProbeResult.NOT_APPLIED,
|
||||
executor_id="worker-recovery",
|
||||
)
|
||||
|
||||
assert controller.execute(
|
||||
approval(), operation="runtime-retry"
|
||||
)["status"] == "EXECUTED"
|
||||
assert controller.execute(
|
||||
approval(), operation="runtime-retry"
|
||||
)["status"] == "IDEMPOTENT"
|
||||
assert calls == ["adapter"]
|
||||
|
||||
|
||||
def test_failed_recoverable_applied_verified_skips_adapter(tmp_path):
|
||||
ledger = CapabilityLedger(tmp_path / "ledger")
|
||||
make_failed_claim(ledger, operation="runtime-applied")
|
||||
calls: list[str] = []
|
||||
controller = CapabilityController(
|
||||
capability="runtime_switch",
|
||||
ledger=ledger,
|
||||
adapter=lambda: calls.append("adapter") or {"verified": True},
|
||||
recovery_probe=lambda: ClaimProbeResult.APPLIED_VERIFIED,
|
||||
executor_id="worker-recovery",
|
||||
)
|
||||
|
||||
assert controller.execute(
|
||||
approval(), operation="runtime-applied"
|
||||
)["status"] == "IDEMPOTENT"
|
||||
assert calls == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("probe", "expected"),
|
||||
(
|
||||
(
|
||||
ClaimProbeResult.INDETERMINATE,
|
||||
"RECONCILIATION_REQUIRED",
|
||||
),
|
||||
(
|
||||
ClaimProbeResult.CONFLICT,
|
||||
"MANUAL_OWNER_REVIEW_REQUIRED",
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_failed_recoverable_unknown_or_conflict_stops_writes(
|
||||
tmp_path, probe, expected
|
||||
):
|
||||
ledger = CapabilityLedger(tmp_path / "ledger")
|
||||
make_failed_claim(ledger, operation=f"runtime-{probe.value.lower()}")
|
||||
calls: list[str] = []
|
||||
controller = CapabilityController(
|
||||
capability="runtime_switch",
|
||||
ledger=ledger,
|
||||
adapter=lambda: calls.append("adapter") or {"verified": True},
|
||||
recovery_probe=lambda: probe,
|
||||
executor_id="worker-recovery",
|
||||
)
|
||||
|
||||
assert controller.execute(
|
||||
approval(), operation=f"runtime-{probe.value.lower()}"
|
||||
)["status"] == expected
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_probe_must_bind_operation_and_be_fresh(tmp_path):
|
||||
ledger = CapabilityLedger(
|
||||
tmp_path / "ledger", maximum_probe_age_seconds=10
|
||||
)
|
||||
make_failed_claim(ledger, operation="runtime-freshness")
|
||||
record = ledger.read(
|
||||
approval(), "runtime_switch", operation="runtime-freshness"
|
||||
)
|
||||
assert record is not None
|
||||
|
||||
with pytest.raises(
|
||||
CapabilityAuthorizationError, match="operation digest"
|
||||
):
|
||||
ledger.claim(
|
||||
approval(),
|
||||
"runtime_switch",
|
||||
operation="runtime-freshness",
|
||||
executor_id="worker-recovery",
|
||||
lease_seconds=10,
|
||||
now=120,
|
||||
side_effect_probe=lambda: ProbeEvidence(
|
||||
result=ClaimProbeResult.NOT_APPLIED,
|
||||
operation_digest="0" * 64,
|
||||
observed_at=120,
|
||||
),
|
||||
)
|
||||
|
||||
with pytest.raises(CapabilityAuthorizationError, match="freshness"):
|
||||
ledger.claim(
|
||||
approval(),
|
||||
"runtime_switch",
|
||||
operation="runtime-freshness",
|
||||
executor_id="worker-recovery",
|
||||
lease_seconds=10,
|
||||
now=120,
|
||||
side_effect_probe=lambda: ProbeEvidence(
|
||||
result=ClaimProbeResult.NOT_APPLIED,
|
||||
operation_digest=record["operation_digest"],
|
||||
observed_at=100,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_probe_journal_is_hash_chained_and_tamper_fails_closed(tmp_path):
|
||||
ledger = CapabilityLedger(tmp_path / "ledger")
|
||||
make_failed_claim(ledger, operation="runtime-journal")
|
||||
ledger.claim(
|
||||
approval(),
|
||||
"runtime_switch",
|
||||
operation="runtime-journal",
|
||||
executor_id="worker-recovery",
|
||||
lease_seconds=10,
|
||||
now=101,
|
||||
side_effect_probe=lambda: ClaimProbeResult.INDETERMINATE,
|
||||
)
|
||||
operation_path = ledger._operation_path(
|
||||
approval(), "runtime_switch", "runtime-journal"
|
||||
)
|
||||
records = ledger.verify_probe_journal(operation_path)
|
||||
assert records[0]["payload"]["result"] == "INDETERMINATE"
|
||||
journal = ledger._probe_journal_path(operation_path)
|
||||
payload = json.loads(journal.read_text(encoding="utf-8"))
|
||||
payload["payload"]["result"] = "NOT_APPLIED"
|
||||
journal.write_text(json.dumps(payload) + "\n", encoding="utf-8")
|
||||
with pytest.raises(Exception, match="hash chain"):
|
||||
ledger.verify_probe_journal(operation_path)
|
||||
|
||||
|
||||
def tag_evidence(**overrides) -> TagPublicationProbeEvidence:
|
||||
values = {
|
||||
"repository_identity": "aios/aios",
|
||||
"remote_reachable": True,
|
||||
"gitea_api_reachable": True,
|
||||
"gitea_listing_reachable": True,
|
||||
"remote_tag_oid": TAG_OID,
|
||||
"annotated_tag_object_oid": TAG_OID,
|
||||
"remote_peeled_target_oid": COMMIT,
|
||||
"gitea_api_tag_oid": TAG_OID,
|
||||
"gitea_api_peeled_target_oid": COMMIT,
|
||||
"gitea_listing_visible": True,
|
||||
"gitea_listing_tag_oid": TAG_OID,
|
||||
"gitea_listing_peeled_target_oid": COMMIT,
|
||||
"observed_at": 100,
|
||||
}
|
||||
values.update(overrides)
|
||||
return TagPublicationProbeEvidence(**values)
|
||||
|
||||
|
||||
def test_tag_probe_exact_absence_is_not_applied():
|
||||
evidence = tag_evidence(
|
||||
remote_tag_oid=None,
|
||||
annotated_tag_object_oid=None,
|
||||
remote_peeled_target_oid=None,
|
||||
gitea_api_tag_oid=None,
|
||||
gitea_api_peeled_target_oid=None,
|
||||
gitea_listing_visible=False,
|
||||
gitea_listing_tag_oid=None,
|
||||
gitea_listing_peeled_target_oid=None,
|
||||
)
|
||||
assert (
|
||||
classify_tag_publication(evidence, tag_plan())
|
||||
== ClaimProbeResult.NOT_APPLIED
|
||||
)
|
||||
|
||||
|
||||
def test_tag_probe_exact_publication_is_applied_verified():
|
||||
assert (
|
||||
classify_tag_publication(tag_evidence(), tag_plan())
|
||||
== ClaimProbeResult.APPLIED_VERIFIED
|
||||
)
|
||||
|
||||
|
||||
def test_tag_probe_unavailable_or_divergent_is_indeterminate():
|
||||
assert (
|
||||
classify_tag_publication(
|
||||
tag_evidence(gitea_api_reachable=False), tag_plan()
|
||||
)
|
||||
== ClaimProbeResult.INDETERMINATE
|
||||
)
|
||||
assert (
|
||||
classify_tag_publication(
|
||||
tag_evidence(gitea_listing_tag_oid=None), tag_plan()
|
||||
)
|
||||
== ClaimProbeResult.INDETERMINATE
|
||||
)
|
||||
|
||||
|
||||
def test_tag_probe_same_name_different_oid_is_conflict():
|
||||
assert (
|
||||
classify_tag_publication(
|
||||
tag_evidence(remote_tag_oid="f" * 40), tag_plan()
|
||||
)
|
||||
== ClaimProbeResult.CONFLICT
|
||||
)
|
||||
assert (
|
||||
classify_tag_publication(
|
||||
tag_evidence(remote_peeled_target_oid="e" * 40), tag_plan()
|
||||
)
|
||||
== ClaimProbeResult.CONFLICT
|
||||
)
|
||||
|
||||
|
||||
def readiness_release_plan(source_commit: str = COMMIT) -> ReleasePlan:
|
||||
policy_sha = "7" * 64
|
||||
return ReleasePlan(
|
||||
deployment=DeploymentPlan(
|
||||
service="aios-silent-release-controller",
|
||||
release_path="/usr/libexec/aios-silent-release-shadow/release-1",
|
||||
release_root="/usr/libexec/aios-silent-release-shadow",
|
||||
cutover_link="/usr/libexec/aios-silent-release-shadow/current",
|
||||
dropin_path=(
|
||||
"/usr/libexec/aios-silent-release-shadow/release-1/"
|
||||
"deploy/systemd/aios-silent-release-controller.service"
|
||||
),
|
||||
dropin_sha256="a" * 64,
|
||||
systemd_unit_sha256="a" * 64,
|
||||
rollback_path=(
|
||||
"/usr/libexec/aios-silent-release-shadow/rollback-1"
|
||||
),
|
||||
rollback_sha256="b" * 64,
|
||||
source_commit=source_commit,
|
||||
tree_oid="3" * 40,
|
||||
artifact_sha256="4" * 64,
|
||||
manifest_sha256="5" * 64,
|
||||
package_closure_sha256="c" * 64,
|
||||
authoritative_git_dir="/data/aios-gitops/repo.git",
|
||||
repository_identity="aios/aios",
|
||||
writer_expected=False,
|
||||
baseline_before_sha256="d" * 64,
|
||||
registry_before_sha256="e" * 64,
|
||||
production_tag_refset_before_sha256="f" * 64,
|
||||
health_contract_version="shadow-health/v1",
|
||||
timeout_policy={"health_seconds": 10},
|
||||
retry_policy={"health_attempts": 1},
|
||||
policy_sha256=policy_sha,
|
||||
health_contract={"shadow_mode": True},
|
||||
),
|
||||
tag=TagPlan(
|
||||
version="shadow-readiness-001",
|
||||
tag_type="annotated",
|
||||
expected_tag_oid=TAG_OID,
|
||||
expected_target_oid=source_commit,
|
||||
evidence_closure_oid="8" * 40,
|
||||
approval_plan_id="owner-plan-readiness-001",
|
||||
repository_identity="aios/aios",
|
||||
gitea_owner="aios",
|
||||
gitea_repository="aios",
|
||||
current_remote_tag_oid=None,
|
||||
remote_absent_evidence_sha256="a" * 64,
|
||||
verification_contract="shadow-only/v1",
|
||||
),
|
||||
finalization=FinalizationPlan(
|
||||
version="shadow-readiness-001",
|
||||
expected_tag_oid=TAG_OID,
|
||||
expected_target_oid=source_commit,
|
||||
expected_previous_baseline_sha256="d" * 64,
|
||||
registry_expected_record={
|
||||
"shadow": True,
|
||||
"target_oid": source_commit,
|
||||
},
|
||||
baseline_expected_record={
|
||||
"shadow": True,
|
||||
"target_oid": source_commit,
|
||||
},
|
||||
final_consistency_contract="shadow-simulated/v1",
|
||||
),
|
||||
policy_sha256=policy_sha,
|
||||
)
|
||||
|
||||
|
||||
def readiness_candidate(source_commit: str = COMMIT) -> ReleaseCandidate:
|
||||
return ReleaseCandidate(
|
||||
candidate_id="rc-shadow-" + source_commit[:12],
|
||||
source_commit=source_commit,
|
||||
tree_oid="3" * 40,
|
||||
artifact_sha256="4" * 64,
|
||||
manifest_sha256="5" * 64,
|
||||
tests={"passed": True, "source": "trusted-builder"},
|
||||
canary={"passed": True, "without_git": True},
|
||||
rollback_ready=True,
|
||||
git_consistency={"consistent": True},
|
||||
gitea_consistency={"consistent": True},
|
||||
approval_state="PENDING",
|
||||
state=ReleaseState.READY_FOR_APPROVAL,
|
||||
)
|
||||
|
||||
|
||||
def trusted_service(tmp_path):
|
||||
builder_signer = LocalTestBuilderSigner.generate()
|
||||
builder_public = builder_signer.public_key.public_bytes(
|
||||
serialization.Encoding.Raw,
|
||||
serialization.PublicFormat.Raw,
|
||||
)
|
||||
|
||||
def collector(intent):
|
||||
source_commit = str(intent["source_commit"])
|
||||
return TrustedCandidateBundle(
|
||||
candidate=readiness_candidate(source_commit),
|
||||
release_plan=readiness_release_plan(source_commit),
|
||||
checks={
|
||||
"authoritative_git": True,
|
||||
"artifact": True,
|
||||
"canary": True,
|
||||
"gitea": True,
|
||||
"manifest": True,
|
||||
"package_closure": True,
|
||||
"rollback": True,
|
||||
"secret_scan": True,
|
||||
"tests": True,
|
||||
},
|
||||
)
|
||||
|
||||
owner_private = Ed25519PrivateKey.generate()
|
||||
owner_public = owner_private.public_key().public_bytes(
|
||||
serialization.Encoding.Raw,
|
||||
serialization.PublicFormat.Raw,
|
||||
)
|
||||
verifier = SignedApprovalVerifier(
|
||||
ApprovalKeyRing({"shadow-owner-test-key": owner_public}),
|
||||
ApprovalRevocationStore(tmp_path / "revocations"),
|
||||
allowed_trust_domains={"aios.shadow"},
|
||||
)
|
||||
policy = TrustedReleasePolicy(
|
||||
services=frozenset({"aios-silent-release-controller"}),
|
||||
release_roots=frozenset(
|
||||
{"/usr/libexec/aios-silent-release-shadow"}
|
||||
),
|
||||
cutover_links=frozenset(
|
||||
{"/usr/libexec/aios-silent-release-shadow/current"}
|
||||
),
|
||||
dropin_paths=frozenset(
|
||||
{
|
||||
"/usr/libexec/aios-silent-release-shadow/release-1/"
|
||||
"deploy/systemd/aios-silent-release-controller.service"
|
||||
}
|
||||
),
|
||||
authoritative_git_dirs=frozenset(
|
||||
{"/data/aios-gitops/repo.git"}
|
||||
),
|
||||
repository_identities=frozenset({"aios/aios"}),
|
||||
)
|
||||
service = SilentReleaseActionService(
|
||||
tmp_path / "state",
|
||||
execution_enabled=False,
|
||||
approval_verifier=verifier,
|
||||
candidate_builder=SignedTrustedBuilderAdapter(
|
||||
collector, builder_signer
|
||||
),
|
||||
builder_attestation_verifier=BuilderAttestationVerifier(
|
||||
{builder_signer.key_id: builder_public}
|
||||
),
|
||||
trusted_policy=policy,
|
||||
expected_repository="aios/aios",
|
||||
policy_sha256="7" * 64,
|
||||
)
|
||||
return service, LocalTestApprovalSigner(
|
||||
"shadow-owner-test-key", owner_private
|
||||
)
|
||||
|
||||
|
||||
def signed_shadow_approval(
|
||||
service: SilentReleaseActionService,
|
||||
signer: LocalTestApprovalSigner,
|
||||
candidate_id: str,
|
||||
) -> SignedReleasePlanApproval:
|
||||
candidate = service.candidates.load(candidate_id)
|
||||
plan = service.release_plans.load(candidate_id)
|
||||
now = datetime.now(timezone.utc)
|
||||
value = SignedReleasePlanApproval(
|
||||
release_plan_id="release-plan-shadow-readiness-001",
|
||||
candidate_id=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,
|
||||
release_plan_sha256=plan.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="9" * 40,
|
||||
capabilities=(
|
||||
"runtime_switch",
|
||||
"tag_publish",
|
||||
"baseline_registry_finalize",
|
||||
),
|
||||
owner_identity="owner:shadow-test",
|
||||
trust_domain="aios.shadow",
|
||||
issued_at=now.isoformat().replace("+00:00", "Z"),
|
||||
not_before=now.isoformat().replace("+00:00", "Z"),
|
||||
expires_at=(now + timedelta(minutes=5))
|
||||
.isoformat()
|
||||
.replace("+00:00", "Z"),
|
||||
nonce="nonce-shadow-readiness-000001",
|
||||
key_id="shadow-owner-test-key",
|
||||
signature="",
|
||||
)
|
||||
return signer.sign(value)
|
||||
|
||||
|
||||
def test_trusted_builder_attestation_and_caller_evidence_gate(tmp_path):
|
||||
service, _ = trusted_service(tmp_path)
|
||||
result = service.create_release_intent(
|
||||
{"event_id": "webhook-main-001", "source_commit": COMMIT}
|
||||
)
|
||||
acceptance = service.get_acceptance(result["candidate_id"])
|
||||
assert acceptance["attestation"]["builder_attestation_id"].startswith(
|
||||
"builder-attestation-"
|
||||
)
|
||||
assert acceptance["checks"]["tests"] is True
|
||||
with pytest.raises(
|
||||
CapabilityAuthorizationError, match="caller-supplied evidence"
|
||||
):
|
||||
service.create_release_intent(
|
||||
{
|
||||
"event_id": "webhook-main-002",
|
||||
"source_commit": COMMIT,
|
||||
"tests": {"passed": True},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_builder_attestation_tamper_is_rejected(tmp_path):
|
||||
service, _ = trusted_service(tmp_path)
|
||||
bundle = service.candidate_builder.build(
|
||||
{"event_id": "webhook-main-001", "source_commit": COMMIT}
|
||||
)
|
||||
tampered = dataclasses.replace(
|
||||
bundle.attestation, artifact_sha256="0" * 64
|
||||
)
|
||||
with pytest.raises(
|
||||
CapabilityAuthorizationError, match="does not bind"
|
||||
):
|
||||
dataclasses.replace(bundle, attestation=tampered).validate(
|
||||
service.builder_attestation_verifier
|
||||
)
|
||||
|
||||
|
||||
def test_production_approval_backend_missing_disables_execution():
|
||||
backend = UnavailableProductionApprovalBackend()
|
||||
assert backend.available is False
|
||||
with pytest.raises(
|
||||
CapabilityAuthorizationError, match="not configured"
|
||||
):
|
||||
backend.verify()
|
||||
|
||||
|
||||
def test_controller_duplicate_webhook_notification_and_restart_are_idempotent(
|
||||
tmp_path,
|
||||
):
|
||||
service, _ = trusted_service(tmp_path)
|
||||
first_controller = PersistentSilentReleaseController(
|
||||
tmp_path / "state",
|
||||
service,
|
||||
main_probe=lambda: COMMIT,
|
||||
)
|
||||
first = first_controller.run_once()
|
||||
second = first_controller.run_once()
|
||||
restarted = PersistentSilentReleaseController(
|
||||
tmp_path / "state",
|
||||
service,
|
||||
main_probe=lambda: COMMIT,
|
||||
).run_once()
|
||||
assert first["intent"]["candidate_id"] == second["intent"]["candidate_id"]
|
||||
assert second["intent"]["idempotent"] is True
|
||||
assert first["notification"]["idempotent"] is False
|
||||
assert second["notification"]["idempotent"] is True
|
||||
assert restarted["notification"]["idempotent"] is True
|
||||
assert restarted["side_effects_executed"] is False
|
||||
|
||||
|
||||
def test_controller_startup_journal_failure_stops_before_main_event(tmp_path):
|
||||
service, _ = trusted_service(tmp_path)
|
||||
service.sagas.create(
|
||||
"rc-shadow-" + COMMIT[:12], "release-plan-shadow-readiness-001"
|
||||
)
|
||||
journal = next(service.sagas.root.glob("*.events.jsonl"))
|
||||
journal.write_text('{"tampered":true}\n', encoding="utf-8")
|
||||
probes: list[str] = []
|
||||
result = PersistentSilentReleaseController(
|
||||
tmp_path / "state",
|
||||
service,
|
||||
main_probe=lambda: probes.append("main") or COMMIT,
|
||||
).run_once()
|
||||
assert result["status"] == "FAIL_CLOSED"
|
||||
assert result["intent"] is None
|
||||
assert result["side_effects_executed"] is False
|
||||
assert probes == []
|
||||
|
||||
|
||||
def test_action_http_shadow_flow_has_zero_side_effects(tmp_path):
|
||||
service, signer = trusted_service(tmp_path)
|
||||
handler = type(
|
||||
"TestShadowHandler",
|
||||
(ShadowActionHandler,),
|
||||
{"service": service, "controller": None},
|
||||
)
|
||||
server = ThreadingHTTPServer(("127.0.0.1", 0), handler)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
base = f"http://127.0.0.1:{server.server_port}"
|
||||
|
||||
def post(path, payload):
|
||||
request = urllib.request.Request(
|
||||
base + path,
|
||||
data=json.dumps(payload).encode(),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
with urllib.request.urlopen(request, timeout=5) as response:
|
||||
return json.load(response)
|
||||
|
||||
try:
|
||||
intent = post(
|
||||
"/createReleaseIntent",
|
||||
{"event_id": "http-main-001", "source_commit": COMMIT},
|
||||
)
|
||||
candidate_id = intent["candidate_id"]
|
||||
approval = signed_shadow_approval(service, signer, candidate_id)
|
||||
accepted = post("/submitSignedApproval", approval.to_dict())
|
||||
deployed = post(
|
||||
"/startDeployment", {"candidate_id": candidate_id}
|
||||
)
|
||||
with urllib.request.urlopen(
|
||||
base + "/getReleasePlan?candidate_id=" + candidate_id,
|
||||
timeout=5,
|
||||
) as response:
|
||||
plan = json.load(response)
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
thread.join(timeout=5)
|
||||
|
||||
assert accepted["status"] == "VERIFIED_AND_STORED"
|
||||
assert deployed == {
|
||||
"candidate_id": candidate_id,
|
||||
"execution_enabled": False,
|
||||
"shadow_mode": True,
|
||||
"side_effects": [],
|
||||
"side_effects_executed": False,
|
||||
"status": "SHADOW_COMPLETED",
|
||||
}
|
||||
assert plan["shadow_mode"] is True
|
||||
assert plan["side_effects_executed"] is False
|
||||
|
||||
|
||||
def test_secret_scan_redacts_placeholders_but_rejects_real_material(tmp_path):
|
||||
(tmp_path / "safe.txt").write_text(
|
||||
"Authorization: Bearer test-placeholder-token-000000",
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert secret_scan(tmp_path) == {"findings": [], "passed": True}
|
||||
(tmp_path / "unsafe.txt").write_text(
|
||||
"-----BEGIN "
|
||||
+ "OPENSSH PRIVATE KEY-----\n"
|
||||
+ ("A" * 80)
|
||||
+ "\n-----END "
|
||||
+ "OPENSSH PRIVATE KEY-----",
|
||||
encoding="utf-8",
|
||||
)
|
||||
result = secret_scan(tmp_path)
|
||||
assert result["passed"] is False
|
||||
assert result["findings"] == ["unsafe.txt"]
|
||||
secret = "live-" + ("A" * 40)
|
||||
redacted = redact_output("Authorization: Bearer " + secret)
|
||||
assert secret not in redacted
|
||||
assert "<redacted>" in redacted
|
||||
(tmp_path / "unsafe.txt").write_text(
|
||||
"Authorization: " + "Bearer live-test-looking-secret-000001",
|
||||
encoding="utf-8",
|
||||
)
|
||||
result = secret_scan(tmp_path)
|
||||
assert result["passed"] is False
|
||||
assert result["findings"] == ["unsafe.txt"]
|
||||
|
||||
|
||||
def test_systemd_shadow_unit_has_both_gates_and_no_secret_environment():
|
||||
unit = (
|
||||
ROOT
|
||||
/ "deploy"
|
||||
/ "systemd"
|
||||
/ "aios-silent-release-controller.service"
|
||||
).read_text(encoding="utf-8")
|
||||
assert "AIOS_GITOPS_WRITES_ENABLED=false" in unit
|
||||
assert "AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false" in unit
|
||||
assert "--host 127.0.0.1" in unit
|
||||
assert "PRIVATE_KEY=" not in unit
|
||||
assert "TOKEN=" not in unit
|
||||
assert "PASSWORD=" not in unit
|
||||
@@ -32,6 +32,7 @@ from silent_release_hardening import ( # noqa: E402
|
||||
CapabilityAuthorizationError,
|
||||
CapabilityController,
|
||||
CapabilityLedger,
|
||||
ClaimProbeResult,
|
||||
DeploymentPlan,
|
||||
PreSwitchValidator,
|
||||
HardenedTagController,
|
||||
@@ -367,6 +368,7 @@ def test_capability_failure_stays_reserved_and_retry_can_complete():
|
||||
capability="runtime_switch",
|
||||
ledger=ledger,
|
||||
adapter=lambda: {"verified": True},
|
||||
recovery_probe=lambda: ClaimProbeResult.NOT_APPLIED,
|
||||
).execute(verified, operation="runtime-switch-001")
|
||||
assert recovered["status"] == "EXECUTED"
|
||||
assert ledger.reserve(
|
||||
@@ -713,7 +715,8 @@ def test_shadow_mode_never_invokes_runtime_tag_or_finalize_adapters():
|
||||
)
|
||||
service.create_release_candidate(candidate().to_dict())
|
||||
result = service.start_deployment("rc-hardening-001")
|
||||
assert result["status"] == "SHADOW_MODE"
|
||||
assert result["status"] == "SHADOW_COMPLETED"
|
||||
assert result["side_effects_executed"] is False
|
||||
|
||||
|
||||
def test_action_service_candidate_and_saga_are_durable():
|
||||
|
||||
@@ -501,7 +501,7 @@ def test_two_processes_execute_adapter_only_once(tmp_path):
|
||||
(ClaimProbeResult.APPLIED_VERIFIED, "IDEMPOTENT"),
|
||||
(
|
||||
ClaimProbeResult.INDETERMINATE,
|
||||
"MANUAL_OWNER_REVIEW_REQUIRED",
|
||||
"RECONCILIATION_REQUIRED",
|
||||
),
|
||||
(
|
||||
ClaimProbeResult.CONFLICT,
|
||||
@@ -947,10 +947,11 @@ def test_failure_after_tag_enters_reconciliation_without_rollback(
|
||||
assert first["state"] == SagaState.RECONCILIATION_REQUIRED
|
||||
assert "rollback" not in calls
|
||||
second = orchestrator.execute("rc-contract-001")
|
||||
assert second["state"] == SagaState.COMPLETED
|
||||
assert second["state"] == SagaState.RECONCILIATION_REQUIRED
|
||||
assert calls.count("tag_publish") == 1
|
||||
assert calls.count("registry_finalize") == 1
|
||||
assert calls.count("baseline_finalize") == 2
|
||||
assert calls.count("baseline_finalize") == 1
|
||||
assert failures["count"] == 1
|
||||
|
||||
|
||||
def test_actual_plan_change_blocks_before_any_adapter(tmp_path):
|
||||
@@ -1051,6 +1052,7 @@ def test_start_deployment_ignores_caller_approval_object(tmp_path):
|
||||
execution_enabled=True,
|
||||
approval_verifier=object(),
|
||||
candidate_builder=builder,
|
||||
builder_attestation_verifier=object(),
|
||||
trusted_policy=trusted_policy(),
|
||||
pre_switch_validator=object(),
|
||||
orchestrator=orchestrator,
|
||||
@@ -1151,6 +1153,7 @@ def test_action_execution_true_runs_complete_orchestrator(tmp_path):
|
||||
execution_enabled=True,
|
||||
approval_verifier=orchestrator.approval_verifier,
|
||||
candidate_builder=object(),
|
||||
builder_attestation_verifier=object(),
|
||||
trusted_policy=orchestrator.trusted_policy,
|
||||
pre_switch_validator=orchestrator.pre_switch_validator,
|
||||
orchestrator=orchestrator,
|
||||
|
||||
Reference in New Issue
Block a user