Skip to content

docs: ADR 0029 — central token mint for secretless .fullsend#655

Merged
waynesun09 merged 8 commits into
fullsend-ai:mainfrom
ifireball:adr/0026-token-mint
May 13, 2026
Merged

docs: ADR 0029 — central token mint for secretless .fullsend#655
waynesun09 merged 8 commits into
fullsend-ai:mainfrom
ifireball:adr/0026-token-mint

Conversation

@ifireball
Copy link
Copy Markdown
Contributor

@ifireball ifireball commented May 5, 2026

Summary

Adds Proposed ADR 0029: shared GitHub Apps, OIDC-bound central token mint, no long-lived secrets in the .fullsend repo, workflow_call instead of dispatch PAT, and notes on multi-mint deployments and extensibility (Tekton, other SCMs).

Renumbered from 0026 after ADR 0026 landed on main.

Context

Captures the architecture direction discussed for eliminating repo-stored App PEMs and related deployment friction; does not change docs/architecture.md or supersede ADR 0007/0008 until the ADR is accepted.

See #308 for a different attempt at dropping the need for the dispatch PAT.

Checklist

  • make lint (ADR linters + pre-commit) passes on this branch

Record proposed architecture: shared GitHub Apps, OIDC-bound token mint,
workflow_call instead of dispatch PAT, and no long-lived secrets in the
config repo. Status Proposed pending implementation and supersession of
ADR 0007/0008 details.

Co-authored-by: Cursor <cursoragent@cursor.com>
@fullsend-ai-review
Copy link
Copy Markdown

fullsend-ai-review Bot commented May 5, 2026

Review: #655

Head SHA: 164aee4
Timestamp: 2026-05-13T00:00:00Z
Outcome: approve

Summary

Clean, well-structured Proposed ADR adding a central token mint architecture to eliminate long-lived secrets from .fullsend repos. The ADR accurately characterizes the relationship to prior ADRs (0007, 0008, 0025, 0026) — verified against source — and correctly identifies ADR 0008 as reversed in mechanism (workflow_dispatch+PAT → workflow_call+OIDC). The security consequences section is thorough, covering mint compromise blast radius, OIDC trust binding, and availability trade-offs. No correctness, security, or injection findings. ADR 0029 numbering has no conflicts with existing ADRs on main.

Findings

No critical, high, medium, or low findings.

Info

  • [style/conventions] docs/ADRs/0029-central-token-mint-secretless-fullsend.md — The "Relationship to prior ADRs (summary)" section is not part of the ADR template (0000-adr-template.md). It enhances readability for a cross-cutting ADR like this one, but future authors may wonder whether it is expected. Consider noting it as optional in the template if this pattern recurs.

Footer

Outcome: approve
This review applies to SHA 164aee440137057fb0ebf587232d003efb44af84. Any push to the PR head clears this review and requires a new evaluation.

Previous run

Review: #655

Head SHA: fc89efb
Timestamp: 2026-05-10T00:00:00Z
Outcome: approve

Summary

This PR adds a well-structured Proposed ADR (0029) documenting the architecture direction for a central token mint and shared GitHub Apps, eliminating long-lived secrets from .fullsend repositories. The ADR follows the repo template, correctly identifies relationships to prior ADRs (0007, 0008, 0009, 0025, 0026), and provides thorough security analysis covering mint compromise blast radius, OIDC trust binding, and availability trade-offs. No code changes are included — this is documentation only. No blocking findings across all six review dimensions.

Findings

Info

  • [style/conventions] Branch name adr/0026-token-mint does not match the final ADR number 0029. This is cosmetic (explained by renumbering after ADRs 0026-0028 landed on main) and does not affect the content.

Footer

Outcome: approve
This review applies to SHA fc89efb01d2552a5812a82ed1a20dd8989b6e7a4. Any push to the PR head clears this review and requires a new evaluation.

Previous run (2)

Review: #655

Head SHA: 1a69009
Timestamp: 2026-05-05T00:00:00Z
Outcome: approve

Summary

Clean, well-structured Proposed ADR adding the central token mint architecture direction. The document correctly references and builds upon existing accepted ADRs (0007, 0008, 0025, 0026), all relative links resolve to files present on main, and the ADR number (0027) does not conflict with any existing ADR. The security analysis is substantive — it identifies the mint as a high-value target, discusses OIDC-based workload identity binding via job_workflow_ref, and correctly scopes the blast radius of a spoofed .fullsend repo. No code changes, no secrets, no sensitive data. Frontmatter and document structure follow established project conventions.

