Skip to content

pr-unblock: derive fix-ci-subscription's start and the restart cap from trusted values #8890

Open

Description

@kyle-sexton

No related issue: follow-up hardening from the Phase 5b review (fix-ci on run-claude-cli).

Problem

Two pr-unblock decisions about fix-ci still read outputs of the read runner's model job (pr-run-activity-read.yml, job run). The 5b review treats those outputs as untrusted, because that job runs PR head code:

  • fix-ci-subscription starts when needs.fix-ci.outputs.lane-class == 'credit-stop' || needs.fix-ci.outputs.credential-present == 'false' (.github/workflows/pr-unblock.yml:395-397).
  • The read runner's report job skips its check-run post when the run reports handed-off == 'true'.

A run that misreports these can end with no pr-unblock / fix-ci check posted and no subscription call. fix-ci-dispatch then dispatches again, and the restart cap never counts it. The worst case is repeated lane runs on one PR, which spend prepaid API credits (auto-refill is off, so it costs credits, not a charge).

Fix

  • Derive the subscription start from values the model job cannot write. Two options: the job's own step outcome read through the jobs API, or the fix-ci-context job's result.
  • Make fix-ci-dispatch count a dispatched run that posted no check toward fix-ci-restarts, so the loop is capped whatever the run reports.

Acceptance

  • fix-ci-subscription's start condition reads no needs.fix-ci.outputs.* value. A test pins this.
  • A dispatched fix-ci run that posts no check counts toward the restart cap. A test covers it.

Priority: high within the lane-funding follow-ups.

🤖 Generated with Claude Code

