You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
(packages/core/src/access.ts:70–75; the spec's group-resolution note says the same — "verify the requester is a member or admin")
So admin is vocabulary with no semantics. The sharper version of the problem: membership rosters are governed by ordinary record permissions on the _group record. Whoever has write access to that record can add or remove member/admin associations — including granting membership, which then confers whatever access group ACLs carry. Membership is self-amplifying through generic write permission, and "roster change" is indistinguishable from "renamed the group."
Decided direction
admin means "can manage the group." No role hierarchy, no custom roles — one distinction, matching what a small cohesive group (the stated scale) actually needs: the group's creator delegating roster upkeep.
Role-gated group management. Mutating a _group record — updating its content, adding/removing member/admin relationship associations, deleting it — requires the requester to be an admin of that group, or the stack owner. Implemented as a named special case of the association-mutation predicate ScopedStack.create() passes caller options through unchecked — attachment-association escalation, parent/app spoofing #51 is already building in ScopedStack; _group is the system type where reference-creation is privilege-creation, so it slots into exactly that machinery.
Permission entries may target a role.{ access: 'group', groupId, role?: 'admin', read, write } — absent role means any member (today's behavior, fully backward compatible); role: 'admin' restricts the entry to admins. Mechanically cheap: isGroupMember already reads the labels, it just starts reporting which one matched.
Bootstrap rule. The creator of a _group record is stamped as its first admin at create time, so no group is ever management-orphaned. The stack owner can always intervene regardless (owner bypasses everything).
Semantics, stated in the spec:member = counted by group ACLs. admin = member + may manage the group record itself.
Per the standing decision (see #57): schema-level fallout, if any, is handled in place with no version bump — no install base yet. _group@1's content schema doesn't change here anyway; the role lives in associations and in the PermissionEntry type, which is record-level, not schema-level.
Alternative considered and set aside
Demote honestly: drop admin from the access check (only member counts), document roles as app-space decoration, and let roster protection come solely from #51's generic association gating. Defensible, but the self-amplifying-roster problem forces a _group special case in the write path regardless — at which point the machinery of the chosen option exists without its vocabulary. Rejected in discussion.
Open question (named, not resolved here)
#49 makes groups key-holding entities. Roles will eventually also answer "who may act as the group" — use its key, accept grants on its behalf, speak for it to other stacks. This issue's admin definition is deliberately about managing the group record and must not foreclose that; the acting-as question is deferred to #49 follow-on work.
Tests: member cannot edit roster or group content; admin can; owner can regardless; role: 'admin' ACL entry excludes plain members; absent role behaves exactly as today; creator-is-admin bootstrap
Spec: §Groups states the member/admin semantics and the bootstrap rule
Refs #51 (association-mutation gating this builds on), #49 (groups as key-holding entities; acting-as deferred), #57 (no-version-bump policy while there's no install base).
Problem
The group vocabulary has two roles, and nothing distinguishes them:
(
packages/core/src/access.ts:70–75; the spec's group-resolution note says the same — "verify the requester is a member or admin")So
adminis vocabulary with no semantics. The sharper version of the problem: membership rosters are governed by ordinary record permissions on the_grouprecord. Whoever has write access to that record can add or removemember/adminassociations — including granting membership, which then confers whatever access group ACLs carry. Membership is self-amplifying through generic write permission, and "roster change" is indistinguishable from "renamed the group."Decided direction
adminmeans "can manage the group." No role hierarchy, no custom roles — one distinction, matching what a small cohesive group (the stated scale) actually needs: the group's creator delegating roster upkeep._grouprecord — updating its content, adding/removingmember/adminrelationship associations, deleting it — requires the requester to be anadminof that group, or the stack owner. Implemented as a named special case of the association-mutation predicate ScopedStack.create() passes caller options through unchecked — attachment-association escalation, parent/app spoofing #51 is already building inScopedStack;_groupis the system type where reference-creation is privilege-creation, so it slots into exactly that machinery.{ access: 'group', groupId, role?: 'admin', read, write }— absentrolemeans any member (today's behavior, fully backward compatible);role: 'admin'restricts the entry to admins. Mechanically cheap:isGroupMemberalready reads the labels, it just starts reporting which one matched._grouprecord is stamped as its firstadminat create time, so no group is ever management-orphaned. The stack owner can always intervene regardless (owner bypasses everything).member= counted by group ACLs.admin= member + may manage the group record itself.Per the standing decision (see #57): schema-level fallout, if any, is handled in place with no version bump — no install base yet.
_group@1's content schema doesn't change here anyway; the role lives in associations and in thePermissionEntrytype, which is record-level, not schema-level.Alternative considered and set aside
Demote honestly: drop
adminfrom the access check (onlymembercounts), document roles as app-space decoration, and let roster protection come solely from #51's generic association gating. Defensible, but the self-amplifying-roster problem forces a_groupspecial case in the write path regardless — at which point the machinery of the chosen option exists without its vocabulary. Rejected in discussion.Open question (named, not resolved here)
#49 makes groups key-holding entities. Roles will eventually also answer "who may act as the group" — use its key, accept grants on its behalf, speak for it to other stacks. This issue's
admindefinition is deliberately about managing the group record and must not foreclose that; the acting-as question is deferred to #49 follow-on work.Work items
isGroupMember→ role-aware return (packages/core/src/access.ts)PermissionEntrygroup variant gains optionalrole(packages/core/src/types.ts+ spec §Permissions)ScopedStackgating for_groupmutations (content update, roster associations, delete) — coordinate with ScopedStack.create() passes caller options through unchecked — attachment-association escalation, parent/app spoofing #51's predicate_grouprecordsrole: 'admin'ACL entry excludes plain members; absentrolebehaves exactly as today; creator-is-admin bootstrapRefs #51 (association-mutation gating this builds on), #49 (groups as key-holding entities; acting-as deferred), #57 (no-version-bump policy while there's no install base).