Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3231b91691 | ||
|
|
be32b693d0 |
@@ -1,328 +0,0 @@
|
||||
# Identity Account-Lifecycle Contract
|
||||
|
||||
Status: DRAFT — awaiting ratification (webui-audit S2, contract 4 of 9).
|
||||
Authority: PRD D10 (better-auth is the account system of record), Q1 ruled O1
|
||||
by Jason 2026-08-26 (webui-audit T10). This document turns that ruling into
|
||||
enforceable policy. It also carries the bootstrap/first-admin invariant from
|
||||
issue #1430, folded in here after PR #1431's independent review showed the
|
||||
quick-fix approach was insufficient.
|
||||
|
||||
Revision 2: addresses the 9 findings of the independent review
|
||||
(`fleet/lanes/webui-audit/findings/pr1433-review.md`) — epoch enforcement
|
||||
tightened (§3), canonical email split from provider claims (§5), external
|
||||
principal keyed by issuer+subject with DB uniqueness and link step-up (§6),
|
||||
JIT default precedence and first-admin SSO path defined (§2, §4),
|
||||
deactivation made measurable (§7.1), deletion kept in scope and the existing
|
||||
hard-delete endpoint required to fail closed (§7.3), workspace identity
|
||||
reconciled with the native-kanban SOT (§1.4), verification matrix expanded
|
||||
(§8), factual labels corrected (§7.3, §8.1).
|
||||
|
||||
Revision 3: addresses the residuals and new findings of the revision-2
|
||||
re-review (`fleet/lanes/webui-audit/findings/pr1433-review-r2.md`) —
|
||||
`users.emailVerified` added to the canonical set with a defined reset rule on
|
||||
email change (§5.1–5.2), external-principal uniqueness moved to
|
||||
(issuer, subject) (§6.1), "can actually use" defined (§6.5), the shipped
|
||||
delete affordances (web admin page, `mosaic auth users delete`) required to
|
||||
be removed or disabled with a defined user-visible state (§7.3), the
|
||||
admin-creation switch removed in favor of plain admin authorization (§2.3),
|
||||
and §8 extended with observables for IdP removal, forward-auth non-use,
|
||||
first-admin SSO, wizard-recorded JIT choice, the admin-guide statement, and
|
||||
positive/expiry-bound step-up cases.
|
||||
|
||||
Scope: account creation, bootstrap, federated login, account linking, claim
|
||||
mapping, deactivation, and (minimally) deletion gating. Out of scope: RBAC
|
||||
grant semantics (contract 2), wizard UX flow (contract 3), hierarchy schema
|
||||
(contract 1), sensitive-data custody (contract 7 / D14).
|
||||
|
||||
## 1. System of record
|
||||
|
||||
1. better-auth's tables (`users`, `accounts`, `sessions`, `verifications`) are
|
||||
the only account system of record. All foreign keys reference `users.id`.
|
||||
2. External IdPs (Authentik or any OIDC provider) are login methods, attached
|
||||
through better-auth's generic-OAuth plugin (`packages/auth/src/sso.ts`).
|
||||
They never own accounts. Removing an IdP removes a login method, not users.
|
||||
3. The forward-auth perimeter shim is a deployment measure. Once in-app OIDC
|
||||
is configured for a deployment, the shim is demoted: it may stay as network
|
||||
perimeter, but no application code may read identity from its headers.
|
||||
4. **Account ≠ workspace membership.** Creating an account (by any path:
|
||||
bootstrap, sign-up, invite, JIT, admin creation) creates no workspace, no
|
||||
hierarchy grant, and no workspace-scoped authority (native-kanban SOT
|
||||
REQ-TEN-001 / REQ-ID-001). The better-auth `role` field is a platform/auth
|
||||
role (`member` | `admin`), not workspace membership. Workspace grants are
|
||||
defined by contract 2; until then a fresh account can authenticate and
|
||||
holds no workspace authority.
|
||||
|
||||
## 2. Registration gating
|
||||
|
||||
Measured current state on `next`: `emailAndPassword.enabled: true` with no
|
||||
gating — anyone who can reach the Gateway can create an account via
|
||||
`POST /api/auth/sign-up/email` and receives role `member`.
|
||||
|
||||
Contract:
|
||||
|
||||
1. A single server-side setting `registration_mode` with values
|
||||
`open | invite | closed`. It lives in the database (admin-mutable at
|
||||
runtime), not in env config.
|
||||
2. Default after bootstrap: `closed`. The wizard (contract 3) may set a
|
||||
different mode during setup, recorded as an explicit operator choice.
|
||||
While the bootstrap epoch is open (§3), the effective mode is `closed`
|
||||
regardless of any stored value: the setting takes effect only after the
|
||||
epoch completes.
|
||||
3. `closed` blocks self-service email/password sign-up. It does not block
|
||||
admin-created users or OIDC JIT (§4). Post-bootstrap admin creation is
|
||||
gated by admin authorization alone — there is no separate switch for it.
|
||||
JIT is gated by its per-provider flag (§4.1). All user-creating paths are
|
||||
closed while the bootstrap epoch is open (§3).
|
||||
4. `invite` requires a single-use, expiring invite token bound to an email
|
||||
address. Invite issuance is an admin operation and is audit-logged.
|
||||
5. Enforcement point: a better-auth hook (or equivalent middleware executed
|
||||
inside the auth handler path), not a Gateway route guard in front of it —
|
||||
the raw `/api/auth/*` handler must be incapable of bypassing the gate.
|
||||
|
||||
## 3. Bootstrap / first-admin invariant (from #1430)
|
||||
|
||||
Invariant: **the system transitions from zero users to one admin user exactly
|
||||
once per bootstrap epoch, atomically, regardless of concurrency or which code
|
||||
path writes users.**
|
||||
|
||||
Constraints any implementation MUST satisfy (each traces to a verified defect
|
||||
in PR #1431's review, `fleet/lanes/webui-audit/findings/pr1431-review.md`):
|
||||
|
||||
1. **Durable fail-closed epoch state, obeyed by every writer.** The epoch
|
||||
lives in a constraint-backed one-row `bootstrap_state` table. While the
|
||||
epoch is open, every non-bootstrap user-creating writer — better-auth
|
||||
sign-up, OIDC JIT, admin creation — refuses, fail-closed, enforced inside
|
||||
the writer's own path (better-auth hook for the raw handler; guard for
|
||||
admin routes). A partial unique index or a winning epoch-transition row is
|
||||
necessary but not sufficient on its own: neither stops an untagged insert
|
||||
from a writer that never consulted the epoch. Both layers are required:
|
||||
database-level transition safety (the epoch-completing write races safely
|
||||
and at most one wins) and writer-level refusal (no path can create a user
|
||||
without reading epoch state).
|
||||
2. **Atomic first-admin transition.** The admin user, its credential account,
|
||||
the initial admin token, and the epoch-completed transition commit in one
|
||||
database transaction or not at all. A better-auth call through
|
||||
`drizzleAdapter(db)` runs on the root pool and is NOT part of any caller
|
||||
transaction; it may be used inside the bootstrap transition only if the
|
||||
adapter is explicitly bound to the transaction handle. Otherwise the
|
||||
bootstrap writer must create the user rows itself within the transaction.
|
||||
3. **Pool safety.** No design may hold a pooled connection inside a
|
||||
transaction while awaiting a write that acquires a second connection from
|
||||
the same pool (`DB_POOL_MAX=1` is a supported configuration).
|
||||
4. **Re-runnability (D4).** Bootstrap is not a one-shot: after the first-admin
|
||||
epoch completes, re-running the wizard reconfigures the system but never
|
||||
re-opens the zero-user transition. "Setup already completed" is a stable,
|
||||
testable state, and factory-reset (a future, explicitly destructive
|
||||
operation) is the only way to open a new epoch.
|
||||
5. **No stranded partial outcome.** A failure at any point in the transition
|
||||
leaves nothing observable (no admin user without its token, no completed
|
||||
epoch without an admin) and setup remains retryable — this follows from
|
||||
§3.2 and is stated separately because it is the pre-existing failure mode
|
||||
the #1431 review verified.
|
||||
6. **First-admin via SSO (D4).** When the operator chooses SSO for the
|
||||
initial user, the wizard executes the OIDC login as part of the bootstrap
|
||||
transition itself: the bootstrap writer creates the account from the
|
||||
asserted identity inside the §3.2 transaction. This path is the bootstrap
|
||||
writer, not JIT — §4's JIT gate stays closed during the epoch and is not
|
||||
an obstacle to D4.
|
||||
|
||||
## 4. JIT provisioning (OIDC first login)
|
||||
|
||||
1. A successful OIDC login with no matching account creates a user
|
||||
just-in-time only when `jit_provisioning` is enabled for that provider.
|
||||
The flag is per-provider and defaults off, always. There is no
|
||||
mode-implied default: Enterprise setup enables JIT only when the wizard
|
||||
records it as an explicit operator choice for a named provider (this
|
||||
replaces revision 1's "Enterprise mode defaults to closed with OIDC JIT
|
||||
enabled", which contradicted the per-provider default).
|
||||
2. JIT users receive platform role `member`, never an elevated role,
|
||||
regardless of IdP claims (§5), and no workspace authority (§1.4).
|
||||
3. An optional per-provider email-domain allowlist constrains JIT. The
|
||||
allowlist matches only when the IdP asserts the email with
|
||||
`email_verified: true`; an unverified address never satisfies the
|
||||
allowlist. Empty allowlist with JIT on means any authenticated subject at
|
||||
that IdP gets an account — permitted, but the wizard must present it as an
|
||||
explicit choice.
|
||||
4. JIT is disabled while the bootstrap epoch is open (§3.1). The first-admin
|
||||
SSO path is §3.6, not JIT.
|
||||
|
||||
## 5. Claim mapping
|
||||
|
||||
1. **Two stores, not one.** Provider-observed claims (`email`,
|
||||
`email_verified`, display name, avatar) are recorded per external
|
||||
principal — keyed by issuer + subject (§6.1) — at first login and
|
||||
refreshed at each login. The canonical account fields (`users.email`,
|
||||
`users.emailVerified`, `users.name`, `users.image`) are set exactly once
|
||||
at account creation and are never silently overwritten by a later login.
|
||||
For SSO-created accounts (JIT or first-admin SSO), `users.emailVerified`
|
||||
is set from the provider's `email_verified` claim at creation; for
|
||||
password-created accounts it is false until the address completes
|
||||
verification.
|
||||
2. **Canonical email changes only through an explicit workflow.** Either the
|
||||
user-initiated email change (with verification of the new address) or an
|
||||
admin edit. Any canonical email change — user- or admin-initiated — sets
|
||||
`users.emailVerified` to false until the new address completes
|
||||
verification; an admin may instead explicitly attest the address as
|
||||
verified in the same operation, and that attestation is audit-logged. A
|
||||
provider-claim refresh never rebinds `users.email` or
|
||||
`users.emailVerified`; a divergence between canonical email and the latest
|
||||
provider-observed email is surfaced per §6.4.
|
||||
3. Never mapped from IdP claims: `role` and any future authorization
|
||||
attribute. Authorization lives in the system of record and in the RBAC
|
||||
layer (contract 2). An IdP group/role claim may at most be recorded for
|
||||
audit; it grants nothing.
|
||||
|
||||
## 6. Account linking trust
|
||||
|
||||
1. **External principal identity is issuer + subject.** A linked identity is
|
||||
keyed by the OIDC issuer and subject claims, not by an unqualified
|
||||
provider subject id and not by email. The linked-identity row stores the
|
||||
issuer, and the database enforces at most one local account per
|
||||
**(issuer, subject)** with a unique constraint on those stored columns —
|
||||
uniqueness on (provider, subject) is insufficient because provider →
|
||||
issuer is not one-to-one: two provider configurations can point at the
|
||||
same issuer, and the identity must not alias across them. The current
|
||||
non-unique `(provider_id, account_id)` index satisfies neither;
|
||||
application-level checks without a uniqueness witness lose
|
||||
concurrent-callback races. Each configured provider additionally binds to
|
||||
exactly one issuer, immutable after creation (changing the issuer means
|
||||
creating a new provider).
|
||||
2. Linking an OIDC identity to an existing account happens only in one of two
|
||||
ways: (a) explicit link initiated by the logged-in user from settings,
|
||||
which requires step-up: a fresh reauthentication (password or existing
|
||||
linked method) no older than a short bound the implementation defines
|
||||
(≤ 10 minutes) — a session cookie alone is insufficient, so a stolen
|
||||
session cannot quietly attach a durable login method; or (b) automatic
|
||||
link when the IdP asserts a verified email exactly matching an existing
|
||||
account **and** the provider is marked `trusted_for_linking`
|
||||
(per-provider flag, default off).
|
||||
3. Untrusted-provider email collision produces a login error naming the
|
||||
conflict, not an auto-link and not a duplicate account.
|
||||
4. A linked identity whose IdP-observed email later diverges from the
|
||||
canonical account email keeps working (the link is by issuer + subject,
|
||||
§6.1) but the divergence is surfaced in the user's settings and audit log
|
||||
(the per-principal claim store in §5.1 is what makes the divergence
|
||||
representable).
|
||||
5. Unlinking a login method is refused when it would leave the account with
|
||||
no **usable** login method. Usable means: a set password, or a linked
|
||||
identity whose provider is currently configured and enabled on this
|
||||
deployment. A linked identity whose provider has been removed or disabled
|
||||
(§1.2) is not usable and does not count; setting a password first lifts
|
||||
the refusal.
|
||||
|
||||
## 7. Deactivation propagation
|
||||
|
||||
1. **Deactivation (better-auth admin ban) is authoritative and bounded.**
|
||||
Concretely:
|
||||
- Ban and session revocation are one operation: the ban commit revokes all
|
||||
better-auth sessions for the user. If revocation partially fails, the
|
||||
ban itself must already be committed and every guard denies from that
|
||||
point (fail closed); the operation is retryable.
|
||||
- Every authenticated entry path checks banned state: HTTP session guards,
|
||||
the admin bearer-token path (which today does not test `banned` — an
|
||||
implementation defect this contract makes non-conformant), MCP, and
|
||||
Socket.IO.
|
||||
- Active socket connections are terminated or denied within 30 seconds of
|
||||
the ban commit, or at the next inbound message on that socket, whichever
|
||||
comes first (socket auth at connect-time only, as today, does not
|
||||
satisfy this).
|
||||
- The current admin ban route updates only the user row; it does not
|
||||
conform to this section until revocation and guard coverage land.
|
||||
- Admin tokens owned by the banned user are revoked in the same operation.
|
||||
2. Deactivation at an external IdP does not propagate automatically in this
|
||||
contract's scope (no SCIM). Operational rule: removing a user from the IdP
|
||||
without banning them in Mosaic leaves any password or other linked login
|
||||
method usable — the admin guide must state this. SCIM/webhook-driven
|
||||
propagation is future work and out of scope here.
|
||||
3. **Deletion is not deactivation, and deletion is gated here.** Account
|
||||
deletion semantics (FK fan-out across the 21 foreign-key constraints to
|
||||
`users.id`, spread over 19 referencing tables) require their own
|
||||
deletion-and-retention contract, chartered as an addition to the S2 list —
|
||||
contract 7 is the D14 sensitive-data custody contract and does not cover
|
||||
account deletion. Until that deletion contract is ratified: the existing
|
||||
hard-delete endpoint (`DELETE /api/admin/users/:id`) is disabled and fails
|
||||
closed, and deactivation is the only supported removal operation. A
|
||||
contract that merely declared deactivation "the only supported removal"
|
||||
while the endpoint stayed live would be false on its face.
|
||||
Disabling the endpoint alone is insufficient — its shipped callers must
|
||||
not be left as advertised operations that now fail generically:
|
||||
- The admin web UI delete action (`apps/web/src/app/(dashboard)/admin/page.tsx`
|
||||
and any SPA port of it) is removed, or replaced by a disabled control
|
||||
whose visible text states that deletion is unavailable pending the
|
||||
deletion-and-retention contract and points at deactivation.
|
||||
- The CLI command `mosaic auth users delete`
|
||||
(`packages/mosaic/src/commands/auth.ts`) is removed, or exits non-zero
|
||||
with a message stating the same and naming the deactivation command.
|
||||
- Both surfaces expose deactivation as the supported operation.
|
||||
|
||||
## 8. Verification requirements
|
||||
|
||||
Every MUST above needs a bounded observable. The matrix:
|
||||
|
||||
1. **Bootstrap invariant (§3).** Real-PostgreSQL concurrency tests using two
|
||||
distinct physical connections (pattern:
|
||||
`apps/gateway/src/agent/connector-lease.postgres.integration.test.ts`,
|
||||
which runs in the `test` CI step against the `ci-postgres` PostgreSQL
|
||||
service — note that pattern multiplexes one pooled handle, so the tests
|
||||
here must explicitly open separate connections). Races to cover:
|
||||
setup-vs-setup, setup-vs-raw-sign-up, setup-vs-JIT, setup-vs-admin-create.
|
||||
Plus: liveness under `DB_POOL_MAX=1`; fault injection after each write in
|
||||
the transition (user, credential, token, epoch) proving nothing observable
|
||||
leaks and setup retries; wizard re-run after completion proving the
|
||||
zero-user transition never re-opens. Mocked-transaction specs are
|
||||
supplementary; they cannot prove serialization.
|
||||
2. **Registration gating (§2).** Spec coverage of all three modes against the
|
||||
raw `/api/auth/` handler path, not only Gateway controllers; invite
|
||||
lifecycle (single-use, expiry, email binding); effective-`closed` while
|
||||
the epoch is open regardless of stored mode.
|
||||
3. **JIT (§4).** Provider flag off → no account on first OIDC login; on →
|
||||
account with platform role `member` and no workspace grant; domain
|
||||
allowlist rejects an unverified email claim even when the domain matches;
|
||||
JIT refused while the epoch is open.
|
||||
4. **Claim mapping (§5).** Login refresh updates the per-principal claim
|
||||
store and touches none of the canonical fields (`users.email`,
|
||||
`users.emailVerified`, name, image); explicit email-change workflow is the
|
||||
only path that rebinds canonical email; every canonical email change
|
||||
resets `users.emailVerified` to false unless the admin attestation path
|
||||
is taken, and that attestation appears in the audit log.
|
||||
5. **Linking (§6).** Unique-constraint witness: concurrent first-login
|
||||
callbacks for the same (issuer, subject) yield exactly one account, and
|
||||
two provider configurations sharing one issuer cannot create two accounts
|
||||
for the same subject; trusted auto-link; untrusted collision error;
|
||||
step-up both ways: an explicit link succeeds immediately after a fresh
|
||||
reauthentication and is refused once the implementation's chosen bound
|
||||
(≤ 10 minutes) has elapsed, and refused with no reauthentication at all;
|
||||
unlink refusal when no remaining method is usable per §6.5, including the
|
||||
removed-provider case, and acceptance after a password is set; divergence
|
||||
surfaced after IdP email change.
|
||||
6. **Deactivation (§7).** Ban revokes sessions atomically or fails closed
|
||||
(partial-failure injection); guard denial post-ban on each transport:
|
||||
HTTP session, admin bearer token, MCP, Socket.IO; active socket terminated
|
||||
within the 30-second/next-message bound; banned user's admin tokens
|
||||
unusable; hard-delete endpoint returns a fail-closed error while the
|
||||
deletion contract is unratified; the admin web UI renders no live delete
|
||||
action (absent, or disabled with the §7.3 text) and `mosaic auth users
|
||||
delete` exits non-zero with the §7.3 message — both asserted by spec.
|
||||
7. **System of record and bootstrap edges (§1, §3.6, §4.3).** IdP removal:
|
||||
deleting a provider configuration leaves every user row intact and every
|
||||
other login method working (spec over the provider-config removal path).
|
||||
Forward-auth non-use: with in-app OIDC configured, a request carrying
|
||||
forward-auth identity headers and no session is treated as anonymous —
|
||||
no code path derives identity from those headers (negative spec at the
|
||||
Gateway entry). First-admin SSO: the §3.6 transition commits account,
|
||||
token, and epoch atomically from the asserted identity, and fault
|
||||
injection mid-transition leaves nothing observable (same harness as §8.1).
|
||||
Wizard-recorded JIT choice: enabling JIT for a provider writes an
|
||||
explicit per-provider operator-choice record, and no mode selection
|
||||
enables it implicitly (assert the stored record, not UI behavior).
|
||||
8. **Documentation observable (§7.2).** The admin guide contains the
|
||||
IdP-removal-does-not-deactivate statement; verified by a docs assertion
|
||||
(content check in CI or an enumerated review-checklist item on the
|
||||
implementing PR) — a MUST about documentation needs a checkable artifact,
|
||||
not intent.
|
||||
|
||||
## Ruling request
|
||||
|
||||
Ratify sections 1–8 as written, with one decision embedded: registration
|
||||
defaults to `closed` after bootstrap (§2.2) — say "agreed" or name the mode
|
||||
you want as the default.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user