diff --git a/pkg/constants/feature_constants.go b/pkg/constants/feature_constants.go index 5c8ce42b9d6..59587f0f792 100644 --- a/pkg/constants/feature_constants.go +++ b/pkg/constants/feature_constants.go @@ -76,4 +76,14 @@ const ( // features: // group-concurrency-queue: false GroupConcurrencyQueueFeatureFlag FeatureFlag = "group-concurrency-queue" + // DangerouslyDisableSandboxAgentFeatureFlag is required to allow sandbox.agent: false. + // Without this flag, setting sandbox.agent to false raises a validation error. + // This flag is intentionally named with "dangerously" to make the security + // implications explicit and visible in the workflow frontmatter. + // + // Workflow frontmatter usage: + // + // features: + // dangerously-disable-sandbox-agent: true + DangerouslyDisableSandboxAgentFeatureFlag FeatureFlag = "dangerously-disable-sandbox-agent" ) diff --git a/pkg/workflow/compiler_validators_test.go b/pkg/workflow/compiler_validators_test.go index 656ec2417ad..340b24f3729 100644 --- a/pkg/workflow/compiler_validators_test.go +++ b/pkg/workflow/compiler_validators_test.go @@ -384,6 +384,9 @@ func TestValidateToolConfiguration_EmitsSandboxWarningBeforeThreatDetectionError workflowData := &WorkflowData{ Name: "Test", + Features: map[string]any{ + "dangerously-disable-sandbox-agent": true, + }, SandboxConfig: &SandboxConfig{ Agent: &AgentSandboxConfig{Disabled: true}, }, diff --git a/pkg/workflow/importable_tools_test.go b/pkg/workflow/importable_tools_test.go index 211c3c751ca..364f380da4e 100644 --- a/pkg/workflow/importable_tools_test.go +++ b/pkg/workflow/importable_tools_test.go @@ -801,6 +801,8 @@ permissions: issues: read tools: bash: true +features: + dangerously-disable-sandbox-agent: true sandbox: agent: false imports: @@ -890,6 +892,8 @@ strict: false permissions: contents: read issues: read +features: + dangerously-disable-sandbox-agent: true sandbox: agent: false imports: @@ -962,6 +966,8 @@ permissions: issues: read tools: github: false +features: + dangerously-disable-sandbox-agent: true sandbox: agent: false imports: diff --git a/pkg/workflow/pull_request_target_validation_test.go b/pkg/workflow/pull_request_target_validation_test.go index bcd5bd051b1..3668bdc4a4e 100644 --- a/pkg/workflow/pull_request_target_validation_test.go +++ b/pkg/workflow/pull_request_target_validation_test.go @@ -36,6 +36,8 @@ on: types: [opened] tools: github: false +features: + dangerously-disable-sandbox-agent: true sandbox: agent: false checkout: false @@ -58,6 +60,8 @@ on: types: [opened] tools: github: false +features: + dangerously-disable-sandbox-agent: true sandbox: agent: false --- @@ -103,6 +107,8 @@ on: types: [opened] tools: github: false +features: + dangerously-disable-sandbox-agent: true sandbox: agent: false --- @@ -124,6 +130,8 @@ on: branches: [main] tools: github: false +features: + dangerously-disable-sandbox-agent: true sandbox: agent: false --- diff --git a/pkg/workflow/sandbox_agent_false_test.go b/pkg/workflow/sandbox_agent_false_test.go index bd8a914dc0e..7fc26de1a23 100644 --- a/pkg/workflow/sandbox_agent_false_test.go +++ b/pkg/workflow/sandbox_agent_false_test.go @@ -10,7 +10,7 @@ import ( ) func TestSandboxAgentMandatory(t *testing.T) { - t.Run("sandbox.agent: false is accepted and disables agent sandbox", func(t *testing.T) { + t.Run("sandbox.agent: false with feature flag is accepted and disables agent sandbox", func(t *testing.T) { // Create temp directory for test workflows workflowsDir := t.TempDir() @@ -20,13 +20,15 @@ network: allowed: - defaults - github.com +features: + dangerously-disable-sandbox-agent: true sandbox: agent: false strict: false on: workflow_dispatch --- -Test workflow to verify sandbox.agent: false is accepted and disables agent sandbox. +Test workflow to verify sandbox.agent: false is accepted when the feature flag is set. ` workflowPath := filepath.Join(workflowsDir, "test-agent-false.md") @@ -35,13 +37,13 @@ Test workflow to verify sandbox.agent: false is accepted and disables agent sand t.Fatalf("Failed to write workflow file: %v", err) } - // Compile the workflow + // Compile the workflow (validation runs unconditionally; validateSandboxConfig + // is not gated by skipValidation, so this exercises the feature-flag check) compiler := NewCompiler() - compiler.SetSkipValidation(true) - // Should succeed in non-strict mode + // Should succeed when the feature flag is set if err := compiler.CompileWorkflow(workflowPath); err != nil { - t.Fatalf("Expected compilation to succeed with sandbox.agent: false in non-strict mode, but got error: %v", err) + t.Fatalf("Expected compilation to succeed with sandbox.agent: false and feature flag, but got error: %v", err) } // Read the compiled workflow @@ -157,6 +159,43 @@ Test workflow to verify default sandbox.agent behavior (awf). }) } +func TestSandboxAgentFalseRequiresFeatureFlag(t *testing.T) { + t.Run("sandbox.agent: false without feature flag is rejected", func(t *testing.T) { + workflowsDir := t.TempDir() + + markdown := `--- +engine: copilot +network: + allowed: + - defaults + - github.com +sandbox: + agent: false +strict: false +on: workflow_dispatch +--- + +Test workflow to verify sandbox.agent: false is rejected without the feature flag. +` + + workflowPath := filepath.Join(workflowsDir, "test-agent-false-no-flag.md") + err := os.WriteFile(workflowPath, []byte(markdown), 0644) + if err != nil { + t.Fatalf("Failed to write workflow file: %v", err) + } + + compiler := NewCompiler() + + err = compiler.CompileWorkflow(workflowPath) + if err == nil { + t.Fatal("Expected compilation to fail when sandbox.agent: false without feature flag, but got nil error") + } + if !strings.Contains(err.Error(), "dangerously-disable-sandbox-agent") { + t.Fatalf("Expected error to reference 'dangerously-disable-sandbox-agent', got: %v", err) + } + }) +} + func TestNetworkFirewallFrontmatterRejected(t *testing.T) { t.Run("network.firewall is rejected by schema", func(t *testing.T) { // Create temp directory for test workflows diff --git a/pkg/workflow/sandbox_validation.go b/pkg/workflow/sandbox_validation.go index 07dd7ee7b47..c606fe276ac 100644 --- a/pkg/workflow/sandbox_validation.go +++ b/pkg/workflow/sandbox_validation.go @@ -76,12 +76,19 @@ func validateSandboxConfig(workflowData *WorkflowData) error { sandboxConfig := workflowData.SandboxConfig // Check if sandbox.agent: false was specified - // In non-strict mode, this is allowed (with a warning shown at compile time) - // The strict mode check happens in validateStrictFirewall() + // This requires the "dangerously-disable-sandbox-agent" feature flag to be enabled. + // Without the feature flag, setting sandbox.agent: false is a validation error. if sandboxConfig.Agent != nil && sandboxConfig.Agent.Disabled { - // sandbox.agent: false is allowed in non-strict mode, so we don't error here - // The warning is emitted in compiler.go - sandboxValidationLog.Print("sandbox.agent: false detected, will be validated by strict mode check") + if !isFeatureEnabled(constants.DangerouslyDisableSandboxAgentFeatureFlag, workflowData) { + flag := string(constants.DangerouslyDisableSandboxAgentFeatureFlag) + return NewValidationError( + "sandbox.agent", + "false", + fmt.Sprintf("disabling the agent sandbox requires the '%s' feature flag", flag), + fmt.Sprintf("Add the feature flag to your workflow frontmatter:\n\nfeatures:\n %s: true\nsandbox:\n agent: false\n\nSee: %s", flag, constants.DocsSandboxURL), + ) + } + sandboxValidationLog.Printf("sandbox.agent: false permitted by %s feature flag", constants.DangerouslyDisableSandboxAgentFeatureFlag) } // Validate mounts syntax if specified in agent config diff --git a/pkg/workflow/workflow_run_validation_test.go b/pkg/workflow/workflow_run_validation_test.go index 37a3dfa2750..e200ba46d71 100644 --- a/pkg/workflow/workflow_run_validation_test.go +++ b/pkg/workflow/workflow_run_validation_test.go @@ -36,6 +36,8 @@ on: types: [completed] tools: github: false +features: + dangerously-disable-sandbox-agent: true sandbox: agent: false --- @@ -81,6 +83,8 @@ on: - develop tools: github: false +features: + dangerously-disable-sandbox-agent: true sandbox: agent: false --- @@ -186,6 +190,8 @@ on: branches: [main] tools: github: false +features: + dangerously-disable-sandbox-agent: true sandbox: agent: false --- @@ -210,6 +216,8 @@ on: types: [completed] tools: github: false +features: + dangerously-disable-sandbox-agent: true sandbox: agent: false --- @@ -233,6 +241,8 @@ on: branches: [] tools: github: false +features: + dangerously-disable-sandbox-agent: true sandbox: agent: false --- @@ -303,6 +313,8 @@ strict: false on: push tools: github: false +features: + dangerously-disable-sandbox-agent: true sandbox: agent: false --- @@ -323,6 +335,8 @@ on: types: [completed] tools: github: false +features: + dangerously-disable-sandbox-agent: true sandbox: agent: false ---