diff --git a/docs/requirements/onboarding-wizard.md b/docs/requirements/onboarding-wizard.md index 11d422fa..57ed7aa2 100644 --- a/docs/requirements/onboarding-wizard.md +++ b/docs/requirements/onboarding-wizard.md @@ -122,6 +122,36 @@ digests asserted, every seed outcome replayed, zero new nodes, run completed — proving the dirty form value never enters derivation and the renamed row resolves cache-independently (F6). +Revision 8 (sol re-review 6: NEW-9): shared replay gains a +**target-result authorization boundary** — returning a recorded +outcome is a read of the canonical records it references, authorized +separately from fresh-mutation authorization: a shared replay +requires the submitter to hold, at replay time, read authority on +every record the recorded outcome references, and a submitter who +passes the fresh-mutation check but lacks target read authority is +refused with the same constant-shape single bounded conflict as a +§4.3 collision, identifying no record — the response confirms no +tenant target's existence, preserving RBAC §7's no-existence-oracle +rule (§4.3). `shared` becomes server-verified seed-only policy: the +command layer re-derives the epoch's **canonical seed tuple set** +from canonical state alone and refuses any shared declaration whose +(operation, key, scope, digest) tuple is not a member, and a missing +seed fence is originated only by a submission passing the full +fresh-mutation authorization for that seed command — a wrong actor +cannot win an unrecorded seed key, and no shared row can exist +outside the seed sequence (§4.3). An authorized shared replay appends +a **replay access event** — a distinct non-mutation audit event class +recording the replaying actor, the current request's correlation ids, +and the fence row whose outcome was returned — so every access to a +recorded outcome is attributed without a duplicate semantic mutation +event, while the fence row and the mutation event immutably retain +the original actor (§4.3, §7.4). §6.7 adds the unauthorized-target +replay refusal, the non-seed shared declaration refusal, and the +unrecorded-seed-key race; §6.3's second-admin witness grants B +explicit read authority on the recorded seed targets and asserts B's +replay access events; replay-affected count equalities are scoped to +the mutation event class (NEW-9). + 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); @@ -345,8 +375,9 @@ the named authority: (recorded outcomes) rather than collide, regardless of any hierarchy rename performed since — and, because every seed submission declares §4.3's `shared` replay mode, they replay for - whichever currently eligible admin performs the re-run, not only - the actor the fence rows record. The §4.3 fence, not the + whichever currently eligible admin holding §4.3 target-result + read authority on the recorded seed targets performs the re-run, + not only the actor the fence rows record. The §4.3 fence, not the parameter's presence or absence, is what prevents re-seeding; the parameter is provenance the derivation reads, never a value any later step may change. @@ -409,15 +440,37 @@ collects no sensitive category, so v1 ships no custody surface. the recorded scope and payload digest equal the submission's, the row's replay mode decides the actor rule: an `actor-bound` row additionally requires the recorded actor to equal the - submitter; a `shared` row replays for **any** authorized - submitter. On replay the command executes nothing and returns - the recorded outcome. A shared replay adds no audit event and - leaves the fence row untouched: audit attributes each mutation - to the actor who performed it, the recorded acting principal is - immutable, and the recorded outcome discloses nothing beyond - what the replaying actor's just-verified live authorization - already grants — the same mutation that actor could have - performed afresh had no fence row existed. + submitter; a `shared` row replays for an authorized submitter + who also passes **target-result authorization**. Returning a + recorded outcome is a read of the canonical records that + outcome references, and fresh-mutation authorization is not + read authorization on records another actor already created: a + shared replay therefore additionally requires the submitter to + hold, at replay time, read authority on every canonical record + the recorded outcome references, under each record's owning + contract's read-authorization rules (for the seed targets, the + contract 2 grant model — RBAC §1.1 platform-admin standing + confers none of it). A submitter who passes the fresh-mutation + check but fails target-result authorization is refused with the + same single bounded conflict error as a collision below — + constant in shape, identifying no record, disclosing nothing of + the recorded outcome or its targets — so the refusal confirms + no tenant target's existence or identity and RBAC §7's + no-existence-oracle rule is preserved. On replay the command + executes nothing and returns the recorded outcome. A shared + replay adds no **mutation** audit event and leaves the fence + row untouched — audit attributes each mutation to the actor who + performed it, and the recorded acting principal is immutable — + but it appends a **replay access event**: a distinct + non-mutation audit event class recording the replaying actor as + accessing principal, the current request's correlation ids + (contract 5 §4.3), and a reference to the fence row whose + outcome was returned. The current submission's correlation + travels only in that access event and the trace layer; the + recorded outcome and its original mutation event are returned + and retained unchanged. Every access to a recorded outcome is + thereby attributed to its accessing actor without a duplicate + semantic mutation event ever existing. - **Collision.** A submission whose (operation, key) pair is recorded but whose scope, payload digest, or declared replay mode differs — or, against an `actor-bound` row, whose actor @@ -425,6 +478,30 @@ collects no sensitive category, so v1 ships no custody surface. refused with a single bounded conflict error (contract 5 §4.2); it executes nothing and discloses nothing of the recorded outcome. + - **Shared-declaration boundary.** `shared` is a server-verified, + seed-only policy, never a caller privilege. The command layer + re-derives, from canonical state alone, the **canonical seed + tuple set** of the current bootstrap epoch: the exact + (operation identifier, key, authorization scope, payload + digest) tuples of §3.4's ordered seed sequence, derived from + the epoch id, the immutable `bootstrap.seed-company-name` + provenance, and the fixed example set — the same derivation + §3.4 requires of a fresh client. A submission declaring + `shared` whose tuple is not a member of that set is refused + with a single bounded refusal (contract 5 §4.2 validation + class) that executes nothing and records no fence row. No + other operation can carry a shared declaration, so no shared + fence row can exist outside the seed sequence — the seed-only + rule is enforced by the command layer, not by wizard + convention. **Origination.** A shared fence row not yet + recorded is originated only by executing its seed mutation: + the originating submission passes the full fresh-mutation + authorization for that seed command (§5.2 eligibility plus the + hierarchy authority the command itself requires), and a + refused submission records no row (no-error replay below). An + actor without the seed mutation's authority therefore cannot + win an unrecorded seed key, and the recorded actor of every + shared row was authorized for the mutation the row fences. - **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 @@ -433,8 +510,9 @@ collects no sensitive category, so v1 ships no custody surface. 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 + (fresh-mutation authorization first — and, for a shared row, + target-result authorization — 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. @@ -443,13 +521,19 @@ collects no sensitive category, so v1 ships no custody surface. seed mutation, and every seed submission declares the `shared` replay mode: the seed set is a per-epoch singleton (§4.4), derived from immutable provenance (§3.4), so whichever currently eligible - admin performs a re-run or resume must replay the recorded + admin holding target-result read authority on the recorded seed + targets performs a re-run or resume must replay the recorded outcomes rather than collide on actor identity — re-runnability is not restricted to the original acting admin (witness §6.3). All other submissions default to `actor-bound`; nothing in this - contract declares `shared` outside the seed sequence. Correlation + contract declares `shared` outside the seed sequence, and the + shared-declaration boundary makes that exclusivity + server-enforced: a shared declaration outside the canonical seed + tuple set is refused, not merely unconventional. Correlation ids (contract 5 §4.3) remain pure trace - linkage and carry no idempotency semantics. Wizard bookkeeping of + linkage and carry no idempotency semantics; a shared replay's + current correlation is carried by its §4.3 replay access event, + never by rewriting the recorded outcome or its mutation event. 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 @@ -584,14 +668,21 @@ Binding on the implementing PRs: still discriminates. **Second-admin re-run witness (§4.3 shared replay):** after admin A's completed first run, create a second account B and grant it - platform-admin and the hierarchy authority the seed commands - require (B is freshly authorized, and no text restricts re-runs - to A); delete the bookkeeping cache; B re-runs the wizard to - completion making no changes — every seed submission replays with - A's recorded outcome (asserted by mutation-count and - audit-event-count equality across the run, the fence rows still - recording A as acting principal), zero new hierarchy nodes, - users, grants, or example content exist, and the run completes. + platform-admin, the hierarchy authority the seed commands + require, and **explicit target-result read authority on every + recorded seed target** — a contract 2 grant conferring read on + the seed company and its seeded children (B is freshly + authorized with explicit target authority, and no text restricts + re-runs to A); delete the bookkeeping cache; B re-runs the + wizard to completion making no changes — every seed submission + replays with A's recorded outcome (asserted by mutation-count + and mutation-audit-event-count equality across the run, the + fence rows still recording A as acting principal), exactly one + §4.3 replay access event exists per replayed submission, each + naming B as accessing principal and carrying B's run's + correlation ids while referencing A's fence row, zero new + hierarchy nodes, users, grants, or example content exist, and + the run completes. **Second company:** a re-run creating a second company succeeds with the running user as actor and names the creator as `owner` in the same audited operation — asserted on the single audit event naming @@ -645,15 +736,19 @@ Binding on the implementing PRs: 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 + mutation-count and mutation-audit-event-count equality); the same + key with a changed payload digest is refused with the single bounded conflict and executes nothing; the same key and payload submitted by a different authorized actor against an **actor-bound** fence row (a control row recorded outside the seed sequence) is refused with the same single bounded conflict (actor mismatch) and executes - nothing, while against a **shared** row (every §3.4 seed fence) it - replays with the recorded outcome, executing nothing and adding no - audit event, the fence row still recording the original actor; a + nothing, while against a **shared** row (every §3.4 seed fence) a + different authorized actor holding target-result read authority + replays with the recorded outcome, executing nothing, adding no + mutation audit event, and appending exactly one replay access + event naming the replaying actor and its correlation ids, the + fence row still recording the original actor; a submission whose declared replay mode differs from the recorded row's is refused with the single bounded conflict and executes nothing; the same key, @@ -662,14 +757,42 @@ Binding on the implementing PRs: replay by an actor who has since lost eligibility (identity §7.1 ban) receives the authorization refusal, not the recorded outcome — asserted against a shared seed fence, - proving shared replay never bypasses fresh authorization; a submission that failed before commit + proving shared replay never bypasses fresh authorization. + **Target-authorization and boundary witnesses (§4.3, NEW-9):** + an **unauthorized-target shared replay** — a platform admin + holding §5.2 fresh-create eligibility but no contract 2 grant on + the seed company derives the canonical seed tuple per §3.4 and + submits it with the recorded `shared` mode — is refused with the + single bounded conflict, and the refusal response is asserted + byte-shape-identical to the changed-digest collision refusal + above (same error class, same fields, no record identifier, no + scope detail beyond the caller's own submission), executes + nothing, changes no fence row, and appends no mutation audit + event — proving the recorded target is neither returned nor + confirmed to exist and RBAC §7's no-existence-oracle rule holds + at the fence; a **non-seed shared declaration** — an ordinary + hierarchy or content command whose tuple is outside the + canonical seed tuple set, submitted with `shared` declared by an + actor fully authorized for the mutation — is refused with the + §4.3 validation refusal, executes nothing, and records no fence + row (asserted absent); an **unrecorded-seed-key race** — with + one seed tuple deliberately left unrecorded, an actor lacking + that seed command's hierarchy authority races the authorized + resume for the same (operation, key): the unauthorized + submission receives the authorization refusal and records no + fence row, the authorized submission executes afresh, and the + resulting fence row records the authorized actor — proving + origination of a missing seed fence is bound to full + fresh-mutation authorization; 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; and with the + key, exactly one mutation and one mutation audit event exist, and + the losing + submission received the winner's recorded outcome (its access + attributed by the §4.3 replay access event); and with the winner's transaction forced to abort, the waiting loser finds no fence row, executes, and commits exactly one mutation and one - audit event. Two envelope-shape witnesses complete the §4.3 + mutation audit event. Two envelope-shape witnesses complete the §4.3 coverage: the same raw key submitted to two DIFFERENT operations executes both independently, each committing its own mutation, audit event, and fence row — proving uniqueness is the @@ -738,11 +861,29 @@ contracts and are not additions: this contract. Including the **replay mode**: each keyed submission declares `actor-bound` (default) or `shared`, the declaration is recorded in the fence row, a shared row replays - for any freshly authorized actor whose scope and payload digest - match while an actor-bound row also requires recorded-actor - equality, and a declared-mode mismatch is a collision. This + for a freshly authorized actor whose scope and payload digest + match and who additionally passes the §4.3 target-result + authorization — read authority on every canonical record the + recorded outcome references, refused otherwise with the + constant-shape conflict that preserves RBAC §7's + no-existence-oracle rule — while an actor-bound row also + requires recorded-actor + equality, and a declared-mode mismatch is a collision. Including + the **shared-declaration boundary**: `shared` is server-verified + against the epoch's canonical seed tuple set re-derived from + canonical state, a shared declaration outside that set is + refused recording nothing, and origination of a missing seed + fence requires the full fresh-mutation authorization for that + seed command (§4.3). Including the **replay access event**: a + distinct non-mutation audit event class, appended on every + shared replay, recording the accessing actor, the current + request's correlation ids, and the fence row returned — the + disclosed mechanism by which a replay's access is attributed + without a duplicate semantic mutation event, the fence row and + mutation event immutably retaining the original actor. This contract declares `shared` for exactly the §3.4 seed sequence — - deterministic, epoch-scoped singletons any eligible admin must be + deterministic, epoch-scoped singletons any eligible, + target-authorized admin must be able to re-run (§4.1) — and for nothing else. 5. The presentation-and-submission obligation for applicable mandatory choices on wizard runs (§3.3).