Repository navigation
docs: give the grant surface a wire account, and fix stale references - #127
Merged
Merged
Conversation
A verification pass over the permission surface, driven by a wire-only client against a real server: 126 assertions across type-level grants, grantee validation, evaluation-time refusal, the _grant write fence, record-level permissions and the reshare gate, group rosters, delegation, query scoping on the worker thread, the change feed, and the journal's authority projection. Enforcement held on every one; what it turned up was documentation and references. docs/api.md described a grant's grantee shape and no way to write one: `_grant` appeared nowhere in the docs, so the only path to a grant -- POST /records with typeId `_grant@1` -- had to be inferred. Give it a section: the four routes that are the grant surface, the owner-acting- alone fence over every write verb, that reads stay on their ordinary gates, and that an unlisted grant still confers but needs includeUnlisted=true to be found, which is what a "what have I granted" view has to pass. docs/deployment.md told operators a grant with no grantee is a grant to the public, and spelled it `grant(null, ...)`. Absence now confers nothing and is refused, so the bullet inverted the rule it warned about; its link into api.md#access-control had no target either. Three spec references named sections that do not exist: the change feed's implementation checklist has moved to its own document, the 400/422 split sits under wire-format's error responses, and the disclosure rule has a document of its own that the code implementing it did not cite. Four comments described the move to the current behavior rather than the behavior. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01St5i1czsAp26y8H56pmANC
This was referenced Sep 19, 2026
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
A verification pass over the permission surface after core 0.37's overhaul, looking for anything the adoption missed.
Enforcement turned out to be clean. What the pass found was documentation and stale references:
docs/api.mddocumented a grant's grantee shape and no way to write one._grantappeared nowhere in the docs, so the only path to a grant —POST /recordswithtypeId: "_grant@1"— had to be inferred from core. Added a The grant surface section: the four routes that are the grant surface, the owner-acting-alone fence and every write verb it covers, that reads stay on their ordinary gates, and that an unlisted grant still confers but needsincludeUnlisted=trueto be found — which is what a "what have I granted" view has to pass.docs/deployment.mdinverted the rule it was warning about. It told operators "a grant with no grantee is a grant to the public" and spelled itgrant(null, ...). Absence now confers nothing and is refused at validation, so the bullet described the exact misreading the overhaul was designed to eliminate. Its link toapi.md#access-controlhad no target either.Three spec references named sections that do not exist. The change feed's implementation checklist has moved to
docs/spec/change-feed.md; the 400/422 split sits under wire-format's§ Error responses, not a§ 400 vs 422; and the disclosure rule has a document of its own (docs/spec/disclosure.md) that the code implementing it did not cite.Four comments described the move to the current behavior rather than the behavior, against the prose rule in AGENTS.md.
No code behavior changes.
Docs
No behavior change — this PR is the docs change. Sections updated:
docs/api.md§ Permissions → new § The grant surfacedocs/deployment.md§ The DID handshake is open by designThe wire contract itself is unchanged, so there is no companion core PR. Everything documented here was verified against this server's existing behavior rather than newly specified.
Verification
pnpm run format:check && pnpm run lint && pnpm typecheck && pnpm test— all clean, 555 tests across 28 files.Beyond that, the claims added to
docs/api.mdare not written from reading core. They were checked against a running server by a wire-only client (fetchplus the DID handshake, noStackimport) drivingstartTestServer()as a small multi-tenant app — 126 assertions, all passing, covering:read-ownvsread-anyreachPOST /recordswith_grant@1, which is exactly the unvetted path the spec anticipates: a grant on_appconfers no create;update-anywith no read companion conveys nothing_grantwrite fence across every verb — PATCH, both/permissionsendpoints, both/associationsendpoints, DELETE, undelete, restore — and that delegation does not carry itpermissionskey riding along with a content patch without moving the ACLFour of that run's initial failures were the test's expectations being wrong rather than the server's behavior — history access is the mutate surface rather than plain read, and the 403/404 split keys on readability. Both behaved exactly as specified once the assertions were corrected.
Notes for reviewers
The
GET /records?typeId=_grant@1+includeUnlisted=truenote is the one place worth a second opinion.Stack.listGrants()returns unlisted grants by default — core is explicit that a grant which confers and cannot be found is the state worth refusing outright — but the wire equivalent excludes them like any other unlisted record unless the owner asks. The server satisfies the letter of the rule; a reader followingdocs/api.mdpreviously had no way to know to pass the flag. Documenting it is the smallest fix, but if the asymmetry is itself unwanted, that is a core conversation rather than this PR.Two follow-ups worth filing, neither in scope here:
This repo has no
check:refs. Core added one and it is why core's own references hold; the three broken references fixed here rotted silently because nothing checks them. Core ships onlydist, so the server cannot validate cross-repo§citations against a local checkout — it needs a design call (fetch core's spec in CI, vendor a heading manifest, or check only this repo's own anchors).Where the wire-level assertions from that run should live. Most of them are obligations on any Haverstack server rather than on this one, which argues for
@haverstack/conformance-fixturesin core, where this repo's existing coverage gate would force adoption. The genuinely server-specific ones — worker-thread query scoping, live feed revocation — belong intests/here. Happy to split them that way in a follow-up.🤖 Generated with Claude Code
https://claude.ai/code/session_01St5i1czsAp26y8H56pmANC
Generated by Claude Code