Skip to content

Commit fa7bf3f

Browse files
committed
fix(feishu): emit non-empty value schema for bitable write tools (#94547)
Type.Record(Type.String(), Type.Any()) serializes via TypeBox to an empty nested value schema (patternProperties: { "^.*$": {} }). AWS Bedrock's strict tool-schema validation rejects the empty sub-schema, breaking all agent turns for Bedrock-backed Anthropic users with Feishu bitable tools enabled. Replace the three affected free-form field maps (create_record, update_record, create_field) with an explicit non-empty FlexibleFieldValue value schema that still accepts any JSON value. Type.Unknown() is avoided because it also serializes to an empty schema. Adds regression coverage asserting the emitted value schema is non-empty.
1 parent ed8ab71 commit fa7bf3f

2 files changed

Lines changed: 58 additions & 3 deletions

File tree

extensions/feishu/src/bitable.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,49 @@ describe("feishu bitable create app cleanup", () => {
172172
expect(client.bitable.appTableRecord.list).toHaveBeenCalledTimes(1);
173173
});
174174
});
175+
176+
describe("feishu bitable write tool schemas (#94547)", () => {
177+
afterAll(() => {
178+
vi.doUnmock("./client.js");
179+
vi.resetModules();
180+
});
181+
182+
beforeEach(() => {
183+
createFeishuClientMock.mockReset();
184+
});
185+
186+
function patternValueSchemaOf(
187+
schema: Record<string, unknown> | undefined,
188+
): Record<string, unknown> | undefined {
189+
const pattern = schema?.patternProperties as Record<string, unknown> | undefined;
190+
if (!pattern) {
191+
return undefined;
192+
}
193+
const keys = Object.keys(pattern);
194+
return keys.length > 0 ? (pattern[keys[0]] as Record<string, unknown>) : undefined;
195+
}
196+
197+
// Bedrock's strict tool-schema validation rejects empty nested schemas
198+
// (`patternProperties: { "^(.*)$": {} }`). Each bitable write tool must emit a
199+
// non-empty value schema for its free-form field map.
200+
it.each([
201+
["feishu_bitable_create_record", "fields"],
202+
["feishu_bitable_update_record", "fields"],
203+
["feishu_bitable_create_field", "property"],
204+
])("%s emits a non-empty value schema for %s", (toolName, propName) => {
205+
const { client } = createBitableClient([]);
206+
createFeishuClientMock.mockReturnValue(client);
207+
const { api, resolveTool } = createToolFactoryHarness(createConfig());
208+
registerFeishuBitableTools(api);
209+
210+
const tool = resolveTool(toolName) as unknown as {
211+
parameters?: { properties?: Record<string, Record<string, unknown>> };
212+
};
213+
const fieldSchema = tool.parameters?.properties?.[propName];
214+
expect(fieldSchema).toBeDefined();
215+
216+
const valueSchema = patternValueSchemaOf(fieldSchema);
217+
expect(valueSchema).toBeDefined();
218+
expect(Object.keys(valueSchema as Record<string, unknown>).length).toBeGreaterThan(0);
219+
});
220+
});

extensions/feishu/src/bitable.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -525,12 +525,21 @@ const GetRecordSchema = Type.Object({
525525
record_id: Type.String({ description: "Record ID to retrieve" }),
526526
});
527527

528+
// Flexible value schema for bitable field maps. TypeBox serializes both
529+
// Type.Any() and Type.Unknown() to an empty `{}` sub-schema, which produces
530+
// `patternProperties: { "^(.*)$": {} }`. AWS Bedrock's strict tool-schema
531+
// validation rejects that empty nested schema, so we emit an explicit
532+
// non-empty value schema that still accepts any JSON value. See issue #94547.
533+
const FlexibleFieldValue = Type.Unsafe<unknown>({
534+
type: ["string", "number", "boolean", "object", "array", "null"],
535+
});
536+
528537
const CreateRecordSchema = Type.Object({
529538
app_token: Type.String({
530539
description: "Bitable app token (use feishu_bitable_get_meta to get from URL)",
531540
}),
532541
table_id: Type.String({ description: "Table ID (from URL: ?table=YYY)" }),
533-
fields: Type.Record(Type.String(), Type.Any(), {
542+
fields: Type.Record(Type.String(), FlexibleFieldValue, {
534543
description:
535544
"Field values keyed by field name. Format by type: Text='string', Number=123, SingleSelect='Option', MultiSelect=['A','B'], DateTime=timestamp_ms, User=[{id:'ou_xxx'}], URL={text:'Display',link:'https://...'}",
536545
}),
@@ -560,7 +569,7 @@ const CreateFieldSchema = Type.Object({
560569
minimum: 1,
561570
}),
562571
property: Type.Optional(
563-
Type.Record(Type.String(), Type.Any(), {
572+
Type.Record(Type.String(), FlexibleFieldValue, {
564573
description: "Field-specific properties (e.g., options for SingleSelect, format for Number)",
565574
}),
566575
),
@@ -572,7 +581,7 @@ const UpdateRecordSchema = Type.Object({
572581
}),
573582
table_id: Type.String({ description: "Table ID (from URL: ?table=YYY)" }),
574583
record_id: Type.String({ description: "Record ID to update" }),
575-
fields: Type.Record(Type.String(), Type.Any(), {
584+
fields: Type.Record(Type.String(), FlexibleFieldValue, {
576585
description: "Field values to update (same format as create_record)",
577586
}),
578587
});

0 commit comments

Comments
 (0)