fix(secrets): scope message SecretRef resolution and harden doctor/status paths (#48728)

* fix(secrets): scope message runtime resolution and harden doctor/status

* docs: align message/doctor/status SecretRef behavior notes

* test(cli): accept scoped targetIds wiring in secret-resolution coverage

* fix(secrets): keep scoped allowedPaths isolation and tighten coverage gate

* fix(secrets): avoid default-account coercion in scoped target selection

* test(doctor): cover inactive telegram secretref inspect path

* docs

Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com>

* changelog

Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com>

---------

Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com>
This commit is contained in:
Josh Avant
2026-03-17 00:01:34 -05:00
committed by GitHub
parent 50c3321d2e
commit da34f81ce2
27 changed files with 854 additions and 76 deletions

View File

@@ -1,13 +1,16 @@
import { afterEach, describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import { setActivePluginRegistry } from "../../plugins/runtime.js";
import { defaultRuntime } from "../../runtime.js";
import {
createChannelTestPluginBase,
createTestRegistry,
} from "../../test-utils/channel-plugins.js";
import {
__testing,
channelSupportsMessageCapability,
channelSupportsMessageCapabilityForChannel,
listChannelMessageActions,
listChannelMessageCapabilities,
listChannelMessageCapabilitiesForChannel,
} from "./message-actions.js";
@@ -56,8 +59,12 @@ function activateMessageActionTestRegistry() {
}
describe("message action capability checks", () => {
const errorSpy = vi.spyOn(defaultRuntime, "error").mockImplementation(() => undefined);
afterEach(() => {
setActivePluginRegistry(emptyRegistry);
__testing.resetLoggedMessageActionErrors();
errorSpy.mockClear();
});
it("aggregates capabilities across plugins", () => {
@@ -122,4 +129,36 @@ describe("message action capability checks", () => {
false,
);
});
it("skips crashing action/capability discovery paths and logs once", () => {
const crashingPlugin: ChannelPlugin = {
...createChannelTestPluginBase({
id: "discord",
label: "Discord",
capabilities: { chatTypes: ["direct", "group"] },
config: {
listAccountIds: () => ["default"],
},
}),
actions: {
listActions: () => {
throw new Error("boom");
},
getCapabilities: () => {
throw new Error("boom");
},
},
};
setActivePluginRegistry(
createTestRegistry([{ pluginId: "discord", source: "test", plugin: crashingPlugin }]),
);
expect(listChannelMessageActions({} as OpenClawConfig)).toEqual(["send", "broadcast"]);
expect(listChannelMessageCapabilities({} as OpenClawConfig)).toEqual([]);
expect(errorSpy).toHaveBeenCalledTimes(2);
expect(listChannelMessageActions({} as OpenClawConfig)).toEqual(["send", "broadcast"]);
expect(listChannelMessageCapabilities({} as OpenClawConfig)).toEqual([]);
expect(errorSpy).toHaveBeenCalledTimes(2);
});
});

View File

@@ -1,5 +1,6 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import type { OpenClawConfig } from "../../config/config.js";
import { defaultRuntime } from "../../runtime.js";
import { getChannelPlugin, listChannelPlugins } from "./index.js";
import type { ChannelMessageCapability } from "./message-capabilities.js";
import type { ChannelMessageActionContext, ChannelMessageActionName } from "./types.js";
@@ -16,13 +17,54 @@ function requiresTrustedRequesterSender(ctx: ChannelMessageActionContext): boole
);
}
const loggedMessageActionErrors = new Set<string>();
function logMessageActionError(params: {
pluginId: string;
operation: "listActions" | "getCapabilities";
error: unknown;
}) {
const message = params.error instanceof Error ? params.error.message : String(params.error);
const key = `${params.pluginId}:${params.operation}:${message}`;
if (loggedMessageActionErrors.has(key)) {
return;
}
loggedMessageActionErrors.add(key);
const stack = params.error instanceof Error && params.error.stack ? params.error.stack : null;
defaultRuntime.error?.(
`[message-actions] ${params.pluginId}.actions.${params.operation} failed: ${stack ?? message}`,
);
}
function runListActionsSafely(params: {
pluginId: string;
cfg: OpenClawConfig;
listActions: NonNullable<ChannelActions["listActions"]>;
}): ChannelMessageActionName[] {
try {
const listed = params.listActions({ cfg: params.cfg });
return Array.isArray(listed) ? listed : [];
} catch (error) {
logMessageActionError({
pluginId: params.pluginId,
operation: "listActions",
error,
});
return [];
}
}
export function listChannelMessageActions(cfg: OpenClawConfig): ChannelMessageActionName[] {
const actions = new Set<ChannelMessageActionName>(["send", "broadcast"]);
for (const plugin of listChannelPlugins()) {
const list = plugin.actions?.listActions?.({ cfg });
if (!list) {
if (!plugin.actions?.listActions) {
continue;
}
const list = runListActionsSafely({
pluginId: plugin.id,
cfg,
listActions: plugin.actions.listActions,
});
for (const action of list) {
actions.add(action);
}
@@ -30,11 +72,21 @@ export function listChannelMessageActions(cfg: OpenClawConfig): ChannelMessageAc
return Array.from(actions);
}
function listCapabilities(
actions: ChannelActions,
cfg: OpenClawConfig,
): readonly ChannelMessageCapability[] {
return actions.getCapabilities?.({ cfg }) ?? [];
function listCapabilities(params: {
pluginId: string;
actions: ChannelActions;
cfg: OpenClawConfig;
}): readonly ChannelMessageCapability[] {
try {
return params.actions.getCapabilities?.({ cfg: params.cfg }) ?? [];
} catch (error) {
logMessageActionError({
pluginId: params.pluginId,
operation: "getCapabilities",
error,
});
return [];
}
}
export function listChannelMessageCapabilities(cfg: OpenClawConfig): ChannelMessageCapability[] {
@@ -43,7 +95,11 @@ export function listChannelMessageCapabilities(cfg: OpenClawConfig): ChannelMess
if (!plugin.actions) {
continue;
}
for (const capability of listCapabilities(plugin.actions, cfg)) {
for (const capability of listCapabilities({
pluginId: plugin.id,
actions: plugin.actions,
cfg,
})) {
capabilities.add(capability);
}
}
@@ -58,7 +114,15 @@ export function listChannelMessageCapabilitiesForChannel(params: {
return [];
}
const plugin = getChannelPlugin(params.channel as Parameters<typeof getChannelPlugin>[0]);
return plugin?.actions ? Array.from(listCapabilities(plugin.actions, params.cfg)) : [];
return plugin?.actions
? Array.from(
listCapabilities({
pluginId: plugin.id,
actions: plugin.actions,
cfg: params.cfg,
}),
)
: [];
}
export function channelSupportsMessageCapability(
@@ -95,3 +159,9 @@ export async function dispatchChannelMessageAction(
}
return await plugin.actions.handleAction(ctx);
}
export const __testing = {
resetLoggedMessageActionErrors() {
loggedMessageActionErrors.clear();
},
};