feat(hooks): allow gmail tailscale target URLs
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
- Cron: `wakeMode: "now"` waits for heartbeat completion (and retries when the main lane is busy). (#666) — thanks @roshanasingh4.
|
||||
- Agents/OpenAI: fix Responses tool-only → follow-up turn handling (avoid standalone `reasoning` items that trigger 400 “required following item”).
|
||||
- Hooks/Gmail: keep Tailscale serve path at `/` while preserving the public path. (#668) — thanks @antons.
|
||||
- Hooks/Gmail: allow Tailscale target URLs to preserve internal serve paths.
|
||||
- Auth: update Claude Code keychain credentials in-place during refresh sync; share JSON file helpers; add CLI fallback coverage.
|
||||
- Auth: throttle external CLI credential syncs (Claude/Codex), reduce Keychain reads, and skip sync when cached credentials are still fresh.
|
||||
- CLI: respect `CLAWDBOT_STATE_DIR` for node pairing + voice wake settings storage. (#664) — thanks @azade-c.
|
||||
|
||||
@@ -105,8 +105,9 @@ Path note: when `tailscale.mode` is enabled, Clawdbot automatically sets
|
||||
`hooks.gmail.serve.path` to `/` and keeps the public path at
|
||||
`hooks.gmail.tailscale.path` (default `/gmail-pubsub`) because Tailscale
|
||||
strips the set-path prefix before proxying.
|
||||
If you need the backend to receive the prefixed path, Tailscale can proxy
|
||||
to a full target URL with a path, but Clawdbot currently passes only a port.
|
||||
If you need the backend to receive the prefixed path, set
|
||||
`hooks.gmail.tailscale.target` (or `--tailscale-target`) to a full URL like
|
||||
`http://127.0.0.1:8788/gmail-pubsub` and match `hooks.gmail.serve.path`.
|
||||
|
||||
Want a custom endpoint? Use `--push-endpoint <url>` or `--tailscale off`.
|
||||
|
||||
|
||||
@@ -292,7 +292,7 @@ Subcommands:
|
||||
Gmail Pub/Sub hook setup + runner. See [/automation/gmail-pubsub](/automation/gmail-pubsub).
|
||||
|
||||
Subcommands:
|
||||
- `hooks gmail setup` (requires `--account <email>`; supports `--project`, `--topic`, `--subscription`, `--label`, `--hook-url`, `--hook-token`, `--push-token`, `--bind`, `--port`, `--path`, `--include-body`, `--max-bytes`, `--renew-minutes`, `--tailscale`, `--tailscale-path`, `--push-endpoint`, `--json`)
|
||||
- `hooks gmail setup` (requires `--account <email>`; supports `--project`, `--topic`, `--subscription`, `--label`, `--hook-url`, `--hook-token`, `--push-token`, `--bind`, `--port`, `--path`, `--include-body`, `--max-bytes`, `--renew-minutes`, `--tailscale`, `--tailscale-path`, `--tailscale-target`, `--push-endpoint`, `--json`)
|
||||
- `hooks gmail run` (runtime overrides for the same flags)
|
||||
|
||||
### `dns setup`
|
||||
|
||||
@@ -2021,6 +2021,8 @@ Gateway auto-start:
|
||||
|
||||
Note: when `tailscale.mode` is on, Clawdbot defaults `serve.path` to `/` so
|
||||
Tailscale can proxy `/gmail-pubsub` correctly (it strips the set-path prefix).
|
||||
If you need the backend to receive the prefixed path, set
|
||||
`hooks.gmail.tailscale.target` to a full URL (and align `serve.path`).
|
||||
|
||||
### `canvasHost` (LAN/tailnet Canvas file server + live reload)
|
||||
|
||||
|
||||
@@ -71,6 +71,10 @@ export function registerHooksCli(program: Command) {
|
||||
"funnel",
|
||||
)
|
||||
.option("--tailscale-path <path>", "Path for tailscale serve/funnel")
|
||||
.option(
|
||||
"--tailscale-target <target>",
|
||||
"Tailscale serve/funnel target (port, host:port, or URL)",
|
||||
)
|
||||
.option("--push-endpoint <url>", "Explicit Pub/Sub push endpoint")
|
||||
.option("--json", "Output JSON summary", false)
|
||||
.action(async (opts) => {
|
||||
@@ -104,6 +108,10 @@ export function registerHooksCli(program: Command) {
|
||||
"Expose push endpoint via tailscale (funnel|serve|off)",
|
||||
)
|
||||
.option("--tailscale-path <path>", "Path for tailscale serve/funnel")
|
||||
.option(
|
||||
"--tailscale-target <target>",
|
||||
"Tailscale serve/funnel target (port, host:port, or URL)",
|
||||
)
|
||||
.action(async (opts) => {
|
||||
try {
|
||||
const parsed = parseGmailRunOptions(opts);
|
||||
@@ -138,6 +146,7 @@ function parseGmailSetupOptions(
|
||||
renewEveryMinutes: numberOption(raw.renewMinutes),
|
||||
tailscale: stringOption(raw.tailscale) as GmailSetupOptions["tailscale"],
|
||||
tailscalePath: stringOption(raw.tailscalePath),
|
||||
tailscaleTarget: stringOption(raw.tailscaleTarget),
|
||||
pushEndpoint: stringOption(raw.pushEndpoint),
|
||||
json: Boolean(raw.json),
|
||||
};
|
||||
@@ -160,6 +169,7 @@ function parseGmailRunOptions(raw: Record<string, unknown>): GmailRunOptions {
|
||||
renewEveryMinutes: numberOption(raw.renewMinutes),
|
||||
tailscale: stringOption(raw.tailscale) as GmailRunOptions["tailscale"],
|
||||
tailscalePath: stringOption(raw.tailscalePath),
|
||||
tailscaleTarget: stringOption(raw.tailscaleTarget),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -297,6 +297,8 @@ export type HooksGmailConfig = {
|
||||
tailscale?: {
|
||||
mode?: HooksGmailTailscaleMode;
|
||||
path?: string;
|
||||
/** Optional tailscale serve/funnel target (port, host:port, or full URL). */
|
||||
target?: string;
|
||||
};
|
||||
/** Optional model override for Gmail hook processing (provider/model or alias). */
|
||||
model?: string;
|
||||
|
||||
@@ -962,6 +962,7 @@ const HooksGmailSchema = z
|
||||
.union([z.literal("off"), z.literal("serve"), z.literal("funnel")])
|
||||
.optional(),
|
||||
path: z.string().optional(),
|
||||
target: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
model: z.string().optional(),
|
||||
|
||||
@@ -60,6 +60,7 @@ export type GmailSetupOptions = {
|
||||
renewEveryMinutes?: number;
|
||||
tailscale?: "off" | "serve" | "funnel";
|
||||
tailscalePath?: string;
|
||||
tailscaleTarget?: string;
|
||||
pushEndpoint?: string;
|
||||
json?: boolean;
|
||||
};
|
||||
@@ -80,6 +81,7 @@ export type GmailRunOptions = {
|
||||
renewEveryMinutes?: number;
|
||||
tailscale?: "off" | "serve" | "funnel";
|
||||
tailscalePath?: string;
|
||||
tailscaleTarget?: string;
|
||||
};
|
||||
|
||||
const DEFAULT_GMAIL_TOPIC_IAM_MEMBER =
|
||||
@@ -134,11 +136,18 @@ export async function runGmailSetup(opts: GmailSetupOptions) {
|
||||
const serveBind = opts.bind ?? DEFAULT_GMAIL_SERVE_BIND;
|
||||
const servePort = opts.port ?? DEFAULT_GMAIL_SERVE_PORT;
|
||||
const configuredServePath = opts.path ?? baseConfig.hooks?.gmail?.serve?.path;
|
||||
const configuredTailscaleTarget =
|
||||
opts.tailscaleTarget ?? baseConfig.hooks?.gmail?.tailscale?.target;
|
||||
const normalizedServePath =
|
||||
typeof configuredServePath === "string" &&
|
||||
configuredServePath.trim().length > 0
|
||||
? normalizeServePath(configuredServePath)
|
||||
: DEFAULT_GMAIL_SERVE_PATH;
|
||||
const normalizedTailscaleTarget =
|
||||
typeof configuredTailscaleTarget === "string" &&
|
||||
configuredTailscaleTarget.trim().length > 0
|
||||
? configuredTailscaleTarget.trim()
|
||||
: undefined;
|
||||
|
||||
const includeBody = opts.includeBody ?? true;
|
||||
const maxBytes = opts.maxBytes ?? DEFAULT_GMAIL_MAX_BYTES;
|
||||
@@ -149,7 +158,9 @@ export async function runGmailSetup(opts: GmailSetupOptions) {
|
||||
// Tailscale strips the path before proxying; keep a public path while gog
|
||||
// listens on "/" whenever Tailscale is enabled.
|
||||
const servePath = normalizeServePath(
|
||||
tailscaleMode !== "off" ? "/" : normalizedServePath,
|
||||
tailscaleMode !== "off" && !normalizedTailscaleTarget
|
||||
? "/"
|
||||
: normalizedServePath,
|
||||
);
|
||||
const tailscalePath = normalizeServePath(
|
||||
opts.tailscalePath ??
|
||||
@@ -189,6 +200,7 @@ export async function runGmailSetup(opts: GmailSetupOptions) {
|
||||
mode: tailscaleMode,
|
||||
path: tailscalePath,
|
||||
port: servePort,
|
||||
target: normalizedTailscaleTarget,
|
||||
token: pushToken,
|
||||
});
|
||||
|
||||
@@ -236,6 +248,7 @@ export async function runGmailSetup(opts: GmailSetupOptions) {
|
||||
...baseConfig.hooks?.gmail?.tailscale,
|
||||
mode: tailscaleMode,
|
||||
path: tailscalePath,
|
||||
target: normalizedTailscaleTarget,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -299,6 +312,7 @@ export async function runGmailService(opts: GmailRunOptions) {
|
||||
renewEveryMinutes: opts.renewEveryMinutes,
|
||||
tailscaleMode: opts.tailscale,
|
||||
tailscalePath: opts.tailscalePath,
|
||||
tailscaleTarget: opts.tailscaleTarget,
|
||||
};
|
||||
|
||||
const resolved = resolveGmailHookRuntimeConfig(config, overrides);
|
||||
@@ -314,6 +328,7 @@ export async function runGmailService(opts: GmailRunOptions) {
|
||||
mode: runtimeConfig.tailscale.mode,
|
||||
path: runtimeConfig.tailscale.path,
|
||||
port: runtimeConfig.serve.port,
|
||||
target: runtimeConfig.tailscale.target,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -261,7 +261,8 @@ export async function ensureSubscription(
|
||||
export async function ensureTailscaleEndpoint(params: {
|
||||
mode: "off" | "serve" | "funnel";
|
||||
path: string;
|
||||
port: number;
|
||||
port?: number;
|
||||
target?: string;
|
||||
token?: string;
|
||||
}): Promise<string> {
|
||||
if (params.mode === "off") return "";
|
||||
@@ -285,7 +286,15 @@ export async function ensureTailscaleEndpoint(params: {
|
||||
throw new Error("tailscale DNS name missing; run tailscale up");
|
||||
}
|
||||
|
||||
const target = String(params.port);
|
||||
const target =
|
||||
typeof params.target === "string" && params.target.trim().length > 0
|
||||
? params.target.trim()
|
||||
: params.port
|
||||
? String(params.port)
|
||||
: "";
|
||||
if (!target) {
|
||||
throw new Error("tailscale target missing; set a port or target URL");
|
||||
}
|
||||
const pathArg = normalizeServePath(params.path);
|
||||
const funnelArgs = [
|
||||
params.mode,
|
||||
|
||||
@@ -156,6 +156,7 @@ export async function startGmailWatcher(
|
||||
mode: runtimeConfig.tailscale.mode,
|
||||
path: runtimeConfig.tailscale.path,
|
||||
port: runtimeConfig.serve.port,
|
||||
target: runtimeConfig.tailscale.target,
|
||||
});
|
||||
log.info(
|
||||
`tailscale ${runtimeConfig.tailscale.mode} configured for port ${runtimeConfig.serve.port}`,
|
||||
|
||||
@@ -130,4 +130,33 @@ describe("gmail hook config", () => {
|
||||
expect(result.value.tailscale.path).toBe("/custom");
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps serve path when tailscale target is set", () => {
|
||||
const result = resolveGmailHookRuntimeConfig(
|
||||
{
|
||||
hooks: {
|
||||
token: "hook-token",
|
||||
gmail: {
|
||||
account: "clawdbot@gmail.com",
|
||||
topic: "projects/demo/topics/gog-gmail-watch",
|
||||
pushToken: "push-token",
|
||||
serve: { path: "/custom" },
|
||||
tailscale: {
|
||||
mode: "funnel",
|
||||
target: "http://127.0.0.1:8788/custom",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{},
|
||||
);
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(result.value.serve.path).toBe("/custom");
|
||||
expect(result.value.tailscale.path).toBe("/custom");
|
||||
expect(result.value.tailscale.target).toBe(
|
||||
"http://127.0.0.1:8788/custom",
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,6 +33,7 @@ export type GmailHookOverrides = {
|
||||
servePath?: string;
|
||||
tailscaleMode?: HooksGmailTailscaleMode;
|
||||
tailscalePath?: string;
|
||||
tailscaleTarget?: string;
|
||||
};
|
||||
|
||||
export type GmailHookRuntimeConfig = {
|
||||
@@ -54,6 +55,7 @@ export type GmailHookRuntimeConfig = {
|
||||
tailscale: {
|
||||
mode: HooksGmailTailscaleMode;
|
||||
path: string;
|
||||
target?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -164,12 +166,20 @@ export function resolveGmailHookRuntimeConfig(
|
||||
typeof servePathRaw === "string" && servePathRaw.trim().length > 0
|
||||
? normalizeServePath(servePathRaw)
|
||||
: DEFAULT_GMAIL_SERVE_PATH;
|
||||
const tailscaleTargetRaw =
|
||||
overrides.tailscaleTarget ?? gmail?.tailscale?.target;
|
||||
|
||||
const tailscaleMode =
|
||||
overrides.tailscaleMode ?? gmail?.tailscale?.mode ?? "off";
|
||||
const tailscaleTarget =
|
||||
tailscaleMode !== "off" &&
|
||||
typeof tailscaleTargetRaw === "string" &&
|
||||
tailscaleTargetRaw.trim().length > 0
|
||||
? tailscaleTargetRaw.trim()
|
||||
: undefined;
|
||||
// Tailscale strips the public path before proxying, so listen on "/" when on.
|
||||
const servePath = normalizeServePath(
|
||||
tailscaleMode !== "off" ? "/" : normalizedServePathRaw,
|
||||
tailscaleMode !== "off" && !tailscaleTarget ? "/" : normalizedServePathRaw,
|
||||
);
|
||||
|
||||
const tailscalePathRaw = overrides.tailscalePath ?? gmail?.tailscale?.path;
|
||||
@@ -200,6 +210,7 @@ export function resolveGmailHookRuntimeConfig(
|
||||
tailscale: {
|
||||
mode: tailscaleMode,
|
||||
path: tailscalePath,
|
||||
target: tailscaleTarget,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user