fix(config): cap extension schema payloads

This commit is contained in:
Vincent Koc
2026-05-01 00:38:20 -07:00
parent ecf6cbf75d
commit 553e842fa6
3 changed files with 127 additions and 2 deletions

View File

@@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Google Meet: interrupt Realtime provider output when local barge-in clears playback, so command-pair audio stops model speech instead of only restarting Chrome playback. Fixes #73850. (#73834) Thanks @shhtheonlyperson.
- Gateway/config: cap oversized plugin-owned schemas in the full `config.schema` response so large installed plugin sets cannot balloon Gateway RSS or crash schema clients. Thanks @vincentkoc.
- Gateway/sessions: use bounded tail reads for sessions-list transcript usage fallbacks, keeping large session stores responsive when rows request derived previews. Thanks @vincentkoc.
- Gateway/chat: bound chat-history transcript reads to the requested display window so large session logs no longer OOM the Gateway when clients ask for a small history page. Thanks @vincentkoc.
- Voice Call/Twilio: honor stored pre-connect TwiML before realtime webhook shortcuts and reject DTMF sequences outside conversation mode, so Meet PIN entry cannot be skipped or silently dropped. Thanks @donkeykong91 and @PfanP.

View File

@@ -179,6 +179,63 @@ describe("config schema", () => {
expect(res.uiHints["channels.matrix.accessToken"]?.sensitive).toBe(true);
});
it("omits a single oversized plugin schema from the full schema response", () => {
const res = buildConfigSchema({
cache: false,
plugins: [
{
id: "huge",
name: "Huge",
configSchema: {
type: "object",
properties: {
huge: {
type: "string",
description: `oversized-marker-${"x".repeat(300_000)}`,
},
},
},
},
],
});
const serialized = JSON.stringify(res);
expect(serialized).not.toContain("oversized-marker");
const lookup = lookupConfigSchema(res, "plugins.entries.huge.config");
expect(lookup?.schema).toMatchObject({
type: "object",
additionalProperties: true,
description: expect.stringContaining("omitted"),
});
});
it("omits later plugin schemas after the aggregate extension schema budget is exhausted", () => {
const res = buildConfigSchema({
cache: false,
plugins: Array.from({ length: 40 }, (_, index) => ({
id: `plugin-${index}`,
configSchema: {
type: "object",
properties: {
value: {
type: "string",
description: `schema-${index}-${"x".repeat(60_000)}`,
},
},
},
})),
});
const first = lookupConfigSchema(res, "plugins.entries.plugin-0.config.value");
const last = lookupConfigSchema(res, "plugins.entries.plugin-39.config");
expect(first?.schema).toMatchObject({ type: "string" });
expect(last?.schema).toMatchObject({
type: "object",
additionalProperties: true,
description: expect.stringContaining("omitted"),
});
});
it("looks up plugin config paths for slash-delimited plugin ids", () => {
const res = buildConfigSchema({
plugins: [

View File

@@ -138,6 +138,71 @@ export type ChannelUiMetadata = {
configUiHints?: Record<string, ConfigUiHint>;
};
const EXTENSION_SCHEMA_MAX_BYTES = 256 * 1024;
const EXTENSION_SCHEMA_TOTAL_MAX_BYTES = 2 * 1024 * 1024;
const EXTENSION_SCHEMA_MAX_ITEMS = 256;
function schemaJsonBytes(schema: JsonSchemaNode): number {
try {
return Buffer.byteLength(JSON.stringify(schema), "utf-8");
} catch {
return Number.POSITIVE_INFINITY;
}
}
function buildOmittedExtensionConfigSchema(kind: "plugin" | "channel", id: string): JsonSchemaNode {
return {
type: "object",
additionalProperties: true,
description: `${kind} config schema for ${id} was omitted from the full config.schema response because installed extension schemas exceeded the Gateway response budget.`,
};
}
function limitExtensionSchemas(params: {
plugins: PluginUiMetadata[];
channels: ChannelUiMetadata[];
}): { plugins: PluginUiMetadata[]; channels: ChannelUiMetadata[] } {
let totalBytes = 0;
let includedItems = 0;
const keepSchema = (schema: JsonSchemaNode): boolean => {
const bytes = schemaJsonBytes(schema);
if (
!Number.isFinite(bytes) ||
bytes > EXTENSION_SCHEMA_MAX_BYTES ||
totalBytes + bytes > EXTENSION_SCHEMA_TOTAL_MAX_BYTES ||
includedItems >= EXTENSION_SCHEMA_MAX_ITEMS
) {
return false;
}
totalBytes += bytes;
includedItems += 1;
return true;
};
const plugins = params.plugins.map((plugin) => {
if (!plugin.configSchema || keepSchema(plugin.configSchema)) {
return plugin;
}
return {
...plugin,
configSchema: buildOmittedExtensionConfigSchema("plugin", plugin.id),
};
});
const channels = params.channels.map((channel) => {
if (!channel.configSchema || keepSchema(channel.configSchema)) {
return channel;
}
return {
...channel,
configSchema: buildOmittedExtensionConfigSchema("channel", channel.id),
};
});
return { plugins, channels };
}
function collectExtensionHintKeys(
hints: ConfigUiHints,
plugins: PluginUiMetadata[],
@@ -487,8 +552,10 @@ export function buildConfigSchema(params?: {
cache?: boolean;
}): ConfigSchemaResponse {
const base = buildBaseConfigSchema();
const plugins = params?.plugins ?? [];
const channels = params?.channels ?? [];
const { plugins, channels } = limitExtensionSchemas({
plugins: params?.plugins ?? [],
channels: params?.channels ?? [],
});
if (plugins.length === 0 && channels.length === 0) {
return base;
}