* test(#4705): add failing-first coverage for native Antigravity tool sequences * fix(#4705): emit Antigravity-native tool names as a YAML sequence convertClaudeAgentToAntigravityAgent and the installer's twin emitted Gemini CLI tool names as a comma-separated scalar. Antigravity's documented subagent contract (antigravity.google/docs/subagents) wants a YAML sequence of native names — view_file, grep_search, run_command, replace_file_content are the documented examples, and wrong or malformed grants can hang the subagent per Antigravity's own warning. Map values move to the native vocabulary where documented (Read -> view_file, Edit -> replace_file_content, Bash -> run_command, Grep -> grep_search); undocumented entries keep their best-known grant rather than being dropped (dropping would silently remove a restriction). The emitter writes one '- name' item per line; an agent whose every tool was filtered emits an explicit tools: [] instead of an empty scalar. Pre-existing pins updated to the native vocabulary. * test(#4705): update the #4727 map-value pin to the Antigravity-native vocabulary The #4727-era pin held the map VALUES at the Gemini CLI dialect on the belief that Antigravity speaks it; the confirmed bug #4705 (with Antigravity's own documented subagent contract) supersedes that for the four documented names. Key/shape pinning is preserved; only the values move. * docs(#4705): backfill changeset PR number --------- Co-authored-by: sim <sim@local>
This commit is contained in:
5
.changeset/bold-zebras-run.md
Normal file
5
.changeset/bold-zebras-run.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 4822
|
||||
---
|
||||
**Antigravity agents get native tool names as a YAML sequence** — converted agents carried Gemini CLI tool names (`read_file`, `search_file_content`, `run_shell_command`) as a comma-separated scalar, while Antigravity's documented subagent contract wants a YAML sequence of native names (`view_file`, `grep_search`, `run_command`, `replace_file_content`); wrong or malformed grants can hang the subagent. The installer's twin converter changes in lockstep. (#4705)
|
||||
@@ -1659,16 +1659,17 @@ const claudeToOpencodeTools = {
|
||||
// Tool name mapping from Claude Code to Antigravity
|
||||
// Antigravity uses Gemini's snake_case built-in tool names
|
||||
const claudeToAntigravityTools = {
|
||||
Read: 'read_file',
|
||||
// #4705: Antigravity-NATIVE tool names (see src/runtime-artifact-conversion.cts)
|
||||
Read: 'view_file',
|
||||
Write: 'write_file',
|
||||
Edit: 'replace',
|
||||
Bash: 'run_shell_command',
|
||||
Edit: 'replace_file_content',
|
||||
Bash: 'run_command',
|
||||
Glob: 'glob',
|
||||
Grep: 'search_file_content',
|
||||
Grep: 'grep_search',
|
||||
WebSearch: 'google_web_search',
|
||||
WebFetch: 'web_fetch',
|
||||
TodoWrite: 'write_todos',
|
||||
};
|
||||
}
|
||||
|
||||
// Tool name mapping from Claude/GSD agents to Kimi CLI module paths.
|
||||
// Kimi custom agent YAML requires fully-qualified module paths.
|
||||
@@ -2527,7 +2528,11 @@ function convertClaudeAgentToAntigravityAgent(content, isGlobal = false) {
|
||||
const mappedTools = claudeTools.map(t => convertAntigravityToolName(t)).filter(Boolean);
|
||||
|
||||
// #2876: quote description for the same reason as the skill variant.
|
||||
let fm = `---\nname: ${name}\ndescription: ${yamlQuote(description)}\ntools: ${mappedTools.join(', ')}\n`;
|
||||
// #4705: tools is a YAML SEQUENCE of native names (see the src twin).
|
||||
const toolsBlock = mappedTools.length > 0
|
||||
? `tools:\n${mappedTools.map((t) => `- ${t}`).join('\n')}\n`
|
||||
: 'tools: []\n';
|
||||
let fm = `---\nname: ${name}\ndescription: ${yamlQuote(description)}\n${toolsBlock}`;
|
||||
if (color) fm += `color: ${color}\n`;
|
||||
fm += '---';
|
||||
|
||||
|
||||
@@ -2431,13 +2431,20 @@ const claudeToCopilotTools = {
|
||||
|
||||
// Tool name mapping from Claude Code to Antigravity
|
||||
// Antigravity uses Gemini's snake_case built-in tool names
|
||||
// #4705: values are Antigravity-NATIVE tool names per the documented subagent
|
||||
// contract (antigravity.google/docs/subagents: view_file, replace_file_content,
|
||||
// grep_search, run_command are the documented examples; the catalog is
|
||||
// non-exhaustive, so unmapped entries keep their best-known grant rather than
|
||||
// being dropped — dropping would silently remove a restriction). The old
|
||||
// values were Gemini CLI dialect names, which Antigravity's tool validation
|
||||
// does not document and may hang the subagent on.
|
||||
const claudeToAntigravityTools = {
|
||||
Read: 'read_file',
|
||||
Read: 'view_file',
|
||||
Write: 'write_file',
|
||||
Edit: 'replace',
|
||||
Bash: 'run_shell_command',
|
||||
Edit: 'replace_file_content',
|
||||
Bash: 'run_command',
|
||||
Glob: 'glob',
|
||||
Grep: 'search_file_content',
|
||||
Grep: 'grep_search',
|
||||
WebSearch: 'google_web_search',
|
||||
WebFetch: 'web_fetch',
|
||||
TodoWrite: 'write_todos',
|
||||
@@ -2552,7 +2559,13 @@ function convertClaudeAgentToAntigravityAgent(content, isGlobal = false) {
|
||||
const mappedTools = claudeTools.map(t => convertAntigravityToolName(t)).filter(Boolean);
|
||||
|
||||
// #2876: quote description for the same reason as the skill variant.
|
||||
let fm = `---\nname: ${name}\ndescription: ${yamlQuote(description)}\ntools: ${mappedTools.join(', ')}\n`;
|
||||
// #4705: tools is a YAML SEQUENCE of native names (one `- name` item per
|
||||
// line), not a comma-separated scalar. Empty mapped set -> `tools: []` so an
|
||||
// agent whose every tool was filtered stays explicitly restricted.
|
||||
const toolsBlock = mappedTools.length > 0
|
||||
? `tools:\n${mappedTools.map((t) => `- ${t}`).join('\n')}\n`
|
||||
: 'tools: []\n';
|
||||
let fm = `---\nname: ${name}\ndescription: ${yamlQuote(description)}\n${toolsBlock}`;
|
||||
if (color) fm += `color: ${color}\n`;
|
||||
fm += '---';
|
||||
|
||||
|
||||
@@ -217,13 +217,14 @@ describe('#1928 Antigravity preserved (shared surface with the removed gemini ru
|
||||
assert.strictEqual(getRuntimeLabel('antigravity'), 'Antigravity');
|
||||
});
|
||||
|
||||
test('the shared Gemini-backend tool vocabulary still powers Antigravity agent conversion', () => {
|
||||
test('the Antigravity-native tool vocabulary powers Antigravity agent conversion (#4705)', () => {
|
||||
const input = ['---', 'name: gsd-x', 'description: d', 'tools: Read, Write, WebFetch, Skill', '---', '', 'body'].join('\n');
|
||||
const toolsLine = convertClaudeAgentToAntigravityAgent(input).split('\n').find((l) => l.startsWith('tools:')) || '';
|
||||
assert.ok(toolsLine.includes('read_file'), 'Read → read_file via the retained convertAntigravityToolName');
|
||||
assert.ok(toolsLine.includes('write_file'), 'Write → write_file');
|
||||
assert.ok(toolsLine.includes('web_fetch'), 'WebFetch → web_fetch');
|
||||
assert.ok(!/\bskill\b/.test(toolsLine), 'Skill is still excluded (would be an invalid backend tool name)');
|
||||
const result = convertClaudeAgentToAntigravityAgent(input);
|
||||
const toolsBlock = result.slice(result.indexOf('tools:')).split('\n').filter((l) => l.startsWith('- '));
|
||||
assert.ok(toolsBlock.includes('- view_file'), 'Read → view_file (native name, #4705)');
|
||||
assert.ok(toolsBlock.includes('- write_file'), 'Write → write_file');
|
||||
assert.ok(toolsBlock.includes('- web_fetch'), 'WebFetch → web_fetch');
|
||||
assert.ok(!toolsBlock.some((l) => /\bskill\b/.test(l)), 'Skill is still excluded (would be an invalid backend tool name)');
|
||||
});
|
||||
|
||||
test('#4727 the rename is complete: no gemini-named alias survives alongside the antigravity-named exports', () => {
|
||||
@@ -250,20 +251,25 @@ describe('#1928 Antigravity preserved (shared surface with the removed gemini ru
|
||||
'the retired gemini-named function must not still be exported — a surviving alias means the rename never finished',
|
||||
);
|
||||
|
||||
// The rename must be a pure identifier change: every value byte-identical, so an added OR
|
||||
// removed key (not just a renamed export) fails this too.
|
||||
// The #4727 rename remains a pure identifier change: same KEYS, same shape
|
||||
// (an added OR removed key still fails). The VALUES moved from the Gemini
|
||||
// CLI dialect to Antigravity-native names in #4705 — Antigravity's own
|
||||
// documented subagent contract (antigravity.google/docs/subagents) names
|
||||
// view_file / replace_file_content / grep_search / run_command as native,
|
||||
// and its tool-validation warning says wrong names can hang the subagent.
|
||||
// The old "Antigravity speaks the Gemini dialect" rationale was the belief
|
||||
// at #4727 time; the confirmed bug #4705 supersedes it.
|
||||
assert.deepStrictEqual(mod.claudeToAntigravityTools, {
|
||||
Read: 'read_file',
|
||||
Read: 'view_file',
|
||||
Write: 'write_file',
|
||||
Edit: 'replace',
|
||||
Bash: 'run_shell_command',
|
||||
Edit: 'replace_file_content',
|
||||
Bash: 'run_command',
|
||||
Glob: 'glob',
|
||||
Grep: 'search_file_content',
|
||||
Grep: 'grep_search',
|
||||
WebSearch: 'google_web_search',
|
||||
WebFetch: 'web_fetch',
|
||||
TodoWrite: 'write_todos',
|
||||
}, 'the tool map values are Gemini\'s built-in tool dialect, which Antigravity speaks — Google\'s '
|
||||
+ 'contract, not GSD\'s to alter by renaming the map that carries it');
|
||||
}, 'the tool map keys are the #4727 rename surface (add/remove still fails); the VALUES are the #4705 Antigravity-native vocabulary');
|
||||
|
||||
// In-set / out-of-set boundary pair: the excluded ids still return null individually...
|
||||
for (const excluded of ['mcp__anything', 'Task', 'Agent', 'AskUserQuestion', 'ask_user', 'Skill', 'SlashCommand']) {
|
||||
|
||||
@@ -478,12 +478,12 @@ tools: Read, Write, Bash, Skill, WebFetch, SlashCommand
|
||||
<role>Plan the phase.</role>`;
|
||||
|
||||
const result = convertClaudeAgentToAntigravityAgent(input);
|
||||
const toolsLine = result.split('\n').find(l => l.startsWith('tools:')) || '';
|
||||
const toolsItems = result.split('\n').filter(l => l.startsWith('- ')).map(l => l.slice(2));
|
||||
|
||||
assert.ok(toolsLine.includes('read_file'), 'maps Read -> read_file');
|
||||
assert.ok(toolsLine.includes('web_fetch'), 'maps WebFetch -> web_fetch');
|
||||
assert.ok(!/\bskill\b/.test(toolsLine), 'no invalid skill tool in Antigravity frontmatter');
|
||||
assert.ok(!/\bslashcommand\b/.test(toolsLine), 'no invalid slashcommand tool in Antigravity frontmatter');
|
||||
assert.ok(toolsItems.includes('view_file'), 'maps Read -> view_file (#4705 native name)');
|
||||
assert.ok(toolsItems.includes('web_fetch'), 'maps WebFetch -> web_fetch');
|
||||
assert.ok(!toolsItems.some(t => /\bskill\b/.test(t)), 'no invalid skill tool in Antigravity frontmatter');
|
||||
assert.ok(!toolsItems.some(t => /\bslashcommand\b/.test(t)), 'no invalid slashcommand tool in Antigravity frontmatter');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2667,3 +2667,40 @@ describe('#3706: the layout seam actually threads the variant', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ── #4705 — Antigravity agent tools: native names, YAML sequence ─────────────
|
||||
// The converter emitted Gemini CLI tool names as a comma-separated scalar;
|
||||
// Antigravity's documented subagent contract (antigravity.google/docs/subagents)
|
||||
// wants a YAML sequence of native tool names (view_file, grep_search,
|
||||
// run_command, replace_file_content are the documented examples). A scalar or a
|
||||
// wrong-vocabulary grant can hang the subagent per Antigravity's own warning.
|
||||
describe('#4705 — Antigravity agent tools are native names in a YAML sequence', () => {
|
||||
const input = `---
|
||||
name: example-reviewer
|
||||
description: Inspect source files
|
||||
tools: Read, Grep, Bash
|
||||
---
|
||||
|
||||
Inspect source files.`;
|
||||
|
||||
test('emits the documented native tools as a YAML sequence (#4705)', () => {
|
||||
const result = convertClaudeAgentToAntigravityAgent(input);
|
||||
assert.match(result, /tools:\n- view_file\n- grep_search\n- run_command\n/,
|
||||
'tools must be a YAML sequence of Antigravity-native names (view_file, grep_search, run_command)');
|
||||
assert.doesNotMatch(result, /tools:.*,/m, 'no comma-separated scalar may survive');
|
||||
});
|
||||
|
||||
test('Edit maps to the documented replace_file_content (#4705)', () => {
|
||||
const result = convertClaudeAgentToAntigravityAgent(
|
||||
`---\nname: e\ndescription: d\ntools: Edit\n---\n\nbody`);
|
||||
assert.match(result, /- replace_file_content/, 'Edit -> replace_file_content (documented native name)');
|
||||
assert.ok(!/- replace\b/.test(result), 'the Gemini CLI name replace must not survive');
|
||||
});
|
||||
|
||||
test('unmapped tools keep their lowercase grant (no silent restriction drop) (#4705)', () => {
|
||||
const result = convertClaudeAgentToAntigravityAgent(
|
||||
`---\nname: e\ndescription: d\ntools: Glob, WebFetch\n---\n\nbody`);
|
||||
assert.match(result, /- glob/, 'undocumented tools keep their mapped/lowercase grant');
|
||||
assert.match(result, /- web_fetch/, 'WebFetch stays granted');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user