fix(support-bundle): retain diagnostics when dotenv cannot be read (#6536)

Co-authored-by: Zhangchenhao-111 <285911435+Zhangchenhao-111@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
haochen 2026-10-10 20:02:39 +08:00 committed by GitHub
parent 737492d4e1
commit 8377aa3d39
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 37 additions and 5 deletions

View File

@ -201,7 +201,8 @@ streams simulate chunks from a non-streaming response, while no-tool streams sta
values override shell exports, and a project-root override alone still uses
`backend/.deer-flow` first. Simple variable references such as
`DEER_FLOW_HOME="$PWD/backend/.deer-flow"` use the checkout as `PWD`;
single-quoted references remain literal. For standalone Gateway launches using `backend/.env`
single-quoted references remain literal. If the checkout `.env` cannot be read or decoded
as UTF-8, thread diagnostics retain shell exports and default storage paths. For standalone Gateway launches using `backend/.env`
or `DEER_FLOW_ENV_FILE`, export the effective `DEER_FLOW_HOME` when collecting
the bundle and ensure the checkout `.env` does not override it.
Doctor's internal tool probes also decode UTF-8 with replacement for invalid

View File

@ -25,6 +25,32 @@ def _zip_text(zip_path, name: str) -> str:
return zf.read(name).decode("utf-8")
@pytest.mark.parametrize("failure", ["permission", "encoding"])
@pytest.mark.parametrize("configured_home", [False, True])
def test_unreadable_dotenv_keeps_thread_diagnostics(tmp_path, monkeypatch, failure, configured_home):
project = tmp_path / "checkout"
project.mkdir()
home = tmp_path / "data" if configured_home else project / ".deer-flow"
(home / "threads" / "broken-env" / "user-data").mkdir(parents=True)
env_file = project / ".env"
# A valid prefix must not partially change runtime paths when decoding fails.
env_file.write_bytes(b"DEER_FLOW_HOME=wrong-home\n" + b"#" * 10000 + b"\xff")
if configured_home:
monkeypatch.setenv("DEER_FLOW_HOME", str(home))
if failure == "permission":
original_open = Path.open
def guarded_open(path, *args, **kwargs):
if path == env_file:
raise PermissionError("Cannot read .env")
return original_open(path, *args, **kwargs)
monkeypatch.setattr(Path, "open", guarded_open)
summary = support_bundle.collect_thread_summary(project, "broken-env")
assert summary["found"] is True
@pytest.mark.parametrize("layout", ["threads/thread-home", "users/alice/threads/thread-home"])
def test_thread_summary_uses_configured_runtime_home(tmp_path, monkeypatch, layout):
project = tmp_path / "checkout"

View File

@ -4,7 +4,9 @@ Thread manifests use a nonempty `DEER_FLOW_HOME` exclusively, resolving relative
values from the checkout like the local launcher. Read root `.env` path settings
without exporting secrets; dotenv overrides shell exports, including empty values.
Expand unquoted leading tildes, preserving quoted literals. If python-dotenv is
unavailable, retain shell/legacy lookup so troubleshooting remains usable.
unavailable or `.env` cannot be read/decoded as UTF-8, retain shell/legacy lookup
so troubleshooting remains usable. Read the complete file before applying any
assignments so a decoding failure cannot partially override shell values.
Resolve `$NAME`, `${NAME}` and `${NAME:-literal}` in unquoted/double-quoted
values using a private environment with checkout `PWD` and earlier dotenv
assignments. Single-quoted values and escaped dollars stay literal. Parse the

View File

@ -12,6 +12,7 @@ import subprocess
import sys
import zipfile
from datetime import UTC, datetime
from io import StringIO
from pathlib import Path
from typing import Any
@ -283,14 +284,16 @@ def _thread_data_roots(project_root: Path) -> list[tuple[Path, str]]:
if env_file.is_file():
try:
from dotenv.parser import parse_stream
except ImportError:
# Diagnostics must still work in an incomplete backend environment.
contents = env_file.read_text(encoding="utf-8-sig")
except (ImportError, OSError, UnicodeError):
# Diagnostics must still work with a broken environment or unreadable .env.
pass
else:
# serve.sh changes to the checkout before sourcing .env. Keep its
# assignment order in a private mapping; never export parsed secrets.
environment = {**os.environ, "PWD": str(project_root.resolve())}
with env_file.open(encoding="utf-8-sig") as stream:
with StringIO(contents) as stream:
for binding in parse_stream(stream):
if not binding.key or binding.value is None or binding.error:
continue