Agent docs

Configuration and secrets

> Evidence: 3 file:line witnesses as of 2026-08-18.

Evidence: 3 file:line witnesses as of 2026-08-18. Confidence: partial, some rules witnessed, the rest inherited from the seams. What changes it: the first module built against this doc (../PLAN.md forge-1) and its instance breakdown.

One typed config object, defaults that make local work, secrets by reference and never printed. The instances agree on the shape more than on anything else in the matrix, so this doc is short and mostly says why.

Audience: anyone adding a config value or a secret to a module.

What the instances do#

versable-runnerspeedwaywalmart-mvp
shapeone Config class, every var has a default, "the app imports with zero env" (services-api/lib/config/__init__.py:116-122, require_env(name, default=…); runner-service keeps it under overrides/)one flat object read once from process.env at load, inline comments per var on the prod-vs-dev fallback (app/lib/config.server.ts:1-139)pydantic BaseSettings, .env + env, lru_cached singleton (backend/app/config.py)
secretsSecret Manager, injected by reference at deploy; 8 secretsenv varsenv vars
echo/health/deep echoes effective runner confignonenone
lapseLANGFUSE_* keys default to empty strings, fine; RUNNER_API_PASSWORD empty silently flips to IAM-locked mode, a behaviour change hidden in an absent valueGCP-specific vars beside vendor-neutral ones with no groupingnone notable

The rule#

One typed config object per module, read once, with a default for every value that has a safe one, env as a first-class field, secrets referenced not embedded, and the effective non-secret config echoed by /health/deep.

One object, read once#

A module has one config class (pydantic, zod, whatever the language does well), constructed once at start from environment variables and, locally, a .env file. Nothing else in the module reads os.environ or process.env. The reason is the same one that makes the runner/payload seam hold: a raw read in a payload is a hidden dependency the runner cannot see, mock, or echo.

Defaults make local true, not prod convenient#

Every value that has a safe local default has one (env: local, in-process adapters, local storage directory, no tracing keys). Values that must be supplied in dev and prod (bucket, queue, issuer URL, service URL) have no default and the module refuses to start without them when env is not local, naming the missing key. An absent value that silently changes behaviour is the trap: versable-runner's empty password flipping auth into IAM-locked mode is a real feature with an invisible switch; the contract wants that switch to be an explicit auth.verifier: jwks | keys | local value.

env is a field, and config is validated against it#

env is one of local, dev, staging, prod. The config validates itself against it at start: no local verifier when env is not local, no bucket name that belongs to another env, no missing issuer client in prod. 12-deployment-and-environments.md.

Grouped by concern, named by concern#

Config is grouped the way the canon is: identity.*, queue.*, storage.*, cache.*, observability.*, usage.*, runtime.*, and vendors.* for outbound keys. A reader of the config file learns the module's shape from the group names, and vendor-specific values are visibly under the adapter that needs them (storage.gcs.bucket, queue.cloudtasks.name).

Secrets#

  • Referenced from a secret store and injected at deploy (Secret Manager on GCP; Render's and Vercel's stores elsewhere), never committed, never in a Dockerfile, never in a log or a health echo. versable-runner does this correctly (deploy.sh:72, --set-secrets … :latest).
  • Declared in the manifest, so an operator can see what a module needs without reading its source: vendors lists each key by env-var name with its purpose and whether the module refuses to start without it (contracts/manifest.md). A key not declared there is a key nobody knows to supply.
  • Presence is reported, value never: /health/deep says vendors.openai: set or absent.
  • Rotated by re-deploying with a new version reference; a module reads a secret at start and again on a SIGHUP or a restart, never on every call.
  • Local .env files are gitignored and carry only local values; a .env.example with every key and a dummy value is committed.

The echo#

/health/deep returns the effective config with secrets redacted to presence, in a stable shape across environments. It is the first thing to diff when two environments disagree (13-local-dev-and-debugging.md).

Do-nots#

  • Do not read an environment variable outside the config object.
  • Do not let an absent value silently change behaviour. Make the mode an explicit value. (versable-runner RUNNER_API_PASSWORD empty)
  • Do not default a value that must differ per environment.
  • Do not print a secret anywhere: log, echo, error message, transcript.
  • Do not commit a .env with real values; commit .env.example.
  • Do not mix vendor-specific and vendor-neutral values in one flat namespace without grouping. (speedway config.server.ts)
@versable-git/ui · reference, canon, and method, read in place