Offline Deployment
Offline deployment mode enforces a fully air-gapped runtime: every registered provider, primary route, fallback route, and default provider must target a ProviderTrustZone.LOCAL trust zone. The builder rejects any non-local provider at build time, before performing artifact verification or registry lookups.
Modules: tramai-sovereign (SovereignDeploymentMode.OFFLINE, SovereignProfileConfiguration.deploymentMode) and tramai-spring-sovereign for the Spring path. For the profile-based setup, see the Sovereign Runtime Quickstart.
In the Spring profile, tramai.sovereign.* supplies the allowlists and zones and the runtime is assembled from them; deploymentMode is an embedded-path setting on SovereignProfileConfiguration, so an OFFLINE contract is declared where the profile is constructed. In both paths the same validation fails the deployment closed before any model is invoked.
What
The offline deployment system is built on four layers:
SovereignDeploymentMode.OFFLINE— an enum value that triggers strict local-only validation during builder assemblyvalidateOfflineDeployment()— called duringSovereignTramai.Builder.build()before any registry lookup or artifact verification- Zero-egress verification — a harness that builds the runtime, invokes a loopback provider, and probes external TCP/DNS connectivity
- Evidence pack integration — zero-egress probe results can be embedded in the deployment evidence pack via
ZeroEgressEvidenceV1
What OFFLINE Enforces
When SovereignProfileConfiguration.deploymentMode == SovereignDeploymentMode.OFFLINE, the builder validates every route:
| Check | Condition | Error Code |
|---|---|---|
| Registered provider | Must be LOCAL trust zone | offline-profile-non-local-provider-rejected |
| Primary route target | Must be LOCAL trust zone | offline-profile-non-local-primary-route-rejected |
| Fallback route target | Must be LOCAL trust zone | offline-profile-non-local-fallback-rejected |
| Default provider | Must be LOCAL trust zone | offline-profile-non-local-default-provider-rejected |
What OFFLINE Does NOT Enforce
The SovereignDeploymentMode enum does not claim to enforce infrastructure-level network isolation. Production offline deployments still require:
- Firewall rules
- Container network policies (Kubernetes
NetworkPolicy) - Reverse proxy restrictions
- Sandbox or jail enforcement
- Physical air-gap controls where required
- Docker
--network=nonefor container-level isolation
Verification Order
Offline validation occurs before artifact verification and registry lookup. The builder calls validateOfflineDeployment() — if it fails, the registry is never accessed. This means a registry that throws on access is safe — offline validation catches the misconfiguration first:
// Internal builder flow:
fun build(): SovereignTramai {
// ... required-input validation ...
validateOfflineDeployment(profile) // Step 1: offline check
val verificationReceipts = verifyLocalModelArtifacts(profile, modelRegistry) // Step 2: artifact check
// ...
}
When to Use
Use offline deployment when:
- Your deployment must never egress to the public internet
- All model providers run on local machines (Ollama, vLLM, llama.cpp, etc.)
- You need build-time rejection of any non-local provider configuration
- You need zero-egress attestation — proof that the runtime cannot call external services
- You are deploying in air-gapped environments (classified networks, critical infrastructure, disconnected sites)
- You process classified data that must never leave the local trust boundary
Do not use offline deployment when:
- You use any cloud-based model provider (OpenAI, Anthropic, etc.)
- You need fallback routes to cloud providers
- Your local providers have dependencies on external services
- You need EU or global cloud trust zones
Quickstart
Kotlin
// 1. Define an offline profile
val profile = SovereignProfileConfiguration(
allowedModels = setOf("llama3.2"),
allowedProviders = setOf("ollama"),
providerZones = mapOf("ollama" to ProviderTrustZone.LOCAL),
deploymentMode = SovereignDeploymentMode.OFFLINE,
)
// 2. Build — will fail at build time if any non-LOCAL provider is configured
val tramai = SovereignTramai.builder()
.profile(profile)
.modelRegistry(localModelRegistry)
.auditStore(auditStore)
.provider(localProvider, name = "ollama", default = true)
.model("llama3.2", "ollama")
.build()
Rejected Configurations
// All of these fail at build time with specific error codes:
// Non-LOCAL registered provider — throws "offline-profile-non-local-provider-rejected"
val cloudProfile = SovereignProfileConfiguration(
allowedModels = setOf("test-model"),
allowedProviders = setOf("cloud-provider"),
providerZones = mapOf("cloud-provider" to ProviderTrustZone.GLOBAL_CLOUD),
deploymentMode = SovereignDeploymentMode.OFFLINE,
)
// Cloud fallback route — throws "offline-profile-non-local-provider-rejected"
// (the registered-provider check fires first, catching the cloud fallback
// provider as a registered non-LOCAL provider)
val fallbackProfile = SovereignProfileConfiguration(
allowedModels = setOf("test-model", "fallback-model"),
allowedProviders = setOf("local-provider", "cloud-provider"),
allowedFallbackProviders = setOf("cloud-provider"),
providerZones = mapOf(
"local-provider" to ProviderTrustZone.LOCAL,
"cloud-provider" to ProviderTrustZone.GLOBAL_CLOUD,
),
deploymentMode = SovereignDeploymentMode.OFFLINE,
)
Validation Reference
The validateOfflineDeployment() method in SovereignTramai.Builder performs these checks:
// Inside SovereignTramai.Builder.validateOfflineDeployment():
// 1. Registered providers — every registered provider must be LOCAL
for (providerName in registeredProviders) {
require(profile.providerZones.getValue(providerName) == ProviderTrustZone.LOCAL) {
"offline-profile-non-local-provider-rejected"
}
}
// 2. Primary routes — every primary route target must be LOCAL
for ((_, providerName) in primaryModelRoutes) {
require(profile.providerZones.getValue(providerName) == ProviderTrustZone.LOCAL) {
"offline-profile-non-local-primary-route-rejected"
}
}
// 3. Fallback routes — every fallback target must be LOCAL
for (fallback in fallbackRoutes) {
require(profile.providerZones.getValue(fallback.providerName) == ProviderTrustZone.LOCAL) {
"offline-profile-non-local-fallback-rejected"
}
}
// 4. Default provider — must be LOCAL if set
defaultProviderName?.let { providerName ->
require(profile.providerZones.getValue(providerName) == ProviderTrustZone.LOCAL) {
"offline-profile-non-local-default-provider-rejected"
}
}
The registered-provider loop runs first. When a cloud provider is registered as both a provider and a fallback or default provider, the offline-profile-non-local-provider-rejected error fires before the fallback or default checks can be reached.
Zero-Egress Verification Harness
The zero-egress verification harness tests that an offline deployment cannot call external services:
// The harness:
// 1. Builds the sovereign runtime in OFFLINE mode
// 2. Invokes the loopback provider (a provider that returns fixed responses)
// 3. Probes external TCP connectivity (e.g., 1.1.1.1:443)
// 4. Probes external DNS resolution (e.g., example.com)
// 5. Records all results in ZeroEgressEvidenceV1
Loopback Provider
A simple loopback provider for CI verification:
class LoopbackProvider : ModelProvider {
override suspend fun complete(request: ModelRequest): ModelResponse =
ModelResponse(content = "Loopback response for ${request.model}")
override fun providerId(): String = "loopback"
}
Verification Report
Results are captured in ZeroEgressEvidenceV1:
val zeroEgress = ZeroEgressEvidenceV1(
deploymentMode = "OFFLINE",
runtimeBuildSucceeded = true,
loopbackProviderInvocationSucceeded = true,
loopbackProviderInvocationCount = 3,
externalTcpProbeBlocked = true,
externalDnsProbeBlocked = true,
)
Attested zero-egress runs
The release repository ships a runnable harness for exactly this (examples/sovereign-offline-verification, an OFFLINE SovereignTramai plus a loopback provider). In one invocation it builds the runtime, invokes the loopback model, probes outbound TCP (to a public IP on 443) and DNS (a public name), validates the audit chain, writes the zero-egress report, and writes the deployment evidence pack.
Two optional links turn that run into supply-chain evidence:
--sbom-path=with--sbom-digest-path=adds aSupplyChainEvidenceV1subsection (SBOM format, spec version, file name,sha256:<hex>digest). The digest file must match^sha256:[a-fA-F0-9]{64}$.--release-bundle-manifest=loads a release-bundle manifest and records the Maven coordinates and digests of every artifact.
When the run happens in CI, the GITHUB_WORKFLOW, GITHUB_RUN_ID, GITHUB_REPOSITORY, and GITHUB_SHA environment variables add an AttestationEvidenceV1 subsection with the workflow identity and the digests of the attested subjects, and the pack is rewritten with it included. Nothing here claims network isolation on its own — the probes prove what the process could reach, and infrastructure controls still have to prevent what the runtime does not attempt.
Docker-Based CI Harness
For CI pipelines, test offline deployment with Docker --network=none:
# .github/workflows/offline-verification.yml
steps:
- name: Build verification image
run: |
docker build -t sovereign-offline-test -f Dockerfile.offline-test .
- name: Run offline verification
run: |
docker run --network=none \
-v $PWD/build/evidence:/evidence \
sovereign-offline-test
- name: Validate evidence report
run: |
python validate_offline_report.py build/evidence/zero-egress.json
Python Report Validation
import json
with open("build/evidence/zero-egress.json") as f:
report = json.load(f)
assert report["deploymentMode"] == "OFFLINE"
assert report["runtimeBuildSucceeded"] == True
assert report["loopbackProviderInvocationSucceeded"] == True
assert report["externalTcpProbeBlocked"] == True
assert report["externalDnsProbeBlocked"] == True
print("Zero-egress verification passed")
Evidence Pack Integration
Zero-egress probe results can be embedded in the deployment evidence pack:
val tramai = SovereignTramai.builder()
.profile(profile)
.modelRegistry(registry)
.auditStore(auditStore)
.provider(loopbackProvider, name = "loopback", default = true)
.model("test-model", "loopback")
.modelArtifactVerifier(verifier)
.modelArtifactVerificationSettings(ModelArtifactVerificationSettings(enabled = true))
.build()
val pack = tramai.evidencePack(
zeroEgress = zeroEgressEvidence,
)
SovereignEvidencePackWriter.write(pack, Paths.get("evidence/sovereign-evidence.json"))
Standalone Test Reference (from tests)
The offline deployment test suite (SovereignOfflineDeploymentTest) proves these behaviors:
| Test | Expected Behavior |
|---|---|
OFFLINE with only LOCAL provider builds | SovereignTramai is created successfully |
STANDARD with LOCAL provider builds | Works in both modes |
STANDARD with EU_CLOUD provider builds | Cloud providers work in STANDARD mode |
STANDARD with GLOBAL_CLOUD provider builds | Cloud providers work in STANDARD mode |
OFFLINE with EU_CLOUD registered provider rejects | Throws IllegalArgumentException("offline-profile-non-local-provider-rejected") |
OFFLINE with GLOBAL_CLOUD registered provider rejects | Same error |
OFFLINE with local primary route and local fallback builds | Valid offline config with fallback |
OFFLINE with cloud fallback route rejects | Registered-provider check catches the cloud fallback |
OFFLINE with cloud default provider rejects | Same — registered-provider check fires first |
offline validation occurs before artifact registry lookup | A throwing registry is never called — offline check rejects first |
offline valid local route with verification enabled builds and stores receipt | verificationReceipts() is non-empty |
offline valid local route with verification disabled builds and empty receipts | verificationReceipts() is empty |
Artifact Verification in Offline Mode
When ModelArtifactVerificationSettings.enabled = true and requireDigestForLocalModels = true, the builder requires every local-model RegisteredModel to have a non-null artifactDigest. If a local model is missing its digest, the builder throws:
IllegalStateException("artifact-digest-required-for-local-model")
Artifact verification targets include both primary and fallback routes for local providers:
private fun verifyLocalModelArtifacts(...) {
val verificationTargets = buildSet {
primaryModelRoutes.forEach { (modelName, providerName) -> add(providerName to modelName) }
fallbackRoutes.forEach { route -> add(route.providerName to route.fallbackModelName) }
}
// Only LOCAL-trust-zone targets are verified
// Non-local targets are skipped
}
Limitations
- No infrastructure enforcement — the enum validates build-time configuration, not network-level isolation. Additional controls (firewall,
NetworkPolicy, physical air-gap) are required for production - Loopback provider is test-only — the loopback/
FakeProvideris for CI verification, not production inference - No continuous monitoring — zero-egress probes are run once at build/CI time; runtime monitoring of network egress is outside scope
- All-local fallback — fallback routes in offline mode must also be
LOCAL; no external failover is possible - Docker
--network=nonelimitations — Docker-level network isolation may not prevent all egress paths (e.g., mounted Unix sockets, host-network containers) - Registered-provider check is greedy — the first validation loop catches all registered non-
LOCALproviders before fallback or default checks are reached, sooffline-profile-non-local-provider-rejectedis the error you will see for most cloud provider misconfigurations
Next Steps
- Sovereign Runtime Quickstart — dependencies,
tramai.profile, and startup validation - Evidence Packs — include zero-egress evidence in deployment attestation
- Artifact Verification — verify local model artifacts for offline deployments
- Sovereign Mode — understand the full sovereign runtime
