Skip to content

Commit ccc8e47

Browse files
ENG-320/Auto-approve controls to restrict Cline actions outside of workspace (RooCodeInc#2779)
* initial- buttons * more buttons * incremental * increment * Renamed old auto approve name * comments * started read options * paused here * cleanup * de-duplication and renames * renames * restored unrelated test file * labels and semantics * fixed labels issue * cleanup/dedup * minor semantics * cleanup * changeset * one line * ellipsis-dev changes * reverting settings names * made new settings optional * Update webview-ui/src/components/chat/AutoApproveMenu.tsx Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * testserver fix * testserver.ts fix / prettier * testserver.ts fix / prettier * Delete src/services/test/TestServer.ts * restored testserver.ts --------- Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
1 parent 9859136 commit ccc8e47

File tree

5 files changed

+136
-39
lines changed

5 files changed

+136
-39
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"claude-dev": minor
3+
---
4+
5+
Added auto-approve options for edits/reads outside of the users workspace

src/core/task/index.ts

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1189,22 +1189,28 @@ export class Task {
11891189
}
11901190

11911191
// Check if the tool should be auto-approved based on the settings
1192-
// Returns bool for most tools, tuple for execute_command (and future nested auto appoved settings)
1192+
// Returns bool for most tools, and tuple for tools with nested settings
11931193
shouldAutoApproveTool(toolName: ToolUseName): boolean | [boolean, boolean] {
11941194
if (this.autoApprovalSettings.enabled) {
11951195
switch (toolName) {
11961196
case "read_file":
11971197
case "list_files":
11981198
case "list_code_definition_names":
11991199
case "search_files":
1200-
return this.autoApprovalSettings.actions.readFiles
1200+
return [
1201+
this.autoApprovalSettings.actions.readFiles,
1202+
this.autoApprovalSettings.actions.readFilesExternally ?? false,
1203+
]
12011204
case "write_to_file":
12021205
case "replace_in_file":
1203-
return this.autoApprovalSettings.actions.editFiles
1206+
return [
1207+
this.autoApprovalSettings.actions.editFiles,
1208+
this.autoApprovalSettings.actions.editFilesExternally ?? false,
1209+
]
12041210
case "execute_command":
12051211
return [
12061212
this.autoApprovalSettings.actions.executeSafeCommands,
1207-
this.autoApprovalSettings.actions.executeAllCommands,
1213+
this.autoApprovalSettings.actions.executeAllCommands ?? false,
12081214
]
12091215
case "browser_action":
12101216
return this.autoApprovalSettings.actions.useBrowser
@@ -1216,6 +1222,32 @@ export class Task {
12161222
return false
12171223
}
12181224

1225+
// Check if the tool should be auto-approved based on the settings
1226+
// and the path of the action. Returns true if the tool should be auto-approved
1227+
// based on the user's settings and the path of the action.
1228+
shouldAutoApproveToolWithPath(blockname: ToolUseName, autoApproveActionpath: string | undefined): boolean {
1229+
let isLocalRead: boolean = false
1230+
if (autoApproveActionpath) {
1231+
const absolutePath = path.resolve(cwd, autoApproveActionpath)
1232+
isLocalRead = absolutePath.startsWith(cwd)
1233+
} else {
1234+
// If we do not get a path for some reason, default to a (safer) false return
1235+
isLocalRead = false
1236+
}
1237+
1238+
// Get auto-approve settings for local and external edits
1239+
const autoApproveResult = this.shouldAutoApproveTool(blockname)
1240+
const [autoApproveLocal, autoApproveExternal] = Array.isArray(autoApproveResult)
1241+
? autoApproveResult
1242+
: [autoApproveResult, false]
1243+
1244+
if ((isLocalRead && autoApproveLocal) || (!isLocalRead && autoApproveLocal && autoApproveExternal)) {
1245+
return true
1246+
} else {
1247+
return false
1248+
}
1249+
}
1250+
12191251
private formatErrorWithStatusCode(error: any): string {
12201252
const statusCode = error.status || error.statusCode || (error.response && error.response.status)
12211253
const message = error.message ?? JSON.stringify(serializeError(error), null, 2)
@@ -1754,7 +1786,8 @@ export class Task {
17541786
if (block.partial) {
17551787
// update gui message
17561788
const partialMessage = JSON.stringify(sharedMessageProps)
1757-
if (this.shouldAutoApproveTool(block.name)) {
1789+
1790+
if (this.shouldAutoApproveToolWithPath(block.name, relPath)) {
17581791
this.removeLastPartialMessageIfExistsWithType("ask", "tool") // in case the user changes auto-approval settings mid stream
17591792
await this.say("tool", partialMessage, undefined, block.partial)
17601793
} else {
@@ -1818,8 +1851,7 @@ export class Task {
18181851
// )
18191852
// : undefined,
18201853
} satisfies ClineSayTool)
1821-
1822-
if (this.shouldAutoApproveTool(block.name)) {
1854+
if (this.shouldAutoApproveToolWithPath(block.name, relPath)) {
18231855
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
18241856
await this.say("tool", completeMessage, undefined, false)
18251857
this.consecutiveAutoApprovedRequestsCount++
@@ -1941,7 +1973,7 @@ export class Task {
19411973
...sharedMessageProps,
19421974
content: undefined,
19431975
} satisfies ClineSayTool)
1944-
if (this.shouldAutoApproveTool(block.name)) {
1976+
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
19451977
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
19461978
await this.say("tool", partialMessage, undefined, block.partial)
19471979
} else {
@@ -1971,7 +2003,7 @@ export class Task {
19712003
...sharedMessageProps,
19722004
content: absolutePath,
19732005
} satisfies ClineSayTool)
1974-
if (this.shouldAutoApproveTool(block.name)) {
2006+
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
19752007
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
19762008
await this.say("tool", completeMessage, undefined, false) // need to be sending partialValue bool, since undefined has its own purpose in that the message is treated neither as a partial or completion of a partial, but as a single complete message
19772009
this.consecutiveAutoApprovedRequestsCount++
@@ -2019,7 +2051,7 @@ export class Task {
20192051
...sharedMessageProps,
20202052
content: "",
20212053
} satisfies ClineSayTool)
2022-
if (this.shouldAutoApproveTool(block.name)) {
2054+
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
20232055
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
20242056
await this.say("tool", partialMessage, undefined, block.partial)
20252057
} else {
@@ -2050,7 +2082,7 @@ export class Task {
20502082
...sharedMessageProps,
20512083
content: result,
20522084
} satisfies ClineSayTool)
2053-
if (this.shouldAutoApproveTool(block.name)) {
2085+
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
20542086
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
20552087
await this.say("tool", completeMessage, undefined, false)
20562088
this.consecutiveAutoApprovedRequestsCount++
@@ -2090,7 +2122,7 @@ export class Task {
20902122
...sharedMessageProps,
20912123
content: "",
20922124
} satisfies ClineSayTool)
2093-
if (this.shouldAutoApproveTool(block.name)) {
2125+
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
20942126
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
20952127
await this.say("tool", partialMessage, undefined, block.partial)
20962128
} else {
@@ -2118,7 +2150,7 @@ export class Task {
21182150
...sharedMessageProps,
21192151
content: result,
21202152
} satisfies ClineSayTool)
2121-
if (this.shouldAutoApproveTool(block.name)) {
2153+
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
21222154
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
21232155
await this.say("tool", completeMessage, undefined, false)
21242156
this.consecutiveAutoApprovedRequestsCount++
@@ -2162,7 +2194,7 @@ export class Task {
21622194
...sharedMessageProps,
21632195
content: "",
21642196
} satisfies ClineSayTool)
2165-
if (this.shouldAutoApproveTool(block.name)) {
2197+
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
21662198
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
21672199
await this.say("tool", partialMessage, undefined, block.partial)
21682200
} else {
@@ -2198,7 +2230,7 @@ export class Task {
21982230
...sharedMessageProps,
21992231
content: results,
22002232
} satisfies ClineSayTool)
2201-
if (this.shouldAutoApproveTool(block.name)) {
2233+
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
22022234
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
22032235
await this.say("tool", completeMessage, undefined, false)
22042236
this.consecutiveAutoApprovedRequestsCount++

src/services/test/TestServer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ async function updateAutoApprovalSettings(context: vscode.ExtensionContext, prov
2323
enabled: true,
2424
actions: {
2525
readFiles: true,
26+
readFilesExternally: true,
2627
editFiles: true,
28+
editFilesExternally: true,
2729
executeSafeCommands: true,
2830
executeAllCommands: true,
2931
useBrowser: false, // Keep browser disabled for tests

src/shared/AutoApprovalSettings.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ export interface AutoApprovalSettings {
33
enabled: boolean
44
// Individual action permissions
55
actions: {
6-
readFiles: boolean // Read files and directories
7-
editFiles: boolean // Edit files
6+
readFiles: boolean // Read files and directories in the working directory
7+
readFilesExternally: boolean // Read files and directories outside of the working directory
8+
editFiles: boolean // Edit files in the working directory
9+
editFilesExternally: boolean // Edit files outside of the working directory
810
executeSafeCommands: boolean // Execute safe commands
911
executeAllCommands: boolean // Execute all commands
1012
useBrowser: boolean // Use browser
@@ -19,7 +21,9 @@ export const DEFAULT_AUTO_APPROVAL_SETTINGS: AutoApprovalSettings = {
1921
enabled: false,
2022
actions: {
2123
readFiles: false,
24+
readFilesExternally: false,
2225
editFiles: false,
26+
editFilesExternally: false,
2327
executeSafeCommands: false,
2428
executeAllCommands: false,
2529
useBrowser: false,

webview-ui/src/components/chat/AutoApproveMenu.tsx

Lines changed: 76 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,40 +27,52 @@ const ACTION_METADATA: {
2727
}[] = [
2828
{
2929
id: "readFiles",
30-
label: "Read files and directories",
31-
shortName: "Read",
32-
description: "Allows access to read any file on your computer.",
30+
label: "Read local files and directories",
31+
shortName: "Read Local",
32+
description: "Allows Cline to read files within your workspace.",
33+
},
34+
{
35+
id: "readFilesExternally",
36+
label: "Read files and directories anywhere",
37+
shortName: "Read (all)",
38+
description: "Allows Cline to read any file on your computer.",
3339
},
3440
{
3541
id: "editFiles",
36-
label: "Edit files",
42+
label: "Edit local files",
3743
shortName: "Edit",
38-
description: "Allows modification of any files on your computer.",
44+
description: "Allows Cline to modify files within your workspace.",
45+
},
46+
{
47+
id: "editFilesExternally",
48+
label: "Edit files anywhere",
49+
shortName: "Edit (all)",
50+
description: "Allows Cline to modify any file on your computer.",
3951
},
4052
{
4153
id: "executeSafeCommands",
4254
label: "Execute safe commands",
4355
shortName: "Safe Commands",
4456
description:
45-
"Allows execution of safe terminal commands. If the model determines a command is potentially destructive, it will still require approval.",
57+
"Allows Cline to execute of safe terminal commands. If the model determines a command is potentially destructive, it will still require approval.",
4658
},
4759
{
4860
id: "executeAllCommands",
4961
label: "Execute all commands",
5062
shortName: "All Commands",
51-
description: "Allows execution of all terminal commands. Use at your own risk.",
63+
description: "Allows Cline to execute all terminal commands. Use at your own risk.",
5264
},
5365
{
5466
id: "useBrowser",
5567
label: "Use the browser",
5668
shortName: "Browser",
57-
description: "Allows ability to launch and interact with any website in a headless browser.",
69+
description: "Allows Cline to launch and interact with any website in a browser.",
5870
},
5971
{
6072
id: "useMcp",
6173
label: "Use MCP servers",
6274
shortName: "MCP",
63-
description: "Allows use of configured MCP servers which may modify filesystem or interact with APIs.",
75+
description: "Allows Cline to use configured MCP servers which may modify filesystem or interact with APIs.",
6476
},
6577
]
6678

@@ -72,21 +84,53 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
7284

7385
const enabledActions = ACTION_METADATA.filter((action) => autoApprovalSettings.actions[action.id])
7486
const enabledActionsList = (() => {
75-
// "All Commands" is the only label displayed if both are set
76-
const safeCommandsEnabled = enabledActions.some((action) => action.id === "executeSafeCommands")
77-
const allCommandsEnabled = enabledActions.some((action) => action.id === "executeAllCommands")
87+
// When nested auto-approve options are used, display the more permissive one (file reads, edits, and commands)
88+
const readFilesEnabled = enabledActions.some((action) => action.id === "readFiles")
89+
const readFilesExternallyEnabled = enabledActions.some((action) => action.id === "readFilesExternally")
90+
91+
const editFilesEnabled = enabledActions.some((action) => action.id === "editFiles")
92+
const editFilesExternallyEnabled = enabledActions.some((action) => action.id === "editFilesExternally") ?? false
7893

94+
const safeCommandsEnabled = enabledActions.some((action) => action.id === "executeSafeCommands")
95+
const allCommandsEnabled = enabledActions.some((action) => action.id === "executeAllCommands") ?? false
96+
// Filter out the potentially nested options so we don't display them twice
7997
const otherActions = enabledActions
80-
.filter((action) => action.id !== "executeSafeCommands" && action.id !== "executeAllCommands")
98+
.filter(
99+
(action) =>
100+
action.id !== "readFiles" &&
101+
action.id !== "readFilesExternally" &&
102+
action.id !== "editFiles" &&
103+
action.id !== "editFilesExternally" &&
104+
action.id !== "executeSafeCommands" &&
105+
action.id !== "executeAllCommands",
106+
)
81107
.map((action) => action.shortName)
82108

83-
if (allCommandsEnabled) {
84-
return ["All Commands", ...otherActions].join(", ")
109+
const labels = []
110+
111+
// Handle read editing labels
112+
if ((readFilesExternallyEnabled ?? false) && readFilesEnabled) {
113+
labels.push("Read (All)")
114+
} else if (readFilesEnabled) {
115+
labels.push("Read")
116+
}
117+
118+
// Handle file editing labels
119+
if ((editFilesExternallyEnabled ?? false) && editFilesEnabled) {
120+
labels.push("Edit (All)")
121+
} else if (editFilesEnabled) {
122+
labels.push("Edit")
123+
}
124+
125+
// Handle command execution labels
126+
if ((allCommandsEnabled ?? false) && safeCommandsEnabled) {
127+
labels.push("All Commands")
85128
} else if (safeCommandsEnabled) {
86-
return ["Safe Commands", ...otherActions].join(", ")
87-
} else {
88-
return otherActions.join(", ")
129+
labels.push("Safe Commands")
89130
}
131+
132+
// Add remaining actions
133+
return [...labels, ...otherActions].join(", ")
90134
})()
91135
const hasEnabledActions = enabledActions.length > 0
92136

@@ -251,13 +295,23 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
251295
caution and only enable if you understand the risks.
252296
</div>
253297
{ACTION_METADATA.map((action) => {
254-
if (action.id === "executeAllCommands") {
298+
// Handle readFilesExternally, editFilesExternally, and executeAllCommands as animated sub-options
299+
if (
300+
action.id === "executeAllCommands" ||
301+
action.id === "editFilesExternally" ||
302+
action.id === "readFilesExternally"
303+
) {
304+
const parentAction =
305+
action.id === "executeAllCommands"
306+
? "executeSafeCommands"
307+
: action.id === "readFilesExternally"
308+
? "readFiles"
309+
: "editFiles"
255310
return (
256-
// Option to make the "Approve All" option animate into the menu when "Approve Safe" is enabled
257-
<SubOptionAnimateIn key={action.id} show={autoApprovalSettings.actions.executeSafeCommands}>
311+
<SubOptionAnimateIn key={action.id} show={autoApprovalSettings.actions[parentAction]}>
258312
<div
259313
style={{
260-
margin: "6px 0",
314+
margin: "3px 0",
261315
marginLeft: "28px",
262316
}}>
263317
<VSCodeCheckbox

0 commit comments

Comments
 (0)