langchain-ai/deepagents · error · ValueError

media path escapes outbound root: {path}

Error message

media path escapes outbound root: {path}

What it means

ValueError raised by resolve_bounded_media_path when the strictly-resolved real path of the file is not contained within the resolved outbound root. This catches symlink escapes: the file itself may exist, but resolving symlinks leads outside the root.

Source

Thrown at libs/talon/deepagents_talon/media.py:234

        require_relative
        and (path.is_absolute() or windows_path.is_absolute() or is_windows_drive_path)
    ):
        msg = f"media path must be a local relative path under the outbound root: {path}"
        raise ValueError(msg)
    if ".." in path.parts or ".." in windows_path.parts:
        msg = f"media path must not contain parent-directory traversal: {path}"
        raise ValueError(msg)

    root_resolved = root.expanduser().resolve()
    candidate_input = root_resolved / path if not path.is_absolute() else path
    try:
        candidate = candidate_input.resolve(strict=True)
    except OSError as exc:
        msg = f"media file is unavailable: {path}"
        raise ValueError(msg) from exc
    if not candidate.is_relative_to(root_resolved):
        msg = f"media path escapes outbound root: {path}"
        raise ValueError(msg)
    if not candidate.is_file():
        msg = f"media path is not a regular file: {path}"
        raise ValueError(msg)
    return candidate


def _document_parts(paths: list[Path]) -> list[str]:
    parts: list[str] = []
    for path in paths:
        if path.suffix.lower() not in READABLE_DOCUMENT_EXTENSIONS:
            parts.append(f"_(Received unsupported document attachment: {path.name}.)_")
            continue
        try:
            if path.stat().st_size > MAX_TEXT_DOCUMENT_BYTES:
                parts.append(f"_(Document attachment is too large to read inline: {path.name}.)_")
                continue
            content = path.read_text(encoding="utf-8", errors="replace")
        except OSError:

View on GitHub (pinned to a1af029e6e)

Solutions

  1. Copy the file into the outbound root instead of symlinking it
  2. Point the symlink target inside the outbound root
  3. If intentional, adjust the root to the common ancestor containing the real file

Example fix

// before
ln -s /var/data/shared.png $OUTBOUND_ROOT/shared.png  # symlink escapes root
// after
cp /var/data/shared.png $OUTBOUND_ROOT/shared.png  # real file inside root
Defensive patterns

Strategy: validation

Validate before calling

root_resolved = root.expanduser().resolve()
try:
    candidate = (root_resolved / relative).resolve(strict=True)
except OSError:
    candidate = None
if candidate is None or not candidate.is_relative_to(root_resolved):
    raise ValueError(f"path escapes root: {relative}")

Type guard

def stays_within_root(p: Path, root: Path) -> bool:
    try:
        return p.expanduser().resolve(strict=True).is_relative_to(root.expanduser().resolve())
    except OSError:
        return False

Try / catch

try:
    resolved = resolve_bounded_media_path(path, root)
except ValueError as exc:
    logger.warning("Media escapes outbound root: %s", exc)
    return None

Prevention

When it happens

Trigger: A symlink inside the outbound root pointing to /etc/passwd or any file outside root; hardlinked or symlinked directories resolving outward.

Common situations: Symlinking shared assets into the outbound folder for convenience; CI artifacts that are symlinks into a cache directory; container volume setups with linked paths.

Related errors


AI-assisted analysis of langchain-ai/deepagents@a1af029e6e (2026-08-29). Data as JSON: /api/errors/044de6ed9b7a55e5. Report an issue: GitHub.