OfflineKit Architecture

Verified against commit 942a18727c (2026-09-26). Re-verify with: dotnet test src/OfflineKit/__Tests/StellaOps.OfflineKit.WebService.Tests/StellaOps.OfflineKit.WebService.Tests.csproj (a database test suite; needs Docker for Testcontainers) and grep -n 'offlinekit' devops/release/bundle/docker-compose.yml. src/ is the source of truth; where this page and the code disagree, the code wins.

OfflineKit is the consolidated offline-exchange service of Stella Ops: one deployable family that produces signed bundles out of an estate (the export plane) and takes signed carriers in (the custody/import plane). It absorbed the Export Center service, the Mirror creator and the two AirGap posture hosts. The predecessor dossier is frozen at docs-archive/modules/export-center/architecture.md and describes hosts that no longer run.

1. Purpose and boundaries

Exception-report application withdrawal (DIP-36 / OD-1)

Exception reports cover Policy exceptions and optional exception history. The includeApplications request member and application output are withdrawn with Policy’s unused application repository. OfflineKit migration 015 removes the persisted include_applications bit; it does not remove report contents or tenant ownership. The Policy-owned exception/history API reader and deployment acceptance are tracked by PCD-871. Source tests and generated OpenAPI do not establish live acceptance.

PCD-871 supplies the tenant-bound exception-report reader. The local estate enables it in the existing ok5 service profile. Its dedicated Authority reader client has only policy:read, an allowlist and no singular tenant pin; both caller and outbound read identities bind the same authenticated tenant. Completed contents remain owned by OfflineKit and readable independently of Policy availability. Doctor/catalog identity settings are preserved.

Source verification for DIP-36: commit 394532d7114420507cb520549f9f335c9e31955d (2026-10-20). Re-verify the named native controls and generated OpenAPI; the deployment receipt is separate.

2. Runtime topology

RoleProjectImageServes
offlinekit-websrc/OfflineKit/StellaOps.OfflineKit.WebServicestellaops/offlinekit-webthe export plane, the custody/import plane, the doctor surface, /health/ready
offlinekit-workersrc/OfflineKit/StellaOps.OfflineKit.Workerstellaops/offlinekit-workerthe export-run executor, the risk-bundle and DevPortal-offline loops; only /health/liveness and /health/readiness over HTTP
offlinekit-exportcenter-init(compose service only)stellaops/offlinekit-web with an sh -c entrypoint, network_mode: none, user 0:0one-shot: mkdir -p /var/lib/stella/exportcenter && chown -R 10001:10001 on the shared export volume; both roles depend on it with service_completed_successfully

Both roles converge the same database on startup; the migration runner serialises through an advisory lock.

VolumeMountHolds
compose_offlinekit-data/var/lib/stellaimport custody: OfflineKit:Import:StorageRoot = /var/lib/stella/offlinekit-import with quarantine/, cas/, multipart-buffer/
compose_exportcenter-object-store-data/var/lib/stella/exportcenterruns/ (export runs), nis2-soa/, assurance/, audit-bundles/, risk-bundles/, devportal-offline/
bind devops/etc/exportcenter (overlay) / ./config/etc/exportcenter (bundle)/app/etc/exportcenter (read-only)operator-mounted export signing material; the directory is operator-created and keys/ is git-ignored
bind OFFLINEKIT_IMPORT_TRUST_PUBLIC_KEY_PATH (overlay) / ${STELLA_OFFLINE_DIR:-./offline}/offlinekit/import-trust.pem (bundle)/app/etc/offlinekit/import-trust.pem (read-only)the operator public key import verifies against
bind OFFLINEKIT_TIME_ANCHOR_TRUST_ROOTS_PATH/app/etc/offlinekit/time-anchor-trust-roots.pem (read-only)the timestamp-authority public chain the sealed-import gate re-verifies time anchors against
bind etc/offlinekit/appsettings.Production.json/app/appsettings.Production.json (read-only)binds Crypto:PublicTrust:CertificateBundles:timestamp-authority to the PEM above (the hyphenated key cannot pass through the sh -c entrypoint as an environment variable)
bind carrier signing key (worker)/app/etc/offlinekit/carrier-signing.pem (read-only)overlay STELLAOPS_OFFLINEKIT_CARRIER_SIGNING_KEY_HOST_PATH; bundle ./secrets/offlinekit/carrier-signing.pem, generated by the installer

3. Planes

The web host serves two planes that look alike and are not (src/OfflineKit/StellaOps.OfflineKit.WebService/Program.cs). The generated route reference is api-reference.md, rendered from openapi/v1.json — the document the host serves at /openapi/v1.json and the gateway aggregate lists as offlinekit (tools/scripts/docs/regen-from-openapi.ps1 -Service offlinekit, SPRINT_20260914_002 EA-6).

3.1 Export plane

The lifted Export Center application (src/OfflineKit/__Libraries/StellaOps.ExportCenter.Application) is mounted as a group at /api/offlinekit; every endpoint extension carries its own /v1/... prefix, so the served paths are:

GroupPrefix under /api/offlinekitSource
Profiles, runs, artifacts, events, verify/v1/exports/profiles, /v1/exports/runs, /v1/exports/runs/{runId}/artifacts, /v1/exports/runs/{runId}/events, /v1/exports/runs/{runId}/verifyApi/ExportApiEndpoints.cs
Part re-activation/v1/imports/{importId}/parts/{domain}/activate (custody plane, airgap:import) — re-offers one admitted part to its owner from its CAS bytes (EA-3)Endpoints/OfflineKitEndpoints.cs
Run carrier descriptor/v1/exports/runs/{runId}/carrier — the five carrier files a completed run wrote, with download routes; 404 when the run wrote noneApi/ExportApiEndpoints.cs, Api/ExportRunCarrierDescriptor.cs
Export attestations/v1/exports/...Attestation/AttestationEndpoints.cs
Promotion attestations/v1/promotionsAttestation/PromotionAttestationEndpoints.cs
Incidents/v1/incidentsIncident/IncidentEndpoints.cs
Risk bundles/v1/risk-bundlesRiskBundle/RiskBundleEndpoints.cs
Simulation exports/v1/exports/simulationsSimulationExport/SimulationExportEndpoints.cs
Audit bundles/v1/audit-bundlesAuditBundle/AuditBundleEndpoints.cs
Exception reports/v1/exports/exceptionsExceptionReport/ExceptionReportEndpoints.cs
Lineage evidence packs/v1/lineage-evidence-packsLineage/LineageExportEndpoints.cs
Assurance exports/v1/exports/assuranceAssuranceExports/AssuranceExportEndpoints.cs
NIS2 SoA/v1/exports/nis2/soaNis2Soa/Nis2SoaExportEndpoints.cs

Authorization is per endpoint: export.viewer for reads, export.operator for profile writes and run start/cancel, export.admin for profile archive (StellaOpsResourceServerPolicies.ExportViewer/ExportOperator/ExportAdmin). There is no global fallback policy; the predecessor’s fallback would have blocked the custody plane and the readiness probe. The tenant middleware is applied to /api/offlinekit except the four custody routes, and tenants resolve from the local catalog replica (Catalog:Replication:Tenants, replica-only resolver: a disabled replica fails the resolve loudly instead of answering “no such tenant”).

Two anonymous internal routes report export-adapter plugin status: GET /internal/plugins/status and POST /internal/plugins/probe. The /internal/* prefix must stay gateway-blocked.

Withdrawn export deprecation helpers (PCD-281, 2026-10-04): the old endpoint registry, sunset/header annotations and notification helpers are unsupported under the pre-release contract. Their sources and historical tests are frozen under src/__Obsoleted/OfflineKit/; the web host no longer registers their notification service. The supported export surface is the native route table above. Assurance profile metadata advertises complete /api/offlinekit/v1/exports/... HTTP paths; the owning host tests check each entry’s method and template against that table.

Withdrawal and advertised-route correction verified against c5cc6723ce. Recheck the compiled-type absence with pwsh -NoProfile -File tools/scripts/test-targeted-xunit.ps1 -Project src/OfflineKit/__Tests/StellaOps.ExportCenter.Tests/StellaOps.ExportCenter.Tests.csproj -Class "*DeprecationWithdrawalTests" -BuildProjectReferences -Restore, and the served route entries with the owning host’s ExportSurfacingClientRouteTableTests through the same wrapper.

3.2 Custody/import plane

src/OfflineKit/StellaOps.OfflineKit.WebService/Endpoints/OfflineKitEndpoints.cs maps the group /api/offlinekit/v1:

RouteScopePurpose
POST /api/offlinekit/v1/importairgap:importverify a carrier, take custody, dispatch parts to owners; answers 202 pointing at /status
GET /api/offlinekit/v1/statusairgap:status:readlatest verified carrier and per-part activation state for the tenant
GET /api/offlinekit/v1/manifestairgap:status:readthe manifest of the latest verified carrier
POST /api/offlinekit/v1/validateairgap:importvalidate a manifest and its envelope without taking custody

This plane resolves its tenant from the authenticated identity envelope, not from the tenants catalogue, which is why the tenant middleware excludes it: applied globally it turned a 422 digest-mismatch answer into a 403 before the verifier could say what was wrong.

3.3 Posture read

Catalog:Replication:EnvironmentState (default off) replicates Platform’s environment_state catalog into this host’s own catalog_replica schema. AirGapPostureReplicaReader answers sealed-posture and time-anchor questions from that replica, so reads keep working when Platform is down. The doctor check doctor.offlinekit.posture.not-stored-here measures that the retired airgap.state table is absent and that the custodian is platform.environment_state, class airgap-seal. The egress side of the same policy is bound from AirGap:Egress.

4. Export run engine

Source: src/OfflineKit/__Libraries/StellaOps.ExportCenter.Application/RunExecution/, src/OfflineKit/__Libraries/StellaOps.ExportCenter.WorkerRuntime/, and the Postgres repositories under src/OfflineKit/__Libraries/StellaOps.ExportCenter.Infrastructure/Postgres/Repositories/.

5. Import pipeline

Source: src/OfflineKit/StellaOps.OfflineKit.WebService/Imports/. Envelope model: OfflineKitCarrierManifest in src/OfflineKit/__Libraries/StellaOps.AirGap.Bundle/Models/OfflineKitCarrierManifest.cs (bundle{version,bundleId,channel,kind,capturedAt,isDelta,baseBundleId,releaseManifestSha256,producer} plus artifacts[]{name,sha256,size,domain,mediaType,generation}).

  1. Carrier. POST /import is multipart: the bundle archive, the manifest JSON, a DSSE envelope over the bundle and a DSSE envelope over the manifest. Size caps come from OfflineKit:Import:Max{Bundle,Manifest,Signature,DecodedSignature}Bytes; concurrency from OfflineKit:Import:MaxConcurrentImports (1 to 8). Multipart buffering is pinned inside the custody volume (multipart-buffer/), never the process temp path.
  2. Verification. Parts land in a private quarantine directory. Both envelopes are verified by OfflineKitDsseVerifier against the operator public key at OfflineKit:Import:TrustedPublicKeyPath (ES256 or PS256, SHA-256 digests, optional TrustedKeyId pin). The key must exist at startup (OfflineKitImportTrustStartupValidator); a key carried by the upload is never trusted. Manifest shape and every artifact’s (domain, mediaType, generation) triple are validated against the owner contracts before anything is promoted.
  3. Posture gate (OfflineKitImportPostureGate). Read from the posture replica: posture not declared or unsealed admits; posture unreadable admits with a logged warning (a replica outage must not strand a sealed estate’s only supply path); posture sealed runs the freshness gate and fails closed with 409 when no trusted time anchor is declared, the anchor cannot be read, or the anchor has breached the seal document’s staleness budget. Import is the sanctioned ingress of a sealed estate; it is freshness-gated, never blocked outright.
  4. Custody. The verified carrier is promoted from quarantine into the content-addressed store under OfflineKit:Import:StorageRoot/cas/ and recorded verified in offlinekit.bundle_imports (tenant-scoped idempotency key, both envelope bytes, the trust anchor’s SPKI digest, the CAS object key). Rejected bytes are deleted from quarantine and never enter the ledger or CAS. Carrier identity is semantic: the first canonical DSSE evidence in CAS wins and a re-signed retry reuses it. The CAS write pause (OfflineKitImportCasWritePause) refuses promotion once the CAS budget is exhausted (operations/import-cas-capacity-guard.md).
  5. Parts and activators. OfflineKitPartExtractor walks the archive once for every declared part a registered IOfflineKitPartActivator claims; OfflineKitPartActivatorRegistry refuses to compose when two activators claim one triple. Each part is recorded quarantined, then staged, and ends activated, failed or superseded (owner considered it and declined) in offlinekit.bundle_import_parts, with the trail in bundle_import_part_events. A part no activator claims is failed with PART_ACTIVATOR_NOT_REGISTERED while the carrier stays verified: custody and activation are separate verdicts. Registered owners:
ActivatorTripleOwner API
ScannerArtifactBomPartActivatorscanner / application/vnd.stellaops.scanner.artifact-boms.v1+json / 1POST /api/internal/v1/offlinekit/artifact-boms/activate on Scanner, scope scanner:offlinekit:artifact-boms
VulnerabilityMirrorSeedPartActivatorvulnerabilities / application/vnd.stellaops.mirror-seed+ndjson / 1POST /api/vulnerabilities/v1/mirror/seeds/import on Vulnerabilities, audience api://vulnerabilities, scopes advisory:ingest and vex:ingest (claim values in VulnerabilityMirrorSeedExchangeContract, DAY-7c); 30 min client timeout

Activators authenticate as the stellaops-offlinekit-activator client with client credentials from OfflineKit:ScannerActivation and OfflineKit:VulnerabilitiesActivation ({Scanner,Vulnerabilities}BaseAddress, Authority, TokenEndpoint) plus the secret in OFFLINEKIT_ACTIVATOR_AUTHORITY_CLIENT_SECRET, read from the process environment. The repository overlay supplies it through .local-secrets/offlinekit-activator-authority.env; the release bundle passes the same .env value to authority, offlinekit-web and offlinekit-worker.

The release installer’s offline vulnerability-data seed (SPRINT_20260925_004 DAY-7, devops/release/bundle/install.sh step 8b) does not use this pipeline: the installer mints a token for the same activator client and posts the seed straight to the hub’s import route. 6. Ledgers and status. /status reports the latest verified carrier and parts[]; contentActivated is derived from parts and never from the carrier verdict. bundle_version_history is the append-audit table reserved for activation history. The service does not enforce import version monotonicity server-side: VersionMonotonicityChecker is used by the CLI with its file-based version store, and no OfflineKit host registers a version store. The baseline’s bundle_versions table, which claimed that gate and which nothing wrote or read, is retired by 013 (guarded: a populated table stops startup with SQLSTATE 55000). CAS promotion and the ledger write are not one transaction: on a ledger failure after promotion the CAS object is retained and status reports only ledger-backed custody.

Per-part content addressing (SPRINT_20260914_002 EA-3). After the carrier is promoted, every part a registered activator claims is stored ONCE by its own digest under <OfflineKit:Import:StorageRoot>/cas/parts/sha256/<xx>/<digest> (OfflineKitImportStorage.StorePartAsync: re-hashed on write, staged through the quarantine so the capacity pause measures it, moved into place, a concurrent writer of the same address loses harmlessly). Two carriers declaring the same bytes share one file; the carrier object under cas/sha256/<carrierDigest>/ stays as custody evidence. The part’s ledger row records the address in bundle_import_parts.cas_part_key (migration 010, nullable: rows written before EA-3 and never-claimed parts carry none), and POST /api/offlinekit/v1/imports/{importId}/parts/{domain}/activate re-reads the bytes, re-hashes them against the row’s signed declaration and re-offers them to the owner through the same staged → activated / superseded / failed lifecycle — the part is verifiable and re-activatable without the carrier that delivered it. Only a failed or superseded part is re-offered; an activated part is terminal (409 PART_ALREADY_ACTIVATED), as the ledger’s forward-only rule requires. The doctor measurer (volume:offlinekit-import-cas) walks cas/ whole, so its byte count reflects one copy per part.

6. Data model

All tables below are created by the embedded migrations. Row-level security keys off app.current_tenant through export_center_app.require_current_tenant() (uuid tenants) and offlinekit.require_current_tenant() (text tenants); both fail loudly when the session set no tenant. Retention classes are recorded as table comments and measured by the doctor checks.

export_center (baseline 001, cursors 007):

TablePurpose
export_profilesoperator-authored export configuration; kind 1 to 5, status Draft/Active/Paused/Archived, scope_json, format_json, signing_json
export_runsone export execution and its outcome; status 1 to 6, trigger 1 to 4, item counters, error_json, expires_at
export_inputsresolved input items per run, written before the adapter runs
export_distributionsproduced artifacts and their distribution state; backs the artifact API; idempotency key, retention expiry, soft-delete markers
export_attestationsDSSE export attestations for readback and verification
promotion_attestation_assembliespromotion attestation assemblies
export_verification_runs, export_verification_artifactsverification readback
export_incidents, export_incident_updatesincident lifecycle (updates partitioned by 006); export_incidents is operational-state since 012: only Resolved (4) and FalsePositive (5) incidents are reclaimed, by age on last_updated_at and by a newest-first cap that counts terminal incidents only, and a reclaimed incident’s update trail cascades with it
export_timeline_notificationsdurable timeline notification sink
audit_bundle_jobsaudit-bundle jobs with lease owner/expiry and payload storage keys
exception_report_jobsexception report jobs; read by tenant and job ID, and an upsert never changes a row’s tenant
risk_bundle_jobs, risk_bundle_job_eventsrisk-bundle jobs and their event trail (events partitioned by 006)
mirror_run_cursors (007, retention 011)resume position per (tenant, profile, source), replaced only by a succeeded mirror run; cascades with the profile; cursors for sources a profile no longer declares are pruned after a successful run; budget Export:Retention:MirrorCursors:MaxCursorsPerProfile (code default 512), measured by doctor.offlinekit.retention.mirror-cursor-budget
export_run_tenants (014)RLS-free tenant-id registry filled atomically by export-run inserts; the worker binds each tenant before querying its FORCE-protected runs; no run content and no age prune while run history preserves custody

export_center_app: no tables; require_current_tenant(), update_updated_at(), register_export_run_tenant() and tenants_with_pending_runs(status).

Migration 014 corrects the applied 007 function’s FORCE-RLS assumption without editing its SQL. SECURITY DEFINER does not exempt a NOBYPASSRLS table owner from FORCE policies. Enumeration now reads registry keys and tests the requested status under each tenant’s bound context, then restores the caller’s context. The forward startup backfill takes an exclusive run-table lock and temporarily lifts FORCE only if it was already set; it restores that posture before the migration transaction and ledger commit. The owner never gains BYPASSRLS. The shipped table posture is otherwise preserved. Fresh and upgrade tests exercise this through the real PostgresExportRunRepository under a NOSUPERUSER NOBYPASSRLS owner.

offlinekit (baseline 001, custody 002, parts 003, states 008; 013 retires bundle_versions):

TablePurpose
bundle_version_historyappend-audit trail of activations (partitioned by 006)
bundle_importstenant-scoped custody ledger of verified carriers; status is always verified
bundle_import_partscurrent lifecycle state per dispatched part: quarantined, staged, activated, failed, superseded
bundle_import_part_eventsappend-audit trail of part state changes (partitioned by 006)

Incident retention (SPRINT_20260916_001 RFC-17, 2026-09-23). export_incidents carried a source-of-truth label from 001, but its manager has always deleted resolved incidents, so the P13 census read it as operational state by shape with no proven obligations. Before RFC-17 the count cap ranked every incident, so more than MaxStoredIncidents incidents in total made the next read delete the least recently updated ones, open or resolved. Both branches are now terminal-only, the budget is bound from Export:Retention:Incidents:*, and the Doctor budget check measures an incidents family. Re-verify:

pwsh ./tools/scripts/test-targeted-xunit.ps1 -Project src/OfflineKit/__Tests/StellaOps.ExportCenter.Tests/StellaOps.ExportCenter.Tests.csproj -Class "*ExportIncidentRetentionTests" -BuildProjectReferences
pwsh ./tools/scripts/test-targeted-xunit.ps1 -Project src/OfflineKit/__Tests/StellaOps.OfflineKit.WebService.Tests/StellaOps.OfflineKit.WebService.Tests.csproj -Method "*RetentionBudgets*" -BuildProjectReferences
python tools/scripts/validate/check-p13-retention.py --repo-root . --tables

Migrations 004, 005, 009, 011 and 012 only correct retention-class comments; applied SQL is never edited (ADR-004). The airgap schema and the predecessor’s private export_schema_version ledger are not reproduced; the standard startup-migration ledger lives in the offlinekit schema and doctor.offlinekit.migration.sole-ledger measures that it is the only one.

7. Configuration

Keys are shown in configuration form; the overlay devops/compose/docker-compose.offlinekit-consolidated.yml spells them with __ separators and maps shell variables onto them as noted.

Database and identity

KeyNotes
STELLAOPS_POSTGRES_OFFLINEKIT_CONNECTIONrequired by both roles; fails closed
Router:Enabled, Router:IdentityEnvelopeSigningKey, Router:Frame:Envelope:*, Router:Messaging:*gateway registration; OFFLINEKIT_ROUTER_ENABLED (default true), STELLAOPS_IDENTITY_ENVELOPE_SIGNING_KEY (required)
Authority:ResourceServer:AuthorityOFFLINEKIT_AUTHORITY_URL
OfflineKit:ServiceIdentity:{Authority,TokenEndpoint,ClientId,ClientSecret}OFFLINEKIT_DOCTOR_CLIENT_ID / OFFLINEKIT_DOCTOR_CLIENT_SECRET; required when any replica or doctor registration is on
Doctor:Registration:{Enabled,PlatformBaseAddress,SelfEndpoint,Tenant}OFFLINEKIT_DOCTOR_REGISTRATION_ENABLED (compose default false; the release bundle’s .env.example sets true)
Catalog:Replication:Tenants:{Enabled,FeedBaseAddress,AuthMode,AuthTenant}tenants replica; enabled in the overlay; OFFLINEKIT_TENANTS_REPLICA_FEED_BASE_ADDRESS
Catalog:Replication:EnvironmentState:*posture replica (default off)

Import custody (web)

KeyNotes
OfflineKit:Import:StorageRootabsolute, not a filesystem root; /var/lib/stella/offlinekit-import
OfflineKit:Import:TrustedPublicKeyPathmust exist at startup; bind-mounted from OFFLINEKIT_IMPORT_TRUST_PUBLIC_KEY_PATH
OfflineKit:Import:TrustedKeyIdoptional key-id pin; OFFLINEKIT_IMPORT_TRUST_KEY_ID
OfflineKit:Import:MaxBundleBytes, MaxManifestBytes, MaxSignatureBytes, MaxDecodedSignatureBytes, MaxConcurrentImportsOFFLINEKIT_IMPORT_MAX_*
OFFLINEKIT_IMPORT_CAS_BUDGET_BYTESshell-safe alias for the doctor state budget of volume:offlinekit-import-cas
OfflineKit:ScannerActivation:{ScannerBaseAddress,Authority,TokenEndpoint}OFFLINEKIT_SCANNER_BASE_ADDRESS
OfflineKit:VulnerabilitiesActivation:{VulnerabilitiesBaseAddress,Authority,TokenEndpoint}OFFLINEKIT_VULNERABILITIES_BASE_ADDRESS
OFFLINEKIT_ACTIVATOR_AUTHORITY_CLIENT_SECRETactivator client secret, environment only

Export plane (web)

KeyNotes
ExportCenter:ObjectStore:Kind, ExportCenter:ObjectStore:RootPathFileSystem, /var/lib/stella/exportcenter/audit-bundles; audit-bundle payloads fail closed without it
ExportCenter:SbomAudit:BaseUrl, ExportCenter:VulnAudit:BaseUrl, ExportCenter:TimelineAudit:BaseUrlaudit-bundle section sources
ExportCenter:RuntimePlugins:*mounted signed exporter bundles (none ship today)
Export:Attestation:Ed25519:{PrivateKeyBase64File,KeyId,PreviousKeyId,PreviousPublicKeyBase64File,Provider}export-attestation signer; STELLAOPS_EXPORTCENTER_ED25519_*
Export:Nis2Soa:{StorageRoot,SigningKeyId,SigningAlgorithmId,SigningPrivateKeyPemFile,TrustRootMode,TrustRoots:*,ServiceAccount:*,AuthorityBaseUrl}NIS2 SoA; STELLAOPS_EXPORTCENTER_NIS2_SOA_*, STELLAOPS_EXPORTCENTER_AUTHORITY_CLIENT_{ID,SECRET}
Export:Assurance:{StorageRoot,SigningKeyId,TrustRoots:*,Nis2Effectiveness:*,DoraRoi:*,DoraMajorIncidentReport:*,DoraInfoSharing:*}Assurance exports; STELLAOPS_EXPORTCENTER_*_HMAC_KEY*
Export:Retention:{RiskBundle,ExceptionReport,AuditBundle,TimelineNotifications,Incidents}:*OFFLINEKIT_RETENTION_*; measured by doctor.offlinekit.retention.bounded-state-budgets. Incidents:{RetentionPeriod,MaxStoredIncidents} default to 1 day and 200 terminal incidents
Export:Retention:MirrorCursors:MaxCursorsPerProfilemirror-cursor budget; code default 512, reported absent until declared
Export:Runs:StorageRoot, OfflineKit:OwnedState:ExportVolumeRootmeasurement roots for the export-volume Doctor states; OFFLINEKIT_EXPORT_RUN_STORAGE_ROOT, OFFLINEKIT_EXPORT_VOLUME_ROOT
AirGap:Egress:*egress policy of the export plane
EvidenceLocker:BaseUrlrequired outside Testing

Worker

KeyNotes
Export:Runs:Worker:EnabledOFFLINEKIT_EXPORT_RUNS_ENABLED (default false); composes the executor and its repositories
Export:Runs:Worker:{WorkerId,PollInterval,ExpirySweepInterval,MaxRunsPerTenantPerPass}lease loop
Export:Runs:StorageRoot/var/lib/stella/exportcenter/runs
Export:Runs:DeclaredInputRoots:Ncontainment roots for declared inputs; overlay lists /var/lib/stella/exportcenter
Export:Carrier:{SigningKeyId,SigningAlgorithmId,SigningProviderHint,SigningPrivateKeyPemFile,Producer}carrier signer (EA-1); OFFLINEKIT_CARRIER_SIGNING_KEY_ID, STELLAOPS_OFFLINEKIT_CARRIER_SIGNING_KEY_PEM_FILE; unset key id = no carrier, noted on the run
OfflineKit:ScannerActivation:{ScannerBaseAddress,Authority,TokenEndpoint} + OFFLINEKIT_ACTIVATOR_AUTHORITY_CLIENT_SECRETthe Scanner exchange identity the owner-part pull mints (same section and secret file as the web host’s activator)
RiskBundles:{Enabled,Storage:RootPath,Signing:Key,Signing:KeyId}OFFLINEKIT_RISK_BUNDLES_ENABLED, OFFLINEKIT_RISK_BUNDLE_SIGNING_KEY*
DevPortalOffline:{Enabled,Storage:RootPath,Signing:*}OFFLINEKIT_DEVPORTAL_OFFLINE_ENABLED
LIVENESS_PATH, READINESS_PATH/health/liveness, /health/readiness (the web role’s healthcheck uses /health/ready for both)

8. Security and custody

Offline carrier test boundaries

Test source verified against 039164bd8e (PCD-360, 2026-10-04); re-run the commands below.

src/__Tests/offline/StellaOps.Offline.E2E.Tests/OfflineKitRoundTripTests.cs uses OfflineKitCarrierWriter and the serving OfflineKitDsseVerifier, replacing the withdrawn snapshot writer/reader. It checks both signed subjects, exact manifest payload binding, public-trust mismatch, unsigned-envelope refusal, archive tampering, every declared part’s bytes/digest and deterministic archive/manifest bytes. ECDSA envelope bytes are not required to be deterministic. The fixture provisions ephemeral keys through the existing crypto provider and purpose resolver; only public trust is written to disk.

This local artifact round-trip does not claim host or database acceptance. The owner’s OfflineKitImportEndpointsTests separately runs the shared writer’s carrier through the HTTP import, real PostgreSQL custody ledger and the owner activation seam. Re-run the complete Offline.E2E and AirGap.Bundle test projects with the canonical tools/scripts/test-targeted-xunit.ps1 -All -BuildProjectReferences -Restore wrapper, then run StellaOps.OfflineKit.WebService.Tests with -Class "*OfflineKitImportEndpointsTests" -BuildProjectReferences -Restore.

9. Design record