Repository navigation
feat(spec): requiredPermissions on ListViewSchema and DashboardSchema, the audience gate, planned until the /meta read gate applies it (#22611) - #22665
Conversation
…, the audience gate, planned until the read gate applies it Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
…iew and dashboard requiredPermissions; regenerate the ui authorable surface Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
…he two planned audience-gate keys Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
…iew and dashboard gate is not enforced yet; changeset Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
…ew-dashboard-required-permissions
…ew-dashboard-required-permissions
…'s editInline default beside this branch's audience-gate rows) Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 139 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 443c399d2a49e4cd4cda4c9447ff5cee55c7689c && git checkout 443c399d2a49e4cd4cda4c9447ff5cee55c7689c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ee3ae0360dc5beff655e4feb8f9624c731e306fd 1b39eebd716d95164adb38b5f508b8390edbc5f5 && git checkout -B drift-repro ee3ae0360dc5beff655e4feb8f9624c731e306fd && git merge --no-ff 1b39eebd716d95164adb38b5f508b8390edbc5f5
node scripts/docs-audit/affected-docs.mjs --json ee3ae0360dc5beff655e4feb8f9624c731e306fd
|
Contract reviewServed-tier: Inputs: card #22611 (body; comments 6094878821, 6095014058, 6095248235, 6096250858), card #22639 (body; comments 6095327925, 6096254862), PR #22665 (body, file list, the one bot comment), the net diff ① Derived judgmentsThe two keys, one shape — right. Every door that now accepts the key, named — right, and the generated references agree. Declared on the shape, so it reaches: Declared-is-enforced in the window before #22639 — every describe sentence is true at this head.
Liveness rows per Does the lint reach what the rows say — yes, exactly. No any-of form, no alias,
Generated references, authorable surface, reconciliation omit rows against the gates. The two ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
… that it declares requiredPermissions Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
|
Review-carry: 6096339589 ·
The head moved after the contract review PASS
The record carries to Generated by Claude Code |
Landing pre-checks at
|
Fixes #22611
Clause-②: yes (widening)
Summary
The spec half of the ruling of record
6095014058on #22611 (letter A).ListViewSchemaandDashboardSchemaeach gainrequiredPermissions, the platform's one audience key. It has the shape and meaning the app, navigation-item, action (ADR-0066 D4) and record-block keys already carry:z.array(z.string()).optional(), a list of capabilities, all required, no default. The two keys share one describe word for word. There is no any-of form; B was not taken. "Or" is one capability granted to several permission sets, and the docs say so whererequiredPermissionsis taught.Declared is enforced, so the window says so. No server reads either key until #22639 (
Blocked-by: #22611) lands the/metaread-gate half. Three things carry that fact:planned, carrier rest(/meta read gate): enforce a list view's and a dashboard'srequiredPermissionson the list read and the by-name read, so each audience is served only its own views and boards (the rest half of #22611) #22639, withauthorWarnand anauthorHint(no tracker number in the hint);[PLANNED — not enforced yet]clause that names no door as enforcing today;object.externalSharingModel,page.print,field.conditionalFormatting): twoomitrows in the form/Zod reconciliation ledger.This PR edits nothing under
packages/rest/**,packages/runtime/**or any server package.ListViewSchema.sharing(ownership and locking) is untouched.What changed
packages/spec/src/ui/audience-required-permissions.ts(new; outside theuibarrel, theview-history.ts/list-view-export-options.tsprecedent, so it is not published API):AUDIENCE_REQUIRED_PERMISSIONS_DESCRIPTION, the one describe. Its docblock explains each clause.packages/spec/src/ui/view.zod.ts:requiredPermissionsis added onListViewShapeSchema. It therefore reaches every list-view door built from that shape:ListViewSchema,ObjectListViewSchema(a container'slist/listViews, an object'slistViews), the view item'sconfigand the flattened list overlay.packages/spec/src/ui/dashboard.zod.ts:requiredPermissionsonDashboardSchema.packages/spec/liveness/view.json(list.children.requiredPermissions) anddashboard.json(requiredPermissions):planned,verifiedAt2026-10-10,authorWarn: true, anauthorHint, and a note that names the carrier and the flip condition. Also: README state-table notes for both types, and regeneratedstate-counts/(view 0 to 1 planned, dashboard 0 to 1 planned).authorable-surface/ui.json(+3:ui/Dashboard,ui/ListViewandui/ObjectListView:requiredPermissions) andcontent/docs/references/**(10 rows:ui/view.mdx6,api/protocol.mdx2,ui/dashboard.mdx1,data/object.mdx1). After the second merge oforigin/main,references/ui/view.mdxwas regenerated from the merged tree. The os-regen driver had kept this branch's bytes and dropped main'seditInlinedefault flip (feat(spec)!: ListView.userActions.editInline defaults to true on the v18 line; editInline: false is the opt-out (#22605, spec half) #22629); the regeneration commit restores it beside this branch's rows.content/docs/permissions/capabilities.mdx: a short section, "Or is one capability, granted twice", with the CLM shape (clm_legal_workbench.viewgranted toclm_legalandclm_admin), the AND trap, and a callout that the list-view and dashboard key is not enforced yet. It names no tracker number..changeset/22611-list-view-dashboard-required-permissions.md:@objectstack/specminor,Clause-②: yes (widening), naming rest(/meta read gate): enforce a list view's and a dashboard'srequiredPermissionson the list read and the by-name read, so each audience is served only its own views and boards (the rest half of #22611) #22639 as the enforcing half.packages/spec/src/ui/audience-required-permissions.test.ts(new), and two ledger rows inmetadata-form-zod-reconciliation.test.ts. Inpackages/lint/src/lint-liveness-properties.test.ts, a real-ledger block.The PM's mechanism assumptions, measured at
origin/main1b99388505app.zod.ts:364),AppSchema(:1593),ActionSchema(action.zod.ts:1536) and the four record blocks (component.zod.ts,RECORD_BLOCK_REQUIRED_PERMISSIONS_DESCRIPTION) are allz.array(z.string()).optional(). No shared schema or exported constant exists: the record-block describe is module-private and states presentation-only semantics, so it fits neither key. So one describe constant was declared, for the two new keys only. No existing describe was touched.src:ListViewSchema,ObjectListViewSchemaandDashboardSchemaeach answeredunrecognized_keys[requiredPermissions].AppSchemawith the same key parsed (the positive control). No alias table on either schema mapsrequiredPermissionsor the near-misses (permissions,requiredPermission,requiredpermission,capabilities). All four are refused as unknown keys, and this PR adds no alias.plannedis the true verdict.packages/rest/src/meta-item-read-gate.tsreadsrequiredPermissionsonly on an app and its navigation entries (filterAppForUserWithReason,:740,:757). The dashboard arms gate widgets onrequiresServiceonly.packages/runtimereads it for actions and apps.packages/lintvalidate-capability-references.tsresolves objects, fields, apps and actions..objectui-shapin20c6d351ad74has zero reads underplugin-list,plugin-viewandplugin-dashboard. The lit control islayout/src/NavigationRenderer.tsx, with 7 hits.dashboards[].requiredPermissions(a top-level row), and an authoredviews[].list.requiredPermissions(one level ofchildren). A container's namedlistViews.NAMEentry is not walked (the lint flattens one level), nor is an object'slistViews. That is why the describe carries the not-enforced clause too. No schema default materializes the key, and "absent stays absent" is pinned.app.form.tsandaction.form.tsofferrequiredPermissionsbecause those gates are enforced. For a planned key the repo's pattern is no form control plus a reconciliation-ledgeromitrow, and this PR follows that pattern.dashboardis object-rooted, so the top-level zod-only direction requires the row: without it, that test reds.viewis union-rooted (TOP_LEVEL_DEFERRED). It gets the same row for symmetry.Verification (head
af81d59c30)Measured after the last commit.
git rev-parse --short HEAD=af81d59c30.Build. The dependency closure was built with
pnpm --workspace-concurrency=2 --filter '@objectstack/lint...' --filter '@objectstack/client-react...' build, spec included: exit 0. There was no whole-workspace build.Tests and typechecks, each through
os-verify-lock.sh:@objectstack/spec:vitest run --project localgave 643 files, 19184 passed (1 todo).typecheck(tsc --noEmit, the scripts typecheck,check:test-typecheck) exited 0: "52 file(s) / 246 error(s) / 135 pinned signature(s) held", which is the existing ledger, unchanged.@objectstack/lint:vitest rungave 134 files, 6135 passed.typecheckexited 0.audience-required-permissions.test.tsplusmetadata-form-zod-reconciliation.test.tsgave 89 passed. The lint real-ledger block gave 3 passed (-t 'audience gate').Consumer reverse check (one-shot). I used a probe file under
packages/lint/srcthat resolves@objectstack/spec/uithrough the rebuiltdist.d.ts:Dashboardand aListViewliteral carryingrequiredPermissionstype-check, exit 0;requiredPermissionis refused withTS2561, exit 2. So the consumer reads the rebuilt declarations.The probe was deleted, and
git statusis clean.Generated artifacts.
check:generated: "All 15 generated artifacts are up to date".check:liveness: green. Its census reads "author warnings: 4 ledger row(s) opt intoauthorWarn… (planned 4); 0 on aliverow": the two existing rows plus these two.check:nul-bytes: OK.Derived gates.
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, with no paths, derived 111 commands ataf81d59c30(18 paths against merge base3d0eeefa4). Reconciled with--ranand an exit-annotated record: "111 derived famil(ies) accounted for — 109 run, 2 NOT-MEASURED".check:platform-checklistexited 1 with 7 problems. It is red onorigin/maintoo: a cleanorigin/main5fb1746611worktree gives the same 7. The citations indocs/qa/platform-checklist/areas/access-security.jsonandattachments-storage.jsonname symbols that leftpackages/metadata-protocol/src/protocol.tsandservice-storage. This diff touches none of those files.check:dual-build-cjs-loads(it needs a whole-workspace build) andcheck:type-check-debt(its--re-measurerunsturbo run buildover every package). The dispatch forbids both. CI runs them.Artifact rosters. The 48 roster commands that need no PR context: 47 exited 0, and
check:published-readme-exportsexited 3 with PREREQUISITE NOT MET: 17 packages whose type entries it reads are not built. That is NOT MEASURED; this diff edits no README and no export of those packages. The 3 PR-context guards (check-closing-target-claim.mjs,check-partof-closing-keyword.mjs,check-single-claim-paths.mjs) run against this PR once it exists; their results are in the report comment.Not measured locally, declared to CI. These are the type-check lanes (
turbo run typecheckover every package, the examples, anddownstream-contract) and the path-scheduled CI jobs. They are additive optional keys: a grep for exhaustivekeyof ListView/keyof Dashboardtyping overpackages,appsandexamplesfinds none.Ablation (one-shot, not kept)
Committed state
f449ed3aac. The mutation went throughnode scripts/ablation-replace.mjs --delete -- …, deleting theDashboardSchemadeclaration line. The anchor hit once (x1 to x0), and the blob moved from6e7bebcde6c2tof99584c01a42. The subject resolves through relative imports tosrc, so no build or dist preflight applies.vitest run src/ui/audience-required-permissions.test.ts src/system/metadata-form-zod-reconciliation.test.tsgave 6 failed / 83 passed:DashboardSchemadoor pins;omitrow no longer resolves).ListViewSchema/ObjectListViewSchema/ container pins stayed green, as expected.6e7bebcde6c2, andgit diff HEADis empty (the tool's own verdict, re-read by hand).Acceptance notes
requiredPermissionson the list read and the by-name read, so each audience is served only its own views and boards (the rest half of #22611) #22639 (the object door).requiredPermissionsreaches an object's ownlistViewsthroughObjectListViewSchema. Those entries are served inside the object definition, not as/meta/viewitems: no server package expandsobjects[].listViews. rest(/meta read gate): enforce a list view's and a dashboard'srequiredPermissionson the list read and the by-name read, so each audience is served only its own views and boards (the rest half of #22611) #22639 names the/meta/viewand dashboard reads only. The same question is in the report'sopen_questions.requiredPermissionson the list read and the by-name read, so each audience is served only its own views and boards (the rest half of #22611) #22639 (the flip). One edit flips both liveness rows tolive, dropsauthorWarn/authorHint, and deletes the describe's[PLANNED — not enforced yet]clause. The two lint warn pins here turn red at that flip on purpose; re-subject them to the silence.validateCapabilityReferences(packages/lint) resolvesrequiredPermissionson objects, fields, apps and actions, not on a list view or dashboard. Once the gate is enforced, a misspelled capability there would hide the item from everyone without a warning. Carrier: rest(/meta read gate): enforce a list view's and a dashboard'srequiredPermissionson the list read and the by-name read, so each audience is served only its own views and boards (the rest half of #22611) #22639, or the seat. Nothing reads the key today, so nothing fails yet.sharingas it is).ViewSharingSchema's docblock reads "Defines who can see and modify a view", and its describe reads "View sharing and access configuration". The ruling recordssharingas ownership and locking, not audience. An author could read the wording as an audience gate.check:type-check-debtandcheck:dual-build-cjs-loadsare NOT MEASURED locally: the first one's--re-measurerunsturbo run buildover every package, and the second needs a whole-workspace build. The dispatch forbids both. During the first gate pass that turbo build clobberedpackages/spec/distmid-run, so I killed it (my own PIDs). Every gate whose run overlapped the rebuild window was re-run after a clean closure build. CI runs both.Generated by Claude Code