mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
fix(cli): keep tools rpc namespace off plugin startup
This commit is contained in:
@@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- CLI/models: restore provider-filtered `models list --all --provider <id>` rows for providers without manifest/static catalog coverage, including Anthropic and Amazon Bedrock, while keeping the compatibility fallback off expensive availability and resolver paths. Thanks @shakkernerd.
|
||||
- CLI/tools: keep the Gateway `tools.*` RPC namespace out of plugin command discovery and managed proxy startup, so stray commands like `openclaw tools effective` fail quickly instead of cold-loading plugin metadata. Refs #73477. Thanks @oromeis.
|
||||
- CLI/status: keep default text `openclaw status --usage` on metadata-only channel scans unless `--deep` or `--all` is set, and send stray `openclaw tools --help` through the precomputed root-help fast path so latency-triage commands avoid plugin/runtime cold loads before printing. Refs #73477 and #74220. Thanks @oromeis and @NianJiuZst.
|
||||
- Plugins/runtime-deps: memoize packaged bundled runtime dist-mirror preparation after the first successful pass while keeping source-checkout mirrors refreshable, so constrained Docker/VPS installs avoid repeated root scans before chat turns. Refs #73428, #73421, #73532, and #73477. Thanks @Dimaoggg, @oromeis, @oadiazp, @jmfraga, @bstanbury, @antoniusfelix, and @jkobject.
|
||||
- Channels/Discord: treat bare numeric outbound targets that match the effective Discord DM allowlist as user DMs while preserving account-specific legacy `dm.allowFrom` precedence over inherited root `allowFrom`. (#74303) Thanks @Squirbie.
|
||||
|
||||
@@ -211,6 +211,10 @@ export const cliCommandCatalog: readonly CliCommandCatalogEntry[] = [
|
||||
},
|
||||
route: { id: "tasks-list" },
|
||||
},
|
||||
{
|
||||
commandPath: ["tools"],
|
||||
policy: { loadPlugins: "never", ensureCliPath: false, networkProxy: "bypass" },
|
||||
},
|
||||
{ commandPath: ["acp"], policy: { networkProxy: "bypass" } },
|
||||
{ commandPath: ["approvals"], policy: { networkProxy: "bypass" } },
|
||||
{ commandPath: ["backup"], policy: { bypassConfigGuard: true, networkProxy: "bypass" } },
|
||||
|
||||
@@ -157,6 +157,7 @@ describe("command-path-policy", () => {
|
||||
expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "googlemeet", "login"])).toBe(
|
||||
"default",
|
||||
);
|
||||
expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "tools", "effective"])).toBe("bypass");
|
||||
});
|
||||
|
||||
it("resolves static network proxy bypass policies from the catalog", () => {
|
||||
|
||||
@@ -50,6 +50,20 @@ describe("command-registration-policy", () => {
|
||||
hasBuiltinPrimary: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldSkipPluginCommandRegistration({
|
||||
argv: ["node", "openclaw", "tools", "effective"],
|
||||
primary: "tools",
|
||||
hasBuiltinPrimary: false,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldSkipPluginCommandRegistration({
|
||||
argv: ["node", "openclaw", "googlemeet", "login"],
|
||||
primary: "googlemeet",
|
||||
hasBuiltinPrimary: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("matches lazy subcommand registration policy", () => {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { isTruthyEnvValue } from "../infra/env.js";
|
||||
import { resolveCliArgvInvocation } from "./argv-invocation.js";
|
||||
|
||||
const RESERVED_NON_PLUGIN_COMMAND_ROOTS = new Set(["tools"]);
|
||||
|
||||
export function shouldRegisterPrimaryCommandOnly(argv: string[]): boolean {
|
||||
const invocation = resolveCliArgvInvocation(argv);
|
||||
return invocation.primary !== null || !invocation.hasHelpOrVersion;
|
||||
@@ -21,6 +23,9 @@ export function shouldSkipPluginCommandRegistration(params: {
|
||||
if (!params.primary) {
|
||||
return invocation.hasHelpOrVersion;
|
||||
}
|
||||
if (RESERVED_NON_PLUGIN_COMMAND_ROOTS.has(params.primary)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -177,6 +177,7 @@ describe("command-startup-policy", () => {
|
||||
expect(shouldEnsureCliPathForCommandPath(["sessions"])).toBe(false);
|
||||
expect(shouldEnsureCliPathForCommandPath(["config", "get"])).toBe(false);
|
||||
expect(shouldEnsureCliPathForCommandPath(["models", "status"])).toBe(false);
|
||||
expect(shouldEnsureCliPathForCommandPath(["tools", "effective"])).toBe(false);
|
||||
expect(shouldEnsureCliPathForCommandPath(["message", "send"])).toBe(true);
|
||||
expect(shouldEnsureCliPathForCommandPath([])).toBe(true);
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ const buildProgramMock = vi.hoisted(() => vi.fn());
|
||||
const getProgramContextMock = vi.hoisted(() => vi.fn(() => null));
|
||||
const registerCoreCliByNameMock = vi.hoisted(() => vi.fn());
|
||||
const registerSubCliByNameMock = vi.hoisted(() => vi.fn());
|
||||
const registerPluginCliCommandsFromValidatedConfigMock = vi.hoisted(() => vi.fn(async () => ({})));
|
||||
const restoreTerminalStateMock = vi.hoisted(() => vi.fn());
|
||||
const hasEnvHttpProxyAgentConfiguredMock = vi.hoisted(() => vi.fn(() => false));
|
||||
const ensureGlobalUndiciEnvProxyDispatcherMock = vi.hoisted(() => vi.fn());
|
||||
@@ -151,6 +152,10 @@ vi.mock("./program/register.subclis.js", () => ({
|
||||
registerSubCliByName: registerSubCliByNameMock,
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/cli.js", () => ({
|
||||
registerPluginCliCommandsFromValidatedConfig: registerPluginCliCommandsFromValidatedConfigMock,
|
||||
}));
|
||||
|
||||
vi.mock("../terminal/restore.js", () => ({
|
||||
restoreTerminalState: restoreTerminalStateMock,
|
||||
}));
|
||||
@@ -358,6 +363,7 @@ describe("runCli exit behavior", () => {
|
||||
["models list", ["node", "openclaw", "models", "list"]],
|
||||
["models status without live probe", ["node", "openclaw", "models", "status"]],
|
||||
["tasks list", ["node", "openclaw", "tasks", "list"]],
|
||||
["gateway tools namespace typo", ["node", "openclaw", "tools", "effective"]],
|
||||
["migrate", ["node", "openclaw", "migrate"]],
|
||||
])("skips managed proxy routing for %s", (_name, argv) => {
|
||||
expect(shouldStartProxyForCli(argv)).toBe(false);
|
||||
@@ -379,6 +385,26 @@ describe("runCli exit behavior", () => {
|
||||
expect(startProxyMock).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
|
||||
it("keeps gateway tool RPC names out of plugin command discovery", async () => {
|
||||
const parseAsync = vi.fn().mockResolvedValueOnce(undefined);
|
||||
buildProgramMock.mockReturnValueOnce({
|
||||
commands: [],
|
||||
parseAsync,
|
||||
});
|
||||
|
||||
await runCli(["node", "openclaw", "tools", "effective"]);
|
||||
|
||||
expect(startProxyMock).not.toHaveBeenCalled();
|
||||
expect(registerSubCliByNameMock).toHaveBeenCalledWith(expect.anything(), "tools", [
|
||||
"node",
|
||||
"openclaw",
|
||||
"tools",
|
||||
"effective",
|
||||
]);
|
||||
expect(registerPluginCliCommandsFromValidatedConfigMock).not.toHaveBeenCalled();
|
||||
expect(parseAsync).toHaveBeenCalledWith(["node", "openclaw", "tools", "effective"]);
|
||||
});
|
||||
|
||||
it("fails protected commands when managed proxy activation fails", async () => {
|
||||
startProxyMock.mockRejectedValueOnce(new Error("proxy: enabled but no HTTP proxy URL"));
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@ describe("shouldEnsureCliPath", () => {
|
||||
expect(shouldEnsureCliPath(["node", "openclaw", "sessions", "--json"])).toBe(false);
|
||||
expect(shouldEnsureCliPath(["node", "openclaw", "config", "get", "update"])).toBe(false);
|
||||
expect(shouldEnsureCliPath(["node", "openclaw", "models", "status", "--json"])).toBe(false);
|
||||
expect(shouldEnsureCliPath(["node", "openclaw", "tools", "effective"])).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps path bootstrap for mutating or unknown commands", () => {
|
||||
|
||||
Reference in New Issue
Block a user