Reference

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:

  1. CLI flag (e.g. --database-url)
  2. Environment variable (DUCTOR_*)
  3. Inline YAML in DUCTOR_CONFIG, or a config file
  4. 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_MODE

router 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-config

Core & logging

YAML keyEnv varTypeDefaultControls
environmentDUCTOR_ENVIRONMENTstringdevelopmentDeployment environment; development enables .env loading
security_profileDUCTOR_SECURITY_PROFILEstring``Named posture; enterprise forces fail-closed controls
(inline)DUCTOR_CONFIGYAML stringInline YAML config, overrides the file
(loader)DUCTOR_LOAD_DOTENVboolForce .env loading outside development
log.levelDUCTOR_LOG_LEVELstringinfoLog level (debug/info/warn/error)
log.formatDUCTOR_LOG_FORMATstringjsonjson or text
log.add_sourceDUCTOR_LOG_ADD_SOURCEboolfalseInclude source file/line in logs

Server

YAML keyEnv varTypeDefaultControls
server.http_addrDUCTOR_SERVER_HTTP_ADDRstring:8080Public HTTP listener (REST + Connect)
server.metrics_addrDUCTOR_SERVER_METRICS_ADDRstring:9090Prometheus metrics listener
server.read_header_timeoutDUCTOR_SERVER_READ_HEADER_TIMEOUTduration10sHTTP read-header timeout
server.read_timeoutDUCTOR_SERVER_READ_TIMEOUTduration30sHTTP read timeout
server.write_timeoutDUCTOR_SERVER_WRITE_TIMEOUTduration60sHTTP write timeout
server.idle_timeoutDUCTOR_SERVER_IDLE_TIMEOUTduration120sHTTP idle timeout
server.shutdown_drain_delayDUCTOR_SERVER_SHUTDOWN_DRAIN_DELAYduration5sGraceful-shutdown drain delay
server.roleDUCTOR_SERVER_ROLEstringallComma 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 keyEnv varTypeDefaultControls
database.urlDUCTOR_DATABASE_URLstringPostgres connection URL (required)
database.auto_migrateDUCTOR_DATABASE_AUTO_MIGRATEboolfalseRun migrations at startup
database.statement_timeoutDUCTOR_DATABASE_STATEMENT_TIMEOUTduration30sPer-statement timeout (0 = off)
database.idle_in_transaction_session_timeoutDUCTOR_DATABASE_IDLE_IN_TRANSACTION_SESSION_TIMEOUTduration60sIdle-in-transaction timeout
database.pool.max_connsDUCTOR_DATABASE_POOL_MAX_CONNSint25Pool max connections
database.pool.min_connsDUCTOR_DATABASE_POOL_MIN_CONNSint5Pool min connections
database.pool.max_conn_lifetimeDUCTOR_DATABASE_POOL_MAX_CONN_LIFETIMEduration5mMax connection lifetime
database.pool.max_conn_idle_timeDUCTOR_DATABASE_POOL_MAX_CONN_IDLE_TIMEduration5mMax connection idle time
database.pool.health_check_periodDUCTOR_DATABASE_POOL_HEALTH_CHECK_PERIODduration30sIdle-conn health-check cadence

Cache (Redis / Dragonfly)

YAML keyEnv varTypeDefaultControls
cache.urlDUCTOR_CACHE_URLstringRedis / Dragonfly URL (required)
cache.force_single_clientDUCTOR_CACHE_FORCE_SINGLE_CLIENTbooltrueForce single-node client mode

Authentication & authorization

YAML keyEnv varTypeDefaultControls
auth.enabledDUCTOR_AUTH_ENABLEDbooltrueEnable authentication
auth.allow_anonymousDUCTOR_AUTH_ALLOW_ANONYMOUSboolfalseAllow anonymous requests
auth.api_key_enabledDUCTOR_AUTH_API_KEY_ENABLEDboolEnable DB-backed API-key auth
auth.api_keysDUCTOR_AUTH_API_KEYSstring``Static (comma-separated) API keys
auth.service_token_secretDUCTOR_AUTH_SERVICE_TOKEN_SECRETstringHS256 secret for internal service tokens
auth.audience_resolverDUCTOR_AUTH_AUDIENCE_RESOLVERstringJWT audience mode ("" / host)
authz.allow_when_unconfiguredDUCTOR_AUTHZ_ALLOW_WHEN_UNCONFIGUREDbooltrueFail-open when no authorizer is wired

API (Connect/gRPC + OIDC)

YAML keyEnv varTypeDefaultControls
api.enabledDUCTOR_API_ENABLEDbooltrueEnable the Connect/gRPC API listener
api.addrDUCTOR_API_ADDRstring:50052API listener address
api.oidc_issuerDUCTOR_API_OIDC_ISSUERstring``OIDC issuer URL
api.oidc_audienceDUCTOR_API_OIDC_AUDIENCEstring``OIDC audience
api.max_request_body_sizeDUCTOR_API_MAX_REQUEST_BODY_SIZEint644194304 (4 MiB)Max request body
api.allowed_hostsDUCTOR_API_ALLOWED_HOSTS[]string[localhost,127.0.0.1,::1,0.0.0.0]Allowed Host headers
api.max_concurrent_requestsDUCTOR_API_MAX_CONCURRENT_REQUESTSint0 (unlimited)Global concurrency cap
api.max_concurrent_requests_per_tenantDUCTOR_API_MAX_CONCURRENT_REQUESTS_PER_TENANTint0 (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 keyEnv varTypeDefaultControls
routing.pipeline_modeDUCTOR_ROUTING_PIPELINE_MODEstringdag (compile default)Global pipeline: linear/dag/shadow/inherit
routing.rule_cache_sizeDUCTOR_ROUTING_RULE_CACHE_SIZEint1000Rule cache size
routing.rule_cache_ttlDUCTOR_ROUTING_RULE_CACHE_TTLduration30sRule cache TTL
routing.capacity_reservation_ttlDUCTOR_ROUTING_CAPACITY_RESERVATION_TTLduration1hCapacity reservation safety TTL
routing.default_timeout_msDUCTOR_ROUTING_DEFAULT_TIMEOUT_MSint5000 (effective)Routing default timeout
routing.max_routing_depthDUCTOR_ROUTING_MAX_ROUTING_DEPTHint3 (effective)Max routing recursion depth
routing.dry_run_defaultDUCTOR_ROUTING_DRY_RUN_DEFAULTboolfalseDefault dry-run
routing.persist_explainDUCTOR_ROUTING_PERSIST_EXPLAINboolfalsePersist explain traces
routing.bundle_fileDUCTOR_ROUTING_BUNDLE_FILEstringExternal routing bundle YAML path
routing.bundle_tenantDUCTOR_ROUTING_BUNDLE_TENANTstringTenant for bundle apply (required with bundle_file)
routing.strategy_deployments.timeoutDUCTOR_ROUTING_STRATEGY_DEPLOYMENTS_TIMEOUTduration5sRemote strategy call timeout
routing.strategy_deployments.fail_policyDUCTOR_ROUTING_STRATEGY_DEPLOYMENTS_FAIL_POLICYstringclosedFail open/closed on remote strategy error
routing.strategy_deployments.credentials.<ref>.token_envDUCTOR_ROUTING_STRATEGY_DEPLOYMENTS_CREDENTIALS_<REF>_TOKEN_ENVstringName of the env var holding a bearer token for auth_ref: <ref>
routing.strategy_deployments.credentials.<ref>.token_fileDUCTOR_ROUTING_STRATEGY_DEPLOYMENTS_CREDENTIALS_<REF>_TOKEN_FILEstringFile path holding a bearer token (alternative to token_env)
routing.strategy_deployments.credentials.<ref>.tls_ca_fileDUCTOR_ROUTING_STRATEGY_DEPLOYMENTS_CREDENTIALS_<REF>_TLS_CA_FILEstringPEM CA bundle that verifies the remote strategy server cert (TLS ≥ 1.2)
routing.strategy_deployments.credentials.<ref>.tls_cert_fileDUCTOR_ROUTING_STRATEGY_DEPLOYMENTS_CREDENTIALS_<REF>_TLS_CERT_FILEstringClient certificate for mTLS (must be set with tls_key_file)
routing.strategy_deployments.credentials.<ref>.tls_key_fileDUCTOR_ROUTING_STRATEGY_DEPLOYMENTS_CREDENTIALS_<REF>_TLS_KEY_FILEstringClient key for mTLS (must be set with tls_cert_file)
routing.strategy_deployments.credentials.<ref>.tls_server_nameDUCTOR_ROUTING_STRATEGY_DEPLOYMENTS_CREDENTIALS_<REF>_TLS_SERVER_NAMEstringOverrides 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 keyEnv varTypeDefaultControls
router.queue.enabledDUCTOR_ROUTER_QUEUE_ENABLEDboolfalseEnable the tiered queue
router.queue.concurrencyDUCTOR_ROUTER_QUEUE_CONCURRENCYint50Global concurrency (0 = unlimited)
router.queue.tenant_concurrencyDUCTOR_ROUTER_QUEUE_TENANT_CONCURRENCYint200Per-tenant concurrency
router.queue.workersDUCTOR_ROUTER_QUEUE_WORKERSint10Worker count
router.queue.visibility_timeoutDUCTOR_ROUTER_QUEUE_VISIBILITY_TIMEOUTduration30sItem visibility timeout
router.queue.retry_delayDUCTOR_ROUTER_QUEUE_RETRY_DELAYduration5sRetry delay
router.queue.max_scavenge_recoveriesDUCTOR_ROUTER_QUEUE_MAX_SCAVENGE_RECOVERIESint3Recoveries before DLQ
router.flow.concurrency.limitDUCTOR_ROUTER_FLOW_CONCURRENCY_LIMITint100Flow concurrency limit
router.flow.adaptive.enabledDUCTOR_ROUTER_FLOW_ADAPTIVE_ENABLEDboolfalseAdaptive flow control

Workflow runtime (workflow_runtime.*)

YAML keyEnv varTypeDefaultControls
workflow_runtime.enabledDUCTOR_WORKFLOW_RUNTIME_ENABLEDbooltrueEnable the DAG runtime
workflow_runtime.poll_intervalDUCTOR_WORKFLOW_RUNTIME_POLL_INTERVALduration5sCoordinator poll interval
workflow_runtime.batch_sizeDUCTOR_WORKFLOW_RUNTIME_BATCH_SIZEint50Wakeup batch size
workflow_runtime.coordinator_tick_timeoutDUCTOR_WORKFLOW_RUNTIME_COORDINATOR_TICK_TIMEOUTduration30sPer-tick timeout
workflow_runtime.recovery_stale_afterDUCTOR_WORKFLOW_RUNTIME_RECOVERY_STALE_AFTERduration30sStale-run recovery threshold
workflow_runtime.continue_as_new_after_historyDUCTOR_WORKFLOW_RUNTIME_CONTINUE_AS_NEW_AFTER_HISTORYint1000ContinueAsNew after N history events
workflow_runtime.continue_as_new_after_wakeupsDUCTOR_WORKFLOW_RUNTIME_CONTINUE_AS_NEW_AFTER_WAKEUPSint500ContinueAsNew after N wakeups
workflow_runtime.max_nodes_per_runDUCTOR_WORKFLOW_RUNTIME_MAX_NODES_PER_RUNint10000Hard node cap per run
workflow_runtime.max_step_output_sizeDUCTOR_WORKFLOW_RUNTIME_MAX_STEP_OUTPUT_SIZEint64104857600 (100 MiB)Step output cap
workflow_runtime.max_run_state_sizeDUCTOR_WORKFLOW_RUNTIME_MAX_RUN_STATE_SIZEint64268435456 (256 MiB)Run-state cap
workflow_runtime.state_checksum_modeDUCTOR_WORKFLOW_RUNTIME_STATE_CHECKSUM_MODEstringauditoff/audit/fail

Run-mode concurrency is an opt-in nested block:

YAML keyTypeDefaultControls
workflow_runtime.run_concurrency.enabledboolfalseAcquire and renew a run-level concurrency lease before advancing work.
workflow_runtime.run_concurrency.lease_ttlduration30mOwnership horizon; must be at least 3s.
workflow_runtime.run_concurrency.resume_backoffduration30sRetry cadence when a parked run cannot reacquire a slot.
workflow_runtime.run_concurrency.early_release.enabledboolfalseRelease the slot while waiting on an event, approval, or digest.
workflow_runtime.run_concurrency.reclaimer.enabledboolfalseEnable leader-elected expired/orphan lease cleanup.
workflow_runtime.run_concurrency.reclaimer.intervalduration30sReclaimer sweep cadence.
workflow_runtime.run_concurrency.reclaimer.key_patternstring{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 keyEnv varTypeDefaultControls
events.enabledDUCTOR_EVENTS_ENABLEDboolfalseEnable Redis event streams
events.stream_prefixDUCTOR_EVENTS_STREAM_PREFIXstring(Ductor prefix)Stream key prefix
tracing.enabledDUCTOR_TRACING_ENABLEDboolfalseEnable OTLP tracing
tracing.endpointDUCTOR_TRACING_ENDPOINTstringhttp://victoriatraces:10428/insert/opentelemetry/v1/tracesOTLP endpoint
tracing.backendDUCTOR_TRACING_BACKENDstringvictoriatracesBackend hint
tracing.sample_rateDUCTOR_TRACING_SAMPLE_RATEfloat640.1Head-sampling fraction
tracing.grpcDUCTOR_TRACING_GRPCboolfalsegRPC exporter instead of HTTP
tracing.insecureDUCTOR_TRACING_INSECUREbooltrueDisable TLS on the exporter

Connector

YAML keyEnv varTypeDefaultControls
connector.encryption_keyDUCTOR_CONNECTOR_ENCRYPTION_KEYstringBase64 32-byte AEAD master key (required for connections)
connector.encryption_key_idDUCTOR_CONNECTOR_ENCRYPTION_KEY_IDuint80Active wire key_id
connector.rotation_keys(list)listDecrypt-only rotation keyring
connector.action_rate_limit.enabledDUCTOR_CONNECTOR_ACTION_RATE_LIMIT_ENABLEDboolfalsePer-tenant action rate limiting
connector.tenant_provider_policy.enabledDUCTOR_CONNECTOR_TENANT_PROVIDER_POLICY_ENABLEDboolfalsePer-tenant provider policy
connector.connect_session.enabledDUCTOR_CONNECTOR_CONNECT_SESSION_ENABLEDboolfalseHosted OAuth connect-session flow
connector.connect_session.base_urlDUCTOR_CONNECTOR_CONNECT_SESSION_BASE_URLstring``Connect-link origin
connector.connection_recovery.enabledDUCTOR_CONNECTOR_CONNECTION_RECOVERY_ENABLEDboolfalseAuto reconnect/refresh recovery

Archival & retention

YAML keyEnv varTypeDefaultControls
archival.enabledDUCTOR_ARCHIVAL_ENABLEDboolfalseEnable archival
archival.backendDUCTOR_ARCHIVAL_BACKENDstringlocallocal / s3 / gcs
archival.retention_daysDUCTOR_ARCHIVAL_RETENTION_DAYSint30Retention days
archival.intervalDUCTOR_ARCHIVAL_INTERVALduration1hSweep interval
archival.storage_pathDUCTOR_ARCHIVAL_STORAGE_PATHstring./data/archiveLocal backend path
archival.s3_bucketDUCTOR_ARCHIVAL_S3_BUCKETstring``S3 bucket
archival.s3_regionDUCTOR_ARCHIVAL_S3_REGIONstringus-east-1S3 region
retention.enabledDUCTOR_RETENTION_ENABLEDboolfalseEnable the retention worker
retention.intervalDUCTOR_RETENTION_INTERVALduration24hSweep interval
retention.decisions_ttlDUCTOR_RETENTION_DECISIONS_TTLduration2160h (90d)Decisions TTL
retention.audit_logs_ttlDUCTOR_RETENTION_AUDIT_LOGS_TTLduration4320h (180d)Audit-logs TTL

Billing, entitlements & rate limits

YAML keyEnv varTypeDefaultControls
stripe.api_keySTRIPE_API_KEY (or DUCTOR_STRIPE_API_KEY)stringStripe API key
stripe.webhook_secretDUCTOR_STRIPE_WEBHOOK_SECRETstringStripe webhook signing secret
stripe.default_currencyDUCTOR_STRIPE_DEFAULT_CURRENCYstringusdDefault currency
entitlement.enforcementDUCTOR_ENTITLEMENT_ENFORCEMENTstringreportreport / off / strict
ratelimit.global_rpsDUCTOR_RATELIMIT_GLOBAL_RPSfloat6410000Global requests/sec
ratelimit.tenant_rpsDUCTOR_RATELIMIT_TENANT_RPSfloat641000Per-tenant requests/sec
ratelimit.fail_modeDUCTOR_RATELIMIT_FAIL_MODEstringopenBehavior on Redis outage

Identity (SCIM / SAML)

YAML keyEnv varTypeDefaultControls
identity.scim.enabledDUCTOR_IDENTITY_SCIM_ENABLEDboolfalseSCIM 2.0 provisioning
identity.scim.base_pathDUCTOR_IDENTITY_SCIM_BASE_PATHstring/scim/v2SCIM router base path
identity.saml.enabledDUCTOR_IDENTITY_SAML_ENABLEDboolfalseSAML SSO service provider
identity.saml.sp_entity_idDUCTOR_IDENTITY_SAML_SP_ENTITY_IDstringSP entity ID
identity.saml.acs_base_urlDUCTOR_IDENTITY_SAML_ACS_BASE_URLstringACS base URL (HTTPS)
identity.saml.session_ttlDUCTOR_IDENTITY_SAML_SESSION_TTLduration8hSAML 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 keyEnv varTypeDefaultControls
ai_inference.enabledDUCTOR_AI_INFERENCE_ENABLEDboolfalseEnable the AI inference proxy
ai_inference.strategyDUCTOR_AI_INFERENCE_STRATEGYstringrandom_weightedProvider-selection strategy across the pool
ai_inference.max_retriesDUCTOR_AI_INFERENCE_MAX_RETRIESint2Retries after the initial call (0 = one attempt, -1 = none)
ai_inference.retry_backoffDUCTOR_AI_INFERENCE_RETRY_BACKOFFduration500msBase exponential backoff between retries
ai_inference.connect_timeoutDUCTOR_AI_INFERENCE_CONNECT_TIMEOUTduration10sTCP connect timeout to a provider
ai_inference.response_timeoutDUCTOR_AI_INFERENCE_RESPONSE_TIMEOUTduration30sNon-streaming response timeout
ai_inference.stream_timeoutDUCTOR_AI_INFERENCE_STREAM_TIMEOUTduration10mStreaming response timeout
ai_inference.circuit_breaker.enabledDUCTOR_AI_INFERENCE_CIRCUIT_BREAKER_ENABLEDbool(module default)Per-provider fail-fast breaker
ai_inference.circuit_breaker.consecutive_failuresDUCTOR_AI_INFERENCE_CIRCUIT_BREAKER_CONSECUTIVE_FAILURESintFailures before the breaker opens
ai_inference.circuit_breaker.open_timeoutDUCTOR_AI_INFERENCE_CIRCUIT_BREAKER_OPEN_TIMEOUTdurationHow long the breaker stays open
ai_inference.circuit_breaker.reset_intervalDUCTOR_AI_INFERENCE_CIRCUIT_BREAKER_RESET_INTERVALdurationFailure-count reset window
ai_inference.circuit_breaker.half_open_max_requestsDUCTOR_AI_INFERENCE_CIRCUIT_BREAKER_HALF_OPEN_MAX_REQUESTSuint32Probe 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 keyEnv varTypeDefaultControls
ai.routing.max_cost_per_decision_usdDUCTOR_AI_ROUTING_MAX_COST_PER_DECISION_USDfloat640 (disabled)Hard cap on LLM spend per routing event
ai.routing.monthly_budget_usdDUCTOR_AI_ROUTING_MONTHLY_BUDGET_USDfloat640 (disabled)Monthly pool-level LLM spend cap
ai.routing.model_priorityDUCTOR_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 keyEnv varTypeDefaultControls
mcp.enabledDUCTOR_MCP_ENABLEDboolfalseMount the inbound MCP server
mcp.pathDUCTOR_MCP_PATHstring/mcpHTTP mount path
mcp.expose_unwired_toolsDUCTOR_MCP_EXPOSE_UNWIRED_TOOLSboolfalseAdvertise 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 keyEnv varTypeDefaultControls
aichat.enabledDUCTOR_AICHAT_ENABLEDboolfalseMount the chat agent endpoint
aichat.durable_runtimeDUCTOR_AICHAT_DURABLE_RUNTIMEboolfalseUse the canonical journal-backed runtime; required when chat is enabled
aichat.modelDUCTOR_AICHAT_MODELstring``Model id requested via the AI inference proxy; required when chat is enabled
aichat.max_iterationsDUCTOR_AICHAT_MAX_ITERATIONSint8LLM-call budget per user turn
aichat.max_tool_callsDUCTOR_AICHAT_MAX_TOOL_CALLSint16Tool-execution budget per user turn
aichat.approval_links_enabledDUCTOR_AICHAT_APPROVAL_LINKS_ENABLEDboolfalseMint encrypted, expiring browser approval links for external conversation bridges
aichat.approval_links_base_urlDUCTOR_AICHAT_APPROVAL_LINKS_BASE_URLURL``Externally reachable HTTPS origin or path prefix for /agent-approval; required when links are enabled
aichat.approval_link_ttlDUCTOR_AICHAT_APPROVAL_LINK_TTLduration15mApproval 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 keyEnv varTypeDefaultControls
analytics.enabledDUCTOR_ANALYTICS_ENABLEDboolfalseEnable the proxy + catalog endpoints
analytics.victoria_metrics_urlDUCTOR_ANALYTICS_VICTORIA_METRICS_URLstringhttp://localhost:8428Upstream VictoriaMetrics base URL
analytics.allow_unscoped_metricsDUCTOR_ANALYTICS_ALLOW_UNSCOPED_METRICSboolfalseAllow tenant queries without a real tenant_id label (enterprise rejects this)
analytics.max_query_rangeDUCTOR_ANALYTICS_MAX_QUERY_RANGEduration744h / 31d (built-in)Cap the start..end window of one query
analytics.min_stepDUCTOR_ANALYTICS_MIN_STEPduration5s (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 keyEnv varTypeDefaultControls
entitlement.enforcementDUCTOR_ENTITLEMENT_ENFORCEMENTstringreportreport (evaluate + admit, emit events) / off (legacy fail-open, deny-list only) / strict (fail closed on missing/stale facts)
entitlement.grace_unprovisionedDUCTOR_ENTITLEMENT_GRACE_UNPROVISIONEDbooltrueStrict 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 keyEnv varTypeDefaultControls
key_provider.enabledDUCTOR_KEY_PROVIDER_ENABLEDboolfalseEnable the BYOK boot path
key_provider.uriDUCTOR_KEY_PROVIDER_URIstringKEK backend URI: local://, awskms://, gcpkms://, vault://
key_provider.key_refDUCTOR_KEY_PROVIDER_KEY_REFstringBackend key reference (e.g. a KMS key ARN/alias); empty falls back to the key in the URI
key_provider.wrapped_masterDUCTOR_KEY_PROVIDER_WRAPPED_MASTERstringBase64 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 dynamicconfig registry — a code-generated, typed, per-key registry (regenerated via make dynamicconfig-generate) that backs the writable runtime surface exposed at /api/config/{schema,export}.
YAML keyEnv varTypeDefaultControls
dynamic_config.enabledDUCTOR_DYNAMIC_CONFIG_ENABLEDboolfalseEnable file-based hot reload
dynamic_config.config_fileDUCTOR_DYNAMIC_CONFIG_CONFIG_FILEstring``Path to the watched YAML file
dynamic_config.watch_intervalDUCTOR_DYNAMIC_CONFIG_WATCH_INTERVALduration30sChange-watch interval
dynamic_config.poll_intervalDUCTOR_DYNAMIC_CONFIG_POLL_INTERVALduration30sFile SHA poll interval
dynamic_config.propagation_latencyDUCTOR_DYNAMIC_CONFIG_PROPAGATION_LATENCYduration5sCross-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 keyEnv varTypeDefaultControls
notification.enabledDUCTOR_NOTIFICATION_ENABLEDboolfalseEnable the notification subscriber, preference, transform, and integration substrate
notification.dispatch.enabledDUCTOR_NOTIFICATION_DISPATCH_ENABLEDboolfalseAllow TriggerNotification to dispatch; disabled triggers fail with HTTP 412 before writing deliveries
notification.fanout.queue_disabledDUCTOR_NOTIFICATION_FANOUT_QUEUE_DISABLEDboolfalseDisable the durable topic queue; topic trigger admission then fails because no synchronous fallback exists
notification.digest.enabledDUCTOR_NOTIFICATION_DIGEST_ENABLEDboolfalseCoalesce channels carrying digest preferences; otherwise sends remain immediate
notification.channel_providersDUCTOR_NOTIFICATION_CHANNEL_PROVIDERSmap{}Ordered provider keys per channel for legacy provider selection
notification.defaultsDUCTOR_NOTIFICATION_DEFAULTSmap{}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.