docs: contract 2 Amendment 1 — company-CRUD capability (S2 follow-up) #1477
@@ -514,3 +514,47 @@ itself permits, so that contract does not stretch A1 by interpretation.
|
|||||||
default-open disclosure.
|
default-open disclosure.
|
||||||
5. Every other constraint of A1 — §8.1.2's remaining bullets, §8.2 in full,
|
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.
|
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.
|
||||||
|
|||||||
@@ -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
|
role-string namespacing rule added (§4.5); ruling request now names the
|
||||||
interpretive resolution of PRD "admins".
|
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
|
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
|
each hierarchy level confers, how grants evaluate down the chain, how
|
||||||
revocation propagates, and who may manage grants. Out of scope: the hierarchy
|
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
|
8. Transfer: both-sides `owner` accepted, each single-side case refused
|
||||||
(completing contract 1 §6.5).
|
(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
|
## Ruling request
|
||||||
|
|
||||||
Ratify sections 1–7 as written, with one decision embedded and one
|
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. A1 §8.1.3 does not attribute grant declaration to
|
||||||
platform admins, and the §1.1 decision above is what makes this reading
|
platform admins, and the §1.1 decision above is what makes this reading
|
||||||
binding.
|
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.
|
||||||
|
|||||||
Reference in New Issue
Block a user