Repository navigation
feat(pm): the fleet-write relay publishes an approved security advisory — reopen, set the versions, publish - #30135
Conversation
… (validator side) Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
…back and annotated Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
…through the relay Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
…lay-advisory-publish
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj Contract-review-claim: session_01QSeKYn5CmmdShCJifm6WYj · head 5aa0bdd · 2026-11-04T15:33Z |
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj Contract reviewServed-tier: Inputs read: card #30110 (body with the maintainer's ruling, and its one comment, the os-dev report); PR #30135 body, file list (7 files, +714/−190) and the net diff against the merge-base with ① Derived judgmentsEvery relay accept-set change, checked against the card's acceptance list, each judged on the tree:
② Semver level
③ Boundary flags
④ Design judgmentFor each governed file, does the rule text match what the relay now enforces, and the maintainer's ruling?
Attempts to break it, judged on the tree:
Findings. Must-fix: none. Should-fix (out of this PR's reach, filed as follow-ups): the Implemented-by: VERDICT: PASS |
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj 席位核对ACCEPT · head
|
Fixes #30110
维护者速读
advisory_state新增「已关闭→草稿」与「草稿→已发布」两种迁移,新增只写版本范围的advisory_versions一行,发布后回读校验并在运行注释里点名公告编号。scripts/pm/fleet-write/ops.mjs的ADVISORY_TRANSITIONS与ADVISORY_FENCES新增的versions围栏;execute.mjs的advisoryPreflight/confirmAdvisory;security-findings.md删去的一行(见 Acceptance notes)。node scripts/pm/fleet-write/validate.mjs --self-test && node scripts/pm/fleet-write/execute.mjs --self-test,预期分别打印 329 与 269 个用例全过。What changes
The relay could not publish a security advisory:
publishedwas refused by name andADVISORY_TRANSITIONSallowed onlytriage → draftandtriage|draft → closed. The maintainer ruled that the relay must publish (the ruling and the superseded clause are quoted verbatim in theops.mjsheader). What is published stays the maintainer's list, approved before each release.advisory_stategains two transitions.closed → draft(the reopen; a fixed advisory is closed once its fix merges) anddraft → published. Nothing leavespublishedorwithdrawn. Each move is still judged on the executor's own GET. A target the table does not name now has no from-states and fails closed at the state fence (before this PR, an unknown target would have fallen back to the collaborators' open states; the validator's enum already kept it out, and now both layers refuse it).ADVISORY_REFUSED_STATESlosespublishedand is empty; the field's named-refusal hook stays wired.advisory_versions. It carriesghsa_idandversions, a capped list of closed entries: exactlypackage(an npm name),vulnerable_version_rangeandpatched_versions. No prose, no summary or description, no vulnerable functions, no state. The range takes one comparator, or a lower bound then an upper bound, each an operator, one space and a release version (GitHub's own example spelling). The patched list holds one to five release versions. A pair whose bounds do not rise, a version named twice, and a patched version inside its own range are refused. The PATCH body is{ vulnerabilities }alone. It travels alone like every advisory row, and its token carriesrepository-advisories: writefor that stroke only.draftwhose every listed package the action names. A listed entry of another ecosystem, a nameless one, or one carryingvulnerable_functions(which the row does not write and the PATCH would drop) is refused with nothing sent. A publish is refused unless the advisory's GET lists at least one entry and every entry is in the row's spelling: the versions are set first.publishedAND lists exactly the entries the preflight GET read (order ignored), so the advisory is read back as published with the versions it was sent. A landed publish alone emits a notice annotation naming the id (public once published). The seat-side reader (dispatch.mjs) parses it, hands it on asadvisories, and says to record the run on the seat post. Every other state move stays unannotated..claude/skills/pm-dispatch/references/).security-findings.md§3 now says that the seat publishes only the list the maintainer approved for that release, through the relay, versions first, recording the run ids on the seat post with no text. §9 says that the publish list stays the maintainer's.lanes/security.mdpoints to §3 instead of "publish by hand in the UI", per §8's one-place rule.rest-channel.mdno longer says that publishing is the maintainer's by hand. Every file stays at its ratchet ceiling.Governed surface: the diff touches
.claude/**(Tier S). It lands on the owning seat's contract-tier review of record, not on green alone.Tests
Relay self-tests, at the base
1f13eb3189and then at this head:validate.mjsexecute.mjsdispatch.mjsThe pins that said "⛔ published is reachable by NO row" moved with the ruling. They now say that published is reached by ONE row (
advisory_state), through ONE field value, fromdraftalone, through the versions fence, and that no request any row spells carries it in a path or a body. The unknown-state cases still fail closed, at the validator's enum and at the state fence. The executor's marker sweep now covers 73 runs, up from 46, including every publish and versions run. The marker reaches no log line, row, summary, annotation or pace log.Ablation, one-shot. Each mutation went through
scripts/ablation-replace.mjs(anchor hit 1 → 0, blob changed) and was restored there (blob == HEAD,git diff HEADempty):publishedmay also leaveclosed.validatewent red on 2 cases (the transition table and the fenced transitions).executewent red on 1 case: an illegalclosed → publishedsent a PATCH.validatewent red on 1 case (the unknown target fails closed).executestayed green: its preflight independently gives an unknown target no from-states, so the second layer held.validatewent red on 3 cases.executewent red on 1 case: an unversioned publish sent a PATCH.Acceptance notes
security-advisories/update-repository-advisory) and the App permission ledger putstate(published, closed, draft) andvulnerabilities(package, range, patched versions, functions) on one PATCH, underrepository_advisories: writealone, with no additional permission. The App's PATCH ofstateis already measured live (triage → draftanddraft → closed, runs 41443030055 and 41443112178). The first live publish after merge confirms four things:closed → draft,draft → publishedand the versions from the App, the versions answered in the spelling sent (the read-back compares the strings exactly, so a platform that normalises them fails closed with nothing re-sent), and whether the platform needs versions before publishing (the relay needs them either way).security-findings.mdhad zero headroom. Its §2 line "while the relay run log still prints the whole payload, no mechanism in the relay payload" is conditional on a state that no longer holds: the relay readsclient_payloadfrom the event file and prints none of it (pinned byvalidate.mjs's event-file battery and the live marker runs). It also contradicts §3, which routes the mechanism throughadvisory_create's payload. If the seat prefers to keep it, the alternative is a ruled +1 on that file's ceiling..github/workflows/fleet-write.yml's comment still lists "FOUR write consumers" ofpermission-repository-advisoriesand does not nameadvisory_versions. The mint input itself needs no change. The comment lives on a Tier H surface, so it was left out of this Tier S PR.ops.mjs'srequestsdocumentation named aconfirmAdvisoryCollaboratorsthat never existed. The sentence was rewritten to nameconfirmAdvisory.Verification
Head
5aa0bddcdc(the branch merged withorigin/main):node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 46 families, and all 46 ran locally, each with its exit captured before any pipe.--ranreconciliation: "46 derived famil(ies) accounted for — 46 run, 0 NOT-MEASURED (a DERIVED zero …)".Local gate exits
Every family exited 0.
pnpm --filter @objectstack/lint run check:doc-formula-expressionsfirst exited 3 (PREREQUISITE NOT MET:@objectstack/formulaand@objectstack/lintunbuilt). It exited 0 afterpnpm exec turbo run build --filter=@objectstack/formula --filter=@objectstack/lintunder the shared verify lock ("VERDICT command-exit 0"). The relay importers outside the derivation,fleet-roster,mailbox-openandrerun-failed, were also run with--self-test, and each exited 0.Families CI runs beyond the local subset are read from this PR's check-runs.
Seat note
Claim:naming this branch was owed by the dispatching seat at dispatch (step 2) and has now been posted on pm/fleet-write: the relay publishes a security advisory on the maintainer's list: reopen, set the affected and patched versions, publish #30110. This body edit re-runsPR-body guards, which read its absence on the first run.