fix(config): accept a UTF-8 BOM in operator-authored env and secret files (#7085) (#7112)

This commit is contained in:
Jay0130-a 2026-10-19 15:48:10 +08:00 committed by GitHub
parent 5f72f28988
commit 46c0899e53
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 46 additions and 2 deletions

View File

@ -96,7 +96,11 @@ def read_or_create_secret_file(
def _read(path: Path) -> str | None:
try:
return path.read_text(encoding="utf-8").strip()
# utf-8-sig: a secret restored or edited on Windows may carry a BOM.
# It is not whitespace, so the strip below keeps it, validation then
# rejects the value, and a credentials key file bricks every stored
# secret. A BOM is never part of a legitimate secret.
return path.read_text(encoding="utf-8-sig").strip()
except FileNotFoundError:
return None

View File

@ -26,7 +26,10 @@ def load_selected_env_file() -> bool:
if not selected or not path.is_file():
raise ValueError("Expected a regular file")
# Read fully before parsing so decode failures cannot partially load env.
contents = path.read_text(encoding="utf-8")
# utf-8-sig consumes a leading BOM (Notepad / PowerShell-written files):
# plain utf-8 keeps U+FEFF, and dotenv then parses the first variable
# under a BOM-prefixed key that no consumer reads.
contents = path.read_text(encoding="utf-8-sig")
except (OSError, ValueError):
raise ValueError("DEER_FLOW_ENV_FILE must point to a readable UTF-8 regular file. Check the path and permissions; relative paths use the backend process working directory.") from None

View File

@ -67,6 +67,21 @@ def test_process_environment_wins(tmp_path, value):
assert json.loads(result.stdout) == [value, None]
@pytest.mark.parametrize("entrypoint", ENTRYPOINTS)
def test_a_bom_prefixed_selection_file_loads_its_first_variable(tmp_path, entrypoint):
"""Windows Notepad and the PowerShell 5.1 ``Set-Content -Encoding UTF8``
switch write a UTF-8 BOM. Plain utf-8 decoding keeps U+FEFF, and dotenv
then parses the first variable under a BOM-prefixed key no consumer
reads — a silently dropped first variable. The BOM is an encoding
artifact, so the selection loader consumes it."""
selected = tmp_path / "bom.env"
selected.write_bytes(b"\xef\xbb\xbf" + b"ENV_FILE_TEST_VALUE=bom-selected\nENV_FILE_TEST_DEFAULT_ONLY=from-bom\n")
assert selected.read_bytes().startswith(b"\xef\xbb\xbf"), "fixture must carry a real BOM"
result = run_startup(tmp_path, entrypoint, selected)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == ["bom-selected", "from-bom"]
@pytest.mark.parametrize("entrypoint", ENTRYPOINTS)
def test_unset_preserves_default_loading(tmp_path, entrypoint):
result = run_startup(tmp_path, entrypoint)

View File

@ -43,6 +43,28 @@ def test_reuses_an_existing_value_without_generating(tmp_path: Path) -> None:
assert path.read_text(encoding="utf-8") == "existing-value\n"
def test_a_bom_prefixed_secret_still_validates(tmp_path: Path) -> None:
"""A secret file saved by a Windows editor may carry a UTF-8 BOM.
U+FEFF is not whitespace, so the strip never removes it and validation
rejects the value — for ``.credentials_key`` that refuses startup and
leaves every stored secret unreadable. The BOM is an encoding artifact,
never part of a legitimate secret, so the reader consumes it.
"""
path = tmp_path / ".secret"
path.write_bytes(b"\xef\xbb\xbf" + b"existing-value\n")
def validate(value: str) -> None:
if value != "existing-value":
raise ValueError("not the expected secret")
def generate() -> str:
raise AssertionError("a readable secret must never be regenerated")
assert read_or_create_secret_file(path, generate, validate=validate) == "existing-value"
assert path.read_bytes().startswith(b"\xef\xbb\xbf"), "reading must not rewrite the file"
def test_a_peer_that_publishes_first_wins(tmp_path: Path) -> None:
"""Deterministic race: the peer creates the file between our existence check and our create.