Skip to content

feat(security)!: enforce book audience at the REST read layer; finish the ADR-0090 D2/D3 cleanup the P1 wave missed#2774

Merged
os-zhuang merged 2 commits into
mainfrom
claude/tender-johnson-nq25xp
Jul 10, 2026
Merged

feat(security)!: enforce book audience at the REST read layer; finish the ADR-0090 D2/D3 cleanup the P1 wave missed#2774
os-zhuang merged 2 commits into
mainfrom
claude/tender-johnson-nq25xp

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

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 only public books/docs; { permissionSet }-gated books require the caller to hold the named set; authenticated non-holders get 403, anonymous get 401.
  • A doc's effective audience is the union over the books that CLAIM it (ADR-0046 §6.7). Unclaimed docs default to org. Deliberate subtlety: the tree renderer's orphan pass ("nothing is ever dropped") does not count as a claim — otherwise any public book 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.
  • Fail closed (ADR-0049): when permission-set holdings cannot be resolved (security service absent / resolution throws), gated audiences deny.
  • doc/book single-item reads bypass the shared meta cache — a per-caller gate cannot share ETags across viewers.
  • Plumbing: pure helpers in spec (audienceAllows, resolveDocAudiences, docAudienceAllows, resolveBookClaimedDocs) so REST and any future portal shell share ONE semantics; plugin-security's security service gains resolvePermissionSetNames(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_REGISTRY still had dead role/profile keys while the position type had LOST its form layout in the P1 rename → role:position:, profile: deleted.
  • metadata's ARTIFACT_FIELD_TO_TYPE still mapped roles → 'role', which matches nothing since stacks declare positionscompiled positions were silently dropped from artifact ingestionpositions → 'position'.
  • EnvironmentArtifactMetadataSchema: roles/profilespositions (old artifacts still parse via passthrough()).
  • Metadata-form translations (en/zh-CN/ja-JP/es-ES): role/profile groups removed (their copy still taught the pre-D3 hierarchy model), position group added, with a vocabulary regression test in platform-objects.

3. D3 vocabulary ratchet for docs + source fixes

  • New scripts/check-role-word.mjs (wired into lint.yml): \brole\b scan over content/docs + skills with 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.mdxaudience-based-interfaces.mdx (content was already audience-vocabulary; the URL wasn't), all inbound links updated.
  • Stale permission-vocabulary copy fixed in 5 docs (incl. a reference to the long-renamed bootstrapDeclaredRoles); identifier/comment cleanup in security-plugin.ts, bootstrap-declared-positions.ts, position.test.ts, audit.zod.ts; RolePosition in the eslint + check-doc-authoring domain lists (defineRole no longer exists).

4. Publish lint learns about books

  • Book name/label join the security-role-word scan (book audience is a permission-model reference now).
  • New advisory rule 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; warning because 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-fixed all pass. Changeset included (spec major; rest/plugin-security/lint/metadata minor; platform-objects patch).

Known follow-ups (not in this PR)

  • Ratchet the 45-file role-word baseline down incrementally.
  • 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 ?package query, 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

… 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
@vercel

vercel Bot commented Jul 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Canceled Canceled Jul 10, 2026 10:51am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation ci/cd dependencies Pull requests that update a dependency file protocol:system tests tooling labels Jul 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 6 package(s): @objectstack/lint, @objectstack/metadata, @objectstack/platform-objects, @objectstack/plugin-security, @objectstack/rest, @objectstack/spec.

97 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata, packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/cli.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via packages/metadata, @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/permissions/access-recipes.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint, packages/plugins/plugin-security, packages/rest, @objectstack/spec)
  • content/docs/permissions/explain.mdx (via @objectstack/plugin-security)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via packages/plugins/plugin-security, @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/plugin-security, @objectstack/rest, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/metadata, @objectstack/platform-objects, @objectstack/plugin-security, @objectstack/rest, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/metadata-service.mdx (via @objectstack/metadata)
  • content/docs/protocol/objectos/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via packages/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via packages/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-security, @objectstack/rest, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/audience-based-interfaces.mdx (via packages/plugins/plugin-security)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/platform-objects, @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…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
os-zhuang marked this pull request as ready for review July 10, 2026 11:32
@os-zhuang
os-zhuang merged commit 02f6af4 into main Jul 10, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/tender-johnson-nq25xp branch July 10, 2026 11:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation protocol:system size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants