From f47a2190fe6c32fe6593c99f7e9eb7999fe81d71 Mon Sep 17 00:00:00 2001 From: Amit Chauhan <70937115+achauhan-scc@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:36:36 +0530 Subject: [PATCH 1/4] adding support meta data flow --- .../agentframework/_ai_agent_adapter.py | 14 ++++++++++++++ .../azure/ai/agentserver/core/server/base.py | 14 +++++++++++++- .../core/server/common/agent_run_context.py | 13 ++++++++++++- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/_ai_agent_adapter.py b/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/_ai_agent_adapter.py index 21bb5c28c88c..249f4447bfdb 100644 --- a/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/_ai_agent_adapter.py +++ b/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/_ai_agent_adapter.py @@ -54,6 +54,20 @@ async def agent_run( # pylint: disable=too-many-statements request_input, agent_thread=agent_thread) logger.debug("Transformed input message type: %s", type(message)) + + # Attach per-request context to the agent instance so tools can access it + # without LLM involvement. Combines HTTP headers and request metadata. + request_context: dict[str, Any] = {} + if context.headers: + request_context.update(context.headers) + # Extract user token from request body metadata (Vnext infra strips auth + # headers, so the REST API layer embeds the token in metadata instead). + metadata = context.raw_payload.get("metadata", {}) + if isinstance(metadata, dict) and metadata.get("user_token"): + request_context["authorization"] = metadata["user_token"] + logger.info("[DEBUG] Found user_token in request metadata") + self._agent._request_headers = request_context # type: ignore[attr-defined] + # Use split converters if context.stream: return self._run_streaming_updates( diff --git a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/server/base.py b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/server/base.py index e2f83ff5d1c6..eb3680c4a8ba 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/server/base.py +++ b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/server/base.py @@ -52,6 +52,10 @@ def __init__(self, app: ASGIApp, agent: Optional['FoundryCBAgent'] = None): super().__init__(app) self.agent = agent + # Headers to capture from the incoming HTTP request and make available + # to the agent invocation via AgentRunContext.headers. + _PASSTHROUGH_HEADERS = frozenset({"authorization", "x-request-id", "x-user-authorization"}) + async def dispatch(self, request: Request, call_next): if request.url.path in ("/runs", "/responses"): try: @@ -61,7 +65,15 @@ async def dispatch(self, request: Request, call_next): logger.error(f"Invalid JSON payload: {e}") return JSONResponse({"error": f"Invalid JSON payload: {e}"}, status_code=400) try: - request.state.agent_run_context = AgentRunContext(payload) + # Log all incoming headers for debugging + logger.info(f"[DEBUG] All request headers: {dict(request.headers)}") + headers = { + k: v + for k, v in request.headers.items() + if k.lower() in self._PASSTHROUGH_HEADERS + } + logger.info(f"[DEBUG] Captured passthrough headers: {list(headers.keys())}") + request.state.agent_run_context = AgentRunContext(payload, headers=headers) self.set_run_context_to_context_var(request.state.agent_run_context) except Exception as e: logger.error(f"Context build failed: {e}.", exc_info=True) diff --git a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/server/common/agent_run_context.py b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/server/common/agent_run_context.py index 87c32926bde4..432079a137b2 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/server/common/agent_run_context.py +++ b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/server/common/agent_run_context.py @@ -16,13 +16,14 @@ class AgentRunContext: """ :meta private: """ - def __init__(self, payload: dict) -> None: + def __init__(self, payload: dict, headers: dict | None = None) -> None: self._raw_payload = payload self._request = _deserialize_create_response(payload) self._id_generator = FoundryIdGenerator.from_request(payload) self._response_id = self._id_generator.response_id self._conversation_id = self._id_generator.conversation_id self._stream = self.request.get("stream", False) + self._headers = headers or {} @property def raw_payload(self) -> dict: @@ -48,6 +49,16 @@ def conversation_id(self) -> Optional[str]: def stream(self) -> bool: return self._stream + @property + def headers(self) -> dict: + """HTTP request headers captured from the incoming request. + + Useful for extracting authentication tokens or other per-request metadata + that should be passed through to the agent or its tools without being + exposed to the LLM. + """ + return self._headers + def get_agent_id_object(self) -> AgentId: agent = self.request.get("agent") if not agent: From ea03fc7e6868d53fc4b13fa850f20c886b34d83d Mon Sep 17 00:00:00 2001 From: Amit Chauhan <70937115+achauhan-scc@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:29:15 +0530 Subject: [PATCH 2/4] add multi-part user_token reassembly for metadata > 512 chars Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agentframework/_ai_agent_adapter.py | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/_ai_agent_adapter.py b/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/_ai_agent_adapter.py index 249f4447bfdb..5f0e3e64916b 100644 --- a/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/_ai_agent_adapter.py +++ b/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/_ai_agent_adapter.py @@ -37,6 +37,34 @@ def __init__(self, agent: AgentProtocol, super().__init__(credentials, thread_repository, project_endpoint=project_endpoint, **kwargs) self._agent = agent + @staticmethod + def _extract_user_token(metadata: dict) -> str | None: + """Reassemble user token from metadata. + + Supports: + - Single key: metadata["user_token"] + - Multi-part: metadata["user_token_1"], ["user_token_2"], … with + metadata["user_token_parts"] indicating the chunk count. + """ + parts_count = metadata.get("user_token_parts") + if parts_count is not None: + try: + n = int(parts_count) + except (ValueError, TypeError): + return metadata.get("user_token") + if n == 1 and "user_token" in metadata: + return metadata["user_token"] + chunks = [] + for i in range(1, n + 1): + chunk = metadata.get(f"user_token_{i}") + if chunk is None: + logger.warning("Missing user_token_%d; expected %d parts", i, n) + return None + chunks.append(chunk) + return "".join(chunks) + # Fallback: single user_token key (backwards compat) + return metadata.get("user_token") + async def agent_run( # pylint: disable=too-many-statements self, context: AgentRunContext ) -> Union[ @@ -62,10 +90,14 @@ async def agent_run( # pylint: disable=too-many-statements request_context.update(context.headers) # Extract user token from request body metadata (Vnext infra strips auth # headers, so the REST API layer embeds the token in metadata instead). + # Supports both single-key (user_token) and multi-part split tokens + # (user_token_1, user_token_2, … + user_token_parts) for tokens > 512 chars. metadata = context.raw_payload.get("metadata", {}) - if isinstance(metadata, dict) and metadata.get("user_token"): - request_context["authorization"] = metadata["user_token"] - logger.info("[DEBUG] Found user_token in request metadata") + if isinstance(metadata, dict): + user_token = self._extract_user_token(metadata) + if user_token: + request_context["authorization"] = f"Bearer {user_token}" + logger.info("[DEBUG] Reassembled user_token from metadata (%d chars)", len(user_token)) self._agent._request_headers = request_context # type: ignore[attr-defined] # Use split converters From 0e9f2263ae93c80bb4d0fcd56528ac6efaf322b5 Mon Sep 17 00:00:00 2001 From: Amit Chauhan <70937115+achauhan-scc@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:43:23 +0530 Subject: [PATCH 3/4] refactor: pass all metadata through generically instead of token-specific logic The hosting framework now merges all request metadata into request_context as-is, letting the agent/tool layer handle interpretation (e.g. token reassembly for the 512-char metadata limit). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agentframework/_ai_agent_adapter.py | 37 +------------------ 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/_ai_agent_adapter.py b/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/_ai_agent_adapter.py index 5f0e3e64916b..e7979071e3a1 100644 --- a/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/_ai_agent_adapter.py +++ b/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/_ai_agent_adapter.py @@ -37,34 +37,6 @@ def __init__(self, agent: AgentProtocol, super().__init__(credentials, thread_repository, project_endpoint=project_endpoint, **kwargs) self._agent = agent - @staticmethod - def _extract_user_token(metadata: dict) -> str | None: - """Reassemble user token from metadata. - - Supports: - - Single key: metadata["user_token"] - - Multi-part: metadata["user_token_1"], ["user_token_2"], … with - metadata["user_token_parts"] indicating the chunk count. - """ - parts_count = metadata.get("user_token_parts") - if parts_count is not None: - try: - n = int(parts_count) - except (ValueError, TypeError): - return metadata.get("user_token") - if n == 1 and "user_token" in metadata: - return metadata["user_token"] - chunks = [] - for i in range(1, n + 1): - chunk = metadata.get(f"user_token_{i}") - if chunk is None: - logger.warning("Missing user_token_%d; expected %d parts", i, n) - return None - chunks.append(chunk) - return "".join(chunks) - # Fallback: single user_token key (backwards compat) - return metadata.get("user_token") - async def agent_run( # pylint: disable=too-many-statements self, context: AgentRunContext ) -> Union[ @@ -88,16 +60,9 @@ async def agent_run( # pylint: disable=too-many-statements request_context: dict[str, Any] = {} if context.headers: request_context.update(context.headers) - # Extract user token from request body metadata (Vnext infra strips auth - # headers, so the REST API layer embeds the token in metadata instead). - # Supports both single-key (user_token) and multi-part split tokens - # (user_token_1, user_token_2, … + user_token_parts) for tokens > 512 chars. metadata = context.raw_payload.get("metadata", {}) if isinstance(metadata, dict): - user_token = self._extract_user_token(metadata) - if user_token: - request_context["authorization"] = f"Bearer {user_token}" - logger.info("[DEBUG] Reassembled user_token from metadata (%d chars)", len(user_token)) + request_context.update(metadata) self._agent._request_headers = request_context # type: ignore[attr-defined] # Use split converters From d1fb1b1c2ab59647cde1d3e412741c6c736cc75a Mon Sep 17 00:00:00 2001 From: Amit Chauhan <70937115+achauhan-scc@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:36:50 +0530 Subject: [PATCH 4/4] address review: remove header passthrough, use metadata only - Remove sensitive header logging (security concern) - Remove _PASSTHROUGH_HEADERS and header capture from middleware - Remove headers param from AgentRunContext (metadata is the recommended transport for per-request context in Vnext infra) - Adapter now only reads metadata from request body payload Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agentframework/_ai_agent_adapter.py | 5 ++--- .../azure/ai/agentserver/core/server/base.py | 14 +------------- .../core/server/common/agent_run_context.py | 13 +------------ 3 files changed, 4 insertions(+), 28 deletions(-) diff --git a/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/_ai_agent_adapter.py b/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/_ai_agent_adapter.py index e7979071e3a1..069c381df2b7 100644 --- a/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/_ai_agent_adapter.py +++ b/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/_ai_agent_adapter.py @@ -56,10 +56,9 @@ async def agent_run( # pylint: disable=too-many-statements logger.debug("Transformed input message type: %s", type(message)) # Attach per-request context to the agent instance so tools can access it - # without LLM involvement. Combines HTTP headers and request metadata. + # without LLM involvement. Uses request body metadata as the transport + # (Vnext infra strips HTTP auth headers before they reach the agent). request_context: dict[str, Any] = {} - if context.headers: - request_context.update(context.headers) metadata = context.raw_payload.get("metadata", {}) if isinstance(metadata, dict): request_context.update(metadata) diff --git a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/server/base.py b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/server/base.py index eb3680c4a8ba..e2f83ff5d1c6 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/server/base.py +++ b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/server/base.py @@ -52,10 +52,6 @@ def __init__(self, app: ASGIApp, agent: Optional['FoundryCBAgent'] = None): super().__init__(app) self.agent = agent - # Headers to capture from the incoming HTTP request and make available - # to the agent invocation via AgentRunContext.headers. - _PASSTHROUGH_HEADERS = frozenset({"authorization", "x-request-id", "x-user-authorization"}) - async def dispatch(self, request: Request, call_next): if request.url.path in ("/runs", "/responses"): try: @@ -65,15 +61,7 @@ async def dispatch(self, request: Request, call_next): logger.error(f"Invalid JSON payload: {e}") return JSONResponse({"error": f"Invalid JSON payload: {e}"}, status_code=400) try: - # Log all incoming headers for debugging - logger.info(f"[DEBUG] All request headers: {dict(request.headers)}") - headers = { - k: v - for k, v in request.headers.items() - if k.lower() in self._PASSTHROUGH_HEADERS - } - logger.info(f"[DEBUG] Captured passthrough headers: {list(headers.keys())}") - request.state.agent_run_context = AgentRunContext(payload, headers=headers) + request.state.agent_run_context = AgentRunContext(payload) self.set_run_context_to_context_var(request.state.agent_run_context) except Exception as e: logger.error(f"Context build failed: {e}.", exc_info=True) diff --git a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/server/common/agent_run_context.py b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/server/common/agent_run_context.py index 432079a137b2..87c32926bde4 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/server/common/agent_run_context.py +++ b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/server/common/agent_run_context.py @@ -16,14 +16,13 @@ class AgentRunContext: """ :meta private: """ - def __init__(self, payload: dict, headers: dict | None = None) -> None: + def __init__(self, payload: dict) -> None: self._raw_payload = payload self._request = _deserialize_create_response(payload) self._id_generator = FoundryIdGenerator.from_request(payload) self._response_id = self._id_generator.response_id self._conversation_id = self._id_generator.conversation_id self._stream = self.request.get("stream", False) - self._headers = headers or {} @property def raw_payload(self) -> dict: @@ -49,16 +48,6 @@ def conversation_id(self) -> Optional[str]: def stream(self) -> bool: return self._stream - @property - def headers(self) -> dict: - """HTTP request headers captured from the incoming request. - - Useful for extracting authentication tokens or other per-request metadata - that should be passed through to the agent or its tools without being - exposed to the LLM. - """ - return self._headers - def get_agent_id_object(self) -> AgentId: agent = self.request.get("agent") if not agent: