Skip to content

Commit 329f154

Browse files
authored
Merge c2aa369 into 56c2d63
2 parents 56c2d63 + c2aa369 commit 329f154

16 files changed

Lines changed: 749 additions & 63 deletions

docs/automation/cron-jobs.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ Keep hook endpoints behind loopback, tailnet, or trusted reverse proxy.
372372
Wire Gmail inbox triggers to OpenClaw via Google PubSub.
373373

374374
<Note>
375-
**Prerequisites:** `gcloud` CLI, `gog` (gogcli), OpenClaw hooks enabled, Tailscale for the public HTTPS endpoint.
375+
**Prerequisites:** `gcloud` CLI, `gog` (gogcli), and OpenClaw hooks enabled. The `openclaw webhooks gmail setup` helper still configures the Pub/Sub push flow, which also needs Tailscale or another HTTPS push endpoint.
376376
</Note>
377377

378378
### Wizard setup (recommended)
@@ -385,7 +385,7 @@ This writes `hooks.gmail` config, enables the Gmail preset, and uses Tailscale F
385385

386386
### Gateway auto-start
387387

388-
When `hooks.enabled=true` and `hooks.gmail.account` is set, the Gateway starts `gog gmail watch serve` on boot and auto-renews the watch. Set `OPENCLAW_SKIP_GMAIL_WATCHER=1` to opt out.
388+
When `hooks.enabled=true` and `hooks.gmail.account` is set, the Gateway starts the configured `gog gmail watch` delivery runner on boot and auto-renews the watch. Push configs start `gog gmail watch serve`; pull configs start `gog gmail watch pull`. Set `OPENCLAW_SKIP_GMAIL_WATCHER=1` to opt out.
389389

390390
### Manual one-time setup
391391

docs/cli/webhooks.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ openclaw webhooks gmail setup --account <email> [...]
1717
openclaw webhooks gmail run [--account <email>] [...]
1818
```
1919

20-
| Subcommand | Description |
21-
| ------------- | -------------------------------------------------------------------------------------------- |
22-
| `gmail setup` | Configure Gmail watch, Pub/Sub topic/subscription, and the OpenClaw webhook delivery target. |
23-
| `gmail run` | Run `gog watch serve` plus the watch auto-renew loop. |
20+
| Subcommand | Description |
21+
| ------------- | ---------------------------------------------------------------------------------------- |
22+
| `gmail setup` | Configure the existing Gmail Pub/Sub push flow and the OpenClaw webhook delivery target. |
23+
| `gmail run` | Run the configured `gog gmail watch` delivery process plus the watch auto-renew loop. |
2424

2525
## `webhooks gmail setup`
2626

27-
Configure Gmail watch, Pub/Sub, and OpenClaw webhook delivery.
27+
Configure Gmail watch, Pub/Sub push, and OpenClaw webhook delivery.
2828

2929
```bash
3030
openclaw webhooks gmail setup --account [email protected]
@@ -83,17 +83,18 @@ openclaw webhooks gmail setup --account [email protected] --hook-url https://gatew
8383

8484
## `webhooks gmail run`
8585

86-
Run `gog watch serve` plus the watch auto-renew loop in the foreground.
86+
Run the configured `gog gmail watch` delivery process plus the watch auto-renew loop in the foreground. Push configs start `gog gmail watch serve`; pull configs start `gog gmail watch pull`.
8787

8888
```bash
8989
openclaw webhooks gmail run --account [email protected]
9090
```
9191

92-
`run` accepts the same `gog watch serve`, OpenClaw delivery, Pub/Sub, and Tailscale flags as `setup`, except:
92+
`run` accepts the same push-oriented `gog watch serve`, OpenClaw delivery, Pub/Sub, and Tailscale flags as `setup`, except:
9393

9494
- `--account` is **optional** on `run` (it falls back to the configured account).
9595
- `run` does **not** accept `--project`, `--push-endpoint`, or `--json`.
9696
- `run` flags have no built-in defaults; missing values fall back to the values written by `setup`.
97+
- Pull mode is selected from `hooks.gmail.delivery.mode` in config. Use `hooks.gmail.delivery.subscription` for the full pull subscription path.
9798

9899
| Category | Flags |
99100
| ----------------- | -------------------------------------------------------------------------------- |

docs/gateway/configuration-reference.md

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -790,23 +790,49 @@ Validation and safety notes:
790790
gmail: {
791791
account: "[email protected]",
792792
topic: "projects/<project-id>/topics/gog-gmail-watch",
793-
subscription: "gog-gmail-watch-push",
794-
pushToken: "shared-push-token",
793+
delivery: {
794+
mode: "pull",
795+
subscription: "projects/<project-id>/subscriptions/gog-gmail-watch",
796+
},
795797
hookUrl: "http://127.0.0.1:18789/hooks/gmail",
796798
includeBody: true,
797799
maxBytes: 20000,
798800
renewEveryMinutes: 720,
799-
serve: { bind: "127.0.0.1", port: 8788, path: "/" },
800-
tailscale: { mode: "funnel", path: "/gmail-pubsub" },
801801
model: "openrouter/meta-llama/llama-3.3-70b-instruct:free",
802802
thinking: "off",
803803
},
804804
},
805805
}
806806
```
807807

808-
- Gateway auto-starts `gog gmail watch serve` on boot when configured. Set `OPENCLAW_SKIP_GMAIL_WATCHER=1` to disable.
809-
- Don't run a separate `gog gmail watch serve` alongside the Gateway.
808+
- Pull delivery starts `gog gmail watch pull` on boot. It consumes a Pub/Sub pull subscription from the gateway host and posts processed Gmail notifications into the local OpenClaw hook endpoint. It does not need `pushToken`, `serve`, `tailscale`, or a public HTTP callback URL.
809+
- Pull delivery requires `delivery.subscription` to be a full Pub/Sub subscription resource path: `projects/<project-id>/subscriptions/<subscription>`.
810+
- Do not run a separate `gog gmail watch pull` against the same subscription alongside the Gateway.
811+
- Set `OPENCLAW_SKIP_GMAIL_WATCHER=1` to disable the Gateway-managed Gmail watcher.
812+
813+
Push delivery is still supported for existing deployments and explicit HTTP ingress setups. Omit `delivery.mode` to preserve existing push behavior, or set it explicitly:
814+
815+
```json5
816+
{
817+
hooks: {
818+
gmail: {
819+
account: "[email protected]",
820+
topic: "projects/<project-id>/topics/gog-gmail-watch",
821+
delivery: {
822+
mode: "push",
823+
subscription: "gog-gmail-watch-push",
824+
},
825+
pushToken: "shared-push-token",
826+
hookUrl: "http://127.0.0.1:18789/hooks/gmail",
827+
serve: { bind: "127.0.0.1", port: 8788, path: "/" },
828+
tailscale: { mode: "funnel", path: "/gmail-pubsub" },
829+
},
830+
},
831+
}
832+
```
833+
834+
- Push delivery starts `gog gmail watch serve` and requires a Pub/Sub push subscription to reach the callback endpoint.
835+
- `openclaw webhooks gmail setup` remains push-oriented in this release. Pull-mode cloud setup is a later slice after pull delivery is dogfooded.
810836

811837
---
812838

src/cli/webhooks-cli.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export function registerWebhooksCli(program: Command) {
3939

4040
gmail
4141
.command("setup")
42-
.description("Configure Gmail watch + Pub/Sub + OpenClaw hooks")
42+
.description("Configure Gmail watch + Pub/Sub push + OpenClaw hooks")
4343
.requiredOption("--account <email>", "Gmail account to watch")
4444
.option("--project <id>", "GCP project id (OAuth client owner)")
4545
.option("--topic <name>", "Pub/Sub topic name", DEFAULT_GMAIL_TOPIC)
@@ -78,10 +78,10 @@ export function registerWebhooksCli(program: Command) {
7878

7979
gmail
8080
.command("run")
81-
.description("Run gog watch serve + auto-renew loop")
81+
.description("Run configured gog gmail watch delivery + auto-renew loop")
8282
.option("--account <email>", "Gmail account to watch")
8383
.option("--topic <topic>", "Pub/Sub topic path (projects/.../topics/..)")
84-
.option("--subscription <name>", "Pub/Sub subscription name")
84+
.option("--subscription <name>", "Pub/Sub subscription name or full path for pull configs")
8585
.option("--label <label>", "Gmail label to watch")
8686
.option("--hook-url <url>", "OpenClaw hook URL")
8787
.option("--hook-token <token>", "OpenClaw hook token")

src/config/schema.help.quality.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ const TARGET_KEYS = [
242242
"hooks.mappings[].channel",
243243
"hooks.mappings[].transform.module",
244244
"hooks.gmail",
245+
"hooks.gmail.delivery.mode",
245246
"hooks.gmail.pushToken",
246247
"hooks.gmail.tailscale.mode",
247248
"hooks.gmail.thinking",
@@ -443,6 +444,7 @@ const ENUM_EXPECTATIONS: Record<string, string[]> = {
443444
"broadcast.strategy": ['"parallel"', '"sequential"'],
444445
"hooks.mappings[].action": ['"wake"', '"agent"'],
445446
"hooks.mappings[].wakeMode": ['"now"', '"next-heartbeat"'],
447+
"hooks.gmail.delivery.mode": ['"push"', '"pull"'],
446448
"hooks.gmail.tailscale.mode": ['"off"', '"serve"', '"funnel"'],
447449
"hooks.gmail.thinking": ['"off"', '"minimal"', '"low"', '"medium"', '"high"'],
448450
"messages.queue.mode": ['"steer"', '"followup"', '"collect"', '"interrupt"'],
@@ -479,6 +481,9 @@ const ENUM_EXPECTATIONS: Record<string, string[]> = {
479481
const TOOLS_HOOKS_TARGET_KEYS = [
480482
"hooks.gmail.account",
481483
"hooks.gmail.allowUnsafeExternalContent",
484+
"hooks.gmail.delivery",
485+
"hooks.gmail.delivery.mode",
486+
"hooks.gmail.delivery.subscription",
482487
"hooks.gmail.hookUrl",
483488
"hooks.gmail.includeBody",
484489
"hooks.gmail.label",

src/config/schema.help.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1824,17 +1824,23 @@ export const FIELD_HELP: Record<string, string> = {
18241824
"hooks.mappings[].transform.export":
18251825
"Named export to invoke from the transform module; defaults to module default export when omitted. Set this when one file hosts multiple transform handlers.",
18261826
"hooks.gmail":
1827-
"Gmail push integration settings used for Pub/Sub notifications and optional local callback serving. Keep this scoped to dedicated Gmail automation accounts where possible.",
1827+
"Gmail integration settings used for Pub/Sub change notifications and local hook delivery. Keep this scoped to dedicated Gmail automation accounts where possible.",
18281828
"hooks.gmail.account":
18291829
"Google account identifier used for Gmail watch/subscription operations in this hook integration. Use a dedicated automation mailbox account to isolate operational permissions.",
18301830
"hooks.gmail.label":
18311831
"Optional Gmail label filter limiting which labeled messages trigger hook events. Keep filters narrow to avoid flooding automations with unrelated inbox traffic.",
18321832
"hooks.gmail.topic":
18331833
"Google Pub/Sub topic name used by Gmail watch to publish change notifications for this account. Ensure the topic IAM grants Gmail publish access before enabling watches.",
18341834
"hooks.gmail.subscription":
1835-
"Pub/Sub subscription consumed by the gateway to receive Gmail change notifications from the configured topic. Keep subscription ownership clear so multiple consumers do not race unexpectedly.",
1835+
"Pub/Sub subscription name used by the Gmail integration. Push mode may use a short subscription name; pull mode should use delivery.subscription with a full Pub/Sub path.",
1836+
"hooks.gmail.delivery":
1837+
"Gmail Pub/Sub delivery mode configuration. Use pull for no-inbound local consumption, or push only when operating an intentional HTTP ingress path.",
1838+
"hooks.gmail.delivery.mode":
1839+
'Gmail Pub/Sub delivery mode. Use "pull" to start a local Pub/Sub consumer with no inbound HTTP endpoint, or "push" to start the existing HTTP callback server. Omitted mode preserves existing push behavior.',
1840+
"hooks.gmail.delivery.subscription":
1841+
"Full Pub/Sub subscription path used by pull mode, for example projects/<project>/subscriptions/<subscription>. Do not share one pull subscription between independent consumers.",
18361842
"hooks.gmail.hookUrl":
1837-
"Public callback URL Gmail or intermediaries invoke to deliver notifications into this hook pipeline. Keep this URL protected with token validation and restricted network exposure.",
1843+
"OpenClaw hook URL invoked by the Gmail watcher after notification processing. Pull mode normally uses a loopback URL; push mode may use the same local hook behind the callback server.",
18381844
"hooks.gmail.includeBody":
18391845
"When true, fetch and include email body content for downstream mapping/agent processing. Keep false unless body text is required, because this increases payload size and sensitivity.",
18401846
"hooks.gmail.allowUnsafeExternalContent":

src/config/schema.labels.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -923,6 +923,9 @@ export const FIELD_LABELS: Record<string, string> = {
923923
"hooks.gmail.label": "Gmail Hook Label",
924924
"hooks.gmail.topic": "Gmail Hook Pub/Sub Topic",
925925
"hooks.gmail.subscription": "Gmail Hook Subscription",
926+
"hooks.gmail.delivery": "Gmail Hook Delivery",
927+
"hooks.gmail.delivery.mode": "Gmail Hook Delivery Mode",
928+
"hooks.gmail.delivery.subscription": "Gmail Hook Pull Subscription",
926929
"hooks.gmail.pushToken": "Gmail Hook Push Token",
927930
"hooks.gmail.hookUrl": "Gmail Hook Callback URL",
928931
"hooks.gmail.includeBody": "Gmail Hook Include Body",

src/config/types.hooks.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,21 @@ export type HookMappingConfig = {
3737
};
3838

3939
export type HooksGmailTailscaleMode = "off" | "serve" | "funnel";
40+
export type HooksGmailDeliveryMode = "push" | "pull";
4041

4142
export type HooksGmailConfig = {
4243
account?: string;
4344
label?: string;
4445
topic?: string;
4546
subscription?: string;
47+
delivery?: {
48+
mode?: HooksGmailDeliveryMode;
49+
/**
50+
* Pull mode requires a full Pub/Sub resource path:
51+
* projects/<project>/subscriptions/<subscription>.
52+
*/
53+
subscription?: string;
54+
};
4655
pushToken?: string;
4756
hookUrl?: string;
4857
includeBody?: boolean;

src/config/zod-schema.hooks.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,13 @@ export const HooksGmailSchema = z
116116
label: z.string().optional(),
117117
topic: z.string().optional(),
118118
subscription: z.string().optional(),
119+
delivery: z
120+
.object({
121+
mode: z.union([z.literal("push"), z.literal("pull")]).optional(),
122+
subscription: z.string().optional(),
123+
})
124+
.strict()
125+
.optional(),
119126
pushToken: z.string().optional().register(sensitive),
120127
hookUrl: z.string().optional(),
121128
includeBody: z.boolean().optional(),

src/hooks/gmail-gog-capability.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Gmail gog capability checks keep pull delivery from respawning unsupported gog builds.
2+
import { runCommandWithTimeout } from "../process/exec.js";
3+
import {
4+
buildGogWatchPullHelpArgs,
5+
type GmailHookRuntimeConfig,
6+
isGmailHookPullRuntimeConfig,
7+
resolveGogExecutable,
8+
} from "./gmail.js";
9+
10+
const GOG_PULL_HELP_TIMEOUT_MS = 30_000;
11+
const GOG_PULL_HELP_OUTPUT_BYTES = 64 * 1024;
12+
13+
export type GmailWatchDeliverySupportResult = { ok: true } | { ok: false; error: string };
14+
15+
export async function checkGmailWatchDeliverySupported(
16+
cfg: GmailHookRuntimeConfig,
17+
options: { signal?: AbortSignal } = {},
18+
): Promise<GmailWatchDeliverySupportResult> {
19+
if (!isGmailHookPullRuntimeConfig(cfg)) {
20+
return { ok: true };
21+
}
22+
23+
const result = await runCommandWithTimeout(
24+
[resolveGogExecutable(), ...buildGogWatchPullHelpArgs()],
25+
{
26+
timeoutMs: GOG_PULL_HELP_TIMEOUT_MS,
27+
maxOutputBytes: GOG_PULL_HELP_OUTPUT_BYTES,
28+
signal: options.signal,
29+
},
30+
);
31+
if (result.code === 0) {
32+
return { ok: true };
33+
}
34+
35+
const detail = (result.stderr || result.stdout || "gog gmail watch pull --help failed").trim();
36+
return {
37+
ok: false,
38+
error: `gog gmail watch pull is unavailable; install gogcli with Pub/Sub pull support. ${detail}`,
39+
};
40+
}

0 commit comments

Comments
 (0)