From 8117e84960d071e3235f7c478dd1d301074b384b Mon Sep 17 00:00:00 2001 From: Barak Korren Date: Thu, 18 Jun 2026 10:53:35 +0300 Subject: [PATCH 1/6] feat(mint): add e2e agent role for pool testing Register e2e in mintcore permissions and config.ValidRoles so mint add-role can bootstrap the e2e app before cross-org CI auth (#2155). Signed-off-by: Barak Korren Co-authored-by: Cursor --- internal/cli/mint_test.go | 4 ++++ internal/config/config.go | 2 +- internal/dispatch/gcf/mintsrc/mintcore/github.go.embed | 6 ++++++ internal/mintcore/github.go | 6 ++++++ internal/mintcore/github_test.go | 2 +- 5 files changed, 18 insertions(+), 2 deletions(-) diff --git a/internal/cli/mint_test.go b/internal/cli/mint_test.go index 534cd752b1..0ea16b2a06 100644 --- a/internal/cli/mint_test.go +++ b/internal/cli/mint_test.go @@ -983,6 +983,10 @@ func TestValidateMintSetupRole(t *testing.T) { require.NoError(t, err) assert.Equal(t, "coder", role) + role, err = validateMintSetupRole("e2e") + require.NoError(t, err) + assert.Equal(t, "e2e", role) + _, err = validateMintSetupRole("fix") require.Error(t, err) assert.Contains(t, err.Error(), "coder") diff --git a/internal/config/config.go b/internal/config/config.go index 6dcf4897eb..ce0c027ec2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -91,7 +91,7 @@ type OrgConfig struct { // ValidRoles returns the set of recognized agent roles. func ValidRoles() []string { - return []string{"fullsend", "triage", "coder", "review", "fix", "retro", "prioritize"} + return []string{"fullsend", "triage", "coder", "review", "fix", "retro", "prioritize", "e2e"} } // ValidProviders returns the set of recognized inference providers. diff --git a/internal/dispatch/gcf/mintsrc/mintcore/github.go.embed b/internal/dispatch/gcf/mintsrc/mintcore/github.go.embed index 9844af361c..16170ea105 100644 --- a/internal/dispatch/gcf/mintsrc/mintcore/github.go.embed +++ b/internal/dispatch/gcf/mintsrc/mintcore/github.go.embed @@ -64,6 +64,12 @@ var canonicalRolePermissions = map[string]map[string]string{ "retro": {"actions": "read", "contents": "read", "pull_requests": "write", "issues": "write", "metadata": "read"}, "prioritize": {"contents": "read", "issues": "write", "organization_projects": "write", "metadata": "read"}, "fullsend": {"actions": "write", "actions_variables": "read", "contents": "write", "pull_requests": "write", "workflows": "write", "metadata": "read"}, + "e2e": { + "actions": "write", "actions_variables": "read", "administration": "write", + "contents": "write", "issues": "write", "members": "write", "metadata": "read", + "organization_administration": "write", "pull_requests": "write", + "secrets": "write", "workflows": "write", + }, } // RolePermissions returns a deep copy of the role-to-permissions map, diff --git a/internal/mintcore/github.go b/internal/mintcore/github.go index 9844af361c..16170ea105 100644 --- a/internal/mintcore/github.go +++ b/internal/mintcore/github.go @@ -64,6 +64,12 @@ var canonicalRolePermissions = map[string]map[string]string{ "retro": {"actions": "read", "contents": "read", "pull_requests": "write", "issues": "write", "metadata": "read"}, "prioritize": {"contents": "read", "issues": "write", "organization_projects": "write", "metadata": "read"}, "fullsend": {"actions": "write", "actions_variables": "read", "contents": "write", "pull_requests": "write", "workflows": "write", "metadata": "read"}, + "e2e": { + "actions": "write", "actions_variables": "read", "administration": "write", + "contents": "write", "issues": "write", "members": "write", "metadata": "read", + "organization_administration": "write", "pull_requests": "write", + "secrets": "write", "workflows": "write", + }, } // RolePermissions returns a deep copy of the role-to-permissions map, diff --git a/internal/mintcore/github_test.go b/internal/mintcore/github_test.go index 81e79cc2fa..c03b7834c4 100644 --- a/internal/mintcore/github_test.go +++ b/internal/mintcore/github_test.go @@ -104,7 +104,7 @@ func TestCreateInstallationToken_UnknownRole(t *testing.T) { } func TestRolePermissions_AllRolesPresent(t *testing.T) { - expectedRoles := []string{"triage", "coder", "review", "fix", "retro", "prioritize", "fullsend"} + expectedRoles := []string{"triage", "coder", "review", "fix", "retro", "prioritize", "fullsend", "e2e"} allPerms := RolePermissions() for _, role := range expectedRoles { perms, ok := allPerms[role] From 2509f5e815fd5694eeb3494734482bc8ca42e9c8 Mon Sep 17 00:00:00 2001 From: Barak Korren Date: Thu, 18 Jun 2026 11:05:33 +0300 Subject: [PATCH 2/6] test(config): expect e2e in ValidRoles Update TestValidRoles for the eighth role added for pool e2e minting. Signed-off-by: Barak Korren Co-authored-by: Cursor --- internal/config/config_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index a9ce98b57c..a204c60c03 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -10,7 +10,7 @@ import ( func TestValidRoles(t *testing.T) { roles := ValidRoles() - assert.Len(t, roles, 7) + assert.Len(t, roles, 8) assert.Contains(t, roles, "fullsend") assert.Contains(t, roles, "triage") assert.Contains(t, roles, "coder") @@ -18,6 +18,7 @@ func TestValidRoles(t *testing.T) { assert.Contains(t, roles, "fix") assert.Contains(t, roles, "retro") assert.Contains(t, roles, "prioritize") + assert.Contains(t, roles, "e2e") } func TestPerRepoDefaultRoles(t *testing.T) { From 8ee2f1ec950ecf88d2093e52fb0bfedb3243c321 Mon Sep 17 00:00:00 2001 From: Barak Korren Date: Thu, 18 Jun 2026 11:53:35 +0300 Subject: [PATCH 3/6] fix(forge): align e2e app manifest with mint role permissions Add AgentAppConfig case and AppPermissions fields so mint add-role --org creates an e2e GitHub App matching canonicalRolePermissions. Signed-off-by: Barak Korren Co-authored-by: Cursor --- internal/forge/github/types.go | 25 ++++++++++++++++++++++--- internal/forge/github/types_test.go | 17 +++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/internal/forge/github/types.go b/internal/forge/github/types.go index 6d0354935c..881d8f6d76 100644 --- a/internal/forge/github/types.go +++ b/internal/forge/github/types.go @@ -11,9 +11,11 @@ type AppPermissions struct { Contents string `json:"contents,omitempty"` Variables string `json:"actions_variables,omitempty"` Workflows string `json:"workflows,omitempty"` - Administration string `json:"administration,omitempty"` - Members string `json:"members,omitempty"` - OrganizationProjects string `json:"organization_projects,omitempty"` + Administration string `json:"administration,omitempty"` + Members string `json:"members,omitempty"` + OrganizationProjects string `json:"organization_projects,omitempty"` + OrganizationAdministration string `json:"organization_administration,omitempty"` + Secrets string `json:"secrets,omitempty"` } // HookAttributes configures the webhook for a GitHub App. @@ -139,6 +141,23 @@ func AgentAppConfig(org, role, appSet string) AppConfig { // No webhook events — triggered via workflow_dispatch from other agents. base.Events = []string{} + case "e2e": + base.Description = fmt.Sprintf("Fullsend e2e pool testing for %s", org) + base.Permissions = AppPermissions{ + Actions: "write", + Variables: "read", + Administration: "write", + Contents: "write", + Issues: "write", + Members: "write", + OrganizationAdministration: "write", + PullRequests: "write", + Secrets: "write", + Workflows: "write", + } + // Pool tests are API/mint driven; no webhook events required. + base.Events = []string{} + default: base.Description = fmt.Sprintf("Fullsend %s agent for %s", role, org) base.Permissions = AppPermissions{ diff --git a/internal/forge/github/types_test.go b/internal/forge/github/types_test.go index 097191002e..48b13e4df7 100644 --- a/internal/forge/github/types_test.go +++ b/internal/forge/github/types_test.go @@ -102,6 +102,23 @@ func TestAgentAppConfig_Retro(t *testing.T) { assert.Empty(t, cfg.Events) } +func TestAgentAppConfig_E2e(t *testing.T) { + cfg := AgentAppConfig("myorg", "e2e", "fullsend-ai") + + assert.Equal(t, "fullsend-ai-e2e", cfg.Name) + assert.Equal(t, "write", cfg.Permissions.Actions) + assert.Equal(t, "read", cfg.Permissions.Variables) + assert.Equal(t, "write", cfg.Permissions.Administration) + assert.Equal(t, "write", cfg.Permissions.Contents) + assert.Equal(t, "write", cfg.Permissions.Issues) + assert.Equal(t, "write", cfg.Permissions.Members) + assert.Equal(t, "write", cfg.Permissions.OrganizationAdministration) + assert.Equal(t, "write", cfg.Permissions.PullRequests) + assert.Equal(t, "write", cfg.Permissions.Secrets) + assert.Equal(t, "write", cfg.Permissions.Workflows) + assert.Empty(t, cfg.Events) +} + func TestAgentAppConfig_UnknownRole(t *testing.T) { cfg := AgentAppConfig("myorg", "custom-bot", "fullsend") From 8c2ff275b72f6775d58ed3eec422212b6b95d400 Mon Sep 17 00:00:00 2001 From: Barak Korren Date: Thu, 18 Jun 2026 11:57:13 +0300 Subject: [PATCH 4/6] chore(forge): gofmt AppPermissions struct alignment Signed-off-by: Barak Korren Co-authored-by: Cursor --- internal/forge/github/types.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/forge/github/types.go b/internal/forge/github/types.go index 881d8f6d76..0bf9caf1d0 100644 --- a/internal/forge/github/types.go +++ b/internal/forge/github/types.go @@ -4,13 +4,13 @@ import "fmt" // AppPermissions defines the permissions for a GitHub App. type AppPermissions struct { - Actions string `json:"actions,omitempty"` - Issues string `json:"issues,omitempty"` - PullRequests string `json:"pull_requests,omitempty"` - Checks string `json:"checks,omitempty"` - Contents string `json:"contents,omitempty"` - Variables string `json:"actions_variables,omitempty"` - Workflows string `json:"workflows,omitempty"` + Actions string `json:"actions,omitempty"` + Issues string `json:"issues,omitempty"` + PullRequests string `json:"pull_requests,omitempty"` + Checks string `json:"checks,omitempty"` + Contents string `json:"contents,omitempty"` + Variables string `json:"actions_variables,omitempty"` + Workflows string `json:"workflows,omitempty"` Administration string `json:"administration,omitempty"` Members string `json:"members,omitempty"` OrganizationProjects string `json:"organization_projects,omitempty"` From aa108a2131820f872d4536dfe0767d5ecf62ac52 Mon Sep 17 00:00:00 2001 From: Barak Korren Date: Fri, 19 Jun 2026 00:52:02 +0300 Subject: [PATCH 5/6] test: add e2e role drift guards across mintcore and forge Assert canonical e2e permissions, ValidRoles/mintcore alignment, and AgentAppConfig parity with mintcore. Tracks consolidation in #2449. Signed-off-by: Barak Korren Co-authored-by: Cursor --- internal/config/config_test.go | 9 +++++ internal/forge/github/types_test.go | 61 +++++++++++++++++++++++++++++ internal/mintcore/github_test.go | 16 ++++++++ 3 files changed, 86 insertions(+) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index a204c60c03..71625a4bcf 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -6,6 +6,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/fullsend-ai/fullsend/internal/mintcore" ) func TestValidRoles(t *testing.T) { @@ -21,6 +23,13 @@ func TestValidRoles(t *testing.T) { assert.Contains(t, roles, "e2e") } +func TestValidRoles_RecognizedByMintcore(t *testing.T) { + for _, role := range ValidRoles() { + assert.True(t, mintcore.HasRole(role), + "ValidRoles() contains %q but mintcore.HasRole is false — role lists may have drifted (see issue tracking consolidation)", role) + } +} + func TestPerRepoDefaultRoles(t *testing.T) { roles := PerRepoDefaultRoles() assert.Len(t, roles, 6) diff --git a/internal/forge/github/types_test.go b/internal/forge/github/types_test.go index 48b13e4df7..4d0a2a0bf7 100644 --- a/internal/forge/github/types_test.go +++ b/internal/forge/github/types_test.go @@ -6,6 +6,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/fullsend-ai/fullsend/internal/mintcore" ) func TestDefaultAgentRoles(t *testing.T) { @@ -119,6 +121,65 @@ func TestAgentAppConfig_E2e(t *testing.T) { assert.Empty(t, cfg.Events) } +// appPermissionsAsMap converts manifest permissions to GitHub API permission names. +func appPermissionsAsMap(p AppPermissions) map[string]string { + out := make(map[string]string) + if p.Actions != "" { + out["actions"] = p.Actions + } + if p.Issues != "" { + out["issues"] = p.Issues + } + if p.PullRequests != "" { + out["pull_requests"] = p.PullRequests + } + if p.Checks != "" { + out["checks"] = p.Checks + } + if p.Contents != "" { + out["contents"] = p.Contents + } + if p.Variables != "" { + out["actions_variables"] = p.Variables + } + if p.Workflows != "" { + out["workflows"] = p.Workflows + } + if p.Administration != "" { + out["administration"] = p.Administration + } + if p.Members != "" { + out["members"] = p.Members + } + if p.OrganizationProjects != "" { + out["organization_projects"] = p.OrganizationProjects + } + if p.OrganizationAdministration != "" { + out["organization_administration"] = p.OrganizationAdministration + } + if p.Secrets != "" { + out["secrets"] = p.Secrets + } + return out +} + +func TestAgentAppConfig_E2eMatchesMintcorePermissions(t *testing.T) { + canonical := mintcore.RolePermissionsFor("e2e") + require.NotNil(t, canonical) + + manifest := appPermissionsAsMap(AgentAppConfig("myorg", "e2e", "fullsend-ai").Permissions) + + // metadata is added at mint token time; GitHub App manifests omit it explicitly. + for key, want := range canonical { + if key == "metadata" { + continue + } + got, ok := manifest[key] + assert.True(t, ok, "AgentAppConfig(e2e) missing permission %q from mintcore canonicalRolePermissions", key) + assert.Equal(t, want, got, "permission %q mismatch between AgentAppConfig and mintcore", key) + } +} + func TestAgentAppConfig_UnknownRole(t *testing.T) { cfg := AgentAppConfig("myorg", "custom-bot", "fullsend") diff --git a/internal/mintcore/github_test.go b/internal/mintcore/github_test.go index c03b7834c4..ce3339d486 100644 --- a/internal/mintcore/github_test.go +++ b/internal/mintcore/github_test.go @@ -115,6 +115,22 @@ func TestRolePermissions_AllRolesPresent(t *testing.T) { } } +func TestRolePermissions_E2e(t *testing.T) { + perms := RolePermissionsFor("e2e") + require.NotNil(t, perms) + assert.Equal(t, "write", perms["actions"]) + assert.Equal(t, "read", perms["actions_variables"]) + assert.Equal(t, "write", perms["administration"]) + assert.Equal(t, "write", perms["contents"]) + assert.Equal(t, "write", perms["issues"]) + assert.Equal(t, "write", perms["members"]) + assert.Equal(t, "read", perms["metadata"]) + assert.Equal(t, "write", perms["organization_administration"]) + assert.Equal(t, "write", perms["pull_requests"]) + assert.Equal(t, "write", perms["secrets"]) + assert.Equal(t, "write", perms["workflows"]) +} + func TestRolePermissions_ReturnsCopy(t *testing.T) { // Mutating the returned map must not affect the canonical definitions. perms := RolePermissions() From 01c65cf93724686407fb31041e30e1c9b46ed77b Mon Sep 17 00:00:00 2001 From: Barak Korren Date: Sun, 21 Jun 2026 09:26:33 +0300 Subject: [PATCH 6/6] fix(layers): skip harness wrapper for e2e mint role e2e is a pool/CI mint role, not an installed agent app. Treat it like fullsend so harness generation does not look for harness/e2e.yaml. Signed-off-by: Barak Korren Co-authored-by: Cursor --- internal/layers/harnesswrappers.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/layers/harnesswrappers.go b/internal/layers/harnesswrappers.go index 98ad631ee9..ce82053b67 100644 --- a/internal/layers/harnesswrappers.go +++ b/internal/layers/harnesswrappers.go @@ -53,9 +53,10 @@ func (l *HarnessWrappersLayer) RequiredScopes(op Operation) []string { // harnessesForRole returns the harness filename(s) for a given agent role. // The coder role maps to both code and fix harnesses (fix reuses the coder app). // The fullsend role is the org-level app and has no harness. +// The e2e role is a pool/CI mint role and is not installed as an agent app. func harnessesForRole(role string) []string { switch role { - case "fullsend": + case "fullsend", "e2e": return nil case "coder": return []string{"code", "fix"}