Skip to content

Commit 4309f74

Browse files
ggallenclaude
andcommitted
feat: extract mintcore shared library with JWKSVerifier and status endpoint
Extract shared functionality from internal/mint into internal/mintcore module for reuse by both the mint and devmint Cloud Functions. Includes JWKSVerifier, OIDC token handling, STS exchange, GitHub API helpers, claims parsing, and the common handler framework. Update provisioner to bundle mintcore alongside mint source, rewriting go.mod replace directives for the deployed directory layout. Add embed copies at internal/dispatch/gcf/mintsrc/mintcore/*.embed. Move all mint tests to mintcore since the logic now lives there. Update CLI commands (admin, inference, mint, run) to use mintcore.BuildRepoProviderID. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3fdaec0 commit 4309f74

50 files changed

Lines changed: 5774 additions & 3357 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

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/`:

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ This is not a product spec. It's an evolving exploration of a hard problem space
4242
- [konflux-ci](docs/problems/applied/konflux-ci/) — Kubernetes-native CI/CD platform (the original proving ground)
4343
- **[docs/plans/](docs/plans/)** — Implementation plans for accepted or in-progress designs:
4444
- [Universal Harness Access](docs/plans/universal-harness-access.md) — Making harnesses and agents universally accessible via URLs and paths, enabling community sharing and composability
45-
- [Universal Harness Access — Implementation](docs/plans/universal-harness-access-implementation.md) — Phased PR breakdown for ADR-0038 Phase 1 (MVP)
45+
- [Universal Harness Access — Phase 1 Implementation](docs/plans/universal-harness-access-phase1.md) — Phased PR breakdown for ADR-0038 Phase 1 (MVP)
46+
- [Universal Harness Access — Phase 2 Implementation](docs/plans/universal-harness-access-phase2.md) — Phased PR breakdown for ADR-0038 Phase 2 (transitive dependency resolution)
4647
- [Agent Execution Environment](docs/plans/agent-execution-environment.md) — Sandbox and runtime environment for agent execution
4748
- [Vertex AI Inference Provisioning](docs/plans/vertex-inference-provisioning.md) — Provisioning and configuration for Vertex AI inference endpoints
4849
- [ADR-0046 Drift Scanner](docs/plans/2026-03-06-adr46-drift-scanner.md) — Implementation plan for ADR-0046 drift detection tool

docs/guides/dev/cli-internals.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ Both per-org and per-repo modes share the same core pipeline. The code follows t
132132
│ │ Both modes: ProvisionWIF() → create pool, provider, IAM │ │
133133
│ │ ┌──────────────────────────────────────────┐ │ │
134134
│ │ │ Per-org: org-wide WIF provider │ │ │
135-
│ │ │ Per-repo: repo-scoped (BuildRepoProviderID)│ │ │
135+
│ │ │ Per-repo: repo-scoped (mintcore.BuildRepoProviderID)│ │ │
136136
│ │ └──────────────────────────────────────────┘ │ │
137137
│ └──────────┬─────────────────────────────────────────────────┘ │
138138
│ ▼ │
@@ -191,7 +191,7 @@ Both modes call the same functions (`runAppSetup`, `gcf.NewProvisioner`, `Provis
191191
| **1. Discover** | `DiscoverMint()`, resolve app IDs | Discovers all org repos | Single repo validation |
192192
| **2. App setup** | `runAppSetup()` → PEMs + App IDs | All 7 roles by default | Excludes "fullsend" role |
193193
| **3. Mint** | `gcf.Provision()` or `EnsureOrgInMint()` || + `RegisterPerRepoWIF()` |
194-
| **4. WIF** | `ProvisionWIF()` | Org-wide provider ID | `BuildRepoProviderID()` (repo-scoped) |
194+
| **4. WIF** | `ProvisionWIF()` | Org-wide provider ID | `mintcore.BuildRepoProviderID()` (repo-scoped) |
195195
| **5. Scaffold** | `scaffold.PerRepoCustomizedDirs()` / `WalkFullsendRepo()` | Creates `.fullsend` repo, pushes workflows + optional binary | Writes `.fullsend/` dir + shim workflow + optional binary in target repo |
196196
| **6. Secrets** | Same secret names, same API calls | Config repo + org variable | Target repo + `PER_REPO_GUARD` |
197197
| **7. Enrollment** || `EnrollmentLayer` enables repos | No-op (self-contained) |

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/cli/run.go

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,11 @@ import (
2424

2525
"github.com/spf13/cobra"
2626

27+
"github.com/fullsend-ai/fullsend/internal/config"
2728
"github.com/fullsend-ai/fullsend/internal/envfile"
29+
"github.com/fullsend-ai/fullsend/internal/fetch"
2830
"github.com/fullsend-ai/fullsend/internal/harness"
31+
"github.com/fullsend-ai/fullsend/internal/resolve"
2932
"github.com/fullsend-ai/fullsend/internal/sandbox"
3033
"github.com/fullsend-ai/fullsend/internal/scaffold"
3134
"github.com/fullsend-ai/fullsend/internal/security"
@@ -49,6 +52,7 @@ func newRunCmd() *cobra.Command {
4952
var envFiles []string
5053
var noPostScript bool
5154
var debugFilter string
55+
var offline bool
5256

5357
cmd := &cobra.Command{
5458
Use: "run <agent-name>",
@@ -58,7 +62,7 @@ func newRunCmd() *cobra.Command {
5862
RunE: func(cmd *cobra.Command, args []string) error {
5963
agentName := args[0]
6064
printer := ui.New(os.Stdout)
61-
return runAgent(agentName, fullsendDir, outputBase, targetRepo, fullsendBinary, envFiles, noPostScript, debugFilter, printer)
65+
return runAgent(cmd.Context(), agentName, fullsendDir, outputBase, targetRepo, fullsendBinary, envFiles, noPostScript, debugFilter, offline, printer)
6266
},
6367
}
6468

@@ -70,13 +74,14 @@ func newRunCmd() *cobra.Command {
7074
cmd.Flags().BoolVar(&noPostScript, "no-post-script", false, "skip post-script execution (agent still runs full inference)")
7175
cmd.Flags().StringVar(&debugFilter, "debug", "", `enable Claude Code debug logging with optional category filter (e.g. "api,hooks")`)
7276
cmd.Flags().Lookup("debug").NoOptDefVal = "*"
77+
cmd.Flags().BoolVar(&offline, "offline", false, "reject network fetches; only use cached remote resources")
7378
_ = cmd.MarkFlagRequired("fullsend-dir")
7479
_ = cmd.MarkFlagRequired("target-repo")
7580

7681
return cmd
7782
}
7883

79-
func runAgent(agentName, fullsendDir, outputBase, targetRepo, fullsendBinary string, envFiles []string, noPostScript bool, debug string, printer *ui.Printer) (runErr error) {
84+
func runAgent(ctx context.Context, agentName, fullsendDir, outputBase, targetRepo, fullsendBinary string, envFiles []string, noPostScript bool, debug string, offline bool, printer *ui.Printer) (runErr error) {
8085
printer.Banner(Version())
8186
printer.Blank()
8287
printer.Header("Running agent: " + agentName)
@@ -109,6 +114,49 @@ func runAgent(agentName, fullsendDir, outputBase, targetRepo, fullsendBinary str
109114
return fmt.Errorf("resolving paths: %w", err)
110115
}
111116

117+
if h.HasURLReferences() {
118+
orgConfigPath := filepath.Join(absFullsendDir, "config.yaml")
119+
orgConfigData, err := os.ReadFile(orgConfigPath)
120+
if err != nil {
121+
printer.StepFail("Failed to load org config")
122+
if os.IsNotExist(err) {
123+
return fmt.Errorf("URL-referenced resources require an org-level config.yaml with allowed_remote_resources (expected at %s)", orgConfigPath)
124+
}
125+
return fmt.Errorf("reading org config for remote resource validation: %w", err)
126+
}
127+
orgCfg, err := config.ParseOrgConfig(orgConfigData)
128+
if err != nil {
129+
printer.StepFail("Failed to parse org config")
130+
return fmt.Errorf("parsing org config: %w", err)
131+
}
132+
133+
if err := h.ValidateAllowedRemoteResources(orgCfg.AllowedRemoteResources); err != nil {
134+
printer.StepFail("Remote resource allowlist validation failed")
135+
return fmt.Errorf("validating allowed remote resources: %w", err)
136+
}
137+
138+
policy := fetch.DefaultPolicy
139+
policy.Offline = offline
140+
141+
deps, err := resolve.ResolveHarness(ctx, h, resolve.ResolveOpts{
142+
WorkspaceRoot: absFullsendDir,
143+
FetchPolicy: policy,
144+
AuditLogPath: filepath.Join(absFullsendDir, ".fullsend-cache", "fetch-audit.jsonl"),
145+
})
146+
if err != nil {
147+
printer.StepFail("Remote resource resolution failed")
148+
return fmt.Errorf("resolving remote resources: %w", err)
149+
}
150+
151+
for _, dep := range deps {
152+
if dep.CacheHit {
153+
printer.StepInfo(fmt.Sprintf("Resolved %s (cache hit)", dep.URL))
154+
} else {
155+
printer.StepInfo(fmt.Sprintf("Fetched %s -> %s", dep.URL, dep.LocalPath))
156+
}
157+
}
158+
}
159+
112160
if resolved, overridden := applySandboxImageOverride(h.Image); overridden {
113161
printer.StepInfo(fmt.Sprintf("Image override via FULLSEND_SANDBOX_IMAGE: %s -> %s", h.Image, resolved))
114162
h.Image = resolved

0 commit comments

Comments
 (0)