Skip to content

[auto] #86 feat: webhook triggers — GitHub/Slack events start agent workflows - #98

Closed
nutt-adam wants to merge 3 commits into
mainfrom
auto/issue-86-20260320232116
Closed

[auto] #86 feat: webhook triggers — GitHub/Slack events start agent workflows#98
nutt-adam wants to merge 3 commits into
mainfrom
auto/issue-86-20260320232116

Conversation

@nutt-adam

@nutt-adam nutt-adam commented Mar 20, 2026

Copy link
Copy Markdown
Contributor

Automated SDLC cycle for #86.

  • planner: completed
  • implementation: completed
  • tests: updated
  • docs/changelog: updated
  • version: bumped if required

Summary by CodeRabbit

  • New Features
    • Added webhook API endpoint (POST /v1/webhooks/generic) to receive events and trigger configured workflows or send operations
    • Introduced webhook configuration system enabling event-based automation with source and event-type matching
    • Added template variable expansion support in prompts for dynamic execution based on webhook payloads

Add WebhookConfig struct to TuttiConfig with [[webhook]] TOML array
parsing (source, events, workflow, agent, prompt fields). Add POST
/v1/webhooks/generic endpoint in serve.rs that accepts JSON payloads,
enforces a 1MB size limit, matches against configured triggers by
source and event type, and dispatches via the existing workflow run
or agent send infrastructure. Includes unit tests for config
deserialization.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Mar 20, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@nutt-adam has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 2 minutes and 42 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 15c797d3-4883-4017-939b-aac2e92642a2

📥 Commits

Reviewing files that changed from the base of the PR and between 947cdf1 and 6034453.

📒 Files selected for processing (3)
  • src/cli/serve.rs
  • src/config/mod.rs
  • src/webhook.rs
📝 Walkthrough

Walkthrough

The changes implement webhook support for the Tutti system by introducing a new WebhookConfig struct, extending TuttiConfig with a webhooks field, creating a webhook module with trigger matching and event logging utilities, and adding a POST /v1/webhooks/generic API endpoint that validates payloads, matches triggers, and dispatches workflows or send operations.

Changes

Cohort / File(s) Summary
Webhook Core Infrastructure
src/config/mod.rs, src/webhook.rs, src/main.rs
Added new WebhookConfig struct with source, events, workflow, agent, and prompt fields; extended TuttiConfig with webhooks vector; created webhook module with match_triggers, expand_template, and log_event utilities for trigger matching, template variable substitution, and event logging.
API Endpoint Implementation
src/cli/serve.rs
Added POST /v1/webhooks/generic endpoint with Content-Length validation, JSON parsing, workspace resolution, trigger matching, and dispatch routing to either workflow or send operations; includes per-dispatch event logging.
Test Configuration Updates
src/automation/mod.rs, src/budget/mod.rs, src/cli/doctor.rs, src/cli/handoff.rs, src/cli/up.rs, src/cli/watch.rs
Updated test helper TuttiConfig struct literals to initialize the new webhooks field with empty vectors across multiple test modules.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Server as API Server
    participant Webhook as Webhook Handler
    participant TriggerMatcher as Trigger Matcher
    participant Dispatcher as Dispatcher
    participant EventLog as Event Logger

    Client->>Server: POST /v1/webhooks/generic<br/>(JSON payload)
    Server->>Webhook: Validate Content-Length
    Webhook->>Webhook: Parse JSON body
    Webhook->>Webhook: Extract source, event, workspace
    Webhook->>TriggerMatcher: match_triggers(webhooks,<br/>source, event)
    alt Triggers Match
        TriggerMatcher->>Webhook: Matched configs
        Webhook->>Dispatcher: Dispatch workflow or send
        Dispatcher->>Dispatcher: Execute operation
        Webhook->>EventLog: log_event(matched_rule,<br/>"dispatched")
        Webhook->>Client: 200 OK with matched: true
    else No Triggers Match
        TriggerMatcher->>Webhook: Empty matches
        Webhook->>EventLog: log_event(no rule,<br/>"no_match")
        Webhook->>Client: 200 OK with matched: false
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 A webhook hops through the system with glee,
Matching triggers and templates wild and free,
Events logged to .tutti/state so neat,
Workflows dispatched to the beat of the beat! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is a high-level summary stating automated cycle completion but does not follow the required template structure (missing versioning details, SemVer choice, version number, validation checkboxes, and release information). Complete the description template by specifying version bump details, SemVer choice (MINOR for new webhook capability), selected version number, and confirmation of validation/CI status.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the primary feature: webhook triggers that enable GitHub/Slack events to start agent workflows, matching the changeset's focus on webhook infrastructure.
Docstring Coverage ✅ Passed Docstring coverage is 83.78% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch auto/issue-86-20260320232116

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…vent logging

Create src/webhook.rs with match_triggers(), expand_template(), and
log_event() — extracting trigger-matching logic from inline serve.rs
code into a dedicated module. Template expansion supports {{event.field}}
and {{event.nested.field}} placeholders resolved against the JSON
payload. Webhook events are logged to .tutti/state/webhook-events.jsonl.

