Offline Sync Flow

Currency check 2026-10-07: owner paths/source placement verified against 3238bd511a37e199ae99d4f17be2f13eabec8dd7. The supported boundary is Vulnerabilities publications and OfflineKit carrier custody (src/Vulnerabilities/StellaOps.Vulnerabilities.WebService/Program.cs). Re-verify the host mount and owning endpoint extension together. This is source verification; deployment and broader audit coverage are separate evidence. Other dated findings retain their stated dates. Re-verify: rg -n "MapGroup|Map.*Endpoints|AddHostedService" src/Vulnerabilities/StellaOps.Vulnerabilities.WebService/Program.cs, then inspect the endpoint extension or library pointer cited at the operation.

Supported implementation

Vulnerabilities owns corpus publication and mirror-seed activation. OfflineKit owns the carrier, private quarantine/CAS, transport verification and custody/part ledgers; it never writes content directly into a sibling database. Platform owns environment posture.

OperationCurrent route
Corpus streamGET /api/vulnerabilities/v1/corpus/export
Current publicationGET /api/vulnerabilities/v1/corpus/publications/current
Publication manifestGET /api/vulnerabilities/v1/corpus/publications/{artifactRef}/manifest
Detached sectionGET /api/vulnerabilities/v1/corpus/publications/{artifactRef}/sections/{sectionName}
Domain activationPOST /api/vulnerabilities/v1/mirror/seeds/import
Carrier custodyPOST /api/offlinekit/v1/import, GET /api/offlinekit/v1/status

The exchange part is vulnerabilities / application/vnd.stellaops.mirror-seed+ndjson / generation 1 (VulnerabilityMirrorSeedExchangeContract). Activation requires the hub’s advisory-ingest and VEX-ingest policies. Carrier verification can succeed while a part is failed or unsupported; report both verdicts.

The old Excititor distribution/domain-index and registration APIs and Concelier mirror-seed tool are predecessor history. They are not renamed Vulnerabilities endpoints. Use publication/activation contracts instead of changing their hostname.

Source pointers:

Posture is held by Platform, exchange custody by OfflineKit, and imported content by the service that owns the domain. The Controller/Time predecessor hosts, their HTTP DTOs and their telemetry meters are withdrawn source. There is no replacement standalone time service to deploy.

ConcernSupported surface and owner
Posture custodyPlatform GET/PUT /api/v1/platform/environment-state/{stateClass}/{scope}; classes airgap-seal and time-anchor, default CLI scope installation
Seal/unseal/statusstella airgap seal, stella airgap unseal, stella airgap status declare/read Platform state with no local marker fallback
Verified time declarationstella airgap time-anchor declare --token anchor.tsr --trust-roots tsa-chain.pem; verification precedes declaration
Time readbackstella airgap time-anchor status --trust-roots tsa-chain.pem re-verifies the stored proof against supplied roots
Carrier import/validationOfflineKit POST /api/offlinekit/v1/import, POST /api/offlinekit/v1/validate
Custody and part stateOfflineKit GET /api/offlinekit/v1/status, GET /api/offlinekit/v1/manifest
Re-offer a failed/superseded partPOST /api/offlinekit/v1/imports/{importId}/parts/{domain}/activate; activated is terminal

Platform stores opaque JSON and enforces class custody, not air-gap semantics. Its read policy accepts envstate:read or the air-gap status grant; declaring airgap-seal needs envstate:airgap-seal:write (the recorded airgap:seal grant is accepted by that custody policy), and time-anchor needs envstate:time-anchor:write. OfflineKit import/validate use airgap:import; custody status/manifest use airgap:status:read. Check the current Authority and Platform policy registrations before assigning grants.

AirGapSealInterpreter and AirGapTimeAnchorInterpreter in src/__Libraries/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/Posture/ distinguish Declared, NotDeclared and Unreadable. Missing/unreadable state is not a measured sealed/unsealed result; an absent anchor is not fresh. Consumers read their environment-state replica so a Platform outage does not force a cross-service posture read.

RFC3161 verification is the library under src/OfflineKit/__Libraries/StellaOps.AirGap.Time.Verification/. The CLI writer checks the token signature and supplied trust chain without certificate downloads or revocation checking, and records revocationChecked: false. Roughtime remains an enum member but has no current verifier. Consumers re-verify stored proof with the public trust purpose timestamp-authority; a declared timestamp alone is not verification evidence.

