fix(googlechat): inherit shared defaults for multi-account webhook auth (#38492)
* fix(googlechat): inherit shared defaults from accounts.default * fix(googlechat): do not inherit default enabled state * fix(googlechat): avoid inheriting default credentials * fix(googlechat): keep dangerous auth flags account-local
This commit is contained in:
@@ -219,6 +219,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/usage normalization: normalize missing or partial assistant usage snapshots before compaction accounting so `openclaw agent --json` no longer crashes when provider payloads omit `totalTokens` or related usage fields. (#34977) thanks @sp-hk2ldn.
|
||||
- Venice/default model refresh: switch the built-in Venice default to `kimi-k2-5`, update onboarding aliasing, and refresh Venice provider docs/recommendations to match the current private and anonymized catalog. (from #12964) Fixes #20156. Thanks @sabrinaaquino and @vincentkoc.
|
||||
- Agents/skill API write pacing: add a global prompt guardrail that treats skill-driven external API writes as rate-limited by default, so runners prefer batched writes, avoid tight request loops, and respect `429`/`Retry-After`. Thanks @vincentkoc.
|
||||
- Google Chat/multi-account webhook auth fallback: when `channels.googlechat.accounts.default` carries shared webhook audience/path settings (for example after config normalization), inherit those defaults for named accounts while preserving top-level and per-account overrides, so inbound webhook verification no longer fails silently for named accounts missing duplicated audience fields. Fixes #38369.
|
||||
|
||||
## 2026.3.2
|
||||
|
||||
|
||||
131
extensions/googlechat/src/accounts.test.ts
Normal file
131
extensions/googlechat/src/accounts.test.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/googlechat";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveGoogleChatAccount } from "./accounts.js";
|
||||
|
||||
describe("resolveGoogleChatAccount", () => {
|
||||
it("inherits shared defaults from accounts.default for named accounts", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
googlechat: {
|
||||
accounts: {
|
||||
default: {
|
||||
audienceType: "app-url",
|
||||
audience: "https://example.com/googlechat",
|
||||
webhookPath: "/googlechat",
|
||||
},
|
||||
andy: {
|
||||
serviceAccountFile: "/tmp/andy-sa.json",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const resolved = resolveGoogleChatAccount({ cfg, accountId: "andy" });
|
||||
expect(resolved.config.audienceType).toBe("app-url");
|
||||
expect(resolved.config.audience).toBe("https://example.com/googlechat");
|
||||
expect(resolved.config.webhookPath).toBe("/googlechat");
|
||||
expect(resolved.config.serviceAccountFile).toBe("/tmp/andy-sa.json");
|
||||
});
|
||||
|
||||
it("prefers top-level and account overrides over accounts.default", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
googlechat: {
|
||||
audienceType: "project-number",
|
||||
audience: "1234567890",
|
||||
accounts: {
|
||||
default: {
|
||||
audienceType: "app-url",
|
||||
audience: "https://default.example.com/googlechat",
|
||||
webhookPath: "/googlechat-default",
|
||||
},
|
||||
april: {
|
||||
webhookPath: "/googlechat-april",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const resolved = resolveGoogleChatAccount({ cfg, accountId: "april" });
|
||||
expect(resolved.config.audienceType).toBe("project-number");
|
||||
expect(resolved.config.audience).toBe("1234567890");
|
||||
expect(resolved.config.webhookPath).toBe("/googlechat-april");
|
||||
});
|
||||
|
||||
it("does not inherit disabled state from accounts.default for named accounts", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
googlechat: {
|
||||
accounts: {
|
||||
default: {
|
||||
enabled: false,
|
||||
audienceType: "app-url",
|
||||
audience: "https://example.com/googlechat",
|
||||
},
|
||||
andy: {
|
||||
serviceAccountFile: "/tmp/andy-sa.json",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const resolved = resolveGoogleChatAccount({ cfg, accountId: "andy" });
|
||||
expect(resolved.enabled).toBe(true);
|
||||
expect(resolved.config.enabled).toBeUndefined();
|
||||
expect(resolved.config.audienceType).toBe("app-url");
|
||||
});
|
||||
|
||||
it("does not inherit default-account credentials into named accounts", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
googlechat: {
|
||||
accounts: {
|
||||
default: {
|
||||
serviceAccountRef: {
|
||||
source: "env",
|
||||
provider: "test",
|
||||
id: "default-sa",
|
||||
},
|
||||
audienceType: "app-url",
|
||||
audience: "https://example.com/googlechat",
|
||||
},
|
||||
andy: {
|
||||
serviceAccountFile: "/tmp/andy-sa.json",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const resolved = resolveGoogleChatAccount({ cfg, accountId: "andy" });
|
||||
expect(resolved.credentialSource).toBe("file");
|
||||
expect(resolved.credentialsFile).toBe("/tmp/andy-sa.json");
|
||||
expect(resolved.config.audienceType).toBe("app-url");
|
||||
});
|
||||
|
||||
it("does not inherit dangerous name matching from accounts.default", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
googlechat: {
|
||||
accounts: {
|
||||
default: {
|
||||
dangerouslyAllowNameMatching: true,
|
||||
audienceType: "app-url",
|
||||
audience: "https://example.com/googlechat",
|
||||
},
|
||||
andy: {
|
||||
serviceAccountFile: "/tmp/andy-sa.json",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const resolved = resolveGoogleChatAccount({ cfg, accountId: "andy" });
|
||||
expect(resolved.config.dangerouslyAllowNameMatching).toBeUndefined();
|
||||
expect(resolved.config.audienceType).toBe("app-url");
|
||||
});
|
||||
});
|
||||
@@ -71,8 +71,22 @@ function mergeGoogleChatAccountConfig(
|
||||
): GoogleChatAccountConfig {
|
||||
const raw = cfg.channels?.["googlechat"] ?? {};
|
||||
const { accounts: _ignored, defaultAccount: _ignored2, ...base } = raw;
|
||||
const defaultAccountConfig = resolveAccountConfig(cfg, DEFAULT_ACCOUNT_ID) ?? {};
|
||||
const account = resolveAccountConfig(cfg, accountId) ?? {};
|
||||
return { ...base, ...account } as GoogleChatAccountConfig;
|
||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||
return { ...base, ...defaultAccountConfig } as GoogleChatAccountConfig;
|
||||
}
|
||||
const {
|
||||
enabled: _ignoredEnabled,
|
||||
dangerouslyAllowNameMatching: _ignoredDangerouslyAllowNameMatching,
|
||||
serviceAccount: _ignoredServiceAccount,
|
||||
serviceAccountRef: _ignoredServiceAccountRef,
|
||||
serviceAccountFile: _ignoredServiceAccountFile,
|
||||
...defaultAccountShared
|
||||
} = defaultAccountConfig;
|
||||
// In multi-account setups, allow accounts.default to provide shared defaults
|
||||
// (for example webhook/audience fields) while preserving top-level and account overrides.
|
||||
return { ...defaultAccountShared, ...base, ...account } as GoogleChatAccountConfig;
|
||||
}
|
||||
|
||||
function parseServiceAccount(value: unknown): Record<string, unknown> | null {
|
||||
|
||||
Reference in New Issue
Block a user