Findings

No critical, high, medium, low, or info findings.

Footer

Outcome: approve
This review applies to SHA 1a6900958de8596eabe1a29b4f7b112730c8b91a. Any push to the PR head clears this review and requires a new evaluation.

Previous run (3)

Review: #655

Head SHA: 1252fdb
Timestamp: 2026-05-05T00:00:00Z
Outcome: comment-only

Summary

Clean, well-structured Proposed ADR that follows the project template and accurately references existing ADRs (0007, 0008, 0025). The security consequences are thoughtfully articulated — particularly the mint-as-high-value-target and the job_workflow_ref trust boundary. One medium observation: the interaction with ADR 0026 (stage-based dispatch) is not acknowledged, even though the proposed workflow_call switch would affect that dispatch model.

Findings

Medium

  • [correctness] docs/ADRs/0027-central-token-mint-secretless-fullsend.md:52 — The Consequences section lists ADRs 0007 and 0008 as needing follow-on superseding ADRs, but omits ADR 0026. ADR 0026 established a workflow_dispatch-based stage dispatcher between shims and agent workflows. This ADR's consequence that shims can switch to workflow_call (line 49) would also affect ADR 0026's dispatch model — the dispatcher currently authenticates gh workflow run calls using workflow_dispatch, and replacing the shim→config-repo boundary with workflow_call changes the trust and credential flow ADR 0026 relies on. Consider adding ADR 0026 to the list of ADRs requiring follow-on updates.

Info

  • [style] docs/ADRs/0027-central-token-mint-secretless-fullsend.md — The ADR template includes an HTML comment block ("Once this ADR is Accepted, its content is frozen...") below the Status section. This ADR omits it. Not blocking since the ADR is Proposed, but including it ensures the freeze notice is present if/when status changes to Accepted.

Footer

Outcome: comment-only
This review applies to SHA 1252fdb3affe9c5eb210bb1a26f69fa47d64071a. Any push to the PR head clears this review and requires a new evaluation.

Previous run (4)

Review: #655

Head SHA: bece422
Timestamp: 2026-05-05T00:00:00Z
Outcome: request-changes

Summary

This PR adds a well-written Proposed ADR describing a central token mint architecture for eliminating long-lived secrets from .fullsend repos. The content is sound and follows the ADR template correctly, with appropriate references to existing ADRs and the security threat model. However, the ADR is numbered 0026, which collides with the already-accepted ADR 0026-stage-based-dispatch-for-agent-workflow-decoupling.md on the base branch. This must be renumbered to 0027 (filename and all internal references) before merging.

Findings

High

  • [Correctness] docs/ADRs/0026-central-token-mint-secretless-fullsend.md — ADR number collision: the base branch already contains 0026-stage-based-dispatch-for-agent-workflow-decoupling.md (status: Accepted). Two ADRs sharing the same number breaks the sequential numbering convention and will cause confusion in cross-references.
    Remediation: Renumber this ADR to 0027. Update the filename to 0027-central-token-mint-secretless-fullsend.md, the frontmatter title field, the H1 heading, and any internal references.

Footer

Outcome: request-changes
This review applies to SHA bece422a5e6eec2362851f63d4713b1f343e6512. Any push to the PR head clears this review and requires a new evaluation.

Copy link
Copy Markdown

@fullsend-ai-review fullsend-ai-review Bot left a comment

Choose a reason for hiding this comment

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

See the review comment for full details.

@ifireball ifireball marked this pull request as ready for review May 5, 2026 12:13
ifireball and others added 2 commits May 5, 2026 15:13
Merge upstream/main; ADR 0026 is now stage-based dispatch. Rename and
re-title the secretless .fullsend ADR as 0027.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ifireball ifireball changed the title docs: ADR 0026 — central token mint for secretless .fullsend docs: ADR 0027 — central token mint for secretless .fullsend May 5, 2026
@ifireball
Copy link
Copy Markdown
Contributor Author

Addressed review feedback and CI:

  • Merged upstream/main (includes accepted ADR 0026 — stage-based dispatch).
  • Renamed this ADR to 0027 (docs/ADRs/0027-central-token-mint-secretless-fullsend.md) so numbering is unique; updated PR title/body accordingly.
  • lint / ADR linters are green on the latest push.

