Skip to content

Commit 681017d

Browse files
Copilotpelikhan
andauthored
Preserve HTTP API proxy target schemes
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
1 parent efcafb1 commit 681017d

5 files changed

Lines changed: 123 additions & 4 deletions

File tree

pkg/constants/version_constants.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,10 @@ const AWFAPIProxyCACertMinVersion Version = "v0.28.10"
176176
// network.verifySbxEgress for fail-closed Docker sbx egress verification.
177177
const AWFVerifySbxEgressMinVersion Version = "v0.28.13"
178178

179+
// AWFHTTPAPITargetMinVersion is the minimum AWF version that supports explicit
180+
// http:// schemes in apiProxy target hosts.
181+
const AWFHTTPAPITargetMinVersion Version = "v0.28.13"
182+
179183
// DefaultGVisorVersion is the pinned gVisor release used by the compiler-generated
180184
// install step. A specific dated release name is used instead of "latest" to ensure
181185
// reproducible, verifiable installs. Each release provides SHA-512 files for

pkg/workflow/awf_config_build.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ var awfConfigLog = logger.New("workflow:awf_config")
2626
//
2727
// The caller is responsible for writing the returned JSON to disk at the path expected
2828
// by the AWF --config flag. See BuildAWFCommand for how this is wired together.
29-
func BuildAWFConfigJSON(config AWFCommandConfig) (string, error) {
29+
func BuildAWFConfigJSON(config AWFCommandConfig) (string, error) { //nolint:largefunc // Assembles the full AWF config by section.
3030
awfConfigLog.Printf("Building AWF config JSON: engine=%s, allowed_domains=%q", config.EngineName, config.AllowedDomains)
3131

3232
// Resolve firewall config once — used for both the schema URL and the container image tag.
@@ -191,11 +191,11 @@ func BuildAWFConfigJSON(config AWFCommandConfig) (string, error) {
191191

192192
targets := map[string]*AWFAPITargetConfig{}
193193

194-
if openaiTarget := extractAPITargetHost(config.WorkflowData, "OPENAI_BASE_URL"); openaiTarget != "" {
194+
if openaiTarget := extractAPIProxyTargetHost(config.WorkflowData, "OPENAI_BASE_URL", firewallConfig); openaiTarget != "" {
195195
targets["openai"] = &AWFAPITargetConfig{Host: openaiTarget}
196196
awfConfigLog.Printf("API proxy: custom openai target=%s", openaiTarget)
197197
}
198-
if anthropicTarget := extractAPITargetHost(config.WorkflowData, "ANTHROPIC_BASE_URL"); anthropicTarget != "" {
198+
if anthropicTarget := extractAPIProxyTargetHost(config.WorkflowData, "ANTHROPIC_BASE_URL", firewallConfig); anthropicTarget != "" {
199199
targets["anthropic"] = &AWFAPITargetConfig{Host: anthropicTarget}
200200
awfConfigLog.Printf("API proxy: custom anthropic target=%s", anthropicTarget)
201201
}
@@ -246,7 +246,11 @@ func BuildAWFConfigJSON(config AWFCommandConfig) (string, error) {
246246
awfConfigLog.Printf("API proxy: copilot sessionId configured")
247247
}
248248
}
249-
if geminiTarget := GetGeminiAPITarget(config.WorkflowData, config.EngineName); geminiTarget != "" {
249+
geminiTarget := extractAPIProxyTargetHost(config.WorkflowData, "GEMINI_API_BASE_URL", firewallConfig)
250+
if geminiTarget == "" {
251+
geminiTarget = GetGeminiAPITarget(config.WorkflowData, config.EngineName)
252+
}
253+
if geminiTarget != "" {
250254
awfConfigLog.Printf("API proxy: custom gemini target=%s", geminiTarget)
251255
targets["gemini"] = &AWFAPITargetConfig{Host: geminiTarget}
252256
}

pkg/workflow/awf_config_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,97 @@ func TestBuildAWFConfigJSON(t *testing.T) {
388388
assert.Contains(t, jsonStr, "my-proxy.internal.example.com", "should include the openai host")
389389
})
390390

391+
t.Run("HTTP API target schemes follow the effective AWF version", func(t *testing.T) {
392+
tests := []struct {
393+
name string
394+
engine string
395+
envVar string
396+
baseURL string
397+
version string
398+
provider string
399+
expectedHost string
400+
}{
401+
{
402+
name: "openai preserves HTTP with the default AWF version",
403+
engine: "codex",
404+
envVar: "OPENAI_BASE_URL",
405+
baseURL: "http://openai-gateway.example.com/v1",
406+
provider: "openai",
407+
expectedHost: "http://openai-gateway.example.com",
408+
},
409+
{
410+
name: "anthropic preserves HTTP at the minimum AWF version",
411+
engine: "claude",
412+
envVar: "ANTHROPIC_BASE_URL",
413+
baseURL: "http://anthropic-gateway.example.com/v1",
414+
version: string(constants.AWFHTTPAPITargetMinVersion),
415+
provider: "anthropic",
416+
expectedHost: "http://anthropic-gateway.example.com",
417+
},
418+
{
419+
name: "gemini preserves HTTP with a newer AWF version",
420+
engine: "gemini",
421+
envVar: "GEMINI_API_BASE_URL",
422+
baseURL: "http://gemini-gateway.example.com/v1",
423+
version: "v0.28.14",
424+
provider: "gemini",
425+
expectedHost: "http://gemini-gateway.example.com",
426+
},
427+
{
428+
name: "older AWF versions keep HTTP targets bare",
429+
engine: "codex",
430+
envVar: "OPENAI_BASE_URL",
431+
baseURL: "http://legacy-gateway.example.com/v1",
432+
version: "v0.28.12",
433+
provider: "openai",
434+
expectedHost: "legacy-gateway.example.com",
435+
},
436+
{
437+
name: "HTTPS targets remain bare",
438+
engine: "claude",
439+
envVar: "ANTHROPIC_BASE_URL",
440+
baseURL: "https://secure-gateway.example.com/v1",
441+
provider: "anthropic",
442+
expectedHost: "secure-gateway.example.com",
443+
},
444+
{
445+
name: "bare targets remain bare",
446+
engine: "gemini",
447+
envVar: "GEMINI_API_BASE_URL",
448+
baseURL: "bare-gateway.example.com/v1",
449+
provider: "gemini",
450+
expectedHost: "bare-gateway.example.com",
451+
},
452+
}
453+
454+
for _, tt := range tests {
455+
t.Run(tt.name, func(t *testing.T) {
456+
config := AWFCommandConfig{
457+
EngineName: tt.engine,
458+
AllowedDomains: "github.com",
459+
WorkflowData: &WorkflowData{
460+
EngineConfig: &EngineConfig{
461+
ID: tt.engine,
462+
Env: map[string]string{tt.envVar: tt.baseURL},
463+
},
464+
NetworkPermissions: &NetworkPermissions{
465+
Firewall: &FirewallConfig{Enabled: true, Version: tt.version},
466+
},
467+
},
468+
}
469+
470+
jsonStr, err := BuildAWFConfigJSON(config)
471+
require.NoError(t, err)
472+
473+
var parsed AWFConfigFile
474+
require.NoError(t, json.Unmarshal([]byte(jsonStr), &parsed))
475+
require.NotNil(t, parsed.APIProxy)
476+
require.Contains(t, parsed.APIProxy.Targets, tt.provider)
477+
assert.Equal(t, tt.expectedHost, parsed.APIProxy.Targets[tt.provider].Host)
478+
})
479+
}
480+
})
481+
391482
t.Run("default max-ai-credits is enabled when frontmatter is unset", func(t *testing.T) {
392483
config := AWFCommandConfig{
393484
EngineName: "copilot",

pkg/workflow/awf_feature_flags.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@ func awfSupportsVerifySbxEgress(firewallConfig *FirewallConfig) bool {
109109
return awfVersionAtLeast(firewallConfig, constants.AWFVerifySbxEgressMinVersion)
110110
}
111111

112+
// awfSupportsHTTPAPITargets returns true when the effective AWF version supports
113+
// explicit http:// schemes in apiProxy target hosts.
114+
func awfSupportsHTTPAPITargets(firewallConfig *FirewallConfig) bool {
115+
return awfVersionAtLeast(firewallConfig, constants.AWFHTTPAPITargetMinVersion)
116+
}
117+
112118
// awfEmitsFilesystemAllowWrite reports whether the compiler may emit the
113119
// filesystem section of awf-config.json for this workflow.
114120
//

pkg/workflow/engine_api_targets.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,20 @@ func extractAPITargetHost(workflowData *WorkflowData, envVar string) string {
7171
return host
7272
}
7373

74+
// extractAPIProxyTargetHost returns the target host format expected by the
75+
// effective AWF version, preserving an explicit http:// scheme when supported.
76+
func extractAPIProxyTargetHost(workflowData *WorkflowData, envVar string, firewallConfig *FirewallConfig) string {
77+
host := extractAPITargetHost(workflowData, envVar)
78+
if host == "" || !awfSupportsHTTPAPITargets(firewallConfig) {
79+
return host
80+
}
81+
82+
if strings.HasPrefix(workflowData.EngineConfig.Env[envVar], "http://") {
83+
return "http://" + host
84+
}
85+
return host
86+
}
87+
7488
// extractAPIBasePath extracts the path component from a custom API base URL in engine.env.
7589
// Returns the path prefix (e.g., "/serving-endpoints") or empty string if no path is present.
7690
// Root-only paths ("/") and empty paths return empty string.

0 commit comments

Comments
 (0)