Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions extensions/ai-providers/antigravity/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/

import { createHash, randomUUID } from "node:crypto";
import { isDeepStrictEqual } from "node:util";
import type {
Api,
AssistantMessage,
Expand Down Expand Up @@ -148,6 +149,8 @@ function sanitizeSchemaForCcaValue(
if (value === null || typeof value !== "object") return value;
const out: Record<string, unknown> = {};
const spill: Array<[string, unknown]> = [];
let constEntry: unknown;
let hasConst = false;
for (const [key, entry] of Object.entries(value)) {
// Keys below `properties` are user-defined parameter names, not JSON
// Schema keywords. A tool parameter named `pattern`, for example, must be
Expand All @@ -156,6 +159,16 @@ function sanitizeSchemaForCcaValue(
out[key] = sanitizeSchemaForCcaValue(entry, false);
continue;
}
if (key === "const") {
hasConst = true;
constEntry = entry;
continue;
}
// Enum entries are instance data, including objects, not subschemas.
if (key === "enum") {
out.enum = entry;
continue;
}
if (Object.hasOwn(CCA_UNSUPPORTED_SCHEMA_FIELDS, key)) {
if (
entry !== undefined &&
Expand All @@ -167,6 +180,19 @@ function sanitizeSchemaForCcaValue(
}
out[key] = sanitizeSchemaForCcaValue(entry, key === "properties");
}
if (hasConst) {
// OpenAPI 3.0 — the schema dialect CCA validates against — has no
// "const"; TypeBox literal unions (`anyOf` of `{type, const}`) 400 the
// whole request. Rewrite the literal as a single-value enum, which CCA
// accepts (enum-carrying parameters in the same request pass).
if (
Array.isArray(out.enum) &&
!out.enum.some((entry) => isDeepStrictEqual(entry, constEntry))
) {
throw new Error("Antigravity tool schema has conflicting const and enum");
}
out.enum = [constEntry];
}
if (spill.length > 0) {
const formatted = `{${spill.map(([key, entry]) => `${key}: ${JSON.stringify(entry)}`).join(", ")}}`;
const existing = typeof out.description === "string" ? out.description : "";
Expand Down
80 changes: 80 additions & 0 deletions tests/extensions/ai-providers/antigravity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,86 @@ test("sanitizeSchemaForCca preserves property names that match schema keywords",
);
});

test("sanitizeSchemaForCca rewrites const literals to enum inside anyOf branches", () => {
// TypeBox serializes Type.Union([Type.Literal(...)]) this way; CCA 400s the
// whole request on "const" while accepting enum in the same payload.
assert.deepEqual(
sanitizeSchemaForCca({
type: "object",
properties: {
evidence: {
description: 'Evidence to read: "transcript", "final", "structured".',
anyOf: [
{ type: "string", const: "transcript" },
{ type: "string", const: "final" },
{ type: "string", const: "structured" },
],
},
},
}),
{
type: "object",
properties: {
evidence: {
description: 'Evidence to read: "transcript", "final", "structured".',
anyOf: [
{ type: "string", enum: ["transcript"] },
{ type: "string", enum: ["final"] },
{ type: "string", enum: ["structured"] },
],
},
},
},
);
});

test("sanitizeSchemaForCca intersects const with a sibling enum", () => {
assert.deepEqual(
sanitizeSchemaForCca({ type: "string", const: "chromium" }),
{
type: "string",
enum: ["chromium"],
},
);
assert.deepEqual(
sanitizeSchemaForCca({
type: "string",
const: "chrome",
enum: ["chrome", "edge"],
}),
{ type: "string", enum: ["chrome"] },
);
assert.throws(
() => sanitizeSchemaForCca({ const: "chrome", enum: ["edge"] }),
/conflicting const and enum/,
);
const literal = { const: "keep", pattern: "literal data" };
assert.deepEqual(sanitizeSchemaForCca({ enum: [literal] }), {
enum: [literal],
});
assert.deepEqual(
sanitizeSchemaForCca({ const: literal, enum: [{ ...literal }] }),
{ enum: [literal] },
);
});

test("sanitizeSchemaForCca preserves a property named const", () => {
assert.deepEqual(
sanitizeSchemaForCca({
type: "object",
properties: {
const: { type: "string", description: "Value to pin" },
},
}),
{
type: "object",
properties: {
const: { type: "string", description: "Value to pin" },
},
},
);
});

test("buildRequestBody strips CCA-rejected keywords and spills constraints into description", () => {
const contextWithTools = {
...SIMPLE_CONTEXT,
Expand Down
Loading