fix(plugins): guard manifest owner metadata

This commit is contained in:
Vincent Koc
2026-06-04 04:52:36 +02:00
parent c7a8114f54
commit 277f5f0b11
2 changed files with 140 additions and 14 deletions

View File

@@ -56,6 +56,32 @@ describe("manifest command aliases", () => {
});
});
it("skips unreadable command alias owner rows", () => {
const poisonedPlugin: {
id: string;
commandAliases: { name: string }[];
} = {
get id(): string {
throw new Error("manifest command alias plugin id exploded");
},
commandAliases: [{ name: "legacy-memory" }],
};
const registry = {
plugins: [
poisonedPlugin,
{
id: "memory",
commandAliases: [{ name: "memory" }],
},
],
};
expect(resolveManifestCommandAliasOwnerInRegistry({ command: "memory", registry })).toEqual({
name: "memory",
pluginId: "memory",
});
});
it("resolves agent tool owners from contracts.tools", () => {
const registry = {
plugins: [
@@ -83,4 +109,33 @@ describe("manifest command aliases", () => {
).toBeUndefined();
expect(resolveManifestToolOwnerInRegistry({ toolName: "", registry })).toBeUndefined();
});
it("skips unreadable tool owner rows", () => {
const poisonedPlugin = Object.defineProperty(
{
id: "poisoned-plugin",
},
"contracts",
{
enumerable: true,
get() {
throw new Error("manifest tool owner contracts exploded");
},
},
);
const registry = {
plugins: [
poisonedPlugin,
{
id: "healthy-plugin",
contracts: { tools: ["healthy_tool"] },
},
],
};
expect(resolveManifestToolOwnerInRegistry({ toolName: "healthy_tool", registry })).toEqual({
toolName: "healthy_tool",
pluginId: "healthy-plugin",
});
});
});

View File

@@ -46,6 +46,69 @@ export type PluginManifestCommandAliasRegistry = {
}[];
};
function readRecordValue(value: unknown, key: string): unknown {
if (!isRecord(value)) {
return undefined;
}
try {
return value[key];
} catch {
return undefined;
}
}
function readArrayEntries(value: unknown): unknown[] {
if (!Array.isArray(value)) {
return [];
}
let length: number;
try {
length = value.length;
} catch {
return [];
}
const entries: unknown[] = [];
for (let index = 0; index < length; index += 1) {
try {
entries.push(value[index]);
} catch {
// Ignore only the unreadable row; readable siblings can still resolve.
}
}
return entries;
}
function readManifestPluginId(plugin: unknown): string | undefined {
return normalizeOptionalString(readRecordValue(plugin, "id"));
}
function readManifestCommandAlias(alias: unknown): PluginManifestCommandAlias | undefined {
const name = normalizeOptionalString(readRecordValue(alias, "name"));
if (!name) {
return undefined;
}
const kind = readRecordValue(alias, "kind") === "runtime-slash" ? "runtime-slash" : undefined;
const cliCommand = normalizeOptionalString(readRecordValue(alias, "cliCommand"));
return {
name,
...(kind ? { kind } : {}),
...(cliCommand ? { cliCommand } : {}),
};
}
function readManifestCommandAliases(plugin: unknown): PluginManifestCommandAlias[] {
return readArrayEntries(readRecordValue(plugin, "commandAliases"))
.map(readManifestCommandAlias)
.filter((alias): alias is PluginManifestCommandAlias => Boolean(alias));
}
function readManifestToolNames(plugin: unknown): string[] {
const contracts = readRecordValue(plugin, "contracts");
return readArrayEntries(readRecordValue(contracts, "tools"))
.map((entry) => normalizeOptionalString(entry))
.filter((entry): entry is string => Boolean(entry));
}
export function normalizeManifestCommandAliases(
value: unknown,
): PluginManifestCommandAlias[] | undefined {
@@ -89,15 +152,18 @@ export function resolveManifestToolOwnerInRegistry(params: {
return undefined;
}
for (const plugin of params.registry.plugins) {
const tools = plugin.contracts?.tools;
if (!tools || tools.length === 0) {
const tools = readManifestToolNames(plugin);
if (tools.length === 0) {
continue;
}
const match = tools.find(
(entry) => normalizeOptionalLowercaseString(entry) === normalizedToolName,
);
if (match) {
return { toolName: match, pluginId: plugin.id };
const pluginId = readManifestPluginId(plugin);
if (!pluginId) {
continue;
}
for (const tool of tools) {
if (normalizeOptionalLowercaseString(tool) === normalizedToolName) {
return { toolName: tool, pluginId };
}
}
}
return undefined;
@@ -112,22 +178,27 @@ export function resolveManifestCommandAliasOwnerInRegistry(params: {
return undefined;
}
const commandIsPluginId = params.registry.plugins.some(
(plugin) => normalizeOptionalLowercaseString(plugin.id) === normalizedCommand,
);
const commandIsPluginId = params.registry.plugins.some((plugin) => {
const pluginId = readManifestPluginId(plugin);
return pluginId ? normalizeOptionalLowercaseString(pluginId) === normalizedCommand : false;
});
for (const plugin of params.registry.plugins) {
const alias = plugin.commandAliases?.find(
const pluginId = readManifestPluginId(plugin);
if (!pluginId) {
continue;
}
const alias = readManifestCommandAliases(plugin).find(
(entry) => normalizeOptionalLowercaseString(entry.name) === normalizedCommand,
);
if (alias) {
if (commandIsPluginId && normalizeOptionalLowercaseString(plugin.id) !== normalizedCommand) {
if (commandIsPluginId && normalizeOptionalLowercaseString(pluginId) !== normalizedCommand) {
continue;
}
return {
...alias,
pluginId: plugin.id,
...(plugin.enabledByDefault === true ? { enabledByDefault: true } : {}),
pluginId,
...(readRecordValue(plugin, "enabledByDefault") === true ? { enabledByDefault: true } : {}),
};
}
}