agentscope-ai/agentscope · error · ValueError
Blob key {key!r} escapes the root directory.
Error message
Blob key {key!r} escapes the root directory. What it means
The second stage of LocalBlobStore's path guard: after resolving (self._root / key), it verifies the resolved path is still under _root. If resolution (symlinks, or clever key construction) escapes the root, it raises ValueError('Blob key ... escapes the root directory.'). This catches traversal the syntactic '..' check in the first stage misses.
Source
Thrown at src/agentscope/app/rag/blob_store/_local.py:72
Rejects keys that try to escape :attr:`_root` via ``..`` or
absolute paths. Callers pick keys server-side so this is
defensive rather than a primary trust boundary, but we still
refuse to write outside the root.
Args:
key (`str`):
Backend-relative key.
Returns:
`Path`:
The absolute filesystem path within :attr:`_root`.
"""
if not key or key.startswith("/") or ".." in Path(key).parts:
raise ValueError(f"Invalid blob key: {key!r}")
path = (self._root / key).resolve()
if self._root not in path.parents and path != self._root:
raise ValueError(f"Blob key {key!r} escapes the root directory.")
return path
def _key_from_uri(self, uri: str) -> str:
"""Extract the backend-relative key from a ``local://`` URI."""
if not uri.startswith(_SCHEME):
raise ValueError(f"Not a local blob URI: {uri!r}")
return uri[len(_SCHEME) :]
async def write_stream(self, key: str, stream: IO[bytes]) -> str:
"""Copy ``stream`` into the blob at ``key`` in 1 MiB chunks.
Creates intermediate directories as needed. Existing blobs at
the same key are overwritten — keys are generated server-side
from document ids, so collisions only happen on intentional
re-uploads.
Args:
key (`str`):View on GitHub (pinned to e90f1c7592)
Solutions
- Ensure the store's root is canonical: pass Path(root).resolve() when constructing LocalBlobStore
- Remove or account for symlinks inside the blob directory
- Never let keys contain user-controlled path segments; map them to opaque ids (uuid/hash) instead
- If you intentionally relocated the root, recreate the store with the new resolved path
Example fix
# before
store = LocalBlobStore(root=Path('/tmp/blobs')) # /tmp may be a symlink
# after
from pathlib import Path
store = LocalBlobStore(root=Path('/tmp/blobs').resolve()) Defensive patterns
Strategy: validation
Validate before calling
from pathlib import Path root = Path(root_dir).resolve() store = LocalBlobStore(root=root) # canonical root avoids false escape errors
Type guard
null
Try / catch
try:
await store.write_stream(key, stream)
except ValueError as e:
if 'escapes the root' in str(e):
... # reject key / fix symlinked root
raise Prevention
- Construct LocalBlobStore with an already-resolved root path
- Avoid symlinks inside the blob directory
- Treat 'escapes the root' as a security signal: log and reject, never rewrite keys to pass
When it happens
Trigger: A key that resolves outside _root because _root itself is reached via a symlink, or a key whose segments symlink upward; can also fire when the key is exactly '.' (path == _root is allowed, but siblings are not) or when _root is a relative path that resolves differently than expected.
Common situations: Placing the blob root inside a symlinked directory (e.g. /tmp → /private/tmp on macOS) while comparing against the unresolved root; container mounts where the root path resolves to a different real path; keys containing symlinks created by other tooling.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- Unsafe upload path: {entry.path!r}
- Invalid blob key: {key!r}
- Bubblewrap workdir escapes basedir.
- workspace_id {workspace_id!r} escapes the workspace base dir
- unsafe tar member: ' + m.name
AI-assisted analysis of agentscope-ai/agentscope@e90f1c7592 (2026-08-28).
Data as JSON: /api/errors/baf56bf547262b69.
Report an issue: GitHub.