Activity

  1. added
    needs-triageNot yet classified. Floor until a type and one priority tier are set.
    on Oct 21, 2026
  2. github-actions commented on Oct 21, 2026

    @github-actions
    Contributor

    Proposed work class: work-class: scoped
    Basis: automated intake triage; the lanes App applied this class.

    Size: Medium
    Complexity: Medium
    Uncertainty: Known
    Flags: none
    Blocked by: none

    Understood: harden pr-unblock so the fix-ci and fix-docs subscription start conditions read no model-job outputs (use trusted values such as the jobs API step outcome or the context job's result). Also make the dispatchers count a dispatched run that posted no check, so the restart cap and the "handled" marking hold whatever the run reports. That covers the L1 re-fire, L2 cap starvation and L3 start-condition findings from the #8964 review. The acceptance tests are specified. Nothing is missing. The approach is a choice between the two named options, which the implementer can settle. Treated as ordinary hardening work (the findings are rated LOW and the spend is prepaid credit), so it is not held for a person. No likely duplicates found among the open issues.

    Automated triage, applied by the lanes App.

  3. added
    needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.
    and removed
    needs-triageNot yet classified. Floor until a type and one priority tier are set.
    on Oct 21, 2026
  4. kyle-sexton commented on Oct 22, 2026

    @kyle-sexton
    MemberAuthor

    Scope widened after the #8964 security pass (fix-docs moved into pr-unblock):

    • fix-docs-subscription repeats the same start condition: it reads needs.fix-docs.outputs.lane-class and credential-present (.github/workflows/pr-unblock.yml:516-517, fallback input at :532, as of feat(pr-pipeline): run fix-docs on run-claude-cli with an apply-patch push job #8964 head e89dc01). Its start must come from trusted values too.
    • fix-docs-dispatch.mjs marks a PR handled only when it finds a pr-unblock / fix-docs check run (:145-147, :187-188). A successful dispatch marks nothing (:199-212), and the in-flight guard skips only runs that are not completed (:177-181). A fix-docs run that ends without posting its check is therefore dispatched again on every push to main, with no per-PR cap. Fix: treat a completed pr-unblock fix-docs #<n> run (runTitle) as handled.

    Acceptance additions:

    • fix-docs-subscription's start condition reads no needs.fix-docs.outputs.* value. A test pins this.
    • A PR whose fix-docs run completed without posting a check is not dispatched again. A test covers it.

    Lands right after #8964 and before Phase 7.2, so the address-feedback and review-dependency subscription jobs don't copy the pattern.

  5. kyle-sexton commented on Oct 22, 2026

    @kyle-sexton
    MemberAuthor

    Security review findings on #8964 (all LOW, all assigned here):

    • L1, re-fire: when the read runner's run job leaves its head output empty, or reports handed-off=true (pr-run-activity-read.yml:2424, :2534-2543), the report job posts no check. The PR is then dispatched again on every push to main, about once per PR per push and at most 2 per push. At last week's main push rate that is up to about 356 runs a day, each spending prepaid API credits. Precondition: a steered model on a listed author's PR. Fix: record the dispatched head at dispatch time (fix-docs-dispatch.mjs:212), or pass a base-derived head so the report posts a neutral not-assessed check.
    • L2, cap starvation: a same-repository PR by an author not on the trusted list stops at the trigger gate and posts no check. It takes a dispatch slot on every push (lowest number first, :160, :175), so two such PRs block fix-docs for every other PR. No model spend. The same fix covers it.
    • L3, subscription start: fix-docs-subscription starts on run-job outputs a forged run can set (pr-unblock.yml:516-517, :532), the same pattern as fix-ci. It widens no credential reach and triggers no metered charge (usage credits stay off).
  6. removed
    needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.
    on Oct 25, 2026
  7. kyle-sexton commented on Oct 25, 2026

    @kyle-sexton
    MemberAuthor

    Released for re-triage under the owner's 2026-10-24 rule: an issue stops for a person only for an intent or vision question, a security call, a host or attended run, or spend outside prepaid credit. The hold label came off and intake triage runs again now; it re-applies a hold, with a numbered question, when one of those applies.

    To hold this issue again, add needs-human (or status: needs-decision) and say why in a comment.

  8. added
    needs-triageNot yet classified. Floor until a type and one priority tier are set.
    area: securitySecurity-relevant: vulnerability, hardening, or disclosure follow-up.
    priority: highSignificant impact, or blocks an imminent release; staff this cycle.
    on Oct 25, 2026
  9. added
    status: readyTriaged, unblocked, and fully specified; eligible to pick up.
    work-class: scopedA briefed fix or small feature; blast radius bounded by the brief, tests exist.
    status: needs-decisionAwaiting a human or maintainer judgment call.
    and removed
    needs-triageNot yet classified. Floor until a type and one priority tier are set.
    on Oct 25, 2026
  10. melodic-automation-lanes commented on Oct 25, 2026

    @melodic-automation-lanes
    Contributor

    Not admitted to the issue lane: no trusted-human acceptance criteria: a listed human must put the confirmed - [ ] criteria in the body or post them as their own comment, or, on an issue a listed human filed, intake's newest refinement comment must hold them unedited. Admission takes the issue once each of those is met, when intake triages it again (.github/actions/resolve-config/admission-rules.json).

  11. melodic-automation-lanes commented on Oct 25, 2026

    @melodic-automation-lanes
    Contributor

    The latest triage verdict holds nothing: admission moved the issue to status: needs-decision; its comment says why.

  12. melodic-automation-lanes commented on Oct 25, 2026

    @melodic-automation-lanes
    Contributor

    Refinement. A read-only run explored this repository, searched its issues and pull requests, and read the allowed docs to draft what follows. On an issue a listed human filed, it counts as the issue's spec once posted, while unedited; it is still model text, so check it.

    Intended outcome: Narrow the issue to the dispatcher half: count a completed fix-ci or fix-docs run that posted no check toward the restart cap or as handled, with tests; the subscription-start half is already in the code.

    Related work

    Open questions

    1. Is the subscription-start half (fix-ci and fix-docs) already done, so the issue shrinks to the dispatcher half?
      RECOMMENDED: Yes. Retitle the issue to the dispatcher half and drop the subscription-start criteria, keeping only a regression test that pins the start conditions. Basis: .github/workflows/pr-unblock.yml:1782-1786 starts on fix-ci-handoff.outputs.posted; .github/workflows/pr-refine.yml:185-190 starts on fix-docs-handoff outputs; neither reads needs.fix-ci.outputs or needs.fix-docs.outputs for its start.
    2. Where should the fix-docs dispatcher change be made, given that the issue cites pr-unblock.yml lines that no longer hold fix-docs?
      RECOMMENDED: Make it in .github/scripts/pr-unblock/fix-docs-dispatch.mjs and its test, and in the pr-refine.yml fix-docs jobs. Treat a completed 'pr-refine fix-docs #<n>' run as handled, using the runTitle helper at fix-docs-dispatch.mjs:146. Basis: .github/scripts/pr-unblock/fix-docs-dispatch.mjs:146,195,210 (the in-flight guard skips only runs that are not completed)
    3. How should fix-ci-dispatch count a dispatched run that posted no check toward fix-ci-restarts?
      RECOMMENDED: Count completed workflow runs titled by runTitle(activity, number) on the current base that have no matching check run, alongside the failed check runs already counted at fix-ci-dispatch.mjs:298-302. Keep the runs-per-head cap. Basis: judgment; fix-ci-dispatch.mjs:298-302 counts only failed check runs, so a run that posts no check is never counted.
    4. Should the fix-docs L2 finding (an untrusted-author same-repository PR takes a dispatch slot on every push) be part of this issue?
      RECOMMENDED: Yes, and fold it into the handled-marking fix, since a completed run title marks that PR handled too. If the owner prefers, split it into a separate issue. Basis: fix-docs-dispatch.mjs:160,175 as cited in the 2026-10-22 comment; judgment on scope

    Sources read

    • context.json: the issue body and the three maintainer comments, with no recorded owner decision
    • /home/runner/work/claude-code-plugins/claude-code-plugins/.base/.github/workflows/pr-unblock.yml:1743-1811: fix-ci-handoff reads the jobs API, and fix-ci-subscription starts on needs.fix-ci-handoff.outputs.posted != 'true'
    • /home/runner/work/claude-code-plugins/claude-code-plugins/.base/.github/workflows/pr-refine.yml:148-190: fix-docs-handoff and fix-docs-subscription start on handoff outputs
    • /home/runner/work/claude-code-plugins/claude-code-plugins/.base/.github/scripts/pr-unblock/fix-ci-dispatch.mjs:298-302: the restart cap counts failed check runs only; line 363 is the in-flight guard
    • /home/runner/work/claude-code-plugins/claude-code-plugins/.base/.github/scripts/pr-unblock/fix-docs-dispatch.mjs:129,195-220: handled means a check run exists, and the in-flight guard skips only runs that are not completed

    Automated refinement by the lanes App. Answer with a comment whose first line is **Decision recorded.**.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: securitySecurity-relevant: vulnerability, hardening, or disclosure follow-up.priority: highSignificant impact, or blocks an imminent release; staff this cycle.status: needs-decisionAwaiting a human or maintainer judgment call.work-class: scopedA briefed fix or small feature; blast radius bounded by the brief, tests exist.

    Type

    Fields

    No fields configured for Task.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions