Install parameters
Every value an installer can set, taken from the chart itself. Image repositories, tags, container ports and resource requests are omitted — the release sets those, and overriding one is a support conversation rather than an install decision.
Defaults are the chart's own. A blank default means the value is unset and the chart expects you to supply it, or that it only applies when some other parameter turns its feature on.
Image source
| Parameter | Default | What it is |
|---|---|---|
global.imageRegistry | Prefix joined onto every prism image, e.g. "myregistry.azurecr.io/prism". Empty = use component repositories verbatim. | |
global.imagePullSecrets | [] | Names of pull secrets that already exist in the namespace. Needed when the registry you mirrored into requires authentication to pull. |
branding
| Parameter | Default | What it is |
|---|---|---|
branding.productName | Replaces the word "Prism" in the title bar and the heading of every page. The rest of each heading is the PAGE ("· ROI Insights", "admin", "· Built-in Self Test"), so pick the product name alone — "Acme Lighthouse", not "Acme Lighthouse ROI Insights". Plain text; markup is escaped, not rendered. | |
branding.logoSvg | Your logo, as SVG SOURCE — not a URL. The appliance may have no route to the internet and must not depend on one to draw its own header, which is the same reason its browser libraries are vendored into the image. Rendered inline, immediately before the product name, sized to the height of the heading (the SVG's own width/height are ignored; give it a viewBox). Supply it with --set-file branding.logoSvg=./logo.svg rather than pasting it in, or paste it under a | block. PNG/JPEG are not accepted: there is nowhere to put a binary in a values file that is also legible in review. It is rebuilt from an allowlist of shape-drawing elements before it reaches a page, so a mark carrying <script>, <style>, <a>, <image>, <use>, <foreignObject> or animation loses those parts — and one whose ROOT element is not <svg>, or which is over 64KB, or which is not well-formed XML, is refused entirely and the header renders with no logo. The app log says which of those happened. Check the header after an upgrade. Backticks are load-bearing above: this comment IS the description in docs/install/parameters.md, and a raw tag there is swallowed by MDX on the docs site (scripts/check_install_docs.py is what says so). |
disclosure
| Parameter | Default | What it is |
|---|---|---|
disclosure.participationDetail | false | may an answer NAME the people in a participation state ("who under Ana has had no pull request activity in 90 days"), or only count them; false (the default) gives the counts and says the names are not available. It does NOT decide what a name is made of — that is hidden on your directory's own columns, so one installation lists staff names and another lists employee ids — and it does NOT close your directory off: a question that groups by a name column returns those names whichever way this is set. What it governs is narrower: whether an answer may attach a person's name to a statement about what they did or did not do. perPersonFloor does NOT reliably keep a small group from being named here: it floors participation counts and names only on a source that declares count_distinct (of the shipped sources a participation question can reach, github only, not Jira or GitLab), only when an OBSERVED count is non-zero and under N, and never the two unknown states, whose people are named at any size — so a window reaching past the index horizon can name a group a narrower window withheld. If names beside these states are not acceptable, leave this false. Set through this value or PRISM_PARTICIPATION_DETAIL; there is no admin-page control for it in 0.15.0. limits.md, "Naming the people behind a participation count" and "What the min-N floor does and does not withhold" |
disclosure.perPersonFloor | 0 | withhold per-person AVERAGES, MEDIANS and PERCENTILES computed over fewer than N observations — on every backend that answers about a named person, whether the rows are keyed on people or the question named them; 0 (the default) publishes them. Which KINDS of figure it covers is the source's own min_n_applies_to declaration, so a source that opts counts or distinct-people counts in has those withheld by this floor too — the shipped github source opts in a count of distinct people. limits.md, "Withholding figures about small groups", says which figures on which backend |
externalCallLog
| Parameter | Default | What it is |
|---|---|---|
externalCallLog.enabled | true | one external_call JSON line per outbound HTTP request or source-database statement, on every container; false turns it off everywhere |
oracle
| Parameter | Default | What it is |
|---|---|---|
oracle.mode | thin | python-oracledb driver mode for every Oracle source: thin | thick. Set thick only for a database that requires Oracle Native Network Encryption, which thin mode cannot negotiate — the symptom is DPY-3001 on the source's probe. oracle-preflight.md, "When a database requires Native Network Encryption", says how to tell |
Data sources
| Parameter | Default | What it is |
|---|---|---|
sources.github | synthetic | pull requests — live needs secrets.githubToken and ingest.github.orgs |
sources.jira | synthetic | issues and story points — live needs secrets.jiraToken and ingest.jira.url |
sources.spend | synthetic | Agent Router token spend: synthetic | indexed | live — indexed and live both need secrets.valetApiKey |
sources.registry.enabled | true | create and seed the source registry and read sources from it (#514) — ON by default since 0.10.0 (#794); false renders nothing at all and is a one-upgrade rollback |
sources.entitlement.contextStreams | 10 | how many context streams this installation is licensed for — stated on the admin page and the self test, never enforced; 0 or a non-number falls back to 10 |
Agent Router
| Parameter | Default | What it is |
|---|---|---|
router.platformUrl | management API (key minting) — REQUIRED in both key modes, byok included: the runner will not start without it. No default: it is your tenant's URL, and ours is not reachable from your network | |
router.proxyUrl | OpenAI-compatible inference — REQUIRED, and an empty value does NOT fall back to platformUrl. No default, for the same reason as platformUrl | |
router.defaultModel | "claude-sonnet-5" | Must name a model the gateway above actually serves. This is the model lever: it applies to every agent that has no model of its own, takes effect on helm upgrade, and is what you change to run against a router with a different catalog. The self test's model catalog line verifies it. |
router.keyMode | auto | auto | byok | mint — which inference credential the runner holds (auto = read it off secrets.*) |
Sign-in email (SMTP)
| Parameter | Default | What it is |
|---|---|---|
smtp.host | REQUIRED when mode=local, e.g. "smtp.example.com" | |
smtp.port | 587 | the relay's submission port — 587 for starttls, 465 for implicit TLS |
smtp.tls | starttls | starttls | implicit | none — anything else fails at render (and the app refuses to start), because an unknown value would otherwise quietly mean cleartext. starttls suits port 587 relays; "none" only for an in-cluster relay whose traffic never leaves the node network. |
smtp.tlsVerify | true | verify the relay's TLS certificate (chain AND hostname). false exists ONLY for relays presenting a private-CA certificate the pod cannot verify — an unverified TLS hop is one anybody on the path can intercept, on the connection carrying sign-in links. For a relay with no TLS at all, tls: none is the honest setting, not this. |
smtp.username | empty = no AUTH (relays that trust network position) | |
smtp.fromAddress | REQUIRED when mode=local: the sender, e.g. "[email protected]" — must be an address your relay accepts MAIL FROM for | |
smtp.fromName | "Prism" | display name on the sign-in mail, beside fromAddress |
Secrets
| Parameter | Default | What it is |
|---|---|---|
secrets.appJwtSecret | HS256 secret the App signs both tokens with (Pattern A) | |
secrets.encryptionKey | base64 of 32 random bytes (runner at-rest key custody) | |
secrets.mcpSharedSecret | REQUIRED — the secret every internal query server checks on every request; openssl rand -hex 32, and see docs/install/rotate-credentials.md | |
secrets.inferenceKey | bring-your-own project-scoped inference key the runner uses directly; set this OR tarePlatformApiKey | |
secrets.tarePlatformApiKey | admin platform key the runner uses to mint a key per user (per-user cost attribution); set this OR inferenceKey | |
secrets.postgresPassword | REQUIRED on an external instance as much as the bundled one: it is PGPASSWORD for every component and the credential in the runner's DSN. With bundled=true it is the superuser password the in-cluster instance is created with; with bundled=false it is the password for postgres.external.user | |
secrets.valetApiKey | Router management credential — for spend=live/indexed | |
secrets.githubToken | read-only GitHub credential, shared by the ingest and the live MCP — fine-grained, all Read: Pull requests, Issues, Metadata, Contents, Checks, Commit statuses | |
secrets.githubAppPrivateKey | the GitHub App's private key, PEM — the .pem GitHub generated for the App, whole, both ----- lines included. Only with ingest.github.app.appId and .installationId; see those | |
secrets.jiraToken | Jira API token (Cloud) or PAT (DC) — only for jira=live | |
secrets.smtpPassword | SMTP AUTH password — only for identity mode local with smtp.username set | |
existingSecret | Name of a Secret you created yourself carrying the keys listed above. Set it and the chart renders no Secret of its own, so no credential passes through your values file or Helm's release history. |
Postgres
| Parameter | Default | What it is |
|---|---|---|
postgres.bundled | true | true = run Postgres in-cluster; false = use external.* below |
postgres.storage | 10Gi | PVC size for the bundled instance; ignored when bundled=false. Also what the self test measures headroom against — less a 2 GiB reserve for the WAL, temp files and filesystem overhead it cannot see — so if you expand the PVC out of band raise this too or the warning fires against the old number. docs/install/limits.md has the sizing formula |
postgres.storageClassName | cluster default when empty | |
postgres.startupWaitSeconds | 180 | how long agent-runner and synth-seed wait for the bundled Postgres to accept connections before starting anyway; ignored when bundled=false |
postgres.readinessProbe.timeoutSeconds | 5 | how long one pg_isready may take before the probe counts it as failed; at Kubernetes' default of 1 a measured install capped at 50m CPU never became Ready with the server up |
postgres.readinessProbe.periodSeconds | 5 | how often the probe runs |
postgres.readinessProbe.initialDelaySeconds | 5 | how long after the container starts before the first probe |
postgres.readinessProbe.failureThreshold | 3 | consecutive failures before a Ready pod is taken out of the Service; does not affect how long a first boot takes to become Ready |
postgres.external.host | hostname of your managed instance — required when bundled=false | |
postgres.external.user | postgres | role the app and runner connect as; needs CREATE EXTENSION vector |
postgres.external.database | prism | the datapond — ingested GitHub/Jira/spend data |
postgres.external.runnerDatabase | runner | second logical database, for the agent runtime's own state |
postgres.external.sslmode | require | libpq sslmode for every PG client AND the runner's DSN option, which the chart makes mean the same thing; standard libpq values only, and require encrypts without verifying the server certificate |
Synthetic data
| Parameter | Default | What it is |
|---|---|---|
synthSeed.seed | prism | RNG seed — the same value regenerates the same fake org |
synthSeed.people | 60 | how many synthetic engineers to invent |
synthSeed.days | 400 | how far back the synthetic history runs |
synthSeed.refreshSchedule | "0 3 * * 1" | weekly; "" disables the CronJob |
Real-source ingest
| Parameter | Default | What it is |
|---|---|---|
ingest.backfillDays | 90 | how far back the first run of each live source reaches; under ingest.engine.mode, GitHub issues reach back at least the 365 days the source manifest declares on a source the engine has not ingested before (where the legacy lane has already run, its recorded horizon wins and ingest.github.rewalkFrom recovers the year), so lowering this moves the pull-request walk alone while setting it to 365 or more deepens both |
ingest.checkSchedule | "*/30 * * * *" | prism-ingest check probes each live source's upstream and records the verdict for the self-test page. It reads nothing and writes no data, so it is cheap (a handful of API calls) — but it does show up in the upstream's audit log, so keep the period conservative on rate-limit-sensitive tenants. Every install/upgrade also runs it once immediately via a hook. |
ingest.runDeadlineSeconds | 86400 | Wall-clock ceiling on one ingest run, in seconds — both the go-live "ingest now" Job and every firing of the incremental CronJob. The CronJob is concurrencyPolicy: Forbid, so without a deadline one wedged run (a hung upstream connection, a socket that never times out) blocks every later firing for as long as the pod lives: the index stops at one date and the Job history stays green. 24h is sized for a full backfill rather than for an incremental run — a CronJob firing that finds no watermark does the whole backfill — so it kills nothing legitimate. Touch it only if a real backfill of yours runs longer than a day (very large orgs, raised backfillDays). SHORTENING IT HAS A FLOOR, and it is not obvious from the number. The GitHub backfill records its progress per completed day bucket, so a run killed before it finishes one records nothing at all — and the next run reads "nothing recorded" as a first run, truncates the table and starts over. Shortened far enough, every firing does that and the index never fills, while each attempt looks like an ordinary interrupted backfill. The floor moves with maxRequestsPerMinute below: a bucket costs at least 3 requests per org and up to 30 on a busy day, so at 1/min that is half a minute at best and half an hour at worst. The GitHub ingest refuses to start where even an empty bucket cannot fit, and warns in its log where a busy one cannot. The Jira lane has the same floor in its own unit: it records progress per page of search results, so its cheapest recordable unit is 3 requests for the whole run (deployment detection, the field registry, one page) and it refuses to start below that in the same way. If the aim is to resume sooner after a kill, tighten the source's schedule instead — that shortens the idle gap without shortening the window the work has to finish in. |
ingest.maxStalenessHours | 0 | INGEST_MAX_STALENESS_HOURS, GitHub ingest only: warn in the job's log when the newest row stamp is older than this, and fail an empty table even on the source's FIRST run. 0 = neither, and 0 is a decision rather than an omission: the stamp only moves for rows a run touches, so on a quiet organisation "nothing changed upstream" and "we failed to fetch what changed" read identically and any number here cries wolf over a bank holiday. Both failures it used to be the only switch for are now caught without it — an empty pull_requests table fails the run by itself once GitHub has ingested here before, and an ingest of either source that stops completing is a failing SOURCES line on the self test (stallAfterIntervals below). |
ingest.stallAfterIntervals | 4 | How many of a source's own scheduled intervals may pass with no completed ingest before the self test calls that source failed rather than merely old. The interval is read from the schedules below, so this is "4 missed firings" at any schedule — 24h on the 6-hourly default. Below about 2 a single evicted pod reads as a stall; 0 turns the verdict off and leaves the line reporting an age for somebody to judge. |
ingest.engine.mode | "authoritative" | off | shadow | authoritative — whether the manifest-driven ingest engine runs beside (shadow) or instead of (authoritative) the legacy lanes. DEFAULT SINCE 0.10.0, and the default is what makes GitLab, an Oracle snapshot and an uploaded roster work at all: those three are engine-native, have no legacy lane, and shadow would walk them into <table>__shadow where nothing reads them (#956). It also promotes GitHub and Jira off their legacy lanes on upgrade — and the takeover CONTINUES from the legacy lane's recorded progress, keeping every row, because both lanes record that progress under the same source name; it truncates only where the source has no WATERMARK — no run of it has ever completed here (a table still holding synthetic seed rows, or a source that has never run, or a first backfill that died before finishing and whose recorded ground does not cover what this run wants). The GitHub COMMIT-AUTHOR identity harvest (#614) does not run in this mode; the profile sweep (#1015) — each org member's PUBLISHED profile address, read on the ingest tick — does, in shadow and authoritative alike, and ingest.github.identityEmailDomains narrows it — but not under "off", which renders no ingest tick and therefore no sweep. Set "off" to keep the pre-0.10.0 behaviour exactly, or a per-source ingest.github.engineMode / ingest.jira.engineMode to hold one lane back |
ingest.engine.tickMinutes | 15 | minutes between firings of the ingest-tick CronJob when ingest.engine.mode is not off (1–59) |
ingest.engine.parityRuns | 3 | consecutive passing shadow runs (ok, or differing only where the engine is ahead) per source before the self test reports the authoritative flip as earned; a run counts only where the comparison window has moved since the last counted one, prism-ingest check parity never advances it, and a manifest or comparator change restarts it (#731) |
ingest.engine.shadowRequestsPerMinute | 0 | the shadow engine's own request ceiling per source, requests per minute; 0 = half the source's (ingest.github.maxRequestsPerMinute) |
ingest.engine.httpTimeoutSeconds | 120 | seconds the engine waits for one HTTP response before retrying (INGEST_HTTP_TIMEOUT_SECONDS); raise it for an upstream that answers slowly; connect keeps its own 30s |
ingest.engine.databaseFetchTimeoutSeconds | 3600 | seconds one database fetch may go unanswered when the manifest sets no statement_timeout (INGEST_DB_FETCH_TIMEOUT_SECONDS) |
ingest.engine.sourceDeadlineSeconds | 7200 | seconds one source may run in a tick before it is stopped and the next source starts (INGEST_SOURCE_DEADLINE_SECONDS) |
ingest.github.schedule | "0 */6 * * *" | how often the incremental GitHub ingest runs |
ingest.github.apiUrl | "https://api.github.com" | GHE Server: https://<host>/api/v3 |
ingest.github.orgs | REQUIRED when sources.github=live (comma list); names ORGANISATIONS for the mirror's walk — the per-repository bound on the live drill-down lane is mcps.githubLive.allowedRepos | |
ingest.github.app.appId | the App's own id (App settings → "App ID"); not secret | |
ingest.github.app.installationId | WHICH installation — the number at the end of the App's "Configure" URL on your org; not secret | |
ingest.github.maxRequestsPerMinute | 30 | Hard ceiling on how fast one run may call GitHub, in requests per minute, across BOTH halves of the walk (the REST search and the GraphQL enrichment share a rate limit upstream, so they share this one). The ingest is strictly serial, so this is a true ceiling rather than an average, and it holds over retries — a run being throttled upstream cannot climb over it by backing off and rushing. The default is set for an upstream we know nothing about, which is the normal case for GitHub Enterprise Server: one request every two seconds is a rate no instance notices, and it sits at github.com's tightest published budget (30/min on search, the endpoint the walk uses) at about a third of its core allowance. Lower it if your platform team wants a smaller fraction of a limit they cannot vouch for, and raise it once they are comfortable watching it — the cost is one-directional and dull: a run takes proportionally longer, and the "worst case behind" in limits.md moves out with it. 0 disables the cap; it has to be typed, because an empty value is read as "use the default" rather than as "no ceiling". |
ingest.github.botLogins | comma list of GitHub logins this organisation runs as CI/automation and which carry no [bot] suffix (PAT-driven service accounts); EMPTY (the default) excludes by account type only, and today it reaches REVIEW figures only — pull-request and issue author counts still include them (#1247) | |
ingest.github.identityEmailDomains | comma list of email domains the identity harvest may record commit author addresses from, on BOTH GitHub and GitLab; EMPTY (the default) harvests none; GitHub's harvest additionally does not run while GitHub is under ingest.engine.mode/ingest.github.engineMode authoritative — see docs/install/engine-flip.md | |
ingest.github.profileEmailSweep | true | look up organisation members' public profile email to join a GitHub login to a person; set false to record none |
ingest.github.profileSweepBudget | 200 | how many GitHub requests one tick's sweep may make (member-list pages and profile lookups together); raise it to finish a first pass sooner, at the cost of the tick's time |
ingest.github.profileSweepTtlDays | 7 | how long a looked-up member stays looked-up before being re-checked for a newly published address |
ingest.github.rewalkFrom | Re-sweep this source ONCE from an earlier date than its watermark (YYYY-MM-DD), then leave the value where it is. Empty — the default — means every run walks forward from the last one, which is the normal case. This is the answer to the one thing ingest.backfillDays cannot do: that value is read only on a source's FIRST run, so raising it after the first backfill finished is a silent no-op — the upgrade renders, a Job runs, and the mirror still holds the window you started with. Setting a date here widens it for real, by walking the ground between that date and the watermark. Nothing is truncated, no coverage already earned is given up, and a day the walk could not fetch in full last time has its cap cleared if this walk gets it whole. It happens ONCE PER DATE: the run that carries it out records the date, so a value left in this file does not re-walk the same months on every firing of the schedule — to re-sweep again, for a wider window or after one that was killed, name a different date. Setting it starts an ingest immediately and a long re-sweep costs what the original backfill cost, so give it the same room. UNDER ingest.engine.mode IT RE-SWEEPS BOTH LANES (#801): the engine's tick reads it too, and each lane records the date separately, so neither can re-walk on the other's claim. That is what makes it the repair for a shadow parity verdict stuck on unenriched_days or capped_days — a day the engine could not enrich, or could not fetch whole, is one no ordinary run ever revisits, so without a re-sweep the verdict reports it for as long as ingest.backfillDays reaches it and the flip streak never leaves 0. The verdict names the dates and points here. So budget TWO passes, not one, and the second is the slow one: the engine's lane runs at half this source's rate unless ingest.engine.shadowRequestsPerMinute overrides it. The "give it the same room as the original backfill" above is the floor under off, not the figure under shadow. AND CLEAR IT BEFORE ARMING THE ENGINE. A date left in this file after a repair is inert for the lane that carried it out and UNCLAIMED by the engine's, which has never seen this value before — so the upgrade that first sets ingest.engine.mode to shadow starts a full re-sweep from that date at the next tick, with nobody having asked for one. Blank it once the lane you repaired has finished. See docs/install/go-live.md. | |
ingest.github.engineMode | engine mode for the GitHub source alone: off | shadow | authoritative, spelt exactly; empty (the default) inherits ingest.engine.mode, and a value other than off here is refused while that is off (the install-wide mode is the master switch) | |
ingest.spend.schedule | "40 2 * * *" | how often Router spend is swept in (sources.spend=indexed) |
ingest.spend.backfillDays | 90 | how far back the FIRST Router spend sweep reaches |
ingest.jira.schedule | "15 */6 * * *" | how often the incremental Jira ingest runs |
ingest.jira.url | REQUIRED when sources.jira=live | |
ingest.jira.deployment | cloud | Which edition of Jira url is: cloud (an Atlassian-hosted site — email plus an API token) or data_center (a self-managed Jira Data Center or Server — a bearer personal access token as jiraToken, and no email). Declared by you, never detected: every ingest run and the self test check it against the instance and stop, naming this value, when they disagree. Data Center installs upgrading from 0.11 or earlier MUST set this — see the 0.12.0 release note and docs/install/jira-ingest.md. The render refuses any other word, and refuses data_center with email set: that pair would make the two Jira readers sign in differently. |
ingest.jira.email | Cloud: the account jiraToken belongs to — REQUIRED when deployment is cloud, and refused when it is data_center (the token alone signs in; an email here would make the chart's own Jira ingest sign in basic). | |
ingest.jira.projects | optional comma list; empty = all visible | |
ingest.jira.storyPointsField | Optional comma list of customfield_NNNNN. REPLACES the fields discovery found rather than being added in front of them (#602): the ids named here are the only ones that can supply story points, in the order written. Empty (the default) leaves discovery alone, which is the normal case. Set it where a matching NAME is not the right field — a "Total Story Points" rollup or an "Original story points" estimate both match and both are wrong in the same silent direction. See docs/install/story-points.md. | |
ingest.jira.maxRequestsPerMinute | 30 | The same ceiling, for Jira, in requests per minute — see ingest.github.maxRequestsPerMinute above for what the number means and why 30 is the default. Applies to every attempt including retries, and is shared by both Jira lanes where an install runs the second one. |
ingest.jira.absoluteWindowBound | false | An escape hatch, not a setting. The ingest asks Jira for its window as an AGE (updated >= -370m) so it never has to know anyone's timezone, and checks once per run that your instance honours it. Turn this on only if that check fails: it sends a dated bound instead, which Jira reads in the timezone on the profile of the ACCOUNT the query is made from (falling back to your site default) rather than in UTC — so on an ingest credential whose profile is west of UTC that silently skips changes for good (east of UTC it only re-fetches rows it already had). See docs/install/jira-ingest.md. |
ingest.jira.rewalkFrom | Re-sweep Jira ONCE from an earlier date than its watermark (YYYY-MM-DD) — the same lever as ingest.github.rewalkFrom above, per source rather than shared, so widening one source's window during a staged go-live cannot start a re-sweep of the other. See there for what it does, why it is once per date, and why under ingest.engine.mode it re-sweeps both lanes; and docs/install/go-live.md for the procedure. | |
ingest.jira.engineMode | engine mode for the Jira source alone: off | shadow | authoritative, spelt exactly; empty (the default) inherits ingest.engine.mode, and a value other than off here is refused while that is off (the install-wide mode is the master switch) | |
ingest.gitlab.rewalkFrom | Re-sweep GitLab ONCE from an earlier date than its watermark (YYYY-MM-DD) — the same lever as ingest.github.rewalkFrom and ingest.jira.rewalkFrom above, per source rather than shared. See ingest.github.rewalkFrom for what it does and why it is once per date, and docs/install/go-live.md for the procedure. It reaches the ingest tick alone, as INGEST_REWALK_FROM_GITLAB; there is no legacy Job for it to start, so the re-sweep runs when the tick next finds the source due. IT IS ALSO HOW THE IDENTITY HARVEST REACHES HISTORY. Turning on ingest.github.identityEmailDomains records addresses on merge requests walked from then on, and GitLab has no equivalent of the GitHub lane's re-walk of unharvested days — so on an install that already holds a GitLab mirror, the two settings belong in the same change (#808). |
Agent runner
| Parameter | Default | What it is |
|---|---|---|
agentRunner.promptCaching | true | Prompt caching on the inference request. Leave it on: it is a large, stable discount on every turn. Set false only if the Agent Router in front of this install mistranslates the caching hints — a Bedrock-backed gateway that folds a tool's cache point into the same toolConfig.tools[] element makes Bedrock answer 400 "can only set one of the following keys" to every turn, and the chat fails with no answer while every other check on /selftest stays green. |
agentRunner.anthropicApi | completions | Which Agent Router API the runner speaks for Anthropic models: "completions" (the OpenAI-compatible endpoint) or "messages" (the Router's native /v1/messages). This decides whether promptCaching above actually takes effect, because caching works on OPPOSITE paths depending on what the Router in front of this install fronts — and it fails silently, with no error and no failing check, just a bigger bill. Measured: a Bedrock-backed Router caches on "completions" and not on "messages"; a Router fronting the Anthropic API directly does the exact reverse. Leave it on "completions" — that is right for Bedrock and for Vertex/AWS Anthropic, which is nearly every install. Set "messages" only if the Router fronts the Anthropic API directly, where it is worth about 9x on the cached part of every turn. |
agentRunner.tracePayloadMax | 100000 | How much of each tool argument and each tool RESULT is kept in the trace — the record the answer's provenance footer is checked against. It is NOT a limit on what the agent reads: the model's copy of a tool result arrives whole regardless, so raising this buys a more complete record and never a better answer, and lowering it hides evidence rather than saving the model any work. 4000 was the runner's own default and it is far below one page of real data — a 30-row GitHub page is about 13,000 characters. Below the size of a normal result, the footer has to report that Prism kept only a prefix of what it is citing, and the answer carries "part of this answer could not be checked automatically" above the figures. That banner then appears on essentially every substantial answer and stops distinguishing the answers that deserve it. 100000 is what Tetrate's own deployments have run since D20 and it clears a page of rows with room to spare. Raise it further only if your own results are bigger; the cost is the size of the run_events rows, which is the trace's storage and nothing else. Lower it only to shrink that storage, knowing the footer will start reporting clips. ONE number for both processes, deliberately: the runner applies the cap and the App compares against it to tell "the runner cut this result" from "this result was simply long". Setting them apart makes the footer either cry wolf or miss real clips, so the chart renders this single value to both. |
agentRunner.clientsYaml | The App client the runner trusts (seeded on boot) is rendered from values — see agent-runner-clients-configmap.yaml. clientId/issuer match what the App mints (client_id=prism-app); customerId tracks app.customerId; and allowedProjectIds is derived from app.projectId (plus "default"), so the single project knob drives both the App's PROJECT_ID and what the runner will accept — they cannot drift into a 403. Set clientsYaml only to override the whole file verbatim (advanced: multiple clients, custom scopes). | |
agentRunner.extraEnv | [] | Extra environment for the runner container, verbatim EnvVar entries (value or valueFrom), appended after the chart's own so a repeated name overrides it. Nothing here is needed for a normal install; it exists because the runner is the container support fixes keep landing on, and a bare kubectl set env is invisible to everyone reading your values file. It survives a routine helm upgrade, but not helm uninstall and reinstall, not helm upgrade --force, and not a later chart version that renders the same variable itself — so a tuning knob a Tetrate engineer asks you to set goes here, where it can be seen and cannot be silently overridden. |
Application
| Parameter | Default | What it is |
|---|---|---|
app.enabled | true | Renders the chat UI. false installs the runner and MCP servers without it — only useful when something other than Prism drives the runner. |
app.customerId | internal | Router customer/tenant — must match clientsYaml + the Router, and the tenant your inference key belongs to; a mismatch is a 403 at the gateway |
app.projectId | default | Router project the app runs under; also drives what the runner accepts — must match your inference key and router.proxyUrl, and may not be empty |
app.agentSlug | prism | which agent on the runner the app drives |
app.agentModel | Pin the Prism agent to one model, overriding router.defaultModel for this app only. "" = follow router.defaultModel, which is what you usually want; set this only to run Prism on a different model from everything else on the same runner. Changing it is picked up on the next chat turn. | |
app.rosterUploadMaxMiB | 12 | The ceiling on an uploaded roster (Admin > Context streams, the roster upload), in MiB of decoded file. It is sized AGAINST resources.limits.memory above: reading an upload costs about ten times its size in memory while the file is parsed, so 12 MiB is ~120 MiB on top of a running app inside 512Mi, and 32 MiB would be OOM-killed. Raise the two together, in that order, and only for a file that really is a staff list — a full HR export that exceeds this has outgrown a hand-cut file and wants a connected directory (docs/install/roster-upload.md). The refusal names this key. |
app.datapondPoolSize | 10 | Connections the app keeps open to the datapond (#851). Every route that reads or writes Prism's own tables — the chat archive, feedback, sign-in, the admin pages, the self test — takes one for the length of its query, so this is how many of those can be in flight at once. Over it, a request waits up to ten seconds and is then refused rather than hanging for ever as it did before this release. RAISE IT only when Prism says it is BUSY — "too many requests are using the database at once". That message is only used when every connection really was in use, so it is the one case this number fixes. If Prism says instead that it cannot REACH its database, the pool was not full and raising this changes nothing: look at the database and the network between it and the cluster. The app log names which of the two it was on every refusal. The cost of raising it is connections on your database server, so it is bounded by what that server allows: Prism's other components take roughly 30 between them, and the bundled Postgres allows 100. On a managed instance check its own limit first — a small tier can allow as few as 35 in total, which this and the other components must fit inside together. Over the server's limit the failure is immediate and says "too many clients", not a hang. docs/install/limits.md has the arithmetic and worked numbers. |
app.limits.turnsPerHour | 60 | chat turns one person may start per hour before /ask refuses with a 429 — the only ceiling on your Router bill; 0 or a non-number falls back to 60 rather than meaning "unlimited" |
app.identity.mode | static | where the signed-in user comes from: static (the fixed identity below) | easyauth (any proxy asserting X-MS-CLIENT-PRINCIPAL-*, incl. Entra ID) | iap | local. An unrecognised value fails the render rather than silently meaning static |
app.identity.userSub | operator | stable id for the static identity — mode=static only |
app.identity.userEmail | [email protected] | email shown in the UI — mode=static only |
app.identity.userName | Prism Operator | display name shown in the UI — mode=static only |
app.auth.superAdmins | [] | emails that also hold super_admin, which /admin and /selftest require under easyauth/iap/local; empty means nobody can open them, and under local mode the chart refuses to render. e.g. ["[email protected]"] |
app.auth.sessionDays | 7 | a session's absolute lifetime; sign-out and user removal end it sooner. No idle timeout. |
app.auth.magicLinkMinutes | 15 | how long an emailed sign-in link stays redeemable (each link also works exactly once) |
app.auth.baseUrl | REQUIRED when mode=local: the EXTERNAL URL users browse to (e.g. "https://prism.example.com") — the emailed link points at it, and the pod cannot infer it from behind the ingress | |
app.ingress.enabled | false | render an Ingress; leave off and reach the app by port-forward |
app.ingress.className | ingressClassName; cluster default when empty | |
app.ingress.host | REQUIRED when ingress.enabled | |
app.ingress.annotations | {} | passed through verbatim to the Ingress |
app.ingress.tls | [] | passed through verbatim to Ingress.spec.tls |
MCP servers
| Parameter | Default | What it is |
|---|---|---|
mcps.management.valetServiceUrl | Agent Router MANAGEMENT plane (spend reads) — REQUIRED once sources.spend is indexed/live; not the same address as router.proxyUrl | |
mcps.githubLive.enabled | false | turn on once secrets.githubToken is set |
mcps.githubLive.toolsets | "pull_requests" | upstream child's toolset; facade curates exposed tools |
mcps.githubLive.host | "" = github.com; https://<host> for GHE Server | |
mcps.githubLive.allowedRepos | comma-separated owner/repo the live GitHub drill-down may answer about; EMPTY = every repository the credential can read, stated as such in every answer | |
mcps.semanticMcp.enabled | true | attach the semantic query MCP (#515) to the agent: renders its Deployment and adds the profile — ON by default since 0.10.0 (#974), because it is the only reader GitLab, an Oracle snapshot and an uploaded roster have; false renders nothing at all and is a one-upgrade rollback |
mcps.semanticMcp.statementTimeoutSeconds | 15 | Seconds the semantic MCP allows ONE statement before it gives up (SEMANTIC_STATEMENT_TIMEOUT_MS, which this renders in milliseconds). This is a CEILING and not a default: a source manifest's own connection.database.statement_timeout may ask for LESS time, never more, because the manifest is the source author's opinion about its own queries and this is the operator's budget for a turn. So a query-through manifest asking for PT10M gets whatever this says. RAISE IT for a warehouse or a view that is genuinely slow to answer — a customer view reached through a synonym over a stack of dependent views measured 21 seconds for a single row, which no manifest value could have rescued (#1138). Read the source's pre-flight timings before choosing a number, rather than doubling it until questions stop failing. THE COST OF RAISING IT is paid per statement, and a question can spend it more than once: a population-scoped question resolves the population under this ceiling and then runs the question under it again, so the worst case is a multiple of this value and not this value. Nothing bounds a turn as a whole yet (#1130). |
mcps.semanticMcp.readsPerMinute | 60 | How many questions a minute may reach ONE context stream that is answered in your own system (#1130). Applies to a query-through stream — an Oracle or Snowflake view Prism reads live — and to nothing Prism holds itself, because the only system this protects is yours. There was no limit at all before this release: a reader's questions reached your warehouse as often as the agent asked, and a reader who thought a slow page had hung and asked again doubled that. 60 a minute is one a second — invisible to a person asking questions, and a hard ceiling on a loop, which is the thing worth stopping. LOWER IT if your database administrators have agreed a rate with you, or if the view is expensive; a stream's own manifest may lower it further for that stream alone, and a registered instance may lower it again. 0 means no limit, which is the right setting when the system being read is one you own and size yourself. Waiting for a slot counts against the 30-second budget one question has, so a stream throttled hard enough will refuse questions rather than queue them for ever — and the refusal says which step ran out of time. |
mcps.discoveryMcp.enabled | true | EXPERIMENTAL: render the Stream setup session's tools (#431); set false to switch it off |