Copy link
Copy Markdown

@fullsend-ai-review fullsend-ai-review Bot left a comment

Choose a reason for hiding this comment

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

See the review comment for full details.

@ifireball ifireball requested review from ralphbean and waynesun09 May 5, 2026 12:30
ifireball and others added 2 commits May 5, 2026 15:30
Address review: acknowledge interaction with stage-based dispatch when
moving to workflow_call; extend follow-on ADR list; match template
HTML comment under Status.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ifireball
Copy link
Copy Markdown
Contributor Author

Updated ADR 0027 per latest review:

  • Context: notes interaction with ADR 0026 when moving toward workflow_call.
  • Consequences: follow-on ADR list now includes 0026 and calls out the stage dispatcher / workflow_dispatch trust boundary.
  • Status: added the template freeze HTML comment below ## Status.

Also merged latest upstream/main into this branch.

Copy link
Copy Markdown

@fullsend-ai-review fullsend-ai-review Bot left a comment

Choose a reason for hiding this comment

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

See the review comment for full details.

@rh-hemartin
Copy link
Copy Markdown
Contributor

How do the mint service accepts orgs? I want to run my own mint service with my set of apps and I don't want anyone hooking to it.

@ifireball
Copy link
Copy Markdown
Contributor Author

ifireball commented May 6, 2026

@rh-hemartin

How do the mint service accepts orgs? I want to run my own mint service with my set of apps and I don't want anyone hooking to it.

The OIDC token that the workflow sends into the mint contains, org, repo, and branch and workflow file path. If furthermore the workflow calls another workflow, we get those details for the called workflow in the job_workflow_ref claim.

We can filter on any of those but I think our logic would be to:

  • Check the job_workflow_ref repo, branch, workflow to ensure its .fullsend/main/(one of ours).
  • Ensure the calling and called workflow are on the same org

After validation the mint needs to mint the app token, for that it needs an ap Id, the API to fetch that is:

GET /repos/{owner}/{repo}/installation

We would use the org and repo from the OIDC token there, which means that:

  1. The app token would be scoped to the calling org and repo
  2. The app has to be installed on the calling org to begin with for any of this to work

So to your question - of you have your own app and mint , I am either:

  1. Unable to install your app because you keep it provate
  2. Able to install your app because you maed it public, but then am only able to get tokens that work on my org

Given this, I'm not sure we really need to add an org or repo based whitelist to the service. Rather limits would be good on any API endpoint so we'll put those in place regardless.

waynesun09 added a commit that referenced this pull request May 6, 2026
ADR 0027 (central token mint for secretless .fullsend) is being
finalized in PR #655 by ifireball. This branch's implementation
will reference that ADR once accepted.

Signed-off-by: Wayne Sun <gsun@redhat.com>
@waynesun09
Copy link
Copy Markdown
Contributor

waynesun09 commented May 6, 2026

Implementation Plan: Token Mint + workflow_call

We have an implementation plan for the first phase of this ADR. Sharing here for design alignment before we start coding.

What this covers (Phase 1 of ADR 0027)

  • Token mint — Cloud Function that validates GitHub OIDC tokens (via WIF) and returns short-lived, org-scoped GitHub App installation tokens. Per-role PEM lookup from GCP Secret Manager.
  • workflow_call shim — enrolled repos call .fullsend/dispatch.yml via native workflow_call. No dispatch PAT, no OIDC curl, no dispatch proxy.
  • Secretless .fullsend — all per-role App PEMs move from repo secrets to the mint's Secret Manager. .fullsend stores zero long-lived secrets in OIDC mode.
  • job_workflow_ref authorization — mint validates that only .fullsend workflow files at pinned refs can request tokens.
  • PAT mode preserved — existing orgs keep working unchanged via mode switch in config.yaml.

What this defers (future phases)

ADR 0027 item Status Notes
Shared public Apps Deferred Per-org Apps remain. Mint architecture supports shared Apps — just change which PEMs are stored
Deployment profiles (multi-mint) Deferred Single GCF deployment. Handler is portable (standard HTTP, no GCP SDK in request path)
Cross-platform extensibility (Tekton) Deferred GitHub Actions OIDC only. Mint can add issuers later
Other SCM support Deferred GitHub only. Follows forge abstraction pattern
Follow-on ADRs superseding 0007/0008 Deferred Will reference this ADR once accepted