For sealed import, OfflineKitImportPostureGate refuses a missing, unreadable or breached trusted time anchor with 409. Warning age admits with a warning. Unsealed/undeclared posture admits; an unreadable posture replica admits with a warning, as implemented. A verified carrier means custody, not activation: each part is offered to its registered owner. Unsupported parts report PART_ACTIVATOR_NOT_REGISTERED; owner activation can fail while custody remains verified.

AirGap:Egress and the in-process IEgressPolicy interpreter govern outbound calls. Sealing state is a declaration, not firewall proof. Apply and verify network controls using the Compose/host tooling in the current sealing guide. Egress policy is a sealed-mode gate, not an SSRF control.

Source pointers:

Operator references

Historical Predecessor offline-sync flow 2026-05-30 (superseded 2026-09-14)

The remainder records the predecessor implementation or unlanded design at the dates stated in its text. All routes, host names, source paths, DTOs, metrics and examples in this section are historical comparison, including sentences that originally said “current” or “today”. They are not instructions for the consolidated estate. The supported section and owner source above supersede them. Do not rebuild or deploy a predecessor from these examples.

Audience: operators and integrators running StellaOps in air-gapped or disconnected estates, and developers working on the AirGap modules (src/AirGap).

Overview

The Offline Sync Flow describes how StellaOps supports air-gapped and disconnected environments through the Offline Kit. This flow covers knowledge-bundle generation, secure transfer, verification, and import in environments with no external network connectivity.

Business Value: Enable full vulnerability scanning and policy evaluation capabilities in highly secure, air-gapped environments while maintaining audit trails and cryptographic verification.

Actors

Reconciled against src/AirGap (Controller, Importer, Time, __Libraries/StellaOps.AirGap.Bundle, __Libraries/StellaOps.AirGap.Sync — the latter withdrawn 2026-09-14 (SPRINT_20260914_002 EA-8 on D-EA4-1), frozen under src/__Obsoleted/OfflineKit/__Libraries/) and the scope catalog src/Authority/.../StellaOps.Auth.Abstractions/StellaOpsScopes.cs. The original actor list attributed offline-bundle sealing/signing to EvidenceLocker and a standalone Signer service; that is INCORRECT. Offline knowledge bundles are built and DSSE-signed inside the AirGap.Bundle library (SnapshotBundleWriter + SnapshotManifestSigner), not by EvidenceLocker (which seals decision capsules, a separate concern — see src/EvidenceLocker/.../Api/Capsules).

ActorTypeRole
Online AdminHumanGenerates and exports offline knowledge bundles (stella airgap export)
Offline AdminHumanImports and verifies bundles (stella airgap import [--verify-only])
Concelier (mirror)ServiceSource of advisory raw data snapshotted into bundles (StellaOps.Concelier.MirrorSeedTool)
AirGap.Bundle (SnapshotBundleWriter)LibraryAssembles the snapshot, computes the Merkle root, writes manifest.json, and DSSE-signs manifest.sig
AirGap ControllerServiceSeal/unseal state machine + status/verify APIs under /system/airgap
AirGap ImporterLibraryValidates bundles (TUF → DSSE → Merkle → rotation → referrer → version monotonicity) and quarantines failures
AirGap TimeServiceTime-anchor ingest/status (/api/v1/time); Roughtime + RFC3161 verification; staleness budgets
Import targetsAdaptersLoad verified content into Concelier (advisories), Excititor (VEX), and the Policy registry

Prerequisites

Online Environment

Offline Environment

Authority Scopes

Source of truth: src/Authority/.../StellaOps.Auth.Abstractions/StellaOpsScopes.cs.

ScopeConstantUsed by
airgap:status:readAirgapStatusReadGET /system/airgap/status, GET /api/v1/time/status
airgap:sealAirgapSealPOST /system/airgap/seal, POST /system/airgap/unseal, POST /api/v1/time/anchor (write)
airgap:importAirgapImportOffline bundle import while sealed

The time-anchor write endpoint (POST /api/v1/time/anchor) is guarded by the ASP.NET policy AirGapTime.AnchorWrite, whose backing scope is airgap:seal (AirGapTimePolicies.AnchorWriteScope = StellaOpsScopes.AirgapSeal) — there is no distinct airgap:anchor:write scope.

