Follow-up to #22939, which reported that an AWF-sandboxed agent cannot reach a database started via GitHub Actions services:. That issue was closed by github/gh-aw-firewall#1436, which added the AWF-side capability. The compiler side was never wired up, and a subsequent change to strict-security-by-default removed the only path that emitted host-access flags at all. The result is that services: is silently unusable from the agent loop today.
Reproduction
Any workflow that declares a service and expects the agent to use it:
runs-on: ubuntu-latest
engine:
id: copilot
services:
postgres:
image: postgres:18
ports:
- 5432:5432
env:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
- Expected: the agent can connect to the service on the published host port.
- Actual: every address fails from inside the sandbox —
host.docker.internal:5432 times out, localhost:5432 and 127.0.0.1:5432 are refused.
The failure is silent and expensive. The service starts normally, the port publishes normally, and ordinary (non-sandboxed) job steps reach it fine — so DB setup steps in the job succeed and nothing looks wrong. Only the agent is blocked. In practice a capable agent will not conclude "the port is firewalled"; it concludes the database is missing and starts building one, e.g.:
No postgres data cluster present. Try starting one.
$ initdb -D /tmp/.../pgdata -U app --auth=trust
$ pg_ctl -D /tmp/.../pgdata start
$ apt-cache search <extension>
Sandbox has no apt sources configured; can't install it.
$ git clone --branch REL_16_STABLE https://github.com/postgres/postgres.git
$ make -C src/backend generated-headers
That is a full timeout budget spent compiling a database from source, next to a running service container that already had everything. Any workflow whose agent runs tests against a service is affected.
Analysis
1. Host-access flags are emitted only in legacy security mode
pkg/workflow/awf_command_builder.go:
isLegacy := agentConfig != nil && agentConfig.LegacySecurity
if isLegacy {
...
awfArgs = append(awfArgs, "--enable-host-access")
if awfSupportsAllowHostPorts(firewallConfig) {
mcpGatewayPort := int(DefaultMCPGatewayPort)
...
hostPorts := fmt.Sprintf("80,443,%d", mcpGatewayPort)
awfArgs = append(awfArgs, "--allow-host-ports", hostPorts)
}
} else {
awfHelpersLog.Print("Strict security: skipping host-access flags (default)")
}
Since strict security became the default, workflows that do not set sandbox.agent.legacy-security: enable get no host-access flags. This regressed silently on upgrade — it is compiler output, so a routine gh aw compile after a version bump drops the flags with no warning and no diagnostic. Observed when upgrading through v0.82.14.
2. The port list is hardcoded, so even legacy mode does not help
hostPorts is always 80,443,<mcp-gateway-port>. There is no input that can add 5432, 6379, or any other service port. Verifiable without running anything: add sandbox.agent.legacy-security: enable to a workflow that declares a services: postgres on 5432, recompile, and the emitted flag is exactly --allow-host-ports 80,443,8080 — the declared service port is absent.
Confirmed at runtime as well. With all three flags active (--legacy-security --enable-host-access --allow-host-ports 80,443,8080), the agent still could not reach the service and fell back to building its own local cluster under ${RUNNER_TEMP}/gh-aw/agent/pgdata. --enable-host-access is therefore not sufficient on its own — the port allowlist is the operative control, and legacy security mode is not a viable workaround for services: access. Opting into legacy-security therefore restores broad host access and still leaves every service port blocked — the worst of both trade-offs: weaker sandbox, same failure.
3. The field designed for this exists in the schema but has no implementation
pkg/workflow/schemas/awf-config.schema.json defines both:
"allowHostPorts": {
"description": "Host TCP ports the agent may connect to (e.g. local dev services)."
},
"allowHostServicePorts": {
"description": "Named service ports on the host that the agent may connect to."
}
A repo-wide search for allowHostServicePorts returns exactly one hit: that schema file. No Go code reads or emits it. allowHostPorts appears in several files but only via the hardcoded CLI flag above; gh-aw does not write a security block into the generated awf-config.json at all.
So AWF can accept a service-port allowlist, and gh-aw never sends one. That is the gap between #22939's fix and observed behavior.
Proposed fix
Derive the allowlist from the services: block the workflow already declares, and emit it under strict security. The user has declared the service and its ports explicitly; honoring exactly those ports is a narrow, auditable grant that does not require the broad host access of legacy mode.
Two mechanisms, in priority order:
- Primary (automatic): parse the
ports: mappings of each entry under services: and pass the host-side ports through. Zero new syntax; existing workflows start working on recompile.
- Secondary (explicit escape hatch): a frontmatter field for ports not expressible via
services: (e.g. a daemon started by a prior job step).
sandbox:
agent:
allow-host-ports: [5432, 6379]
Both should work without legacy-security.
Implementation plan
1. Schema — pkg/parser/schemas/main_workflow_schema.json
Add allow-host-ports alongside legacy-security (same object: /properties/sandbox/.../properties/agent/...):
"allow-host-ports": {
"type": "array",
"items": { "type": "integer", "minimum": 1, "maximum": 65535 },
"uniqueItems": true,
"description": "Additional host TCP ports the agent may connect to. Ports published by `services:` are allowed automatically; use this only for ports not declared there."
}
2. Parser — wherever LegacySecurity is populated into the agent config
Add AllowHostPorts []int. Validate: reject values outside 1–65535 and reject any port in the AWF dangerousPorts set, following the repo error template [what's wrong]. [what's expected]. [example]:
return fmt.Errorf("invalid allow-host-ports value: %d. Expected a TCP port between 1 and 65535. Example: allow-host-ports: [5432]", port)
3. Service-port derivation — new helper in pkg/workflow
collectServiceHostPorts(workflowData *WorkflowData) []int — walk services:, parse each ports: entry, take the host side of "HOST:CONTAINER" (and the sole port for a bare "5432"). Handle the "127.0.0.1:5432:5432" form by taking the middle field. Ignore /udp suffixes (AWF allows TCP only). Deduplicate against the explicit list.
4. Emission — pkg/workflow/awf_command_builder.go
Move --allow-host-ports out of the isLegacy branch. Build the union of {80, 443, mcpGatewayPort} (existing behavior), derived service ports, and explicit allow-host-ports; sort and dedupe for deterministic lock files. Keep the awfSupportsAllowHostPorts version gate (AWFAllowHostPortsMinVersion, currently v0.25.24) so older pinned AWF versions still do not receive the flag. When the version gate fails but ports were requested, emit a compile warning naming the minimum version — silent omission is what made this hard to spot.
Note --enable-host-access should stay legacy-only. Allowing a declared port list is a narrower grant and should not imply full host access — and the runtime evidence above shows host access without the port is useless anyway, so the port list is the control that matters. If AWF turns out to require the two together, that coupling is worth stating explicitly in the AWF docs rather than leaving callers to infer it.
5. Tests — pkg/workflow/awf_command_builder_test.go
- strict security + a
services: block with 5432:5432 → args contain --allow-host-ports including 5432, and do not contain --enable-host-access
- strict security, no services, no explicit ports → unchanged from today (no regression for workflows that need nothing)
- explicit
allow-host-ports: [9200] merges with derived service ports, sorted and deduped
- port-mapping parse table:
"5432:5432", "5432", "127.0.0.1:5432:5432", "5432:5432/udp"
- AWF version below
AWFAllowHostPortsMinVersion with ports requested → flag omitted and a warning emitted
- legacy security + services → still emits
--enable-host-access, port list now includes the service ports
6. Docs
Document in the sandbox/security page that services: ports are reachable from the agent, and note the behavior change for anyone who previously worked around this. A short note in the upgrade guide would help others who hit the silent regression.
Workaround for anyone finding this issue
There is currently no configuration that makes a services: port reachable from the agent. legacy-security: enable restores host access but not the port, and has been confirmed at runtime not to help. The only options are to run the dependency inside the agent's own environment, or to move the work that needs it into a non-sandboxed job step.
Follow-up to #22939, which reported that an AWF-sandboxed agent cannot reach a database started via GitHub Actions
services:. That issue was closed by github/gh-aw-firewall#1436, which added the AWF-side capability. The compiler side was never wired up, and a subsequent change to strict-security-by-default removed the only path that emitted host-access flags at all. The result is thatservices:is silently unusable from the agent loop today.Reproduction
Any workflow that declares a service and expects the agent to use it:
host.docker.internal:5432times out,localhost:5432and127.0.0.1:5432are refused.The failure is silent and expensive. The service starts normally, the port publishes normally, and ordinary (non-sandboxed) job steps reach it fine — so DB setup steps in the job succeed and nothing looks wrong. Only the agent is blocked. In practice a capable agent will not conclude "the port is firewalled"; it concludes the database is missing and starts building one, e.g.:
That is a full timeout budget spent compiling a database from source, next to a running service container that already had everything. Any workflow whose agent runs tests against a service is affected.
Analysis
1. Host-access flags are emitted only in legacy security mode
pkg/workflow/awf_command_builder.go:Since strict security became the default, workflows that do not set
sandbox.agent.legacy-security: enableget no host-access flags. This regressed silently on upgrade — it is compiler output, so a routinegh aw compileafter a version bump drops the flags with no warning and no diagnostic. Observed when upgrading throughv0.82.14.2. The port list is hardcoded, so even legacy mode does not help
hostPortsis always80,443,<mcp-gateway-port>. There is no input that can add5432,6379, or any other service port. Verifiable without running anything: addsandbox.agent.legacy-security: enableto a workflow that declares aservices:postgres on5432, recompile, and the emitted flag is exactly--allow-host-ports 80,443,8080— the declared service port is absent.Confirmed at runtime as well. With all three flags active (
--legacy-security --enable-host-access --allow-host-ports 80,443,8080), the agent still could not reach the service and fell back to building its own local cluster under${RUNNER_TEMP}/gh-aw/agent/pgdata.--enable-host-accessis therefore not sufficient on its own — the port allowlist is the operative control, and legacy security mode is not a viable workaround forservices:access. Opting intolegacy-securitytherefore restores broad host access and still leaves every service port blocked — the worst of both trade-offs: weaker sandbox, same failure.3. The field designed for this exists in the schema but has no implementation
pkg/workflow/schemas/awf-config.schema.jsondefines both:A repo-wide search for
allowHostServicePortsreturns exactly one hit: that schema file. No Go code reads or emits it.allowHostPortsappears in several files but only via the hardcoded CLI flag above; gh-aw does not write asecurityblock into the generatedawf-config.jsonat all.So AWF can accept a service-port allowlist, and gh-aw never sends one. That is the gap between #22939's fix and observed behavior.
Proposed fix
Derive the allowlist from the
services:block the workflow already declares, and emit it under strict security. The user has declared the service and its ports explicitly; honoring exactly those ports is a narrow, auditable grant that does not require the broad host access of legacy mode.Two mechanisms, in priority order:
ports:mappings of each entry underservices:and pass the host-side ports through. Zero new syntax; existing workflows start working on recompile.services:(e.g. a daemon started by a prior job step).Both should work without
legacy-security.Implementation plan
1. Schema —
pkg/parser/schemas/main_workflow_schema.jsonAdd
allow-host-portsalongsidelegacy-security(same object:/properties/sandbox/.../properties/agent/...):2. Parser — wherever
LegacySecurityis populated into the agent configAdd
AllowHostPorts []int. Validate: reject values outside 1–65535 and reject any port in the AWFdangerousPortsset, following the repo error template [what's wrong]. [what's expected]. [example]:3. Service-port derivation — new helper in
pkg/workflowcollectServiceHostPorts(workflowData *WorkflowData) []int— walkservices:, parse eachports:entry, take the host side of"HOST:CONTAINER"(and the sole port for a bare"5432"). Handle the"127.0.0.1:5432:5432"form by taking the middle field. Ignore/udpsuffixes (AWF allows TCP only). Deduplicate against the explicit list.4. Emission —
pkg/workflow/awf_command_builder.goMove
--allow-host-portsout of theisLegacybranch. Build the union of{80, 443, mcpGatewayPort}(existing behavior), derived service ports, and explicitallow-host-ports; sort and dedupe for deterministic lock files. Keep theawfSupportsAllowHostPortsversion gate (AWFAllowHostPortsMinVersion, currentlyv0.25.24) so older pinned AWF versions still do not receive the flag. When the version gate fails but ports were requested, emit a compile warning naming the minimum version — silent omission is what made this hard to spot.Note
--enable-host-accessshould stay legacy-only. Allowing a declared port list is a narrower grant and should not imply full host access — and the runtime evidence above shows host access without the port is useless anyway, so the port list is the control that matters. If AWF turns out to require the two together, that coupling is worth stating explicitly in the AWF docs rather than leaving callers to infer it.5. Tests —
pkg/workflow/awf_command_builder_test.goservices:block with5432:5432→ args contain--allow-host-portsincluding5432, and do not contain--enable-host-accessallow-host-ports: [9200]merges with derived service ports, sorted and deduped"5432:5432","5432","127.0.0.1:5432:5432","5432:5432/udp"AWFAllowHostPortsMinVersionwith ports requested → flag omitted and a warning emitted--enable-host-access, port list now includes the service ports6. Docs
Document in the sandbox/security page that
services:ports are reachable from the agent, and note the behavior change for anyone who previously worked around this. A short note in the upgrade guide would help others who hit the silent regression.Workaround for anyone finding this issue
There is currently no configuration that makes a
services:port reachable from the agent.legacy-security: enablerestores host access but not the port, and has been confirmed at runtime not to help. The only options are to run the dependency inside the agent's own environment, or to move the work that needs it into a non-sandboxed job step.