Production Hardening & Security
TramAI is built for production environments where cost control, data privacy, and reliability matter. This guide covers the hardening mechanisms documented for the 0.6.0 line.
What This Covers
- Runtime selection:
tramai.profileand the sovereign profile - PII masking and data redaction with OperationInterceptor
- DLP interceptors for model output and tool result scanning
- Secret management with SecretValueResolver
- Token budgets for cost control
- Circuit breakers, retry pacing, and fallback routing
- The 0.6.0 hardening guarantees: bounded reads, SSRF admission, sanitized storage failures, static-safety guards, cancellation propagation
- Sovereign Mode for hardened, auditable deployments
Runtime Selection
One starter, two profiles. tramai-spring-boot-starter aggregates the standard and sovereign Spring integrations, and tramai.profile chooses which one runs:
tramai:
profile: standard # default; omit the property for the same result
tramai:
profile: sovereign
sovereign:
allowed-models: [local-invoice-model]
allowed-providers: [ollama]
provider-zones:
ollama: LOCAL
models:
local-invoice-model: ollama
Unsupported values fail at startup rather than falling back: Unsupported tramai.profile '<value>'. Supported values: standard, sovereign. The former standalone sovereign starter coordinate (tramai-spring-boot-starter-sovereign) is gone — there is no second dependency to add, and tramai.sovereign.enabled: false is rejected because the profile is the sole selector. See the Sovereign Runtime Quickstart.
0.6.0 Hardening Guarantees
These are enforced properties of the runtime, not opt-in features. They are the reason several failure modes now look different than they did in 0.4.x.
Bounded reads
Provider response bodies and downloaded payloads are read through bounded helpers (dev.tramai.core.provider.transport.readBoundedBodyBytes and friends) that fail loudly when the source exceeds the limit instead of silently truncating. The stream is always closed, so a hostile or broken endpoint cannot exhaust memory or leak a connection.
SSRF admission for image retrieval
Multimodal image retrieval (dev.tramai.core.util.ImageDownloader, backed by OutboundAddressSecurity) admits only what it can justify:
- only
httpandhttpsschemes —file:,jar:,data:, and friends are rejected - the host is canonicalized (IDN to ASCII, alternative IPv4 notations parsed) and resolved before connecting
- loopback, link-local, RFC 1918 private, carrier-grade NAT, unique-local IPv6, multicast, and any-local addresses are rejected, as is
localhost - URLs carrying embedded user info (
http://user:pass@host/) are rejected - every redirect target is re-validated, with a redirect cap
- the payload is capped at 20 MB per image, and the connection is disconnected on every path
Treat this as the reference implementation for any outbound call you add around TramAI: validate, then connect, then bound.
Sanitized storage failures
JDBC stores wrap database operations so raw driver diagnostics do not reach your application. A java.sql.SQLException becomes IllegalStateException with redacted details; coroutine CancellationException and TramAI domain exceptions (TramaiException, approval-store exceptions, IllegalArgumentException) propagate unwrapped.
Migration note: stop catching java.sql.SQLException around store calls. Catch IllegalStateException or a general RuntimeException instead — the underlying SQL text, table names, and connection details are no longer in the message.
Static safety guards
The build fails closed on four classes of production-code defect: raw coroutine-scope creation, unbounded response-body reads, logging of sensitive payloads, and System.out / System.err usage. If you are building an extension inside the TramAI repository, verifyStaticSafetyGuards is the gate you will hit; in your own application, the same rules are worth applying directly.
Cancellation propagation
CancellationException is preserved and propagated on every path — provider execution, tool coordination, workflow steps, storage. If you write catch (e: Exception) around TramAI calls, rethrow cancellation first:
import dev.tramai.core.coroutines.rethrowIfCancellation
try {
service.analyse(input)
} catch (e: Exception) {
e.rethrowIfCancellation() // never swallow coroutine cancellation
// handle the real failure
}
A cancelled call must not be converted into a retry, a fallback, a circuit-breaker count, or an approval failure.
Internal and preview boundaries
Symbols annotated @ExperimentalTramaiInternalApi or @ExperimentalProviderTransportApi are internal cross-module contracts, excluded from the stable compatibility guarantee. Reference them only if you are prepared to track them.
The same caution applies to policy presets: PolicyConfiguration.secure() is the deny-by-default baseline and the correct production posture. PolicyConfiguration.preview() is a 0.4.x migration/testing preset — it uses wildcard allowlists — and must not be shipped to production.
PII Masking with OperationInterceptor
When to use: You need to redact or transform sensitive data in request messages before they leave the JVM.
Minimum setup: Implement OperationInterceptor and register it on your TramAI builder or as a Spring bean.
The OperationInterceptor interface has two hooks:
interceptRequest(...)— inspect and modify messages before provider transportinterceptResponse(...)— inspect and modify provider responses before the engine processes them
Standalone
val tramai = Tramai {
provider(OpenAiProvider(System.getenv("OPENAI_API_KEY")), name = "openai", default = true)
model("gpt-4o", "openai")
interceptor(object : OperationInterceptor {
override fun interceptRequest(
context: OperationCallContext,
messages: List<Message>,
): List<Message> = messages.map { message ->
message.copy(content = redactPii(message.content))
}
})
}
Spring Boot
Spring auto-configuration composes ordered OperationInterceptor beans into the generated TramAI instance.
@Configuration
class TramaiSecurityConfiguration {
@Bean
fun piiMaskingInterceptor(): OperationInterceptor = object : OperationInterceptor {
override fun interceptRequest(
context: OperationCallContext,
messages: List<Message>,
): List<Message> = messages.map { message ->
message.copy(content = maskSensitiveData(message.content))
}
}
}
Current limitations:
- interceptors are engine-level request/response hooks
- they are opt-in — no interceptor is applied by default
- Spring auto-configuration composes registered interceptor beans in order via
CompositeOperationInterceptor
DLP (Data Loss Prevention)
When to use: You need to scan model outputs and tool results for sensitive data before they reach the caller, structured parser, or cache.
Minimum setup: Register a DlpInterceptor on the TramAI builder or as a Spring bean.
The DLP system inspects model outputs and tool results after the provider returns but before the result reaches the caller or structured parser. The SPI is a single-function interface:
fun interface DlpInterceptor {
fun inspect(context: DlpContext, text: String): DlpResult
}
Misconfiguration example — two DlpInterceptor beans will cause Spring auto-configuration to throw at startup. Define exactly one.
For complete DLP documentation including RuleBasedDlpInterceptor, DlpRedactionAuditEmitter, rule configuration, and cache bypass — see the dedicated DLP guide.
Secret Management
When to use: You need to resolve provider credentials without hard-coding them in application code.
Minimum setup: Use the built-in env: and file: resolvers, or implement SecretValueResolver for cloud secret stores.
Built-in Resolvers
env:NAME— resolves from environment variablesfile:/path/to/secret.txt— resolves from a file on disk
Standalone
val secretResolver = CompositeSecretValueResolver(
listOf(
SecretValueResolver { secretRef ->
if (!secretRef.startsWith("vault:")) null
else vaultClient.read(secretRef.removePrefix("vault:"))
},
EnvironmentSecretValueResolver,
FileSecretValueResolver,
),
)
val tramai = Tramai {
provider(
OpenAiProvider(
apiKey = secretResolver.resolve("vault:providers/openai/api-key")
?: error("Missing OpenAI API key"),
),
name = "openai",
default = true,
)
model("gpt-4o", "openai")
}
Spring Boot
Spring Boot auto-configuration composes SecretValueResolver beans. Reference secrets with *-secret-ref properties:
tramai:
providers:
openai:
api-key-secret-ref: vault:providers/openai/api-key
@Bean
fun vaultSecretValueResolver(): SecretValueResolver = SecretValueResolver { secretRef ->
if (!secretRef.startsWith("vault:")) null
else vaultClient.read(secretRef.removePrefix("vault:"))
}
Current limitations:
- the built-in standalone resolvers cover
env:andfile: - cloud secret stores need a resolver implementation of your own, or the Spring starters below
- in Spring Boot,
tramai-spring-secrets-file,tramai-spring-secrets-vault, andtramai-spring-secrets-awsship ready-made resolvers; each is a separate opt-in dependency
Token Budgets
When to use: You need to cap token consumption per attempt or per logical operation to control cost.
Minimum setup: Configure TokenBudgetSettings on the engine.
Available Controls
hardMaxTokensPerAttempt— hard cap per single provider callhardMaxTokensPerOperation— hard cap across the entire logical operation (including retries and tool loops)softMaxTokensPerOperation— emits a warning event when crossed, does not fail
Standalone
val tramai = Tramai {
provider(OpenAiProvider(System.getenv("OPENAI_API_KEY")), name = "openai", default = true)
model("gpt-4o", "openai")
tokenBudget(
TokenBudgetSettings(
hardMaxTokensPerAttempt = 4_000,
hardMaxTokensPerOperation = 20_000,
softMaxTokensPerOperation = 10_000,
),
)
}
Spring Boot
tramai:
cost:
token-budget:
hard-max-tokens-per-attempt: 4000
hard-max-tokens-per-operation: 20000
soft-max-tokens-per-operation: 10000
Practical notes:
- budgets apply across retries
- structured-output retries count toward the operation total
- tool-call loops count toward the operation total
- soft limit crossing emits an engine event instead of failing the call
Resilience Controls
When to use: You need fallback models, circuit breakers, and retry pacing for reliable provider access.
Fallback Routing
Fallback routes are explicit ordered routes for a requested model.
Circuit Breaking
Prevents repeated calls into an unhealthy provider after a configurable failure threshold.
Retry Pacing
Exponential backoff with jitter, honoring provider Retry-After hints.
Standalone
val tramai = Tramai {
provider(OpenAiProvider(System.getenv("OPENAI_API_KEY")), name = "openai", default = true)
provider(AnthropicProvider(System.getenv("ANTHROPIC_API_KEY")), name = "anthropic")
model("gpt-4o", "openai")
model("claude-sonnet-4-20250514", "anthropic")
fallbackModel("gpt-4o", "claude-sonnet-4-20250514", "anthropic")
circuitBreaker(
CircuitBreakerSettings(
enabled = true,
failureThreshold = 3,
openDurationMillis = 30_000,
),
)
retryPolicy(
RetryPolicySettings(
maxRetryAfterMillis = 20_000,
jitterRatio = 0.1,
),
)
}
Spring Boot
tramai:
models:
gpt-4o: openai
claude-sonnet-4-20250514: anthropic
fallbacks:
gpt-4o:
- provider: anthropic
model: claude-sonnet-4-20250514
resilience:
circuit-breaker:
enabled: true
failure-threshold: 3
open-duration-millis: 30000
retry:
max-retry-after-millis: 20000
jitter-ratio: 0.1
Current limitations:
- fallback routing is explicit, not heuristic
- streaming failover is only allowed before the first emitted token
- once a stream has emitted user-visible output, TramAI returns a terminal error rather than stitching providers together
Sovereign Mode
When to use: You need a hardened, auditable deployment profile with deny-by-default policy, approved-model routing, classification-aware provider trust zones, and hash-chained audit.
In 0.6.0 Sovereign Mode is a profile of the same starter — tramai.profile: sovereign plus the tramai.sovereign.* allowlist block — and the embedded equivalent is the SovereignTramai builder. Either path enforces deny-by-default policy, fail-fast validation, and append-only hash-linked audit emission.
For the dependency list, the full property reference, and the startup error codes, see the Sovereign Runtime Quickstart; for the builder API and trust zones, see the Sovereign Mode guide.
Next Steps
- Sovereign Runtime Quickstart — select the sovereign profile
- Troubleshooting — the 0.6.0 failure signatures and recovery paths
- DLP Guide — deep dive into Data Loss Prevention interceptors
- Platform Operations — API keys, multi-tenancy, plugins
- Observability — monitor production behavior
