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
5 changes: 5 additions & 0 deletions docs/src/content/docs/reference/frontmatter-full.md
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,11 @@ network:
# (optional)
log-level: "debug"

# Custom path to AWF binary. When specified, skips downloading AWF from GitHub
# releases. Supports absolute paths or paths relative to GITHUB_WORKSPACE.
# (optional)
path: "example-value"

# Sandbox runtime configuration for AI engines. Controls the execution sandbox
# (AWF or Sandbox Runtime). Only supported for Copilot engine.
# (optional)
Expand Down
33 changes: 33 additions & 0 deletions docs/src/content/docs/reference/network.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,39 @@ When the firewall is disabled with specific `allowed` domains:

This configuration is useful during development or when the firewall is incompatible with your workflow requirements. For production workflows, enabling the firewall is recommended for better network security.

### Custom AWF Binary Path

Specify a custom AWF binary instead of downloading from GitHub releases:

```yaml wrap
network:
firewall:
path: /usr/local/bin/awf-custom # Absolute path
allowed:
- defaults
```

Relative paths are resolved relative to the repository root:

```yaml wrap
network:
firewall:
path: bin/awf # Resolves to ${GITHUB_WORKSPACE}/bin/awf
allowed:
- defaults
```

When `path` is specified:
- AWF is not downloaded from GitHub releases
- The specified binary must exist and be executable
- Path is validated before workflow execution
- The `version` field is ignored (if also specified)

**Use cases:**
- Pre-installed AWF on self-hosted runners
- Custom AWF builds with patches or modifications
- Repository-specific AWF versions

## Best Practices

Follow the principle of least privilege by only allowing access to domains and ecosystems actually needed. Prefer ecosystem identifiers over listing individual domains. When adding custom domains, use the base domain (e.g., `trusted.com`) which automatically includes all subdomains—do not use wildcard syntax like `*.trusted.com`.
Expand Down
4 changes: 4 additions & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1708,6 +1708,10 @@
"type": "string",
"description": "AWF log level (default: info). Valid values: debug, info, warn, error",
"enum": ["debug", "info", "warn", "error"]
},
"path": {
"type": "string",
"description": "Custom path to AWF binary. When specified, skips downloading AWF from GitHub releases. Supports absolute paths or paths relative to GITHUB_WORKSPACE."
}
},
"additionalProperties": false
Expand Down
82 changes: 72 additions & 10 deletions pkg/workflow/copilot_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import (
"fmt"
"path/filepath"
"sort"
"strings"

Expand Down Expand Up @@ -108,16 +109,23 @@
srtInstall := generateSRTInstallationStep()
steps = append(steps, srtInstall)
} else if isFirewallEnabled(workflowData) {
// Install AWF after Node.js setup but before Copilot CLI installation
// Add AWF installation or validation steps only if firewall is enabled
firewallConfig := getFirewallConfig(workflowData)
var awfVersion string
if firewallConfig != nil {
awfVersion = firewallConfig.Version
}

// Install AWF binary
awfInstall := generateAWFInstallationStep(awfVersion)
steps = append(steps, awfInstall)
if firewallConfig == nil || firewallConfig.Path == "" {
// Default: Download and install AWF from GitHub releases
var awfVersion string
if firewallConfig != nil {
awfVersion = firewallConfig.Version
}

awfInstall := generateAWFInstallationStep(awfVersion)
steps = append(steps, awfInstall)
} else {
// Custom path: Validate the binary exists and is executable
validationStep := generateAWFPathValidationStep(firewallConfig.Path)
steps = append(steps, validationStep)
}
}

// Add Copilot CLI installation step after sandbox installation
Expand Down Expand Up @@ -321,11 +329,14 @@
awfArgs = append(awfArgs, firewallConfig.Args...)
}

// Get AWF binary path (custom or default)
awfBinary := getAWFBinaryPath(firewallConfig)

// Build the full AWF command with proper argument separation
// AWF v0.2.0 uses -- to separate AWF args from the actual command
// The command arguments should be passed as individual shell arguments, not as a single string
command = fmt.Sprintf(`set -o pipefail
sudo -E awf %s \
sudo -E %s %s \
-- %s \
2>&1 | tee %s

Expand All @@ -337,7 +348,7 @@
sudo mkdir -p %s
sudo mv "$AGENT_LOGS_DIR"/* %s || true
sudo rmdir "$AGENT_LOGS_DIR" || true
fi`, shellJoinArgs(awfArgs), copilotCommand, shellEscapeArg(logFile), shellEscapeArg(logsFolder), shellEscapeArg(logsFolder), shellEscapeArg(logsFolder))
fi`, shellEscapeArg(awfBinary), shellJoinArgs(awfArgs), copilotCommand, shellEscapeArg(logFile), shellEscapeArg(logsFolder), shellEscapeArg(logsFolder), shellEscapeArg(logsFolder))
} else {
// Run copilot command without AWF wrapper
command = fmt.Sprintf(`set -o pipefail
Expand Down Expand Up @@ -960,6 +971,56 @@
return GitHubActionStep(stepLines)
}

// resolveAWFPath handles path resolution for absolute and relative paths
// It returns the path to use for the AWF binary, resolving relative paths against GITHUB_WORKSPACE
func resolveAWFPath(customPath string) string {
if customPath == "" {
return "/usr/local/bin/awf"
}

// Clean the path to normalize separators and remove redundant elements
// Note: filepath.Clean is used for normalization only, the path is not resolved against the filesystem
cleanPath := filepath.Clean(customPath)

if strings.HasPrefix(cleanPath, "/") {
return cleanPath // Absolute path
}

// Relative path - resolve against GITHUB_WORKSPACE
return fmt.Sprintf("${GITHUB_WORKSPACE}/%s", cleanPath)
}

// getAWFBinaryPath returns appropriate AWF binary path for execution
func getAWFBinaryPath(firewallConfig *FirewallConfig) string {
if firewallConfig != nil && firewallConfig.Path != "" {
return resolveAWFPath(firewallConfig.Path)
}
return "awf" // Default (in PATH from installation step)
}

// generateAWFPathValidationStep creates a validation step to verify custom AWF binary
func generateAWFPathValidationStep(customPath string) GitHubActionStep {
resolvedPath := resolveAWFPath(customPath)
escapedPath := shellEscapeArg(resolvedPath)

stepLines := []string{
" - name: Validate custom AWF binary",
" run: |",
fmt.Sprintf(" echo \"Validating custom AWF binary at: %s\"", escapedPath),
fmt.Sprintf(" if [ ! -f %s ]; then", escapedPath),
fmt.Sprintf(" echo \"Error: AWF binary not found at %s\"", escapedPath),
" exit 1",
" fi",
fmt.Sprintf(" if [ ! -x %s ]; then", escapedPath),
fmt.Sprintf(" echo \"Error: AWF binary at %s is not executable\"", escapedPath),
" exit 1",
" fi",
fmt.Sprintf(" %s --version", escapedPath),
}

return GitHubActionStep(stepLines)
}

// generateSRTSystemDepsStep creates a GitHub Actions step to install SRT system dependencies
func generateSRTSystemDepsStep() GitHubActionStep {
stepLines := []string{
Expand Down Expand Up @@ -1132,6 +1193,7 @@
return script
}


Check failure on line 1196 in pkg/workflow/copilot_engine.go

View workflow job for this annotation

GitHub Actions / lint

File is not properly formatted (gofmt)
// generateSquidLogsCollectionStep creates a GitHub Actions step to collect Squid logs from AWF
func generateSquidLogsCollectionStep(workflowName string) GitHubActionStep {
sanitizedName := strings.ToLower(SanitizeWorkflowName(workflowName))
Expand Down
1 change: 1 addition & 0 deletions pkg/workflow/firewall.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type FirewallConfig struct {
Args []string `yaml:"args,omitempty"` // Additional arguments to pass to AWF
LogLevel string `yaml:"log_level,omitempty"` // AWF log level (default: "info")
CleanupScript string `yaml:"cleanup_script,omitempty"` // Cleanup script path (default: "./scripts/ci/cleanup.sh")
Path string `yaml:"path,omitempty"` // Custom AWF binary path (skips download when specified)
}

// isFirewallEnabled checks if AWF firewall is enabled for the workflow
Expand Down
Loading
Loading