Refactor route_webhook in serve.rs to use the new webhook module
functions and apply template expansion to agent prompts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@nutt-adam

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 20, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/cli/serve.rs`:
- Around line 793-797: The code resolves a top-level workspace with
resolve_action_workspace before checking webhook rules, which prevents matching
rules in other workspaces; move the webhook matching call before
resolve_action_workspace so webhook::match_triggers(&target.config.webhooks,
source, event_type) (or an equivalent call that iterates all served workspace
configs) runs using the incoming payload/body and source/event_type first (using
workspace_hint only for optional narrowing), and only call
resolve_action_workspace(targets, workspace_hint) to pick a target after
matching triggers; adjust control flow around workspace_hint, payload,
webhook::match_triggers, and resolve_action_workspace accordingly.
- Around line 812-867: The webhook dispatch loop currently executes workflows or
sends unconditionally (inside the for loop over matched using wh.workflow,
super::run::run and super::send::run) which allows replay; add a delivery-key
idempotency check before dispatching each matched entry: extract a delivery key
from the incoming payload or headers (same key format used by the existing
/v1/actions/* idempotency logic), look up/record that key in the same
idempotency store used elsewhere in this file, and skip dispatch/logging/adding
to dispatched if the key already exists; implement the check at the top of the
loop (before calling with_project_root, super::run::run or super::send::run and
before webhook::log_event) and ensure you persist the delivery key after a
successful dispatch so retries are ignored.
- Around line 771-783: The current Content-Length check is insufficient because
read_json_body can still read unlimited bytes when Content-Length is
missing/invalid; update the flow to enforce WEBHOOK_MAX_BODY_BYTES at the reader
boundary by replacing or wrapping read_json_body with a bounded reader variant
(e.g., read_json_body_limited) that uses
request.as_reader().take((WEBHOOK_MAX_BODY_BYTES + 1) as u64) and reads to a
buffer, then returns a ConfigValidation error if the buffer length exceeds
WEBHOOK_MAX_BODY_BYTES, treats all-whitespace bodies as empty JSON, and finally
parses the buffer with serde_json::from_slice; call this new bounded function
where read_json_body(...) is currently invoked and keep the existing
Content-Length pre-check as a fast-fail optimization.

In `@src/config/mod.rs`:
- Around line 315-331: WebhookConfig allows ambiguous states (both or neither of
workflow/agent); add a validation method on the WebhookConfig struct (e.g., impl
WebhookConfig { pub fn validate(&self) -> Result<(), ConfigError> }) that
returns an error if both workflow and agent are set or if neither is set (and
optionally validate events is not empty), and then call this validate() from the
global config validation path after deserialization so invalid webhook configs
fail at load time rather than at runtime dispatch.

In `@src/webhook.rs`:
- Around line 6-10: The exported helpers match_triggers, expand_template, and
log_event violate the crate rule that public functions must return Result<T,
TuttiError>; either make them internal by removing pub or change their
signatures to return Result with TuttiError and propagate errors through
callers—specifically: for match_triggers and expand_template update their return
types to Result<Vec<&WebhookConfig>, TuttiError> (or appropriate Result<T,
TuttiError>) and adjust callers to handle the Result, and for log_event stop
swallowing append failures by returning Result<(), TuttiError> and propagating
any underlying I/O/logging errors so callers can choose best-effort vs required
behavior; ensure TuttiError variants cover the new failure cases and update all
call sites to handle the Result.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7513b808-3950-4bf5-b53b-b2590c301285

📥 Commits

Reviewing files that changed from the base of the PR and between 78b11ad and 947cdf1.

📒 Files selected for processing (10)
  • src/automation/mod.rs
  • src/budget/mod.rs
  • src/cli/doctor.rs
  • src/cli/handoff.rs
  • src/cli/serve.rs
  • src/cli/up.rs
  • src/cli/watch.rs
  • src/config/mod.rs
  • src/main.rs
  • src/webhook.rs

Comment thread src/cli/serve.rs Outdated
Comment thread src/cli/serve.rs Outdated
Comment thread src/cli/serve.rs
Comment thread src/config/mod.rs
Comment thread src/webhook.rs
1. Enforce 1 MiB body cap via .take() on the reader, not just
   Content-Length header check (serve.rs read_json_body).
2. Match webhook triggers before resolving workspace — scan all targets
   for matching triggers, then resolve workspace from the match.
3. Add replay protection using delivery ID headers (X-GitHub-Delivery,
   X-Delivery-ID, X-Request-Id, Idempotency-Key) or SHA-256 payload
   hash, with a persistent replay store capped at 10k entries.
4. Add webhook config validation: source non-empty, workflow/agent
   mutual exclusivity, referenced names exist, reject prompt on
   workflow webhooks.
5. Make webhook module public functions return Result — log_event,
   is_replay, and record_delivery now propagate errors properly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@nutt-adam

Copy link
Copy Markdown
Contributor Author

Superseded by PR #97 which already delivered the webhook feature for issue #86. This PR was a second automation pass that conflicts with the merged code.

@nutt-adam nutt-adam closed this Mar 20, 2026
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