Skip to content

Commit 4c2e750

Browse files
ggallenclaude
andcommitted
refactor: extract mintcore shared library from GCP mint
Extract shared types, validation, and GitHub API helpers from the monolithic mint Cloud Function into a reusable mintcore module. The GCP mint now imports mintcore for claims validation, JWT generation, and installation token creation. Signed-off-by: Greg Allen <gallen@redhat.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3fdaec0 commit 4c2e750

40 files changed

Lines changed: 4168 additions & 2105 deletions

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ Fullsend is a platform for fully autonomous agentic development for GitHub-hoste
2222

2323
When changing `internal/mint/main.go`, always copy it to `internal/dispatch/gcf/mintsrc/main.go.embed`. If `go.mod` or `go.sum` changed, sync those to `go.mod.embed` and `go.sum.embed` too.
2424

25+
The `internal/mintcore/` module is shared between the mint and devmint. Its files are also embedded for Cloud Function deployment at `internal/dispatch/gcf/mintsrc/mintcore/*.embed`. When changing any file in `internal/mintcore/`, sync it to the corresponding `.embed` file under `mintsrc/mintcore/`. Note: the mint's `go.mod.embed` uses `replace mintcore => ./mintcore` (not `../mintcore`), because `provisioner.go` rewrites the replace directive at bundle time to match the deployed directory layout.
26+
2527
**Forge abstraction:** All git forge operations must go through the `forge.Client` interface in `internal/forge/forge.go`. Do not use `exec.Command("gh", ...)` or direct GitHub API calls outside `internal/forge/github/`. See [AGENTS.md](AGENTS.md#forge-abstraction) for details.
2628

2729
When making changes to Go code under `cmd/` or `internal/`:

docs/guides/infrastructure/infrastructure-reference.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ The mint is a GCP Cloud Function that exchanges GitHub OIDC tokens for scoped Gi
6363
│ │ 6. Create Scoped Installation Token │ │
6464
│ │ ├─ POST /installations/{id}/access_tokens │ │
6565
│ │ ├─ Scope to requested repos[] │ │
66-
│ │ └─ Apply rolePermissions minimum set │ │
66+
│ │ └─ Apply RolePermissions() minimum set │ │
6767
│ │ │ │
6868
│ └──────────┬───────────────────────────────────────┘ │
6969
│ │ │
@@ -102,6 +102,19 @@ A single mint instance can serve multiple orgs:
102102
- `ROLE_APP_IDS` maps `{org}/{role}` to GitHub App IDs
103103
- Updates are applied atomically by redeploying the function with updated env vars
104104

105+
### Status Endpoint
106+
107+
`GET /v1/status` returns the configured roles available for the authenticated caller's org.
108+
109+
- **Authentication:** Bearer OIDC JWT (same as `/v1/token`)
110+
- **Authorization:** Any valid OIDC token from an allowed org — no role restriction
111+
- **Response:**
112+
```json
113+
{"org": "my-org", "roles": ["coder", "review", "triage"]}
114+
```
115+
- **Use case:** Workflow diagnostics — discover which roles are available before requesting a token
116+
- **Security:** Returns only the requesting org and its role names (not app IDs, not other orgs' roles)
117+
105118
---
106119

107120
## Inference — Agent Platform with Workload Identity Federation

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ require (
1111
github.com/spf13/cobra v1.10.2
1212
github.com/stretchr/testify v1.11.1
1313
golang.org/x/crypto v0.50.0
14-
golang.org/x/net v0.52.0
1514
golang.org/x/oauth2 v0.36.0
1615
golang.org/x/term v0.42.0
1716
golang.org/x/text v0.36.0
@@ -32,6 +31,7 @@ require (
3231
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
3332
github.com/deckarep/golang-set/v2 v2.8.0 // indirect
3433
github.com/dustin/go-humanize v1.0.1 // indirect
34+
github.com/fullsend-ai/fullsend/internal/mintcore v0.0.0
3535
github.com/go-errors/errors v1.5.1 // indirect
3636
github.com/go-jose/go-jose/v3 v3.0.5 // indirect
3737
github.com/go-logr/logr v1.4.3 // indirect
@@ -63,3 +63,5 @@ require (
6363
google.golang.org/protobuf v1.36.11 // indirect
6464
k8s.io/klog/v2 v2.140.0 // indirect
6565
)
66+
67+
replace github.com/fullsend-ai/fullsend/internal/mintcore => ./internal/mintcore

go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
144144
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
145145
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
146146
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
147-
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
148-
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
149147
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
150148
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
151149
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

internal/cli/admin.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/fullsend-ai/fullsend/internal/config"
2323
"github.com/fullsend-ai/fullsend/internal/dispatch"
2424
"github.com/fullsend-ai/fullsend/internal/dispatch/gcf"
25+
"github.com/fullsend-ai/fullsend/internal/mintcore"
2526
"github.com/fullsend-ai/fullsend/internal/forge"
2627
gh "github.com/fullsend-ai/fullsend/internal/forge/github"
2728
"github.com/fullsend-ai/fullsend/internal/inference"
@@ -792,7 +793,7 @@ func runPerRepoInstall(ctx context.Context, c perRepoInstallConfig) error {
792793
printer.StepInfo("Would provision WIF infrastructure in GCP project " + inferenceProject)
793794
printer.StepInfo(fmt.Sprintf(" Service account: fullsend-mint@%s.iam.gserviceaccount.com", inferenceProject))
794795
printer.StepInfo(" WIF pool: " + gcf.DefaultInferencePool)
795-
printer.StepInfo(fmt.Sprintf(" WIF provider: %s", gcf.BuildRepoProviderID(owner, repo)))
796+
printer.StepInfo(fmt.Sprintf(" WIF provider: %s", mintcore.BuildRepoProviderID(owner, repo)))
796797
printer.StepInfo(fmt.Sprintf(" Repo restriction: %s/%s", owner, repo))
797798
printer.Blank()
798799
}

internal/cli/inference.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/spf13/cobra"
1010

1111
"github.com/fullsend-ai/fullsend/internal/dispatch/gcf"
12+
"github.com/fullsend-ai/fullsend/internal/mintcore"
1213
"github.com/fullsend-ai/fullsend/internal/ui"
1314
)
1415

@@ -135,7 +136,7 @@ func runInferenceProvisionDryRun(cmd *cobra.Command, printer *ui.Printer, org, r
135136
printer.Blank()
136137
printer.StepInfo(fmt.Sprintf("Repository: %s", repo))
137138
parts := strings.SplitN(repo, "/", 2)
138-
providerID := gcf.BuildRepoProviderID(parts[0], parts[1])
139+
providerID := mintcore.BuildRepoProviderID(parts[0], parts[1])
139140
printer.StepInfo(fmt.Sprintf("WIF provider: %s (repo-scoped)", providerID))
140141
printer.StepInfo(fmt.Sprintf("Condition: assertion.repository == '%s'", strings.ToLower(repo)))
141142
} else {
@@ -285,7 +286,7 @@ func runInferenceStatus(cmd *cobra.Command, org, repo, project, pool, provider,
285286
providerName := provider
286287
if repo != "" {
287288
parts := strings.SplitN(repo, "/", 2)
288-
providerName = gcf.BuildRepoProviderID(parts[0], parts[1])
289+
providerName = mintcore.BuildRepoProviderID(parts[0], parts[1])
289290
}
290291

291292
result := &inferenceStatusResult{
@@ -505,7 +506,7 @@ func runInferenceDeprovisionDryRun(printer *ui.Printer, org, repo, project, pool
505506
printer.Header("Dry run: deprovision repo " + repo + " from inference")
506507
printer.Blank()
507508
parts := strings.SplitN(repo, "/", 2)
508-
providerID := gcf.BuildRepoProviderID(parts[0], parts[1])
509+
providerID := mintcore.BuildRepoProviderID(parts[0], parts[1])
509510
printer.StepInfo(fmt.Sprintf("Repository: %s", repo))
510511
printer.StepInfo(fmt.Sprintf("GCP project: %s", project))
511512
printer.StepInfo(fmt.Sprintf("WIF pool: %s", pool))
@@ -543,7 +544,7 @@ func runInferenceDeprovision(cmd *cobra.Command, printer *ui.Printer, org, repo,
543544
printer.Blank()
544545

545546
parts := strings.SplitN(repo, "/", 2)
546-
providerID := gcf.BuildRepoProviderID(parts[0], parts[1])
547+
providerID := mintcore.BuildRepoProviderID(parts[0], parts[1])
547548

548549
provisioner := gcf.NewProvisioner(gcf.Config{
549550
ProjectID: project,

internal/cli/mint.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/fullsend-ai/fullsend/internal/appsetup"
2929
"github.com/fullsend-ai/fullsend/internal/config"
3030
"github.com/fullsend-ai/fullsend/internal/dispatch/gcf"
31+
"github.com/fullsend-ai/fullsend/internal/mintcore"
3132
"github.com/fullsend-ai/fullsend/internal/ui"
3233
)
3334

@@ -697,7 +698,7 @@ func runMintEnrollRepo(ctx context.Context, printer *ui.Printer, repoFullName, p
697698
printer.StepInfo(fmt.Sprintf(" Would add %s to ALLOWED_ORGS", owner))
698699
printer.StepInfo(fmt.Sprintf(" Would copy PEMs from %s for %d roles", appSet, len(roleList)))
699700
printer.StepInfo(fmt.Sprintf(" Would add %s to PER_REPO_WIF_REPOS", repoFullName))
700-
printer.StepInfo(fmt.Sprintf(" Would create WIF provider: %s", gcf.BuildRepoProviderID(owner, repo)))
701+
printer.StepInfo(fmt.Sprintf(" Would create WIF provider: %s", mintcore.BuildRepoProviderID(owner, repo)))
701702
return nil
702703
}
703704

@@ -1078,7 +1079,7 @@ func runMintUnenrollRepo(ctx context.Context, printer *ui.Printer, repoFullName,
10781079
printer.StepDone("Mint verified")
10791080

10801081
if dryRun {
1081-
providerID := gcf.BuildRepoProviderID(owner, repo)
1082+
providerID := mintcore.BuildRepoProviderID(owner, repo)
10821083
printer.Blank()
10831084
printer.StepInfo("Dry run — no changes will be made")
10841085
printer.Blank()
@@ -1110,7 +1111,7 @@ func runMintUnenrollRepo(ctx context.Context, printer *ui.Printer, repoFullName,
11101111
printer.StepDone("Repo removed from PER_REPO_WIF_REPOS")
11111112

11121113
// Step 2: Disable or delete WIF provider.
1113-
providerID := gcf.BuildRepoProviderID(owner, repo)
1114+
providerID := mintcore.BuildRepoProviderID(owner, repo)
11141115
if deleteProvider {
11151116
printer.StepStart("Deleting WIF provider " + providerID)
11161117
if err := provisioner.DeleteWIFProvider(ctx, providerID); err != nil {

internal/dispatch/gcf/mintsrc/go.mod.embed

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ module github.com/fullsend-ai/fullsend/internal/mint
22

33
go 1.26
44

5-
require github.com/GoogleCloudPlatform/functions-framework-go v1.9.0
5+
require (
6+
github.com/GoogleCloudPlatform/functions-framework-go v1.9.0
7+
github.com/fullsend-ai/fullsend/internal/mintcore v0.0.0
8+
)
69

710
require (
811
github.com/cloudevents/sdk-go/v2 v2.15.2 // indirect
@@ -13,4 +16,7 @@ require (
1316
go.uber.org/atomic v1.4.0 // indirect
1417
go.uber.org/multierr v1.1.0 // indirect
1518
go.uber.org/zap v1.10.0 // indirect
19+
golang.org/x/sync v0.20.0 // indirect
1620
)
21+
22+
replace github.com/fullsend-ai/fullsend/internal/mintcore => ./mintcore

internal/dispatch/gcf/mintsrc/go.sum.embed

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
2222
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
2323
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
2424
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
25-
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
26-
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
25+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
26+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
2727
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
2828
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
2929
go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
@@ -32,6 +32,8 @@ go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
3232
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
3333
go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
3434
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
35+
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
36+
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
3537
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs=
3638
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
3739
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

0 commit comments

Comments
 (0)