Files
aios/tests/test_model_api_trusted_candidate_builder.py
T
AIOS Governed Continuation 98bdeae6b4
git-diff-check git diff --check PASS on exact PR head
py-compile Python compileall PASS on active-runtime/app
reconciliation-contract Full source regression includes completed-replay/reconciliation contract PASS
secret-scan Diff-added lines secret scan PASS; no credential material
signer-validator-contract Signer contract suite PASS; live signer remains opt-in and no key was provisioned
campaign-approval-gate V2R2C2 trusted-controller source binding gate PASS; durable candidate creation remains post-merge
release-state-machine V2R2C2 release-controller source contract PASS; no production mutation enabled
autonomy-smoke-tests Source snapshot smoke/contract suite passed; production mutations disabled
autonomy-unit-tests 816 passed, 3 skipped, 154 subtests passed
fix(release): bind trusted builder to canonical purpose
2026-08-29 15:25:31 +08:00

507 lines
18 KiB
Python

from __future__ import annotations
import base64
import dataclasses
import hashlib
import importlib
import importlib.util
import json
import socket
import sys
import threading
import urllib.error
import urllib.request
from datetime import datetime, timedelta, timezone
from pathlib import Path
from types import SimpleNamespace
import pytest
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
APP = Path(__file__).resolve().parents[1] / "active-runtime" / "app"
AGENT_OS = Path(__file__).resolve().parents[1] / "runtime" / "agent-os"
for import_root in (APP, AGENT_OS):
if str(import_root) not in sys.path:
sys.path.insert(0, str(import_root))
from silent_release_hardening import ( # noqa: E402
BuilderAttestation,
BuilderAttestationKeyRing,
BuilderAttestationVerifier,
CapabilityAuthorizationError,
SilentReleaseActionService,
TrustedCandidateBuilderUnavailable,
)
import silent_release_hardening as hardening # noqa: E402
COMMIT = "a" * 40
TREE = "b" * 40
SHA = "c" * 64
GITEA = {"consistent": True}
def _af_inet_available() -> bool:
try:
probe = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except OSError:
return False
probe.close()
return True
def _canonical_digest(value: dict) -> str:
return hashlib.sha256(
json.dumps(value, sort_keys=True, separators=(",", ":")).encode()
).hexdigest()
def _instant(delta: timedelta = timedelta()) -> str:
return (datetime.now(timezone.utc) + delta).isoformat().replace(
"+00:00", "Z"
)
def _keyring_payload(private: Ed25519PrivateKey, *, purpose: str) -> dict:
public = private.public_key().public_bytes(
serialization.Encoding.Raw, serialization.PublicFormat.Raw
)
record = {
"algorithm": "Ed25519",
"purpose": purpose,
"key_id": "builder-key-001",
"generation": 1,
"not_before": _instant(timedelta(minutes=-1)),
"expires_at": _instant(timedelta(days=1)),
"status": "ACTIVE",
"public_key": base64.b64encode(public).decode("ascii"),
"public_key_sha256": hashlib.sha256(public).hexdigest(),
}
payload = {
"algorithm": "Ed25519",
"purpose": "AIOS_TRUSTED_RELEASE_BUILDER",
"keys": [record],
}
payload["keyring_sha256"] = hashlib.sha256(
json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
).hexdigest()
return payload
def _refresh_keyring_digest(payload: dict) -> dict:
canonical = {
"algorithm": payload["algorithm"],
"purpose": payload["purpose"],
"keys": payload["keys"],
}
payload["keyring_sha256"] = hashlib.sha256(
json.dumps(canonical, sort_keys=True, separators=(",", ":")).encode("utf-8")
).hexdigest()
return payload
def _attestation(private: Ed25519PrivateKey) -> BuilderAttestation:
unsigned = BuilderAttestation(
builder_attestation_id="builder-attestation-001",
builder_key_id="builder-key-001",
source_commit=COMMIT,
tree_oid=TREE,
artifact_sha256=SHA,
manifest_sha256=SHA,
package_closure_sha256=SHA,
test_evidence_sha256=SHA,
canary_evidence_sha256=SHA,
issued_at=_instant(),
signature="",
builder_key_generation=1,
signature_algorithm="Ed25519",
signature_purpose="AIOS_TRUSTED_RELEASE_BUILDER",
gitea_evidence_sha256=_canonical_digest(GITEA),
rollback_evidence_sha256=_canonical_digest({
"ready": True,
"rollback_sha256": SHA,
"source_commit": COMMIT,
}),
policy_evidence_sha256=SHA,
secret_scan_evidence_sha256=SHA,
)
return BuilderAttestation(
**{
**unsigned.canonical_payload,
"signature": base64.b64encode(
private.sign(unsigned.canonical_bytes())
).decode("ascii"),
}
)
def test_builder_verifier_rejects_owner_key_record_cross_purpose(tmp_path):
private = Ed25519PrivateKey.generate()
payload = _keyring_payload(private, purpose="owner_approval")
path = tmp_path / "builder-keyring.json"
path.write_text(json.dumps(payload), encoding="utf-8")
with pytest.raises(ValueError, match="purpose"):
BuilderAttestationKeyRing.from_file(path)
def test_builder_verifier_checks_domain_key_window_digest_and_signature(tmp_path):
private = Ed25519PrivateKey.generate()
path = tmp_path / "builder-keyring.json"
path.write_text(
json.dumps(_keyring_payload(private, purpose="AIOS_TRUSTED_RELEASE_BUILDER")),
encoding="utf-8",
)
verifier = BuilderAttestationVerifier(
BuilderAttestationKeyRing.from_file(path)
)
candidate = SimpleNamespace(
source_commit=COMMIT,
tree_oid=TREE,
artifact_sha256=SHA,
manifest_sha256=SHA,
gitea_consistency=GITEA,
rollback_ready=True,
)
plan = SimpleNamespace(
deployment=SimpleNamespace(
package_closure_sha256=SHA,
rollback_sha256=SHA,
),
policy_sha256=SHA,
)
assert verifier.verify(_attestation(private), candidate, plan).builder_key_id == "builder-key-001"
@pytest.mark.parametrize("state", ["REVOKED", "EXPIRED", "FUTURE"])
def test_builder_verifier_rejects_unavailable_key_lifecycle_states(tmp_path, state):
private = Ed25519PrivateKey.generate()
payload = _keyring_payload(private, purpose="AIOS_TRUSTED_RELEASE_BUILDER")
record = payload["keys"][0]
if state == "REVOKED":
record["status"] = "REVOKED"
elif state == "EXPIRED":
record["not_before"] = _instant(timedelta(days=-2))
record["expires_at"] = _instant(timedelta(days=-1))
else:
record["not_before"] = _instant(timedelta(days=1))
record["expires_at"] = _instant(timedelta(days=2))
path = tmp_path / "builder-keyring.json"
path.write_text(json.dumps(_refresh_keyring_digest(payload)), encoding="utf-8")
verifier = BuilderAttestationVerifier(BuilderAttestationKeyRing.from_file(path))
candidate = SimpleNamespace(
source_commit=COMMIT, tree_oid=TREE,
artifact_sha256=SHA, manifest_sha256=SHA,
gitea_consistency=GITEA, rollback_ready=True,
)
plan = SimpleNamespace(deployment=SimpleNamespace(package_closure_sha256=SHA, rollback_sha256=SHA), policy_sha256=SHA)
with pytest.raises(CapabilityAuthorizationError, match="validity|revoked|unavailable"):
verifier.verify(_attestation(private), candidate, plan)
def test_builder_verifier_rejects_unknown_key_invalid_signature_and_keyring_digest(tmp_path):
private = Ed25519PrivateKey.generate()
payload = _keyring_payload(private, purpose="AIOS_TRUSTED_RELEASE_BUILDER")
path = tmp_path / "builder-keyring.json"
path.write_text(json.dumps(payload), encoding="utf-8")
verifier = BuilderAttestationVerifier(BuilderAttestationKeyRing.from_file(path))
candidate = SimpleNamespace(
source_commit=COMMIT, tree_oid=TREE,
artifact_sha256=SHA, manifest_sha256=SHA,
gitea_consistency=GITEA, rollback_ready=True,
)
plan = SimpleNamespace(deployment=SimpleNamespace(package_closure_sha256=SHA, rollback_sha256=SHA), policy_sha256=SHA)
with pytest.raises(CapabilityAuthorizationError, match="not trusted"):
verifier.verify(
dataclasses.replace(_attestation(private), builder_key_id="unknown-builder-key"),
candidate,
plan,
)
with pytest.raises(CapabilityAuthorizationError, match="signature"):
verifier.verify(
dataclasses.replace(_attestation(private), signature=base64.b64encode(b"x" * 64).decode()),
candidate,
plan,
)
payload["keyring_sha256"] = "0" * 64
path.write_text(json.dumps(payload), encoding="utf-8")
with pytest.raises(ValueError, match="keyring digest"):
BuilderAttestationKeyRing.from_file(path)
def test_builder_attestation_rejects_tampered_digest_and_signing_metadata():
private = Ed25519PrivateKey.generate()
raw = _attestation(private).to_dict()
raw["fingerprint"] = "0" * 64
with pytest.raises(CapabilityAuthorizationError, match="digest is invalid"):
BuilderAttestation.from_dict(raw, require_fingerprint=True)
@pytest.mark.parametrize(
"field,value",
[
("builder_key_generation", 2),
("signature_algorithm", "RSA"),
("signature_purpose", "owner_approval"),
],
)
def test_builder_keyring_rejects_attestation_metadata_mismatch(tmp_path, field, value):
private = Ed25519PrivateKey.generate()
path = tmp_path / "builder-keyring.json"
path.write_text(
json.dumps(_keyring_payload(private, purpose="AIOS_TRUSTED_RELEASE_BUILDER")),
encoding="utf-8",
)
verifier = BuilderAttestationVerifier(
BuilderAttestationKeyRing.from_file(path)
)
candidate = SimpleNamespace(
source_commit=COMMIT, tree_oid=TREE,
artifact_sha256=SHA, manifest_sha256=SHA,
gitea_consistency=GITEA, rollback_ready=True,
)
plan = SimpleNamespace(deployment=SimpleNamespace(package_closure_sha256=SHA, rollback_sha256=SHA), policy_sha256=SHA)
with pytest.raises(CapabilityAuthorizationError, match="metadata"):
verifier.verify(
dataclasses.replace(_attestation(private), **{field: value}),
candidate,
plan,
)
raw = _attestation(private).to_dict()
raw["builder_key_generation"] = 2
with pytest.raises(CapabilityAuthorizationError, match="digest is invalid"):
BuilderAttestation.from_dict(raw, require_fingerprint=True)
def test_builder_unavailable_intent_is_bounded_retryable_and_recovers_without_secret(tmp_path, monkeypatch):
class UnavailableBuilder:
def build(self, intent):
raise TrustedCandidateBuilderUnavailable("socket token=should-not-leak")
monkeypatch.setattr(hardening, "_fsync_directory", lambda _path: None)
service = SilentReleaseActionService(
tmp_path,
candidate_builder=UnavailableBuilder(),
max_builder_attempts=2,
)
payload = {"event_id": "event-builder-001", "source_commit": COMMIT}
first = service.create_release_intent(payload)
second = service.create_release_intent(payload)
assert first["status"] == "BUILDER_RETRYABLE_FAILURE"
assert first["retryable"] is True
assert first["attempt_count"] == 1
assert "should-not-leak" not in json.dumps(first)
assert second["status"] == "BUILDER_TERMINAL_FAILURE"
assert second["retryable"] is False
assert second["attempt_count"] == 2
def test_stale_building_intent_recovers_after_restart_without_double_running_active_attempt(tmp_path, monkeypatch):
class CountingUnavailableBuilder:
calls = 0
def build(self, intent):
self.calls += 1
raise TrustedCandidateBuilderUnavailable("nonsecret")
monkeypatch.setattr(hardening, "_fsync_directory", lambda _path: None)
builder = CountingUnavailableBuilder()
service = SilentReleaseActionService(
tmp_path,
candidate_builder=builder,
max_builder_attempts=3,
builder_attempt_timeout_seconds=1,
)
payload = {"event_id": "event-builder-crash-001", "source_commit": COMMIT}
intent_id = "intent-" + hashlib.sha256(
f"{payload['event_id']}\0{COMMIT}".encode()
).hexdigest()[:24]
intent_path = tmp_path / "intents" / f"{intent_id}.json"
intent_path.parent.mkdir(parents=True)
active_record = {
"attempt_count": 1,
"attempt_started_at": _instant(),
"created_at": _instant(timedelta(minutes=-1)),
"event_id": payload["event_id"],
"intent_id": intent_id,
"source_commit": COMMIT,
"status": "BUILDING",
}
intent_path.write_text(json.dumps(active_record), encoding="utf-8")
active = service.create_release_intent(payload)
assert active["status"] == "BUILDING"
assert active["idempotent"] is True
assert builder.calls == 0
active_record["attempt_started_at"] = _instant(timedelta(minutes=-5))
intent_path.write_text(json.dumps(active_record), encoding="utf-8")
recovered = service.create_release_intent(payload)
assert recovered["status"] == "BUILDER_RETRYABLE_FAILURE"
assert recovered["attempt_count"] == 2
assert recovered["failure_code"] == "TRUSTED_BUILDER_UNAVAILABLE"
assert recovered["last_recovery_code"] == "TRUSTED_BUILDER_CRASH_RECOVERED"
assert recovered["recovery_count"] == 1
assert builder.calls == 1
@pytest.mark.skipif(sys.platform == "win32", reason="model API runtime requires POSIX fcntl")
@pytest.mark.skipif(
not _af_inet_available(),
reason="isolated executor permits AF_UNIX only; live HTTP probe runs at activation",
)
def test_model_api_keeps_process_liveness_separate_from_builder_readiness(monkeypatch):
api = importlib.import_module("aios_model_api_staging")
class FakeService:
ready = False
def get_builder_readiness(self):
return {"status": "READY" if self.ready else "UNAVAILABLE"}
def create_release_candidate(self, payload):
if not payload:
raise ValueError("event_id is required")
return {"status": "READY_FOR_APPROVAL"}
service = FakeService()
monkeypatch.setattr(api, "silent_release_service", lambda: service)
monkeypatch.setattr(
api,
"authenticate",
lambda token: (
{"account": "test", "role": "owner", "scopes": ["release:candidate"]}
if token == "test-token"
else None
),
)
server = api.ThreadingHTTPServer(("127.0.0.1", 0), api.H)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
base = f"http://127.0.0.1:{server.server_port}"
def request(path, body=None):
data = None if body is None else json.dumps(body).encode("utf-8")
value = urllib.request.Request(
base + path,
data=data,
headers={"Authorization": "Bearer test-token"},
method="POST" if body is not None else "GET",
)
try:
with urllib.request.urlopen(value, timeout=5) as response:
return response.status, json.load(response)
except urllib.error.HTTPError as error:
return error.code, json.load(error)
try:
health_code, health = request("/health")
unavailable_code, unavailable = request("/createReleaseCandidate", {})
service.ready = True
invalid_code, invalid = request("/createReleaseCandidate", {})
finally:
server.shutdown()
server.server_close()
thread.join(timeout=5)
assert health_code == 200
assert health["process_liveness"] == "PASS"
assert health["builder_readiness"]["status"] == "UNAVAILABLE"
assert unavailable_code == 503
assert unavailable["error"] == "TRUSTED_BUILDER_UNAVAILABLE"
assert invalid_code == 400
assert invalid["error"] == "event_id is required"
@pytest.mark.skipif(sys.platform == "win32", reason="model API runtime requires POSIX fcntl")
def test_host_adapter_accepts_a_real_builder_bundle_response():
spec = importlib.util.spec_from_file_location(
"trusted_builder_public_seam",
APP.parents[1] / "tests" / "test_trusted_candidate_builder.py",
)
assert spec and spec.loader
backend_seam = importlib.util.module_from_spec(spec)
spec.loader.exec_module(backend_seam)
server, client, _ = backend_seam.make_seam()
candidate_id = "rc-v2r2c2-d2b5a8339a34"
source_commit = "d2b5a8339a34a8f958a177f8a3b9e8296a57a7a6"
response = server.handle_request(
client.create_request(
"V2R2C2", candidate_id, source_commit, nonce="z" * 32
),
connection=object(),
)
class ResponseClient:
def build(self, version, supplied_candidate_id, supplied_source, *, nonce):
assert version == "V2R2C2"
assert supplied_candidate_id == candidate_id
assert supplied_source == source_commit
assert len(nonce) >= 32
return response
api = importlib.import_module("aios_model_api_staging")
bundle = api._HostTrustedCandidateBuilderAdapter(ResponseClient()).build(
{
"candidate_id": candidate_id,
"source_commit": source_commit,
"version": "V2R2C2",
}
)
assert bundle.validate().candidate.source_commit == response["base_oid"]
assert bundle.attestation.builder_key_id == response["attestation"]["builder_key_id"]
@pytest.mark.skipif(sys.platform == "win32", reason="model API runtime requires POSIX fcntl")
def test_host_adapter_forwards_v2r2c3_to_the_policy_and_fails_closed_when_unavailable():
from aios_trusted_candidate_builder import BuilderError
api = importlib.import_module("aios_model_api_staging")
calls = []
class DerivedOnlyClient:
def build(self, version, candidate_id, source_commit, *, nonce):
calls.append((version, candidate_id, source_commit, nonce))
raise BuilderError("GOVERNED_V2R2C3_DERIVATION_REQUIRED")
with pytest.raises(TrustedCandidateBuilderUnavailable):
api._HostTrustedCandidateBuilderAdapter(DerivedOnlyClient()).build(
{
"candidate_id": "r42-release-candidate-v2r2c3-20260820",
"source_commit": COMMIT,
"version": "V2R2C3",
}
)
assert calls[0][:3] == (
"V2R2C3",
"r42-release-candidate-v2r2c3-20260820",
COMMIT,
)
def test_host_adapter_accepts_intent_only_without_caller_candidate_id():
api = importlib.import_module('aios_model_api_staging')
calls = []
class IntentClient:
def build(self, version, candidate_id, source_commit, *, nonce):
calls.append((version, candidate_id, source_commit, nonce))
raise RuntimeError('intent-only probe')
with pytest.raises(RuntimeError, match='intent-only probe'):
api._HostTrustedCandidateBuilderAdapter(IntentClient()).build({
'event_id': 'event-intent-only',
'intent_id': 'intent-intent-only',
'source_commit': COMMIT,
'version': 'V2R2C2',
})
assert calls[0][0] == 'V2R2C2'
assert calls[0][1] == ''
assert calls[0][2] == COMMIT
assert len(calls[0][3]) >= 32