Skip to content

Commit 61cf005

Browse files
committed
fix: normalize canvas numeric params
1 parent 61c538e commit 61cf005

3 files changed

Lines changed: 91 additions & 28 deletions

File tree

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
import {
2+
optionalFiniteNumberSchema,
3+
optionalNonNegativeIntegerSchema,
4+
optionalPositiveIntegerSchema,
5+
stringEnum,
6+
} from "openclaw/plugin-sdk/channel-actions";
17
import { Type } from "typebox";
28

39
export const CANVAS_ACTIONS = [
@@ -12,30 +18,23 @@ export const CANVAS_ACTIONS = [
1218

1319
export const CANVAS_SNAPSHOT_FORMATS = ["png", "jpg", "jpeg"] as const;
1420

15-
function stringEnum<T extends readonly string[]>(values: T) {
16-
return Type.Unsafe<T[number]>({
17-
type: "string",
18-
enum: [...values],
19-
});
20-
}
21-
2221
export const CanvasToolSchema = Type.Object({
2322
action: stringEnum(CANVAS_ACTIONS),
2423
gatewayUrl: Type.Optional(Type.String()),
2524
gatewayToken: Type.Optional(Type.String()),
26-
timeoutMs: Type.Optional(Type.Number()),
25+
timeoutMs: optionalPositiveIntegerSchema(),
2726
node: Type.Optional(Type.String()),
2827
target: Type.Optional(Type.String()),
29-
x: Type.Optional(Type.Number()),
30-
y: Type.Optional(Type.Number()),
31-
width: Type.Optional(Type.Number()),
32-
height: Type.Optional(Type.Number()),
28+
x: optionalFiniteNumberSchema(),
29+
y: optionalFiniteNumberSchema(),
30+
width: optionalFiniteNumberSchema(),
31+
height: optionalFiniteNumberSchema(),
3332
url: Type.Optional(Type.String()),
3433
javaScript: Type.Optional(Type.String()),
3534
outputFormat: Type.Optional(stringEnum(CANVAS_SNAPSHOT_FORMATS)),
36-
maxWidth: Type.Optional(Type.Number()),
37-
quality: Type.Optional(Type.Number()),
38-
delayMs: Type.Optional(Type.Number()),
35+
maxWidth: optionalPositiveIntegerSchema(),
36+
quality: optionalFiniteNumberSchema({ minimum: 0, maximum: 1 }),
37+
delayMs: optionalNonNegativeIntegerSchema(),
3938
jsonl: Type.Optional(Type.String()),
4039
jsonlPath: Type.Optional(Type.String()),
4140
});

extensions/canvas/src/tool.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,72 @@ describe("Canvas tool", () => {
9797
expect(imageResultParams?.imageSanitization).toEqual({ maxDimensionPx: 1600 });
9898
});
9999

100+
it("normalizes numeric string params before invoking node canvas commands", async () => {
101+
mocks.callGatewayTool.mockResolvedValue({
102+
payload: {
103+
format: "png",
104+
base64: Buffer.from("not-a-real-png").toString("base64"),
105+
},
106+
});
107+
const tool = createCanvasTool();
108+
109+
await tool.execute("tool-call-1", {
110+
action: "present",
111+
timeoutMs: "1500",
112+
x: "10.5",
113+
y: "-2",
114+
width: "640",
115+
height: "480",
116+
});
117+
118+
expect(mocks.callGatewayTool).toHaveBeenLastCalledWith(
119+
"node.invoke",
120+
{ timeoutMs: 1500 },
121+
expect.objectContaining({
122+
command: "canvas.present",
123+
params: {
124+
placement: {
125+
x: 10.5,
126+
y: -2,
127+
width: 640,
128+
height: 480,
129+
},
130+
},
131+
}),
132+
);
133+
134+
await tool.execute("tool-call-2", {
135+
action: "snapshot",
136+
maxWidth: "800",
137+
quality: "0.75",
138+
});
139+
140+
expect(mocks.callGatewayTool).toHaveBeenLastCalledWith(
141+
"node.invoke",
142+
{},
143+
expect.objectContaining({
144+
command: "canvas.snapshot",
145+
params: {
146+
format: "png",
147+
maxWidth: 800,
148+
quality: 0.75,
149+
},
150+
}),
151+
);
152+
});
153+
154+
it("rejects malformed numeric canvas params before invoking node commands", async () => {
155+
const tool = createCanvasTool();
156+
157+
await expect(
158+
tool.execute("tool-call-1", {
159+
action: "snapshot",
160+
maxWidth: "800px",
161+
}),
162+
).rejects.toThrow("maxWidth must be a positive integer");
163+
expect(mocks.callGatewayTool).not.toHaveBeenCalled();
164+
});
165+
100166
it("rejects node-controlled snapshot formats before creating image results", async () => {
101167
mocks.callGatewayTool.mockResolvedValue({
102168
payload: {

extensions/canvas/src/tool.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
jsonResult,
1212
readStringParam,
1313
} from "openclaw/plugin-sdk/channel-actions";
14+
import { readFiniteNumberParam, readPositiveIntegerParam } from "openclaw/plugin-sdk/param-readers";
1415
import type { AnyAgentTool, OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
1516
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
1617
import { normalizeCanvasSnapshotFileExtension, parseCanvasSnapshotPayload } from "./cli-helpers.js";
@@ -29,7 +30,7 @@ function readGatewayCallOptions(params: Record<string, unknown>) {
2930
return {
3031
gatewayUrl: readStringParam(params, "gatewayUrl", { trim: false }),
3132
gatewayToken: readStringParam(params, "gatewayToken", { trim: false }),
32-
timeoutMs: typeof params.timeoutMs === "number" ? params.timeoutMs : undefined,
33+
timeoutMs: readPositiveIntegerParam(params, "timeoutMs"),
3334
};
3435
}
3536

@@ -114,10 +115,10 @@ export function createCanvasTool(options?: CanvasToolOptions): AnyAgentTool {
114115
switch (action) {
115116
case "present": {
116117
const placement = {
117-
x: typeof params.x === "number" ? params.x : undefined,
118-
y: typeof params.y === "number" ? params.y : undefined,
119-
width: typeof params.width === "number" ? params.width : undefined,
120-
height: typeof params.height === "number" ? params.height : undefined,
118+
x: readFiniteNumberParam(params, "x"),
119+
y: readFiniteNumberParam(params, "y"),
120+
width: readFiniteNumberParam(params, "width"),
121+
height: readFiniteNumberParam(params, "height"),
121122
};
122123
const invokeParams: Record<string, unknown> = {};
123124
const presentTarget =
@@ -169,14 +170,11 @@ export function createCanvasTool(options?: CanvasToolOptions): AnyAgentTool {
169170
? params.outputFormat.trim().toLowerCase()
170171
: "png";
171172
const format = formatRaw === "jpg" || formatRaw === "jpeg" ? "jpeg" : "png";
172-
const maxWidth =
173-
typeof params.maxWidth === "number" && Number.isFinite(params.maxWidth)
174-
? params.maxWidth
175-
: undefined;
176-
const quality =
177-
typeof params.quality === "number" && Number.isFinite(params.quality)
178-
? params.quality
179-
: undefined;
173+
const maxWidth = readPositiveIntegerParam(params, "maxWidth");
174+
const quality = readFiniteNumberParam(params, "quality", {
175+
min: 0,
176+
max: 1,
177+
});
180178
const raw = (await invoke("canvas.snapshot", {
181179
format,
182180
maxWidth,

0 commit comments

Comments
 (0)