Apply the user-directed Emergent Security guidance to close the redundant role gate. Add closed request/receipt validation bound to run, cluster, implementation, mode and authority reference; preserve platform enforcement and operation admission gates. Decision-Class: task-scoped-code-and-tests Delegation: explicit-user-task; Emergent-Security-A6 Evidence: 653-tests-passed; 1-skipped; focused-43-passed; ruff-clean Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a100de-2e1c-7e23-9ae9-2da0e0499411
102 lines
4.3 KiB
Python
102 lines
4.3 KiB
Python
"""Candidate scheduling/owner handoff for ACTIVITY-WP-0042.
|
|
|
|
Pure envelope validation, not operation admission or authorization. Nothing in
|
|
this module executes a restore. The platform enforcement point must validate
|
|
per-run authority and produce its own recovery/cleanup evidence.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timedelta
|
|
from typing import Literal
|
|
from uuid import UUID
|
|
|
|
from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, ValidationError, model_validator
|
|
|
|
|
|
class RecoveryRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
|
|
schema_id: Literal["activity.platform-recovery-request.v1"] = "activity.platform-recovery-request.v1"
|
|
run_id: UUID
|
|
operation: Literal["apps_pg_restore_evidence"] = "apps_pg_restore_evidence"
|
|
target: Literal["apps-pg"] = "apps-pg"
|
|
cluster_uid: UUID
|
|
implementation_sha256: str = Field(pattern=r"^[0-9a-f]{64}$")
|
|
mode: Literal["dry_run", "apply"]
|
|
# Opaque reference only. Presence never establishes authority (C1/C2/C9).
|
|
authority_ref: UUID | None = None
|
|
|
|
@model_validator(mode="after")
|
|
def require_apply_reference(self) -> RecoveryRequest:
|
|
if self.mode == "apply" and self.authority_ref is None:
|
|
raise ValueError("apply requires a per-run authority reference")
|
|
return self
|
|
|
|
|
|
class RecoveryReceipt(BaseModel):
|
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
|
|
schema_id: Literal["activity.platform-recovery-receipt.v1"]
|
|
run_id: UUID
|
|
operation: Literal["apps_pg_restore_evidence"]
|
|
target: Literal["apps-pg"]
|
|
cluster_uid: UUID
|
|
mode: Literal["dry_run", "apply"]
|
|
authority_ref: UUID | None
|
|
outcome: Literal["planned", "succeeded", "failed", "rejected", "unknown"]
|
|
started_at: AwareDatetime
|
|
finished_at: AwareDatetime
|
|
validation_succeeded: bool = Field(strict=True)
|
|
cleanup: Literal["completed", "not_created", "failed", "unknown"]
|
|
# Digests bind owner evidence; payloads, paths and logs do not cross here.
|
|
evidence_sha256: str = Field(pattern=r"^[0-9a-f]{64}$")
|
|
implementation_sha256: str = Field(pattern=r"^[0-9a-f]{64}$")
|
|
|
|
|
|
def parse_recovery_request(payload: object) -> RecoveryRequest:
|
|
"""Reject malformed requests without echoing potentially sensitive input."""
|
|
try:
|
|
return RecoveryRequest.model_validate(payload)
|
|
except ValidationError:
|
|
raise ValueError("invalid recovery request envelope") from None
|
|
|
|
|
|
def validate_recovery_receipt(
|
|
request: RecoveryRequest,
|
|
payload: object,
|
|
*,
|
|
observed_at: datetime,
|
|
max_age: timedelta,
|
|
) -> RecoveryReceipt:
|
|
"""Check correlation and receipt shape; the owner validates the recovery.
|
|
|
|
The caller must supply an owner-accepted freshness window and authenticated
|
|
transport. A structurally valid receipt is not authority to run anything.
|
|
Ambiguous/failed results remain visible, never retried by this module.
|
|
"""
|
|
if observed_at.tzinfo is None or observed_at.utcoffset() is None or max_age <= timedelta(0):
|
|
raise ValueError("aware observation time and positive freshness window required")
|
|
try:
|
|
receipt = RecoveryReceipt.model_validate(payload)
|
|
except ValidationError:
|
|
raise ValueError("invalid recovery receipt envelope") from None
|
|
for field in ("run_id", "operation", "target", "cluster_uid", "implementation_sha256", "mode", "authority_ref"):
|
|
if getattr(receipt, field) != getattr(request, field):
|
|
raise ValueError("recovery receipt does not match request")
|
|
if not receipt.started_at <= receipt.finished_at <= observed_at:
|
|
raise ValueError("invalid recovery receipt timestamps")
|
|
if observed_at - receipt.finished_at > max_age:
|
|
raise ValueError("stale recovery receipt")
|
|
if receipt.outcome == "succeeded" and (
|
|
receipt.mode != "apply"
|
|
or not receipt.validation_succeeded
|
|
or receipt.cleanup != "completed"
|
|
):
|
|
raise ValueError("successful recovery requires validation and completed cleanup")
|
|
if receipt.mode == "dry_run" and (
|
|
receipt.validation_succeeded or receipt.cleanup != "not_created"
|
|
):
|
|
raise ValueError("dry-run receipt cannot claim recovery or cleanup effects")
|
|
if receipt.outcome == "planned" and receipt.mode != "dry_run":
|
|
raise ValueError("planned outcome is reserved for dry-run requests")
|
|
return receipt
|