Skip to content

Changelog

Published verbatim from Gungner’s Brayns partner interface pack, version 1.6.0 (CHANGELOG.md). This page is not edited here.

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/resolve gains the Lithuanian schemes (imones-kodas, asmens-kodas), and orgnr/personnummer with a country prefix name that country’s company or person scheme.
  • country everywhere it is about something. On company profile, officers and signatories, beneficial owners, person profile, engagements, documents, every event, and every identifier on a subject.
  • countries on tokens. The token response and the JWT carry the countries the grant allows, beside scope; 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 for POST /v1/companies/{orgnr}/refresh, the paid re-fetch.
  • refresh_company — its MCP tool, listed only to tokens holding kyb: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’s gid, a 400 validation 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.suspended gains reason: country_deprovisioned: withdrawing a country from the tenant suspends its watches; restoring the country resumes those that no other condition still suspends.
  • semantics.md amended in §1, §2, §3, §4, §6, §7, §8, §10, §11 and §12, with a new §13 (Countries).

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:read no longer opens the refresh. POST /v1/companies/{orgnr}/refresh requires kyb:refresh; a token without it gets 403 forbidden (scope kyb:refresh is required). The scope-miss code stays forbidden, 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 200 with an empty page and the same cursor back, where it used to answer 400 invalid_cursor: with reads bounded by country, that refusal told a grant how many events it could not read. invalid_cursor is now for a malformed cursor only.
  • A screening name carrying a personal identity number is refused. An ad-hoc screening check whose name carries a number shaped like a Swedish or Lithuanian personal identity number answers name_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 by subject_id.

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_whitelist and list_screening_overrides (the two screening policy reads). Every other API operation now has a tool or a stated reason not to, listed in mcp-tools.md: GET /v1/documents/{id} is binary; POST /v1/companies/{orgnr}/refresh is a paid write opened by the read scope kyb: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 two lookup_* tools, whose resolve step is a read), destructiveHint: false stated on every tool, and idempotentHint.
  • outputSchema on every single-route tool — the route’s own 2xx response schema plus summary. structuredContent validates 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:subscribe and screening:run list the reads they already opened. A token with monitoring:subscribe sees list_subscriptions/get_subscription, and one with screening:run sees the screening check reads. No scope opens anything it did not open before.

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. /mcp authenticates the request before reading any MCP message: no, invalid, expired or revoked credentials are 401 at the transport, so an anonymous initialize or tools/list — which worked before and could never call a tool — is refused. tools/list now 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 /mcp on 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 is structuredContent.
  • 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 answered 400 with 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/subscriptions already answered it). Every route now answers that body: bad_request, or legal_basis_invalid on POST /v1/subscriptions when legal_basis is wrong. Nothing from the request is repeated. Over MCP, content[1] relays the same body.
  • Refusal wording. A -32602 refusal 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’s Tool <name> not found is now Tool not found: name must be one of …. A malformed tools/call — no name, a name that is not a string, arguments that are not an object, or a task-augmented call (every tool is taskSupport: forbidden) — is now a JSON-RPC -32602 (it was -32603) and is audited like every other tool call.

Screening (SPEC-004b / ADR-013). Additive: no route, tool, event, error or field already published changes shape.

  • Two event payloads pinned — screening.hit and screening.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) and screening:policy (propose and approve policy versions and tenant overrides). screening:run is 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:policy is 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/checks runs a check and returns scored candidates with structured reasons and per-candidate evidence; GET /v1/screening/checks and GET /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/subjects is 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_snapshot identifiers are masked for a caller without kyc:read, with identifier_masked saying which — so “there was no organisationsnummer” and “you may not see it” are distinguishable rather than both being null.

  • One candidate reason, id_number_match, is withheld from a caller without kyc:read on 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:read as well as screening:run, and reading one back requires it as well as screening: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 is 403 forbidden and the read returns the check with its name and birth date withheld and input_snapshot.person_fields_masked: true saying so. A company check needs neither. As with identifier_masked, the flag exists so a withheld name is never read as an absent one.

  • input_snapshot.input_hash is the scoping key for a whitelist entry about an ad-hoc identity, and null on a subject check, which scopes by subject_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 a 16-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 into upstream_unavailable: that one invites you to accept stale: true on a later call, and a screening check has no stale mode — nothing was stored, so there is nothing to serve. Retry.

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 of lookup_company. It closes the gap ADR-012 §10 recorded and left open: get_person_profile takes an identifier_ref, and the only tools that returned one required kyb:read, so a kyc: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 means 403 spar_permit_required (or spar_permit_suspended), no upstream call and no cached profile, and under a protection marker nothing that locates the person is returned. Its type argument (personnummer or samordningsnummer) is named rather than inferred, exactly as POST /v1/subjects/resolve asks for it.

  • New scope monitoring:read — the read half of monitoring: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/subscriptions and GET /v1/subscriptions/{id} now accept it in addition to monitoring:subscribe, kyb:read and kyc:read, so nothing a consumer could call before stops working, and list_subscriptions documents it as the scope to ask for. The write routes are unchanged: creating and ending a subscription still require monitoring:subscribe and nothing else grants it.

  • New error code identifier_in_argument (422). No route returns it. It is the typed error on an assistant tool_result block 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.

Patch — no route, tool, event, error or field changed shape.

  • POST /oauth/token now also accepts application/x-www-form-urlencoded client credentials, alongside the application/json body 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.

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 on kyc:read and 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 gets 403 spar_permit_required and 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:read is 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_ref on POST /v1/subjects/resolve, and that route now accepts kyc:read as well as kyb:read — without both, a kyc: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.suspended event (semantics.md §7). What such a subscription does not yet produce is person.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 returns 503 — see semantics.md §8 for why.

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} and POST /v1/subscriptions/{id}/end, with the MCP tools subscribe_monitoring, list_subscriptions and end_subscription onto 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.changed and person.changed are emitted for real now, and refresh.completed, refresh.deferred and the subscription.* lifecycle types have pinned payloads. Every one carries the subscription id, the canonical subject id with aliases, evidence, and trigger (on_demand | scheduled).
  • Filtered replay. GET /v1/events?types= restricts a page to named event types, and the list_changes tool is that route with the five change types preselected. Cursors are unaffected: a filtered page can come back short while next_cursor advances.
  • Field diffs gain an optional change tag (added | removed | changed) for officer, signatory and beneficial-owner membership changes. {field, old, new} is unchanged; person diffs carry old: null by design (ADR-011 §8).
  • New error codes: subscription_exists (409), subscription_ended (409), legal_basis_invalid (400), and spar_permit_required (403) — the last one already documented in semantics.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}/refresh still 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 and events_emitted: 0.

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.