[AZ-507] [AZ-323] [AZ-324] C10 Manifest build + verify + AZ-270 hygiene

AZ-507: codify cross-component import rule. Added
_types/inference_errors.py shim re-exporting EngineBuildError +
CalibrationCacheError from c7_inference; narrowed C10
EngineCompiler's except Exception to the two typed errors so unknown
exceptions propagate (AC-3). Rewrote module-layout.md "Imports from"
sections for 9 components + added Rule 9; appended an
architecture.md ADR-009 note explaining why components must go
through _types/*.

AZ-323: ManifestBuilder + Ed25519ManifestSigner. Canonical JSON via
orjson OPT_SORT_KEYS+OPT_INDENT_2, atomic-write Manifest.json + sha
sidecar + .sig via AZ-280, operator-key fingerprint allowlist gate
(C10-ST-01), ADR-010 takeoff_origin + flight_id baked into Manifest
AND manifest_hash so re-planned routes change the cache identity
(AC-15/AC-16). 20 unit tests cover all 16 ACs.

AZ-324: ManifestVerifierImpl. Fail-closed Steps A-D: Manifest.json
sidecar self-hash, Ed25519 trust-key set, schema parse with
absolute/.. path rejection + takeoff_origin in-bbox check, stream
SHA-256 per artifact with multi-failure accumulation. Operator mode
re-derives tiles_coverage_sha256 from C6; airborne mode trusts the
signed aggregate. 19 unit tests cover all 17 ACs.

Composition root: c10_factory.build_manifest_builder +
build_manifest_verifier + c6_tile_metadata_store_to_tiles_query
adapter (the one place that legitimately imports both C6 and C10
without violating the AZ-270 lint).

Dependency: pinned cryptography>=43.0,<46.0 in pyproject.toml.

Tests: 1300 passed, 80 skipped (env-only), ruff clean for all
AZ-323/324 files.

AZ-306 (FAISS) intentionally deferred to batch 35 — needs C++
pybind11 toolchain not present in this environment.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-13 02:37:14 +03:00
parent 6ca8d78190
commit e2bebefdfc
20 changed files with 3406 additions and 26 deletions
@@ -19,27 +19,38 @@ from typing import TYPE_CHECKING
from gps_denied_onboard.components.c10_provisioning import (
BackboneSpec,
Ed25519ManifestSigner,
EngineCompiler,
ManifestBuilder,
ManifestVerifierImpl,
TileHashRecord,
TilesByBboxQuery,
)
from gps_denied_onboard.components.c10_provisioning.config import (
BackboneConfig,
C10ProvisioningConfig,
)
from gps_denied_onboard.helpers.sha256_sidecar import Sha256Sidecar
from gps_denied_onboard.logging import get_logger
from gps_denied_onboard.runtime_root.inference_factory import (
build_inference_runtime,
)
if TYPE_CHECKING:
from gps_denied_onboard.clock import Clock
from gps_denied_onboard.components.c6_tile_cache import TileMetadataStore
from gps_denied_onboard.config.schema import Config
__all__ = [
"build_backbone_specs",
"build_engine_compiler",
"build_manifest_builder",
"build_manifest_verifier",
"c6_tile_metadata_store_to_tiles_query",
]
def build_engine_compiler(config: "Config") -> EngineCompiler:
def build_engine_compiler(config: Config) -> EngineCompiler:
"""Construct a wired :class:`EngineCompiler` from ``config``.
The factory:
@@ -61,7 +72,7 @@ def build_engine_compiler(config: "Config") -> EngineCompiler:
return EngineCompiler(inference_runtime=runtime, logger=logger)
def build_backbone_specs(config: "Config") -> tuple[BackboneSpec, ...]:
def build_backbone_specs(config: Config) -> tuple[BackboneSpec, ...]:
"""Materialise :class:`BackboneSpec` tuple from
``config.components['c10_provisioning'].backbones``.
@@ -83,3 +94,128 @@ def _backbone_spec_from_config(
expected_input_shape=tuple(backbone.expected_input_shape),
input_name=backbone.input_name,
)
def build_manifest_builder(
config: Config,
*,
tile_metadata_store: TileMetadataStore,
clock: Clock,
) -> ManifestBuilder:
"""Construct a wired :class:`ManifestBuilder` (AZ-323).
The ``tile_metadata_store`` argument is the AZ-303 C6 store; this
factory wraps it in the consumer-side
:class:`TilesByBboxQuery` adapter so the C10 module never imports
``components.c6_tile_cache`` directly (AZ-270 + AZ-507 boundary).
``clock`` is supplied explicitly rather than re-resolved through
a clock factory because the composition root selects the clock
strategy (WallClock for live, TlogDerivedClock for replay) per
AZ-398 and threads the SAME instance through every consumer.
"""
block: C10ProvisioningConfig = config.components["c10_provisioning"]
sidecar = Sha256Sidecar()
signer = Ed25519ManifestSigner()
logger = get_logger("c10_provisioning.manifest")
tiles_query = c6_tile_metadata_store_to_tiles_query(tile_metadata_store)
return ManifestBuilder(
sidecar=sidecar,
signer=signer,
tile_metadata_store=tiles_query,
logger=logger,
clock=clock,
config=block.manifest,
)
def build_manifest_verifier(
config: Config,
*,
clock: Clock,
tile_metadata_store: TileMetadataStore | None = None,
with_tile_store: bool = False,
) -> ManifestVerifierImpl:
"""Construct a wired :class:`ManifestVerifierImpl` (AZ-324).
``with_tile_store=True`` (operator C12 mode) requires
``tile_metadata_store`` to be supplied — the verifier re-derives
``tiles_coverage_sha256`` from C6 and reports drift.
``with_tile_store=False`` (airborne C5 mode) trusts the recorded
aggregate after the Ed25519 signature passes (MV-INV-5); the
``tile_metadata_store`` argument is ignored.
"""
sidecar = Sha256Sidecar()
logger = get_logger("c10_provisioning.verify")
# AZ-324 silently accepting a tile_metadata_store with
# `with_tile_store=False` would mask a composition-root mistake
# (operator mode wired in an airborne binary by accident); we keep
# the airborne path explicit by ignoring the argument here.
if with_tile_store:
if tile_metadata_store is None:
raise ValueError(
"build_manifest_verifier(with_tile_store=True) requires "
"tile_metadata_store; supply None or set with_tile_store=False"
)
tiles_query: TilesByBboxQuery | None = c6_tile_metadata_store_to_tiles_query(
tile_metadata_store
)
else:
tiles_query = None
return ManifestVerifierImpl(
sidecar=sidecar,
logger=logger,
clock=clock,
tile_metadata_store=tiles_query,
)
def c6_tile_metadata_store_to_tiles_query(
tile_metadata_store: TileMetadataStore,
) -> TilesByBboxQuery:
"""Adapt the C6 ``TileMetadataStore`` to the C10 ``TilesByBboxQuery`` cut.
Lives in the composition root because it is the only place that
may import both C6 and C10 (the AZ-270 lint allows
``runtime_root``). C6 returns ``TileMetadata`` rows; AZ-323 needs
a ``TileHashRecord`` with ``(zoom, lat, lon, source, sha256_hex)``
and nothing else.
"""
from gps_denied_onboard.components.c6_tile_cache import (
SectorClassification as C6SectorClassification,
)
class _C6TilesAdapter:
def __init__(self, store: TileMetadataStore) -> None:
self._store = store
def query_by_bbox(
self,
*,
bbox,
zoom_levels,
sector_class,
):
c6_sector = C6SectorClassification(sector_class)
rows = self._store.query_by_bbox(
bbox=bbox,
zoom_levels=zoom_levels,
sector_class=c6_sector,
)
return tuple(
TileHashRecord(
zoom=row.tile_id.zoom_level,
lat=row.tile_id.lat,
lon=row.tile_id.lon,
source=row.source.value
if hasattr(row.source, "value")
else str(row.source),
sha256_hex=row.content_sha256_hex,
)
for row in rows
)
return _C6TilesAdapter(tile_metadata_store)