Use case
On runner.topology: arc-dind, a workflow's services: containers are unreachable from the sandboxed agent, so any workload that must speak a service's native protocol — an integration-test suite dialing a database driver, a migration tool, a client library under test — cannot run inside the sandbox. An MCP server (per guides/mcps.md) covers the case where the agent queries the service, but not the case where the code under test is the client.
There is a small, capability-free pattern that solves this on ARC/DinD, verified end to end. This issue proposes documenting it (or productizing it — see Implementation options). Postgres is the worked example; the pattern applies to any TCP service container.
Why the documented routes don't apply on ARC/DinD
All measured on gh-aw v0.87.10 / AWF v0.28.10, ARC gha-runner-scale-set 0.14.2 in dind mode:
The pattern
The agent and the service are containers on the same DinD daemon; they need a shared network and a name, not a route through the host. docker network connect adds a second interface to the running service container on awf-net — the same mechanism AWF itself uses for topology peers — and Docker's embedded DNS (the agent's resolver) serves the alias.
# 1. Declare the service with NO published ports. Ports are unnecessary (the
# agent reaches it over awf-net) and omitting them avoids the published-ports
# check, so the default `docker` runtime profile suffices — no
# docker-sudo-iptables, no sudo, no extra capabilities.
services:
postgres:
image: postgres:18.3@sha256:...
env:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: app
options: >-
--health-cmd "pg_isready --username=app --dbname=app"
--health-interval 5s --health-timeout 5s --health-retries 24
# 2. Point the workload at the alias.
engine:
env:
ConnectionStrings__app: Host=service-db;Port=5432;Database=app;Username=app;Password=app
# 3. A pre-step launches a background waiter. awf-net is created by AWF inside
# the agent step — after pre-steps have run — so the waiter polls for it,
# then joins the service container with a stable alias.
steps:
- name: Join the service container to awf-net when it appears
run: |
for i in $(seq 1 60); do
SVC=$(docker ps --format '{{.Names}}' | grep -i postgres | head -1)
[ -n "$SVC" ] && break
sleep 2
done
if [ -z "$SVC" ]; then echo "::error::service container not found"; exit 1; fi
nohup bash -c '
for i in $(seq 1 600); do
docker network inspect awf-net >/dev/null 2>&1 && break
sleep 2
done
docker network connect --alias service-db awf-net "'"$SVC"'"' \
> "${RUNNER_TEMP}/svc-join.log" 2>&1 &
disown
The pre-step's docker CLI drives the DinD daemon over the shared socket; the daemon (already privileged in ARC dind mode) performs the namespace work, so the runner container needs no added capabilities.
Security considerations
- Join direction is load-bearing. The service joins the agent's internal network and gains no egress from it. Never attach the agent to the runner's network — that would bypass the egress firewall.
- The service container keeps its original runner-network interface (with egress), unlike a pure
--topology-attach peer. Join only trusted images — the same trust already extended to services: containers generally.
- Raw TCP between
awf-net peers does not traverse squid; the egress allowlist is unaffected.
Verified
- Manual join on a running container, then automated (pre-step) join: agent resolves the alias via embedded DNS, TCP opens, and the service answers protocol-level probes (Postgres replied
N to an SSLRequest).
- Full end-to-end workload:
dotnet test integration suite over Npgsql through an env-var connection string — 669/669 passed, 0 skipped, on a stock-capability runner pod under the default docker runtime profile.
- Evidence lives in a private repository; happy to share log excerpts on request.
Implementation options (maintainers' pick)
- Docs only: a section under reference/self-hosted-runners or the ARC/DinD guide, with the snippet above. Positioned relative to guides/mcps.md: MCP server when the agent is the client; this pattern when the workload is.
- Productize: compiler sugar (e.g.
services.<name>.attach: true or extending network.topologyAttach to service containers) emitting the waiter automatically — the pattern reduces to one frontmatter key and removes the shell from user space. The doc then shrinks to the key and the security note.
Related work
🤖 Drafted with Claude Code; the pattern was designed and tested by a human, who reviewed and approved this issue before it was filed.
Use case
On
runner.topology: arc-dind, a workflow'sservices:containers are unreachable from the sandboxed agent, so any workload that must speak a service's native protocol — an integration-test suite dialing a database driver, a migration tool, a client library under test — cannot run inside the sandbox. An MCP server (per guides/mcps.md) covers the case where the agent queries the service, but not the case where the code under test is the client.There is a small, capability-free pattern that solves this on ARC/DinD, verified end to end. This issue proposes documenting it (or productizing it — see Implementation options). Postgres is the worked example; the pattern applies to any TCP service container.
Why the documented routes don't apply on ARC/DinD
All measured on gh-aw v0.87.10 / AWF v0.28.10, ARC
gha-runner-scale-set0.14.2 in dind mode:services:containers on its owngithub_network_<hash>bridge; AWF places the agent on the internalawf-net. Two unrouted bridges on the same DinD daemon.services:ports are unreachable from the sandboxed agent —--allow-host-portsis hardcoded andallowHostServicePortsis never emitted #51433 → Allow sandbox agents to reach declared service ports #51842) works in strict mode on VM runners, but on ARC/DinD it depends on host iptables that network-isolation mode never programs (gh-aw-firewall#7266, closed not planned). Observed: thehost.docker.internalalias is attached only to the squid container, squid gets no ACL for the service port, and theawf-netgateway refuses connections.--enable-host-accessis accepted but has no agent-visible effect under isolation;--no-network-isolationwould strand the MCP gateway (the compiler bakesMCP_GATEWAY_DOMAIN="awmg-mcpg", a name that exists only on the isolated network).The pattern
The agent and the service are containers on the same DinD daemon; they need a shared network and a name, not a route through the host.
docker network connectadds a second interface to the running service container onawf-net— the same mechanism AWF itself uses for topology peers — and Docker's embedded DNS (the agent's resolver) serves the alias.The pre-step's
dockerCLI drives the DinD daemon over the shared socket; the daemon (already privileged in ARC dind mode) performs the namespace work, so the runner container needs no added capabilities.Security considerations
--topology-attachpeer. Join only trusted images — the same trust already extended toservices:containers generally.awf-netpeers does not traverse squid; the egress allowlist is unaffected.Verified
Nto an SSLRequest).dotnet testintegration suite over Npgsql through an env-var connection string — 669/669 passed, 0 skipped, on a stock-capability runner pod under the defaultdockerruntime profile.Implementation options (maintainers' pick)
services.<name>.attach: trueor extendingnetwork.topologyAttachto service containers) emitting the waiter automatically — the pattern reduces to one frontmatter key and removes the shell from user space. The doc then shrinks to the key and the security note.Related work
services:#22939, BUG:services:ports are unreachable from the sandboxed agent —--allow-host-portsis hardcoded andallowHostServicePortsis never emitted #51433, BUG: v0.86.2 does not fix #51433 —--allow-host-service-portsis inert becauselegacy-securitynever leaves network-isolation mode (your smoke-service-ports test cannot pass) #52140 —services:unreachable from the sandbox (the ARC/DinD case remains, per gh-aw-firewall#7266 not planned).--allow-host-portsin strict mode; this proposal covers the topology that fix cannot reach.🤖 Drafted with Claude Code; the pattern was designed and tested by a human, who reviewed and approved this issue before it was filed.