# Contract: tile_uploader **Component**: c11_tilemanager **Producer task**: AZ-319_c11_tile_uploader (initial), Batch 44 C11-SRP-revert (v2.0.0 gate removal) **Consumer tasks**: AZ-329 (C12 `PostLandingUploadOrchestrator`) — see `_docs/02_document/contracts/c12_operator_orchestrator/` for the C12 surface that owns the post-landing safety gate. **Version**: 2.0.0 **Status**: frozen **Last Updated**: 2026-05-13 ## Migration note — v1.0.0 → v2.0.0 Batch 44 removed C11's internal post-landing safety gate per SRP. v1.0.0 exposed `confirm_flight_state(): FlightStateSignal` and raised `FlightStateNotOnGroundError` from `upload_pending_tiles`. v2.0.0 drops both — the equivalent check moved to C12's `PostLandingUploadOrchestrator` (AZ-329), which inspects the C13 `flight_footer` FDR record and refuses to invoke `upload_pending_tiles` unless `clean_shutdown=True` is recorded. C11 is now a dumb pipe. Consumers that still call `confirm_flight_state` or catch `FlightStateNotOnGroundError` MUST migrate to consuming C12's `FlightStateNotConfirmedError` family instead. ADR-004 process-level isolation remains the primary control — C11 never runs on the companion at all. ## Purpose The `TileUploader` Protocol is C11's operator-side post-landing upload interface. C12's `PostLandingUploadOrchestrator` (AZ-329) invokes it during F10 (post-landing) AFTER it has confirmed `clean_shutdown=True` from the C13 `flight_footer` FDR record. C11 then reads mid-flight tiles flagged pending-upload from C6 (`source = onboard_ingest`, `voting_status = pending`), packages them per the D-PROJ-2 ingest contract sketch, signs each tile payload with the per-flight ephemeral key (AZ-318), and POSTs to `satellite-provider`'s `/api/satellite/tiles/ingest` endpoint. Acknowledged tiles are marked uploaded in C6. C11 is operator-side ONLY; ADR-004 forbids the airborne companion image from importing this module. ## Shape ### Function / method API ```python from typing import Protocol, runtime_checkable @runtime_checkable class TileUploader(Protocol): def upload_pending_tiles(self, request: UploadRequest) -> UploadBatchReport: ... def enumerate_pending_tiles(self, flight_id: uuid.UUID | None = None) -> list[TileMetadata]: ... ``` | Name | Signature | Throws / Errors | Blocking? | |------|-----------|-----------------|-----------| | `upload_pending_tiles` | `(request: UploadRequest) -> UploadBatchReport` | `SatelliteProviderError`, `RateLimitedError`, `SignatureRejectedError`, `TileMetadataError` | sync (post-landing; minutes) | | `enumerate_pending_tiles` | `(flight_id: uuid.UUID \| None) -> list[TileMetadata]` | `TileMetadataError` | sync (seconds) | ### Data DTOs ```python @dataclass(frozen=True) class UploadRequest: flight_id: uuid.UUID | None # None = all flights with pending batch_size: int # tiles per HTTP POST satellite_provider_url: str # parent-suite ingest base URL @dataclass(frozen=True) class UploadBatchReport: batch_uuid: uuid.UUID # assigned by parent-suite ingest per_tile_status: tuple[PerTileStatus, ...] retry_count: int next_retry_at_s: int | None # set when partial-success outcome: UploadOutcome # success | partial | failure public_key_fingerprint: str # 16-hex; from AZ-318 @dataclass(frozen=True) class PerTileStatus: tile_id: TileId # from c6_tile_cache status: IngestStatus # queued | rejected | duplicate | superseded rejection_reason: str | None ``` | Field | Type | Required | Description | Constraints | |-------|------|----------|-------------|-------------| | `UploadRequest.flight_id` | `UUID \| None` | no | Restricts batch to one flight | None = all pending across flights | | `UploadRequest.batch_size` | `int` | yes | Tiles per HTTP POST | `1 ≤ batch_size ≤ 200` | | `UploadBatchReport.batch_uuid` | `UUID` | yes | Parent-suite batch identifier | Server-assigned per D-PROJ-2 | | `UploadBatchReport.per_tile_status` | `tuple[PerTileStatus, ...]` | yes | Per-tile result | Length = number of tiles attempted in this report | | `UploadBatchReport.outcome` | `UploadOutcome` | yes | Aggregate outcome | `success` (all queued/duplicate/superseded) \| `partial` (some rejected/timeout) \| `failure` (gate blocked or full failure) | | `UploadBatchReport.public_key_fingerprint` | `str` | yes | Identifies the per-flight signing key | 16 hex chars from AZ-318 | | `PerTileStatus.status` | `IngestStatus` | yes | Server response status | `queued` \| `rejected` \| `duplicate` \| `superseded` | ## Invariants - I-1 (v2.0.0): C11 itself does NOT gate on flight state. The pre-call gate is C12's `PostLandingUploadOrchestrator` (AZ-329), which inspects the C13 `flight_footer` FDR record for `clean_shutdown=True` BEFORE invoking `upload_pending_tiles`. C11 is a dumb pipe — once called, it proceeds to read C6 + POST to the satellite-provider with no internal short-circuit. ADR-004 process-level isolation remains the primary defence (C11 never runs on the companion). - I-2: Every uploaded tile carries a signature produced by the AZ-318 per-flight key manager's `sign(payload)`. The parent suite verifies against the public key it received via the safety officer's pre-flight enrolment OR the `kind="c11.upload.session.key.public"` FDR record. - I-3: A tile acknowledged as `queued`, `duplicate`, or `superseded` by the parent suite is marked `uploaded` in C6 (`mark_uploaded(tile_id)`); a tile acknowledged as `rejected` is NOT marked uploaded — it remains `pending` for human review. - I-4: The per-flight signing key is zeroised at the end of `upload_pending_tiles` regardless of success or failure (try/finally in the caller; AZ-318's `end_session()`). - I-5: A `SignatureRejectedError` from the parent suite triggers an FDR alert (AZ-318's `record_signature_rejection`); it is NEVER silently caught. - I-6: The uploader writes via the AZ-303 `TileMetadataStore.mark_uploaded` Protocol; it does NOT update the metadata table directly. - I-7: Partial-success batches are reported (not raised as failures) so the caller can re-invoke for the unacked tiles; idempotent retry behaviour is owned by the AZ-320 decorator that wraps this Protocol's impl. - I-8: The signed payload includes `capture_timestamp` per the D-PROJ-2 contract sketch; the parent suite's nonce / timestamp validation owns replay defence. ## Non-Goals - Not covered: airborne or in-flight uploads (RESTRICT-SAT-1 forbids them; airborne process cannot import this module per ADR-004). - Not covered: orchestration of when the operator runs F10 — owned by C12. - Not covered: tile downloads from `satellite-provider` — owned by `TileDownloader` (separate contract). - Not covered: parent-suite voting / trust-promotion of uploaded tiles — owned by D-PROJ-2 design task #2 (`satellite-provider`). - Not covered: HSM / TPM-backed key storage — out of scope this cycle (in-memory key with zeroisation). - Not covered: mid-upload key rotation — one key per session. - Not covered: idempotent retry across partial-success batches — separate task in this epic decorates this contract. ## Versioning Rules - **Breaking changes** (renamed method, removed required field, changed return type, changed signature contract) require a major version bump. C12 is the sole consumer today; coordinate via Choose A/B/C/D when bumping. - **Non-breaking additions** (new optional field on the report, new `IngestStatus` enum value the consumer already tolerates via `_ = status`) require a minor version bump. ## Test Cases | Case | Input | Expected | Notes | |------|-------|----------|-------| | upload-happy-path | 50 pending tiles, parent-suite returns 202 with all `queued` | `UploadBatchReport.outcome = success`; all 50 marked `uploaded` in C6; signature verifies on each | C11-IT-03 | | post-landing-gate-in-c12 | C12 `PostLandingUploadOrchestrator` invocation flow | The flight-state gate lives in C12 (`FlightStateNotConfirmedError`), not C11. v2.0.0 removed the C11 internal gate. | See `c12_operator_orchestrator` contract + AZ-329 spec | | signature-rejected | Parent suite returns `rejected` for 1 tile with reason `"invalid signature"` | `PerTileStatus.status = rejected`; `outcome = partial`; FDR `c11.upload.signature_rejected` emitted; the tile NOT marked uploaded | I-5 | | duplicate-acknowledged | Parent suite returns `duplicate` for 5 tiles (already ingested in a prior batch) | All 5 marked `uploaded`; `outcome = success` | I-3 | | signing-key-zeroised | Run a successful upload, then assert the AZ-318 manager's `_private_key is None` | Always zeroised; FDR `c11.upload.session.key.zeroised` recorded | I-4 | | signing-key-zeroised-on-failure | Network drop mid-batch raises `SatelliteProviderError`, then assert key zeroised | Always zeroised even on failure | I-4 | | empty-pending-set | No pending tiles | `outcome = success` with empty `per_tile_status`; zero POSTs; zero key generation | edge case | | public-key-in-fdr-before-first-post | Capture FDR records | `kind="c11.upload.session.key.public"` precedes any `c11.upload.tile.*` records | safety-officer correlation | ## Change Log | Version | Date | Change | Author | |---------|------|--------|--------| | 1.0.0 | 2026-05-10 | Initial contract — produced by AZ-319 (E-C11 decomposition) | autodev | | 2.0.0 | 2026-05-13 | Batch 44: remove C11 internal flight-state gate per SRP. `confirm_flight_state` method dropped; `FlightStateNotOnGroundError` retired; post-landing safety gate now owned by C12's `PostLandingUploadOrchestrator` (AZ-329). Breaking — consumers MUST migrate to C12's `FlightStateNotConfirmedError`. | autodev (Batch 44) |