Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/smoke-project.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pkg/constants/spec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,8 @@ func TestSpec_VersionConstraints_MinVersionValues(t *testing.T) {
{name: "AWFCliProxyMinVersion", constant: constants.AWFCliProxyMinVersion, expected: "v0.25.17"},
// From spec: CopilotNoAskUserMinVersion // "1.0.19"
{name: "CopilotNoAskUserMinVersion", constant: constants.CopilotNoAskUserMinVersion, expected: "1.0.19"},
// From spec: AWFModelsMinVersion // "v0.25.38"
{name: "AWFModelsMinVersion", constant: constants.AWFModelsMinVersion, expected: "v0.25.38"},
}

for _, tt := range tests {
Expand Down
7 changes: 7 additions & 0 deletions pkg/constants/version_constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ const AWFCliProxyMinVersion Version = "v0.25.17"
// --allow-host-ports or the run will fail at startup with an unknown flag error.
const AWFAllowHostPortsMinVersion Version = "v0.25.24"

// AWFModelsMinVersion is the minimum AWF version that supports the apiProxy.models
// config field. This field allows workflows to define model aliases and fallback
// policies for AI model selection.
// Workflows pinning an older AWF version must not emit apiProxy.models or the run
// will fail at startup with "config.models is not supported".
const AWFModelsMinVersion Version = "v0.25.38"

// CopilotNoAskUserMinVersion is the minimum Copilot CLI version that supports the --no-ask-user
// flag, which enables fully autonomous agentic runs by suppressing interactive prompts.
// Workflows using an older Copilot CLI version must not emit --no-ask-user or the run will fail.
Expand Down
4 changes: 3 additions & 1 deletion pkg/workflow/awf_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,9 @@ func BuildAWFConfigJSON(config AWFCommandConfig) (string, error) {
}

// ── Models section (nested under apiProxy per AWF config schema) ──────────
if config.WorkflowData != nil && len(config.WorkflowData.ModelMappings) > 0 {
// apiProxy.models requires AWF v0.25.38+; skip for workflows that pin an older version
// to avoid "config.models is not supported" startup failures.
if config.WorkflowData != nil && len(config.WorkflowData.ModelMappings) > 0 && awfSupportsModels(firewallConfig) {
apiProxy.Models = config.WorkflowData.ModelMappings
awfConfigLog.Printf("Models section: %d alias entries", len(config.WorkflowData.ModelMappings))
}
Expand Down
31 changes: 31 additions & 0 deletions pkg/workflow/awf_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -684,3 +684,34 @@ func awfSupportsAllowHostPorts(firewallConfig *FirewallConfig) bool {
minVersion := string(constants.AWFAllowHostPortsMinVersion)
return semverutil.Compare(versionStr, minVersion) >= 0
}

// awfSupportsModels returns true when the effective AWF version supports the
// apiProxy.models config field for model alias and fallback policy definitions.
//
// The apiProxy.models field was introduced in AWF v0.25.38. Any workflow that
// pins an explicit version older than v0.25.38 must not emit apiProxy.models or
// the run will fail at startup with "config.models is not supported".
//
// Special cases:
// - No version override (firewallConfig is nil or has no Version): use DefaultFirewallVersion
// and compare against AWFModelsMinVersion (currently this returns true because
// DefaultFirewallVersion is at or above the minimum supported version).
// - "latest": always returns true (latest is always a new release).
// - Any semver string ≥ AWFModelsMinVersion: returns true.
// - Any semver string < AWFModelsMinVersion: returns false.
// - Non-semver string (e.g. a branch name): returns false (conservative).
func awfSupportsModels(firewallConfig *FirewallConfig) bool {
var versionStr string
if firewallConfig != nil && firewallConfig.Version != "" {
versionStr = firewallConfig.Version
} else {
versionStr = string(constants.DefaultFirewallVersion)
}

if strings.EqualFold(versionStr, "latest") {
return true
}

minVersion := string(constants.AWFModelsMinVersion)
return semverutil.Compare(versionStr, minVersion) >= 0
}
69 changes: 67 additions & 2 deletions pkg/workflow/awf_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1173,8 +1173,73 @@ func TestAWFSupportsAllowHostPorts(t *testing.T) {
}
}

// TestGetGeminiAPITarget tests the GetGeminiAPITarget helper that resolves the effective
// Gemini API target from GEMINI_API_BASE_URL in engine.env or the default endpoint.
// TestAWFSupportsModels tests the awfSupportsModels version gate function.
func TestAWFSupportsModels(t *testing.T) {
tests := []struct {
name string
firewallConfig *FirewallConfig
want bool
}{
{
name: "nil firewall config returns true (uses default version)",
firewallConfig: nil,
want: true,
},
{
name: "empty version returns true (uses default version)",
firewallConfig: &FirewallConfig{},
want: true,
},
{
name: "latest returns true",
firewallConfig: &FirewallConfig{Version: "latest"},
want: true,
},
{
name: "v0.25.38 supports apiProxy.models (exact minimum version)",
firewallConfig: &FirewallConfig{Version: "v0.25.38"},
want: true,
},
{
name: "v0.25.41 supports apiProxy.models",
firewallConfig: &FirewallConfig{Version: "v0.25.41"},
want: true,
},
{
name: "v0.26.0 supports apiProxy.models",
firewallConfig: &FirewallConfig{Version: "v0.26.0"},
want: true,
},
{
name: "v0.25.37 does not support apiProxy.models",
firewallConfig: &FirewallConfig{Version: "v0.25.37"},
want: false,
},
{
name: "v0.25.0 does not support apiProxy.models",
firewallConfig: &FirewallConfig{Version: "v0.25.0"},
want: false,
},
{
name: "v0.24.0 does not support apiProxy.models",
firewallConfig: &FirewallConfig{Version: "v0.24.0"},
want: false,
},
{
name: "non-semver branch name returns false (conservative)",
firewallConfig: &FirewallConfig{Version: "main"},
want: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := awfSupportsModels(tt.firewallConfig)
assert.Equal(t, tt.want, got, "awfSupportsModels result")
})
}
}

