mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 12:00:44 +00:00
fix: keep read-only channel scope complete
This commit is contained in:
@@ -21,6 +21,12 @@ type ReadOnlyChannelPluginOptions = {
|
||||
cache?: boolean;
|
||||
};
|
||||
|
||||
export type ReadOnlyChannelPluginResolution = {
|
||||
plugins: ChannelPlugin[];
|
||||
configuredChannelIds: string[];
|
||||
missingConfiguredChannelIds: string[];
|
||||
};
|
||||
|
||||
function resolveReadOnlyChannelPluginOptions(
|
||||
envOrOptions?: NodeJS.ProcessEnv | ReadOnlyChannelPluginOptions,
|
||||
): ReadOnlyChannelPluginOptions {
|
||||
@@ -130,6 +136,13 @@ export function listReadOnlyChannelPluginsForConfig(
|
||||
cfg: OpenClawConfig,
|
||||
envOrOptions?: NodeJS.ProcessEnv | ReadOnlyChannelPluginOptions,
|
||||
): ChannelPlugin[] {
|
||||
return resolveReadOnlyChannelPluginsForConfig(cfg, envOrOptions).plugins;
|
||||
}
|
||||
|
||||
export function resolveReadOnlyChannelPluginsForConfig(
|
||||
cfg: OpenClawConfig,
|
||||
envOrOptions?: NodeJS.ProcessEnv | ReadOnlyChannelPluginOptions,
|
||||
): ReadOnlyChannelPluginResolution {
|
||||
const options = resolveReadOnlyChannelPluginOptions(envOrOptions);
|
||||
const env = options.env ?? process.env;
|
||||
const workspaceDir = resolveReadOnlyWorkspaceDir(cfg, options);
|
||||
@@ -143,6 +156,7 @@ export function listReadOnlyChannelPluginsForConfig(
|
||||
...new Set(
|
||||
listConfiguredChannelIdsForReadOnlyScope({
|
||||
config: cfg,
|
||||
activationSourceConfig: options.activationSourceConfig ?? cfg,
|
||||
workspaceDir,
|
||||
env,
|
||||
cache: options.cache,
|
||||
@@ -197,5 +211,10 @@ export function listReadOnlyChannelPluginsForConfig(
|
||||
);
|
||||
}
|
||||
|
||||
return [...byId.values()];
|
||||
const plugins = [...byId.values()];
|
||||
return {
|
||||
plugins,
|
||||
configuredChannelIds,
|
||||
missingConfiguredChannelIds: configuredChannelIds.filter((channelId) => !byId.has(channelId)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,6 +16,11 @@ const mocks = vi.hoisted(() => ({
|
||||
callGateway: vi.fn(),
|
||||
getDaemonStatusSummary: vi.fn(),
|
||||
getNodeDaemonStatusSummary: vi.fn(),
|
||||
resolveReadOnlyChannelPluginsForConfig: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../channels/plugins/read-only.js", () => ({
|
||||
resolveReadOnlyChannelPluginsForConfig: mocks.resolveReadOnlyChannelPluginsForConfig,
|
||||
}));
|
||||
|
||||
vi.mock("../infra/provider-usage.js", () => ({
|
||||
@@ -43,6 +48,11 @@ describe("status-runtime-shared", () => {
|
||||
mocks.callGateway.mockResolvedValue({ ok: true });
|
||||
mocks.getDaemonStatusSummary.mockResolvedValue({ label: "LaunchAgent" });
|
||||
mocks.getNodeDaemonStatusSummary.mockResolvedValue({ label: "node" });
|
||||
mocks.resolveReadOnlyChannelPluginsForConfig.mockReturnValue({
|
||||
plugins: [{ id: "telegram" }],
|
||||
configuredChannelIds: ["telegram"],
|
||||
missingConfiguredChannelIds: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves the shared security audit payload", async () => {
|
||||
@@ -59,6 +69,31 @@ describe("status-runtime-shared", () => {
|
||||
includeChannelSecurity: true,
|
||||
plugins: expect.any(Array),
|
||||
});
|
||||
expect(mocks.resolveReadOnlyChannelPluginsForConfig).toHaveBeenCalledWith(
|
||||
{ gateway: {} },
|
||||
{ activationSourceConfig: { gateway: {} } },
|
||||
);
|
||||
});
|
||||
|
||||
it("lets the security audit load configured channel plugins when read-only discovery is incomplete", async () => {
|
||||
mocks.resolveReadOnlyChannelPluginsForConfig.mockReturnValue({
|
||||
plugins: [],
|
||||
configuredChannelIds: ["external"],
|
||||
missingConfiguredChannelIds: ["external"],
|
||||
});
|
||||
|
||||
await resolveStatusSecurityAudit({
|
||||
config: { gateway: {} },
|
||||
sourceConfig: { gateway: {} },
|
||||
});
|
||||
|
||||
expect(mocks.runSecurityAudit).toHaveBeenCalledWith({
|
||||
config: { gateway: {} },
|
||||
sourceConfig: { gateway: {} },
|
||||
deep: false,
|
||||
includeFilesystem: true,
|
||||
includeChannelSecurity: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves usage summaries with the provided timeout", async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { listReadOnlyChannelPluginsForConfig } from "../channels/plugins/read-only.js";
|
||||
import { resolveReadOnlyChannelPluginsForConfig } from "../channels/plugins/read-only.js";
|
||||
import type { OpenClawConfig } from "../config/types.js";
|
||||
import type { HeartbeatEventPayload } from "../infra/heartbeat-events.js";
|
||||
import type { HealthSummary } from "./health.js";
|
||||
@@ -28,13 +28,18 @@ export async function resolveStatusSecurityAudit(params: {
|
||||
sourceConfig: OpenClawConfig;
|
||||
}) {
|
||||
const { runSecurityAudit } = await loadSecurityAuditModule();
|
||||
const readOnlyPlugins = resolveReadOnlyChannelPluginsForConfig(params.config, {
|
||||
activationSourceConfig: params.sourceConfig,
|
||||
});
|
||||
return await runSecurityAudit({
|
||||
config: params.config,
|
||||
sourceConfig: params.sourceConfig,
|
||||
deep: false,
|
||||
includeFilesystem: true,
|
||||
includeChannelSecurity: true,
|
||||
plugins: listReadOnlyChannelPluginsForConfig(params.config),
|
||||
...(readOnlyPlugins.missingConfiguredChannelIds.length === 0
|
||||
? { plugins: readOnlyPlugins.plugins }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -672,6 +672,30 @@ describe("listConfiguredChannelIdsForReadOnlyScope", () => {
|
||||
).toEqual(["external-env-channel"]);
|
||||
});
|
||||
|
||||
it("ignores manifest env vars from untrusted external plugins", () => {
|
||||
expect(
|
||||
listConfiguredChannelIdsForReadOnlyScope({
|
||||
config: {} as OpenClawConfig,
|
||||
workspaceDir: "/tmp",
|
||||
env: {
|
||||
EXTERNAL_ENV_CHANNEL_TOKEN: "token",
|
||||
} as NodeJS.ProcessEnv,
|
||||
includePersistedAuthState: false,
|
||||
}),
|
||||
).toEqual([]);
|
||||
|
||||
expect(
|
||||
hasConfiguredChannelsForReadOnlyScope({
|
||||
config: {} as OpenClawConfig,
|
||||
workspaceDir: "/tmp",
|
||||
env: {
|
||||
EXTERNAL_ENV_CHANNEL_TOKEN: "token",
|
||||
} as NodeJS.ProcessEnv,
|
||||
includePersistedAuthState: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("ignores ambient or malformed manifest env vars as read-only configured channel triggers", () => {
|
||||
expect(
|
||||
listConfiguredChannelIdsForReadOnlyScope({
|
||||
|
||||
@@ -76,10 +76,23 @@ function hasNonEmptyEnvValue(env: NodeJS.ProcessEnv, key: string): boolean {
|
||||
|
||||
function listEnvConfiguredManifestChannelIds(params: {
|
||||
records: readonly PluginManifestRecord[];
|
||||
config: OpenClawConfig;
|
||||
activationSourceConfig?: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): string[] {
|
||||
const channelIds = new Set<string>();
|
||||
const trustConfig = params.activationSourceConfig ?? params.config;
|
||||
const normalizedConfig = normalizePluginsConfig(trustConfig.plugins);
|
||||
for (const record of params.records) {
|
||||
if (
|
||||
!isChannelPluginEligibleForScopedOwnership({
|
||||
plugin: record,
|
||||
normalizedConfig,
|
||||
rootConfig: trustConfig,
|
||||
})
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
for (const channelId of record.channels) {
|
||||
const envVars = record.channelEnvVars?.[channelId] ?? [];
|
||||
if (envVars.some((envVar) => hasNonEmptyEnvValue(params.env, envVar))) {
|
||||
@@ -92,6 +105,7 @@ function listEnvConfiguredManifestChannelIds(params: {
|
||||
|
||||
export function listConfiguredChannelIdsForPluginScope(params: {
|
||||
config: OpenClawConfig;
|
||||
activationSourceConfig?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env: NodeJS.ProcessEnv;
|
||||
cache?: boolean;
|
||||
@@ -113,6 +127,8 @@ export function listConfiguredChannelIdsForPluginScope(params: {
|
||||
}),
|
||||
...listEnvConfiguredManifestChannelIds({
|
||||
records,
|
||||
config: params.config,
|
||||
activationSourceConfig: params.activationSourceConfig,
|
||||
env: params.env,
|
||||
}),
|
||||
]),
|
||||
@@ -121,6 +137,7 @@ export function listConfiguredChannelIdsForPluginScope(params: {
|
||||
|
||||
export function listConfiguredChannelIdsForReadOnlyScope(params: {
|
||||
config: OpenClawConfig;
|
||||
activationSourceConfig?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
cache?: boolean;
|
||||
@@ -133,6 +150,7 @@ export function listConfiguredChannelIdsForReadOnlyScope(params: {
|
||||
resolveAgentWorkspaceDir(params.config, resolveDefaultAgentId(params.config));
|
||||
return listConfiguredChannelIdsForPluginScope({
|
||||
config: params.config,
|
||||
activationSourceConfig: params.activationSourceConfig,
|
||||
workspaceDir,
|
||||
env,
|
||||
cache: params.cache,
|
||||
@@ -143,6 +161,7 @@ export function listConfiguredChannelIdsForReadOnlyScope(params: {
|
||||
|
||||
export function hasConfiguredChannelsForReadOnlyScope(params: {
|
||||
config: OpenClawConfig;
|
||||
activationSourceConfig?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
cache?: boolean;
|
||||
@@ -328,6 +347,7 @@ export function resolveConfiguredChannelPluginIds(params: {
|
||||
const configuredChannelIds = new Set(
|
||||
listConfiguredChannelIdsForPluginScope({
|
||||
config: params.config,
|
||||
activationSourceConfig: params.activationSourceConfig,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
}).map((id) => id.trim()),
|
||||
|
||||
Reference in New Issue
Block a user