Feature request: a workflow action that runs an allowlisted local command
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
## Summary
`TriggerDef::Schedule` gives workflows a real cron/interval scheduler, but `ActionDef`
(`crates/buzz-workflow/src/schema.rs:92`) can only *communicate*:
```
SendMessage · SendDm · SetChannelTopic · AddReaction · CallWebhook · RequestApproval · Delay
```
There is no way for a scheduled workflow to cause work to happen on a machine. In practice
that means the scheduler can't be used for the most common reason people want a scheduler —
running a job — without building a separate HTTP surface to receive the trigger.
I'd like an action along the lines of `RunCommand`: a workflow step that invokes a
**pre-registered, allowlisted** command on a host, rather than an arbitrary shell string.
## Why `CallWebhook` doesn't cover it
It's the documented workaround, and it works, but the cost is disproportionate for a local job:
1. **`check_ssrf` rejects private and reserved addresses**
(`crates/buzz-workflow/src/executor.rs:745`, `:766`). That's correct and I'm not asking
for it to be relaxed — but it means a workflow *cannot* reach a service on the same host
or LAN as the relay. The only path is out through the public internet and back.
2. So triggering a local job requires **exposing a new authenticated HTTPS endpoint** to the
internet. For a nightly cache refresh on a box that already sits next to the relay, that
is a real security surface added purely to receive a timer tick.
3. Every user who wants a scheduled job writes the same receiver: auth check, ACK, detach,
report. That's a missing primitive, expressed as boilerplate.
## Proposed shape
Deliberately **not** arbitrary shell — the value is in a narrow interface:
```yaml
trigger:
schedule:
cron: "30 10 * * *"
actions:
- run_command:
command: decksmith-sync # id of a registered command, NOT a shell string
args: ["--store", "all", "--full"]
timeout: 20m
capture_output: true
- send_message:
text: "Inventory sync: {{ steps.0.status }} — {{ steps.0.stdout_tail }}"
```
Sketch of the properties that seem to matter:
- **Registration is out-of-band and host-side** — an operator declares
`decksmith-sync → /usr/local/bin/decksmith-sync` in host config. The workflow can only
name a registered id; it can never introduce a new executable.
- **Args allowlisted or schema-constrained per command**, so a workflow can't smuggle
behavior through argv.
- **Runs under the existing systemd sandbox**, same posture as agent units
(`NoNewPrivileges`, `ProtectSystem=strict`, dedicated service user, declared
`ReadWritePaths`). The unit stays the authority boundary.
- **Long jobs shouldn't block the executor** — either async with completion posted as a
follow-up event, or a documented timeout ceiling.
- **Output redaction** — and per the lesson in this codebase's own broker work, error text
must not echo caller-supplied values, or redaction becomes a confirmation oracle.
- Presumably gated by channel role, and worth considering whether it should be
owner-only by default given it is the most powerful action in the set.
## Use case that motivated this
A nightly inventory sync (~11 min) on the same host as the relay. Everything else about it
is a natural fit for Buzz: the schedule, reporting results into a channel, alerting on
failure, and an agent in that channel answering questions about the data afterward.
The only missing piece is "start the job", and closing that one gap currently costs a
public HTTPS endpoint. With `RunCommand` the entire job is one workflow definition and no
new attack surface.
Other shapes this would cover: backups, mirror/reconciler ticks, health probes that need
a local binary, cache warms, report generation.
## Alternatives considered
| Alternative | Why it falls short |
|---|---|
| `CallWebhook` → local receiver | Works today; costs a public endpoint + per-user boilerplate (above) |
| Schedule → `SendMessage` → agent runs it via MCP shell | Puts an LLM in the path of a deterministic job; long tool calls are fragile |
| Keep cron/systemd, use Buzz only for reporting | What most people will do — but then the scheduler in Buzz goes unused for its main purpose |
Happy to help shape or implement this if the direction is welcome.
---
*Verified against a local checkout at `44599e7`. Line references are from that tree.*
Contributor guide
Assessment
This issue has not been assessed yet.