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 7829014b..accf2830 100644 --- a/docs/requirements/rbac-grant-model.md +++ b/docs/requirements/rbac-grant-model.md @@ -30,7 +30,11 @@ 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 (§8.1). The hierarchy role +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 @@ -292,15 +296,26 @@ further amendment, not an implementation decision. (`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 mutation result and its audit event - disclose to the holder the target's existence and its old/new - visibility values, and **nothing else** — no name, slug, structure, - content, grant, or membership information. 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 and are otherwise untouched. Witnessed in §8.6.3. + 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 @@ -308,13 +323,19 @@ further amendment, not an implementation decision. `platform_capabilities` table whose column set is exactly (nothing else, per the contract 1 §2.7 exhaustiveness discipline): - `id` — uuid, primary key; - - `user_id` — uuid, NOT NULL, FK `users` **ON DELETE RESTRICT**; + - `user_id` — text, NOT NULL, FK `users` **ON DELETE RESTRICT**; - `capability` — text, NOT NULL, constraint-checked against exactly `company_crud`; - - `granted_by` — uuid, NOT NULL, FK `users` **ON DELETE RESTRICT**; + - `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 @@ -391,12 +412,19 @@ further amendment, not an implementation decision. 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): the mutation result - for a private-valid target, a directory-valid target, and a - nonexistent id is exactly {success, success, `not_found`}; no - result or error payload carries name, slug, structure, content, - grant, or membership data; the holder's audit-visible surface - carries only existence and old/new visibility. + 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 @@ -437,7 +465,9 @@ interpretive resolution named: ## Ruling request (Amendment 1) -Ratify §8 and the Amendment 1 header note as written, with one decision +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,