# AirGap Controller Scaffold (Draft) — PREP-AIRGAP-CTL-56-001/002/57-001/57-002/58-001 Status: Draft (2025-11-20) Owners: AirGap Controller Guild · Observability Guild · AirGap Time Guild · DevOps Guild Scope: Define the baseline project skeleton, APIs, telemetry, and staleness fields needed to unblock controller tasks 56-001 through 58-001. ## 1) Project layout - Project: `src/AirGap/StellaOps.AirGap.Controller` (net10.0, minimal API host). - Tests: `tests/AirGap/StellaOps.AirGap.Controller.Tests` with xunit + deterministic time provider. - Shared contracts: DTOs under `Endpoints/Contracts`, domain state under `Domain/AirGapState.cs`. ## 2) State model - Persistent document `airgap_state` (Mongo): - `id` (const `singleton`), `tenant_id`, `sealed` (bool), `policy_hash`, `time_anchor` (nullable), `last_transition_at` (UTC), `staleness_budget_seconds` (int?, optional per bundle), `notes`. - Index on `{tenant_id}`; unique on `singleton` within tenant. - In-memory cache with monotonic timestamp to avoid stale reads; cache invalidated on transitions. ## 3) Endpoints (56-002 baseline) - `GET /system/airgap/status` → returns current state + staleness summary: - `{sealed, policy_hash, time_anchor:{source, anchored_at, drift_seconds}, staleness:{seconds_remaining?, budget_seconds?}, last_transition_at}`. - `POST /system/airgap/seal` → body `{policy_hash, time_anchor?, staleness_budget_seconds?}`; requires Authority scopes `airgap:seal` + `effective:write`. - `POST /system/airgap/unseal` → requires `airgap:seal`. - Validation: reject seal if missing `policy_hash` or time anchor when platform requires sealed mode. ## 4) Telemetry (57-002) - Structured logs: `airgap.sealed`, `airgap.unsealed`, `airgap.status.read` with tenant_id, policy_hash, time_anchor_source, drift_seconds. - Metrics (Prometheus/OpenTelemetry): counters `airgap_seal_total`, `airgap_unseal_total`; gauges `airgap_time_anchor_drift_seconds`, `airgap_staleness_budget_seconds`. - Timeline events (Observability stream): `airgap.sealed`, `airgap.unsealed` with correlation_id. ## 5) Staleness & time (58-001) - Staleness computation: `drift_seconds = now_utc - time_anchor.anchored_at`; `seconds_remaining = max(0, staleness_budget_seconds - drift_seconds)`. - Time anchors accept Roughtime or RFC3161 token parsed via AirGap Time component (imported service). - Status response includes drift and remaining budget; sealed mode refuses to run if budget exceeded. ## 6) Determinism & offline rules - No external network calls; time source injected `IClock` seeded in tests. - All timestamps RFC3339 UTC; responses sorted properties (serializer config). ## 7) Open decisions - Final scopes list (Authority) for status read vs seal/unseal. - Whether to require dual authorization for `seal` (two-man rule) in sealed environments. - Retention/rotation policy for `airgap_state` audit trail (append-only vs mutation). ## 8) Handoff This document satisfies PREP-AIRGAP-CTL-56-001 through 58-001. Update once Authority scopes and time-anchor token format are finalized; then promote to v1 schema doc and wire tests accordingly.