NOTE: POST /system/airgap/verify is registered against the AirGapPolicies.Verify policy, whose backing scope is airgap:status:read (StellaOpsScopes.AirgapStatusRead, Program.cs:59-69) — verification is a read-only integrity check, so it shares the air-gap read scope. There is no airgap:verify scope; only the three scopes above are catalogued. (airgap:write still appears as an expected import scope in AirGapControllerContractTests:224 and is absent from the catalog — that one remains a code/catalog gap.)

Bundle Types

In practice these are sections of one knowledge snapshot bundle (KnowledgeSnapshotManifest carries advisories, vexStatements, policies, ruleBundles, trustRoots, and an optional timeAnchor), rather than five independently scheduled artifacts. The “Frequency” column is operator convention, not an enforced schedule.

SectionMaps toManifest field
Advisoriesper-feed advisory files (advisories/<feedId>/)advisories (AdvisorySnapshotEntry)
VEXper-source VEX files (vex/<sourceId>/)vexStatements (VexSnapshotEntry)
Policiespolicy files (policies/)policies (PolicySnapshotEntry)
Rule bundlesrule files (rules/<bundleId>)ruleBundles (RuleBundleSnapshotEntry)
Trust rootsDSSE trust material (trust/)trustRoots (TrustRootSnapshotEntry)
Time anchorRoughtime or RFC3161 tokentimeAnchor (TimeAnchorEntry)

The original “Time Anchor … NTP alternatives” entry is corrected: implemented anchor formats are Roughtime and RFC3161 only (TimeTokenFormat); plain NTP is not a supported anchor source.

Flow Diagram

NOTE: the diagram is conceptual. The “Evidence Locker” / “Sign” / “Verif” boxes predate reconciliation — in the implemented system the online side is SnapshotBundleWriter + SnapshotManifestSigner (DSSE), and the offline side is the AirGap Controller (seal/verify) plus the Importer validation chain. See the Actors table and step-by-step for accurate component names.

┌─────────────────────────────────────────────────────────────────────────────────┐
│                            Offline Sync Flow                                     │
└─────────────────────────────────────────────────────────────────────────────────┘

 ONLINE ENVIRONMENT                         OFFLINE ENVIRONMENT
┌────────────────────────────────┐         ┌────────────────────────────────┐
│                                │         │                                │
│ ┌────────┐  ┌────────┐  ┌────┐│         │┌────┐  ┌─────────┐  ┌────────┐│
│ │ Mirror │  │Evidence│  │Sign││         ││Verif│  │ AirGap  │  │Concelier││
│ │        │  │ Locker │  │    ││         ││     │  │Importer │  │        ││
│ └───┬────┘  └───┬────┘  └──┬─┘│         │└──┬──┘  └────┬────┘  └───┬────┘│
│     │           │          │  │         │   │          │           │     │
│     │ Snapshot  │          │  │         │   │          │           │     │
│     │ advisories│          │  │         │   │          │           │     │
│     │───┐       │          │  │         │   │          │           │     │
│     │   │       │          │  │         │   │          │           │     │
│     │<──┘       │          │  │         │   │          │           │     │
│     │           │          │  │         │   │          │           │     │
│     │ Create    │          │  │         │   │          │           │     │
│     │ bundle    │          │  │         │   │          │           │     │
│     │──────────>│          │  │         │   │          │           │     │
│     │           │          │  │         │   │          │           │     │
│     │           │ Seal     │  │         │   │          │           │     │
│     │           │──────────>  │         │   │          │           │     │
│     │           │          │  │         │   │          │           │     │
│     │           │ Signed   │  │         │   │          │           │     │
│     │           │ bundle   │  │         │   │          │           │     │
│     │           │<──────────  │         │   │          │           │     │
│     │           │          │  │         │   │          │           │     │
│     │           │          │  │         │   │          │           │     │
│     │      [Export to       ] │         │   │          │           │     │
│     │      [removable media ] │ =========> [Import from│           │     │
│     │           │          │  │         │   │removable]│           │     │
│     │           │          │  │         │   │          │           │     │
│     │           │          │  │         │   │ Verify   │           │     │
│     │           │          │  │         │   │ signature│           │     │
│     │           │          │  │         │   │───┐      │           │     │
│     │           │          │  │         │   │   │      │           │     │
│     │           │          │  │         │   │<──┘      │           │     │
│     │           │          │  │         │   │          │           │     │
│     │           │          │  │         │   │ Verify   │           │     │
│     │           │          │  │         │   │ Merkle   │           │     │
│     │           │          │  │         │   │───┐      │           │     │
│     │           │          │  │         │   │   │      │           │     │
│     │           │          │  │         │   │<──┘      │           │     │
│     │           │          │  │         │   │          │           │     │
│     │           │          │  │         │   │ Import   │           │     │
│     │           │          │  │         │   │──────────>           │     │
│     │           │          │  │         │   │          │           │     │
│     │           │          │  │         │   │          │ Unpack    │     │
│     │           │          │  │         │   │          │ advisories│     │
│     │           │          │  │         │   │          │───┐       │     │
│     │           │          │  │         │   │          │   │       │     │
│     │           │          │  │         │   │          │<──┘       │     │
│     │           │          │  │         │   │          │           │     │
│     │           │          │  │         │   │          │ Merge to  │     │
│     │           │          │  │         │   │          │ Concelier │     │
│     │           │          │  │         │   │          │──────────>│     │
│     │           │          │  │         │   │          │           │     │
│     │           │          │  │         │   │          │           │     │
└────────────────────────────────┘         └────────────────────────────────┘

Step-by-Step

1. Advisory Snapshot (Online)

Source advisory data comes from Concelier’s raw advisory store (mirror seeding via StellaOps.Concelier.MirrorSeedTool). The illustrative snapshot_id / delta_from envelope below is conceptual — there is no snapshot.json of this shape in src/AirGap; per-feed snapshots are captured into the manifest as AdvisorySnapshotEntry rows (feedId, relativePath, digest, recordCount, snapshotAt) when SnapshotBundleWriter builds the bundle.

{
  "snapshot_id": "snap-20241229",
  "created_at": "2024-12-29T00:00:00Z",
  "sources": [
    {"name": "nvd", "last_sync": "2024-12-29T00:00:00Z", "count": 245678},
    {"name": "ghsa", "last_sync": "2024-12-28T23:45:00Z", "count": 45678},
    {"name": "osv", "last_sync": "2024-12-28T23:30:00Z", "count": 89012}
  ],
  "delta_from": "snap-20241228",
  "advisories": {
    "new": 127,
    "updated": 456,
    "unchanged": 245095
  }
}

2. Bundle Generation (Online)

SnapshotBundleWriter (in StellaOps.AirGap.Bundle) assembles the snapshot and writes a manifest.json (KnowledgeSnapshotManifest). The manifest below reflects the actual schema — the original example invented an integrity/contents block that does not exist in code:

{
  "bundleId": "knowledge-2024-12-29",
  "name": "knowledge-2024-12-29",
  "version": "1.0.0",
  "createdAt": "2024-12-29T01:00:00Z",
  "schemaVersion": "1.0.0",
  "merkleRoot": "sha256:abc123...",
  "totalSizeBytes": 131072000,
  "entryCount": 15,
  "advisories": [
    { "feedId": "nvd", "relativePath": "advisories/nvd/nvd.ndjson", "digest": "sha256:...", "sizeBytes": 0, "recordCount": 245678 }
  ],
  "vexStatements": [],
  "policies": [],
  "trustRoots": [],
  "ruleBundles": [],
  "timeAnchor": null
}

The Merkle root is computed over the per-entry digests (ComputeMerkleRoot) and stored inside the manifest — there is no separate merkle/tree.json artifact. The archive is a deterministic PAX tar.gz (TarEntryFormat.Pax, written by SnapshotBundleWriter.Tar.cs) with a fixed modification time and ordinal path ordering. The on-disk extension is .tar.gz (NormalizeOutputPath appends .tar.gz if missing); the CLI default output name is knowledge-<yyyyMMdd>.tar.gz. There is no .stella.bundle.tgz extension in the code.

3. Bundle Signing (Online)

SnapshotManifestSigner produces a DSSE envelope over the canonical manifest bytes and writes it to manifest.sig. This corrects the original “detached PKCS#7 signature + full cert chain + external TSA” model, which is not what the code does:

{
  "payloadType": "application/vnd.stellaops.knowledge-snapshot+json",
  "payload": "base64(manifest.json)...",
  "signatures": [
    { "keyid": "<sha256 fingerprint of public key>", "sig": "base64(signature)..." }
  ]
}

4. Export Package

Final package layout reflects the directories actually created by SnapshotBundleWriter (Sections.* partials). The original tree listed a merkle/tree.json, verify.sh, and README.md that the writer does not emit:

knowledge-<yyyyMMdd>.tar.gz       (deterministic PAX tar.gz)
├── manifest.json                 # KnowledgeSnapshotManifest (includes merkleRoot, entries)
├── manifest.sig                  # DSSE envelope over the manifest (present only when signed)
├── advisories/
│   └── <feedId>/<fileName>       # e.g. advisories/nvd/nvd.ndjson
├── vex/
│   └── <sourceId>/<fileName>
├── policies/
│   └── <fileName>
├── rules/
│   └── <ruleBundleId>/<fileName> # rule files live under the bundle dir
└── trust/
    └── <fileName>                # trust-root material referenced by the manifest

A timeAnchor entry, when present, is recorded in manifest.json (TimeAnchorEntry) rather than as a fixed top-level directory.

5. Secure Transfer

Transfer via approved mechanism:

6. Import Verification (Offline)

The CLI binary is stella (the airgap command group lives in AirGapCommandGroup.cs); there is no stellaops-airgap binary. The airgap group exposes export, import, diff, status, and a jobs subgroup (export/import/list) — there is no airgap verify-bundle subcommand. Verification-only is the import command with --verify-only, and import takes the bundle as a positional argument, not --bundle. Verification runs through ImportValidator.ValidateAsync, which executes a fixed ordered chain and quarantines the bundle on any failure (under /updates/quarantine/<tenantId>/...):

# Verify a bundle without applying changes (positional bundle arg)
stella airgap import /mnt/media/knowledge-20241229.tar.gz --verify-only

# Import (verifies, then activates) — see step 8
stella airgap import /mnt/media/knowledge-20241229.tar.gz

A separate stella verify bundle --bundle <path> command exists (VerifyCommandGroup.cs), but it verifies E2E evidence bundles (input-hash + verdict replay), not knowledge-snapshot bundles — do not confuse the two.

ImportValidator chain (each step fails closed; reason codes are emitted as structured logs):

  1. TUF metadata — TufMetadataValidator.Validate(root, snapshot, timestamp) (TUF_INVALID).
  2. DSSE signature — DsseVerifier.Verify against trusted fingerprints/keys + trust window (DSSE_INVALID).
  3. Merkle root — MerkleRootCalculator.ComputeRoot over payload entries must equal the manifest’s merkleRoot (MERKLE_ROOT_MISSING / MERKLE_ROOT_MISMATCH).
  4. Root rotation policy — RootRotationPolicy.Validate over active/pending keys + approver IDs (ROTATION_INVALID).
  5. Referrer validation — only for mirror-bundle / offline-kit types (REFERRER_VALIDATION_FAILED; counts missing/checksum/size mismatches).
  6. Version monotonicity — IVersionMonotonicityChecker blocks rollback per tenant/type (VERSION_NON_MONOTONIC); overridable only with --force and a forceActivateReason.

The original example showed an X.509 certificate-chain check and a TSA timestamp window. Neither matches the code: trust is fingerprint-based DSSE, and freshness is enforced separately by the Time service’s staleness budgets (see step 7), not as part of bundle signature verification.

7. Time Anchor Verification (Offline)

The AirGap Time service (/api/v1/time) ingests a time anchor and reports staleness without network time. Anchors are Roughtime or RFC3161 tokens (TimeTokenFormat), verified by RoughtimeVerifier / Rfc3161Verifier. Freshness is expressed as a staleness budget (StalenessBudget: warningSeconds, breachSeconds; default 3600 / 7200), evaluated into a StalenessEvaluation (isWarning / isBreach) — there is no FRESH/STALE/EXPIRED enum.

# Read status requires airgap:status:read; write (set anchor) requires airgap:seal
# (policy AirGapTime.AnchorWrite → scope airgap:seal). The body maps to SetAnchorRequest;
# trustRootKeyId / trustRootAlgorithm / trustRootPublicKeyBase64 are optional and only
# honoured outside Production (AllowRequestSuppliedTrustRoots).
POST /api/v1/time/anchor
{ "tenantId": "...", "hexToken": "<roughtime|rfc3161 token hex>", "format": "Roughtime",
  "warningSeconds": 3600, "breachSeconds": 7200 }

GET /api/v1/time/status returns a TimeStatusDto. The actual shape is wider than the staleness evaluation alone — it embeds anchor identity plus per-content staleness. Note secondsRemaining is a computed property on StalenessEvaluation, not a field of TimeStatusDto:

{
  "anchorTime": "2024-12-28T16:00:00.0000000Z",
  "format": "Roughtime",
  "source": "roughtime:<server>",
  "fingerprint": "<signing key fingerprint>",
  "digest": "<token digest>",
  "ageSeconds": 32400,
  "warningSeconds": 3600,
  "breachSeconds": 7200,
  "isWarning": true,
  "isBreach": true,
  "contentStaleness": { "advisories": { "ageSeconds": 32400, "warningSeconds": 3600, "breachSeconds": 7200, "isWarning": true, "isBreach": true } },
  "evaluatedAtUtc": "2024-12-29T01:00:00.0000000Z"
}

The seal flow (POST /system/airgap/seal) records a driftBaselineSeconds (wall clock vs. anchor at seal) and per-content staleness budgets (advisories / vex / policy). Request-supplied trust roots are rejected outside Development/Testing (AllowRequestSuppliedTrustRoots).

8. Advisory Import (Offline)

KnowledgeSnapshotImporter loads verified content into the registered import targets:

The result shape is SnapshotImportResult. The per-module processed/created/updated/failed counts live in a nested Statistics object (ImportStatistics), not flat at the root; the root carries success, bundleId, startedAt, completedAt, errors, and error. ImportStatistics also exposes computed totalProcessed/totalCreated/totalUpdated/totalFailed rollups. The original example’s import_id / advisories_imported / snapshot-state fields are not the actual contract:

{
  "success": true,
  "bundleId": "knowledge-2024-12-29",
  "startedAt": "2024-12-29T02:00:00Z",
  "completedAt": "2024-12-29T02:00:42Z",
  "statistics": {
    "advisoriesProcessed": 583,
    "advisoriesCreated": 127,
    "advisoriesUpdated": 456,
    "advisoriesFailed": 0,
    "vexProcessed": 0,
    "vexCreated": 0,
    "vexUpdated": 0,
    "vexFailed": 0,
    "policiesProcessed": 0,
    "policiesCreated": 0,
    "policiesUpdated": 0,
    "policiesFailed": 0
  },
  "errors": [],
  "error": null
}

Bundle Freshness Policies

DRAFT / illustrative. The YAML profiles below are an operator-facing convention and do not map to a config schema in src/AirGap. The implemented freshness control is the per-tenant StalenessBudget (warningSeconds / breachSeconds, default 3600 / 7200) set on the time anchor and at seal time, plus optional per-content budgets (advisories / vex / policy). Treat the mode / max_age / require_tsa keys as roadmap, not current behavior.

Strict (High Security)

freshness_policy:
  mode: strict
  max_age:
    advisory: 24h
    vex: 24h
    policy: 7d
    trust: 30d
  require_tsa: true
  reject_stale: true

Standard

freshness_policy:
  mode: standard
  max_age:
    advisory: 7d
    vex: 7d
    policy: 30d
    trust: 90d
  require_tsa: false
  warn_stale: true

Permissive (Disconnected Operations)

freshness_policy:
  mode: permissive
  max_age:
    advisory: 30d
    vex: 30d
    policy: 90d
    trust: 365d
  require_tsa: false
  warn_stale: true
  allow_manual_override: true

Data Contracts

The TypeScript interfaces below are an illustrative/aspirational view. The authoritative schemas live in C# under StellaOps.AirGap.Bundle.Models: the on-disk knowledge manifest is KnowledgeSnapshotManifest (see step 2 — bundleId/name/version/merkleRoot/entryCount plus typed entry lists), and import results are SnapshotImportResult (step 8). Field names below use snake_case and a flatter OfflineBundle shape that does not match the implemented camelCase records; retained here as a conceptual summary, flagged as not-implemented-as-written.

Bundle Manifest Schema (conceptual — see KnowledgeSnapshotManifest for the real schema)

interface OfflineBundle {
  bundle_id: string;
  bundle_type: 'advisory' | 'vex' | 'policy' | 'trust' | 'time_anchor';
  version: string;
  created_at: string;
  created_by: string;
  contents: {
    files: Array<{
      path: string;
      size: number;
      sha256: string;
    }>;
    metadata: Record<string, unknown>;
  };
  integrity: {
    merkle_root: string;
    algorithm: 'sha256';
    tree_path: string;
  };
  signature: {
    keyid: string;
    algorithm: string;
    sig: string;
  };
  freshness: {
    timestamp: string;
    tsa_timestamp?: string;
    valid_until?: string;
  };
}

Import Result Schema (conceptual — see SnapshotImportResult for the real schema)

interface ImportResult {
  import_id: string;
  bundle_id: string;
  status: 'success' | 'partial' | 'failed';
  started_at: string;
  completed_at: string;
  results: {
    records_imported: number;
    records_updated: number;
    records_new: number;
    conflicts: number;
    errors: number;
  };
  verification: {
    signature_valid: boolean;
    merkle_verified: boolean;
    freshness_check: 'FRESH' | 'STALE' | 'EXPIRED';
  };
  audit_log_entry: string;
}

Scheduling Strategies

DRAFT / illustrative. The offline_sync: YAML below is an operator convention; there is no such config schema in src/AirGap. Bundle export is invoked on demand via stella airgap export (and can be wrapped in any external scheduler/cron). type: delta vs. full and retention are roadmap conventions, not implemented config keys.

Daily Sync

offline_sync:
  advisory_bundle:
    schedule: "0 1 * * *"  # Daily at 1 AM
    type: delta
    retention: 7
  vex_bundle:
    schedule: "0 2 * * *"  # Daily at 2 AM
    type: delta
    retention: 7

Weekly Full + Daily Delta

offline_sync:
  advisory_bundle:
    full:
      schedule: "0 0 * * SUN"  # Weekly full on Sunday
      retention: 4
    delta:
      schedule: "0 1 * * MON-SAT"  # Daily delta Mon-Sat
      retention: 7

Error Handling

Reconciled to ImportValidator reason codes. Any failure in the validation chain is logged with a reason_code and the bundle is moved to quarantine (/updates/quarantine/<tenantId>/...) for triage rather than partially imported.

FailureReason codeBehavior
TUF metadata invalidTUF_INVALIDReject + quarantine
DSSE signature untrusted/invalidDSSE_INVALIDReject + quarantine
Computed Merkle root emptyHASH_MISMATCH (reason merkle-empty)Reject + quarantine
Merkle root missing/mismatchMERKLE_ROOT_MISSING / MERKLE_ROOT_MISMATCHReject + quarantine
Trust-root rotation invalidROTATION_INVALIDReject + quarantine
Referrer validation failedREFERRER_VALIDATION_FAILEDReject + quarantine (mirror/offline-kit types)
Version rollback (non-monotonic)VERSION_NON_MONOTONICReject unless ForceActivate + ForceActivateReason
Force activate without reasonFORCE_ACTIVATE_REASON_REQUIREDReject + quarantine
Manifest version unparseableVERSION_PARSE_FAILEDReject + quarantine
Version store write failed(reason version-store-write-failed:*)Reject + quarantine
Time anchor over budget(Time service)isWarning / isBreach on staleness evaluation

Observability

Metric names reconciled against AirGapTelemetry (Controller meter StellaOps.AirGap.Controller) and OfflineKitMetrics (Importer meter StellaOps.AirGap.Importer). The original offline_* metric names were invented and have been replaced with the emitted instruments.

Metrics — Controller (StellaOps.AirGap.Controller)

MetricTypeLabels / Tags
airgap_seal_totalCountertenant, sealed
airgap_unseal_totalCountertenant, sealed
airgap_startup_blocked_totalCountertenant, reason
airgap_time_anchor_age_secondsObservableGaugetenant
airgap_staleness_budget_secondsObservableGaugetenant

Metrics — Importer (StellaOps.AirGap.Importer)

MetricTypeLabels / Tags
offlinekit_import_totalCounterstatus, tenant_id
offlinekit_attestation_verify_latency_secondsHistogramattestation_type, success
attestor_rekor_success_totalCountermode
attestor_rekor_retry_totalCounterreason
rekor_inclusion_latencyHistogramsuccess

Key Log Events

Structured log message templates as emitted in code (see AirGapTelemetry / ImportValidator).

EventLevelFields
airgap.sealedINFOtenant, policy_hash, anchor_source, anchor_digest, age_seconds
airgap.unsealedINFOtenant, last_transition_at
airgap.startup.validation (passed/failed)INFO / CRITICALtenant, allowlist_count / reason
offlinekit.import.validation (succeeded/failed)INFO / WARNtenant_id, bundle_type, bundle_digest, reason_code
offlinekit.import.force_activationWARNtenant_id, bundle_type, incoming_version, current_version, force_activate_reason
offlinekit.import.quarantineERRORtenant_id, bundle_path, reason_code