|
1 | 1 | """Repository management routes - CRUD and indexing.""" |
2 | 2 | from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect, Depends, BackgroundTasks |
3 | | -from pydantic import BaseModel |
| 3 | +from pydantic import BaseModel, validator |
4 | 4 | from typing import List, Optional |
5 | 5 | from pathlib import Path |
6 | 6 | import hashlib |
@@ -178,7 +178,7 @@ async def delete_repository( |
178 | 178 | raise HTTPException(status_code=500, detail="Failed to delete repository") |
179 | 179 |
|
180 | 180 |
|
181 | | -def _scan_directories(local_path: Path) -> list: |
| 181 | +def _scan_directories(local_path: Path) -> List[dict]: |
182 | 182 | """Scan top-level directories and count code files in each. |
183 | 183 |
|
184 | 184 | Runs synchronously -- call via asyncio.to_thread() from async handlers |
@@ -206,7 +206,7 @@ def _scan_directories(local_path: Path) -> list: |
206 | 206 | async def get_repo_directories( |
207 | 207 | repo_id: str, |
208 | 208 | auth: AuthContext = Depends(require_auth), |
209 | | -): |
| 209 | +) -> dict: |
210 | 210 | """Return the top-level directory tree of a cloned repo. |
211 | 211 |
|
212 | 212 | Used for monorepo subset selection -- lets the user pick which |
@@ -461,6 +461,17 @@ class IndexConfig(BaseModel): |
461 | 461 | include_paths: Optional[List[str]] = None # e.g. ["packages/effect", "packages/schema"] |
462 | 462 | incremental: bool = True |
463 | 463 |
|
| 464 | + @validator("include_paths", each_item=True, pre=True) |
| 465 | + @classmethod |
| 466 | + def sanitize_path(cls, v: str) -> str: |
| 467 | + """Reject path traversal, empty strings, and normalize slashes.""" |
| 468 | + v = v.strip().strip("/") |
| 469 | + if not v: |
| 470 | + raise ValueError("include_paths entries must not be empty") |
| 471 | + if ".." in v.split("/"): |
| 472 | + raise ValueError(f"Path traversal not allowed: {v}") |
| 473 | + return v |
| 474 | + |
464 | 475 |
|
465 | 476 | @router.post("/{repo_id}/index/async", status_code=202) |
466 | 477 | async def index_repository_async( |
@@ -564,7 +575,13 @@ async def _authenticate_websocket(websocket: WebSocket) -> Optional[dict]: |
564 | 575 | # Note: WebSocket routes need to be registered on the main app, not router |
565 | 576 | # This function is exported and called from main.py |
566 | 577 | async def websocket_index(websocket: WebSocket, repo_id: str): |
567 | | - """Real-time repository indexing with progress updates.""" |
| 578 | + """Real-time repository indexing with progress updates. |
| 579 | +
|
| 580 | + NOTE: This WebSocket-direct-indexing path does NOT support include_paths |
| 581 | + (monorepo subset selection). Use the HTTP async endpoint instead: |
| 582 | + POST /repos/{id}/index/async with IndexConfig body. |
| 583 | + This handler is the older pattern -- kept for backward compatibility. |
| 584 | + """ |
568 | 585 | user = await _authenticate_websocket(websocket) |
569 | 586 | if not user: |
570 | 587 | return |
|
0 commit comments