Skip to content

Run Harbor Compose tasks on local Docker - #2547

Open
xeophon wants to merge 2 commits into
feat/local-container-runtimesfrom
harbor-compose
Open

Run Harbor Compose tasks on local Docker#2547
xeophon wants to merge 2 commits into
feat/local-container-runtimesfrom
harbor-compose

Conversation

@xeophon

@xeophon xeophon commented Sep 6, 2026

Copy link
Copy Markdown
Member

Adds support for Harbor tasks that define Docker Compose environments. The Harbor env owns the project and lends its main container to the existing agent rollout.

Depends on #2528 for the shared container runtime and Docker callback proxy.

Compose preserves authored commands, entrypoints, dependencies, health checks and networking. Harbor supplies the base template and startup environment override; the Docker runtime supplies execution, live processes, file access and host callbacks. Main receives the resolved CPU and memory limits, and its runtime service port is published on host loopback through the service that owns its network namespace.

The project is removed with a checked Compose teardown, including volumes and orphan containers. Separate graders retain their own fresh runtime. This adapter supports CPU tasks with unrestricted local Docker networking.

Note

Add HarborComposeRuntime to run Harbor tasks on local Docker Compose

  • Introduces HarborComposeRuntime in runtime.py, a DockerRuntime subclass that manages the Compose project lifecycle and exposes the main service as the agent runtime.
  • Reworks HarborEnv.run in env.py so tasks with a Compose file start the project before the agent rollout, pass the runtime to the agent, and stop it on exit; setup is bounded by the resolved setup timeout.
  • Adds optional environment overrides to the host-process helper and CLI wrapper in container.py so Compose commands run with caller-supplied env vars merged over the parent environment.
  • HarborComposeRuntime.start validates the Compose topology, generates overlay files, launches the project with detached wait semantics, selects the main container, and starts an egress proxy.
  • Updates harbor.md with a Docker Compose section covering supported and unsupported scenarios.
  • Risk: HarborComposeRuntime.__init__ rejects restricted-network and GPU configurations with a configuration error; tasks requiring those features are not supported under Compose.

Macroscope summarized f589839.


Note

Medium Risk
New sandbox lifecycle path (Compose up/down, port publishing, egress proxy) affects task isolation and cleanup; scope is limited to CPU tasks with public Docker networking and local Docker only.

Overview
Adds Docker Compose support for Harbor tasks that ship environment/docker-compose.yaml, so multi-service topologies run on local Docker while the agent still executes in the main service.

HarborComposeRuntime (new) owns the Compose project lifecycle: it merges Harbor’s prebuilt compose stack with overlays for main (resolved image, workdir, CPU/memory), publishes the runtime service port on the service that owns main’s network namespace, runs docker compose up --detach --wait, and tears the project down with volumes on exit. GPU and restricted-network agent configs are rejected for this path.

HarborEnv.run now starts that runtime inside an AsyncExitStack when a compose file exists (setup bounded by the agent setup timeout), passes it into agents.agent.run(..., runtime=...), and keeps separate-verifier behavior (defer scoring, artifact collection) unchanged. Separate graders still use the normal verifier runtime, not Compose.

The shared cli / _communicate helpers accept an optional env dict merged over the host environment so Compose commands get Harbor infra variables.

Docs document Compose behavior, limits (no GPU/restricted net, no sidecar collect on Compose), and clarify that ENTRYPOINT replacement only applies outside Compose.

Reviewed by Cursor Bugbot for commit f589839. Bugbot is set up for automated code reviews on this repo. Configure here.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 6, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-06T18:13:17.407523Z 6bff76b New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Comment thread verifiers/v1/tasksets/harbor/runtime.py Outdated
port = parsed.port or (443 if parsed.scheme == "https" else 80)
if port not in self._forwarders:
listener = socket.socket()
listener.bind((self._gateway, 0))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High harbor/runtime.py:140

On Linux, callback forwarders bind only to the Compose network gateway, but host.docker.internal resolves to Docker's configured host-gateway address, so Compose containers cannot connect to these listeners and model callbacks fail. Bind the listener to an address reachable through host-gateway (such as all host interfaces), or otherwise use the same gateway address for both.

