Repository navigation
feat(app-shell,fields,console): Setup's positions and permission sets read the registry, through the metadata-admin pages' environment scope (part of objectui#7611) - #12089
Conversation
…in list and editors in their environment scope (objectui#7611)
ADR-0131 D3/D7: positions and permission sets live in the environment
registry, and Setup lists that registry. Built by reuse: the metadata-admin
list and editors gain an environment scope (`?scope=environment`) instead of a
new page family.
- the list lists every registry item of the type, re-gated for Setup: create
only for `manage_metadata` holders, a posture-aware read-only reason for
everyone else, an active switch and a status filter behind one row-state
seam (`catalog-activation.ts`) pending the activation ledger;
- the editors are read-only with a reason for a caller without
`manage_metadata`; in the environment scope the permission-set editor shows
the set's holders and the position editor shows the position's holders;
- the set's holders are read by name: grants by the `permission_set` column,
distributing positions from the registry's `permissionSets`;
- the recipient picker lists the `position` registry;
- the console's `system/{roles,positions,permissions}` land on the catalog.
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rerax7QTjKMPCUZxQUtPFR
…r gate, the row-state seam and the holders sections (objectui#7611) Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rerax7QTjKMPCUZxQUtPFR
…, the locked item's New offer follows the caller gate, and the permission list drops a Source column the registry never fills (objectui#7611) Found driving the Setup catalog in a browser against objectstack main: - `position` renders through its designer preview, so the environment section is placed under the designer as a bounded strip; - the lock banner's "New" offer is shown only to a caller who may author; - the `permission` registration's `managedBy` column read "Custom" for every set, the platform's own included: the registry serves no such key. The list's own Source badge, read from `_packageId` / `_provenance`, stays. Adds the changeset for the Setup catalog work. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rerax7QTjKMPCUZxQUtPFR
…ir environment scope (objectui#7611) Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rerax7QTjKMPCUZxQUtPFR
|
changeset-claim-re-read
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: PR #12089 (draft by dispatch, ① Derived judgmentsEach public-surface or accept-set change the diff implies, and whether it is right.
Not implied by the diff, stated so nobody reads silence as a judgment: no ② Semver level
③ Boundary flagsEvery dev flag (the report's Deviations
NOT MEASURED, and what answers each
Landing order this record relies on (the PR is draft by dispatch; the epic PM moves it):
Implemented-by: VERDICT: PASS Generated by Claude Code |
…ctui#7611 stage-1 patch round) Claude-Session: https://claude.ai/code/session_01Rerax7QTjKMPCUZxQUtPFR Co-authored-by: Claude <noreply@anthropic.com>
…write sites this branch adds (objectui#7611) Merging main brought objectui#12082's write census, which counts every DataSource write call site in the console tree. This branch adds three: the position holders' add and remove (sys_user_position) and the catalog switch's row write (catalog-activation's writeCatalogActive). Each is entered as `unmapped`, the same entry the census gives the sibling permission-set assignment section (AssignedUsersSection): none reads a CRUD grant on its object. The switch is offered on the caller's manage_metadata gate. Claude-Session: https://claude.ai/code/session_01Rerax7QTjKMPCUZxQUtPFR Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…say what objectstack's ADR-0131 stage 1 changed (objectui#7611) Stage 1 (objectstack bfc15d27) moved the authorization resolver's deactivation read onto the activation ledger: it reads neither catalog row's `active` column. The catalog switch still writes that row flag (the ledger door for `permission` and `position` is objectstack#15204 stage 2c, not landed), so on a stage-1 framework the switch changes the flag and nothing else. catalog-activation.ts said the resolver still reads the row flag; it now says which half moved and which has not. The changeset and the app-shell README say the same, and the changeset says that a set lists no holders through a position on a framework from before stage 1. The capability exclusion's stated reason (the registry served the package-declared capabilities only) stopped holding when objectstack#22669 declared the platform's capabilities as metadata; catalog-scope.ts, the README and a test comment now name the remaining reason, the follow-up. No code path changes. Claude-Session: https://claude.ai/code/session_01Rerax7QTjKMPCUZxQUtPFR Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
|
os-dev-report Generated by Claude Code |
…ctui#7611 round 2) Claude-Session: https://claude.ai/code/session_01Rerax7QTjKMPCUZxQUtPFR Co-authored-by: Claude <noreply@anthropic.com>
…he activation ledger through its door (objectui#7611)
objectstack's ADR-0131 stage 2c landed the activation door for the
security catalog, POST /api/v1/security/_activation/:type/:name with
{ enabled }, which writes one sys_metadata_activation row and no catalog
row. The resolver reads that ledger and never a catalog row's own
`active` column, so the switch now does the same on both sides:
- The state shown is the ledger's, read through the data door's list
of sys_metadata_activation the way the resolver reads it (false or 0
is off, no row is active). The record and truncation readers are
Setup > Packaged automation's, imported. No catalog row is read, and
there is no fallback onto the row flag (objectstack#22601 B).
- The write goes through the door by item name. Its refusals reach the
page as the server's own sentence (actionErrorDetail).
- The audience anchors everyone and guest, which the door refuses in
both directions, render a disabled switch with the reason. The names
are the spec's AUDIENCE_ANCHOR_POSITIONS, imported lazily as
clientValidation.ts imports the same module, because the list page is
in the console's eager closure.
- An item with no catalog row (an environment-authored position) is
switchable now, so the dash-with-reason state and its i18n key go.
- capability keeps no switch (the door refuses the type with 400).
The write census loses the switch's data-door update site.
PositionHoldersSection's docblock no longer says a registry-only
position is refused: stage 2a judges the assignment by the catalog.
Claude-Session: https://claude.ai/code/session_01Rerax7QTjKMPCUZxQUtPFR
Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…ts state and offers no click (objectui#7611) Measured as an organization administrator (no manage_metadata) on the permission-set catalog: the switches are disabled, and the read-only reason says why, but each one still announced "Active — click to deactivate". That is the same offer-that-does-nothing shape the switch's label had on a stage-1 framework (finding F3 of round 1). A disabled switch for a caller who cannot author now reads "Active" or "Inactive", the status filter's own strings; an author's switch keeps the click wording, and an audience anchor keeps its reason. Claude-Session: https://claude.ai/code/session_01Rerax7QTjKMPCUZxQUtPFR Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
|
os-dev-report Generated by Claude Code |
Part of #7611
Clause-②: yes
noprovisionally and said it becomesyesif the public surface widens. It widens by one export:@object-ui/app-shellgainsENVIRONMENT_SCOPE_QUERY, a string constant. No accept set narrows. The changeset isminor.Draft by dispatch (objectui AGENTS.md §9 ②). The epic PM of objectstack#15194 moves this PR after objectstack#15204 stage 1 has merged and been measured. Several halves of the card need server or spec changes first; they are listed below with the gate each one waits on. Size: 1,935 changed lines (additions plus deletions, tests included), 25 files. One PR, under the 3,000-line budget.
What this PR does
The Setup catalog for positions and permission sets reads the registry. It reuses the metadata-admin list and editors in an environment scope (
?scope=environment) instead of adding a page family, as ruling 6094171670 asks ("Build it by reuse, not by new pages"):…/metadata/position?scope=environment,…/metadata/permission?scope=environment) shows every itemGET /api/v1/meta/TYPEserves, the platform's own sets included. The Studio list still shows one project package's slice. Every link the list emits keeps the scope, and so does the editor's breadcrumb.manage_metadatagets no create affordance and a read-only editor, and the page says why. Undersinglethe reason is that the platform administrator defines these items and the caller's organization assigns them. Under a wall the reason is that the operator defines them in Studio. Both editors (generic and permission matrix) now apply this caller gate wherever they render, because the metadata door refuses that caller's save in every scope. The lock banner's "New" offer follows the same gate.AssignedUsersSection) and the position editor shows the position's holders (PositionHoldersSection, new). Both read and write assignment rows by the item's name.AssignedUsersSectionreads by name. Grants come from thepermission_setname column. The positions that distribute a set come from the registry's position definitions (permissionSets, the shape on stage 1's branch). The section no longer readssys_position_permission_setorsys_positionrows.positionregistry through the console's metadata store, and no longer readssys_positionrows.system/roles,system/positionsandsystem/permissionsgo to the catalog list instead of the object pages.catalog-activation.ts, described next.The active switch is pending the server half
The PM's supplement routes the switch to the activation ledger. I measured whether the ledger's doors accept these types on objectstack
main(d85615dd). They do not. The ledger has exactly two write doors,POST /automation/:name/toggleandPOST /actions/_activation/:object/:action. Each fixes its ownmetadata_type:flowin service-automation'sflow-activation-store.ts, andactionin objectql'saction-activation.ts. So neither door acceptspermissionorposition. The authorization resolver also still reads the row flag (isRowActive), both onmainand on stage 1's branch.So the switch keeps today's behaviour: it writes the catalog row's
activecolumn through the data door. That is what the Setup object pages' Activate and Deactivate actions do.catalog-activation.tsis the only module on these pages that reads or writes a catalog row, and it is the one that changes when the server half lands. An item with no catalog row shows a dash and the reason. A position authored through the metadata door is one such item.Server gate it waits on: an activation door that accepts
metadata_typepermissionandposition, behind the samerefuseUngrantedActivationWritegate (runtime/src/domains/activation-gate.ts) the flow and action doors use. The resolver's consult point must also move from the row flag to the ledger.Measured against a running objectstack showcase (
maind85615dd, fresh database)GET /api/v1/meta/permissionsys_permission_setrows. No item carriesactive,managedByor_lock.GET /api/v1/meta/positionsys_positionrows. No item carriesactiveoris_default.GET /api/v1/meta/capabilitysys_capabilityrows. The nine platform capabilities (manage_metadata,manage_users,setup.accessand the others) exist only as rows.PUT /meta/permission/NAMEactive: true,managed_by: admin).PUT /meta/position/NAMEGET /meta/permission,/position,/capabilityPUT /meta/permission/…,/meta/position/…FORBIDDEN, "Saving a metadata item requires themanage_metadatacapability."PATCH sys_permission_setactivePOST sys_user_permission_setwith onlypermission_setVALIDATION_FAILED.permission_set_idis still required.VALIDATION_FAILED, "no position is named …": the assignment door looks the name up insys_positionrows.The organization-admin persona was the showcase's auditor demo user, promoted to org
admin(manage_org_users,setup.access,setup.write; nomanage_metadata).Browser verification (objectui at this branch's head, Vite on a private port, proxied to the showcase above)
Platform administrator,
single:system/permissionslands on/apps/setup/metadata/permission?scope=environment. The list has 18 rows; every link carries the scope, New is offered, and no read-only reason shows.GET /meta/permission/c9_ui_setanswers 200 and the row is projected. Edit: granting Read onshowcase_accountand saving answers{"showcase_account":{"allowRead":true}}. Deactivate and reactivate: the switch wrote the row toactive: false, then back totrue.system/positionslands on the position catalog (17 rows). A position created from New (draft, then Publish) is live in the registry. Theauditorpage lists its holders. Assign by name: adding the phone persona landed{"position":"auditor"}insys_user_position.Organization administrator: the list states the reason, offers no New, and its switches are disabled. The set editor has no Save and states the reason, and so does the position editor.
Requests the catalog made:
/meta/permission,/meta/position,/meta/*/NAME/layers, and the row-state seam's/data/sys_permission_setand/data/sys_positionwithselect=id,name,active. No request fetched a merged list. The matrix editor's capability picker still reads/data/sys_capability; see the gates below.NOT MEASURED: a walled posture (
group/isolated). The showcase runssingle, and the walled reason text is covered only by a unit test.What this PR cannot do yet: the server and spec halves, each with its gate
CapabilityMultiSelectField) still readsys_capability. The registry serves 2 of 11; moving now would hide the platform capabilities. Gate: registerPLATFORM_CAPABILITIESinto the registry the wayregisterBuiltinPositionsregisters the built-in positions. Stage 6b deletesbootstrapSystemCapabilities, so the registration has to land before or with it.permission_set_id. That is one row lookup,resolveGrantRowId, at add time. Gate: the grant door acceptspermission_setwithoutpermission_set_id.sys_user_position.positionreference check resolves the name in the registry, not insys_positionrows. Until then, a position created in Setup can be assigned only after a restart, when the declared-position seeder gives it a row.approverIdentity,useApproverDirectory,FlowReferenceField,flow-node-config) and the decision-output position picker (decisionOutputParams). These follow the spec:APPROVER_VALUE_BINDINGS.positionis{ source: 'record', object: 'sys_position', valueField: 'name' }, andDecisionOutputDefsays apositionoutput collectssys_positionrecord ids. Contract-first: the spec changes first, then these readers. Gate: a registry binding in@objectstack/spec, plus a name-valuedpositiondecision output.nav_positions,nav_permission_setsandnav_capabilities, alltype: 'object'in plugin-security) still opens the object pages until stage 8. The catalog URLs for stage 8 are/apps/setup/metadata/position?scope=environmentand/apps/setup/metadata/permission?scope=environment. The object pages' special cases stay until then, because those pages remain reachable: theRecordDetailViewassignments slot,recordDelete's reset copy,data-objectstack's facet-widget stamp andPermissionFacetLink. They become dead code at stage 8 and leave in a cleanup paired with it.clone_permission_setrow action, which stage 8 deletes. Rebuilding it on the metadata door means deciding which keys a clone carries.adminScopeis ruled out.isDefaultwould silently add the copy to the everyone baseline. That decision is raised in the report rather than made here.PositionSchema.permissionSetsbut no field inposition.form.ts, so the generic editor has nothing to render it with. This PR builds the read side against that shape (positionsDistributing); the editor is the spec form line plus a registry picker widget.Cloud: the
.objectui-shahold this PR needscloud consumes objectui
mainon framework56bf27affb(the v17 line). I read that commit's source. It has nosecurity-catalog.ts, nobuiltin-positions.tsregistration and no position write-through. Positions are seeded and declared into rows only (bootstrap-builtin-positions,bootstrap-declared-positions). SoGET /api/v1/meta/positionthere serves the declared positions only: no built-in position and no organization-created position. The recipient picker and the position catalog would show cloud's organizations fewer positions than they see today. Hold: cloud's.objectui-shamust not move past this PR's merge commit until cloud moves to v18 (after C7).objectui#7205
This PR does not close it. The Setup catalog pages are no longer
ObjectViews, so a column-header click there no longer persists an org-wide overlay. That was this re-route's share of the card. The persistence inObjectView's sort handler is untouched, and it is still live for every other object, so objectui#7205 remains open.objectstack#11753
That card answers 404 on REST and on the web, while its neighbours answer 200, so it has been destroyed. What survives is its record in objectstack: the CHANGELOG entry for
ActionParamSchema.carryOver(spec half, #11992) and ADR-0126 §7.1. objectui never renderedcarryOver. Its only producers are the fiveclone_permission_setfacet params, which stage 8 deletes with the action. A metadata-door clone copies the whole definition and collects only a name and a label, so it needs no carry-over param. Once stage 8 lands,carryOverhas no producer; that is reported for the PM.Reader census (37 files at base
023f00d4, tests, docs and Markdown excluded)AppContent.tsx(three routes),AssignedUsersSection.tsx,RecipientPickerField.tsx.CapabilityMultiSelectField.tsx(gate 2);approverIdentity.ts,useApproverDirectory.ts,decisionOutputParams.ts,FlowReferenceField.tsxandflow-node-config.ts(gate 5);permission-set-clone-dispatch.ts,PermissionMatrixEditor.tsx's clone path and its fouri18n.tsclone strings (gate 7).RecordDetailView.tsx,RecordPermissionAssignmentsRenderer.tsx,recordDelete.ts,data-objectstack/src/index.tsandPermissionFacetLink.tsx.ObjectView.tsx,capabilityLint.ts,fields/src/index.tsx,MetadataFieldsPage.tsx,InlineFieldInput.tsx,RelatedList.tsx,plugin-detail/src/index.tsx,record-related-list.tsx,ObjectForm.tsxandfield-types.ts, plus all ten locale files (11 hits, every one a comment).Verification
Measured at the branch head named in the report.
type-check, hyphenated):@object-ui/fields,@object-ui/app-shelland@object-ui/consoleall pass, each after building its dependency closure.ResourceListPage.environmentScope-76119/9;ResourceEditPage.setupCatalog-76115/5;PermissionMatrixEditor.setupCatalog-76115/5;PositionHoldersSection4/4;catalog-activation5/5;AssignedUsersSectionand its envelope test 9/9;RecipientPickerField11/11;AppContent.systemHubRoutes22/22.testsfield.HEADand proven by blob hash and an emptygit diff HEAD):canAuthorfrom the editor'scanWriteturned exactly the caller-gate pin red (1 failed, 4 passed).check:new-line-citations(0 new),check:control-bytes,check:i18n-keys,check:i18n-dead-keys,check:side-effects-array,check:unreferenced-sources,check:vi-mock-specifiers,check:vi-mock-inherit,check:vi-mock-override-shape,check:test-path-roots,check:doc-fences,check:doc-typesandcheck:docs-route-closure. The changeset gates pass too: presence (21 published source files, 1 changeset) and no-major.@object-ui/cliand@object-ui/plugin-ai, outside this diff's closure):check:readme-exports(the entries it could judge had 0 wrong-path and 0 fabricated) andcheck:doc-snippets. Also not run locally:pnpm lintand the eager-closure budget, which needs a console production build. Both run in CI.Acceptance notes
permissionlist registration dropped itsmanagedBy"Source" column. The registry serves no such key, so it read "Custom" for every set, the platform's own included. The list's own Source badge (Artifact or Runtime, from_packageIdand_provenance) remains.main. Onmainthe bindings are junction rows, which this section no longer reads.Generated by Claude Code