mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-16 16:21:34 +00:00
* feat: node-hosted plugins — dynamic tools, MCP servers, and skills Nodes become declarative plugin hosts: - node.pluginTools.update: node hosts publish plugin-registered agent tool descriptors; gateway materializes them as agent tools executing via node.invoke under the node command allowlist, with tools.effective invalidation and node online/offline removal. - Trusted paired-node descriptors: no gateway-side plugin registration required; gateway.nodes.pluginTools.enabled off-switch (default on); description/count caps; deterministic node-prefixed collision names. - Declarative node-hosted MCP: nodeHost.mcp.servers (McpServerConfig shape) starts MCP clients on the node host, publishes tools as pluginId node-mcp, executes via built-in mcp.tools.call.v1 with per-layer timeouts, failure isolation, and orphan-safe shutdown. No re-pairing when servers change. - Node-hosted skills: node.skills.update publishes ~/.openclaw/skills content (64 skills/64KB/512KB caps both sides); gateway merges them into the skills snapshot while connected and exec host=node is available, with node:// locators, node-prefixed collisions, disabled command dispatch, and gateway.nodes.skills.enabled + nodeHost.skills.enabled switches. - Security: node-supplied pluginIds cannot satisfy pluginId-scoped tool allowlists unless gateway-registered; reserved node-mcp id requires the core MCP descriptor shape; protocol registry kept out of public plugin-sdk dts. - E2E: pond harness proves publication, MCP round-trip, skills locator, and disconnect/reconnect for all three surfaces. * style: format node-plugin-tools test * fix(skills): keep status loader unfiltered when eligibility is passed skills.status started passing eligibility for the node-skill merge, which flipped loadWorkspaceSkillEntries into filtered mode and dropped disabled skills from status reports (QA plugin-lifecycle-hot-reload timeout). Status now merges node skills explicitly around an unfiltered load. Also: regen docs_map for new node docs sections; add the intentional node-host MCP onclose suppression to the lint-suppression allowlist.
55 lines
1.7 KiB
TypeScript
55 lines
1.7 KiB
TypeScript
import type { NodeSkillDescriptor } from "../../packages/gateway-protocol/src/schema/nodes.js";
|
|
import { createSubsystemLogger } from "../logging/subsystem.js";
|
|
import {
|
|
NODE_SKILL_MAX_CONTENT_BYTES,
|
|
NODE_SKILL_MAX_COUNT,
|
|
NODE_SKILL_MAX_DESCRIPTION_LENGTH,
|
|
NODE_SKILL_MAX_TOTAL_BYTES,
|
|
NODE_SKILL_NAME_RE,
|
|
} from "../shared/node-skill-constraints.js";
|
|
|
|
const log = createSubsystemLogger("gateway/node-skills");
|
|
|
|
export function normalizeNodeSkillDescriptors(params: {
|
|
nodeId: string;
|
|
skills?: readonly NodeSkillDescriptor[];
|
|
enabled?: boolean;
|
|
}): NodeSkillDescriptor[] {
|
|
if (params.enabled === false) {
|
|
return [];
|
|
}
|
|
|
|
const normalized: NodeSkillDescriptor[] = [];
|
|
const seen = new Set<string>();
|
|
let totalBytes = 0;
|
|
let droppedCount = 0;
|
|
for (const skill of params.skills ?? []) {
|
|
const name = skill.name.trim();
|
|
const description = skill.description.trim();
|
|
const contentBytes = Buffer.byteLength(skill.content, "utf8");
|
|
if (
|
|
!NODE_SKILL_NAME_RE.test(name) ||
|
|
!description ||
|
|
description.length > NODE_SKILL_MAX_DESCRIPTION_LENGTH ||
|
|
!skill.content ||
|
|
contentBytes > NODE_SKILL_MAX_CONTENT_BYTES ||
|
|
seen.has(name) ||
|
|
normalized.length >= NODE_SKILL_MAX_COUNT ||
|
|
totalBytes + contentBytes > NODE_SKILL_MAX_TOTAL_BYTES
|
|
) {
|
|
droppedCount += 1;
|
|
continue;
|
|
}
|
|
seen.add(name);
|
|
totalBytes += contentBytes;
|
|
normalized.push({ name, description, content: skill.content });
|
|
}
|
|
|
|
if (droppedCount > 0) {
|
|
log.warn(
|
|
`node ${params.nodeId} published ${params.skills?.length ?? 0} skill descriptors; dropped ${droppedCount} invalid or over-limit descriptors`,
|
|
);
|
|
}
|
|
return normalized.toSorted((left, right) => left.name.localeCompare(right.name, "en"));
|
|
}
|