-            listener.bind((self._gateway, 0))
+            listener.bind(("0.0.0.0", 0))
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @verifiers/v1/tasksets/harbor/runtime.py around line 140:

On Linux, callback forwarders bind only to the Compose network gateway, but `host.docker.internal` resolves to Docker's configured `host-gateway` address, so Compose containers cannot connect to these listeners and model callbacks fail. Bind the listener to an address reachable through `host-gateway` (such as all host interfaces), or otherwise use the same gateway address for both.

Comment thread verifiers/v1/tasksets/harbor/runtime.py Outdated
Comment on lines +69 to +71
overlay["services"].setdefault(owner, {})["extra_hosts"] = {
"host.docker.internal": "host-gateway"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High harbor/runtime.py:69

When main uses network_mode: service:<sidecar>, the agent cannot resolve the host.docker.internal hostname in callback URLs, so model/interception requests fail. The mapping is added only to owner; add it to both main and the namespace owner.

-        overlay["services"].setdefault(owner, {})["extra_hosts"] = {
-            "host.docker.internal": "host-gateway"
-        }
+        for service in {owner, "main"}:
+            overlay["services"].setdefault(service, {})["extra_hosts"] = {
+                "host.docker.internal": "host-gateway"
+            }
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @verifiers/v1/tasksets/harbor/runtime.py around lines 69-71:

When `main` uses `network_mode: service:<sidecar>`, the agent cannot resolve the `host.docker.internal` hostname in callback URLs, so model/interception requests fail. The mapping is added only to `owner`; add it to both `main` and the namespace owner.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 28dffe7255

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +60 to +63
main: dict[str, object] = {
"image": self.config.image,
"working_dir": self.config.workdir,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the main service's authored runtime settings

When docker-compose.yaml is the sole source of main.image or main.working_dir, this override replaces those values with the default DockerConfig values (python:3.11-slim and /app). Such tasks therefore run the agent in the wrong image and directory rather than in the authored Compose environment, potentially invalidating both the solution and grading; only explicitly configured task/runtime overrides should replace these Compose fields.

Useful? React with 👍 / 👎.

Comment thread verifiers/v1/tasksets/harbor/runtime.py Outdated
Comment on lines +68 to +71
overlay = {"services": {"main": main}}
overlay["services"].setdefault(owner, {})["extra_hosts"] = {
"host.docker.internal": "host-gateway"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add the host callback alias to the main container

When main uses network_mode: service:<sidecar>, owner becomes that sidecar and this installs host.docker.internal only in the sidecar. Although the containers share a network namespace, their /etc/hosts files are separate, so the agent running in main cannot resolve the hostname returned by host_url() and consequently cannot reach the interception/model or host-local MCP endpoints. Keep any owner-specific setup needed for forwarding, but add the alias to main as well.

Useful? React with 👍 / 👎.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread verifiers/v1/tasksets/harbor/runtime.py
Comment thread verifiers/v1/tasksets/harbor/runtime.py
@macroscopeapp

macroscopeapp Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR adds a substantial Docker Compose runtime path with new container lifecycle, networking, callback, and borrowed-rollout behavior. Unresolved concerns about startup deadlines, callback reachability, Compose configuration preservation, and retries leave material runtime risk requiring human review.

Not approved because:

  • 5 blocking correctness issues found at or above your repo's Minimum Blocking Severity

No code changes detected at f589839. Prior analysis still applies.

Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more.

@xeophon
xeophon changed the base branch from main to feat/local-container-runtimes September 6, 2026 18:10
Comment on lines +66 to +68
overlay["services"].setdefault(owner, {})["ports"] = [
f"127.0.0.1::{SERVICE_PORT}"
]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High harbor/runtime.py:66

A Compose task whose main service uses network_mode: host fails to start because this code adds a ports mapping to main, which Docker Compose forbids with host networking. Skip the port mapping when the owner uses host networking.

-        overlay["services"].setdefault(owner, {})["ports"] = [
-            f"127.0.0.1::{SERVICE_PORT}"
-        ]
+        if services.get(owner, {}).get("network_mode") != "host":
+            overlay["services"].setdefault(owner, {})["ports"] = [
+                f"127.0.0.1::{SERVICE_PORT}"
+            ]
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @verifiers/v1/tasksets/harbor/runtime.py around lines 66-68:

A Compose task whose `main` service uses `network_mode: host` fails to start because this code adds a `ports` mapping to `main`, which Docker Compose forbids with host networking. Skip the port mapping when the owner uses host networking.

Comment on lines +106 to +109
self._container = (
await self._compose("ps", "--all", "--quiet", "main")
).strip()
self.info.id = self._container

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High harbor/runtime.py:106

Scaled main services cause setup to pass a newline-separated list of container IDs as one _container value to docker inspect, so Compose startup fails instead of handling replicas deterministically. Split the output into IDs and reject anything other than exactly one container (or select a replica explicitly).

-        self._container = (
-            await self._compose("ps", "--all", "--quiet", "main")
-        ).strip()
+        container_ids = (
+            await self._compose("ps", "--all", "--quiet", "main")
+        ).splitlines()
+        if len(container_ids) != 1:
+            raise SandboxError(
+                f"Expected exactly one Compose main container, got {len(container_ids)}"
+            )
+        self._container = container_ids[0]
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @verifiers/v1/tasksets/harbor/runtime.py around lines 106-109:

Scaled `main` services cause setup to pass a newline-separated list of container IDs as one `_container` value to `docker inspect`, so Compose startup fails instead of handling replicas deterministically. Split the output into IDs and reject anything other than exactly one container (or select a replica explicitly).

await self._compose("config", "--quiet")
self._created = True
# Finish an interrupted Compose launch before deleting its partial project.
await run_shielded(self._compose("up", "--detach", "--wait"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High harbor/runtime.py:105

A Compose service that remains in its starting or health-check state makes docker compose up --wait run indefinitely, so run_shielded() prevents asyncio.timeout(timeouts.setup) from stopping setup and the runtime never tears down. Pass a --wait-timeout matching the configured setup timeout (or otherwise enforce that deadline) before waiting for Compose readiness.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @verifiers/v1/tasksets/harbor/runtime.py around line 105:

A Compose service that remains in its starting or health-check state makes `docker compose up --wait` run indefinitely, so `run_shielded()` prevents `asyncio.timeout(timeouts.setup)` from stopping setup and the runtime never tears down. Pass a `--wait-timeout` matching the configured setup timeout (or otherwise enforce that deadline) before waiting for Compose readiness.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6bff76b193

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

await self._compose("config", "--quiet")
self._created = True
# Finish an interrupted Compose launch before deleting its partial project.
await run_shielded(self._compose("up", "--detach", "--wait"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bound the shielded Compose startup wait

When image pulling or a service health transition lasts beyond the configured setup timeout, run_shielded absorbs the cancellation from asyncio.timeout(timeouts.setup) and continues awaiting this command, so the timeout cannot actually stop startup and a worker may remain stuck indefinitely. Compose exposes --wait-timeout specifically to bound --wait (docker compose up); pass the remaining setup budget there or otherwise terminate the command before shielding cleanup.

Useful? React with 👍 / 👎.

)
await agents.agent.run(
task.defer_scoring() if separate else task,
runtime=runtime,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor configured retries for Compose rollouts

When a Compose rollout ends with a retryable provider or harness error and agent retries are configured, supplying this runtime makes Agent.run treat the box as borrowed; its retry loop explicitly stops after the first attempt because a borrowed box is dirty. Consequently every Compose task silently gets only one attempt while equivalent non-Compose tasks receive the configured retries; each retry needs a freshly provisioned Compose project or equivalent ownership-aware retry handling.

Useful? React with 👍 / 👎.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread verifiers/v1/tasksets/harbor/runtime.py
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

1 similar comment
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f589839. Configure here.

main: dict[str, object] = {
"image": self.config.image,
"working_dir": self.config.workdir,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overlay overwrites authored main image

High Severity

The compose overlay always sets main.image and main.working_dir from the resolved Docker config. When the task has no docker_image, that image is the runtime default (python:3.11-slim), so a later -f file replaces the authored compose image or build. An existing default image also makes Compose skip sidecar/main builds. The same overlay replaces an authored working_dir, so a preserved entrypoint can start in the wrong directory.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f589839. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant