Skip to content

Commit 8150aa8

Browse files
authored
Merge pull request #5 from RooVetGit/feature/autoCommandAllowList
Adding allow-list for auto-executable commands
2 parents 9c2c269 + 1f74d6e commit 8150aa8

File tree

6 files changed

+134
-12
lines changed

6 files changed

+134
-12
lines changed

README.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,18 @@
44

55
### Packaging
66
1. Bump the version in `package.json`
7-
2. Build the VSIX file:
7+
2. Remove the old VSIX file:
8+
```bash
9+
rm bin/roo-cline-*.vsix
10+
```
11+
3. Build the VSIX file:
812
```bash
913
npm run vsix
1014
```
11-
3. The new VSIX file will be created in the `bin/` directory
12-
4. Commit the new VSIX file to git and remove the old one:
15+
4. The new VSIX file will be created in the `bin/` directory
16+
5. Commit the new VSIX file to git:
1317
```bash
14-
git rm bin/roo-cline-*.vsix
15-
git add bin/roo-cline-<new_version>.vsix
18+
git add bin/*.vsix
1619
git commit -m "chore: update VSIX to version <new_version>"
1720
```
1821

bin/roo-cline-1.0.2.vsix

-5.55 MB
Binary file not shown.

bin/roo-cline-1.0.3.vsix

