fix(security): close bearer side-effect boundaries

This commit is contained in:
RaresKeY
2026-08-29 00:19:59 +00:00
parent 50c8675a21
commit 31c7249ef3
14 changed files with 826 additions and 80 deletions
+2
View File
@@ -31,6 +31,7 @@ class RequestCapability:
allow_detached_execution: bool
allow_message_events: bool
allow_auto_naming: bool
allow_live_probes: bool
def is_bearer_principal(request: Request) -> bool:
@@ -101,6 +102,7 @@ def request_capability(request: Request) -> RequestCapability:
allow_detached_execution=not bearer,
allow_message_events=not bearer,
allow_auto_naming=not bearer,
allow_live_probes=not bearer,
)
+9 -1
View File
@@ -330,12 +330,16 @@ async def maybe_compact(
*,
persist: bool = True,
compaction_state: Optional[Dict[str, Any]] = None,
allow_live_probes: bool = True,
) -> tuple:
"""Check context usage and compact if above threshold.
Returns (messages, context_length, was_compacted).
"""
context_length = get_context_length(endpoint_url, model)
context_kwargs = {}
if not allow_live_probes:
context_kwargs["allow_live_probes"] = False
context_length = get_context_length(endpoint_url, model, **context_kwargs)
used = estimate_tokens(messages)
pct = (used / context_length) * 100 if context_length else 0
@@ -392,6 +396,9 @@ async def maybe_compact(
]
try:
summary_kwargs = {}
if not allow_live_probes:
summary_kwargs["allow_live_probes"] = False
summary = await llm_call_async(
compact_url,
compact_model,
@@ -400,6 +407,7 @@ async def maybe_compact(
max_tokens=SUMMARY_MAX_TOKENS,
headers=compact_headers,
timeout=30,
**summary_kwargs,
)
except Exception as e:
logger.error(f"Compaction summary failed: {e}")
+39 -8
View File
@@ -1903,6 +1903,7 @@ def list_model_ids(
*,
owner: Optional[str] = None,
endpoint_id: Optional[str] = None,
allow_live_probes: bool = True,
) -> List[str]:
"""List available model IDs from an endpoint."""
cached = _configured_cached_model_ids(base_chat_url, owner=owner, endpoint_id=endpoint_id)
@@ -1911,6 +1912,8 @@ def list_model_ids(
provider = _detect_provider(base_chat_url)
if provider == "anthropic":
return list(ANTHROPIC_MODELS)
if not allow_live_probes:
return []
try:
h = {}
if headers:
@@ -1952,9 +1955,16 @@ def normalize_model_id(
*,
owner: Optional[str] = None,
endpoint_id: Optional[str] = None,
allow_live_probes: bool = True,
) -> Optional[str]:
"""Normalize a model ID to match available models."""
avail = list_model_ids(endpoint_url, timeout, owner=owner, endpoint_id=endpoint_id)
avail = list_model_ids(
endpoint_url,
timeout,
owner=owner,
endpoint_id=endpoint_id,
allow_live_probes=allow_live_probes,
)
if not avail:
return None
if requested in avail:
@@ -1968,7 +1978,8 @@ def normalize_model_id(
def llm_call(url: str, model: str, messages: List[Dict], temperature: float = LLMConfig.DEFAULT_TEMPERATURE,
max_tokens: int = LLMConfig.DEFAULT_MAX_TOKENS, headers: Optional[Dict] = None,
timeout: int = LLMConfig.DEFAULT_TIMEOUT, prompt_type: Optional[str] = None) -> str:
timeout: int = LLMConfig.DEFAULT_TIMEOUT, prompt_type: Optional[str] = None,
allow_live_probes: bool = True) -> str:
"""Synchronous LLM call with optional prompt type enhancement."""
h = _provider_headers(_detect_provider(url))
# Tolerate headers that arrive as a JSON string (some sessions stored them
@@ -2012,9 +2023,12 @@ def llm_call(url: str, model: str, messages: List[Dict], temperature: float = LL
payload = _build_anthropic_payload(model, messages_copy, temperature, max_tokens)
elif provider == "ollama":
target_url = _normalize_ollama_url(url)
context_kwargs = {}
if not allow_live_probes:
context_kwargs["allow_live_probes"] = False
payload = _build_ollama_payload(
model, messages_copy, temperature, max_tokens,
stream=False, num_ctx=get_context_length(url, model),
stream=False, num_ctx=get_context_length(url, model, **context_kwargs),
)
else:
target_url = _normalize_openai_chat_url(url)
@@ -2273,6 +2287,7 @@ async def llm_call_async(
workload: str = "foreground",
availability_only_transport: bool = False,
return_model_metadata: bool = False,
allow_live_probes: bool = True,
) -> str | tuple[str, str]:
"""Asynchronous LLM call using httpx with connection pooling, timeout, retry logic, and performance logging."""
provider = _detect_provider(url)
@@ -2307,6 +2322,9 @@ async def llm_call_async(
# Reuse stream_llm's validated Codex SSE path and collect deltas.
parts: List[str] = []
actual_model = model
stream_kwargs = {"workload": workload}
if not allow_live_probes:
stream_kwargs["allow_live_probes"] = False
async for chunk in stream_llm(
url,
model,
@@ -2315,7 +2333,7 @@ async def llm_call_async(
max_tokens=max_tokens,
headers=headers,
timeout=timeout,
workload=workload,
**stream_kwargs,
):
event_is_error = False
for line in str(chunk).splitlines():
@@ -2372,9 +2390,12 @@ async def llm_call_async(
h = {"Content-Type": "application/json"}
if headers:
h.update(headers)
context_kwargs = {}
if not allow_live_probes:
context_kwargs["allow_live_probes"] = False
payload = _build_ollama_payload(
model, messages_copy, temperature, max_tokens,
stream=False, num_ctx=get_context_length(url, model),
stream=False, num_ctx=get_context_length(url, model, **context_kwargs),
)
else:
target_url = _normalize_openai_chat_url(url)
@@ -2560,9 +2581,13 @@ async def stream_llm(url: str, model: str, messages: List[Dict], temperature: fl
max_tokens: int = LLMConfig.DEFAULT_MAX_TOKENS, headers: Optional[Dict] = None,
timeout: int = LLMConfig.STREAM_TIMEOUT, prompt_type: Optional[str] = None,
tools: Optional[List[Dict]] = None, session_id: Optional[str] = None,
tool_choice_none: bool = False, workload: str = "foreground"):
tool_choice_none: bool = False, workload: str = "foreground",
allow_live_probes: bool = True):
target_url = _stream_target_url(url)
async with _local_model_slot(target_url, model, workload):
inner_kwargs = {}
if not allow_live_probes:
inner_kwargs["allow_live_probes"] = False
async for chunk in _stream_llm_inner(
url,
model,
@@ -2575,6 +2600,7 @@ async def stream_llm(url: str, model: str, messages: List[Dict], temperature: fl
tools=tools,
session_id=session_id,
tool_choice_none=tool_choice_none,
**inner_kwargs,
):
yield chunk
@@ -2583,7 +2609,7 @@ async def _stream_llm_inner(url: str, model: str, messages: List[Dict], temperat
max_tokens: int = LLMConfig.DEFAULT_MAX_TOKENS, headers: Optional[Dict] = None,
timeout: int = LLMConfig.STREAM_TIMEOUT, prompt_type: Optional[str] = None,
tools: Optional[List[Dict]] = None, session_id: Optional[str] = None,
tool_choice_none: bool = False):
tool_choice_none: bool = False, allow_live_probes: bool = True):
"""Stream LLM responses with improved error handling.
Yields SSE chunks:
@@ -2618,9 +2644,14 @@ async def _stream_llm_inner(url: str, model: str, messages: List[Dict], temperat
h = {"Content-Type": "application/json"}
if headers:
h.update(headers)
context_kwargs = {}
if not allow_live_probes:
context_kwargs["allow_live_probes"] = False
payload = _build_ollama_payload(
model, messages_copy, temperature, max_tokens,
stream=True, tools=tools, num_ctx=get_context_length(url, model),
stream=True,
tools=tools,
num_ctx=get_context_length(url, model, **context_kwargs),
)
elif provider == "chatgpt-subscription":
target_url = _normalize_chatgpt_subscription_url(url)
+39 -6
View File
@@ -238,16 +238,31 @@ KNOWN_CONTEXT_WINDOWS = {
_context_cache: Dict[Tuple[str, str], Tuple[int, bool]] = {}
def _get_context_length_cached(endpoint_url: str, model: str) -> Tuple[int, bool]:
def _get_context_length_cached(
endpoint_url: str,
model: str,
*,
allow_live_probes: bool = True,
) -> Tuple[int, bool]:
"""Return (context_length, known). ``known`` is False only when the value is a
bare DEFAULT_CONTEXT fallback (no endpoint report and not in the known table)."""
cache_key = (endpoint_url, model)
if not allow_live_probes:
# A bearer may consume metadata already learned by an interactive or
# explicitly privileged refresh, but a context build must not cause a
# new /slots, /models, or catalog request or populate those caches.
cached = _context_cache.get(cache_key)
if cached:
return cached
known = _lookup_known(model)
return (known, True) if known else (DEFAULT_CONTEXT, False)
configured_kind = _configured_endpoint_kind(endpoint_url)
is_local = is_local_endpoint(endpoint_url)
# Key on (endpoint_url, model): the same model id can be served by two
# different remote endpoints with different real context windows (e.g. a
# capped proxy vs. the full provider), so caching by model id alone would
# serve one endpoint's window for the other (issue #2603).
cache_key = (endpoint_url, model)
if not is_local and cache_key in _context_cache:
return _context_cache[cache_key]
@@ -261,23 +276,41 @@ def _get_context_length_cached(endpoint_url: str, model: str) -> Tuple[int, bool
return ctx, known
def get_context_length(endpoint_url: str, model: str) -> int:
def get_context_length(
endpoint_url: str,
model: str,
*,
allow_live_probes: bool = True,
) -> int:
"""Get the context window size for a model.
Queries /v1/models on the endpoint and looks for context_length
or context_window fields. Caches result per (endpoint, model).
Falls back to DEFAULT_CONTEXT if unavailable.
"""
return _get_context_length_cached(endpoint_url, model)[0]
return _get_context_length_cached(
endpoint_url,
model,
allow_live_probes=allow_live_probes,
)[0]
def get_context_length_known(endpoint_url: str, model: str) -> Tuple[int, bool]:
def get_context_length_known(
endpoint_url: str,
model: str,
*,
allow_live_probes: bool = True,
) -> Tuple[int, bool]:
"""Like ``get_context_length`` but also returns whether the window was actually
discovered (endpoint-reported or in the known-models table) rather than the bare
DEFAULT_CONTEXT fallback. Callers that *scale* a budget off the window must not
trust an unknown value — a fallback 128K isn't proof the model holds 128K
(review on #4122)."""
return _get_context_length_cached(endpoint_url, model)
return _get_context_length_cached(
endpoint_url,
model,
allow_live_probes=allow_live_probes,
)
def budget_context_for_model(endpoint_url: str, model: str, *, fallback: int = 0) -> int: