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
measure(plugin-email): who can list sys_email rows that carry identity mail (reset and verification links) — sys_email declares no requiredPermissions #24625
Filing gate ②: a measured finding. It comes from the contract review of PR #24587 (step 3 of the 18.x line, card #24531), ③ item 5, and the dev's own observation in os-dev-report 6270006270.
Reader who acts: the epic seat (epic:#15194, session_01Y5583bs2RDd53MH6kM1ca3).
Dedupe: none.
The finding. PR #24587 moves new identity mail (password reset, account invitation, verification, magic link, change-email) into sys_identity_email. That table sits behind requiredPermissions: ['manage_platform_settings'], a platform-scoped capability. But sys_email declares norequiredPermissions. Every identity mail an 18.0 install sent stays in sys_email until the 18.x ceremony moves it (#24581), and those rows carry reset and verification links. Nobody has measured whether a non-administrator, an organization administrator, or any principal with a CRUD grant on sys_email can list them through /data/sys_email or a view.
Report whether a reset or verification link is readable by anyone who should not hold it.
If it is, propose the smallest fix and say what it closes. Candidate fixes: requiredPermissions on sys_email, an internal flag on the body fields, or redaction on read.
Priority p1 until measured, because a reset link is a credential.
Claim: PM loop round 12 · 2026-10-19T16:34Z
Session: session_01Y5583bs2RDd53MH6kM1ca3
Account: os-steve (the seat's linked user as GET /user answers it; the card's assignee)
Branch: none. This is a measure-only stage: no commit and no PR.
Worktree: the cloud session's own checkout (mode:cloud)
Domain: domain:services
Seat: epic:#15194 (the epic lane)
File surface: none written. The dev reads plugin-email, plugin-security, the /data door, views, export, search and the activity mirror, plus #24552's 18.0 fixture.
Container & model: S, mode:cloud, model: TIER_DEFAULT
Clause-②: no — measure only
Responsibility: n/a — a measured finding (the #24587 contract review, item ③5)
Thread-read: 6271215222
Serial constraints cleared:
Measured · epic PM session_01Y5583bs2RDd53MH6kM1ca3 (os-steve) · 2026-10-19T22:39Z. The measure-only dev session (session_01VQqYtVmS5pxercvwxnkDWM) ran these probes over #24552's 18.0 fixture under single, on main5e80009 (before #24587) and on #24587's head cd6fe0b4f. Its own write-up was not posted, so the seat records the result here. Credential values are deliberately left out: this card is public.
Verdict: exposed. A principal with organization-administrator rights and without manage_platform_settings can read identity mail that already sits in sys_email, including the bodies that carry reset and invitation links.
New identity mail goes to sys_identity_email. The same organization admin is refused there (403), and the mirrors and search return nothing for it.
What remains is (a) the identity rows already in sys_email from 18.0 / 18.0.0-next databases, and their activity and audit mirrors, and (b) a hygiene finding. The sys_activity and sys_audit_log mirrors of a sys_identity_email row hold a copy of its body. Only the platform administrator can read those copies, so they are no escalation, but they are a credential copied at rest.
Released line (read from the code, not measured): at @objectstack/plugin-email@17.3.0, sys_email declares no requiredPermissions and its body fields are not internal. Under ruling A of #21993, a fix reaches 17.x through the release/17.x branch that cloud pins. That branch does not exist yet; it is cut at the v18 opening, and this fix is owed there as a backport.
Next: the maintainer ordered the fix dispatched (「24625 这种你直接派发处理」). The claim follows.
Claim: epic seat round · 2026-10-19T22:40Z
Session: session_01Y5583bs2RDd53MH6kM1ca3
Account: os-steve (the card's assignee)
Branch: claude/issue-24625-identity-mail-read-gate
Worktree: the cloud session's own checkout (mode:cloud)
Domain: domain:services
Seat: epic:#15194 (the epic lane) The maintainer's order, verbatim: 「24625 这种你直接派发处理」. This claim takes the card from measurement to fix: the PR Closes #24625.
File surface, read on origin/main at or after bfc1539fe. The dev measures first; a breach stops the work and is explained in the report.
The read side of identity mail in sys_email: the object declaration (packages/platform-objects/src/audit/sys-email.object.ts), any read middleware or row/field gate plugin-email owns, and the search projection of sys_email.
The mirrors: the sys_activity and sys_audit_log writers (plugin-audit and the activity writer). Identity-mail bodies, from both sys_email and sys_identity_email, are never copied in clear; mask them as credential fields are masked.
Tests and fixtures for the principal × door matrix in the measured record above.
Steps 5+6 (claude/issue-24581-ceremony-open) touch the ceremony, not the read side. If both touch plugin-email, whichever lands second merges main.
Merge gate: the PR opens as a draft. The seat runs a CONTRACT_REVIEW_TIER review, and it lands with R1–R4 holding and the harness green.
os-dev-report
{
"issue": 24625,
"status": "done",
"branch": "claude/issue-24625-identity-mail-read-gate",
"pr": "#24688",
"session": "session_01CAtkMwk5tY1jPaUCdKosnk",
"premise_still_valid": true,
"summary": "Re-measured on main bfc1539 over the 18.0 fixture (one reset row, one organization-invitation row in sys_email): an organization admin without manage_platform_settings read the reset body on /data list, get and query, in global search, and in both mirrors; the writers stored identity and tenant mail bodies in clear (9 red / 11 green). No reliable identity-row selector exists: magic link and notification mail both carry no related_object and sys_email has no template column, so the fix is field-level. (1) body_text and body_html on sys_email and sys_identity_email declare requiredPermissions manage_platform_settings: the org admin keeps every row without the bodies; body filters are refused; search does not match; the mirror read redaction and the existing value-column guard close the mirrors. (2) plugin-audit's ledger view masks both logs' bodies like a credential field on every snapshot side. (3) A shipped defect the narrowing would trigger: sys_approval_request free-text search auto-included payload_json, which the snapshot guard refuses to any reader withheld any field, so the org admin's whole global search went 403; the object now declares searchableFields = auto-default minus payload_json. Changeset: minor x3, Clause-② yes (narrowing), before / now / remedy.",
"tests": "Head e7f2dd0. New dogfood identity-mail-read-gate (22 cases, principal x door, platform-admin controls, org-admin non-identity read): 22 passed; on unchanged main 9 red. R2 batch (upgrade-18x-harness, the new matrix, search-skip-unreadable, audit-log-admin-search, activity-text-predicate, signup-organization-census): 6 files, 62 passed. R3 = the harness's v17 case, green. R4 cli integration serve-walled-posture-refusal: 5 passed. R1 pnpm dev --fresh -p 38625: admin signs in, /data/sys_email, /data/sys_identity_email, /data/showcase_task, /search all 200, reset request 200, no error lines, stopped, port free. vitest: platform-objects 1196 passed, plugin-audit 935, plugin-approvals 1140, plugin-email 571; typecheck exit 0 for those four plus dogfood. Ablations via ablation-replace + rebuild + ablation-dist-preflight, each restored to the HEAD blob with git diff HEAD empty: A1 remove sys_email body requiredPermissions: 8 dogfood red, marker present in 4 built files, absent after restore. A2 disable the ledger mask: 4 plugin-audit unit red + 2 dogfood writer red; this leg's preflight exit was read through a pipe and is void, dist reach shown by the dogfood red; absent after restore exit 0. A3 put payload_json back in the approvals search set: 1 unit red + org-admin global search red, marker present then absent.",
"gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 74 commands on e7f2dd0; all 74 ran with exit codes written before any pipe; 72 exit 0 on the first pass, 2 exit 3 on prerequisites (check-plugin-teardown-shape --self-test needed its pinned fixture commit fetched; check:dual-build-cjs-loads needed a full pnpm build), both re-run exit 0. --ran reconciliation: 74 derived, 74 run, 0 NOT-MEASURED, 0 UNRUN. check-governed-merges --branch HEAD: NOT governed, 531 changed lines. CI on #24688: not yet read at report time (in_progress).",
"mcp_calls": "6 — issue_read x4 (#24625 get and comments, #24531 get and comments), plus ToolSearch loads; no MCP write tool used",
"api_writes": "2 relay strokes: (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/24688/assignees, read back identical, assignee os-steve; (2) this report, POST /repos//issues/24625/comments. git push not counted.",
"open_questions": [
{
"question": "Field-level gate hides the organization's own mail bodies (notifications, invitations) from the organization admin, not only identity bodies. Accept, or pursue a precise identity-row selector later?",
"options": ["A accept field-level (taken): no column distinguishes 18.0 magic-link rows from notification mail", "B after the 18.x ceremony moves identity rows out of sys_email (#24581), relax the sys_email body gate back to the tenant wall"],
"recommendation": "A now, B as a ceremony follow-up: once no identity row can remain in sys_email, the sys_email body gate has no reason left."
}
],
"out_of_scope_findings": [
"class: a · reach: exception security · evidence: plugin-email@17.3.0 sys_email has the same exposure; backport to release/17.x owed when that branch is cut (seat record 6278938192) · dedupe words: sys_email body backport, 17.x identity mail read gate",
"carrier: #24581 (承接者) · mirror rows written before this release still hold identity bodies at rest; served only to readers served the body; rewriting them belongs with the ceremony that moves the rows"
]
}
Landed and closed · epic PM session_01Y5583bs2RDd53MH6kM1ca3 (os-steve) · 2026-10-20T00:36Z. PR #24688 has merged as d1173825b.
reading
value
contract review
on PR #24688 at e7f2dd039, CONTRACT_REVIEW_TIER (narrowing face), PASS, no blocking items
matrix
22 cases green (9 red on main before the fix), with platform-admin and organization-admin-non-body controls
R1–R4
as the dev reported in 6279731318
Delivered.
body_text / body_html of sys_email and sys_identity_email are read only with manage_platform_settings. Every door the review checked is closed for every other principal: /data list, get, query and select, body filters, aggregate, global search (including the pinyin companion), both mirrors, export, views and related lists, analytics, and MCP.
The audit and activity writers mask both bodies on every snapshot side.
sys_approval_request declares its searchableFields, so an organization admin's global search no longer turns into a 403.
Activity
objectstack-fleet commentedon Oct 19, 2026
Claim: PM loop round 12 · 2026-10-19T16:34Z
Session:
session_01Y5583bs2RDd53MH6kM1ca3Account:
os-steve(the seat's linked user asGET /useranswers it; the card's assignee)Branch: none. This is a measure-only stage: no commit and no PR.
Worktree: the cloud session's own checkout (
mode:cloud)Domain:
domain:servicesSeat:
epic:#15194(the epic lane)File surface: none written. The dev reads
plugin-email,plugin-security, the/datadoor, views, export, search and the activity mirror, plus #24552's 18.0 fixture.Container & model:
S,mode:cloud,model: TIER_DEFAULTClause-②: no — measure only
Responsibility: n/a — a measured finding (the #24587 contract review, item ③5)
Thread-read: 6271215222
Serial constraints cleared:
objectstack-fleet commentedon Oct 19, 2026
Measured · epic PM
session_01Y5583bs2RDd53MH6kM1ca3(os-steve) · 2026-10-19T22:39Z. The measure-only dev session (session_01VQqYtVmS5pxercvwxnkDWM) ran these probes over #24552's 18.0 fixture undersingle, onmain5e80009(before #24587) and on #24587's headcd6fe0b4f. Its own write-up was not posted, so the seat records the result here. Credential values are deliberately left out: this card is public.Verdict: exposed. A principal with organization-administrator rights and without
manage_platform_settingscan read identity mail that already sits insys_email, including the bodies that carry reset and invitation links./datalist / get / querysys_activitymirrorsys_audit_logmirrormanage_platform_settingsSince #24587 (on
mainasfbafb8533):sys_identity_email. The same organization admin is refused there (403), and the mirrors and search return nothing for it.sys_emailfrom 18.0 / 18.0.0-next databases, and their activity and audit mirrors, and (b) a hygiene finding. Thesys_activityandsys_audit_logmirrors of asys_identity_emailrow hold a copy of its body. Only the platform administrator can read those copies, so they are no escalation, but they are a credential copied at rest.Released line (read from the code, not measured): at
@objectstack/plugin-email@17.3.0,sys_emaildeclares norequiredPermissionsand its body fields are notinternal. Under ruling A of #21993, a fix reaches 17.x through therelease/17.xbranch that cloud pins. That branch does not exist yet; it is cut at the v18 opening, and this fix is owed there as a backport.Next: the maintainer ordered the fix dispatched (「24625 这种你直接派发处理」). The claim follows.
objectstack-fleet commentedon Oct 19, 2026
Claim: epic seat round · 2026-10-19T22:40Z
Session:
session_01Y5583bs2RDd53MH6kM1ca3Account:
os-steve(the card's assignee)Branch:
claude/issue-24625-identity-mail-read-gateWorktree: the cloud session's own checkout (
mode:cloud)Domain:
domain:servicesSeat:
epic:#15194(the epic lane)The maintainer's order, verbatim: 「24625 这种你直接派发处理」. This claim takes the card from measurement to fix: the PR
Closes #24625.File surface, read on
origin/mainat or afterbfc1539fe. The dev measures first; a breach stops the work and is explained in the report.sys_email: the object declaration (packages/platform-objects/src/audit/sys-email.object.ts), any read middleware or row/field gate plugin-email owns, and the search projection ofsys_email.sys_activityandsys_audit_logwriters (plugin-audit and the activity writer). Identity-mail bodies, from bothsys_emailandsys_identity_email, are never copied in clear; mask them as credential fields are masked.docs/adr/**. ⛔ Notcontent/docs/releases/. ⛔ No credential value in any test name, fixture comment, log line or report.Container & model:
M,mode:cloud,model: TIER_DEFAULTClause-②: yes (narrowing). A principal loses read access to identity-mail bodies. The dev re-measures.
Responsibility: n/a. A measured security finding (p1) and the maintainer's order.
Serial constraints cleared:
claude/issue-24581-ceremony-open) touch the ceremony, not the read side. If both touch plugin-email, whichever lands second mergesmain.Merge gate: the PR opens as a draft. The seat runs a
CONTRACT_REVIEW_TIERreview, and it lands with R1–R4 holding and the harness green.objectstack-fleet commentedon Oct 19, 2026
os-dev-report
{
"issue": 24625,
"status": "done",
"branch": "claude/issue-24625-identity-mail-read-gate",
"pr": "#24688",
"session": "session_01CAtkMwk5tY1jPaUCdKosnk",
"premise_still_valid": true,
"summary": "Re-measured on main bfc1539 over the 18.0 fixture (one reset row, one organization-invitation row in sys_email): an organization admin without manage_platform_settings read the reset body on /data list, get and query, in global search, and in both mirrors; the writers stored identity and tenant mail bodies in clear (9 red / 11 green). No reliable identity-row selector exists: magic link and notification mail both carry no related_object and sys_email has no template column, so the fix is field-level. (1) body_text and body_html on sys_email and sys_identity_email declare requiredPermissions manage_platform_settings: the org admin keeps every row without the bodies; body filters are refused; search does not match; the mirror read redaction and the existing value-column guard close the mirrors. (2) plugin-audit's ledger view masks both logs' bodies like a credential field on every snapshot side. (3) A shipped defect the narrowing would trigger: sys_approval_request free-text search auto-included payload_json, which the snapshot guard refuses to any reader withheld any field, so the org admin's whole global search went 403; the object now declares searchableFields = auto-default minus payload_json. Changeset: minor x3, Clause-② yes (narrowing), before / now / remedy.",
"tests": "Head e7f2dd0. New dogfood identity-mail-read-gate (22 cases, principal x door, platform-admin controls, org-admin non-identity read): 22 passed; on unchanged main 9 red. R2 batch (upgrade-18x-harness, the new matrix, search-skip-unreadable, audit-log-admin-search, activity-text-predicate, signup-organization-census): 6 files, 62 passed. R3 = the harness's v17 case, green. R4 cli integration serve-walled-posture-refusal: 5 passed. R1 pnpm dev --fresh -p 38625: admin signs in, /data/sys_email, /data/sys_identity_email, /data/showcase_task, /search all 200, reset request 200, no error lines, stopped, port free. vitest: platform-objects 1196 passed, plugin-audit 935, plugin-approvals 1140, plugin-email 571; typecheck exit 0 for those four plus dogfood. Ablations via ablation-replace + rebuild + ablation-dist-preflight, each restored to the HEAD blob with git diff HEAD empty: A1 remove sys_email body requiredPermissions: 8 dogfood red, marker present in 4 built files, absent after restore. A2 disable the ledger mask: 4 plugin-audit unit red + 2 dogfood writer red; this leg's preflight exit was read through a pipe and is void, dist reach shown by the dogfood red; absent after restore exit 0. A3 put payload_json back in the approvals search set: 1 unit red + org-admin global search red, marker present then absent.",
"gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 74 commands on e7f2dd0; all 74 ran with exit codes written before any pipe; 72 exit 0 on the first pass, 2 exit 3 on prerequisites (check-plugin-teardown-shape --self-test needed its pinned fixture commit fetched; check:dual-build-cjs-loads needed a full pnpm build), both re-run exit 0. --ran reconciliation: 74 derived, 74 run, 0 NOT-MEASURED, 0 UNRUN. check-governed-merges --branch HEAD: NOT governed, 531 changed lines. CI on #24688: not yet read at report time (in_progress).",
"mcp_calls": "6 — issue_read x4 (#24625 get and comments, #24531 get and comments), plus ToolSearch loads; no MCP write tool used",
"api_writes": "2 relay strokes: (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/24688/assignees, read back identical, assignee os-steve; (2) this report, POST /repos//issues/24625/comments. git push not counted.",
"open_questions": [
{
"question": "Field-level gate hides the organization's own mail bodies (notifications, invitations) from the organization admin, not only identity bodies. Accept, or pursue a precise identity-row selector later?",
"options": ["A accept field-level (taken): no column distinguishes 18.0 magic-link rows from notification mail", "B after the 18.x ceremony moves identity rows out of sys_email (#24581), relax the sys_email body gate back to the tenant wall"],
"recommendation": "A now, B as a ceremony follow-up: once no identity row can remain in sys_email, the sys_email body gate has no reason left."
}
],
"out_of_scope_findings": [
"class: a · reach: exception security · evidence: plugin-email@17.3.0 sys_email has the same exposure; backport to release/17.x owed when that branch is cut (seat record 6278938192) · dedupe words: sys_email body backport, 17.x identity mail read gate",
"carrier: #24581 (承接者) · mirror rows written before this release still hold identity bodies at rest; served only to readers served the body; rewriting them belongs with the ceremony that moves the rows"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Landed and closed · epic PM
session_01Y5583bs2RDd53MH6kM1ca3(os-steve) · 2026-10-20T00:36Z. PR #24688 has merged asd1173825b.e7f2dd039,CONTRACT_REVIEW_TIER(narrowing face), PASS, no blocking itemsmainbefore the fix), with platform-admin and organization-admin-non-body controlsDelivered.
body_text/body_htmlofsys_emailandsys_identity_emailare read only withmanage_platform_settings. Every door the review checked is closed for every other principal:/datalist, get, query and select, body filters, aggregate, global search (including the pinyin companion), both mirrors, export, views and related lists, analytics, and MCP.sys_approval_requestdeclares itssearchableFields, so an organization admin's global search no longer turns into a 403.Carried:
sys_audit_log/sys_activitybefore this fix. Dispatched now. It is also the precondition for giving organization admins their own mail bodies back ("option B"), together with one exported identity selector.release/17.xis cut, under release: cut a release/17.x branch for cloud's pin at the v18 opening, with the backport rule — no 17.x npm publish lane (ruled; precondition for closing #15193) #21993's ruling.Clause-②: yes (narrowing); the reference spells a pure narrowingno (narrowing).The card closes
completed. The dev session is archived.