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:

refusedwhy
ECKO_AI_BASE_URLredirects every AI call, and the API key goes with it
ECKO_PKG_GIT_BASE, ECKO_TEMPLATES_REPOdecide where packages and templates are fetched from
ECKO_AI_API_KEY, ECKO_LLM_API_KEY, ECKO_PKG_TOKENcredentials
ECKO_PKG_TOKEN_HOSTSdecides which hosts receive the package token
ECKO_NET_ALLOW_HTTPallows package fetches over plain http
ECKO_AI_TRACE, ECKO_AI_CACHEwrite every prompt and response to a path
ECKO_PY_ENVforwards 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.