Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit c0a84c0

Browse files
committed
feat: add Orchestrator prompt guidance for permissions and UI visibility
- Enhance Orchestrator customInstructions with guidance on using the permissions parameter (filePatterns, commandPatterns, allowedTools, deniedTools) including example use cases and most-restrictive-wins semantics explanation - Add permission boundaries display in the ChatRow newTask approval message so users can see what restrictions are being set before approving subtask creation - Add i18n translation keys for permission display - Add 8 new tests across packages/types and webview-ui
1 parent eb25622 commit c0a84c0

6 files changed

Lines changed: 261 additions & 2 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { describe, it, expect } from "vitest"
2+
import { DEFAULT_MODES } from "../mode.js"
3+
import type { ModeConfig } from "../mode.js"
4+
5+
describe("Orchestrator mode - permissions prompt guidance", () => {
6+
const orchestratorMode = DEFAULT_MODES.find((m: ModeConfig) => m.slug === "orchestrator")
7+
8+
it("should have the orchestrator mode defined", () => {
9+
expect(orchestratorMode).toBeDefined()
10+
})
11+
12+
it("should include permissions guidance in customInstructions", () => {
13+
expect(orchestratorMode!.customInstructions).toContain("permissions")
14+
expect(orchestratorMode!.customInstructions).toContain("filePatterns")
15+
expect(orchestratorMode!.customInstructions).toContain("commandPatterns")
16+
expect(orchestratorMode!.customInstructions).toContain("allowedTools")
17+
expect(orchestratorMode!.customInstructions).toContain("deniedTools")
18+
})
19+
20+
it("should mention most-restrictive-wins semantics", () => {
21+
expect(orchestratorMode!.customInstructions).toContain("most-restrictive-wins")
22+
})
23+
24+
it("should provide example use cases for permissions", () => {
25+
// Guidance about restricting file access
26+
expect(orchestratorMode!.customInstructions).toContain("specific directory")
27+
// Guidance about read-only research tasks
28+
expect(orchestratorMode!.customInstructions).toContain("read-only research")
29+
// Guidance about blocking shell access
30+
expect(orchestratorMode!.customInstructions).toContain("shell access")
31+
})
32+
})

