mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:50:43 +00:00
fix(channels): keep status accessors config-only
This commit is contained in:
@@ -40,6 +40,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Channels/status: keep Telegram, Slack, and Google Chat read-only allowlist/default-target accessors on config-only paths, so status and channel summaries do not resolve SecretRef-backed runtime credentials. Thanks @eusine.
|
||||
- Channels/Discord: keep read-only allowlist/default-target accessors from resolving SecretRef-backed bot tokens, so status and channel summaries no longer fail when tokens are only available in gateway runtime. (#74737) Thanks @eusine.
|
||||
- Gateway/sessions: align session abort wait semantics across `chat`, `agent`, and `sessions` server methods so abort RPCs return after the targeted sessions actually halt instead of resolving early while runs are still draining. (#74751) Thanks @BunsDev.
|
||||
- Agents/output: drop copied inbound metadata-only assistant replay turns before provider replay instead of synthesizing a placeholder, so Telegram and other channels cannot receive `[assistant copied inbound metadata omitted]` as model output. Fixes #74745. Thanks @adamwdear and @Marvae.
|
||||
|
||||
@@ -24,6 +24,10 @@ export type ResolvedGoogleChatAccount = {
|
||||
credentialsFile?: string;
|
||||
};
|
||||
|
||||
export type GoogleChatConfigAccessorAccount = {
|
||||
config: GoogleChatAccountConfig;
|
||||
};
|
||||
|
||||
const ENV_SERVICE_ACCOUNT = "GOOGLE_CHAT_SERVICE_ACCOUNT";
|
||||
const ENV_SERVICE_ACCOUNT_FILE = "GOOGLE_CHAT_SERVICE_ACCOUNT_FILE";
|
||||
const JsonRecordSchema = z.record(z.string(), z.unknown());
|
||||
@@ -34,7 +38,7 @@ const {
|
||||
} = createAccountListHelpers("googlechat");
|
||||
export { listGoogleChatAccountIds, resolveDefaultGoogleChatAccountId };
|
||||
|
||||
function mergeGoogleChatAccountConfig(
|
||||
export function mergeGoogleChatAccountConfig(
|
||||
cfg: OpenClawConfig,
|
||||
accountId: string,
|
||||
): GoogleChatAccountConfig {
|
||||
@@ -62,6 +66,16 @@ function mergeGoogleChatAccountConfig(
|
||||
return { ...defaultAccountShared, ...base } as GoogleChatAccountConfig;
|
||||
}
|
||||
|
||||
export function resolveGoogleChatConfigAccessorAccount(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}): GoogleChatConfigAccessorAccount {
|
||||
const accountId = normalizeAccountId(
|
||||
params.accountId ?? params.cfg.channels?.googlechat?.defaultAccount,
|
||||
);
|
||||
return { config: mergeGoogleChatAccountConfig(params.cfg, accountId) };
|
||||
}
|
||||
|
||||
function parseServiceAccount(value: unknown): Record<string, unknown> | null {
|
||||
if (isSecretRef(value)) {
|
||||
return null;
|
||||
|
||||
39
extensions/googlechat/src/channel-config.test.ts
Normal file
39
extensions/googlechat/src/channel-config.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { googlechatPlugin } from "./channel.js";
|
||||
|
||||
describe("googlechatPlugin config adapter", () => {
|
||||
it("keeps read-only accessors from resolving service account SecretRefs", () => {
|
||||
const cfg = {
|
||||
secrets: {
|
||||
providers: {
|
||||
google_chat_service_account: {
|
||||
source: "file",
|
||||
path: "/tmp/openclaw-missing-google-chat-service-account",
|
||||
mode: "singleValue",
|
||||
},
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
googlechat: {
|
||||
serviceAccount: {
|
||||
source: "file",
|
||||
provider: "google_chat_service_account",
|
||||
id: "value",
|
||||
},
|
||||
dm: {
|
||||
allowFrom: ["users/123"],
|
||||
},
|
||||
defaultTo: "spaces/AAA",
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(googlechatPlugin.config.resolveAllowFrom?.({ cfg, accountId: "default" })).toEqual([
|
||||
"users/123",
|
||||
]);
|
||||
expect(googlechatPlugin.config.resolveDefaultTo?.({ cfg, accountId: "default" })).toBe(
|
||||
"spaces/AAA",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -15,7 +15,9 @@ export {
|
||||
type OpenClawConfig,
|
||||
} from "../runtime-api.js";
|
||||
export {
|
||||
type GoogleChatConfigAccessorAccount,
|
||||
listGoogleChatAccountIds,
|
||||
resolveGoogleChatConfigAccessorAccount,
|
||||
resolveDefaultGoogleChatAccountId,
|
||||
resolveGoogleChatAccount,
|
||||
type ResolvedGoogleChatAccount,
|
||||
|
||||
@@ -7,7 +7,9 @@ import {
|
||||
import type { ChannelPlugin } from "openclaw/plugin-sdk/channel-core";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
||||
import {
|
||||
type GoogleChatConfigAccessorAccount,
|
||||
listGoogleChatAccountIds,
|
||||
resolveGoogleChatConfigAccessorAccount,
|
||||
resolveDefaultGoogleChatAccountId,
|
||||
resolveGoogleChatAccount,
|
||||
type ResolvedGoogleChatAccount,
|
||||
@@ -24,10 +26,14 @@ const formatGoogleChatAllowFromEntry = (entry: string) =>
|
||||
.replace(/^users\//i, ""),
|
||||
);
|
||||
|
||||
const googleChatConfigAdapter = createScopedChannelConfigAdapter<ResolvedGoogleChatAccount>({
|
||||
const googleChatConfigAdapter = createScopedChannelConfigAdapter<
|
||||
ResolvedGoogleChatAccount,
|
||||
GoogleChatConfigAccessorAccount
|
||||
>({
|
||||
sectionKey: "googlechat",
|
||||
listAccountIds: listGoogleChatAccountIds,
|
||||
resolveAccount: adaptScopedAccountAccessor(resolveGoogleChatAccount),
|
||||
resolveAccessorAccount: resolveGoogleChatConfigAccessorAccount,
|
||||
defaultAccountId: resolveDefaultGoogleChatAccountId,
|
||||
clearBaseFields: [
|
||||
"serviceAccount",
|
||||
|
||||
@@ -30,6 +30,8 @@ import {
|
||||
isGoogleChatUserTarget,
|
||||
listGoogleChatAccountIds,
|
||||
normalizeGoogleChatTarget,
|
||||
type GoogleChatConfigAccessorAccount,
|
||||
resolveGoogleChatConfigAccessorAccount,
|
||||
resolveDefaultGoogleChatAccountId,
|
||||
resolveGoogleChatAccount,
|
||||
type ChannelMessageActionAdapter,
|
||||
@@ -65,10 +67,14 @@ const meta = {
|
||||
markdownCapable: true,
|
||||
};
|
||||
|
||||
const googleChatConfigAdapter = createScopedChannelConfigAdapter<ResolvedGoogleChatAccount>({
|
||||
const googleChatConfigAdapter = createScopedChannelConfigAdapter<
|
||||
ResolvedGoogleChatAccount,
|
||||
GoogleChatConfigAccessorAccount
|
||||
>({
|
||||
sectionKey: "googlechat",
|
||||
listAccountIds: listGoogleChatAccountIds,
|
||||
resolveAccount: adaptScopedAccountAccessor(resolveGoogleChatAccount),
|
||||
resolveAccessorAccount: resolveGoogleChatConfigAccessorAccount,
|
||||
defaultAccountId: resolveDefaultGoogleChatAccountId,
|
||||
clearBaseFields: [
|
||||
"serviceAccount",
|
||||
@@ -80,13 +86,13 @@ const googleChatConfigAdapter = createScopedChannelConfigAdapter<ResolvedGoogleC
|
||||
"botUser",
|
||||
"name",
|
||||
],
|
||||
resolveAllowFrom: (account: ResolvedGoogleChatAccount) => account.config.dm?.allowFrom,
|
||||
resolveAllowFrom: (account) => account.config.dm?.allowFrom,
|
||||
formatAllowFrom: (allowFrom) =>
|
||||
formatNormalizedAllowFromEntries({
|
||||
allowFrom,
|
||||
normalizeEntry: formatAllowFromEntry,
|
||||
}),
|
||||
resolveDefaultTo: (account: ResolvedGoogleChatAccount) => account.config.defaultTo,
|
||||
resolveDefaultTo: (account) => account.config.defaultTo,
|
||||
});
|
||||
|
||||
const googlechatActions: ChannelMessageActionAdapter = {
|
||||
|
||||
@@ -35,6 +35,11 @@ export type ResolvedSlackAccount = {
|
||||
config: SlackAccountConfig;
|
||||
} & SlackAccountSurfaceFields;
|
||||
|
||||
export type SlackConfigAccessorAccount = {
|
||||
allowFrom: string[] | undefined;
|
||||
defaultTo: string | undefined;
|
||||
};
|
||||
|
||||
const { listAccountIds, resolveDefaultAccountId } = createAccountListHelpers("slack");
|
||||
export const listSlackAccountIds = listAccountIds;
|
||||
export const resolveDefaultSlackAccountId = resolveDefaultAccountId;
|
||||
@@ -73,6 +78,20 @@ export function resolveSlackAccountAllowFrom(params: {
|
||||
return allowFrom ? mapAllowFromEntries(allowFrom) : undefined;
|
||||
}
|
||||
|
||||
export function resolveSlackConfigAccessorAccount(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}): SlackConfigAccessorAccount {
|
||||
const accountId = normalizeAccountId(
|
||||
params.accountId ?? resolveDefaultSlackAccountId(params.cfg),
|
||||
);
|
||||
const config = mergeSlackAccountConfig(params.cfg, accountId);
|
||||
return {
|
||||
allowFrom: resolveSlackAccountAllowFrom({ cfg: params.cfg, accountId }),
|
||||
defaultTo: config.defaultTo,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveSlackAccountDmPolicy(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
|
||||
@@ -6,9 +6,10 @@ import {
|
||||
import { type ResolvedSlackAccount } from "./accounts.js";
|
||||
import {
|
||||
listSlackAccountIds,
|
||||
resolveSlackConfigAccessorAccount,
|
||||
resolveDefaultSlackAccountId,
|
||||
resolveSlackAccount,
|
||||
resolveSlackAccountAllowFrom,
|
||||
type SlackConfigAccessorAccount,
|
||||
} from "./accounts.js";
|
||||
import { type ChannelPlugin } from "./channel-api.js";
|
||||
import { SlackChannelConfigSchema } from "./config-schema.js";
|
||||
@@ -23,25 +24,14 @@ const slackSetupWizard = createSlackSetupWizardProxy(async () => ({
|
||||
slackSetupWizard: (await import("./setup-surface.js")).slackSetupWizard,
|
||||
}));
|
||||
|
||||
type SlackSetupConfigAccessorAccount = {
|
||||
allowFrom: string[] | undefined;
|
||||
defaultTo: string | undefined;
|
||||
};
|
||||
|
||||
const slackSetupConfigAdapter = createScopedChannelConfigAdapter<
|
||||
ResolvedSlackAccount,
|
||||
SlackSetupConfigAccessorAccount
|
||||
SlackConfigAccessorAccount
|
||||
>({
|
||||
sectionKey: SLACK_CHANNEL,
|
||||
listAccountIds: listSlackAccountIds,
|
||||
resolveAccount: adaptScopedAccountAccessor(resolveSlackAccount),
|
||||
resolveAccessorAccount: (params) => {
|
||||
const account = resolveSlackAccount(params);
|
||||
return {
|
||||
allowFrom: resolveSlackAccountAllowFrom({ cfg: params.cfg, accountId: account.accountId }),
|
||||
defaultTo: account.config.defaultTo,
|
||||
};
|
||||
},
|
||||
resolveAccessorAccount: resolveSlackConfigAccessorAccount,
|
||||
defaultAccountId: resolveDefaultSlackAccountId,
|
||||
clearBaseFields: ["botToken", "appToken", "name"],
|
||||
resolveAllowFrom: (account) => account.allowFrom,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createSlackPluginBase, setSlackChannelAllowlist } from "./shared.js";
|
||||
import { createSlackPluginBase, setSlackChannelAllowlist, slackConfigAdapter } from "./shared.js";
|
||||
|
||||
describe("createSlackPluginBase", () => {
|
||||
it("owns Slack native command name overrides", () => {
|
||||
@@ -56,3 +57,35 @@ describe("setSlackChannelAllowlist", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("slackConfigAdapter", () => {
|
||||
it("keeps read-only accessors from resolving token SecretRefs", () => {
|
||||
const cfg = {
|
||||
secrets: {
|
||||
providers: {
|
||||
slack_bot: {
|
||||
source: "file",
|
||||
path: "/tmp/openclaw-missing-slack-bot-token",
|
||||
mode: "singleValue",
|
||||
},
|
||||
slack_app: {
|
||||
source: "file",
|
||||
path: "/tmp/openclaw-missing-slack-app-token",
|
||||
mode: "singleValue",
|
||||
},
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
slack: {
|
||||
botToken: { source: "file", provider: "slack_bot", id: "value" },
|
||||
appToken: { source: "file", provider: "slack_app", id: "value" },
|
||||
allowFrom: ["U123"],
|
||||
defaultTo: "C123",
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
expect(slackConfigAdapter.resolveAllowFrom?.({ cfg, accountId: "default" })).toEqual(["U123"]);
|
||||
expect(slackConfigAdapter.resolveDefaultTo?.({ cfg, accountId: "default" })).toBe("C123");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,16 +7,16 @@ import {
|
||||
import { inspectSlackAccount } from "./account-inspect.js";
|
||||
import {
|
||||
listSlackAccountIds,
|
||||
resolveSlackConfigAccessorAccount,
|
||||
resolveDefaultSlackAccountId,
|
||||
resolveSlackAccount,
|
||||
resolveSlackAccountAllowFrom,
|
||||
type SlackConfigAccessorAccount,
|
||||
type ResolvedSlackAccount,
|
||||
} from "./accounts.js";
|
||||
import { getChatChannelMeta, type ChannelPlugin } from "./channel-api.js";
|
||||
import { SlackChannelConfigSchema } from "./config-schema.js";
|
||||
import { slackDoctor } from "./doctor.js";
|
||||
import { isSlackInteractiveRepliesEnabled } from "./interactive-replies.js";
|
||||
import type { OpenClawConfig } from "./runtime-api.js";
|
||||
import { collectRuntimeConfigAssignments, secretTargetRegistryEntries } from "./secret-contract.js";
|
||||
import { slackSecurityAdapter } from "./security.js";
|
||||
import { SLACK_CHANNEL } from "./setup-shared.js";
|
||||
@@ -40,22 +40,6 @@ export function isSlackPluginAccountConfigured(account: ResolvedSlackAccount): b
|
||||
return Boolean(account.appToken?.trim());
|
||||
}
|
||||
|
||||
type SlackConfigAccessorAccount = {
|
||||
allowFrom: string[] | undefined;
|
||||
defaultTo: string | undefined;
|
||||
};
|
||||
|
||||
function resolveSlackConfigAccessorAccount(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}): SlackConfigAccessorAccount {
|
||||
const account = resolveSlackAccount(params);
|
||||
return {
|
||||
allowFrom: resolveSlackAccountAllowFrom({ cfg: params.cfg, accountId: account.accountId }),
|
||||
defaultTo: account.config.defaultTo,
|
||||
};
|
||||
}
|
||||
|
||||
export const slackConfigAdapter = createScopedChannelConfigAdapter<
|
||||
ResolvedSlackAccount,
|
||||
SlackConfigAccessorAccount
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { ResolvedTelegramAccount } from "./accounts.js";
|
||||
import { createTelegramPluginBase } from "./shared.js";
|
||||
import { createTelegramPluginBase, telegramConfigAdapter } from "./shared.js";
|
||||
|
||||
const telegramPluginBase = createTelegramPluginBase({
|
||||
setupWizard: {} as never,
|
||||
@@ -166,4 +166,32 @@ describe("createTelegramPluginBase config duplicate token guard", () => {
|
||||
expect(await telegramPluginBase.config.isConfigured!(account, cfg)).toBe(false);
|
||||
expect(telegramPluginBase.config.unconfiguredReason?.(account, cfg)).toContain("unavailable");
|
||||
});
|
||||
|
||||
it("keeps read-only accessors from resolving bot token SecretRefs", () => {
|
||||
const cfg = {
|
||||
secrets: {
|
||||
providers: {
|
||||
telegram_token: {
|
||||
source: "file",
|
||||
path: "/tmp/openclaw-missing-telegram-token",
|
||||
mode: "singleValue",
|
||||
},
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: { source: "file", provider: "telegram_token", id: "value" },
|
||||
allowFrom: ["1128540374256849009"],
|
||||
defaultTo: "1498959610751750304",
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
expect(telegramConfigAdapter.resolveAllowFrom?.({ cfg, accountId: "default" })).toEqual([
|
||||
"1128540374256849009",
|
||||
]);
|
||||
expect(telegramConfigAdapter.resolveDefaultTo?.({ cfg, accountId: "default" })).toBe(
|
||||
"1498959610751750304",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,11 +7,12 @@ import {
|
||||
} from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { createChannelPluginBase, type ChannelPlugin } from "openclaw/plugin-sdk/channel-core";
|
||||
import { getChatChannelMeta } from "openclaw/plugin-sdk/channel-plugin-common";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||
import type { OpenClawConfig, TelegramAccountConfig } from "openclaw/plugin-sdk/config-types";
|
||||
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/routing";
|
||||
import { inspectTelegramAccount } from "./account-inspect.js";
|
||||
import {
|
||||
listTelegramAccountIds,
|
||||
mergeTelegramAccountConfig,
|
||||
resolveDefaultTelegramAccountId,
|
||||
resolveTelegramAccount,
|
||||
type ResolvedTelegramAccount,
|
||||
@@ -32,6 +33,10 @@ import { namedAccountPromotionKeys, singleAccountKeysToMove } from "./setup-cont
|
||||
|
||||
export const TELEGRAM_CHANNEL = "telegram" as const;
|
||||
|
||||
type TelegramConfigAccessorAccount = {
|
||||
config: TelegramAccountConfig;
|
||||
};
|
||||
|
||||
export function findTelegramTokenOwnerAccountId(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId: string;
|
||||
@@ -99,17 +104,31 @@ function isBlockedByMultiBotGuard(cfg: OpenClawConfig, accountId: string): boole
|
||||
return !resolveNormalizedAccountEntry(accounts, accountId, normalizeAccountId);
|
||||
}
|
||||
|
||||
export const telegramConfigAdapter = createScopedChannelConfigAdapter<ResolvedTelegramAccount>({
|
||||
function resolveTelegramConfigAccessorAccount(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}): TelegramConfigAccessorAccount {
|
||||
const accountId = normalizeAccountId(
|
||||
params.accountId ?? resolveDefaultTelegramAccountId(params.cfg),
|
||||
);
|
||||
return { config: mergeTelegramAccountConfig(params.cfg, accountId) };
|
||||
}
|
||||
|
||||
export const telegramConfigAdapter = createScopedChannelConfigAdapter<
|
||||
ResolvedTelegramAccount,
|
||||
TelegramConfigAccessorAccount
|
||||
>({
|
||||
sectionKey: TELEGRAM_CHANNEL,
|
||||
listAccountIds: listTelegramAccountIds,
|
||||
resolveAccount: adaptScopedAccountAccessor(resolveTelegramAccount),
|
||||
resolveAccessorAccount: resolveTelegramConfigAccessorAccount,
|
||||
inspectAccount: adaptScopedAccountAccessor(inspectTelegramAccount),
|
||||
defaultAccountId: resolveDefaultTelegramAccountId,
|
||||
clearBaseFields: ["botToken", "tokenFile", "name"],
|
||||
resolveAllowFrom: (account: ResolvedTelegramAccount) => account.config.allowFrom,
|
||||
resolveAllowFrom: (account) => account.config.allowFrom,
|
||||
formatAllowFrom: (allowFrom) =>
|
||||
formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(telegram|tg):/i }),
|
||||
resolveDefaultTo: (account: ResolvedTelegramAccount) => account.config.defaultTo,
|
||||
resolveDefaultTo: (account) => account.config.defaultTo,
|
||||
});
|
||||
|
||||
export function createTelegramPluginBase(params: {
|
||||
|
||||
@@ -413,6 +413,31 @@ describe("createScopedChannelConfigAdapter", () => {
|
||||
});
|
||||
expectAdapterAllowFromAndDefaultTo(adapter);
|
||||
});
|
||||
|
||||
it("keeps read-only accessors on the accessor resolver", () => {
|
||||
const adapter = createScopedChannelConfigAdapter<
|
||||
{ accountId: string; token: string },
|
||||
{ allowFrom: string[]; defaultTo: string }
|
||||
>({
|
||||
sectionKey: "demo",
|
||||
listAccountIds: () => ["default"],
|
||||
resolveAccount: () => {
|
||||
throw new Error("runtime account resolver should not run for read-only accessors");
|
||||
},
|
||||
resolveAccessorAccount: ({ accountId }) => ({
|
||||
allowFrom: [accountId ?? "default"],
|
||||
defaultTo: " room:123 ",
|
||||
}),
|
||||
defaultAccountId: resolveDefaultAccountId,
|
||||
clearBaseFields: ["token"],
|
||||
resolveAllowFrom: (account) => account.allowFrom,
|
||||
formatAllowFrom: (allowFrom) => allowFrom.map((entry) => String(entry)),
|
||||
resolveDefaultTo: (account) => account.defaultTo,
|
||||
});
|
||||
|
||||
expect(adapter.resolveAllowFrom?.({ cfg: {}, accountId: "default" })).toEqual(["default"]);
|
||||
expect(adapter.resolveDefaultTo?.({ cfg: {}, accountId: "default" })).toBe("room:123");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createScopedDmSecurityResolver", () => {
|
||||
|
||||
Reference in New Issue
Block a user