diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d2d3bfdb..a5b52384e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Custom model providers: `models.providers` merges into `~/.clawdis/agent/models.json` (merge/replace modes) for LiteLLM, local OpenAI-compatible servers, Anthropic proxies, etc. - Group chat activation modes: per-group `/activation mention|always` command with status visibility. - Gateway webhooks: external `wake` and isolated `agent` hooks with dedicated token auth. +- Hook mappings + Gmail Pub/Sub helper (`clawdis hooks gmail setup/run`) with auto-renew + Tailscale Funnel support. ### Breaking - Config refactor: `inbound.*` removed; use top-level `routing` (allowlists + group rules + transcription), `messages` (prefixes/timestamps), and `session` (scoping/store/mainKey). No legacy keys read. diff --git a/docs/configuration.md b/docs/configuration.md index d30873cdb..d5995e756 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -318,13 +318,27 @@ Enable a simple HTTP webhook surface on the Gateway HTTP server. Defaults: - enabled: `false` - path: `/hooks` +- maxBodyBytes: `262144` (256 KB) ```json5 { hooks: { enabled: true, token: "shared-secret", - path: "/hooks" + path: "/hooks", + presets: ["gmail"], + transformsDir: "~/.clawdis/hooks", + mappings: [ + { + match: { path: "gmail" }, + action: "agent", + wakeMode: "now", + name: "Gmail", + sessionKey: "hook:gmail:{{messages[0].id}}", + messageTemplate: + "From: {{messages[0].from}}\nSubject: {{messages[0].subject}}\n{{messages[0].snippet}}", + }, + ], } } ``` @@ -337,9 +351,37 @@ Requests must include the hook token: Endpoints: - `POST /hooks/wake` → `{ text, mode?: "now"|"next-heartbeat" }` - `POST /hooks/agent` → `{ message, name?, sessionKey?, wakeMode?, deliver?, channel?, to?, thinking?, timeoutSeconds? }` +- `POST /hooks/` → resolved via `hooks.mappings` `/hooks/agent` always posts a summary into the main session (and can optionally trigger an immediate heartbeat via `wakeMode: "now"`). +Mapping notes: +- `match.path` matches the sub-path after `/hooks` (e.g. `/hooks/gmail` → `gmail`). +- `match.source` matches a payload field (e.g. `{ source: "gmail" }`) so you can use a generic `/hooks/ingest` path. +- Templates like `{{messages[0].subject}}` read from the payload. +- `transform` can point to a JS/TS module that returns a hook action. + +Gmail helper config (used by `clawdis hooks gmail setup` / `run`): + +```json5 +{ + hooks: { + gmail: { + account: "clawdbot@gmail.com", + topic: "projects//topics/gog-gmail-watch", + subscription: "gog-gmail-watch-push", + pushToken: "shared-push-token", + hookUrl: "http://127.0.0.1:18789/hooks/gmail", + includeBody: true, + maxBytes: 20000, + renewEveryMinutes: 720, + serve: { bind: "127.0.0.1", port: 8788, path: "/gmail-pubsub" }, + tailscale: { mode: "funnel", path: "/gmail-pubsub" }, + } + } +} +``` + ### `canvasHost` (LAN/tailnet Canvas file server + live reload) The Gateway serves a directory of HTML/CSS/JS over HTTP so iOS/Android nodes can simply `canvas.navigate` to it. diff --git a/docs/gmail-pubsub.md b/docs/gmail-pubsub.md new file mode 100644 index 000000000..57e867e6b --- /dev/null +++ b/docs/gmail-pubsub.md @@ -0,0 +1,179 @@ +--- +summary: "Gmail Pub/Sub push wired into Clawdis webhooks via gogcli" +read_when: + - Wiring Gmail inbox triggers to Clawdis + - Setting up Pub/Sub push for agent wake +--- + +# Gmail Pub/Sub -> Clawdis + +Goal: Gmail watch -> Pub/Sub push -> `gog gmail watch serve` -> Clawdis webhook. + +## Prereqs + +- `gcloud` installed and logged in. +- `gog` (gogcli) installed and authorized for the Gmail account. +- Clawdis hooks enabled (see `docs/webhook.md`). +- `tailscale` logged in if you want a public HTTPS endpoint via Funnel. + +Example hook config (enable Gmail preset mapping): + +```json5 +{ + hooks: { + enabled: true, + token: "CLAWDIS_HOOK_TOKEN", + path: "/hooks", + presets: ["gmail"] + } +} +``` + +To customize payload handling, add `hooks.mappings` or a JS/TS transform module +under `hooks.transformsDir` (see `docs/webhook.md`). + +## Wizard (recommended) + +Use the Clawdis helper to wire everything together (installs deps on macOS via brew): + +```bash +clawdis hooks gmail setup \ + --account clawdbot@gmail.com +``` + +Defaults: +- Uses Tailscale Funnel for the public push endpoint. +- Writes `hooks.gmail` config for `clawdis hooks gmail run`. +- Enables the Gmail hook preset (`hooks.presets: ["gmail"]`). + +Want a custom endpoint? Use `--push-endpoint ` or `--tailscale off`. + +Platform note: on macOS the wizard installs `gcloud`, `gogcli`, and `tailscale` +via Homebrew; on Linux install them manually first. + +Run the daemon (starts `gog gmail watch serve` + auto-renew): + +```bash +clawdis hooks gmail run +``` + +## One-time setup + +1) Select the GCP project **that owns the OAuth client** used by `gog`. + +```bash +gcloud auth login +gcloud config set project +``` + +Note: Gmail watch requires the Pub/Sub topic to live in the same project as the OAuth client. + +2) Enable APIs: + +```bash +gcloud services enable gmail.googleapis.com pubsub.googleapis.com +``` + +3) Create a topic: + +```bash +gcloud pubsub topics create gog-gmail-watch +``` + +4) Allow Gmail push to publish: + +```bash +gcloud pubsub topics add-iam-policy-binding gog-gmail-watch \ + --member=serviceAccount:gmail-api-push@system.gserviceaccount.com \ + --role=roles/pubsub.publisher +``` + +## Start the watch + +```bash +gog gmail watch start \ + --account clawdbot@gmail.com \ + --label INBOX \ + --topic projects//topics/gog-gmail-watch +``` + +Save the `history_id` from the output (for debugging). + +## Run the push handler + +Local example (shared token auth): + +```bash +gog gmail watch serve \ + --account clawdbot@gmail.com \ + --bind 127.0.0.1 \ + --port 8788 \ + --path /gmail-pubsub \ + --token \ + --hook-url http://127.0.0.1:18789/hooks/gmail \ + --hook-token CLAWDIS_HOOK_TOKEN \ + --include-body \ + --max-bytes 20000 +``` + +Notes: +- `--token` protects the push endpoint (`x-gog-token` or `?token=`). +- `--hook-url` points to Clawdis `/hooks/gmail` (mapped; isolated run + summary to main). +- `--include-body` and `--max-bytes` control the body snippet sent to Clawdis. + +Recommended: `clawdis hooks gmail run` wraps the same flow and auto-renews the watch. + +## Expose the handler (dev) + +For local testing, tunnel the handler and use the public URL in the push subscription: + +```bash +cloudflared tunnel --url http://127.0.0.1:8788 --no-autoupdate +``` + +Use the generated URL as the push endpoint: + +```bash +gcloud pubsub subscriptions create gog-gmail-watch-push \ + --topic gog-gmail-watch \ + --push-endpoint "https:///gmail-pubsub?token=" +``` + +Production: use a stable HTTPS endpoint and configure Pub/Sub OIDC JWT, then run: + +```bash +gog gmail watch serve --verify-oidc --oidc-email +``` + +## Test + +Send a message to the watched inbox: + +```bash +gog gmail send \ + --account clawdbot@gmail.com \ + --to clawdbot@gmail.com \ + --subject "watch test" \ + --body "ping" +``` + +Check watch state and history: + +```bash +gog gmail watch status --account clawdbot@gmail.com +gog gmail history --account clawdbot@gmail.com --since +``` + +## Troubleshooting + +- `Invalid topicName`: project mismatch (topic not in the OAuth client project). +- `User not authorized`: missing `roles/pubsub.publisher` on the topic. +- Empty messages: Gmail push only provides `historyId`; fetch via `gog gmail history`. + +## Cleanup + +```bash +gog gmail watch stop --account clawdbot@gmail.com +gcloud pubsub subscriptions delete gog-gmail-watch-push +gcloud pubsub topics delete gog-gmail-watch +``` diff --git a/docs/webhook.md b/docs/webhook.md index 2194e2f88..b72b40774 100644 --- a/docs/webhook.md +++ b/docs/webhook.md @@ -80,6 +80,20 @@ Effect: - Always posts a summary into the **main** session - If `wakeMode=now`, triggers an immediate heartbeat +### `POST /hooks/` (mapped) + +Custom hook names are resolved via `hooks.mappings` (see configuration). A mapping can +turn arbitrary payloads into `wake` or `agent` actions, with optional templates or +code transforms. + +Mapping options (summary): +- `hooks.presets: ["gmail"]` enables the built-in Gmail mapping. +- `hooks.mappings` lets you define `match`, `action`, and templates in config. +- `hooks.transformsDir` + `transform.module` loads a JS/TS module for custom logic. +- Use `match.source` to keep a generic ingest endpoint (payload-driven routing). +- TS transforms require a TS loader (e.g. `tsx`) or precompiled `.js` at runtime. +- `clawdis hooks gmail setup` writes `hooks.gmail` config for `clawdis hooks gmail run`. + ## Responses - `200` for `/hooks/wake` @@ -104,6 +118,13 @@ curl -X POST http://127.0.0.1:18789/hooks/agent \ -d '{"message":"Summarize inbox","name":"Email","wakeMode":"next-heartbeat"}' ``` +```bash +curl -X POST http://127.0.0.1:18789/hooks/gmail \ + -H 'Authorization: Bearer SECRET' \ + -H 'Content-Type: application/json' \ + -d '{"source":"gmail","messages":[{"from":"Ada","subject":"Hello","snippet":"Hi"}]}' +``` + ## Security - Keep hook endpoints behind loopback, tailnet, or trusted reverse proxy. diff --git a/src/cli/hooks-cli.ts b/src/cli/hooks-cli.ts new file mode 100644 index 000000000..23dbfac9a --- /dev/null +++ b/src/cli/hooks-cli.ts @@ -0,0 +1,182 @@ +import type { Command } from "commander"; + +import { danger } from "../globals.js"; +import { + DEFAULT_GMAIL_LABEL, + DEFAULT_GMAIL_MAX_BYTES, + DEFAULT_GMAIL_RENEW_MINUTES, + DEFAULT_GMAIL_SERVE_BIND, + DEFAULT_GMAIL_SERVE_PATH, + DEFAULT_GMAIL_SERVE_PORT, + DEFAULT_GMAIL_SUBSCRIPTION, + DEFAULT_GMAIL_TOPIC, +} from "../hooks/gmail.js"; +import { + type GmailRunOptions, + type GmailSetupOptions, + runGmailService, + runGmailSetup, +} from "../hooks/gmail-ops.js"; +import { defaultRuntime } from "../runtime.js"; + +export function registerHooksCli(program: Command) { + const hooks = program + .command("hooks") + .description("Webhook helpers and hook-based integrations"); + + const gmail = hooks + .command("gmail") + .description("Gmail Pub/Sub hooks (via gogcli)"); + + gmail + .command("setup") + .description("Configure Gmail watch + Pub/Sub + Clawdis hooks") + .requiredOption("--account ", "Gmail account to watch") + .option("--project ", "GCP project id (OAuth client owner)") + .option("--topic ", "Pub/Sub topic name", DEFAULT_GMAIL_TOPIC) + .option( + "--subscription ", + "Pub/Sub subscription name", + DEFAULT_GMAIL_SUBSCRIPTION, + ) + .option("--label