{"record":{"id":"2ff299525dafc081","repo":"HKUDS/Vibe-Trading","slug":"workspace-root-must-stay-under-default-root","errorCode":null,"errorMessage":"workspace root must stay under {default_root}","messagePattern":"workspace root must stay under (.+?)","errorType":"exception","errorClass":"WorkspaceScopeError","httpStatus":null,"severity":"warning","filePath":"agent/src/channelsui/gateway_services.py","lineNumber":131,"sourceCode":"        \"\"\"Return the active scope for a message.\"\"\"\n        del chat_running, controls_available\n        return self._scope_from_envelope(envelope) or self._scopes.get(chat_id) or self._default_scope()\n\n    def persist_scope(self, chat_id: str, scope: WorkspaceScope) -> None:\n        \"\"\"Persist an in-memory scope for the current gateway process.\"\"\"\n        self._scopes[chat_id] = scope\n\n    def _scope_from_envelope(self, envelope: dict[str, Any]) -> WorkspaceScope | None:\n        raw = envelope.get(\"workspace_scope\")\n        if not isinstance(raw, dict):\n            return None\n        root = raw.get(\"root\")\n        if not isinstance(root, str) or not root.strip():\n            return None\n        resolved = Path(root).expanduser().resolve()\n        default_root = self.workspace_path.expanduser().resolve()\n        if self.default_restrict_to_workspace and not _is_relative_to(resolved, default_root):\n            raise WorkspaceScopeError(f\"workspace root must stay under {default_root}\")\n        restrict = raw.get(\"restrict_to_workspace\", self.default_restrict_to_workspace)\n        return WorkspaceScope(root=str(resolved), restrict_to_workspace=bool(restrict))\n\n\nclass SimpleHttpRouter:\n    \"\"\"Small HTTP fallback router for WebSocket server requests.\"\"\"\n\n    def workspace_controls_available(self, connection: Any) -> bool:\n        \"\"\"Return whether workspace controls may be shown to this connection.\"\"\"\n        del connection\n        return True\n\n    async def dispatch(self, connection: Any, request: Any) -> Any:\n        \"\"\"Return a compact JSON 404 for non-WebSocket HTTP requests.\"\"\"\n        del request\n        return connection.respond(\n            404,\n            json.dumps({\"detail\": \"not found\"}, ensure_ascii=False),","sourceCodeStart":113,"sourceCodeEnd":149,"githubUrl":"https://github.com/HKUDS/Vibe-Trading/blob/80ffdda44c5c4db0dd84d70e051cca591cea67df/agent/src/channelsui/gateway_services.py#L113-L149","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Set root in the envelope to a directory inside the gateway workspace_path.","If cross-root access is intentional and trusted, disable default_restrict_to_workspace on the gateway config.","Check for symlinks that resolve outside the root and adjust the path accordingly."],"exampleFix":"# before\nenvelope = {\"root\": \"/etc\"}\n\n# after\nenvelope = {\"root\": \"/srv/workspace/project-a\"}  # inside workspace_path","handlingStrategy":"validation","validationCode":"from pathlib import Path\n\ndef safe_root(raw_root: str, workspace: str) -> str | None:\n    resolved = Path(raw_root).expanduser().resolve()\n    ws = Path(workspace).expanduser().resolve()\n    try:\n        resolved.relative_to(ws)\n        return str(resolved)\n    except ValueError:\n        return None  # caller should reject/fix before sending the envelope","typeGuard":null,"tryCatchPattern":"try:\n    scope = gateway.scope_for_message(envelope)\nexcept WorkspaceScopeError as e:\n    scope = fallback_default_scope()  # fall back to the default workspace root","preventionTips":["Always send roots relative to the known workspace_path","Resolve symlinks client-side before sending the envelope","Keep restrict_to_workspace enabled except in fully trusted deployments"],"tags":["workspace","path-security","validation","gateway"],"backgroundTag":"path-traversal-validation","analyzedSha":"80ffdda44c5c4db0dd84d70e051cca591cea67df","analyzedAt":"2026-08-28T12:46:38.989Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}