fix(runtime): keep the default path blocking on the proxy endpoint and discard a sandbox created after a cancelled deferred bring-up

This commit is contained in:
Ahmed Allam 2026-10-13 15:36:25 +00:00 • committed by Ahmed Allam
parent 4bf7541599
commit 0f8db1fca8
2 changed files with 58 additions and 10 deletions

View file

@ -3,6 +3,7 @@
from __future__ import annotations
import asyncio
import contextlib
import io
import logging
import os
@ -23,6 +24,8 @@ from strix.runtime.deferred_session import DeferredSandboxClient, DeferredSandbo
if TYPE_CHECKING:
from collections.abc import Awaitable
from agents.sandbox.session import BaseSandboxSession
from strix.runtime.status import StatusSink
@ -328,11 +331,13 @@ async def create_or_reuse(
async def _bring_up() -> tuple[Any, Any]:
report("Starting sandbox container")
client, session = await backend(
image=image,
manifest=manifest,
exposed_ports=(_CONTAINER_CAIDO_PORT,),
bind_mounts=bind_mounts,
client, session = await _create_or_discard(
backend(
image=image,
manifest=manifest,
exposed_ports=(_CONTAINER_CAIDO_PORT,),
bind_mounts=bind_mounts,
)
)
if extra_file_archive is not None:
report("Placing workspace files")
@ -343,13 +348,16 @@ async def create_or_reuse(
raise
return client, session
async def _caido(session: Any) -> Any:
async def _caido_url(session: Any) -> str:
report("Setting up the proxy")
caido_endpoint = await session.resolve_exposed_port(_CONTAINER_CAIDO_PORT)
scheme = "https" if caido_endpoint.tls else "http"
host_caido_url = f"{scheme}://{caido_endpoint.host}:{caido_endpoint.port}"
logger.debug("Caido host endpoint resolved: %s", host_caido_url)
return await bootstrap_caido(
return host_caido_url
def _caido(session: Any, host_caido_url: str) -> Any:
return bootstrap_caido(
session,
host_url=host_caido_url,
container_url=container_caido_url,
@ -364,7 +372,8 @@ async def create_or_reuse(
)
async def _caido_when_up() -> Any:
return await _caido(await deferred.resolve())
session = await deferred.resolve()
return await _caido(session, await _caido_url(session))
bundle = {
"client": DeferredSandboxClient(deferred),
@ -378,8 +387,9 @@ async def create_or_reuse(
return bundle
client, session = await _bring_up()
host_caido_url = await _caido_url(session)
caido_client = CaidoBootstrapHandle(
asyncio.create_task(_caido(session), name=f"caido-bootstrap-{scan_id}")
asyncio.create_task(_caido(session, host_caido_url), name=f"caido-bootstrap-{scan_id}")
)
bundle = {
@ -392,6 +402,26 @@ async def create_or_reuse(
return bundle
_DISCARD_TASKS: set[asyncio.Task[None]] = set()
async def _create_or_discard(create: Awaitable[tuple[Any, Any]]) -> tuple[Any, Any]:
pending = asyncio.ensure_future(create)
try:
return await asyncio.shield(pending)
except asyncio.CancelledError:
discard = asyncio.create_task(_discard_when_created(pending))
_DISCARD_TASKS.add(discard)
discard.add_done_callback(_DISCARD_TASKS.discard)
raise
async def _discard_when_created(create: asyncio.Future[tuple[Any, Any]]) -> None:
with contextlib.suppress(BaseException):
client, session = await create
await _discard_session(client, session)
async def _discard_session(client: Any, session: Any) -> None:
"""Best-effort teardown of a session that never made it into the cache."""
try:

View file

@ -129,6 +129,11 @@ def backend(monkeypatch: pytest.MonkeyPatch, request: pytest.FixtureRequest) ->
_BIND_MOUNT_BACKENDS.discard(name)
async def _settle() -> None:
for _ in range(10):
await asyncio.sleep(0)
async def _create(scan_id: str, **kwargs: Any) -> dict[str, Any]:
return await session_manager.create_or_reuse(
scan_id, image="img", local_sources=[], defer=True, **kwargs
@ -144,7 +149,7 @@ async def test_deferred_bundle_returns_before_the_backend_finishes(backend: _Bac
assert isinstance(session, DeferredSandboxSession)
assert isinstance(bundle["client"], DeferredSandboxClient)
assert session.pending
await asyncio.sleep(0)
await _settle()
assert backend.calls == 1
assert await session.running()
assert session.state.manifest.environment.value["PYTHONUNBUFFERED"] == "1"
@ -205,6 +210,19 @@ async def test_cleanup_while_pending_cancels_the_bring_up(backend: _Backend) ->
assert session.bring_up.cancelled()
assert backend.client.deleted == []
assert session_manager._SESSION_CACHE.get(scan_id) is None
backend.gate.set()
await _settle()
assert backend.client.deleted == [backend.session]
@pytest.mark.asyncio
async def test_cleanup_while_pending_ignores_a_failed_late_bring_up(backend: _Backend) -> None:
scan_id = "deferred-cancel-fail"
await _create(scan_id)
await asyncio.wait_for(session_manager.cleanup(scan_id), timeout=1)
backend.gate.set()
await _settle()
assert backend.client.deleted == []
@pytest.mark.asyncio