From b7a2e630b8740b5a6c0f61ae44cc47bf77a7ffd2 Mon Sep 17 00:00:00 2001 From: Ning Zhou Date: Fri, 31 Jul 2026 14:40:30 +0800 Subject: [PATCH] feat: gate protected real feature reexecution --- .github/workflows/ci.yml | 5 + ...protected_real_feature_reexecution_gate.py | 491 ++++++++++++++++++ ...protected_real_feature_reexecution_gate.py | 175 +++++++ ...protected-real-feature-reexecution-gate.md | 78 +++ ...l-feature-reexecution-gate-2026-07-31.json | 280 ++++++++++ docs/roadmap.md | 4 +- docs/system-of-record-matrix-2026-07-24.md | 13 +- ...protected-real-feature-reexecution-gate.sh | 22 + 8 files changed, 1061 insertions(+), 7 deletions(-) create mode 100644 data_agent/metadata_fabric_protected_real_feature_reexecution_gate.py create mode 100644 data_agent/test_metadata_fabric_protected_real_feature_reexecution_gate.py create mode 100644 docs/architecture-decisions/adr-072-protected-real-feature-reexecution-gate.md create mode 100644 docs/evidence/metadata-fabric-protected-real-feature-reexecution-gate-2026-07-31.json create mode 100755 scripts/metadata-fabric-protected-real-feature-reexecution-gate.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 407eb9ea..3fbd202c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,7 @@ on: - feat/ar1-metadata-fabric-real-feature-ingestion - feat/ar1-metadata-fabric-real-feature-ledger-promotion - feat/ar1-metadata-fabric-retained-real-feature-terminal-success + - feat/ar1-metadata-fabric-retained-real-feature-restart-recovery env: PYTHON_VERSION: "3.13" @@ -212,6 +213,9 @@ jobs: - name: Validate retained real-feature restart recovery evidence run: python -m data_agent.metadata_fabric_retained_real_feature_restart_recovery validate + - name: Validate protected real-feature re-execution decision + run: python -m data_agent.metadata_fabric_protected_real_feature_reexecution_gate validate + - name: Validate Active Metadata consumer deployment boundary run: python -m data_agent.active_metadata_consumer_deployment validate @@ -311,6 +315,7 @@ jobs: data_agent/test_metadata_fabric_real_feature_ledger_promotion.py \ data_agent/test_metadata_fabric_retained_real_feature_terminal_success.py \ data_agent/test_metadata_fabric_retained_real_feature_restart_recovery.py \ + data_agent/test_metadata_fabric_protected_real_feature_reexecution_gate.py \ data_agent/test_metadata_fabric_lineage_delivery.py \ data_agent/test_metadata_fabric_provider_identity.py \ data_agent/test_metadata_fabric_gravitino_identity.py \ diff --git a/data_agent/metadata_fabric_protected_real_feature_reexecution_gate.py b/data_agent/metadata_fabric_protected_real_feature_reexecution_gate.py new file mode 100644 index 00000000..9085596d --- /dev/null +++ b/data_agent/metadata_fabric_protected_real_feature_reexecution_gate.py @@ -0,0 +1,491 @@ +"""Gate protected re-execution of the checked real Chongqing feature input. + +M3-26 composes the production identity and object-store readiness gates with +the immutable M3-25 predecessor. It never promotes local retained material and +never authorizes scheduler or provider mutations. A successful decision only +means that a separately approved execution may re-ingest the same content in a +protected environment. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import re +from collections.abc import Mapping +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + +from . import ( + metadata_fabric_identity_gate as identity_gate, +) +from . import ( + metadata_fabric_object_store_gate as object_store_gate, +) +from . import ( + metadata_fabric_retained_real_feature_restart_recovery as predecessor, +) + +CONTRACT_SCHEMA = "gda.protected_real_feature_reexecution_contract.v1" +DECISION_SCHEMA = "gda.protected_real_feature_reexecution_decision.v1" +VALIDATION_SCHEMA = "gda.protected_real_feature_reexecution_validation.v1" + +REPO_ROOT = Path(__file__).resolve().parent.parent +DEFAULT_SOURCE_EVIDENCE_PATH = ( + REPO_ROOT + / "docs/evidence/metadata-fabric-retained-real-feature-restart-recovery-2026-07-31.json" +) +DEFAULT_DECISION_PATH = ( + REPO_ROOT + / "docs/evidence/metadata-fabric-protected-real-feature-reexecution-gate-2026-07-31.json" +) + +SOURCE_EVIDENCE_FILE_SHA256 = ( + "6880ff81dcde37f824ab3c7d04f62863375d5a6f1ada2a2dbfa832e77da7cfb1" +) +SOURCE_EVIDENCE_SHA256 = ( + "1b5a5ceeadee88868bab6237b3f3280c8b13793cc54193592fec7dbbfdd4e8a6" +) +SOURCE_RETENTION_ID = "m3-24-229740ac50ebb53b" + +READY_STATUS = "ready_for_protected_reexecution" +BLOCKED_STATUS = "blocked_pending_protected_attestation" +SHA40_PATTERN = re.compile(r"^[0-9a-f]{40}$") +SENSITIVE_KEY_PATTERN = re.compile( + r"(^|[-_.])(password|passwd|secret|client[-_.]?secret|private[-_.]?key|" + r"access[-_.]?key|access[-_.]?token|refresh[-_.]?token|authorization[-_.]?header)" + r"($|[-_.])", + re.IGNORECASE, +) + +DECISION_INVENTORY = { + "schema", + "status", + "evaluated_at", + "contract_sha256", + "source_binding", + "identity_report", + "object_store_report", + "identity_attestation", + "object_store_attestation", + "blockers", + "checked_real_feature_predecessor_verified", + "production_profiles_valid", + "protected_identity_attested", + "protected_object_store_attested", + "cross_gate_source_revision_aligned", + "protected_tenant_controls_attested", + "ready_for_protected_reexecution", + "fresh_protected_ingestion_required", + "local_retained_material_dependency", + "source_payload_dependency", + "local_material_promotion_allowed", + "scheduler_submission_authorized", + "provider_mutation_authorized", + "production_ingestion_verified", + "production_ready", + "decision_sha256", +} + + +class ProtectedRealFeatureReexecutionGateError(RuntimeError): + """The protected real-feature re-execution gate failed closed.""" + + +def _mapping(value: Any) -> Mapping[str, Any]: + return value if isinstance(value, Mapping) else {} + + +def canonical_json_fingerprint(value: Any) -> str: + payload = json.dumps( + value, ensure_ascii=True, sort_keys=True, separators=(",", ":") + ).encode("utf-8") + return hashlib.sha256(payload).hexdigest() + + +def _file_sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _load_json_object(path: Path) -> dict[str, Any]: + value = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise TypeError("JSON document is not an object") + return value + + +def _parse_time(value: Any, *, label: str) -> datetime: + try: + parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00")) + except ValueError as exc: + raise ProtectedRealFeatureReexecutionGateError( + f"{label} is not a valid timestamp" + ) from exc + if parsed.tzinfo is None or parsed.utcoffset() is None: + raise ProtectedRealFeatureReexecutionGateError( + f"{label} must be timezone-aware" + ) + return parsed.astimezone(UTC) + + +def _source_evidence(path: Path = DEFAULT_SOURCE_EVIDENCE_PATH) -> dict[str, Any]: + if _file_sha256(path) != SOURCE_EVIDENCE_FILE_SHA256: + raise ProtectedRealFeatureReexecutionGateError( + "M3-25 source evidence file fingerprint does not match" + ) + source = _load_json_object(path) + errors = predecessor.validate_evidence(source) + if errors: + raise ProtectedRealFeatureReexecutionGateError( + "M3-25 source evidence is invalid: " + "; ".join(errors) + ) + if source.get("evidence_sha256") != SOURCE_EVIDENCE_SHA256: + raise ProtectedRealFeatureReexecutionGateError( + "M3-25 source evidence fingerprint does not match" + ) + if source.get("retention_id") != SOURCE_RETENTION_ID: + raise ProtectedRealFeatureReexecutionGateError( + "M3-25 source retention identity does not match" + ) + return source + + +def build_source_binding( + path: Path = DEFAULT_SOURCE_EVIDENCE_PATH, +) -> dict[str, Any]: + source = _source_evidence(path) + material = _mapping(_mapping(source.get("material")).get("after")) + quality = _mapping(_mapping(source.get("independent_quality")).get("after")) + ledger = _mapping(_mapping(source.get("control_ledger")).get("after_terminal_replay")) + return { + "source_evidence_path": str(path.relative_to(REPO_ROOT)), + "source_evidence_file_sha256": SOURCE_EVIDENCE_FILE_SHA256, + "source_evidence_sha256": SOURCE_EVIDENCE_SHA256, + "tenant_id": source.get("tenant_id"), + "run_id": source.get("run_id"), + "output_resource_version_id": source.get("output_resource_version_id"), + "output_content_sha256": source.get("output_content_sha256"), + "retention_id": source.get("retention_id"), + "retention_expires_at": source.get("retention_expires_at"), + "snapshot_id": material.get("snapshot_id"), + "object_inventory_sha256": material.get("object_inventory_sha256"), + "data_body_sha256": quality.get("data_body_sha256"), + "row_set_sha256": quality.get("row_set_sha256"), + "feature_count": _mapping(quality.get("metrics")).get("feature_count"), + "control_facts_sha256": ledger.get("facts_sha256"), + "platform_run_status": ledger.get("platform_run_status"), + "platform_run_state_version": ledger.get("platform_run_state_version"), + } + + +def build_contract_report() -> dict[str, Any]: + source_binding = build_source_binding() + identity_report = identity_gate.build_identity_readiness_report() + object_store_report = object_store_gate.build_object_store_readiness_report() + errors = [ + *identity_gate.verify_report_integrity(identity_report), + *object_store_gate.verify_report_integrity(object_store_report), + ] + if identity_report.get("profile_valid") is not True: + errors.append("production identity profile is invalid") + if object_store_report.get("profile_valid") is not True: + errors.append("production object-store profile is invalid") + stable = { + "schema": CONTRACT_SCHEMA, + "status": "valid" if not errors else "invalid", + "errors": errors, + "source_binding": source_binding, + "identity_profile_fingerprint": identity_report.get("profile_fingerprint"), + "object_store_profile_fingerprint": object_store_report.get( + "profile_fingerprint" + ), + "requires_protected_identity_attestation": True, + "requires_protected_object_store_attestation": True, + "requires_cross_gate_source_revision_alignment": True, + "requires_tenant_isolation_from_both_gates": True, + "fresh_protected_ingestion_required": True, + "local_material_promotion_forbidden": True, + "scheduler_submission_authorized": False, + "provider_mutation_authorized": False, + "production_ingestion_verified": False, + "production_ready": False, + } + return {**stable, "contract_sha256": canonical_json_fingerprint(stable)} + + +def _sensitive_paths(value: Any, prefix: str = "") -> list[str]: + findings: list[str] = [] + if isinstance(value, Mapping): + for key, item in value.items(): + path = f"{prefix}.{key}" if prefix else str(key) + if SENSITIVE_KEY_PATTERN.search(str(key)): + findings.append(path) + findings.extend(_sensitive_paths(item, path)) + elif isinstance(value, list): + for index, item in enumerate(value): + findings.extend(_sensitive_paths(item, f"{prefix}[{index}]")) + return findings + + +def _source_revision_alignment( + identity_attestation: Mapping[str, Any] | None, + object_store_attestation: Mapping[str, Any] | None, +) -> bool: + if identity_attestation is None or object_store_attestation is None: + return False + identity_revision = str(identity_attestation.get("source_revision") or "") + object_store_revision = str(object_store_attestation.get("source_revision") or "") + return bool( + SHA40_PATTERN.fullmatch(identity_revision) + and identity_revision == object_store_revision + ) + + +def _blockers( + identity_report: Mapping[str, Any], + object_store_report: Mapping[str, Any], + *, + source_revision_aligned: bool, +) -> list[str]: + blockers = [ + *(f"identity.profile:{item}" for item in identity_report.get("profile_blockers", [])), + *(f"identity.attestation:{item}" for item in identity_report.get("attestation_errors", [])), + *( + f"object_store.profile:{item}" + for item in object_store_report.get("profile_blockers", []) + ), + *( + f"object_store.attestation:{item}" + for item in object_store_report.get("attestation_errors", []) + ), + ] + if ( + identity_report.get("attestation_valid") is True + and object_store_report.get("attestation_valid") is True + and not source_revision_aligned + ): + blockers.append("cross_gate:source_revision_mismatch") + return sorted(set(str(item) for item in blockers)) + + +def build_decision( + *, + identity_attestation: Mapping[str, Any] | None = None, + object_store_attestation: Mapping[str, Any] | None = None, + identity_profile_path: Path = identity_gate.DEFAULT_PROFILE_PATH, + object_store_profile_path: Path = object_store_gate.DEFAULT_PROFILE_PATH, + source_evidence_path: Path = DEFAULT_SOURCE_EVIDENCE_PATH, + now: datetime | None = None, +) -> dict[str, Any]: + evaluated_at = (now or datetime.now(UTC)).astimezone(UTC) + source_binding = build_source_binding(source_evidence_path) + identity_report = identity_gate.build_identity_readiness_report( + profile_path=identity_profile_path, + attestation=identity_attestation, + now=evaluated_at, + ) + object_store_report = object_store_gate.build_object_store_readiness_report( + profile_path=object_store_profile_path, + attestation=object_store_attestation, + now=evaluated_at, + ) + aligned = _source_revision_alignment( + identity_attestation, object_store_attestation + ) + identity_passed = identity_report.get("production_identity_gate_passed") is True + object_store_passed = ( + object_store_report.get("production_object_store_gate_passed") is True + ) + tenant_attested = bool( + identity_passed + and object_store_passed + and object_store_report.get("tenant_isolation_verified") is True + ) + ready = bool(identity_passed and object_store_passed and aligned and tenant_attested) + blockers = _blockers( + identity_report, + object_store_report, + source_revision_aligned=aligned, + ) + contract = build_contract_report() + stable = { + "schema": DECISION_SCHEMA, + "status": READY_STATUS if ready else BLOCKED_STATUS, + "evaluated_at": evaluated_at.isoformat().replace("+00:00", "Z"), + "contract_sha256": contract["contract_sha256"], + "source_binding": source_binding, + "identity_report": identity_report, + "object_store_report": object_store_report, + "identity_attestation": ( + dict(identity_attestation) if identity_attestation is not None else None + ), + "object_store_attestation": ( + dict(object_store_attestation) + if object_store_attestation is not None + else None + ), + "blockers": blockers, + "checked_real_feature_predecessor_verified": True, + "production_profiles_valid": bool( + identity_report.get("profile_valid") is True + and object_store_report.get("profile_valid") is True + ), + "protected_identity_attested": identity_passed, + "protected_object_store_attested": object_store_passed, + "cross_gate_source_revision_aligned": aligned, + "protected_tenant_controls_attested": tenant_attested, + "ready_for_protected_reexecution": ready, + "fresh_protected_ingestion_required": True, + "local_retained_material_dependency": False, + "source_payload_dependency": False, + "local_material_promotion_allowed": False, + "scheduler_submission_authorized": False, + "provider_mutation_authorized": False, + "production_ingestion_verified": False, + "production_ready": False, + } + return {**stable, "decision_sha256": canonical_json_fingerprint(stable)} + + +def validate_decision(decision: Mapping[str, Any]) -> list[str]: + errors: list[str] = [] + if set(decision) != DECISION_INVENTORY: + errors.append("M3-26 decision inventory does not match") + stable = {key: value for key, value in decision.items() if key != "decision_sha256"} + if decision.get("decision_sha256") != canonical_json_fingerprint(stable): + errors.append("M3-26 decision fingerprint does not match") + try: + evaluated_at = _parse_time(decision.get("evaluated_at"), label="evaluated_at") + expected = build_decision( + identity_attestation=( + _mapping(decision.get("identity_attestation")) or None + ), + object_store_attestation=( + _mapping(decision.get("object_store_attestation")) or None + ), + now=evaluated_at, + ) + except (OSError, TypeError, ValueError, ProtectedRealFeatureReexecutionGateError) as exc: + errors.append(f"M3-26 decision inputs are invalid: {exc}") + expected = None + if expected is not None and dict(decision) != expected: + errors.append("M3-26 decision does not match current bound inputs") + identity_report = _mapping(decision.get("identity_report")) + object_store_report = _mapping(decision.get("object_store_report")) + errors.extend(identity_gate.verify_report_integrity(identity_report)) + errors.extend(object_store_gate.verify_report_integrity(object_store_report)) + if _sensitive_paths(decision): + errors.append("M3-26 decision contains credential-bearing fields") + for claim in ( + "local_material_promotion_allowed", + "scheduler_submission_authorized", + "provider_mutation_authorized", + "production_ingestion_verified", + "production_ready", + ): + if decision.get(claim) is not False: + errors.append(f"M3-26 decision may not claim {claim}") + if decision.get("fresh_protected_ingestion_required") is not True: + errors.append("M3-26 decision must require fresh protected ingestion") + if decision.get("local_retained_material_dependency") is not False: + errors.append("M3-26 decision may not depend on retained local material") + if decision.get("source_payload_dependency") is not False: + errors.append("M3-26 decision may not depend on deleted source payload") + return sorted(set(errors)) + + +def build_validation_report( + decision_path: Path = DEFAULT_DECISION_PATH, +) -> dict[str, Any]: + try: + decision = _load_json_object(decision_path) + errors = validate_decision(decision) + except (OSError, TypeError, ValueError, json.JSONDecodeError) as exc: + decision = {} + errors = [f"M3-26 decision is unreadable: {type(exc).__name__}"] + return { + "schema": VALIDATION_SCHEMA, + "status": "valid" if not errors else "invalid", + "errors": errors, + "contract_sha256": decision.get("contract_sha256"), + "decision_sha256": decision.get("decision_sha256"), + "ready_for_protected_reexecution": decision.get( + "ready_for_protected_reexecution" + ), + } + + +def _write_json(path: Path, payload: Mapping[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + subparsers = parser.add_subparsers(dest="command", required=True) + subparsers.add_parser("contract") + validate = subparsers.add_parser("validate") + validate.add_argument("--decision", type=Path, default=DEFAULT_DECISION_PATH) + snapshot = subparsers.add_parser("snapshot") + snapshot.add_argument("--output", type=Path, default=DEFAULT_DECISION_PATH) + evaluate = subparsers.add_parser("evaluate") + evaluate.add_argument("--identity-attestation", type=Path, required=True) + evaluate.add_argument("--object-store-attestation", type=Path, required=True) + evaluate.add_argument( + "--identity-profile", + type=Path, + default=identity_gate.DEFAULT_PROFILE_PATH, + ) + evaluate.add_argument( + "--object-store-profile", + type=Path, + default=object_store_gate.DEFAULT_PROFILE_PATH, + ) + evaluate.add_argument("--output", type=Path, required=True) + args = parser.parse_args(argv) + try: + if args.command == "contract": + report = build_contract_report() + exit_code = 0 if report["status"] == "valid" else 1 + elif args.command == "validate": + report = build_validation_report(args.decision) + exit_code = 0 if report["status"] == "valid" else 1 + elif args.command == "snapshot": + report = build_decision() + _write_json(args.output, report) + exit_code = 0 + else: + report = build_decision( + identity_attestation=_load_json_object(args.identity_attestation), + object_store_attestation=_load_json_object( + args.object_store_attestation + ), + identity_profile_path=args.identity_profile, + object_store_profile_path=args.object_store_profile, + ) + _write_json(args.output, report) + exit_code = 0 if report["ready_for_protected_reexecution"] else 1 + print(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True)) + return exit_code + except ( + OSError, + TypeError, + ValueError, + json.JSONDecodeError, + ProtectedRealFeatureReexecutionGateError, + ) as exc: + print(f"protected real-feature re-execution gate: {exc}") + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/data_agent/test_metadata_fabric_protected_real_feature_reexecution_gate.py b/data_agent/test_metadata_fabric_protected_real_feature_reexecution_gate.py new file mode 100644 index 00000000..3f015be6 --- /dev/null +++ b/data_agent/test_metadata_fabric_protected_real_feature_reexecution_gate.py @@ -0,0 +1,175 @@ +import json +from copy import deepcopy +from datetime import UTC, datetime + +import pytest + +from data_agent import metadata_fabric_protected_real_feature_reexecution_gate as gate + +EVALUATED_AT = datetime(2026, 7, 31, 6, 45, tzinfo=UTC) + + +def _checked_decision() -> dict: + return json.loads(gate.DEFAULT_DECISION_PATH.read_text(encoding="utf-8")) + + +def _ready_identity_report(attestation: dict) -> dict: + report = gate.identity_gate.build_identity_readiness_report(now=EVALUATED_AT) + report["profile_blockers"] = [] + report["ready_for_protected_verification"] = True + report["attestation_valid"] = True + report["attestation_errors"] = [] + report["attestation_fingerprint"] = gate.canonical_json_fingerprint(attestation) + for claim in gate.identity_gate.REPORT_CLAIMS: + report[claim] = True + stable = {key: value for key, value in report.items() if key != "report_fingerprint"} + report["report_fingerprint"] = gate.identity_gate.recovery._canonical_sha256(stable) + assert gate.identity_gate.verify_report_integrity(report) == [] + return report + + +def _ready_object_store_report(attestation: dict) -> dict: + report = gate.object_store_gate.build_object_store_readiness_report( + now=EVALUATED_AT + ) + report["profile_blockers"] = [] + report["ready_for_protected_verification"] = True + report["attestation_valid"] = True + report["attestation_errors"] = [] + report["attestation_fingerprint"] = gate.canonical_json_fingerprint(attestation) + for claim in gate.object_store_gate.REPORT_CLAIMS: + report[claim] = True + stable = {key: value for key, value in report.items() if key != "report_fingerprint"} + report["report_fingerprint"] = gate.object_store_gate.recovery._canonical_sha256( + stable + ) + assert gate.object_store_gate.verify_report_integrity(report) == [] + return report + + +def test_contract_binds_real_predecessor_and_keeps_execution_closed(): + contract = gate.build_contract_report() + + assert contract["status"] == "valid" + assert contract["errors"] == [] + assert contract["source_binding"]["source_evidence_sha256"] == ( + gate.SOURCE_EVIDENCE_SHA256 + ) + assert contract["source_binding"]["feature_count"] == 20 + assert contract["source_binding"]["platform_run_status"] == "succeeded" + assert contract["requires_protected_identity_attestation"] is True + assert contract["requires_protected_object_store_attestation"] is True + assert contract["fresh_protected_ingestion_required"] is True + assert contract["local_material_promotion_forbidden"] is True + assert contract["scheduler_submission_authorized"] is False + assert contract["provider_mutation_authorized"] is False + assert contract["production_ready"] is False + + +def test_checked_pending_decision_is_valid_and_exposes_exact_blockers(): + decision = _checked_decision() + validation = gate.build_validation_report() + + assert gate.validate_decision(decision) == [] + assert validation["status"] == "valid" + assert validation["errors"] == [] + assert decision["status"] == gate.BLOCKED_STATUS + assert decision["ready_for_protected_reexecution"] is False + assert decision["production_profiles_valid"] is True + assert len(decision["identity_report"]["profile_blockers"]) == 40 + assert len(decision["object_store_report"]["profile_blockers"]) == 43 + assert len(decision["blockers"]) == 85 + assert decision["identity_attestation"] is None + assert decision["object_store_attestation"] is None + + +def test_composition_requires_both_gates_and_one_source_revision(monkeypatch): + identity_attestation = {"source_revision": "a" * 40} + object_store_attestation = {"source_revision": "a" * 40} + identity_report = _ready_identity_report(identity_attestation) + object_store_report = _ready_object_store_report(object_store_attestation) + monkeypatch.setattr( + gate.identity_gate, + "build_identity_readiness_report", + lambda **_: deepcopy(identity_report), + ) + monkeypatch.setattr( + gate.object_store_gate, + "build_object_store_readiness_report", + lambda **_: deepcopy(object_store_report), + ) + + decision = gate.build_decision( + identity_attestation=identity_attestation, + object_store_attestation=object_store_attestation, + now=EVALUATED_AT, + ) + + assert decision["status"] == gate.READY_STATUS + assert decision["ready_for_protected_reexecution"] is True + assert decision["protected_tenant_controls_attested"] is True + assert decision["cross_gate_source_revision_aligned"] is True + assert decision["blockers"] == [] + assert decision["scheduler_submission_authorized"] is False + assert decision["provider_mutation_authorized"] is False + assert decision["production_ingestion_verified"] is False + assert decision["production_ready"] is False + + mismatched = gate.build_decision( + identity_attestation=identity_attestation, + object_store_attestation={"source_revision": "b" * 40}, + now=EVALUATED_AT, + ) + assert mismatched["status"] == gate.BLOCKED_STATUS + assert mismatched["ready_for_protected_reexecution"] is False + assert "cross_gate:source_revision_mismatch" in mismatched["blockers"] + + +def test_source_evidence_file_drift_fails_closed(tmp_path): + drifted = tmp_path / "source.json" + drifted.write_bytes(gate.DEFAULT_SOURCE_EVIDENCE_PATH.read_bytes() + b"\n") + + with pytest.raises( + gate.ProtectedRealFeatureReexecutionGateError, + match="source evidence file fingerprint does not match", + ): + gate.build_source_binding(drifted) + + +def test_outer_rehash_cannot_hide_nested_tampering_or_overclaim(): + decision = _checked_decision() + decision["identity_report"]["profile_fingerprint"] = "0" * 64 + decision["local_material_promotion_allowed"] = True + stable = { + key: value for key, value in decision.items() if key != "decision_sha256" + } + decision["decision_sha256"] = gate.canonical_json_fingerprint(stable) + + errors = gate.validate_decision(decision) + + assert "M3-26 decision does not match current bound inputs" in errors + assert "identity readiness report fingerprint does not match" in errors + assert "M3-26 decision may not claim local_material_promotion_allowed" in errors + + +def test_checked_decision_contains_no_local_path_or_feature_payload(): + rendered = gate.DEFAULT_DECISION_PATH.read_text(encoding="utf-8") + decision = json.loads(rendered) + + assert "/Users/" not in rendered + assert "Downloads/" not in rendered + assert "geometry_values" not in rendered + assert decision["local_retained_material_dependency"] is False + assert decision["source_payload_dependency"] is False + assert decision["fresh_protected_ingestion_required"] is True + + +def test_wrapper_is_strict_and_invokes_gate(): + wrapper = ( + gate.REPO_ROOT + / "scripts/metadata-fabric-protected-real-feature-reexecution-gate.sh" + ) + text = wrapper.read_text(encoding="utf-8") + + assert "set -euo pipefail" in text + assert "metadata_fabric_protected_real_feature_reexecution_gate" in text diff --git a/docs/architecture-decisions/adr-072-protected-real-feature-reexecution-gate.md b/docs/architecture-decisions/adr-072-protected-real-feature-reexecution-gate.md new file mode 100644 index 00000000..0ee8a695 --- /dev/null +++ b/docs/architecture-decisions/adr-072-protected-real-feature-reexecution-gate.md @@ -0,0 +1,78 @@ +# ADR-072: Protected real-feature re-execution gate + +**Status**: Accepted + +**Date**: 2026-07-31 + +**Decision owners**: Data Platform, Metadata Platform, Data Engineering, Security, SRE, Platform Architecture + +**Related decisions**: [ADR-053](adr-053-production-metadata-fabric-identity-readiness-gate.md) | [ADR-057](adr-057-production-object-store-readiness-gate.md) | [ADR-071](adr-071-retained-real-feature-restart-recovery.md) + +## Context + +M3-25 proved that one real Chongqing 20-feature authority remains byte-stable across ordered local process restart. Its namespace, PVC, MinIO, Gravitino and GDA Control database still share one development host and use local credentials and HTTP. The roadmap therefore requires protected identity, object storage and tenant attestation before another production-path execution can be considered. + +ADR-053 and ADR-057 already define fail-closed production identity and object-store gates, but they do not bind one another or a real feature predecessor. Evaluating them independently could accept attestations from different source revisions, and neither gate alone prevents an operator from treating the retained local material as a production promotion candidate. + +## Considered options + +### 1. Promote the retained local Iceberg material + +This would avoid re-ingestion, but it would carry a single-host MinIO/PVC authority and local identity into a protected environment. The physical material does not satisfy the production storage, tenancy or identity contracts. Rejected. + +### 2. Run another local backup or scale rehearsal first + +This could add local evidence, but it would bypass the roadmap order and leave the actual production identity/storage decision unresolved. Such rehearsals remain useful after the protected provider profile is selected, not as a substitute for that selection. Rejected as the next gate. + +### 3. Evaluate identity and object storage separately + +This reuses existing contracts with no new code, but does not bind both attestations to one source revision or the checked M3-25 ResourceVersion/content/quality/ledger predecessor. Rejected. + +### 4. Compose both gates around the immutable predecessor + +This makes the exact external blockers and cross-gate binding machine-verifiable while preserving the claim ceiling. Adopted as M3-26. + +## Decision + +### 1. Bind the gate to checked real-data facts + +M3-26 accepts only the checked M3-25 evidence file SHA `6880ff81dcde37f824ab3c7d04f62863375d5a6f1ada2a2dbfa832e77da7cfb1` and evidence SHA `1b5a5ceeadee88868bab6237b3f3280c8b13793cc54193592fec7dbbfdd4e8a6`. + +The decision records the source tenant and Run, output ResourceVersion/content SHA, Iceberg snapshot and object inventory, Parquet body and row-set SHA, 20-feature count, GDA Control facts SHA and `succeeded@3`. It contains no source path, feature payload or credential material. + +### 2. Require both protected production gates + +The gate re-evaluates the current identity and object-store profiles through their existing validators. Eligibility requires both fresh protected attestations to pass their complete checks, including workload identity, provider minimum privilege, TLS, KMS/durability, persistent catalog and cross-tenant denial. + +Both attestations must bind the same valid 40-character source revision. A valid identity attestation from one revision and object-store attestation from another fails closed even when each individual gate passes. + +### 3. Re-ingest; never promote local material + +The retained local namespace, PVC, Iceberg objects and GDA Control database are audit evidence only. They are not an input dependency for the protected execution and cannot be copied or promoted as production material. Once the composed gate passes, a new PolicyDecision/Approval may authorize fresh ingestion of the same content-bound source through the protected provider path. + +M3-26 itself never authorizes scheduler submission or provider mutation. `scheduler_submission_authorized`, `provider_mutation_authorized`, `production_ingestion_verified` and `production_ready` remain false even when `ready_for_protected_reexecution` becomes true. + +### 4. Commit the current blocked decision + +The checked production profiles are structurally valid but intentionally pending. The M3-26 decision therefore has status `blocked_pending_protected_attestation`, with 85 explicit blockers: 40 identity profile decisions, one missing identity attestation, 43 object-store profile decisions and one missing object-store attestation. + +This is a valid fail-closed decision, not a failed test and not a production readiness claim. The `evaluate` command returns success only after the approved profiles and both fresh attestations pass; `validate` verifies the integrity of the checked pending decision. + +## Verification + +- contract SHA `39411248b37b7d8d43ac7ad37737de15d6b6d4c5e4feb2088f0a87cec888b5f9`; +- decision SHA `39246eacdd1793f23aecb71195cc4c9d8c63d7125aad8cd9bbb59a96c588cd73`; +- decision file SHA `62624b96d83b085cbb82d29d618042d7f4faa5193847c26af859ec1c87cd4f11`; +- checked M3-25 predecessor, identity profile and object-store profile all validate; +- current decision has 85 blockers and `ready_for_protected_reexecution=false`; +- focused tests cover predecessor drift, report tampering, outer rehash, source-revision alignment, local material prohibition and production overclaim. + +## Consequences + +**Positive**: the next real-data execution now has one deterministic admission boundary rather than a checklist of unrelated production gates. Local evidence cannot silently become production material, and attestations from different revisions cannot be combined. + +**Negative**: M3-26 does not deploy identity, storage, catalog, scheduler or ingestion infrastructure. It deliberately remains blocked until the external owners approve and materialize the profiles. + +**Next gate**: approve the production identity and object-store profiles, deploy their protected paths and generate both fresh attestations on one source revision. Then create a tenant-bound PolicyDecision/Approval for a fresh protected re-execution, deploy the persistent scheduler/executor, and validate ingestion, backup/PITR, independent failure domains, scale and Spark/Flink conformance without weakening this gate. + +**Revisit trigger**: supersede this ADR if the production provider is not S3-compatible, the identity integration modes change, or a unified protected-environment attestation service replaces the two existing gates. diff --git a/docs/evidence/metadata-fabric-protected-real-feature-reexecution-gate-2026-07-31.json b/docs/evidence/metadata-fabric-protected-real-feature-reexecution-gate-2026-07-31.json new file mode 100644 index 00000000..c91a6c99 --- /dev/null +++ b/docs/evidence/metadata-fabric-protected-real-feature-reexecution-gate-2026-07-31.json @@ -0,0 +1,280 @@ +{ + "blockers": [ + "identity.attestation:production identity attestation is missing", + "identity.profile:catalog.backup_policy_reference", + "identity.profile:catalog.catalog_reference", + "identity.profile:catalog.decision_status", + "identity.profile:catalog.gravitino_backend", + "identity.profile:catalog.persistence_reference", + "identity.profile:federation.audience", + "identity.profile:federation.decision_status", + "identity.profile:federation.discovery_uri", + "identity.profile:federation.issuer", + "identity.profile:federation.jwks_uri", + "identity.profile:federation.token_exchange_mode", + "identity.profile:federation.trust_policy_reference", + "identity.profile:operations.audit_log_reference", + "identity.profile:operations.identity_owner", + "identity.profile:operations.incident_owner", + "identity.profile:operations.revocation_slo_minutes", + "identity.profile:operations.rollback_runbook.uri", + "identity.profile:operations.rollback_runbook.version", + "identity.profile:operations.rotation_slo_minutes", + "identity.profile:operations.runbook.uri", + "identity.profile:operations.runbook.version", + "identity.profile:operations.security_owner", + "identity.profile:providers.gravitino.authentication_component_reference", + "identity.profile:providers.gravitino.environment_binding", + "identity.profile:providers.gravitino.integration_mode", + "identity.profile:providers.gravitino.kubernetes_service_account", + "identity.profile:providers.gravitino.namespace_template", + "identity.profile:providers.gravitino.workload_identity_reference", + "identity.profile:providers.openmetadata.authentication_component_reference", + "identity.profile:providers.openmetadata.environment_binding", + "identity.profile:providers.openmetadata.integration_mode", + "identity.profile:providers.openmetadata.kubernetes_service_account", + "identity.profile:providers.openmetadata.namespace_template", + "identity.profile:providers.openmetadata.workload_identity_reference", + "identity.profile:tenancy.isolation_mode", + "identity.profile:tenancy.policy_reference", + "identity.profile:tls.certificate_policy_reference", + "identity.profile:tls.gravitino_endpoint", + "identity.profile:tls.openmetadata_endpoint", + "identity.profile:tls.trust_bundle_reference", + "object_store.attestation:production object-store attestation is required", + "object_store.profile:consistency.multipart_upload_cleanup_reference", + "object_store.profile:consistency.orphan_file_cleanup_reference", + "object_store.profile:durability.recovery_bucket_reference", + "object_store.profile:durability.replication_policy_reference", + "object_store.profile:durability.retention_policy_reference", + "object_store.profile:encryption.key_policy_reference", + "object_store.profile:encryption.key_reference", + "object_store.profile:encryption.rotation_days", + "object_store.profile:identity.bucket_policy_reference", + "object_store.profile:identity.integration_mode", + "object_store.profile:identity.kubernetes_service_account", + "object_store.profile:identity.least_privilege_policy_reference", + "object_store.profile:identity.workload_identity_reference", + "object_store.profile:operations.attestation_policy_reference", + "object_store.profile:operations.audit_log_reference", + "object_store.profile:operations.availability_slo_percent", + "object_store.profile:operations.incident_owner", + "object_store.profile:operations.latency_slo_ms", + "object_store.profile:operations.metrics_alert_reference", + "object_store.profile:operations.platform_owner", + "object_store.profile:operations.recovery_runbook.uri", + "object_store.profile:operations.recovery_runbook.version", + "object_store.profile:operations.rollback_runbook.uri", + "object_store.profile:operations.rollback_runbook.version", + "object_store.profile:operations.runbook.uri", + "object_store.profile:operations.runbook.version", + "object_store.profile:operations.security_owner", + "object_store.profile:operations.storage_owner", + "object_store.profile:provider.account_reference", + "object_store.profile:provider.bucket", + "object_store.profile:provider.decision_status", + "object_store.profile:provider.endpoint", + "object_store.profile:provider.failure_domain_reference", + "object_store.profile:provider.infrastructure_reference", + "object_store.profile:provider.provider_type", + "object_store.profile:provider.recovery_region", + "object_store.profile:provider.region", + "object_store.profile:tenancy.policy_reference", + "object_store.profile:transport.certificate_policy_reference", + "object_store.profile:transport.dns_policy_reference", + "object_store.profile:transport.endpoint", + "object_store.profile:transport.private_connectivity_reference", + "object_store.profile:transport.trust_bundle_reference" + ], + "checked_real_feature_predecessor_verified": true, + "contract_sha256": "39411248b37b7d8d43ac7ad37737de15d6b6d4c5e4feb2088f0a87cec888b5f9", + "cross_gate_source_revision_aligned": false, + "decision_sha256": "39246eacdd1793f23aecb71195cc4c9d8c63d7125aad8cd9bbb59a96c588cd73", + "evaluated_at": "2026-07-31T06:32:10.229686Z", + "fresh_protected_ingestion_required": true, + "identity_attestation": null, + "identity_report": { + "attestation_errors": [ + "production identity attestation is missing" + ], + "attestation_fingerprint": null, + "attestation_valid": false, + "authorization_fingerprint": "b94f37189c36aa2c9c29d6f0da88ec0cc0889e2c5e9da7d37ae23e11e4e61497", + "catalog_fingerprint": "6eea9f47d191acb93e0383e2de850800b8a62e4a41eeff431c9c76ffefa7ae75", + "credential_revocation_verified": false, + "credential_rotation_verified": false, + "environment": "production", + "federation_fingerprint": "dd8deb0ec29438b960fe6e9609f078e0b2afa891ea2168df345b2e5a8595ad3b", + "oidc_verified": false, + "persistent_catalog_identity_binding_verified": false, + "production_identity_gate_passed": false, + "production_identity_verified": false, + "production_ready": false, + "profile_blockers": [ + "federation.decision_status", + "federation.issuer", + "federation.discovery_uri", + "federation.jwks_uri", + "federation.audience", + "federation.token_exchange_mode", + "federation.trust_policy_reference", + "providers.openmetadata.integration_mode", + "providers.openmetadata.authentication_component_reference", + "providers.openmetadata.environment_binding", + "providers.openmetadata.workload_identity_reference", + "providers.openmetadata.kubernetes_service_account", + "providers.openmetadata.namespace_template", + "providers.gravitino.integration_mode", + "providers.gravitino.authentication_component_reference", + "providers.gravitino.environment_binding", + "providers.gravitino.workload_identity_reference", + "providers.gravitino.kubernetes_service_account", + "providers.gravitino.namespace_template", + "tls.openmetadata_endpoint", + "tls.gravitino_endpoint", + "tls.trust_bundle_reference", + "tls.certificate_policy_reference", + "catalog.decision_status", + "catalog.gravitino_backend", + "catalog.catalog_reference", + "catalog.persistence_reference", + "catalog.backup_policy_reference", + "tenancy.isolation_mode", + "tenancy.policy_reference", + "operations.identity_owner", + "operations.security_owner", + "operations.incident_owner", + "operations.audit_log_reference", + "operations.rotation_slo_minutes", + "operations.revocation_slo_minutes", + "operations.runbook.uri", + "operations.runbook.version", + "operations.rollback_runbook.uri", + "operations.rollback_runbook.version" + ], + "profile_errors": [], + "profile_fingerprint": "2e9d5cac3560b853820f923669f6794ead63bcb36a528639fc0e9539e148ee2f", + "profile_valid": true, + "protected_workload_identity_verified": false, + "provider_bindings_fingerprint": "76f1250f5cccad3eec22c31e1663f4790387fe72d3407f8d9ce889df8c7ca02d", + "provider_minimum_privilege_verified": false, + "ready_for_protected_verification": false, + "report_fingerprint": "c607589ee25a87acc8a1ab71372618a9a4c10c1e8ebff15b8db7e78b37600b9f", + "schema": "gda.metadata_fabric_identity_readiness_report.v1", + "tenancy_fingerprint": "7d0f5108529f57abe1f97ff6fcf8af748550b6a9dcbd47700e162c88003a055c", + "tls_fingerprint": "71a42d8ab9604010a713ff5cd1cd62795a88e274675504d70157e4a33ab526ec", + "tls_verified": false + }, + "local_material_promotion_allowed": false, + "local_retained_material_dependency": false, + "object_store_attestation": null, + "object_store_report": { + "attestation_errors": [ + "production object-store attestation is required" + ], + "attestation_fingerprint": null, + "attestation_valid": false, + "consistency_fingerprint": "3c44bbdef05b3d4f12171b8076faed5ecac20daf68ab80b9884f6224581abe5b", + "durability_fingerprint": "337e7f60709732c316a7a67f2490a373015e8e02cd9cce271556770c7c39e39c", + "encryption_fingerprint": "199db11cc7f83b5117a3238a1c9c0511c579da1788c684a16f1ff76a46c1152d", + "environment": "production", + "identity_fingerprint": "19a3051e73bf0901e9967b33c93c0fd5371a788331c098f53c42158b6c0fef05", + "kms_encryption_verified": false, + "local_evidence_fingerprint": "05844457efb378581fb7fc2e7ed3c706819b2d8fa5a52b2f82577051d38c2cd1", + "object_store_decision_frozen": false, + "object_store_durability_verified": false, + "object_store_failure_recovery_verified": false, + "operations_fingerprint": "3ba50113e419a3a7a85c3917e4225d27fad3a65a74a6236bc954ea999812d513", + "production_object_store_gate_passed": false, + "production_object_store_verified": false, + "production_ready": false, + "profile_blockers": [ + "provider.decision_status", + "provider.provider_type", + "provider.account_reference", + "provider.region", + "provider.endpoint", + "provider.bucket", + "provider.infrastructure_reference", + "provider.failure_domain_reference", + "provider.recovery_region", + "identity.integration_mode", + "identity.workload_identity_reference", + "identity.kubernetes_service_account", + "identity.least_privilege_policy_reference", + "identity.bucket_policy_reference", + "transport.endpoint", + "transport.private_connectivity_reference", + "transport.dns_policy_reference", + "transport.trust_bundle_reference", + "transport.certificate_policy_reference", + "encryption.key_reference", + "encryption.key_policy_reference", + "encryption.rotation_days", + "durability.retention_policy_reference", + "durability.replication_policy_reference", + "durability.recovery_bucket_reference", + "consistency.multipart_upload_cleanup_reference", + "consistency.orphan_file_cleanup_reference", + "tenancy.policy_reference", + "operations.platform_owner", + "operations.security_owner", + "operations.storage_owner", + "operations.incident_owner", + "operations.audit_log_reference", + "operations.metrics_alert_reference", + "operations.availability_slo_percent", + "operations.latency_slo_ms", + "operations.attestation_policy_reference", + "operations.runbook.uri", + "operations.runbook.version", + "operations.recovery_runbook.uri", + "operations.recovery_runbook.version", + "operations.rollback_runbook.uri", + "operations.rollback_runbook.version" + ], + "profile_errors": [], + "profile_fingerprint": "668e194b3c688307014148391e7f389c9d6e9ca69c95d7b4cc92b4acae93181a", + "profile_valid": true, + "protected_workload_identity_verified": false, + "provider_fingerprint": "4954b59d7d5fc484da81ed6c46ef9330ca0aba322d7ed1b1196f5af29bd9933e", + "ready_for_protected_verification": false, + "report_fingerprint": "85362dd10b7dc565f9fa567673d90b774cdec714bd1e70fb2c3c83c1af48b5ea", + "schema": "gda.metadata_fabric_object_store_readiness_report.v1", + "tenancy_fingerprint": "8885071181dbc17029d29ebb4d79e2d3e1324ed6dc45b03509dde24096428525", + "tenant_isolation_verified": false, + "tls_verified": false, + "transport_fingerprint": "203f4794eee155c16af456ee00d9bad615a4dc83cb8f77f91afcc4b172ad019c" + }, + "production_ingestion_verified": false, + "production_profiles_valid": true, + "production_ready": false, + "protected_identity_attested": false, + "protected_object_store_attested": false, + "protected_tenant_controls_attested": false, + "provider_mutation_authorized": false, + "ready_for_protected_reexecution": false, + "scheduler_submission_authorized": false, + "schema": "gda.protected_real_feature_reexecution_decision.v1", + "source_binding": { + "control_facts_sha256": "5c0b8a58729e551c250b0410bcebdfe3f019f215a50b20d503f094fe1562d8b5", + "data_body_sha256": "6cc0fc9eaf48f8106f9afe192704c44407c86c9ea119ae20894bf369a8e74779", + "feature_count": 20, + "object_inventory_sha256": "de4a0efed9fdb68f0019b843377f6c8de71664de955130d0dd38e99eccdb8034", + "output_content_sha256": "bdc06792e8b935176ee6df6f6f6d4be1535622d54d9b994a778cabfe5a574618", + "output_resource_version_id": "a6000000-0000-4000-8000-000000000002", + "platform_run_state_version": 3, + "platform_run_status": "succeeded", + "retention_expires_at": "2026-08-07T04:15:23.082316Z", + "retention_id": "m3-24-229740ac50ebb53b", + "row_set_sha256": "c26ff708f4b6be082327dff63a6a8659420dbc4cab37dea1cac7b40f147512df", + "run_id": "a9000000-0000-4000-8000-000000000009", + "snapshot_id": 8034081021802585202, + "source_evidence_file_sha256": "6880ff81dcde37f824ab3c7d04f62863375d5a6f1ada2a2dbfa832e77da7cfb1", + "source_evidence_path": "docs/evidence/metadata-fabric-retained-real-feature-restart-recovery-2026-07-31.json", + "source_evidence_sha256": "1b5a5ceeadee88868bab6237b3f3280c8b13793cc54193592fec7dbbfdd4e8a6", + "tenant_id": "metadata-authorization-local" + }, + "source_payload_dependency": false, + "status": "blocked_pending_protected_attestation" +} diff --git a/docs/roadmap.md b/docs/roadmap.md index fc2318b9..51997b79 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -419,7 +419,7 @@ AR-0 Architecture / Schema / Runtime Truth 当前 Metadata Fabric 证据边界:M1 只读 bridge 合同已验证;ADR-037 至 ADR-046 分别覆盖本地 foundation/recovery/metrics/network-policy 演练与 production readiness contracts;ADR-047 至 ADR-050 已依次建立 deterministic projection plan、本地双 provider replay、tenant-scoped binding ledger 与本地 OpenLineage 幂等 wire delivery;ADR-051 以临时非管理员 OpenMetadata bot 证明项目专用 grant 只有 `table/Create`、`policy/Create` 被 403 拒绝,且 JWT 轮换/吊销后旧值/当前值均返回 401;ADR-052 又在隔离 Gravitino `1.3.0` Basic IdP 中证明 bounded user 的 `USE_CATALOG`、`USE_SCHEMA`、`CREATE_TABLE` 范围、catalog-create 403、密码轮换和用户吊销;ADR-053 将生产 OIDC federation、双 provider integration/workload identity、最小权限、TLS/mTLS、持久 Gravitino catalog、tenant isolation、运营责任和新鲜 protected attestation 冻结为 fail-closed readiness contract。Gravitino `1.3.0` 镜像只发现 Basic IdP,不假设 native OIDC;当前 profile 仍有 40 个外部 blockers 且未提交真实 attestation。`local_openmetadata_minimum_privilege_verified=true` 与 `local_gravitino_minimum_privilege_verified=true` 都只描述各自临时 provider rehearsal;M3-2 ingestion 仍使用 bootstrap admin,Gravitino probe catalog 仍是 memory catalog。因此 `provider_minimum_privilege_verified`、protected workload identity、OIDC、TLS、持久 catalog、生产 ingestion/conformance、生产 lineage receiver、`production_identity_gate_passed` 与 `production_ready` 仍为 false。 -M3-22 至 M3-25 已把一份真实重庆 20-feature EPSG:4490 slice 从受授权 Spark/Sedona + JDBC/S3 Iceberg ingestion,推进到保留 7 天的 staging material、原子 GDA Control `ResourceVersion + 2 Artifacts + QualityResult + LineageEvent` 晋级、数据库裁决的 `succeeded@3` 和保留运行时的受控 restart continuity。M3-24 已持久化完整 execution-plan/PolicyDecision/Approval Artifacts,回读真实 DolphinScheduler `SUCCESS`,由独立 evaluator 重开 Parquet 并重算九项空间质量与 row fingerprint;M3-25 又按 PostgreSQL -> MinIO -> Gravitino -> GDA Control 顺序重启同一运行时,证明稳定 namespace/StatefulSet/Service/PVC/container/volume 身份不变、Pod/PID 轮换,且 Iceberg snapshot/Parquet/Gravitino projection/GDA ledger facts SHA 与 `succeeded@3` 精确 replay 均不漂移、不新增事实。该结果仍是单开发主机上的 retained local staging process-restart rehearsal;生产 identity/storage/tenant attestation、常驻 scheduler/executor、独立故障域、backup/PITR、production restart recovery、staging scale 和完整 Spark/Flink conformance 是下一门槛。边界见 [ADR-070](architecture-decisions/adr-070-retained-real-feature-terminal-success.md) 与 [ADR-071](architecture-decisions/adr-071-retained-real-feature-restart-recovery.md)。 +M3-22 至 M3-25 已把一份真实重庆 20-feature EPSG:4490 slice 从受授权 Spark/Sedona + JDBC/S3 Iceberg ingestion,推进到保留 7 天的 staging material、原子 GDA Control `ResourceVersion + 2 Artifacts + QualityResult + LineageEvent` 晋级、数据库裁决的 `succeeded@3` 和保留运行时的受控 restart continuity。M3-24 已持久化完整 execution-plan/PolicyDecision/Approval Artifacts,回读真实 DolphinScheduler `SUCCESS`,由独立 evaluator 重开 Parquet 并重算九项空间质量与 row fingerprint;M3-25 又按 PostgreSQL -> MinIO -> Gravitino -> GDA Control 顺序重启同一运行时,证明稳定 namespace/StatefulSet/Service/PVC/container/volume 身份不变、Pod/PID 轮换,且 Iceberg snapshot/Parquet/Gravitino projection/GDA ledger facts SHA 与 `succeeded@3` 精确 replay 均不漂移、不新增事实。M3-26 进一步把该真实 predecessor 与 production identity/object-store gates 组合为受保护重执行准入,强制两份 attestation 绑定同一 source revision,并禁止本地 retained material 直推;当前两个 profile 虽结构有效,但 85 个外部 blockers 未解除,所以 `ready_for_protected_reexecution=false`,没有调度或 provider 写入授权。生产 identity/storage/tenant attestation、常驻 scheduler/executor、独立故障域、backup/PITR、production restart recovery、staging scale 和完整 Spark/Flink conformance 仍是下一门槛。边界见 [ADR-070](architecture-decisions/adr-070-retained-real-feature-terminal-success.md)、[ADR-071](architecture-decisions/adr-071-retained-real-feature-restart-recovery.md) 与 [ADR-072](architecture-decisions/adr-072-protected-real-feature-reexecution-gate.md)。 ### AR-2 — Source, Ingestion and Geospatial Lakehouse Vertical Slice(P0) @@ -686,7 +686,7 @@ Golden checks 至少覆盖: 1. 导出所有目标环境 schema/config fingerprint,修复重复 migration ID、checksum 和 fail-open runner。 2. 完成部署、存储、bucket、registry、scheduler/job、API/GIS endpoint、图层/样式/缓存、provider/Gateway、数据资产、消费者和权限事实盘点;部署 OpenMetadata/Gravitino/DolphinScheduler/Temporal sandbox,冻结 owner、version、OIDC、backup/restore 和升级责任。 3. 冻结 ResourceURN、ResourceVersion、PlatformDefinition/PlatformRun/FrameworkAttemptObservation/Artifact/LineageEvent、SubjectContext 与 storage/table/compute provider 最小合同。 -4. 分阶段实现 `gda-metadata-fabric-bridge`:M1 只读 mapping/reconciliation、M2a 本地 foundation/重启连续性、M2b 本地与跨集群恢复、M2c metrics/OTel 故障演练、M2d production readiness contracts,以及 M3-1 至 M3-25 projection、provider identity/interoperability、Active Metadata、真实 feature ingestion、原子 ledger promotion、retained terminal success 和同一保留 authority 的本地进程重启连续性已验证。下一步以 production identity/storage/tenant attestation 为先决条件,部署常驻 scheduler/executor 与持久 catalog/control/storage,验证 backup/PITR、独立故障域、production restart recovery、staging scale、完整 Spark/Flink conformance 和真实告警/runbook;本地 retained evidence 不计入生产退出门。 +4. 分阶段实现 `gda-metadata-fabric-bridge`:M1 只读 mapping/reconciliation、M2a 本地 foundation/重启连续性、M2b 本地与跨集群恢复、M2c metrics/OTel 故障演练、M2d production readiness contracts,以及 M3-1 至 M3-25 projection、provider identity/interoperability、Active Metadata、真实 feature ingestion、原子 ledger promotion、retained terminal success 和同一保留 authority 的本地进程重启连续性已验证;M3-26 已建立真实 predecessor 的 protected re-execution 组合门禁,但因 85 个外部 blockers 保持关闭。下一步必须先批准 production identity/object-store profiles,并在同一 source revision 生成受保护 identity/storage/tenant attestations;随后才可部署常驻 scheduler/executor 与持久 catalog/control/storage,做 fresh protected ingestion,并验证 backup/PITR、独立故障域、production restart recovery、staging scale、完整 Spark/Flink conformance 和真实告警/runbook。本地 retained material 不得直接晋级,也不计入生产退出门。 5. 实现 `gda-orchestration-gateway`、DolphinScheduler process/task/schedule/complement/worker-group、Spark/Flink provider task adapter 和故障注入;不再开发新的 lease/queue/scheduler。 6. 冻结首条地类图斑数据、标准版本、敏感级别、owner、SLO 和 golden result。 7. 冻结 Default Lakehouse、Cloud Managed、Lightweight Integrated profiles;以统一 Run 完成默认 MinIO/Iceberg/Spark/Flink、轻量 PostGIS/DuckDB 和 Azure 代表 adapter 的 conformance smoke。 diff --git a/docs/system-of-record-matrix-2026-07-24.md b/docs/system-of-record-matrix-2026-07-24.md index ebf9cbf6..486b39aa 100644 --- a/docs/system-of-record-matrix-2026-07-24.md +++ b/docs/system-of-record-matrix-2026-07-24.md @@ -2,9 +2,9 @@ 日期:2026-07-31 -阶段:AR-0 `in_progress`;AR-1 gateway、成功终局 evidence gate、DolphinScheduler adapter sandbox POC、Metadata Fabric M1/M2、M2c-4/M2d-2 production readiness contracts、M3-1 至 M3-25 retained real-feature restart continuity 已验证;生产 policy/tenant isolation、生产 identity/object-store attestation、常驻 production consumer/scheduler/executor、backup/PITR、独立故障域、production restart recovery、scale/conformance 和生产切换仍 `in_progress` +阶段:AR-0 `in_progress`;AR-1 gateway、成功终局 evidence gate、DolphinScheduler adapter sandbox POC、Metadata Fabric M1/M2、M2c-4/M2d-2 production readiness contracts、M3-1 至 M3-25 retained real-feature restart continuity 已验证,M3-26 protected re-execution 组合门禁已建立但因 85 个外部 blockers 保持关闭;生产 policy/tenant isolation、生产 identity/object-store attestation、常驻 production consumer/scheduler/executor、backup/PITR、独立故障域、production restart recovery、scale/conformance 和生产切换仍 `in_progress` -适用分支:`feat/ar1-metadata-fabric-retained-real-feature-restart-recovery` +适用分支:`feat/ar1-metadata-fabric-protected-real-feature-reexecution-gate` ## 判定规则 @@ -19,7 +19,7 @@ |---|---|---|---|---|---| | SQL schema 历史 | PostgreSQL `schema_migrations`,以完整 migration ID + checksum 为权威 | migration CLI 的 JSON 报告 | 保持现有 ledger;任何 drift fail closed | Data Platform | AR-0,已验证 | | 部署配置策略 | Compose/K8s/进程环境;`platform_truth.CONFIG_SPECS` 定义关键类型与策略;DolphinScheduler worker 与 Active Metadata consumer 均有默认零副本、外部 ConfigMap/Secret 驱动的 Kustomize 模板和静态 validator,前者另有 staging activation preflight | `.env` 仅补默认;脱敏 snapshot、Secret key attestation、未扩容 Deployment 和 `ready_for_activation` 都是观测/模板 | 版本化 DeploymentProfile + secret reference;部署环境始终优先;模板或 preflight 通过都不等于环境已启用 | Platform/SRE/Security | AR-0,部分实现;worker 模板/preflight 本地已验证 | -| 环境发布与晋级 | 本地 candidate/registry/provenance/release/live 合同已绑定 publisher、verifier、OCI 和 manifest identity;canonical `main@0182406`、archive refs、三组 active ruleset 与 `staging-provenance` protected environment 已建立,但尚无成功 publisher/verifier 或 deployment | 旧 mainline、feature branch、CI artifact、JSON、离线 report 和合成 `verified_for_staging_apply` 都不能单独成为发布权威;publisher SHA、verifier SHA 与 branch lineage 必须分别验证 | 由受保护 environment 的 DeploymentRevision 绑定 OCI、provenance artifact、release manifest 与全部 live verdict | Platform/SRE/Security/Repository Owner | AR-1 mainline 治理已恢复 -> 首次 GHCR publish/verify -> 真实 staging | +| 环境发布与晋级 | 本地 candidate/registry/provenance/release/live 合同已绑定 publisher、verifier、OCI 和 manifest identity;canonical `main@0182406`、archive refs、三组 active ruleset 与 `staging-provenance` protected environment 已建立,但尚无成功 publisher/verifier 或 deployment;M3-26 又把真实重庆 predecessor 与 identity/object-store production gates 组合为 source-revision-aligned protected re-execution preflight | 旧 mainline、feature branch、CI artifact、JSON、离线 report、合成 `verified_for_staging_apply` 和 M3-26 pending decision 都不能单独成为发布或执行权威;本地 retained material 禁止直接晋级 | 由受保护 environment 的 DeploymentRevision 绑定 OCI、provenance artifact、release manifest 与全部 live verdict;fresh protected ingestion 还须另有 tenant-bound PolicyDecision/Approval 与 scheduler command | Platform/SRE/Security/Repository Owner | AR-1 mainline 治理已恢复;M3-26 preflight `ready=false` -> 批准 production profiles/attestations -> 首次 GHCR publish/verify -> 真实 staging | | 后台运行时清单 | `platform_truth.RUNTIME_INVENTORY` 是代码层登记;`gda_control` 已有受控 PlatformRun 写入口;DolphinScheduler managed worker 已登记但尚无生产调用方;Active Metadata consumer 登记为 `activation_request_staging_only`,其 deployment 默认为 0 replicas;M3-24 terminal-success 与 M3-25 restart-recovery rehearsal 及此前 Metadata Fabric rehearsals 均登记为 `local_verification_only` | AST primitive report、worker status JSON、FrameworkAttemptObservation、DolphinScheduler instance state、本地 recovery/metrics/network-policy/catalog/interoperability/failure/outbox/consumer/authorization/delivery/projection/binding/ingestion/promotion/terminal/restart evidence | PlatformRun ledger 唯一登记最终状态;M3-24 的 Run 已由数据库 evidence gate 裁决为 `succeeded@3`,M3-25 只证明同一单主机 retained namespace/PVC/MinIO-Iceberg/control DB 的进程重启连续性;两者均不得变成生产控制器、catalog/storage/output、tenant-isolation 或 production recovery 权威 | Platform Architecture | AR-1 adapter/worker、M3-15 至 M3-25 本地控制链、retained terminal success 与进程重启连续性已验证;常驻受保护 scheduler/executor、生产 storage/control、backup/PITR 与独立故障域待接入 | | 原始文件/对象 | 当前 local uploads、S3/MinIO/OBS 均可能被直接写入,权威边界未统一 | 临时上传、下载缓存、预览文件 | Landing object 以 immutable URI + checksum + retention 为权威;本地 scratch 可删除 | Data Platform | AR-2 | | 湖仓表与 snapshot | Iceberg/STAC/S3A 有局部实现,尚无通用发布权威 | STAC item、GeoParquet export | Iceberg catalog snapshot 是分析表版本权威;对象是物理内容,STAC 是发现投影 | Data Platform | AR-2 | @@ -69,6 +69,7 @@ 24. M3-23 只证明 M3-22 path-free candidates 可在临时 PostgreSQL GDA Control 中按 `ResourceVersion -> output Artifact -> quality evidence Artifact -> QualityResult -> LineageEvent` 单事务追加。缺 authority、半状态、跨租户和 direct mutation 均拒绝,故障注入整笔回滚,精确 replay 不新增;Run 保持 `accepted@0` 且 success finalization 被拒绝。M3-22 material、M3-23 数据库与完整 authorization Artifacts 均未保留或伪造补写;这不证明 persistent authority、terminal success、staging/生产 ingestion 或 readiness。 25. M3-24 以同一真实重庆 20-feature slice 新跑一次受控链:完整 execution-plan/PolicyDecision/Approval 先落 GDA Control,DolphinScheduler Shell task 回调短生命周期 executor 执行 Spark/Sedona JDBC/S3 Iceberg ingestion,真实 `SUCCESS` 只作为 attempt observation;独立 evaluator 重开 retained Parquet 并创建质量 evidence,M3-23 promoter 原子追加 output bundle,既有数据库 finalizer 唯一裁决 `succeeded@3`,精确 terminal replay 不新增。含 BSM/WKB 的 ConfigMap 和临时 scheduler 已删除;namespace/PVC/MinIO-Iceberg material 与专用 GDA Control PostgreSQL 受 retention ID 约束保留 7 天。该单开发主机 rehearsal 不证明生产 identity/storage/tenant attestation、常驻 scheduler/executor、restart/recovery、scale/conformance 或 production readiness。 26. M3-25 不重新 ingest、不创建新 ResourceVersion/Run,也不改写 M3-24 verdict;它只绑定 M3-24 evidence/retention ID,按 PostgreSQL、MinIO、Gravitino、GDA Control 顺序重启同一保留运行时。三组 Pod UID 与 control PID/StartedAt 必须轮换,namespace/StatefulSet/Service/PVC/image/control container/volume 必须不变;Iceberg inventory/metadata/snapshot、Parquet 九项质量、Gravitino projection 和 GDA Control facts SHA 在重启前后与 exact terminal replay 后必须一致且零新增。该结果只证明单开发主机本地进程重启连续性,不证明 backup/PITR、独立故障域、production restart recovery 或 production readiness。 +27. M3-26 将 checked M3-25 evidence file/evidence SHA、真实 output ResourceVersion/content、Iceberg/Parquet/quality/control facts 与 ADR-053 identity gate、ADR-057 object-store gate 组合为单一 protected re-execution preflight。两份新鲜 protected attestation 必须各自通过既有完整 gate,且绑定同一 source revision;本地 retained material 永远不可直推,preflight 通过也只表示可另行申请 fresh protected ingestion,不创建 PolicyDecision/Approval、scheduler command、Run 或 provider mutation。当前 profiles 结构有效但有 identity 40 + object-store 43 项未决输入及两份缺失 attestation,共 85 blockers,故 `ready_for_protected_reexecution=false`,所有 production claims 仍为 false。 ## 已建立的 AR-0/AR-1 entry 证据 @@ -117,11 +118,13 @@ - Metadata Fabric M3-23 已把 M3-22 的 output ResourceVersion、output Artifact、quality evidence Artifact、独立 passed QualityResult 与 source-to-output LineageEvent 作为一个 `RunOutputLedgerPromotion`,由独立 promoter 在一个 PlatformGateway 事务中写入真实临时 PostgreSQL,同时保持此前 gateway-bound evidence 指纹不变。缺 authority 先验拒绝;QualityResult 前故障注入后候选计数全为 0;首次 `created=true`、精确 replay `created=false`,最终计数为 `1/2/1/1`。FORCE RLS、最小 grant、跨租户读/直写和八个 direct UPDATE/DELETE 拒绝通过;Run 保持 `accepted@0`,success finalization 被既有 gate 拒绝。promotion/contract/evidence SHA 为 `404b6e4e5d8194f092bd83ef99cbf2d1d727015b926cd438a79eb0210f969a22` / `bd21c81925f66acdfecca5cabd78651f31deab4165da2ccd6900c4e5796e5735` / `f6efea5000791dec1716a8354a8e39a8425b083ca4d409f4bcb61f0e7e03580d`。M3-22 material 与临时数据库已清理,完整 authorization Artifacts 未补写;`output_material_retained=false`、`platform_run_succeeded=false`、`production_ready=false`。 - Metadata Fabric M3-24 已用新的受控执行把同一重庆 slice 推进到 retained terminal success。完整 execution-plan/PolicyDecision/Approval Artifacts 在 dispatch 前持久化;DolphinScheduler `3.4.2` task 返回真实 `SUCCESS`,provider observations 为 2;独立 evaluator 重开 94,603-byte Parquet 并使 feature/unique/non-empty/valid/Z/SRID/positive-area/bbox/row-fingerprint 九项计数均为 20。M3-23 promoter 与 migration 096 finalizer 依次提交 5 Artifacts、1 QualityResult、1 LineageEvent 和 `succeeded@3`,精确 replay 不新增。contract/evidence SHA 为 `9c8f20ca1fb9995530c4e988ced627f665857ecdf0e104bb7d07c4a4a486057a` / `d966668b5a2ea57c7a4b2a3bc9824daab9b0128d9f94e515d7be649b145de418`;retention ID 为 `m3-24-229740ac50ebb53b`,到期 `2026-08-07T04:15:23.082316Z`。source payload 与 scheduler 已清理,retained material/control DB 仍可审计;六项 production claims 与 `production_ready` 均为 `false`。 - Metadata Fabric M3-25 已在同一 retention ID 和 expiry 内按 PostgreSQL -> MinIO -> Gravitino -> GDA Control 顺序完成进程重启。三组 Pod UID 与 control PID 均轮换,namespace/StatefulSet/Service/PVC/image/control container/volume 均保持;object inventory SHA `de4a0efed9fdb68f0019b843377f6c8de71664de955130d0dd38e99eccdb8034`、snapshot `8034081021802585202`、94,603-byte Parquet body/row-set SHA、九项 20-count 质量、Gravitino projection SHA `f30feb94a5a8280597f331a7f965762bfabb9b82397af49dda05b61ce00bbb1e` 与 GDA Control facts SHA `5c0b8a58729e551c250b0410bcebdfe3f019f215a50b20d503f094fe1562d8b5` 前后不变;terminal replay 为 `created=false`,ledger 计数零新增。contract/evidence SHA 为 `83ed15ae4eed85e0c261c2b3a04ea2ad559f3deb7b86c7c2f2dedd0cf28d23d0` / `1b5a5ceeadee88868bab6237b3f3280c8b13793cc54193592fec7dbbfdd4e8a6`;backup/PITR、独立故障域、production restart recovery 与 `production_ready` 均为 `false`。 +- Metadata Fabric M3-26 已把 M3-25 evidence file/evidence SHA、20-feature output ResourceVersion/content、snapshot/object/Parquet/row-set/control facts 与 production identity/object-store profile fingerprints 组合为 deterministic protected re-execution contract。contract/decision SHA 为 `39411248b37b7d8d43ac7ad37737de15d6b6d4c5e4feb2088f0a87cec888b5f9` / `39246eacdd1793f23aecb71195cc4c9d8c63d7125aad8cd9bbb59a96c588cd73`;当前 decision 自验证通过,但 85 个外部 blockers 和缺失 protected attestations 使 status 保持 `blocked_pending_protected_attestation`。本地 retained material 不可直推,fresh protected ingestion、scheduler submission、provider mutation、production ingestion 与 `production_ready` 均未获授权或验证。 ## 下一验收证据 -- M3-21 空表 metadata promotion、M3-22 临时真实 feature ingestion、M3-23 临时 ledger promotion、M3-24 retained terminal success 与 M3-25 本地进程重启连续性都不计入生产对象存储、持久生产 authority、production recovery 或 ingestion 退出门,五阶段历史 candidate/evidence 保持不变; -- M3-25 已跨过同一 retained authority 的 PostgreSQL/MinIO/Gravitino/control process restart continuity、独立数据/目录/ledger 回读与 terminal replay 零新增门槛;下一步必须由受保护 production identity/storage/tenant attestation 选择生产 provider,并部署常驻 scheduler/executor、持久 catalog/control/storage,验证 backup/PITR、独立故障域、production restart recovery、staging scale 与 Spark/Flink conformance; +- M3-21 空表 metadata promotion、M3-22 临时真实 feature ingestion、M3-23 临时 ledger promotion、M3-24 retained terminal success、M3-25 本地进程重启连续性与 M3-26 pending preflight 都不计入生产对象存储、持久生产 authority、production recovery 或 ingestion 退出门,历史 candidate/evidence 保持不变; +- M3-26 已将下一次真实数据执行的 admission boundary 冻结:先批准 production identity/object-store profiles,在同一 source revision 生成两份 protected attestations并清空 85 blockers;随后另行创建 tenant-bound PolicyDecision/Approval 和 scheduler command,做 fresh protected ingestion。任何本地 retained material 复制/直推或跨 revision attestation 组合都必须拒绝; +- preflight 通过后再部署常驻 scheduler/executor、持久 catalog/control/storage,验证 backup/PITR、独立故障域、production restart recovery、staging scale 与 Spark/Flink conformance; - 完成首次 application subject publish 与 protected verifier run;当前 mainline、archive refs、ruleset、required reviewer、禁止 bypass 和 environment enable variable 已配置并复核; - 真实 provenance artifact verify、受保护 overlay 的 `verified_for_staging_apply` release report,以及 staging/production 的 schema、config/runtime snapshot、registry/live DeploymentRevision 绑定、release/live artifact attestation 和环境 compare 报告; - staging 的 migration role、应用 login membership、连接池 role/tenant 复位、双租户 API 和 success finalization 运行产物; diff --git a/scripts/metadata-fabric-protected-real-feature-reexecution-gate.sh b/scripts/metadata-fabric-protected-real-feature-reexecution-gate.sh new file mode 100755 index 00000000..00254883 --- /dev/null +++ b/scripts/metadata-fabric-protected-real-feature-reexecution-gate.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +common_git_dir="$(git -C "$repo_root" rev-parse --path-format=absolute --git-common-dir 2>/dev/null || true)" +shared_root="" +if [ -n "$common_git_dir" ]; then + shared_root="$(cd "$common_git_dir/.." && pwd)" +fi + +if [ -n "${PYTHON:-}" ]; then + : +elif [ -x "$repo_root/.venv/bin/python" ]; then + PYTHON="$repo_root/.venv/bin/python" +elif [ -n "$shared_root" ] && [ -x "$shared_root/.venv/bin/python" ]; then + PYTHON="$shared_root/.venv/bin/python" +else + PYTHON="python" +fi + +cd "$repo_root" +exec "$PYTHON" -m data_agent.metadata_fabric_protected_real_feature_reexecution_gate "$@"