refactor: tighten plugin boundary surfaces

This commit is contained in:
Peter Steinberger
2026-04-27 11:18:54 +01:00
parent c4fe72b8d6
commit 9b0a0fb0a7
33 changed files with 157 additions and 69 deletions

View File

@@ -1,2 +1,2 @@
8371f19a19ceeae4eb20fbfe8e68e51f6f54f42c487d7d5c75f214ab1ba0922a plugin-sdk-api-baseline.json
a5f5e15e75f8cf27ebaa1302cfe0488974edd53121279c1e90705bc531a4761a plugin-sdk-api-baseline.jsonl
6820a26fa0570caae81669337ec98f4946c096036e5603248c71a7afa4cb12a1 plugin-sdk-api-baseline.json
a3959468d81184676d92af7f8958c680ce351ac81d4bafade4428b0cb9609e65 plugin-sdk-api-baseline.jsonl

View File

@@ -463,7 +463,7 @@ Keep capability registration public. Trim non-contract helper exports:
- vendor-specific convenience helpers
- setup/onboarding helpers that are implementation details
Some bundled-plugin helper subpaths still remain in the generated SDK export map for compatibility and bundled-plugin maintenance. Current examples include `plugin-sdk/feishu`, `plugin-sdk/feishu-setup`, `plugin-sdk/zalo`, `plugin-sdk/zalo-setup`, and several `plugin-sdk/matrix*` seams. Treat those as reserved implementation-detail exports, not as the recommended SDK pattern for new third-party plugins.
Some bundled-plugin helper subpaths still remain in the generated SDK export map for compatibility and bundled-plugin maintenance. Current examples include `plugin-sdk/feishu`, `plugin-sdk/feishu-setup`, `plugin-sdk/zalo`, `plugin-sdk/zalo-setup`, `plugin-sdk/channel-config-schema-legacy`, and several `plugin-sdk/matrix*` seams. Treat those as deprecated reserved exports, not as the recommended SDK pattern for new third-party plugins.
## Internals and reference

View File

@@ -251,7 +251,8 @@ releases.
| `plugin-sdk/channel-pairing` | DM pairing primitives | `createChannelPairingController` |
| `plugin-sdk/channel-reply-pipeline` | Reply prefix + typing wiring | `createChannelReplyPipeline` |
| `plugin-sdk/channel-config-helpers` | Config adapter factories | `createHybridChannelConfigAdapter` |
| `plugin-sdk/channel-config-schema` | Config schema builders | Shared channel config schema primitives; bundled-channel-named schema exports are legacy compatibility only |
| `plugin-sdk/channel-config-schema` | Config schema builders | Shared channel config schema primitives and the generic builder only |
| `plugin-sdk/channel-config-schema-legacy` | Deprecated bundled config schemas | Bundled compatibility only; new plugins must define plugin-local schemas |
| `plugin-sdk/telegram-command-config` | Telegram command config helpers | Command-name normalization, description trimming, duplicate/conflict validation |
| `plugin-sdk/channel-policy` | Group/DM policy resolution | `resolveChannelGroupRequireMention` |
| `plugin-sdk/channel-lifecycle` | Account status and draft stream lifecycle helpers | `createAccountStatusSink`, draft preview finalization helpers |

View File

@@ -37,9 +37,9 @@ the broader umbrella surface and shared helpers such as
For channel config, publish the channel-owned JSON Schema through
`openclaw.plugin.json#channelConfigs`. The `plugin-sdk/channel-config-schema`
subpath is for shared schema primitives and the generic builder. Any
bundled-channel-named schema exports on that subpath are legacy compatibility
exports, not a pattern for new plugins.
subpath is for shared schema primitives and the generic builder. Deprecated
bundled-channel schema exports live on `plugin-sdk/channel-config-schema-legacy`
for bundled compatibility only; they are not a pattern for new plugins.
<Warning>
Do not import provider- or channel-branded convenience seams (for example
@@ -94,6 +94,10 @@ methods:
| `api.registerTool(tool, opts?)` | Agent tool (required or `{ optional: true }`) |
| `api.registerCommand(def)` | Custom command (bypasses the LLM) |
Plugin commands can set `agentPromptGuidance` when the agent needs a short,
command-owned routing hint. Keep that text about the command itself; do not add
provider- or plugin-specific policy to core prompt builders.
### Infrastructure
| Method | What it registers |

View File

@@ -43,7 +43,8 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview)
| `plugin-sdk/channel-pairing` | `createChannelPairingController` |
| `plugin-sdk/channel-reply-pipeline` | `createChannelReplyPipeline` |
| `plugin-sdk/channel-config-helpers` | `createHybridChannelConfigAdapter` |
| `plugin-sdk/channel-config-schema` | Channel config schema types |
| `plugin-sdk/channel-config-schema` | Shared channel config schema primitives and generic builder |
| `plugin-sdk/channel-config-schema-legacy` | Deprecated bundled-channel config schemas for bundled compatibility only |
| `plugin-sdk/telegram-command-config` | Telegram custom-command normalization/validation helpers with bundled-contract fallback |
| `plugin-sdk/command-gating` | Narrow command authorization gate helpers |
| `plugin-sdk/channel-policy` | `resolveChannelGroupRequireMention` |
@@ -272,7 +273,7 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview)
| Matrix | `plugin-sdk/matrix`, `plugin-sdk/matrix-helper`, `plugin-sdk/matrix-runtime-heavy`, `plugin-sdk/matrix-runtime-shared`, `plugin-sdk/matrix-runtime-surface`, `plugin-sdk/matrix-surface`, `plugin-sdk/matrix-thread-bindings` | Bundled Matrix helper/runtime surface |
| Line | `plugin-sdk/line`, `plugin-sdk/line-core`, `plugin-sdk/line-runtime`, `plugin-sdk/line-surface` | Bundled LINE helper/runtime surface |
| IRC | `plugin-sdk/irc`, `plugin-sdk/irc-surface` | Bundled IRC helper surface |
| Channel-specific helpers | `plugin-sdk/googlechat`, `plugin-sdk/zalouser`, `plugin-sdk/bluebubbles`, `plugin-sdk/bluebubbles-policy`, `plugin-sdk/mattermost`, `plugin-sdk/mattermost-policy`, `plugin-sdk/feishu-conversation`, `plugin-sdk/msteams`, `plugin-sdk/nextcloud-talk`, `plugin-sdk/nostr`, `plugin-sdk/tlon`, `plugin-sdk/twitch` | Bundled channel compatibility/helper seams |
| Channel-specific helpers | `plugin-sdk/googlechat`, `plugin-sdk/zalouser`, `plugin-sdk/bluebubbles`, `plugin-sdk/bluebubbles-policy`, `plugin-sdk/mattermost`, `plugin-sdk/mattermost-policy`, `plugin-sdk/feishu-conversation`, `plugin-sdk/msteams`, `plugin-sdk/nextcloud-talk`, `plugin-sdk/nostr`, `plugin-sdk/tlon`, `plugin-sdk/twitch` | Deprecated bundled channel compatibility/helper seams. New plugins should import generic SDK subpaths or plugin-local barrels. |
| Auth/plugin-specific helpers | `plugin-sdk/github-copilot-login`, `plugin-sdk/github-copilot-token`, `plugin-sdk/diagnostics-otel`, `plugin-sdk/diagnostics-prometheus`, `plugin-sdk/diffs`, `plugin-sdk/llm-task`, `plugin-sdk/thread-ownership`, `plugin-sdk/voice-call` | Bundled feature/plugin helper seams; `plugin-sdk/github-copilot-token` currently exports `DEFAULT_COPILOT_API_BASE_URL`, `deriveCopilotApiBaseUrlFromToken`, and `resolveCopilotApiToken` |
</Accordion>
</AccordionGroup>

View File

@@ -12,6 +12,10 @@ export function createCodexCommand(options: {
return {
name: "codex",
description: "Inspect and control the Codex app-server harness",
agentPromptGuidance: [
"Native Codex app-server plugin is available (`/codex ...`). For Codex bind/control/thread/resume/steer/stop requests, prefer `/codex bind`, `/codex threads`, `/codex resume`, `/codex steer`, and `/codex stop` over ACP.",
"Use ACP for Codex only when the user explicitly asks for ACP/acpx or wants to test the ACP path.",
],
acceptsArgs: true,
requireAuth: true,
handler: (ctx) => handleCodexCommand(ctx, options),

View File

@@ -1,4 +1,4 @@
export {
buildChannelConfigSchema,
DiscordConfigSchema,
} from "openclaw/plugin-sdk/channel-config-schema";
} from "openclaw/plugin-sdk/channel-config-schema-legacy";

View File

@@ -1,5 +1,5 @@
import { GoogleChatConfigSchema } from "openclaw/plugin-sdk/googlechat";
import { describe, expect, it } from "vitest";
import { GoogleChatConfigSchema } from "../runtime-api.js";
describe("googlechat config schema", () => {
it("accepts serviceAccount refs", () => {

View File

@@ -1,3 +1,3 @@
import { buildChannelConfigSchema, GoogleChatConfigSchema } from "openclaw/plugin-sdk/googlechat";
import { buildChannelConfigSchema, GoogleChatConfigSchema } from "../runtime-api.js";
export const GoogleChatChannelConfigSchema = buildChannelConfigSchema(GoogleChatConfigSchema);

View File

@@ -1,4 +1,4 @@
export {
buildChannelConfigSchema,
IMessageConfigSchema,
} from "openclaw/plugin-sdk/channel-config-schema";
} from "openclaw/plugin-sdk/channel-config-schema-legacy";

View File

@@ -1,4 +1,4 @@
export {
buildChannelConfigSchema,
MSTeamsConfigSchema,
} from "openclaw/plugin-sdk/channel-config-schema";
} from "openclaw/plugin-sdk/channel-config-schema-legacy";

View File

@@ -1,4 +1,4 @@
export {
buildChannelConfigSchema,
SignalConfigSchema,
} from "openclaw/plugin-sdk/channel-config-schema";
} from "openclaw/plugin-sdk/channel-config-schema-legacy";

View File

@@ -1,4 +1,4 @@
export {
buildChannelConfigSchema,
SlackConfigSchema,
} from "openclaw/plugin-sdk/channel-config-schema";
} from "openclaw/plugin-sdk/channel-config-schema-legacy";

View File

@@ -1,7 +1,7 @@
export {
buildChannelConfigSchema,
TelegramConfigSchema,
} from "openclaw/plugin-sdk/channel-config-schema";
} from "openclaw/plugin-sdk/channel-config-schema-legacy";
export {
normalizeTelegramCommandDescription,
normalizeTelegramCommandName,

View File

@@ -1,4 +1,4 @@
export {
buildChannelConfigSchema,
WhatsAppConfigSchema,
} from "openclaw/plugin-sdk/channel-config-schema";
} from "openclaw/plugin-sdk/channel-config-schema-legacy";

View File

@@ -642,6 +642,10 @@
"types": "./dist/plugin-sdk/channel-config-schema.d.ts",
"default": "./dist/plugin-sdk/channel-config-schema.js"
},
"./plugin-sdk/channel-config-schema-legacy": {
"types": "./dist/plugin-sdk/channel-config-schema-legacy.d.ts",
"default": "./dist/plugin-sdk/channel-config-schema-legacy.js"
},
"./plugin-sdk/channel-actions": {
"types": "./dist/plugin-sdk/channel-actions.d.ts",
"default": "./dist/plugin-sdk/channel-actions.js"

View File

@@ -47,6 +47,9 @@ export const pluginSdkDocMetadata = {
"channel-config-schema": {
category: "channel",
},
"channel-config-schema-legacy": {
category: "channel",
},
"channel-contract": {
category: "channel",
},

View File

@@ -144,6 +144,7 @@
"channel-config-writes",
"channel-config-primitives",
"channel-config-schema",
"channel-config-schema-legacy",
"channel-actions",
"channel-plugin-common",
"channel-core",

View File

@@ -21,7 +21,7 @@ import { formatErrorMessage } from "../../../infra/errors.js";
import { resolveHeartbeatSummaryForAgent } from "../../../infra/heartbeat-summary.js";
import { getMachineDisplayName } from "../../../infra/machine-name.js";
import { MAX_IMAGE_BYTES } from "../../../media/constants.js";
import { listRegisteredPluginCommands } from "../../../plugins/command-registry-state.js";
import { listRegisteredPluginAgentPromptGuidance } from "../../../plugins/command-registry-state.js";
import { getGlobalHookRunner } from "../../../plugins/hook-runner-global.js";
import {
extractModelCompat,
@@ -1132,7 +1132,7 @@ export async function runEmbeddedAttempt(
config: params.config,
sandboxed: sandboxInfo?.enabled === true,
}),
nativeCommandNames: listRegisteredPluginCommands().map((command) => command.name),
nativeCommandGuidanceLines: listRegisteredPluginAgentPromptGuidance(),
runtimeInfo,
messageToolHints,
sandboxInfo,

View File

@@ -34,6 +34,8 @@ export function buildEmbeddedSystemPrompt(params: {
acpEnabled?: boolean;
/** Registered runtime slash/native command names such as `codex`. */
nativeCommandNames?: string[];
/** Plugin-owned prompt guidance for registered native slash commands. */
nativeCommandGuidanceLines?: string[];
runtimeInfo: {
agentId?: string;
host: string;
@@ -79,6 +81,7 @@ export function buildEmbeddedSystemPrompt(params: {
promptMode: params.promptMode,
acpEnabled: params.acpEnabled,
nativeCommandNames: params.nativeCommandNames,
nativeCommandGuidanceLines: params.nativeCommandGuidanceLines,
runtimeInfo: params.runtimeInfo,
messageToolHints: params.messageToolHints,
sandboxInfo: params.sandboxInfo,

View File

@@ -5,7 +5,7 @@ import { isAcpRuntimeSpawnAvailable } from "../acp/runtime/availability.js";
import type { SessionEntry } from "../config/sessions/types.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { SubagentSpawnPreparation } from "../context-engine/types.js";
import { listRegisteredPluginCommands } from "../plugins/command-registry-state.js";
import { listRegisteredPluginAgentPromptGuidance } from "../plugins/command-registry-state.js";
import type { SubagentLifecycleHookRunner } from "../plugins/hooks.js";
import { isValidAgentId, normalizeAgentId, parseAgentSessionKey } from "../routing/session-key.js";
import {
@@ -931,7 +931,7 @@ export async function spawnSubagentDirect(
config: cfg,
sandboxed: childRuntime.sandboxed,
}),
nativeCommandNames: listRegisteredPluginCommands().map((command) => command.name),
nativeCommandGuidanceLines: listRegisteredPluginAgentPromptGuidance(),
childDepth,
maxSpawnDepth,
});

View File

@@ -11,6 +11,8 @@ export function buildSubagentSystemPrompt(params: {
acpEnabled?: boolean;
/** Registered runtime slash/native command names such as `codex`. */
nativeCommandNames?: string[];
/** Plugin-owned prompt guidance for registered native slash commands. */
nativeCommandGuidanceLines?: string[];
/** Depth of the child being spawned (1 = sub-agent, 2 = sub-sub-agent). */
childDepth?: number;
/** Config value: max allowed spawn depth. */
@@ -25,8 +27,8 @@ export function buildSubagentSystemPrompt(params: {
? params.maxSpawnDepth
: DEFAULT_SUBAGENT_MAX_SPAWN_DEPTH;
const acpEnabled = params.acpEnabled === true;
const nativeCodexCommandAvailable = (params.nativeCommandNames ?? []).some(
(name) => name.trim().replace(/^\/+/, "").toLowerCase() === "codex",
const nativeCommandGuidanceLines = Array.from(
new Set((params.nativeCommandGuidanceLines ?? []).map((line) => line.trim()).filter(Boolean)),
);
const canSpawn = childDepth < maxSpawnDepth;
const parentLabel = childDepth >= 2 ? "parent orchestrator" : "main agent";
@@ -95,11 +97,7 @@ export function buildSubagentSystemPrompt(params: {
"Coordinate their work and synthesize results before reporting back.",
...(acpEnabled
? [
...(nativeCodexCommandAvailable
? [
"Native Codex app-server plugin is available (`/codex ...`). Prefer that path for Codex bind/control/thread/resume/steer/stop requests; use Codex ACP only when explicitly requested.",
]
: []),
...nativeCommandGuidanceLines,
'For ACP harness sessions (claudecode/gemini/opencode, or Codex only when explicit ACP/acpx), use `sessions_spawn` with `runtime: "acp"` (set `agentId` unless `acp.defaultAgent` is configured).',
'`agents_list` and `subagents` apply to OpenClaw sub-agents (`runtime: "subagent"`); ACP harness ids are controlled by `acp.allowedAgents`.',
"Do not ask users to run slash commands or CLI when `sessions_spawn` can do it directly.",

View File

@@ -335,7 +335,10 @@ describe("buildAgentSystemPrompt", () => {
const prompt = buildAgentSystemPrompt({
workspaceDir: "/tmp/openclaw",
toolNames: ["sessions_spawn", "subagents", "agents_list", "exec"],
nativeCommandNames: ["codex"],
nativeCommandGuidanceLines: [
"Native Codex app-server plugin is available (`/codex ...`). For Codex bind/control/thread/resume/steer/stop requests, prefer `/codex bind`, `/codex threads`, `/codex resume`, `/codex steer`, and `/codex stop` over ACP.",
"Use ACP for Codex only when the user explicitly asks for ACP/acpx or wants to test the ACP path.",
],
acpEnabled: true,
});
@@ -1038,7 +1041,9 @@ describe("buildSubagentSystemPrompt", () => {
task: "research task",
childDepth: 1,
maxSpawnDepth: 2,
nativeCommandNames: ["codex"],
nativeCommandGuidanceLines: [
"Native Codex app-server plugin is available (`/codex ...`). Prefer that path for Codex bind/control/thread/resume/steer/stop requests; use Codex ACP only when explicitly requested.",
],
acpEnabled: true,
});

View File

@@ -381,14 +381,6 @@ function buildMessagingSection(params: {
];
}
function hasNativeCommand(params: { nativeCommandNames?: string[]; command: string }): boolean {
const target = normalizeLowercaseStringOrEmpty(params.command);
return (params.nativeCommandNames ?? []).some((name) => {
const normalized = normalizeLowercaseStringOrEmpty(name).replace(/^\/+/, "");
return normalized === target;
});
}
function buildVoiceSection(params: { isMinimal: boolean; ttsHint?: string }) {
if (params.isMinimal) {
return [];
@@ -472,6 +464,8 @@ export function buildAgentSystemPrompt(params: {
acpEnabled?: boolean;
/** Registered runtime slash/native command names such as `codex`. */
nativeCommandNames?: string[];
/** Plugin-owned prompt guidance for registered native slash commands. */
nativeCommandGuidanceLines?: string[];
runtimeInfo?: {
agentId?: string;
host?: string;
@@ -580,10 +574,9 @@ export function buildAgentSystemPrompt(params: {
const availableTools = new Set(normalizedTools);
const hasSessionsSpawn = availableTools.has("sessions_spawn");
const acpHarnessSpawnAllowed = hasSessionsSpawn && acpSpawnRuntimeEnabled;
const nativeCodexCommandAvailable = hasNativeCommand({
nativeCommandNames: params.nativeCommandNames,
command: "codex",
});
const nativeCommandGuidanceLines = Array.from(
new Set((params.nativeCommandGuidanceLines ?? []).map((line) => line.trim()).filter(Boolean)),
);
const externalToolSummaries = new Map<string, string>();
for (const [key, value] of Object.entries(params.toolSummaries ?? {})) {
const normalized = key.trim().toLowerCase();
@@ -733,12 +726,7 @@ export function buildAgentSystemPrompt(params: {
`For long waits, avoid rapid poll loops: use ${execToolName} with enough yieldMs or ${processToolName}(action=poll, timeout=<ms>).`,
"If a task is more complex or takes longer, spawn a sub-agent. Completion is push-based: it will auto-announce when done.",
'Sub-agents start isolated by default. Use `sessions_spawn` with `context:"fork"` only when the child needs the current transcript context; otherwise omit `context` or use `context:"isolated"`.',
...(nativeCodexCommandAvailable
? [
"Native Codex app-server plugin is available (`/codex ...`). For Codex bind/control/thread/resume/steer/stop requests, prefer `/codex bind`, `/codex threads`, `/codex resume`, `/codex steer`, and `/codex stop` over ACP.",
"Use ACP for Codex only when the user explicitly asks for ACP/acpx or wants to test the ACP path.",
]
: []),
...nativeCommandGuidanceLines,
...(acpHarnessSpawnAllowed
? [
'For requests like "do this in claude code/cursor/gemini/opencode" or similar ACP harnesses, treat it as ACP harness intent and call `sessions_spawn` with `runtime: "acp"`.',

View File

@@ -14,7 +14,7 @@ import { buildSystemPromptParams } from "../../agents/system-prompt-params.js";
import { buildAgentSystemPrompt } from "../../agents/system-prompt.js";
import type { WorkspaceBootstrapFile } from "../../agents/workspace.js";
import { getRemoteSkillEligibility } from "../../infra/skills-remote.js";
import { listRegisteredPluginCommands } from "../../plugins/command-registry-state.js";
import { listRegisteredPluginAgentPromptGuidance } from "../../plugins/command-registry-state.js";
import { buildTtsSystemPromptHint } from "../../tts/tts.js";
import type { HandleCommandsParams } from "./commands-types.js";
import { resolveRuntimePolicySessionKey } from "./runtime-policy-session-key.js";
@@ -168,7 +168,7 @@ export async function resolveCommandsSystemPromptBundle(
config: params.cfg,
sandboxed: sandboxRuntime.sandboxed,
}),
nativeCommandNames: listRegisteredPluginCommands().map((command) => command.name),
nativeCommandGuidanceLines: listRegisteredPluginAgentPromptGuidance(),
runtimeInfo,
sandboxInfo,
memoryCitationsMode: params.cfg?.memory?.citations,

View File

@@ -0,0 +1,34 @@
/**
* Deprecated bundled-channel compatibility surface.
*
* New plugins should define plugin-local schemas and import primitives from
* openclaw/plugin-sdk/channel-config-schema instead of depending on these
* bundled channel schemas.
*/
export {
AllowFromListSchema,
buildChannelConfigSchema,
buildCatchallMultiAccountChannelSchema,
buildNestedDmConfigSchema,
} from "../channels/plugins/config-schema.js";
export {
BlockStreamingCoalesceSchema,
ContextVisibilityModeSchema,
DmConfigSchema,
DmPolicySchema,
GroupPolicySchema,
MarkdownConfigSchema,
ReplyRuntimeConfigSchemaShape,
requireOpenAllowFrom,
} from "../config/zod-schema.core.js";
export { ToolPolicySchema } from "../config/zod-schema.agent-runtime.js";
export {
DiscordConfigSchema,
GoogleChatConfigSchema,
IMessageConfigSchema,
MSTeamsConfigSchema,
SignalConfigSchema,
SlackConfigSchema,
TelegramConfigSchema,
} from "../config/zod-schema.providers-core.js";
export { WhatsAppConfigSchema } from "../config/zod-schema.providers-whatsapp.js";

View File

@@ -16,16 +16,3 @@ export {
requireOpenAllowFrom,
} from "../config/zod-schema.core.js";
export { ToolPolicySchema } from "../config/zod-schema.agent-runtime.js";
// Legacy bundled channel schema exports. New channel plugins should define
// plugin-local schemas and expose JSON Schema through openclaw.plugin.json
// channelConfigs or a lightweight plugin-owned config artifact.
export {
DiscordConfigSchema,
GoogleChatConfigSchema,
IMessageConfigSchema,
MSTeamsConfigSchema,
SignalConfigSchema,
SlackConfigSchema,
TelegramConfigSchema,
} from "../config/zod-schema.providers-core.js";
export { WhatsAppConfigSchema } from "../config/zod-schema.providers-whatsapp.js";

View File

@@ -102,6 +102,17 @@ export function validatePluginCommandDefinition(
if (!command.description.trim()) {
return "Command description cannot be empty";
}
if (command.agentPromptGuidance !== undefined && !Array.isArray(command.agentPromptGuidance)) {
return "Agent prompt guidance must be an array of strings";
}
for (const [index, guidance] of (command.agentPromptGuidance ?? []).entries()) {
if (typeof guidance !== "string") {
return `Agent prompt guidance ${index + 1} must be a string`;
}
if (!guidance.trim()) {
return `Agent prompt guidance ${index + 1} cannot be empty`;
}
}
const nameError = validateCommandName(command.name.trim());
if (nameError) {
return nameError;
@@ -167,6 +178,9 @@ export function registerPluginCommand(
...command,
name,
description,
...(command.agentPromptGuidance
? { agentPromptGuidance: command.agentPromptGuidance.map((line) => line.trim()) }
: {}),
};
const invocationKeys = listPluginInvocationKeys(normalizedCommand);
const key = `/${normalizeLowercaseStringOrEmpty(name)}`;

View File

@@ -57,6 +57,22 @@ export function listRegisteredPluginCommands(): RegisteredPluginCommand[] {
return Array.from(pluginCommands.values());
}
export function listRegisteredPluginAgentPromptGuidance(): string[] {
const lines: string[] = [];
const seen = new Set<string>();
for (const command of pluginCommands.values()) {
for (const line of command.agentPromptGuidance ?? []) {
const trimmed = line.trim();
if (!trimmed || seen.has(trimmed)) {
continue;
}
seen.add(trimmed);
lines.push(trimmed);
}
}
return lines;
}
export function restorePluginCommands(commands: readonly RegisteredPluginCommand[]): void {
pluginCommands.clear();
for (const command of commands) {

View File

@@ -1,6 +1,7 @@
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js";
import { listRegisteredPluginAgentPromptGuidance } from "./command-registry-state.js";
import {
__testing,
clearPluginCommands,
@@ -250,6 +251,19 @@ describe("registerPluginCommand", () => {
error: "Command description must be a string",
},
},
{
name: "rejects invalid agent prompt guidance",
command: {
name: "demo",
description: "Demo",
agentPromptGuidance: "use /demo" as unknown as string[],
handler: async () => ({ text: "ok" }),
},
expected: {
ok: false,
error: "Agent prompt guidance must be an array of strings",
},
},
] as const)("$name", ({ command, expected }) => {
expect(registerPluginCommand("demo-plugin", command)).toEqual(expected);
});
@@ -258,6 +272,7 @@ describe("registerPluginCommand", () => {
const result = registerPluginCommand("demo-plugin", {
name: " demo_cmd ",
description: " Demo command ",
agentPromptGuidance: [" Use /demo_cmd for demo routing. "],
handler: async () => ({ text: "ok" }),
});
expect(result).toEqual({ ok: true });
@@ -276,6 +291,7 @@ describe("registerPluginCommand", () => {
acceptsArgs: false,
},
]);
expect(listRegisteredPluginAgentPromptGuidance()).toEqual(["Use /demo_cmd for demo routing."]);
});
it("matches underscore aliases for hyphenated command names", () => {

View File

@@ -33,7 +33,7 @@ const BUNDLED_EXTENSION_CONFIG_IMPORT_GUARDS = [
},
{
path: "extensions/googlechat/src/config-schema.ts",
allowedSpecifier: "openclaw/plugin-sdk/googlechat",
allowedSpecifier: "../runtime-api.js",
},
// Teams keeps a package-local config barrel so production code does not
// self-import via openclaw/plugin-sdk/msteams from inside the same extension.

View File

@@ -162,12 +162,15 @@ describe("config footprint guardrails", () => {
);
});
it("keeps bundled channel schemas as a fixed legacy SDK compatibility surface", () => {
it("keeps bundled channel schemas out of the generic channel config SDK surface", () => {
const source = readSource("src/plugin-sdk/channel-config-schema.ts");
const legacySection = source.slice(source.indexOf("Legacy bundled channel schema exports"));
const legacySource = readSource("src/plugin-sdk/channel-config-schema-legacy.ts");
const legacySection = legacySource.slice(
legacySource.indexOf("Deprecated bundled-channel compatibility surface"),
);
const bundledSchemaExportBlocks = Array.from(
legacySection.matchAll(
/export \{(?<exports>[\s\S]*?)\} from "\.\.\/config\/zod-schema\.providers-(?:core|whatsapp)\.js";/g,
/export \{(?<exports>[^}]*)\} from "\.\.\/config\/zod-schema\.providers-(?:core|whatsapp)\.js";/g,
),
)
.map((match) => match.groups?.exports)
@@ -190,6 +193,10 @@ describe("config footprint guardrails", () => {
"TelegramConfigSchema",
"WhatsAppConfigSchema",
]);
expect(source).toContain("Legacy bundled channel schema exports");
for (const schemaName of exportedSchemaNames) {
expect(source).not.toContain(schemaName);
}
expect(legacySource).toContain("Deprecated bundled-channel compatibility surface");
expect(legacySource).toContain("openclaw/plugin-sdk/channel-config-schema");
});
});

View File

@@ -1873,6 +1873,8 @@ export type OpenClawPluginCommandDefinition = {
};
/** Description shown in /help and command menus */
description: string;
/** Optional system-prompt guidance for agents when this command is registered. */
agentPromptGuidance?: readonly string[];
/** Whether this command accepts arguments */
acceptsArgs?: boolean;
/** Whether only authorized senders can use this command (default: true) */