fix(shell): keep shell config consistent

Treat shell access as one logical toggle during agent creation and apply bash compatibility rules before explicit per-shell overrides. This avoids disabling the active Windows shell unexpectedly and keeps pwsh and powershell overrides deterministic.
This commit is contained in:
LukeParkerDev 2026-04-03 15:08:30 +10:00
commit 32ec3666b7
3 changed files with 46 additions and 50 deletions

View file

@ -9,24 +9,12 @@ import fs from "fs/promises"
import { Filesystem } from "../../util/filesystem"
import matter from "gray-matter"
import { Instance } from "../../project/instance"
import { ShellTool } from "../../tool/shell/id"
import { EOL } from "os"
import type { Argv } from "yargs"
type AgentMode = "all" | "primary" | "subagent"
const AVAILABLE_TOOLS = [
...ShellTool.ids,
"read",
"write",
"edit",
"list",
"glob",
"grep",
"webfetch",
"task",
"todowrite",
]
const AVAILABLE_TOOLS = ["bash", "read", "write", "edit", "list", "glob", "grep", "webfetch", "task", "todowrite"]
const AgentCreateCommand = cmd({
command: "create",

View file

@ -276,46 +276,30 @@ export namespace Permission {
return pattern
}
function pushRules(ruleset: Ruleset, permission: string, value: Config.PermissionRule) {
if (typeof value === "string") {
ruleset.push({ permission, action: value, pattern: "*" })
return
}
ruleset.push(
...Object.entries(value).map(([pattern, action]) => ({ permission, pattern: expand(pattern), action })),
)
}
export function fromConfig(permission: Config.Permission) {
const ruleset: Ruleset = []
const bash = permission["bash"]
if (bash !== undefined) {
pushRules(ruleset, "bash", bash)
pushRules(ruleset, "pwsh", bash)
pushRules(ruleset, "powershell", bash)
}
for (const [key, value] of Object.entries(permission)) {
if (key === "bash") {
if (typeof value === "string") {
ruleset.push({ permission: "bash", action: value, pattern: "*" })
ruleset.push({ permission: "pwsh", action: value, pattern: "*" })
ruleset.push({ permission: "powershell", action: value, pattern: "*" })
} else {
ruleset.push(
...Object.entries(value).map(([pattern, action]) => ({
permission: "bash",
pattern: expand(pattern),
action,
})),
)
ruleset.push(
...Object.entries(value).map(([pattern, action]) => ({
permission: "pwsh",
pattern: expand(pattern),
action,
})),
)
ruleset.push(
...Object.entries(value).map(([pattern, action]) => ({
permission: "powershell",
pattern: expand(pattern),
action,
})),
)
}
continue
}
if (typeof value === "string") {
ruleset.push({ permission: key, action: value, pattern: "*" })
continue
}
ruleset.push(
...Object.entries(value).map(([pattern, action]) => ({ permission: key, pattern: expand(pattern), action })),
)
if (key === "bash") continue
pushRules(ruleset, key, value)
}
return ruleset
}

View file

@ -71,6 +71,30 @@ test("fromConfig - mixed string and object values", () => {
])
})
test("fromConfig - explicit pwsh overrides bash regardless of key order", () => {
const result = Permission.fromConfig({
pwsh: "deny",
bash: "allow",
})
expect(result).toEqual([
{ permission: "bash", pattern: "*", action: "allow" },
{ permission: "pwsh", pattern: "*", action: "allow" },
{ permission: "powershell", pattern: "*", action: "allow" },
{ permission: "pwsh", pattern: "*", action: "deny" },
])
expect(Permission.evaluate("pwsh", "ls", result).action).toBe("deny")
expect(Permission.evaluate("bash", "ls", result).action).toBe("allow")
})
test("fromConfig - explicit powershell pattern overrides bash pattern regardless of key order", () => {
const result = Permission.fromConfig({
powershell: { "rm *": "deny" },
bash: { "*": "allow", "rm *": "ask" },
})
expect(Permission.evaluate("powershell", "rm foo", result).action).toBe("deny")
expect(Permission.evaluate("pwsh", "rm foo", result).action).toBe("ask")
})
test("fromConfig - empty object", () => {
const result = Permission.fromConfig({})
expect(result).toEqual([])