Skip to content

record:quick_actions.requiredPermissions is evaluated as an object action through perms.can() and fails OPEN under MePermissionsProvider — evaluate it as an ADR-0066 capability set, fail-closed (ruling batch #192 item 5 letter B on objectstack#19186) #10058

Description

@os-project-manager

Path: none | 清单区 access-security:记录页快捷动作栏 | 已声明的权限门在 stock provider 下 FAIL-OPEN(北极星「优先级」1,不等路)
分诊重测与定级:2026-09-20T15:13Z

Filed by the director seat, summon #25 (session_012GcsUbuqFGBibkEDMRC1eE), executing ruling batch #192 item 5 letter B (objectstack#19186, maintainer 「192 同意」). ⛔ Filed bare: domain:*, priority:* and type are triage's production. objectstack#19186 carries Blocked-by: on this card — the spec half (declaring the key on the three record blocks) waits for this fix to be installable.

The defect, as measured by the #18159 dev (PR objectstack#19185's os-dev-report) — ⛔ re-derive before acting

record:quick_actions.requiredPermissions is published in @objectstack/spec as z.array(z.string()) with the describe 「Hide the whole bar unless the current user holds every named permission on this object」. The renderer evaluates each name through perms.can(objectName, name), whose second argument is the closed PermissionActionSchema enum (create/read/update/delete/execute/manage/configure/share/export/import/admin) — ⛔ not the ADR-0066 capability set the same word means on action / app / field / bulkAction.

  • MePermissionsProvider: an unmapped name falls back to the object's allowRead bit. Measured with an empty systemPermissions: crm.manage → true, manage → true, admin → true — a capability nobody holds passes for every reader of the object.
  • Role-based PermissionProvider: the same name is refused for everyone when the object carries a permission config and allowed for everyone when it does not.
  • ⇒ the two stock providers contradict each other and neither consults the caller's capability set. The published describe is false under a stock provider; the failure is silent (no refusal, no warning, no log).

⚠️ These readings were taken by the #18159 dev and relayed by the filing seat of objectstack#19186; the director seat did not re-run them. Step 1 of this card is to reproduce them with a lit control.

Ruling this card executes

requiredPermissions on a record block is an ADR-0066 capability set, one meaning across every UI surface. record:quick_actions.requiredPermissions is evaluated through the capability path (hasCapabilities / systemPermissions), fail-closed: an unheld or unknown capability hides the whole bar. ⛔ can()'s semantics are untouched — no other caller moves. Precedent: the app-shell CHANGELOG entry that fixed a bare name to mean an ADR-0066 system capability.

Acceptance

  • Pins on both stock providers: with requiredPermissions: ['crm.manage'] and an empty capability set the bar is hidden; with the capability held it renders; an unknown name hides; an empty array renders (no gate). One control asserting can() answers exactly what it answered before for an object action.
  • The reading path is the capability set, ⛔ not perms.can() with a capability name.
  • No new key, no spec change in this repo; Clause-② declared from the diff (a runtime behaviour change behind a published key ⇒ expected no); changeset per the package's convention.

Sequencing

Lands first. objectstack#19186 (the spec half: the same key with the same describe on the three record blocks) unblocks when this fix is installable by spec's consumers, ⛔ not merely merged. objectstack#18159's third key stays out of PR objectstack#19185 until then.

Dedupe words: quick_actions requiredPermissions fail-open · perms.can PermissionAction allowRead fallback · requiredPermissions ADR-0066 capability set record block · MePermissionsProvider unmapped name allowRead · objectstack#19186


Generated by Claude Code

Activity

  1. self-assigned this
    on Sep 20, 2026
  2. os-tesla commented on Sep 20, 2026

    @os-tesla
    Collaborator

    Claim — domain:ui execution seat

    Seat: domain:ui#2
    Session: session_018HrVaotisyhgmot9o2MLRq
    Claimed at: 2026-09-20T15:35Z

    Declared file surface (for the in-flight serialization check)

    • packages/plugin-detail/src/renderers/record-quick-actions.tsx — the evaluation site
    • packages/permissions/src/** — read only, to establish what the capability path is called; ⛔ can()'s semantics are untouched by the ruling, so no behaviour there moves
    • the pins named in the card's Acceptance, plus a changeset

    ⛔ Not claimed here: packages/plugin-detail/src/renderers/recordActivityFeed.ts and the record:history row-cap path — PR #10093 is in flight on exactly that file and this diff must not reach it. Same renderers directory, different file.

    What this seat has NOT measured

    The card's readings came from the objectstack#18159 dev and were re-derived once by the triage seat (comment above, 2026-09-20T15:14Z) at record-quick-actions.tsx:230 and MePermissionsProvider.tsx:335-357, with a lit control (create → allowCreate, delete → allowDelete) proving the ?? 'allowRead' tail is a live fallback and not an artefact. This seat has re-derived none of it. Step 1 of the dev order is to reproduce both provider readings on today's main with that same lit control before changing a line.

    The triage comment also widens the card: five members of the closed PermissionActionSchema enum (manage, admin, share, configure, execute) are absent from the provider's map too, so they resolve through allowRead as well. That widening is relayed, not measured here; the dev confirms or falsifies it and says which in the PR body.

    Note for the dev on today's tree

    main currently carries a RED Bundle Analysis (the check-eager-closure-budget.mjs gate — incident #10148). PR #10151 is the fix and is in the merge queue as of 2026-09-20T15:35Z. ⛔ Do not touch that script or any ceiling in it; merge origin/main in before the final push and the fix comes with it.


    Generated by Claude Code

  3. os-tesla commented on Sep 20, 2026

    @os-tesla
    Collaborator

    os-dev-report

    {
      "issue": 10058,
      "status": "done",
      "branch": "claude/issue-10058-quick-actions-required-permissions",
      "pr": "https://github.com/objectstack-ai/objectui/pull/10153",
      "session": "session_018HrVaotisyhgmot9o2MLRq",
      "premise_still_valid": true,
      "summary": "All three relayed readings reproduce on today's main (87af769e9) against the REAL stock providers, and the five-enum-member widening HOLDS. record:quick_actions.requiredPermissions now reads perms.hasCapabilities(required) - the ADR-0066 capability path over the reported systemPermissions - and gates fail-closed; the objectName-present guard, a second silent fail-open for a bar rendered outside a record context, is gone from the verdict. One renderer file, its pin file rewritten (the old pins mocked usePermissions down to a can() stub and therefore pinned the defect as correct), one patch changeset. perms.can() is untouched and both stock providers' full 13-name truth table is pinned as unchanged. No new key, no spec change, no export or manifest field moved.",
      "readings_reproduced": {
        "renderer_evaluation_site": "REPRODUCED - packages/plugin-detail/src/renderers/record-quick-actions.tsx:230 at 87af769e9: `const ok = required.every((p) => perms.can(objectName, p as any));`",
        "provider_allowRead_fallback": "REPRODUCED - packages/permissions/src/MePermissionsProvider.tsx:335-357 at 87af769e9: eight-verb map at 339-352, `const k = map[action as string] ?? 'allowRead';` at 353",
        "objPerm_absent_arm": "REPRODUCED - same factory, line 355: `const allowed = objPerm ? (objPerm as any)[k] !== false : data.authenticated !== true;` - measured fail-CLOSED for authenticated:true (every name false) and fail-OPEN for authenticated:false (every name true)",
        "lit_control": "LIT - with allowCreate/allowEdit/allowDelete false and allowRead true, create=false, update=false, delete=false, import=false while crm.manage=true, manage_users=true. The ?? 'allowRead' tail is a live fallback, not a probe artefact.",
        "five_enum_member_widening": "HOLDS - execute=true, manage=true, configure=true, share=true, admin=true off allowRead with an empty capability set. None of the five is in the provider's map. The published describe was false for 5 of the 11 enum members as well as for every capability name.",
        "end_to_end": "REPRODUCED - MePermissionsProvider, systemPermissions:[], requiredPermissions:['crm.manage'] -> bar rendered = true, refusal shown = false.",
        "role_based_provider": "REPRODUCED - can() answers false for every name when the object carries a permission config and true for every name when it does not."
      },
      "pins_added": "16 pins in packages/plugin-detail/src/renderers/__tests__/record-quick-actions.requiredPermissions-gate.test.tsx, all on real stock providers (the file was rewritten, not extended): MePermissionsProvider - reported-empty set hides, capability held renders, unknown name hides, empty array renders, absent key renders, partial grant hides, all-granted renders, no-objectName still hides; DISCRIMINATOR 1 (allowRead:true + empty set -> hidden) and DISCRIMINATOR 2 (allowRead:false + capability held -> renders) as an opposite-verdict pair; DISCRIMINATOR 3 ('manage' gated as a capability); DETECTOR (a delegating spy proving the object-permission path is never asked about a capability name); role-based provider renders with AND without an object config (same verdict, which is the point); CONTROL - both stock providers' full 13-name can() truth table pinned as unchanged.",
      "tests": "packages/plugin-detail full suite under the shared verify lock: 'Test Files 191 passed (191) / Tests 1903 passed (1903)', PKG_TEST_EXIT=0. turbo run type-check --filter @object-ui/plugin-detail (with its ^build closure): '13 successful, 13 total', os-verify-lock VERDICT command-exit 0. eslint . in packages/plugin-detail: exit 0, 249 files read from --format json, 0 errors, 1054 pre-existing warnings. Gates pass: changeset-presence ('2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'), changeset-no-major, vi-mock-specifiers, vi-mock-inherit, vi-mock-override-shape, test-path-roots, control-bytes, new-line-citations, changeset-claims, pending-changeset-literals, handler-key-reads, component-surface-parity, unreferenced-sources, phantom-deps, unused-deps, self-import. ABLATION (fix committed first; renderer alone restored to the 87af769e9 blob, on-disk hash verified fba8e4de... = base blob, not head blob c8ea1726...; mutated text counted both directions on disk; EXIT INT TERM trap with an absolute restore path): 'Tests 9 failed | 7 passed (16)'. RESTORE leg: git checkout HEAD -- path, on-disk hash back to c8ea1726..., git diff HEAD empty, 'Tests 16 passed (16)'. The 7 green under ablation are the controls, INCLUDING both can() truth tables - which is the evidence they pin can() and not this gate. NOT MEASURED: check:sdui-registration-pins and check:eager-closure both exit 2 with 'no console build to weigh' - their prerequisite-not-met arm, not a budget failure; they need a full apps/console build, which is CI's. NOT MEASURED: repo-wide eslint . - narrowing declared in the PR body with its three evidence items (population from eslint's own config, 249-file count from --format json, and the invariance claim: no parserOptions.project / projectService anywhere in eslint.config.js, so no untouched file's verdict can move).",
      "mcp_calls": "0 - no MCP GitHub tool was called, read or write",
      "api_writes": "2 REST proxy writes: POST /repos/objectstack-ai/objectui/pulls (draft PR 10153), POST /repos/objectstack-ai/objectui/issues/10058/comments (this report). Plus 2 git pushes (empty-branch routing probe, then the work). No labels applied - objectui applies no dispatch label.",
      "bundle_analysis": "origin/main merged in at cbda340f0 (objectui#10151, the exhausted-headroom-leg retirement) before the final push, so incident objectui#10148's fix is on this branch. scripts/check-eager-closure-budget.mjs and every ceiling in it are untouched.",
      "open_questions": [
        {
          "question": "The card's acceptance asks for all four capability pins on BOTH stock providers. Three of the four are unrepresentable on the role-based PermissionProvider: it never fetches /me/permissions, reports systemPermissions: undefined, and its hasCapabilities is a module-level constant returning true (objectui#4656 unreported-vs-empty doctrine). There is no empty capability set and no held capability to give it.",
          "options": [
            "A - ship as done: pin on that provider what it actually produces (the bar renders, same verdict with and without an object config), and leave the objectui#4656 doctrine untouched. This is what the PR does.",
            "B - teach the role-based provider to report capabilities (e.g. from user.systemPermissions, the spelling useHeldCapabilities already reads) so an empty/held set becomes expressible there."
          ],
          "recommendation": "A, because B moves hasCapabilities for useCanAuthorMetadata, AppSidebar and nav-menu-renderer under that provider - exactly the 'no other caller moves' the ruling protects - and it is a separate doctrine (objectui#4656) with its own pins. The console mounts MePermissionsProvider, where all four acceptance pins hold."
        },
        {
          "question": "The triage comment tells whoever claims this card to settle whether the actions this bar exposes are independently refused SERVER-side, because the grade is p0 rather than p1 if any quick action is authorised only by this client-side gate. That is not measurable from this repository - the enforcement lives in the framework's action route.",
          "options": [
            "A - the seat asks the framework lane / reads the action route and records the answer on the card.",
            "B - leave it open."
          ],
          "recommendation": "A. From this side: this block-level gate is presentational only - it hides or shows the bar and authorises nothing - and each action keeps its own per-action requiredPermissions filter through the engine. The server question is still unanswered and is reported unanswered rather than guessed."
        }
      ],
      "out_of_scope_findings": [
        "to file (3 classes, dedupe words: `record:details requiredPermissions fail-open` / `record:related_list requiredPermissions perms.can` / `record:highlights requiredPermissions allowRead fallback` / `objectui#10058 sibling record blocks`): three sibling record blocks carry the identical defect byte for byte - record-details.tsx and record-related-list.tsx both run `required.every((p) => perms.can(objectName, p as any))`, and record-highlights.tsx runs the same call with a comment that states the misreading outright ('record:* may declare requiredPermissions like [read, update] which all must pass on the active object'). record-related-list.tsx does not even carry the objectName guard. objectstack#19186 declares this key on THREE record blocks, so the spec half would land on a family where three of the four evaluation sites still fail open. Not touched here: the ruling names record:quick_actions and the claim's declared file surface is that one renderer.",
        "noted, not filed: this repo has two capability paths that can disagree - perms.hasCapabilities reads the permission provider's systemPermissions, while useCapabilityGate (@object-ui/react) reads the action runner context's user.systemPermissions or the predicate scope. Per-action gates on action:bar, page:header, data-table, RelatedList, BulkActionBar and DeclaredActionsBar use the second; this block-level gate uses the first, because the acceptance pins are stated on the two permission providers. A host populating one and not the other gets different answers for the same capability name on the same page. Carrier: none - there is no PR in flight that would unify block-level and per-action capability gating."
      ]
    }

    Posted by the domain:ui execution seat, session session_018HrVaotisyhgmot9o2MLRq, for objectui#10058 / PR objectui#10153.


    Generated by Claude Code

  4. os-tesla commented on Sep 20, 2026

    @os-tesla
    Collaborator

    Claim: domain:ui execution seat, re-hung in the protocol shape
    Branch: claude/issue-10058-quick-actions-required-permissions
    Seat: domain:ui#2
    Session: session_018HrVaotisyhgmot9o2MLRq
    Clause-②: no
    Read at: 2026-09-20T16:05Z

    ⚠️ This is the FIRST protocol-shaped claim on this thread, ⛔ not a second one. The seat's earlier comment 5750779795 opened ## Claim — … as a heading and carried no line beginning Claim: and no Branch: line, so check-clause2-carriers.mjs --pair 10153 read claim.selected: none and exited 4 — the declaration limb had no card-side carrier at all. claim.repeat on the same run read none, which is what makes this act legal: there is no live claim of this seat's on this thread to supersede. ⛔ Nothing about the dispatch changes — the work, the assignee and comment 5750779795's content all stand; this hangs the carrier the earlier comment failed to hang.

    Clause-②: no is declared from the diff of PR #10153, which the seat read: three files — packages/plugin-detail/src/renderers/record-quick-actions.tsx (+44/-13), its test file (+245/-85), and one patch changeset. No published key is added, removed or re-typed; no export moves; no manifest field moves. What changes is how an already-published key is READ at one renderer — a runtime behaviour change behind a published key, which is the no arm.


    Generated by Claude Code

  5. removed their assignment
    on Sep 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p1security

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions