feat(workflow): persist and resume approval gates (WF-08)#2377
Open
brocoppler wants to merge 1 commit into
Open
feat(workflow): persist and resume approval gates (WF-08)#2377brocoppler wants to merge 1 commit into
brocoppler wants to merge 1 commit into
Conversation
brocoppler
force-pushed
the
feat/workflow-approval-gates
branch
from
July 22, 2026 15:35
d1423cf to
4f34ee5
Compare
Runs that hit a request_approval step were marked Failed with 'approval gates not yet implemented' — the executor minted a token and returned Suspended, but finalize_run discarded it, so the grant/deny handlers, resume_workflow_after_approval, and the workflow_approvals CRUD were all dead code. This wires the missing suspend half: - executor: StepResult::Suspended now carries PendingApproval (token, step_id, resolved approver spec, message, timeout). The approver spec is resolved up front — "any", hex pubkey, or npub1…, with or without a leading @ — and unresolvable specs (display names) fail the step loudly instead of minting an approval that check_approver_spec could never authorize. Invalid timeout strings now error at the step too. - finalize_run: replaces the Failed stub with arm-and-announce ordering: create the workflow_approvals row (raw token hashed by buzz-db), move the run to WaitingApproval at the suspended step, then announce via the new ActionSink::emit_approval_requested. Any persistence failure marks the run Failed so a run never waits on an ungrantable gate; the announcement is best-effort once the gate is armed, logged at ERROR on failure. - relay sink: emits kind:46010 signed by the relay keypair — d tag = SHA-256 token hash (the grant handlers' lookup key, now derived from the shared buzz_db::workflow::hash_approval_token), p tag = resolved approver (or workflow owner for "any") so the request lands in the needs-action feed, h tag when the workflow is channel-scoped, raw token + message + run/step metadata in the JSON content. With the row minted and the run in WaitingApproval, the existing grant path (handle_approval_grant → resume_workflow_after_approval → execute_from_step) and deny path (→ Cancelled) operate unchanged. Not covered here, noted for follow-up: kind:46011/46012 lifecycle emission on grant/deny, approval-token multi-tenant conformance rows (currently #[ignore]d pending_lane), and display-name approver resolution via the member directory. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Broc Oppler <brocoppler@gmail.com>
brocoppler
force-pushed
the
feat/workflow-approval-gates
branch
from
July 22, 2026 21:04
4f34ee5 to
38b2681
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this solves
Closes the WF-08 gap called out in VISION.md and ARCHITECTURE.md (known-gap #5): runs that hit
request_approvalwere marked Failed with "approval gates not yet implemented". The resume half — grant/deny handlers,resume_workflow_after_approval,execute_from_step,workflow_approvalsCRUD, migration, CLI, SDK — already existed but was unreachable because the suspend half never persisted anything. This PR wires that half. Design decisions are laid out in #2376 for sign-off.How it works
buzz-workflow):StepResult::Suspendednow carries aPendingApproval(token, step id, resolved approver spec, message, timeout), captured where the resolved action is in scope. The approver spec is resolved before suspension —"any", 64-char hex, ornpub1…, each with optional leading@. Unresolvable specs (display names) fail the step with an actionable error instead of minting an approvalcheck_approver_speccould never authorize. Invalidtimeout:strings now error at the step as well.finalize_run): the Failed stub is replaced with arm-then-announce: (1)create_approvalrow — raw token hashed bybuzz-db; (2) run →WaitingApprovalat the suspended step with the accumulated trace; (3) announce via the newActionSink::emit_approval_requested. Ordering means a half-armed gate is never observable. Any persistence failure marks the run Failed; announce failures are logged at ERROR but don't fail the run (the gate is armed and grantable by token).dtag = SHA-256 token hash (the grant handlers'get_approval_by_stored_hashkey, derived from the now-pubbuzz_db::workflow::hash_approval_token, keeping every hash derivation on one definition),ptag = resolved approver or the workflow owner for"any"(so the request lands inquery_needs_action),htag when channel-scoped, and content JSON carrying the raw token, message, workflow/run/step ids, and expiry. Persist + fan-out mirror thesend_messagepath (insert_event_with_thread_metadata+dispatch_persistent_event).With the row minted and the run in
WaitingApproval, the existing grant path (handle_approval_grant→ spawnresume_workflow_after_approval(step_index + 1)) and deny path (→ Cancelled) operate unchanged — no relay handler changes.Testing
cargo test -p buzz-workflow: 153 passed, 0 failed.cargo clippy --all-targets --all-featuresclean across buzz-workflow/buzz-relay/buzz-db; fmt clean.cargo test -p buzz-relay --lib: 719 passed; 1 failure inapi::mesh_demo::tests::demo_join_forwarded_arm_round_trips_echowhich reproduces identically on cleanmainin my environment (passes standalone, fails under the parallel suite) — pre-existing flake, unrelated to this change; can file separately.#[ignore]d conformance rows (approval_token_is_community_confined,workflow_trigger_is_community_confined) are now unblocked — I'd like to rewrite them as a follow-up PR unless you want them in this one.Follow-ups (deliberately out of scope)
resolve_mention_pubkeys).pending_laneconformance rows against the live gate.🤖 Generated with Claude Code