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

  1. Ensure the store's root is canonical: pass Path(root).resolve() when constructing LocalBlobStore
  2. Remove or account for symlinks inside the blob directory
  3. Never let keys contain user-controlled path segments; map them to opaque ids (uuid/hash) instead
  4. 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

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


AI-assisted analysis of agentscope-ai/agentscope@e90f1c7592 (2026-08-28). Data as JSON: /api/errors/baf56bf547262b69. Report an issue: GitHub.