Skip to content

Commit 8ca9dd7

Browse files
feat(pm/fleet-write): advisory_create, a relay row that files a security finding as a draft repository advisory (#29436)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ86v8RKUswByetRaM96ZPSt9)_ Fixes #29410 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:中继新增第四个公告操作 `advisory_create`,以 App 身份在仓上直接建一份 draft 安全公告;安全发现规则改为首选它,`advisory_report` 私报只留给 App 无公告写权的仓。 - **为什么改**:GitHub 按账号限每日新私报,全队共用一个 App 账号,今天第一份私报落地后其余八笔都被 403 拒;建草稿公告是仓自己的操作,按文档不受这个限额。 - **风险与回滚**:该操作单独成笔,令牌只在这一笔带 `repository-advisories: write`;只开放 `POST …/security-advisories` 这一条路径(列表 GET 不可达),`published` 仍无任何操作可达;公告正文不进任何日志、结果行、步骤摘要或注解。回滚 = revert 本 PR(6 个文件,无数据变更)。 - **先看**:`scripts/pm/fleet-write/ops.mjs` 的 `advisory_create` 行与 `REFUSED_PATH_FAMILIES` 的 security-advisories 一行;`execute.mjs` 的 `confirmAdvisory`;`.claude/skills/pm-dispatch/references/security-findings.md` 第 3 节。 - **两分钟验证**:`pnpm check:pm-fleet-write-validate && pnpm check:pm-fleet-write-execute`,应看到两行 ✓,分别含 "four advisory rows"。 - **需要你做的**:Tier H(`.claude/**`):契约复核 PASS 之后,以 GOVERNED_APPROVERS 账号批准一次;合并后席位做首跑实测(见 Acceptance notes)。 ## What changes - **`scripts/pm/fleet-write/ops.mjs`.** A new row, `advisory_create`, shaped like `advisory_report`: - One `POST /repos/{owner}/{repo}/security-advisories`, which creates an advisory in `draft`. It travels `alone` and spends `repository-advisories: write`, the grant minted for its stroke only (`STROKE_SCOPED_PERMISSIONS`, unchanged: the workflow's mint step carries the row as is). - Closed keys: required `summary`, `description` and `packages`; optional `severity` and `cwe_ids`. `packages` is a new quiet list field: 1 to 10 npm package names in `NPM_PACKAGE_SHAPE`, at most 214 characters each. Each name becomes one `vulnerabilities` entry carrying the ecosystem `npm` and the name, and nothing else: no version range, patched versions or vulnerable functions. - The body names no state (the endpoint creates in `draft` and takes none), and the table spells no `credits`, `cvss_vector_string`, `cve_id` or `start_private_fork`. A payload carrying any of them, or its own `vulnerabilities`, is refused by name. - It LANDS on a 2xx whose answer carries a `ghsa_id` and reads state `draft` (`advisoryCreateReadBack`). A 2xx with no id is UNCONFIRMED and a 2xx in another state FAILED; neither is ever re-sent. The landed row (`advisoryCreateLandedText`) prints the id and the state alone. - `REFUSED_PATH_FAMILIES` opens the bare collection path (`ADVISORY_CREATE_PATH_SHAPE`) beside one advisory's path and the reports endpoint. That path also answers the advisory list to a `GET`. No row sends that verb there, and the validator pins that the only request on it is this row's POST. A trailing slash, a query string or any other tail stays refused. - The header section is now "The four rows that touch a security advisory": the table, the ruling, and what the first live run decides for this row. - **`scripts/pm/fleet-write/execute.mjs`.** `confirmAdvisory` judges the new row on its own answer; the landing reaches the row, the step summary and one notice annotation through the existing advisory paths. New cases in the advisory battery (22 → 27): the one POST and its exact body, the row and the annotation, UNCONFIRMED and wrong-state answers, a 403 and a 422, four more payloads refused before any request, and the marker sweep, now over 41 runs. - **`scripts/pm/fleet-write/dispatch.mjs`.** `ADVISORY_ANNOTATED_OPS` gains `advisory_create`, so the seat reads the draft's id from the run's annotation and hands it on (`--json`: `advisories`) for the `advisory_collaborators_add` stroke. The no-locator note above `READ_BACK_LOCATORS` names this row too. Seat battery 12 → 14 cases. This file was not on the claim's file surface; the id cannot reach the seat without it (see Deviations). - **`scripts/pm/fleet-write/validate.mjs`.** Header and self-test: the row, the `packages` field, the exact request and body, accepted payloads, quiet refusals that never echo the marker, by-name refusals, the stroke fence, the scoped grant, the path families, the read-back and the landed text. Advisory battery 44 → 51 cases. - **`.claude/skills/pm-dispatch/references/security-findings.md` section 3.** The seat files a finding as a draft advisory through `advisory_create` first, then names its account with `advisory_collaborators_add`. `advisory_report` stays for a repository where the App lacks the advisory grant, and the per-account daily cap on new private reports is recorded. "⛔ 席位永不以自身账号私报或写公告" stays, now ending "发布归维护者". Section 4's cross-repo line follows. 55 → 55 lines. - **`.claude/skills/pm-dispatch/references/rest-channel.md`.** The advisory line now names `advisory_create` as preferred and points at `security-findings.md`, which already says it is the one home ("他处只指向本文"). 81 → 81 lines. The line's ✓ is dropped, because the row as a whole now includes an op no seat has called yet. The seat's live run puts it back. Line budget: both governed files net zero, every changed line at or under 120 bytes, no ratchet row moved. ## Why GitHub caps the new private vulnerability reports one account files per day (changelog 2026-10-01), and every relay report is filed as the one fleet App. So the whole fleet gets about one report a day: on 2026-11-02 the first `advisory_report` landed and the next eight were refused 403 (#29402). A draft advisory is the repository's own act under the App's existing advisory grant. By GitHub's REST documentation it is not a report and not under that cap, and it lands in `draft` directly, the state a report reaches only once accepted. ## Tests - `pnpm check:pm-fleet-write-validate`: 275 cases pass across 15 batteries; advisory battery 51, pinned at 51 (a pin of 52 reds: "registered 51 case(s), below its pinned floor of 52"). - `pnpm check:pm-fleet-write-execute`: 237 cases pass across 21 batteries; advisory battery 27, pinned at 27. - `pnpm check:pm-fleet-write-dispatch`: 298 cases pass across 26 batteries; advisory seat battery 14, pinned at 14. - `node scripts/pm/fleet-write/ops.mjs --self-test`: exit 0. - Ablation, one time, on the draft's landed row (committed first, restored with blob == HEAD): when the landed row also prints the answer's `summary`, the execute self-test reds on 2 of 237 cases, the row case and THE MARKER TEST. Restored, the self-test exits 0. <details><summary>Local gate subset at f68850f: 46 derived, 46 run, all exit 0</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 46 commands. Each was run after `turbo run build --filter=@objectstack/formula --filter=@objectstack/lint` under the verify lock (exit 0). `--ran` reconciliation: "46 derived famil(ies) accounted for — 46 run, 0 NOT-MEASURED". Every family CI runs is read from this PR's check-runs, not re-run locally. - `node scripts/check-closing-keyword-parity.mjs` :: exit 0 - `node scripts/check-closing-keyword-parity.mjs --self-test` :: exit 0 - `node scripts/check-comment-mask-corpus.mjs` :: exit 0 - `node scripts/check-declaration-mirrors.mjs` :: exit 0 - `node scripts/check-declaration-mirrors.mjs --self-test` :: exit 0 - `node scripts/check-self-test-wired.mjs` :: exit 0 - `node scripts/check-self-test-wired.mjs --self-test` :: exit 0 - `node scripts/check-self-test-workflow-commands.mjs` :: exit 0 - `node scripts/check-self-test-workflow-commands.mjs --self-test` :: exit 0 - `node scripts/check-whole-set-label-write.mjs` :: exit 0 - `node scripts/check-whole-set-label-write.mjs --self-test` :: exit 0 - `node scripts/ci/plan-test-core-shards.mjs --self-test` :: exit 0 - `node scripts/pm/bare-root-worklist.mjs --self-test` :: exit 0 - `node scripts/pm/check-harness-current.mjs --self-test` :: exit 0 - `pnpm --filter @objectstack/lint run check:doc-formula-expressions` :: exit 0 - `pnpm check:agent-test-spelling` :: exit 0 - `pnpm check:bash32-floor` :: exit 0 - `pnpm check:ci-filter-parity` :: exit 0 - `pnpm check:cli-command-ids` :: exit 0 - `pnpm check:cross-package-test-inputs` :: exit 0 - `pnpm check:doc-authoring` :: exit 0 - `pnpm check:driver-memory-census` :: exit 0 - `pnpm check:entry-guard` :: exit 0 - `pnpm check:gitlink-declared` :: exit 0 - `pnpm check:nul-bytes` :: exit 0 - `pnpm check:parse-guard` :: exit 0 - `pnpm check:pm-close-cards` :: exit 0 - `pnpm check:pm-dispatch-gates` :: exit 0 - `pnpm check:pm-fleet-write-dispatch` :: exit 0 - `pnpm check:pm-fleet-write-execute` :: exit 0 - `pnpm check:pm-fleet-write-validate` :: exit 0 - `pnpm check:pm-governed-merges` :: exit 0 - `pnpm check:pm-issue-create` :: exit 0 - `pnpm check:pm-issue-transfer` :: exit 0 - `pnpm check:pm-label-write` :: exit 0 - `pnpm check:pm-post-stamped` :: exit 0 - `pnpm check:pm-record-recognisers` :: exit 0 - `pnpm check:pm-skill-id-lint` :: exit 0 - `pnpm check:pm-skill-ratchet` :: exit 0 - `pnpm check:pm-write-pace` :: exit 0 - `pnpm check:pnpm-filter-targets` :: exit 0 - `pnpm check:pnpm-flag-spellings` :: exit 0 - `pnpm check:refd-timer-probe` :: exit 0 - `pnpm check:scripts-symbol-anchors` :: exit 0 - `pnpm check:skill-frame-sync` :: exit 0 - `pnpm check:watch-hint-literal` :: exit 0 </details> ## Acceptance notes - **Live acceptance runs after merge**, because the relay workflow runs from `main`; the seat runs it and reports on the card, as #29222's rows were. These are the readings no test here can take: - The App's installation token creates a draft at all (H1; GitHub's docs list the "Repository security advisories" write permission for this endpoint). - One `vulnerabilities` entry of an ecosystem and a name is the least the endpoint accepts (H3; the docs mark `vulnerabilities` required and the name nullable; no version range is sent). - Three drafts land in one hour, the measurement that the per-account report cap does not reach this endpoint (H2). - `advisory_collaborators_add` lands on such a draft (its fence already admits `draft`). - If the first live run is refused, the seat stops and puts the platform's status and listed message to the maintainer. It never sends the payload anywhere else, and never re-sends under a wider token (`ADVISORY_ACCESS_REMEDY`). - **Not changed, noted:** the comment above `permission-repository-advisories` in `.github/workflows/fleet-write.yml` still says "THREE consumers". The mint step needed no change, and the claim keeps `.github/workflows/**` out of this diff. The comment is one row short until a workflow-touching PR corrects it. - The `security-findings.md` line on private repositories ("私有仓无私密漏洞报告功能,那些操作上线后仍会话专递") is unchanged. Whether a private repository takes a draft advisory is not measured here. ## Verification - Card premise, read at `origin/main` 7f277a8: `REFUSED_PATH_FAMILIES` refused the bare collection, and `security-findings.md` section 3 said the relay had no draft-create op. Both hold, and this PR changes both. - No advisory text reaches any public surface added here. The marker sweep covers the draft's runs: created, unconfirmed, wrong-state, refused and refused-before-sending. ## Deviations - `scripts/pm/fleet-write/dispatch.mjs` is edited although the claim's file surface did not name it. The executor's advisory annotation is read through `ADVISORY_ANNOTATED_OPS` there, and without the entry a created draft's id could not reach the seat. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (not touched) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes (CI-owned). - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. Nothing publishes (`.claude/**` and `scripts/pm/**` only): `skip-changeset`; both declaration lines are above. - [x] 6. `packages/spec` not touched. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English (the maintainer's rulings and the governed skill text quoted verbatim). - [x] The *Before Submitting* list in CONTRIBUTING.md is done. --- _Generated by [Claude Code](https://claude.ai/code/session_01JQwxbZ5rVEB642GP9VD5Xj)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent aca58cd commit 8ca9dd7

6 files changed

Lines changed: 284 additions & 117 deletions

File tree

‎.claude/skills/pm-dispatch/references/rest-channel.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
- ✓ 请审 `POST .../pulls/{n}/requested_reviewers`(Tier H 归中继 op)· 开 PR `POST .../pulls` 带 `draft=true`。
4848
- ✓ `origin/main` 合进 PR head:中继 op `pr_update_branch`(`pull` + 40 位 `expected_head_sha`),真合并提交。
4949
- 挂子卡走中继 op `sub_issue_add`/`sub_issue_remove`(`issue` 父号,`sub_issue` 子号),回读父列表。
50-
- ✓ 安全通报经中继 `advisory_report`·`advisory_collaborators_add`·`advisory_state`;发布归维护者。
50+
- 公告经中继 `advisory_create`(首选)等四操作,规则住 `security-findings.md`;发布归维护者。
5151
- ✓ 落地经中继 `node scripts/pm/fleet-write/dispatch.mjs --repo O/N --actions-file F`;`--via dispatch` 同载。
5252
- ✓ 启动 run 走 op `workflow_dispatch`:解锁物是白名单 run 才启;入表只经 workflow 自己的 PR。
5353
- F = `[{"op":"workflow_dispatch","workflow":"release.yml","ref":"main","inputs":{"refresh_version_pr":true}}]`。

‎.claude/skills/pm-dispatch/references/security-findings.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,18 @@
1818
## 3. 私卡是仓库安全公告(选 B)
1919

2020
- 私卡 = 受影响仓上一份未发布的安全公告,只有管理员、安全经理与受邀协作者可见。
21-
- 建草稿公告须管理员或安全经理,中继无此操作 ⇒ 草稿由维护者建,⛔ 席位不建。
22-
- ⛔ 席位永不以自身账号私报或写公告:GitHub 写只走中继,以 App 身份。
23-
- 中继私报操作上线前一律会话专递(选 A):机制只在会话里交维护者,由其私报或建草稿。
24-
- 中继私报与加协作者操作上线后,席位经它私报、带具名账号进公告;未上线 ⛔ 不假设有。
21+
- 首选中继 `advisory_create` 建草稿公告,再 `advisory_collaborators_add` 带具名账号进公告。
22+
- `advisory_report` 只留给 App 无公告写权的仓:GitHub 按账号限每日新私报,全队共一个 App。
23+
- ⛔ 席位永不以自身账号私报或写公告:GitHub 写只走中继,以 App 身份;发布归维护者。
24+
- 两操作皆被拒 ⇒ 会话专递(选 A):机制只在会话里交维护者,由其建草稿,⛔ 不换账号重发。
2525
- 私有仓无私密漏洞报告功能,那些操作上线后仍会话专递。
2626
- 回退时座位贴只写不透明编号与状态,⛔ 无细节。
2727
- 修复经 pm-dispatch 以无卡派发令派 `os-dev`,`xhigh`;机制只进派发令与公告。
2828
- 用到公告的临时私有 fork ⇒ 修复从该 fork 落地。
2929

3030
## 4. 跨仓发现者(应用仓量框架、cloud 席位)
3131

32-
- 中继私报操作上线后在框架仓经它私报(第 3 节);此前 ⇒ 机制在会话里交维护者。
32+
- 在框架仓经中继建草稿公告或私报(第 3 节);中继被拒 ⇒ 机制在会话里交维护者。
3333
- 公开面至多一行「a security item is with the maintainer」,⛔ 无细节。
3434

3535
## 5. os-dev 报告

0 commit comments

Comments
 (0)