mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
Save permission rules to config (#7143)
* 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 * 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 * fix: remove unnecessary dispose con config global update --------- 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
bea9eb6bf4
commit
28330ea765
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6ba76e2d5b993e999914fd0538650728adf78f7ad70978d8ee9982525f6ab5e8
|
||||
size 17626
|
||||
oid sha256:321afc41e5908e73656b1a8d5e3c32f23f54d10dee12655506b4b58348230305
|
||||
size 13718
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:29872fa36a6a6ba9e20765f92380b86f9a91beb9702b3cdb95914ea46a2cf40c
|
||||
size 17833
|
||||
oid sha256:666d73c69b19e8c79815129b9d17747dbf755c316f3e6da0cba6977966b9cccc
|
||||
size 13906
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ce915e449cdec411c00df613ffc1017f95dba69737e0474fc8c483ea9f284026
|
||||
size 17872
|
||||
oid sha256:b0608142ccb5bab0468ae44060bed17c5ba98b8ee2d5a477627644d1c91e760a
|
||||
size 13952
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b39675360da34fc6cb9c1ff4e51afeff79417ca4baa88816967d2b0a4ece858f
|
||||
size 18043
|
||||
oid sha256:7dd67617fdfc9ea6c4b0f54895d930d76c5c712b158e4be93967a3a3239a53b2
|
||||
size 14143
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c3b7d96f123e880b1395d160d17a48f385ef59344326c44d7cd13b342d2ca49e
|
||||
size 24193
|
||||
oid sha256:43b16bc81164f310ae40f7ebe6459281f374be323dfc07e3bef6b2d1608b6637
|
||||
size 20835
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:739b8eb94e5cde39e4127a6ed3c499392174a380812e2e3497965643cac3dee9
|
||||
size 22265
|
||||
oid sha256:ee48a78cd2fe43c34d0014262ac794e48e5405d2116140d08219fc8e1621be0c
|
||||
size 18516
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:62ef30b05bf2ec47a7ddd02a03f5f1cded5ced9cf9332dd3bd50d6809bcce96d
|
||||
size 18447
|
||||
oid sha256:f69b6545bd98003edf8c7208eb655066a24847e055cf0a5492c99ff67bbbab38
|
||||
size 14621
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1f9864861136b578c4e46ece7dae5a139ada7e7be3d219462ef3a549b94239ec
|
||||
size 18648
|
||||
oid sha256:5a7db3c77ca7dee3a74fe3fd08b45e8d32a00193d1fb13ec7b4e2b1671dac7a3
|
||||
size 14831
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:514803fa46f43ff58d8d403e630afc5c01a8841ac420d406755515bceb38beae
|
||||
size 23132
|
||||
oid sha256:c4aa38c3ade48132b3f0083438ef71c0e34f180db3bbd4d70455d6bb34a1225c
|
||||
size 19202
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:578f6daa54a25fba38b049d623e2b12069b0cbe1c5e094031d8a0b0af0a56f52
|
||||
size 23966
|
||||
oid sha256:e46b79adada26121c948ce33b78ba78c8b0b13dc153708b60a56f1cfc6034503
|
||||
size 20095
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:91ba11c0a81ced5dfe824a4c9e8fdcbaebaaddc660ef2b245df56903e180e4b7
|
||||
size 18949
|
||||
oid sha256:699b49cbb768c3a9cea64878a01b5507a1f304a14f12363f2329ad07c8cc35cb
|
||||
size 15082
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6f8bc26191620881314eff9ac921abba4e201c5af7ec8d222e405fdee9ddae9a
|
||||
size 17909
|
||||
oid sha256:60e6d4b1d0e860195c26752e50a97e6d22d7911d029e7d08d24f46b327b21a01
|
||||
size 14075
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ce112e9762168271fd8b7b4316b740008269e9d248a668887adce3dde13478f0
|
||||
size 18362
|
||||
oid sha256:4ff80ac6cd76dbb278e6483c2e5c352095fc893ae5f5d5cb2eb70ded640bfc0e
|
||||
size 16913
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d499d73467c37052d1277deefdd419e370a8239d7e117806d8442b2b9cc851a7
|
||||
size 18886
|
||||
oid sha256:6531e4d0e975a29d538bc7b609a987071afc0997b396fd06e52cbbf2b7ef2e26
|
||||
size 17467
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b3656d23c630a2d7f412d2fc3eee231f45cbe47e6d1e4e7ca44ae4cca3c3160b
|
||||
size 15323
|
||||
oid sha256:cb8dd7f0ecbbeed00e1321c050964b54f832a571c8acbc37269de205bd64a483
|
||||
size 13788
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c95ebadbd707aab03b0b14858cd9e25c4e7b06e8976abf76e377b9eaa5bd2144
|
||||
size 16900
|
||||
oid sha256:484ec50824ececf29d8b1464fb94ef48637d4f97b9a3c68dcf3ae5d8f157dcf6
|
||||
size 15502
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1c43a0a2f6a351d53da542aba6b68113e8231c1ee9cf7276bf6a16263b65d811
|
||||
size 18775
|
||||
oid sha256:ab9edfc18d0d7094f7c5410d7b08e0012c2128f1638b7090aad9301f127473ed
|
||||
size 17425
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d23b2a1621e33e92e65c7ead23b42f05decd443b370ef7122b840e6f5cf043e1
|
||||
size 21177
|
||||
oid sha256:05783f1a5b4c8631dee2582b243a724eb4fb811cde894f86ce3575e2da1a997e
|
||||
size 17416
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:63b8651967a042ef6e79f721fc4bf99eae875296dbd991566d3c3b7e90f04669
|
||||
size 18542
|
||||
oid sha256:24364efcbb54ed842a9af0c630e6b2a4f5df37d8cabb651e18dcbebda37a26c5
|
||||
size 17221
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:94ef29b3eca3b0e9e407daf9c17388dcf5735c134a74bc13500559bcf9707c9b
|
||||
size 16338
|
||||
oid sha256:e8e117613ac4378e82739c266c2e2f818e9ed79bc0e84e0ebe7b8f61c02b88c0
|
||||
size 14836
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:168eacdc6523223587bad5f72f3828e03211733b4dc1fa39f152409089e03ff3
|
||||
size 16468
|
||||
oid sha256:2259e821d911a5455a4bdd72716d5ae1de1a5fd5b6aaad666d41846edac716cc
|
||||
size 15079
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9c2402fb61b05c355cedea5a26c39f4a5b4d4a1e04c0fb8ac1ac50edbdc746b3
|
||||
size 16426
|
||||
oid sha256:3fc2414e502c0f064fc8d15e5f14ccdc5c250ded619b8362472d2a41fb522de9
|
||||
size 15023
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:38712f08584902b1ccb267fed0271c6c1a9b5f549b47bd743ca344604df2095a
|
||||
size 17433
|
||||
oid sha256:acb0efba94927a8ada6a16a66bd82160a1f41d320a2f512114da54aaa2cc1574
|
||||
size 16143
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:84e3f1c43bd36828f6548f3a3a85827693b370f6f3fc319408829eb742146bb5
|
||||
size 19287
|
||||
oid sha256:c21917a9f4f80354ac18a7c4ced1f204a1b28bed4ac5fb0cf34e339dbdc7d5da
|
||||
size 15929
|
||||
|
||||
@@ -179,7 +179,6 @@ export const PermissionDock: Component<{
|
||||
<div data-slot="permission-hint">{toolDescription()}</div>
|
||||
</Show>
|
||||
|
||||
<p data-slot="permission-session-hint">{language.t("ui.permission.sessionHint")}</p>
|
||||
<div data-slot="permission-actions">
|
||||
<Button
|
||||
variant="primary"
|
||||
|
||||
@@ -1377,14 +1377,6 @@
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
[data-slot="permission-session-hint"] {
|
||||
font-size: 11px;
|
||||
color: var(--text-weak, var(--vscode-descriptionForeground));
|
||||
margin: 0;
|
||||
padding: 4px 0 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
[data-slot="permission-actions"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -158,12 +158,12 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
|
||||
<Switch>
|
||||
<Match when={props.request.always.length === 1 && props.request.always[0] === "*"}>
|
||||
{/* kilocode_change */}
|
||||
<TextBody title={"This will allow " + props.request.permission + " until Kilo is restarted."} />
|
||||
<TextBody title={"This will allow " + props.request.permission + " permanently."} />
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<box paddingLeft={1} gap={1}>
|
||||
{/* kilocode_change */}
|
||||
<text fg={theme.textMuted}>This will allow the following patterns until Kilo is restarted</text>
|
||||
<text fg={theme.textMuted}>This will allow the following patterns permanently</text>
|
||||
<box>
|
||||
<For each={props.request.always}>
|
||||
{(pattern) => (
|
||||
|
||||
@@ -1426,7 +1426,7 @@ export namespace Config {
|
||||
export async function update(config: Info) {
|
||||
const filepath = path.join(Instance.directory, "config.json")
|
||||
const existing = await loadFile(filepath)
|
||||
await Filesystem.writeJson(filepath, stripNulls(mergeDeep(existing, config) as Record<string, unknown>)) // kilocode_change - strip null delete sentinels
|
||||
await Filesystem.writeJson(filepath, mergeConfig(existing, config)) // kilocode_change
|
||||
await Instance.dispose()
|
||||
}
|
||||
|
||||
@@ -1462,6 +1462,35 @@ export namespace Config {
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
// kilocode_change start — merge config with normalization pipeline
|
||||
/**
|
||||
* Merge a patch into an existing config:
|
||||
* 1. Normalize permission scalars → objects when the patch has an object
|
||||
* (e.g. existing `"bash": "ask"` + patch `"bash": { "npm *": "allow" }`
|
||||
* → promotes existing to `"bash": { "*": "ask" }` so mergeDeep works)
|
||||
* 2. Deep-merge
|
||||
* 3. Strip null delete sentinels
|
||||
*/
|
||||
function mergeConfig(existing: Info, patch: Info): Info {
|
||||
const e = { ...existing } as Record<string, unknown>
|
||||
const p = patch as Record<string, unknown>
|
||||
// Normalize permission scalars before merge (clone to avoid mutating the input)
|
||||
const existingPerm = e.permission
|
||||
const patchPerm = p.permission
|
||||
if (isRecord(existingPerm) && isRecord(patchPerm)) {
|
||||
const cloned = { ...existingPerm }
|
||||
for (const [key, patchValue] of Object.entries(patchPerm)) {
|
||||
const existingValue = cloned[key]
|
||||
if (typeof existingValue === "string" && isRecord(patchValue)) {
|
||||
cloned[key] = { "*": existingValue }
|
||||
}
|
||||
}
|
||||
e.permission = cloned
|
||||
}
|
||||
return stripNulls(mergeDeep(e, p) as Record<string, unknown>) as Info
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
function patchJsonc(input: string, patch: unknown, path: string[] = []): string {
|
||||
if (!isRecord(patch)) {
|
||||
// kilocode_change - null means "delete this key" — pass undefined to jsonc-parser's modify()
|
||||
@@ -1478,11 +1507,15 @@ export namespace Config {
|
||||
// scalar (e.g. permission.bash is "ask" as a string), jsonc-parser cannot
|
||||
// add child keys to it. Detect this case and replace the whole node with
|
||||
// the patch object in a single modify() call instead of recursing.
|
||||
// For permission keys, promote the scalar to { "*": scalarValue } so the
|
||||
// wildcard default is preserved. For other keys, replace directly.
|
||||
if (path.length > 0) {
|
||||
const tree = parseTree(input)
|
||||
const node = tree && findNodeAtLocation(tree, path)
|
||||
if (node && node.type !== "object") {
|
||||
const edits = modify(input, path, patch, {
|
||||
const isPermissionKey = path[0] === "permission" && path.length === 2
|
||||
const replacement = isPermissionKey ? { "*": node.value, ...patch } : patch
|
||||
const edits = modify(input, path, replacement, {
|
||||
formattingOptions: { insertSpaces: true, tabSize: 2 },
|
||||
})
|
||||
return applyEdits(input, edits)
|
||||
@@ -1540,7 +1573,7 @@ export namespace Config {
|
||||
const next = await (async () => {
|
||||
if (!filepath.endsWith(".jsonc")) {
|
||||
const existing = parseConfig(before, filepath)
|
||||
const merged = stripNulls(mergeDeep(existing, config) as Record<string, unknown>) as Info // kilocode_change - strip null delete sentinels
|
||||
const merged = mergeConfig(existing, config) // kilocode_change
|
||||
await Filesystem.writeJson(filepath, merged)
|
||||
return merged
|
||||
}
|
||||
@@ -1553,11 +1586,6 @@ export namespace Config {
|
||||
|
||||
global.reset()
|
||||
|
||||
// kilocode_change start - only reset config cache, don't dispose all instances.
|
||||
// Instance.disposeAll() was destroying all session state, MCP connections, and
|
||||
// in-flight operations across every project whenever any global config changed
|
||||
// (e.g. removing a mode). The cache reset above is sufficient — consumers will
|
||||
// pick up the new config on their next read.
|
||||
GlobalBus.emit("event", {
|
||||
directory: "global",
|
||||
payload: {
|
||||
@@ -1565,7 +1593,6 @@ export namespace Config {
|
||||
properties: {},
|
||||
},
|
||||
})
|
||||
// kilocode_change end
|
||||
|
||||
return next
|
||||
}
|
||||
|
||||
@@ -65,6 +65,28 @@ export namespace PermissionNext {
|
||||
return rulesets.flat()
|
||||
}
|
||||
|
||||
// kilocode_change start — inverse of fromConfig: convert rules back to config format
|
||||
export function toConfig(rules: Ruleset): Config.Permission {
|
||||
const result: Config.Permission = {}
|
||||
for (const rule of rules) {
|
||||
const existing = result[rule.permission]
|
||||
if (existing === undefined) {
|
||||
// Always 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 }
|
||||
continue
|
||||
}
|
||||
if (typeof existing === "string") {
|
||||
result[rule.permission] = { "*": existing, [rule.pattern]: rule.action }
|
||||
continue
|
||||
}
|
||||
existing[rule.pattern] = rule.action
|
||||
}
|
||||
return result
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
export const Request = z
|
||||
.object({
|
||||
id: Identifier.schema("permission"),
|
||||
@@ -175,11 +197,19 @@ export namespace PermissionNext {
|
||||
const validRules = new Set(existing.info.metadata?.rules ?? [])
|
||||
const permission = existing.info.permission
|
||||
|
||||
for (const pattern of input.approvedAlways ?? []) {
|
||||
if (validRules.has(pattern)) s.approved.push({ permission, pattern, action: "allow" })
|
||||
// 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 ?? []) {
|
||||
if (approvedSet.has(pattern)) newRules.push({ permission, pattern, action: "allow" })
|
||||
if (deniedSet.has(pattern)) newRules.push({ permission, pattern, action: "deny" })
|
||||
}
|
||||
for (const pattern of input.deniedAlways ?? []) {
|
||||
if (validRules.has(pattern)) s.approved.push({ permission, pattern, action: "deny" })
|
||||
s.approved.push(...newRules)
|
||||
|
||||
if (newRules.length > 0) {
|
||||
await Config.updateGlobal({ permission: toConfig(newRules) })
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -254,6 +284,16 @@ export namespace PermissionNext {
|
||||
// UI to manage it
|
||||
// db().insert(PermissionTable).values({ projectID: Instance.project.id, data: s.approved })
|
||||
// .onConflictDoUpdate({ target: PermissionTable.projectID, set: { data: s.approved } }).run()
|
||||
// kilocode_change start - persist always rules to global config
|
||||
const alwaysRules: Ruleset = existing.info.always.map((pattern) => ({
|
||||
permission: existing.info.permission,
|
||||
pattern,
|
||||
action: "allow" as const,
|
||||
}))
|
||||
if (alwaysRules.length > 0) {
|
||||
await Config.updateGlobal({ permission: toConfig(alwaysRules) })
|
||||
}
|
||||
// kilocode_change end
|
||||
return
|
||||
}
|
||||
},
|
||||
|
||||
@@ -166,4 +166,132 @@ describe("saveAlwaysRules", () => {
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("mixed allow/deny preserves metadata.rules order", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const askPromise = PermissionNext.ask({
|
||||
id: "permission_5",
|
||||
sessionID: "session_test",
|
||||
permission: "bash",
|
||||
patterns: ["npm install lodash"],
|
||||
// rules ordered broad → specific
|
||||
metadata: { rules: ["npm *", "npm install *"] },
|
||||
always: ["npm install *"],
|
||||
ruleset: [],
|
||||
})
|
||||
|
||||
// Deny broad, allow specific — specific should win
|
||||
await PermissionNext.saveAlwaysRules({
|
||||
requestID: "permission_5",
|
||||
approvedAlways: ["npm install *"],
|
||||
deniedAlways: ["npm *"],
|
||||
})
|
||||
await PermissionNext.reply({ requestID: "permission_5", reply: "once" })
|
||||
await expect(askPromise).resolves.toBeUndefined()
|
||||
|
||||
// "npm install foo" matches both rules; "npm install *" (allow) comes
|
||||
// after "npm *" (deny) in metadata.rules order, so allow wins
|
||||
const result = await PermissionNext.ask({
|
||||
sessionID: "session_test",
|
||||
permission: "bash",
|
||||
patterns: ["npm install foo"],
|
||||
metadata: {},
|
||||
always: [],
|
||||
ruleset: [],
|
||||
})
|
||||
expect(result).toBeUndefined()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("deny broad + allow specific: specific allow wins", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const askPromise = PermissionNext.ask({
|
||||
id: "permission_6",
|
||||
sessionID: "session_test",
|
||||
permission: "bash",
|
||||
patterns: ["git log --oneline"],
|
||||
metadata: { rules: ["git *", "git log *"] },
|
||||
always: ["git log *"],
|
||||
ruleset: [],
|
||||
})
|
||||
|
||||
await PermissionNext.saveAlwaysRules({
|
||||
requestID: "permission_6",
|
||||
approvedAlways: ["git log *"],
|
||||
deniedAlways: ["git *"],
|
||||
})
|
||||
await PermissionNext.reply({ requestID: "permission_6", reply: "once" })
|
||||
await expect(askPromise).resolves.toBeUndefined()
|
||||
|
||||
// "git log --oneline" should be allowed (specific allow after broad deny)
|
||||
const allowed = await PermissionNext.ask({
|
||||
sessionID: "session_test",
|
||||
permission: "bash",
|
||||
patterns: ["git log --oneline"],
|
||||
metadata: {},
|
||||
always: [],
|
||||
ruleset: [],
|
||||
})
|
||||
expect(allowed).toBeUndefined()
|
||||
|
||||
// "git status" should be denied (only matches broad deny)
|
||||
await expect(
|
||||
PermissionNext.ask({
|
||||
sessionID: "session_test",
|
||||
permission: "bash",
|
||||
patterns: ["git status"],
|
||||
metadata: {},
|
||||
always: [],
|
||||
ruleset: [],
|
||||
}),
|
||||
).rejects.toBeInstanceOf(PermissionNext.DeniedError)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("rules not in metadata.rules are silently ignored", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const askPromise = PermissionNext.ask({
|
||||
id: "permission_7",
|
||||
sessionID: "session_test",
|
||||
permission: "bash",
|
||||
patterns: ["npm install"],
|
||||
metadata: { rules: ["npm *"] },
|
||||
always: ["npm *"],
|
||||
ruleset: [],
|
||||
})
|
||||
|
||||
// "curl" is not in metadata.rules — should be silently ignored
|
||||
await PermissionNext.saveAlwaysRules({
|
||||
requestID: "permission_7",
|
||||
approvedAlways: ["npm *", "curl *"],
|
||||
})
|
||||
await PermissionNext.reply({ requestID: "permission_7", reply: "once" })
|
||||
await expect(askPromise).resolves.toBeUndefined()
|
||||
|
||||
// curl should still require permission (not auto-allowed)
|
||||
const curlPromise = PermissionNext.ask({
|
||||
id: "permission_curl2",
|
||||
sessionID: "session_test",
|
||||
permission: "bash",
|
||||
patterns: ["curl http://example.com"],
|
||||
metadata: {},
|
||||
always: [],
|
||||
ruleset: [],
|
||||
})
|
||||
await PermissionNext.reply({ requestID: "permission_curl2", reply: "reject" })
|
||||
await expect(curlPromise).rejects.toBeInstanceOf(PermissionNext.RejectedError)
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { test, expect } from "bun:test"
|
||||
import { PermissionNext } from "../../src/permission/next"
|
||||
|
||||
// toConfig tests (inverse of fromConfig)
|
||||
|
||||
test("toConfig - single wildcard rule uses object format", () => {
|
||||
const result = PermissionNext.toConfig([{ permission: "read", pattern: "*", action: "allow" }])
|
||||
expect(result).toEqual({ read: { "*": "allow" } })
|
||||
})
|
||||
|
||||
test("toConfig - single non-wildcard rule uses object format", () => {
|
||||
const result = PermissionNext.toConfig([{ permission: "bash", pattern: "npm *", action: "allow" }])
|
||||
expect(result).toEqual({ bash: { "npm *": "allow" } })
|
||||
})
|
||||
|
||||
test("toConfig - multiple rules for same permission use object format", () => {
|
||||
const result = PermissionNext.toConfig([
|
||||
{ permission: "bash", pattern: "*", action: "ask" },
|
||||
{ permission: "bash", pattern: "npm *", action: "allow" },
|
||||
])
|
||||
expect(result).toEqual({ bash: { "*": "ask", "npm *": "allow" } })
|
||||
})
|
||||
|
||||
test("toConfig - mixed permissions", () => {
|
||||
const result = PermissionNext.toConfig([
|
||||
{ permission: "read", pattern: "*", action: "allow" },
|
||||
{ permission: "bash", pattern: "npm *", action: "allow" },
|
||||
{ permission: "bash", pattern: "git *", action: "allow" },
|
||||
])
|
||||
expect(result).toEqual({
|
||||
read: { "*": "allow" },
|
||||
bash: { "npm *": "allow", "git *": "allow" },
|
||||
})
|
||||
})
|
||||
|
||||
test("toConfig - empty rules returns empty object", () => {
|
||||
const result = PermissionNext.toConfig([])
|
||||
expect(result).toEqual({})
|
||||
})
|
||||
|
||||
test("toConfig - wildcard then specific promotes to object", () => {
|
||||
const result = PermissionNext.toConfig([
|
||||
{ permission: "bash", pattern: "*", action: "ask" },
|
||||
{ permission: "bash", pattern: "rm *", action: "deny" },
|
||||
])
|
||||
expect(result).toEqual({ bash: { "*": "ask", "rm *": "deny" } })
|
||||
})
|
||||
|
||||
test("toConfig - roundtrip with fromConfig (simple) always uses object format", () => {
|
||||
const config = { read: "allow" as const, bash: "ask" as const }
|
||||
const rules = PermissionNext.fromConfig(config)
|
||||
const result = PermissionNext.toConfig(rules)
|
||||
// toConfig always uses object format to avoid erasing existing granular rules on merge
|
||||
expect(result).toEqual({ read: { "*": "allow" }, bash: { "*": "ask" } })
|
||||
})
|
||||
|
||||
test("toConfig - roundtrip with fromConfig (object)", () => {
|
||||
const config = { bash: { "*": "ask" as const, "npm *": "allow" as const, "git *": "allow" as const } }
|
||||
const rules = PermissionNext.fromConfig(config)
|
||||
const result = PermissionNext.toConfig(rules)
|
||||
expect(result).toEqual(config)
|
||||
})
|
||||
@@ -120,7 +120,6 @@ export const dict = {
|
||||
"ui.permission.deny": "رفض",
|
||||
"ui.permission.allowAlways": "السماح دائمًا",
|
||||
"ui.permission.allowOnce": "السماح مرة واحدة",
|
||||
"ui.permission.sessionHint": '"السماح دائمًا" ينطبق على هذه الجلسة فقط. استخدم الإعدادات للأذونات العامة.',
|
||||
|
||||
"ui.message.expand": "توسيع الرسالة",
|
||||
"ui.message.collapse": "طي الرسالة",
|
||||
|
||||
@@ -120,8 +120,6 @@ export const dict = {
|
||||
"ui.permission.deny": "Negar",
|
||||
"ui.permission.allowAlways": "Permitir sempre",
|
||||
"ui.permission.allowOnce": "Permitir uma vez",
|
||||
"ui.permission.sessionHint":
|
||||
'"Permitir sempre" aplica-se apenas a esta sessão. Use as configurações para permissões globais.',
|
||||
|
||||
"ui.message.expand": "Expandir mensagem",
|
||||
"ui.message.collapse": "Recolher mensagem",
|
||||
|
||||
@@ -124,8 +124,6 @@ export const dict = {
|
||||
"ui.permission.deny": "Zabrani",
|
||||
"ui.permission.allowAlways": "Uvijek dozvoli",
|
||||
"ui.permission.allowOnce": "Dozvoli jednom",
|
||||
"ui.permission.sessionHint":
|
||||
'"Uvijek dozvoli" primjenjuje se samo na ovu sesiju. Koristite postavke za globalne dozvole.',
|
||||
|
||||
"ui.message.expand": "Proširi poruku",
|
||||
"ui.message.collapse": "Sažmi poruku",
|
||||
|
||||
@@ -119,8 +119,6 @@ export const dict = {
|
||||
"ui.permission.deny": "Afvis",
|
||||
"ui.permission.allowAlways": "Tillad altid",
|
||||
"ui.permission.allowOnce": "Tillad én gang",
|
||||
"ui.permission.sessionHint":
|
||||
'"Tillad altid" gælder kun for denne session. Brug indstillinger til globale tilladelser.',
|
||||
|
||||
"ui.message.expand": "Udvid besked",
|
||||
"ui.message.collapse": "Skjul besked",
|
||||
|
||||
@@ -125,8 +125,6 @@ export const dict = {
|
||||
"ui.permission.deny": "Verweigern",
|
||||
"ui.permission.allowAlways": "Immer erlauben",
|
||||
"ui.permission.allowOnce": "Einmal erlauben",
|
||||
"ui.permission.sessionHint":
|
||||
'"Immer erlauben" gilt nur für diese Sitzung. Globale Berechtigungen in den Einstellungen ändern.',
|
||||
|
||||
"ui.message.expand": "Nachricht erweitern",
|
||||
"ui.message.collapse": "Nachricht reduzieren",
|
||||
|
||||
@@ -121,7 +121,6 @@ export const dict: Record<string, string> = {
|
||||
"ui.permission.deny": "Deny",
|
||||
"ui.permission.allowAlways": "Allow always",
|
||||
"ui.permission.allowOnce": "Allow once",
|
||||
"ui.permission.sessionHint": '"Allow always" applies to this session only. Use settings for global permissions.',
|
||||
|
||||
"ui.message.expand": "Expand message",
|
||||
"ui.message.collapse": "Collapse message",
|
||||
|
||||
@@ -120,8 +120,6 @@ export const dict = {
|
||||
"ui.permission.deny": "Denegar",
|
||||
"ui.permission.allowAlways": "Permitir siempre",
|
||||
"ui.permission.allowOnce": "Permitir una vez",
|
||||
"ui.permission.sessionHint":
|
||||
'"Permitir siempre" se aplica solo a esta sesión. Usa la configuración para permisos globales.',
|
||||
|
||||
"ui.message.expand": "Expandir mensaje",
|
||||
"ui.message.collapse": "Colapsar mensaje",
|
||||
|
||||
@@ -120,8 +120,6 @@ export const dict = {
|
||||
"ui.permission.deny": "Refuser",
|
||||
"ui.permission.allowAlways": "Toujours autoriser",
|
||||
"ui.permission.allowOnce": "Autoriser une fois",
|
||||
"ui.permission.sessionHint":
|
||||
'"Toujours autoriser" s\'applique uniquement à cette session. Utilisez les paramètres pour les autorisations globales.',
|
||||
|
||||
"ui.message.expand": "Développer le message",
|
||||
"ui.message.collapse": "Réduire le message",
|
||||
|
||||
@@ -119,8 +119,6 @@ export const dict = {
|
||||
"ui.permission.deny": "拒否",
|
||||
"ui.permission.allowAlways": "常に許可",
|
||||
"ui.permission.allowOnce": "今回のみ許可",
|
||||
"ui.permission.sessionHint":
|
||||
"「常に許可」はこのセッションにのみ適用されます。グローバルな権限は設定で変更してください。",
|
||||
|
||||
"ui.message.expand": "メッセージを展開",
|
||||
"ui.message.collapse": "メッセージを折りたたむ",
|
||||
|
||||
@@ -120,7 +120,6 @@ export const dict = {
|
||||
"ui.permission.deny": "거부",
|
||||
"ui.permission.allowAlways": "항상 허용",
|
||||
"ui.permission.allowOnce": "한 번만 허용",
|
||||
"ui.permission.sessionHint": '"항상 허용"은 현재 세션에만 적용됩니다. 전역 권한은 설정에서 변경하세요.',
|
||||
|
||||
"ui.message.expand": "메시지 펼치기",
|
||||
"ui.message.collapse": "메시지 접기",
|
||||
|
||||
@@ -123,8 +123,6 @@ export const dict: Record<Keys, string> = {
|
||||
"ui.permission.deny": "Avslå",
|
||||
"ui.permission.allowAlways": "Tillat alltid",
|
||||
"ui.permission.allowOnce": "Tillat én gang",
|
||||
"ui.permission.sessionHint":
|
||||
'"Tillat alltid" gjelder bare for denne økten. Bruk innstillinger for globale tillatelser.',
|
||||
|
||||
"ui.message.expand": "Utvid melding",
|
||||
"ui.message.collapse": "Skjul melding",
|
||||
|
||||
@@ -119,8 +119,6 @@ export const dict = {
|
||||
"ui.permission.deny": "Odmów",
|
||||
"ui.permission.allowAlways": "Zezwalaj zawsze",
|
||||
"ui.permission.allowOnce": "Zezwól raz",
|
||||
"ui.permission.sessionHint":
|
||||
'"Zezwalaj zawsze" dotyczy tylko tej sesji. Użyj ustawień, aby zmienić globalne uprawnienia.',
|
||||
|
||||
"ui.message.expand": "Rozwiń wiadomość",
|
||||
"ui.message.collapse": "Zwiń wiadomość",
|
||||
|
||||
@@ -119,8 +119,6 @@ export const dict = {
|
||||
"ui.permission.deny": "Запретить",
|
||||
"ui.permission.allowAlways": "Разрешить всегда",
|
||||
"ui.permission.allowOnce": "Разрешить один раз",
|
||||
"ui.permission.sessionHint":
|
||||
"«Разрешить всегда» применяется только к текущей сессии. Для глобальных разрешений используйте настройки.",
|
||||
|
||||
"ui.message.expand": "Развернуть сообщение",
|
||||
"ui.message.collapse": "Свернуть сообщение",
|
||||
|
||||
@@ -121,7 +121,6 @@ export const dict = {
|
||||
"ui.permission.deny": "ปฏิเสธ",
|
||||
"ui.permission.allowAlways": "อนุญาตเสมอ",
|
||||
"ui.permission.allowOnce": "อนุญาตครั้งเดียว",
|
||||
"ui.permission.sessionHint": '"อนุญาตเสมอ" ใช้ได้เฉพาะในเซสชันนี้เท่านั้น ใช้การตั้งค่าสำหรับสิทธิ์ส่วนกลาง',
|
||||
|
||||
"ui.message.expand": "ขยายข้อความ",
|
||||
"ui.message.collapse": "ย่อข้อความ",
|
||||
|
||||
@@ -124,7 +124,6 @@ export const dict = {
|
||||
"ui.permission.deny": "拒绝",
|
||||
"ui.permission.allowAlways": "始终允许",
|
||||
"ui.permission.allowOnce": "允许一次",
|
||||
"ui.permission.sessionHint": '"始终允许" 仅适用于本次会话。如需全局权限设置,请使用设置。',
|
||||
|
||||
"ui.message.expand": "展开消息",
|
||||
"ui.message.collapse": "收起消息",
|
||||
|
||||
@@ -124,7 +124,6 @@ export const dict = {
|
||||
"ui.permission.deny": "拒絕",
|
||||
"ui.permission.allowAlways": "永遠允許",
|
||||
"ui.permission.allowOnce": "允許一次",
|
||||
"ui.permission.sessionHint": '"永遠允許" 僅適用於此工作階段。如需全域權限設定,請使用設定。',
|
||||
|
||||
"ui.message.expand": "展開訊息",
|
||||
"ui.message.collapse": "收合訊息",
|
||||
|
||||
Reference in New Issue
Block a user