Environment Variable Configuration
PlantPulse AI configuration starts from a single file: /etc/kopens/plantpulse-ai.env.
sudo vi /etc/kopens/plantpulse-ai.env
/etc/kopens/The path is /etc/kopens/plantpulse-ai.env. It is outside the repo tree, not inside the installation directory
(/opt/kopens/plantpulse-ai-docker), and its permissions are 0600.
All three products — platform · ai · studio — use /etc/kopens/plantpulse-<product>.env under the same convention.
🔴 Do not edit compose/.env. Nobody reads it — the stack will not change.
For *_PASSWORD · *_API_KEY in the table below, editing the file alone does not complete the change. For values whose
authoritative copy is the server account — Neo4j, for example — you must change the server side as well, and getting the order wrong
leaves authentication broken. Use the dedicated tool — Changing Passwords and API Keys.
Values live in three places (2026.10)
From 2026.10, the host env holds only values unique to this installation. Everything else is version-controlled with the release.
| Where | What | How to change it |
|---|---|---|
Host env /etc/kopens/plantpulse-ai.env | Hardware shape (PP_HW_PROFILE) · site values (language · time zone · certificate name) · platform connection · secrets | Edit this file and re-create the services |
Hardware profile compose/profiles/<모양>.env | Values that differ per hardware — the GPU share of each of the three vLLM instances and the cap on their sum | Set by the release |
| compose defaults | Product defaults — images · model names and paths · service tuning | Set by the release. Writing a different value in the host env overrides it for that installation only |
PP_HW_PROFILE is either gb10 (GB10 · aarch64) or g7e (AWS g7e · x86_64). The template already fills it in; if it is missing or
conflicts with the machine's architecture, the stack will not start.
In the tables below, any variable not present in the host env template (those with a default listed) is a compose default. Using it as-is is normal; write that line into the host env with a different value only when you want to change it.
After changing a value you must re-create the services (docker restart does not re-read env).
cd /opt/kopens/plantpulse-ai-docker
bin/restart.sh
Before bringing the stack up, the startup script uses bin/lint-env.sh to judge whether every line of this file belongs there, and
refuses to start if any of the following is present.
| Verdict | What |
|---|---|
release | Image references — DOCKER_REGISTRY · DOCKER_NAMESPACE · UPSTREAM_* · *_IMAGE · PI_IMAGE_TAG_* · a non-empty PP_IMAGE_TAG |
profile | Hardware profile keys (LLM_GPU_UTIL and the like) |
redundant | A line that repeats a compose default verbatim |
derived | PP_HOST — computed from the host of PP_DATALAKE_ADMIN_URL |
unknown | A key nothing reads |
renamed | An old key that has been renamed (CH_* → CO_*) — it says «move it», not «delete it» (see "Copilot Settings" below) |
Beyond that, a ${…} substitution left in a value position, a missing PP_HW_PROFILE, or exceeding the GPU share also halt startup. A value that
differs from the compose default is treated as an intentional override and only raises a notice ([note]). The check prints a reason per line, along with
a single sed command (in a form that leaves a backup) that deletes all the deletable lines at once. renamed lines are not put on the deletion list; a separate sed that only renames them
is printed instead. Values are never printed.
Lines that change nothing if left alone are reported as [warn] only and do not block startup — aliases still read under the old name during this release
(alias), old lines left with the same value as the new name (oldname), and lines identical to what the video · audio switch supplies (media-same).
The check also prints a single sed line that cleans all of these up at once, and bin/doctor.sh shows them not as «bad» but separately as
«old or redundant lines». If the video · audio switch value is outside true · false, or PI_INBOUND_API_KEY is
empty, startup halts (see "Video · Audio Ingestion" and "Backend Inbound Key" below).
Images and Registry
🔴 From 2026.10, image references are not written in the host env. Which image and which tag come up is decided by the release's
compose, and if any of the variables below are in the host env the pre-start check halts startup with release. Pins left on the host
have masked the next release, and have leaked into bundle builds.
| Variable | Meaning |
|---|---|
DOCKER_REGISTRY · DOCKER_NAMESPACE | Where first-party images are pulled from (docker.kopens.io / pi) |
UPSTREAM_DOCKERHUB · UPSTREAM_NVCR | In-house proxy that pulls third-party images on your behalf |
PP_IMAGE_TAG | Default tag for the set. Present in the template as an empty line only |
PI_IMAGE_TAG_<component> | For pinning a single component to a different snapshot (hotfix · rollback). There are thirteen, and the suffix is the image name in uppercase (plantpulse-timeseries-insight-api → PI_IMAGE_TAG_TIMESERIES_INSIGHT_API) |
If an emergency requires bringing up a single component on a different tag, supply it through the shell environment for that run only, not through the file. The next ordinary startup returns to the release tag.
cd /opt/kopens/plantpulse-ai-docker
PI_IMAGE_TAG_RAG_API=<태그> bin/up.sh
In 2026.10 the first-party images became twelve — digital thread (plantpulse-digital-thread-api) was
added, and LightRAG and LiteLLM no longer use upstream images directly but images we wrap
(pi/plantpulse-lightrag · pi/plantpulse-litellm). The two wrapped images follow the monthly tag
(2026.10) like any other first-party image, and to pin one of them you use PI_IMAGE_TAG_LIGHTRAG · PI_IMAGE_TAG_LITELLM.
The wrapped bases are LightRAG v1.5.7 · LiteLLM v1.103.0.
In 2026.10 re-release 3 one more was added, making thirteen — the audio library image used by transcription serving,
pi/plantpulse-asr-audio (about 130 MB; pin with PI_IMAGE_TAG_ASR_AUDIO). It only comes up on installations with video · audio enabled, but
it is always included in the bundle (see "Video · Audio Ingestion" below).
PP_IMAGE_TAG emptyLeft empty, the calver literal baked into compose is used. That literal is moved by bin/bump-version.sh every release,
so the release month has exactly one home. If you write a value, the pre-start check halts startup.
YYYY.MM is a «moving» tagRe-releasing within the same month overwrites it. To pin a point in time, supply a dated tag
(YYYY.MM-YYYYMMDD) via the shell-environment method above, but check that it exists per image and pin only those that have it —
not every image has one, so swapping them all makes the missing ones fail to pull.
The admin console has two images — the variables for pinning are
PI_IMAGE_TAG_ADMIN_API and PI_IMAGE_TAG_ADMIN_WEB (both fall back to
PP_IMAGE_TAG when empty). They come from the same commit, so always pin the two together even when pinning individually.
If they diverge, the console is paired with a backend that lacks the APIs it calls.
Language and Time Zone
| Variable | Default | Meaning |
|---|---|---|
PP_LANG | ko | The installation's default language — the language used where no request specifies one, such as emails · scheduled runs · saved posts. Automation text written by the server — summaries · run reasons · PDF section names · Excel sheet names — is also in this language → Automation — Report titles. In the copilot UI, the language you picked yourself → the browser language take precedence, and this value applies only when neither is Korean · English → UI language. The admin console follows this value only for the home card names; every other screen follows the language toggle on the login screen (English · 한국어) · Only ko and en are valid. If it is empty or any other value, the services refuse to start (installation template default ko) |
PP_TZ | Asia/Seoul | Time zone for the installation — all services and the host OS use this single value. The installer picks it (value given at install → value already in env → host time zone (if not UTC) → Asia/Seoul, in that order) |
LIGHTRAG_SUMMARY_LANGUAGE | Korean | Language in which RAG summarizes knowledge |
The reason the summary language is kept separate from the UI language is that the UI may be in English while the corpus (field SOPs) is frequently in Korean.
What Is Borrowed from the Platform
🔴 The AI stack does not bring up PostgreSQL · Aster TSDB · Redis · RustFS. It shares the platform installation. If these values are wrong, AI will come up but store nothing.
Platform credentials are fetched by containers «at startup»
From 2026.10, compose does not pass the platform storage accounts · passwords · ports (PG · Aster TSDB · Redis · RustFS) to
containers. The six services that use them (copilot-web · timeseries-insight-api · lightrag ·
rag-api · digital-thread-api · litellm) fetch them from the platform credential API at startup, map them onto their own
names, and then launch the app. The only things written in the host env are the address and key used to call that API.
| Variable | Meaning |
|---|---|
PP_DATALAKE_ADMIN_URL | Platform data lake management API — https://<플랫폼 호스트>:4950. The host in this URL is the platform host |
PP_DATALAKE_ADMIN_API_KEY | Key for that API. Copy the identically named line verbatim from /etc/kopens/plantpulse-platform.env on the platform host |
The template contains both lines with placeholder values, and interactive installation (INTERACTIVE=1) prompts for both. The prerequisites for fetching are in
Installation Overview (platform allowlist · trust anchor root.crt).
- If only one of the two values is written, the container refuses.
- A failed fetch stops with FATAL (connection failures · 5xx are retried briefly, 4xx immediately). There is no cache that lets it come up on the previous values.
- Server certificates are verified with
/etc/kopens/ca/root.crtonly. A missing file is FATAL, and there is no way to disable verification.
If you changed a password on the platform, there is no file to edit on the AI side — restart that container and it will fetch the new
value. Only the key names and their count are left in the log (docker logs <컨테이너> 2>&1 | grep credentials:).
Host scripts fetch along the same path. Pre-start DB provisioning (provision-platform-db.sh), the DB migration scripts you run by hand,
and the preflight check of install.sh START=1 all call the same API under the same rules (verified with root.crt).
That is why there are no platform accounts or passwords in the host env either. On failure they stop with a single [FATAL] [platform-credentials] … line giving
the reason (unreachable · 401/403 · key missing · URL/key/CA missing). Only when testing somewhere the platform is unreachable can you supply values via
the shell environment for that run, and those win.
The only platform values remaining in the host env are the following.
| Variable | Meaning |
|---|---|
PP_DATALAKE_ADMIN_URL · PP_DATALAKE_ADMIN_API_KEY | Above — where credentials are fetched from |
PP_API_URL · PP_API_TOKEN | Platform V5 API — not in the credential store, so it remains in the host env (see "Platform Integration MCP" below) |
PP_PG_PORT · PP_CASSANDRA_PORT | Present only as comments in the template. Read by host scripts only; leave empty if the platform uses its default ports |
The platform host (PP_HOST) is not written — the host of PP_DATALAKE_ADMIN_URL is it, and containers and
host scripts compute it by the same rule. The PG database names AI uses (PP_LIGHTRAG_PG_DB default
lightrag, etc.) are compose defaults.
The AI applications' own PG data lives in DB pi of the platform PG, split into a schema per app (copilot co ·
digital thread dt, with account PP_PG_USER). DB ch from older installations is migrated to pi.co at startup.
The installer and update.sh do not re-create an existing env. An env from 2026.09 or earlier still holds many
lines that no longer belong — image references, GPU shares, values identical to compose defaults, PP_HOST, platform accounts and passwords
(PP_PG_USER · PP_PG_PASSWORD · PP_CASSANDRA_* · PP_REDIS_PASSWORD · PP_S3_*, etc.) — so
the pre-start check will halt startup. Clean it up before upgrading.
cd /opt/kopens/plantpulse-ai-docker
sudo bash bin/lint-env.sh # 무엇이 남았나 — 바꾸지 않습니다
sudo cp -p /etc/kopens/plantpulse-ai.env ~/plantpulse-ai.env.bak-$(date +%Y%m%d-%H%M)
# lint-env 가 찍은 sed 한 줄을 그대로 실행한 뒤, 하드웨어 모양을 더합니다(GB10 = gb10 · g7e = g7e)
grep -q '^PP_HW_PROFILE=' /etc/kopens/plantpulse-ai.env || echo 'PP_HW_PROFILE=gb10' | sudo tee -a /etc/kopens/plantpulse-ai.env >/dev/null
sudo bash bin/lint-env.sh && bin/restart.sh
- Add before deleting — add
PP_DATALAKE_ADMIN_URL·PP_DATALAKE_ADMIN_API_KEYand place the trust anchor. The check only says «delete» about the old platform account lines, so add these two first. - Old
CH_*lines are flagged asrenamedand a separatesedthat only renames them is printed — running it moves the values as-is toCO_*(see "Copilot Settings" below). - Values deliberately set differently from the defaults (model paths, for example) remain as
overridenotices and are not deleted.
The copilot subscribes to platform equipment alarms over MQTT (2026.09; previously Kafka). The broker address follows
the host of PP_API_URL and the port defaults to 1884 (TLS), so there is nothing separate to write.
The former PP_KAFKA_PORT is gone — even if it remains, nothing reads it.
Platform Integration MCP
This is the channel through which copilot chat pulls the platform's real-time data.
| Variable | Meaning |
|---|---|
PP_API_URL | Platform server-web base URL |
PP_API_TOKEN | API token issued by the platform |
Compose requires both values — the stack will not come up if they are empty. If the placeholder remains, bin/up.sh will not bring it up.
But with a wrong token the stack comes up normally and only the platform MCP tools in chat silently become zero. If the symptom is "I can't see the tools," look here first.
Models and Gateway
All model and gateway values are compose defaults (only the installer-generated secrets live in the host env); vLLM is
v0.31.0 and docling-serve is v1.36.0. The images (VLLM_IMAGE · DOCLING_IMAGE) follow the rules in "Images and Registry"
above and are therefore not written in the host env. Model locations are LLM_MODEL_PATH · EMB_MODEL_PATH · RERANK_MODEL_PATH
(by default the Aura-* directory under /opt/kopens/models).
| Variable | Default | Meaning |
|---|---|---|
LLM_NAME | aura-lite | The name used to call the generation model. Doubles as the vision model |
EMB_NAME | aura-embedding-4b | Embedding model name |
RERANK_NAME | aura-reranker-0.6b | Reranking model name |
LLM_MODEL_PATH | /opt/kopens/models/Aura-Lite-NVFP4 | Weights folder for the generation model. Aura-Lite-NVFP4.tar.zst takes the last folder name |
LLM_MODEL_ARGS | --tool-call-parser gemma4 --reasoning-parser gemma4 --async-scheduling --enable-chunked-prefill | vLLM startup arguments. The parser is matched to the weights (Gemma 4 family), so change it only together with the weights |
VISION_LLM_MODEL | (empty) | Only when using a different model for vision requests. Empty means LLM_NAME is used |
PI_VLLM_API_KEY | Installer-generated | Key in front of vLLM |
PI_LLM_API_KEY | Installer-generated | LiteLLM master key |
PI_LITELLM_SALT_KEY | Installer-generated | Salt LiteLLM uses to encrypt stored values |
PI_LITELLM_UI_USERNAME · PI_LITELLM_UI_PASSWORD | admin · admin123! | Login for the LiteLLM admin UI (:4000/ui). The password is required → Changing the Initial Password |
PI_LITELLM_PORT | 4000 | Port on which LiteLLM is exposed on the host. Change it only if another LiteLLM on this host already uses 4000. Consumers inside the stack call plantpulse-litellm:4000 over the container network, so this value does not affect them. After changing it, re-create with bin/restart.sh plantpulse-litellm |
LITELLM_IMAGE is goneFrom 2026.10 the gateway image is a first-party image (pi/plantpulse-litellm) and follows the image tag rules above.
A LITELLM_IMAGE line left in an old env is caught by the pre-start check as an image reference (release) and halts startup — delete it.
aura-* — there are no aliases for the old namesThe names registered on the gateway are aura-lite · aura-vision (vision alias) ·
aura-embedding-4b · aura-reranker-0.6b. The LLM's serving name changed from gpt-4o → aura-35b in 2026.09 and
from aura-35b → aura-lite in 2026.10 re-release (2026-10-20), and compatibility aliases for the old names are not provided —
calling the old name returns 404, so you must change the model value in external integrations to the new name. VLM_NAME is also
gone (the vision alias is held by the gateway configuration).
The serving name (LLM_NAME) and the weights folder (LLM_MODEL_PATH, default /opt/kopens/models/Aura-Lite-NVFP4) are separate settings.
Both changed in re-release (2026-10-20) — the weights folder went from Aura-35B-A3B-NVFP4 → Aura-Lite-NVFP4 (bundled model file Aura-Lite-NVFP4.tar.zst).
Writing these three (LLM_NAME · LLM_MODEL_PATH · LLM_MODEL_ARGS) into the host env with the same values as the defaults makes the pre-start check (lint-env)
block startup with redundant — write them only with «different values», when testing another model.
PI_LITELLM_SALT_KEYThis is the key that encrypts external provider API keys entered on the Model Registration screen of the admin console. Change it and already-stored entries cannot be decrypted — the container comes up healthy and the model list looks unchanged, yet only the calls fail silently. Leave the value as first set, and include it in your backups.
GPU Memory Shares
The three vLLM instances (generation · embedding · reranking), the speech transcription vLLM (plantpulse-asr), and the time series inference server share one GPU.
Each share and the cap on their sum are all held by the hardware profile (compose/profiles/<PP_HW_PROFILE>.env) —
writing them in the host env makes the pre-start check halt startup with profile.
| Variable | gb10 / g7e | Meaning |
|---|---|---|
LLM_GPU_UTIL · EMB_GPU_UTIL · RERANK_GPU_UTIL | 0.4 · 0.10 · 0.08 / 0.53 · 0.13 · 0.10 | GPU memory shares of the three vLLM instances |
TS_GPU_UTIL | 0.13 / 0.15 | Share of the time series inference server |
ASR_GPU_UTIL | 0.03 / 0.04 | Share of the speech transcription vLLM — it only comes up on installations with video · audio enabled (PI_MEDIA_ENABLED=true), but it counts toward the sum even when disabled (so the cap is not exceeded on the day you enable it) |
PP_GPU_BUDGET_CEILING | 0.75 / 0.95 | Cap on the sum of the five shares. Exceeding it stops the stack before startup |
GB10 uses unified memory, so its cap is 0.75 (sum 0.74); machines with dedicated VRAM (AWS g7e) use 0.95 (sum 0.95).
At startup vLLM checks that «free memory ≥ its own share» and dies if short. If the sum overflows, it becomes a race in which whoever comes up first wins, and the loser restarts in a loop while chat stalls — nowhere in the logs does it say "the configuration was too large." That is why arithmetic blocks it before startup. If you must reduce, reduce the largest share first.
On GB10, GPU memory comes from the same pool as system RAM, so raising the cap leaves nothing for the OS and the DBs. This is also why the installer stops when the template and the machine's architecture disagree.
Copilot Settings — the prefix is CO_
From 2026.10, environment variables going to the copilot (copilot-web) begin with CO_ (previously CH_).
CO_LLM_* · CO_TOOL_RESULT_MAX_CHARS · CO_API_V1_KEY · CO_JAVA_OPTS · CO_NAVIGATOR_CRON ·
CO_ANOMALY_CRON · CO_INCIDENT_DIAG_CONCURRENCY · CO_OTEL_* are all affected.
CH_* lines, «move» themThere is no fallback to the old names. The installer does not re-create an existing env, so lines in an older env such as
CH_API_V1_KEY · CH_LLM_MAX_COMPLETION_TOKENS · CH_OTEL_GENAI_CONTENT remain, but the copilot does not read
those names. If you merely delete the lines, those values fall back to their defaults — for example, the public API you had opened with CH_API_V1_KEY
can no longer be reached with that key.
The pre-start check (bin/lint-env.sh) catches these lines as renamed, halts startup, and states what to do for each line.
| What the check prints | Meaning · action |
|---|---|
only the old name is set — rename it | Only the old name is present. The accompanying single sed line (which leaves a backup) renames it and leaves the value as-is |
both set with the same value | The new name already exists with the same value. Delete the old line |
both set and DIFFERENT | Both exist with different values. The new name wins — confirm which value is correct, then delete the old line |
sudo grep -n '^CH_' /etc/kopens/plantpulse-ai.env # 남은 옛 줄
After moving, re-create with bin/restart.sh plantpulse-copilot-web.
Where Automation Sends Output — Mail · Messenger · PDF
The paths that send automation results out by mail · messenger are closed by default and open only when the values below are set. PDFs are produced only when the conversion service is reachable.
| What | Where it is set | If empty |
|---|---|---|
| PDF conversion service (Gotenberg) | Environment variable CO_GOTENBERG_URL — compose default http://plantpulse-gotenberg:3000 (the conversion container that comes up with the stack) | If the value is empty or the conversion container at that address does not respond, only PDF is disabled. PDF download for reports and the PDF-attachment button for automation mail are disabled, and the screen explains why. Excel · CSV are unaffected by this value |
| Allowed mail domains | Admin console › Notification Channels › "Allowed mail domains" (config key automation.mail.allowed-domains, comma-separated · up to 20) | 🔴 Automation mail is off. It turns on only once you enter your in-house domains |
| Mail server | Admin console › System Settings › smtp.host · smtp.port · smtp.username · smtp.password · smtp.starttls, sender navigator.report.email.from | Mail does not go out ("the mail server is not configured" in the run history) |
| Messenger channels (Teams · Slack) | Register the webhook URL in Admin console › Notification Channels — not an environment variable | Messenger channels cannot be selected under «Send» in automation |
| Links in mail · messenger bodies | Admin console › System Settings › app.base.url (the external address of this installation) | No links are included in the body |
CO_GOTENBERG_URL must be a http:// · https:// address. Any other value leaves a warning and disables PDF. The PDF buttons and attachment chips on screen follow not whether an address is set but whether
the converter responds right now (/health, re-checked every 30 seconds) — on an installation with an address but no container, they appear disabled. When the converter is unreachable, mail goes out minus the PDF only
("could not attach" in the run history), and report downloads answer with "PDF cannot be generated right now." The converter's state appears as off · unreachable · up in gotenberg of /actuator/health/deps,
and a dead converter does not enter the copilot's startup check.
The daily send limits are system settings values (they change without a restart). Even if never edited, they appear with their defaults in Admin console › System Settings, where you can change them or restore the default.
The automation interval · token caps (agentdef.*) and the two-person approval tools (access.two_person.tools) are in the same list → Copilot Admin Console — System Settings
| Config key | Default | Meaning |
|---|---|---|
automation.deliver.mail.daily-max | 6 | Number of mails one automation sends to one person per day |
automation.deliver.webhook.daily-max | 12 | Number of messenger messages one automation sends to one channel per day |
automation.deliver.role.max-members | 50 | Number of people expanded when recipients are chosen by role |
What you do on screen is covered in Copilot Admin Console — Notification Channels.
Copilot Chat — Thinking Mode and Three Limits
These are the three values that determine how the conversational copilot (copilot-web) calls the gateway. All three handle the same single failure through different handles — the model «thinks» before answering, and that thinking also consumes generation tokens.
| Variable | Default | Meaning |
|---|---|---|
CO_LLM_THINKING_ENABLED | true (compose, from 2026.10 re-release (2026-10-20). The app's standalone default is empty) | Thinking mode switch → chat_template_kwargs.enable_thinking. Empty means it is not sent (the model default applies — AURA-Lite's template default is «off») |
CO_LLM_MAX_COMPLETION_TOKENS | 4096 (16384 for first-party stack deployments) | Generation token limit for a single call. It must be large enough to hold both the thinking and the answer. 0 means no limit is sent |
CO_TOOL_RESULT_MAX_CHARS | 24000 | Maximum number of characters one tool result can contribute to the model. Beyond that, the deepest arrays are trimmed first and a _truncated marker is left. 0 means no truncation |
On the Qwen3.5 family, turning it off with CO_LLM_THINKING_ENABLED=false cuts responses to a few seconds, but the model does not call tools and invents nonexistent assets and alarms (measured on staging, 2026-09-18). This is not a matter of trading speed against quality but of whether the answer is right or wrong.
Turning it off is only meaningful on models that call tools even without thinking.
Thinking Depth — Defaults per Path
From 2026.10 re-release (2026-10-20), in addition to turning thinking on and off you choose how much to think (reasoning_effort). It is not an env value but a key in the copilot's
Admin › System Settings (/admin/config), and once saved it applies from the next call without a restart.
| Key | Screen name |
|---|---|
llm.reasoning.chat | Default chat thinking depth |
llm.reasoning.automation | Automation thinking depth |
llm.reasoning.diagnosis | Anomaly diagnosis thinking depth |
llm.reasoning.proposal | Action recommendation thinking depth |
llm.reasoning.other | Thinking depth for other generation |
- Values are
none(off) ·low·medium·high, or empty («follow the server default»).noneturns thinking off; the others keep thinking on and send it at that depth. - The order of precedence is the value the user chose in the prompt → the per-path key above →
CO_LLM_THINKING_ENABLED. - The thinking depth button on the prompt screen appears only when the global value or
llm.reasoning.chatis set.
If the thinking does not finish within the limit, generation is cut off at length and the body is 0 characters. The stream terminates normally, so on screen it looks only like "no answer comes back," and reads like a gateway failure.
Here is the same question measured side by side on staging.
| Setting | Result |
|---|---|
Thinking on + 4096 | Blank answer — the thinking consumes the entire limit |
| Thinking off | Fast, but it skips the tools and makes things up |
Thinking on + 16384 | Calls tools and gives the right answer (20 seconds to 4 minutes depending on the question) |
If raising the limit still does not let it finish, then look at reducing the prompt or the tool result size (CO_TOOL_RESULT_MAX_CHARS).
Document Indexing (LightRAG) Time Budget
The defaults as shipped are correct. Touch them only when you have measured that "this machine needs more/less."
| Variable | Default | Meaning |
|---|---|---|
LIGHTRAG_LLM_TIMEOUT | 3600 | Limit (seconds) on entity-extraction LLM calls. The worker limit is twice this value |
LIGHTRAG_LLM_MAX_TOKENS | 32768 | Maximum tokens a single extraction call may produce — the runaway ceiling |
LIGHTRAG_MAX_ASYNC | 4 | Number of concurrent LLM calls. 🔴 Lowering it makes things slower |
Worker execution timeoutA thinking model spends many tokens per call. With the previous defaults (240 s, worker 480 s), large PDFs
failed outright mid-indexing. If you see this error, raise LIGHTRAG_LLM_TIMEOUT but also keep an eye on
LIGHTRAG_LLM_MAX_TOKENS — without a ceiling, a single runaway holds a GPU slot for that long.
Public API Machine Authentication
This is the key external systems use when calling the copilot's public API (/api/v1) →
API Integration Guide
| Variable | Default | Meaning |
|---|---|---|
CO_API_V1_KEY | (empty) | Key accepted in the X-API-Key: <value> header. The name up to 2026.09 was CH_API_V1_KEY — move it |
If it is empty, this environment-variable key is not accepted. In that case, the only thing that can get in via X-API-Key is a named key issued on the
API Keys screen (/admin/api-keys) of the copilot admin console, and without that, only session login is accepted.
This prevents a deployment that forgot the setting from becoming an unauthenticated public API. Either key's authority reaches the public API only;
it does not reach the admin screens (/admin/**) or the API documentation.
A single environment-variable key cannot tell you who it was given to, and if it leaks, every consumer is cut off at once. To issue separate keys per external system, use the copilot admin console → Copilot Admin Console — API Keys
Admin Console and API Keys
| Variable | Meaning |
|---|---|
PI_ADMIN_USER · PI_ADMIN_PASSWORD | Admin console login. ID defaults to admin; the password is chosen by a person at install time |
PI_ADMIN_SESSION_SECRET | Admin console session signing key — generated by the installer |
ADMIN_SESSION_MAX_AGE | Session lifetime (minutes). Default 480 |
PI_ADMIN_WEB_PORT | Admin console HTTP port. Default 5959 |
PI_ADMIN_WEB_TLS_PORT | Admin console HTTPS port. Default 5960 — the proxy terminates it and forwards to the same console (the certificate is the same set as :443) |
PI_NEO4J_USER · PI_NEO4J_PASSWORD | Graph account — both Neo4j instances (LightRAG knowledge graph plantpulse-neo4j · factory graph mirror plantpulse-digital-thread-neo4j) use the same account |
PI_RAG_API_KEY | RAG API call key |
PI_INBOUND_API_KEY | Key for requests inbound to the backend services (RAG · time series insight · document understanding · vision · digital thread). If empty, the pre-start check stops the stack (see "Backend Inbound Key" below) |
Editing the file alone does not finish the job for these values — where the server account is authoritative, as with Neo4j, the server side
must be changed too. bin/passwd.sh walks that order for you →
Changing Passwords and API Keys.
Both entrances reach the same console, and both are open (plaintext is not redirected to HTTPS). Leave them empty for the
defaults above; if you change them, the installer opens the firewall on those values. After changing them,
re-create both containers with bin/restart.sh plantpulse-admin-web plantpulse-proxy.
Stored Credential Encryption Keys — PI_COPILOT_SECRET_KEY · PI_DT_SECRET_KEY
These are the keys that seal credentials entered on screen into the DB with AES-256-GCM. The value is 32 bytes written in base64, and the installer generates it at first install.
| Variable | What it seals | When empty |
|---|---|---|
PI_COPILOT_SECRET_KEY | Credentials the copilot stores — external MCP connector authentication values, LLM keys, SMTP passwords, Slack webhooks, Web Push private keys | 🔴 Only the copilot refuses to start, and prints the one line to add into the log. The rest of the stack comes up |
PI_DT_SECRET_KEY | Connector credentials in the digital thread connector registry | Only the connector registry returns config_error (503). The graph mirror keeps running |
-
These two values must be in the
/etc/kopens/plantpulse-ai.envbackup together with it. If you restore only the DB and lack the keys, connector and notification credentials must be re-entered. -
Hosts upgraded from 2026.09 or earlier do not have these lines. The installer does not add lines to an existing env, and the pre-start check reports «the stored credential encryption key is missing». Add it yourself (in a form that does not print the value to screen).
echo "PI_COPILOT_SECRET_KEY=$(openssl rand -base64 32)" | sudo tee -a /etc/kopens/plantpulse-ai.env >/dev/nullecho "PI_DT_SECRET_KEY=$(openssl rand -base64 32)" | sudo tee -a /etc/kopens/plantpulse-ai.env >/dev/null -
bin/passwd.shdoes not rotate these two keys. Replacement is done only via the procedure that places the old value in_PREVIOUS→ Changing Passwords and API Keys
Digital Thread
From 2026.10, plantpulse-digital-thread-api is included in the stack by default (no port is opened on the host;
it is called as 7140 inside the stack). The factory graph mirror lives in a dedicated Neo4j (plantpulse-digital-thread-neo4j),
and the connector registry lives in schema dt of PG DB pi. The platform event reader ships enabled.
These are the handles you can touch on the host. The defaults as shipped are correct.
| Variable | Default | Meaning |
|---|---|---|
PI_DT_SECRET_KEY | Installer-generated | Connector credential encryption key (above) |
DT_INSIGHT_EVENTS_PUSH | false | Whether the digital thread pushes the platform events it read to time series insight |
TS_EVENTS_SOURCE | direct | Where time series insight gets platform events — direct (reads directly) · shadow (reads directly and compares against what was pushed) · digital_thread (uses only what was pushed — requires the switch above to be on) |
DT_PLATFORM_READ · CO_PLATFORM_READ · TS_PLATFORM_READ | db | How the platform ledger is read — db (platform tables directly) · shadow (reads both, answers with db, and counts the differences) · api (platform V5 REST, using the existing PP_API_URL · PP_API_TOKEN) |
Connector Periodic Sync and Freshness (2026.10 re-release 3)
Enabled connectors are re-synchronized periodically. A sync does not delete vanished connections but «closes» them (recording the end time), so history queried at a past point in time stays correct. The defaults as shipped are correct.
| Variable | Default | Meaning |
|---|---|---|
DT_CONNECTOR_SYNC_ENABLED | true | Periodic sync. false stops it, leaving only «Sync now» in the admin console |
DT_CONNECTOR_SYNC_INTERVAL_S | 0 | Sync interval (seconds). 0 means per-family — half that family's freshness limit, minimum 300 seconds |
DT_CONNECTOR_MAX_AGE | (empty) | Freshness limit for connector data. A 가족=기간 list (e.g. mes=2h,erp=1d, units s · m · h · d). Empty means per-family defaults — ehs 1h, mes · sched · wms 2h, cmms 4h, lims 12h, everything else 1d. If a connector is staler than its limit, recovery and impact figures are not produced |
DT_LINK_HISTORY_DAYS | (empty — 180) | How long closed connections are retained (days, 1–3650) |
The values currently in effect and the sync state appear in the digital thread's /api/v1/config (dt_connector_max_age_effective and the like) and in
connector_sync of /health. connector_sync is informational only and does not make the service degraded.
Backend Inbound Key — Empty Means It Will Not Start
RAG (rag-api) · the time series insight API · document understanding · vision · digital thread all verify inbound requests with
the same single key (PI_INBOUND_API_KEY). Compose distributes this value to each service under its own name (RAG_INBOUND_API_KEY · TS_INBOUND_API_KEY ·
INBOUND_API_KEY …), and the copilot and the admin console backend call these services with the same value.
If the key is empty, there is no path by which anything comes up «accidentally» open without authentication.
| Where | If the key is empty |
|---|---|
Pre-start check (bin/lint-env.sh) | Stops with [fail] PI_INBOUND_API_KEY is empty and PI_INBOUND_AUTH_DISABLED is not true without creating a single container |
| Each service | If its own key is empty it refuses to start and leaves a line of the FATAL: … is empty — refusing to start … form |
The only way to bring them up without authentication is one explicit switch, intended for development and testing.
| Variable | Default | Meaning |
|---|---|---|
PI_INBOUND_AUTH_DISABLED | false | Only when it is true and the key is empty do the five services come up without authentication. Compose passes it per service as RAG_INBOUND_AUTH_DISABLED · TS_INBOUND_AUTH_DISABLED · DT_INBOUND_AUTH_DISABLED · INBOUND_AUTH_DISABLED (document understanding · vision) |
Here is what the pre-start check says about this switch.
| Output | Meaning |
|---|---|
[fail] PI_INBOUND_AUTH_DISABLED=<value> — write true or false | A value outside true · false |
[note] PI_INBOUND_AUTH_DISABLED=true is ignored — PI_INBOUND_API_KEY is set and the key wins | If a key is present, the key wins — authentication stays on |
A service brought up without authentication carries that fact in /health (document understanding inbound_auth: "disabled", digital thread
auth.inbound=disabled, etc.). On production installations, the answer is to set the key.
The only things open without authentication are the health paths /ping · /health (exactly those paths). Metrics /metrics are also
behind the key → Logging and Monitoring
CHANGE_ME_… is not an empty value, so neither compose nor the services block it, and that string is identical on every installation. The installer replaces it with
a new value at first install, but if the env was hand-written, put in a new value with bin/passwd.sh --generate PI_INBOUND_API_KEY
→ Changing Passwords and API Keys
Letting the Document · Photo Tools Accept Internal URLs
The document understanding and vision tools take a URL, not a file, and download it themselves. If that address resolves to loopback, a private network, or link-local, they reject it → MCP Tool Map
Places you intend to open — an in-house file server, for example — go in the allowlist. Compose passes this value to both the document understanding and vision services.
| Variable | Default | Meaning |
|---|---|---|
FETCH_ALLOWED_HOSTS | (empty) | Hosts to fetch from even at internal addresses. Comma-separated for several. Must match the URL's host name exactly (case-insensitive) — writing the port too, as in files.example.local or files.example.local:8080, matches that port only; for a server addressed by IP, write the IP as-is |
echo 'FETCH_ALLOWED_HOSTS=files.example.local,10.0.0.15' | sudo tee -a /etc/kopens/plantpulse-ai.env >/dev/null
cd /opt/kopens/plantpulse-ai-docker
bin/restart.sh plantpulse-document-understanding-api plantpulse-vision-api
- There are no wildcards or ranges (CIDR). Subdomains are listed one by one.
- Even an allowed host is accepted only over
http·https, and the size limits (documents 200 MiB · photos 32 MiB) still apply. Redirects are re-checked at every hop, so if an allowed host redirects to a disallowed internal address, it is rejected there. - This line overrides a compose default (an empty value), so the pre-start check raises a
overridenotice — that is normal.
RAG Object Storage Bucket
rag-api keeps manually uploaded source documents · drawing structures and previews · video playback copies and transcripts in a single bucket in the platform RustFS.
| Variable | Default | Meaning |
|---|---|---|
RAG_S3_BUCKET | plantpulse-rag | The bucket name. Give the second installation a different name only when two AI installations share the same platform |
Drawing Ingestion — Off by Default
This feature reads and indexes drawings (DXF · vector PDFs with a text layer) as title blocks · dimensions · tolerances · BOM rows. It ships disabled, and while disabled, rag-api's drawing APIs and the related tables are never created at all (same as before the feature existed).
| Variable | Default | Meaning |
|---|---|---|
DRAWING_INGEST_ENABLED | false | true makes rag-api enable drawing ingestion. .dxf is always treated as a drawing; a PDF is read as a drawing only when its type is given as «drawing» (document_kind=drawing) at upload. Values outside true · false leave a warning and are read as off |
echo 'DRAWING_INGEST_ENABLED=true' | sudo tee -a /etc/kopens/plantpulse-ai.env >/dev/null
cd /opt/kopens/plantpulse-ai-docker
bin/restart.sh plantpulse-rag-api
- DWG is not accepted — export to DXF or PDF and upload that. The per-file limit is 50 MB.
- Platform asset documents also arrive as drawings (2026.10 re-release 3). The sync that imports documents attached to platform assets feeds
.dxffiles and PDFs whose «document type» isDRAWINGinto the drawing path. Drawings that arrive this way are owned by the platform, so overwriting or deleting them from the admin console returns409("drawing is platform-managed …") — edit or delete them on the platform, and to exclude them from search only, use «Exclude from corpus» in the admin console document table (from 2026.10 re-release 4 the console button is enabled for ledger drawings; drawings you uploaded yourself still cannot be excluded). While disabled,.dxfonly counts them (drawing_disabled) without ingesting, and PDFs are ingested as ordinary documents. - Whether a drawing's revision is the one to work from is asked of the digital thread (the PLM released revision · open engineering changes). If the title block lacks a part number or
revision, or the digital thread cannot answer, the revision state is
unknownand it is not shown as «latest». - When enabled, rag-api creates the drawing tables (
rag_drawing_*) in therag_ingestDB at startup — there is no migration to run by hand.
Video · Audio Ingestion — Off by Default, Enabled with One Line
The feature that transcribes video · audio (speech → time-stamped text) and indexes it, and ask by voice in the copilot chat input, both use the same transcription
serving (plantpulse-asr). It ships disabled, and what enables it is one line in the host env: PI_MEDIA_ENABLED=true
(present in the template as the comment #PI_MEDIA_ENABLED=true). Usage is covered in Video · Audio Search.
| What this one line also determines | Details |
|---|---|
| Services that come up | plantpulse-asr-audio — a one-shot service that moves the audio library into volume pi-asr-audio and exits · plantpulse-asr — vLLM Whisper large-v3-turbo, serving name aura-asr, internal 8000 (not exposed on the host). The operations scripts bring them up by attaching the compose profile media |
| Container values | rag-api MEDIA_INGEST_ENABLED=true · document understanding ASR_URL=http://plantpulse-asr:8000 (the installation's compose/media/true.env) |
| Model | bin/fetch-models.sh fetches the transcription model Aura-ASR-Turbo (about 1.5 GB to download, about 1.6 GB unpacked). Disabled installations do not fetch it |
| GPU | ASR_GPU_UTIL is included in the hardware profile's sum even when disabled, so enabling it does not exceed the cap |
cd /opt/kopens/plantpulse-ai-docker
sudo cp -p /etc/kopens/plantpulse-ai.env /etc/kopens/plantpulse-ai.env.bak-media-$(date +%Y%m%d-%H%M)
sudo sed -i '/^#\?PI_MEDIA_ENABLED=/d' /etc/kopens/plantpulse-ai.env && echo PI_MEDIA_ENABLED=true | sudo tee -a /etc/kopens/plantpulse-ai.env >/dev/null
sudo bash bin/fetch-models.sh # Aura-ASR-Turbo — 이미 있으면 건너뜁니다
sudo bash bin/lint-env.sh && bin/restart.sh # rag-api · 문서 이해가 새 값으로 다시 만들어집니다
bin/status.sh # 켠 설치는 23/23 — plantpulse-asr-audio 는 «Exited (0)» 이 정상입니다
You can tell whether it is on in three places.
# rag-api — "media_ingest_enabled":true
curl -s -H "X-API-Key: <PI_INBOUND_API_KEY>" http://127.0.0.1:7115/api/v1/config | grep -o '"media_ingest_enabled":[a-z]*'
# 문서 이해 — components.asr.status 가 "up"
docker exec plantpulse-document-understanding-api python3 -c \
"import urllib.request;print(urllib.request.urlopen('http://localhost:7120/health').read().decode())"
bin/status.sh counts a one-shot service (plantpulse-asr-audio) that ended with «exit code 0» as healthy. If it is not 0,
plantpulse-asr will not come up either — start with that log.
- Disabling — delete the line or set it to
PI_MEDIA_ENABLED=false, runbin/restart.sh, then remove the two remaining containers withdocker rm -f plantpulse-asr plantpulse-asr-audio(once the profile is off, the operations scripts no longer touch those two). - Air-gapped network — the images are always in the bundle. Only the model must be brought in separately → Air-Gapped Installation
- A value outside
true·falsemakes the pre-start check stop with[fail] PI_MEDIA_ENABLED=<value> — true 또는 false 만 받는다.
If you hand-wrote MEDIA_INGEST_ENABLED · ASR_URL into the host env on a pre-2026.10 version, the upgrade does not stop and keeps running on those values
(with no PI_MEDIA_ENABLED line, the old name is read as the switch). On its first run, the operations script moves the old name to
PI_MEDIA_ENABLED and deletes ASR_URL lines identical to what the switch supplies.
| Pre-start check | Meaning |
|---|---|
alias [warn] | Only the old name is present — it will still start. Move it with the accompanying single sed line. When the alias is removed in 2026.11, lines not moved become renamed [fail] |
media-same [warn] | ASR_URL in the host env is identical to what the switch supplies — it can be deleted |
media [fail] | ASR_URL in the host env differs from what the switch supplies — decide what you intended, then delete it. One line, PI_MEDIA_ENABLED=true, is what enables it |
| The two names have different values | [fail] — a person decides which was meant |
If it runs under an account that cannot write the env file, it cannot move it itself and prints [paths] [warn] cannot write … along with the sudo sh -c '…'
command for a person to run. The name read inside the container (rag-api) is still MEDIA_INGEST_ENABLED — that is set by the switch, so it is
not written in the host env.
The handles are compose defaults. Leaving them at their defaults is normal.
| Variable | Service | Default | Meaning |
|---|---|---|---|
PI_MEDIA_ENABLED | (host switch) | false | Above — enables video · audio ingestion and ask-by-voice together |
MEDIA_MAX_FILE_MB | rag-api | 2048 | Per-file video limit (MB). Separate from the document limit |
MEDIA_MAX_DURATION_MIN | rag-api | 120 | Length limit for a single video (minutes) |
MEDIA_TEXT_TRACKS | rag-api | subtitles | Text in video — off · subtitles (embedded subtitle tracks · sidecar subtitles srt/vtt) · all (+ burned-in subtitles · on-screen text read by the LLM) |
MEDIA_SPAN_TARGET_SEC | rag-api | 0 | Index segment length (seconds, 20–300). 0 makes one transcription chunk (about 120 seconds) one segment. Out-of-range values leave a warning and are read as 0. Changes apply from the next re-index onward |
MEDIA_PROCESSING_WINDOW | rag-api | (empty — 20:00-06:00) | The window in which video processing may start (HH:MM-HH:MM in PP_TZ; if the start is later than the end, it crosses midnight). For always-on processing, use always. Empty or malformed values fall back to the rag-api default 20:00-06:00. The window applies only to the start of processing — processing that has begun runs to completion even after the window closes. Processing shares the same LLM as chat |
ASR_URL | Document understanding | (set by the switch) | Transcription serving address. Enabled: http://plantpulse-asr:8000; disabled: empty (the transcription entry point returns 503). 🔴 Do not write it in the host env — see media · media-same in the table above |
ASR_NAME · ASR_BACKEND | Document understanding · asr | aura-asr · vllm | Serving name · engine (only vllm is accepted) |
ASR_MODEL_PATH | asr | /opt/kopens/models/Aura-ASR-Turbo | Transcription model location |
OCR_URL · OCR_BACKEND | Document understanding | http://plantpulse-llm:8000 · vlm | Serving LLM that reads text in video. Used only when transcription is enabled |
Vision Text Reading Engine
| Variable | Default | Meaning |
|---|---|---|
VISION_OCR_ENGINE | (empty — vision default vlm) | The engine for vision_ocr. With vlm, the serving LLM reads it and must produce the same string two times out of three to answer (it reads Korean). docling delegates to document understanding's OCR (which cannot read Korean) |
Related Documents
- Installation Overview — where it is installed and what comes up
- Service Port Map
- Service Operations