Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/cli/repos.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ fullsend repos set-default forge.github.mint_url "" # removes the key
|-----|------|-------------|
| `defaults.allowed_remote_resources` | comma-separated URLs | HTTPS URLs agents may fetch at runtime |
| `forge.github.url` | URL | GitHub instance URL (default: `https://github.com`) |
| `forge.github.mint_url` | URL | Cloud Run endpoint URL for the token mint (defaults to `https://mint.fullsend.sh` in public mode) |
| `forge.github.mint_url` | URL | Token mint service URL (defaults to `https://mint.fullsend.sh` in public mode) |
| `forge.github.mint_mode` | `public` or `private` | Controls the default mint URL: `public` defaults to `https://mint.fullsend.sh`; `private` requires an explicit `mint_url` (default: `public`) |
| `forge.github.fullsend_ref` | ref string | Git ref to pin in scaffold workflow YAML |
| `forge.gitlab.url` | URL | GitLab instance URL |
Expand Down
20 changes: 3 additions & 17 deletions docs/guides/getting-started/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,6 @@ sidebar_position: 1
This section contains easy and to the point guides to help you
set up Fullsend. These are intended to be read in a certain order:

1. **Mint enrollment** — before configuring anything, your org or repo
must be enrolled in a fullsend token mint service so the mint
accepts token requests from your GitHub Actions workflows.
The CLI defaults to the hosted mint. To enroll, contact the
fullsend team in the internal Slack channel with your GitHub org
name (for org mode) or `owner/repo` (for per-repo mode). To deploy
and manage your own mint instead, see the
[Mint administration](../infrastructure/mint-administration.md) guide.

> **Note:** Self-service enrollment is not yet available.
> [Public mint mode](https://github.com/fullsend-ai/fullsend/pull/1580)
> will remove the need for per-org/repo enrollment, but is still
> in progress.

2. [Getting Inference](getting-inference.md)
3. [Configuring GitHub](configuring-github.md)
4. [Organization Mode](org-mode.md)
1. [Getting Inference](getting-inference.md)
2. [Configuring GitHub](configuring-github.md)
3. [Organization Mode](org-mode.md)
1 change: 0 additions & 1 deletion docs/guides/getting-started/configuring-github.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ The goal of this document is that you configure Fullsend for your GitHub reposit

## Prerequisites

* Your org or repo is enrolled in a fullsend token mint service (see [Getting Started](README.md) step 1).
* You have your WIF provider URL from [Getting Inference](getting-inference.md).
* Download the latest [fullsend](https://github.com/fullsend-ai/fullsend/releases) CLI.
* Download the latest [gh](https://cli.github.com/) CLI and authenticate with it.
Expand Down
6 changes: 3 additions & 3 deletions docs/guides/getting-started/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ To remove fullsend from a single repository:

1. Delete `.github/workflows/fullsend.yaml` and repo-level secrets/variables
2. Run `fullsend inference deprovision "$OWNER/$REPO"` to remove WIF access
3. Contact the fullsend team to unenroll the repo from the hosted mint
3. Remove the `FULLSEND_MINT_URL` repository variable (if set) — no separate unenrollment is needed for the hosted community mint

**GitLab repos:**

Expand All @@ -78,7 +78,7 @@ To remove fullsend from a single repository:
3. Revoke the `fullsend-bot` project access token (Settings → Access Tokens)
4. Delete fullsend pipeline schedules (`fullsend slash poll` and `fullsend event poll`)

If you manage your own self-hosted mint, run `fullsend mint unenroll "$OWNER/$REPO"` instead of GitHub step 3. See the [standalone commands](#standalone-commands) table for details.
If you manage your own self-hosted mint, run `fullsend mint unenroll "$OWNER/$REPO"` to remove the repo from the mint's allowlist. See the [standalone commands](#standalone-commands) table for details.

## Standalone commands

Expand Down Expand Up @@ -115,7 +115,7 @@ For organizations that separate GCP and GitHub responsibilities across teams, fu
| Developer | `fullsend agent update <name> [sha]` | Re-pin a URL agent to a new commit SHA |
| Developer | `fullsend agent remove <name>` | Unregister an agent from config |

The typical handoff: a GCP admin runs `mint deploy` + `mint enroll` + `inference provision`, then passes the mint URL and WIF provider resource name to a GitHub maintainer who runs `github setup --mint-url=... --inference-wif-provider=...`.
The typical handoff for self-managed mints: a GCP admin runs `mint deploy` + `mint enroll` + `inference provision`, then passes the mint URL and WIF provider resource name to a GitHub maintainer who runs `github setup --mint-url=... --inference-wif-provider=...`. For the hosted community mint, enrollment is automatic — install the shared Apps and use the CLI defaults.

### Per-command IAM role breakdown

Expand Down
11 changes: 6 additions & 5 deletions docs/guides/getting-started/repo-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ fullsend across an organization. Individual repo owners should use
- **fullsend CLI** installed (see [releases](https://github.com/fullsend-ai/fullsend/releases))
- **GitHub access** — admin or write access to the target repositories
- **`gh` CLI** authenticated with the required OAuth scopes (see [OAuth scope reference](../infrastructure/advanced-setup.md#oauth-scope-reference))
- **GCP prerequisites** — GCP WIF provisioning (`fullsend inference provision`) and mint enrollment (`fullsend mint enroll`, GitHub only) must be completed separately before running `repos install`. When multiple repos share the same GCP project, existing inference secrets are reused automatically. See [Mint administration](../infrastructure/mint-administration.md) and [Advanced setup](../infrastructure/advanced-setup.md).
- **GCP prerequisites** — GCP WIF provisioning (`fullsend inference provision`) must be completed separately before running `repos install`. For self-managed mints, mint enrollment (`fullsend mint enroll`) is also required. The hosted community mint needs no enrollment — install the shared Apps and use the CLI defaults. When multiple repos share the same GCP project, existing inference secrets are reused automatically. See [Mint administration](../infrastructure/mint-administration.md) and [Advanced setup](../infrastructure/advanced-setup.md).

## Getting started

Expand Down Expand Up @@ -139,9 +139,10 @@ Install runs in three phases:
drift (synced automatically) and scaffold ref drift (upgraded
automatically).

> **Prerequisite:** GCP infrastructure (WIF pools/providers, mint
> enrollment) must be provisioned separately before running install.
> See `fullsend inference provision` and `fullsend mint enroll`.
> **Prerequisite:** GCP WIF provisioning (`fullsend inference provision`)
> must be completed before running install. For self-managed mints,
> also run `fullsend mint enroll`. The hosted community mint needs no
> enrollment.

> **Note:** When your token does not have direct push access to a target
> repository, the install command creates a fork and submits the scaffold
Expand Down Expand Up @@ -448,7 +449,7 @@ infrastructure, coordinate between roles:
|------|------|---------|
| 1 | Platform Admin | `fullsend repos uninstall "org/*" --yes` (forge-side cleanup + manifest removal) |
| 2 | GCP Admin (Inference) | `fullsend inference deprovision <org>` (WIF cleanup) |
| 3 | GCP Admin (Mint) | `fullsend mint unenroll <org>` |
| 3 | GCP Admin (Mint) | `fullsend mint unenroll <org>` (self-hosted mints only; not needed for the hosted community mint) |

Each `fullsend` command that prompts for confirmation accepts a skip
flag: `--yes` for `repos` commands, `--yolo` for `github` and `mint`
Expand Down
4 changes: 2 additions & 2 deletions docs/guides/infrastructure/mint-administration.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ This guide covers deploying and managing the fullsend token mint. The mint is th

## Hosted mint

The fullsend team operates a public hosted mint service. If your organization is enrolled, you can use it directly without deploying your own.
The fullsend team operates a public hosted community mint service. Install the shared public GitHub Apps and use the CLI defaults — no separate enrollment step is required.

**Platform GCP project:** The hosted mint currently runs in GCP project `it-gcp-konflux-dev-fullsend` (region `us-central1`).

Expand All @@ -30,7 +30,7 @@ The fullsend team operates a public hosted mint service. If your organization is
https://mint.fullsend.sh
```

Pass this URL as `--mint-url` when running `fullsend github setup`, or set the `FULLSEND_MINT_URL` repository/org variable in GitHub. If you are using the hosted mint, the rest of this guide (deploying, enrolling, troubleshooting) is handled by the fullsend team — you do not need to manage mint infrastructure yourself.
The CLI defaults to this URL. You can also set the `FULLSEND_MINT_URL` repository/org variable in GitHub explicitly. If you are using the hosted community mint, the rest of this guide (deploying, enrolling, troubleshooting) is for platform operators managing self-hosted mints — you do not need to manage mint infrastructure yourself.

## Prerequisites

Expand Down
4 changes: 2 additions & 2 deletions docs/guides/infrastructure/standalone-mint.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ For roles without a local PEM, the optional fallback proxy forwards the request
- **Go 1.26+** to build the binary (or use a pre-built release)
- **A GitHub organization** where you will install your custom GitHub Apps
- **The hosted mint URL** (optional, for fallback proxy): `https://mint.fullsend.sh`
- **Your organization enrolled in the hosted mint** (optional, for fallback proxy) — see [Mint service administration](mint-administration.md)
- **Shared public GitHub Apps installed** (optional, for fallback proxy) — see [Getting Started](../getting-started/)

## Step 1: Create a GitHub App

Expand Down Expand Up @@ -264,7 +264,7 @@ When `FALLBACK_MINT_URL` is set, the standalone mint acts as a transparent proxy
| `GET /health` | Always handled locally |
| `GET /v1/status` | Always handled locally |

The proxy forwards the original OIDC bearer token and request body to the upstream mint, and returns the upstream response verbatim. The upstream mint performs its own OIDC validation — your organization must be enrolled on the upstream mint for proxied requests to succeed.
The proxy forwards the original OIDC bearer token and request body to the upstream mint, and returns the upstream response verbatim. The upstream mint performs its own OIDC validation — the shared public GitHub Apps must be installed in your organization for proxied requests to succeed.

When `FALLBACK_MINT_URL` is not set, requests for roles without local PEMs are rejected with a `403 Forbidden` response.

Expand Down
4 changes: 2 additions & 2 deletions e2e/admin/admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func TestAdminInstallUninstall(t *testing.T) {
"admin", "install", env.org,
"--skip-app-setup",
"--skip-mint-check",
"--mint-url", env.cfg.MintURL,
"--mint-url", e2etest.DefaultPoolOrgInstallMintURL,
"--app-set", e2eAppSet,
"--enroll-all",
"--vendor",
Expand Down Expand Up @@ -898,7 +898,7 @@ func TestVendorFromSubdirectory(t *testing.T) {
"admin", "install", env.org,
"--skip-app-setup",
"--skip-mint-check",
"--mint-url", env.cfg.MintURL,
"--mint-url", e2etest.DefaultPoolOrgInstallMintURL,
"--app-set", e2eAppSet,
"--enroll-none",
"--vendor",
Expand Down
22 changes: 17 additions & 5 deletions internal/cli/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import (
// DefaultMintURL is the hosted public mint URL used when --mint-url is not
// explicitly provided. Users who self-host a mint can override this via
// the --mint-url flag.
const DefaultMintURL = "https://fullsend-mint-gljhbkcloq-uc.a.run.app"
const DefaultMintURL = "https://mint.fullsend.sh"
Comment thread
rh-hemartin marked this conversation as resolved.

// adminMintDiscovery holds the results of a mint infrastructure discovery call.
type adminMintDiscovery struct {
Expand Down Expand Up @@ -197,6 +197,17 @@ func validateWIFProvider(raw string) error {
return nil
}

// IsHostedMintURL reports whether raw is the hosted community mint URL
// (mint.fullsend.sh). This check is also used by pkg/e2etest to resolve
// the GCP project for hosted-mint enrollment.
func IsHostedMintURL(raw string) bool {
parsed, err := url.Parse(raw)
if err != nil {
return false
}
return strings.EqualFold(parsed.Hostname(), "mint.fullsend.sh")
}

func validateMintURL(raw string) error {
if err := validateMintURLHTTPS(raw); err != nil {
return err
Expand All @@ -206,11 +217,12 @@ func validateMintURL(raw string) error {
return err
}
host := parsed.Hostname()
if !strings.HasSuffix(host, ".run.app") &&
!strings.HasSuffix(host, ".cloudfunctions.net") {
return fmt.Errorf("--mint-url must be a Cloud Run URL (.run.app or .cloudfunctions.net), got host %q", host)
if strings.EqualFold(host, "mint.fullsend.sh") ||

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] DRY-internal-reuse

validateMintURL duplicates the mint.fullsend.sh hostname check that IsHostedMintURL already encapsulates.

Suggested fix: Consider extracting the hostname comparison into a helper that takes a pre-parsed hostname.

strings.HasSuffix(host, ".run.app") ||
strings.HasSuffix(host, ".cloudfunctions.net") {
return nil
}
return nil
return fmt.Errorf("--mint-url must be mint.fullsend.sh or a Cloud Run URL (.run.app or .cloudfunctions.net), got host %q", host)
}

func validateSkipMintCheck(mintURL string) error {
Expand Down
41 changes: 40 additions & 1 deletion internal/cli/admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ func TestInstallCmd_PerRepoRejectsNonCloudRunMintURL(t *testing.T) {
cmd.SetArgs([]string{"admin", "install", "acme/widget", "--mint-url", "https://evil.example.com", "--inference-project", "my-project"})
err := cmd.Execute()
require.Error(t, err)
assert.Contains(t, err.Error(), "--mint-url must be a Cloud Run URL")
assert.Contains(t, err.Error(), "--mint-url must be mint.fullsend.sh or a Cloud Run URL")
}

func TestInstallCmd_PerRepoRejectsPerOrgFlags(t *testing.T) {
Expand Down Expand Up @@ -1780,6 +1780,45 @@ func TestValidateSkipMintCheck(t *testing.T) {
require.NoError(t, validateSkipMintCheck("https://mint.example.com/v1/token"))
}

func TestValidateMintURL_AcceptsHostedCommunityMint(t *testing.T) {
require.NoError(t, validateMintURL("https://mint.fullsend.sh"))
}

func TestValidateMintURL_AcceptsCloudRunURL(t *testing.T) {
require.NoError(t, validateMintURL("https://fullsend-mint-abc123.run.app"))
}

func TestValidateMintURL_AcceptsCloudFunctionsURL(t *testing.T) {
require.NoError(t, validateMintURL("https://us-central1-my-project.cloudfunctions.net"))
}

func TestValidateMintURL_RejectsArbitraryHosts(t *testing.T) {
err := validateMintURL("https://evil.example.com")
require.Error(t, err)
assert.Contains(t, err.Error(), "got host")
}

func TestValidateMintURL_RejectsOtherFullsendSubdomains(t *testing.T) {
err := validateMintURL("https://evil.fullsend.sh")
require.Error(t, err)
assert.Contains(t, err.Error(), "got host")
}

func TestDefaultMintURL_IsHostedCommunity(t *testing.T) {
assert.Equal(t, "https://mint.fullsend.sh", DefaultMintURL)
}

func TestIsHostedMintURL(t *testing.T) {
assert.True(t, IsHostedMintURL("https://mint.fullsend.sh"))
assert.True(t, IsHostedMintURL("https://mint.fullsend.sh/v1/token"))
assert.True(t, IsHostedMintURL("https://mint.fullsend.sh:443"))
assert.True(t, IsHostedMintURL("https://Mint.Fullsend.SH"))
assert.False(t, IsHostedMintURL("https://evil.example.com"))
assert.False(t, IsHostedMintURL("https://fullsend-mint-abc123.run.app"))
assert.False(t, IsHostedMintURL(""))
assert.False(t, IsHostedMintURL("://"))
}

func TestValidateWIFProvider_Valid(t *testing.T) {
valid := []string{
"projects/123456789/locations/global/workloadIdentityPools/fullsend-pool/providers/gh-acme-widget",
Expand Down
2 changes: 1 addition & 1 deletion internal/config/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ package config
const (
// DefaultPerRepoMintURL is the hosted public mint used when no
// mint_url is configured in config.yaml or config.base.yaml.
DefaultPerRepoMintURL = "https://fullsend-mint-gljhbkcloq-uc.a.run.app"
DefaultPerRepoMintURL = "https://mint.fullsend.sh"

// DefaultPerRepoInferenceProvider is the default inference backend.
DefaultPerRepoInferenceProvider = "vertex"
Expand Down
24 changes: 17 additions & 7 deletions internal/dispatch/gcf/provisioner.go
Original file line number Diff line number Diff line change
Expand Up @@ -664,10 +664,15 @@ func (p *Provisioner) provisionWithExistingMint(ctx context.Context) (map[string
}

parsedURL, err := url.Parse(p.cfg.MintURL)
if err != nil || parsedURL.Scheme != "https" ||
(!strings.HasSuffix(parsedURL.Host, ".run.app") &&
!strings.HasSuffix(parsedURL.Host, ".cloudfunctions.net")) {
return nil, fmt.Errorf("MintURL %q must be a valid Cloud Run URL (.run.app or .cloudfunctions.net)", p.cfg.MintURL)
if err != nil {
return nil, fmt.Errorf("MintURL %q must be mint.fullsend.sh or a Cloud Run URL (.run.app or .cloudfunctions.net)", p.cfg.MintURL)
}
host := parsedURL.Hostname()
if parsedURL.Scheme != "https" ||
(!strings.EqualFold(host, "mint.fullsend.sh") &&
!strings.HasSuffix(host, ".run.app") &&
!strings.HasSuffix(host, ".cloudfunctions.net")) {
return nil, fmt.Errorf("MintURL %q must be mint.fullsend.sh or a Cloud Run URL (.run.app or .cloudfunctions.net)", p.cfg.MintURL)
}

// Store new PEMs once per role (shared across orgs on the mint).
Expand Down Expand Up @@ -1003,9 +1008,14 @@ func (p *Provisioner) provisionSelfManaged(ctx context.Context) (map[string]stri
}

parsedURL, err := url.Parse(mintURL)
if err != nil || parsedURL.Scheme != "https" ||
(!strings.HasSuffix(parsedURL.Host, ".run.app") &&
!strings.HasSuffix(parsedURL.Host, ".cloudfunctions.net")) {
if err != nil {
return nil, fmt.Errorf("function URL %q is not a valid Cloud Run URL", mintURL)
}
host := parsedURL.Hostname()
if parsedURL.Scheme != "https" ||
(!strings.EqualFold(host, "mint.fullsend.sh") &&
!strings.HasSuffix(host, ".run.app") &&
!strings.HasSuffix(host, ".cloudfunctions.net")) {
return nil, fmt.Errorf("function URL %q is not a valid Cloud Run URL", mintURL)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] error-message-inconsistency

Error messages in provisionSelfManaged say 'function URL is not a valid Cloud Run URL' but validation now accepts mint.fullsend.sh. The provisionWithExistingMint block was updated but provisionSelfManaged retains the old wording in both error branches (lines 1012 and 1019).

Suggested fix: Update both error format strings to match the other validation sites.

}

Expand Down
51 changes: 50 additions & 1 deletion internal/dispatch/gcf/provisioner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,54 @@ func TestProvisioner_Provision_BundledMode(t *testing.T) {
assert.Contains(t, fake.calls, "AddSecretVersion")
}

func TestProvisioner_Provision_BundledMode_HostedMintURL(t *testing.T) {
fake := newFakeGCFClient()
fake.errs["GetSecret"] = ErrSecretNotFound
fake.functionInfo = &FunctionInfo{
Name: "projects/shared-project/locations/us-central1/functions/fullsend-mint",
State: "ACTIVE",
URI: "https://mint.fullsend.sh",
EnvVars: map[string]string{
"ALLOWED_ORGS": "test-org",
},
}

p := newTestProvisioner(Config{
ProjectID: "shared-project",
GitHubOrgs: []string{"test-org"},
AgentPEMs: singleRolePEMs(),
MintURL: "https://mint.fullsend.sh",
}, fake)

vars, err := p.Provision(context.Background())
require.NoError(t, err)
assert.Equal(t, "https://mint.fullsend.sh", vars["FULLSEND_MINT_URL"])
}

func TestProvisioner_Provision_BundledMode_CloudFunctionsURL(t *testing.T) {
fake := newFakeGCFClient()
fake.errs["GetSecret"] = ErrSecretNotFound
fake.functionInfo = &FunctionInfo{
Name: "projects/shared-project/locations/us-central1/functions/fullsend-mint",
State: "ACTIVE",
URI: "https://us-central1-shared-project.cloudfunctions.net",
EnvVars: map[string]string{
"ALLOWED_ORGS": "test-org",
},
}

p := newTestProvisioner(Config{
ProjectID: "shared-project",
GitHubOrgs: []string{"test-org"},
AgentPEMs: singleRolePEMs(),
MintURL: "https://us-central1-shared-project.cloudfunctions.net",
}, fake)

vars, err := p.Provision(context.Background())
require.NoError(t, err)
assert.Equal(t, "https://us-central1-shared-project.cloudfunctions.net", vars["FULLSEND_MINT_URL"])
}

func TestProvisioner_Provision_BundledMode_PublicMintSkipsPerRepoWIF(t *testing.T) {
fake := newFakeGCFClient()
fake.functionInfo = &FunctionInfo{
Expand Down Expand Up @@ -739,6 +787,7 @@ func TestProvisioner_Provision_BundledMode_InvalidMintURL(t *testing.T) {
{"HTTP not HTTPS", "http://mint.example.com"},
{"no scheme", "mint.example.com"},
{"empty host", "https://"},
{"other fullsend subdomain", "https://evil.fullsend.sh"},
}

for _, tc := range tests {
Expand All @@ -752,7 +801,7 @@ func TestProvisioner_Provision_BundledMode_InvalidMintURL(t *testing.T) {

_, err := p.Provision(context.Background())
require.Error(t, err)
assert.Contains(t, err.Error(), "must be a valid Cloud Run URL")
assert.Contains(t, err.Error(), "must be mint.fullsend.sh or a Cloud Run URL")
})
}
}
Expand Down
Loading
Loading