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>
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 C13flight_footerFDR record forclean_shutdown=TrueBEFORE invokingupload_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 thekind="c11.upload.session.key.public"FDR record. - I-3: A tile acknowledged as
queued,duplicate, orsupersededby the parent suite is markeduploadedin C6 (mark_uploaded(tile_id)); a tile acknowledged asrejectedis NOT marked uploaded — it remainspendingfor human review. - I-4: The per-flight signing key is zeroised at the end of
upload_pending_tilesregardless of success or failure (try/finally in the caller; AZ-318'send_session()). - I-5: A
SignatureRejectedErrorfrom the parent suite triggers an FDR alert (AZ-318'srecord_signature_rejection); it is NEVER silently caught. - I-6: The uploader writes via the AZ-303
TileMetadataStore.mark_uploadedProtocol; 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_timestampper 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 byTileDownloader(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
IngestStatusenum 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) |