Skip to content

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

Merged
nutt-adam merged 8 commits into
mainfrom
auto/issue-86-20260320222548
Mar 20, 2026
Merged

[auto] #86 feat: webhook triggers — GitHub/Slack events start agent workflows#97
nutt-adam merged 8 commits into
mainfrom
auto/issue-86-20260320222548

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 support with a generic HTTP webhook endpoint, configurable webhook rules, trigger matching (including wildcards), and template-driven payload expansion.
  • Improvements

    • Idempotent/deduplicated deliveries, serialized dispatch to avoid races, persistent event logging, and enforced 1MB request body limit. Config validation added for webhook rules and webhook config field included in app config. Profile-aware agent capacity counting.
  • Tests

    • Added/updated tests for webhook parsing, matching, templating, logging, and profile compatibility.

@coderabbitai

coderabbitai Bot commented Mar 20, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a webhook subsystem: new webhook module, POST /v1/webhooks/generic handler with 1MB body cap, idempotency/deduplication, trigger matching, template expansion, JSONL event logging, serialized dispatch to workflows or agents, and config/schema additions for webhooks. Also makes agent concurrency/accounting profile-aware.

Changes

Cohort / File(s) Summary
Webhook runtime & handler
src/cli/serve.rs, src/webhook.rs, src/main.rs
New webhook module and HTTP route POST /v1/webhooks/generic: JSON body parsing (source/event/workspace/payload), 1MB read cap, idempotency key derivation/lookup/save, trigger matching, template expansion, JSONL event logging, and serialized dispatch to workflows (run) or agents (send).
Config schema & validation
src/config/mod.rs
Added pub webhooks: Vec<WebhookConfig> to TuttiConfig, new WebhookConfig struct (source, events, workflow?, agent?, prompt?), serde defaults/rename, and validate_webhooks() enforcing source non-empty and exactly one of workflow/agent. Unit tests for serde/validation added.
Profile-aware agent counting & launch logic
src/cli/up.rs
Introduced agent_uses_profile(...); switched to profile-filtered running-agent counts and tightened concurrency checks so limits apply only to compatible agents. Added unit test for runtime/profile compatibility.
Test helpers updated for new field
src/automation/mod.rs, src/budget/mod.rs, src/cli/doctor.rs, src/cli/handoff.rs, src/cli/watch.rs
Added webhooks: vec![] to test TuttiConfig literals across multiple test helpers to match the new config field; no runtime behavior changes.
Tests & utilities
src/webhook.rs (tests), other test files
Added/adjusted unit tests for webhook matching, template expansion, logging, and profile compatibility; minor test helper adjustments.

Sequence Diagram(s)

sequenceDiagram
participant Client
participant HTTP as HTTPServer
participant Deduper as Deduplicator
participant Resolver as WorkspaceResolver
participant Matcher as TriggerMatcher
participant Dispatcher as Dispatcher
participant Workflow as WorkflowRunner
participant Agent as AgentSender

Client->>HTTP: POST /v1/webhooks/generic {body, headers}
HTTP->>HTTP: enforce 1MB body cap
HTTP->>Deduper: compute/lookup dedup key (headers/body)
Deduper-->>HTTP: cached response? / none
alt cached
HTTP-->>Client: 200 cached response
else not cached
HTTP->>HTTP: parse JSON -> source,event,workspace,payload
HTTP->>Resolver: resolve_action_workspace(workspace_hint)
Resolver-->>HTTP: workspace
HTTP->>Matcher: match_triggers(source,event)
Matcher-->>HTTP: matched_rules
alt no matches
HTTP->>WebhookLog: log_event(matched=false)
Deduper->>Deduper: save response under dedup key
HTTP-->>Client: 200 {"matched":false,"triggers_fired":0}
else matches
HTTP->>Dispatcher: acquire WEBHOOK_DISPATCH_LOCK
loop per matched_rule
alt rule.workflow
Dispatcher->>Workflow: run(workspace, workflow, payload)
Workflow-->>Dispatcher: workflow result
else rule.agent
Dispatcher->>Agent: expand_template & send(workspace, agent, prompt)
Agent-->>Dispatcher: send result
end
end
Dispatcher->>WebhookLog: log_event(matched=true)
Deduper->>Deduper: save response under dedup key
HTTP-->>Client: 200 {"matched":true,"triggers_fired":n,"dispatched":[...]}
end
end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Poem

🐇
I hopped through configs, webhooks in tow,
Matched events, expanded prompts aglow,
Dedup kept rhythm, locks kept the queue,
Workflows and agents woke anew—
My paws tapped code; the logs said "phew."

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is largely incomplete; it only provides a high-level status checklist without the required template sections like summary, versioning details (no checkbox confirmation or SemVer selection), validation status, or release information. Complete the PR description template by filling in: (1) specific summary with issue reference and detailed change list, (2) versioning section with Cargo.toml/CHANGELOG.md confirmation and explicit SemVer selection (PATCH/MINOR/MAJOR with rationale), (3) validation confirmation of test and CI status, and (4) release tagging plan.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main feature: webhook triggers that dispatch GitHub/Slack events to agent workflows, matching the comprehensive changes across config, webhook matching, and CLI routing.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% 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-20260320222548

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.

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/cli/up.rs (1)

154-167: ⚠️ Potential issue | 🟠 Major

Inconsistent profile-awareness between run() and run_all().

The single-workspace run() function doesn't check agent_uses_profile before blocking or incrementing, unlike run_all(). In a mixed-runtime workspace (e.g., claude-code + codex agents) with a Claude profile limit:

  1. Line 155: The capacity check blocks all agents when the Claude limit is reached, even codex agents that don't use the Claude profile.
  2. Line 334: The increment applies unconditionally, incorrectly counting codex agents against the Claude profile.
🐛 Proposed fix to add profile compatibility check

For the capacity check (around line 154):

         if let Some(limit) = &profile_limit
+            && global.as_ref().is_some_and(|g| agent_uses_profile(&config, g, agent, &limit.profile_name))
             && active_for_profile >= limit.max_concurrent
         {

For the post-launch increment (around line 333):

         launched.push((agent.name.clone(), session, runtime_name));
-        if profile_limit.is_some() {
+        if let Some(limit) = &profile_limit
+            && global.as_ref().is_some_and(|g| agent_uses_profile(&config, g, agent, &limit.profile_name))
+        {
             active_for_profile += 1;
         }

Also applies to: 331-335

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/up.rs` around lines 154 - 167, The run() path is incorrectly applying
a profile limit to agents that may not use that profile; update the capacity
check and the post-launch increment to first verify the agent actually uses the
profile (use the same agent_uses_profile predicate used by run_all()) before
comparing against limit.max_concurrent or incrementing active_for_profile;
specifically, wrap the check that currently sets refused_by_limit and the block
that increments the profile counter (references: run(), profile_limit,
active_for_profile, limit.max_concurrent, refused_by_limit, agent.name, and the
post-launch increment near the existing code around lines ~331-335) so they only
run when agent_uses_profile(agent, &limit.profile_name) is true.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/cli/up.rs`:
- Around line 154-167: The run() path is incorrectly applying a profile limit to
agents that may not use that profile; update the capacity check and the
post-launch increment to first verify the agent actually uses the profile (use
the same agent_uses_profile predicate used by run_all()) before comparing
against limit.max_concurrent or incrementing active_for_profile; specifically,
wrap the check that currently sets refused_by_limit and the block that
increments the profile counter (references: run(), profile_limit,
active_for_profile, limit.max_concurrent, refused_by_limit, agent.name, and the
post-launch increment near the existing code around lines ~331-335) so they only
run when agent_uses_profile(agent, &limit.profile_name) is true.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 50a21b65-bee7-48d3-add6-6e59df2f4b3e

📥 Commits

Reviewing files that changed from the base of the PR and between 78b11ad and 236cd27.

📒 Files selected for processing (2)
  • src/cli/serve.rs
  • src/cli/up.rs

coderabbitai[bot]
coderabbitai Bot previously approved these changes Mar 20, 2026
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 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 766-818: The webhook handler currently loops over matched triggers
(matched -> wh) and unconditionally calls super::run::run or super::send::run,
causing duplicate non-idempotent executions; add idempotency/deduplication by
deriving or reading a delivery key (e.g., a header or fingerprint from
source/event_type/wh) and performing an atomic check-and-set against a durable
idempotency store before dispatching each trigger (check store for key -> if
present skip and don't push to dispatched; if absent insert key and then call
super::run::run or super::send::run). Ensure the check-and-set is atomic to
prevent races and update the dispatched array and triggers_fired count to
reflect only newly executed actions; reference the loop over matched,
wh.workflow/wh.agent, dispatched, and the calls to super::run::run and
super::send::run when implementing.
- Around line 709-723: The current Content-Length check is bypassable because
read_json_body(request) still consumes the whole stream; change route_webhook to
read the request body through a bounded reader limited to WEBHOOK_MAX_BODY_BYTES
+ 1 (e.g., use a Take wrapper on the Request body or otherwise read at most that
many bytes), read into a buffer, reject with an error if the actual bytes read
exceed WEBHOOK_MAX_BODY_BYTES, and then parse the JSON from that buffer instead
of calling read_json_body; update references to Request and read_json_body
accordingly so parsing uses the bounded buffer and not the unbounded stream.
- Around line 769-800: The issue is that with_project_root() calls
std::env::set_current_dir() which mutates global CWD and races when HTTP
requests are handled concurrently by
start_control_http_server()/route_webhook(), causing run::run and send::run to
execute in the wrong workspace; fix by removing dependence on process-global CWD
in the webhook dispatch paths: either (preferred) modify run::run and send::run
(and any helpers they call) to accept an explicit project_root/Path parameter
and use that for all file/path operations instead of relying on
set_current_dir(), then call those new APIs from route_webhook with
target.project_root; or if changing APIs is not feasible, serialize CWD changes
with a global Mutex around with_project_root() so only one request can set CWD
at a time (ensuring the mutex guards the entire call into run::run or
send::run).

In `@src/cli/up.rs`:
- Around line 1340-1373: The profile concurrency gate is still applied globally
and blocks agents with incompatible runtimes; update the checks in run() (both
spots currently around lines 154-167 and 333-335) and in run_all() (around lines
1541-1549) to only consider agents that actually use the target profile by
invoking agent_uses_profile(config, global, agent, profile_name) when
computing/inspecting concurrent counts; replace or augment any existing calls
that use count_running_agents_for_profile or raw max_concurrent comparisons so
they filter agents with agent_uses_profile before enforcing max_concurrent,
ensuring incompatible runtimes are not counted against the profile limit.

In `@src/config/mod.rs`:
- Around line 315-331: TuttiConfig::validate() currently skips checking
WebhookConfig entries; update validate to iterate over self.webhooks and
enforce: WebhookConfig.source is non-empty, exactly one of
WebhookConfig.workflow or WebhookConfig.agent is present (reject both or
neither), and if workflow is set it exists in self.workflows (or the workflows
collection used in config), and if agent is set it exists in self.agents (or the
agents collection); return a clear Err for the first invalid webhook mentioning
the offending source/index and the missing/unknown name so invalid webhooks fail
at startup.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b45af228-4b66-4018-ac65-9fdfbbec474a

📥 Commits

Reviewing files that changed from the base of the PR and between 236cd27 and ae93ab1.

📒 Files selected for processing (8)
  • 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
✅ Files skipped from review due to trivial changes (3)
  • src/cli/doctor.rs
  • src/budget/mod.rs
  • src/automation/mod.rs

Comment thread src/cli/serve.rs
Comment thread src/cli/serve.rs
Comment thread src/cli/serve.rs
Comment thread src/cli/up.rs
Comment thread src/config/mod.rs
- Enforce webhook body size cap while reading via take() instead of
  only checking Content-Length header (serve.rs)
- Add webhook deduplication using X-GitHub-Delivery header,
  Idempotency-Key, or payload hash to prevent duplicate dispatches
  from retries (serve.rs)
- Serialize webhook dispatches with a mutex to prevent concurrent
  cwd races from with_project_root (serve.rs)
- Only enforce profile max_concurrent limit against agents that
  actually use the profile's runtime (up.rs)
- Add webhook config validation for workflow and agent references
  (config/mod.rs)
- Extract webhook matching, template expansion, and event logging
  into src/webhook.rs module

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

@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: 1

♻️ Duplicate comments (2)
src/cli/serve.rs (2)

709-712: ⚠️ Potential issue | 🔴 Critical

The CWD mutex still leaves cross-thread races open.

This guard is only taken in route_webhook(). /v1/actions/* and the recovery tick still call with_project_root() without the same mutex, so an action/recovery thread can still interleave with a webhook dispatch and run commands in the wrong workspace. The serialization needs to live in with_project_root() itself, or these paths need to stop mutating process-global CWD entirely.

Also applies to: 757-761

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/serve.rs` around lines 709 - 712, The current Mutex
(WEBHOOK_DISPATCH_LOCK) only guards route_webhook(), leaving with_project_root()
calls from /v1/actions/* and the recovery tick unprotected and allowing CWD
races; move the serialization into with_project_root() itself by acquiring a
global static Mutex (or reuse/rename WEBHOOK_DISPATCH_LOCK to something like
PROJECT_ROOT_CWD_LOCK) at the start of with_project_root() so every caller that
mutates the process CWD is serialized, and remove or stop using the ad-hoc lock
in route_webhook(); alternatively, change with_project_root() and all callers to
avoid mutating the global CWD entirely if you prefer not to use a global lock.

735-738: ⚠️ Potential issue | 🔴 Critical

Webhook idempotency is still non-atomic.

idempotency_lookup() runs before the critical section, and the key is only saved after every matched trigger succeeds. Two identical deliveries can both miss the cache before either saves, and a retry after trigger 1 succeeds / trigger 2 fails will replay trigger 1. The check-and-set needs to happen inside the same atomic section, with per-trigger or in-progress state if partial failures should be retry-safe.

Also applies to: 757-761, 763-830

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/serve.rs` around lines 735 - 738, The idempotency check
(webhook_dedup_key + idempotency_lookup) occurs outside the critical section so
two concurrent deliveries can both miss the cache; change the flow to an atomic
claim pattern: implement a new idempotency_claim (or update idempotency_lookup
to support claim semantics) that does an atomic check-and-set (or DB
transaction/row-level lock) to insert an "in-progress" marker for the dedup_key
before processing triggers, and update that marker to the final response on
success or a failure/cleared state on error; update all callsites (the webhook
handling block around webhook_dedup_key/idempotency_lookup and the other ranges
noted) to use this claim API so concurrent deliveries cannot both proceed.
🧹 Nitpick comments (1)
src/webhook.rs (1)

6-22: Keep these helpers off the public API surface.

These are internal helpers, but the module currently exposes pub fn signatures returning bare Vec/String/(). Either lower the visibility boundary here or wrap them in a public API that returns Result<_, TuttiError> so the module stays aligned with the repo’s public-function rule.

As per coding guidelines, "All public functions must return Result<T, TuttiError>".

Also applies to: 27-45, 65-90

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/webhook.rs` around lines 6 - 22, The function match_triggers is an
internal helper but is currently public; either make it non-public (remove the
pub) or change its signature to return Result<Vec<&'a WebhookConfig>,
TuttiError> and map any internal failure into a TuttiError so it conforms to the
rule "All public functions must return Result<T, TuttiError>"; update any call
sites accordingly and apply the same visibility/signature fix to the other
internal helpers noted (the functions around 27-45 and 65-90) so they are either
private or return Result with TuttiError, referencing the WebhookConfig and
TuttiError types when making the change.
🤖 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/config/mod.rs`:
- Around line 677-703: The webhook validation loop in src/config/mod.rs
currently allows a webhook with both workflow set and prompt set which will be
ignored at runtime in route_webhook; update the validation in the for (i, wh) in
self.webhooks.iter().enumerate() block to reject webhooks where
wh.workflow.is_some() && wh.prompt.is_some() (or alternatively wire wh.prompt
through to route_webhook), by returning a TuttiError::ConfigValidation
explaining that webhook[{i}] cannot specify both 'workflow' and 'prompt' (or
that 'prompt' is unsupported for workflow webhooks) so the config fails fast
instead of silently dropping the prompt.

---

Duplicate comments:
In `@src/cli/serve.rs`:
- Around line 709-712: The current Mutex (WEBHOOK_DISPATCH_LOCK) only guards
route_webhook(), leaving with_project_root() calls from /v1/actions/* and the
recovery tick unprotected and allowing CWD races; move the serialization into
with_project_root() itself by acquiring a global static Mutex (or reuse/rename
WEBHOOK_DISPATCH_LOCK to something like PROJECT_ROOT_CWD_LOCK) at the start of
with_project_root() so every caller that mutates the process CWD is serialized,
and remove or stop using the ad-hoc lock in route_webhook(); alternatively,
change with_project_root() and all callers to avoid mutating the global CWD
entirely if you prefer not to use a global lock.
- Around line 735-738: The idempotency check (webhook_dedup_key +
idempotency_lookup) occurs outside the critical section so two concurrent
deliveries can both miss the cache; change the flow to an atomic claim pattern:
implement a new idempotency_claim (or update idempotency_lookup to support claim
semantics) that does an atomic check-and-set (or DB transaction/row-level lock)
to insert an "in-progress" marker for the dedup_key before processing triggers,
and update that marker to the final response on success or a failure/cleared
state on error; update all callsites (the webhook handling block around
webhook_dedup_key/idempotency_lookup and the other ranges noted) to use this
claim API so concurrent deliveries cannot both proceed.

---

Nitpick comments:
In `@src/webhook.rs`:
- Around line 6-22: The function match_triggers is an internal helper but is
currently public; either make it non-public (remove the pub) or change its
signature to return Result<Vec<&'a WebhookConfig>, TuttiError> and map any
internal failure into a TuttiError so it conforms to the rule "All public
functions must return Result<T, TuttiError>"; update any call sites accordingly
and apply the same visibility/signature fix to the other internal helpers noted
(the functions around 27-45 and 65-90) so they are either private or return
Result with TuttiError, referencing the WebhookConfig and TuttiError types when
making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 086b0a61-0e9c-440b-b995-3a4613111f37

📥 Commits

Reviewing files that changed from the base of the PR and between ae93ab1 and e0f40ce.

📒 Files selected for processing (5)
  • src/cli/serve.rs
  • src/cli/up.rs
  • src/config/mod.rs
  • src/main.rs
  • src/webhook.rs
✅ Files skipped from review due to trivial changes (1)
  • src/main.rs

Comment thread src/config/mod.rs
Prompt is only consumed for direct agent dispatch; a workflow webhook
with prompt silently drops it. Fail at config validation instead.

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

@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: 2

🤖 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 910-916: The current fallback dedup key uses Rust's process-seeded
DefaultHasher (serialized, hasher, hasher.finish(),
format!("webhook:hash:...")), which won't be stable across restarts; replace it
with a stable digest (e.g., SHA-256) by importing a stable hash implementation
(sha2::Sha256 and Digest) and computing the hash over body bytes (or serialized)
to produce a hex string, then use a stable key namespace like
"webhook:sha256:<hex>"; add the sha2 (and hex if needed) crate to Cargo.toml and
update the imports to use Digest instead of DefaultHasher so the dedup key
survives process restarts.
- Around line 823-877: The code currently ignores wh.agent when wh.workflow is
Some because the branch is "if let Some(workflow) { ... } else if let
Some(agent) { ... }"; change the behavior to dispatch both when both are present
by replacing the "else if let Some(agent)" with a separate "if let Some(agent)"
block (or extract the agent-send logic into a helper and call it from both
places) so both super::run::run (for workflow) and super::send::run (for agent)
are invoked, ensure webhook::log_event and pushed entries to dispatched remain
correct (add both "dispatched_workflow" and "dispatched_send" entries when
applicable), and keep error propagation with the existing ? returns;
alternatively, if you prefer mutual exclusivity instead, add validation in
validate_webhooks() to reject configs where both workflow and agent are set.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 82023e51-b0f4-45de-a76e-74529656d2d9

📥 Commits

Reviewing files that changed from the base of the PR and between e0f40ce and 4457945.

📒 Files selected for processing (3)
  • src/cli/serve.rs
  • src/config/mod.rs
  • src/main.rs
✅ Files skipped from review due to trivial changes (1)
  • src/main.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/config/mod.rs

Comment thread src/cli/serve.rs
Comment thread src/cli/serve.rs Outdated
- Reject webhooks that specify both workflow and agent in validation
- Replace DefaultHasher with FNV-1a for deterministic dedup keys
  across server restarts

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

@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: 1

🧹 Nitpick comments (1)
src/cli/serve.rs (1)

940-953: Minor: Error message mentions "webhook" but function is shared.

read_json_body is called for both action endpoints (line 622) and webhooks (line 776), but the error message specifically says "webhook payload too large". Consider a generic message.

Suggested fix
     if body.len() > WEBHOOK_MAX_BODY_BYTES {
         return Err(TuttiError::ConfigValidation(format!(
-            "webhook payload too large (>{WEBHOOK_MAX_BODY_BYTES} bytes)"
+            "request payload too large (>{WEBHOOK_MAX_BODY_BYTES} bytes)"
         )));
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/serve.rs` around lines 940 - 953, The error string in read_json_body
is specific to "webhook" but the function is reused for action endpoints too;
update the TuttiError::ConfigValidation message in read_json_body to a generic
phrase like "payload too large" that does not mention webhook so it correctly
reflects all callers (function: read_json_body, constant:
WEBHOOK_MAX_BODY_BYTES).
🤖 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 848-864: The webhook prompt expansion currently calls
webhook::expand_template(raw_prompt, &payload) and injects the resulting prompt
into super::send::run which allows untrusted payload fields to become executable
instructions; update this flow to either (a) perform escaping/quoting of
interpolated values inside webhook::expand_template (escape control characters,
newlines, template markers and shell/command-like tokens), (b) add an allowlist
mechanism in webhook::expand_template so only approved JSON paths from payload
are substituted (reject or replace others with a safe placeholder), and (c)
surface an operator-facing configuration/documentation flag on the webhook
trigger to explicitly enable raw interpolation; ensure the sanitized/allowlisted
prompt variable (the one passed to super::send::run) is used instead of the raw
expansion and keep the behavior when wh.agent is None unchanged.

---

Nitpick comments:
In `@src/cli/serve.rs`:
- Around line 940-953: The error string in read_json_body is specific to
"webhook" but the function is reused for action endpoints too; update the
TuttiError::ConfigValidation message in read_json_body to a generic phrase like
"payload too large" that does not mention webhook so it correctly reflects all
callers (function: read_json_body, constant: WEBHOOK_MAX_BODY_BYTES).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4d0e15a1-6d44-4d1a-ba39-5aa93350e325

📥 Commits

Reviewing files that changed from the base of the PR and between 4457945 and 40ec0f6.

📒 Files selected for processing (2)
  • src/cli/serve.rs
  • src/config/mod.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/config/mod.rs

Comment thread src/cli/serve.rs
Webhook payloads are untrusted; interpolated values are inserted
verbatim. Document this so operators configure appropriate agent
permissions for webhook-triggered workflows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@nutt-adam
nutt-adam merged commit b8b5978 into main Mar 20, 2026
11 checks passed
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