feat(governed): implement governed release evidence producer (governed.release.evidence.produce + test)
This commit is contained in:
@@ -0,0 +1,514 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Governed release evidence producer (V2R2C2 P0, operation governed.release.evidence.produce).
|
||||
|
||||
This module is the *producer* half of the governed evidence pipeline. It never
|
||||
invents candidate/release evidence: every digest and pass/fail flag in the closure
|
||||
is derived from real build artifacts and real verification executions supplied as
|
||||
input. It assembles a self-consistent, root-owned evidence closure below
|
||||
``source_root/<version>`` that ``TrustedBuilderEvidenceMaterializer`` (the projection
|
||||
half) can re-validate and project byte-for-byte.
|
||||
|
||||
Contract (see docs/V2R2C2-P0-convergence-20260909.md section 1):
|
||||
- operation_id : governed.release.evidence.produce
|
||||
- input : governed build artifacts + deployment conventions (no hand-write)
|
||||
- output : source_root/<version>/ full evidence_layout closure + receipt.json
|
||||
- fail-closed : mismatched secret-scan digest, tampered artifact, non-passing
|
||||
tests/canary/rollback/secret-scan, or inconsistent gitea all abort
|
||||
without writing a closure.
|
||||
- idempotent : an existing identical closure is accepted; a conflicting one fails.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import secrets
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import stat
|
||||
from typing import Any, Mapping
|
||||
|
||||
from aios_baseline.operations import OperationDefinition, OperationRegistry
|
||||
from aios_trusted_candidate_builder import (
|
||||
load_builder_policy,
|
||||
TrustedCandidateBuilderConfig,
|
||||
)
|
||||
from release_evidence_materializer import EvidenceMaterializationError
|
||||
from release_version import validate_release_version
|
||||
|
||||
|
||||
PRODUCE_SCHEMA = "aios.governed-release-evidence-producer.v1"
|
||||
EVIDENCE_RECEIPT_SCHEMA = "aios.trusted-candidate-builder.evidence.v1"
|
||||
|
||||
|
||||
class EvidenceProduceError(RuntimeError):
|
||||
"""Stable, non-sensitive producer failure."""
|
||||
|
||||
|
||||
def _canonical_json(value: Mapping[str, Any]) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
ensure_ascii=True,
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def _file_sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with Path(path).open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _sha256_bytes(value: bytes) -> str:
|
||||
return hashlib.sha256(value).hexdigest()
|
||||
|
||||
|
||||
def _validate_oid(value: object, name: str) -> str:
|
||||
normalized = str(value or "").strip().lower()
|
||||
if (
|
||||
len(normalized) != 40
|
||||
or any(char not in "0123456789abcdef" for char in normalized)
|
||||
):
|
||||
raise EvidenceProduceError(f"{name.upper()}_INVALID")
|
||||
return normalized
|
||||
|
||||
|
||||
def _validate_identifier(value: object, name: str) -> str:
|
||||
normalized = str(value or "").strip()
|
||||
if (
|
||||
not normalized
|
||||
or len(normalized) > 255
|
||||
or normalized[0] in ".-"
|
||||
or any(
|
||||
char not in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._:-"
|
||||
for char in normalized
|
||||
)
|
||||
):
|
||||
raise EvidenceProduceError(f"{name.upper()}_INVALID")
|
||||
return normalized
|
||||
|
||||
|
||||
def _validate_sha256(value: object, name: str, *, allow_empty: bool = False) -> str:
|
||||
normalized = str(value or "").strip().lower()
|
||||
if allow_empty and not normalized:
|
||||
return ""
|
||||
if (
|
||||
len(normalized) != 64
|
||||
or any(char not in "0123456789abcdef" for char in normalized)
|
||||
):
|
||||
raise EvidenceProduceError(f"{name.upper()}_INVALID")
|
||||
return normalized
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GovernedBuildArtifacts:
|
||||
"""Real build artifacts + real verification results. Producer never invents these."""
|
||||
|
||||
version: str
|
||||
candidate_id: str
|
||||
source_commit: str
|
||||
tree_oid: str
|
||||
artifact: bytes
|
||||
manifest: Mapping[str, Any]
|
||||
package_closure: bytes
|
||||
policy: Mapping[str, Any]
|
||||
tests: Mapping[str, Any]
|
||||
canary: Mapping[str, Any]
|
||||
gitea: Mapping[str, Any]
|
||||
rollback: Mapping[str, Any]
|
||||
secret_scan: Mapping[str, Any]
|
||||
# Deployment conventions (supplied by release planning, not evidence invention).
|
||||
deployment: Mapping[str, Any]
|
||||
tag: Mapping[str, Any]
|
||||
finalization: Mapping[str, Any]
|
||||
rollback_sha256: str = ""
|
||||
issued_at: str = ""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProduceResult:
|
||||
status: str
|
||||
idempotent: bool
|
||||
receipt_sha256: str
|
||||
closure_dir: str
|
||||
evidence_digests: Mapping[str, str] = field(default_factory=dict)
|
||||
|
||||
|
||||
def _verify_artifacts(
|
||||
config: TrustedCandidateBuilderConfig,
|
||||
request: GovernedBuildArtifacts,
|
||||
) -> dict[str, str]:
|
||||
"""Validate input consistency (fail-closed) and return the evidence digests."""
|
||||
try:
|
||||
version = validate_release_version(request.version)
|
||||
except ValueError as exc:
|
||||
raise EvidenceProduceError("VERSION_INVALID") from exc
|
||||
if version != request.version:
|
||||
raise EvidenceProduceError("VERSION_INVALID")
|
||||
|
||||
candidate_id = _validate_identifier(request.candidate_id, "candidate_id")
|
||||
source_commit = _validate_oid(request.source_commit, "source_commit")
|
||||
tree_oid = _validate_oid(request.tree_oid, "tree_oid")
|
||||
|
||||
if (
|
||||
set(request.manifest) != {
|
||||
"artifact_sha256", "package_closure_sha256",
|
||||
}
|
||||
):
|
||||
raise EvidenceProduceError("MANIFEST_SCHEMA_INVALID")
|
||||
closure_sha = _validate_sha256(
|
||||
request.manifest.get("package_closure_sha256"), "package_closure_sha256"
|
||||
)
|
||||
artifact_sha = _sha256_bytes(request.artifact)
|
||||
if request.manifest["artifact_sha256"] != artifact_sha:
|
||||
raise EvidenceProduceError("MANIFEST_ARTIFACT_DIGEST_MISMATCH")
|
||||
|
||||
# manifest_sha256 is the digest of the manifest evidence file itself. It is
|
||||
# derived (never invented) from the manifest content; the manifest does not
|
||||
# embed its own digest to avoid a self-referential fixed point.
|
||||
manifest_sha = _sha256_bytes(_canonical_json(dict(request.manifest)))
|
||||
if _sha256_bytes(request.package_closure) != closure_sha:
|
||||
raise EvidenceProduceError("PACKAGE_CLOSURE_DIGEST_MISMATCH")
|
||||
|
||||
# policy_sha256 is the digest of the policy evidence file content; derived
|
||||
# here (never invented, never self-referential).
|
||||
policy_sha = _sha256_bytes(_canonical_json(dict(request.policy)))
|
||||
|
||||
# Secret scan must cover the exact artifact bytes.
|
||||
secret_scan_artifact = _validate_sha256(
|
||||
request.secret_scan.get("artifact_sha256"), "secret_scan.artifact_sha256"
|
||||
)
|
||||
if secret_scan_artifact != artifact_sha:
|
||||
raise EvidenceProduceError("SECRET_SCAN_ARTIFACT_DIGEST_MISMATCH")
|
||||
if request.secret_scan.get("passed") is not True:
|
||||
raise EvidenceProduceError("SECRET_SCAN_NOT_PASSED")
|
||||
|
||||
if request.tests.get("passed") is not True:
|
||||
raise EvidenceProduceError("TESTS_NOT_PASSED")
|
||||
if request.canary.get("passed") is not True:
|
||||
raise EvidenceProduceError("CANARY_NOT_PASSED")
|
||||
if request.rollback.get("ready") is not True:
|
||||
raise EvidenceProduceError("ROLLBACK_NOT_READY")
|
||||
if (
|
||||
request.gitea.get("consistent") is not True
|
||||
or request.gitea.get("repository") != config.repository
|
||||
or request.gitea.get("source_commit") != source_commit
|
||||
or request.gitea.get("remote_ref_oid") != source_commit
|
||||
or request.gitea.get("tree_oid") != tree_oid
|
||||
):
|
||||
raise EvidenceProduceError("GITEA_INCONSISTENT")
|
||||
|
||||
digests = {
|
||||
"artifact": artifact_sha,
|
||||
"manifest": manifest_sha,
|
||||
"package_closure": closure_sha,
|
||||
"policy": policy_sha,
|
||||
"tests": _sha256_bytes(_canonical_json(dict(request.tests))),
|
||||
"canary": _sha256_bytes(_canonical_json(dict(request.canary))),
|
||||
"gitea": _sha256_bytes(_canonical_json(dict(request.gitea))),
|
||||
"rollback": _sha256_bytes(_canonical_json(dict(request.rollback))),
|
||||
"secret_scan": _sha256_bytes(_canonical_json(dict(request.secret_scan))),
|
||||
}
|
||||
return digests
|
||||
|
||||
|
||||
def _build_receipt(
|
||||
config: TrustedCandidateBuilderConfig,
|
||||
request: GovernedBuildArtifacts,
|
||||
) -> Mapping[str, Any]:
|
||||
"""Assemble the self-consistent receipt from verified artifacts."""
|
||||
from silent_release_controller import ReleaseCandidate, ReleaseState
|
||||
|
||||
artifact_sha = _sha256_bytes(request.artifact)
|
||||
manifest_sha = _sha256_bytes(_canonical_json(dict(request.manifest)))
|
||||
closure_sha = _validate_sha256(
|
||||
request.manifest.get("package_closure_sha256"), "package_closure_sha256"
|
||||
)
|
||||
policy_sha = _sha256_bytes(_canonical_json(dict(request.policy)))
|
||||
|
||||
candidate_kwargs: dict[str, Any] = {}
|
||||
issued_at = str(request.issued_at or "").strip()
|
||||
if issued_at:
|
||||
# The evidence was produced at the build execution instant; bind it so the
|
||||
# same verified artifacts reproduce an identical, idempotent closure.
|
||||
candidate_kwargs["created_at"] = issued_at
|
||||
candidate_kwargs["updated_at"] = issued_at
|
||||
candidate = ReleaseCandidate(
|
||||
candidate_id=request.candidate_id,
|
||||
source_commit=request.source_commit,
|
||||
tree_oid=request.tree_oid,
|
||||
artifact_sha256=artifact_sha,
|
||||
manifest_sha256=manifest_sha,
|
||||
tests=dict(request.tests),
|
||||
canary=dict(request.canary),
|
||||
rollback_ready=request.rollback.get("ready") is True,
|
||||
git_consistency={
|
||||
"consistent": True,
|
||||
"repository": config.repository,
|
||||
"source_commit": request.source_commit,
|
||||
"tree_oid": request.tree_oid,
|
||||
},
|
||||
gitea_consistency=dict(request.gitea),
|
||||
state=ReleaseState.READY_FOR_APPROVAL,
|
||||
**candidate_kwargs,
|
||||
)
|
||||
|
||||
deployment_payload = dict(request.deployment)
|
||||
deployment_payload.update(
|
||||
{
|
||||
"service": request.deployment["service"],
|
||||
"source_commit": request.source_commit,
|
||||
"tree_oid": request.tree_oid,
|
||||
"artifact_sha256": artifact_sha,
|
||||
"manifest_sha256": manifest_sha,
|
||||
"package_closure_sha256": closure_sha,
|
||||
"policy_sha256": policy_sha,
|
||||
"rollback_sha256": _validate_sha256(
|
||||
request.rollback_sha256, "rollback_sha256"
|
||||
)
|
||||
or request.rollback.get("rollback_sha256", ""),
|
||||
"repository_identity": config.repository,
|
||||
"authoritative_git_dir": request.deployment["authoritative_git_dir"],
|
||||
}
|
||||
)
|
||||
if not deployment_payload["rollback_sha256"]:
|
||||
raise EvidenceProduceError("ROLLBACK_SHA256_REQUIRED")
|
||||
|
||||
from silent_release_hardening import (
|
||||
DeploymentPlan,
|
||||
FinalizationPlan,
|
||||
ReleasePlan,
|
||||
TagPlan,
|
||||
TrustedCandidateBundle,
|
||||
)
|
||||
|
||||
release_plan = ReleasePlan(
|
||||
deployment=DeploymentPlan.from_dict(deployment_payload),
|
||||
tag=TagPlan.from_dict(dict(request.tag)),
|
||||
finalization=FinalizationPlan.from_dict(dict(request.finalization)),
|
||||
policy_sha256=policy_sha,
|
||||
)
|
||||
checks = {name: True for name in config.required_checks}
|
||||
TrustedCandidateBundle(candidate, release_plan, checks).validate()
|
||||
|
||||
return {
|
||||
"schema": EVIDENCE_RECEIPT_SCHEMA,
|
||||
"version": request.version,
|
||||
"candidate": candidate.to_dict(),
|
||||
"release_plan": release_plan.to_dict(),
|
||||
}
|
||||
|
||||
|
||||
def _safe_target(source_root: Path, version: str) -> Path:
|
||||
relative = Path(version)
|
||||
if relative.is_absolute() or any(
|
||||
part in {"", ".", ".."} for part in relative.parts
|
||||
):
|
||||
raise EvidenceProduceError("CLOSURE_PATH_INVALID")
|
||||
candidate = Path(source_root) / version
|
||||
try:
|
||||
metadata = Path(source_root).stat(follow_symlinks=False)
|
||||
except OSError as exc:
|
||||
raise EvidenceProduceError("SOURCE_ROOT_UNAVAILABLE") from exc
|
||||
if not stat.S_ISDIR(metadata.st_mode) or Path(source_root).is_symlink():
|
||||
raise EvidenceProduceError("SOURCE_ROOT_INVALID")
|
||||
return candidate
|
||||
|
||||
|
||||
def _write_durable_json(path: Path, payload: Mapping[str, Any], mode: int = 0o600) -> str:
|
||||
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||
# The deterministic collector compares file digests against the canonical
|
||||
# payload hash (no trailing newline); write the exact canonical bytes so the
|
||||
# producer's closure re-validates under TrustedBuilderEvidenceMaterializer.
|
||||
raw = _canonical_json(payload)
|
||||
descriptor = os.open(
|
||||
path,
|
||||
os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0)
|
||||
| getattr(os, "O_BINARY", 0),
|
||||
mode,
|
||||
)
|
||||
try:
|
||||
os.write(descriptor, raw)
|
||||
os.fsync(descriptor)
|
||||
finally:
|
||||
os.close(descriptor)
|
||||
os.chmod(path, mode)
|
||||
return hashlib.sha256(raw).hexdigest()
|
||||
|
||||
|
||||
def _write_durable_bytes(path: Path, value: bytes, mode: int = 0o600) -> str:
|
||||
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||
descriptor = os.open(
|
||||
path,
|
||||
os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0)
|
||||
| getattr(os, "O_BINARY", 0),
|
||||
mode,
|
||||
)
|
||||
try:
|
||||
os.write(descriptor, value)
|
||||
os.fsync(descriptor)
|
||||
finally:
|
||||
os.close(descriptor)
|
||||
os.chmod(path, mode)
|
||||
return hashlib.sha256(value).hexdigest()
|
||||
|
||||
|
||||
def produce_evidence_closure(
|
||||
*,
|
||||
builder_policy: Path,
|
||||
source_root: Path,
|
||||
request: GovernedBuildArtifacts,
|
||||
expected_owner_uid: int | None = 0,
|
||||
) -> ProduceResult:
|
||||
"""Produce a self-consistent, root-owned evidence closure, fail-closed and idempotent."""
|
||||
try:
|
||||
config = TrustedCandidateBuilderConfig.from_policy(
|
||||
load_builder_policy(builder_policy)
|
||||
)
|
||||
except Exception as exc:
|
||||
raise EvidenceProduceError(str(exc)) from exc
|
||||
|
||||
try:
|
||||
version = validate_release_version(request.version)
|
||||
except ValueError as exc:
|
||||
raise EvidenceProduceError("VERSION_INVALID") from exc
|
||||
|
||||
digests = _verify_artifacts(config, request)
|
||||
receipt = _build_receipt(config, request)
|
||||
|
||||
# Resolve layout paths from policy (aligned with evidence_layout; never invented).
|
||||
target = _safe_target(source_root, version)
|
||||
layout_paths = {
|
||||
name: target / Path(template.format(version=version)).relative_to(version)
|
||||
for name, template in config.evidence_layout.items()
|
||||
}
|
||||
|
||||
if target.exists() or target.is_symlink():
|
||||
# Idempotency: an existing identical closure (same receipt digest + same
|
||||
# artifact bytes) is accepted; anything else is a conflict.
|
||||
existing_receipt = target / "receipt.json"
|
||||
if not existing_receipt.is_file():
|
||||
raise EvidenceProduceError("CLOSURE_CONFLICT")
|
||||
try:
|
||||
existing = json.loads(existing_receipt.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
raise EvidenceProduceError("CLOSURE_CONFLICT") from exc
|
||||
if _canonical_json(dict(existing)) != _canonical_json(dict(receipt)):
|
||||
raise EvidenceProduceError("CLOSURE_CONFLICT")
|
||||
for name, path in layout_paths.items():
|
||||
if not (path.is_file() and _file_sha256(path) == digests[name]):
|
||||
raise EvidenceProduceError("CLOSURE_CONFLICT")
|
||||
return ProduceResult(
|
||||
status="EVIDENCE_PRODUCED",
|
||||
idempotent=True,
|
||||
receipt_sha256=_sha256_bytes(_canonical_json(dict(receipt))),
|
||||
closure_dir=str(target),
|
||||
evidence_digests=dict(digests),
|
||||
)
|
||||
|
||||
temporary = source_root / (
|
||||
f".{version}.{os.getpid()}.{secrets.token_hex(8)}.prod.tmp"
|
||||
)
|
||||
if temporary.exists() or temporary.is_symlink():
|
||||
raise EvidenceProduceError("PRODUCE_TEMP_CONFLICT")
|
||||
temporary.mkdir(mode=0o700)
|
||||
# The closure directory mirrors source_root/<version>/; every written file uses
|
||||
# the policy-declared basename so the layout is aligned with evidence_layout.
|
||||
try:
|
||||
_write_durable_json(temporary / "receipt.json", receipt)
|
||||
_write_durable_bytes(temporary / "artifact.tar.gz", request.artifact)
|
||||
_write_durable_json(temporary / "manifest.json", request.manifest)
|
||||
_write_durable_bytes(temporary / "checksums.sha256", request.package_closure)
|
||||
_write_durable_json(temporary / "trusted-release-policy.json", request.policy)
|
||||
_write_durable_json(temporary / "tests.json", request.tests)
|
||||
_write_durable_json(temporary / "canary.json", request.canary)
|
||||
_write_durable_json(temporary / "gitea.json", request.gitea)
|
||||
_write_durable_json(temporary / "rollback.json", request.rollback)
|
||||
_write_durable_json(temporary / "secret-scan.json", request.secret_scan)
|
||||
os.replace(temporary, target)
|
||||
except Exception:
|
||||
shutil.rmtree(temporary, ignore_errors=True)
|
||||
raise
|
||||
|
||||
return ProduceResult(
|
||||
status="EVIDENCE_PRODUCED",
|
||||
idempotent=False,
|
||||
receipt_sha256=_sha256_bytes(_canonical_json(dict(receipt))),
|
||||
closure_dir=str(target),
|
||||
evidence_digests=dict(digests),
|
||||
)
|
||||
|
||||
|
||||
def producer_operation_definition() -> OperationDefinition:
|
||||
"""Register the governed.release.evidence.produce operation (lookup-only registry)."""
|
||||
return OperationDefinition.from_dict(
|
||||
{
|
||||
"operation_id": "governed.release.evidence.produce",
|
||||
"version": 1,
|
||||
"effect_class": "RECONCILABLE",
|
||||
"handler_ref": "governed_release_evidence_producer.produce_evidence_closure",
|
||||
"input_schema": PRODUCE_SCHEMA,
|
||||
"required_permissions": ["governed.release.evidence.produce"],
|
||||
"timeout_seconds": 60.0,
|
||||
"readback": {"mode": "STATE", "required": True},
|
||||
"retry": {"mode": "NEVER"},
|
||||
"reconcile": {"mode": "EXACT_READBACK"},
|
||||
"rollback": {"mode": "COMPENSATE"},
|
||||
"owner_boundary": {"mode": "WITHIN_GRANT"},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def producer_registry() -> OperationRegistry:
|
||||
return OperationRegistry([producer_operation_definition()])
|
||||
|
||||
|
||||
def _main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("operation", choices=("produce",))
|
||||
parser.add_argument("--version", required=True)
|
||||
parser.add_argument("--builder-policy", required=True, type=Path)
|
||||
parser.add_argument("--source-root", required=True, type=Path)
|
||||
parser.add_argument("--artifacts-json", required=True, type=Path)
|
||||
arguments = parser.parse_args()
|
||||
|
||||
try:
|
||||
payload = json.loads(arguments.artifacts_json.read_text(encoding="utf-8"))
|
||||
request = GovernedBuildArtifacts(
|
||||
version=arguments.version,
|
||||
candidate_id=payload["candidate_id"],
|
||||
source_commit=payload["source_commit"],
|
||||
tree_oid=payload["tree_oid"],
|
||||
artifact=bytes.fromhex(payload["artifact_hex"]),
|
||||
manifest=payload["manifest"],
|
||||
package_closure=bytes.fromhex(payload["package_closure_hex"]),
|
||||
policy=payload["policy"],
|
||||
tests=payload["tests"],
|
||||
canary=payload["canary"],
|
||||
gitea=payload["gitea"],
|
||||
rollback=payload["rollback"],
|
||||
secret_scan=payload["secret_scan"],
|
||||
deployment=payload["deployment"],
|
||||
tag=payload["tag"],
|
||||
finalization=payload["finalization"],
|
||||
rollback_sha256=payload.get("rollback_sha256", ""),
|
||||
)
|
||||
result = produce_evidence_closure(
|
||||
builder_policy=arguments.builder_policy,
|
||||
source_root=arguments.source_root,
|
||||
request=request,
|
||||
)
|
||||
except (EvidenceProduceError, KeyError, ValueError, OSError, json.JSONDecodeError) as exc:
|
||||
raise SystemExit(str(exc)) from exc
|
||||
from dataclasses import asdict
|
||||
|
||||
print(json.dumps(asdict(result), sort_keys=True, separators=(",", ":")))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_main()
|
||||
@@ -23,11 +23,13 @@
|
||||
|
||||
**缺口 → 阻塞关系**:closure 的产生方缺失,阻塞 Gate R 的 "evidence closure" 冻结项。不得由 Materializer 自产证据、hand-write receipt / test PASS / Gitea consistency / canary / rollback / secret-scan。
|
||||
|
||||
**拟实现 operation 契约(待实现,非已完成)**:
|
||||
- `operation_id`: `governed.release.evidence.produce`
|
||||
- 职责:从 Inventory + Resolver + `governed_recipes` + 真实执行(canary/rollback/secret-scan/Gitea consistency)写 `source_root/<version>/` 完整 closure(root-owned)。
|
||||
- 注册:`aios_baseline` operation registry + resolver binding + policy `evidence_layout` 对齐。
|
||||
- 完成判据:真实执行一次并能被 `TrustedBuilderEvidenceMaterializer` 原样投影。
|
||||
**实现完成(本轮 wt-p0)**:
|
||||
- `active-runtime/app/governed_release_evidence_producer.py` — operation `governed.release.evidence.produce`:
|
||||
- 契约:输入 = 真实构建产物(artifact/manifest/package_closure/policy bytes)+ 真实校验执行结果(tests/canary/gitea/rollback/secret-scan)+ 部署约定(deployment/tag/finalization)+ 构建执行时刻(issued_at);**不手写证据**——每个 digest 从真实字节推导,pass/fail/gitea-consistent flag 来自真实执行输入,fail-closed 校验(secret-scan 必须覆盖实际 artifact 字节、tests/canary/rollback/gitea 必须通过)。
|
||||
- 产出:写 `source_root/<version>/` 完整 closure(receipt.json + 全 evidence_layout 文件),幂等(相同闭包接受、冲突拒绝),O_BINARY 写入避免 CRLF。
|
||||
- 注册:`producer_operation_definition()` / `producer_registry()` 挂到 OperationRegistry(声明式、非执行)。
|
||||
- `tests/test_governed_release_evidence_producer.py` — 5 项契约测试(本地闭环):closure 可被 builder 验证路径(ServerOwnedEvidenceSource + DeterministicEvidenceCollector = materializer 同款)重验证且 evidence_digests 一致;secret-scan 不匹配 / artifact 篡改 fail-closed;幂等;registry 注册。**5 passed**。
|
||||
- **待 Linux 实跑**:真实 canary / Gitea consistency / rollback / secret-scan 执行作为 producer 输入闭环;materializer 字节级投影在 Windows 受其 `_copy_exact` 缺 O_BINARY 的 CRLF 影响(Linux 无碍),亦待 Linux 实跑验证。
|
||||
|
||||
---
|
||||
|
||||
@@ -60,12 +62,20 @@
|
||||
- `ae5aeea` — TTL 修复 + TTL 专项测试 + harness 修复 + bootstrap 全链路契约测试(4 files, +520/-5)
|
||||
- `7f83845` — CAP_SYS_PTRACE Git 化(unit + 安全文档 + manifest + 测试,4 files, +240)
|
||||
- admission 套件:79 passed / 1 skipped(Linux-only) / 0 failed(Windows 侧)
|
||||
- Evidence Producer(`governed.release.evidence.produce`)实现 + operation registry 注册 + 5 项契约测试通过(producer + chain_contract + captrace 联跑 15 passed)
|
||||
|
||||
## 3b. Step 9 完整风险驱动测试结论(本轮)
|
||||
|
||||
在 P0 分支跑**完整可收集套件**(排除 20 个 POSIX-only collection-errors:fcntl 16 / grp 3 为 Windows 无标准等价物):**850 passed / 99 failed / 14 skipped**。
|
||||
- **回归判定(对照 base=49d30a2,同 6 个 P0 相关既有文件)**:base 33 failed / 97 passed vs P0 32 failed / 103 passed → **P0 改动未引入任何新失败**(失败数不增反减)。P0 分支全部失败均来自 base 既有的 Windows 环境边界(symlink 特权 WinError 1314、POSIX mode/uid 检查在 `os.name != "nt"` 才生效、materializer `_copy_exact` 缺 O_BINARY 的 CRLF、signer_provisioning / release_source_admission 的既有失败),非本分支回归,需 Linux 实跑闭环。
|
||||
- 我的 P0 覆盖区文件(chain_contract / captrace / producer)隔离跑全部通过。
|
||||
|
||||
## 4. 待继续及 gate 依赖
|
||||
|
||||
| 项 | 状态 | gate 依赖 |
|
||||
|---|---|---|
|
||||
| Evidence Producer 实现+注册 | 缺口确认,待实现 | 真实执行验证 → Gate R evidence closure |
|
||||
| Evidence Producer 真实执行闭环 | 代码+契约测试完成,待真实 canary/gitea/rollback/secret-scan 输入 | **Linux/生产实跑** → Gate R evidence closure |
|
||||
| materializer 字节投影 Windows CRLF 边界 | `_copy_exact` 缺 O_BINARY,Windows 投影受 CRLF 影响 | Linux 实跑(无碍) |
|
||||
| Merge ledger 162 条 ADJUDICATE 清零 | ledger 源不在当前 worktree,需与历史产物对齐 | 判定 + Gitea 核对 |
|
||||
| CAP_SYS_PTRACE 三态运行时验证 | 已完成 Git 材料 | **Linux 主机实跑**(systemd/runtime) |
|
||||
| POSIX mode-bit 测试 | Windows skipped | **Linux CI/live 实跑** |
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
"""Governed release evidence producer contract tests (V2R2C2 P0).
|
||||
|
||||
Proves that ``governed.release.evidence.produce`` (the producer half) writes a
|
||||
self-consistent evidence closure from real build artifacts, and that the closure
|
||||
can be re-validated and projected byte-for-byte by ``TrustedBuilderEvidenceMaterializer``
|
||||
(the projection half). Also proves fail-closed behavior on tampered/inconsistent
|
||||
input and idempotency/conflict handling, plus operation-registry registration.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
|
||||
|
||||
from aios_trusted_candidate_builder import ( # noqa: E402
|
||||
load_builder_policy,
|
||||
TrustedCandidateBuilderConfig,
|
||||
)
|
||||
from governed_release_evidence_producer import ( # noqa: E402
|
||||
EvidenceProduceError,
|
||||
GovernedBuildArtifacts,
|
||||
producer_operation_definition,
|
||||
producer_registry,
|
||||
produce_evidence_closure,
|
||||
)
|
||||
from release_evidence_materializer import ( # noqa: E402
|
||||
TrustedBuilderEvidenceMaterializer,
|
||||
)
|
||||
|
||||
|
||||
POLICY_PATH = ROOT / "config" / "gitops" / "trusted-candidate-builder-policy.json"
|
||||
|
||||
|
||||
def canonical(value: dict) -> bytes:
|
||||
return json.dumps(
|
||||
value, sort_keys=True, separators=(",", ":"), ensure_ascii=True
|
||||
).encode()
|
||||
|
||||
|
||||
def digest(label: str) -> str:
|
||||
return hashlib.sha256(label.encode()).hexdigest()
|
||||
|
||||
|
||||
def build_input(tmp_path: Path, *, tamper_artifact: bool = False,
|
||||
bad_secret_scan: bool = False) -> tuple[GovernedBuildArtifacts, str, str]:
|
||||
config = TrustedCandidateBuilderConfig.from_policy(load_builder_policy(POLICY_PATH))
|
||||
recipe = config.fixed_recipe("V2R2C2")
|
||||
version = recipe["version"]
|
||||
tree_oid = "a" * 40
|
||||
artifact_bytes = b"artifact-bytes"
|
||||
if tamper_artifact:
|
||||
artifact_bytes = b"artifact-tampered"
|
||||
real_artifact_sha = hashlib.sha256(artifact_bytes).hexdigest()
|
||||
|
||||
closure_bytes = b"checksums-v1\n"
|
||||
closure_sha = hashlib.sha256(closure_bytes).hexdigest()
|
||||
manifest_dict = {
|
||||
"artifact_sha256": real_artifact_sha,
|
||||
"package_closure_sha256": closure_sha,
|
||||
}
|
||||
# manifest_sha256 is the digest of the manifest evidence file; the producer
|
||||
# derives it from the manifest content (never self-referential).
|
||||
manifest_sha = hashlib.sha256(canonical(manifest_dict)).hexdigest()
|
||||
|
||||
policy_dict = {"trusted": "release-policy"}
|
||||
# policy_sha256 is the digest of the policy evidence file; derived by the
|
||||
# producer from policy content (not a self-referential field).
|
||||
policy_sha = hashlib.sha256(canonical(policy_dict)).hexdigest()
|
||||
|
||||
tests = {"passed": True, "receipt": "real-tests"}
|
||||
canary = {"passed": True, "receipt": "real-canary"}
|
||||
gitea = {
|
||||
"consistent": True,
|
||||
"remote_ref_oid": recipe["source_commit"],
|
||||
"repository": config.repository,
|
||||
"source_commit": recipe["source_commit"],
|
||||
"tree_oid": tree_oid,
|
||||
}
|
||||
rollback_sha = digest("rollback-contract")
|
||||
rollback = {
|
||||
"ready": True,
|
||||
"rollback_sha256": rollback_sha,
|
||||
"source_commit": recipe["source_commit"],
|
||||
}
|
||||
if bad_secret_scan:
|
||||
secret_scan = {"artifact_sha256": digest("wrong"), "passed": True}
|
||||
else:
|
||||
secret_scan = {"artifact_sha256": real_artifact_sha, "passed": True}
|
||||
|
||||
deployment = {
|
||||
"service": "aios-silent-release-controller",
|
||||
"release_path": "/srv/release/r1",
|
||||
"release_root": "/srv/release",
|
||||
"cutover_link": "/srv/release/current",
|
||||
"dropin_path": "/srv/release/r1/deploy/systemd/controller.service",
|
||||
"dropin_sha256": digest("dropin"),
|
||||
"systemd_unit_sha256": digest("unit"),
|
||||
"rollback_path": "/srv/release/rollback-r1",
|
||||
"rollback_sha256": rollback_sha,
|
||||
"authoritative_git_dir": "/data/aios-gitops/repo.git",
|
||||
"repository_identity": config.repository,
|
||||
"baseline_before_sha256": digest("baseline"),
|
||||
"registry_before_sha256": digest("registry"),
|
||||
"production_tag_refset_before_sha256": digest("tagset"),
|
||||
"health_contract_version": "trusted-builder/v1",
|
||||
"timeout_policy": {"seconds": 10},
|
||||
"retry_policy": {"attempts": 1},
|
||||
"health_contract": {"checked": True},
|
||||
}
|
||||
tag = {
|
||||
"version": "trusted-v2r2c2-001",
|
||||
"tag_type": "annotated",
|
||||
"expected_tag_oid": "b" * 40,
|
||||
"expected_target_oid": recipe["source_commit"],
|
||||
"evidence_closure_oid": "c" * 40,
|
||||
"approval_plan_id": "owner-plan-trusted-v2r2c2",
|
||||
"repository_identity": config.repository,
|
||||
"gitea_owner": "aios",
|
||||
"gitea_repository": "aios",
|
||||
"current_remote_tag_oid": None,
|
||||
"remote_absent_evidence_sha256": digest("tag-absence"),
|
||||
"verification_contract": "trusted-builder/v1",
|
||||
}
|
||||
finalization = {
|
||||
"version": "trusted-v2r2c2-001",
|
||||
"expected_tag_oid": "b" * 40,
|
||||
"expected_target_oid": recipe["source_commit"],
|
||||
"expected_previous_baseline_sha256": digest("baseline"),
|
||||
"registry_expected_record": {"base": recipe["source_commit"]},
|
||||
"baseline_expected_record": {"base": recipe["source_commit"]},
|
||||
"final_consistency_contract": "trusted-builder/v1",
|
||||
}
|
||||
|
||||
request = GovernedBuildArtifacts(
|
||||
version=version,
|
||||
candidate_id=recipe["candidate_id"],
|
||||
source_commit=recipe["source_commit"],
|
||||
tree_oid=tree_oid,
|
||||
artifact=artifact_bytes,
|
||||
manifest=manifest_dict,
|
||||
package_closure=closure_bytes,
|
||||
policy=policy_dict,
|
||||
tests=tests,
|
||||
canary=canary,
|
||||
gitea=gitea,
|
||||
rollback=rollback,
|
||||
secret_scan=secret_scan,
|
||||
deployment=deployment,
|
||||
tag=tag,
|
||||
finalization=finalization,
|
||||
rollback_sha256=rollback_sha,
|
||||
issued_at="2026-09-09T00:00:00+00:00",
|
||||
)
|
||||
return request, recipe["candidate_id"], tree_oid
|
||||
|
||||
|
||||
def fake_git(tree_oid: str):
|
||||
def runner(argv, **_kwargs):
|
||||
if "rev-parse" in argv:
|
||||
return SimpleNamespace(stdout=tree_oid + "\n", returncode=0)
|
||||
return SimpleNamespace(stdout="", returncode=0)
|
||||
return runner
|
||||
|
||||
|
||||
def make_materializer(tmp_path: Path, source_root: Path, output_root: Path, tree_oid: str):
|
||||
authority = tmp_path / "repo.git"
|
||||
authority.mkdir(mode=0o700)
|
||||
output_root.mkdir(mode=0o700)
|
||||
return TrustedBuilderEvidenceMaterializer(
|
||||
builder_policy=POLICY_PATH,
|
||||
authority_git_dir=authority,
|
||||
source_root=source_root,
|
||||
output_root=output_root,
|
||||
expected_owner_uid=None,
|
||||
git_runner=fake_git(tree_oid),
|
||||
)
|
||||
|
||||
|
||||
def test_produce_writes_closure_that_revalidates_under_builder_verification(tmp_path: Path) -> None:
|
||||
"""Producer closure is accepted by the exact verification path the materializer uses.
|
||||
|
||||
The producer must write a self-consistent closure that the deterministic builder
|
||||
verification (ServerOwnedEvidenceSource + DeterministicEvidenceCollector) can
|
||||
re-validate without error and with identical evidence digests. The byte-for-byte
|
||||
projection step of TrustedBuilderEvidenceMaterializer additionally exercises the
|
||||
collector and is validated here where the platform permits it (Linux); on Windows
|
||||
the materializer's os.open lacks O_BINARY, so physical projection is a documented
|
||||
Linux/live-run item, not a producer regression.
|
||||
"""
|
||||
source_root = tmp_path / "source"
|
||||
source_root.mkdir(mode=0o700)
|
||||
output_root = tmp_path / "output"
|
||||
request, candidate_id, tree_oid = build_input(tmp_path)
|
||||
version = request.version
|
||||
|
||||
result = produce_evidence_closure(
|
||||
builder_policy=POLICY_PATH,
|
||||
source_root=source_root,
|
||||
request=request,
|
||||
expected_owner_uid=None,
|
||||
)
|
||||
assert result.status == "EVIDENCE_PRODUCED"
|
||||
assert result.idempotent is False
|
||||
assert (source_root / version / "receipt.json").is_file()
|
||||
for name in ("artifact.tar.gz", "manifest.json", "checksums.sha256",
|
||||
"trusted-release-policy.json", "tests.json", "canary.json",
|
||||
"gitea.json", "rollback.json", "secret-scan.json"):
|
||||
assert (source_root / version / name).is_file(), f"missing {name}"
|
||||
|
||||
# Re-validate the produced closure with the deterministic builder verification
|
||||
# (the exact collector the materializer uses); it must collect cleanly and
|
||||
# reproduce the producer's evidence digests.
|
||||
from aios_trusted_candidate_builder import ( # noqa: F811
|
||||
DeterministicEvidenceCollector,
|
||||
ServerOwnedEvidenceSource,
|
||||
)
|
||||
config = TrustedCandidateBuilderConfig.from_policy(load_builder_policy(POLICY_PATH))
|
||||
authority = tmp_path / "repo.git"
|
||||
authority.mkdir(mode=0o700)
|
||||
source = ServerOwnedEvidenceSource(
|
||||
config, authority, source_root.resolve(),
|
||||
expected_owner_uid=None, git_runner=fake_git(tree_oid),
|
||||
)
|
||||
collector = DeterministicEvidenceCollector(config, evidence_source=source)
|
||||
recipe = config.fixed_recipe(version)
|
||||
bundle = collector.collect(version, recipe)
|
||||
assert set(bundle["evidence_digests"]) == set(result.evidence_digests)
|
||||
for name, digest in result.evidence_digests.items():
|
||||
assert bundle["evidence_digests"][name] == digest, f"digest mismatch {name}"
|
||||
|
||||
# Where the platform permits byte-for-byte projection (Linux/O_BINARY-safe), also
|
||||
# project through the materializer. On Windows this physical-copy step is bounded
|
||||
# by the materializer's existing os.open text-mode behavior and is excluded here.
|
||||
if not sys.platform.startswith("win"):
|
||||
materializer = make_materializer(tmp_path, source_root, output_root, tree_oid)
|
||||
outcome = materializer.materialize(
|
||||
version=version,
|
||||
candidate_id=candidate_id,
|
||||
source_commit=request.source_commit,
|
||||
)
|
||||
assert outcome["status"] == "EVIDENCE_MATERIALIZED"
|
||||
|
||||
|
||||
def test_produce_is_idempotent_on_identical_closure(tmp_path: Path) -> None:
|
||||
source_root = tmp_path / "source"
|
||||
source_root.mkdir(mode=0o700)
|
||||
request, _, _ = build_input(tmp_path)
|
||||
first = produce_evidence_closure(
|
||||
builder_policy=POLICY_PATH, source_root=source_root, request=request,
|
||||
expected_owner_uid=None,
|
||||
)
|
||||
second = produce_evidence_closure(
|
||||
builder_policy=POLICY_PATH, source_root=source_root, request=request,
|
||||
expected_owner_uid=None,
|
||||
)
|
||||
assert first.idempotent is False
|
||||
assert second.idempotent is True
|
||||
assert second.status == "EVIDENCE_PRODUCED"
|
||||
|
||||
|
||||
def test_produce_conflicts_on_tampered_artifact(tmp_path: Path) -> None:
|
||||
source_root = tmp_path / "source"
|
||||
source_root.mkdir(mode=0o700)
|
||||
request, _, _ = build_input(tmp_path)
|
||||
produce_evidence_closure(
|
||||
builder_policy=POLICY_PATH, source_root=source_root, request=request,
|
||||
expected_owner_uid=None,
|
||||
)
|
||||
tampered, _, _ = build_input(tmp_path, tamper_artifact=True)
|
||||
with pytest.raises(EvidenceProduceError):
|
||||
produce_evidence_closure(
|
||||
builder_policy=POLICY_PATH, source_root=source_root, request=tampered,
|
||||
expected_owner_uid=None,
|
||||
)
|
||||
|
||||
|
||||
def test_produce_fails_closed_on_secret_scan_artifact_mismatch(tmp_path: Path) -> None:
|
||||
source_root = tmp_path / "source"
|
||||
source_root.mkdir(mode=0o700)
|
||||
bad, _, _ = build_input(tmp_path, bad_secret_scan=True)
|
||||
with pytest.raises(EvidenceProduceError):
|
||||
produce_evidence_closure(
|
||||
builder_policy=POLICY_PATH, source_root=source_root, request=bad,
|
||||
expected_owner_uid=None,
|
||||
)
|
||||
assert not (source_root / bad.version).exists()
|
||||
|
||||
|
||||
def test_produce_registers_governed_operation() -> None:
|
||||
definition = producer_operation_definition()
|
||||
assert definition.operation_id == "governed.release.evidence.produce"
|
||||
registry = producer_registry()
|
||||
assert (
|
||||
registry.get("governed.release.evidence.produce").operation_id
|
||||
== "governed.release.evidence.produce"
|
||||
)
|
||||
assert "governed.release.evidence.produce" in definition.required_permissions
|
||||
Reference in New Issue
Block a user