{"record":{"id":"0061886c459e1def","repo":"OpenBMB/ChatDev","slug":"script-path-is-outside-workspace-root","errorCode":null,"errorMessage":"script path is outside workspace root","messagePattern":"script path is outside workspace root","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"functions/function_calling/uv_related.py","lineNumber":60,"sourceCode":"        if ctx is None:\n            raise ValueError(\"_context is required for uv tools\")\n        self.workspace_root = self._require_workspace(ctx.get(\"python_workspace_root\"))\n        self._raw_ctx = ctx\n\n    @staticmethod\n    def _require_workspace(raw_path: Any) -> Path:\n        if raw_path is None:\n            raise ValueError(\"python_workspace_root missing from _context\")\n        path = Path(raw_path).expanduser().resolve()\n        path.mkdir(parents=True, exist_ok=True)\n        return path\n\n    def resolve_under_workspace(self, relative_path: str | Path) -> Path:\n        candidate = Path(relative_path)\n        absolute = candidate if candidate.is_absolute() else self.workspace_root / candidate\n        absolute = absolute.expanduser().resolve()\n        if self.workspace_root not in absolute.parents and absolute != self.workspace_root:\n            raise ValueError(\"script path is outside workspace root\")\n        return absolute\n\n\ndef _validate_packages(packages: Sequence[str]) -> List[str]:\n    normalized: List[str] = []\n    for pkg in packages:\n        if not isinstance(pkg, str):\n            raise ValueError(\"package entries must be strings\")\n        stripped = pkg.strip()\n        if not stripped:\n            raise ValueError(\"package names cannot be empty\")\n        if not _SAFE_PACKAGE_RE.match(stripped):\n            raise ValueError(f\"unsafe characters detected in package spec {pkg}\")\n        if stripped.startswith(\"-\"):\n            raise ValueError(f\"flags are not allowed in packages list: {pkg}\")\n        normalized.append(stripped)\n    if not normalized:\n        raise ValueError(\"at least one package is required\")","sourceCodeStart":42,"sourceCodeEnd":78,"githubUrl":"https://github.com/OpenBMB/ChatDev/blob/4fb2db0ea90375ce1059f44fe03ffbd191a7a169/functions/function_calling/uv_related.py#L42-L78","documentation":"WorkspaceCommandContext.resolve_under_workspace confines all paths to the workspace root: it resolves the candidate absolutely and rejects it unless it equals the root or is a descendant of it. This is a sandbox guarantee so uv-run scripts cannot escape the workspace via ../ traversal or absolute paths. Note resolve() also collapses symlinks, so symlinked locations resolving outside the root are rejected too.","triggerScenarios":"Passing a script path like \"../outside/run.py\" or \"/etc/run.py\" to uv_run; a path inside the workspace that is a symlink to a directory outside it; path built by joining user input that contains .. segments.","commonSituations":"Monorepo setups where the script lives in a sibling directory; symlinked workspaces (e.g. /tmp symlink on macOS) where resolve() escapes the root; naive path joins of untrusted user input.","solutions":["Move or copy the script under the workspace root and reference it relatively","Replace symlinks that resolve outside the workspace with real files/directories inside it","Normalize and reject '..' segments in user-supplied paths before building the final path"],"exampleFix":"# before\nuv_run(script=\"../shared/run.py\")\n# after\nuv_run(script=\"shared/run.py\")  # shared/ moved/copied under workspace root","handlingStrategy":"validation","validationCode":"from pathlib import Path\n\ndef under_root(root, p) -> bool:\n    root = Path(root).resolve()\n    q = (root / p if not Path(p).is_absolute() else Path(p)).resolve()\n    return q == root or root in q.parents\n\nif not under_root(ws_root, script):\n    script = copy_into_workspace(ws_root, script)","typeGuard":"def is_within_workspace(root, candidate) -> bool:\n    root = Path(root).expanduser().resolve()\n    c = Path(candidate)\n    c = c if c.is_absolute() else root / c\n    c = c.expanduser().resolve()\n    return c == root or root in c.parents","tryCatchPattern":"try:\n    uv_run(script=script, _context=ctx)\nexcept ValueError as e:\n    if \"outside workspace root\" in str(e):\n        raise ValueError(f\"relocate {script} inside the workspace\") from e\n    raise","preventionTips":["Reject '..' segments in user-supplied script paths","Avoid symlinks inside the workspace that point outside it","Keep all runnable scripts under the workspace root"],"tags":["security","path-traversal","uv","sandbox"],"backgroundTag":"path-outside-allowed-root","analyzedSha":"4fb2db0ea90375ce1059f44fe03ffbd191a7a169","analyzedAt":"2026-08-27T14:35:29.622Z","schemaVersion":2},"datasetVersion":"2026-08-27T19:17:21.184Z"}