Files
gps-denied-onboard/_docs/02_document/contracts/c11_tilemanager/tile_uploader.md
T
Oleksandr Bezdieniezhnykh 5fe67023b2 [AZ-329] [AZ-330] [AZ-523] [AZ-524] Batch 44 atomic refactor
Implements two new C12 services and rebalances the C11/C12 boundary
in one atomic commit:

* AZ-329 PostLandingUploadOrchestrator — gates C11 upload on the
  `flight_footer` FDR record's `clean_shutdown` field; 4 refusal
  modes; new FdrFooterReader Protocol + LocalFdrFooterReader.
* AZ-330 OperatorReLocService — AC-3.4 visual-loss re-localization
  hint; reuses shared LatLonAlt; OperatorCommandTransport Protocol
  cut (E-C8 owns the future pymavlink concrete); new FDR record
  kind `c12.reloc.requested`; log redaction (lat/lon 5 decimals,
  reason 200 chars).
* AZ-523 C11 internal flight-state gate removed (SRP refactor):
  `confirm_flight_state` / `FlightStateSignal` use /
  `FlightStateNotOnGroundError` deleted from C11; TileUploader
  contract bumped to v2.0.0 (frozen) with migration note; AZ-317
  superseded.
* AZ-524 Package rename `c12_operator_tooling` →
  `c12_operator_orchestrator` across source, tests, pyproject,
  CMake, Dockerfile, compose, CI, runtime-root services class
  (`OperatorOrchestratorServices`) + factory function
  (`build_operator_orchestrator`), logger namespaces, config slug,
  docs, and the E-C12 epic title.

Tests: 1543 passed, 80 skipped (all environment gates). Targeted
AC suite (AZ-329 + AZ-330 + FdrFooterReader): 37 passed. Cold-start
NFR-perf still ≤ 500 ms p99.

Tracker: AZ-317 → Done (superseded); AZ-319 v2.0.0 contract bump
comment; AZ-329/AZ-330 → In Testing; AZ-253 epic renamed; AZ-523
+ AZ-524 created and closed as audit-trail tickets.

See `_docs/03_implementation/batch_44_cycle1_report.md`.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 19:42:46 +03:00

9.7 KiB

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

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

@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)