Skip to content

fix(pm): advisory_read holds its sender to target-write, by the maintainer's ruling - #29656

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-29644-advisory-read-sender-gate
Nov 3, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-29644-advisory-read-sender-gate

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #29644
Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:中继的密封公告读取操作(advisory_read)现在只要求发送者对目标仓有写、维护或管理权限,与其余中继操作同一道门;不再要求发送者在该公告的协作者名单上。原「协作者」档位整个删除,任何其他取值仍一律拒绝、不封装任何内容。
  • 为什么改:按维护者裁决「同意改成对仓库有写权限,立卡派发修复」。旧门槛让首批三次真实读取全部被拒,而有写权限的账号本可自行加为协作者,旧门槛并未挡住任何人。
  • 风险与回滚:读取令牌仍只铸「只读」公告权限,未提升;改动只落在中继 scripts/pm/fleet-write/ 三个文件,不进任何发布包;回滚即还原这一个 commit。
  • 先看:scripts/pm/fleet-write/ops.mjs(advisory_read 行的 senderGate 与 sender 关卡,风险最高);execute.mjs(advisory_read 自测电池中的发送者用例);validate.mjs(关卡用例)。
  • 两分钟验证:node scripts/pm/fleet-write/execute.mjs --self-test,预期末行以 ✓ fleet-write/execute self-test: 260 cases pass across 22 batteries 开头。
  • 需要你做的:无,席位落地。

What changes

  • advisory_read's senderGate is 'target-write' (was 'collaborator'). A read is held to the gate every relay stroke already passes, write, maintain or admin on the target, which the executor judges on the stroke before the advisory's GET. The maintainer's ruling is quoted verbatim in the ops.mjs header paragraph and in the row comment, replacing the text that called collaborator "the card's recommendation" and said the maintainer would rule on the pull request.
  • The row's grant stays read. The row comment says a read refused under it is the maintainer's to rule on, never a raise to write.
  • The collaborator arm is removed, not kept. It measured as refusing every live read, it excluded no one (a write sender can add itself to an advisory), and nothing outside these three files names it, so under the startup-stage rule it retires instead of staying a tested but unused setting. ADVISORY_READ_SENDER_GATES keeps the one value, and any other value, collaborator included, fails closed with nothing sealed.
  • Follow-on inside the same files: the sender fence no longer reads the sender, so advisoryReadFenceFailures' context drops sender and advisoryReadAction no longer takes it. No exported symbol is renamed or removed.
  • dispatch.mjs is unchanged. None of its cases pins the gate: its end-to-end read's sender is on the fixture's collaborators, so it passes under either gate.

A hypothesis, not a finding

These are the readings from the session that led to the ruling (run ids only). The add's PATCH answer listed the sender (run 41545025429). Three reads after it were refused at fence sender (runs 41545143476, 41545259848, 41546082963). A re-sent add's pre-PATCH GET under the write grant already listed the sender, so it was a no-op (run 41546190167).

That the read-grant GET answer omits collaborating_users is a hypothesis. It is UNVERIFIED. This PR does not depend on it and does not prove it, and nothing here measured it. The ops.mjs header says the same.

Tests

  • Executor (execute.mjs), advisory_read battery, 17 to 18 cases:
    • Rewritten: a sender with write on the target who is not on the advisory's collaborating_users is now sealed for, and the parts open byte for byte for the seat's key. Before, this case pinned a refusal.
    • New: a sender with triage on the target is refused before the advisory's GET, even when the advisory lists it as a collaborator. Exit 4, only the permission read is sent, and there is no row, no seal and no part.
    • Rewritten: a made-up gate and the retired collaborator both fail closed after the GET, even for a write sender the advisory lists.
    • The marker sweep now covers 20 runs (was 19), with the battery name, the roster pin and the verdict line updated to match.
  • Validator (validate.mjs), advisory_read battery, 28 cases (count unchanged):
    • The row pin is now one setting, target-write, with the grant read.
    • target-write reads no collaborator: someone else listed, none, null or a team alone are all accepted.
    • Any other gate fails closed: the retired value, misspellings, a prototype key, empty, or absent.
    • The fence-read case now refuses at sender only under an unknown gate.
    • The leak case now feeds a prose gate value and asserts it is never echoed.

Acceptance notes

  • The card's third acceptance item is the seat's to do after merge: one live advisory_read by a seat with write on the target that opens, recorded on the card as a run id. It is not done here.
  • ops.mjs has no --self-test of its own. Run with the flag, it exits 0 and prints nothing, because it is only an import. Its table is pinned by validate.mjs --self-test.
  • No changeset: scripts/pm/ ships in no package, so this PR carries skip-changeset.
  • origin/main moved two commits past base 3482f3d8. Neither touches scripts/pm/fleet-write/ or its imports, so nothing was merged in.

Verification

All readings below were taken at head a8eed928.

Relay self-tests, before (base 3482f3d) and after (a8eed92)
before  validate EXIT=0  303 cases pass across 16 batteries
before  execute  EXIT=0  259 cases pass across 22 batteries
before  dispatch EXIT=0  315 cases pass across 27 batteries
before  ops      EXIT=0  (no --self-test handler: zero bytes printed)
after   validate EXIT=0  303 cases pass across 16 batteries
after   execute  EXIT=0  260 cases pass across 22 batteries
after   dispatch EXIT=0  315 cases pass across 27 batteries
after   ops      EXIT=0  (no --self-test handler: zero bytes printed)
Ablation, one-shot (scripts/ablation-replace.mjs, wrap mode, restore proven)

The expected directions were written down before each leg ran. The fix was committed first, so each restore leg points at HEAD.

Leg A: ops.mjs  senderGate 'target-write' replaced by 'collaborator' (the pre-change value, now unknown)
  anchor x1 to x0, replacement x0 to x1, blob 8bd15e599238 to 46d1f81abb0a
  validate EXIT=1  1 of 303 failed  (the row pin)
  execute  EXIT=1  8 of 260 failed  (the landed read, its opening and parts, the long read, the write-sender-off-collaborators case)
  dispatch EXIT=1  4 of 315 failed  (the end-to-end read and its follow-ons)
  restored: blob == HEAD (8bd15e599238), git diff HEAD empty

Leg B: ops.mjs  sender fence made to pass always (fail open)
  anchor x1 to x0, replacement x0 to x1, blob 8bd15e599238 to 8e4242593a79
  validate EXIT=1  3 of 303 failed  (any other gate closed; fence read under an unknown gate; the prose-gate leak case)
  execute  EXIT=1  1 of 260 failed  (an unknown and the retired gate fail closed)
  dispatch EXIT=0  315 pass         (as predicted: it pins no unknown gate)
  restored: blob == HEAD (8bd15e599238), git diff HEAD empty, git status clean
Gates: dispatch-gates --commands --repo objectstack-ai/objectstack (39 families, all run at a8eed92)

Every exit code was captured before any pipe, and each log's own verdict line was read. pnpm check:pm-dispatch-gates ran under nohup: its fast tier passed 1945 cases in 403.2s.

0 node scripts/check-closing-keyword-parity.mjs            0 pnpm check:entry-guard
0 node scripts/check-closing-keyword-parity.mjs --self-test 0 pnpm check:gitlink-declared
0 node scripts/check-comment-mask-corpus.mjs               0 pnpm check:nul-bytes
0 node scripts/check-declaration-mirrors.mjs               0 pnpm check:parse-guard
0 node scripts/check-declaration-mirrors.mjs --self-test   0 pnpm check:pm-close-cards
0 node scripts/check-self-test-wired.mjs                   0 pnpm check:pm-dispatch-gates
0 node scripts/check-self-test-wired.mjs --self-test       0 pnpm check:pm-fleet-write-dispatch
0 node scripts/check-self-test-workflow-commands.mjs       0 pnpm check:pm-fleet-write-execute
0 node scripts/check-self-test-workflow-commands.mjs --self-test  0 pnpm check:pm-fleet-write-validate
0 node scripts/check-whole-set-label-write.mjs             0 pnpm check:pm-issue-create
0 node scripts/check-whole-set-label-write.mjs --self-test 0 pnpm check:pm-issue-transfer
0 node scripts/ci/plan-test-core-shards.mjs --self-test    0 pnpm check:pm-label-write
0 node scripts/pm/bare-root-worklist.mjs --self-test       0 pnpm check:pm-post-stamped
0 pnpm check:agent-test-spelling                           0 pnpm check:pm-record-recognisers
0 pnpm check:bash32-floor                                  0 pnpm check:pm-write-pace
0 pnpm check:ci-filter-parity                              0 pnpm check:pnpm-filter-targets
0 pnpm check:cli-command-ids                               0 pnpm check:pnpm-flag-spellings
0 pnpm check:cross-package-test-inputs                     0 pnpm check:refd-timer-probe
0 pnpm check:driver-memory-census                          0 pnpm check:scripts-symbol-anchors
                                                           0 pnpm check:watch-hint-literal

--ran reconciliation:
✓ dispatch-gates --ran: 39 derived famil(ies) accounted for — 39 run, 0 NOT-MEASURED (a DERIVED zero — all 39 recorded an exit code and none of them is 3).

These run in CI and not locally: the 52 artifact-roster families, the 11 wide-population families, the type-check lanes and the planned Test Core shard step. This diff touches no TypeScript package.


Generated by Claude Code

…ainer's ruling

The relay's advisory_read row now holds its sender to target-write (write,
maintain or admin on the target), the gate every other relay row keeps,
in place of the advisory's own collaborating_users. The ruling is quoted
verbatim beside the row and in the header. The row's grant stays read.

The collaborator arm is retired rather than kept as a second setting: it
refused every live read, and it excluded no one, since a sender with write
on the target can add itself to an advisory. ADVISORY_READ_SENDER_GATES
keeps the one value, so any other, collaborator included, fails closed.

Self-tests: the executor's advisory_read battery pins a write sender off
the collaborators sealed for, a triage sender refused before the GET, and
an unknown or retired gate closed (17 -> 18 cases); the validator's fence
cases move to the one gate.

Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Nov 3, 2026
@github-actions github-actions Bot added the size/m label Nov 3, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#2 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj

席位核对

ACCEPT · head a8eed92854506c06d65249b2293b965aa705bd2f · 2026-11-03T00:24Z

  • Shape: 3 files under scripts/pm/fleet-write/ (+61/−58), all inside the claimed surface. Not a governed path, under the 3000-line threshold, Tier S path rules not reached. skip-changeset is right: scripts/pm/ ships in no package.
  • Diff read by the seat: advisory_read.senderGate is 'target-write' and grant stays 'read'. ADVISORY_READ_SENDER_GATES holds one key. The sender fence returns null for target-write and refuses any other value, so the retired collaborator, misspellings, prototype keys, empty and absent values all fail closed. The ruling is quoted verbatim in the header paragraph and in the row comment. The read-grant collaborating_users cause is stated as an unmeasured hypothesis. Removing the collaborator arm left no dangling reference: advisoryCollaboratorLogins and LOGIN_SHAPE remain in use by advisory_collaborators_add.
  • Premise measured: the seat's own live reads (runs 41545143476, 41545259848, 41546082963 refused at fence sender; run 41546190167 a no-op add).
  • Gates, re-run by the seat at this head: validate.mjs --self-test 303 cases / 16 batteries exit 0; execute.mjs --self-test 260 / 22 exit 0; dispatch.mjs --self-test 315 / 27 exit 0. These match the report.
  • Check-runs at this head: 26 success, 20 skipped, 0 failed (all completed). The 20 skips are path-filtered UI/build/docs/dogfood jobs and opt-in harnesses, judged by name by the seat: check-expected-skips.mjs could not run in this container (no node_modules after a restart), so that verdict is NOT MEASURED. check-governed-merges.mjs --pr 29656: NOT governed, 119 changed lines. mergeable_state: clean.
  • Accepted deviations: dispatch.mjs is unedited because it pins no gate (measured by the dev, confirmed by its passing self-test). No origin/main merge, since the two newer commits touch no file here. The empty-branch push is mandated by os-dev rule 1.
  • Out of scope: none. security_findings: none.
  • Landing: seat lands it (ready, then auto-merge through the queue). After merge the seat runs one live advisory_read and records the run id on pm/fleet-write: advisory_read holds its sender to target-write, by the maintainer's ruling, since the collaborator gate refuses every live read #29644.

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

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pm/fleet-write: advisory_read holds its sender to target-write, by the maintainer's ruling, since the collaborator gate refuses every live read

1 participant