Skip to content
Closed
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
14 changes: 14 additions & 0 deletions docs/ADRs/0033-per-repo-installation-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,20 @@ Use one GitHub App for triage, code, review, and fix roles to simplify per-repo

**Rejected**: GitHub suppresses events triggered by pushes made with any `GITHUB_TOKEN` or GitHub App installation token, to prevent infinite loops. Two separate Apps work because a push made with App-A's token _does_ generate events that trigger workflows authenticated as App-B. The fix→review loop requires the coder/fix agent to push commits that trigger review — if both roles share one App, the push token matches the workflow's App and the event is silently suppressed, breaking the feedback cycle. At minimum, coder and review must be separate Apps.

_Clarification (2026-07):_ GitHub's event-suppression is specific to the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[MEDIUM] ADR 0033 annotation substantially re-argues the superseded rationale instead of a short clarifying note

The block added at lines 69-81 doesn't just cross-reference ADR 0075 — it re-derives the technical reasoning and explicitly states the original "Rejected" verdict for Alternative 2 rested on an incorrect premise ("the fix→review loop this alternative worried about is not actually broken by sharing one App", "not because sharing one App is technically broken"). This appears technically accurate (GitHub's documented event-suppression is specific to GITHUB_TOKEN and does exempt App tokens/PATs), but it goes well beyond a "short note" or "clarifying remark." It's inconsistent with this repo's own ADR-immutability convention: ADR 0029 states "Once this ADR is Accepted, its content is frozen. Do not edit the Context, Decision, or Consequences sections... Only status changes and links to superseding ADRs should be added," and this same PR's new ADR 0075 codifies an equivalent norm in its own template comment ("Minor annotations are welcome... However, do not substantially rewrite the Context, Decision, or Consequences sections"). ADR 0033 itself carries no such comment, so nothing is technically violated, but the edit sets a precedent inconsistent with the standard this very PR introduces elsewhere.

Suggestion: Trim the ADR 0033 annotation to a short pointer (e.g. "Clarification (2026-07): the GITHUB_TOKEN-suppression reasoning above was imprecise — see ADR 0075 for the corrected analysis") and keep the full technical re-argument only in ADR 0075's own Context, which already restates it.

built-in `GITHUB_TOKEN`; App installation tokens and PATs are documented as
exempt, so a shared App's pushes/labels would trigger downstream workflows
normally — the fix→review loop this alternative worried about is not
actually broken by sharing one App. The one thing a single App genuinely
cannot do is approve-and-merge a PR it authored (`422 Can not approve your
own pull request`), regardless of token type. A single App can still cover
triage, code, and review as long as review stops at a `COMMENT`-type verdict
and a human merges — see [ADR 0075](0075-lite-auth-mode.md),
which documents exactly this as the opt-in path for per-repo Lite Auth
Mode. Separate Apps per role remain the right call at org scale, but for
[ADR 0007](0007-per-role-github-apps.md)'s least-privilege/blast-radius
reasoning, not because sharing one App is technically broken.

### Alternative 3: Per-repo as a separate codebase

Build a standalone per-repo tool or action that does not share infrastructure with per-org fullsend.
Expand Down
61 changes: 61 additions & 0 deletions docs/ADRs/0075-lite-auth-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[HIGH] ADR number 0075 collides with open PR #5562, which independently also claims 0075

Verified via gh pr view 5562: open PR #5562 ("fix(telemetry)!: respect OTEL variables default behaviour", updated 2026-07-28) adds docs/ADRs/0075-diataxis-documentation-quadrants.md. Both this PR and #5562 claim ADR number 75 with different filename slugs, so git will not flag a conflict at merge time — if both merge, docs/ADRs/ ends up with two files numbered 75. This is exactly the failure mode CONTRIBUTING.md's "ADR numbering" section exists to catch ("Before merging, use the /renumber-adr skill to check whether your ADR number is still available on the target branch and renumber if needed"), backed by skills/renumber-adr/scripts/inflight-adr-numbers.sh. Also verified: open PR #5611 separately claims ADR 0074, and open PR #5685 separately claims ADR 0076 — so 74/75/76 are simultaneously contested by three different open PRs right now, meaning even a naive renumber isn't safe without re-checking at actual merge time.

Suggestion: Run the /renumber-adr skill (or skills/renumber-adr/scripts/inflight-adr-numbers.sh) before merge and take the next free number, re-verifying at merge time since other ADR PRs may land in the meantime.

