From 3c5865f0bd7fa1a5feada7996039d01c6dc46343 Mon Sep 17 00:00:00 2001 From: Ning Zhou Date: Fri, 31 Jul 2026 15:25:26 +0800 Subject: [PATCH] feat: assign protected profile owner decisions --- .github/workflows/ci.yml | 5 + ...abric_protected_profile_decision_packet.py | 908 ++++++++++++++++++ ...abric_protected_profile_decision_packet.py | 155 +++ ...protected-profile-owner-decision-packet.md | 77 ++ ...ed-profile-decision-packet-2026-07-31.json | 702 ++++++++++++++ docs/roadmap.md | 6 +- docs/system-of-record-matrix-2026-07-24.md | 11 +- ...abric-protected-profile-decision-packet.sh | 22 + 8 files changed, 1879 insertions(+), 7 deletions(-) create mode 100644 data_agent/metadata_fabric_protected_profile_decision_packet.py create mode 100644 data_agent/test_metadata_fabric_protected_profile_decision_packet.py create mode 100644 docs/architecture-decisions/adr-073-protected-profile-owner-decision-packet.md create mode 100644 docs/evidence/metadata-fabric-protected-profile-decision-packet-2026-07-31.json create mode 100755 scripts/metadata-fabric-protected-profile-decision-packet.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3fbd202c..71ddf96a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,6 +41,7 @@ on: - 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 + - feat/ar1-metadata-fabric-protected-real-feature-reexecution-gate env: PYTHON_VERSION: "3.13" @@ -216,6 +217,9 @@ jobs: - name: Validate protected real-feature re-execution decision run: python -m data_agent.metadata_fabric_protected_real_feature_reexecution_gate validate + - name: Validate protected profile owner decision packet + run: python -m data_agent.metadata_fabric_protected_profile_decision_packet validate + - name: Validate Active Metadata consumer deployment boundary run: python -m data_agent.active_metadata_consumer_deployment validate @@ -316,6 +320,7 @@ jobs: 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_protected_profile_decision_packet.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_profile_decision_packet.py b/data_agent/metadata_fabric_protected_profile_decision_packet.py new file mode 100644 index 00000000..c72bfd16 --- /dev/null +++ b/data_agent/metadata_fabric_protected_profile_decision_packet.py @@ -0,0 +1,908 @@ +"""Build the owner decision packet for protected production profiles. + +M3-27 turns the exact M3-26 blocker inventory into an assignable dependency +graph. The checked packet records no production choices, credentials, or +attestations and grants no execution authority. Owners must materialize +approved values in the existing production profiles and produce fresh +protected attestations before M3-26 can be evaluated again. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +from collections import Counter +from collections.abc import Mapping +from copy import deepcopy +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + +from . import metadata_fabric_protected_real_feature_reexecution_gate as reexecution + +PACKET_SCHEMA = "gda.protected_profile_owner_decision_packet.v1" +VALIDATION_SCHEMA = "gda.protected_profile_owner_decision_packet_validation.v1" +PACKET_STATUS = "unresolved_owner_decisions" +GROUP_STATUS = "unresolved" + +REPO_ROOT = Path(__file__).resolve().parent.parent +DEFAULT_PREDECESSOR_PATH = reexecution.DEFAULT_DECISION_PATH +DEFAULT_PACKET_PATH = ( + REPO_ROOT / "docs/evidence/metadata-fabric-protected-profile-decision-packet-2026-07-31.json" +) + +PREDECESSOR_FILE_SHA256 = "62624b96d83b085cbb82d29d618042d7f4faa5193847c26af859ec1c87cd4f11" +PREDECESSOR_DECISION_SHA256 = "39246eacdd1793f23aecb71195cc4c9d8c63d7125aad8cd9bbb59a96c588cd73" +PREDECESSOR_CONTRACT_SHA256 = "39411248b37b7d8d43ac7ad37737de15d6b6d4c5e4feb2088f0a87cec888b5f9" + +IDENTITY_PROFILE = "config/metadata-fabric-identity.production.yaml" +OBJECT_STORE_PROFILE = "config/metadata-fabric-object-store.production.yaml" +IDENTITY_VERIFY = ( + "python -m data_agent.metadata_fabric_identity_gate evaluate " + f"--profile {IDENTITY_PROFILE} --attestation $GDA_IDENTITY_ATTESTATION" +) +OBJECT_STORE_VERIFY = ( + "python -m data_agent.metadata_fabric_object_store_gate evaluate " + f"--profile {OBJECT_STORE_PROFILE} --attestation $GDA_OBJECT_STORE_ATTESTATION" +) + +PACKET_INVENTORY = { + "schema", + "status", + "generated_at", + "predecessor_binding", + "decision_groups", + "blocker_summary", + "resolution_policy", + "claims", + "packet_sha256", +} +GROUP_INVENTORY = { + "decision_id", + "title", + "owner_roles", + "depends_on", + "blockers", + "profile_paths", + "allowed_choices", + "forbidden_choices", + "required_artifacts", + "protected_verification_command", + "status", +} +CLAIMS = { + "owner_decisions_approved", + "production_profiles_ready", + "protected_identity_attested", + "protected_object_store_attested", + "ready_for_protected_reexecution", + "scheduler_submission_authorized", + "provider_mutation_authorized", + "production_ingestion_verified", + "production_ready", +} + + +def _profile_blockers(prefix: str, *paths: str) -> list[str]: + return [f"{prefix}.profile:{path}" for path in paths] + + +DECISION_GROUP_SPECS: tuple[dict[str, Any], ...] = ( + { + "decision_id": "identity-federation", + "title": "Identity federation", + "owner_roles": ["Security Architecture", "IAM Platform"], + "depends_on": [], + "blockers": _profile_blockers( + "identity", + "federation.audience", + "federation.decision_status", + "federation.discovery_uri", + "federation.issuer", + "federation.jwks_uri", + "federation.token_exchange_mode", + "federation.trust_policy_reference", + ), + "allowed_choices": [ + "An approved production OIDC authority with discovery and JWKS endpoints", + ( + "A short-lived token exchange mode and reviewed trust-policy " + "reference accepted by ADR-053" + ), + ], + "forbidden_choices": [ + "Static or long-lived workload credentials", + "Local, cluster-only, example, or unreviewed identity endpoints", + ], + "required_artifacts": [ + "Approved identity federation architecture decision", + "OIDC authority ownership and trust-policy review evidence", + ], + "protected_verification_command": IDENTITY_VERIFY, + }, + { + "decision_id": "openmetadata-identity-binding", + "title": "OpenMetadata identity binding", + "owner_roles": ["Metadata Platform", "IAM Platform"], + "depends_on": ["identity-federation"], + "blockers": _profile_blockers( + "identity", + "providers.openmetadata.authentication_component_reference", + "providers.openmetadata.environment_binding", + "providers.openmetadata.integration_mode", + "providers.openmetadata.kubernetes_service_account", + "providers.openmetadata.namespace_template", + "providers.openmetadata.workload_identity_reference", + ), + "allowed_choices": [ + "Provider-native OIDC or an approved identity-aware proxy", + "A tenant-bound workload identity and least-privilege service account", + ], + "forbidden_choices": [ + "Bootstrap administrator identity for production projection", + "Direct provider access outside the attested workload identity path", + ], + "required_artifacts": [ + "OpenMetadata authentication component and environment binding references", + "Service-account and workload-subject ownership review", + ], + "protected_verification_command": IDENTITY_VERIFY, + }, + { + "decision_id": "gravitino-identity-binding", + "title": "Gravitino identity binding", + "owner_roles": ["Lakehouse Platform", "IAM Platform"], + "depends_on": ["identity-federation"], + "blockers": _profile_blockers( + "identity", + "providers.gravitino.authentication_component_reference", + "providers.gravitino.environment_binding", + "providers.gravitino.integration_mode", + "providers.gravitino.kubernetes_service_account", + "providers.gravitino.namespace_template", + "providers.gravitino.workload_identity_reference", + ), + "allowed_choices": [ + "An approved OIDC authenticator or identity-aware proxy", + "A tenant-bound workload identity and least-privilege service account", + ], + "forbidden_choices": [ + "Unauthenticated production catalog access", + "Direct provider access outside the attested workload identity path", + ], + "required_artifacts": [ + "Gravitino authentication component and environment binding references", + "Service-account and workload-subject ownership review", + ], + "protected_verification_command": IDENTITY_VERIFY, + }, + { + "decision_id": "metadata-tls", + "title": "Metadata transport security", + "owner_roles": ["Security Architecture", "Platform SRE"], + "depends_on": ["identity-federation"], + "blockers": _profile_blockers( + "identity", + "tls.certificate_policy_reference", + "tls.gravitino_endpoint", + "tls.openmetadata_endpoint", + "tls.trust_bundle_reference", + ), + "allowed_choices": [ + "Production HTTPS endpoints with reviewed certificate and trust-bundle policies", + "TLS 1.2 or newer with mTLS on internal hops as frozen in the profile", + ], + "forbidden_choices": [ + "Plaintext provider endpoints", + "Local, cluster-only, example, or certificate-bypass endpoints", + ], + "required_artifacts": [ + "Certificate lifecycle policy", + "Trust-bundle ownership and endpoint inventory", + ], + "protected_verification_command": IDENTITY_VERIFY, + }, + { + "decision_id": "persistent-catalog", + "title": "Persistent catalog", + "owner_roles": ["Lakehouse Platform", "Database SRE"], + "depends_on": ["gravitino-identity-binding", "metadata-tls"], + "blockers": _profile_blockers( + "identity", + "catalog.backup_policy_reference", + "catalog.catalog_reference", + "catalog.decision_status", + "catalog.gravitino_backend", + "catalog.persistence_reference", + ), + "allowed_choices": [ + "An approved Iceberg REST or JDBC catalog with persistent metadata storage", + "A catalog binding with reviewed backup and restore policy", + ], + "forbidden_choices": [ + "In-memory catalog authority", + "Ephemeral metadata storage or unverified backup claims", + ], + "required_artifacts": [ + "Approved catalog backend decision", + "Persistence, backup, and restore ownership references", + ], + "protected_verification_command": IDENTITY_VERIFY, + }, + { + "decision_id": "metadata-tenancy", + "title": "Metadata tenancy", + "owner_roles": ["Data Governance", "Security Architecture"], + "depends_on": [ + "openmetadata-identity-binding", + "gravitino-identity-binding", + ], + "blockers": _profile_blockers( + "identity", + "tenancy.isolation_mode", + "tenancy.policy_reference", + ), + "allowed_choices": [ + "An explicit tenant-isolation mode bound to the gda_tenant_id claim", + "A reviewed policy that includes cross-tenant denial probes", + ], + "forbidden_choices": [ + "Shared unscoped provider identity", + "Tenant isolation based only on caller-supplied naming conventions", + ], + "required_artifacts": [ + "Tenant isolation policy", + "Cross-tenant denial test plan", + ], + "protected_verification_command": IDENTITY_VERIFY, + }, + { + "decision_id": "identity-operations", + "title": "Identity operations and runbooks", + "owner_roles": ["IAM Platform", "Security Operations", "Platform SRE"], + "depends_on": [ + "identity-federation", + "openmetadata-identity-binding", + "gravitino-identity-binding", + "metadata-tls", + "persistent-catalog", + "metadata-tenancy", + ], + "blockers": _profile_blockers( + "identity", + "operations.audit_log_reference", + "operations.identity_owner", + "operations.incident_owner", + "operations.revocation_slo_minutes", + "operations.rollback_runbook.uri", + "operations.rollback_runbook.version", + "operations.rotation_slo_minutes", + "operations.runbook.uri", + "operations.runbook.version", + "operations.security_owner", + ), + "allowed_choices": [ + "Named ownership references, measurable rotation and revocation SLOs", + "Versioned production runbooks and auditable identity events", + ], + "forbidden_choices": [ + "Unowned incident response or unversioned runbooks", + "Manual credential rotation without a protected audit trail", + ], + "required_artifacts": [ + "Identity operations ownership record", + "Versioned incident, rotation, revocation, and rollback runbooks", + "Audit-log delivery reference", + ], + "protected_verification_command": IDENTITY_VERIFY, + }, + { + "decision_id": "object-store-provider", + "title": "Object-store provider", + "owner_roles": ["Cloud Platform", "Storage Platform"], + "depends_on": [], + "blockers": _profile_blockers( + "object_store", + "provider.account_reference", + "provider.bucket", + "provider.decision_status", + "provider.endpoint", + "provider.failure_domain_reference", + "provider.infrastructure_reference", + "provider.provider_type", + "provider.recovery_region", + "provider.region", + ), + "allowed_choices": [ + "A production provider accepted by ADR-057 with independently owned infrastructure", + "Distinct primary and recovery regions with multi-zone durability", + ], + "forbidden_choices": [ + "The retained local MinIO namespace as production authority", + "A provider account, bucket, or failure domain selected without owner approval", + ], + "required_artifacts": [ + "Approved provider and region architecture decision", + "Infrastructure, account, bucket, and failure-domain ownership references", + ], + "protected_verification_command": OBJECT_STORE_VERIFY, + }, + { + "decision_id": "object-store-workload-identity", + "title": "Object-store workload identity", + "owner_roles": ["Cloud Platform", "IAM Platform", "Security Architecture"], + "depends_on": ["identity-federation", "object-store-provider"], + "blockers": _profile_blockers( + "object_store", + "identity.bucket_policy_reference", + "identity.integration_mode", + "identity.kubernetes_service_account", + "identity.least_privilege_policy_reference", + "identity.workload_identity_reference", + ), + "allowed_choices": [ + "OIDC workload federation with the frozen least-privilege operation inventory", + "A tenant-bound service account with a maximum 900-second session", + ], + "forbidden_choices": [ + "Static access keys or node-wide provider credentials", + "Bucket permissions broader than the checked operation inventory", + ], + "required_artifacts": [ + "Workload identity and service-account binding", + "Least-privilege and bucket-policy reviews", + ], + "protected_verification_command": OBJECT_STORE_VERIFY, + }, + { + "decision_id": "object-store-transport", + "title": "Object-store transport", + "owner_roles": ["Network Platform", "Security Architecture"], + "depends_on": ["object-store-provider"], + "blockers": _profile_blockers( + "object_store", + "transport.certificate_policy_reference", + "transport.dns_policy_reference", + "transport.endpoint", + "transport.private_connectivity_reference", + "transport.trust_bundle_reference", + ), + "allowed_choices": [ + "The approved provider HTTPS endpoint over reviewed private connectivity and DNS", + "TLS 1.2 or newer with managed trust and certificate lifecycle", + ], + "forbidden_choices": [ + "Public or plaintext data paths not covered by the network policy", + "Local, example, certificate-bypass, or split-horizon endpoints without review", + ], + "required_artifacts": [ + "Private connectivity and DNS policy references", + "Certificate lifecycle and trust-bundle references", + ], + "protected_verification_command": OBJECT_STORE_VERIFY, + }, + { + "decision_id": "object-store-encryption", + "title": "Object-store encryption and KMS", + "owner_roles": ["Cloud Security", "Key Management"], + "depends_on": ["object-store-provider", "object-store-workload-identity"], + "blockers": _profile_blockers( + "object_store", + "encryption.key_policy_reference", + "encryption.key_reference", + "encryption.rotation_days", + ), + "allowed_choices": [ + "A provider KMS key with reviewed key policy and rotation of at most 365 days", + "Server-side KMS encryption with bucket-key support as frozen in the profile", + ], + "forbidden_choices": [ + "Unmanaged or shared encryption keys without tenant and workload controls", + "Encryption claims without protected read/write verification", + ], + "required_artifacts": [ + "KMS key ownership and policy review", + "Key rotation decision and recovery procedure", + ], + "protected_verification_command": OBJECT_STORE_VERIFY, + }, + { + "decision_id": "object-store-durability-consistency", + "title": "Object-store durability and consistency", + "owner_roles": ["Storage Platform", "Platform SRE"], + "depends_on": [ + "object-store-provider", + "object-store-transport", + "object-store-encryption", + ], + "blockers": _profile_blockers( + "object_store", + "consistency.multipart_upload_cleanup_reference", + "consistency.orphan_file_cleanup_reference", + "durability.recovery_bucket_reference", + "durability.replication_policy_reference", + "durability.retention_policy_reference", + ), + "allowed_choices": [ + ( + "Versioned recovery and asynchronous cross-region replication " + "meeting the frozen RPO/RTO" + ), + "Reviewed multipart-upload and orphan-file cleanup controls", + ], + "forbidden_choices": [ + "Single-copy durability or same-region recovery authority", + "Rename-based correctness assumptions for object storage", + ], + "required_artifacts": [ + "Retention and replication policies", + "Recovery bucket ownership and cleanup-control references", + ], + "protected_verification_command": OBJECT_STORE_VERIFY, + }, + { + "decision_id": "object-store-tenancy", + "title": "Object-store tenancy", + "owner_roles": ["Data Governance", "Cloud Security"], + "depends_on": ["object-store-provider", "object-store-workload-identity"], + "blockers": _profile_blockers( + "object_store", + "tenancy.policy_reference", + ), + "allowed_choices": [ + "Bucket-prefix and provider-policy isolation bound to tenants/{tenant_id}/warehouse/", + "A policy with public-access blocking and cross-tenant denial probes", + ], + "forbidden_choices": [ + "Shared prefixes without provider-enforced tenant policy", + "Public access or client-only tenant filtering", + ], + "required_artifacts": [ + "Tenant storage isolation policy", + "Cross-tenant and public-access denial test plan", + ], + "protected_verification_command": OBJECT_STORE_VERIFY, + }, + { + "decision_id": "object-store-operations", + "title": "Object-store operations, SLOs, and runbooks", + "owner_roles": ["Storage Platform", "Platform SRE", "Security Operations"], + "depends_on": [ + "object-store-provider", + "object-store-workload-identity", + "object-store-transport", + "object-store-encryption", + "object-store-durability-consistency", + "object-store-tenancy", + ], + "blockers": _profile_blockers( + "object_store", + "operations.attestation_policy_reference", + "operations.audit_log_reference", + "operations.availability_slo_percent", + "operations.incident_owner", + "operations.latency_slo_ms", + "operations.metrics_alert_reference", + "operations.platform_owner", + "operations.recovery_runbook.uri", + "operations.recovery_runbook.version", + "operations.rollback_runbook.uri", + "operations.rollback_runbook.version", + "operations.runbook.uri", + "operations.runbook.version", + "operations.security_owner", + "operations.storage_owner", + ), + "allowed_choices": [ + "Named ownership, measurable availability and latency SLOs, and actionable alerts", + ( + "Versioned operation, recovery, and rollback runbooks tied to " + "protected attestation policy" + ), + ], + "forbidden_choices": [ + "Unowned incidents, unversioned runbooks, or synthetic-only alerts", + "Availability or recovery claims without protected failure rehearsal", + ], + "required_artifacts": [ + "Operations and incident ownership record", + "SLO, alert, audit-log, and attestation policy references", + "Versioned operation, recovery, and rollback runbooks", + ], + "protected_verification_command": OBJECT_STORE_VERIFY, + }, + { + "decision_id": "protected-identity-attestation", + "title": "Protected identity attestation", + "owner_roles": ["Security Operations", "Platform SRE"], + "depends_on": ["identity-operations"], + "blockers": [ + "identity.attestation:production identity attestation is missing", + ], + "allowed_choices": [ + "A fresh ADR-053 attestation emitted by the protected production identity environment", + ( + "An attestation bound to the approved profile, exact source " + "revision, tenant denial, and audit evidence" + ), + ], + "forbidden_choices": [ + "A local, manually asserted, expired, or cross-revision attestation", + "Reusing M3-24 or M3-25 retained material as production identity evidence", + ], + "required_artifacts": [ + "Fresh protected identity attestation", + "Protected workflow and reviewer evidence URI", + ], + "protected_verification_command": IDENTITY_VERIFY, + }, + { + "decision_id": "protected-object-store-attestation", + "title": "Protected object-store attestation", + "owner_roles": ["Storage Platform", "Security Operations", "Platform SRE"], + "depends_on": ["object-store-operations"], + "blockers": [ + "object_store.attestation:production object-store attestation is required", + ], + "allowed_choices": [ + ( + "A fresh ADR-057 attestation emitted by the protected production " + "object-store environment" + ), + ( + "An attestation bound to the approved profile, exact source " + "revision, tenant controls, KMS, and recovery evidence" + ), + ], + "forbidden_choices": [ + "A local, manually asserted, expired, or cross-revision attestation", + "Treating the retained MinIO rehearsal as a production object-store attestation", + ], + "required_artifacts": [ + "Fresh protected object-store attestation", + "Protected workflow and reviewer evidence URI", + ], + "protected_verification_command": OBJECT_STORE_VERIFY, + }, +) + + +class ProtectedProfileDecisionPacketError(RuntimeError): + """The M3-27 decision packet failed closed.""" + + +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) -> datetime: + try: + parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00")) + except ValueError as exc: + raise ProtectedProfileDecisionPacketError("generated_at is not a valid timestamp") from exc + if parsed.tzinfo is None or parsed.utcoffset() is None: + raise ProtectedProfileDecisionPacketError("generated_at must be timezone-aware") + return parsed.astimezone(UTC) + + +def _load_predecessor(path: Path = DEFAULT_PREDECESSOR_PATH) -> dict[str, Any]: + if _file_sha256(path) != PREDECESSOR_FILE_SHA256: + raise ProtectedProfileDecisionPacketError( + "M3-26 predecessor file fingerprint does not match" + ) + decision = _load_json_object(path) + errors = reexecution.validate_decision(decision) + if errors: + raise ProtectedProfileDecisionPacketError( + "M3-26 predecessor is invalid: " + "; ".join(errors) + ) + if ( + decision.get("decision_sha256") != PREDECESSOR_DECISION_SHA256 + or decision.get("contract_sha256") != PREDECESSOR_CONTRACT_SHA256 + or decision.get("status") != reexecution.BLOCKED_STATUS + or decision.get("ready_for_protected_reexecution") is not False + ): + raise ProtectedProfileDecisionPacketError("M3-26 predecessor binding does not match") + return decision + + +def _profile_paths(blockers: list[Any]) -> list[str]: + paths: list[str] = [] + for blocker in blockers: + if not isinstance(blocker, str): + continue + if blocker.startswith("identity.profile:"): + paths.append(f"{IDENTITY_PROFILE}#{blocker.split(':', 1)[1]}") + elif blocker.startswith("object_store.profile:"): + paths.append(f"{OBJECT_STORE_PROFILE}#{blocker.split(':', 1)[1]}") + return paths + + +def _decision_groups() -> list[dict[str, Any]]: + groups: list[dict[str, Any]] = [] + for spec in DECISION_GROUP_SPECS: + group = deepcopy(spec) + group["profile_paths"] = _profile_paths(group["blockers"]) + group["status"] = GROUP_STATUS + groups.append(group) + return groups + + +def _blocker_domain(blocker: Any) -> str: + if not isinstance(blocker, str): + return "unknown" + if blocker.startswith("identity.profile:"): + return "identity_profile" + if blocker.startswith("identity.attestation:"): + return "identity_attestation" + if blocker.startswith("object_store.profile:"): + return "object_store_profile" + if blocker.startswith("object_store.attestation:"): + return "object_store_attestation" + return "unknown" + + +def build_packet( + *, + predecessor_path: Path = DEFAULT_PREDECESSOR_PATH, + generated_at: datetime | None = None, +) -> dict[str, Any]: + decision = _load_predecessor(predecessor_path) + timestamp = (generated_at or datetime.now(UTC)).astimezone(UTC) + groups = _decision_groups() + assigned = [blocker for group in groups for blocker in group["blockers"]] + domains = Counter(_blocker_domain(blocker) for blocker in assigned) + stable = { + "schema": PACKET_SCHEMA, + "status": PACKET_STATUS, + "generated_at": timestamp.isoformat().replace("+00:00", "Z"), + "predecessor_binding": { + "path": str(predecessor_path.relative_to(REPO_ROOT)), + "file_sha256": PREDECESSOR_FILE_SHA256, + "decision_sha256": PREDECESSOR_DECISION_SHA256, + "contract_sha256": PREDECESSOR_CONTRACT_SHA256, + "evaluated_at": decision.get("evaluated_at"), + "status": decision.get("status"), + "blocker_count": len(decision.get("blockers", [])), + "ready_for_protected_reexecution": False, + }, + "decision_groups": groups, + "blocker_summary": { + "expected": len(decision.get("blockers", [])), + "assigned": len(assigned), + "unique": len(set(assigned)), + "decision_groups": len(groups), + "unresolved_decision_groups": len(groups), + "identity_profile": domains["identity_profile"], + "identity_attestation": domains["identity_attestation"], + "object_store_profile": domains["object_store_profile"], + "object_store_attestation": domains["object_store_attestation"], + }, + "resolution_policy": { + "authoritative_profile_paths": [IDENTITY_PROFILE, OBJECT_STORE_PROFILE], + "profile_changes_require_owner_approval": True, + "attestations_must_be_fresh_protected_outputs": True, + "attestations_must_share_source_revision": True, + "credential_material_forbidden": True, + "local_retained_material_promotion_forbidden": True, + "packet_records_decisions": False, + "packet_grants_execution_authority": False, + }, + "claims": {claim: False for claim in sorted(CLAIMS)}, + } + return {**stable, "packet_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 reexecution.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 _dependency_errors(groups: list[Any]) -> list[str]: + errors: list[str] = [] + mappings = [group for group in groups if isinstance(group, Mapping)] + if len(mappings) != len(groups): + errors.append("M3-27 decision group is not an object") + ids = [str(group.get("decision_id")) for group in mappings] + if len(ids) != len(set(ids)): + errors.append("M3-27 decision IDs are not unique") + known = set(ids) + graph: dict[str, list[str]] = {} + for group in mappings: + decision_id = str(group.get("decision_id")) + dependencies = group.get("depends_on") + if not isinstance(dependencies, list) or not all( + isinstance(item, str) for item in dependencies + ): + errors.append(f"M3-27 dependencies are invalid: {decision_id}") + graph[decision_id] = [] + continue + graph[decision_id] = dependencies + unknown = sorted(set(dependencies) - known) + if unknown: + errors.append(f"M3-27 dependencies are unknown for {decision_id}: {','.join(unknown)}") + + visiting: set[str] = set() + visited: set[str] = set() + + def visit(node: str) -> None: + if node in visiting: + errors.append("M3-27 dependency graph contains a cycle") + return + if node in visited: + return + visiting.add(node) + for dependency in graph.get(node, []): + if dependency in graph: + visit(dependency) + visiting.remove(node) + visited.add(node) + + for decision_id in graph: + visit(decision_id) + return errors + + +def validate_packet(packet: Mapping[str, Any]) -> list[str]: + errors: list[str] = [] + if set(packet) != PACKET_INVENTORY: + errors.append("M3-27 packet inventory does not match") + stable = {key: value for key, value in packet.items() if key != "packet_sha256"} + if packet.get("packet_sha256") != canonical_json_fingerprint(stable): + errors.append("M3-27 packet fingerprint does not match") + + groups_value = packet.get("decision_groups") + groups = groups_value if isinstance(groups_value, list) else [] + if not isinstance(groups_value, list): + errors.append("M3-27 decision groups are not a list") + errors.extend(_dependency_errors(groups)) + blockers: list[str] = [] + for group in groups: + if not isinstance(group, Mapping): + errors.append("M3-27 decision group is not an object") + continue + decision_id = str(group.get("decision_id")) + if set(group) != GROUP_INVENTORY: + errors.append(f"M3-27 group inventory does not match: {decision_id}") + if group.get("status") != GROUP_STATUS: + errors.append(f"M3-27 group is not unresolved: {decision_id}") + for field in ( + "owner_roles", + "blockers", + "allowed_choices", + "forbidden_choices", + "required_artifacts", + ): + value = group.get(field) + if ( + not isinstance(value, list) + or not value + or not all(isinstance(item, str) and item for item in value) + ): + errors.append(f"M3-27 group {field} is invalid: {decision_id}") + group_blockers = group.get("blockers") + if isinstance(group_blockers, list): + blockers.extend(str(item) for item in group_blockers) + if group.get("profile_paths") != _profile_paths(group_blockers): + errors.append(f"M3-27 profile paths do not match: {decision_id}") + if not isinstance(group.get("protected_verification_command"), str): + errors.append(f"M3-27 verification command is invalid: {decision_id}") + + try: + expected_blockers = [str(item) for item in _load_predecessor()["blockers"]] + except (OSError, TypeError, ValueError, ProtectedProfileDecisionPacketError) as exc: + errors.append(f"M3-27 predecessor is invalid: {exc}") + expected_blockers = [] + counts = Counter(blockers) + duplicates = sorted(item for item, count in counts.items() if count != 1) + if duplicates: + errors.append("M3-27 blockers are not assigned exactly once") + if set(blockers) != set(expected_blockers): + errors.append("M3-27 blocker coverage does not match M3-26") + + claims = packet.get("claims") + if not isinstance(claims, Mapping) or set(claims) != CLAIMS: + errors.append("M3-27 claims inventory does not match") + elif any(value is not False for value in claims.values()): + errors.append("M3-27 packet may not assert production claims") + if packet.get("status") != PACKET_STATUS: + errors.append("M3-27 packet must remain unresolved") + if _sensitive_paths(packet): + errors.append("M3-27 packet contains credential-bearing fields") + + try: + generated_at = _parse_time(packet.get("generated_at")) + expected = build_packet(generated_at=generated_at) + except (OSError, TypeError, ValueError, ProtectedProfileDecisionPacketError) as exc: + errors.append(f"M3-27 packet inputs are invalid: {exc}") + expected = None + if expected is not None and dict(packet) != expected: + errors.append("M3-27 packet does not match current bound inputs") + return sorted(set(errors)) + + +def build_validation_report(packet_path: Path = DEFAULT_PACKET_PATH) -> dict[str, Any]: + try: + packet = _load_json_object(packet_path) + errors = validate_packet(packet) + except (OSError, TypeError, ValueError, json.JSONDecodeError) as exc: + packet = {} + errors = [f"M3-27 packet is unreadable: {type(exc).__name__}"] + summary = packet.get("blocker_summary") + return { + "schema": VALIDATION_SCHEMA, + "status": "valid" if not errors else "invalid", + "errors": errors, + "packet_sha256": packet.get("packet_sha256"), + "packet_status": packet.get("status"), + "blocker_summary": dict(summary) if isinstance(summary, Mapping) else None, + } + + +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) + validate = subparsers.add_parser("validate") + validate.add_argument("--packet", type=Path, default=DEFAULT_PACKET_PATH) + snapshot = subparsers.add_parser("snapshot") + snapshot.add_argument("--output", type=Path, default=DEFAULT_PACKET_PATH) + args = parser.parse_args(argv) + try: + if args.command == "validate": + report = build_validation_report(args.packet) + exit_code = 0 if report["status"] == "valid" else 1 + else: + report = build_packet() + _write_json(args.output, report) + exit_code = 0 + print(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True)) + return exit_code + except ( + OSError, + TypeError, + ValueError, + json.JSONDecodeError, + ProtectedProfileDecisionPacketError, + ) as exc: + print(f"protected profile decision packet: {exc}") + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/data_agent/test_metadata_fabric_protected_profile_decision_packet.py b/data_agent/test_metadata_fabric_protected_profile_decision_packet.py new file mode 100644 index 00000000..993e874b --- /dev/null +++ b/data_agent/test_metadata_fabric_protected_profile_decision_packet.py @@ -0,0 +1,155 @@ +import json +from copy import deepcopy +from datetime import UTC, datetime + +from data_agent import metadata_fabric_protected_profile_decision_packet as packet + +GENERATED_AT = datetime(2026, 7, 31, 8, 30, tzinfo=UTC) + + +def _checked_packet() -> dict: + return json.loads(packet.DEFAULT_PACKET_PATH.read_text(encoding="utf-8")) + + +def _rehash(value: dict) -> None: + stable = {key: item for key, item in value.items() if key != "packet_sha256"} + value["packet_sha256"] = packet.canonical_json_fingerprint(stable) + + +def test_packet_assigns_every_m3_26_blocker_exactly_once(): + value = packet.build_packet(generated_at=GENERATED_AT) + predecessor = json.loads(packet.DEFAULT_PREDECESSOR_PATH.read_text(encoding="utf-8")) + assigned = [blocker for group in value["decision_groups"] for blocker in group["blockers"]] + + assert len(value["decision_groups"]) == 16 + assert len(assigned) == 85 + assert len(set(assigned)) == 85 + assert set(assigned) == set(predecessor["blockers"]) + assert value["blocker_summary"] == { + "assigned": 85, + "decision_groups": 16, + "expected": 85, + "identity_attestation": 1, + "identity_profile": 40, + "object_store_attestation": 1, + "object_store_profile": 43, + "unique": 85, + "unresolved_decision_groups": 16, + } + + +def test_checked_packet_is_valid_unresolved_and_non_authoritative(): + value = _checked_packet() + report = packet.build_validation_report() + + assert packet.validate_packet(value) == [] + assert report["status"] == "valid" + assert report["errors"] == [] + assert value["status"] == packet.PACKET_STATUS + assert all(group["status"] == packet.GROUP_STATUS for group in value["decision_groups"]) + assert set(value["claims"]) == packet.CLAIMS + assert not any(value["claims"].values()) + assert value["resolution_policy"]["packet_records_decisions"] is False + assert value["resolution_policy"]["packet_grants_execution_authority"] is False + + +def test_dependency_graph_rejects_unknown_edges_and_cycles(): + unknown = packet.build_packet(generated_at=GENERATED_AT) + unknown["decision_groups"][0]["depends_on"] = ["missing-owner-decision"] + _rehash(unknown) + unknown_errors = packet.validate_packet(unknown) + + cyclic = packet.build_packet(generated_at=GENERATED_AT) + cyclic["decision_groups"][0]["depends_on"] = ["protected-identity-attestation"] + _rehash(cyclic) + cyclic_errors = packet.validate_packet(cyclic) + + assert any("dependencies are unknown" in error for error in unknown_errors) + assert "M3-27 dependency graph contains a cycle" in cyclic_errors + + +def test_malformed_group_and_blocker_fail_closed_without_exception(): + malformed_group = packet.build_packet(generated_at=GENERATED_AT) + malformed_group["decision_groups"][0] = "not-an-object" + _rehash(malformed_group) + + malformed_blocker = packet.build_packet(generated_at=GENERATED_AT) + malformed_blocker["decision_groups"][0]["blockers"][0] = 7 + _rehash(malformed_blocker) + + group_errors = packet.validate_packet(malformed_group) + blocker_errors = packet.validate_packet(malformed_blocker) + + assert "M3-27 decision group is not an object" in group_errors + assert any("group blockers is invalid" in error for error in blocker_errors) + assert "M3-27 blocker coverage does not match M3-26" in blocker_errors + + +def test_outer_rehash_cannot_hide_duplicate_resolution_or_overclaim(): + value = packet.build_packet(generated_at=GENERATED_AT) + value["decision_groups"][1]["blockers"].append(value["decision_groups"][0]["blockers"][0]) + value["decision_groups"][1]["profile_paths"] = packet._profile_paths( + value["decision_groups"][1]["blockers"] + ) + value["decision_groups"][0]["status"] = "approved" + value["claims"]["production_profiles_ready"] = True + _rehash(value) + + errors = packet.validate_packet(value) + + assert "M3-27 blockers are not assigned exactly once" in errors + assert any("group is not unresolved" in error for error in errors) + assert "M3-27 packet may not assert production claims" in errors + assert "M3-27 packet does not match current bound inputs" in errors + + +def test_profile_paths_are_exact_and_attestations_have_no_profile_path(): + value = packet.build_packet(generated_at=GENERATED_AT) + + for group in value["decision_groups"]: + assert group["profile_paths"] == packet._profile_paths(group["blockers"]) + command = group["protected_verification_command"] + assert " evaluate " in command + assert "--attestation $GDA_" in command + if group["decision_id"].startswith("protected-"): + assert group["profile_paths"] == [] + + +def test_checked_packet_contains_no_local_path_payload_or_credential_fields(): + rendered = packet.DEFAULT_PACKET_PATH.read_text(encoding="utf-8") + value = json.loads(rendered) + + assert "/Users/" not in rendered + assert "Downloads/" not in rendered + assert "geometry_values" not in rendered + assert packet._sensitive_paths(value) == [] + assert value["resolution_policy"]["credential_material_forbidden"] is True + assert value["resolution_policy"]["local_retained_material_promotion_forbidden"] is True + + +def test_predecessor_or_group_tampering_fails_closed(tmp_path): + predecessor = tmp_path / "m3-26.json" + predecessor.write_bytes(packet.DEFAULT_PREDECESSOR_PATH.read_bytes() + b"\n") + + try: + packet.build_packet( + predecessor_path=predecessor, + generated_at=GENERATED_AT, + ) + except packet.ProtectedProfileDecisionPacketError as exc: + assert "predecessor file fingerprint does not match" in str(exc) + else: + raise AssertionError("drifted predecessor was accepted") + + value = deepcopy(_checked_packet()) + value["decision_groups"][0]["allowed_choices"] = ["Unreviewed provider"] + _rehash(value) + assert "M3-27 packet does not match current bound inputs" in packet.validate_packet(value) + + +def test_wrapper_is_strict_and_invokes_packet_validator(): + wrapper = packet.REPO_ROOT / "scripts/metadata-fabric-protected-profile-decision-packet.sh" + text = wrapper.read_text(encoding="utf-8") + + assert "set -euo pipefail" in text + assert "metadata_fabric_protected_profile_decision_packet" in text diff --git a/docs/architecture-decisions/adr-073-protected-profile-owner-decision-packet.md b/docs/architecture-decisions/adr-073-protected-profile-owner-decision-packet.md new file mode 100644 index 00000000..fd06bbc0 --- /dev/null +++ b/docs/architecture-decisions/adr-073-protected-profile-owner-decision-packet.md @@ -0,0 +1,77 @@ +# ADR-073: Protected profile owner decision packet + +**Status**: Accepted + +**Date**: 2026-07-31 + +## Context + +ADR-072/M3-26 binds the checked Chongqing 20-feature predecessor to the production identity and object-store readiness gates. Its deterministic decision is valid but closed: 40 identity profile fields, 43 object-store profile fields, one identity attestation and one object-store attestation remain unresolved. + +Those 85 blockers are machine-readable, but a flat list does not identify who must decide each concern, what it depends on, which profile paths may change, or which protected artifacts must prove the result. Selecting an IdP, provider account, bucket, region, KMS key or production owner inside the repository would exceed repository authority and create false production claims. + +## Decision + +Adopt the checked M3-27 protected profile owner decision packet as the only repository-maintained decomposition of the M3-26 blocker inventory. + +The packet binds the M3-26 evidence file SHA `62624b96d83b085cbb82d29d618042d7f4faa5193847c26af859ec1c87cd4f11`, decision SHA `39246eacdd1793f23aecb71195cc4c9d8c63d7125aad8cd9bbb59a96c588cd73` and contract SHA `39411248b37b7d8d43ac7ad37737de15d6b6d4c5e4feb2088f0a87cec888b5f9`. + +It assigns every blocker exactly once across 16 dependency-ordered groups: + +1. identity federation: 7; +2. OpenMetadata identity binding: 6; +3. Gravitino identity binding: 6; +4. metadata TLS: 4; +5. persistent catalog: 5; +6. metadata tenancy: 2; +7. identity operations and runbooks: 10; +8. object-store provider: 9; +9. object-store workload identity: 5; +10. object-store transport: 5; +11. object-store encryption and KMS: 3; +12. object-store durability and consistency: 5; +13. object-store tenancy: 1; +14. object-store operations, SLOs and runbooks: 15; +15. protected identity attestation: 1; +16. protected object-store attestation: 1. + +Each group contains stable owner roles, dependencies, exact M3-26 blockers, writable profile paths, allowed and forbidden decision boundaries, required artifacts, a protected verification command and `status=unresolved`. + +The validator fails closed when: + +- the M3-26 predecessor file or bound SHA changes; +- any blocker is missing, duplicated, invented or assigned more than once; +- a dependency is unknown or the graph contains a cycle; +- any group is marked resolved or its checked content drifts; +- a credential-bearing field appears; +- any execution, ingestion or production claim becomes true. + +## Authority boundary + +The packet does not record approved production choices. Owners must approve and materialize decisions in: + +- `config/metadata-fabric-identity.production.yaml`; +- `config/metadata-fabric-object-store.production.yaml`. + +The two final blockers can only be removed by fresh outputs from their protected environments. Both attestations must bind the approved profiles and the same source revision. Passing their individual gates still requires a new M3-26 composite evaluation. + +The packet never creates a PolicyDecision, Approval, PlatformRun, scheduler command, provider mutation or production data product. It cannot promote retained M3-24/M3-25 material and it does not authorize a fresh ingestion. + +## Consequences + +**Positive**: every external dependency is assignable, reviewable and testable without inventing production infrastructure choices. + +**Positive**: exact blocker coverage and the dependency graph are enforced in CI, so owner work cannot silently diverge from the admission gate. + +**Negative**: M3-27 intentionally leaves all 16 groups unresolved and does not reduce the current count of 85 blockers. + +**Negative**: production progress now requires decisions and protected evidence from IAM, metadata, lakehouse, cloud, storage, network, security, governance and SRE owners outside repository code. + +## Verification + +```bash +./scripts/metadata-fabric-protected-profile-decision-packet.sh validate +python -m pytest data_agent/test_metadata_fabric_protected_profile_decision_packet.py -q +``` + +The checked packet SHA is `b23e1becafa0c91541a653d467e7959107008d4dc41576018b57b564e6b46a36`. diff --git a/docs/evidence/metadata-fabric-protected-profile-decision-packet-2026-07-31.json b/docs/evidence/metadata-fabric-protected-profile-decision-packet-2026-07-31.json new file mode 100644 index 00000000..6325cdff --- /dev/null +++ b/docs/evidence/metadata-fabric-protected-profile-decision-packet-2026-07-31.json @@ -0,0 +1,702 @@ +{ + "blocker_summary": { + "assigned": 85, + "decision_groups": 16, + "expected": 85, + "identity_attestation": 1, + "identity_profile": 40, + "object_store_attestation": 1, + "object_store_profile": 43, + "unique": 85, + "unresolved_decision_groups": 16 + }, + "claims": { + "owner_decisions_approved": false, + "production_ingestion_verified": false, + "production_profiles_ready": false, + "production_ready": false, + "protected_identity_attested": false, + "protected_object_store_attested": false, + "provider_mutation_authorized": false, + "ready_for_protected_reexecution": false, + "scheduler_submission_authorized": false + }, + "decision_groups": [ + { + "allowed_choices": [ + "An approved production OIDC authority with discovery and JWKS endpoints", + "A short-lived token exchange mode and reviewed trust-policy reference accepted by ADR-053" + ], + "blockers": [ + "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" + ], + "decision_id": "identity-federation", + "depends_on": [], + "forbidden_choices": [ + "Static or long-lived workload credentials", + "Local, cluster-only, example, or unreviewed identity endpoints" + ], + "owner_roles": [ + "Security Architecture", + "IAM Platform" + ], + "profile_paths": [ + "config/metadata-fabric-identity.production.yaml#federation.audience", + "config/metadata-fabric-identity.production.yaml#federation.decision_status", + "config/metadata-fabric-identity.production.yaml#federation.discovery_uri", + "config/metadata-fabric-identity.production.yaml#federation.issuer", + "config/metadata-fabric-identity.production.yaml#federation.jwks_uri", + "config/metadata-fabric-identity.production.yaml#federation.token_exchange_mode", + "config/metadata-fabric-identity.production.yaml#federation.trust_policy_reference" + ], + "protected_verification_command": "python -m data_agent.metadata_fabric_identity_gate evaluate --profile config/metadata-fabric-identity.production.yaml --attestation $GDA_IDENTITY_ATTESTATION", + "required_artifacts": [ + "Approved identity federation architecture decision", + "OIDC authority ownership and trust-policy review evidence" + ], + "status": "unresolved", + "title": "Identity federation" + }, + { + "allowed_choices": [ + "Provider-native OIDC or an approved identity-aware proxy", + "A tenant-bound workload identity and least-privilege service account" + ], + "blockers": [ + "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" + ], + "decision_id": "openmetadata-identity-binding", + "depends_on": [ + "identity-federation" + ], + "forbidden_choices": [ + "Bootstrap administrator identity for production projection", + "Direct provider access outside the attested workload identity path" + ], + "owner_roles": [ + "Metadata Platform", + "IAM Platform" + ], + "profile_paths": [ + "config/metadata-fabric-identity.production.yaml#providers.openmetadata.authentication_component_reference", + "config/metadata-fabric-identity.production.yaml#providers.openmetadata.environment_binding", + "config/metadata-fabric-identity.production.yaml#providers.openmetadata.integration_mode", + "config/metadata-fabric-identity.production.yaml#providers.openmetadata.kubernetes_service_account", + "config/metadata-fabric-identity.production.yaml#providers.openmetadata.namespace_template", + "config/metadata-fabric-identity.production.yaml#providers.openmetadata.workload_identity_reference" + ], + "protected_verification_command": "python -m data_agent.metadata_fabric_identity_gate evaluate --profile config/metadata-fabric-identity.production.yaml --attestation $GDA_IDENTITY_ATTESTATION", + "required_artifacts": [ + "OpenMetadata authentication component and environment binding references", + "Service-account and workload-subject ownership review" + ], + "status": "unresolved", + "title": "OpenMetadata identity binding" + }, + { + "allowed_choices": [ + "An approved OIDC authenticator or identity-aware proxy", + "A tenant-bound workload identity and least-privilege service account" + ], + "blockers": [ + "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" + ], + "decision_id": "gravitino-identity-binding", + "depends_on": [ + "identity-federation" + ], + "forbidden_choices": [ + "Unauthenticated production catalog access", + "Direct provider access outside the attested workload identity path" + ], + "owner_roles": [ + "Lakehouse Platform", + "IAM Platform" + ], + "profile_paths": [ + "config/metadata-fabric-identity.production.yaml#providers.gravitino.authentication_component_reference", + "config/metadata-fabric-identity.production.yaml#providers.gravitino.environment_binding", + "config/metadata-fabric-identity.production.yaml#providers.gravitino.integration_mode", + "config/metadata-fabric-identity.production.yaml#providers.gravitino.kubernetes_service_account", + "config/metadata-fabric-identity.production.yaml#providers.gravitino.namespace_template", + "config/metadata-fabric-identity.production.yaml#providers.gravitino.workload_identity_reference" + ], + "protected_verification_command": "python -m data_agent.metadata_fabric_identity_gate evaluate --profile config/metadata-fabric-identity.production.yaml --attestation $GDA_IDENTITY_ATTESTATION", + "required_artifacts": [ + "Gravitino authentication component and environment binding references", + "Service-account and workload-subject ownership review" + ], + "status": "unresolved", + "title": "Gravitino identity binding" + }, + { + "allowed_choices": [ + "Production HTTPS endpoints with reviewed certificate and trust-bundle policies", + "TLS 1.2 or newer with mTLS on internal hops as frozen in the profile" + ], + "blockers": [ + "identity.profile:tls.certificate_policy_reference", + "identity.profile:tls.gravitino_endpoint", + "identity.profile:tls.openmetadata_endpoint", + "identity.profile:tls.trust_bundle_reference" + ], + "decision_id": "metadata-tls", + "depends_on": [ + "identity-federation" + ], + "forbidden_choices": [ + "Plaintext provider endpoints", + "Local, cluster-only, example, or certificate-bypass endpoints" + ], + "owner_roles": [ + "Security Architecture", + "Platform SRE" + ], + "profile_paths": [ + "config/metadata-fabric-identity.production.yaml#tls.certificate_policy_reference", + "config/metadata-fabric-identity.production.yaml#tls.gravitino_endpoint", + "config/metadata-fabric-identity.production.yaml#tls.openmetadata_endpoint", + "config/metadata-fabric-identity.production.yaml#tls.trust_bundle_reference" + ], + "protected_verification_command": "python -m data_agent.metadata_fabric_identity_gate evaluate --profile config/metadata-fabric-identity.production.yaml --attestation $GDA_IDENTITY_ATTESTATION", + "required_artifacts": [ + "Certificate lifecycle policy", + "Trust-bundle ownership and endpoint inventory" + ], + "status": "unresolved", + "title": "Metadata transport security" + }, + { + "allowed_choices": [ + "An approved Iceberg REST or JDBC catalog with persistent metadata storage", + "A catalog binding with reviewed backup and restore policy" + ], + "blockers": [ + "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" + ], + "decision_id": "persistent-catalog", + "depends_on": [ + "gravitino-identity-binding", + "metadata-tls" + ], + "forbidden_choices": [ + "In-memory catalog authority", + "Ephemeral metadata storage or unverified backup claims" + ], + "owner_roles": [ + "Lakehouse Platform", + "Database SRE" + ], + "profile_paths": [ + "config/metadata-fabric-identity.production.yaml#catalog.backup_policy_reference", + "config/metadata-fabric-identity.production.yaml#catalog.catalog_reference", + "config/metadata-fabric-identity.production.yaml#catalog.decision_status", + "config/metadata-fabric-identity.production.yaml#catalog.gravitino_backend", + "config/metadata-fabric-identity.production.yaml#catalog.persistence_reference" + ], + "protected_verification_command": "python -m data_agent.metadata_fabric_identity_gate evaluate --profile config/metadata-fabric-identity.production.yaml --attestation $GDA_IDENTITY_ATTESTATION", + "required_artifacts": [ + "Approved catalog backend decision", + "Persistence, backup, and restore ownership references" + ], + "status": "unresolved", + "title": "Persistent catalog" + }, + { + "allowed_choices": [ + "An explicit tenant-isolation mode bound to the gda_tenant_id claim", + "A reviewed policy that includes cross-tenant denial probes" + ], + "blockers": [ + "identity.profile:tenancy.isolation_mode", + "identity.profile:tenancy.policy_reference" + ], + "decision_id": "metadata-tenancy", + "depends_on": [ + "openmetadata-identity-binding", + "gravitino-identity-binding" + ], + "forbidden_choices": [ + "Shared unscoped provider identity", + "Tenant isolation based only on caller-supplied naming conventions" + ], + "owner_roles": [ + "Data Governance", + "Security Architecture" + ], + "profile_paths": [ + "config/metadata-fabric-identity.production.yaml#tenancy.isolation_mode", + "config/metadata-fabric-identity.production.yaml#tenancy.policy_reference" + ], + "protected_verification_command": "python -m data_agent.metadata_fabric_identity_gate evaluate --profile config/metadata-fabric-identity.production.yaml --attestation $GDA_IDENTITY_ATTESTATION", + "required_artifacts": [ + "Tenant isolation policy", + "Cross-tenant denial test plan" + ], + "status": "unresolved", + "title": "Metadata tenancy" + }, + { + "allowed_choices": [ + "Named ownership references, measurable rotation and revocation SLOs", + "Versioned production runbooks and auditable identity events" + ], + "blockers": [ + "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" + ], + "decision_id": "identity-operations", + "depends_on": [ + "identity-federation", + "openmetadata-identity-binding", + "gravitino-identity-binding", + "metadata-tls", + "persistent-catalog", + "metadata-tenancy" + ], + "forbidden_choices": [ + "Unowned incident response or unversioned runbooks", + "Manual credential rotation without a protected audit trail" + ], + "owner_roles": [ + "IAM Platform", + "Security Operations", + "Platform SRE" + ], + "profile_paths": [ + "config/metadata-fabric-identity.production.yaml#operations.audit_log_reference", + "config/metadata-fabric-identity.production.yaml#operations.identity_owner", + "config/metadata-fabric-identity.production.yaml#operations.incident_owner", + "config/metadata-fabric-identity.production.yaml#operations.revocation_slo_minutes", + "config/metadata-fabric-identity.production.yaml#operations.rollback_runbook.uri", + "config/metadata-fabric-identity.production.yaml#operations.rollback_runbook.version", + "config/metadata-fabric-identity.production.yaml#operations.rotation_slo_minutes", + "config/metadata-fabric-identity.production.yaml#operations.runbook.uri", + "config/metadata-fabric-identity.production.yaml#operations.runbook.version", + "config/metadata-fabric-identity.production.yaml#operations.security_owner" + ], + "protected_verification_command": "python -m data_agent.metadata_fabric_identity_gate evaluate --profile config/metadata-fabric-identity.production.yaml --attestation $GDA_IDENTITY_ATTESTATION", + "required_artifacts": [ + "Identity operations ownership record", + "Versioned incident, rotation, revocation, and rollback runbooks", + "Audit-log delivery reference" + ], + "status": "unresolved", + "title": "Identity operations and runbooks" + }, + { + "allowed_choices": [ + "A production provider accepted by ADR-057 with independently owned infrastructure", + "Distinct primary and recovery regions with multi-zone durability" + ], + "blockers": [ + "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" + ], + "decision_id": "object-store-provider", + "depends_on": [], + "forbidden_choices": [ + "The retained local MinIO namespace as production authority", + "A provider account, bucket, or failure domain selected without owner approval" + ], + "owner_roles": [ + "Cloud Platform", + "Storage Platform" + ], + "profile_paths": [ + "config/metadata-fabric-object-store.production.yaml#provider.account_reference", + "config/metadata-fabric-object-store.production.yaml#provider.bucket", + "config/metadata-fabric-object-store.production.yaml#provider.decision_status", + "config/metadata-fabric-object-store.production.yaml#provider.endpoint", + "config/metadata-fabric-object-store.production.yaml#provider.failure_domain_reference", + "config/metadata-fabric-object-store.production.yaml#provider.infrastructure_reference", + "config/metadata-fabric-object-store.production.yaml#provider.provider_type", + "config/metadata-fabric-object-store.production.yaml#provider.recovery_region", + "config/metadata-fabric-object-store.production.yaml#provider.region" + ], + "protected_verification_command": "python -m data_agent.metadata_fabric_object_store_gate evaluate --profile config/metadata-fabric-object-store.production.yaml --attestation $GDA_OBJECT_STORE_ATTESTATION", + "required_artifacts": [ + "Approved provider and region architecture decision", + "Infrastructure, account, bucket, and failure-domain ownership references" + ], + "status": "unresolved", + "title": "Object-store provider" + }, + { + "allowed_choices": [ + "OIDC workload federation with the frozen least-privilege operation inventory", + "A tenant-bound service account with a maximum 900-second session" + ], + "blockers": [ + "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" + ], + "decision_id": "object-store-workload-identity", + "depends_on": [ + "identity-federation", + "object-store-provider" + ], + "forbidden_choices": [ + "Static access keys or node-wide provider credentials", + "Bucket permissions broader than the checked operation inventory" + ], + "owner_roles": [ + "Cloud Platform", + "IAM Platform", + "Security Architecture" + ], + "profile_paths": [ + "config/metadata-fabric-object-store.production.yaml#identity.bucket_policy_reference", + "config/metadata-fabric-object-store.production.yaml#identity.integration_mode", + "config/metadata-fabric-object-store.production.yaml#identity.kubernetes_service_account", + "config/metadata-fabric-object-store.production.yaml#identity.least_privilege_policy_reference", + "config/metadata-fabric-object-store.production.yaml#identity.workload_identity_reference" + ], + "protected_verification_command": "python -m data_agent.metadata_fabric_object_store_gate evaluate --profile config/metadata-fabric-object-store.production.yaml --attestation $GDA_OBJECT_STORE_ATTESTATION", + "required_artifacts": [ + "Workload identity and service-account binding", + "Least-privilege and bucket-policy reviews" + ], + "status": "unresolved", + "title": "Object-store workload identity" + }, + { + "allowed_choices": [ + "The approved provider HTTPS endpoint over reviewed private connectivity and DNS", + "TLS 1.2 or newer with managed trust and certificate lifecycle" + ], + "blockers": [ + "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" + ], + "decision_id": "object-store-transport", + "depends_on": [ + "object-store-provider" + ], + "forbidden_choices": [ + "Public or plaintext data paths not covered by the network policy", + "Local, example, certificate-bypass, or split-horizon endpoints without review" + ], + "owner_roles": [ + "Network Platform", + "Security Architecture" + ], + "profile_paths": [ + "config/metadata-fabric-object-store.production.yaml#transport.certificate_policy_reference", + "config/metadata-fabric-object-store.production.yaml#transport.dns_policy_reference", + "config/metadata-fabric-object-store.production.yaml#transport.endpoint", + "config/metadata-fabric-object-store.production.yaml#transport.private_connectivity_reference", + "config/metadata-fabric-object-store.production.yaml#transport.trust_bundle_reference" + ], + "protected_verification_command": "python -m data_agent.metadata_fabric_object_store_gate evaluate --profile config/metadata-fabric-object-store.production.yaml --attestation $GDA_OBJECT_STORE_ATTESTATION", + "required_artifacts": [ + "Private connectivity and DNS policy references", + "Certificate lifecycle and trust-bundle references" + ], + "status": "unresolved", + "title": "Object-store transport" + }, + { + "allowed_choices": [ + "A provider KMS key with reviewed key policy and rotation of at most 365 days", + "Server-side KMS encryption with bucket-key support as frozen in the profile" + ], + "blockers": [ + "object_store.profile:encryption.key_policy_reference", + "object_store.profile:encryption.key_reference", + "object_store.profile:encryption.rotation_days" + ], + "decision_id": "object-store-encryption", + "depends_on": [ + "object-store-provider", + "object-store-workload-identity" + ], + "forbidden_choices": [ + "Unmanaged or shared encryption keys without tenant and workload controls", + "Encryption claims without protected read/write verification" + ], + "owner_roles": [ + "Cloud Security", + "Key Management" + ], + "profile_paths": [ + "config/metadata-fabric-object-store.production.yaml#encryption.key_policy_reference", + "config/metadata-fabric-object-store.production.yaml#encryption.key_reference", + "config/metadata-fabric-object-store.production.yaml#encryption.rotation_days" + ], + "protected_verification_command": "python -m data_agent.metadata_fabric_object_store_gate evaluate --profile config/metadata-fabric-object-store.production.yaml --attestation $GDA_OBJECT_STORE_ATTESTATION", + "required_artifacts": [ + "KMS key ownership and policy review", + "Key rotation decision and recovery procedure" + ], + "status": "unresolved", + "title": "Object-store encryption and KMS" + }, + { + "allowed_choices": [ + "Versioned recovery and asynchronous cross-region replication meeting the frozen RPO/RTO", + "Reviewed multipart-upload and orphan-file cleanup controls" + ], + "blockers": [ + "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" + ], + "decision_id": "object-store-durability-consistency", + "depends_on": [ + "object-store-provider", + "object-store-transport", + "object-store-encryption" + ], + "forbidden_choices": [ + "Single-copy durability or same-region recovery authority", + "Rename-based correctness assumptions for object storage" + ], + "owner_roles": [ + "Storage Platform", + "Platform SRE" + ], + "profile_paths": [ + "config/metadata-fabric-object-store.production.yaml#consistency.multipart_upload_cleanup_reference", + "config/metadata-fabric-object-store.production.yaml#consistency.orphan_file_cleanup_reference", + "config/metadata-fabric-object-store.production.yaml#durability.recovery_bucket_reference", + "config/metadata-fabric-object-store.production.yaml#durability.replication_policy_reference", + "config/metadata-fabric-object-store.production.yaml#durability.retention_policy_reference" + ], + "protected_verification_command": "python -m data_agent.metadata_fabric_object_store_gate evaluate --profile config/metadata-fabric-object-store.production.yaml --attestation $GDA_OBJECT_STORE_ATTESTATION", + "required_artifacts": [ + "Retention and replication policies", + "Recovery bucket ownership and cleanup-control references" + ], + "status": "unresolved", + "title": "Object-store durability and consistency" + }, + { + "allowed_choices": [ + "Bucket-prefix and provider-policy isolation bound to tenants/{tenant_id}/warehouse/", + "A policy with public-access blocking and cross-tenant denial probes" + ], + "blockers": [ + "object_store.profile:tenancy.policy_reference" + ], + "decision_id": "object-store-tenancy", + "depends_on": [ + "object-store-provider", + "object-store-workload-identity" + ], + "forbidden_choices": [ + "Shared prefixes without provider-enforced tenant policy", + "Public access or client-only tenant filtering" + ], + "owner_roles": [ + "Data Governance", + "Cloud Security" + ], + "profile_paths": [ + "config/metadata-fabric-object-store.production.yaml#tenancy.policy_reference" + ], + "protected_verification_command": "python -m data_agent.metadata_fabric_object_store_gate evaluate --profile config/metadata-fabric-object-store.production.yaml --attestation $GDA_OBJECT_STORE_ATTESTATION", + "required_artifacts": [ + "Tenant storage isolation policy", + "Cross-tenant and public-access denial test plan" + ], + "status": "unresolved", + "title": "Object-store tenancy" + }, + { + "allowed_choices": [ + "Named ownership, measurable availability and latency SLOs, and actionable alerts", + "Versioned operation, recovery, and rollback runbooks tied to protected attestation policy" + ], + "blockers": [ + "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" + ], + "decision_id": "object-store-operations", + "depends_on": [ + "object-store-provider", + "object-store-workload-identity", + "object-store-transport", + "object-store-encryption", + "object-store-durability-consistency", + "object-store-tenancy" + ], + "forbidden_choices": [ + "Unowned incidents, unversioned runbooks, or synthetic-only alerts", + "Availability or recovery claims without protected failure rehearsal" + ], + "owner_roles": [ + "Storage Platform", + "Platform SRE", + "Security Operations" + ], + "profile_paths": [ + "config/metadata-fabric-object-store.production.yaml#operations.attestation_policy_reference", + "config/metadata-fabric-object-store.production.yaml#operations.audit_log_reference", + "config/metadata-fabric-object-store.production.yaml#operations.availability_slo_percent", + "config/metadata-fabric-object-store.production.yaml#operations.incident_owner", + "config/metadata-fabric-object-store.production.yaml#operations.latency_slo_ms", + "config/metadata-fabric-object-store.production.yaml#operations.metrics_alert_reference", + "config/metadata-fabric-object-store.production.yaml#operations.platform_owner", + "config/metadata-fabric-object-store.production.yaml#operations.recovery_runbook.uri", + "config/metadata-fabric-object-store.production.yaml#operations.recovery_runbook.version", + "config/metadata-fabric-object-store.production.yaml#operations.rollback_runbook.uri", + "config/metadata-fabric-object-store.production.yaml#operations.rollback_runbook.version", + "config/metadata-fabric-object-store.production.yaml#operations.runbook.uri", + "config/metadata-fabric-object-store.production.yaml#operations.runbook.version", + "config/metadata-fabric-object-store.production.yaml#operations.security_owner", + "config/metadata-fabric-object-store.production.yaml#operations.storage_owner" + ], + "protected_verification_command": "python -m data_agent.metadata_fabric_object_store_gate evaluate --profile config/metadata-fabric-object-store.production.yaml --attestation $GDA_OBJECT_STORE_ATTESTATION", + "required_artifacts": [ + "Operations and incident ownership record", + "SLO, alert, audit-log, and attestation policy references", + "Versioned operation, recovery, and rollback runbooks" + ], + "status": "unresolved", + "title": "Object-store operations, SLOs, and runbooks" + }, + { + "allowed_choices": [ + "A fresh ADR-053 attestation emitted by the protected production identity environment", + "An attestation bound to the approved profile, exact source revision, tenant denial, and audit evidence" + ], + "blockers": [ + "identity.attestation:production identity attestation is missing" + ], + "decision_id": "protected-identity-attestation", + "depends_on": [ + "identity-operations" + ], + "forbidden_choices": [ + "A local, manually asserted, expired, or cross-revision attestation", + "Reusing M3-24 or M3-25 retained material as production identity evidence" + ], + "owner_roles": [ + "Security Operations", + "Platform SRE" + ], + "profile_paths": [], + "protected_verification_command": "python -m data_agent.metadata_fabric_identity_gate evaluate --profile config/metadata-fabric-identity.production.yaml --attestation $GDA_IDENTITY_ATTESTATION", + "required_artifacts": [ + "Fresh protected identity attestation", + "Protected workflow and reviewer evidence URI" + ], + "status": "unresolved", + "title": "Protected identity attestation" + }, + { + "allowed_choices": [ + "A fresh ADR-057 attestation emitted by the protected production object-store environment", + "An attestation bound to the approved profile, exact source revision, tenant controls, KMS, and recovery evidence" + ], + "blockers": [ + "object_store.attestation:production object-store attestation is required" + ], + "decision_id": "protected-object-store-attestation", + "depends_on": [ + "object-store-operations" + ], + "forbidden_choices": [ + "A local, manually asserted, expired, or cross-revision attestation", + "Treating the retained MinIO rehearsal as a production object-store attestation" + ], + "owner_roles": [ + "Storage Platform", + "Security Operations", + "Platform SRE" + ], + "profile_paths": [], + "protected_verification_command": "python -m data_agent.metadata_fabric_object_store_gate evaluate --profile config/metadata-fabric-object-store.production.yaml --attestation $GDA_OBJECT_STORE_ATTESTATION", + "required_artifacts": [ + "Fresh protected object-store attestation", + "Protected workflow and reviewer evidence URI" + ], + "status": "unresolved", + "title": "Protected object-store attestation" + } + ], + "generated_at": "2026-07-31T07:22:25.389762Z", + "packet_sha256": "b23e1becafa0c91541a653d467e7959107008d4dc41576018b57b564e6b46a36", + "predecessor_binding": { + "blocker_count": 85, + "contract_sha256": "39411248b37b7d8d43ac7ad37737de15d6b6d4c5e4feb2088f0a87cec888b5f9", + "decision_sha256": "39246eacdd1793f23aecb71195cc4c9d8c63d7125aad8cd9bbb59a96c588cd73", + "evaluated_at": "2026-07-31T06:32:10.229686Z", + "file_sha256": "62624b96d83b085cbb82d29d618042d7f4faa5193847c26af859ec1c87cd4f11", + "path": "docs/evidence/metadata-fabric-protected-real-feature-reexecution-gate-2026-07-31.json", + "ready_for_protected_reexecution": false, + "status": "blocked_pending_protected_attestation" + }, + "resolution_policy": { + "attestations_must_be_fresh_protected_outputs": true, + "attestations_must_share_source_revision": true, + "authoritative_profile_paths": [ + "config/metadata-fabric-identity.production.yaml", + "config/metadata-fabric-object-store.production.yaml" + ], + "credential_material_forbidden": true, + "local_retained_material_promotion_forbidden": true, + "packet_grants_execution_authority": false, + "packet_records_decisions": false, + "profile_changes_require_owner_approval": true + }, + "schema": "gda.protected_profile_owner_decision_packet.v1", + "status": "unresolved_owner_decisions" +} diff --git a/docs/roadmap.md b/docs/roadmap.md index 51997b79..f8813222 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -417,9 +417,9 @@ AR-0 Architecture / Schema / Runtime Truth - Web、DolphinScheduler、OpenMetadata、Gravitino、worker 和 bridge 重启后不丢版本、产品、血缘、PlatformRun correlation 或审批状态。 - DataOps 状态可从 DolphinScheduler/OpenMetadata/Gravitino/GDA evidence 恢复;任何 release/deployment 都能从事件、Artifact、评测、策略、incident 和 rollback pointer 重放。Temporal durable recovery 在 AR-5/AR-7 单独验收。 -当前 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。 +当前 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。M3-27 已将 identity/object-store 两个 profile 与两份 protected attestation 的全部 85 个 blocker 精确且唯一分配给 16 个无环 owner decision groups,但不替 owner 选择 IdP、provider、account、bucket、region、KMS 或运行责任,所有 group 仍为 `unresolved`。`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 均不漂移、不新增事实。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)。 +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 直推;M3-27 又将 85 个 blocker 精确映射为 16 个 owner decision groups,固化依赖、允许/禁止边界、profile path、required artifact 和 protected verification command。当前两个 profile 虽结构有效,但所有 group 仍未决,所以 `ready_for_protected_reexecution=false`,没有调度或 provider 写入授权。生产 owner 决策与 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) 与 [ADR-073](architecture-decisions/adr-073-protected-profile-owner-decision-packet.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 的本地进程重启连续性已验证;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 不得直接晋级,也不计入生产退出门。 +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 组合门禁,M3-27 已把 85 个外部 blockers 收敛为 16 个可分派、可校验但仍未决的 owner decision groups。下一步必须由 packet 指定的 owner 批准并物化 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 486b39aa..5fecf1df 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 已验证,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` +阶段: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 组合门禁已建立,M3-27 已把 85 个 blockers 收敛为 16 个仍未决的 owner decision groups;生产 policy/tenant isolation、生产 identity/object-store attestation、常驻 production consumer/scheduler/executor、backup/PITR、独立故障域、production restart recovery、scale/conformance 和生产切换仍 `in_progress` -适用分支:`feat/ar1-metadata-fabric-protected-real-feature-reexecution-gate` +适用分支:`feat/ar1-metadata-fabric-protected-profile-decision-packet` ## 判定规则 @@ -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;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 | +| 环境发布与晋级 | 本地 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,M3-27 将其 85 个 blocker 唯一分配给 16 个 owner decision groups | 旧 mainline、feature branch、CI artifact、JSON、离线 report、合成 `verified_for_staging_apply`、M3-26 pending decision 和 M3-27 unresolved packet 都不能单独成为发布或执行权威;本地 retained material 禁止直接晋级 | 由受保护 environment 的 DeploymentRevision 绑定 OCI、provenance artifact、release manifest 与全部 live verdict;owner 必须先把批准值物化到 production profiles 并生成同 revision attestations,fresh protected ingestion 还须另有 tenant-bound PolicyDecision/Approval 与 scheduler command | Platform/SRE/Security/Repository Owner | AR-1 mainline 治理已恢复;M3-27 groups `unresolved` -> owner 批准 profiles/attestations -> M3-26 preflight 复评 -> 首次 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 | @@ -70,6 +70,7 @@ 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。 +28. M3-27 固定绑定 M3-26 evidence file/decision/contract SHA,将 identity profile 40、object-store profile 43 和两份 attestation blocker 精确且唯一分配到 16 个无环 owner decision groups。每组冻结 owner role、依赖、profile path、允许/禁止边界、required artifact 与 protected verification command;所有 status 均为 `unresolved`,packet 不记录生产选择、不含凭据、不创建授权或 provider mutation,也不降低 85 blocker 计数。 ## 已建立的 AR-0/AR-1 entry 证据 @@ -119,11 +120,13 @@ - 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` 均未获授权或验证。 +- Metadata Fabric M3-27 已把 M3-26 的 85 个 blocker 收敛为 16 个 owner decision groups:identity profile `40`、object-store profile `43`、identity/object-store attestation 各 `1`,全集相等且每项唯一、依赖图无环。packet SHA 为 `b23e1becafa0c91541a653d467e7959107008d4dc41576018b57b564e6b46a36`;所有 group 均为 `unresolved`,全部 production/execution claims 为 `false`。它只是外部决策工作包,不是批准、attestation、调度或生产 authority。 ## 下一验收证据 - 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 组合都必须拒绝; +- M3-27 已将下一次真实数据执行前的 owner 工作顺序冻结:先完成 identity federation/provider binding/TLS/catalog/tenancy/operations 与 object-store provider/workload identity/transport/KMS/durability/tenancy/operations groups,再由两个 protected environment 对同一 source revision 生成新鲜 attestations,并由 M3-26 复评清空 85 blockers; +- M3-26 通过后仍须另行创建 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 报告; diff --git a/scripts/metadata-fabric-protected-profile-decision-packet.sh b/scripts/metadata-fabric-protected-profile-decision-packet.sh new file mode 100755 index 00000000..b49b710b --- /dev/null +++ b/scripts/metadata-fabric-protected-profile-decision-packet.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_profile_decision_packet "$@"