fix: tighten channel presence triage

This commit is contained in:
Gustavo Madeira Santana
2026-04-21 20:00:16 -04:00
parent 9b7bbd2662
commit e1745cd621
3 changed files with 170 additions and 32 deletions

View File

@@ -290,18 +290,10 @@ function resolveReadOnlyWorkspaceDir(
return options.workspaceDir ?? resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg));
}
function listExternalChannelManifestRecords(params: {
cfg: OpenClawConfig;
workspaceDir?: string;
env: NodeJS.ProcessEnv;
cache?: boolean;
}): PluginManifestRecord[] {
return loadPluginManifestRegistry({
config: params.cfg,
workspaceDir: params.workspaceDir,
env: params.env,
cache: params.cache,
}).plugins.filter((plugin) => plugin.origin !== "bundled" && plugin.channels.length > 0);
function listExternalChannelManifestRecords(
records: readonly PluginManifestRecord[],
): PluginManifestRecord[] {
return records.filter((plugin) => plugin.origin !== "bundled" && plugin.channels.length > 0);
}
function resolveExternalReadOnlyChannelPluginIds(params: {
@@ -353,12 +345,13 @@ export function resolveReadOnlyChannelPluginsForConfig(
): ReadOnlyChannelPluginResolution {
const env = options.env ?? process.env;
const workspaceDir = resolveReadOnlyWorkspaceDir(cfg, options);
const externalManifestRecords = listExternalChannelManifestRecords({
cfg,
const manifestRecords = loadPluginManifestRegistry({
config: cfg,
workspaceDir,
env,
cache: options.cache,
});
}).plugins;
const externalManifestRecords = listExternalChannelManifestRecords(manifestRecords);
const configuredChannelIds = [
...new Set(
listConfiguredChannelIdsForReadOnlyScope({
@@ -368,6 +361,7 @@ export function resolveReadOnlyChannelPluginsForConfig(
env,
cache: options.cache,
includePersistedAuthState: options.includePersistedAuthState,
manifestRecords,
}),
),
];

View File

@@ -822,6 +822,73 @@ describe("listConfiguredChannelIdsForReadOnlyScope", () => {
).toEqual(["demo-channel"]);
});
it("does not treat disabled stale channel config as explicit read-only intent", () => {
const config = {
channels: {
"demo-channel": {
enabled: false,
token: "stale-token",
},
},
} as OpenClawConfig;
expect(listExplicitConfiguredChannelIdsForConfig(config)).toEqual([]);
expect(
resolveConfiguredChannelPresencePolicy({
config,
workspaceDir: "/tmp",
env: {},
includePersistedAuthState: false,
}),
).toEqual([]);
expect(
listConfiguredChannelIdsForReadOnlyScope({
config,
workspaceDir: "/tmp",
env: {},
includePersistedAuthState: false,
}),
).toEqual([]);
});
it("lets explicit bundled channel config bypass restrictive allowlists", () => {
const config = {
channels: {
"demo-channel": {
token: "configured",
},
},
plugins: {
allow: ["browser"],
},
} as OpenClawConfig;
expect(
resolveConfiguredChannelPresencePolicy({
config,
workspaceDir: "/tmp",
env: {},
includePersistedAuthState: false,
}),
).toEqual([
{
channelId: "demo-channel",
sources: ["explicit-config"],
effective: true,
pluginIds: ["demo-channel"],
blockedReasons: [],
},
]);
expect(
listConfiguredChannelIdsForReadOnlyScope({
config,
workspaceDir: "/tmp",
env: {},
includePersistedAuthState: false,
}),
).toEqual(["demo-channel"]);
});
it("keeps explicitly configured bundled channels discovered from potential ids", () => {
listPotentialConfiguredChannelIds.mockReturnValue(["demo-channel"]);
listPotentialConfiguredChannelPresenceSignals.mockReturnValue([
@@ -905,6 +972,37 @@ describe("listConfiguredChannelIdsForReadOnlyScope", () => {
).toEqual(["demo-channel"]);
});
it("does not let disabled mixed-case channel config announce ambient matches", () => {
listPotentialConfiguredChannelIds.mockReturnValue(["demo-channel"]);
listPotentialConfiguredChannelPresenceSignals.mockReturnValue([
{ channelId: "demo-channel", source: "env" },
]);
expect(
listConfiguredAnnounceChannelIdsForConfig({
config: {
channels: {
"Demo-Channel": {
enabled: false,
token: "stale-token",
},
},
plugins: {
entries: {
"demo-channel": {
enabled: true,
},
},
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {
DEMO_CHANNEL_TOKEN: "ambient",
} as NodeJS.ProcessEnv,
}),
).toEqual([]);
});
it("uses effective read-only channel policy for announce channels", () => {
listPotentialConfiguredChannelIds.mockReturnValue(["demo-channel", "demo-other-channel"]);
listPotentialConfiguredChannelPresenceSignals.mockReturnValue([
@@ -932,6 +1030,40 @@ describe("listConfiguredChannelIdsForReadOnlyScope", () => {
).toEqual(["demo-other-channel"]);
});
it("does not treat activation-only declarations as channel ownership", () => {
listPotentialConfiguredChannelIds.mockReturnValue(["activation-only-channel"]);
listPotentialConfiguredChannelPresenceSignals.mockReturnValue([
{ channelId: "activation-only-channel", source: "env" },
]);
expect(
resolveConfiguredChannelPresencePolicy({
config: {
plugins: {
entries: {
"activation-only-channel-plugin": {
enabled: true,
},
},
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {
ACTIVATION_ONLY_CHANNEL_TOKEN: "ambient",
} as NodeJS.ProcessEnv,
includePersistedAuthState: false,
}),
).toEqual([
{
channelId: "activation-only-channel",
sources: ["env"],
effective: false,
pluginIds: [],
blockedReasons: ["no-channel-owner"],
},
]);
});
it("uses manifest env vars as read-only configured channel triggers", () => {
expect(
listConfiguredChannelIdsForReadOnlyScope({

View File

@@ -82,7 +82,11 @@ export function hasExplicitChannelConfig(params: {
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
return false;
}
return (entry as { enabled?: unknown }).enabled === true || hasMeaningfulChannelConfig(entry);
const enabled = (entry as { enabled?: unknown }).enabled;
if (enabled === false) {
return false;
}
return enabled === true || hasMeaningfulChannelConfig(entry);
}
export function listExplicitConfiguredChannelIdsForConfig(config: OpenClawConfig): string[] {
@@ -99,12 +103,12 @@ export function listExplicitConfiguredChannelIdsForConfig(config: OpenClawConfig
.toSorted((left, right) => left.localeCompare(right));
}
function recordOwnsChannel(record: PluginManifestRecord, channelId: string): boolean {
function recordDeclaresChannel(record: PluginManifestRecord, channelId: string): boolean {
const normalizedChannelId = normalizeOptionalLowercaseString(channelId) ?? "";
if (!normalizedChannelId) {
return false;
}
return [...record.channels, ...(record.activation?.onChannels ?? [])].some(
return record.channels.some(
(ownedChannelId) =>
(normalizeOptionalLowercaseString(ownedChannelId) ?? "") === normalizedChannelId,
);
@@ -167,6 +171,7 @@ function normalizeActivationBlockedReason(reason?: string): ConfiguredChannelBlo
function resolveBasePolicyBlockedReason(params: {
plugin: Pick<PluginManifestRecord, "id">;
normalizedConfig: ReturnType<typeof normalizePluginsConfig>;
allowRestrictiveAllowlistBypass?: boolean;
}): ConfiguredChannelBlockedReason | null {
if (!params.normalizedConfig.enabled) {
return "plugins-disabled";
@@ -178,6 +183,7 @@ function resolveBasePolicyBlockedReason(params: {
return "plugin-disabled";
}
if (
params.allowRestrictiveAllowlistBypass !== true &&
params.normalizedConfig.allow.length > 0 &&
!params.normalizedConfig.allow.includes(params.plugin.id)
) {
@@ -222,9 +228,16 @@ function evaluateEffectiveChannelPlugin(params: {
config: OpenClawConfig;
activationSource: ReturnType<typeof createPluginActivationSource>;
}): { effective: boolean; pluginId: string; blockedReason?: ConfiguredChannelBlockedReason } {
const explicitBundledChannelConfig =
isBundledManifestOwner(params.plugin) &&
hasExplicitChannelConfig({
config: params.activationSource.rootConfig ?? params.config,
channelId: params.channelId,
});
const baseBlockedReason = resolveBasePolicyBlockedReason({
plugin: params.plugin,
normalizedConfig: params.normalizedConfig,
allowRestrictiveAllowlistBypass: explicitBundledChannelConfig,
});
if (baseBlockedReason) {
return {
@@ -262,12 +275,7 @@ function evaluateEffectiveChannelPlugin(params: {
};
}
if (
hasExplicitChannelConfig({
config: params.activationSource.rootConfig ?? params.config,
channelId: params.channelId,
})
) {
if (explicitBundledChannelConfig) {
return { effective: true, pluginId: params.plugin.id };
}
@@ -354,7 +362,7 @@ export function resolveConfiguredChannelPresencePolicy(params: {
const normalizedConfig = activationSource.plugins;
const entries: ConfiguredChannelPresencePolicyEntry[] = [];
for (const channelId of normalizeChannelIds(entrySources.keys())) {
const owningRecords = records.filter((record) => recordOwnsChannel(record, channelId));
const owningRecords = records.filter((record) => recordDeclaresChannel(record, channelId));
const evaluations = owningRecords.map((plugin) =>
evaluateEffectiveChannelPlugin({
plugin,
@@ -423,7 +431,8 @@ export function listConfiguredAnnounceChannelIdsForConfig(params: {
(value as { enabled?: unknown }).enabled === false
);
})
.map(([channelId]) => channelId)
.map(([channelId]) => normalizeOptionalLowercaseString(channelId))
.filter((channelId): channelId is string => Boolean(channelId))
: [],
);
return normalizeChannelIds([
@@ -509,12 +518,15 @@ export function resolveConfiguredChannelPluginIds(params: {
workspaceDir?: string;
env: NodeJS.ProcessEnv;
}): string[] {
const configuredChannelIds = listConfiguredChannelIdsForReadOnlyScope({
config: params.config,
activationSourceConfig: params.activationSourceConfig,
workspaceDir: params.workspaceDir,
env: params.env,
});
const configuredChannelIds = normalizeChannelIds([
...listConfiguredChannelIdsForReadOnlyScope({
config: params.config,
activationSourceConfig: params.activationSourceConfig,
workspaceDir: params.workspaceDir,
env: params.env,
}),
...listExplicitConfiguredChannelIdsForConfig(params.activationSourceConfig ?? params.config),
]);
if (configuredChannelIds.length === 0) {
return [];
}