title: "75. Lite auth mode"
status: Accepted
relates_to:
- agent-architecture
- security-threat-model
topics:
- identity
- per-repo
- least-privilege
---

# 75. Lite auth mode

Date: 2026-07-22

## Status

Accepted

<!-- ADRs are point-in-time records, but not fully frozen after acceptance.
Minor annotations are welcome: cross-references to related ADRs, short
notes linking to newer decisions, or clarifying remarks. However, do not
substantially rewrite the Context, Decision, or Consequences sections. If
the decision itself needs to change, write a new ADR that supersedes this
one. For evolving design narrative, use docs/architecture.md. -->

## Context

Per-repo installation ([ADR 0033](0033-per-repo-installation-mode.md)) historically required provisioning per-role GitHub Apps ([ADR 0007](0007-per-role-github-apps.md)) and a token mint ([ADR 0029](0029-central-token-mint-secretless-fullsend.md)), creating nontrivial setup friction. While the Per-role App + Mint architecture remains the default for scaled, highly isolated environments, some per-repo adopters need a zero-ceremony opt-in path using the default `secrets.GITHUB_TOKEN`.

A design spike investigated using `secrets.GITHUB_TOKEN` (`github-actions[bot]`). Because GitHub suppresses events triggered by `GITHUB_TOKEN`, standard stage handoffs (`labeled`, `pull_request_review.submitted`) cannot initiate subsequent workflow steps natively. However, `workflow_dispatch` is exempt from event suppression and can be invoked securely to chain stages without over-privileged scopes (maintaining `contents: read` for triage and review, per `internal/mintcore/github.go`).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[MEDIUM] internal/mintcore/github.go citation implies mint enforcement that Lite Auth Mode explicitly bypasses

Context justifies keeping contents:read for triage/review "per internal/mintcore/github.go" — but Decision item 1 states Lite Auth Mode uses the default GITHUB_TOKEN "instead of a mint-provided token," i.e. the mint (and its Go role-permission map) is bypassed entirely in this mode. Verified internal/mintcore/github.go's canonicalRolePermissions: triage and review do show contents:read there (so that part happens to coincidentally hold), but that map governs mint-issued GitHub App installation tokens (the default, non-Lite mode) and has no "actions" key at all for either role. The actual enforcement point for Lite Auth Mode's GITHUB_TOKEN is verified to be the workflow-level permissions: blocks already present in internal/scaffold/fullsend-repo/.github/workflows/triage.yml and review.yml (contents: read, actions: write, id-token: write, issues: write, plus pull-requests: write for review) — a mechanism unrelated to, and not covered by, mintcore's canonicalRolePermissions map cited here.

Suggestion: Cite internal/scaffold/fullsend-repo/.github/workflows/{triage,review}.yml's permissions: blocks as the actual source for the GITHUB_TOKEN scopes, and reword the internal/mintcore/github.go reference to signal design parity ("matching the same read/write split as...") rather than a shared enforcement path, since Lite Auth Mode never calls the mint.


Additionally, native auto-merge requires `APPROVE` reviews, which fail with a 422 error if the same identity that authored the PR (e.g., `GITHUB_TOKEN`) tries to approve it. Exchanging a Bring-Your-Own-App (BYOA) PEM for a separate token bypasses this restriction, but exposes the PEM to prompt-injection or runner-compromise threats if not properly isolated.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[HIGH] "BYOA" acronym collides with the established "Bring Your Own Agent" term

This ADR defines "BYOA" to mean "Bring-Your-Own-App"/"Bring Your Own App" (a dedicated Reviewer GitHub App) at lines 34, 42, 50, and 51, echoed in docs/architecture.md:185 ("a BYOA escape hatch"). But "BYOA" is already a heavily established acronym in this repo meaning "Bring Your Own Agent." Verified: docs/roadmap.md uses "BYOA" as a bolded, linked heading and literally the team's #1 "Now" priority category ("| 1 | BYOA | Agent catalog, harness triggers, config knobs, shareable config profiles | Now |", plus a full "### BYOA" section describing it as "the team's highest priority for July"). The underlying concept also has a dedicated guide (docs/guides/user/bring-your-own-agent.md) referenced from docs/glossary.md and from ADRs 0058, 0065, and 0036. Reusing the same acronym for an unrelated GitHub-App-credential mechanism will collide with anyone searching docs/glossary or cross-referencing the roadmap's top-priority term.

Suggestion: Rename the new concept to avoid the collision — e.g. "Bring Your Own Reviewer (App)" or "BYO Reviewer App" — in both docs/ADRs/0075-lite-auth-mode.md and the docs/architecture.md bullet.


## Options

- **`GITHUB_TOKEN` with native event triggers:** Rejected because GitHub suppresses downstream workflows triggered by `GITHUB_TOKEN` events.
- **`GITHUB_TOKEN` with `repository_dispatch` handoffs:** Rejected because `repository_dispatch` requires `contents: write` on triage/review, violating least-privilege role definitions.
- **`GITHUB_TOKEN` with `workflow_dispatch` handoffs:** Chosen. Only requires `actions: write` (already held), preserving `contents: read` for triage/review.
- **Single self-owned App with native handoffs (no mint):** Rejected for default lite mode because storing the PEM as a static secret introduces standing-credential risk.
- **Bring Your Own App (BYOA) for Review:** Chosen for auto-merge. Bypasses the 422 self-approval error by using a dedicated Reviewer App token for the review approval step.

## Decision

Introduce **Lite Auth Mode** as an opt-in alternative to the default Per-role GitHub App + Mint architecture for per-repo installations. In Lite Auth Mode:

1. **Authentication:** The default `GITHUB_TOKEN` is used for triage, code, and review steps instead of a mint-provided token.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[MEDIUM] Stage-coverage list omits fix/retro, inconsistent with the rest of the ADR

Decision item 1 says the default GITHUB_TOKEN "is used for triage, code, and review steps" — omitting fix and retro. But Decision item 2 (line 49) calls for updating post-retro.sh, implying retro is in-pipeline under Lite Auth Mode, and Consequences (line 58) states "only code and fix carry contents: write," implying fix is in-pipeline too. It's never stated whether fix and retro also run under the default GITHUB_TOKEN — which is needed for the "zero GitHub-credential secrets" claim in Consequences (line 55) to actually hold across the full pipeline — or authenticate some other way.

Suggestion: Enumerate all pipeline stages Lite Auth Mode covers (triage, code, review, fix, retro) consistently everywhere a stage list appears in this ADR, or explicitly scope fix/retro out and explain their authentication path.

2. **Handoffs:** Stage handoffs use `workflow_dispatch` (`gh workflow run`) rather than `repository_dispatch` or native events. This requires only `actions: write`, allowing triage and review to maintain least-privilege `contents: read`. `post-review.sh` and `post-retro.sh` will also be updated to use `workflow_dispatch` to close pipeline loops.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[HIGH] ADR assigns post-review.sh/post-retro.sh work to a repo where those scripts no longer exist

Decision item 2 states: "post-review.sh and post-retro.sh will also be updated to use workflow_dispatch to close pipeline loops." Verified directly against origin/main: internal/scaffold/fullsend-repo/scripts/ contains no post-review.sh or post-retro.sh (only pre-code.sh, pre-fix.sh, pre-fetch-prior-review.sh, reconcile-repos.sh, validate-source-repo.sh, install-precommit-tools.sh, setup-prioritize.sh). Both scripts were migrated to fullsend-ai/agents per ADR 0058 Phase 4: docs/plans/agent-extraction-to-agents-repo.md's migration table lists scripts/post-review.sh and scripts/post-retro.sh as "Copy as-is", docs/problems/review-autonomy-evidence.md:156 explicitly says "the existing protected-path downgrade in post-review.sh (now in fullsend-ai/agents)", and internal/cli/run.go has a comment stating "...agent scripts now live in that repo, not internal/scaffold/fullsend-repo/... post-review.sh/post-triage.sh/post-retro.sh/post-prioritize.sh still scan for the last iteration-*/output blindly" (referencing fullsend-ai/agents#411). The ADR is silent about this repo split, so an implementer following Decision item 2 literally would look for (or try to edit) files that no longer live in fullsend-ai/fullsend. By contrast, the other follow-up item in Consequences (line 59, reusable-dispatch workflow and shim configuration) correctly remains fullsend-ai/fullsend's responsibility.

Suggestion: Add a note to Decision item 2 (or Consequences) that the post-review.sh/post-retro.sh workflow_dispatch updates are fullsend-ai/agents-side follow-up work, tracked separately per the ADR-0058 Phase 4 repo split.

3. **Auto-Merge (BYOA):** To bypass GitHub's self-approval 422 restriction, operators can provide a dedicated "Reviewer" App ID and PEM as repository secrets (`FULLSEND_REVIEWER_APP_ID`, `FULLSEND_REVIEWER_APP_PEM`).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[MEDIUM] BYOA Reviewer App's required permission scope for satisfying branch-protection approvals is unspecified

Decision item 3 has operators provision a dedicated "Reviewer" App ID/PEM but never states what GitHub permission level that App needs (e.g. contents:write). This matters: open PR #2791 ("docs: ADR 0071 — Auto-merge") states as a verified key finding, "GitHub only counts approvals toward branch protection when the reviewer has contents: write — our review bot has contents: read, so its approvals are currently informational only." Verified internal/mintcore/github.go's canonicalRolePermissions["review"] is indeed {"contents": "read", "pull_requests": "write", ...}. If an operator provisions the Reviewer App with that same low-privilege convention (the natural default, since it stands in for the review role), its APPROVE review may not count toward required-approvals at all, silently defeating the auto-merge escape hatch this ADR introduces. Separately, ADR 0075 and the parallel, still-open ADR 0071 proposal (PR #2791) are two independent, non-cross-referenced designs solving the identical self-approval/auto-merge restriction via a separate App identity.

Suggestion: Specify the required permission scope for the Reviewer App (explicitly call out contents:write if that's what's needed for the approval to count toward branch protection), and cross-reference PR #2791/ADR 0071 so the two auto-merge designs are reconciled before either is implemented.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[MEDIUM] FULLSEND_REVIEWER_APP_ID described as a secret, contradicting the established App-ID-as-variable convention

Decision item 3 has operators provide "a dedicated 'Reviewer' App ID and PEM as repository secrets (FULLSEND_REVIEWER_APP_ID, FULLSEND_REVIEWER_APP_PEM)." Verified this contradicts the existing, implemented convention: ADR 0007 states "Private keys (PEMs) are stored as repo-level secrets... Client IDs are stored as repo-level variables," and ADR 0014 codifies exactly this as repository secrets FULLSEND_<ROLE>_APP_PRIVATE_KEY plus variables FULLSEND_<ROLE>_CLIENT_ID — a pattern actually implemented in internal/layers/secrets.go (with matching unit tests) and exercised today by .github/workflows/reusable-review.yml, which reads vars.FULLSEND_REVIEW_CLIENT_ID. A GitHub App ID isn't sensitive and doesn't need secrets-manager handling — only the private key does. The naming also diverges from the established vocabulary (_CLIENT_ID/_APP_PRIVATE_KEY, not _APP_ID/_APP_PEM) and from the canonical role spelling used elsewhere ("review"/"REVIEW", e.g. FULLSEND_REVIEW_CLIENT_ID) — this ADR instead uses "REVIEWER".

Suggestion: Store the App ID as a repository variable (e.g. vars.FULLSEND_REVIEWER_CLIENT_ID) rather than a secret, and align naming with the FULLSEND_<ROLE>_APP_PRIVATE_KEY / FULLSEND_<ROLE>_CLIENT_ID pattern (and the existing "REVIEW" role spelling) unless there's a stated reason this new credential surface should diverge.

4. **BYOA Security Architecture:** To mitigate prompt-injection threats, the Reviewer App token **MUST** be minted in an isolated workflow step or job *after* the LLM agent has safely exited, ensuring the PEM is never exposed to the LLM execution context.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[HIGH] Reviewer-App-token isolation requirement contradicts itself on step vs. job, and "step" alone doesn't defend the named threat

Decision item 4 requires the Reviewer App token be minted "in an isolated workflow step or job" after the LLM agent exits — but Consequences (line 57) narrows the same claim to only "a post-execution job," eliminating "step" as an option. The ADR contradicts itself on its own central security control. This isn't pedantic: Context (line 34) names "runner-compromise" as one of the two threats being defended against, and GitHub Actions steps within a single job share the same runner filesystem/process environment as every prior step in that job — a later step does not isolate the PEM from a compromised or prompt-injected agent that ran earlier in the same job; only a separate job (fresh runner) does. As written, an implementation can satisfy the letter of the MUST clause ("a step... after the agent has safely exited") while remaining exposed to the exact threat the ADR names, and the Consequences section's stronger claim ("eliminates the threat") is not actually guaranteed by what Decision item 4 requires.

