ProPR and ProCursor read their startup settings from the process environment. There is no configuration file to edit; everything else is configured in the management UI.
Defaults and accepted ranges are stated here. Other pages name a variable and link to this one.
How a value can fail to take effect
A wrong value behaves in one of three ways:
| What you did | What happens |
|---|---|
| Set it somewhere the container never sees | Silently ignored. See the example stack below. |
| Set it to something unparsable - a word where a number belongs, or a blank value | Silently ignored; the default is used. |
| Set it to a parsable value outside the accepted range | Startup fails for most settings, with a message naming the setting and its bounds. |
The following settings depart from that:
MEISTER_SESSION_IDLE_MINUTESandMEISTER_SESSION_ABSOLUTE_HOURSfail startup on anything that is not a positive integer. They do not fall back to their default.- The
MEISTER_PLATFORM_ADMIN_SSO_*variables fail startup on any value outside the accepted ones, and when one of them is set withoutMEISTER_PLATFORM_ADMIN_SSO_AUTHORITY. See single sign-on for platform administrators. - The
MEISTER_PROVISIONED_USER_SSO_*variables fail startup on any value outside the accepted ones, and when one of them is set withoutMEISTER_PROVISIONED_USER_SSO_AUTHORITY. See single sign-on for provisioned users. MEISTER_LOCAL_SIGN_IN_ENABLEDandMEISTER_TENANT_SSO_ENABLEDaccepttrueorfalsewithout regard to case or surrounding whitespace. Unset, blank, or whitespace-only values use the defaulttrue; other unparsable values fail startup. See installation sign-in policy.MEISTER_PLATFORM_ADMIN_ELEVATION_MAX_MINUTESfails startup on anything that is not a whole number from 1 to 1440. It does not fall back to its default.MEISTER_PROVISIONED_USER_PAT_MAX_DAYSfails startup unless it is unset or a whole number from 1 to 3650.MEISTER_DEFAULT_ENABLED_PROVIDERSfails startup when it names a provider family other thanazureDevOps,github,gitLaborforgejo, or when a set value names no family. See enabling a provider family.- A value under the minimum of an interval marked “clamped” is silently raised to that minimum.
- Most
AI_*review-tuning settings are not range-checked. An out-of-range value is used as given. Their listed ranges describe supported operation. The two evidence judge budgets are validated at startup.
Other symptoms, and the page that fixes each: troubleshooting.
Does the example stack forward it?
Compose passes a variable to a container only when the service’s environment: block names it. The
bundled example/docker-compose/docker-compose.yml names a fixed set. Anything else you put in .env
is silently ignored.
In an env file, do not put spaces around the equals sign. Write KEY=value, not KEY = value - the
spaces become part of the name and the value.
The last column of every table below says which:
| Value | Meaning |
|---|---|
| yes | The compose file forwards it from .env |
| pinned | The compose file sets a fixed value; .env cannot change it |
| no | Not forwarded. Setting it in .env does nothing. |
To use one that is not forwarded, add it to the meisterpropr service’s environment: block yourself:
environment:
- MEISTER_ALLOW_PRIVATE_EGRESS=${MEISTER_ALLOW_PRIVATE_EGRESS:-}
Required values
| Variable | What it does | Default | Accepted | Example stack |
|---|---|---|---|---|
MEISTER_JWT_SECRET | Signing secret for session access tokens | none | at least 32 characters | yes |
MEISTER_BOOTSTRAP_ADMIN_USER | Username of the admin account seeded on first start | none | - | yes |
MEISTER_BOOTSTRAP_ADMIN_PASSWORD | Password for that account | none | - | yes |
DB_CONNECTION_STRING | PostgreSQL connection string for the ProPR database | none | - | yes |
PROCURSOR_SHARED_KEY | Shared secret the API and ProCursor authenticate to each other with | none | - | yes |
PROCURSOR_DB_CONNECTION_STRING | PostgreSQL connection string for the ProCursor database | none | - | yes |
PROCURSOR_PROPR_BASE_URL | Internal base URL ProCursor calls the API on | none | absolute URL | yes |
What happens when one of them is absent:
- The bootstrap admin values are read only when no active admin user exists. When one does, they are ignored. When none does and they are absent, startup fails.
- ProPR registers its database-backed features only when
DB_CONNECTION_STRINGis set. - The ProCursor service refuses to start without its own connection string, its shared key, and the API base URL. On the API side, an absent shared key or service base URL turns ProCursor off - see running without ProCursor.
MEISTER_JWT_SECRETis read when the first token is signed, not at startup. A missing or too-short secret lets the stack come up healthy and fails the first sign-in.- On the bundled compose stack, an absent
PROCURSOR_SHARED_KEYdoes not turn ProCursor off. That stack always defines the service and gates the API on it, so ProCursor crash-loops and the API never starts. See running without ProCursor.
Use a long random string for MEISTER_JWT_SECRET and PROCURSOR_SHARED_KEY. What rotating the JWT
secret costs: sign-in and sessions.
Public URL and browser origins
| Variable | What it does | Default | Accepted | Example stack |
|---|---|---|---|---|
MEISTER_PUBLIC_BASE_URL | The externally reachable API base URL, used for webhook listener URLs, SSO redirects and the allowed browser origin | falls back to the request host | absolute URL, including the proxy’s /api prefix if it has one | yes |
CORS_ORIGINS | Extra browser origins allowed to call the API | none | comma-separated origins | yes |
See deployment topology for why both matter behind your own ingress.
Encryption key ring
| Variable | What it does | Default | Accepted | Example stack |
|---|---|---|---|---|
MEISTER_DATA_PROTECTION_KEYS_PATH | Directory holding the key ring that encrypts stored provider and AI credentials and the activated license | unset - keys live on the container’s own filesystem and are lost when it is replaced | ProPR creates the writable directory if absent. Relative paths resolve against the process working directory, and whitespace is preserved. | pinned |
MEISTER_DATA_PROTECTION_PROTECTOR | Encrypts the key files themselves, so a copy of the key directory reads nothing on its own | none | none, certificate, or the name of a protector add-in | no |
MEISTER_DATA_PROTECTION_ADD_IN_DIRECTORY | Directory protector add-ins are read from | data-protection-add-ins beside the service’s own files, which is where the images put the add-ins they ship | directory path | no |
The example stack pins the key path to a fixed in-container path and mounts a named volume there, shared by both services. To put the key ring on durable storage of your own, edit that line and mount your volume in its place.
Both services must be given the same path and the same protector. Give them an absolute path, or the same relative path under working directories backed by the same volume. Relative paths resolve against each process’s working directory. Different working directories can therefore select different key rings.
What the key ring protects and what to back up with it: the encryption key ring.
The certificate protector
MEISTER_DATA_PROTECTION_PROTECTOR=certificate encrypts the key files with a PKCS#12 certificate you hold.
| Variable | What it does | Default | Accepted | Example stack |
|---|---|---|---|---|
MEISTER_DATA_PROTECTION_CERTIFICATE_PATH | The certificate the key files are encrypted with | none | path to a .pfx file | no |
MEISTER_DATA_PROTECTION_CERTIFICATE_PASSWORD | The password of the certificate files | none | the password, or unset for a file without one | no |
MEISTER_DATA_PROTECTION_PREVIOUS_CERTIFICATE_PATHS | Certificates an earlier key was encrypted with, which stay readable during a rotation | none | paths separated by ;, on every platform | no |
One password applies to every file in the list, the current certificate and the previous ones alike. To
rotate to a certificate carrying a password of its own, export the superseded certificate again under the new
password, list that copy in MEISTER_DATA_PROTECTION_PREVIOUS_CERTIFICATE_PATHS, and set
MEISTER_DATA_PROTECTION_CERTIFICATE_PASSWORD to the new password. Keep a superseded certificate listed
until every key it encrypted has expired; a key removed from the list is a key nothing can read. The
semicolon separates the entries, so a certificate whose path contains one cannot be listed: move or rename
that file. A certificate the service cannot open, and a protector name nothing provides, both stop the
service from starting, with the variable named.
The Azure Key Vault protector
MEISTER_DATA_PROTECTION_PROTECTOR=azure-key-vault wraps the key ring with a Key Vault key. Both service
images carry this protector, so selecting it needs no rebuild.
| Variable | What it does | Default | Accepted | Example stack |
|---|---|---|---|---|
MEISTER_DATA_PROTECTION_AZURE_KEY_VAULT_KEY_ID | The vault key the ring is wrapped with | none | a key identifier URL, https://<vault>.vault.azure.net/keys/<key>/<version> | no |
MEISTER_DATA_PROTECTION_AZURE_BLOB_URI | Stores the key ring in a blob instead of a directory | unset: the ring stays where MEISTER_DATA_PROTECTION_KEYS_PATH points | a blob URL | no |
The service authenticates to Azure with the credential described under
a process-wide Azure credential. That identity needs wrap and unwrap on
the key, and read and write on the blob where one is configured. Each URL you set must use https and name a
host; the blob URI stays optional. A missing key identifier stops the service from starting, with the
variable named. Give both services the same key, and the blob URI where you use one.
Sessions and sign-in protection
| Variable | What it does | Default | Accepted | Example stack |
|---|---|---|---|---|
MEISTER_SESSION_IDLE_MINUTES | Idle timeout, renewed by activity | 480 | any positive integer | no |
MEISTER_SESSION_ABSOLUTE_HOURS | Absolute session lifetime, fixed at sign-in | 72 | any positive integer | no |
MEISTER_AUTH_LOCKOUT_MAX_ATTEMPTS | Consecutive failed passwords that trigger a lockout | 5 | 1–100 | no |
MEISTER_AUTH_LOCKOUT_BASE_MINUTES | Lockout duration at the first threshold | 15 | 1–1440 | no |
MEISTER_AUTH_LOCKOUT_MAX_MINUTES | Cap on the exponentially backed-off lockout duration | 60 | 1–10080 | no |
MEISTER_AUTH_RATELIMIT_ENABLED | Whether the per-IP limiter on the auth endpoints runs | true | true, false | no |
MEISTER_AUTH_RATELIMIT_PERMITS | Auth requests permitted per window per client IP | 20 | 1–10000 | no |
MEISTER_AUTH_RATELIMIT_WINDOW_SECONDS | Length of that window | 60 | 1–3600 | no |
What these controls are for: sign-in and sessions.
Single sign-on for platform administrators
Platform administrators can sign in through one OpenID Connect identity provider beside the local form at
/login. This sign-in needs a commercial license that includes single sign-on (sso-authentication, see
editions); without it, ProPR ignores the provider and offers the local form only.
Setting MEISTER_PLATFORM_ADMIN_SSO_AUTHORITY turns the provider on. How the role decides the rights of an account
is described under platform administrators and the identity provider of the installation.
| Variable | What it does | Default | Accepted | Example stack |
|---|---|---|---|---|
MEISTER_PLATFORM_ADMIN_SSO_AUTHORITY | The issuer URL of the provider, for example https://id.example.com/realms/staff | unset: platform administrators sign in with local accounts only | absolute https URL without credentials, query or fragment; http in Development | no |
MEISTER_PLATFORM_ADMIN_SSO_METADATA_URL | The address ProPR reads the provider’s discovery document from | <authority>/.well-known/openid-configuration | absolute https URL without credentials or fragment; http in Development | no |
MEISTER_PLATFORM_ADMIN_SSO_CLIENT_ID | The client ID of ProPR at the provider | none | required with the authority | no |
MEISTER_PLATFORM_ADMIN_SSO_CLIENT_SECRET | The client secret of ProPR at the provider | none | required with the authority | no |
MEISTER_PLATFORM_ADMIN_SSO_ROLE_CLAIM | The ID token claim that lists the roles of the account | roles | the name of a top-level claim whose value is a string or an array of strings | no |
MEISTER_PLATFORM_ADMIN_SSO_REQUIRED_ROLE | The role that gives an account platform administrator rights | none | required with the authority; compared case-sensitively | no |
MEISTER_PLATFORM_ADMIN_SSO_DISPLAY_NAME | The name on the sign-in button | the host of the authority | at most 80 characters on one line | no |
Register ProPR at the provider as a confidential client that uses the authorization code flow, with
<MEISTER_PUBLIC_BASE_URL>/auth/platform-admin/callback as its redirect URI. Without MEISTER_PUBLIC_BASE_URL,
the redirect URI starts with the address the browser reaches the API on. When the user interface runs on another
origin than MEISTER_PUBLIC_BASE_URL, list that origin in CORS_ORIGINS, because the sign-in returns the access
token only to the origins named under
the return address of a sign-in. ProPR sends a PKCE code challenge
(S256) and a nonce, and it sends the client secret in the body of the token request (client_secret_post).
It requests the scopes openid, profile and email.
The ID token must carry the role claim at its top level. Keycloak puts realm roles into the nested claim
realm_access.roles by default, which ProPR does not read; add a realm role mapper to the client that writes the
roles into a claim named roles of the ID token.
Set MEISTER_PLATFORM_ADMIN_SSO_METADATA_URL when ProPR reaches the provider at another address than the browser
does, for example over an internal network. The discovery document at that address must name the authority as its
issuer, or ProPR refuses every sign-in through the provider. Outside Development, the authorization endpoint and the
token endpoint that the document names must use https as well.
Single sign-on for provisioned users
Commercial feature. This sign-in needs a commercial license that includes single sign-on
(sso-authentication, see editions).
A hosting platform that runs ProPR for its customers provisions their accounts through the administration API, and
these customers sign in through one OpenID Connect identity provider beside the other sign-ins at /login.
Setting MEISTER_PROVISIONED_USER_SSO_AUTHORITY turns on the sign-in and the
provisioning endpoints. How the platform decides who signs in is described under
provisioned users and the identity provider of a hosting platform.
| Variable | What it does | Default | Accepted | Example stack |
|---|---|---|---|---|
MEISTER_PROVISIONED_USER_SSO_AUTHORITY | The issuer URL of the provider, for example https://id.example.com/realms/customers | unset: no sign-in of provisioned users, and the provisioning endpoints answer 404 | absolute https URL without credentials, query or fragment; http in Development | no |
MEISTER_PROVISIONED_USER_SSO_METADATA_URL | The address ProPR reads the provider’s discovery document from | <authority>/.well-known/openid-configuration | absolute https URL without credentials or fragment; http in Development | no |
MEISTER_PROVISIONED_USER_SSO_CLIENT_ID | The client ID of ProPR at the provider | none | required with the authority | no |
MEISTER_PROVISIONED_USER_SSO_CLIENT_SECRET | The client secret of ProPR at the provider | none | required with the authority | no |
MEISTER_PROVISIONED_USER_SSO_DISPLAY_NAME | The name on the sign-in button | the host of the authority | at most 80 characters on one line | no |
Register ProPR at the provider as a confidential client that uses the authorization code flow, with
<MEISTER_PUBLIC_BASE_URL>/auth/provisioned-user/callback as its redirect URI. ProPR reads the subject of the ID
token and no role. The return address, PKCE, the nonce, the client secret in the body of the token request, the scopes
and the metadata URL are the same as under
single sign-on for platform administrators.
ProPR records every provisioned user under the authority without a trailing slash. After a change of
MEISTER_PROVISIONED_USER_SSO_AUTHORITY, the users that the platform provisioned under the former authority no longer
sign in, and the platform provisions them again.
Personal access tokens of provisioned users
MEISTER_PROVISIONED_USER_PAT_MAX_DAYS limits the lifetime of personal access tokens owned by provisioned users.
Leave it unset to impose no installation limit, or set a whole number from 1 to 3650. Hosted cells can set it to 90.
Platform administrators and accounts that were not provisioned are exempt.
ProPR assigns new tokens the earlier of the requested expiry and their creation time plus the limit. An omitted expiry uses the limit. Existing tokens also expire at their creation time plus the configured limit, even when they were issued without an expiry. An earlier stored expiry still applies. Token listing and authentication use the same effective expiry. The Personal Access Tokens page shows the applicable limit and the resulting expiry.
The setting takes effect after a restart. Lowering it can invalidate existing customer tokens immediately. Increasing or removing it can make an existing token usable again if its stored expiry and revocation state allow it. Revoke a token to end its access independently of future setting changes.
Installation sign-in policy
An installation that signs people in only through its own identity providers turns off the password sign-in and the
identity providers of tenants. A cell of a hosting platform, whose people sign in through the providers for platform
administrators and for provisioned users, sets both variables to false.
| Variable | What it does | Default | Accepted | Example stack |
|---|---|---|---|---|
MEISTER_LOCAL_SIGN_IN_ENABLED | Whether people sign in with a user name and a password, at /login and at the local sign-in of a tenant | true | true, false | no |
MEISTER_TENANT_SSO_ENABLED | Whether the members of a tenant sign in through the identity providers that the tenant configures | true | true, false | no |
With MEISTER_LOCAL_SIGN_IN_ENABLED=false, ProPR refuses the password sign-in of every account, also in a tenant
that allows local login. The sign-in page shows no password form. ProPR changes no password and creates no account
with a password. Personal access tokens keep working, so scripts and a hosting platform call the API with the
X-User-Pat header as before. ProPR still creates the bootstrap administrator from MEISTER_BOOTSTRAP_ADMIN_USER
and MEISTER_BOOTSTRAP_ADMIN_PASSWORD when no active platform administrator exists. The password of that account
works again once the variable is true.
Without the password sign-in, platform administrators sign in only through the
identity provider for platform administrators. ProPR therefore refuses
to start when MEISTER_PLATFORM_ADMIN_SSO_AUTHORITY is not set, when the license does not grant
sso-authentication, and when the license cannot be read at startup. The error message contains the names of the
variables. If the license stops granting sso-authentication while ProPR runs, platform administrators cannot sign
in, and the next start fails until a license grants it again or the variable is true.
With MEISTER_TENANT_SSO_ENABLED=false, ProPR offers no identity provider of a tenant and refuses the sign-ins
through these providers. A tenant administrator cannot create a provider or save one as enabled, and can still
disable and delete providers. ProPR keeps the providers and offers the enabled ones again once the variable is true.
A session that started before either change runs until it ends.
Switching to installation identity providers
An installation keeps a way for administrators to sign in when it turns the sign-ins off in this order:
- Start a new installation with
MEISTER_LOCAL_SIGN_IN_ENABLED=trueso ProPR applies its migrations and creates the bootstrap administrator. License activation requires the administration API;--verify-licenseverifies a document without activating it. - Configure the identity provider for platform administrators, and for a hosting platform the
identity provider for provisioned users. Activate a license that grants
sso-authentication. After the restart, the log of the start contains the message that platform administrators can sign in through the provider. - Sign in through the provider with an account that holds the required role. ProPR gives the account platform administrator rights.
- If scripts or a hosting platform need API access, create their tokens under Settings → Personal Access Tokens. Tokens that exist keep working. An SSO administrator’s tokens are revoked when that account signs in without the required administrator role; use a dedicated account for automation.
- Set
MEISTER_TENANT_SSO_ENABLED=false, so the members of tenants sign in through the identity providers of the installation only. - Set
MEISTER_LOCAL_SIGN_IN_ENABLED=falseand restart. If the start fails, the error message contains the name of the variable to correct. WithMEISTER_LOCAL_SIGN_IN_ENABLED=true, the installation starts as before. - Open
/login. The customer provider is the primary sign-in, and Staff access opens the administrator provider. An installation without a customer provider shows the administrator provider directly. The password form is hidden.
To use the password sign-in again, for example while the identity provider is unreachable, set
MEISTER_LOCAL_SIGN_IN_ENABLED=true and restart. The bootstrap administrator then signs in with its password.
Review workers
| Variable | What it does | Default | Accepted | Example stack |
|---|---|---|---|---|
WORKER_MAX_CONCURRENT_REVIEW_JOBS | How many review jobs one API instance runs at once | 4 | 1–64 | no |
WORKER_POLL_INTERVAL_MILLISECONDS | How often the worker looks for pending jobs | 2000 | 10–60000 | no |
WORKER_STUCK_JOB_TIMEOUT_MINUTES | Retired. Accepted and ignored; the worker warns at startup when it is set | - | - | no |
WORKER_MAX_CONCURRENT_REVIEW_JOBS takes effect only with a commercial license that allows parallel
review execution. Without one, ProPR runs one review at a time. Where the license allows fewer concurrent
reviews than this setting, the license limit applies - see editions. How this
setting and AI_MAX_FILE_REVIEW_CONCURRENCY combine into peak load:
review workers.
A platform administrator can also limit the concurrent reviews of one tenant. That limit counts the reviews of the tenant on every instance and runner, and it applies together with this setting and the license limit - see tenant review limits.
The runner host
Read by the runner container, not by the control plane. The runner exposes a health endpoint and nothing else.
| Variable | What it does | Default | Accepted | Example stack |
|---|---|---|---|---|
RUNNER_CONTROL_PLANE_URL | Base URL of the control plane it leases from | none | absolute URL | no |
RUNNER_CREDENTIAL | The credential issued at enrollment | none | - | no |
RUNNER_REGISTRATION_TOKEN | Operator-issued token, spending one of its uses to enroll | none | - | no |
RUNNER_DISPLAY_NAME | Operator-facing name in the registry | the machine name | - | no |
RUNNER_TAGS | Tags this runner declares | none | comma-separated | no |
RUNNER_CAPACITY | How many jobs it runs at once | 2 | 1-64 | no |
RUNNER_POLL_INTERVAL_SECONDS | Wait between asks when the last found no work | 5 | 1-3600 | no |
RUNNER_MAX_BACKOFF_SECONDS | Backoff ceiling while the control plane is unreachable | 60 | 5-3600 | no |
RUNNER_WORK_ROOT | Where leased jobs are worked | a temp directory | writable path | no |
RUNNER_LOG_LEVEL | Minimum log level | Information | a Serilog level name | no |
The control plane reads these two, not the runner. They govern the registry the fleet is listed from:
| Variable | Meaning | Default | Range | Secret |
|---|---|---|---|---|
RUNNER_PRUNE_UNSEEN_DAYS | How long a runner may be silent before its row is removed. 0 keeps every row until an operator deletes it | 30 | 0-3650 | no |
RUNNER_PRUNE_INTERVAL_SECONDS | How often the prune sweep runs | 3600 | 60+ | no |
A runner holds its registration token and its credential in memory only, so a host that restarts enrolls
again as a new runner and leaves its earlier row in the registry. The sweep deletes rows that have not
been heard from within RUNNER_PRUNE_UNSEEN_DAYS. It skips a runner still holding a lease and removes it
on a later sweep.
An enrolled runner counts towards the licensed number of runners as long as its credential is valid, whether or not the host is still running. A runner left behind by a restart therefore holds its place until the credential expires or the sweep deletes it. Deleting it on the Runners page frees the place immediately.
The runner reads OTLP_ENDPOINT like every other service. With none set it installs no exporter.
RUNNER_CONTROL_PLANE_URL must be https, except for loopback addresses. The credential is sent on
every call.
Set RUNNER_REGISTRATION_TOKEN, not RUNNER_CREDENTIAL. Issue the token in the admin UI and give
it to the host. The runner exchanges it for a credential on its first cycle and renews that credential
before it expires. A host given neither reports that on /healthz and keeps running.
A registration token is single-use by default, and every enrollment spends one use. A restart is an enrollment, so a host you start by hand needs a fresh token each time it restarts, unless you issue the token for more than one use.
Issue a token for as many hosts as the deployment will start. A scaling group’s replicas come up without an operator present to issue each of them a token. Give the group one token whose enrollment count covers the replicas it may run, and keep it in the platform’s secret store. The Runners page shows each token’s remaining uses, and a token can be revoked at any point. To rotate one, issue the replacement before revoking the old token.
Both bounds are optional. Leave the lifetime empty for a token that does not expire, and the enrollment count empty for one with no limit. Set both to cover the scaling window you expect, and leave them empty only where you have decided a standing credential is acceptable.
A token is a credential. Whoever holds it can enroll as many hosts as its remaining uses allow, for as long as its lifetime runs. Keep an unbounded token in a secret store with a named owner, and rotate it on the cadence you would rotate any other standing credential.
To remove an enrolled host, revoke the runner, not its token. A token’s use is spent at enrollment, so revoking the token afterwards stops only the uses that remain. Revoking the runner makes every call it makes fail authentication. A lease it holds expires by itself, up to one lease duration later.
What touches the runner’s disk. The repository content under review sits in plaintext under
RUNNER_WORK_ROOT while a job runs, and is purged when the job ends and again at startup. Everything
else a review produces - trace, results, spend - is held in memory, batched to the control plane, and
never written to disk. Purging cannot cover a host imaged or destroyed mid-job, so where disk remanence
matters, put RUNNER_WORK_ROOT on an encrypted or ephemeral volume.
RUNNER_CREDENTIAL is for a host that already has one, such as a redeploy that must keep its registry
identity. Leave it unset otherwise.
The runner requests a lease only when it has a free slot. An unreachable control plane, a full slot
pool, an unsupported contract version and a drain are each reported on /healthz and retried with
backoff. None of them exits the process. On shutdown the runner releases the leases it holds.
The runner also reads the AI_* review options, because it runs the review pipeline. Set them to
the same values as the control plane. A runner with different values reviews differently, and nothing
reports the difference. It reads no other AI_* value: connections, keys and model bindings stay on the
control plane.
Runner fleet and queue stalls
A runner counts as active when it is enrolled, not revoked, speaks a contract version this control
plane can serve, and was heard from inside RUNNER_ACTIVE_HEARTBEAT_WINDOW_SECONDS.
The control plane does not execute a job itself when an active runner is eligible for that job’s client. Jobs for clients no active runner can serve continue to run in the control plane. There is no setting that re-enables in-process execution for a job a runner could take.
| Variable | What it does | Default | Accepted | Example stack |
|---|---|---|---|---|
RUNNER_ACTIVE_HEARTBEAT_WINDOW_SECONDS | How recently a runner must have been heard from to count as capacity | 120 | 15-3600 | no |
RUNNER_FLEET_EMPTY_SETTLE_SECONDS | How long the fleet must be continuously empty before in-process execution resumes | 300 | 0-3600 | no |
RUNNER_QUEUE_STALL_GRACE_SECONDS | How long work may sit pending with no runner taking it before the queue is called stalled | 600 | 30-86400 | no |
RUNNER_ADVERTISED_URL | Base URL this control-plane replica advertises to runners for job-scoped calls | unset | absolute https URL (loopback may be http) | no |
RUNNER_ACTIVE_HEARTBEAT_WINDOW_SECONDS must not exceed REVIEW_LEASE_DURATION_SECONDS, and startup
fails when it does. A runner counted as available for longer than its leases survive leaves work with
nobody to run it.
RUNNER_FLEET_EMPTY_SETTLE_SECONDS delays only the return to in-process execution. A runner becoming
active takes effect at once.
A stalled queue reports one of NoActiveRunner, NoFreeSlot or NoRunnerMatchesRequiredTags.
Running more than one control-plane replica with runners requires RUNNER_ADVERTISED_URL on every
replica. The replica that grants a lease serves that job: its workspace mirror is on local disk, and
the job’s budget scope, tools and workspace registration live in its process. A runner configured with
only a load-balanced URL reaches whichever replica the balancer picks, and that replica refuses the job’s
calls as though the lease were lost. Set each replica’s own reachable address here. The lease carries it
to the runner, which uses it for everything job-scoped and keeps the load-balanced URL for enrollment and
for asking for work. Unset, the lease carries no address and runners use RUNNER_CONTROL_PLANE_URL for
everything, which works for a single replica.
Review job leases
A review job is claimed under a lease. The claim is a single conditional database write, so exactly one host wins a given job however many are polling. The holder keeps the lease alive by renewing it on a timer that runs independently of review progress. A job whose lease stops being renewed counts as abandoned, however long it had been processing.
| Variable | What it does | Default | Accepted | Example stack |
|---|---|---|---|---|
REVIEW_LEASE_DURATION_SECONDS | How long a claim holds a job before its lease must be renewed | 120 | 30–3600 | no |
REVIEW_LEASE_HEARTBEAT_INTERVAL_SECONDS | How often the holder renews its lease | 20 | 5–1200 | no |
REVIEW_LEASE_HEARTBEAT_JITTER_FRACTION | Random fraction of the interval each renewal is brought forward by | 0.2 | 0–0.5 | no |
REVIEW_LEASE_MAX_HEARTBEAT_FAILURES | Consecutive renewal failures tolerated before the holder stops working on the job | 3 | 1–20 | no |
REVIEW_LEASE_CLAIM_CANDIDATE_LIMIT | How many pending jobs one poll cycle considers | 50 | 1–500 | no |
REVIEW_LEASE_DURATION_SECONDS must be at least three times REVIEW_LEASE_HEARTBEAT_INTERVAL_SECONDS,
and startup fails when it is not. A lease that survives only one or two renewals is lost to a single slow
database call, and the job is handed to another host while the first is still reviewing it.
Raise the duration where reviews run on hosts with slow or intermittent database access. A longer lease also means a dead host’s jobs wait longer before another host can take them over.
Reclaim
A job whose lease expires is taken back and offered again, not failed. Reclaim replaced the retired
WORKER_STUCK_JOB_TIMEOUT_MINUTES.
Reclaims are budgeted, which bounds what a job that keeps being reclaimed can spend. Completing further files clears the consecutive count, so only a job that keeps cycling without progress exhausts its budget. A graceful release during a deploy or scale-in does not count against either budget.
| Variable | What it does | Default | Accepted | Example stack |
|---|---|---|---|---|
REVIEW_LEASE_MAX_CONSECUTIVE_RECLAIMS | Reclaims allowed without completing further files | 3 | 1–50 | no |
REVIEW_LEASE_MAX_TOTAL_RECLAIMS | Reclaims allowed in total, whatever the progress | 12 | 1–500 | no |
REVIEW_LEASE_RECLAIM_BACKOFF_SECONDS | How long a reclaimed job is left alone before it may be reclaimed again | 60 | 0–3600 | no |
REVIEW_LEASE_MAX_RECLAIMS_PER_SWEEP | How many jobs one sweep takes back | 20 | 1–500 | no |
REVIEW_LEASE_RECLAIM_SWEEP_INTERVAL_SECONDS | Seconds between reclaim sweeps | 30 | 5–3600 | no |
REVIEW_LEASE_PUBLICATION_TIMEOUT_MINUTES | How long publication may run before the job counts as stuck | 30 | 1–720 | no |
REVIEW_LEASE_MAX_REVIEW_DURATION_MINUTES | How long one execution of a review may run before it is failed | 180 | 5–1440 | no |
A job that exhausts its reclaim budget is failed with a reason naming the lease loss, so it reads differently from a review that failed for its own reasons.
A review that is publishing its comments is not reclaimable, however long its lease has been gone, because reclaiming it mid-publication posts the same comments twice. Publication has its own longer timeout. A publication that outlives it fails the job instead of retrying it, because some comments may already be posted.
The reclaim rules above deal with a holder that stopped renewing. A holder that keeps renewing is never
taken off its job, however long it takes, so REVIEW_LEASE_MAX_REVIEW_DURATION_MINUTES caps one
execution: past it the next renewal is refused and the job is failed with that reason. The cap counts
from when the attempt started processing, so a reclaimed job gets the full allowance again. The reclaim
budgets above bound how often that can repeat.
The check happens when a renewal is attempted, so an execution can overrun the cap by up to one
REVIEW_LEASE_HEARTBEAT_INTERVAL_SECONDS before it stops. A review that finishes inside that window
publishes normally. The refused renewal cancels the token the review runs under, so the review is
interrupted wherever it had got to, which can be part-way through posting comments. The job ends as
failed, and comments it had already posted stay on the pull request.
REVIEW_LEASE_PUBLICATION_TIMEOUT_MINUTES does not prevent this; it governs how long a job may sit in
publication before a sweep treats it as stuck. Set the cap well above how long a review of your largest
pull requests takes.
A platform administrator can set a shorter maximum review duration for one tenant. A review that reaches it starts no further files and ends as completed, with a summary of the files it has reviewed - see tenant review limits.
Review workspace
| Variable | What it does | Default | Accepted | Example stack |
|---|---|---|---|---|
REVIEW_WORKSPACE_ROOT_PATH | Where repository mirrors and per-review workspaces are stored | a directory under the service account’s local application data | writable directory path | no |
REVIEW_WORKSPACE_MAX_CACHE_SIZE_MEGABYTES | Size the mirror eviction sweep works towards. Mirrors held by a running review are skipped, so the total can exceed it while those reviews run; per-review checkouts are not counted against it | 4096 | 128–1048576 | no |
REVIEW_WORKSPACE_MIRROR_RETENTION_DAYS | How long a mirror is kept after the last fetch into it. The sweep removes a mirror older than this whatever the cache holds in total, and skips one a running review holds | 7 | 1–365 | no |
REVIEW_WORKSPACE_RETENTION_MINUTES | How long an unreferenced workspace directory waits before the sweep removes it. This covers what a failed preparation, an interrupted review, an interrupted release, a failed delete, or a restart leaves behind; a review that ends normally deletes its own checkout at once | 180 | 1–10080 | no |
REVIEW_WORKSPACE_MAX_CONCURRENT_PREPARATIONS | How many workspaces may be prepared at once | 4 | 1–128 | no |
REVIEW_WORKSPACE_FETCH_DEPTH_POLICY | How much of a repository a mirror fetch brings down | full | full, blobless, shallow | no |
REVIEW_WORKSPACE_FETCH_DEPTH | Commits fetched under the shallow policy. Ignored by the others, which also do not check the range | 200 | 1–100000 | no |
A repository nobody has reviewed for REVIEW_WORKSPACE_MIRROR_RETENTION_DAYS is cloned again on its next
review.
One preparation fetches into a mirror and writes a checkout of the repository, so
REVIEW_WORKSPACE_MAX_CONCURRENT_PREPARATIONS bounds how much of the workspace disk is written at any
moment. Lower it on a small disk: running out of space during a checkout fails the review.
The fetch depth policy trades local disk against fetching over the network:
fullfetches commits, trees and file contents. Nothing is fetched again during a review.bloblessfetches commits and trees and leaves file contents on the server, downloading them when something reads them. It needs a server that offers filtered fetches; Azure DevOps and GitHub both do. It saves the file contents of the revisions nothing reads, which in a repository with long history is most of them. A review still downloads the head revision it checks out, and the base-revision files it compares the target side against, as it reads them. The saving therefore grows with the history behind a repository, not with its size at one commit, and the first review after the policy changes pays for what it reads.shallowfetchesREVIEW_WORKSPACE_FETCH_DEPTHcommits. The depth has to exceed the divergence of the pull requests being reviewed: a merge base outside the fetched history cannot be resolved and the review fails to prepare. Preferbloblessfor that reason.
A policy change applies to every existing mirror on its next fetch. A mirror fetched under shallow has
its boundary removed by the next fetch under full or blobless, and one fetched under blobless stops
filtering under full or shallow.
Leaving blobless costs one large fetch. The widening fetch asks for the contents the filter omitted, so
the first preparation after the change transfers and writes the file contents of every fetched revision,
and reviews after it read locally. Expect that fetch to take as long as a fresh clone of the repository,
and the mirror to grow to its unfiltered size.
Why this wants a mounted volume: review workspace.
Background intervals
| Variable | What it does | Default | Accepted | Example stack |
|---|---|---|---|---|
PR_CRAWL_INTERVAL_SECONDS | How often the crawler polls for open pull requests | 60 | clamped to at least 10 | no |
MENTION_CRAWL_INTERVAL_SECONDS | How often the mention scan runs. Each mention configuration may also ask to be visited less often than this | 60 | clamped to at least 10 | no |
THREAD_PASS_SCAN_INTERVAL_SECONDS | How often queued thread passes are picked up and run | 30 | clamped to at least 5 | no |
REVIEW_ARCHIVE_PURGE_INTERVAL_SECONDS | How often the retention purge sweeps archived pull-request content | 3600 | clamped to at least 60 | no |
LICENSE_STAGE_INTERVAL_SECONDS | How often the licensing sweep runs: it reads the license stage and logs transitions, re-observes the system profile, and evaluates the metered author allowance. 0 or a negative value disables all three | 900 | values at or below 0 disable; positive values are clamped to at least 60 | no |
WEBHOOK_DELIVERY_IDLE_POLL_SECONDS | How often an idle installation asks for a queued webhook delivery | 2 | 1–300 | no |
WEBHOOK_DELIVERY_MAX_CONCURRENCY | Deliveries one replica turns into reviews at once | 4 | 1–32 | no |
WEBHOOK_DELIVERY_CLAIM_SECONDS | How long one replica’s claim on a delivery is good for | 300 | 30–3600 | no |
WEBHOOK_DELIVERY_MAX_ATTEMPTS | Attempts a delivery gets before it is kept as failed and no longer retried | 5 | 1–20 | no |
WEBHOOK_DELIVERY_RETRY_BACKOFF_SECONDS | Wait before a failed delivery is eligible again | 30 | 1–3600 | no |
A webhook delivery is answered as soon as it is verified and stored. A worker turns it into a review
afterwards, so no provider’s delivery timeout decides whether a review happens. A backlog is drained
without waiting; WEBHOOK_DELIVERY_IDLE_POLL_SECONDS governs only how often an empty queue is asked.
Raise WEBHOOK_DELIVERY_CLAIM_SECONDS above the slowest intake a large pull request can take on your
providers, because a delivery held longer than that is assumed abandoned and given to another replica.
Raise WEBHOOK_DELIVERY_MAX_CONCURRENCY when reviews are waiting while runners sit idle. Turning a
delivery into a review takes seconds, so a replica working one at a time creates roughly one job every
few seconds however much execution capacity is available. Measured against a three-runner fleet with six
slots, serial intake held the fleet to four slots at peak. The provider’s rate limit is the ceiling,
because each delivery reads a pull request from the provider that sent it.
Crawling and @-mention scanning need a commercial license (editions); the purge sweep does not. What the purge deletes: what ProPR stores.
LICENSE_STAGE_INTERVAL_SECONDS sets how often the licensing sweep runs. Each run logs license stage
changes, re-reads the system profile, and checks the monthly author count. Switching it off changes nothing
about what the license allows, because every capability check reads the license directly. It stops the
profile and author checks from running.
The link to ProCursor
Read by the API to reach ProCursor, and by ProCursor to reach the API.
| Variable | What it does | Default | Accepted | Example stack |
|---|---|---|---|---|
PROCURSOR_REMOTE_MODE | proprManagedRemote to use the service, disabled to run without it | inferred: remote when a service base URL and shared key are both set, otherwise disabled | proprManagedRemote, disabled | yes |
PROCURSOR_SERVICE_BASE_URL | Internal base URL the API calls ProCursor on | none | absolute URL | yes |
PROCURSOR_HEALTH_ENDPOINT | Path the API probes for ProCursor’s health | /healthz | path | no |
PROCURSOR_REQUEST_TIMEOUT_SECONDS | Timeout budget for calls between the two services | 30 | 1–600 | no |
PROCURSOR_RUNTIME_CONFIG_TTL_SECONDS | How long ProCursor caches the runtime configuration it fetches from the API | 300 | 1 or more | no |
ProCursor indexing and queries
Read by the ProCursor service.
| Variable | What it does | Default | Accepted | Example stack |
|---|---|---|---|---|
PROCURSOR_MAX_INDEX_CONCURRENCY | Concurrent indexing jobs | 2 | 1–32 | no |
PROCURSOR_MAX_QUERY_RESULTS | Results returned per knowledge or symbol query | 5 | 1–20 | no |
PROCURSOR_MAX_SOURCES_PER_QUERY | Sources scanned for one query | 20 | 1–50 | no |
PROCURSOR_CHUNK_TARGET_LINES | Target chunk size, in lines, for indexed text | 120 | 10–1000 | no |
PROCURSOR_MINI_INDEX_TTL_MINUTES | Lifetime of a review-time mini-index overlay | 30 | 1–1440 | no |
PROCURSOR_REFRESH_POLL_SECONDS | How often the indexing worker looks for work | 30 | 1–3600 | no |
PROCURSOR_INDEX_JOB_LEASE_SECONDS | How long a claim holds an indexing job before its lease must be renewed | 300 | 10–3600 | no |
PROCURSOR_TEMP_WORKSPACE_RETENTION_MINUTES | How long a stale temporary indexing workspace is kept | 120 | 1–1440 | no |
PROCURSOR_EMBEDDING_DIMENSIONS | Expected embedding vector width; must match the embedding model in use | 1536 | 1–4096 | no |
PROCURSOR_TOKEN_USAGE_ROLLUP_POLL_SECONDS | How often ProCursor aggregates its token usage | 900 | 10–86400 | no |
PROCURSOR_TOKEN_USAGE_EVENT_RETENTION_DAYS | Retention for raw ProCursor token usage events | 365 | 1–3650 | no |
PROCURSOR_TOKEN_USAGE_ROLLUP_RETENTION_DAYS | Retention for aggregated ProCursor token rollups | 730 | 1–3650 | no |
The indexing worker renews the lease of a running job five times per PROCURSOR_INDEX_JOB_LEASE_SECONDS. When the
lease of a job expires, for example because the ProCursor service stopped while the job ran, the worker returns the
job to the queue, and after the third attempt it marks the job as failed. A job that is cancelled while it runs stops
at the next renewal. When no renewal succeeds, for example because the database cannot be reached, the worker stops
the job before its lease expires, so that the job does not run twice once another worker recovers it.
Review loop budgets
These bound the work one review may do, and the management UI does not expose them. Of everything on this page they move token spend the most - see control cost.
| Variable | What it does | Default | Accepted | Example stack |
|---|---|---|---|---|
AI_MAX_ITERATIONS_LOW | Investigation iterations allowed on a low-complexity file | 5 | 1–100 | yes |
AI_MAX_ITERATIONS_MEDIUM | Same, medium complexity | 10 | 1–100 | yes |
AI_MAX_ITERATIONS_HIGH | Same, high complexity | 20 | 1–100 | yes |
AI_MAX_REVIEW_ITERATIONS | Iteration ceiling for review work that is not per-file, where no complexity tier applies | 20 | 1–100 | no |
AI_MAX_FILE_REVIEW_CONCURRENCY | How many files one review works on in parallel | 3 | 1–10 | no |
AI_MAX_FILE_REVIEW_RETRIES | Retries for a review job with failed file passes | 3 | 1–10 | no |
AI_MAX_RATE_LIMIT_RETRIES | Transparent retries after a provider rate-limit response | 3 | 1–10 | no |
AI_MAX_BACKOFF_SECONDS | Maximum backoff between those retries | 30 | 5–120 | no |
AI_FILE_BATCH_LINES | Lines returned per file-content read the reviewer makes | 100 | 10–1000 | no |
AI_MAX_FILE_SIZE_BYTES | Largest file the reviewer may read; above it the read returns an error instead of content | 1048576 | 1024 or more | no |
A tenant administrator can set both AI_MAX_FILE_SIZE_BYTES and AI_MAX_STRUCTURAL_PARSE_BYTES for one
tenant through the tenant patch endpoint. Setting them requires the budgeting capability, which a commercial
license carries; a value already stored is applied in every edition - see
editions. A tenant value applies to every review that tenant’s clients run, here
and on a runner. A tenant that sets neither reviews at the installation values above. The installation
value of AI_MAX_STRUCTURAL_PARSE_BYTES is the ceiling: the structural analyzer is built with it, so a
tenant value above it is clamped and a file over the installation value is still reported as too large to
parse.
The size of a whole review is bounded per client instead, on the client’s Budget tab, and those bounds refuse a review before it starts - see what you can tune.
AI_MAX_FILE_REVIEW_CONCURRENCY needs a commercial license for parallel review execution to have any
effect - see editions. Without it a review works on one file at a time, the
same rule the worker applies to whole jobs. Sizing it alongside
WORKER_MAX_CONCURRENT_REVIEW_JOBS: review workers.
When a provider rate-limits a call, ProPR waits for the delay that provider asked for. The delay is read
from the Retry-After header, or from the error message when the provider states it there instead.
AI_MAX_BACKOFF_SECONDS caps that wait and AI_MAX_RATE_LIMIT_RETRIES sets how many further attempts the
call gets. While one connection is rate-limited, other calls bound for the same connection wait too, so a
review working on several files at once stops sending requests the provider has already refused.
Which files land in which complexity tier is decided per review - see reviews.
Evidence judge budgets
The evidence judge requests low reasoning effort when the bundled catalog advertises it for the exact
model and the selected transport supports it. Unknown or incompatible bindings use provider defaults.
AI_VERIFICATION_REASONING_EFFORT sets one effort for the judge and both contradiction checks on every
model. Set it only for models that accept a reasoning effort, because a model that does not accept one
rejects the request, and the output ceilings include the reasoning tokens.
The output ceiling includes reasoning tokens. An output-limit response, or an unusable verdict at the
measured ceiling, receives one larger retry. Completed tool results remain available; the retry permits
no further lookups and shares the claim’s 120-second deadline. Both attempts are recorded and charged.
A retry that also exhausts its
ceiling withholds the finding with judge_output_limit.
| Variable | What it does | Default | Accepted | Example stack |
|---|---|---|---|---|
AI_EVIDENCE_JUDGE_MAX_OUTPUT_TOKENS | Initial reasoning-inclusive output ceiling per judge call | 4000 | 512–32768 | no |
AI_EVIDENCE_JUDGE_RETRY_MAX_OUTPUT_TOKENS | Output ceiling for the single recovery attempt | 8000 | 512–32768; greater than the initial ceiling | no |
AI_VERIFICATION_REASONING_EFFORT | Reasoning effort the judge and both contradiction checks request | none | none, low, medium, high | no |
Both the control plane and runner read these installation settings. A parsable value outside these bounds, or a retry ceiling no greater than the initial ceiling, fails startup. Unparsable values use the defaults. These checks apply only to the two judge budgets. Configure both ceilings within the judge model’s supported completion limit.
Finding thresholds
| Variable | What it does | Default | Accepted | Example stack |
|---|---|---|---|---|
AI_CONFIDENCE_THRESHOLD | Confidence at which the reviewer stops investigating a concern | 70 | 0–100 | no |
AI_CONFIDENCE_FLOOR_ERROR | Inactive compatibility setting; file confidence does not change finding classification | 80 | 0–100 | yes |
AI_CONFIDENCE_FLOOR_WARNING | Inactive compatibility setting; file confidence does not change finding classification | 60 | 0–100 | yes |
AI_QUALITY_FILTER_THRESHOLD | Total comment count across all files below which the cross-file quality pass is skipped | 20 | 1–500 | yes |
A finding that clears these thresholds still goes through the publication gate - see why a finding did not get posted.
The marker on posted comments
| Variable | What it does | Default | Accepted | Example stack |
|---|---|---|---|---|
MEISTER_AI_GENERATED_MARKER | Sentence ProPR ends every comment it posts with, stating that the content was generated by AI | Generated by ProPR, an AI code reviewer. | up to 200 characters on one line, without <, >, &, ", *, _, `, [, ] or \, and not starting with / or # | yes |
The marker is the last line of every summary, inline finding and reply ProPR posts, on all four hosts.
The accepted characters are what a host is guaranteed to render as written. GitLab encodes the comment
body, and a leading slash there starts a quick action. ProPR wraps the marker in Markdown emphasis, and
each refused character is Markdown syntax a host can render as a link, a code span or a heading in some
position, not one that always does: [ needs a following (url) to form a link, and # starts a
heading only at the beginning of a line. Validation runs once against the configured wording and cannot
know where in the finished comment the marker will land, so it refuses the character regardless of
position. A value outside the accepted set stops startup with an error naming the variable. The wording
applies to the whole installation, not to one client.
Thread memory
| Variable | What it does | Default | Accepted | Example stack |
|---|---|---|---|---|
AI_MEMORY_TOP_N | Past resolutions retrieved per file review | 3 | 1–20 | yes |
AI_MEMORY_MIN_SIMILARITY | Minimum similarity for a past resolution to be considered | 0.80 | 0.0–1.0 | yes |
AI_MEMORY_EMBEDDING_DIMENSIONS | Embedding width; must match the model bound to the embedding purpose | 1536 | 64–4096 | yes |
AI_POSTED_FINDING_MIN_SIMILARITY | Minimum similarity for a finding to count as a duplicate of one already posted on the pull request | 0.85 | 0.0–1.0 | yes |
The embedding model itself is configured per client, not here - see purposes.
Code-structure tools
The AI_ENABLE_* switches turn these tools off and fall back to simpler behaviour. The budgets bound how
much work one lookup may do before it returns what it has, marked truncated.
| Variable | What it does | Default | Accepted | Example stack |
|---|---|---|---|---|
AI_ENABLE_STRUCTURAL_BOUNDARY_RESOLUTION | Use the structural analyzer to pick context boundaries instead of a line heuristic | true | true, false | no |
AI_STRUCTURAL_PARSE_TIMEOUT_MS | Per-file budget for that analysis before it falls back to the heuristic | 200 | 10–5000 | no |
AI_MAX_STRUCTURAL_PARSE_BYTES | Largest file it will parse | 1048576 | 1024–5242880 | no |
AI_ENABLE_STRUCTURAL_REFERENCE_TOOLS | Register the cross-file reference and definition lookups and the caller-evidence feed | true | true, false | no |
AI_MAX_REFERENCE_CANDIDATE_FILES | Files scanned per reference lookup | 200 | 1–2000 | no |
AI_MAX_REFERENCE_RESULTS | Confirmed sites returned per lookup | 50 | 1–1000 | no |
AI_MAX_REFERENCE_RESULT_CHARS | Character budget for one lookup result | 8000 | 256–64000 | no |
AI_REFERENCE_RESOLUTION_TIMEOUT_MS | Wall-clock budget for one lookup | 4000 | 50–30000 | no |
AI_ENABLE_RETAINED_TOOL_EVIDENCE | Keeps fetched file content across context compaction instead of dropping it, so the reviewer does not re-read content it already holds. Set to false to drop it | true | true, false | no |
Linked work items and issues
| Variable | What it does | Default | Accepted | Example stack |
|---|---|---|---|---|
AI_ENABLE_LINKED_ITEM_TOOLS | Whether the on-demand linked-item lookups are available; false withholds them even from clients that include linked items | true | true, false | no |
AI_MAX_LINKED_ITEMS_IN_CONTEXT | Linked items injected into the review context before the rest are dropped | 5 | 1–50 | no |
AI_MAX_LINKED_ITEM_DESCRIPTION_CHARS | Length each linked item’s description is truncated to | 2000 | 128–20000 | no |
AI_MAX_LINKED_ITEM_TOOL_CALLS | On-demand linked-item lookups allowed per review | 6 | 0–100 | no |
AI_MAX_LINKED_ITEM_TOOL_RESULT_CHARS | Character budget for one such result | 8000 | 256–64000 | no |
AI_LINKED_ITEM_TOOL_TIMEOUT_MS | Budget for one such lookup; on overshoot it returns empty | 5000 | 100–30000 | no |
Whether linked items are pulled in at all is a per-client setting - see what you can tune.
AI provider add-ins
| Variable | What it does | Default | Accepted | Example stack |
|---|---|---|---|---|
AI_PLUGIN_DIRECTORY | Directory holding privately built AI provider add-ins, one folder per family | plugins beside the application | readable directory path; a relative one resolves against the application directory | no |
The AI provider families the image ships load from a directory inside the image, at a path the image fixes, whatever this is set to. This names the second directory, for a family you built yourself. A directory that is not there is reported once at startup and is not a failure, since most installations have none. The directories are read once while the host starts, so a file added or removed afterwards changes nothing until the host restarts.
Whoever can write to this directory runs code in the API host. The host loads the assemblies it finds there at startup and executes them in its own process, with its configuration, its network and its database connection. An add-in is not sandboxed and is not meant to be. Treat write access to the directory as equivalent to deploy access: mount it read-only where the platform allows it, keep it out of any volume a review or a webhook can write to, and check a binary before you put it there.
How to install a family and what the host does with one it cannot load: installing an AI provider add-in.
Egress and stored content
| Variable | What it does | Default | Accepted | Example stack |
|---|---|---|---|---|
MEISTER_ALLOW_PRIVATE_EGRESS | Permit outbound calls to private, loopback and link-local addresses, for AI endpoints and source-control hosts alike | false | true, false | no |
AI_CAPTURE_REASONING_IN_PROTOCOL | Record the model’s reasoning into the review protocol; it can contain verbatim source excerpts | true | true, false | no |
AI_ALLOW_PRIVATE_EGRESS is the earlier name of the same setting and is still read, so an installation
that carries it keeps working. Where both are set, MEISTER_ALLOW_PRIVATE_EGRESS decides.
What private egress does and does not permit:
outbound request protection. What the captured
reasoning contains, and when to turn it off:
what ProPR stores. A tenant can override
AI_CAPTURE_REASONING_IN_PROTOCOL for its own reviews:
model reasoning in the job trace.
Platform administrator elevations
| Variable | What it does | Default | Accepted | Example stack |
|---|---|---|---|---|
MEISTER_PLATFORM_ADMIN_ELEVATION_MAX_MINUTES | Longest duration, in minutes, that a platform administrator can request for one elevation into the content of a tenant | 240 | 1–1440 | no |
What an elevation grants and what ProPR records about it: platform administrator access to tenant content.
A process-wide Azure credential
| Variable | What it does | Default | Accepted | Example stack |
|---|---|---|---|---|
AZURE_TENANT_ID | Microsoft Entra tenant of the service principal, or of the federated workload identity | none | - | no |
AZURE_CLIENT_ID | Application ID of the service principal, or of the user-assigned managed identity | none | - | no |
AZURE_CLIENT_SECRET | Its client secret | none | - | no |
All three together supply one Azure service principal to the backend process. That credential serves two unrelated purposes: Azure DevOps operations for a client that has no connection of its own - see global Azure fallback - and Azure-hosted AI endpoints configured with Azure Identity instead of a key. The key ring is wrapped with it where the Azure Key Vault protector is configured - see the Azure Key Vault protector.
Set AZURE_CLIENT_SECRET without both ids and the service does not start, naming the id that is missing. A
secret is usable only as part of a service principal, and the ambient credential would otherwise authenticate
as an identity you did not choose.
Every other combination uses the ambient Azure credential: a managed identity, a federated workload identity,
an Azure CLI login, or whatever else the host offers. The two ids keep the meanings Azure gives them there.
AZURE_CLIENT_ID with no secret names the user-assigned managed identity to authenticate as.
AZURE_TENANT_ID and AZURE_CLIENT_ID with no secret are the workload identity pair, which the ambient
credential reads together with the federated token file its own variable points at.
Code Insights
Client collection policy and the commercial Code Insights capability gate analytics. These variables control score coverage, background work and retention; the example stack does not forward them by default.
| Variable | What it does | Default | Accepted |
|---|---|---|---|
CODE_INSIGHTS_MIN_COVERAGE_PERCENTAGE | Advisory percentage of covered source pull requests for range diagnostics | 80 | Integer, clamped to 0–100 |
CODE_INSIGHTS_MIN_COVERED_PULL_REQUESTS | Advisory covered source pull requests for range diagnostics | 1 | Integer, floored at 1 |
CODE_INSIGHTS_CLASSIFICATION_INTERVAL_SECONDS | Interval between classification backlog sweeps | 60 | Integer, floored at 10 seconds |
CODE_INSIGHTS_CATCHUP_INTERVAL_SECONDS | Interval between projection, seal and provider-evidence catch-up sweeps | 21600 | Integer, floored at 600 seconds |
CODE_INSIGHTS_BACKFILL_MAX_JOBS | Maximum jobs processed in one catch-up sweep | 50 | Integer, floored at 1 |
CODE_INSIGHTS_PROVIDER_EVIDENCE_BACKFILL_MAX_PULL_REQUESTS | Maximum retained pull requests refreshed from provider evidence per catch-up sweep | 25 | Integer, clamped to 1–250 |
CODE_INSIGHTS_PROVIDER_EVIDENCE_SWEEP_BUDGET_SECONDS | Time budget for one provider-evidence sweep | 300 | Integer, clamped to 10–600 seconds |
CODE_INSIGHTS_PROVIDER_EVIDENCE_LEASE_MINUTES | Maximum duration of a historical observation claim | 15 | Integer, clamped to 1–60 minutes |
CODE_INSIGHTS_MODEL_EVIDENCE_RETRY_COOLDOWN_MINUTES | Delay between bounded local classification retries for one captured model source | 5 | Integer, clamped to 1–1440 minutes |
CODE_INSIGHTS_PROVIDER_EVIDENCE_RETRY_COOLDOWN_MINUTES | Delay before an unavailable, retryable historical observation is eligible again | 360 | Integer, clamped to 10–1440 minutes |
CODE_INSIGHTS_PURGE_INTERVAL_SECONDS | Interval between source and saved-report expiration sweeps | 3600 | Integer, floored at 60 seconds |
CODE_INSIGHTS_RETENTION_DAYS | Ordinary collected source-data retention | 365 | Integer, floored at 1 day |
CODE_INSIGHTS_REPORT_RETENTION_DAYS | Independent immutable saved-report retention | 365 | Integer, floored at 1 day |
Source expiration preserves reports until their independent expiry. Collection opt-out stops new collection and retains authorized reports until expiry or deletion. Client collection-data deletion and client deletion remove complete reports containing that client’s data, including mixed-client reports. Provider refresh runs after projection and sealing, with its own batch, time and lease limits. Retryable failures use the configured cooldown. Missing usable connections, source coordinates or provider identity, unsupported historical collection and missing observer dependencies pause automatic retries until repair and explicit requeue. Generic provider failures remain retryable when retained evidence cannot establish their cause. Collection-disabled clients are not claimed.
Requeue requires tenant administration and the Code Insights capability. It rejects an active lease or disabled client collection and queues a subsequent bounded sweep without collecting evidence in the request. It preserves retained observations and retention timestamps. Shortening the catch-up interval does not shorten a retryable failure’s cooldown.
Each provider-evidence sweep logs attempted, observed, unavailable and disabled totals. Failures log the pull request identity, bounded failure reason, failing stage and recovery disposition. Unexpected failures include the exception type and numeric diagnostic code without provider messages or payloads.
Coverage settings provide advisory warnings for current range queries. Scores remain available below these minimums when covered evidence supports the calculation. Their effective values are captured with saved reports; changing them does not recalculate a capture. Recall and F1 require at least one covered source. Zero settled misses does not impose an additional sample requirement. Empty denominators remain unavailable. Reviewer performance describes live score ranges and saved reports.
Observability
| Variable | What it does | Default | Accepted | Example stack |
|---|---|---|---|---|
OTLP_ENDPOINT | OTLP collector to export traces to | none - no trace pipeline is built at all, so spans are never assembled | absolute URL | no |
OTEL_EXPORTER_OTLP_ENDPOINT | The same collector, under the name the OpenTelemetry specification gives it. Read when OTLP_ENDPOINT is unset or empty. A managed OpenTelemetry agent injects this name | none | absolute URL | no |
TELEMETRY_HTTP_CLIENT_TRACES | Which outbound requests become spans. foreground skips unattended work: crawl cycles, mention scans and health probes | foreground | foreground, all, off | no |
TELEMETRY_TRACE_SAMPLE_RATIO | Head-sampling ratio applied to the traces that survive the filters | 1.0 - no sampler is installed, which leaves the standard OTEL_TRACES_SAMPLER and OTEL_TRACES_SAMPLER_ARG in charge | 0.0 to 1.0, clamped into range | no |
TELEMETRY_TRACE_IGNORED_PATHS | Request path prefixes that are never traced, inbound or outbound | /healthz,/livez,/metrics | comma-separated path prefixes, leading / optional | no |
LOKI_URL | Grafana Loki instance to ship logs to | none - logs go to stdout only | absolute URL | pinned |
ASPNETCORE_ENVIRONMENT | The runtime environment name | Production when unset | Production, Development, or your own name | pinned |
The three TELEMETRY_ variables do something only while an OTLP endpoint is configured, under either
name. None of them affect /metrics, which keeps counting every request. An unrecognised value is not an
error: the trace mode falls back to foreground and an unparseable ratio to 1.0, so a typo silently
gets you the default and the service still starts. Why you would change them:
trace volume.
Development serves the API documentation UI - see the API reference - and
relaxes the outbound AI egress checks - see
outbound request protection. Use Production
outside development. Where traces and logs end up:
observability.
An optional Azure OpenAI instruction evaluator
| Variable | What it does | Default | Accepted | Example stack |
|---|---|---|---|---|
AI_EVALUATOR_ENDPOINT | Azure OpenAI endpoint for the repository-instruction relevance evaluator | none | Azure OpenAI endpoint URL | no |
AI_EVALUATOR_DEPLOYMENT | Deployment name to call on it | none | - | no |
AI_API_KEY | Key for that endpoint; omit to authenticate with the host’s ambient Azure credential instead | none | - | no |
This evaluator judges which of a repository’s instruction files apply to a diff. It is registered only when the endpoint and the deployment are both set, and it calls Azure OpenAI directly. It does not go through the per-client AI connections, so a client’s provider configuration does not control it. Leave all three unset unless you want it.
AI_EVALUATOR_ENDPOINT follows the address rule every AI endpoint follows: https, and no private,
loopback or link-local address without MEISTER_ALLOW_PRIVATE_EGRESS - see
outbound request protection. An address this
installation refuses stops the service from starting, with the variable named. AI_API_KEY travels in a
request header, so a plain http endpoint would put it on the wire unencrypted.
Discussion synchronization
| Variable | What it does | Default | Accepted | Example stack |
|---|---|---|---|---|
DISCUSSION_RECONCILIATION_INTERVAL_SECONDS | Full discussion reconciliation cadence, independent of PR discovery | 300 | integer seconds, clamped to at least 10 | no |
CRAWL_GLOBAL_CONCURRENCY | Concurrent crawl and discussion operations across the process | 8 | integer, clamped to at least 1 | no |
CRAWL_PROVIDER_HOST_CONCURRENCY | Concurrent operations for one provider and host | 4 | integer, clamped to at least 1 | no |
CRAWL_CLIENT_PROVIDER_HOST_CONCURRENCY | Concurrent operations for one client, provider and host | 1 | integer, clamped to at least 1 | no |
CRAWL_OPERATION_TIMEOUT_SECONDS | Deadline for an individual pull-request synchronization operation | 90 | integer seconds, clamped to at least 10 | no |
Authenticated webhook notifications make affected discussions eligible immediately. Full reconciliation recovers missed notifications and providers with incomplete event coverage. See discussion synchronization for retries, retention and diagnostics.