Configuration
Every DUCTOR_* environment variable and YAML key, grouped by subsystem, with types and defaults.
Ductor is configured from a single typed Config struct, loaded once at
startup. Every field is settable three ways: a YAML file, an inline YAML blob,
or a DUCTOR_* environment variable. This page documents the loading model and
the most commonly-used keys, grouped by subsystem.
Large surface, one rule
Ductor exposes a very large configuration surface (hundreds of leaf keys across
many optional subsystems). This reference covers the keys you'll actually reach
for. Every leaf is bindable by the same mechanical rule below, so if you
see a YAML key in configs/ductor.yaml that isn't listed here, its env var is
derived exactly the same way.
How configuration loads
Configuration is resolved with Viper and
unmarshalled into internal/config/config.go. Precedence, highest to lowest:
- CLI flag (e.g.
--database-url) - Environment variable (
DUCTOR_*) - Inline YAML in
DUCTOR_CONFIG, or a config file - Registered defaults (from the code's
config.Default())
The env var rule
- Prefix is
DUCTOR_. - A YAML key path maps to an env var by uppercasing and replacing dots with underscores.
database.url → DUCTOR_DATABASE_URL
server.http_addr → DUCTOR_SERVER_HTTP_ADDR
router.queue.enabled → DUCTOR_ROUTER_QUEUE_ENABLED
tracing.pressure.mode → DUCTOR_TRACING_PRESSURE_MODErouter vs routing
Two prefixes look similar but are distinct. Queue and flow-control tuning lives
under router.* (DUCTOR_ROUTER_QUEUE_*, DUCTOR_ROUTER_FLOW_*), while
the routing engine's own knobs live under routing.*
(DUCTOR_ROUTING_*). This split is historical — mind the prefix.
Config file
If neither DUCTOR_CONFIG nor --config is set, Ductor searches for a
ductor.yaml in $HOME, the current directory, and /etc/ductor. Override with
--config <path>.
Inline YAML
DUCTOR_CONFIG may contain a full YAML document. When set, it is parsed directly
and takes precedence over any config file (the file is skipped). Invalid YAML
aborts startup. This is the recommended way to inject config in Kubernetes — the
Helm chart mounts it as DUCTOR_CONFIG from a Secret.
.env files
In development, a ./.env file is auto-loaded. It loads only when
DUCTOR_ENVIRONMENT=development or DUCTOR_LOAD_DOTENV=true. A missing .env
is ignored.
Validate what resolved
Use the CLI to print the effective config, its source, and DSN-redacted values:
ductor validate-configCore & logging
| YAML key | Env var | Type | Default | Controls |
|---|---|---|---|---|
environment | DUCTOR_ENVIRONMENT | string | development | Deployment environment; development enables .env loading |
security_profile | DUCTOR_SECURITY_PROFILE | string | `` | Named posture; enterprise forces fail-closed controls |
| (inline) | DUCTOR_CONFIG | YAML string | — | Inline YAML config, overrides the file |
| (loader) | DUCTOR_LOAD_DOTENV | bool | — | Force .env loading outside development |
log.level | DUCTOR_LOG_LEVEL | string | info | Log level (debug/info/warn/error) |
log.format | DUCTOR_LOG_FORMAT | string | json | json or text |
log.add_source | DUCTOR_LOG_ADD_SOURCE | bool | false | Include source file/line in logs |
Server
| YAML key | Env var | Type | Default | Controls |
|---|---|---|---|---|
server.http_addr | DUCTOR_SERVER_HTTP_ADDR | string | :8080 | Public HTTP listener (REST + Connect) |
server.metrics_addr | DUCTOR_SERVER_METRICS_ADDR | string | :9090 | Prometheus metrics listener |
server.read_header_timeout | DUCTOR_SERVER_READ_HEADER_TIMEOUT | duration | 10s | HTTP read-header timeout |
server.read_timeout | DUCTOR_SERVER_READ_TIMEOUT | duration | 30s | HTTP read timeout |
server.write_timeout | DUCTOR_SERVER_WRITE_TIMEOUT | duration | 60s | HTTP write timeout |
server.idle_timeout | DUCTOR_SERVER_IDLE_TIMEOUT | duration | 120s | HTTP idle timeout |
server.shutdown_drain_delay | DUCTOR_SERVER_SHUTDOWN_DRAIN_DELAY | duration | 5s | Graceful-shutdown drain delay |
server.role | DUCTOR_SERVER_ROLE | string | all | Comma role set: all/api/worker/executor |
The server.* struct also carries an internal transport plane
(server.internal_*) and an executor plane (server.exec_*) with their own
mTLS, runner-kind, and payload/timeout settings — see the executor flags in the
CLI reference.
Database
| YAML key | Env var | Type | Default | Controls |
|---|---|---|---|---|
database.url | DUCTOR_DATABASE_URL | string | — | Postgres connection URL (required) |
database.auto_migrate | DUCTOR_DATABASE_AUTO_MIGRATE | bool | false | Run migrations at startup |
database.statement_timeout | DUCTOR_DATABASE_STATEMENT_TIMEOUT | duration | 30s | Per-statement timeout (0 = off) |
database.idle_in_transaction_session_timeout | DUCTOR_DATABASE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT | duration | 60s | Idle-in-transaction timeout |
database.pool.max_conns | DUCTOR_DATABASE_POOL_MAX_CONNS | int | 25 | Pool max connections |
database.pool.min_conns | DUCTOR_DATABASE_POOL_MIN_CONNS | int | 5 | Pool min connections |
database.pool.max_conn_lifetime | DUCTOR_DATABASE_POOL_MAX_CONN_LIFETIME | duration | 5m | Max connection lifetime |
database.pool.max_conn_idle_time | DUCTOR_DATABASE_POOL_MAX_CONN_IDLE_TIME | duration | 5m | Max connection idle time |
database.pool.health_check_period | DUCTOR_DATABASE_POOL_HEALTH_CHECK_PERIOD | duration | 30s | Idle-conn health-check cadence |
Cache (Redis / Dragonfly)
| YAML key | Env var | Type | Default | Controls |
|---|---|---|---|---|
cache.url | DUCTOR_CACHE_URL | string | — | Redis / Dragonfly URL (required) |
cache.force_single_client | DUCTOR_CACHE_FORCE_SINGLE_CLIENT | bool | true | Force single-node client mode |
Authentication & authorization
| YAML key | Env var | Type | Default | Controls |
|---|---|---|---|---|
auth.enabled | DUCTOR_AUTH_ENABLED | bool | true | Enable authentication |
auth.allow_anonymous | DUCTOR_AUTH_ALLOW_ANONYMOUS | bool | false | Allow anonymous requests |
auth.api_key_enabled | DUCTOR_AUTH_API_KEY_ENABLED | bool | — | Enable DB-backed API-key auth |
auth.api_keys | DUCTOR_AUTH_API_KEYS | string | `` | Static (comma-separated) API keys |
auth.service_token_secret | DUCTOR_AUTH_SERVICE_TOKEN_SECRET | string | — | HS256 secret for internal service tokens |
auth.audience_resolver | DUCTOR_AUTH_AUDIENCE_RESOLVER | string | — | JWT audience mode ("" / host) |
authz.allow_when_unconfigured | DUCTOR_AUTHZ_ALLOW_WHEN_UNCONFIGURED | bool | true | Fail-open when no authorizer is wired |
API (Connect/gRPC + OIDC)
| YAML key | Env var | Type | Default | Controls |
|---|---|---|---|---|
api.enabled | DUCTOR_API_ENABLED | bool | true | Enable the Connect/gRPC API listener |
api.addr | DUCTOR_API_ADDR | string | :50052 | API listener address |
api.oidc_issuer | DUCTOR_API_OIDC_ISSUER | string | `` | OIDC issuer URL |
api.oidc_audience | DUCTOR_API_OIDC_AUDIENCE | string | `` | OIDC audience |
api.max_request_body_size | DUCTOR_API_MAX_REQUEST_BODY_SIZE | int64 | 4194304 (4 MiB) | Max request body |
api.allowed_hosts | DUCTOR_API_ALLOWED_HOSTS | []string | [localhost,127.0.0.1,::1,0.0.0.0] | Allowed Host headers |
api.max_concurrent_requests | DUCTOR_API_MAX_CONCURRENT_REQUESTS | int | 0 (unlimited) | Global concurrency cap |
api.max_concurrent_requests_per_tenant | DUCTOR_API_MAX_CONCURRENT_REQUESTS_PER_TENANT | int | 0 (unlimited) | Per-tenant concurrency cap |
Set allowed_hosts in production
api.allowed_hosts defaults to loopback plus 0.0.0.0 for local development.
In production, restrict it to the hostnames Ductor actually serves.
Routing engine (routing.*)
| YAML key | Env var | Type | Default | Controls |
|---|---|---|---|---|
routing.pipeline_mode | DUCTOR_ROUTING_PIPELINE_MODE | string | dag (compile default) | Global pipeline: linear/dag/shadow/inherit |
routing.rule_cache_size | DUCTOR_ROUTING_RULE_CACHE_SIZE | int | 1000 | Rule cache size |
routing.rule_cache_ttl | DUCTOR_ROUTING_RULE_CACHE_TTL | duration | 30s | Rule cache TTL |
routing.capacity_reservation_ttl | DUCTOR_ROUTING_CAPACITY_RESERVATION_TTL | duration | 1h | Capacity reservation safety TTL |
routing.default_timeout_ms | DUCTOR_ROUTING_DEFAULT_TIMEOUT_MS | int | 5000 (effective) | Routing default timeout |
routing.max_routing_depth | DUCTOR_ROUTING_MAX_ROUTING_DEPTH | int | 3 (effective) | Max routing recursion depth |
routing.dry_run_default | DUCTOR_ROUTING_DRY_RUN_DEFAULT | bool | false | Default dry-run |
routing.persist_explain | DUCTOR_ROUTING_PERSIST_EXPLAIN | bool | false | Persist explain traces |
routing.bundle_file | DUCTOR_ROUTING_BUNDLE_FILE | string | — | External routing bundle YAML path |
routing.bundle_tenant | DUCTOR_ROUTING_BUNDLE_TENANT | string | — | Tenant for bundle apply (required with bundle_file) |
routing.strategy_deployments.timeout | DUCTOR_ROUTING_STRATEGY_DEPLOYMENTS_TIMEOUT | duration | 5s | Remote strategy call timeout |
routing.strategy_deployments.fail_policy | DUCTOR_ROUTING_STRATEGY_DEPLOYMENTS_FAIL_POLICY | string | closed | Fail open/closed on remote strategy error |
routing.strategy_deployments.credentials.<ref>.token_env | DUCTOR_ROUTING_STRATEGY_DEPLOYMENTS_CREDENTIALS_<REF>_TOKEN_ENV | string | — | Name of the env var holding a bearer token for auth_ref: <ref> |
routing.strategy_deployments.credentials.<ref>.token_file | DUCTOR_ROUTING_STRATEGY_DEPLOYMENTS_CREDENTIALS_<REF>_TOKEN_FILE | string | — | File path holding a bearer token (alternative to token_env) |
routing.strategy_deployments.credentials.<ref>.tls_ca_file | DUCTOR_ROUTING_STRATEGY_DEPLOYMENTS_CREDENTIALS_<REF>_TLS_CA_FILE | string | — | PEM CA bundle that verifies the remote strategy server cert (TLS ≥ 1.2) |
routing.strategy_deployments.credentials.<ref>.tls_cert_file | DUCTOR_ROUTING_STRATEGY_DEPLOYMENTS_CREDENTIALS_<REF>_TLS_CERT_FILE | string | — | Client certificate for mTLS (must be set with tls_key_file) |
routing.strategy_deployments.credentials.<ref>.tls_key_file | DUCTOR_ROUTING_STRATEGY_DEPLOYMENTS_CREDENTIALS_<REF>_TLS_KEY_FILE | string | — | Client key for mTLS (must be set with tls_cert_file) |
routing.strategy_deployments.credentials.<ref>.tls_server_name | DUCTOR_ROUTING_STRATEGY_DEPLOYMENTS_CREDENTIALS_<REF>_TLS_SERVER_NAME | string | — | Overrides the SNI / certificate verification hostname |
The credentials map resolves the auth_ref/tls_ref names on a remote strategy
deployment into concrete transport security, and resolution fails closed — a
deployment whose ref points at a missing or incomplete entry never registers. Each
entry is keyed by an operator-chosen ref name (the <ref> above), so the map is
normally set in YAML or inline DUCTOR_CONFIG rather than per-leaf env vars; the
secret values themselves live in the env var named by token_env or the files the
other keys point at, never in the config document. An entry must resolve at least
one of token_env/token_file or TLS material, and tls_cert_file/tls_key_file
must be set together. See
Writing a custom strategy → Secure the transport.
routing.* has many more advanced sub-blocks (capacity_reconciliation,
pin_cache, market_controls, middleware, runtime.queue, runtime.flow,
dag_bridge). Consult internal/config/routing.go for the full set.
Queue & flow control (router.*)
| YAML key | Env var | Type | Default | Controls |
|---|---|---|---|---|
router.queue.enabled | DUCTOR_ROUTER_QUEUE_ENABLED | bool | false | Enable the tiered queue |
router.queue.concurrency | DUCTOR_ROUTER_QUEUE_CONCURRENCY | int | 50 | Global concurrency (0 = unlimited) |
router.queue.tenant_concurrency | DUCTOR_ROUTER_QUEUE_TENANT_CONCURRENCY | int | 200 | Per-tenant concurrency |
router.queue.workers | DUCTOR_ROUTER_QUEUE_WORKERS | int | 10 | Worker count |
router.queue.visibility_timeout | DUCTOR_ROUTER_QUEUE_VISIBILITY_TIMEOUT | duration | 30s | Item visibility timeout |
router.queue.retry_delay | DUCTOR_ROUTER_QUEUE_RETRY_DELAY | duration | 5s | Retry delay |
router.queue.max_scavenge_recoveries | DUCTOR_ROUTER_QUEUE_MAX_SCAVENGE_RECOVERIES | int | 3 | Recoveries before DLQ |
router.flow.concurrency.limit | DUCTOR_ROUTER_FLOW_CONCURRENCY_LIMIT | int | 100 | Flow concurrency limit |
router.flow.adaptive.enabled | DUCTOR_ROUTER_FLOW_ADAPTIVE_ENABLED | bool | false | Adaptive flow control |
Workflow runtime (workflow_runtime.*)
| YAML key | Env var | Type | Default | Controls |
|---|---|---|---|---|
workflow_runtime.enabled | DUCTOR_WORKFLOW_RUNTIME_ENABLED | bool | true | Enable the DAG runtime |
workflow_runtime.poll_interval | DUCTOR_WORKFLOW_RUNTIME_POLL_INTERVAL | duration | 5s | Coordinator poll interval |
workflow_runtime.batch_size | DUCTOR_WORKFLOW_RUNTIME_BATCH_SIZE | int | 50 | Wakeup batch size |
workflow_runtime.coordinator_tick_timeout | DUCTOR_WORKFLOW_RUNTIME_COORDINATOR_TICK_TIMEOUT | duration | 30s | Per-tick timeout |
workflow_runtime.recovery_stale_after | DUCTOR_WORKFLOW_RUNTIME_RECOVERY_STALE_AFTER | duration | 30s | Stale-run recovery threshold |
workflow_runtime.continue_as_new_after_history | DUCTOR_WORKFLOW_RUNTIME_CONTINUE_AS_NEW_AFTER_HISTORY | int | 1000 | ContinueAsNew after N history events |
workflow_runtime.continue_as_new_after_wakeups | DUCTOR_WORKFLOW_RUNTIME_CONTINUE_AS_NEW_AFTER_WAKEUPS | int | 500 | ContinueAsNew after N wakeups |
workflow_runtime.max_nodes_per_run | DUCTOR_WORKFLOW_RUNTIME_MAX_NODES_PER_RUN | int | 10000 | Hard node cap per run |
workflow_runtime.max_step_output_size | DUCTOR_WORKFLOW_RUNTIME_MAX_STEP_OUTPUT_SIZE | int64 | 104857600 (100 MiB) | Step output cap |
workflow_runtime.max_run_state_size | DUCTOR_WORKFLOW_RUNTIME_MAX_RUN_STATE_SIZE | int64 | 268435456 (256 MiB) | Run-state cap |
workflow_runtime.state_checksum_mode | DUCTOR_WORKFLOW_RUNTIME_STATE_CHECKSUM_MODE | string | audit | off/audit/fail |
Run-mode concurrency is an opt-in nested block:
| YAML key | Type | Default | Controls |
|---|---|---|---|
workflow_runtime.run_concurrency.enabled | bool | false | Acquire and renew a run-level concurrency lease before advancing work. |
workflow_runtime.run_concurrency.lease_ttl | duration | 30m | Ownership horizon; must be at least 3s. |
workflow_runtime.run_concurrency.resume_backoff | duration | 30s | Retry cadence when a parked run cannot reacquire a slot. |
workflow_runtime.run_concurrency.early_release.enabled | bool | false | Release the slot while waiting on an event, approval, or digest. |
workflow_runtime.run_concurrency.reclaimer.enabled | bool | false | Enable leader-elected expired/orphan lease cleanup. |
workflow_runtime.run_concurrency.reclaimer.interval | duration | 30s | Reclaimer sweep cadence. |
workflow_runtime.run_concurrency.reclaimer.key_pattern | string | {wf-conc}:conc:wf:* | Redis scan boundary for run leases. |
See Run-concurrency lease ownership for rollout and monitoring guidance.
The workflow runtime has an extensive set of optional feature blocks
(sharding.range_id_fencing, result_externalization,
scatter, stream, transition_compaction, defer, ops.*,
worker_compatibility, inline_run, bridge_*). See
internal/config/config_workflow.go.
Events & tracing
| YAML key | Env var | Type | Default | Controls |
|---|---|---|---|---|
events.enabled | DUCTOR_EVENTS_ENABLED | bool | false | Enable Redis event streams |
events.stream_prefix | DUCTOR_EVENTS_STREAM_PREFIX | string | (Ductor prefix) | Stream key prefix |
tracing.enabled | DUCTOR_TRACING_ENABLED | bool | false | Enable OTLP tracing |
tracing.endpoint | DUCTOR_TRACING_ENDPOINT | string | http://victoriatraces:10428/insert/opentelemetry/v1/traces | OTLP endpoint |
tracing.backend | DUCTOR_TRACING_BACKEND | string | victoriatraces | Backend hint |
tracing.sample_rate | DUCTOR_TRACING_SAMPLE_RATE | float64 | 0.1 | Head-sampling fraction |
tracing.grpc | DUCTOR_TRACING_GRPC | bool | false | gRPC exporter instead of HTTP |
tracing.insecure | DUCTOR_TRACING_INSECURE | bool | true | Disable TLS on the exporter |
Connector
| YAML key | Env var | Type | Default | Controls |
|---|---|---|---|---|
connector.encryption_key | DUCTOR_CONNECTOR_ENCRYPTION_KEY | string | — | Base64 32-byte AEAD master key (required for connections) |
connector.encryption_key_id | DUCTOR_CONNECTOR_ENCRYPTION_KEY_ID | uint8 | 0 | Active wire key_id |
connector.rotation_keys | (list) | list | — | Decrypt-only rotation keyring |
connector.action_rate_limit.enabled | DUCTOR_CONNECTOR_ACTION_RATE_LIMIT_ENABLED | bool | false | Per-tenant action rate limiting |
connector.tenant_provider_policy.enabled | DUCTOR_CONNECTOR_TENANT_PROVIDER_POLICY_ENABLED | bool | false | Per-tenant provider policy |
connector.connect_session.enabled | DUCTOR_CONNECTOR_CONNECT_SESSION_ENABLED | bool | false | Hosted OAuth connect-session flow |
connector.connect_session.base_url | DUCTOR_CONNECTOR_CONNECT_SESSION_BASE_URL | string | `` | Connect-link origin |
connector.connection_recovery.enabled | DUCTOR_CONNECTOR_CONNECTION_RECOVERY_ENABLED | bool | false | Auto reconnect/refresh recovery |
Archival & retention
| YAML key | Env var | Type | Default | Controls |
|---|---|---|---|---|
archival.enabled | DUCTOR_ARCHIVAL_ENABLED | bool | false | Enable archival |
archival.backend | DUCTOR_ARCHIVAL_BACKEND | string | local | local / s3 / gcs |
archival.retention_days | DUCTOR_ARCHIVAL_RETENTION_DAYS | int | 30 | Retention days |
archival.interval | DUCTOR_ARCHIVAL_INTERVAL | duration | 1h | Sweep interval |
archival.storage_path | DUCTOR_ARCHIVAL_STORAGE_PATH | string | ./data/archive | Local backend path |
archival.s3_bucket | DUCTOR_ARCHIVAL_S3_BUCKET | string | `` | S3 bucket |
archival.s3_region | DUCTOR_ARCHIVAL_S3_REGION | string | us-east-1 | S3 region |
retention.enabled | DUCTOR_RETENTION_ENABLED | bool | false | Enable the retention worker |
retention.interval | DUCTOR_RETENTION_INTERVAL | duration | 24h | Sweep interval |
retention.decisions_ttl | DUCTOR_RETENTION_DECISIONS_TTL | duration | 2160h (90d) | Decisions TTL |
retention.audit_logs_ttl | DUCTOR_RETENTION_AUDIT_LOGS_TTL | duration | 4320h (180d) | Audit-logs TTL |
Billing, entitlements & rate limits
| YAML key | Env var | Type | Default | Controls |
|---|---|---|---|---|
stripe.api_key | STRIPE_API_KEY (or DUCTOR_STRIPE_API_KEY) | string | — | Stripe API key |
stripe.webhook_secret | DUCTOR_STRIPE_WEBHOOK_SECRET | string | — | Stripe webhook signing secret |
stripe.default_currency | DUCTOR_STRIPE_DEFAULT_CURRENCY | string | usd | Default currency |
entitlement.enforcement | DUCTOR_ENTITLEMENT_ENFORCEMENT | string | report | report / off / strict |
ratelimit.global_rps | DUCTOR_RATELIMIT_GLOBAL_RPS | float64 | 10000 | Global requests/sec |
ratelimit.tenant_rps | DUCTOR_RATELIMIT_TENANT_RPS | float64 | 1000 | Per-tenant requests/sec |
ratelimit.fail_mode | DUCTOR_RATELIMIT_FAIL_MODE | string | open | Behavior on Redis outage |
Identity (SCIM / SAML)
| YAML key | Env var | Type | Default | Controls |
|---|---|---|---|---|
identity.scim.enabled | DUCTOR_IDENTITY_SCIM_ENABLED | bool | false | SCIM 2.0 provisioning |
identity.scim.base_path | DUCTOR_IDENTITY_SCIM_BASE_PATH | string | /scim/v2 | SCIM router base path |
identity.saml.enabled | DUCTOR_IDENTITY_SAML_ENABLED | bool | false | SAML SSO service provider |
identity.saml.sp_entity_id | DUCTOR_IDENTITY_SAML_SP_ENTITY_ID | string | — | SP entity ID |
identity.saml.acs_base_url | DUCTOR_IDENTITY_SAML_ACS_BASE_URL | string | — | ACS base URL (HTTPS) |
identity.saml.session_ttl | DUCTOR_IDENTITY_SAML_SESSION_TTL | duration | 8h | SAML session TTL |
AI inference (ai_inference.*)
The AI inference subsystem is the proxy every AI-assisted feature (routing strategies, the chat agent) calls through. It is off by default; enabling it wires a provider pool and a per-provider circuit breaker.
| YAML key | Env var | Type | Default | Controls |
|---|---|---|---|---|
ai_inference.enabled | DUCTOR_AI_INFERENCE_ENABLED | bool | false | Enable the AI inference proxy |
ai_inference.strategy | DUCTOR_AI_INFERENCE_STRATEGY | string | random_weighted | Provider-selection strategy across the pool |
ai_inference.max_retries | DUCTOR_AI_INFERENCE_MAX_RETRIES | int | 2 | Retries after the initial call (0 = one attempt, -1 = none) |
ai_inference.retry_backoff | DUCTOR_AI_INFERENCE_RETRY_BACKOFF | duration | 500ms | Base exponential backoff between retries |
ai_inference.connect_timeout | DUCTOR_AI_INFERENCE_CONNECT_TIMEOUT | duration | 10s | TCP connect timeout to a provider |
ai_inference.response_timeout | DUCTOR_AI_INFERENCE_RESPONSE_TIMEOUT | duration | 30s | Non-streaming response timeout |
ai_inference.stream_timeout | DUCTOR_AI_INFERENCE_STREAM_TIMEOUT | duration | 10m | Streaming response timeout |
ai_inference.circuit_breaker.enabled | DUCTOR_AI_INFERENCE_CIRCUIT_BREAKER_ENABLED | bool | (module default) | Per-provider fail-fast breaker |
ai_inference.circuit_breaker.consecutive_failures | DUCTOR_AI_INFERENCE_CIRCUIT_BREAKER_CONSECUTIVE_FAILURES | int | — | Failures before the breaker opens |
ai_inference.circuit_breaker.open_timeout | DUCTOR_AI_INFERENCE_CIRCUIT_BREAKER_OPEN_TIMEOUT | duration | — | How long the breaker stays open |
ai_inference.circuit_breaker.reset_interval | DUCTOR_AI_INFERENCE_CIRCUIT_BREAKER_RESET_INTERVAL | duration | — | Failure-count reset window |
ai_inference.circuit_breaker.half_open_max_requests | DUCTOR_AI_INFERENCE_CIRCUIT_BREAKER_HALF_OPEN_MAX_REQUESTS | uint32 | — | Probe requests allowed while half-open |
Providers (ai_inference.providers.<name>.*)
providers is a map keyed by an arbitrary provider name you choose (e.g.
primary, fallback). Each entry configures one upstream:
Prop
Type
Provider API keys never live in config
A provider's API key is not a config value. api_key_env names an
environment variable — Ductor reads the secret from that env var at runtime.
Never put the key itself in a YAML file or in DUCTOR_CONFIG. This keeps
provider secrets out of rendered config, validate-config output, and the
/api/config/export surface.
AI routing policy (ai.routing.*)
Server-level cost and model-selection policy for AI-assisted routing decisions.
Distinct from ai_inference.* (the transport) and from per-pool AI strategy
config (which is dynamic, pool-level config, not here).
| YAML key | Env var | Type | Default | Controls |
|---|---|---|---|---|
ai.routing.max_cost_per_decision_usd | DUCTOR_AI_ROUTING_MAX_COST_PER_DECISION_USD | float64 | 0 (disabled) | Hard cap on LLM spend per routing event |
ai.routing.monthly_budget_usd | DUCTOR_AI_ROUTING_MONTHLY_BUDGET_USD | float64 | 0 (disabled) | Monthly pool-level LLM spend cap |
ai.routing.model_priority | DUCTOR_AI_ROUTING_MODEL_PRIORITY | []string | [claude-haiku-4-5-20251001, claude-sonnet-4-6] | Cost/quality ladder, cheapest first |
Enterprise profile forces nonzero budgets
Under security_profile: enterprise, zero cost caps are rejected at load:
max_cost_per_decision_usd defaults to 1.00, monthly_budget_usd to
1000.00, and model_priority must be non-empty. In other profiles 0
means the guard is disabled.
MCP (mcp.*)
Ductor's inbound Model Context Protocol server (it also imports tools from external MCP servers — see the glossary).
| YAML key | Env var | Type | Default | Controls |
|---|---|---|---|---|
mcp.enabled | DUCTOR_MCP_ENABLED | bool | false | Mount the inbound MCP server |
mcp.path | DUCTOR_MCP_PATH | string | /mcp | HTTP mount path |
mcp.expose_unwired_tools | DUCTOR_MCP_EXPOSE_UNWIRED_TOOLS | bool | false | Advertise tools with no wired backend in tools/list |
expose_unwired_tools only affects discovery: tools/call still returns
ErrNotImplemented for an unwired tool regardless of the flag. Discovery is
filtered; execution is not.
AI chat agent (aichat.*)
Mounts the durable chat API: turn admission, session list/detail, resumable events,
input responses, turn cancellation, and feedback under /api/v1/chat. When a Slack
connector is bound for agent conversations, the same runtime also serves the signed
POST /agent-webhook/slack bridge. Approval links add GET and POST
/agent-approval.
| YAML key | Env var | Type | Default | Controls |
|---|---|---|---|---|
aichat.enabled | DUCTOR_AICHAT_ENABLED | bool | false | Mount the chat agent endpoint |
aichat.durable_runtime | DUCTOR_AICHAT_DURABLE_RUNTIME | bool | false | Use the canonical journal-backed runtime; required when chat is enabled |
aichat.model | DUCTOR_AICHAT_MODEL | string | `` | Model id requested via the AI inference proxy; required when chat is enabled |
aichat.max_iterations | DUCTOR_AICHAT_MAX_ITERATIONS | int | 8 | LLM-call budget per user turn |
aichat.max_tool_calls | DUCTOR_AICHAT_MAX_TOOL_CALLS | int | 16 | Tool-execution budget per user turn |
aichat.approval_links_enabled | DUCTOR_AICHAT_APPROVAL_LINKS_ENABLED | bool | false | Mint encrypted, expiring browser approval links for external conversation bridges |
aichat.approval_links_base_url | DUCTOR_AICHAT_APPROVAL_LINKS_BASE_URL | URL | `` | Externally reachable HTTPS origin or path prefix for /agent-approval; required when links are enabled |
aichat.approval_link_ttl | DUCTOR_AICHAT_APPROVAL_LINK_TTL | duration | 15m | Approval bearer-link lifetime; must be greater than zero and at most 24h |
aichat prerequisites
The chat agent needs aichat.durable_runtime=true, a model,
ai_inference.enabled with a matching provider, Postgres (journal and transcript
persistence), and a ready operational MCP backend (its tools). Configuration
validation fails at startup when these invariants are not met.
See External Conversation Bridges for Slack signature verification, participant bindings, ordered admission, same-thread replies, and single-use approval behavior.
Analytics (analytics.*)
A read-only proxy that lets the dashboard query Ductor's Prometheus metrics (stored in VictoriaMetrics) without the browser reaching VM directly. It validates PromQL against the K1 metric catalog and enforces tenant scoping.
| YAML key | Env var | Type | Default | Controls |
|---|---|---|---|---|
analytics.enabled | DUCTOR_ANALYTICS_ENABLED | bool | false | Enable the proxy + catalog endpoints |
analytics.victoria_metrics_url | DUCTOR_ANALYTICS_VICTORIA_METRICS_URL | string | http://localhost:8428 | Upstream VictoriaMetrics base URL |
analytics.allow_unscoped_metrics | DUCTOR_ANALYTICS_ALLOW_UNSCOPED_METRICS | bool | false | Allow tenant queries without a real tenant_id label (enterprise rejects this) |
analytics.max_query_range | DUCTOR_ANALYTICS_MAX_QUERY_RANGE | duration | 744h / 31d (built-in) | Cap the start..end window of one query |
analytics.min_step | DUCTOR_ANALYTICS_MIN_STEP | duration | 5s (built-in) | Smallest step resolution the proxy forwards |
Entitlement enforcement
Entitlement enforcement decides what happens when a tenant's plan/feature facts are missing, stale, or deny an action.
| YAML key | Env var | Type | Default | Controls |
|---|---|---|---|---|
entitlement.enforcement | DUCTOR_ENTITLEMENT_ENFORCEMENT | string | report | report (evaluate + admit, emit events) / off (legacy fail-open, deny-list only) / strict (fail closed on missing/stale facts) |
entitlement.grace_unprovisioned | DUCTOR_ENTITLEMENT_GRACE_UNPROVISIONED | bool | true | Strict mode only. Admit never-provisioned tenants under a loud bootstrap grace so flipping strict on doesn't instantly deny un-onboarded tenants |
grace_unprovisioned only softens the never-had-any-entitlement case. Genuine
gaps — a stale snapshot, a present-but-incomplete snapshot, or a tenant with
sources but no snapshot — still fail closed under strict.
Key provider / BYOK (key_provider.*)
Bring-Your-Own-Key boot path for the connector-credential master key. When
disabled (the default), the legacy connector.encryption_key path is used
unchanged. When enabled, uri dispatches to a registered KEK backend and
wrapped_master is unwrapped at boot into the 32-byte AEAD master.
| YAML key | Env var | Type | Default | Controls |
|---|---|---|---|---|
key_provider.enabled | DUCTOR_KEY_PROVIDER_ENABLED | bool | false | Enable the BYOK boot path |
key_provider.uri | DUCTOR_KEY_PROVIDER_URI | string | — | KEK backend URI: local://, awskms://, gcpkms://, vault:// |
key_provider.key_ref | DUCTOR_KEY_PROVIDER_KEY_REF | string | — | Backend key reference (e.g. a KMS key ARN/alias); empty falls back to the key in the URI |
key_provider.wrapped_master | DUCTOR_KEY_PROVIDER_WRAPPED_MASTER | string | — | Base64 wrapped 32-byte master key (for local://, the unwrapped key) |
Security profiles (security_profile)
security_profile (top-level, DUCTOR_SECURITY_PROFILE) applies a named posture
after static config is resolved. The enterprise profile forces fail-closed
controls — among them it requires key_provider.enabled, rejects a local
key provider, rejects analytics.allow_unscoped_metrics, and forces the nonzero
AI budget defaults described above. Config that would otherwise load happily is
rejected under this profile, so validate it explicitly with ductor validate-config before rollout.
Dynamic configuration
Ductor has two distinct dynamic-config systems — don't conflate them:
dynamic_config.*— a file-based hot-reload source. It watches a YAML file and re-reads on change so a subset of config can be updated without a restart.- The
dynamicconfigregistry — a code-generated, typed, per-key registry (regenerated viamake dynamicconfig-generate) that backs the writable runtime surface exposed at/api/config/{schema,export}.
| YAML key | Env var | Type | Default | Controls |
|---|---|---|---|---|
dynamic_config.enabled | DUCTOR_DYNAMIC_CONFIG_ENABLED | bool | false | Enable file-based hot reload |
dynamic_config.config_file | DUCTOR_DYNAMIC_CONFIG_CONFIG_FILE | string | `` | Path to the watched YAML file |
dynamic_config.watch_interval | DUCTOR_DYNAMIC_CONFIG_WATCH_INTERVAL | duration | 30s | Change-watch interval |
dynamic_config.poll_interval | DUCTOR_DYNAMIC_CONFIG_POLL_INTERVAL | duration | 30s | File SHA poll interval |
dynamic_config.propagation_latency | DUCTOR_DYNAMIC_CONFIG_PROPAGATION_LATENCY | duration | 5s | Cross-pod propagation target |
Notifications (notification.*)
Notification management and dispatch are separately gated. Subscriber, topic, integration, layout, and preference APIs require the notification substrate; provider sends additionally require dispatch. Topic sends require the durable fan-out queue and never fall back to an in-process loop.
| YAML key | Env var | Type | Default | Controls |
|---|---|---|---|---|
notification.enabled | DUCTOR_NOTIFICATION_ENABLED | bool | false | Enable the notification subscriber, preference, transform, and integration substrate |
notification.dispatch.enabled | DUCTOR_NOTIFICATION_DISPATCH_ENABLED | bool | false | Allow TriggerNotification to dispatch; disabled triggers fail with HTTP 412 before writing deliveries |
notification.fanout.queue_disabled | DUCTOR_NOTIFICATION_FANOUT_QUEUE_DISABLED | bool | false | Disable the durable topic queue; topic trigger admission then fails because no synchronous fallback exists |
notification.digest.enabled | DUCTOR_NOTIFICATION_DIGEST_ENABLED | bool | false | Coalesce channels carrying digest preferences; otherwise sends remain immediate |
notification.channel_providers | DUCTOR_NOTIFICATION_CHANNEL_PROVIDERS | map | {} | Ordered provider keys per channel for legacy provider selection |
notification.defaults | DUCTOR_NOTIFICATION_DEFAULTS | map | {} | Optional per-channel defaults such as sender addresses |
Durable fan-out is mandatory in enterprise mode
When security_profile=enterprise and notification dispatch is enabled,
configuration validation requires notification.fanout.queue_disabled=false.
In every profile, a topic trigger fails before acceptance if durable payload,
manifest, or queue storage is unavailable.
See Notification Platform for the send API and Delivery Reliability for payload limits, attempt semantics, observations, dead letters, and redrive.
Other subsystems
Ductor ships many more optional subsystems, each config-gated and off by default
unless noted: webhooks.*, inbox.*, realtime.*,
visibility.*, template.*, inbound_mail.*, dashboard.*, editor.*,
cases.*, tables.*, approval.*, multi_region.*, per_namespace.*,
bridge.*, egress.*, payload.*, audit.*, consent.*, fraud.*,
yield.*, marketplace.*, reliability.*, debug.*, tls.*, cors.*. Each
follows the same env-var rule; consult the corresponding file in
internal/config/ for defaults.
See the API surface reference for how these subsystems' endpoints are served and discovered.
tenant.enabled is special
DUCTOR_TENANT_ENABLED is a legacy multi-tenant flag read directly from Viper
rather than through the Config struct — it has no field on Config. Set it
as an env var when you need to toggle tenancy.