mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-10-25 17:36:29 +08:00
Merge pull request #3805 from rudycelekli/fix/registered-workspace-preview-20261021
fix(web): serve artifacts from registered agent workspaces
This commit is contained in:
3 files changed
+115
-3
No files matched your search
@@ -833,8 +833,8 @@ def _live_channel_manager():
|
||||
def _serve_allowed_roots() -> list:
|
||||
"""Roots that /api/file and /preview may read from (symlinks resolved).
|
||||
|
||||
Includes the configured serve root, the Agent workspace, and any project
|
||||
directory a session has opened. Project dirs may live outside the serve
|
||||
Includes the configured serve root, all configured Agent workspaces
|
||||
(including disabled Agents), and any project directory a session has opened. Project dirs may live outside the serve
|
||||
root (e.g. ``/tmp/foo``), so previewing files in an opened project would
|
||||
otherwise be denied.
|
||||
"""
|
||||
@@ -843,6 +843,14 @@ def _serve_allowed_roots() -> list:
|
||||
os.path.realpath(os.path.expanduser(serve_root)),
|
||||
os.path.realpath(_get_workspace_root()),
|
||||
]
|
||||
# Artifact URLs are global, and the artifact index includes disabled
|
||||
# Agents too. Their explicitly configured workspaces remain readable.
|
||||
try:
|
||||
from agent.registry import get_agent_registry
|
||||
for profile in get_agent_registry().list(include_disabled=True):
|
||||
roots.append(os.path.realpath(profile.workspace))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
from agent.workspace import project_store
|
||||
for rec in project_store.list_recents():
|
||||
|
||||
@@ -28,7 +28,7 @@ The Web Console is CowAgent's default channel. It runs automatically once starte
|
||||
| `external_api_enabled` | Turn on the [Chat API](/extensions/chat-api) | `false` |
|
||||
| `external_api_token` | API key of the [Chat API](/extensions/chat-api), kept while the API is off | `""` |
|
||||
| `web_session_expire_days` | Login session validity in days | `30` |
|
||||
| `web_file_serve_root` | Root directory the web console can directly read/send files from. Defaults to the user home dir and agent workspace only; set to `/` to allow the whole filesystem | `"~"` |
|
||||
| `web_file_serve_root` | Additional root directory the web console can directly read/send files from (defaults to the user home directory). Configured Agent workspaces, including disabled Agents, and opened project directories are also allowed; set to `/` to allow the whole filesystem | `"~"` |
|
||||
| `enable_thinking` | Whether to enable deep thinking mode | `false` |
|
||||
|
||||
Once a password is configured, you must enter it to log in when accessing the console. The login session is kept for 30 days by default, so restarting the service during that period does not require re-login. The password can also be changed online from the "Configuration" page in the console.
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
"""Global artifact routes can serve files in configured Agent workspaces."""
|
||||
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import pytest
|
||||
|
||||
web = pytest.importorskip("web")
|
||||
|
||||
from agent.registry import AgentProfile, AgentRegistry, set_agent_registry # noqa: E402
|
||||
from channel.web.api import files # noqa: E402
|
||||
from channel.web.core import _common # noqa: E402
|
||||
from config import conf # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def registered_files(tmp_path, monkeypatch):
|
||||
default = tmp_path / "default"
|
||||
helper = tmp_path / "helper"
|
||||
disabled = tmp_path / "disabled"
|
||||
for folder in (default, helper, disabled):
|
||||
folder.mkdir()
|
||||
registry = AgentRegistry(
|
||||
[
|
||||
AgentProfile("default", "Default", str(default)),
|
||||
AgentProfile("helper", "Helper", str(helper)),
|
||||
AgentProfile("disabled", "Disabled", str(disabled), enabled=False),
|
||||
],
|
||||
"default",
|
||||
)
|
||||
set_agent_registry(registry)
|
||||
monkeypatch.setitem(conf(), "web_password", "")
|
||||
monkeypatch.setitem(conf(), "web_file_serve_root", str(default))
|
||||
monkeypatch.setattr(_common, "_PREVIEW_SECRET", b"local-test-preview-secret")
|
||||
app = web.application(
|
||||
("/api/file", "FileServeHandler", "/preview/(.*)", "PreviewHandler"),
|
||||
vars(files),
|
||||
)
|
||||
return app, {"default": default, "helper": helper, "disabled": disabled}
|
||||
|
||||
|
||||
def request_file(app, path, preview):
|
||||
url = (
|
||||
_common._build_preview_url(str(path))
|
||||
if preview
|
||||
else "/api/file?" + urlencode({"path": str(path)})
|
||||
)
|
||||
return app.request(url)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("preview", [False, True])
|
||||
@pytest.mark.parametrize("owner", ["default", "helper", "disabled"])
|
||||
def test_registered_workspace_artifact_bytes_are_readable(
|
||||
registered_files, owner, preview
|
||||
):
|
||||
app, folders = registered_files
|
||||
path = folders[owner] / "report.txt"
|
||||
path.write_bytes(b"Native artifact bytes\n")
|
||||
response = request_file(app, path, preview)
|
||||
assert response.status == "200 OK"
|
||||
assert response.data == path.read_bytes()
|
||||
assert response.headers["Content-Type"] == "text/plain"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("preview", [False, True])
|
||||
@pytest.mark.parametrize("alias", [False, True])
|
||||
def test_unregistered_paths_and_workspace_symlink_escapes_stay_denied(
|
||||
registered_files, tmp_path, alias, preview
|
||||
):
|
||||
app, folders = registered_files
|
||||
unrelated = tmp_path / "helper-unregistered"
|
||||
unrelated.mkdir()
|
||||
path = unrelated / "private.txt"
|
||||
path.write_bytes(b"outside configured roots")
|
||||
if alias:
|
||||
link = folders["helper"] / "linked.txt"
|
||||
try:
|
||||
link.symlink_to(path)
|
||||
except OSError:
|
||||
pytest.skip("symlinks unavailable")
|
||||
path = link
|
||||
response = request_file(app, path, preview)
|
||||
assert response.status == "404 Not Found"
|
||||
assert b"outside configured roots" not in response.data
|
||||
|
||||
|
||||
def test_global_file_route_still_requires_authentication(registered_files, monkeypatch):
|
||||
app, folders = registered_files
|
||||
path = folders["helper"] / "report.txt"
|
||||
path.write_bytes(b"requires login")
|
||||
monkeypatch.setitem(conf(), "web_password", "test-password")
|
||||
response = request_file(app, path, False)
|
||||
assert response.status == "401 Unauthorized"
|
||||
assert b"requires login" not in response.data
|
||||
|
||||
|
||||
def test_preview_route_rejects_unsigned_directory_tokens(registered_files):
|
||||
app, folders = registered_files
|
||||
path = folders["helper"] / "report.txt"
|
||||
path.write_bytes(b"requires signature")
|
||||
url = _common._build_preview_url(str(path))
|
||||
token, name = url.removeprefix("/preview/").split("/", 1)
|
||||
response = app.request("/preview/" + token.rsplit(".", 1)[0] + ".invalid/" + name)
|
||||
assert response.status == "404 Not Found"
|
||||
assert b"requires signature" not in response.data
|
||||
Reference in new issue
Block a user