Artifact Verification
The FileSystemModelArtifactVerifier is tested but the safe-code reason codes and manifest format are still evolving. Verification settings may gain additional options in minor releases.
Artifact verification ensures that local model files loaded at build time match their registered digests. The sovereign builder verifies every local-model artifact before building the runtime, producing immutable verification receipts that can be included in evidence packs.
Package: dev.tramai.core.model (SPIs, manifests, receipts), dev.tramai.security.verification (FileSystemModelArtifactVerifier)
What
The verification system operates at four levels:
ModelArtifactVerifierSPI —suspend fun verify(registeredModel: RegisteredModel): VerifiedLocalModelArtifact?FileSystemModelArtifactVerifier— verifies file-system artifacts against an immutableLocalModelArtifactManifestV1ModelArtifactVerificationSettings— controls whether verification is enabled and whether local models require a pinned digestVerifiedLocalModelArtifact— immutable receipt produced by successful verification
Verification Flow
SovereignTramai.Builder.build()
→ validateOfflineDeployment()
→ verifyLocalModelArtifacts()
→ For each (providerName, modelName) pair with LOCAL trust zone:
→ modelRegistry.findApprovedModel(providerName, modelName)
→ verifier.verify(registeredModel)
→ manifest lookup by registryEntryId (null if not found)
→ identity match check (registryEntryId, providerId, modelName, revision)
→ aggregate digest check (if artifactDigest is pinned)
→ per-file streaming SHA-256 verification
→ symlink policy enforcement
→ collect VerifiedLocalModelArtifact receipt
→ store immutable receipt list
When to Use
Use artifact verification when:
- You need build-time integrity guarantees for local model artifacts
- You need to detect tampering — modified model files, substituted artifacts, symlink attacks
- You need a verification receipt for audit or evidence pack generation
- You run in offline/air-gapped deployments where artifact integrity cannot be verified at runtime
Do not use artifact verification for cloud-hosted model providers — verification is only applied to LOCAL trust zone providers (trust zone check happens before registry lookup).
Quickstart
Configuration
// 1. Define your artifact manifests
val manifests = mapOf(
"ollama-llama3.2" to LocalModelArtifactManifestV1(
schemaVersion = 1,
registryEntryId = "ollama-llama3.2",
providerId = "ollama",
modelName = "llama3.2",
revision = "1.0",
artifacts = listOf(
LocalModelArtifactFileV1(
relativePath = "models/llama3.2/model.bin",
sizeBytes = 4_123_456_789,
digest = ModelArtifactDigest.of("sha256:abc123def456abc123def456abc123def456abc123def456abc123def456abcd"),
),
),
),
)
// 2. Create the verifier
val verifier = FileSystemModelArtifactVerifier(
allowedRootDirectories = setOf(Paths.get("/opt/models")),
manifests = manifests,
)
// 3. Build with verification enabled
val tramai = SovereignTramai.builder()
.profile(localProfile)
.modelRegistry(registry)
.auditStore(auditStore)
.provider(ollamaProvider, name = "ollama", default = true)
.model("llama3.2", "ollama")
.modelArtifactVerifier(verifier)
.modelArtifactVerificationSettings(
ModelArtifactVerificationSettings(
enabled = true,
requireDigestForLocalModels = true,
),
)
.build()
// 4. Access receipts
val receipts = tramai.verificationReceipts()
receipts.forEach { receipt ->
println("Verified ${receipt.modelName}:")
println(" Manifest digest: ${receipt.manifestDigest}")
println(" Artifacts: ${receipt.artifactCount}")
println(" Total size: ${receipt.totalSizeBytes}")
println(" Verified at: ${receipt.verifiedAt}")
}
FileSystemModelArtifactVerifier
Constructor
class FileSystemModelArtifactVerifier(
allowedRootDirectories: Set<Path>,
manifests: Map<String, LocalModelArtifactManifestV1>,
private val clock: Clock = Clock.systemUTC(),
) : ModelArtifactVerifier
| Parameter | Description |
|---|---|
allowedRootDirectories | Directories where artifact files are allowed (path traversal protection) |
manifests | Map of registryEntryId to immutable manifest |
clock | Clock for receipt timestamps (default: system UTC) |
Verification Checks
For each registered model, the verifier:
- Manifest lookup — looks up the manifest by
registryEntryId; returnsnullif not found (safe code:artifact-manifest-not-found) - Identity check — verifies
registryEntryId,providerId,modelName, andrevisionmatch between manifest and registry (safe code:artifact-manifest-identity-drift) - Canonical digest computation — computes SHA-256 of the manifest's canonical serialization (key=value lines, artifacts sorted by
relativePath) - Aggregate digest check — if
registeredModel.artifactDigestis pinned, verifies the canonical manifest digest matches (safe code:artifact-aggregate-digest-mismatch) - Per-file verification — for each artifact file in the manifest:
- Path resolution — resolves relative path against each allowed root
- Path traversal check — ensures resolved normalized path stays within the root
- File existence — returns null if not found in any root (safe code:
artifact-file-not-found) - Strict symlink policy — calls
Files.isSymbolicLink()on the resolved path; also checks viatoRealPath()comparison and rejects any symlink in the resolution chain (safe code:artifact-file-symlink-rejected) - Regular file check — rejects directories (safe code:
artifact-directory-substituted-for-file) and non-regular files (safe code:artifact-not-a-regular-file) - Size check before hash — verifies file size matches manifest via
Files.size()(safe code:artifact-file-size-mismatch) - Streaming SHA-256 hash — hashes file with 64KB buffer via
MessageDigest.getInstance("SHA-256")(safe code:artifact-file-digest-mismatch) - Size check after hash — verifies file size again to detect concurrent modification (safe code:
artifact-file-size-mismatch) - I/O errors produce
artifact-file-access-failed
- Total size check — verifies no arithmetic overflow in total size via
Math.addExact(safe code:artifact-total-size-overflow)
Safe-Code Reasons
All verification failures use deterministic safe-code strings:
| Safe Code | When |
|---|---|
artifact-manifest-not-found | No manifest for this registry entry |
artifact-manifest-identity-drift | Manifest identity fields don't match registry |
artifact-aggregate-digest-mismatch | Canonical manifest digest doesn't match pinned value |
artifact-file-not-found | Artifact file not found in any allowed root directory |
artifact-file-symlink-rejected | Symlink detected in the resolution chain |
artifact-file-size-mismatch | File size doesn't match manifest size |
artifact-file-digest-mismatch | SHA-256 digest doesn't match manifest digest |
artifact-file-access-failed | I/O error reading the file |
artifact-traversal-rejected | Path traversal detected |
artifact-directory-substituted-for-file | Expected a regular file, found a directory |
artifact-not-a-regular-file | Not a regular file (special file, device, etc.) |
artifact-total-size-overflow | Total artifact size exceeds Long.MAX_VALUE |
LocalModelArtifactManifestV1
Immutable snapshot of expected artifact state at build time:
class LocalModelArtifactManifestV1(
val schemaVersion: Int, // Must be 1
val registryEntryId: String,
val providerId: String,
val modelName: String,
val revision: String,
artifacts: List<LocalModelArtifactFileV1>,
) {
val artifacts: List<LocalModelArtifactFileV1> // Unmodifiable
}
Init validation:
schemaVersionmust be 1- All string fields validated: no blanks, no control characters, max 256 chars
- At least one artifact file required
- No duplicate artifact paths (case-sensitive)
- Paths validated for relative format, no traversal, no self-references, no double slashes, no Windows drive prefixes, no UNC paths
LocalModelArtifactFileV1
data class LocalModelArtifactFileV1(
val relativePath: String,
val sizeBytes: Long,
val digest: ModelArtifactDigest,
)
Init validation on relativePath:
- Must not be blank
- Must be relative (no
/,\\,C:\prefixes) - Must not traverse upward (
..) - Must not contain self-reference segments (
./) - Must not contain double slashes
- Must not contain control characters
- Must use consistent forward-slash separators
ModelArtifactDigest
@JvmInline
value class ModelArtifactDigest private constructor(val value: String) {
companion object {
fun of(raw: String): ModelArtifactDigest // validated: "sha256:<64 lowercase hex>"
}
}
Canonicial Bytes
The manifest's canonicalBytes() method produces a deterministic byte representation used for aggregate digest verification:
schemaVersion=1
registryEntryId=ollama-llama3.2
providerId=ollama
modelName=llama3.2
revision=1.0
artifact_count=1
relativePath=models/llama3.2/model.bin
sizeBytes=4123456789
digest=sha256:abc123...
VerifiedLocalModelArtifact Receipt
data class VerifiedLocalModelArtifact(
val registryEntryId: String,
val manifestDigest: ModelArtifactDigest,
val modelName: String,
val verifiedAt: Instant,
val artifactCount: Int,
val totalSizeBytes: Long,
)
Receipts are collected during SovereignTramai.Builder.build() and stored in an immutable list accessible via SovereignTramai.verificationReceipts().
ModelArtifactVerificationSettings
data class ModelArtifactVerificationSettings(
val enabled: Boolean = false,
val requireDigestForLocalModels: Boolean = false,
)
| Field | Default | Description |
|---|---|---|
enabled | false | When false, no verification is performed and receipts are empty |
requireDigestForLocalModels | false | When true, every local model must have an artifactDigest pinned in the registry (IllegalStateException("artifact-digest-required-for-local-model") if missing) |
Sovereign Builder Integration
The sovereign builder automatically during build():
- Collects all
(providerName, modelName)pairs from primary and fallback routes - Filters to
LOCALtrust zone providers only (trust zone check happens before registry lookup) - Looks up each approved model in the registry via
modelRegistry.findApprovedModel() - If
requireDigestForLocalModelsis true andartifactDigestis null → throwsIllegalStateException("artifact-digest-required-for-local-model") - Calls
verifier.verify(registeredModel)for each - Collects non-null receipts into an immutable list
// Internal: buildSet of verification targets
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
NoOpModelArtifactVerifier
For testing or when no verification is needed:
object NoOpModelArtifactVerifier : ModelArtifactVerifier {
override suspend fun verify(registeredModel: RegisteredModel): VerifiedLocalModelArtifact? = null
}
RegisteredModel and Artifact Digest
data class RegisteredModel(
val registryEntryId: String,
val providerId: String,
val modelName: String,
val revision: String,
val artifactDigest: ModelArtifactDigest? = null, // null = no aggregate digest check
val enabled: Boolean = true,
)
When artifactDigest is null and requireDigestForLocalModels is true, the builder throws before calling the verifier.
Limitations
- File-system only — verification checks on-disk files against manifests; no support for remote artifact verification
- Manifests must be pre-built — manifests are constructed in code and passed to the verifier; no auto-generation
- Size check race window — size is checked before and after hashing, but concurrent modification during hashing could theoretically bypass the check (acceptable for build-time verification)
- No automatic re-verification — once verified at build time, the receipts are immutable; no runtime re-verification
- Local-only — verification only applies to
LOCALtrust zone providers; cloud models are not verified
Minimum Configuration
val verifier = FileSystemModelArtifactVerifier(
allowedRootDirectories = setOf(Paths.get("/opt/models")),
manifests = manifestMap,
)
SovereignTramai.builder()
.profile(profile)
.modelRegistry(registry)
.auditStore(auditStore)
.provider(ollamaProvider, name = "ollama", default = true)
.model("llama3.2", "ollama")
.modelArtifactVerifier(verifier)
.modelArtifactVerificationSettings(
ModelArtifactVerificationSettings(enabled = true)
)
.build()
For development without strict enforcement, set requireDigestForLocalModels = false to allow local models without a pinned artifactDigest.
Next Steps
- Evidence Packs — include verification receipts in deployment evidence
- Offline Deployment — combine with offline mode for air-gapped deployments
- Sovereign Mode — understand the full sovereign runtime
