Found while measuring the button-visibility half of #11993 (not fixed there — different direction, and touching who sees which action is a permission-shaped change).
The asymmetry
ApprovalService.recall admits two callers: the submitter, and a privileged platform/tenant admin releasing a stuck record. isOverrideActor's own doc block names recall as one of the four override levers, in so many words:
A platform or tenant admin ... may always act on a PENDING request to release it: approve, reject, reassign it to a real approver, or recall it.
The declared action that reaches that endpoint does not agree. In packages/plugins/plugin-approvals/src/sys-approval-request.object.ts, approval_recall's visible predicate is submitter-only:
has(record.status) && (record.status == "pending" || record.status == "returned")
&& has(record.viewer) && has(record.viewer.is_submitter) && record.viewer.is_submitter == true
Its three siblings that the same override covers — approval_approve, approval_reject, approval_reassign — all OR in record.viewer.can_override, and sys-approval-request.object.test.ts pins exactly that:
it('the core decision levers OR in the admin override (#3424) so a stuck request is recoverable', ...)
can_override is computed server-side for every viewer (attachViewers), so the signal the predicate would need is already on the row.
Why it matters
#3424 exists because an approval routed to an unstaffed position locks its record with no in-product recovery. Recall is the lever that RELEASES the record without recording a decision on someone else's behalf — arguably the one an operator wants most, and the only one of the four with no button for the actor the service authorises. The admin can approve or reject their way out (which writes a decision that did not happen) or reassign, but cannot simply withdraw.
What this is NOT
Not a permission change: the service already allows this and has since #3424. This is declared-vs-enforced drift in the other direction from the usual — a capability the server grants that no UI entry exposes.
Shape
Add record.viewer.can_override to approval_recall's visible predicate the way the three sibling levers spell it, and extend the pin test to cover it. Whether the override arm should be limited to pending (the service's override branch is pending-only; the returned/revise-window arm is a submitter concept) is the one judgement call, and it belongs to a triage seat rather than to a dev finding.
Measured on origin/main at 15bf9e859.
Filed unassigned by the domain:services dev seat working #11993.
Found while measuring the button-visibility half of #11993 (not fixed there — different direction, and touching who sees which action is a permission-shaped change).
The asymmetry
ApprovalService.recalladmits two callers: the submitter, and a privileged platform/tenant admin releasing a stuck record.isOverrideActor's own doc block names recall as one of the four override levers, in so many words:The declared action that reaches that endpoint does not agree. In
packages/plugins/plugin-approvals/src/sys-approval-request.object.ts,approval_recall'svisiblepredicate is submitter-only:Its three siblings that the same override covers —
approval_approve,approval_reject,approval_reassign— all OR inrecord.viewer.can_override, andsys-approval-request.object.test.tspins exactly that:can_overrideis computed server-side for every viewer (attachViewers), so the signal the predicate would need is already on the row.Why it matters
#3424 exists because an approval routed to an unstaffed position locks its record with no in-product recovery. Recall is the lever that RELEASES the record without recording a decision on someone else's behalf — arguably the one an operator wants most, and the only one of the four with no button for the actor the service authorises. The admin can approve or reject their way out (which writes a decision that did not happen) or reassign, but cannot simply withdraw.
What this is NOT
Not a permission change: the service already allows this and has since #3424. This is declared-vs-enforced drift in the other direction from the usual — a capability the server grants that no UI entry exposes.
Shape
Add
record.viewer.can_overridetoapproval_recall'svisiblepredicate the way the three sibling levers spell it, and extend the pin test to cover it. Whether the override arm should be limited topending(the service's override branch is pending-only; the returned/revise-window arm is a submitter concept) is the one judgement call, and it belongs to a triage seat rather than to a dev finding.Measured on
origin/mainat15bf9e859.Filed unassigned by the
domain:servicesdev seat working #11993.