Lands the first task of the implementation epic AZ-626: a cargo workspace
with 14 crates (shared + autopilot binary + 12 component crates), a
multi-stage Dockerfile + dev/test compose stacks, a Woodpecker CI pipeline,
the on-airframe systemd unit with flight-gate wiring, three environment
TOML configs, and the canonical entity catalogue from data_model.md as
`shared::models`.
Per-AC verification (full detail in
_docs/03_implementation/batch_01_cycle1_report.md):
- AC-1 cargo check --workspace clean
- AC-2 cargo test --workspace passes; per-crate it_compiles() <0.01 s
- AC-6 cargo build/test --no-default-features clean; VlmClient default
impl returns VlmAssessment::disabled()
- AC-9 tracing-subscriber emits JSON logs with ts/level/target/fields
- AC-10 runtime::ensure_state_directories creates mapobjects/, audit/,
pending_pushes/ under storage.state_dir
Deferred to external infra (artifacts written, verification re-runs in CI
and in downstream tasks):
- AC-3 Woodpecker runner; CI yml in place
- AC-4 docker-compose mocks land with AZ-660/AZ-644/AZ-675
- AC-5 SITL conformance lands with AZ-641/AZ-648/AZ-652
- AC-7 aarch64 cross-compile via cargo-zigbuild stage
- AC-8 systemd unit (Linux + systemd host)
Layering invariants from module-layout.md hold: shared (L1) imports
nothing; Layer 2 actor crates import only shared; Layer 3 coordinators
(operator_bridge, mission_executor) import only their documented Layer 2
deps; Layer 4 (scan_controller) imports its documented Layer 2 + Layer 3
deps; the autopilot binary (L5) is the only consumer of every component.
cargo fmt --all --check + cargo clippy --all-targets -- -D warnings both
clean. Jira AZ-640 transitioned to In Progress at the start of this batch;
the matching In Testing transition follows this commit.
Co-authored-by: Cursor <cursoragent@cursor.com>
26 KiB
Initial Project Structure
Task: AZ-640_initial_structure Name: Initial Structure Description: Scaffold the Rust cargo workspace — per-component crates, shared crate, runtime composition root, Dockerfile + docker-compose for dev/test, Woodpecker CI pipeline, observability scaffold, on-device state directory, env config, and replay-based integration test layout. Complexity: 5 points Dependencies: None Component: Bootstrap Tracker: AZ-640 Epic: AZ-626
Project Folder Layout
autopilot/
├── Cargo.toml # cargo workspace manifest
├── Cargo.lock
├── rust-toolchain.toml # pin stable channel + components
├── .cargo/
│ └── config.toml # cross-compile target = aarch64-unknown-linux-gnu
├── .woodpecker.yml # CI pipeline (per deployment/ci_cd_pipeline.md)
├── .dockerignore
├── Dockerfile # multi-stage; non-root; pinned l4t-base for prod, ubuntu:22.04 for emul
├── docker-compose.yml # dev: autopilot + mock detections + mock missions + mock ground-station
├── docker-compose.test.yml # blackbox: autopilot + ArduPilot SITL + mock detections + replay sources
├── .env.example # documented environment variables
├── config/
│ ├── autopilot.dev.toml # dev profile (mock endpoints)
│ ├── autopilot.staging.toml # staging profile (real endpoints, non-flight)
│ └── autopilot.prod.toml # prod template (Jetson on-airframe)
├── crates/
│ ├── autopilot/ # binary crate — runtime composition root
│ │ ├── Cargo.toml # `[[bin]] name = "autopilot"`
│ │ ├── src/
│ │ │ ├── main.rs # CLI parse, config load, wire actors, run
│ │ │ ├── runtime.rs # actor topology, health aggregator, shutdown
│ │ │ └── health_server.rs # HTTP /health endpoint (port from config)
│ │ └── tests/ # cross-crate integration tests (replay-based)
│ ├── shared/
│ │ ├── Cargo.toml
│ │ └── src/
│ │ ├── lib.rs # re-exports
│ │ ├── models/ # canonical entities from data_model.md
│ │ │ ├── mod.rs
│ │ │ ├── frame.rs # Frame, BoundingBox
│ │ │ ├── detection.rs # Detection, DetectionBatch
│ │ │ ├── movement.rs # MovementCandidate
│ │ │ ├── tier2.rs # Tier2Evidence
│ │ │ ├── vlm.rs # VlmAssessment
│ │ │ ├── poi.rs # POI
│ │ │ ├── mapobject.rs # MapObject, MapObjectObservation, MapObjectsBundle, IgnoredItem
│ │ │ ├── mission.rs # MissionItem, MissionWaypoint, Geofence, Coordinate
│ │ │ ├── operator.rs # OperatorCommand
│ │ │ └── gimbal.rs # GimbalState
│ │ ├── config/ # toml loader + typed config sections
│ │ ├── error.rs # AutopilotError enum, Result alias
│ │ ├── health.rs # ComponentHealth, AggregatedHealth
│ │ ├── observability/ # tracing-subscriber init + log field constants
│ │ └── clock.rs # monotonic + wall-clock binding (GPS / NTP)
│ ├── frame_ingest/
│ │ ├── Cargo.toml
│ │ ├── src/lib.rs # public API trait + actor handle
│ │ ├── src/internal/ # decoder, RTSP client
│ │ └── tests/ # replay-based unit tests against fixture RTSP clips
│ ├── detection_client/
│ │ ├── Cargo.toml
│ │ ├── build.rs # tonic-build for ../detections .proto
│ │ ├── proto/ # copy of ../detections gRPC contract
│ │ ├── src/lib.rs
│ │ └── tests/
│ ├── movement_detector/
│ │ ├── Cargo.toml
│ │ ├── src/lib.rs
│ │ ├── src/internal/ # ego-motion, optical-flow, per-zoom-band thresholds
│ │ └── tests/ # replay fixtures, zoom-out + zoom-in
│ ├── semantic_analyzer/
│ │ ├── Cargo.toml
│ │ ├── src/lib.rs
│ │ ├── src/internal/ # primitive graph, ROI CNN call
│ │ └── tests/
│ ├── vlm_client/
│ │ ├── Cargo.toml # feature = ["vlm"] — see autopilot/Cargo.toml
│ │ ├── src/lib.rs # default impl returns VlmAssessment{status=vlm_disabled}
│ │ ├── src/internal/ # UDS client + peer-cred check
│ │ └── tests/
│ ├── scan_controller/
│ │ ├── Cargo.toml
│ │ ├── src/lib.rs
│ │ ├── src/state_machine/ # ZoomedOut / ZoomedIn / TargetFollow types
│ │ ├── src/poi_queue/ # priority queue + ≤5 POIs/min cap
│ │ └── tests/ # behaviour-tree scenarios from system-flows.md §F4
│ ├── mapobjects_store/
│ │ ├── Cargo.toml
│ │ ├── src/lib.rs
│ │ ├── src/internal/h3_index/ # h3rs wrapper
│ │ ├── src/internal/engine/ # engine trait + in-memory+snapshot default impl (Q3)
│ │ └── tests/
│ ├── gimbal_controller/
│ │ ├── Cargo.toml
│ │ ├── src/lib.rs
│ │ ├── src/internal/a40_protocol/ # ViewPro A40 UDP vendor protocol
│ │ └── tests/ # mock A40 over UDP
│ ├── operator_bridge/
│ │ ├── Cargo.toml
│ │ ├── src/lib.rs
│ │ ├── src/internal/auth/ # OperatorCommand envelope validation (Q9 — stubbed)
│ │ └── tests/
│ ├── mission_executor/
│ │ ├── Cargo.toml
│ │ ├── src/lib.rs
│ │ ├── src/internal/multirotor/ # multirotor variant FSM
│ │ ├── src/internal/fixed_wing/ # fixed-wing variant FSM
│ │ ├── src/internal/geofence/ # INCLUSION + EXCLUSION enforcement
│ │ ├── src/internal/failsafe/ # lost-link ladder, battery thresholds
│ │ └── tests/ # ArduPilot SITL fixtures
│ ├── mavlink_layer/
│ │ ├── Cargo.toml
│ │ ├── src/lib.rs
│ │ ├── src/internal/codec/ # MAVLink v2 encode/decode (only §7.7 surface)
│ │ ├── src/internal/transport/ # UDP and serial connection abstraction
│ │ └── tests/ # SITL conformance fixtures
│ ├── mission_client/
│ │ ├── Cargo.toml
│ │ ├── src/lib.rs
│ │ ├── src/internal/missions_api/ # HTTPS REST client; pull + middle-waypoint POST
│ │ ├── src/internal/mapobjects_sync/ # pre-flight GET + post-flight POST of /mapobjects bundles
│ │ └── tests/ # mock missions API
│ └── telemetry_stream/
│ ├── Cargo.toml
│ ├── src/lib.rs
│ ├── src/internal/uplink/ # modem push of frames + telemetry + bbox overlay
│ └── tests/ # mock Ground Station receiver
├── tests/
│ └── e2e/ # cross-crate blackbox scenarios (used by docker-compose.test.yml)
├── benches/
│ ├── tier1_latency.rs # benchmark-gate harness for §6 NFRs
│ ├── tier2_latency.rs
│ ├── gimbal_zoom.rs
│ └── movement_fpr.rs # per-zoom-band FPR replay benchmark
├── fixtures/
│ ├── rtsp/ # pre-recorded RTSP clips
│ ├── mavlink/ # ArduPilot SITL replay scripts
│ ├── missions/ # mission JSON fixtures
│ └── detections/ # deterministic Tier-1 response fixtures
├── deploy/
│ ├── systemd/
│ │ └── autopilot.service # per deployment/containerization.md §3
│ └── jetson/
│ └── README.md # on-airframe install steps
└── README.md
Layout Rationale
- Cargo workspace with one crate per component matches the recommended Rust layout in
_docs/02_document/decompose/templates/module-layout.md(crates/<component>/). It enforces module boundaries: a crate's internals (internal/, private modules) are unreachable from sibling components — only itslib.rspublic surface is. - Single binary crate
crates/autopilot/is the runtime composition root (perdeployment/containerization.md— "single Rust binary"). It depends on every component crate and wires the actor topology inruntime.rs. crates/shared/owns the canonical entity catalogue fromdata_model.mdand cross-cutting concerns (config, error, health, observability, clock). All component crates may import from it; it imports from no one.fixtures/separate fromtests/because the same fixtures feed unit tests, replay-based integration tests, blackbox tests, and benchmark gates.vlm_clientcrate exists unconditionally; the optional behaviour is implemented via a defaultVlmAssessmentprovider that returnsstatus=vlm_disabledwhen thevlmfeature is off (perarchitecture.md §7.6"Optionality model").
DTOs and Interfaces
Shared DTOs (live in crates/shared/src/models/)
| DTO | Source spec | Used by components |
|---|---|---|
Frame, BoundingBox |
data_model.md §2 |
frame_ingest, detection_client, movement_detector, semantic_analyzer, telemetry_stream |
Detection, DetectionBatch |
data_model.md §2 |
detection_client, scan_controller, telemetry_stream, operator_bridge |
MovementCandidate |
data_model.md §2 |
movement_detector, scan_controller |
Tier2Evidence |
data_model.md §2 |
semantic_analyzer, scan_controller |
VlmAssessment |
data_model.md §2 |
vlm_client, scan_controller |
POI |
data_model.md §3 |
scan_controller, operator_bridge, telemetry_stream |
MapObject, MapObjectObservation, MapObjectsBundle, IgnoredItem |
data_model.md §3 |
mapobjects_store, mission_client, scan_controller |
Coordinate, Geofence, MissionItem |
data_model.md §4 |
mission_client, mission_executor, operator_bridge |
MissionWaypoint |
data_model.md §4 |
mission_executor, mavlink_layer |
OperatorCommand |
data_model.md §4 |
operator_bridge, scan_controller, mission_executor |
GimbalState |
data_model.md §4 |
gimbal_controller, frame_ingest, movement_detector |
AutopilotError, Result<T> |
new | every crate |
ComponentHealth, AggregatedHealth |
new (per containerization.md §7) |
every crate + autopilot/runtime.rs |
Component Public APIs (live in each component's lib.rs)
Each component exposes an actor handle plus its narrow request/response trait. Inter-component communication is Tokio channels owned inside the component; consumers receive a typed handle, not the underlying tokio::sync::* types.
| Component | Public surface (handle methods) | Exposed to |
|---|---|---|
frame_ingest |
FrameIngestHandle::subscribe() -> FrameStream, health() |
detection_client, movement_detector, telemetry_stream |
detection_client |
DetectionClientHandle::request(Frame) -> Result<DetectionBatch>, health() |
scan_controller, movement_detector, telemetry_stream |
movement_detector |
MovementDetectorHandle::candidates() -> CandidateStream, health() |
scan_controller |
semantic_analyzer |
SemanticAnalyzerHandle::analyze(Roi) -> Result<Tier2Evidence>, health() |
scan_controller |
vlm_client (trait) |
VlmProvider::assess(Roi) -> Result<VlmAssessment> (default impl returns vlm_disabled) |
scan_controller |
scan_controller |
ScanControllerHandle::tick(), submit_operator_cmd(OperatorCommand), health() |
autopilot::runtime |
mapobjects_store |
MapObjectsStoreHandle::classify(Detection) -> Classification, apply_decline(Poi), dump_pending() -> MapObjectsBundle, hydrate(MapObjectsBundle), health() |
scan_controller, mission_client |
gimbal_controller |
GimbalControllerHandle::set_pose(GimbalCommand), zoom(level), state() -> GimbalState, health() |
scan_controller |
operator_bridge |
OperatorBridgeHandle::surface_poi(POI) -> OperatorDecision, cmds() -> CommandStream, health() |
scan_controller, mission_executor |
mission_executor |
MissionExecutorHandle::start(Mission), insert_middle_waypoint(Coordinate), failsafe_trigger(FailsafeKind), health() |
scan_controller, operator_bridge |
mavlink_layer |
MavlinkHandle::send(Command), telemetry() -> TelemetryStream, health() |
mission_executor, telemetry_stream |
mission_client |
MissionClientHandle::pull_mission() -> Mission, post_middle_waypoint(Coordinate), pull_mapobjects(MissionId) -> MapObjectsBundle, push_mapobjects(MapObjectsBundle), health() |
mission_executor, mapobjects_store |
telemetry_stream |
TelemetryStreamHandle::push_frame(Frame, Overlay), push_telemetry(Sample), health() |
frame_ingest, detection_client, mavlink_layer, operator_bridge |
CI/CD Pipeline
Single Woodpecker pipeline (per deployment/ci_cd_pipeline.md §2). Stages run sequentially; a failed stage stops the run.
| Stage | Purpose | Tool / Command |
|---|---|---|
| Fetch | Clone, restore Cargo cache | cargo fetch with remote cache key |
| Lint | cargo fmt --check; cargo clippy --all-targets --all-features -- -D warnings |
Hard fail on any warning |
| Unit Tests | cargo test --workspace (host-arch) |
Most logic is platform-independent |
| Build arm64 | Cross-compile for aarch64-unknown-linux-gnu |
cross or cargo zigbuild; produce binary + debug symbols |
| Build no-vlm | cargo build --workspace --no-default-features |
Enforces VLM optionality contract |
| Integration Tests | Replay-based, no hardware | cargo test --test '*' -- --include-ignored=false; fixtures from fixtures/ |
| SITL Conformance | ArduPilot SITL + autopilot binary in containers, fixed mission, asserts §7.7 surface + geofence | docker compose -f docker-compose.test.yml up --abort-on-container-exit |
| Security Scan | cargo audit + cargo deny check |
Dependency CVE scan |
| Benchmark Gate (manual / nightly) | Tier 1 / 2 / VLM / gimbal latency on real Jetson | Runs on self-hosted Jetson Orin Nano runner |
| Package | Build container image | Multi-arch tag azaion/autopilot:<branch>-arm64 |
| Sign | Cosign for image; OS signing flow for binary | Tagged builds only |
| Publish | Push image + binary to internal registry | Tagged builds only |
Pipeline Configuration Notes
- Cache
~/.cargo/registry/,~/.cargo/git/, andtarget/between runs keyed onCargo.lockhash. --features vlmand the no-feature path are both built and tested to enforce the optionality contract.devandmainbranches are protected; force-push forbidden; merges require a green pipeline.- Benchmark gate is opt-in (manual approval or nightly cron) because it requires a Jetson runner.
Environment Strategy
| Environment | Purpose | Configuration Notes |
|---|---|---|
| Development (local) | Run autopilot locally against mock detections + mock missions + mock Ground Station; iterate on logic | docker compose -f docker-compose.yml up; config/autopilot.dev.toml; RUST_LOG=info,autopilot=debug |
| Staging | Pre-production: real ../detections, real missions API, real Ground Station, but no airframe MAVLink (SITL instead) |
config/autopilot.staging.toml; secrets via EnvironmentFile= |
| Production (airframe) | Native systemd on Jetson Orin Nano per containerization.md §3 |
/etc/azaion/autopilot/config.toml; /etc/systemd/system/autopilot.service; /var/lib/autopilot/; /run/azaion/in-flight flight-gate marker |
| CI (Tier-1) | Lint + unit + replay-based integration on amd64 | GitHub-hosted runner; no GPU |
| CI (Tier-2) | Benchmark gate on real Jetson | Self-hosted Jetson Orin Nano Super runner; pinned JetPack + power mode |
Environment Variables
| Variable | Dev | Staging | Production | Description |
|---|---|---|---|---|
AUTOPILOT_CONFIG |
./config/autopilot.dev.toml |
/etc/azaion/autopilot/config.toml |
/etc/azaion/autopilot/config.toml |
Path to TOML config |
RUST_LOG |
info,autopilot=debug |
info |
info |
tracing-subscriber filter |
AUTOPILOT_MISSION_ID |
(per-flight CLI arg) | (per-flight CLI arg) | (per-flight CLI arg) | Active mission UUID; CLI arg, not env |
AUTOPILOT_HEALTH_BIND |
127.0.0.1:8080 |
127.0.0.1:8080 |
127.0.0.1:8080 |
HTTP /health bind address |
AUTOPILOT_VLM_ENABLED |
false |
false (until benchmark passes) |
per benchmark | Runtime VLM flag; binary must also build with --features vlm |
MISSIONS_API_TOKEN |
(mock) | from EnvironmentFile= |
from EnvironmentFile= |
Bearer token; never in config.toml |
GROUND_STATION_TOKEN |
(mock) | from EnvironmentFile= |
from EnvironmentFile= |
Bearer / session token |
All non-secret configuration lives in config.toml (per containerization.md §6). Secrets come from EnvironmentFile= on systemd, from compose secrets: in containers.
Database Migration Approach
Migration tool: none — autopilot has no traditional database.
Persistence strategy: the only persisted data is the on-device mapobjects_store. Its engine is open (architecture.md §8 Q3); the bootstrap default is in-memory + snapshot to /var/lib/autopilot/mapobjects/ (file-backed, no schema migrations). When Q3 resolves toward SQLite + H3 or another engine, the mapobjects_store crate's engine module is swapped without changing its public API. The central missions API owns its own Postgres schema (per architecture.md §7.13) — autopilot does NOT migrate central tables.
Initial Persisted Surface
| Subsystem | What is persisted | Where | Format |
|---|---|---|---|
mapobjects_store |
current_state, pending_observations, pending_ignored, sync_state |
/var/lib/autopilot/mapobjects/ |
engine-defined; default = JSON snapshots + append-only log |
operator_bridge audit log |
accepted/rejected OperatorCommand envelopes |
/var/lib/autopilot/audit/ |
newline-delimited JSON |
mission_client deferred uploads |
post-flight push payload on push failure | /var/lib/autopilot/pending_pushes/ |
JSON files keyed by mission ID |
Disk quota for /var/lib/autopilot/ is configured in config.toml; persistent-store-full at pre-flight BIT is a takeoff blocker (per architecture.md §5).
Test Structure
crates/<component>/
└── tests/ # crate-level integration tests; per-crate
└── <scenario>.rs
tests/
└── e2e/ # workspace-level end-to-end (uses docker-compose.test.yml)
├── sitl_conformance.rs # SITL gate per ci_cd_pipeline.md §5
├── geofence_inclusion.rs
├── geofence_exclusion.rs # explicit regression vs earlier silent-ignore behaviour
├── lost_link_failsafe.rs
└── operator_command_replay.rs
fixtures/
├── rtsp/<clip>.h264
├── mavlink/<replay>.tlog
├── missions/<mission>.json
└── detections/<deterministic>.json
benches/
├── tier1_latency.rs # benchmark-gate harness
├── tier2_latency.rs
├── gimbal_zoom.rs
└── movement_fpr.rs # per-zoom-band FPR replay
Test Configuration Notes
- Unit tests live alongside each component's source in
#[cfg(test)] mod tests { ... }withinsrc/files. They MUST run in <5 s on developer workstation; no network, no Docker. - Crate-level integration tests live in
crates/<component>/tests/. They may use fixtures fromfixtures/but MUST NOT cross component boundaries — that's what workspace e2e is for. - Workspace e2e in
tests/e2e/exercises the full binary against a docker-compose-managed stack (ArduPilot SITL, mock missions API, mock detections gRPC, replay RTSP). - Replay-driven debugging: all non-trivial decisions are reconstructable from logs + size-capped raw inputs (per
observability.md §6). Replay fixtures are the foundation of regression tests. - Test runner:
cargo test --workspacefor unit + integration;docker compose -f docker-compose.test.yml up --abort-on-container-exitfor e2e;cargo bench(orcriterion) for benchmark-gate measurements. - Mock-data discipline: mocks live in
tests/directories only — never in production crates (percoderule.mdc).
Implementation Order
| Order | Component | Reason |
|---|---|---|
| 1 | shared (models + config + error + health + observability + clock) |
Every other crate depends on it; nothing depends on it. Must land first. |
| 2 | mavlink_layer |
Self-contained transport; required by mission_executor and telemetry_stream; SITL conformance lands the first hard gate early. |
| 3 | mission_client |
Self-contained REST client; required by mission_executor and mapobjects_store sync. |
| 4 | mission_executor |
Combines mavlink_layer + mission_client + geofence/failsafe logic; gates takeoff via BIT. |
| 5 | gimbal_controller |
Self-contained A40 UDP driver; required by scan_controller. |
| 6 | frame_ingest |
RTSP decoder; required by all perception crates. |
| 7 | detection_client |
gRPC client to ../detections; required by scan_controller and telemetry_stream. |
| 8 | movement_detector |
Depends on frame_ingest + GimbalState; standalone otherwise. |
| 9 | mapobjects_store |
Engine choice may be deferred; default in-memory+snapshot unblocks scan_controller. |
| 10 | semantic_analyzer |
Tier 2; depends on Frame + Detection. |
| 11 | vlm_client |
Optional; default impl returns vlm_disabled. Real IPC implementation can land later. |
| 12 | telemetry_stream |
Pure egress; ready once frame_ingest, detection_client, mavlink_layer exist. |
| 13 | operator_bridge |
Depends on telemetry_stream + mapobjects_store; envelope auth scheme is Q9-stubbed. |
| 14 | scan_controller |
Sits on top of everything in Perception + Action; lands last. |
| 15 | autopilot binary (composition root) |
Wires every component handle; runs the actor topology. |
Acceptance Criteria
AC-1: Workspace scaffolded
Given the structure plan above
When the implementer executes this task
Then cargo metadata lists all 14 crates (shared, autopilot, and 12 components — vlm_client is the 13th component crate but listed under perception above) and cargo check --workspace succeeds with no compile errors.
AC-2: Stub tests runnable
Given the scaffolded workspace
When cargo test --workspace runs on a developer workstation (no Docker, no GPU)
Then every crate's stub test (e.g. it_compiles()) passes within 5 seconds total.
AC-3: CI pipeline configured
Given the scaffolded workspace
When the Woodpecker pipeline runs on a feature branch push
Then fetch → lint → unit-test → build-arm64 → build-no-vlm → integration-test → sitl-conformance all complete successfully on a known-good baseline commit.
AC-4: Dev compose boots
Given docker-compose.yml
When docker compose -f docker-compose.yml up -d runs on a fresh workstation
Then the autopilot container starts, the /health endpoint returns HTTP 200 with status: green | yellow (red is acceptable here only for components without a mock target), and the mock detections + mock missions services are reachable.
AC-5: Blackbox compose boots with SITL
Given docker-compose.test.yml
When docker compose -f docker-compose.test.yml up --abort-on-container-exit runs
Then ArduPilot SITL + autopilot + mock detections + replay RTSP all start, and the SITL conformance e2e test exits 0.
AC-6: Optionality contract enforced
Given the scaffolded workspace
When cargo build --workspace --no-default-features runs
Then the binary builds and links without the vlm feature; cargo test --workspace --no-default-features passes; the VlmProvider default impl returns VlmAssessment{status=vlm_disabled}.
AC-7: Cross-compile target ready
Given .cargo/config.toml configured for aarch64-unknown-linux-gnu
When cross build --target aarch64-unknown-linux-gnu --release (or cargo zigbuild equivalent) runs in CI
Then an aarch64 binary is produced and stored as an artifact.
AC-8: Flight-gate marker wiring exists
Given deploy/systemd/autopilot.service
When systemd parses the unit
Then ExecStartPre asserts /run/azaion/in-flight is created and ExecStopPost removes it (per containerization.md §3 and the suite-level flight-gate convention).
AC-9: Observability scaffold initialised
Given the autopilot binary
When it starts
Then tracing-subscriber emits JSON-formatted logs to stdout with the per-line fields enumerated in observability.md §2 (ts, ts_mono_ns, level, target, event), and the /health endpoint returns the per-component breakdown documented in containerization.md §7.
AC-10: Persistent state directory created
Given /var/lib/autopilot/ (or its container-mounted equivalent)
When autopilot starts in dev or prod
Then the binary creates mapobjects/, audit/, and pending_pushes/ subdirectories with the owning user, fails closed if any directory cannot be created, and surfaces the failure to /health (red on mapobjects_store).