Relationships
#4493 Expose a manual_approval gate's decision identity (decidedBy) to workflow expressions
Opened by kneel · 11/7/2026
Summary
A workflow step that runs after a manual_approval gate has no way to record WHO approved the gate. The runtime captures the approver, but it is not reachable from any expression a step can bind, so a method that needs to persist "approved by X" is forced to bind run.initiatedBy -- the identity that STARTED the run, not the one that APPROVED the gate. When those differ (the whole point of a separation-of-duties gate), the persisted approver is wrong.
Evidence (swamp source)
- The approver IS captured:
src/libswamp/workflows/run.ts:451recordsapprovedBy: decision.decidedByinto the run's approval record, and it renders in the CLI (presentation/renderers/workflow_run_display.ts). - But the
run.*expression context exposes only seven fields (design/enablers/expressions.md):id,workflowId,workflowName,startedAt,tags,initiatedBy,inputs. There is nodecidedBy/approvedBy. run.initiatedByis resolved once at run start (src/cli/auth_context.ts:148->user:<username>orghost); it does not change when the gate is later approved.- A model method's
execute()context (src/domain/models/method_execution_service.ts) exposes logger/redactor/vault/data but no verified caller identity, so the method cannot self-stamp the approver either.
Net: there is no correct field, anywhere, for a post-gate step to bind the real approver.
Impact
Any gated approval that must be RECORDED (not just enforced) cannot attribute the approval truthfully under segregation-of-duties. Concretely, an HR timesheet-approval workflow that writes an approval record stores the run's initiator instead of the approver whenever they differ -- a correctness/audit-integrity problem for compliance records. It is only correct when the same identity starts and approves the run.
Proposed solution
Expose the gate decision identity to workflow expressions so a downstream step can bind it. Either:
- add
decidedBy(and ideallydecidedAt) to a per-gate/step context, e.g.steps.<gateStep>.decidedBy, as the approval step's outcome; or - surface the most-recent approval decision on the run context, e.g.
run.lastApproval.decidedBy.
The value already exists at run.ts:451; this is a plumbing/exposure change, not new capture logic.
Alternatives considered
- Run-as-approver convention (the approver runs the workflow, so
initiatedBy== approver): works only when start == approve; does not support true separation-of-duties. This is the current stopgap. - Method self-stamps the caller: not possible --
execute()has no verified caller identity.
Scope note
Not a code bug; a deliberate capability gap in what gate decisions expose to expressions.
Open
No activity in this phase yet.
Sign in to post a ripple.