Changelog
Published verbatim from Gungner’s Brayns partner interface pack, version 1.6.0 (
CHANGELOG.md). This page is not edited here.
1.6.0 — 2026-09-27
Section titled “1.6.0 — 2026-09-27”Countries, grants and a write scope for the paid refresh (SPEC-006d / ADR-017). No route, tool, event or field is renamed or removed.
- Wire form. Identifiers accept a country prefix:
lt:123456789; unprefixed still means Sweden, so every existing call is unchanged.POST /v1/subjects/resolvegains the Lithuanian schemes (imones-kodas,asmens-kodas), andorgnr/personnummerwith a country prefix name that country’s company or person scheme. countryeverywhere it is about something. On company profile, officers and signatories, beneficial owners, person profile, engagements, documents, every event, and every identifier on a subject.countrieson tokens. The token response and the JWT carry the countries the grant allows, besidescope; the JWT also names its grant (gid).- Four error codes.
country_not_granted(403),country_not_provisioned(403),country_not_supported(501) and, on the grant forms,grant_countries_invalid(400). kyb:refresh— a write scope forPOST /v1/companies/{orgnr}/refresh, the paid re-fetch.refresh_company— its MCP tool, listed only to tokens holdingkyb:refresh,readOnlyHint: false.- A coverage matrix by country in
reference.md. - A per-grant access log, and its rule in
reference.md: every authenticated request that reached a route is logged under the grant’sgid, a400validation failure excepted. provisioned_countries_invalid(400) is catalogued: answered on Gungner’s own ops routes when a tenant’s own country would fall outside its provisioned countries. No consumer route returns it.subscription.suspendedgainsreason: country_deprovisioned: withdrawing a country from the tenant suspends its watches; restoring the country resumes those that no other condition still suspends.semantics.mdamended in §1, §2, §3, §4, §6, §7, §8, §10, §11 and §12, with a new §13 (Countries).
Behaviour
Section titled “Behaviour”Four changes a client can observe, named apart from the additions. Under semantics.md §11 they are
changes; they ship in 1.6.0 as an explicit, recorded exception (SPEC-006d D6/D11 and Rev C, owner’s and
strategy chat’s decisions) — no integrator is on 1.5.0.
kyb:readno longer opens the refresh.POST /v1/companies/{orgnr}/refreshrequireskyb:refresh; a token without it gets403 forbidden(scope kyb:refresh is required). The scope-miss code staysforbidden, as on every other route.- Reads are bounded by country. A token only sees data in its grant’s countries; a Swedish-only grant — every grant that existed before this version — sees exactly what it saw before.
- A cursor past the end is no longer an error — a security fix. A well-formed cursor beyond the newest event
the token can see answers
200with an empty page and the same cursor back, where it used to answer400 invalid_cursor: with reads bounded by country, that refusal told a grant how many events it could not read.invalid_cursoris now for a malformed cursor only. - A screening name carrying a personal identity number is refused. An ad-hoc screening check whose
namecarries a number shaped like a Swedish or Lithuanian personal identity number answersname_is_identifier(400, newly catalogued) where it used to be screened: a name-only check is visible to every grant of the tenant. It is refused, never redacted, and the answer does not repeat the value; screen a person bysubject_id.
1.5.0 — 2026-09-25
Section titled “1.5.0 — 2026-09-25”The MCP server, closed against the API (SPEC-007c / ADR-016). No tool is renamed, and no published input schema, result field or route changes shape — the 1.4.0 tool list is reproduced byte for byte.
- Three new MCP tools, for operations that had none:
get_subscription(GET /v1/subscriptions/{id}),list_screening_whitelistandlist_screening_overrides(the two screening policy reads). Every other API operation now has a tool or a stated reason not to, listed inmcp-tools.md:GET /v1/documents/{id}is binary;POST /v1/companies/{orgnr}/refreshis a paid write opened by the read scopekyb:read, so it stays REST-only until the route asks for a write scope; the portal, assistant and token surfaces are not consumer operations. - Annotations on every tool —
readOnlyHint(true for the reads, including the twolookup_*tools, whose resolve step is a read),destructiveHint: falsestated on every tool, andidempotentHint. outputSchemaon every single-route tool — the route’s own 2xx response schema plussummary.structuredContentvalidates against it.mcp-tools.json— the machine-readable manifest: per tool the scopes that list it, the route(s), annotations, the served input schema and its hash, the output-schema reference; the transport; the excluded operations.monitoring:subscribeandscreening:runlist the reads they already opened. A token withmonitoring:subscribeseeslist_subscriptions/get_subscription, and one withscreening:runsees the screening check reads. No scope opens anything it did not open before.
Behaviour
Section titled “Behaviour”Five changes a client can observe, named apart from the additions above. Under
semantics.md §11 they are changes rather than additions; they ship in 1.5.0 as an explicit,
recorded exception to that rule (SPEC-007c, owner’s decision), not as a reading of it.
- No anonymous discovery.
/mcpauthenticates the request before reading any MCP message: no, invalid, expired or revoked credentials are401at the transport, so an anonymousinitializeortools/list— which worked before and could never call a tool — is refused.tools/listnow shows only the tools the token’s scopes open; a tool not shown can still be called and gets the API’s own answer. - The endpoint moved. The MCP server is served by the API process at
/mcpon the API’s own origin, not by a separate process — the same host and port as every other route, not a second one. - A second content block. Every result’s
content[1]is the REST response body verbatim (errors: the API’s error body).content[0]— the summary, or<code>: <message>— is unchanged, and so isstructuredContent. - Validation failures in the errors.md shape. A request that fails a route’s own schema — a
wrong type, an out-of-range
limit, an unknown enum value — was answered400with the validator’s raw issue list, which for an enum field quoted the value sent, rather than the{error, message}body errors.md publishes (POST /v1/subscriptionsalready answered it). Every route now answers that body:bad_request, orlegal_basis_invalidonPOST /v1/subscriptionswhenlegal_basisis wrong. Nothing from the request is repeated. Over MCP,content[1]relays the same body. - Refusal wording. A
-32602refusal names the parameter and what it accepts, and never repeats the value sent: an unknown tool answers with the tools this credential can list rather than echoing the name, and an invalid argument names its allowed values rather than quoting what arrived. The invalid-arguments prefix (MCP error -32602: Input validation error: Invalid arguments for tool <name>:) is unchanged; the unknown-tool text is not — 1.4.0’sTool <name> not foundis nowTool not found: name must be one of …. A malformedtools/call— noname, anamethat is not a string,argumentsthat are not an object, or a task-augmented call (every tool istaskSupport: forbidden) — is now a JSON-RPC-32602(it was-32603) and is audited like every other tool call.
1.4.0 — 2026-09-04
Section titled “1.4.0 — 2026-09-04”Screening (SPEC-004b / ADR-013). Additive: no route, tool, event, error or field already published changes shape.
-
Two event payloads pinned —
screening.hitandscreening.cleared. Both types have been in the catalogue since the beginning with their payload deliberately left unpinned, because the spec that emits them did not exist and inventing a shape would have fixed a contract nobody had designed. SPEC-004b designs it, so they are pinned now: check id, band, candidate count, and the versions the result was produced under — policy version, tenant override version, source version ids and matcher version. No consumer can have received one of these events before today, so pinning them takes nothing away; a payload that arrives without its versions is now refused at the schema rather than stored, which is what ADR-013 §5 asks for when it says a result cites version ids and never bare dates. -
Three new scopes —
screening:read(read a check that already exists),screening:run(run one) andscreening:policy(propose and approve policy versions and tenant overrides).screening:runis a write scope even though the route it will guard is read-shaped: running a check writes append-only rows about a natural person, emits an event and meters a unit.screening:policyis not a consumer scope in v1 — it is for portal users and platform admins. No existing scope changes meaning and no consumer loses one. -
New routes.
POST /v1/screening/checksruns a check and returns scored candidates with structured reasons and per-candidate evidence;GET /v1/screening/checksandGET /v1/screening/checks/{id}read them back. Every result stamps the policy version, the tenant override version, the source versions and the matcher version it was produced under — enough to re-run it and get the same answer, which is what makes a disagreement resolvable. A check is not wrapped in{stale, data}: it is produced on demand and never served from a revalidation window, the same reason/v1/subjectsis un-enveloped. -
Two result fields worth handling.
degraded_sources[]names lists whose most recent import partly failed — the check still ran, because a stale public list beats no screening, and the stamp is what makes that explainable.truncated_datasets[]names lists that held more candidates than were scored. Both are non-empty rarely and matter when they are: a narrowed or stale result otherwise reads exactly like a complete one. -
input_snapshotidentifiers are masked for a caller withoutkyc:read, withidentifier_maskedsaying which — so “there was no organisationsnummer” and “you may not see it” are distinguishable rather than both beingnull. -
One candidate reason,
id_number_match, is withheld from a caller withoutkyc:readon a check whose organisationsnummer is masked: it asserts that the withheld identifier equals the registration number on a listed entry the same response names, so it would restate the masked value by reference. Every other reason, and every listing, is returned unredacted — a sanctions listing is public. -
Screening a person subject requires
kyc:readas well asscreening:run, and reading one back requires it as well asscreening:read. Screening a person reads its name and birth date from SPAR, so the scope that governs person data governs here too; without it the run is403 forbiddenand the read returns the check with its name and birth date withheld andinput_snapshot.person_fields_masked: truesaying so. A company check needs neither. As withidentifier_masked, the flag exists so a withheld name is never read as an absent one. -
input_snapshot.input_hashis the scoping key for a whitelist entry about an ad-hoc identity, andnullon a subject check, which scopes bysubject_id. Read it off the check and post it back to the whitelist route. It is a keyed digest of the normalised fields that were screened, and its encoding is stable by contract — the same identity written with a16-prefixed or hyphenated organisationsnummer, different case, or the nationalities in another order produces the same value. Two properties follow from the key and are deliberate: the value is tenant-scoped and not portable between tenants, and rotating Gungner’s identifier pepper invalidates stored hashes, so standing ad-hoc whitelist entries stop matching and have to be recreated. -
Whitelist and tenant-override routes, gated on
screening:policy. Suppressing a candidate changes what every future check reports, and moving a threshold changes what every future check means; both are governance, and both are four-eyed at the database — the principal who proposes may not approve. -
Three new MCP tools —
screen_subject,get_screening_check,list_screening_checks— 1:1 on those routes, same tokens and scopes. -
New error code
screening_unavailable(503). Deliberately not folded intoupstream_unavailable: that one invites you to acceptstale: trueon a later call, and a screening check has no stale mode — nothing was stored, so there is nothing to serve. Retry.
1.3.0 — 2026-09-02
Section titled “1.3.0 — 2026-09-02”The assistant’s tool catalogue (SPEC-006b / ADR-014). Additive throughout — nothing already published changes shape, and no consumer loses a scope it held.
-
New MCP tool
lookup_person— resolves a personnummer or samordningsnummer to its canonical subject id and returns the SPAR person profile in one call, the person mirror oflookup_company. It closes the gap ADR-012 §10 recorded and left open:get_person_profiletakes anidentifier_ref, and the only tools that returned one requiredkyb:read, so akyc:read-only consumer driving Gungner through MCP could not reach a person at all. It is gated exactly as every other SPAR path — no active permit means403 spar_permit_required(orspar_permit_suspended), no upstream call and no cached profile, and under a protection marker nothing that locates the person is returned. Itstypeargument (personnummerorsamordningsnummer) is named rather than inferred, exactly asPOST /v1/subjects/resolveasks for it. -
New scope
monitoring:read— the read half ofmonitoring:subscribe. Until now, reading which subjects a tenant watches and starting or stopping a watch were the same scope; a consumer that should only ever look had to be granted the authority to change.GET /v1/subscriptionsandGET /v1/subscriptions/{id}now accept it in addition tomonitoring:subscribe,kyb:readandkyc:read, so nothing a consumer could call before stops working, andlist_subscriptionsdocuments it as the scope to ask for. The write routes are unchanged: creating and ending a subscription still requiremonitoring:subscribeand nothing else grants it. -
New error code
identifier_in_argument(422). No route returns it. It is the typed error on an assistanttool_resultblock when the model’s own arguments carried a raw personnummer or samordningsnummer instead of the opaque handle it was given — the assistant refuses its own tool call, nothing is looked up, and the value is not stored. Listed here because it is a code a customer sees and because Gungner’s error catalogue is one closed set; a Brayns consumer will never receive it.
1.2.1 — 2026-08-28
Section titled “1.2.1 — 2026-08-28”Patch — no route, tool, event, error or field changed shape.
POST /oauth/tokennow also acceptsapplication/x-www-form-urlencodedclient credentials, alongside theapplication/jsonbody it has always accepted (RFC 6749 §4.4.2 — the encoding every standard OAuth2 client library sends by default). Same field names, same response, same errors either way; a caller can pick whichever its HTTP client makes easier and switch later without a code change on Gungner’s side.
1.2.0 — 2026-08-27
Section titled “1.2.0 — 2026-08-27”SPAR person data (SPEC-003b / ADR-012). Additive throughout — nothing already published changes shape, so a consumer pinned to 1.1.0 or 1.0.0 keeps working unchanged.
- New route
GET /v1/persons/{ref}— the SPAR person profile, gated onkyc:readand on the tenant’s own SPAR permit.semantics.md§8 is now what ships rather than what is forthcoming: a tenant without an active permit gets403 spar_permit_requiredand no upstream call is made, and under a protection marker nothing that locates the person is returned on any surface. - New MCP tool
get_person_profile, 1:1 with that route. kyc:readis live. It was declared in the scope set and opened no route until now.- New error code
spar_permit_suspended(403) — a withdrawn permit is a different condition from an absent one, and the fix differs.spar_permit_required(403, introduced in 1.1.0 for person subscriptions) is now emitted by person reads too, under the same permit and with the same status. - New response field
identifier_refonPOST /v1/subjects/resolve, and that route now acceptskyc:readas well askyb:read— without both, akyc:read-only consumer could not reach the person route at all. - Person subscriptions can now be created. 1.1.0 shipped them against a permit stub that
always refused; the permit gate is now real, so a tenant holding an active permit can create
one, and a permit that is withdrawn suspends its person subscriptions automatically with a
subscription.suspendedevent (semantics.md§7). What such a subscription does not yet produce isperson.changed— the scheduled person source is not built, so for now a person subscription tracks the permit, not the person. Do not build a change feed on it yet. - One delivery rule differs from every other read: a person profile is never served
stale. Where a KYB read degrades to
stale: true, this returns503— seesemantics.md§8 for why.
1.1.0 — 2026-08-26
Section titled “1.1.0 — 2026-08-26”Monitoring (SPEC-005 / ADR-011). Additive throughout — nothing already published changes shape, so a consumer pinned to 1.0.0 keeps working unchanged.
- Subscriptions.
POST /v1/subscriptions,GET /v1/subscriptions,GET /v1/subscriptions/{id}andPOST /v1/subscriptions/{id}/end, with the MCP toolssubscribe_monitoring,list_subscriptionsandend_subscriptiononto the same routes. A subject is monitored only while an active subscription carries its legal basis —semantics.md§7. - Change events are live.
company.changed,company.status.changed,signatory.changed,ubo.changedandperson.changedare emitted for real now, andrefresh.completed,refresh.deferredand thesubscription.*lifecycle types have pinned payloads. Every one carries the subscription id, the canonical subject id with aliases, evidence, andtrigger(on_demand|scheduled). - Filtered replay.
GET /v1/events?types=restricts a page to named event types, and thelist_changestool is that route with the five change types preselected. Cursors are unaffected: a filtered page can come back short whilenext_cursoradvances. - Field diffs gain an optional
changetag (added|removed|changed) for officer, signatory and beneficial-owner membership changes.{field, old, new}is unchanged; person diffs carryold: nullby design (ADR-011 §8). - New error codes:
subscription_exists(409),subscription_ended(409),legal_basis_invalid(400), andspar_permit_required(403) — the last one already documented insemantics.md§8 and now actually emitted, by person-kind subscription creation, ahead of SPEC-003b’s person routes. - Behaviour change worth reading:
POST /v1/companies/{orgnr}/refreshstill always re-fetches, but now emits change events only while an active subscription covers that company. With none, or with one suspended or ended, it returns the data andevents_emitted: 0.
1.0.0 — 2026-08-26
Section titled “1.0.0 — 2026-08-26”Initial release of the Brayns interface pack. Documents everything in code as of this
version: the consumer-reachable REST surface (including /oauth/token), the eight
SPEC-007a MCP tools, the event catalogue, and every error code apps/api can emit.
Known gap against semantics.md, tracked for the next revision: the evidence bundle
(semantics.md §5) describes call_ref/upstream_ref/digest/schema_version fields
that land with SPEC-003a-rev-B; until that merges, reference.md’s evidence section
reflects the current transaction_ref-only shape, since everything here is generated from
code as it exists today.