Repository navigation
Description
Found while implementing #14157. Filed unassigned. The development-lane half is fixed there; this card is the part that needs a ruling, because it moves a PUBLIC door.
The measured facts
Measured on a real ObjectQL over better-sqlite3, with 13 seeded sys_user rows and zero sys_account rows, default audience posture:
api.signUpEmail(...)is refusedSELF_REGISTRATION_CLOSED—isBootstrapCreation()reads 13 humans and answers "populated", so the bootstrap carve-out does not fire.count('sys_user', {})= 13, which is whatGET /api/v1/auth/bootstrap-statusused to answerhasOwner: truefrom.
In DEVELOPMENT #14157 closes this: the dev-admin seed now provisions on that population, so a login exists and everything downstream is consistent. Outside development the seed is hard-gated off by NODE_ENV, and then:
- nobody can sign in (no
sys_accountrow at all); - self-registration is refused by the default
invite_onlyposture; - there is no administrator to issue an invitation, because there is no administrator;
- so the deployment cannot be recovered from inside. That is exactly the outcome the bootstrap carve-out exists to prevent — "a fresh install must never lock its operator out" — and it does not fire, because the population it counts is people rather than logins.
Why #14157 did not simply fix it
Both predicates would have to move together, and moving them widens a public door. Today, on that population, an unknown visitor hitting /sign-up/email is refused. If isBootstrapCreation counted LOGINS instead of humans, that visitor would be admitted as the bootstrap account — not promoted (plugin-security still sees 13 older humans), but admitted into a walled deployment. Moving only bootstrap-status is worse in a different way: the console would offer a first-run setup flow that the admission gate then refuses 403.
#14157 therefore made bootstrap-status AGREE with the admission gate rather than diverge from it (it now asks AuthManager.hasBootstrapWindow(), which also closed a real disagreement: the handler used to count every sys_user row including the legacy usr_system service row, so a database carrying only that row reported an owner while the admission gate and plugin-security's first-user detection both stood ready to admit and promote the first human). What is left is one question, and it is a posture question.
The decision
Should the bootstrap carve-out mean "no human users" or "no LOGIN"?
- A. Keep "no human users" (status quo). No public door moves. A production install that seeds a people directory and nothing else stays unrecoverable from inside; the operator must provision an account out of band. The cost is a silent dead end whose only symptom is a 401 on credentials nobody has.
- B. Count logins instead. The carve-out then fires exactly when nobody can sign in, which is what it says it protects against, and both surfaces flip together. The cost: on a directory-seeded, publicly reachable deployment the first stranger to reach
/sign-up/emailis admitted (roleuser, unpromoted) instead of refused. - C. Keep the population predicate and make the dead end LOUD instead. At
kernel:ready, when human rows exist and nosys_accountdoes, report it at error level naming the consequence and the remedy (provision an account out of band, or open the posture). No admission semantics change; the operator is told rather than left to find a 401.
No recommendation is offered here, because option B changes who may create an account on a walled deployment — a posture ruling rather than an implementation choice. C is cheap, moves nothing, and composes with either of the others.
<!-- os-decision-facets -->
① 项目长远合理性(权重 ≥50%):C 既不扩大接受集、也不增生谓词 —— 今天「引导期」这个名字说的是「没人能登录」,做的却是「没有人类行」,名实不符会被一再重新发现(平台内已有三处按人口作答:isBootstrapCreation、probeWalledOwnerAccountState,以及按全部行数作答的 bootstrap-status 处理器)。A 单独留着这个名实缺口;B 想用「打开一扇门」去买名实一致 —— 按本轴本义(缩小而非扩大特例)读,⛔ 不构成对 B 的背书。
② 实际业务拉动:零 —— 本形态是 #14157 的实现者构造出来的,不是哪位运营商报的障,今天没有一位客户撞上。按零拉动默认走 ④ 不扩散。
③ 防 AI 犯错:今天这台部署静默死掉,运营商能看见的唯一症状是「没人持有的那副凭据上的一个 401」;C 把沉默换成开机时的响亮拒绝(响亮拒绝优于静默容忍)。B 的失败面反过来同样静默:一台围墙部署被路过的陌生人抢到第一个账号,而代价记在别人的数据上。
④ 创业阶段不扩散:C 只加一条开机诊断,不新增任何长期声明义务;B 新增一条永久姿态承诺(「什么时候允许陌生人自助开户」),每一条这种已声明的键都是永久维护义务。
推荐:C(已拆为 #14353,与本裁无关可立即执行)+ A 暂留现状待裁;B 恒交维护者 —— 它动的是「围墙部署上谁可以开户」,属安全/权限边界,⛔ 席位不代裁,四棱同向也不改这一点。
置信缺口(本分析看不见什么):看不见是否已有真实部署处于这个形态 —— 平台无此遥测,卡上那 13 行是构造出来的;也没有量出 B 若落地需同时移动几处谓词才不制造新的分歧(已数出三处读同一人口,未逐处验证)。
Generated by Claude Code
Activity
huangyiirene commentedon Sep 2, 2026
Triage: stays in the decision inbox.
needs-user-decision·priority:p1·security·domain:services· typeFeature. The standard four-facet block is now on the card face (decision-inbox duty: the block is completed when a card enters the inbox, not when the maintainer arrives).Faces verified on
origin/main@72adb7f, not taken on the filer's word:packages/plugins/plugin-auth/src/auth-manager.ts:3866—isBootstrapCreation()pagessys_userand returnsfalseifrows.some(isHumanUserRow). It counts humans. ✔packages/plugins/plugin-auth/src/auth-plugin.ts:1987— thebootstrap-statushandler isconst count = await dataEngine.count('sys_user', {}); return c.json({ hasOwner: (count ?? 0) > 0 }). It counts every row,usr_systemincluded — so the disagreement the card describes between the handler and the admission gate is real onmaintoday. ✔packages/plugins/plugin-auth/src/walled-owner-verification-path.ts:143—isDevAdminSeedArmed()opens withif (env().NODE_ENV !== 'development') return false. The seed is hard-gated as claimed. ✔invite_onlyand theSELF_REGISTRATION_CLOSEDrefusal are pinned bypackages/spec/src/migrations/entries/semantic/18.audience-posture-default-invite-only.ts. ✔hasBootstrapWindowdoes not exist onmain— consistent with the card, which describes it asmaybeSeedDevAdmingates on "any sys_user row", but runs atkernel:ready— so any app that seeds people permanently loses theobjectstack devlogin #14157's change. ✔Two facts the card does not carry, both of which the maintainer will want when ruling:
probeWalledOwnerAccountState(feat(auth): walled deployment's declared owner is email-verified at operator-provisioned creation (extends #11343 to production walled boots) #12751,walled-owner-verification-path.ts:226) endsreturn humansExist ? 'owner-absent' : 'no-human-users', keyed on the sameisHumanUserRow. So three sites answer three slightly different questions about one population today. B has to move them together or it creates new disagreements in place of the onemaybeSeedDevAdmingates on "any sys_user row", but runs atkernel:ready— so any app that seeds people permanently loses theobjectstack devlogin #14157 just closed. That raises B's cost above what the card implies.content/docs/deployment/self-hosting.mdx:428tells the operator to "have each of those people register with exactly that address and verify their email" — there is nothing to grant and nothing to click. On the card's population that path is refused by theinvite_onlydefault with no administrator to invite. So "provision an account out of band" under option A is not merely undocumented; the published documentation actively points at the door that is shut.Why
priority:p1with zero reported incidence. Nobody has hit this — it was constructed by #14157's implementer, and the platform has no telemetry that would say otherwise (recorded in the facets confidence gap). p1 is for the consequence class, not the rate: the failure is total, irreversible from inside, and silent, and the ruling also fixes what #14353's diagnostic is allowed to say.Why type
Feature. The mechanical boundary test reads the change under consideration, and limb B widens the public accept set (a visitor refused today would be admitted).Featuretherefore lands this on the human floor by routing as well as by subject. ⛔ This is a routing decision, not a pre-judgment that B wins — A and C change no accept set at all.Human floor, no seat adjudication. "Who may create an account on a walled deployment" is a security/permission boundary. The four facets happen to point one way (C, with A as the standing state), but alignment does not open the auto-adjudication gate here — that gate requires the card to be off the human floor, and this one is not. The recommendation on the card face is an input to the maintainer, never a release.
Not presented yet. Decision batch 1 is outstanding and unanswered; ⛔ no second batch until it is. This card rolls into the next batch with the two facts above written into its prose.
Option C is already separable and moving: #14353 carries it as
pm:queue/p2/domain:services, because the filer is right that it composes with every limb. Its message text is the one thing that depends on this ruling, and that dependency is now recorded there.Generated by Claude Code
os-sales commentedon Sep 2, 2026
Decision facets block — Chinese per the maintainer's 2026-08-19 instruction that decision analyses on issues are written in Chinese. Filed into the decision inbox (
needs-user-decision) by thedomain:servicesPM seat, session_01AUF1NoViznQK32gqpK8wS8, 2026-09-02. Not self-adjudicated: option B moves a public door, which is the maintainer's floor.<!-- os-decision-facets -->
四维分析
defineStack({ data })种目录、不种凭据、NODE_ENV不是 development」,症状是 401 且没有任何人能开邀请。零拉动 ⇒ 默认不扩散。kernel:ready;级别须遵守 [Decision] plugin-sharing's refused-backfill report lands atwarnwhere AGENTS.md puts it aterror— and the card that was supposed to carry the level is CLOSED #13398 类裁决——经已发布 sink 形状报告的站点不得升到error,实施时按warn或按该 sink 允许的最高级别定)。推荐:C,现在就做(零门移动,与任何终态可组合)。 ①指向的终态是「收窄版 B」,但它是接受集变化,只能由裁决给;裁决之前 C 是唯一既不缩也不扩接受集的一步。B 仅凭裁决;若裁 B,请同时裁「窗口是否只对匹配目录地址的注册开放」这一收窄。回退项 A(什么都不做,由运维文档承担出口)。
置信缺口(本分析看不见什么):(a) 现网是否已存在「种目录不种凭据」的非 development 部署——零拉动是脚手架读数,不是现网读数;(b) 「匹配目录地址才放行」变体与 better-auth 的
signUpEmail流程是否可组合——未测量;(c) C 的诊断在多副本下是否每副本各喊一次——噪声上界未测。裁后执行段(你不用管):裁 C ⇒ 本席立
domain:services卡派发(plugin-authkernel:ready诊断 + 测试,级别按 #13398 类裁决),不触公开门;裁 B(含收窄版)⇒ 接受集变化先过 Clause-② 契约复核,与 #14348(plugin-security 提升最老的人行)同批考虑,因为两者合起来才决定「谁能登录、谁是管理员」;裁 A ⇒ 本卡关 not planned,出口写进运维文档卡。Generated by Claude Code
os-project-manager commentedon Sep 2, 2026
Maintainer ruling recorded — A: the bootstrap carve-out keeps counting humans; no public door moves
Director seat, summon #8, session
session_01ShyhexkB2d1AeRZ85tgAAe, 2026-09-02.Provenance (who / verbatim / where): maintainer, live PM chat with the director seat, 2026-09-02, replying to decision batch #2 in which this card was item 1 with the recommendation 「A 现状不动(C 已在队列,自动落地);B′ 记为出现真实客户拉动时的形状,不预批」. Verbatim reply: 「14361 你不处理;14261 现在就改,定位为 minor;其他同意」 — 「其他同意」 adopts the recommendation as presented.
Ruled: A.
isBootstrapCreation()keeps answering on the human population; the admission gate andbootstrap-statusstay in agreement as #14157 left them. Option B (count logins) is not taken. The narrowed shape B′ — the window opens only when no login exists AND the registering address matches a directory row that already exists — is recorded here as the shape to present again if a real deployment reports the dead end; it is not pre-approved, and it stays on the security/permission floor. Option C is already separable and queued as #14353 (boot-time diagnostic when human rows exist and nosys_accountdoes); its message text may now say plainly that the door stays shut and the remedy is out-of-band provisioning.Follow-up filed in the same stroke: a
documentationcard forcontent/docs/deployment/self-hosting.mdx— the published operator instruction ("have each of those people register with exactly that address") points at the door theinvite_onlydefault shuts on this population; the page must document the path that actually works, measured, not assumed.State transition, same stroke:
needs-user-decisioncleared and the card closed not planned — the decision is recorded, the door does not move, reopen is free if pull appears. Ledger: director seat post #12708, summon #8.Generated by Claude Code
fix(plugin-auth): say remedy (b) cannot recover an EXISTING person at…
fix(plugin-auth): name a remedy that works in the no-sign-in boot rep…