mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-29 03:44:06 +08:00
Merge pull request #10787 from Kilo-Org/picayune-yacht
fix(vscode): route worktree permission approvals correctly
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Route Agent Manager permission approvals to the worktree that created the request.
|
||||
@@ -1165,7 +1165,16 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
|
||||
// Subscribe to SSE events for this webview (filtered by tracked sessions)
|
||||
this.unsubscribeEvent = this.connectionService.onEventFiltered(
|
||||
(event) => {
|
||||
(event, directory) => {
|
||||
// Preserve the request origin even when a worktree session is not tracked yet.
|
||||
// Manual replies must target the Instance that owns the pending permission.
|
||||
if (event.type === "permission.asked" && directory) {
|
||||
this.permissionDirectories.set(event.properties.id, directory)
|
||||
}
|
||||
if (event.type === "permission.replied") {
|
||||
this.permissionDirectories.delete(event.properties.requestID)
|
||||
}
|
||||
|
||||
// Remote status events are global and should always pass through
|
||||
if (event.type === "kilo-sessions.remote-status-changed") return true
|
||||
const sessionId = this.connectionService.resolveEventSessionId(event)
|
||||
@@ -2874,13 +2883,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
// let a foreign session through if it was accidentally tracked.
|
||||
if (isEventFromForeignProject(event, this.projectID)) return
|
||||
|
||||
if (event.type === "permission.asked" && directory) {
|
||||
this.permissionDirectories.set(event.properties.id, directory)
|
||||
}
|
||||
if (event.type === "permission.replied") {
|
||||
this.permissionDirectories.delete(event.properties.requestID)
|
||||
}
|
||||
|
||||
if (event.type === "mcp.browser.open.failed") {
|
||||
McpOAuth.openMcpOAuthUrlOnce(event.properties.url)
|
||||
return
|
||||
|
||||
@@ -83,12 +83,12 @@ export function registerToggleAutoApprove(
|
||||
return active
|
||||
}
|
||||
|
||||
const unsubscribe = connectionService.onEvent((event: Event) => {
|
||||
const unsubscribe = connectionService.onEvent((event: Event, directory?: string) => {
|
||||
if (!active) return
|
||||
if (event.type !== "permission.asked") return
|
||||
const client = tryGetClient(connectionService)
|
||||
if (!client) return
|
||||
const dir = resolve(event.properties.sessionID)
|
||||
const dir = directory ?? resolve(event.properties.sessionID)
|
||||
client.permission.reply({ requestID: event.properties.id, directory: dir, reply: "once" }).catch((err) => {
|
||||
console.error("[Kilo New] toggleAutoApprove: failed to auto-reply:", err)
|
||||
})
|
||||
|
||||
@@ -80,13 +80,13 @@ function context() {
|
||||
}
|
||||
|
||||
function connection(client: KiloClient | null) {
|
||||
const listeners: Array<(event: Event) => void> = []
|
||||
const listeners: Array<(event: Event, directory?: string) => void> = []
|
||||
const svc = {
|
||||
getClient: () => {
|
||||
if (!client) throw new Error("not connected")
|
||||
return client
|
||||
},
|
||||
onEvent: (listener: (event: Event) => void) => {
|
||||
onEvent: (listener: (event: Event, directory?: string) => void) => {
|
||||
listeners.push(listener)
|
||||
return () => {
|
||||
const index = listeners.indexOf(listener)
|
||||
@@ -97,8 +97,8 @@ function connection(client: KiloClient | null) {
|
||||
|
||||
return {
|
||||
svc,
|
||||
emit(event: Event) {
|
||||
for (const listener of listeners) listener(event)
|
||||
emit(event: Event, directory?: string) {
|
||||
for (const listener of listeners) listener(event, directory)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -152,6 +152,26 @@ describe("registerToggleAutoApprove", () => {
|
||||
expect(env.messages).toContain("Auto-approve enabled")
|
||||
})
|
||||
|
||||
it("uses the SSE directory for worktree permissions before session mappings are available", () => {
|
||||
config(true)
|
||||
const replies: unknown[] = []
|
||||
const conn = connection(client({ reply: async (args) => replies.push(args) }))
|
||||
registerToggleAutoApprove(
|
||||
context(),
|
||||
conn.svc,
|
||||
() => "/workspace",
|
||||
() => ["/workspace"],
|
||||
)
|
||||
|
||||
conn.emit(asked("perm_worktree", "ses_worktree"), "/workspace/.kilo/worktrees/feature")
|
||||
conn.emit(asked("perm_child", "ses_child"), "/workspace/.kilo/worktrees/feature")
|
||||
|
||||
expect(replies).toEqual([
|
||||
{ requestID: "perm_worktree", directory: "/workspace/.kilo/worktrees/feature", reply: "once" },
|
||||
{ requestID: "perm_child", directory: "/workspace/.kilo/worktrees/feature", reply: "once" },
|
||||
])
|
||||
})
|
||||
|
||||
it("cancels pending permission drains when disabled during an enable generation", async () => {
|
||||
config(false)
|
||||
const gate = defer<{ data: Permission[] }>()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, it, expect } from "bun:test"
|
||||
import {
|
||||
fetchAndSendPendingPermissions,
|
||||
handlePermissionResponse,
|
||||
recoverablePermissions,
|
||||
recoveryDirs,
|
||||
type RecoverablePermission,
|
||||
@@ -20,7 +21,12 @@ function pending(id: string, sessionID: string, permission = "bash"): Recoverabl
|
||||
}
|
||||
}
|
||||
|
||||
function permissionClient(permsPerDir: Record<string, ReturnType<typeof pending>[]>, queries: string[]) {
|
||||
function permissionClient(
|
||||
permsPerDir: Record<string, ReturnType<typeof pending>[]>,
|
||||
queries: string[],
|
||||
saves: unknown[] = [],
|
||||
replies: unknown[] = [],
|
||||
) {
|
||||
return {
|
||||
permission: {
|
||||
list: async (args?: { directory?: string }) => {
|
||||
@@ -28,8 +34,14 @@ function permissionClient(permsPerDir: Record<string, ReturnType<typeof pending>
|
||||
queries.push(dir)
|
||||
return { data: permsPerDir[dir] ?? [] }
|
||||
},
|
||||
saveAlwaysRules: async () => ({ data: true }),
|
||||
reply: async () => ({ data: true }),
|
||||
saveAlwaysRules: async (args: unknown) => {
|
||||
saves.push(args)
|
||||
return { data: true }
|
||||
},
|
||||
reply: async (args: unknown) => {
|
||||
replies.push(args)
|
||||
return { data: true }
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -37,8 +49,10 @@ function permissionClient(permsPerDir: Record<string, ReturnType<typeof pending>
|
||||
function client(
|
||||
permsPerDir: Record<string, ReturnType<typeof pending>[]>,
|
||||
queries: string[],
|
||||
saves: unknown[] = [],
|
||||
replies: unknown[] = [],
|
||||
): PermissionContext["client"] {
|
||||
return permissionClient(permsPerDir, queries) as unknown as PermissionContext["client"]
|
||||
return permissionClient(permsPerDir, queries, saves, replies) as unknown as PermissionContext["client"]
|
||||
}
|
||||
|
||||
function ctx(opts: {
|
||||
@@ -49,8 +63,10 @@ function ctx(opts: {
|
||||
}) {
|
||||
const messages: unknown[] = []
|
||||
const queries: string[] = []
|
||||
const saves: unknown[] = []
|
||||
const replies: unknown[] = []
|
||||
const perms = opts.permsPerDir ?? {}
|
||||
const sdk = client(perms, queries)
|
||||
const sdk = client(perms, queries, saves, replies)
|
||||
|
||||
const permDirs = new Map<string, string>()
|
||||
const fake: PermissionContext = {
|
||||
@@ -72,7 +88,7 @@ function ctx(opts: {
|
||||
},
|
||||
}
|
||||
|
||||
return { fake, messages, queries, permDirs }
|
||||
return { fake, messages, queries, saves, replies, permDirs }
|
||||
}
|
||||
|
||||
describe("recoveryDirs", () => {
|
||||
@@ -94,6 +110,34 @@ describe("recoveryDirs", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("handlePermissionResponse", () => {
|
||||
it("uses the recorded SSE directory instead of a stale session fallback", async () => {
|
||||
const { fake, replies, permDirs } = ctx({ tracked: ["s1"] })
|
||||
permDirs.set("p1", "/workspace/.kilo/worktrees/feature")
|
||||
|
||||
await handlePermissionResponse(fake, "p1", "s1", "once", [], [])
|
||||
|
||||
expect(replies).toEqual([{ requestID: "p1", reply: "once", directory: "/workspace/.kilo/worktrees/feature" }])
|
||||
})
|
||||
|
||||
it("saves selected rules and replies in the recorded SSE directory", async () => {
|
||||
const { fake, saves, replies, permDirs } = ctx({ tracked: ["s1"] })
|
||||
permDirs.set("p1", "/workspace/.kilo/worktrees/feature")
|
||||
|
||||
await handlePermissionResponse(fake, "p1", "s1", "reject", ["bun *"], ["rm *"])
|
||||
|
||||
expect(saves).toEqual([
|
||||
{
|
||||
requestID: "p1",
|
||||
directory: "/workspace/.kilo/worktrees/feature",
|
||||
approvedAlways: ["bun *"],
|
||||
deniedAlways: ["rm *"],
|
||||
},
|
||||
])
|
||||
expect(replies).toEqual([{ requestID: "p1", reply: "reject", directory: "/workspace/.kilo/worktrees/feature" }])
|
||||
})
|
||||
})
|
||||
|
||||
describe("recoverablePermissions", () => {
|
||||
it("filters out untracked permissions", () => {
|
||||
const seen = new Set<string>()
|
||||
|
||||
Reference in New Issue
Block a user