Skip to content
Registry StackDocsDevelopment (unreleased)

Configure the citizen chat assistant

View as Markdown

Configure breg-mcp and breg-review when a citizen should be able to ask a chat host, such as an AI assistant, to read their own Base Registry Engine (BReg) record and prepare a change to it, and then confirm that change themselves. This page covers what the registry and the authorization server must provide, both runtime configurations, and the Model Context Protocol (MCP) tools the gateway serves. Operate the citizen chat assistant covers running both services. To see both working first, run a citizen chat assistant locally.

Both are supporting services beside BReg, not products of their own. They add no registry semantics: what a citizen may read or change is decided by BReg, under the access profile each service names.

  • breg-mcp is the MCP gateway. A chat host calls it with an access token issued to the chat host for one signed-in citizen. The gateway reads that citizen’s own record and creates or edits a change-request draft, which it calls an application.
  • breg-review is a server-rendered page. The citizen opens the link the gateway returns, signs in, reads the draft beside the record it would change, and submits it.

After submission the change request goes to the review authority its entity declares, Registry Casework in the reference project, and BReg applies it only after approval. The two services are separate processes with separate configuration and credentials, so no single process holds both the authority to prepare a change and the authority to submit it.

These properties hold for every deployment, whatever the chat host does:

  • The gateway never submits, revises, cancels, or applies a change request, and writes no record directly. It has no code path to do so: the gateway’s lint configuration disallows the client methods that would, and a boundary check proves the list still refuses each one.
  • BReg enforces the same split. The gateway’s access profile is a standing agent, and the BReg compiler refuses a standing agent profile any lifecycle operation, direct write, or immediate action.
  • The target of an application comes from the citizen’s own linked record, never from tool arguments. The gateway resolves that record through the agent profile’s linked lookup and writes the target and owner fields itself. A tool argument or edit naming either field is refused before the gateway reads or writes any record.
  • The chat host’s token is never forwarded. The gateway removes it from the request once verified and performs an RFC 8693 token exchange on every tool call, calling BReg only with the delegated token that exchange returns.
  • The gateway never uses a delegated token after the citizen’s own token expires.
  • The review page re-reads the draft’s target under the citizen’s own token before it offers submission, and again before it submits, so a draft naming a record the citizen cannot read never reaches a submittable form.

Registry-level target binding is not yet in place, so any client other than these two services must not rely on BReg to tie a change request’s target to the requester’s own record.

The registry project declares what both services may do. The citizen address correction project is the reference; adapt its shape to your own entities.

DeclarationWhat it needs
A record entityThe citizen’s own record, such as an address
A link entityA steward-provisioned link from a verified principal to one record, with an active flag
A change-request entityA draft entity whose fields reference the target record and record the owner’s principal
An agent access profileactorKind: agent, the gateway’s exchange client in requesterClients, and no taskGrant
A review access profileactorKind: human and the review page’s client in requesterClients

The agent profile reads the citizen’s record through a membership boundary on the active link, and may create, get, and patch the change-request entity under a row boundary where the owner field equals the principal claim, with requestVisibility: owner. Its writable fields include the target and owner fields, because the gateway writes both. A lookup that finds no record, or more than one, refuses the call and creates nothing.

The review profile may get, patch, and submit_request the same change-request entity under the same row boundary, with the owner field not writable, and may get the target record through the same membership boundary.

In the BReg runtime configuration, set authentication.oidc.audience to the resource both services request, admit both clients in authentication.oidc.allowedClients, and map the gateway’s exchange client to the actor subject the authorization server writes in act.sub under authentication.authorityClaims.trustedActors:

authentication:
authorityClaims:
principal: sub
trustedActors:
<gateway-exchange-client-id>: <gateway-actor-subject>

BReg treats a token with a verified trusted actor as an agent, whatever its own actor-kind claim says. Configure BReg describes access profiles and the standing agent ceiling in full.

Both services are identity-provider neutral. Each takes an issuer, endpoints, and keys, not a vendor. The authorization server must support the following.

For the chat host’s token, which the gateway verifies:

  • RFC 9068 JWT access tokens (typ of at+jwt) whose audience is exactly the gateway’s resource identifier, https://<gateway-host>/mcp, issued to a preregistered chat-host client, carrying the required scopes, and signed with RS256, RS384, ES256, ES384, or EdDSA.

