The agentic permission seed changed the initdb bytes, so the canonical
hash moved from 0d9ad80328ef1a6ba91e53cf0c6db25812b8bd236e280939f967beee687d5108
to 78692f01fac7814f6a225ff87532502536a23972bf05dd0ab8768e629be19dc8;
resync the embedded ddl_hash and the compose/env pins with
schema_fingerprint.py --sync. Databases initialized before the seed
converge via migrations/05-agentic-permission-resources.sql plus a
public.dc3_schema_fingerprint ddl_hash update at cutover.
leshan-core's ObjectLoader.loadAllDefault streams its default OMA model
DDFs from models/*.xml inside the jar, but the jar ships no native-image
configuration of its own; dropped from the image, the lwm2m driver died
at boot with FileNotFoundException /models/0-1_0.xml and had been
crash-looping since the 2026-10-16 deploy. Register a models/* glob in
DriverNativeRuntimeHints; resource hints are globs, not regexes, in
Spring Boot 4's unified reachability-metadata.json.
The native profile passes -H:ConfigurationFileDirectories, which
disables classpath scanning of jars' bundled native-image configs, so
the caffeine policy family that the new chat/connectivity caches
resolve at runtime must be registered through Spring AOT hints; without
them the agentic center crash-looped on native boot with
ClassNotFoundException SSMSA. Copy the registration pattern from
DataNativeRuntimeHints and wire the registrar through
META-INF/spring/aot.factories.
The agentic MessageServiceImpl kept Spring's default bean name, which
collides with dc3-common-data's same-named class once dc3-center-single
component-scans both modules: the AOT context (and plain JVM startup)
dies with ConflictingBeanDefinitionException. Follow the
dataDashboardService precedent and pin an explicit module-prefixed name.
The audit report was a working artifact for the hardening campaign; the
fixes and their tests carry the knowledge now.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Includes the named-column fix for the message count query (the
positional row.get(0) broke on the scripted test harness).
Co-Authored-By: Claude Code <noreply@anthropic.com>
- flatten the provider routes in openapi-agentic.json
(/provider/config/{add,update,delete} -> /provider/{add,update,delete})
- move /message/list and /action/pending to POST bodies
(ConversationPageQueryDTO) with OffsetPage envelopes; add the
OffsetPageMessageVO schema
- add the missing /attachment/delete endpoint (query id, 204)
- document /chat/completions responses with ChatCompletionResponseVO /
ChatCompletionChunkVO schemas, the finish_reason enum (with the
max_rounds/error/cancelled platform extensions) and SSE event naming
(chat.completion.chunk, agentic.event, agentic.visualization, [DONE],
keep-alive heartbeat)
- switch /attachment/upload to a multipart/form-data file part
- trim SessionVO (tenantId/userId) and AttachmentVO (filePath); extend
SessionExt with icon/category/archived; fix MessageVO.status to
COMPLETED/FAILED/CANCELLED
- add the 11 lastCheck* health-profile fields to ModelProviderVO /
ModelConfigVO and fix the NOT_DEFAULT example
- add AgenticOpenApiSnapshotGuardTest: reflects the dc3-common-agentic
controllers and fails the build on snapshot route drift (test-scoped
dependency on dc3-common-agentic; allowlist covers the x-dc3-ai hidden
provider/model checks and is itself verified against the annotations)
- align the e2e fixture provider routes, the live-harness message/list
call (POST body + OffsetPage unwrap, throw on non-200), and the mock
layer (OpenAI non-stream envelope, SSE event lines + keep-alive
comments, unreachable chat dispatch handler removed, conversation_id
required validation mirroring the server's 422)
- drop the deleted AgenticToolMetadata entry from the center-agentic
native reachability metadata
Co-Authored-By: Claude Code <noreply@anthropic.com>
1. acquireInFlightSlot: register a doOnDiscard hook so a permit granted
to a cancelled subscriber (tryAcquire won, emission refused) is
returned instead of leaking the slot; verified against reactor-core's
MonoCallable discard path and covered by a cancel-race test.
2. evict(): keep the provider in-flight semaphores - the key space is
bounded by database (tenant, provider) rows, while removing one with
outstanding leases would over-admit on rebuild and orphan the
registered permit gauge.
3. listOptions(): stop advertising the fallback model while
transport-fallback-enabled is off (the default); selecting it 503s.
4. Attachment quotas: ReactiveAttachmentStore gains countByTenant and
sumSizeByTenant (tenant-wide, soft-deleted rows excluded); upload
enforces attachment-tenant-max-count / attachment-tenant-max-bytes
after the whitelist check and rejects with the tenant's current
usage before a single byte is written.
5. Web 401 convergence: extract handleAuthFailure (aggregated warning
toast, auth-session reset, router redirect) shared by the axios
response interceptor and the fetch-based agentic stream path - no
raw window.location.hash jumps.
6. Dead code removal (all zero references): AgenticConstant
MAX_AGENT_LOOP_ROUNDS, R2dbcModelProviderStore utc()/utcNow(), the
unused Mono import in ReactiveAgenticToolCallback, InsightTool
DOMAIN.
7. Documentation hygiene: README component table lists only real
classes (adds InsightTool/HistoryTool, the registry as the single
registration path); MessageServiceImpl javadoc reflects the
pushed-down pagination; CommandTool/PointValueTool javadoc name the
tool registry instead of Spring AI @Tool.
8. Property samples: application-agentic.yml and the root .env.example
carry the security master key, transport limiting, cache and
attachment quota knobs with production guidance (master key must be
set, fallback stays off, private base URLs are rejected).
9. Attachment ids are numeric end to end: ChatCompletionRequest and
message contentExt use number[] (server List<Long>); the store sends
raw ids with legacy numeric-string coercion on retry/history; the
mock seeds and upload handler return numeric ids.
Co-Authored-By: Claude Code <noreply@anthropic.com>
AgenticBaseUrlValidator.pinnedDns resolves and validates the host now
and returns a resolver fixed to those addresses, so probe clients can
close the DNS-rebinding window between validation and connect. The
functional shape (host -> validated addresses) keeps the module free of
HTTP-client dependencies; wiring it into the OkHttp clients of both
connectivity checkers lands with the transport factory consolidation.
Co-Authored-By: Claude Code <noreply@anthropic.com>
dc3.agentic.context-token-budget (default off) bounds the assembled
prompt: when the estimated user message plus memory turns exceed the
budget the oldest turns are dropped until it fits, so long sessions no
longer rely solely on the provider-side overflow error. Tool schemas
and tool results stay bounded by the runtime truncation cap instead.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The provider-format tool list and callback map were rebuilt and the
schema JSON re-parsed on every round of every request; both are now
cached against the immutable registry snapshot. The agent loop
additionally tracks identical (tool, arguments) pairs per turn: from the
third repetition the tool still executes but its result tells the model
to change course, so stuck loops self-correct instead of burning the
round budget. Regression tests cover the backpressure guard's
budget/grace/reset semantics.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Two audit residuals: the base URL validator now rejects plain http by
default (dc3.agentic.security-base-url-https-only, opt-out for trusted
plain-http deployments) and blocks NAT64 64:ff9b::/96, 6to4 2002::/16
and IPv4-mapped addresses that could tunnel past the IPv4 rules; tool
tracing now records agentic.tool.calls{tool,domain,outcome} counters and
agentic.tool.duration timers through the registry's optional meter
registry, closing the last missing observability group.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The runtime captured usage but nothing consumed it. Usage now flows from
both runtimes (stream terminal frames carry it; the Spring AI path maps
ChatResponse metadata) through AgenticChatServiceImpl into: the blocking
response VO (estimated=false), the stream terminal chunk (include_usage
semantics, estimates never hit the wire), the persisted message tokens
(new estimated flag distinguishes reported from estimated), and a new
agentic.chat.tokens counter. Meter model labels are bounded so
user-supplied model names cannot explode registry cardinality.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The update path kept the stored (write-only) API key whenever the
request omitted one — including when the base URL changed, so a
compromised tenant session could silently redirect the unreadable
stored secret to an arbitrary public host. The update path now enforces
the same rule as the connectivity probe: a changed base URL without an
explicit new key is rejected.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The store layering fix removed AttachmentFileCleaner from the R2DBC
session store constructor; the dialect contract test still wired the
old signature and only compiled in a full-reactor build.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The repo-wide logging policy gate rejects message concatenation; the
five call sites introduced by the hardening campaign are folded into
single placeholder templates.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The message list endpoint materialized the full conversation and sliced
in memory. The port gains a paged list default (adapters are encouraged
to override with LIMIT/OFFSET) plus a count query; the R2DBC adapter
overrides both and MessageServiceImpl assembles the page from a zipped
count+page instead of collecting the whole conversation.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Follow-up to the store workstream: R2dbcModelProviderStore kept the
UTC-naive LocalDateTime bindings, so wall-clock columns drifted with the
database session timezone. All time bindings now go through
R2dbcDialect.bindInstant like the other agentic stores.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Close the gap the security workstream left unplugged: uploads now pass
an extension whitelist before any byte hits disk, the persisted file
name is normalized (control characters stripped, length capped with the
extension preserved), the storage path segment is bounded so a long name
cannot blow the filesystem limit, and channel open/close run on bounded
elastic instead of the event loop. Regression tests cover the whitelist
rejection and the name normalization contract.
Co-Authored-By: Claude Code <noreply@anthropic.com>
M21 (soft-delete tombstone poisons conversationId): touch() hit the
full-table unique constraint uq_session_tenant_user_conversation when a
soft-deleted row still held the natural key and surfaced the raw
DataIntegrityViolationException. The constraint cannot become a partial
unique index: dc3_message, dc3_attachment, and dc3_action reference it
with foreign keys, and PostgreSQL FK targets must be a non-partial
unique constraint/index. Product semantics are implemented in the write
path instead: touch() now locks the row unfiltered (SELECT ... FOR
UPDATE without a deleted filter), revives a tombstone as a brand-new
session (deleted=0, title and session_ext reset, creator and create_time
restarted), and retries the whole decision once when a concurrent
insert wins the unique-key race. No DDL change, no fingerprint churn.
M22 (session_ext lost update): update() read-merged-wrote without a
lock or transaction, so concurrent renames overwrote each other. It now
runs lock-merge-update inside one transaction, reusing touch()'s row
lock, so concurrent preference changes merge.
L51 (attachment save TOCTOU): R2dbcAttachmentStore.save inserted without
checking the session, so a save racing a concurrent session delete
survived as a live attachment row of a soft-deleted conversation. save
now takes the message store's live-session guard (SELECT id FROM
dc3_session ... deleted = 0 FOR UPDATE) in the same transaction, and
the session delete locks the session row before soft-deleting children,
so upload and delete serialize: the upload either lands before the
delete or fails its guard. File cleanup is collected after commit from
the new listDeletedFilePaths port query (soft-deleted rows only, so a
revived conversation keeps its new files).
M17 (layering inversion): R2dbcSessionStore injected the service-layer
AttachmentFileCleaner and performed filesystem unlinking from the
persistence adapter. The orchestration moved to SessionServiceImpl:
the store is a pure SQL adapter again, the service reads the deleted
attachment paths after the delete commits and unlinks best-effort.
A layering guard test keeps service.impl imports out of the db module.
M23 (blocking file IO on the event loop): AttachmentFileCleaner ran
Files.deleteIfExists on the caller's thread, which is the Reactor
Netty event loop after R2DBC completions. The cleaner now owns the
offload: every unlink/unlinkAll subscribes on bounded elastic, with a
documented exception in the r2dbc migration gate for the one
boundedElastic reference. AttachmentServiceImpl's open/close paths are
left for the security theater to align.
M24 (timezone drift): the agentic stores bound UTC-naive LocalDateTime
to TIMESTAMPTZ columns, which drifts whenever the database session
timezone is not UTC. All timestamp binds across R2dbcSessionStore,
R2dbcMessageStore, R2dbcActionStore, R2dbcAttachmentStore, and
R2dbcModelConfigStore now go through dialect.bindInstant (Instant, and
Instant-typed nulls), matching the platform convention; BO wall-clock
fields stay UTC-naive and reads normalize via DatabaseInstant. The same
pattern remains in R2dbcModelProviderStore (owned by another theater).
Regression coverage: a scripted in-memory DatabaseClient models READ
COMMITTED row locking and unique-key conflicts, with tests for
tombstone revive, concurrent-update merge without lost writes, the
delete-vs-upload TOCTOU, guard ordering, Instant binds, and non-UTC
offset decode normalization. dc3-db-agentic gains its first test suite
(26 tests); dc3-common-agentic keeps 206 green.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Follow-up to the contract workstream: the mock handlers, the frontend
chat request type (conversationId now optional), the mock-adapter test
and the dc3-cli provider/session commands move to the new routes —
provider /add|update|delete without the config nesting, POST body
pagination for message/list and action/pending.
Co-Authored-By: Claude Code <noreply@anthropic.com>
- H7 (OpenAI wire divergence): the non-streaming completion response serialized
finishReason/promptTokens/completionTokens/totalTokens in camelCase while the
chunk VO and the frontend contract already declared snake_case, so clients never
read the values. Choice.finishReason and all three Usage fields now carry
@JsonProperty(finish_reason/prompt_tokens/completion_tokens/total_tokens); the
request side accepts max_tokens and max_completion_tokens via @JsonAlias and the
value is already mapped through to the provider request.
- H8 (conversationId contradiction): the OpenAPI schema said omitted ids start a
new conversation, but the preparer rejected the request at runtime. The server
now generates a fresh UUIDv7 conversation id when the field is omitted (OpenAI
stateless style); an explicit id still enables memory replay.
- M29 (messages semantics): the request declared full-context messages but only
the last user message was used. The preparer now honors the whole transcript:
system messages merge into the system prompt after the platform prompts, the
last user message drives the turn, and every earlier user/assistant turn is
replayed. When the client carries prior turns it manages its own context, so
platform memory replay is skipped for that turn (no double replay); with only
the current turn, platform memory replays per memoryEnabled/historyWindowSize.
Tool-role messages are accepted but not replayed (no tool-call correlation).
- M31/M55/M18 (message list): the message list endpoint bypassed the service
layer and returned an unpaged list. Added MessageService/MessageServiceImpl,
moved the controller to controller->service->repository layering, and switched
/message/list to POST-body offset pagination (defaults 50, cap 200, OffsetPage
envelope) matching the session convention. Pagination is applied in the service
until the store port gains an offset/limit overload (storage war zone).
- M32 (pagination style split): the module mixed POST-body, GET-query and absent
pagination. Session list keeps the platform POST-body style; /message/list and
/action/pending now use the same POST-body + OffsetPage shape; attachment and
model/provider catalog lists stay unpaginated as bounded per-conversation or
config-scope sets.
- M33/L53 (filePath leak): AttachmentVO exposed the server-side storage path;
the field is removed from the VO (the BO keeps it internally) and the frontend
attachment type no longer expects it.
- M56 (provider route nesting): a provider is a single resource, so
/provider/config/{add,update,delete} collapsed to /provider/{add,update,delete}
(breaking; the frontend API layer now calls the flat routes).
- M57 (fake session fields): SessionVO carried tenantId/userId that the
controller nulled per response; the fields are removed together with the
sanitize workaround and a serialization guard test asserts the keys never
reappear.
- M27/M30/M55 (permission model): verbs now follow the platform five-verb
convention - chatCompletion: chat:add; action confirm/reject: action:update;
action pending: action:list; attachment upload: attachment:add (breaking).
The agentic permission codes were absent from the dc3_resource seed, so only
wildcard admins could ever use the assistant: 21 API-type resource rows are
seeded in initdb/02-iot-dc3-auth.sql and backfilled for existing databases by
migrations/05-agentic-permission-resources.sql (idempotent ON CONFLICT DO
NOTHING; entity_id -1 placeholder is adopted and relinked by the runtime
resource registrar, which retires the pre-fix verb codes on first sync).
- H6 attachment side (prompt injection delimiting): attachment metadata enters
the prompt wrapped in explicit UNTRUSTED_ATTACHMENT_METADATA markers with a
data-only notice, control characters are stripped (a file name cannot forge
markers or line structure) and per-line length is capped by
attachmentNameMaxLength; instructions always precede the untrusted block.
- M37/M46 (SSE hardening): streaming frames now carry event names derived from
their object discriminator (chat.completion.chunk, agentic.event,
agentic.visualization) with [DONE] kept on the default event, and a
keep-alive comment heartbeat (streamHeartbeatSeconds, 0 disables) is merged
into the stream and terminates with it.
Co-Authored-By: Claude Code <noreply@anthropic.com>
H4 (SSRF): every user-supplied base URL now passes AgenticBaseUrlValidator
(http/https only; all resolved addresses must be public — loopback, private,
link-local, CGNAT, TEST-NET, reserved and ULA ranges rejected, DNS-rebinding
multi-answer included, verdicts cached 30s). Draft probes, stored-provider
probes (including model-config checks) and provider add/update all validate
through it, and probing a stored provider against a different base URL
requires an explicit API key so the stored key is never replayed to an
arbitrary host.
H5 (plaintext keys at rest): new AgenticKeyCipher (AES-256-GCM envelope,
enc:v1:<base64(iv||ciphertext||tag)>). R2dbcModelProviderStore encrypts on
write and decrypts on read; legacy plaintext rows pass through on read and
upgrade to ciphertext on the next write (lazy migration). Master key resolves
from dc3.agentic.security-master-key, then DC3_AGENTIC_MASTER_KEY, then a
stable HKDF-SHA256 derivation from DC3_SECURITY_KEY (warned once at startup);
decrypt failures fail loudly instead of degrading to plaintext.
M25/M38 (unbounded check caches): connectivity results move to bounded
Caffeine caches — stored results capped by check-cache-maximum-size/TTL,
draft probes (client-material keys) in a separate 32-entry partition with
half the TTL — and the in-flight merge map is capped, running probes
unmerged past the cap instead of growing.
M26/M35 (half-finished throttling): the platform-wide fallback is now
opt-in (transport-fallback-enabled, default false) — tenants without a model
configuration get a clear 503 problem instead of silently using the global
key; the fallback client is never cached. Cache-miss/unknown-model acquires
are throttled by the global semaphore, acquire waits are bounded by
transport-acquire-timeout-seconds (fast 503 on timeout, no event-loop
blocking — the wait parks on the virtual-thread slot scheduler), and chat
entries pass a per-tenant QPS token-bucket gate (transport-per-tenant-qps,
429 on overflow, 0 disables).
M19/M36 (no cache TTL): the four ChatClientFactory caches expire after
transport-cache-ttl-seconds, so out-of-band provider edits stop serving
stale transports.
H9 (zero metrics): Micrometer instrumentation — provider/global semaphore
permit gauges and acquire-timeout counter in ChatClientFactory,
agentic.chat.requests{tenant,provider,model,result} counter plus a duration
timer and a per-tenant QPS gate counter in AgenticChatServiceImpl, and an
agentic.check.results{kind,result} counter for connectivity probes.
H10 (broken trace): new RequestIdContextPropagationConfig in dc3-common-web
registers a ThreadLocalAccessor bridging the dc3.requestId Reactor context
key into the MDC (mirroring the MQ module precedent) and enables automatic
context propagation, so requestId reaches every thread a reactive chain hops
to and flows through the insight RPC chain.
L54 (insight error leakage): InsightTool failures now answer the model with
a stable, category-classified phrase (network/storage/invalid
request/unexpected) plus the correlated requestId, capped at 200 chars;
internal details only reach the server log.
L58 (logs missing tenant): chat lifecycle logs carry tenantId, userId and
durationMs (no request content, no credentials).
Co-Authored-By: Claude Code <noreply@anthropic.com>
- H11 (IME enter): the composer submitted the draft on the Enter that
confirms an IME candidate, so CJK users could not type multi-character
candidates. All Enter-driven actions on agentic text inputs (composer,
header rename, rail rename) now guard event.isComposing ||
event.keyCode === 229 via the shared isImeCompositionKey helper.
- M39 (quadratic streaming hot path): every token triggered a full
markdown re-parse + DOMPurify pass + O(n) scroll signature rebuild.
Deltas now coalesce in a 50ms window before hitting the store (always
flushed synchronously on finish/failure/reset so no text is lost), the
streaming tail renders as plain text while closed segments render from
a memoized parse+sanitize cache, and the cache is dropped at completion
so the final frame re-parses and re-sanitizes the whole message. The
scroll signature is O(1): list length + tail identity/lengths only.
- M40 (mock mega-deltas): the scripted engine emitted each answer as one
giant delta, so the corpus never exercised token streaming. It now
slices replies into random 1-8 char deltas and splits one content frame
across two enqueues per stream; new transport tests cover cross-read
frame reassembly, multi-frame reads, unterminated trailing frames,
malformed payloads, and mid-stream aborts.
- M41 (docked history dropdown fork): the dropdown mixed archived
sessions in, had no truncation hint and no search, diverging from the
workbench rail. Both surfaces now share one contract (sessionList
helpers): archived folded out, one title+summary search rule, and
pagination reachable from both.
- M42 (session list hard cap): conversations beyond the first 50-session
page were unreachable. loadMoreSessions appends the next page (stable
row identities, deduped, offset tracked over server rows only) and the
rail/dropdown expose a load-more control driven by the page hasNext
flag.
- L60 (no retry for failed turns): failed assistant replies now carry a
regenerate action that cuts the failed turn and replays its user
message (with the original attachment ids) through the normal
sendMessage pipeline.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Raw exception messages could echo internal class, query and host details
into the model context and the persisted trace. Argument failures now
map to a stable phrase, everything else is length-capped; full details
stay in server-side logs.
Co-Authored-By: Claude Code <noreply@anthropic.com>
normalizeIds silently capped lookups at MAX_IDS; the model received a
partial answer and treated it as complete. Every bulk-lookup tool now
appends a truncation note ("queried first N of M provided ids") to its
result message, matching the SystemTool id-list behavior from the
registry unification.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Root-cause fixes for the audit's registry findings:
- One registration path: the tenant/user/system lookups move from the
reflective @Tool channel into the reactive registry, so both agent
runtimes expose the same tool surface; the MethodToolCallbackProvider
channel, its tracing callback wrapper and the @AgenticToolMetadata
annotation are deleted (domain is now declared explicitly at
registration and carried through the tracing wrapper).
- The 8 verbatim-duplicated functional interfaces and factory methods
collapse into one ToolCall contract and one tool() factory that parses
arguments once and turns malformed JSON into an INVALID_ARGUMENT
envelope the model can self-correct from, instead of an error.
- The three telescoping constructors are gone; the traced snapshot is
built once at construction time and returned as the same immutable,
order-preserving instance on every call.
- number() coerces LLM-friendly integer forms via BigDecimal
(10.0, 1e3, "42"); non-integral or unparsable values yield null
instead of NumberFormatException. The history bridge parses after its
null guard now.
- Analytics schemas spell out their series/window items (mirroring the
AnalyticsModel records) instead of bare {"type":"object"} shapes;
numeric bounds come from ToolLimit-bound constants and the offset/
limit fragment is assembled by a single helper.
- SystemTool id lists are capped at ToolLimit.MAX_IDS with a truncation
note in the result message; the contract test now guards that every
public tool method is actually registered (and vice versa).
Co-Authored-By: Claude Code <noreply@anthropic.com>
Root-cause fixes for the audit's runtime findings:
- One tool registry, both runtimes: ChatClientConfig no longer maintains a
separate reflective @Tool channel; the Spring AI path now consumes the
reactive registry through ReactiveAgenticToolCallback bridges, so the
tool surface is provider-independent (the bridge runs on bounded
elastic, guaranteed by subscribing the whole Spring AI chain there).
- Hallucinated tool names answer with a tool-error envelope listing the
real tools instead of killing the whole turn (H1).
- Slow SSE consumers are tolerated up to a bounded backpressure buffer
with an idle-timeout grace period instead of the zero-demand instant
kill (H2); AgenticBackpressureGuard carries the accounting.
- The loop budget closes turns with a forced no-tools final round
(finish reason max_rounds) instead of hard-failing (loop policy
configurable via dc3.agentic.loop-*, legacy behavior opt-out).
- Provider-reported token usage is captured from usage frames and
completions and carried on AgenticRuntimeResult (M34).
- Oversized tool results are truncated inside a valid envelope instead
of crowding out the conversation context.
- The system prompt gains an untrusted-content security section (H6);
tool tracing domains come from explicit declarations instead of
name-prefix heuristics.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The opc-ua driver (Eclipse Milo + netty + jaxb universe) exhausts the
16GB hosted runner during the native-image analysis step even with
-J-Xmx10g; it builds fine on larger local machines. Its native image has
never shipped, so dropping it from the matrix removes nothing users
already have. It stays on the JVM line with dnp3/ble/opc-da until the
heap cap is property-driven or a larger runner hosts the job.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Both drivers died at boot with NoClassDefFoundError on
org.slf4j.MarkerFactory: their protocol SDKs (Paho, Modbus4J via the
mangoautomation repository) exercise ServiceLoader paths that
DriverNativeRuntimeHints does not express, and unlike the rest of the
driver line they had no agent snapshot to close the gap. The snapshots
are committed per native_agent_collect.sh's stated-reason exception:
booting each repackaged JAR with the native-image-agent records the
SLF4J/logback binding metadata the closed-world analysis needs.
Verified by rebuilding both native images from scratch and running them
bare against a live broker: both subscribe on untagged dc3.q.* queues
and stay up instead of crash-looping.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Native binaries cannot re-resolve the spring.profiles.active
${NODE_ENV:dev} placeholder at runtime: the AOT pass freezes the dev
profile (and application-dev.yml) into the image. RabbitEnvironmentConfig
then reads spring.env=dev and prefixes every exchange and queue with the
dev_<user> isolation tag, which silently splits the broker topology
against JVM services running with NODE_ENV=test (messages end up
NO_ROUTE between the two lines).
Build-time profile pinning does not reach the AOT snapshot (verified
with NODE_ENV, SPRING_PROFILES_ACTIVE env vars and MAVEN_OPTS system
properties). SPRING_ENV feeds the same EnvironmentPostProcessor through
the system-environment property source, which outranks profile YAML at
runtime, so one ENV default in native-runtime-base covers every native
target. Deployments that want an isolation tag can override the
variable.
Verified by rebuilding dc3-driver-virtual-native and running the image
bare: subscriptions now use untagged dc3.q.* names.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Two fast CI gates caught the tail of the driver native campaign: the
root pom declared swagger-annotations-jakarta twice (the pin already
existed next to the mockito pins - Maven 4 treats the duplicate
dependencyManagement entry as an error, which killed every early plain
mvn invocation on CI while local 3.9 only warned), and the r2dbc
migration gate's literal jdbc: pattern matched the hyphenated
'sqlite-jdbc:' inside a DriverNativeRuntimeHints comment. Remove the
duplicate entry and reword the comment; no gate semantics change.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Sl651DriverCustomServiceImpl reaches the SL651 stack through
Class.forName and a JDK proxy over an optional dependency. Without
registration a native image silently takes the sl651ApiMissing branch:
the driver boots and reports health but never receives a message. Add a
module-level Sl651NativeRuntimeHints registering SL651Server and
ISl651MessageListener, wire it through aot.factories, and carry the
module's tracing snapshot. Functional acceptance remains a live SL651
message round-trip on real hardware.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Batch 3 joins the native line: modbus-rtu, serial, mbus, dlt645, dlms
and zigbee - the jSerialComm serial family. jSerialComm ships no
native-image configuration of its own, so DriverNativeRuntimeHints
registers the embedded per-platform JNI libraries as resources and the
SerialPort JNI surface explicitly (string-based references keep
non-serial drivers unaffected). The tracing snapshots carry the early
boot surface only - serial paths open on device access and stay covered
by the explicit registrations. zigbee is best-effort: no radio hardware
locally, gRPC registration and scheduling are the acceptance line.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Batch 2 joins the native line: mysql, postgresql, sqlserver, oracle,
redis and kafka - database dialects and messaging clients, all pure
Java. DriverNativeRuntimeHints gains the four JDBC dialect driver
constructors (string-based TypeReferences; each class exists only on
its own driver's classpath and native-image ignores hints for absent
classes). Same generated shape as the previous batches: targets, matrix
entries, compose blocks and per-driver agent snapshots.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Batch 1b joins the native line: knx, bacnet-ip, melsec, plcs7, lorawan,
opc-ua, lwm2m and iec61850 - pure-Java protocol SDKs (calimero,
bacnet4j, iot-communication, milo, leshan, openiec61850). iec61850's
GOOSE raw-ethernet paths stay untested on native and are validated in
TCP mode only, as flagged in the CI matrix comment. Same shape as batch
1a: native targets, matrix entries, compose overlay blocks behind the
native-drivers profile, and per-driver agent snapshots.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Batch 1a joins the native line: tcp-udp, listening-virtual, can,
ethernet-ip, fins, http, snmp, iec104 and coap - pure-Java protocol
stacks with no JNI dependencies. Native targets mirror the jar line's
directory layout (listening-virtual keeps its TCP/UDP EXPOSEs), the CI
matrix gains the nine entries, and the compose overlay defines the nine
services behind the native-drivers profile with keys matching
docker-compose.yml so overlay semantics hold (the pilot keys are renamed
to the short main-compose form for the same reason).
Each driver carries a tracing-agent snapshot per the dual-track policy:
RuntimeHints covers the shared SDK surface, snapshots carry the
logback/logstash parsing depth and per-protocol reflective access.
Co-Authored-By: Claude Code <noreply@anthropic.com>