Description
When using engine.env.ANTHROPIC_BASE_URL with a GitHub Actions expression (e.g., ${{ vars.ANTHROPIC_BASE_URL }}), the AWF API proxy constructs a malformed URL by prepending https:// to a target that already includes the scheme. This only affects expression-based values — hardcoded URLs are stripped correctly at compile time.
Reproduction
Workflow .md configuration:
engine:
id: claude
version: "2.1.92"
model: claude-sonnet-4-6
env:
ANTHROPIC_BASE_URL: ${{ vars.ANTHROPIC_BASE_URL }}
network:
allowed:
- defaults
- my-custom-gateway.example.com
Where vars.ANTHROPIC_BASE_URL = https://my-custom-gateway.example.com/some-path
Compiled output
The compiler generates:
--enable-api-proxy --anthropic-api-target '${{ vars.ANTHROPIC_BASE_URL }}'
At runtime, GitHub resolves this to --anthropic-api-target 'https://my-custom-gateway.example.com/some-path'.
Expected behavior
The API proxy forwards requests to https://my-custom-gateway.example.com/some-path/v1/messages correctly.
Actual behavior
The Squid proxy logs show a request to https://https/* and returns ERR_ACCESS_DENIED (403).
Root cause
Traced through the source — this is a compile-time/runtime mismatch across three layers:
1. Compiler: pkg/workflow/awf_helpers.go — extractAPITargetHost()
host := baseURL
if idx := strings.Index(host, "://"); idx != -1 {
host = host[idx+3:]
}
At compile time, the value is the literal string ${{ vars.ANTHROPIC_BASE_URL }} — which does not contain ://. The function returns it unchanged, and the lock file emits:
--anthropic-api-target '${{ vars.ANTHROPIC_BASE_URL }}'
If the value were hardcoded (e.g., https://my-gateway.example.com), the scheme would be stripped correctly. The bug only manifests with ${{ ... }} expressions.
2. AWF CLI: src/cli.ts
At runtime, GitHub resolves the expression to the actual URL (e.g., https://my-gateway.example.com). The CLI does no scheme stripping — it passes the full URL into the container as ANTHROPIC_API_TARGET.
3. API Proxy: containers/api-proxy/server.js
The proxy reads the env var directly:
const ANTHROPIC_API_TARGET = process.env.ANTHROPIC_API_TARGET || 'api.anthropic.com';
Then uses it in two places that assume a bare hostname:
buildUpstreamPath(): new URL(reqUrl, "https://${targetHost}") — constructs https://https://my-gateway.example.com/... (double scheme)
proxyRequest(): hostname: targetHost passed to https.request() — Node treats https://my-gateway.example.com as a literal hostname, and HttpsProxyAgent sends CONNECT https://my-gateway.example.com:443 through Squid, which parses the host as https
Proposed fix
The simplest defensive fix is a one-liner in containers/api-proxy/server.js — normalize ANTHROPIC_API_TARGET to strip any scheme on startup:
- const ANTHROPIC_API_TARGET = process.env.ANTHROPIC_API_TARGET || 'api.anthropic.com';
+ const ANTHROPIC_API_TARGET = (process.env.ANTHROPIC_API_TARGET || 'api.anthropic.com').replace(/^https?:\/\//, '');
This works regardless of whether the value arrives with or without a scheme, and covers both the expression-based path and any future callers. The compiler's extractAPITargetHost() already handles hardcoded URLs correctly, so no change is needed there. An optional belt-and-suspenders strip in src/cli.ts before passing to the container would also be reasonable.
Environment
- gh-aw v0.67.1
- AWF image tag: 0.25.13
- Runner: ubuntu-latest
Description
When using
engine.env.ANTHROPIC_BASE_URLwith a GitHub Actions expression (e.g.,${{ vars.ANTHROPIC_BASE_URL }}), the AWF API proxy constructs a malformed URL by prependinghttps://to a target that already includes the scheme. This only affects expression-based values — hardcoded URLs are stripped correctly at compile time.Reproduction
Workflow
.mdconfiguration:Where
vars.ANTHROPIC_BASE_URL=https://my-custom-gateway.example.com/some-pathCompiled output
The compiler generates:
At runtime, GitHub resolves this to
--anthropic-api-target 'https://my-custom-gateway.example.com/some-path'.Expected behavior
The API proxy forwards requests to
https://my-custom-gateway.example.com/some-path/v1/messagescorrectly.Actual behavior
The Squid proxy logs show a request to
https://https/*and returnsERR_ACCESS_DENIED(403).Root cause
Traced through the source — this is a compile-time/runtime mismatch across three layers:
1. Compiler:
pkg/workflow/awf_helpers.go—extractAPITargetHost()At compile time, the value is the literal string
${{ vars.ANTHROPIC_BASE_URL }}— which does not contain://. The function returns it unchanged, and the lock file emits:If the value were hardcoded (e.g.,
https://my-gateway.example.com), the scheme would be stripped correctly. The bug only manifests with${{ ... }}expressions.2. AWF CLI:
src/cli.tsAt runtime, GitHub resolves the expression to the actual URL (e.g.,
https://my-gateway.example.com). The CLI does no scheme stripping — it passes the full URL into the container asANTHROPIC_API_TARGET.3. API Proxy:
containers/api-proxy/server.jsThe proxy reads the env var directly:
Then uses it in two places that assume a bare hostname:
buildUpstreamPath():new URL(reqUrl, "https://${targetHost}")— constructshttps://https://my-gateway.example.com/...(double scheme)proxyRequest():hostname: targetHostpassed tohttps.request()— Node treatshttps://my-gateway.example.comas a literal hostname, andHttpsProxyAgentsendsCONNECT https://my-gateway.example.com:443through Squid, which parses the host ashttpsProposed fix
The simplest defensive fix is a one-liner in
containers/api-proxy/server.js— normalizeANTHROPIC_API_TARGETto strip any scheme on startup:This works regardless of whether the value arrives with or without a scheme, and covers both the expression-based path and any future callers. The compiler's
extractAPITargetHost()already handles hardcoded URLs correctly, so no change is needed there. An optional belt-and-suspenders strip insrc/cli.tsbefore passing to the container would also be reasonable.Environment