[security] fix(workspace): restrict session workspaces to trusted roots (#416)

* fix(workspace): restrict session workspaces to trusted roots

* fix: use boot-time DEFAULT_WORKSPACE instead of profile default for trusted workspace root

_profile_default_workspace() reads the agent's terminal.cwd which may differ
from the WebUI's configured workspace root. Use _BOOT_DEFAULT_WORKSPACE (which
respects HERMES_WEBUI_DEFAULT_WORKSPACE for test isolation) to stay consistent
with how new_session() seeds the initial workspace.

* docs: v0.50.34 release — version badge and CHANGELOG

---------

Co-authored-by: hinotoi-agent <paperlantern.agent@gmail.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
This commit is contained in:
nesquena-hermes
2026-04-13 23:44:03 -07:00
committed by GitHub
parent a5abe51cc5
commit 2a7a5ddfaf
9 changed files with 152 additions and 45 deletions

View File

@@ -180,6 +180,7 @@ from api.workspace import (
list_dir,
read_file_content,
safe_resolve_ws,
resolve_trusted_workspace,
)
from api.upload import handle_upload, handle_transcribe
from api.streaming import _sse, _run_agent_streaming, cancel_stream
@@ -638,7 +639,11 @@ def handle_post(handler, parsed) -> bool:
body = read_body(handler)
if parsed.path == "/api/session/new":
s = new_session(workspace=body.get("workspace"), model=body.get("model"))
try:
workspace = str(resolve_trusted_workspace(body.get("workspace"))) if body.get("workspace") else None
except ValueError as e:
return bad(handler, str(e))
s = new_session(workspace=workspace, model=body.get("model"))
return j(handler, {"session": s.compact() | {"messages": s.messages}})
if parsed.path == "/api/sessions/cleanup":
@@ -713,7 +718,10 @@ def handle_post(handler, parsed) -> bool:
s = get_session(body["session_id"])
except KeyError:
return bad(handler, "Session not found", 404)
new_ws = str(Path(body.get("workspace", s.workspace)).expanduser().resolve())
try:
new_ws = str(resolve_trusted_workspace(body.get("workspace", s.workspace)))
except ValueError as e:
return bad(handler, str(e))
s.workspace = new_ws
s.model = body.get("model", s.model)
s.save()
@@ -1668,7 +1676,10 @@ def _handle_chat_start(handler, body):
if not msg:
return bad(handler, "message is required")
attachments = [str(a) for a in (body.get("attachments") or [])][:20]
workspace = str(Path(body.get("workspace") or s.workspace).expanduser().resolve())
try:
workspace = str(resolve_trusted_workspace(body.get("workspace") or s.workspace))
except ValueError as e:
return bad(handler, str(e))
model = body.get("model") or s.model
stream_id = uuid.uuid4().hex
s.workspace = workspace
@@ -2016,11 +2027,10 @@ def _handle_workspace_add(handler, body):
name = body.get("name", "").strip()
if not path_str:
return bad(handler, "path is required")
p = Path(path_str).expanduser().resolve()
if not p.exists():
return bad(handler, f"Path does not exist: {p}")
if not p.is_dir():
return bad(handler, f"Path is not a directory: {p}")
try:
p = resolve_trusted_workspace(path_str)
except ValueError as e:
return bad(handler, str(e))
wss = load_workspaces()
if any(w["path"] == str(p) for w in wss):
return bad(handler, "Workspace already in list")