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:
Tak Hoffman
2026-03-06 21:11:55 -06:00
committed by GitHub
parent ba9eaf2ee2
commit a01978ba96
3 changed files with 147 additions and 1 deletions

View File

@@ -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

View 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");
});
});

View File

@@ -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 {