diff --git a/nanobot/webui/file_preview.py b/nanobot/webui/file_preview.py index 6e104882..82e6b852 100644 --- a/nanobot/webui/file_preview.py +++ b/nanobot/webui/file_preview.py @@ -28,7 +28,7 @@ def file_preview_payload( scope: WorkspaceScope, max_bytes: int = MAX_FILE_PREVIEW_BYTES, ) -> dict[str, Any]: - """Return a text preview for a file inside the session workspace.""" + """Return a text preview for a file allowed by the session workspace scope.""" path = _clean_preview_path(raw_path) if not path: @@ -40,7 +40,7 @@ def file_preview_payload( resolved = resolve_allowed_path( path, workspace=scope.project_path, - allowed_root=scope.project_path, + allowed_root=scope.project_path if scope.restrict_to_workspace else None, strict=True, ) except FileNotFoundError as e: diff --git a/tests/channels/test_websocket_channel.py b/tests/channels/test_websocket_channel.py index b8da34e7..66e03076 100644 --- a/tests/channels/test_websocket_channel.py +++ b/tests/channels/test_websocket_channel.py @@ -2914,7 +2914,11 @@ def test_handle_file_preview_rejects_paths_outside_workspace(tmp_path) -> None: outside = tmp_path / "secret.py" outside.write_text("secret = True\n", encoding="utf-8") - gateway = _basic_handler(MagicMock(), workspace_path=workspace) + gateway = _basic_handler( + MagicMock(), + workspace_path=workspace, + default_restrict_to_workspace=True, + ) gateway.tokens.api_tokens["tok"] = time.monotonic() + 300.0 key = "websocket:file-preview" enc = quote(key, safe="") @@ -2928,6 +2932,39 @@ def test_handle_file_preview_rejects_paths_outside_workspace(tmp_path) -> None: assert resp.status_code == 403 +def test_handle_file_preview_allows_paths_outside_workspace_in_full_access(tmp_path) -> None: + from urllib.parse import quote + + from websockets.datastructures import Headers + from websockets.http11 import Request + + workspace = tmp_path / "workspace" + workspace.mkdir() + outside = tmp_path / "notes.py" + outside.write_text("value = 42\n", encoding="utf-8") + + gateway = _basic_handler( + MagicMock(), + workspace_path=workspace, + default_restrict_to_workspace=False, + ) + gateway.tokens.api_tokens["tok"] = time.monotonic() + 300.0 + key = "websocket:file-preview" + enc = quote(key, safe="") + req = Request( + f"/api/sessions/{enc}/file-preview?path={quote(str(outside), safe='')}", + Headers([("Authorization", "Bearer tok")]), + ) + + resp = gateway.http._handle_file_preview(req, enc) + + assert resp.status_code == 200 + body = json.loads(resp.body.decode()) + assert body["path"] == str(outside.resolve()) + assert body["display_path"] == outside.resolve().as_posix() + assert body["content"].splitlines() == ["value = 42"] + + def test_handle_webui_thread_get_backfills_legacy_missing_user_rows( tmp_path, monkeypatch,