From f3dc56aecf3794d1975149d1dd9d0b24606b51e6 Mon Sep 17 00:00:00 2001 From: Wayne Sun Date: Wed, 6 May 2026 16:29:47 -0400 Subject: [PATCH 01/11] docs: add ADR 0031 per-repo installation mode Proposes a per-repo installation mode where fullsend runs entirely within a single repository, building on ADR 0030 (reusable workflows) and ADR 0027 (central token mint). Enables users without org admin access to adopt fullsend by adding a single ~30-line workflow file. Signed-off-by: Wayne Sun --- docs/ADRs/0031-per-repo-installation-mode.md | 275 +++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 docs/ADRs/0031-per-repo-installation-mode.md diff --git a/docs/ADRs/0031-per-repo-installation-mode.md b/docs/ADRs/0031-per-repo-installation-mode.md new file mode 100644 index 0000000000..68b3fa4785 --- /dev/null +++ b/docs/ADRs/0031-per-repo-installation-mode.md @@ -0,0 +1,275 @@ +--- +title: "0031. Per-repo installation mode" +status: Proposed +relates_to: + - agent-infrastructure + - agent-architecture + - security-threat-model +topics: + - installation + - per-repo + - reusable-workflows + - distribution + - github-apps +--- + +# 0031. Per-repo installation mode + +Date: 2026-05-06 + +## Status + +Proposed + +## Context + +Fullsend's installation model is per-org: `fullsend admin install` creates a dedicated `.fullsend` config repo, per-role GitHub Apps ([ADR 0007](0007-per-role-github-apps.md)), an org-level dispatch PAT ([ADR 0008](0008-workflow-dispatch-for-cross-repo-dispatch.md)), and shim workflows in enrolled repos. This requires org admin access and assumes all enrolled repos share agent configuration, credentials, and policies. + +Some users cannot or do not want to use the per-org model: + +1. **No org admin access** — contributors who admin specific repos but not the GitHub org. +2. **No sharing desired** — teams who want isolated agent configs, credentials, and billing for a single repo. +3. **Quick evaluation** — users who want to try fullsend on one repo without committing to org-wide setup. +4. **Personal repos** — individual developers on personal GitHub accounts (no org at all). + +Two proposed ADRs create the building blocks that make per-repo possible: + +- [ADR 0030](0030-reusable-workflows-for-action-installed-distribution.md) publishes reusable workflows and a root composite action from `fullsend-ai/fullsend`, enabling any repo to call fullsend infrastructure via `workflow_call` without copying workflow files. +- [ADR 0027](0027-central-token-mint-secretless-fullsend.md) introduces a central token mint with shared GitHub Apps, eliminating PEM secrets from config repos via OIDC-based credential issuance. + +Combined, these make per-repo installation viable: a single ~30-line workflow file in the target repo, calling upstream reusable workflows, with credentials stored as repo-level secrets or obtained via the token mint. + +## Options + +### Alternative 1: Per-repo via scaffold copy + +Run `fullsend admin install` targeting a single repo instead of an org. Copy all scaffold files (agent workflows, composite action, dispatcher, scripts) into the target repo. + +**Rejected**: Same maintenance burden as per-org — the repo must re-run install to pick up upstream patches. Contradicts ADR 0030's motivation to eliminate workflow drift. + +### Alternative 2: Single GitHub App for all roles + +Use one GitHub App for triage, code, review, and fix roles to simplify per-repo setup. + +**Rejected**: GitHub suppresses `pull_request_target` events when the triggering token belongs to the same App that owns the workflow. The fix→review loop requires the coder/fix agent to push commits that trigger review — if both roles share one App, the event is silently suppressed and the feedback cycle breaks. At minimum, coder and review must be separate Apps. + +### 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. + +**Rejected**: Duplicates agent logic, composite action, and security controls. Per-repo should reuse the same reusable workflows as per-org, with mode detection to adapt behavior. + +### Alternative 4: Two-app minimum (coder + review) + +Reduce per-repo to two Apps instead of matching the full per-org app set. + +**Rejected**: Dropping the triage App forces triage to share one of the other App identities, which conflates permissions (triage only needs `issues:write`, while coder has `contents:write`). The full per-role model (ADR 0007) provides least-privilege isolation. CLI automation (`fullsend init`) makes creating the Apps straightforward. + +## Decision + +### Overview + +Add a **per-repo installation mode** where fullsend runs entirely within a single repository — no `.fullsend` config repo, no cross-repo dispatch, no org-level secrets. The target repo IS the config repo. + +Per-repo reuses the reusable workflows from ADR 0030, adding one new artifact: `reusable-fullsend.yml`, an all-in-one routing and dispatch workflow that combines event-to-stage routing (currently in the ~380-line shim) with per-stage dispatch into a single `workflow_call` entry point. + +### 1. Architecture + +``` +Per-org (current): + +ENROLLED REPO .FULLSEND CONFIG REPO +───────────── ───────────────────── +fullsend.yml (shim, ~380 lines) dispatch.yml → stage workflows + │ workflow_dispatch (PAT) │ + └──────────────────────────────────────┘ + +Per-repo (proposed): + +TARGET REPO (self-contained) +──────────────────────────── +.github/workflows/fullsend.yml (~30 lines, thin caller) + │ + │ workflow_call (native, no PAT) + └──> fullsend-ai/fullsend/.github/workflows/reusable-fullsend.yml@v1 + ├── routes event to stage + ├── skips enrollment validation (per-repo mode) + ├──> reusable-triage.yml ─┐ + ├──> reusable-code.yml ─┤── reusable workflows (ADR 0030) + ├──> reusable-review.yml ─┤ + └──> reusable-fix.yml ─┘ + │ + uses: fullsend-ai/fullsend@v1 (composite action) + config: .fullsend/ directory in target repo +``` + +Per-repo requirements: repo admin, 3 GitHub Apps (triage + coder + review), GCP project for inference. No org admin, no dispatch PAT, no dedicated config repo. + +### 2. Repo layout + +``` +target-repo/ +├── .github/workflows/fullsend.yml ← single workflow file (~30 lines) +├── .fullsend/ ← in-repo config (optional) +│ ├── agents/ ← agent prompt overrides +│ ├── harness/ ← harness config overrides +│ ├── policies/ ← sandbox policies +│ ├── skills/ ← repo-specific skills +│ ├── scripts/ ← pre/post scripts +│ └── config.yaml ← repo-level config +├── AGENTS.md +└── ... (source code) +``` + +The `.fullsend/` directory is optional. Without it, upstream defaults apply. Users add files to `.fullsend/` only to customize agent behavior. + +### 3. Config layering + +Per-repo collapses the three-tier config model to two tiers: + +``` +fullsend-ai/fullsend defaults < .fullsend/ directory < AGENTS.md +(base) (customize) (instructions) +``` + +The org-level `.fullsend` config repo tier is skipped — the in-repo `.fullsend/` directory serves as both org and repo config. + +### 4. The `reusable-fullsend.yml` workflow + +This is the key new artifact, published in `fullsend-ai/fullsend/.github/workflows/`. It accepts event metadata via `workflow_call` inputs, routes events to stages using the same logic currently embedded in the shim, and conditionally dispatches to per-stage reusable workflows. + +The routing logic maps: +- `issues` + `labeled` → stage based on label name (`ready-to-code` → code, `ready-for-review` → review) +- `issue_comment` + slash commands → `/triage`, `/code`, `/review`, `/fix` +- `pull_request_target` → review (or retro on close) +- `pull_request_review` + `changes_requested` from bot → fix + +This workflow serves both per-repo and per-org simplified shims. Per-org thin shims (from ADR 0030) can also use it to replace the ~380-line shim + dispatcher. + +**Nesting depth**: target-repo workflow → `reusable-fullsend.yml` → `reusable-code.yml` = 2 levels of `workflow_call` (GitHub limit is 4). + +### 5. Per-repo mode detection + +Reusable workflows detect per-repo mode when `source_repo == github.repository` — the calling repo IS the target repo. + +In per-repo mode: +- Enrollment validation is skipped (always self-enrolled). +- A single checkout retrieves both config (`.fullsend/` subdirectory) and code (repo root). +- `fullsend run` receives `--fullsend-dir=.fullsend` and `--target-repo=.`. + +In per-org mode: +- Enrollment is validated against `config.yaml`. +- Two checkouts: `.fullsend` repo (config), then target repo into `target-repo/`. +- `fullsend run` receives `--fullsend-dir=.` and `--target-repo=target-repo`. + +### 6. Credential models + +Per-repo supports two credential models: + +**Model A: Per-role Apps (own)** + +Same as per-org ([ADR 0007](0007-per-role-github-apps.md)), but Apps are user-owned and installed on specific repos. Each role gets its own GitHub App: + +| App | Role | Key permissions | +|-----|------|-----------------| +| `{user}-fullsend` | Orchestrator | `actions:write`, `contents:write`, `workflows:write`, `administration:write` | +| `{user}-triage` | Triage | `issues:write` | +| `{user}-coder` | Code + fix | `contents:write`, `pull-requests:write`, `issues:read`, `checks:read` | +| `{user}-review` | Review | `contents:read`, `pull-requests:write`, `issues:read`, `checks:read` | + +The orchestrator App is optional for per-repo (it handles enrollment reconciliation, which does not apply). Per-repo users need at minimum triage, coder, and review. + +PEMs stored as repo secrets, client IDs as repo variables. + +**Model B: Token mint + shared Apps ([ADR 0027](0027-central-token-mint-secretless-fullsend.md))** + +Shared public fullsend Apps installed on the repo. Token mint handles credential issuance via OIDC. No PEMs in the repo — the mint holds them. + +The mint's `job_workflow_ref` validation accepts both patterns: +- `{org}/.fullsend/.github/workflows/*.yml@*` (per-org) +- `fullsend-ai/fullsend/.github/workflows/reusable-*.yml@*` (per-repo) + +The `repository_owner` claim still scopes tokens to the calling org/user. + +**Credential auto-detection**: Reusable workflows detect the credential model automatically: +- `FULLSEND_MINT_URL` present → mint mode (OIDC token exchange) +- `FULLSEND_CODER_APP_PRIVATE_KEY` + `FULLSEND_REVIEW_APP_PRIVATE_KEY` present → per-role App mode +- Neither → error with setup instructions + +### 7. CLI support: `fullsend init` + +A new CLI command for per-repo setup: + +``` +fullsend init [--mint-url URL] [--skip-orchestrator] +``` + +1. Generates `.github/workflows/fullsend.yml` from template. +2. Guides user through GitHub App creation via manifest flow — creates triage, coder, and review Apps. +3. Stores PEMs as repo secrets, client IDs as repo variables. +4. Optionally creates `.fullsend/` directory with default config. + +With `--mint-url`, App creation is skipped — the user installs shared Apps instead. + +### 8. Coexistence + +Per-repo and per-org coexist within the same org. Some repos use the org `.fullsend` config repo (per-org), others run independently (per-repo). There is no conflict — they use different dispatch paths and credential stores. + +Migration between models is straightforward: +- **Per-repo → per-org**: Remove workflow file from target repo, add to `.fullsend/config.yaml` enrollment. +- **Per-org → per-repo**: Remove from enrollment, add workflow file and secrets to target repo. + +## Consequences + +### Positive + +- **No org admin required**: Repo admins can adopt fullsend without org-level access or coordination. +- **Self-contained**: Everything fullsend needs lives in one repo — simpler mental model, easier cleanup. +- **Reuses ADR 0030 infrastructure**: Per-repo adds one workflow (`reusable-fullsend.yml`); all other reusable workflows and the composite action are shared with per-org. +- **Low entry barrier**: Copy one workflow file, create Apps (or install shared ones), set secrets — working fullsend in under 15 minutes. +- **Reduced blast radius**: Credential compromise affects only the single repo, not all enrolled repos in an org. +- **Same agent behavior**: Triage → Code → Review → Fix workflow is identical from the user's perspective. + +### Negative + +- **More Apps per user**: Each per-repo user creates their own Apps (unless using the token mint). +- **Config governance weaker**: In per-org, agent config lives in a separate repo with its own CODEOWNERS. In per-repo, `.fullsend/` config lives alongside code — a code contributor could modify agent behavior in a PR (mitigated by CODEOWNERS on `.fullsend/` and base-branch checkout). +- **No centralized policy**: Per-repo users set their own policies. An org cannot enforce uniform agent behavior across independently-installed repos. +- **Credential rotation burden**: Each per-repo user manages their own App PEM rotation (unless using the token mint). + +### Risks + +- **`pull_request_target` misconfiguration**: Per-repo workflows MUST use `pull_request_target` (not `pull_request`) to prevent PR authors from modifying the workflow to exfiltrate secrets. The workflow template enforces this, but users could edit it. +- **`event_payload` size**: The per-repo workflow passes `toJSON(github.event)` as a `workflow_call` input. GitHub's `workflow_call` inputs have a 65KB limit. Large PR event payloads could exceed this. +- **App identity confusion**: Users unfamiliar with the fix→review loop requirement may attempt a single-App setup and get silent failures (no review triggered after fix pushes). + +### Mitigations + +- **Template validation**: `fullsend init` generates the workflow file with `pull_request_target` — users who modify it are warned in documentation. +- **Payload trimming**: Start with full `toJSON(github.event)` for simplicity; add payload trimming if size becomes an issue in practice. +- **Clear error messages**: Credential auto-detection reports why coder and review Apps must be separate, with a link to setup documentation. +- **Migration path**: Per-repo users who outgrow the model can migrate to per-org without changing agent behavior — the same reusable workflows power both modes. + +## Open Questions + +### `reusable-fullsend.yml` for per-org shim simplification + +This workflow is also useful as a per-org shim simplification (replacing the ~380-line shim + dispatcher with a thin caller). Should per-org thin shims also adopt it? + +**Trade-off**: Sharing the routing workflow between per-repo and per-org reduces maintenance (one routing implementation), but couples per-org dispatch to the upstream workflow's release cadence. Per-org deployments currently control their own dispatch timing. + +### Retro stage + +The routing logic includes a retro stage (PR closed). Reusable workflows for retro (`reusable-retro.yml`) are not yet defined in ADR 0030. This stage should be added to the reusable workflow set or explicitly deferred. + +### Concurrency groups + +Concurrent fullsend runs for the same issue/PR should be prevented. Options: workflow-level concurrency in the caller, or per-stage concurrency inside `reusable-fullsend.yml`. Per-stage concurrency inside the reusable workflow keeps the caller simple and applies consistently across per-repo and per-org modes. + +## References + +- [ADR 0007: Per-role GitHub Apps](0007-per-role-github-apps.md) — authentication model replicated in per-repo +- [ADR 0008: workflow_dispatch for cross-repo dispatch](0008-workflow-dispatch-for-cross-repo-dispatch.md) — replaced by `workflow_call` in per-repo +- [ADR 0026: Stage-based dispatch](0026-stage-based-dispatch-for-agent-workflow-decoupling.md) — routing logic extracted into `reusable-fullsend.yml` +- [ADR 0027: Central token mint](0027-central-token-mint-secretless-fullsend.md) — optional credential enhancement for per-repo +- [ADR 0030: Reusable workflows](0030-reusable-workflows-for-action-installed-distribution.md) — foundation that makes per-repo possible From ab81047ffb21e7ae5f97092c3e0a41d9f542b442 Mon Sep 17 00:00:00 2001 From: Wayne Sun Date: Sat, 9 May 2026 09:14:44 -0400 Subject: [PATCH 02/11] docs: update ADR 0031 per-repo for OIDC token mint architecture Align with PR #503 changes: - Token mint is now the default credential model (Model A), own Apps is opt-in (Model B) - Remove dispatch PAT references (PATs eliminated entirely) - Remove *_CLIENT_ID vars (mint-token uses role-based lookup) - Update architecture diagrams for workflow_call dispatch - Clarify org admin still needed for GitHub App installation - Update CLI section: mint is default, --own-apps is opt-in - Reference four composite actions (fullsend, mint-token, validate-enrollment, setup-gcp) Signed-off-by: Wayne Sun --- docs/ADRs/0031-per-repo-installation-mode.md | 104 +++++++++++-------- 1 file changed, 60 insertions(+), 44 deletions(-) diff --git a/docs/ADRs/0031-per-repo-installation-mode.md b/docs/ADRs/0031-per-repo-installation-mode.md index 68b3fa4785..29b716cd5b 100644 --- a/docs/ADRs/0031-per-repo-installation-mode.md +++ b/docs/ADRs/0031-per-repo-installation-mode.md @@ -23,21 +23,21 @@ Proposed ## Context -Fullsend's installation model is per-org: `fullsend admin install` creates a dedicated `.fullsend` config repo, per-role GitHub Apps ([ADR 0007](0007-per-role-github-apps.md)), an org-level dispatch PAT ([ADR 0008](0008-workflow-dispatch-for-cross-repo-dispatch.md)), and shim workflows in enrolled repos. This requires org admin access and assumes all enrolled repos share agent configuration, credentials, and policies. +Fullsend's installation model is per-org: `fullsend admin install` creates a dedicated `.fullsend` config repo, per-role GitHub Apps ([ADR 0007](0007-per-role-github-apps.md)), shim workflows in enrolled repos, and a central token mint for OIDC-based credential issuance ([ADR 0027](0027-central-token-mint-secretless-fullsend.md)). This requires org admin access and assumes all enrolled repos share agent configuration, credentials, and policies. Some users cannot or do not want to use the per-org model: -1. **No org admin access** — contributors who admin specific repos but not the GitHub org. +1. **No org-wide setup desired** — teams who want fullsend on specific repos without the full `fullsend admin install` org setup (org admin is still needed to approve GitHub App installation on the repo). 2. **No sharing desired** — teams who want isolated agent configs, credentials, and billing for a single repo. 3. **Quick evaluation** — users who want to try fullsend on one repo without committing to org-wide setup. 4. **Personal repos** — individual developers on personal GitHub accounts (no org at all). -Two proposed ADRs create the building blocks that make per-repo possible: +Two ADRs create the building blocks that make per-repo possible: -- [ADR 0030](0030-reusable-workflows-for-action-installed-distribution.md) publishes reusable workflows and a root composite action from `fullsend-ai/fullsend`, enabling any repo to call fullsend infrastructure via `workflow_call` without copying workflow files. -- [ADR 0027](0027-central-token-mint-secretless-fullsend.md) introduces a central token mint with shared GitHub Apps, eliminating PEM secrets from config repos via OIDC-based credential issuance. +- [ADR 0030](0030-reusable-workflows-for-action-installed-distribution.md) publishes reusable workflows and four composite actions (`fullsend`, `mint-token`, `validate-enrollment`, `setup-gcp`) from `fullsend-ai/fullsend`, enabling any repo to call fullsend infrastructure via `workflow_call` without copying workflow files. +- [ADR 0027](0027-central-token-mint-secretless-fullsend.md) replaces PEM secrets and dispatch PATs with OIDC-based credential issuance via a central token mint. The `mint-token` composite action takes a role name (triage, coder, review, fix) and returns a scoped GitHub App installation token — no PEMs or client IDs in the calling repo. -Combined, these make per-repo installation viable: a single ~30-line workflow file in the target repo, calling upstream reusable workflows, with credentials stored as repo-level secrets or obtained via the token mint. +Combined, these make per-repo installation viable: a single ~30-line workflow file in the target repo, calling upstream reusable workflows, with credentials issued by the token mint. ## Options @@ -80,9 +80,12 @@ Per-org (current): ENROLLED REPO .FULLSEND CONFIG REPO ───────────── ───────────────────── -fullsend.yml (shim, ~380 lines) dispatch.yml → stage workflows - │ workflow_dispatch (PAT) │ +fullsend.yml (shim) dispatch.yml → thin caller stage workflows + │ workflow_call │ workflow_call └──────────────────────────────────────┘ + └──> reusable workflows (ADR 0030) + uses: fullsend-ai/fullsend@v1 + uses: mint-token, validate-enrollment, setup-gcp Per-repo (proposed): @@ -90,7 +93,7 @@ TARGET REPO (self-contained) ──────────────────────────── .github/workflows/fullsend.yml (~30 lines, thin caller) │ - │ workflow_call (native, no PAT) + │ workflow_call └──> fullsend-ai/fullsend/.github/workflows/reusable-fullsend.yml@v1 ├── routes event to stage ├── skips enrollment validation (per-repo mode) @@ -99,11 +102,13 @@ TARGET REPO (self-contained) ├──> reusable-review.yml ─┤ └──> reusable-fix.yml ─┘ │ - uses: fullsend-ai/fullsend@v1 (composite action) + uses: fullsend-ai/fullsend@v1 (run agent) + uses: ./.github/actions/mint-token (OIDC → scoped token) + uses: ./.github/actions/setup-gcp (GCP auth) config: .fullsend/ directory in target repo ``` -Per-repo requirements: repo admin, 3 GitHub Apps (triage + coder + review), GCP project for inference. No org admin, no dispatch PAT, no dedicated config repo. +Per-repo requirements: repo admin + org admin to install GitHub Apps on the repo, GCP project for inference. No dedicated config repo, no shim workflows, no cross-repo dispatch. ### 2. Repo layout @@ -166,50 +171,61 @@ In per-org mode: Per-repo supports two credential models: -**Model A: Per-role Apps (own)** +**Model A: Token mint (default, [ADR 0027](0027-central-token-mint-secretless-fullsend.md))** -Same as per-org ([ADR 0007](0007-per-role-github-apps.md)), but Apps are user-owned and installed on specific repos. Each role gets its own GitHub App: +GitHub Apps managed by the mint operator are installed on the repo. The +`mint-token` composite action exchanges a GitHub OIDC token for a scoped +GitHub App installation token — no PEMs, client IDs, or App secrets in the +repo. The action takes a `role` parameter (triage, coder, review, fix) and +the mint looks up the correct App PEM in GCP Secret Manager using the +`repository_owner` claim from the OIDC token. + +The mint's `job_workflow_ref` validation accepts both patterns: +- `{org}/.fullsend/.github/workflows/*.yml@*` (per-org) +- `fullsend-ai/fullsend/.github/workflows/reusable-*.yml@*` (per-repo) + +The `repository_owner` claim scopes tokens to the calling org/user. +`ALLOWED_ORGS` on the mint controls which orgs may request tokens. + +**Model B: Own Apps (self-managed)** + +For users who want full control over their GitHub Apps — same per-role model +as per-org ([ADR 0007](0007-per-role-github-apps.md)), but Apps are +user-owned and installed on specific repos: | App | Role | Key permissions | |-----|------|-----------------| -| `{user}-fullsend` | Orchestrator | `actions:write`, `contents:write`, `workflows:write`, `administration:write` | | `{user}-triage` | Triage | `issues:write` | | `{user}-coder` | Code + fix | `contents:write`, `pull-requests:write`, `issues:read`, `checks:read` | | `{user}-review` | Review | `contents:read`, `pull-requests:write`, `issues:read`, `checks:read` | -The orchestrator App is optional for per-repo (it handles enrollment reconciliation, which does not apply). Per-repo users need at minimum triage, coder, and review. - -PEMs stored as repo secrets, client IDs as repo variables. - -**Model B: Token mint + shared Apps ([ADR 0027](0027-central-token-mint-secretless-fullsend.md))** - -Shared public fullsend Apps installed on the repo. Token mint handles credential issuance via OIDC. No PEMs in the repo — the mint holds them. - -The mint's `job_workflow_ref` validation accepts both patterns: -- `{org}/.fullsend/.github/workflows/*.yml@*` (per-org) -- `fullsend-ai/fullsend/.github/workflows/reusable-*.yml@*` (per-repo) - -The `repository_owner` claim still scopes tokens to the calling org/user. +PEMs stored in the user's own GCP Secret Manager project (accessed via the +same `mint-token` action pointed at a self-hosted mint) or as repo secrets +with direct App token generation via `actions/create-github-app-token`. -**Credential auto-detection**: Reusable workflows detect the credential model automatically: -- `FULLSEND_MINT_URL` present → mint mode (OIDC token exchange) -- `FULLSEND_CODER_APP_PRIVATE_KEY` + `FULLSEND_REVIEW_APP_PRIVATE_KEY` present → per-role App mode -- Neither → error with setup instructions +**Credential detection**: The `mint-token` composite action is the default +path. Reusable workflows call `mint-token` with the agent role; the action +handles OIDC exchange and token scoping. For self-managed Apps using direct +token generation, the reusable workflow falls back to +`actions/create-github-app-token` when `FULLSEND_MINT_URL` is not set and +App PEM secrets are present. ### 7. CLI support: `fullsend init` A new CLI command for per-repo setup: ``` -fullsend init [--mint-url URL] [--skip-orchestrator] +fullsend init [--mint-url URL] [--own-apps] ``` 1. Generates `.github/workflows/fullsend.yml` from template. -2. Guides user through GitHub App creation via manifest flow — creates triage, coder, and review Apps. -3. Stores PEMs as repo secrets, client IDs as repo variables. +2. Sets `FULLSEND_MINT_URL` as a repo variable (default: public mint). +3. Guides user to install the shared fullsend Apps on their repo. 4. Optionally creates `.fullsend/` directory with default config. -With `--mint-url`, App creation is skipped — the user installs shared Apps instead. +With `--own-apps`, the user creates their own GitHub Apps via the manifest +flow instead of using the shared mint. PEMs are stored in the user's own +GCP Secret Manager project. ### 8. Coexistence @@ -223,19 +239,19 @@ Migration between models is straightforward: ### Positive -- **No org admin required**: Repo admins can adopt fullsend without org-level access or coordination. +- **No org admin required**: Repo admins can adopt fullsend without org-level access or coordination (though org admin is still needed to install the GitHub Apps on the repo). - **Self-contained**: Everything fullsend needs lives in one repo — simpler mental model, easier cleanup. -- **Reuses ADR 0030 infrastructure**: Per-repo adds one workflow (`reusable-fullsend.yml`); all other reusable workflows and the composite action are shared with per-org. -- **Low entry barrier**: Copy one workflow file, create Apps (or install shared ones), set secrets — working fullsend in under 15 minutes. -- **Reduced blast radius**: Credential compromise affects only the single repo, not all enrolled repos in an org. +- **Reuses ADR 0030 infrastructure**: Per-repo adds one workflow (`reusable-fullsend.yml`); all other reusable workflows and the four composite actions are shared with per-org. +- **Low entry barrier**: Copy one workflow file, install shared Apps, set mint URL — working fullsend in under 15 minutes. No PEMs or client IDs to manage. +- **Reduced blast radius**: Token mint scopes tokens to the requesting repo via the `repository` OIDC claim. Credential compromise affects only the single repo. - **Same agent behavior**: Triage → Code → Review → Fix workflow is identical from the user's perspective. ### Negative -- **More Apps per user**: Each per-repo user creates their own Apps (unless using the token mint). +- **Org admin still needed for App installation**: While per-repo removes the need for org admin to run `fullsend admin install`, an org admin must still approve the GitHub App installation on the repo. - **Config governance weaker**: In per-org, agent config lives in a separate repo with its own CODEOWNERS. In per-repo, `.fullsend/` config lives alongside code — a code contributor could modify agent behavior in a PR (mitigated by CODEOWNERS on `.fullsend/` and base-branch checkout). - **No centralized policy**: Per-repo users set their own policies. An org cannot enforce uniform agent behavior across independently-installed repos. -- **Credential rotation burden**: Each per-repo user manages their own App PEM rotation (unless using the token mint). +- **Self-managed Apps increase burden**: Users who opt out of the token mint (Model B) manage their own App PEM rotation and GCP Secret Manager project. ### Risks @@ -245,7 +261,7 @@ Migration between models is straightforward: ### Mitigations -- **Template validation**: `fullsend init` generates the workflow file with `pull_request_target` — users who modify it are warned in documentation. +- **Template validation**: `fullsend init` generates the workflow file with `pull_request_target` — users who modify it are warned in documentation. CODEOWNERS on `.github/workflows/fullsend.yml` prevents unauthorized changes. - **Payload trimming**: Start with full `toJSON(github.event)` for simplicity; add payload trimming if size becomes an issue in practice. - **Clear error messages**: Credential auto-detection reports why coder and review Apps must be separate, with a link to setup documentation. - **Migration path**: Per-repo users who outgrow the model can migrate to per-org without changing agent behavior — the same reusable workflows power both modes. @@ -269,7 +285,7 @@ Concurrent fullsend runs for the same issue/PR should be prevented. Options: wor ## References - [ADR 0007: Per-role GitHub Apps](0007-per-role-github-apps.md) — authentication model replicated in per-repo -- [ADR 0008: workflow_dispatch for cross-repo dispatch](0008-workflow-dispatch-for-cross-repo-dispatch.md) — replaced by `workflow_call` in per-repo +- [ADR 0008: workflow_dispatch for cross-repo dispatch](0008-workflow-dispatch-for-cross-repo-dispatch.md) — superseded by `workflow_call` (ADR 0027 removes the original constraint) - [ADR 0026: Stage-based dispatch](0026-stage-based-dispatch-for-agent-workflow-decoupling.md) — routing logic extracted into `reusable-fullsend.yml` -- [ADR 0027: Central token mint](0027-central-token-mint-secretless-fullsend.md) — optional credential enhancement for per-repo +- [ADR 0027: Central token mint](0027-central-token-mint-secretless-fullsend.md) — default credential model for per-repo - [ADR 0030: Reusable workflows](0030-reusable-workflows-for-action-installed-distribution.md) — foundation that makes per-repo possible From c0fc4528f7985651d8ad4eebc0477b6436b0805a Mon Sep 17 00:00:00 2001 From: Wayne Sun Date: Sat, 9 May 2026 09:16:57 -0400 Subject: [PATCH 03/11] docs: add private repo use case to per-repo ADR context Signed-off-by: Wayne Sun --- docs/ADRs/0031-per-repo-installation-mode.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/ADRs/0031-per-repo-installation-mode.md b/docs/ADRs/0031-per-repo-installation-mode.md index 29b716cd5b..154d1934a1 100644 --- a/docs/ADRs/0031-per-repo-installation-mode.md +++ b/docs/ADRs/0031-per-repo-installation-mode.md @@ -28,9 +28,10 @@ Fullsend's installation model is per-org: `fullsend admin install` creates a ded Some users cannot or do not want to use the per-org model: 1. **No org-wide setup desired** — teams who want fullsend on specific repos without the full `fullsend admin install` org setup (org admin is still needed to approve GitHub App installation on the repo). -2. **No sharing desired** — teams who want isolated agent configs, credentials, and billing for a single repo. -3. **Quick evaluation** — users who want to try fullsend on one repo without committing to org-wide setup. -4. **Personal repos** — individual developers on personal GitHub accounts (no org at all). +2. **Private repos** — private repos cannot call `workflow_call` into a separate `.fullsend` config repo unless that repo is also visible to the caller. Per-repo avoids cross-repo visibility constraints by calling upstream `fullsend-ai/fullsend` (public) directly. +3. **No sharing desired** — teams who want isolated agent configs, credentials, and billing for a single repo. +4. **Quick evaluation** — users who want to try fullsend on one repo without committing to org-wide setup. +5. **Personal repos** — individual developers on personal GitHub accounts (no org at all). Two ADRs create the building blocks that make per-repo possible: From 6c458550be87430b1ee602201905dc5977b6a8a2 Mon Sep 17 00:00:00 2001 From: Wayne Sun Date: Sun, 10 May 2026 17:42:57 -0400 Subject: [PATCH 04/11] docs: renumber ADR 0031 to 0033 (per-repo installation) ADR 0031 slot now used by reusable workflows (PR #688). Renumber to 0033 and update internal cross-references from ADR 0030 to 0031. Signed-off-by: Wayne Sun --- ....md => 0033-per-repo-installation-mode.md} | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) rename docs/ADRs/{0031-per-repo-installation-mode.md => 0033-per-repo-installation-mode.md} (96%) diff --git a/docs/ADRs/0031-per-repo-installation-mode.md b/docs/ADRs/0033-per-repo-installation-mode.md similarity index 96% rename from docs/ADRs/0031-per-repo-installation-mode.md rename to docs/ADRs/0033-per-repo-installation-mode.md index 154d1934a1..5b2e16ab3e 100644 --- a/docs/ADRs/0031-per-repo-installation-mode.md +++ b/docs/ADRs/0033-per-repo-installation-mode.md @@ -1,5 +1,5 @@ --- -title: "0031. Per-repo installation mode" +title: "33. Per-repo installation mode" status: Proposed relates_to: - agent-infrastructure @@ -13,7 +13,7 @@ topics: - github-apps --- -# 0031. Per-repo installation mode +# 33. Per-repo installation mode Date: 2026-05-06 @@ -35,7 +35,7 @@ Some users cannot or do not want to use the per-org model: Two ADRs create the building blocks that make per-repo possible: -- [ADR 0030](0030-reusable-workflows-for-action-installed-distribution.md) publishes reusable workflows and four composite actions (`fullsend`, `mint-token`, `validate-enrollment`, `setup-gcp`) from `fullsend-ai/fullsend`, enabling any repo to call fullsend infrastructure via `workflow_call` without copying workflow files. +- [ADR 0031](0031-reusable-workflows-for-action-installed-distribution.md) publishes reusable workflows and four composite actions (`fullsend`, `mint-token`, `validate-enrollment`, `setup-gcp`) from `fullsend-ai/fullsend`, enabling any repo to call fullsend infrastructure via `workflow_call` without copying workflow files. - [ADR 0027](0027-central-token-mint-secretless-fullsend.md) replaces PEM secrets and dispatch PATs with OIDC-based credential issuance via a central token mint. The `mint-token` composite action takes a role name (triage, coder, review, fix) and returns a scoped GitHub App installation token — no PEMs or client IDs in the calling repo. Combined, these make per-repo installation viable: a single ~30-line workflow file in the target repo, calling upstream reusable workflows, with credentials issued by the token mint. @@ -46,7 +46,7 @@ Combined, these make per-repo installation viable: a single ~30-line workflow fi Run `fullsend admin install` targeting a single repo instead of an org. Copy all scaffold files (agent workflows, composite action, dispatcher, scripts) into the target repo. -**Rejected**: Same maintenance burden as per-org — the repo must re-run install to pick up upstream patches. Contradicts ADR 0030's motivation to eliminate workflow drift. +**Rejected**: Same maintenance burden as per-org — the repo must re-run install to pick up upstream patches. Contradicts ADR 0031's motivation to eliminate workflow drift. ### Alternative 2: Single GitHub App for all roles @@ -72,7 +72,7 @@ Reduce per-repo to two Apps instead of matching the full per-org app set. Add a **per-repo installation mode** where fullsend runs entirely within a single repository — no `.fullsend` config repo, no cross-repo dispatch, no org-level secrets. The target repo IS the config repo. -Per-repo reuses the reusable workflows from ADR 0030, adding one new artifact: `reusable-fullsend.yml`, an all-in-one routing and dispatch workflow that combines event-to-stage routing (currently in the ~380-line shim) with per-stage dispatch into a single `workflow_call` entry point. +Per-repo reuses the reusable workflows from ADR 0031, adding one new artifact: `reusable-fullsend.yml`, an all-in-one routing and dispatch workflow that combines event-to-stage routing (currently in the ~380-line shim) with per-stage dispatch into a single `workflow_call` entry point. ### 1. Architecture @@ -84,7 +84,7 @@ ENROLLED REPO .FULLSEND CONFIG REPO fullsend.yml (shim) dispatch.yml → thin caller stage workflows │ workflow_call │ workflow_call └──────────────────────────────────────┘ - └──> reusable workflows (ADR 0030) + └──> reusable workflows (ADR 0031) uses: fullsend-ai/fullsend@v1 uses: mint-token, validate-enrollment, setup-gcp @@ -99,7 +99,7 @@ TARGET REPO (self-contained) ├── routes event to stage ├── skips enrollment validation (per-repo mode) ├──> reusable-triage.yml ─┐ - ├──> reusable-code.yml ─┤── reusable workflows (ADR 0030) + ├──> reusable-code.yml ─┤── reusable workflows (ADR 0031) ├──> reusable-review.yml ─┤ └──> reusable-fix.yml ─┘ │ @@ -150,7 +150,7 @@ The routing logic maps: - `pull_request_target` → review (or retro on close) - `pull_request_review` + `changes_requested` from bot → fix -This workflow serves both per-repo and per-org simplified shims. Per-org thin shims (from ADR 0030) can also use it to replace the ~380-line shim + dispatcher. +This workflow serves both per-repo and per-org simplified shims. Per-org thin shims (from ADR 0031) can also use it to replace the ~380-line shim + dispatcher. **Nesting depth**: target-repo workflow → `reusable-fullsend.yml` → `reusable-code.yml` = 2 levels of `workflow_call` (GitHub limit is 4). @@ -242,7 +242,7 @@ Migration between models is straightforward: - **No org admin required**: Repo admins can adopt fullsend without org-level access or coordination (though org admin is still needed to install the GitHub Apps on the repo). - **Self-contained**: Everything fullsend needs lives in one repo — simpler mental model, easier cleanup. -- **Reuses ADR 0030 infrastructure**: Per-repo adds one workflow (`reusable-fullsend.yml`); all other reusable workflows and the four composite actions are shared with per-org. +- **Reuses ADR 0031 infrastructure**: Per-repo adds one workflow (`reusable-fullsend.yml`); all other reusable workflows and the four composite actions are shared with per-org. - **Low entry barrier**: Copy one workflow file, install shared Apps, set mint URL — working fullsend in under 15 minutes. No PEMs or client IDs to manage. - **Reduced blast radius**: Token mint scopes tokens to the requesting repo via the `repository` OIDC claim. Credential compromise affects only the single repo. - **Same agent behavior**: Triage → Code → Review → Fix workflow is identical from the user's perspective. @@ -277,7 +277,7 @@ This workflow is also useful as a per-org shim simplification (replacing the ~38 ### Retro stage -The routing logic includes a retro stage (PR closed). Reusable workflows for retro (`reusable-retro.yml`) are not yet defined in ADR 0030. This stage should be added to the reusable workflow set or explicitly deferred. +The routing logic includes a retro stage (PR closed). Reusable workflows for retro (`reusable-retro.yml`) are not yet defined in ADR 0031. This stage should be added to the reusable workflow set or explicitly deferred. ### Concurrency groups @@ -289,4 +289,4 @@ Concurrent fullsend runs for the same issue/PR should be prevented. Options: wor - [ADR 0008: workflow_dispatch for cross-repo dispatch](0008-workflow-dispatch-for-cross-repo-dispatch.md) — superseded by `workflow_call` (ADR 0027 removes the original constraint) - [ADR 0026: Stage-based dispatch](0026-stage-based-dispatch-for-agent-workflow-decoupling.md) — routing logic extracted into `reusable-fullsend.yml` - [ADR 0027: Central token mint](0027-central-token-mint-secretless-fullsend.md) — default credential model for per-repo -- [ADR 0030: Reusable workflows](0030-reusable-workflows-for-action-installed-distribution.md) — foundation that makes per-repo possible +- [ADR 0031: Reusable workflows](0031-reusable-workflows-for-action-installed-distribution.md) — foundation that makes per-repo possible From 515d279c2e1eafdd049086b2ebff092148ecb11a Mon Sep 17 00:00:00 2001 From: Wayne Sun Date: Sun, 10 May 2026 17:49:56 -0400 Subject: [PATCH 05/11] =?UTF-8?q?docs:=20update=20ADR=200027=20=E2=86=92?= =?UTF-8?q?=200029=20cross-references=20in=20ADR=200033?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Token mint ADR renumbered from 0027 to 0029 on PR #655. Signed-off-by: Wayne Sun --- docs/ADRs/0033-per-repo-installation-mode.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/ADRs/0033-per-repo-installation-mode.md b/docs/ADRs/0033-per-repo-installation-mode.md index 5b2e16ab3e..56291ca6b7 100644 --- a/docs/ADRs/0033-per-repo-installation-mode.md +++ b/docs/ADRs/0033-per-repo-installation-mode.md @@ -23,7 +23,7 @@ Proposed ## Context -Fullsend's installation model is per-org: `fullsend admin install` creates a dedicated `.fullsend` config repo, per-role GitHub Apps ([ADR 0007](0007-per-role-github-apps.md)), shim workflows in enrolled repos, and a central token mint for OIDC-based credential issuance ([ADR 0027](0027-central-token-mint-secretless-fullsend.md)). This requires org admin access and assumes all enrolled repos share agent configuration, credentials, and policies. +Fullsend's installation model is per-org: `fullsend admin install` creates a dedicated `.fullsend` config repo, per-role GitHub Apps ([ADR 0007](0007-per-role-github-apps.md)), shim workflows in enrolled repos, and a central token mint for OIDC-based credential issuance ([ADR 0029](0029-central-token-mint-secretless-fullsend.md)). This requires org admin access and assumes all enrolled repos share agent configuration, credentials, and policies. Some users cannot or do not want to use the per-org model: @@ -36,7 +36,7 @@ Some users cannot or do not want to use the per-org model: Two ADRs create the building blocks that make per-repo possible: - [ADR 0031](0031-reusable-workflows-for-action-installed-distribution.md) publishes reusable workflows and four composite actions (`fullsend`, `mint-token`, `validate-enrollment`, `setup-gcp`) from `fullsend-ai/fullsend`, enabling any repo to call fullsend infrastructure via `workflow_call` without copying workflow files. -- [ADR 0027](0027-central-token-mint-secretless-fullsend.md) replaces PEM secrets and dispatch PATs with OIDC-based credential issuance via a central token mint. The `mint-token` composite action takes a role name (triage, coder, review, fix) and returns a scoped GitHub App installation token — no PEMs or client IDs in the calling repo. +- [ADR 0029](0029-central-token-mint-secretless-fullsend.md) replaces PEM secrets and dispatch PATs with OIDC-based credential issuance via a central token mint. The `mint-token` composite action takes a role name (triage, coder, review, fix) and returns a scoped GitHub App installation token — no PEMs or client IDs in the calling repo. Combined, these make per-repo installation viable: a single ~30-line workflow file in the target repo, calling upstream reusable workflows, with credentials issued by the token mint. @@ -172,7 +172,7 @@ In per-org mode: Per-repo supports two credential models: -**Model A: Token mint (default, [ADR 0027](0027-central-token-mint-secretless-fullsend.md))** +**Model A: Token mint (default, [ADR 0029](0029-central-token-mint-secretless-fullsend.md))** GitHub Apps managed by the mint operator are installed on the repo. The `mint-token` composite action exchanges a GitHub OIDC token for a scoped @@ -286,7 +286,7 @@ Concurrent fullsend runs for the same issue/PR should be prevented. Options: wor ## References - [ADR 0007: Per-role GitHub Apps](0007-per-role-github-apps.md) — authentication model replicated in per-repo -- [ADR 0008: workflow_dispatch for cross-repo dispatch](0008-workflow-dispatch-for-cross-repo-dispatch.md) — superseded by `workflow_call` (ADR 0027 removes the original constraint) +- [ADR 0008: workflow_dispatch for cross-repo dispatch](0008-workflow-dispatch-for-cross-repo-dispatch.md) — superseded by `workflow_call` (ADR 0029 removes the original constraint) - [ADR 0026: Stage-based dispatch](0026-stage-based-dispatch-for-agent-workflow-decoupling.md) — routing logic extracted into `reusable-fullsend.yml` -- [ADR 0027: Central token mint](0027-central-token-mint-secretless-fullsend.md) — default credential model for per-repo +- [ADR 0029: Central token mint](0027-central-token-mint-secretless-fullsend.md) — default credential model for per-repo - [ADR 0031: Reusable workflows](0031-reusable-workflows-for-action-installed-distribution.md) — foundation that makes per-repo possible From 6455f274d6de6b3394bbcafce6d3b5983e799334 Mon Sep 17 00:00:00 2001 From: Wayne Sun Date: Sun, 10 May 2026 18:35:35 -0400 Subject: [PATCH 06/11] docs: sync ADR 0033 with PR 792 reusable workflows implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update per-repo installation ADR to build on top of the actual architecture from PR 792 (reusable workflows, centralized routing, layered content resolution): - Reference ADR 0034 (centralized routing) and ADR 0035 (layered content) as building blocks alongside ADR 0029/0031 - Update architecture diagrams to show PR 792's 3-hop per-org flow (shim → dispatch.yml → thin caller → reusable workflow) - Rename reusable-fullsend.yml to reusable-dispatch.yml (reusable version of dispatch.yml routing logic) - Adopt customized/ convention inside .fullsend/ for per-repo overrides, matching per-org ADR 0035 pattern with parameterized workspace root - Add three credential profiles (SaaS, Bundled, Self-managed) from PR 503/655 shared Apps implementation - Document private repo visibility tradeoffs: per-repo recommended as default for private repos to avoid content exposure through public .fullsend config repo - Update routing rules, payload handling, nesting depth, and open questions to match PR 792 implementation Signed-off-by: Wayne Sun --- docs/ADRs/0033-per-repo-installation-mode.md | 241 +++++++++++-------- 1 file changed, 141 insertions(+), 100 deletions(-) diff --git a/docs/ADRs/0033-per-repo-installation-mode.md b/docs/ADRs/0033-per-repo-installation-mode.md index 56291ca6b7..616801fd3e 100644 --- a/docs/ADRs/0033-per-repo-installation-mode.md +++ b/docs/ADRs/0033-per-repo-installation-mode.md @@ -28,17 +28,29 @@ Fullsend's installation model is per-org: `fullsend admin install` creates a ded Some users cannot or do not want to use the per-org model: 1. **No org-wide setup desired** — teams who want fullsend on specific repos without the full `fullsend admin install` org setup (org admin is still needed to approve GitHub App installation on the repo). -2. **Private repos** — private repos cannot call `workflow_call` into a separate `.fullsend` config repo unless that repo is also visible to the caller. Per-repo avoids cross-repo visibility constraints by calling upstream `fullsend-ai/fullsend` (public) directly. +2. **Private repos** — the `.fullsend` config repo defaults to public so that all enrolled repos can call its workflows via `workflow_call`. A private repo _can_ call into a public `.fullsend`, but event payloads and workflow run logs flow through the public repo's context, creating content exposure risk. Setting `.fullsend` to private restricts callers to private repos within the same org (public enrolled repos can no longer reach it). Per-repo sidesteps this entirely: the shim calls upstream `fullsend-ai/fullsend` reusable workflows directly, and stage workflows run in the private repo's own context — no cross-repo payload exposure. **Per-repo is the recommended default for private repos.** 3. **No sharing desired** — teams who want isolated agent configs, credentials, and billing for a single repo. 4. **Quick evaluation** — users who want to try fullsend on one repo without committing to org-wide setup. 5. **Personal repos** — individual developers on personal GitHub accounts (no org at all). -Two ADRs create the building blocks that make per-repo possible: +Three ADRs and the implementation in PR 792 create the building blocks that make per-repo possible: -- [ADR 0031](0031-reusable-workflows-for-action-installed-distribution.md) publishes reusable workflows and four composite actions (`fullsend`, `mint-token`, `validate-enrollment`, `setup-gcp`) from `fullsend-ai/fullsend`, enabling any repo to call fullsend infrastructure via `workflow_call` without copying workflow files. - [ADR 0029](0029-central-token-mint-secretless-fullsend.md) replaces PEM secrets and dispatch PATs with OIDC-based credential issuance via a central token mint. The `mint-token` composite action takes a role name (triage, coder, review, fix) and returns a scoped GitHub App installation token — no PEMs or client IDs in the calling repo. +- [ADR 0031](0031-reusable-workflows-for-action-installed-distribution.md) publishes five reusable workflows (`reusable-triage.yml`, `reusable-code.yml`, `reusable-review.yml`, `reusable-fix.yml`, `reusable-retro.yml`) and four composite actions (`fullsend`, `mint-token`, `validate-enrollment`, `setup-gcp`) from `fullsend-ai/fullsend`, enabling any repo to call fullsend infrastructure via `workflow_call` without copying workflow files. Scaffold stage workflows in `.fullsend` are now thin callers (41–66 lines) that delegate to these reusable workflows. +- [ADR 0034](0034-centralized-event-routing.md) centralizes event-to-stage routing in `dispatch.yml` within the `.fullsend` config repo. The enrolled-repo shim (~70 lines) forwards raw event context to `dispatch.yml` via `workflow_call`; `dispatch.yml` (~370 lines) determines the stage, mints an OIDC dispatch token, validates the stage, checks the kill switch, and dispatches to the matching thin caller via `gh workflow run`. Adding a new stage requires only a case branch in `dispatch.yml` — zero changes to enrolled repos. +- [ADR 0035](0035-layered-content-resolution.md) introduces layered content resolution: upstream defaults (agents, skills, schemas, harness, policies, scripts) are sparse-checked from `fullsend-ai/fullsend` at runtime, then org overrides from `customized/` are copied on top. The scaffold installs only org-specific files (~23 files instead of ~68). -Combined, these make per-repo installation viable: a single ~30-line workflow file in the target repo, calling upstream reusable workflows, with credentials issued by the token mint. +The per-org flow after PR 792: + +``` +enrolled repo (shim, ~70 lines) + └─ workflow_call ─→ .fullsend/dispatch.yml (routing, ~370 lines) + └─ gh workflow run ─→ .fullsend/code.yml (thin caller, ~43 lines) + └─ uses: fullsend-ai/fullsend/reusable-code.yml@v1 + (workspace prep, mint-token, agent run) +``` + +Combined, these make per-repo installation viable: a workflow file in the target repo calling upstream reusable workflows, with credentials issued by the token mint and routing handled by a reusable dispatch workflow. ## Options @@ -46,7 +58,7 @@ Combined, these make per-repo installation viable: a single ~30-line workflow fi Run `fullsend admin install` targeting a single repo instead of an org. Copy all scaffold files (agent workflows, composite action, dispatcher, scripts) into the target repo. -**Rejected**: Same maintenance burden as per-org — the repo must re-run install to pick up upstream patches. Contradicts ADR 0031's motivation to eliminate workflow drift. +**Rejected**: Same maintenance burden as per-org — the repo must re-run install to pick up upstream patches. Contradicts ADR 0031's motivation to eliminate workflow drift. ADR 0035's layered content resolution already solves the upstream-defaults problem for per-org; per-repo should reuse the same mechanism. ### Alternative 2: Single GitHub App for all roles @@ -72,87 +84,102 @@ Reduce per-repo to two Apps instead of matching the full per-org app set. Add a **per-repo installation mode** where fullsend runs entirely within a single repository — no `.fullsend` config repo, no cross-repo dispatch, no org-level secrets. The target repo IS the config repo. -Per-repo reuses the reusable workflows from ADR 0031, adding one new artifact: `reusable-fullsend.yml`, an all-in-one routing and dispatch workflow that combines event-to-stage routing (currently in the ~380-line shim) with per-stage dispatch into a single `workflow_call` entry point. +Per-repo reuses the reusable workflows from ADR 0031, adding one new artifact: `reusable-dispatch.yml`, a reusable version of the per-org `dispatch.yml` (ADR 0034) that combines event-to-stage routing with per-stage dispatch into a single `workflow_call` entry point, published in `fullsend-ai/fullsend`. This eliminates the need for a `.fullsend` config repo — the target repo's shim calls `reusable-dispatch.yml` directly. ### 1. Architecture ``` -Per-org (current): - -ENROLLED REPO .FULLSEND CONFIG REPO -───────────── ───────────────────── -fullsend.yml (shim) dispatch.yml → thin caller stage workflows - │ workflow_call │ workflow_call - └──────────────────────────────────────┘ - └──> reusable workflows (ADR 0031) - uses: fullsend-ai/fullsend@v1 - uses: mint-token, validate-enrollment, setup-gcp +Per-org (current, after ADR 0031/0034/0035): + +ENROLLED REPO .FULLSEND CONFIG REPO +───────────── ───────────────────── +fullsend.yml (shim, ~70 lines) dispatch.yml (routing, ~370 lines) + │ workflow_call │ determines stage + └──────────────────────────────────────┘ gh workflow run + │ + code.yml / review.yml / ... (thin callers, ~43 lines) + │ uses: (workflow_call) + └──> fullsend-ai/fullsend/reusable-code.yml@v1 + ├── prepare workspace (ADR 0035 layering) + ├── validate-enrollment + ├── mint-token (OIDC → scoped token) + ├── setup-gcp + └── fullsend action (run agent) Per-repo (proposed): TARGET REPO (self-contained) ──────────────────────────── -.github/workflows/fullsend.yml (~30 lines, thin caller) +.github/workflows/fullsend.yml (~70 lines, shim) │ │ workflow_call - └──> fullsend-ai/fullsend/.github/workflows/reusable-fullsend.yml@v1 - ├── routes event to stage - ├── skips enrollment validation (per-repo mode) + └──> fullsend-ai/fullsend/.github/workflows/reusable-dispatch.yml@v1 + ├── routes event to stage (same logic as .fullsend/dispatch.yml) + ├── dispatches to per-stage reusable workflows: ├──> reusable-triage.yml ─┐ ├──> reusable-code.yml ─┤── reusable workflows (ADR 0031) ├──> reusable-review.yml ─┤ - └──> reusable-fix.yml ─┘ + ├──> reusable-fix.yml ─┤ + └──> reusable-retro.yml ─┘ │ uses: fullsend-ai/fullsend@v1 (run agent) - uses: ./.github/actions/mint-token (OIDC → scoped token) - uses: ./.github/actions/setup-gcp (GCP auth) + uses: fullsend-ai/fullsend/mint-token (OIDC → scoped token) + uses: fullsend-ai/fullsend/setup-gcp (GCP auth) + skips enrollment validation (per-repo mode) config: .fullsend/ directory in target repo ``` -Per-repo requirements: repo admin + org admin to install GitHub Apps on the repo, GCP project for inference. No dedicated config repo, no shim workflows, no cross-repo dispatch. +Per-repo requirements: repo admin + org admin to install GitHub Apps on the repo, GCP project for inference. No dedicated config repo, no cross-repo dispatch. ### 2. Repo layout ``` target-repo/ -├── .github/workflows/fullsend.yml ← single workflow file (~30 lines) -├── .fullsend/ ← in-repo config (optional) -│ ├── agents/ ← agent prompt overrides -│ ├── harness/ ← harness config overrides -│ ├── policies/ ← sandbox policies -│ ├── skills/ ← repo-specific skills -│ ├── scripts/ ← pre/post scripts +├── .github/workflows/fullsend.yml ← single workflow file (~70 lines, shim) +├── .fullsend/ ← in-repo config workspace (optional) +│ ├── customized/ ← user overrides (same convention as per-org) +│ │ ├── agents/ ← agent prompt overrides +│ │ ├── harness/ ← harness config overrides +│ │ ├── policies/ ← sandbox policies +│ │ ├── skills/ ← repo-specific skills +│ │ └── scripts/ ← pre/post scripts │ └── config.yaml ← repo-level config ├── AGENTS.md └── ... (source code) ``` -The `.fullsend/` directory is optional. Without it, upstream defaults apply. Users add files to `.fullsend/` only to customize agent behavior. +The `.fullsend/` directory mirrors the `.fullsend` config repo structure. It acts as a self-contained config workspace — analogous to the `.fullsend` repo root in per-org mode. At runtime, workspace prep populates `.fullsend/agents/`, `.fullsend/skills/`, etc. from upstream defaults, then copies `.fullsend/customized/*` on top. This is the same `customized/` convention from ADR 0035, rooted at `.fullsend/` instead of `.`. + +The `.fullsend/` directory is optional. Without it, upstream defaults apply. Users add override files to `.fullsend/customized/` only to customize agent behavior — the top-level dirs inside `.fullsend/` are runtime-populated and should not be committed. ### 3. Config layering -Per-repo collapses the three-tier config model to two tiers: +ADR 0035 introduces layered content resolution for per-org: upstream defaults are sparse-checked at runtime, then org overrides from `customized/` are copied on top. Per-repo uses the same `customized/` convention, rooted inside `.fullsend/`: ``` -fullsend-ai/fullsend defaults < .fullsend/ directory < AGENTS.md -(base) (customize) (instructions) +fullsend-ai/fullsend defaults < .fullsend/customized/ < AGENTS.md +(base, sparse-checked) (overrides) (instructions) ``` -The org-level `.fullsend` config repo tier is skipped — the in-repo `.fullsend/` directory serves as both org and repo config. +The org-level `.fullsend` config repo tier is skipped — the in-repo `.fullsend/` directory serves as the config workspace. The reusable workflows' "Prepare workspace" step is parameterized by root directory: `.` for per-org (the `.fullsend` repo checkout), `.fullsend/` for per-repo. In both modes, it sparse-checkouts upstream defaults into `{root}/agents/`, `{root}/skills/`, etc., then copies `{root}/customized/*` on top — identical code path, different root. -### 4. The `reusable-fullsend.yml` workflow +### 4. The `reusable-dispatch.yml` workflow -This is the key new artifact, published in `fullsend-ai/fullsend/.github/workflows/`. It accepts event metadata via `workflow_call` inputs, routes events to stages using the same logic currently embedded in the shim, and conditionally dispatches to per-stage reusable workflows. +This is the key new artifact, published in `fullsend-ai/fullsend/.github/workflows/`. It is a reusable version of the per-org `dispatch.yml` (ADR 0034), accepting event context via `workflow_call` inputs and performing the same routing and dispatch logic. -The routing logic maps: +The routing logic (identical to per-org `dispatch.yml`) maps: - `issues` + `labeled` → stage based on label name (`ready-to-code` → code, `ready-for-review` → review) -- `issue_comment` + slash commands → `/triage`, `/code`, `/review`, `/fix` -- `pull_request_target` → review (or retro on close) -- `pull_request_review` + `changes_requested` from bot → fix +- `issue_comment` + slash commands → `/triage`, `/code`, `/review`, `/fix`, `/retro` +- `issue_comment` + `needs-info` label (non-command) → auto-triage +- `pull_request_target` + `opened`/`synchronize`/`ready_for_review` → review +- `pull_request_target` + `closed` → retro +- `pull_request_review` + `changes_requested` from review bot → fix (same-repo PRs only) -This workflow serves both per-repo and per-org simplified shims. Per-org thin shims (from ADR 0031) can also use it to replace the ~380-line shim + dispatcher. +In per-org mode, `dispatch.yml` routes events and dispatches to thin callers via `gh workflow run` (breaking the `workflow_call` chain). In per-repo mode, `reusable-dispatch.yml` routes events and dispatches to per-stage reusable workflows directly via conditional `workflow_call` jobs, keeping the entire pipeline within a single `workflow_call` chain. -**Nesting depth**: target-repo workflow → `reusable-fullsend.yml` → `reusable-code.yml` = 2 levels of `workflow_call` (GitHub limit is 4). +**Dispatch mechanism**: Per-org uses `gh workflow run` (workflow_dispatch) to fan out to thin callers — this avoids deep `workflow_call` nesting but requires thin caller workflow files in `.fullsend`. Per-repo uses conditional `workflow_call` jobs inside `reusable-dispatch.yml` to call `reusable-code.yml` etc. directly, eliminating the need for thin callers. + +**Nesting depth**: target-repo shim → `reusable-dispatch.yml` → `reusable-code.yml` = 2 levels of `workflow_call` (GitHub limit is 4). ### 5. Per-repo mode detection @@ -161,25 +188,44 @@ Reusable workflows detect per-repo mode when `source_repo == github.repository` In per-repo mode: - Enrollment validation is skipped (always self-enrolled). - A single checkout retrieves both config (`.fullsend/` subdirectory) and code (repo root). +- The "Prepare workspace" step runs with root=`.fullsend/` — populates `.fullsend/agents/`, `.fullsend/skills/`, etc. from upstream, then copies `.fullsend/customized/*` on top. - `fullsend run` receives `--fullsend-dir=.fullsend` and `--target-repo=.`. In per-org mode: - Enrollment is validated against `config.yaml`. - Two checkouts: `.fullsend` repo (config), then target repo into `target-repo/`. +- The "Prepare workspace" step runs with root=`.` — populates `agents/`, `skills/`, etc. from upstream, then copies `customized/*` on top. - `fullsend run` receives `--fullsend-dir=.` and `--target-repo=target-repo`. ### 6. Credential models -Per-repo supports two credential models: - -**Model A: Token mint (default, [ADR 0029](0029-central-token-mint-secretless-fullsend.md))** - -GitHub Apps managed by the mint operator are installed on the repo. The -`mint-token` composite action exchanges a GitHub OIDC token for a scoped -GitHub App installation token — no PEMs, client IDs, or App secrets in the -repo. The action takes a `role` parameter (triage, coder, review, fix) and -the mint looks up the correct App PEM in GCP Secret Manager using the -`repository_owner` claim from the OIDC token. +[ADR 0029](0029-central-token-mint-secretless-fullsend.md) defines three +installation profiles based on who owns the GitHub Apps and the token mint. +Role-only PEM naming (`fullsend-{role}-app-pem`, no org prefix) and +`--public` Apps enable shared Apps across orgs — onboarding a new org to +a shared mint requires zero Secret Manager work since the PEM is already +stored from when the App was created. + +Per-repo maps to these profiles: + +| Profile | Who manages Apps + mint | Per-repo user does | +|---------|------------------------|--------------------| +| **SaaS** (default) | Platform operator (fullsend-ai) pre-provisions shared public Apps and mint | Install shared Apps on repo, set `FULLSEND_MINT_URL` | +| **Bundled** | Enterprise admin runs one mint + shared `--public` Apps for multiple orgs | Install shared Apps on repo, point at enterprise mint URL | +| **Self-managed** | Per-repo user deploys own mint + own Apps | `fullsend init --mint-project=my-proj` creates everything | + +**SaaS profile (default)**: The simplest path. Shared public Apps +(`fullsend-triage`, `fullsend-coder`, `fullsend-review`) are pre-created +by the platform operator and installed on the per-repo user's repo (requires +org admin approval). The `mint-token` composite action exchanges a GitHub +OIDC token for a scoped installation token — no PEMs, client IDs, or App +secrets in the repo. The mint looks up the PEM via role-only naming +(`fullsend-{role}-app-pem`) in Secret Manager. + +**Self-managed profile**: For users who want full control — same per-role +Apps ([ADR 0007](0007-per-role-github-apps.md)), but user-owned. The user +deploys their own mint and creates their own Apps via `fullsend init +--mint-project=my-proj`. The mint's `job_workflow_ref` validation accepts both patterns: - `{org}/.fullsend/.github/workflows/*.yml@*` (per-org) @@ -188,53 +234,42 @@ The mint's `job_workflow_ref` validation accepts both patterns: The `repository_owner` claim scopes tokens to the calling org/user. `ALLOWED_ORGS` on the mint controls which orgs may request tokens. -**Model B: Own Apps (self-managed)** - -For users who want full control over their GitHub Apps — same per-role model -as per-org ([ADR 0007](0007-per-role-github-apps.md)), but Apps are -user-owned and installed on specific repos: - -| App | Role | Key permissions | -|-----|------|-----------------| -| `{user}-triage` | Triage | `issues:write` | -| `{user}-coder` | Code + fix | `contents:write`, `pull-requests:write`, `issues:read`, `checks:read` | -| `{user}-review` | Review | `contents:read`, `pull-requests:write`, `issues:read`, `checks:read` | - -PEMs stored in the user's own GCP Secret Manager project (accessed via the -same `mint-token` action pointed at a self-hosted mint) or as repo secrets -with direct App token generation via `actions/create-github-app-token`. - -**Credential detection**: The `mint-token` composite action is the default -path. Reusable workflows call `mint-token` with the agent role; the action -handles OIDC exchange and token scoping. For self-managed Apps using direct -token generation, the reusable workflow falls back to -`actions/create-github-app-token` when `FULLSEND_MINT_URL` is not set and -App PEM secrets are present. - ### 7. CLI support: `fullsend init` A new CLI command for per-repo setup: ``` -fullsend init [--mint-url URL] [--own-apps] +fullsend init [--mint-url URL] [--mint-project PROJECT] ``` +**SaaS profile (default)**: + 1. Generates `.github/workflows/fullsend.yml` from template. -2. Sets `FULLSEND_MINT_URL` as a repo variable (default: public mint). -3. Guides user to install the shared fullsend Apps on their repo. +2. Sets `FULLSEND_MINT_URL` as a repo variable (default: platform mint). +3. Guides user to install the shared public fullsend Apps on their repo. 4. Optionally creates `.fullsend/` directory with default config. -With `--own-apps`, the user creates their own GitHub Apps via the manifest -flow instead of using the shared mint. PEMs are stored in the user's own -GCP Secret Manager project. +No Apps to create, no PEMs to manage, no GCP project required for +credentials. The platform operator's shared Apps and mint handle everything. + +**Self-managed profile** (`--mint-project`): + +1. Deploys a mint to the user's GCP project. +2. Creates per-role Apps via the manifest flow (`fullsend-triage`, + `fullsend-coder`, `fullsend-review`) and stores PEMs in Secret Manager + using role-only naming (`fullsend-{role}-app-pem`). +3. Generates the shim workflow and sets `FULLSEND_MINT_URL` to the + user's mint. ### 8. Coexistence Per-repo and per-org coexist within the same org. Some repos use the org `.fullsend` config repo (per-org), others run independently (per-repo). There is no conflict — they use different dispatch paths and credential stores. +A mixed-visibility org is a natural fit: public repos use per-org with a public `.fullsend`, while private repos use per-repo to avoid routing event payloads through a public config repo. Per-repo should be the default recommendation for any private repo. + Migration between models is straightforward: - **Per-repo → per-org**: Remove workflow file from target repo, add to `.fullsend/config.yaml` enrollment. -- **Per-org → per-repo**: Remove from enrollment, add workflow file and secrets to target repo. +- **Per-org → per-repo**: Remove from enrollment, add workflow file and set `FULLSEND_MINT_URL` as a repo variable. ## Consequences @@ -242,9 +277,10 @@ Migration between models is straightforward: - **No org admin required**: Repo admins can adopt fullsend without org-level access or coordination (though org admin is still needed to install the GitHub Apps on the repo). - **Self-contained**: Everything fullsend needs lives in one repo — simpler mental model, easier cleanup. -- **Reuses ADR 0031 infrastructure**: Per-repo adds one workflow (`reusable-fullsend.yml`); all other reusable workflows and the four composite actions are shared with per-org. -- **Low entry barrier**: Copy one workflow file, install shared Apps, set mint URL — working fullsend in under 15 minutes. No PEMs or client IDs to manage. +- **Reuses ADR 0031/0034/0035 infrastructure**: Per-repo adds one workflow (`reusable-dispatch.yml`); all stage reusable workflows, composite actions, and layered content resolution are shared with per-org. +- **Low entry barrier**: Install shared public Apps on repo, copy one workflow file, set mint URL — working fullsend in under 15 minutes. No Apps to create, no PEMs, no GCP project for credentials (SaaS profile). - **Reduced blast radius**: Token mint scopes tokens to the requesting repo via the `repository` OIDC claim. Credential compromise affects only the single repo. +- **Private repo safe by default**: Per-repo workflows run in the repo's own context — event payloads and logs never transit a public `.fullsend` config repo. Per-org requires choosing between a public `.fullsend` (content exposure risk for private enrolled repos) or a private `.fullsend` (blocks public enrolled repos). Per-repo eliminates this tradeoff. - **Same agent behavior**: Triage → Code → Review → Fix workflow is identical from the user's perspective. ### Negative @@ -252,41 +288,46 @@ Migration between models is straightforward: - **Org admin still needed for App installation**: While per-repo removes the need for org admin to run `fullsend admin install`, an org admin must still approve the GitHub App installation on the repo. - **Config governance weaker**: In per-org, agent config lives in a separate repo with its own CODEOWNERS. In per-repo, `.fullsend/` config lives alongside code — a code contributor could modify agent behavior in a PR (mitigated by CODEOWNERS on `.fullsend/` and base-branch checkout). - **No centralized policy**: Per-repo users set their own policies. An org cannot enforce uniform agent behavior across independently-installed repos. -- **Self-managed Apps increase burden**: Users who opt out of the token mint (Model B) manage their own App PEM rotation and GCP Secret Manager project. +- **Self-managed profile increases burden**: Users who opt for self-managed (own mint + own Apps) manage their own App PEM rotation and GCP Secret Manager project. The SaaS profile avoids this entirely. ### Risks - **`pull_request_target` misconfiguration**: Per-repo workflows MUST use `pull_request_target` (not `pull_request`) to prevent PR authors from modifying the workflow to exfiltrate secrets. The workflow template enforces this, but users could edit it. -- **`event_payload` size**: The per-repo workflow passes `toJSON(github.event)` as a `workflow_call` input. GitHub's `workflow_call` inputs have a 65KB limit. Large PR event payloads could exceed this. +- **`event_payload` size**: Per-org's `dispatch.yml` builds a minimal payload from `$GITHUB_EVENT_PATH` (extracting only `issue`, `pull_request`, and `comment` fields), avoiding the 65KB `workflow_call` input limit. Per-repo's shim forwards `event_action` via `workflow_call` and `reusable-dispatch.yml` reads remaining context from `github.event.*` expressions, following the same pattern. Large PR event payloads are unlikely to be an issue since the shim does not pass the full payload as an input. - **App identity confusion**: Users unfamiliar with the fix→review loop requirement may attempt a single-App setup and get silent failures (no review triggered after fix pushes). ### Mitigations - **Template validation**: `fullsend init` generates the workflow file with `pull_request_target` — users who modify it are warned in documentation. CODEOWNERS on `.github/workflows/fullsend.yml` prevents unauthorized changes. -- **Payload trimming**: Start with full `toJSON(github.event)` for simplicity; add payload trimming if size becomes an issue in practice. +- **Minimal payload**: Following per-org `dispatch.yml`, `reusable-dispatch.yml` reads event context from `github.event.*` expressions (available in `workflow_call` callee context) rather than passing the full payload as an input. - **Clear error messages**: Credential auto-detection reports why coder and review Apps must be separate, with a link to setup documentation. - **Migration path**: Per-repo users who outgrow the model can migrate to per-org without changing agent behavior — the same reusable workflows power both modes. ## Open Questions -### `reusable-fullsend.yml` for per-org shim simplification - -This workflow is also useful as a per-org shim simplification (replacing the ~380-line shim + dispatcher with a thin caller). Should per-org thin shims also adopt it? +### `reusable-dispatch.yml` dispatch mechanism -**Trade-off**: Sharing the routing workflow between per-repo and per-org reduces maintenance (one routing implementation), but couples per-org dispatch to the upstream workflow's release cadence. Per-org deployments currently control their own dispatch timing. +Per-org `dispatch.yml` uses `gh workflow run` to fan out to thin caller stage workflows. `reusable-dispatch.yml` for per-repo needs a different mechanism since there are no thin callers in the target repo. Two options: -### Retro stage +1. **Conditional `workflow_call` jobs**: `reusable-dispatch.yml` defines one job per stage, each with an `if:` condition based on the routing output. Only the matched stage job runs. This keeps the pipeline within `workflow_call` but means `reusable-dispatch.yml` has ~5 job definitions. +2. **Single job with composite action**: Route in a step, then call the matched reusable workflow dynamically. GitHub Actions does not support dynamic `uses:` values, so this would require a wrapper action. -The routing logic includes a retro stage (PR closed). Reusable workflows for retro (`reusable-retro.yml`) are not yet defined in ADR 0031. This stage should be added to the reusable workflow set or explicitly deferred. +Option 1 is simpler and stays within GitHub Actions' native capabilities. ### Concurrency groups -Concurrent fullsend runs for the same issue/PR should be prevented. Options: workflow-level concurrency in the caller, or per-stage concurrency inside `reusable-fullsend.yml`. Per-stage concurrency inside the reusable workflow keeps the caller simple and applies consistently across per-repo and per-org modes. +Per-org thin callers define per-stage concurrency groups (e.g., `fullsend-code-{repo}-{issue}`). In per-repo, `reusable-dispatch.yml` handles routing and dispatch — concurrency should be set at the per-stage job level inside `reusable-dispatch.yml` to match the per-org behavior. + +### `stop-fix` job placement + +The per-org shim includes a `stop-fix` job that adds the `fullsend-no-fix` label. For per-repo, this job should live in the target repo's shim workflow (same location as per-org) since it only needs the default `GITHUB_TOKEN` — no mint or reusable workflow involvement. ## References - [ADR 0007: Per-role GitHub Apps](0007-per-role-github-apps.md) — authentication model replicated in per-repo -- [ADR 0008: workflow_dispatch for cross-repo dispatch](0008-workflow-dispatch-for-cross-repo-dispatch.md) — superseded by `workflow_call` (ADR 0029 removes the original constraint) -- [ADR 0026: Stage-based dispatch](0026-stage-based-dispatch-for-agent-workflow-decoupling.md) — routing logic extracted into `reusable-fullsend.yml` -- [ADR 0029: Central token mint](0027-central-token-mint-secretless-fullsend.md) — default credential model for per-repo -- [ADR 0031: Reusable workflows](0031-reusable-workflows-for-action-installed-distribution.md) — foundation that makes per-repo possible +- [ADR 0008: workflow_dispatch for cross-repo dispatch](0008-workflow-dispatch-for-cross-repo-dispatch.md) — superseded by `workflow_call` (ADR 0034 centralizes routing) +- [ADR 0026: Stage-based dispatch](0026-stage-based-dispatch-for-agent-workflow-decoupling.md) — stage model preserved in reusable workflows +- [ADR 0029: Central token mint](0029-central-token-mint-secretless-fullsend.md) — default credential model for per-repo +- [ADR 0031: Reusable workflows](0031-reusable-workflows-for-action-installed-distribution.md) — publishes stage reusable workflows and composite actions +- [ADR 0034: Centralized event routing](0034-centralized-event-routing.md) — routing logic in `dispatch.yml`, replicated as `reusable-dispatch.yml` for per-repo +- [ADR 0035: Layered content resolution](0035-layered-content-resolution.md) — upstream defaults sparse-checked at runtime, overrides via `customized/` (per-org) or `.fullsend/` (per-repo) From 3df07bb95bf796787a8ea3f48dec69819bee0d9e Mon Sep 17 00:00:00 2001 From: Wayne Sun Date: Sun, 10 May 2026 23:11:48 -0400 Subject: [PATCH 07/11] docs: update ADR 0033 to reflect PR 799 implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Status: Proposed → Accepted - CLI: fullsend init → fullsend admin install (per Hector's feedback) - Mode detection: document install_mode input parameter on reusable workflows - Open Questions → Resolved Questions (dispatch mechanism, concurrency, stop-fix, CLI design) - Update all fullsend init references throughout the document Signed-off-by: Wayne Sun --- docs/ADRs/0033-per-repo-installation-mode.md | 66 ++++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/docs/ADRs/0033-per-repo-installation-mode.md b/docs/ADRs/0033-per-repo-installation-mode.md index 616801fd3e..c06e41ed63 100644 --- a/docs/ADRs/0033-per-repo-installation-mode.md +++ b/docs/ADRs/0033-per-repo-installation-mode.md @@ -19,7 +19,7 @@ Date: 2026-05-06 ## Status -Proposed +Accepted ## Context @@ -76,7 +76,7 @@ Build a standalone per-repo tool or action that does not share infrastructure wi Reduce per-repo to two Apps instead of matching the full per-org app set. -**Rejected**: Dropping the triage App forces triage to share one of the other App identities, which conflates permissions (triage only needs `issues:write`, while coder has `contents:write`). The full per-role model (ADR 0007) provides least-privilege isolation. CLI automation (`fullsend init`) makes creating the Apps straightforward. +**Rejected**: Dropping the triage App forces triage to share one of the other App identities, which conflates permissions (triage only needs `issues:write`, while coder has `contents:write`). The full per-role model (ADR 0007) provides least-privilege isolation. CLI automation (`fullsend admin install`) makes creating the Apps straightforward. ## Decision @@ -183,7 +183,7 @@ In per-org mode, `dispatch.yml` routes events and dispatches to thin callers via ### 5. Per-repo mode detection -Reusable workflows detect per-repo mode when `source_repo == github.repository` — the calling repo IS the target repo. +Reusable workflows accept an `install_mode` input (`per-org` default; `per-repo` switches behavior). The shim passes `install_mode: per-repo` to `reusable-dispatch.yml`, which propagates it to per-stage reusable workflows. The `validate-enrollment` action also accepts `install_mode` and skips `config.yaml` enrollment checks in per-repo mode. In per-repo mode: - Enrollment validation is skipped (always self-enrolled). @@ -212,7 +212,7 @@ Per-repo maps to these profiles: |---------|------------------------|--------------------| | **SaaS** (default) | Platform operator (fullsend-ai) pre-provisions shared public Apps and mint | Install shared Apps on repo, set `FULLSEND_MINT_URL` | | **Bundled** | Enterprise admin runs one mint + shared `--public` Apps for multiple orgs | Install shared Apps on repo, point at enterprise mint URL | -| **Self-managed** | Per-repo user deploys own mint + own Apps | `fullsend init --mint-project=my-proj` creates everything | +| **Self-managed** | Per-repo user deploys own mint + own Apps | `fullsend admin install owner/repo --mint-project=my-proj` creates everything | **SaaS profile (default)**: The simplest path. Shared public Apps (`fullsend-triage`, `fullsend-coder`, `fullsend-review`) are pre-created @@ -224,8 +224,8 @@ secrets in the repo. The mint looks up the PEM via role-only naming **Self-managed profile**: For users who want full control — same per-role Apps ([ADR 0007](0007-per-role-github-apps.md)), but user-owned. The user -deploys their own mint and creates their own Apps via `fullsend init ---mint-project=my-proj`. +deploys their own mint and creates their own Apps via `fullsend admin +install owner/repo --mint-project=my-proj`. The mint's `job_workflow_ref` validation accepts both patterns: - `{org}/.fullsend/.github/workflows/*.yml@*` (per-org) @@ -234,32 +234,33 @@ The mint's `job_workflow_ref` validation accepts both patterns: The `repository_owner` claim scopes tokens to the calling org/user. `ALLOWED_ORGS` on the mint controls which orgs may request tokens. -### 7. CLI support: `fullsend init` +### 7. CLI support: `fullsend admin install ` -A new CLI command for per-repo setup: +The existing `fullsend admin install` command handles both per-org and per-repo modes. The argument format determines the mode: ``` -fullsend init [--mint-url URL] [--mint-project PROJECT] +fullsend admin install # per-org installation +fullsend admin install # per-repo installation ``` -**SaaS profile (default)**: +Per-repo flags (only valid with `owner/repo` argument): +- `--mint-url` — token mint URL for OIDC token exchange (required) +- `--gcp-auth-mode` — GCP authentication mode: `wif` or `sa_key` (default: `sa_key`) +- `--scaffold-customized` — create `.fullsend/customized/` directory structure -1. Generates `.github/workflows/fullsend.yml` from template. -2. Sets `FULLSEND_MINT_URL` as a repo variable (default: platform mint). -3. Guides user to install the shared public fullsend Apps on their repo. -4. Optionally creates `.fullsend/` directory with default config. +Per-org-only flags (`--mint-project`, `--mint-region`, `--public`, etc.) are rejected when an `owner/repo` argument is given. -No Apps to create, no PEMs to manage, no GCP project required for -credentials. The platform operator's shared Apps and mint handle everything. +**Per-repo install steps**: -**Self-managed profile** (`--mint-project`): +1. Generates `.github/workflows/fullsend.yml` from the per-repo shim template. +2. Generates `.fullsend/config.yaml` with agent roles and kill switch. +3. Optionally creates `.fullsend/customized/` directory structure (`--scaffold-customized`). +4. Commits all scaffold files to the target repo via the GitHub API. +5. Sets repository variables (`FULLSEND_MINT_URL`, `FULLSEND_GCP_REGION`, `FULLSEND_GCP_AUTH_MODE`). +6. Sets repository secrets (`FULLSEND_GCP_PROJECT_ID`, and either WIF credentials or SA key JSON depending on `--gcp-auth-mode`). +7. In WIF mode, auto-provisions WIF pool/provider/service account if `--gcp-wif-provider` is omitted. -1. Deploys a mint to the user's GCP project. -2. Creates per-role Apps via the manifest flow (`fullsend-triage`, - `fullsend-coder`, `fullsend-review`) and stores PEMs in Secret Manager - using role-only naming (`fullsend-{role}-app-pem`). -3. Generates the shim workflow and sets `FULLSEND_MINT_URL` to the - user's mint. +Per-repo install requires only `repo` and `workflow` OAuth scopes (no `admin:org`). ### 8. Coexistence @@ -298,29 +299,28 @@ Migration between models is straightforward: ### Mitigations -- **Template validation**: `fullsend init` generates the workflow file with `pull_request_target` — users who modify it are warned in documentation. CODEOWNERS on `.github/workflows/fullsend.yml` prevents unauthorized changes. +- **Template validation**: `fullsend admin install` generates the workflow file with `pull_request_target` — users who modify it are warned in documentation. CODEOWNERS on `.github/workflows/fullsend.yml` prevents unauthorized changes. - **Minimal payload**: Following per-org `dispatch.yml`, `reusable-dispatch.yml` reads event context from `github.event.*` expressions (available in `workflow_call` callee context) rather than passing the full payload as an input. - **Clear error messages**: Credential auto-detection reports why coder and review Apps must be separate, with a link to setup documentation. - **Migration path**: Per-repo users who outgrow the model can migrate to per-org without changing agent behavior — the same reusable workflows power both modes. -## Open Questions +## Resolved Questions ### `reusable-dispatch.yml` dispatch mechanism -Per-org `dispatch.yml` uses `gh workflow run` to fan out to thin caller stage workflows. `reusable-dispatch.yml` for per-repo needs a different mechanism since there are no thin callers in the target repo. Two options: - -1. **Conditional `workflow_call` jobs**: `reusable-dispatch.yml` defines one job per stage, each with an `if:` condition based on the routing output. Only the matched stage job runs. This keeps the pipeline within `workflow_call` but means `reusable-dispatch.yml` has ~5 job definitions. -2. **Single job with composite action**: Route in a step, then call the matched reusable workflow dynamically. GitHub Actions does not support dynamic `uses:` values, so this would require a wrapper action. - -Option 1 is simpler and stays within GitHub Actions' native capabilities. +**Resolved (PR #799):** Option 1 — conditional `workflow_call` jobs. `reusable-dispatch.yml` defines one job per stage (`triage`, `code`, `review`, `fix`, `retro`), each with an `if:` condition based on the routing output. Only the matched stage job runs. This keeps the pipeline within `workflow_call` and stays within GitHub Actions' native capabilities. The nesting depth is 3 levels (shim → `reusable-dispatch.yml` → `reusable-{stage}.yml`), within GitHub's 4-level limit. ### Concurrency groups -Per-org thin callers define per-stage concurrency groups (e.g., `fullsend-code-{repo}-{issue}`). In per-repo, `reusable-dispatch.yml` handles routing and dispatch — concurrency should be set at the per-stage job level inside `reusable-dispatch.yml` to match the per-org behavior. +**Resolved (PR #799):** Concurrency groups are set at the per-stage job level inside `reusable-dispatch.yml`, matching the per-org behavior. ### `stop-fix` job placement -The per-org shim includes a `stop-fix` job that adds the `fullsend-no-fix` label. For per-repo, this job should live in the target repo's shim workflow (same location as per-org) since it only needs the default `GITHUB_TOKEN` — no mint or reusable workflow involvement. +**Resolved (PR #799):** The `stop-fix` job lives in the target repo's shim workflow (same location as per-org) since it only needs the default `GITHUB_TOKEN` — no mint or reusable workflow involvement. + +### CLI command design + +**Resolved (PR #799):** Per-repo uses the existing `fullsend admin install` command rather than a separate `fullsend init` subcommand. The argument format determines the mode: `fullsend admin install ` for per-org, `fullsend admin install ` for per-repo. Per-org-only and per-repo-only flags are validated and rejected when used with the wrong mode. ## References From 3e4a50c7e148813a5a0ff1dcde2685df588a5da9 Mon Sep 17 00:00:00 2001 From: Wayne Sun Date: Sun, 10 May 2026 23:18:43 -0400 Subject: [PATCH 08/11] docs: address Ralph's review feedback on ADR 0033 - Fix event suppression wording: token-based, not ownership-based - Add credential separation collapse as explicit negative consequence - Specify base-branch-only config reads for .fullsend/ and AGENTS.md - Expand Risks section with threat-priority ordering, pwn request surface, insider modification risk, and CODEOWNERS mitigation - Fix broken ADR 0034 cross-reference link filename Signed-off-by: Wayne Sun --- docs/ADRs/0033-per-repo-installation-mode.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/docs/ADRs/0033-per-repo-installation-mode.md b/docs/ADRs/0033-per-repo-installation-mode.md index c06e41ed63..b964c00ace 100644 --- a/docs/ADRs/0033-per-repo-installation-mode.md +++ b/docs/ADRs/0033-per-repo-installation-mode.md @@ -37,7 +37,7 @@ Three ADRs and the implementation in PR 792 create the building blocks that make - [ADR 0029](0029-central-token-mint-secretless-fullsend.md) replaces PEM secrets and dispatch PATs with OIDC-based credential issuance via a central token mint. The `mint-token` composite action takes a role name (triage, coder, review, fix) and returns a scoped GitHub App installation token — no PEMs or client IDs in the calling repo. - [ADR 0031](0031-reusable-workflows-for-action-installed-distribution.md) publishes five reusable workflows (`reusable-triage.yml`, `reusable-code.yml`, `reusable-review.yml`, `reusable-fix.yml`, `reusable-retro.yml`) and four composite actions (`fullsend`, `mint-token`, `validate-enrollment`, `setup-gcp`) from `fullsend-ai/fullsend`, enabling any repo to call fullsend infrastructure via `workflow_call` without copying workflow files. Scaffold stage workflows in `.fullsend` are now thin callers (41–66 lines) that delegate to these reusable workflows. -- [ADR 0034](0034-centralized-event-routing.md) centralizes event-to-stage routing in `dispatch.yml` within the `.fullsend` config repo. The enrolled-repo shim (~70 lines) forwards raw event context to `dispatch.yml` via `workflow_call`; `dispatch.yml` (~370 lines) determines the stage, mints an OIDC dispatch token, validates the stage, checks the kill switch, and dispatches to the matching thin caller via `gh workflow run`. Adding a new stage requires only a case branch in `dispatch.yml` — zero changes to enrolled repos. +- [ADR 0034](0034-centralized-shim-routing-via-dispatch.md) centralizes event-to-stage routing in `dispatch.yml` within the `.fullsend` config repo. The enrolled-repo shim (~70 lines) forwards raw event context to `dispatch.yml` via `workflow_call`; `dispatch.yml` (~370 lines) determines the stage, mints an OIDC dispatch token, validates the stage, checks the kill switch, and dispatches to the matching thin caller via `gh workflow run`. Adding a new stage requires only a case branch in `dispatch.yml` — zero changes to enrolled repos. - [ADR 0035](0035-layered-content-resolution.md) introduces layered content resolution: upstream defaults (agents, skills, schemas, harness, policies, scripts) are sparse-checked from `fullsend-ai/fullsend` at runtime, then org overrides from `customized/` are copied on top. The scaffold installs only org-specific files (~23 files instead of ~68). The per-org flow after PR 792: @@ -64,7 +64,7 @@ Run `fullsend admin install` targeting a single repo instead of an org. Copy all Use one GitHub App for triage, code, review, and fix roles to simplify per-repo setup. -**Rejected**: GitHub suppresses `pull_request_target` events when the triggering token belongs to the same App that owns the workflow. The fix→review loop requires the coder/fix agent to push commits that trigger review — if both roles share one App, the event is silently suppressed and the feedback cycle breaks. At minimum, coder and review must be separate Apps. +**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. ### Alternative 3: Per-repo as a separate codebase @@ -163,6 +163,8 @@ fullsend-ai/fullsend defaults < .fullsend/customized/ < AGENTS.md The org-level `.fullsend` config repo tier is skipped — the in-repo `.fullsend/` directory serves as the config workspace. The reusable workflows' "Prepare workspace" step is parameterized by root directory: `.` for per-org (the `.fullsend` repo checkout), `.fullsend/` for per-repo. In both modes, it sparse-checkouts upstream defaults into `{root}/agents/`, `{root}/skills/`, etc., then copies `{root}/customized/*` on top — identical code path, different root. +**Git ref for config reads**: In per-repo mode, `.fullsend/`, `AGENTS.md`, and `.github/workflows/fullsend.yml` are always read from the **base branch** (the default branch of the repository), not the PR head branch. This is enforced by `pull_request_target`, which checks out the base branch by default. The reusable workflows do not check out the PR head ref for config or agent instructions — only the target repo's source code is checked out from the PR head for the agent to operate on. This prevents PR authors from injecting modified agent instructions, policies, or workflow files via their PR — the project's #1 threat category (external prompt injection). + ### 4. The `reusable-dispatch.yml` workflow This is the key new artifact, published in `fullsend-ai/fullsend/.github/workflows/`. It is a reusable version of the per-org `dispatch.yml` (ADR 0034), accepting event context via `workflow_call` inputs and performing the same routing and dispatch logic. @@ -289,17 +291,24 @@ Migration between models is straightforward: - **Org admin still needed for App installation**: While per-repo removes the need for org admin to run `fullsend admin install`, an org admin must still approve the GitHub App installation on the repo. - **Config governance weaker**: In per-org, agent config lives in a separate repo with its own CODEOWNERS. In per-repo, `.fullsend/` config lives alongside code — a code contributor could modify agent behavior in a PR (mitigated by CODEOWNERS on `.fullsend/` and base-branch checkout). - **No centralized policy**: Per-repo users set their own policies. An org cannot enforce uniform agent behavior across independently-installed repos. +- **Credential separation collapses**: In per-org mode (ADR 0008), the dispatch PAT only grants `actions:write` on `.fullsend` — enrolled repos can trigger dispatch but never access PEM secrets. In per-repo mode, the repo that triggers workflows IS the repo holding secrets (WIF provider, SA key, or GCP project ID), eliminating this credential separation. A compromised repo contributor with write access could potentially access secrets stored at the repo level. The token mint mitigates this partially — PEMs remain in Secret Manager, not in the repo — but repo-level secrets (`FULLSEND_GCP_WIF_PROVIDER`, `FULLSEND_GCP_PROJECT_ID`) are still co-located with the code. - **Self-managed profile increases burden**: Users who opt for self-managed (own mint + own Apps) manage their own App PEM rotation and GCP Secret Manager project. The SaaS profile avoids this entirely. ### Risks -- **`pull_request_target` misconfiguration**: Per-repo workflows MUST use `pull_request_target` (not `pull_request`) to prevent PR authors from modifying the workflow to exfiltrate secrets. The workflow template enforces this, but users could edit it. +Ordered by the project's threat priority (external injection > insider > drift > supply chain): + +- **External injection — `pull_request_target` misconfiguration**: Per-repo workflows MUST use `pull_request_target` (not `pull_request`) to prevent PR authors from modifying the workflow to exfiltrate secrets. In per-repo mode, the workflow file lives in the same repo as the code — unlike per-org where the shim is pushed by the orchestrator app. A contributor with write access could change `pull_request_target` to `pull_request` in a PR, and if that PR is merged, subsequent PRs could exfiltrate secrets. The workflow template enforces `pull_request_target`, but users could edit it. +- **External injection — untrusted code checkout**: `pull_request_target` triggers reusable workflows that check out and execute against the PR's head ref for source code — the classic "pwn request" surface. This is distinct from workflow modification risk: even with the correct trigger, the agent sandbox executes untrusted code from the PR. The agent sandbox, restricted tool permissions, and base-branch-only config reads (see Config layering) mitigate this surface. +- **Insider — workflow and config modification**: In per-repo mode, `.github/workflows/fullsend.yml` and `.fullsend/` live alongside code. A contributor with write access could modify agent behavior, sandbox policies, or the workflow trigger in a PR. Without CODEOWNERS protection, these changes could be merged by any approver. - **`event_payload` size**: Per-org's `dispatch.yml` builds a minimal payload from `$GITHUB_EVENT_PATH` (extracting only `issue`, `pull_request`, and `comment` fields), avoiding the 65KB `workflow_call` input limit. Per-repo's shim forwards `event_action` via `workflow_call` and `reusable-dispatch.yml` reads remaining context from `github.event.*` expressions, following the same pattern. Large PR event payloads are unlikely to be an issue since the shim does not pass the full payload as an input. - **App identity confusion**: Users unfamiliar with the fix→review loop requirement may attempt a single-App setup and get silent failures (no review triggered after fix pushes). ### Mitigations -- **Template validation**: `fullsend admin install` generates the workflow file with `pull_request_target` — users who modify it are warned in documentation. CODEOWNERS on `.github/workflows/fullsend.yml` prevents unauthorized changes. +- **CODEOWNERS on workflow and config**: `fullsend admin install` should add CODEOWNERS entries protecting `.github/workflows/fullsend.yml` and `.fullsend/` so that changes to the workflow trigger, agent config, and sandbox policies require approval from designated owners. This is the primary defense against both workflow misconfiguration and insider modification of agent behavior. In per-org mode, the `.fullsend` config repo has its own CODEOWNERS; per-repo must replicate this governance at the file level. +- **Base-branch config reads**: Reusable workflows read `.fullsend/`, `AGENTS.md`, and workflow files from the base branch only (enforced by `pull_request_target`). PR authors cannot inject modified agent instructions or policies via their PR. +- **Template validation**: `fullsend admin install` generates the workflow file with `pull_request_target`. Users who modify it are warned in documentation. - **Minimal payload**: Following per-org `dispatch.yml`, `reusable-dispatch.yml` reads event context from `github.event.*` expressions (available in `workflow_call` callee context) rather than passing the full payload as an input. - **Clear error messages**: Credential auto-detection reports why coder and review Apps must be separate, with a link to setup documentation. - **Migration path**: Per-repo users who outgrow the model can migrate to per-org without changing agent behavior — the same reusable workflows power both modes. @@ -329,5 +338,5 @@ Migration between models is straightforward: - [ADR 0026: Stage-based dispatch](0026-stage-based-dispatch-for-agent-workflow-decoupling.md) — stage model preserved in reusable workflows - [ADR 0029: Central token mint](0029-central-token-mint-secretless-fullsend.md) — default credential model for per-repo - [ADR 0031: Reusable workflows](0031-reusable-workflows-for-action-installed-distribution.md) — publishes stage reusable workflows and composite actions -- [ADR 0034: Centralized event routing](0034-centralized-event-routing.md) — routing logic in `dispatch.yml`, replicated as `reusable-dispatch.yml` for per-repo +- [ADR 0034: Centralized event routing](0034-centralized-shim-routing-via-dispatch.md) — routing logic in `dispatch.yml`, replicated as `reusable-dispatch.yml` for per-repo - [ADR 0035: Layered content resolution](0035-layered-content-resolution.md) — upstream defaults sparse-checked at runtime, overrides via `customized/` (per-org) or `.fullsend/` (per-repo) From 89cd3a9e0db87631b6225a3db260fbc8e8ec0cb1 Mon Sep 17 00:00:00 2001 From: Wayne Sun Date: Mon, 11 May 2026 07:28:53 -0400 Subject: [PATCH 09/11] docs: fix dispatch mechanism from gh workflow run to workflow_call in ADR 0033 The token mint (ADR 0029) migrates per-org dispatch from workflow_dispatch + gh workflow run to native workflow_call. Update all diagrams and descriptions to reflect the post-token-mint mechanism. Signed-off-by: Wayne Sun --- docs/ADRs/0033-per-repo-installation-mode.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/ADRs/0033-per-repo-installation-mode.md b/docs/ADRs/0033-per-repo-installation-mode.md index b964c00ace..178ed68123 100644 --- a/docs/ADRs/0033-per-repo-installation-mode.md +++ b/docs/ADRs/0033-per-repo-installation-mode.md @@ -37,7 +37,7 @@ Three ADRs and the implementation in PR 792 create the building blocks that make - [ADR 0029](0029-central-token-mint-secretless-fullsend.md) replaces PEM secrets and dispatch PATs with OIDC-based credential issuance via a central token mint. The `mint-token` composite action takes a role name (triage, coder, review, fix) and returns a scoped GitHub App installation token — no PEMs or client IDs in the calling repo. - [ADR 0031](0031-reusable-workflows-for-action-installed-distribution.md) publishes five reusable workflows (`reusable-triage.yml`, `reusable-code.yml`, `reusable-review.yml`, `reusable-fix.yml`, `reusable-retro.yml`) and four composite actions (`fullsend`, `mint-token`, `validate-enrollment`, `setup-gcp`) from `fullsend-ai/fullsend`, enabling any repo to call fullsend infrastructure via `workflow_call` without copying workflow files. Scaffold stage workflows in `.fullsend` are now thin callers (41–66 lines) that delegate to these reusable workflows. -- [ADR 0034](0034-centralized-shim-routing-via-dispatch.md) centralizes event-to-stage routing in `dispatch.yml` within the `.fullsend` config repo. The enrolled-repo shim (~70 lines) forwards raw event context to `dispatch.yml` via `workflow_call`; `dispatch.yml` (~370 lines) determines the stage, mints an OIDC dispatch token, validates the stage, checks the kill switch, and dispatches to the matching thin caller via `gh workflow run`. Adding a new stage requires only a case branch in `dispatch.yml` — zero changes to enrolled repos. +- [ADR 0034](0034-centralized-shim-routing-via-dispatch.md) centralizes event-to-stage routing in `dispatch.yml` within the `.fullsend` config repo. The enrolled-repo shim (~70 lines) forwards raw event context to `dispatch.yml` via `workflow_call`; `dispatch.yml` (~370 lines) determines the stage, mints an OIDC dispatch token, validates the stage, checks the kill switch, and dispatches to the matching thin caller via `workflow_call`. Adding a new stage requires only a case branch in `dispatch.yml` — zero changes to enrolled repos. - [ADR 0035](0035-layered-content-resolution.md) introduces layered content resolution: upstream defaults (agents, skills, schemas, harness, policies, scripts) are sparse-checked from `fullsend-ai/fullsend` at runtime, then org overrides from `customized/` are copied on top. The scaffold installs only org-specific files (~23 files instead of ~68). The per-org flow after PR 792: @@ -45,7 +45,7 @@ The per-org flow after PR 792: ``` enrolled repo (shim, ~70 lines) └─ workflow_call ─→ .fullsend/dispatch.yml (routing, ~370 lines) - └─ gh workflow run ─→ .fullsend/code.yml (thin caller, ~43 lines) + └─ workflow_call ───→ .fullsend/code.yml (thin caller, ~43 lines) └─ uses: fullsend-ai/fullsend/reusable-code.yml@v1 (workspace prep, mint-token, agent run) ``` @@ -95,7 +95,7 @@ ENROLLED REPO .FULLSEND CONFIG REPO ───────────── ───────────────────── fullsend.yml (shim, ~70 lines) dispatch.yml (routing, ~370 lines) │ workflow_call │ determines stage - └──────────────────────────────────────┘ gh workflow run + └──────────────────────────────────────┘ workflow_call │ code.yml / review.yml / ... (thin callers, ~43 lines) │ uses: (workflow_call) @@ -177,9 +177,9 @@ The routing logic (identical to per-org `dispatch.yml`) maps: - `pull_request_target` + `closed` → retro - `pull_request_review` + `changes_requested` from review bot → fix (same-repo PRs only) -In per-org mode, `dispatch.yml` routes events and dispatches to thin callers via `gh workflow run` (breaking the `workflow_call` chain). In per-repo mode, `reusable-dispatch.yml` routes events and dispatches to per-stage reusable workflows directly via conditional `workflow_call` jobs, keeping the entire pipeline within a single `workflow_call` chain. +In per-org mode, `dispatch.yml` routes events and dispatches to thin callers via `workflow_call`. In per-repo mode, `reusable-dispatch.yml` routes events and dispatches to per-stage reusable workflows directly via conditional `workflow_call` jobs, keeping the entire pipeline within a single `workflow_call` chain. -**Dispatch mechanism**: Per-org uses `gh workflow run` (workflow_dispatch) to fan out to thin callers — this avoids deep `workflow_call` nesting but requires thin caller workflow files in `.fullsend`. Per-repo uses conditional `workflow_call` jobs inside `reusable-dispatch.yml` to call `reusable-code.yml` etc. directly, eliminating the need for thin callers. +**Dispatch mechanism**: Per-org uses `workflow_call` to fan out to thin callers in `.fullsend`, which in turn call upstream reusable workflows via `workflow_call`. Per-repo uses conditional `workflow_call` jobs inside `reusable-dispatch.yml` to call `reusable-code.yml` etc. directly, eliminating the need for thin callers. **Nesting depth**: target-repo shim → `reusable-dispatch.yml` → `reusable-code.yml` = 2 levels of `workflow_call` (GitHub limit is 4). From e3b71fc2d1de22cd57c3fa7ab644d60120f60545 Mon Sep 17 00:00:00 2001 From: Wayne Sun Date: Mon, 11 May 2026 10:21:14 -0400 Subject: [PATCH 10/11] docs: fix frontmatter status to match Accepted in ADR 0033 Signed-off-by: Wayne Sun --- docs/ADRs/0033-per-repo-installation-mode.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ADRs/0033-per-repo-installation-mode.md b/docs/ADRs/0033-per-repo-installation-mode.md index 178ed68123..76507a5b71 100644 --- a/docs/ADRs/0033-per-repo-installation-mode.md +++ b/docs/ADRs/0033-per-repo-installation-mode.md @@ -1,6 +1,6 @@ --- title: "33. Per-repo installation mode" -status: Proposed +status: Accepted relates_to: - agent-infrastructure - agent-architecture From b553cadad7b3b238b5974e1f451be425f4f71c6e Mon Sep 17 00:00:00 2001 From: Wayne Sun Date: Mon, 11 May 2026 11:01:43 -0400 Subject: [PATCH 11/11] docs: remove broken links to unmerged ADRs 0029 and 0035 The markdown link linter checks that linked files exist. ADR 0029 (token mint) and ADR 0035 (layered content) haven't landed yet, so convert their markdown links to plain text references. Signed-off-by: Wayne Sun --- docs/ADRs/0033-per-repo-installation-mode.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/ADRs/0033-per-repo-installation-mode.md b/docs/ADRs/0033-per-repo-installation-mode.md index 76507a5b71..a2a21ab385 100644 --- a/docs/ADRs/0033-per-repo-installation-mode.md +++ b/docs/ADRs/0033-per-repo-installation-mode.md @@ -23,7 +23,7 @@ Accepted ## Context -Fullsend's installation model is per-org: `fullsend admin install` creates a dedicated `.fullsend` config repo, per-role GitHub Apps ([ADR 0007](0007-per-role-github-apps.md)), shim workflows in enrolled repos, and a central token mint for OIDC-based credential issuance ([ADR 0029](0029-central-token-mint-secretless-fullsend.md)). This requires org admin access and assumes all enrolled repos share agent configuration, credentials, and policies. +Fullsend's installation model is per-org: `fullsend admin install` creates a dedicated `.fullsend` config repo, per-role GitHub Apps ([ADR 0007](0007-per-role-github-apps.md)), shim workflows in enrolled repos, and a central token mint for OIDC-based credential issuance (ADR 0029). This requires org admin access and assumes all enrolled repos share agent configuration, credentials, and policies. Some users cannot or do not want to use the per-org model: @@ -35,10 +35,10 @@ Some users cannot or do not want to use the per-org model: Three ADRs and the implementation in PR 792 create the building blocks that make per-repo possible: -- [ADR 0029](0029-central-token-mint-secretless-fullsend.md) replaces PEM secrets and dispatch PATs with OIDC-based credential issuance via a central token mint. The `mint-token` composite action takes a role name (triage, coder, review, fix) and returns a scoped GitHub App installation token — no PEMs or client IDs in the calling repo. +- ADR 0029 replaces PEM secrets and dispatch PATs with OIDC-based credential issuance via a central token mint. The `mint-token` composite action takes a role name (triage, coder, review, fix) and returns a scoped GitHub App installation token — no PEMs or client IDs in the calling repo. - [ADR 0031](0031-reusable-workflows-for-action-installed-distribution.md) publishes five reusable workflows (`reusable-triage.yml`, `reusable-code.yml`, `reusable-review.yml`, `reusable-fix.yml`, `reusable-retro.yml`) and four composite actions (`fullsend`, `mint-token`, `validate-enrollment`, `setup-gcp`) from `fullsend-ai/fullsend`, enabling any repo to call fullsend infrastructure via `workflow_call` without copying workflow files. Scaffold stage workflows in `.fullsend` are now thin callers (41–66 lines) that delegate to these reusable workflows. - [ADR 0034](0034-centralized-shim-routing-via-dispatch.md) centralizes event-to-stage routing in `dispatch.yml` within the `.fullsend` config repo. The enrolled-repo shim (~70 lines) forwards raw event context to `dispatch.yml` via `workflow_call`; `dispatch.yml` (~370 lines) determines the stage, mints an OIDC dispatch token, validates the stage, checks the kill switch, and dispatches to the matching thin caller via `workflow_call`. Adding a new stage requires only a case branch in `dispatch.yml` — zero changes to enrolled repos. -- [ADR 0035](0035-layered-content-resolution.md) introduces layered content resolution: upstream defaults (agents, skills, schemas, harness, policies, scripts) are sparse-checked from `fullsend-ai/fullsend` at runtime, then org overrides from `customized/` are copied on top. The scaffold installs only org-specific files (~23 files instead of ~68). +- ADR 0035 introduces layered content resolution: upstream defaults (agents, skills, schemas, harness, policies, scripts) are sparse-checked from `fullsend-ai/fullsend` at runtime, then org overrides from `customized/` are copied on top. The scaffold installs only org-specific files (~23 files instead of ~68). The per-org flow after PR 792: @@ -201,7 +201,7 @@ In per-org mode: ### 6. Credential models -[ADR 0029](0029-central-token-mint-secretless-fullsend.md) defines three +ADR 0029 defines three installation profiles based on who owns the GitHub Apps and the token mint. Role-only PEM naming (`fullsend-{role}-app-pem`, no org prefix) and `--public` Apps enable shared Apps across orgs — onboarding a new org to @@ -336,7 +336,7 @@ Ordered by the project's threat priority (external injection > insider > drift > - [ADR 0007: Per-role GitHub Apps](0007-per-role-github-apps.md) — authentication model replicated in per-repo - [ADR 0008: workflow_dispatch for cross-repo dispatch](0008-workflow-dispatch-for-cross-repo-dispatch.md) — superseded by `workflow_call` (ADR 0034 centralizes routing) - [ADR 0026: Stage-based dispatch](0026-stage-based-dispatch-for-agent-workflow-decoupling.md) — stage model preserved in reusable workflows -- [ADR 0029: Central token mint](0029-central-token-mint-secretless-fullsend.md) — default credential model for per-repo +- ADR 0029: Central token mint — default credential model for per-repo - [ADR 0031: Reusable workflows](0031-reusable-workflows-for-action-installed-distribution.md) — publishes stage reusable workflows and composite actions - [ADR 0034: Centralized event routing](0034-centralized-shim-routing-via-dispatch.md) — routing logic in `dispatch.yml`, replicated as `reusable-dispatch.yml` for per-repo -- [ADR 0035: Layered content resolution](0035-layered-content-resolution.md) — upstream defaults sparse-checked at runtime, overrides via `customized/` (per-org) or `.fullsend/` (per-repo) +- ADR 0035: Layered content resolution — upstream defaults sparse-checked at runtime, overrides via `customized/` (per-org) or `.fullsend/` (per-repo)