mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
Merge pull request #10121 from shssoichiro/apply-auto-to-subagents
fix(cli): apply `--auto` flag to subagents from Task tool
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Auto-approve Task subagent tool permissions when running `kilo run --auto`.
|
||||
@@ -28,6 +28,7 @@ import { TodoWriteTool } from "../../tool/todo"
|
||||
import { Locale } from "@/util/locale"
|
||||
import { importCloudSession, validateCloudFork } from "@/kilocode/cloud-session" // kilocode_change
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { KiloRunAuto } from "@/kilocode/cli/run-auto" // kilocode_change
|
||||
|
||||
type ToolProps<T> = {
|
||||
input: Tool.InferParameters<T>
|
||||
@@ -472,6 +473,9 @@ export const RunCommand = cmd({
|
||||
|
||||
if (event.type === "message.part.updated") {
|
||||
const part = event.properties.part
|
||||
// kilocode_change start - track Task child sessions for --auto permission replies
|
||||
if (args.auto) KiloRunAuto.track(auto, part)
|
||||
// kilocode_change end
|
||||
if (part.sessionID !== sessionID) continue
|
||||
|
||||
if (part.type === "tool" && (part.state.status === "completed" || part.state.status === "error")) {
|
||||
@@ -566,25 +570,27 @@ export const RunCommand = cmd({
|
||||
|
||||
if (event.type === "permission.asked") {
|
||||
const permission = event.properties
|
||||
if (permission.sessionID !== sessionID) continue
|
||||
|
||||
// kilocode_change start - In auto mode, approve root and tracked Task child permissions only
|
||||
if (args.auto) {
|
||||
// kilocode_change - In auto mode, automatically approve all permissions without prompting
|
||||
if (!KiloRunAuto.allowed(auto, permission.sessionID)) continue
|
||||
await sdk.permission.reply({
|
||||
requestID: permission.id,
|
||||
reply: "once",
|
||||
})
|
||||
} else {
|
||||
UI.println(
|
||||
UI.Style.TEXT_WARNING_BOLD + "!",
|
||||
UI.Style.TEXT_NORMAL +
|
||||
`permission requested: ${permission.permission} (${permission.patterns.join(", ")}); auto-rejecting`,
|
||||
)
|
||||
await sdk.permission.reply({
|
||||
requestID: permission.id,
|
||||
reply: "reject",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if (permission.sessionID !== sessionID) continue
|
||||
UI.println(
|
||||
UI.Style.TEXT_WARNING_BOLD + "!",
|
||||
UI.Style.TEXT_NORMAL +
|
||||
`permission requested: ${permission.permission} (${permission.patterns.join(", ")}); auto-rejecting`,
|
||||
)
|
||||
await sdk.permission.reply({
|
||||
requestID: permission.id,
|
||||
reply: "reject",
|
||||
})
|
||||
// kilocode_change end
|
||||
}
|
||||
// kilocode_change start - network retry handling
|
||||
if (event.type === "session.network.asked") {
|
||||
@@ -677,6 +683,7 @@ export const RunCommand = cmd({
|
||||
UI.error("Session not found")
|
||||
process.exit(1)
|
||||
}
|
||||
const auto = KiloRunAuto.create(sessionID) // kilocode_change
|
||||
await share(sdk, sessionID)
|
||||
|
||||
loop().catch((e) => {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// kilocode_change - new file
|
||||
export namespace KiloRunAuto {
|
||||
export interface State {
|
||||
root: string
|
||||
sessions: Set<string>
|
||||
}
|
||||
|
||||
export interface Part {
|
||||
type?: string
|
||||
tool?: string
|
||||
sessionID?: string
|
||||
state?: unknown
|
||||
}
|
||||
|
||||
export function create(root: string): State {
|
||||
return {
|
||||
root,
|
||||
sessions: new Set([root]),
|
||||
}
|
||||
}
|
||||
|
||||
export function allowed(state: State, sessionID: string) {
|
||||
return state.sessions.has(sessionID)
|
||||
}
|
||||
|
||||
export function track(state: State, part: Part) {
|
||||
if (part.type !== "tool") return
|
||||
if (part.tool !== "task") return
|
||||
if (part.sessionID !== state.root) return
|
||||
const id = child(meta(part.state))
|
||||
if (!id) return
|
||||
state.sessions.add(id)
|
||||
}
|
||||
|
||||
function meta(state: unknown) {
|
||||
if (!state || typeof state !== "object") return
|
||||
return (state as Record<string, unknown>).metadata
|
||||
}
|
||||
|
||||
function child(meta: unknown) {
|
||||
if (!meta || typeof meta !== "object") return
|
||||
const id = (meta as Record<string, unknown>).sessionId
|
||||
if (typeof id !== "string") return
|
||||
if (!id) return
|
||||
return id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// kilocode_change - new file
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { KiloRunAuto } from "../../src/kilocode/cli/run-auto"
|
||||
|
||||
describe("KiloRunAuto", () => {
|
||||
test("tracks task child sessions without allowing unrelated sessions", () => {
|
||||
const state = KiloRunAuto.create("ses_root")
|
||||
|
||||
expect(KiloRunAuto.allowed(state, "ses_root")).toBe(true)
|
||||
expect(KiloRunAuto.allowed(state, "ses_child")).toBe(false)
|
||||
|
||||
KiloRunAuto.track(state, {
|
||||
type: "tool",
|
||||
tool: "task",
|
||||
sessionID: "ses_root",
|
||||
state: {
|
||||
metadata: {
|
||||
sessionId: "ses_child",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(KiloRunAuto.allowed(state, "ses_child")).toBe(true)
|
||||
expect(KiloRunAuto.allowed(state, "ses_other")).toBe(false)
|
||||
})
|
||||
|
||||
test("ignores malformed or non-root task metadata", () => {
|
||||
const state = KiloRunAuto.create("ses_root")
|
||||
|
||||
KiloRunAuto.track(state, {
|
||||
type: "tool",
|
||||
tool: "task",
|
||||
sessionID: "ses_root",
|
||||
state: {
|
||||
metadata: {
|
||||
sessionId: "",
|
||||
},
|
||||
},
|
||||
})
|
||||
KiloRunAuto.track(state, {
|
||||
type: "tool",
|
||||
tool: "task",
|
||||
sessionID: "ses_other",
|
||||
state: {
|
||||
metadata: {
|
||||
sessionId: "ses_wrong",
|
||||
},
|
||||
},
|
||||
})
|
||||
KiloRunAuto.track(state, {
|
||||
type: "text",
|
||||
sessionID: "ses_root",
|
||||
state: {},
|
||||
})
|
||||
|
||||
expect(KiloRunAuto.allowed(state, "ses_wrong")).toBe(false)
|
||||
expect(KiloRunAuto.allowed(state, "")).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,177 @@
|
||||
// kilocode_change - new file
|
||||
import { afterEach, describe, expect, mock, test } from "bun:test"
|
||||
|
||||
type Event = {
|
||||
type: string
|
||||
properties: Record<string, unknown>
|
||||
}
|
||||
|
||||
function feed<T>() {
|
||||
const list: T[] = []
|
||||
const wait: Array<() => void> = []
|
||||
const state = { done: false }
|
||||
|
||||
return {
|
||||
push(item: T) {
|
||||
list.push(item)
|
||||
while (wait.length) wait.shift()?.()
|
||||
},
|
||||
end() {
|
||||
state.done = true
|
||||
while (wait.length) wait.shift()?.()
|
||||
},
|
||||
async *stream() {
|
||||
while (!state.done || list.length) {
|
||||
if (list.length) {
|
||||
yield list.shift() as T
|
||||
continue
|
||||
}
|
||||
await new Promise<void>((resolve) => wait.push(resolve))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function task(child: string): Event {
|
||||
return {
|
||||
type: "message.part.updated",
|
||||
properties: {
|
||||
part: {
|
||||
id: "prt_task",
|
||||
type: "tool",
|
||||
tool: "task",
|
||||
sessionID: "ses_root",
|
||||
state: {
|
||||
status: "running",
|
||||
input: {
|
||||
description: "inspect bug",
|
||||
prompt: "check child permissions",
|
||||
subagent_type: "general",
|
||||
},
|
||||
metadata: {
|
||||
sessionId: child,
|
||||
},
|
||||
time: { start: 0 },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function permission(id: string, sessionID: string): Event {
|
||||
return {
|
||||
type: "permission.asked",
|
||||
properties: {
|
||||
id,
|
||||
sessionID,
|
||||
permission: "bash",
|
||||
patterns: ["npm test"],
|
||||
metadata: { command: "npm test" },
|
||||
always: ["npm *"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function idle(): Event {
|
||||
return {
|
||||
type: "session.status",
|
||||
properties: {
|
||||
sessionID: "ses_root",
|
||||
status: { type: "idle" },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function args() {
|
||||
return {
|
||||
_: [],
|
||||
$0: "kilo",
|
||||
message: ["hi"],
|
||||
command: undefined,
|
||||
continue: false,
|
||||
session: "ses_root",
|
||||
fork: false,
|
||||
"cloud-fork": false,
|
||||
cloudFork: false,
|
||||
share: false,
|
||||
model: undefined,
|
||||
agent: undefined,
|
||||
format: "json",
|
||||
file: undefined,
|
||||
title: undefined,
|
||||
attach: "http://127.0.0.1:4096",
|
||||
password: undefined,
|
||||
dir: undefined,
|
||||
port: undefined,
|
||||
variant: undefined,
|
||||
thinking: false,
|
||||
auto: true,
|
||||
"dangerously-skip-permissions": false,
|
||||
dangerouslySkipPermissions: false,
|
||||
"--": [],
|
||||
}
|
||||
}
|
||||
|
||||
const tty = Object.getOwnPropertyDescriptor(process.stdin, "isTTY")
|
||||
|
||||
afterEach(() => {
|
||||
if (tty) {
|
||||
Object.defineProperty(process.stdin, "isTTY", tty)
|
||||
return
|
||||
}
|
||||
delete (process.stdin as { isTTY?: boolean }).isTTY
|
||||
})
|
||||
|
||||
async function run(sdk: Record<string, unknown>) {
|
||||
mock.module("@kilocode/sdk/v2", () => ({
|
||||
createKiloClient: () => sdk,
|
||||
}))
|
||||
|
||||
Object.defineProperty(process.stdin, "isTTY", {
|
||||
configurable: true,
|
||||
value: true,
|
||||
})
|
||||
|
||||
const key = JSON.stringify({ time: Date.now(), rand: Math.random() })
|
||||
const { RunCommand } = await import(`../../src/cli/cmd/run?${key}`)
|
||||
return RunCommand.handler(args() as never)
|
||||
}
|
||||
|
||||
describe("cli run auto permissions", () => {
|
||||
test("auto approves tracked subagent permissions and ignores unrelated sessions", async () => {
|
||||
const q = feed<Event>()
|
||||
const calls: Array<{ requestID: string; reply: string }> = []
|
||||
const done = Promise.withResolvers<void>()
|
||||
|
||||
const sdk = {
|
||||
config: {
|
||||
get: async () => ({ data: { share: "manual" } }),
|
||||
},
|
||||
event: {
|
||||
subscribe: async () => ({ stream: q.stream() }),
|
||||
},
|
||||
permission: {
|
||||
reply: async (input: { requestID: string; reply: string }) => {
|
||||
calls.push(input)
|
||||
if (input.requestID === "perm_child") done.resolve()
|
||||
return { data: true }
|
||||
},
|
||||
},
|
||||
session: {
|
||||
prompt: async () => {
|
||||
q.push(task("ses_child"))
|
||||
q.push(permission("perm_other", "ses_other"))
|
||||
q.push(permission("perm_child", "ses_child"))
|
||||
q.push(idle())
|
||||
await Promise.race([done.promise, new Promise((resolve) => setTimeout(resolve, 25))])
|
||||
q.end()
|
||||
return { data: undefined }
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
await run(sdk)
|
||||
|
||||
expect(calls).toEqual([{ requestID: "perm_child", reply: "once" }])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user