From c33bb176d43c605dce99673daf60cc33167b3ec7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Aug 2026 08:43:08 +0000 Subject: [PATCH 1/5] Initial plan From fb1457ef80d84119de2976bea6d4796a60e50d9c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Aug 2026 08:49:48 +0000 Subject: [PATCH 2/5] Document Copilot billing tip opt-out Co-authored-by: dsyme <7204669+dsyme@users.noreply.github.com> --- docs/src/content/docs/reference/billing.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/src/content/docs/reference/billing.md b/docs/src/content/docs/reference/billing.md index 6385cf8bb54..4d12afeb161 100644 --- a/docs/src/content/docs/reference/billing.md +++ b/docs/src/content/docs/reference/billing.md @@ -35,6 +35,13 @@ There are two billing paths for the Copilot engine (`engine: copilot`, the defau `gh aw compile` does **not** auto-inject `copilot-requests: write` into arbitrary workflow source. The permission must be declared in the workflow frontmatter. Some authoring flows such as `gh aw add` can insert it when the author explicitly chooses Copilot org billing, but the compiler otherwise only emits an informational tip. +To use individual/seat billing without receiving this tip on future compilations, explicitly disable organization billing: + +```yaml +permissions: + copilot-requests: none +``` + **Individual/seat billing** — If the above conditions are not met, the workflow must be configured with a user-supplied [`COPILOT_GITHUB_TOKEN`](/gh-aw/reference/auth/#copilot_github_token). In this case inference is attributed to (and limited by) the PAT owner's Copilot entitlements rather than being billed centrally through the organization. See [Engines](/gh-aw/reference/engines/) for a full list of engines and their authentication requirements, and [Authentication](/gh-aw/reference/auth/) for configuration details. For Copilot model pricing and AIC rates, see [GitHub Copilot models and pricing](https://docs.github.com/copilot/reference/copilot-billing/models-and-pricing). From 55bb74b0d234c44ec6298d493c81993488e47367 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Aug 2026 09:07:10 +0000 Subject: [PATCH 3/5] Add PAT Copilot billing opt-out Co-authored-by: dsyme <7204669+dsyme@users.noreply.github.com> --- pkg/cli/add_command.go | 6 ++- pkg/cli/add_command_test.go | 44 +++++++++++++++++++++ pkg/cli/add_copilot_permissions.go | 14 ++++++- pkg/cli/add_interactive_engine.go | 6 +-- pkg/cli/add_interactive_engine_test.go | 6 +++ pkg/cli/add_interactive_git.go | 34 ++++++++-------- pkg/cli/add_interactive_orchestrator.go | 3 ++ pkg/cli/add_workflow_content.go | 11 ++++++ pkg/cli/codemod_copilot_requests_feature.go | 19 +++++---- 9 files changed, 112 insertions(+), 31 deletions(-) diff --git a/pkg/cli/add_command.go b/pkg/cli/add_command.go index 6ce44db86ad..3b08ca90d04 100644 --- a/pkg/cli/add_command.go +++ b/pkg/cli/add_command.go @@ -81,7 +81,11 @@ type AddOptions struct { // the workflow frontmatter, enabling GitHub Actions token auth for Copilot. // Set by the add-wizard when the user selects org-billing auth instead of a PAT. AddCopilotRequestsPermission bool - addWizard *addWizardOptions + // AddCopilotRequestsNonePermission injects permissions.copilot-requests: none into + // the workflow frontmatter, explicitly selecting PAT-based Copilot authentication. + // Set by the add-wizard only when the user actively selects PAT auth. + AddCopilotRequestsNonePermission bool + addWizard *addWizardOptions } type addWizardOptions struct { diff --git a/pkg/cli/add_command_test.go b/pkg/cli/add_command_test.go index 432a66d40cc..9f43b2c18bb 100644 --- a/pkg/cli/add_command_test.go +++ b/pkg/cli/add_command_test.go @@ -1324,6 +1324,27 @@ func TestAddCopilotRequestsPermissionToContent(t *testing.T) { }) } +func TestAddCopilotRequestsNonePermissionToContent(t *testing.T) { + t.Parallel() + t.Run("adds none permission to workflow without existing permissions block", func(t *testing.T) { + t.Parallel() + content := "---\nengine: copilot\n---\nDo the thing.\n" + result, err := addCopilotRequestsNonePermissionToContent(content) + require.NoError(t, err) + assert.Contains(t, result, "permissions:") + assert.Contains(t, result, "copilot-requests: none") + }) + + t.Run("replaces existing write permission", func(t *testing.T) { + t.Parallel() + content := "---\nengine: copilot\npermissions:\n copilot-requests: write\n---\nDo the thing.\n" + result, err := addCopilotRequestsNonePermissionToContent(content) + require.NoError(t, err) + assert.Contains(t, result, "copilot-requests: none") + assert.NotContains(t, result, "copilot-requests: write") + }) +} + func TestAddWorkflowWithTracking_CopilotRequestsPermission(t *testing.T) { t.Run("injects copilot-requests permission when option is set", func(t *testing.T) { dir := testutil.TempDir(t, "test-copilot-requests-perm-*") @@ -1351,6 +1372,29 @@ func TestAddWorkflowWithTracking_CopilotRequestsPermission(t *testing.T) { assert.Contains(t, string(written), "copilot-requests: write") }) + t.Run("injects copilot-requests none permission when option is set", func(t *testing.T) { + dir := testutil.TempDir(t, "test-copilot-requests-none-perm-*") + setupMinimalGitRepo(t, dir) + + content := "---\nengine: copilot\n---\nDo the thing.\n" + resolved := &ResolvedWorkflow{ + Spec: &WorkflowSpec{WorkflowPath: "workflows/my-workflow3.md", WorkflowName: "my-workflow3"}, + Content: []byte(content), + SourceInfo: &FetchedWorkflow{IsLocal: true}, + } + + err := addWorkflowWithTracking(context.Background(), resolved, nil, AddOptions{ + Quiet: true, + AddCopilotRequestsNonePermission: true, + DisableSecurityScanner: true, + }) + require.NoError(t, err) + + written, readErr := os.ReadFile(filepath.Join(dir, ".github", "workflows", "my-workflow3.md")) + require.NoError(t, readErr) + assert.Contains(t, string(written), "copilot-requests: none") + }) + t.Run("does not inject permission when option is false", func(t *testing.T) { dir := testutil.TempDir(t, "test-copilot-requests-noperm-*") setupMinimalGitRepo(t, dir) diff --git a/pkg/cli/add_copilot_permissions.go b/pkg/cli/add_copilot_permissions.go index ee49e60f64b..798f885d8da 100644 --- a/pkg/cli/add_copilot_permissions.go +++ b/pkg/cli/add_copilot_permissions.go @@ -42,9 +42,19 @@ func isCopilotWorkflowContent(content string) bool { // Returns an error if the permission could not be injected and is not already present // (e.g., when `permissions:` is a non-mapping scalar like `read-all`). func addCopilotRequestsPermissionToContent(content string) (string, error) { + return addCopilotRequestsPermissionToContentWithLevel(content, "write", false) +} + +// addCopilotRequestsNonePermissionToContent injects `permissions.copilot-requests: none` +// into the workflow frontmatter, explicitly selecting PAT-based Copilot authentication. +func addCopilotRequestsNonePermissionToContent(content string) (string, error) { + return addCopilotRequestsPermissionToContentWithLevel(content, "none", true) +} + +func addCopilotRequestsPermissionToContentWithLevel(content, level string, replaceExisting bool) (string, error) { var injectionFailed bool newContent, modified, err := applyFrontmatterLineTransform(content, func(lines []string) ([]string, bool) { - updated := ensureCopilotRequestsWritePermission(lines) + updated := ensureCopilotRequestsPermission(lines, level, replaceExisting) // Detect whether ensureCopilotRequestsWritePermission actually made a change. // When lengths differ, a line was added — modified is true without needing element comparison. // When lengths are equal, compare element-by-element (safe since len(updated)==len(lines)). @@ -68,7 +78,7 @@ func addCopilotRequestsPermissionToContent(content string) (string, error) { }) if injectionFailed { copilotPermissionsLog.Print("Failed to inject copilot-requests permission: permissions block is a non-mapping scalar") - return content, errors.New("permissions.copilot-requests could not be injected because 'permissions' is a non-mapping scalar value. Expected 'permissions' to be a mapping object. Example:\npermissions:\n contents: read\n copilot-requests: write") + return content, errors.New("permissions.copilot-requests could not be injected because 'permissions' is a non-mapping scalar value. Expected 'permissions' to be a mapping object. Example:\npermissions:\n contents: read\n copilot-requests: " + level) } if err != nil { return content, err diff --git a/pkg/cli/add_interactive_engine.go b/pkg/cli/add_interactive_engine.go index d9e81c53b51..c305b37b737 100644 --- a/pkg/cli/add_interactive_engine.go +++ b/pkg/cli/add_interactive_engine.go @@ -236,15 +236,13 @@ func (c *AddInteractiveConfig) configureEngineAPISecret(engine string) error { // (permissions.copilot-requests: write). Extracted as a package-level constant so both the // form definition and applyCopilotAuthMethodChoice reference the same sentinel. const authMethodCopilotRequests = "copilot-requests" +const authMethodPAT = "pat" // selectCopilotAuthMethod prompts the user to choose between copilot-requests (org billing) // and a Personal Access Token for Copilot authentication. // Sets c.UseCopilotRequests when org billing is chosen. func (c *AddInteractiveConfig) selectCopilotAuthMethod() error { addInteractiveLog.Print("Prompting user for Copilot authentication method") - - const authMethodPAT = "pat" - // Detect org Copilot CLI billing status before building the form. // c.RepoOverride is in "owner/repo" format; we need just the org login. // When no org login is available the result is inconclusive (same as a @@ -324,10 +322,12 @@ func copilotAuthMethodDescription(probe orgCopilotBillingProbeResult, source sec func (c *AddInteractiveConfig) applyCopilotAuthMethodChoice(authMethod string) { if authMethod == authMethodCopilotRequests { c.UseCopilotRequests = true + c.UseCopilotPAT = false fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Selected copilot-requests: permissions.copilot-requests: write will be added to your workflow")) fmt.Fprintln(os.Stderr, console.FormatInfoMessage("No COPILOT_GITHUB_TOKEN secret is required — Copilot usage is billed to your org's Copilot seat.")) } else { c.UseCopilotRequests = false + c.UseCopilotPAT = authMethod == authMethodPAT fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Selected authentication: COPILOT_GITHUB_TOKEN")) } } diff --git a/pkg/cli/add_interactive_engine_test.go b/pkg/cli/add_interactive_engine_test.go index 0df869c518d..d7adde35105 100644 --- a/pkg/cli/add_interactive_engine_test.go +++ b/pkg/cli/add_interactive_engine_test.go @@ -16,21 +16,25 @@ func TestApplyCopilotAuthMethodChoice(t *testing.T) { name string authMethod string wantCopilotReqs bool + wantCopilotPAT bool }{ { name: "copilot-requests sets UseCopilotRequests true", authMethod: "copilot-requests", wantCopilotReqs: true, + wantCopilotPAT: false, }, { name: "pat sets UseCopilotRequests false", authMethod: "pat", wantCopilotReqs: false, + wantCopilotPAT: true, }, { name: "empty value (form cancelled) sets UseCopilotRequests false", authMethod: "", wantCopilotReqs: false, + wantCopilotPAT: false, }, } for _, tc := range tests { @@ -39,6 +43,7 @@ func TestApplyCopilotAuthMethodChoice(t *testing.T) { cfg := &AddInteractiveConfig{} cfg.applyCopilotAuthMethodChoice(tc.authMethod) assert.Equal(t, tc.wantCopilotReqs, cfg.UseCopilotRequests) + assert.Equal(t, tc.wantCopilotPAT, cfg.UseCopilotPAT) }) } } @@ -54,6 +59,7 @@ func TestApplyCopilotAuthMethodChoice_ReEntryClearsOldValue(t *testing.T) { // User changes selection to PAT — old value must not persist cfg.applyCopilotAuthMethodChoice("pat") assert.False(t, cfg.UseCopilotRequests) + assert.True(t, cfg.UseCopilotPAT) } func TestCopilotAuthMethodDescription(t *testing.T) { diff --git a/pkg/cli/add_interactive_git.go b/pkg/cli/add_interactive_git.go index 37242b57280..74bef100c1d 100644 --- a/pkg/cli/add_interactive_git.go +++ b/pkg/cli/add_interactive_git.go @@ -43,21 +43,22 @@ func (c *AddInteractiveConfig) createWorkflowChangesAndConfigureSecret(ctx conte // Pass Quiet=true to suppress detailed output (already shown earlier in interactive mode) // This returns the result including PR number and HasWorkflowDispatch opts := AddOptions{ - Verbose: c.Verbose, - Quiet: true, - EngineOverride: c.EngineOverride, - Name: "", - Force: c.forceOverwrite, - AppendText: c.AppendText, - CreatePR: createPR, - NoGitattributes: c.NoGitattributes, - WorkflowDir: c.WorkflowDir, - NoStopAfter: c.NoStopAfter, - StopAfter: c.StopAfter, - DisableSecurityScanner: c.DisableSecurityScanner, - RepoSlug: c.RepoOverride, - AddCopilotRequestsPermission: c.UseCopilotRequests, - GhAwRef: c.GhAwRef, + Verbose: c.Verbose, + Quiet: true, + EngineOverride: c.EngineOverride, + Name: "", + Force: c.forceOverwrite, + AppendText: c.AppendText, + CreatePR: createPR, + NoGitattributes: c.NoGitattributes, + WorkflowDir: c.WorkflowDir, + NoStopAfter: c.NoStopAfter, + StopAfter: c.StopAfter, + DisableSecurityScanner: c.DisableSecurityScanner, + RepoSlug: c.RepoOverride, + AddCopilotRequestsPermission: c.UseCopilotRequests, + AddCopilotRequestsNonePermission: c.UseCopilotPAT, + GhAwRef: c.GhAwRef, addWizard: &addWizardOptions{ initializedFiles: initFiles, workingTreePrevalidated: createPR, @@ -246,14 +247,12 @@ func (c *AddInteractiveConfig) configureRepositorySecret(secretName, secretValue // the merged workflow files, which are required when offering to run the workflow. func (c *AddInteractiveConfig) updateLocalBranch() error { addInteractiveLog.Print("Updating local branch with merged changes") - // Get the default branch name using gh output, err := workflow.RunGHCombined("Getting default branch...", "repo", "view", "--repo", c.RepoOverride, "--json", "defaultBranchRef", "--jq", ".defaultBranchRef.name") defaultBranch := "" if err == nil { defaultBranch = strings.TrimSpace(string(output)) } - // Fallback: query the local origin remote directly (works even when gh repo // view fails, e.g. forks without a default remote set). if defaultBranch == "" { @@ -264,7 +263,6 @@ func (c *AddInteractiveConfig) updateLocalBranch() error { defaultBranch = parseDefaultBranchFromLsRemote(string(lsOutput)) } } - if defaultBranch == "" { defaultBranch = "main" } diff --git a/pkg/cli/add_interactive_orchestrator.go b/pkg/cli/add_interactive_orchestrator.go index 7f388fc4644..9b7ffaa7cae 100644 --- a/pkg/cli/add_interactive_orchestrator.go +++ b/pkg/cli/add_interactive_orchestrator.go @@ -44,6 +44,9 @@ type AddInteractiveConfig struct { // When true, COPILOT_GITHUB_TOKEN secret setup is skipped and // permissions.copilot-requests: write is injected into the workflow. UseCopilotRequests bool + // UseCopilotPAT indicates the user actively chose PAT authentication for Copilot. + // When true, permissions.copilot-requests: none is injected into the workflow. + UseCopilotPAT bool // copilotCLIBillingStatus is the detected org Copilot CLI billing status. // "enabled" — confirmed available; "disabled" — confirmed unavailable; "" — inconclusive. diff --git a/pkg/cli/add_workflow_content.go b/pkg/cli/add_workflow_content.go index 4f147bf595d..dce2f6eda75 100644 --- a/pkg/cli/add_workflow_content.go +++ b/pkg/cli/add_workflow_content.go @@ -217,6 +217,17 @@ func applyEngineAndPermissionModifications(content string, opts AddOptions) (str } } } + if opts.AddCopilotRequestsNonePermission && isCopilotWorkflowContent(content) { + updatedContent, err := addCopilotRequestsNonePermissionToContent(content) + if err != nil { + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to disable copilot-requests permission: %v", err))) + } else { + content = updatedContent + if opts.Verbose { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Added permissions.copilot-requests: none to workflow")) + } + } + } return content, nil } diff --git a/pkg/cli/codemod_copilot_requests_feature.go b/pkg/cli/codemod_copilot_requests_feature.go index f3151fda5e2..fea8f053932 100644 --- a/pkg/cli/codemod_copilot_requests_feature.go +++ b/pkg/cli/codemod_copilot_requests_feature.go @@ -96,10 +96,13 @@ func canSafelyAddCopilotRequestsPermission(frontmatter map[string]any) bool { } func ensureCopilotRequestsWritePermission(lines []string) []string { + return ensureCopilotRequestsPermission(lines, "write", false) +} + +func ensureCopilotRequestsPermission(lines []string, level string, replaceExisting bool) []string { permissionsIdx := -1 permissionsIndent := "" permissionsEnd := len(lines) - for i, line := range lines { if isTopLevelKey(line) && strings.HasPrefix(strings.TrimSpace(line), "permissions:") { permissionsIdx = i @@ -113,16 +116,14 @@ func ensureCopilotRequestsWritePermission(lines []string) []string { break } } - if permissionsIdx == -1 { insertAt := findPermissionsInsertIndex(lines) result := make([]string, 0, len(lines)+2) result = append(result, lines[:insertAt]...) - result = append(result, "permissions:", " copilot-requests: write") + result = append(result, "permissions:", " copilot-requests: "+level) result = append(result, lines[insertAt:]...) return result } - trimmedPermissionsLine := strings.TrimSpace(lines[permissionsIdx]) inlineValue := strings.TrimSpace(strings.TrimPrefix(trimmedPermissionsLine, "permissions:")) if inlineValue != "" && !strings.HasPrefix(inlineValue, "#") { @@ -135,23 +136,27 @@ func ensureCopilotRequestsWritePermission(lines []string) []string { result := make([]string, 0, len(lines)+1) result = append(result, lines[:permissionsIdx]...) result = append(result, permissionsIndent+"permissions:") - result = append(result, permissionsIndent+" copilot-requests: write") + result = append(result, permissionsIndent+" copilot-requests: "+level) result = append(result, lines[permissionsIdx+1:]...) return result } return lines } - for i := permissionsIdx + 1; i < permissionsEnd; i++ { trimmed := strings.TrimSpace(lines[i]) if parseYAMLMapKey(trimmed) == "copilot-requests" { + if replaceExisting { + result := append([]string(nil), lines...) + result[i] = getIndentation(lines[i]) + "copilot-requests: " + level + return result + } return lines } } result := make([]string, 0, len(lines)+1) result = append(result, lines[:permissionsEnd]...) - result = append(result, permissionsIndent+" copilot-requests: write") + result = append(result, permissionsIndent+" copilot-requests: "+level) result = append(result, lines[permissionsEnd:]...) return result } From 7605f2f86046975e478b2177fe83f9fd6fa73f3f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Aug 2026 09:15:14 +0000 Subject: [PATCH 4/5] Clarify Copilot billing tip opt-out Co-authored-by: dsyme <7204669+dsyme@users.noreply.github.com> --- pkg/workflow/compiler_validators_test.go | 1 + pkg/workflow/copilot_requests_tip_test.go | 4 ++++ pkg/workflow/permissions_compiler_validator.go | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/workflow/compiler_validators_test.go b/pkg/workflow/compiler_validators_test.go index 00cb9d4bb19..38be811069c 100644 --- a/pkg/workflow/compiler_validators_test.go +++ b/pkg/workflow/compiler_validators_test.go @@ -668,6 +668,7 @@ func TestValidatePermissions_EmitsCopilotRequestsTipOncePerMarkdownPath(t *testi const tipText = "Tip: set permissions.copilot-requests: write to use GitHub Actions token-based inference" assert.Equal(t, 1, strings.Count(stderr, tipText), "copilot-requests tip should be emitted only once per markdown path") + assert.Contains(t, stderr, "To suppress this tip when using a PAT, set permissions.copilot-requests: none") } func TestValidatePermissions_QuietSuppressesCopilotRequestsTip(t *testing.T) { diff --git a/pkg/workflow/copilot_requests_tip_test.go b/pkg/workflow/copilot_requests_tip_test.go index 8d0d0ef8462..4eb7d91bb63 100644 --- a/pkg/workflow/copilot_requests_tip_test.go +++ b/pkg/workflow/copilot_requests_tip_test.go @@ -95,6 +95,7 @@ permissions: const tipText = "Tip: set permissions.copilot-requests: write to use GitHub Actions token-based inference" const tipLink = "https://github.github.com/gh-aw/reference/billing/" const tipOrgNote = "requires that your organization has centralized Copilot billing enabled and may not be available" + const tipOptOut = "To suppress this tip when using a PAT, set permissions.copilot-requests: none" if tt.expectTip && !strings.Contains(stderrOutput, tipText) { t.Fatalf("Expected copilot-requests tip in stderr, got:\n%s", stderrOutput) } @@ -104,6 +105,9 @@ permissions: if tt.expectTip && !strings.Contains(stderrOutput, tipOrgNote) { t.Fatalf("Expected org billing note in copilot-requests tip, got:\n%s", stderrOutput) } + if tt.expectTip && !strings.Contains(stderrOutput, tipOptOut) { + t.Fatalf("Expected PAT opt-out in copilot-requests tip, got:\n%s", stderrOutput) + } if !tt.expectTip && strings.Contains(stderrOutput, tipText) { t.Fatalf("Did not expect copilot-requests tip in stderr, got:\n%s", stderrOutput) } diff --git a/pkg/workflow/permissions_compiler_validator.go b/pkg/workflow/permissions_compiler_validator.go index 733c8a20ea2..343adf228d3 100644 --- a/pkg/workflow/permissions_compiler_validator.go +++ b/pkg/workflow/permissions_compiler_validator.go @@ -199,7 +199,7 @@ Ensure proper audience validation and trust policies are configured.` if c.batchMode { c.copilotTipNeeded = true } else { - tipMsg := `Tip: set permissions.copilot-requests: write to use GitHub Actions token-based inference with the Copilot engine instead of a personal access token (COPILOT_GITHUB_TOKEN). This option requires that your organization has centralized Copilot billing enabled and may not be available in all organizations — see https://github.github.com/gh-aw/reference/billing/ for details.` + tipMsg := `Tip: set permissions.copilot-requests: write to use GitHub Actions token-based inference with the Copilot engine instead of a personal access token (COPILOT_GITHUB_TOKEN). This option requires that your organization has centralized Copilot billing enabled and may not be available in all organizations. To suppress this tip when using a PAT, set permissions.copilot-requests: none — see https://github.github.com/gh-aw/reference/billing/ for details.` fmt.Fprintln(os.Stderr, formatCompilerMessage(markdownPath, "info", tipMsg)) } c.copilotRequestsTipShown[markdownPath] = true From 7d6b6ffda90d5c082813d94edd17d5e5302f3e65 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Aug 2026 18:05:50 +0000 Subject: [PATCH 5/5] Fix Copilot PAT opt-out follow-ups Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- pkg/cli/add_copilot_permissions.go | 7 +++++-- pkg/cli/add_copilot_permissions_test.go | 5 +++++ pkg/cli/compile_batch_notices_test.go | 1 + pkg/cli/compile_pipeline.go | 1 + 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pkg/cli/add_copilot_permissions.go b/pkg/cli/add_copilot_permissions.go index 798f885d8da..2c4e0bf96fb 100644 --- a/pkg/cli/add_copilot_permissions.go +++ b/pkg/cli/add_copilot_permissions.go @@ -12,7 +12,7 @@ var copilotPermissionsLog = logger.New("cli:add_copilot_permissions") // This file handles Copilot-specific workflow permission injection. -// isCopilotWorkflowContent returns true when the workflow frontmatter declares engine: copilot. +// isCopilotWorkflowContent returns true unless the workflow frontmatter explicitly declares a non-Copilot engine. // It is used to guard AddCopilotRequestsPermission injection so that the flag is only applied // to Copilot workflows even when multiple workflows of different engines are processed together. func isCopilotWorkflowContent(content string) bool { @@ -20,6 +20,9 @@ func isCopilotWorkflowContent(content string) bool { if err != nil { return false } + if len(lines) == 0 { + return false + } for _, line := range lines { if !isTopLevelKey(line) { continue @@ -30,7 +33,7 @@ func isCopilotWorkflowContent(content string) bool { return val == string(constants.CopilotEngine) } } - return false + return true } // addCopilotRequestsPermissionToContent injects `permissions.copilot-requests: write` diff --git a/pkg/cli/add_copilot_permissions_test.go b/pkg/cli/add_copilot_permissions_test.go index 6968cf5dca0..23f73a9402c 100644 --- a/pkg/cli/add_copilot_permissions_test.go +++ b/pkg/cli/add_copilot_permissions_test.go @@ -25,6 +25,11 @@ func TestIsCopilotWorkflowContent(t *testing.T) { content: "---\nengine: claude\n---\nbody\n", want: false, }, + { + name: "default engine", + content: "---\non: workflow_dispatch\n---\nbody\n", + want: true, + }, { name: "no frontmatter", content: "body\n", diff --git a/pkg/cli/compile_batch_notices_test.go b/pkg/cli/compile_batch_notices_test.go index 793686f43c4..735ef7f98cd 100644 --- a/pkg/cli/compile_batch_notices_test.go +++ b/pkg/cli/compile_batch_notices_test.go @@ -73,6 +73,7 @@ func TestDisplayBatchCompilationNotices(t *testing.T) { config: CompileConfig{}, expectedInOutput: []string{ "Copilot token-based inference may be available", + "To suppress this tip when using a PAT, set permissions.copilot-requests: none", }, notExpectedInOutput: []string{}, }, diff --git a/pkg/cli/compile_pipeline.go b/pkg/cli/compile_pipeline.go index 84e9d51dedd..011af1cd0a2 100644 --- a/pkg/cli/compile_pipeline.go +++ b/pkg/cli/compile_pipeline.go @@ -705,6 +705,7 @@ func displayBatchCompilationNotices(compiler *workflow.Compiler, config CompileC if compiler.CopilotRequestsTipNeeded() { fmt.Fprintln(os.Stderr, console.FormatInfoMessageStderr( "Copilot token-based inference may be available: add permissions.copilot-requests: write. "+ + "To suppress this tip when using a PAT, set permissions.copilot-requests: none. "+ "See https://github.github.com/gh-aw/reference/billing/", )) }