23.8 MB
Binary file not shown.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "roo-cline",
33
"displayName": "Roo Cline",
44
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
5-
"version": "1.0.2",
5+
"version": "1.0.3",
66
"icon": "assets/icons/icon.png",
77
"galleryBanner": {
88
"color": "#617A91",

src/core/Cline.ts

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,16 @@ type UserContent = Array<
5656
Anthropic.TextBlockParam | Anthropic.ImageBlockParam | Anthropic.ToolUseBlockParam | Anthropic.ToolResultBlockParam
5757
>
5858

59+
// Add near the top of the file, after imports:
60+
const ALLOWED_AUTO_EXECUTE_COMMANDS = [
61+
'npm',
62+
'npx',
63+
'tsc',
64+
'git log',
65+
'git diff',
66+
'list'
67+
] as const
68+
5969
export class Cline {
6070
readonly taskId: string
6171
api: ApiHandler
@@ -124,6 +134,25 @@ export class Cline {
124134
}
125135
}
126136

137+
protected isAllowedCommand(command?: string): boolean {
138+
if (!command) {
139+
return false;
140+
}
141+
// Check for command chaining characters
142+
if (command.includes('&&') ||
143+
command.includes(';') ||
144+
command.includes('||') ||
145+
command.includes('|') ||
146+
command.includes('$(') ||
147+
command.includes('`')) {
148+
return false;
149+
}
150+
const trimmedCommand = command.trim().toLowerCase();
151+
return ALLOWED_AUTO_EXECUTE_COMMANDS.some(prefix =>
152+
trimmedCommand.startsWith(prefix.toLowerCase())
153+
);
154+
}
155+
127156
// Storing task to disk for history
128157

129158
private async ensureTaskDirectoryExists(): Promise<string> {
@@ -555,8 +584,8 @@ export class Cline {
555584
: [{ type: "text", text: lastMessage.content }]
556585
if (previousAssistantMessage && previousAssistantMessage.role === "assistant") {
557586
const assistantContent = Array.isArray(previousAssistantMessage.content)
558-
? previousAssistantMessage.content
559-
: [{ type: "text", text: previousAssistantMessage.content }]
587+
? previousAssistantMessage.content
588+
: [{ type: "text", text: previousAssistantMessage.content }]
560589

561590
const toolUseBlocks = assistantContent.filter(
562591
(block) => block.type === "tool_use"
@@ -839,7 +868,7 @@ export class Cline {
839868
// (have to do this for partial and complete since sending content in thinking tags to markdown renderer will automatically be removed)
840869
// Remove end substrings of <thinking or </thinking (below xml parsing is only for opening tags)
841870
// (this is done with the xml parsing below now, but keeping here for reference)
842-
// content = content.replace(/<\/?t(?:h(?:i(?:n(?:k(?:i(?:n(?:g)?)?)?)?)?)?)?$/, "")
871+
// content = content.replace(/<\/?t(?:h(?:i(?:n(?:k(?:i(?:n(?:g)?)?)?)?)?$/, "")
843872
// Remove all instances of <thinking> (with optional line break after) and </thinking> (with optional line break before)
844873
// - Needs to be separate since we dont want to remove the line break before the first tag
845874
// - Needs to happen before the xml parsing below
@@ -1503,7 +1532,7 @@ export class Cline {
15031532
const command: string | undefined = block.params.command
15041533
try {
15051534
if (block.partial) {
1506-
if (this.alwaysAllowExecute) {
1535+
if (this.alwaysAllowExecute && this.isAllowedCommand(command)) {
15071536
await this.say("command", command, undefined, block.partial)
15081537
} else {
15091538
await this.ask("command", removeClosingTag("command", command), block.partial).catch(
@@ -1520,7 +1549,9 @@ export class Cline {
15201549
break
15211550
}
15221551
this.consecutiveMistakeCount = 0
1523-
const didApprove = this.alwaysAllowExecute || (await askApproval("command", command))
1552+
1553+
const didApprove = (this.alwaysAllowExecute && this.isAllowedCommand(command)) ||
1554+
(await askApproval("command", command))
15241555
if (!didApprove) {
15251556
break
15261557
}

src/core/__tests__/Cline.test.ts

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,5 +318,93 @@ describe('Cline', () => {
318318
expect(writeDisabledCline.alwaysAllowWrite).toBe(false);
319319
// The write operation would require approval in actual implementation
320320
});
321-
});
321+
});
322+
323+
describe('isAllowedCommand', () => {
324+
let cline: any
325+
326+
beforeEach(() => {
327+
// Create a more complete mock provider
328+
const mockProvider = {
329+
context: {
330+
globalStorageUri: { fsPath: '/mock/path' }
331+
},
332+
postStateToWebview: jest.fn(),
333+
postMessageToWebview: jest.fn(),
334+
updateTaskHistory: jest.fn()
335+
}
336+
337+
// Mock the required dependencies
338+
const mockApiConfig = {
339+
getModel: () => ({
340+
id: 'claude-3-sonnet',
341+
info: { supportsComputerUse: true }
342+
})
343+
}
344+
345+
// Create test instance with mocked constructor params
346+
cline = new Cline(
347+
mockProvider as any,
348+
mockApiConfig as any,
349+
undefined, // customInstructions
350+
false, // alwaysAllowReadOnly
351+
false, // alwaysAllowWrite
352+
false, // alwaysAllowExecute
353+
'test task' // task
354+
)
355+
356+
// Mock internal methods that are called during initialization
357+
cline.initiateTaskLoop = jest.fn()
358+
cline.say = jest.fn()
359+
cline.addToClineMessages = jest.fn()
360+
cline.overwriteClineMessages = jest.fn()
361+
cline.addToApiConversationHistory = jest.fn()
362+
cline.overwriteApiConversationHistory = jest.fn()
363+
})
364+
365+
test('returns true for allowed commands', () => {
366+
expect(cline.isAllowedCommand('npm install')).toBe(true)
367+
expect(cline.isAllowedCommand('npx create-react-app')).toBe(true)
368+
expect(cline.isAllowedCommand('tsc --watch')).toBe(true)
369+
expect(cline.isAllowedCommand('git log --oneline')).toBe(true)
370+
expect(cline.isAllowedCommand('git diff main')).toBe(true)
371+
})
372+
373+
test('returns true regardless of case or whitespace', () => {
374+
expect(cline.isAllowedCommand('NPM install')).toBe(true)
375+
expect(cline.isAllowedCommand(' npm install')).toBe(true)
376+
expect(cline.isAllowedCommand('GIT DIFF')).toBe(true)
377+
})
378+
379+
test('returns false for non-allowed commands', () => {
380+
expect(cline.isAllowedCommand('rm -rf /')).toBe(false)
381+
expect(cline.isAllowedCommand('git push')).toBe(false)
382+
expect(cline.isAllowedCommand('git commit')).toBe(false)
383+
expect(cline.isAllowedCommand('curl http://example.com')).toBe(false)
384+
})
385+
386+
test('returns false for undefined or empty commands', () => {
387+
expect(cline.isAllowedCommand()).toBe(false)
388+
expect(cline.isAllowedCommand('')).toBe(false)
389+
expect(cline.isAllowedCommand(' ')).toBe(false)
390+
})
391+
392+
test('returns false for commands with chaining operators', () => {
393+
const maliciousCommands = [
394+
'npm install && rm -rf /',
395+
'git status; dangerous-command',
396+
'git log || evil-script',
397+
'git status | malicious-pipe',
398+
'git log $(evil-command)',
399+
'git status `rm -rf /`',
400+
'npm install && echo "malicious"',
401+
'git status; curl http://evil.com',
402+
'tsc --watch || wget malware',
403+
];
404+
405+
maliciousCommands.forEach(cmd => {
406+
expect(cline.isAllowedCommand(cmd)).toBe(false);
407+
});
408+
});
409+
})
322410
});

0 commit comments

Comments
 (0)