diff --git a/backend/packages/harness/deerflow/agents/middlewares/AGENTS.md b/backend/packages/harness/deerflow/agents/middlewares/AGENTS.md index 9da25304d..bd1acd865 100644 --- a/backend/packages/harness/deerflow/agents/middlewares/AGENTS.md +++ b/backend/packages/harness/deerflow/agents/middlewares/AGENTS.md @@ -81,7 +81,7 @@ Before changing a later authorization phase, read the [authorization RFC](../../ **Lead-only middlewares** (`build_middlewares`, appended after the base): -14. **DynamicContextMiddleware** - Injects the current date (and optionally memory) as a `` into the first HumanMessage, keeping the base system prompt fully static for prefix-cache reuse +14. **DynamicContextMiddleware** - Injects the current date (and optionally memory) as a `` into the first HumanMessage, keeping the base system prompt fully static for prefix-cache reuse. The injected date follows the server-local timezone unless `DEER_FLOW_DATE_TIMEZONE` names an IANA zone (invalid values fall back to server-local). 15. **SkillActivationMiddleware** - Detects strict `/skill-name task` syntax on the latest real user message, resolves only enabled and runtime-allowed skills, injects the `SKILL.md` body as hidden current-turn context, and records a `middleware:skill_activation` audit event 16. **SkillToolPolicyMiddleware** - Applies `allowed-tools` only after real activation; passive enabled skills and a custom agent's configured skill allowlist do not clamp the lead toolset. A run-scoped slash activation is authoritative and suppresses `skill_context` as a policy source, so reading another skill cannot widen the explicit skill's tools; without slash activation, skills captured after configured `read_file` loads retain the existing union semantics. The middleware filters model-visible schemas and blocks unauthorized execution, resolving canonical paths against the live enabled/agent-allowed registry on every model call, then stores a versioned, JSON-safe, middleware-token-bound decision signed by policy source plus active paths in run context for the resulting tool calls to reuse. The next model call always refreshes it, and malformed, foreign, stale, or unmatched decisions fall back to live resolution. `tool_search` and `describe_skill` remain framework-safe discovery tools under a restrictive policy; they may reveal or promote metadata, but a deferred business tool must still be declared by the active policy before its schema or execution can survive the policy middleware. The decision's owner token is authorization-sensitive, so its reserved context key is owned by `runtime.secret_context` and included in `REDACTED_CONTEXT_KEYS` for observable and persisted context copies. Registry load failures and a non-empty active set with no authorized skill fail closed to framework-safe tools; an individual stale path is skipped only when at least one valid active skill remains. This is best-effort behavioral scoping rather than a hard security boundary: alternate loads such as `bash cat` are not captured, and bounded autonomous `skill_context` can evict old entries. `task` is not framework-exempt, so a restricted skill cannot delegate around its policy. The middleware must remain immediately after `SkillActivationMiddleware` (which publishes the slash source through `runtime.secret_context`'s public path helpers authenticated by a required token shared only within the assembled middleware chain) and immediately before `DurableContextMiddleware`; assembly and compiled-graph tests pin ordering, token sharing, schema filtering, and execution blocking. 17. **DurableContextMiddleware** - Captures `task` delegations into `ThreadState.delegations` (including in-progress dispatches and terminal result summaries) and loaded skill-file references (name/path/description, parsed in-memory - not the body) into `ThreadState.skill_context` before summarization can compact the paired tool-call/result messages, then projects durable context into each model request. Static authority rules are injected as a `SystemMessage`; untrusted field values (`summary_text`, delegation results, skill descriptions) are injected separately as a hidden `HumanMessage` data block so compressed history, delegated work, and which skills are active stay visible without being stored as `messages` or promoted to system-role instructions. `build_subagent_runtime_middlewares` also attaches this middleware immediately before subagent summarization so a compacted `summary_text` is projected ahead of a preserved assistant/tool tail instead of leaving strict providers with an assistant-first request. diff --git a/backend/packages/harness/deerflow/agents/middlewares/dynamic_context_middleware.py b/backend/packages/harness/deerflow/agents/middlewares/dynamic_context_middleware.py index 9cc98a7c5..b2effeae6 100644 --- a/backend/packages/harness/deerflow/agents/middlewares/dynamic_context_middleware.py +++ b/backend/packages/harness/deerflow/agents/middlewares/dynamic_context_middleware.py @@ -24,6 +24,18 @@ Date-update format: 2026-05-09, Saturday + +By default the injected date follows the server's local timezone. Set the +``DEER_FLOW_DATE_TIMEZONE`` environment variable to an IANA timezone name (for +example ``Asia/Shanghai``) when the host clock runs UTC but the conversation +date should follow another zone. Invalid values log a warning and fall back to +the server-local timezone. + +The knob is deliberately an environment variable rather than a config field: +it is read directly by both date-context middlewares at injection time, so an +operator can point a container at another zone without mounting a config.yaml, +and the lead and built-in-subagent paths can never drift apart on which zone +they render. """ from __future__ import annotations @@ -31,10 +43,13 @@ from __future__ import annotations import asyncio import hashlib import logging +import os +import posixpath import re import uuid -from datetime import datetime +from datetime import datetime, tzinfo from typing import TYPE_CHECKING, override +from zoneinfo import ZoneInfo, ZoneInfoNotFoundError from deerflow_extension_api import ContentKind, provenance_kwargs from langchain.agents.middleware import AgentMiddleware @@ -76,8 +91,101 @@ __all__ = [ ] +_DATE_TIMEZONE_ENV = "DEER_FLOW_DATE_TIMEZONE" + + +def _date_timezone() -> tzinfo | None: + """Resolve the configured IANA timezone for injected dates, or None for server-local.""" + raw = os.environ.get(_DATE_TIMEZONE_ENV, "").strip() + if not raw: + return None + try: + return ZoneInfo(raw) + except (ZoneInfoNotFoundError, ValueError, OSError): + # Only configuration-shaped failures degrade to server-local. A + # BlockingError-style guard (blocking-I/O regression suite) or any + # unrelated exception must propagate instead of being misread as an + # invalid timezone name. + logger.warning("Invalid %s=%r; falling back to the server-local timezone", _DATE_TIMEZONE_ENV, raw) + return None + + +def _server_local_timezone_name() -> str | None: + """IANA key of the server's local zone, or ``None`` when not resolvable. + + ``datetime.now().astimezone().tzinfo`` is always a plain fixed-offset + ``datetime.timezone`` (an abbreviation such as ``CST`` is ambiguous and + DST-churns), never a ``zoneinfo.ZoneInfo`` carrying an IANA key. The key is + instead read from the platform: the ``TZ`` environment variable when it + names a real zone, or the ``/etc/localtime`` symlink target on + Linux/macOS. Only the symlink's *direct* target is read (``os.readlink``), + not a fully resolved path: on macOS ``/etc/localtime`` points into + ``/var/db/timezone/zoneinfo/`` whose own directory symlink resolves to a + versioned path (``.../tz//zoneinfo/...``) that would defeat any + fixed prefix list. The zone key is whatever follows the last ``/zoneinfo/`` + segment. Hosts with no symlink (Windows, stripped containers) return + ``None``. + """ + tz_env = os.environ.get("TZ", "").strip() + if tz_env: + try: + return ZoneInfo(tz_env).key + except (ZoneInfoNotFoundError, ValueError, OSError): + pass + try: + target = os.readlink("/etc/localtime") + except OSError: + return None + if not target.startswith("/"): + target = posixpath.normpath(posixpath.join("/etc", target)) + zoneinfo_marker = "/zoneinfo/" + marker_index = target.rfind(zoneinfo_marker) + if marker_index == -1: + return None + key = target[marker_index + len(zoneinfo_marker) :] + if not key or key.startswith("/") or ".." in key: + return None + return key + + +def _server_local_utc_offset_minutes() -> int: + """Current UTC offset of the server's local zone, in minutes.""" + offset = datetime.now().astimezone().utcoffset() + return int(offset.total_seconds() // 60) if offset is not None else 0 + + +def _effective_date_timezone_name() -> str: + """Stable label of the timezone the injected date actually follows. + + A configured, valid ``DEER_FLOW_DATE_TIMEZONE`` is reported by its IANA + key; without one, the server-local zone is reported by its resolved IANA + key when the platform exposes it. When no IANA key is recoverable the + declaration falls back to a ``server-local(±HH:MM)`` sentinel carrying the + current UTC offset - never a bare abbreviation, which would be ambiguous + (``CST`` is shared by China, US Central, and Cuba) and would churn across + DST. Declaring the effective zone (never a bare ``probed``) lets the + assembly descriptor tell deployments that anchor the injected date + differently apart. + """ + tz = _date_timezone() + if tz is not None: + key = getattr(tz, "key", None) + if isinstance(key, str) and key: + return key + return "UTC" + local_key = _server_local_timezone_name() + if local_key is not None: + return local_key + offset_minutes = _server_local_utc_offset_minutes() + sign = "+" if offset_minutes >= 0 else "-" + offset_minutes = abs(offset_minutes) + return f"server-local({sign}{offset_minutes // 60:02d}:{offset_minutes % 60:02d})" + + def _format_current_date() -> str: - return datetime.now().strftime("%Y-%m-%d, %A") + tz = _date_timezone() + now = datetime.now(tz) if tz is not None else datetime.now() + return now.strftime("%Y-%m-%d, %A") def _format_current_date_reminder(current_date: str) -> str: @@ -163,6 +271,10 @@ class SubagentDateContextMiddleware(AgentMiddleware): model call without coupling the two runtime paths. """ + def release_policy_parameters(self) -> dict[str, object]: + """The injected date's effective timezone is this middleware's behaviour identity.""" + return {"current_date_timezone": _effective_date_timezone_name()} + @staticmethod def _inject() -> dict: current_date = _format_current_date() @@ -185,8 +297,24 @@ class SubagentDateContextMiddleware(AgentMiddleware): return self._inject() @override - async def abefore_agent(self, state, runtime: Runtime) -> dict: - return self._inject() + async def abefore_agent(self, state, runtime: Runtime) -> dict | None: + # _inject() can resolve DEER_FLOW_DATE_TIMEZONE through ZoneInfo, + # which reads the OS zone database (or the tzdata wheel) on a cold + # cache. SubagentDateContextMiddleware runs on the async subagent path, + # where no assembly observer necessarily warmed that resolution first, + # so the injection is offloaded like DynamicContextMiddleware does (see + # #3402) to keep filesystem work off the event loop. + try: + return await asyncio.wait_for( + asyncio.to_thread(self._inject), + timeout=_INJECT_TIMEOUT_SECONDS, + ) + except TimeoutError: + logger.warning( + "SubagentDateContextMiddleware: date injection timed out (%.1fs); skipping for this run", + _INJECT_TIMEOUT_SECONDS, + ) + return None class DynamicContextMiddleware(AgentMiddleware): @@ -222,6 +350,10 @@ class DynamicContextMiddleware(AgentMiddleware): self._agent_name = agent_name self._app_config = app_config + def release_policy_parameters(self) -> dict[str, object]: + """Declare the injected date's effective timezone for assembly identity.""" + return {"current_date_timezone": _effective_date_timezone_name()} + def _build_full_reminder(self, runtime: Runtime | None = None) -> tuple[str, str | None]: """Return (date_reminder, memory_block | None). diff --git a/backend/packages/harness/pyproject.toml b/backend/packages/harness/pyproject.toml index b77b0eaba..08d4c5688 100644 --- a/backend/packages/harness/pyproject.toml +++ b/backend/packages/harness/pyproject.toml @@ -50,6 +50,10 @@ dependencies = [ "alembic>=1.13", "cryptography>=50.0.0", "e2b-code-interpreter>=2.8.0", + # zoneinfo fallback: ZoneInfo reads the OS zone database first and this + # wheel when the image has none (stripped containers, Windows without + # tzdata), keeping the DEER_FLOW_DATE_TIMEZONE knob portable. + "tzdata>=2025.1", ] [project.scripts] diff --git a/backend/tests/blocking_io/test_subagent_date_context_middleware.py b/backend/tests/blocking_io/test_subagent_date_context_middleware.py new file mode 100644 index 000000000..770db35b8 --- /dev/null +++ b/backend/tests/blocking_io/test_subagent_date_context_middleware.py @@ -0,0 +1,60 @@ +"""Regression anchor: SubagentDateContextMiddleware must not block the event loop. + +``_inject`` can resolve ``DEER_FLOW_DATE_TIMEZONE`` through ``ZoneInfo``, which +reads the OS timezone database (or the bundled ``tzdata`` wheel) on a cold +cache. ``abefore_agent`` runs on the async subagent path with no guarantee that +an assembly observer warmed that resolution first, so it offloads the call via +``asyncio.to_thread`` — the same pattern ``DynamicContextMiddleware`` uses for +its file-I/O injection (see issue #3402). + +This anchor drives the real ``create_agent`` graph via ``ainvoke`` under the +strict Blockbuster gate with the knob enabled. If the offload regresses and +``ZoneInfo`` resolution runs on the event loop, Blockbuster raises +``BlockingError`` and this test fails. +""" + +from __future__ import annotations + +import asyncio + +import pytest +from langchain.agents import create_agent +from langchain_core.language_models.fake_chat_models import FakeMessagesListChatModel +from langchain_core.messages import AIMessage, HumanMessage, SystemMessage + +from deerflow.agents.middlewares.dynamic_context_middleware import ( + _DYNAMIC_CONTEXT_REMINDER_KEY, + SubagentDateContextMiddleware, +) + +pytestmark = pytest.mark.asyncio + + +class _FakeModel(FakeMessagesListChatModel): + """FakeMessagesListChatModel with a no-op ``bind_tools`` for create_agent.""" + + def bind_tools(self, tools, **kwargs): # type: ignore[override] + return self + + +async def test_subagent_abefore_agent_does_not_block_event_loop_with_timezone_enabled(monkeypatch) -> None: + """A cold DEER_FLOW_DATE_TIMEZONE resolution must stay off the event loop.""" + monkeypatch.setenv("DEER_FLOW_DATE_TIMEZONE", "Asia/Shanghai") + middleware = SubagentDateContextMiddleware() + + agent = await asyncio.to_thread( + lambda: create_agent( + model=_FakeModel(responses=[AIMessage(content="ok")]), + tools=[], + middleware=[middleware], + ) + ) + + result = await agent.ainvoke( + {"messages": [HumanMessage(content="hi")]}, + {"configurable": {"thread_id": "test-thread"}}, + ) + + reminders = [message for message in result["messages"] if isinstance(message, SystemMessage) and (message.additional_kwargs or {}).get(_DYNAMIC_CONTEXT_REMINDER_KEY)] + assert reminders, "the subagent date reminder must have been injected" + assert "" in reminders[0].content diff --git a/backend/tests/test_dynamic_context_middleware.py b/backend/tests/test_dynamic_context_middleware.py index 945063b3e..8b8304877 100644 --- a/backend/tests/test_dynamic_context_middleware.py +++ b/backend/tests/test_dynamic_context_middleware.py @@ -800,3 +800,168 @@ def test_no_recursive_id_swap_in_full_middleware_flow(): msgs_v2 = result_v2["messages"] assert msgs_v2[0].id == "msg-2" # reminder takes new message's ID assert msgs_v2[1].id == "msg-2__user" # user content gets derived ID + + +# --------------------------------------------------------------------------- +# Date timezone formatting +# --------------------------------------------------------------------------- + + +def test_format_current_date_defaults_to_server_local_without_env(monkeypatch): + """Without DEER_FLOW_DATE_TIMEZONE the formatter keeps the legacy server-local behavior.""" + from datetime import datetime + + from deerflow.agents.middlewares.dynamic_context_middleware import _format_current_date + + with mock.patch("deerflow.agents.middlewares.dynamic_context_middleware.datetime") as mock_dt: + mock_dt.now.return_value = datetime(2026, 5, 8, 9, 0) + monkeypatch.delenv("DEER_FLOW_DATE_TIMEZONE", raising=False) + + assert _format_current_date() == "2026-05-08, Friday" + mock_dt.now.assert_called_once_with() + + +def test_format_current_date_honors_configured_timezone(monkeypatch): + """A UTC instant must be rendered in the IANA zone named by DEER_FLOW_DATE_TIMEZONE.""" + from datetime import UTC, datetime + + from deerflow.agents.middlewares.dynamic_context_middleware import _format_current_date + + # 2026-09-02 20:30 UTC is 2026-09-03 04:30 in Asia/Shanghai: a UTC-only + # formatter would report the wrong day for a Shanghai user. + fixed_utc = datetime(2026, 9, 2, 20, 30, 0, tzinfo=UTC) + + def fake_now(tz=None): + # datetime.now(tz) semantics: the fixed instant expressed in *tz*. + return fixed_utc.astimezone(tz) if tz is not None else fixed_utc.replace(tzinfo=None) + + with mock.patch("deerflow.agents.middlewares.dynamic_context_middleware.datetime") as mock_dt: + mock_dt.now.side_effect = fake_now + monkeypatch.setenv("DEER_FLOW_DATE_TIMEZONE", "Asia/Shanghai") + + assert _format_current_date() == "2026-09-03, Thursday" + + +def test_format_current_date_invalid_timezone_falls_back(monkeypatch, caplog): + """An unparseable IANA name must warn and degrade to the server-local timezone.""" + from datetime import datetime + + from deerflow.agents.middlewares.dynamic_context_middleware import _format_current_date + + with mock.patch("deerflow.agents.middlewares.dynamic_context_middleware.datetime") as mock_dt: + mock_dt.now.return_value = datetime(2026, 5, 8, 9, 0) + monkeypatch.setenv("DEER_FLOW_DATE_TIMEZONE", "Not/A_Zone") + + assert _format_current_date() == "2026-05-08, Friday" + assert "DEER_FLOW_DATE_TIMEZONE" in caplog.text + + +def _declared_date_timezone_policies(): + from deerflow.agents.middlewares.dynamic_context_middleware import ( + DynamicContextMiddleware, + SubagentDateContextMiddleware, + ) + + return [ + DynamicContextMiddleware().release_policy_parameters(), + SubagentDateContextMiddleware().release_policy_parameters(), + ] + + +def test_date_middlewares_declare_configured_timezone(monkeypatch): + """Assembly identity must reflect the zone the injected date follows.""" + monkeypatch.setenv("DEER_FLOW_DATE_TIMEZONE", "Asia/Shanghai") + + assert _declared_date_timezone_policies() == [ + {"current_date_timezone": "Asia/Shanghai"}, + {"current_date_timezone": "Asia/Shanghai"}, + ] + + +def test_date_middlewares_declare_utc_timezone(monkeypatch): + monkeypatch.setenv("DEER_FLOW_DATE_TIMEZONE", "UTC") + + assert _declared_date_timezone_policies() == [ + {"current_date_timezone": "UTC"}, + {"current_date_timezone": "UTC"}, + ] + + +def test_date_middlewares_declare_resolved_local_zone_without_env(monkeypatch): + """Without the knob the declaration resolves the actual local zone, so two + hosts that render different dates still get different assembly fingerprints.""" + from deerflow.agents.middlewares.dynamic_context_middleware import _effective_date_timezone_name + + monkeypatch.delenv("DEER_FLOW_DATE_TIMEZONE", raising=False) + expected = {"current_date_timezone": _effective_date_timezone_name()} + assert expected["current_date_timezone"] + + assert _declared_date_timezone_policies() == [expected, expected] + + +def test_date_middlewares_declare_resolved_local_zone_for_invalid_env(monkeypatch, caplog): + """An invalid IANA name degrades to server-local and is declared as such.""" + from deerflow.agents.middlewares.dynamic_context_middleware import _effective_date_timezone_name + + monkeypatch.setenv("DEER_FLOW_DATE_TIMEZONE", "Not/A_Zone") + expected = {"current_date_timezone": _effective_date_timezone_name()} + + assert _declared_date_timezone_policies() == [expected, expected] + assert "DEER_FLOW_DATE_TIMEZONE" in caplog.text + + +def test_server_local_timezone_name_reads_tz_env(monkeypatch): + """A POSIX TZ env var naming a real zone resolves to its IANA key.""" + from deerflow.agents.middlewares.dynamic_context_middleware import _server_local_timezone_name + + monkeypatch.setenv("TZ", "Asia/Shanghai") + assert _server_local_timezone_name() == "Asia/Shanghai" + + +def test_server_local_timezone_name_reads_direct_macos_symlink_target(monkeypatch): + """macOS /etc/localtime points at the unversioned zoneinfo dir; the direct + symlink target must be read instead of a fully resolved path.""" + import deerflow.agents.middlewares.dynamic_context_middleware as module + + monkeypatch.delenv("TZ", raising=False) + monkeypatch.setattr(module.os, "readlink", lambda _path: "/var/db/timezone/zoneinfo/Asia/Shanghai") + + assert module._server_local_timezone_name() == "Asia/Shanghai" + + +def test_server_local_timezone_name_reads_apple_versioned_symlink_target(monkeypatch): + """Apple's canonical versioned zoneinfo path must still yield the zone key.""" + import deerflow.agents.middlewares.dynamic_context_middleware as module + + monkeypatch.delenv("TZ", raising=False) + monkeypatch.setattr( + module.os, + "readlink", + lambda _path: "/private/var/db/timezone/tz/2026c.1.0/zoneinfo/America/New_York", + ) + + assert module._server_local_timezone_name() == "America/New_York" + + +def test_server_local_timezone_name_normalizes_relative_symlink_target(monkeypatch): + """A relative /etc/localtime target is resolved against /etc.""" + import deerflow.agents.middlewares.dynamic_context_middleware as module + + monkeypatch.delenv("TZ", raising=False) + monkeypatch.setattr(module.os, "readlink", lambda _path: "../usr/share/zoneinfo/Etc/UTC") + + assert module._server_local_timezone_name() == "Etc/UTC" + + +def test_effective_timezone_sentinel_uses_offset_when_local_zone_is_not_resolvable(monkeypatch): + """Without a recoverable IANA key the declaration pins a stable sentinel.""" + import deerflow.agents.middlewares.dynamic_context_middleware as module + + monkeypatch.setattr(module, "_server_local_timezone_name", lambda: None) + monkeypatch.setattr(module, "_server_local_utc_offset_minutes", lambda: 8 * 60) + monkeypatch.delenv("DEER_FLOW_DATE_TIMEZONE", raising=False) + + assert module._effective_date_timezone_name() == "server-local(+08:00)" + + monkeypatch.setattr(module, "_server_local_utc_offset_minutes", lambda: -5 * 60 - 30) + assert module._effective_date_timezone_name() == "server-local(-05:30)" diff --git a/backend/tests/test_middleware_release_policy.py b/backend/tests/test_middleware_release_policy.py index 500ed8cea..9a4bb29e8 100644 --- a/backend/tests/test_middleware_release_policy.py +++ b/backend/tests/test_middleware_release_policy.py @@ -189,6 +189,18 @@ def _make_system_message_coalescing_middleware(): return SystemMessageCoalescingMiddleware() +def _make_dynamic_context_middleware(): + from deerflow.agents.middlewares.dynamic_context_middleware import DynamicContextMiddleware + + return DynamicContextMiddleware() + + +def _make_subagent_date_context_middleware(): + from deerflow.agents.middlewares.dynamic_context_middleware import SubagentDateContextMiddleware + + return SubagentDateContextMiddleware() + + # Single source of truth for "which middlewares declare a release policy" so # the existence check and the construct-call-hash check below can never drift # apart into two separately-maintained middleware lists. Every entry here is @@ -210,6 +222,11 @@ _MIDDLEWARE_DECLARATIONS = [ ("deerflow.agents.middlewares.tool_output_budget_middleware", "ToolOutputBudgetMiddleware", _make_tool_output_budget_middleware), ("deerflow.agents.middlewares.skill_activation_middleware", "SkillActivationMiddleware", _make_skill_activation_middleware), ("deerflow.agents.middlewares.system_message_coalescing_middleware", "SystemMessageCoalescingMiddleware", _make_system_message_coalescing_middleware), + # The date middlewares declare the effective timezone the injected + # follows, so differently-anchored deployments fingerprint + # differently. + ("deerflow.agents.middlewares.dynamic_context_middleware", "DynamicContextMiddleware", _make_dynamic_context_middleware), + ("deerflow.agents.middlewares.dynamic_context_middleware", "SubagentDateContextMiddleware", _make_subagent_date_context_middleware), ] diff --git a/backend/uv.lock b/backend/uv.lock index 292e05ed4..565eaf73e 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -960,6 +960,7 @@ dependencies = [ { name = "sqlalchemy", extra = ["asyncio"] }, { name = "tavily-python" }, { name = "tiktoken" }, + { name = "tzdata" }, ] [package.optional-dependencies] @@ -1054,6 +1055,7 @@ requires-dist = [ { name = "tenki", marker = "extra == 'tenki'", specifier = ">=1.0.0" }, { name = "textual", marker = "extra == 'tui'", specifier = ">=0.80" }, { name = "tiktoken", specifier = ">=0.8.0" }, + { name = "tzdata", specifier = ">=2025.1" }, ] provides-extras = ["tui", "groundroute", "ollama", "postgres", "redis", "pymupdf", "boxlite", "tenki", "opensandbox", "monocle", "browser", "memory-zh"] diff --git a/config.example.yaml b/config.example.yaml index 168d06c05..df8fd4401 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -7,6 +7,11 @@ # `DEER_FLOW_CONFIG_PATH` to point at a specific config file. # - Runtime state defaults to `.deer-flow` under the project root. Override it # with `DEER_FLOW_HOME` when you need a different writable data directory. +# - Set `DEER_FLOW_DATE_TIMEZONE` to an IANA timezone name (e.g. `Asia/Shanghai`) +# when the conversation date injected into agents should follow a zone other +# than the server's local timezone. It is read at runtime by the date-context +# middlewares (not a config-schema field), so it can be set on a container +# without mounting a `config.yaml`. # - Environment variables are available for all field values. Example: `api_key: $OPENAI_API_KEY` # - The `use` path is a string that looks like "package_name.sub_package_name.module_name:class_name/variable_name".