Suggestion: Resolve the contradiction by requiring a separate job specifically (drop "step" as a satisfying option) in Decision item 4, matching what Consequences already claims and reserving any step-level option for steps that themselves run in a freshly provisioned container/runner.


## Consequences

- Per-repo installations can adopt Lite Auth Mode with zero GitHub-credential secrets, though GCP inference credentials are still required.
- Auto-merging PRs requires provisioning a dedicated Reviewer App to bypass the 422 self-approval restriction.
- Isolating the Reviewer App token exchange in a post-execution job eliminates the threat of exposing the PEM to an untrusted LLM context.
- Triage and review jobs maintain least-privilege `contents: read` scopes, while only code and fix carry `contents: write`.
- The reusable-dispatch workflow and shim configurations require updating to handle `workflow_dispatch` inputs and routing hints.
- Per-role least-privilege scoping is lost at the identity level, but GHA job-level permissions still restrict the ephemeral `GITHUB_TOKEN`.
- Lite Auth Mode remains a strictly opt-in mechanism, preserving the Per-role GitHub App + Mint mode as the default for scaled installations.
1 change: 1 addition & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ Identity is not the same as trust. An agent's identity lets it authenticate to e
- Standalone mint deployment: `cmd/mint/` provides a self-contained HTTP server that uses direct JWKS verification and filesystem PEM storage instead of GCP infrastructure. It shares the `internal/mintcore/` library with the GCF mint and adds support for custom role permissions and a fallback proxy to an upstream mint. Custom role permissions live in mintcore (not `cmd/mint/`) so that `RolePermissionsFor`, `HasRole`, and `CreateInstallationToken` return a unified view without callers needing to distinguish built-in from custom roles. The GCF mint never calls `RegisterCustomRolePermissions`, so the code is inert there. See the [standalone mint guide](guides/infrastructure/standalone-mint.md).
- Hosted public community mint: steady-state deployment on Cloudflare Workers (JWKS + WAF + single ops console), with interim GCP Cloud Function acceptable until the Worker port is production-ready. Trust policy (`ALLOWED_ORGS=*`, upstream-only workflow provenance) is in [ADR 0059](ADRs/0059-public-mint-mode-with-wildcard-allowlists.md); deployment, edge security, monitoring, and phasing are in [ADR 0068](ADRs/0068-public-community-mint-architecture.md). Enrollment is installing the shared Apps—no per-org mint env registration ([#1145](https://github.com/fullsend-ai/fullsend/issues/1145)).
- Named privilege levels: each role defines ordered named levels (`read`, `write`), where each level's permissions are a superset of preceding levels. `read` for built-in roles is derived by downgrading `*:write` permissions to their `read` counterparts. The mint API accepts an optional `level` field (default `read`); omitting it produces narrower tokens than the current behavior. `write` is defined as the current max permission set for each built-in role. `CUSTOM_ROLE_PERMISSIONS` auto-detects a multi-level JSON shape alongside the existing flat format, with mixed format supported per role. The harness `privilege_levels` flag maps run-stages to levels; omitting it defaults to `write`, preserving backward compatibility for existing harness configurations ([ADR 0073](ADRs/0073-named-mint-privilege-levels.md)).
- Single-identity Lite Auth Mode: an opt-in alternative to the default Per-role GitHub App + Mint architecture for per-repo installations. Uses the default `GITHUB_TOKEN` for triage, code, and review, with `workflow_dispatch` (not label/review events) for stage handoffs — chosen over `repository_dispatch` because it needs only `actions: write`, letting triage and review keep `contents: read` per the canonical Go role definitions; merging requires a human approval by default — a BYOA escape hatch for fully autonomous auto-merge is available. Trades away per-role least-privilege scoping at the identity level; org-scoped or highly-automated installs use the default full App + mint stack ([ADR 0075](ADRs/0075-lite-auth-mode.md)).

One concrete implementation option is [`oidcx`](https://github.com/oxidecomputer/oidcx): a service that accepts OIDC identity tokens and exchanges them for short-lived access tokens. It can mint tokens scoped to selected GitHub repositories and permissions, or to selected Oxide silos and permissions, and it also ships with a GitHub Action wrapper. In a Fullsend deployment, this can be used by the sandbox entrypoint to narrow a broad GitHub App identity down to only the specific permissions an agent needs for the current run.

Expand Down
Loading