Skip to content

Commit 7404b2b

Browse files
authored
fix(channels): keep contributed message-tool schema properties optional (#91137)
Co-authored-by: Lundog <[email protected]>
1 parent 73aabcc commit 7404b2b

2 files changed

Lines changed: 50 additions & 3 deletions

File tree

src/channels/plugins/message-action-discovery.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
77
import { uniqueStrings } from "@openclaw/normalization-core/string-normalization";
8-
import type { TSchema } from "typebox";
8+
import { Type, type TSchema } from "typebox";
99
import type { OpenClawConfig } from "../../config/types.openclaw.js";
1010
import { formatErrorMessage } from "../../infra/errors.js";
1111
import { defaultRuntime } from "../../runtime.js";
@@ -361,9 +361,14 @@ function mergeToolSchemaProperties(
361361
return;
362362
}
363363
for (const [name, schema] of Object.entries(source)) {
364-
if (!(name in target)) {
365-
target[name] = schema;
364+
if (name in target) {
365+
continue;
366366
}
367+
// Message-tool params dispatch on `action`; no contributed property may be
368+
// object-level required. Type.Object treats schemas missing typebox's
369+
// non-enumerable `~optional` marker (plain JSON or cloned/serialized plugin
370+
// schemas) as required, which fails validation for every message call.
371+
target[name] = Type.IsOptional(schema) ? schema : Type.Optional(schema);
367372
}
368373
}
369374

src/channels/plugins/message-actions.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,48 @@ describe("message action capability checks", () => {
194194
).toHaveProperty("components");
195195
});
196196

197+
it("keeps contributed schema properties optional so only action stays required", () => {
198+
const contributingPlugin: ChannelPlugin = {
199+
...createChannelTestPluginBase({
200+
id: "demo-contrib",
201+
label: "Demo Contrib",
202+
capabilities: { chatTypes: ["direct", "group"] },
203+
config: {
204+
listAccountIds: () => ["default"],
205+
},
206+
}),
207+
actions: {
208+
describeMessageTool: () => ({
209+
actions: ["send"],
210+
schema: {
211+
properties: {
212+
// Non-optional TypeBox schema: plugin forgot Type.Optional.
213+
components: Type.Array(Type.String()),
214+
// Cloning strips typebox's non-enumerable `~optional` marker;
215+
// mirrors serialized/external plugin contributions.
216+
chatRef: structuredClone(Type.Optional(Type.String())),
217+
media: Type.Optional(Type.String()),
218+
},
219+
},
220+
}),
221+
},
222+
};
223+
setActivePluginRegistry(
224+
createTestRegistry([
225+
{ pluginId: "demo-contrib", source: "test", plugin: contributingPlugin },
226+
]),
227+
);
228+
229+
const properties = resolveChannelMessageToolSchemaProperties({
230+
cfg: {} as OpenClawConfig,
231+
channel: "demo-contrib",
232+
});
233+
// Regression: required leakage made every message tool call fail validation
234+
// with "must have required properties chatRef, media, ...".
235+
const toolSchema = Type.Object({ action: Type.String(), ...properties });
236+
expect(toolSchema.required).toEqual(["action"]);
237+
});
238+
197239
it("filters only actions that depend on current-channel-only schema", () => {
198240
const scopedSchemaPlugin: ChannelPlugin = {
199241
...createChannelTestPluginBase({

0 commit comments

Comments
 (0)