Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-permissions-speak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Show effective permission defaults and clarify external-directory approvals in VS Code.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ test.describe("Permission Dock Dropdown — external directory", () => {
await page.waitForSelector("#storybook-root *", { state: "attached" })
await openDropdown(page)

const text = "Read External Directory /Users/developer/projects/kilo-bench/dashboard/app/routes/*"
const text = "Access External Directory /Users/developer/projects/kilo-bench/dashboard/app/routes/*"
const hint = page.locator('[data-slot="permission-hint"]')
await expect(hint).toHaveText(text)
await expect(hint).toHaveAttribute("title", text)
Expand Down
10 changes: 5 additions & 5 deletions packages/kilo-vscode/tests/unit/permission-description.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const labels: Record<string, string> = {
"ui.permission.toolLabel.webSearch": "Web Search",
"ui.permission.toolLabel.list": "List",
"ui.permission.toolLabel.bash": "Bash",
"ui.permission.toolLabel.externalDirectory": "Read External Directory",
"ui.permission.toolLabel.externalDirectory": "Access External Directory",
"ui.permission.toolLabel.webFetch": "Web Fetch",
"ui.permission.toolLabel.codeSearch": "Code Search",
"ui.permission.toolLabel.todoRead": "Todo Read",
Expand Down Expand Up @@ -79,9 +79,9 @@ describe("describePatterns", () => {
expect(result).toEqual({ kind: "single", text: "Edit file.ts" })
})

test("external_directory uses Read External Directory label", () => {
test("external_directory uses Access External Directory label", () => {
const result = describePatterns("external_directory", ["/home/user/project/*"], t)
expect(result).toEqual({ kind: "single", text: "Read External Directory /home/user/project/*" })
expect(result).toEqual({ kind: "single", text: "Access External Directory /home/user/project/*" })
})

test("glob tool uses Glob Search label", () => {
Expand Down Expand Up @@ -181,7 +181,7 @@ describe("describeRule", () => {

test("preserves existing permission rule labels", () => {
expect(describeRule("read", "*", t)).toBe("Read")
expect(describeRule("external_directory", "*", t)).toBe("Read External Directory")
expect(describeRule("external_directory", "*", t)).toBe("Access External Directory")
expect(describeRule("read", "src/app.ts", t)).toBe("Read src/app.ts")
})
})
Expand Down Expand Up @@ -214,7 +214,7 @@ describe("resolveLabel", () => {
grep: "Grep Search",
list: "List",
bash: "Bash",
external_directory: "Read External Directory",
external_directory: "Access External Directory",
webfetch: "Web Fetch",
websearch: "Web Search",
codesearch: "Code Search",
Expand Down
28 changes: 28 additions & 0 deletions packages/kilo-vscode/tests/unit/permission-editor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import {
addExceptionPatch,
clearGroupedPatch,
clearWildcardPatch,
DEFAULT_RULES,
inheritedWildcard,
mostRestrictive,
permissionExceptions,
removeExceptionPatch,
ruleset,
setExceptionPatch,
setGroupedPatch,
setWildcardPatch,
Expand Down Expand Up @@ -45,6 +47,31 @@ describe("effectiveRuleLevel", () => {
})
})

describe("ruleset", () => {
it("preserves backend defaults when config only customizes bash", () => {
const rules = [...DEFAULT_RULES, ...ruleset({ bash: { "*": "ask", "git status *": "allow" } })]

expect(effectiveRuleLevel(rules, "edit")).toBe("allow")
expect(effectiveRuleLevel(rules, "external_directory")).toBe("ask")
expect(effectiveRuleLevel(rules, "bash")).toBe("ask")
expect(effectiveRuleLevel(rules, "doom_loop")).toBe("ask")
})

it("applies top-level wildcards and direct rules in config order", () => {
const broadLast = [...DEFAULT_RULES, ...ruleset({ edit: "allow", "*": "ask" })]
const directLast = [...DEFAULT_RULES, ...ruleset({ "*": "ask", edit: "allow" })]

expect(effectiveRuleLevel(broadLast, "edit")).toBe("ask")
expect(effectiveRuleLevel(directLast, "edit")).toBe("allow")
})

it("skips config delete sentinels", () => {
expect(ruleset({ edit: null, bash: { "*": null, "git status *": "allow" } })).toEqual([
{ permission: "bash", pattern: "git status *", action: "allow" },
])
})
})

describe("PermissionEditor inherited wildcard state", () => {
it("distinguishes inherited defaults from explicit wildcard rules", () => {
expect(wildcardAction(undefined, "ask")).toBe("ask")
Expand All @@ -68,6 +95,7 @@ describe("PermissionEditor patch generation", () => {
})

it("preserves exceptions when changing wildcard overrides", () => {
expect(setWildcardPatch(undefined, "edit", "ask")).toEqual({ edit: "ask" })
expect(setWildcardPatch({ "*": "deny", "src/**": "allow", "dist/**": null }, "edit", "ask")).toEqual({
edit: { "*": "ask", "src/**": "allow" },
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ export const PermissionDock: Component<{
}}
disabled={props.responding}
>
{language.t("ui.permission.run")}
{language.t("ui.permission.allowOnce")}
</Button>
<Button
variant="ghost"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { TextField } from "@kilocode/kilo-ui/text-field"
import { useConfig } from "../../context/config"
import { useLanguage } from "../../context/language"
import PermissionEditor from "./PermissionEditor"
import { DEFAULT_RULES } from "./permission-utils"
import SettingsRow from "./SettingsRow"

const AutoApproveTab: Component = () => {
Expand Down Expand Up @@ -54,7 +55,10 @@ const AutoApproveTab: Component = () => {

<PermissionEditor
permissions={permissions()}
rules={DEFAULT_RULES}
description={language.t("settings.autoApprove.description")}
inherited
showDefaultLevel
onChange={(patch) => updateConfig({ permission: patch })}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ import {
mostRestrictive,
permissionExceptions,
removeExceptionPatch,
ruleset,
setExceptionPatch,
setGroupedPatch,
setWildcardPatch,
wildcardAction,
type PermissionPatch,
} from "./permission-utils"

Expand Down Expand Up @@ -136,12 +136,13 @@ const PermissionEditor: Component<{
description?: string
component?: string
inherited?: boolean
showDefaultLevel?: boolean
onChange: (patch: PermissionPatch) => void
}> = (props) => {
const perms = createMemo(() => props.permissions ?? {})
const rules = createMemo(() => [...(props.rules ?? []), ...ruleset(perms())])

const levelFor = (tool: string): PermissionLevel =>
wildcardAction(perms()[tool], effectiveRuleLevel(props.rules, tool))
const levelFor = (tool: string): PermissionLevel => effectiveRuleLevel(rules(), tool)

const ruleFor = (tool: string): PermissionRule | undefined => perms()[tool]

Expand Down Expand Up @@ -205,6 +206,7 @@ const PermissionEditor: Component<{
fallback={levelFor(tool.id)}
inherited={props.inherited && inheritedWildcard(ruleFor(tool.id))}
allowInherit={props.inherited}
showDefaultLevel={props.showDefaultLevel}
onWildcardChange={(level) => setWildcard(tool.id, level)}
onWildcardInherit={() => clearWildcard(tool.id)}
onExceptionChange={(pattern, level) => setException(tool.id, pattern, level)}
Expand All @@ -220,7 +222,8 @@ const PermissionEditor: Component<{
id={tool.id}
descriptionKey={tool.descriptionKey}
level={levelFor(tool.id)}
inherited={props.inherited && ruleFor(tool.id) === undefined}
inherited={props.inherited && inheritedWildcard(ruleFor(tool.id))}
showDefaultLevel={props.showDefaultLevel}
onChange={(level) => setSimple(tool.id, level)}
onInherit={() => clearSimple(tool.id)}
/>
Expand All @@ -233,7 +236,8 @@ const PermissionEditor: Component<{
id={group.label}
descriptionKey={group.descriptionKey}
level={mostRestrictive(group.ids.map(levelFor))}
inherited={props.inherited && group.ids.every((id) => ruleFor(id) === undefined)}
inherited={props.inherited && group.ids.every((id) => inheritedWildcard(ruleFor(id)))}
showDefaultLevel={props.showDefaultLevel}
onChange={(level) => setGrouped(group.ids, level)}
onInherit={() => clearGrouped(group.ids)}
/>
Expand All @@ -246,7 +250,8 @@ const PermissionEditor: Component<{
id={tool.id}
descriptionKey={tool.descriptionKey}
level={levelFor(tool.id)}
inherited={props.inherited && ruleFor(tool.id) === undefined}
inherited={props.inherited && inheritedWildcard(ruleFor(tool.id))}
showDefaultLevel={props.showDefaultLevel}
onChange={(level) => setSimple(tool.id, level)}
onInherit={() => clearSimple(tool.id)}
/>
Expand All @@ -261,6 +266,7 @@ const SimpleToolRow: Component<{
descriptionKey: string
level: PermissionLevel
inherited?: boolean
showDefaultLevel?: boolean
onChange: (level: PermissionLevel) => void
onInherit?: () => void
}> = (props) => {
Expand Down Expand Up @@ -293,6 +299,7 @@ const SimpleToolRow: Component<{
<ActionSelect
level={props.level}
inherited={props.inherited}
showDefaultLevel={props.showDefaultLevel}
onChange={props.onChange}
onInherit={props.onInherit}
/>
Expand All @@ -306,6 +313,7 @@ const GranularToolRow: Component<{
fallback: PermissionLevel
inherited?: boolean
allowInherit?: boolean
showDefaultLevel?: boolean
onWildcardChange: (level: PermissionLevel) => void
onWildcardInherit: () => void
onExceptionChange: (pattern: string, level: PermissionLevel) => void
Expand All @@ -325,8 +333,6 @@ const GranularToolRow: Component<{
const excs = createMemo(() => permissionExceptions(props.rule))
const expanded = createMemo(() => override() ?? excs().length <= 5)
const toggle = () => setOverride(!expanded())
const level = createMemo(() => wildcardAction(props.rule, props.fallback))

const submit = () => {
const val = input().trim()
if (val) {
Expand Down Expand Up @@ -375,8 +381,9 @@ const GranularToolRow: Component<{
</div>
</div>
<ActionSelect
level={level()}
level={props.fallback}
inherited={props.inherited}
showDefaultLevel={props.showDefaultLevel}
onChange={props.onWildcardChange}
onInherit={props.allowInherit ? props.onWildcardInherit : undefined}
/>
Expand Down Expand Up @@ -524,17 +531,23 @@ const GranularToolRow: Component<{
const ActionSelect: Component<{
level: PermissionLevel
inherited?: boolean
showDefaultLevel?: boolean
onChange: (level: PermissionLevel) => void
onInherit?: () => void
}> = (props) => {
const language = useLanguage()
const opts = createMemo(() => (props.onInherit ? [INHERIT_OPTION, ...LEVEL_OPTIONS] : LEVEL_OPTIONS))
const label = (option: LevelOption) => {
if (option.value !== "inherit" || !props.showDefaultLevel) return language.t(option.labelKey)
const level = LEVEL_OPTIONS.find((item) => item.value === props.level)!
return `${language.t(option.labelKey)} (${language.t(level.labelKey)})`
}
return (
<Select
options={opts()}
current={props.inherited ? INHERIT_OPTION : LEVEL_OPTIONS.find((option) => option.value === props.level)}
value={(option) => option.value}
label={(option) => language.t(option.labelKey)}
label={label}
onSelect={(option) => {
if (!option) return
if (option.value === "inherit") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ export type PermissionPatch = PermissionConfig

const RESTRICTION_ORDER: Record<PermissionLevel, number> = { allow: 0, ask: 1, deny: 2 }

// Keep wildcard defaults aligned with the CLI's base rules and Kilo Bash policy.
export const DEFAULT_RULES: PermissionRuleItem[] = [
{ permission: "*", pattern: "*", action: "allow" },
{ permission: "external_directory", pattern: "*", action: "ask" },
{ permission: "bash", pattern: "*", action: "ask" },
{ permission: "doom_loop", pattern: "*", action: "ask" },
]

function matchTool(tool: string, pattern: string): boolean {
if (pattern === tool || pattern === "*") return true
if (!pattern.includes("*")) return false
Expand All @@ -20,6 +28,21 @@ export function effectiveRuleLevel(rules: PermissionRuleItem[] | undefined, tool
return "ask"
}

export function ruleset(config: PermissionConfig): PermissionRuleItem[] {
const result: PermissionRuleItem[] = []
for (const [permission, rule] of Object.entries(config)) {
if (rule === null || rule === undefined) continue
if (typeof rule === "string") {
result.push({ permission, pattern: "*", action: rule })
continue
}
for (const [pattern, action] of Object.entries(rule)) {
if (action !== null) result.push({ permission, pattern, action })
}
}
return result
}

export function mostRestrictive(levels: PermissionLevel[]): PermissionLevel {
return levels.reduce<PermissionLevel>(
(best, level) => (RESTRICTION_ORDER[level] > RESTRICTION_ORDER[best] ? level : best),
Expand Down
2 changes: 1 addition & 1 deletion packages/kilo-vscode/webview-ui/src/i18n/ar.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/kilo-vscode/webview-ui/src/i18n/br.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/kilo-vscode/webview-ui/src/i18n/bs.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/kilo-vscode/webview-ui/src/i18n/da.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/kilo-vscode/webview-ui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -643,7 +643,7 @@ export const dict = {
"ui.permission.toolLabel.grepSearch": "Grep-Suche",
"ui.permission.toolLabel.webSearch": "Websuche",
"ui.permission.toolLabel.list": "Auflisten",
"ui.permission.toolLabel.externalDirectory": "Externes Verzeichnis lesen",
"ui.permission.toolLabel.externalDirectory": "Zugriff auf externen Ordner erlauben",
"ui.permission.toolLabel.webFetch": "Web-Abruf",
"ui.permission.toolLabel.task": "Aufgabe",
"ui.permission.toolLabel.skill": "Fähigkeit",
Expand Down
Loading
Loading