Skip to content

feat: add development-only local mint server#1784

Open
ggallen wants to merge 2 commits into
fullsend-ai:mainfrom
ggallen:feat/dev-mint-v2
Open

feat: add development-only local mint server#1784
ggallen wants to merge 2 commits into
fullsend-ai:mainfrom
ggallen:feat/dev-mint-v2

Conversation

@ggallen
Copy link
Copy Markdown
Contributor

@ggallen ggallen commented Jun 2, 2026

Summary

Add a standalone token mint for local development and evaluation. This eliminates the need for GCP infrastructure (Secret Manager, WIF, Cloud Functions) to get started with fullsend.

Depends on #1783 — merge the mintcore extraction first, then this PR's diff will reduce to just the new feature code (~11 files).

Dev mint server (internal/devmint/)

  • Local HTTP server that mints real GitHub App installation tokens
  • Disk-based PEM storage with fsnotify hot reload — write PEM files and the mint picks them up
  • Optional OIDC verification (--insecure-no-auth flag for local dev)
  • cloudflared tunnel support for exposing the local mint to GitHub Actions runners

CLI changes

  • fullsend mint run command with --data-dir, --port, --tunnel, --insecure-no-auth, --oidc-audience flags
  • --mint-data-dir flag on fullsend admin install — writes PEMs directly to disk instead of Secret Manager
  • Enhanced --mint-url validation (allows http://localhost for dev mint)
  • storePEMToDisk() for disk-based PEM persistence during install

Documentation

  • New guide: docs/guides/infrastructure/dev-mint.md
  • Updated installation guide and docs README

Review focus

  1. Is storePEMToDisk safe against path traversal via role names?
  2. Is the cloudflared tunnel lifecycle robust (startup wait, shutdown, URL extraction)?
  3. Do --insecure-no-auth and --mint-data-dir compose correctly with existing flags?

Test plan

  • go test ./internal/devmint/... — 22 tests pass
  • go test ./internal/cli/... — CLI tests pass
  • go vet ./... — clean
  • Manual: fullsend mint run → tunnel → fullsend admin install → agent runs

🤖 Generated with Claude Code

ggallen and others added 2 commits June 2, 2026 10:15
Extract shared types and functions from internal/mint/main.go into
internal/mintcore/: OIDC verification (OIDCVerifier interface with
JWKSVerifier and STSVerifier implementations), JWT generation, GitHub
API helpers, claims validation, and pattern constants.

Refactor the GCP mint to import from mintcore instead of having inline
code. External behavior is unchanged — same HTTP API, same env vars,
same validation rules.

Update the Cloud Function provisioner to bundle mintcore alongside the
mint function for deployment. Add embed sync tests for mintcore files.

This is the first half of the dev-mint feature — extracting shared code
so it can be reused by a standalone development mint in a follow-up PR.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a standalone token mint (fullsend mint run) for local development
and evaluation. The dev mint stores GitHub App PEMs on disk with
fsnotify hot reload, verifies OIDC tokens via JWKS, and mints real
installation tokens — no GCP infrastructure required.

New CLI flags:
- fullsend mint run --data-dir --port --tunnel --insecure-no-auth
- fullsend admin install --mint-data-dir (writes PEMs to disk)

The --tunnel flag starts a cloudflared quick tunnel so GitHub Actions
runners can reach the local mint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 2, 2026

Site preview

Preview: https://82f4fd4d-site.fullsend-ai.workers.dev

Commit: abbd57b94458cd75e64847d6f2cccdc18464d46a

@fullsend-ai-review
Copy link
Copy Markdown

Review

Findings

Medium

  • [data-exposure] internal/devmint/devmint.go — The /v1/status endpoint on the dev mint server has no authentication check, even when --insecure-no-auth is NOT set and OIDC verification is enabled. While /v1/token correctly validates OIDC Bearer tokens, /v1/status is served unconditionally, exposing the org name, all registered roles, and their GitHub App IDs to any caller. This is inconsistent with the Cloud Function handler, where /v1/status requires a valid OIDC Bearer token (the auth check at the top of ServeHTTP runs before the /v1/status branch). When the dev mint is exposed via a cloudflared tunnel (--tunnel), this metadata is publicly accessible without authentication.
    Remediation: Gate /v1/status behind the same OIDC verification used for /v1/token, or at minimum require the request originate from localhost when insecureNoAuth is false. The Cloud Function handler already does this correctly — mirror that behavior.

  • [stale-doc] docs/guides/dev/cli-internals.md — The mint CLI command tree does not include the new fullsend mint run subcommand introduced in this PR. The existing tree shows deploy, enroll, unenroll, status but omits run. This is a public CLI feature documented in the new dev-mint.md guide.
    Remediation: Add run to the mint command tree section with description "Run a standalone token mint (no GCP required)".

Low

  • [error-handling-idiom] internal/cli/admin.go:395storePEMToDisk silently ignores non-file-not-found errors when reading the existing config.json (e.g., permission denied). The if data, err := os.ReadFile(configPath); err == nil pattern skips all errors, proceeding with a fresh empty config. This could silently overwrite all previously stored role mappings. Note: loadFromDisk in devmint.go handles this correctly by distinguishing IsNotExist from other errors.
    Remediation: Add an explicit check: if err != nil && !os.IsNotExist(err) { return fmt.Errorf("reading existing config: %w", err) }.

  • [missing-test] internal/cli/admin_test.goTestValidateSkipMintCheck does not test the http://[::1]:8321 case. The code and docs both state that HTTP is permitted for ::1, and the implementation uses parsed.Hostname() (which strips IPv6 brackets), but this path has no test coverage.
    Remediation: Add require.NoError(t, validateSkipMintCheck("http://[::1]:8321")) to the test.

  • [edge-case] internal/devmint/devmint.goloadFromDisk() writes to s.org, s.pems, and s.appIDs without holding the mutex. This is currently safe because it is only called from Start() before the HTTP server begins accepting connections, but if the code evolves to support runtime reload, this would become a data race.
    Remediation: Wrap the final assignments in s.mu.Lock()/s.mu.Unlock() for future safety.

@fullsend-ai-review fullsend-ai-review Bot added the requires-manual-review Review requires human judgment label Jun 2, 2026
## Prerequisites

- **fullsend CLI** (v0.5.0+ or built from source):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This will available after 0.13.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

requires-manual-review Review requires human judgment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants