Commit d0ba8bf
feat(agentex): add Slack Events API url_verification endpoint (#369)
## What
Adds a minimal `POST /slack/events` endpoint that:
- echoes Slack's `url_verification` challenge (the one-time Request URL
ownership check), and
- acks every other event with `200` so Slack stops retrying.
Also whitelists `/slack` from authentication — Slack requests can't
present an SGP principal, so the handshake must reach the route
uncredentialed (same rationale as `/agents/forward`).
## Why
Slack's Events API won't accept a Request URL until a live, publicly
reachable HTTPS endpoint answers the verification challenge. This is the
single piece needed to get past that setup step and mark the URL
**Verified**. Once verified, real logic can be added without
re-verifying (re-verification only triggers if the URL itself changes).
## Scope
Intentionally minimal. Real event handling — signature verification,
dispatch to an agent, and reply delivery — is **not** in this PR and
lands separately.
## Test
- Unit: `TestClient` confirms `url_verification` → `{"challenge": ...}`
and other events → `200 {"ok": true}`.
- `ruff` / `ruff format` clean.
> Note: the `agentex/openapi.yaml` regen pre-commit hook was skipped in
the local commit because it writes a spurious gitignored root-level
`openapi.yaml` under the hook's run environment; the spec in
`agentex/openapi.yaml` was regenerated correctly and is included.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
This PR adds a minimal `POST /slack/events` endpoint to pass Slack's
one-time `url_verification` handshake, echoing the challenge back and
acking all other events with `200`. It also whitelists the `/slack`
prefix from the authentication middleware so Slack's credentialless
requests can reach the route.
- **New route** (`agentex/src/api/routes/slack.py`): echoes `challenge`
for `url_verification` events, returns `{\"ok\": true}` for everything
else; no request-body schema or Slack signature validation.
- **Auth whitelist** (`agentex/src/api/middleware_utils.py`): adds
`\"/slack\"` as a prefix entry, which exempts not just `/slack/events`
but any future `/slack/*` route from authentication by default.
- **Router registration** (`agentex/src/api/app.py`) and OpenAPI spec
(`agentex/openapi.yaml`) are updated accordingly.
<details><summary><h3>Confidence Score: 3/5</h3></summary>
The new Slack endpoint is live and publicly reachable without
credentials from the moment this merges; real event handlers that land
in the follow-up PR will inherit the same unauthenticated surface unless
signature verification is added first.
The endpoint intentionally bypasses authentication and currently does no
signature verification. While the current no-op behavior is harmless,
the open /slack/events surface and the prefix-level auth whitelist
create a footgun for the next PR that adds real event processing.
agentex/src/api/routes/slack.py and agentex/src/api/middleware_utils.py
— signature verification and whitelist scope both need attention before
real event handling is added.
</details>
<details open><summary><h3>Security Review</h3></summary>
- **No Slack request-signature verification**
(`agentex/src/api/routes/slack.py`): The `/slack/events` endpoint
accepts any POST without validating `X-Slack-Signature` /
`X-Slack-Request-Timestamp`. The route is already publicly reachable
without credentials; any actor can forge Slack event payloads today, and
the risk compounds immediately when real event handling is added in the
next PR.
- **Overly broad auth bypass** (`agentex/src/api/middleware_utils.py`):
The whitelist entry `\"/slack\"` uses prefix matching, so all future
`/slack/*` routes are unauthenticated by default unless explicitly
carved out.
</details>
<details><summary><h3>Important Files Changed</h3></summary>
| Filename | Overview |
|----------|----------|
| agentex/src/api/routes/slack.py | New Slack Events ingress endpoint —
handles url_verification challenge and acks all other events; missing
Slack request-signature verification and no JSON parse error handling. |
| agentex/src/api/middleware_utils.py | Adds "/slack" prefix to auth
whitelist; this silently bypasses auth for all future /slack/* routes,
not just /slack/events. |
| agentex/src/api/app.py | Registers the new Slack router —
straightforward include, no issues. |
| agentex/openapi.yaml | Auto-generated OpenAPI spec addition for POST
/slack/events — correct and consistent with the new endpoint. |
</details>
<details><summary><h3>Sequence Diagram</h3></summary>
<a href="#gh-light-mode-only">
```mermaid
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Slack
participant AuthMiddleware
participant SlackEventsRoute
Slack->>AuthMiddleware: POST /slack/events
Note over AuthMiddleware: /slack prefix is whitelisted — auth skipped
AuthMiddleware->>SlackEventsRoute: forward request (no credentials required)
alt "type == "url_verification""
SlackEventsRoute-->>Slack: "200 {"challenge": "..."}"
else any other event type
SlackEventsRoute-->>Slack: "200 {"ok": true}"
end
```
</a>
<a href="#gh-dark-mode-only">
```mermaid
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Slack
participant AuthMiddleware
participant SlackEventsRoute
Slack->>AuthMiddleware: POST /slack/events
Note over AuthMiddleware: /slack prefix is whitelisted — auth skipped
AuthMiddleware->>SlackEventsRoute: forward request (no credentials required)
alt "type == "url_verification""
SlackEventsRoute-->>Slack: "200 {"challenge": "..."}"
else any other event type
SlackEventsRoute-->>Slack: "200 {"ok": true}"
end
```
</a>
</details>
<a
href="https://app.greptile.com/api/ide/cursor?prompt=Fix%20the%20following%203%20code%20review%20issues.%20Work%20through%20them%20one%20at%20a%20time%2C%20proposing%20concise%20fixes.%0A%0A---%0A%0A%23%23%23%20Issue%201%20of%203%0Aagentex%2Fsrc%2Fapi%2Froutes%2Fslack.py%3A20-25%0A**Missing%20Slack%20request-signature%20verification**%0A%0AThe%20endpoint%20accepts%20every%20POST%20without%20checking%20the%20%60X-Slack-Signature%60%20%2F%20%60X-Slack-Request-Timestamp%60%20headers%20that%20Slack%20includes%20on%20every%20request.%20As%20soon%20as%20real%20event%20handling%20lands%20in%20the%20next%20PR%2C%20any%20external%20party%20can%20forge%20Slack%20events%20%28DM%20dispatch%2C%20slash%20commands%2C%20interactive%20callbacks%29%20by%20POSTing%20to%20this%20unauthenticated%20route.%20The%20route%20is%20already%20live%20and%20reachable%20without%20credentials%2C%20so%20the%20attack%20surface%20exists%20from%20the%20moment%20this%20merges%2C%20even%20if%20the%20handling%20is%20currently%20a%20no-op.%20Slack's%20signature%20verification%20is%20a%20one-time%20add%20of%20~10%20lines%20using%20%60hmac.compare_digest%60%20against%20the%20%60SLACK_SIGNING_SECRET%60%3B%20deferring%20it%20past%20the%20first%20real%20handler%20significantly%20increases%20the%20window%20for%20exploitation.%0A%0A%23%23%23%20Issue%202%20of%203%0Aagentex%2Fsrc%2Fapi%2Fmiddleware_utils.py%3A32-33%0A**Prefix%20whitelist%20silently%20covers%20all%20future%20%60%2Fslack%2F*%60%20routes**%0A%0A%60%22%2Fslack%22%60%20uses%20the%20boundary-aware%20prefix%20match%20%28%60startswith%28%22%2Fslack%2F%22%29%60%29%2C%20so%20every%20future%20route%20added%20under%20this%20prefix%20%E2%80%94%20management%20endpoints%2C%20admin%20callbacks%2C%20webhook%20delivery%20%E2%80%94%20will%20be%20unauthenticated%20by%20default.%20Contrast%20with%20%60%2Fagents%2Fregister%60%20and%20%60%2Fagents%2Fforward%60%2C%20which%20are%20explicit%20paths.%20If%20a%20future%20route%20under%20%60%2Fslack%2F%60%20should%20require%20authentication%2C%20the%20developer%20must%20remember%20to%20override%20or%20restructure%20the%20whitelist.%20It%20may%20be%20safer%20to%20add%20the%20exact%20path%20%60%22%2Fslack%2Fevents%22%60%20here%20to%20limit%20the%20blast%20radius.%0A%0A%23%23%23%20Issue%203%20of%203%0Aagentex%2Fsrc%2Fapi%2Froutes%2Fslack.py%3A21-22%0A**Unhandled%20JSON%20parse%20error**%0A%0A%60await%20request.json%28%29%60%20raises%20an%20exception%20if%20the%20body%20is%20not%20valid%20JSON%20%28e.g.%20a%20malformed%20webhook%20retry%20or%20a%20probe%29%2C%20yielding%20an%20unhandled%20500%20rather%20than%20a%20graceful%20400.%20Wrapping%20this%20in%20a%20%60try%2Fexcept%60%20with%20an%20early%20%60raise%20HTTPException%28status_code%3D400%29%60%20keeps%20the%20error%20surface%20clean%20and%20avoids%20surfacing%20stack%20traces%20in%20logs.%0A%0A&pr=369&platform=github"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCursorDark.svg?v=6"><source
media="(prefers-color-scheme: light)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCursor.svg?v=6"><img
alt="Fix All in Cursor"
src="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCursor.svg?v=6"></picture></a>
<a
href="https://app.greptile.com/ide/claude-code?prompt=Fix%20the%20following%203%20code%20review%20issues.%20Work%20through%20them%20one%20at%20a%20time%2C%20proposing%20concise%20fixes.%0A%0A---%0A%0A%23%23%23%20Issue%201%20of%203%0Aagentex%2Fsrc%2Fapi%2Froutes%2Fslack.py%3A20-25%0A**Missing%20Slack%20request-signature%20verification**%0A%0AThe%20endpoint%20accepts%20every%20POST%20without%20checking%20the%20%60X-Slack-Signature%60%20%2F%20%60X-Slack-Request-Timestamp%60%20headers%20that%20Slack%20includes%20on%20every%20request.%20As%20soon%20as%20real%20event%20handling%20lands%20in%20the%20next%20PR%2C%20any%20external%20party%20can%20forge%20Slack%20events%20%28DM%20dispatch%2C%20slash%20commands%2C%20interactive%20callbacks%29%20by%20POSTing%20to%20this%20unauthenticated%20route.%20The%20route%20is%20already%20live%20and%20reachable%20without%20credentials%2C%20so%20the%20attack%20surface%20exists%20from%20the%20moment%20this%20merges%2C%20even%20if%20the%20handling%20is%20currently%20a%20no-op.%20Slack's%20signature%20verification%20is%20a%20one-time%20add%20of%20~10%20lines%20using%20%60hmac.compare_digest%60%20against%20the%20%60SLACK_SIGNING_SECRET%60%3B%20deferring%20it%20past%20the%20first%20real%20handler%20significantly%20increases%20the%20window%20for%20exploitation.%0A%0A%23%23%23%20Issue%202%20of%203%0Aagentex%2Fsrc%2Fapi%2Fmiddleware_utils.py%3A32-33%0A**Prefix%20whitelist%20silently%20covers%20all%20future%20%60%2Fslack%2F*%60%20routes**%0A%0A%60%22%2Fslack%22%60%20uses%20the%20boundary-aware%20prefix%20match%20%28%60startswith%28%22%2Fslack%2F%22%29%60%29%2C%20so%20every%20future%20route%20added%20under%20this%20prefix%20%E2%80%94%20management%20endpoints%2C%20admin%20callbacks%2C%20webhook%20delivery%20%E2%80%94%20will%20be%20unauthenticated%20by%20default.%20Contrast%20with%20%60%2Fagents%2Fregister%60%20and%20%60%2Fagents%2Fforward%60%2C%20which%20are%20explicit%20paths.%20If%20a%20future%20route%20under%20%60%2Fslack%2F%60%20should%20require%20authentication%2C%20the%20developer%20must%20remember%20to%20override%20or%20restructure%20the%20whitelist.%20It%20may%20be%20safer%20to%20add%20the%20exact%20path%20%60%22%2Fslack%2Fevents%22%60%20here%20to%20limit%20the%20blast%20radius.%0A%0A%23%23%23%20Issue%203%20of%203%0Aagentex%2Fsrc%2Fapi%2Froutes%2Fslack.py%3A21-22%0A**Unhandled%20JSON%20parse%20error**%0A%0A%60await%20request.json%28%29%60%20raises%20an%20exception%20if%20the%20body%20is%20not%20valid%20JSON%20%28e.g.%20a%20malformed%20webhook%20retry%20or%20a%20probe%29%2C%20yielding%20an%20unhandled%20500%20rather%20than%20a%20graceful%20400.%20Wrapping%20this%20in%20a%20%60try%2Fexcept%60%20with%20an%20early%20%60raise%20HTTPException%28status_code%3D400%29%60%20keeps%20the%20error%20surface%20clean%20and%20avoids%20surfacing%20stack%20traces%20in%20logs.%0A%0A&repo=scaleapi%2Fscale-agentex&pr=369&platform=github"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInClaudeDark.svg?v=6"><source
media="(prefers-color-scheme: light)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInClaude.svg?v=6"><img
alt="Fix All in Claude Code"
src="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInClaude.svg?v=6"></picture></a>
<a
href="https://app.greptile.com/api/ide/codex?prompt=IMPORTANT%3A%20Work%20in%20the%20repository%20%22scaleapi%2Fscale-agentex%22%20on%20the%20existing%20branch%20%22mc%2Fslack-dummy-endpoint%22.%20Checkout%20that%20branch%20%E2%80%94%20do%20NOT%20create%20a%20new%20branch%20or%20open%20a%20new%20PR.%20Push%20your%20changes%20to%20%22mc%2Fslack-dummy-endpoint%22.%0A%0AFix%20the%20following%203%20code%20review%20issues.%20Work%20through%20them%20one%20at%20a%20time%2C%20proposing%20concise%20fixes.%0A%0A---%0A%0A%23%23%23%20Issue%201%20of%203%0Aagentex%2Fsrc%2Fapi%2Froutes%2Fslack.py%3A20-25%0A**Missing%20Slack%20request-signature%20verification**%0A%0AThe%20endpoint%20accepts%20every%20POST%20without%20checking%20the%20%60X-Slack-Signature%60%20%2F%20%60X-Slack-Request-Timestamp%60%20headers%20that%20Slack%20includes%20on%20every%20request.%20As%20soon%20as%20real%20event%20handling%20lands%20in%20the%20next%20PR%2C%20any%20external%20party%20can%20forge%20Slack%20events%20%28DM%20dispatch%2C%20slash%20commands%2C%20interactive%20callbacks%29%20by%20POSTing%20to%20this%20unauthenticated%20route.%20The%20route%20is%20already%20live%20and%20reachable%20without%20credentials%2C%20so%20the%20attack%20surface%20exists%20from%20the%20moment%20this%20merges%2C%20even%20if%20the%20handling%20is%20currently%20a%20no-op.%20Slack's%20signature%20verification%20is%20a%20one-time%20add%20of%20~10%20lines%20using%20%60hmac.compare_digest%60%20against%20the%20%60SLACK_SIGNING_SECRET%60%3B%20deferring%20it%20past%20the%20first%20real%20handler%20significantly%20increases%20the%20window%20for%20exploitation.%0A%0A%23%23%23%20Issue%202%20of%203%0Aagentex%2Fsrc%2Fapi%2Fmiddleware_utils.py%3A32-33%0A**Prefix%20whitelist%20silently%20covers%20all%20future%20%60%2Fslack%2F*%60%20routes**%0A%0A%60%22%2Fslack%22%60%20uses%20the%20boundary-aware%20prefix%20match%20%28%60startswith%28%22%2Fslack%2F%22%29%60%29%2C%20so%20every%20future%20route%20added%20under%20this%20prefix%20%E2%80%94%20management%20endpoints%2C%20admin%20callbacks%2C%20webhook%20delivery%20%E2%80%94%20will%20be%20unauthenticated%20by%20default.%20Contrast%20with%20%60%2Fagents%2Fregister%60%20and%20%60%2Fagents%2Fforward%60%2C%20which%20are%20explicit%20paths.%20If%20a%20future%20route%20under%20%60%2Fslack%2F%60%20should%20require%20authentication%2C%20the%20developer%20must%20remember%20to%20override%20or%20restructure%20the%20whitelist.%20It%20may%20be%20safer%20to%20add%20the%20exact%20path%20%60%22%2Fslack%2Fevents%22%60%20here%20to%20limit%20the%20blast%20radius.%0A%0A%23%23%23%20Issue%203%20of%203%0Aagentex%2Fsrc%2Fapi%2Froutes%2Fslack.py%3A21-22%0A**Unhandled%20JSON%20parse%20error**%0A%0A%60await%20request.json%28%29%60%20raises%20an%20exception%20if%20the%20body%20is%20not%20valid%20JSON%20%28e.g.%20a%20malformed%20webhook%20retry%20or%20a%20probe%29%2C%20yielding%20an%20unhandled%20500%20rather%20than%20a%20graceful%20400.%20Wrapping%20this%20in%20a%20%60try%2Fexcept%60%20with%20an%20early%20%60raise%20HTTPException%28status_code%3D400%29%60%20keeps%20the%20error%20surface%20clean%20and%20avoids%20surfacing%20stack%20traces%20in%20logs.%0A%0A&repo=scaleapi%2Fscale-agentex&pr=369&platform=github"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCodexDark.svg?v=6"><source
media="(prefers-color-scheme: light)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCodex.svg?v=6"><img
alt="Fix All in Codex"
src="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCodex.svg?v=6"></picture></a>
<details><summary>Prompt To Fix All With AI</summary>
`````markdown
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 3
agentex/src/api/routes/slack.py:20-25
**Missing Slack request-signature verification**
The endpoint accepts every POST without checking the `X-Slack-Signature` / `X-Slack-Request-Timestamp` headers that Slack includes on every request. As soon as real event handling lands in the next PR, any external party can forge Slack events (DM dispatch, slash commands, interactive callbacks) by POSTing to this unauthenticated route. The route is already live and reachable without credentials, so the attack surface exists from the moment this merges, even if the handling is currently a no-op. Slack's signature verification is a one-time add of ~10 lines using `hmac.compare_digest` against the `SLACK_SIGNING_SECRET`; deferring it past the first real handler significantly increases the window for exploitation.
### Issue 2 of 3
agentex/src/api/middleware_utils.py:32-33
**Prefix whitelist silently covers all future `/slack/*` routes**
`"/slack"` uses the boundary-aware prefix match (`startswith("/slack/")`), so every future route added under this prefix — management endpoints, admin callbacks, webhook delivery — will be unauthenticated by default. Contrast with `/agents/register` and `/agents/forward`, which are explicit paths. If a future route under `/slack/` should require authentication, the developer must remember to override or restructure the whitelist. It may be safer to add the exact path `"/slack/events"` here to limit the blast radius.
### Issue 3 of 3
agentex/src/api/routes/slack.py:21-22
**Unhandled JSON parse error**
`await request.json()` raises an exception if the body is not valid JSON (e.g. a malformed webhook retry or a probe), yielding an unhandled 500 rather than a graceful 400. Wrapping this in a `try/except` with an early `raise HTTPException(status_code=400)` keeps the error surface clean and avoids surfacing stack traces in logs.
`````
</details>
<sub>Reviews (1): Last reviewed commit: ["feat(agentex): add Slack
Events API
url\_..."](3215134)
| [Re-trigger
Greptile](https://app.greptile.com/api/retrigger?id=46036999)</sub>
> Greptile also left **3 inline comments** on this PR.
<!-- /greptile_comment -->
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>1 parent 0d859c2 commit d0ba8bf
4 files changed
Lines changed: 45 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2536 | 2536 | | |
2537 | 2537 | | |
2538 | 2538 | | |
| 2539 | + | |
| 2540 | + | |
| 2541 | + | |
| 2542 | + | |
| 2543 | + | |
| 2544 | + | |
| 2545 | + | |
| 2546 | + | |
| 2547 | + | |
| 2548 | + | |
| 2549 | + | |
| 2550 | + | |
| 2551 | + | |
| 2552 | + | |
| 2553 | + | |
2539 | 2554 | | |
2540 | 2555 | | |
2541 | 2556 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
| 41 | + | |
41 | 42 | | |
42 | 43 | | |
43 | 44 | | |
| |||
200 | 201 | | |
201 | 202 | | |
202 | 203 | | |
| 204 | + | |
203 | 205 | | |
204 | 206 | | |
205 | 207 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
31 | 34 | | |
32 | 35 | | |
33 | 36 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
0 commit comments