Skip to content

pm/fleet-write: the relay publishes a security advisory on the maintainer's list: reopen, set the affected and patched versions, publish #30110

Closed

Description

@objectstack-fleet

security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj

What

The fleet-write relay (scripts/pm/fleet-write/ops.mjs, execute.mjs, validate.mjs) refuses published by name. ADVISORY_REFUSED_STATES says "publishing stays with the maintainer, by hand, and no relay row reaches it", and ADVISORY_TRANSITIONS allows only triage → draft and triage|draft → closed.

The maintainer has ruled that the relay must be able to publish.

The ruling

The maintainer, in session session_01QSeKYn5CmmdShCJifm6WYj on 2026-11-04, was told that the relay does not allow an advisory to be set to published, so the seat had handed the maintainer a list to publish by hand. The answer:

中继不允许把公告设为已发布 ,立卡解决

This supersedes the "发布留给我" clause of the #29160 scope that the ops.mjs header quotes. Record both verbatim, the old clause as superseded.

The standing rule that decides WHAT is published is unchanged: 「哪些高危要发布应该在我版本发布前决策吧」. The maintainer decides the list before each release. The relay only carries out a list the maintainer has approved.

What a publish needs on the platform (to be measured, not assumed)

A fixed advisory in this repository is closed once its fix merges, by standing order. Publishing one needs three steps:

  1. Reopen. closed → draft. Measure whether the REST PATCH …/security-advisories/{id} accepts it.
  2. Set the versions. Each vulnerabilities entry carries vulnerable_version_range (e.g. <= 18.2.0) and patched_versions (e.g. 18.3.0). Today a draft names its packages and nothing else (advisory_create).
  3. Publish. draft → published.

Measure which of these the App token's repository-advisories: write grant can do, and whether the platform needs the versions before it publishes.

Acceptance

  • advisory_state gains closed → draft and draft → published. Every other transition stays refused: from published, and from withdrawn. Each transition is judged on the executor's own GET, as today.
  • One new row sets ONLY the vulnerabilities entries' package, vulnerable_version_range and patched_versions.
    • These are closed, shape-checked keys: an npm package name, and a semver range or version list.
    • The row carries no prose and travels alone, like the other advisory rows.
    • ⛔ Editing an advisory's summary or description stays out of reach of every row.
  • A publish stroke carries one advisory alone.
    • The run annotation names the id. That is public once published, which is the point.
    • The executor reads the advisory back as published, with the versions it was sent.
  • ADVISORY_REFUSED_STATES loses published. The header prose, the row comments, the self-tests in validate.mjs and execute.mjs, and the "⛔ published is reachable by NO row" pins all move with the ruling.
    • An unknown state or transition still fails closed.
  • .claude/skills/pm-dispatch/references/lanes/security.md and security-findings.md say:
    • the seat publishes through the relay only advisories on the maintainer's approved list for that release;
    • the seat sets the versions first, then publishes;
    • the seat records the run ids on the seat post (ids only, no text).
  • After merge, one live publish of an advisory the maintainer has approved, with the run id recorded here.

Out of bounds

  • ⛔ No advisory text, mechanism or unpublished advisory id in the PR, the commits or the changeset.
  • ⛔ No CVE request, no temporary private fork, and no collaborator removal: they stay out of reach, as today.
  • ⛔ The sender gate stays target-write, the gate every advisory row keeps.
  • If the App token cannot reopen or publish, that is a fork for the maintainer, never a widened grant.

