Files
2026-07-13 13:39:12 +08:00

254 lines
7.2 KiB
TypeScript

type JsonRecord = Record<string, unknown>;
export const AG_TOOL_SUFFIX = "_ide";
const AG_DEFAULT_TOOL_NAMES = [
"browser_subagent",
"command_status",
"find_by_name",
"generate_image",
"grep_search",
"list_dir",
"list_resources",
"multi_replace_file_content",
"notify_user",
"read_resource",
"read_terminal",
"read_url_content",
"replace_file_content",
"run_command",
"search_web",
"send_command_input",
"task_boundary",
"view_content_chunk",
"view_file",
"write_to_file",
] as const;
const AG_DECOY_TOOL_NAMES = [
...AG_DEFAULT_TOOL_NAMES,
"mcp_sequential_thinking_sequentialthinking",
] as const;
export const AG_DEFAULT_TOOLS = new Set<string>(AG_DEFAULT_TOOL_NAMES);
export const AG_DECOY_TOOLS = AG_DECOY_TOOL_NAMES.map((name) =>
Object.freeze({
name,
description: "This tool is currently unavailable.",
parameters: {
type: "OBJECT",
properties: {},
required: [],
},
})
);
function asRecord(value: unknown): JsonRecord | null {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : null;
}
function toToolName(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
}
/**
* Recursively strip `enumDescriptions` from a JSON schema.
*
* VSCode Copilot emits `enumDescriptions` inside tool parameter schemas, but the
* Antigravity API rejects any request carrying that field with HTTP 400. Walk the
* schema's `properties` and `items` so the field is removed at every nesting level
* before the declaration is sent upstream.
*/
export function stripEnumDescriptions(schema: unknown): unknown {
if (!schema || typeof schema !== "object") return schema;
if (Array.isArray(schema)) {
return schema.map((entry) => stripEnumDescriptions(entry));
}
const result: JsonRecord = { ...(schema as JsonRecord) };
delete result.enumDescriptions;
const properties = asRecord(result.properties);
if (properties) {
const nextProperties: JsonRecord = {};
for (const key of Object.keys(properties)) {
nextProperties[key] = stripEnumDescriptions(properties[key]);
}
result.properties = nextProperties;
}
if (result.items !== undefined) {
result.items = stripEnumDescriptions(result.items);
}
return result;
}
export function shouldCloakAntigravityTool(toolName: string): boolean {
return (
toolName.length > 0 && !AG_DEFAULT_TOOLS.has(toolName) && !toolName.endsWith(AG_TOOL_SUFFIX)
);
}
export function getCloakedAntigravityToolName(toolName: string): string {
return shouldCloakAntigravityTool(toolName) ? `${toolName}${AG_TOOL_SUFFIX}` : toolName;
}
export function cloakAntigravityToolPayload<T extends JsonRecord>(
body: T
): {
body: T;
toolNameMap: Map<string, string> | null;
} {
const request = asRecord(body.request);
if (!request) {
return { body, toolNameMap: null };
}
const existingToolNameMap =
body._toolNameMap instanceof Map ? (body._toolNameMap as Map<string, string>) : null;
const toolNameMap = existingToolNameMap
? new Map(existingToolNameMap)
: new Map<string, string>();
let changed = false;
const nextRequest: JsonRecord = {
...request,
};
if (Array.isArray(request.tools)) {
const preservedTools: JsonRecord[] = [];
const cloakedDeclarations: JsonRecord[] = [];
for (const toolValue of request.tools) {
const tool = asRecord(toolValue);
if (!tool || !Array.isArray(tool.functionDeclarations)) {
preservedTools.push(toolValue as JsonRecord);
continue;
}
for (const declarationValue of tool.functionDeclarations) {
const declaration = asRecord(declarationValue);
if (!declaration) continue;
// VSCode Copilot sends `enumDescriptions` in tool parameter schemas, but the
// Antigravity API rejects requests carrying that field with HTTP 400. Strip it
// recursively before the declaration is forwarded upstream.
const stripped: JsonRecord =
declaration.parameters !== undefined
? { ...declaration, parameters: stripEnumDescriptions(declaration.parameters) }
: declaration;
if (stripped !== declaration) {
changed = true;
}
const rawName = toToolName(stripped.name);
if (!rawName) {
cloakedDeclarations.push({ ...stripped });
continue;
}
const cloakedName = getCloakedAntigravityToolName(rawName);
if (cloakedName !== rawName) {
changed = true;
toolNameMap.set(cloakedName, toolNameMap.get(rawName) ?? rawName);
}
cloakedDeclarations.push({
...stripped,
name: cloakedName,
});
}
}
if (cloakedDeclarations.length > 0) {
const declaredNames = new Set(
cloakedDeclarations
.map((declaration) => toToolName(declaration.name))
.filter((name) => name.length > 0)
);
const decoys = AG_DECOY_TOOLS.filter((declaration) => !declaredNames.has(declaration.name));
nextRequest.tools = [
...preservedTools,
{ functionDeclarations: [...cloakedDeclarations, ...decoys] },
];
changed = true;
}
}
if (Array.isArray(request.contents)) {
let contentsChanged = false;
const nextContents = request.contents.map((contentValue) => {
const content = asRecord(contentValue);
if (!content || !Array.isArray(content.parts)) return contentValue;
let partChanged = false;
const nextParts = content.parts.map((partValue) => {
const part = asRecord(partValue);
if (!part) return partValue;
const nextPart: JsonRecord = { ...part };
const functionCall = asRecord(part.functionCall);
if (functionCall) {
const rawName = toToolName(functionCall.name);
const cloakedName = getCloakedAntigravityToolName(rawName);
if (cloakedName !== rawName) {
nextPart.functionCall = {
...functionCall,
name: cloakedName,
};
toolNameMap.set(cloakedName, toolNameMap.get(rawName) ?? rawName);
partChanged = true;
}
}
const functionResponse = asRecord(part.functionResponse);
if (functionResponse) {
const rawName = toToolName(functionResponse.name);
const cloakedName = getCloakedAntigravityToolName(rawName);
if (cloakedName !== rawName) {
nextPart.functionResponse = {
...functionResponse,
name: cloakedName,
};
toolNameMap.set(cloakedName, toolNameMap.get(rawName) ?? rawName);
partChanged = true;
}
}
return partChanged ? nextPart : partValue;
});
if (!partChanged) return contentValue;
contentsChanged = true;
return {
...content,
parts: nextParts,
};
});
if (contentsChanged) {
nextRequest.contents = nextContents;
changed = true;
}
}
if (!changed) {
return {
body,
toolNameMap: toolNameMap.size > 0 ? toolNameMap : null,
};
}
return {
body: {
...body,
request: nextRequest,
},
toolNameMap: toolNameMap.size > 0 ? toolNameMap : null,
};
}