mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
Remove allow always button (#7159)
* feat: save permissions to config * fix: preserve wildcard default in jsonc scalar promotion and skip dispose for permission saves * fix: update CLI permission hint to reflect permanent persistence * fix: avoid mutating input in mergeConfig permission normalization * chore: update kilo-vscode visual regression baselines * fix(vscode): remove Allow Always button from PermissionDock * fix(kilo-vscode): removed unreachable onDecide option * chore: update kilo-vscode visual regression baselines * fix: scope patchJsonc scalar promotion to permission keys only * fix: toConfig always uses object format to preserve existing granular rules * fix: preserve metadata.rules order in saveAlwaysRules for correct precedence * feat:show non-bash tool permissions on dropdown * fix: include always patterns in saveAlwaysRules validation loop * test(kilo-vscode): remove write mixed-decisions screenshot test for single-rule dropdown * test: updated visual tests * fix: formatting * chore: update kilo-vscode visual regression baselines * fix: use scalar format in toConfig for PermissionAction-only permissions * fix: serialize scalar-only permissions as scalars for all patterns * fix: skip non-wildcard patterns for scalar-only permissions in toConfig * fix: promote doom_loop to PermissionRule for per-tool persistence * fix: keep doom_loop as PermissionAction and skip non-wildcard scalar-only patterns in toConfig --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
github-actions[bot]
parent
ac0a3ae997
commit
0adb514da4
@@ -1868,6 +1868,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
sessionID: perm.sessionID,
|
||||
toolName: perm.permission,
|
||||
patterns: perm.patterns,
|
||||
always: perm.always,
|
||||
args: perm.metadata,
|
||||
message: `Permission required: ${perm.permission}`,
|
||||
tool: perm.tool,
|
||||
|
||||
@@ -167,6 +167,7 @@ export type WebviewMessage =
|
||||
sessionID: string
|
||||
toolName: string
|
||||
patterns: string[]
|
||||
always: string[]
|
||||
args: Record<string, unknown>
|
||||
message: string
|
||||
tool?: { messageID: string; callID: string }
|
||||
@@ -240,6 +241,7 @@ export function mapSSEEventToWebviewMessage(event: Event, sessionID: string | un
|
||||
sessionID: event.properties.sessionID,
|
||||
toolName: event.properties.permission,
|
||||
patterns: event.properties.patterns ?? [],
|
||||
always: event.properties.always ?? [],
|
||||
args: event.properties.metadata,
|
||||
message: `Permission required: ${event.properties.permission}`,
|
||||
tool: event.properties.tool,
|
||||
|
||||
@@ -160,22 +160,6 @@ test.describe("Permission Dock Dropdown — write", () => {
|
||||
const root = page.locator("#storybook-root")
|
||||
await expect(root).toHaveScreenshot(["permission-dock-dropdown", "write-expanded-pending.png"])
|
||||
})
|
||||
|
||||
test("rules expanded — mixed decisions", async ({ page }) => {
|
||||
await page.goto(storyUrl(STORY_ID), { waitUntil: "load" })
|
||||
await disableAnimations(page)
|
||||
await page.waitForSelector("#storybook-root *", { state: "attached" })
|
||||
await openDropdown(page)
|
||||
|
||||
// Approve first rule, deny second (dispatchEvent bypasses tooltip overlays)
|
||||
const approveButtons = page.locator('[data-slot="permission-rule-toggle"][data-variant="approve"]')
|
||||
const denyButtons = page.locator('[data-slot="permission-rule-toggle"][data-variant="deny"]')
|
||||
await approveButtons.first().dispatchEvent("click")
|
||||
await denyButtons.nth(1).dispatchEvent("click")
|
||||
|
||||
const root = page.locator("#storybook-root")
|
||||
await expect(root).toHaveScreenshot(["permission-dock-dropdown", "write-rules-mixed.png"])
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:321afc41e5908e73656b1a8d5e3c32f23f54d10dee12655506b4b58348230305
|
||||
size 13718
|
||||
oid sha256:3ef8ec5886054bf0e7ae288c66ce5175204e74728c3e37f3669c551be98755b7
|
||||
size 12476
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:666d73c69b19e8c79815129b9d17747dbf755c316f3e6da0cba6977966b9cccc
|
||||
size 13906
|
||||
oid sha256:54ff3bd3916245aa1dc65384db4a0b5908b4d78cc3700157112f1be63ce054f9
|
||||
size 12663
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b0608142ccb5bab0468ae44060bed17c5ba98b8ee2d5a477627644d1c91e760a
|
||||
size 13952
|
||||
oid sha256:9edcfe9f76314110e9c1795ee0008ca35187ce28a19c86cfcf1f8bf499fe151c
|
||||
size 12714
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7dd67617fdfc9ea6c4b0f54895d930d76c5c712b158e4be93967a3a3239a53b2
|
||||
size 14143
|
||||
oid sha256:4e314513c9b4e964ea3b9a7cdbfc561e57eab2fa867aadfd370d1c0ad68c11b8
|
||||
size 12903
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:43b16bc81164f310ae40f7ebe6459281f374be323dfc07e3bef6b2d1608b6637
|
||||
size 20835
|
||||
oid sha256:a3b9925b5424dd81e0fd60dc3cc4307730abdc010cccd73e7243292880dd6b09
|
||||
size 16833
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ee48a78cd2fe43c34d0014262ac794e48e5405d2116140d08219fc8e1621be0c
|
||||
size 18516
|
||||
oid sha256:ef1d55223be06fd9d16566bc80a525c84c933a337ee4ccf2c41809e4984571b6
|
||||
size 17276
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f69b6545bd98003edf8c7208eb655066a24847e055cf0a5492c99ff67bbbab38
|
||||
size 14621
|
||||
oid sha256:5fb585654180101c29bd4e7efbc7129bf29e8681bc764cdae0d8233fe302a29e
|
||||
size 12711
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5a7db3c77ca7dee3a74fe3fd08b45e8d32a00193d1fb13ec7b4e2b1671dac7a3
|
||||
size 14831
|
||||
oid sha256:c0702dda15977143eeac1557040a583946f2fed4b1610c97a772cff4ae3f2482
|
||||
size 12842
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c4aa38c3ade48132b3f0083438ef71c0e34f180db3bbd4d70455d6bb34a1225c
|
||||
size 19202
|
||||
oid sha256:9784fcf6612167075eb796887c9ac6ad8ab1596ef03dd0ace2b159f82dcb3675
|
||||
size 17978
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e46b79adada26121c948ce33b78ba78c8b0b13dc153708b60a56f1cfc6034503
|
||||
size 20095
|
||||
oid sha256:606cc8d90900a26682852c9eb14d24305f31672ecd40265954b7b85cca7d3c48
|
||||
size 18850
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:699b49cbb768c3a9cea64878a01b5507a1f304a14f12363f2329ad07c8cc35cb
|
||||
size 15082
|
||||
oid sha256:ab71bff42d0bd60c84f75cc04d91fdae8516667913152a0ae4e792268368c9d1
|
||||
size 13844
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:60e6d4b1d0e860195c26752e50a97e6d22d7911d029e7d08d24f46b327b21a01
|
||||
size 14075
|
||||
oid sha256:8d37fd5942ec471c7f534b7b65ac568cdde7cd1b7f3542c50a72500ac3823cae
|
||||
size 12827
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4ff80ac6cd76dbb278e6483c2e5c352095fc893ae5f5d5cb2eb70ded640bfc0e
|
||||
size 16913
|
||||
oid sha256:04377b0ddbaa0e2f4f126adff431db902fbff65ce653f2bc59434d150a833578
|
||||
size 15638
|
||||
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6531e4d0e975a29d538bc7b609a987071afc0997b396fd06e52cbbf2b7ef2e26
|
||||
size 17467
|
||||
@@ -284,6 +284,7 @@ describe("mapSSEEventToWebviewMessage", () => {
|
||||
expect(msg.permission.args).toEqual({ path: "/foo" })
|
||||
expect(msg.permission.message).toBe("Permission required: read_file")
|
||||
expect(msg.permission.patterns).toEqual(["**/*.ts"])
|
||||
expect(msg.permission.always).toEqual([])
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:de6be977bd3805fdbbbb1fdaf3498201c1097adea26ebe460c56b681d1828e39
|
||||
size 9457
|
||||
oid sha256:c5ada07e94ff10490732ba4b27d8e204102816aa00e472324a31ee9f8c7e8484
|
||||
size 9470
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3164d510232a0457cf935a3032f736825486f133e89994e544c678ccc7e6849a
|
||||
size 30652
|
||||
oid sha256:0bbec9564c9fa46fce155ada8e176641e540c912461102b7b424d317b3283c38
|
||||
size 31033
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dc40d7bbc5f3eaee3b48b4f90046a10e54db3288e71214eec190e4b0fa61cc16
|
||||
size 22777
|
||||
oid sha256:48ebccc3ab71403efa74803f140b17d97a000989b64dd42c65d8f9774d059cb8
|
||||
size 23139
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3da4a2d29601e45eb4b5fe007a62a203c2b5151b2d74437d22630d2ddc8582a6
|
||||
size 28723
|
||||
oid sha256:371621a81d9c42d1dc327492fb2e3d2516857dfa189adf3c924ac6951d33e910
|
||||
size 29073
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bc5f6d5b31970c33aaf062756bbdd73637db17f1bc74c1558e9a81c33c5d602d
|
||||
size 4582
|
||||
oid sha256:742b7939e4b5a803413aad42418fe60f8d138bf6153087baae8cc22fba4202ae
|
||||
size 4755
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cd828877cfcbb7d3f3af475c2ca5f92b2fd627ae41f1a03725ab91a948b40833
|
||||
size 4704
|
||||
oid sha256:5ab73f8c0585e9f889c51073428aa0e7082d7aecdc3d5b0c459720c1b860c436
|
||||
size 4890
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cf635f68a3a3ea84caa026f3be0481f21a2a7424acd8d2fdc138f8018f843af8
|
||||
oid sha256:0e29bb3e5a3d408f6a8edabbf12a59a53c3e39e7fd3efa7cadc0e3c7c380e5cc
|
||||
size 18908
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cb8dd7f0ecbbeed00e1321c050964b54f832a571c8acbc37269de205bd64a483
|
||||
size 13788
|
||||
oid sha256:c6226c4c30415c1e8d1103003464f3a7c86df362bb7b49d2d37400acfd4c1372
|
||||
size 12528
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:484ec50824ececf29d8b1464fb94ef48637d4f97b9a3c68dcf3ae5d8f157dcf6
|
||||
size 15502
|
||||
oid sha256:fb035f3cea015f0a5e693ac3bef31848c5c596642948ddd3f524699cd3637b23
|
||||
size 14245
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:22c9bc250a347451c3a194f3ec701bfaf9db67f3104d12bde92adffc08bcd30b
|
||||
size 6263
|
||||
oid sha256:61c9ddc7067281976b9183e0696952d1ed2f3453c67f54b3c8766ddf1f9b3ba5
|
||||
size 6402
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8f7fd08433524c113a2d7b3fed8bef5c023d9a451621f658b836c7967ca954aa
|
||||
size 8310
|
||||
oid sha256:52f85f9f63eccd6217f37616c525df311e4d0a558547ae127366a7388e859dee
|
||||
size 8395
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ab9edfc18d0d7094f7c5410d7b08e0012c2128f1638b7090aad9301f127473ed
|
||||
size 17425
|
||||
oid sha256:1cf3791372e3e69fd759f2596e72211c982e8694990103c178f1aed0c59229f1
|
||||
size 16139
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:05783f1a5b4c8631dee2582b243a724eb4fb811cde894f86ce3575e2da1a997e
|
||||
size 17416
|
||||
oid sha256:1f3dc016ba452b8eaf69c5f4532d0edc4915c43c345a62029fcbaad46f8ac902
|
||||
size 16140
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:24364efcbb54ed842a9af0c630e6b2a4f5df37d8cabb651e18dcbebda37a26c5
|
||||
size 17221
|
||||
oid sha256:469f10e66c7505ff9034663696fdefc400f21803612fb5df8a58f4f991155ac3
|
||||
size 15945
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e8e117613ac4378e82739c266c2e2f818e9ed79bc0e84e0ebe7b8f61c02b88c0
|
||||
size 14836
|
||||
oid sha256:36b72898b310b3c4e57fbc506e5ee663d74ed64ed2967d8a9d1201da50904bf0
|
||||
size 13583
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2259e821d911a5455a4bdd72716d5ae1de1a5fd5b6aaad666d41846edac716cc
|
||||
size 15079
|
||||
oid sha256:6a1ebf498979d1e4371169657582cfb4fe0cf50f2dce9a666c8b3f66355057e1
|
||||
size 13824
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3fc2414e502c0f064fc8d15e5f14ccdc5c250ded619b8362472d2a41fb522de9
|
||||
size 15023
|
||||
oid sha256:cce4f8672aba4f77ed12ecd59fa0b4846ee6673d59e91de748e5f3333f124292
|
||||
size 13766
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:acb0efba94927a8ada6a16a66bd82160a1f41d320a2f512114da54aaa2cc1574
|
||||
size 16143
|
||||
oid sha256:2fc8f143da673f83abe1def14f87aa325ecfb8127de9c5f66c97096e43dc1f28
|
||||
size 14876
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0dcef8172fb810da542e5d4f364a60fa8cb0d0d3a212ad939e0117ca91b5b57c
|
||||
size 30461
|
||||
oid sha256:b3f472c100d4432064af12da764bd44baa3295ad4b20c6158cc3fa467667797a
|
||||
size 30794
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7e3d24b86091c0041a1e7426209a1d1d826b656f04b9fed1d662e3d5b67aa95f
|
||||
size 6185
|
||||
oid sha256:74d53efec74b56fcd689b1b2de4facb0570306995a6f5451d38e8303b65bbcc6
|
||||
size 6187
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c21917a9f4f80354ac18a7c4ced1f204a1b28bed4ac5fb0cf34e339dbdc7d5da
|
||||
size 15929
|
||||
oid sha256:38d2cef0a125b5b6bba81af716c1accd522a60e0c915c8f9a7be502382bfcb2b
|
||||
size 14661
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d2c75a7bb06035f07f364c7fb7a1eb0b813663658d018e4eea881c48cc0e276c
|
||||
size 8698
|
||||
oid sha256:ce16f0acdd7d82bb2b1bfd993e015cf89f1d20d50d2c045a6ad08fd4a6382c50
|
||||
size 8931
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
* Uses kilo-ui's DockPrompt component for proper surface styling.
|
||||
*
|
||||
* Per-rule toggles allow users to approve/deny individual permission rules for future requests.
|
||||
* The command buttons (Deny / Allow Always / Allow Once) control the current command.
|
||||
* For bash, the hierarchical rules from metadata.rules are shown.
|
||||
* For other tools, the always array is shown so users can configure per-tool permissions.
|
||||
* The command buttons (Deny / Allow Once) control the current command.
|
||||
* When all rules are toggled ✓, the command auto-runs.
|
||||
*/
|
||||
|
||||
@@ -24,14 +26,14 @@ let rulesExpandedPreference = false
|
||||
export const PermissionDock: Component<{
|
||||
request: PermissionRequest
|
||||
responding: boolean
|
||||
onDecide: (response: "once" | "always" | "reject", approvedAlways: string[], deniedAlways: string[]) => void
|
||||
onDecide: (response: "once" | "reject", approvedAlways: string[], deniedAlways: string[]) => void
|
||||
}> = (props) => {
|
||||
const session = useSession()
|
||||
const language = useLanguage()
|
||||
|
||||
const fromChild = () => props.request.sessionID !== session.currentSessionID()
|
||||
// Bash sends fine-grained rules via metadata.rules; other tools have no dropdown.
|
||||
const rules = () => props.request.args?.rules ?? []
|
||||
// Bash sends fine-grained rules via metadata.rules; other tools use the always array.
|
||||
const rules = () => props.request.args?.rules ?? props.request.always ?? []
|
||||
// Rules like "git *" or "git log *" — strip the trailing wildcard for display.
|
||||
// A bare "*" (global wildcard) becomes empty so only the tool name shows.
|
||||
const label = (rule: string) => (rule === "*" ? "" : rule.replace(/ \*$/, ""))
|
||||
@@ -43,7 +45,6 @@ export const PermissionDock: Component<{
|
||||
const [decisions, setDecisions] = createSignal<Record<number, RuleDecision>>({})
|
||||
const [expanded, setExpanded] = createSignal(rulesExpandedPreference)
|
||||
|
||||
const hasDenied = () => Object.values(decisions()).some((d) => d === "denied")
|
||||
const hasRules = () => rules().length > 0
|
||||
|
||||
const toggleExpanded = () => {
|
||||
@@ -191,17 +192,6 @@ export const PermissionDock: Component<{
|
||||
>
|
||||
{language.t("ui.permission.allowOnce")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
const { approved, denied } = collectRules()
|
||||
props.onDecide("always", approved, denied)
|
||||
}}
|
||||
disabled={props.responding || hasDenied()}
|
||||
>
|
||||
{language.t("ui.permission.allowAlways")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
|
||||
@@ -162,7 +162,8 @@ const globPermission: PermissionRequest = {
|
||||
sessionID: SESSION_ID,
|
||||
toolName: "glob",
|
||||
patterns: ["**/*.md"],
|
||||
args: { pattern: "**/*.md", rules: ["**/*.md"] },
|
||||
always: ["*"],
|
||||
args: { pattern: "**/*.md" },
|
||||
tool: { messageID: ASST_MSG_ID, callID: "call-glob-001" },
|
||||
}
|
||||
|
||||
@@ -171,6 +172,7 @@ const bashPermission: PermissionRequest = {
|
||||
sessionID: SESSION_ID,
|
||||
toolName: "bash",
|
||||
patterns: ["bun test"],
|
||||
always: ["bun *"],
|
||||
args: { command: "bun test", rules: ["bun *", "bun test"] },
|
||||
tool: { messageID: ASST_MSG_ID, callID: "call-bash-001" },
|
||||
}
|
||||
@@ -180,7 +182,8 @@ const dockPermission: PermissionRequest = {
|
||||
sessionID: SESSION_ID,
|
||||
toolName: "write",
|
||||
patterns: ["src/main.tsx", "src/utils.ts"],
|
||||
args: { rules: ["src/main.tsx", "src/utils.ts"] },
|
||||
always: ["*"],
|
||||
args: {},
|
||||
// No `tool` field — this is a non-tool (dock) permission
|
||||
}
|
||||
|
||||
@@ -292,7 +295,8 @@ const todoWritePermission: PermissionRequest = {
|
||||
sessionID: SESSION_ID,
|
||||
toolName: "todowrite",
|
||||
patterns: ["*"],
|
||||
args: { rules: ["*"] },
|
||||
always: ["*"],
|
||||
args: {},
|
||||
tool: { messageID: ASST_MSG_ID, callID: "call-todo-001" },
|
||||
}
|
||||
|
||||
@@ -612,7 +616,8 @@ const editPermission: PermissionRequest = {
|
||||
sessionID: SESSION_ID,
|
||||
toolName: "edit",
|
||||
patterns: ["src/components/App.tsx", "src/utils/helpers.ts"],
|
||||
args: { rules: ["src/components/App.tsx", "src/utils/helpers.ts"] },
|
||||
always: ["*"],
|
||||
args: {},
|
||||
tool: { messageID: ASST_MSG_ID, callID: "call-edit-001" },
|
||||
}
|
||||
|
||||
@@ -645,7 +650,8 @@ const websearchPermission: PermissionRequest = {
|
||||
sessionID: SESSION_ID,
|
||||
toolName: "websearch",
|
||||
patterns: ["*"],
|
||||
args: { rules: ["*"] },
|
||||
always: ["*"],
|
||||
args: {},
|
||||
tool: { messageID: ASST_MSG_ID, callID: "call-websearch-001" },
|
||||
}
|
||||
|
||||
@@ -677,8 +683,9 @@ const externalDirPermission: PermissionRequest = {
|
||||
id: "perm-extdir-001",
|
||||
sessionID: SESSION_ID,
|
||||
toolName: "external_directory",
|
||||
patterns: ["/home/user/other-project/config.json"],
|
||||
args: { rules: ["/home/user/other-project/config.json"] },
|
||||
patterns: ["/home/user/other-project/*"],
|
||||
always: ["/home/user/other-project/*"],
|
||||
args: { filepath: "/home/user/other-project/config.json" },
|
||||
tool: { messageID: ASST_MSG_ID, callID: "call-extdir-001" },
|
||||
}
|
||||
|
||||
@@ -711,6 +718,7 @@ const bashManyRulesPermission: PermissionRequest = {
|
||||
sessionID: SESSION_ID,
|
||||
toolName: "bash",
|
||||
patterns: ["npm install"],
|
||||
always: ["npm install *"],
|
||||
args: {
|
||||
command: "npm install",
|
||||
rules: ["npm *", "npm install", "npm run *", "npm test", "npm run build", "npx *"],
|
||||
@@ -747,6 +755,7 @@ const subagentPermission: PermissionRequest = {
|
||||
sessionID: "child-session-001",
|
||||
toolName: "bash",
|
||||
patterns: ["git status"],
|
||||
always: ["git status *"],
|
||||
args: { command: "git status", rules: ["git *", "git status"] },
|
||||
tool: { messageID: ASST_MSG_ID, callID: "call-subagent-001" },
|
||||
}
|
||||
|
||||
@@ -157,6 +157,7 @@ export interface PermissionRequest {
|
||||
sessionID: string
|
||||
toolName: string
|
||||
patterns: string[]
|
||||
always: string[]
|
||||
args: Record<string, unknown> & { rules?: string[] }
|
||||
message?: string
|
||||
tool?: { messageID: string; callID: string }
|
||||
|
||||
@@ -1563,7 +1563,10 @@ export namespace Config {
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateGlobal(config: Info) {
|
||||
// kilocode_change start — add dispose option to skip Instance.disposeAll for permission-only changes
|
||||
export async function updateGlobal(config: Info, options?: { dispose?: boolean }) {
|
||||
const dispose = options?.dispose ?? true
|
||||
// kilocode_change end
|
||||
const filepath = globalConfigFile()
|
||||
const before = await Filesystem.readText(filepath).catch((err: any) => {
|
||||
if (err.code === "ENOENT") return "{}"
|
||||
|
||||
@@ -66,12 +66,36 @@ export namespace PermissionNext {
|
||||
}
|
||||
|
||||
// kilocode_change start — inverse of fromConfig: convert rules back to config format
|
||||
/**
|
||||
* Permissions typed as PermissionAction in the config schema (scalar-only).
|
||||
* These must be serialized as "allow"/"deny"/"ask", not as { "*": "allow" }.
|
||||
*/
|
||||
const SCALAR_ONLY_PERMISSIONS = new Set([
|
||||
"todowrite",
|
||||
"todoread",
|
||||
"question",
|
||||
"webfetch",
|
||||
"websearch",
|
||||
"codesearch",
|
||||
"doom_loop",
|
||||
])
|
||||
|
||||
export function toConfig(rules: Ruleset): Config.Permission {
|
||||
const result: Config.Permission = {}
|
||||
for (const rule of rules) {
|
||||
const existing = result[rule.permission]
|
||||
|
||||
// Scalar-only permissions (e.g. websearch, todowrite, doom_loop) only
|
||||
// accept PermissionAction ("allow"/"deny"/"ask"), not object form.
|
||||
// Use scalar format for "*"; skip non-wildcard patterns (they can't be
|
||||
// represented in the config schema — they only work in-memory).
|
||||
if (SCALAR_ONLY_PERMISSIONS.has(rule.permission)) {
|
||||
if (rule.pattern === "*") result[rule.permission] = rule.action
|
||||
continue
|
||||
}
|
||||
|
||||
if (existing === undefined) {
|
||||
// Always use object format to avoid replacing existing granular rules
|
||||
// Use object format to avoid replacing existing granular rules
|
||||
// when merged via updateGlobal (e.g. { read: "allow" } would wipe
|
||||
// { read: { "*": "ask", "src/*": "allow" } })
|
||||
result[rule.permission] = { [rule.pattern]: rule.action }
|
||||
@@ -194,15 +218,15 @@ export namespace PermissionNext {
|
||||
const existing = s.pending[input.requestID]
|
||||
if (!existing) throw new NotFoundError({ message: `Permission request ${input.requestID} not found` })
|
||||
|
||||
const validRules = new Set(existing.info.metadata?.rules ?? [])
|
||||
// Combine metadata.rules (bash hierarchy) and always (all tools).
|
||||
// Set preserves insertion order and deduplicates.
|
||||
const validRules = new Set([...(existing.info.metadata?.rules ?? []), ...existing.info.always])
|
||||
const permission = existing.info.permission
|
||||
|
||||
// Build rules in metadata.rules order so broader patterns come before
|
||||
// specific ones, preserving intended precedence for evaluate(findLast).
|
||||
const approvedSet = new Set(input.approvedAlways ?? [])
|
||||
const deniedSet = new Set(input.deniedAlways ?? [])
|
||||
const newRules: Ruleset = []
|
||||
for (const pattern of existing.info.metadata?.rules ?? []) {
|
||||
for (const pattern of validRules) {
|
||||
if (approvedSet.has(pattern)) newRules.push({ permission, pattern, action: "allow" })
|
||||
if (deniedSet.has(pattern)) newRules.push({ permission, pattern, action: "deny" })
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ describe("saveAlwaysRules", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("ignores patterns not in metadata.rules", async () => {
|
||||
test("ignores patterns not in metadata.rules or always", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
@@ -97,7 +97,7 @@ describe("saveAlwaysRules", () => {
|
||||
ruleset: [],
|
||||
})
|
||||
|
||||
// "curl" is not in metadata.rules — should be silently ignored
|
||||
// "curl" is not in metadata.rules or always — should be silently ignored
|
||||
await PermissionNext.saveAlwaysRules({
|
||||
requestID: "permission_3",
|
||||
approvedAlways: ["npm install", "curl http://evil.com"],
|
||||
@@ -133,6 +133,40 @@ describe("saveAlwaysRules", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("accepts patterns from always array (non-bash tools)", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const askPromise = PermissionNext.ask({
|
||||
id: "permission_nonbash",
|
||||
sessionID: "session_test",
|
||||
permission: "read",
|
||||
patterns: ["src/main.ts"],
|
||||
metadata: {},
|
||||
always: ["*"],
|
||||
ruleset: [],
|
||||
})
|
||||
|
||||
// "*" is in always — should be accepted even without metadata.rules
|
||||
await PermissionNext.saveAlwaysRules({ requestID: "permission_nonbash", approvedAlways: ["*"] })
|
||||
await PermissionNext.reply({ requestID: "permission_nonbash", reply: "once" })
|
||||
await expect(askPromise).resolves.toBeUndefined()
|
||||
|
||||
// "*" wildcard should auto-allow any read
|
||||
const result = await PermissionNext.ask({
|
||||
sessionID: "session_test",
|
||||
permission: "read",
|
||||
patterns: ["any/file.ts"],
|
||||
metadata: {},
|
||||
always: [],
|
||||
ruleset: [],
|
||||
})
|
||||
expect(result).toBeUndefined()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("accepts hierarchy patterns from metadata.rules", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
|
||||
@@ -60,3 +60,29 @@ test("toConfig - roundtrip with fromConfig (object)", () => {
|
||||
const result = PermissionNext.toConfig(rules)
|
||||
expect(result).toEqual(config)
|
||||
})
|
||||
|
||||
test("toConfig - scalar-only permission uses scalar format", () => {
|
||||
const result = PermissionNext.toConfig([{ permission: "websearch", pattern: "*", action: "allow" }])
|
||||
expect(result).toEqual({ websearch: "allow" })
|
||||
})
|
||||
|
||||
test("toConfig - scalar-only permission with non-wildcard pattern is skipped", () => {
|
||||
// doom_loop uses always: [toolName], so pattern can be "bash" etc.
|
||||
// Non-wildcard patterns for scalar-only permissions can't be represented
|
||||
// in the config schema — they only work in-memory (known limitation).
|
||||
const result = PermissionNext.toConfig([{ permission: "doom_loop", pattern: "bash", action: "allow" }])
|
||||
expect(result).toEqual({})
|
||||
})
|
||||
|
||||
test("toConfig - mixed scalar-only and rule-capable permissions", () => {
|
||||
const result = PermissionNext.toConfig([
|
||||
{ permission: "websearch", pattern: "*", action: "allow" },
|
||||
{ permission: "todowrite", pattern: "*", action: "allow" },
|
||||
{ permission: "bash", pattern: "npm *", action: "allow" },
|
||||
])
|
||||
expect(result).toEqual({
|
||||
websearch: "allow",
|
||||
todowrite: "allow",
|
||||
bash: { "npm *": "allow" },
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user