Configuration #

Every setting is an environment variable prefixed AGENT_, with __ separating a section from a field. .env.example is the complete list with defaults; this document explains the ones whose value is a decision rather than a number.

Settings are read once per process and cached. Unknown keys in a known section are rejected at startup — a mistyped variable used to be accepted, silently dropped, and the deployment ran on the default while its operator believed otherwise.

Environment #

AGENT_ENVIRONMENT is local, test, staging or production.

production is not a label. It turns on invariants that are checked before the first request, and a process that fails any of them refuses to start:

Invariant Why
security.require_api_key must be true An unauthenticated support API is an open account-lookup service
verification.enabled must be true It is the control that stops the agent asserting facts no tool returned
policy.require_identity_for_account_data must be true It is the identity boundary
observability.log_message_content must be false Message text is customer data
storage.echo_sql must be false Echoed statements carry their parameters
storage.database_url must not be SQLite Single-writer storage behind more than one process silently corrupts or blocks

Two more apply in every environment:

The failure names every problem at once, so a misconfigured deployment is fixed in one pass rather than one restart per mistake.

Authentication #

AGENT_SECURITY__REQUIRE_API_KEY=true
AGENT_SECURITY__API_KEYS=acme:s3cr3t,beta:0th3r:knowledge:read|order:read
AGENT_SECURITY__DEFAULT_TENANT=default

A key is written tenant:secret or tenant:secret:scope|scope. Clients present only the secret half; the tenant and scopes come from the credential.

Generate one with:

python -c "import secrets; print(secrets.token_urlsafe(32))"
Scope Grants
knowledge:read Search published articles
account:read Read the verified customer's record
order:read Read orders and evaluate eligibility
ticket:write Raise a ticket
refund:write Not used by any built-in tool. Reserved.
admin Reserved for administrative surfaces

A key with no scopes listed gets knowledge:read, account:read, order:read and ticket:write — deliberately not refund:write.

DEFAULT_TENANT is the tenant assigned when authentication is off, and it is what the CLI seeds and evaluates against. They have to agree, or a local instance serves an empty database while support-agent seed reports success.

A typo in a scope name raises at startup rather than granting nothing quietly.

The model #

AGENT_CHAT__BACKEND=template   # template | ollama | openai
Backend Needs Use
template nothing Default. Renders tool results and decisions directly. Cannot invent anything.
ollama a local Ollama Real generation with no credentials or per-token cost
openai an API key Any OpenAI-compatible endpoint — Azure, vLLM, LiteLLM, llama.cpp

The model is used for one thing: phrasing material that has already been decided. Its absence removes fluency and nothing else.

A provider failure falls back to template composition and marks the response degraded, so the degradation is visible rather than silently producing a worse answer.

Budgets #

AGENT_LIMITS__MAX_STEPS=12
AGENT_LIMITS__MAX_TOOL_CALLS=6
AGENT_LIMITS__MAX_TOOL_CALLS_PER_TOOL=3
AGENT_LIMITS__MAX_RUN_SECONDS=30
AGENT_LIMITS__TOOL_TIMEOUT_SECONDS=5

These are what make the agent bounded rather than a loop. Every one is enforced, and exhausting any of them escalates to a human rather than continuing.

max_tool_calls may not exceed max_steps; a plan that cannot fit in the step budget is a misconfiguration and is rejected at construction.

max_tool_calls_per_tool stops one tool consuming the whole budget.

Policy #

AGENT_POLICY__REFUND_WINDOW_DAYS=30
AGENT_POLICY__DIGITAL_REFUND_WINDOW_DAYS=14
AGENT_POLICY__REFUND_APPROVAL_THRESHOLD_MINOR_UNITS=50000
AGENT_POLICY__ALLOW_AGENT_INITIATED_REFUNDS=false

These are the product's promises to customers, expressed as configuration. The threshold is in minor units — 50000 means 500.00 — because storing money as a float is how rounding errors become refunds.

ALLOW_AGENT_INITIATED_REFUNDS is the most consequential setting in the file. It is false, no built-in tool issues a refund, and turning it on is not covered by the threat model.

Verification #

AGENT_VERIFICATION__ENABLED=true
AGENT_VERIFICATION__MIN_SUPPORTED_RATIO=0.8
AGENT_VERIFICATION__FORBID_UNSUPPORTED_NUMBERS=true
AGENT_VERIFICATION__ON_FAILURE=escalate   # escalate | strip | refuse

escalate hands the conversation to a person, which is the right default: the customer asked something answerable and the agent could not answer it safely. strip removes the unsupported sentences and sends the rest — useful when most of a reply is grounded. refuse declines outright.

Production refuses to start with verification disabled.

Injection handling #

AGENT_SECURITY__INJECTION_ACTION=neutralise   # annotate | neutralise | refuse
AGENT_SECURITY__INJECTION_REFUSE_THRESHOLD=0.85

neutralise replaces matched spans with an explicit marker and keeps the surrounding facts citable. refuse terminates the run when aggregate risk exceeds the threshold; that is what happens to a direct override in a customer message regardless of this setting, because the message is scanned before any tool runs.

The threshold is on noisy-OR aggregate risk, not on a single rule. A single high-severity match does not reach it — a document about prompt injection is a legitimate thing for a support article to be.

Observability #

AGENT_OBSERVABILITY__LOG_FORMAT=json       # json | console
AGENT_OBSERVABILITY__LOG_MESSAGE_CONTENT=false
AGENT_OBSERVABILITY__TRACING_ENABLED=false
AGENT_OBSERVABILITY__OTLP_ENDPOINT=http://localhost:4318

console is readable in a terminal; json is what an aggregator wants.

LOG_MESSAGE_CONTENT includes customer text in logs. It is false everywhere and production refuses to start with it on. Turn it on locally, briefly, to debug a classification — never anywhere a log is retained.

Tracing is off by default because an OTLP exporter that cannot reach a collector adds latency to every request for no benefit.

Storage #

AGENT_STORAGE__DATABASE_URL=sqlite+aiosqlite:///./var/agent.db
# AGENT_STORAGE__DATABASE_URL=postgresql+psycopg://agent:pass@host:5432/agent

SQLite runs with foreign_keys=ON and WAL. It is genuinely fine for one process, and genuinely wrong for more than one, which is why production refuses to start on it. PostgreSQL needs the extra:

uv sync --extra postgres

Precedence #

  1. Environment variables
  2. .env in the working directory
  3. Defaults in config.py

Unrecognised AGENT_* variables that do not name a known section are ignored, so an unrelated variable in a shared environment cannot stop the process starting. An unrecognised field inside a known section is rejected.