block / block/buzz

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

Open
#2,376 2 comments 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
32.7k
Forks
4.3k
Avg merge
1d 13h
Merged PRs (30d)
253

Description

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_grant` → `resume_workflow_after_approval` → `execute_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.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.