mirror of https://github.com/openclaw/openclaw.git
fix(cron): remove OpenAPI 3.0 incompatible JSON Schema keywords from cron tool (#61221)
The cron tool schema used type arrays (['string','null']), the 'not' keyword, and 'const' — all unsupported by the OpenAPI 3.0 subset that Gemini-backed providers (e.g. GitHub Copilot) enforce. This caused HTTP 400 for every request when cron was enabled. Replace type arrays with scalar types, remove not/const from CronFailureAlertSchema, and add 'not' to the Gemini unsupported keywords list as defense-in-depth. Fixes #61206
This commit is contained in:
parent
359be4eb48
commit
8c1ca1f245
|
|
@ -96,4 +96,46 @@ describe("cleanSchemaForGemini", () => {
|
|||
expect(cleaned.required).toEqual(["nested"]);
|
||||
expect(cleaned.properties?.nested).not.toHaveProperty("required");
|
||||
});
|
||||
|
||||
// Regression: #61206 — `not` keyword is not part of the OpenAPI 3.0 subset
|
||||
// and must be stripped to avoid HTTP 400 from Gemini-backed providers.
|
||||
it("strips the not keyword from schemas", () => {
|
||||
const cleaned = cleanSchemaForGemini({
|
||||
type: "object",
|
||||
not: { const: true },
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
},
|
||||
}) as Record<string, unknown>;
|
||||
|
||||
expect(cleaned).not.toHaveProperty("not");
|
||||
expect(cleaned.type).toBe("object");
|
||||
expect(cleaned.properties).toEqual({ name: { type: "string" } });
|
||||
});
|
||||
|
||||
// Regression: #61206 — type arrays like ["string", "null"] must be
|
||||
// collapsed to a single scalar type for OpenAPI 3.0 compatibility.
|
||||
it("collapses type arrays by stripping null entries", () => {
|
||||
const cleaned = cleanSchemaForGemini({
|
||||
type: ["string", "null"],
|
||||
description: "nullable field",
|
||||
}) as Record<string, unknown>;
|
||||
|
||||
expect(cleaned.type).toBe("string");
|
||||
expect(cleaned.description).toBe("nullable field");
|
||||
});
|
||||
|
||||
it("collapses type arrays in nested property schemas", () => {
|
||||
const cleaned = cleanSchemaForGemini({
|
||||
type: "object",
|
||||
properties: {
|
||||
agentId: {
|
||||
type: ["string", "null"],
|
||||
description: "Agent id",
|
||||
},
|
||||
},
|
||||
}) as { properties?: { agentId?: Record<string, unknown> } };
|
||||
|
||||
expect(cleaned.properties?.agentId?.type).toBe("string");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,6 +27,11 @@ export const GEMINI_UNSUPPORTED_SCHEMA_KEYWORDS = new Set([
|
|||
"uniqueItems",
|
||||
"minProperties",
|
||||
"maxProperties",
|
||||
|
||||
// JSON Schema composition keywords not supported by OpenAPI 3.0 subset.
|
||||
// `const` is handled separately (converted to enum) in the cleaning loop,
|
||||
// but `not` has no safe equivalent and must be stripped.
|
||||
"not",
|
||||
]);
|
||||
|
||||
const SCHEMA_META_KEYS = ["description", "title", "default"] as const;
|
||||
|
|
|
|||
|
|
@ -136,29 +136,34 @@ describe("CronToolSchema", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("job.failureAlert also allows boolean false", () => {
|
||||
it("job.failureAlert uses plain object type for OpenAPI 3.0 compat", () => {
|
||||
const root = (CronToolSchema as Record<string, unknown>).properties as
|
||||
| Record<string, { properties?: Record<string, unknown>; type?: unknown }>
|
||||
| undefined;
|
||||
const jobProps = root?.job?.properties as
|
||||
| Record<string, { type?: unknown; not?: { const?: unknown } }>
|
||||
| Record<string, { type?: unknown; description?: string }>
|
||||
| undefined;
|
||||
const schema = jobProps?.failureAlert;
|
||||
expect(schema?.type).toEqual(["object", "boolean"]);
|
||||
expect(schema?.not?.const).toBe(true);
|
||||
// Must be a plain "object" type — not a type array — so providers that
|
||||
// enforce an OpenAPI 3.0 subset (e.g. Gemini via GitHub Copilot) accept it.
|
||||
expect(schema?.type).toBe("object");
|
||||
// The description must mention "false" so LLMs know they can disable alerts.
|
||||
expect(schema?.description).toMatch(/false/i);
|
||||
});
|
||||
|
||||
it("job.agentId and job.sessionKey accept null for clear/keep-unset flows", () => {
|
||||
it("job.agentId and job.sessionKey use plain string type for OpenAPI 3.0 compat", () => {
|
||||
const root = (CronToolSchema as Record<string, unknown>).properties as
|
||||
| Record<string, { properties?: Record<string, unknown> }>
|
||||
| undefined;
|
||||
const jobProps = root?.job?.properties as Record<string, { type?: unknown }> | undefined;
|
||||
|
||||
expect(jobProps?.agentId?.type).toEqual(["string", "null"]);
|
||||
expect(jobProps?.sessionKey?.type).toEqual(["string", "null"]);
|
||||
// Must be plain "string" — not ["string", "null"] — for provider compat.
|
||||
// Null semantics are conveyed via the field description and handled at runtime.
|
||||
expect(jobProps?.agentId?.type).toBe("string");
|
||||
expect(jobProps?.sessionKey?.type).toBe("string");
|
||||
});
|
||||
|
||||
it("patch.payload.toolsAllow accepts null for clear flows", () => {
|
||||
it("patch.payload.toolsAllow uses plain array type for OpenAPI 3.0 compat", () => {
|
||||
const root = (CronToolSchema as Record<string, unknown>).properties as
|
||||
| Record<string, { properties?: Record<string, unknown> }>
|
||||
| undefined;
|
||||
|
|
@ -166,6 +171,17 @@ describe("CronToolSchema", () => {
|
|||
| Record<string, { properties?: Record<string, { type?: unknown }> }>
|
||||
| undefined;
|
||||
|
||||
expect(patchProps?.payload?.properties?.toolsAllow?.type).toEqual(["array", "null"]);
|
||||
// Must be plain "array" — not ["array", "null"] — for provider compat.
|
||||
expect(patchProps?.payload?.properties?.toolsAllow?.type).toBe("array");
|
||||
});
|
||||
|
||||
// Regression guard: ensure no OpenAPI 3.0 incompatible keywords leak into the
|
||||
// serialized cron tool schema. This catches future regressions at the source.
|
||||
it("serialized schema contains no type-array or not/const keywords", () => {
|
||||
const json = JSON.stringify(CronToolSchema);
|
||||
// type arrays like ["string","null"] are not valid in OpenAPI 3.0
|
||||
expect(json).not.toMatch(/"type"\s*:\s*\[/);
|
||||
// "not" composition keyword is not supported by OpenAPI 3.0
|
||||
expect(json).not.toMatch(/"not"\s*:\s*\{/);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -55,22 +55,11 @@ const REMINDER_CONTEXT_TOTAL_MAX = 700;
|
|||
const REMINDER_CONTEXT_MARKER = "\n\nRecent context:\n";
|
||||
|
||||
function nullableStringSchema(description: string) {
|
||||
return Type.Optional(
|
||||
Type.Unsafe<string | null>({
|
||||
type: ["string", "null"],
|
||||
description,
|
||||
}),
|
||||
);
|
||||
return Type.Optional(Type.String({ description }));
|
||||
}
|
||||
|
||||
function nullableStringArraySchema(description: string) {
|
||||
return Type.Optional(
|
||||
Type.Unsafe<string[] | null>({
|
||||
type: ["array", "null"],
|
||||
items: { type: "string" },
|
||||
description,
|
||||
}),
|
||||
);
|
||||
return Type.Optional(Type.Array(Type.String(), { description }));
|
||||
}
|
||||
|
||||
function cronPayloadObjectSchema(params: { toolsAllow: TSchema }) {
|
||||
|
|
@ -138,12 +127,14 @@ const CronDeliverySchema = Type.Optional(
|
|||
),
|
||||
);
|
||||
|
||||
// Keep `false` expressible without reintroducing anyOf/oneOf into the raw tool schema.
|
||||
// Omitting `failureAlert` means "leave defaults/unchanged"; `false` explicitly disables alerts.
|
||||
// Runtime handles `failureAlert === false` in cron/service/timer.ts.
|
||||
// The schema declares `type: "object"` to stay compatible with providers that
|
||||
// enforce an OpenAPI 3.0 subset (e.g. Gemini via GitHub Copilot). The
|
||||
// description tells the LLM that `false` is also accepted.
|
||||
const CronFailureAlertSchema = Type.Optional(
|
||||
Type.Unsafe<Record<string, unknown> | false>({
|
||||
type: ["object", "boolean"],
|
||||
not: { const: true },
|
||||
type: "object",
|
||||
properties: {
|
||||
after: Type.Optional(Type.Number({ description: "Failures before alerting" })),
|
||||
channel: Type.Optional(Type.String({ description: "Alert channel" })),
|
||||
|
|
@ -153,7 +144,8 @@ const CronFailureAlertSchema = Type.Optional(
|
|||
accountId: Type.Optional(Type.String()),
|
||||
},
|
||||
additionalProperties: true,
|
||||
description: "Failure alert object, or false to disable alerts for this job",
|
||||
description:
|
||||
"Failure alert config object, or the boolean value false to disable alerts for this job",
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue