mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-25 05:53:57 +00:00
feat(swarm-p4): leader permission bridge for concurrent worker tool approval
Workers that are running concurrently (parent still alive in task Map) now route tool-approval through LeaderPermissionBridge instead of surfacing a UI prompt on the hidden worker task. - LeaderPermissionBridge: module-level singleton, register/submit/dialog - presentAssistantMessage: intercept askApproval for concurrent workers - ClineProvider: register showWorkerPermissionDialog on construction - events/task.ts: PermissionRequested + PermissionResolved event types Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4705627fe4
commit
a49d9614b0
5 changed files with 149 additions and 1 deletions
|
|
@ -57,6 +57,8 @@ export enum RooCodeEventName {
|
|||
WorkerRegistered = "workerRegistered",
|
||||
WorkerIdle = "workerIdle",
|
||||
WorkerShutdown = "workerShutdown",
|
||||
PermissionRequested = "permissionRequested",
|
||||
PermissionResolved = "permissionResolved",
|
||||
|
||||
// Evals
|
||||
EvalPass = "evalPass",
|
||||
|
|
@ -124,6 +126,8 @@ export const rooCodeEventsSchema = z.object({
|
|||
[RooCodeEventName.WorkerRegistered]: z.tuple([z.string(), z.string(), z.string(), z.string()]), // [sessionId, taskId, agentName, agentColor]
|
||||
[RooCodeEventName.WorkerIdle]: z.tuple([z.string(), z.string()]), // [sessionId, taskId]
|
||||
[RooCodeEventName.WorkerShutdown]: z.tuple([z.string(), z.string()]), // [sessionId, taskId]
|
||||
[RooCodeEventName.PermissionRequested]: z.tuple([z.string(), z.string(), z.string()]), // [workerTaskId, toolName, requestId]
|
||||
[RooCodeEventName.PermissionResolved]: z.tuple([z.string(), z.string(), z.boolean()]), // [workerTaskId, requestId, allowed]
|
||||
|
||||
[RooCodeEventName.ModeChanged]: z.tuple([z.string()]),
|
||||
[RooCodeEventName.ProviderProfileChanged]: z.tuple([z.object({ name: z.string(), provider: z.string() })]),
|
||||
|
|
@ -313,6 +317,16 @@ export const taskEventSchema = z.discriminatedUnion("eventName", [
|
|||
payload: rooCodeEventsSchema.shape[RooCodeEventName.WorkerShutdown],
|
||||
taskId: z.number().optional(),
|
||||
}),
|
||||
z.object({
|
||||
eventName: z.literal(RooCodeEventName.PermissionRequested),
|
||||
payload: rooCodeEventsSchema.shape[RooCodeEventName.PermissionRequested],
|
||||
taskId: z.number().optional(),
|
||||
}),
|
||||
z.object({
|
||||
eventName: z.literal(RooCodeEventName.PermissionResolved),
|
||||
payload: rooCodeEventsSchema.shape[RooCodeEventName.PermissionResolved],
|
||||
taskId: z.number().optional(),
|
||||
}),
|
||||
|
||||
// Evals
|
||||
z.object({
|
||||
|
|
|
|||
|
|
@ -91,6 +91,8 @@ export type TaskProviderEvents = {
|
|||
[RooCodeEventName.WorkerRegistered]: [sessionId: string, taskId: string, agentName: string, agentColor: string]
|
||||
[RooCodeEventName.WorkerIdle]: [sessionId: string, taskId: string]
|
||||
[RooCodeEventName.WorkerShutdown]: [sessionId: string, taskId: string]
|
||||
[RooCodeEventName.PermissionRequested]: [workerTaskId: string, toolName: string, requestId: string]
|
||||
[RooCodeEventName.PermissionResolved]: [workerTaskId: string, requestId: string, allowed: boolean]
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import { serializeError } from "serialize-error"
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
|
||||
import type { ToolName, ClineAsk, ToolProgressStatus } from "@roo-code/types"
|
||||
import { ConsecutiveMistakeError, TelemetryEventName } from "@roo-code/types"
|
||||
import { ConsecutiveMistakeError, TelemetryEventName, RooCodeEventName } from "@roo-code/types"
|
||||
import { submitWorkerPermissionRequest } from "../swarm/LeaderPermissionBridge"
|
||||
import { TelemetryService } from "@roo-code/telemetry"
|
||||
import { customToolRegistry } from "@roo-code/core"
|
||||
|
||||
|
|
@ -504,6 +505,49 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
progressStatus?: ToolProgressStatus,
|
||||
isProtected?: boolean,
|
||||
) => {
|
||||
// Concurrent workers route tool-approval through the leader permission bridge
|
||||
// instead of surfacing a UI prompt on the worker task itself.
|
||||
if (type === "tool" && cline.parentTaskId) {
|
||||
type PermissionProvider = {
|
||||
getTaskById?: (id: string) => unknown
|
||||
swarmRegistry?: {
|
||||
getSessionForTask(
|
||||
taskId: string,
|
||||
):
|
||||
| {
|
||||
sessionId: string
|
||||
teammates: Record<
|
||||
string,
|
||||
{ agentName: string; color: import("@roo-code/types").AgentColorName }
|
||||
>
|
||||
}
|
||||
| undefined
|
||||
}
|
||||
}
|
||||
const provider = cline.providerRef.deref() as PermissionProvider | undefined
|
||||
const parentIsAlive = provider?.getTaskById?.(cline.parentTaskId) !== undefined
|
||||
if (parentIsAlive) {
|
||||
const session = provider?.swarmRegistry?.getSessionForTask(cline.taskId)
|
||||
const identity = session?.teammates[cline.taskId]
|
||||
const agentName = identity?.agentName ?? "Worker"
|
||||
const color = identity?.color ?? ("blue" as import("@roo-code/types").AgentColorName)
|
||||
const toolName = String(block.name ?? "tool")
|
||||
const allowed = await submitWorkerPermissionRequest(
|
||||
cline.taskId,
|
||||
agentName,
|
||||
color,
|
||||
toolName,
|
||||
partialMessage ?? "",
|
||||
)
|
||||
if (!allowed) {
|
||||
pushToolResult(formatResponse.toolDenied())
|
||||
cline.didRejectTool = true
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const { response, text, images } = await cline.ask(
|
||||
type,
|
||||
partialMessage,
|
||||
|
|
|
|||
81
src/core/swarm/LeaderPermissionBridge.ts
Normal file
81
src/core/swarm/LeaderPermissionBridge.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import * as vscode from "vscode"
|
||||
import { randomUUID } from "crypto"
|
||||
|
||||
import type { AgentColorName } from "@roo-code/types"
|
||||
|
||||
/**
|
||||
* A pending tool-approval request from a concurrent worker agent.
|
||||
*/
|
||||
export interface WorkerPermissionRequest {
|
||||
requestId: string
|
||||
workerTaskId: string
|
||||
agentName: string
|
||||
color: AgentColorName
|
||||
/** Canonical tool name (e.g. "write_to_file") */
|
||||
toolName: string
|
||||
/** Human-readable description shown in the approval dialog */
|
||||
description: string
|
||||
}
|
||||
|
||||
type PermissionHandler = (req: WorkerPermissionRequest) => Promise<boolean>
|
||||
|
||||
// Module-level singleton — one ClineProvider registers itself; all in-process
|
||||
// worker tasks can reach it without needing a direct reference.
|
||||
let activeHandler: PermissionHandler | null = null
|
||||
|
||||
/**
|
||||
* Called by ClineProvider to register itself as the approval surface.
|
||||
* Returns a cleanup function that deregisters the handler on dispose.
|
||||
*/
|
||||
export function registerPermissionHandler(handler: PermissionHandler): () => void {
|
||||
activeHandler = handler
|
||||
return () => {
|
||||
if (activeHandler === handler) activeHandler = null
|
||||
}
|
||||
}
|
||||
|
||||
/** True when a ClineProvider has registered a handler. */
|
||||
export function hasPermissionHandler(): boolean {
|
||||
return activeHandler !== null
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by concurrent workers when they need tool-use approval.
|
||||
* Routes the request to the registered leader handler.
|
||||
* Returns false (deny) when no handler is registered — safe fail-closed.
|
||||
*/
|
||||
export async function submitWorkerPermissionRequest(
|
||||
workerTaskId: string,
|
||||
agentName: string,
|
||||
color: AgentColorName,
|
||||
toolName: string,
|
||||
description: string,
|
||||
): Promise<boolean> {
|
||||
if (!activeHandler) return false
|
||||
const req: WorkerPermissionRequest = {
|
||||
requestId: randomUUID(),
|
||||
workerTaskId,
|
||||
agentName,
|
||||
color,
|
||||
toolName,
|
||||
description,
|
||||
}
|
||||
return activeHandler(req)
|
||||
}
|
||||
|
||||
/**
|
||||
* Default VS Code approval handler used by ClineProvider.
|
||||
* Shows a modal dialog with the worker's identity and tool details.
|
||||
*/
|
||||
export async function showWorkerPermissionDialog(req: WorkerPermissionRequest): Promise<boolean> {
|
||||
const label = `${req.agentName} (${req.color})`
|
||||
const detail = req.description.length > 400 ? req.description.slice(0, 400) + "…" : req.description
|
||||
|
||||
const result = await vscode.window.showInformationMessage(
|
||||
`Worker approval request from ${label}`,
|
||||
{ modal: true, detail: `Tool: ${req.toolName}\n\n${detail}` },
|
||||
"Allow",
|
||||
"Deny",
|
||||
)
|
||||
return result === "Allow"
|
||||
}
|
||||
|
|
@ -80,6 +80,7 @@ import { SkillsManager } from "../../services/skills/SkillsManager"
|
|||
import { TeamsManager } from "../../services/teams/TeamsManager"
|
||||
import { SwarmRegistry } from "../swarm/SwarmRegistry"
|
||||
import { MailboxManager } from "../swarm/MailboxManager"
|
||||
import { registerPermissionHandler, showWorkerPermissionDialog } from "../swarm/LeaderPermissionBridge"
|
||||
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { setTtsEnabled, setTtsSpeed } from "../../utils/tts"
|
||||
|
|
@ -158,6 +159,7 @@ export class ClineProvider
|
|||
protected teamsManager?: TeamsManager
|
||||
protected swarmRegistry: SwarmRegistry = new SwarmRegistry()
|
||||
protected mailboxManager: MailboxManager = new MailboxManager()
|
||||
private permissionBridgeCleanup?: () => void
|
||||
private marketplaceManager: MarketplaceManager
|
||||
private mdmService?: MdmService
|
||||
private taskCreationCallback: (task: Task) => void
|
||||
|
|
@ -253,6 +255,9 @@ export class ClineProvider
|
|||
this.log(`Failed to initialize Teams Manager: ${error}`)
|
||||
})
|
||||
|
||||
// Register as the in-process permission approval surface for concurrent workers.
|
||||
this.permissionBridgeCleanup = registerPermissionHandler(showWorkerPermissionDialog)
|
||||
|
||||
this.marketplaceManager = new MarketplaceManager(this.context, this.customModesManager)
|
||||
|
||||
// Forward <most> task events to the provider.
|
||||
|
|
@ -747,6 +752,8 @@ export class ClineProvider
|
|||
this.teamsManager = undefined
|
||||
this.swarmRegistry.dispose()
|
||||
this.mailboxManager.dispose()
|
||||
this.permissionBridgeCleanup?.()
|
||||
this.permissionBridgeCleanup = undefined
|
||||
this.marketplaceManager?.cleanup()
|
||||
this.customModesManager?.dispose()
|
||||
this.taskHistoryStore.dispose()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue