Skip to content

Commit 7603884

Browse files
committed
Additional checkbox for auto-approving reads and writes outside of the workspace
1 parent 4fa4943 commit 7603884

28 files changed

+598
-43
lines changed

src/core/Cline.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import pWaitFor from "p-wait-for"
1111
import getFolderSize from "get-folder-size"
1212
import { serializeError } from "serialize-error"
1313
import * as vscode from "vscode"
14+
import { isPathOutsideWorkspace } from "../utils/pathUtils"
1415

1516
import { TokenUsage } from "../exports/roo-code"
1617
import { ApiHandler, buildApiHandler } from "../api"
@@ -1606,9 +1607,14 @@ export class Cline extends EventEmitter<ClineEvents> {
16061607
}
16071608
}
16081609

1610+
// Determine if the path is outside the workspace
1611+
const fullPath = relPath ? path.resolve(this.cwd, removeClosingTag("path", relPath)) : ""
1612+
const isOutsideWorkspace = isPathOutsideWorkspace(fullPath)
1613+
16091614
const sharedMessageProps: ClineSayTool = {
16101615
tool: fileExists ? "editedExistingFile" : "newFileCreated",
16111616
path: getReadablePath(this.cwd, removeClosingTag("path", relPath)),
1617+
isOutsideWorkspace,
16121618
}
16131619
try {
16141620
if (block.partial) {
@@ -2245,9 +2251,15 @@ export class Cline extends EventEmitter<ClineEvents> {
22452251
const relPath: string | undefined = block.params.path
22462252
const startLineStr: string | undefined = block.params.start_line
22472253
const endLineStr: string | undefined = block.params.end_line
2254+
2255+
// Get the full path and determine if it's outside the workspace
2256+
const fullPath = relPath ? path.resolve(this.cwd, removeClosingTag("path", relPath)) : ""
2257+
const isOutsideWorkspace = isPathOutsideWorkspace(fullPath)
2258+
22482259
const sharedMessageProps: ClineSayTool = {
22492260
tool: "readFile",
22502261
path: getReadablePath(this.cwd, removeClosingTag("path", relPath)),
2262+
isOutsideWorkspace,
22512263
}
22522264
try {
22532265
if (block.partial) {

src/core/webview/ClineProvider.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -971,10 +971,18 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
971971
await this.updateGlobalState("alwaysAllowReadOnly", message.bool ?? undefined)
972972
await this.postStateToWebview()
973973
break
974+
case "alwaysAllowReadOnlyOutsideWorkspace":
975+
await this.updateGlobalState("alwaysAllowReadOnlyOutsideWorkspace", message.bool ?? undefined)
976+
await this.postStateToWebview()
977+
break
974978
case "alwaysAllowWrite":
975979
await this.updateGlobalState("alwaysAllowWrite", message.bool ?? undefined)
976980
await this.postStateToWebview()
977981
break
982+
case "alwaysAllowWriteOutsideWorkspace":
983+
await this.updateGlobalState("alwaysAllowWriteOutsideWorkspace", message.bool ?? undefined)
984+
await this.postStateToWebview()
985+
break
978986
case "alwaysAllowExecute":
979987
await this.updateGlobalState("alwaysAllowExecute", message.bool ?? undefined)
980988
await this.postStateToWebview()
@@ -2490,7 +2498,9 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
24902498
lastShownAnnouncementId,
24912499
customInstructions,
24922500
alwaysAllowReadOnly,
2501+
alwaysAllowReadOnlyOutsideWorkspace,
24932502
alwaysAllowWrite,
2503+
alwaysAllowWriteOutsideWorkspace,
24942504
alwaysAllowExecute,
24952505
alwaysAllowBrowser,
24962506
alwaysAllowMcp,
@@ -2544,7 +2554,9 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
25442554
apiConfiguration,
25452555
customInstructions,
25462556
alwaysAllowReadOnly: alwaysAllowReadOnly ?? false,
2557+
alwaysAllowReadOnlyOutsideWorkspace: alwaysAllowReadOnlyOutsideWorkspace ?? false,
25472558
alwaysAllowWrite: alwaysAllowWrite ?? false,
2559+
alwaysAllowWriteOutsideWorkspace: alwaysAllowWriteOutsideWorkspace ?? false,
25482560
alwaysAllowExecute: alwaysAllowExecute ?? false,
25492561
alwaysAllowBrowser: alwaysAllowBrowser ?? false,
25502562
alwaysAllowMcp: alwaysAllowMcp ?? false,
@@ -2707,7 +2719,9 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
27072719
lastShownAnnouncementId: stateValues.lastShownAnnouncementId,
27082720
customInstructions: stateValues.customInstructions,
27092721
alwaysAllowReadOnly: stateValues.alwaysAllowReadOnly ?? false,
2722+
alwaysAllowReadOnlyOutsideWorkspace: stateValues.alwaysAllowReadOnlyOutsideWorkspace ?? false,
27102723
alwaysAllowWrite: stateValues.alwaysAllowWrite ?? false,
2724+
alwaysAllowWriteOutsideWorkspace: stateValues.alwaysAllowWriteOutsideWorkspace ?? false,
27112725
alwaysAllowExecute: stateValues.alwaysAllowExecute ?? false,
27122726
alwaysAllowBrowser: stateValues.alwaysAllowBrowser ?? false,
27132727
alwaysAllowMcp: stateValues.alwaysAllowMcp ?? false,

src/core/webview/__tests__/ClineProvider.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,9 @@ describe("ClineProvider", () => {
434434
},
435435
customInstructions: undefined,
436436
alwaysAllowReadOnly: false,
437+
alwaysAllowReadOnlyOutsideWorkspace: false,
437438
alwaysAllowWrite: false,
439+
alwaysAllowWriteOutsideWorkspace: false,
438440
alwaysAllowExecute: false,
439441
alwaysAllowBrowser: false,
440442
alwaysAllowMcp: false,

src/exports/roo-code.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,9 @@ export type GlobalStateKey =
182182
| "lastShownAnnouncementId"
183183
| "customInstructions"
184184
| "alwaysAllowReadOnly"
185+
| "alwaysAllowReadOnlyOutsideWorkspace"
185186
| "alwaysAllowWrite"
187+
| "alwaysAllowWriteOutsideWorkspace"
186188
| "alwaysAllowExecute"
187189
| "alwaysAllowBrowser"
188190
| "alwaysAllowMcp"

src/shared/ExtensionMessage.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,9 @@ export interface ExtensionState {
120120
customModePrompts?: CustomModePrompts
121121
customSupportPrompts?: CustomSupportPrompts
122122
alwaysAllowReadOnly?: boolean
123+
alwaysAllowReadOnlyOutsideWorkspace?: boolean
123124
alwaysAllowWrite?: boolean
125+
alwaysAllowWriteOutsideWorkspace?: boolean
124126
alwaysAllowExecute?: boolean
125127
alwaysAllowBrowser?: boolean
126128
alwaysAllowMcp?: boolean
@@ -192,6 +194,7 @@ export interface ClineSayTool {
192194
filePattern?: string
193195
mode?: string
194196
reason?: string
197+
isOutsideWorkspace?: boolean
195198
}
196199

197200
// Must keep in sync with system prompt.

src/shared/WebviewMessage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ export interface WebviewMessage {
2222
| "customInstructions"
2323
| "allowedCommands"
2424
| "alwaysAllowReadOnly"
25+
| "alwaysAllowReadOnlyOutsideWorkspace"
2526
| "alwaysAllowWrite"
27+
| "alwaysAllowWriteOutsideWorkspace"
2628
| "alwaysAllowExecute"
2729
| "webviewDidLaunch"
2830
| "newTask"

src/shared/globalState.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@ export const GLOBAL_STATE_KEYS = [
4949
"lastShownAnnouncementId",
5050
"customInstructions",
5151
"alwaysAllowReadOnly",
52+
"alwaysAllowReadOnlyOutsideWorkspace",
5253
"alwaysAllowWrite",
54+
"alwaysAllowWriteOutsideWorkspace",
5355
"alwaysAllowExecute",
5456
"alwaysAllowBrowser",
5557
"alwaysAllowMcp",

src/utils/pathUtils.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import * as vscode from "vscode"
2+
import * as path from "path"
3+
4+
/**
5+
* Checks if a file path is outside all workspace folders
6+
* @param filePath The file path to check
7+
* @returns true if the path is outside all workspace folders, false otherwise
8+
*/
9+
export function isPathOutsideWorkspace(filePath: string): boolean {
10+
// If there are no workspace folders, consider everything outside workspace for safety
11+
if (!vscode.workspace.workspaceFolders || vscode.workspace.workspaceFolders.length === 0) {
12+
return true
13+
}
14+
15+
// Normalize and resolve the path to handle .. and . components correctly
16+
const absolutePath = path.resolve(filePath)
17+
18+
// Check if the path is within any workspace folder
19+
return !vscode.workspace.workspaceFolders.some((folder) => {
20+
const folderPath = folder.uri.fsPath
21+
// Path is inside a workspace if it equals the workspace path or is a subfolder
22+
return absolutePath === folderPath || absolutePath.startsWith(folderPath + path.sep)
23+
})
24+
}

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

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
5555
mcpServers,
5656
alwaysAllowBrowser,
5757
alwaysAllowReadOnly,
58+
alwaysAllowReadOnlyOutsideWorkspace,
5859
alwaysAllowWrite,
60+
alwaysAllowWriteOutsideWorkspace,
5961
alwaysAllowExecute,
6062
alwaysAllowMcp,
6163
allowedCommands,
@@ -649,26 +651,60 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
649651
(message: ClineMessage | undefined) => {
650652
if (!autoApprovalEnabled || !message || message.type !== "ask") return false
651653

652-
return (
653-
(alwaysAllowBrowser && message.ask === "browser_action_launch") ||
654-
(alwaysAllowReadOnly && message.ask === "tool" && isReadOnlyToolAction(message)) ||
655-
(alwaysAllowWrite && message.ask === "tool" && isWriteToolAction(message)) ||
656-
(alwaysAllowExecute && message.ask === "command" && isAllowedCommand(message)) ||
657-
(alwaysAllowMcp && message.ask === "use_mcp_server" && isMcpToolAlwaysAllowed(message)) ||
658-
(alwaysAllowModeSwitch &&
659-
message.ask === "tool" &&
660-
JSON.parse(message.text || "{}")?.tool === "switchMode") ||
661-
(alwaysAllowSubtasks &&
662-
message.ask === "tool" &&
663-
["newTask", "finishTask"].includes(JSON.parse(message.text || "{}")?.tool))
664-
)
654+
if (message.ask === "browser_action_launch") {
655+
return alwaysAllowBrowser
656+
}
657+
658+
if (message.ask === "use_mcp_server") {
659+
return alwaysAllowMcp && isMcpToolAlwaysAllowed(message)
660+
}
661+
662+
if (message.ask === "command") {
663+
return alwaysAllowExecute && isAllowedCommand(message)
664+
}
665+
666+
// For read/write operations, check if it's outside workspace and if we have permission for that
667+
if (message.ask === "tool") {
668+
let tool: any = {}
669+
try {
670+
tool = JSON.parse(message.text || "{}")
671+
} catch (error) {
672+
console.error("Failed to parse tool:", error)
673+
}
674+
675+
if (!tool) {
676+
return false
677+
}
678+
679+
if (tool?.tool === "switchMode") {
680+
return alwaysAllowModeSwitch
681+
}
682+
683+
if (["newTask", "finishTask"].includes(tool?.tool)) {
684+
return alwaysAllowSubtasks
685+
}
686+
687+
const isOutsideWorkspace = !!tool.isOutsideWorkspace
688+
689+
if (isReadOnlyToolAction(message)) {
690+
return alwaysAllowReadOnly && (!isOutsideWorkspace || alwaysAllowReadOnlyOutsideWorkspace)
691+
}
692+
693+
if (isWriteToolAction(message)) {
694+
return alwaysAllowWrite && (!isOutsideWorkspace || alwaysAllowWriteOutsideWorkspace)
695+
}
696+
}
697+
698+
return false
665699
},
666700
[
667701
autoApprovalEnabled,
668702
alwaysAllowBrowser,
669703
alwaysAllowReadOnly,
704+
alwaysAllowReadOnlyOutsideWorkspace,
670705
isReadOnlyToolAction,
671706
alwaysAllowWrite,
707+
alwaysAllowWriteOutsideWorkspace,
672708
isWriteToolAction,
673709
alwaysAllowExecute,
674710
isAllowedCommand,
@@ -1047,7 +1083,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
10471083
handlePrimaryButtonClick,
10481084
alwaysAllowBrowser,
10491085
alwaysAllowReadOnly,
1086+
alwaysAllowReadOnlyOutsideWorkspace,
10501087
alwaysAllowWrite,
1088+
alwaysAllowWriteOutsideWorkspace,
10511089
alwaysAllowExecute,
10521090
alwaysAllowMcp,
10531091
messages,

0 commit comments

Comments
 (0)