Repository navigation
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
Description
Filing gate ③: a task the maintainer directed. These are steps 1–3 of the 18.x line, under ruling records 6266750393 and 6267391172 on #15194. The second records the maintainer's 「用量很多,不用节约」: steps 1–3 start now, in parallel.
- Reader who acts: the epic seat (
epic:#15194,session_01Y5583bs2RDd53MH6kM1ca3), which dispatches it now. - Dedupe: none. Before this card, the stage lived only in the plan rows on feat(spec,drivers,objectql,plugin-security):
organization_idNOT NULL per cleared table; one predicate for Layer 0 and every driver; bothorWhereNullarms, the__global__sentinel and the #13491 ledger retire (ADR-0131 D1/D8/D9) — protocol 18 #15212.
Part of #24343 (three framework writers), from decision #24377 Q2 A (ruling 6257510973) · Step 3: 8b-0d′, identity mail goes to a deployment-level sibling of sys_email · ⛔ Not an 18.0 gate.
The line's rule (binding)
Once merged, this PR leaves main releasable:
- R1. A fresh install boots and serves.
- R2. A database an 18.0 install created upgrades to that commit, boots, and keeps its data. test(qa): the 18.x line's upgrade harness (an 18.0 database boots on every later main with its data) and the measured step plan #24522's harness proves this.
- R3. A v17 database is still refused.
- R4. A walled posture is still refused at boot.
Expand first. This step only adds: the new write shape, plus reads that accept both the 18.0 shape and the new one. It never moves or deletes an 18.0 row and never drops a column. Each such removal is written down for the step-5 ceremony.
Merge gate. The PR stays a draft until #24522's harness has merged. It lands after it has merged main and is green on R1–R4.
Scope
- Per [Decision] C8 under a wall: four framework writes that have no organization to name — self-registration grant, identity mail, activity, deployment-level secrets #24377 Q2 A (ruling 6257510973), identity mail persists to a new deployment-level sibling of
sys_email, not tosys_email. Identity mail means verification, password reset and similar mail about a user with no organization. - Expand: new identity mail goes to the sibling. Every reader of identity mail (resend, rate limits, audit and admin views) reads both the sibling and the 18.0 rows in
sys_email. Moving the old rows is a step-5 ceremony item. - 8b-0f (fix(plugin-auth): the self-registration grant settles on the registrant's first membership (ADR-0131 D3, C8 8b-0f) #24453) has merged, so
auth-manager.tsis free. - Plan amendment 13 (6259616282) on feat(spec,drivers,objectql,plugin-security):
organization_idNOT NULL per cleared table; one predicate for Layer 0 and every driver; bothorWhereNullarms, the__global__sentinel and the #13491 ledger retire (ADR-0131 D1/D8/D9) — protocol 18 #15212 and ADR-0131 D9 on a walled control plane: three framework writers land NULL organization_id rows on every sign-up (the self-registration grant, the verification mail, activity about org-less records) #24343's body carry the detail. Read them.
Acceptance
- The new shape is pinned on a fresh install. The dual read is pinned on an 18.0-shaped row.
- The harness stays green.
- The report lists every removal the step-5 ceremony owes, table by table.
- Measure first. If expand-first is impossible or unsafe for any part, stop and report that part with the alternative. Do not build a destructive change.
Activity
objectstack-fleet commentedon Oct 19, 2026
Claim: PM loop round 10 · 2026-10-19T12:43Z
Session:
session_01Y5583bs2RDd53MH6kM1ca3Account:
os-steve(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-24531-8b0d-identity-mail-sibling. If the dev splits this step into several independently releasable PRs, each PR's branch isclaude/issue-24531-8b0d-identity-mail-sibling-<part>and carriesPart of #24531.Worktree: the cloud session's own checkout (
mode:cloud)Domain:
domain:servicesSeat:
epic:#15194(the epic lane)File surface, read on
origin/mainat or after5b14b7f3a. The dev measures first; a breach stops the work and is explained in the report.packages/plugins/plugin-auth/**(identity-mail persistence and its readers) andpackages/plugins/plugin-email/**, as measured.sys_email.packages/qa/dogfood/**harness files of test(qa): the 18.x line's upgrade harness (an 18.0 database boots on every later main with its data) and the measured step plan #24522. ⛔ Notdocs/adr/**. ⛔ Notcontent/docs/releases/.Container & model:
M,mode:cloud,model: TIER_DEFAULTClause-②: yes — the dev re-measures; a new deployment-level sibling object is an added schema face
Responsibility: n/a — a ruled scope stage (ruling records 6266750393 and 6267391172 on [epic] ADR-0131 — total organization ownership: no NULL organization_id (v18 line) #15194; [Decision] C8 under a wall: four framework writes that have no organization to name — self-registration grant, identity mail, activity, deployment-level secrets #24377 Q2 A, 6257510973)
Thread-read: 6267391172
Serial constraints cleared:
objectstack-fleet commentedon Oct 19, 2026
os-dev-report
{
"issue": 24531,
"status": "done",
"branch": "claude/issue-24531-8b0d-identity-mail-sibling",
"pr": "#24587",
"session": "session_016CvQEdxxYYVa8C8AjKZBtL",
"premise_still_valid": true,
"summary": "Identity mail is now written to sys_identity_email, a new deployment-level sibling of sys_email. It has the same fields and indexes (held equal by a test), systemFields tenant false so no organization column, append-only, get/list, and manage_platform_settings to read. The contract gains SendEmailInput.identityMail and SendTemplateInput.identityMail; sending one with an organizationId is refused as VALIDATION_FAILED. plugin-email's whole pipeline reads and writes the log the row lives in: send, sendTemplate, persistence, the queue worker, the drain hook, the per-log boot sweep, reclaim and the header readback. A sibling job carries object sys_identity_email; a job with no object reads sys_email, which covers every 18.0 job. plugin-auth's five identity sends (verify, reset, account invitation, magic link, email-change notice) say identityMail and name no organization in every posture; the organization invitation stays tenant mail. Expand-first: no 18.0 row is moved or deleted, and 18.0 sys_email identity rows and jobs are still delivered, swept and reclaimed (pinned on an 18.0-shaped fixture). The inventory row, the census JSON and the tenant-audit census were regenerated. The card's assignee was os-steve at dispatch and was not written; the PR assignee is os-steve.",
"tests": "All on head 3bc88f5 unless noted. plugin-email: typecheck plus vitest, 34 files, 571 passed. plugin-auth: typecheck plus vitest, 165 files, 3168 passed and 10 skipped, run before the main merge; main's two commits do not touch plugin-auth; identity-mail-sibling, auth-email-locale and verification-email-failure-propagation re-run on the final head, 77 passed. platform-objects: typecheck plus vitest, 78 files, 1188 passed; src/audit re-run on the final head, 27 passed. spec: platform-object-names.test.ts and email-service.test.ts, 15 passed; typecheck exit 0. cli unit: organization-ownership-inventory.test.ts, 25 passed. dogfood: signup-organization-census.dogfood.test.ts, 3 passed (verification mail in sys_identity_email and none in sys_email; the sibling schema has no organization_id, with sys_email having it as the control). Fresh showcase boot (pnpm dev --fresh -p 38531): a password reset and a sign-up verification each wrote one sys_identity_email row, sent, with no organization_id key; invite-member wrote one sys_email row, related_object sys_invitation, stamped with the organization; the server was killed by its recorded PID and the port is free. Ablations via scripts/ablation-replace.mjs, each restored to the HEAD blob with git diff HEAD empty: A1, route every send to sys_email: 4 red. A2, the worker ignores the job's object: 2 red. A3, a job with no object reads the sibling: 11 red, including the 18.0 rowId-job pin. A4, reclaim always reads sys_email: 2 red. A5, plugin-auth verification send without identityMail: 3 red; the first A5 attempt was refused by the tool (replacement was a substring of the anchor) and was a no-op. A6, the same mutation through plugin-auth dist: ablation-dist-preflight found the marker in 2 built files, the dogfood census went 2 red, and after restore, rebuild and preflight --absent it was 3 green; the first A6 attempt's mutated build failed on a TS no-overlap error, so that reading is void.",
"gates": "Local, head 3bc88f5: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 126 commands; all 126 ran, exit codes were written to a file before any pipe, and all 126 were exit 0. The --ran reconciliation with 'command :: exit N' records reads 126 derived, 126 run, 0 NOT MEASURED (a derived zero). check:dev-prereqs exit 0. Earlier reds were fixed: check-keyed-text-bounds exit 2 (a fields reference it cannot read, now a literal plus an equality test); check-tenant-audit-census exit 1 (regenerated, two prose numbers updated); check-plugin-teardown-shape --self-test exit 3 (shallow clone; fetched its pinned control commit). STDERR artifact-roster block: 51 roster families, NOT MEASURED locally, CI's. Also outside the derived total: 11 wide-population families, 6 workflow-valued families, 5 path-scheduled CI jobs and 4 type-check lanes, all CI's. CI on 3bc88f5 at report time: 'No other open PR may claim the same issue' success; Lint (all lanes), Type Check (4 lanes), Governed Surface Queue Guard, Check Changeset and the rest in_progress; Packed-tarball smoke skipped.",
"mcp_calls": "0",
"api_writes": "2 relay strokes: (1) pr_create, which is POST /repos/objectstack-ai/objectstack/pulls (draft) plus POST /repos//issues/24587/assignees, read back byte-identical with assignee os-steve; (2) this report, POST /repos//issues/24531/comments. git push is not counted. Reads used gh api under the session login.",
"ceremony_owes": [
"sys_email: move identity rows to sys_identity_email without organization_id. These are rows about a person: related_object sys_user, or none (magic link). Walled 18.0 rows are organization-less; single 18.0 rows carry the Default Organization, and the ceremony must rule whether they move (the ruling names walled rows anchored on sys_user or on nothing). Move only terminal rows (sent/failed). A queued row's pending rowId job reads sys_email and would never deliver after a move.",
"sys_email attachment reclaim jobs (email.attachment.reclaim) with no object: after a move they take the row-gone branch and DELETE the content. Either rewrite pending payloads to add object sys_identity_email, or move rows only after their reclaim window.",
"sys_email email.send.async jobs with no object for queued identity rows: drain them before the move, or rewrite their payloads to add object.",
"sys_activity: rows whose object_name or source_object is sys_email for a moved row point at a missing record; re-point or delete them (Q3 A's deletion covers the organization-less ones).",
"sys_email.organization_id NOT NULL (8e): only after the move leaves zero NULL rows.",
"step 7: the 'no object reads sys_email' default stays correct for tenant jobs; once no 18.0 job can still be pending, nothing identity-specific is left to remove.",
"outside this repo or card: cloud's wall-test non-vacuity control is rewritten against the sibling (cloud lane, per the ruling); the ADR-0131 D7 Q2 A note's 'has not shipped' line needs its dated update (Tier H, docs/adr off this card)."
],
"open_questions": [
{
"question": "Decided: the sibling's name and the contract member's name.",
"options": [
"A sys_identity_email plus identityMail (the ruling's 'identity mail' words)",
"B sys_platform_email plus a scope enum (sys_platform_setting's precedent)"
],
"recommendation": "A, taken: it names what the rows are, matches the ruling's vocabulary, and a boolean is the smallest widening of the contract."
},
{
"question": "Decided: identityMail sent together with organizationId.",
"options": [
"A refuse as VALIDATION_FAILED and record the rejection in the sibling",
"B drop the organization silently",
"C let organizationId win and write to sys_email"
],
"recommendation": "A, taken: contract-first; either silent choice writes the row somewhere its caller did not mean."
},
{
"question": "Decided: who may read sys_identity_email.",
"options": [
"A manage_platform_settings, as sys_job and sys_platform_setting",
"B open reads like sys_email"
],
"recommendation": "A, taken: with no column there is no wall (D7), and the rows carry reset and sign-in links."
},
{
"question": "Decided: under single, identity mail no longer carries the Default Organization.",
"options": [
"A identity mail goes to the sibling in every posture",
"B only under a wall"
],
"recommendation": "A, taken: the ruling makes identity mail deployment-level outright, and one route per mail class keeps the readers single."
}
],
"out_of_scope_findings": [
"class: none filed. Observation: sys_email declares no requiredPermissions while 18.0 identity rows there carry reset links; whether a non-administrator can list them was NOT MEASURED (the showcase registrant could not sign in unverified). reach: exception security, unmeasured. evidence: packages/platform-objects/src/audit/sys-email.object.ts enable.apiMethods get/list with no requiredPermissions. dedupe words: sys_email read permission, reset link exposure, requiredPermissions sys_email",
"carrier: none (承接者:无). check:keyed-text-bounds reads only a literal fields map, so an object that reuses another's fields must spell them out; noted in the PR's Acceptance notes."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
os-dev-report addendum: #24552 harness run on PR #24587
maininto the branch: headcd6fe0b4f, a clean merge with no regeneration deferral.pnpm install --frozen-lockfileand the dogfood dependency-closure build both exit 0.pnpm --filter @objectstack/dogfood exec vitest run test/upgrade-18x-harness.dogfood.test.ts test/signup-organization-census.dogfood.test.ts, throughos-verify-lock. Result: 2 files, 8 passed, VERDICT exit 0. Per test:sys_emailrows.organization_id IS NULL. Every reader goes by table, id and status. The stamped 18.0 rows stay insys_emailand keep being delivered, swept and reclaimed.sys_email.sys_emailis untouched.cd6fe0b4f.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Landed and closed · epic PM
session_01Y5583bs2RDd53MH6kM1ca3(os-steve) · 2026-10-19T17:03Z. PR #24587 (18.x step 3, 8b-0d′: identity mail moves to a deployment-level sibling ofsys_email) has merged.merged: trueat 2026-10-19T16:49Z; merge commitfbafb8533CONTRACT_REVIEW_TIER, schema face, PASScd6fe0b4f(addendum 6270136912)Delivered (a complete feature, per 6270035914).
sys_identity_email. That covers reset, account invitation, verification, magic link and change-email. The table has no organization column and sits behindmanage_platform_settings. The organization invitation stays onsys_email.sys_emailare still sent, retried and reclaimed, and none is moved.Carried.
related_object.sys_emailexposure is being measured on measure(plugin-email): who can list sys_email rows that carry identity mail (reset and verification links) — sys_email declares no requiredPermissions #24625.auth.account_invitationbranch inidentity-mail-sibling.test.ts, and add the sibling to theDEPLOYMENT_LEVELroster test.The card closes
completed. The dev session is archived.