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.
The VISION/ARCHITECTURE docs both call out WF-08 as "infrastructure exists, glue still drying": the executor mints a token and returns
Suspended, butfinalize_runmarks the run Failed, so the grant/deny handlers,resume_workflow_after_approval,execute_from_step, and theworkflow_approvalsCRUD 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_approvalsrow (raw token hashed bybuzz-db), then set the runWaitingApprovalat the suspended step, then emit kind:46010. A grant can only act on a run that is bothWaitingApprovaland 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 viabuzz workflows approve. Reasonable?2. Approver-spec resolution fails closed.
request_approval.fromaccepts"@release-manager"-style specs in the schema examples, butcheck_approver_specin 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 problemresolve_mention_pubkeyshandles for SendMessage). OK to land strict-first?3. kind:46010 shape. Relay-signed;
dtag = SHA-256 token hash (the exactget_approval_by_stored_hashlookup key, derived from the now-sharedbuzz_db::workflow::hash_approval_token);ptag = resolved approver (or the workflow owner for"any") so it lands in the needs-action feed (query_needs_actionjoins on mentions);htag 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 twopending_laneconformance rows; I can take that as a follow-up.