Skip to content
Merged
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
6 changes: 6 additions & 0 deletions docs/src/content/docs/reference/frontmatter-full.md
Original file line number Diff line number Diff line change
Expand Up @@ -1868,6 +1868,12 @@ sandbox:
# (optional)
version: "example-value"

# AWF platform.type override. Declares the GitHub deployment type so AWF can
# apply deterministic Copilot auth behavior without relying on host heuristics.
# Omit to let AWF use its default host heuristic behavior.
# (optional)
platform: "ghes"
Comment thread
Copilot marked this conversation as resolved.

# Container mounts to add when using AWF. Each mount is specified using Docker
# mount syntax: 'source:destination:mode' where mode can be 'ro' (read-only) or
# 'rw' (read-write). Example: '/host/path:/container/path:ro'
Expand Down
42 changes: 42 additions & 0 deletions pkg/parser/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,48 @@ func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_MaxDailyAICreditsN
}
}

func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_SandboxAgentPlatform(t *testing.T) {
t.Parallel()

t.Run("valid platform is accepted", func(t *testing.T) {
t.Parallel()

frontmatter := map[string]any{
"on": "push",
"sandbox": map[string]any{
"agent": map[string]any{
"id": "awf",
"platform": "ghes",
},
},
}

err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(frontmatter, "/tmp/gh-aw/awf-platform-ghes-test.md")
if err != nil {
t.Fatalf("expected sandbox.agent.platform=ghes to pass schema validation, got: %v", err)
}
})

t.Run("unknown platform is rejected", func(t *testing.T) {
t.Parallel()

frontmatter := map[string]any{
"on": "push",
"sandbox": map[string]any{
"agent": map[string]any{
"id": "awf",
"platform": "github-enterprise",
},
},
}

err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(frontmatter, "/tmp/gh-aw/awf-platform-invalid-test.md")
if err == nil {
t.Fatal("expected sandbox.agent.platform=github-enterprise to fail schema validation")
}
})
}

