agentscope-ai/agentscope · error · ValueError

Invalid blob key: {key!r}

Error message

Invalid blob key: {key!r}

What it means

LocalBlobStore._path_for validates every blob key before mapping it to a filesystem path under the store's root. It raises ValueError('Invalid blob key: ...') when the key is empty, starts with '/' (absolute path), or contains a '..' path component. This is a path-traversal guard shared by write_stream, open, size, delete, and exists.

Source

Thrown at src/agentscope/app/rag/blob_store/_local.py:69

    def _path_for(self, key: str) -> Path:
        """Resolve a backend-relative key to an absolute filesystem path.

        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.

View on GitHub (pinned to e90f1c7592)

Solutions

  1. Pass a relative key like 'kb123/chunk.bin', never a URI or absolute path
  2. Strip the scheme first: use the store's _key_from_uri helper or uri.removeprefix('local://') if you hold a URI
  3. Validate/sanitize user-supplied keys: reject empty, absolute, and '..'-containing segments before calling the store
  4. Normalize with PurePosixPath(key).parts to inspect segments

Example fix

# before
await store.write_stream('/data/abc.bin', stream)  # ValueError: Invalid blob key

# after
await store.write_stream('data/abc.bin', stream)
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import PurePosixPath

def valid_blob_key(key: str) -> bool:
    if not key or key.startswith('/'):
        return False
    return '..' not in PurePosixPath(key).parts

assert valid_blob_key(key), f'bad blob key {key!r}'

Type guard

from pathlib import PurePosixPath

def is_valid_blob_key(key: str) -> bool:
    return bool(key) and not key.startswith('/') and '..' not in PurePosixPath(key).parts

Try / catch

try:
    await store.write_stream(key, stream)
except ValueError as e:
    if 'Invalid blob key' in str(e):
        ...  # sanitize key and retry
    raise

Prevention

When it happens

Trigger: Calling any LocalBlobStore blob method with key='' , key='/abs/path', or key='a/../../etc/passwd'. Windows-style components or URLs (e.g. 'local://foo') passed as keys also fail the checks.

Common situations: Passing a full local:// URI where a bare key is expected; constructing keys from unvalidated user input; joining paths with a leading slash; sending filenames containing '..' from an upload form.

Related errors


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