mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:40:44 +00:00
fix(discord): handle SecretRefs in message actions
This commit is contained in:
@@ -52,6 +52,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Auto-reply/docking: require `/dock-*` route switches to start from direct chats, so group or channel participants cannot reroute a shared session's future replies into a linked DM. Thanks @vincentkoc.
|
||||
- Discord: keep text-DM main-session route updates pinned to the configured DM owner, matching component interactions so another direct-message sender cannot redirect future main-session replies. Thanks @vincentkoc.
|
||||
- Mattermost/Matrix: keep direct-message main-session route updates pinned to the configured DM owner so paired or temporarily allowed senders cannot redirect future shared-session replies. Thanks @vincentkoc.
|
||||
- Discord: keep SecretRef-backed bot tokens discoverable for message actions without resolving the token during schema generation, and resolve scoped channel SecretRefs before outbound agent message sends even when the tool is built from a config snapshot. Fixes #75324. Thanks @slideshow-dingo and @Conan-Scott.
|
||||
- Updates: run package post-install doctor repair with the managed Gateway service profile and state paths when a daemon is installed, so shell/profile mismatches no longer repair the caller state while the restarted Gateway keeps stale config. Thanks @vincentkoc.
|
||||
- Models/DeepInfra: declare DeepInfra manifest catalog discovery and derive its runtime fallback catalog from the manifest, restoring provider-filtered `models list --all --provider deepinfra` rows without duplicated static model data. Thanks @shakkernerd.
|
||||
- CLI/update: verify managed gateway restarts against the installed service port instead of the caller shell port, so package updates do not report a healthy daemon as failed when profiles use different gateway ports. Thanks @vincentkoc.
|
||||
|
||||
@@ -61,6 +61,58 @@ describe("discordMessageActions", () => {
|
||||
expect(discovery?.actions).not.toContain("role-add");
|
||||
});
|
||||
|
||||
it("describes actions when the Discord token is an unresolved SecretRef", () => {
|
||||
const discovery = discordMessageActions.describeMessageTool?.({
|
||||
cfg: {
|
||||
channels: {
|
||||
discord: {
|
||||
token: { source: "env", provider: "default", id: "DISCORD_BOT_TOKEN" },
|
||||
actions: {
|
||||
polls: true,
|
||||
reactions: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig,
|
||||
});
|
||||
|
||||
expect(discovery?.capabilities).toEqual(["presentation"]);
|
||||
expect(discovery?.actions).toEqual(
|
||||
expect.arrayContaining(["send", "poll", "react", "reactions", "emoji-list"]),
|
||||
);
|
||||
});
|
||||
|
||||
it("describes scoped account actions when the account token is an unresolved SecretRef", () => {
|
||||
const discovery = discordMessageActions.describeMessageTool?.({
|
||||
cfg: {
|
||||
channels: {
|
||||
discord: {
|
||||
token: "Bot token-main",
|
||||
actions: {
|
||||
polls: true,
|
||||
reactions: false,
|
||||
},
|
||||
accounts: {
|
||||
ops: {
|
||||
token: { source: "file", provider: "filemain", id: "/DISCORD_BOT_TOKEN" },
|
||||
actions: {
|
||||
polls: false,
|
||||
reactions: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig,
|
||||
accountId: "ops",
|
||||
});
|
||||
|
||||
expect(discovery?.actions).toEqual(
|
||||
expect.arrayContaining(["send", "react", "reactions", "emoji-list"]),
|
||||
);
|
||||
expect(discovery?.actions).not.toContain("poll");
|
||||
});
|
||||
|
||||
it("honors account-scoped action gates during discovery", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
import {
|
||||
createUnionActionGate,
|
||||
listTokenSourcedAccounts,
|
||||
} from "openclaw/plugin-sdk/channel-actions";
|
||||
import { createUnionActionGate } from "openclaw/plugin-sdk/channel-actions";
|
||||
import type {
|
||||
ChannelMessageActionAdapter,
|
||||
ChannelMessageActionName,
|
||||
ChannelMessageToolDiscovery,
|
||||
} from "openclaw/plugin-sdk/channel-contract";
|
||||
import type { DiscordActionConfig } from "openclaw/plugin-sdk/config-types";
|
||||
import type { DiscordActionConfig, OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { extractToolSend } from "openclaw/plugin-sdk/tool-send";
|
||||
import {
|
||||
createDiscordActionGate,
|
||||
listEnabledDiscordAccounts,
|
||||
resolveDiscordAccount,
|
||||
} from "./accounts.js";
|
||||
import { inspectDiscordAccount } from "./account-inspect.js";
|
||||
import { createDiscordActionGate, listDiscordAccountIds } from "./accounts.js";
|
||||
|
||||
let discordChannelActionsRuntimePromise:
|
||||
| Promise<typeof import("./channel-actions.runtime.js")>
|
||||
@@ -25,8 +19,14 @@ async function loadDiscordChannelActionsRuntime() {
|
||||
return await discordChannelActionsRuntimePromise;
|
||||
}
|
||||
|
||||
function resolveDiscordActionDiscovery(cfg: Parameters<typeof listEnabledDiscordAccounts>[0]) {
|
||||
const accounts = listTokenSourcedAccounts(listEnabledDiscordAccounts(cfg));
|
||||
function listDiscoverableDiscordAccounts(cfg: OpenClawConfig) {
|
||||
return listDiscordAccountIds(cfg)
|
||||
.map((accountId) => inspectDiscordAccount({ cfg, accountId }))
|
||||
.filter((account) => account.enabled && account.configured);
|
||||
}
|
||||
|
||||
function resolveDiscordActionDiscovery(cfg: OpenClawConfig) {
|
||||
const accounts = listDiscoverableDiscordAccounts(cfg);
|
||||
if (accounts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -43,14 +43,14 @@ function resolveDiscordActionDiscovery(cfg: Parameters<typeof listEnabledDiscord
|
||||
}
|
||||
|
||||
function resolveScopedDiscordActionDiscovery(params: {
|
||||
cfg: Parameters<typeof listEnabledDiscordAccounts>[0];
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}) {
|
||||
if (!params.accountId) {
|
||||
return resolveDiscordActionDiscovery(params.cfg);
|
||||
}
|
||||
const account = resolveDiscordAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
if (!account.enabled || !account.token.trim()) {
|
||||
const account = inspectDiscordAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
if (!account.enabled || !account.configured) {
|
||||
return null;
|
||||
}
|
||||
const gate = createDiscordActionGate({
|
||||
|
||||
@@ -280,6 +280,51 @@ describe("message tool secret scoping", () => {
|
||||
new Set(["channels.discord.token", "channels.discord.accounts.ops.token"]),
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves scoped channel SecretRefs even when constructed with a config snapshot", async () => {
|
||||
mockSendResult({ channel: "discord", to: "channel:123" });
|
||||
const rawConfig = {
|
||||
channels: {
|
||||
discord: {
|
||||
token: { source: "env", provider: "default", id: "DISCORD_BOT_TOKEN" },
|
||||
},
|
||||
},
|
||||
};
|
||||
const resolvedConfig = {
|
||||
channels: {
|
||||
discord: {
|
||||
token: "resolved-discord-token",
|
||||
},
|
||||
},
|
||||
};
|
||||
mocks.resolveCommandSecretRefsViaGateway.mockResolvedValueOnce({
|
||||
resolvedConfig,
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
const tool = createMessageTool({
|
||||
config: rawConfig as never,
|
||||
currentChannelProvider: "discord",
|
||||
currentChannelId: "channel:123",
|
||||
resolveCommandSecretRefsViaGateway: mocks.resolveCommandSecretRefsViaGateway as never,
|
||||
runMessageAction: mocks.runMessageAction as never,
|
||||
});
|
||||
|
||||
await tool.execute("1", {
|
||||
action: "send",
|
||||
message: "hi",
|
||||
});
|
||||
|
||||
const secretResolveCall = mocks.resolveCommandSecretRefsViaGateway.mock.calls.at(-1)?.[0] as {
|
||||
config?: unknown;
|
||||
targetIds?: Set<string>;
|
||||
allowedPaths?: Set<string>;
|
||||
};
|
||||
expect(secretResolveCall.config).toBe(rawConfig);
|
||||
expect(secretResolveCall.targetIds).toEqual(new Set(["channels.discord.token"]));
|
||||
expect(secretResolveCall.allowedPaths).toEqual(new Set(["channels.discord.token"]));
|
||||
expect(mocks.runMessageAction.mock.calls[0]?.[0]?.cfg).toBe(resolvedConfig);
|
||||
});
|
||||
});
|
||||
|
||||
describe("message tool agent routing", () => {
|
||||
|
||||
@@ -703,32 +703,29 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
|
||||
}
|
||||
}
|
||||
|
||||
let cfg = options?.config;
|
||||
if (!cfg) {
|
||||
const loadedRaw = loadConfigForTool();
|
||||
const scope = resolveMessageSecretScope({
|
||||
channel: params.channel,
|
||||
target: params.target,
|
||||
targets: params.targets,
|
||||
fallbackChannel: options?.currentChannelProvider,
|
||||
accountId: params.accountId,
|
||||
fallbackAccountId: agentAccountId,
|
||||
});
|
||||
const scopedTargets = getScopedSecretTargetsForTool({
|
||||
config: loadedRaw,
|
||||
channel: scope.channel,
|
||||
accountId: scope.accountId,
|
||||
});
|
||||
cfg = (
|
||||
await resolveSecretRefsForTool({
|
||||
config: loadedRaw,
|
||||
commandName: "tools.message",
|
||||
targetIds: scopedTargets.targetIds,
|
||||
...(scopedTargets.allowedPaths ? { allowedPaths: scopedTargets.allowedPaths } : {}),
|
||||
mode: "enforce_resolved",
|
||||
})
|
||||
).resolvedConfig;
|
||||
}
|
||||
const rawConfig = options?.config ?? loadConfigForTool();
|
||||
const scope = resolveMessageSecretScope({
|
||||
channel: params.channel,
|
||||
target: params.target,
|
||||
targets: params.targets,
|
||||
fallbackChannel: options?.currentChannelProvider,
|
||||
accountId: params.accountId,
|
||||
fallbackAccountId: agentAccountId,
|
||||
});
|
||||
const scopedTargets = getScopedSecretTargetsForTool({
|
||||
config: rawConfig,
|
||||
channel: scope.channel,
|
||||
accountId: scope.accountId,
|
||||
});
|
||||
const cfg = (
|
||||
await resolveSecretRefsForTool({
|
||||
config: rawConfig,
|
||||
commandName: "tools.message",
|
||||
targetIds: scopedTargets.targetIds,
|
||||
...(scopedTargets.allowedPaths ? { allowedPaths: scopedTargets.allowedPaths } : {}),
|
||||
mode: "enforce_resolved",
|
||||
})
|
||||
).resolvedConfig;
|
||||
|
||||
const accountId = readStringParam(params, "accountId") ?? agentAccountId;
|
||||
if (accountId) {
|
||||
|
||||
Reference in New Issue
Block a user