Skip to main content

Environment Variable Configuration

PlantPulse AI configuration starts from a single file: /etc/kopens/plantpulse-ai.env.

sudo vi /etc/kopens/plantpulse-ai.env
The authoritative configuration lives under /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.

Do not edit passwords and API keys by hand

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.

WhereWhatHow to change it
Host env /etc/kopens/plantpulse-ai.envHardware shape (PP_HW_PROFILE) · site values (language · time zone · certificate name) · platform connection · secretsEdit this file and re-create the services
Hardware profile compose/profiles/<모양>.envValues that differ per hardware — the GPU share of each of the three vLLM instances and the cap on their sumSet by the release
compose defaultsProduct defaults — images · model names and paths · service tuningSet 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.

VerdictWhat
releaseImage references — DOCKER_REGISTRY · DOCKER_NAMESPACE · UPSTREAM_* · *_IMAGE · PI_IMAGE_TAG_* · a non-empty PP_IMAGE_TAG
profileHardware profile keys (LLM_GPU_UTIL and the like)
redundantA line that repeats a compose default verbatim
derivedPP_HOST — computed from the host of PP_DATALAKE_ADMIN_URL
unknownA key nothing reads
renamedAn 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.

VariableMeaning
DOCKER_REGISTRY · DOCKER_NAMESPACEWhere first-party images are pulled from (docker.kopens.io / pi)
UPSTREAM_DOCKERHUB · UPSTREAM_NVCRIn-house proxy that pulls third-party images on your behalf
PP_IMAGE_TAGDefault 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).

Leave PP_IMAGE_TAG empty

Left 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» tag

Re-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​

VariableDefaultMeaning
PP_LANGkoThe 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_TZAsia/SeoulTime 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_LANGUAGEKoreanLanguage 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.

VariableMeaning
PP_DATALAKE_ADMIN_URLPlatform data lake management API — https://<플랫폼 호스트>:4950. The host in this URL is the platform host
PP_DATALAKE_ADMIN_API_KEYKey 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 the fetch fails, that container «stops»
  • 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.crt only. 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.

VariableMeaning
PP_DATALAKE_ADMIN_URL · PP_DATALAKE_ADMIN_API_KEYAbove — where credentials are fetched from
PP_API_URL · PP_API_TOKENPlatform V5 API — not in the credential store, so it remains in the host env (see "Platform Integration MCP" below)
PP_PG_PORT · PP_CASSANDRA_PORTPresent 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.

Upgrading to 2026.10 — an old env will not come up as-is

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_KEY and 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 as renamed and a separate sed that only renames them is printed — running it moves the values as-is to CO_* (see "Copilot Settings" below).
  • Values deliberately set differently from the defaults (model paths, for example) remain as override notices and are not deleted.
Equipment alarms arrive over MQTT

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.

VariableMeaning
PP_API_URLPlatform server-web base URL
PP_API_TOKENAPI token issued by the platform
The stack starts even with a «wrong» token

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).

VariableDefaultMeaning
LLM_NAMEaura-liteThe name used to call the generation model. Doubles as the vision model
EMB_NAMEaura-embedding-4bEmbedding model name
RERANK_NAMEaura-reranker-0.6bReranking model name
LLM_MODEL_PATH/opt/kopens/models/Aura-Lite-NVFP4Weights 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-prefillvLLM 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_KEYInstaller-generatedKey in front of vLLM
PI_LLM_API_KEYInstaller-generatedLiteLLM master key
PI_LITELLM_SALT_KEYInstaller-generatedSalt LiteLLM uses to encrypt stored values
PI_LITELLM_UI_USERNAME · PI_LITELLM_UI_PASSWORDadmin · admin123!Login for the LiteLLM admin UI (:4000/ui). The password is required → Changing the Initial Password
PI_LITELLM_PORT4000Port 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 gone

From 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.

The model name is aura-* — there are no aliases for the old names

The 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.

Do not change PI_LITELLM_SALT_KEY

This 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.

Variablegb10 / g7eMeaning
LLM_GPU_UTIL · EMB_GPU_UTIL · RERANK_GPU_UTIL0.4 · 0.10 · 0.08 / 0.53 · 0.13 · 0.10GPU memory shares of the three vLLM instances
TS_GPU_UTIL0.13 / 0.15Share of the time series inference server
ASR_GPU_UTIL0.03 / 0.04Share 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_CEILING0.75 / 0.95Cap 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).

If the sum is exceeded, vLLM dies and respawns every 24 seconds

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.

Upgrading to 2026.10 — do not delete CH_* lines, «move» them

There 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 printsMeaning · action
only the old name is set — rename itOnly 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 valueThe new name already exists with the same value. Delete the old line
both set and DIFFERENTBoth 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.

WhatWhere it is setIf 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 domainsAdmin 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 serverAdmin console › System Settings › smtp.host · smtp.port · smtp.username · smtp.password · smtp.starttls, sender navigator.report.email.fromMail 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 variableMessenger channels cannot be selected under «Send» in automation
Links in mail · messenger bodiesAdmin 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 keyDefaultMeaning
automation.deliver.mail.daily-max6Number of mails one automation sends to one person per day
automation.deliver.webhook.daily-max12Number of messenger messages one automation sends to one channel per day
automation.deliver.role.max-members50Number 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.

VariableDefaultMeaning
CO_LLM_THINKING_ENABLEDtrue (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_TOKENS4096 (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_CHARS24000Maximum 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
Turning thinking off is faster but it «makes things up»

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.

KeyScreen name
llm.reasoning.chatDefault chat thinking depth
llm.reasoning.automationAutomation thinking depth
llm.reasoning.diagnosisAnomaly diagnosis thinking depth
llm.reasoning.proposalAction recommendation thinking depth
llm.reasoning.otherThinking depth for other generation
  • Values are none (off) · low · medium · high, or empty («follow the server default»). none turns 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.chat is set.
If an answer ends up «blank», look at the limit

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.

SettingResult
Thinking on + 4096Blank answer — the thinking consumes the entire limit
Thinking offFast, but it skips the tools and makes things up
Thinking on + 16384Calls 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."

VariableDefaultMeaning
LIGHTRAG_LLM_TIMEOUT3600Limit (seconds) on entity-extraction LLM calls. The worker limit is twice this value
LIGHTRAG_LLM_MAX_TOKENS32768Maximum tokens a single extraction call may produce — the runaway ceiling
LIGHTRAG_MAX_ASYNC4Number of concurrent LLM calls. 🔴 Lowering it makes things slower
If indexing fails with Worker execution timeout

A 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

VariableDefaultMeaning
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
Empty means «no entry» with this key — not a free pass

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​

VariableMeaning
PI_ADMIN_USER · PI_ADMIN_PASSWORDAdmin console login. ID defaults to admin; the password is chosen by a person at install time
PI_ADMIN_SESSION_SECRETAdmin console session signing key — generated by the installer
ADMIN_SESSION_MAX_AGESession lifetime (minutes). Default 480
PI_ADMIN_WEB_PORTAdmin console HTTP port. Default 5959
PI_ADMIN_WEB_TLS_PORTAdmin 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_PASSWORDGraph account — both Neo4j instances (LightRAG knowledge graph plantpulse-neo4j · factory graph mirror plantpulse-digital-thread-neo4j) use the same account
PI_RAG_API_KEYRAG API call key
PI_INBOUND_API_KEYKey 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.

The admin console ports moved from 9080 to 5959 · 5960 (2026.10)

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.

VariableWhat it sealsWhen empty
PI_COPILOT_SECRET_KEYCredentials 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_KEYConnector credentials in the digital thread connector registryOnly the connector registry returns config_error (503). The graph mirror keeps running
Change or lose it and stored credentials cannot be decrypted
  • These two values must be in the /etc/kopens/plantpulse-ai.env backup 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/null
    echo "PI_DT_SECRET_KEY=$(openssl rand -base64 32)" | sudo tee -a /etc/kopens/plantpulse-ai.env >/dev/null
  • bin/passwd.sh does 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.

VariableDefaultMeaning
PI_DT_SECRET_KEYInstaller-generatedConnector credential encryption key (above)
DT_INSIGHT_EVENTS_PUSHfalseWhether the digital thread pushes the platform events it read to time series insight
TS_EVENTS_SOURCEdirectWhere 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_READdbHow 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.

VariableDefaultMeaning
DT_CONNECTOR_SYNC_ENABLEDtruePeriodic sync. false stops it, leaving only «Sync now» in the admin console
DT_CONNECTOR_SYNC_INTERVAL_S0Sync 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.

WhereIf 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 serviceIf 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.

VariableDefaultMeaning
PI_INBOUND_AUTH_DISABLEDfalseOnly 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.

OutputMeaning
[fail] PI_INBOUND_AUTH_DISABLED=<value> — write true or falseA value outside true · false
[note] PI_INBOUND_AUTH_DISABLED=true is ignored — PI_INBOUND_API_KEY is set and the key winsIf 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

Placeholders pass this check

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.

VariableDefaultMeaning
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 override notice — 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.

VariableDefaultMeaning
RAG_S3_BUCKETplantpulse-ragThe bucket name. Give the second installation a different name only when two AI installations share the same platform

→ Databases


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).

VariableDefaultMeaning
DRAWING_INGEST_ENABLEDfalsetrue 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 .dxf files and PDFs whose «document type» is DRAWING into the drawing path. Drawings that arrive this way are owned by the platform, so overwriting or deleting them from the admin console returns 409 ("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, .dxf only 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 unknown and it is not shown as «latest».
  • When enabled, rag-api creates the drawing tables (rag_drawing_*) in the rag_ingest DB 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 determinesDetails
Services that come upplantpulse-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 valuesrag-api MEDIA_INGEST_ENABLED=true · document understanding ASR_URL=http://plantpulse-asr:8000 (the installation's compose/media/true.env)
Modelbin/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
GPUASR_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, run bin/restart.sh, then remove the two remaining containers with docker 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 · false makes the pre-start check stop with [fail] PI_MEDIA_ENABLED=<value> — true 또는 false 만 받는다.
The old name MEDIA_INGEST_ENABLED — an alias for this release only

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 checkMeaning
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.

VariableServiceDefaultMeaning
PI_MEDIA_ENABLED(host switch)falseAbove — enables video · audio ingestion and ask-by-voice together
MEDIA_MAX_FILE_MBrag-api2048Per-file video limit (MB). Separate from the document limit
MEDIA_MAX_DURATION_MINrag-api120Length limit for a single video (minutes)
MEDIA_TEXT_TRACKSrag-apisubtitlesText 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_SECrag-api0Index 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_WINDOWrag-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_URLDocument 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_BACKENDDocument understanding · asraura-asr · vllmServing name · engine (only vllm is accepted)
ASR_MODEL_PATHasr/opt/kopens/models/Aura-ASR-TurboTranscription model location
OCR_URL · OCR_BACKENDDocument understandinghttp://plantpulse-llm:8000 · vlmServing LLM that reads text in video. Used only when transcription is enabled

Vision Text Reading Engine​

VariableDefaultMeaning
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)