Repository navigation
fix(plugin-audit): a mention notification reaches a mentioned user only when they may read the comment it previews - #26497
Conversation
…o read the comment Claude-Session: https://claude.ai/code/session_012YjcDon6BvR9MN93ZsFq3U Co-Authored-By: Claude <noreply@anthropic.com>
…t gate Claude-Session: https://claude.ai/code/session_012YjcDon6BvR9MN93ZsFq3U Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012YjcDon6BvR9MN93ZsFq3U Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012YjcDon6BvR9MN93ZsFq3U Co-Authored-By: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 10 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 5f053e0c8ffffd77dee778df46235bb02538df50 && git checkout 5f053e0c8ffffd77dee778df46235bb02538df50
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 00a4593ad7a7f007c7988d94c8258510ea126e73 88a2b6cd2d8f54cc143884a0d2a4041ee8805f08 && git checkout -B drift-repro 00a4593ad7a7f007c7988d94c8258510ea126e73 && git merge --no-ff 88a2b6cd2d8f54cc143884a0d2a4041ee8805f08
node scripts/docs-audit/affected-docs.mjs --json 00a4593ad7a7f007c7988d94c8258510ea126e73
|
席位核对 — ACCEPT ·
|
…n gate now asks The mention recipient gate admits a mentioned user only when they may read the comment as well as its record (#26497). personas[1] says the @mention target must hold read on sys_comment; acceptance[9] and negative[6] name both reads; revision 7 → 8 with a history row. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012YjcDon6BvR9MN93ZsFq3U
Fixes #26453
Clause-②: no
Metadata-protocol: none
维护者速读
@objectstack/plugin-audit一个包,没有数据迁移,也没有 spec 变更。packages/plugins/plugin-audit/src/mention-recipient-gate.ts里逐收件人判定循环新增的评论读取那一行(readableIn(..., COMMENT_OBJECT, commentId));②mention-recipient-gate.integration.test.ts里新增的 DAVE 用例和sys_comment读权限替身;③.changeset/26453-mention-recipient-reads-comment.md。pnpm --filter '@objectstack/plugin-audit^...' build,再跑pnpm --filter @objectstack/plugin-audit exec vitest run --maxWorkers=2 src/mention-recipient-gate.integration.test.ts,应该看到 15/15 通过。What changes
mention-recipient-gate.ts: the gate used to decide each mentioned user on one read, the record the comment is on. It now also runs that user's own read of the comment, by id, throughresolveReadableParentIds, the same evaluator the comment-reaction create door asks of a comment. The read runs in the user's resolved envelope, with their organization claim vetted exactly as before. When that read does not return the comment, the user is withheld and logged with the new reasoncannot_read_thread. A comment with no id withholds every candidate (no_comment_id). The record read stays in place and runs first, for two reasons: the deep link opens the record, and an engine with no middleware seam has no thread read gate to fold the record question into the comment read. The module header is rewritten so that what it claims is true. The old header said a thread is readable exactly when its record is, and that is wrong, because the comment object carries its own grant.comment-access-hooks.ts:COMMENT_OBJECTis exported from this internal module so the gate names the object from one source. The package index is unchanged.audit-writers.ts,audit-plugin.ts: the doc comments now describe the two-read rule. No code changes in either file.patchon@objectstack/plugin-audit.Tests
mention-recipient-gate.integration.test.tsruns a real kernel, ObjectQL, SQLite,AuditPlugin(including its comment read gate) and the real grant resolver.sys_comment. It is keyed on a position assigned throughsys_user_position, which the real resolver reads, and it refuses the way the real layer does: a throw carryingPERMISSION_DENIED/ 403. A new fellow member, DAVE, can read the open record but does not hold that position.code/status;cannot_read_threadandno_comment_id, and never the comment text.Ablation: deleting the comment-read limb turns exactly the 5 new gate pins red. The other 10 stay green, including the stand-in control. The file was then restored, and its blob matches HEAD.
Pin, package suite, typecheck (head 88a2b6c)
Ablation, engine layer
The test imports its subject through relative
srcpaths, so no rebuild was involved.One-time check in a real composition (throwaway, not committed)
A throwaway dogfood file booted
bootStackwith the real plugin-security, plugin-audit and messaging, org-bound, and drove the gate through the REST layer. All three legs ran at 88a2b6c:ablation-dist-preflight --absentshowed the limb absent fromdist/. The gate admitted the record-only recipient too, and that was the one red assertion. The rebuild exited 1 at its DTS stage after writing the JS bundles the suite consumes, so thedist/preflight reading, not the build's exit code, certifies this leg.dist/again, and the result was green again.The throwaway file was then deleted, and
git status --porcelainis empty.Acceptance notes
audit-writers.tsis the only producer that copiessys_commenttext into a notification.approval.commentin plugin-approvals notifies an approval request's own participants about approval-action comments. That is a different object with its own audience, and it is not in scope here.afterInsert. The engine binds reads there to the write's own unit of work (the ambient transaction store), so the row is visible. A read that cannot see the row withholds, which is the fail-closed direction. The existing CAROL pins exercise this path end to end.test:repo(the vitestrepoproject): it belongs to CI'sTest Core, and this diff changes no test in that project.!and there is no(narrowing)arm.packages/cliintegration tier: not touched.Verification
Local subset, run at head
88a2b6cd2d. The families CI runs are read from this PR's check-runs, not re-run here.Derived gate list: 64 of 64 exit 0
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived 64 commands for the 6 changed paths. Each ran sequentially from the worktree root, with its exit code captured before any pipe. Reconciliation (--ran, exit 0):64 derived famil(ies) accounted for — 64 run, 0 NOT-MEASURED.check:dual-build-cjs-loadsfirst read exit 1. At that moment a concurrent closure build of mine was rewritingdist/, and the finding was a missing CommonJS file of a package this diff does not touch. Re-run on the quiet tree, it exited 0: 112 entry points across 69 packages load.428f1f9c20, before the merge ofmain, had 62 commands at exit 0 and 2 at exit 3 (PREREQUISITE NOT MET:check:dual-build-cjs-loadsandcheck:i18n). Both are measured green at88a2b6cd2d.Narrowed spot readings
Generated by Claude Code