Skip to content

Move _grant's grantee into content, add listGrants()/revoke() (#57)#99

Merged
cuibonobo merged 1 commit into
mainfrom
claude/issue-57-design-review
Jul 16, 2026
Merged

Move _grant's grantee into content, add listGrants()/revoke() (#57)#99
cuibonobo merged 1 commit into
mainfrom
claude/issue-57-design-review

Conversation

@cuibonobo

Copy link
Copy Markdown
Member

Summary

Stack.grant() wrote the grantee into record.entityId — the field that means "author" everywhere else. The owner creates a grant for Alice, and the record said Alice authored it, even though she may never have touched the stack. Concretely this meant:

  • Authorship queries (filter: { entityId: 'alice' } — "everything Alice wrote") returned grants about Alice, not by her.
  • RecordVersion.entityId inherited the same lie on any future grant edit.
  • hasGrant's -own check, applied to a _grant record itself, resolved to "is the grantee" rather than "authored it."

Changes

  • GrantContent gains optional granteeEntityId?: string. Absent = default grant (unchanged semantics).
  • grant() writes the grantee into content and no longer stamps record.entityId — a grant record is now owner-authored like any other system record the owner creates, closing the gap where every grant record violated the "owner records carry no entityId" invariant.
  • hasGrant() reads content.granteeEntityId for the grantee comparison instead of record.entityId.
  • No version bump for _grant@1 — additive optional field, no install base yet (per the standing policy this issue itself established).
  • Promotes the issue's own follow-up comment ("making the natural companion note official") into this PR: listGrants(entityId?) and revoke(entityId, grants) round out grant management now that the grantee is a queryable content field. revoke() matches by typeId baseId + action set (the same granularity grant() writes at) and soft-deletes — an accidental revocation is undeletable like any other mutation.
  • docs/spec.md Grant section updated: granteeEntityId in the content shape, the authorship invariant now holding for grant records too, and the new listGrants()/revoke() API.

Test plan

  • packages/core typecheck clean
  • packages/core lint clean
  • Full packages/core test suite passing (419/419), including:
    • updated grant tests asserting entityId is always absent and the grantee lives in content
    • authorship query (filter: { entityId }) no longer picks up grants naming that entity
    • a grant still resolves correctly through ScopedStack for its named grantee (regression check)
    • listGrants(): no-arg returns all, null returns only defaults, a specific entity returns grants naming them plus every default grant
    • revoke(): exact-match deletion by entity+typeId+action-set, leaves other entities/action-sets/defaults untouched, matches across type-family versions by baseId, soft-delete is undeletable

Generated by Claude Code

…ke() (#57)

grant() stamped the grantee into record.entityId, the field that means
"author" everywhere else — the owner creates a grant for Alice, and the
record says Alice authored it, an entity who may never have touched the
stack. This polluted "everything this entity authored" queries with grants
merely naming them, and made hasGrant's -own check resolve to "is the
grantee" instead of "authored it" for grant records specifically.

GrantContent gains an optional granteeEntityId; grant() writes it there and
stops stamping entityId, so a grant record is owner-authored like any other
system record it creates. hasGrant() reads content.granteeEntityId instead
of record.entityId. No version bump — additive field, no install base yet,
per #57.

Also promotes the issue's own follow-up comment to landed: listGrants(entityId?)
and revoke(entityId, grants) round out the read/undo side of grant
management now that the grantee is a queryable content field. revoke() is a
soft delete like any other mutation, so an accidental revocation is
undeletable the same way.

Fixes #57

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDmUW3oNxPSCLf2ftqaF8q
@cuibonobo
cuibonobo merged commit 7cf1ac9 into main Jul 16, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants