mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-11-07 19:20:34 +00:00
fix(harness): reject Windows reserved device names in thread ID validation (#7892)
* fix(harness): reject Windows reserved device names in thread_id validation A caller-supplied thread ID of NUL (any casing) passes validate_thread_id() but Paths.ensure_thread_dirs() then fails on Windows hosts with FileNotFoundError (WinError 3), because NUL is a reserved device name in every directory. Reject the reserved device names (CON, PRN, AUX, NUL, COM1-9, LPT1-9) case-insensitively in the shared validator, including the ".ext" form kept legal by recent Windows versions, so thread IDs stay safe for every filesystem backend regardless of host OS. * style(harness): apply ruff format to thread_id validation * refactor(harness): reuse windows_incompatible_segment for thread ID check Address review feedback on #7892: - Drop the private _WINDOWS_RESERVED_DEVICE_NAMES copy and route the thread-ID check through the canonical windows_incompatible_segment() from deerflow.utils.host_paths, which uploads, sandbox writes and skill support files already use. The canonical list also covers CONIN$/CONOUT$ and the superscript-digit COM/LPT variants. - Remove the unreachable stem-split: the thread-ID pattern cannot contain a dot, so test whole IDs and pin the reserved-name branch with the dedicated 'reserved Windows device name' message; the unreachable 'NUL.txt' case is removed from the parametrization. - Document the default-behavior change (Linux-host threads with reserved-name IDs will fail validation after upgrade; deletion is unaffected) under ### Fixed in CHANGELOG.md.
This commit is contained in:
parent
8331afa759
commit
b561242ba3
10
CHANGELOG.md
10
CHANGELOG.md
@ -19,6 +19,15 @@ This section accumulates work toward the **2.3.0** milestone
|
||||
|
||||
### Fixed
|
||||
|
||||
- **gateway:** A caller-supplied thread ID that is a Windows reserved device
|
||||
name (`NUL`, `CON`, `COM1`, ...) is now rejected at the shared
|
||||
`validate_thread_id()` boundary. Such IDs previously passed validation but
|
||||
failed workspace creation on Windows hosts with `FileNotFoundError`. The
|
||||
check reuses `windows_incompatible_segment()` and applies on every platform,
|
||||
so threads with reserved-name IDs created on Linux hosts will fail
|
||||
validation on read/resume/upload routes after upgrade (deletion still
|
||||
works). ([#7892])
|
||||
|
||||
- **extensions:** Context-compaction observers now learn which task a
|
||||
compaction happened in. Inside a lead run or subagent they receive that
|
||||
task's store instead of a detached one, and `CompactionEvent` gains
|
||||
@ -16012,3 +16021,4 @@ with **180 merged pull requests** since the first 2.0 milestone tag.
|
||||
[#7781]: https://github.com/bytedance/deer-flow/pull/7781
|
||||
[#7792]: https://github.com/bytedance/deer-flow/pull/7792
|
||||
[#7831]: https://github.com/bytedance/deer-flow/pull/7831
|
||||
[#7892]: https://github.com/bytedance/deer-flow/pull/7892
|
||||
|
||||
@ -8,6 +8,8 @@ from typing import Annotated
|
||||
|
||||
from pydantic import AfterValidator, StringConstraints
|
||||
|
||||
from deerflow.utils.host_paths import windows_incompatible_segment
|
||||
|
||||
THREAD_ID_PATTERN = r"^[A-Za-z0-9_-]{1,64}$"
|
||||
_THREAD_ID_RE = re.compile(THREAD_ID_PATTERN)
|
||||
|
||||
@ -20,6 +22,15 @@ def validate_thread_id(thread_id: str) -> str:
|
||||
"""
|
||||
if not isinstance(thread_id, str) or _THREAD_ID_RE.fullmatch(thread_id) is None:
|
||||
raise ValueError("Invalid thread_id: expected 1-64 ASCII letters, digits, hyphens, or underscores")
|
||||
# Windows reserves device names such as NUL in every directory; creating a
|
||||
# thread workspace named "NUL" fails with FileNotFoundError on Windows
|
||||
# hosts. Reuse the canonical Windows-name check that uploads, sandbox
|
||||
# writes and skill support files already go through, so thread IDs follow
|
||||
# the same documented "rejected on every platform when a name is created"
|
||||
# contract instead of keeping a second name list in sync.
|
||||
reason = windows_incompatible_segment(thread_id)
|
||||
if reason is not None:
|
||||
raise ValueError(f"Invalid thread_id {thread_id!r}: {reason}")
|
||||
return thread_id
|
||||
|
||||
|
||||
|
||||
@ -27,6 +27,18 @@ def test_validate_thread_id_accepts_canonical_ids(thread_id: str) -> None:
|
||||
"has space",
|
||||
"line\nbreak",
|
||||
"线程",
|
||||
# Windows reserved device names break workspace creation on Windows
|
||||
# hosts (e.g. Paths.ensure_thread_dirs raising FileNotFoundError for
|
||||
# "NUL"), so they are rejected at the shared validation boundary via
|
||||
# windows_incompatible_segment().
|
||||
"NUL",
|
||||
"nul",
|
||||
"Nul",
|
||||
"CON",
|
||||
"PRN",
|
||||
"AUX",
|
||||
"COM1",
|
||||
"LPT9",
|
||||
],
|
||||
)
|
||||
def test_validate_thread_id_rejects_noncanonical_ids(thread_id: str) -> None:
|
||||
@ -36,6 +48,28 @@ def test_validate_thread_id_rejects_noncanonical_ids(thread_id: str) -> None:
|
||||
validate_thread_id(thread_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"thread_id",
|
||||
[
|
||||
"NUL",
|
||||
"nul",
|
||||
"Nul",
|
||||
"CON",
|
||||
"PRN",
|
||||
"AUX",
|
||||
"COM1",
|
||||
"LPT9",
|
||||
],
|
||||
)
|
||||
def test_validate_thread_id_rejects_windows_reserved_names(thread_id: str) -> None:
|
||||
"""Reserved device names hit the windows_incompatible_segment branch, not
|
||||
just the character-pattern branch."""
|
||||
from deerflow.utils.thread_id import validate_thread_id
|
||||
|
||||
with pytest.raises(ValueError, match="reserved Windows device name"):
|
||||
validate_thread_id(thread_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("thread_id", [1, {}, []])
|
||||
def test_validate_thread_id_rejects_non_strings(thread_id: object) -> None:
|
||||
from deerflow.utils.thread_id import validate_thread_id
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user