std.defaults
The ecko.json sitting next to the file you run is loaded automatically, before your program starts. There is no loader to call and no setup step - the values are simply there.
{
"app": "billing",
"version": "1.0",
"api_url": "https://api.example.com",
"environment": { "ECKO_AI_PROVIDER": "ollama" }
}
import std.defaults
print(defaults.app) # "billing"
print(defaults.version) # "1.0"
Two separate things happen when that file is read.
Every key becomes a default
Each top-level key other than environment becomes a member of std.defaults, keeping its JSON type - strings stay strings, numbers become int or float, and lists and objects become Ecko lists and maps.
import std.defaults
defaults.retries + 1 # a JSON number is a real number
defaults.hosts[0] # a JSON list is a real list
Indexing by string reaches the same value, which is what you want when the key is only known at runtime:
import std.defaults
wanted = "api_url"
defaults[wanted] # "https://api.example.com"
Reading a key that isn't there is an error, like any other missing member. Since ecko check has no way to know what a given ecko.json holds, it never reports members of std.defaults as unknown.
Every key must be a valid identifier
Letters, digits and underscores, not starting with a digit - the same rule that governs variable names. Keywords are fine, so defaults.type works.
JSON is happy to hold "api-url" or "two words", but neither can be read back: defaults.api-url parses as a subtraction. Rather than load values that nothing can name, the file is refused when it loads:
error: ecko.json: 'api-url' is not a usable key - every key is read as
defaults.<name>, so it must be a valid identifier: letters, digits and
underscores, not starting with a digit
The same rule applies to names inside environment, where a name a shell cannot export is no use either.
environment configures the process
The optional environment object is applied to the process environment before your program runs, so it reads back through std.os like anything else:
import std.os
os.env("ECKO_AI_PROVIDER") # "ollama"
Values must be a string, number, or boolean. An object or a list is an error rather than a silently JSON-encoded string - there is no sensible environment-variable spelling for one.
The shell wins. An entry applies only to a variable that is not already set, the same env-over-file rule as std.config: the block gives a project its defaults, and whoever runs it can still change any of them by exporting the variable. (Before 0.58 the block overrode the shell, which also meant a cloned repository could override what you had exported.)
Because it is applied before evaluation, it also configures Ecko itself - most of Ecko's ECKO_* environment variables work here:
{ "environment": { "ECKO_AI_PROVIDER": "ollama", "ECKO_LIMIT_DEPTH": "5000" } }
Eleven are refused, because a manifest arrives by cloning a repository and is applied before your program starts. It may tune how the program behaves, but not change where its data goes or what it trusts:
| refused | why |
|---|---|
ECKO_AI_BASE_URL | redirects every AI call, and the API key goes with it |
ECKO_PKG_GIT_BASE, ECKO_TEMPLATES_REPO | decide where packages and templates are fetched from |
ECKO_AI_API_KEY, ECKO_LLM_API_KEY, ECKO_PKG_TOKEN | credentials |
ECKO_PKG_TOKEN_HOSTS | decides which hosts receive the package token |
ECKO_NET_ALLOW_HTTP | allows package fetches over plain http |
ECKO_AI_TRACE, ECKO_AI_CACHE | write every prompt and response to a path |
ECKO_PY_ENV | forwards environment variables into the Python worker |
Set those in the shell that runs ecko, where they are the operator's choice rather than the project's. Package code has always been refused them; the manifest simply had no check.
The same file is the package manifest
There is no second file. A package manifest is just a defaults file that has no environment block, so its own fields read back the same way:
import std.defaults
defaults.version # the manifest's version
defaults.entrypoint # "main.ecko"
The two readers are independent. The manifest reader stays strict about the fields a package needs; the defaults reader accepts any JSON object. A project that only wants defaults therefore never has to satisfy the manifest schema - this is a perfectly good ecko.json:
{ "app": "scratch", "environment": { "TZ": "UTC" } }
What belongs here
This is project configuration, not a secret store: ecko.json is a file you commit, and std.defaults is ungated because it holds what you put in it.
Secrets belong in the environment, which stays behind the env capability, or in std.config with secret: true.