Skip to content

feat(spec)!: book audience gates by permission set — retire the { profile } arm (ADR-0090)#2732

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

feat(spec)!: book audience gates by permission set — retire the { profile } arm (ADR-0090)#2732
os-zhuang merged 1 commit into
mainfrom
claude/tender-johnson-nq25xp

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Summary

BookAudienceSchema (packages/spec/src/system/book.zod.ts) still modelled its gated arm as { profile: string } — a reference to the Profile concept ADR-0090 D2 deleted — with pre-D3 vocabulary ("role-gated", "guest profile") in the comments. This PR retires it with a launch-window one-step rename, no aliases.

Decision: permission set, not position

The gated arm becomes { permissionSet: string }, a capability reference, for three reasons grounded in ADR-0090:

  1. Package ownership (D9). Books ship in packages; packages own permission sets but never positions/BUs/teams — those are environment-owned. A book gating to a position name would reference an entity the package cannot know, which is exactly the environment-ownership argument (D2 Add metamodel interfaces for ObjectQL/ObjectUI contract #1) that removed profiles.
  2. D9's admin stance is this shape. App-level admin is "an ordinary package set (crm_admin) the customer binds to a position of their own choosing" — so a CRM package's Admin Guide gates to { permissionSet: 'crm_admin' }, keeping ADR-0086 provenance and uninstall semantics intact.
  3. Vocabulary axis (D3). "Who may read this book" is a capability question; capability = permission_set. Positions are distribution. The two literals already map to the D9 anchors: 'org' inherits the package grant (authenticated members), 'public' ≡ the built-in guest position.

ADR-0046 §6.7 itself glossed the arm as "gates a book to a profile / permission set", so this lands on the reading that survived ADR-0090.

Changes

The REST read layer only evaluates isPublicAudience (anonymous gate), so no runtime evaluation code changes; enforcement of the gated arm remains the ADR-0046 Phase D item it already was.

Companion PR

ObjectUI mirrors this shape in three places (metadata-admin JSON schema oneOf, BookPreview audience chip, book list-column renderer) — companion PR in objectstack-ai/objectui updates them to { permissionSet }.

Why the D3 forbidden-word lint didn't catch this

The D3 lint (security-role-word, packages/lint/src/validate-security-posture.ts, shipped in P3 #2711) validates published stack metadata — names/labels of objects, fields, actions, permission sets, positions, and apps. It could not have caught this because:

  1. The violation lives in spec source code (a TS comment and a zod key), which no lint scans for the word ban — the ADR's "identifiers, UI copy, and docs" scope is enforced for metadata only; source hygiene relied on the P1 rename sweep (feat(security)!: ADR-0090 P1 breaking wave — position rename, profile removal, secure OWD default, principal/external spec shapes #2697), which was identifier-driven (sys_role, isProfile, …) and missed this file because the arm was spelled profile, not role.
  2. Even as metadata, books are not among the kinds the lint scans — and profile (unlike role) is not a banned word, so a book authored with { profile: ... } drew no finding. After this PR the schema itself rejects the shape, which is the stronger, contract-first gate.

Possible follow-ups (not in this PR): a source/docs-level \brole\b sweep check (e.g. content/docs/ui/role-based-interfaces.mdx still violates the D3 docs ban), and adding book name/label to the lint's role-word scan now that book audience is a permission-model reference. Two more D2 leftovers found in objectui's default-schemas.ts (role and profile metadata-type entries mirroring the framework's metadata-form-registry.ts profile: permissionForm row) are flagged in the companion PR rather than fixed, since removing them needs a coordinated registry change.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ph4jEAfWDh6taMbZweWhom


Generated by Claude Code

…file } arm (ADR-0090)

ADR-0090 D2 removed the Profile concept, but BookAudienceSchema still
modelled its gated arm as { profile: string } (with pre-D3 vocabulary in
the comments). Books ship in packages, and packages own permission sets
but never positions (D9 — the same environment-ownership argument that
removed profiles), so the gate is a capability reference:

- `{ profile: string }` → `{ permissionSet: string }` — one-step rename,
  no alias; the zod union now rejects `{ profile }` at parse time.
- `'public'` comment now points at the built-in `guest` position (D9)
  instead of the removed "guest profile".
- Regenerated content/docs/references (book.mdx), which also catches up
  security/* with the AdminScope (D12) and explain (D6) shapes that
  landed without a docs regen.

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 Ready Ready Preview, Comment Jul 10, 2026 3:44am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:system tests tooling size/m labels Jul 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

93 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/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @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 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/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 @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/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @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/spec)
  • content/docs/plugins/packages.mdx (via @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/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/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @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/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.

@os-zhuang
os-zhuang marked this pull request as ready for review July 10, 2026 04:43
@os-zhuang
os-zhuang merged commit 80f12ca into main Jul 10, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/tender-johnson-nq25xp branch July 10, 2026 04:43
os-zhuang added a commit to objectstack-ai/objectui that referenced this pull request Jul 10, 2026
…pec's { permissionSet } shape (ADR-0090) (#2371)

The framework retired BookAudience's { profile } arm (ADR-0090 D2 removed
the Profile concept; D9 makes the gate a capability reference). Update the
three UI mirrors: the metadata-admin default JSON schema oneOf, the
BookPreview audience chip, and the book list-column renderer. The "Role-
gated" form title also violated the D3 word ban and is now
"Permission-set gated".

Companion to objectstack-ai/objectstack#2732.


Claude-Session: https://claude.ai/code/session_01Ph4jEAfWDh6taMbZweWhom

Co-authored-by: Claude <noreply@anthropic.com>
os-zhuang added a commit that referenced this pull request Jul 10, 2026
… the ADR-0090 D2/D3 cleanup the P1 wave missed (#2774)

* feat(security)!: enforce book audience at the REST read layer; finish 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

* test(lint): de-flake lazy-deps in-process gate test — explicit 30s timeout

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:system size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants