HKUDS/Vibe-Trading · warning · WorkspaceScopeError

workspace root must stay under {default_root}

Error message

workspace root must stay under {default_root}

What it means

Raised by _scope_from_envelope when an incoming envelope's workspace root resolves outside the gateway's configured workspace_path and default_restrict_to_workspace is enabled. It is a security guard preventing clients from scoping the agent to arbitrary filesystem locations. The raised type is WorkspaceScopeError.

Source

Thrown at agent/src/channelsui/gateway_services.py:131

        """Return the active scope for a message."""
        del chat_running, controls_available
        return self._scope_from_envelope(envelope) or self._scopes.get(chat_id) or self._default_scope()

    def persist_scope(self, chat_id: str, scope: WorkspaceScope) -> None:
        """Persist an in-memory scope for the current gateway process."""
        self._scopes[chat_id] = scope

    def _scope_from_envelope(self, envelope: dict[str, Any]) -> WorkspaceScope | None:
        raw = envelope.get("workspace_scope")
        if not isinstance(raw, dict):
            return None
        root = raw.get("root")
        if not isinstance(root, str) or not root.strip():
            return None
        resolved = Path(root).expanduser().resolve()
        default_root = self.workspace_path.expanduser().resolve()
        if self.default_restrict_to_workspace and not _is_relative_to(resolved, default_root):
            raise WorkspaceScopeError(f"workspace root must stay under {default_root}")
        restrict = raw.get("restrict_to_workspace", self.default_restrict_to_workspace)
        return WorkspaceScope(root=str(resolved), restrict_to_workspace=bool(restrict))


class SimpleHttpRouter:
    """Small HTTP fallback router for WebSocket server requests."""

    def workspace_controls_available(self, connection: Any) -> bool:
        """Return whether workspace controls may be shown to this connection."""
        del connection
        return True

    async def dispatch(self, connection: Any, request: Any) -> Any:
        """Return a compact JSON 404 for non-WebSocket HTTP requests."""
        del request
        return connection.respond(
            404,
            json.dumps({"detail": "not found"}, ensure_ascii=False),

View on GitHub (pinned to 80ffdda44c)

Solutions

  1. Set root in the envelope to a directory inside the gateway workspace_path.
  2. If cross-root access is intentional and trusted, disable default_restrict_to_workspace on the gateway config.
  3. Check for symlinks that resolve outside the root and adjust the path accordingly.

Example fix

# before
envelope = {"root": "/etc"}

# after
envelope = {"root": "/srv/workspace/project-a"}  # inside workspace_path
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

def safe_root(raw_root: str, workspace: str) -> str | None:
    resolved = Path(raw_root).expanduser().resolve()
    ws = Path(workspace).expanduser().resolve()
    try:
        resolved.relative_to(ws)
        return str(resolved)
    except ValueError:
        return None  # caller should reject/fix before sending the envelope

Try / catch

try:
    scope = gateway.scope_for_message(envelope)
except WorkspaceScopeError as e:
    scope = fallback_default_scope()  # fall back to the default workspace root

Prevention

When it happens

Trigger: scope_for_new_chat / scope_for_set_request / scope_for_message receiving a root that is absolute, symlinked, or '..'-escaping beyond the configured workspace root while restrict_to_workspace defaults to true.

Common situations: Client sends a root like '/etc' or '/home/other-user'; symlinks inside the workspace pointing outside; containerized deployment where the client's paths don't match the server's mount layout.

Related errors


AI-assisted analysis of HKUDS/Vibe-Trading@80ffdda44c (2026-08-28). Data as JSON: /api/errors/2ff299525dafc081. Report an issue: GitHub.