From 6e16675ea2189eff1398aee1a9d622917c82efdf Mon Sep 17 00:00:00 2001 From: fred Date: Fri, 28 Aug 2026 22:57:11 +0000 Subject: [PATCH] docs: deployment mode and conversion contract (S2 contract 6) (#1439) --- docs/requirements/mode-conversion.md | 279 +++++++++++++++++++++++++++ 1 file changed, 279 insertions(+) create mode 100644 docs/requirements/mode-conversion.md diff --git a/docs/requirements/mode-conversion.md b/docs/requirements/mode-conversion.md new file mode 100644 index 00000000..f83522cd --- /dev/null +++ b/docs/requirements/mode-conversion.md @@ -0,0 +1,279 @@ +# Deployment Mode and Conversion Contract (D3) + +Status: DRAFT — awaiting ratification (webui-audit S2, contract 6 of 9). +Authority: PRD D3 (Part I §3) — two modes chosen at install time, +Standalone and Enterprise, with the mode table (brains, user-data +isolation, secrets, conversion); Standalone → Enterprise conversion is +**one-way** and Enterprise is a **terminal state**. PRD D14 (Part I §7) +— the per-user brain split is optional in Standalone and keeping it is +the recommended default because it preserves forward-compatibility with +the one-way conversion. PRD D11 (Part I §9) — v1 ships the Standalone +flow only; Enterprise conversion is explicitly deferred. PRD D3 +federation clause — federation is intentionally not fully designed, +deferred, and nothing in v1 may foreclose it. + +Revision 2 (luna review F1–F7): the identity precondition restated in +identity-contract terms with a conversion-local acknowledgment record +this contract owns (F1); a durable, keyed preparation state with a +Standalone-safe representation rule, an in-transaction re-check fence, +and an exact flip boundary (F2); the §5.4 unknown-value rule stated +directly without the contradictory non-exhaustiveness clause (F3); the +D14 boundary bound here with a stable column-allowlist witness instead +of delegated to an unratified layout (F4); the conversion witness +matrix extended to every §4.2/§4.4 condition (F5); the mode-record +writer coverage imported concretely from contract 1 §6.3 with a named +schema, closed writer set, crafted-write probe, and mode-resolution +assertion (F6); the mode read command flagged as a §12.1 drafting +addition rather than a D8 mandate (F7). Ownership language aligned +with contract 3 revision 2: mode is recorded at bootstrap and read by +the wizard as input. + +This contract binds the mode as a canonical platform property (§2), the +per-mode obligations and which contract owns each (§3), the conversion +transition (§4), the v1 non-foreclosure obligations (§5), and their +witnesses (§6). Domain semantics stay with their owning contracts: +wizard branching (contract 3 §2), identity/SSO +(`identity-lifecycle.md`), custody and per-user brain mechanics +(contract 7, `custody-schema.md`), tool mapping +(`tool-gateway-mapping.md`). + +## 1. Definitions + +1. **Mode**: the platform-wide deployment mode, exactly one of + `standalone` or `enterprise`. The vocabulary is closed in v1; + extension (e.g. a federation mode) is by amendment to this contract, + never ad hoc. +2. **Conversion**: the one-way transition `standalone → enterprise`. + No other mode transition exists. +3. **Conversion preconditions**: the verifiable conditions of §4.2 that + must all hold before the mode record may change. +4. **Preparation unit**: one re-runnable piece of pre-conversion work — + the migration of one secret to the Vault backend, or the partition + of one user's brain content (§4.3). + +## 2. Mode is a canonical recorded property + +1. Mode is recorded canonically in the platform database at bootstrap + as the operator's install-time choice (D3: modes are "chosen at + install time"). The record is a single-row keyed record + (`platform_mode`: mode value, recorded-at timestamp, bootstrap epoch + reference); this contract owns it, the bootstrap writer performs the + one v1 write (§6.2), and the wizard reads it as input (contract 3 + §2.3). Mode is never derived from feature state (presence of Vault, + count of brains, count of users), and no component may infer a + different mode than the record states. +2. The record is readable by any authenticated user through a Gateway + command with CLI exposure. This read command is a **drafting + addition** ratified with this contract (PRD §12.1), not a D8 + mandate: D8 binds only that any surface exposing the value goes + through official tooling. When a webUI surface consumes the read, a + mapping row is added to `tool-gateway-mapping.md` by amendment — + the same route §4.4 already binds for the conversion command. + Components branch on the read value only. +3. The record is immutable except by the §4 conversion transition. + Editing it by direct database access, config file, environment + variable, or wizard re-run is non-conformant (contract 3 §2.3: + changing mode later is conversion, not a wizard re-run). + +## 3. Per-mode obligations (owner map) + +The PRD mode table binds four rows; this contract assigns each an +owning contract so no obligation is unowned and none is bound twice: + +| Obligation | Standalone | Enterprise | Owner | +| ------------------- | -------------------------------------- | -------------------------------------------------- | --------------------------------------------- | +| Brains | one mosaic-brain (system + user files) | system brain for config + one brain per user | contract 7 (custody/brain mechanics) | +| User-data isolation | single user | no user-data leakage between users; sharing opt-in | contract 7 (enforced by architecture, D14) | +| Secrets | OpenBao/Vault or flat files | OpenBao/Vault REQUIRED | this contract (§4.2 gate; steady-state check) | +| Conversion | may convert to Enterprise, one-way | terminal state | this contract (§4) | + +The Standalone brains row states the default layout, not the only +valid one: the D14 per-user split is a MAY in Standalone with keeping +it the recommended default (PRD §7, contract 7 §6), and Vault-backed +secrets are equally valid Standalone configuration. Both prepared +states are therefore themselves valid Standalone states — the fact +§4.3 relies on. + +In Enterprise steady state, a flat-file secrets backend is +non-conformant; the platform refuses to start Enterprise-mode +components against a flat-file secrets configuration (fail-closed, not +warn-and-run). + +## 4. Conversion transition + +1. **Direction and terminality.** The only transition is + `standalone → enterprise`. `enterprise → standalone` does not exist: + there is no command, no admin override, and no support path. An + attempt is refused with the precondition/state error class of the + command envelope (`tool-gateway-mapping.md` §4.2). +2. **Preconditions (all verified before the record changes):** + - Secrets: OpenBao/Vault is configured and reachable, and every + required secret is served from the Vault backend — none from a + flat-file backend. Secret migration completes before conversion; + this contract does not define the migration tooling, only the + gate. + - Brains: the per-user brain split required by the Enterprise row of + §3 is established for **every** existing user (or the deployment + already kept the split, the D14 recommended default). Brain + partitioning mechanics are contract 7; this contract binds only + that the split is complete before the mode flips. + - Identity: at least one platform administrator account exists that + is active in identity-contract terms — authenticated capability, + not banned, not deactivated (identity §2, §5). And the conversion + request carries a **configuration acknowledgment**: the current + canonical values of registration mode and per-provider JIT + enablement (identity §2.2, §4.1), echoed back in the request. A + mismatch between the echoed values and the canonical values at + verification refuses the conversion. This acknowledgment record + is conversion-local, owned by this contract, and stored with the + §4.4 audit event as the precondition evidence; it adds no + identity-contract obligation and no mode-specific identity + default — identity's own defaults remain valid states. +3. **Preparation state and the flip boundary.** Preparatory work is + tracked durably: each preparation unit (§1.4) records its + completion in a preparation table keyed by (bootstrap epoch, unit + identity — the secret's path, the user's id), written in the same + transaction as the unit's own effect where the unit's backend + allows it, and reconciled from the backend's actual state where it + does not (a secret already served by Vault, a brain already split, + is complete regardless of the table). Units are at-most-once per + key and re-runnable across attempts. **Standalone-safe + representation:** every preparation unit moves the deployment into + a state that is itself valid Standalone configuration (§3 note), so + an interrupted preparation leaves a fully operational Standalone + deployment reading its state through the ordinary contracts — no + rollback, fencing, or special Standalone read path is needed, and + no component behavior may key on "preparation in progress". + **The flip:** one transaction that (a) locks the mode record, (b) + re-verifies every §4.2 precondition after acquiring the lock, and + (c) writes the mode record and the §4.4 audit event. Any re-check + failure aborts with no write. External state that changes after the + re-check but before commit is bounded by the transaction window; + an external backend (Vault) failing after conversion is an + Enterprise runtime fault handled by §3's fail-closed steady-state + rule, not a conversion defect. An interrupted or failed conversion + leaves the record `standalone` and the platform fully operational; + there is no intermediate mode and no half-converted state + observable through the record. +4. **Authority and audit.** Conversion is a platform-administrator + command carrying an explicit irreversibility acknowledgment in its + request (distinct from the §4.2 configuration acknowledgment). It + is an official Gateway/CLI command (D8): when built, it is added to + the tool↔Gateway mapping by amendment (`tool-gateway-mapping.md` + §3.3). The transition emits an audit event (actor, prior mode, new + mode, precondition evidence reference including the configuration + acknowledgment) in the same transaction as the record change; the + event survives indefinitely. A refused attempt emits a refusal + event naming the failed precondition class and actor, with no + mode-change event. + +## 5. v1 obligations (non-foreclosure) + +v1 ships Standalone only (D11); the conversion command is deferred +work. v1 still MUST: + +1. Record the mode per §2 at bootstrap, with `enterprise` a reserved, + refused value for bootstrap — v1 bootstrap accepts `standalone` + only. The wizard reads the record (contract 3 §2.3); nothing in v1 + writes it after bootstrap. +2. Keep the §2.3 immutability rule: no v1 surface mutates the mode + record. +3. Not foreclose conversion: the v1 platform database holds no + sensitive user content — sensitive categories live in the owning + user's brain, and postgres holds structure, consent records, and + pointers only (the D14 boundary, PRD §7). Custody mechanics are + contract 7's; this contract binds the boundary itself here so v1 + cannot ship a layout that makes the §4.2 brain precondition + unsatisfiable, and §6.3 gives it a stable witness that does not + depend on contract 7's internals. Conversion implementation + additionally requires contract 7 ratified. +4. Not foreclose federation: v1 components accept exactly the two §1.1 + values wherever a mode value is parsed and refuse any other value + **before side effects** — a refused configuration, not undefined + behavior and not a crash mid-operation. Forward compatibility lives + in storage and architecture, not in parser speculation: the mode + record's storage is not structurally locked to two values (no + database-level two-value enum), and any future value (e.g. a + federation mode) is defined by a versioned amendment to this + contract before any component accepts it. The PRD defers + federation's shape entirely; this contract does not presume it + arrives as a third mode value. + +## 6. Verification requirements + +Binding on the implementing PRs: + +1. **Mode-record witness (v1):** after bootstrap the mode is readable + via the Gateway command and CLI and equals the bootstrap-recorded + choice; bootstrap with mode `enterprise` is refused; bootstrap with + any unknown mode value is refused before side effects (§5.4). +2. **Writer-coverage witness (v1):** the mode record's writer set is + closed by the same three-prong static assertion contract 1 §6.3(b) + defines — symbol, class-table literal, and raw-execution prongs + with its allowlist composition rules — scoped to the + `platform_mode` table, with a writer allowlist containing exactly + the bootstrap writer in v1 (and exactly plus the conversion command + at the conversion milestone). Companions: a crafted direct write + attempted in a test fails and leaves the record unchanged; a + mode-resolution assertion that no shipped component derives mode + from feature state (mode reads occur only through the §2.2 read + surface — static assertion over Gateway, CLI, bootstrap, and + repository sources). +3. **D14-boundary witness (v1):** a column-allowlist assertion in the + style of contract 1 §6.2 that the platform database schema contains + no sensitive-content column — the §5.3 boundary — stable regardless + of contract 7's internals (contract 7 §7 carries the full custody + witnesses). +4. **No-downgrade witness (conversion milestone):** with mode + `enterprise`, a conversion request to `standalone` (and any crafted + mode-write) is refused with the precondition/state error class and + no record change. +5. **Precondition witnesses (conversion milestone),** each refused + with no record change and no partial mode effect, parameterized + over both OpenBao and Vault where secrets are involved: + (a) secrets backend unreachable; (b) one required secret still + flat-file backed (migration incomplete); (c) one unpartitioned user + brain in a **multi-user** deployment where every other user is + partitioned; (d) no active platform administrator (the only admin + banned or deactivated); (e) configuration acknowledgment missing or + mismatching the canonical registration/JIT values; (f) actor not a + platform administrator (authorization refusal); (g) irreversibility + acknowledgment absent. And the steady-state rule: an + Enterprise-mode component started against a flat-file secrets + configuration refuses to start (§3). +6. **Interruption and fence witnesses (conversion milestone):** fault + injection aborting conversion after each preparation unit and + between preparation and flip leaves the record `standalone` and the + platform operational in Standalone semantics (§4.3 + Standalone-safety), and a re-attempt completes without duplicating + prepared state (at-most-once keys); a precondition invalidated + after preparation but before the flip (a secret reverted to + flat-file) is caught by the in-transaction re-check and refused. +7. **Audit witnesses (conversion milestone):** a completed conversion + has exactly one mode-change audit event, same-transaction with the + record change (transaction linkage asserted), carrying actor, prior + mode, new mode, and the precondition evidence reference including + the configuration acknowledgment; a failed attempt has a refusal + event naming the failed precondition class and no mode-change + event; the mode-change event remains queryable after subsequent + unrelated audit activity (retention probe). +8. **Mapping witness (conversion milestone):** the conversion command + and the mode read command each have their + `tool-gateway-mapping.md` row (added by amendment per §2.2/§4.4) + before the commands ship. + +## Ruling request + +Ratify sections 1–6 as written, with one decision embedded: + +- Decision (§5): v1 implements the **mode record and its immutability + only** — bootstrap records `standalone`, the `enterprise` value is + reserved and refused, and the conversion command itself is deferred + to the Enterprise milestone, consistent with D11's deferred list. + v1 carries three obligations beyond the record: the closed writer + assertion, the D14 column boundary, and the unknown-value refusal + (§6.1–§6.3) — these are the non-foreclosure floor, not hidden + conversion work. Alternative if rejected: build the conversion + command inside v1 — rejected because D11 scopes v1 to the Standalone + slice and conversion depends on contract 7 custody mechanics that + are themselves not in the v1 slice.