You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
collaboration: authorization / tenancy-wall defect in record-discussion mention notifications under a walled posture (18.0 round-3 walled pass) — detail withheld pending maintainer #24785
Filing gate: ① product defect, authorization class — published under the RUNNER rule 2 carve-out: item, clause and existence only; the reproduction is withheld pending the maintainer (it is held in the dispatching QA session's report).
Path: records-forms.record-discussion-mentions
QA-source: #24783 · records-forms.record-discussion-mentions · acceptance[4] (recipient scoping) — observed beyond the clause's wording, on both walled postures
Checklist item priority: P1
type: Bug
reach: measured once per posture through the public HTTP door on walled boots (OS_TENANCY_POSTURE=isolated and group) at framework 580fd4e, and re-derived independently on fresh boots by a second agent (RUNNER rule 7) — detail withheld pending maintainer.
What: a behaviour of the comment-mention notification path crosses the organization wall a walled posture declares. No record or comment row becomes readable across organizations; the crossing is in the notification audience.
Severity read by both agents: medium — not a record-confidentiality breach, but a cross-organization reach the posture says is walled.
Acting reader: the lane owning plugin-audit's comment-mention writer and the messaging recipient resolution (domain:security to triage the routing). The maintainer receives the reproduction and the mechanism from the dispatching session.
Claim: maintainer-directed dispatch (18.0 QA wave, round 3)
Session: session_01CfRDy7SuTfUqaTu2AEH2Hi
Account: os-warren (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-24785-mention-wall
Worktree: none — the dev runs in its own cloud session checked out at origin/main74f78159af
Domain: domain:services (self-applied on the filing side, SKILL exception two: the card's body carries the three gate answers; triage may re-route)
Seat: domain:services#1 (nominal, the absent-line default — this is the 18.0 QA wave's dispatch session acting on the maintainer's direct instruction quoted below, not a lane seat)
File surface: packages/plugins/plugin-audit/src/** (the comment-mention notification writer); packages/services/service-messaging/src/** only if the fix belongs at the recipient-resolution seam; their tests; .changeset/ (stop on breach; explain in the report)
Container & model: M, mode:cloud, model: opus (TIER_DEFAULT — dispatch-gates.mjs --tier on this surface reads "no path-derived mandate · floor sonnet · default opus · ceiling fable")
Clause-②: no — the change narrows who a notification reaches; it is a runtime security-boundary change, so it sits on the human floor and its PR stays draft until the maintainer approves
Responsibility: plugin-audit (comment-mention notification writer) | none known | an authenticated user on a walled posture, through a public door — detail withheld
Thread-read: none
Serial constraints cleared: none — no open PR touches plugin-audit or service-messaging (the file lists of all 11 open PRs read), and no pm:dispatched claim's file surface names either
Directed-by: the maintainer, verbatim 「你测试出的安全问题如果不方便写issue 的,你可以自己派发」, in Claude Code session session_01CfRDy7SuTfUqaTu2AEH2Hi, 2026-10-19. The reproduction is handed to the dev inside its session; ⛔ nothing on this card or its PR carries it.
{
"issue": 24785,
"status": "done",
"branch": "claude/issue-24785-mention-wall",
"pr": "https://github.com/objectstack-ai/objectstack/pull/24794",
"session": "session_01BatqV4sRzXmRBizCSYJkMS",
"premise_still_valid": true,
"summary": "Mention notification recipients are now filtered by read access to the comment's record. New plugin-audit module mention-recipient-gate.ts: each mentions entry resolves to a user id (an email is looked up to its user; role:/team:/owner_of: entries are not mentions); the author is dropped; each remaining user is judged in their own envelope (core resolveUserAuthzGrants, the org claim vetted by vetOrganizationClaim under effectiveTenancyPosture over the tenancy service) through core resolveReadableParentIds, so no new ACL code. Withheld recipients get a warn line with user id + reason + comment id, never comment text. Fail closed on no record, missing record, or grant failure. installAuditWriters takes the gate as a new admitMentionRecipients option, which AuditPlugin wires; without it no mention is emitted (warned once). service-messaging is untouched: RecipientResolver.resolveEmail serves every topic, so the narrowing stays on the mention path and messaging receives only admitted ids. The single-organization case was confirmed and is covered by the same rule (a same-org member the record's sharing excludes is not notified), as the fix requirement states; no product decision was needed. PR is draft, assignee os-warren, not readied, no auto-merge.",
"tests": "Head 2a048ca9 (the last commit is comment-only; test runs on 43bbd4e4, byte-identical code). `pnpm --filter @objectstack/plugin-audit typecheck && pnpm --filter @objectstack/plugin-audit exec vitest run --maxWorkers=1` → Test Files 60 passed (60), Tests 976 passed (976), exit 0. New src/mention-recipient-gate.integration.test.ts (10 cases: real kernel + ObjectQL + SQLite + AuditPlugin + platform sys_user/sys_member + core resolver; plugin-security's read layer played by one WHERE-composing middleware; messaging by an emit recorder) → Tests 10 passed (10). It covers: isolated, another-org user mentioned by id or by email gets nothing; same-org reader gets it with source=record and org; email resolves to id; author never notified; audience expansions are no mention. group: non-member withheld, member notified. single: member excluded by record sharing withheld. The log line carries {reason, userId?, commentId} only. audit-writers.test.ts +3 (no gate → no emit; gate throws → no emit; only admitted ids emitted, with the gate input pinned). Ablations on the committed fix via scripts/ablation-replace.mjs, restored byte-identical with git status clean: (1) vetting removed → 4/10 red; (2) writer bypasses the gate → 8/10 red. End-to-end on a walled isolated boot (scratch showcase copy, real plugin-security, organizations, messaging inbox, full build at 43bbd4e4): outside-org mentionee unreadCount 0→0; same-org mentionee 0→1 with actionUrl set to the record page and record read 200; author 0. Control leg with ablation 2 rebuilt into dist (marker present by ablation-dist-preflight) let the outside-org mentionee receive it; then restored, rebuilt, marker absent (--absent).",
"gates": "Local: `node scripts/pm/dispatch-gates.mjs --commands` derived 70 families; `--ran` reconciliation: 70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN. 66 exited 0 on the first pass. Not first-pass green: check:dts-closure exit 1 and check:dual-build-cjs-loads exit 3 (PREREQUISITE NOT MET), both run mid-build, re-run after build exit 0. check:query-options-erasure exit 1 (3 new untyped option sites in the new test), fixed, re-run exit 0. check-plugin-teardown-shape.mjs --self-test exit 3: the shallow clone cannot reach its pinned control commit, NOT MEASURED locally, CI runs it. Re-run on final code: cross-package-test-inputs 0, test-source-alias 0, nul-bytes 0, query-options-erasure 0. CI on PR #24794 at report time: 'No other open PR may claim the same issue' success; 'Packed-tarball smoke (opt-in)' skipped; everything else in_progress (Lint ×9, Type Check ×4, Check Changeset, Check PR Size, Governed Surface Queue Guard, 'The card this PR closes must claim this branch', docs checks). test:repo not run locally (CI's Test Core).",
"mcp_calls": "3 — mcp__github__issue_read (get_comments, #24785), mcp__github__pull_request_read (get_check_runs, #24794), mcp__github__pull_request_read (get, #24794 verification); no MCP write tool used",
"api_writes": "2 — fleet-write relay pr_create (POST /repos/objectstack-ai/objectstack/pulls draft + POST /issues/24794/assignees), post-stamped os-dev-report comment (POST /repos/objectstack-ai/objectstack/issues/24785/comments); plus git push (not REST)",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none (承接者:无) · noted in PR Acceptance notes, not filed — an HTTP-level dogfood case for this rule with the real plugin-security belongs in packages/qa/dogfood, outside this card's file surface; the walled-boot run is the manual equivalent"
]
}
What it binds. 18.0.0 is cut after this card lands. In flight: PR #24794 (draft), under the claim above. The disclosure rule stands: ⛔ no reproduction on GitHub.
Activity
objectstack-fleet commentedon Oct 20, 2026
Claim: maintainer-directed dispatch (18.0 QA wave, round 3)
Session:
session_01CfRDy7SuTfUqaTu2AEH2HiAccount:
os-warren(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24785-mention-wallWorktree: none — the dev runs in its own cloud session checked out at
origin/main74f78159afDomain:
domain:services(self-applied on the filing side, SKILL exception two: the card's body carries the three gate answers; triage may re-route)Seat: domain:services#1 (nominal, the absent-line default — this is the 18.0 QA wave's dispatch session acting on the maintainer's direct instruction quoted below, not a lane seat)
File surface:
packages/plugins/plugin-audit/src/**(the comment-mention notification writer);packages/services/service-messaging/src/**only if the fix belongs at the recipient-resolution seam; their tests;.changeset/(stop on breach; explain in the report)Container & model:
M,mode:cloud,model: opus(TIER_DEFAULT —dispatch-gates.mjs --tieron this surface reads "no path-derived mandate · floor sonnet · default opus · ceiling fable")Clause-②: no — the change narrows who a notification reaches; it is a runtime security-boundary change, so it sits on the human floor and its PR stays draft until the maintainer approves
Responsibility:
plugin-audit (comment-mention notification writer) | none known | an authenticated user on a walled posture, through a public door — detail withheldThread-read: none
Serial constraints cleared: none — no open PR touches
plugin-auditorservice-messaging(the file lists of all 11 open PRs read), and nopm:dispatchedclaim's file surface names eitherDirected-by: the maintainer, verbatim 「你测试出的安全问题如果不方便写issue 的,你可以自己派发」, in Claude Code session
session_01CfRDy7SuTfUqaTu2AEH2Hi, 2026-10-19. The reproduction is handed to the dev inside its session; ⛔ nothing on this card or its PR carries it.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
os-dev-report
{ "issue": 24785, "status": "done", "branch": "claude/issue-24785-mention-wall", "pr": "https://github.com/objectstack-ai/objectstack/pull/24794", "session": "session_01BatqV4sRzXmRBizCSYJkMS", "premise_still_valid": true, "summary": "Mention notification recipients are now filtered by read access to the comment's record. New plugin-audit module mention-recipient-gate.ts: each mentions entry resolves to a user id (an email is looked up to its user; role:/team:/owner_of: entries are not mentions); the author is dropped; each remaining user is judged in their own envelope (core resolveUserAuthzGrants, the org claim vetted by vetOrganizationClaim under effectiveTenancyPosture over the tenancy service) through core resolveReadableParentIds, so no new ACL code. Withheld recipients get a warn line with user id + reason + comment id, never comment text. Fail closed on no record, missing record, or grant failure. installAuditWriters takes the gate as a new admitMentionRecipients option, which AuditPlugin wires; without it no mention is emitted (warned once). service-messaging is untouched: RecipientResolver.resolveEmail serves every topic, so the narrowing stays on the mention path and messaging receives only admitted ids. The single-organization case was confirmed and is covered by the same rule (a same-org member the record's sharing excludes is not notified), as the fix requirement states; no product decision was needed. PR is draft, assignee os-warren, not readied, no auto-merge.", "tests": "Head 2a048ca9 (the last commit is comment-only; test runs on 43bbd4e4, byte-identical code). `pnpm --filter @objectstack/plugin-audit typecheck && pnpm --filter @objectstack/plugin-audit exec vitest run --maxWorkers=1` → Test Files 60 passed (60), Tests 976 passed (976), exit 0. New src/mention-recipient-gate.integration.test.ts (10 cases: real kernel + ObjectQL + SQLite + AuditPlugin + platform sys_user/sys_member + core resolver; plugin-security's read layer played by one WHERE-composing middleware; messaging by an emit recorder) → Tests 10 passed (10). It covers: isolated, another-org user mentioned by id or by email gets nothing; same-org reader gets it with source=record and org; email resolves to id; author never notified; audience expansions are no mention. group: non-member withheld, member notified. single: member excluded by record sharing withheld. The log line carries {reason, userId?, commentId} only. audit-writers.test.ts +3 (no gate → no emit; gate throws → no emit; only admitted ids emitted, with the gate input pinned). Ablations on the committed fix via scripts/ablation-replace.mjs, restored byte-identical with git status clean: (1) vetting removed → 4/10 red; (2) writer bypasses the gate → 8/10 red. End-to-end on a walled isolated boot (scratch showcase copy, real plugin-security, organizations, messaging inbox, full build at 43bbd4e4): outside-org mentionee unreadCount 0→0; same-org mentionee 0→1 with actionUrl set to the record page and record read 200; author 0. Control leg with ablation 2 rebuilt into dist (marker present by ablation-dist-preflight) let the outside-org mentionee receive it; then restored, rebuilt, marker absent (--absent).", "gates": "Local: `node scripts/pm/dispatch-gates.mjs --commands` derived 70 families; `--ran` reconciliation: 70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN. 66 exited 0 on the first pass. Not first-pass green: check:dts-closure exit 1 and check:dual-build-cjs-loads exit 3 (PREREQUISITE NOT MET), both run mid-build, re-run after build exit 0. check:query-options-erasure exit 1 (3 new untyped option sites in the new test), fixed, re-run exit 0. check-plugin-teardown-shape.mjs --self-test exit 3: the shallow clone cannot reach its pinned control commit, NOT MEASURED locally, CI runs it. Re-run on final code: cross-package-test-inputs 0, test-source-alias 0, nul-bytes 0, query-options-erasure 0. CI on PR #24794 at report time: 'No other open PR may claim the same issue' success; 'Packed-tarball smoke (opt-in)' skipped; everything else in_progress (Lint ×9, Type Check ×4, Check Changeset, Check PR Size, Governed Surface Queue Guard, 'The card this PR closes must claim this branch', docs checks). test:repo not run locally (CI's Test Core).", "mcp_calls": "3 — mcp__github__issue_read (get_comments, #24785), mcp__github__pull_request_read (get_check_runs, #24794), mcp__github__pull_request_read (get, #24794 verification); no MCP write tool used", "api_writes": "2 — fleet-write relay pr_create (POST /repos/objectstack-ai/objectstack/pulls draft + POST /issues/24794/assignees), post-stamped os-dev-report comment (POST /repos/objectstack-ai/objectstack/issues/24785/comments); plus git push (not REST)", "open_questions": [], "out_of_scope_findings": [ "carrier: none (承接者:无) · noted in PR Acceptance notes, not filed — an HTTP-level dogfood case for this rule with the real plugin-security belongs in packages/qa/dogfood, outside this card's file surface; the walled-boot run is the manual equivalent" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Ruled: into 18.0 (
target:v18) · recorded by the triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-20T07:01ZThe maintainer, in this seat's session, verbatim: 「进 18.0 同意你的清单」. That answers the triage seat's 18.0 list:
What it binds. 18.0.0 is cut after this card lands. In flight: PR #24794 (draft), under the claim above. The disclosure rule stands: ⛔ no reproduction on GitHub.
Generated by Claude Code