feat(security)!: enforce book audience at the REST read layer; finish the ADR-0090 D2/D3 cleanup the P1 wave missed#2774
Merged
Conversation
… the ADR-0090 D2/D3 cleanup the P1 wave missed
Follow-through on the { permissionSet } book-audience rename (#2732), in
ADR-0049 discipline — the gated arm existed but nothing enforced it:
- rest: /meta/book, /meta/doc, and /meta/book/:name/tree now enforce the
ADR-0046 §6.7 audience model. Anonymous callers see only public
books/docs; { permissionSet }-gated books require holding the named set;
a doc's effective audience is the union over the books that CLAIM it
(unclaimed → org; orphan rendering never inherits public). Fails CLOSED
when holdings cannot be resolved. doc/book item reads bypass the shared
meta cache (per-caller gate vs shared ETag). Nine new route tests.
- spec: pure helpers powering the gate (audienceAllows,
resolveDocAudiences, docAudienceAllows, resolveBookClaimedDocs) with
unit tests; the REST layer and any future portal share ONE semantics.
- plugin-security: security service exposes resolvePermissionSetNames —
the same resolution as data-plane enforcement.
- D2/D3 leftovers: METADATA_FORM_REGISTRY role→position (the position
type had LOST its form layout in the P1 rename) and profile removed;
artifact ingestion maps positions→'position' (stale roles→'role'
matched nothing and silently dropped compiled positions);
EnvironmentArtifactMetadataSchema declares positions; metadata-form
translations regain position and drop role/profile in all four locales
(+ vocabulary regression test); position.test/audit.zod/security-plugin
identifiers and comments de-role'd; eslint + doc-authoring domain lists
Role→Position; content/docs/ui/role-based-interfaces.mdx renamed to
audience-based-interfaces.mdx with stale permission-vocabulary copy
fixed across five docs.
- lint: books join the D3 role-word scan; new advisory rule
security-book-audience-unknown-set flags a gated audience naming a set
the stack does not declare (runtime fails closed — surface the typo at
author time).
- scripts/check-role-word.mjs: ADR-0090 D3 vocabulary RATCHET over
content/docs + skills (baseline freezes the 45 current files; new
occurrences fail CI; improvements ratchet the baseline down). Wired
into the lint workflow.
Verified: spec/objectql(808)/cli(464)/rest(227+9)/metadata(260)/
platform-objects(76)/plugin-security/lint(32) suites green; eslint,
check:role-word, check:doc-authoring, check:api-surface (regenerated),
check:liveness, check:skill-docs, check-changeset-fixed all pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ph4jEAfWDh6taMbZweWhom
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 6 package(s): 97 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…meout The in-process variant cold-loads sucrase + typescript and took 5.6s on a loaded CI runner, tripping vitest's default 5s per-test timeout (Test Core failure on this PR's first run). The test asserts a loading CONTRACT, not latency — give it an explicit generous timeout like the sibling dist-based variants effectively have via their spawn overhead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ph4jEAfWDh6taMbZweWhom
os-zhuang
marked this pull request as ready for review
July 10, 2026 11:32
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
Follow-through on #2732 (book audience
{ permissionSet }rename). Five threads, all ADR-0090-grounded:1. The read layer now ENFORCES book audience (ADR-0049)
The gated arm existed but nothing enforced it — an unenforced security property. Now:
/meta/book(list + item),/meta/doc(list + item),/meta/book/:name/tree: anonymous callers see onlypublicbooks/docs;{ permissionSet }-gated books require the caller to hold the named set; authenticated non-holders get 403, anonymous get 401.org. Deliberate subtlety: the tree renderer's orphan pass ("nothing is ever dropped") does not count as a claim — otherwise anypublicbook would leak every unclaimed doc of its package. Tree entries are additionally filtered per-doc, so an anonymous reader of a public book never sees nav entries that would 401 on fetch.doc/booksingle-item reads bypass the shared meta cache — a per-caller gate cannot share ETags across viewers.audienceAllows,resolveDocAudiences,docAudienceAllows,resolveBookClaimedDocs) so REST and any future portal shell share ONE semantics;plugin-security'ssecurityservice gainsresolvePermissionSetNames(ctx)— the same resolution as data-plane enforcement (positions expanded, additive baseline), so the docs gate can never drift from it.2. D2/D3 leftovers — two real regressions found
METADATA_FORM_REGISTRYstill had deadrole/profilekeys while thepositiontype had LOST its form layout in the P1 rename →role:→position:,profile:deleted.metadata'sARTIFACT_FIELD_TO_TYPEstill mappedroles → 'role', which matches nothing since stacks declarepositions— compiled positions were silently dropped from artifact ingestion →positions → 'position'.EnvironmentArtifactMetadataSchema:roles/profiles→positions(old artifacts still parse viapassthrough()).role/profilegroups removed (their copy still taught the pre-D3 hierarchy model),positiongroup added, with a vocabulary regression test in platform-objects.3. D3 vocabulary ratchet for docs + source fixes
scripts/check-role-word.mjs(wired into lint.yml):\brole\bscan overcontent/docs+skillswith a ratchet baseline — the 45 current files are frozen (many are legitimate: better-auth boundary, ARIA samples, educational "formerly roles"); NEW occurrences fail CI; improvements must ratchet the baseline down.content/docs/ui/role-based-interfaces.mdx→audience-based-interfaces.mdx(content was already audience-vocabulary; the URL wasn't), all inbound links updated.bootstrapDeclaredRoles); identifier/comment cleanup insecurity-plugin.ts,bootstrap-declared-positions.ts,position.test.ts,audit.zod.ts;Role→Positionin the eslint + check-doc-authoring domain lists (defineRoleno longer exists).4. Publish lint learns about books
security-role-wordscan (bookaudienceis a permission-model reference now).security-book-audience-unknown-set: a{ permissionSet }audience naming a set the stack does not declare. Runtime fails closed — the typo cost is "nobody can read the book" — so surface it at author time;warningbecause an environment-authored book may legitimately reference an installed package's set.Verification
spec (full), objectql (808), cli (464), rest (227 + 9 new gating tests), metadata (260), platform-objects (76 incl. new vocabulary guard), plugin-security (261), lint (32 incl. 3 new) — all green. eslint,
check:role-word,check:doc-authoring,check:api-surface(regenerated),check:liveness,check:skill-docs,check-changeset-fixedall pass. Changeset included (spec major; rest/plugin-security/lint/metadata minor; platform-objects patch).Known follow-ups (not in this PR)
AgentSchema.role(AI persona string) →persona— the last substantive source-level exception to the word ban.resolveBookTree's orphan pass is not package-scoped: without a?packagequery, other packages' unclaimed docs join any book's Uncategorized group. The audience gate now bounds the blast radius, but nav correctness deserves its own issue.🤖 Generated with Claude Code
https://claude.ai/code/session_01Ph4jEAfWDh6taMbZweWhom
Generated by Claude Code