mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
fix(config): cap extension schema payloads
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user