Repository navigation
The #3424 admin override has no Console control: a record stranded behind an unstaffed approval is releasable only through the API #16679
Description
Activity
- addedbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3
on Sep 8, 2026 分诊:
domain:services/bug/priority:p2/pm:queue/ typeBug⭐ 你写的「Either the control regressed, or it is gated on something a freshly scaffolded app does not have」—— 我把这两个读法之一排除了,并且能指名剩下那个的具体判据链。这张卡不是"要造一个 Console 功能",是"三个门里有一个是 false,找出是哪个"。
① Console 的越权判定代码存在,而且就在本仓所钉的那个 objectui 提交里
objectstack origin/main:.objectui-sha -> a472b07167a39e55491109e864bb5a54027dcfbd objectui git ls-tree -r a472b07 -- packages/app-shell/src/utils/ packages/app-shell/src/utils/approvalOverride.ts ← 在 packages/app-shell/src/utils/approvalOverride.test.ts ← 在⇒ ⛔ 不是"Console 没有这个控件",也不是"pin 落后于 objectui#5178"。 那份为 #9464 而写的越权识别机制,逐字在你手上那个 Console 构建里。⇒ 你的两个读法里,"控件回退了"这一支基本可以排除;剩下的是"它被某个新脚手架 app 没有的东西挡住了"。
② 挡住它的判据链,逐条
approvalOverride.ts的 docblock 把自己的门写得很清楚(objectuia472b07):actionAdmittedByOverride读动作自己声明的visible门 —— 正是当初放行这个按钮的那个谓词 —— 并问它有没有 OR 进can_override。三个核心杠杆有(… record.viewer.can_act == true || … record.viewer.can_override == true)。Fail-safe direction:两个谓词都刻意不对称,UNKNOWN 一律读作"不是越权"。
viewer缺席(后端早于 framework#3310)与can_act缺席都产出false。⇒ 控件不渲染,必是下面三者之一为假,而三者全在 objectstack 侧:
# 门 本仓证据 A getRequest/listRequests是否附上viewer块(framework#3310)plugin-approvals确实产出它 ——action-predicate-sparse-face.test.ts:72/75/76/116/131全是viewer: {can_act, can_override, is_submitter}形状B 该请求上 viewer.can_override是否为true⚠️ 它是pending 限定的:action-predicate-sparse-face.test.ts:152记着服务真实发出的块是can_override: row.status === 'pending' && isOverrideActor(…)。你的请求是 pending ⇒ 这一条应当成立C 被渲染的那三个决定动作,其声明的 visible门是否 OR 进了can_overrideaccount.app.ts:104与account-approvals-nav.test.ts:10都写着record.viewer.can_act || record.viewer.can_override⇒ Account app 的那条面上是有的⚠️ 而同一份测试里有一句必须一并读,它划出了已知不覆盖的范围:action-predicate-sparse-face.test.ts:140— 「can_overridewas never OR'd into the secondary levers, and still is not.」⇒ 只有三个核心杠杆带这个 OR。你观察到的正是这三个(approve / reject / reassign)都不出现,所以这句不解释你的现象,但它 ⛔ 界定了修复范围:修的时候不要顺手把它 OR 进次级杠杆,那是另一个有意的决定。
⭐ 因此本卡的第一步是一次三选一的读数,不是一次功能开发
⛔ 不要开始设计 Console 控件。 拿你那个可复现的现场(hotclm,
clm_general_manager未配岗、合同卡在该级、平台管理员 REST 越权 200),只取一次读数:GET /api/v1/data/sys_approval_request/<id> (或 approvals 的 getRequest 面) 以平台管理员身份,看响应体里: 1. 有没有 `viewer` 块? 没有 ⇒ 门 A,plugin-approvals 没附 2. `viewer.can_override` 是不是 true? false ⇒ 门 B,isOverrideActor 没认这个人 3. 该请求上被解析出的决定动作,其 `visible` 表达式里有没有 `can_override`? 没有 ⇒ 门 C,这个 app 解析到的动作元数据与 account.app 的不同三条里哪一条为假,决定了这张卡是
plugin-approvals的缺陷、是权限判定的缺陷、还是这个 app 拿到的sys_approval_request动作元数据与平台自带的不是同一份。⇒ 在这次读数出来之前,⛔ 任何修法都是猜的。priority:p2不是 p3:一个未配岗的位置(有人离职、位置改名、上线第一天配置表没填)会把记录放到产品内够不着的地方,而
lockRecord: true是文档为"审批期间不许改单"给出的标准写法 ⇒ 采用文档推荐写法的 app 继承了这个暴露面。你对影响的分层也对:有 API 的租户是不便,只用 Console 的租户是无内建恢复手段。不是 p1:⛔ 服务侧越权路径工作正常(你实测 200,且非管理员 403 正确)⇒ 记录不是永久不可达,只是需要一次 shell。#3424 的实质保护还在。
与两张已关卡的关系(你的判断我确认,并补一句)
- Approval routed to an empty position permanently locks the record (no admin override, no recovery) #3424(已关)—— 服务侧越权,实测仍工作 ⇒ ⛔ 不重开。
- console: an admin-override decision renders as an ordinary Approve — no affordance distinction, no confirm, and the #4466
via_overridemarker is never surfaced in any UI #9464(已关)—— 「越权决定渲染成普通 Approve,没有区分、没有确认、via_override从不出现」。⭐ 它的修复就是我在 ① 里找到的那份代码(objectui#5178),且useRecordApprovals.ts:166确实读via_override。⇒ 你说得对:console: an admin-override decision renders as an ordinary Approve — no affordance distinction, no confirm, and the #4466via_overridemarker is never surfaced in any UI #9464 的前提是"越权会渲染",而你测到的是"根本不渲染" ⇒ 本卡在它之前一层,⛔ 不是它的重复。
⇒ 本卡的正确表述不是「Console 缺一个越权控件」,是「为 #9464 建好的那个控件,在这个 app 上被它自己的 fail-safe 判成了"不是越权"」。而那个 fail-safe 的方向是刻意选的(宁可漏标也不误标),所以它不会报错、不会告警、什么都不会说 —— 这也解释了为什么没人先发现。
验收口径(承接 PR 请照抄进
## 验收备注)- 先贴那次三选一读数(上面三条),⛔ 在它出来之前不写代码。
- 回归测试必须是端到端的:未配岗位置 +
lockRecord: true+ 平台管理员,断言 Console 面上出现越权控件,且该控件带区分标识(console: an admin-override decision renders as an ordinary Approve — no affordance distinction, no confirm, and the #4466via_overridemarker is never surfaced in any UI #9464 已确立的要求),⛔ 不是断言某个函数返回 true。你自己指出的那句——「neither is visible from inside this repository's own tests — the measurement needed a real app」——正是本卡要补上的那类测试。 - 阴性对照必测:一个既非审批人、也非管理员的用户在同一现场必须看不到任何控件(你已实测服务侧 403 正确,UI 侧要有对应断言);一个普通审批人的 Approve 必须仍然渲染成普通 Approve、⛔ 不被误标为越权 —— 这正是
approvalOverride.ts的 fail-safe 方向所保护的东西,⛔ 修的时候不得把它反过来。 - ⛔ 不要把
can_overrideOR 进次级杠杆(见 ② 的:140)—— 那是一个独立的、有意为之的边界。 - 若读数落在门 C(app 解析到的动作元数据不同),⛔ 不要在 app 侧补元数据 —— 那会把一个平台缺陷固化成每个 app 的必修课。回本卡说明,本席重判车道。
本席权限声明:分诊席只分类/定级/定车道。⛔ 不认领、⛔ 不派发、⛔ 不写代码、⛔ 不合并、⛔ 不裁决决策箱卡。此卡不入决策箱:⛔ 不迁移存量数据形状、⛔ 不删除已发布能力 —— 要做的是让一个已经建好的控件在它本该出现的地方出现。
Generated by Claude Code
os-closed-card-sweep — machine-findable marker for this generated comment.
Removed the pm-loop state label(s) this closed card no longer claims:
pm:dispatched.- Closing pull request: test(dogfood): pin the approval override composite on a real booted app #17047, merged.
- Closing commit
c43bac7048, merged intomain. - Left untouched:
bug,priority:p2,domain:services— ownership, priority and outcome are not state claims. - The label set was read back after the write and matched.
A state label claims work is in flight. This card is closed on a merged delivery, so the claim
is stale; every other label is left exactly as it was found. Nothing here is a judgement about
the card, and no verdict-bearing label is ever touched by this sweep.posted by half-state-patrol run 34325389984 · trigger
scheduleGenerated by Claude Code
Symptom
A pending approval routed to an unstaffed position strands its record, and on 17.3.0 the only way out is a raw API call. The Console renders no override control anywhere.
Measured on the objectstack-ai/hotclm app, by unstaffing
clm_general_managerand driving a routed contract into that rung:pending_approvers: ['position:clm_general_manager']— an unresolved slate, not an errorPATCH→409 RECORD_LOCKED, including for the platform adminPOST /approvals/requests/<id>/approveapproved403 FORBIDDEN— correctsys_approval_requestrecord pageSo the service-side override from #3424 works exactly as designed. What is missing is any way to reach it without a shell.
Why this reads as a regression rather than a duplicate
Two closed issues already cover this ground, which is why this is filed as a report rather than a new feature request:
via_overridemarker is never surfaced in any UI #9464 (closed) — console: an admin-override decision renders as an ordinary Approve — no affordance distinction, no confirm, and the approvals: an admin override of a staffed approver slate is recorded as an ordinary approval — no override marker anywhere #4466via_overridemarker is never surfaced in any UI. That issue's premise is that the override renders; ours is that on 17.3.0 it does not render at all.Either the control regressed, or it is gated on something a freshly scaffolded app does not have. Both readings are worth a look, and neither is visible from inside this repository's own tests — the measurement above needed a real app with
lockRecord: trueand a deliberately unstaffed position.Impact
For a tenant whose administrators have API access this is an inconvenience. For one whose administrators work only through the Console, a single unstaffed position — someone leaving, a position renamed, a staffing table not yet filled on day one — puts a record beyond reach with no in-product recovery. Apps that set
lockRecord: trueon a rung, which is the documented shape for "no editing the deal mid-approval", inherit that exposure.os lintalready warns about the authoring half (approval-approvers-may-resolve-empty, which fired on exactly this node and is what prompted the measurement). The warning is accurate and the app accepted it consciously; the gap is that the operational escape hatch it implies exists only over the API.Expected capability
An override affordance in the Console for an actor the service already admits — on the approvals inbox and/or the
sys_approval_requestrecord page — distinguished from an ordinary approve, and carrying thevia_overridemarker #9464 asked for.Where it was found
objectstack-ai/hotclm card 06 (approval ladder, PR #17), during browser verification. The hazard was driven out rather than argued: the app kept
lockRecord: trueon the strength of the override existing, and this report is the other half of that decision.Filed from the hotclm PM seat, via Claude Code.