mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-11-08 12:00:36 +00:00
* 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.
49 lines
1.8 KiB
Python
49 lines
1.8 KiB
Python
"""Canonical thread identifier validation shared across DeerFlow backends."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import uuid
|
|
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)
|
|
|
|
|
|
def validate_thread_id(thread_id: str) -> str:
|
|
"""Return a valid thread ID or raise ``ValueError``.
|
|
|
|
Thread IDs are caller-defined opaque identifiers, not necessarily UUIDs,
|
|
but they must be safe for every persistence and filesystem backend.
|
|
"""
|
|
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
|
|
|
|
|
|
def resolve_thread_id(thread_id: str | None) -> str:
|
|
"""Validate a supplied ID, generating a UUID only when it is ``None``."""
|
|
if thread_id is None:
|
|
return str(uuid.uuid4())
|
|
return validate_thread_id(thread_id)
|
|
|
|
|
|
ThreadId = Annotated[
|
|
str,
|
|
StringConstraints(min_length=1, max_length=64, pattern=THREAD_ID_PATTERN),
|
|
AfterValidator(validate_thread_id),
|
|
]
|