Changelog
All notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased] · 1 security · 6 breaking · 1 added · 1 fixed
Security
- Known gap in 0.7.x and earlier, closed in 0.8.0: on a source record, a
@JsonPropertyrename that swaps two components' names, or a class-level serializer that writes one component's value under another component's name, could release a sensitive value under the policy of a@NonSensitivefield. Every release up to and including 0.7.1 is affected. 0.8.0 fixes this by construction: the engine reads each record component itself and writes it under its own name, and startup refuses Jackson annotations on source models. There is no 0.7.x patch; upgrade to 0.8.0.
Breaking
ValueTokenSource.available()now defaults tofalse(it wastrue). A customValueTokenSourcethat hashes and tokenizes must override it to returntrue; until it does, a model describing HASH or TOKENIZE fields omits them and says the source cannot tokenize. A decorator must delegate it to the source it wraps.- Startup refuses with
MODEL_DESCRIPTOR_RESOLVER_REPLACEDwhendataprism.privacy.descriptor-fileis set and either anyFieldMetadataResolverbean other than the one Data Prism builds from that file is present (including aDescriptorFieldMetadataResolverthe application built itself), or aJsonTreeScrubbingEnginebean does not use that resolver. A deployer who needs a custom resolver or engine must leavedataprism.privacy.descriptor-fileunset and build aDescriptorFieldMetadataResolverthemselves. - Source models are read by the engine itself, and startup is stricter about them. Startup refuses a Jackson annotation on a source model with
SOURCE_MODEL_JACKSON_ANNOTATION, and the engine refusesSOURCE_MODEL_RECORD_NOT_DECLARED,SOURCE_MODEL_NOT_READABLEandSOURCE_MODEL_FOREIGN_NODE. The allowance of@JsonSerialize,@JsonAnyGetter,@JsonValue,@JsonUnwrapped,@JsonNamingand@JsonFormaton source records is withdrawn. JDK reference and holder types other thanOptional(AtomicReference,java.lang.refreferences,ThreadLocal,Future) and single-use types (Stream,Iterator,Spliterator,Enumeration) are refused in source models. A pre-builtJsonNodeholding aPOJONodeor a non-Jackson node class is refused. An enum is written byname()as a value and as a map key, whatever its annotations. A record at array level two or deeper, or anywhere inside a map value, is now also refused at scrub time with its startup code. A record at anObject, interface or other undeclared position is now refused whether or not it is classified.@JsonAnyGetteris refused: a record source can no longer carry undeclared keys, and extra keys go in a namedMap<String, Object>orJsonNodecomponent, which is never descended into, so the unclassified-structure handling applies to it whole (or in a configured JSON source, for per-key handling). Component values are read from fields, not accessors. When reading a source value throws (an exception from the adapter's own collection, map or other object), that source alone is refused withSOURCE_VALUE_READ_FAILEDand the other sources still answer: where a caller sawREQUEST_FAILED(a Java caller) or "the request could not be completed" (an MCP client,ToolCalls.java:164-166on wave4), a caller now gets a response in which that source is markedREFUSED, and seesSOURCE_VALUE_READ_FAILEDas the call's refusal (a Java caller) orrefused: SOURCE_VALUE_READ_FAILED at <path>(an MCP client) only when no other source answered (trees.isEmpty()) (DefaultContextOrchestrator.java:209-215on wave4). Errors and checked exceptions from such a read, which escaped unaudited, are now refused and audited. To fix a model refused withSOURCE_MODEL_JACKSON_ANNOTATION, remove the annotation the message names. From 0.8, Data Prism reads a source record itself: each component is written under its own name with its own value, and Jackson annotations on the record, its components, its methods and the enums it holds change nothing. If the annotation renamed a component in the output, rename the component. If@JsonPropertymapped an upstream response's name onto the component, use@JsonAliasfor that name instead. If it changed how a value is written (@JsonValue,@JsonSerialize,@JsonFormat), give the component the type you want written: for a single sensitive value, aStringwith@SensitiveDatainstead of a record that serialises as one; for a formatted date, aStringholding the text. If it hid a component (@JsonIgnore), remove the component or classify it. If it added keys to the object (@JsonAnyGetter), the keys can no longer sit beside the components: hold them in a namedMap<String, Object>orJsonNodecomponent. That component is never descended into. Classify it as a whole: unannotated or marked@NonSensitive, the profile's setting for unclassified data decides the whole value, and the bundled DEFAULT profile refuses it. If the keys must be handled one by one, serve that payload as a configured JSON source instead.@JsonAlias,@JsonCreator,@JsonDeserialize,@JsonSetterand@JsonIgnoreProperties(ignoreUnknown = true)stay allowed, because they affect only how an adapter reads the upstream response. To fixSOURCE_MODEL_RECORD_NOT_DECLARED, declare the position as the record type itself, notObject, an interface,Record, a type variable, an unbounded wildcard or a raw collection. To fixSOURCE_MODEL_NOT_READABLE, which can happen only on the module path, add an unqualifiedopensfor the model's package to itsmodule-info.java. To fixSOURCE_MODEL_NOT_A_RECORDon anAtomicReferenceor another reference type, hold the value itself, or anOptionalof it. On aStream, anIteratoror another single-use type, collect it into aListfirst. To fixSOURCE_MODEL_FOREIGN_NODE, build the tree from Jackson's own node types and do not put a Java object into it (putPOJO). - A source whose value holds more than 1,000,000 nodes (records, elements, map entries,
JsonNodes and leaves, each counted once, whatever their size) is refused withSOURCE_TREE_TOO_LARGE; that source alone is refused and the others still answer. - A configured JSON source whose response carries a structure (an object or an array) at a root leaf field, one with no
nested:of its own, is now refused withNESTED_LEAF_NOT_SCALAR. Before, that case was refused asUNCLASSIFIED_STRUCTURE. A root leaf the profile removes (actionREMOVE) is exempt and is dropped as before. A singledataprism.json-sources.config-locationvalue is no longer split at commas; several files are listed as indexed entries. - A tool call that breaks the tool's input schema now returns
TOOL_INPUT_INVALID, or, on alist_sourcescall that carries apagekey, the link refusal (LINK_INVALID, "The link is not valid."), instead ofTool (<name>) input validation failed: .... The check moved from the MCP SDK into the tool handlers, after authentication, so every such call is now audited once and counted as denied, and a call with apagekey counts toward the link limiter. A client that matched the old text must change. A blankentityTypeorsubjectIdno longer answers "entityType and subjectId are both required"; it isTOOL_INPUT_INVALID. The served schemas do not change.
Added
AuditChainVerifierCli accept-crash-line <audit-directory> --checkpoints <anchor-store-file> --segment <name> --offset <n> --sha256 <hex>(exit code 6 on refusal, see--help). Use it after a purge refuses withAUDIT_RETENTION_CHAIN_UNVERIFIEDbecause a crash left a line that parses but was never hashed, and an investigation finds a crash line, not tampering. It appends oneCRASH_LINE_ACCEPTEDcheckpoint to the anchor store, and purge then tolerates that exact line (segment, offset and the SHA-256 of its bytes) and nothing else. No audit file is changed. The checks do not prove a crash; your investigation decides, so accept a line only after confirming a known crash or power loss of that instance at that time. Someone who can write the audit directory can plant a line that passes every check in order to get it accepted. Stop the server first, run the command as the account that owns the anchor store, and re-run purge until it refuses only at the crash line's day (finish a partly deleted day first). The command refuses, writing nothing, when aBOOT,PERIODICorSHUTDOWNcheckpoint shows the writer got past the line (a power loss on a disk that does not honour fsync can trigger this; extend retention by hand), and when the store holds noBOOTfor the line's instance, which in a deployment with a checkpoint sink is a strong sign of tampering. Only deployments with no checkpoint sink, and instances that ran before the store existed, should pass--no-boot-checkpoint, after investigating; it waives that one check, the waiver is recorded in the acceptance (bootWaived), and every later CLI verify run given--checkpointsprints aNOTICEfor it (a program callingverify()directly does not see it). These checks refuse a line whose own evidence contradicts the store or the keys; they do not defend against someone who can write the audit directory. In a keyed deployment pass--hmac-key-file. The command reports whether the line's own hash recomputes:KEYED_MATCH <i>(the 1-based position of the matching key),UNKEYED_MATCH,NO_MATCH_LOOKS_CUT(looks like a cut or an edit),NO_UNKEYED_MATCH(no key given; the line looks cut if the deployment is unkeyed) andUNKEYED_ONLY_FORGER_SHAPE(the shape a forger without the key produces; refused). Apart from that refusal the report does not decide acceptance. An anchor store that contains an acceptance cannot be read by 0.7.x or earlier: a 0.7.x verifier stops with an unreadable checkpoint file, and a server rolled back to 0.7.x fails closed, whenever it has an expired day to purge, withAUDIT_RETENTION_CHAIN_UNVERIFIED: could not read the earlier retention anchors.
Fixed
- Retention purge could erase audit records while verification still passed. Affected: 0.4.0 through 0.7.1.
Anyone with write access to the audit directory (but without the chain key or the checkpoint store) could,
by appending or editing a line that the verifier skips without hashing, make a purge record a forged retention
anchor, or delete a day's records, including records still inside the retention period, with no trace. In
0.8.0 a purge builds anchors and day boundaries only from records the verifier actually hashed. A day is
deleted only when every record in it that parses was verified, each writer's next record after the day links
to the day's last verified record, and no record is dated after the day in its file's name. Otherwise the
purge keeps the day and every later expired day and fails with
AUDIT_RETENTION_CHAIN_UNVERIFIED. A day is also kept when a writer'sSHUTDOWNcheckpoint contradicts its records; the message says to compare the checkpoint store (dataprism.audit.checkpoint.file-path) with the audit directory before purging. The clean-shutdown cover needs a checkpoint sink that implements theSHUTDOWNread-back (AuditCheckpointSink.shutdownCheckpoints());FileAuditCheckpointSinkdoes, and a custom sink without it falls back to the residual below. A crash can leave a line that looks like a complete record but was never verified. The purge of that day, and of every later expired day, then stops withAUDIT_RETENTION_CHAIN_UNVERIFIED, and thedataprism.audit.retention.unverifiedmetric fires until the operator acts. A crash fragment that does not parse as a record is tolerated and does not stop the purge. Recovery from a complete-looking line is not part of this release. Residual, not closed: if a writer stopped without a clean shutdown (for example after a crash) and never writes again, deleting its last records before a purge is not detected by the purge. Periodic checkpoints (dataprism.audit.checkpoint.interval) are the defence; a writer that shut down cleanly is covered by itsSHUTDOWNcheckpoint. Also not closed: the purge still deletes an expiring day from which every line of a writer was removed. The full verifier reports that afterwards (MISSING_WRITER, or a non-GENESIS start when the writer survives elsewhere), except when the writer's highest checkpoint isBOOT, when a retention anchor covers it, or when the writer has no checkpoint at all (no checkpoint sink, or a verify run without a checkpoint file); then nothing detects it. This behaviour has been present since 0.4.0, as has the defect above. The defence is an audit directory writable only by the server. Workaround for anyone staying on an affected version: setdataprism.audit.retentionto a period longer than the oldest segment, for exampleP100Y, and keep the audit directory anddataprism.audit.checkpoint.file-pathwritable only by the server. Plain-Java embedders who never callpurge()are unaffected. (No affected version has an on/off switch for the purge: it runs wheneverdataprism.audit.directoryis set.)
0.7.1 — 2026-10-21 · 3 security · 3 changed · 2 fixed · 5 build
Security
- A field whose declared type was a user generic type (for example
Envelope<Leaf>) was scrubbed, and its prohibited values were collected, with the type argument's rules instead of the field's own type. When the argument type declared the same field names as@NonSensitive, values from the outer type could be released under the DEFAULT profile, and neither output validator caught them. AMapwhose values are records was scrubbed as if the map were a record. The defect is present in every release up to and including 0.7.0. It affects deployments whose source models use user generic types or maps of records; models built only from records, scalars, JDK collections,Optionaland arrays were not affected by this entry (nested collections are the next entry). Fixed in 0.7.1: upgrade, and change any model that is now refused at startup (see Changed). - When a field held an array of arrays (for example
List<List<Leaf>>orLeaf[][]), the field's own policy was applied to each inner array whole. A field marked@NonSensitivetherefore released every record inside the inner arrays unscrubbed under the DEFAULT profile, and neither output validator caught it. The same happened at request time for anObjectorList<Object>field holding nested lists of records. The defect is present in every release up to and including 0.7.0. It affects deployments whose source models hold records inside a collection inside a collection, or whoseObject-typed fields can carry nested lists of records; a single level ofList,Set, array orOptionalaround a record was not affected. Fixed in 0.7.1: upgrade, and change any model that is now refused at startup (see Changed). - Embedded Tomcat is managed at 11.0.27. Spring Boot 4.1.1 manages Tomcat
11.0.24, which is affected by GHSA-9xv2-5v5q-p794 (CVE-2026-65905),
GHSA-gcx9-497g-6cp6 (CVE-2026-65182) and GHSA-h3x4-894j-xpx5 (CVE-2026-68525),
all fixed in 11.0.25. Data Prism manages 11.0.27 for the server, the starter
and the quickstart images. Data Prism does not configure Tomcat DIGEST or FORM
authentication anywhere: requests are authenticated by Spring Security JWT
validation, and the quickstart issuer signs the tokens. An application that
embeds
data-prism-spring-boot-starterand imports its own Spring Boot BOM must manageorg.apache.tomcat.embedat 11.0.25 or later itself, ahead of that BOM (docs/extending.md, "The pom shape").
Changed
- Two new startup refusal codes for source models.
SOURCE_MODEL_GENERICis raised for a generic record, or for a component whose declared type is a parameterized user type.SOURCE_MODEL_MAP_OF_RECORDis raised for a JDK map whose value type contains a record. A JDK wrapper outside collections,Optionaland maps (for exampleAtomicReference<X>) is no longer scrubbed withX's rules; it takes the unclassified-structure path, which refuses under DEFAULT. These deliberately refuse models that 0.7.0 accepted, in a patch release, because those models were not protected. - A new startup refusal code,
SOURCE_MODEL_NESTED_COLLECTION_OF_RECORD, is raised for a component that puts a record inside a collection or array inside another collection or array (Optionaldoes not count as a level). At request time, a record inside a nested array is refused as an unclassified structure (UNCLASSIFIED_STRUCTUREunder DEFAULT). On a field holding nested arrays of scalars, an action other than pass-through now applies to each scalar rather than to each inner array whole; pass-through output is unchanged. Like the entry above, this deliberately refuses models that 0.7.0 accepted, in a patch release, because those models were not protected. - Clients that got an MCP protocol error, or "the request could not be
completed", when a denial's audit write failed now get an error tool result
refused: AUDIT_UNAVAILABLE.AuditUnavailableExceptionis deprecated for removal.
Fixed
- On both tools (
get_entity_contextandcompare_entity_sources), every denial whose audit write fails now answersrefused: AUDIT_UNAVAILABLEand increments the new metricdataprism.audit.denial_unavailable. Before, an orchestrator-side DENY that could not be written surfaced as a generic failure. - A re-identification refusal or operator denial whose audit write fails now
answers
AUDIT_UNAVAILABLE(HTTP 503 on the operator API) and incrementsdataprism.audit.denial_unavailable, instead of returning the original code with no audit record. A resume that fails after itsALLOW:RESUMEDis written now leaves a linkedDENY:<code>under the same correlation id, and state stays unchanged.
Build
- Every GitHub Actions reference is pinned to a full version, and a check on
pull requests that touch
.github/confirms each reference resolves. - A weekly advisory scan of the resolved Maven dependency trees (all modules,
including
data-prism-server). High, critical and unrated advisories fail the scan; every ignore must carry a reason and an expiry date. Dependabot now also covers the pip files andload-test. - One SBOM build is shared by the GitHub Release and Central: the release attaches the same SBOM files that go in the Central bundle, and a step asserts their SHA-256 values are equal.
- Each per-architecture image digest reachable from a manifest list is signed
and attested, and the image publish fails if any list or platform digest is
unsigned. The Central publish is refused unless the GitHub release exists.
docs/verifying-releases.mdshows the per-architecture and release-SBOM checks. - The Jackson 3 override stays at 3.1.8: Spring Boot 4.1.1 still manages 3.1.5, which is below the fixed 3.1.7.
0.7.0 — 2026-10-20 · 11 breaking · 9 added · 9 changed · 7 fixed · 4 security · 6 build
Breaking
The full list, grouped by who has to act, is in docs/migration-0.7.md.
get_entity_contextresponse shape. Eachsourcesentry is now{ "status": "<CODE>", "entity": { ... } }, withentitypresent only forANSWERED, and the top-levelentityfield is removed. Sources are no longer merged, so two sources that use the same field name for different values are both returned.ContextResponse.sources()isMap<String, SourceResult>;ContextResponse.entity()and the constructors that took it are removed.- Per-source refusal and a new status. A source whose scrub or validation
refuses is withheld and marked
REFUSED, and the other sources are returned. The call is refused only when every source that answered is refused, or the final check over the assembled response fails. The seven status codes are a documented contract, and a client must treat an unknown code as "not answered". - Audit logs. A refused source is recorded as
REFUSED:<code>under<source>:<refused>(it wasREFUSED) and is listed as<source>:REFUSEDamong the source systems. A log written by 0.7 cannot be read by a reader or verifier older than 0.7. Logs written before 0.7 still verify with the new one. LlmResponseValidator. The final pass now receives a JSON array of the returned sources' trees, not an object. A custom validator that assumes an object root throws, and the call fails closed (DENY:REQUEST_FAILED). Per-source calls still pass one object.SensitivePatternValidatoris always on in Spring. The starter, the packaged server and the configured-JSON-sources orchestrator now register it as thePRIVACY_CRITICALbeandataPrismSensitivePatternValidator. Before, onlyRawValueLeakValidatorran there. A sensitive-shaped value, for example an email-shaped one, in a@NonSensitivefield is now refused withVALIDATION_FAILED/SENSITIVE_PATTERN. Classify such fields correctly; there is no opt-out property.- Record and response-size caps are enforced. A response over
dataprism.limits.max-recordsordataprism.limits.max-response-sizethat 0.6.x returned is now refused (see Changed). - Hazelcast approvals cannot be upgraded in place. Approval entries, id claims and pending entries carry a creation token, and approval entries written by 0.6.x are unreadable and refused. Stop every member and start 0.7; do not run old and new members together, and re-request approvals that were pending.
- Hazelcast
tcp-ipapproval quorum. With approvals in use, the approval minimum defaults to a majority of the listed members, so a two-member cluster stops approval-gated tools while either member is down. Setdataprism.hazelcast.approval-minimum-members: 1to opt out. - Hand-wired
PrivacyCluster.embedded(ClusterMembership, boolean). The 0.6 overload now applies the default approval quorum whether or not approvals are in use. A hand-wiredtcp-ipmembership of hostnames that started on 0.6 now throwsPrivacyClusterRefusalMISSING_APPROVAL_QUORUM, a wildcard entry getsINVALID_APPROVAL_QUORUM, and an IP-only list now gets a majority write quorum on the approval map. Pass an explicit minimum through the new overload. - Cluster refusals now reach callers as their own codes (see Changed): the
operator API's
503 OVERSIGHT_UNAVAILABLEand the re-identificationREIDENTIFICATION_UNAVAILABLEbecomeCLUSTER_LOCK_TIMEOUTorCLUSTER_OPERATION_REFUSEDwhen the cluster was the cause. RequestLimitsbean. An application that supplied its ownRequestLimitsbean is refused at startup withFORBIDDEN_PRIVACY_OVERRIDE; usedataprism.limits.*.
Added
- The operator approval view (
GET /operator/approvalsandGET /operator/approvals/{id}) now carriesstatus:PENDING,APPROVED,REJECTED,CONSUMEDorEXPIRED. A storedPENDINGpast its expiry is shown asEXPIRED. dataprism.limits.max-sources,max-records,max-response-size,per-source-timeout,max-concurrencyandscope-read-budgetset the request limits that were fixed at their defaults, which are unchanged. The per-source timeout is measured from the start of the fan-out, not per call. Startup is refused withINVALID_REQUEST_LIMITSfor a zero or negative limit or a response size that does not fit in anint.dataprism.hazelcast.lock-timeout(defaultPT2S,PT100MStoPT30S; startup codeINVALID_LOCK_TIMEOUT, unconditional).dataprism.hazelcast.approval-minimum-membersand split-brain protection on the approval map. Startup codes:MISSING_APPROVAL_QUORUM(applied only when approvals are in use) andINVALID_APPROVAL_QUORUM(an explicit minimum below 1, set withjoin.mode: none, or above the counted members is refused unconditionally; an uncountabletcp-ipmember only when approvals are in use).lock-timeoutandapproval-minimum-membersare refused withCLUSTER_SETTINGS_IGNOREDundersingle-node, andapproval-minimum-membersalso beside an application-suppliedPrivacyCluster.dataprism.hazelcast.approval-sweep-interval, aDuration(defaultPT5M,PT1StoP1D) for how often each member sweeps pending approvals and approval-id claims. A value outside the range is refused withINVALID_APPROVAL_SWEEP_INTERVAL, and the key is refused withCLUSTER_SETTINGS_IGNOREDundersingle-nodeand beside an application-suppliedPrivacyCluster.- Refusal codes
TOO_MANY_RECORDS,RESPONSE_TOO_LARGE,CLUSTER_LOCK_TIMEOUTandCLUSTER_OPERATION_REFUSED. - Metrics
dataprism.privacy.source.withheldanddataprism.approval.release.skipped. docs/verifying-releases.md: how to verify the GPG signatures, SBOMs, image signatures and build provenance of a release.docs/migration-0.7.md.
Changed
- Enforce the configured record and response-size caps before returning a
context. Exceeding either refuses the whole call with
TOO_MANY_RECORDSorRESPONSE_TOO_LARGEand one correlatedDENY:<code>audit event. Equality is allowed. The size cap bounds the compact body returned to the client, including statuses and findings, and forcompare_entity_sourcesits projection; it is not a per-source limit and does not count the MCP envelope. - Audit: a refused source is recorded as
REFUSED:<code>; see Breaking. compare_entity_sources:identitytakes each field from the first answered source, in ascending key order, that holds it.dataprism.privacy.failclosedcounts refused calls only; each source withheld from a returned response is counted indataprism.privacy.source.withheld.- A call that waits for a cluster lock now refuses after two seconds by default
with
CLUSTER_LOCK_TIMEOUTinstead of waiting, including a read blocked on the shared read budget. The failed lock wait changes nothing, but steps that ran earlier in the same call are not undone: an approval consumed by admission is not restored, so a retry of an approval-gated tool after a refused read needs a fresh approval. See "What a cluster refusal leaves behind" indocs/multiple-instances.md. - A tool call that the cluster refuses an operation for returns
CLUSTER_OPERATION_REFUSED(auditedDENY:CLUSTER_OPERATION_REFUSED) instead ofOVERSIGHT_UNAVAILABLE, and a read whose cluster budget lock the cluster rejects returnsrefused: CLUSTER_OPERATION_REFUSED at $instead ofSCOPE_READ_BUDGET. Any other budget failure still refuses the read asSCOPE_READ_BUDGET. - Re-identification (request, approve and collect) answers
CLUSTER_OPERATION_REFUSEDorCLUSTER_LOCK_TIMEOUTinstead ofREIDENTIFICATION_UNAVAILABLE, and the operator API answers503with{"code":"<code>"}instead of503 OVERSIGHT_UNAVAILABLE(approve and reject; pause, resume and the state and approval reads can return them only with a customOversightStateorApprovalStore) and instead ofREIDENTIFICATION_UNAVAILABLE(the re-identification endpoints). Any other failure keeps the old code. The cause is logged once atWARNwith the cause's class name only. - A Hazelcast approval create that the cluster rejects (for example a configured
hazelcast.lock.max.lease.time.secondsbelow the lease) is refused withCLUSTER_OPERATION_REFUSEDinstead of anIllegalArgumentExceptionthat looked like a duplicate id. Two maps are added,dataprism.approval.idanddataprism.approval.pending. ClusterOperationRefusedExceptionis inio.github.aindriub.dataprism.core.refusal. It was never released, so there is no alias.
Fixed
- An approval can no longer be consumed twice across members: consumption is a compare-and-set, not a lock followed by a check.
- After a split cluster heals, an approval either side consumed, rejected or
expired stays so (a merge policy on the approval map). Previously the larger
side's
APPROVEDcould win. - A cluster lock held by another member or by something on the member port no
longer hangs request threads. Every wait is bounded and refuses with
CLUSTER_LOCK_TIMEOUT. - Hazelcast approval creation no longer relies on locks that never expire. The
pending cap per requester and the uniqueness of an approval id across scopes
hold through atomic map operations, so a creating member that stalls past its
lock lease can no longer let a requester exceed the cap or one id exist in two
scopes. Every cluster lock is leased for five times
dataprism.hazelcast.lock-timeout. Pending entries carry no TTL; a periodic sweep bounds their retention. - Every Hazelcast approval creation carries a random token that every release, removal, withdrawal, undo, takeover, sweep and merge compares, so a clean-up that belongs to one creation of an id can no longer strip the claim or pending slot of a later creation of the same id in the same scope. An approval is usable only while the claim for its id names its entry and token.
- Hazelcast approvals: approving or rejecting no longer waits for ever, or
reports failure after it has committed, when the requester's pending entry is
locked; the release is bounded and an over-count is left for the sweep. A
decision commits its new value and its expiry in one step. Ending a scope or
forgetting its approvals is bounded and refuses with
CLUSTER_LOCK_TIMEOUT, leaving the approvals it could not lock in place. The pending sweep skips keys that are locked. - The key scan in the leak validators honours the match cap and the character budget; 1,000 sensitive keys no longer give 1,000 matches.
Security
- Jackson 3 (
tools.jackson) is managed at 3.1.8, ahead of the Spring Boot 4.1.1 BOM, fixing GHSA-wv8q-qhhj-9h54, GHSA-cxp5-3px4-pw24, GHSA-q4xh-88c3-wmh7, GHSA-wjgm-6hv5-3cvf and GHSA-gx83-3vf8-gh7j in the published artifacts and images. An application that uses the Data Prism libraries with its own Spring Boot 4.1.1 BOM must importtools.jackson:jackson-bom3.1.8 or later itself, ahead of the Boot BOM, because the override does not reach a consumer's build. The consumer snippet indocs/extending.mdnow does. - The response leak validators scan property names as well as values, so a map
keyed by an email address or other identifier is refused even under a
Java-built
PASS_THROUGH_UNSAFEprofile. A flagged name is reported as<undeclared>. A key that matches no known identifier shape still passes. - For all five images (the server and the four quickstart images), each
image's manifest list and per-architecture build index are signed
keylessly with cosign, and images, jars and release assets carry GitHub
build-provenance attestations. See
docs/verifying-releases.md. - A failed audit write on an approve could leave a usable but unaudited approval,
because the undo could itself fail. Re-identification and tool-call approvals now
write
ALLOW:APPROVEDfirst and commit second, so a failed write leaves the approvalPENDING, and a decision that then does not apply is followed by aDENYwith the samecorrelationId. New metricsdataprism.approval.decision.audit_unavailableanddataprism.approval.decision.unconfirmed.
Build
- Release builds attach a CycloneDX SBOM to every module published to Maven Central and to the GitHub Release. Images are pushed with BuildKit SBOM and provenance attestations.
- Added a report-only
mutationMaven profile (PIT) and a weekly and manualmutationworkflow over the core, pseudonymisation, validation and orchestration modules; it sets no threshold and never fails a build. - Added
load-test/, a standalone load-test harness (outside the root reactor) with scenarios for 8, 32, 100 and 300 sources and aload-testworkflow. - Pin the build to Maven 3.9.16 through the Maven Wrapper; CI and release workflows now call
./mvnw. Maven 3.10.0 on the runner image broke the Central bundle. - The Central publish workflow now fails unless Maven is 3.9.x, in both the
stageandpublishjobs. - Dependabot no longer proposes moving the Maven Wrapper to Maven 3.10 or later.
0.6.0 — 2026-10-09 · 14 breaking · 6 added · 4 changed · 2 fixed · 4 build
Breaking
0.6.0 is a clean break. There are no deprecated overloads and no forwarding
types at the old names, because there are no external users to carry through a
deprecation cycle. Every consumer recompiles and changes imports. The full
old-to-new tables, grouped by who has to act, are in docs/migration-0.6.md.
- The
corepackage split. The root packageio.github.aindriub.dataprism.coreis now empty and its 34 types moved tocore.spi,core.model,core.engine,core.refusal,core.limitsandcore.metrics.core.spiis the extension-facing package: it holds the interfaces an adapter or integration implements (DataSourceAdapter,IdentityResolver,FieldMetadataResolver,ScrubbingEngine,SecretKeyProvider,SyntheticValueSource,ValueTokenSource,EntityCorrelationService) and the request types they exchange (DataRequest,SourceCallContext), plusPassThroughIdentityResolver. - The
auditpackage split. The contract stays inio.github.aindriub.dataprism.audit, and 15 types moved toaudit.format,audit.sink,audit.checkpoint,audit.retentionandaudit.verify. - Operator-visible: the verifier command changed. The main class is now
io.github.aindriub.dataprism.audit.verify.AuditChainVerifierCli. The old nameio.github.aindriub.dataprism.audit.AuditChainVerifierClino longer exists, so any script, cron job or runbook that runsjava -cp <classpath> io.github.aindriub.dataprism.audit.AuditChainVerifierClifails until the name is changed. Arguments and exit codes are unchanged. - The MCP tool constructors, the
DataPrismMcpServerfactories,ContextRequestandSourceFanOutlose their overloads.GetEntityContextToolandCompareEntitySourcesToolhave one constructor each,DataPrismMcpServer.stdioandstreamableHttphave one method each, and they take aToolOptionsrecord (admission, fingerprinter, correlation requirement, MDC, audited entity types). There is no default admission: namenoAdmission()or a real policy.SourceFanOuttakes aSourceFanOutOptions, andContextRequestkeeps its canonical constructor,ofandcomparison. A request built through the removed shorter constructors used to derive its audited entity type from the shape fallback;ofandcomparisonaudit<unregistered>. - The nested
DataPrismPropertiestypes are now 13 top-level classes inio.github.aindriub.dataprism.spring.boot(TransportProperties,SecurityProperties,AuditPropertiesand so on).DataPrismContractValidatoris now public inspring.boot.validation, beside the newDataPrismPropertiesValidator. JwtDecoderSupportandJwtCallerContextExtractormoved toio.github.aindriub.dataprism.spring.boot.jwt.DataPrismAutoConfigurationkeeps its FQCN but no longer declares beans. It imports 14 package-private configuration classes. Operators who set log levels on...DataPrismAutoConfiguration$AuditSinkSelectionor...DataPrismAutoConfiguration$JsonProjectionmust useio.github.aindriub.dataprism.spring.boot.AuditSinkSelectionandio.github.aindriub.dataprism.spring.boot.JsonProjection, and the logger ofJwtCallerContextExtractormoved with its package. The bean names of the configuration classes themselves changed with their class names.- Jackson 3. The classpath is Jackson 3 (
tools.jackson) and the Jackson 2 artifacts are banned in this build;jackson-annotationsstays. The MCP SDK binding ismcp-json-jackson3(wasmcp-json-jackson2) and the Spring side isspring-boot-starter-jackson(wasspring-boot-jackson2). Public signatures that carried Jackson 2 types now carry Jackson 3 ones:SourceTree(andSourceTree.textnow returnsStringNode),ScrubResult,Generalizer,ContextResponse,ComparisonResponse.identity,LlmResponseValidator.validate,RawValueLeakValidator.validate,SensitivePatternValidator.validateandSensitiveDataScanner.scan.DataPrismObjectMapperand itscreate()are no longer public, and the two tool constructors no longer take anObjectMapper. - Source models must be records. A
DataSourceAdapterresponse type that is not a record, or that holds a bean, a user collection or map subclass, or a class-level serializer on a non-record at any depth, is refused withSOURCE_MODEL_NOT_A_RECORD(at startup for declared types, at request time for the actual object graph). Records are read by their components only.java.timevalues andOptionalare now accepted:java.timeis written as ISO-8601 text and anOptionalis unwrapped. Jackson 2 refused both. - The YAML configuration readers (
SecurityPolicy,PrivacyProfiles,ModelDescriptors,RestSources,VocabularyRegistryandConfiguredJsonSources) refuse at startup what 0.5.x accepted, so a configuration that loaded on 0.5.x may now refuse to start. The nine codes areDUPLICATE_CONFIG_KEY,UNKNOWN_CONFIG_KEY,NON_STRING_CONFIG_SCALAR,INVALID_CONFIG_BOOLEAN,LEADING_ZERO_CONFIG_NUMBER,TRAILING_CONFIG_CONTENT,UNSUPPORTED_CONFIG_YAML(aliases, anchors and tags),INVALID_CONFIG_SHAPEandNULL_LIKE_CONFIG_SCALAR. A present but null value is refused, and an emptytls:is refused where it used to disablerequireHttps. Quoting rule: a value in a string-typed field that YAML reads as a number or boolean, such as1.0,1e3,42ortrue, must be quoted. Messages name the key and its path (a key is cut to 64 characters) and never the value. - YAML is now parsed as YAML 1.2 (Jackson 3).
yes,no,on,off,True,FALSEand any other spelling than lower-casetrue/falseare text, not booleans: in a boolean-typed field they are refused asINVALID_CONFIG_BOOLEAN, and in a string-typed field they now load as the text you wrote, where 0.5.x loadedtrueorfalse. Leading-zero numbers are no longer octal: they are refused in numeric fields (band bounds, vocabulary versions) asLEADING_ZERO_CONFIG_NUMBER, and in a string-typed field010now loads as the text010, where 0.5.x loaded8. Neither string-field change is refused, so check string fields that held such a value. - Error messages no longer echo configured URLs or credentials
(
ConfiguredJsonSources,RestSources,RestSourceand the json-sources initializer). A config location in a message is shown as a placeholder, a local path cut at?or#, orscheme://host[:port]. - A JSON audit projection (
dataprism.audit.output.json-directory) now needs the built-in hash-chained sink. An applicationAuditSinkbean combined withdataprism.audit.sink=hash-chainedand ajson-directoryrefuses to start withAUDIT_JSON_PROJECTION_WITHOUT_BUILT_IN_SINK, and ajson-directorywith no registered projection refuses withAUDIT_JSON_PROJECTION_MISSING. - A tool call whose response cannot be converted for the MCP result now returns "the response could not be serialised"; on 0.5.x a failed conversion fell through to "the request could not be completed".
Not changed: the dataprism.* property names, the
io.github.aindriub.dataprism.spring.boot.DataPrismAutoConfiguration FQCN and
its entry in AutoConfiguration.imports, the existing refusal codes, the audit
record format (recordVersion 3) and the dataprism.audit logger name.
Added
- Added AuditEventListener, a read-only SPI called after the configured audit sink accepted each event, on one bounded asynchronous dispatcher (dataprism.audit.listeners.queue-capacity, default 1024). It is best effort: events are dropped for listeners only when the queue is full, and the audit log is unaffected. Failures are logged by class name only as AUDIT_LISTENER_FAILED, and drops as AUDIT_LISTENER_DROPPED.
- Shutdown of the audit listener dispatcher is bounded. On close it drains queued events for up to 5 seconds, then counts and logs the rest as dropped and interrupts the dispatcher. The final drop report is written on a short-lived thread joined for at most 1 second, so a blocked listener or appender cannot hang shutdown, and closing from an already interrupted thread still drains, joins and restores the interrupt flag.
- The data-prism-spring-boot-autoconfigure jar now ships Spring configuration metadata (META-INF/spring-configuration-metadata.json), so IDEs complete and describe dataprism.* keys in YAML. The processor runs only at build time and is not a dependency, and a build test keeps the metadata and docs/configuration.md in step.
- New startup refusal codes, each failing closed:
INVALID_AUDIT_LISTENER_QUEUE_CAPACITY(dataprism.audit.listeners.queue-capacitybelow 1),AUDIT_EVENT_LISTENERS_NOT_REPLACEABLE(a second dispatcher bean),SOURCE_MODEL_NOT_A_RECORD,AUDIT_JSON_PROJECTION_WITHOUT_BUILT_IN_SINK,AUDIT_JSON_PROJECTION_MISSINGand the nine YAML codes listed under Breaking. - The offline verifier reports a damaged checkpoint line as the anomaly
TORN_CHECKPOINT_LINE(exit code 4), never as a break. TeeAuditSinkisCloseable. It closes the JSON projection and then the primary sink, and closes the primary even if the projection close throws.
Changed
get_entity_context'ssourcesentries are now in ascending key order (the alias, or the real name for a caller holdingEXPOSE_SOURCE_NAMES), where before the order varied between JVM runs.ToolAdmission.none()returns one shared instance, andToolOptions.defaults().admission(ToolAdmission.none(), null)is accepted without a fingerprinter. Any other admission still needs one.- The JSON audit projection is its own bean,
dataPrismJsonAuditProjection, classifiedPRIVACY_CRITICALwithCOMPETING_BEAN_REFUSAL. It is deliberately not anAuditSink, because that made injecting anAuditRecorderambiguous. The hash-chained sink bean resolves the projection before it opens the authoritative file, and closes the authoritative file again if the tee cannot be built. INVALID_MODEL_DESCRIPTOR_FILEnow chains a value-free cause when the reader's message carries its own code (for exampleUNKNOWN_CONFIG_KEY), so the operator learns which rule the file broke. Uncoded, enum and I/O messages are not chained.
Fixed
TeeAuditSinknow poisons itself on anyThrowablethrown by the JSON projection after the primary write, including anErrorand a sneakily thrown checked exception, and rethrows the original unchanged. Before, only aRuntimeExceptionfrom the projection did.- A resumed checkpoint writer now terminates a torn tail (a checkpoint file not
ending in a newline) with
\r\nand fsyncs before its first append, and fails the open if it cannot. The verifier reads such a line, or a final unterminated chunk that is not a checkpoint, asTORN_CHECKPOINT_LINErather than failing, and still uses the intact checkpoints. Any other unparseable checkpoint line is still unreadable input (exit code 1).
Build
ci-gateis a single summary check inbuild.ymlthat is green only when the build matrix andcontainer-smokeall succeeded. The Pages build job is renameddocs-site.build.ymlnow builds the Javadoc with the release profile (unsigned, no secrets) on every pull request, on the JDK 21 leg. 0.5.0 would otherwise have found a Javadoc error only at publish time, becauserelease.ymlruns a plainmvn verify.spring-boot-configuration-processorruns on the autoconfigure module's annotation-processor path only.maven-dependency-pluginis managed at 3.11.0.- ArchUnit now locks the new layout: an empty
coreroot,core.spinot depending oncore.engine, acycliccoreandauditsubpackages,auditandoversightunder the core outer-layer rule,validationandjwtindependent of each other, and rules for obtained mappers and streaming JSON factories.
0.5.0 — 2026-10-08 · 1 breaking · 8 added · 4 changed · 2 fixed · 1 upgrade notes
Breaking
- The starter and auto-configuration now require Spring Boot 4.1 (4.1.1, Spring Framework 7.0.9). Spring Boot 3 users stay on 0.4.x. The library jars still run on Java 21 or newer.
Added
- Inbound correlation id. A validated external correlation id can arrive on
MCP tool calls through a configured HTTP header
(
dataprism.correlation.inbound.*). The strict default pattern accepts only a UUID, 16 to 128 hex characters containing at least one letter a to f, or a W3Ctraceparent. Digit-only values, such as card numbers, are refused. A broader pattern is available only by explicit configuration. It is off unless a header is set. - Outbound correlation header. REST sources send the id as a header. It is off unless configured, and a call without an id sends nothing.
dataprism.correlation.outbound.header, an optional global outbound header. A per-sourcecorrelation-headeroverrides it.dataprism.correlation.mdc-key, unset by default. When set, it puts only the validated id into the SLF4J MDC on the tool handler thread and on each fan-out task, and clears it afterwards. Warning: whatever the inbound pattern admits then appears in every log line written on those threads, so keep the pattern to generated ids.- A JSON/ECS audit projection, written through a tee, with
dataprism.audit.output.*configuration (field preset, field names, routing constants and an optional NDJSONjson-directory). The hash-chained.logstays authoritative and the JSON is a separate projection. ECSevent.outcomeis derived frompolicyDecision, and the raw decision is kept beside it. dataprism.audit.entity-types, an optional list of the entity types that may appear verbatim in an audit record.- New startup refusal codes, each failing closed:
INVALID_CORRELATION_FORMAT,INVALID_CORRELATION_PATTERN,CORRELATION_PATTERN_NOT_APPLICABLE,INVALID_CORRELATION_HEADER,CORRELATION_REQUIRED_WITHOUT_HEADER,CORRELATION_REQUIRES_HTTP_TRANSPORT,INVALID_CORRELATION_MDC_KEY,CORRELATION_MDC_KEY_RESERVED,CORRELATION_MDC_KEY_WITHOUT_HEADER,INVALID_AUDIT_FIELD_PRESET,INVALID_AUDIT_FIELD_PATH,UNKNOWN_AUDIT_FIELD,AUDIT_FIELD_MAPPING_CONFLICT,INVALID_AUDIT_ROUTING_VALUE,AUDIT_JSON_REQUIRES_SEGMENTED_SINK,AUDIT_JSON_DIRECTORY_SAME_AS_AUDIT,AUDIT_JSON_DIRECTORY_UNUSABLEandINVALID_AUDIT_ENTITY_TYPE. - New runtime codes, which are not startup refusals: a call refused and audited
before any source is called with
EXTERNAL_CORRELATION_ID_REQUIREDorEXTERNAL_CORRELATION_ID_INVALIDwheninbound.requiredis true;EXTERNAL_CORRELATION_ID_DROPPEDlogged at WARN when an invalid inbound id is dropped and the call proceeds without one; andAUDIT_PROJECTION_FAILED, which fails later audited calls closed after a JSON projection write fails, until the process restarts. The verifier reportsFIELD_COUNT_MISMATCHandINTERRUPTED_WRITE_FRAGMENTas anomalies, not as startup refusals.
Changed
- The published images run Java 25. Library bytecode is still Java 21
(
--release 21, class major 65). - Audit record version 3. Records are written as
recordVersion3 with 25 fields, andexternalCorrelationIdis inside the hash. Version 1 and version 2 records still verify. The offline verifier reports a line with too many fields, or a full-length line that declares the wrong version, asFIELD_COUNT_MISMATCH, a break (exit code 2). A shorter line that could be a torn write is reported as an interrupted write (exit code 4), not a break. - The audited
entityType.entityTypeis audited verbatim only when it is a registered entity type or an upper-case-identifier-shaped token; otherwise it is recorded as<unregistered>. With no list configured, an upper-case token still passes, so setdataprism.audit.entity-types. - Build and tests. Test ports are claimed by an OS file lock and probes run sequentially, to stop port-race flakes. Dependabot version updates are grouped into one pull request per ecosystem per week, and Java image majors and non-LTS Java 26 and later are ignored.
Fixed
- An interrupted audit write is no longer reported as tampering after a
restart. A writer that opened a file or segment ending in an unterminated
fragment appended its first record onto that same line, and the verifier
reported the over-long line as
FIELD_COUNT_MISMATCH(exit code 2). A resumed writer now writes\r\nand fsyncs before its first record when the last byte is not a newline. It never truncates or rewrites existing bytes, and it fails the open (AUDIT_SINK_OPEN_FAILED) if it cannot do so. The verifier reads a line ending in a raw carriage return, which a serialized record never contains, as anINTERRUPTED_WRITE_FRAGMENT(exit code 4) and does not parse it. Directory mode uses the same terminator. The rule that an over-count line with a parseable version is a break is unchanged. One live writer per file or segment directory is assumed. - The configured outbound correlation header (including
traceparent) is now removed from every outbound REST request, so a client-level default or preset header can no longer send an unvalidated value when no valid id is present.
Upgrade notes
- Logs written before 0.5.0, including every 0.4.x release, can already hold a
torn fragment fused with a restarted writer's first record on one line. The
0.4.x verifier mostly reported these as
INTERRUPTED_WRITE_FRAGMENT(exit code 4). The 0.5.0 verifier reports them asFIELD_COUNT_MISMATCH(exit code 2), and retention stops there. To check, see whether the trailing 20, 24 or 25 fields of the line parse as aGENESISrecord of a newinstanceId, and whether the record after the line continues that writer. If so it is a legacy interrupted write, not an edit.
0.4.1 — 2026-10-07 · 2 breaking · 3 security · 6 added · 1 fixed · 2 known limitations · 3 upgrading starter consumers to hazelcast 5.7.0
Real, explicit clustering. In 0.4.0, topology: embedded started a bare
Hazelcast member that was never configured to join anything, so separate
instances never shared pause, approvals, the read budget or rate limits, even
though the docs said they did. This release lets members join one cluster by
explicit configuration, refuses to start without it, and says plainly what is
and is not shared and protected. Member traffic is not encrypted and members
do not authenticate each other: open-source Hazelcast has neither.
Breaking
topology: embeddednow refuses startup unlessdataprism.hazelcast.cluster-nameanddataprism.hazelcast.join.modeare set (MISSING_CLUSTER_NAME,MISSING_CLUSTER_JOIN), and the cluster name must not bedev(RESERVED_CLUSTER_NAME). The migration for a single member is one line:dataprism.hazelcast.join.mode: none(plus acluster-name), an explicit single member bound to loopback that neither discovers nor accepts other members. A multi-instance deployment choosestcp-iporkubernetesinstead. 0.4.0embeddedmembers never clustered, so no working multi-instance deployment breaks; only the configuration needs editing.dataprism.hazelcast.tls-key-referenceandtls-trust-referencenow refuse startup withHAZELCAST_TLS_UNSUPPORTED. They were read by nothing in 0.4.0.
Security
- Hazelcast auto-detection, multicast and phone-home are always disabled, so a
member can no longer join an unrelated cluster named
devby discovery.PrivacyClusteralso refuses an enabled advanced network config and an application-supplied instance that has auto-detection or multicast on (UNSAFE_HAZELCAST_DISCOVERY). ApplicationPrivacyClusterbeans are validated, and cluster properties set beside one refuse withCLUSTER_SETTINGS_IGNORED. - Member TLS is unsupported, because open-source Hazelcast 5.7.0 has no member
TLS engine and no member authentication. Member traffic is not encrypted.
Network isolation (a private network, a
NetworkPolicyor an mTLS mesh) is the deployer's responsibility, andmember.interfaceshould pin the member to that network. Without it, atcp-iporkubernetesmember binds every interface. - The server ships, and the starter declares, Hazelcast 5.7.0. 0.4.0 shipped 5.5.0, because Spring Boot's dependency management overrode the declared version; the root pom now pins 5.7.0 ahead of the Boot BOM. A starter consumer must pin it too (see "Upgrading starter consumers to Hazelcast 5.7.0" below). 0.4.0's multi-member behaviour was only ever exercised on 5.5.0.
Added
dataprism.hazelcast.cluster-name,join.mode(tcp-ip,kubernetesornone),join.members,join.kubernetes.namespace,join.kubernetes.service-nameorservice-dns,member.port(default 5701) andmember.interface, with startup refusals for missing or invalid values and for a member port shared with another listener (CLUSTER_PORT_SHARED).docker/multi-instance/: a two-member Docker Compose example and a Kubernetes manifest (DNS mode first; API mode, with its own ServiceAccount and Role, commented).- A new "multiple instances" page,
docs/multiple-instances.md, covering what members share, what they do not, and the residual risk that losing an entry's owner and its backup together loses the entry (backup count is 1). EXPOSE 5701in the server image. It is for private networks only; do not publish it, for example withdocker run -P.server.jsongains the cluster variables (DATAPRISM_HAZELCAST_CLUSTERNAME, join and member settings) and the operator variables (DATAPRISM_OPERATOR_*).- A multi-member test that runs real server members and checks pause, approvals, the read budget, rate limits and re-identification are shared, and that refusals survive losing the member that owns the pause key.
Fixed
- The docs claimed cluster-wide sharing, member TLS and member validation that did not exist. The configuration, EU AI Act support, architecture and re-identification pages now say what clustering shares and what it does not.
Publication
0.4.1 publishes the library modules to Maven Central (not data-prism-server),
the quickstart images and the server image (ghcr.io/aindriub/data-prism-server)
to GHCR, and the server entry to the MCP Registry.
Known limitations
- Anyone who can reach the member port can read and write cluster state.
They can read subject ids, forge an APPROVED approval and bypass four-eyes,
lift a pause by deleting its flag, and reset read budgets and rate
limits. Member traffic is neither authenticated nor encrypted on open-source
Hazelcast, so isolating the member port is the deployer's job. This includes a
plain Hazelcast client configured with the cluster name: it can connect to a
tcp-iporkubernetesmember's port and read or write maps. Anonemember refuses both members and clients. - The Docker Compose example's
clusternetwork is not a security boundary. The members bind and advertise only their cluster address, but OrbStack was observed to route across Docker networks, so a container on another network reached port 5701 on that address. Docker Desktop may behave the same (not tested). Linux Docker's network isolation is expected to block it (not tested here). Isolate 5701 with a host firewall, aNetworkPolicyor a private network in production.
Upgrading starter consumers to Hazelcast 5.7.0
The starter declares Hazelcast 5.7.0, but Spring Boot's dependency management in your build overrides that declaration, so a starter consumer gets 5.7.0 only by saying so:
- With
spring-boot-starter-parent: set<hazelcast.version>5.7.0</hazelcast.version>in your<properties>. - When importing
spring-boot-dependencies: add your own<dependencyManagement>entry forcom.hazelcast:hazelcast:5.7.0. The property override does nothing for an imported BOM. - With Gradle and the Spring dependency-management plugin:
ext['hazelcast.version'] = '5.7.0'.
See "Using the starter" in docs/multiple-instances.md.
0.4.0 — 2026-10-06 · 10 breaking and behaviour changes · 7 added · 3 changed · 2 security
Human oversight, a separate operator surface, audited re-identification, and an audit trail with daily segments, external checkpoints and retention. These features support an operator's work toward the human-oversight, record-keeping and special-category obligations in the EU AI Act and GDPR Art. 9; they do not by themselves establish that a deployment meets those obligations. Several defaults and refusals change (see "Breaking and behaviour changes").
Breaking and behaviour changes
- Release note (task 94): a PHI rule weaker than
REDACTnow refuses to start withSPECIAL_CATEGORY_EXPOSED, and a Java-built profile with no PHI rule resolves PHI toREMOVE. Seven GDPR Art. 9 special-category classifications fail closed. Check any custom profile before upgrading. - All denials are recorded in the audit trail as
DENY:<code>, from both the orchestrator and the MCP tools. A malformed code is recorded asDENY:INVALID_REFUSAL_CODE. Anything that filters audit records on the old denial value must change. - Audit record version 2: the hash is computed over an unambiguous, length-prefixed encoding and the record carries per-field dispositions and approval identity. Version 1 records still verify.
- A configured audit retention below 6 months refuses startup unless
dataprism.audit.retention-overrideis set. - Audited calls are refused with
AUDIT_CHECKPOINT_UNAVAILABLEwhile an audit checkpoint cannot be written. ContextOrchestrator.buildContextnow throwsAuditedRefusalException(aPrivacyRefusedException) with codeREQUEST_FAILEDfor audited internal failures. Starter users who mapPrivacyRefusedExceptionto 403 should checkcode().- The MCP server always enforces admission (pause, rate limit, approval).
Four-eyes defaults to on for re-identification
(
dataprism.reidentification.four-eyes=true). Tool calls need an approval only when the tool is listed indataprism.oversight.approval-required-tools, which is empty by default. There are at most 5 live pending approvals per requester and kind, by default (TOO_MANY_PENDING); the oversight and re-identification caps are configured separately. data-prism-servernow excludes Spring Boot'sHazelcastAutoConfiguration. This is visible only with ahazelcast.xmlorhazelcast.yamlon its classpath.- Refusal codes returned by application scrubbers, validators and resolvers are
validated before they reach the client; a malformed one becomes
INVALID_REFUSAL_CODE. - The MCP
serverInfoversion is read from the build rather than hardcoded.
Added
- Human oversight: pause (cluster-wide with Hazelcast), per-caller rate limits, approvals bound to the call's purpose, profile, client and capabilities, and four-eyes approval, with in-memory and Hazelcast implementations.
- The operator surface, served on
dataprism.operator.portin the same process with its own filter chain; the MCP and operator endpoints never share a port. It carries audited pause, approvals and re-identification. - The
data-prism-reidentificationmodule and index: audited, purpose-bound re-identification with optional four-eyes approval, wired over the application's synthetic value source. - External audit checkpoints (BOOT, PERIODIC and SHUTDOWN) and audit retention
that purges expired segments behind
RETENTION_ANCHORcheckpoints; the verifier gains directory mode and--checkpoints, which exits 5 on tail truncation or a missing boot. - Opt-in daily audit segments named
audit-YYYY-MM-DD.log, written whendataprism.audit.directoryis set.dataprism.audit.file-pathstill writes a single file. - Per-field dispositions in the audit record, and a
correlationIdreturned in the MCP tool result_meta. - The EU AI Act and GDPR Art. 9 support page, mapping what the project
supports to each provision, and a table of every
policyDecisionform indocs/audit.md.
Changed
- Undeclared payload keys render as
<undeclared>and bracketed indices as[*]on every refusal and warning sink; undeclared property names never reach the model (<undeclared-N>underREDACT_AND_WARN, dropped underDROP_AND_WARN) and are not scanned by validators. InMemoryApprovalStorenow refuses non-pending, duplicate and null-approver approvals exactly as the Hazelcast store does.- Integration tests derive the artifact version from the build.
Security
- Special-category (Art. 9) data fails closed: see the release note above.
- Refusal paths no longer leak undeclared payload keys, and refusal codes from application code cannot carry free text to the client.
Publication
0.4.0 is published to Maven Central (the library modules; data-prism-server
stays off Central and comes from source, a GitHub Release or GHCR), to GHCR,
and to the MCP Registry.
0.3.1 — 2026-09-24 · 3 added · 3 changed · 1 fixed
No runtime behaviour changed on the server. This release refreshes the docs site and the MCP Registry listing, and corrects several documentation and CHANGELOG claims found while writing them.
Added
- A MkDocs Material docs site published from the existing user docs, with an identity (mark, favicon, palette, landing page), six concept diagrams embedded in context, and dedicated FAQ, comparison and three intent use-case pages.
- A developer guide overview with tutorials for writing an adapter and
writing a custom
IdentityResolver, both checked against the code they document, plus an exampleIdentityResolverand its unit tests indata-prism-quickstart-extension. websiteUrland the current canonical tagline in the MCP Registryserver.jsonentry, so the next registry publish carries them instead of the stale v0.3.0 description and missingwebsiteUrl.
Changed
- The changelog page on the docs site now collapses every release but the
latest, without editing
CHANGELOG.mditself. - CHANGELOG: corrected the 0.3.0
AuditChainVerifierentry, which attached "caught even on the last record" to deletions as well as edits; only edits are caught at a chain's tail, a deletion is caught only when a later record follows it (seedocs/audit.md). - Adopted a new canonical project description in
README.mdand the rootpom.xml, replacing v0.3.0's "A privacy layer between MCP clients and enterprise APIs.": it now says Data Prism pseudonymises personal data per privacy scope and refuses anything unclassified. The same text is the abstract of the newCITATION.cff.
Fixed
docs/tools.md's claim that an unclassified field is "dropped"; the shippedDEFAULT/STRICTprofiles both refuse the whole request instead.
Not published to Maven Central; the Maven artifacts remain at 0.3.0.
0.3.0 — 2026-09-23 · 6 added · 3 changed · 3 fixed · 4 behavioural changes · 1 not changed
Nested JSON catalogues, one level deep, and a durable, hash-chained audit trail — protect a real API without writing Java, and prove what happened.
Added
- The configuration-driven JSON REST connector gains one level of named
nested catalogues: a field can declare
nested: <name>, pointing at an entry in a top-level nested-catalogues map whose own leaves may benonSensitiveor classified only — a nested leaf carries no identifier of its own (it inherits its subject from the enclosing record), soidentifier: trueand a furthernested:are both refused there. No dotted paths, no JSONPath, no wildcard descent, no inferring structure from the wire —subject-json-pathis untouched and a nested object never carries its own subject. A response nesting deeper than declared — a leaf the catalogue says is a scalar turning up as a structure — refuses with the new, distinctNESTED_LEAF_NOT_SCALARcode rather than falling through to core's genericUNCLASSIFIED_STRUCTURE; the mirror case, a declared structure that turns up as a scalar, refuses withNESTED_FIELD_NOT_STRUCTURED. - A durable, append-only, hash-chained audit sink. Selecting
dataprism.audit.sink: hash-chained, alongside the now-requireddataprism.audit.file-path, produces aFileAuditSinkbean: one file, fsync per record, no rotation. Omitting the file path refuses at startup withMISSING_AUDIT_FILE_PATH; a path this process cannot open refuses withAUDIT_SINK_FILE_UNUSABLE, instead of degrading silently to no auditing. The canonical audit-record hash covers nineteen fields, includingtimestampandsourceSystems. - An offline
AuditChainVerifierCLI replays a hash-chained audit file's writers from a copy and reports one of five outcomes: exit 0 intact; 1 unreadable input; 2 a detected break — an edit inside one writer's chain, caught anywhere including that chain's own last record, or a deletion caught only when a later record follows it in the same chain; 3 the final record has no terminating newline, reported as possibly in flight, which is proof of neither health nor tampering; and 4 a structural anomaly (an interrupted-write fragment, a duplicate sequence, or a chain not starting atGENESISimmediately after another anomaly), never returned together with a break. Exit codes 3 and 4 name shapes ordinary operation can also produce; neither rules out tampering. The verifier cannot detect truncation of a writer's most recent records at all — a file with its tail removed verifies intact at exit 0, because nothing remains in the file to disagree with — and the same blind spot extends to a whole process boot: deleting every record of one boot leaves the surviving writers reporting intact and never mentions the deleted one, indistinguishable from that boot never having run. - An opt-in
dataprism.identity.resolver: pass-throughproperty selectsPassThroughIdentityResolver, refusing an unrecognised value withUNSUPPORTED_IDENTITY_RESOLVER; an application-suppliedIdentityResolverbean still wins. Combined with the configuration-driven JSON REST connector, this makes protecting a flat JSON API genuinely possible with no Java class. A SpringFailureAnalyzernow renders everyDataPrismConfigurationExceptionas an operator-facing block naming the refusal code, what to supply, and the two docs pages that explain it,docs/configuration.mdanddocs/quickstart.md, with no stack frame. A new walkthrough,docs/protect-your-own-api.md, takes a reader with a flat JSON REST API from nothing to a pseudonymised MCP response using only YAML. - A one-command demo,
examples/quickstart-demo/run.sh, drives the running Compose quickstart end to end over its real MCP transport (obtaining a token, handshaking over SSE-framedtools/callbodies, and printing a pseudonymised response), extracted out of the CI smoke test so a reader can run the same thing locally. - The four Compose quickstart images (server-with-extension, fixtures,
issuer, certs-init) are now published as multi-architecture GHCR
manifests;
docker compose uppulls them by default, with the from-source build path moved todocker compose -f compose.yaml -f compose.build.yaml up --build.
Changed
data-prism-exampleis renamed todata-prism-integration-tests: it hosts 11 integration test classes with no duplicate elsewhere, includingPiiLogScanTest, the sole enforcement of privacy rule 7, and was never a demo. Packageio.github.aindriub.dataprism.exampleandExampleApplicationare unchanged. Sixdata-prism-examplestrings survive deliberately insidedata-prism-integration-tests— its own JWTissuerand auditwriter-idconfig values, and the tests asserting on them — because they are observable audit output, not a module identifier.- A configured JSON source's
DataSourceAdapterbean no longer needs a matchingdataprism.sourcesentry:DataPrismContractValidator's cross-check betweendataprism.sourcesand the supplied adapters now requires only thatdataprism.sourcesbe a subset of the supplied adapters, not an exact match, so a source whose transport lives entirely in its own JSON catalogue can supply an adapter with no correspondingdataprism.sourcesentry. EveryDataSourceAdapterbean still has to earn its way onto the review allow-list, though: it must be named bydataprism.sourcesor supplied by the JSON-catalogue mechanism (the catalogue's own source names), or startup refuses with the stable codeUNREVIEWED_SOURCE_ADAPTER— an adapter bean present on the classpath for neither reason is not implicitly approved. - Documentation reconciled against the shipped code rather than the plan that
preceded it: two consumer guides,
docs/extending.mdanddocs/tools.md, are now linked fromREADME.md,docs/quickstart.mdanddocs/agents/README.md; every stale "one tool" claim across those files anddocs/architecture.mdis corrected to name both shipped tools,get_entity_contextandcompare_entity_sources;docs/architecture.mdnow attributesArchitectureTesttodata-prism-architecture, the module that hosts it, instead of the renamed module; andREADME.md's "Until Task 20 delivers…" claim is replaced — the configuration-driven JSON REST mode shipped as the publisheddata-prism-connectors-restartefact, self- registering via Spring'sAutoConfiguration.importsand requiring no Java.
Fixed
AuditRecorderno longer advances its in-memorypreviousHashuntil the sink'srecordcall actually succeeds. Previously, a throwing sink still left the chain head pointing past an event that was never durably written, so the next successful write chained against a hash for a record that does not exist; a throwing sink now rolls the recorder back to byte-identical prior state instead.AutoConfiguredBeanClassificationTest's sweep now walks@Imported and nested configuration classes recursively, closing a gap that let a bean be placed specifically to dodge classification.- A sink failure raised while recording an audit event no longer reaches the
MCP client carrying its own exception text.
GetEntityContextToolandCompareEntitySourcesToolnow catch only the audit-record failure and rethrow it asAuditUnavailableException, exposed to the client as the stable codeAUDIT_UNAVAILABLEwith no text derived from the caught exception — closing a path by which a hash-chained sink's failure could disclose the server-side audit file's path to the MCP client. The response is still refused, exactly as before; only what the client is told changed.
Behavioural change for API consumers
HmacSyntheticGenerator's discriminator widens from 20 bits (masked out of a single 4-byte digest word, four Crockford base32 characters) to 40 bits (eight distinct digest bytes, eight characters), andADDRESS— which previously rendered no discriminator at all — now carries one like every other namespace. Every pseudonym this generator produces changes as a result; a pseudonym stored or compared under 0.2.0 will not match the one produced under 0.3.0 for the same input.PseudonymisationVersionnow rejects a MAC algorithm whose digest is too short for the generator's own reads at construction time, with the stable codepseudonymisation.algorithm-digest-too-short, rather than surfacing anArrayIndexOutOfBoundsExceptionlater;HmacMD5andHmacSHA1are both now rejected, so an operator configured with either must move to an algorithm whose MAC output is at least 24 bytes. This reduces collision probability substantially; it does not make collisions impossible, and no such claim is made.AuditRecordernow derivesinstanceIdas<writer-id>/<per-boot random UUID>instead of the writer-id alone, so a restart under the same writer-id is reported as a new writer starting atGENESISrather than a false chain break.instanceIdvalues recorded before this change are not comparable to ones recorded after it. A writer-id containing/is now refused at startup with the new codeINVALID_AUDIT_WRITER; rename any writer-id that contains a/before upgrading.- Configuring
dataprism.audit.sink: approved-sinkwith no matchingAuditSinkbean now refuses with the new codeAUDIT_SINK_BEAN_REQUIREDinstead of the genericMISSING_AUDIT_SINK, which is retained unchanged for the separate case of an absent or blankdataprism.audit.sinkproperty. Anything keyed on the old code for the bean-absent case must switch to the new one. - In 0.2.0,
DataPrismContractValidatorcompareddataprism.sourcesand the suppliedDataSourceAdapterbeans for exact equality, so an adapter bean present on the classpath with no matchingdataprism.sourcesentry refused withUNRESOLVED_SOURCE_ADAPTER. In 0.3.0 that same case — an adapter named by neitherdataprism.sourcesnor the JSON-catalogue mechanism — refuses instead with the new codeUNREVIEWED_SOURCE_ADAPTER(see the Changed entry above);UNRESOLVED_SOURCE_ADAPTERis retained, unchanged, for the other direction: adataprism.sourcesentry with no adapter bean supplied for it. Anything keyed on the old code for the adapter-present case must switch to the new one.
Not changed
- What the hash-chained audit trail's tamper-evidence covers, stated
precisely: an edit of a record inside one writer's chain, caught anywhere
including that writer's own last record, and a deletion of a record inside
one writer's chain, caught only when a later record follows it. It does
not cover truncation of a
writer's most recent records, or deletion of an entire process boot's
records — both are undetectable from inside the file alone (see the
verifier entry above).
AuditEventHashis also unkeyed SHA-256, so anyone with write access to the audit file can recompute the whole chain after tampering with it; the trail does not resist an operator, or anyone else who already has that access. Nothing here should be read as, or later restated as, a claim that the audit log is tamper-proof, immutable, or independently complete.
0.2.0 — 2026-09-17 · 1 added · 1 changed · 1 behavioural change · 1 not changed
A second MCP tool, compare_entity_sources, and no other new feature.
Added
compare_entity_sources, the second MCP tool: per-fieldidentityplus findings over the same correlated, scrubbedContextResponseget_entity_contextalready builds. Findings report agreement, disagreement (INCONSISTENT/FORMATTING_ONLY/ABBREVIATION) andMISSING_IN_SOME_SOURCES, each distinguishable by an explicit discriminator rather than by absence, so a caller can tell "compared and consistent" from "never compared".
Changed
- The MCP registry namespace is corrected to
io.github.AindriuB/data-prism, matching the casing the registry actually grants for the GitHub login. Maven Central'sio.github.aindriuband the GHCR pathghcr.io/aindriub/data-prism-serverare different identifiers, each correct in its own system and untouched by this correction — do not "fix" the casing inconsistency between them; doing so would break two already-published artifacts. No MCP registry listing for this server exists yet; this corrected namespace, and the label it is checked against, is what the first successful publish will use.server.json's OCI package reference is also corrected to the canonical form the registry requires: noregistryBaseUrl, andidentifiernow carries the image tag directly (ghcr.io/aindriub/data-prism-server:0.2.0) rather than a bare path paired with a separateversionfield.
Behavioural change for API consumers
ContextResponsegained a new record component (fieldsByNamespace), so itsequals,hashCodeandtoStringnow include it. This is not a linkage break — binary compatibility was verified withjavapagainst the published 0.1.1 jar, and the old constructor signature still works — but twoContextResponsevalues that compared equal under 0.1.1 may no longer compare equal under 0.2.0.
Not changed
- The privacy engine, pseudonymisation and security modules: no behaviour change in this release.
0.1.1 — 2026-09-17 · 3 changed
A release-plumbing patch, no features. 0.1.1 supersedes 0.1.0 for the
published image: the v0.1.0 tag predates the multi-architecture publish
pipeline, so a dispatch against that tag would silently re-run the old
single-architecture workflow rather than publish a multi-arch image. main
has also diverged from the tag since (CI action bumps, documentation, a
quickstart dependency pin), so an image built from main and labelled
0.1.0 would not correspond to the tagged tree. Cutting a patch version is
cheaper and more honest than force-moving a tag a published GitHub Release
already points at. No production source differs from 0.1.0, so the library
artifacts on Maven Central are functionally identical and consumers of
those jars have no functional reason to upgrade; 0.1.1 exists for the image
and the publish pipeline, where multi-architecture support is a real
improvement for anyone running the image.
Changed
- The server image is now published as a multi-architecture manifest list
(
linux/amd64+linux/arm64) instead oflinux/amd64only. - Three GitHub Actions dependency bumps in CI workflows.
- nimbus-jose-jwt bumped to 10.9.1 in
data-prism-quickstart-issuer. This affects the quickstart issuer only, not the published server or library artifacts, which never depend on it directly.
0.1.0 — 2026-09-16 · 9 added · 4 not included
First release: the walking skeleton and every slice through S9a.
Added
- Privacy engine that assigns one deterministic synthetic identity per
(scope, subject, namespace, algorithm version, key), never random and never stored in plaintext. - Correlation and consistency findings that surface source-data inconsistencies instead of hiding them.
- Parallel mTLS connectors to enterprise source APIs.
- Embedded Hazelcast identity cache and per-scope read budget.
- OAuth2 resource server with session-derived
PrivacyContext. - Audit logging and metrics.
- One MCP tool,
get_entity_context. - Standalone server as the primary deployment surface, and a Spring Boot starter for embedding the privacy layer directly.
- A one-command local Compose quickstart.
Not included in this release
- The re-identification operator surface, deferred past V1 by decision (see
docs/architecture.md#decisions-worth-knowing). - The Elasticsearch connector and its search tools.
- The three additional MCP tools named in the design review — only
get_entity_contextexists today. - An append-only audit sink with hash-chain verifier. The only audit sink in this release writes to a file and to SLF4J.