Source code for milp_flare.harness.runner.docker

from __future__ import annotations

import os
import subprocess
import time
import uuid
from collections.abc import Iterator
from pathlib import Path
from typing import IO, ClassVar

from milp_flare.harness.runner.base import IMAGE, AgentRun, AuthSpec, Runner


[docs] class DockerAgentRun(AgentRun): """Live handle for a single in-flight Docker agent run. Parameters ---------- popen : subprocess.Popen[str] The Popen handle for the ``docker run`` running container process. name : str The unique container name for this run, used to target cancellation. stderr_f : IO[bytes] The file where the container's stderr is being redirected. start : float The wall-clock time when the container was launched, for duration tracking. """ def __init__( self, popen: subprocess.Popen[str], name: str, stderr_f: IO[bytes], start: float, ) -> None: super().__init__(start) self._popen = popen self._name = name self._stderr_f = stderr_f @property def stdout(self) -> Iterator[str]: assert self._popen.stdout is not None for line in self._popen.stdout: yield line.rstrip("\n") def cancel(self) -> None: # The agent working directory is bind-mounted into the container, so all # partial artifacts are already on the host. Just kill the container. subprocess.run( ["docker", "kill", self._name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) def _teardown(self) -> None: super()._teardown() # Killing the Docker container kills the process and the stdout pipe # which should release the wait() below. self._popen.wait() self._stderr_f.close()
[docs] class DockerRunner(Runner): """Run the agent in a local Docker container. Parameters ---------- image : str, default :const:`~milp_flare.harness.runner.base.IMAGE` The Docker image tag to run. Built via ``milp-flare build-docker-image``; see :doc:`/user_guide/docker`. """ name: ClassVar[str] = "docker" home: ClassVar[str] = "/home/agent" def __init__(self, image: str = IMAGE) -> None: self._image = image @property def image(self) -> str: return self._image def start(self, wd: Path, auth: AuthSpec) -> AgentRun: # A unique per-run name lets cancel() stop just this container. name = f"flare-{uuid.uuid4().hex[:12]}" start = time.time() # Redirect stderr straight to a file so the undrained pipe can't deadlock # the run; stdout stays a pipe we stream line by line. The handle owns # the file and closes it on teardown. stderr_f = open(wd / "docker_stderr.txt", "wb") try: popen = subprocess.Popen( self._build_docker_cmd(wd, auth, name=name), stdout=subprocess.PIPE, stderr=stderr_f, text=True, # Without start_new_session, Ctrl+C in the terminal sends SIGINT # to both the driver and the agent container, which can cause the # container to terminate prematurely. start_new_session=True # detaches the subprocess from the terminal's process group. start_new_session=True, ) except BaseException: stderr_f.close() raise return DockerAgentRun(popen, name, stderr_f, start) def _build_docker_cmd( self, wd: Path, auth: AuthSpec, name: str | None = None ) -> list[str]: """Assemble the full ``docker run`` command from an :class:`AuthSpec`.""" cmd = ["docker", "run"] # Automatically remove the container when it exits cmd += ["--rm"] # A unique per-run name enables per-run cancel (`docker kill <name>`) # without affecting other containers in the same batch. if name: cmd += ["--name", name] # Bind mount the agent's working directory to /workspace/wd in the container cmd += ["-v", f"{wd.resolve()}:/workspace/wd"] # Label the container with the FLARE run ID (if present) run_id = os.environ.get("FLARE_RUN_ID") if run_id: cmd += ["--label", f"flare-run={run_id}"] # Forward agent credentials (env vars and host config dirs). for name in auth.env: cmd += ["-e", name] for host_dir, dest in auth.home_dirs: # Mount rw so e.g. codex can refresh its access token mid-session. cmd += ["-v", f"{host_dir}:{self.home}/{dest}"] # Finally, specify the image to run cmd += [self._image] return cmd