Repository navigation
feat(plugin-security,platform-objects): grant forms pick a position or permission set by name from the catalog (#15204 B2) - #22939
Conversation
…r permission set by name from the catalog (#15204 B2) The user page's related-list add pickers listed sys_position / sys_permission_set rows, which a deployment need not hold (ADR-0131 D3/D4). Removed; New opens the grant form, whose name field declares widget position-ref / permission-ref. The legacy permission_set_id lookup is hidden from the forms. Remove is the host's row delete, measured independent of add. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XKtmoR5QCUSTMjiQ3os49
…e grant lists' add labels left (#15204 B2) Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XKtmoR5QCUSTMjiQ3os49
📓 Docs Drift CheckThis PR changes 3 package(s): 11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 10 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 41 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 c6d4c0b294bba6e9748fc057aa9341c1ca3f46dc && git checkout c6d4c0b294bba6e9748fc057aa9341c1ca3f46dc
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 51b43a8dc45fef9d86229509ab6b2679ffa9d501 7de0f00c91aaaecd2d70ba59695d373baa290bf1 && git checkout -B drift-repro 51b43a8dc45fef9d86229509ab6b2679ffa9d501 && git merge --no-ff 7de0f00c91aaaecd2d70ba59695d373baa290bf1
node scripts/docs-audit/affected-docs.mjs --json 51b43a8dc45fef9d86229509ab6b2679ffa9d501
|
…5: position-ref / permission-ref), and re-measure the spec's pin citations Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XKtmoR5QCUSTMjiQ3os49
This reverts commit e486abb. .objectui-sha is #22874's single-writer path, and objectui from e531116 on reads allowEnvironmentOverlay only, which main's server sends once #22874 lands. The pin move returns as its own PR after #22874. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XKtmoR5QCUSTMjiQ3os49
Seat ACCEPT: #22939 (#22865 B2 phase 2: grant name pickers on the user page) at
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 38208298792 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
|
Seat note: dequeued on one suite-duration ceiling, in a package this PR does not touch; re-queued once · epic PM What red: queue build 38208298792, Why it is not this PR's:
No fix exists yet. The seat spends this PR's one re-run as one re-queue on the unchanged head Generated by Claude Code |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 38208631030 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
|
Seat note: re-queued behind W2, whose queue group carries #22949's fix · epic PM This PR left the queue on the suite-duration ceiling step (
Re-queued on the unchanged head Generated by Claude Code |
…e anchors (objectui#12200) (objectstack-ai#12219) Fixes objectstack-ai#12200 Clause-②: no The `position-ref` grant picker (`@object-ui/fields`, `RegistryRefField`) stops offering the `everyone` and `guest` audience anchors. A grant cannot name one: every authenticated or anonymous principal holds an anchor implicitly (ADR-0090 D5/D9), and the server refuses a `sys_user_position` row that names one. The on-record measurement is objectstack-ai/objectstack#22967's browser check (comment 6120504736): 16 positions listed, `everyone` among them. ## What changes - `position-ref` leaves out exactly the spec's `AUDIENCE_ANCHOR_POSITIONS`, read from `@objectstack/spec` and never copied, so an anchor the spec adds is left out with no change here. - A row that already stores an anchor still shows it, by its label beside its name, as its own option. It is not flagged "not found", because the registry does list it. When the row holds another value, no anchor is offered. Read-only shows the same label. - A registry that lists nothing but anchors gives the typed-name box, the LOADED-but-empty arm, now judged on what a grant can name. - `permission-ref` is unchanged: a permission set named like an anchor is listed and written as before. - There is no new prop, widget option, type member or i18n string. There is one `@object-ui/fields` patch changeset. ## H1: the mark, measured - `@objectstack/spec` exports `AUDIENCE_ANCHOR_POSITIONS` from both its root entry and `./identity`. The declaration is `declare const AUDIENCE_ANCHOR_POSITIONS: readonly ["everyone", "guest"];`. - `@object-ui/fields` declares `^17.6.0`; the lockfile installs **17.7.0**. The 17.7.0 `dist/identity/index.d.ts` re-exports it (`b as AUDIENCE_ANCHOR_POSITIONS`). A runtime import of `@objectstack/spec/identity` answers `["everyone","guest"]`. - The range floor, the **17.6.0** registry tarball, exports it from `dist/index.d.mts` and `dist/identity/index.d.ts` too. - The registry marks no item itself. `PositionSchema` has no anchor key, and the meta read gives `{ name, label }` headers. The spec constant IS the mark, so no name is hard-coded here and no server-side companion is owed. - On the server, the grant refusal is `plugin-security`'s `delegated-admin-gate.ts` invariant ("audience anchor is implicit — it cannot be assigned via sys_user_position"), on objectstack `main` and on objectstack-ai/objectstack#22939's head. That gate keeps its own `new Set(['everyone', 'guest'])` and does not import the constant (see Acceptance notes). The catalog-activation gate in `packages/runtime` does import it. ## H2: which import, measured three ways `RegistryRefField.tsx` IS in the console's eager closure. The console's chunk grouping puts `packages/fields` into the eager `ui-components` chunk (the gate's chunk-membership line), and the widget's own string `fields.registryRef.loadFailed` sits in `ui-components-*.js`, which `eager-closure.json` lists. `@objectstack/spec/identity` is a lazy chunk of its own (`identity-*.js`, not in the eager list). | route | eager closure, gzip bytes vs base | `ui-components` vs its ceiling | `check:eager-closure` | `check:spec-floors`, `@object-ui/fields` | |---|---|---|---|---| | base `9646e2e` | | 281.5 KB / 282.2 KB | exit 0 | not built | | static `import … from '@objectstack/spec/identity'` (counterfactual, restored) | +6953 | 288.3 KB, over by 6.1 KB | exit 1 | not run | | lazy `import('@objectstack/spec/identity')` (commit `8bd8a14`) | +174 | 281.7 KB | exit 0 | `opaque-subpath` finding, exit 1 | | static import from the root `@objectstack/spec` (this PR, `aa16a9c`) | −5 (noise) | 281.6 KB | exit 0 | judged clean (308 pairs judged, against 307 for the lazy build) | Why `check:spec-floors` decides it: `changeset:publish` runs `scripts/check-spec-range-floors.mjs` before `changeset publish`, and so does the nightly `spec-range-floors.yml`. The gate reads symbol names from static imports only. A dynamic `import()` leaves the subpath unjudged, and the gate reports that as a finding, so the lazy route would have reddened the publish lane. So the PR takes a third route that the dispatch's static-or-lazy framing did not name. It imports statically from the root entry, as `@object-ui/auth` imports `BUILTIN_MEMBERSHIP_ROLES`. It keeps the suggested shape: a `Set` built from `AUDIENCE_ANCHOR_POSITIONS`, keyed on `registryType === 'position'`, one small helper (`nonGrantableNames`) beside the widget. The anchor set is known on the first render, so no catalog arm changes shape. Commit `aa16a9c` says why the lazy commit was replaced. ## H3: every `position-ref` field - objectstack `main` `6a40a09ed`: none. `git grep -c position-ref` answers 0; the control, `widget: '…-ref'` on the same tree, finds two `object-ref` declarations. - objectstack-ai/objectstack#22939 head `7de0f00c9`: exactly one, **`sys_user_position.position`** (`plugin-security`, `sys-user-position.object.ts`), beside `permission-ref` on `sys_user_permission_set.permission_set`. The server refuses an anchor there, so no field declares `position-ref` where an anchor is a valid value. - objectui: no object field declares it; only the docs examples do. ## Tests, at `aa16a9c` - New `RegistryRefField.audienceAnchors-12200.test.tsx` has 5 pins: the anchors are left out, an anchors-only registry gives the text box, a stored `everyone` shows unflagged as its own option (H4), a read-only stored anchor shows its label, and `permission-ref` lists and writes a set named `everyone` (H5). - `pnpm exec vitest run` over the six files that render the widget, run as one command: `Test Files 6 passed (6)`, `Tests 311 passed (311)`. The files are the two `RegistryRefField` suites, `widget-aria-invalid-registry-e2e`, `widget-dom-leak-e2e`, `plugin-form`'s `objectFormRegistryNameWidget-7611` and `components`' `form-data-source-wiring`. - `pnpm --filter @object-ui/fields type-check` exits 0. `tsconfig.test.json` lists the new test file. - Reverse verification ran on the committed fix. Each mutation went through `ablation-replace.mjs`, and each restore was proven by blob == HEAD with an empty `git diff HEAD`: - the filter off: 3 failed / 12 passed; - the stored anchor's own option off: 1 failed / 14 passed; - a partial list (`[AUDIENCE_ANCHOR_POSITIONS[0]]`): 3 failed / 12 passed. - Gates, run after the last commit: `check:eager-closure` 0, `check:phantom-deps` 0, `check:vi-mock-specifiers` 0, `check:vi-mock-inherit` 0, `check:vi-mock-override-shape` 0, `check:spec-symbols` 0, `check:esm-specifiers` 0, `check-changeset-presence.mjs` 0, `check:changeset-claims` 0, `check:pending-changeset-literals` 0, `check:control-bytes` 0, `check:new-line-citations` 0. - `check:spec-floors` exits 1 locally, and every one of its 15 findings is `no-artifact` for a package this tree did not build. `@object-ui/fields` was built and has no finding. A Node ESM import of the built `@object-ui/fields` dist loads. - ESLint over the two changed sources: 2 files, 0 errors, 2 `no-explicit-any` warnings (test casts, as in the 7611 suite). Type-aware linting is not configured, so the result for untouched files cannot move. ## Acceptance notes - **Docs.** `content/docs/guide/fields.md` and `packages/fields/README.md` describe `position-ref` as listing "the `position` registry" and do not mention the exclusion. They are outside this claim's file surface; one sentence in each would cover it. - **The server keeps a second copy of the list.** `delegated-admin-gate.ts`'s `ANCHOR_POSITIONS` is a literal `['everyone', 'guest']`, not the spec constant. The values agree today. Noted, not filed. - **`check:spec-floors` reads no dynamic `import()`.** A symbol taken that way is never judged against the floor. app-shell's lazy `/identity` imports (`audienceAnchorsOf`, `clientValidation`) pass only because `FlowReferenceField.tsx` imports that subpath statically. Noted, not filed. - **Headroom.** `ui-components` has 0.7 KB of per-chunk headroom before and after this PR. The next growth of `@object-ui/fields` meets that ceiling. - **Browser.** No re-check was run after this change; it needs objectstack-ai/objectstack#22939's server. Dev run for objectui#12200, seat `domain:ui#1`, session `https://claude.ai/code/session_016djJF12Qt14ejKR5Vjt4sK`. --- _Generated by [Claude Code](https://claude.ai/code/session_016djJF12Qt14ejKR5Vjt4sK)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Part of #15204
Clause-②: no
B2, objectstack half (objectui#7611 names it). Setup's user page grants a position or a permission set by picking its NAME from the security catalog, instead of an
addpicker that listedsys_position/sys_permission_setrows (ADR-0131 D3/D4). This PR is one of the pair that gates #22900 (the boot stops writingsys_permission_setrows): once #22900 lands, a fresh database holds no such row, and the old pickers would offer nothing.What changes
sys-user.page.tsPositions / Permission Sets listsaddpickers oversys_position/sys_permission_setrows, beside Newaddblocks are deleted. New opens the grant form. Remove is the host's row delete (RelatedRecordActionsBridge), unchanged. The Business Units list keeps itsaddsys_user_position.positiontext)widget: 'position-ref': the form listsGET /api/v1/meta/positionand stores the chosen namesys_user_permission_set.permission_setwidget: 'permission-ref': the form listsGET /api/v1/meta/permissionand stores the chosen namesys_user_permission_set.permission_set_id("Permission Set (legacy id)")hidden: true, so it is off the create and edit forms (maintainer, 2026-10-12, 「legacy id 字段隐藏,同意」). The column stays until ADR-0131 C7b drops its dataUntil the console pin moves, the two fields render as text inputs (the type renderer: a text box for the name).
position-ref/permission-refship in objectui#12175 (PR-5, merged as088e0f705520), and this repo's.objectui-shastill pins4997995022f8. An unregistered widget falls back to thetextrenderer, so nothing breaks. The pin follows #22874:.objectui-shais that PR's single-writer path, and objectui frome5311165on readsallowEnvironmentOverlayonly, whichmain's server sends once #22874 lands. A follow-up PR then moves the pin to088e0f705(or later), re-measuring only that hop.mainis unreleased, so no released build shows the gap.This branch briefly carried that pin move (
e486abb9c), reverted by7de0f00c9. The net diff is phase 2 only: 9 files, tree-identical tod4c22c8fe.Measured before the build (phase 1, #15204 6116275825)
On a booted showcase, as the admin:
addblocks deleted, every row still offers Delete, and a delete was exercised end to end (UI 4 → 3 rows; the API shows the row gone).The renderer's
onRowDeleteprefers the host'sonDelete; theadd-based remove is only its fallback when no host bridge is mounted.Pins
packages/qa/dogfood/test/grant-name-pickers.dogfood.test.tsreads the SERVEDGET /meta/object/<name>on a booted showcase:positioncarriesposition-ref;permission_setcarriespermission-ref;permission_set_idishidden, withpermission_setnot hidden as the control.hiddenfrom the objects, then rebuilding, turned all 3 cases red (expected undefined to be 'position-ref', …); restoring turned them green.sys-user-permission-set-window-columns.test.tsasserts that neither grant list carriesadd, with the Business Units list'saddas the control. Ablation: re-adding anaddto the Permission Sets list turned that case red.format.server-ready-dev-admin-audience.test.ts: the CLI banner's "grant it a permission set" pin is re-anchored on the Permission Sets list (its New), since the "Grant permission set" label is gone.platform-page-i18n-parity.test.ts: the anti-vacuity floor of inline locale maps is 44 (was 45), because the twoaddlabels went with their blocks.Browser check against the widget (objectui#12175 head
8237a1cd886d, built locally before it merged; the merge088e0f705520carries the same widget files)registry-ref-position/registry-ref-permission), listing 16 positions and 17 permission sets.auditorandmcp_agent_data_read, then pressing Create, wrote both grant rows (verified through the data API).Verification (local, at this head)
platform-page-i18n-parity27/27);dispatch-gates --ran: 72 derived families, all recorded with exit 0.Changeset
@objectstack/platform-objectsand@objectstack/plugin-securitypatch,Clause-②: no. This changes shipped metadata only; no key anyone authors moves.Acceptance notes
position-refpicker lists theeveryoneaudience anchor among positions, and the server refuses it as a grant target. Not filtered here.🤖 Generated with Claude Code
https://claude.ai/code/session_013XKtmoR5QCUSTMjiQ3os49