Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
272dd6c
docs: add implementation plan for admin CLI
ralphbean Apr 2, 2026
662fc56
feat: initialize Go module and build infrastructure
ralphbean Apr 2, 2026
ca367f1
feat: add terminal UI package with styled output
ralphbean Apr 2, 2026
a61f3a9
feat: add forge interface and thread-safe fake client
ralphbean Apr 2, 2026
c5a1a5e
feat: add GitHub REST API client implementing forge.Client
ralphbean Apr 2, 2026
d5c9ae5
feat: add config package for org-level configuration
ralphbean Apr 2, 2026
2f77d42
feat: add layer model for ordered install/uninstall/analyze
ralphbean Apr 2, 2026
ab73c65
feat: add config repo layer for .fullsend repo management
ralphbean Apr 2, 2026
1a4ef07
feat: add workflows layer for CI workflow file management
ralphbean Apr 2, 2026
0152dc3
feat: add secrets layer for agent credential management
ralphbean Apr 2, 2026
b738fcf
feat: add enrollment layer for repo onboarding
ralphbean Apr 2, 2026
41c6a3a
feat: add GitHub App manifest flow for agent app setup
ralphbean Apr 2, 2026
616a41a
feat: add CLI with admin install/uninstall/analyze subcommands
ralphbean Apr 2, 2026
c19ff32
fix: address code review findings from PR #142
ralphbean Apr 2, 2026
e8dcad5
docs: add institutional knowledge comments from PR #132 lessons
ralphbean Apr 2, 2026
daf1afa
feat: add preflight scope checks and auto-reuse existing apps
ralphbean Apr 2, 2026
e637527
feat: open browser to app deletion pages during uninstall
ralphbean Apr 2, 2026
e129504
fix: make all layer operations idempotent
ralphbean Apr 2, 2026
daeac43
fix: fall back to default app names when config repo is gone
ralphbean Apr 2, 2026
2465354
fix: include redirect_url and hook_attributes in app manifest JSON
ralphbean Apr 2, 2026
ca61bf0
feat: change app naming convention to <org>-<role>
ralphbean Apr 2, 2026
796094c
fix: retry file writes on 404/409 after repo creation
ralphbean Apr 2, 2026
0a8cba1
fix: handle existing shim workflow on enrollment branch
ralphbean Apr 2, 2026
7744552
feat: add org-level secret methods and repo ID to forge interface
ralphbean Apr 2, 2026
d3d3631
feat: add dispatch token layer for org-level cross-repo dispatch
ralphbean Apr 2, 2026
93b35d7
feat: switch from workflow_call to workflow_dispatch for security
ralphbean Apr 2, 2026
2eaf0b1
feat: wire dispatch token layer into admin install/uninstall/analyze
ralphbean Apr 2, 2026
6335088
docs: add ADRs for forge abstraction, layer model, app model, dispatc…
ralphbean Apr 2, 2026
5547429
fix: run preflight before dispatch token prompt, handle 403 gracefully
ralphbean Apr 2, 2026
5d38ed0
feat: open browser with pre-filled PAT creation for dispatch token
ralphbean Apr 2, 2026
fcb853a
feat: update existing enrollment PRs on re-install
ralphbean Apr 2, 2026
b1d23b7
refactor: replace curl with gh workflow run in shim workflow
ralphbean Apr 2, 2026
61d45bf
fix: verify dispatch token can access .fullsend before storing
ralphbean Apr 2, 2026
64a89fe
feat: auto-detect app installation instead of waiting for Enter
ralphbean Apr 2, 2026
856da28
fix: only open app deletion pages for apps that actually exist
ralphbean Apr 2, 2026
2a5cdd7
fix: create .fullsend repo before dispatch token prompt
ralphbean Apr 2, 2026
0d8ebff
fix: verify dispatch token has Actions access, not just metadata
ralphbean Apr 2, 2026
e2cbe1e
fix: sanitize dispatch token input to strip stray whitespace
ralphbean Apr 2, 2026
9739bb7
fix: verify dispatch token by triggering a real workflow dispatch
ralphbean Apr 2, 2026
9452510
fix: accept 204 No Content from workflow dispatch endpoint
ralphbean Apr 2, 2026
056eb43
Drop old superpowers plan
ralphbean Apr 2, 2026
dfc9e83
Setup go in ci lint
ralphbean Apr 2, 2026
5a73f66
feat: add release workflow with GoReleaser
ralphbean Apr 2, 2026
563a376
Merge pull request #151 from fullsend-ai/release-workflow
ralphbean Apr 2, 2026
ff74452
Revert "Merge pull request #142 from fullsend-ai/agent-admin-cli-clea…
ralphbean Apr 3, 2026
ea3d8cd
Merge branch 'agent-admin-cli-clean-room-v6' (PR #160) - correct admi…
ralphbean Apr 3, 2026
3c14381
feat(e2e): admin e2e tests adapted for v6 admin CLI with dispatch tok…
ralphbean Apr 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ __pycache__/
.ruff_cache/
_site/
bin/
.playwright/
14 changes: 12 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.DEFAULT_GOAL := help
.PHONY: help bootstrap lint check fmt lint-adr-status lint-adr-numbers lint-adr-frontmatter mindmap \
go-build go-test go-lint go-fmt go-vet go-tidy
go-build go-test go-lint go-fmt go-vet go-tidy e2e-test e2e-playwright

help:
@echo "Available targets:"
Expand All @@ -19,6 +19,7 @@ help:
@echo " go-fmt - Format Go code"
@echo " go-vet - Run go vet"
@echo " go-tidy - Run go mod tidy"
@echo " e2e-test - Run admin e2e tests (requires E2E_GITHUB_USERNAME and E2E_GITHUB_PASSWORD or E2E_GITHUB_PASSWORD_FILE)"

# Install all development tools needed for linting, formatting, and pre-commit hooks.
# Prerequisites: uv (https://docs.astral.sh/uv/) and go (https://go.dev/)
Expand All @@ -28,7 +29,7 @@ help:
BOOTSTRAP_TOOL_DIR := $(HOME)/.local/share/uv-tools
BOOTSTRAP_BIN_DIR := $(HOME)/.local/bin

bootstrap:
bootstrap: e2e-playwright
@mkdir -p "$(BOOTSTRAP_BIN_DIR)"
@echo "==> Installing Python 3.12 (via uv)..."
uv python install 3.12
Expand Down Expand Up @@ -91,3 +92,12 @@ go-vet:

go-tidy:
go mod tidy

e2e-test: e2e-playwright
go test -tags e2e -v -count=1 -timeout 4m ./e2e/admin/

e2e-playwright:
@if [ -z "$$(ls -d $(HOME)/.cache/ms-playwright/chromium-* 2>/dev/null)" ]; then \
echo "==> Installing Playwright Chromium..."; \
go run github.com/playwright-community/playwright-go/cmd/playwright install chromium; \
fi
36 changes: 36 additions & 0 deletions docs/ADRs/0004-forge-abstraction-layer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
title: "4. Forge abstraction layer"
status: Accepted
relates_to:
- agent-infrastructure
- agent-architecture
topics:
- forge
- portability
- interfaces
---

# 4. Forge abstraction layer

Date: 2026-04-02

## Status

Accepted

## Context

Fullsend must eventually support GitHub, GitLab, and Forgejo. Every operation that touches the git forge — creating repos, managing secrets, writing files, listing installations — must work across all three. Without a shared abstraction, forge-specific logic would spread throughout the codebase, making multi-forge support a rewrite rather than an extension.

## Decision

All forge operations go through the `forge.Client` interface (`internal/forge/forge.go`). The interface uses forge-neutral vocabulary: `ChangeProposal` instead of "pull request" or "merge request," `CreateChangeProposal` instead of `CreatePR`. Forge-specific implementations live in sub-packages (`internal/forge/github/`). A thread-safe `FakeClient` exists for testing without forge access.

No code outside `internal/forge/` imports forge-specific packages directly.

## Consequences

- Adding a new forge (GitLab, Forgejo) requires implementing `forge.Client` — no changes to layers, CLI, or app setup code.
- Forge-neutral naming occasionally feels awkward (e.g., `ChangeProposal`), but prevents GitHub-centric thinking from leaking into the model.
- The interface will grow as new operations are needed; keeping it cohesive requires discipline.
- The `FakeClient` enables deterministic testing of every layer without network calls.
38 changes: 38 additions & 0 deletions docs/ADRs/0005-ordered-layer-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
title: "5. Ordered layer model for install, uninstall, and analyze"
status: Accepted
relates_to:
- agent-infrastructure
topics:
- installation
- layers
- idempotency
---

# 5. Ordered layer model for install, uninstall, and analyze

Date: 2026-04-02

## Status

Accepted

## Context

Installing fullsend into an org involves multiple concerns with ordering dependencies: the config repo must exist before workflows can be written to it; secrets must be stored before enrollment can reference them. Uninstalling must reverse this order. An analyze command must inspect each concern independently to report what exists, what is missing, and what install would do.

## Decision

Each installation concern is a `Layer` implementing `Install`, `Uninstall`, and `Analyze`. Layers are composed into an ordered `Stack`. Install runs layers forward; uninstall runs them in reverse; analyze runs them forward and collects reports.

The current stack order is: config-repo → workflows → secrets → dispatch-token → enrollment.

Each layer is idempotent — re-running install skips already-completed work. Uninstall collects all errors rather than stopping on the first, so partial teardown still makes progress. Each layer declares the OAuth scopes it needs via `RequiredScopes`, enabling a preflight check that fails early when the token lacks required permissions.

## Consequences

- Adding a new installation concern means implementing the `Layer` interface and inserting it at the right position in the stack.
- The analyze command can report partial installations and explain exactly what install would create or fix.
- Idempotency means install is also the repair command — no separate "fix" operation needed.
- The ordering contract is implicit (stack construction order). A future layer that violates ordering assumptions will fail at runtime, not compile time.
- Reverse-order uninstall with error collection ensures best-effort cleanup even when some layers fail.
39 changes: 39 additions & 0 deletions docs/ADRs/0006-per-role-github-apps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
title: "6. Per-role GitHub Apps with manifest-based creation"
status: Accepted
relates_to:
- agent-architecture
- security-threat-model
topics:
- identity
- github-apps
- least-privilege
---

# 6. Per-role GitHub Apps with manifest-based creation

Date: 2026-04-02

## Status

Accepted

## Context

Agents need forge credentials to act on repos. A single shared credential for all agent roles violates least-privilege: a review agent would hold write permissions it should never use. The identity model must scope permissions per role while keeping setup automatable. See [agent-architecture.md](../problems/agent-architecture.md) and [security-threat-model.md](../problems/security-threat-model.md).

## Decision

Each agent role (triage, implementation, review) gets its own GitHub App, created via the [app manifest flow](https://docs.github.com/en/apps/sharing-github-apps/registering-a-github-app-from-a-manifest). Apps follow the naming convention `<org>-<role>`. The manifest defines per-role permissions (e.g., review gets read-only code access; implementation gets read-write).

Private keys (PEMs) are stored as repo-level secrets on the `.fullsend` config repo. App IDs are stored as repo-level variables. Secrets never leave the config repo — agent dispatch workflows in `.fullsend` read them at runtime.

The installer checks for existing app installations before creating new ones. If an app exists and its PEM secret is present, it is reused. If the PEM is lost (it is only available at creation time), the user must delete the app and re-run install.

## Consequences

- Each role gets exactly the permissions it needs — compromising one app does not grant another role's permissions.
- The manifest flow requires a browser-based OAuth redirect, making fully headless installation impossible. Acceptable for an org-admin operation run infrequently.
- PEMs are write-once secrets: lost keys require app deletion and recreation.
- The per-app model scales linearly with roles. Adding a new role means creating a new app — no shared credential rotation needed.
- GitLab and Forgejo will need equivalent per-role identity mechanisms when their forge implementations are built.
39 changes: 39 additions & 0 deletions docs/ADRs/0007-workflow-dispatch-for-cross-repo-dispatch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
title: "7. workflow_dispatch for cross-repo agent dispatch"
status: Accepted
relates_to:
- agent-infrastructure
- security-threat-model
topics:
- dispatch
- secrets
- workflows
---

# 7. workflow_dispatch for cross-repo agent dispatch

Date: 2026-04-02

## Status

Accepted

## Context

Enrolled repos must route events (issues, PRs, comments) to the agent dispatch workflow in the `.fullsend` config repo. The original design used `workflow_call` (reusable workflows), which requires the calling workflow to pass secrets explicitly — every enrolled repo's shim workflow would contain secret references, and the called workflow's secrets are scoped to the *caller's* repo, not the config repo where the App PEMs live.

See [security-threat-model.md](../problems/security-threat-model.md) and [agent-infrastructure.md](../problems/agent-infrastructure.md).

## Decision

Use `workflow_dispatch` instead of `workflow_call`. Enrolled repos trigger a dispatch event on `.fullsend` via a curl call authenticated with `FULLSEND_DISPATCH_TOKEN` — a fine-grained PAT scoped to `.fullsend` with `actions:write`. The dispatch token is stored as an org-level Actions secret with visibility restricted to enrolled repos only.

This means secrets are separated by layer: the dispatch token (org secret, visible to enrolled repos) enables triggering; the App PEMs (repo secrets on `.fullsend`) are only accessible to workflows running *in* `.fullsend`. Enrolled repos never see the PEMs.

## Consequences

- App PEM secrets stay in the config repo. No secret passing across repo boundaries.
- The dispatch token is a single PAT with narrow scope — the blast radius of its compromise is limited to triggering workflow_dispatch events on `.fullsend`, not credential theft.
- `workflow_dispatch` is compute-platform-agnostic: any CI system that can receive dispatch events works.
- The dispatch token must be manually created (fine-grained PATs cannot be created via API). This is a one-time step during install.
- Adding or removing enrolled repos requires updating the org secret's repo access list.
45 changes: 45 additions & 0 deletions docs/ADRs/0008-pull-request-target-in-shim-workflows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
title: "8. Use pull_request_target in shim workflows"
status: Accepted
relates_to:
- security-threat-model
topics:
- workflows
- security
- pull-request-target
---

# 8. Use pull_request_target in shim workflows

Date: 2026-04-02

## Status

Accepted

## Context

The shim workflow in enrolled repos (`.github/workflows/fullsend.yaml`) references `FULLSEND_DISPATCH_TOKEN` to trigger agent dispatch. Using `pull_request` as the trigger means a malicious PR could modify the workflow file to exfiltrate this token — `pull_request` runs the *PR branch* version of the workflow. Using `pull_request_target` runs the *base branch* version, so PR authors cannot alter the workflow that executes.

## Decision

Use `pull_request_target` for PR-related events in the shim workflow. The shim never checks out PR code — it is a static curl call that forwards event metadata to the dispatch workflow in `.fullsend`.

**Why this is safe despite `pull_request_target`'s reputation:** The "pwn request" vulnerability class requires `pull_request_target` combined with checkout of untrusted code and execution of that code. Our shim does none of that — it reads only `github.event_name`, `github.repository`, and `toJSON(github.event)` from the event context, then curls the dispatch endpoint. No checkout, no build, no script execution from the PR.

**Residual risk:** A compromised dispatch token could trigger `workflow_dispatch` events on `.fullsend`. This is a DoS vector (burn Actions minutes) but not credential theft — the dispatch workflow reads its own repo secrets, and the caller cannot influence which secrets are accessed. This risk is acceptable.

CODEOWNERS on the shim workflow path provides defense-in-depth: even if an attacker could somehow modify the base branch workflow, the change requires human approval.

**Alternatives considered:**

1. **`pull_request`** — exposes the dispatch token to PR-authored workflow modifications. Rejected.
2. **No token / webhook-based dispatch** — requires a hosted webhook receiver, breaking compute-platform agnosticism. Rejected.
3. **Org-level `pull_request_target` prohibition** — some orgs disable `pull_request_target` via repository rulesets. Document as a known configuration requirement for adopters.

## Consequences

- PR authors cannot modify the shim workflow to exfiltrate the dispatch token.
- The shim must never be extended to checkout PR code — this invariant must be maintained as the shim evolves.
- Orgs with blanket `pull_request_target` prohibitions must allowlist the shim workflow.
- Security auditors reviewing the repo will flag `pull_request_target` — the shim's inline comments explain why it is safe.
15 changes: 13 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ This is the "where do agents physically run" question — whether that's a manag

Infrastructure platform choice and configuration are specified in the adopting organization's **`.fullsend`** repository. (See [ADR 0003](ADRs/0003-org-config-repo-convention.md).)

**Decided:**

- Forge abstraction: all forge operations go through the `forge.Client` interface, keeping the rest of the codebase forge-agnostic ([ADR 0004](ADRs/0004-forge-abstraction-layer.md)).
- Installation model: ordered layer stack (install forward, uninstall reverse, analyze for status reporting) with idempotent operations ([ADR 0005](ADRs/0005-ordered-layer-model.md)).
- Cross-repo dispatch: `workflow_dispatch` with an org-level dispatch token replaces `workflow_call`, keeping App PEM secrets in the config repo ([ADR 0007](ADRs/0007-workflow-dispatch-for-cross-repo-dispatch.md)).
- Shim workflow security: `pull_request_target` prevents PR authors from modifying the shim to exfiltrate the dispatch token ([ADR 0008](ADRs/0008-pull-request-target-in-shim-workflows.md)).

**Open questions:**

- Do we adopt a 3rd party platform, use existing internal infrastructure, or build our own? (See [agent-infrastructure.md](problems/agent-infrastructure.md) for the three directions.)
Expand Down Expand Up @@ -68,12 +75,16 @@ The system that gives agents credentials to act on external services. Responsibl

Identity is not the same as trust. An agent's identity lets it authenticate to external services; the trust model is defined by repository permissions and CODEOWNERS, not by which credentials the agent holds. (See [agent-architecture.md](problems/agent-architecture.md) — "trust derives from repository permissions, not agent identity.")

**Decided:**

- Per-role GitHub Apps with manifest-based creation. Each agent role gets its own app with scoped permissions. PEMs stored as repo secrets on `.fullsend` ([ADR 0006](ADRs/0006-per-role-github-apps.md)).

**Open questions:**

- What identity model fits best — separate bot accounts per agent role, a single bot account with role metadata, GitHub App installations, or something else? (See [agent-architecture.md](problems/agent-architecture.md).)
- How are credentials scoped so that agents only get the permissions they need?
- ~~What identity model fits best — separate bot accounts per agent role, a single bot account with role metadata, GitHub App installations, or something else?~~ Decided in [ADR 0006](ADRs/0006-per-role-github-apps.md).
- How are credentials rotated and revoked, and who has authority to do that?
- Does the identity provider integrate with existing secrets management, or is it a new system?
- How will per-role identity work on GitLab and Forgejo, which lack GitHub's app manifest flow?

## Work Coordinator

Expand Down
2 changes: 1 addition & 1 deletion docs/problems/agent-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ Without a coordinator, what happens when agents disagree? (e.g., correctness age

- Should agents be stateless (fresh context per task) or stateful (accumulated knowledge of the codebase)? Stateless is safer (no poisoned state persists) but less efficient.
- Should there be one instance of each agent type per repo, per org, or shared? Per-repo is simpler but more expensive. Shared agents need careful isolation. (Infrastructure constrains this — see [agent-infrastructure.md](agent-infrastructure.md).)
- What's the right model for agent identity? Agents need GitHub accounts to post comments and status checks. Separate bot accounts per agent role? A single bot account with role indicated in the comment? GitHub App installations?
- ~~What's the right model for agent identity? Agents need GitHub accounts to post comments and status checks. Separate bot accounts per agent role? A single bot account with role indicated in the comment? GitHub App installations?~~ Decided in [ADR 0006](../ADRs/0006-per-role-github-apps.md): per-role GitHub Apps with manifest-based creation.
- How do we test the interaction model? Can we simulate adversarial scenarios (injection attempts, unauthorized changes, agent disagreements) in a sandbox repo?
- How does the two-phase review model work in practice? Does the implementation agent run all six sub-agents locally, or a subset? Is the pre-PR review a lighter version? (Depends on [agent-infrastructure.md](agent-infrastructure.md) — what compute is available where.)
- What's the iteration limit before human escalation? Too low and humans get pulled in constantly. Too high and the system wastes resources on unresolvable conflicts.
Expand Down
Loading
Loading