mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:30:43 +00:00
fix(cli): avoid plugin preload for agent bindings
This commit is contained in:
@@ -61,6 +61,9 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- CLI/agents: keep `agents bind`, `agents unbind`, and `agents bindings` on
|
||||
setup-safe channel metadata paths so they do not preload bundled plugin
|
||||
runtimes or stage runtime dependencies. Fixes #71743.
|
||||
- Plugins/registry: preserve explicit disabled plugin records during registry migration without persisting every unused bundled plugin discovered on disk. Thanks @shakkernerd.
|
||||
- Windows/native: keep CLI startup and bundled provider plugin loading off
|
||||
Windows ESM raw-path failure paths, fixing native onboarding/install smoke on
|
||||
|
||||
@@ -41,6 +41,21 @@ export const cliCommandCatalog: readonly CliCommandCatalogEntry[] = [
|
||||
{ commandPath: ["channels"], policy: { loadPlugins: "always" } },
|
||||
{ commandPath: ["directory"], policy: { loadPlugins: "always" } },
|
||||
{ commandPath: ["agents"], policy: { loadPlugins: "always" } },
|
||||
{
|
||||
commandPath: ["agents", "bind"],
|
||||
exact: true,
|
||||
policy: { loadPlugins: "never" },
|
||||
},
|
||||
{
|
||||
commandPath: ["agents", "bindings"],
|
||||
exact: true,
|
||||
policy: { loadPlugins: "never" },
|
||||
},
|
||||
{
|
||||
commandPath: ["agents", "unbind"],
|
||||
exact: true,
|
||||
policy: { loadPlugins: "never" },
|
||||
},
|
||||
{ commandPath: ["configure"], policy: { bypassConfigGuard: true, loadPlugins: "never" } },
|
||||
{
|
||||
commandPath: ["status"],
|
||||
|
||||
@@ -43,6 +43,22 @@ describe("command-path-policy", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps agent binding commands on config-only startup", () => {
|
||||
for (const commandPath of [
|
||||
["agents", "bind"],
|
||||
["agents", "bindings"],
|
||||
["agents", "unbind"],
|
||||
]) {
|
||||
expect(resolveCliCommandPathPolicy(commandPath)).toEqual({
|
||||
bypassConfigGuard: false,
|
||||
routeConfigGuard: "never",
|
||||
loadPlugins: "never",
|
||||
hideBanner: false,
|
||||
ensureCliPath: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("resolves mixed startup-only rules", () => {
|
||||
expect(resolveCliCommandPathPolicy(["configure"])).toEqual({
|
||||
bypassConfigGuard: true,
|
||||
|
||||
@@ -89,6 +89,24 @@ describe("command-startup-policy", () => {
|
||||
jsonOutputMode: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldLoadPluginsForCommandPath({
|
||||
commandPath: ["agents", "bind"],
|
||||
jsonOutputMode: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldLoadPluginsForCommandPath({
|
||||
commandPath: ["agents", "bindings"],
|
||||
jsonOutputMode: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldLoadPluginsForCommandPath({
|
||||
commandPath: ["agents", "unbind"],
|
||||
jsonOutputMode: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("matches banner suppression policy", () => {
|
||||
|
||||
@@ -9,6 +9,11 @@ import {
|
||||
} from "./agents.bind.test-support.js";
|
||||
import { baseConfigSnapshot } from "./test-runtime-config-helpers.js";
|
||||
|
||||
const pluginRegistryMocks = vi.hoisted(() => ({
|
||||
loadPluginRegistrySnapshot: vi.fn(() => ({})),
|
||||
listPluginContributionIds: vi.fn(() => ["external-chat"]),
|
||||
}));
|
||||
|
||||
vi.mock("../agents/agent-scope.js", () => ({
|
||||
listAgentEntries: (
|
||||
cfg: {
|
||||
@@ -28,6 +33,11 @@ vi.mock("../config/bindings.js", () => ({
|
||||
(cfg.bindings ?? []).filter((binding) => Boolean(binding.match)),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/plugin-registry.js", () => ({
|
||||
loadPluginRegistrySnapshot: pluginRegistryMocks.loadPluginRegistrySnapshot,
|
||||
listPluginContributionIds: pluginRegistryMocks.listPluginContributionIds,
|
||||
}));
|
||||
|
||||
type BindingResolverTestPlugin = Pick<ChannelPlugin, "id" | "meta" | "capabilities" | "config"> & {
|
||||
setup?: Pick<NonNullable<ChannelPlugin["setup"]>, "resolveBindingAccountId">;
|
||||
};
|
||||
@@ -59,6 +69,12 @@ function createBindingResolverTestPlugin(params: {
|
||||
}
|
||||
|
||||
vi.mock("../channels/plugins/index.js", () => {
|
||||
return {
|
||||
getLoadedChannelPlugin: () => undefined,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../channels/plugins/bundled.js", () => {
|
||||
const knownChannels = new Map([
|
||||
[
|
||||
"discord",
|
||||
@@ -78,17 +94,10 @@ vi.mock("../channels/plugins/index.js", () => {
|
||||
],
|
||||
]);
|
||||
return {
|
||||
getChannelPlugin: (channel: string) => {
|
||||
getBundledChannelSetupPlugin: (channel: string) => {
|
||||
const normalized = channel.trim().toLowerCase();
|
||||
return knownChannels.get(normalized);
|
||||
},
|
||||
normalizeChannelId: (channel: string) => {
|
||||
const normalized = channel.trim().toLowerCase();
|
||||
if (knownChannels.has(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -104,6 +113,8 @@ describe("agents bind/unbind commands", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
resetAgentsBindTestHarness();
|
||||
pluginRegistryMocks.loadPluginRegistrySnapshot.mockClear();
|
||||
pluginRegistryMocks.listPluginContributionIds.mockClear();
|
||||
});
|
||||
|
||||
it("lists all bindings by default", async () => {
|
||||
@@ -141,6 +152,29 @@ describe("agents bind/unbind commands", () => {
|
||||
expect(runtime.exit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("binds manifest-known external channels without loading plugin runtime", async () => {
|
||||
readConfigFileSnapshotMock.mockResolvedValue({
|
||||
...baseConfigSnapshot,
|
||||
config: {},
|
||||
});
|
||||
|
||||
await agentsBindCommand({ bind: ["external-chat:work"] }, runtime);
|
||||
|
||||
expect(writeConfigFileMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
bindings: [
|
||||
{
|
||||
type: "route",
|
||||
agentId: "main",
|
||||
match: { channel: "external-chat", accountId: "work" },
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(pluginRegistryMocks.loadPluginRegistrySnapshot).toHaveBeenCalled();
|
||||
expect(runtime.exit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("unbinds all routes for an agent", async () => {
|
||||
readConfigFileSnapshotMock.mockResolvedValue({
|
||||
...baseConfigSnapshot,
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { getBundledChannelSetupPlugin } from "../channels/plugins/bundled.js";
|
||||
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
|
||||
import { getChannelPlugin, normalizeChannelId } from "../channels/plugins/index.js";
|
||||
import { getLoadedChannelPlugin } from "../channels/plugins/index.js";
|
||||
import type { ChannelId } from "../channels/plugins/types.public.js";
|
||||
import { normalizeChannelId as normalizeBundledChannelId } from "../channels/registry.js";
|
||||
import { isRouteBinding, listRouteBindings } from "../config/bindings.js";
|
||||
import type { AgentRouteBinding } from "../config/types.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import {
|
||||
listPluginContributionIds,
|
||||
loadPluginRegistrySnapshot,
|
||||
} from "../plugins/plugin-registry.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAgentId } from "../routing/session-key.js";
|
||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
import { normalizeStringEntries } from "../shared/string-normalization.js";
|
||||
@@ -206,13 +212,47 @@ export function removeAgentBindings(
|
||||
}
|
||||
|
||||
function resolveDefaultAccountId(cfg: OpenClawConfig, provider: ChannelId): string {
|
||||
const plugin = getChannelPlugin(provider);
|
||||
const plugin = getBindingChannelPlugin(provider);
|
||||
if (!plugin) {
|
||||
return DEFAULT_ACCOUNT_ID;
|
||||
}
|
||||
return resolveChannelDefaultAccountId({ plugin, cfg });
|
||||
}
|
||||
|
||||
function listManifestChannelIds(config: OpenClawConfig): Set<string> {
|
||||
const index = loadPluginRegistrySnapshot({
|
||||
config,
|
||||
env: process.env,
|
||||
});
|
||||
return new Set(
|
||||
listPluginContributionIds({
|
||||
index,
|
||||
contribution: "channels",
|
||||
includeDisabled: true,
|
||||
config,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeBindingChannelId(
|
||||
raw: string | undefined,
|
||||
config: OpenClawConfig,
|
||||
): ChannelId | null {
|
||||
const bundled = normalizeBundledChannelId(raw);
|
||||
if (bundled) {
|
||||
return bundled;
|
||||
}
|
||||
const normalized = normalizeOptionalString(raw)?.toLowerCase();
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
return listManifestChannelIds(config).has(normalized) ? normalized : null;
|
||||
}
|
||||
|
||||
function getBindingChannelPlugin(channel: ChannelId) {
|
||||
return getLoadedChannelPlugin(channel) ?? getBundledChannelSetupPlugin(channel);
|
||||
}
|
||||
|
||||
function resolveBindingAccountId(params: {
|
||||
channel: ChannelId;
|
||||
config: OpenClawConfig;
|
||||
@@ -224,7 +264,7 @@ function resolveBindingAccountId(params: {
|
||||
return explicitAccountId;
|
||||
}
|
||||
|
||||
const plugin = getChannelPlugin(params.channel);
|
||||
const plugin = getBindingChannelPlugin(params.channel);
|
||||
const pluginAccountId = plugin?.setup?.resolveBindingAccountId?.({
|
||||
cfg: params.config,
|
||||
agentId: params.agentId,
|
||||
@@ -279,7 +319,7 @@ export function parseBindingSpecs(params: {
|
||||
continue;
|
||||
}
|
||||
const [channelRaw, accountRaw] = trimmed.split(":", 2);
|
||||
const channel = normalizeChannelId(channelRaw);
|
||||
const channel = normalizeBindingChannelId(channelRaw, params.config);
|
||||
if (!channel) {
|
||||
errors.push(`Unknown channel "${channelRaw}".`);
|
||||
continue;
|
||||
|
||||
Reference in New Issue
Block a user