Repository navigation
fix(core): authority is gated the same way whenever it is written - #468
Merged
Merged
Conversation
A creator removed from the roster that conveyed their read could still editAccess() their Record and grant themselves back in: the reshare gate asked owner-or-creator but never whether the requester reaches the Record. canReshare() now asks read too, so "mutation is never blind" covers the authority aspect as it already covered the data aspect. Spec: access-control.md states the read requirement on resharing; identity.md § Group gains "Governing a group stack" — owner authority, unrecorded owner reads, and what removal takes away. Closes #419 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C1hFUmrN4Jzp8KUz6mdMsY
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C1hFUmrN4Jzp8KUz6mdMsY
🦋 Changeset detectedLatest commit: 4366c68 The changes in this PR will be included in the next version bump. This PR includes changesets to release 10 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Resharing needs read on the Record; setting `permissions` or `unlisted` at create() is the same capability exercised before the Record exists, so it is asked the same question. A non-owner creator may pass either only if their type grants alone would let them read the Record — the permissions being written cannot supply that read. A drop-box contributor can no longer name themselves a reader at create time. Also: the changeset is minor (a reshare that succeeded now refuses), and identity.md § Governing a group stack is trimmed to links and states what removal does and does not take away. Refs #419 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C1hFUmrN4Jzp8KUz6mdMsY
…make The create-time check restated canReshare() by hand, with its own _group branch and its own delegation check. It now builds the Record the create would produce — stamped as Stack.create() stamps it, without the permissions being written — and asks canReshare() of it. The stamped admin answers the _group case, and owner-or-creator answers delegation, so mayGrantAccess() and the two delegated refusals in create() go. Spec: the create-time rule and the delegation paragraph state it as the reshare gate; the per-verb row is simplified; "authorship alone confers no read". identity.md drops § Governing a group stack: the owner points are already owned by access-control.md and journal.md, and the removal rule by access-control.md, which § Group now links in one sentence. Refs #419 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C1hFUmrN4Jzp8KUz6mdMsY
A delegated app could name itself admin on a Group it created, then manage and reshare that Group — at create time once the reshare gate replaced mayGrantAccess(), and through editAccess() afterwards on main. Group-targeted grants already refuse roster-derived authority on the principal's side; group management now follows the same rule: under delegation the principal must be the owner and the subject an admin or the owner. Creating a Group writes its roster, so the create is asked the management rule too, of the Group it would make — the same move as the reshare gate on create-time permissions. Both now share one prospective Record, built by createAssociations(), which Stack.create() also uses to stamp a Group's first admin. Spec: identity.md § Group states the delegated management rule and the create gate; access-control.md fixes the per-verb row, the delegation sentence, and names unlisted beside permissions at create. Refs #419 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C1hFUmrN4Jzp8KUz6mdMsY
passesWriterFence() decides every family's fence — none for ordinary families, the owner acting alone for _grant and _install, the Group's managers for _group — and is asked of a stored Record on update and of the Record a create would make. It replaces requireOwnerAloneWriter() and gateVerb()'s own group branch, so "the family fences apply to create() too" is the same code rather than an outcome of grant rules. The create-time Record now carries none of the authority the call writes: the caller's roster is left out as its permissions already were, per "no call can combine gates". Spec: the delegation paragraph that let a rostered app manage a Group for another admin states the rule as it is. Refs #419 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C1hFUmrN4Jzp8KUz6mdMsY
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #419.
A creator removed from the roster that gave them read could still reshare their Record.
editAccess(),setAccess()andsetUnlisted()asked only owner-or-creator, never whether the requester can read the Record. That let a removed member grant themselves back in. Fixing it surfaced two related gaps, and the PR closes all three with existing rules rather than exceptions.1. Resharing needs read.
Authority.canReshare()asks owner-or-creator, on both sides of a delegation, plus read._groupRecords keep their own rule: roster management.2. Authority written at
create()is gated as a later write of it would be. The gate is asked of the Record the create would make:_group's stamped first admin (viacreateAssociations()inaccess.ts, shared withStack.create()).permissionsnor a roster, so no call can satisfy its own gate.passesWriterFence()is one predicate deciding every family's fence (owner-alone for_grant/_install, managers for_group, none otherwise) on both create and update, andcanReshare()decidespermissionsandunlisted.mayGrantAccess(), the two delegated-only refusals increate(),requireOwnerAloneWriter(), andgateVerb()'s separate group branch are gone.3. A roster entry never counts on the principal's side. Group-targeted grants already followed this rule; Group management didn't. A delegated app could name itself
adminand then manage and reshare that Group, througheditAccess()onmaintoo. Under delegation,isGroupManager()now requires the principal to be the owner and the subject to be an admin or the owner. So a delegated app neither creates nor manages a Group, and the owner's own software acting for a subject still can.Tests pin:
groupgrantee;read-owncontributor passes;unlistedrefusal at create;_groupgrants, refused management (editAccess,patchContent);Changeset:
minorfor@haverstack/core.Spec
access-control.md § Write implies read: resharing reads the same way, asked at create of the Record being made, which carries none of the authority being written. Authorship alone confers no read.access-control.md § Delegation: principal and subject: Group management is the owner on the principal's side; a rostered app manages a Group only when signing as itself; authority written atcreate()is gated as later.access-control.md § Reference-creation gating:permissionsandunlistedat create pass the reshare gate.access-control.md § Per-verb gates: the reshare row says "asked of both sides of a delegation", and the family fences apply tocreate()too.identity.md § Group: the delegated management rule; the create bullet asks the management rule of the Group being made; removal withdraws what the roster conveyed.Verification
All pass locally:
pnpm run format:check,check:refs,lint,test,build,typecheck. Each new test fails without its fix.Notes for reviewers
main. A later way for the owner to give an app management directly, through a grant rather than a roster entry, would add to this without undoing it._groupcreate that also setspermissionsorunlisted, the management check is asked twice: once as the family fence, once insidecanReshare(). That's the same order the update path uses (gateReshare()), and the second can't fail once the first passes. Folding them would need a_groupbranch at create, which is what this PR removes.StackNotFoundError(disclosure.md). At create time the refusal isStackPermissionError.🤖 Generated with Claude Code
https://claude.ai/code/session_01C1hFUmrN4Jzp8KUz6mdMsY