mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 12:40:43 +00:00
fix: tighten channel presence triage
This commit is contained in:
@@ -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,
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user