mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 07:51:28 +00:00
Decompose Step 6 snapshot: 140 task specs + contract docs
Closes out greenfield Step 6 (Decompose) for all 14 components (C1-C13 + cross-cutting helpers/replay). Covers tasks AZ-266..AZ-446 plus the _dependencies_table.md and component contract documents. State file updated to greenfield Step 7 (Implement), not_started. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
# Contract: tile_uploader
|
||||
|
||||
**Component**: c11_tilemanager
|
||||
**Producer task**: AZ-319_c11_tile_uploader
|
||||
**Consumer tasks**: AZ-253 (E-C12 Operator Pre-flight Tooling — TBD at C12 decompose time)
|
||||
**Version**: 1.0.0
|
||||
**Status**: draft
|
||||
**Last Updated**: 2026-05-10
|
||||
|
||||
## Purpose
|
||||
|
||||
The `TileUploader` Protocol is C11's operator-side post-landing upload interface. C12 invokes it during F10 (post-landing) to read mid-flight tiles flagged pending-upload from C6 (`source = onboard_ingest`, `voting_status = pending`), package them per the D-PROJ-2 ingest contract sketch, sign each tile payload with the per-flight ephemeral key (AZ-318), and POST to `satellite-provider`'s `/api/satellite/tiles/ingest` endpoint. Acknowledged tiles are marked uploaded in C6.
|
||||
|
||||
The uploader gates on `flight_state == ON_GROUND` (AZ-317) before any network egress. 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]: ...
|
||||
def confirm_flight_state(self) -> FlightStateSignal: ...
|
||||
```
|
||||
|
||||
| Name | Signature | Throws / Errors | Blocking? |
|
||||
|------|-----------|-----------------|-----------|
|
||||
| `upload_pending_tiles` | `(request: UploadRequest) -> UploadBatchReport` | `FlightStateNotOnGroundError`, `SatelliteProviderError`, `RateLimitedError`, `SignatureRejectedError`, `TileMetadataError` | sync (post-landing; minutes) |
|
||||
| `enumerate_pending_tiles` | `(flight_id: uuid.UUID \| None) -> list[TileMetadata]` | `TileMetadataError` | sync (seconds) |
|
||||
| `confirm_flight_state` | `() -> FlightStateSignal` | `FlightStateNotOnGroundError` | sync (≤ 1 ms) |
|
||||
|
||||
### 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: `confirm_flight_state` is called by `upload_pending_tiles` BEFORE any C6 read or network egress; if `FlightStateNotOnGroundError` is raised, NO tiles are read, NO POSTs are issued, NO C6 mutation occurs. The gate is closed by default.
|
||||
- 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, ON_GROUND, parent-suite returns 202 with all `queued` | `UploadBatchReport.outcome = success`; all 50 marked `uploaded` in C6; signature verifies on each | C11-IT-03 |
|
||||
| flight-state-blocks | `FlightStateSource` returns `IN_FLIGHT` | `FlightStateNotOnGroundError`; zero C6 reads; zero POSTs | C11-IT-04 |
|
||||
| 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 |
|
||||
Reference in New Issue
Block a user