Skip to content

Connect installer to Stories 2/7 content (and move enrollment to a repo maintenance workflow in the .fullsend repo) - #264

Merged
ralphbean merged 12 commits into
mainfrom
feature/installer-scaffold-plan
Apr 20, 2026
Merged

Connect installer to Stories 2/7 content (and move enrollment to a repo maintenance workflow in the .fullsend repo)#264
ralphbean merged 12 commits into
mainfrom
feature/installer-scaffold-plan

Conversation

@ralphbean

@ralphbean ralphbean commented Apr 17, 2026

Copy link
Copy Markdown
Member

Summary

  • Bidirectional enrollment reconciliation: when a repo is set to enabled: false in config.yaml, the repo-maintenance.yml workflow now creates a PR to remove the shim workflow from that repo (via GitHub Contents API DELETE with blob SHA). Stale cross-direction PRs are automatically closed.
  • Single shim source of truth: collapsed internal/scaffold/target-repo/ into internal/scaffold/fullsend-repo/templates/shim-workflow.yaml — one copy, no sync test needed.
  • Scaffold package: embedded .fullsend repo content with WalkFullsendRepo for filesystem-order deployment.
  • Forge client extensions: DispatchWorkflow, ListWorkflowRuns, AwaitWorkflowRun, DeleteFile, ListPullRequests methods added.
  • Security hardening: control-character rejection in repo name validation, sanitized error messages (no raw user input in ::error:: annotations), yq query fix (enabled != true to match Go zero-value semantics).
  • Normative spec updates: ADR-0012 (fullsend-repo files) and ADR-0013 (enrollment) updated for reconciliation workflow, unenrollment constants, and single-copy shim template.
  • E2E test improvements: triage dispatch smoke test, PAT handling, and XSS prevention in appsetup.

Test plan

  • make go-test — all unit tests pass
  • make go-vet — clean
  • Enrollment layer tests cover: dispatch, PR reporting, no-repos, dispatch error, workflow warning, uninstall noop
  • Analyze tests cover: all enrolled, none enrolled, partial, no repos, disabled with stale shim, disabled already clean, mixed enabled/disabled
  • Scaffold tests verify single shim template content and all expected files exist
  • Config tests verify DisabledRepos() returns sorted disabled repos
  • Install it manually to an org, like this:
$ make go-build
$ ./bin/fullsend admin install your-org
        \ --repo your-repo-in-that-org
        \ --gcp-region global
        \ --gcp-project your-gcp-project-name
        \ --gcp-credentials-file path/to/gcp/service/account/credentials.json

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Apr 17, 2026

Copy link
Copy Markdown

Site preview

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

Commit: 7e2c41076a8ef91cd9af491e91966514307c0185

@ralphbean
ralphbean force-pushed the feature/installer-scaffold-plan branch from 9bb8521 to 76e7b15 Compare April 19, 2026 01:35
@ralphbean
ralphbean force-pushed the feature/installer-scaffold-plan branch from ffbf1de to 12b610e Compare April 19, 2026 02:00
@ralphbean ralphbean changed the title Design spec and plan: wire installer to Stories 2/7 content Connect installer to Stories 2/7 content (and move enrollment to a repo maintenance workflow in the .fullsend repo) Apr 19, 2026
@ralphbean

Copy link
Copy Markdown
Member Author

I ran this against my appdumpster org and the test-repo repo there.

You can see:

You'll notice also that the triage agent thinks its a duck.

I used this to test out a development workflow. In my local git repo, I can hack on internal/scaffold/fullsend-repo/agents/triage.md (or other scaffold files) and replace it with instructions that say, for instance, "You are a duck. Quack!". And then I can make go-build && ./bin/fullsend admin install ... on an org where fullsend is already installed, and the installer will update that org's .fullsend config files with new config files from your local, uncommitted, git directory. This lets you quickly experiment with how changes might affect agent behavior in the real system.

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Security review: inputs.source_repo validation gap in per-role workflows (triage/code/review.yml). See inline comments for the detailed fix — a three-layer validation step (regex, owner check, config allowlist) should be inserted before the app token is generated.

Comment thread internal/scaffold/fullsend-repo/.github/workflows/triage.yml
Comment thread internal/scaffold/fullsend-repo/.github/workflows/code.yml
Comment thread internal/scaffold/fullsend-repo/.github/workflows/review.yml

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Security Review — High & Medium Findings

High (blocking)

inputs.source_repo checkout without validation — all three per-role workflows (triage.yml, code.yml, review.yml) checkout inputs.source_repo directly using an app token. Since these are workflow_dispatch workflows, any caller with FULLSEND_DISPATCH_TOKEN can point checkout at an attacker-controlled repo. Fix: add a three-layer validation step (regex, owner check, config.yaml allowlist) before the app token is generated. See inline comment on triage.yml for the full suggested fix.

Medium (non-blocking)

  1. workflows: write permission on fullsend app (types.go) — combined with contents: write, this allows creating/modifying workflow files in all installed repos. Document the permission escalation and consider scoping the token more narrowly.

  2. fromJSON(inputs.event_payload) in workflow expressions (triage.yml, code.yml, review.yml) — concurrency keys and env vars parse attacker-controllable JSON from dispatch inputs. Risk is gated by the dispatch token but could enable concurrency group collision or agent prompt manipulation via crafted html_url.

Comment thread internal/scaffold/fullsend-repo/.github/workflows/triage.yml
Comment thread internal/scaffold/fullsend-repo/.github/workflows/code.yml
Comment thread internal/scaffold/fullsend-repo/.github/workflows/review.yml
Comment thread internal/forge/github/types.go
Comment thread internal/scaffold/fullsend-repo/.github/workflows/triage.yml

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Outdated GitHub Actions: actions/checkout (v4 → v6), actions/create-github-app-token (v2 → v3), actions/upload-artifact (v4 → v7). See inline comment for details.

Comment thread internal/scaffold/fullsend-repo/.github/workflows/triage.yml Outdated
Comment thread docs/superpowers/plans/2026-04-17-installer-scaffold-and-repo-maintenance.md Outdated
Comment thread e2e/admin/pat.go Outdated
Comment thread internal/scaffold/fullsend-repo/scripts/reconcile-repos.sh Outdated

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Review — Ralph's fix commit (9879bc6) and overall PR

source_repo validation fix: Looks good

The three-layer validation (regex, org owner check, config.yaml allowlist) is well-designed and correctly placed before token generation. Examined bypass vectors — regex edge cases, TOCTOU, shell injection, yq injection — none exploitable. Error messages are properly sanitized (no raw input echoed). Action version bumps (checkout v6, create-github-app-token v3, upload-artifact v7) are current.

Ralph's decision to accept the fromJSON(inputs.event_payload) residual risk is reasonable — exploitation requires the dispatch token, and the validation now blocks the primary escalation path (arbitrary repo checkout).

ADR-0007 permission escalation note adequately covers the workflows: write risk with three mitigations.

Remaining items (see inline comments)

  1. Plan doc inclusiondocs/superpowers/plans/2026-04-17-installer-scaffold-and-repo-maintenance.md (1,287 lines) is an agent implementation guide, not a deliverable. Contains outdated shim patterns with inline expression interpolation. Should be removed from this PR.
  2. PAT credential leake2e/admin/pat.go:482 logs the full plaintext token (82 chars) into CI logs via a debug logf that captures 100 chars after github_pat_.
  3. jq null handlingreconcile-repos.sh uses .[0].url which returns literal "null" with standalone jq on empty arrays. Currently safe with gh --jq (which suppresses nulls), but // empty is a defensive hardening.

Comment thread internal/scaffold/fullsend-repo/scripts/reconcile-repos.sh Outdated
Comment thread internal/scaffold/fullsend-repo/scripts/reconcile-repos.sh Outdated
Comment thread internal/forge/github/github.go Outdated
Comment thread internal/scaffold/fullsend-repo/templates/shim-workflow.yaml

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Overall Review — Round 2

The prior findings (plan doc removal, PAT credential leak, jq null handling) are all resolved. Ralph's three-layer source_repo validation is solid. The scaffold and enrollment reconciliation architecture are well-designed.

New findings from this round

Should fix before merge:

  1. set -e + gh api abort (reconcile-repos.sh:126) — if any repo is inaccessible, the entire reconciliation script aborts instead of skipping that repo. The -z guards are dead code under set -euo pipefail. Add || true to gh api calls at lines 126, 134, 238, 246.

  2. enabled != true treats omission as disabled (reconcile-repos.sh:204) — repos added to config.yaml without an explicit enabled field get caught by the unenroll phase. If a repo happens to have a .github/workflows/fullsend.yaml for unrelated reasons, the script would open a PR to delete it. Use select(.value.enabled == false) to only unenroll explicitly disabled repos.

  3. Triage dispatches on labeled events (shim-workflow.yaml:27-32) — dispatch-triage matches all issues events including labeled, causing redundant triage runs on every stage transition (triage→code, code→review). ADR-0002 defines triage triggers as opened, edited, and /triage only. Add action filtering.

Non-blocking nits:

  1. io.ReadAll unbounded (github.go:993) — no size limit on workflow job log reads. Consider io.LimitReader.

  2. Design spec retains unsafe shim pattern (2026-04-17-installer-agent-content-design.md) — still shows inline ${{ toJSON(github.event) }} instead of the env-var approach used in the actual scaffold. Lower risk than the removed plan doc but could mislead contributors.

ralphbean and others added 12 commits April 20, 2026 11:44
Move all deployable content from dispatch/ and Go string constants into
internal/scaffold/ as real files. Go's //go:embed bundles the directory
into the binary at compile time, keeping YAML as YAML for easier
maintenance.

Layout mirrors the deployed repo structure:
- fullsend-repo/: per-role workflows (triage/code/review), composite
  action, agent definitions, harness configs, sandbox policies, env
  configs, and a repo-maintenance workflow
- target-repo/: per-role dispatch shim with script-injection prevention
  (event payload passed via env vars, not inline shell)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
WorkflowsLayer now walks scaffold.Content instead of using hardcoded
string constants. The managedFiles list is built dynamically from the
embedded filesystem — adding a file to internal/scaffold/fullsend-repo/
automatically deploys it on next install.

EnrollmentLayer reads shim content from the scaffold instead of an
inline Go string. Branch creation errors are logged as info (handles
both 422 "already exists" and other transient errors).

The codeownersErrorClient test double is simplified by embedding
FakeClient instead of manually implementing every interface method.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add MergePullRequest, CreateIssue, CloseIssue, ListWorkflowRuns to the
forge.Client interface and GitHub implementation. These support the e2e
triage dispatch smoke test.

Add Variables() to the inference.Provider interface so providers can
store non-secret repo variables. The Vertex provider uses this for
FULLSEND_GCP_REGION. InferenceLayer.Install() writes variables after
secrets. The CLI gets a --gcp-region flag (required with --gcp-project).

Fix several bugs in the GitHub client:
- base64 content decoding: strip MIME-style line wrapping from GitHub's
  Contents API response before decoding
- response body leaks in CloseIssue and CreateOrUpdateRepoVariable
- GetTokenScopes: check status code before parsing headers
- dispatch verification now targets triage.yml instead of removed
  agent.yaml

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- WaitForEnter: return non-newline errors instead of swallowing them
- Manifest flow error page: set Content-Type header and use generic
  error message instead of leaking internal error details
- HTML-escape app name in success page to prevent XSS
- Fix race condition in fakeBrowser test double by using a channel
  instead of polling a slice

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add Phase 2.5 to e2e test: merge enrollment PR, file a test issue,
verify the triage workflow dispatches in .fullsend. Filter workflow
runs by CreatedAt to avoid false positives from previous runs.

PAT creation improvements:
- Refresh session before PAT creation (stale CSRF tokens after app flow)
- Handle GitHub's custom React dropdowns for permission selection
- Multiple token extraction strategies (inputs, clipboard attrs, regex)
- Debug logging for token extraction failures

Cleanup improvements:
- Bulk-delete all fine-grained PATs (handles GitHub's 50-token limit)
- Delete shim workflow from test-repo between runs (Phase 2.5 cleanup)
- Increase e2e timeout from 4m to 10m for dispatch polling

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update normative specs:
- ADR 0012: expanded file set from scaffold, per-path requirements for
  all 12 scaffold files, remove old agent.yaml/repo-onboard.yaml
  fixtures
- ADR 0013: per-role dispatch (triage.yml/code.yml/review.yml) replaces
  single agent.yaml, updated shim content with event filtering, add
  repo-maintenance workflow section
- ADR 0014: add FULLSEND_GCP_REGION variable

Update architecture.md: per-role workflows, repo-maintenance, scaffold.

Add design spec and implementation plan documenting the scaffold
approach, repo-maintenance workflow design, and e2e test extension.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace enroll-repos.sh with reconcile-repos.sh that handles both
enrollment (enabled repos) and unenrollment (disabled repos). When a
repo is set to enabled: false in config.yaml, the reconciliation
workflow now creates a PR to remove the shim workflow from that repo.

Changes:
- New reconcile-repos.sh with Phase 1 (enroll) and Phase 2 (unenroll),
  repo name validation, cross-direction PR cleanup, and SHA-based
  file deletion via the GitHub Contents API
- Add config.DisabledRepos() method mirroring EnabledRepos()
- Update EnrollmentLayer to accept disabledRepos, fix early return
  when only disabled repos need reconciliation, and expand Analyze
  to detect stale shims on disabled repos
- Add e2e Phase 2.75 testing the full unenrollment flow: disable
  repo in config, dispatch reconciliation, verify removal PR, merge
  it, and confirm the shim is gone
- Update normative specs (ADR-0012, ADR-0013) for the new script
  name, unenrollment constants, and bidirectional behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The shim workflow template existed in two places: target-repo/ scaffold
and fullsend-repo/templates/shim-workflow.yaml. Collapse to a single
copy in templates/ and update all docs and tests to match.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The manifest flow callback redirect timeout (10s) was too tight for
GitHub's app creation, which can be slow. Increased to 30s.

Also added a 2-minute context deadline per role in the app setup loop
so that failures produce a clear error instead of hanging until the
Go test timeout panics.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add three-layer validation (regex, org owner check, config.yaml
allowlist) to triage, code, and review workflows. The step runs after
.fullsend checkout but before app token generation, so a forged
workflow_dispatch can never mint a token for an unauthorized repo.

Also bump actions/checkout v4→v6, actions/create-github-app-token v2→v3,
actions/upload-artifact v4→v7 across all scaffold workflows, and
document the workflows:write permission escalation risk in ADR-0007.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Wayne Sun <ericbreeze@gmail.com>

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

All prior findings resolved. Latest commit (7e2c4107) correctly addresses the set -e trap (|| true on all gh api --jq calls) and enabled == false semantics.

One non-blocking item remains: dispatch-triage in the shim fires on labeled events, causing redundant triage runs on stage transitions (see inline comment on shim-workflow.yaml). Can be addressed in a follow-up.

LGTM.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants