diff --git a/docs/requirements/onboarding-wizard.md b/docs/requirements/onboarding-wizard.md index 689ced06..1f51dc68 100644 --- a/docs/requirements/onboarding-wizard.md +++ b/docs/requirements/onboarding-wizard.md @@ -48,6 +48,26 @@ better-auth ban, with no separate state (F9). Example-data seeding is mandatory; the skippability policy is removed (PRD §6.4) (N2). All drafting additions are consolidated in §7. +Revision 4 (sol re-review 2: residual F2/F5/F7/N1, new N3/N4): the +post-epoch application phase is replaced by a single **bootstrap +finalize command** — step 3 submits the first-admin fields and every +collected value (settings, registration mode, applicable JIT choices, +seed parameters) in one command on identity §3's bootstrap surface, +executed server-side in one transaction that creates the admin, closes +the epoch, and commits the carried values under the new admin's +authority; after it, every collected value is canonical state, so an +interrupted run resumes from canonical state alone (N1, N3). The v1 +first admin is password-only; identity §3.6's SSO variant is not +composed by the v1 wizard (N1's SSO ordering). The bootstrap-status +response is a closed two-field schema (epoch state plus a +pre-epoch-only mode field), with the contract 6 §2.2 amendment +disclosed (F2). The idempotency envelope is completed: fence rows +record operation, actor, scope, and payload digest; replay is +re-authorized; changed-payload collision is refused; only committed +outcomes are recorded; the concurrent loser receives the winner's +recorded outcome (F7). §7 now names the contract 5 §3.1 rank-6 +mapping rows the composition expansion amends (F5, N4). + Scope: the Gateway-backed product onboarding wizard. Out of scope: the host-local install wizard (`mosaic wizard`, which drives host install and gateway bootstrap and is not this artifact — audit REPORT.md layer 3); @@ -69,13 +89,17 @@ through the extensibility rule §2.4). Gateway call issued from the wizard's web modules (or the equivalent scripted sequence), so contract 5 §6.1's outbound-call inventory, scoped to those modules, sees the wizard's complete operation set. - The composed operations span six existing families, each already - within contract 5's mapping: the rank-1 hierarchy commands and the - rank-4 enrollment command (contract 5 §3.1 — built first), the - settings command family (steps 1–2), the identity bootstrap and - registration surface (step 3), the mode reads (contract 6 §2.2 - post-epoch; the §2.3 bootstrap-status field pre-epoch), and the - ordinary content commands used for example seeding (step 4). + The composed operations span six existing families: the rank-1 + hierarchy commands and the rank-4 enrollment command (contract 5 + §3.1 — built first), the settings command family (steps 1–2), the + identity bootstrap and registration surface (step 3, including the + §3.3 finalize command), the mode reads (contract 6 §2.2 post-epoch; + the §2.3 bootstrap-status field pre-epoch), and the ordinary + content commands used for example seeding (step 4). Two of these + families (ranks 1 and 4) are in contract 5 §3.1's live rank-6 + composition row; the other four are added by the disclosed §7.8 + mapping amendment, without which their operations are unmapped and + blocked under contract 5 §5. 2. **The wizard introduces no new mutation surface.** Every state change it performs is an existing command with its own contract: user and epoch writes under the identity contract §3, hierarchy writes under @@ -88,8 +112,10 @@ through the extensibility rule §2.4). system's current configuration (read through the same commands) and applies deltas; the wizard does not keep an answer file whose contents can drift from reality. Values a first run has collected but not yet - applied (the §3.3 application rule) are transient run state, not a - persisted answer file. The wizard MAY persist run bookkeeping + committed by the §3.3 finalize command are transient run state, not + a persisted answer file; after finalize every collected value is + canonical state, and no transient value is needed to resume + (§4.4). The wizard MAY persist run bookkeeping (started/completed timestamps); bookkeeping is a cache, never the source of truth for any decision. The §4.3 idempotency fence is command-layer canonical state, not wizard bookkeeping — losing @@ -118,9 +144,18 @@ through the extensibility rule §2.4). unreachable there; the pre-epoch flow instead branches on the mode value reported by the **unauthenticated bootstrap-status response** of the bootstrap surface (identity §3), which evaluates the mode - record server-side and discloses exactly one field beyond epoch - state: the recorded mode value (drafting addition, §7). Post-epoch - wizard runs read mode through contract 6 §2.2 unchanged. Changing + record server-side. The bootstrap-status response is a **closed + two-field schema**: `epoch` — one field whose value set is exactly + the bootstrap epoch states identity §3 defines for + `bootstrap_state`, closed to that enumeration — and `mode` — the + recorded mode value, present **only while the epoch is open** and + absent from the response schema once the epoch has completed. No + other field exists in the response. The unauthenticated pre-epoch + mode disclosure is a disclosed amendment to contract 6 §2.2's + authenticated-read rule (§7.9); post-epoch, §2.2's + authenticated-only rule holds unchanged and the bootstrap-status + response carries no mode field. Post-epoch wizard runs read mode + through contract 6 §2.2 unchanged. Changing mode later is conversion (contract 6), not a wizard re-run. In v1 a recorded mode of `enterprise` is refused at bootstrap (contract 6 §5), so the wizard's Enterprise branch is unreachable in v1; a @@ -140,46 +175,72 @@ the named authority: 1. **System and company name.** System name is a settings value; company name feeds step 4's company creation. On a first run this step - collects and validates only — application follows step 3's - application rule below. On a re-run (an authenticated admin exists) + collects and validates only — the collected values are committed by + step 3's finalize command (§3.3). On a re-run (an authenticated admin exists) it applies directly through the settings commands. 2. **Component choices.** Mosaic Comms/Matrix vs external; Mosaic SSO/Authentik vs external; Mosaic DB/PostgreSQL vs external; vector DB (D4). v1 records the choices as settings (first run: collected, - applied per step 3's application rule; re-run: applied directly) and + committed by step 3's finalize command §3.3; re-run: applied + directly) and configures what is installable in-product; component provisioning beyond that is host tooling, out of scope here. -3. **Initial user.** Email, password, and display name, or SSO (PRD §6 - step list: "email/password/name/SSO"), per identity §3: the - first-admin transition is atomic, one per bootstrap epoch, and the - SSO variant runs inside the bootstrap transaction (identity §3.6). - The display-name source is bound per path: the password path uses - the submitted display name; the SSO variant uses the name claim - mapped under identity §5's claim mapping. Canonical `users.name` - MUST equal that source value (witness §6.5); no field of the PRD's - step list is dropped. The wizard also presents the applicable +3. **Initial user and finalize.** Email, password, and display name + (PRD §6 step list: "email/password/name/SSO"), per identity §3: the + first-admin transition is atomic and one per bootstrap epoch. **The + v1 first admin is password-only.** A step-2 SSO choice cannot yield + a canonically configured provider before the epoch closes, so an + SSO-authenticated first admin is not constructible in this flow; + the v1 wizard does not compose identity §3.6's SSO variant, and the + PRD step list's SSO option is satisfied post-epoch (providers + configured and JIT-enabled after finalize; identity §5 governs + every SSO-created account). This is a disclosed sequencing decision + (§7.10). The password path's canonical `users.name` MUST equal the + submitted display name; any SSO-created account's name is bound to + identity §5's mapped name claim (witness §6.5); no field of the + PRD's step list is dropped. The wizard also presents the applicable mandatory choices — registration mode (identity §2.2) and, for each - configured SSO provider, that provider's JIT enablement with its - allowlist warning (identity §4.1, §4.3). The wizard MUST obtain an - explicit submission for each applicable mandatory choice and MUST - refuse step completion while one is unsubmitted. This is a - presentation-and-submission obligation on wizard runs only: - identity's own defaults (registration `closed`, JIT off) continue to - govern everything a wizard run never touches, and this contract - creates no new stored-record obligation beyond those identity itself - defines. **Application rule (first runs):** immediately after the - epoch transition, authenticated as the newly created admin — the - first-admin transition (identity §3) yields the account holding the - RBAC §1.1 platform authority that system and component settings - writes require — the run applies the values collected in steps 1–2 - through the ordinary settings commands. Before the transition the - run holds collected values only: no canonical settings write occurs - pre-epoch, and no pre-admin write authority exists or is invented - (disclosed sequencing addition, §7; witness §6.11). -4. **Initial hierarchy and seeding.** Runs strictly after step 3's epoch - transition, authenticated as the newly created admin. The bootstrap - writer (identity §3) creates only the first account and closes the - epoch; it has no hierarchy authority and creates no hierarchy node. + SSO provider canonically configured at finalize time, that + provider's JIT enablement with its allowlist warning (identity + §4.1, §4.3); a provider configured after finalize receives its JIT + choice at configuration time under identity §4.1's default. The + wizard MUST obtain an explicit submission for each applicable + mandatory choice and MUST refuse finalize while one is unsubmitted. + This is a presentation-and-submission obligation on wizard runs + only: identity's own defaults (registration `closed`, JIT off) + continue to govern everything a wizard run never touches, and this + contract creates no new stored-record obligation beyond those + identity itself defines. **Finalize rule (first runs):** step 3 + ends in a single **bootstrap finalize command** on identity §3's + bootstrap surface — a disclosed extension of that surface (§7.3) — + carrying the first-admin fields and every collected value: the + step-1 system name, the step-1 company name (recorded as the §3.4 + seed parameter), the step-2 component choices, the + registration-mode submission, and the applicable JIT submissions. + The command executes **in one transaction**: it creates the admin + and closes the epoch exactly as identity §3 defines, then — with + the new admin existing inside the same transaction, holding the + RBAC §1.1 platform authority those writes require — commits every + carried value through the ordinary settings and identity + configuration writes, each attributed in audit to the new admin. + Either the whole transaction commits or none of it exists (identity + §3.5's atomicity extends over the carried writes). Before finalize + the run holds collected values only: no canonical write of any kind + occurs pre-epoch, and no pre-admin write authority exists or is + invented (witness §6.11). After finalize, every collected value is + canonical state; no transient value is needed to resume (§4.4, + witness §6.7). +4. **Initial hierarchy and seeding.** Runs strictly after step 3's + finalize transaction, authenticated as the newly created admin. The + bootstrap writer (identity §3) creates only the first account, + closes the epoch, and commits the §3.3 carried values; it has no + hierarchy authority and creates no hierarchy node. The seed + sequence is derived entirely from canonical state: the seed + parameter the finalize command recorded (the company name) and the + fixed example set (§4.3). A resumed run — including a fresh client + holding none of the original run's transient state — reconstructs + the same ordered sequence and the same deterministic §4.3 keys from + that canonical state alone. The first company is created by the ordinary top-level company command under §5.2's eligibility policy, with the new admin as actor, naming the admin as initial `owner` in the same audited operation @@ -220,18 +281,45 @@ collects no sensitive category, so v1 ships no custody surface. epoch. The at-most-once mechanism is an **idempotency key**, a disclosed drafting addition to contract 5's §4 command envelope ratified with this contract (§7): a mutating command MAY carry a - caller-supplied idempotency key; the command layer records the key - in a uniqueness-constrained fence table **in the same transaction** - as the mutation and its audit event, and a submission whose key is - already recorded executes nothing and returns the recorded outcome. + caller-supplied idempotency key. The complete envelope: + - **Fence row.** The command layer records, in a + uniqueness-constrained fence table **in the same transaction** + as the mutation and its audit event: the key, the operation + identifier, the acting principal, the mutation's authorization + scope (the target company, or the platform scope for top-level + operations), a digest of the canonicalized request payload, and + a reference to the committed outcome. Fence uniqueness is the + pair (operation identifier, key). + - **Replay.** A submission whose (operation, key) pair is recorded + is first authorized exactly as a fresh submission would be — an + actor who is not authorized receives the authorization refusal, + never the recorded outcome. If authorization passes and the + recorded actor, scope, and payload digest all equal the + submission's, the command executes nothing and returns the + recorded outcome. + - **Collision.** A submission whose (operation, key) pair is + recorded but whose actor, scope, or payload digest differs is + refused with a single bounded conflict error (contract 5 §4.2); + it executes nothing and discloses nothing of the recorded + outcome. + - **No error replay.** The fence row commits only with its + mutation, so only committed outcomes are ever recorded. A failed + or refused submission records no fence row; a retry executes + afresh. There is no recorded-error state. + - **Concurrency.** Two submissions with the same (operation, key) + pair serialize on the fence's unique constraint: exactly one + executes. The loser waits for the winner's transaction to + resolve; if it committed, the loser is handled as a replay + (authorization first, then the recorded outcome, or the + collision refusal on mismatch); if it aborted, no fence row + exists and the loser executes. The loser never performs a second + mutation and is never left without a defined response. + The wizard submits deterministic keys derived from the bootstrap epoch and the seed role (e.g. epoch id + "seed-company") for every seed mutation. Correlation ids (contract 5 §4.3) remain pure trace - linkage and carry no idempotency semantics. Two concurrent resumed - runs serialize on the fence's unique constraint: exactly one - execution per key; the other submission receives the recorded - outcome or a single bounded conflict refusal, never a second - mutation. Wizard bookkeeping of seed node ids remains a cache + linkage and carry no idempotency semantics. Wizard bookkeeping of + seed node ids remains a cache (§1.3): losing it cannot cause duplication, because the fence is canonical command-layer state. A re-run offers to create _additional_ hierarchy nodes (PRD: users can create N companies, N @@ -240,18 +328,26 @@ collects no sensitive category, so v1 ships no custody surface. likewise at-most-once per epoch; the example set is fixed and its seeding is not skippable (PRD §6.4: a completed first run contains the fixed example set). + 4. **Interrupted runs strand nothing.** The wizard is resumable at the - granularity of a single command: each mutation either commits through - its command with its same-transaction audit event and fence row, or - leaves nothing (identity §3.5 for the bootstrap step). Step 4 is a - sequence of individually atomic commands, not one transaction: an - interruption between them leaves a prefix of committed seed nodes, - and the §4.3 fence makes the resumed run complete exactly the - remaining suffix — the run re-submits the full ordered seed sequence - with the same deterministic keys, already-committed mutations return - their recorded outcomes, and only the remainder executes, without - duplication and without compensating rollback of completed commands. - There is no wizard-level transaction spanning steps. + granularity of a single command: each mutation either commits + through its command with its same-transaction audit event and fence + row, or leaves nothing (identity §3.5, extended over the carried + writes, for the §3.3 finalize command). A run interrupted before + finalize has written nothing; its collected values are re-entered. + A run interrupted after finalize resumes from canonical state + alone: every collected value was committed by the finalize + transaction, and the remaining work — the step 4–5 command + sequence — is derivable from it (§3.4). Step 4 is a sequence of + individually atomic commands, not one transaction: an interruption + between them leaves a prefix of committed seed nodes, and the §4.3 + fence makes the resumed run complete exactly the remaining suffix — + the run re-derives the full ordered seed sequence from canonical + state and re-submits it with the same deterministic keys, + already-committed mutations return their recorded outcomes, and + only the remainder executes, without duplication and without + compensating rollback of completed commands. There is no + wizard-level transaction spanning steps. ## 5. Seeding authority (resolves contract 2 review NEW-1) @@ -299,10 +395,12 @@ Binding on the implementing PRs: 2. **Mode witnesses:** the branch point exists — a pre-epoch flow resolves its step set from the bootstrap-status mode field (§2.3), a post-epoch run from contract 6 §2.2's authenticated read; the - bootstrap-status response discloses exactly one field beyond epoch - state (closed-field assertion on the response schema); post-epoch, - an unauthenticated mode read is refused (contract 6 §2.2 holds - unchanged); v1 with mode `standalone` yields the §3 step set; a + bootstrap-status response matches §2.3's closed two-field schema + exactly while the epoch is open, and after the epoch completes + contains the epoch field only — the mode field absent from the + response (closed-field assertion on the response schema in both + phases); post-epoch, an unauthenticated mode read through any + surface is refused (contract 6 §2.2, as amended by §7.9, holds); v1 with mode `standalone` yields the §3 step set; a simulated unsupported or unreadable mode record yields one bounded precondition refusal (§2.3) and no partial flow; wizard sources contain no mode write and no feature-state mode derivation. @@ -339,10 +437,12 @@ Binding on the implementing PRs: 5. **Explicit-choice witness:** a run submitting explicit values completes, and each canonical value equals the value the run submitted (asserted against the captured submissions, not by reading - the canonical store twice); canonical `users.name` equals the path's - declared source — the submitted display name for the password path, - the identity §5 mapped name claim for the SSO variant (asserted - against the captured submission or captured assertion); a run with + the canonical store twice); canonical `users.name` equals the + declared source — for the first admin (password path), the + submitted display name; for an SSO-created account (a post-epoch + JIT-provisioned user), the identity §5 mapped name claim (each + asserted against the captured submission or captured assertion); a + run with an applicable mandatory choice unsubmitted is refused at step 3 (§3.3); identity's defaults are asserted for a provider the run never touched (registration `closed`, JIT off — identity §2.2, @@ -350,23 +450,35 @@ Binding on the implementing PRs: 6. **Gating-independence witness:** with any wizard-completion marker deleted (§1.4), authorization and registration gating behave identically — proving no gate reads wizard state. -7. **Interrupted-run witness:** inject failure after each internal - mutation of a run — after the bootstrap transaction, after each - individual seed command (company, estate, project, workspace, each - example item), and after the bookkeeping cache write — and for each - injection point assert: no stranded partial outcome (every committed - command has its same-transaction audit event and fence row; nothing - else exists), and a resumed run completes exactly the remaining - mutations with no duplication (identity §8 covers the bootstrap - step; §4.3–§4.4 define the fence the witness exercises). Deleting - the bookkeeping cache between failure and resume MUST NOT change the - outcome. **Idempotency-fence witnesses:** re-submitting a committed - seed command with its same key executes nothing and returns the - recorded outcome (asserted by mutation-count and audit-event-count - equality); two concurrent resumed runs executing the seed sequence - yield exactly one seed set — per key, exactly one mutation and one - audit event exist, and the losing submission received the recorded - outcome or the single bounded conflict refusal. +7. **Interrupted-run witness:** inject failure at each point of a + run — **inside the finalize transaction** (after admin creation but + before the carried writes commit: assert nothing exists — no + account, the epoch still open, canonical settings untouched, no + fence row, no audit event), after the committed finalize + transaction, after each individual seed command (company, estate, + project, workspace, each example item), and after the bookkeeping + cache write — and for each injection point assert: no stranded + partial outcome (every committed command has its same-transaction + audit event and fence row; nothing else exists), and a resumed run + completes exactly the remaining mutations with no duplication + (identity §8 covers the epoch transition; §4.3–§4.4 define the + fence the witness exercises). The resume after the committed + finalize MUST be performed by a fresh client holding none of the + original run's transient state, deriving the remaining seed + sequence from canonical state alone (§3.4). Deleting the + bookkeeping cache between failure and resume MUST NOT change the + outcome. **Idempotency-fence witnesses (§4.3):** re-submitting a + committed seed command with its same key, actor, and payload + executes nothing and returns the recorded outcome (asserted by + mutation-count and audit-event-count equality); the same key with a + changed payload digest is refused with the single bounded conflict + and executes nothing; a replay by an actor who has since lost + eligibility (identity §7.1 ban) receives the authorization refusal, + not the recorded outcome; a submission that failed before commit + leaves no fence row and its retry executes; two concurrent resumed + runs executing the seed sequence yield exactly one seed set — per + key, exactly one mutation and one audit event exist, and the losing + submission received the winner's recorded outcome. 8. **Actor-matrix witness (§5.2):** post-bootstrap top-level company creation succeeds for an ordinary authenticated non-admin user (positive), and is refused for an unauthenticated caller and for a @@ -383,13 +495,17 @@ Binding on the implementing PRs: 10. **Enrollment witness:** the enrollment step exists, invokes only the rank-4 family, and a run that skips it completes with zero enrollment-family mutations. -11. **Pre-epoch authority witness (§3.3 application rule):** during a - first run, before the epoch transition, canonical settings are - untouched (settings-state comparison against the pre-run capture), - and an injected settings-write attempt from the unauthenticated - pre-epoch flow is refused with contract 5 §4.2's authorization - class; after step 3, the collected values are applied and each - application is attributable in audit to the new admin. +11. **Pre-epoch authority and finalize witness (§3.3):** during a + first run, before finalize, canonical state is untouched + (settings-state comparison against the pre-run capture), and an + injected settings-write attempt from the unauthenticated pre-epoch + flow is refused with contract 5 §4.2's **authentication-failure + class** (the caller is unauthenticated, not an + authorized-but-refused actor); the finalize command commits the + admin, the epoch transition, and every carried value in one + transaction, each carried write attributable in audit to the new + admin; the §6.7 inside-finalize injection covers the atomicity + negative. ## 7. Drafting additions (PRD §12.1 disclosure) @@ -400,18 +516,41 @@ contracts and are not additions: 1. Enrollment-step skippability (§3.5) — the PRD step list does not mark the step optional. -2. The unauthenticated bootstrap-status mode disclosure — exactly one - field (the recorded mode value), during the bootstrap epoch only - (§2.3). -3. The collect-first, post-epoch application sequencing for steps 1–2 - (§3.3 application rule). -4. The idempotency-key field and its same-transaction uniqueness fence - (§4.3) — an addition to contract 5 §4's command envelope, proposed - and ratified here, severable from the rest of this contract. +2. The unauthenticated bootstrap-status mode disclosure — the closed + two-field response schema (epoch state plus the recorded mode + value, the latter present during the bootstrap epoch only) (§2.3). +3. The **bootstrap finalize command** (§3.3) — an extension of + identity §3's bootstrap surface: one transaction carrying the + first-admin fields, the collected settings, registration-mode, and + JIT values, and the §3.4 seed parameter, committing the carried + writes under the new admin's authority with identity §3.5's + atomicity extended over them. Severable from the rest of this + contract. +4. The idempotency-key field and its same-transaction uniqueness + fence, with the §4.3 replay, collision, no-error-replay, and + concurrency rules — an addition to contract 5 §4's command + envelope, proposed and ratified here, severable from the rest of + this contract. 5. The presentation-and-submission obligation for applicable mandatory choices on wizard runs (§3.3). 6. The no-server-side-orchestrator architectural constraint (§1.1). 7. The wizard-completion-marker presentational-only rule (§1.4). +8. The contract 5 §3.1 rank-6 mapping expansion (§1.1): the rank-6 + composition row, which today names only the rank-1 hierarchy and + rank-4 enrollment families, is amended to name all six composed + families — adding the settings command family, the identity + bootstrap and registration surface (including the §3.3 finalize + command), the mode reads (contract 6 §2.2 and the §2.3 + bootstrap-status field), and the ordinary content commands used for + example seeding — with one mapping row per newly named family. + Without this amendment those operations are unmapped and blocked + under contract 5 §5. +9. The contract 6 §2.2 amendment permitting the single unauthenticated + pre-epoch mode disclosure through the §2.3 bootstrap-status field; + post-epoch, §2.2's authenticated-only rule is unchanged. +10. The password-only v1 first admin (§3.3): identity §3.6's SSO + variant is not composed by the v1 wizard; the PRD step list's SSO + option is satisfied post-epoch. ## Ruling request