Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
501112d2a4 | ||
|
|
3538d33106 |
@@ -1,279 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
# RBAC Grant Model Contract
|
||||||
|
|
||||||
|
Status: DRAFT — awaiting ratification (webui-audit S2, contract 2 of 9).
|
||||||
|
Authority: PRD Part I §4 ("Granular RBAC: admins restrict access per company,
|
||||||
|
estate, and project; grants are evaluated down the chain") and the
|
||||||
|
native-kanban SOT Amendment A1 (§8.1.3 RBAC evaluation, §8.3 acceptance 2).
|
||||||
|
This document defines the grant vocabulary, evaluation semantics, and
|
||||||
|
revocation propagation that the hierarchy schema contract
|
||||||
|
(`docs/requirements/hierarchy-schema.md`, contract 1) attaches to. Contract 1
|
||||||
|
pins the `hierarchy_grants` table shape and defers the `role` vocabulary and
|
||||||
|
the meaning of "authority" here; the identity contract
|
||||||
|
(`docs/requirements/identity-lifecycle.md` §1.4) pins that account creation
|
||||||
|
grants nothing.
|
||||||
|
|
||||||
|
Revision 2 (independent review, GLM 5.3): §1.1 consequence analysis
|
||||||
|
completed — the two existing platform-admin bypass code paths are named as
|
||||||
|
non-conformant and §7.4 retires them; team grant subjects suspended pending
|
||||||
|
a team contract (§1.4, §3.3–3.4, §7.5); no-self-escalation restated with
|
||||||
|
its true rationale and a constructible observable (§4.2, §7.7);
|
||||||
|
node-creation seeding scoped to the bootstrap path, resolving the §7.7/§4.3
|
||||||
|
contradiction; A1 quotation corrected; audit-field provenance corrected;
|
||||||
|
principal-position consequence named (§1.3); membership-row,
|
||||||
|
fail-closed-fault, and existence-oracle observables added (§7);
|
||||||
|
role-string namespacing rule added (§4.5); ruling request now names the
|
||||||
|
interpretive resolution of PRD "admins".
|
||||||
|
|
||||||
|
Scope: the roles that can appear in `hierarchy_grants.role`, what a grant at
|
||||||
|
each hierarchy level confers, how grants evaluate down the chain, how
|
||||||
|
revocation propagates, and who may manage grants. Out of scope: the hierarchy
|
||||||
|
tables themselves (contract 1), workspace-internal membership and its
|
||||||
|
role/capability vocabulary (native-kanban SOT REQ-ID-001 and its implementing
|
||||||
|
schema), roll-up projection semantics (contract 8), wizard seeding
|
||||||
|
(contract 3), the team model (suspended here; see §1.4).
|
||||||
|
|
||||||
|
## 1. Three authority layers, none substitutable
|
||||||
|
|
||||||
|
1. **Platform role** (`users.role`, better-auth: `member` | `admin`) governs
|
||||||
|
instance administration — user management, system settings, provider
|
||||||
|
configuration. It is not tenancy authority: holding platform `admin`
|
||||||
|
confers **no implicit hierarchy grant and no workspace authorization**.
|
||||||
|
An operator who should see tenant content holds an explicit, audited
|
||||||
|
grant like anyone else. This is the deny-by-default consequence of A1
|
||||||
|
§8.1.3 ("not a bypass of workspace authorization"). `AdminGuard`'s
|
||||||
|
`role === 'admin'` check on admin endpoints stays the platform role's
|
||||||
|
only meaning. **Two shipped code paths violate this rule today and are
|
||||||
|
implementation defects this contract makes non-conformant:** (a) the
|
||||||
|
command authorization service short-circuits every command scope to
|
||||||
|
allowed for platform admins
|
||||||
|
(`apps/gateway/src/commands/command-authorization.service.ts`,
|
||||||
|
`hasScope` returning true when `role === 'admin'`), and (b) the MCP
|
||||||
|
scope derivation maps platform `admin` to tenant-admin MCP scopes
|
||||||
|
including task create/update
|
||||||
|
(`apps/gateway/src/mcp/mcp.service.ts`,
|
||||||
|
`deriveMcpToolScopesForUser`). Ratifying this contract revokes both;
|
||||||
|
§7.4 names them as the surfaces the deny-by-default test retires.
|
||||||
|
2. **Hierarchy grants** (`hierarchy_grants`, contract 1 §3) declare tenancy
|
||||||
|
authority at company, estate, or platform-project scope and evaluate down
|
||||||
|
the chain to workspace-scoped authorization (§3 below).
|
||||||
|
3. **Workspace membership** (SOT REQ-ID-001) remains its own mechanism.
|
||||||
|
A chain grant confers command authorization over descendant workspaces;
|
||||||
|
it does not create membership rows, and row-level principal positions
|
||||||
|
(task owner, proposer, decision actor) still require ACTIVE workspace
|
||||||
|
membership exactly as REQ-TEN-001/REQ-ID-001 acceptance states.
|
||||||
|
Consequence, stated so implementing PRs do not weaken REQ-TEN-001 to
|
||||||
|
remove the friction: a chain-granted actor who is not a workspace member
|
||||||
|
may issue the write commands their role implies but cannot occupy a
|
||||||
|
principal position — any command taking a principal argument must name
|
||||||
|
an ACTIVE member of the target workspace (§7.2 enumerates this cell).
|
||||||
|
4. **Team grant subjects are suspended.** Contract 1 §3.1 reserves a
|
||||||
|
`team_id` attachment point, but no ratified contract yet defines the
|
||||||
|
team it would bind: the only existing `teams` table is the legacy global
|
||||||
|
Brain table (own authority columns, no workspace binding, not
|
||||||
|
repurposed per contract 1 §1.3), while the SOT's teams are
|
||||||
|
workspace-bound (REQ-ID-001) — and a workspace-bound team holding a
|
||||||
|
company-level grant would be a cross-workspace authority group nothing
|
||||||
|
has ratified. Until a team contract defines the subject (which table,
|
||||||
|
which membership rows, and its relation to D2/REQ-ID-001), creating a
|
||||||
|
grant with a team subject MUST be refused at the command surface (the
|
||||||
|
schema column remains, per contract 1). §3's evaluation semantics for
|
||||||
|
team-conferred grants are specified now so the team contract activates
|
||||||
|
them without amending this one.
|
||||||
|
|
||||||
|
## 2. Role vocabulary
|
||||||
|
|
||||||
|
One vocabulary at every hierarchy level, totally ordered — a higher role
|
||||||
|
includes everything below it:
|
||||||
|
|
||||||
|
1. `viewer` — read: sees the node, its subtree structure, and the roll-up
|
||||||
|
aggregates over descendant workspaces (within contract 8's carve-out
|
||||||
|
bounds); read access to descendant workspace content per the SOT's read
|
||||||
|
command families. No mutation of anything.
|
||||||
|
2. `member` — work: everything `viewer` has, plus write authorization for
|
||||||
|
business/orchestration command families in descendant workspaces (the
|
||||||
|
concrete command-family mapping is implementation work under SOT
|
||||||
|
REQ-ID-001; this contract pins that `member` maps to the workspace write
|
||||||
|
families and nothing structural).
|
||||||
|
3. `owner` — structure: everything `member` has, plus hierarchy mutations on
|
||||||
|
the subtree (create/rename/delete child nodes, transfers per §5), and
|
||||||
|
grant management on the node and its subtree (§4).
|
||||||
|
|
||||||
|
No other value is valid in `hierarchy_grants.role`; the column is
|
||||||
|
constraint-checked against exactly these three. Extending the vocabulary is a
|
||||||
|
contract amendment, not an implementation decision.
|
||||||
|
|
||||||
|
## 3. Evaluation semantics
|
||||||
|
|
||||||
|
1. **Deny by default.** No grant on any ancestor → no authority. There are
|
||||||
|
no implicit grants: not from platform role (§1.1), not from creating a
|
||||||
|
node (§4.3), not from workspace membership (membership without a chain
|
||||||
|
grant confers exactly what the SOT's own membership rules confer inside
|
||||||
|
that workspace, nothing up the chain).
|
||||||
|
2. **Down-the-chain only.** A grant on a node applies to that node and its
|
||||||
|
entire descendant subtree. Nothing evaluates upward or sideways: a grant
|
||||||
|
on an estate says nothing about the parent company or sibling estates.
|
||||||
|
3. **Effective role = maximum.** A subject's effective role at any node is
|
||||||
|
the highest role among grants held directly by the subject's user on
|
||||||
|
that node or any ancestor — and, once the team contract activates team
|
||||||
|
subjects (§1.4), grants held by any team the user is a member of on that
|
||||||
|
node or any ancestor. Roles never subtract — there is no negative/deny
|
||||||
|
grant in this model; revocation is deletion (§6).
|
||||||
|
4. **Team grants follow live membership** (specified now, active only per
|
||||||
|
§1.4). A team grant confers its role on the team's current members,
|
||||||
|
evaluated at decision time. Leaving the team is loss of the grant with
|
||||||
|
§6's propagation bound.
|
||||||
|
5. **Live evaluation, fail closed.** Authorization decisions derive from the
|
||||||
|
live grant and team-membership rows (or from a cache that is invalidated
|
||||||
|
in the same transaction as any grant/membership/hierarchy mutation). A
|
||||||
|
decision path that cannot read grant state denies. No materialized ACL is
|
||||||
|
ever authoritative.
|
||||||
|
6. **Tenant context stays derived from authenticated authority**
|
||||||
|
(REQ-TEN-001). The chain adds where grants can be declared; a workspace
|
||||||
|
request is still authorized against that workspace, with the chain
|
||||||
|
contributing the effective role — never letting the chain become what A1
|
||||||
|
§8.1.3 forbids: "a bypass of workspace authorization".
|
||||||
|
|
||||||
|
## 4. Grant management
|
||||||
|
|
||||||
|
1. Creating, changing, or revoking a grant on a node requires effective
|
||||||
|
`owner` on that node (directly or via any ancestor).
|
||||||
|
2. **No self-escalation.** A grant manager cannot create a grant with a role
|
||||||
|
higher than their own effective role on the target node. Under the §2
|
||||||
|
vocabulary this rule is currently implied by §4.1 (managers are `owner`,
|
||||||
|
the top role — no constructible grant exceeds it); it is stated
|
||||||
|
explicitly so it survives any future amendment that decouples
|
||||||
|
grant-management authority from role height. Its observable is the §7.7
|
||||||
|
audit invariant, not a refusal test.
|
||||||
|
3. **Bootstrap of authority is explicit; inheritance covers the rest.**
|
||||||
|
Creating the first company (the wizard path, contract 3) and any
|
||||||
|
top-level company creation MUST name the initial `owner` grant in the
|
||||||
|
same audited operation — a top-level node has no ancestor to inherit
|
||||||
|
from, so without this the node would be unownable. Creating a child node
|
||||||
|
(estate, platform-project, workspace) requires effective `owner` on the
|
||||||
|
parent (§2.3) and confers no automatic grant; the creator's authority
|
||||||
|
over the new node already follows from §3.2 down-the-chain evaluation.
|
||||||
|
The creating command MAY additionally name an explicit initial grant for
|
||||||
|
a child node; it is not required to.
|
||||||
|
4. Every grant mutation is a semantic audit event under contract 1 §5.2's
|
||||||
|
guarantees, extended by this contract with two further fields: the event
|
||||||
|
carries actor, verb, target, **subject, and role** (subject and role are
|
||||||
|
this contract's addition; contract 1 §5.2 does not enumerate them).
|
||||||
|
5. **Role strings are namespaced.** `viewer`/`member` exist at hierarchy
|
||||||
|
level, `member`/`admin` on `users.role`, and the current command layer
|
||||||
|
uses a third `viewer|member|admin` vocabulary — same strings, different
|
||||||
|
meanings. Any serialized role string (audit events per §4.4, API
|
||||||
|
responses, logs) MUST identify its layer (e.g. `hierarchy:owner`,
|
||||||
|
`platform:admin`); a bare role string in a serialized artifact is
|
||||||
|
non-conformant.
|
||||||
|
|
||||||
|
## 5. Transfer authority (completes contract 1 §4.2)
|
||||||
|
|
||||||
|
"Authority over BOTH the source and the destination parent" means: effective
|
||||||
|
`owner` on the current parent node (or an ancestor) AND effective `owner` on
|
||||||
|
the destination parent node (or an ancestor), evaluated at transfer time in
|
||||||
|
the transfer's own transaction. One subject must hold both; two cooperating
|
||||||
|
half-authorized subjects are not a transfer protocol this contract defines.
|
||||||
|
|
||||||
|
## 6. Revocation propagation
|
||||||
|
|
||||||
|
1. Revoking a grant (deleting the row), removing a user from a team that
|
||||||
|
carries a grant (once team subjects activate, §1.4), or the cascade
|
||||||
|
deletion of a node's grants during node deletion (contract 1 §3.3) all
|
||||||
|
propagate identically: the authority derived from that grant is gone for
|
||||||
|
every descendant workspace.
|
||||||
|
2. **Bound:** the next authorization decision on any affected transport
|
||||||
|
decides against the revoked grant. Concretely: no new HTTP/MCP command
|
||||||
|
authorized by the revoked grant after the revoking transaction commits;
|
||||||
|
an open Socket.IO connection whose subscriptions depend on the revoked
|
||||||
|
grant is re-evaluated within 30 seconds or at its next inbound message,
|
||||||
|
whichever comes first (same bound as the identity contract's §7.1
|
||||||
|
deactivation rule; same mechanism may serve both).
|
||||||
|
3. Revocation is subtractive only in effect, not in representation: the
|
||||||
|
evaluator never needs tombstones; deletion of the row is the revocation.
|
||||||
|
|
||||||
|
## 7. Verification requirements
|
||||||
|
|
||||||
|
Binding on the implementing PRs (extends A1 §8.3 acceptance 2–3 and
|
||||||
|
contract 1 §6):
|
||||||
|
|
||||||
|
1. Vocabulary: the role CHECK constraint rejects any value outside
|
||||||
|
`viewer|member|owner` (real-PostgreSQL witness, `ci-postgres` service in
|
||||||
|
the `test` CI step).
|
||||||
|
2. Per-level conferral: for each of the three levels × three roles, a grant
|
||||||
|
yields exactly the implied workspace authorization in a descendant
|
||||||
|
workspace and nothing in a non-descendant workspace (the A1 §8.3
|
||||||
|
"exactly the permissions the chain implies" matrix, enumerated). The
|
||||||
|
matrix includes: a chain grant creates zero workspace-membership rows
|
||||||
|
(assert row counts); a chain-granted non-member is refused as the
|
||||||
|
principal argument of any principal-taking command while their
|
||||||
|
non-principal writes succeed (§1.3); structure reads leak no existence
|
||||||
|
of nodes the reader holds no grant on (no cross-tenant existence
|
||||||
|
oracle, A1 §8.3 acceptance 3).
|
||||||
|
3. Ordering: `owner` ⊇ `member` ⊇ `viewer` behaviorally — each higher role
|
||||||
|
passes every lower role's positive cases.
|
||||||
|
4. Deny-by-default: platform `admin` with no grant reaches no tenant
|
||||||
|
content — asserted against the two §1.1 non-conformant surfaces after
|
||||||
|
their retirement: the command-authorization admin short-circuit and the
|
||||||
|
MCP tenant-admin scope derivation both gone (a platform admin with no
|
||||||
|
grant is refused workspace commands and receives no tenant MCP scopes);
|
||||||
|
workspace member with no chain grant gains nothing outside SOT
|
||||||
|
membership semantics; fresh account reaches nothing (identity contract
|
||||||
|
§1.4 cross-check).
|
||||||
|
5. Team subjects: while suspended (§1.4), creating a team-subject grant is
|
||||||
|
refused at the command surface. On activation by the team contract:
|
||||||
|
user-direct and team-conferred grants combine to the maximum; team-leave
|
||||||
|
drops authority within the §6.2 bound; decision-time evaluation
|
||||||
|
witnessed (grant added → next decision allows; no restart or re-login
|
||||||
|
required).
|
||||||
|
6. Revocation: each revocation path in §6.1 denies the next command on
|
||||||
|
every transport; the socket bound is measured; a cached-authorization
|
||||||
|
implementation proves transactional invalidation (grant revoked and
|
||||||
|
decision made on two distinct physical connections). Fail-closed fault
|
||||||
|
witness for §3.5: with grant state unreadable (fault injection), the
|
||||||
|
decision denies.
|
||||||
|
7. Grant management: non-`owner` cannot mutate grants; top-level company
|
||||||
|
creation without the named initial `owner` grant is refused, while child
|
||||||
|
node creation under ancestor authority succeeds without one (§4.3 both
|
||||||
|
directions); every mutation produces its audit event with the §4.4
|
||||||
|
fields. Self-escalation observable: over the audit event stream, every
|
||||||
|
grant-create/change event's role is ≤ the acting user's effective role
|
||||||
|
on the target at event time (reconstructable invariant, not a refusal
|
||||||
|
test — see §4.2).
|
||||||
|
8. Transfer: both-sides `owner` accepted, each single-side case refused
|
||||||
|
(completing contract 1 §6.5).
|
||||||
|
|
||||||
|
## Ruling request
|
||||||
|
|
||||||
|
Ratify sections 1–7 as written, with one decision embedded and one
|
||||||
|
interpretive resolution named:
|
||||||
|
|
||||||
|
- Decision: platform `admin` confers no implicit tenant access — operators
|
||||||
|
see tenant content only through explicit, audited grants (§1.1), which
|
||||||
|
retires the two existing admin bypass paths named there. Say "agreed" or
|
||||||
|
name the implicit access you want platform admins to keep.
|
||||||
|
- Interpretive resolution (for visibility, not a separate question): PRD
|
||||||
|
Part I §4 says "admins restrict access per company, estate, and project";
|
||||||
|
this contract resolves "admins" as hierarchy `owner`s (§4.1), not
|
||||||
|
platform admins. A1 §8.1.3 does not attribute grant declaration to
|
||||||
|
platform admins, and the §1.1 decision above is what makes this reading
|
||||||
|
binding.
|
||||||
Reference in New Issue
Block a user