Skip to content

fix(container): pin host.docker.internal to OneCLI's bridge IP in rootless Docker#2168

Open
kpscheffel wants to merge 1 commit into
nanocoai:mainfrom
kpscheffel:pr/onecli-bridge-routing
Open

fix(container): pin host.docker.internal to OneCLI's bridge IP in rootless Docker#2168
kpscheffel wants to merge 1 commit into
nanocoai:mainfrom
kpscheffel:pr/onecli-bridge-routing

Conversation

@kpscheffel
Copy link
Copy Markdown

Type of Change

  • Fix - bug fix or security fix to source code

Description

What. When OneCLI is on the default bridge Docker network, pin the agent container's host.docker.internal mapping to OneCLI's bridge IP at spawn time instead of relying on host-gateway. Falls back to host-gateway when OneCLI isn't on the bridge, so the default behaviour for rootful Docker / non-OneCLI installs is unchanged.

Why. In rootless Docker, OneCLI's gateway port (10255) is bound to 127.0.0.1 inside rootlesskit. The agent container reaches it via HTTPS_PROXY=http://...@host.docker.internal:10255, which on Linux resolves to the bridge gateway (host-gateway). Whether the gateway forwards back into rootlesskit's loopback is brittle — it silently breaks across host restarts, leaving the next agent container unable to reach the proxy. The SDK call dies with ConnectionRefused and (pre-#2167) the runner used to ack the message as completed, producing a silent task day. The systemd-unit hint ExecStartPre=-/usr/bin/docker network connect bridge onecli is a no-op when OneCLI is already on the bridge — it doesn't fix the actual routing flake.

OneCLI is on bridge alongside every spawned agent container, and container-to-container traffic on a shared bridge always works. Pinning host.docker.internal to OneCLI's bridge IP turns the proxy lookup into a direct bridge hop with no rootlesskit involvement — same hostname, different IP, no other agent code changes.

How it works.

  • src/container-runtime.ts — adds getOnecliBridgeIp() (a 5 s cap'd docker inspect onecli --format '{{ index .NetworkSettings.Networks "bridge" "IPAddress" }}', returns null on any failure / non-IPv4 output / Go template's <no value> literal). hostGatewayArgs(opts?) accepts an onecliBridgeIp and returns --add-host=host.docker.internal:<ip> when it's a valid IP, otherwise the existing host-gateway value.
  • src/container-runner.ts — calls getOnecliBridgeIp() once per spawn, passes the result into hostGatewayArgs, logs a single line when it pins.

How it was tested.

  • 7 new unit tests in src/container-runtime.test.ts covering: non-Linux returns empty (host.docker.internal is built-in on macOS/Windows); Linux + no IP falls back to host-gateway; Linux + IP pins to that IP; getOnecliBridgeIp happy path; OneCLI not on bridge (empty output); Go template <no value> literal; docker inspect throws.
  • Verified on a real install: before the patch, a fresh agent container's curl to host.docker.internal:10255 was `Failed to connect (Couldn't connect to server)` while curl to OneCLI's bridge IP `172.17.0.2:10255` succeeded — confirming the rootless-loopback gap. After deploying, the new log line `Pinning host.docker.internal to OneCLI bridge IP onecliBridgeIp="172.17.0.2"` fires at every spawn, the agent container's host.docker.internal resolves to 172.17.0.2, and an end-to-end SDK call through the proxy succeeds.

…ass rootless Docker port-forward flake

In rootless Docker, OneCLI's gateway port (10255) is bound to 127.0.0.1
inside rootlesskit. The agent container reaches it via
HTTPS_PROXY=http://...@host.docker.internal:10255, which on Linux
resolves to the bridge gateway (host-gateway feature). Whether the
gateway forwards back into rootlesskit's loopback is brittle — it
silently breaks across host restarts, leaving the next agent container
unable to reach the proxy. The SDK call dies with ConnectionRefused,
the runner used to ack the message as completed (the v2 lie-by-omission
just patched), and the user gets a silent task day.

OneCLI is on the default `bridge` network alongside every spawned
agent container, and container-to-container traffic on a shared
bridge always works. So pin host.docker.internal to OneCLI's bridge
IP at spawn time — the proxy lookup becomes a direct bridge hop, no
rootlesskit involved.

Falls back to host-gateway when OneCLI isn't on the bridge (rootful
Docker, OneCLI not installed, etc.) so the default behaviour is
unchanged for those setups.

Adds 7 unit tests covering the platform branches, the IP-pinning
branch, and the four `getOnecliBridgeIp` failure modes (valid IP,
empty output, "<no value>" go-template literal, docker inspect
throws).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

PR: Fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant