refactor(plugins): make manifests own config ui hints (#121329)

This commit is contained in:
Peter Steinberger
2026-08-09 18:48:49 -07:00
committed by GitHub
parent a6c410ef51
commit 620fcd0e27
16 changed files with 122 additions and 432 deletions
+24
View File
@@ -16,6 +16,30 @@
"configContracts": {
"compatibilityMigrationPaths": ["canvasHost"]
},
"uiHints": {
"host": {
"label": "Canvas Host",
"help": "Serves local Canvas and A2UI files for paired nodes.",
"advanced": true
},
"host.enabled": {
"label": "Canvas Host Enabled",
"advanced": true
},
"host.root": {
"label": "Canvas Host Root Directory",
"help": "Directory to serve. Defaults to the OpenClaw state canvas directory.",
"advanced": true
},
"host.port": {
"label": "Canvas Host Port",
"advanced": true
},
"host.liveReload": {
"label": "Canvas Host Live Reload",
"advanced": true
}
},
"configSchema": {
"type": "object",
"additionalProperties": false,
+34
View File
@@ -1,6 +1,8 @@
// Canvas tests cover config plugin behavior.
import { readFileSync } from "node:fs";
import { afterEach, describe, expect, it } from "vitest";
import {
canvasConfigSchema,
isCanvasHostEnabled,
isCanvasPluginEnabled,
parseCanvasPluginConfig,
@@ -18,6 +20,38 @@ describe("Canvas plugin config", () => {
}
});
it("keeps host config presentation metadata manifest-owned", () => {
const manifest = JSON.parse(
readFileSync(new URL("../openclaw.plugin.json", import.meta.url), "utf8"),
) as { uiHints?: Record<string, Record<string, unknown>> };
expect(canvasConfigSchema).not.toHaveProperty("uiHints");
expect(manifest.uiHints).toEqual({
host: {
label: "Canvas Host",
help: "Serves local Canvas and A2UI files for paired nodes.",
advanced: true,
},
"host.enabled": {
label: "Canvas Host Enabled",
advanced: true,
},
"host.root": {
label: "Canvas Host Root Directory",
help: "Directory to serve. Defaults to the OpenClaw state canvas directory.",
advanced: true,
},
"host.port": {
label: "Canvas Host Port",
advanced: true,
},
"host.liveReload": {
label: "Canvas Host Live Reload",
advanced: true,
},
});
});
it("parses host config from the plugin entry", () => {
expect(
parseCanvasPluginConfig({
+1 -26
View File
@@ -29,7 +29,6 @@ export type CanvasPluginConfig = {
type CanvasPluginConfigSchema = {
parse: (value: unknown) => CanvasPluginConfig;
uiHints: Record<string, { label: string; help?: string; advanced?: boolean }>;
};
function readPositiveInteger(value: unknown): number | undefined {
@@ -97,31 +96,7 @@ export function isCanvasHostEnabled(config?: OpenClawConfig): boolean {
return resolveCanvasHostConfig({ config }).enabled !== false;
}
/** Config schema metadata for Canvas plugin settings. */
/** Runtime config parser for Canvas plugin settings. */
export const canvasConfigSchema: CanvasPluginConfigSchema = {
parse: parseCanvasPluginConfig,
uiHints: {
host: {
label: "Canvas Host",
help: "Serves local Canvas and A2UI files for paired nodes.",
advanced: true,
},
"host.enabled": {
label: "Canvas Host Enabled",
advanced: true,
},
"host.root": {
label: "Canvas Host Root Directory",
help: "Directory to serve. Defaults to the OpenClaw state canvas directory.",
advanced: true,
},
"host.port": {
label: "Canvas Host Port",
advanced: true,
},
"host.liveReload": {
label: "Canvas Host Live Reload",
advanced: true,
},
},
};
+1
View File
@@ -50,6 +50,7 @@ describe("cua-computer plugin registration", () => {
expect(validateManifestConfig(config).ok).toBe(true);
expect(plugin.configSchema.safeParse?.({ unexpected: true }).success).toBe(false);
expect(validateManifestConfig({ unexpected: true }).ok).toBe(false);
expect(plugin.configSchema).not.toHaveProperty("uiHints");
const commands: OpenClawPluginNodeHostCommand[] = [];
plugin.register({
+1 -3
View File
@@ -8,9 +8,7 @@ const CuaComputerConfigSchema = z.strictObject({
driverPath: z.string().optional(),
});
const configSchema = buildPluginConfigSchema(CuaComputerConfigSchema, {
uiHints: {},
});
const configSchema = buildPluginConfigSchema(CuaComputerConfigSchema);
export default definePluginEntry({
id: "cua-computer",
+4 -7
View File
@@ -1240,7 +1240,7 @@ describe("google-meet plugin", () => {
await transcriptionHandle.stop();
});
it("declares advanced config metadata in the plugin entry and manifest", () => {
it("keeps advanced config metadata manifest-owned", () => {
const manifest = JSON.parse(
readFileSync(new URL("./openclaw.plugin.json", import.meta.url), "utf8"),
) as {
@@ -1248,11 +1248,9 @@ describe("google-meet plugin", () => {
configSchema?: GoogleMeetManifestConfigSchema;
};
const configSchema = requireGoogleMeetManifestConfigSchema(manifest);
const entry = plugin as unknown as {
configSchema: {
uiHints?: Record<string, unknown>;
};
};
const entry = plugin as unknown as { configSchema: Record<string, unknown> };
expect(entry.configSchema).not.toHaveProperty("uiHints");
for (const key of [
"chrome.audioBufferBytes",
@@ -1263,7 +1261,6 @@ describe("google-meet plugin", () => {
"voiceCall.dtmfDelayMs",
"voiceCall.postDtmfSpeechDelayMs",
]) {
expect(entry.configSchema.uiHints?.[key]).toHaveProperty("advanced", true);
expect(manifest.uiHints?.[key]).toHaveProperty("advanced", true);
}
const chromeProperties = configSchema.properties?.chrome?.properties;
-169
View File
@@ -6,175 +6,6 @@ export const googleMeetConfigSchema = {
parse(value: unknown) {
return resolveGoogleMeetConfig(value);
},
uiHints: {
"defaults.meeting": {
label: "Default Meeting",
help: "Meet URL, meeting code, or spaces/{id} used when CLI commands omit a meeting.",
},
"preview.enrollmentAcknowledged": {
label: "Preview Acknowledged",
help: "Confirms you understand the Google Meet Media API is still Developer Preview.",
advanced: true,
},
defaultTransport: {
label: "Default Transport",
help: "Chrome uses a signed-in browser profile. Chrome-node runs Chrome on a paired node. Twilio uses Meet dial-in numbers.",
},
defaultMode: {
label: "Default Mode",
help: "Agent uses realtime transcription plus regular OpenClaw TTS. Bidi uses the realtime voice model directly. Transcribe observes only.",
},
"chrome.audioBackend": {
label: "Chrome Audio Backend",
help: "Auto selects BlackHole 2ch on macOS or PipeWire-Pulse on Linux.",
},
"chrome.launch": { label: "Launch Chrome" },
"chrome.browserProfile": { label: "Chrome Profile", advanced: true },
"chrome.guestName": {
label: "Guest Name",
help: "Used when Chrome lands on the signed-out Meet guest-name screen.",
},
"chrome.reuseExistingTab": {
label: "Reuse Existing Meet Tab",
help: "Avoids opening duplicate tabs for the same Meet URL.",
},
"chrome.autoJoin": {
label: "Auto Join Guest Screen",
help: "Best-effort guest-name fill and Join Now click through OpenClaw browser automation.",
},
"chrome.waitForInCallMs": {
label: "Wait For In-Call (ms)",
help: "Waits for Chrome to report that the Meet tab is in-call before the realtime intro speaks.",
advanced: true,
},
"chrome.audioFormat": {
label: "Audio Format",
help: "Command-pair audio format. PCM16 24 kHz is the default Chrome/Meet path; G.711 mu-law 8 kHz remains available for legacy command pairs.",
advanced: true,
},
"chrome.audioBufferBytes": {
label: "Audio Buffer Bytes",
help: "Processing buffer for generated Chrome command-pair audio. Lower values reduce latency but may underrun on busy hosts.",
advanced: true,
},
"chrome.audioInputCommand": {
label: "Audio Input Command",
help: "Command that writes meeting audio to stdout in chrome.audioFormat.",
advanced: true,
},
"chrome.audioOutputCommand": {
label: "Audio Output Command",
help: "Command that reads assistant audio from stdin in chrome.audioFormat.",
advanced: true,
},
"chrome.bargeInInputCommand": {
label: "Barge-In Input Command",
help: "Optional Gateway-hosted microphone command that writes signed 16-bit little-endian mono PCM for human interruption detection while assistant playback is active.",
advanced: true,
},
"chrome.bargeInRmsThreshold": {
label: "Barge-In RMS Threshold",
help: "RMS level on chrome.bargeInInputCommand that counts as a human interruption.",
advanced: true,
},
"chrome.bargeInPeakThreshold": {
label: "Barge-In Peak Threshold",
help: "Peak level on chrome.bargeInInputCommand that counts as a human interruption.",
advanced: true,
},
"chrome.bargeInCooldownMs": {
label: "Barge-In Cooldown (ms)",
help: "Minimum delay between repeated barge-in clears.",
advanced: true,
},
"chrome.audioBridgeCommand": { label: "Audio Bridge Command", advanced: true },
"chrome.audioBridgeHealthCommand": {
label: "Audio Bridge Health Command",
advanced: true,
},
"chromeNode.node": {
label: "Chrome Node",
help: "Node id/name/IP that owns Chrome and the native virtual-audio backend for chrome-node transport.",
advanced: true,
},
"twilio.defaultDialInNumber": {
label: "Default Dial-In Number",
placeholder: "+15551234567",
},
"twilio.defaultPin": { label: "Default PIN", advanced: true },
"twilio.defaultDtmfSequence": { label: "Default DTMF Sequence", advanced: true },
"voiceCall.enabled": { label: "Delegate To Voice Call" },
"voiceCall.gatewayUrl": { label: "Voice Call Gateway URL", advanced: true },
"voiceCall.token": {
label: "Voice Call Gateway Token",
sensitive: true,
advanced: true,
},
"voiceCall.requestTimeoutMs": {
label: "Voice Call Request Timeout (ms)",
advanced: true,
},
"voiceCall.dtmfDelayMs": {
label: "DTMF Wait Before PIN (ms)",
help: "Leading Twilio wait time before playing a PIN-derived Meet DTMF sequence. Increase it if Meet asks for the PIN after DTMF was sent.",
advanced: true,
},
"voiceCall.postDtmfSpeechDelayMs": {
label: "Post-DTMF Speech Delay (ms)",
help: "Delay before requesting the realtime intro greeting after Voice Call starts the Twilio leg.",
advanced: true,
},
"voiceCall.introMessage": { label: "Voice Call Intro Message", advanced: true },
"realtime.strategy": {
label: "Realtime Strategy",
help: "Legacy realtime alias setting. Use mode=agent or mode=bidi for new Meet joins.",
},
"realtime.provider": {
label: "Speech Provider",
help: "Compatibility fallback for both realtime transcription and bidi voice. Prefer realtime.transcriptionProvider and realtime.voiceProvider for new configs.",
},
"realtime.transcriptionProvider": {
label: "Realtime Transcription Provider",
help: "Agent mode uses this provider to transcribe meeting audio before regular OpenClaw TTS answers.",
},
"realtime.voiceProvider": {
label: "Bidi Voice Provider",
help: "Bidi mode uses this realtime voice provider. Falls back to realtime.provider when unset.",
},
"realtime.model": {
label: "Bidi Realtime Model",
help: "Only used by mode=bidi. Agent mode answers with the configured OpenClaw agent and regular TTS.",
advanced: true,
},
"realtime.instructions": { label: "Realtime Instructions", advanced: true },
"realtime.introMessage": {
label: "Realtime Intro Message",
help: "Spoken once when the realtime bridge is ready. Set to an empty string to join silently.",
},
"realtime.agentId": {
label: "Realtime Consult Agent",
help: 'OpenClaw agent id used by openclaw_agent_consult. Defaults to "main".',
advanced: true,
},
"realtime.toolPolicy": {
label: "Realtime Tool Policy",
help: "Safe read-only tools are available by default; owner requests can unlock broader tools.",
advanced: true,
},
"oauth.clientId": { label: "OAuth Client ID" },
"oauth.clientSecret": { label: "OAuth Client Secret", sensitive: true },
"oauth.refreshToken": { label: "OAuth Refresh Token", sensitive: true },
"oauth.accessToken": {
label: "Cached Access Token",
sensitive: true,
advanced: true,
},
"oauth.expiresAt": {
label: "Cached Access Token Expiry",
help: "Unix epoch milliseconds used only for the cached access-token fast path.",
advanced: true,
},
},
};
export const GoogleMeetToolSchema = Type.Object({
+3 -1
View File
@@ -28,7 +28,8 @@ describe("linux-node config", () => {
});
it("exports the same strict shape through the plugin schema", () => {
const safeParse = createLinuxNodePluginConfigSchema().safeParse;
const schema = createLinuxNodePluginConfigSchema();
const safeParse = schema.safeParse;
if (!safeParse) {
throw new Error("missing config schema validator");
}
@@ -37,5 +38,6 @@ describe("linux-node config", () => {
unexpected: true,
});
expect(result.success).toBe(false);
expect(schema).not.toHaveProperty("uiHints");
});
});
+1 -16
View File
@@ -19,22 +19,7 @@ export type ResolvedLinuxNodePluginConfig = {
};
export function createLinuxNodePluginConfigSchema() {
return buildPluginConfigSchema(LinuxNodePluginConfigSchema, {
uiHints: {
"notify.enabled": {
label: "Desktop Notifications",
help: "Expose system.notify when notify-send is installed. Enabled by default.",
},
"camera.enabled": {
label: "Camera",
help: "Expose camera commands when FFmpeg is installed. Requires a node service restart.",
},
"location.enabled": {
label: "Location",
help: "Expose location.get when the GeoClue where-am-i demo is installed. Requires a node service restart.",
},
},
});
return buildPluginConfigSchema(LinuxNodePluginConfigSchema);
}
export function resolveLinuxNodePluginConfig(value: unknown): ResolvedLinuxNodePluginConfig {
+9 -1
View File
@@ -9,9 +9,17 @@ import { memoryConfigSchema } from "./config.js";
const manifest = JSON.parse(
fs.readFileSync(new URL("./openclaw.plugin.json", import.meta.url), "utf-8"),
) as { configSchema: JsonSchemaObject };
) as { configSchema: JsonSchemaObject; uiHints?: Record<string, unknown> };
describe("memory-lancedb config", () => {
it("keeps config presentation metadata manifest-owned", () => {
expect(memoryConfigSchema).not.toHaveProperty("uiHints");
expect(manifest.uiHints?.["embedding.apiKey"]).toMatchObject({
label: "Embedding API Key",
sensitive: true,
});
});
it("accepts dreaming in the manifest schema and preserves it in runtime parsing", () => {
const manifestResult = validateJsonSchemaValue({
schema: manifest.configSchema,
-67
View File
@@ -226,71 +226,4 @@ export const memoryConfigSchema = {
...(storageOptions ? { storageOptions } : {}),
};
},
uiHints: {
"embedding.provider": {
label: "Embedding Provider",
placeholder: "openai",
help: "Memory embedding provider adapter to use (for example openai, github-copilot, ollama)",
},
"embedding.apiKey": {
label: "OpenAI API Key",
sensitive: true,
placeholder: "sk-proj-...",
help: "Optional API key override for OpenAI-compatible embeddings; omit to use configured provider auth",
},
"embedding.baseUrl": {
label: "Base URL",
placeholder: "https://api.openai.com/v1",
help: "Optional provider or OpenAI-compatible embedding endpoint base URL",
advanced: true,
},
"embedding.dimensions": {
label: "Dimensions",
placeholder: "1536",
help: "Vector dimensions for custom models (required for non-standard models)",
advanced: true,
},
"embedding.model": {
label: "Embedding Model",
placeholder: DEFAULT_MODEL,
help: "OpenAI embedding model to use",
},
dbPath: {
label: "Database Path",
placeholder: "~/.openclaw/memory/lancedb",
advanced: true,
help: "Local filesystem path or cloud storage URI (s3://, gs://) for LanceDB database",
},
autoCapture: {
label: "Auto-Capture",
help: "Automatically capture important information from conversations",
},
autoRecall: {
label: "Auto-Recall",
help: "Automatically inject relevant memories into context",
},
captureMaxChars: {
label: "Capture Max Chars",
help: "Maximum message length eligible for auto-capture",
advanced: true,
placeholder: String(DEFAULT_CAPTURE_MAX_CHARS),
},
customTriggers: {
label: "Custom Triggers",
help: "Literal phrases that should make auto-capture consider a message memory-worthy",
advanced: true,
},
recallMaxChars: {
label: "Recall Query Max Chars",
help: "Maximum prompt/query length embedded for memory recall. Lower for small local embedding models.",
advanced: true,
placeholder: String(DEFAULT_RECALL_MAX_CHARS),
},
storageOptions: {
label: "Storage Options",
sensitive: true,
advanced: true,
help: "Storage configuration options (access_key, secret_key, endpoint, etc.); supports ${ENV_VAR} values",
},
},
};
+27
View File
@@ -260,6 +260,33 @@ describe("voice-call plugin", () => {
];
});
it("keeps config presentation metadata manifest-owned", () => {
const manifest = JSON.parse(
fs.readFileSync(new URL("./openclaw.plugin.json", import.meta.url), "utf8"),
) as {
uiHints?: Record<string, Record<string, unknown>>;
configSchema?: { properties?: Record<string, unknown> };
};
const entry = plugin as unknown as { configSchema: Record<string, unknown> };
expect(entry.configSchema).not.toHaveProperty("uiHints");
expect(manifest.uiHints?.agentId).toEqual({
label: "Response Agent ID",
help: 'Agent workspace used for voice response generation. Defaults to "main".',
advanced: true,
});
expect(manifest.configSchema?.properties?.agentId).toEqual({
type: "string",
minLength: 1,
});
expect(manifest.uiHints?.["realtime.consultThinkingLevel"]).toHaveProperty("advanced", true);
expect(manifest.uiHints?.["realtime.consultFastMode"]).toHaveProperty("advanced", true);
expect(manifest.uiHints?.sessionScope).toMatchObject({
label: "Session Scope",
help: expect.stringContaining("per-phone"),
});
});
it("defaults canonical plugin config to an enabled mock runtime", async () => {
const { service } = setup({});
-140
View File
@@ -43,146 +43,6 @@ const voiceCallConfigSchema = {
provider: config.provider ?? (enabled ? "mock" : undefined),
});
},
uiHints: {
provider: {
label: "Provider",
help: "Use twilio, telnyx, or mock for dev/no-network.",
},
fromNumber: { label: "From Number", placeholder: "+15550001234" },
toNumber: { label: "Default To Number", placeholder: "+15550001234" },
inboundPolicy: { label: "Inbound Policy" },
allowFrom: { label: "Inbound Allowlist" },
inboundGreeting: { label: "Inbound Greeting", advanced: true },
numbers: {
label: "Per-number Routing",
help: "Inbound overrides keyed by dialed E.164 number.",
advanced: true,
},
"telnyx.apiKey": { label: "Telnyx API Key", sensitive: true },
"telnyx.connectionId": { label: "Telnyx Connection ID" },
"telnyx.publicKey": { label: "Telnyx Public Key", sensitive: true },
"twilio.accountSid": { label: "Twilio Account SID" },
"twilio.authToken": { label: "Twilio Auth Token", sensitive: true },
"twilio.region": { label: "Twilio Region", advanced: true },
"outbound.defaultMode": { label: "Default Call Mode" },
"outbound.notifyHangupDelaySec": {
label: "Notify Hangup Delay (sec)",
advanced: true,
},
"serve.port": { label: "Webhook Port" },
"serve.bind": { label: "Webhook Bind" },
"serve.path": { label: "Webhook Path" },
"tailscale.mode": { label: "Tailscale Mode", advanced: true },
"tailscale.path": { label: "Tailscale Path", advanced: true },
"tunnel.provider": { label: "Tunnel Provider", advanced: true },
"tunnel.ngrokAuthToken": {
label: "ngrok Auth Token",
sensitive: true,
advanced: true,
},
"tunnel.ngrokDomain": { label: "ngrok Domain", advanced: true },
"tunnel.allowNgrokFreeTierLoopbackBypass": {
label: "Allow ngrok Free Tier (Loopback Bypass)",
advanced: true,
},
"streaming.enabled": {
label: "Enable Streaming",
help: "Classic streaming transcription currently requires the Twilio call provider.",
advanced: true,
},
"streaming.provider": {
label: "Streaming Provider",
help: "Uses the first registered realtime transcription provider when unset.",
advanced: true,
},
"streaming.providers": { label: "Streaming Provider Config", advanced: true },
"streaming.streamPath": { label: "Media Stream Path", advanced: true },
"realtime.enabled": { label: "Enable Realtime Voice", advanced: true },
"realtime.provider": {
label: "Realtime Voice Provider",
help: "Uses the first registered realtime voice provider when unset.",
advanced: true,
},
"realtime.streamPath": { label: "Realtime Stream Path", advanced: true },
"realtime.instructions": { label: "Realtime Instructions", advanced: true },
"realtime.toolPolicy": {
label: "Realtime Tool Policy",
help: "Controls the shared openclaw_agent_consult tool.",
advanced: true,
},
"realtime.consultPolicy": {
label: "Realtime Consult Policy",
help: "Guides when the realtime voice model should call openclaw_agent_consult.",
advanced: true,
},
"realtime.fastContext.enabled": {
label: "Enable Fast Realtime Context",
help: "Searches memory/session context before the full consult agent.",
advanced: true,
},
"realtime.fastContext.timeoutMs": {
label: "Fast Context Timeout",
advanced: true,
},
"realtime.fastContext.maxResults": {
label: "Fast Context Result Limit",
advanced: true,
},
"realtime.fastContext.sources": {
label: "Fast Context Sources",
advanced: true,
},
"realtime.fastContext.fallbackToConsult": {
label: "Fallback To Full Consult",
advanced: true,
},
"realtime.agentContext.enabled": {
label: "Enable Agent Voice Context",
help: "Injects a compact agent identity and workspace context capsule into realtime voice instructions.",
advanced: true,
},
"realtime.agentContext.maxChars": {
label: "Agent Voice Context Limit",
advanced: true,
},
"realtime.agentContext.includeIdentity": {
label: "Include Agent Identity",
advanced: true,
},
"realtime.agentContext.includeWorkspaceFiles": {
label: "Include Agent Workspace Files",
advanced: true,
},
"realtime.agentContext.files": {
label: "Agent Voice Context Files",
advanced: true,
},
"realtime.providers": { label: "Realtime Provider Config", advanced: true },
"tts.provider": {
label: "TTS Provider Override",
help: "Deep-merges with tts (Microsoft is ignored for calls).",
advanced: true,
},
"tts.providers": { label: "TTS Provider Config", advanced: true },
publicUrl: { label: "Public Webhook URL", advanced: true },
skipSignatureVerification: {
label: "Skip Signature Verification",
advanced: true,
},
store: { label: "Call Log Store Path", advanced: true },
agentId: {
label: "Response Agent ID",
help: 'Agent workspace used for voice response generation. Defaults to "main".',
advanced: true,
},
responseModel: {
label: "Response Model",
help: "Optional override. Falls back to the runtime default model when unset.",
advanced: true,
},
responseSystemPrompt: { label: "Response System Prompt", advanced: true },
responseTimeoutMs: { label: "Response Timeout (ms)", advanced: true },
},
};
const VoiceCallToolSchema = Type.Union([
@@ -228,6 +228,11 @@
"label": "Call Log Store Path",
"advanced": true
},
"agentId": {
"label": "Response Agent ID",
"help": "Agent workspace used for voice response generation. Defaults to \"main\".",
"advanced": true
},
"sessionScope": {
"label": "Session Scope",
"help": "Use per-phone to preserve caller memory across calls, or per-call to isolate every call into a fresh voice session."
@@ -883,6 +888,10 @@
"store": {
"type": "string"
},
"agentId": {
"type": "string",
"minLength": 1
},
"sessionScope": {
"type": "string",
"enum": ["per-phone", "per-call"]
+2
View File
@@ -17,12 +17,14 @@ type ZodSchemaWithToJsonSchema = ZodTypeAny & {
};
type BuildPluginConfigSchemaOptions = {
/** @deprecated Declare top-level `uiHints` in `openclaw.plugin.json`. */
uiHints?: Record<string, PluginConfigUiHint>;
safeParse?: OpenClawPluginConfigSchema["safeParse"];
};
type BuildJsonPluginConfigSchemaOptions = {
cacheKey?: string;
/** @deprecated Declare top-level `uiHints` in `openclaw.plugin.json`. */
uiHints?: Record<string, PluginConfigUiHint>;
safeParse?: OpenClawPluginConfigSchema["safeParse"];
};
+6 -2
View File
@@ -7,8 +7,7 @@ type PluginConfigValidation = { ok: true; value?: unknown } | { ok: false; error
* Config schema contract accepted by plugin manifests and runtime registration.
*
* Plugins can provide a Zod-like parser, a lightweight `validate(...)`
* function, or both. `uiHints` and `jsonSchema` are optional extras for docs,
* forms, and config UIs.
* function, or both. `jsonSchema` is optional runtime schema metadata.
*/
export type OpenClawPluginConfigSchema = {
safeParse?: (value: unknown) => {
@@ -20,6 +19,11 @@ export type OpenClawPluginConfigSchema = {
};
parse?: (value: unknown) => unknown;
validate?: (value: unknown) => PluginConfigValidation;
/**
* @deprecated Declare config presentation metadata in the plugin's
* `openclaw.plugin.json` manifest via top-level `uiHints`. The host reads
* manifest hints and does not consume runtime config-schema hints.
*/
uiHints?: Record<string, PluginConfigUiHint>;
jsonSchema?: JsonSchemaObject;
};