Repository navigation
fix: framework producers on tenant-column objects name the organization they can derive (ADR-0131 D9, C8 stage 8b-0b) - #24369
Conversation
…n derive (ADR-0131 D9) The self-registration grant and the auth mails about a user read one derivable organization (the Default Organization under single); the activity writer falls back to it for a record with no organization; the settings secret and setting-audit rows carry their setting's owner; the verify RLS personas' grants carry the persona's membership; the business unit dogfood fixture stops swallowing its organization read. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nNHWXUVqXs5TqaLtf2aGt
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nNHWXUVqXs5TqaLtf2aGt
… Default Organization Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nNHWXUVqXs5TqaLtf2aGt
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nNHWXUVqXs5TqaLtf2aGt
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nNHWXUVqXs5TqaLtf2aGt
📓 Docs Drift CheckThis PR changes 4 package(s): 11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 25 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a5e15d3c99fc453c4911737b7b3ae1f5f98a330f && git checkout a5e15d3c99fc453c4911737b7b3ae1f5f98a330f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 41c7405364be55d302191f55b9301ef96cb7f432 02778102c241db46c2a17dfc6deb0cca927d32b9 && git checkout -B drift-repro 41c7405364be55d302191f55b9301ef96cb7f432 && git merge --no-ff 02778102c241db46c2a17dfc6deb0cca927d32b9
node scripts/docs-audit/affected-docs.mjs --json 41c7405364be55d302191f55b9301ef96cb7f432
|
…llback for a reduced logger Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nNHWXUVqXs5TqaLtf2aGt
Contract reviewServed-tier: Rendered in-seat by the epic seat ( Read: card #24343 and its comments (claim 6251523047, report 6252315398, addendum 6253007344); the PR body; the net diff Check-runs on the head: 40 in all. 31 ① Derived judgments
② Semver level
③ Boundary flags
RemedyThe dev picks one remedy, measured, and says why in the PR.
Under either remedy, rewrite the changeset clause in item 1 so it says what the head does. Implemented-by: VERDICT: FAIL |
…et and takes an error-carrying sink beside it Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nNHWXUVqXs5TqaLtf2aGt
Contract reviewServed-tier: Rendered in-seat by the epic seat ( Read:
Nothing was built or run. Check-runs on the head: 47 in all.
The verdict does not depend on the open shard. Landing waits for it. ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
Part of #24343
Part of #15212
Clause-②: yes (widening)
C8 stage 8b-0b (plan 6251466182 on #15212). Before stage 8b lifts the system-insert exemption, each framework producer that writes organization-less rows on a tenant-column object now names the organization it can derive itself (ADR-0131 D9: derived under
single, refused otherwise). Undersinglethat organization is the Default Organization, which is the owner the engine would derive, so no reader changes. Under a wall a row is stamped only where the producer has an organization. A row it cannot attribute is written exactly as before, and its fate stays the engine's (stage 8b). This PR does not lift the exemption and does not retire any NULL reader.Why
Part of #24343and notFixes. #24343's acceptance is "0 NULL rows on the three tables after a sign-up on a walled control plane, and the ceremony's advisory count reads 0". The walled half cannot be met by naming a derivable organization, because a sign-up has none to derive (measured below). Reaching it takes the fate decisions listed under "Not built". Merging this PR must not close the card.Why
Clause-②: yes (widening)when the claim saidno. Three exported types gain optional members:AuditWriterOptions.getDefaultOrganizationId(plugin-audit), andorganization_idonSettingsSecretStore.insert's row andorganizationIdonSettingsAuditWriter.write's entry (service-settings). These are additive widenings of a published surface, so those two packages takeminor. Nothing narrows.buildSettingAuditWriter's optionalloggerwas briefly narrowed on this branch (headbc7e2b791,{ error?; warn }), and the contract review caught it (record 6253189824). It is now{ warn? } | { error; warn? }:main's accept set (no sink,{},{ warn? }) plus a sink that carrieserror. A tsc probe of those four shapes plus a typed{ error?; warn? }passed, then the probe was deleted.check:optional-error-sinkandcheck:durability-log-levelexit 0.Measured first (framework at this branch, in-process sqlite,
examples/app-crm)A sign-up through
POST /auth/sign-up/emailwith the audience opened (audience_posture: open, declared setmember_default), then one global encrypted settings write (mail.api_key). Rows written after the snapshot:single(this PR)isolated(multiTenant: 'posture-only', this PR)sys_user_permission_setsys_emailsys_activitysys_user(created, updated),sys_email(created, updated),sys_user_permission_set,sys_platform_setting×3,sys_setting_audit×3,sys_secretsys_secret(D4)mail.api_keyciphertext)sys_setting_audit(D4)scope: globalsys_user,sys_platform_setting,sys_secret…) or that are org-less for the same reason. Undersingle, the verification and reset mails and the activity about org-less records used to land NULL, and they now carry the Default Organization. The grant already carried it.scope: global(ai ×10, auth, knowledge ×2, mail, sms ×2, storage), so everysys_secretrow the settings producer writes from the built-in catalog is deployment-level. The datasource binder's credential rows are deployment-level by construction. No webhook producer writessys_secret. Every global-rung write lands an org-lesssys_setting_auditrow.What changed
auth-manager.ts). One reading,derivableWriteOrganizationId(), which istenancy.defaultOrgId(): the Default Organization undersingle, none under a wall. It feeds the self-registration grant (settleSelfRegistrationGrant) and the five auth mails about a user: verification, password reset, account invitation, magic link and the change-email notice.audit-writers.ts,audit-plugin.ts). An activity row whose record has no organization, written with no session organization, takesgetDefaultOrganizationId().AuditPluginwires that to thetenancyservice. The record's own organization and then the session's still win, and the ledger'stenant_idattribution (D7) is untouched.settings-service.ts,settings-service-plugin.ts,settings-service.types.ts). Thesys_secretciphertext and thesys_setting_auditrow of atenantoruservalue carry that value's organization, the same owner as itssys_settingrow. Aglobalvalue's rows name none (D4, below). The setting-audit writer'scatchnow reports aterrorwhen the sink carries it (elsewarn), under the degradation log-level rule: the settings write succeeds while the history row is lost. Without this a refusal from stage 8b would be a silent audit loss. The first report carries the consequence and the fix.rls.ts). The RLS probe and position personas' grants belong to the persona's single membership, which undersingleis the Default Organization. They were written with an explicitorganization_id: null.showcase-bu-hierarchy-sharing.dogfood.test.ts, the "fixture :66"). ItsfindOne('sys_organization', { where: {} })is refused by the engine as an arbitrary pick. The.catch(() => null)swallowed that refusal, so the fixture always minted a stand-inorg_bubeside the real Default Organization. It now reads the Default Organization from thetenancyservice, and it stamps itssys_business_unit_memberrows too.Not built — fates ADR-0131 D7/D9 do not settle (reported to the seat)
sys_email).sys_activity).sys_secret(global settings secrets, datasource credentials) andsys_setting_audit(global rung) rows. No object leaves the tenant-column set here.The ceremony's advisory count (
packages/cli) is not touched. It reads 0 for these tables only once 1–3 are decided.Tests
plugin-auth/src/producers-name-organization.test.ts: the verification mail, the reset mail and the grant, each undersingle(Default Organization) and under a wall (none).plugin-audit/src/audit-writers.test.ts: three cases. The Default Organization is used undersingle; none under a wall, with no resolver, or when the resolver throws; a known organization is never overridden and the resolver is not asked.service-settings/src/settings-secret-organization.test.ts: tenant value vs global value, and the audit writer'sorganization_id.settings-user-reference.pin.test.tsfollows theerrorchannel.verify/src/rls-persona-grant.test.ts: the persona with one membership, none, and two.dogfood/test/signup-organization-census.dogfood.test.ts: thesinglehalf of 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 acceptance census. After a real open-audience sign-up, every row the sign-up wrote on the three tables names the Default Organization. The platform admin's promotion grant, which is stage 8b-0a's, is set aside by snapshot.scripts/ablation-replace.mjswith a trap, then a rebuild): the fallback's resolver returned undefined (return ['ABLATED'][1]).ablation-dist-preflightfound the marker indist, and the census went red (sys_activity: rows without an organization, 2 rows). Restored: blob equal to HEAD,git diff HEADempty,--absentpreflight passed, 2/2 green.5c3cbdd90: plugin-auth 161 files / 3123 passed, 10 skipped; plugin-audit 56 / 897; service-settings 50 / 874; verify 41 / 363.typecheckpassed for all four packages and for dogfood. Dogfood journeys passed 6 files / 28 tests: the census, the business-unit sharing fixture,rls-fixture,admin-identity-audit-trail,settings-organization-isolationandemail-template-materialization.Gates
node scripts/pm/dispatch-gates.mjs --commandsderived 77 families; all were run and reconciled with--ran(76 run, 1 NOT MEASURED). The NOT MEASURED one ischeck:dual-build-cjs-loads, which exited 3 (PREREQUISITE NOT MET: no fullpnpm buildon this box), so CI's reading counts for it.check:engine-double-contractwas red once, on afindOnein the new grant double. It was fixed by dropping the member the writer never calls, and the re-run passed.check-plugin-teardown-shape --self-testneeded its pinned commit fetched (shallow clone) and then passed. The family list re-derived after the last commit is identical.Acceptance notes
IEmailService'sSendEmailInput.organizationIdTSDoc (packages/spec) still says auth verification and reset mail "genuinely has none". That is now true only under a wall. Not edited here (outside the file surface).single, the platform admin's promotion grant (admin_full_access) still lands without an organization. That is stage 8b-0a's file (bootstrap-platform-admin.ts, platform-admin re-anchor follow-up (Choice 4B): config-anchor thesingleposture — first-user promotion becomes development-only fallback #11979), measured by the census and set aside there.sys_audit_log.tenant_idstays NULL on the sign-up's rows. That is D7's attribution field, not the anchor.🤖 Generated with Claude Code
https://claude.ai/code/session_015nNHWXUVqXs5TqaLtf2aGt
Generated by Claude Code