Skip to content

ci: publish AI triage comments from a validated payload, not model free text - #581

Closed
allxsmith wants to merge 3 commits into
mainfrom
ci/457-triage-structured-payload
Closed

allxsmith wants to merge 3 commits into
mainfrom
ci/457-triage-structured-payload

Conversation

@allxsmith

@allxsmith allxsmith commented Aug 28, 2026 •

Copy link
Copy Markdown
Owner

Fixes #457. Refs #361, #340, #317, #338, #312.

The triage session stops posting. It reports TRIAGE-PAYLOAD: lines; a fail-closed validator parses them and renders the comment bodies from a trusted skeleton; a separate step upserts those bodies by marker as bestaxbot. This is the shape claude-repro.yml already uses, applied to the sibling that did not have it.

This is a security change (.github/CLAUDE.md rule 2)

Two parts, both narrowing:

1. The allowlist shrinks. Bash(gh pr comment:*) and Bash(gh issue comment:*) are removed, leaving GET-only gh reads plus Task. Nothing is added.

2. AI_LOOP_PAT leaves the Claude step. The session now gets the job's GITHUB_TOKEN; the PAT appears in exactly one place, the publish step's env.

The second is the part worth reviewing, and the argument for it is not the re-trigger one:

  • AI_LOOP_PAT is documented as full repo write and is the same durable credential claude-implement.yml pushes with and claude-pr-loop.yml uses across four steps. Leaking it means revoking the whole loop's identity. GITHUB_TOKEN expires with the job and is scoped to this repo.
  • This session has Bash and Task, ingests attacker-controlled issue and PR text by design, and runs with egress unenforced ([Security] harden-runner silently downgrades egress-policy: block to audit — no AI job has ever enforced egress #487).
  • Reading an environment variable is not a write, so the tool allowlist never guarded that credential at all. That is invariant I1's own reasoning applied to a credential I1 does not currently name.

The re-trigger and full-repo-write arguments are real but secondary: both require the allowlist to fail first, and after this change the session has no comment tool regardless of which token it holds.

What does not change

Published comments are identical in provenance: bestaxbot authors them, via the PAT, in the publish step, so they still emit issue_comment events exactly as before.

  • Copilot is untouched. Its review is requested by claude-implement.yml (gh pr edit --add-reviewer "@copilot"), which already runs on GITHUB_TOKEN, and its auto-review fires on PR open/push — never on triage comments.
  • Nothing in this repo consumes triage-comment events anyway: bestaxbot-reply.yml and claude.yml both gate on sender.login != 'bestaxbot', and claude-pr-loop.yml fires on CodeRabbit reviews.

Watch item for the first live run

ai-scan.yml proves gh issue view / gh pr view / gh pr diff work on the same action SHA with GITHUB_TOKEN, but gh search has no in-repo precedent — triage is the only session that searches. Same 30 req/min class, public repo, and the command files already cap 6 searches per agent, so this should be a non-event. If it 403s, the revert is one field: restore github_token: ${{ secrets.AI_LOOP_PAT }} on the Claude step. Everything else, publish step included, is unaffected.

The renderer

scripts/render-triage-comments.mjs replaces the workflow's inline sentinel jq (rule 9) and reuses parse-scan-verdict.mjs's exported helpers rather than growing the YAML.

  • Anchoring: payload lines must be the last non-empty lines of the last result record, one per expected command, with the whole-message count exactly right. An echoed copy mid-message makes the count N+1 and fails. Whitespace-only lines still count as lines. Leading narration is tolerated (the run-29794090279 lesson).
  • Closed schema: exact key sets, so an unknown key — __proto__ included — rejects. Numbers must be safe integers in range and not the item being triaged; duplicateOf must name one of the listed duplicates; titles and reasons are byte-capped and must contain no control characters, which is the structural kill for newline smuggling.
  • Sanitization runs on fields, before interpolation — never on the assembled body. This is the deliberate inversion from sanitize-repro-draft.mjs, and the header says why: a whole-body pass would defang the skeleton's own <!-- ai-triage:dedupe --> marker and Duplicate of #N line, which auto-close-duplicates.mjs consumes, and silently break auto-close. Every @ is encoded, not just the three re-trigger handles, because bestaxbot authors these comments so any live mention pings a person.
  • Fails closed: any defect exits non-zero with empty stdout. The wrapper runs set -euo pipefail with no fallback plus an exit-0-empty-stdout guard (the claude-repro precedent). stderr carries fixed strings only, never payload-derived text.

The test sibling pins the rendered bodies byte-for-byte, imports the consumer's own MARKER/DUPLICATE_RE so the coupling cannot drift, and pre-asserts its hostile fixtures are actually hostile before checking they are neutralized.

Behavior change worth noting

The empty-result trigger policy moves out of the prompt and into buildEnvelope: dedupe reports its empty result on any trigger; the two PR commands stay silent on opened and post on labeled. That rule used to depend on the model remembering it. It is now structural — a labeled rerun cannot be silently skipped.

Incidentally this also fixes a real cosmetic bug: existing triage comments show the literal template text **Related** (optional, at most 3), because the model copied the parenthetical out of the command file. The renderer emits **Related**.

Expected self-inflicted failure (rule 9 bootstrap gap)

This PR's own auto-triage run did not exercise the gap: the gate skipped it with PR body already links an issue (Fixes/Closes) — skipping, so no session started (run 33137204485). The gap is still real for a manual ai-triage label run on this branch — the job checks out the default branch, which does not yet contain scripts/render-triage-comments.mjs, so the Validate step would exit MODULE_NOT_FOUND and fail the job.

That is the documented cost of extraction and is not to be fixed by checking out PR head — running PR-authored code in a credential-holding job is the thing that pin prevents. The label-removal step is always(), so the ai-triage button would not wedge.

Merge order

Please merge #580 (#455) first. Both PRs edit .github/CLAUDE.md rule 2's table — that one changes the ai-scan row and the table's intro line, this one changes the ai-triage row — so this branch will want a rebase afterward.

Review checklist

  • Allowlist growth: none, it shrinks by two entries.
  • Model session gaining execute/fetch/network: no. --disallowedTools unchanged.
  • Model text reaching a comment body: only through the validated payload and sanitizeField, into a trusted skeleton.
  • Posting identity: bestaxbot's PAT, in the deterministic publish step only. The Claude step now holds the job GITHUB_TOKEN (still write-scoped for the gate and label removal — the allowlist is the control there, and the header says so).
  • New repository variable: none. AI_TRIAGE_AUTOCLOSE moves from the prompt to the render step; the model is no longer told it.
  • Action SHAs: unchanged, still on the repo-wide pins.
  • Verdict path fails closed: yes, and its matrix is now unit-tested rather than inline jq.
  • Comment upsert scoped by marker + (bestaxbot OR Bot-type author), never --edit-last (rule 6).

Verification

pnpm all green. 500 script tests pass, 47 of them new. The two new workflow steps were also run end-to-end locally against fixture execution files with gh stubbed: full dedupe render with the auto-close notice, PR-mode opened silence vs labeled posting on identical payloads, trailing-narration rejection with exit 1, and the missing-execution-file tolerance path.

Summary by CodeRabbit

  • New Features

    • Improved automated triage comment publishing with validated, consistently formatted results.
    • Added safer handling for duplicate, issue-linking, and duplicate pull request triage outcomes.
    • Ensured triage comments are updated reliably without selecting human-authored comments.
  • Bug Fixes

    • Prevented untrusted content from spoofing triage result markers.
    • Added fail-closed behavior for malformed or unsafe triage results.
  • Documentation

    • Clarified triage behavior, comment authorship, and local versus automated runs.

…ee text

The triage session goes GET-only: it reports TRIAGE-PAYLOAD lines that a
fail-closed validator parses and renders into comment bodies, and a
separate step upserts them by marker as bestaxbot. Both comment commands
leave the allowlist, which narrows rather than widens it.

AI_LOOP_PAT leaves the Claude step; the session now holds only the job
GITHUB_TOKEN. That is the part worth reviewing: the PAT is full repo
write and the same durable credential claude-implement.yml pushes with,
so leaking it means revoking the whole loop's identity — and this session
runs Bash and Task over attacker-controlled text with egress unenforced
(#487). Reading an env var is not a write, so the allowlist never guarded
that credential at all. Comments are still authored by bestaxbot, so
their provenance and event behavior are unchanged.

scripts/render-triage-comments.mjs replaces the workflow's inline
sentinel jq (rule 9), reusing parse-scan-verdict.mjs's helpers. It
end-anchors the payload lines and requires an exact count, validates a
closed schema, and renders from a trusted skeleton — sanitizing only the
model's title/reason strings, before interpolation, never the assembled
body, so the marker and Duplicate-of line auto-close-duplicates.mjs
consumes cannot be self-defanged. Its test sibling pins the bodies
byte-for-byte and the fail-closed matrix, and asserts a hostile field
cannot forge either.

The empty-result trigger policy moves out of the prompt into the
renderer, so a labeled rerun can no longer be silently skipped.

Fixes #457. Refs #361, #340, #317, #338, #312.
Copilot AI balanced review requested due to automatic review settings August 28, 2026 02:52
@coderabbitai

coderabbitai Bot commented Aug 28, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 45d8a7fa-69ac-4365-bd9f-f40b7edacdef

📥 Commits

Reviewing files that changed from the base of the PR and between 0df313b and ad4a690.

📒 Files selected for processing (13)
  • .claude/commands/triage-dedupe.md
  • .claude/commands/triage-find-duplicate-prs.md
  • .claude/commands/triage-find-issues.md
  • .github/CLAUDE.md
  • .github/workflows/ai-triage.yml
  • CLAUDE.md
  • docs/docs/guides/getting-started/ai-development.md
  • scripts/pick-triage-upsert.mjs
  • scripts/pick-triage-upsert.test.mjs
  • scripts/render-triage-comments.mjs
  • scripts/render-triage-comments.test.mjs
  • scripts/sanitize-repro-draft.mjs
  • scripts/sanitize-repro-draft.test.mjs

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


Walkthrough

The AI triage workflow now emits structured payloads. A deterministic renderer validates and formats comments. A separate publisher uses AI_LOOP_PAT to upsert comments as bestaxbot. The session has GET-only GitHub access. New scripts and tests cover rendering, sanitization, validation, and comment selection.

Changes

Structured triage publishing

Layer / File(s) Summary
Structured payload contract
.claude/commands/triage-*.md, .github/workflows/ai-triage.yml, CLAUDE.md, docs/docs/guides/getting-started/ai-development.md
CI triage sessions now report TRIAGE-PAYLOAD JSON instead of posting comments. Local posting instructions remain separate.
Payload validation and rendering
scripts/render-triage-comments.mjs, scripts/render-triage-comments.test.mjs
The renderer validates schemas, sanitizes fields, applies trigger policies, and creates deterministic comment bodies.
Workflow credential and publishing separation
.github/workflows/ai-triage.yml
The session uses GET-only tools and GITHUB_TOKEN. A separate step uses AI_LOOP_PAT to publish marker-tagged comments.
Automation comment selection
scripts/pick-triage-upsert.mjs, scripts/pick-triage-upsert.test.mjs
The selector finds the newest automation-authored comment for a marker and reports defined CLI error codes.
Payload sentinel protection and architecture documentation
.github/CLAUDE.md, scripts/sanitize-repro-draft.mjs, scripts/sanitize-repro-draft.test.mjs
Documentation records the separated publishing architecture. The sanitizer defangs line-start TRIAGE-PAYLOAD markers.

Estimated code review effort: 5 (Critical) | ~90 minutes

Merge Risk: 🔵 Low · up to ad4a6

The change safely separates validated triage rendering from comment publication, but merge readiness still requires owner awareness because marked comments can be updated when authored by any bot-class account rather than only the intended publisher, and the publishing credential’s effective scope is not established in repository configuration.

Sequence Diagram(s)

sequenceDiagram
  participant TriageSession
  participant RenderTriageComments
  participant PickTriageUpsert
  participant GitHub
  TriageSession->>RenderTriageComments: Emit TRIAGE-PAYLOAD JSON
  RenderTriageComments->>RenderTriageComments: Validate and sanitize payload
  RenderTriageComments->>PickTriageUpsert: Select marker comment for refresh
  PickTriageUpsert->>GitHub: Read comments
  GitHub-->>PickTriageUpsert: Return comment records
  PickTriageUpsert-->>RenderTriageComments: Return target comment ID
  RenderTriageComments->>GitHub: Render and upsert comment as bestaxbot
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.06% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 17 functions across 6 files. (7 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the primary change: publishing AI triage comments from validated payloads instead of model-generated free text.
Description check ✅ Passed The description provides a detailed change summary, linked issues, security rationale, behavior changes, risks, merge guidance, and verification results. It does not reproduce all template headings or…
Linked Issues check ✅ Passed The changes satisfy issue #457 by introducing structured payload validation, deterministic rendering, PAT-based bestaxbot publishing, removal of comment-posting tools from the session, and comprehensi…
Out of Scope Changes check ✅ Passed The workflow, renderer, upsert helper, tests, sanitizer update, and documentation changes directly support the linked issue and the stated security objectives. No unrelated code changes are evident.
Full details: Description check

Explanation

The description provides a detailed change summary, linked issues, security rationale, behavior changes, risks, merge guidance, and verification results. It does not reproduce all template headings or checklist items, but the missing items are non-critical.

Full details: Linked Issues check

Explanation

The changes satisfy issue #457 by introducing structured payload validation, deterministic rendering, PAT-based bestaxbot publishing, removal of comment-posting tools from the session, and comprehensive security-focused tests.

Full details: Docstring Coverage

Explanation

Docstring coverage is 47.06% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 17 functions across 6 files. (7 skipped: 7 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/457-triage-structured-payload

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://463f072f.bestax.pages.dev

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The security-sensitive PAT publication logic remains as untested inline workflow shell, contrary to the repository’s extraction convention.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Moves AI triage to validated structured payloads, addressing #457 while isolating the bestaxbot PAT from the model session.

Changes:

  • Adds a fail-closed payload validator, sanitizer, renderer, and comprehensive tests.
  • Restricts the model session to read-only tools and publishes comments deterministically.
  • Updates triage commands and documentation for the new architecture.
File summaries
File Description
.github/workflows/ai-triage.yml Validates, renders, and publishes structured triage results.
.github/CLAUDE.md Documents the security boundary and renderer.
.claude/commands/triage-dedupe.md Defines CI payload reporting for issue deduplication.
.claude/commands/triage-find-issues.md Defines payload reporting for related issues.
.claude/commands/triage-find-duplicate-prs.md Defines payload reporting for duplicate PRs.
scripts/render-triage-comments.mjs Implements validation, sanitization, and rendering.
scripts/render-triage-comments.test.mjs Covers renderer and payload behavior.
scripts/sanitize-repro-draft.mjs Defangs triage payload sentinels in drafts.
scripts/sanitize-repro-draft.test.mjs Tests sentinel sanitization.
docs/docs/guides/getting-started/ai-development.md Explains deterministic triage publication.
CLAUDE.md Updates repository-level triage documentation.
Review details
  • Files reviewed: 11/11 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +572 to +575
run: |
set -euo pipefail
COUNT=$(jq '.comments | length' "$ENVELOPE")
for i in $(seq 0 $((COUNT - 1))); do
Copilot's review of the publish step was right that the marker/author
comment selection is exactly the shape rule 9 sends to a tested script.
Extracting it turned up a second problem: the jq was a reimplementation
of logic auto-close-duplicates.mjs already exports, and it had drifted.
Its author test was `login == "bestaxbot" or type == "Bot"`, which misses
a `[bot]`-suffixed login typed as User, where the shared
isAutomationAuthor covers Bot-type, the suffix, and named machine users.

scripts/pick-triage-upsert.mjs now makes that choice, importing that
helper so the two consumers of the triage markers cannot drift again, and
parse-scan-verdict.mjs's parseExecutionRecords to flatten the
one-array-per-line stream `gh api --paginate` produces when --jq is
applied per page. That pagination shape is what the old `tail -n 1`
survived only by accident: a marker comment on any page but the last was
found only if the last page happened to hold one too. Its test sibling
pins that case, the legacy author classes (claude[bot],
github-actions[bot]) a labeled re-run on an old item depends on, and the
rule 6 failure the whole thing exists to prevent — never selecting a
human comment that quotes the marker.

The remaining shell is a loop, a disposition check, and PATCH-vs-POST,
which is the size claude-repro.yml's publish keeps inline.
Copilot AI review requested due to automatic review settings August 28, 2026 13:08
@allxsmith

Copy link
Copy Markdown
Owner Author

Thanks — the rule 9 point on the publish shell was right, and acting on it surfaced a second problem the comment did not mention.

Extracted to scripts/pick-triage-upsert.mjs (+ test sibling, 18 tests) in 07161a6. The extraction is not a like-for-like move: the jq was a reimplementation of logic auto-close-duplicates.mjs already exports, and it had drifted. The jq tested login == "bestaxbot" or type == "Bot"; the shared isAutomationAuthor also covers a [bot]-suffixed login typed as User. The new script imports that helper, so the two consumers of these markers cannot drift again — which is a stronger outcome than testing the jq in place would have been.

It also imports parse-scan-verdict.mjs's parseExecutionRecords to flatten the one-array-per-line stream gh api --paginate emits when --jq is applied per page. That shape is what the old tail -n 1 survived only by accident: a marker comment on any page but the last was found only when the last page happened to contain one too. There is now a test for exactly that.

On scope, I stopped short of your suggestion to cover "refresh/post and disposition: none behavior with a stubbed API". The in-repo precedent argues against it: auto-close-duplicates.mjs does all its own HTTP with fetch and its test sibling stubs none of it — it tests the pure helpers only. What remains inline here is a loop, a disposition check, and PATCH-vs-POST, which is the size claude-repro.yml's publish step keeps inline and which rule 9 explicitly leaves there. If you think the HTTP half should be tested too, that is a change to the repo's convention rather than to this PR, and worth its own issue.

@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://b78d49e8.bestax.pages.dev

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Unicode sanitization gaps and inaccurate credential-boundary documentation should be corrected before approval.

Review details

Suppressed comments (4)

Previously missed (4) — in code that hasn't changed since the last review.

scripts/render-triage-comments.mjs:142

  • The single-line guard omits C1 controls and Unicode line/paragraph separators. A payload can encode U+0085, U+2028, or U+2029, pass validation, and place a line-separator character into the bestaxbot comment despite the closed schema’s no-control/single-line contract. Reject these ranges as well and cover them in the hostile-text test.
const CONTROL_RE = /[\u0000-\u001F\u007F]/;

scripts/render-triage-comments.mjs:163

  • This is not the complete Unicode Bidi_Control set: U+061C ARABIC LETTER MARK is missing, so attacker-controlled titles/reasons can still alter bidirectional rendering even though this sanitizer is intended to remove bidi controls. Include U+061C and add it to the bidi regression test.
  [/[\u200E\u200F\u202A-\u202E\u2066-\u2069]/g, ''],

.github/workflows/ai-triage.yml:172

  • Job-level permissions apply to the Claude step too, and line 373 passes that write-scoped GITHUB_TOKEN into the session. Saying these grants only cover the gate and label-removal steps contradicts the later security note and obscures that the read-only tool allowlist is the session’s confinement boundary.
      # Triage comments themselves post via bestaxbot's PAT (see the publish
      # step; the Claude step holds only this job's GITHUB_TOKEN), not
      # GITHUB_TOKEN — these grants only cover the gate and label-removal
      # steps.

.github/CLAUDE.md:85

  • The renderer and PAT holder are different steps: validation/rendering occurs at lines 512–544, while only the publish step at lines 565–611 receives AI_LOOP_PAT. The current wording incorrectly says the rendering step holds the credential, which misdocuments the security boundary this PR introduces.
  payload, a deterministic step renders the body from a trusted skeleton, and only that
  step holds bestaxbot's PAT — which also means no model session shares an environment
  • Files reviewed: 13/13 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Deep review — 0 blocking · 3 advisory

# Severity Area Finding Location
1 🔵 Advisory Robustness The strict NDJSON parse of gh api --paginate --jq '[…]' output rests on gh emitting one compact array per page; no in-repo run has exercised >1 page through this stricter path (the budget probe uses a tolerant jq -s). Fails safe. scripts/pick-triage-upsert.mjs:96
2 🔵 Advisory Robustness Bash(gh search:*) has no prior in-repo precedent under GITHUB_TOKEN; a 403 would fail closed (no payload → job errors, no comment) rather than post wrong. .github/workflows/ai-triage.yml:393
3 🔵 Advisory Robustness Documented rule-9 bootstrap gap: a manual ai-triage label run on this branch fails MODULE_NOT_FOUND until merged, since the job checks out the default branch. Accepted; label removal is always(). .github/workflows/ai-triage.yml:344

Overall: The change is sound and, as advertised, strictly narrowing — it removes the two gh … comment allowlist entries and moves AI_LOOP_PAT out of the model session into a deterministic publish step, so no model session shares an environment with a re-trigger-capable credential (I2). The riskiest surface is the new render-triage-comments.mjs trust boundary between model output and a posted comment; I exercised it and it holds: end-anchoring plus an exact per-command payload count reject echoed/interleaved/trailing sentinels, the schema is closed (unknown keys including __proto__, control chars, out-of-range/self numbers, and a duplicateOf not in the listed duplicates all reject), and sanitizeField runs on title/reason before interpolation so a hostile field cannot forge the <!-- ai-triage:dedupe --> marker or the Duplicate of #N line the auto-close cron consumes. The human should focus first on confirming the operational watch-items (the gh search token class and the multi-page comment fetch) on the first live run — both fail safe, so they gate nothing.

Residual risk (for the class this PR addresses — model free text reaching a re-trigger-capable comment):

  • Forged marker / Duplicate of #N injection — refuted: sanitizeField encodes <!--, both -->/--!> spellings, and Duplicate of #; CONTROL_RE rejects newline/tab at validation so no title can inject an extra line. Verified against auto-close-duplicates.mjs's own MARKER/DUPLICATE_RE in the test sibling (65/65 pass).
  • Attacker text echoed as a payload — refuted: any extra sentinel line makes the count differ from expected and fails closed; the last result record's tail must be exactly the expected payloads, position-matched to the command.
  • Wrong comment PATCHed / duplicated on re-run — refuted: pickUpsertTarget selects only automation-authored comments carrying the exact marker (shared isAutomationAuthor); a human comment quoting a marker is never selected, and "print nothing → POST" is the safe direction.
  • Silent no-op (a bailed session posting nothing quietly) — refuted: a session that dies mid-task emits no payloads, the count check fails, and the job fails loudly; the #317/#338 shapes both map to a non-zero exit with empty stdout.

🏄 Dude, this one's a clean drop-in — they didn't paddle out for more capability, they trimmed the sail: two comment tools gone, the gnarly PAT off the model's board and locked in the deterministic publish channel, and a fail-closed renderer standing between the model's words and the wave. Tests are glassy at 65/65. Send it. 🌊

#455 landed while this branch was open, and both changes rewrote the same
rule 2 table in .github/CLAUDE.md — #455 the ai-scan row, this branch the
ai-triage one. Resolved by taking both rows on #455's wider header, which
already frames the column as "which credential", and by carrying that
framing into the ai-triage row: repository write is still the allowlist's
to hold there (the gate and label removal need a write-scoped job token),
the model credential likewise, and what changed is that AI_LOOP_PAT is no
longer in the job at all.

Everything else auto-merged: rule 9 keeps this branch's entry for the two
new scripts, rule 2's closing bullet keeps #455's worked example, and the
ai-development guide keeps both edited paragraphs.
Copilot AI review requested due to automatic review settings August 28, 2026 16:04
@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://7298462a.bestax.pages.dev

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The PAT-backed publish step executes scripts from a workspace that the untrusted model session can potentially modify.

Review details

Suppressed comments (1)

.github/workflows/ai-triage.yml:601

  • Blocking: this PAT-holding step executes pick-triage-upsert.mjs from the same checkout the untrusted Claude session just had Bash access to. Bash redirection can modify tracked files despite Edit/Write denies (the repository already documents this at .github/workflows/claude-pr-loop.yml:163-166); for example, an injected session can redirect an attacker-controlled issue body into this script, emit an otherwise-valid payload, and then have line 600 execute that body with GH_TOKEN=AI_LOOP_PAT. The validator invoked at line 530 is mutable for the same reason, so step-scoped PAT injection does not establish I2. Perform validation and publishing in fresh isolated jobs/checkouts, transferring only the execution file and validated envelope between them, so no PAT-backed step executes a workspace the model could modify.
            EXISTING=$(node scripts/pick-triage-upsert.mjs \
              --comments-file="$COMMENTS" --marker="$MARKER")
  • Files reviewed: 13/13 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@allxsmith

Copy link
Copy Markdown
Owner Author

Superseded by #582, which merged as 4361064. Both carry Fixes #457, so closing this one to avoid two implementations of the same issue.

Recording what was compared, since the two designs differ in ways worth remembering if this is ever revisited:

The reasoning in this description was sound and largely matches where #582 landed after review, including the point that reading an environment variable is not a write, so the tool allowlist never guarded AI_LOOP_PAT at all. #582 reaches the same conclusion by splitting the job so the session holds no PAT.

@allxsmith allxsmith closed this Aug 28, 2026
@allxsmith
allxsmith deleted the ci/457-triage-structured-payload branch August 28, 2026 19:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Publish AI triage comments from a structured payload, not model free text

2 participants