mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-12 20:30:43 +00:00
fix(cli): clarify plugin tool command mistakes
Summary: - clarify CLI diagnostics when an unknown subcommand is actually a plugin agent tool - route early proxy-preflight misses through the same policy helper - refresh bundled sidecar update fixtures for current package ownership Verification: - pnpm test src/cli/run-main.test.ts src/cli/run-main.exit.test.ts src/plugins/manifest-command-aliases.test.ts - pnpm test src/infra/update-global.test.ts src/infra/update-runner.test.ts - pnpm exec oxfmt --check --threads=1 CHANGELOG.md src/cli/run-main-policy.ts src/cli/run-main.ts src/cli/run-main.test.ts src/cli/run-main.exit.test.ts src/plugins/manifest-command-aliases.ts src/plugins/manifest-command-aliases.runtime.ts src/plugins/manifest-command-aliases.test.ts - pnpm build - live temp lossless-claw manifest: pnpm openclaw lcm_recent emits the agent-tool diagnostic and no plugins.allow suggestion Co-authored-by: 100yenadmin <100yenadmin+agent-77214@100yen.org>
This commit is contained in:
committed by
GitHub
parent
678c41a222
commit
0c50957dbb
@@ -1,10 +1,18 @@
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
|
||||
import {
|
||||
resolveManifestCommandAliasOwnerInRegistry,
|
||||
resolveManifestToolOwnerInRegistry,
|
||||
type PluginManifestCommandAliasRegistry,
|
||||
type PluginManifestCommandAliasRecord,
|
||||
type PluginManifestToolOwnerRecord,
|
||||
} from "./manifest-command-aliases.js";
|
||||
import { loadManifestMetadataRegistry } from "./manifest-contract-eligibility.js";
|
||||
import {
|
||||
isManifestPluginAvailableForControlPlane,
|
||||
loadManifestMetadataRegistry,
|
||||
loadManifestMetadataSnapshot,
|
||||
} from "./manifest-contract-eligibility.js";
|
||||
import { hasManifestToolAvailability } from "./manifest-tool-availability.js";
|
||||
|
||||
export function resolveManifestCommandAliasOwner(params: {
|
||||
command: string | undefined;
|
||||
@@ -25,3 +33,83 @@ export function resolveManifestCommandAliasOwner(params: {
|
||||
registry,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve which plugin owns an agent-tool name, applying control-plane
|
||||
* availability filters so disabled/denied plugins are not falsely attributed.
|
||||
*
|
||||
* Behavior:
|
||||
* - Walks the full manifest snapshot (not the lighter-weight registry view) so
|
||||
* per-tool `configSignals`/`authSignals` are visible.
|
||||
* - Skips plugins that fail `isManifestPluginAvailableForControlPlane`
|
||||
* (`plugins.allow` / `plugins.deny` / `plugins.entries[id].enabled` /
|
||||
* installed-index).
|
||||
* - For matched tools, runs `hasManifestToolAvailability` to check the
|
||||
* tool's own configSignals (e.g. Feishu's `appId`/`appSecret` gate).
|
||||
* - Reports `availability: "loaded"` when both filters pass, enough for a
|
||||
* direct "available from this plugin" diagnostic.
|
||||
* - Reports `availability: "manifest-only"` when the manifest declares
|
||||
* ownership but availability is not provable from manifest alone (e.g.
|
||||
* per-account `enabled` flags or per-tool toggles that are runtime-only).
|
||||
* Caller should soften the wording to "may be provided by".
|
||||
*
|
||||
* Falls back to the pure registry walk only when an explicit registry is
|
||||
* supplied (no snapshot to filter against).
|
||||
*/
|
||||
export function resolveManifestToolOwner(params: {
|
||||
toolName: string | undefined;
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
registry?: PluginManifestCommandAliasRegistry;
|
||||
}): PluginManifestToolOwnerRecord | undefined {
|
||||
if (params.registry) {
|
||||
return resolveManifestToolOwnerInRegistry({
|
||||
toolName: params.toolName,
|
||||
registry: params.registry,
|
||||
});
|
||||
}
|
||||
const normalizedToolName = normalizeOptionalLowercaseString(params.toolName);
|
||||
if (!normalizedToolName) {
|
||||
return undefined;
|
||||
}
|
||||
const snapshot = loadManifestMetadataSnapshot({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
});
|
||||
const env = params.env ?? process.env;
|
||||
for (const plugin of snapshot.plugins) {
|
||||
const tools = plugin.contracts?.tools;
|
||||
if (!tools || tools.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const match = tools.find(
|
||||
(entry) => normalizeOptionalLowercaseString(entry) === normalizedToolName,
|
||||
);
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
const pluginAvailable = isManifestPluginAvailableForControlPlane({
|
||||
snapshot,
|
||||
plugin,
|
||||
config: params.config,
|
||||
});
|
||||
if (!pluginAvailable) {
|
||||
// Plugin is denied/disabled/uninstalled; do not attribute this tool to it.
|
||||
continue;
|
||||
}
|
||||
const toolAvailable = hasManifestToolAvailability({
|
||||
plugin,
|
||||
toolNames: [match],
|
||||
config: params.config,
|
||||
env,
|
||||
});
|
||||
return {
|
||||
toolName: match,
|
||||
pluginId: plugin.id,
|
||||
availability: toolAvailable ? "loaded" : "manifest-only",
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
normalizeManifestCommandAliases,
|
||||
resolveManifestCommandAliasOwnerInRegistry,
|
||||
resolveManifestToolOwnerInRegistry,
|
||||
} from "./manifest-command-aliases.js";
|
||||
|
||||
describe("manifest command aliases", () => {
|
||||
@@ -46,4 +47,31 @@ describe("manifest command aliases", () => {
|
||||
name: "legacy-memory",
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves agent tool owners from contracts.tools", () => {
|
||||
const registry = {
|
||||
plugins: [
|
||||
{
|
||||
id: "lossless-claw",
|
||||
contracts: { tools: ["lcm_recent", "lcm_search"] },
|
||||
},
|
||||
{
|
||||
id: "other-plugin",
|
||||
contracts: { tools: ["unrelated_tool"] },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(resolveManifestToolOwnerInRegistry({ toolName: "lcm_recent", registry })).toMatchObject({
|
||||
pluginId: "lossless-claw",
|
||||
toolName: "lcm_recent",
|
||||
});
|
||||
expect(resolveManifestToolOwnerInRegistry({ toolName: "LCM_Recent", registry })).toMatchObject({
|
||||
pluginId: "lossless-claw",
|
||||
});
|
||||
expect(
|
||||
resolveManifestToolOwnerInRegistry({ toolName: "missing_tool", registry }),
|
||||
).toBeUndefined();
|
||||
expect(resolveManifestToolOwnerInRegistry({ toolName: "", registry })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,11 +20,29 @@ export type PluginManifestCommandAliasRecord = PluginManifestCommandAlias & {
|
||||
enabledByDefault?: boolean;
|
||||
};
|
||||
|
||||
export type PluginManifestToolOwnerRecord = {
|
||||
toolName: string;
|
||||
pluginId: string;
|
||||
/**
|
||||
* "loaded" — the owning plugin passes control-plane availability filters and
|
||||
* the tool itself passes manifest-tool-availability checks (configSignals/
|
||||
* authSignals). The diagnostic can say the tool is available from this plugin.
|
||||
*
|
||||
* "manifest-only" — the manifest claims ownership but availability checks
|
||||
* either failed (plugin denied/disabled, missing required config) or were
|
||||
* not performed (pure registry lookup with no plugin metadata snapshot).
|
||||
* Emit a softer "may be provided by" message in that case so the diagnostic
|
||||
* does not over-assert about plugins that the runtime never registered.
|
||||
*/
|
||||
availability?: "loaded" | "manifest-only";
|
||||
};
|
||||
|
||||
export type PluginManifestCommandAliasRegistry = {
|
||||
plugins: readonly {
|
||||
id: string;
|
||||
enabledByDefault?: boolean;
|
||||
commandAliases?: readonly PluginManifestCommandAlias[];
|
||||
contracts?: { tools?: readonly string[] };
|
||||
}[];
|
||||
};
|
||||
|
||||
@@ -62,6 +80,29 @@ export function normalizeManifestCommandAliases(
|
||||
return normalized.length > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
export function resolveManifestToolOwnerInRegistry(params: {
|
||||
toolName: string | undefined;
|
||||
registry: PluginManifestCommandAliasRegistry;
|
||||
}): PluginManifestToolOwnerRecord | undefined {
|
||||
const normalizedToolName = normalizeOptionalLowercaseString(params.toolName);
|
||||
if (!normalizedToolName) {
|
||||
return undefined;
|
||||
}
|
||||
for (const plugin of params.registry.plugins) {
|
||||
const tools = plugin.contracts?.tools;
|
||||
if (!tools || tools.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const match = tools.find(
|
||||
(entry) => normalizeOptionalLowercaseString(entry) === normalizedToolName,
|
||||
);
|
||||
if (match) {
|
||||
return { toolName: match, pluginId: plugin.id };
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function resolveManifestCommandAliasOwnerInRegistry(params: {
|
||||
command: string | undefined;
|
||||
registry: PluginManifestCommandAliasRegistry;
|
||||
|
||||
Reference in New Issue
Block a user