From f0f5b51dd282aa2bb3e9e019ee62a8d68551b467 Mon Sep 17 00:00:00 2001 From: Haakam Aujla Date: Tue, 27 Jan 2026 13:45:54 -0800 Subject: [PATCH] Webhook config --- docs/channels/agentmail.md | 4 +- extensions/agentmail/src/accounts.test.ts | 41 +++++++++++++- extensions/agentmail/src/accounts.ts | 31 +++++++++- extensions/agentmail/src/config-schema.ts | 4 +- extensions/agentmail/src/onboarding.ts | 69 ++++++----------------- 5 files changed, 87 insertions(+), 62 deletions(-) diff --git a/docs/channels/agentmail.md b/docs/channels/agentmail.md index 160f3e63c..7663e07e5 100644 --- a/docs/channels/agentmail.md +++ b/docs/channels/agentmail.md @@ -81,8 +81,8 @@ Minimal config: | `enabled` | boolean | Enable/disable the channel (default: true) | | `token` | string | AgentMail API token (required) | | `emailAddress` | string | AgentMail inbox email address to monitor (required) | -| `webhookUrl` | string | Gateway public base URL (e.g., `https://gw.ngrok.io`) | -| `webhookPath` | string | Custom webhook path (default: `/webhooks/agentmail`) | +| `webhookUrl` | string | Full public webhook URL (e.g., `https://gw.ngrok.io/webhooks/agentmail`) | +| `webhookPath` | string | Local webhook path (derived from URL or default: `/webhooks/agentmail`) | | `allowFrom` | string[] | Allowed sender emails/domains (empty = allow all) | ## Sender Filtering diff --git a/extensions/agentmail/src/accounts.test.ts b/extensions/agentmail/src/accounts.test.ts index 011c1e32f..3bf6bb96a 100644 --- a/extensions/agentmail/src/accounts.test.ts +++ b/extensions/agentmail/src/accounts.test.ts @@ -96,10 +96,47 @@ describe("resolveCredentials", () => { it("resolves webhookUrl from env", () => { const cfg: CoreConfig = {}; const env = { - AGENTMAIL_WEBHOOK_URL: "https://env-gateway.example.com", + AGENTMAIL_WEBHOOK_URL: "https://env-gateway.example.com/hooks/email", }; const result = resolveCredentials(cfg, env); - expect(result.webhookUrl).toBe("https://env-gateway.example.com"); + expect(result.webhookUrl).toBe("https://env-gateway.example.com/hooks/email"); + }); + + it("derives webhookPath from webhookUrl", () => { + const cfg: CoreConfig = { + channels: { + agentmail: { + webhookUrl: "https://my-gateway.ngrok.io/custom/path", + }, + }, + }; + const result = resolveCredentials(cfg, {}); + expect(result.webhookPath).toBe("/custom/path"); + }); + + it("uses default webhookPath when URL has no path", () => { + const cfg: CoreConfig = { + channels: { + agentmail: { + webhookUrl: "https://my-gateway.ngrok.io", + }, + }, + }; + const result = resolveCredentials(cfg, {}); + expect(result.webhookPath).toBe("/webhooks/agentmail"); + }); + + it("explicit webhookPath takes precedence over URL-derived path", () => { + const cfg: CoreConfig = { + channels: { + agentmail: { + webhookUrl: "https://my-gateway.ngrok.io/url-path", + webhookPath: "/explicit-path", + }, + }, + }; + const result = resolveCredentials(cfg, {}); + expect(result.webhookPath).toBe("/explicit-path"); }); }); diff --git a/extensions/agentmail/src/accounts.ts b/extensions/agentmail/src/accounts.ts index 4a82200b8..a70845b4a 100644 --- a/extensions/agentmail/src/accounts.ts +++ b/extensions/agentmail/src/accounts.ts @@ -31,21 +31,46 @@ export type ResolvedAgentMailCredentials = { webhookPath: string; }; +const DEFAULT_WEBHOOK_PATH = "/webhooks/agentmail"; + +/** Extracts the path from a URL string. Returns undefined if just root "/". */ +function extractPathFromUrl(url: string): string | undefined { + try { + const parsed = new URL(url); + // Return undefined if just root path "/" - use default instead + if (!parsed.pathname || parsed.pathname === "/") { + return undefined; + } + return parsed.pathname; + } catch { + return undefined; + } +} + /** * Resolves AgentMail credentials from config and environment. * Maps user-facing keys (token, emailAddress) to SDK names (apiKey, inboxId). + * Derives webhookPath from webhookUrl if not explicitly set. */ export function resolveCredentials( cfg: CoreConfig, env: Record = process.env ): ResolvedAgentMailCredentials { const base = cfg.channels?.agentmail ?? {}; + const webhookUrl = base.webhookUrl || env.AGENTMAIL_WEBHOOK_URL; + + // Derive path from URL if not explicitly set + let webhookPath = base.webhookPath || env.AGENTMAIL_WEBHOOK_PATH; + if (!webhookPath && webhookUrl) { + webhookPath = extractPathFromUrl(webhookUrl); + } + webhookPath = webhookPath || DEFAULT_WEBHOOK_PATH; + return { apiKey: base.token || env.AGENTMAIL_TOKEN, inboxId: base.emailAddress || env.AGENTMAIL_EMAIL_ADDRESS, - webhookUrl: base.webhookUrl || env.AGENTMAIL_WEBHOOK_URL, - webhookPath: - base.webhookPath || env.AGENTMAIL_WEBHOOK_PATH || "/webhooks/agentmail", + webhookUrl, + webhookPath, }; } diff --git a/extensions/agentmail/src/config-schema.ts b/extensions/agentmail/src/config-schema.ts index 73d605046..df0674013 100644 --- a/extensions/agentmail/src/config-schema.ts +++ b/extensions/agentmail/src/config-schema.ts @@ -13,9 +13,9 @@ export const AgentMailConfigSchema = z.object({ token: z.string().optional(), /** AgentMail inbox email address to monitor (required). */ emailAddress: z.string().optional(), - /** Public base URL of the gateway (e.g., https://my-gateway.ngrok.io). */ + /** Full public webhook URL (e.g., https://my-gateway.ngrok.io/webhooks/agentmail). */ webhookUrl: z.string().optional(), - /** Custom webhook path for receiving emails (default: /webhooks/agentmail). */ + /** Local webhook path (default: /webhooks/agentmail). Derived from webhookUrl if not set. */ webhookPath: z.string().optional(), /** Allowed sender emails/domains. Empty = allow all. */ allowFrom: z.array(z.string()).optional(), diff --git a/extensions/agentmail/src/onboarding.ts b/extensions/agentmail/src/onboarding.ts index abc20b140..2274a0333 100644 --- a/extensions/agentmail/src/onboarding.ts +++ b/extensions/agentmail/src/onboarding.ts @@ -173,11 +173,11 @@ export const agentmailOnboardingAdapter: ChannelOnboardingAdapter = { let webhookPath = DEFAULT_WEBHOOK_PATH; if (hasPublicUrl) { - // Get the public base URL - const baseUrl = String( + // Get the full public webhook URL + webhookUrl = String( await prompter.text({ - message: "Gateway public URL", - placeholder: "https://my-gateway.ngrok.io", + message: "Full webhook URL", + placeholder: `https://my-gateway.ngrok.io${DEFAULT_WEBHOOK_PATH}`, validate: (v) => { const trimmed = v?.trim(); if (!trimmed) return "Required"; @@ -190,42 +190,24 @@ export const agentmailOnboardingAdapter: ChannelOnboardingAdapter = { return undefined; }, }) - ) - .trim() - .replace(/\/+$/, ""); // Remove trailing slashes + ).trim(); - // Ask for webhook path - const customPath = await prompter.confirm({ - message: `Customize webhook path? (default: ${DEFAULT_WEBHOOK_PATH})`, - initialValue: false, - }); - - if (customPath) { - const pathInput = String( - await prompter.text({ - message: "Webhook path", - placeholder: DEFAULT_WEBHOOK_PATH, - }) - ).trim(); - - if (pathInput) { - webhookPath = pathInput.startsWith("/") ? pathInput : `/${pathInput}`; - } + // Derive path from URL + try { + const parsed = new URL(webhookUrl); + webhookPath = parsed.pathname || DEFAULT_WEBHOOK_PATH; + } catch { + webhookPath = DEFAULT_WEBHOOK_PATH; } - webhookUrl = baseUrl; - // Auto-register webhook with AgentMail try { await client.webhooks.create({ - url: `${baseUrl}${webhookPath}`, + url: webhookUrl, eventTypes: ["message.received"], - clientId: `moltbot-${emailAddress}`, // Idempotent per inbox + clientId: `clawdbot-${emailAddress}`, // Idempotent per inbox }); - await prompter.note( - `Webhook registered: ${baseUrl}${webhookPath}`, - "Webhook Created" - ); + await prompter.note(`Webhook registered: ${webhookUrl}`, "Webhook Created"); } catch (err) { // Webhook may already exist or API error - show warning but continue await prompter.note( @@ -233,34 +215,15 @@ export const agentmailOnboardingAdapter: ChannelOnboardingAdapter = { `Could not auto-register webhook: ${String(err)}`, "", "You may need to configure it manually in the AgentMail dashboard:", - ` URL: ${baseUrl}${webhookPath}`, + ` URL: ${webhookUrl}`, " Event: message.received", ].join("\n"), "Webhook Warning" ); } - } else { - // No public URL - ask for path only and show manual instructions - const customPath = await prompter.confirm({ - message: `Customize webhook path? (default: ${DEFAULT_WEBHOOK_PATH})`, - initialValue: false, - }); - - if (customPath) { - const pathInput = String( - await prompter.text({ - message: "Webhook path", - placeholder: DEFAULT_WEBHOOK_PATH, - }) - ).trim(); - - if (pathInput) { - webhookPath = pathInput.startsWith("/") ? pathInput : `/${pathInput}`; - } - } } - // Save webhook config + // Save webhook config (webhookPath derived from URL or default) next = updateAgentMailConfig(next, { webhookUrl, webhookPath }); // Ask about allowFrom