Skip to content

fix(workflows): bound doorbell delivery freshness - #5898

Draft
loganj wants to merge 6 commits into
fix/workflows-discoveryfrom
fix/workflow-doorbell-timing
Draft

fix(workflows): bound doorbell delivery freshness#5898
loganj wants to merge 6 commits into
fix/workflows-discoveryfrom
fix/workflow-doorbell-timing

Conversation

@loganj

@loganj loganj commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

🤖

Summary

A workflow doorbell captured from the relay could previously be replayed indefinitely after an ACP restart because semantic deduplication is intentionally process-local. That left an old, once-valid delivery able to ring again long after the owner triggered the workflow.

This change makes doorbell freshness an admission requirement: ACP accepts a relay-signed workflow doorbell only when the doorbell's signed created_at is within five minutes of the harness's local clock. The check is symmetric and runs before cause verification. The relay-signed timestamp is the relay's claim of when it rang the doorbell; it deliberately does not use the original trigger time, so workflows that spend minutes in delay steps or hours awaiting approval remain valid.

Security effect

This narrows three concrete attack paths:

  • Captured-doorbell replay: the captured doorbell is dead after five minutes. Nobody—including the relay—can re-fire yesterday's exact doorbell because changing its signed timestamp changes the event and requires a new signature.
  • Clock games: backdated or future-dated doorbells crafted to slip past process-local deduplication fail the symmetric freshness check. Exactly ±300 seconds is accepted; ±301 seconds is rejected.
  • Relay-key compromise blast radius: combined with semantic deduplication and refetching the owner-signed cause, a stolen relay key gets at most one firing per unique cause within the five-minute window, and only for workflows and trigger content the owner already signed. It does not grant authority to invent owner-approved workflow content.

Schedule causes additionally require the relay's canonical RFC 3339 encoding before semantic deduplication, preventing equivalent timestamp spellings from creating distinct dedup keys.

Deliberate residuals

  • A live compromised relay can spam newly signed, fresh doorbells. This is nuisance-only rather than an authority escalation: ACP still refetches and verifies owner-signed workflow definitions and causes, and semantic deduplication admits at most one firing per unique cause during the process lifetime. Preventing a trusted relay key from sending fresh signed events requires relay-side key containment or durable client state, so it is outside this narrow admission fix.
  • A malicious or unavailable relay can silently suppress doorbells. That is a liveness failure, not an authorization failure, and a client cannot recover a delivery it never receives. Relay availability and delivery guarantees are therefore out of scope.

This PR is stacked on #2737 (fix/workflows-discovery) and should land after it. Durable restart-resistant deduplication and webhook trust changes remain intentionally out of scope.

Related issue

Follow-up to #2737 and #5871.

Testing

  • cargo test -p buzz-workflow — 156 passed, 2 ignored
  • cargo test -p buzz-acp — 788 passed; 9 lifecycle tests passed
  • cargo clippy -p buzz-acp --all-targets -- -D warnings
  • Release-binary live-local relay + CLI + real ACP harness at exact head 7dcae6095: owner manual trigger produced run ac021093-97a7-4261-a71e-315043d5f146, fresh relay doorbell b76bdec2…, and agent-signed LIVE_MANUAL_ACTION reply f9a3850b… to that exact doorbell.
  • No UI change; screenshots are not applicable.

npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je and others added 6 commits August 14, 2026 07:14
Signed-off-by: npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
Signed-off-by: npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
Resolve the Desktop workflow overview against the authenticated user's active channel memberships in PostgreSQL, with bounded keyset pagination. Remove the multi-channel request array and SQL pushdown path while preserving exact single-channel reads.

Signed-off-by: npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
Signed-off-by: Brother Darryl <146fb160a3266e6165bfa385f6048c975eda9e21cf65da097a0b5ea7952532a5@buzz.block.builderlab.xyz>
🤖
## Summary

Workflow messages still wake the managed agent named by their
owner-signed definition, but the relay is now only a doorbell. It
carries pointers to the exact workflow revision and cause; ACP
independently verifies those signed artifacts, reconstructs trigger
input, and renders the prompt locally. Relay-authored prose is never
shown to the agent or treated as owner authority.

Before this change, a compromised relay signing key could attach
`workflow-owner` to chosen message content. After this change, ACP
grants the workflow owner principal only when the doorbell, owner-signed
kind `30620` definition, referenced step, channel, and cause all verify.
Invalid workflow-shaped deliveries are dropped rather than falling
through as ordinary relay messages.

### Doorbell behavior

- Workflow rows retain the exact kind `30620` event ID that materialized
them. Existing rows remain nullable and fail closed until the definition
is saved again.
- Relay events contain the definition event/step and one typed cause:
signed event, signed manual command, deterministic schedule slot, or
webhook cargo.
- ACP refetches and verifies the owner-signed definition. It also
refetches signed event/command causes, reconstructs their trigger
context, rechecks that the signed trigger matches the definition,
validates exact cron/interval slots, and renders `send_message` text
itself.
- Manual commands from an agent's cryptographically verified human owner
remain authorized, but command JSON deliberately no longer populates
arbitrary webhook fields. Unrelated channel members remain denied. If
parameterized manual runs are added later, those parameters must return
as explicitly untrusted cargo under the same boundary as webhooks rather
than inheriting owner authority.
- Prior workflow-step outputs cannot be independently reconstructed, so
`send_message` templates and conditions that depend on them fail closed
rather than treating unresolved placeholders as owner-authorized text.
- Mention wake targets are resolved only from the owner-signed template.
`@Name` text arriving through a signed source event or webhook field
cannot steer a different agent.

### Webhooks and replay controls

Webhooks have no signed source event. Their JSON therefore travels as
explicitly untrusted external cargo, can fill only `{{trigger.*}}` slots
declared by the owner-signed template, and is labelled as untrusted in
the prompt. ACP applies a per-definition token bucket (five immediate
turns, refilling at five per minute) and caps serialized cargo at 61,440
bytes.

Signed event, command, and schedule causes are deduplicated by
definition revision × semantic cause. Dedupe is intentionally
process-lifetime/in-memory: an ACP restart can admit a replay again.
Schedule causes must be exact authorized cron occurrences or interval
boundaries; wall-clock freshness/skew policy is deferred.

### Rollout

This is a coordinated protocol change. New ACP drops old relay workflow
messages because they lack the `doorbell-v1` pointer shape. Old ACP does
not recognize new doorbells as delegated owner messages, so owner-only
policy rejects them. Existing database rows without a definition event
ID do not fire a doorbell until the workflow is re-saved.

### Related issue

Security correction stacked directly on #2737. The owner-trigger
authorization in #5854 remains separate and will be restacked after this
correction merges into #2737.

### Testing

- `cargo test -p buzz-workflow` — 156 passed, 2 Postgres-gated ignored
- `cargo test -p buzz-acp` — 783 unit + 9 lifecycle passed
- `cargo test -p buzz-db migration --lib` — 10 passed, 6 Postgres-gated
ignored
- `cargo test -p buzz-relay --no-run`
- `cargo test -p buzz-test-client --test e2e_workflow_agent_owner
--no-run`
- `cargo clippy -p buzz-acp -p buzz-workflow -p buzz-relay -p
buzz-test-client --all-targets`
- `cargo fmt --check` and `git diff --check`

---------

Signed-off-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
Co-authored-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
Signed-off-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
@loganj
loganj force-pushed the fix/workflows-discovery branch 3 times, most recently from f78ab0e to aa8f8f6 Compare August 19, 2026 18:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant