Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,14 @@ Note: Both `TriggerDef` and `ActionDef` use serde internally-tagged enums. Trigg
| `request_approval` | Suspend execution; fields: `from`, `message`, `timeout` (default 24h) |
| `delay` | Pause execution (max 300 seconds) |

**Workflow-to-agent authority:** A workflow `send_message` action persists one ordinary relay-signed kind `9` timeline message containing the rendered text, then creates one durable delivery row per owner-signed target. The visible message is identified by a single `["buzz:workflow", "message-v1"]` marker and binds the exact kind `30620` definition revision, run, step, typed cause (`event`, `command`, `schedule`, or `webhook`), channel, and owner. Mention targets are derived only from the owner-signed template, never from rendered trigger or webhook data; `p` tags are routing metadata and never grant authority.

The relay publishes an ephemeral kind `24620` wake containing the durable delivery ID and immutable binding fields. ACP accepts workflow authority only after authenticating that wake as relay-signed, atomically claiming its target-specific delivery lease, and verifying the referenced visible message and signed definition against the delivery's immutable execution snapshot. It reconstructs the rendered text from the exact stored trigger context and prior-step trace and requires byte-for-byte equality with the persisted timeline content. The durable `(run, step, target)` identity collapses retries while preserving distinct `send_message` steps from the same run. Lease renewal and fenced terminal acknowledgement provide reconnect and crash recovery without opening a second authority path.

Webhook data remains untrusted and size-limited at ingress. It may affect rendered text only through slots declared by the signed definition; it is retained in the private delivery snapshot rather than exposed as transport payload. An unclaimed workflow-marked kind `9`, a malformed or forged wake, a mismatched definition/run/step/channel/message binding, missing relay identity, or rendering mismatch fails closed. A verified workflow owner remains subject to the same `respond_to` policy and DM hardening as a directly authored message.

**Partial rollout:** durable workflow-to-agent delivery is producer-gated by `BUZZ_WORKFLOW_AGENT_DELIVERY_ENABLED`, which defaults to `false`. Operators upgrade all ACP harnesses first, then enable the relay producer; while disabled, `send_message` fails before publishing either the visible kind `9` or durable rows. This fence is required because a legacy ACP configured `respond-to=anyone` would otherwise execute a relay-authored kind `9` without claiming it. Once enabled, upgraded ACP drops any workflow-marked kind `9` that has not arrived through a successfully claimed durable wake. Ordinary messages are unaffected.

**Template variables:** `{{trigger.text}}`, `{{trigger.author}}`, `{{steps.ID.output.FIELD}}`. Single-pass resolution (not recursive). Unknown variables left as literal text.

**Condition evaluation:** `evalexpr` with `HashMapContext`. Dot notation converted to underscores (`trigger.text` → `trigger_text`). Custom functions registered: `str_contains`, `str_starts_with`, `str_ends_with`, `str_len`. 100ms timeout prevents adversarial expressions from blocking.
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/buzz-acp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ path = "src/main.rs"
# Internal
buzz-core = { workspace = true }
buzz-sdk = { workspace = true }
buzz-workflow = { workspace = true }
buzz-persona = { path = "../buzz-persona" }

# Nostr
Expand Down
8 changes: 6 additions & 2 deletions crates/buzz-acp/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1277,7 +1277,8 @@ pub fn resolve_channel_filters(
rules: &[SubscriptionRule],
) -> HashMap<Uuid, ChannelFilter> {
use buzz_core::kind::{
KIND_STREAM_MESSAGE, KIND_STREAM_REMINDER, KIND_WORKFLOW_APPROVAL_REQUESTED,
KIND_STREAM_MESSAGE, KIND_STREAM_REMINDER, KIND_WORKFLOW_AGENT_WAKE,
KIND_WORKFLOW_APPROVAL_REQUESTED,
};

let target_channels: Vec<Uuid> = if let Some(ref overrides) = config.channels_override {
Expand All @@ -1297,6 +1298,7 @@ pub fn resolve_channel_filters(
let kinds = config.kinds_override.clone().unwrap_or_else(|| {
vec![
KIND_STREAM_MESSAGE,
KIND_WORKFLOW_AGENT_WAKE,
KIND_WORKFLOW_APPROVAL_REQUESTED,
KIND_STREAM_REMINDER,
]
Expand Down Expand Up @@ -1379,7 +1381,8 @@ pub fn resolve_dynamic_channel_filter(
rules: &[crate::filter::SubscriptionRule],
) -> Option<ChannelFilter> {
use buzz_core::kind::{
KIND_STREAM_MESSAGE, KIND_STREAM_REMINDER, KIND_WORKFLOW_APPROVAL_REQUESTED,
KIND_STREAM_MESSAGE, KIND_STREAM_REMINDER, KIND_WORKFLOW_AGENT_WAKE,
KIND_WORKFLOW_APPROVAL_REQUESTED,
};

// In Mentions/All mode, if the operator explicitly constrained channels
Expand All @@ -1402,6 +1405,7 @@ pub fn resolve_dynamic_channel_filter(
kinds: Some(config.kinds_override.clone().unwrap_or_else(|| {
vec![
KIND_STREAM_MESSAGE,
KIND_WORKFLOW_AGENT_WAKE,
KIND_WORKFLOW_APPROVAL_REQUESTED,
KIND_STREAM_REMINDER,
]
Expand Down
Loading
Loading