OfflineKit
Verified against commit
942a18727c(2026-09-26). Re-verify with:grep -n 'Map[A-Za-z]*Endpoints\|MapGet\|MapPost\|MapGroup' src/OfflineKit/StellaOps.OfflineKit.WebService/Program.cs,ls src/OfflineKit/__Libraries/StellaOps.OfflineKit.Persistence/Migrations/andgrep -n 'offlinekit' devops/release/bundle/docker-compose.yml.The estate-wide service, database, compose and bundle wiring is generated in the service inventory; this dossier describes the module and defers to that inventory for the service list.
The consolidated offline-exchange service. One deployable family, two roles, one database.
It absorbed the Export Center service (hosts frozen at src/__Obsoleted/ExportCenter/) and the Mirror creator at SPRINT_20260722_025 (owner-approved EVL-3), and it holds the surviving AirGap exchange libraries. The AirGap posture hosts were withdrawn rather than absorbed: posture custody moved to Platform (platform.environment_state); see AirGap. The libraries the predecessors contributed keep their original namespaces on purpose: the lift was byte-identical so no consumer using had to change.
| Role | Image | Serves |
|---|---|---|
offlinekit-web | stellaops/offlinekit-web | the whole /api/offlinekit/v1 surface (export plane and carrier-custody plane), /doctor/offlinekit/checks, /health/ready, /openapi/v1.json; hosts the audit-bundle loop |
offlinekit-worker | stellaops/offlinekit-worker | the export-run executor, risk-bundle jobs and the DevPortal-offline job; HTTP only for /health/liveness and /health/readiness |
offlinekit-exportcenter-init | stellaops/offlinekit-web (one-shot) | creates /var/lib/stella/exportcenter on the shared export volume and hands it to uid 10001; both roles wait for it to complete |
Database: stellaops_offlinekit, one role, connection only from STELLAOPS_POSTGRES_OFFLINEKIT_CONNECTION (fail closed), migrated forward-only on startup from embedded SQL (ADR-004). Schemas: offlinekit (carrier custody, part activation, migration ledger), export_center (the export engine), export_center_app (tenant-isolation functions), catalog_replica + eventing (the tenants and environment-state replicas).
Volumes: offlinekit-data (import custody under /var/lib/stella/offlinekit-import) and exportcenter-object-store-data (export runs and bundles under /var/lib/stella/exportcenter).
Router name: offlinekit. One process publishes one HELLO name, which is why the predecessor’s exportcenter name died with it and every bare /v1/exports path was re-prefixed under /api/offlinekit.
The two planes, and why the distinction matters
The host serves two things that look alike and are not:
- The export plane: producing bundles out of the estate. Tenant-scoped through the standard tenant middleware, per-endpoint scopes
export.viewer/export.operator/export.admin. - The carrier-custody plane: taking signed bundles in. Q-25 verified-carrier custody, which resolves its tenant from the authenticated envelope and is therefore deliberately excluded from the tenant middleware. Applying that middleware globally made the import endpoint answer 403 where it had answered 422 for a digest mismatch, rejecting the request before the verifier could tell the operator what was wrong with their bundle.
Capabilities
| Capability | Where it lives |
|---|---|
| Export profiles, runs, artifacts, distributions | StellaOps.ExportCenter.Application/Api/, .Application/RunExecution/, .Core/Persistence/ |
| Assurance exports (CRA, NIS2 SoA, NIS2 effectiveness, DORA) | .Application/AssuranceExports/, .Application/Nis2Soa/, .Adapters.Cra, .Adapters.Nis2, .Adapters.StandardsMapping |
| Audit bundles, incidents, lineage, exception reports, simulation exports | .Application/{AuditBundle,Incident,Lineage,ExceptionReport,SimulationExport}/ |
| Risk bundles | .RiskBundles, .Application/RiskBundle/, .WorkerRuntime |
| Attestation and promotion attestation | .Application/Attestation/ |
| Mirror bundle production (a profile kind, DC-22) | .Core/MirrorBundle/MirrorBundleBuilder.cs, .Application/RunExecution/MirrorExportRunMaterializer.cs, .Application/Mirror/ |
| DevPortal offline bundle | .Core/DevPortalOffline/, job on offlinekit-worker |
| Carrier custody: import, status, manifest, validate, part re-activation | StellaOps.OfflineKit.WebService/Imports/, Endpoints/ |
| Generic part activation | …/Imports/OfflineKitPartActivation.cs, OfflineKitPartExtractor.cs |
| Sealed-posture reads | StellaOps.AirGap.Policy.Replication over this host’s own replica |
| Time-anchor verification | StellaOps.AirGap.Time.Verification |
The old StellaOps.Mirror.Creator library (project StellaOps.Mirror.Creator.Core) and its two shell scripts were withdrawn on 2026-09-26 to src/__Obsoleted/OfflineKit/__Libraries/StellaOps.Mirror.Creator/. Its only service failed closed and nothing outside its own tests used it. Mirror bundles are the mirror export profile kind.
The part-activation seam
OfflineKit owns custody; it never owns another domain’s content. An adapter implementing IOfflineKitPartActivator is keyed on the (domain, mediaType, generation) triple that the manifest validates and the parts ledger keys on, and it owns exactly three things: its own payload type, its own bounded schema, and the call to its owner API. It never touches the ledger: an activator that could would be a second custody authority.
The registry refuses to compose when two activators claim one triple. Two owners each believing they activate the same signed content is not a runtime condition to recover from, and whichever the container resolved last would silently win.
A declared part that no registered activator claims is recorded failed with PART_ACTIVATOR_NOT_REGISTERED while the carrier stays verified. Custody and activation are separate verdicts; collapsing them would make a missing adapter look like an untrusted bundle.
Registered adapters: ScannerArtifactBomPartActivator (Scanner artifact BOMs) and VulnerabilityMirrorSeedPartActivator (Vulnerabilities mirror seeds, OK-7V). Both authenticate as the stellaops-offlinekit-activator client; details in architecture.md§5.
Changes recorded 2026-09-14 to 2026-09-26
- RFC-17 (2026-09-23):
export_center.export_incidentsisoperational-state; only resolved and false-positive incidents are reclaimed, the budget is bound fromExport:Retention:Incidents:*(migration012), and the Doctor budget check measures anincidentsfamily. - Q-44 (RFC-14, ruled 2026-09-23): migration
013retires the unusedofflinekit.bundle_versionstable behind a guard that stops startup with SQLSTATE 55000 if the table holds rows.bundle_version_historyis unaffected. - CIC-2 (2026-09-25):
offlinekit-workerstarts on a fresh database with risk bundles disabled: the risk-bundle loop, its signer and store are composed only whenRiskBundles:Enabledis true, and thenRiskBundles:Signing:KeyandRiskBundles:Storage:RootPathare required at start. - DAY-7 (2026-09-26, sprint row BLOCKED on external inputs): the
stellaops-offlinekit-activatorclient’s allowed scopes arescanner:offlinekit:artifact-boms advisory:ingest vex:ingest, and the mirror-seed contract constants carry the claim valuesadvisory:ingest/vex:ingest. The release installer imports a shipped vulnerability-data seed directly into the Vulnerabilities hub (POST /api/vulnerabilities/v1/mirror/seeds/import) with that client’s token; the seed does not pass through OfflineKit custody.
Where the detail lives
architecture.md: runtime topology, the two planes, the export run engine, the import pipeline, data model, configuration and custody.api-reference.md: generated fromopenapi/v1.json(tools/scripts/docs/regen-from-openapi.ps1 -Service offlinekit).operations/import-cas-capacity-guard.md: the import CAS owned-state measurer and the write pause (OK-CAP).operations/export-storage-measurement.md: the export volume and run-storage measurements.../../runbooks/airgap/offlinekit-import.md: the operator path forstella airgap import, exit codes and the posture gate.
Retained format records, behind pointer stubs for the retired topology:
export-center/: profiles, determinism, provenance and signing, DevPortal offline bundles, mirror bundle layout, registry compatibility, the timestamp trust bundle, CLI verbs, schemas and operations notes. The retired service dossier is archived atdocs-archive/modules/export-center/.mirror/: mirror formats.airgap/: sealed and offline operation concepts and the surviving AirGap libraries.airgap-controller/: pointer stub for the retired host.
Operator-facing: docs/OFFLINE_KIT.md.
Known gaps, stated rather than implied
- One verification story (EA-2, 2026-09-14). The custody ledger has exactly one writer, the import service after
OfflineKitDsseVerifier; the architecture pack pins it (OfflineKitAdmissionConformanceTests). The legacy knowledge-snapshot and export-archive verifiers are withdrawn undersrc/__Obsoleted/OfflineKit/__Libraries/. The CLI’s client-side pre-checks (stella offline import --verify-*,stella verify offline) verify and admit nothing. TheAirGap.Syncjob-log dialect was withdrawn 2026-09-14 (SPRINT_20260914_002EA-8 on D-EA4-1): frozen undersrc/__Obsoleted/OfflineKit/__Libraries/StellaOps.AirGap.Sync/. The carrier is the one envelope. - Export runs are carriers (EA-1, 2026-09-14) only when the worker has a carrier signing key id (
Export:Carrier:SigningKeyId, release bundleOFFLINEKIT_CARRIER_SIGNING_KEY_ID, default empty). Without it a run completes withEXPORT_RUN_CARRIER_NOT_WRITTENand is downloadable but not importable.
