Summary
_maybe_intercept_web_search calls loop.run_until_complete(_perform_web_search(...)) from inside an already-running asyncio event loop. This pattern unconditionally deadlocks in Python's default asyncio runtime.
Evidence
codex_shim/server.py, _maybe_intercept_web_search():
try:
loop = asyncio.get_running_loop()
result_text = loop.run_until_complete(_perform_web_search(query))
except RuntimeError:
result_text = "Web search unavailable in this context."
asyncio.AbstractEventLoop.run_until_complete raises RuntimeError("This event loop is already running.") when called from a coroutine that is itself executing on that loop. The except RuntimeError block catches this exception and silently swallows it, returning the string "Web search unavailable in this context." — meaning web search always silently fails.
The caller (_post_openai_chat / _post_anthropic) invokes _maybe_intercept_web_search from an async def handler body, i.e., it is always already inside the running loop.
Why this matters
The web search feature is permanently broken in production: every web search call silently returns the fallback string rather than real results. Users and operators have no way to detect the failure because no error is logged. The except RuntimeError block was presumably intended to catch "no event loop" (non-async context) but instead catches the "loop is running" error that is always raised.
Attack or failure scenario
Not a security issue — this is a silent reliability failure. A Codex user triggers a model that invokes a web-search tool; the shim intercepts the call, enters _maybe_intercept_web_search, attempts to run the DuckDuckGo fetch, immediately hits RuntimeError, silently returns "Web search unavailable in this context.", and the model receives that string as the search result. The user sees a made-up or confused AI response with no indication the search failed.
Root cause
loop.run_until_complete() is a blocking, top-level API for non-async callers. Inside an already-running async context it cannot be used. The correct pattern is await _perform_web_search(query) from an async caller, or asyncio.run_coroutine_threadsafe if a background thread is needed. The except RuntimeError catch-all hides the failure rather than surfacing it.
Recommended fix
Since _maybe_intercept_web_search is called from async route handlers, make it async and await the coroutine directly:
async def _maybe_intercept_web_search(payload: dict[str, Any]) -> dict[str, Any] | None:
...
result_text = await _perform_web_search(query)
...
Update all callers (_post_openai_chat, _post_anthropic) to await _maybe_intercept_web_search(...). The asyncio import and the loop.run_until_complete block are then deleted entirely.
Acceptance criteria
- A test fires a non-streaming request through
_post_openai_chat with a mock upstream response containing a web_search_call item. The test verifies the returned payload contains function_call_output results, not "Web search unavailable in this context.".
- No
loop.run_until_complete remains in _maybe_intercept_web_search.
Suggested labels
bug, reliability
Priority
P1 — the web search feature is completely non-functional and the failure is invisible to operators.
Severity
High — silent functional failure; users cannot rely on web-search behaviour.
Confidence
Confirmed — loop.run_until_complete inside a running loop always raises RuntimeError in CPython; the catch block confirms the symptom.
Summary
_maybe_intercept_web_searchcallsloop.run_until_complete(_perform_web_search(...))from inside an already-running asyncio event loop. This pattern unconditionally deadlocks in Python's default asyncio runtime.Evidence
codex_shim/server.py,_maybe_intercept_web_search():asyncio.AbstractEventLoop.run_until_completeraisesRuntimeError("This event loop is already running.")when called from a coroutine that is itself executing on that loop. Theexcept RuntimeErrorblock catches this exception and silently swallows it, returning the string"Web search unavailable in this context."— meaning web search always silently fails.The caller (
_post_openai_chat/_post_anthropic) invokes_maybe_intercept_web_searchfrom anasync defhandler body, i.e., it is always already inside the running loop.Why this matters
The web search feature is permanently broken in production: every web search call silently returns the fallback string rather than real results. Users and operators have no way to detect the failure because no error is logged. The
except RuntimeErrorblock was presumably intended to catch "no event loop" (non-async context) but instead catches the "loop is running" error that is always raised.Attack or failure scenario
Not a security issue — this is a silent reliability failure. A Codex user triggers a model that invokes a web-search tool; the shim intercepts the call, enters
_maybe_intercept_web_search, attempts to run the DuckDuckGo fetch, immediately hitsRuntimeError, silently returns"Web search unavailable in this context.", and the model receives that string as the search result. The user sees a made-up or confused AI response with no indication the search failed.Root cause
loop.run_until_complete()is a blocking, top-level API for non-async callers. Inside an already-running async context it cannot be used. The correct pattern isawait _perform_web_search(query)from an async caller, orasyncio.run_coroutine_threadsafeif a background thread is needed. Theexcept RuntimeErrorcatch-all hides the failure rather than surfacing it.Recommended fix
Since
_maybe_intercept_web_searchis called from async route handlers, make itasyncand await the coroutine directly:Update all callers (
_post_openai_chat,_post_anthropic) toawait _maybe_intercept_web_search(...). Theasyncioimport and theloop.run_until_completeblock are then deleted entirely.Acceptance criteria
_post_openai_chatwith a mock upstream response containing aweb_search_callitem. The test verifies the returned payload containsfunction_call_outputresults, not"Web search unavailable in this context.".loop.run_until_completeremains in_maybe_intercept_web_search.Suggested labels
bug, reliability
Priority
P1 — the web search feature is completely non-functional and the failure is invisible to operators.
Severity
High — silent functional failure; users cannot rely on web-search behaviour.
Confidence
Confirmed —
loop.run_until_completeinside a running loop always raisesRuntimeErrorin CPython; the catch block confirms the symptom.