@@ -310,6 +310,94 @@ def has_valid_internal_mcp_runtime_auth_header(request: Request) -> bool:
310310 return hmac .compare_digest (provided , _expected_internal_mcp_runtime_auth_header ())
311311
312312
313+ # Internal-dispatch trust gate, defined once and shared by all callers.
314+ INTERNAL_MCP_PATH_PREFIX = "/_internal/mcp"
315+ INTERNAL_A2A_PATH_PREFIX = "/_internal/a2a"
316+ INTERNAL_RUNTIME_MARKER_HEADER = "x-contextforge-mcp-runtime"
317+ INTERNAL_AUTH_CONTEXT_HEADER = "x-contextforge-auth-context"
318+ TRUSTED_INTERNAL_RUNTIME_MARKERS = frozenset ({"rust" , "affinity" })
319+
320+
321+ def _internal_path_requires_auth_context (path : str ) -> bool :
322+ """Whether an internal route requires an auth context.
323+
324+ Only ``*/authenticate`` is exempt, since it creates the context; every
325+ other internal route must carry one.
326+
327+ Args:
328+ path: The request path to classify.
329+
330+ Returns:
331+ ``False`` for ``*/authenticate``, otherwise ``True``.
332+ """
333+ return not path .rstrip ("/" ).endswith ("/authenticate" )
334+
335+
336+ def is_trusted_internal_runtime_request (
337+ request : Request ,
338+ * ,
339+ allowed_prefixes : tuple [str , ...],
340+ require_auth_context : bool ,
341+ path : Optional [str ] = None ,
342+ ) -> bool :
343+ """Return whether a request is a trusted in-process internal-runtime hop.
344+
345+ The trust boundary is the HMAC header and, when required, the encoded
346+ ``x-contextforge-auth-context``. The loopback check is defense in depth,
347+ not an independent gate: ProxyHeaders(trusted_hosts="*") lets a direct
348+ external caller influence ``request.client.host`` (the replay is hardened
349+ separately by stripping forwarded / client-IP headers).
350+
351+ Args:
352+ request: Incoming request to inspect.
353+ allowed_prefixes: Internal path prefixes this caller trusts.
354+ require_auth_context: Require a non-empty ``x-contextforge-auth-context``.
355+ path: Explicit path override for callers that strip a ``root_path``;
356+ defaults to ``request.url.path``.
357+
358+ Returns:
359+ ``True`` only when prefix, runtime marker, HMAC, optional auth context,
360+ and loopback all hold; otherwise ``False``.
361+ """
362+ p = path if path is not None else (getattr (getattr (request , "url" , None ), "path" , "" ) or "" )
363+ if not any (p == prefix or p .startswith (f"{ prefix } /" ) for prefix in allowed_prefixes ):
364+ return False
365+ if request .headers .get (INTERNAL_RUNTIME_MARKER_HEADER ) not in TRUSTED_INTERNAL_RUNTIME_MARKERS :
366+ return False
367+ if not has_valid_internal_mcp_runtime_auth_header (request ):
368+ return False
369+ if require_auth_context and not request .headers .get (INTERNAL_AUTH_CONTEXT_HEADER ):
370+ return False
371+ client_host = getattr (getattr (request , "client" , None ), "host" , None )
372+ return client_host in ("127.0.0.1" , "::1" )
373+
374+
375+ def is_trusted_internal_mcp_request (request : Request , * , path : Optional [str ] = None ) -> bool :
376+ """MCP + A2A internal trust gate with a path-aware auth-context requirement.
377+
378+ ``*/authenticate`` routes are exempt from the auth-context requirement;
379+ every other internal route requires it. ``/_internal/a2a/*`` is trusted
380+ only when the A2A feature is enabled.
381+
382+ Args:
383+ request: Incoming request to inspect.
384+ path: Explicit path override (e.g. a ``root_path``-stripped path);
385+ defaults to ``request.url.path``.
386+
387+ Returns:
388+ ``True`` when the request is a trusted internal MCP/A2A hop.
389+ """
390+ p = path if path is not None else (getattr (getattr (request , "url" , None ), "path" , "" ) or "" )
391+ if p .startswith (f"{ INTERNAL_A2A_PATH_PREFIX } /" ) and not settings .mcpgateway_a2a_enabled :
392+ return False
393+ return is_trusted_internal_runtime_request (
394+ request ,
395+ allowed_prefixes = (INTERNAL_MCP_PATH_PREFIX , INTERNAL_A2A_PATH_PREFIX ),
396+ require_auth_context = _internal_path_requires_auth_context (p ),
397+ path = p ,
398+ )
399+
400+
313401def get_token_teams_from_request (request : Request ) -> Optional [List [str ]]:
314402 """Extract and normalize teams from verified JWT token.
315403
0 commit comments