From 5f14d1b7043f97856eb20a99dbe3a7a65f9e329e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 22:55:00 +0000 Subject: [PATCH 1/6] Initial plan From 81740c089d8eb62caadc237fea9f81aabb3c8959 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 23:17:10 +0000 Subject: [PATCH 2/6] feat: add permissions field to github-app config for extra token permissions Allow users to specify org-level permissions (e.g., members: read) under tools.github.github-app.permissions. These are merged into the minted GitHub App installation access token with nested permissions winning over job-level permissions for overlapping scopes. Fixes: Can not fetch members from org when using an app token Agent-Logs-Url: https://github.com/github/gh-aw/sessions/8134923b-eeed-4351-bd5e-531c7dc8f3ce Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/parser/schemas/main_workflow_schema.json | 17 +++ pkg/workflow/compiler_github_mcp_steps.go | 16 +++ pkg/workflow/github_mcp_app_token_test.go | 103 +++++++++++++++++++ pkg/workflow/safe_outputs_app_config.go | 21 +++- 4 files changed, 153 insertions(+), 4 deletions(-) diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 64e1c1c4913..d40080f9f5b 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -9677,6 +9677,23 @@ "items": { "type": "string" } + }, + "permissions": { + "type": "object", + "description": "Optional extra permissions to merge into the minted token. These override the job-level permissions for the token (nested wins). Use this to add org-level permissions (e.g., members: read) that are not valid GitHub Actions scopes but are supported by GitHub Apps.", + "additionalProperties": { + "type": "string", + "enum": ["read", "write", "none"] + }, + "examples": [ + { + "members": "read" + }, + { + "members": "read", + "organization-administration": "read" + } + ] } }, "required": ["app-id", "private-key"], diff --git a/pkg/workflow/compiler_github_mcp_steps.go b/pkg/workflow/compiler_github_mcp_steps.go index de1199b4389..2965965f650 100644 --- a/pkg/workflow/compiler_github_mcp_steps.go +++ b/pkg/workflow/compiler_github_mcp_steps.go @@ -2,8 +2,10 @@ package workflow import ( "fmt" + "os" "strings" + "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/constants" ) @@ -102,6 +104,20 @@ func (c *Compiler) generateGitHubMCPAppTokenMintingStep(yaml *strings.Builder, d permissions = NewPermissions() } + // Apply extra permissions from github-app.permissions (nested wins over job-level) + if len(app.Permissions) > 0 { + githubConfigLog.Printf("Applying %d extra permissions from github-app.permissions", len(app.Permissions)) + for key, val := range app.Permissions { + scope := convertStringToPermissionScope(key) + if scope == "" { + msg := fmt.Sprintf("Unknown permission scope %q in tools.github.github-app.permissions. Valid scopes include: members, organization-administration, team-discussions, organization-members, administration, etc.", key) + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(msg)) + continue + } + permissions.Set(scope, PermissionLevel(val)) + } + } + // Generate the token minting step using the existing helper from safe_outputs_app.go steps := c.buildGitHubAppTokenMintStep(app, permissions, "") diff --git a/pkg/workflow/github_mcp_app_token_test.go b/pkg/workflow/github_mcp_app_token_test.go index 609cd2d48f5..1f14c9c4cf4 100644 --- a/pkg/workflow/github_mcp_app_token_test.go +++ b/pkg/workflow/github_mcp_app_token_test.go @@ -385,3 +385,106 @@ Test that permission-vulnerability-alerts is emitted in the App token minting st } } } + +// TestGitHubMCPAppTokenWithExtraPermissions tests that extra permissions under +// tools.github.github-app.permissions are merged into the minted token (nested wins). +// This allows org-level permissions (e.g. members: read) that are not valid GitHub +// Actions scopes but are supported by GitHub Apps. +func TestGitHubMCPAppTokenWithExtraPermissions(t *testing.T) { + compiler := NewCompilerWithVersion("1.0.0") + + markdown := `--- +on: issues +permissions: + contents: read + issues: read +strict: false +tools: + github: + mode: local + toolsets: [orgs, users] + github-app: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + repositories: ["*"] + permissions: + members: read + organization-administration: read +--- + +# Test Workflow + +Test extra org-level permissions in GitHub App token. +` + + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "test.md") + err := os.WriteFile(testFile, []byte(markdown), 0644) + require.NoError(t, err, "Failed to write test file") + + err = compiler.CompileWorkflow(testFile) + require.NoError(t, err, "Failed to compile workflow") + + lockFile := strings.TrimSuffix(testFile, ".md") + ".lock.yml" + content, err := os.ReadFile(lockFile) + require.NoError(t, err, "Failed to read lock file") + lockContent := string(content) + + // Verify the token minting step is present + assert.Contains(t, lockContent, "id: github-mcp-app-token", "GitHub App token step should be generated") + + // Verify that job-level permissions are still included + assert.Contains(t, lockContent, "permission-contents: read", "Should include job-level contents permission") + assert.Contains(t, lockContent, "permission-issues: read", "Should include job-level issues permission") + + // Verify that the extra org-level permissions from github-app.permissions are included + assert.Contains(t, lockContent, "permission-members: read", "Should include extra members permission from github-app.permissions") + assert.Contains(t, lockContent, "permission-organization-administration: read", "Should include extra organization-administration permission from github-app.permissions") +} + +// TestGitHubMCPAppTokenExtraPermissionsOverrideJobLevel tests that extra permissions +// under tools.github.github-app.permissions override job-level permissions (nested wins). +func TestGitHubMCPAppTokenExtraPermissionsOverrideJobLevel(t *testing.T) { + compiler := NewCompilerWithVersion("1.0.0") + + markdown := `--- +on: issues +permissions: + contents: read + issues: read +strict: false +tools: + github: + mode: local + github-app: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + permissions: + contents: write +--- + +# Test Workflow + +Test that nested permissions override job-level (nested wins). +` + + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "test.md") + err := os.WriteFile(testFile, []byte(markdown), 0644) + require.NoError(t, err, "Failed to write test file") + + err = compiler.CompileWorkflow(testFile) + require.NoError(t, err, "Failed to compile workflow") + + lockFile := strings.TrimSuffix(testFile, ".md") + ".lock.yml" + content, err := os.ReadFile(lockFile) + require.NoError(t, err, "Failed to read lock file") + lockContent := string(content) + + // The nested permission (write) should win over the job-level permission (read) + assert.Contains(t, lockContent, "permission-contents: write", "Nested contents: write should override job-level contents: read") + assert.NotContains(t, lockContent, "permission-contents: read", "Job-level contents: read should be overridden by nested write") + + // Other job-level permissions should still be present + assert.Contains(t, lockContent, "permission-issues: read", "Unaffected job-level issues permission should still be present") +} diff --git a/pkg/workflow/safe_outputs_app_config.go b/pkg/workflow/safe_outputs_app_config.go index 92b7bbddf7f..ff0df2a1296 100644 --- a/pkg/workflow/safe_outputs_app_config.go +++ b/pkg/workflow/safe_outputs_app_config.go @@ -16,10 +16,11 @@ var safeOutputsAppLog = logger.New("workflow:safe_outputs_app") // GitHubAppConfig holds configuration for GitHub App-based token minting type GitHubAppConfig struct { - AppID string `yaml:"app-id,omitempty"` // GitHub App ID (e.g., "${{ vars.APP_ID }}") - PrivateKey string `yaml:"private-key,omitempty"` // GitHub App private key (e.g., "${{ secrets.APP_PRIVATE_KEY }}") - Owner string `yaml:"owner,omitempty"` // Optional: owner of the GitHub App installation (defaults to current repository owner) - Repositories []string `yaml:"repositories,omitempty"` // Optional: comma or newline-separated list of repositories to grant access to + AppID string `yaml:"app-id,omitempty"` // GitHub App ID (e.g., "${{ vars.APP_ID }}") + PrivateKey string `yaml:"private-key,omitempty"` // GitHub App private key (e.g., "${{ secrets.APP_PRIVATE_KEY }}") + Owner string `yaml:"owner,omitempty"` // Optional: owner of the GitHub App installation (defaults to current repository owner) + Repositories []string `yaml:"repositories,omitempty"` // Optional: comma or newline-separated list of repositories to grant access to + Permissions map[string]string `yaml:"permissions,omitempty"` // Optional: extra permission-* fields to merge into the minted token (nested wins over job-level) } // ======================================== @@ -65,6 +66,18 @@ func parseAppConfig(appMap map[string]any) *GitHubAppConfig { } } + // Parse permissions (optional) - extra permission-* fields to merge into the minted token + if perms, exists := appMap["permissions"]; exists { + if permsMap, ok := perms.(map[string]any); ok { + appConfig.Permissions = make(map[string]string, len(permsMap)) + for key, val := range permsMap { + if valStr, ok := val.(string); ok { + appConfig.Permissions[key] = valStr + } + } + } + } + return appConfig } From a879f399f3aaa9ff6a5dd4818b2be040d2039d09 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 23:29:39 +0000 Subject: [PATCH 3/6] fix: restrict github-app.permissions to explicit app-only scopes with read/none only - Enumerate all GitHub App-only scopes explicitly instead of using additionalProperties - Only allow 'read' and 'none' values (not 'write') - Update override test to use a valid GitHub App-only scope (vulnerability-alerts)" Agent-Logs-Url: https://github.com/github/gh-aw/sessions/3e32b3bc-7c8b-4f55-a88f-3d940a2ae8e6 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/parser/schemas/main_workflow_schema.json | 157 ++++++++++++++++++- pkg/workflow/github_mcp_app_token_test.go | 15 +- 2 files changed, 162 insertions(+), 10 deletions(-) diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index d40080f9f5b..0bedfd48368 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -9680,10 +9680,159 @@ }, "permissions": { "type": "object", - "description": "Optional extra permissions to merge into the minted token. These override the job-level permissions for the token (nested wins). Use this to add org-level permissions (e.g., members: read) that are not valid GitHub Actions scopes but are supported by GitHub Apps.", - "additionalProperties": { - "type": "string", - "enum": ["read", "write", "none"] + "description": "Optional extra GitHub App-only permissions to merge into the minted token. These are added on top of job-level permissions (nested wins). Use this for org-level permissions (e.g., members: read) that are not valid GitHub Actions scopes. Only 'read' and 'none' are allowed.", + "additionalProperties": false, + "properties": { + "administration": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for repository administration (read/none). GitHub App-only permission for repository administration." + }, + "codespaces": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for Codespaces (read/none). GitHub App-only permission." + }, + "codespaces-lifecycle-admin": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for Codespaces lifecycle administration (read/none). GitHub App-only permission." + }, + "codespaces-metadata": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for Codespaces metadata (read/none). GitHub App-only permission." + }, + "email-addresses": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for user email addresses (read/none). GitHub App-only permission." + }, + "environments": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for repository environments (read/none). GitHub App-only permission." + }, + "git-signing": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for git signing (read/none). GitHub App-only permission." + }, + "members": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization members (read/none). Required for org team membership API calls." + }, + "organization-administration": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization administration (read/none). GitHub App-only permission." + }, + "organization-announcement-banners": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization announcement banners (read/none). GitHub App-only permission." + }, + "organization-codespaces": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization Codespaces (read/none). GitHub App-only permission." + }, + "organization-copilot": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization Copilot (read/none). GitHub App-only permission." + }, + "organization-custom-org-roles": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization custom org roles (read/none). GitHub App-only permission." + }, + "organization-custom-properties": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization custom properties (read/none). GitHub App-only permission." + }, + "organization-custom-repository-roles": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization custom repository roles (read/none). GitHub App-only permission." + }, + "organization-events": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization events (read/none). GitHub App-only permission." + }, + "organization-hooks": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization webhooks (read/none). GitHub App-only permission." + }, + "organization-members": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization members management (read/none). GitHub App-only permission." + }, + "organization-packages": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization packages (read/none). GitHub App-only permission." + }, + "organization-personal-access-token-requests": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization personal access token requests (read/none). GitHub App-only permission." + }, + "organization-personal-access-tokens": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization personal access tokens (read/none). GitHub App-only permission." + }, + "organization-plan": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization plan (read/none). GitHub App-only permission." + }, + "organization-self-hosted-runners": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization self-hosted runners (read/none). GitHub App-only permission." + }, + "organization-user-blocking": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization user blocking (read/none). GitHub App-only permission." + }, + "repository-custom-properties": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for repository custom properties (read/none). GitHub App-only permission." + }, + "repository-hooks": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for repository webhooks (read/none). GitHub App-only permission." + }, + "single-file": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for single file access (read/none). GitHub App-only permission." + }, + "team-discussions": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for team discussions (read/none). GitHub App-only permission." + }, + "vulnerability-alerts": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for Dependabot vulnerability alerts (read/none). GitHub App-only permission." + }, + "workflows": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for GitHub Actions workflow files (read/none). GitHub App-only permission." + } }, "examples": [ { diff --git a/pkg/workflow/github_mcp_app_token_test.go b/pkg/workflow/github_mcp_app_token_test.go index 1f14c9c4cf4..32c56abd280 100644 --- a/pkg/workflow/github_mcp_app_token_test.go +++ b/pkg/workflow/github_mcp_app_token_test.go @@ -443,7 +443,8 @@ Test extra org-level permissions in GitHub App token. } // TestGitHubMCPAppTokenExtraPermissionsOverrideJobLevel tests that extra permissions -// under tools.github.github-app.permissions override job-level permissions (nested wins). +// under tools.github.github-app.permissions can suppress a GitHub App-only scope +// that was set at job level by overriding it with 'none' (nested wins). func TestGitHubMCPAppTokenExtraPermissionsOverrideJobLevel(t *testing.T) { compiler := NewCompilerWithVersion("1.0.0") @@ -452,6 +453,7 @@ on: issues permissions: contents: read issues: read + vulnerability-alerts: read strict: false tools: github: @@ -460,12 +462,12 @@ tools: app-id: ${{ vars.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} permissions: - contents: write + vulnerability-alerts: none --- # Test Workflow -Test that nested permissions override job-level (nested wins). +Test that nested permissions override job-level GitHub App-only scopes (nested wins). ` tmpDir := t.TempDir() @@ -481,10 +483,11 @@ Test that nested permissions override job-level (nested wins). require.NoError(t, err, "Failed to read lock file") lockContent := string(content) - // The nested permission (write) should win over the job-level permission (read) - assert.Contains(t, lockContent, "permission-contents: write", "Nested contents: write should override job-level contents: read") - assert.NotContains(t, lockContent, "permission-contents: read", "Job-level contents: read should be overridden by nested write") + // The nested permission (none) should win over the job-level permission (read) + assert.Contains(t, lockContent, "permission-vulnerability-alerts: none", "Nested vulnerability-alerts: none should override job-level: read") + assert.NotContains(t, lockContent, "permission-vulnerability-alerts: read", "Job-level vulnerability-alerts: read should be overridden by nested none") // Other job-level permissions should still be present + assert.Contains(t, lockContent, "permission-contents: read", "Unaffected job-level contents permission should still be present") assert.Contains(t, lockContent, "permission-issues: read", "Unaffected job-level issues permission should still be present") } From a6d3df90b64fc4dfb8494d8fedf7beb4b58b4702 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 23:42:03 +0000 Subject: [PATCH 4/6] refactor: extract permissions maps into $defs and use $ref for reuse - Add $defs.github_actions_permissions (the full GitHub Actions permissions object) - Add $defs.github_app_permissions (the GitHub App-only permissions object) - Replace inline definitions with $ref references at both usage points Agent-Logs-Url: https://github.com/github/gh-aw/sessions/8f5e79a1-f47d-4d4a-bfd6-0e5f8edd76cc Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/parser/schemas/main_workflow_schema.json | 534 ++++++++++--------- 1 file changed, 270 insertions(+), 264 deletions(-) diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 0bedfd48368..e514fd2ab1c 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -2147,106 +2147,7 @@ "description": "Simple permissions string: 'read-all' (all read permissions) or 'write-all' (all write permissions)" }, { - "type": "object", - "description": "Detailed permissions object with granular control over specific GitHub API scopes", - "additionalProperties": false, - "properties": { - "actions": { - "type": "string", - "enum": ["read", "write", "none"], - "description": "Permission for GitHub Actions workflows and runs (read: view workflows, write: manage workflows, none: no access)" - }, - "attestations": { - "type": "string", - "enum": ["read", "write", "none"], - "description": "Permission for artifact attestations (read: view attestations, write: create attestations, none: no access)" - }, - "checks": { - "type": "string", - "enum": ["read", "write", "none"], - "description": "Permission for repository checks and status checks (read: view checks, write: create/update checks, none: no access)" - }, - "contents": { - "type": "string", - "enum": ["read", "write", "none"], - "description": "Permission for repository contents (read: view files, write: modify files/branches, none: no access)" - }, - "deployments": { - "type": "string", - "enum": ["read", "write", "none"], - "description": "Permission for repository deployments (read: view deployments, write: create/update deployments, none: no access)" - }, - "discussions": { - "type": "string", - "enum": ["read", "write", "none"], - "description": "Permission for repository discussions (read: view discussions, write: create/update discussions, none: no access)" - }, - "id-token": { - "type": "string", - "enum": ["write", "none"], - "description": "Permission level for OIDC token requests (write/none only - read is not supported). Allows workflows to request JWT tokens for cloud provider authentication." - }, - "issues": { - "type": "string", - "enum": ["read", "write", "none"], - "description": "Permission for repository issues (read: view issues, write: create/update/close issues, none: no access)" - }, - "models": { - "type": "string", - "enum": ["read", "none"], - "description": "Permission for GitHub Copilot models (read: access AI models for agentic workflows, none: no access)" - }, - "metadata": { - "type": "string", - "enum": ["read", "write", "none"], - "description": "Permission for repository metadata (read: view repository information, write: update repository metadata, none: no access)" - }, - "packages": { - "type": "string", - "enum": ["read", "write", "none"], - "description": "Permission level for GitHub Packages (read/write/none). Controls access to publish, modify, or delete packages." - }, - "pages": { - "type": "string", - "enum": ["read", "write", "none"], - "description": "Permission level for GitHub Pages (read/write/none). Controls access to deploy and manage GitHub Pages sites." - }, - "pull-requests": { - "type": "string", - "enum": ["read", "write", "none"], - "description": "Permission level for pull requests (read/write/none). Controls access to create, edit, review, and manage pull requests." - }, - "repository-projects": { - "type": "string", - "enum": ["read", "write", "none"], - "description": "Permission level for repository projects (read/write/none). Controls access to manage repository-level GitHub Projects boards." - }, - "organization-projects": { - "type": "string", - "enum": ["read", "write", "none"], - "description": "Permission level for organization projects (read/write/none). Controls access to manage organization-level GitHub Projects boards." - }, - "security-events": { - "type": "string", - "enum": ["read", "write", "none"], - "description": "Permission level for security events (read/write/none). Controls access to view and manage code scanning alerts and security findings." - }, - "statuses": { - "type": "string", - "enum": ["read", "write", "none"], - "description": "Permission level for commit statuses (read/write/none). Controls access to create and update commit status checks." - }, - "vulnerability-alerts": { - "type": "string", - "enum": ["read", "write", "none"], - "description": "Permission level for Dependabot vulnerability alerts (read/write/none). GitHub App-only permission: required to access Dependabot alerts via the GitHub MCP server. The GITHUB_TOKEN does not have this permission \u2014 a GitHub App must be configured." - }, - "all": { - "type": "string", - "enum": ["read"], - "description": "Permission shorthand that applies read access to all permission scopes. Can be combined with specific write permissions to override individual scopes. 'write' is not allowed for all." - } - } + "$ref": "#/$defs/github_actions_permissions" } ] }, @@ -9679,170 +9580,7 @@ } }, "permissions": { - "type": "object", - "description": "Optional extra GitHub App-only permissions to merge into the minted token. These are added on top of job-level permissions (nested wins). Use this for org-level permissions (e.g., members: read) that are not valid GitHub Actions scopes. Only 'read' and 'none' are allowed.", - "additionalProperties": false, - "properties": { - "administration": { - "type": "string", - "enum": ["read", "none"], - "description": "Permission level for repository administration (read/none). GitHub App-only permission for repository administration." - }, - "codespaces": { - "type": "string", - "enum": ["read", "none"], - "description": "Permission level for Codespaces (read/none). GitHub App-only permission." - }, - "codespaces-lifecycle-admin": { - "type": "string", - "enum": ["read", "none"], - "description": "Permission level for Codespaces lifecycle administration (read/none). GitHub App-only permission." - }, - "codespaces-metadata": { - "type": "string", - "enum": ["read", "none"], - "description": "Permission level for Codespaces metadata (read/none). GitHub App-only permission." - }, - "email-addresses": { - "type": "string", - "enum": ["read", "none"], - "description": "Permission level for user email addresses (read/none). GitHub App-only permission." - }, - "environments": { - "type": "string", - "enum": ["read", "none"], - "description": "Permission level for repository environments (read/none). GitHub App-only permission." - }, - "git-signing": { - "type": "string", - "enum": ["read", "none"], - "description": "Permission level for git signing (read/none). GitHub App-only permission." - }, - "members": { - "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization members (read/none). Required for org team membership API calls." - }, - "organization-administration": { - "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization administration (read/none). GitHub App-only permission." - }, - "organization-announcement-banners": { - "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization announcement banners (read/none). GitHub App-only permission." - }, - "organization-codespaces": { - "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization Codespaces (read/none). GitHub App-only permission." - }, - "organization-copilot": { - "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization Copilot (read/none). GitHub App-only permission." - }, - "organization-custom-org-roles": { - "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization custom org roles (read/none). GitHub App-only permission." - }, - "organization-custom-properties": { - "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization custom properties (read/none). GitHub App-only permission." - }, - "organization-custom-repository-roles": { - "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization custom repository roles (read/none). GitHub App-only permission." - }, - "organization-events": { - "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization events (read/none). GitHub App-only permission." - }, - "organization-hooks": { - "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization webhooks (read/none). GitHub App-only permission." - }, - "organization-members": { - "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization members management (read/none). GitHub App-only permission." - }, - "organization-packages": { - "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization packages (read/none). GitHub App-only permission." - }, - "organization-personal-access-token-requests": { - "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization personal access token requests (read/none). GitHub App-only permission." - }, - "organization-personal-access-tokens": { - "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization personal access tokens (read/none). GitHub App-only permission." - }, - "organization-plan": { - "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization plan (read/none). GitHub App-only permission." - }, - "organization-self-hosted-runners": { - "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization self-hosted runners (read/none). GitHub App-only permission." - }, - "organization-user-blocking": { - "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization user blocking (read/none). GitHub App-only permission." - }, - "repository-custom-properties": { - "type": "string", - "enum": ["read", "none"], - "description": "Permission level for repository custom properties (read/none). GitHub App-only permission." - }, - "repository-hooks": { - "type": "string", - "enum": ["read", "none"], - "description": "Permission level for repository webhooks (read/none). GitHub App-only permission." - }, - "single-file": { - "type": "string", - "enum": ["read", "none"], - "description": "Permission level for single file access (read/none). GitHub App-only permission." - }, - "team-discussions": { - "type": "string", - "enum": ["read", "none"], - "description": "Permission level for team discussions (read/none). GitHub App-only permission." - }, - "vulnerability-alerts": { - "type": "string", - "enum": ["read", "none"], - "description": "Permission level for Dependabot vulnerability alerts (read/none). GitHub App-only permission." - }, - "workflows": { - "type": "string", - "enum": ["read", "none"], - "description": "Permission level for GitHub Actions workflow files (read/none). GitHub App-only permission." - } - }, - "examples": [ - { - "members": "read" - }, - { - "members": "read", - "organization-administration": "read" - } - ] + "$ref": "#/$defs/github_app_permissions" } }, "required": ["app-id", "private-key"], @@ -10122,6 +9860,274 @@ "description": "GitHub App configuration used to mint a token for the search API request. Mutually exclusive with github-token." } } + }, + "github_actions_permissions": { + "type": "object", + "description": "Detailed permissions object with granular control over specific GitHub API scopes", + "additionalProperties": false, + "properties": { + "actions": { + "type": "string", + "enum": ["read", "write", "none"], + "description": "Permission for GitHub Actions workflows and runs (read: view workflows, write: manage workflows, none: no access)" + }, + "attestations": { + "type": "string", + "enum": ["read", "write", "none"], + "description": "Permission for artifact attestations (read: view attestations, write: create attestations, none: no access)" + }, + "checks": { + "type": "string", + "enum": ["read", "write", "none"], + "description": "Permission for repository checks and status checks (read: view checks, write: create/update checks, none: no access)" + }, + "contents": { + "type": "string", + "enum": ["read", "write", "none"], + "description": "Permission for repository contents (read: view files, write: modify files/branches, none: no access)" + }, + "deployments": { + "type": "string", + "enum": ["read", "write", "none"], + "description": "Permission for repository deployments (read: view deployments, write: create/update deployments, none: no access)" + }, + "discussions": { + "type": "string", + "enum": ["read", "write", "none"], + "description": "Permission for repository discussions (read: view discussions, write: create/update discussions, none: no access)" + }, + "id-token": { + "type": "string", + "enum": ["write", "none"], + "description": "Permission level for OIDC token requests (write/none only - read is not supported). Allows workflows to request JWT tokens for cloud provider authentication." + }, + "issues": { + "type": "string", + "enum": ["read", "write", "none"], + "description": "Permission for repository issues (read: view issues, write: create/update/close issues, none: no access)" + }, + "models": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission for GitHub Copilot models (read: access AI models for agentic workflows, none: no access)" + }, + "metadata": { + "type": "string", + "enum": ["read", "write", "none"], + "description": "Permission for repository metadata (read: view repository information, write: update repository metadata, none: no access)" + }, + "packages": { + "type": "string", + "enum": ["read", "write", "none"], + "description": "Permission level for GitHub Packages (read/write/none). Controls access to publish, modify, or delete packages." + }, + "pages": { + "type": "string", + "enum": ["read", "write", "none"], + "description": "Permission level for GitHub Pages (read/write/none). Controls access to deploy and manage GitHub Pages sites." + }, + "pull-requests": { + "type": "string", + "enum": ["read", "write", "none"], + "description": "Permission level for pull requests (read/write/none). Controls access to create, edit, review, and manage pull requests." + }, + "repository-projects": { + "type": "string", + "enum": ["read", "write", "none"], + "description": "Permission level for repository projects (read/write/none). Controls access to manage repository-level GitHub Projects boards." + }, + "organization-projects": { + "type": "string", + "enum": ["read", "write", "none"], + "description": "Permission level for organization projects (read/write/none). Controls access to manage organization-level GitHub Projects boards." + }, + "security-events": { + "type": "string", + "enum": ["read", "write", "none"], + "description": "Permission level for security events (read/write/none). Controls access to view and manage code scanning alerts and security findings." + }, + "statuses": { + "type": "string", + "enum": ["read", "write", "none"], + "description": "Permission level for commit statuses (read/write/none). Controls access to create and update commit status checks." + }, + "vulnerability-alerts": { + "type": "string", + "enum": ["read", "write", "none"], + "description": "Permission level for Dependabot vulnerability alerts (read/write/none). GitHub App-only permission: required to access Dependabot alerts via the GitHub MCP server. The GITHUB_TOKEN does not have this permission \u2014 a GitHub App must be configured." + }, + "all": { + "type": "string", + "enum": ["read"], + "description": "Permission shorthand that applies read access to all permission scopes. Can be combined with specific write permissions to override individual scopes. 'write' is not allowed for all." + } + } + }, + "github_app_permissions": { + "type": "object", + "description": "Optional extra GitHub App-only permissions to merge into the minted token. These are added on top of job-level permissions (nested wins). Use this for org-level permissions (e.g., members: read) that are not valid GitHub Actions scopes. Only 'read' and 'none' are allowed.", + "additionalProperties": false, + "properties": { + "administration": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for repository administration (read/none). GitHub App-only permission for repository administration." + }, + "codespaces": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for Codespaces (read/none). GitHub App-only permission." + }, + "codespaces-lifecycle-admin": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for Codespaces lifecycle administration (read/none). GitHub App-only permission." + }, + "codespaces-metadata": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for Codespaces metadata (read/none). GitHub App-only permission." + }, + "email-addresses": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for user email addresses (read/none). GitHub App-only permission." + }, + "environments": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for repository environments (read/none). GitHub App-only permission." + }, + "git-signing": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for git signing (read/none). GitHub App-only permission." + }, + "members": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization members (read/none). Required for org team membership API calls." + }, + "organization-administration": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization administration (read/none). GitHub App-only permission." + }, + "organization-announcement-banners": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization announcement banners (read/none). GitHub App-only permission." + }, + "organization-codespaces": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization Codespaces (read/none). GitHub App-only permission." + }, + "organization-copilot": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization Copilot (read/none). GitHub App-only permission." + }, + "organization-custom-org-roles": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization custom org roles (read/none). GitHub App-only permission." + }, + "organization-custom-properties": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization custom properties (read/none). GitHub App-only permission." + }, + "organization-custom-repository-roles": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization custom repository roles (read/none). GitHub App-only permission." + }, + "organization-events": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization events (read/none). GitHub App-only permission." + }, + "organization-hooks": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization webhooks (read/none). GitHub App-only permission." + }, + "organization-members": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization members management (read/none). GitHub App-only permission." + }, + "organization-packages": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization packages (read/none). GitHub App-only permission." + }, + "organization-personal-access-token-requests": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization personal access token requests (read/none). GitHub App-only permission." + }, + "organization-personal-access-tokens": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization personal access tokens (read/none). GitHub App-only permission." + }, + "organization-plan": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization plan (read/none). GitHub App-only permission." + }, + "organization-self-hosted-runners": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization self-hosted runners (read/none). GitHub App-only permission." + }, + "organization-user-blocking": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for organization user blocking (read/none). GitHub App-only permission." + }, + "repository-custom-properties": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for repository custom properties (read/none). GitHub App-only permission." + }, + "repository-hooks": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for repository webhooks (read/none). GitHub App-only permission." + }, + "single-file": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for single file access (read/none). GitHub App-only permission." + }, + "team-discussions": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for team discussions (read/none). GitHub App-only permission." + }, + "vulnerability-alerts": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for Dependabot vulnerability alerts (read/none). GitHub App-only permission." + }, + "workflows": { + "type": "string", + "enum": ["read", "none"], + "description": "Permission level for GitHub Actions workflow files (read/none). GitHub App-only permission." + } + }, + "examples": [ + { + "members": "read" + }, + { + "members": "read", + "organization-administration": "read" + } + ] } } } From 3335321ecd5ca5032fb670d7e0f788ac23cd2a34 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 00:03:01 +0000 Subject: [PATCH 5/6] feat: allow "write" in github_app_permissions schema but reject it at compile time - Add "write" to all enum values in $defs.github_app_permissions so editors can offer it as a completion and users get a clear compiler error instead of a silent schema validation pass-through - Add validateGitHubMCPAppPermissionsNoWrite() to github_app_permissions_validation.go with an informative error listing the offending scopes - Wire the new validator into compiler.go alongside the existing GitHub App permission checks - Add TestGitHubMCPAppTokenExtraPermissionsWriteRejected to cover the error path Agent-Logs-Url: https://github.com/github/gh-aw/sessions/17aa2920-f546-4f22-b7aa-992c63901959 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/parser/schemas/main_workflow_schema.json | 122 +++++++++--------- pkg/workflow/compiler.go | 6 + .../github_app_permissions_validation.go | 42 ++++++ pkg/workflow/github_mcp_app_token_test.go | 37 ++++++ 4 files changed, 146 insertions(+), 61 deletions(-) diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index e514fd2ab1c..d4b18ab3470 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -9965,158 +9965,158 @@ }, "github_app_permissions": { "type": "object", - "description": "Optional extra GitHub App-only permissions to merge into the minted token. These are added on top of job-level permissions (nested wins). Use this for org-level permissions (e.g., members: read) that are not valid GitHub Actions scopes. Only 'read' and 'none' are allowed.", + "description": "Optional extra GitHub App-only permissions to merge into the minted token. These are added on top of job-level permissions (nested wins). Use this for org-level permissions (e.g., members: read) that are not valid GitHub Actions scopes. Only \"read\" and \"none\" are allowed; specifying \"write\" is a compiler error.", "additionalProperties": false, "properties": { "administration": { "type": "string", - "enum": ["read", "none"], - "description": "Permission level for repository administration (read/none). GitHub App-only permission for repository administration." + "enum": ["read", "none", "write"], + "description": "Permission level for repository administration (read/none; \"write\" is rejected by the compiler). GitHub App-only permission for repository administration." }, "codespaces": { "type": "string", - "enum": ["read", "none"], - "description": "Permission level for Codespaces (read/none). GitHub App-only permission." + "enum": ["read", "none", "write"], + "description": "Permission level for Codespaces (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "codespaces-lifecycle-admin": { "type": "string", - "enum": ["read", "none"], - "description": "Permission level for Codespaces lifecycle administration (read/none). GitHub App-only permission." + "enum": ["read", "none", "write"], + "description": "Permission level for Codespaces lifecycle administration (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "codespaces-metadata": { "type": "string", - "enum": ["read", "none"], - "description": "Permission level for Codespaces metadata (read/none). GitHub App-only permission." + "enum": ["read", "none", "write"], + "description": "Permission level for Codespaces metadata (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "email-addresses": { "type": "string", - "enum": ["read", "none"], - "description": "Permission level for user email addresses (read/none). GitHub App-only permission." + "enum": ["read", "none", "write"], + "description": "Permission level for user email addresses (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "environments": { "type": "string", - "enum": ["read", "none"], - "description": "Permission level for repository environments (read/none). GitHub App-only permission." + "enum": ["read", "none", "write"], + "description": "Permission level for repository environments (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "git-signing": { "type": "string", - "enum": ["read", "none"], - "description": "Permission level for git signing (read/none). GitHub App-only permission." + "enum": ["read", "none", "write"], + "description": "Permission level for git signing (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "members": { "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization members (read/none). Required for org team membership API calls." + "enum": ["read", "none", "write"], + "description": "Permission level for organization members (read/none; \"write\" is rejected by the compiler). Required for org team membership API calls." }, "organization-administration": { "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization administration (read/none). GitHub App-only permission." + "enum": ["read", "none", "write"], + "description": "Permission level for organization administration (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "organization-announcement-banners": { "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization announcement banners (read/none). GitHub App-only permission." + "enum": ["read", "none", "write"], + "description": "Permission level for organization announcement banners (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "organization-codespaces": { "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization Codespaces (read/none). GitHub App-only permission." + "enum": ["read", "none", "write"], + "description": "Permission level for organization Codespaces (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "organization-copilot": { "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization Copilot (read/none). GitHub App-only permission." + "enum": ["read", "none", "write"], + "description": "Permission level for organization Copilot (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "organization-custom-org-roles": { "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization custom org roles (read/none). GitHub App-only permission." + "enum": ["read", "none", "write"], + "description": "Permission level for organization custom org roles (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "organization-custom-properties": { "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization custom properties (read/none). GitHub App-only permission." + "enum": ["read", "none", "write"], + "description": "Permission level for organization custom properties (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "organization-custom-repository-roles": { "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization custom repository roles (read/none). GitHub App-only permission." + "enum": ["read", "none", "write"], + "description": "Permission level for organization custom repository roles (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "organization-events": { "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization events (read/none). GitHub App-only permission." + "enum": ["read", "none", "write"], + "description": "Permission level for organization events (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "organization-hooks": { "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization webhooks (read/none). GitHub App-only permission." + "enum": ["read", "none", "write"], + "description": "Permission level for organization webhooks (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "organization-members": { "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization members management (read/none). GitHub App-only permission." + "enum": ["read", "none", "write"], + "description": "Permission level for organization members management (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "organization-packages": { "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization packages (read/none). GitHub App-only permission." + "enum": ["read", "none", "write"], + "description": "Permission level for organization packages (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "organization-personal-access-token-requests": { "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization personal access token requests (read/none). GitHub App-only permission." + "enum": ["read", "none", "write"], + "description": "Permission level for organization personal access token requests (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "organization-personal-access-tokens": { "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization personal access tokens (read/none). GitHub App-only permission." + "enum": ["read", "none", "write"], + "description": "Permission level for organization personal access tokens (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "organization-plan": { "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization plan (read/none). GitHub App-only permission." + "enum": ["read", "none", "write"], + "description": "Permission level for organization plan (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "organization-self-hosted-runners": { "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization self-hosted runners (read/none). GitHub App-only permission." + "enum": ["read", "none", "write"], + "description": "Permission level for organization self-hosted runners (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "organization-user-blocking": { "type": "string", - "enum": ["read", "none"], - "description": "Permission level for organization user blocking (read/none). GitHub App-only permission." + "enum": ["read", "none", "write"], + "description": "Permission level for organization user blocking (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "repository-custom-properties": { "type": "string", - "enum": ["read", "none"], - "description": "Permission level for repository custom properties (read/none). GitHub App-only permission." + "enum": ["read", "none", "write"], + "description": "Permission level for repository custom properties (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "repository-hooks": { "type": "string", - "enum": ["read", "none"], - "description": "Permission level for repository webhooks (read/none). GitHub App-only permission." + "enum": ["read", "none", "write"], + "description": "Permission level for repository webhooks (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "single-file": { "type": "string", - "enum": ["read", "none"], - "description": "Permission level for single file access (read/none). GitHub App-only permission." + "enum": ["read", "none", "write"], + "description": "Permission level for single file access (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "team-discussions": { "type": "string", - "enum": ["read", "none"], - "description": "Permission level for team discussions (read/none). GitHub App-only permission." + "enum": ["read", "none", "write"], + "description": "Permission level for team discussions (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "vulnerability-alerts": { "type": "string", - "enum": ["read", "none"], - "description": "Permission level for Dependabot vulnerability alerts (read/none). GitHub App-only permission." + "enum": ["read", "none", "write"], + "description": "Permission level for Dependabot vulnerability alerts (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." }, "workflows": { "type": "string", - "enum": ["read", "none"], - "description": "Permission level for GitHub Actions workflow files (read/none). GitHub App-only permission." + "enum": ["read", "none", "write"], + "description": "Permission level for GitHub Actions workflow files (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." } }, "examples": [ diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index 109cc362698..fee40a0ffad 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -150,6 +150,12 @@ func (c *Compiler) validateWorkflowData(workflowData *WorkflowData, markdownPath return formatCompilerError(markdownPath, "error", err.Error(), err) } + // Validate tools.github.github-app.permissions does not use "write" + log.Printf("Validating GitHub MCP app permissions (no write)") + if err := validateGitHubMCPAppPermissionsNoWrite(workflowData); err != nil { + return formatCompilerError(markdownPath, "error", err.Error(), err) + } + // Validate agent file exists if specified in engine config log.Printf("Validating agent file if specified") if err := c.validateAgentFile(workflowData, markdownPath); err != nil { diff --git a/pkg/workflow/github_app_permissions_validation.go b/pkg/workflow/github_app_permissions_validation.go index 2158f7bceb4..663df330b68 100644 --- a/pkg/workflow/github_app_permissions_validation.go +++ b/pkg/workflow/github_app_permissions_validation.go @@ -109,6 +109,48 @@ func formatWriteOnAppScopesError(scopes []PermissionScope) error { return errors.New(strings.Join(lines, "\n")) } +// validateGitHubMCPAppPermissionsNoWrite validates that no scope in +// tools.github.github-app.permissions is set to "write". +// The schema allows "write" so that editors can offer it as a completion, but +// the compiler must reject it because GitHub App-only scopes have no write-level +// semantics in this context — write operations must go through safe-outputs. +func validateGitHubMCPAppPermissionsNoWrite(workflowData *WorkflowData) error { + if workflowData.ParsedTools == nil || + workflowData.ParsedTools.GitHub == nil || + workflowData.ParsedTools.GitHub.GitHubApp == nil { + return nil + } + app := workflowData.ParsedTools.GitHub.GitHubApp + if len(app.Permissions) == 0 { + return nil + } + + var writeScopes []string + for scope, level := range app.Permissions { + if level == string(PermissionWrite) { + writeScopes = append(writeScopes, scope) + } + } + if len(writeScopes) == 0 { + return nil + } + sort.Strings(writeScopes) + + var lines []string + lines = append(lines, `"write" is not allowed in tools.github.github-app.permissions.`) + lines = append(lines, "GitHub App-only scopes in this section must be declared as \"read\" or \"none\".") + lines = append(lines, "Write operations must be performed via safe-outputs, not through declared permissions.") + lines = append(lines, "") + lines = append(lines, "The following scopes were declared with \"write\" access:") + lines = append(lines, "") + for _, s := range writeScopes { + lines = append(lines, " - "+s) + } + lines = append(lines, "") + lines = append(lines, "Change the permission level to \"read\" for read-only access, or remove the entry.") + return errors.New(strings.Join(lines, "\n")) +} + func hasGitHubAppConfigured(workflowData *WorkflowData) bool { // Check tools.github.github-app if workflowData.ParsedTools != nil && diff --git a/pkg/workflow/github_mcp_app_token_test.go b/pkg/workflow/github_mcp_app_token_test.go index 32c56abd280..d25e8dc91c4 100644 --- a/pkg/workflow/github_mcp_app_token_test.go +++ b/pkg/workflow/github_mcp_app_token_test.go @@ -491,3 +491,40 @@ Test that nested permissions override job-level GitHub App-only scopes (nested w assert.Contains(t, lockContent, "permission-contents: read", "Unaffected job-level contents permission should still be present") assert.Contains(t, lockContent, "permission-issues: read", "Unaffected job-level issues permission should still be present") } + +// TestGitHubMCPAppTokenExtraPermissionsWriteRejected tests that the compiler +// rejects a workflow where tools.github.github-app.permissions contains a "write" +// value, since write access is not allowed for GitHub App-only scopes in this section. +func TestGitHubMCPAppTokenExtraPermissionsWriteRejected(t *testing.T) { + compiler := NewCompilerWithVersion("1.0.0") + + markdown := `--- +on: issues +permissions: + contents: read +strict: false +tools: + github: + mode: local + github-app: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + permissions: + members: write +--- + +# Test Workflow + +Test that write is rejected in tools.github.github-app.permissions. +` + + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "test.md") + err := os.WriteFile(testFile, []byte(markdown), 0644) + require.NoError(t, err, "Failed to write test file") + + err = compiler.CompileWorkflow(testFile) + require.Error(t, err, "Compiler should reject write in tools.github.github-app.permissions") + assert.Contains(t, err.Error(), `"write" is not allowed in tools.github.github-app.permissions`, "Error should mention that write is not allowed") + assert.Contains(t, err.Error(), "members", "Error should mention the offending scope") +} From 77fbde1a0148bd364747cdf8a8a98fbdc9794a80 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 00:25:19 +0000 Subject: [PATCH 6/6] fix: address copilot reviewer comments on github-app permissions - parseAppConfig: log warnings for non-string permission values and non-map permissions entries instead of silently dropping them - compiler_github_mcp_steps: normalize permission level (trim+lowercase) and emit a warning for any level that isn't "read" or "none" before skipping it - validateGitHubMCPAppPermissionsNoWrite: broaden validation to reject all invalid levels (not just "write") after normalization; list both write-specific and generic invalid scopes with separate explanations in the error message - warnGitHubAppPermissionsUnsupportedContexts: new function that emits a compile-time warning when permissions is set in safe-outputs.github-app, on.github-app, or the top-level github-app fallback (where it has no effect); called from compiler.go after the no-write check - schema: keep permissions in $defs.github_app (allOf+additionalProperties:false conflict prevents scoping via allOf) but update its description to clarify it only takes effect for tools.github.github-app; scoping enforced at runtime via the new warning function - test: update write-rejection assertion strings to match the new error format Agent-Logs-Url: https://github.com/github/gh-aw/sessions/293c96f3-a7d6-461b-88c3-8ded806c935c Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/parser/schemas/main_workflow_schema.json | 16 +--- pkg/workflow/compiler.go | 3 + pkg/workflow/compiler_github_mcp_steps.go | 8 +- .../github_app_permissions_validation.go | 79 ++++++++++++++++--- pkg/workflow/github_mcp_app_token_test.go | 3 +- pkg/workflow/safe_outputs_app_config.go | 4 + 6 files changed, 87 insertions(+), 26 deletions(-) diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index d4b18ab3470..166491a5d5f 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -3599,18 +3599,7 @@ }, "github-app": { "$ref": "#/$defs/github_app", - "description": "GitHub App configuration for token minting. When configured, a GitHub App installation access token is minted at workflow start and used instead of the default token. This token overrides any custom github-token setting and provides fine-grained permissions matching the agent job requirements.", - "examples": [ - { - "app-id": "${{ vars.APP_ID }}", - "private-key": "${{ secrets.APP_PRIVATE_KEY }}" - }, - { - "app-id": "${{ vars.APP_ID }}", - "private-key": "${{ secrets.APP_PRIVATE_KEY }}", - "repositories": ["repo1", "repo2"] - } - ] + "description": "GitHub App configuration for token minting. When configured, a GitHub App installation access token is minted at workflow start and used instead of the default token. This token overrides any custom github-token setting and provides fine-grained permissions matching the agent job requirements." } }, "additionalProperties": false, @@ -9580,7 +9569,8 @@ } }, "permissions": { - "$ref": "#/$defs/github_app_permissions" + "$ref": "#/$defs/github_app_permissions", + "description": "Optional extra GitHub App-only permissions to merge into the minted token. Only takes effect for tools.github.github-app; ignored in other github-app contexts." } }, "required": ["app-id", "private-key"], diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index fee40a0ffad..395e3076d73 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -156,6 +156,9 @@ func (c *Compiler) validateWorkflowData(workflowData *WorkflowData, markdownPath return formatCompilerError(markdownPath, "error", err.Error(), err) } + // Warn when github-app.permissions is set in contexts that don't support it + warnGitHubAppPermissionsUnsupportedContexts(workflowData) + // Validate agent file exists if specified in engine config log.Printf("Validating agent file if specified") if err := c.validateAgentFile(workflowData, markdownPath); err != nil { diff --git a/pkg/workflow/compiler_github_mcp_steps.go b/pkg/workflow/compiler_github_mcp_steps.go index 2965965f650..00bb19945fe 100644 --- a/pkg/workflow/compiler_github_mcp_steps.go +++ b/pkg/workflow/compiler_github_mcp_steps.go @@ -114,7 +114,13 @@ func (c *Compiler) generateGitHubMCPAppTokenMintingStep(yaml *strings.Builder, d fmt.Fprintln(os.Stderr, console.FormatWarningMessage(msg)) continue } - permissions.Set(scope, PermissionLevel(val)) + level := strings.ToLower(strings.TrimSpace(val)) + if level != string(PermissionRead) && level != string(PermissionNone) { + msg := fmt.Sprintf("Unknown permission level %q for scope %q in tools.github.github-app.permissions. Valid levels are: read, none.", val, key) + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(msg)) + continue + } + permissions.Set(scope, PermissionLevel(level)) } } diff --git a/pkg/workflow/github_app_permissions_validation.go b/pkg/workflow/github_app_permissions_validation.go index 663df330b68..c9fc228cbb3 100644 --- a/pkg/workflow/github_app_permissions_validation.go +++ b/pkg/workflow/github_app_permissions_validation.go @@ -2,8 +2,12 @@ package workflow import ( "errors" + "fmt" + "os" "sort" "strings" + + "github.com/github/gh-aw/pkg/console" ) var githubAppPermissionsLog = newValidationLogger("github_app_permissions") @@ -109,11 +113,12 @@ func formatWriteOnAppScopesError(scopes []PermissionScope) error { return errors.New(strings.Join(lines, "\n")) } -// validateGitHubMCPAppPermissionsNoWrite validates that no scope in -// tools.github.github-app.permissions is set to "write". +// validateGitHubMCPAppPermissionsNoWrite validates that every scope in +// tools.github.github-app.permissions is set to "read" or "none" (after trimming/lowercasing). // The schema allows "write" so that editors can offer it as a completion, but // the compiler must reject it because GitHub App-only scopes have no write-level // semantics in this context — write operations must go through safe-outputs. +// Any other unrecognised level is also rejected here. func validateGitHubMCPAppPermissionsNoWrite(workflowData *WorkflowData) error { if workflowData.ParsedTools == nil || workflowData.ParsedTools.GitHub == nil || @@ -125,32 +130,84 @@ func validateGitHubMCPAppPermissionsNoWrite(workflowData *WorkflowData) error { return nil } + var invalidScopes []string var writeScopes []string for scope, level := range app.Permissions { - if level == string(PermissionWrite) { - writeScopes = append(writeScopes, scope) + normalized := strings.ToLower(strings.TrimSpace(level)) + switch normalized { + case string(PermissionRead), string(PermissionNone): + // valid + default: + invalidScopes = append(invalidScopes, scope+" (level: "+level+")") + if normalized == string(PermissionWrite) { + writeScopes = append(writeScopes, scope) + } } } - if len(writeScopes) == 0 { + if len(invalidScopes) == 0 { return nil } + sort.Strings(invalidScopes) sort.Strings(writeScopes) var lines []string - lines = append(lines, `"write" is not allowed in tools.github.github-app.permissions.`) - lines = append(lines, "GitHub App-only scopes in this section must be declared as \"read\" or \"none\".") - lines = append(lines, "Write operations must be performed via safe-outputs, not through declared permissions.") + lines = append(lines, "Invalid permission levels in tools.github.github-app.permissions.") + lines = append(lines, "Each permission level must be exactly \"read\" or \"none\".") + if len(writeScopes) > 0 { + lines = append(lines, "") + lines = append(lines, `"write" is not allowed: write operations must be performed via safe-outputs.`) + lines = append(lines, "") + lines = append(lines, "The following scopes were declared with \"write\" access:") + lines = append(lines, "") + for _, s := range writeScopes { + lines = append(lines, " - "+s) + } + } lines = append(lines, "") - lines = append(lines, "The following scopes were declared with \"write\" access:") + lines = append(lines, "The following scopes have invalid permission levels:") lines = append(lines, "") - for _, s := range writeScopes { + for _, s := range invalidScopes { lines = append(lines, " - "+s) } lines = append(lines, "") - lines = append(lines, "Change the permission level to \"read\" for read-only access, or remove the entry.") + lines = append(lines, "Change the permission level to \"read\" for read-only access, or \"none\" to disable the scope.") return errors.New(strings.Join(lines, "\n")) } +// warnGitHubAppPermissionsUnsupportedContexts emits a warning when +// tools.github.github-app.permissions is set in contexts that do not support it. +// The permissions field only takes effect for the GitHub MCP token minting step; +// it is silently ignored if set on safe-outputs.github-app, on.github-app, or the +// top-level github-app fallback. +func warnGitHubAppPermissionsUnsupportedContexts(workflowData *WorkflowData) { + type context struct { + label string + app *GitHubAppConfig + } + unsupported := []context{ + {"safe-outputs.github-app", safeOutputsGitHubApp(workflowData)}, + {"on.github-app", workflowData.ActivationGitHubApp}, + {"github-app (top-level fallback)", workflowData.TopLevelGitHubApp}, + } + for _, ctx := range unsupported { + if ctx.app != nil && len(ctx.app.Permissions) > 0 { + msg := fmt.Sprintf( + "The 'permissions' field under '%s' has no effect. "+ + "Extra GitHub App permissions only apply to tools.github.github-app.", + ctx.label, + ) + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(msg)) + } + } +} + +func safeOutputsGitHubApp(workflowData *WorkflowData) *GitHubAppConfig { + if workflowData.SafeOutputs == nil { + return nil + } + return workflowData.SafeOutputs.GitHubApp +} + func hasGitHubAppConfigured(workflowData *WorkflowData) bool { // Check tools.github.github-app if workflowData.ParsedTools != nil && diff --git a/pkg/workflow/github_mcp_app_token_test.go b/pkg/workflow/github_mcp_app_token_test.go index d25e8dc91c4..af0e86bcc6f 100644 --- a/pkg/workflow/github_mcp_app_token_test.go +++ b/pkg/workflow/github_mcp_app_token_test.go @@ -525,6 +525,7 @@ Test that write is rejected in tools.github.github-app.permissions. err = compiler.CompileWorkflow(testFile) require.Error(t, err, "Compiler should reject write in tools.github.github-app.permissions") - assert.Contains(t, err.Error(), `"write" is not allowed in tools.github.github-app.permissions`, "Error should mention that write is not allowed") + assert.Contains(t, err.Error(), "Invalid permission levels in tools.github.github-app.permissions", "Error should mention invalid permission levels") + assert.Contains(t, err.Error(), `"write" is not allowed`, "Error should mention that write is not allowed") assert.Contains(t, err.Error(), "members", "Error should mention the offending scope") } diff --git a/pkg/workflow/safe_outputs_app_config.go b/pkg/workflow/safe_outputs_app_config.go index ff0df2a1296..dff060e66f0 100644 --- a/pkg/workflow/safe_outputs_app_config.go +++ b/pkg/workflow/safe_outputs_app_config.go @@ -73,8 +73,12 @@ func parseAppConfig(appMap map[string]any) *GitHubAppConfig { for key, val := range permsMap { if valStr, ok := val.(string); ok { appConfig.Permissions[key] = valStr + } else { + safeOutputsAppLog.Printf("Ignoring github-app.permissions[%q]: expected string value, got %T", key, val) } } + } else { + safeOutputsAppLog.Printf("Ignoring github-app.permissions: expected object, got %T", perms) } }