mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
fix: avoid shell-escaping special characters except quotes (#7242)
This commit is contained in:
@@ -2,6 +2,7 @@ import ExtensionItem from './ExtensionItem';
|
||||
import builtInExtensionsData from '../../../../built-in-extensions.json';
|
||||
import { ExtensionConfig } from '../../../../api';
|
||||
import { FixedExtensionEntry } from '../../../ConfigContext';
|
||||
import { combineCmdAndArgs } from '../utils';
|
||||
|
||||
interface ExtensionListProps {
|
||||
extensions: FixedExtensionEntry[];
|
||||
@@ -137,7 +138,7 @@ export function getSubtitle(config: ExtensionConfig) {
|
||||
default:
|
||||
return {
|
||||
description: config.description || null,
|
||||
command: 'cmd' in config ? [config.cmd, ...config.args].join(' ') : null,
|
||||
command: 'cmd' in config ? combineCmdAndArgs(config.cmd, config.args) : null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,6 +156,85 @@ describe('Extension Utils', () => {
|
||||
headers: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should not escape @ in command args', () => {
|
||||
const extension: FixedExtensionEntry = {
|
||||
type: 'stdio',
|
||||
name: 'context7',
|
||||
description: 'Context7 MCP',
|
||||
cmd: 'npx',
|
||||
args: ['-y', '@upstash/context7-mcp'],
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
const formData = extensionToFormData(extension);
|
||||
expect(formData.cmd).toBe('npx -y @upstash/context7-mcp');
|
||||
});
|
||||
|
||||
it('should quote args with spaces', () => {
|
||||
const extension: FixedExtensionEntry = {
|
||||
type: 'stdio',
|
||||
name: 'java-app',
|
||||
description: 'Java app',
|
||||
cmd: '/Applications/IntelliJ IDEA.app/Contents/jbr/Contents/Home/bin/java',
|
||||
args: ['-classpath', '/path/with spaces/lib.jar', 'Main'],
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
const formData = extensionToFormData(extension);
|
||||
expect(formData.cmd).toBe(
|
||||
'"/Applications/IntelliJ IDEA.app/Contents/jbr/Contents/Home/bin/java" -classpath "/path/with spaces/lib.jar" Main'
|
||||
);
|
||||
});
|
||||
|
||||
it('should roundtrip command with @ through form data', () => {
|
||||
const extension: FixedExtensionEntry = {
|
||||
type: 'stdio',
|
||||
name: 'context7',
|
||||
description: 'Context7 MCP',
|
||||
cmd: 'npx',
|
||||
args: ['-y', '@upstash/context7-mcp'],
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
const formData = extensionToFormData(extension);
|
||||
const { cmd, args } = splitCmdAndArgs(formData.cmd || '');
|
||||
expect(cmd).toBe('npx');
|
||||
expect(args).toEqual(['-y', '@upstash/context7-mcp']);
|
||||
});
|
||||
|
||||
it('should roundtrip command with spaces through form data', () => {
|
||||
const extension: FixedExtensionEntry = {
|
||||
type: 'stdio',
|
||||
name: 'java-app',
|
||||
description: 'Java app',
|
||||
cmd: '/Applications/IntelliJ IDEA.app/Contents/jbr/Contents/Home/bin/java',
|
||||
args: ['-classpath', '/path/with spaces/lib.jar', 'Main'],
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
const formData = extensionToFormData(extension);
|
||||
const { cmd, args } = splitCmdAndArgs(formData.cmd || '');
|
||||
expect(cmd).toBe('/Applications/IntelliJ IDEA.app/Contents/jbr/Contents/Home/bin/java');
|
||||
expect(args).toEqual(['-classpath', '/path/with spaces/lib.jar', 'Main']);
|
||||
});
|
||||
|
||||
it('should roundtrip args with double quotes and spaces through form data', () => {
|
||||
const extension: FixedExtensionEntry = {
|
||||
type: 'stdio',
|
||||
name: 'test',
|
||||
description: 'test',
|
||||
cmd: 'node',
|
||||
args: ['/My "Project"/bin/run'],
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
const formData = extensionToFormData(extension);
|
||||
expect(formData.cmd).toBe('node \'/My "Project"/bin/run\'');
|
||||
const { cmd, args } = splitCmdAndArgs(formData.cmd || '');
|
||||
expect(cmd).toBe('node');
|
||||
expect(args).toEqual(['/My "Project"/bin/run']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createExtensionConfig', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FixedExtensionEntry } from '../../ConfigContext';
|
||||
import type { ExtensionConfig } from '../../../api/types.gen';
|
||||
import { parse as parseShellQuote, quote as quoteShell } from 'shell-quote';
|
||||
import { parse as parseShellQuote } from 'shell-quote';
|
||||
|
||||
// Default extension timeout in seconds
|
||||
// TODO: keep in sync with rust better
|
||||
@@ -100,11 +100,11 @@ export function extensionToFormData(extension: FixedExtensionEntry): ExtensionFo
|
||||
description: extension.description || '',
|
||||
type:
|
||||
extension.type === 'frontend' ||
|
||||
extension.type === 'inline_python' ||
|
||||
extension.type === 'platform'
|
||||
extension.type === 'inline_python' ||
|
||||
extension.type === 'platform'
|
||||
? 'stdio'
|
||||
: extension.type,
|
||||
cmd: extension.type === 'stdio' ? quoteShell([extension.cmd, ...extension.args]) : undefined,
|
||||
cmd: extension.type === 'stdio' ? combineCmdAndArgs(extension.cmd, extension.args) : undefined,
|
||||
endpoint:
|
||||
extension.type === 'streamable_http' || extension.type === 'sse'
|
||||
? (extension.uri ?? undefined)
|
||||
@@ -187,7 +187,13 @@ export function splitCmdAndArgs(str: string): { cmd: string; args: string[] } {
|
||||
}
|
||||
|
||||
export function combineCmdAndArgs(cmd: string, args: string[]): string {
|
||||
return quoteShell([cmd, ...args]);
|
||||
return [cmd, ...args]
|
||||
.map((a) => {
|
||||
if (!a.includes(' ')) return a;
|
||||
if (a.includes('"')) return `'${a}'`;
|
||||
return `"${a}"`;
|
||||
})
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
export function extractCommand(link: string): string {
|
||||
|
||||
Reference in New Issue
Block a user