MINARA

Environment Variables

How the agent loads operator env, and how that relates to Settings

This page covers how minara-agent-v2 loads env at boot. For the per-variable reference with defaults, formats, consumer files, and "what breaks if unset", see Reference → Environment variables.

User-visible knobs persist in Settings (settings.json#preferences). Secrets persist in credentials.json (Settings → API Keys / Messaging). Call sites read prefs.get / secrets.* / infra.get. Env is the operator boot layer and the documented fallback in that chain.

Both the reference pages and the .env.example template at the project root are generated from a single declaration source: apps/agent/src/config/env-docs/ (narratives, in four languages) joined with the runtime registries (preferences schema, API-key families, messaging providers, LLM provider families, infra schema). They cannot drift from each other; CI regenerates and diffs both.

MINARA_ is reserved for Minara-platform-specific variables (MINARA_API_KEY, MINARA_BASE_URL, MINARA_SKIP_FUND_CONFIRM, MINARA_DATA_DIR, MINARA_OPENAI_BASE_URL, MINARA_OPENAI_CHAT_PATH). Agent-loop infrastructure variables (gateway, log, model, scenario, memory knobs) drop the prefix.

TL;DR Convention

Any new skill or tool that needs an API key, secret, token, or overridable URL MUST:

  1. Persist it in the right home, then read the façade. Secrets go through the API-key or messaging registry and secrets.dataSource / secrets.messaging. User knobs go through the preferences schema and prefs.get. Operator infra (GATEWAY_PORT, MINARA_DATA_DIR) goes through config/infra/schema.ts and infra.get. Never hard-code secrets. Never accept them as CLI args you echo.

  2. Declare the narrative in apps/agent/src/config/env-docs/ in the same commit: add a block to the matching section module (what it controls, which skill/tool consumes it, when an operator would set it, the default behavior when unset, and the accepted value format), plus the cn / ja / ko translations under env-docs/i18n/. Then run pnpm --filter @minara/agent generate:env-example and pnpm --filter @minara/docs generate and commit the regenerated .env.example and reference pages. A var that is not covered by any registry must declare an exempt bucket, or the generator fails.

  3. For domain skills (apps/agent/src/skills/builtin/*.ts or apps/agent/src/skills/external/<id>/), declare the var in requires_env so the SkillRegistry hides the skill entirely when the credential is missing:

    export const myNewSkill: DomainSkill = {
      id: "research.my_provider",
      // ...
      requires_env: ["MY_PROVIDER_API_KEY"],
    };
  4. For tools (apps/agent/src/tools/*.ts), the factory should return an empty ToolEntry[] when the secret is missing. The tool registry silently excludes unregistered names, so downstream skill tool_names references will gracefully degrade:

    export function createMyProviderTools(): ToolEntry[] {
      const apiKey = secrets.dataSource("MY_PROVIDER_API_KEY");
      if (!apiKey) return [];
      // ...
    }
  5. Never commit a real secret. .env is git-ignored; .env.example is the committed template with blank values.

How .env loading works

Loading is handled by apps/agent/src/config/load-env.ts, a side-effect module that calls Node 22's built-in process.loadEnvFile(".env"). It's imported as the very first line of every entrypoint:

  • apps/agent/src/gateway/cli.ts (REPL mode)
  • apps/agent/src/gateway/server.ts (HTTP mode)

The loader fills missing keys only. After boot, nothing writes process.env. Resolvers classify origin from the remaining env snapshot plus the Settings / credentials override.

Precedence at the call site, highest first: Settings / credentials override, then variables already exported by your shell / CI / systemd, then project .env, then the schema or infra default. The loader does not overwrite existing keys. This matches Node's default behavior and is the safest rule (no surprise masking of CI-injected secrets).

No dotenv dependency. We rely purely on process.loadEnvFile, stable since Node 22.5. Missing .env is a no-op (not an error).

Checklist for adding a new key

Before merging:

  • Home picked: preferences schema, API-key / messaging registry, or infra schema (plus an exempt bucket when it belongs to no prefs/secrets registry)
  • Call site reads prefs.get / secrets.* / infra.get, not process.env.<NAME>
  • Narrative block added to the matching section module under apps/agent/src/config/env-docs/sections/, covering purpose, consumer, when to set, default-when-unset, and value format
  • cn / ja / ko translations added under apps/agent/src/config/env-docs/i18n/ (the parity check fails otherwise)
  • .env.example and content/docs/reference/env/ regenerated and committed (generate:env-example + pnpm --filter @minara/docs generate)
  • For domain skills: requires_env: ["<NAME>"] is set so the skill self-hides when the credential is missing
  • For tools: factory returns [] when the secret is missing (never throws at boot)
  • Var name follows the convention <PROVIDER>_API_KEY / <PROVIDER>_TOKEN / <PROVIDER>_<FIELD>: UPPER_SNAKE, provider prefix first
  • No real secret value is committed anywhere

On this page