Skip to content
Registry StackDocsv0.38.0

Operate the citizen chat assistant

View as Markdown

You have two runtime configurations that pass check, one for the breg-mcp gateway and one for the breg-review page, and you want both serving behind your own TLS proxy. This page covers what the proxy owns, how to probe and watch both processes, where their audit journals live, how to rotate their keys, and how to upgrade them.

Writing the configurations, preparing the registry, and preparing the authorization server are in Configure the citizen chat assistant. To see both services work end to end first, run the local journey.

Neither service terminates TLS. With tlsTermination: operator-controlled-upstream, your proxy owns the public listener and forwards plain HTTP to a loopback or private address. The proxy has these responsibilities:

  • Terminate TLS for both public origins.
  • Keep the gateway’s Host. On /mcp, the gateway refuses with 403 any request whose Host does not match the authority of resourceServer.resource. Forward the original Host, or rewrite it to that authority.
  • Never add an Origin header. The gateway refuses any /mcp request that carries one, since a chat host is not a browser page. The refusal happens before the token is verified.
  • Pass Authorization through unchanged to the gateway.
  • Rate limit at the edge. The gateway charges its per-citizen and per-client limits only after a token verifies, so requests with missing or invalid tokens cost nothing in process. The review page reads no peer address and no forwarded header, so limits.globalSignIn is one ceiling shared by everyone on /signin and /signin/callback. Limit both services by client address at the proxy, and give the sign-in routes their own tighter limit.
  • Serve the review page at publicOrigin. The page builds its sign-in redirect URI from publicOrigin, so the origin browsers see must be exactly that value.

The gateway checks Host and Origin on /mcp only. /health, /ready, and the protected-resource metadata routes answer any host.

Both services answer GET /health and GET /ready with a JSON body and Cache-Control: no-store. Probe them over HTTP from your platform.

Routebreg-mcpbreg-review
GET /health200 {"status":"alive"}200 {"status":"alive"}
GET /ready, journal writable200 {"status":"ready"}200 {"status":"ready"}
GET /ready, journal not writable503 {"status":"not-ready"}503 {"status":"not-ready"}

Readiness reflects the audit journal alone. Neither route calls the registry or the authorization server, so a ready service can still answer a tool call with registry-unavailable or a sign-in with an error page. Watch the warnings in the next section for those.

Each service writes one JSON object per line on standard output. The level is set by BREG_MCP_LOG or BREG_REVIEW_LOG, which accept exactly error, warn, or info and default to info. Any other value stops the process before it starts, with a message on standard error and exit status 2. See environment variables.

Startup failures take different channels. breg-mcp writes an ERROR JSON line on standard output and exits with status 1. breg-review writes a plain line starting breg-review: on standard error and exits with status 1. Collect both streams.

These records are the ones to watch:

ServiceLevelMessage
breg-mcpinfotool call, with request_id, tool, and outcome
breg-mcpwarnthe access-token key set is unavailable
breg-mcperrortool audit append failed
breg-reviewwarna rate limit refused a request
breg-reviewwarnthe registry could not answer a review
breg-reviewwarnthe registry declined a review request
breg-reviewerroran action request could not be audited
breg-reviewerroran action response could not be audited

Neither service logs a token, a secret, a cookie, or a field value. The request_id of a gateway log record matches the operation correlation in its audit entries.

Each service writes its own audit stream, separate from the registry’s audit. The registry still audits the reads and writes it performs for the gateway’s delegated token.

ServiceOperationsPseudonym classes
breg-mcpTool callsbreg-mcp-principal-v1, breg-mcp-client-v1
breg-reviewAdmitted sign-in callbacks, reads, submissions, and sign-outsbreg-review-principal-v1, breg-review-client-v1

Each line carries schema, eventId, time, phase, correlation, and record. A request entry precedes protected I/O; a response entry precedes release of the result. Both carry the same fresh operation correlation. A change-request identifier identifies the record under review; it does not identify the operation, since multiple reviews can overlap.

A gateway response entry records the tool call’s outcome. It is ok when the tool answered, refused when the call failed with no registry effect, and unfinished when a create or patch was sent and its effect is unknown: a transport failure, a server error, or a response the gateway cannot use. A failed call also records its tool error code as reason. A call answered unfinished may have changed the registry. A cancelled call is also recorded unfinished, whether or not it had sent a create or patch.

Citizen and client identifiers become keyed HMAC-SHA-256 pseudonyms derived from the issuer and subject or client identifier. No field value, prompt, tool argument, token, cookie, or network address belongs in the stream. Before sign-in verifies a subject, its records omit the citizen pseudonym. Different service classes give the same citizen different pseudonyms.

A refused request append prevents protected I/O. A refused response append withholds the result, but does not undo a registry effect or restore a revoked session. If an operation is cancelled, the request guard attempts a minimized unfinished response while the process and writer can still complete it. A crash or failed destination can leave a request without a response; responses can also be duplicated. Reconcile by correlation and registry state, not by assuming one terminal line proves exactly one effect.

For durable local acceptance, use audit.destination: file and give each process its own absolute audit.path. The runtime creates a missing directory, refuses group- or world-writable audit directories, and keeps files owner-only. The active file has .lock and .seq companions. Keep those companions in place while the stream is in use.