Architecture

ENROLLED REPO                          .FULLSEND REPO
─────────────                          ──────────────
fullsend.yaml ── workflow_call ──────> dispatch.yml
(shim)                                      │
                                     ┌──────┴──────┐
                                     │  Fan-out    │
                                     └──────┬──────┘
                          ┌─────────────────┼─────────────────┐
                     triage.yml        code.yml          review.yml
                          │                │                   │
                     OIDC → mint      OIDC → mint        OIDC → mint
                     → triage token   → coder token      → review token

                              TOKEN MINT (Cloud Function)
                              1. Validate OIDC JWT (WIF)
                              2. Check job_workflow_ref claim
                              3. Look up PEM for (org, role)
                              4. Mint scoped installation token
                              5. Return token + expiry

Token Mint API

POST /v1/token
Authorization: Bearer <GitHub OIDC JWT>
{"role": "coder", "repos": ["target-repo"]}

→ {"token": "ghs_xxxx", "expires_at": "..."}

Org derived from repository_owner claim — callers cannot request tokens for other orgs.

Key design decisions

  • Token mint, not dispatch proxy — the mint only issues tokens. Fan-out logic stays in dispatch.yml where it belongs. Simpler function, easier to test, portable across FaaS.
  • Per-org PEM namespacing — secret names: fullsend-{org}-{role}-app-pem. When shared Apps land, just swap which PEMs the mint holds.
  • Immediate PEM persistenceStoreAgentPEM(org, role, pem) called right after each App creation via manifest flow, survives partial install failures.
  • Portable handler — HTTP handler has no GCP imports in the request path. Only the provisioner (deployment automation) is GCP-specific. Same handler deploys to Lambda, Cloud Run, or a container.

Gap analysis vs ADR 0027

The main gap is shared Apps as the default onboarding path. This plan keeps per-org Apps (ADR 0007) and implements the mint as infrastructure that makes shared Apps possible later. The question for this ADR: should Phase 1 be accepted as a valid partial implementation, or does the ADR expect shared Apps from day one?

PR #503 (OIDC dispatch proxy) will be rewritten to implement this plan directly — no transitional dispatch proxy merged.


Update (2026-05-07): PR #503 now implements this plan. Key changes since this comment:

  • Mint moved from internal/dispatch/gcf/function/ to internal/mint/
  • Dispatch mode renamed from oidc-gcf to oidc-mint
  • Org is derived from the JWT repository_owner claim (not env var) — validated against ALLOWED_ORGS
  • dispatch.yml uses OIDC mint for its own token in workflow_call mode (no secrets flow through shim)

@waynesun09
Copy link
Copy Markdown
Contributor

Correction: Deployment models

After more thought — per-org Apps + shared mint is not a broken trust model. It's the bundled model: an enterprise admin managing multiple GitHub orgs runs one mint for all of them. Same admin controls both the Apps and the mint — no trust split.

Three deployment models, same mint code:

Model Apps Mint Trust boundary Example
Self-managed Per-org Per-org Org admin Small org, own GCP project
Bundled Per-org Shared Company admin Enterprise with 5 GitHub orgs
SaaS Shared (public) Platform-operated Platform operator Fullsend-as-a-service

How each works with `admin install`:

  • Self-managed: `--dispatch-mode=oidc-mint --mint-gcp-project=my-proj` — deploys mint + stores PEMs
  • Bundled: First org deploys mint. Others: `--dispatch-mode=oidc-mint --mint-url=https://existing-mint.run.app\` — stores PEMs in existing mint, skips deployment
  • SaaS: Platform pre-provisions mint + shared Apps. Orgs install Apps and set `--mint-url`

Per-org PEM namespacing (`fullsend-{org}-{role}-app-pem`) + WIF `repository_owner` claim isolation supports all three models. Phase 1 implements self-managed + bundled. SaaS deferred until shared Apps land.

waynesun09 added a commit that referenced this pull request May 6, 2026
ADR 0027 (central token mint for secretless .fullsend) is being
finalized in PR #655 by ifireball. This branch's implementation
will reference that ADR once accepted.

Signed-off-by: Wayne Sun <gsun@redhat.com>
waynesun09 added a commit that referenced this pull request May 7, 2026
ADR 0027 (central token mint for secretless .fullsend) is being
finalized in PR #655 by ifireball. This branch's implementation
will reference that ADR once accepted.

Signed-off-by: Wayne Sun <gsun@redhat.com>
@ifireball
Copy link
Copy Markdown
Contributor Author

ifireball commented May 7, 2026

@waynesun09

Per-org PEM namespacing (`fullsend-{org}-{role}-app-pem`) + WIF `repository_owner` claim isolation supports all three models. Phase 1 implements self-managed + bundled. SaaS deferred until shared Apps land.

I don't fully understand how this enables the shared app mode, for that to work we would need to define a set of pems that are not namespaced to a particular org and setup some rule for when to use them (e.g. use shared pems when there are no org-specific ones).

Or did you indend to have a separate copy of the shared PEMs for each org that uses them?

@waynesun09
Copy link
Copy Markdown
Contributor

waynesun09 commented May 7, 2026

Superseded — line number references in this comment drifted after review-fix rounds on PR #503. See updated comment with corrected references.

Original (stale line numbers)

@ifireball Good question — let me ground this in the actual code from PR #503.

How PEMs are stored (provisioner)gcf.go:105-107:

func secretID(org, role string) string {
    return fmt.Sprintf("fullsend-%s-%s-app-pem", strings.ToLower(org), role)
}

How PEMs are looked up (mint)internal/mint/main.go:132:

name := fmt.Sprintf("projects/%s/secrets/fullsend-%s-%s-app-pem/versions/latest",
    projectNumber, strings.ToLower(org), role)

The org comes from the cryptographically signed JWT repository_owner claim (main.go:438), validated against the ALLOWED_ORGS list (main.go:496). Both paths are always org-scoped — there is no fallback to a "shared" namespace today.

For SaaS with shared Apps, there are two options:

Option A — Separate copy per org (no code changes): The platform operator stores the same shared App PEM under each org's namespace: fullsend-acme-coder-app-pem, fullsend-widgetco-coder-app-pem — same key material, different secret entries. The onboarding step just calls StoreAgentPEM(ctx, "acme", "coder", sharedPEM) for each org. Works with the current code as-is.

Option B — Fallback lookup (small code change): Add a fullsend-shared-{role}-app-pem convention. The mint tries fullsend-{org}-{role}-app-pem first, falls back to fullsend-shared-{role}-app-pem if not found. ~5 lines in mintToken(). Avoids PEM duplication but introduces a "shared" namespace concept.

My intent was Option A for simplicity — the PEM duplication is negligible (one Secret Manager entry per org, same key bytes) and avoids any special-case lookup logic.


Update (2026-05-07): Corrected file paths and org derivation. The mint was refactored from internal/dispatch/gcf/function/ to internal/mint/. Org is now derived from the JWT repository_owner claim (not GITHUB_ORG env var), validated against ALLOWED_ORGS. See PR #503 for the current implementation.

waynesun09 added a commit that referenced this pull request May 7, 2026
ADR 0027 (central token mint for secretless .fullsend) is being
finalized in PR #655 by ifireball. This branch's implementation
will reference that ADR once accepted.

Signed-off-by: Wayne Sun <gsun@redhat.com>
Renumber from 0027 to avoid collision with 0027-allowed-and-disallowed-tools.
Merge upstream/main for a clean base. Address review: precise ADR 0026
wording, explicit ADR 0008 reversal, expanded security and availability
consequences. Defer accepted-ADR status updates until this ADR is Accepted.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions
Copy link
Copy Markdown

fullsend review is working on this — view logs

@ifireball
Copy link
Copy Markdown
Contributor Author

Checklist: follow-ups when ADR 0029 is Accepted (not part of this PR)

Accepted ADRs stay immutable in Context / Decision / Consequences (ADR 0001, template). Only status, frontmatter status, and a short supersession / pointer to the successor are appropriate.

Living document

Item Action
docs/architecture.md Update narrative so “current truth” reflects central token mint, optional PAT compatibility mode, workflow_call shim path, and shared Apps — without requiring readers to diff ADR chains.

Accepted ADRs — status-only + link (wording to refine at accept time)

ADR Relationship to 0029 Suggested handling when 0029 is Accepted
0008workflow_dispatch + PAT Mechanism superseded once mint + workflow_call is the chosen default for that boundary. Set status to Superseded (or Deprecated if you reserve Superseded for full replacement). Add one line under Status + matching YAML: Superseded by ADR 0029 for the enrolled-repo → .fullsend dispatch pattern; historical rationale preserved below.
0007 — PEMs in .fullsend Partially revised — per-role Apps remain; default PEM placement / shared Apps + mint. Prefer Accepted + a single Status note: Partially superseded by ADR 0029 for default forge credential placement; per-role App model remains. If you want stricter hygiene, use Superseded only when a new ADR restates 0007 in full (not required here).
0026 — stage dispatcher Intent retained; 0008’s auth/trigger assumptions at the shim edge change under 0029. Accepted + Status note: Cross-repo trigger/auth assumptions updated by ADR 0029; stage-based decoupling and marker fan-out unchanged.
0014 (if still relevant) Installer / secrets layout Follow-on PR or spec when install flow is defined; optional Status pointer to 0029 + implementation ADR.

This PR (for reviewers)


If anything in the “Suggested handling” column should use Superseded vs Accepted + note differently, that can be decided at merge-to-main time when flipping 0029 to Accepted.

@ifireball ifireball changed the title docs: ADR 0027 — central token mint for secretless .fullsend docs: ADR 0029 — central token mint for secretless .fullsend May 10, 2026
@ifireball
Copy link
Copy Markdown
Contributor Author

@fullsend-ai-review Pushed ADR 0029 (docs/ADRs/0029-central-token-mint-secretless-fullsend.md) with expanded relationships and consequences. Please re-run the review agent on the latest head if useful.

@rh-hemartin The new Consequences spell out centralized mint blast radius and org-scoped installs; your mint-isolation question matches the deployment-profile / installation story already discussed in-thread.

@waynesun09 FYI the Proposed ADR is now numbered 0029 (0027 on main is allowed/disallowed tools); implementation-plan threads stay valid conceptually.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 10, 2026

Site preview

Preview: https://dbf52fe5-site.fullsend-ai.workers.dev

Commit: 164aee440137057fb0ebf587232d003efb44af84

@waynesun09
Copy link
Copy Markdown
Contributor

@ifireball Agreed — after thinking through what the three profiles actually mean from an org admin's perspective, going with B (role-only PEM naming, no org prefix) makes more sense. The only thing it eliminates is the ability to have private per-org Apps in a shared mint, which is a scenario we don't need to support.

PR #503 has been updated to implement this. Here's what changed:

Provisionergcf.go:132-134:

func secretID(role string) string {
    return fmt.Sprintf("fullsend-%s-app-pem", role)
}

Mintinternal/mint/main.go:139-140:

name := fmt.Sprintf("projects/%s/secrets/fullsend-%s-app-pem/versions/latest",
    s.gcpProjectNum, role)

PEMAccessor interfaceinternal/mint/main.go:292-294:

type PEMAccessor interface {
    AccessPEM(ctx context.Context, role string) ([]byte, error)
}

App slugsappsetup.AppSlug(role) now returns fullsend-{role} (no org prefix), which is also what enables --public Apps for multi-org installs.

The three profiles now look like this from an org admin's perspective:

Profile What the admin does What it means
Self-managed admin install --mint-project=my-proj myorg Deploys their own mint + Apps. Full control.
Bundled First org: same as self-managed. Additional orgs: admin install --mint-url=https://existing-mint.run.app Enterprise admin runs one mint for multiple orgs. Each org gets its own Apps but they share the mint. Apps can be --public so the same App is installed across orgs — one PEM per role in Secret Manager, not one per org.
SaaS Platform pre-provisions mint + public shared Apps. Org admin: installs the Apps and runs admin install --mint-url=... Platform operator manages the mint. Org admins just install pre-made Apps.

The key simplification: since PEMs are role-only (fullsend-coder-app-pem), onboarding a new org to a shared mint with public Apps requires zero Secret Manager work — the PEM is already there from when the App was created. The org admin just installs the App on their org and points at the mint URL.

waynesun09 added a commit that referenced this pull request May 10, 2026
- ~20 line thin callers → ~40 lines (actual)
- secrets: inherit → explicit secret passthrough (least-privilege)
- workflow_call nesting limit 10 → 4 (GitHub actual limit)
- ~60 scaffold files → ~80 (actual count)
- Token generation: remove stale "transitional PEM" paragraph,
  reflect current OIDC mint-token implementation
- ADR 0027 → ADR 0029 (token mint renumbered on PR #655)

Signed-off-by: Wayne Sun <gsun@redhat.com>
waynesun09 added a commit that referenced this pull request May 10, 2026
Token mint ADR renumbered from 0027 to 0029 on PR #655.

Signed-off-by: Wayne Sun <gsun@redhat.com>
waynesun09 added a commit that referenced this pull request May 10, 2026
Token mint ADR renumbered from 0027 to 0029 on PR #655.

Signed-off-by: Wayne Sun <gsun@redhat.com>
Copy link
Copy Markdown
Contributor

@waynesun09 waynesun09 left a comment

Choose a reason for hiding this comment

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

The implementation in PR #503 defaults to private per-org apps with an opt-in --public flag for multi-org, which diverges from this ADR's decision statement ("public, shared GitHub Apps as the default"). Suggest updating the decision to reflect the implemented behavior:

Default: private apps, one set per org. This is the safer default — blast radius is contained to a single org's installations. The --public flag opts in to multi-org shared apps when needed.

Private → public is a one-click change. GitHub App visibility is toggled under Settings → Advanced → Danger zone → "Make public". No re-registration or PEM rotation needed. Once public, other orgs install via the app's installation URL. Verified on the nonflux test apps — the toggle is available. (Note: public apps cannot be made private again if installed on other accounts.)

Role-only PEM naming enables both modes. PEMs are stored as fullsend-{role}-app-pem (no org prefix), so a single PEM serves all orgs sharing the same app. Additional orgs using --mint-url require zero Secret Manager work — the PEMs are already there.

Suggested wording for the Decision section:

Adopt a central token mint and per-role GitHub Apps (private by default, optionally public for multi-org) as the default way to give Fullsend agents forge identity...

  1. Per-role Apps. Each agent role gets a dedicated GitHub App. Apps are private to the creating org by default. For multi-org deployments, apps can be made public (unlisted) via --public at install time or toggled in GitHub App settings afterward.

This aligns the ADR with what shipped and preserves the option to go public when orgs are ready.

@ifireball
Copy link
Copy Markdown
Contributor Author

@waynesun09

The implementation in PR #503 defaults to private per-org apps with an opt-in --public flag for multi-org, which diverges from this ADR's decision statement ("public, shared GitHub Apps as the default"). Suggest updating the decision to reflect the implemented behavior:

Default: private apps, one set per org. This is the safer default — blast radius is contained to a single org's installations. The --public flag opts in to multi-org shared apps when needed.

Private → public is a one-click change. GitHub App visibility is toggled under Settings → Advanced → Danger zone → "Make public". No re-registration or PEM rotation needed. Once public, other orgs install via the app's installation URL. Verified on the nonflux test apps — the toggle is available. (Note: public apps cannot be made private again if installed on other accounts.)

Role-only PEM naming enables both modes. PEMs are stored as fullsend-{role}-app-pem (no org prefix), so a single PEM serves all orgs sharing the same app. Additional orgs using --mint-url require zero Secret Manager work — the PEMs are already there.

Suggested wording for the Decision section:

Adopt a central token mint and per-role GitHub Apps (private by default, optionally public for multi-org) as the default way to give Fullsend agents forge identity...

  1. Per-role Apps. Each agent role gets a dedicated GitHub App. Apps are private to the creating org by default. For multi-org deployments, apps can be made public (unlisted) via --public at install time or toggled in GitHub App settings afterward.

This aligns the ADR with what shipped and preserves the option to go public when orgs are ready.

This seems self contradictory, if, as you say, we implement the mint so that it can only store one set of apps (no org scoping for the PEMs) then only two modes are possible:

  1. Private per-org app + per-org mint
  2. Public shared apps + multi-org mint

The so called "bundled" option of a central mint with multiple per-org private app sets cannot exist in this mode (Nor do I think we need it, if an org wants its own apps -> it should deploy its own mint).

gklein pushed a commit to gklein/fullsend that referenced this pull request May 11, 2026
Move event-to-stage routing from the per-target-repo shim into
dispatch.yml. The shim shrinks from 8 jobs (~380 lines) to 2 jobs
(~50 lines). New stages require zero changes to enrolled repos.

Sequenced after the token mint migration (ADR 0027 / PR fullsend-ai#655)
which provides the workflow_call mechanism this ADR uses.

Signed-off-by: Wayne Sun <gsun@redhat.com>
@waynesun09
Copy link
Copy Markdown
Contributor

@ifireball You're right — the "bundled" concept doesn't hold up with role-only PEM naming. Dropping it.

With fullsend-{role}-app-pem (no org prefix), one mint holds exactly one PEM per role. That means:

Mode Apps Mint How
Self-managed Private, per-org Per-org mint (own GCP project) admin install myorg --mint-project=my-proj
Multi-org shared Public (unlisted), shared One shared mint First org deploys mint + creates public apps. Additional orgs: admin install --mint-url=https://existing-mint.run.app --public — installs the same public apps, no Secret Manager work needed

"Bundled" (one mint, multiple orgs, private apps) is not supported — and shouldn't be. Private apps can't be installed on other orgs, and role-only PEMs mean one mint can only hold one PEM per role. Two private orgs = two different app PEMs for the same role = doesn't fit in one mint.

If one admin manages two orgs with private apps, they deploy two separate mints in different GCP projects. That's just self-managed applied twice, not a distinct mode.

Infrastructure resource names (fullsend-mint, fullsend-dispatch, fullsend-pool) are also hardcoded per GCP project, so two mints can't coexist in the same project regardless.

Suggested update to the ADR: remove the three-tier deployment model (self-managed / bundled / SaaS) and describe two modes:

  1. Self-managed — private apps, own mint, single org
  2. Multi-org shared — public apps, shared mint, multiple orgs

This matches what the implementation actually supports and avoids confusing readers with a "bundled" concept that doesn't exist.

ralphbean added a commit that referenced this pull request May 11, 2026
ADRs 0029 and 0033 haven't merged yet — link to their PRs (#655, #707)
so the markdown link linter passes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ifireball
Copy link
Copy Markdown
Contributor Author

@ralphbean can you remove the "request changes" flag so we can merge this?

Copy link
Copy Markdown
Contributor

@ralphbean ralphbean left a comment

Choose a reason for hiding this comment

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

The previous CHANGES_REQUESTED items are all addressed in the current head:

  • ADR 0026 characterization is now precise (preserves dispatch intent, only auth assumptions change)
  • ADR 0008 reversal is explicit — both in the Context paragraph and the new "Relationship to prior ADRs" section
  • Renumbered to 0029 to avoid collision
  • Consequences section now includes specific blast-radius and SPOF bullets

One deferred note inline on the "public, shared Apps as default" wording vs. the Phase 1 implementation (raised by @waynesun09). Not blocking — but should be resolved before this ADR is accepted.


## Decision

Adopt a **central token mint** and **public, shared GitHub Apps** as the default way to give Fullsend agents forge identity, so the `.fullsend` repository needs **no long-lived secrets** for LLM access or App private keys.
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.

[moderate, deferred] The Decision section says "public, shared GitHub Apps" as the default, but the Phase 1 implementation (PR #503) ships private, per-org apps as the default, with --public as an opt-in flag. @waynesun09's review and implementation plan comment both flag "Shared public Apps" as explicitly deferred.

This isn't blocking for a Proposed ADR, but before acceptance the Decision wording should either:

  • (a) Align with what shipped: "per-role GitHub Apps (private by default, optionally public for multi-org)" — @waynesun09 has suggested specific replacement wording in their review.
  • (b) Be explicit about intent vs. current state: If shared public Apps are the architectural target and Phase 1 is a deliberate partial realization, say so here so future readers understand the delta.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I have changed the language to explicitly state ultimate intended outcome, v.s. shipped intermediate states, and clarified language about multi-org mints to say that those would expose public apps (because an app cannot be multi-org otherwise) rather then multiple sets of private single-org apps.

@github-actions
Copy link
Copy Markdown

fullsend review is working on this — view logs

Copy link
Copy Markdown
Contributor

@waynesun09 waynesun09 left a comment

Choose a reason for hiding this comment

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

ADR 0029 looks good. Ralph's deferred feedback on public/shared vs. private/per-org app wording is addressed — Decision section now clearly separates normative end-state from phased rollout reality, with self-managed as a first-class indefinite option.

@waynesun09 waynesun09 added this pull request to the merge queue May 13, 2026
Merged via the queue into fullsend-ai:main with commit 44c270c May 13, 2026
47 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants