Skip to content

54. Require authorization on all agent dispatch paths

Date: 2026-05-29

Status

Accepted

Builds on ADR 0034 (centralized dispatch routing) and ADR 0042 (/fs- prefix convention).

Related: #877 (agents must not model their own authority limitations — this ADR implements the platform-level enforcement that principle requires).

Context

The dispatch routing logic (dispatch.yml / reusable-dispatch.yml) defines an is_authorized helper that checks whether the acting user has write-level permission on the repository. Today, only a subset of dispatch paths gate on this check:

TriggerGated?Notes
/fs-triageNoAny commenter triggers triage
/fs-codeNoAny commenter triggers code
/fs-reviewNoAny commenter triggers review
/fs-fixYesis_authorized + non-Bot check
/fs-retroYesis_authorized + non-Bot check
/fs-prioritizeYesis_authorized + non-Bot check
issues.openedNoAny issue opener triggers triage
pull_request_target.openedNoAny PR author triggers review

The ungated paths allow any GitHub user to trigger agent inference runs — either by commenting a slash command on a public issue/PR, or by opening an issue or PR directly. This creates two risks:

  1. Cost exposure. Each agent run consumes inference compute. An external user opening issues or posting /fs-code across a public org could generate significant cost with no rate limit.
  2. Abuse surface. The security threat model (security-threat-model.md) ranks external prompt injection as the highest-priority threat. An unauthorized user triggering agent runs is a prerequisite for many injection attacks — the attacker needs the agent to run before they can influence its behavior.

The inconsistency also violates the principle of least surprise: a contributor who sees /fs-fix rejected would reasonably expect /fs-code and auto-triage to behave the same way.

Decision

All agent dispatch paths require authorization before dispatching. The check applies universally — to slash commands and to automatic event triggers where the acting user may be external.

Both is_authorized (slash commands) and is_event_actor_authorized (event triggers) delegate to a shared has_write_permission helper that calls the collaborator permission API. This ensures consistent behavior across all paths.

Slash commands

The dispatch routing logic must call is_authorized for /fs-triage, /fs-code, and /fs-review with the same guard pattern already used by /fs-fix, /fs-retro, and /fs-prioritize:

bash
if [[ "${COMMENT_USER_TYPE}" != "Bot" ]] && is_authorized; then
  STAGE="<stage>"
fi

Authorization mechanism: collaborator permission API

Why not author_association? The author_association field in webhook payloads does not correctly reflect private org membership — an org admin with private membership gets CONTRIBUTOR instead of MEMBER (see github/gh-aw-mcpg#2862).

Instead, all authorization checks (both slash commands and event triggers) use the collaborator permission API (GET /repos/{owner}/{repo}/collaborators/{username}/permission) which returns the user's effective role including inherited org grants regardless of membership visibility.

The implementation uses a three-function layering:

  • has_write_permission(username) — calls the API, checks .role_name
  • is_authorized() — delegates to has_write_permission for the comment author
  • is_event_actor_authorized(username) — delegates for event actors

See the workflow files (reusable-dispatch.yml, scaffold dispatch.yml) for the canonical implementation.

Users with admin, maintain, or write role are authorized. Users with only triage or read role are denied. This maps to "users with push access to the repository."

Automatic event triggers

EventActor checkedGated?
issues.openedIssue openerYes
issues.editedEvent sender (editor)Yes
pull_request_target.opened / synchronizePR authorYes
issues.labeledLabel applierAlready implicit (requires write access)
pull_request_target.ready_for_reviewPR authorYes (same branch as opened/synchronize)
pull_request_target.closedCloserAlready implicit (requires write access)
pull_request_review.submittedReviewerAlready gated (requires review-bot authorship)
issue_comment (needs-info re-triage)CommenterWeaker gate: author_association != NONE or issue author (intentional — allows clarification from external reporters)

For external contributors (issues opened or PRs submitted by non-members), the agent does not fire automatically. A maintainer can still trigger the agent explicitly by:

  • Applying a label (ready-to-code, ready-for-review) — label application requires write access, which is an implicit auth gate.
  • Posting a slash command (/fs-triage, /fs-code, /fs-review).

This does not prevent external contributions — it prevents spending inference compute on them automatically.

Bot-to-bot workflows are preserved

Agent-to-agent handoffs use label-based triggers, not slash commands. When one agent completes a stage, its post-script applies a label (e.g., ready-for-triage, ready-to-code, ready-for-review) which triggers the next stage via the issues.labeled dispatch path. Label application requires write access — an implicit authorization gate — so no explicit is_authorized check is needed on that path.

The COMMENT_USER_TYPE != "Bot" check in the slash command guard means bot accounts cannot invoke slash commands at all (the condition short-circuits to false). This is intentional: bots have no need to use slash commands because they orchestrate via labels.

Visible feedback for unauthorized users

When a non-Bot user fails is_authorized, the dispatch script should provide visible feedback. The dispatch mechanism is open source and present in every enrolled repo's workflow files — silent failure provides no security benefit but does confuse legitimate contributors.

The dispatch script should provide some form of visible response (e.g., a reaction, a comment, or both) so the user knows their command was received but not executed. This is not yet implemented — commands currently fail silently. Tracked as future work.

For automatic triggers (e.g., unauthorized user opens an issue), no feedback is needed — the user didn't explicitly request an agent run.

Interaction with per-repo configurability

The is_authorized check is a platform-level security boundary, not a per-repo policy. Individual repos cannot disable it. Per-repo configurability (e.g., which stages are enabled, which labels trigger automation) operates within the authorization boundary — a repo can disable /fs-code entirely, but it cannot make /fs-code available to unauthorized users.

If a future per-repo configuration system needs to customize authorization rules (e.g., allowing triage or read permission), it should do so by extending the has_write_permission function's allowed permission list, not by bypassing the check.

Consequences

  • All dispatch paths require write-level repository permission, closing the cost-exposure and abuse-surface gaps for both slash commands and automatic triggers.
  • External users can no longer trigger agent runs by opening issues, PRs, or posting slash commands on public repos.
  • Maintainers retain full control: labels and slash commands let them trigger agents on external contributions when appropriate.
  • Bot-to-bot orchestration (e.g., triage → code handoff) is unaffected because it uses label-based triggers, which require write access and do not pass through the slash command authorization gate.
  • The dispatch routing logic becomes consistent: every dispatch path checks authorization of the acting user, reducing cognitive load.
  • Unauthorized slash command attempts currently fail silently (STAGE remains empty). Visible feedback (reaction + comment) is desirable future work to improve UX for legitimate contributors who don't yet have the required permission.
  • External contributors who don't want to become members will depend on maintainers to trigger agents on their behalf — an acceptable trade-off to keep the abuse surface minimal.
  • Future work: rate-limited auto-triage for external issue reporters (#1687, vouch, or per-org trust policies) could relax this boundary for drive-by bug reports without re-opening the abuse surface for slash commands.