Skip to content

spec: BulkActionDefSchema 缺 requiredPermissions —— 渲染器已按它过滤,作者却写不出来(内联 update/delete 批量定义没有任何合法写法加能力门) #6257

Description

@baozhoutao

BulkActionDefSchema 是 .strict() 且没有 requiredPermissions,所以内联批量定义声明不了能力门:

bulkActionDefs: [
  { name: 'bulk_delete_plan', operation: 'delete', requiredPermissions: ['plan.delete'] },
]
Unrecognized key(s) on this bulk action definition: `requiredPermissions`.

而渲染侧早已在读这个键 —— objectui#3492 落地后 BulkActionBar 就是按它过滤的:

const permittedDefs = (actionDefs ?? []).filter(d => mayInvoke(d?.requiredPermissions));

也就是说这是「declared ≠ enforced」的镜像面:enforced ≠ declarable。运行时认这个键、按它隐藏按钮,spec 却不让作者写出来。

现状盘点(对着合并后的 main 实测,三条断言全过)

写法 能否拿到 requiredPermissions 机制
bulkActions: ['my_action'](命名式) ✅ resolveBulkActions 提升时转发动作上的声明(objectui#3492)
bulkActionDefs: [{ name, operation:'custom', execution:'aggregate' }],name 命中已声明动作 ✅ 继承 该形态会解析 objectDef.actions 并把命中动作合并进 def
bulkActionDefs: [{ …, requiredPermissions: [...] }] 直写 ❌ spec 拒绝 本单
bulkActionDefs: [{ operation:'update' | 'delete', patch }] ❌ 无处继承 数据面 def 不解析对象动作

前两行意味着大多数场景今天就有出路(也是 schema 自己在 label 描述里推荐的那条:「declare a real action and name it in bulkActions to get localization」)。

真正表达不出来的是最后一行:内联的 update / delete 数据面批量定义。它按设计就不该去引用一个对象动作(它不派发动作,它是数据面 mass mutation),于是没有任何合法写法能给「批量删除」加上能力门 —— 而这恰恰是最需要门的一类按钮。

方案

给 BulkActionDefSchema(packages/spec/src/ui/bulk-action.zod.ts)加:

requiredPermissions: z.array(z.string()).optional().describe(
  'ADR-0066 D4 capability gate, same semantics as `action.requiredPermissions`: ' +
  'an empty/absent declaration always passes, several are AND-ed, and a client that ' +
  'cannot resolve the caller’s capabilities fails OPEN (the server is the authority). ' +
  'A def PROMOTED from `bulkActions: [name]` inherits the action’s declaration instead — ' +
  'this key is for inline defs, notably the `update`/`delete` data-plane forms that ' +
  'dispatch no action and therefore have nothing to inherit from.'
),

消费侧无需改动 —— objectui 的 BulkActionDef 类型与 BulkActionBar 的过滤都已就位,加上 spec 声明即打通。

配套:examples/app-showcase 里补一条内联 delete + requiredPermissions 的标本(#6157 的动作显隐矩阵已经把命名式的四种规格钉住了,缺的正是这一格)。

验收

bulkActionDefs: [{ operation: 'delete', requiredPermissions: ['x'] }] 能通过 objectstack validate,且无权用户在批量条上看不到该按钮 —— 与列表工具栏 / 行内 kebab / 记录页头三面结论一致。

Activity

  1. self-assigned this
    on Aug 7, 2026
  2. claude commented on Aug 7, 2026

    @claude
    Contributor

    Triage: pm:queue + domain:spec.

    Landing site. packages/spec/src/ui/bulk-action.zod.ts ⇒ domain:spec, and under "shared contract surfaces have one owner" anything touching packages/spec belongs to that seat regardless of who needs it.

    Premise verified on origin/main@2598216. BulkActionDefSchema ends in }, { error: bulkActionDefUnknownKeyError }).strict() and its key set is name / label / icon / variant / operation / execution / patch / params / confirmText / confirmLabel / visible / maxRecords / batchSize / condition / style — no requiredPermissions. The consumer half is already shipped: objectui#3492 landed via objectui#3548 (d915c47, "批量按钮补上 requiredPermissions 与布尔 visible"). So this really is enforced ≠ declarable, and the fix is additive with no consumer change.

    Cross-repo dedup — a field-report shadow existed and has been converged. objectui#3564 reported the same gap from a running 17.0.0-rc.5 deployment. Per the convergence rule (one thing, exactly one dispatch entry) it has been closed as a duplicate of this issue, and its independent evidence is carried over here because it strengthens the case:

    • real-machine probe on a device list, admin account: a bulk button with requiredPermissions: ['<nonexistent capability>'] is filtered out, and admin is not exempt — same semantics as navigation-item requiredPermissions;
    • the documented workaround (declare an object action, reference it by name from bulkActionDefs) requires operation === 'custom' && execution === 'aggregate', after which operation is pinned to custom. So a declarative operation: 'update' + patch bulk button and a capability gate are mutually exclusive today — matching this issue's fourth table row;
    • reported user-visible impact: four declarative bulk buttons (reassign / push-down / transfer / bulk delete) visible to every user who can open the list, rejected per-record by a server hook only after the click.

    ⚠️ One process note, no action implied. This issue carried an assignee before any triage label existed, and has no claim comment. Flagging because "an unlabelled issue must not be claimed" is the lane protocol's only mechanical guard, and because a claim comment is what tells a later reader whose claim it is. This seat never assigns and has not touched the assignee.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  3. baozhoutao commented on Aug 7, 2026

    @baozhoutao
    ContributorAuthor

    认领。维护者(assignee)已直接委派本任务;此前实现分支已删除,从零重做。

    • 分支:claude/issue-6257-bulk-action-required-permissions
    • 会话:session_01PD7tZG1vENc5peMaLQC1uD

    范围按本单方案执行:BulkActionDefSchema 增加可选 requiredPermissions(含 did-you-mean 键池与生成 artifacts 同步),schema 测试,examples/app-showcase 补内联 update/delete + requiredPermissions 标本(与 #6157 矩阵的 showcase.restricted_ops / showcase.export_data 对齐),并在 showcase UI 真机 A/B 实测后附证据。


    Generated by Claude Code

  4. baozhoutao commented on Aug 7, 2026

    @baozhoutao
    ContributorAuthor

    已完成并开 PR:#6332(分支 claude/issue-6257-bulk-action-required-permissions,会话 session_01PD7tZG1vENc5peMaLQC1uD)。

    真机 UI 实测矩阵(console dev :5190 → showcase --fresh 后端,admin 勾选全部 5 行,同一用户同一批记录):

    步骤 授予 purge_restricted 声明 批量条
    baseline 仅平台能力 ['showcase.restricted_ops'] 4 个未加门按钮,两个带门按钮均不在
    授予 showcase_ops +showcase.export_data 同上 Relabel (Ops) 出现,Purge 仍不在
    声明翻转 同上 [] Purge (Restricted) 出现
    声明还原 同上 ['showcase.restricted_ops'] Purge 再次消失

    夹具(relabel_ops / purge_restricted 内联 def + e2e/bulk-capability-gate.spec.ts)永久保留在 showcase。spec 全量 8500 passed;showcase validate/typecheck/146 tests 全绿;check:generated 10/10。

    过程中记录到一个独立缺口(另行立案,不在本单修):hono current-user-endpoints 的独立解析器不读 sys_user_position,岗位绑定的能力到不了 /me/permissions,因此正向对照改用 sys_user_permission_set 用户直绑验证。


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions