-
Notifications
You must be signed in to change notification settings - Fork 606
Preserve interception for offline Docker agents #1841
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
07e5e51
7416022
b63ed87
83c11a6
b1ac94f
8d2399a
407b979
81fed07
4d23b5a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,13 +17,13 @@ | |
| from typing import TYPE_CHECKING | ||
| from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit | ||
|
|
||
| from verifiers.v1.errors import RolloutError, ToolsetError, UserError | ||
| from verifiers.v1.errors import RolloutError, SandboxError, ToolsetError, UserError | ||
| from verifiers.v1.interception.tunnel import PrimeTunnel | ||
| from verifiers.v1.mcp.server import STATE_SECRET_PARAM, STATE_URL_PARAM, ServerBase | ||
| from verifiers.v1.runtimes import ( | ||
| Runtime, | ||
| make_runtime, | ||
| runtime_is_local, | ||
| runtime_reaches_host_locally, | ||
| ) | ||
| from verifiers.v1.runtimes.base import _ENSURE_UV | ||
| from verifiers.v1.types import Messages | ||
|
|
@@ -217,21 +217,24 @@ async def serve_in_runtime( | |
|
|
||
| @contextlib.asynccontextmanager | ||
| async def reachable_url( | ||
| service: Runtime, port: int, *, colocated: bool, consumer_is_local: bool | ||
| service: Runtime, port: int, *, colocated: bool, consumer_reaches_host: bool | ||
| ) -> AsyncIterator[str]: | ||
| """Yield the URL a consumer uses to reach the server at (`service`, `port`), over two | ||
| primitives: `Runtime.expose` (publish a port out of a sandbox) and a host `Tunnel` (reach | ||
| into the host from a remote runtime). `colocated` = the server shares the consumer's | ||
| runtime; `consumer_is_local` = the consumer is on the host network. | ||
| runtime; `consumer_reaches_host` = the consumer reaches the host network directly. | ||
|
|
||
| - `colocated` -> localhost (same runtime, in-sandbox or host loopback); | ||
| - the server runs in a remote sandbox -> its own published URL (`expose`), reachable anywhere; | ||
| - the server is not host-reachable -> its published URL (`expose`), reachable anywhere; | ||
| - else it's on the host network -> localhost to a local consumer, a host tunnel to a remote one.""" | ||
| if colocated: | ||
| yield f"http://127.0.0.1:{port}" | ||
| elif not service.is_local: # in a remote sandbox → it publishes its own port | ||
| yield await service.expose(port) | ||
| elif consumer_is_local: # host network, local consumer → localhost, no tunnel | ||
| elif not service.reachable_from_host_locally: | ||
| url = await service.expose(port) | ||
| if url is None: | ||
| raise SandboxError(f"{service.type} runtime cannot expose port {port}") | ||
| yield url | ||
| elif consumer_reaches_host: # host network, direct consumer → localhost, no tunnel | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the harness is Useful? React with 👍 / 👎. |
||
| yield f"http://127.0.0.1:{port}" | ||
| else: # host network, remote consumer → a host tunnel publishes the port outward | ||
| async with PrimeTunnel().expose(port) as url: | ||
|
|
@@ -243,7 +246,7 @@ async def serve( | |
| server: ServerBase, | ||
| harness_runtime: Runtime | None = None, | ||
| for_host: bool = False, | ||
| harness_is_local: bool = True, | ||
| harness_reaches_host: bool = True, | ||
| *, | ||
| state_secret: str = "", | ||
| state_base: str | None = None, | ||
|
|
@@ -254,6 +257,9 @@ async def serve( | |
| runtime = harness_runtime | ||
| else: | ||
| runtime = make_runtime(cfg.runtime) | ||
| if runtime.network_policy.restricted: | ||
| error = UserError if for_host else ToolsetError | ||
| raise error("network policies are only applied to harness runtimes") | ||
| await runtime.start() | ||
| stack.push_async_callback(runtime.stop) | ||
| # Only consumers outside the server runtime need its fixed published port. Colocated tools | ||
|
|
@@ -276,19 +282,22 @@ async def serve( | |
| # Who consumes the server decides reachability: a user sim is reached by the HOST | ||
| # (`for_host`, always local, never colocated with it); a tool by the harness — colocated | ||
| # when it shares the harness's runtime, reached with the harness's locality (read off the | ||
| # harness runtime when there is one, else `harness_is_local` for an eval-level shared tool). | ||
| # harness runtime when there is one, else `harness_reaches_host` for a shared tool). | ||
| if for_host: | ||
| colocated, consumer_is_local = False, True | ||
| colocated, consumer_reaches_host = False, True | ||
| else: | ||
| colocated = runtime is harness_runtime | ||
| consumer_is_local = ( | ||
| harness_runtime.is_local | ||
| consumer_reaches_host = ( | ||
| harness_runtime.reaches_host_locally | ||
| if harness_runtime is not None | ||
| else harness_is_local | ||
| else harness_reaches_host | ||
| ) | ||
| base = await stack.enter_async_context( | ||
| reachable_url( | ||
| runtime, port, colocated=colocated, consumer_is_local=consumer_is_local | ||
| runtime, | ||
| port, | ||
| colocated=colocated, | ||
| consumer_reaches_host=consumer_reaches_host, | ||
| ) | ||
| ) | ||
| if not for_host and not colocated and harness_runtime is not None: | ||
|
|
@@ -299,25 +308,25 @@ async def serve( | |
| @dataclass(frozen=True) | ||
| class SharedToolServer: | ||
| """One live taskset-scoped (shared) server, as the rollouts see it: its eval-level | ||
| `url` plus whether its runtime is `local` (host-reachable) — a remote one is an | ||
| `url` plus whether its runtime reaches the host directly — otherwise it is an | ||
| interception consumer, so the interception must be exposed for it to reach the | ||
| `/state` channel (see `Environment._requires_tunnel`). An `external` server (a | ||
| config-`url` endpoint) was not launched by the framework and sits outside its state | ||
| machinery entirely: rollouts get its URL bare — no state tag (and no per-rollout | ||
| secret sent to a third party).""" | ||
|
|
||
| url: str | ||
| local: bool | ||
| reaches_host: bool | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Renaming this field leaves Useful? React with 👍 / 👎. |
||
| external: bool = False | ||
|
|
||
|
|
||
| @contextlib.asynccontextmanager | ||
| async def serve_shared(toolsets: list[Toolset], harness_is_local: bool = True): | ||
| async def serve_shared(toolsets: list[Toolset], harness_reaches_host: bool = True): | ||
| """Start the taskset-scoped (shared) tool servers ONCE for a whole eval, each in its OWN | ||
| `runtime`, and yield `{name: SharedToolServer}` reachable by every rollout's harness. | ||
| Reachability mirrors a per-rollout tool, but there's no single harness runtime to read | ||
| locality off — the caller (`Environment.shared_tools`) passes the harness runtime's | ||
| `harness_is_local`, so a host tool gets one host bridge (tunnel) when the harness runs | ||
| `harness_reaches_host`, so a host tool gets one host bridge when the harness runs | ||
| remotely, and a remote tool runtime publishes its own URL. Torn down when the eval ends. | ||
| A shared server is task-agnostic — the taskset carries no per-row data — so its `setup` | ||
| gets no task (its `setup_task` is never called; the per-rollout servers fetch | ||
|
|
@@ -342,14 +351,15 @@ async def serve_shared(toolsets: list[Toolset], harness_is_local: bool = True): | |
| ) | ||
| if cfg.url: # already running remotely; nothing launched, nothing to bridge | ||
| servers[name] = SharedToolServer( | ||
| url=cfg.url, local=False, external=True | ||
| url=cfg.url, reaches_host=False, external=True | ||
| ) | ||
| else: | ||
| url = await stack.enter_async_context( | ||
| serve(toolset, harness_is_local=harness_is_local) | ||
| serve(toolset, harness_reaches_host=harness_reaches_host) | ||
| ) | ||
| servers[name] = SharedToolServer( | ||
| url=url, local=runtime_is_local(cfg.runtime) | ||
| url=url, | ||
| reaches_host=runtime_reaches_host_locally(cfg.runtime), | ||
| ) | ||
| logger.info("shared tool server '%s': %s", name, servers[name].url) | ||
| yield servers | ||
|
|
@@ -393,7 +403,11 @@ async def serve_tools( | |
| urls[name] = server.url | ||
| logger.info("tool server '%s' (shared, external): %s", name, server.url) | ||
| continue | ||
| url = harness_runtime.host_url(server.url) if server.local else server.url | ||
| url = ( | ||
| harness_runtime.host_url(server.url) | ||
| if server.reaches_host | ||
| else server.url | ||
| ) | ||
| urls[name] = _shared_url_for_rollout(url, state_base, state_secret) | ||
| # The tagged URL contains the bearer secret; log only the untagged base URL. | ||
| logger.info("tool server '%s' (shared): %s", name, server.url) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shared interception host too early
High Severity
Eval
serving()fixesextra_hostfromdocker_interception_host()before any rollout creates its setup network, and every rollout shares that single bind address. Per-rollout bridge gateways differ, so shared interception often never listens on the address restricted Docker harnesses use to reach the host.Additional Locations (1)
verifiers/v1/interception/server.py#L175-L185Reviewed by Cursor Bugbot for commit 81fed07. Configure here.