Skip to content

Commit a8dfc16

Browse files
committed
docs(adr): ADR-0090 D5 offending bits are platform system permissions, not any systemPermissions
D5's last bullet made any non-empty `systemPermissions` an offending bit for an `everyone` anchor binding, and the shared predicate implemented it literally. That list conflates two unlike tokens: the platform's own powers, and a capability a package declared for itself under ADR-0066 D1. The bullet is narrowed to a `systemPermissions` entry naming a PLATFORM system permission, with a dated revision note recording the two token kinds, the three fail-closed boundaries (the absolute platform floor, provenance rather than spelling, and omission refusing), that the D9 `guest` tier is untouched, and that the consuming callers keep the pre-revision behaviour until they supply the declared list. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
1 parent 26e0d81 commit a8dfc16

1 file changed

Lines changed: 47 additions & 2 deletions

File tree

‎docs/adr/0090-permission-model-v2-concept-convergence.md‎

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,53 @@ Replaces both the `member_default` builtin-set fallback and ADR-0056 D7's defaul
237237
install-time prompt ("CRM suggests adding `crm_readonly` to Everyone — accept?"). It is **never**
238238
auto-bound: installing a package must not silently widen every tenant user's access.
239239
- **Lint (D7) hard-blocks high-privilege bits on `everyone` bindings**: `viewAllRecords`,
240-
`modifyAllRecords`, `allowDelete`, `allowPurge`, `allowTransfer`, and `systemPermissions` are
241-
rejected (or force a break-glass confirmation) on any set bound to `everyone`.
240+
`modifyAllRecords`, `allowDelete`, `allowPurge`, `allowTransfer`, and a `systemPermissions`
241+
entry naming a **platform** system permission are rejected (or force a break-glass
242+
confirmation) on any set bound to `everyone`. An **app-declared capability token** — one a
243+
package declared for itself under ADR-0066 D1, entering `sys_capability` with
244+
`managed_by: 'package'` + `package_id` provenance — is **not** an offending bit: it gates
245+
only what that same package's own resources require of it, so conferring it on the
246+
authenticated audience widens nothing the package did not itself define. See the revision
247+
note below.
248+
249+
> **Revision (2026-09-12, #17189) — the offending bit is a `systemPermissions` entry naming a
250+
> PLATFORM permission, not `systemPermissions` at all.** [ruled]
251+
>
252+
> As first written, the bullet above made **any** non-empty `systemPermissions` an offending bit,
253+
> and `describeHighPrivilegeBits` (`@objectstack/spec/security`) implemented it literally.
254+
> `systemPermissions` carries two unlike kinds of token, though: the platform's own powers
255+
> (`manage_users` and friends), and a capability a package **declared for itself** (ADR-0066 D1).
256+
> An app whose navigation gates on its own token therefore could not ship the set every employee
257+
> holds — the set's own gate made it unbindable to `everyone` — so the app was pushed toward
258+
> declaring no gates at all, the opposite of what ADR-0066 D1 exists to encourage. Reported by a
259+
> named downstream consumer that had to fall back to binding its baseline set to each position by
260+
> hand, plus one manual grant per new hire, indefinitely.
261+
>
262+
> **What changed**: an offending `systemPermissions` bit is one naming a platform system
263+
> permission. A token this stack declared as a package capability is not counted. Three boundaries
264+
> hold the narrowing, and each fails closed:
265+
>
266+
> 1. **The platform floor is absolute.** A platform capability name stays high-privilege however it
267+
> is declared, so a package cannot launder `manage_users` past the anchor gate by declaring a
268+
> capability of that name.
269+
> 2. **The discriminator is provenance, never spelling.** The predicate is *told* which names this
270+
> stack declared; it never infers "app token" from the shape of a name. ⛔ A naming-syntax rule
271+
> (dotted ⇒ app token) was considered and rejected: it misjudges in silence the first dotted
272+
> platform permission — `setup.access` is one today — and the first undotted app token.
273+
> 3. **Omission refuses.** A caller that cannot enumerate the stack's declarations gets the
274+
> pre-revision verdict, so a missing input narrows nothing.
275+
>
276+
> **`guest` is untouched.** D9's strictest tier keeps refusing any non-empty `systemPermissions`:
277+
> D5 speaks for authenticated members, and conferring an app's own gate on anonymous visitors is a
278+
> different act, not decided here.
279+
>
280+
> Ruling: director seat, batch #110, 2026-09-10 — option (i), the predicate takes the declared
281+
> capability list as an input. The landing order is the maintainer's, verbatim:
282+
> 「我们的项目以objectstack 协议为准,文档应该以实际实现为准。协议不正确的应该先修改协议。」
283+
> ⇒ this revision and the `packages/spec` predicate land first; the `plugin-security` boot refusal
284+
> and the `@objectstack/lint` `security-anchor-high-privilege` rule follow. Until those callers
285+
> supply the declared list they keep the pre-revision behaviour — the predicate's default — so the
286+
> narrowing reaches a binding only where a caller can name what this stack declared.
242287
243288
### D6 — Explain engine is P0; access-matrix snapshots gate publishes
244289

0 commit comments

Comments
 (0)