For the gateway’s token exchange:

  • private_key_jwt client authentication for the gateway’s exchange client.
  • A client_credentials grant for that client that honors the RFC 8707 resource parameter and issues the actor token for the gateway’s own resource identifier, never with the registry’s audience. An actor token the registry would accept is a standing registry credential with no citizen behind it.
  • RFC 8693 token exchange that accepts an already-issued access token as subject_token, with subject_token_type set to the access-token type, and an actor_token. The issued token names the citizen in sub, the registry in its audience, the gateway’s client as its client, and writes the actor as an act claim of exactly sub, or sub and iss where iss equals the token’s issuer. BReg refuses any other act shape, including a nested act.

For the review page’s sign-in:

  • OpenID Connect discovery at the issuer, publishing authorization_endpoint, token_endpoint, and jwks_uri.
  • The authorization code flow with PKCE (S256), private_key_jwt client authentication with the issuer as the assertion audience, and the RFC 8707 resource parameter.
  • ID tokens signed with EdDSA, ES256, ES384, RS256, or PS256.
  • A registry access token for the review client that carries registry_actor_kind: human.

The gateway serves no authorization, token, or registration endpoint. Register every chat host you accept, and the review page’s client, with the authorization server ahead of time, each with its own client identity and key.

ThunderID 1.0.1 has been shown to perform the gateway’s token exchange when the subject token came from a client-credentials grant. A browser sign-in through ThunderID, for the chat host or for the review page, has not been proven.

Both services refuse an inline credential. Every key is a reference: secret:file/<name> reads <name> under secretProviders.file.root, and secret:env/<NAME> reads an environment variable when secretProviders.environment is enabled. A secret file must be a regular file owned by the runtime user, with mode 0400 or 0600 and exactly one hard link.

Each service needs two secrets: a private JSON Web Key (JWK) for its own private_key_jwt client, and an audit key of at least 32 bytes that keys its audit journal and pseudonyms. Give the two services different keys.

breg-mcp reads one closed YAML document named by --runtime-config, which must be an absolute path with no symlink components. The shared loader accepts one UTF-8 YAML document up to 1 MiB, refuses duplicate and unknown keys, and reports the file and field without echoing configured values. Ordinary string values support ${VAR}, ${VAR:-default}, and ${VAR:?message} substitution. Secret references and secretProviders never accept environment expressions; use a declared secret provider for credentials.

apiVersion: registry.registrystack.org/breg-mcp-runtime/v1alpha1
kind: BRegMcpRuntimeConfig
listener:
bind: 10.0.0.10:8110
tlsTermination: operator-controlled-upstream
networkExposure: private-address
secretProviders:
file:
root: /run/secrets/breg-mcp
resourceServer:
resource: https://assistant.example.com/mcp
issuer: https://login.example.com
jwksSource:
kind: uri
uri: https://login.example.com/jwks.json
algorithms:
- ES256
allowedClients:
- <chat-host-client-id>
requiredScopes:
- address-correction:self
registry:
baseUrl: https://registry.example.com/
accessProfile: citizen-agent
audience: https://registry.example.com/citizen-address-correction
scopes:
- address-correction:self
exchange:
tokenEndpoint: https://login.example.com/token
clientId: <gateway-exchange-client-id>
privateKeyRef: secret:file/gateway-key
service:
name: Address correction
description: Correct the postal address the registry holds for you.
disclosure: The assistant you use will see your current address and the correction you request.
details:
entity: person-address
application:
entity: address-correction-request
targetField: address
ownerField: owner
reviewBaseUrl: https://review.example.com/
audit:
destination: file
path: /var/lib/breg-mcp/audit/audit.jsonl
hashKeyRef: secret:file/audit-key
rateLimits:
perCitizen:
requestsPerMinute: 30
burst: 10
perClient:
requestsPerMinute: 600
burst: 100

Every URL must use https with no credentials, query, or fragment. Plain http is accepted only for a loopback host under tlsTermination: development-loopback.

listener.bind is required; use 127.0.0.1:8110 for a loopback deployment. listener.tlsTermination is operator-controlled-upstream, for a service behind your TLS proxy, or development-loopback, which also turns off HSTS and admits loopback http URLs. listener.networkExposure decides which addresses the listener may bind:

networkExposureAddresses accepted
private-address (default)IPv4 loopback or private, IPv6 loopback or unique-local
container-privateThe same, plus the unspecified address, such as 0.0.0.0 inside a container

development-loopback accepts only a loopback address with private-address.

KeyMeaning
resourceThe gateway’s resource identifier: the exact https URL of its /mcp endpoint
issuerThe issuer every accepted token must name
jwksSourcekind: discovery (default), kind: uri with uri, or kind: static with documentRef, a secret holding the key set
algorithmsAccepted signature algorithms: RS256, RS384, ES256, ES384, or EdDSA
allowedClientsThe chat-host clients whose tokens are accepted
requiredScopesScopes every accepted token must carry
scopeClaimThe claim holding scopes; defaults to scope
maxTokenLifetimeSecondsThe longest token lifetime accepted; defaults to 3600

