fix(install): harden Antigravity target containment, agent tools, and binary asset copies

This commit is contained in:
minteeqahmed-png committed 2026-10-29 21:37:41 +05:00
1 parent 25278e7d7b
commit 68eb49d175
5 files changed
+275 -11

No files matched your search

+21 -1
View File
@@ -1,9 +1,29 @@
'use strict';
function hasSingleSurroundingQuotePair(trimmed, quote) {
let escaped = false;
for (let index = 1; index < trimmed.length - 1; index += 1) {
const character = trimmed[index];
if (escaped) {
escaped = false;
} else if (character === '\\') {
escaped = true;
} else if (character === quote) {
return false;
}
}
return !escaped;
}
function stripSurroundingQuotes(value) {
const trimmed = value.trim();
const quote = trimmed[0];
if ((quote === '"' || quote === "'") && trimmed.endsWith(quote)) {
if (
trimmed.length >= 2
&& (quote === '"' || quote === "'")
&& trimmed.endsWith(quote)
&& hasSingleSurroundingQuotePair(trimmed, quote)
) {
return trimmed.slice(1, -1).trim();
}
return trimmed;
@@ -12,6 +12,7 @@ const SUPPORTED_SOURCE_PREFIXES = ['rules', 'commands', 'agents', 'skills'];
function supportsAntigravitySourcePath(sourceRelativePath) {
const normalizedPath = normalizeRelativePath(sourceRelativePath);
if (normalizedPath.split('/').includes('..')) return false;
return SUPPORTED_SOURCE_PREFIXES.some(prefix => (
normalizedPath === prefix || normalizedPath.startsWith(`${prefix}/`)
));
+17 -7
View File
@@ -1,9 +1,14 @@
'use strict';
const { normalizeAgentTools } = require('../agent-tools');
const TOOL_NAMES = Object.freeze({
Read: 'view_file',
ReadFile: 'view_file',
NotebookRead: 'view_file',
Write: 'write_to_file',
Edit: 'replace_file_content',
MultiEdit: 'replace_file_content',
Grep: 'grep_search',
Glob: 'find_by_name',
Bash: 'run_command',
@@ -18,7 +23,10 @@ const MODEL_NAMES = Object.freeze({
});
function splitFrontmatter(source, label) {
const match = String(source || '').match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
const raw = String(source || '');
const usesCrlf = raw.includes('\r\n');
const normalized = usesCrlf ? raw.replace(/\r\n/g, '\n') : raw;
const match = normalized.match(/^---\n([\s\S]*?)\n---\n/);
if (!match) {
throw new Error(`Cannot adapt Antigravity agent ${label}: missing YAML frontmatter`);
}
@@ -33,20 +41,19 @@ function splitFrontmatter(source, label) {
return {
frontmatter,
body: source.slice(match[0].length),
body: normalized.slice(match[0].length),
usesCrlf,
};
}
function normalizeToolNames(value) {
const names = Array.isArray(value) ? value : String(value || '').split(',');
return [...new Set(names
.map(name => String(name).trim())
return [...new Set(normalizeAgentTools(value)
.filter(name => Object.hasOwn(TOOL_NAMES, name))
.map(name => TOOL_NAMES[name]))];
}
function adaptAntigravityAgent(source, label = '<unknown>') {
const { frontmatter, body } = splitFrontmatter(source, label);
const { frontmatter, body, usesCrlf } = splitFrontmatter(source, label);
const { color: _claudeColor, ...supportedFrontmatter } = frontmatter;
const adapted = { ...supportedFrontmatter };
if (Object.hasOwn(frontmatter, 'tools')) {
@@ -60,9 +67,12 @@ function adaptAntigravityAgent(source, label = '<unknown>') {
const serialized = require('js-yaml')
.dump(adapted, { lineWidth: -1, noRefs: true })
.trimEnd();
return `---\n${serialized}\n---\n${body}`;
const output = `---\n${serialized}\n---\n${body}`;
return usesCrlf ? output.replace(/\n/g, '\r\n') : output;
}
module.exports = {
adaptAntigravityAgent,
MODEL_NAMES,
TOOL_NAMES,
};
+6 -3
View File
@@ -104,10 +104,13 @@ function buildCopyFileOperation({
strategy,
contentTransform,
}) {
// Copilot's agent and workflow transforms are textual. Their directories may
// also contain binary assets and executable helpers, which must keep bytes.
// Agent frontmatter and workflow transforms are textual Markdown projections.
// Their directories may also contain binary assets and executable helpers,
// which must keep their bytes intact.
const applicableTransform = (contentTransform === 'copilot-workflow-paths'
|| contentTransform === 'copilot-agent-frontmatter')
|| contentTransform === 'copilot-agent-frontmatter'
|| contentTransform === 'antigravity-agent-frontmatter'
|| contentTransform === 'qwen-agent-frontmatter')
&& !/\.md$/i.test(sourceRelativePath) ? undefined : contentTransform;
return {
kind: 'copy-file',
@@ -0,0 +1,230 @@
/**
* Contract tests for the Antigravity install target and agent frontmatter transform.
*/
'use strict';
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const REPO_ROOT = path.resolve(__dirname, '..', '..');
const { adaptAntigravityAgent } = require(path.join(REPO_ROOT, 'scripts', 'lib', 'install', 'antigravity-agent'));
const antigravityAdapter = require(path.join(REPO_ROOT, 'scripts', 'lib', 'install-targets', 'antigravity-project'));
const { getInstallTargetAdapter } = require(path.join(REPO_ROOT, 'scripts', 'lib', 'install-targets', 'registry'));
const { SUPPORTED_INSTALL_TARGETS } = require(path.join(REPO_ROOT, 'scripts', 'lib', 'install-manifests'));
let passed = 0;
let failed = 0;
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
passed += 1;
} catch (error) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${error.message}`);
failed += 1;
}
}
const CLAUDE_AGENT = [
'---',
'name: code-simplifier',
'description: Refactoring and simplification specialist.',
'tools: Read, ReadFile, NotebookRead, Edit, MultiEdit, Write, Grep, Glob, Bash, WebSearch, WebFetch, Task',
'model: opus',
'color: teal',
'---',
'',
'## Role',
'',
'You simplify complex code.',
'',
].join('\n');
function planFor(modules) {
return antigravityAdapter.planOperations({
repoRoot: '/repo',
projectRoot: '/project',
homeDir: '/home/user',
modules,
});
}
console.log('=== Testing Antigravity install target ===\n');
test('registry resolves the antigravity target and adapter id', () => {
assert.strictEqual(getInstallTargetAdapter('antigravity').id, 'antigravity-project');
assert.strictEqual(getInstallTargetAdapter('antigravity-project').id, 'antigravity-project');
assert.strictEqual(antigravityAdapter.target, 'antigravity');
assert.strictEqual(antigravityAdapter.kind, 'project');
});
test('antigravity is a supported install target', () => {
assert.ok(SUPPORTED_INSTALL_TARGETS.includes('antigravity'));
});
test('transform maps Claude tools (including MultiEdit/ReadFile/NotebookRead), maps model tiers, and strips color', () => {
const yaml = require('js-yaml');
const adapted = adaptAntigravityAgent(CLAUDE_AGENT, 'agents/code-simplifier.md');
const match = adapted.match(/^---\n([\s\S]*?)\n---\n/);
assert.ok(match, 'Expected YAML frontmatter');
const parsed = yaml.load(match[1]);
assert.strictEqual(parsed.name, 'code-simplifier');
assert.strictEqual(parsed.description, 'Refactoring and simplification specialist.');
assert.strictEqual(parsed.model, 'pro');
assert.strictEqual(Object.hasOwn(parsed, 'color'), false);
assert.deepStrictEqual(parsed.tools, [
'view_file',
'replace_file_content',
'write_to_file',
'grep_search',
'find_by_name',
'run_command',
'search_web',
'read_url_content',
]);
assert.ok(adapted.includes('## Role'));
assert.ok(adapted.includes('You simplify complex code.'));
});
test('transform parses quoted tool scalars and maps haiku to flash', () => {
const yaml = require('js-yaml');
const source = [
'---',
'name: fast-helper',
'description: Quick lookup agent',
'tools: \'"Read", "MultiEdit"\'',
'model: haiku',
'---',
'',
'Body',
'',
].join('\n');
const adapted = adaptAntigravityAgent(source, 'agents/fast-helper.md');
const parsed = yaml.load(adapted.match(/^---\n([\s\S]*?)\n---\n/)[1]);
assert.strictEqual(parsed.model, 'flash');
assert.deepStrictEqual(parsed.tools, ['view_file', 'replace_file_content']);
});
test('transform preserves CRLF line endings consistently on Windows-saved agents', () => {
const crlfSource = [
'---',
'name: windows-agent',
'description: Windows line endings',
'tools: Read, MultiEdit',
'model: sonnet',
'color: purple',
'---',
'',
'Body line 1',
'Body line 2',
].join('\r\n');
const adapted = adaptAntigravityAgent(crlfSource, 'agents/windows-agent.md');
assert.ok(adapted.includes('\r\n'), 'Expected CRLF line endings to be preserved');
assert.ok(!/[^\r]\n/.test(adapted), 'Expected no mixed bare LF line endings in CRLF output');
assert.ok(!adapted.includes('color:'));
assert.ok(adapted.includes('replace_file_content'));
});
test('transform rejects missing or non-object frontmatter', () => {
assert.throws(
() => adaptAntigravityAgent('# no frontmatter\n', 'agents/broken.md'),
/missing YAML frontmatter/
);
assert.throws(
() => adaptAntigravityAgent('---\n- item\n---\nbody\n', 'agents/broken.md'),
/frontmatter must be an object/
);
});
test('traversal segments produce no planned operations', () => {
for (const value of [
'agents/../../escape.md',
'skills/../escape.md',
'commands/../../escape.md',
'rules/../../escape.md',
'agents\\..\\..\\escape.md',
]) {
assert.deepStrictEqual(planFor([{ id: 'agents-core', paths: [value] }]), []);
}
});
test('every planned operation is contained within .agents', () => {
const operations = planFor([
{ id: 'agents-core', paths: ['agents', '.agents', 'AGENTS.md'] },
{ id: 'commands-core', paths: ['commands'] },
{ id: 'framework-language', paths: ['skills/api-design'] },
]);
const root = path.join('/project', '.agents');
assert.ok(operations.length > 0);
for (const operation of operations) {
const relative = path.relative(root, operation.destinationPath);
assert.ok(
relative && !relative.startsWith('..') && !path.isAbsolute(relative),
`operation escapes .agents: ${operation.destinationPath}`
);
}
});
test('Antigravity installs non-Markdown agent assets byte-for-byte while adapting Markdown agents', () => {
const { createManifestInstallPlan, applyInstallPlan } = require('../../scripts/lib/install-executor');
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-antigravity-agent-assets-'));
try {
const sourceRoot = path.join(temp, 'source');
fs.mkdirSync(path.join(sourceRoot, 'manifests'), { recursive: true });
fs.writeFileSync(path.join(sourceRoot, 'manifests', 'install-modules.json'), JSON.stringify({
version: 1,
modules: [{
id: 'fixture',
kind: 'agents',
paths: ['agents'],
targets: ['antigravity'],
dependencies: [],
}],
}));
fs.writeFileSync(path.join(sourceRoot, 'manifests', 'install-profiles.json'), JSON.stringify({
version: 1,
profiles: {},
}));
const agentRoot = path.join(sourceRoot, 'agents');
fs.mkdirSync(path.join(agentRoot, 'assets'), { recursive: true });
const binary = Buffer.from([0, 255, 254, 128, 42]);
const helper = '# helper text without YAML frontmatter\n';
fs.writeFileSync(path.join(agentRoot, 'assets', 'icon.png'), binary);
fs.writeFileSync(path.join(agentRoot, 'assets', 'helper.txt'), helper);
fs.writeFileSync(path.join(agentRoot, 'code-simplifier.MD'), CLAUDE_AGENT);
const plan = createManifestInstallPlan({
sourceRoot,
target: 'antigravity',
moduleIds: ['fixture'],
projectRoot: path.join(temp, 'project'),
homeDir: path.join(temp, 'home'),
env: {},
});
applyInstallPlan(plan);
const installed = path.join(plan.targetRoot, 'agents');
assert.deepStrictEqual(fs.readFileSync(path.join(installed, 'assets', 'icon.png')), binary);
assert.strictEqual(fs.readFileSync(path.join(installed, 'assets', 'helper.txt'), 'utf8'), helper);
assert.strictEqual(
fs.readFileSync(path.join(installed, 'code-simplifier.MD'), 'utf8'),
adaptAntigravityAgent(CLAUDE_AGENT, 'agents/code-simplifier.MD')
);
for (const operation of plan.operations.filter(op => op.sourceRelativePath.replace(/\\/g, '/').includes('/assets/'))) {
assert.ok(!operation.contentTransform, operation.sourceRelativePath);
}
} finally {
fs.rmSync(temp, { recursive: true, force: true });
}
});
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed === 0 ? 0 : 1);