mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 18:10:45 +00:00
fix: centralize channel presence gating
This commit is contained in:
@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Discord: keep slash command follow-up chunks ephemeral when the command is configured for ephemeral replies, so long `/status` output no longer leaks fallback model or runtime details into the public channel. (#69869) thanks @gumadeiras.
|
||||
- Plugins/discovery: reject package plugin source entries that escape the package directory before explicit runtime entries or inferred built JavaScript peers can be used. (#69868) thanks @gumadeiras.
|
||||
- CLI/channels: keep ambient channel env vars and stale persisted auth from surfacing disabled bundled plugins in status, doctor, security audit, and cron delivery validation unless the channel or plugin is effectively enabled or explicitly configured. (#69862) Thanks @gumadeiras.
|
||||
|
||||
## 2026.4.21
|
||||
|
||||
|
||||
@@ -651,7 +651,10 @@ See [Configuration reference](/gateway/configuration) for the full `plugins.*` s
|
||||
hardcoding the owning provider.
|
||||
- `channelEnvVars` is the cheap metadata path for shell-env fallback, setup
|
||||
prompts, and similar channel surfaces that should not boot plugin runtime
|
||||
just to inspect env names.
|
||||
just to inspect env names. Env names are metadata, not activation by
|
||||
themselves: status, audit, cron delivery validation, and other read-only
|
||||
surfaces still apply plugin trust and effective activation policy before they
|
||||
treat an env var as a configured channel.
|
||||
- `providerAuthChoices` is the cheap metadata path for auth-choice pickers,
|
||||
`--auth-choice` resolution, preferred-provider mapping, and simple onboarding
|
||||
CLI flag registration before provider runtime loads. For runtime wizard
|
||||
|
||||
@@ -155,6 +155,117 @@ module.exports = {
|
||||
return { pluginDir, fullMarker, setupMarker };
|
||||
}
|
||||
|
||||
function writeBundledSetupChannelPlugin(
|
||||
options: {
|
||||
pluginId?: string;
|
||||
channelId?: string;
|
||||
envVar?: string;
|
||||
} = {},
|
||||
) {
|
||||
const bundledRoot = makeTempDir();
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledRoot;
|
||||
const pluginId = options.pluginId ?? "bundled-chat";
|
||||
const channelId = options.channelId ?? pluginId;
|
||||
const envVar = options.envVar ?? "BUNDLED_CHAT_TOKEN";
|
||||
const pluginDir = path.join(bundledRoot, pluginId);
|
||||
fs.mkdirSync(pluginDir, { recursive: true });
|
||||
const fullMarker = path.join(pluginDir, "full-loaded.txt");
|
||||
const setupMarker = path.join(pluginDir, "setup-loaded.txt");
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "package.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: `@openclaw/${pluginId}`,
|
||||
version: "1.0.0",
|
||||
type: "commonjs",
|
||||
openclaw: {
|
||||
extensions: ["./index.cjs"],
|
||||
setupEntry: "./setup-entry.cjs",
|
||||
channel: {
|
||||
id: channelId,
|
||||
label: "Bundled Chat",
|
||||
selectionLabel: "Bundled Chat",
|
||||
docsPath: `/channels/${channelId}`,
|
||||
blurb: "bundled setup entry",
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "openclaw.plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
id: pluginId,
|
||||
configSchema: EMPTY_PLUGIN_SCHEMA,
|
||||
channels: [channelId],
|
||||
channelEnvVars: {
|
||||
[channelId]: [envVar],
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "index.cjs"),
|
||||
`require("node:fs").writeFileSync(${JSON.stringify(fullMarker)}, "loaded", "utf-8");
|
||||
module.exports = {
|
||||
kind: "bundled-channel-entry",
|
||||
id: ${JSON.stringify(pluginId)},
|
||||
name: "Bundled Chat",
|
||||
description: "full entry",
|
||||
register() {},
|
||||
loadChannelPlugin() {
|
||||
return {
|
||||
id: ${JSON.stringify(channelId)},
|
||||
meta: { id: ${JSON.stringify(channelId)}, label: "Bundled Chat" },
|
||||
capabilities: { chatTypes: ["direct"] },
|
||||
config: {
|
||||
listAccountIds: () => ["default"],
|
||||
resolveAccount: () => ({ accountId: "default", token: "configured" }),
|
||||
},
|
||||
outbound: { deliveryMode: "direct" },
|
||||
};
|
||||
},
|
||||
};`,
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "setup-entry.cjs"),
|
||||
`module.exports = {
|
||||
kind: "bundled-channel-setup-entry",
|
||||
loadSetupPlugin() {
|
||||
require("node:fs").writeFileSync(${JSON.stringify(setupMarker)}, "loaded", "utf-8");
|
||||
return {
|
||||
id: ${JSON.stringify(channelId)},
|
||||
meta: {
|
||||
id: ${JSON.stringify(channelId)},
|
||||
label: "Bundled Chat",
|
||||
selectionLabel: "Bundled Chat",
|
||||
docsPath: ${JSON.stringify(`/channels/${channelId}`)},
|
||||
blurb: "bundled setup entry",
|
||||
},
|
||||
capabilities: { chatTypes: ["direct"] },
|
||||
config: {
|
||||
listAccountIds: () => ["default"],
|
||||
resolveAccount: () => ({ accountId: "default", token: "configured" }),
|
||||
},
|
||||
outbound: { deliveryMode: "direct" },
|
||||
};
|
||||
},
|
||||
};`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
return { bundledRoot, pluginDir, fullMarker, setupMarker, pluginId, channelId, envVar };
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
resetPluginLoaderTestStateForTest();
|
||||
});
|
||||
@@ -366,6 +477,49 @@ describe("listReadOnlyChannelPluginsForConfig", () => {
|
||||
expect(fs.existsSync(fullMarker)).toBe(false);
|
||||
});
|
||||
|
||||
it("does not promote disabled bundled channels from ambient env", () => {
|
||||
const { channelId, envVar, fullMarker, setupMarker } = writeBundledSetupChannelPlugin();
|
||||
const plugins = listReadOnlyChannelPluginsForConfig(
|
||||
{
|
||||
plugins: {
|
||||
allow: ["memory-core"],
|
||||
},
|
||||
} as never,
|
||||
{
|
||||
env: { ...process.env, [envVar]: "configured" },
|
||||
includePersistedAuthState: false,
|
||||
},
|
||||
);
|
||||
|
||||
expect(plugins.some((entry) => entry.id === channelId)).toBe(false);
|
||||
expect(fs.existsSync(setupMarker)).toBe(false);
|
||||
expect(fs.existsSync(fullMarker)).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps explicitly enabled bundled channels visible from env configuration", () => {
|
||||
const { channelId, envVar, fullMarker, pluginId, setupMarker } =
|
||||
writeBundledSetupChannelPlugin();
|
||||
const plugins = listReadOnlyChannelPluginsForConfig(
|
||||
{
|
||||
plugins: {
|
||||
allow: [pluginId],
|
||||
entries: {
|
||||
[pluginId]: { enabled: true },
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
{
|
||||
env: { ...process.env, [envVar]: "configured" },
|
||||
includePersistedAuthState: false,
|
||||
},
|
||||
);
|
||||
|
||||
const plugin = plugins.find((entry) => entry.id === channelId);
|
||||
expect(plugin?.meta.blurb).toBe("bundled setup entry");
|
||||
expect(fs.existsSync(setupMarker)).toBe(true);
|
||||
expect(fs.existsSync(fullMarker)).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts option-like env keys through the explicit env option", () => {
|
||||
const { pluginDir, fullMarker, setupMarker } = writeExternalSetupChannelPlugin({
|
||||
pluginId: "external-chat-plugin",
|
||||
|
||||
@@ -368,7 +368,6 @@ export function resolveReadOnlyChannelPluginsForConfig(
|
||||
env,
|
||||
cache: options.cache,
|
||||
includePersistedAuthState: options.includePersistedAuthState,
|
||||
manifestRecords: externalManifestRecords,
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -47,8 +47,10 @@ export async function formatConfigChannelsStatusLines(
|
||||
return buildChannelAccountLine(provider, account, bits);
|
||||
});
|
||||
|
||||
const plugins = listReadOnlyChannelPluginsForConfig(cfg);
|
||||
const sourceConfig = opts?.sourceConfig ?? cfg;
|
||||
const plugins = listReadOnlyChannelPluginsForConfig(cfg, {
|
||||
activationSourceConfig: sourceConfig,
|
||||
});
|
||||
for (const plugin of plugins) {
|
||||
const accountIds = plugin.config.listAccountIds(cfg);
|
||||
if (!accountIds.length) {
|
||||
|
||||
@@ -211,6 +211,7 @@ export async function channelsStatusCommand(
|
||||
},
|
||||
configuredChannels: listConfiguredChannelIdsForReadOnlyScope({
|
||||
config: resolvedConfig,
|
||||
activationSourceConfig: cfg,
|
||||
env: process.env,
|
||||
includePersistedAuthState: false,
|
||||
}),
|
||||
|
||||
@@ -14,6 +14,7 @@ vi.mock("../channels/plugins/bundled-ids.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../channels/plugins/persisted-auth-state.js", () => ({
|
||||
listBundledChannelIdsWithPersistedAuthState: () => ["matrix", "whatsapp"],
|
||||
hasBundledChannelPersistedAuthState: () => false,
|
||||
}));
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@ import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { listAgentEntries, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import { listBundledChannelPluginIds } from "../channels/plugins/bundled-ids.js";
|
||||
import { hasBundledChannelPersistedAuthState } from "../channels/plugins/persisted-auth-state.js";
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import { resolveOAuthDir, resolveStateDir } from "../config/paths.js";
|
||||
import {
|
||||
@@ -21,6 +19,7 @@ import { loadSessionStore } from "../config/sessions/store-load.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { resolveRequiredHomeDir } from "../infra/home-dir.js";
|
||||
import { resolveMemoryBackendConfig } from "../memory-host-sdk/engine-storage.js";
|
||||
import { listConfiguredChannelIdsForReadOnlyScope } from "../plugins/channel-plugin-ids.js";
|
||||
import { normalizeAgentId } from "../routing/session-key.js";
|
||||
import { parseAgentSessionKey } from "../sessions/session-key-utils.js";
|
||||
import { asNullableObjectRecord } from "../shared/record-coerce.js";
|
||||
@@ -549,10 +548,23 @@ function shouldRequireOAuthDir(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): boo
|
||||
if (!channels) {
|
||||
return false;
|
||||
}
|
||||
for (const channelId of listBundledChannelPluginIds()) {
|
||||
if (hasBundledChannelPersistedAuthState({ channelId, cfg, env })) {
|
||||
return true;
|
||||
}
|
||||
const withPersistedAuth = new Set(
|
||||
listConfiguredChannelIdsForReadOnlyScope({
|
||||
config: cfg,
|
||||
env,
|
||||
cache: true,
|
||||
}),
|
||||
);
|
||||
const withoutPersistedAuth = new Set(
|
||||
listConfiguredChannelIdsForReadOnlyScope({
|
||||
config: cfg,
|
||||
env,
|
||||
cache: true,
|
||||
includePersistedAuthState: false,
|
||||
}),
|
||||
);
|
||||
if ([...withPersistedAuth].some((channelId) => !withoutPersistedAuth.has(channelId))) {
|
||||
return true;
|
||||
}
|
||||
// Pairing allowlists are persisted under credentials/<channel>-allowFrom.json.
|
||||
for (const [channelId, channelCfg] of Object.entries(channels)) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import * as configPresence from "../../../channels/config-presence.js";
|
||||
import * as manifestRegistry from "../../../plugins/manifest-registry.js";
|
||||
import { scanConfiguredChannelPluginBlockers } from "./channel-plugin-blockers.js";
|
||||
|
||||
@@ -9,7 +8,6 @@ describe("channel plugin blockers", () => {
|
||||
});
|
||||
|
||||
it("skips plugin registry work when config has no plugin blocker surfaces", () => {
|
||||
const presenceSpy = vi.spyOn(configPresence, "listPotentialConfiguredChannelIds");
|
||||
const registrySpy = vi.spyOn(manifestRegistry, "loadPluginManifestRegistry");
|
||||
|
||||
const hits = scanConfiguredChannelPluginBlockers({
|
||||
@@ -25,12 +23,10 @@ describe("channel plugin blockers", () => {
|
||||
});
|
||||
|
||||
expect(hits).toEqual([]);
|
||||
expect(presenceSpy).not.toHaveBeenCalled();
|
||||
expect(registrySpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("still evaluates configured channels when plugins are disabled globally", () => {
|
||||
vi.spyOn(configPresence, "listPotentialConfiguredChannelIds").mockReturnValue(["slack"]);
|
||||
vi.spyOn(manifestRegistry, "loadPluginManifestRegistry").mockReturnValue({
|
||||
plugins: [
|
||||
{
|
||||
@@ -66,4 +62,48 @@ describe("channel plugin blockers", () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("ignores ambient channel env when reporting plugin blockers", () => {
|
||||
vi.spyOn(manifestRegistry, "loadPluginManifestRegistry").mockReturnValue({
|
||||
plugins: [
|
||||
{
|
||||
id: "slack",
|
||||
origin: "bundled",
|
||||
channels: ["slack"],
|
||||
enabledByDefault: true,
|
||||
},
|
||||
{
|
||||
id: "telegram",
|
||||
origin: "bundled",
|
||||
channels: ["telegram"],
|
||||
enabledByDefault: true,
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
} as unknown as ReturnType<typeof manifestRegistry.loadPluginManifestRegistry>);
|
||||
|
||||
const hits = scanConfiguredChannelPluginBlockers(
|
||||
{
|
||||
plugins: {
|
||||
enabled: false,
|
||||
},
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: "configured",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
SLACK_BOT_TOKEN: "ambient",
|
||||
} as NodeJS.ProcessEnv,
|
||||
);
|
||||
|
||||
expect(hits).toEqual([
|
||||
{
|
||||
channelId: "telegram",
|
||||
pluginId: "telegram",
|
||||
reason: "plugins disabled",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { listPotentialConfiguredChannelIds } from "../../../channels/config-presence.js";
|
||||
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
|
||||
import { listExplicitConfiguredChannelIdsForConfig } from "../../../plugins/channel-plugin-ids.js";
|
||||
import {
|
||||
normalizePluginsConfig,
|
||||
resolveEffectivePluginActivationState,
|
||||
@@ -39,9 +39,7 @@ export function scanConfiguredChannelPluginBlockers(
|
||||
if (!hasExplicitChannelPluginBlockerConfig(cfg)) {
|
||||
return [];
|
||||
}
|
||||
const configuredChannelIds = new Set(
|
||||
listPotentialConfiguredChannelIds(cfg, env).map((id) => id.trim()),
|
||||
);
|
||||
const configuredChannelIds = new Set(listExplicitConfiguredChannelIdsForConfig(cfg));
|
||||
if (configuredChannelIds.size === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -205,7 +205,10 @@ export async function buildChannelsTable(
|
||||
rows: Array<Record<string, string>>;
|
||||
}> = [];
|
||||
|
||||
for (const plugin of listReadOnlyChannelPluginsForConfig(cfg)) {
|
||||
const sourceConfig = opts?.sourceConfig ?? cfg;
|
||||
for (const plugin of listReadOnlyChannelPluginsForConfig(cfg, {
|
||||
activationSourceConfig: sourceConfig,
|
||||
})) {
|
||||
const accountIds = plugin.config.listAccountIds(cfg);
|
||||
const defaultAccountId = resolveChannelDefaultAccountId({
|
||||
plugin,
|
||||
@@ -215,7 +218,6 @@ export async function buildChannelsTable(
|
||||
const resolvedAccountIds = accountIds.length > 0 ? accountIds : [defaultAccountId];
|
||||
|
||||
const accounts: ChannelAccountRow[] = [];
|
||||
const sourceConfig = opts?.sourceConfig ?? cfg;
|
||||
for (const accountId of resolvedAccountIds) {
|
||||
accounts.push(
|
||||
await resolveChannelAccountRow({
|
||||
|
||||
@@ -14,8 +14,12 @@ export type LinkChannelContext = {
|
||||
|
||||
export async function resolveLinkChannelContext(
|
||||
cfg: OpenClawConfig,
|
||||
options: { sourceConfig?: OpenClawConfig } = {},
|
||||
): Promise<LinkChannelContext | null> {
|
||||
for (const plugin of listReadOnlyChannelPluginsForConfig(cfg)) {
|
||||
const sourceConfig = options.sourceConfig ?? cfg;
|
||||
for (const plugin of listReadOnlyChannelPluginsForConfig(cfg, {
|
||||
activationSourceConfig: sourceConfig,
|
||||
})) {
|
||||
const { defaultAccountId, account, enabled, configured } =
|
||||
await resolveDefaultChannelAccountContext(plugin, cfg, {
|
||||
mode: "read_only",
|
||||
|
||||
@@ -13,8 +13,8 @@ const mocks = vi.hoisted(() => ({
|
||||
buildChannelsTable: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../channels/config-presence.js", () => ({
|
||||
hasPotentialConfiguredChannels: mocks.hasPotentialConfiguredChannels,
|
||||
vi.mock("../plugins/channel-plugin-ids.js", () => ({
|
||||
hasConfiguredChannelsForReadOnlyScope: mocks.hasPotentialConfiguredChannels,
|
||||
}));
|
||||
|
||||
vi.mock("../cli/command-config-resolution.js", () => ({
|
||||
|
||||
@@ -133,7 +133,7 @@ export async function collectStatusScanOverview(params: {
|
||||
showSecrets: boolean;
|
||||
runtime?: RuntimeEnv;
|
||||
allowMissingConfigFastPath?: boolean;
|
||||
resolveHasConfiguredChannels?: (cfg: OpenClawConfig) => boolean;
|
||||
resolveHasConfiguredChannels?: (cfg: OpenClawConfig, sourceConfig: OpenClawConfig) => boolean;
|
||||
includeChannelsData?: boolean;
|
||||
useGatewayCallOverridesForChannelsStatus?: boolean;
|
||||
progress?: {
|
||||
@@ -177,8 +177,8 @@ export async function collectStatusScanOverview(params: {
|
||||
});
|
||||
params.progress?.tick();
|
||||
const hasConfiguredChannels = params.resolveHasConfiguredChannels
|
||||
? params.resolveHasConfiguredChannels(cfg)
|
||||
: hasConfiguredChannelsForReadOnlyScope({ config: cfg });
|
||||
? params.resolveHasConfiguredChannels(cfg, sourceConfig)
|
||||
: hasConfiguredChannelsForReadOnlyScope({ config: cfg, activationSourceConfig: sourceConfig });
|
||||
const osSummary = resolveOsSummary();
|
||||
const bootstrap = await createStatusScanCoreBootstrap<
|
||||
Awaited<ReturnType<typeof getAgentLocalStatusesFn>>
|
||||
|
||||
@@ -13,7 +13,7 @@ type StatusJsonScanPolicy = {
|
||||
commandName: string;
|
||||
allowMissingConfigFastPath?: boolean;
|
||||
includeChannelSummary?: boolean;
|
||||
resolveHasConfiguredChannels: (cfg: OpenClawConfig) => boolean;
|
||||
resolveHasConfiguredChannels: (cfg: OpenClawConfig, sourceConfig: OpenClawConfig) => boolean;
|
||||
resolveMemory: Parameters<typeof executeStatusScanFromOverview>[0]["resolveMemory"];
|
||||
};
|
||||
|
||||
@@ -58,9 +58,10 @@ export async function scanStatusJsonFast(
|
||||
commandName: "status --json",
|
||||
allowMissingConfigFastPath: true,
|
||||
includeChannelSummary: false,
|
||||
resolveHasConfiguredChannels: (cfg) =>
|
||||
resolveHasConfiguredChannels: (cfg, sourceConfig) =>
|
||||
hasConfiguredChannelsForReadOnlyScope({
|
||||
config: cfg,
|
||||
activationSourceConfig: sourceConfig,
|
||||
env: process.env,
|
||||
includePersistedAuthState: false,
|
||||
}),
|
||||
|
||||
@@ -25,8 +25,11 @@ export async function scanStatus(
|
||||
_runtime,
|
||||
{
|
||||
commandName: "status --json",
|
||||
resolveHasConfiguredChannels: (cfg) =>
|
||||
hasConfiguredChannelsForReadOnlyScope({ config: cfg }),
|
||||
resolveHasConfiguredChannels: (cfg, sourceConfig) =>
|
||||
hasConfiguredChannelsForReadOnlyScope({
|
||||
config: cfg,
|
||||
activationSourceConfig: sourceConfig,
|
||||
}),
|
||||
resolveMemory: async ({ cfg, agentStatus, memoryPlugin }) =>
|
||||
await resolveStatusMemoryStatusSnapshot({
|
||||
cfg,
|
||||
|
||||
@@ -118,14 +118,15 @@ export async function getStatusSummary(
|
||||
resolveSessionModelRef,
|
||||
} = await loadStatusSummaryRuntimeModule();
|
||||
const cfg = options.config ?? (await loadConfigIoModule()).loadConfig();
|
||||
const channelScopeConfig =
|
||||
options.sourceConfig === undefined
|
||||
? { config: cfg }
|
||||
: { config: cfg, activationSourceConfig: options.sourceConfig };
|
||||
const needsChannelPlugins =
|
||||
includeChannelSummary &&
|
||||
hasConfiguredChannelsForReadOnlyScope({
|
||||
config: cfg,
|
||||
});
|
||||
includeChannelSummary && hasConfiguredChannelsForReadOnlyScope(channelScopeConfig);
|
||||
const linkContext = needsChannelPlugins
|
||||
? await loadLinkChannelModule().then(({ resolveLinkChannelContext }) =>
|
||||
resolveLinkChannelContext(cfg),
|
||||
resolveLinkChannelContext(cfg, { sourceConfig: options.sourceConfig }),
|
||||
)
|
||||
: null;
|
||||
const agentList = listGatewayAgentsBasic(cfg);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { listPotentialConfiguredChannelIds } from "../../channels/config-presence.js";
|
||||
import { loadConfig } from "../../config/config.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { resolveCronDeliveryPreviews } from "../../cron/delivery-preview.js";
|
||||
@@ -13,6 +12,7 @@ import { isInvalidCronSessionTargetIdError } from "../../cron/session-target.js"
|
||||
import type { CronDelivery, CronJob, CronJobCreate, CronJobPatch } from "../../cron/types.js";
|
||||
import { validateScheduleTimestamp } from "../../cron/validate-timestamp.js";
|
||||
import { formatErrorMessage } from "../../infra/errors.js";
|
||||
import { listConfiguredAnnounceChannelIdsForConfig } from "../../plugins/channel-plugin-ids.js";
|
||||
import { normalizeMessageChannel } from "../../utils/message-channel.js";
|
||||
import {
|
||||
ErrorCodes,
|
||||
@@ -30,9 +30,11 @@ import {
|
||||
import type { GatewayRequestHandlers } from "./types.js";
|
||||
|
||||
function listConfiguredAnnounceChannelIds(cfg: OpenClawConfig): string[] {
|
||||
return listPotentialConfiguredChannelIds(cfg, process.env, {
|
||||
includePersistedAuthState: false,
|
||||
}).filter((channelId) => cfg.channels?.[channelId]?.enabled !== false);
|
||||
return listConfiguredAnnounceChannelIdsForConfig({
|
||||
config: cfg,
|
||||
env: process.env,
|
||||
cache: true,
|
||||
});
|
||||
}
|
||||
|
||||
function assertConfiguredAnnounceChannel(params: {
|
||||
|
||||
@@ -740,6 +740,12 @@ describe("gateway server cron", () => {
|
||||
appToken: "xapp-slack-token",
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
telegram: { enabled: true },
|
||||
slack: { enabled: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
@@ -763,6 +769,43 @@ describe("gateway server cron", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("ignores ambient disabled channel env when validating announce delivery", async () => {
|
||||
vi.stubEnv("SLACK_BOT_TOKEN", "xoxb-ambient");
|
||||
vi.stubEnv("TELEGRAM_BOT_TOKEN", "ambient-telegram");
|
||||
const { prevSkipCron } = await setupCronTestRun({
|
||||
tempPrefix: "openclaw-gw-cron-ambient-disabled-delivery-",
|
||||
cronEnabled: false,
|
||||
});
|
||||
|
||||
await writeCronConfig({
|
||||
session: {
|
||||
mainKey: "main",
|
||||
},
|
||||
plugins: {
|
||||
allow: ["memory-core"],
|
||||
},
|
||||
});
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
|
||||
try {
|
||||
const addRes = await rpcReq(ws, "cron.add", {
|
||||
name: "ambient disabled announce",
|
||||
enabled: true,
|
||||
schedule: { kind: "every", everyMs: 60_000 },
|
||||
sessionTarget: "isolated",
|
||||
wakeMode: "next-heartbeat",
|
||||
payload: { kind: "agentTurn", message: "hello" },
|
||||
delivery: { mode: "announce" },
|
||||
});
|
||||
|
||||
expect(addRes.ok).toBe(true);
|
||||
} finally {
|
||||
await cleanupCronTestRun({ ws, server, prevSkipCron });
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects ambiguous announce delivery on update when multiple channels are configured", async () => {
|
||||
const { prevSkipCron } = await setupCronTestRun({
|
||||
tempPrefix: "openclaw-gw-cron-ambiguous-delivery-update-",
|
||||
@@ -782,6 +825,12 @@ describe("gateway server cron", () => {
|
||||
appToken: "xapp-slack-token",
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
telegram: { enabled: true },
|
||||
slack: { enabled: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
@@ -832,6 +881,11 @@ describe("gateway server cron", () => {
|
||||
appToken: "xapp-slack-token",
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
slack: { enabled: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
|
||||
@@ -48,9 +48,14 @@ async function loadChannelSummaryConfig(): Promise<OpenClawConfig> {
|
||||
return loadConfig();
|
||||
}
|
||||
|
||||
async function listChannelSummaryPlugins(cfg: OpenClawConfig): Promise<ChannelPlugin[]> {
|
||||
async function listChannelSummaryPlugins(params: {
|
||||
cfg: OpenClawConfig;
|
||||
sourceConfig: OpenClawConfig;
|
||||
}): Promise<ChannelPlugin[]> {
|
||||
const { listReadOnlyChannelPluginsForConfig } = await import("../channels/plugins/read-only.js");
|
||||
return listReadOnlyChannelPluginsForConfig(cfg);
|
||||
return listReadOnlyChannelPluginsForConfig(params.cfg, {
|
||||
activationSourceConfig: params.sourceConfig,
|
||||
});
|
||||
}
|
||||
|
||||
const buildAccountDetails = (params: {
|
||||
@@ -123,7 +128,8 @@ export async function buildChannelSummary(
|
||||
resolved.colorize && color ? color(value) : value;
|
||||
const sourceConfig = options?.sourceConfig ?? effective;
|
||||
|
||||
const plugins = options?.plugins ?? (await listChannelSummaryPlugins(effective));
|
||||
const plugins =
|
||||
options?.plugins ?? (await listChannelSummaryPlugins({ cfg: effective, sourceConfig }));
|
||||
for (const plugin of plugins) {
|
||||
const accountIds = plugin.config.listAccountIds(effective);
|
||||
const defaultAccountId =
|
||||
|
||||
@@ -3,11 +3,22 @@ import type { OpenClawConfig } from "../config/config.js";
|
||||
|
||||
const listPotentialConfiguredChannelIds = vi.hoisted(() => vi.fn());
|
||||
const hasPotentialConfiguredChannels = vi.hoisted(() => vi.fn());
|
||||
const hasMeaningfulChannelConfig = vi.hoisted(() =>
|
||||
vi.fn((value: unknown) => {
|
||||
return (
|
||||
!!value &&
|
||||
typeof value === "object" &&
|
||||
!Array.isArray(value) &&
|
||||
Object.keys(value).some((key) => key !== "enabled")
|
||||
);
|
||||
}),
|
||||
);
|
||||
const loadPluginManifestRegistry = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../channels/config-presence.js", () => ({
|
||||
listPotentialConfiguredChannelIds,
|
||||
hasPotentialConfiguredChannels,
|
||||
hasMeaningfulChannelConfig,
|
||||
}));
|
||||
|
||||
vi.mock("./manifest-registry.js", async (importOriginal) => {
|
||||
@@ -20,7 +31,9 @@ vi.mock("./manifest-registry.js", async (importOriginal) => {
|
||||
|
||||
import {
|
||||
hasConfiguredChannelsForReadOnlyScope,
|
||||
listConfiguredAnnounceChannelIdsForConfig,
|
||||
listConfiguredChannelIdsForReadOnlyScope,
|
||||
listExplicitConfiguredChannelIdsForConfig,
|
||||
resolveConfiguredChannelPluginIds,
|
||||
resolveGatewayStartupPluginIds,
|
||||
} from "./channel-plugin-ids.js";
|
||||
@@ -652,9 +665,167 @@ describe("listConfiguredChannelIdsForReadOnlyScope", () => {
|
||||
beforeEach(() => {
|
||||
listPotentialConfiguredChannelIds.mockReset().mockReturnValue([]);
|
||||
hasPotentialConfiguredChannels.mockReset().mockReturnValue(false);
|
||||
hasMeaningfulChannelConfig.mockClear();
|
||||
loadPluginManifestRegistry.mockReset().mockReturnValue(createManifestRegistryFixture());
|
||||
});
|
||||
|
||||
it("filters bundled ambient channel triggers through effective activation", () => {
|
||||
listPotentialConfiguredChannelIds.mockReturnValue(["demo-channel"]);
|
||||
|
||||
expect(
|
||||
listConfiguredChannelIdsForReadOnlyScope({
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["memory-core"],
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
workspaceDir: "/tmp",
|
||||
env: {
|
||||
DEMO_CHANNEL_TOKEN: "token",
|
||||
} as NodeJS.ProcessEnv,
|
||||
includePersistedAuthState: false,
|
||||
}),
|
||||
).toEqual([]);
|
||||
|
||||
expect(
|
||||
hasConfiguredChannelsForReadOnlyScope({
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["memory-core"],
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
workspaceDir: "/tmp",
|
||||
env: {
|
||||
DEMO_CHANNEL_TOKEN: "token",
|
||||
} as NodeJS.ProcessEnv,
|
||||
includePersistedAuthState: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps explicitly enabled bundled ambient channel triggers", () => {
|
||||
listPotentialConfiguredChannelIds.mockReturnValue(["demo-channel"]);
|
||||
|
||||
expect(
|
||||
listConfiguredChannelIdsForReadOnlyScope({
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
"demo-channel": {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
workspaceDir: "/tmp",
|
||||
env: {
|
||||
DEMO_CHANNEL_TOKEN: "token",
|
||||
} as NodeJS.ProcessEnv,
|
||||
includePersistedAuthState: false,
|
||||
}),
|
||||
).toEqual(["demo-channel"]);
|
||||
});
|
||||
|
||||
it("keeps explicitly configured bundled channels discovered from potential ids", () => {
|
||||
listPotentialConfiguredChannelIds.mockReturnValue(["demo-channel"]);
|
||||
|
||||
expect(
|
||||
listConfiguredChannelIdsForReadOnlyScope({
|
||||
config: {
|
||||
channels: {
|
||||
"demo-channel": {
|
||||
token: "configured",
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
workspaceDir: "/tmp",
|
||||
env: {},
|
||||
includePersistedAuthState: false,
|
||||
}),
|
||||
).toEqual(["demo-channel"]);
|
||||
});
|
||||
|
||||
it("blocks explicitly configured bundled channels when plugins are disabled or denied", () => {
|
||||
listPotentialConfiguredChannelIds.mockReturnValue(["demo-channel"]);
|
||||
|
||||
expect(
|
||||
listConfiguredChannelIdsForReadOnlyScope({
|
||||
config: {
|
||||
channels: {
|
||||
"demo-channel": {
|
||||
token: "configured",
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
enabled: false,
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
workspaceDir: "/tmp",
|
||||
env: {},
|
||||
includePersistedAuthState: false,
|
||||
}),
|
||||
).toEqual([]);
|
||||
|
||||
expect(
|
||||
listConfiguredChannelIdsForReadOnlyScope({
|
||||
config: {
|
||||
channels: {
|
||||
"demo-channel": {
|
||||
token: "configured",
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
deny: ["demo-channel"],
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
workspaceDir: "/tmp",
|
||||
env: {},
|
||||
includePersistedAuthState: false,
|
||||
}),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("lists explicit configured channels without ambient env triggers", () => {
|
||||
expect(
|
||||
listExplicitConfiguredChannelIdsForConfig({
|
||||
channels: {
|
||||
defaults: {
|
||||
model: "sonnet-4.6",
|
||||
},
|
||||
"demo-channel": {
|
||||
token: "configured",
|
||||
},
|
||||
"demo-other-channel": {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig),
|
||||
).toEqual(["demo-channel"]);
|
||||
});
|
||||
|
||||
it("uses effective read-only channel policy for announce channels", () => {
|
||||
listPotentialConfiguredChannelIds.mockReturnValue(["demo-channel", "demo-other-channel"]);
|
||||
|
||||
expect(
|
||||
listConfiguredAnnounceChannelIdsForConfig({
|
||||
config: {
|
||||
channels: {
|
||||
"demo-other-channel": {
|
||||
token: "configured",
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
allow: ["demo-other-channel"],
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
workspaceDir: "/tmp",
|
||||
env: {
|
||||
DEMO_CHANNEL_TOKEN: "ambient",
|
||||
} as NodeJS.ProcessEnv,
|
||||
}),
|
||||
).toEqual(["demo-other-channel"]);
|
||||
});
|
||||
|
||||
it("uses manifest env vars as read-only configured channel triggers", () => {
|
||||
expect(
|
||||
listConfiguredChannelIdsForReadOnlyScope({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import { collectConfiguredAgentHarnessRuntimes } from "../agents/harness-runtimes.js";
|
||||
import {
|
||||
hasPotentialConfiguredChannels,
|
||||
hasMeaningfulChannelConfig,
|
||||
listPotentialConfiguredChannelIds,
|
||||
} from "../channels/config-presence.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
@@ -66,6 +66,8 @@ function normalizeChannelIds(channelIds: Iterable<string>): string[] {
|
||||
).toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
const IGNORED_CHANNEL_CONFIG_KEYS = new Set(["defaults", "modelByChannel"]);
|
||||
|
||||
function hasNonEmptyEnvValue(env: NodeJS.ProcessEnv, key: string): boolean {
|
||||
if (!isSafeChannelEnvVarTriggerName(key)) {
|
||||
return false;
|
||||
@@ -104,6 +106,127 @@ function listEnvConfiguredManifestChannelIds(params: {
|
||||
return [...channelIds].toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export function hasExplicitChannelConfig(params: {
|
||||
config: OpenClawConfig;
|
||||
channelId: string;
|
||||
}): boolean {
|
||||
const channels = params.config.channels;
|
||||
if (!channels || typeof channels !== "object" || Array.isArray(channels)) {
|
||||
return false;
|
||||
}
|
||||
const entry = (channels as Record<string, unknown>)[params.channelId];
|
||||
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
||||
return false;
|
||||
}
|
||||
return (entry as { enabled?: unknown }).enabled === true || hasMeaningfulChannelConfig(entry);
|
||||
}
|
||||
|
||||
export function listExplicitConfiguredChannelIdsForConfig(config: OpenClawConfig): string[] {
|
||||
const channels = config.channels;
|
||||
if (!channels || typeof channels !== "object" || Array.isArray(channels)) {
|
||||
return [];
|
||||
}
|
||||
return Object.keys(channels)
|
||||
.filter(
|
||||
(channelId) =>
|
||||
!IGNORED_CHANNEL_CONFIG_KEYS.has(channelId) &&
|
||||
hasExplicitChannelConfig({ config, channelId }),
|
||||
)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
function recordOwnsChannel(record: PluginManifestRecord, channelId: string): boolean {
|
||||
const normalizedChannelId = normalizeOptionalLowercaseString(channelId) ?? "";
|
||||
if (!normalizedChannelId) {
|
||||
return false;
|
||||
}
|
||||
return [...record.channels, ...(record.activation?.onChannels ?? [])].some(
|
||||
(ownedChannelId) =>
|
||||
(normalizeOptionalLowercaseString(ownedChannelId) ?? "") === normalizedChannelId,
|
||||
);
|
||||
}
|
||||
|
||||
function isChannelPluginEligibleForEffectiveConfiguredChannel(params: {
|
||||
plugin: PluginManifestRecord;
|
||||
channelId: string;
|
||||
normalizedConfig: ReturnType<typeof normalizePluginsConfig>;
|
||||
config: OpenClawConfig;
|
||||
activationSource: ReturnType<typeof createPluginActivationSource>;
|
||||
}): boolean {
|
||||
if (
|
||||
!passesManifestOwnerBasePolicy({
|
||||
plugin: params.plugin,
|
||||
normalizedConfig: params.normalizedConfig,
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!isBundledManifestOwner(params.plugin)) {
|
||||
if (params.plugin.origin === "global" || params.plugin.origin === "config") {
|
||||
return hasExplicitManifestOwnerTrust({
|
||||
plugin: params.plugin,
|
||||
normalizedConfig: params.normalizedConfig,
|
||||
});
|
||||
}
|
||||
return isActivatedManifestOwner({
|
||||
plugin: params.plugin,
|
||||
normalizedConfig: params.normalizedConfig,
|
||||
rootConfig: params.activationSource.rootConfig,
|
||||
});
|
||||
}
|
||||
if (
|
||||
hasExplicitChannelConfig({
|
||||
config: params.activationSource.rootConfig ?? params.config,
|
||||
channelId: params.channelId,
|
||||
})
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return resolveEffectivePluginActivationState({
|
||||
id: params.plugin.id,
|
||||
origin: params.plugin.origin,
|
||||
config: params.normalizedConfig,
|
||||
rootConfig: params.config,
|
||||
enabledByDefault: params.plugin.enabledByDefault,
|
||||
activationSource: params.activationSource,
|
||||
}).enabled;
|
||||
}
|
||||
|
||||
function filterEffectiveConfiguredChannelIds(params: {
|
||||
channelIds: Iterable<string>;
|
||||
records: readonly PluginManifestRecord[];
|
||||
config: OpenClawConfig;
|
||||
activationSourceConfig?: OpenClawConfig;
|
||||
}): string[] {
|
||||
const channelIds = normalizeChannelIds(params.channelIds);
|
||||
if (channelIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const activationSource = createPluginActivationSource({
|
||||
config: params.activationSourceConfig ?? params.config,
|
||||
});
|
||||
const normalizedConfig = activationSource.plugins;
|
||||
const effective = new Set<string>();
|
||||
for (const channelId of channelIds) {
|
||||
if (
|
||||
params.records.some(
|
||||
(record) =>
|
||||
recordOwnsChannel(record, channelId) &&
|
||||
isChannelPluginEligibleForEffectiveConfiguredChannel({
|
||||
plugin: record,
|
||||
channelId,
|
||||
normalizedConfig,
|
||||
config: params.config,
|
||||
activationSource,
|
||||
}),
|
||||
)
|
||||
) {
|
||||
effective.add(channelId);
|
||||
}
|
||||
}
|
||||
return [...effective].toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
function listConfiguredChannelIdsForPluginScope(params: {
|
||||
config: OpenClawConfig;
|
||||
activationSourceConfig?: OpenClawConfig;
|
||||
@@ -121,7 +244,7 @@ function listConfiguredChannelIdsForPluginScope(params: {
|
||||
env: params.env,
|
||||
cache: params.cache,
|
||||
}).plugins;
|
||||
return [
|
||||
const channelIds = [
|
||||
...new Set([
|
||||
...listPotentialConfiguredChannelIds(params.config, params.env, {
|
||||
includePersistedAuthState: params.includePersistedAuthState,
|
||||
@@ -133,7 +256,13 @@ function listConfiguredChannelIdsForPluginScope(params: {
|
||||
env: params.env,
|
||||
}),
|
||||
]),
|
||||
].toSorted((left, right) => left.localeCompare(right));
|
||||
];
|
||||
return filterEffectiveConfiguredChannelIds({
|
||||
channelIds,
|
||||
records,
|
||||
config: params.config,
|
||||
activationSourceConfig: params.activationSourceConfig,
|
||||
});
|
||||
}
|
||||
|
||||
export function listConfiguredChannelIdsForReadOnlyScope(params: {
|
||||
@@ -169,22 +298,48 @@ export function hasConfiguredChannelsForReadOnlyScope(params: {
|
||||
includePersistedAuthState?: boolean;
|
||||
manifestRecords?: readonly PluginManifestRecord[];
|
||||
}): boolean {
|
||||
const env = params.env ?? process.env;
|
||||
if (
|
||||
hasPotentialConfiguredChannels(params.config, env, {
|
||||
includePersistedAuthState: params.includePersistedAuthState,
|
||||
})
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
listConfiguredChannelIdsForReadOnlyScope({
|
||||
...params,
|
||||
env,
|
||||
}).length > 0
|
||||
);
|
||||
}
|
||||
|
||||
export function listConfiguredAnnounceChannelIdsForConfig(params: {
|
||||
config: OpenClawConfig;
|
||||
activationSourceConfig?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
cache?: boolean;
|
||||
}): string[] {
|
||||
const channels = params.config.channels;
|
||||
const disabledChannelIds = new Set(
|
||||
channels && typeof channels === "object" && !Array.isArray(channels)
|
||||
? Object.entries(channels)
|
||||
.filter(([, value]) => {
|
||||
return (
|
||||
value &&
|
||||
typeof value === "object" &&
|
||||
!Array.isArray(value) &&
|
||||
(value as { enabled?: unknown }).enabled === false
|
||||
);
|
||||
})
|
||||
.map(([channelId]) => channelId)
|
||||
: [],
|
||||
);
|
||||
return normalizeChannelIds([
|
||||
...listExplicitConfiguredChannelIdsForConfig(params.config),
|
||||
...listConfiguredChannelIdsForReadOnlyScope({
|
||||
config: params.config,
|
||||
activationSourceConfig: params.activationSourceConfig,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
cache: params.cache,
|
||||
includePersistedAuthState: false,
|
||||
}),
|
||||
]).filter((channelId) => !disabledChannelIds.has(channelId));
|
||||
}
|
||||
|
||||
function isChannelPluginEligibleForScopedOwnership(params: {
|
||||
plugin: PluginManifestRecord;
|
||||
normalizedConfig: ReturnType<typeof normalizePluginsConfig>;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import path from "node:path";
|
||||
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import { resolveSandboxConfigForAgent } from "../agents/sandbox/config.js";
|
||||
import { hasPotentialConfiguredChannels } from "../channels/config-presence.js";
|
||||
import type { listChannelPlugins } from "../channels/plugins/index.js";
|
||||
import type { ConfigFileSnapshot, OpenClawConfig } from "../config/config.js";
|
||||
import { resolveConfigPath, resolveStateDir } from "../config/paths.js";
|
||||
@@ -14,7 +13,10 @@ import {
|
||||
} from "../infra/exec-safe-bin-runtime-policy.js";
|
||||
import { listRiskyConfiguredSafeBins } from "../infra/exec-safe-bin-semantics.js";
|
||||
import { normalizeTrustedSafeBinDirs } from "../infra/exec-safe-bin-trust.js";
|
||||
import { resolveConfiguredChannelPluginIds } from "../plugins/channel-plugin-ids.js";
|
||||
import {
|
||||
hasConfiguredChannelsForReadOnlyScope,
|
||||
resolveConfiguredChannelPluginIds,
|
||||
} from "../plugins/channel-plugin-ids.js";
|
||||
import { getActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { DEFAULT_AGENT_ID } from "../routing/session-key.js";
|
||||
import { asNullableRecord } from "../shared/record-coerce.js";
|
||||
@@ -1009,7 +1011,12 @@ export async function runSecurityAudit(opts: SecurityAuditOptions): Promise<Secu
|
||||
const shouldAuditChannelSecurity =
|
||||
context.includeChannelSecurity &&
|
||||
(context.plugins !== undefined ||
|
||||
hasPotentialConfiguredChannels(cfg, env) ||
|
||||
hasConfiguredChannelsForReadOnlyScope({
|
||||
config: cfg,
|
||||
activationSourceConfig: context.sourceConfig,
|
||||
workspaceDir: context.workspaceDir,
|
||||
env,
|
||||
}) ||
|
||||
resolveConfiguredChannelPluginIds({
|
||||
config: cfg,
|
||||
activationSourceConfig: context.sourceConfig,
|
||||
|
||||
Reference in New Issue
Block a user