Skip to content
8 changes: 6 additions & 2 deletions actions/setup/sh/install_awf_binary.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# its SHA256 checksum before installation to protect against supply chain attacks.
#
# Arguments:
# VERSION - AWF version to install (e.g., v0.25.10)
# VERSION - AWF version to install (e.g., v0.25.10) or "latest"
#
# Install strategy:
# 1. If Node.js >= 20 is available, download the lightweight awf-bundle.js (~357KB)
Expand Down Expand Up @@ -44,7 +44,11 @@ ARCH="$(uname -m)"
echo "Installing awf with checksum verification (version: ${AWF_VERSION}, os: ${OS}, arch: ${ARCH})"

# Download URLs
BASE_URL="https://github.com/${AWF_REPO}/releases/download/${AWF_VERSION}"
if [ "$AWF_VERSION" = "latest" ]; then
BASE_URL="https://github.com/${AWF_REPO}/releases/latest/download"
else
BASE_URL="https://github.com/${AWF_REPO}/releases/download/${AWF_VERSION}"
fi
CHECKSUMS_URL="${BASE_URL}/checksums.txt"

# Platform-portable SHA256 function
Expand Down
62 changes: 62 additions & 0 deletions pkg/constants/feature_constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ type FeatureFlag string

// Feature flag identifiers
const (
// NoFeatureFlag indicates that no feature-flag-based override should be applied.
NoFeatureFlag FeatureFlag = ""
// MCPScriptsFeatureFlag is the name of the feature flag for mcp-scripts
MCPScriptsFeatureFlag FeatureFlag = "mcp-scripts"
// MCPGatewayFeatureFlag is the feature flag name for enabling MCP gateway
Expand Down Expand Up @@ -60,6 +62,66 @@ const (
// features:
// byok-copilot: true
ByokCopilotFeatureFlag FeatureFlag = "byok-copilot"
// CopilotVersionFeatureFlag overrides the default Copilot CLI version.
// When set to a non-empty string, Copilot CLI installation uses this
// version instead of DefaultCopilotVersion. Supports explicit versions
// (e.g. "1.0.21") and the "latest" tag.
//
// Workflow frontmatter usage:
//
// features:
// copilot-version: "latest"
CopilotVersionFeatureFlag FeatureFlag = "copilot-version"
// MCPGVersionFeatureFlag overrides the default MCP gateway container version.
// When set to a non-empty string, MCP gateway image references use this
// version instead of DefaultMCPGatewayVersion.
//
// Workflow frontmatter usage:
//
// features:
// mcpg-version: "v0.2.27"
MCPGVersionFeatureFlag FeatureFlag = "mcpg-version"
// FirewallVersionFeatureFlag overrides the default firewall container version.
// When set to a non-empty string, AWF installation and image references use
// this version instead of DefaultFirewallVersion.
//
// Workflow frontmatter usage:
//
// features:
// firewall-version: "v0.25.27"
FirewallVersionFeatureFlag FeatureFlag = "firewall-version"
// CodexVersionFeatureFlag overrides the default Codex CLI version.
// Supports explicit versions (e.g. "0.121.0") and the "latest" tag.
//
// Workflow frontmatter usage:
//
// features:
// codex-version: "latest"
CodexVersionFeatureFlag FeatureFlag = "codex-version"
// ClaudeVersionFeatureFlag overrides the default Claude CLI version.
// Supports explicit versions (e.g. "2.1.47") and the "latest" tag.
//
// Workflow frontmatter usage:
//
// features:
// claude-version: "latest"
ClaudeVersionFeatureFlag FeatureFlag = "claude-version"
// OpenCodeVersionFeatureFlag overrides the default OpenCode CLI version.
// Supports explicit versions and the "latest" tag.
//
// Workflow frontmatter usage:
//
// features:
// opencode-version: "latest"
OpenCodeVersionFeatureFlag FeatureFlag = "opencode-version"
// GeminiVersionFeatureFlag overrides the default Gemini CLI version.
// Supports explicit versions and the "latest" tag.
//
// Workflow frontmatter usage:
//
// features:
// gemini-version: "latest"
GeminiVersionFeatureFlag FeatureFlag = "gemini-version"
// IntegrityReactionsFeatureFlag enables reaction-based integrity promotion/demotion
// in the MCPG allow-only policy. When enabled, the compiler injects
// endorsement-reactions and disapproval-reactions fields into the allow-only policy.
Expand Down
1 change: 1 addition & 0 deletions pkg/workflow/claude_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ func (e *ClaudeEngine) GetInstallationSteps(workflowData *WorkflowData) []GitHub
string(constants.DefaultClaudeCodeVersion),
"Install Claude Code CLI",
"claude",
constants.ClaudeVersionFeatureFlag,
workflowData,
)
return BuildNpmEngineInstallStepsWithAWF(npmSteps, workflowData)
Expand Down
10 changes: 10 additions & 0 deletions pkg/workflow/claude_engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ func TestClaudeEngine(t *testing.T) {
t.Errorf("Expected '%s' in install step, got: %s", expectedInstallCommand, installStep)
}

featureInstallSteps := engine.GetInstallationSteps(&WorkflowData{
Features: map[string]any{
string(constants.ClaudeVersionFeatureFlag): "latest",
},
})
featureInstallStep := strings.Join([]string(featureInstallSteps[1]), "\n")
if !strings.Contains(featureInstallStep, "npm install --ignore-scripts -g @anthropic-ai/claude-code@latest") {
t.Errorf("Expected claude-version feature override to use latest, got: %s", featureInstallStep)
}

// Test execution steps
workflowData := &WorkflowData{
Name: "test-workflow",
Expand Down
8 changes: 7 additions & 1 deletion pkg/workflow/codex_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,17 @@ func (e *CodexEngine) GetInstallationSteps(workflowData *WorkflowData) []GitHubA
}

// Use base installation steps (npm install only; secret validation is in the activation job)
codexVersion := resolveEngineInstallVersion(
string(constants.DefaultCodexVersion),
constants.CodexVersionFeatureFlag,
workflowData,
)

steps := GetBaseInstallationSteps(EngineInstallConfig{
Secrets: []string{"CODEX_API_KEY", "OPENAI_API_KEY"},
DocsURL: "https://github.github.com/gh-aw/reference/engines/#openai-codex",
NpmPackage: "@openai/codex",
Version: string(constants.DefaultCodexVersion),
Version: codexVersion,
Name: "Codex CLI",
InstallStepName: "Install Codex CLI",
CliName: "codex",
Expand Down
19 changes: 19 additions & 0 deletions pkg/workflow/codex_engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,25 @@ func TestCodexEngineWithVersion(t *testing.T) {
if !foundVersionInstall {
t.Error("Expected versioned npm install command with @openai/codex@3.0.1")
}

// Test installation steps with feature-based version override
stepsWithFeatureVersion := engine.GetInstallationSteps(&WorkflowData{
Features: map[string]any{
string(constants.CodexVersionFeatureFlag): "latest",
},
})
foundFeatureVersionInstall := false
for _, step := range stepsWithFeatureVersion {
for _, line := range step {
if strings.Contains(line, "npm install") && strings.Contains(line, "@openai/codex@latest") {
foundFeatureVersionInstall = true
break
}
}
}
if !foundFeatureVersionInstall {
t.Error("Expected feature version override npm install command with @openai/codex@latest")
}
}

func TestCodexEngineExecutionIncludesGitHubAWPrompt(t *testing.T) {
Expand Down
9 changes: 5 additions & 4 deletions pkg/workflow/copilot_engine_installation.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,11 @@ func (e *CopilotEngine) GetInstallationSteps(workflowData *WorkflowData) []GitHu
}

// Determine Copilot version
copilotVersion := string(constants.DefaultCopilotVersion)
if workflowData.EngineConfig != nil && workflowData.EngineConfig.Version != "" {
copilotVersion = workflowData.EngineConfig.Version
}
copilotVersion := resolveEngineInstallVersion(
string(constants.DefaultCopilotVersion),
constants.CopilotVersionFeatureFlag,
workflowData,
)
if isFeatureEnabled(constants.ByokCopilotFeatureFlag, workflowData) {
copilotVersion = "latest"
copilotInstallLog.Print("byok-copilot enabled: forcing Copilot CLI install version to latest")
Expand Down
90 changes: 90 additions & 0 deletions pkg/workflow/copilot_installer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,3 +281,93 @@ func TestCopilotInstallerByokFeatureUsesLatestVersion(t *testing.T) {
t.Errorf("Expected byok-copilot to ignore pinned version, got:\n%s", installStep)
}
}

func TestCopilotInstallerCopilotVersionFeatureUsesLatestVersion(t *testing.T) {
engine := NewCopilotEngine()
workflowData := &WorkflowData{
Name: "test-workflow",
Features: map[string]any{
string(constants.CopilotVersionFeatureFlag): "latest",
},
}

steps := engine.GetInstallationSteps(workflowData)

var installStep string
for _, step := range steps {
stepContent := strings.Join(step, "\n")
if strings.Contains(stepContent, "install_copilot_cli.sh") {
installStep = stepContent
break
}
}

if installStep == "" {
t.Fatal("Could not find install step with install_copilot_cli.sh")
}

if !strings.Contains(installStep, `install_copilot_cli.sh" latest`) {
t.Errorf("Expected copilot-version=latest to force latest Copilot CLI install, got:\n%s", installStep)
}
}

func TestCopilotInstallerEngineConfigVersionTakesPrecedenceOverFeatureVersion(t *testing.T) {
engine := NewCopilotEngine()
workflowData := &WorkflowData{
Name: "test-workflow",
EngineConfig: &EngineConfig{
Version: "1.0.0",
},
Features: map[string]any{
string(constants.CopilotVersionFeatureFlag): "latest",
},
}

steps := engine.GetInstallationSteps(workflowData)

var installStep string
for _, step := range steps {
stepContent := strings.Join(step, "\n")
if strings.Contains(stepContent, "install_copilot_cli.sh") {
installStep = stepContent
break
}
}

if installStep == "" {
t.Fatal("Could not find install step with install_copilot_cli.sh")
}

if !strings.Contains(installStep, `install_copilot_cli.sh" 1.0.0`) {
t.Errorf("Expected engine.version to take precedence over copilot-version feature flag, got:\n%s", installStep)
}
}

func TestCopilotInstallerCopilotVersionFeatureSupportsSpecificVersion(t *testing.T) {
engine := NewCopilotEngine()
workflowData := &WorkflowData{
Name: "test-workflow",
Features: map[string]any{
string(constants.CopilotVersionFeatureFlag): "1.2.3",
},
}

steps := engine.GetInstallationSteps(workflowData)

var installStep string
for _, step := range steps {
stepContent := strings.Join(step, "\n")
if strings.Contains(stepContent, "install_copilot_cli.sh") {
installStep = stepContent
break
}
}

if installStep == "" {
t.Fatal("Could not find install step with install_copilot_cli.sh")
}

if !strings.Contains(installStep, `install_copilot_cli.sh" 1.2.3`) {
t.Errorf("Expected copilot-version to support specific version values, got:\n%s", installStep)
}
}
1 change: 1 addition & 0 deletions pkg/workflow/crush_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ func (e *CrushEngine) GetInstallationSteps(workflowData *WorkflowData) []GitHubA
string(constants.DefaultCrushVersion),
"Install Crush CLI",
"crush",
constants.NoFeatureFlag,
workflowData,
)
return BuildNpmEngineInstallStepsWithAWF(npmSteps, workflowData)
Expand Down
7 changes: 1 addition & 6 deletions pkg/workflow/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,7 @@ func collectDockerImages(tools map[string]any, workflowData *WorkflowData, actio
mcpGateway := workflowData.SandboxConfig.MCP
if mcpGateway.Container != "" {
image := mcpGateway.Container
if mcpGateway.Version != "" {
image += ":" + mcpGateway.Version
} else {
// Use default version if not specified (consistent with mcp_servers.go)
image += ":" + string(constants.DefaultMCPGatewayVersion)
}
image += ":" + getMCPGatewayVersion(workflowData, mcpGateway.Version)
if !imageSet[image] {
images = append(images, image)
imageSet[image] = true
Expand Down
30 changes: 30 additions & 0 deletions pkg/workflow/docker_api_proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,33 @@ func TestCollectDockerImages_APIProxyForEnginesWithLLMGateway(t *testing.T) {
})
}
}

func TestCollectDockerImages_FirewallVersionFeatureOverride(t *testing.T) {
featureVersion := "v0.25.27"
featureTag := "0.25.27"
workflowData := &WorkflowData{
AI: "claude",
Features: map[string]any{
string(constants.FirewallVersionFeatureFlag): featureVersion,
},
NetworkPermissions: &NetworkPermissions{
Firewall: &FirewallConfig{
Enabled: true,
},
},
}

images := collectDockerImages(nil, workflowData, ActionModeRelease)

expectedImages := []string{
constants.DefaultFirewallRegistry + "/squid:" + featureTag,
constants.DefaultFirewallRegistry + "/agent:" + featureTag,
constants.DefaultFirewallRegistry + "/api-proxy:" + featureTag,
}

for _, expectedImage := range expectedImages {
if !slices.Contains(images, expectedImage) {
t.Errorf("Expected image %q in collected images, got %v", expectedImage, images)
}
}
}
Loading
Loading