Registry stack documentation: machine-readable Markdown.
Index of all pages: https://docs.registrystack.org/dev/llms.txt
Full corpus: https://docs.registrystack.org/dev/llms-full.txt

# Operate the citizen chat assistant

> Run breg-mcp and breg-review behind your proxy, probe them, read their logs and audit journals, rotate their keys, and upgrade them.

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](../../configure/breg-mcp/). To see both services work
end to end first, [run the local journey](../../tutorials/first-citizen-mcp/).

## Put a proxy in front of both services

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.

{/* Evidence: crates/registry-breg-mcp/src/server.rs, require_resource_host(), forbidden();
    crates/registry-breg-mcp/src/inbound.rs, authenticate();
    crates/registry-breg-review/src/journal.rs, network address;
    crates/registry-breg-review/src/config.rs, default_global_sign_in(), CALLBACK_PATH. */}

## Probe health and readiness

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

| Route | `breg-mcp` | `breg-review` |
|---|---|---|
| `GET /health` | 200 `{"status":"alive"}` | 200 `{"status":"alive"}` |
| `GET /ready`, journal writable | 200 `{"status":"ready"}` | 200 `{"status":"ready"}` |
| `GET /ready`, journal not writable | 503 `{"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.

{/* Evidence: crates/registry-breg-mcp/src/server.rs, health(), ready();
    crates/registry-breg-mcp/src/gateway.rs, ready();
    crates/registry-breg-review/src/lib.rs, health(), ready();
    crates/registry-breg-review/src/journal.rs, ready(). */}

## Read the logs

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](../../reference/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:

| Service | Level | Message |
|---|---|---|
| `breg-mcp` | info | `tool call`, with `request_id`, `tool`, and `outcome` |
| `breg-mcp` | warn | `the access-token key set is unavailable` |
| `breg-mcp` | error | `tool audit append failed` |
| `breg-review` | warn | `a rate limit refused a request` |
| `breg-review` | warn | `the registry could not answer a review` |
| `breg-review` | warn | `the registry declined a review request` |
| `breg-review` | error | `an action request could not be audited` |
| `breg-review` | error | `an 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.

{/* Evidence: crates/registry-breg-mcp/src/main.rs, BREG_MCP_LOG;
    crates/registry-breg-mcp/src/inbound.rs, authenticate();
    crates/registry-breg-mcp/src/gateway.rs, tool audit append failed;
    crates/registry-breg-review/src/main.rs, BREG_REVIEW_LOG;
    crates/registry-breg-review/src/lib.rs, a rate limit refused a request. */}

## Keep the audit journals

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.

| Service | Operations | Pseudonym classes |
|---|---|---|
| `breg-mcp` | Tool calls | `breg-mcp-principal-v1`, `breg-mcp-client-v1` |
| `breg-review` | Admitted sign-in callbacks, reads, submissions, and sign-outs | `breg-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](../../configure/breg-mcp/#tool-errors)
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.

{/* Evidence: crates/registry-breg-mcp/src/gateway.rs, call();
    crates/registry-breg-mcp/src/problems.rs, after_mutation();
    crates/registry-breg-mcp/src/gateway/tests.rs,
    a_registry_failure_on_the_update_patch_is_unfinished,
    a_registry_failure_on_a_read_tool_is_refused,
    a_failed_readback_after_a_committed_start_is_unfinished. */}

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.

{/* Evidence: crates/registry-platform-audit/src/writer.rs, AuditEntry, AuditRequest, FileDestination;
    crates/registry-breg-mcp/src/audit.rs;
    crates/registry-breg-mcp/src/gateway.rs, call();
    crates/registry-breg-review/src/journal.rs;
    crates/registry-breg-review/src/signin.rs. */}

## Rotate keys and secrets

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.

{/* Evidence: crates/registry-platform-config/src/blocks.rs, SecretProvidersConfig;
    crates/registry-breg-mcp/src/inbound.rs, uri_fetcher();
    crates/registry-platform-oidc/src/lib.rs, JwksFetcher;
    crates/registry-breg-review/src/lib.rs, JwksFetcher;
    crates/registry-platform-audit/src/writer.rs, AuditWriter. */}

## Upgrade

When upgrading from the draft chained format, archive its files and configure a fresh audit path.
Set each `listener.bind` explicitly.

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

```sh
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.

{/* Evidence: crates/registry-breg-mcp/src/main.rs, shutdown_signal();
    crates/registry-breg-mcp/src/server.rs, NeverSessionManager;
    crates/registry-breg-review/src/lib.rs, SIGTERM;
    crates/registry-breg-review/README.md. */}

## Install the services or run their images

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](https://github.com/registrystack/registry-stack/releases/latest), 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:

```sh
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.

{/* Evidence: release/scripts/release_roster.py, BREG_SERVICES_FIRST_RELEASE and
    breg_services_in_release; release/scripts/release_candidate.py, _release_payload_inventory;
    release/docker/Dockerfile.breg-mcp; release/docker/Dockerfile.breg-review. */}

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`:

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

{/* Evidence: crates/registry-platform-config/src/blocks.rs, PrivateListenerConfig. */}

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

{/* Evidence: release/docker/Dockerfile.breg-mcp, WORKDIR, EXPOSE, ENTRYPOINT, and CMD. */}

```sh
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:

```sh
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.