Workflow engine has no re-trigger cycle breaker: a message_posted workflow that posts into its own channel self-sustains via agent replies
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
_Filed by Brain (an agent) on behalf of Wes (`wesbillman`) — the account is his, the finding and evidence are mine._
Follow-up split out of #6953 (agent wake from workflow messages) so the identity-seam fix is not coupled to an engine change. **This is pre-existing behavior, not introduced by #6953** — but #6953 makes it reachable without a human in the loop, which is why it is worth fixing now.
## The cycle
A `message_posted` workflow whose step posts back into the same channel re-fires on its own output's *downstream effect*.
Proven empirically with a scratch Postgres test in `buzz-workflow --lib`: a `message_posted` workflow + one human message + one ordinary agent-signed kind:9 reply in the same channel produced **2 runs**, not 1.
## Why the existing loop fence does not cover it
`crates/buzz-relay/src/handlers/event.rs` skips workflow triggering only for events that are **relay-signed AND** carry the `buzz:workflow` marker. That correctly fences the workflow's *own* output. It does not fence the *reply to* that output: an agent's reply is signed by the agent key and carries no `buzz:workflow` tag, so it is an ordinary kind:9 that re-enters `WorkflowEngine::on_event`.
There is no per-channel run budget, no cause-chain depth counter, and no trigger-event dedup on that path. `max_concurrent: 100` is a global concurrency semaphore, not a cycle breaker.
## What #6953 changes about it
Before: the workflow's output could not reliably wake an agent, so sustaining the cycle required a human to reply each round.
After: the output reliably wakes a mentioned agent, and agents reliably reply — so the cycle self-sustains.
To be clear about severity: this is **not** an authorization failure. The loop runs entirely as the workflow owner's own authority in their own channel, and nothing crosses an identity boundary. It is a runaway-execution / cost concern.
## Mitigation available today (no code change)
A `message_posted` trigger filter on `trigger_author` — e.g. fire only for the owner's own messages. This is trustworthy because `build_trigger_context` sources `author` from the **event signature**, not from event content.
Two asks here:
1. **Docs:** tell workflow authors to filter on `trigger_author` when a step mentions an agent.
2. **Engine:** an actual breaker, so a mis-authored workflow cannot run away. Either a cause-chain depth limit (carry a depth counter through workflow-caused events and refuse past N) or a per-channel/per-workflow run budget over a window. Depth is the more precise of the two — a budget also throttles legitimate bursts.
## Suggested acceptance
A regression that reproduces the 2-run result above and asserts the cycle terminates at the configured bound.
Contributor guide
Assessment
This issue has not been assessed yet.