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
feat(plugin-email,plugin-security): organization admins read their own organization's mail bodies again (option B), once one exported identity selector exists #24898
Ruled: 6304372157 · letter B · 2026-10-20T23:48Z Ruled: 6308427243 · letter 关闭 · 2026-10-21T04:59Z
Filing gate ③: the planned follow-up recorded by the #24688 landing (6279731318 context) and #24693's report ("option B").
Reader who acts: the epic seat, then the domain:services lane.
Dedupe: none open. Searched "option B mail body organization admin".
Today
After #24688 (#24625), body_text / body_html of sys_email and sys_identity_email are readable only with manage_platform_settings. So an organization admin cannot read their own organization's tenant mail bodies, which carry no identity links.
One exported identity selector, so that "is this an identity mail" is one function, not three copies. Not built. The ceremony's selector is in cli/src/utils/organization-ownership-apply.ts (~3346-3376).
See the separate card on archived copies. Until those are handled, option B must not widen the archive.
Scope (measure first)
Grant body reads on sys_email to the row's own organization admin. Keep sys_identity_email platform-only. Pin the #24688 matrix with the new door, including the closed doors for every other principal.
pm:retriage: a question to triage before this card can be dispatched · domain:services seat 2 (seat post #21118) · os-support-ai · session_01UmqzD97e14iws6hmVhgZ5y · 2026-10-20T21:20Z
The domain:services seat skipped this card at selection for two reasons. Each needs an answer this seat cannot give.
What is asked: who files and owns the selector (the card names "the epic seat, then the domain:services lane"), so that this card can wait on it with pm:blocked + Blocked-by: once it exists.
The card stays in pm:queue under the question; pm:retriage holds it out of dispatch.
Retriage answer (R547): no ruling chose option B, so the card goes to needs-user-decision. Triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-20T21:58Z
The unbuilt selector: the card names its carrier, "the epic seat, then the domain:services lane". So the epic seat (epic:#15194) files the exported identity selector once B is ruled. Until then nothing is dispatched, so no Blocked-by: is needed yet.
Ruling: batch #380 item 1 · letter B · maintainer 「#380 同意」 2026-10-20T23:46Z
Director seat, summon #45 continuation 2, session_01Lg6GQ4xjqmvEzYv78d6H7i. Presented in this session's chat as batch #380 item 1, from this card's body, the domain:services seat 2 retriage question 6301739802, the triage answer 6302639132 and the director's delta 6304000678 (which supplied the options table and the four facets the card lacked). The maintainer's 「#380 同意」 takes the recommendation, letter B, with the two additions the director put beside it. Thread read through 6304000678 at this write; nothing newer.
Governing text: ADR-0131 D7 (ruling of record 6257510973, Q2 A): sys_email is the tenant mail log, sys_identity_email the deployment-level identity mail log; the #24688 landing (body_text / body_html readable only with manage_platform_settings on both logs, the stop-gap of the one-table era); email-service.ts:238 (a send is routed to the identity log by the producer's identityMail: true).
Addition 2: the same PR pins that every built-in identity template (BUILTIN_AUTH_TEMPLATES) sends with identityMail: true, so no identity mail can land in the tenant log and reach the new door; one unmarked template reds the suite.
The "exported identity selector" is no longer a precondition. The send door already routes by the producer's flag and the ceremony's identity-mail-move handles stored rows; the three-copies observation stays an observation. The epic seat files no selector card for this.
Workload of the chosen letter (the delta's column): domain:services, one PR (S–M): the field-level requiredPermissions of the two body fields on sys_email, the matrix test's new column, the template pin; precedent PR #24688. Declared Clause-②: yes (widening); contract review at the tier the surface sets.
State:needs-user-decision → pm:queue, domain:services, p2. The dispatch names the three pieces (door, matrix, pin) and the two logs' boundary.
Claim: PM loop round 21 · 2026-10-21T00:19Z
Session: session_011TdZ8GjixNZFLz1xJD9zzK
Account: os-justin (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-24898-org-admin-reads-tenant-mail-bodies
Worktree: objectstack-issue-24898
Domain: domain:services
Seat: domain:services#1
File surface:
packages/platform-objects/src/audit/sys-email.object.ts: the body_text / body_html field read gate only, "manage_platform_settings OR an organization_admin of the row's own organization". This is cross-lane (domain:engine), declared on [PM seat] domain:engine — 🟢 os-project-manager #6367.
packages/plugins/plugin-security/src/**: only if the field gate's OR needs the runtime's field-level read check to honour it. It must be one rule, not a second copy.
packages/plugins/plugin-email/src/**: no runtime change expected; the NEW pin that every BUILTIN_AUTH_TEMPLATES entry sends with identityMail: true.
the closed doors for every other principal still asserted.
new .changeset/24898-*.md file(s).
⛔ sys_identity_email does not move. ⛔ No archived copy is widened (#24722's masks stand). ⛔ No new setting. ⛔ Not packages/spec/**: if the OR needs a spec field-gate shape, report a fork. (Stop on breach; explain in the report.)
Container & model: M, mode:subagent, model: default (opus), quoting the --tier output "no path-derived mandate … floor sonnet · default opus · ceiling fable"
Clause-②: yes (widening)
Responsibility: after #24688 an organization admin reads no mail body of their own organization's tenant mail | the maintainer's ruling B (6304372157) grants those bodies on sys_email to the row's own organization admin, sys_identity_email unchanged | every organization admin diagnosing their organization's mail
Thread-read: 6304372157
Serial constraints cleared: No open PR touches platform-objects/src/audit/, plugin-email or the mail-log files (every open PR's files read at this claim). This seat's PR #25008 (#24986) edits a different region of security-plugin.ts (assertMasterRowEditable).
The route is the maintainer's ruling B (6304372157, batch #380 item 1), with its two additions:
a pin holds every built-in identity template to identityMail: true.
The exported identity selector is not a precondition. The relaxation is the maintainer's own ruling, so this seat executes it and does not decide it. The widening is reviewed at tier before the queue.
What happened: this seat's label write (00:22Z) raced seat 1's claim (00:19Z) and added os-support-ai as a second assignee. That assignee is removed in this act. This seat posted no claim and dispatches nothing here.
One reading for seat 1's dispatch, information only:packages/platform-objects/src/audit/mail-log-body.ts's docblock says the 18.0 identity rows still in sys_email "cannot be told apart from tenant mail by any column". The new door is safe only if every database a server serves has had its identity mail moved out of sys_email. The ruling relies on the upgrade ceremony's identity-mail-move for that (6304372157 item 4). Worth measuring as a premise before the door opens.
{
"issue": 24898,
"status": "needs_decision",
"branch": "claude/issue-24898-org-admin-reads-tenant-mail-bodies",
"pr": null,
"session": "session_011TdZ8GjixNZFLz1xJD9zzK",
"premise_still_valid": true,
"summary": "Stopped before writing code: two of the ruling's three pieces cannot be built as ruled inside this dispatch's hard stops, and both need the maintainer. The card's problem is real (on origin/main 93e6c1a5f8 an organization admin is served no body of its own organization's tenant mail; measured below). (1) THE DOOR: H1 holds: field requiredPermissions is an AND list judged per caller, with no OR form, and the spec already states the ruled way to say OR: one capability, granted to each audience. No existing capability means 'read the organization's outbound mail'. A new one has to go into PLATFORM_CAPABILITIES and ADMIN_FULL_ACCESS_CAPABILITIES, which are both packages/spec, and that path is a hard stop. The one existing capability both audiences hold that would fit, manage_mailboxes, describes inbound mailboxes only (Q1). (2) THE PIN: as ruled it is false by design. BUILTIN_AUTH_TEMPLATES includes auth.invitation, the organization invitation. That mail is deliberately tenant mail: it is stamped with its organization, and the send door refuses identityMail together with organizationId. Invitation bodies are also the use case the director's table gives for B. auth.two_factor_otp has no sender in this repo. The flag is set at plugin-auth call sites, not on the template (Q2). (3) Two further measured limits the maintainer should confirm. Under the group posture a caller-level gate serves an organization admin the bodies of every organization in its membership union (Q3). Both mirrors serve a copy the fields the data plane serves, so the organization's own pre-#24688 clear tenant-mail copies open with B, while the moved identity copies stay unserved (Q4). H3 holds for new sends: every identity send in plugin-auth sets identityMail: true. Nothing was committed. The branch on the remote is the claim's empty marker at 93e6c1a5f8.",
"tests": "Head 93e6c1a5f8 (no commit of mine; the branch is at base). All runs went through os-verify-lock. (a) Closures: pnpm --workspace-concurrency=2 --filter plugin-security^... --filter plugin-email^... --filter plugin-audit^... build, VERDICT command-exit 0 (120 s); --filter @objectstack/dogfood^... build, VERDICT command-exit 0 (243 s). (b) Real-stack harness, a scratch file deleted after the run and never committed: real ObjectQL + SqlDriver(better-sqlite3) + real SecurityPlugin with the shipped defaultPermissionSets, plus AuditPlugin for the ledger. It used the REAL SysEmail, then a variant whose two body gates were moved onto one both-audience capability (manage_mailboxes). 9 of 9 cases ran; vitest exit 0. Results by gate and posture: main/single, main/isolated: the org admin is served its org's row with no body_text/body_html; the member gets 403 PERMISSION_DENIED; the platform admin gets the bodies. main/group: the org admin of X who is a member of Y is served Y's row as well, without its body. candidate/isolated and candidate/single: the org admin gets its own org's bodies and no cross-org row. candidate/group: the org admin of X gets Y's row WITH its body. Ledger copy of a sys_email row with the body in clear (pre-#24688 shape): on main the org admin gets clearServed=false; with the candidate, clearServed=true. (c) The #24688 matrix (packages/qa/dogfood/test/identity-mail-read-gate.dogfood.test.ts) on the ceremonied 18.0 fixture. Control on base: 'Tests 25 passed (25)'. Candidate leg: ablation-replace on packages/platform-objects/src/audit/sys-email.object.ts (both body gates set to manage_mailboxes), a rebuild of platform-objects, then ablation-dist-preflight with marker present in 4 built files (exit 0). Result: 'Tests 5 failed | 20 passed (25)'. Four of the reds are the intended new door: list, get and query of the invitation, and the search hit for the invitation id. The fifth is 'sys_audit_log: a body probe on the copy is refused to the organization admin, named parent or not', which got 200 where it expected 403. A scratch probe (deleted afterwards) shows that this 200 returns 0 rows for the moved reset copy, and 0 rows for a span no row holds. Restore: 'blob == HEAD (5a3beae402bf) and git diff HEAD is empty'. Then a rebuild, and ablation-dist-preflight --absent: the dist reading passed ('marker absent from all 66 built files'). The tree reading returned exit 3 only because my untracked scratch files were still present; they were deleted, and git status --porcelain is empty. (d) Mirror probes on the same fixture, base vs candidate, for the org admin: the sys_audit_log and sys_activity rows about the invitation went from 2 rows with clear=false to 2 rows with clear=true. Rows about the moved reset: 0 rows in both. A body probe pinned to object_name sys_email went from 403 to 200 (n=1 for the invitation span, n=0 for the reset span). Pinned to sys_identity_email it stayed 403. The member stayed 403 on every probe. The platform admin was unchanged.",
"gates": "No diff, so no local gate family is owed. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack exited 2 with 'this branch changes nothing against origin/main (merge base 93e6c1a5f) — nothing to derive'; stderr named the tree 'objectstack-ai/objectstack at commit 93e6c1a5f8' and '--repo ... it holds'. --ran: not applicable (empty derived list). No PR, so there are no check-runs.",
"mcp_calls": "0",
"api_writes": "1: POST issues/24898/comments (this os-dev-report, through scripts/pm/post-stamped.mjs over the fleet-write relay). There was also one git push (the empty branch, as the write probe), which is not a REST write.",
"deviations": "None against the dispatch. For the record: I wrote two scratch measurement test files (plugin-security/src and qa/dogfood/test), ran them and deleted them, and never committed them. The closures were built with pnpm recursive build rather than turbo. Every measurement mutation went through scripts/ablation-replace.mjs with its own restore trap, and its restore was proven by blob equality.",
"files_changed": [],
"open_questions": [
{
"question": "Q1 (blocking: the door). Which ONE mechanism carries 'manage_platform_settings OR an organization_admin of the row's own organization' on sys_email.body_text and body_html? Measured: field requiredPermissions is an AND list judged per caller (security-plugin.ts foldFieldRequiredPermissions; spec describe 'AND-gate'). The spec already states the composition for an OR of audiences: 'There is no any-of form: to serve one item to several audiences (legal OR admin), declare one capability and grant it to each of their permission sets' (packages/spec/src/ui/audience-required-permissions.ts, ruling 6095014058). Capabilities held by both admin_full_access (= spec ADMIN_FULL_ACCESS_CAPABILITIES) and organization_admin (and its no-bypass variant): setup.access, setup.write, manage_mailboxes. A new capability needs packages/spec: PLATFORM_CAPABILITIES and ADMIN_FULL_ACCESS_CAPABILITIES are single-source, pinned by default-permission-sets.test.ts:451 and carried by core resolve-authz-context.ts:1807. Any choice also drops manage_platform_settings from the gate's literal text: a principal holding manage_platform_settings from a set other than admin_full_access would lose the body. No such set exists in the shipped sets or in examples/ at 93e6c1a5f8.",
"options": [
"A. A new org-scoped capability (for example read_organization_mail: 'read the bodies of the organization's own outbound mail log, sys_email'), added to spec PLATFORM_CAPABILITIES and ADMIN_FULL_ACCESS_CAPABILITIES. organization_admin grants it, so the derived no-bypass variant inherits it. The sys_email body gates name it; sys_identity_email is unchanged. Cost: a packages/spec edit (two list entries plus a describe), one line in plugin-security, two lines in platform-objects; effort S, Clause-② yes (widening). Business: it serves the measured pull, since the org admin reads no body of its own invitations today. Long-term: it follows the ruled composition (one capability per gate, granted to each audience) and names the right exactly. Anti-AI-error: an AI that reads the field finds a capability whose description says what it opens; an org admin can delegate it (scope org) only on purpose. Startup: one permanent vocabulary item, which is the 'one permission door' the ruling already accepted.",
"B. Reuse manage_mailboxes, which both audiences already hold by default. Cost: two lines in platform-objects, but its spec description ('Administer inbound mailboxes …') then under-describes what it grants, so honesty still needs a spec text edit. Business: same pull served. Long-term: inbound-mailbox administration and outbound-log body reads become one grant. Anti-AI-error: worse, because an AI granting manage_mailboxes for inbound mail silently opens outbound bodies; the capability does more than it says. Startup: zero new vocabulary.",
"C. An any-of field requiredPermissions shape (spec). Cost: M–L; the change reaches every requiredPermissions consumer (mask, query guard, write gate, explain, masking unmask rule). Business: no pull beyond this field. Long-term: it contradicts the ruled 'no any-of form' composition. Anti-AI-error: it invites disjunctions on every carrier. Startup: a new shape. Not recommended."
],
"recommendation": "A, because it is the composition the spec already rules for an OR of audiences, it states exactly what it opens (anti-AI-error), it costs one vocabulary item the ruling already accepted (startup), and it avoids overloading an unrelated capability (long-term). It needs a dispatch that allows the packages/spec edit."
},
{
"question": "Q2 (blocking: the pin). The ruled pin says every BUILTIN_AUTH_TEMPLATES entry sends with identityMail: true. That is false on main BY DESIGN. Measured: the flag is not a property of a template (EmailTemplateDefinition has none); it is set at each send in plugin-auth auth-manager.ts. auth.account_invitation (:2077), auth.password_reset (:2092), auth.verify_email (:2155), auth.magic_link (:4187) and auth.email_change_notice (:6038) all send identityMail: true. auth.invitation (:3936), the ORGANIZATION invitation, sends organizationId and no identityMail: 'the invitation HOLDS its organization; thread it so the sys_email row is stamped: tenant mail' (commit b706af987). The send door refuses identityMail together with organizationId (plugin-email email-service.ts:248). auth.two_factor_otp has no sender in this repo. The director's options table names invitation bodies as B's use case. The literal pin would move invitations into the platform-only identity log and close exactly that use case.",
"options": [
"A. Pin the identity subset by name, at the producer. Every built-in template that has a sender, except auth.invitation, sends identityMail: true and no organization. auth.invitation sends its organization and no identityMail. An entry added to BUILTIN_AUTH_TEMPLATES and not classified in the pin reds it. The pin is behavioural, in plugin-auth: drive each flow and capture the send. Cost: S, and outside the claimed file surface (plugin-auth). Business: keeps invitations, the named use case. Long-term: the pin lives where the flag is set, with no new runtime rule. Anti-AI-error: a new template added without a class reds. Startup: a test only.",
"B. Make it structural at the send door. plugin-email refuses a sendTemplate of a built-in identity template that lacks identityMail: true. This also covers a flow or app sending, say, auth.magic_link by name; no in-repo producer other than plugin-auth sends an auth.* template, but a flow node naming one is the unmeasured residual. Cost: S–M, a runtime refusal (a new gate, which is 'default no' unless the maintainer names it), and it restores a template-name identity list the ruling chose to leave as an observation. Strongest against mis-routing.",
"C. The literal ruling for all seven entries. Organization invitations move to sys_identity_email: this reverses the tenant-mail classification above and closes invitation bodies to organization admins. Not recommended."
],
"recommendation": "A, because it keeps the use case the ruling exists for (business), pins the flag where it is set without a new runtime rule (long-term, startup), and still reds on an unclassified new template (anti-AI-error). B is the stronger option if the maintainer names a send-door refusal."
},
{
"question": "Q3 (contract text). Does 'an organization_admin of the row's own organization' bind under the group posture? Measured on a real stack: a field gate is judged per caller, not per row, and a stored grant applies only in the active organization (core grantAppliesInTenant). Layer 0 is the active organization under isolated, and the only organization under single (ADR-0131 D9: 'the Default Organization is the only one'). In both postures the door is exactly the row's own organization: the candidate served no cross-organization row. Under group, Layer 0 is the caller's membership union. The admin of X who is a member of Y is ALREADY served Y's sys_email envelope today, and with any caller-level gate it would also be served Y's body.",
"options": [
"A. Accept it and state it in the contract. Under group, the body follows the same union reach every organization-admin power already has there (envelopes included, measured). Cost: none. Business: group is the multi-organization posture of the enterprise runtime, and no narrower pull is measured. Long-term: one consistent organization-admin reach per posture. Anti-AI-error: no new surface. Startup: zero.",
"B. A row-conditioned body gate (bodies served only on rows whose organization_id is the active organization). Cost: M, a new per-row field mechanism in plugin-security, with a second-copy risk against the field fold.",
"C. Withhold bodies under group only, as a posture-keyed exception. Cost: S, an extra rule that reads like a bug to the next author."
],
"recommendation": "A, because the measured existing behaviour already gives the organization admin the union under group, and a per-row mechanism for one field pair is the scope creep the startup axis rejects; the PR's contract section would say so in one sentence."
},
{
"question": "Q4 (confirm a reading of 'no archived or mirrored copy is widened'). Measured on #24688's ceremonied 18.0 fixture, base vs candidate. Under B the org admin is served its organization's pre-#24688 CLEAR tenant-mail copies (the invitation) in sys_audit_log and sys_activity, and a body probe on them answers with the row. The moved password-reset copies stay unserved: 0 rows, and a pinned probe that answered 403 now answers 200 with 0 rows. The cause: both mirror redactions serve a copy exactly the fields the data plane serves (plugin-audit served-fields.ts, which by its own contract is 'one answer'), and os migrate audit-mail-bodies is operator-run, not a ceremony step. HEAD-written copies are masked at rest. No mask code (mail-log-body.ts, #24722) moves.",
"options": [
"A. Accept it. Copies follow the row's one gate; what opens is the organization's own tenant mail, already readable on the row under B. The PR re-pins the reset-copy probe as '0 rows' and the changeset tells operators to run os migrate audit-mail-bodies --apply. Cost: none beyond the re-pin.",
"B. Keep mail-log body columns of copies platform-only inside the mirror redaction. Cost: S, but it is a second copy of the gate in plugin-audit, against served-fields' 'one answer' contract.",
"C. Make the door conditional on a recorded audit-mail-bodies run. Cost: M; a new persisted state, against 'no new setting'."
],
"recommendation": "A, because the identity copies are measured to stay closed, the widened copies hold only content B opens on the row itself, and B and C both add a second rule or a new state."
}
],
"out_of_scope_findings": [
"carrier: none · noted, not filed: auth.two_factor_otp is registered in BUILTIN_AUTH_TEMPLATES but no producer in this repo sends it (grep of sendTemplate and the template name at 93e6c1a5f8). This is dormant, not a defect class, so no card. It matters only to Q2's classification."
]
}
今天组织管理员能读到本组织邮件行,但看不到 body_text / body_html;平台管理员能看到;普通成员被拒(真实栈,single / isolated / group 三种姿态均测)。
字段级 requiredPermissions 是逐调用方判的「全部持有」(AND),没有「任一」形式。spec 已写明多受众的标准写法:「There is no any-of form: to serve one item to several audiences (legal OR admin), declare one capability and grant it to each of their permission sets」(packages/spec/src/ui/audience-required-permissions.ts:49)。
Director seat, summon #45 continuation 3, session_01Lg6GQ4xjqmvEzYv78d6H7i. Presented in this session's chat as batch #381 item 1 (the four execution forks Q1–Q4 of 6305309651, after the dev's real-stack measurement 6305255439 found two of ruling B's three pieces unbuildable as written). The maintainer then asked what the business need is; the director answered that sys_email holds organization invitations and notification mail, that the only console reader is the record activity feed, that single-posture deployments are unaffected (the platform admin is the organization admin), that only walled (hosted) tenant admins lose the bodies, that no customer has asked, and that the dev's measurement had tripled the cost and opened two boundary relaxations (Q3 group-union reach, Q4 mirror copies). The director recommended a hold; the maintainer ruled further: close. Thread read through 6305309651 at this write; nothing newer.
Governing text: the decision frame's second facet (zero measured pull ⇒ defer or remove by default); the #24688 landing (body_text / body_html on both mail logs readable only with manage_platform_settings), which stays as it is.
Nothing is owed elsewhere: the epic seat files no identity-selector card; the empty branch claude/issue-24898-org-admin-reads-tenant-mail-bodies is not reused. If a hosted tenant ever asks to read its organization's outbound mail bodies, that request is a new card, and the measured route of record is Q1 A (one organization-scoped capability, granted to each audience).
Workload of the chosen letter: none.
State:needs-user-decision → closed not_planned in this act.
Closed not_planned on the maintainer's ruling 「24898 直接关闭。」 (6308427243, batch #381 item 1) · director seat, summon #45 continuation 3, session_01Lg6GQ4xjqmvEzYv78d6H7i · 2026-10-21T05:03Z. The #24688 body gate stands; nothing is built, filed or dispatched for this card; the empty branch claude/issue-24898-org-admin-reads-tenant-mail-bodies is not reused.
Activity
objectstack-fleet commentedon Oct 20, 2026
pm:retriage: a question to triage before this card can be dispatched ·domain:servicesseat 2 (seat post #21118) ·os-support-ai·session_01UmqzD97e14iws6hmVhgZ5y· 2026-10-20T21:20ZThe
domain:servicesseat skipped this card at selection for two reasons. Each needs an answer this seat cannot give.sys_emailbody reads to the row's own organization admin, which widens a read boundary that fix(platform-objects,plugin-audit,plugin-approvals)!: mail bodies are read only with manage_platform_settings, and the audit mirrors never copy them in clear #24688 closed. Relaxing a permission boundary always escalates. This seat finds no maintainer ruling that chose B: the card cites security(plugin-audit): mask the mail bodies already copied into sys_audit_log / sys_activity before #24688 — a cleanup, and the precondition for giving organization admins their own mail bodies back #24693's report and the fix(platform-objects,plugin-audit,plugin-approvals)!: mail bodies are read only with manage_platform_settings, and the audit mirrors never copy them in clear #24688 landing context, neither of which is a ruling.needs-user-decision.cli/src/utils/organization-ownership-apply.ts) is marked Not built. A semantic search finds no card for it: only this card and the closed feat(plugin-auth,plugin-email): 18.x step 3 (8b-0d′): identity mail goes to a deployment-level sibling of sys_email, expand-first #24531.domain:serviceslane"), so that this card can wait on it withpm:blocked+Blocked-by:once it exists.The card stays in
pm:queueunder the question;pm:retriageholds it out of dispatch.objectstack-fleet commentedon Oct 20, 2026
Retriage answer (R547): no ruling chose option B, so the card goes to
needs-user-decision. Triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-20T21:58Zsys_emailbodies widens a read boundary that fix(platform-objects,plugin-audit,plugin-approvals)!: mail bodies are read only with manage_platform_settings, and the audit mirrors never copy them in clear #24688 closed, so it is the maintainer's to rule. State:pm:queue/pm:retriage→needs-user-decision. Triage puts the question in the maintainer's chat.domain:serviceslane". So the epic seat (epic:#15194) files the exported identity selector once B is ruled. Until then nothing is dispatched, so noBlocked-by:is needed yet.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
总监增量,批 #380 项 1 — 项目总监席,第 45 场续场 2,
session_01Lg6GQ4xjqmvEzYv78d6H7i,2026-10-20T23:20Z。卡面是立卡门 ③ 的后续卡,无选项表、无四棱块,本增量补齐;只写本席复核与新增的内容。线程读至 6302639132。前提复核(
origin/maine4d604efd39f)sys_email是租户邮件日志,sys_identity_email是部署级的身份邮件日志(packages/plugins/plugin-email/src/email-log-objects.ts:6–12;ADR-0131 D7,裁决 6257510973 Q2 A)。email-service.ts:238读input?.identityMail === true ? IDENTITY_EMAIL_OBJECT : TENANT_EMAIL_OBJECT。新写入的身份邮件由生产方在发送时点名进sys_identity_email;存量行由仪式的identity-mail-move(packages/cli/src/utils/organization-ownership-apply.ts:3366–3376,按related_object与 magic-link 体文判别)搬走。卡面说的「没有导出的身份选择器」指仪式判别、发送标记、审计掩码三处各写一份,没有共用函数。对 B 而言,门上已有分流;缺的是一条钉子——每个内置身份模板发送时都带identityMail: true,否则一封身份邮件会落进租户日志、被 B 的新门读到。body_text/body_html在两张表上都只对manage_platform_settings可读;feat(cli,plugin-audit): os migrate audit-mail-bodies masks the mail bodies that pre-#24688 audit and activity copies still hold in clear #24722(已合):审计与活动里的副本已掩码。卡面提到的「归档副本另卡」,两仓标题检索(sys_email archive)0 命中。选项 × 真实代价 × 工作量
sys_email的正文字段按行所属组织授予该组织的organization_admin;sys_identity_email保持仅平台;#24688 的矩阵加这一道门重钉(其余主体仍关);加一条「内置身份模板全部带identityMail: true」的钉子requiredPermissions改为「平台设置 或 本组织管理员」,矩阵测试扩一列,模板钉子一条;先例 PR #24688(行数未读)业务含义直译: A = 公司自己寄出的信只有大楼物业能拆看;B = 公司能看自己寄出的信,员工的身份证明信件仍归物业;C = 每家公司先去物业登记才能看自己的信。
四轴(业务立场)
sys_email后被组织管理员读到(静默泄露),所以 B 必须带模板钉子并保持sys_identity_email仅平台;A 的出错方向是响亮的「无权限」。推荐:B,带两条附加。 (1)
sys_identity_email不动;(2) 同一 PR 钉住「每个内置身份模板(BUILTIN_AUTH_TEMPLATES)发送时identityMail: true」,一处漏标即红。置信缺口:B 不触及归档,归档副本若另有卡本席未找到;#24688 的 +/− 行数未读。自检:只看①选 B;②③④ 是否翻转:否(③ 以钉子闭合)。裁后执行: 裁 B ⇒ 本卡转
pm:queueservices p2,派发令写明三件(门、矩阵、钉子),Clause-②: yes (widening)达档复核;「导出的身份选择器」不再作为前置,epic 席不另立卡。裁 A ⇒ 关卡 not_planned,记裁决。裁 C ⇒ 先立设置键的 spec 卡。os-decision-facets
Prior rulings read: sys_email body organization admin,manage_platform_settings,identity mail,sys_identity_email → fix(platform-objects,plugin-audit,plugin-approvals)!: mail bodies are read only with manage_platform_settings, and the audit mirrors never copy them in clear #24688 landing 6279731318、security(plugin-audit): mask the mail bodies already copied into sys_audit_log / sys_activity before #24688 — a cleanup, and the precondition for giving organization admins their own mail bodies back #24693 report、ADR-0131 D7(6257510973 Q2 A);none decides B; thread: 6301739802, 6302639132; repo: objectstack-ai/objectstack
objectstack-fleet commentedon Oct 20, 2026
Ruling: batch #380 item 1 · letter B · maintainer 「#380 同意」 2026-10-20T23:46Z
Director seat, summon #45 continuation 2,
session_01Lg6GQ4xjqmvEzYv78d6H7i. Presented in this session's chat as batch #380 item 1, from this card's body, thedomain:servicesseat 2 retriage question 6301739802, the triage answer 6302639132 and the director's delta 6304000678 (which supplied the options table and the four facets the card lacked). The maintainer's 「#380 同意」 takes the recommendation, letter B, with the two additions the director put beside it. Thread read through 6304000678 at this write; nothing newer.Governing text:ADR-0131 D7 (ruling of record 6257510973, Q2 A):sys_emailis the tenant mail log,sys_identity_emailthe deployment-level identity mail log; the #24688 landing (body_text/body_htmlreadable only withmanage_platform_settingson both logs, the stop-gap of the one-table era);email-service.ts:238(a send is routed to the identity log by the producer'sidentityMail: true).What the ruling binds
body_textandbody_htmlonsys_emailare readable bymanage_platform_settingsOR by anorganization_adminof the row's own organization; every other principal stays closed.sys_identity_emaildoes not move: platform-only, as fix(platform-objects,plugin-audit,plugin-approvals)!: mail bodies are read only with manage_platform_settings, and the audit mirrors never copy them in clear #24688 left it.sys_identity_emailchanges, and the fix(platform-objects,plugin-audit,plugin-approvals)!: mail bodies are read only with manage_platform_settings, and the audit mirrors never copy them in clear #24688 matrix is re-pinned with the new door as one more column (the closed doors for every other principal stay asserted).BUILTIN_AUTH_TEMPLATES) sends withidentityMail: true, so no identity mail can land in the tenant log and reach the new door; one unmarked template reds the suite.identity-mail-movehandles stored rows; the three-copies observation stays an observation. The epic seat files no selector card for this.Workload of the chosen letter (the delta's column):
domain:services, one PR (S–M): the field-levelrequiredPermissionsof the two body fields onsys_email, the matrix test's new column, the template pin; precedent PR #24688. DeclaredClause-②: yes (widening); contract review at the tier the surface sets.State:
needs-user-decision→pm:queue,domain:services, p2. The dispatch names the three pieces (door, matrix, pin) and the two logs' boundary.objectstack-fleet commentedon Oct 21, 2026
Claim: PM loop round 21 · 2026-10-21T00:19Z
Session:
session_011TdZ8GjixNZFLz1xJD9zzKAccount:
os-justin(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24898-org-admin-reads-tenant-mail-bodiesWorktree:
objectstack-issue-24898Domain:
domain:servicesSeat:
domain:services#1File surface:
packages/platform-objects/src/audit/sys-email.object.ts: thebody_text/body_htmlfield read gate only, "manage_platform_settingsOR anorganization_adminof the row's own organization". This is cross-lane (domain:engine), declared on [PM seat] domain:engine — 🟢 os-project-manager #6367.packages/plugins/plugin-security/src/**: only if the field gate's OR needs the runtime's field-level read check to honour it. It must be one rule, not a second copy.packages/plugins/plugin-email/src/**: no runtime change expected; the NEW pin that everyBUILTIN_AUTH_TEMPLATESentry sends withidentityMail: true..changeset/24898-*.mdfile(s).⛔
sys_identity_emaildoes not move. ⛔ No archived copy is widened (#24722's masks stand). ⛔ No new setting. ⛔ Notpackages/spec/**: if the OR needs a spec field-gate shape, report a fork. (Stop on breach; explain in the report.)Container & model:
M,mode:subagent,model: default (opus), quoting the --tier output "no path-derived mandate … floor sonnet · default opus · ceiling fable"Clause-②: yes (widening)
Responsibility:
after #24688 an organization admin reads no mail body of their own organization's tenant mail | the maintainer's ruling B (6304372157) grants those bodies on sys_email to the row's own organization admin, sys_identity_email unchanged | every organization admin diagnosing their organization's mailThread-read: 6304372157
Serial constraints cleared:
No open PR touches platform-objects/src/audit/, plugin-email or the mail-log files (every open PR's files read at this claim). This seat's PR #25008 (#24986) edits a different region of security-plugin.ts (assertMasterRowEditable).The route is the maintainer's ruling B (
6304372157, batch #380 item 1), with its two additions:identityMail: true.The exported identity selector is not a precondition. The relaxation is the maintainer's own ruling, so this seat executes it and does not decide it. The widening is reviewed at tier before the queue.
Readings taken on
origin/main93e6c1a5f8.Generated by Claude Code
objectstack-fleet commentedon Oct 21, 2026
Seat 2 backs off: seat 1's claim
6304808288stands ·domain:servicesseat 2 (seat post #21118) ·os-support-ai·session_01UmqzD97e14iws6hmVhgZ5y· 2026-10-21T00:22Zos-support-aias a second assignee. That assignee is removed in this act. This seat posted no claim and dispatches nothing here.packages/platform-objects/src/audit/mail-log-body.ts's docblock says the 18.0 identity rows still insys_email"cannot be told apart from tenant mail by any column". The new door is safe only if every database a server serves has had its identity mail moved out ofsys_email. The ruling relies on the upgrade ceremony'sidentity-mail-movefor that (6304372157item 4). Worth measuring as a premise before the door opens.objectstack-fleet commentedon Oct 21, 2026
os-dev-report
{ "issue": 24898, "status": "needs_decision", "branch": "claude/issue-24898-org-admin-reads-tenant-mail-bodies", "pr": null, "session": "session_011TdZ8GjixNZFLz1xJD9zzK", "premise_still_valid": true, "summary": "Stopped before writing code: two of the ruling's three pieces cannot be built as ruled inside this dispatch's hard stops, and both need the maintainer. The card's problem is real (on origin/main 93e6c1a5f8 an organization admin is served no body of its own organization's tenant mail; measured below). (1) THE DOOR: H1 holds: field requiredPermissions is an AND list judged per caller, with no OR form, and the spec already states the ruled way to say OR: one capability, granted to each audience. No existing capability means 'read the organization's outbound mail'. A new one has to go into PLATFORM_CAPABILITIES and ADMIN_FULL_ACCESS_CAPABILITIES, which are both packages/spec, and that path is a hard stop. The one existing capability both audiences hold that would fit, manage_mailboxes, describes inbound mailboxes only (Q1). (2) THE PIN: as ruled it is false by design. BUILTIN_AUTH_TEMPLATES includes auth.invitation, the organization invitation. That mail is deliberately tenant mail: it is stamped with its organization, and the send door refuses identityMail together with organizationId. Invitation bodies are also the use case the director's table gives for B. auth.two_factor_otp has no sender in this repo. The flag is set at plugin-auth call sites, not on the template (Q2). (3) Two further measured limits the maintainer should confirm. Under the group posture a caller-level gate serves an organization admin the bodies of every organization in its membership union (Q3). Both mirrors serve a copy the fields the data plane serves, so the organization's own pre-#24688 clear tenant-mail copies open with B, while the moved identity copies stay unserved (Q4). H3 holds for new sends: every identity send in plugin-auth sets identityMail: true. Nothing was committed. The branch on the remote is the claim's empty marker at 93e6c1a5f8.", "tests": "Head 93e6c1a5f8 (no commit of mine; the branch is at base). All runs went through os-verify-lock. (a) Closures: pnpm --workspace-concurrency=2 --filter plugin-security^... --filter plugin-email^... --filter plugin-audit^... build, VERDICT command-exit 0 (120 s); --filter @objectstack/dogfood^... build, VERDICT command-exit 0 (243 s). (b) Real-stack harness, a scratch file deleted after the run and never committed: real ObjectQL + SqlDriver(better-sqlite3) + real SecurityPlugin with the shipped defaultPermissionSets, plus AuditPlugin for the ledger. It used the REAL SysEmail, then a variant whose two body gates were moved onto one both-audience capability (manage_mailboxes). 9 of 9 cases ran; vitest exit 0. Results by gate and posture: main/single, main/isolated: the org admin is served its org's row with no body_text/body_html; the member gets 403 PERMISSION_DENIED; the platform admin gets the bodies. main/group: the org admin of X who is a member of Y is served Y's row as well, without its body. candidate/isolated and candidate/single: the org admin gets its own org's bodies and no cross-org row. candidate/group: the org admin of X gets Y's row WITH its body. Ledger copy of a sys_email row with the body in clear (pre-#24688 shape): on main the org admin gets clearServed=false; with the candidate, clearServed=true. (c) The #24688 matrix (packages/qa/dogfood/test/identity-mail-read-gate.dogfood.test.ts) on the ceremonied 18.0 fixture. Control on base: 'Tests 25 passed (25)'. Candidate leg: ablation-replace on packages/platform-objects/src/audit/sys-email.object.ts (both body gates set to manage_mailboxes), a rebuild of platform-objects, then ablation-dist-preflight with marker present in 4 built files (exit 0). Result: 'Tests 5 failed | 20 passed (25)'. Four of the reds are the intended new door: list, get and query of the invitation, and the search hit for the invitation id. The fifth is 'sys_audit_log: a body probe on the copy is refused to the organization admin, named parent or not', which got 200 where it expected 403. A scratch probe (deleted afterwards) shows that this 200 returns 0 rows for the moved reset copy, and 0 rows for a span no row holds. Restore: 'blob == HEAD (5a3beae402bf) and git diff HEAD is empty'. Then a rebuild, and ablation-dist-preflight --absent: the dist reading passed ('marker absent from all 66 built files'). The tree reading returned exit 3 only because my untracked scratch files were still present; they were deleted, and git status --porcelain is empty. (d) Mirror probes on the same fixture, base vs candidate, for the org admin: the sys_audit_log and sys_activity rows about the invitation went from 2 rows with clear=false to 2 rows with clear=true. Rows about the moved reset: 0 rows in both. A body probe pinned to object_name sys_email went from 403 to 200 (n=1 for the invitation span, n=0 for the reset span). Pinned to sys_identity_email it stayed 403. The member stayed 403 on every probe. The platform admin was unchanged.", "gates": "No diff, so no local gate family is owed. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack exited 2 with 'this branch changes nothing against origin/main (merge base 93e6c1a5f) — nothing to derive'; stderr named the tree 'objectstack-ai/objectstack at commit 93e6c1a5f8' and '--repo ... it holds'. --ran: not applicable (empty derived list). No PR, so there are no check-runs.", "mcp_calls": "0", "api_writes": "1: POST issues/24898/comments (this os-dev-report, through scripts/pm/post-stamped.mjs over the fleet-write relay). There was also one git push (the empty branch, as the write probe), which is not a REST write.", "deviations": "None against the dispatch. For the record: I wrote two scratch measurement test files (plugin-security/src and qa/dogfood/test), ran them and deleted them, and never committed them. The closures were built with pnpm recursive build rather than turbo. Every measurement mutation went through scripts/ablation-replace.mjs with its own restore trap, and its restore was proven by blob equality.", "files_changed": [], "open_questions": [ { "question": "Q1 (blocking: the door). Which ONE mechanism carries 'manage_platform_settings OR an organization_admin of the row's own organization' on sys_email.body_text and body_html? Measured: field requiredPermissions is an AND list judged per caller (security-plugin.ts foldFieldRequiredPermissions; spec describe 'AND-gate'). The spec already states the composition for an OR of audiences: 'There is no any-of form: to serve one item to several audiences (legal OR admin), declare one capability and grant it to each of their permission sets' (packages/spec/src/ui/audience-required-permissions.ts, ruling 6095014058). Capabilities held by both admin_full_access (= spec ADMIN_FULL_ACCESS_CAPABILITIES) and organization_admin (and its no-bypass variant): setup.access, setup.write, manage_mailboxes. A new capability needs packages/spec: PLATFORM_CAPABILITIES and ADMIN_FULL_ACCESS_CAPABILITIES are single-source, pinned by default-permission-sets.test.ts:451 and carried by core resolve-authz-context.ts:1807. Any choice also drops manage_platform_settings from the gate's literal text: a principal holding manage_platform_settings from a set other than admin_full_access would lose the body. No such set exists in the shipped sets or in examples/ at 93e6c1a5f8.", "options": [ "A. A new org-scoped capability (for example read_organization_mail: 'read the bodies of the organization's own outbound mail log, sys_email'), added to spec PLATFORM_CAPABILITIES and ADMIN_FULL_ACCESS_CAPABILITIES. organization_admin grants it, so the derived no-bypass variant inherits it. The sys_email body gates name it; sys_identity_email is unchanged. Cost: a packages/spec edit (two list entries plus a describe), one line in plugin-security, two lines in platform-objects; effort S, Clause-② yes (widening). Business: it serves the measured pull, since the org admin reads no body of its own invitations today. Long-term: it follows the ruled composition (one capability per gate, granted to each audience) and names the right exactly. Anti-AI-error: an AI that reads the field finds a capability whose description says what it opens; an org admin can delegate it (scope org) only on purpose. Startup: one permanent vocabulary item, which is the 'one permission door' the ruling already accepted.", "B. Reuse manage_mailboxes, which both audiences already hold by default. Cost: two lines in platform-objects, but its spec description ('Administer inbound mailboxes …') then under-describes what it grants, so honesty still needs a spec text edit. Business: same pull served. Long-term: inbound-mailbox administration and outbound-log body reads become one grant. Anti-AI-error: worse, because an AI granting manage_mailboxes for inbound mail silently opens outbound bodies; the capability does more than it says. Startup: zero new vocabulary.", "C. An any-of field requiredPermissions shape (spec). Cost: M–L; the change reaches every requiredPermissions consumer (mask, query guard, write gate, explain, masking unmask rule). Business: no pull beyond this field. Long-term: it contradicts the ruled 'no any-of form' composition. Anti-AI-error: it invites disjunctions on every carrier. Startup: a new shape. Not recommended." ], "recommendation": "A, because it is the composition the spec already rules for an OR of audiences, it states exactly what it opens (anti-AI-error), it costs one vocabulary item the ruling already accepted (startup), and it avoids overloading an unrelated capability (long-term). It needs a dispatch that allows the packages/spec edit." }, { "question": "Q2 (blocking: the pin). The ruled pin says every BUILTIN_AUTH_TEMPLATES entry sends with identityMail: true. That is false on main BY DESIGN. Measured: the flag is not a property of a template (EmailTemplateDefinition has none); it is set at each send in plugin-auth auth-manager.ts. auth.account_invitation (:2077), auth.password_reset (:2092), auth.verify_email (:2155), auth.magic_link (:4187) and auth.email_change_notice (:6038) all send identityMail: true. auth.invitation (:3936), the ORGANIZATION invitation, sends organizationId and no identityMail: 'the invitation HOLDS its organization; thread it so the sys_email row is stamped: tenant mail' (commit b706af987). The send door refuses identityMail together with organizationId (plugin-email email-service.ts:248). auth.two_factor_otp has no sender in this repo. The director's options table names invitation bodies as B's use case. The literal pin would move invitations into the platform-only identity log and close exactly that use case.", "options": [ "A. Pin the identity subset by name, at the producer. Every built-in template that has a sender, except auth.invitation, sends identityMail: true and no organization. auth.invitation sends its organization and no identityMail. An entry added to BUILTIN_AUTH_TEMPLATES and not classified in the pin reds it. The pin is behavioural, in plugin-auth: drive each flow and capture the send. Cost: S, and outside the claimed file surface (plugin-auth). Business: keeps invitations, the named use case. Long-term: the pin lives where the flag is set, with no new runtime rule. Anti-AI-error: a new template added without a class reds. Startup: a test only.", "B. Make it structural at the send door. plugin-email refuses a sendTemplate of a built-in identity template that lacks identityMail: true. This also covers a flow or app sending, say, auth.magic_link by name; no in-repo producer other than plugin-auth sends an auth.* template, but a flow node naming one is the unmeasured residual. Cost: S–M, a runtime refusal (a new gate, which is 'default no' unless the maintainer names it), and it restores a template-name identity list the ruling chose to leave as an observation. Strongest against mis-routing.", "C. The literal ruling for all seven entries. Organization invitations move to sys_identity_email: this reverses the tenant-mail classification above and closes invitation bodies to organization admins. Not recommended." ], "recommendation": "A, because it keeps the use case the ruling exists for (business), pins the flag where it is set without a new runtime rule (long-term, startup), and still reds on an unclassified new template (anti-AI-error). B is the stronger option if the maintainer names a send-door refusal." }, { "question": "Q3 (contract text). Does 'an organization_admin of the row's own organization' bind under the group posture? Measured on a real stack: a field gate is judged per caller, not per row, and a stored grant applies only in the active organization (core grantAppliesInTenant). Layer 0 is the active organization under isolated, and the only organization under single (ADR-0131 D9: 'the Default Organization is the only one'). In both postures the door is exactly the row's own organization: the candidate served no cross-organization row. Under group, Layer 0 is the caller's membership union. The admin of X who is a member of Y is ALREADY served Y's sys_email envelope today, and with any caller-level gate it would also be served Y's body.", "options": [ "A. Accept it and state it in the contract. Under group, the body follows the same union reach every organization-admin power already has there (envelopes included, measured). Cost: none. Business: group is the multi-organization posture of the enterprise runtime, and no narrower pull is measured. Long-term: one consistent organization-admin reach per posture. Anti-AI-error: no new surface. Startup: zero.", "B. A row-conditioned body gate (bodies served only on rows whose organization_id is the active organization). Cost: M, a new per-row field mechanism in plugin-security, with a second-copy risk against the field fold.", "C. Withhold bodies under group only, as a posture-keyed exception. Cost: S, an extra rule that reads like a bug to the next author." ], "recommendation": "A, because the measured existing behaviour already gives the organization admin the union under group, and a per-row mechanism for one field pair is the scope creep the startup axis rejects; the PR's contract section would say so in one sentence." }, { "question": "Q4 (confirm a reading of 'no archived or mirrored copy is widened'). Measured on #24688's ceremonied 18.0 fixture, base vs candidate. Under B the org admin is served its organization's pre-#24688 CLEAR tenant-mail copies (the invitation) in sys_audit_log and sys_activity, and a body probe on them answers with the row. The moved password-reset copies stay unserved: 0 rows, and a pinned probe that answered 403 now answers 200 with 0 rows. The cause: both mirror redactions serve a copy exactly the fields the data plane serves (plugin-audit served-fields.ts, which by its own contract is 'one answer'), and os migrate audit-mail-bodies is operator-run, not a ceremony step. HEAD-written copies are masked at rest. No mask code (mail-log-body.ts, #24722) moves.", "options": [ "A. Accept it. Copies follow the row's one gate; what opens is the organization's own tenant mail, already readable on the row under B. The PR re-pins the reset-copy probe as '0 rows' and the changeset tells operators to run os migrate audit-mail-bodies --apply. Cost: none beyond the re-pin.", "B. Keep mail-log body columns of copies platform-only inside the mirror redaction. Cost: S, but it is a second copy of the gate in plugin-audit, against served-fields' 'one answer' contract.", "C. Make the door conditional on a recorded audit-mail-bodies run. Cost: M; a new persisted state, against 'no new setting'." ], "recommendation": "A, because the identity copies are measured to stay closed, the widened copies hold only content B opens on the row itself, and B and C both add a second rule or a new state." } ], "out_of_scope_findings": [ "carrier: none · noted, not filed: auth.two_factor_otp is registered in BUILTIN_AUTH_TEMPLATES but no producer in this repo sends it (grep of sendTemplate and the template name at 93e6c1a5f8). This is dormant, not a defect class, so no card. It matters only to Q2's classification." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 21, 2026
决策请求 ·
domain:servicesseat 1 ·os-justin·session_011TdZ8GjixNZFLz1xJD9zzK· 2026-10-21T00:57Z依据:os-dev 报告
6305255439(本卡),读数基于origin/main93e6c1a5f8,真实栈实测,未提交任何代码。维护者裁决6304372157(批 #380 项 1,B)的方向不变:组织管理员能读本组织租户邮件的正文,sys_identity_email不动。dev 停在写代码之前,原因是裁决的三件里有两件照字面做不出来:「门」和「钉子」。另有两处放宽的边界需要维护者确认(安全与权限边界的放宽是人工地板,席位不代裁)。共四问,每问一个字母。一句话问题: 让组织管理员看到自己组织的邮件正文,现有权限写法表达不了「平台管理员或本组织管理员」;裁决里的「每个内置身份模板都进身份邮件日志」也会把组织邀请邮件——恰是这项需求要看的那类邮件——挪到组织管理员看不到的地方。
背景(实测)
body_text/body_html;平台管理员能看到;普通成员被拒(真实栈,single / isolated / group 三种姿态均测)。requiredPermissions是逐调用方判的「全部持有」(AND),没有「任一」形式。spec 已写明多受众的标准写法:「There is no any-of form: to serve one item to several audiences (legal OR admin), declare one capability and grant it to each of their permission sets」(packages/spec/src/ui/audience-required-permissions.ts:49)。setup.access、setup.write、manage_mailboxes;其中manage_mailboxes的 spec 描述是「Administer inbound mailboxes」(packages/spec/src/security/capabilities.ts:96),只管收件。auth.invitation按设计是租户邮件:发送时带组织(plugin-auth/src/auth-manager.ts:3951-3955,commit b706af9「the invitation HOLDS its organization … tenant mail」),而发送门拒绝identityMail与organizationId同时出现(plugin-email/src/email-service.ts:248)。总监选项表给 B 举的用例正是邀请邮件正文。其余五个有发送方的内置身份模板都已带identityMail: true;auth.two_factor_otp仓内无发送方。Governing text
6304372157:B 门 =manage_platform_settings或行所属组织的organization_admin;附加 1 矩阵加列;附加 2「every built-in identity template (BUILTIN_AUTH_TEMPLATES) sends withidentityMail: true」。requiredPermissions为能力 AND 门;上引 spec「no any-of form」的组合规则(裁决 6095014058)。sys_email租户日志,sys_identity_email部署级身份日志。filing-gate.md):改 spec 声明面属协议类,由维护者裁决。协议声明、是否改协议
Q1 推荐的 A 在
packages/spec的平台能力表新增一个能力名,是 spec 声明面的扩大(Clause-②: yes (widening)),属协议类,执行走domain:spec。其余三问不改 spec。前提(各带 re-check 命令)
git grep -n "any-of form" packages/spec/src/ui/audience-required-permissions.ts(1 处)。git grep -n "invitation HOLDS its organization" packages/plugins/plugin-auth/src/auth-manager.ts(1 处);git grep -n "identityMail === true && input.organizationId" packages/plugins/plugin-email/src/email-service.ts(1 处)。manage_mailboxes只描述收件:git grep -n "'manage_mailboxes', label" packages/spec/src/security/capabilities.ts(1 处)。git grep -n "requiredPermissions: \['manage_platform_settings'\]" packages/platform-objects/src/audit/sys-email.object.ts(2 处)。Q1 · 门:用什么表达「平台管理员或本组织管理员」
read_organization_mail:读本组织外发邮件日志的正文),平台管理员与组织管理员默认授予;sys_email两个正文字段改门到它domain:spec1 PR(S)+domain:services1 PR(S);先例 PR #24688(+530/−1)manage_mailboxesmanage_mailboxesrequiredPermissions载体都可写「或」业务含义直译: A = 给「查看本公司寄出的信」单独配一把钥匙,发给物业和公司负责人;B = 把收件室的钥匙顺便当成寄件档案柜的钥匙;C = 把所有门锁都换成「多把钥匙任一可开」。
四轴(业务立场):
manage_mailboxes,对方随即能读所有外发正文,而权限说明不提;A 的能力名与描述就是它开的门。推荐:A。 终态句:两年后,每个字段读门都点名一个说清自己开什么门的能力,按受众各自授予;参照 Salesforce 的 custom permission 经 permission set 分配。置信缺口:A 的 spec 单测与默认权限集钉子(
plugin-security/src/objects/default-permission-sets.test.ts:451)未实跑。B 不取:能力做的比它说的多;C 不取:推翻已裁规则且波及面大。自检:只看①选 A;②③④ 是否翻转:否。Q2 · 钉子:「每个内置身份模板都带
identityMail: true」plugin-auth驱动每个流程抓发送:除auth.invitation外的有发送方模板都带identityMail: true且无组织;auth.invitation带组织、不带identityMail;新增模板未分类即红plugin-email拒绝缺identityMail的内置身份模板发送业务含义直译: A = 按信的种类贴标签,邀请函归公司、身份证明归物业,漏贴即报警;B = 收发室拒收没贴标签的身份证明信;C = 把邀请函也当身份证明交给物业。
四轴: ① A 钉在标记设置的地方,不新增运行时规则;C 与租户邮件归类相矛盾。② 邀请正文是 B 的用例(总监表),C 让需求落空。③ A 新模板未分类即响亮报红;C 出错时是组织管理员静默看不到本该看到的邀请。④ A 只加测试;B 新增一道门禁。
推荐:A。 终态句:身份邮件与租户邮件的分流由发送方声明、测试按名覆盖每个内置模板。置信缺口:flow 节点按名发送身份模板这一残余未实测(仓内无此生产方)。B 不取:新增门禁默认否,除非维护者点名;C 不取:让需求落空。自检:只看①选 A;②③④ 是否翻转:否。
Q3 · group 姿态下的跨组织
实测:字段门逐调用方判,不逐行判。single 与 isolated 下门恰好是行所属组织(候选实现未返回任何跨组织行)。group 下第 0 层是调用方的成员关系并集:X 的组织管理员若也是 Y 的成员,今天已能读到 Y 的邮件行(无正文);任何调用方级的门都会让它也读到 Y 的正文。
业务含义直译: A = 在几家公司都有门禁卡的人,能看这几家公司寄出的信;B = 只能看当前所在那家;C = 联合办公楼里谁都不能看。
四轴: ① A 与组织管理员在 group 下的其余权力同一可达面,不另造机制;B 新增逐行字段机制。② 未实测到更窄的拉动。③ A 无新面;B 的第二份规则是 AI 改漏的温床。④ B 为一对字段新增机制。
推荐:A。 终态句:组织管理员的可达面每种姿态只有一个答案,字段与行同面。置信缺口:无 group 姿态的真实部署读数。这是权限边界放宽,由维护者裁定。 自检:只看①选 A;②③④ 是否翻转:否。
Q4 · 镜像副本
实测(#24688 的 18.0 仪式夹具):两份镜像(
sys_audit_log、sys_activity)按数据面同一答案返回字段(plugin-auditserved-fields,「one answer」契约)。B 之后,本组织 #24688 之前的明文租户邮件副本(邀请)对组织管理员可见;已迁走的重置邮件副本仍不可见(0 行),绑定sys_identity_email的探测仍 403。新写入的副本静态已掩码,mail-log-body.ts(#24722)不动。os migrate audit-mail-bodies --apply业务含义直译: A = 档案柜复印件和原件同一把锁;B = 复印件另配一把更严的锁;C = 先确认旧复印件销毁过才开锁。
四轴: ① A 保持「一个答案」;B 是第二份门。② 开放的只是 B 已在原行开放的内容。③ B 的两份规则会分叉;A 一处改全处随。④ C 新增状态。
推荐:A。 终态句:副本永远与原行同门。置信缺口:运维未执行
audit-mail-bodies的存量部署数未知。这是归档副本的放宽,裁决原文「no archived copy is widened」与之相关,由维护者裁定是否属于该禁止。 自检:只看①选 A;②③④ 是否翻转:否。裁后执行(四问皆 A 时)
domain:spec立执行卡:新增组织级能力(名与描述按维护者所定),进PLATFORM_CAPABILITIES与ADMIN_FULL_ACCESS_CAPABILITIES,Clause-②: yes (widening)。sys_email两个正文字段改门到新能力,organization_admin授予它;fix(platform-objects,plugin-audit,plugin-approvals)!: mail bodies are read only with manage_platform_settings, and the audit mirrors never copy them in clear #24688 矩阵加列并把重置副本探测重钉为 0 行;plugin-auth按名分类的发送钉子;契约段写明 group 姿态的并集可达;changeset 提示运维执行os migrate audit-mail-bodies --apply。达档复核后入队。pm:blocked,Blocked-by:指向 spec 卡。相关单与 PR
PR #24688(#24625,本卡的先例与矩阵)、#24722(副本掩码)、#24681(身份邮件拆表)、
6304000678(总监增量)、6301739802/6302639132(席位 2 问与分诊答)。os-decision-facets
manage_mailboxes会让为收件授权的 AI 静默放开外发正文;按名分类的钉子让新模板未分类即响亮报红。Prior rulings read:
requiredPermissions any-of·sys_email body organization admin·identityMail organizationId→ 3 hits (6095014058 the no-any-of composition; 6304372157 this card's ruling B; 6257510973 ADR-0131 D7 Q2 A); ADR-0066 D3, ADR-0131 D7; thread: 6304372157, 6304000678.Generated by Claude Code
objectstack-fleet commentedon Oct 21, 2026
Ruling: batch #381 item 1 · letter 关闭 · maintainer 「24898 直接关闭。」 2026-10-21T04:58Z
Director seat, summon #45 continuation 3,
session_01Lg6GQ4xjqmvEzYv78d6H7i. Presented in this session's chat as batch #381 item 1 (the four execution forks Q1–Q4 of 6305309651, after the dev's real-stack measurement 6305255439 found two of ruling B's three pieces unbuildable as written). The maintainer then asked what the business need is; the director answered thatsys_emailholds organization invitations and notification mail, that the only console reader is the record activity feed, thatsingle-posture deployments are unaffected (the platform admin is the organization admin), that only walled (hosted) tenant admins lose the bodies, that no customer has asked, and that the dev's measurement had tripled the cost and opened two boundary relaxations (Q3 group-union reach, Q4 mirror copies). The director recommended a hold; the maintainer ruled further: close. Thread read through 6305309651 at this write; nothing newer.Governing text:the decision frame's second facet (zero measured pull ⇒ defer or remove by default); the #24688 landing (body_text/body_htmlon both mail logs readable only withmanage_platform_settings), which stays as it is.What the ruling binds
sys_emailandsys_identity_emailstay readable only withmanage_platform_settings. No spec capability is added, no plugin-auth pin, no contract text on the group posture, no mirror re-pin.claude/issue-24898-org-admin-reads-tenant-mail-bodiesis not reused. If a hosted tenant ever asks to read its organization's outbound mail bodies, that request is a new card, and the measured route of record is Q1 A (one organization-scoped capability, granted to each audience).Workload of the chosen letter: none.
State:
needs-user-decision→ closednot_plannedin this act.objectstack-fleet commentedon Oct 21, 2026
Closed not_planned on the maintainer's ruling 「24898 直接关闭。」 (6308427243, batch #381 item 1) · director seat, summon #45 continuation 3,
session_01Lg6GQ4xjqmvEzYv78d6H7i· 2026-10-21T05:03Z. The #24688 body gate stands; nothing is built, filed or dispatched for this card; the empty branchclaude/issue-24898-org-admin-reads-tenant-mail-bodiesis not reused.