-
Notifications
You must be signed in to change notification settings - Fork 103
feat(#5650): add /fs-stop to skip auto agent triggers #5920
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b8e2e81
32dadea
234cdc4
d0818e9
12651ef
2cfd42e
8c73c1c
005304a
f63613c
77c6ec2
2dc6384
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,182 @@ | ||
| #!/usr/bin/env bash | ||
| # stop-agent.sh — Apply fullsend-no-* labels for /fs-stop and /fs-fix-stop. | ||
| # Invoked by the shim stop-agent job. Requires env: | ||
| # GH_TOKEN, REPO, ISSUE_NUMBER, COMMENT_USER_LOGIN, ISSUE_USER_LOGIN, | ||
| # COMMENT_BODY, ISSUE_IS_PR ("true"|"false") | ||
| set -euo pipefail | ||
|
|
||
| post_comment() { | ||
| local body_file="$1" | ||
| if [[ "${ISSUE_IS_PR}" == "true" ]]; then | ||
| gh pr comment "${ISSUE_NUMBER}" --repo "${REPO}" --body-file "${body_file}" | ||
| else | ||
| gh issue comment "${ISSUE_NUMBER}" --repo "${REPO}" --body-file "${body_file}" | ||
| fi | ||
| } | ||
|
|
||
| make_body_file() { | ||
| local f | ||
| if ! f="$(mktemp)"; then | ||
| echo "::warning::Failed to create temp file for stop-agent comment body" | ||
| return 1 | ||
| fi | ||
| printf '%s' "$f" | ||
| } | ||
|
|
||
| # Trim leading blank lines and leading whitespace on the first non-blank line | ||
| # so copy/paste / markdown-quoted comments still work (matches awk tokenization | ||
| # used by other slash commands in dispatch). | ||
| FIRST="$(printf '%s\n' "${COMMENT_BODY}" | sed '/^[[:space:]]*$/d' | head -1 | sed 's/^[[:space:]]*//' | tr -d '\r')" | ||
| CMD="$(printf '%s\n' "${FIRST}" | awk '{print $1}')" | ||
| ARG="$(printf '%s\n' "${FIRST}" | awk '{print $2}')" | ||
| # Sanitize for workflow-command interpolation (defense in depth). | ||
| SAFE_CMD="${CMD//::/_}" | ||
| SAFE_USER="${COMMENT_USER_LOGIN//::/_}" | ||
|
|
||
| # Agents with auto-trigger paths gated by fullsend-no-* in dispatch. | ||
| # prioritize is slash-only and has no auto-trigger to suppress. | ||
| # Bare /fs-stop only applies labels meaningful for this item type. | ||
| VALID_ALL="triage code review fix retro" | ||
| VALID_ISSUE="triage code" | ||
| VALID_PR="review fix retro" | ||
| if [[ "${ISSUE_IS_PR}" == "true" ]]; then | ||
| VALID_BARE="${VALID_PR}" | ||
| else | ||
| VALID_BARE="${VALID_ISSUE}" | ||
| fi | ||
| VALID="${VALID_ALL}" | ||
| AGENTS=() | ||
| CROSS_CONTEXT=false | ||
| if [[ "${CMD}" == "/fs-fix-stop" ]]; then | ||
| AGENTS=(fix) | ||
| if [[ "${ISSUE_IS_PR}" != "true" ]]; then | ||
| CROSS_CONTEXT=true | ||
| fi | ||
| elif [[ "${CMD}" == "/fs-stop" ]]; then | ||
| if [[ -z "${ARG:-}" ]]; then | ||
| # shellcheck disable=SC2206 | ||
| AGENTS=(${VALID_BARE}) | ||
| elif [[ "${ARG}" =~ ^[a-z]+$ ]] && [[ " ${VALID} " == *" ${ARG} "* ]]; then | ||
| AGENTS=("${ARG}") | ||
| if [[ "${ISSUE_IS_PR}" == "true" ]]; then | ||
| if [[ " ${VALID_ISSUE} " == *" ${ARG} "* ]] && [[ " ${VALID_PR} " != *" ${ARG} "* ]]; then | ||
| CROSS_CONTEXT=true | ||
| fi | ||
| else | ||
| if [[ " ${VALID_PR} " == *" ${ARG} "* ]] && [[ " ${VALID_ISSUE} " != *" ${ARG} "* ]]; then | ||
| CROSS_CONTEXT=true | ||
| fi | ||
| fi | ||
| else | ||
| AGENTS=() | ||
| UNKNOWN_AGENT=true | ||
| fi | ||
| else | ||
| echo "::notice::Ignoring unrecognized stop command: ${SAFE_CMD}" | ||
| exit 0 | ||
| fi | ||
|
|
||
| # ADR 0054: authorize via the collaborator permission API | ||
| # (admin|maintain|write), not author_association — the latter grants | ||
| # contributor status to anyone with a single merged PR (issue #5421). | ||
| # Mirrors has_repo_permission() in dispatch.yml; keep the two in sync. | ||
| # | ||
| # Author escape hatch is intentionally limited to stopping *fix* only | ||
|
ascerra marked this conversation as resolved.
|
||
| # (historical /fs-fix-stop behavior). Stopping review/triage/code/retro — | ||
| # including bare /fs-stop — requires write-level permission so PR authors | ||
| # cannot unilaterally suppress security-relevant auto-gates. | ||
| authorized=false | ||
| is_author=false | ||
| if [[ -n "${COMMENT_USER_LOGIN}" && "${COMMENT_USER_LOGIN}" == "${ISSUE_USER_LOGIN}" ]]; then | ||
| is_author=true | ||
| fi | ||
| author_fix_only=false | ||
| if [[ "${is_author}" == "true" && "${UNKNOWN_AGENT:-}" != "true" && "${#AGENTS[@]}" -eq 1 && "${AGENTS[0]}" == "fix" ]]; then | ||
| author_fix_only=true | ||
| authorized=true | ||
| fi | ||
| if [[ "${authorized}" != "true" ]]; then | ||
| if api_err=$(mktemp); then | ||
| if role=$(gh api "repos/${REPO}/collaborators/${COMMENT_USER_LOGIN}/permission" \ | ||
| --jq '.role_name' 2>"${api_err}"); then | ||
| case "${role}" in | ||
| admin|maintain|write) authorized=true ;; | ||
| esac | ||
| else | ||
| api_err_safe="$(tr -d '\r' <"${api_err}" | tr '\n' ' ')" | ||
| api_err_safe="${api_err_safe//::/_}" | ||
| echo "::warning::Permission API call failed for ${SAFE_USER}: ${api_err_safe}" | ||
| fi | ||
| rm -f "${api_err}" | ||
| else | ||
| echo "::warning::Failed to create temp file for permission check of ${SAFE_USER}" | ||
| fi | ||
| fi | ||
| if [[ "${authorized}" != "true" ]]; then | ||
| if [[ "${is_author}" == "true" && "${author_fix_only}" != "true" ]]; then | ||
| echo "::notice::User ${SAFE_USER} is not authorized to stop these agents (PR/issue authors may only /fs-stop fix or /fs-fix-stop; write access required otherwise)" | ||
| else | ||
| echo "::notice::User ${SAFE_USER} is not authorized to stop agents (requires write access, or authorship for /fs-stop fix only)" | ||
| fi | ||
| exit 0 | ||
| fi | ||
|
|
||
| if [[ "${UNKNOWN_AGENT:-}" == "true" ]]; then | ||
| BODY_FILE="$(make_body_file)" || exit 0 | ||
| { | ||
| printf 'Unknown or unsupported agent.' | ||
| printf ' Valid auto-stop targets: %s.' "${VALID}" | ||
| printf ' Usage: `/fs-stop <agent>` or `/fs-stop` for all meaningful on this item.' | ||
| printf ' Note: prioritize is slash-only (`/fs-prioritize`); there is no auto-trigger to stop.' | ||
| } >"${BODY_FILE}" | ||
| post_comment "${BODY_FILE}" || true | ||
| rm -f "${BODY_FILE}" | ||
| exit 0 | ||
| fi | ||
|
|
||
| APPLIED=() | ||
| for agent in "${AGENTS[@]}"; do | ||
| label="fullsend-no-${agent}" | ||
| gh label create "${label}" --repo "${REPO}" \ | ||
| --description "Skip auto-triggered ${agent} agent runs" --color "FBCA04" \ | ||
| --force 2>/dev/null || true | ||
| if [[ "${ISSUE_IS_PR}" == "true" ]]; then | ||
| if gh pr edit "${ISSUE_NUMBER}" --repo "${REPO}" --add-label "${label}"; then | ||
| APPLIED+=("\`${label}\`") | ||
| else | ||
| echo "::warning::Failed to apply label ${label}" | ||
| fi | ||
| else | ||
| if gh issue edit "${ISSUE_NUMBER}" --repo "${REPO}" --add-label "${label}"; then | ||
| APPLIED+=("\`${label}\`") | ||
| else | ||
| echo "::warning::Failed to apply label ${label}" | ||
| fi | ||
| fi | ||
| done | ||
|
|
||
| BODY_FILE="$(make_body_file)" || exit 0 | ||
| if [[ "${#APPLIED[@]}" -eq 0 ]]; then | ||
| printf 'Agent stop requested for #%s, but no labels were applied (label API calls failed — see workflow run logs).\n' \ | ||
| "${ISSUE_NUMBER}" >"${BODY_FILE}" | ||
| else | ||
| LIST="$(printf '%s, ' "${APPLIED[@]}")" | ||
| LIST="${LIST%, }" | ||
| { | ||
| printf 'Agent stop applied for #%s: %s.\n' "${ISSUE_NUMBER}" "${LIST}" | ||
| printf 'Auto-triggers for these agents are skipped while the label(s) remain.\n' | ||
| printf 'On-demand `/fs-<agent>` commands still work.\n' | ||
| printf 'In-flight runs are not cancelled by this command — remove the label(s) or re-run `/fs-<agent>` to continue.\n' | ||
| if [[ "${CROSS_CONTEXT}" == "true" ]]; then | ||
| printf 'Note: this label has no effect on this item type' | ||
| if [[ "${ISSUE_IS_PR}" == "true" ]]; then | ||
| printf ' (auto-triggers for that agent run on issues, not PRs)' | ||
| else | ||
| printf ' (auto-triggers for that agent run on PRs, not issues)' | ||
| fi | ||
| printf ', and it does not carry over to a linked issue or PR.\n' | ||
| fi | ||
| } >"${BODY_FILE}" | ||
| fi | ||
| post_comment "${BODY_FILE}" || true | ||
| rm -f "${BODY_FILE}" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -48,6 +48,13 @@ jobs: | |
| || github.event.pull_request.head.ref != 'fullsend/scaffold-install') | ||
| && (github.event_name != 'issue_comment' | ||
| || github.event.comment.user.type != 'Bot') | ||
| && ( | ||
| github.event_name != 'issue_comment' | ||
| || ( | ||
| !contains(github.event.comment.body, '/fs-stop') | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [low] edge-case The dispatch job's if: condition uses !contains(github.event.comment.body, '/fs-stop') to skip dispatch. Substring matching means a multi-line comment with /fs-review on line 1 and /fs-stop on a later line would suppress dispatch for the entire comment. Intentionally coarse per YAML comments.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Acknowledged — no code change. The dispatch
ascerra marked this conversation as resolved.
|
||
| && !contains(github.event.comment.body, '/fs-fix-stop') | ||
| ) | ||
| ) | ||
| && ( | ||
| github.event.action != 'labeled' | ||
| || startsWith(github.event.label.name, 'ready-') | ||
|
|
@@ -61,63 +68,42 @@ jobs: | |
| with: | ||
| event_action: ${{ github.event.action }} | ||
|
|
||
| stop-fix: | ||
| # Job-level if: is intentionally coarse — it only screens for the | ||
| # /fs-fix-stop command on a PR from a non-bot. The authoritative | ||
| # authorization decision (collaborator permission API + PR-author escape | ||
| # hatch) is made in the step below, so a maintainer whose author_association | ||
| # is not MEMBER (e.g. private org membership) is not filtered out (ADR 0054). | ||
| stop-agent: | ||
| # Generalized /fs-stop <agent> (and /fs-fix-stop alias). Job-level if: is | ||
| # intentionally coarse — authorization uses the collaborator permission API | ||
| # + issue/PR-author escape hatch for /fs-stop fix only (ADR 0054 / #5421). | ||
| # Broad contains: exact token matching (incl. leading-whitespace trim) is in | ||
| # .github/scripts/stop-agent.sh so /fs-stopper is rejected there, not here. | ||
| if: >- | ||
|
ascerra marked this conversation as resolved.
|
||
| github.event_name == 'issue_comment' | ||
| && github.event.issue.pull_request | ||
| && github.event.comment.user.type != 'Bot' | ||
| && github.event.comment.body == '/fs-fix-stop' | ||
| && ( | ||
| contains(github.event.comment.body, '/fs-stop') | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [low] edge-case The stop-agent job-level if: uses contains() which matches any comment containing /fs-stop anywhere. Intentionally coarse with exact matching deferred to the script. The job runs but takes no action on false positives.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Acknowledged — no code change. Job-level |
||
| || contains(github.event.comment.body, '/fs-fix-stop') | ||
| ) | ||
| runs-on: ubuntu-24.04 | ||
| permissions: | ||
| contents: read | ||
| issues: write | ||
| pull-requests: write | ||
| steps: | ||
| - name: Add fullsend-no-fix label and notify | ||
| - name: Checkout stop-agent script | ||
|
ascerra marked this conversation as resolved.
|
||
| # Pin default branch explicitly (ADR 0054): issue_comment must not take | ||
| # stop-agent logic from an attacker-controlled ref. | ||
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
|
ascerra marked this conversation as resolved.
|
||
| with: | ||
| ref: ${{ github.event.repository.default_branch }} | ||
| persist-credentials: false | ||
| sparse-checkout: | | ||
| .github/scripts/stop-agent.sh | ||
| sparse-checkout-cone-mode: false | ||
| - name: Apply fullsend-no-* labels and notify | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| PR_NUMBER: ${{ github.event.issue.number }} | ||
| ISSUE_NUMBER: ${{ github.event.issue.number }} | ||
| REPO: ${{ github.repository }} | ||
| COMMENT_USER_LOGIN: ${{ github.event.comment.user.login }} | ||
| ISSUE_USER_LOGIN: ${{ github.event.issue.user.login }} | ||
| run: | | ||
| set -euo pipefail | ||
| # ADR 0054: authorize via the collaborator permission API | ||
| # (admin|maintain|write), not author_association — the latter grants | ||
| # contributor status to anyone with a single merged PR (issue #5421). | ||
| # Mirrors has_repo_permission() in dispatch.yml; keep the two in sync. | ||
| # The PR author may always stop the fix agent on their own PR. | ||
| authorized=false | ||
| if [[ -n "$COMMENT_USER_LOGIN" && "$COMMENT_USER_LOGIN" == "$ISSUE_USER_LOGIN" ]]; then | ||
| authorized=true | ||
| else | ||
| if api_err=$(mktemp); then | ||
| if role=$(gh api "repos/$REPO/collaborators/$COMMENT_USER_LOGIN/permission" \ | ||
| --jq '.role_name' 2>"$api_err"); then | ||
| case "$role" in | ||
| admin|maintain|write) authorized=true ;; | ||
| esac | ||
| else | ||
| echo "::warning::Permission API call failed for $COMMENT_USER_LOGIN: $(cat "$api_err")" | ||
| fi | ||
| rm -f "$api_err" | ||
| else | ||
| echo "::warning::Failed to create temp file for permission check of $COMMENT_USER_LOGIN" | ||
| fi | ||
| fi | ||
| if [[ "$authorized" != "true" ]]; then | ||
| echo "::notice::User $COMMENT_USER_LOGIN is not authorized to stop the fix agent (requires write access or PR authorship)" | ||
| exit 0 | ||
| fi | ||
| gh label create "fullsend-no-fix" --repo "$REPO" \ | ||
| --description "Skip bot-triggered fix agent runs" --color "FBCA04" \ | ||
| --force 2>/dev/null || true | ||
| gh pr edit "$PR_NUMBER" --repo "$REPO" \ | ||
| --add-label "fullsend-no-fix" | ||
| gh pr comment "$PR_NUMBER" --repo "$REPO" \ | ||
| --body "Fix agent disabled for this PR. Remove the \`fullsend-no-fix\` label or use \`/fs-fix\` to re-engage." | ||
| COMMENT_BODY: ${{ github.event.comment.body }} | ||
| ISSUE_IS_PR: ${{ github.event.issue.pull_request && 'true' || 'false' }} | ||
| run: bash .github/scripts/stop-agent.sh | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[low] user-experience-complexity
Cross-context label application: script allows /fs-stop fix on issues (where fix doesn't auto-trigger) and warns 'has no effect on this item type.' Label pollution is possible but the warning is clearly surfaced.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Acknowledged — no code change. Cross-context labels are allowed with an explicit user-facing note that the label has no effect on this item type and does not carry over to a linked issue/PR.