defmodule Planck.Headless.Config do @moduledoc """ Resolved runtime configuration for `planck_headless`. Config is resolved by Skogsra, which reads from three sources in priority order (highest first): 1. Environment variables (`PLANCK_*`) 2. Application config — `config :planck, , ...` 3. Hardcoded defaults Values are cached in persistent terms via `preload/0` at application boot; the application also calls `validate!/0` to fail fast on malformed config. To change a value at runtime, set the application env and call the Skogsra-generated `reload_/0` function. JSON config files (`~/.planck/config.json` and `.planck/config.json`) are read via `JsonBinding` (internal module) as part of Skogsra's binding chain. Keys that appear in a JSON file override application config but are overridden by env vars. ## Env vars ### Planner config | Env var | Config key | Default | |---------------------------|----------------------|-----------------------------------| | `PLANCK_DEFAULT_PROVIDER` | `:default_provider` | `nil` | | `PLANCK_DEFAULT_MODEL` | `:default_model` | `nil` | | `PLANCK_SESSIONS_DIR` | `:sessions_dir` | `.planck/sessions` | | `PLANCK_SKILLS_DIRS` | `:skills_dirs` | `.planck/skills:~/.planck/skills` | | `PLANCK_TEAMS_DIRS` | `:teams_dirs` | `.planck/teams:~/.planck/teams` | | `PLANCK_SIDECAR` | `:sidecar` | `.planck/sidecar` | `*_DIRS` env vars take a colon-separated list; paths are expanded at runtime (`~` and relative paths resolved). The `:models` key has no env var equivalent — declare models in `.planck/config.json` or `config :planck, :models, [...]`. ### Provider API keys API keys are not included in `get/0` or the `%Config{}` struct to avoid accidental exposure in logs or inspect output. Use the generated getter functions directly (e.g. `Planck.Headless.Config.anthropic_api_key!/0`). | Env var | Config key | Used for | |-----------------------|------------------------|----------------------------| | `ANTHROPIC_API_KEY` | `:anthropic_api_key` | Anthropic (Claude) models | | `OPENAI_API_KEY` | `:openai_api_key` | OpenAI models | | `GOOGLE_API_KEY` | `:google_api_key` | Google (Gemini) models | """ use Skogsra defmodule Providers do @moduledoc "Skogsra type for the providers map in config.json." use Skogsra.Type @impl Skogsra.Type @spec cast(term()) :: {:ok, %{String.t() => map()}} | {:error, String.t()} def cast(map) when is_map(map), do: {:ok, map} def cast(_), do: {:error, "expected a map of provider entries"} end defmodule Models do @moduledoc "Skogsra type for the models list in config.json." # Raw list passthrough — model structs are built lazily by # Planck.AI.Config.from_config/2 when available_models are needed. # No env-var form — model declarations are too structured for a flat string. use Skogsra.Type @impl Skogsra.Type @spec cast(term()) :: {:ok, [map()]} | {:error, String.t()} def cast(list) when is_list(list), do: {:ok, list} def cast(_), do: {:error, "expected a list of model entries"} end defmodule PathList do @moduledoc """ Skogsra type for path lists set via environment variables. Uses `:` as separator on Unix and `;` on Windows, matching each platform's `PATH` convention. Both separators are accepted on all platforms so that cross-platform config files stay portable. Drive-letter colons (e.g. `C:`) are never mistaken for separators because they are immediately followed by `\\` or `/`. Examples: Unix: `~/.planck/skills:.planck/skills` Windows: `~/.planck/skills;.planck/skills` """ use Skogsra.Type @impl Skogsra.Type @spec cast(term()) :: {:ok, [String.t()]} | {:error, String.t()} def cast(value) when is_binary(value) do paths = value |> String.split(~r/;|:(?![\/\\])/) |> Enum.map(&String.trim/1) |> Enum.reject(&(&1 == "")) {:ok, paths} end def cast(value) when is_list(value) do if Enum.all?(value, &is_binary/1) do {:ok, value} else {:error, "expected a list of strings, got: #{inspect(value)}"} end end def cast(value) do {:error, "expected a path list string or list of strings, got: #{inspect(value)}"} end end @typedoc """ The resolved configuration struct returned by `get/0`. """ @type t :: %__MODULE__{ default_provider: String.t() | nil, default_model: String.t() | nil, sessions_dir: Path.t(), skills_dirs: [Path.t()], teams_dirs: [Path.t()], sidecar: Path.t(), providers: %{String.t() => map()}, models: [map()], tool_proxy: String.t() | nil, tool_proxy_ca_cert: String.t() | nil, secrets_hook: String.t() | nil } defstruct default_provider: nil, default_model: nil, sessions_dir: ".planck/sessions", skills_dirs: [".planck/skills", "~/.planck/skills"], teams_dirs: [".planck/teams", "~/.planck/teams"], sidecar: ".planck/sidecar", providers: %{}, models: [], tool_proxy: nil, tool_proxy_ca_cert: nil, secrets_hook: nil @envdoc """ Colon-separated list of JSON config files to read at boot, in order. Later files override earlier ones. Not read from the JSON files themselves — that would be circular. Defaults to the user-global file followed by the project-local file (project-local wins on collision). """ app_env :config_files, :planck, :config_files, type: PathList, default: ["~/.planck/config.json", ".planck/config.json"] @envdoc """ Ordered list of `.env` files to read for API keys. Global file is read first; project-local file wins on collision. Not read from the `.env` files themselves — that would be circular. """ app_env :env_files, :planck, :env_files, type: PathList, default: ["~/.planck/.env", "./.planck/.env"] # Config keys that can also be set in .planck/config.json or ~/.planck/config.json. # API keys are intentionally excluded — credentials must not live in config files. @json [:system, Planck.Headless.Config.JsonBinding, :config] # API key binding order: system env → project .env → global .env → Elixir config. @dotenv [:system, Planck.Headless.Config.EnvBinding, :config] @envdoc "Default provider key — references an entry in the `providers` map (e.g. \"anthropic\")." app_env :default_provider, :planck, :default_provider, default: nil, binding_order: @json @envdoc "Default model id within the default provider (e.g. claude-sonnet-4-6)." app_env :default_model, :planck, :default_model, default: nil, binding_order: @json @envdoc """ UI locale (e.g. `"en"`, `"es"`). Set in `.planck/config.json` for a project-specific language or in `~/.planck/config.json` for a global preference. When absent the browser's Accept-Language header is used, falling back to English. """ app_env :locale, :planck, :locale, default: nil, binding_order: @json @envdoc "Path to the sessions directory." app_env :sessions_dir, :planck, :sessions_dir, default: ".planck/sessions", binding_order: @json @envdoc "Colon-separated list of skill directories." app_env :skills_dirs, :planck, :skills_dirs, type: PathList, default: [".planck/skills", "~/.planck/skills"], binding_order: @json @envdoc "Maximum number of recently-used skills shown in the agent skill index." app_env :top_skills, :planck, :top_skills, type: :integer, default: 5, binding_order: @json @envdoc "Colon-separated list of team directories." app_env :teams_dirs, :planck, :teams_dirs, type: PathList, default: [".planck/teams", "~/.planck/teams"], binding_order: @json @envdoc """ Path to the sidecar Mix project directory. planck_headless starts the sidecar application from this path when it exists on disk. Set to a non-existent path to disable sidecar startup. """ app_env :sidecar, :planck, :sidecar, os_env: "PLANCK_SIDECAR", default: ".planck/sidecar", binding_order: @json @envdoc """ Module that implements `Planck.Headless.Secrets` for storing and retrieving API keys. Defaults to `Planck.Headless.Secrets.EnvFile` (reads/writes `.planck/.env`). Set to `"Sidecar.Secrets.AgentVault"` to store credentials in agent-vault instead. """ app_env :secrets_hook, :planck, :secrets_hook, os_env: "PLANCK_SECRETS_HOOK", default: nil, binding_order: @json @envdoc """ HTTP proxy URL for all outgoing LLM requests (e.g. `"http://vault:14322"`). When set, Planck configures its HTTP client to route LLM API calls through this proxy and sets standard proxy env vars for child processes (bash tools, sidecar, etc.). Designed for use with credential-injecting proxies such as agent-vault. """ app_env :tool_proxy, :planck, :tool_proxy, os_env: "PLANCK_TOOL_PROXY", default: nil, binding_order: @json @envdoc """ Path to the proxy CA certificate PEM file (e.g. `"/certs/ca.pem"`). Required when using a TLS MITM proxy such as agent-vault. Planck passes this to its HTTP client and sets `SSL_CERT_FILE`/`CURL_CA_BUNDLE` for child processes. """ app_env :tool_proxy_ca_cert, :planck, :tool_proxy_ca_cert, os_env: "PLANCK_TOOL_PROXY_CA_CERT", default: nil, binding_order: @json @envdoc """ Map of named provider entries. Each key is a user-defined provider alias; the value describes the provider type and connection details. Only readable from `.planck/config.json` or application config — no env var equivalent. Example (in .planck/config.json): ```json "providers": { "anthropic": { "type": "anthropic" }, "nvidia": { "type": "openai", "base_url": "https://integrate.api.nvidia.com/v1", "identifier": "NVIDIA" }, "local-ollama": { "type": "openai", "base_url": "http://localhost:11434", "has_api_key": false } } ``` """ app_env :providers, :planck, :providers, type: Providers, default: %{}, binding_order: @json @envdoc """ List of model declarations. Each entry references a key in `providers` and assigns a user alias. Only readable from `.planck/config.json` or application config — no env var equivalent (the format is too structured for a flat string). Example (in .planck/config.json): ```json "models": [ { "id": "sonnet", "model": "claude-sonnet-4-6", "provider": "anthropic" }, { "id": "llama70b", "model": "meta/llama-3.3-70b-instruct", "provider": "nvidia", "params": { "temperature": 0.6, "receive_timeout": 600000 } } ] ``` """ app_env :models, :planck, :models, type: Models, default: [], binding_order: @json # Provider API keys — not included in get/0 or %Config{} to avoid # accidental exposure. Use the generated getters directly. @envdoc "Anthropic API key." app_env :anthropic_api_key, :req_llm, :anthropic_api_key, os_env: "ANTHROPIC_API_KEY", default: nil, binding_order: @dotenv @envdoc "OpenAI API key." app_env :openai_api_key, :req_llm, :openai_api_key, os_env: "OPENAI_API_KEY", default: nil, binding_order: @dotenv @envdoc "Google API key." app_env :google_api_key, :req_llm, :google_api_key, os_env: "GOOGLE_API_KEY", default: nil, binding_order: @dotenv @doc "Return the fully-resolved config as a `%Planck.Headless.Config{}` struct." @spec get() :: t() def get do %__MODULE__{ default_provider: default_provider!(), default_model: default_model!(), sessions_dir: sessions_dir!(), skills_dirs: skills_dirs!(), teams_dirs: teams_dirs!(), sidecar: sidecar!(), providers: providers!(), models: models!(), tool_proxy: tool_proxy!(), tool_proxy_ca_cert: tool_proxy_ca_cert!(), secrets_hook: secrets_hook!() } end end