Secrets
The file holds names and paths, never values.
The configuration file holds NAMES, never values. A configuration file that contains a secret is a payload, not configuration.
The one credentials file
Every credential the engine itself reads lives in one file per host, credentials.json: a
flat JSON object of NAME → value, mode 0600. The configuration names that file once, and each
section that needs a credential names an entry in it:
| key | what it carries |
|---|---|
credentials.file | the absolute path of the file. A relative path is refused |
channel.credential · evidence.credential · evidence.reader_credential · lane.<alias>.credential · notice.binding.<name>.credential | the name of an entry in that file — shaped [A-Za-z_][A-Za-z0-9_]*. Anything else (a pasted user:password or sk-…) is refused as credentials-name-not-a-name, and the refusal does not print what you typed |
The file is validated when the configuration loads, not when a credential is first needed:
every verb except config set refuses by name until the file and the configuration agree. The
value is then read fresh at the moment of use, so a file re-chmodded or rewritten later is caught
too. The credential value never appears in any message, argv, or record — refusals name the
key, the entry name and the file path, nothing else.
| refusal | what it means |
|---|---|
credentials-file-undeclared | a section names a credential and no credentials.file is set |
credentials-file-not-absolute | credentials.file is relative, or padded with whitespace |
credentials-file-absent | the declared file is not on this host |
credentials-file-not-regular | the path is a directory, FIFO, socket or device — refused before it is opened, because opening one can block |
credentials-file-mode | the file is not mode 0600 — fix with chmod 600 <file> |
credentials-file-unreadable | the file is regular and 0600 and still could not be read |
credentials-file-not-json · credentials-file-not-an-object | the content is not a flat JSON object (the parser's message is withheld, because it would quote the bytes) |
credentials-value-not-a-string · credentials-value-empty · credentials-name-empty | an entry is malformed |
credentials-name-unknown | a section names an entry the file does not carry; the refusal lists the names it does carry |
credentials-key-retired | the configuration uses an older per-section spelling (credentials_env, credentials_file, couchdb_credentials_env …); the refusal names the key that replaces it and what to do with the old file |
For a lane, two more: lane-unknown (no such lane is declared — the refusal lists the declared
ones) and lane-credentials-undeclared (the lane declares no credential).
A lane's credential does not travel. When a dispatch carries a lane declaration to another
host, only the base_url, the credential's NAME and the api_protocol travel; the execution host
resolves the value from its own credentials file, and refuses
lane-credential-not-on-this-host before the first model call if it cannot.
Keys that still name an environment variable
A few server keys name an environment variable rather than an entry: store.postgres_dsn_env,
and in [oauth_server] postgres_dsn_env, turnstile_secret_env and mail_token_env. The
variable must be set in the environment of the process that reads it.
⚠ gateway.couchdb_proxy_secret_env is resolved as an entry in the credentials file, not as
an environment variable, although its help text says otherwise; and
gateway.couchdb_proxy_secret_file is accepted by config set but refused as retired when the
gateway uses it. Put the proxy secret in credentials.json and name its entry in
gateway.couchdb_proxy_secret_env.