Repository navigation
Commit 8ca9dd7
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
- scripts/pm/fleet-write
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
50 | | - | |
| 50 | + | |
51 | 51 | | |
52 | 52 | | |
53 | 53 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
21 | | - | |
22 | | - | |
23 | | - | |
24 | | - | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
32 | | - | |
| 32 | + | |
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
| |||
0 commit comments