fix(ci): let Repo Guard check out fork PRs from allowlisted contributors (#437) - #438
Conversation
…ors (#437) actions/checkout added an `allow-unsafe-pr-checkout` input (default false) that refuses to check out fork PR code from `pull_request_target` workflows. It does not read the job-level actor allowlist, so the allowlist alone cannot satisfy it — every fork PR failed at the checkout step before the review step ran. Step results on #393 show the job was admitted, not skipped: 1. Set up job: success <- if: gate passed 2. Run actions/checkout@v4: failure 3. Run ceilf6/repo-guard@main: skipped Earlier green runs were all same-repo branches, which take the `head.repo.full_name == github.repository` path and never hit the refusal. Set the opt-in explicitly and record why it is defensible here: triggering stays gated to repo members plus the named allowlist, `persist-credentials` stays false, and the action runtime comes from ceilf6/repo-guard@main rather than the PR. The trust boundary is those specific contributors. Adds a regression test so the opt-in cannot be dropped silently, and asserts the allowlist gate is still present alongside it.
There was a problem hiding this comment.
代码评审报告: fix(ci): let Repo Guard check out fork PRs from allowlisted contributors (#437)
风险等级: 中
处理建议: 需要人工判断
决策摘要: 主修复本身正确且必要(allow-unsafe-pr-checkout 是唯一开关,job 级 if 确实无法满足它),但 PR/注释/CHANGELOG 声称的信任边界「仅这几个具名的人」在 issue_comment 路径上不成立——那条路径校验的是评论者而非 PR 作者,因此仍可对任意 fork 作者的代码执行 checkout;这属于维护者需要显式拍板的信任决策,不应由自动评审直接批准。
级联分析
- 变更符号: 无(CI YAML + 测试断言 + CHANGELOG 条目,无被索引的代码符号)
- 受影响流程: Repo Guard 的两条 checkout 路径 ——
pull_request_targetfork PR 评审、issue_comment对 PR 的评审;.github/workflows/repo-guard.yml:63-68 - 变更集外调用方: 仓库内无。
.github/workflows/下只有 repo-guard.yml 使用pull_request_target(contract-guard/ci 均为pull_request,不受该拒绝影响),故不存在遗漏的同类工作流。外部消费方是ceilf6/repo-guard@main,它消费 checkout 出来的工作区,本仓库无法验证其行为。 - 置信度: medium(仓库内证据完整;但两个决定性外部产物 ——
actions/checkout的拒绝判定范围、ceilf6/repo-guard@main是否执行工作区脚本 —— 在本 checkout 内不可验证)
问题发现
-
[高]
issue_comment路径的门禁校验评论者,而非 PR 作者,与记录在案的信任边界不一致- 证据: job 级
if对issue_comment的判断是github.event.comment.author_association/github.event.comment.user.login(.github/workflows/repo-guard.yml:43-47);checkout 步骤对该事件取refs/pull/{issue.number}/head(:64-66)。也就是说任一 OWNER/MEMBER/COLLABORATOR(或两位具名白名单)在任意 fork PR 下留言,就会 checkout 该 fork 作者的代码。issue_comment事件的 payload 里github.event.issue.pull_request只有 url 字段,无法据此判断 head repo,所以现有表达式也无从补上这层判断。而工作流注释(:61-62)与 CHANGELOG(CHANGELOG.md:13)都把开关的正当性建立在「信任边界是这几个具名的人而非任意 fork 提交者」之上。 - 受影响调用方/流程: 自托管 runner(
vars.REPO_GUARD_RUNNER)上的 Repo Guard 评审 job,env 携带secrets.LLM_API_KEY;PR 自己也承认「ceilf6/repo-guard@main是否会调用工作区内的 package scripts」尚未确认——若会,则任意 fork 代码在该路径上从「评审数据」变成「可执行代码」。 - 说明: 我无法在本 checkout 内确认
actions/checkout的拒绝逻辑是否只按pull_request_target/workflow_run事件名生效。若是,则该暴露面在本 PR 之前就已存在,本 PR 不引入新风险,只是把一个不准确的边界声明写进了仓库;若拒绝逻辑更宽,则本 PR 新打开了这条路径。这一分叉正是需要人工判断的原因。 - 最小可行修复: 二选一。(a) 把声明改准确 —— 注释与 CHANGELOG 明确写出
issue_comment路径信任的是「有权评论的成员会先审阅 PR 内容」,而非 PR 作者本身;(b) 若不接受该语义,把 checkout 步骤的if拆成两条,仅在pull_request_target分支保留 fork 允许,issue_comment分支先用一步gh api取回 head repo 再决定是否 checkout。无需改动pull_request_target主路径。
- 证据: job 级
-
[中] 新测试无法兑现「开关不会比其正当性条件活得更久」的承诺
- 证据:
scripts/tests/workflow-rules.test.mjs:546-549三条断言都是整文件级正则。persist-credentials:\s*false不与 checkout 步骤绑定;contains\(fromJSON\('\["NanluQingshi","HaveNiceDa"\]'\)在文件里有三处匹配(:32、:39、:46),因此把pull_request_target分支的那条白名单(:32,即真正决定 fork PR 能否触发评审的那条)整个删掉,测试依然全绿。 - 受影响调用方/流程:
test:workflows(38 项)对 repo-guard 信任配置的回归保护;PR 描述把这条测试作为接受该 opt-in 的理由之一。 - 最小可行修复: 把断言锚定到
pull_request_target分支的结构,例如匹配github\.event\.pull_request\.user\.login与head\.repo\.full_name == github\.repository同时存在;同时用结构而非具名人名断言(contains\(fromJSON\(.+\), github\.event\.pull_request\.user\.login\)),这样既锚定到正确路径,也避免日后增删白名单成员时打破一条无关的工作流测试。
- 证据:
-
[低]
actions/checkout@v4仍是移动 tag,且与仓库其余工作流不一致
行级发现
- [.github/workflows/repo-guard.yml:68] 该 opt-in 同时作用于下方
if的issue_comment分支,那条分支只校验评论者身份、不校验 PR 作者,因此覆盖范围大于注释声明的「具名白名单」;要么按注释收紧该分支,要么改正注释。 - [.github/workflows/repo-guard.yml:62] 「信任边界是这几个具名的人」对
pull_request_target成立,对:43-47的issue_comment分支不成立(那里判断的是comment.user.login/author_association);请补一句说明该路径信任的是评论成员的审阅行为。 - [scripts/tests/workflow-rules.test.mjs:549] 这条正则同样匹配
:39、:46两处 issue/comment 白名单,删掉真正相关的:32(PR 作者白名单)测试仍会通过;改为断言github\.event\.pull_request\.user\.login所在的那条,并去掉硬编码人名。 - [scripts/tests/workflow-rules.test.mjs:547]
persist-credentials: false是整文件匹配,未与 checkout 步骤绑定;若日后新增其他 checkout 步骤,这条断言可能被无关步骤满足,建议在同一with:块内做定位。 - [CHANGELOG.md:13] 「trust boundary is those specific contributors rather than arbitrary fork authors」与
issue_comment路径的实际门禁不符,需与工作流注释一并改准确。
Karpathy 评审
- 假设: 核心假设「job 级
if已把触发者限死在具名白名单」只在pull_request_target路径成立,issue_comment路径未被验证即被纳入同一句结论。另一处显式承认但未闭环的假设是ceilf6/repo-guard@main不执行工作区脚本 —— 它决定了「checkout 只是数据」这句话是否成立,PR 正确地把它列为 follow-up 而非当作已证事实。 - 简洁性: 修复是一行 action input,没有引入抽象、开关或间接层,与问题规模匹配。注释偏长但记录的是信任决策,属于有价值的密度而非噪声。
- 结构质量: 无退化。未新增分支、wrapper、特例路径;工作流的 job 图、触发集、权限、门禁条件均未变动。测试文件新增 12 行,未跨越可扫描边界。
- 变更范围: 三个文件全部服务于既定目标,无夹带重构或格式噪声。作者主动把 SHA pinning、prompt injection 两项排除在本 PR 之外,范围克制得当。
- 验证:
pnpm quality:precommit全绿(38/38 workflow 测试)已被诚实报告,且作者明确说明「fork 路径要等下一个 fork PR 才被真正验证」—— 这是准确的边界陈述,没有把同仓库分支的绿色当成 fork 路径的证据。缺口在于新测试锚定过松(见发现 2),以及无任何断言覆盖 checkout 步骤自身的if条件。
缺失覆盖
- 断言 opt-in 与
persist-credentials: false位于同一个 checkout 步骤的with:块内,而非文件内任意位置。 - 断言
pull_request_target分支的 fork 门禁结构(head.repo.full_name == github.repository或pull_request.user.login白名单)仍然存在,这是该 opt-in 唯一真正依赖的条件。 issue_comment触发路径的 checkout 语义目前无任何测试覆盖;若采纳发现 1 的方案 (b),需要一条断言该分支不会无条件 checkout fork head。- fork 路径的端到端验证只能由下一个 fork PR 提供,作者已在 PR 描述中说明;合并后建议保留 #437 直到该验证到位。
| with: | ||
| ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || format('refs/pull/{0}/head', github.event.issue.number) }} | ||
| persist-credentials: false | ||
| allow-unsafe-pr-checkout: true |
There was a problem hiding this comment.
该 opt-in 同时作用于下方 if 的 issue_comment 分支,那条分支只校验评论者身份、不校验 PR 作者,因此覆盖范围大于注释声明的「具名白名单」;要么按注释收紧该分支,要么改正注释。
| # 且只认这一个开关——上面 job 级 if 白名单是 workflow 层的,action 不读它, | ||
| # 不开这一项则本 job 对所有 fork PR 必然 fail(#437)。 | ||
| # 开启的前提是上面的 if 门禁已把触发者限死在仓库成员 + 具名白名单, | ||
| # 即信任边界是「这几个具名的人」而非任意 fork 提交者;这是一次显式的信任声明。 |
There was a problem hiding this comment.
「信任边界是这几个具名的人」对 pull_request_target 成立,对 :43-47 的 issue_comment 分支不成立(那里判断的是 comment.user.login / author_association);请补一句说明该路径信任的是评论成员的审阅行为。
| assert.match(repoGuard, /allow-unsafe-pr-checkout:\s*true/u); | ||
| assert.match(repoGuard, /persist-credentials:\s*false/u); | ||
| // The opt-in is only defensible while triggering stays gated to known actors. | ||
| assert.match(repoGuard, /contains\(fromJSON\('\["NanluQingshi","HaveNiceDa"\]'\)/u); |
There was a problem hiding this comment.
这条正则同样匹配 :39、:46 两处 issue/comment 白名单,删掉真正相关的 :32(PR 作者白名单)测试仍会通过;改为断言 github\.event\.pull_request\.user\.login 所在的那条,并去掉硬编码人名。
| const repoGuard = readFileSync('.github/workflows/repo-guard.yml', 'utf8'); | ||
|
|
||
| assert.match(repoGuard, /allow-unsafe-pr-checkout:\s*true/u); | ||
| assert.match(repoGuard, /persist-credentials:\s*false/u); |
There was a problem hiding this comment.
persist-credentials: false 是整文件匹配,未与 checkout 步骤绑定;若日后新增其他 checkout 步骤,这条断言可能被无关步骤满足,建议在同一 with: 块内做定位。
|
|
||
| ### Changed | ||
|
|
||
| - **ci**: Repo Guard can review fork PRs again. `actions/checkout` now refuses fork checkouts under `pull_request_target` unless `allow-unsafe-pr-checkout` is set, and it does not read the workflow's own actor allowlist — so every fork PR failed at checkout before the review step ran. The opt-in is set explicitly; triggering stays gated to repo members plus the named allowlist, so the trust boundary is those specific contributors rather than arbitrary fork authors. (#437) |
There was a problem hiding this comment.
「trust boundary is those specific contributors rather than arbitrary fork authors」与 issue_comment 路径的实际门禁不符,需与工作流注释一并改准确。
…boundary Addresses repo-guard review on #438. The previous test did not do what the PR claimed. All three assertions were file-wide regexes, and the allowlist pattern also matches the `issues` and `issue_comment` gates. Deleting the `pull_request_target` PR-author allowlist — the only gate the opt-in actually depends on — left the suite green; verified by removing that line and re-running the patterns. Assertions are now anchored: the opt-in and `persist-credentials: false` must live in the checkout step's own block, and the gate is asserted structurally via `github.event.pull_request.user.login` plus the same-repo condition, so it catches removal and does not break when allowlist membership changes. The recorded trust boundary was also inaccurate. It held for the `pull_request_target` path but not for `issue_comment`, which gates on the commenter rather than the PR author — any member commenting on any fork PR checks out that fork's head. The comment and CHANGELOG now state both paths separately. Resolves the reviewer's open question about whether this PR newly opens the `issue_comment` path: it does not. `assertSafePrCheckout` in actions/checkout returns early unless the event is `pull_request_target` or `workflow_run`, so that path never hit the refusal and is unaffected by this flag. The exposure there is pre-existing; only its description was wrong. Left as follow-ups per the review: `@v4` -> `@v6` alignment and SHA pinning (#437), and confirming ceilf6/repo-guard@main never executes workspace scripts.
|
Addressed in a30dfd0. Both substantive findings were verified before acting rather than taken at face value. 发现 1 —
|
There was a problem hiding this comment.
代码评审报告: fix(ci): let Repo Guard check out fork PRs from allowlisted contributors (#437)
风险等级: 中
处理建议: 评论
决策摘要: 可以合并——改动与 #437 的诊断和提议修复一致,我核对了工作流里 opt-in 所依赖的三个前提(PR 作者门禁、persist-credentials: false、运行时来自外部 action)全部实际存在;剩余问题都是非阻塞的耐久性和测试锚定问题,其中最值得在合并前顺手处理的是把 opt-in 收窄到 pull_request_target 这一条路径。
级联分析
- 变更符号: 无代码符号。变更为 CI YAML 配置输入、一条测试断言、一条 CHANGELOG 条目。
- 受影响流程: Repo Guard 工作流的
pull_request_targetfork checkout 路径;issue_comment路径按注释所述不受影响(未在本仓库内可验证)。 - 变更集外调用方: 仓库内对
.github/workflows/repo-guard.yml的唯一程序性消费者是scripts/tests/workflow-rules.test.mjs(另一处已有断言在:527,与新断言无冲突)(text search)。真正的外部消费者是 GitHub Actions runner 与ceilf6/repo-guard@main,二者不在本 checkout 内,无法核验。仓库无 actionlint/yamllint,因此不存在会对未知with:键报错的 CI 环节 (text search)。 - 置信度: medium
问题发现
-
[中] opt-in 的作用范围宽于它的信任论证
- 证据:
.github/workflows/repo-guard.yml:75无条件设置allow-unsafe-pr-checkout: true,但该 step 的if:(:71)同时服务两条路径。注释:62-69自己指出这两条路径信任边界不同:pull_request_target判 PR 作者,issue_comment判评论者。论证只覆盖前者,开关却覆盖整个 step。 - 受影响调用方/流程: 目前无行为差异(action 的拒绝逻辑只在
pull_request_target/workflow_run生效)。风险在未来:若 action 把该守卫扩展到更多事件,这个已置位的开关会静默把issue_comment这条按评论者门禁的路径也一并放行,而它正是注释中信任边界更宽的那条。 - 最小可行修复:
allow-unsafe-pr-checkout: ${{ github.event_name == 'pull_request_target' }}(渲染为true/false,core.getBooleanInput可接受),让开关范围与注释里的论证严格一致。
- 证据:
-
[中] 回归测试断言的是 YAML 文本而非行为,无法防住与 #437 同类的静默失效
- 证据:
scripts/tests/workflow-rules.test.mjs:550只断言字符串allow-unsafe-pr-checkout: true存在。.github/workflows/repo-guard.yml:70仍是可移动 tagactions/checkout@v4;GitHub Actions 对未知with:键只给 warning、不失败。附带一点:本仓库其余工作流(ci.yml、release.yml、contract-guard.yml、gitguardian.yml)已在@v6,repo-guard 是少数仍在@v4的 (text search)。 - 受影响调用方/流程: 若 tag 移动导致该输入被重命名或移除,fork PR 重新在 checkout 步失败,而
test:workflows依旧全绿——这正是 #437 隐藏至今的机制。 - 最小可行修复: 把该 step 钉到 commit SHA 并加
# v4.x.y注释。#437 已把 SHA pinning 列为 out of scope,因此这里不作为阻塞项,但它是本次修复耐久性的直接前提,建议要么在本 PR 顺手做,要么单独建 issue 跟踪而不只是留在 #437 正文里。
- 证据:
-
[中] 测试未断言
ref仍钉在head.sha,而这是 opt-in 成立的条件之一- 证据: 新测试注释(
scripts/tests/workflow-rules.test.mjs:553-557)声称 opt-in「不会比证成它的条件活得更久」,断言了门禁与persist-credentials,但没有断言.github/workflows/repo-guard.yml:73的ref: ...head.sha。文件顶部的安全论证(:6「固定 sha」)恰恰依赖它。 - 受影响调用方/流程: fork checkout 现在真的会发生了。若
ref后续被改成分支 ref 或refs/pull/{n}/merge,白名单准入的 commit 与实际检出的内容之间会出现 TOCTOU 窗口,且不会触发任何测试失败。 - 最小可行修复: 在同一个测试里加
assert.match(checkoutStep, /github\.event\.pull_request\.head\.sha/u);。
- 证据: 新测试注释(
-
[低] 注释锚定在会静默失准的引用上
- 证据:
.github/workflows/repo-guard.yml:63、:65用硬编码行号(:32)/(:46)指向门禁条件;:68引用assertSafePrCheckout这一 actions/checkout 内部函数名。我核对了两个行号今天是准确的(:32为 PR 作者白名单,:46为评论者白名单)。 - 受影响调用方/流程: 任意在
if:块上方的插入都会让行号失准且无人察觉;上游重命名内部函数也会让:68的论证失去可核验的落点,而这条论证正是「issue_comment路径不受影响」的唯一依据。 - 最小可行修复: 引用条件表达式本身(
github.event.pull_request.user.login/github.event.comment.author_association),并把:68改为引用:58已写明的事件范围(pull_request_target/workflow_run)而非内部函数名。
- 证据:
-
[低] anchor 正则有两个脆弱点,其一与它自己的注释声明不符
- 证据:
scripts/tests/workflow-rules.test.mjs:547的/- uses: actions\/checkout@[\s\S]*?(?=\n\s+- uses:)/u只取文件里第一个 checkout step。注释(:545-546)声称锚定可防止「未来无关的 checkout step 在错误的 step 上满足断言」,但它只挡住了全文件匹配这一种情况:若之后在同一 workflow 追加第二个 checkout step,该 step 完全不受检查。另外,若 checkout 变成文件里最后一个- uses:,lookahead 失败、checkoutStep为undefined,测试会以repo-guard has no actions/checkout step这条与事实不符的消息失败。 - 受影响调用方/流程: 仅
test:workflows。 - 最小可行修复: 终止条件改为
(?=\n\s{6}- |$);若确实关心多 step 场景,改用matchAll并要求所有 checkout step 都满足断言。
- 证据:
行级发现
- [.github/workflows/repo-guard.yml:75] 开关无条件生效,但同一 step 服务的
issue_comment路径按评论者而非 PR 作者门禁;改为allow-unsafe-pr-checkout: ${{ github.event_name == 'pull_request_target' }},使范围与:62-69的信任论证一致。 - [.github/workflows/repo-guard.yml:63] 硬编码行号
(:32)(以及:65的(:46))今天准确,但任何在if:块上方的插入都会让它静默失准;改为引用条件表达式本身。 - [.github/workflows/repo-guard.yml:68]
assertSafePrCheckout是 actions/checkout 的内部函数名,上游重命名后这条论证就无从核验;改为引用:58已写明的事件范围。 - [scripts/tests/workflow-rules.test.mjs:547] lazy lookahead 只覆盖第一个 checkout step(与
:545-546的注释声明不符),且当 checkout 是最后一个- uses:时会以误导性消息失败;终止条件改为(?=\n\s{6}- |$)。 - [scripts/tests/workflow-rules.test.mjs:551] 紧接着补一条
assert.match(checkoutStep, /github\.event\.pull_request\.head\.sha/u);——ref 钉死在准入 SHA 上同样是 opt-in 成立的条件,目前无断言保护。
Karpathy 评审
- 假设: 两个关键假设均被显式记录。「
allow-unsafe-pr-checkout是 v4 上的真实输入」由 #437 引用的运行期错误文本支撑(该错误由@v4产生并点名了这个输入),证据充分;「守卫仅在pull_request_target/workflow_run生效」只由一个内部函数名支撑,来源较弱(见发现 4)。 - 简洁性: 一个配置输入 + 注释 + 一条测试,无新抽象、无新层级。14 行注释配 1 行配置偏重,但记录的是真实信任决策,符合本仓库既有注释密度,不作为问题。
- 结构质量: 无退化。工作流仍为 86 行,未新增分支、flag 或条件路径;测试文件新增 28 行且与相邻断言风格一致。
- 变更范围: 外科手术式。三个文件均可追溯到既定目标,无夹带重构或格式噪声。CHANGELOG 条目偏长,但与相邻的 mcp-filesense 条目篇幅一致,属既有风格。
- 验证:
pnpm quality:precommit的结果我无法在本 checkout 中复跑核实;我逐条手工核对了新测试的四个正则确实能匹配当前工作流内容。真正的 fork 路径按构造无法在本 PR 内验证,PR 正文对此做了诚实说明。
缺失覆盖
ref钉在github.event.pull_request.head.sha的断言(见发现 3)——fork checkout 真正启用后,这是白名单准入与实际检出内容一致性的唯一保证。- fork 路径端到端未验证,这是本次修复的固有属性。建议按 PR 所述在下一个 fork PR 上确认,失败则重开 #437;在此之前不要把
test:workflows全绿当作 fork 路径可用的证据。 - 剩余风险(本次不可在仓库内核验,PR 已列为 follow-up,此处仅记录不作为阻塞):
ceilf6/repo-guard@main是否会调用工作区内的 package scripts。这决定了自托管 runner 上被检出的 fork 代码是「评审数据」还是「可执行代码」,而该 job 的 env 携带secrets.LLM_API_KEY。需要说明的是,这条风险并非本 PR 引入——它属于该工作流原本就成立的设计,本 PR 只是恢复了 action 行为变更前既有的 checkout 行为。
| with: | ||
| ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || format('refs/pull/{0}/head', github.event.issue.number) }} | ||
| persist-credentials: false | ||
| allow-unsafe-pr-checkout: true |
There was a problem hiding this comment.
开关无条件生效,但同一 step 服务的 issue_comment 路径按评论者而非 PR 作者门禁;改为 allow-unsafe-pr-checkout: ${{ github.event_name == 'pull_request_target' }},使范围与 :62-69 的信任论证一致。
| # 不开这一项则本 job 对所有 fork PR 必然 fail(#437)。 | ||
| # | ||
| # 两条 checkout 路径的信任边界并不相同,分别记录: | ||
| # - pull_request_target:门禁判 PR 作者(:32),信任边界是「本仓库分支 |
There was a problem hiding this comment.
硬编码行号 (:32)(以及 :65 的 (:46))今天准确,但任何在 if: 块上方的插入都会让它静默失准;改为引用条件表达式本身。
| # - issue_comment:门禁判的是**评论者**(:46),不是 PR 作者,所以任一有权 | ||
| # 评论的成员在任意 fork PR 下留言都会 checkout 该 fork 的 head。这条路径 | ||
| # 信任的是「成员在要求评审前已自行审阅过该 PR」这一行为,而非 PR 作者身份。 | ||
| # 注:assertSafePrCheckout 仅在事件为 pull_request_target / workflow_run 时 |
There was a problem hiding this comment.
assertSafePrCheckout 是 actions/checkout 的内部函数名,上游重命名后这条论证就无从核验;改为引用 :58 已写明的事件范围。
| const repoGuard = readFileSync('.github/workflows/repo-guard.yml', 'utf8'); | ||
| // Anchor to the checkout step's own block: a file-wide match would let an | ||
| // unrelated future checkout step satisfy these on the wrong step. | ||
| const checkoutStep = /- uses: actions\/checkout@[\s\S]*?(?=\n\s+- uses:)/u.exec(repoGuard)?.[0]; |
There was a problem hiding this comment.
lazy lookahead 只覆盖第一个 checkout step(与 :545-546 的注释声明不符),且当 checkout 是最后一个 - uses: 时会以误导性消息失败;终止条件改为 (?=\n\s{6}- |$)。
|
|
||
| assert.ok(checkoutStep, 'repo-guard has no actions/checkout step'); | ||
| assert.match(checkoutStep, /allow-unsafe-pr-checkout:\s*true/u); | ||
| assert.match(checkoutStep, /persist-credentials:\s*false/u); |
There was a problem hiding this comment.
紧接着补一条 assert.match(checkoutStep, /github\.event\.pull_request\.head\.sha/u);——ref 钉死在准入 SHA 上同样是 opt-in 成立的条件,目前无断言保护。
…d.sha Second round of repo-guard review on #438. Verdict was "可以合并" with three non-blocking findings; two are cheap and directly harden the security argument this PR documents, so they are applied before merge. Scope the opt-in by event: allow-unsafe-pr-checkout: ${{ github.event_name == 'pull_request_target' }} The checkout step's `if:` serves two paths with different trust boundaries — pull_request_target gates on the PR author, issue_comment gates on the commenter. The justification for the opt-in only covers the former, but a blanket `true` covered the whole step. Behavior is identical today because the action's guard only fires on pull_request_target / workflow_run, but if that guard ever widens, an unconditional flag would silently admit the broader path. Rendering yields the string "true"/"false" and the action parses it as `getInput(...).toUpperCase() === 'TRUE'`, so the false branch genuinely disables it. Also assert the `ref` stays pinned to `head.sha`. The file's safety argument depends on it; a branch ref or refs/pull/{n}/merge would open a TOCTOU gap between the commit the gate admitted and the content checked out, and nothing would have failed. Both new assertions were checked against deliberately weakened copies to confirm they fail when the property is removed. Remaining finding — pinning actions/checkout to a SHA — stays out of scope and gets its own tracking issue rather than living only in #437's body.
There was a problem hiding this comment.
代码评审报告: fix(ci): let Repo Guard check out fork PRs from allowlisted contributors (#437)
风险等级: 中
处理建议: 评论
决策摘要: ** 修复本身正确、最小且与 #437 的根因一致,无阻塞缺陷;合并前建议同步更新已过期的 PR 描述与验证记录,并确认「checkout 内容不被执行」这一前提——它是整个安全论证的承重点,但作者自述尚未验证。
级联分析
- 变更符号: 原始模型未提供结构化级联字段。
- 受影响流程: 原始模型未提供结构化级联字段。
- 变更集外调用方: unknown
- 置信度: degraded
问题发现
- [中] 安全论证的承重前提「checkout 内容只被读取、不被执行」未经确认
- 证据: PR 与 #437 均把「确认
ceilf6/repo-guard@main不调用 workspace 内的 package scripts」列为 out-of-scope follow-up。repo-guard.yml:52的 runner 来自vars.REPO_GUARD_RUNNER(自托管持久主机),:89将secrets.LLM_API_KEY注入同一 job。若该前提不成立,本开关等于在持久主机上恢复 fork 代码执行面。 - 受影响调用方/流程:
pull_request_target路径的 fork checkout;外部 action 运行时。 - 最小可行修复: 合并前口头/书面确认 repo-guard action 不在 checkout 目录执行任何脚本或工具链即可,无需改代码。需要说明的是:这一前提今天已经是承重的——
:77-79的issue_comment路径本就以refs/pull/{n}/head无条件 checkout 任意 fork 的 head,且不受 action 守卫约束。因此本 PR 并未新增该暴露面,阻塞它也不会降低风险,故不作为 blocking finding。
- 证据: PR 与 #437 均把「确认
- [中] PR 描述与 head 提交不一致,验证证据对应的是旧实现
- 证据: 描述称
allow-unsafe-pr-checkout: true、+8 lines, no logic change、测试+12 lines;实际 diff 是${{ github.event_name == 'pull_request_target' }},workflow +21、测试 +38。head 提交3c84b3f(scope the checkout opt-in…)之后正文未更新。同一提交信息里的 "pin head.sha" 在最终 diff 中也无对应改动——ref:行是上下文行,head.sha是既有实现,本 PR 只是为它补了断言。CHANGELOG.md:13是准确的,只有 PR 正文过期。 - 受影响调用方/流程: 维护者的合并判断。这是一次安全边界变更,正文描述的控制项与实际生效的控制项不同(实际更窄),且
quality:precommit的验证叙述对应的是改造前的版本。 - 最小可行修复: 更新 Summary / Impact Scope / Verification 三节以匹配当前 head,并注明验证是在
3c84b3f上重跑的。
- 证据: 描述称
- [中] 新测试对门禁的断言是全文子串匹配,弱于它自称的保证
- 证据:
workflow-rules.test.mjs:563-575的注释宣称「the opt-in cannot outlive the conditions that justify it」,但两条assert.match作用于整份文件,只验证两段文本各自存在。若:29的github.event_name == 'pull_request_target' &&包裹被删除,或该 OR 组被放宽(例如追加更宽的分支),断言仍全绿。:561的head.sha同理:只验证该字符串出现在 step 内,未验证它仍是三元表达式中pull_request_target那一侧的取值。 - 受影响调用方/流程: 该测试是本 PR 声明的回归保护,也是契约规则要求的关键骨架测试。
- 最小可行修复: 把两条断言合成一条跨行正则,匹配
:29-33的完整门禁块(event_name == 'pull_request_target' && ( full_name == github.repository || contains(...user.login) ));head.sha同样匹配整个三元表达式而非裸字符串。
- 证据:
- [低] 注释绑定了易腐化的外部事实与行号
- 证据:
repo-guard.yml:63、:65用(:32)/(:46)硬编码行号(今天准确,但本 PR 自身就是该文件被编辑的证明);:68与:75断言了actions/checkout的内部实现(assertSafePrCheckout的适用事件、toUpperCase() === 'TRUE'的解析方式),而:76仍固定在移动 tag@v4——正是 #437 这次静默行为变更的传播途径。功能上无风险:${{ }}渲染结果恒为true/false,任何布尔解析实现都接受。 - 受影响调用方/流程: 后续维护者对该注释的信任度。
- 最小可行修复: 行号改为按事件名指代门禁分支;删除对 action 内部实现的具体断言,只保留可观测行为。SHA 固定已在 #437 列为 follow-up,本 PR 不必处理;另注意仓内其余 workflow 均为
actions/checkout@v6(ci.yml、contract-guard.yml、release.yml、gitguardian.yml),若无自托管 runner 的兼容原因,可一并对齐。
- 证据:
- [低]
issue_comment才是更宽的信任边界,本 PR 把它写进了文档但未收敛- 证据:
repo-guard.yml:43-47的门禁判定的是评论者,:77-79随后 checkoutrefs/pull/{n}/head。任一 MEMBER/COLLABORATOR(或白名单中的两位具名外部人员)在任意第三方 fork PR 下留言,都会把该 fork 的 head 拉到自托管 runner 上。:65-69的新注释准确记录了这一点。 - 受影响调用方/流程:
issue_comment触发的评审路径。 - 最小可行修复: 本 PR 无需处理(属既有行为、#437 范围之外)。建议单开 issue 跟踪,选项包括将该路径的 checkout 也纳入 PR 作者白名单,或改为按 API 解析出的固定 head SHA 取值。
- 证据:
行级发现
- [scripts/tests/workflow-rules.test.mjs:568] 该断言作用于整份文件而非 guard job 的
if:块,无法察觉pull_request_target &&包裹被移除或门禁被放宽;改为用一条跨行正则匹配:29-33的完整条件组。 - [scripts/tests/workflow-rules.test.mjs:547] 步骤切分依赖「后面还有一个
- uses:」;若ceilf6/repo-guard步骤改写为- name:形式或调整顺序,测试会以 "repo-guard has no actions/checkout step" 失败,把维护者指向一个并未被删除的步骤。终止条件改用缩进回退(如(?=\n {6}- ))或直接解析 YAML。 - [.github/workflows/repo-guard.yml:63] (:32)
/(:46)` 这类硬编码行号会随文件编辑失效;改为按事件名指代对应门禁分支。 - [.github/workflows/repo-guard.yml:75] 这里断言了
actions/checkout的内部解析方式,而该依赖固定在移动 tag@v4;渲染值恒为规范布尔字面量,任何实现都接受,建议删掉对内部实现的具体描述,只保留可观测行为。
Karpathy 评审
- 假设: 模型输出需要归一化为固定 Markdown 契约。
- 简洁性: 已提取 summary、finding、evidence 与 fix;原始 prose 不再附在评论中,避免占用下游解析与代理上下文。
- 变更范围: 原始模型未提供结构化范围字段。
- 验证: 需要查看 CI、测试或人工 CR 证据补强合并信心。
缺失覆盖
- 输出未命中 Repo Guard Markdown 契约;建议补充真实模型质量评估覆盖。
| // allowlist exists somewhere": the same names also appear in the issues and | ||
| // issue_comment gates, so a file-wide match stays green even if this one — | ||
| // the only gate the opt-in actually depends on — is deleted. | ||
| assert.match( |
There was a problem hiding this comment.
该断言作用于整份文件而非 guard job 的 if: 块,无法察觉 pull_request_target && 包裹被移除或门禁被放宽;改为用一条跨行正则匹配 :29-33 的完整条件组。
| const repoGuard = readFileSync('.github/workflows/repo-guard.yml', 'utf8'); | ||
| // Anchor to the checkout step's own block: a file-wide match would let an | ||
| // unrelated future checkout step satisfy these on the wrong step. | ||
| const checkoutStep = /- uses: actions\/checkout@[\s\S]*?(?=\n\s+- uses:)/u.exec(repoGuard)?.[0]; |
There was a problem hiding this comment.
步骤切分依赖「后面还有一个 - uses:」;若 ceilf6/repo-guard 步骤改写为 - name: 形式或调整顺序,测试会以 "repo-guard has no actions/checkout step" 失败,把维护者指向一个并未被删除的步骤。终止条件改用缩进回退(如 (?=\n {6}- ))或直接解析 YAML。
| # 不开这一项则本 job 对所有 fork PR 必然 fail(#437)。 | ||
| # | ||
| # 两条 checkout 路径的信任边界并不相同,分别记录: | ||
| # - pull_request_target:门禁判 PR 作者(:32),信任边界是「本仓库分支 |
There was a problem hiding this comment.
(:32)/(:46)` 这类硬编码行号会随文件编辑失效;改为按事件名指代对应门禁分支。
| # 上面那段信任论证严格一致。今天两者行为相同(action 的守卫本就只认这两个 | ||
| # 事件),但若守卫日后扩展到更多事件,一个无条件置位的开关会把 issue_comment | ||
| # 这条按评论者门禁的路径也静默放行,而那条恰恰是边界更宽的一条。 | ||
| # 渲染结果是字符串 true/false,action 侧按 toUpperCase() === 'TRUE' 判定。 |
There was a problem hiding this comment.
这里断言了 actions/checkout 的内部解析方式,而该依赖固定在移动 tag @v4;渲染值恒为规范布尔字面量,任何实现都接受,建议删掉对内部实现的具体描述,只保留可观测行为。
Third round of repo-guard review on #438. Verdict was "无阻塞缺陷"; these are the cheap correctness items. The gate assertions were file-wide matches on individual operands, so dropping the `pull_request_target &&` wrapper, or widening the condition group, left the suite green — the same overstated-guarantee problem as the previous round, one level deeper. The gate is now matched as one cross-line regex covering the whole condition group, and `head.sha` is matched as the pull_request_target branch of the ternary rather than as a bare substring that a merge ref would still satisfy. Step extraction now terminates on the next step's indentation instead of on a following `- uses:`, so rewriting the sibling step to `- name:` form no longer fails with a misleading "no checkout step" message. Comments no longer hardcode line numbers into a file this PR is itself editing, and no longer assert actions/checkout's internal parsing while pinned to a moving tag — only observable behavior is stated. The issue_comment trust boundary now points at #441 for follow-up. All four assertions were checked against deliberately weakened copies to confirm they fail when the property is removed.
There was a problem hiding this comment.
代码评审报告: fix(ci): let Repo Guard check out fork PRs from allowlisted contributors (#437)
风险等级: 中
处理建议: 评论
决策摘要: 修复方向正确且是最小 diff——allow-unsafe-pr-checkout 是该 action 唯一认的开关,按事件收窄到 pull_request_target 与 job 级门禁的信任边界一致,测试正则经静态核对确实能匹配当前 YAML;可以合并,但合并前请确认工作流注释里引用的 #441 确实存在,并把「repo-guard@main 不执行 workspace 代码」这一承重前提列为可追踪项。
级联分析
- 变更符号:
.github/workflows/repo-guard.yml中 checkout step 的with:输入(新增allow-unsafe-pr-checkout),无代码符号变更。 - 受影响流程: Repo Guard job 的两条 checkout 路径——
pull_request_target(本仓库分支 / 具名白名单作者)与issue_comment(按评论者门禁)。同仓库分支路径行为不变(非 fork,守卫本就不触发,置位为 no-op)。 - 变更集外调用方: 仓库内无其他引用(text search:
allow-unsafe-pr-checkout仅出现在 repo-guard.yml:80 与新测试;其余工作流各自独立 checkout)。唯一的外部消费者是ceilf6/repo-guard@main,它消费 checkout 出来的 workspace,本仓库无法验证其行为。 - 置信度: medium(无代码图谱可用——diff 全是 CI YAML/测试/CHANGELOG,无索引符号;仓库侧断言由文件读取与文本搜索直接核对,外部 action 与
actions/checkout内部守卫范围无法在此 checkout 内验证)
问题发现
-
[中] 安全论证的承重前提未验证,且被推迟为 follow-up
- 证据: repo-guard.yml:50-52 说明
REPO_GUARD_RUNNER是自托管 runner、REPO_GUARD_ENGINE=claude-code在本机跑 CLI,job env 携带secrets.LLM_API_KEY(:88)。PR/Issue 的论证核心是「checkout 只是评审数据,不被执行」,但 issue #437 自己把「确认 repo-guard@main 从不调用 workspace 里的 package script」列为 out of scope。 - 受影响调用方/流程: fork PR → 自托管 runner 工作目录 → repo-guard action 运行时。若该 action 在 workspace 内执行任何安装/脚本/工具链,fork 内容就从「评审数据」变成「可执行代码」,并处于持有密钥的持久化主机上。
- 缓解事实(降低严重度,不消除):
issue_comment分支今天已按评论者门禁 checkout 任意 fork 的 head(:76-78),因此「任意 fork 内容到达 runner」并非本 PR 首次引入;本 PR 的增量是pull_request_target路径,且被具名白名单收窄到两个人 + 仓库成员。 - 最小可行修复: 不需要改本 diff。合并后在下一次 fork PR 运行前,确认 repo-guard action 的运行时不
install/不执行 workspace 脚本,并把结论回填到 #437;若无法确认,先只保留issue_comment触发路径。
- 证据: repo-guard.yml:50-52 说明
-
[低] 工作流注释引用的
#441在仓库内无任何佐证 -
[低] repo-guard.yml 仍用移动标签
actions/checkout@v4,与仓库其余工作流不一致- 证据: ci.yml:16,33、contract-guard.yml:20、gitguardian.yml:22、release.yml:35 均为
@v6;repo-guard.yml:75 为@v4。本次故障正是移动标签静默改行为造成的,而 repo-guard 是信任姿态最敏感的工作流。 - 受影响调用方/流程: 下一次
actions/checkout行为变更会再次在 fork PR 上静默炸掉同一个 job。 - 最小可行修复: 属 #437 已记录的 follow-up,不必在本 PR 处理;单独 PR 里 pin 到 SHA(并顺带对齐版本)。
- 证据: ci.yml:16,33、contract-guard.yml:20、gitguardian.yml:22、release.yml:35 均为
行级发现
- [.github/workflows/repo-guard.yml:80] 按事件收窄的写法依赖「checkout 的守卫只按 event 生效」这一对第三方 action 内部实现的假设;若守卫实际按「head 是 fork」判定,
issue_comment路径会拿到false而继续失败。失败是 fail-loud(job 在 checkout 步骤报错,与 #437 现象相同),可接受,但建议在注释里点明这一检测方式,而不是断言「两种写法行为相同」。 - [.github/workflows/repo-guard.yml:68] 注释引用的
#441在仓库内无其他佐证,PR 描述则把 follow-up 归到 #437;确认编号或改为 #437。
Karpathy 评审
- 假设: 三条承重假设——(a)
allow-unsafe-pr-checkout是@v4上的真实输入(由 #437 引用的失败日志本身给出,日志出自该 v4 运行并直接点名该输入,证据充分);(b) 表达式渲染出的true/false能被该输入的布尔解析接受(未知输入只会 warning,错了也是 fail-loud);(c) repo-guard@main 不执行 workspace 代码(未验证,见发现 1)。 - 简洁性: 单行输入,无新抽象、无新层级、无配置化,方案规模与问题匹配。19 行注释对 1 行改动偏重,且同一段论证在 workflow 注释、CHANGELOG:13 和 #437 三处各存一份,存在漂移风险;但记录信任决策是 issue 的显式要求,不作为 finding。
- 结构质量: 无退化。未新增分支或 flag(
allow-unsafe-pr-checkout的表达式复用了同 step 已有的 event 判别),未触碰共享路径,测试文件 673 行,未接近 1000 行边界。 - 变更范围: 严格贴合 #437 的 Proposed fix,没有顺手重构、没有格式噪声、没有把 issue 里的两条 follow-up(pin SHA、确认 action 运行时)混进来。
- 验证: 新测试的断言经静态核对确实成立——步骤块正则
(?=\n {6}- )在 repo-guard.yml:81 处终止,捕获范围恰为 :75-80,三条 step 内断言分别命中 :80/:79/:78;全文门禁正则与 :29-33 的括号结构和空白逐字符匹配,fromJSON('["NanluQingshi","HaveNiceDa"]')内不含),[^)]*成立。我未执行测试(本会话无 shell),以上为静态核对结论;PR 报告test:workflows38/38 通过。断言把 opt-in 与它的两个前置条件(白名单门禁、persist-credentials: false)绑定在一起,方向正确。
缺失覆盖
- fork 路径无法在本 PR 内端到端验证(作者已声明)。合并后建议在 #393 上用
issue_comment触发一次,再等一次真实 forkpull_request_target,两条路径都确认后再关闭 #437。 - 没有断言覆盖
issue_comment路径的 ref 形态::78 的 fallback 是refs/pull/{n}/head(可移动 ref,存在门禁时刻与 checkout 内容不一致的 TOCTOU 窗口)。这是既有语义、不由本 PR 引入,但既然测试已经为pull_request_target分支锁定了 head SHA,值得在 #441/#437 中一并记录该分支为何不需要同等锁定。 - 无测试防止
actions/checkout版本回退或跨工作流版本漂移;若采纳发现 3,可在同一测试文件里加一条版本/SHA 断言。
| with: | ||
| ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || format('refs/pull/{0}/head', github.event.issue.number) }} | ||
| persist-credentials: false | ||
| allow-unsafe-pr-checkout: ${{ github.event_name == 'pull_request_target' }} |
There was a problem hiding this comment.
按事件收窄的写法依赖「checkout 的守卫只按 event 生效」这一对第三方 action 内部实现的假设;若守卫实际按「head 是 fork」判定,issue_comment 路径会拿到 false 而继续失败。失败是 fail-loud(job 在 checkout 步骤报错,与 #437 现象相同),可接受,但建议在注释里点明这一检测方式,而不是断言「两种写法行为相同」。
| # - issue_comment 分支:job 级门禁判的是**评论者**,不是 PR 作者,所以任一 | ||
| # 有权评论的成员在任意 fork PR 下留言,都会 checkout 该 fork 的 head。 | ||
| # 这条路径信任的是「成员在要求评审前已自行审阅过该 PR」这一行为, | ||
| # 而非 PR 作者身份;该语义是既有的,不由本开关引入(#441)。 |
#437's mechanism was a moving tag: `actions/checkout@v4` silently began refusing fork checkouts under `pull_request_target`, and Repo Guard failed on every fork PR with no change on our side. The regression test written for it asserts workflow *text*, and Actions only warns on unknown `with:` keys rather than failing — so if the tag moves again and `allow-unsafe-pr-checkout` is renamed or dropped, checkout breaks while `test:workflows` stays green. That is the same mechanism that hid #437 in the first place. Every third-party action now carries a SHA plus a trailing version comment, and a contract test requires both halves — the SHA so behavior is frozen, the comment so a future upgrade can tell how far it is jumping. Verified that reverting either half fails the test. Pinned to each action's *current* major, not the latest release: `checkout` is at v7.0.1 upstream and this stays on v6.1.0. A pin commit that also bumps majors is a behavior change wearing a chore's clothes. The two `actions/checkout@v4` usages (repo-guard, update-npm-downloads) move to the same v6 SHA as everything else. Confirmed `allow-unsafe-pr-checkout` is present in action.yml at both the old v4.4.0 and the new v6.1.0 SHAs, so the input #438 depends on survives the jump. `ceilf6/repo-guard@main` stays on a moving ref and is allowlisted explicitly. It is first-party, and pinning it would mean a bump PR here for every repo-guard change; it is also the step that runs on the self-hosted runner with `secrets.LLM_API_KEY`, so the trade is real. That is a maintainer policy call — recorded next to the allowlist, not decided here. Closes #440 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Linked Issue Or Context
Closes #437
Surfaced on #393: Repo Guard failed there and would fail on every fork PR, so allowlisted external contributors get no review at all.
Follow-ups split out of review on this PR: #440 (pin
actions/checkoutto a SHA), #441 (theissue_commentpath's wider trust boundary).Summary
actions/checkoutadded anallow-unsafe-pr-checkoutinput (default: false) that refuses to check out fork PR code frompull_request_target/workflow_runworkflows. It does not read the workflow's own actor allowlist, so theif:gate cannot satisfy it — the job is admitted and then dies at the checkout step.Step results from the #393 run prove the allowlist was working and the failure is downstream of it:
Every recent green Repo Guard run (
fix/search-code-invalid-regex,fix/filesense-ground-plan-paths,fix/workspace-endpoint-trust-421) was on a same-repo branch, taking thehead.repo.full_name == github.repositorypath — no fork, no refusal. #393 was the first fork PR to run after the action's behavior change, which is why this stayed hidden.The opt-in is set scoped by event, not blanket-true:
Impact Scope
3 files, +62 lines, no deletions. No application code touched. Same-repo branch behavior is unchanged.
.github/workflows/repo-guard.yml(+20) — the scoped opt-in on the checkout step, plus a comment recording the trust decision for each of the step's two pathsscripts/tests/workflow-rules.test.mjs(+41) — regression test pinning the four properties the opt-in depends onCHANGELOG.md(+1) —[Unreleased]→### ChangedWhy the opt-in is scoped rather than blanket-true
The checkout step's
if:serves two paths with different trust boundaries:pull_request_targetissue_commentThe justification for the opt-in only covers the first. A blanket
truewould cover the whole step. Behavior is identical today —assertSafePrCheckoutonly fires onpull_request_target/workflow_run— but if that guard ever widens, an unconditional flag would silently admit the broader path. Scoping by event keeps the flag's reach equal to its argument. The expression renders totrue/falseand the action parses it as a boolean.The
issue_commentboundary is pre-existing, not introduced here; it is now documented in the workflow and tracked in #441.Security rationale
REPO_GUARD_RUNNER = repo-guard-claudeis a self-hosted runner (persistent host, not a disposable container),REPO_GUARD_ENGINE = claude-coderuns the Claude Code CLI there, andsecrets.LLM_API_KEYis in the job env — the scenario GitHub's guard targets. What makes the opt-in acceptable:pull_request_targetgate restricts triggering to repo members plus two named individuals — the trust boundary is those specific people.persist-credentials: falsekeeps the token off disk.ceilf6/repo-guard@main'saction.yml: its composite steps only runnode "${{ github.action_path }}/scripts/review.mjs"and clone skills intogithub.action_path— nothing resolves to the workspace, which is passed as an env var and documented as explored "with read-only tools". This was listed as an unverified assumption in earlier revisions of this description; it is now verified.Residual risk, unchanged by this PR: the reviewing agent reads fork-authored files, so a PR could attempt prompt injection against the reviewer. That is a different risk from code execution, and it already applies via the
issue_commentpath today.Regression test
The test pins the four properties the opt-in actually depends on, each anchored so removal fails it:
pull_request_target, not blanket-truepersist-credentials: false— in the checkout step's own blockrefresolves tohead.shaon thepull_request_targetbranch of the ternary (a merge ref there would open a TOCTOU gap between the commit the gate admitted and the content checked out)pull_request_targetgate block matches as a whole — wrapper plus condition group — since the allowlist names also appear in theissuesandissue_commentgatesEvery assertion was checked against a deliberately weakened copy to confirm it fails when the property is removed. Earlier revisions of this test claimed this guarantee without delivering it — deleting the PR-author allowlist left it green — which the review caught twice; these are the corrected versions.
Verification
Re-run on
433b81e:pnpm quality:precommit→ passed end-to-endbiome check .→ 417 files, exit 0 (1 pre-existing fixablestyle/useTemplateinfo inpackages/hallucination-guard, untouched here)turbo typecheck→ 25/25 tasks successfulturbo test→ 25/25 tasks successfulnode --test scripts/tests/*.test.mjs→ 38 tests, 38 pass, 0 failallow-unsafe-pr-checkoutis a real input onactions/checkout(action.yml,default: false) and that it is parsed asgetInput(...).toUpperCase() === 'TRUE', so thefalsebranch genuinely disables it.unsafe-pr-checkout-helper.tsthat the guard returns early unless the event ispull_request_target/workflow_run.Not verified here: the fork path itself. This PR is a same-repo branch, so its green
guardcheck exercises the non-fork path only. The fix is confirmed by the next fork PR; #437 should stay open until then.Checklist
pnpm quality:precommit, or explained why it could not run.pnpm quality:localfor critical skeleton changes, or explained why it could not run. (quality:precommitran in full; the extra step inquality:localiscontract:local+build:verify, and this diff contains no buildable source change.)GitNexus Impact Summary
.github/workflows/repo-guard.ymlclassifies asrepo-harnessunderclassifyContractPaths. The change adds one action input (scoped by an event expression) plus comments; no job graph, trigger set, permissions, or gating condition is altered.detect_changes(scope compare vsorigin/develop, index fresh at the branch head) reports no changed symbols and no affected execution flows — the diff is CI YAML, test assertions, and a changelog line, none of which are indexed code symbols, so a per-symbolimpactrun has no target.queryfor the repo-guard/contract area returnsRunGitNexusContract → GitText(proc_200) as the nearest indexed flow; it coversscripts/workflows/contract-check.mjs, which this PR does not modify — onlyscripts/tests/workflow-rules.test.mjsgains assertions. Blast radius is confined to CI configuration.pnpm quality:precommitpassed in full on433b81e—biome checkclean,turbo typecheck25/25,turbo test25/25,test:workflows38/38.