Skip to content
132 changes: 119 additions & 13 deletions docs/guides/infrastructure/mint-administration.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

This guide covers deploying and managing the fullsend token mint Cloud Function. The mint is the OIDC token exchange service that lets GitHub Actions workflows authenticate as GitHub Apps — it is infrastructure that serves all enrolled organizations and repositories.

| Command | Description |
|---------|-------------|
| `mint deploy` | Deploy or update the mint Cloud Function and GCP infrastructure |
| `mint add-role` | Add an agent role (PEM secret + `ROLE_APP_IDS` entry) |
| `mint remove-role` | Remove an agent role from the mint (deletes PEM secret by default) |
| `mint enroll` | Register an org or repo in `ALLOWED_ORGS` and configure WIF |
| `mint unenroll` | Remove an org or repo from the mint |
| `mint status` | Inspect mint health, enrolled orgs, and PEM secrets |
| `mint token` | Exchange a GitHub Actions OIDC token for an installation token |

Comment thread
ifireball marked this conversation as resolved.
Resolved
> **This guide is for platform operators** who deploy, manage, or troubleshoot the token mint Cloud Function. If you are an end user setting up fullsend for your organization, see [Installing fullsend](../../reference/installation.md) instead — the mint is typically deployed once by a platform operator, and organizations are enrolled as needed.

## Hosted mint
Expand Down Expand Up @@ -35,21 +45,25 @@ Pass this URL as `--mint-url` when running `fullsend admin install`, or set the

- **GCP IAM roles** — the user running mint commands authenticates via ADC (`gcloud auth application-default login`). The required roles depend on the command:

| IAM Role | `mint deploy` | `mint enroll` | `mint unenroll` | `mint status` |
|----------|:---:|:---:|:---:|:---:|
| `roles/iam.serviceAccountAdmin` | x | | | |
| `roles/iam.workloadIdentityPoolAdmin` | x | x | x | |
| `roles/resourcemanager.projectIamAdmin` | \* | \*\* | | |
| `roles/secretmanager.admin` | \* | | | |
| `roles/cloudfunctions.developer` | x | | | |
| `roles/cloudfunctions.viewer` | | x | x | x |
| `roles/run.admin` | x | x | x | |
| `roles/secretmanager.viewer` | | | | x |
| IAM Role | `mint deploy` | `mint add-role` | `mint remove-role` | `mint enroll` | `mint unenroll` | `mint status` |
|----------|:---:|:---:|:---:|:---:|:---:|:---:|
| `roles/iam.serviceAccountAdmin` | x | | | | | |
| `roles/iam.workloadIdentityPoolAdmin` | x | | | x | x | |
| `roles/resourcemanager.projectIamAdmin` | \* | | | \*\* | | |
| `roles/secretmanager.admin` | \* | \*\*\* | \*\*\*\* | | | |
| `roles/cloudfunctions.developer` | x | | | | | |
| `roles/cloudfunctions.viewer` | | x | x | x | x | x |
| `roles/run.admin` | x | x | x | x | x | |
| `roles/secretmanager.viewer` | | | | | | x |

\* `roles/resourcemanager.projectIamAdmin` and `roles/secretmanager.admin` are required for `mint deploy` only when using `--pem-dir` (first-time bootstrap). Standard deploys without `--pem-dir` do not need these roles.

\*\* `roles/resourcemanager.projectIamAdmin` is required for `mint enroll` only in per-repo mode (`mint enroll owner/repo`). Org-scoped enrollment does not grant IAM bindings — use `inference provision` separately.

\*\*\* `roles/secretmanager.admin` is required for `mint add-role` when uploading a new PEM (`--pem` or browser mode). It is not required when using `--use-existing-pem-secret`.

\*\*\*\* `roles/secretmanager.admin` is required for `mint remove-role` unless `--keep-pem` is passed (default deletes the PEM secret).

`roles/owner` covers all of the above for users with broad access.

An administrator can grant all required roles with a single script:
Expand Down Expand Up @@ -111,10 +125,102 @@ The `--pem-dir` directory must contain one `{role}.pem` file per agent role (e.g

### Mint URL stability

The mint URL is stable across redeploys within the same project and region — updating the Cloud Function does not change its URL. Adding a new org to an existing mint only updates `ALLOWED_ORGS` (and WIF configuration) without redeploying the function. Shared `ROLE_APP_IDS` are set at deploy time and are not modified per enrollment. Existing enrolled repos continue working with no changes.
The mint URL is stable across redeploys within the same project and region — updating the Cloud Function does not change its URL. Adding a new org to an existing mint only updates `ALLOWED_ORGS` (and WIF configuration) without redeploying the function. Shared `ROLE_APP_IDS` are managed at deploy/bootstrap time (`mint deploy --pem-dir`) or per-role via `mint add-role` / `remove-role` — not during enrollment. Existing enrolled repos continue working with no changes when orgs are added.

Deploying to a **different region** (e.g., changing `--region` from `us-central1` to `us-east5`) creates a new Cloud Run service with a different URL. All enrolled repos store the mint URL in a repo or org variable (`FULLSEND_MINT_URL`), so changing the region requires updating every enrolled repo's variable. Avoid changing `--region` after initial deployment unless you plan to update all consumers.

## Managing roles

Agent roles on the mint are **global** — each role maps to a GitHub App PEM secret (`fullsend-{role}-app-pem`) and an entry in the shared `ROLE_APP_IDS` environment variable. Use `fullsend mint add-role` and `fullsend mint remove-role` to manage individual roles after the mint is deployed.

| Command | When to use |
|---------|-------------|
| `mint deploy --pem-dir` | First-time bootstrap of the default app set (`fullsend-ai`) — seeds all default roles at once |
| `mint add-role` | Add a single role later, or register a custom app set one role at a time |
| `mint remove-role` | Remove a role from the mint (updates env vars; deletes PEM secret by default) |

`mint enroll` does **not** create or modify roles — it only authorizes orgs/repos to use roles that already exist on the mint.

### Adding a role

`fullsend mint add-role` requires the mint to already be deployed. Choose one of three mutually exclusive input modes:

**1. Existing app + PEM file** (`--slug` and `--pem`):

```bash
fullsend mint add-role coder \
--project="$GCP_PROJECT" \
--slug=fullsend-ai-coder \
--pem=/path/to/coder.pem
```

The CLI looks up the app's numeric ID from the GitHub API, verifies the PEM matches the app, stores the PEM in Secret Manager, and updates `ROLE_APP_IDS` / `ALLOWED_ROLES`.

**2. Existing PEM secret** (`--slug` and `--use-existing-pem-secret`):

```bash
fullsend mint add-role review \
--project="$GCP_PROJECT" \
--slug=fullsend-ai-review \
--use-existing-pem-secret
```

Use this when the PEM secret `fullsend-{role}-app-pem` already exists in Secret Manager (for example, copied from another project) and you only need to register the app ID on the mint. `--pem` and `--use-existing-pem-secret` cannot be combined.

**3. Create GitHub App via browser** (`--org`):

```bash
fullsend mint add-role prioritize \
--project="$GCP_PROJECT" \
--org=acme-corp \
--app-set=acme
```

Opens the GitHub App manifest flow in your browser, stores the PEM in Secret Manager, and updates the mint. Requires a GitHub token (`GH_TOKEN`, `GITHUB_TOKEN`, or `gh auth login`).

#### add-role flags

| Flag | Default | Description |
|------|---------|-------------|
| `--project` | | GCP project ID (required) |
| `--region` | `us-central1` | Cloud region for the mint service |
| `--slug` | | GitHub App slug (with `--pem` or `--use-existing-pem-secret`) |
| `--pem` | | Path to PEM file (with `--slug`; mutually exclusive with `--use-existing-pem-secret`) |
| `--use-existing-pem-secret` | `false` | Skip PEM upload; require existing Secret Manager secret (with `--slug`) |
| `--org` | | GitHub org for browser-based app creation |
| `--app-set` | `fullsend-ai` | App set prefix for browser mode (`{app-set}-{role}`) |
| `--public` | `false` | Install existing public app without confirm prompt (browser mode) |
| `--force` | `false` | Overwrite existing `ROLE_APP_IDS` entry for this role |
| `--dry-run` | `false` | Preview changes without making them |

The `fix` and `code` roles reuse the `coder` app — add role `coder` instead.

### Removing a role

`fullsend mint remove-role` removes a role from `ROLE_APP_IDS` and `ALLOWED_ROLES`. By default it also deletes the PEM secret from Secret Manager. Use `--keep-pem` to retain the secret for later re-registration.

```bash
# Remove role and delete PEM secret (default)
fullsend mint remove-role retro --project="$GCP_PROJECT"

# Remove role but keep PEM secret
fullsend mint remove-role retro --project="$GCP_PROJECT" --keep-pem
```

Requires typing the role name to confirm (unless `--dry-run` or `--yolo`). Removing `coder` also prevents `fix`/`code` token minting.

#### remove-role flags

| Flag | Default | Description |
|------|---------|-------------|
| `--project` | | GCP project ID (required) |
| `--region` | `us-central1` | Cloud region for the mint service |
| `--keep-pem` | `false` | Retain PEM secret in Secret Manager (default: delete) |
| `--dry-run` | `false` | Preview changes without making them |
| `--yolo` | `false` | Skip interactive confirmation |

This command does not uninstall GitHub Apps from organizations or update org `.fullsend` configuration — use `fullsend github setup` or edit config repos separately.

## Enrolling organizations and repositories

`fullsend mint enroll` registers an organization or repository in the mint and configures WIF to accept OIDC tokens from the target.
Expand All @@ -139,7 +245,7 @@ Enrollment does **not** grant Agent Platform (inference) access — use `fullsen

### Migration from per-org app ID flags

Prior versions of `mint enroll` accepted `--app-set`, `--role-app-ids`, `--roles`, and `--source-org` to copy per-org app ID mappings into `ROLE_APP_IDS`. App IDs are now **shared per role** on the mint (like PEM secrets) and are set at deploy time via `mint deploy --pem-dir` or `fullsend admin install`. Enrollment only adds the org to `ALLOWED_ORGS` and updates WIF — remove those flags from scripts and ensure the mint already has role-keyed `ROLE_APP_IDS` before enrolling.
Prior versions of `mint enroll` accepted `--app-set`, `--role-app-ids`, `--roles`, and `--source-org` to copy per-org app ID mappings into `ROLE_APP_IDS`. App IDs are now **shared per role** on the mint (like PEM secrets) and are set at deploy time via `mint deploy --pem-dir`, `fullsend admin install`, or per-role via `mint add-role`. Enrollment only adds the org to `ALLOWED_ORGS` and updates WIF — remove those flags from scripts and ensure the mint already has role-keyed `ROLE_APP_IDS` before enrolling.

### What enrollment does

Expand All @@ -148,7 +254,7 @@ Prior versions of `mint enroll` accepted `--app-set`, `--role-app-ids`, `--roles
3. Runs post-enrollment verification (see below)
4. Configures the mint-side WIF provider to accept OIDC tokens from the organization's repositories

Role PEM secrets and `ROLE_APP_IDS` must already exist on the mint, created during `mint deploy --pem-dir` or `fullsend admin install`. Enrollment does not create, copy, or modify PEM secrets or app ID mappings.
Role PEM secrets and `ROLE_APP_IDS` must already exist on the mint, created during `mint deploy --pem-dir`, `fullsend admin install`, or `mint add-role`. Enrollment does not create, copy, or modify PEM secrets or app ID mappings.

### Post-enrollment verification

Expand Down
4 changes: 3 additions & 1 deletion internal/cli/mint.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,13 +316,15 @@ func newMintCmd() *cobra.Command {
Long: `Manage the GCP Cloud Function that mints GitHub App installation tokens,
and mint short-lived tokens via OIDC.

Infrastructure subcommands (deploy, enroll, unenroll, status) require GCP
Infrastructure subcommands (deploy, enroll, unenroll, status, add-role, remove-role) require GCP
project access. The 'token' subcommand requires only GitHub Actions OIDC.`,
}
cmd.AddCommand(newMintDeployCmd())
cmd.AddCommand(newMintEnrollCmd())
cmd.AddCommand(newMintUnenrollCmd())
cmd.AddCommand(newMintStatusCmd())
cmd.AddCommand(newMintAddRoleCmd())
cmd.AddCommand(newMintRemoveRoleCmd())
cmd.AddCommand(newMintTokenCmd())
return cmd
}
Expand Down
Loading
Loading