Skip to content

WF-08: wire approval-gate persist/suspend/resume (design + implementation) #2376

Description

@brocoppler

The VISION/ARCHITECTURE docs both call out WF-08 as "infrastructure exists, glue still drying": the executor mints a token and returns Suspended, but finalize_run marks the run Failed, so the grant/deny handlers, resume_workflow_after_approval, execute_from_step, and the workflow_approvals CRUD are all currently unreachable. The conformance suite has two #[ignore]d rows explicitly blocked on this.

I've implemented the missing half and I'm opening the PR alongside this issue — flagging three design decisions here for explicit sign-off:

1. Arm-then-announce ordering. On suspension: create the workflow_approvals row (raw token hashed by buzz-db), then set the run WaitingApproval at the suspended step, then emit kind:46010. A grant can only act on a run that is both WaitingApproval and has a pending row, so this order never exposes a half-armed gate. Any persistence failure marks the run Failed (a run must never wait on an ungrantable gate). The announcement is best-effort once the gate is armed — an emit failure is logged at ERROR but doesn't fail the run, since the token remains grantable via buzz workflows approve. Reasonable?

2. Approver-spec resolution fails closed. request_approval.from accepts "@release-manager"-style specs in the schema examples, but check_approver_spec in the grant handler authorizes only ""/"any"/64-char hex. Unresolved, that's an approval nobody can ever grant — a silent dead end until expiry. I resolve at suspend time: any/hex/npub1… (with or without @) are accepted; display names fail the step loudly with an actionable error. Display-name → pubkey resolution via the member directory would be a follow-up (it needs a name-collision policy — same ambiguity problem resolve_mention_pubkeys handles for SendMessage). OK to land strict-first?

3. kind:46010 shape. Relay-signed; d tag = SHA-256 token hash (the exact get_approval_by_stored_hash lookup key, derived from the now-shared buzz_db::workflow::hash_approval_token); p tag = resolved approver (or the workflow owner for "any") so it lands in the needs-action feed (query_needs_action joins on mentions); h tag when the workflow is channel-scoped; content JSON carries the raw token + message + workflow/run/step + expiry. Grant/deny lifecycle events (46011/46012) are still unemitted — same family, happy to add in the PR or a follow-up, your call.

With the gate armed, the existing grant path (handle_approval_grantresume_workflow_after_approvalexecute_from_step) and deny path (→ Cancelled) work unchanged — no relay handler changes needed. This also un-blocks rewriting the two pending_lane conformance rows; I can take that as a follow-up.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions