mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
Merge pull request #11475 from Kilo-Org/second-smile
fix(cli): preserve custom subagent permissions
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Preserve custom subagent tool permissions when tasks inherit restrictions from their parent agent.
|
||||
@@ -42,12 +42,18 @@ export namespace KiloTask {
|
||||
}
|
||||
|
||||
/**
|
||||
* Build inherited permission rules from the calling agent.
|
||||
* Build inherited permission ceilings from the calling agent.
|
||||
* Merges the static agent definition with the session's accumulated permissions
|
||||
* so restrictions survive multi-hop chains (plan → general → explore).
|
||||
* so denials survive multi-hop chains (plan → general → explore) without
|
||||
* overriding the selected subagent's own allowlist with parent ask/allow rules.
|
||||
*
|
||||
* OpenCode removed parent-agent inheritance entirely in anomalyco/opencode#31696.
|
||||
* Kilo intentionally differs: parent denials remain hard ceilings for Plan Mode
|
||||
* and MCP restrictions, while parent ask/allow rules must not replace the
|
||||
* selected subagent's policy. Preserve this distinction during upstream merges.
|
||||
*
|
||||
* The caller must resolve `caller` (Agent.Info) and `session` (Session.Info)
|
||||
* before calling — this function is pure/synchronous.
|
||||
* before calling. This function is pure/synchronous.
|
||||
*/
|
||||
export function inherited(input: {
|
||||
caller: Agent.Info
|
||||
@@ -58,7 +64,8 @@ export namespace KiloTask {
|
||||
const prefixes = Object.keys(input.mcp ?? {}).map((k) => k.replace(/[^a-zA-Z0-9_-]/g, "_") + "_")
|
||||
const isMcp = (p: string) => prefixes.some((prefix) => p.startsWith(prefix))
|
||||
return rules.filter(
|
||||
(r: Permission.Rule) => r.permission === "edit" || r.permission === "bash" || isMcp(r.permission),
|
||||
(r: Permission.Rule) =>
|
||||
r.action === "deny" && (r.permission === "edit" || r.permission === "bash" || isMcp(r.permission)),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import type { SessionPrompt } from "../../src/session/prompt"
|
||||
import { MessageID, PartID } from "../../src/session/schema"
|
||||
import { ModelID, ProviderID } from "../../src/provider/schema"
|
||||
import { Provider } from "../../src/provider/provider"
|
||||
import { Permission } from "../../src/permission"
|
||||
import { TaskTool, type TaskPromptOps } from "../../src/tool/task"
|
||||
import { KiloSessionPrompt } from "../../src/kilocode/session/prompt"
|
||||
import { Truncate } from "../../src/tool/truncate"
|
||||
@@ -232,6 +233,79 @@ describe("Kilo task nesting", () => {
|
||||
])
|
||||
})
|
||||
|
||||
it.live("preserves a custom subagent bash policy while inheriting parent denials", () =>
|
||||
provideTmpdirInstance(
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const sessions = yield* Session.Service
|
||||
const agents = yield* Agent.Service
|
||||
const { chat, assistant } = yield* seed()
|
||||
const tool = yield* TaskTool
|
||||
const def = yield* tool.init()
|
||||
|
||||
const result = yield* def.execute(
|
||||
{
|
||||
description: "validate ansible",
|
||||
prompt: "run ansible-lint --version",
|
||||
subagent_type: "validator",
|
||||
},
|
||||
{
|
||||
sessionID: chat.id,
|
||||
messageID: assistant.id,
|
||||
agent: "build",
|
||||
abort: new AbortController().signal,
|
||||
extra: { promptOps: stubOps() },
|
||||
messages: [],
|
||||
metadata: () => Effect.void,
|
||||
ask: () => Effect.void,
|
||||
},
|
||||
)
|
||||
|
||||
const child = yield* sessions.get(result.metadata.sessionId)
|
||||
const validator = yield* agents.get("validator")
|
||||
expect(validator).toBeDefined()
|
||||
if (!validator) return
|
||||
|
||||
expect(Permission.evaluate("bash", "ansible-lint --version", validator.permission).action).toBe("allow")
|
||||
expect(Permission.evaluate("bash", "rm -rf build", validator.permission).action).toBe("deny")
|
||||
|
||||
const effective = Permission.merge(
|
||||
validator.permission,
|
||||
KiloSessionPrompt.guardPermissions({ agent: validator, session: child }),
|
||||
)
|
||||
expect(child.permission).not.toContainEqual({ permission: "bash", pattern: "*", action: "ask" })
|
||||
expect(child.permission).toContainEqual({ permission: "bash", pattern: "rm -rf *", action: "deny" })
|
||||
expect({
|
||||
allowed: Permission.evaluate("bash", "ansible-lint --version", effective).action,
|
||||
denied: Permission.evaluate("bash", "rm -rf build", effective).action,
|
||||
}).toEqual({ allowed: "allow", denied: "deny" })
|
||||
}),
|
||||
{
|
||||
config: {
|
||||
permission: {
|
||||
bash: {
|
||||
"*": "ask",
|
||||
"git -c *": "allow",
|
||||
"echo *": "allow",
|
||||
"rm -rf *": "deny",
|
||||
},
|
||||
},
|
||||
agent: {
|
||||
validator: {
|
||||
mode: "subagent",
|
||||
permission: {
|
||||
bash: {
|
||||
"*": "deny",
|
||||
"*ansible-lint*": "allow",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
it.live("refreshes inherited restrictions when resuming a task child", () =>
|
||||
provideTmpdirInstance(() =>
|
||||
Effect.gen(function* () {
|
||||
|
||||
Reference in New Issue
Block a user