Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Clarify Foundry session path terminology
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 3e5c81ad-75e8-4e92-a883-8bbd676c6c8c
  • Loading branch information
eavanvalkenburg committed Jul 30, 2026
commit 6499d711a3137a2e14170d047c9d643ed11a10f1
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import os
from typing import Literal

from azure.ai.agentserver.core import FoundryAgentRequestContext, get_request_context
from azure.ai.agentserver.core import FoundryAgentRequestContext

_PROTOCOL_V2_REQUIRED_MESSAGE = (
"The hosted environment is running on protocol 1.0.0, but the agent requires protocol 2.0.0. "
Expand Down Expand Up @@ -51,12 +51,3 @@ def validate_foundry_request_context(
"The hosted environment is missing the platform user ID in the request context. "
"Please ensure that the request is coming from a valid Foundry platform service."
)


def request_user_directory_segment() -> str | None:
"""Return the validated platform user ID for an on-disk directory."""
user_id = get_request_context().user_id
if not user_id:
return None
validate_path_segment(user_id, kind="user id")
return user_id
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@

from agent_framework import ExperimentalFeature, FileSessionStore
from agent_framework._feature_stage import experimental
from azure.ai.agentserver.core import get_request_context

from ._request_context import request_user_directory_segment
from ._request_context import validate_path_segment


@experimental(feature_id=ExperimentalFeature.SESSION_STORE)
Expand Down Expand Up @@ -37,19 +38,22 @@ def __init__(

def _session_file_path(self, session_id: str) -> Path:
"""Resolve a snapshot path within the active Foundry user's directory."""
directory_segment = request_user_directory_segment()
candidate_directory = self._storage_root / directory_segment if directory_segment else self._storage_root
session_directory = candidate_directory.resolve()
if session_directory != candidate_directory or not session_directory.is_relative_to(self._storage_root):
raise ValueError(f"Session directory escaped storage directory: '{session_directory}'.")
session_directory.mkdir(parents=True, exist_ok=True)

candidate_path = session_directory / self._session_file_name(session_id)
file_path = candidate_path.resolve()
user_directory = self._storage_root
if user_id := get_request_context().user_id:
validate_path_segment(user_id, kind="user id")
candidate_user_directory = self._storage_root / user_id
user_directory = candidate_user_directory.resolve()
if user_directory != candidate_user_directory or not user_directory.is_relative_to(self._storage_root):
raise ValueError(f"User directory escaped storage directory: '{user_directory}'.")
user_directory.mkdir(parents=True, exist_ok=True)

session_file_name = self._session_file_name(session_id)
candidate_session_file_path = user_directory / session_file_name
session_file_path = candidate_session_file_path.resolve()
if (
file_path != candidate_path
or not file_path.is_relative_to(session_directory)
or not file_path.is_relative_to(self._storage_root)
session_file_path != candidate_session_file_path
or not session_file_path.is_relative_to(user_directory)
or not session_file_path.is_relative_to(self._storage_root)
):
raise ValueError(f"Session path escaped storage directory: {session_id!r}")
return file_path
raise ValueError(f"Session file path escaped user directory: {session_id!r}")
return session_file_path
8 changes: 4 additions & 4 deletions python/packages/foundry_hosting/tests/test_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,7 @@ async def test_scoped_file_store_rejects_symlinked_session_leaf(self, tmp_path:
except OSError as exc:
pytest.skip(f"Symlinks are not available: {exc}")

with _request_context(user_id="user-A"), pytest.raises(ValueError, match="escaped storage directory"):
with _request_context(user_id="user-A"), pytest.raises(ValueError, match="escaped user directory"):
await store.get("conversation-1")

async def test_scoped_file_store_rejects_symlinked_session_leaf_within_user_directory(
Expand All @@ -556,7 +556,7 @@ async def test_scoped_file_store_rejects_symlinked_session_leaf_within_user_dire
except OSError as exc:
pytest.skip(f"Symlinks are not available: {exc}")

with _request_context(user_id="user-A"), pytest.raises(ValueError, match="escaped storage directory"):
with _request_context(user_id="user-A"), pytest.raises(ValueError, match="escaped user directory"):
await store.get("conversation-1")

async def test_scoped_file_store_rejects_symlinked_isolation_directory(self, tmp_path: Path) -> None:
Expand All @@ -569,7 +569,7 @@ async def test_scoped_file_store_rejects_symlinked_isolation_directory(self, tmp
except OSError as exc:
pytest.skip(f"Symlinks are not available: {exc}")

with _request_context(user_id="user-A"), pytest.raises(ValueError, match="Session directory escaped"):
with _request_context(user_id="user-A"), pytest.raises(ValueError, match="User directory escaped"):
await store.get("conversation-1")

async def test_scoped_file_store_rejects_symlinked_isolation_directory_within_storage_root(
Expand All @@ -584,7 +584,7 @@ async def test_scoped_file_store_rejects_symlinked_isolation_directory_within_st
except OSError as exc:
pytest.skip(f"Symlinks are not available: {exc}")

with _request_context(user_id="user-A"), pytest.raises(ValueError, match="Session directory escaped"):
with _request_context(user_id="user-A"), pytest.raises(ValueError, match="User directory escaped"):
await store.get("conversation-1")

@pytest.mark.parametrize("user_id", ["../../escape", "/tmp/escape", "user/subdirectory", "user\\subdirectory"])
Expand Down