Migrating to TramAI 0.6.0
This guide covers upgrading an existing application from TramAI 0.5.0 to 0.6.0. Five changes can require an edit; the rest of the release is additive or internal.
Start by moving to the new BOM. If you already use tramai-bom, the version bump is the whole change:
dependencies {
implementation(platform("dev.tramai:tramai-bom:0.6.0"))
}
<dependencyManagement>
<dependencies>
<dependency>
<groupId>dev.tramai</groupId>
<artifactId>tramai-bom</artifactId>
<version>0.6.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
| Change | Action required |
|---|---|
| Unified Spring Boot starter | Replace the old sovereign starter coordinate; no code change |
| Profile-based runtime selection | Move sovereign allowlists under tramai.sovereign.*; drop the old enable flag |
| JDBC store exceptions | Change catch (SQLException) to catch (IllegalStateException) or catch (RuntimeException) |
| Multimodal image retrieval | Review fetched URLs against the outbound address rules |
| Internal API annotations | Only relevant if you reference cross-module @Experimental* declarations |
| Custom extensions | Enrol the subproject in the module catalogue and pass the relevant TCK |
1. One Spring Boot Starter Coordinate
In 0.5.0 the sovereign runtime had its own starter artifact. In 0.6.0 there is a single canonical starter that composes both runtime integrations, and the old standalone sovereign starter is gone — the build fails on the old coordinate, it does not silently fall back.
// build.gradle.kts
dependencies {
implementation(platform("dev.tramai:tramai-bom:0.6.0"))
implementation("dev.tramai:tramai-spring-boot-starter")
implementation("dev.tramai:tramai-spring-provider-openai") // or -anthropic / -ollama
}
tramai-spring-boot-starter brings in tramai-spring-core (standard runtime) and tramai-spring-sovereign (sovereign runtime). Add exactly one tramai-spring-provider-* adapter, and optionally a tramai-spring-secrets-* adapter if you use vault: or aws-secretsmanager: secret references.
@AiService interfaces become Spring beans with no manual wiring. @EnableTramai is not required — it selects a Spring model, not a TramAI runtime profile.
2. Select the Runtime With tramai.profile
Runtime selection is explicit and profile-only. tramai.profile accepts exactly two values, standard and sovereign (case-insensitive); an unsupported value fails at startup naming the supported set. Omitting the property selects standard.
# Standard cloud-connected runtime (default)
tramai:
profile: standard
For sovereign, the allowlists move under tramai.sovereign.*. This is a complete working configuration:
tramai:
profile: sovereign
sovereign:
allowed-models:
- local-invoice-model
allowed-providers:
- deterministic-local-provider
allowed-tools:
- schedule-payment
allowed-permissions:
- payment.schedule
provider-zones:
deterministic-local-provider: LOCAL
models:
local-invoice-model: deterministic-local-provider
Trust-zone values are LOCAL, EU_CLOUD, and GLOBAL_CLOUD. The configuration is validated at startup, and the rules are strict:
| Rule | Failure if violated |
|---|---|
allowed-models non-empty | Startup failure: missing allowed models |
allowed-providers non-empty | Startup failure: missing allowed providers |
Every allowed provider has a provider-zones entry | Startup failure naming the provider with no trust zone |
Every provider-zones key is an allowed provider | Startup failure: zone configured for a non-allowed provider |
Every models key is in allowed-models | Startup failure: route for an unknown model |
Every allowed model has a models route | Startup failure: allowed model with no route |
Every models route targets an allowed provider | Startup failure: route to a non-allowed provider |
Two things changed shape, so delete them from your configuration:
- The enable flag is gone.
tramai.sovereign.enabledis retained only as a compatibility key, and setting it tofalseis rejected with a deterministic startup failure.tramai.profileis the sole runtime selector. Removetramai.sovereign.enabledentirely. - Sovereign mode is never implicit. In 0.5.0 the sovereign starter could default the application into sovereign mode through an environment post-processor. That post-processor was removed, so a misconfigured deployment now runs standard rather than silently running sovereign. Select
sovereigndeliberately.
Standard (non-sovereign) Spring settings are unchanged and stay under tramai.*: default-provider, models, fallbacks, resilience.circuit-breaker.*, resilience.retry.*, cost.token-budget.*, cache.*, and security.*.
3. JDBC Store Exceptions Are Sanitized
In 0.5.0, a JDBC store could propagate the driver's java.sql.SQLException to your code, including connection strings, table structures, and vendor diagnostics. In 0.6.0 every raw database operation in the JDBC stores runs through a sanitizing wrapper:
- Driver failures — connection errors, constraint violations, SQL syntax errors — are mapped to
IllegalStateExceptionwith redacted detail. - Coroutine
CancellationExceptionpropagates unwrapped, always. - Domain exceptions (
TramaiException, store exceptions) and argument-validation failures propagate unwrapped.
Update any catch block that named the JDBC exception type:
// 0.5.0
try {
approvalStore.consume(approvalId, token, actorId)
} catch (e: java.sql.SQLException) {
logger.error("store failure", e)
return Failure.Retryable
}
// 0.6.0 — infrastructure failure surfaces as IllegalStateException
try {
approvalStore.consume(approvalId, token, actorId)
} catch (e: IllegalStateException) {
logger.error("store failure", e)
return Failure.Retryable
}
Do not catch Exception and treat everything as retryable. CancellationException must be rethrown so that cancelling a coroutine actually cancels it — if you must catch broadly, call rethrowIfCancellation() from dev.tramai.core.coroutines first.
If you have no catch block around store calls, nothing changes: an unhandled store failure was always an infrastructure error, and it still is.
4. Multimodal Image Retrieval: Outbound Address Rules
Image URLs are now validated at the address level before a connection is made, so redirects and DNS-based bypasses cannot reach an internal host. ImageDownloader.download(url) enforces:
| Rule | Rejected |
|---|---|
| Scheme | Anything other than http and https — file:, ftp:, jar:, data:, gopher: |
| User info in the URL | http://user:pass@host/ |
| Hostname | localhost |
| Loopback | 127.0.0.0/8, ::1 |
| Link-local | 169.254.0.0/16 (including cloud metadata endpoints), fe80::/10 |
| RFC 1918 private | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fec0::/10 |
| Other reserved ranges | CGNAT 100.64.0.0/10, IPv6 unique-local fc00::/7, any-local 0.0.0.0 / ::, multicast |
| Redirects | Each redirect destination is revalidated; at most 3 redirects |
| Size | 20 MB per image, enforced as a bounded read rather than a truncation |
The practical consequence: an application that used image URLs pointing at an internal file service, a private S3-compatible endpoint, or a file:// path must switch to a publicly resolvable HTTPS URL, or fetch the bytes itself and pass an ImagePart instead of an ImageUrlContent.
If the validation types are annotated @ExperimentalProviderTransportApi, that annotation describes the API status, not the protection — the checks always run.
5. Internal APIs Are Outside the Compatibility Promise
Declarations annotated @ExperimentalTramaiInternalApi or @ExperimentalProviderTransportApi are internal cross-module contracts. They are excluded from the stable API freeze and may change in any patch release.
If your code references one directly, replace it with the corresponding stable facade under dev.tramai.core.*. If no stable equivalent exists, that is a signal the capability was not intended for application code — wrap the supported entry point instead.
Both annotations are the runtime's mechanism for keeping the stable surface honest: the parts that are not a promise are named, so the parts that are remain trustworthy.
6. Building Extensions
If you develop custom TramAI providers, stores, or plugins as Gradle subprojects in the TramAI repository, two gates apply that did not exist in 0.5.0.
Enrol the subproject in the module catalogue. Every Gradle project must appear exactly once in config/quality/module-catalog.yml with its maturity, visibility, owner, dependency policy, release inclusion, layer, and publishability. A project with no catalogue entry fails the architecture verification, and its layer also determines which dependencies are legal.
Pass the relevant contract suite. ProviderTck in tramai-testing is the offline behavioral contract for a ModelProvider; the harness pins the expected provider identity and capabilities, so a provider passes by matching the contract, not by self-declaring. A custom store implementation should pass the matching TCK:
| Extension | Contract suite |
|---|---|
| Provider adapter | ProviderTck |
| Approval store | ApprovalStoreTck |
| Approval continuation store | ApprovalContinuationStoreTck |
| Audit store | AuditStoreTck |
| Suspended invocation store | SuspendedInvocationStoreTck |
| Workflow checkpoint store | WorkflowCheckpointStoreTck |
| Workflow lease store | WorkflowLeaseStoreTck |
| Sovereign audit outbox store | SovereignOpsAuditOutboxStoreTck |
| Chat memory store | ChatMemoryStoreTck |
These suites live in tramai-testing test fixtures and run offline and deterministically.
What Does Not Change
- The
@AiService,@Operation,@AiTool,@AiRange,@AiMinItems,@System/@User, and@ConversationIddeclaration model. - The
ModelProviderSPI and the provider adapter behavior. - The
tramai-standalonebuilder DSL and the runtime entry points (tramai-standalone,tramai-spring). - Structured-output schema generation for Kotlin return types and JavaBean DTOs.
- Policy enforcement points and
PolicyConfiguration.secure()as the deny-by-default baseline. - The approval suspension and resume contract, including exactly-once consumption.
- The BOM coordinate. Only the version changes.
- A standard Spring application: with no
tramai.profileset, the 0.5.0 configuration keeps working after the version bump.
Checklist
- Bump
tramai-bomto0.6.0(or every explicitdev.tramai:*version). - Replace the sovereign starter coordinate with
dev.tramai:tramai-spring-boot-starter. - Set
tramai.profileexplicitly (standardorsovereign); removetramai.sovereign.enabled. - Move sovereign allowlists under
tramai.sovereign.*and confirm every startup validation rule holds. - Replace
catch (java.sql.SQLException)withcatch (IllegalStateException)orcatch (RuntimeException)around store calls. - Confirm
CancellationExceptionis rethrown, not swallowed, in your broad catch blocks. - Review every image URL used by multimodal operations for scheme, host, and size.
- Remove references to
@ExperimentalTramaiInternalApi/@ExperimentalProviderTransportApisymbols. - For extensions: enrol the subproject in
config/quality/module-catalog.ymland run the relevant TCK. - Re-run your acceptance suite against
0.6.0.
For a per-change description of the release itself, see 0.6.0 — Clarity Is a Runtime Property. For older upgrades, see the Migration Guide.
