Skip to content

Windows: file:///C:/... artifact URIs are resolved to the wrong filesystem path #5486

@cyrus-mz

Description

@cyrus-mz

Description

On Windows, --artifact_service_uri with a canonical local file URI such as file:///C:/... is parsed incorrectly for FileArtifactService. The URI path is passed directly from urlparse(uri).path into Path(...) without Windows URI-to-filesystem normalization. As a result, the artifact root is created in the wrong location, with the path duplicated under the current working directory.

Steps to Reproduce:

  1. Install google-adk on Windows.
  2. Use any valid ADK app directory, for example, a minimal single-agent app directory, as [AGENTS_DIR].
  3. Start the ADK API server from a project directory with:
    adk api_server `
      --artifact_service_uri "file:///C:/Users/user1/projects/agent1/runtime/artifacts" `
      [AGENTS_DIR]
  4. Inspect the created artifact directory on disk.

Expected Behavior:
The artifact root should be created at:
C:\Users\user1\projects\agent1\runtime\artifacts

Observed Behavior:
The artifact root is created in the wrong location:
C:\Users\user1\projects\agent1\Users\user1\projects\agent1\runtime\artifacts

I traced this to google/adk/cli/service_registry.py, where file_artifact_factory() does:

  def file_artifact_factory(uri: str, **_):
    from ..artifacts.file_artifact_service import FileArtifactService

    parsed_uri = urlparse(uri)
    if parsed_uri.netloc not in ("", "localhost"):
      raise ValueError(
          "file:// artifact URIs must reference the local filesystem."
      )
    if not parsed_uri.path:
      raise ValueError("file:// artifact URIs must include a path component.")
    artifact_path = Path(unquote(parsed_uri.path))
    return FileArtifactService(root_dir=artifact_path)

On Windows, parsed_uri.path for file:///C:/... is still a file-URI path, not a native Windows filesystem path, but it needs Windows-specific normalization before constructing Path(...).

I locally patched it with:

from urllib.request import url2pathname
import os

def file_artifact_factory(uri: str, **_):
    from ..artifacts.file_artifact_service import FileArtifactService

    parsed_uri = urlparse(uri)
    if parsed_uri.netloc not in ("", "localhost"):
        raise ValueError(
            "file:// artifact URIs must reference the local filesystem."
        )
    if not parsed_uri.path:
        raise ValueError("file:// artifact URIs must include a path component.")

    artifact_path_str = unquote(parsed_uri.path)
    if os.name == "nt":
        artifact_path_str = url2pathname(artifact_path_str)

    artifact_path = Path(artifact_path_str)
    return FileArtifactService(root_dir=artifact_path)

After this patch, the artifact root is created in the correct directory.

Environment Details:

ADK Library Version (pip show google-adk): 1.31.1
Desktop OS: Windows 11
Python Version (python -V): 3.13.4

Model Information:

Are you using LiteLLM: No
Which model is being used: gemini-2.5-flash-lite

Additional Context:
This seems to be a Windows-specific URI-to-path conversion issue in the file_artifact_factory(), not in FileArtifactService itself.

I also noticed a workaround-like behavior: if the drive prefix is omitted and a non-canonical path form is used (e.g., instead of file:///C:/User/..., use this file:///User/...), the current code may appear to work because resolve() in the following snippet from the FileArtifactService class, which resides in google/adk/artifacts/file_artifact_service.py, reconstructs a drive-rooted path. However, I do not think that should be considered the supported or correct behavior. The canonical Windows file URI form file:///C:/... should work.

class FileArtifactService(BaseArtifactService):
  """Stores filesystem-backed artifacts beneath a configurable root directory."""

  # Storage layout matches the cloud and in-memory services:
  # root/
  # └── users/
  #     └── {user_id}/
  #         ├── sessions/
  #         │   └── {session_id}/
  #         │       └── artifacts/
  #         │           └── {artifact_path}/  # derived from filename
  #         │               └── versions/
  #         │                   └── {version}/
  #         │                       ├── {original_filename}
  #         │                       └── metadata.json
  #         └── artifacts/
  #             └── {artifact_path}/...
  #
  # Artifact paths are derived from the provided filenames: separators create
  # nested directories, and path traversal is rejected to keep the layout
  # portable across filesystems. `{artifact_path}` therefore mirrors the
  # sanitized, scope-relative path derived from each filename.

  def __init__(self, root_dir: Path | str):
    """Initializes the file-based artifact service.

    Args:
      root_dir: The directory that will contain artifact data.
    """
    self.root_dir = Path(root_dir).expanduser().resolve()
    self.root_dir.mkdir(parents=True, exist_ok=True)

Minimal Reproduction Code:

from urllib.parse import urlparse, unquote
from pathlib import Path

uri = "file:///C:/Users/user1/projects/agent1/runtime/artifacts"
parsed_uri = urlparse(uri)
print(parsed_uri.path) # Output-> /C:/Users/user1/projects/agent1/runtime/artifacts
print(Path(unquote(parsed_uri.path)).resolve()) # Output-> C:Users\user1\projects\agent1\runtime\artifacts

On Windows, this shows that the URI path is not being normalized to a proper native filesystem path before Path(...) is used.

Metadata

Metadata

Labels

request clarification[Status] The maintainer need clarification or more information from the authorservices[Component] This issue is related to runtime services, e.g. sessions, memory, artifacts, etc

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions