TramAI - governed AI workflows for Java and Kotlin

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:

  1. SovereignDeploymentMode.OFFLINE — an enum value that triggers strict local-only validation during builder assembly
  2. validateOfflineDeployment() — called during SovereignTramai.Builder.build() before any registry lookup or artifact verification
  3. Zero-egress verification — a harness that builds the runtime, invokes a loopback provider, and probes external TCP/DNS connectivity
  4. 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:

CheckConditionError Code
Registered providerMust be LOCAL trust zoneoffline-profile-non-local-provider-rejected
Primary route targetMust be LOCAL trust zoneoffline-profile-non-local-primary-route-rejected
Fallback route targetMust be LOCAL trust zoneoffline-profile-non-local-fallback-rejected
Default providerMust be LOCAL trust zoneoffline-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=none for 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 a SupplyChainEvidenceV1 subsection (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:

TestExpected Behavior
OFFLINE with only LOCAL provider buildsSovereignTramai is created successfully
STANDARD with LOCAL provider buildsWorks in both modes
STANDARD with EU_CLOUD provider buildsCloud providers work in STANDARD mode
STANDARD with GLOBAL_CLOUD provider buildsCloud providers work in STANDARD mode
OFFLINE with EU_CLOUD registered provider rejectsThrows IllegalArgumentException("offline-profile-non-local-provider-rejected")
OFFLINE with GLOBAL_CLOUD registered provider rejectsSame error
OFFLINE with local primary route and local fallback buildsValid offline config with fallback
OFFLINE with cloud fallback route rejectsRegistered-provider check catches the cloud fallback
OFFLINE with cloud default provider rejectsSame — registered-provider check fires first
offline validation occurs before artifact registry lookupA throwing registry is never called — offline check rejects first
offline valid local route with verification enabled builds and stores receiptverificationReceipts() is non-empty
offline valid local route with verification disabled builds and empty receiptsverificationReceipts() 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/FakeProvider is 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=none limitations — 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-LOCAL providers before fallback or default checks are reached, so offline-profile-non-local-provider-rejected is the error you will see for most cloud provider misconfigurations

Next Steps