PR #2666 threaded explicit app_config through the lead-agent prompt and subagent task path, intentionally scoped to bug fixes. The following structural improvements were deferred and are tracked here for follow-up PRs.
Items are roughly ordered by impact/effort.
1. Push sandbox-fallback into is_host_bash_allowed
task_tool.py:105 and subagents/registry.py:157 both forward an optional config to is_host_bash_allowed with the same conditional pattern. The registry.py site was hardened in #2666 with a hasattr(app_config, "sandbox") guard so a SubagentsAppConfig falls back to ambient lookup, but task_tool.py:105 still has the original cfg if cfg is not None else <ambient> pattern and remains vulnerable to the same silent bash-disable bug if ever called with a SubagentsAppConfig.
Fix: teach is_host_bash_allowed itself to fall back to get_app_config() when the passed config lacks .sandbox, and collapse all call sites to a single is_host_bash_allowed(app_config).
Files:
backend/packages/harness/deerflow/sandbox/security.py
backend/packages/harness/deerflow/subagents/registry.py
backend/packages/harness/deerflow/tools/builtins/task_tool.py
2. Single source of truth for ACP agents
AppConfig.acp_agents (Pydantic field, added in #2666) and _acp_agents (module singleton populated by load_acp_config_from_dict) parse the same yaml dict independently. They are equivalent today, but the two paths will drift the moment load_acp_config_from_dict adds any transformation (e.g., $VAR env-variable interpolation already documented on ACPAgentConfig.env).
Fix: either (a) make AppConfig.from_file re-derive the field from the singleton after load_acp_config_from_dict, or (b) drop the singleton and route all reads through app_config.acp_agents.
Files:
backend/packages/harness/deerflow/config/app_config.py
backend/packages/harness/deerflow/config/acp_config.py
backend/packages/harness/deerflow/agents/lead_agent/prompt.py (_build_acp_section)
3. Type-narrow _resolve_subagents_app_config
Currently typed Any | None and uses getattr(app_config, "subagents", app_config) to accept either an AppConfig or a SubagentsAppConfig. End-to-end type flow is broken — any object with a .subagents attribute is silently accepted.
Fix: narrow to AppConfig | SubagentsAppConfig | None with explicit isinstance dispatch.
Files:
backend/packages/harness/deerflow/subagents/registry.py
4. Flatten conditional kwarg assembly in task_tool.py
task_tool.py builds available_tools_kwargs and executor_kwargs as dicts, then conditionally adds app_config only when non-None — five sites total (incl. _build_subagent_section in prompt.py). All target signatures (get_available_tools, get_subagent_config, is_host_bash_allowed, SubagentExecutor) already accept app_config=None, so this can be flattened to direct kwarg passing.
Files:
backend/packages/harness/deerflow/tools/builtins/task_tool.py
backend/packages/harness/deerflow/agents/lead_agent/prompt.py
5. Type the runtime context
runtime/runs/worker.py currently uses Runtime(context=cast(Any, runtime_ctx), store=store) — the cast escapes the type system. The shape is {thread_id: str, run_id: str, app_config?: AppConfig, agent_name?: str} plus arbitrary caller-passthrough keys.
Fix: introduce a DeerFlowRuntimeContext TypedDict, which also lets _get_runtime_app_config(runtime) in task_tool.py use static member access instead of isinstance(context, dict) + .get().
Files:
backend/packages/harness/deerflow/runtime/runs/worker.py
backend/packages/harness/deerflow/tools/builtins/task_tool.py
6. Verify removed try/except in _create_summarization_middleware
#2666 removed the surrounding try/except that previously logged + fell back to a default skills container path on get_app_config() failure. This is correct only if startup is guaranteed to have already loaded AppConfig successfully by the time this factory runs. If not, a config load failure now propagates instead of degrading.
Action: confirm the invariant holds; if not, restore the graceful degrade.
Files:
backend/packages/harness/deerflow/agents/lead_agent/agent.py
PR #2666 threaded explicit
app_configthrough the lead-agent prompt and subagent task path, intentionally scoped to bug fixes. The following structural improvements were deferred and are tracked here for follow-up PRs.Items are roughly ordered by impact/effort.
1. Push sandbox-fallback into
is_host_bash_allowedtask_tool.py:105andsubagents/registry.py:157both forward an optional config tois_host_bash_allowedwith the same conditional pattern. Theregistry.pysite was hardened in #2666 with ahasattr(app_config, "sandbox")guard so aSubagentsAppConfigfalls back to ambient lookup, buttask_tool.py:105still has the originalcfg if cfg is not None else <ambient>pattern and remains vulnerable to the same silent bash-disable bug if ever called with aSubagentsAppConfig.Fix: teach
is_host_bash_alloweditself to fall back toget_app_config()when the passed config lacks.sandbox, and collapse all call sites to a singleis_host_bash_allowed(app_config).Files:
backend/packages/harness/deerflow/sandbox/security.pybackend/packages/harness/deerflow/subagents/registry.pybackend/packages/harness/deerflow/tools/builtins/task_tool.py2. Single source of truth for ACP agents
AppConfig.acp_agents(Pydantic field, added in #2666) and_acp_agents(module singleton populated byload_acp_config_from_dict) parse the same yaml dict independently. They are equivalent today, but the two paths will drift the momentload_acp_config_from_dictadds any transformation (e.g.,$VARenv-variable interpolation already documented onACPAgentConfig.env).Fix: either (a) make
AppConfig.from_filere-derive the field from the singleton afterload_acp_config_from_dict, or (b) drop the singleton and route all reads throughapp_config.acp_agents.Files:
backend/packages/harness/deerflow/config/app_config.pybackend/packages/harness/deerflow/config/acp_config.pybackend/packages/harness/deerflow/agents/lead_agent/prompt.py(_build_acp_section)3. Type-narrow
_resolve_subagents_app_configCurrently typed
Any | Noneand usesgetattr(app_config, "subagents", app_config)to accept either anAppConfigor aSubagentsAppConfig. End-to-end type flow is broken — any object with a.subagentsattribute is silently accepted.Fix: narrow to
AppConfig | SubagentsAppConfig | Nonewith explicitisinstancedispatch.Files:
backend/packages/harness/deerflow/subagents/registry.py4. Flatten conditional kwarg assembly in
task_tool.pytask_tool.pybuildsavailable_tools_kwargsandexecutor_kwargsas dicts, then conditionally addsapp_configonly when non-None — five sites total (incl._build_subagent_sectioninprompt.py). All target signatures (get_available_tools,get_subagent_config,is_host_bash_allowed,SubagentExecutor) already acceptapp_config=None, so this can be flattened to direct kwarg passing.Files:
backend/packages/harness/deerflow/tools/builtins/task_tool.pybackend/packages/harness/deerflow/agents/lead_agent/prompt.py5. Type the runtime context
runtime/runs/worker.pycurrently usesRuntime(context=cast(Any, runtime_ctx), store=store)— the cast escapes the type system. The shape is{thread_id: str, run_id: str, app_config?: AppConfig, agent_name?: str}plus arbitrary caller-passthrough keys.Fix: introduce a
DeerFlowRuntimeContextTypedDict, which also lets_get_runtime_app_config(runtime)intask_tool.pyuse static member access instead ofisinstance(context, dict) + .get().Files:
backend/packages/harness/deerflow/runtime/runs/worker.pybackend/packages/harness/deerflow/tools/builtins/task_tool.py6. Verify removed try/except in
_create_summarization_middleware#2666 removed the surrounding
try/exceptthat previously logged + fell back to a default skills container path onget_app_config()failure. This is correct only if startup is guaranteed to have already loadedAppConfigsuccessfully by the time this factory runs. If not, a config load failure now propagates instead of degrading.Action: confirm the invariant holds; if not, restore the graceful degrade.
Files:
backend/packages/harness/deerflow/agents/lead_agent/agent.py