Skip to content

feat(conformance-fixtures): pin the grantee arms and the evaluation-time refusals - #320

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

Two fixtures covered a _grant with no grantee at all and a group arm with no role. Both said in prose that the other arms "are refused the same way" — a rule argued in a fixture description and checked by nothing. This adds the nine fixtures that stand behind it.

Seven pin the write:

Fixture What it pins
…grant-entity-grantee-without-entity-id the entity arm's half of the rule the existing group-without-role fixture pins
…grant-entity-grantee-with-empty-entity-id empty as a separate input class from absent — where a presence check and a truthiness check stop agreeing
…grant-group-grantee-without-group-id a role with no roster to read it from names nobody
…grant-group-grantee-with-listing-only-role role: 'any' is a widening a listGrants() query may ask for and a stored grantee may never carry
…grant-grantee-with-unknown-kind there is no wider tier to fall back to
…grant-null-grantee null is the missing field it is, not absent-then-defaulted
…anyone-element-labelled-write read is the whole of what the tier can say

The last is load-bearing past the element itself: stored, an anyone element carrying write would read as world read and satisfy the read a write element needs beside it, so one malformed element would silence write-implies-read for every grantee on the record.

Two more pin the half no request shape can show — that a grant reaching storage unvetted confers nothing when it is read back:

  • error-permission-grant-on-an-ungrantable-family-confers-nothing — a stored _grant naming _app@1 still answers 403 on a create.
  • error-not-found-mutate-grant-with-no-read-companion — a grant carrying update-any with no read-any beside it conveys neither the write nor a read, so it answers 404, not 403.

Both are states grant() refuses and that a server mapping a request body, an import, or a foreign server's response can still produce — which is the case the two-sided rule exists for, and the one a second implementation is most likely to get half right.

Spec

No behavior change. Every rule here is already stated in docs/spec/access-control.md — these fixtures pin what the spec says rather than changing it. Each fixture cites the section it comes from:

Verification

All six, clean:

pnpm run format:check   ✓
pnpm run check:refs     ✓  454 anchor links and 468 § citations all resolve
pnpm run lint           ✓
pnpm test               ✓  no failures across all 12 packages
pnpm run build          ✓
pnpm run typecheck      ✓

Every expected response was observed against the reference server, not derived from the spec — details paths and messages included, since adapter-api's error dispatch checks only status and code and the details would otherwise be free to drift. The two evaluation-time fixtures were confirmed the same way: a stored grant on _app@1 answering 403 on a create, and a companion-less update-any answering 404.

One test change fell out of that: adapter-api's error-fixture dispatch had no arm for the permissions endpoint, so the anyone fixture had nowhere to route. Added one that calls associate(), which picks the endpoint from the element's own kind.

Notes for reviewers

This will fail haverstack/server's CI once released, by design. That repo holds errorResponseFixtures to a coverage gate, so the nine new names have to be dispatched or explicitly skipped there. The server-side dispatch is a follow-up PR that has to wait on this release — it cannot pass CI against the registry until then. The minor bump is for the same reason: a consumer with a gate can observe new fixtures.

On the details fields. adapter-api compares status and error.code only, and the server's conformance test does the same, so details is normative documentation for other implementations rather than an asserted contract. That makes it the part most able to rot quietly. Worth deciding separately whether either consumer should start asserting it; I left both as they are.

Deliberately left out, as sequence fixtures rather than single pairs, and worth filing if wanted:

  • a soft-deleted _group withdrawing every permission and grant that reached through its roster
  • an unlisted _grant still conferring while excluded from enumeration
  • a permissions key the reshare gate reads as inert being dropped rather than forwarded

All three are overhaul rules worth pinning across implementations; each needs prior state and an ordering that a single request/response pair cannot express.

These came out of a verification pass on the server repo, driven by a wire-only client against a running server — 126 assertions over the permission surface. Enforcement held throughout; these are the rules that turned out to be pinned by prose alone. Server-side findings are in haverstack/server#127.

🤖 Generated with Claude Code

https://claude.ai/code/session_01St5i1czsAp26y8H56pmANC


Generated by Claude Code

…ime refusals

Two fixtures covered a _grant with no grantee and a group arm with no role,
and both stated in prose that the other arms "are refused the same way" — a
rule argued in a fixture description and checked by nothing.

Nine fixtures now stand behind it. Seven at the write: each incomplete arm,
an empty entityId as a separate input class from an absent one, the
listing-only role 'any' that a listGrants() query may ask for and a stored
grantee may never carry, an unrecognized kind, a null grantee reported as
the missing field it is rather than read as absent-then-defaulted, and an
`anyone` element labelled anything but `read`.

That last one is load-bearing past the element: stored, it would read as
world read *and* satisfy the `read` a `write` element needs beside it, so
one malformed element would silence write-implies-read for every grantee on
the record.

Two more pin the half no request shape can show — that a grant reaching
storage unvetted confers nothing when it is read back. A grant naming _app@1
still answers 403 on a create, and a mutate verb with no read companion in
the same grant conveys neither the write nor a read, so it answers 404
rather than 403. Both are states grant() refuses and a server mapping a
request body, an import, or a foreign server's response can still produce,
which is the case the two-sided rule exists for.

Every expected response was observed against the reference server rather
than derived from the spec, details included — adapter-api's error dispatch
had no arm for the permissions endpoint, which the `anyone` fixture needed.

No behavior change: each rule is already in docs/spec/access-control.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01St5i1czsAp26y8H56pmANC
@changeset-bot

changeset-bot Bot commented Sep 19, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f99ab6b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@haverstack/conformance-fixtures Minor

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

@cuibonobo
cuibonobo merged commit 7c65fd8 into main Sep 19, 2026
9 checks passed
@cuibonobo
cuibonobo deleted the claude/server-permissions-verify-0qq3ai branch September 19, 2026 22:01
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