func TestMainWorkflowSchema_WorkflowDispatchNumberTypeDocumentation(t *testing.T) {
t.Parallel()

Expand Down
5 changes: 5 additions & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -3252,6 +3252,11 @@
"type": "string",
"description": "AWF version override used to install and run the matching firewall version."
},
"platform": {
"type": "string",
"enum": ["github.com", "ghes", "ghec", "ghec-self-hosted"],
"description": "AWF platform.type override. Declares the GitHub deployment type so AWF can apply deterministic Copilot auth behavior without relying on host heuristics. Omit to let AWF use its default host heuristic behavior."
},
"command": {
"type": "string",
"x-internal": true,
Expand Down
29 changes: 29 additions & 0 deletions pkg/workflow/awf_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ type AWFConfigFile struct {
// Network contains network egress control configuration.
Network *AWFNetworkConfig `json:"network,omitempty"`

// Platform contains GitHub deployment metadata used by AWF auth handling.
Platform *AWFPlatformConfig `json:"platform,omitempty"`

// APIProxy contains API proxy (LLM gateway) configuration.
APIProxy *AWFAPIProxyConfig `json:"apiProxy,omitempty"`

Expand All @@ -186,6 +189,12 @@ type AWFNetworkConfig struct {
BlockDomains []string `json:"blockDomains,omitempty"`
}

// AWFPlatformConfig is the "platform" section of the AWF config file.
type AWFPlatformConfig struct {
// Type is the GitHub deployment type consumed by AWF for auth behavior.
Type string `json:"type,omitempty"`
}

// AWFAPIProxyConfig is the "apiProxy" section of the AWF config file.
// It maps to the --enable-api-proxy and --*-api-target CLI flags.
type AWFAPIProxyConfig struct {
Expand Down Expand Up @@ -352,6 +361,11 @@ func BuildAWFConfigJSON(config AWFCommandConfig) (string, error) {
}
}

if platformType := extractPlatformType(config.WorkflowData); platformType != "" {
awfConfig.Platform = &AWFPlatformConfig{Type: platformType}
awfConfigLog.Printf("Platform section: type=%s", platformType)
}

// ── API proxy section ─────────────────────────────────────────────────────
// maxAICredits is taken from frontmatter/imports only; when unset (0) the
// runtime value is resolved from vars.GH_AW_DEFAULT_MAX_AI_CREDITS via a
Expand Down Expand Up @@ -517,6 +531,21 @@ func extractModelMultipliers(workflowData *WorkflowData) map[string]float64 {
return workflowData.EngineConfig.TokenWeights.Multipliers
}

// extractPlatformType returns sandbox.agent.platform only for enabled AWF sandbox
// agents, or an empty string to let AWF fall back to its default platform logic.
func extractPlatformType(workflowData *WorkflowData) string {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice helper. Consider adding a short doc comment on extractPlatformType describing precedence (disabled / unsupported sandbox type returns empty string) to match the style of the surrounding extractor functions.

if workflowData == nil || workflowData.SandboxConfig == nil || workflowData.SandboxConfig.Agent == nil {
return ""
}
if workflowData.SandboxConfig.Agent.Disabled {
return ""
}
if !isSupportedSandboxType(getAgentType(workflowData.SandboxConfig.Agent)) {
return ""
}
return workflowData.SandboxConfig.Agent.Platform

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Disabled=true guard is untested: a regression that removes this check would silently emit platform.type into AWF config even when the sandbox agent is disabled.

💡 Details and suggested test

extractPlatformType correctly short-circuits when Agent.Disabled == true, but no test covers this path. If someone inadvertently removes or inverts this guard in a future change, no test will catch it.

Add a subtest in TestBuildAWFConfigJSON (or TestBuildAWFCommand_EmbedsPlatformConfig):

t.Run("platform config is omitted when sandbox agent is disabled", func(t *testing.T) {
    config := AWFCommandConfig{
        EngineName:     "copilot",
        AllowedDomains: "github.com",
        WorkflowData: &WorkflowData{
            EngineConfig: &EngineConfig{ID: "copilot"},
            NetworkPermissions: &NetworkPermissions{
                Firewall: &FirewallConfig{Enabled: true},
            },
            SandboxConfig: &SandboxConfig{
                Agent: &AgentSandboxConfig{
                    Type:     SandboxTypeAWF,
                    Platform: "ghes",
                    Disabled: true,
                },
            },
        },
    }
    jsonStr, err := BuildAWFConfigJSON(config)
    require.NoError(t, err)
    assert.NotContains(t, jsonStr, `"platform":`, "platform should be absent when sandbox agent is disabled")
})

}
Comment thread
Copilot marked this conversation as resolved.

// extractModelFallback returns an AWFModelFallbackConfig if the workflow has configured
// sandbox.agent.model-fallback, or nil if the field is absent (letting AWF use its default).
func extractModelFallback(workflowData *WorkflowData) *AWFModelFallbackConfig {
Expand Down
94 changes: 94 additions & 0 deletions pkg/workflow/awf_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,30 @@ func TestBuildAWFConfigJSON(t *testing.T) {
assert.Contains(t, jsonStr, `"imageTag"`, "should include imageTag")
})

t.Run("platform config is omitted when sandbox agent is disabled", func(t *testing.T) {
config := AWFCommandConfig{
EngineName: "copilot",
AllowedDomains: "github.com",
WorkflowData: &WorkflowData{
EngineConfig: &EngineConfig{ID: "copilot"},
NetworkPermissions: &NetworkPermissions{
Firewall: &FirewallConfig{Enabled: true},
},
SandboxConfig: &SandboxConfig{
Agent: &AgentSandboxConfig{
Type: SandboxTypeAWF,
Platform: "ghes",
Disabled: true,
},
},
},
}

jsonStr, err := BuildAWFConfigJSON(config)
require.NoError(t, err)
assert.NotContains(t, jsonStr, `"platform":`, "platform should be absent when sandbox agent is disabled")
})

t.Run("blocked domains are included in the network section", func(t *testing.T) {
config := AWFCommandConfig{
EngineName: "copilot",
Expand Down Expand Up @@ -695,6 +719,51 @@ func TestBuildAWFConfigJSON(t *testing.T) {
assert.NotContains(t, jsonStr, `"authHeader"`, "authHeader should be absent when not configured")
})

t.Run("sandbox agent platform is emitted in awf platform config", func(t *testing.T) {
config := AWFCommandConfig{
EngineName: "copilot",
AllowedDomains: "github.com",
WorkflowData: &WorkflowData{
EngineConfig: &EngineConfig{ID: "copilot"},
NetworkPermissions: &NetworkPermissions{
Firewall: &FirewallConfig{Enabled: true},
},
SandboxConfig: &SandboxConfig{
Agent: &AgentSandboxConfig{
Type: SandboxTypeAWF,
Platform: "ghes",
},
},
},
}

jsonStr, err := BuildAWFConfigJSON(config)
require.NoError(t, err)
assert.Contains(t, jsonStr, `"platform":{"type":"ghes"}`, "should include AWF platform.type when configured")
})

t.Run("platform config is omitted when sandbox agent platform is not configured", func(t *testing.T) {
config := AWFCommandConfig{
EngineName: "copilot",
AllowedDomains: "github.com",
WorkflowData: &WorkflowData{
EngineConfig: &EngineConfig{ID: "copilot"},
NetworkPermissions: &NetworkPermissions{
Firewall: &FirewallConfig{Enabled: true},
},
SandboxConfig: &SandboxConfig{
Agent: &AgentSandboxConfig{
Type: SandboxTypeAWF,
},
},
},
}

jsonStr, err := BuildAWFConfigJSON(config)
require.NoError(t, err)
assert.NotContains(t, jsonStr, `"platform":`, "platform section should be absent when sandbox.agent.platform is unset")
})

t.Run("model-fallback is emitted when enabled is explicitly set to false", func(t *testing.T) {
disabled := TemplatableBool("false")
config := AWFCommandConfig{
Expand Down Expand Up @@ -1196,6 +1265,31 @@ func TestBuildAWFCommand_UsesConfigFile(t *testing.T) {
assert.Contains(t, command, `\"enabled\":true`, "config JSON should have apiProxy enabled")
}

func TestBuildAWFCommand_EmbedsPlatformConfig(t *testing.T) {
config := AWFCommandConfig{
EngineName: "copilot",
EngineCommand: "copilot --prompt-file /tmp/prompt.txt",
LogFile: "/tmp/gh-aw/agent-stdio.log",
AllowedDomains: "github.com",
WorkflowData: &WorkflowData{
EngineConfig: &EngineConfig{ID: "copilot"},
NetworkPermissions: &NetworkPermissions{
Firewall: &FirewallConfig{Enabled: true},
},
SandboxConfig: &SandboxConfig{
Agent: &AgentSandboxConfig{
Type: SandboxTypeAWF,
Platform: "ghes",
},
},
},
}

command := BuildAWFCommand(config)

assert.Contains(t, command, `\"platform\":{\"type\":\"ghes\"}`, "expected awf-config JSON in command to include platform.type")
}

func TestBuildAWFCommand_ResolvesMaxAICreditsFromEnv(t *testing.T) {
tests := []struct {
name string
Expand Down
7 changes: 7 additions & 0 deletions pkg/workflow/frontmatter_extraction_security.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,13 @@ func (c *Compiler) extractAgentSandboxConfig(agentVal any) *AgentSandboxConfig {
}
}

// Extract platform (AWF platform.type override)
if platformVal, hasPlatform := agentObj["platform"]; hasPlatform {
if platformStr, ok := platformVal.(string); ok {
agentConfig.Platform = platformStr
}
}

// Extract config for SRT
if configVal, hasConfig := agentObj["config"]; hasConfig {
agentConfig.Config = c.extractSRTConfig(configVal)
Expand Down
15 changes: 15 additions & 0 deletions pkg/workflow/frontmatter_extraction_security_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@ func TestExtractAgentSandboxConfigVersion(t *testing.T) {
})
}

func TestExtractAgentSandboxConfigPlatform(t *testing.T) {
compiler := &Compiler{}

t.Run("extracts sandbox.agent.platform from object format", func(t *testing.T) {
agentObj := map[string]any{
"id": "awf",
"platform": "ghes",
}

config := compiler.extractAgentSandboxConfig(agentObj)
require.NotNil(t, config, "Should extract agent sandbox config")
assert.Equal(t, "ghes", config.Platform, "Should extract sandbox.agent.platform")
})
}

func TestExtractAgentSandboxConfigModelFallback(t *testing.T) {
compiler := &Compiler{}

Expand Down
9 changes: 8 additions & 1 deletion pkg/workflow/frontmatter_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,8 @@ func TestParseFrontmatterConfig(t *testing.T) {
frontmatter := map[string]any{
"sandbox": map[string]any{
"agent": map[string]any{
"type": "awf",
"type": "awf",
"platform": "ghes",
},
},
}
Expand All @@ -271,6 +272,12 @@ func TestParseFrontmatterConfig(t *testing.T) {
if config.Sandbox == nil {
t.Fatal("Sandbox should not be nil")
}
if config.Sandbox.Agent == nil {
t.Fatal("Sandbox.Agent should not be nil")
}
if config.Sandbox.Agent.Platform != "ghes" {
t.Fatalf("Sandbox.Agent.Platform = %q, want %q", config.Sandbox.Agent.Platform, "ghes")
}
})

t.Run("handles observability configuration", func(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions pkg/workflow/sandbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type AgentSandboxConfig struct {
ID string `yaml:"id,omitempty"` // Agent ID: "awf" or "srt" (replaces Type in new object format)
Type SandboxType `yaml:"type,omitempty"` // Sandbox type: "awf" or "srt" (legacy, use ID instead)
Version string `yaml:"version,omitempty"` // AWF version override used to install and run the matching firewall version
Platform string `yaml:"platform,omitempty"` // AWF platform.type override (github.com, ghes, ghec, ghec-self-hosted)
Disabled bool `yaml:"-"` // True when agent is explicitly set to false (disables firewall). This is a runtime flag, not serialized to YAML.
DisableReason string `yaml:"-"` // Operator-authored justification from dangerously-disable-sandbox-agent feature; available for diagnostics and audit logging.
Config *SandboxRuntimeConfig `yaml:"config,omitempty"` // Custom SRT config (optional)
Expand Down
Loading