Skip to content

feat(app-shell): say why a capability-gated action is missing, in the action designer - #9567

Merged
os-justin merged 2 commits into
mainfrom
claude/issue-7234-requiredpermissions-author-channel
Sep 15, 2026
Merged

os-justin merged 2 commits into
mainfrom
claude/issue-7234-requiredpermissions-author-channel

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #7234

Ruling B from decision batch #81 (2026-09-08), landed: a declared action whose requiredPermissions the viewer lacks stays invisible to end users, and the reason becomes visible where the person who configures the app looks.

The card's premise was already falsified, and stays falsified

The card says the relay drops objectDef.actions. It does not — PR #8123 measured the relay carrying it, the button drawn, and ADR-0066 D4's capability gate doing the hiding (differential F vs G: identical payload, identical code path, only the held capability set differs). Nothing here re-investigates that, and nothing here changes the gate.

Which existing channel, and why this one

The ruling leaves the channel to the implementer, requiring only that it already exist and add no end-user surface. Measured on origin/main this fire:

candidate what it actually shows who actually looks verdict
preview/capabilityLint.ts that a referenced capability string is registered nowhere — a typo, over PENDING DRAFTS whoever clicks Publish, once, as a transient toast (usePublishAllDrafts passes the warnings to toast) ⛔ wrong question, wrong audience. The reporting admin installed a published package: no pending drafts, no Publish click, no toast. And a toast cannot be where someone looks for a standing reason
metadata-admin/inspectors/ObjectDefaultInspector.tsx the object's access.default posture and the object's own requiredPermissions object designers ⛔ right family, wrong subject — this card is action.requiredPermissions
the action designerinspectors/ActionDefaultInspector.tsx + previews/ActionPreview.tsx the action's Placement and Where it appears exactly the person the console docs send to the Studio Data tab's Actions panel to configure actions[] ✅ chosen

Two things settled it. The inspector's Placement section already carries the sibling notice for the other way a declared action never surfaces (empty locations, from #3142) — so "here is why your button will not appear" is an established job of this panel, not a new one. And ActionPreview's Where it appears frames drew the button in every declared location unconditionally; its own PlacementPreview note rules against exactly that shape, naming the ADR-0078 "declares, 'renders', reports success, and does nothing" harm as the reason the retired global_nav frame was removed. A capability-gated action was the surviving instance of it.

What changed

  • Inspector, Placement section. Names the gating capabilities and says the action is hidden, not disabled or errored. When the signed-in session is itself missing one of them it says so in the first person — which is what answers the reported complaint, "I configured the buttons and I see none of them." The held set comes from MePermissionsProvider through usePermissions, the same signal useCanAuthorMetadata consumes; this is not a second client-side permission derivation. An unreported held set is unknown, and that clause stays silent rather than guessing — mirroring the gate's own fail-open doctrine.
  • Action preview. The capability line in the metadata strip and above the Where it appears frames. Declaration-side only: it renders a draft, not a session (pinned by case H).
  • content/docs/guide/console.md. States the hide and where the reason is shown.

Ruling part (a) is pinned, not promised

No end-user surface is added, and option C (drawing the action greyed out with the missing capability named) is not implemented. Case J, added to #8123's existing nine-case pin, asserts the gated end-user list surface still explains nothing: no capability name, no notice standing in for the button, paired with a positive that the list itself rendered. An implementer who lands this reason on a running app's list surface turns it red.

Where ruling part (c) landed, and where it did not

action.requiredPermissions's authoritative prose lives in @objectstack/spec — a different repository and the domain:spec seat's lane, fenced out of this one. It was read, not edited, and it owes nothing:

  • "State the hide" is already there. The requiredPermissions member of the spec's action schema describes itself as "Enforced with 403 on the platform action route (script/flow/modal + MCP) and mirrored as a UI hide".
  • "Where the reason is shown" cannot go there. The channel is an objectui surface. The spec is renderer-agnostic and objectui is one consumer of it; a spec description naming this repo's action designer would couple the protocol to one renderer.

⇒ part (c) is an objectui obligation, and content/docs/guide/console.md discharges it. ⛔ No upstream card is owed. ⛔ Neither packages/types/src/objectql.ts nor its Zod twin was touched — the sibling card #7804 holds both.

Clause-② — the declaration was conservative, and the route came in under it

needs:contract-review is hung on this PR as required. Evidence for a re-grade at review, ⛔ not a request to strip it: this route adds no new exported symbol and no new diagnostic code. git diff adds no export to either changed source file; the only new import is usePermissions, already a published export of @object-ui/permissions and already a declared dependency of @object-ui/app-shell. The change is copy inside two panels that already exist, plus tests and docs.

Gates — each exit code captured to a file before reading

gate exit
pnpm exec vitest run packages/app-shell/ (707 files, 6948 passed, 1 skipped, 0 failed) 0
packages/app-shell type-check (tsc --noEmit and tsc -p tsconfig.test.json) 0
eslint . over the whole packages/app-shell package: 0 errors, pre-existing warnings only 0
check:changeset-claims · check-changeset-presence · check-changeset-no-major 0
check:control-bytes · check:phantom-deps · check:test-path-roots 0
check:i18n-keys · check:i18n-drift · check:i18n-designer-parity · check:designer-field-key-parity 0
check:vi-mock-inherit · check:vi-mock-override-shape · check:vi-mock-specifiers 0
check:new-line-citations · check:doc-fences 0
check-governed-queue-guard --test over all six changed paths 0 — NOT GOVERNED

The lint reading is a declared narrowing: the repo-wide pnpm lint is turbo run lint across every package and belongs to CI. Run here instead: packages/app-shell's own eslint ., whose population is the file list eslint enumerated from its own config rather than a guess of mine, with the count read from --format json. The narrowing is sound because type-aware linting is not enabled anywhere in the flat config — no projectService, no parserOptions.project, no project: — so this diff cannot move a verdict on a file it does not touch.

Reverse verification — direction predicted before each run, three legs

Every leg mutated from the committed state, proved the mutation reached disk by blob hash before running anything, restored with git checkout HEAD -- PATH, and verified git diff HEAD empty afterwards. No build or dist step is involved: both mutations and both pins live inside packages/app-shell and the tests import them by relative path, so nothing resolves through a package's exports.

leg predicted measured
inspector notice deleted A, C, D, E red; B (control) and the three preview cases green 4 failed (A, C, D, E) / 4 passed
both preview notices deleted F and H red only 2 failed (F, H) / 6 passed
an end-user permission notice injected into ObjectView's list toolbar J red only 1 failed (J) / 9 passed

The second leg is the one worth reading twice: before this branch hardened case H it compared two reads of the notice, and with the notice deleted both are undefined, so it passed for a deleted renderer. Case J had the same shape and now asserts a positive over the same render. Both hardenings are in their own commit.

The third leg is the pin that matters to the ruling: it is the only leg that fires on the failure this card must not produce, and the only one whose mutation is an addition rather than a deletion.

Out of scope, reported rather than fixed

  • DeclaredActionsBar applies no capability gate. It renders objectDef.actions[] for a record at a location, filtering on locations and the visible CEL only — no useCapabilityGate, unlike action:bar, page:header, the grid row menu and the selection bar. The same divergence on BulkActionBar was treated as a defect when it was found. Not touched here: out of this card's surface, and it needs its own measurement of whether that bar is reachable with gated actions.
  • The client gate may be stricter than the server for a platform admin (useCapabilityGate reads only user.systemPermissions and ignores the isPlatformAdmin that useConsoleActionRuntime passes alongside it). Explicitly ⛔ not this card; unmeasurable from this repository.

Session: session_01KSd9P5u2Mf4p8g4n4SD4Fx.


🤖 Generated with Claude Code

https://claude.ai/code/session_01KSd9P5u2Mf4p8g4n4SD4Fx


Generated by Claude Code

… action designer

`action.requiredPermissions` (ADR-0066 D4) is mirrored as a UI hide, and the
mirror is silent by design: a viewer who does not hold every listed capability
gets no button, no greyed-out control and no message, at every declared
location at once. objectui#7234 was opened by an admin who had configured a set
of object-bound buttons, held no permission set yet, opened the list, saw
nothing, and read it as a broken feature — nothing in the product said
otherwise.

Maintainer ruling 2026-09-08, option B: the hide STAYS for end users and the
reason becomes visible in an author/admin channel that already exists. Option C
(greying the action out with the missing capability named) was rejected because
it advertises to end users capabilities they do not have.

End-user behaviour is unchanged, and that is pinned rather than promised: case J
in ObjectView.objectBoundActions-7234.test.tsx asserts the gated end-user list
surface still explains nothing.

The channel is the action designer, the panel pair the Studio Data tab's Actions
config already uses:

- the inspector's Placement section, beside the existing empty-`locations`
  notice, names the gating capabilities and says the action is hidden rather
  than disabled — and when the signed-in session is itself missing one, says so
  in the first person, reading the held set from MePermissionsProvider through
  usePermissions (the signal useCanAuthorMetadata already consumes, not a second
  client-side permission derivation). An unreported held set is unknown, and
  that clause stays silent, mirroring the gate's own fail-open doctrine;
- the action preview carries the capability line in its metadata strip and above
  its "Where it appears" frames, which previously drew the button in every
  declared location without qualification.

content/docs/guide/console.md states the hide and where the reason is shown.

Part of #7234

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSd9P5u2Mf4p8g4n4SD4Fx
… can pass vacuously

`H` compared two reads of the preview's capability notice; with the notice
deleted both are `undefined` and the comparison passed for a deleted renderer.
`J` asserted only absences, which an empty tree supplies. Each now asserts one
positive over the same render first — the asymmetry the ObjectView pin's own
ablation note already states for its control cases.

Also satisfies `MetadataDefaultInspectorProps` in full (`readOnly`, `locale`)
so the package's `tsc -p tsconfig.test.json` pass type-checks the new file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSd9P5u2Mf4p8g4n4SD4Fx
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3063.5 KB 3104.5 KB
Main entry chunk (gzip) 145.7 KB 350 KB
Entry file index-Bil7dIJ5.js
Status PASS

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
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.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 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.68KB 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) 544.93KB 130.50KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 215.98KB 59.97KB
fields (index.js) 249.20KB 62.88KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.83KB 10.95KB
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.75KB
mobile (index.js) 1.99KB 0.87KB
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.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.99KB 14.24KB
plugin-charts (index.js) 71.33KB 19.90KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.44KB 34.65KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 252.65KB 65.65KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.71KB 34.16KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 212.83KB 57.97KB
plugin-kanban (index.js) 46.63KB 14.53KB
plugin-list (index.js) 112.67KB 27.68KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 84.89KB 20.96KB
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.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 99.04KB 32.62KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
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 (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.04KB 5.36KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
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

Copy link
Copy Markdown
Collaborator Author

Contract review — head fc7b3eb5c3866e080fd130225d81f7c24df25d7aPASS

In-seat clause-② review by the dispatching domain:ui seat, default judgement tier plus the gates, per this lane's recorded tier ruling (5612097546, objectstack#17285: the lane's default-tier review is the review of record for this seat; the contract-review-tier fuse binds only the spec and skills seats). Judged from the PR's actual diff, ⛔ not from the claim and ⛔ not from the report's account of itself.

The card-side declaration was Clause-②: yes, written conservatively at claim time because the ruling left the channel unchosen and a route adding a published diagnostic code or exported symbol would have widened a public surface. This review judges the route that was actually taken.

① Derived judgments — does the diff widen an accept set or a public surface?

judgment reading on this head
new exported symbol in either changed source file 0 — measured over the added lines of ActionDefaultInspector.tsx (+55) and ActionPreview.tsx (+37): zero added lines contain export
new diagnostic code / rule / finding shape none — no new code constant, no new rule id, no new entry point
new import reaching outside the published dependency set one import added, usePermissions from @object-ui/permissions — an already-published export and an already-declared dependency of @object-ui/app-shell (check:phantom-deps exit 0)
new authorable key, or a key newly accepted nonerequiredPermissions is read, never declared or accepted here; no schema, zod mirror or .d.ts shape moves
packages/spec/** touched no — read only, confirmed against the changed-file list

⇒ the change is copy inside two panels that already exist, plus tests, one doc page and one changeset. Nothing is widened. The conservative yes was correct to declare at claim time and the delivered route came in under it.

② Semver

@object-ui/app-shell: minor, single changeset, check:changeset-no-major exit 0 (this repo forbids major; a break would ship minor with the break spelled out). Minor is right: the author-facing panels gain behaviour, nothing is removed, and no consumer contract narrows. ⛔ Not patch, because a panel that now renders a capability notice is new behaviour an author can see.

③ Boundary flags

  • Runtime permission / security behaviour: UNCHANGED, and this is the flag that matters most here. ⇒ ⛔ not the manual-floor security/permission class. The capability gate is untouched at every gate site; the report enumerates five object-bound gate sites and none is edited.
  • End-user surface: UNCHANGED, and pinned rather than promised. Case J asserts the gated end-user tree contains neither the declared capability string nor any match for /capabilit/i, /requiredPermissions/i, /permission/i, over the whole container — with a paired positive that the list surface itself rendered, so an empty tree cannot satisfy it. Ablation leg 3 injects the leak and fires exactly that case (1 failed / 9 passed).
  • Governed surface: NOT GOVERNED. All 6 changed paths checked by this seat against the five governed surfaces (docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md) — no match, so this PR takes the merge-queue route and ⛔ not the maintainer four-piece route.
  • Upstream obligation: NONE, verified at source. The report claims ruling part (c)'s "state the hide" half is already correct upstream. ⭐ I verified the exact sentence rather than the claim: objectstack:packages/spec/src/ui/action.zod.ts:1412 declares requiredPermissions as "[ADR-0066 D4] Capabilities required to invoke this action. Enforced with 403 on the platform action route (script/flow/modal + MCP) and mirrored as a UI hide…". ⇒ ⛔ no domain:spec card is owed, and the objectui half is the whole remaining obligation.

Independence pair

Implemented-by:  claude/issue-7234-requiredpermissions-author-channel
Reviewed-by:     session_01KSd9P5u2Mf4p8g4n4SD4Fx

⚠️ Read the pair as this post's §0a prescribes: a mode:subagent dev has no session of its own, so the implementer identity is the BRANCH. The session field inside the dev's own JSON echoes the dispatching session for that reason and is ⛔ not a self-review tell. The reviewing identity is the seat.

Verdict — PASS

CI on this exact head: 36 checks, 33 success, 3 skipped, 0 non-green; Lint and Type Check read individually and both success, each pinned to head.sha (⛔ not an aggregate reading, ⛔ not the dev's local union). mergeable_state: clean. ⇒ both carriers are stripped by this seat in one stroke, citing this record.

domain:ui execution seat · session_01KSd9P5u2Mf4p8g4n4SD4Fx (os-justin) · reviewed at 2026-09-15T15:17Z


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: fc7b3eb5c3866e080fd130225d81f7c24df25d7a

⚠️ Supersedes my record 5682848795 on this same head, which was refused by check-clause2-carriers --pair 9567 at exit 4: it carried no Served-tier: line and put the head sha in the heading instead of a span of its own, so it was not 同形 and 「无此行不成裁决」. ⛔ Nothing about the findings changed — only the shape. The guard was right and I post the correction rather than widening the row.

⚠️ Recorded for the next seat, because this lane's seat post §0a lists the 同形 requirements without Served-tier: — that list predates the charter move. The authoritative shape is references/contract-review.md:29 plus --template, and per :51 the 降档保险丝 binds only the spec seat and the skills seat, so this lane reviewing at its default tier (maintainer ruling 5612097546, objectstack#17285) is correct and unchanged; the Served-tier: token is the constant's NAME the gate compares against, ⛔ never a model identifier, which AGENTS.md forbids in any artifact.

① Derived judgments

Judged from the PR's actual diff, ⛔ not from the claim and ⛔ not from the report's self-account. The card-side declaration was Clause-②: yes, written conservatively at claim time because the ruling left the channel unchosen and a route adding a published diagnostic code or an exported symbol would have widened a public surface.

judgment reading on this head
new exported symbol in either changed source file 0 — measured over added lines of ActionDefaultInspector.tsx (+55) and ActionPreview.tsx (+37): zero added lines contain export
new diagnostic code / rule / finding shape none
new import outside the published dependency set one, usePermissions from @object-ui/permissions — already a published export and an already-declared dependency (check:phantom-deps exit 0)
new authorable key, or a key newly accepted nonerequiredPermissions is read, never declared or accepted; no schema, zod mirror or .d.ts shape moves
packages/spec/** touched no — read only

⇒ copy inside two panels that already exist, plus tests, one doc page, one changeset. Nothing is widened. The conservative yes was right to declare and the delivered route came in under it.

② Semver level

@object-ui/app-shell: minor, one changeset, check:changeset-no-major exit 0 (this repo forbids major). Minor is right: the author-facing panels gain behaviour, nothing is removed, no consumer contract narrows. ⛔ Not patch — a panel that now renders a capability notice is new behaviour an author can see.

③ Boundary flags

  • Runtime permission / security behaviour: UNCHANGED ⇒ ⛔ not the manual-floor security class. The gate is untouched at every site; the report enumerates five object-bound gate sites and none is edited.
  • End-user surface: UNCHANGED, pinned rather than promised. Case J asserts the gated end-user tree contains neither the declared capability string nor any match for /capabilit/i, /requiredPermissions/i, /permission/i, over the whole container, with a paired positive that the list surface rendered — so an empty tree cannot satisfy it. Ablation leg 3 injects the leak and fires exactly that case (1 failed / 9 passed).
  • Governed surface: NOT GOVERNED — all 6 changed paths checked by this seat against the five governed surfaces; no match ⇒ merge-queue route, ⛔ not the maintainer four-piece route.
  • Upstream obligation: NONE, verified at source, ⛔ not from the report. objectstack:packages/spec/src/ui/action.zod.ts:1412 declares requiredPermissions as "[ADR-0066 D4] Capabilities required to invoke this action. Enforced with 403 on the platform action route (script/flow/modal + MCP) and mirrored as a UI hide; …". ⇒ ruling part (c)'s "state the hide" half is already correct upstream and ⛔ no domain:spec card is owed.
  • dev flags and open_questions: open_questions empty; the three out_of_scope_findings are answered below the record — one class (b) to file by this seat, two correctly noted-not-filed.

Implemented-by: claude/issue-7234-requiredpermissions-author-channel
Reviewed-by: session_01KSd9P5u2Mf4p8g4n4SD4Fx

⚠️ Read the pair as contract-review.md prescribes: a mode:subagent dev records its BRANCH, having no session of its own, so the session field echoed inside the dev's own JSON is the dispatching session by construction and is ⛔ not a SELF-REVIEW tell.

CI on this exact head: 36 checks — 33 success, 3 skipped, 0 non-green; Lint and Type Check read individually, both success, each pinned to head.sha. mergeable_state: clean.

VERDICT: PASS

domain:ui execution seat · session_01KSd9P5u2Mf4p8g4n4SD4Fx (os-justin) · reviewed at 2026-09-15T15:19Z


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Provenance for the carrier clear — objectui#7234 / PR objectui#9567

Both needs:contract-review carriers (PR-side and card-side) were stripped by this seat, the dispatching domain:ui seat session_01KSd9P5u2Mf4p8g4n4SD4Fx (os-justin).

  • Record of record: comment 5682884903 on this PR — ## Contract review, VERDICT: PASS.
  • Head judged: fc7b3eb5c3866e080fd130225d81f7c24df25d7a, which is this PR's current head.
  • Machine check: PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 9567exit 0 (exit code captured to a file before reading, ⛔ not after a pipe).

⚠️ Disclosed rather than buried: I stripped the two carriers before running --pair, which is the wrong order — the check then returned exit 4 against my first record 5682848795, because it carried no Served-tier: line and put the head sha in the heading rather than in a span of its own. The fix was to post a 同形 record (5682884903), ⛔ not to widen the row or to re-order the evidence. The carriers stayed stripped throughout; the guard is report-only and writes no labels, and its own text says the newest heading comment on the head governs. ⇒ recorded so the sequence is auditable and so the next seat runs --pair before the strip, not after.


Generated by Claude Code

@os-justin
os-justin marked this pull request as ready for review September 15, 2026 15:21
@os-justin
os-justin added this pull request to the merge queue Sep 15, 2026
Merged via the queue into main with commit 45889f8 Sep 15, 2026
38 checks passed
@os-justin
os-justin deleted the claude/issue-7234-requiredpermissions-author-channel branch September 15, 2026 15:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants