Evidence Packs
Evidence packs capture the security posture of a sovereign TramAI deployment as a deterministic, safe-for-auditors JSON artifact. They contain no secrets, tokens, prompts, stack traces, or filesystem paths — every identifier is sanitized through EvidenceSafeString before being written.
Module: tramai-sovereign — entry points under dev.tramai.sovereign.evidence.*: the SovereignEvidencePackV1 DTO, SovereignEvidencePackWriter, and SovereignEvidencePackGenerator.
For a runnable path from dependencies to a written evidence pack, see the Sovereign Runtime Quickstart.
What
An evidence pack is a SovereignEvidencePackV1 DTO that summarizes:
- Deployment identity — deployment mode (
STANDARDorOFFLINE), allowed models (sorted), allowed providers (sorted), trust zone mappings - Artifact verification — settings snapshot, per-artifact receipts (manifest digest, artifact count, total size)
- Zero-egress evidence — optional subsection from offline deployment testing (loopback invocations, TCP/DNS probes)
- Audit chain — optional validation of the hash-chained audit event integrity
- Supply chain — optional SBOM linkage (CycloneDX format with SHA-256 digest)
- Release bundle — optional build artifact evidence with Maven coordinates and digests for every JAR
- CI/CD attestation — optional build provenance from a CI pipeline (GitHub Actions)
Deterministic JSON
The SovereignEvidencePackWriter produces JSON with:
- Stable field ordering matching the data class declaration order
- Full JSON control-character escaping (all chars < 0x20 as
\uXXXX) - No external JSON library dependency
- Parent directory auto-creation
Same input produces identical output every time — suitable for hashing, signing, or comparing in CI.
Safety Guarantees
All string values are validated through EvidenceSafeString.sanitize():
- Rejects identifiers containing path prefixes (
/tmp/,/home/,/Users/) - Rejects identifiers containing secrets-adjacent terms (
token,secret,password,prompt,rawRequest,rawResponse,stacktrace) - Rejects control characters
- Rejects Windows drive paths
SHA-256 digest values (sha256:<hex>) bypass string sanitization and are validated with strict regex instead.
Runtime Evidence Families
The deployment evidence pack is the posture snapshot. The decisions the runtime actually made are exported separately, into a runtime-evidence/ bundle, by the exporters in dev.tramai.security.evidence.*:
| Family | eventType | Exporter | File |
|---|---|---|---|
| Policy decisions | policy.decision | PolicyDecisionRuntimeEvidenceExporter | runtime-evidence/policy-decisions.jsonl |
| Approval decisions | approval.decision | approval decision exporter | runtime-evidence/approval-decisions.jsonl |
| Provider routing | provider.route | provider routing exporter | runtime-evidence/provider-routing.jsonl |
| Tool permissions | tool.permission | ToolPermissionRuntimeEvidenceExporter | runtime-evidence/tool-permissions.jsonl |
RuntimeEvidenceBundleWriter.write(bundleDirectory, records) writes the records transactionally, replacing any existing section, and the bundle root manifest.json records the digest of every file including the runtime-evidence ones. Every record follows the runtime-evidence.v1 schema; records are single-line JSON so the files stay streamable.
The tool.permission family is the 0.6.0 partition for governed tool use. Decisions taken at BEFORE_TOOL_EXPOSURE, BEFORE_TOOL_EXECUTION, and BEFORE_TOOL_RESULT_REINJECTION belong to it, and the policy.decision exporter deliberately excludes them — so a tool denial never appears twice, or only in the generic family. Only safe metadata keys (toolName, enforcementPoint, riskLevel, classification, classificationSource) are carried into tool.permission records, and a tool enforcement event missing toolName fails closed rather than falling back.
Approval decisions and their suspend/resume lifecycle are covered in Approval Workflows.
When to Use
Use evidence packs when:
- You need audit-grade documentation of your TramAI deployment's security posture
- You need CI attestation — generate and upload evidence packs as CI artifacts with provenance
- You need enterprise security review artifacts that auditors can inspect without access to the runtime
- You need air-gap validation evidence — prove that an offline deployment has zero egress
- You need a verifiable deployment snapshot — hash the pack and sign it
Do not use evidence packs for runtime monitoring or live debugging — they are static snapshots.
Quickstart
Basic Generation
// 1. Build your sovereign runtime
val tramai = 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()
// 2. Generate an evidence pack
val pack = tramai.evidencePack()
// 3. Write to file
SovereignEvidencePackWriter.write(pack, Paths.get("evidence/sovereign-evidence.json"))
With All Optional Sub-Sections
val pack = tramai.evidencePack(
zeroEgress = ZeroEgressEvidenceV1(
deploymentMode = "OFFLINE",
runtimeBuildSucceeded = true,
loopbackProviderInvocationSucceeded = true,
loopbackProviderInvocationCount = 3,
externalTcpProbeBlocked = true,
externalDnsProbeBlocked = true,
),
auditChain = AuditChainEvidenceV1(
isValid = true,
totalEvents = 42,
),
supplyChain = SupplyChainEvidenceV1(
schemaVersion = 1,
sbomFormat = "CycloneDX",
sbomSpecVersion = "1.6",
sbomFileName = "bom.json",
sbomSha256 = "sha256:abc123def456abc123def456abc123def456abc123def456abc123def456abcd",
generatedBy = "CycloneDX Gradle Plugin 3.2.4",
),
releaseBundle = ReleaseBundleEvidenceV1(
schemaVersion = 1,
buildTool = "Gradle",
javaVersion = "21",
gradleVersion = "8.12",
artifacts = listOf(
ReleaseArtifactEvidenceV1(
groupId = "dev.tramai",
artifactId = "tramai-core",
version = "0.6.0",
classifier = null,
extension = "jar",
fileName = "tramai-core-0.6.0.jar",
sha256 = "sha256:abc123def456abc123def456abc123def456abc123def456abc123def456abcd",
sizeBytes = 245760,
),
),
),
attestation = AttestationEvidenceV1(
schemaVersion = 1,
provider = "GitHub Artifact Attestations",
workflowName = "deploy",
workflowRunId = "1234567890",
repository = "my-org/my-repo",
commitSha = "abc123def456abc123def456abc123def456abc1",
attestedSubjects = listOf(
AttestedSubjectV1(
fileName = "sovereign-evidence.json",
sha256 = "sha256:abc123def456abc123def456abc123def456abc123def456abc123def456abcd",
attestationType = "build-provenance",
),
),
),
)
Evidence Pack Schema (V1)
SovereignEvidencePackV1
| Field | Type | Description |
|---|---|---|
schemaVersion | Int | Schema version (currently 1) |
deploymentMode | String | "STANDARD" or "OFFLINE" |
allowedModels | List<String> | Allowed model names (sorted) |
allowedProviders | List<String> | Allowed provider names (sorted) |
providerZones | Map<String, String> | Trust zone per provider |
artifactVerificationSettings | Map<String, Any?> | Verification settings snapshot (enabled, requireDigestForLocalModels) |
artifacts | List<ArtifactEvidenceV1> | Per-artifact verification receipts |
zeroEgress | ZeroEgressEvidenceV1? | Zero-egress probe results |
auditChain | AuditChainEvidenceV1? | Audit chain validation |
supplyChain | SupplyChainEvidenceV1? | SBOM linkage |
releaseBundle | ReleaseBundleEvidenceV1? | Build artifact evidence |
attestation | AttestationEvidenceV1? | CI/CD attestation |
generatedAt | String | ISO-8601 generation timestamp |
ArtifactEvidenceV1
| Field | Type | Description |
|---|---|---|
registryEntryId | String | Registry entry identifier |
manifestDigest | String | SHA-256 of the manifest |
modelName | String | Model name |
verifiedAt | String | ISO-8601 verification timestamp |
artifactCount | Int | Number of verified artifact files |
totalSizeBytes | Long | Total verified size |
ZeroEgressEvidenceV1
| Field | Type | Description |
|---|---|---|
deploymentMode | String | Deployment mode under test |
runtimeBuildSucceeded | Boolean | Runtime built successfully |
loopbackProviderInvocationSucceeded | Boolean | Loopback provider returned valid response |
loopbackProviderInvocationCount | Int | Number of loopback invocations |
externalTcpProbeBlocked | Boolean | External TCP connect to 1.1.1.1:443 blocked |
externalDnsProbeBlocked | Boolean | External DNS resolution of example.com blocked |
AuditChainEvidenceV1
| Field | Type | Description |
|---|---|---|
isValid | Boolean | Audit chain integrity validated |
totalEvents | Int | Total events in the chain |
SupplyChainEvidenceV1
| Field | Type | Description |
|---|---|---|
schemaVersion | Int | Schema version (must be 1) |
sbomFormat | String | SBOM format (e.g., "CycloneDX") |
sbomSpecVersion | String | Format spec version (e.g., "1.6") |
sbomFileName | String | SBOM file name (no path separators) |
sbomSha256 | String | sha256:<hex> digest — validated against ^sha256:[a-fA-F0-9]{64}$ |
generatedBy | String | SBOM generator identifier |
AttestationEvidenceV1
| Field | Type | Description |
|---|---|---|
schemaVersion | Int | Schema version (must be 1) |
provider | String | Attestation provider (e.g., "GitHub Artifact Attestations") |
workflowName | String | CI workflow name |
workflowRunId | String | Numeric workflow run ID — validated against ^[0-9]+$ |
repository | String | owner/repo format — validated against ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ |
commitSha | String | 40-character hex commit SHA — validated against ^[a-fA-F0-9]{40}$ |
attestedSubjects | List<AttestedSubjectV1> | Attested file digests (must not be empty) |
AttestedSubjectV1
| Field | Type | Description |
|---|---|---|
fileName | String | File name (no path separators) |
sha256 | String | sha256:<hex> digest |
attestationType | String | "build-provenance" or "sbom" only |
ReleaseBundleEvidenceV1
| Field | Type | Description |
|---|---|---|
schemaVersion | Int | Schema version (must be 1) |
buildTool | String | Build tool identifier (e.g., "Gradle") |
javaVersion | String | Java runtime version |
gradleVersion | String | Gradle version |
artifacts | List<ReleaseArtifactEvidenceV1> | Release artifacts (must not be empty) |
ReleaseArtifactEvidenceV1
| Field | Type | Description |
|---|---|---|
groupId | String | Maven group ID (e.g., "dev.tramai") |
artifactId | String | Maven artifact ID (e.g., "tramai-core") |
version | String | Artifact version (e.g., "0.6.0") |
classifier | String? | Optional Maven classifier |
extension | String | File extension (e.g., "jar") |
fileName | String | File name (no path separators) |
sha256 | String | sha256:<hex> digest — validated against ^sha256:[a-fA-F0-9]{64}$ |
sizeBytes | Long | File size in bytes (must be >= 0) |
Writer
The SovereignEvidencePackWriter produces deterministic JSON output:
SovereignEvidencePackWriter.write(pack, Paths.get("evidence/pack.json"))
Output format:
{
"schemaVersion": 1,
"deploymentMode": "OFFLINE",
"allowedModels": [
"llama3.2"
],
"allowedProviders": [
"ollama"
],
"providerZones": {
"ollama": "LOCAL"
},
"artifactVerificationSettings": {
"enabled": true,
"requireDigestForLocalModels": true
},
"artifacts": [
{
"registryEntryId": "ollama-llama3.2",
"manifestDigest": "sha256:abc123def456abc123def456abc123def456abc123def456abc123def456abcd",
"modelName": "llama3.2",
"verifiedAt": "2025-06-14T12:00:00Z",
"artifactCount": 3,
"totalSizeBytes": 1234567890
}
],
"zeroEgress": null,
"auditChain": null,
"supplyChain": null,
"releaseBundle": null,
"attestation": null,
"generatedAt": "2025-06-14T12:00:00Z"
}
Generator Validation
The SovereignEvidencePackGenerator.generate() method performs validation and sanitization:
- EvidenceSafeString sanitization — all model names, provider names, zone keys, registry entry IDs, and workflow names are sanitized (rejects paths, secrets-adjacent terms, control characters)
- Supply chain validation —
sbomSha256must match^sha256:[a-fA-F0-9]{64}$,schemaVersionmust be 1,sbomFileNamemust not contain path separators - Attestation validation —
workflowRunIdmust be numeric,repositorymust matchowner/repo,commitShamust be 40 hex chars,attestedSubjectsmust not be empty,attestationTypemust be"build-provenance"or"sbom" - Release bundle validation —
sha256for each artifact must match^sha256:[a-fA-F0-9]{64}$,sizeBytesmust be >= 0, artifacts list must not be empty - Deterministic sorting —
allowedModelsandallowedProvidersare sorted;providerZoneskeys are sorted alphabetically in the writer
All validation failures use fixed safe-code error messages:
"evidence-unsafe-identifier""evidence-unsafe-digest-format""evidence-unsafe-control-character""evidence-unsupported-supply-chain-schema-version""evidence-unsupported-attestation-schema-version""evidence-unsafe-attestation-workflow-run-id""evidence-unsafe-attestation-commit-sha""evidence-unsafe-attestation-repository""evidence-unsafe-attestation-subjects""evidence-unsupported-attestation-type""evidence-unsupported-release-bundle-schema-version""evidence-unsafe-release-artifacts""evidence-unsafe-artifact-size"
CI Workflow Integration
Evidence packs are designed for CI artifact upload with attestation:
# .github/workflows/deploy.yml
steps:
- name: Generate evidence pack
run: ./gradlew sovereignEvidencePack
- name: Sign the evidence pack
run: |
openssl dgst -sha256 -sign private-key.pem \
-out build/evidence/sovereign-evidence.json.sig \
build/evidence/sovereign-evidence.json
- name: Upload evidence
uses: actions/upload-artifact@v4
with:
name: sovereign-evidence
path: build/evidence/
Extension Pattern
To add a new evidence sub-section:
- Define the DTO — a new data class like
ZeroEgressEvidenceV1with documented properties and@paramKDoc - Add to SovereignEvidencePackV1 — add an optional field (
val newSection: NewSectionV1? = null) - Update the writer — add a
serializeNewSection()method and wire it intoserialize()matching the data class declaration order - Update the generator — add sanitization and validation in
SovereignEvidencePackGenerator.generate()
Limitations
- Snapshot-only — evidence packs capture a point-in-time view; they are not updated automatically
- No secrets — by design, evidence packs cannot contain secrets, tokens, or credentials (
EvidenceSafeStringactively rejects them) - No runtime data — no prompts, responses, or model outputs are included
- Manual sub-sections —
zeroEgress,auditChain,supplyChain,releaseBundle, andattestationmust be constructed manually and passed toevidencePack() - Schema version 1 — the current schema is v1; breaking changes will increment
schemaVersion
Next Steps
- Sovereign Runtime Quickstart — generate a pack from a running sovereign runtime
- Offline Deployment — generate zero-egress evidence for air-gapped deployments
- Artifact Verification — verification receipts feed into evidence packs
- Sovereign Mode — understanding the sovereign runtime
