Skip to content

docs: give the grant surface a wire account, and fix stale references - #127

Merged
cuibonobo merged 1 commit into
mainfrom
claude/server-permissions-verify-0qq3ai
Sep 19, 2026
Merged

cuibonobo merged 1 commit into
mainfrom
claude/server-permissions-verify-0qq3ai

Conversation

@cuibonobo

Copy link
Copy Markdown
Member

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.md documented 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 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 needs includeUnlisted=true to be found — which is what a "what have I granted" view has to pass.

  • docs/deployment.md inverted the rule it was warning about. It 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 at validation, so the bullet described the exact misreading the overhaul was designed to eliminate. Its link to 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 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 surface
  • docs/deployment.md § The DID handshake is open by design

The 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.md are not written from reading core. They were checked against a running server by a wire-only client (fetch plus the DID handshake, no Stack import) driving startTestServer() as a small multi-tenant app — 126 assertions, all passing, covering:

  • type-level grants and the contributor bundle; read-own vs read-any reach
  • grantee validation — all eight malformed grantee shapes refused at the write
  • refusal again at evaluation, via POST /records with _grant@1, which is exactly the unvetted path the spec anticipates: a grant on _app confers no create; update-any with no read companion conveys nothing
  • the _grant write fence across every verb — PATCH, both /permissions endpoints, both /associations endpoints, DELETE, undelete, restore — and that delegation does not carry it
  • the reshare gate, write-implies-read, and an inert permissions key riding along with a content patch without moving the ACL
  • group rosters: roster removal and group deletion both withdrawing immediately, a record-target roster entry conferring nothing
  • delegation as the intersection of both parties' grants, and default/group grants not counting on the principal side
  • query scoping re-derived on the worker thread, including revocation taking effect on the next query
  • change-feed scoping on a live subscription, including a revocation mid-stream
  • the journal's authority projection: present for a resharer, withheld from a non-resharer and under delegation

Four 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=true note 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 following docs/api.md previously 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:

  1. 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 only dist, 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).

  2. 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-fixtures in core, where this repo's existing coverage gate would force adoption. The genuinely server-specific ones — worker-thread query scoping, live feed revocation — belong in tests/ 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

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
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