git:20261003.7b8a792 to git:20261003.9eb01b3
5 added, 1 removed. Audit A to A.
### Configuration System
Operator prompt overlays: `lead_prompt_overlay` on AppConfig and
`subagents.agents.<name>.prompt_overlay` accept literal `prepend`/`append` strings.
The per-assembly snapshot owns these settings; no run-context override exists.
DeerMem owns its separate `memory.backend_config.prompt_prepend`/`prompt_append`
fields so the memory package remains host-agnostic.
Custom Agent `AgentConfig.display_name` is an optional, whitespace-trimmed Unicode
label of at most 100 Unicode code points. C0/C1 controls and bidirectional
formatting controls (U+202A–U+202E, U+2066–U+2069) are rejected before trimming.
Also reject soft hyphen, Arabic letter mark, U+200B, U+200E–U+200F,
U+2028–U+2029, U+2060–U+2065 and U+FEFF. Labels consisting only of
marks, separators or other invisible characters are invalid; ZWNJ/ZWJ remain
supported inside ordinary text and emoji.
It is stored in the existing config document by
both agent stores; it never participates in paths, routing, or authorization.
Gateway create/update/response models share its validation. It remains outside
`MANAGED_AGENT_CONFIG_FIELDS` so `update_agent` preserves it. `setup_agent`
explicitly carries forward the existing owner's display name when re-bootstrapping;
the Gateway explicitly overrides it when supplied, including null to clear.
Both stores use `parse_agent_config` to ignore only an invalid stored
`display_name` on read, logging the agent identifier without the invalid value
and without rewriting storage. Other config errors still
raise, and API create/update validation remains strict.
**Main Configuration** (`config.yaml`):
- Setup: Copy `config.example.yaml` to `config.yaml` in the **project root** directory.
+ Setup: Copy root `config.example.yaml` to `config.yaml`. Startup callers use
+ `deerflow.env.load_selected_env_file()` for optional `DEER_FLOW_ENV_FILE` (cwd-relative,
+ strict readable file, process env wins). Reject a disabling `PYTHON_DOTENV_DISABLED`
+ when selection is explicit; otherwise retain the original `load_dotenv()`.
+ Keep this helper free of config imports so auth/debug cannot preload defaults.
**Config Versioning**: `config.example.yaml` has a `config_version` field. On startup, `AppConfig.from_file()` compares user version vs example version and emits a warning if outdated. Missing `config_version` = version 0. Run `make config-upgrade` to auto-merge missing fields. When changing the config schema, bump `config_version` in `config.example.yaml`.
The v46 upgrade treats an existing `tools[]` entry in the `knowledge` group as
pre-gate enablement and sets `knowledge_base.enabled: true` only when that flag
was absent. Explicit `true` or `false` values remain authoritative. Moving
legacy provider settings out of `knowledge_base` remains specific to the
RAGFlow `knowledge_search` tool; LightRAG and other knowledge providers keep
their tool-local settings unchanged.
Top-level `recursion_limit` and `max_recursion_limit` are hot-reloaded per Gateway run. The former supplies the default when a request omits or provides an invalid value; the latter caps both configured and client-provided budgets.
`tool_output` character/count limits and every `tool_overrides` value share
non-negative integer validation. Reject booleans before Pydantic coercion;
preserve numeric strings from environment substitution and explicit zero.
Zero per-tool overrides disable externalization only; a positive global
fallback budget still truncates oversized output. Regression tests exercise
YAML loading and both middleware tool-call paths in `test_tool_output_config_limits.py`.
**Config Caching**: `get_app_config()` caches the parsed config, but automatically reloads it when the resolved config path or file content signature changes. The signature includes file metadata and a content digest, so Gateway and LangGraph reads stay aligned with `config.yaml` edits even on object-store or network mounts where mtime can remain stale. The loader reads the file once through `file_signature.read_config_with_signature` and parses those same bytes, so the recorded signature always describes the parsed content: a write that races the load can only cause one extra reload, never a cache that holds one revision under another revision's signature (which the comparison could never detect).
**Config Hot-Reload Boundary**: Gateway dependencies route through `get_app_config()` on every request, so per-run fields like `models[*].max_tokens`, `summarization.*`, `title.*`, `memory.*`, `subagents.*`, `verification.*`, `tools[*]`, and the agent system prompt pick up `config.yaml` edits on the next message. `AppConfig` is intentionally **not** cached on `app.state` — `lifespan()` keeps a local `startup_config` variable for one-shot bootstrap work and passes it to `langgraph_runtime(app, startup_config)`.
Infrastructure fields are **restart-required**. The authoritative list lives in `packages/harness/deerflow/config/reload_boundary.py::STARTUP_ONLY_FIELDS` and is mirrored by the standardised `"startup-only:"` prefix on the corresponding `Field(description=...)` in `AppConfig` or an explicitly registered nested config model, so IDE hover on those fields surfaces the reason inline (no need to context-switch into this table). Currently registered: `plugins`, `database`, `checkpointer`, `run_events`, `agent_storage`, `stream_bridge`, `sandbox`, `skills.container_path`, `log_level`, `logging`, `channels`, `channel_connections`, `scheduler`, `mcp_tasks`, `subagent_runtime`, `subagent_batches`, `run_ownership`, `dedupe_storage`. Adding a new restart-required field requires updating the registry; drift is pinned by `tests/test_reload_boundary.py`. `scheduler.recursion_limit` is the exception inside that section: it is read from `get_app_config()` at each scheduled dispatch, so a YAML edit applies to the next run without restarting the poller.
**Persistence backend resolution**: the unified `database` section selects the
Gateway's LangGraph checkpointer, LangGraph Store, and DeerFlow SQL repositories.
The deprecated `checkpointer` section remains backward compatible and, when
present, overrides `database` for the LangGraph checkpointer and Store only;
application repositories continue to use `database`.
Configuration priority:
1. Explicit `config_path` argument
2. `DEER_FLOW_CONFIG_PATH` environment variable (a missing file is an error, not a fallthrough)
3. `config.yaml` under `DEER_FLOW_PROJECT_ROOT`, or the current directory when it is unset
4. Legacy `backend/config.yaml`, then repository-root `config.yaml` (project root is the **recommended location**)
`scripts/config-upgrade.sh` calls `AppConfig.resolve_config_path` rather than copying this order.
The legacy locations are anchored to the installed harness source, not to the caller's checkout.
`scripts/doctor.py` calls `AppConfig.resolve_config_path` rather than copying this order.
Config values starting with `$` are resolved as environment variables (e.g., `$OPENAI_API_KEY`).
`ModelConfig` also declares `use_responses_api` and `output_version` so OpenAI `/v1/responses` can be enabled explicitly while still using `langchain_openai:ChatOpenAI`.
`ModelConfig.request_admission` is optional and is not a provider parameter.
Its positive RPM, finite wait deadline, queue bound and optional quota-group name
configure process-local model pacing. `requests_per_minute` and `max_queue_size` are strict
integers (bools and floats are rejected) behind a `BeforeValidator` that converts a
decimal literal delivered as a string, because `$VAR` substitution always yields
`str`; any other string still fails validation. Models sharing an explicit group must use
identical policies. Restart after changing, disabling or regrouping an active
policy; conflicting policies fail construction rather than silently resetting
an active budget. This nested model option is enforced by its limiter registry,
not by the top-level infrastructure reload-boundary registry.
**Extensions Configuration** (`extensions_config.json`):
MCP servers and skills are configured together in `extensions_config.json` in project root:
Both the runtime loader and raw read-modify-write reader accept UTF-8 with or
without a leading BOM. Writes remain UTF-8 without a BOM.
Docker development mounts the project directory at `/app/project` and points
`DEER_FLOW_CONFIG_PATH` / `DEER_FLOW_EXTENSIONS_CONFIG_PATH` into that directory.
Keep mutable config files behind a directory bind mount: single-file bind mounts
can become stale or inaccessible when a host editor replaces a file on save.
Configuration priority:
1. Explicit `config_path` argument
2. `DEER_FLOW_EXTENSIONS_CONFIG_PATH` environment variable
3. `extensions_config.json` in current directory (backend/)
4. `extensions_config.json` in parent directory (project root - **recommended location**)
Extensions are optional only in the fallback *search* mode (priority 3-4 above): `ExtensionsConfig.resolve_config_path()` returns `None` when neither an explicit `config_path` nor `DEER_FLOW_EXTENSIONS_CONFIG_PATH` is given and the search locations find nothing. An explicit `config_path` argument or a set `DEER_FLOW_EXTENSIONS_CONFIG_PATH` (priority 1-2) is an operator assertion that one particular file must be used, so a missing file in either of those modes raises `FileNotFoundError` instead — including when the file existed earlier and has since been deleted. The MCP tools cache's staleness check (`deerflow.mcp.cache._resolve_config_path`) is a narrow, deliberate exception to that rule: it catches that `FileNotFoundError` locally and treats it as "unconfigured" so a previously-valid config disappearing mid-run degrades the cache to serving its last-known-good tools instead of raising out of a per-request hot path (see the MCP System section below).
### Config Schema
**`config.yaml`** key sections:
- `models[]` - LLM configs with `use` class path, `supports_thinking`, `supports_vision`, provider-specific fields. An optional `reasoning:` block (issue #5073) declares thinking availability (`unsupported|optional|required`), the accepted effort `values` with `aliases`/`default`/`path`, the payload `dialect`, and the reasoning `history` requirement; when present, `supports_thinking` / `supports_reasoning_effort` are derived from it and contradictory combinations fail validation in `ModelConfig`
A declared custom `reasoning.effort.path` is the only effort serialization path: `ModelConfig` rejects leftover `reasoning_effort` keys in the profile or thinking templates, even if their values are otherwise accepted.
A boolean or level-string `reasoning` (`true` / `false`, or `low|medium|high` for gpt-oss style models) remains the legacy native ChatOllama setting and is passed to the provider; only a mapping opts into the contract.
- `logging.enhance` - Log output only (`enabled`, `format`): whether log records carry a `trace_id` field, and in which format. Trace ids are issued unconditionally — the Gateway `X-Trace-Id` header and Langfuse `deerflow_trace_id` metadata are always present whatever this says (see the Request Trace Context section in `packages/harness/deerflow/AGENTS.md`); restart-required
- vLLM reasoning models should use `deerflow.models.vllm_provider:VllmChatModel`; for Qwen-style parsers prefer `when_thinking_enabled.extra_body.chat_template_kwargs.enable_thinking`, and DeerFlow will also normalize the older `thinking` alias
- `tools[]` - Tool configs with `use` variable path and `group`
- `tool_groups[]` - Logical groupings for tools
- `sandbox.use` - Sandbox provider class path
- `sandbox.ownership` - Cross-instance sandbox lease storage. Renewal interval
and TTL multiplier must each be finite, and their derived lease TTL must also
remain finite. Redis-backed lease TTLs must be at least one millisecond and
use at most half of Redis's signed 64-bit millisecond range, leaving
deterministic headroom for conversion from a relative TTL to an absolute Unix
timestamp during validation. The Redis store rounds fractional milliseconds
up so its integer TTL never shortens the validated lease.
- `skills.path` / `skills.container_path` - Host and container paths to skills directory. AIO and E2B snapshot the container path at provider startup. Their local/remote backends and the Kubernetes provisioner require one canonical absolute non-root path outside reserved platform mounts; custom roots participate in deterministic sandbox identity, and E2B records the root in remote metadata.
- `skills.deferred_discovery` - When `true`, replaces the full-metadata `<available_skills>` prompt block with a compact `<skill_index>` (names only) and registers the `describe_skill` tool so the agent fetches metadata on demand. Defaults to `false` (legacy full-metadata injection)
- `title` - Auto-title generation (enabled, max_words, max_chars, model_name; null model_name uses fast local fallback, explicit model_name uses the prompt_template LLM path)
- `summarization` - Context summarization (enabled, trigger conditions, keep policy)
- `subagents.enabled` - Master switch for subagent delegation
- `subagent_runtime` - Startup-only shared process admission (`max_running`, bounded async wait queue, queue/reject policy, and queue timeout) for ordinary and durable-batch native subagents
- `subagent_batches` - Startup-only explicit durable batch scheduler limits (disabled by default), including separate total, live, and running dimensions plus leases/retries/result bounds
- `memory` - Memory system (enabled, storage_path, debounce_seconds, shutdown_flush_timeout_seconds, model_name, max_facts, fact_confidence_threshold, injection_enabled, max_injection_tokens, staleness_review_enabled, staleness_age_days, staleness_min_candidates, staleness_max_removals_per_cycle, staleness_protected_categories, staleness_max_lifetime_multiplier, staleness_max_extension_days)
- `knowledge_base` - Hot-reloadable, provider-agnostic knowledge capability and custom-agent scope-selector flags. It gates the read-only Agent tools and selector; provider connection, allowlist, and retrieval defaults belong to the matching entry in `tools[]` (for example, the RAGFlow `knowledge_search` tool).
**`extensions_config.json`**:
- `mcpServers` - Map of server name → config (enabled, type, command, args, env, url, headers, oauth, description, `routing`, `tools`, `tool_call_timeout`, `session_init_timeout`). `routing.mode="prefer"` emits `<mcp_routing_hints>` prompt guidance; if `tool_search` defers the hinted tool, `McpRoutingMiddleware` can also auto-promote matching deferred schemas before the model call. It does not hard-disable other tools. `session_init_timeout` (default `DEFAULT_MCP_SESSION_INIT_TIMEOUT` = 60s, `null` to disable) bounds server bring-up: tool discovery and persistent stdio session initialization, so a hung server cannot block agent construction indefinitely; durable HTTP/SSE task calls use it for their ephemeral session initialization too. `tool_call_timeout` bounds individual stdio calls and durable-task calls on every transport; other HTTP/SSE tools use transport-level timeouts.
- `tool_search.auto_promote_top_k` - Global MCP routing auto-promote breadth. Default `3`, clamped to `1..5`; applies only when `tool_search.enabled=true` and only to deferred MCP tools with `routing.mode="prefer"` and non-empty keywords. For lead agents the deferred catalog is built from the full configured MCP set; auto-promotion never grants authority because an active skill's runtime policy still filters model-visible schemas, `tool_search` results, and execution.
- `skills` - Map of skill name → state (enabled)
- `middlewares` - `AgentMiddleware` entries for lead and subagent runtime extension: class-path strings or `{class, kwargs}` objects. `kwargs` values must be JSON types; YAML dates and timestamps are coerced to ISO strings so they match JSON. `config.yaml -> extensions` can override these fields after validation; overrides are replace-per-field, not list concatenation.
Gateway API endpoints and `DeerFlowClient` methods can modify MCP servers and skill state at runtime; their `extensions_config.json` writes use the shared atomic replacement helper, while `middlewares` remains an operator-controlled config-file extension point.
Values beginning with `$` are resolved from the environment when the file is loaded, and an unset variable becomes `""`. Runtime writers (MCP router, skill toggle, `DeerFlowClient`) therefore read the raw file with `read_raw_extensions_config`, merge into it (`set_raw_skill_enabled` for skill state), check the candidate with `validate_raw_extensions_config`, and write that raw dict. They never serialize an `ExtensionsConfig` model back to disk: its resolved values would persist secrets in plaintext and erase the references. When the file does not exist yet, the Gateway skill toggle seeds only the cached skill states. `tests/test_extensions_config_raw_writes.py` and the placeholder tests in `tests/test_client.py` pin this.
`AgentConfig.knowledge_scope` uses the versioned `KnowledgeScope` contract as a
Gateway new-turn default. The API preserves omitted updates and clears explicit
null; file and SQL stores persist it in the existing config document. Keep it
outside `MANAGED_AGENT_CONFIG_FIELDS` so harness self-updates preserve it;
`setup_agent` also preserves the binding when re-bootstrapping. Admission uses
explicit message scope before this default and snapshots it onto the current
human message. Resolve the executing agent with runtime context over configurable,
including legacy callers using `context.agent_name`; bootstrap skips defaults.
Recovery (including a legacy null scope) never reapplies defaults.
The operator allowlist is still checked by retrieval; this is not authorization.
Unscoped new runs persist pre-default request digests even for unbound agents.
Digest-free legacy retries compare pre-default canonical input; explicit scopes
and recovery are excluded. Retries preserve the original run across binding edits.
The file-backed singleton entrypoints additionally merge administrator-managed shared
models from the encrypted runtime-home catalog. YAML entries win name conflicts;
managed changes create new effective snapshots and do not alter an active runtime
or an explicitly injected AppConfig. See `../models/AGENTS.md` for storage and reload
boundaries. `managed_model_providers.py` derives provider defaults from validated
endpoints; `ManagedModel.runtime_config()` combines them with profile fields.
`AppConfig.from_file()` remains YAML-only.