from __future__ import annotations import asyncio from functools import partial from pathlib import Path from typing import Any from app.agents.tools.types import RegisteredTool, ToolResult PROJECT_ROOT = Path(__file__).resolve().parents[5] def _validate_path(raw_path: str) -> Path: """경로를 검증하고 절대 경로로 변환합니다. 프로젝트 루트 외부 접근 및 경로 순회 공격을 차단합니다. """ resolved = (PROJECT_ROOT / raw_path).resolve() if not resolved.is_relative_to(PROJECT_ROOT): raise ValueError(f"프로젝트 외부 경로 접근 불가: {raw_path}") return resolved def _read_sync(path: Path, offset: int, limit: int) -> str: text = path.read_text(encoding="utf-8") lines = text.splitlines(keepends=True) selected = lines[offset : offset + limit] return "".join(selected) def create_read_file_tool() -> RegisteredTool: """파일 읽기 도구를 생성합니다.""" async def execute(params: dict[str, Any]) -> ToolResult: raw_path: str = params["path"] offset: int = params.get("offset", 0) limit: int = params.get("limit", 2000) try: path = _validate_path(raw_path) except ValueError as e: return ToolResult(data=f"오류: {e}") if not path.exists(): return ToolResult(data=f"파일 없음: {raw_path}") if not path.is_file(): return ToolResult(data=f"파일이 아님: {raw_path}") loop = asyncio.get_running_loop() try: content = await loop.run_in_executor( None, partial(_read_sync, path, offset, limit) ) except Exception as e: return ToolResult(data=f"파일 읽기 실패: {e}") return ToolResult(data=content) return RegisteredTool( name="read_file", description="파일 내용을 읽습니다. 프로젝트 내 파일만 접근 가능합니다.", compact_description="파일 읽기", concurrency_safe=True, execute=execute, )