豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content

Commit 44e511f

Browse files
authored
chore: add function to sanitize params for tool calls (#1250)
This adds functions to sanitize the tool call parameters. They are not called as of now since we don't have server side changes landed yet to support these.
1 parent e513db1 commit 44e511f

File tree

3 files changed

+169
-1
lines changed

3 files changed

+169
-1
lines changed

src/telemetry/ClearcutLogger.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import process from 'node:process';
88

99
import {DAEMON_CLIENT_NAME} from '../daemon/utils.js';
1010
import {logger} from '../logger.js';
11+
import type {zod, ShapeOutput} from '../third_party/index.js';
1112

1213
import type {LocalState, Persistence} from './persistence.js';
1314
import {FilePersistence} from './persistence.js';
@@ -20,6 +21,108 @@ import {
2021
import {WatchdogClient} from './WatchdogClient.js';
2122

2223
const MS_PER_DAY = 24 * 60 * 60 * 1000;
24+
const PARAM_BLOCKLIST = new Set(['uid']);
25+
26+
const SUPPORTED_ZOD_TYPES = [
27+
'ZodString',
28+
'ZodNumber',
29+
'ZodBoolean',
30+
'ZodArray',
31+
'ZodEnum',
32+
] as const;
33+
type ZodType = (typeof SUPPORTED_ZOD_TYPES)[number];
34+
35+
function isZodType(type: string): type is ZodType {
36+
return SUPPORTED_ZOD_TYPES.includes(type as ZodType);
37+
}
38+
39+
function getZodType(zodType: zod.ZodTypeAny): ZodType {
40+
const def = zodType._def;
41+
const typeName = def.typeName;
42+
43+
if (
44+
typeName === 'ZodOptional' ||
45+
typeName === 'ZodDefault' ||
46+
typeName === 'ZodNullable'
47+
) {
48+
return getZodType(def.innerType);
49+
}
50+
if (typeName === 'ZodEffects') {
51+
return getZodType(def.schema);
52+
}
53+
54+
if (isZodType(typeName)) {
55+
return typeName;
56+
}
57+
throw new Error(`Unsupported zod type for tool parameter: ${typeName}`);
58+
}
59+
60+
type LoggedToolCallArgValue = string | number | boolean;
61+
62+
function transformName(zodType: ZodType, name: string): string {
63+
if (zodType === 'ZodString') {
64+
return `${name}_length`;
65+
} else if (zodType === 'ZodArray') {
66+
return `${name}_count`;
67+
} else {
68+
return name;
69+
}
70+
}
71+
72+
function transformValue(
73+
zodType: ZodType,
74+
value: unknown,
75+
): LoggedToolCallArgValue {
76+
if (zodType === 'ZodString') {
77+
return (value as string).length;
78+
} else if (zodType === 'ZodArray') {
79+
return (value as unknown[]).length;
80+
} else {
81+
return value as LoggedToolCallArgValue;
82+
}
83+
}
84+
85+
function hasEquivalentType(zodType: ZodType, value: unknown): boolean {
86+
if (zodType === 'ZodString') {
87+
return typeof value === 'string';
88+
} else if (zodType === 'ZodArray') {
89+
return Array.isArray(value);
90+
} else if (zodType === 'ZodNumber') {
91+
return typeof value === 'number';
92+
} else if (zodType === 'ZodBoolean') {
93+
return typeof value === 'boolean';
94+
} else if (zodType === 'ZodEnum') {
95+
return (
96+
typeof value === 'string' ||
97+
typeof value === 'number' ||
98+
typeof value === 'boolean'
99+
);
100+
} else {
101+
return false;
102+
}
103+
}
104+
105+
export function sanitizeParams(
106+
params: ShapeOutput<zod.ZodRawShape>,
107+
schema: zod.ZodRawShape,
108+
): ShapeOutput<zod.ZodRawShape> {
109+
const transformed: ShapeOutput<zod.ZodRawShape> = {};
110+
for (const [name, value] of Object.entries(params)) {
111+
if (PARAM_BLOCKLIST.has(name)) {
112+
continue;
113+
}
114+
const zodType = getZodType(schema[name]);
115+
if (!hasEquivalentType(zodType, value)) {
116+
throw new Error(
117+
`parameter ${name} has type ${zodType} but value ${value} is not of equivalent type`,
118+
);
119+
}
120+
const transformedName = transformName(zodType, name);
121+
const transformedValue = transformValue(zodType, value);
122+
transformed[transformedName] = transformedValue;
123+
}
124+
return transformed;
125+
}
23126

24127
function detectOsType(): OsType {
25128
switch (process.platform) {

src/third_party/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export {hideBin} from 'yargs/helpers';
1919
export {default as debug} from 'debug';
2020
export type {Debugger} from 'debug';
2121
export {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
22+
export {type ShapeOutput} from '@modelcontextprotocol/sdk/server/zod-compat.js';
2223
export {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
2324
export {StdioClientTransport} from '@modelcontextprotocol/sdk/client/stdio.js';
2425
export {Client} from '@modelcontextprotocol/sdk/client/index.js';

tests/telemetry/ClearcutLogger.test.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@ import {describe, it, afterEach, beforeEach} from 'node:test';
1010
import sinon from 'sinon';
1111

1212
import {DAEMON_CLIENT_NAME} from '../../src/daemon/utils.js';
13-
import {ClearcutLogger} from '../../src/telemetry/ClearcutLogger.js';
13+
import {
14+
ClearcutLogger,
15+
sanitizeParams,
16+
} from '../../src/telemetry/ClearcutLogger.js';
1417
import type {Persistence} from '../../src/telemetry/persistence.js';
1518
import {FilePersistence} from '../../src/telemetry/persistence.js';
1619
import {WatchdogMessageType} from '../../src/telemetry/types.js';
1720
import {WatchdogClient} from '../../src/telemetry/WatchdogClient.js';
21+
import {zod} from '../../src/third_party/index.js';
1822

1923
describe('ClearcutLogger', () => {
2024
let mockPersistence: sinon.SinonStubbedInstance<Persistence>;
@@ -163,4 +167,64 @@ describe('ClearcutLogger', () => {
163167
assert(mockPersistence.saveState.called);
164168
});
165169
});
170+
171+
describe('sanitizeParams', () => {
172+
it('filters out uid and transforms strings and arrays', () => {
173+
const schema = {
174+
uid: zod.string(),
175+
myString: zod.string(),
176+
myArray: zod.array(zod.string()),
177+
myNumber: zod.number(),
178+
myBool: zod.boolean(),
179+
myEnum: zod.enum(['a', 'b']),
180+
};
181+
182+
const params = {
183+
uid: 'sensitive',
184+
myString: 'hello',
185+
myArray: ['one', 'two'],
186+
myNumber: 42,
187+
myBool: true,
188+
myEnum: 'a' as const,
189+
};
190+
191+
const sanitized = sanitizeParams(params, schema);
192+
193+
assert.deepStrictEqual(sanitized, {
194+
myString_length: 5,
195+
myArray_count: 2,
196+
myNumber: 42,
197+
myBool: true,
198+
myEnum: 'a',
199+
});
200+
});
201+
202+
it('throws error for unsupported types', () => {
203+
const schema = {
204+
myObj: zod.object({foo: zod.string()}),
205+
};
206+
const params = {
207+
myObj: {foo: 'bar'},
208+
};
209+
210+
assert.throws(
211+
() => sanitizeParams(params, schema),
212+
/Unsupported zod type for tool parameter: ZodObject/,
213+
);
214+
});
215+
216+
it('throws error when value is not of equivalent type', () => {
217+
const schema = {
218+
myString: zod.string(),
219+
};
220+
const params = {
221+
myString: 123,
222+
};
223+
224+
assert.throws(
225+
() => sanitizeParams(params, schema),
226+
/parameter myString has type ZodString but value 123 is not of equivalent type/,
227+
);
228+
});
229+
});
166230
});

0 commit comments

Comments
 (0)