From 635cb1f666940ead7159d42f04decd5c549572d8 Mon Sep 17 00:00:00 2001 From: fred Date: Sat, 29 Aug 2026 19:34:48 +0000 Subject: [PATCH] =?UTF-8?q?docs:=20contract=202=20Amendment=201=20?= =?UTF-8?q?=E2=80=94=20company-CRUD=20capability=20(S2=20follow-up)=20(#14?= =?UTF-8?q?77)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/requirements/native-kanban-sot.md | 44 +++++ docs/requirements/rbac-grant-model.md | 219 +++++++++++++++++++++++++ 2 files changed, 263 insertions(+) diff --git a/docs/requirements/native-kanban-sot.md b/docs/requirements/native-kanban-sot.md index ea99a50b..704827e0 100644 --- a/docs/requirements/native-kanban-sot.md +++ b/docs/requirements/native-kanban-sot.md @@ -514,3 +514,47 @@ itself permits, so that contract does not stretch A1 by interpretation. default-open disclosure. 5. Every other constraint of A1 — §8.1.2's remaining bullets, §8.2 in full, and §8.3's other acceptance criteria — is untouched. + +## 10. Amendment A3 — capability-holder existence disclosure + +**Status:** amendment to Amendment A2, added by reviewed PR together with +contract 2 Amendment 1 (`rbac-grant-model.md` §8, this PR), under that +amendment's ruling request (decision owner Jason). It binds if and only if +contract 2 Amendment 1 ratifies; until then §9.1.2's sole-disclosure rule +stands unmodified — which is consistent, because until ratification the +company-CRUD capability class is empty and the carve-out below has no +holders. Everything in §§1–9 remains binding verbatim, with exactly the one +express modification below. The detailed contract text lives in +`rbac-grant-model.md` §8.1; this amendment changes only what A2 itself +permits, so that contract does not stretch A2 by interpretation. + +### 10.1 What A3 modifies in A2 + +1. **Capability-holder disclosure (narrows §9.1.2's sole-disclosure rule + by one carve-out).** §9.1.2 makes the directory the sole permitted + existence disclosure and keeps private companies undisclosed to + non-granted subjects everywhere. A3 admits exactly one further + disclosure channel: a subject holding the company-CRUD capability + (contract 2 §8), when exercising the hierarchy schema §5.5 visibility + command, learns the target company's existence and its old/new + visibility values through the command's redacted actor receipt — + success for an existing target (private or directory alike) versus + `not_found` for a nonexistent id — bounded exactly as contract 2 §8.1 + states: no name, slug, structure, content, grant, or membership + information, and no read command of any kind. To every other + non-granted subject, private companies remain undisclosed everywhere, + including the directory; the directory remains the sole + existence-disclosure _listing_. + +### 10.2 What A3 explicitly does not change + +1. The directory itself is unchanged: read-only, directory-class companies + only, existence/name/slug only (§9.1.2's enumeration is narrowed for + capability holders' receipts, widened for nothing). +2. No join-request surface, no curation listing, no read command of any + family is authorized (§9.2.2 unchanged; a curation listing is a further + amendment per contract 2 §8.1). +3. The canonical audit event for visibility mutations is untouched — it + keeps hierarchy schema §5.2's full immutable target snapshot; the + capability confers no audit read (contract 2 §8.5). +4. Every other constraint of A1 and A2 is untouched. diff --git a/docs/requirements/rbac-grant-model.md b/docs/requirements/rbac-grant-model.md index edcd86f8..accf2830 100644 --- a/docs/requirements/rbac-grant-model.md +++ b/docs/requirements/rbac-grant-model.md @@ -24,6 +24,26 @@ 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". +Amendment 1 (company-CRUD capability): defines the capability class that +contract 1 Amendment 1 (Ruling 4b, 2026-08-28) and hierarchy schema §5.5 +anticipate. §8 defines the capability as a platform-scoped, admin-assigned, +audited delegation of exactly the hierarchy schema §5.5 company visibility +command — no read command, no other company operation; the mutation's +inherent existence disclosure is ratified as a bounded carve-out to +hierarchy schema §6.7/§2.8 and to kanban SOT Amendment A2's +sole-disclosure rule — SOT Amendment A3 (native-kanban-sot.md §10, this +PR) expressly extends A2 by exactly this carve-out (§8.1). The holder +sees only a redacted actor receipt; the canonical audit event keeps +hierarchy schema §5.2's full immutable snapshot. The hierarchy role +vocabulary (§2), every evaluation rule (§3), and grant management (§4) are +untouched: the capability is not a `hierarchy_grants.role` value and +evaluates outside the chain; capability-row deletion joins §6.1's +revocation enumeration (§8.4). Until this amendment ratifies, the capability +class is empty and +the visibility command remains admin-only (hierarchy schema §5.5 states +this fallback; the shipped gate at +`apps/gateway/src/hierarchy/hierarchy.repository.ts` implements it). + 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 @@ -242,6 +262,191 @@ contract 1 §6): 8. Transfer: both-sides `owner` accepted, each single-side case refused (completing contract 1 §6.5). +## 8. Company-CRUD capability (Amendment 1) + +Hierarchy schema §5.5 authorizes the company visibility mutation for +exactly two actor classes: platform admins and "subjects holding the +company-CRUD capability that a follow-up amendment to contract 2 will +define". This section is that definition. The name is historical — coined +in contract 1 Amendment 1 before the capability's content was fixed — and +confers nothing by connotation: the ratified content is exactly §8.1. +Company _creation_ is already ruled open to active users and always +private (contract 3 §5.2, Ruling 4); rename, delete, and transfer of +companies remain hierarchy `owner` operations (§2.3, §5); none of those is +part of this capability, and widening it to any other operation is a +further amendment, not an implementation decision. + +1. **Content: exactly one command, no read command, disclosure stated.** + Holding the capability authorizes executing the hierarchy schema §5.5 + visibility command (`companies.visibility`, both directions: + `private → directory` and `directory → private`) on any company in the + deployment, and no other command of any family. It confers **no read + command**: no company enumeration, no curation listing, no structure + read. The practical flow this implies is deliberate: to publish a + private company, the holder is given the target identifier by the + requesting company `owner` out of band; to unpublish, the target is + already directory-listed. A curation listing for capability holders, + if ever wanted, is a further amendment with its own disclosure + analysis under hierarchy schema §6.7. + + **Existence disclosure carve-out, stated rather than pretended away:** + exercising a mutation inherently discloses its target's existence. + The command's result distinguishes an existing company (success, for + private and directory targets alike) from a nonexistent id + (`not_found`), so a holder presenting candidate ids learns existence — + exactly as a platform admin already does through the same command. + This amendment ratifies that disclosure as part of the §5.5 curation + authority, bounded as follows. The holder-visible surface is the + command's **actor receipt** — the mutation result payload, carrying + exactly the target id, old visibility, and new visibility, and + **nothing else**: no name, slug, structure, content, grant, or + membership information. The actor receipt is a redacted projection + distinct from the **canonical audit event**, which is unchanged by + this amendment: it keeps hierarchy schema §5.2's deletion-safe + immutable target snapshot (id, slug, and parent chain at event time) + in full. The two never converge on the holder: the capability confers + no audit read (§8.5), so the canonical event — and with it the slug + and parent chain — is reachable only by subjects independently + authorized to read audit data, never through this capability. A + successful publish additionally makes the target directory-listed to + every authenticated user; that is the command's ratified purpose + (hierarchy schema §5.5), not a leak. Hierarchy schema §6.7's + existence-oracle rule and §2.8's directory-only disclosure are amended + by exactly this carve-out for capability holders, kanban SOT Amendment + A3 (native-kanban-sot.md §10, this PR) expressly extends A2's + sole-disclosure enumeration by the same carve-out, and all three are + otherwise untouched. Witnessed in §8.6.3. + +2. **Holding: platform-scoped assignment, user subjects only.** The + capability is not a hierarchy grant: it attaches to no node, has no + role, and never enters §3 chain evaluation. It is held via a + `platform_capabilities` table whose column set is exactly (nothing + else, per the contract 1 §2.7 exhaustiveness discipline): + - `id` — uuid, primary key; + - `user_id` — text, NOT NULL, FK `users` **ON DELETE RESTRICT**; + - `capability` — text, NOT NULL, constraint-checked against exactly + `company_crud`; + - `granted_by` — text, NOT NULL, FK `users` **ON DELETE RESTRICT**; + - `created_at` — timestamptz, NOT NULL; + - UNIQUE (`user_id`, `capability`). + + The user FKs are **text**, not uuid, because `users.id` is a BetterAuth + text key (`packages/db/src/schema.ts`; custody schema records the same) + — PostgreSQL cannot reference a text primary key with a uuid column. + This matches the shipped `hierarchy_grants` shape exactly: uuid + surrogate `id`, text FKs to `users`. + + Both user FKs are RESTRICT for the same reason contract 1 §3.3 pins + RESTRICT on principal FKs: the identity contract (§7.3) gates user + deletion, and a cascade here could silently destroy a capability + without its §8.3 revocation audit event. Revocation is row deletion + through the §8.3 command — there is no other removal path, no expiry + column, and no tombstone. A deactivated holder confers nothing while + deactivated: identity contract §7.1 denies all authorization to + deactivated accounts, and the §8.4 predicate evaluates on the + authenticated live user. No team subjects (§1.4's suspension reasoning + applies with more force here — a workspace-bound team holding + deployment-wide curation authority has no ratified meaning). + +3. **Assignment is instance administration on the normal admin surface.** + Only platform admins (`users.role = 'admin'`) may assign or revoke the + capability, through an ordinary admin command (the same command class + `AdminGuard` governs, §1.1) — not through direct table writes. + Assignment delegates a slice of instance administration and is itself + an instance-administration act under §1.1. A capability holder as such + may NOT assign or revoke it (no self-propagation). Every assignment + and revocation is a semantic audit event carrying actor, verb, subject + user, and capability; serialized capability strings are namespaced per + §4.5 (`platform-capability:company-crud` — a bare `company_crud` in + any serialized artifact is non-conformant). +4. **Evaluation and revocation follow this contract's existing rules.** + The hierarchy schema §5.5 command's authorization predicate is: + `users.role = 'admin'` OR a live `platform_capabilities` row + (`user_id`, `company_crud`). Both disjuncts are evaluated live and + fail closed per §3.5 — **independently**: with capability state + unreadable (fault), the capability disjunct denies, but a platform + admin whose `users.role` is readable remains authorized through the + admin disjunct; with role state unreadable, the admin disjunct denies + likewise. A decision that can read neither denies. Capability-row + deletion is hereby added to §6.1's enumerated revocation paths: + it propagates identically, under §6.2's bound, on every transport — + no new HTTP/MCP command authorized by the deleted row after the + revoking transaction commits, and any cached authorization is + invalidated in the revoking transaction (§3.5). +5. **What it does not confer**, stated so implementing PRs cannot drift: + no hierarchy grant or effective role at any node; no workspace + authorization or membership; no content, structure, or roll-up read; + no grant management (§4.1 unchanged); no MCP scope; no other instance + administration (user management, system settings, provider + configuration remain platform-admin-only); no company create, rename, + delete, or transfer. Hierarchy schema §5.5's rule that a company + `owner` as such may NOT change visibility is unchanged — `owner` and + this capability are disjoint authorities that combine only by a + subject holding both. +6. **Verification requirements** (extends §7, binding on implementing + PRs): + 1. Schema witnesses (real PostgreSQL, `ci-postgres` service in the + `test` CI step): the `capability` CHECK constraint rejects any + value outside `company_crud`; NOT NULL enforced on every declared + NOT NULL column; UNIQUE (`user_id`, `capability`) rejects a + duplicate; both user FKs reject a dangling reference AND deleting a + referenced user is refused (RESTRICT witnessed in both directions); + the table's column set is exactly the §8.2 declared set (contract 1 + §6.2 discipline). + 2. Capability-only command matrix — the witness that proves "exactly + one command", not merely "at least one": a non-admin holder with no + other grants succeeds on the visibility command in **both** + directions with contract 1 §5.2's audit event (old and new values + as semantic content), and the **same** actor is refused, case by + enumerated case: every hierarchy mutation family (company/child + create under another's node, rename, delete, transfer); grant + create/change/revoke; the workspace read and write command + families; roll-up reads; structure reads — including the + not-found-indistinguishable refusal on a structure read of the very + company they just mutated (hierarchy schema §6.7); every + instance-administration surface other than the visibility command + (user management, system settings, provider configuration, and + capability assign/revoke itself); and MCP scope derivation yields + nothing — the §7.4 deny-by-default matrix gains this row. Company + creation compares against an eligible-user baseline: the holder's + create behaves exactly as any active user's — always `private`, + and a creation request carrying a visibility argument is refused + for holder and baseline alike (contract 3 §5.2). + 3. Disclosure bound (§8.1 carve-out witnessed, receipt and canonical + event separately): the mutation result for a private-valid target, + a directory-valid target, and a nonexistent id is exactly {success, + success, `not_found`}; the actor receipt for a success carries + exactly {target id, old visibility, new visibility} and no result + or error payload carries name, slug, structure, content, grant, or + membership data; the canonical audit event for the same mutation — + asserted directly against the hierarchy outbox, not through any + holder-facing surface — carries hierarchy schema §5.2's full + immutable snapshot (id, slug, parent chain); and the holder's + attempt to read audit data is refused (no audit read conferred, + §8.5), proving the receipt/event separation reaches the holder as + a redaction, not a weakened event. + 4. Assignment path, both polarities: a platform admin assigns and + revokes through the normal admin command (positive witnesses — + assign then observe the §8.6.2 allow, revoke then observe deny); a + non-admin — including a current capability holder — is refused + assign and revoke; every assign/revoke produces its audit event + with the namespaced string (§8.3); a direct-write path that skips + the command surface is non-conformant (the §8.3 command is the only + writer of `platform_capabilities`). + 5. Revocation joins the §7.6 matrix: assignment is decision-time-live + (capability assigned → the holder's next visibility command allows, + no re-login); after row deletion, the ex-holder's next visibility + command is refused **on every exposed transport**, measured with + the revocation and the decision on distinct physical connections; a + cached-authorization implementation proves transactional + invalidation (§3.5). Fail-closed fault witnesses, both disjuncts + (§8.4): with `platform_capabilities` unreadable, a non-admin holder + is denied while a platform admin remains authorized; with role + state unreadable, the admin disjunct denies. + 6. Owner-as-such refusal re-witnessed: hierarchy schema §6.9's + owner-cannot-publish witness re-asserted with the + `platform_capabilities` table present and empty for that owner. + ## Ruling request Ratify sections 1–7 as written, with one decision embedded and one @@ -257,3 +462,17 @@ interpretive resolution named: 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. + +## Ruling request (Amendment 1) + +Ratify §8, the Amendment 1 header note, and kanban SOT Amendment A3 +(native-kanban-sot.md §10 — the express A2 carve-out extension, which +binds only with this ratification) as written, with one decision +embedded: + +- Decision: the company-CRUD capability is a platform-scoped, + admin-assigned, audited delegation of exactly the hierarchy schema §5.5 + visibility command — no read command, no other company operation, with + the mutation's inherent existence disclosure ratified as a bounded + carve-out (§8.1). Say "agreed" or name the additional operations (or + the curation listing) you want it to carry.