refactor: split channel presence policy

This commit is contained in:
Gustavo Madeira Santana
2026-04-21 19:31:08 -04:00
parent b8787904ab
commit 9b7bbd2662
8 changed files with 896 additions and 628 deletions

View File

@@ -6,6 +6,7 @@ import type { OpenClawConfig } from "../config/config.js";
import {
hasMeaningfulChannelConfig,
hasPotentialConfiguredChannels,
listPotentialConfiguredChannelPresenceSignals,
listPotentialConfiguredChannelIds,
} from "./config-presence.js";
@@ -90,6 +91,11 @@ describe("config presence", () => {
expectedConfigured: true,
options: { includePersistedAuthState: false },
});
expect(
listPotentialConfiguredChannelPresenceSignals({}, env, {
includePersistedAuthState: false,
}),
).toEqual([{ channelId: "matrix", source: "env" }]);
});
it("detects persisted Matrix credentials without config or env", () => {

View File

@@ -24,6 +24,13 @@ type ChannelPresenceOptions = {
};
};
export type ChannelPresenceSignalSource = "config" | "env" | "persisted-auth";
export type ChannelPresenceSignal = {
channelId: string;
source: ChannelPresenceSignalSource;
};
export function hasMeaningfulChannelConfig(value: unknown): boolean {
if (!isRecord(value)) {
return false;
@@ -76,6 +83,30 @@ export function listPotentialConfiguredChannelIds(
env: NodeJS.ProcessEnv = process.env,
options: ChannelPresenceOptions = {},
): string[] {
return [
...new Set(
listPotentialConfiguredChannelPresenceSignals(cfg, env, options).map(
(signal) => signal.channelId,
),
),
];
}
export function listPotentialConfiguredChannelPresenceSignals(
cfg: OpenClawConfig,
env: NodeJS.ProcessEnv = process.env,
options: ChannelPresenceOptions = {},
): ChannelPresenceSignal[] {
const signals: ChannelPresenceSignal[] = [];
const seenSignals = new Set<string>();
const addSignal = (channelId: string, source: ChannelPresenceSignalSource) => {
const key = `${source}:${channelId}`;
if (seenSignals.has(key)) {
return;
}
seenSignals.add(key);
signals.push({ channelId, source });
};
const configuredChannelIds = new Set<string>();
const channelIds = listBundledChannelPluginIds();
const channelEnvPrefixes = listChannelEnvPrefixes(channelIds);
@@ -87,6 +118,7 @@ export function listPotentialConfiguredChannelIds(
}
if (hasMeaningfulChannelConfig(value)) {
configuredChannelIds.add(key);
addSignal(key, "config");
}
}
}
@@ -98,6 +130,7 @@ export function listPotentialConfiguredChannelIds(
for (const [prefix, channelId] of channelEnvPrefixes) {
if (key.startsWith(prefix)) {
configuredChannelIds.add(channelId);
addSignal(channelId, "env");
}
}
}
@@ -106,11 +139,12 @@ export function listPotentialConfiguredChannelIds(
for (const channelId of listPersistedAuthStateChannelIds(options)) {
if (hasPersistedAuthState({ channelId, cfg, env, options })) {
configuredChannelIds.add(channelId);
addSignal(channelId, "persisted-auth");
}
}
}
return [...configuredChannelIds];
return signals.filter((signal) => configuredChannelIds.has(signal.channelId));
}
function hasEnvConfiguredChannel(

View File

@@ -482,6 +482,10 @@ vi.mock("../channels/config-presence.js", () => ({
),
listPotentialConfiguredChannelIds: (cfg: { channels?: Record<string, unknown> }) =>
Object.keys(cfg.channels ?? {}).filter((key) => key !== "defaults" && key !== "modelByChannel"),
listPotentialConfiguredChannelPresenceSignals: (cfg: { channels?: Record<string, unknown> }) =>
Object.keys(cfg.channels ?? {})
.filter((key) => key !== "defaults" && key !== "modelByChannel")
.map((channelId) => ({ channelId, source: "config" })),
}));
vi.mock("../plugins/memory-runtime.js", () => ({

View File

@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
const listPotentialConfiguredChannelIds = vi.hoisted(() => vi.fn());
const listPotentialConfiguredChannelPresenceSignals = vi.hoisted(() => vi.fn());
const hasPotentialConfiguredChannels = vi.hoisted(() => vi.fn());
const hasMeaningfulChannelConfig = vi.hoisted(() =>
vi.fn((value: unknown) => {
@@ -17,6 +18,7 @@ const loadPluginManifestRegistry = vi.hoisted(() => vi.fn());
vi.mock("../channels/config-presence.js", () => ({
listPotentialConfiguredChannelIds,
listPotentialConfiguredChannelPresenceSignals,
hasPotentialConfiguredChannels,
hasMeaningfulChannelConfig,
}));
@@ -34,6 +36,7 @@ import {
listConfiguredAnnounceChannelIdsForConfig,
listConfiguredChannelIdsForReadOnlyScope,
listExplicitConfiguredChannelIdsForConfig,
resolveConfiguredChannelPresencePolicy,
resolveConfiguredChannelPluginIds,
resolveGatewayStartupPluginIds,
} from "./channel-plugin-ids.js";
@@ -329,6 +332,14 @@ describe("resolveGatewayStartupPluginIds", () => {
}
return ["demo-channel"];
});
listPotentialConfiguredChannelPresenceSignals
.mockReset()
.mockImplementation((config: OpenClawConfig) => {
return listPotentialConfiguredChannelIds(config).map((channelId: string) => ({
channelId,
source: "config",
}));
});
hasPotentialConfiguredChannels.mockReset().mockImplementation((config: OpenClawConfig) => {
if (Object.prototype.hasOwnProperty.call(config, "channels")) {
return Object.keys(config.channels ?? {}).length > 0;
@@ -502,6 +513,14 @@ describe("resolveConfiguredChannelPluginIds", () => {
}
return [];
});
listPotentialConfiguredChannelPresenceSignals
.mockReset()
.mockImplementation((config: OpenClawConfig) => {
return listPotentialConfiguredChannelIds(config).map((channelId: string) => ({
channelId,
source: "config",
}));
});
hasPotentialConfiguredChannels.mockReset().mockImplementation((config: OpenClawConfig) => {
if (Object.prototype.hasOwnProperty.call(config, "channels")) {
return Object.keys(config.channels ?? {}).length > 0;
@@ -664,6 +683,7 @@ describe("resolveConfiguredChannelPluginIds", () => {
describe("listConfiguredChannelIdsForReadOnlyScope", () => {
beforeEach(() => {
listPotentialConfiguredChannelIds.mockReset().mockReturnValue([]);
listPotentialConfiguredChannelPresenceSignals.mockReset().mockReturnValue([]);
hasPotentialConfiguredChannels.mockReset().mockReturnValue(false);
hasMeaningfulChannelConfig.mockClear();
loadPluginManifestRegistry.mockReset().mockReturnValue(createManifestRegistryFixture());
@@ -671,6 +691,9 @@ describe("listConfiguredChannelIdsForReadOnlyScope", () => {
it("filters bundled ambient channel triggers through effective activation", () => {
listPotentialConfiguredChannelIds.mockReturnValue(["demo-channel"]);
listPotentialConfiguredChannelPresenceSignals.mockReturnValue([
{ channelId: "demo-channel", source: "env" },
]);
expect(
listConfiguredChannelIdsForReadOnlyScope({
@@ -703,8 +726,41 @@ describe("listConfiguredChannelIdsForReadOnlyScope", () => {
).toBe(false);
});
it("returns reason-rich policy entries for blocked ambient channel triggers", () => {
listPotentialConfiguredChannelIds.mockReturnValue(["demo-channel"]);
listPotentialConfiguredChannelPresenceSignals.mockReturnValue([
{ channelId: "demo-channel", source: "env" },
]);
expect(
resolveConfiguredChannelPresencePolicy({
config: {
plugins: {
allow: ["memory-core"],
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {
DEMO_CHANNEL_TOKEN: "token",
} as NodeJS.ProcessEnv,
includePersistedAuthState: false,
}),
).toEqual([
{
channelId: "demo-channel",
sources: ["env"],
effective: false,
pluginIds: [],
blockedReasons: ["not-in-allowlist"],
},
]);
});
it("keeps explicitly enabled bundled ambient channel triggers", () => {
listPotentialConfiguredChannelIds.mockReturnValue(["demo-channel"]);
listPotentialConfiguredChannelPresenceSignals.mockReturnValue([
{ channelId: "demo-channel", source: "env" },
]);
expect(
listConfiguredChannelIdsForReadOnlyScope({
@@ -726,8 +782,51 @@ describe("listConfiguredChannelIdsForReadOnlyScope", () => {
).toEqual(["demo-channel"]);
});
it("treats enabled-only channel config as explicit read-only intent", () => {
expect(
resolveConfiguredChannelPresencePolicy({
config: {
channels: {
"demo-channel": {
enabled: true,
},
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {},
includePersistedAuthState: false,
}),
).toEqual([
{
channelId: "demo-channel",
sources: ["explicit-config"],
effective: true,
pluginIds: ["demo-channel"],
blockedReasons: [],
},
]);
expect(
listConfiguredChannelIdsForReadOnlyScope({
config: {
channels: {
"demo-channel": {
enabled: true,
},
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {},
includePersistedAuthState: false,
}),
).toEqual(["demo-channel"]);
});
it("keeps explicitly configured bundled channels discovered from potential ids", () => {
listPotentialConfiguredChannelIds.mockReturnValue(["demo-channel"]);
listPotentialConfiguredChannelPresenceSignals.mockReturnValue([
{ channelId: "demo-channel", source: "config" },
]);
expect(
listConfiguredChannelIdsForReadOnlyScope({
@@ -747,6 +846,9 @@ describe("listConfiguredChannelIdsForReadOnlyScope", () => {
it("blocks explicitly configured bundled channels when plugins are disabled or denied", () => {
listPotentialConfiguredChannelIds.mockReturnValue(["demo-channel"]);
listPotentialConfiguredChannelPresenceSignals.mockReturnValue([
{ channelId: "demo-channel", source: "config" },
]);
expect(
listConfiguredChannelIdsForReadOnlyScope({
@@ -805,6 +907,10 @@ describe("listConfiguredChannelIdsForReadOnlyScope", () => {
it("uses effective read-only channel policy for announce channels", () => {
listPotentialConfiguredChannelIds.mockReturnValue(["demo-channel", "demo-other-channel"]);
listPotentialConfiguredChannelPresenceSignals.mockReturnValue([
{ channelId: "demo-channel", source: "env" },
{ channelId: "demo-other-channel", source: "config" },
]);
expect(
listConfiguredAnnounceChannelIdsForConfig({
@@ -948,6 +1054,7 @@ describe("listConfiguredChannelIdsForReadOnlyScope", () => {
it("uses manifest env vars for read-only channel presence checks", () => {
listPotentialConfiguredChannelIds.mockReturnValue([]);
listPotentialConfiguredChannelPresenceSignals.mockReturnValue([]);
hasPotentialConfiguredChannels.mockReturnValue(false);
expect(

View File

@@ -1,627 +1,19 @@
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
import { collectConfiguredAgentHarnessRuntimes } from "../agents/harness-runtimes.js";
import {
hasMeaningfulChannelConfig,
listPotentialConfiguredChannelIds,
} from "../channels/config-presence.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import {
resolveMemoryDreamingConfig,
resolveMemoryDreamingPluginConfig,
resolveMemoryDreamingPluginId,
} from "../memory-host-sdk/dreaming.js";
import { isSafeChannelEnvVarTriggerName } from "../secrets/channel-env-var-names.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
import { resolveManifestActivationPluginIds } from "./activation-planner.js";
import {
createPluginActivationSource,
normalizePluginId,
normalizePluginsConfig,
resolveEffectivePluginActivationState,
} from "./config-state.js";
import {
hasExplicitManifestOwnerTrust,
isActivatedManifestOwner,
isBundledManifestOwner,
passesManifestOwnerBasePolicy,
} from "./manifest-owner-policy.js";
import { loadPluginManifestRegistry, type PluginManifestRecord } from "./manifest-registry.js";
import { hasKind } from "./slots.js";
export {
hasConfiguredChannelsForReadOnlyScope,
hasExplicitChannelConfig,
listConfiguredAnnounceChannelIdsForConfig,
listConfiguredChannelIdsForReadOnlyScope,
listExplicitConfiguredChannelIdsForConfig,
resolveConfiguredChannelPluginIds,
resolveConfiguredChannelPresencePolicy,
resolveDiscoverableScopedChannelPluginIds,
type ConfiguredChannelBlockedReason,
type ConfiguredChannelPresencePolicyEntry,
type ConfiguredChannelPresenceSource,
} from "./channel-presence-policy.js";
function hasRuntimeContractSurface(plugin: PluginManifestRecord): boolean {
return Boolean(
plugin.providers.length > 0 ||
plugin.cliBackends.length > 0 ||
plugin.contracts?.speechProviders?.length ||
plugin.contracts?.mediaUnderstandingProviders?.length ||
plugin.contracts?.imageGenerationProviders?.length ||
plugin.contracts?.videoGenerationProviders?.length ||
plugin.contracts?.musicGenerationProviders?.length ||
plugin.contracts?.webFetchProviders?.length ||
plugin.contracts?.webSearchProviders?.length ||
plugin.contracts?.memoryEmbeddingProviders?.length ||
hasKind(plugin.kind, "memory"),
);
}
function isGatewayStartupMemoryPlugin(plugin: PluginManifestRecord): boolean {
return hasKind(plugin.kind, "memory");
}
function isGatewayStartupSidecar(plugin: PluginManifestRecord): boolean {
return plugin.channels.length === 0 && !hasRuntimeContractSurface(plugin);
}
function dedupeSortedPluginIds(values: Iterable<string>): string[] {
return [...new Set(values)].toSorted((left, right) => left.localeCompare(right));
}
function normalizeChannelIds(channelIds: Iterable<string>): string[] {
return Array.from(
new Set(
[...channelIds]
.map((channelId) => normalizeOptionalLowercaseString(channelId))
.filter((channelId): channelId is string => Boolean(channelId)),
),
).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;
}
const trimmed = key.trim();
const value = env[trimmed] ?? env[trimmed.toUpperCase()];
return typeof value === "string" && value.trim().length > 0;
}
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))) {
channelIds.add(channelId);
}
}
}
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;
workspaceDir?: string;
env: NodeJS.ProcessEnv;
cache?: boolean;
includePersistedAuthState?: boolean;
manifestRecords?: readonly PluginManifestRecord[];
}): string[] {
const records =
params.manifestRecords ??
loadPluginManifestRegistry({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
cache: params.cache,
}).plugins;
const channelIds = [
...new Set([
...listPotentialConfiguredChannelIds(params.config, params.env, {
includePersistedAuthState: params.includePersistedAuthState,
}),
...listEnvConfiguredManifestChannelIds({
records,
config: params.config,
activationSourceConfig: params.activationSourceConfig,
env: params.env,
}),
]),
];
return filterEffectiveConfiguredChannelIds({
channelIds,
records,
config: params.config,
activationSourceConfig: params.activationSourceConfig,
});
}
export function listConfiguredChannelIdsForReadOnlyScope(params: {
config: OpenClawConfig;
activationSourceConfig?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
cache?: boolean;
includePersistedAuthState?: boolean;
manifestRecords?: readonly PluginManifestRecord[];
}): string[] {
const env = params.env ?? process.env;
const workspaceDir =
params.workspaceDir ??
resolveAgentWorkspaceDir(params.config, resolveDefaultAgentId(params.config));
return listConfiguredChannelIdsForPluginScope({
config: params.config,
activationSourceConfig: params.activationSourceConfig,
workspaceDir,
env,
cache: params.cache,
includePersistedAuthState: params.includePersistedAuthState,
manifestRecords: params.manifestRecords,
});
}
export function hasConfiguredChannelsForReadOnlyScope(params: {
config: OpenClawConfig;
activationSourceConfig?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
cache?: boolean;
includePersistedAuthState?: boolean;
manifestRecords?: readonly PluginManifestRecord[];
}): boolean {
return (
listConfiguredChannelIdsForReadOnlyScope({
...params,
}).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>;
rootConfig: OpenClawConfig;
}): boolean {
if (
!passesManifestOwnerBasePolicy({
plugin: params.plugin,
normalizedConfig: params.normalizedConfig,
})
) {
return false;
}
if (isBundledManifestOwner(params.plugin)) {
return true;
}
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.rootConfig,
});
}
function resolveScopedChannelOwnerPluginIds(params: {
config: OpenClawConfig;
activationSourceConfig?: OpenClawConfig;
channelIds: readonly string[];
workspaceDir?: string;
env: NodeJS.ProcessEnv;
cache?: boolean;
}): string[] {
const channelIds = normalizeChannelIds(params.channelIds);
if (channelIds.length === 0) {
return [];
}
const registry = loadPluginManifestRegistry({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
cache: params.cache,
});
const trustConfig = params.activationSourceConfig ?? params.config;
const normalizedConfig = normalizePluginsConfig(trustConfig.plugins);
const candidateIds = dedupeSortedPluginIds(
channelIds.flatMap((channelId) => {
return resolveManifestActivationPluginIds({
trigger: {
kind: "channel",
channel: channelId,
},
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
cache: params.cache,
});
}),
);
if (candidateIds.length === 0) {
return [];
}
const candidateIdSet = new Set(candidateIds);
return registry.plugins
.filter((plugin) => {
if (!candidateIdSet.has(plugin.id)) {
return false;
}
return isChannelPluginEligibleForScopedOwnership({
plugin,
normalizedConfig,
rootConfig: trustConfig,
});
})
.map((plugin) => plugin.id)
.toSorted((left, right) => left.localeCompare(right));
}
function resolveScopedChannelPluginIds(params: {
config: OpenClawConfig;
activationSourceConfig?: OpenClawConfig;
channelIds: readonly string[];
workspaceDir?: string;
env: NodeJS.ProcessEnv;
cache?: boolean;
}): string[] {
return resolveScopedChannelOwnerPluginIds(params);
}
export function resolveDiscoverableScopedChannelPluginIds(params: {
config: OpenClawConfig;
activationSourceConfig?: OpenClawConfig;
channelIds: readonly string[];
workspaceDir?: string;
env: NodeJS.ProcessEnv;
cache?: boolean;
}): string[] {
return resolveScopedChannelOwnerPluginIds(params);
}
function resolveGatewayStartupDreamingPluginIds(config: OpenClawConfig): Set<string> {
const dreamingConfig = resolveMemoryDreamingConfig({
pluginConfig: resolveMemoryDreamingPluginConfig(config),
cfg: config,
});
if (!dreamingConfig.enabled) {
return new Set();
}
return new Set(["memory-core", resolveMemoryDreamingPluginId(config)]);
}
function resolveExplicitMemorySlotStartupPluginId(config: OpenClawConfig): string | undefined {
const configuredSlot = config.plugins?.slots?.memory?.trim();
if (!configuredSlot || configuredSlot.toLowerCase() === "none") {
return undefined;
}
return normalizePluginId(configuredSlot);
}
function shouldConsiderForGatewayStartup(params: {
plugin: PluginManifestRecord;
startupDreamingPluginIds: ReadonlySet<string>;
explicitMemorySlotStartupPluginId?: string;
}): boolean {
if (isGatewayStartupSidecar(params.plugin)) {
return true;
}
if (!isGatewayStartupMemoryPlugin(params.plugin)) {
return false;
}
if (params.startupDreamingPluginIds.has(params.plugin.id)) {
return true;
}
return params.explicitMemorySlotStartupPluginId === params.plugin.id;
}
export function resolveChannelPluginIds(params: {
config: OpenClawConfig;
workspaceDir?: string;
env: NodeJS.ProcessEnv;
}): string[] {
return loadPluginManifestRegistry({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
})
.plugins.filter((plugin) => plugin.channels.length > 0)
.map((plugin) => plugin.id);
}
export function resolveConfiguredChannelPluginIds(params: {
config: OpenClawConfig;
activationSourceConfig?: OpenClawConfig;
workspaceDir?: string;
env: NodeJS.ProcessEnv;
}): string[] {
const configuredChannelIds = new Set(
listConfiguredChannelIdsForPluginScope({
config: params.config,
activationSourceConfig: params.activationSourceConfig,
workspaceDir: params.workspaceDir,
env: params.env,
}).map((id) => id.trim()),
);
if (configuredChannelIds.size === 0) {
return [];
}
return resolveScopedChannelPluginIds({
...params,
channelIds: [...configuredChannelIds],
});
}
export function resolveConfiguredDeferredChannelPluginIds(params: {
config: OpenClawConfig;
workspaceDir?: string;
env: NodeJS.ProcessEnv;
}): string[] {
const configuredChannelIds = new Set(
listPotentialConfiguredChannelIds(params.config, params.env).map((id) => id.trim()),
);
if (configuredChannelIds.size === 0) {
return [];
}
return loadPluginManifestRegistry({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
})
.plugins.filter(
(plugin) =>
plugin.channels.some((channelId) => configuredChannelIds.has(channelId)) &&
plugin.startupDeferConfiguredChannelFullLoadUntilAfterListen === true,
)
.map((plugin) => plugin.id);
}
export function resolveGatewayStartupPluginIds(params: {
config: OpenClawConfig;
activationSourceConfig?: OpenClawConfig;
workspaceDir?: string;
env: NodeJS.ProcessEnv;
}): string[] {
const configuredChannelIds = new Set(
listPotentialConfiguredChannelIds(params.config, params.env).map((id) => id.trim()),
);
const pluginsConfig = normalizePluginsConfig(params.config.plugins);
// Startup must classify allowlist exceptions against the raw config snapshot,
// not the auto-enabled effective snapshot, or configured-only channels can be
// misclassified as explicit enablement.
const activationSource = createPluginActivationSource({
config: params.activationSourceConfig ?? params.config,
});
const requiredAgentHarnessPluginIds = new Set(
collectConfiguredAgentHarnessRuntimes(
params.activationSourceConfig ?? params.config,
params.env,
).flatMap((runtime) =>
resolveManifestActivationPluginIds({
trigger: {
kind: "agentHarness",
runtime,
},
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
cache: true,
}),
),
);
const startupDreamingPluginIds = resolveGatewayStartupDreamingPluginIds(params.config);
const explicitMemorySlotStartupPluginId = resolveExplicitMemorySlotStartupPluginId(
params.activationSourceConfig ?? params.config,
);
return loadPluginManifestRegistry({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
})
.plugins.filter((plugin) => {
if (plugin.channels.some((channelId) => configuredChannelIds.has(channelId))) {
return true;
}
if (requiredAgentHarnessPluginIds.has(plugin.id)) {
const activationState = resolveEffectivePluginActivationState({
id: plugin.id,
origin: plugin.origin,
config: pluginsConfig,
rootConfig: params.config,
enabledByDefault: plugin.enabledByDefault,
activationSource,
});
return activationState.enabled;
}
if (
!shouldConsiderForGatewayStartup({
plugin,
startupDreamingPluginIds,
explicitMemorySlotStartupPluginId,
})
) {
return false;
}
const activationState = resolveEffectivePluginActivationState({
id: plugin.id,
origin: plugin.origin,
config: pluginsConfig,
rootConfig: params.config,
enabledByDefault: plugin.enabledByDefault,
activationSource,
});
if (!activationState.enabled) {
return false;
}
if (plugin.origin !== "bundled") {
return activationState.explicitlyEnabled;
}
return activationState.source === "explicit" || activationState.source === "default";
})
.map((plugin) => plugin.id);
}
export {
resolveChannelPluginIds,
resolveConfiguredDeferredChannelPluginIds,
resolveGatewayStartupPluginIds,
} from "./gateway-startup-plugin-ids.js";

View File

@@ -0,0 +1,525 @@
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
import {
hasMeaningfulChannelConfig,
listPotentialConfiguredChannelPresenceSignals,
type ChannelPresenceSignalSource,
} from "../channels/config-presence.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { isSafeChannelEnvVarTriggerName } from "../secrets/channel-env-var-names.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
import { resolveManifestActivationPluginIds } from "./activation-planner.js";
import {
createPluginActivationSource,
normalizePluginsConfig,
resolveEffectivePluginActivationState,
} from "./config-state.js";
import {
hasExplicitManifestOwnerTrust,
isActivatedManifestOwner,
isBundledManifestOwner,
passesManifestOwnerBasePolicy,
} from "./manifest-owner-policy.js";
import { loadPluginManifestRegistry, type PluginManifestRecord } from "./manifest-registry.js";
const IGNORED_CHANNEL_CONFIG_KEYS = new Set(["defaults", "modelByChannel"]);
export type ConfiguredChannelPresenceSource =
| "explicit-config"
| Exclude<ChannelPresenceSignalSource, "config">
| "manifest-env";
export type ConfiguredChannelBlockedReason =
| "plugins-disabled"
| "blocked-by-denylist"
| "plugin-disabled"
| "not-in-allowlist"
| "workspace-disabled-by-default"
| "bundled-disabled-by-default"
| "untrusted-plugin"
| "no-channel-owner"
| "not-activated";
export type ConfiguredChannelPresencePolicyEntry = {
channelId: string;
sources: ConfiguredChannelPresenceSource[];
effective: boolean;
pluginIds: string[];
blockedReasons: ConfiguredChannelBlockedReason[];
};
function dedupeSortedPluginIds(values: Iterable<string>): string[] {
return [...new Set(values)].toSorted((left, right) => left.localeCompare(right));
}
function normalizeChannelIds(channelIds: Iterable<string>): string[] {
return Array.from(
new Set(
[...channelIds]
.map((channelId) => normalizeOptionalLowercaseString(channelId))
.filter((channelId): channelId is string => Boolean(channelId)),
),
).toSorted((left, right) => left.localeCompare(right));
}
function hasNonEmptyEnvValue(env: NodeJS.ProcessEnv, key: string): boolean {
if (!isSafeChannelEnvVarTriggerName(key)) {
return false;
}
const trimmed = key.trim();
const value = env[trimmed] ?? env[trimmed.toUpperCase()];
return typeof value === "string" && value.trim().length > 0;
}
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 listManifestEnvConfiguredChannelSignals(params: {
records: readonly PluginManifestRecord[];
activationSourceConfig?: OpenClawConfig;
config: OpenClawConfig;
env: NodeJS.ProcessEnv;
}): Array<{ channelId: string; source: "manifest-env" }> {
const signals: Array<{ channelId: string; source: "manifest-env" }> = [];
const seen = 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))) {
continue;
}
if (seen.has(channelId)) {
continue;
}
seen.add(channelId);
signals.push({ channelId, source: "manifest-env" });
}
}
return signals.toSorted((left, right) => left.channelId.localeCompare(right.channelId));
}
function normalizeActivationBlockedReason(reason?: string): ConfiguredChannelBlockedReason {
switch (reason) {
case "plugins disabled":
return "plugins-disabled";
case "blocked by denylist":
return "blocked-by-denylist";
case "disabled in config":
return "plugin-disabled";
case "not in allowlist":
return "not-in-allowlist";
case "workspace plugin (disabled by default)":
return "workspace-disabled-by-default";
case "bundled (disabled by default)":
return "bundled-disabled-by-default";
default:
return "not-activated";
}
}
function resolveBasePolicyBlockedReason(params: {
plugin: Pick<PluginManifestRecord, "id">;
normalizedConfig: ReturnType<typeof normalizePluginsConfig>;
}): ConfiguredChannelBlockedReason | null {
if (!params.normalizedConfig.enabled) {
return "plugins-disabled";
}
if (params.normalizedConfig.deny.includes(params.plugin.id)) {
return "blocked-by-denylist";
}
if (params.normalizedConfig.entries[params.plugin.id]?.enabled === false) {
return "plugin-disabled";
}
if (
params.normalizedConfig.allow.length > 0 &&
!params.normalizedConfig.allow.includes(params.plugin.id)
) {
return "not-in-allowlist";
}
return null;
}
function isChannelPluginEligibleForScopedOwnership(params: {
plugin: PluginManifestRecord;
normalizedConfig: ReturnType<typeof normalizePluginsConfig>;
rootConfig: OpenClawConfig;
}): boolean {
if (
!passesManifestOwnerBasePolicy({
plugin: params.plugin,
normalizedConfig: params.normalizedConfig,
})
) {
return false;
}
if (isBundledManifestOwner(params.plugin)) {
return true;
}
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.rootConfig,
});
}
function evaluateEffectiveChannelPlugin(params: {
plugin: PluginManifestRecord;
channelId: string;
normalizedConfig: ReturnType<typeof normalizePluginsConfig>;
config: OpenClawConfig;
activationSource: ReturnType<typeof createPluginActivationSource>;
}): { effective: boolean; pluginId: string; blockedReason?: ConfiguredChannelBlockedReason } {
const baseBlockedReason = resolveBasePolicyBlockedReason({
plugin: params.plugin,
normalizedConfig: params.normalizedConfig,
});
if (baseBlockedReason) {
return {
effective: false,
pluginId: params.plugin.id,
blockedReason: baseBlockedReason,
};
}
if (!isBundledManifestOwner(params.plugin)) {
if (params.plugin.origin === "global" || params.plugin.origin === "config") {
const trusted = hasExplicitManifestOwnerTrust({
plugin: params.plugin,
normalizedConfig: params.normalizedConfig,
});
return trusted
? { effective: true, pluginId: params.plugin.id }
: {
effective: false,
pluginId: params.plugin.id,
blockedReason: "untrusted-plugin",
};
}
const activated = isActivatedManifestOwner({
plugin: params.plugin,
normalizedConfig: params.normalizedConfig,
rootConfig: params.activationSource.rootConfig,
});
return activated
? { effective: true, pluginId: params.plugin.id }
: {
effective: false,
pluginId: params.plugin.id,
blockedReason: "untrusted-plugin",
};
}
if (
hasExplicitChannelConfig({
config: params.activationSource.rootConfig ?? params.config,
channelId: params.channelId,
})
) {
return { effective: true, pluginId: params.plugin.id };
}
const activationState = resolveEffectivePluginActivationState({
id: params.plugin.id,
origin: params.plugin.origin,
config: params.normalizedConfig,
rootConfig: params.config,
enabledByDefault: params.plugin.enabledByDefault,
activationSource: params.activationSource,
});
return activationState.enabled
? { effective: true, pluginId: params.plugin.id }
: {
effective: false,
pluginId: params.plugin.id,
blockedReason: normalizeActivationBlockedReason(activationState.reason),
};
}
function addPolicySignal(
entries: Map<string, Set<ConfiguredChannelPresenceSource>>,
channelId: string,
source: ConfiguredChannelPresenceSource,
) {
const normalized = normalizeOptionalLowercaseString(channelId);
if (!normalized) {
return;
}
let sources = entries.get(normalized);
if (!sources) {
sources = new Set();
entries.set(normalized, sources);
}
sources.add(source);
}
export function resolveConfiguredChannelPresencePolicy(params: {
config: OpenClawConfig;
activationSourceConfig?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
cache?: boolean;
includePersistedAuthState?: boolean;
manifestRecords?: readonly PluginManifestRecord[];
}): ConfiguredChannelPresencePolicyEntry[] {
const env = params.env ?? process.env;
const workspaceDir =
params.workspaceDir ??
resolveAgentWorkspaceDir(params.config, resolveDefaultAgentId(params.config));
const records =
params.manifestRecords ??
loadPluginManifestRegistry({
config: params.config,
workspaceDir,
env,
cache: params.cache,
}).plugins;
const entrySources = new Map<string, Set<ConfiguredChannelPresenceSource>>();
for (const channelId of listExplicitConfiguredChannelIdsForConfig(params.config)) {
addPolicySignal(entrySources, channelId, "explicit-config");
}
for (const signal of listPotentialConfiguredChannelPresenceSignals(params.config, env, {
includePersistedAuthState: params.includePersistedAuthState,
})) {
if (signal.source === "config") {
continue;
}
addPolicySignal(entrySources, signal.channelId, signal.source);
}
for (const signal of listManifestEnvConfiguredChannelSignals({
records,
config: params.config,
activationSourceConfig: params.activationSourceConfig,
env,
})) {
addPolicySignal(entrySources, signal.channelId, signal.source);
}
const activationSource = createPluginActivationSource({
config: params.activationSourceConfig ?? params.config,
});
const normalizedConfig = activationSource.plugins;
const entries: ConfiguredChannelPresencePolicyEntry[] = [];
for (const channelId of normalizeChannelIds(entrySources.keys())) {
const owningRecords = records.filter((record) => recordOwnsChannel(record, channelId));
const evaluations = owningRecords.map((plugin) =>
evaluateEffectiveChannelPlugin({
plugin,
channelId,
normalizedConfig,
config: params.config,
activationSource,
}),
);
const effectivePluginIds = evaluations
.filter((entry) => entry.effective)
.map((entry) => entry.pluginId);
const blockedReasons =
owningRecords.length === 0
? ["no-channel-owner" as const]
: [
...new Set(
evaluations
.map((entry) => entry.blockedReason)
.filter((reason): reason is ConfiguredChannelBlockedReason => Boolean(reason)),
),
].toSorted((left, right) => left.localeCompare(right));
entries.push({
channelId,
sources: [...(entrySources.get(channelId) ?? [])].toSorted((left, right) =>
left.localeCompare(right),
),
effective: effectivePluginIds.length > 0,
pluginIds: dedupeSortedPluginIds(effectivePluginIds),
blockedReasons,
});
}
return entries;
}
export function listConfiguredChannelIdsForReadOnlyScope(
params: Parameters<typeof resolveConfiguredChannelPresencePolicy>[0],
): string[] {
return resolveConfiguredChannelPresencePolicy(params)
.filter((entry) => entry.effective)
.map((entry) => entry.channelId);
}
export function hasConfiguredChannelsForReadOnlyScope(
params: Parameters<typeof resolveConfiguredChannelPresencePolicy>[0],
): boolean {
return listConfiguredChannelIdsForReadOnlyScope(params).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 resolveScopedChannelOwnerPluginIds(params: {
config: OpenClawConfig;
activationSourceConfig?: OpenClawConfig;
channelIds: readonly string[];
workspaceDir?: string;
env: NodeJS.ProcessEnv;
cache?: boolean;
}): string[] {
const channelIds = normalizeChannelIds(params.channelIds);
if (channelIds.length === 0) {
return [];
}
const registry = loadPluginManifestRegistry({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
cache: params.cache,
});
const trustConfig = params.activationSourceConfig ?? params.config;
const normalizedConfig = normalizePluginsConfig(trustConfig.plugins);
const candidateIds = dedupeSortedPluginIds(
channelIds.flatMap((channelId) => {
return resolveManifestActivationPluginIds({
trigger: {
kind: "channel",
channel: channelId,
},
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
cache: params.cache,
});
}),
);
if (candidateIds.length === 0) {
return [];
}
const candidateIdSet = new Set(candidateIds);
return registry.plugins
.filter((plugin) => {
if (!candidateIdSet.has(plugin.id)) {
return false;
}
return isChannelPluginEligibleForScopedOwnership({
plugin,
normalizedConfig,
rootConfig: trustConfig,
});
})
.map((plugin) => plugin.id)
.toSorted((left, right) => left.localeCompare(right));
}
export function resolveDiscoverableScopedChannelPluginIds(params: {
config: OpenClawConfig;
activationSourceConfig?: OpenClawConfig;
channelIds: readonly string[];
workspaceDir?: string;
env: NodeJS.ProcessEnv;
cache?: boolean;
}): string[] {
return resolveScopedChannelOwnerPluginIds(params);
}
export function resolveConfiguredChannelPluginIds(params: {
config: OpenClawConfig;
activationSourceConfig?: OpenClawConfig;
workspaceDir?: string;
env: NodeJS.ProcessEnv;
}): string[] {
const configuredChannelIds = listConfiguredChannelIdsForReadOnlyScope({
config: params.config,
activationSourceConfig: params.activationSourceConfig,
workspaceDir: params.workspaceDir,
env: params.env,
});
if (configuredChannelIds.length === 0) {
return [];
}
return resolveScopedChannelOwnerPluginIds({
...params,
channelIds: configuredChannelIds,
});
}

View File

@@ -0,0 +1,200 @@
import { collectConfiguredAgentHarnessRuntimes } from "../agents/harness-runtimes.js";
import { listPotentialConfiguredChannelIds } from "../channels/config-presence.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import {
resolveMemoryDreamingConfig,
resolveMemoryDreamingPluginConfig,
resolveMemoryDreamingPluginId,
} from "../memory-host-sdk/dreaming.js";
import { resolveManifestActivationPluginIds } from "./activation-planner.js";
import {
createPluginActivationSource,
normalizePluginId,
normalizePluginsConfig,
resolveEffectivePluginActivationState,
} from "./config-state.js";
import { loadPluginManifestRegistry, type PluginManifestRecord } from "./manifest-registry.js";
import { hasKind } from "./slots.js";
function hasRuntimeContractSurface(plugin: PluginManifestRecord): boolean {
return Boolean(
plugin.providers.length > 0 ||
plugin.cliBackends.length > 0 ||
plugin.contracts?.speechProviders?.length ||
plugin.contracts?.mediaUnderstandingProviders?.length ||
plugin.contracts?.imageGenerationProviders?.length ||
plugin.contracts?.videoGenerationProviders?.length ||
plugin.contracts?.musicGenerationProviders?.length ||
plugin.contracts?.webFetchProviders?.length ||
plugin.contracts?.webSearchProviders?.length ||
plugin.contracts?.memoryEmbeddingProviders?.length ||
hasKind(plugin.kind, "memory"),
);
}
function isGatewayStartupMemoryPlugin(plugin: PluginManifestRecord): boolean {
return hasKind(plugin.kind, "memory");
}
function isGatewayStartupSidecar(plugin: PluginManifestRecord): boolean {
return plugin.channels.length === 0 && !hasRuntimeContractSurface(plugin);
}
function resolveGatewayStartupDreamingPluginIds(config: OpenClawConfig): Set<string> {
const dreamingConfig = resolveMemoryDreamingConfig({
pluginConfig: resolveMemoryDreamingPluginConfig(config),
cfg: config,
});
if (!dreamingConfig.enabled) {
return new Set();
}
return new Set(["memory-core", resolveMemoryDreamingPluginId(config)]);
}
function resolveExplicitMemorySlotStartupPluginId(config: OpenClawConfig): string | undefined {
const configuredSlot = config.plugins?.slots?.memory?.trim();
if (!configuredSlot || configuredSlot.toLowerCase() === "none") {
return undefined;
}
return normalizePluginId(configuredSlot);
}
function shouldConsiderForGatewayStartup(params: {
plugin: PluginManifestRecord;
startupDreamingPluginIds: ReadonlySet<string>;
explicitMemorySlotStartupPluginId?: string;
}): boolean {
if (isGatewayStartupSidecar(params.plugin)) {
return true;
}
if (!isGatewayStartupMemoryPlugin(params.plugin)) {
return false;
}
if (params.startupDreamingPluginIds.has(params.plugin.id)) {
return true;
}
return params.explicitMemorySlotStartupPluginId === params.plugin.id;
}
export function resolveChannelPluginIds(params: {
config: OpenClawConfig;
workspaceDir?: string;
env: NodeJS.ProcessEnv;
}): string[] {
return loadPluginManifestRegistry({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
})
.plugins.filter((plugin) => plugin.channels.length > 0)
.map((plugin) => plugin.id);
}
export function resolveConfiguredDeferredChannelPluginIds(params: {
config: OpenClawConfig;
workspaceDir?: string;
env: NodeJS.ProcessEnv;
}): string[] {
const configuredChannelIds = new Set(
listPotentialConfiguredChannelIds(params.config, params.env).map((id) => id.trim()),
);
if (configuredChannelIds.size === 0) {
return [];
}
return loadPluginManifestRegistry({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
})
.plugins.filter(
(plugin) =>
plugin.channels.some((channelId) => configuredChannelIds.has(channelId)) &&
plugin.startupDeferConfiguredChannelFullLoadUntilAfterListen === true,
)
.map((plugin) => plugin.id);
}
export function resolveGatewayStartupPluginIds(params: {
config: OpenClawConfig;
activationSourceConfig?: OpenClawConfig;
workspaceDir?: string;
env: NodeJS.ProcessEnv;
}): string[] {
const configuredChannelIds = new Set(
listPotentialConfiguredChannelIds(params.config, params.env).map((id) => id.trim()),
);
const pluginsConfig = normalizePluginsConfig(params.config.plugins);
// Startup must classify allowlist exceptions against the raw config snapshot,
// not the auto-enabled effective snapshot, or configured-only channels can be
// misclassified as explicit enablement.
const activationSource = createPluginActivationSource({
config: params.activationSourceConfig ?? params.config,
});
const requiredAgentHarnessPluginIds = new Set(
collectConfiguredAgentHarnessRuntimes(
params.activationSourceConfig ?? params.config,
params.env,
).flatMap((runtime) =>
resolveManifestActivationPluginIds({
trigger: {
kind: "agentHarness",
runtime,
},
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
cache: true,
}),
),
);
const startupDreamingPluginIds = resolveGatewayStartupDreamingPluginIds(params.config);
const explicitMemorySlotStartupPluginId = resolveExplicitMemorySlotStartupPluginId(
params.activationSourceConfig ?? params.config,
);
return loadPluginManifestRegistry({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
})
.plugins.filter((plugin) => {
if (plugin.channels.some((channelId) => configuredChannelIds.has(channelId))) {
return true;
}
if (requiredAgentHarnessPluginIds.has(plugin.id)) {
const activationState = resolveEffectivePluginActivationState({
id: plugin.id,
origin: plugin.origin,
config: pluginsConfig,
rootConfig: params.config,
enabledByDefault: plugin.enabledByDefault,
activationSource,
});
return activationState.enabled;
}
if (
!shouldConsiderForGatewayStartup({
plugin,
startupDreamingPluginIds,
explicitMemorySlotStartupPluginId,
})
) {
return false;
}
const activationState = resolveEffectivePluginActivationState({
id: plugin.id,
origin: plugin.origin,
config: pluginsConfig,
rootConfig: params.config,
enabledByDefault: plugin.enabledByDefault,
activationSource,
});
if (!activationState.enabled) {
return false;
}
if (plugin.origin !== "bundled") {
return activationState.explicitlyEnabled;
}
return activationState.source === "explicit" || activationState.source === "default";
})
.map((plugin) => plugin.id);
}