WF-08: approval gate is ~90% built — finalize_run drops the token instead of creating WaitingApproval
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
## Summary
**WF-08 is much closer to done than VISION/ARCHITECTURE suggest.** The executor already suspends and returns a full resumption payload; the approve/deny/resume path is already written. The only hard block is `finalize_run` marking the run **Failed** when `approval_token` is present, so **no `WaitingApproval` row is ever created** and grant/deny handlers have nothing to resume.
Verified against `main` @ `485d03a` (static analysis only — no `cargo` run in this environment).
## What already works
| Piece | Location (today) | Status |
|-------|------------------|--------|
| Suspend + mint token | `buzz-workflow/src/executor.rs` — `StepResult::Suspended`, `ExecutionResult { approval_token, step_index, step_outputs, trace }` | Done |
| Resume executor | `execute_from_step` | Done |
| Resume after grant | `command_executor.rs` ~1114–1124: `resume_index = approval.step_index as usize + 1`, then `resume_workflow_after_approval` | Done |
| Grant/deny gates | `get_approval_by_stored_hash` + `update_approval_by_stored_hash` with race check (`if !updated`) | Done |
| Deny status guard | `run.status != WaitingApproval` → no-op | Done |
| DB CRUD | `create_approval` / `get_approval` / `update_approval` in `buzz-db` | Done |
| Status enum + serde | `RunStatus::WaitingApproval` | Done |
| Kind + push | `KIND_WORKFLOW_APPROVAL_REQUESTED = 46010`, push lease lists 46010 as **urgent** | Done |
| CLI approve path | `buzz-cli` approval_token validation + hash | Done |
Docs that undersell this:
- `VISION.md`: \"executor doesn't yet persist the approval token or suspend execution\"
- `ARCHITECTURE.md` ~552 / ~826: \"does not yet persist the token or resume\" / \"intercepts before creating WaitingApproval rows\"
The executor **does** suspend and **does** return the token. Resume is implemented. Persistence is the missing wire.
## The single blocker
`crates/buzz-workflow/src/lib.rs` — `finalize_run` (~229–253):
```rust
if result.approval_token.is_some() {
// Approval gates are not yet implemented (WF-08).
// Fail explicitly rather than creating unreachable WaitingApproval rows.
...
RunStatus::Failed,
...
Some(\"approval gates not yet implemented — see WF-08\"),
}
```
That comment was true when written (if nothing consumed `WaitingApproval`). It is stale now: approve/deny handlers require `WaitingApproval` and call `execute_from_step`.
Corroboration: in the **relay** crate, the string `approval_token` does not appear as a field consumer of the executor result (finalize_run is the choke point). The executor hands the token up; the finalizer drops it into a Failed status.
Also note executor TODO at `executor.rs` ~663: \"TODO (WF-08): create approval record in DB, emit kind:46010\" — that belongs with finalize_run (or a shared helper), not only at mint time.
## Proposed scoped change (one function + event emit)
In `finalize_run`, when `result.approval_token` is `Some(token)`:
1. **Persist** via existing `create_approval` / `db.create_approval` with:
- `token` (raw; DB hashes)
- `run_id`, `workflow_id` (must be available to finalize_run — may need to pass workflow_id / step_id / approver_spec through `ExecutionResult` or load run→workflow from DB)
- `step_id` / `step_index` = **suspended** step (`ExecutionResult.step_index` is that index — see caveat below)
- `expires_at`
2. **Update run** to `RunStatus::WaitingApproval` (not Failed), with full trace and `current_step = step_index`.
3. **Publish** kind `46010` (`KIND_WORKFLOW_APPROVAL_REQUESTED`) so existing urgent push fires.
Grant path already does:
```rust
let resume_index = approval.step_index as usize + 1;
```
so storing the **suspended** step index is required.
### Caveats (easy to get wrong)
1. **`step_index` semantics:** On suspend, `ExecutionResult.step_index` is the index of the **suspended** approval step (`i` in the loop). Resume **must** start at `step_index + 1` (already done on grant). If create_approval stores the wrong index, the approval step re-fires and mints a **second** token.
2. **Double-approve race:** `update_approval_by_stored_hash` returning false already rejects races on the approval row. Additionally, only one concurrent resume should run for a given `run_id` (lease / single-flight). Status guard `WaitingApproval` → `Running` should be atomic before `execute_from_step`, or two granted tokens could double-execute.
3. **`workflow_id` / `step_id` / `approver_spec`:** finalize_run today only receives `community_id`, `run_id`, `ExecutionResult`. May need to extend `ExecutionResult` (or re-load run + definition) so create_approval gets full params without plumbing hacks.
4. **Line numbers drift** — re-verify on a fresh clone before coding.
## Suggested acceptance checks
- [ ] YAML workflow with `request_approval` leaves run status `waiting_approval`, not `failed`
- [ ] Row exists in `workflow_approvals` with pending status and hashed token
- [ ] Kind 46010 event published; push path can deliver urgent
- [ ] Grant → run resumes at step_index+1; completes or fails later steps normally
- [ ] Deny → run `cancelled`
- [ ] Concurrent double-grant: only one resume executes
- [ ] Community isolation: token hash scoped by community (existing tests)
## Intent
This is a **finding + offer to implement**, not a drive-by PR. Happy to open a follow-up PR against `main` if nobody is mid-flight on WF-08.
---
**Source:** static read of public `block/buzz` @ `485d03a` while integrating agent-workflow products elsewhere. No affiliation with Block.
Contributor guide
Assessment
This issue has not been assessed yet.