5. Configuration files
There are two layers. Later layers win.
| Layer | Files | Holds |
|---|---|---|
| Environment | config/{env}.yaml (development, demo, test, production) | ports, database, queue, JWT, model and parser endpoints, the settings: block |
| Local override | config/{env}.local.yaml (git-ignored) | your machine's changes, deep-merged by Loco |
| Definitions | config/agents/** | workflows, prompts, schemas, connectors, tools, roles, policies |
| Tenant override | config/agents/tenants/{slug}.yaml | one customer's differences |
Choose the environment with LOCO_ENV.
5.1 Environment references
Secrets never appear in YAML. Read them from the environment with this tag, in both layers:
api_key: '<%= get_env(name="VLLM_API_KEY", default="dev") %>'
- Quote the tag. Unquoted, an empty default such as
default=""becomes YAMLnull, and boot fails withinvalid type: null, expected a string. - A variable that is unset and has no default stops boot and names the variable.
- Nothing else is rendered:
{{ session_id }}in a workflow is a runtime placeholder and stays as written.
5.2 The settings: block
settings:
definitions_dir: config/agents # where the definitions live
documents_dir: storage/documents # uploads: <dir>/<tenant id>/<sha256>
llm:
providers: # referenced by models.yaml `provider`
vllm_chat:
kind: openai_compatible # or `fixture`
base_url: '<%= get_env(name="VLLM_CHAT_URL", default="http://localhost:8000") %>'
api_key: '<%= get_env(name="VLLM_API_KEY", default="dev") %>'
timeout_sec: 120
max_concurrency: 8
options: {} # fixture only: `extract: {…}` is what it returns
documents:
parsers: # referenced by document.parse `parser`
docling:
kind: docling_serve # or `plain_text`
base_url: '<%= get_env(name="DOCLING_URL", default="http://localhost:5001") %>'
api_key: '<%= get_env(name="DOCLING_API_KEY", default="") %>'
options: { do_ocr: true, table_mode: accurate, do_picture_classification: false, do_formula_enrichment: false }
telemetry:
enabled: false # true installs OTLP export to otlp_endpoint
otlp_endpoint: http://localhost:4317
service_name: bo-app
export_prompt_bodies: false
agui:
protocol_version: "1.0"
legacy_interrupt_event: on_interrupt # event name sent to pre-1.0 AG-UI clients
sse_keepalive_sec: 15
5.3 The definition files
| File | Purpose |
|---|---|
models.yaml | The model catalogue: provider, name, revision, license, role (chat or embedding), dimensions (embeddings), params (temperature, max_tokens, enable_thinking). allowed_licenses lists what may boot. |
prompts/*.yaml | id, version, system, user (must contain {{ input }}), guards.fence_tag, guards.max_input_chars. |
schemas/*.yaml | JSON Schema written as YAML, named by id. Model output is validated against it. |
connectors/*.yaml | id, kind (accounting_stub or webhook), enabled, credentials, options, actions.<name>.{requires_approval, idempotent}. |
tools/*.yaml | Tools a future chat model may call: id, description, kind, with, input_schema, requires_approval. Validated at boot; not yet offered to a model. |
workflows/*.yaml | Workflows — see §6. |
rbac.yaml | roles.<name>.{inherits, can}. |
policies.yaml | rate_limits, pii.hash_fields, pii.redact_in_traces, retention.{parsed_markdown_days, audit_log_days}, audit.hash_chain. |
tenants/*.yaml | Per-tenant overrides — see §8. |
Of the policies, pii.hash_fields (applied at extraction) and
retention.parsed_markdown_days (applied by retention_purge) take effect today. Rate limits,
trace redaction and audit-log retention are declared for the parts not built yet.
Every file rejects keys it does not know. To get autocompletion and validation in your editor, map
schemas/*.schema.json to these files. Regenerate the schemas after changing the Rust structs:
LOCO_ENV=demo cargo run -p bo-app -- task schema_export
A test fails if the committed schemas fall out of date.