diff --git a/backend/app/core/config.py b/backend/app/core/config.py index cec3d5fc..3eb8997b 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -35,13 +35,6 @@ class Settings(BaseSettings): cors_origins: str = "" base_url: str = "" - # Optional: local directory where the backend is allowed to write "preserved" agent - # workspace files (e.g. USER.md/SELF.md/MEMORY.md). If empty, local - # writes are disabled and provisioning relies on the gateway API. - # - # Security note: do NOT point this at arbitrary system paths in production. - local_agent_workspace_root: str = "" - # Database lifecycle db_auto_migrate: bool = False diff --git a/backend/app/services/openclaw/provisioning.py b/backend/app/services/openclaw/provisioning.py index 1a436c15..1f939ba3 100644 --- a/backend/app/services/openclaw/provisioning.py +++ b/backend/app/services/openclaw/provisioning.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio -import hashlib import json import re from abc import ABC, abstractmethod @@ -215,40 +214,6 @@ def _workspace_path(agent: Agent, workspace_root: str) -> str: return f"{root}/workspace-{_slugify(key)}" -def _ensure_workspace_file( - workspace_path: str, - name: str, - content: str, - *, - overwrite: bool = False, -) -> None: - if not workspace_path or not name: - return - # Only write to a dedicated, explicitly-configured local directory. - # Using `gateway.workspace_root` directly here is unsafe. - # CodeQL correctly flags that value because it is DB-backed config. - base_root = (settings.local_agent_workspace_root or "").strip() - if not base_root: - return - base = Path(base_root).expanduser() - - # Derive a stable, safe directory name from the untrusted workspace path. - # This prevents path traversal and avoids writing to arbitrary locations. - digest = hashlib.sha256(workspace_path.encode("utf-8")).hexdigest()[:16] - root = base / f"gateway-workspace-{digest}" - - # Ensure `name` is a plain filename (no path separators). - if Path(name).name != name: - return - path = root / name - if not overwrite and path.exists(): - return - root.mkdir(parents=True, exist_ok=True) - tmp_path = path.with_suffix(f"{path.suffix}.tmp") - tmp_path.write_text(content, encoding="utf-8") - tmp_path.replace(path) - - def _build_context( agent: Agent, board: Board, @@ -795,13 +760,6 @@ class BaseAgentLifecycleManager(ABC): template_overrides=self._template_overrides(), ) - for name in PRESERVE_AGENT_EDITABLE_FILES: - content = rendered.get(name) - if not content: - continue - with suppress(OSError): - _ensure_workspace_file(workspace_path, name, content, overwrite=False) - await self._set_agent_files( agent_id=agent_id, rendered=rendered, diff --git a/backend/tests/test_agent_provisioning_utils.py b/backend/tests/test_agent_provisioning_utils.py index de6f7f46..ac7e1e50 100644 --- a/backend/tests/test_agent_provisioning_utils.py +++ b/backend/tests/test_agent_provisioning_utils.py @@ -8,6 +8,7 @@ from uuid import UUID, uuid4 import pytest import app.services.openclaw.provisioning as agent_provisioning +from app.services.openclaw.agent_service import AgentLifecycleService from app.services.openclaw.shared import GatewayAgentIdentity @@ -65,6 +66,20 @@ def test_agent_key_uses_session_key_when_present(monkeypatch): agent2 = _AgentStub(name="Alice", openclaw_session_id=None) assert agent_provisioning._agent_key(agent2) == "slugged" +def test_workspace_path_preserves_tilde_in_workspace_root(): + # Mission Control accepts a user-entered workspace root (from the UI) and must + # treat it as an opaque string. In particular, we must not expand "~" to a + # filesystem path since that behavior depends on the host environment. + agent = _AgentStub(name="Alice", openclaw_session_id="agent:alice:main") + assert agent_provisioning._workspace_path(agent, "~/.openclaw") == "~/.openclaw/workspace-alice" + + +def test_agent_lifecycle_workspace_path_preserves_tilde_in_workspace_root(): + assert ( + AgentLifecycleService.workspace_path("Alice", "~/.openclaw") + == "~/.openclaw/workspace-alice" + ) + @dataclass class _GatewayStub: