diff --git a/.changeset/patch-add-copilot-requests-feature.md b/.changeset/patch-add-copilot-requests-feature.md new file mode 100644 index 0000000000..d8999ee06e --- /dev/null +++ b/.changeset/patch-add-copilot-requests-feature.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Documented the `features.copilot-requests` feature flag so GitHub Actions token authentication, threat detection permissions, and the Copilot CLI execution environment honor the Copilot requests flow (injecting `copilot-requests: write` permissions and enabling `S2STOKENS=true`). diff --git a/.github/smoke-tests/claude-22334678781.txt b/.github/smoke-tests/claude-22334678781.txt new file mode 100644 index 0000000000..4747a7af26 --- /dev/null +++ b/.github/smoke-tests/claude-22334678781.txt @@ -0,0 +1 @@ +Smoke test push verification - Run 22334678781 diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index d5fe78fcc0..eb2cc7ea5b 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -29,7 +29,7 @@ # - shared/github-queries-safe-input.md # - shared/reporting.md # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"f84cb897ab3a37514906baf0de987adbf90160780c7d3fa2dff0b66603e93e23"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"d8f85237def24565e6e04a7afcf460aab687ec2e8da39e945f8ccd8bdefaf5ed"} name: "Smoke Copilot" "on": @@ -281,6 +281,7 @@ jobs: permissions: actions: read contents: read + copilot-requests: write discussions: read issues: read pull-requests: read @@ -300,7 +301,6 @@ jobs: model: ${{ steps.generate_aw_info.outputs.model }} output: ${{ steps.collect_output.outputs.output }} output_types: ${{ steps.collect_output.outputs.output_types }} - secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} steps: - name: Checkout actions folder uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -433,11 +433,6 @@ jobs: // Set model as output for reuse in other steps/jobs core.setOutput('model', awInfo.model); - - name: Validate COPILOT_GITHUB_TOKEN secret - id: validate-secret - run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default - env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Install GitHub Copilot CLI run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.414 - name: Install awf binary @@ -1680,7 +1675,7 @@ jobs: -- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-all-tools --add-dir /tmp/gh-aw/cache-memory/ --allow-all-paths --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_COPILOT:+ --model "$GH_AW_MODEL_AGENT_COPILOT"}' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ github.token }} GH_AW_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json GH_AW_MODEL_AGENT_COPILOT: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} @@ -1693,6 +1688,7 @@ jobs: GITHUB_REF_NAME: ${{ github.ref_name }} GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} GITHUB_WORKSPACE: ${{ github.workspace }} + S2STOKENS: true XDG_CONFIG_HOME: /home/runner - name: Configure Git credentials env: @@ -1742,8 +1738,7 @@ jobs: const { main } = require('/opt/gh-aw/actions/redact_secrets.cjs'); await main(); env: - GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' - SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -1928,7 +1923,6 @@ jobs: GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_WORKFLOW_ID: "smoke-copilot" - GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.agent.outputs.secret_verification_result }} GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_CREATE_DISCUSSION_ERRORS: ${{ needs.safe_outputs.outputs.create_discussion_errors }} GH_AW_CREATE_DISCUSSION_ERROR_COUNT: ${{ needs.safe_outputs.outputs.create_discussion_error_count }} @@ -1984,6 +1978,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + copilot-requests: write timeout-minutes: 10 outputs: success: ${{ steps.parse_results.outputs.success }} @@ -2031,11 +2026,6 @@ jobs: run: | mkdir -p /tmp/gh-aw/threat-detection touch /tmp/gh-aw/threat-detection/detection.log - - name: Validate COPILOT_GITHUB_TOKEN secret - id: validate-secret - run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default - env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Install GitHub Copilot CLI run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.414 - name: Execute GitHub Copilot CLI @@ -2059,13 +2049,14 @@ jobs: copilot --add-dir /tmp/ --add-dir /tmp/gh-aw/ --add-dir /tmp/gh-aw/agent/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --allow-tool 'shell(cat)' --allow-tool 'shell(grep)' --allow-tool 'shell(head)' --allow-tool 'shell(jq)' --allow-tool 'shell(ls)' --allow-tool 'shell(tail)' --allow-tool 'shell(wc)' --prompt "$COPILOT_CLI_INSTRUCTION"${GH_AW_MODEL_DETECTION_COPILOT:+ --model "$GH_AW_MODEL_DETECTION_COPILOT"} 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log env: COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ github.token }} GH_AW_MODEL_DETECTION_COPILOT: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GITHUB_HEAD_REF: ${{ github.head_ref }} GITHUB_REF_NAME: ${{ github.ref_name }} GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} GITHUB_WORKSPACE: ${{ github.workspace }} + S2STOKENS: true XDG_CONFIG_HOME: /home/runner - name: Parse threat detection results id: parse_results diff --git a/.github/workflows/smoke-copilot.md b/.github/workflows/smoke-copilot.md index ce3ce7f9cc..49425cb046 100644 --- a/.github/workflows/smoke-copilot.md +++ b/.github/workflows/smoke-copilot.md @@ -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 diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 5525bc4013..17c40dcdaf 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -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 diff --git a/pkg/workflow/copilot_engine_execution.go b/pkg/workflow/copilot_engine_execution.go index 5028ecaa95..73539ac2b9 100644 --- a/pkg/workflow/copilot_engine_execution.go +++ b/pkg/workflow/copilot_engine_execution.go @@ -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", @@ -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" diff --git a/pkg/workflow/copilot_engine_installation.go b/pkg/workflow/copilot_engine_installation.go index bb2e0d1121..2b8c6e0d52 100644 --- a/pkg/workflow/copilot_engine_installation.go +++ b/pkg/workflow/copilot_engine_installation.go @@ -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 diff --git a/pkg/workflow/permissions.go b/pkg/workflow/permissions.go index 1ad619987c..78dc40674f 100644 --- a/pkg/workflow/permissions.go +++ b/pkg/workflow/permissions.go @@ -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 "" @@ -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 diff --git a/pkg/workflow/threat_detection.go b/pkg/workflow/threat_detection.go index ecdafff0d1..1cd00f80e4 100644 --- a/pkg/workflow/threat_detection.go +++ b/pkg/workflow/threat_detection.go @@ -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) @@ -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"}, @@ -365,6 +376,7 @@ func (c *Compiler) buildEngineSteps(data *WorkflowData) []string { Network: "", EngineConfig: detectionEngineConfig, AI: engineSetting, + Features: data.Features, } var steps []string diff --git a/pkg/workflow/tools.go b/pkg/workflow/tools.go index 06021b1c0d..5495757b65 100644 --- a/pkg/workflow/tools.go +++ b/pkg/workflow/tools.go @@ -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 @@ -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 }