Files rotate at audit.rotateBytes, 100 MiB by default. Sealed files expire after audit.retainDays, 90 days by default. Ship sealed files to append-only storage before expiry; the local files have no hash chain and cannot prove completeness. audit.destination: stdout is best effort: flushing a line does not prove durable storage. A failed writer stays unavailable until restart, so investigate the operator diagnostic and destination before restarting.

Both services read every secret once, at startup. Replacing a secret file takes effect only on the next restart.

Client keys. The gateway’s exchange client and the review page’s sign-in client each authenticate with a private JWK. To rotate one, register the new public key with the authorization server beside the old one, replace the secret file, restart the service, and then remove the old public key.

Issuer signing keys. With jwksSource.kind: discovery or uri, the gateway refetches the discovered or configured key set when a token names a key it does not hold, so the authorization server can publish a new key and sign with it without a gateway restart. With jwksSource.kind: static, add the new public key to the key set secret and restart before the authorization server signs with it. The review page always fetches the key set named by the issuer’s discovery document.

Audit keys. Keep the key stable across restarts: it preserves pseudonym correlation and the gateway’s deterministic retry identity. Changing it changes both. Arrange a controlled restart with no outstanding gateway retries, archive the old stream, and select a fresh audit.path. Retain the old key securely for as long as your policy requires recomputing old pseudonyms. The new writer does not verify a hash chain or detect a changed pseudonym key.

When upgrading from the draft chained format, archive its files and configure a fresh audit path. Rename resourceServer.jwks to jwksSource, replace audit.maximumFileBytes with rotateBytes, and set each listener.bind explicitly.

Run check from the new binary against the revised configuration, then replace the binary and restart:

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

Both services stop accepting connections on SIGTERM or SIGINT and finish the requests already in flight. A restart loses different state in each:

  • breg-mcp keeps no session between requests. Drafts live in the registry, so a chat host carries on after a restart.
  • breg-review keeps sessions and pending sign-ins in memory. A restart signs every citizen out and abandons sign-ins in progress; the citizen opens the review link again and signs in.

For the same reason, run one review page instance, or route each browser to the same instance with sticky sessions. The in-process limits count per instance, so each replica you add raises the effective limit.

The first release eligible to publish the paired services is v0.38.0. For a published release at or after that boundary, choose <tag> from the latest release, download the breg-mcp-<tag>-linux-amd64 and breg-review-<tag>-linux-amd64 assets, and authenticate both through the shared SHA256SUMS procedure at https://github.com/registrystack/registry-stack/blob/<tag>/release/VERIFY.md. Install the exact pair from one tag:

Terminal window
tag="${TAG:?set TAG to a published tag that includes breg-mcp and breg-review}"
mkdir -p ~/.local/bin
for binary in breg-mcp breg-review; do
install -m 0755 "${binary}-${tag}-linux-amd64" "$HOME/.local/bin/${binary}"
done
export PATH="$HOME/.local/bin:$PATH"
breg-mcp --version
breg-review --version

The install replaces either file already at that destination. Preserve the old pair first when you need a rollback path. Build both services from a checkout when no published release contains them or when you need another platform.

The same release publishes ghcr.io/registrystack/breg-mcp:<tag> and ghcr.io/registrystack/breg-review:<tag>. Each image carries one binary on Distroless cc-debian13 as the nonroot user. Authenticate the image digests through the release manifest and deploy the digest for each service rather than the tag alone.

Mount the runtime configuration and the secrets read-only, and give the journal a writable directory the image user owns. Inside a container, bind the listener on 0.0.0.0, which requires networkExposure: container-private:

listener:
bind: 0.0.0.0:8110
tlsTermination: operator-controlled-upstream
networkExposure: container-private

A gateway container then looks like this, with the review page following the same pattern on port 8115:

Terminal window
BREG_MCP_IMAGE='ghcr.io/registrystack/breg-mcp@sha256:<verified-digest>'
docker run --rm \
--mount type=bind,source=/etc/breg-mcp/runtime.yaml,target=/etc/breg-mcp/runtime.yaml,readonly \
--mount type=bind,source=/run/secrets/breg-mcp,target=/run/secrets/breg-mcp,readonly \
--mount type=volume,source=breg-mcp-audit,target=/var/lib/breg-mcp \
--publish 127.0.0.1:8110:8110 \
"$BREG_MCP_IMAGE"

Set audit.path to a file in the mounted journal directory, such as /var/lib/breg-mcp/audit.jsonl. The image user owns that directory, and the audit journal refuses a directory it does not own. Run check in the same image before you serve:

Terminal window
docker run --rm \
--mount type=bind,source=/etc/breg-mcp/runtime.yaml,target=/etc/breg-mcp/runtime.yaml,readonly \
--mount type=bind,source=/run/secrets/breg-mcp,target=/run/secrets/breg-mcp,readonly \
"$BREG_MCP_IMAGE" \
--runtime-config /etc/breg-mcp/runtime.yaml check

The secret files must be readable by the image user. The image carries no shell and no HTTP client, so probe /health and /ready from the platform.