func TestGetGeminiAPITarget(t *testing.T) {
tests := []struct {
name string
Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/compiler_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,7 @@ type WorkflowData struct {
CachedAllowedDomainsStr string // cached allowed-domains string for sanitization (for performance optimization); computed once and reused across multiple compilation steps
CachedAllowedDomainsComputed bool // true once CachedAllowedDomainsStr has been set; distinguishes "computed empty" from "not yet computed"
KnownActionCredentialEnvVars map[string]bool // env vars for clean_known_action_credentials.sh; keyed by GH_AW_CLEAN_* names; nil when no known credential-leaking actions are detected
ModelMappings map[string][]string // merged model alias map (builtins + imported workflow aliases + main frontmatter overrides, in priority order); NOT yet emitted to AWF config JSON — pending AWF firewall support (config.models)
ModelMappings map[string][]string // merged model alias map (builtins + imported workflow aliases + main frontmatter overrides, in priority order); emitted under apiProxy.models in the AWF config JSON when the firewall version supports it (AWF v0.25.38+, see awfSupportsModels)
}

// PinContext returns an actionpins.PinContext backed by this WorkflowData.
Expand Down
22 changes: 22 additions & 0 deletions pkg/workflow/model_aliases_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,28 @@ func TestBuildAWFConfigJSON_ModelsSection(t *testing.T) {

assert.NotContains(t, jsonStr, `"models"`, "models section should be absent when ModelMappings is nil")
})

t.Run("no models section when firewall version is older than v0.25.38", func(t *testing.T) {
config := AWFCommandConfig{
EngineName: "copilot",
AllowedDomains: "github.com",
WorkflowData: &WorkflowData{
EngineConfig: &EngineConfig{ID: "copilot"},
NetworkPermissions: &NetworkPermissions{
// Pin an old version that does not support apiProxy.models.
Firewall: &FirewallConfig{Enabled: true, Version: "v0.25.37"},
},
ModelMappings: MergeImportedModelAliases(nil, nil),
},
}

jsonStr, err := BuildAWFConfigJSON(config)
require.NoError(t, err, "BuildAWFConfigJSON should not return an error for old version")

// models must NOT appear when the pinned firewall version predates apiProxy.models support.
assert.NotContains(t, jsonStr, `"models"`,
"models section must be absent for firewall versions older than v0.25.38 to avoid startup failure")
})
}

// TestMergeImportedModelAliases verifies the three-layer merge: builtins → imports → main.
Expand Down