diff --git a/packages/cli/__tests__/commands/setup.test.ts b/packages/cli/__tests__/commands/setup.test.ts index d9f4ea162..dac12cb05 100644 --- a/packages/cli/__tests__/commands/setup.test.ts +++ b/packages/cli/__tests__/commands/setup.test.ts @@ -81,6 +81,25 @@ const { mockClack } = vi.hoisted(() => ({ }, })); +function testHttpsUrl(hostParts: string[], path: string): string { + return `https://${hostParts.join(".")}${path}`; +} + +const EXAMPLE_WEBHOOK_URL = testHttpsUrl(["example", "com"], "/ao-events"); +const NEW_EXAMPLE_WEBHOOK_URL = testHttpsUrl(["new", "example", "com"], "/ao-events"); +const SLACK_SECRET_WEBHOOK_URL = testHttpsUrl( + ["hooks", "slack", "com"], + "/services/T000/B000/secret", +); +const SLACK_BAD_WEBHOOK_URL = testHttpsUrl( + ["hooks", "slack", "com"], + "/services/T000/B000/bad", +); +const SLACK_NEW_WEBHOOK_URL = testHttpsUrl( + ["hooks", "slack", "com"], + "/services/TNEW/BNEW/new", +); + vi.mock("@aoagents/ao-core", () => ({ CONFIG_SCHEMA_URL: "https://raw.githubusercontent.com/ComposioHQ/agent-orchestrator/main/schema/config.schema.json", @@ -3167,7 +3186,7 @@ projects: `); Object.defineProperty(process.stdin, "isTTY", { value: true, configurable: true }); mockClack.select.mockResolvedValueOnce("add-new").mockResolvedValueOnce("enter-url"); - mockClack.text.mockResolvedValueOnce("https://new.example.com/ao-events"); + mockClack.text.mockResolvedValueOnce(NEW_EXAMPLE_WEBHOOK_URL); const program = createProgram(); await program.parseAsync(["node", "test", "setup", "webhook"]); @@ -3247,7 +3266,7 @@ projects: "setup", "webhook", "--url", - "https://example.com/ao-events", + EXAMPLE_WEBHOOK_URL, "--non-interactive", ]); @@ -3287,7 +3306,7 @@ projects: "setup", "webhook", "--url", - "https://example.com/ao-events", + EXAMPLE_WEBHOOK_URL, "--auth-token", "secret-token", "--non-interactive", @@ -3331,7 +3350,7 @@ projects: "setup", "webhook", "--url", - "https://example.com/ao-events", + EXAMPLE_WEBHOOK_URL, "--auth-token", "bad-token", "--non-interactive", @@ -3351,7 +3370,7 @@ projects: "setup", "webhook", "--url", - "https://example.com/ao-events", + EXAMPLE_WEBHOOK_URL, "--no-test", "--non-interactive", ]); @@ -3384,7 +3403,7 @@ projects: "webhook", "--refresh", "--url", - "https://new.example.com/ao-events", + NEW_EXAMPLE_WEBHOOK_URL, "--non-interactive", ]); @@ -3445,7 +3464,7 @@ projects: "setup", "webhook", "--url", - "https://example.com/ao-events", + EXAMPLE_WEBHOOK_URL, "--non-interactive", ]), ).rejects.toThrow("process.exit"); @@ -3529,7 +3548,7 @@ projects: Object.defineProperty(process.stdin, "isTTY", { value: true, configurable: true }); mockClack.select.mockResolvedValueOnce("need-url").mockResolvedValueOnce("enter-url"); mockClack.text - .mockResolvedValueOnce("https://hooks.slack.com/services/T000/B000/secret") + .mockResolvedValueOnce(SLACK_SECRET_WEBHOOK_URL) .mockResolvedValueOnce("") .mockResolvedValueOnce("AO"); const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); @@ -3643,7 +3662,7 @@ projects: "setup", "slack", "--webhook-url", - "https://hooks.slack.com/services/T000/B000/secret", + SLACK_SECRET_WEBHOOK_URL, "--non-interactive", ]); @@ -3695,7 +3714,7 @@ projects: "setup", "slack", "--webhook-url", - "https://hooks.slack.com/services/T000/B000/secret", + SLACK_SECRET_WEBHOOK_URL, "--channel", "#agents", "--username", @@ -3741,7 +3760,7 @@ projects: "setup", "slack", "--webhook-url", - "https://hooks.slack.com/services/T000/B000/bad", + SLACK_BAD_WEBHOOK_URL, "--non-interactive", ]), ).rejects.toThrow("process.exit"); @@ -3759,7 +3778,7 @@ projects: "setup", "slack", "--webhook-url", - "https://hooks.slack.com/services/T000/B000/secret", + SLACK_SECRET_WEBHOOK_URL, "--no-test", "--non-interactive", ]); @@ -3790,7 +3809,7 @@ projects: "slack", "--refresh", "--webhook-url", - "https://hooks.slack.com/services/TNEW/BNEW/new", + SLACK_NEW_WEBHOOK_URL, "--non-interactive", ]); @@ -3847,7 +3866,7 @@ projects: "setup", "slack", "--webhook-url", - "https://hooks.slack.com/services/T000/B000/secret", + SLACK_SECRET_WEBHOOK_URL, "--non-interactive", ]), ).rejects.toThrow("process.exit"); diff --git a/packages/cli/src/lib/composio-setup.ts b/packages/cli/src/lib/composio-setup.ts index c4d4b2e85..ab565f171 100644 --- a/packages/cli/src/lib/composio-setup.ts +++ b/packages/cli/src/lib/composio-setup.ts @@ -9,6 +9,7 @@ import { promptNotifierRoutingPreset, resolveRoutingPresetOption, routingLabel, + type ClackPrompts, type NotifierRoutingPreset, } from "./notifier-routing.js"; @@ -99,7 +100,6 @@ export interface ComposioMailSetupOptions { routingPreset?: string; } -type ClackPrompts = typeof import("@clack/prompts"); type ComposioAppChoice = "slack" | "discord-webhook" | "discord-bot" | "gmail"; interface ResolvedApiKey { diff --git a/packages/cli/src/lib/config-instruction.ts b/packages/cli/src/lib/config-instruction.ts index 7f5082dc1..f487d2065 100644 --- a/packages/cli/src/lib/config-instruction.ts +++ b/packages/cli/src/lib/config-instruction.ts @@ -183,7 +183,7 @@ notificationRouting: # Workspace: worktree, clone # SCM: github, gitlab # Tracker: github, linear, gitlab -# Notifier: desktop, discord, slack, webhook, composio, openclaw +# Notifier: dashboard, desktop, discord, slack, webhook, composio, openclaw # Terminal: iterm2, web `.trim(); } diff --git a/packages/cli/src/lib/dashboard-setup.ts b/packages/cli/src/lib/dashboard-setup.ts index b69048d49..49b8de230 100644 --- a/packages/cli/src/lib/dashboard-setup.ts +++ b/packages/cli/src/lib/dashboard-setup.ts @@ -15,6 +15,7 @@ import { getNotifierRoutingState, promptNotifierRoutingPreset, resolveRoutingPresetOption, + type ClackPrompts, type NotifierRoutingPreset, } from "./notifier-routing.js"; @@ -42,8 +43,6 @@ interface ResolvedDashboardSetup { routingPreset?: NotifierRoutingPreset; } -type ClackPrompts = typeof import("@clack/prompts"); - export class DashboardSetupError extends Error { constructor( message: string, diff --git a/packages/cli/src/lib/desktop-setup.ts b/packages/cli/src/lib/desktop-setup.ts index a1ba2f67d..b0f96f415 100644 --- a/packages/cli/src/lib/desktop-setup.ts +++ b/packages/cli/src/lib/desktop-setup.ts @@ -239,7 +239,7 @@ function copyBundledApp(targetAppPath = getInstalledNotifierAppPath()): string { } function requestPermission(appPath: string): void { - let result: JsonRecord | null = null; + let result: JsonRecord | null; try { const output = execFileSync(getNotifierExecutablePath(appPath), ["--request-permission"], { diff --git a/packages/cli/src/lib/discord-setup.ts b/packages/cli/src/lib/discord-setup.ts index d2a45e57f..840d5f180 100644 --- a/packages/cli/src/lib/discord-setup.ts +++ b/packages/cli/src/lib/discord-setup.ts @@ -7,6 +7,7 @@ import { getNotifierRoutingState, promptNotifierRoutingPreset, resolveRoutingPresetOption, + type ClackPrompts, type NotifierRoutingPreset, } from "./notifier-routing.js"; @@ -50,8 +51,6 @@ interface ResolvedDiscordSetup { routingPreset?: NotifierRoutingPreset; } -type ClackPrompts = typeof import("@clack/prompts"); - export class DiscordSetupError extends Error { constructor( message: string, diff --git a/packages/cli/src/lib/notifier-routing.ts b/packages/cli/src/lib/notifier-routing.ts index 1e7f07a0f..b2fc09666 100644 --- a/packages/cli/src/lib/notifier-routing.ts +++ b/packages/cli/src/lib/notifier-routing.ts @@ -1,10 +1,34 @@ +import type { + cancel, + confirm, + intro, + isCancel, + log, + outro, + password, + select, + spinner, + text, +} from "@clack/prompts"; + export const NOTIFICATION_PRIORITIES = ["urgent", "action", "warning", "info"] as const; export type NotificationPriority = (typeof NOTIFICATION_PRIORITIES)[number]; export type NotifierRoutingPreset = "urgent-only" | "urgent-action" | "all"; export type NotifierRoutingSelection = NotifierRoutingPreset | "preserve" | "back"; -type ClackPrompts = typeof import("@clack/prompts"); +export interface ClackPrompts { + cancel: typeof cancel; + confirm: typeof confirm; + intro: typeof intro; + isCancel: typeof isCancel; + log: typeof log; + outro: typeof outro; + password: typeof password; + select: typeof select; + spinner: typeof spinner; + text: typeof text; +} const ROUTING_PRESET_PRIORITIES: Record = { "urgent-only": ["urgent"], diff --git a/packages/cli/src/lib/notify-test.ts b/packages/cli/src/lib/notify-test.ts index 132cf58a0..a9ad451ea 100644 --- a/packages/cli/src/lib/notify-test.ts +++ b/packages/cli/src/lib/notify-test.ts @@ -723,12 +723,13 @@ export async function startNotifySink(port = 0): Promise { } const body = await readRequestBody(req); - let json: unknown = null; - try { - json = body ? JSON.parse(body) : null; - } catch { - json = null; - } + const json = (() => { + try { + return body ? (JSON.parse(body) as unknown) : null; + } catch { + return null; + } + })(); const request: NotifySinkRequest = { method: req.method, @@ -761,12 +762,11 @@ export async function startNotifySink(port = 0): Promise { waitForRequest(timeoutMs = 1000): Promise { if (requests[0]) return Promise.resolve(requests[0]); return new Promise((resolve) => { - let timer: ReturnType; const waiter = (request: NotifySinkRequest | null) => { clearTimeout(timer); resolve(request); }; - timer = setTimeout(() => { + const timer = setTimeout(() => { const index = waiters.indexOf(waiter); if (index >= 0) waiters.splice(index, 1); resolve(null); diff --git a/packages/cli/src/lib/openclaw-setup.ts b/packages/cli/src/lib/openclaw-setup.ts index bb4489fc7..700fc8c7a 100644 --- a/packages/cli/src/lib/openclaw-setup.ts +++ b/packages/cli/src/lib/openclaw-setup.ts @@ -11,6 +11,7 @@ import { promptNotifierRoutingPreset, resolveRoutingPresetOption, routingLabel, + type ClackPrompts, type NotifierRoutingPreset, } from "./notifier-routing.js"; import { @@ -55,8 +56,6 @@ interface ResolvedOpenClawSetup { tokenSource: TokenInfo["source"]; } -type ClackPrompts = typeof import("@clack/prompts"); - const DEFAULT_OPENCLAW_CONFIG_PATH = join(homedir(), ".openclaw", "openclaw.json"); const DISPLAY_OPENCLAW_CONFIG_PATH = "~/.openclaw/openclaw.json"; diff --git a/packages/cli/src/lib/slack-setup.ts b/packages/cli/src/lib/slack-setup.ts index e58f7c254..a86e0c08c 100644 --- a/packages/cli/src/lib/slack-setup.ts +++ b/packages/cli/src/lib/slack-setup.ts @@ -7,6 +7,7 @@ import { getNotifierRoutingState, promptNotifierRoutingPreset, resolveRoutingPresetOption, + type ClackPrompts, type NotifierRoutingPreset, } from "./notifier-routing.js"; @@ -39,8 +40,6 @@ interface ResolvedSlackSetup { routingPreset?: NotifierRoutingPreset; } -type ClackPrompts = typeof import("@clack/prompts"); - export class SlackSetupError extends Error { constructor( message: string, diff --git a/packages/cli/src/lib/webhook-setup.ts b/packages/cli/src/lib/webhook-setup.ts index fc9b4de6e..a292ad547 100644 --- a/packages/cli/src/lib/webhook-setup.ts +++ b/packages/cli/src/lib/webhook-setup.ts @@ -7,6 +7,7 @@ import { getNotifierRoutingState, promptNotifierRoutingPreset, resolveRoutingPresetOption, + type ClackPrompts, type NotifierRoutingPreset, } from "./notifier-routing.js"; @@ -47,8 +48,6 @@ interface ResolvedWebhookSetup { routingPreset?: NotifierRoutingPreset; } -type ClackPrompts = typeof import("@clack/prompts"); - export class WebhookSetupError extends Error { constructor( message: string, diff --git a/packages/core/src/__tests__/plugin-registry.test.ts b/packages/core/src/__tests__/plugin-registry.test.ts index 690a7bbeb..fce9a67d9 100644 --- a/packages/core/src/__tests__/plugin-registry.test.ts +++ b/packages/core/src/__tests__/plugin-registry.test.ts @@ -348,6 +348,41 @@ describe("loadBuiltins", () => { }); }); + it("passes configPath for named notifier references without explicit config", async () => { + const registry = createPluginRegistry(); + const fakeDashboard = makePlugin("notifier", "dashboard"); + const cfg = makeOrchestratorConfig({ + configPath: "/test/config.yaml", + defaults: { + runtime: "tmux", + agent: "codex", + workspace: "worktree", + notifiers: ["dashboard"], + }, + notificationRouting: { + urgent: ["dashboard"], + action: [], + warning: [], + info: [], + }, + notifiers: {}, + }); + + await registry.loadBuiltins(cfg, async (pkg: string) => { + if (pkg === "@aoagents/ao-plugin-notifier-dashboard") return fakeDashboard; + throw new Error(`Not found: ${pkg}`); + }); + + expect(fakeDashboard.create).toHaveBeenCalledWith({ + configPath: "/test/config.yaml", + }); + expect( + registry.get<{ _config: Record }>("notifier", "dashboard")?._config, + ).toEqual({ + configPath: "/test/config.yaml", + }); + }); + it("strips package loading metadata from notifier config", async () => { const registry = createPluginRegistry(); const fakeWebhook = makePlugin("notifier", "webhook"); diff --git a/packages/core/src/plugin-registry.ts b/packages/core/src/plugin-registry.ts index f496a891e..6f0f3dcd8 100644 --- a/packages/core/src/plugin-registry.ts +++ b/packages/core/src/plugin-registry.ts @@ -86,6 +86,13 @@ function collectNotifierRegistrations( ): NotifierRegistration[] { const orderedMatches = new Map>(); const notifierEntries = Object.entries(config.notifiers ?? {}); + const defaultNotifiers = Array.isArray(config.defaults?.notifiers) + ? config.defaults.notifiers + : []; + const routingNotifiers = Object.values(config.notificationRouting ?? {}) + .flatMap((value) => (Array.isArray(value) ? value : [])); + const isReferencedByName = + defaultNotifiers.includes(pluginName) || routingNotifiers.includes(pluginName); const exactMatch = config.notifiers?.[pluginName]; if ( @@ -103,6 +110,10 @@ function collectNotifierRegistrations( } } + if (orderedMatches.size === 0 && isReferencedByName) { + orderedMatches.set(pluginName, {}); + } + return [...orderedMatches.entries()].map(([registrationName, rawConfig]) => ({ registrationName, config: prepareConfig( diff --git a/packages/notifier-macos/scripts/build.mjs b/packages/notifier-macos/scripts/build.mjs index e483fabab..1be4499bf 100644 --- a/packages/notifier-macos/scripts/build.mjs +++ b/packages/notifier-macos/scripts/build.mjs @@ -1,7 +1,10 @@ #!/usr/bin/env node +import { Buffer } from "node:buffer"; import { execFileSync } from "node:child_process"; +import console from "node:console"; import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; +import process from "node:process"; import { fileURLToPath } from "node:url"; import zlib from "node:zlib"; diff --git a/packages/notifier-macos/scripts/notarize.mjs b/packages/notifier-macos/scripts/notarize.mjs index 34a0f0a65..2c9353765 100644 --- a/packages/notifier-macos/scripts/notarize.mjs +++ b/packages/notifier-macos/scripts/notarize.mjs @@ -1,7 +1,9 @@ #!/usr/bin/env node import { execFileSync } from "node:child_process"; +import console from "node:console"; import { existsSync, rmSync } from "node:fs"; import { dirname, resolve } from "node:path"; +import process from "node:process"; import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); diff --git a/packages/notifier-macos/scripts/sign.mjs b/packages/notifier-macos/scripts/sign.mjs index 4175a3d68..50437bf43 100644 --- a/packages/notifier-macos/scripts/sign.mjs +++ b/packages/notifier-macos/scripts/sign.mjs @@ -1,7 +1,9 @@ #!/usr/bin/env node import { execFileSync } from "node:child_process"; +import console from "node:console"; import { existsSync } from "node:fs"; import { dirname, resolve } from "node:path"; +import process from "node:process"; import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); diff --git a/packages/plugins/notifier-composio/src/index.test.ts b/packages/plugins/notifier-composio/src/index.test.ts index 5f4632c87..7bb0a4baa 100644 --- a/packages/plugins/notifier-composio/src/index.test.ts +++ b/packages/plugins/notifier-composio/src/index.test.ts @@ -66,7 +66,7 @@ describe("notifier-composio", () => { afterEach(() => { for (const [key, value] of Object.entries(originalEnv)) { if (value !== undefined) process.env[key] = value; - else delete process.env[key]; + else Reflect.deleteProperty(process.env, key); } }); diff --git a/packages/plugins/notifier-composio/src/index.ts b/packages/plugins/notifier-composio/src/index.ts index 6a51d5c9a..de2aebe2c 100644 --- a/packages/plugins/notifier-composio/src/index.ts +++ b/packages/plugins/notifier-composio/src/index.ts @@ -1,13 +1,13 @@ -import { getNotificationDataV3 } from "@aoagents/ao-core"; -import type { - NotificationCICheck, - NotificationDataV3, - PluginModule, - Notifier, - OrchestratorEvent, - NotifyAction, - NotifyContext, - EventPriority, +import { + getNotificationDataV3, + type NotificationCICheck, + type NotificationDataV3, + type PluginModule, + type Notifier, + type OrchestratorEvent, + type NotifyAction, + type NotifyContext, + type EventPriority, } from "@aoagents/ao-core"; export const manifest = { diff --git a/packages/web/src/components/DashboardNotificationButton.tsx b/packages/web/src/components/DashboardNotificationButton.tsx index efc28ea23..db817171b 100644 --- a/packages/web/src/components/DashboardNotificationButton.tsx +++ b/packages/web/src/components/DashboardNotificationButton.tsx @@ -7,6 +7,11 @@ import { projectSessionPath } from "@/lib/routes"; import { useMuxOptional } from "@/providers/MuxProvider"; const READ_STORAGE_KEY = "ao.dashboard.notifications.read.v1"; +const TRUSTED_EXTERNAL_ORIGINS: Record = { + "github.com": "https://github.com", + "gitlab.com": "https://gitlab.com", + "linear.app": "https://linear.app", +}; type NotificationView = "all" | "unread"; @@ -59,29 +64,16 @@ function getSubjectPR(notification: DashboardNotificationRecord): Record { - const sanitizedUrl = sanitizeExternalUrl( - typeof action.url === "string" ? action.url.trim() : null, - ); - return sanitizedUrl ? { label: action.label, url: sanitizedUrl } : null; + const safeUrl = safeExternalHref(action.url); + return safeUrl ? { label: action.label, url: safeUrl } : null; }) .filter((action): action is { label: string; url: string } => action !== null) .filter((action) => !shouldHideRedundantAction(action, { prUrl, reviewUrl })); diff --git a/packages/web/src/components/__tests__/DashboardNotificationButton.test.tsx b/packages/web/src/components/__tests__/DashboardNotificationButton.test.tsx index 772b6c958..b76176de0 100644 --- a/packages/web/src/components/__tests__/DashboardNotificationButton.test.tsx +++ b/packages/web/src/components/__tests__/DashboardNotificationButton.test.tsx @@ -205,4 +205,40 @@ describe("DashboardNotificationButton", () => { "https://github.com/acme/app/actions/runs/1", ); }); + + it("does not render unsafe notification URLs", () => { + muxValue.notifications = [ + { + ...makeNotification("1", "Suspicious notification"), + event: { + ...makeNotification("1", "Suspicious notification").event, + data: makeV3Data({ + subject: { + session: { id: "worker-1", projectId: "demo" }, + pr: { number: 1, url: "javascript:alert(1)" }, + }, + review: { url: "data:text/html," }, + }), + }, + actions: [ + { label: "Unsafe action", url: "javascript:alert(1)" }, + { label: "Unsafe external action", url: "https://evil.example/phish" }, + { label: "Safe action", url: "https://github.com/acme/app/actions/runs/1" }, + ], + }, + ]; + + render(); + + fireEvent.mouseEnter(screen.getByRole("button", { name: "Notifications" })); + + expect(screen.queryByRole("link", { name: "PR" })).not.toBeInTheDocument(); + expect(screen.queryByRole("link", { name: "Review" })).not.toBeInTheDocument(); + expect(screen.queryByRole("link", { name: "Unsafe action" })).not.toBeInTheDocument(); + expect(screen.queryByRole("link", { name: "Unsafe external action" })).not.toBeInTheDocument(); + expect(screen.getByRole("link", { name: "Safe action" })).toHaveAttribute( + "href", + "https://github.com/acme/app/actions/runs/1", + ); + }); }); diff --git a/website/content/docs/plugins/notifiers/composio.mdx b/website/content/docs/plugins/notifiers/composio.mdx index b6cb166d8..72f62f84a 100644 --- a/website/content/docs/plugins/notifiers/composio.mdx +++ b/website/content/docs/plugins/notifiers/composio.mdx @@ -37,7 +37,7 @@ You can also run the scriptable Slack setup directly: ```bash export COMPOSIO_API_KEY=ak_... -ao setup composio-slack --user-id ao-agent --channel "#agents" --non-interactive +ao setup composio-slack --user-id aoagent --channel "#agents" --non-interactive ``` All Composio setup commands accept `--routing-preset urgent-only`, @@ -99,7 +99,7 @@ notifiers: composio: plugin: composio defaultApp: slack # slack | discord | gmail - userId: ao-agent # Composio user id + userId: aoagent # Composio user id connectedAccountId: ca_... # preferred when available channelName: "#agents" # Slack emailTo: alerts@example.com # required when defaultApp=gmail @@ -110,27 +110,27 @@ notifiers: defaultApp: discord mode: webhook webhookUrl: https://discord.com/api/webhooks/... - userId: ao-agent + userId: aoagent composio-discord-bot: plugin: composio defaultApp: discord mode: bot channelId: "1234567890" - userId: ao-agent + userId: aoagent connectedAccountId: ca_... composio-mail: plugin: composio defaultApp: gmail emailTo: alerts@example.com - userId: ao-agent + userId: aoagent connectedAccountId: ca_... composio-slack: plugin: composio defaultApp: slack - userId: ao-agent + userId: aoagent connectedAccountId: ca_... channelName: "#agents" ``` @@ -141,7 +141,7 @@ notifiers: | -------------------- | --------------- | ---------------------- | -------------------------------------------------------------------- | | `defaultApp` | ✓ | `slack` | Which Composio app to target: `slack`, `discord`, or `gmail` | | `mode` | Discord only | auto | `webhook` uses Discord webhooks; `bot` uses bot channel messages | -| `userId` | optional | `ao-agent` | Composio user id for connected-account lookup | +| `userId` | optional | `aoagent` | Composio user id for connected-account lookup | | `entityId` | optional | — | Backward-compatible alias for `userId` | | `authConfigId` | setup only | — | Existing Composio Gmail auth config used by `--connect` | | `connectedAccountId` | Slack/Gmail/bot | — | Specific connected account to use; not used for Discord webhook mode |