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
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,18 +139,27 @@ Slack (events/messages)
└─▶ POST /api/coding-agent/callback ← Trigger.dev callback URL
└─▶ Posts result back to Slack thread

WhatsApp (messages)
└─▶ GET /api/coding-agent/whatsapp ← Meta hub.challenge verification handshake
└─▶ POST /api/coding-agent/whatsapp ← Incoming messages (X-Hub-Signature-256 verified)
└─▶ Chat SDK (WhatsAppAdapter)
└─▶ Handler dispatches Trigger.dev task
└─▶ POST /api/coding-agent/callback ← Trigger.dev callback URL
└─▶ Posts result back to WhatsApp thread

GitHub (webhooks)
└─▶ PR events, repo operations
└─▶ lib/github/* helpers (file trees, submodules, repo management)
```

- **Slack adapter** — Receives messages via Slack's Events API, processes them through Chat SDK handlers, and triggers coding agent tasks via Trigger.dev. Results are posted back to the originating Slack thread.
- **WhatsApp adapter** — Receives messages via the WhatsApp Business Cloud API. Uses the same coding agent handlers as Slack. Verification handshake is handled automatically on GET.
- **GitHub integration** — Manages repo operations (file trees, submodules, PRs) used by the coding agent to create and update pull requests.
- **State** — Thread state (status, run IDs, PRs) is stored in Redis via the Chat SDK's ioredis state adapter.

### Updating Testing/Dev URLs

When deploying to a new environment (e.g. preview branches, local dev via ngrok), you need to update callback URLs in two places:
When deploying to a new environment (e.g. preview branches, local dev via ngrok), you need to update callback URLs in the relevant places:

#### Slack — Subscription Events & Interactivity

Expand All @@ -165,6 +174,24 @@ When deploying to a new environment (e.g. preview branches, local dev via ngrok)
```
4. Slack will send a `url_verification` challenge — the route handles this automatically.

#### WhatsApp — Meta App Webhook

1. Go to [developers.facebook.com/apps](https://developers.facebook.com/apps) → select your app
2. **WhatsApp > Configuration** → Update the Callback URL to:
```
https://<your-host>/api/coding-agent/whatsapp
```
3. Set the **Verify Token** to match your `WHATSAPP_VERIFY_TOKEN` env var
4. Subscribe to the `messages` webhook field

Required environment variables:
```
WHATSAPP_ACCESS_TOKEN=... # Meta access token (permanent or system user token)
WHATSAPP_APP_SECRET=... # App secret for X-Hub-Signature-256 verification
WHATSAPP_PHONE_NUMBER_ID=... # Bot's phone number ID from Meta dashboard
WHATSAPP_VERIFY_TOKEN=... # User-defined secret for webhook verification
```

#### Trigger.dev — Callback URL

The coding agent task calls back to the API when it finishes. Update the callback URL environment variable so it points to your current deployment:
Expand Down
29 changes: 28 additions & 1 deletion app/api/coding-agent/[platform]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,38 @@ import { after } from "next/server";
import { codingAgentBot } from "@/lib/coding-agent/bot";
import "@/lib/coding-agent/handlers/registerHandlers";

/**
* GET /api/coding-agent/[platform]
*
* Handles webhook verification handshakes for platforms that use GET-based challenges.
* Currently handles WhatsApp's hub.challenge verification.
*
* @param request - The incoming webhook verification request
* @param params.params
* @param params - Route params containing the platform name
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ platform: string }> },
) {
const { platform } = await params;

await codingAgentBot.initialize();

const handler = codingAgentBot.webhooks[platform as keyof typeof codingAgentBot.webhooks];

if (!handler) {
return new Response("Unknown platform", { status: 404 });
}

return handler(request, { waitUntil: p => after(() => p) });
Comment on lines +16 to +30
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

The verification fast-path is ineffective while the bot stays module-scoped.

codingAgentBot is instantiated during module import in lib/coding-agent/bot.ts:66, so these handlers still validate env and touch Redis before GET or POST runs. That makes unknown-platform requests and Slack/WhatsApp verification depend on unrelated startup state, which can turn a simple 404/challenge response into a 500 or timeout. Validate platform first, then lazily create and initialize the bot only after a real handler has been selected.

As per coding guidelines "All API endpoints should use a validate function for input parsing using Zod for schema validation".

Also applies to: 43-58

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

In `@app/api/coding-agent/`[platform]/route.ts around lines 16 - 30, Validate and
parse the incoming platform param first (use a Zod validate function) and
early-return 404 for unknown platforms before touching the module-scoped bot;
then lazily create/initialize the bot and resolve the handler. Concretely: in
GET (and similarly in POST), run Zod validation on params to extract platform,
check codingAgentBot.webhooks[platform] for a handler and return 404 if missing,
and only after selecting a valid handler call a lazy factory or function to
obtain/initialize the bot (instead of relying on the module-scoped
codingAgentBot created at import) and finally invoke handler(request, {
waitUntil: p => after(() => p) }). Ensure you update both occurrences (lines
matching GET and the POST handler region) to follow this pattern.

}
Comment on lines +6 to +31
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Please add route coverage for the new verification branches.

The new GET handler and Slack challenge shortcut add several contracts that can regress quietly: verification success, unknown platform handling, invalid challenge payloads, and normal webhook delegation. I don't see matching tests in this PR.

Based on learnings "Write tests for new API endpoints covering all success and error paths".

Also applies to: 33-67

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

In `@app/api/coding-agent/`[platform]/route.ts around lines 6 - 31, Add
unit/integration tests for the new GET route handler to cover all branches: call
the exported GET function (or exercising the route) and assert (1) successful
verification flow when a valid platform-specific challenge is provided (mock
codingAgentBot.initialize and the platform handler in codingAgentBot.webhooks to
return a valid Response), (2) unknown platform returns 404 when
codingAgentBot.webhooks lacks the key, (3) invalid/malformed challenge payloads
produce the expected error response (mock the platform handler to simulate
invalid payload handling), and (4) normal webhook delegation forwards the
request to the platform handler and respects waitUntil behavior (mock handler to
capture the passed request and promise). Mock codingAgentBot.initialize and
codingAgentBot.webhooks entries and assert responses and side effects for each
case.


/**
* POST /api/coding-agent/[platform]
*
* Webhook endpoint for the coding agent bot.
* Currently handles Slack webhooks via dynamic [platform] segment.
* Handles Slack, WhatsApp, and GitHub webhook events via the dynamic [platform] segment.
*
* @param request - The incoming webhook request
* @param params.params
Expand Down
53 changes: 52 additions & 1 deletion app/api/coding-agent/__tests__/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,21 @@ vi.mock("next/server", async () => {

const mockInitialize = vi.fn();
const mockSlackWebhook = vi.fn().mockResolvedValue(new Response("ok", { status: 200 }));
const mockWhatsAppWebhook = vi.fn().mockResolvedValue(new Response("ok", { status: 200 }));

vi.mock("@/lib/coding-agent/bot", () => ({
codingAgentBot: {
initialize: mockInitialize,
webhooks: {
slack: mockSlackWebhook,
whatsapp: mockWhatsAppWebhook,
},
},
}));

vi.mock("@/lib/coding-agent/handlers/registerHandlers", () => ({}));

const { POST } = await import("../[platform]/route");
const { GET, POST } = await import("../[platform]/route");

describe("POST /api/coding-agent/[platform]", () => {
beforeEach(() => {
Expand Down Expand Up @@ -113,6 +115,26 @@ describe("POST /api/coding-agent/[platform]", () => {
expect(callOrder).toEqual(["initialize", "webhook"]);
});

it("delegates WhatsApp POST events to bot webhook handler", async () => {
const body = JSON.stringify({
object: "whatsapp_business_account",
entry: [{ changes: [{ value: { messages: [] }, field: "messages" }] }],
});

const request = new NextRequest("https://example.com/api/coding-agent/whatsapp", {
method: "POST",
body,
headers: { "content-type": "application/json", "x-hub-signature-256": "sha256=test" },
});

const response = await POST(request, {
params: Promise.resolve({ platform: "whatsapp" }),
});

expect(response.status).toBe(200);
expect(mockWhatsAppWebhook).toHaveBeenCalled();
});

it("does not call initialize for url_verification challenges", async () => {
const body = JSON.stringify({
type: "url_verification",
Expand All @@ -132,3 +154,32 @@ describe("POST /api/coding-agent/[platform]", () => {
expect(mockInitialize).not.toHaveBeenCalled();
});
});

describe("GET /api/coding-agent/[platform]", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it("delegates WhatsApp GET verification to bot webhook handler", async () => {
const request = new NextRequest(
"https://example.com/api/coding-agent/whatsapp?hub.mode=subscribe&hub.verify_token=test&hub.challenge=abc123",
);

const response = await GET(request, {
params: Promise.resolve({ platform: "whatsapp" }),
});

expect(response.status).toBe(200);
expect(mockWhatsAppWebhook).toHaveBeenCalled();
});

it("returns 404 for unknown platforms on GET", async () => {
const request = new NextRequest("https://example.com/api/coding-agent/unknown");

const response = await GET(request, {
params: Promise.resolve({ platform: "unknown" }),
});

expect(response.status).toBe(404);
});
});
16 changes: 13 additions & 3 deletions lib/coding-agent/bot.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Chat, ConsoleLogger } from "chat";
import { SlackAdapter } from "@chat-adapter/slack";
import { createGitHubAdapter } from "@chat-adapter/github";
import { createWhatsAppAdapter } from "@chat-adapter/whatsapp";

Check failure on line 4 in lib/coding-agent/bot.ts

View workflow job for this annotation

GitHub Actions / test

lib/coding-agent/__tests__/bot.test.ts > createCodingAgentBot > sets userName to Recoup Agent

Error: Cannot find package '@chat-adapter/whatsapp' imported from '/home/runner/work/api/api/lib/coding-agent/bot.ts' ❯ lib/coding-agent/bot.ts:4:1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_MODULE_NOT_FOUND' } Caused by: Error: Failed to load url @chat-adapter/whatsapp (resolved id: @chat-adapter/whatsapp) in /home/runner/work/api/api/lib/coding-agent/bot.ts. Does the file exist? ❯ loadAndTransform node_modules/.pnpm/vite@7.3.0_@types+node@20.19.25_jiti@1.21.7_yaml@2.8.2/node_modules/vite/dist/node/chunks/config.js:22662:33

Check failure on line 4 in lib/coding-agent/bot.ts

View workflow job for this annotation

GitHub Actions / test

lib/coding-agent/__tests__/bot.test.ts > createCodingAgentBot > creates GitHub adapter with correct config

Error: Cannot find package '@chat-adapter/whatsapp' imported from '/home/runner/work/api/api/lib/coding-agent/bot.ts' ❯ lib/coding-agent/bot.ts:4:1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_MODULE_NOT_FOUND' } Caused by: Error: Failed to load url @chat-adapter/whatsapp (resolved id: @chat-adapter/whatsapp) in /home/runner/work/api/api/lib/coding-agent/bot.ts. Does the file exist? ❯ loadAndTransform node_modules/.pnpm/vite@7.3.0_@types+node@20.19.25_jiti@1.21.7_yaml@2.8.2/node_modules/vite/dist/node/chunks/config.js:22662:33

Check failure on line 4 in lib/coding-agent/bot.ts

View workflow job for this annotation

GitHub Actions / test

lib/coding-agent/__tests__/bot.test.ts > createCodingAgentBot > creates a Chat instance with github adapter

Error: Cannot find package '@chat-adapter/whatsapp' imported from '/home/runner/work/api/api/lib/coding-agent/bot.ts' ❯ lib/coding-agent/bot.ts:4:1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_MODULE_NOT_FOUND' } Caused by: Error: Failed to load url @chat-adapter/whatsapp (resolved id: @chat-adapter/whatsapp) in /home/runner/work/api/api/lib/coding-agent/bot.ts. Does the file exist? ❯ loadAndTransform node_modules/.pnpm/vite@7.3.0_@types+node@20.19.25_jiti@1.21.7_yaml@2.8.2/node_modules/vite/dist/node/chunks/config.js:22662:33

Check failure on line 4 in lib/coding-agent/bot.ts

View workflow job for this annotation

GitHub Actions / test

lib/coding-agent/__tests__/bot.test.ts > createCodingAgentBot > uses ioredis state adapter with existing Redis client

Error: Cannot find package '@chat-adapter/whatsapp' imported from '/home/runner/work/api/api/lib/coding-agent/bot.ts' ❯ lib/coding-agent/bot.ts:4:1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_MODULE_NOT_FOUND' } Caused by: Error: Failed to load url @chat-adapter/whatsapp (resolved id: @chat-adapter/whatsapp) in /home/runner/work/api/api/lib/coding-agent/bot.ts. Does the file exist? ❯ loadAndTransform node_modules/.pnpm/vite@7.3.0_@types+node@20.19.25_jiti@1.21.7_yaml@2.8.2/node_modules/vite/dist/node/chunks/config.js:22662:33

Check failure on line 4 in lib/coding-agent/bot.ts

View workflow job for this annotation

GitHub Actions / test

lib/coding-agent/__tests__/bot.test.ts > createCodingAgentBot > creates a SlackAdapter with correct config

Error: Cannot find package '@chat-adapter/whatsapp' imported from '/home/runner/work/api/api/lib/coding-agent/bot.ts' ❯ lib/coding-agent/bot.ts:4:1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_MODULE_NOT_FOUND' } Caused by: Error: Failed to load url @chat-adapter/whatsapp (resolved id: @chat-adapter/whatsapp) in /home/runner/work/api/api/lib/coding-agent/bot.ts. Does the file exist? ❯ loadAndTransform node_modules/.pnpm/vite@7.3.0_@types+node@20.19.25_jiti@1.21.7_yaml@2.8.2/node_modules/vite/dist/node/chunks/config.js:22662:33

Check failure on line 4 in lib/coding-agent/bot.ts

View workflow job for this annotation

GitHub Actions / test

lib/coding-agent/__tests__/bot.test.ts > createCodingAgentBot > creates a Chat instance with slack adapter

Error: Cannot find package '@chat-adapter/whatsapp' imported from '/home/runner/work/api/api/lib/coding-agent/bot.ts' ❯ lib/coding-agent/bot.ts:4:1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_MODULE_NOT_FOUND' } Caused by: Caused by: Error: Failed to load url @chat-adapter/whatsapp (resolved id: @chat-adapter/whatsapp) in /home/runner/work/api/api/lib/coding-agent/bot.ts. Does the file exist? ❯ loadAndTransform node_modules/.pnpm/vite@7.3.0_@types+node@20.19.25_jiti@1.21.7_yaml@2.8.2/node_modules/vite/dist/node/chunks/config.js:22662:33
import { createIoRedisState } from "@chat-adapter/state-ioredis";
import redis from "@/lib/redis/connection";
import type { CodingAgentThreadState } from "./types";
Expand All @@ -9,7 +10,7 @@
const logger = new ConsoleLogger();

/**
* Creates a new Chat bot instance configured with the Slack adapter.
* Creates a new Chat bot instance configured with Slack, GitHub, and WhatsApp adapters.
*/
export function createCodingAgentBot() {
validateCodingAgentEnv();
Expand Down Expand Up @@ -40,9 +41,18 @@
logger,
});

return new Chat<{ slack: SlackAdapter; github: ReturnType<typeof createGitHubAdapter> }, CodingAgentThreadState>({
const whatsapp = createWhatsAppAdapter({ logger });

return new Chat<
{
slack: SlackAdapter;
github: ReturnType<typeof createGitHubAdapter>;
whatsapp: ReturnType<typeof createWhatsAppAdapter>;
},
CodingAgentThreadState
>({
userName: "Recoup Agent",
adapters: { slack, github },
adapters: { slack, github, whatsapp },
state,
});
}
Expand Down
4 changes: 4 additions & 0 deletions lib/coding-agent/validateEnv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ const REQUIRED_ENV_VARS = [
"GITHUB_WEBHOOK_SECRET",
"REDIS_URL",
"CODING_AGENT_CALLBACK_SECRET",
"WHATSAPP_ACCESS_TOKEN",
"WHATSAPP_APP_SECRET",
"WHATSAPP_PHONE_NUMBER_ID",
"WHATSAPP_VERIFY_TOKEN",
] as const;

/**
Expand Down
Loading