Skip to content

fix(app-shell): the permission matrix honors allowRuntimeCreate — and the read-only banner tells the truth (#4446) - #4519

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4446-permission-matrix-writable
Aug 13, 2026
Merged

fix(app-shell): the permission matrix honors allowRuntimeCreate — and the read-only banner tells the truth (#4446)#4519
yinlianghui merged 1 commit into
mainfrom
claude/issue-4446-permission-matrix-writable

Conversation

@yinlianghui

@yinlianghui yinlianghui commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Closes #4446

Phase 1 — pin provenance: DESCRIPTION, not decision

The card warned that two suites pin the current behaviour on purpose and that a decision there would make this a ruling request rather than a patch. Traced both, plus the upstream thread. Verdict: they describe the code, and the one on-record ruling in the area points the same way this PR goes.

PermissionMatrixEditor.readonly.test.tsx:153 — introduced by PR #2570 (57a61b097, 2026-07-16). That PR's own change was purely additive: it added the host gate (&& !readOnly) so the Studio Access pillar could lock a read-only package. It inherited writable = allowOrgOverride and described it — "内部 writable = allowOrgOverridetype 级(环境 OS_METADATA_WRITABLE)概念" — which is precisely the mis-attribution this card is about. Its case 4 pins that the two gates keep separate wording ("两个闸门语义分离"), not that the type gate must be strict. No ruling.

PermissionMatrixEditor.readonlyHeaderBadge.test.tsx:17-19 — introduced by PR #4341 (f046f885a), the B-half of objectstack#5768 / #4036. Its header does carry decision language ("a fix that made anything editable would be the wrong fix"), but it is scoped to the ruling it quotes — Studio 维持包级只读, the package-level gate. Measured: every case in that suite drives the host gate (readOnly true/absent) with allowOrgOverride: true throughout. It contains no allowOrgOverride: false case, so it never renders the shape it describes in prose and does not pin the type-gate formula.

The upstream ruling. objectstack#5768's 裁决 of 2026-08-06 (maintainer-authorized, quoted verbatim and untranslated):

A2:Studio 维持包级只读;收紧的是服务端——15 类 allowOrgOverride 对照 ADR-0005 修正案白名单逐类复核,尤其 permission/flow/tool/skill 四类(ADR-0005 2026-05-22 修正案明文禁止其 per-org override,permission 的理由是 silent privilege drift——注册表现状与 Accepted ADR 冲突,必须归一,收紧或改 ADR 二选一在复核单里定);「可写」徽标随复核结果对齐。A1(Studio 放开 overlay 编辑)等真实定制拉动再议。

That server-side tightening has since landed — objectstack#6483, 2026-08-08 maintainer ruling — and it deliberately closed one door while keeping the other open. From the permission entry's own rationale in packages/spec/src/kernel/metadata-plugin.zod.ts:

Runtime-created sets — including package-bound rows MATERIALIZED through the metadata door, whose provenance is sys_metadata, not an artifact — ride allowRuntimeCreate (still true) and keep working; a data-door edit of a CODE-DECLARED (artifact-backed) set now refuses with 403

So there is no ruling that permission must gate stricter than the server. The ruling closed the overlay door and preserved the runtime-create door; this card asks the editor to consult exactly the door that was preserved. A1 (opening overlay editing) stays untouched and still deferred.

Phase 2 — the fix

1. The switch reads the disjunction. allowOrgOverride is permission to OVERLAY a code-shipped item per org; allowRuntimeCreate is permission to AUTHOR at runtime — and authoring is what this editor's Save does under a packageId (mode: 'draft' + packageId, ADR-0086 P0/P2, PermissionMatrixEditor.tsx:567). The server's own gate is that same disjunction, refusing only when both are false (!isOverlayAllowed && !isRuntimeCreateAllowed, in saveMetaItem and promoteDraftForPublish). permission sits exactly in the gap: allowOrgOverride: false, allowRuntimeCreate: true.

Read off the raw server entry, as DirectoryPage:171/175, EmbeddedItemEditor:93 and ResourceEditPage:1332 all do, and as useMetadata.ts documents on the field itself ("UI affordances … should activate when either flag is true"). Note the card's suggested spelling resolved.allowRuntimeCreate could not have worked: resolveResourceConfig forwards allowOrgOverride only, so that read would have been silently undefined. resolveResourceConfig is now unused here and its import is dropped.

The host gate is unchanged and still dominant, so Studio 维持包级只读 holds exactly as before.

2. The badge/controls divergence, resolved rather than half-migrated. PageShell's WritabilityBadge is read-only exactly when readOnly || (!allowOrgOverride && !allowRuntimeCreate); the controls were read-only when readOnly || !allowOrgOverride. The gap is precisely !allowOrgOverride && allowRuntimeCreate — today's permission, where the screen showed a "create-only" badge above 207 dead checkboxes. The badge already consulted the full disjunction; the controls were the outlier. Fixing the controls makes the two predicates byte-identical, which a new four-state table pins so neither side can drift again. PageShell itself is untouched — passing !writable into it would have collapsed the inputs but made a TYPE-gate lock claim the PACKAGE as its reason (that branch's tooltip is engine.studio.pkg.readonlyHint), trading the divergence for a fresh lie.

3. Banner honesty, and the measurement behind it. The old caption blamed a deployment env var for a per-type registry declaration — and it had no reachable honest case at all. OS_METADATA_WRITABLE does not sit beside allowOrgOverride, it flips it: getMetaTypes emits allowOrgOverride: base.allowOrgOverride || isEnvOverridden. So whenever the hatch is on for a type, that type is writable and this badge never renders. There is therefore no state to keep the old wording for. It now names the per-type declaration that actually locked the surface and keeps the env var as the documented remedy, in the hint — the same place the server's own 403 text puts it. New rows go through the metadata-admin defaults maps (EN + ZH), the channel this surface uses.

Red-first

Predicted directions were written down before the revert; the run matched exactly — 3 red, 13 green. Reverting only the two source files to origin/main and keeping the tests:

× runtime-creatable-only type in a writable package → writable, and both renderings agree
  AssertionError: expected null not to be null
× no write channel at all (both flags false) hides Save and names the TYPE as the reason
  AssertionError: expected [ div element ] to be null
× stock-boot permission shape (allowRuntimeCreate only) is EDITABLE — the server accepts this write
  TestingLibraryElementError: Unable to find an accessible element with the role "button" and name `/^Save$/`
Tests  3 failed | 13 passed (16)

(The second line reads expected + the rendered div element + to be null in the real output. The element is transcribed as [ div element ] here because GitHub's body sanitizer strips an angle bracket followed by a letter as an HTML tag at rest, and ate it verbatim on the first save of this description.)

The misleading caption, captured verbatim from that run:

Read-only (OS_METADATA_WRITABLE not enabled)

The 13 that stay green are green on purpose, and include the three must-not-change pins the ruling named: both flags false stays read-only (with the honest caption), the readOnly prop still wins over allowRuntimeCreate: true, and an allowOrgOverride package is byte-identical to before.

Verification

  • Build closure first: pnpm --filter '@object-ui/app-shell^...' build → exit 0.
  • pnpm exec vitest run --maxWorkers=2 packages/app-shell/src/views/metadata-admin/145 files / 1521 passed, 1 skipped.
  • pnpm exec vitest run --maxWorkers=2 packages/app-shell/src/views/studio-design/20 files / 127 passed (the Access pillar hosts this editor).
  • Both tsc projects: tsc --noEmit → exit 0; tsc -p tsconfig.test.json → exit 0. (This package has no tsconfig.typetests.json; its second project is tsconfig.test.json.)
  • eslint on the four changed files → 0 errors, 24 warnings, all pre-existing: the identical count is produced by the same command on origin/main.
  • Gates: check-control-bytes OK (4226 files), check:i18n-keys OK, check:i18n-drift OK (0 en values changed — that gate scopes to packages/i18n/src/locales, and these rows live in the app-shell defaults maps).

.d.ts measured both ways: built @object-ui/app-shell with the fix and again with the two source files reverted, hashing every emitted .d.ts. Identical — 37dfb635b78d3db7941734cf6b4f23d6a9fbb7623f1eacfaadb33919f2dbe6ae both times. No public type surface change, so the changeset is patch.

Out of scope, filed separately

#4518PermissionMatrixEditor models only the server's type tier, not its artifact tier (ResourceEditPage:1332 models both). On a multi-environment kernel an env-scope edit of a code-declared permission set can now offer Save and get a 403. Not folded in here: the ruling scoped this card to the type-tier disjunction and to trusting the entry's flags without a new probe, and adding artifact awareness would have re-locked the card's own headline case, which the QA run measured returning 200.


Generated by Claude Code

… the read-only banner tells the truth (#4446)

The writability switch read `allowOrgOverride` alone, so a writable package
rendered read-only while the server accepted the package-door write. It now
reads the disjunction `allowOrgOverride || allowRuntimeCreate` off the raw
server entry — the repo's own convention and the server's own predicate.

The package-level `readOnly` gate is untouched and still dominant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 13, 2026 3:28am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 24.7 KB 350 KB
Entry file index-Ago7fnOR.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 9.56KB 3.59KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 8.92KB 3.41KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 25.13KB 5.40KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 38.46KB 10.17KB
auth (createAuthenticatedFetch.js) 6.34KB 2.43KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.02KB 0.88KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 489.32KB 108.45KB
core (index.js) 3.37KB 1.34KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 153.79KB 41.35KB
fields (index.js) 230.14KB 57.12KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 3.35KB 1.38KB
i18n (pickLocalized.js) 3.69KB 1.73KB
i18n (provider.js) 23.12KB 7.62KB
i18n (useDisplayLocale.js) 2.84KB 1.45KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.98KB 10.85KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.86KB 12.91KB
plugin-charts (index.js) 62.07KB 17.65KB
plugin-chatbot (index.js) 181.21KB 43.14KB
plugin-dashboard (index.js) 120.95KB 31.53KB
plugin-designer (index.js) 212.58KB 42.83KB
plugin-detail (index.js) 239.88KB 59.99KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 114.58KB 27.68KB
plugin-gantt (index.js) 164.14KB 39.98KB
plugin-grid (index.js) 188.40KB 50.10KB
plugin-kanban (index.js) 48.62KB 13.42KB
plugin-list (index.js) 111.13KB 27.12KB
plugin-map (index.js) 18.16KB 5.81KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 41.16KB 10.96KB
plugin-timeline (index.js) 26.21KB 7.52KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.08KB 20.55KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 23.73KB 7.96KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.23KB 0.66KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 3.05KB 1.52KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants