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
17 changes: 5 additions & 12 deletions docs/guides/getting-started/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,13 @@ 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
1. **Token mint** — the CLI defaults to the hosted community mint at
`https://mint.fullsend.sh`. Install the shared public GitHub Apps
for your organization and set (or rely on the CLI default)
`FULLSEND_MINT_URL` — no separate enrollment step is required.
To deploy and manage your own self-hosted 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)
6 changes: 3 additions & 3 deletions docs/guides/getting-started/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,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 @@ -84,7 +84,7 @@ To remove fullsend from a single repository:
4. Delete fullsend pipeline schedules (`fullsend slash poll` and `fullsend event poll`)
5. For WIF-mode repos: delete the bot token Secret Manager secret (named `fullsend-bot-token-<owner>--<repo>`) from the GCP project

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 @@ -121,7 +121,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
15 changes: 8 additions & 7 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** (WIF mode only) — GCP WIF provisioning (`fullsend inference provision`) and mint enrollment (`fullsend mint enroll`) must be completed separately before running `repos install`. OIDC and token modes do not require GCP infrastructure. See [Mint administration](../infrastructure/mint-administration.md) and [Advanced setup](../infrastructure/advanced-setup.md).
- **GCP prerequisites** (WIF mode only) — 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. See [Mint administration](../infrastructure/mint-administration.md) and [Advanced setup](../infrastructure/advanced-setup.md).

## Getting started

Expand Down Expand Up @@ -112,7 +112,7 @@ Set `credential_mode` at the forge level or per-repo in the manifest:
forge:
github:
credential_mode: oidc
mint_url: https://fullsend-mint-example.run.app
mint_url: https://mint.fullsend.sh
gitlab:
credential_mode: token
url: https://gitlab.example.com
Expand Down Expand Up @@ -169,11 +169,12 @@ Install runs in three phases:
drift (synced automatically) and scaffold ref drift (upgraded
automatically).

> **Prerequisite:** For repos using `wif` credential mode, GCP
> infrastructure (WIF pools/providers, mint enrollment) must be
> provisioned separately before running install. Repos using `oidc`
> or `token` mode do not require GCP infrastructure.
> See `fullsend inference provision` and `fullsend mint enroll`.
> **Prerequisite:** For repos using `wif` credential mode, 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. Repos using `oidc` or `token` mode do not require
> GCP infrastructure.

> **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
2 changes: 1 addition & 1 deletion docs/guides/infrastructure/layered-config-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ compiled-in defaults apply:
| `allowed_remote_resources` | `["https://raw.githubusercontent.com/fullsend-ai/fullsend/", "https://raw.githubusercontent.com/fullsend-ai/agents/"]` |
| `forge` | `""` (GitHub) |
| `tracker` | `""` (none — `--tracker` is required unless set) |
| `mint_url` | `"https://fullsend-mint-gljhbkcloq-uc.a.run.app"` (hosted public mint) |
| `mint_url` | `"https://mint.fullsend.sh"` (hosted public mint) |
| `inference.provider` | `"vertex"` |
| `inference.project` | `""` (empty — must be provided) |
| `inference.region` | `"global"` |
Expand Down
6 changes: 3 additions & 3 deletions docs/guides/infrastructure/mint-administration.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,17 @@ 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`).

**Mint URL:**

```
https://fullsend-mint-gljhbkcloq-uc.a.run.app
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
14 changes: 7 additions & 7 deletions docs/guides/infrastructure/standalone-mint.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ 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://fullsend-mint-gljhbkcloq-uc.a.run.app`
- **Your organization enrolled in the hosted mint** (optional, for fallback proxy) — see [Mint service administration](mint-administration.md)
- **The hosted mint URL** (optional, for fallback proxy): `https://mint.fullsend.sh`
- **Shared public GitHub Apps installed** (optional, for fallback proxy) — see [Getting Started](../getting-started/)

## Step 1: Create a GitHub App

Expand Down Expand Up @@ -113,7 +113,7 @@ The standalone mint is configured entirely through environment variables:
| Variable | Description | Example |
|----------|-------------|---------|
| `ALLOWED_WORKFLOW_FILES` | Comma-separated workflow file allowlist; `*` for all | `*` |
| `FALLBACK_MINT_URL` | Upstream mint URL for roles without local PEMs | `https://fullsend-mint-gljhbkcloq-uc.a.run.app` |
| `FALLBACK_MINT_URL` | Upstream mint URL for roles without local PEMs | `https://mint.fullsend.sh` |
| `CUSTOM_ROLE_PERMISSIONS` | JSON map of custom role permissions (see below) | `{"scanner":{"contents":"read"}}` |
| `PER_REPO_WIF_REPOS` | Comma-separated repos with per-repo WIF treatment. Use `*` for public mint mode (all repos get per-repo treatment). Per-repo callers can only mint to their own repo scope. Callers not in this list fall through to per-org (`ALLOWED_ORGS`) and get org-mode repos shapes. | `myorg/private-repo` |
| `WORKFLOW_HOST_REPOS` | Comma-separated repos whose workflows are trusted to call the mint for per-repo callers. Per-org callers are not affected (they hard-wire to `{org}/.fullsend` and upstream). Defaults to `fullsend-ai/fullsend` when unset. | `fullsend-ai/fullsend,myorg/my-workflows` |
Expand All @@ -138,7 +138,7 @@ export ROLE_APP_IDS='{"triage":"4087047","scanner":"5555555"}'
export OIDC_AUDIENCE="fullsend-mint"
export PEM_DIR="./pems"
export ALLOWED_WORKFLOW_FILES="*"
export FALLBACK_MINT_URL="https://fullsend-mint-gljhbkcloq-uc.a.run.app"
export FALLBACK_MINT_URL="https://mint.fullsend.sh"
export CUSTOM_ROLE_PERMISSIONS='{"scanner":{"contents":"read","security_events":"write","metadata":"read"}}'

./fullsend-mint
Expand All @@ -148,7 +148,7 @@ On startup, the mint logs the configuration:

```
2026/06/18 12:00:00 custom role permissions registered: [scanner]
2026/06/18 12:00:00 fallback mint configured: https://fullsend-mint-gljhbkcloq-uc.a.run.app (local roles: [scanner triage])
2026/06/18 12:00:00 fallback mint configured: https://mint.fullsend.sh (local roles: [scanner triage])
2026/06/18 12:00:00 fullsend-mint starting on :8080 (standalone mode)
```

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 Expand Up @@ -336,7 +336,7 @@ export ROLE_APP_IDS='{"triage":"4087047","scanner":"5555555"}'
export OIDC_AUDIENCE="fullsend-mint"
export PEM_DIR="./pems"
export ALLOWED_WORKFLOW_FILES="*"
export FALLBACK_MINT_URL="https://fullsend-mint-gljhbkcloq-uc.a.run.app"
export FALLBACK_MINT_URL="https://mint.fullsend.sh"
export CUSTOM_ROLE_PERMISSIONS='{"scanner":{"contents":"read","security_events":"write","metadata":"read"}}'

./fullsend-mint
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
23 changes: 18 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"

// 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 @@ -205,11 +216,13 @@ func validateMintURL(raw string) error {
if err != nil {
return err
}
if !strings.HasSuffix(parsed.Host, ".run.app") &&
!strings.HasSuffix(parsed.Host, ".cloudfunctions.net") {
return fmt.Errorf("--mint-url must be a Cloud Run URL (.run.app or .cloudfunctions.net), got host %q", parsed.Host)
host := parsed.Hostname()
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] permission-expansion

The validateMintURL hostname allowlist now accepts the exact host mint.fullsend.sh (via case-insensitive EqualFold) in addition to the pre-existing .run.app and .cloudfunctions.net suffix checks. Safe against subdomain-prefix bypass. HTTPS and no-credentials checks remain enforced. No security regression; noting for visibility.

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

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-consistency

Error message says '.fullsend.sh' but the code only accepts the exact hostname 'mint.fullsend.sh'. The message mixes a suffix pattern (.fullsend.sh) with what is actually an exact-match check.

Suggested fix: Change the error message to reference 'mint.fullsend.sh' explicitly.

}

func validateSkipMintCheck(mintURL string) error {
Expand Down
34 changes: 33 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 a hosted mint or Cloud Run URL")
}

func TestInstallCmd_PerRepoRejectsPerOrgFlags(t *testing.T) {
Expand Down Expand Up @@ -1780,6 +1780,38 @@ 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 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(""))
}

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
12 changes: 11 additions & 1 deletion pkg/e2etest/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ func runningInGitHubActions() bool {
return os.Getenv("GITHUB_ACTIONS") == "true"
}

// DefaultPoolOrgInstallMintURL is written into pool orgs as FULLSEND_MINT_URL by
// admin e2e install tests. Distinct from resolveMintURL() / cli.DefaultMintURL,
// which CI uses for cross-org e2e org locking.
//
// Admin e2e tests exercise per-org installation; workflows on the installed org
// mint against FULLSEND_MINT_URL. The community hosted mint (mint.fullsend.sh)
// runs in public mode and does not support per-org installs, so org-mode admin
// e2e must keep using the legacy per-org hosted dev mint until that changes.
const DefaultPoolOrgInstallMintURL = "https://fullsend-mint-gljhbkcloq-uc.a.run.app"

// resolveMintURL returns the mint endpoint from FULLSEND_MINT_URL or the hosted
// default (same as fullsend admin --mint-url).
func resolveMintURL() string {
Expand All @@ -63,7 +73,7 @@ func MintEnrollProjectID(cfg EnvConfig) string {
if mintURL == "" {
mintURL = cli.DefaultMintURL
}
if mintURL == cli.DefaultMintURL {
if mintURL == DefaultPoolOrgInstallMintURL || cli.IsHostedMintURL(mintURL) {
return DefaultHostedMintGCPProject
}
return strings.TrimSpace(cfg.GCPProjectID)
Expand Down
13 changes: 11 additions & 2 deletions pkg/e2etest/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,24 @@ import (

func TestMintEnrollProjectID(t *testing.T) {
t.Setenv("E2E_GCP_MINT_PROJECT_ID", "")

// Pool-org install mint (DefaultPoolOrgInstallMintURL) → hosted project.
cfg := EnvConfig{
MintURL: cli.DefaultMintURL,
MintURL: DefaultPoolOrgInstallMintURL,
GCPProjectID: "inference-only-project",
}
assert.Equal(t, DefaultHostedMintGCPProject, MintEnrollProjectID(cfg))

// Community mint (cli.DefaultMintURL / mint.fullsend.sh) → hosted project
// via IsHostedMintURL.
cfg.MintURL = cli.DefaultMintURL
assert.Equal(t, DefaultHostedMintGCPProject, MintEnrollProjectID(cfg))

// Env override takes precedence.
t.Setenv("E2E_GCP_MINT_PROJECT_ID", "override-mint-project")
assert.Equal(t, "override-mint-project", MintEnrollProjectID(cfg))

// Custom (non-hosted) mint → inference project.
t.Setenv("E2E_GCP_MINT_PROJECT_ID", "")
cfg.MintURL = "https://mint.example.com"
assert.Equal(t, "inference-only-project", MintEnrollProjectID(cfg))
Expand All @@ -37,7 +46,7 @@ func TestMintEnrollProjectID_EmptyWithoutHostedMint(t *testing.T) {

func TestMintEnrollProjectID_RespectsEnvOverride(t *testing.T) {
t.Setenv("E2E_GCP_MINT_PROJECT_ID", "from-env")
cfg := EnvConfig{MintURL: cli.DefaultMintURL}
cfg := EnvConfig{MintURL: DefaultPoolOrgInstallMintURL}
assert.Equal(t, "from-env", MintEnrollProjectID(cfg))
_ = os.Unsetenv("E2E_GCP_MINT_PROJECT_ID")
}
Expand Down
Loading