diff --git a/docs/src/content/docs/reference/frontmatter.md b/docs/src/content/docs/reference/frontmatter.md index e4783a71082..a6a4eae804a 100644 --- a/docs/src/content/docs/reference/frontmatter.md +++ b/docs/src/content/docs/reference/frontmatter.md @@ -423,15 +423,15 @@ Debug workflow using script mode for custom actions. #### Copilot BYOK Mode (`features.byok-copilot`) -Enables Copilot offline Bring Your Own Key (BYOK) mode with a single flag, bundling three required behaviors: injecting a dummy `COPILOT_API_KEY` to trigger the AWF BYOK runtime path, implicitly enabling `cli-proxy`, and forcing the Copilot CLI to install at `latest` (ignoring any pinned `engine.version`). +`byok-copilot` is **enabled by default** for Copilot workflows. It enables Copilot offline Bring Your Own Key (BYOK) mode by bundling three behaviors: injecting a dummy `COPILOT_API_KEY` to trigger the AWF BYOK runtime path, implicitly enabling `cli-proxy`, and forcing the Copilot CLI to install at `latest` (ignoring any pinned `engine.version`). ```yaml wrap engine: copilot features: - byok-copilot: true + byok-copilot: false ``` -Without this flag, BYOK mode requires manual composition of all three behaviors. With `byok-copilot: true`, the compiler handles the wiring automatically. +Set `features.byok-copilot: false` to opt out. > [!IMPORTANT] > `byok-copilot` is a gh-aw convenience extension point, not an enforcement boundary. gh-aw does not enforce Copilot BYOK usage. diff --git a/docs/src/content/docs/reference/glossary.md b/docs/src/content/docs/reference/glossary.md index 8ec031f2c26..63798cccb76 100644 --- a/docs/src/content/docs/reference/glossary.md +++ b/docs/src/content/docs/reference/glossary.md @@ -291,7 +291,7 @@ See [Engines Reference](/gh-aw/reference/engines/). ### Feature Flags (`features:`) -A frontmatter section that enables experimental or optional compiler and runtime behaviors as key-value pairs. Feature flags provide controlled access to new capabilities before they become defaults or are fully stabilized. Common flags include `action-mode` (controls how custom action references are compiled), `copilot-requests` (enables GitHub Actions token authentication for Copilot; currently in **private preview** — will not work unless your account has been onboarded), `byok-copilot` (enables Copilot offline BYOK mode with dummy `COPILOT_API_KEY`, API proxy sidecar, implicit `cli-proxy`, and latest Copilot CLI install), `mcp-gateway` (enables the MCP gateway proxy), `integrity-reactions` (enables reaction-based integrity promotion and demotion), `cli-proxy` (enables CLI proxy mode for integrity enforcement at the network boundary), and `awf-diagnostic-logs` (enables AWF Docker operational diagnostics collection on failure). See [Frontmatter Reference](/gh-aw/reference/frontmatter/#feature-flags-features). +A frontmatter section that enables experimental or optional compiler and runtime behaviors as key-value pairs. Feature flags provide controlled access to new capabilities before they become defaults or are fully stabilized. Common flags include `action-mode` (controls how custom action references are compiled), `copilot-requests` (enables GitHub Actions token authentication for Copilot; currently in **private preview** — will not work unless your account has been onboarded), `byok-copilot` (enabled by default for Copilot workflows; set `features.byok-copilot: false` to disable; BYOK mode uses a dummy `COPILOT_API_KEY`, API proxy sidecar, implicit `cli-proxy`, and latest Copilot CLI install), `mcp-gateway` (enables the MCP gateway proxy), `integrity-reactions` (enables reaction-based integrity promotion and demotion), `cli-proxy` (enables CLI proxy mode for integrity enforcement at the network boundary), and `awf-diagnostic-logs` (enables AWF Docker operational diagnostics collection on failure). See [Frontmatter Reference](/gh-aw/reference/frontmatter/#feature-flags-features). ### Fuzzy Scheduling diff --git a/pkg/workflow/copilot_installer_test.go b/pkg/workflow/copilot_installer_test.go index 561386d6194..d830f5045f4 100644 --- a/pkg/workflow/copilot_installer_test.go +++ b/pkg/workflow/copilot_installer_test.go @@ -109,7 +109,7 @@ func TestGenerateCopilotInstallerSteps(t *testing.T) { } func TestCopilotInstallerCustomVersion(t *testing.T) { - // Test that custom version from engine config is used + // Test that custom version from engine config is used when byok-copilot is disabled. engine := NewCopilotEngine() customVersion := "1.0.0" @@ -118,6 +118,9 @@ func TestCopilotInstallerCustomVersion(t *testing.T) { EngineConfig: &EngineConfig{ Version: customVersion, }, + Features: map[string]any{ + string(constants.ByokCopilotFeatureFlag): false, + }, } steps := engine.GetInstallationSteps(workflowData) @@ -205,6 +208,7 @@ func TestGenerateCopilotInstallerSteps_ExpressionVersion(t *testing.T) { func TestCopilotInstallerExpressionVersion_ViaEngineConfig(t *testing.T) { // Test that expression version from engine config uses env var injection + // when byok-copilot is disabled. engine := NewCopilotEngine() expressionVersion := "${{ inputs.engine-version }}" @@ -213,6 +217,9 @@ func TestCopilotInstallerExpressionVersion_ViaEngineConfig(t *testing.T) { EngineConfig: &EngineConfig{ Version: expressionVersion, }, + Features: map[string]any{ + string(constants.ByokCopilotFeatureFlag): false, + }, } steps := engine.GetInstallationSteps(workflowData) diff --git a/pkg/workflow/features.go b/pkg/workflow/features.go index 2c7a0126922..c786f4afcb3 100644 --- a/pkg/workflow/features.go +++ b/pkg/workflow/features.go @@ -67,6 +67,12 @@ func isFeatureEnabled(flag constants.FeatureFlag, workflowData *WorkflowData) bo // Fall back to checking the environment variable features := os.Getenv("GH_AW_FEATURES") if features == "" { + // byok-copilot is enabled by default and can be explicitly disabled + // via features.byok-copilot: false in frontmatter. + if flag == constants.ByokCopilotFeatureFlag { + featuresLog.Printf("Feature not found, GH_AW_FEATURES empty: %s=true (default)", flagLower) + return true + } featuresLog.Printf("Feature not found, GH_AW_FEATURES empty: %s=false", flagLower) return false } @@ -83,6 +89,11 @@ func isFeatureEnabled(flag constants.FeatureFlag, workflowData *WorkflowData) bo } } + if flag == constants.ByokCopilotFeatureFlag { + featuresLog.Printf("Feature not found in GH_AW_FEATURES: %s=true (default)", flagLower) + return true + } + featuresLog.Printf("Feature not found: %s=false", flagLower) return false } diff --git a/pkg/workflow/features_test.go b/pkg/workflow/features_test.go index f4c9fb02ca6..09c524998d7 100644 --- a/pkg/workflow/features_test.go +++ b/pkg/workflow/features_test.go @@ -86,6 +86,14 @@ func TestIsFeatureEnabledNoEnv(t *testing.T) { } } +func TestIsFeatureEnabledByokCopilotDefaultOn(t *testing.T) { + t.Setenv("GH_AW_FEATURES", "") + result := isFeatureEnabled(constants.ByokCopilotFeatureFlag, nil) + if result != true { + t.Errorf("isFeatureEnabled(%q, nil) with no env = %v, want true", constants.ByokCopilotFeatureFlag, result) + } +} + func TestIsFeatureEnabledWithData(t *testing.T) { tests := []struct { name string @@ -181,6 +189,46 @@ func TestIsFeatureEnabledWithData(t *testing.T) { expected: false, description: "byok-copilot implication should only apply to engine=copilot", }, + { + name: "byok-copilot defaults to enabled for copilot workflows", + envValue: "", + frontmatter: map[string]any{}, + engineID: string(constants.CopilotEngine), + flag: constants.ByokCopilotFeatureFlag, + expected: true, + description: "byok-copilot should default to enabled", + }, + { + name: "byok-copilot can be disabled with explicit false (case-insensitive key)", + envValue: "", + frontmatter: map[string]any{ + "BYOK-copilot": false, + }, + engineID: string(constants.CopilotEngine), + flag: constants.ByokCopilotFeatureFlag, + expected: false, + description: "explicit frontmatter false should disable default byok-copilot behavior", + }, + { + name: "byok default implies cli-proxy for copilot engine", + envValue: "", + frontmatter: map[string]any{}, + engineID: string(constants.CopilotEngine), + flag: constants.CliProxyFeatureFlag, + expected: true, + description: "default byok-copilot should imply cli-proxy for engine=copilot", + }, + { + name: "explicit byok false disables cli-proxy implication", + envValue: "", + frontmatter: map[string]any{ + "BYOK-copilot": false, + }, + engineID: string(constants.CopilotEngine), + flag: constants.CliProxyFeatureFlag, + expected: false, + description: "explicit byok-copilot false should disable implicit cli-proxy", + }, } for _, tt := range tests { diff --git a/pkg/workflow/github_remote_mode_test.go b/pkg/workflow/github_remote_mode_test.go index 7d149875d38..39ae52a4b2b 100644 --- a/pkg/workflow/github_remote_mode_test.go +++ b/pkg/workflow/github_remote_mode_test.go @@ -108,6 +108,8 @@ permissions: issues: read engine: copilot strict: false +features: + byok-copilot: false tools: github: mode: remote @@ -126,6 +128,8 @@ permissions: issues: read engine: copilot strict: false +features: + byok-copilot: false tools: github: mode: remote @@ -387,6 +391,8 @@ permissions: issues: read engine: copilot strict: false +features: + byok-copilot: false tools: github: mode: remote diff --git a/pkg/workflow/model_env_vars_test.go b/pkg/workflow/model_env_vars_test.go index 49aa9bc8350..73c7e5dcdb2 100644 --- a/pkg/workflow/model_env_vars_test.go +++ b/pkg/workflow/model_env_vars_test.go @@ -225,7 +225,7 @@ func TestCopilotFallbackModelMapsToNativeEnvVar(t *testing.T) { name: "Agent job maps GH_AW_MODEL_AGENT_COPILOT to COPILOT_MODEL", safeOutputs: &SafeOutputsConfig{}, expectedOrgVar: constants.EnvVarModelAgentCopilot, - expectedTail: "''", + expectedTail: "'" + constants.CopilotBYOKDefaultModel + "'", }, { name: "Agent job with byok-copilot uses non-empty COPILOT_MODEL fallback", @@ -240,7 +240,7 @@ func TestCopilotFallbackModelMapsToNativeEnvVar(t *testing.T) { name: "Detection job maps GH_AW_MODEL_DETECTION_COPILOT to COPILOT_MODEL", safeOutputs: nil, expectedOrgVar: constants.EnvVarModelDetectionCopilot, - expectedTail: "''", + expectedTail: "'" + constants.CopilotBYOKDefaultModel + "'", }, } diff --git a/pkg/workflow/threat_detection_test.go b/pkg/workflow/threat_detection_test.go index eecfad59fb8..22381a87c1f 100644 --- a/pkg/workflow/threat_detection_test.go +++ b/pkg/workflow/threat_detection_test.go @@ -1021,10 +1021,9 @@ func TestBuildDetectionEngineExecutionStepStripsAgentField(t *testing.T) { } } -// TestCopilotDetectionDefaultModel verifies that the copilot engine uses the -// Copilot CLI's native default model for the detection step when no model is specified. -// Detection now matches main agent behavior: both use ${{ vars.* || ” }} so the -// Copilot CLI picks its native default (currently claude-sonnet-4.6). +// TestCopilotDetectionDefaultModel verifies copilot model resolution in detection steps. +// With default-on byok-copilot, workflows without an explicit model use a non-empty +// fallback: ${{ vars.* || '' }}. func TestCopilotDetectionDefaultModel(t *testing.T) { compiler := NewCompiler() @@ -1043,9 +1042,7 @@ func TestCopilotDetectionDefaultModel(t *testing.T) { }, }, shouldContainModel: true, - // Detection uses env var fallback (same pattern as main agent), allowing - // the Copilot CLI to pick its native default (currently claude-sonnet-4.6) - expectedModel: "${{ vars." + constants.EnvVarModelDetectionCopilot + " || '' }}", + expectedModel: "${{ vars." + constants.EnvVarModelDetectionCopilot + " || '" + constants.CopilotBYOKDefaultModel + "' }}", }, { name: "copilot engine with custom model uses specified model", @@ -1091,7 +1088,7 @@ func TestCopilotDetectionDefaultModel(t *testing.T) { }, }, shouldContainModel: true, - expectedModel: "${{ vars." + constants.EnvVarModelDetectionCopilot + " || '' }}", + expectedModel: "${{ vars." + constants.EnvVarModelDetectionCopilot + " || '" + constants.CopilotBYOKDefaultModel + "' }}", }, { name: "claude engine does not add model parameter",