mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-27 00:17:53 +00:00
* fix(workspace-changes): classify a symlink replacing a file distinctly from deleted scan_workspace_roots() skipped every symlinked path entirely (host_file.is_symlink() -> continue), so the path was completely absent from a snapshot instead of being recorded as a metadata-only stub the way binary/large/sensitive-looking files already are. When an agent run replaces a tracked file with a symlink (e.g. rm config.txt && ln -s /some/other/path config.txt), the after-snapshot never contained that path at all, so compare_snapshots()'s _status() saw after_file=None and reported plain "deleted" -- silently hiding that the path is still alive on disk, now as a symlink that can point anywhere on the host, including outside the workspace root. Add a "symlink" classification mirroring the existing binary/large/ sensitive pattern: scan_workspace_roots() now records a symlink as a metadata-only FileSnapshot stub (symlink=True, symlink_target from os.readlink(), lstat'd without ever following the link) instead of omitting it. _status() reports "symlink_created" whenever a symlink newly occupies a path that was not already a symlink (brand new or replacing a prior file), so the security-relevant fact surfaces distinctly instead of collapsing into "deleted". A symlink genuinely removed with nothing replacing it is unchanged: still "deleted". Verified against a real POSIX symlink (WSL; native Windows symlink creation needs elevated privilege) driving the unmodified scan_workspace_roots()/compare_snapshots() functions, and via a patch-file revert/reapply cycle on this same fix to confirm the added regression tests fail before and pass after. * fix(workspace-changes): count symlink_created in the changed-file badge getChangedFileCount summed only created + modified + deleted, so a run whose only change was a symlink replacing a file (reported as the new symlink_created status, not deleted) produced a count of 0 and WorkspaceChangeBadge hid the badge entirely -- the exact scenario this PR targets, with the opposite of the intended result. Add symlink_created to the frontend WorkspaceChangeSummary interface (already emitted by the backend) and include it in the count. The per-file StatusIcon/statusLabel "modified" fallthrough is unaffected and left as a follow-up, per review. Regression test reverts cleanly to reproduce a count of 0 pre-fix. * fix(workspace-changes): rank symlink_created in file sort and complete the type contract sortWorkspaceChanges's statusRank had no entry for the new symlink_created status, so statusRank[left.status] - statusRank[right.status] evaluated to NaN for any comparison involving a symlink-created file, violating Array#sort's ordering contract instead of producing a deterministic order. The frontend WorkspaceChangeStatus and DiffUnavailableReason unions, and the WorkspaceFileChange symlink fields, also stayed narrower than what the backend now emits, so TypeScript's satisfies Record<...> guard on statusRank could not catch the gap. Widen WorkspaceChangeStatus to include "symlink_created" and DiffUnavailableReason to include "symlink", add the matching symlink/symlink_target_before/symlink_target_after fields to WorkspaceFileChange, and give symlink_created a rank alongside modified in statusRank -- restoring the satisfies guard's ability to catch a future unranked status. unavailableLabel now has an explicit "symlink" branch (new symlinkUnavailable i18n string, en-US + zh-CN) instead of falling through to the generic label. Also fixes the failing e2e-tests CI check: the existing workspace-changes.spec.ts mock summary predates the symlink_created field, so getChangedFileCount computed 1 + 1 + 0 + undefined = NaN and the badge rendered "Edited NaN files" instead of "Edited 2 files". Added symlink_created: 0 to the mock to match the real backend contract. New sortWorkspaceChanges unit tests revert cleanly against the unranked statusRank to reproduce the NaN-driven misordering. pnpm test (626 tests), pnpm check, and pnpm format are clean, and the previously-failing e2e spec plus the full e2e suite (94 tests) pass.
77 lines
1.9 KiB
Python
77 lines
1.9 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from .types import WORKSPACE_CHANGES_EVENT_TYPE, WORKSPACE_CHANGES_METADATA_KEY
|
|
|
|
EMPTY_SUMMARY = {
|
|
"created": 0,
|
|
"modified": 0,
|
|
"deleted": 0,
|
|
"symlink_created": 0,
|
|
"additions": 0,
|
|
"deletions": 0,
|
|
"truncated": False,
|
|
}
|
|
|
|
|
|
async def get_workspace_changes_response(
|
|
event_store: Any,
|
|
thread_id: str,
|
|
run_id: str,
|
|
*,
|
|
include_files: bool = True,
|
|
include_diff: bool = True,
|
|
) -> dict[str, Any]:
|
|
events = await event_store.list_events(
|
|
thread_id,
|
|
run_id,
|
|
event_types=[WORKSPACE_CHANGES_EVENT_TYPE],
|
|
limit=10,
|
|
)
|
|
if not events:
|
|
return _empty_response()
|
|
|
|
payload = _extract_workspace_changes_payload(events[-1])
|
|
if not isinstance(payload, dict):
|
|
return _empty_response()
|
|
|
|
response = dict(payload)
|
|
response["available"] = True
|
|
response.setdefault("summary", dict(EMPTY_SUMMARY))
|
|
if include_files:
|
|
response.setdefault("files", [])
|
|
if not include_diff:
|
|
response["files"] = [_without_diff(file) for file in response["files"]]
|
|
else:
|
|
response["files"] = []
|
|
return response
|
|
|
|
|
|
def _empty_response() -> dict[str, Any]:
|
|
return {
|
|
"available": False,
|
|
"version": 1,
|
|
"summary": dict(EMPTY_SUMMARY),
|
|
"files": [],
|
|
"limits": {},
|
|
}
|
|
|
|
|
|
def _extract_workspace_changes_payload(event: dict[str, Any]) -> Any:
|
|
metadata = event.get("metadata") or {}
|
|
if isinstance(metadata, dict) and WORKSPACE_CHANGES_METADATA_KEY in metadata:
|
|
return metadata[WORKSPACE_CHANGES_METADATA_KEY]
|
|
content = event.get("content")
|
|
if isinstance(content, dict):
|
|
return content
|
|
return None
|
|
|
|
|
|
def _without_diff(file: Any) -> Any:
|
|
if not isinstance(file, dict):
|
|
return file
|
|
sanitized = dict(file)
|
|
sanitized["diff"] = ""
|
|
return sanitized
|