Activity

  1. added
    toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface
    priority:p1High: required for production / M2
    domain:skillsDomain lane — seat card indexed by label:pm:seat
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Nov 4, 2026
  2. objectstack-fleet commented on Nov 4, 2026

    @objectstack-fleet
    ContributorAuthor

    security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj
    os-dev-report

    {
      "issue": 30110,
      "status": "done",
      "branch": "claude/issue-30110-relay-advisory-publish",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/30135",
      "session": "session_01QSeKYn5CmmdShCJifm6WYj",
      "premise_still_valid": true,
      "summary": "The relay now publishes. `advisory_state` gains closed → draft (the reopen) and draft → published. Nothing leaves published or withdrawn, and a target the table does not name fails closed at both the validator's enum and the state fence. `ADVISORY_REFUSED_STATES` is empty. A new row, `advisory_versions`, carries ghsa_id and closed `versions` entries: exactly an npm package, a vulnerable_version_range and its patched_versions. It carries no prose, travels alone, and its PATCH body is { vulnerabilities } only. The executor sends versions only to a draft whose whole list the action may replace, because the documented PATCH replaces the array. It publishes only when its own GET reads every entry in that spelling, so the versions are set first. A publish lands only when the answer reads published and lists exactly the entries the GET read, and that landing alone is annotated with the id, which the seat-side reader hands on. The ops.mjs header quotes the new ruling verbatim and records the 发布留给我 clause as superseded. security-findings.md §3 carries the seat's publish rule (the approved list only, versions first, run ids on the seat post), lanes/security.md points to it, and rest-channel.md is corrected; every file stays at its line ceiling. The card had no Claim: comment and no assignee when I read it; I worked from the dispatch order and wrote neither field. The PR touches .claude/** (Tier S), so it lands by the seat's contract review.",
      "tests": "Relay self-tests at base 1f13eb3189 → head 5aa0bddcdc: validate 303 → 329 cases (advisory battery floor 51 → 77), execute 260 → 269 (32 → 41), dispatch 315 → 318 (14 → 17); all three exit 0. The executor's marker sweep covers 73 runs, up from 46: the marker reaches no log line, row, summary, annotation or pace log. Ablation, one-shot through scripts/ablation-replace.mjs, each leg landed (anchor 1 → 0, blob changed) and restored (blob == HEAD, git diff HEAD empty). A: published may also leave closed; validate exit 1 (2 cases), execute exit 1 (an illegal closed → published sent a PATCH). B: the fail-closed default removed from the state fence; validate exit 1 (the unknown-target case), execute exit 0, because its preflight independently gives an unknown target no from-states. C: the publish half of the versions fence removed; validate exit 1 (3 cases), execute exit 1 (an unversioned publish sent a PATCH). Importers of the relay outside the derivation (fleet-roster, mailbox-open, rerun-failed) each ran --self-test with exit 0.",
      "gates": "Local, at 5aa0bddcdc (the branch merged with origin/main): dispatch-gates --commands --repo objectstack-ai/objectstack derived 46 families. All 46 ran with exits captured before any pipe and all exited 0, except check:doc-formula-expressions, which first exited 3 (PREREQUISITE NOT MET: formula and lint unbuilt). It exited 0 after the turbo build of @objectstack/formula and @objectstack/lint under the verify lock (VERDICT command-exit 0), and the --ran record carries that rerun's exit. --ran: '46 derived famil(ies) accounted for — 46 run, 0 NOT-MEASURED (a DERIVED zero — all 46 recorded an exit code and none of them is 3)'. CI on PR 30135 when read: Check Changeset success; Governed Surface Queue Guard, the Lint and Type Check lanes, PR-body guards and Check PR Size in_progress. Not waited on.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "0",
      "api_writes": "2 — POST /repos/objectstack-ai/objectstack/dispatches for pr_create (relay run 41858288100: the draft pull plus its skip-changeset label leg; the body read back byte-identical, 9718 bytes) and POST /repos/objectstack-ai/objectstack/dispatches for this os-dev-report comment through post-stamped; git push is not counted",
      "open_questions": [
        { "question": "Four platform facts are documentation readings, not live: closed → draft by PATCH (the REST description names that move neither way), draft → published by the App token, the versions accepted from the App and answered in the spelling sent, and whether the platform needs versions before publishing. First live run confirms.", "options": ["A: the first live publish after merge measures all four on one advisory the maintainer approved, and records the run ids on the card", "B: a throwaway advisory first"], "recommendation": "A, because the card already orders exactly that publish, and every refusal fails closed with nothing re-sent: a refused reopen or publish is a FAILED action carrying ADVISORY_ACCESS_REMEDY, and a re-spelled versions answer fails the read-back" },
        { "question": "The publish read-back judges the PATCH's own answer (state published plus the exact entries the preflight GET read), as every other advisory row does. It sends no second GET after the publish.", "options": ["A: the PATCH answer, as implemented", "B: add a GET after the PATCH"], "recommendation": "A, because the PATCH answer is the stored advisory, and that shape is already measured live for the App's two state moves; B adds a request and no new fact" },
        { "question": "security-findings.md had zero line headroom, so I deleted its §2 line about the relay log printing the whole payload. That line is conditional on a state that no longer holds, and it contradicts §3's advisory_create route.", "options": ["A: keep the deletion", "B: restore the line and take a ruled +1 on the file's ceiling"], "recommendation": "A, because the relay reads client_payload from the event file and prints none of it, which validate.mjs's event-file battery and the live marker runs both pin" }
      ],
      "security_findings": "none",
      "out_of_scope_findings": [
        ".github/workflows/fleet-write.yml's comment on permission-repository-advisories still says FOUR write consumers and does not name advisory_versions; the mint input itself needs no change · a Tier H surface, so it was left out of this Tier S PR · carrier: none — noted in the PR's Acceptance notes",
        "ops.mjs's requests documentation named a confirmAdvisoryCollaborators that never existed · rewritten in place to confirmAdvisory, because the diff rewrote that sentence · carrier: none — fixed by this diff"
      ]
    }
  3. objectstack-fleet commented on Nov 4, 2026

    @objectstack-fleet
    ContributorAuthor

    security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj

    Claim: maintainer-directed dispatch · 2026-11-04T15:45Z (step 2 owed since the dispatch of 2026-11-04 14:1xZ; posted late, before landing)
    Session: session_01QSeKYn5CmmdShCJifm6WYj
    Account: os-justin (the card's assignee)
    Branch: claude/issue-30110-relay-advisory-publish
    Worktree: objectstack-issue-30110
    Domain: domain:skills
    Seat: security#1
    File surface: scripts/pm/fleet-write/{ops,validate,execute,dispatch}.mjs, .claude/skills/pm-dispatch/references/{lanes/security.md,security-findings.md,rest-channel.md}
    Container & model: M, mode:subagent, model: opus, effort: xhigh
    Clause-②: no
    Metadata-protocol: none
    Thread-read: none
    Premise: holds (the ruling is in the card body; the dev's report is comment 6634747999; PR #30135)

    Ruling carried: the maintainer's, verbatim, in the card body.

  4. objectstack-fleet commented on Nov 4, 2026

    @objectstack-fleet
    ContributorAuthor

    security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj

    First live publish after PR #30135 merged, 2026-11-04T17:07Z. The advisory was on the maintainer's approved list; it is chosen by the maintainer and named only on the seat post.

    • Reopen closed → draft: relay run 41873738957, success.
    • Versions set and read back: relay run 41873976222, success.
    • Publish draft → published, read back with its versions: relay run 41874125239, success. The annotation names the id, as designed.

    The four platform facts the card called "first live run confirms" all hold for the App token: the PATCH takes the reopen, the App may publish, the versions are accepted and answered in the spelling sent, and the publish lands after the versions are set.

    One note for the next seat: the relay validates locally before it dispatches, so run it from a checkout that carries the merged ops.mjs. A stale checkout refuses advisory_versions as an unknown op, and nothing is sent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:skillsDomain lane — seat card indexed by label:pm:seatpriority:p1High: required for production / M2toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions