Adds owner-only agent mentions, a relay setting,
`BUZZ_WORKFLOW_AGENT_MENTIONS`. Its values are `any`, the default and
today's behaviour, and `owner`. Any other value fails startup. With
`owner`, a workflow message can direct an agent only if the run's cause
and definition allow it. The setting is relay-wide.
**Recording what caused each run.** `workflow_runs` gets `cause_source`
and `cause_pubkey` columns. Migration `0087` adds them with named checks
marked `NOT VALID`, so it does not scan existing rows under its lock;
`0088` validates the checks separately. `create_workflow_run` now takes
a required `RunCause`, so every place that creates a run has to set one:
- **Event trigger:** the cause is the signer of the message, reaction or
diff. For a reaction, that is the reactor, not the author of the message
being reacted to.
- **Cron and interval schedules:** the cause is the timer.
- **Webhook:** the caller is unknown.
- **Manual trigger:** the cause is the invoking pubkey.
Rows written before this change have no cause. A stored source without
the pubkey it requires, or with one it forbids, is read as no cause.
**Deciding eligibility.** A run's messages may direct agents only when
all of these hold:
- the cause is a schedule, or an event or manual trigger whose pubkey is
the workflow owner's;
- every step of the definition that is running is `send_message` or
`delay`. This is an allowlist, so any other action, including one added
later, makes the definition ineligible, even when an `if:` skips that
step;
- the run did not resume after an approval.
Webhook runs and runs with no recorded cause are never eligible. The
executor decides this from `WorkflowConfig::agent_mentions`, and passes
`ActionSink::send_message` the owner's stored step text with a
`may_direct_agents` flag. The sink adds `buzz:workflow-mention` tags
only when the flag is set, and returns `SentMessage { event_id,
agent_mentions_withheld }`, which is true only when it left out a tag
that a member named in both the stored and rendered text would otherwise
have received. Webhook and manual runs start through `execute_run`, so
`execute_from_step` is used only to resume after an approval.
**The gate.** With `owner`, the relay adds `buzz:workflow-mention` only
to messages from eligible runs. Messages from other runs still post with
their plain `p` tags and still notify people. With `any`, mention tags
are added exactly as before.
**Making it visible.** When a `send_message` step leaves the tags out:
- its output in the run trace gets `"agent_mentions": {"withheld": true,
"reasons": [...]}`, listing each reason that applies: `cause` (not
started by the owner or a timer), `steps` (a step other than
`send_message` or `delay`), `resumed` (resumed after an approval). The
key is absent when nothing was withheld. The trace is what the app's run
history shows;
- the relay logs it at info level under the
`buzz_relay::workflow_agent_mentions` target (visible under the default
filter) with the community, workflow, run, step and reasons;
- the counter `buzz_workflow_agent_mentions_withheld_total` increases
once per reason, labelled `reason`.
**Discovery.** The NIP-11 document always carries
`"workflow_agent_mentions": "any" | "owner"`, so a missing field means
an older relay.
`NOSTR.md` now specifies `buzz:workflow-mention` for clients: what the
tag means, the checks an agent makes before acting, that a relay may
leave it out under a policy, and the NIP-11 `workflow_agent_mentions`
field. The relay policy itself is in the new operator guide,
`docs/operator-workflow-agent-mentions.md`: the setting and its values,
what counts as a run's cause for each trigger, which steps are allowed,
the trace key, the log and metric, the NIP-11 field, when to use
`owner`, rollout across pods, and what it doesn't cover. The
`TESTING.md` and `.env.example` entries link to it.
---------
Signed-off-by: Will Pfleger <wpfleger@block.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
3.7 KiB
Owner-Only Agent Mentions
A workflow message carries a buzz:workflow-mention tag for each agent the
owner's stored step text names. That tag lets the message direct the agent on
the owner's behalf (see "Workflow Mentions" in NOSTR.md).
Owner-only agent mentions keep the tag off messages from runs that someone
other than the owner could have steered.
| Setting | Values | Default |
|---|---|---|
BUZZ_WORKFLOW_AGENT_MENTIONS |
any, owner |
any |
Any other value fails startup. With any, the relay adds the tags for every
run. With owner, a send_message step adds them only when the run is
eligible: its cause qualifies and its steps qualify. The setting is
relay-wide: it applies to every community the relay serves.
Cause
The relay records what caused each run when it creates it.
| Trigger | Cause | Eligible |
|---|---|---|
| Event (message, reaction, diff) | the event's signer; for a reaction, the reactor, not the author of the message reacted to | only if that is the owner |
| Schedule (cron or interval) | the timer | yes |
| Webhook | unknown | never |
| Manual | whoever ran it | only if that is the owner |
Only the owner or a timer makes a run eligible. The owner's other agents count as someone else.
Runs created before causes were recorded have no cause, and are never eligible.
Steps
Every step of the workflow definition must be send_message or delay. Any
other step makes the run ineligible, even one the run skips. A run that
resumes after an approval is ineligible too.
What people see
An ineligible run's messages still post, with their p tags, so mentioned
people are still notified. No agent acts on them.
When an ineligible run's message names a channel member in the workflow's
stored text, and that name is also in the posted text, the relay leaves off
that member's buzz:workflow-mention tag. The step's output in the run's
trace, which the owner sees in the app's run history, then records why:
{"sent": true, "event_id": "…", "agent_mentions": {"withheld": true, "reasons": ["cause", "steps"]}}
reasons lists every reason that applies, in this order:
| Reason | Meaning |
|---|---|
cause |
Neither the owner nor a timer started the run. |
steps |
The definition has a step other than send_message or delay. |
resumed |
The run resumed after an approval. |
The key is absent when no tag was left off: on eligible runs, with any, and
for messages with no such mention, such as plain text, a mention that comes
only from substituted text, or a name that matches no member or more than one.
Logs and metrics
Each withheld step logs at info level owner-only agent mentions withheld buzz:workflow-mention tags, with community, workflow, run_id, step
and reasons. The counter buzz_workflow_agent_mentions_withheld_total
increases by one for each reason, labelled reason (cause, steps or
resumed), so one step with two reasons counts under both.
Discovery
The NIP-11 relay information document always carries
"workflow_agent_mentions": "any" or "owner". A client that finds no field
is talking to a relay older than this setting.
When to use owner
When people other than a workflow's owner can trigger it: by posting or reacting in its channel, or through its webhook.
Rollout
Any pod set to any still adds the tags, so the setting protects agents only
once every relay pod runs with owner. Keep client-side defenses in
place until then.
What it does not cover
- An agent that acts on a workflow message still reads thread, pull request and repository text that anyone can write, the same as for a direct mention.
- A replayed workflow message behaves as it does with
any.