Current section
Files
Jump to
Current section
Files
lib/planck/headless/config.ex
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, <key>, ...`
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_<key>/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