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
5 changes: 5 additions & 0 deletions .changeset/patch-add-copilot-requests-feature.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .github/smoke-tests/claude-22334678781.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Smoke test push verification - Run 22334678781
25 changes: 8 additions & 17 deletions .github/workflows/smoke-copilot.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions .github/workflows/smoke-copilot.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ safe-outputs:
run-failure: "📰 DEVELOPING STORY: [{workflow_name}]({run_url}) reports {status}. Our correspondents are investigating the incident..."
timeout-minutes: 15
strict: true
features:
copilot-requests: true
---

# Smoke Test: Copilot Engine Validation
Expand Down
4 changes: 4 additions & 0 deletions pkg/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,10 @@ const (
DangerousPermissionsWriteFeatureFlag FeatureFlag = "dangerous-permissions-write"
// DisableXPIAPromptFeatureFlag is the feature flag name for disabling XPIA prompt
DisableXPIAPromptFeatureFlag FeatureFlag = "disable-xpia-prompt"
// CopilotRequestsFeatureFlag is the feature flag name for enabling copilot-requests mode.
// When enabled: no secret validation step is generated, copilot-requests: write permission is added,
// and the GitHub Actions token is used as the agentic engine secret.
CopilotRequestsFeatureFlag FeatureFlag = "copilot-requests"
)

// Step IDs for pre-activation job
Expand Down
24 changes: 19 additions & 5 deletions pkg/workflow/copilot_engine_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,11 +225,19 @@ COPILOT_CLI_INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"
%s%s 2>&1 | tee %s`, mkdirCommands.String(), copilotCommand, logFile)
}

// Use COPILOT_GITHUB_TOKEN
// #nosec G101 -- This is NOT a hardcoded credential. It's a GitHub Actions expression template
// that GitHub Actions runtime replaces with the actual secret value. The string "${{ secrets.COPILOT_GITHUB_TOKEN }}"
// is a placeholder, not an actual credential.
copilotGitHubToken := "${{ secrets.COPILOT_GITHUB_TOKEN }}"
// Use COPILOT_GITHUB_TOKEN: when the copilot-requests feature is enabled, use the GitHub
// Actions token directly (${{ github.token }}). Otherwise use the COPILOT_GITHUB_TOKEN secret.
// #nosec G101 -- These are NOT hardcoded credentials. They are GitHub Actions expression templates
// that the runtime replaces with actual values. The strings "${{ secrets.COPILOT_GITHUB_TOKEN }}"
// and "${{ github.token }}" are placeholders, not actual credentials.
var copilotGitHubToken string
useCopilotRequests := isFeatureEnabled(constants.CopilotRequestsFeatureFlag, workflowData)
if useCopilotRequests {
copilotGitHubToken = "${{ github.token }}"
copilotExecLog.Print("Using GitHub Actions token as COPILOT_GITHUB_TOKEN (copilot-requests feature enabled)")
} else {
copilotGitHubToken = "${{ secrets.COPILOT_GITHUB_TOKEN }}"
}

env := map[string]string{
"XDG_CONFIG_HOME": "/home/runner",
Expand All @@ -241,6 +249,12 @@ COPILOT_CLI_INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"
"GITHUB_WORKSPACE": "${{ github.workspace }}",
}

// When copilot-requests feature is enabled, set S2STOKENS=true to allow the Copilot CLI
// to accept GitHub App installation tokens (ghs_*) such as ${{ github.token }}.
if useCopilotRequests {
env["S2STOKENS"] = "true"
}

// Always add GH_AW_PROMPT for agentic workflows
env["GH_AW_PROMPT"] = "/tmp/gh-aw/aw-prompts/prompt.txt"

Expand Down
22 changes: 14 additions & 8 deletions pkg/workflow/copilot_engine_installation.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,20 @@ func (e *CopilotEngine) GetInstallationSteps(workflowData *WorkflowData) []GitHu
InstallStepName: "Install GitHub Copilot CLI",
}

// Add secret validation step
secretValidation := GenerateMultiSecretValidationStep(
config.Secrets,
config.Name,
config.DocsURL,
getEngineEnvOverrides(workflowData),
)
steps = append(steps, secretValidation)
// Add secret validation step unless copilot-requests feature is enabled.
// When copilot-requests is enabled, the GitHub Actions token is used directly
// (no COPILOT_GITHUB_TOKEN secret required).
if !isFeatureEnabled(constants.CopilotRequestsFeatureFlag, workflowData) {
secretValidation := GenerateMultiSecretValidationStep(
config.Secrets,
config.Name,
config.DocsURL,
getEngineEnvOverrides(workflowData),
)
steps = append(steps, secretValidation)
} else {
copilotInstallLog.Print("Skipping secret validation step: copilot-requests feature enabled, using GitHub Actions token")
}

// Determine Copilot version
copilotVersion := config.Version
Expand Down
5 changes: 5 additions & 0 deletions pkg/workflow/permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ func convertStringToPermissionScope(key string) PermissionScope {
return PermissionSecurityEvents
case "statuses":
return PermissionStatuses
case "copilot-requests":
return PermissionCopilotRequests
case "all":
// "all" is a meta-key handled at the parser level; it is not a real scope
return ""
Expand Down Expand Up @@ -82,6 +84,9 @@ const (
PermissionOrganizationProj PermissionScope = "organization-projects"
PermissionSecurityEvents PermissionScope = "security-events"
PermissionStatuses PermissionScope = "statuses"
// PermissionCopilotRequests is a GitHub Actions permission scope used with the copilot-requests feature.
// It enables use of the GitHub Actions token as the Copilot authentication token.
PermissionCopilotRequests PermissionScope = "copilot-requests"
)

// GetAllPermissionScopes returns all available permission scopes
Expand Down
12 changes: 12 additions & 0 deletions pkg/workflow/threat_detection.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,15 @@ func (c *Compiler) buildThreatDetectionJob(data *WorkflowData, mainJobName strin
permissions = NewPermissionsEmpty().RenderToYAML()
}

// When the copilot-requests feature is enabled, inject copilot-requests: write permission.
// This is required so the GitHub Actions token has the necessary scope to authenticate
// with the Copilot API in the detection job (mirrors the agent job logic in tools.go).
if isFeatureEnabled(constants.CopilotRequestsFeatureFlag, data) {
perms := NewPermissionsParser(permissions).ToPermissions()
perms.Set(PermissionCopilotRequests, PermissionWrite)
permissions = perms.RenderToYAML()
}

// Generate agent concurrency configuration (same as main agent job)
agentConcurrency := GenerateJobConcurrencyConfig(data)

Expand Down Expand Up @@ -357,6 +366,8 @@ func (c *Compiler) buildEngineSteps(data *WorkflowData) []string {

// Create minimal WorkflowData for threat detection
// Configure bash read tools for accessing the agent output file
// Features are inherited from the main workflow data so feature flags
// (e.g. copilot-requests) apply consistently to both agent and detection jobs.
threatDetectionData := &WorkflowData{
Tools: map[string]any{
"bash": []any{"cat", "head", "tail", "wc", "grep", "ls", "jq"},
Expand All @@ -365,6 +376,7 @@ func (c *Compiler) buildEngineSteps(data *WorkflowData) []string {
Network: "",
EngineConfig: detectionEngineConfig,
AI: engineSetting,
Features: data.Features,
}

var steps []string
Expand Down
24 changes: 22 additions & 2 deletions pkg/workflow/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,10 @@ func (c *Compiler) applyDefaults(data *WorkflowData, markdownPath string) error
data.ParsedTools = NewTools(data.Tools)

// Check if permissions is explicitly empty ({}) - this means user wants no permissions
// In this case, we should NOT apply default read-all
if data.Permissions == "permissions: {}" {
// In this case, we should NOT apply default read-all.
// Exception: if copilot-requests feature is enabled, we still need to fall through
// so the injection block below can add copilot-requests: write.
if data.Permissions == "permissions: {}" && !isFeatureEnabled(constants.CopilotRequestsFeatureFlag, data) {
// Explicitly empty permissions - preserve the empty state
// The agent job in dev mode will add contents: read if needed for local actions
return nil
Expand All @@ -181,6 +183,24 @@ func (c *Compiler) applyDefaults(data *WorkflowData, markdownPath string) error
}
data.Permissions = strings.Join(lines, "\n")
}

// When the copilot-requests feature is enabled, inject copilot-requests: write permission.
// This is required so that the GitHub Actions token has the necessary scope
// to authenticate with the Copilot API.
if isFeatureEnabled(constants.CopilotRequestsFeatureFlag, data) {
perms := NewPermissionsParser(data.Permissions).ToPermissions()
perms.Set(PermissionCopilotRequests, PermissionWrite)
yaml := perms.RenderToYAML()
// Adjust from job-level indentation (6 spaces) to workflow-level (2 spaces)
lines := strings.Split(yaml, "\n")
for i := 1; i < len(lines); i++ {
if strings.HasPrefix(lines[i], " ") {
lines[i] = " " + lines[i][6:]
}
}
data.Permissions = strings.Join(lines, "\n")
}

return nil
}

Expand Down