packages/types/src/mode.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,6 @@ export const DEFAULT_MODES: readonly ModeConfig[] = [
222222
description: "Coordinate tasks across multiple modes",
223223
groups: [],
224224
customInstructions:
225-
"Your role is to coordinate complex workflows by delegating tasks to specialized modes. As an orchestrator, you should:\n\n1. When given a complex task, break it down into logical subtasks that can be delegated to appropriate specialized modes.\n\n2. For each subtask, use the `new_task` tool to delegate. Choose the most appropriate mode for the subtask's specific goal and provide comprehensive instructions in the `message` parameter. These instructions must include:\n * All necessary context from the parent task or previous subtasks required to complete the work.\n * A clearly defined scope, specifying exactly what the subtask should accomplish.\n * An explicit statement that the subtask should *only* perform the work outlined in these instructions and not deviate.\n * An instruction for the subtask to signal completion by using the `attempt_completion` tool, providing a concise yet thorough summary of the outcome in the `result` parameter, keeping in mind that this summary will be the source of truth used to keep track of what was completed on this project.\n * A statement that these specific instructions supersede any conflicting general instructions the subtask's mode might have.\n\n3. Track and manage the progress of all subtasks. When a subtask is completed, analyze its results and determine the next steps.\n\n4. Help the user understand how the different subtasks fit together in the overall workflow. Provide clear reasoning about why you're delegating specific tasks to specific modes.\n\n5. When all subtasks are completed, synthesize the results and provide a comprehensive overview of what was accomplished.\n\n6. Ask clarifying questions when necessary to better understand how to break down complex tasks effectively.\n\n7. Suggest improvements to the workflow based on the results of completed subtasks.\n\nUse subtasks to maintain clarity. If a request significantly shifts focus or requires a different expertise (mode), consider creating a subtask rather than overloading the current one.",
225+
'Your role is to coordinate complex workflows by delegating tasks to specialized modes. As an orchestrator, you should:\n\n1. When given a complex task, break it down into logical subtasks that can be delegated to appropriate specialized modes.\n\n2. For each subtask, use the `new_task` tool to delegate. Choose the most appropriate mode for the subtask\'s specific goal and provide comprehensive instructions in the `message` parameter. These instructions must include:\n * All necessary context from the parent task or previous subtasks required to complete the work.\n * A clearly defined scope, specifying exactly what the subtask should accomplish.\n * An explicit statement that the subtask should *only* perform the work outlined in these instructions and not deviate.\n * An instruction for the subtask to signal completion by using the `attempt_completion` tool, providing a concise yet thorough summary of the outcome in the `result` parameter, keeping in mind that this summary will be the source of truth used to keep track of what was completed on this project.\n * A statement that these specific instructions supersede any conflicting general instructions the subtask\'s mode might have.\n\n3. Track and manage the progress of all subtasks. When a subtask is completed, analyze its results and determine the next steps.\n\n4. Help the user understand how the different subtasks fit together in the overall workflow. Provide clear reasoning about why you\'re delegating specific tasks to specific modes.\n\n5. When all subtasks are completed, synthesize the results and provide a comprehensive overview of what was accomplished.\n\n6. Ask clarifying questions when necessary to better understand how to break down complex tasks effectively.\n\n7. Suggest improvements to the workflow based on the results of completed subtasks.\n\n8. When delegating subtasks, consider using the optional `permissions` parameter on `new_task` to restrict what the subtask can do. This is especially useful when:\n * The subtask should only modify files in a specific directory (use `filePatterns`, e.g. `["src/components/.*"]`).\n * The subtask should only run certain commands (use `commandPatterns`, e.g. `["npm test.*", "npm run lint"]`).\n * The subtask should be limited to specific tools (use `allowedTools`, e.g. `["read_file", "search_files"]` for read-only research tasks).\n * Certain tools should be explicitly blocked (use `deniedTools`, e.g. `["execute_command"]` to prevent shell access).\n Permissions are enforced at runtime and follow most-restrictive-wins semantics when subtasks are nested. Use them to keep subtasks focused and safe.\n\nUse subtasks to maintain clarity. If a request significantly shifts focus or requires a different expertise (mode), consider creating a subtask rather than overloading the current one.',
226226
},
227227
] as const

packages/types/src/vscode-extension-host.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -766,6 +766,13 @@ export interface ClineSayTool {
766766
description?: string
767767
// Properties for skill tool
768768
skill?: string
769+
// Properties for newTask tool - permission boundaries set by parent
770+
permissions?: {
771+
filePatterns?: string[]
772+
commandPatterns?: string[]
773+
allowedTools?: string[]
774+
deniedTools?: string[]
775+
}
769776
}
770777

771778
export interface ClineAskUseMcpServer {

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -869,6 +869,39 @@ export const ChatRowContent = ({
869869
</div>
870870
<div className="border-l border-muted-foreground/80 ml-2 pl-4 pb-1">
871871
<MarkdownBlock markdown={tool.content} />
872+
{tool.permissions && (
873+
<div className="mt-2 p-2 rounded text-xs text-vscode-descriptionForeground bg-vscode-editor-background border border-vscode-editorGroup-border">
874+
<div className="font-semibold mb-1">{t("chat:subtasks.permissionBoundaries")}</div>
875+
{tool.permissions.filePatterns && (
876+
<div>
877+
{t("chat:subtasks.permissionFilePatterns", {
878+
patterns: tool.permissions.filePatterns.join(", "),
879+
})}
880+
</div>
881+
)}
882+
{tool.permissions.commandPatterns && (
883+
<div>
884+
{t("chat:subtasks.permissionCommandPatterns", {
885+
patterns: tool.permissions.commandPatterns.join(", "),
886+
})}
887+
</div>
888+
)}
889+
{tool.permissions.allowedTools && (
890+
<div>
891+
{t("chat:subtasks.permissionAllowedTools", {
892+
tools: tool.permissions.allowedTools.join(", "),
893+
})}
894+
</div>
895+
)}
896+
{tool.permissions.deniedTools && (
897+
<div>
898+
{t("chat:subtasks.permissionDeniedTools", {
899+
tools: tool.permissions.deniedTools.join(", "),
900+
})}
901+
</div>
902+
)}
903+
</div>
904+
)}
872905
<div>
873906
{childTaskId && !isFollowedBySubtaskResult && (
874907
<button
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import React from "react"
2+
import { render, screen } from "@/utils/test-utils"
3+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
4+
import { ChatRowContent } from "../ChatRow"
5+
import type { HistoryItem, ClineMessage } from "@roo-code/types"
6+
7+
// Mock vscode API
8+
const mockPostMessage = vi.fn()
9+
vi.mock("@src/utils/vscode", () => ({
10+
vscode: {
11+
postMessage: (msg: unknown) => mockPostMessage(msg),
12+
},
13+
}))
14+
15+
// Mock i18n - return key-based strings so we can assert on the right keys
16+
vi.mock("react-i18next", () => ({
17+
useTranslation: () => ({
18+
t: (key: string, params?: Record<string, string>) => {
19+
const map: Record<string, string> = {
20+
"chat:subtasks.wantsToCreate": "Roo wants to create a new subtask",
21+
"chat:subtasks.permissionBoundaries": "Permission Boundaries",
22+
"chat:subtasks.goToSubtask": "Go to subtask",
23+
}
24+
if (key === "chat:subtasks.permissionFilePatterns" && params?.patterns) {
25+
return `Allowed files: ${params.patterns}`
26+
}
27+
if (key === "chat:subtasks.permissionCommandPatterns" && params?.patterns) {
28+
return `Allowed commands: ${params.patterns}`
29+
}
30+
if (key === "chat:subtasks.permissionAllowedTools" && params?.tools) {
31+
return `Allowed tools: ${params.tools}`
32+
}
33+
if (key === "chat:subtasks.permissionDeniedTools" && params?.tools) {
34+
return `Denied tools: ${params.tools}`
35+
}
36+
return map[key] ?? key
37+
},
38+
i18n: { exists: () => true },
39+
}),
40+
Trans: ({ children }: { children?: React.ReactNode }) => <>{children}</>,
41+
initReactI18next: { type: "3rdParty", init: () => {} },
42+
}))
43+
44+
// Mock extension state context
45+
let mockCurrentTaskItem: Partial<HistoryItem> | undefined = undefined
46+
let mockClineMessages: ClineMessage[] = []
47+
48+
vi.mock("@src/context/ExtensionStateContext", () => ({
49+
useExtensionState: () => ({
50+
mcpServers: [],
51+
alwaysAllowMcp: false,
52+
currentCheckpoint: null,
53+
mode: "code",
54+
apiConfiguration: {},
55+
clineMessages: mockClineMessages,
56+
currentTaskItem: mockCurrentTaskItem,
57+
}),
58+
}))
59+
60+
// Mock useSelectedModel hook
61+
vi.mock("@src/components/ui/hooks/useSelectedModel", () => ({
62+
useSelectedModel: () => ({ info: { supportsImages: true } }),
63+
}))
64+
65+
const queryClient = new QueryClient()
66+
67+
function renderChatRow(message: any, currentTaskItem?: Partial<HistoryItem>, clineMessages?: ClineMessage[]) {
68+
mockCurrentTaskItem = currentTaskItem
69+
mockClineMessages = clineMessages || [message]
70+
71+
return render(
72+
<QueryClientProvider client={queryClient}>
73+
<ChatRowContent
74+
message={message}
75+
isExpanded={false}
76+
isLast={false}
77+
isStreaming={false}
78+
onToggleExpand={() => {}}
79+
onSuggestionClick={() => {}}
80+
onBatchFileResponse={() => {}}
81+
onFollowUpUnmount={() => {}}
82+
isFollowUpAnswered={false}
83+
/>
84+
</QueryClientProvider>,
85+
)
86+
}
87+
88+
describe("ChatRow - permission boundaries display", () => {
89+
beforeEach(() => {
90+
mockPostMessage.mockClear()
91+
})
92+
93+
it("should display permission boundaries when permissions are set on a newTask", () => {
94+
const message = {
95+
ts: Date.now(),
96+
type: "ask" as const,
97+
ask: "tool" as const,
98+
text: JSON.stringify({
99+
tool: "newTask",
100+
mode: "code",
101+
content: "Edit the Button component",
102+
permissions: {
103+
filePatterns: ["src/components/.*"],
104+
commandPatterns: ["npm test.*"],
105+
deniedTools: ["execute_command"],
106+
},
107+
}),
108+
}
109+
110+
renderChatRow(message)
111+
112+
expect(screen.getByText("Permission Boundaries")).toBeInTheDocument()
113+
expect(screen.getByText("Allowed files: src/components/.*")).toBeInTheDocument()
114+
expect(screen.getByText("Allowed commands: npm test.*")).toBeInTheDocument()
115+
expect(screen.getByText("Denied tools: execute_command")).toBeInTheDocument()
116+
})
117+
118+
it("should display allowedTools when set", () => {
119+
const message = {
120+
ts: Date.now(),
121+
type: "ask" as const,
122+
ask: "tool" as const,
123+
text: JSON.stringify({
124+
tool: "newTask",
125+
mode: "ask",
126+
content: "Research the API",
127+
permissions: {
128+
allowedTools: ["read_file", "search_files", "codebase_search"],
129+
},
130+
}),
131+
}
132+
133+
renderChatRow(message)
134+
135+
expect(screen.getByText("Permission Boundaries")).toBeInTheDocument()
136+
expect(screen.getByText("Allowed tools: read_file, search_files, codebase_search")).toBeInTheDocument()
137+
})
138+
139+
it("should not display permission boundaries when permissions are not set", () => {
140+
const message = {
141+
ts: Date.now(),
142+
type: "ask" as const,
143+
ask: "tool" as const,
144+
text: JSON.stringify({
145+
tool: "newTask",
146+
mode: "code",
147+
content: "Implement feature X",
148+
}),
149+
}
150+
151+
renderChatRow(message)
152+
153+
expect(screen.queryByText("Permission Boundaries")).not.toBeInTheDocument()
154+
})
155+
156+
it("should display multiple permission types together", () => {
157+
const message = {
158+
ts: Date.now(),
159+
type: "ask" as const,
160+
ask: "tool" as const,
161+
text: JSON.stringify({
162+
tool: "newTask",
163+
mode: "code",
164+
content: "Edit and test components",
165+
permissions: {
166+
filePatterns: ["src/components/.*", "src/utils/.*"],
167+
commandPatterns: ["npm test.*", "npm run lint"],
168+
allowedTools: ["read_file", "write_to_file", "execute_command"],
169+
deniedTools: ["apply_patch"],
170+
},
171+
}),
172+
}
173+
174+
renderChatRow(message)
175+
176+
expect(screen.getByText("Permission Boundaries")).toBeInTheDocument()
177+
expect(screen.getByText("Allowed files: src/components/.*, src/utils/.*")).toBeInTheDocument()
178+
expect(screen.getByText("Allowed commands: npm test.*, npm run lint")).toBeInTheDocument()
179+
expect(screen.getByText("Allowed tools: read_file, write_to_file, execute_command")).toBeInTheDocument()
180+
expect(screen.getByText("Denied tools: apply_patch")).toBeInTheDocument()
181+
})
182+
})

webview-ui/src/i18n/locales/en/chat.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,12 @@
301301
"resultContent": "Subtask completed",
302302
"defaultResult": "Please continue to the next task.",
303303
"completionInstructions": "You can review the results and suggest any corrections or next steps. If everything looks good, confirm to return the result to the parent task.",
304-
"goToSubtask": "View task"
304+
"goToSubtask": "View task",
305+
"permissionBoundaries": "Permission Boundaries",
306+
"permissionFilePatterns": "Allowed files: {{patterns}}",
307+
"permissionCommandPatterns": "Allowed commands: {{patterns}}",
308+
"permissionAllowedTools": "Allowed tools: {{tools}}",
309+
"permissionDeniedTools": "Denied tools: {{tools}}"
305310
},
306311
"questions": {
307312
"hasQuestion": "Roo has a question"

0 commit comments

Comments
 (0)