allowedClients and requiredScopes take between 1 and 128 entries. In production, a discovered or jwksSource.uri key set is fetched under a strict policy that refuses loopback, private-range, and cloud-metadata addresses. Use a static key set when the issuer’s keys are reachable only on a private network. development-loopback permits loopback HTTP for local evaluation, while still refusing non-loopback private and metadata targets. The gateway publishes RFC 9728 protected-resource metadata naming the issuer and the required scopes.

KeyMeaning
registry.baseUrlThe BReg deployment the gateway acts on
registry.accessProfileThe standing agent access profile every call names
registry.audienceThe registry’s RFC 8707 resource, requested in the exchange; must differ from resourceServer.resource
registry.scopesThe scopes requested for the delegated token
registry.requestTimeoutMillisecondsThe timeout for each registry call; defaults to 10000
exchange.tokenEndpointThe authorization server’s token endpoint
exchange.clientIdThe gateway’s own client; must not appear in resourceServer.allowedClients
exchange.privateKeyRefThe secret holding that client’s private JWK
exchange.assertionAudienceOptional client-assertion audience, when the server expects one other than its token endpoint
KeyMeaning
name, descriptionWhat describe_service tells the chat host about the service
disclosureWhat the citizen is told about the data the chat host will see
details.entityThe entity holding the citizen’s own record
application.entityThe change-request entity the gateway drafts
application.targetFieldThe field identifier of the reference to the citizen’s record
application.ownerFieldThe field identifier of the field recording the citizen’s principal
reviewBaseUrlThe review page’s publicOrigin, with no path; the gateway returns <reviewBaseUrl>/requests/<id>

name, description, and disclosure are each at most 4096 bytes. The two field keys take the field identifier as the project declares it, such as new-address-line, not the API name newAddressLine the registry’s metadata publishes for it, and must name two different fields.

audit.hashKeyRef names the key used for pseudonyms and gateway retry identity. With audit.destination: file (the default), audit.path is the absolute active-file path. audit.rotateBytes defaults to 104857600 (100 MiB), and audit.retainDays defaults to 90. Sealed segments are written beside the active file and expire under that retention policy. Ship them to your audit store before expiry.

audit.destination: stdout accepts no path, rotateBytes, or retainDays. It flushes entries to standard output but cannot prove durable storage; use file when local durable acceptance is required. See audit operation.

rateLimits.perCitizen and rateLimits.perClient are required, each with requestsPerMinute and burst greater than zero. limits.maxRequestBodyBytes defaults to 64 KiB and accepts at most 1 MiB.

breg-review reads one closed YAML document with the same rules for paths, unknown keys, and error messages.

apiVersion: registry.registrystack.org/breg-review-runtime/v1alpha1
kind: BRegReviewRuntimeConfig
listener:
bind: 10.0.0.11:8115
tlsTermination: operator-controlled-upstream
networkExposure: private-address
publicOrigin: https://review.example.com
secretProviders:
file:
root: /run/secrets/breg-review
signIn:
issuer: https://login.example.com
clientId: <review-page-client-id>
clientKeyRef: secret:file/client-key.jwk
scopes:
- address-correction:self
registry:
baseUrl: https://registry.example.com
resource: https://registry.example.com/citizen-address-correction
entity: address-correction-request
targetField: address
accessProfile: citizen-review
audit:
destination: file
path: /var/lib/breg-review/audit/journal.jsonl
hashKeyRef: secret:file/audit-key

listener follows the same rules as the gateway’s; set bind: 127.0.0.1:8115 for loopback. development-loopback also drops the Secure cookie attribute and the __Host- cookie prefix.

KeyMeaning
publicOriginThe page’s own https origin, with no path or query. The redirect URI to register is publicOrigin followed by /signin/callback
signIn.issuerThe OpenID Connect issuer the citizen signs in with
signIn.clientIdThe page’s client identifier
signIn.clientKeyRefThe secret holding the page’s private JWK
signIn.scopes1 to 16 unique scopes to request besides openid, which the page always adds
registry.baseUrlThe BReg deployment
registry.resourceThe registry’s RFC 8707 resource, requested at sign-in
registry.entity, registry.accessProfileThe change-request entity and the review access profile
registry.targetFieldThe field identifier of the target reference, such as address

registry.targetField takes the field identifier as the project declares it, the same form as the gateway’s application.targetField.

In production the page fetches the issuer’s discovery document, key set, and token endpoint under a strict policy that refuses loopback, private-range, and cloud-metadata addresses, so the authorization server must be reachable at a public address.

Both sections are optional:

KeyDefault
limits.perCitizen120 requests per minute, burst 30, for each signed-in citizen
limits.globalSignIn600 requests per minute, burst 120, shared by everyone on the sign-in routes
session.maximumSessions10000, from 1 to 1000000
session.maximumPendingSignIns10000, from 1 to 1000000
session.maximumLifetimeSeconds3600, from 60 to 86400
session.signInLifetimeSeconds600, from 60 to 3600

Startup refuses a maximumPendingSignIns smaller than the sign-ins globalSignIn admits within signInLifetimeSeconds: its burst plus its rate over that lifetime. A session ends at maximumLifetimeSeconds or when the citizen’s access token expires, whichever comes first. One citizen holds at most three sessions at once: signing in a fourth time ends their oldest session rather than take another slot, so one citizen takes at most three of the maximumSessions slots. A full store answers a sessions-exhausted page.

Validate each document before you serve it:

Terminal window
breg-mcp --runtime-config /etc/breg-mcp/runtime.yaml check
breg-review --runtime-config /etc/breg-review/runtime.yaml check

breg-mcp check resolves every secret without opening a socket or the audit journal. breg-review check reads and parses its secrets and refuses a client key serve would refuse, such as one without a kid, without fetching the issuer’s discovery document or opening its journal. Neither command proves the authorization server or the registry accepts the configuration; the first serve does.

The gateway serves MCP at /mcp over streamable HTTP, statelessly, with JSON responses. It serves six tools:

ToolArgumentsReturns
describe_serviceNoneThe configured name, description, and disclosure
get_my_detailsNoneThe citizen’s own record as labelled fields
start_applicationThe editable application fieldsA new draft’s applicationId, revision, and status
update_applicationapplicationId, expectedRevision, patchThe edited draft
prepare_reviewapplicationIdThe draft and its reviewUrl
get_application_statusapplicationIdThe draft’s status

Tool input schemas come from the registry’s metadata for the agent access profile, so a field the profile cannot write is never offered, and the target and owner fields never are. patch holds 1 to 64 edits, each an add, replace, or remove on one editable field path such as /newLocality; expectedRevision must match the draft’s current revision. Registry text is returned as labelled data with a notice that it is data, never instructions.

status is one of prepared, submitted, under_review, approved, rejected, applied, or cancelled, as BReg reports it. cancelled is the request’s own state: a review that was cancelled, answered, or superseded, or that asked for changes, leaves the request submitted and open, so its status is under_review. rejected, applied, and cancelled are closed. The agent profile in the reference project reads no review outcome, so its status stays submitted until the change is applied or the request closes.

Writes carry an idempotency key derived from the citizen, the tool, and the arguments, so a retried call replays its first answer instead of creating a second draft. A start_application identical to one whose draft is still open returns that draft. BReg binds each key to the active package revision, so after a package activation an identical start_application creates a new draft, even beside one still open from before the activation. BReg replays a key’s answer only within its receipt horizon (idempotency.receiptRetentionDays, 7 days by default), so an identical start_application past that horizon also creates a new draft beside an open one. Once more than 32 identical applications have closed or been left behind by an activation or a horizon, it answers not-permitted.

A failed tool call answers {"error":{"code":"<code>","message":"<text>"}}, with a traceId when the registry supplied one. Each code carries fixed text:

  • invalid-arguments: the arguments do not match what the tool accepts.
  • record-not-resolved: the registry found no single record for the citizen, so nothing was created.
  • not-found: no application with that identifier is available to the citizen. An application that no longer names the citizen’s own record answers the same way.
  • application-not-editable: the application can no longer be changed.
  • stale-application: the application changed since it was read, possibly by an earlier call whose answer was lost or whose identical retry came past the receipt horizon. Read its status before trying again.
  • idempotency-conflict: an earlier identical request is still being processed or differed. Only update_application answers this code; start_application moves on to a new draft instead.
  • not-permitted: the registry does not permit the action.
  • authorization-failed: the registry did not accept the delegated authorization.
  • registry-unavailable: the registry is temporarily unavailable.
  • service-unavailable: the gateway is not available.
  • unexpected-response: the registry returned a response the gateway cannot use.

A request refused before it reaches a tool gets an RFC 9457 problem document with a stable code:

Statuscode
401unauthorized or invalid-token, with an RFC 6750 WWW-Authenticate challenge naming the metadata URL
403insufficient-scope, or forbidden when the Host does not match the resource or an Origin header is present
404not-found
429rate-limited, with Retry-After
503temporarily-unavailable

The endpoint serves chat-host back ends, not browser pages, so any request carrying an Origin header is refused.