Skip to content

Commit 4db47e8

Browse files
committed
feat: resolve relative paths against session cwd in all file endpoints
All LLM-exposed file endpoints (list_files, read_file, write_file, display_file, replace_file_content, grep_search, glob_search) now read the X-Session-Id header and resolve relative paths against the session's working directory instead of always using fs.home. - Added optional cwd parameter to UserFS.resolve_path() - Relative paths join against cwd if provided, else fs.home - Absolute paths are unaffected - execute endpoint also resolves relative cwd param against session cwd
1 parent fbaf2e8 commit 4db47e8

File tree

2 files changed

+35
-15
lines changed

2 files changed

+35
-15
lines changed

open_terminal/main.py

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -450,10 +450,13 @@ async def set_cwd(
450450
},
451451
)
452452
async def list_files(
453+
http_request: Request,
453454
directory: str = Query(".", description="Directory path to list."),
454455
fs: UserFS = Depends(get_filesystem),
455456
):
456-
target = fs.resolve_path(directory)
457+
session_id = http_request.headers.get("x-session-id")
458+
session_cwd = _get_session_cwd(session_id, fs) if session_id else None
459+
target = fs.resolve_path(directory, cwd=session_cwd)
457460
if not await fs.isdir(target):
458461
raise HTTPException(status_code=404, detail="Directory not found")
459462
entries = await fs.listdir(target)
@@ -473,6 +476,7 @@ async def list_files(
473476
},
474477
)
475478
async def read_file(
479+
http_request: Request,
476480
path: str = Query(..., description="Path to the file to read."),
477481
start_line: Optional[int] = Query(
478482
None, description="First line to return (1-indexed, inclusive). Defaults to the beginning of the file.", ge=1
@@ -482,8 +486,9 @@ async def read_file(
482486
),
483487
fs: UserFS = Depends(get_filesystem),
484488
):
485-
486-
target = fs.resolve_path(path)
489+
session_id = http_request.headers.get("x-session-id")
490+
session_cwd = _get_session_cwd(session_id, fs) if session_id else None
491+
target = fs.resolve_path(path, cwd=session_cwd)
487492
if not await fs.isfile(target):
488493
raise HTTPException(status_code=404, detail="File not found")
489494

@@ -544,6 +549,7 @@ async def read_file(
544549
},
545550
)
546551
async def display_file(
552+
http_request: Request,
547553
path: str = Query(..., description="Absolute path to the file to display."),
548554
fs: UserFS = Depends(get_filesystem),
549555
):
@@ -554,7 +560,9 @@ async def display_file(
554560
intercepting this response and presenting the file in its own UI (e.g.
555561
opening a preview pane, launching a viewer, etc.).
556562
"""
557-
target = fs.resolve_path(path)
563+
session_id = http_request.headers.get("x-session-id")
564+
session_cwd = _get_session_cwd(session_id, fs) if session_id else None
565+
target = fs.resolve_path(path, cwd=session_cwd)
558566
exists = await fs.isfile(target)
559567
return {"path": target, "exists": exists}
560568

@@ -595,8 +603,10 @@ async def view_file(
595603
401: {"description": "Invalid or missing API key."},
596604
},
597605
)
598-
async def write_file(request: WriteRequest, fs: UserFS = Depends(get_filesystem)):
599-
target = fs.resolve_path(request.path)
606+
async def write_file(http_request: Request, request: WriteRequest, fs: UserFS = Depends(get_filesystem)):
607+
session_id = http_request.headers.get("x-session-id")
608+
session_cwd = _get_session_cwd(session_id, fs) if session_id else None
609+
target = fs.resolve_path(request.path, cwd=session_cwd)
600610
try:
601611
await fs.write(target, request.content)
602612
except (OSError, subprocess.CalledProcessError) as e:
@@ -676,8 +686,10 @@ async def move_entry(request: MoveRequest, fs: UserFS = Depends(get_filesystem))
676686
401: {"description": "Invalid or missing API key."},
677687
},
678688
)
679-
async def replace_file_content(request: ReplaceRequest, fs: UserFS = Depends(get_filesystem)):
680-
target = fs.resolve_path(request.path)
689+
async def replace_file_content(http_request: Request, request: ReplaceRequest, fs: UserFS = Depends(get_filesystem)):
690+
session_id = http_request.headers.get("x-session-id")
691+
session_cwd = _get_session_cwd(session_id, fs) if session_id else None
692+
target = fs.resolve_path(request.path, cwd=session_cwd)
681693
if not await fs.isfile(target):
682694
raise HTTPException(status_code=404, detail="File not found")
683695

@@ -735,6 +747,7 @@ async def replace_file_content(request: ReplaceRequest, fs: UserFS = Depends(get
735747
},
736748
)
737749
async def grep_search(
750+
http_request: Request,
738751
query: str = Query(..., description="Text or regex pattern to search for."),
739752
path: str = Query(".", description="Directory or file to search in."),
740753
regex: bool = Query(False, description="Treat query as a regex pattern."),
@@ -754,7 +767,9 @@ async def grep_search(
754767
),
755768
fs: UserFS = Depends(get_filesystem),
756769
):
757-
target = fs.resolve_path(path)
770+
session_id = http_request.headers.get("x-session-id")
771+
session_cwd = _get_session_cwd(session_id, fs) if session_id else None
772+
target = fs.resolve_path(path, cwd=session_cwd)
758773
if not await aiofiles.os.path.exists(target):
759774
raise HTTPException(status_code=404, detail="Search path not found")
760775

@@ -845,6 +860,7 @@ def _search_file(file_path: str):
845860
},
846861
)
847862
async def glob_search(
863+
http_request: Request,
848864
pattern: str = Query(..., description="Glob pattern to search for (e.g. '*.py')."),
849865
path: str = Query(".", description="Directory to search within."),
850866
exclude: Optional[list[str]] = Query(
@@ -860,7 +876,9 @@ async def glob_search(
860876
),
861877
fs: UserFS = Depends(get_filesystem),
862878
):
863-
target = fs.resolve_path(path)
879+
session_id = http_request.headers.get("x-session-id")
880+
session_cwd = _get_session_cwd(session_id, fs) if session_id else None
881+
target = fs.resolve_path(path, cwd=session_cwd)
864882
if not await aiofiles.os.path.isdir(target):
865883
raise HTTPException(status_code=404, detail="Search directory not found")
866884

@@ -1093,7 +1111,8 @@ async def execute(
10931111
):
10941112
fs = get_filesystem(http_request)
10951113
session_id = http_request.headers.get("x-session-id")
1096-
cwd = fs.resolve_path(request.cwd) if request.cwd else _get_session_cwd(session_id, fs)
1114+
session_cwd = _get_session_cwd(session_id, fs) if session_id else None
1115+
cwd = fs.resolve_path(request.cwd, cwd=session_cwd) if request.cwd else (session_cwd or fs.home)
10971116

10981117
subprocess_env = {**os.environ, **request.env} if request.env else None
10991118
runner = await create_runner(

open_terminal/utils/fs.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,13 @@ def __init__(self, username: str | None = None, home: str | None = None):
3838
# Path resolution
3939
# ------------------------------------------------------------------
4040

41-
def resolve_path(self, path: str) -> str:
41+
def resolve_path(self, path: str, cwd: str | None = None) -> str:
4242
"""Resolve *path* to an absolute path relative to the user's home.
4343
4444
Absolute paths are normalised in place. Relative paths are joined
45-
to ``self.home`` so that they resolve against the user's home
46-
directory rather than the server process's ``os.getcwd()``.
45+
to *cwd* (if provided) or ``self.home`` so that they resolve against
46+
the session's working directory rather than the server process's
47+
``os.getcwd()``.
4748
4849
In multi-user mode, paths under ``/home/user`` (the server process's
4950
default home) are automatically rewritten to the provisioned user's
@@ -61,7 +62,7 @@ def resolve_path(self, path: str) -> str:
6162
path = self.home + path[len(prefix):]
6263
break
6364
return os.path.normpath(path)
64-
return os.path.normpath(os.path.join(self.home, path))
65+
return os.path.normpath(os.path.join(cwd or self.home, path))
6566

6667
# ------------------------------------------------------------------
6768
# Path validation

0 commit comments

Comments
 (0)